Repository: retoaccess1/haveno-reto Branch: master Commit: db6c0f7238d9 Files: 1890 Total size: 13.8 MB Directory structure: gitextract_ersdwbkw/ ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ └── workflows/ │ ├── build.yml │ ├── codacy-code-reporter.yml │ ├── codeql-analysis.yml │ └── label.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── apitest/ │ ├── scripts/ │ │ ├── editf2faccountform.py │ │ ├── get-haveno-pid.sh │ │ ├── limit-order-simulation.sh │ │ ├── mainnet-test.sh │ │ ├── rolling-offer-simulation.sh │ │ ├── trade-simulation-env.sh │ │ ├── trade-simulation-utils.sh │ │ ├── trade-simulation.sh │ │ ├── trade-xmr-simulation.sh │ │ └── version-parser.bash │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── haveno/ │ │ │ └── apitest/ │ │ │ ├── ApiTestMain.java │ │ │ ├── Scaffold.java │ │ │ ├── SetupTask.java │ │ │ ├── SmokeTestBashCommand.java │ │ │ ├── SmokeTestBitcoind.java │ │ │ ├── config/ │ │ │ │ ├── ApiTestConfig.java │ │ │ │ ├── ApiTestRateMeterInterceptorConfig.java │ │ │ │ └── HavenoAppConfig.java │ │ │ └── linux/ │ │ │ ├── AbstractLinuxProcess.java │ │ │ ├── BashCommand.java │ │ │ ├── BitcoinCli.java │ │ │ ├── BitcoinDaemon.java │ │ │ ├── HavenoProcess.java │ │ │ ├── LinuxProcess.java │ │ │ ├── SystemCommandExecutor.java │ │ │ └── ThreadedStreamHandler.java │ │ └── resources/ │ │ ├── apitest.properties │ │ ├── blocknotify │ │ ├── haveno.properties │ │ └── logback.xml │ └── test/ │ ├── java/ │ │ └── haveno/ │ │ └── apitest/ │ │ ├── ApiTestCase.java │ │ ├── method/ │ │ │ ├── BitcoinCliHelper.java │ │ │ ├── CallRateMeteringInterceptorTest.java │ │ │ ├── GetMethodHelpTest.java │ │ │ ├── GetVersionTest.java │ │ │ ├── MethodTest.java │ │ │ ├── RegisterDisputeAgentsTest.java │ │ │ ├── offer/ │ │ │ │ ├── AbstractOfferTest.java │ │ │ │ ├── CancelOfferTest.java │ │ │ │ ├── CreateOfferUsingFixedPriceTest.java │ │ │ │ ├── CreateOfferUsingMarketPriceMarginTest.java │ │ │ │ ├── CreateXMROffersTest.java │ │ │ │ └── ValidateCreateOfferTest.java │ │ │ ├── payment/ │ │ │ │ ├── AbstractPaymentAccountTest.java │ │ │ │ ├── CreatePaymentAccountTest.java │ │ │ │ └── GetPaymentMethodsTest.java │ │ │ ├── trade/ │ │ │ │ ├── AbstractTradeTest.java │ │ │ │ ├── ExpectedProtocolStatus.java │ │ │ │ ├── TakeBuyBTCOfferTest.java │ │ │ │ ├── TakeBuyBTCOfferWithNationalBankAcctTest.java │ │ │ │ ├── TakeBuyXMROfferTest.java │ │ │ │ ├── TakeSellBTCOfferTest.java │ │ │ │ └── TakeSellXMROfferTest.java │ │ │ └── wallet/ │ │ │ ├── BtcWalletTest.java │ │ │ ├── WalletProtectionTest.java │ │ │ └── WalletTestUtil.java │ │ └── scenario/ │ │ ├── LongRunningOfferDeactivationTest.java │ │ ├── LongRunningTradesTest.java │ │ ├── OfferTest.java │ │ ├── PaymentAccountTest.java │ │ ├── ScriptedBotTest.java │ │ ├── StartupTest.java │ │ ├── TradeTest.java │ │ ├── WalletTest.java │ │ └── bot/ │ │ ├── AbstractBotTest.java │ │ ├── Bot.java │ │ ├── BotClient.java │ │ ├── BotPaymentAccountGenerator.java │ │ ├── InvalidRandomOfferException.java │ │ ├── PaymentAccountNotFoundException.java │ │ ├── RandomOffer.java │ │ ├── RobotBob.java │ │ ├── protocol/ │ │ │ ├── BotProtocol.java │ │ │ ├── MakerBotProtocol.java │ │ │ ├── ProtocolStep.java │ │ │ └── TakerBotProtocol.java │ │ ├── script/ │ │ │ ├── BashScriptGenerator.java │ │ │ ├── BotScript.java │ │ │ └── BotScriptGenerator.java │ │ └── shutdown/ │ │ ├── ManualBotShutdownException.java │ │ └── ManualShutdown.java │ └── resources/ │ └── logback.xml ├── assets/ │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── haveno/ │ │ │ └── asset/ │ │ │ ├── AbstractAsset.java │ │ │ ├── AddressValidationResult.java │ │ │ ├── AddressValidator.java │ │ │ ├── Asset.java │ │ │ ├── AssetRegistry.java │ │ │ ├── Base58AddressValidator.java │ │ │ ├── BitcoinAddressValidator.java │ │ │ ├── BitcoinCashAddressValidator.java │ │ │ ├── CardanoAddressValidator.java │ │ │ ├── Coin.java │ │ │ ├── CryptoAccountDisclaimer.java │ │ │ ├── CryptoNoteAddressValidator.java │ │ │ ├── CryptoNoteUtils.java │ │ │ ├── Erc20Token.java │ │ │ ├── EtherAddressValidator.java │ │ │ ├── GrinAddressValidator.java │ │ │ ├── I18n.java │ │ │ ├── LiquidBitcoinAddressValidator.java │ │ │ ├── NetworkParametersAdapter.java │ │ │ ├── PrintTool.java │ │ │ ├── RegexAddressValidator.java │ │ │ ├── RippleAddressValidator.java │ │ │ ├── SolanaAddressValidator.java │ │ │ ├── Token.java │ │ │ ├── Trc20Token.java │ │ │ ├── TronAddressValidator.java │ │ │ ├── coins/ │ │ │ │ ├── Bitcoin.java │ │ │ │ ├── BitcoinCash.java │ │ │ │ ├── Cardano.java │ │ │ │ ├── Dogecoin.java │ │ │ │ ├── Ether.java │ │ │ │ ├── Litecoin.java │ │ │ │ ├── Monero.java │ │ │ │ ├── Ripple.java │ │ │ │ ├── Solana.java │ │ │ │ └── Tron.java │ │ │ ├── package-info.java │ │ │ └── tokens/ │ │ │ ├── AugmintEuro.java │ │ │ ├── DaiStablecoinERC20.java │ │ │ ├── EtherStone.java │ │ │ ├── TetherUSDERC20.java │ │ │ ├── TetherUSDTRC20.java │ │ │ ├── TrueUSD.java │ │ │ ├── USDCoinERC20.java │ │ │ └── VectorspaceAI.java │ │ └── resources/ │ │ ├── META-INF/ │ │ │ └── services/ │ │ │ └── haveno.asset.Asset │ │ └── i18n/ │ │ └── displayStrings-assets.properties │ └── test/ │ └── java/ │ └── haveno/ │ └── asset/ │ ├── AbstractAssetTest.java │ └── coins/ │ ├── BitcoinCashTest.java │ ├── BitcoinTest.java │ ├── CardanoTest.java │ ├── DogecoinTest.java │ ├── LitecoinTest.java │ ├── MoneroTest.java │ ├── RippleTest.java │ ├── SolanaTest.java │ ├── TetherUSDERC20Test.java │ ├── TetherUSDTRC20Test.java │ └── TronTest.java ├── build.gradle ├── cli/ │ ├── package/ │ │ └── create-cli-dist.sh │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── haveno/ │ │ │ └── cli/ │ │ │ ├── CliMain.java │ │ │ ├── ColumnHeaderConstants.java │ │ │ ├── CryptoCurrencyUtil.java │ │ │ ├── CurrencyFormat.java │ │ │ ├── DirectionFormat.java │ │ │ ├── GrpcClient.java │ │ │ ├── GrpcStubs.java │ │ │ ├── Method.java │ │ │ ├── PasswordCallCredentials.java │ │ │ ├── TransactionFormat.java │ │ │ ├── opts/ │ │ │ │ ├── AbstractMethodOptionParser.java │ │ │ │ ├── ArgumentList.java │ │ │ │ ├── CancelOfferOptionParser.java │ │ │ │ ├── CreateCryptoCurrencyPaymentAcctOptionParser.java │ │ │ │ ├── CreateOfferOptionParser.java │ │ │ │ ├── CreatePaymentAcctOptionParser.java │ │ │ │ ├── GetAddressBalanceOptionParser.java │ │ │ │ ├── GetBTCMarketPriceOptionParser.java │ │ │ │ ├── GetBalanceOptionParser.java │ │ │ │ ├── GetOfferOptionParser.java │ │ │ │ ├── GetOffersOptionParser.java │ │ │ │ ├── GetPaymentAcctFormOptionParser.java │ │ │ │ ├── GetTradeOptionParser.java │ │ │ │ ├── GetTradesOptionParser.java │ │ │ │ ├── GetTransactionOptionParser.java │ │ │ │ ├── MethodOpts.java │ │ │ │ ├── OfferIdOptionParser.java │ │ │ │ ├── OptLabel.java │ │ │ │ ├── RegisterDisputeAgentOptionParser.java │ │ │ │ ├── RemoveWalletPasswordOptionParser.java │ │ │ │ ├── SendBtcOptionParser.java │ │ │ │ ├── SetTxFeeRateOptionParser.java │ │ │ │ ├── SetWalletPasswordOptionParser.java │ │ │ │ ├── SimpleMethodOptionParser.java │ │ │ │ ├── TakeOfferOptionParser.java │ │ │ │ ├── UnlockWalletOptionParser.java │ │ │ │ └── WithdrawFundsOptionParser.java │ │ │ ├── request/ │ │ │ │ ├── OffersServiceRequest.java │ │ │ │ ├── PaymentAccountsServiceRequest.java │ │ │ │ ├── TradesServiceRequest.java │ │ │ │ └── WalletsServiceRequest.java │ │ │ └── table/ │ │ │ ├── Table.java │ │ │ ├── builder/ │ │ │ │ ├── AbstractTableBuilder.java │ │ │ │ ├── AbstractTradeListBuilder.java │ │ │ │ ├── AddressBalanceTableBuilder.java │ │ │ │ ├── BtcBalanceTableBuilder.java │ │ │ │ ├── ClosedTradeTableBuilder.java │ │ │ │ ├── FailedTradeTableBuilder.java │ │ │ │ ├── OfferTableBuilder.java │ │ │ │ ├── OpenTradeTableBuilder.java │ │ │ │ ├── PaymentAccountTableBuilder.java │ │ │ │ ├── TableBuilder.java │ │ │ │ ├── TableBuilderConstants.java │ │ │ │ ├── TableType.java │ │ │ │ ├── TradeDetailTableBuilder.java │ │ │ │ ├── TradeTableColumnSupplier.java │ │ │ │ └── TransactionTableBuilder.java │ │ │ └── column/ │ │ │ ├── AbstractColumn.java │ │ │ ├── BooleanColumn.java │ │ │ ├── BtcColumn.java │ │ │ ├── Column.java │ │ │ ├── CryptoVolumeColumn.java │ │ │ ├── DoubleColumn.java │ │ │ ├── IntegerColumn.java │ │ │ ├── Iso8601DateTimeColumn.java │ │ │ ├── LongColumn.java │ │ │ ├── MixedTradeFeeColumn.java │ │ │ ├── NumberColumn.java │ │ │ ├── SatoshiColumn.java │ │ │ ├── StringColumn.java │ │ │ └── ZippedStringColumns.java │ │ └── resources/ │ │ └── logback.xml │ └── test/ │ └── java/ │ └── haveno/ │ └── cli/ │ ├── AbstractCliTest.java │ ├── CreateOfferSmokeTest.java │ ├── EditXmrOffersSmokeTest.java │ ├── GetOffersSmokeTest.java │ ├── GetTradesSmokeTest.java │ ├── opts/ │ │ └── OptionParsersTest.java │ └── table/ │ ├── AddressCliOutputDiffTest.java │ ├── GetBalanceCliOutputDiffTest.java │ ├── GetOffersCliOutputDiffTest.java │ ├── GetTradeCliOutputDiffTest.java │ ├── GetTransactionCliOutputDiffTest.java │ └── PaymentAccountsCliOutputDiffTest.java ├── common/ │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── haveno/ │ │ └── common/ │ │ ├── ClockWatcher.java │ │ ├── Envelope.java │ │ ├── FrameRateTimer.java │ │ ├── HavenoException.java │ │ ├── MasterTimer.java │ │ ├── Payload.java │ │ ├── Proto.java │ │ ├── ThreadUtils.java │ │ ├── Timer.java │ │ ├── UserThread.java │ │ ├── app/ │ │ │ ├── AppModule.java │ │ │ ├── AsciiLogo.java │ │ │ ├── Capabilities.java │ │ │ ├── Capability.java │ │ │ ├── DevEnv.java │ │ │ ├── HasCapabilities.java │ │ │ ├── Log.java │ │ │ ├── LogHighlighter.java │ │ │ └── Version.java │ │ ├── config/ │ │ │ ├── BaseCurrencyNetwork.java │ │ │ ├── CompositeOptionSet.java │ │ │ ├── Config.java │ │ │ ├── ConfigException.java │ │ │ ├── ConfigFileEditor.java │ │ │ ├── ConfigFileOption.java │ │ │ ├── ConfigFileReader.java │ │ │ ├── EnumValueConverter.java │ │ │ └── HavenoHelpFormatter.java │ │ ├── consensus/ │ │ │ └── UsedForTradeContractJson.java │ │ ├── crypto/ │ │ │ ├── CryptoException.java │ │ │ ├── CryptoUtils.java │ │ │ ├── Encryption.java │ │ │ ├── Hash.java │ │ │ ├── IncorrectPasswordException.java │ │ │ ├── KeyConversionException.java │ │ │ ├── KeyRing.java │ │ │ ├── KeyStorage.java │ │ │ ├── PubKeyRing.java │ │ │ ├── PubKeyRingProvider.java │ │ │ ├── ScryptUtil.java │ │ │ ├── SealedAndSigned.java │ │ │ └── Sig.java │ │ ├── file/ │ │ │ ├── CorruptedStorageFileHandler.java │ │ │ ├── FileUtil.java │ │ │ ├── JsonFileManager.java │ │ │ └── ResourceNotFoundException.java │ │ ├── handlers/ │ │ │ ├── ErrorMessageHandler.java │ │ │ ├── ExceptionHandler.java │ │ │ ├── FaultHandler.java │ │ │ └── ResultHandler.java │ │ ├── persistence/ │ │ │ └── PersistenceManager.java │ │ ├── proto/ │ │ │ ├── ProtoResolver.java │ │ │ ├── ProtoUtil.java │ │ │ ├── ProtobufferException.java │ │ │ ├── ProtobufferRuntimeException.java │ │ │ ├── network/ │ │ │ │ ├── GetDataResponsePriority.java │ │ │ │ ├── NetworkEnvelope.java │ │ │ │ ├── NetworkPayload.java │ │ │ │ └── NetworkProtoResolver.java │ │ │ └── persistable/ │ │ │ ├── NavigationPath.java │ │ │ ├── PersistableEnvelope.java │ │ │ ├── PersistableList.java │ │ │ ├── PersistableListAsObservable.java │ │ │ ├── PersistablePayload.java │ │ │ ├── PersistedDataHost.java │ │ │ └── PersistenceProtoResolver.java │ │ ├── reactfx/ │ │ │ ├── FxTimer.java │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ └── Timer.java │ │ ├── setup/ │ │ │ ├── CommonSetup.java │ │ │ ├── GracefulShutDownHandler.java │ │ │ └── UncaughtExceptionHandler.java │ │ ├── taskrunner/ │ │ │ ├── InterceptTaskException.java │ │ │ ├── Model.java │ │ │ ├── Task.java │ │ │ └── TaskRunner.java │ │ └── util/ │ │ ├── Base64.java │ │ ├── CollectionUtils.java │ │ ├── CompletableFutureUtils.java │ │ ├── DesktopUtil.java │ │ ├── DoubleSummaryStatisticsWithStdDev.java │ │ ├── ExtraDataMapValidator.java │ │ ├── GcUtil.java │ │ ├── Hex.java │ │ ├── InvalidVersionException.java │ │ ├── JsonExclude.java │ │ ├── MathUtils.java │ │ ├── PermutationUtil.java │ │ ├── Preconditions.java │ │ ├── Profiler.java │ │ ├── ReflectionUtils.java │ │ ├── RestartUtil.java │ │ ├── SingleThreadExecutorUtils.java │ │ ├── Tuple2.java │ │ ├── Tuple3.java │ │ ├── Tuple4.java │ │ ├── Tuple5.java │ │ ├── Utilities.java │ │ └── ZipUtils.java │ └── test/ │ └── java/ │ └── haveno/ │ └── common/ │ ├── app/ │ │ ├── CapabilitiesTest.java │ │ └── VersionTest.java │ ├── config/ │ │ ├── ConfigFileEditorTests.java │ │ ├── ConfigFileOptionTests.java │ │ ├── ConfigFileReaderTests.java │ │ └── ConfigTests.java │ └── util/ │ ├── MathUtilsTest.java │ ├── PermutationTest.java │ ├── PreconditionsTests.java │ └── UtilitiesTest.java ├── config/ │ └── checkstyle/ │ └── checkstyle.xml ├── core/ │ ├── .tx/ │ │ └── config │ ├── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── haveno/ │ │ │ │ └── core/ │ │ │ │ ├── account/ │ │ │ │ │ ├── sign/ │ │ │ │ │ │ ├── SignedWitness.java │ │ │ │ │ │ ├── SignedWitnessService.java │ │ │ │ │ │ ├── SignedWitnessStorageService.java │ │ │ │ │ │ └── SignedWitnessStore.java │ │ │ │ │ └── witness/ │ │ │ │ │ ├── AccountAgeWitness.java │ │ │ │ │ ├── AccountAgeWitnessService.java │ │ │ │ │ ├── AccountAgeWitnessStorageService.java │ │ │ │ │ ├── AccountAgeWitnessStore.java │ │ │ │ │ └── AccountAgeWitnessUtils.java │ │ │ │ ├── alert/ │ │ │ │ │ ├── Alert.java │ │ │ │ │ ├── AlertManager.java │ │ │ │ │ ├── AlertModule.java │ │ │ │ │ ├── PrivateNotificationManager.java │ │ │ │ │ ├── PrivateNotificationMessage.java │ │ │ │ │ └── PrivateNotificationPayload.java │ │ │ │ ├── api/ │ │ │ │ │ ├── AccountServiceListener.java │ │ │ │ │ ├── CoreAccountService.java │ │ │ │ │ ├── CoreApi.java │ │ │ │ │ ├── CoreContext.java │ │ │ │ │ ├── CoreDisputeAgentsService.java │ │ │ │ │ ├── CoreDisputesService.java │ │ │ │ │ ├── CoreHelpService.java │ │ │ │ │ ├── CoreNotificationService.java │ │ │ │ │ ├── CoreOffersService.java │ │ │ │ │ ├── CorePaymentAccountsService.java │ │ │ │ │ ├── CorePriceService.java │ │ │ │ │ ├── CoreTradesService.java │ │ │ │ │ ├── CoreWalletsService.java │ │ │ │ │ ├── NotificationListener.java │ │ │ │ │ ├── XmrConnectionService.java │ │ │ │ │ ├── XmrKeyImageListener.java │ │ │ │ │ ├── XmrKeyImagePoller.java │ │ │ │ │ ├── XmrLocalNode.java │ │ │ │ │ ├── XmrLocalNodeListener.java │ │ │ │ │ └── model/ │ │ │ │ │ ├── AddressBalanceInfo.java │ │ │ │ │ ├── BalancesInfo.java │ │ │ │ │ ├── BtcBalanceInfo.java │ │ │ │ │ ├── ContractInfo.java │ │ │ │ │ ├── EncryptedConnection.java │ │ │ │ │ ├── MarketDepthInfo.java │ │ │ │ │ ├── MarketPriceInfo.java │ │ │ │ │ ├── OfferInfo.java │ │ │ │ │ ├── PaymentAccountForm.java │ │ │ │ │ ├── PaymentAccountFormField.java │ │ │ │ │ ├── TradeInfo.java │ │ │ │ │ ├── XmrBalanceInfo.java │ │ │ │ │ ├── XmrDestination.java │ │ │ │ │ ├── XmrIncomingTransfer.java │ │ │ │ │ ├── XmrOutgoingTransfer.java │ │ │ │ │ ├── XmrTx.java │ │ │ │ │ └── builder/ │ │ │ │ │ ├── OfferInfoBuilder.java │ │ │ │ │ └── TradeInfoV1Builder.java │ │ │ │ ├── app/ │ │ │ │ │ ├── AppStartupState.java │ │ │ │ │ ├── AvoidStandbyModeService.java │ │ │ │ │ ├── ConsoleInput.java │ │ │ │ │ ├── ConsoleInputReadTask.java │ │ │ │ │ ├── CoreModule.java │ │ │ │ │ ├── DomainInitialisation.java │ │ │ │ │ ├── HavenoExecutable.java │ │ │ │ │ ├── HavenoHeadlessApp.java │ │ │ │ │ ├── HavenoHeadlessAppMain.java │ │ │ │ │ ├── HavenoSetup.java │ │ │ │ │ ├── HeadlessApp.java │ │ │ │ │ ├── P2PNetworkSetup.java │ │ │ │ │ ├── TorSetup.java │ │ │ │ │ ├── WalletAppSetup.java │ │ │ │ │ └── misc/ │ │ │ │ │ ├── AppSetup.java │ │ │ │ │ ├── AppSetupWithP2P.java │ │ │ │ │ ├── ExecutableForAppWithP2p.java │ │ │ │ │ └── ModuleForAppWithP2p.java │ │ │ │ ├── exceptions/ │ │ │ │ │ └── TradePriceOutOfToleranceException.java │ │ │ │ ├── filter/ │ │ │ │ │ ├── Filter.java │ │ │ │ │ ├── FilterManager.java │ │ │ │ │ ├── FilterModule.java │ │ │ │ │ └── PaymentAccountFilter.java │ │ │ │ ├── locale/ │ │ │ │ │ ├── BankUtil.java │ │ │ │ │ ├── Country.java │ │ │ │ │ ├── CountryUtil.java │ │ │ │ │ ├── CryptoCurrency.java │ │ │ │ │ ├── CurrencyTuple.java │ │ │ │ │ ├── CurrencyUtil.java │ │ │ │ │ ├── GlobalSettings.java │ │ │ │ │ ├── LanguageUtil.java │ │ │ │ │ ├── LocaleUtil.java │ │ │ │ │ ├── Region.java │ │ │ │ │ ├── Res.java │ │ │ │ │ ├── TradeCurrency.java │ │ │ │ │ └── TraditionalCurrency.java │ │ │ │ ├── monetary/ │ │ │ │ │ ├── CryptoExchangeRate.java │ │ │ │ │ ├── CryptoMoney.java │ │ │ │ │ ├── MonetaryWrapper.java │ │ │ │ │ ├── Price.java │ │ │ │ │ ├── TraditionalExchangeRate.java │ │ │ │ │ ├── TraditionalMoney.java │ │ │ │ │ └── Volume.java │ │ │ │ ├── network/ │ │ │ │ │ ├── CoreBanFilter.java │ │ │ │ │ ├── MessageState.java │ │ │ │ │ └── p2p/ │ │ │ │ │ ├── inventory/ │ │ │ │ │ │ ├── GetInventoryRequestHandler.java │ │ │ │ │ │ ├── GetInventoryRequestManager.java │ │ │ │ │ │ ├── GetInventoryRequester.java │ │ │ │ │ │ ├── messages/ │ │ │ │ │ │ │ ├── GetInventoryRequest.java │ │ │ │ │ │ │ └── GetInventoryResponse.java │ │ │ │ │ │ └── model/ │ │ │ │ │ │ ├── Average.java │ │ │ │ │ │ ├── DeviationByIntegerDiff.java │ │ │ │ │ │ ├── DeviationByPercentage.java │ │ │ │ │ │ ├── DeviationOfHashes.java │ │ │ │ │ │ ├── DeviationSeverity.java │ │ │ │ │ │ ├── DeviationType.java │ │ │ │ │ │ ├── InventoryItem.java │ │ │ │ │ │ └── RequestInfo.java │ │ │ │ │ └── seed/ │ │ │ │ │ └── DefaultSeedNodeRepository.java │ │ │ │ ├── notifications/ │ │ │ │ │ ├── MobileMessage.java │ │ │ │ │ ├── MobileMessageEncryption.java │ │ │ │ │ ├── MobileMessageType.java │ │ │ │ │ ├── MobileModel.java │ │ │ │ │ ├── MobileNotificationService.java │ │ │ │ │ ├── MobileNotificationValidator.java │ │ │ │ │ └── alerts/ │ │ │ │ │ ├── DisputeMsgEvents.java │ │ │ │ │ ├── MyOfferTakenEvents.java │ │ │ │ │ ├── TradeEvents.java │ │ │ │ │ ├── market/ │ │ │ │ │ │ ├── MarketAlertFilter.java │ │ │ │ │ │ └── MarketAlerts.java │ │ │ │ │ └── price/ │ │ │ │ │ ├── PriceAlert.java │ │ │ │ │ └── PriceAlertFilter.java │ │ │ │ ├── offer/ │ │ │ │ │ ├── AvailabilityResult.java │ │ │ │ │ ├── CreateOfferService.java │ │ │ │ │ ├── MarketPriceNotAvailableException.java │ │ │ │ │ ├── Offer.java │ │ │ │ │ ├── OfferBookService.java │ │ │ │ │ ├── OfferDirection.java │ │ │ │ │ ├── OfferFilterService.java │ │ │ │ │ ├── OfferForJson.java │ │ │ │ │ ├── OfferModule.java │ │ │ │ │ ├── OfferPayload.java │ │ │ │ │ ├── OfferRestrictions.java │ │ │ │ │ ├── OfferUtil.java │ │ │ │ │ ├── OpenOffer.java │ │ │ │ │ ├── OpenOfferManager.java │ │ │ │ │ ├── SignedOffer.java │ │ │ │ │ ├── SignedOfferList.java │ │ │ │ │ ├── TriggerPriceService.java │ │ │ │ │ ├── availability/ │ │ │ │ │ │ ├── DisputeAgentSelection.java │ │ │ │ │ │ ├── OfferAvailabilityModel.java │ │ │ │ │ │ ├── OfferAvailabilityProtocol.java │ │ │ │ │ │ └── tasks/ │ │ │ │ │ │ ├── ProcessOfferAvailabilityResponse.java │ │ │ │ │ │ └── SendOfferAvailabilityRequest.java │ │ │ │ │ ├── messages/ │ │ │ │ │ │ ├── OfferAvailabilityRequest.java │ │ │ │ │ │ ├── OfferAvailabilityResponse.java │ │ │ │ │ │ ├── OfferMessage.java │ │ │ │ │ │ ├── SignOfferRequest.java │ │ │ │ │ │ └── SignOfferResponse.java │ │ │ │ │ ├── placeoffer/ │ │ │ │ │ │ ├── PlaceOfferModel.java │ │ │ │ │ │ ├── PlaceOfferProtocol.java │ │ │ │ │ │ └── tasks/ │ │ │ │ │ │ ├── MakerProcessSignOfferResponse.java │ │ │ │ │ │ ├── MakerReserveOfferFunds.java │ │ │ │ │ │ ├── MakerSendSignOfferRequest.java │ │ │ │ │ │ ├── MaybeAddToOfferBook.java │ │ │ │ │ │ └── ValidateOffer.java │ │ │ │ │ └── takeoffer/ │ │ │ │ │ └── TakeOfferModel.java │ │ │ │ ├── payment/ │ │ │ │ │ ├── AchTransferAccount.java │ │ │ │ │ ├── AdvancedCashAccount.java │ │ │ │ │ ├── AliPayAccount.java │ │ │ │ │ ├── AmazonGiftCardAccount.java │ │ │ │ │ ├── AssetAccount.java │ │ │ │ │ ├── AustraliaPayidAccount.java │ │ │ │ │ ├── BankAccount.java │ │ │ │ │ ├── BankNameRestrictedBankAccount.java │ │ │ │ │ ├── BizumAccount.java │ │ │ │ │ ├── CapitualAccount.java │ │ │ │ │ ├── CashAppAccount.java │ │ │ │ │ ├── CashAtAtmAccount.java │ │ │ │ │ ├── CashDepositAccount.java │ │ │ │ │ ├── CelPayAccount.java │ │ │ │ │ ├── ChargeBackRisk.java │ │ │ │ │ ├── ChaseQuickPayAccount.java │ │ │ │ │ ├── CountryBasedPaymentAccount.java │ │ │ │ │ ├── CryptoCurrencyAccount.java │ │ │ │ │ ├── DomesticWireTransferAccount.java │ │ │ │ │ ├── F2FAccount.java │ │ │ │ │ ├── FasterPaymentsAccount.java │ │ │ │ │ ├── HalCashAccount.java │ │ │ │ │ ├── IfscBasedAccount.java │ │ │ │ │ ├── ImpsAccount.java │ │ │ │ │ ├── InstantCryptoCurrencyAccount.java │ │ │ │ │ ├── InteracETransferAccount.java │ │ │ │ │ ├── JapanBankAccount.java │ │ │ │ │ ├── JapanBankData.java │ │ │ │ │ ├── MoneseAccount.java │ │ │ │ │ ├── MoneyBeamAccount.java │ │ │ │ │ ├── MoneyGramAccount.java │ │ │ │ │ ├── NationalBankAccount.java │ │ │ │ │ ├── NeftAccount.java │ │ │ │ │ ├── NequiAccount.java │ │ │ │ │ ├── OKPayAccount.java │ │ │ │ │ ├── PaxumAccount.java │ │ │ │ │ ├── PayByMailAccount.java │ │ │ │ │ ├── PayPalAccount.java │ │ │ │ │ ├── PaymentAccount.java │ │ │ │ │ ├── PaymentAccountFactory.java │ │ │ │ │ ├── PaymentAccountList.java │ │ │ │ │ ├── PaymentAccountTypeAdapter.java │ │ │ │ │ ├── PaymentAccountUtil.java │ │ │ │ │ ├── PaymentAccounts.java │ │ │ │ │ ├── PaysafeAccount.java │ │ │ │ │ ├── PayseraAccount.java │ │ │ │ │ ├── PaytmAccount.java │ │ │ │ │ ├── PerfectMoneyAccount.java │ │ │ │ │ ├── PixAccount.java │ │ │ │ │ ├── PopmoneyAccount.java │ │ │ │ │ ├── PromptPayAccount.java │ │ │ │ │ ├── ReceiptPredicates.java │ │ │ │ │ ├── ReceiptValidator.java │ │ │ │ │ ├── RevolutAccount.java │ │ │ │ │ ├── RtgsAccount.java │ │ │ │ │ ├── SameBankAccount.java │ │ │ │ │ ├── SameCountryRestrictedBankAccount.java │ │ │ │ │ ├── SatispayAccount.java │ │ │ │ │ ├── SepaAccount.java │ │ │ │ │ ├── SepaInstantAccount.java │ │ │ │ │ ├── SpecificBanksAccount.java │ │ │ │ │ ├── StrikeAccount.java │ │ │ │ │ ├── SwiftAccount.java │ │ │ │ │ ├── SwishAccount.java │ │ │ │ │ ├── TikkieAccount.java │ │ │ │ │ ├── TradeLimits.java │ │ │ │ │ ├── TransferwiseAccount.java │ │ │ │ │ ├── TransferwiseUsdAccount.java │ │ │ │ │ ├── USPostalMoneyOrderAccount.java │ │ │ │ │ ├── UpholdAccount.java │ │ │ │ │ ├── UpiAccount.java │ │ │ │ │ ├── VenmoAccount.java │ │ │ │ │ ├── VerseAccount.java │ │ │ │ │ ├── WeChatPayAccount.java │ │ │ │ │ ├── WesternUnionAccount.java │ │ │ │ │ ├── ZelleAccount.java │ │ │ │ │ ├── payload/ │ │ │ │ │ │ ├── AchTransferAccountPayload.java │ │ │ │ │ │ ├── AdvancedCashAccountPayload.java │ │ │ │ │ │ ├── AliPayAccountPayload.java │ │ │ │ │ │ ├── AmazonGiftCardAccountPayload.java │ │ │ │ │ │ ├── AssetAccountPayload.java │ │ │ │ │ │ ├── AustraliaPayidAccountPayload.java │ │ │ │ │ │ ├── BankAccountPayload.java │ │ │ │ │ │ ├── BizumAccountPayload.java │ │ │ │ │ │ ├── CapitualAccountPayload.java │ │ │ │ │ │ ├── CashAppAccountPayload.java │ │ │ │ │ │ ├── CashAtAtmAccountPayload.java │ │ │ │ │ │ ├── CashDepositAccountPayload.java │ │ │ │ │ │ ├── CelPayAccountPayload.java │ │ │ │ │ │ ├── ChaseQuickPayAccountPayload.java │ │ │ │ │ │ ├── CountryBasedPaymentAccountPayload.java │ │ │ │ │ │ ├── CryptoCurrencyAccountPayload.java │ │ │ │ │ │ ├── DomesticWireTransferAccountPayload.java │ │ │ │ │ │ ├── F2FAccountPayload.java │ │ │ │ │ │ ├── FasterPaymentsAccountPayload.java │ │ │ │ │ │ ├── HalCashAccountPayload.java │ │ │ │ │ │ ├── IfscBasedAccountPayload.java │ │ │ │ │ │ ├── ImpsAccountPayload.java │ │ │ │ │ │ ├── InstantCryptoCurrencyPayload.java │ │ │ │ │ │ ├── InteracETransferAccountPayload.java │ │ │ │ │ │ ├── JapanBankAccountPayload.java │ │ │ │ │ │ ├── MoneseAccountPayload.java │ │ │ │ │ │ ├── MoneyBeamAccountPayload.java │ │ │ │ │ │ ├── MoneyGramAccountPayload.java │ │ │ │ │ │ ├── NationalBankAccountPayload.java │ │ │ │ │ │ ├── NeftAccountPayload.java │ │ │ │ │ │ ├── NequiAccountPayload.java │ │ │ │ │ │ ├── OKPayAccountPayload.java │ │ │ │ │ │ ├── PaxumAccountPayload.java │ │ │ │ │ │ ├── PayByMailAccountPayload.java │ │ │ │ │ │ ├── PayPalAccountPayload.java │ │ │ │ │ │ ├── PayloadWithHolderName.java │ │ │ │ │ │ ├── PaymentAccountPayload.java │ │ │ │ │ │ ├── PaymentMethod.java │ │ │ │ │ │ ├── PaysafeAccountPayload.java │ │ │ │ │ │ ├── PayseraAccountPayload.java │ │ │ │ │ │ ├── PaytmAccountPayload.java │ │ │ │ │ │ ├── PerfectMoneyAccountPayload.java │ │ │ │ │ │ ├── PixAccountPayload.java │ │ │ │ │ │ ├── PopmoneyAccountPayload.java │ │ │ │ │ │ ├── PromptPayAccountPayload.java │ │ │ │ │ │ ├── RevolutAccountPayload.java │ │ │ │ │ │ ├── RtgsAccountPayload.java │ │ │ │ │ │ ├── SameBankAccountPayload.java │ │ │ │ │ │ ├── SatispayAccountPayload.java │ │ │ │ │ │ ├── SepaAccountPayload.java │ │ │ │ │ │ ├── SepaInstantAccountPayload.java │ │ │ │ │ │ ├── SpecificBanksAccountPayload.java │ │ │ │ │ │ ├── StrikeAccountPayload.java │ │ │ │ │ │ ├── SwiftAccountPayload.java │ │ │ │ │ │ ├── SwishAccountPayload.java │ │ │ │ │ │ ├── TikkieAccountPayload.java │ │ │ │ │ │ ├── TransferwiseAccountPayload.java │ │ │ │ │ │ ├── TransferwiseUsdAccountPayload.java │ │ │ │ │ │ ├── USPostalMoneyOrderAccountPayload.java │ │ │ │ │ │ ├── UpholdAccountPayload.java │ │ │ │ │ │ ├── UpiAccountPayload.java │ │ │ │ │ │ ├── VenmoAccountPayload.java │ │ │ │ │ │ ├── VerseAccountPayload.java │ │ │ │ │ │ ├── WeChatPayAccountPayload.java │ │ │ │ │ │ ├── WesternUnionAccountPayload.java │ │ │ │ │ │ └── ZelleAccountPayload.java │ │ │ │ │ └── validation/ │ │ │ │ │ ├── AccountNrValidator.java │ │ │ │ │ ├── AdvancedCashValidator.java │ │ │ │ │ ├── AliPayValidator.java │ │ │ │ │ ├── AustraliaPayidAccountNameValidator.java │ │ │ │ │ ├── AustraliaPayidValidator.java │ │ │ │ │ ├── BICValidator.java │ │ │ │ │ ├── BankIdValidator.java │ │ │ │ │ ├── BankValidator.java │ │ │ │ │ ├── BranchIdValidator.java │ │ │ │ │ ├── CapitualValidator.java │ │ │ │ │ ├── ChaseQuickPayValidator.java │ │ │ │ │ ├── CountryCallingCodes.java │ │ │ │ │ ├── CryptoAddressValidator.java │ │ │ │ │ ├── EmailOrMobileNrOrCashtagValidator.java │ │ │ │ │ ├── EmailOrMobileNrOrUsernameValidator.java │ │ │ │ │ ├── EmailOrMobileNrValidator.java │ │ │ │ │ ├── EmailValidator.java │ │ │ │ │ ├── F2FValidator.java │ │ │ │ │ ├── FiatVolumeValidator.java │ │ │ │ │ ├── HalCashValidator.java │ │ │ │ │ ├── IBANValidator.java │ │ │ │ │ ├── InteracETransferAnswerValidator.java │ │ │ │ │ ├── InteracETransferQuestionValidator.java │ │ │ │ │ ├── InteracETransferValidator.java │ │ │ │ │ ├── JapanBankAccountNameValidator.java │ │ │ │ │ ├── JapanBankAccountNumberValidator.java │ │ │ │ │ ├── JapanBankBranchCodeValidator.java │ │ │ │ │ ├── JapanBankBranchNameValidator.java │ │ │ │ │ ├── JapanBankTransferValidator.java │ │ │ │ │ ├── LengthValidator.java │ │ │ │ │ ├── MoneyBeamValidator.java │ │ │ │ │ ├── NationalAccountIdValidator.java │ │ │ │ │ ├── PercentageNumberValidator.java │ │ │ │ │ ├── PerfectMoneyValidator.java │ │ │ │ │ ├── PhoneNumberValidator.java │ │ │ │ │ ├── PopmoneyValidator.java │ │ │ │ │ ├── PromptPayValidator.java │ │ │ │ │ ├── RevolutValidator.java │ │ │ │ │ ├── SecurityDepositValidator.java │ │ │ │ │ ├── SepaIBANValidator.java │ │ │ │ │ ├── SwishValidator.java │ │ │ │ │ ├── TransferwiseValidator.java │ │ │ │ │ ├── USPostalMoneyOrderValidator.java │ │ │ │ │ ├── UpholdValidator.java │ │ │ │ │ ├── WeChatPayValidator.java │ │ │ │ │ └── XmrValidator.java │ │ │ │ ├── presentation/ │ │ │ │ │ ├── BalancePresentation.java │ │ │ │ │ ├── CorePresentationModule.java │ │ │ │ │ ├── SupportTicketsPresentation.java │ │ │ │ │ └── TradePresentation.java │ │ │ │ ├── proto/ │ │ │ │ │ ├── CoreProtoResolver.java │ │ │ │ │ ├── ProtoDevUtil.java │ │ │ │ │ ├── network/ │ │ │ │ │ │ └── CoreNetworkProtoResolver.java │ │ │ │ │ └── persistable/ │ │ │ │ │ └── CorePersistenceProtoResolver.java │ │ │ │ ├── provider/ │ │ │ │ │ ├── FeeHttpClient.java │ │ │ │ │ ├── HttpClientProvider.java │ │ │ │ │ ├── MempoolHttpClient.java │ │ │ │ │ ├── PriceHttpClient.java │ │ │ │ │ ├── ProvidersRepository.java │ │ │ │ │ ├── fee/ │ │ │ │ │ │ ├── FeeProvider.java │ │ │ │ │ │ └── FeeRequest.java │ │ │ │ │ └── price/ │ │ │ │ │ ├── MarketPrice.java │ │ │ │ │ ├── PriceFeedService.java │ │ │ │ │ ├── PriceProvider.java │ │ │ │ │ ├── PriceRequest.java │ │ │ │ │ └── PriceRequestException.java │ │ │ │ ├── setup/ │ │ │ │ │ ├── CoreNetworkCapabilities.java │ │ │ │ │ ├── CorePersistedDataHost.java │ │ │ │ │ └── CoreSetup.java │ │ │ │ ├── support/ │ │ │ │ │ ├── SupportManager.java │ │ │ │ │ ├── SupportSession.java │ │ │ │ │ ├── SupportType.java │ │ │ │ │ ├── dispute/ │ │ │ │ │ │ ├── Attachment.java │ │ │ │ │ │ ├── Dispute.java │ │ │ │ │ │ ├── DisputeAlreadyOpenException.java │ │ │ │ │ │ ├── DisputeList.java │ │ │ │ │ │ ├── DisputeListService.java │ │ │ │ │ │ ├── DisputeManager.java │ │ │ │ │ │ ├── DisputeMessageDeliveryFailedException.java │ │ │ │ │ │ ├── DisputeResult.java │ │ │ │ │ │ ├── DisputeSession.java │ │ │ │ │ │ ├── DisputeSummaryVerification.java │ │ │ │ │ │ ├── DisputeValidation.java │ │ │ │ │ │ ├── agent/ │ │ │ │ │ │ │ ├── DisputeAgent.java │ │ │ │ │ │ │ ├── DisputeAgentLookupMap.java │ │ │ │ │ │ │ ├── DisputeAgentManager.java │ │ │ │ │ │ │ ├── DisputeAgentService.java │ │ │ │ │ │ │ └── MultipleHolderNameDetection.java │ │ │ │ │ │ ├── arbitration/ │ │ │ │ │ │ │ ├── ArbitrationDisputeList.java │ │ │ │ │ │ │ ├── ArbitrationDisputeListService.java │ │ │ │ │ │ │ ├── ArbitrationManager.java │ │ │ │ │ │ │ ├── ArbitrationSession.java │ │ │ │ │ │ │ ├── TraderDataItem.java │ │ │ │ │ │ │ ├── arbitrator/ │ │ │ │ │ │ │ │ ├── Arbitrator.java │ │ │ │ │ │ │ │ ├── ArbitratorManager.java │ │ │ │ │ │ │ │ └── ArbitratorService.java │ │ │ │ │ │ │ └── messages/ │ │ │ │ │ │ │ └── ArbitrationMessage.java │ │ │ │ │ │ ├── mediation/ │ │ │ │ │ │ │ ├── FileTransferReceiver.java │ │ │ │ │ │ │ ├── FileTransferSender.java │ │ │ │ │ │ │ ├── FileTransferSession.java │ │ │ │ │ │ │ ├── MediationDisputeList.java │ │ │ │ │ │ │ ├── MediationDisputeListService.java │ │ │ │ │ │ │ ├── MediationManager.java │ │ │ │ │ │ │ ├── MediationResultState.java │ │ │ │ │ │ │ ├── MediationSession.java │ │ │ │ │ │ │ └── mediator/ │ │ │ │ │ │ │ ├── Mediator.java │ │ │ │ │ │ │ ├── MediatorManager.java │ │ │ │ │ │ │ └── MediatorService.java │ │ │ │ │ │ ├── messages/ │ │ │ │ │ │ │ ├── DisputeClosedMessage.java │ │ │ │ │ │ │ ├── DisputeMessage.java │ │ │ │ │ │ │ └── DisputeOpenedMessage.java │ │ │ │ │ │ └── refund/ │ │ │ │ │ │ ├── RefundDisputeList.java │ │ │ │ │ │ ├── RefundDisputeListService.java │ │ │ │ │ │ ├── RefundManager.java │ │ │ │ │ │ ├── RefundResultState.java │ │ │ │ │ │ ├── RefundSession.java │ │ │ │ │ │ └── refundagent/ │ │ │ │ │ │ ├── RefundAgent.java │ │ │ │ │ │ ├── RefundAgentManager.java │ │ │ │ │ │ └── RefundAgentService.java │ │ │ │ │ ├── messages/ │ │ │ │ │ │ ├── ChatMessage.java │ │ │ │ │ │ └── SupportMessage.java │ │ │ │ │ └── traderchat/ │ │ │ │ │ ├── TradeChatSession.java │ │ │ │ │ └── TraderChatManager.java │ │ │ │ ├── trade/ │ │ │ │ │ ├── ArbitratorTrade.java │ │ │ │ │ ├── BuyerAsMakerTrade.java │ │ │ │ │ ├── BuyerAsTakerTrade.java │ │ │ │ │ ├── BuyerTrade.java │ │ │ │ │ ├── CleanupMailboxMessages.java │ │ │ │ │ ├── CleanupMailboxMessagesService.java │ │ │ │ │ ├── ClosedTradableFormatter.java │ │ │ │ │ ├── ClosedTradableManager.java │ │ │ │ │ ├── ClosedTradableUtil.java │ │ │ │ │ ├── Contract.java │ │ │ │ │ ├── HavenoUtils.java │ │ │ │ │ ├── MakerTrade.java │ │ │ │ │ ├── SellerAsMakerTrade.java │ │ │ │ │ ├── SellerAsTakerTrade.java │ │ │ │ │ ├── SellerTrade.java │ │ │ │ │ ├── TakerTrade.java │ │ │ │ │ ├── Tradable.java │ │ │ │ │ ├── TradableList.java │ │ │ │ │ ├── Trade.java │ │ │ │ │ ├── TradeDataValidation.java │ │ │ │ │ ├── TradeManager.java │ │ │ │ │ ├── TradeModule.java │ │ │ │ │ ├── TradeTxException.java │ │ │ │ │ ├── TradeUtil.java │ │ │ │ │ ├── failed/ │ │ │ │ │ │ └── FailedTradesManager.java │ │ │ │ │ ├── handlers/ │ │ │ │ │ │ ├── TradeResultHandler.java │ │ │ │ │ │ └── TransactionResultHandler.java │ │ │ │ │ ├── messages/ │ │ │ │ │ │ ├── DepositRequest.java │ │ │ │ │ │ ├── DepositResponse.java │ │ │ │ │ │ ├── DepositsConfirmedMessage.java │ │ │ │ │ │ ├── InitMultisigRequest.java │ │ │ │ │ │ ├── InitTradeRequest.java │ │ │ │ │ │ ├── MediatedPayoutTxPublishedMessage.java │ │ │ │ │ │ ├── MediatedPayoutTxSignatureMessage.java │ │ │ │ │ │ ├── PaymentReceivedMessage.java │ │ │ │ │ │ ├── PaymentSentMessage.java │ │ │ │ │ │ ├── SignContractRequest.java │ │ │ │ │ │ ├── SignContractResponse.java │ │ │ │ │ │ ├── TradeMailboxMessage.java │ │ │ │ │ │ ├── TradeMessage.java │ │ │ │ │ │ └── TradeProtocolVersion.java │ │ │ │ │ ├── protocol/ │ │ │ │ │ │ ├── ArbitratorProtocol.java │ │ │ │ │ │ ├── BuyerAsMakerProtocol.java │ │ │ │ │ │ ├── BuyerAsTakerProtocol.java │ │ │ │ │ │ ├── BuyerProtocol.java │ │ │ │ │ │ ├── DisputeProtocol.java │ │ │ │ │ │ ├── FluentProtocol.java │ │ │ │ │ │ ├── MakerProtocol.java │ │ │ │ │ │ ├── ProcessModel.java │ │ │ │ │ │ ├── ProcessModelServiceProvider.java │ │ │ │ │ │ ├── SellerAsMakerProtocol.java │ │ │ │ │ │ ├── SellerAsTakerProtocol.java │ │ │ │ │ │ ├── SellerProtocol.java │ │ │ │ │ │ ├── TakerProtocol.java │ │ │ │ │ │ ├── TradeListener.java │ │ │ │ │ │ ├── TradePeer.java │ │ │ │ │ │ ├── TradeProtocol.java │ │ │ │ │ │ ├── TradeProtocolFactory.java │ │ │ │ │ │ ├── TradeTaskRunner.java │ │ │ │ │ │ ├── TraderProtocol.java │ │ │ │ │ │ └── tasks/ │ │ │ │ │ │ ├── ApplyFilter.java │ │ │ │ │ │ ├── ArbitratorProcessDepositRequest.java │ │ │ │ │ │ ├── ArbitratorProcessReserveTx.java │ │ │ │ │ │ ├── ArbitratorSendDisputeOpenedMessage.java │ │ │ │ │ │ ├── ArbitratorSendDisputeOpenedMessageToBuyer.java │ │ │ │ │ │ ├── ArbitratorSendDisputeOpenedMessageToSeller.java │ │ │ │ │ │ ├── ArbitratorSendInitTradeOrMultisigRequests.java │ │ │ │ │ │ ├── BuyerPreparePaymentSentMessage.java │ │ │ │ │ │ ├── BuyerSendPaymentSentMessage.java │ │ │ │ │ │ ├── BuyerSendPaymentSentMessageToArbitrator.java │ │ │ │ │ │ ├── BuyerSendPaymentSentMessageToSeller.java │ │ │ │ │ │ ├── MakerRecreateReserveTx.java │ │ │ │ │ │ ├── MakerSendInitTradeRequestToArbitrator.java │ │ │ │ │ │ ├── MakerSetLockTime.java │ │ │ │ │ │ ├── MaybeResendDisputeClosedMessageWithPayout.java │ │ │ │ │ │ ├── MaybeSendSignContractRequest.java │ │ │ │ │ │ ├── ProcessDepositResponse.java │ │ │ │ │ │ ├── ProcessDepositsConfirmedMessage.java │ │ │ │ │ │ ├── ProcessInitMultisigRequest.java │ │ │ │ │ │ ├── ProcessInitTradeRequest.java │ │ │ │ │ │ ├── ProcessPaymentReceivedMessage.java │ │ │ │ │ │ ├── ProcessPaymentSentMessage.java │ │ │ │ │ │ ├── ProcessSignContractRequest.java │ │ │ │ │ │ ├── SellerPreparePaymentReceivedMessage.java │ │ │ │ │ │ ├── SellerSendPaymentReceivedMessage.java │ │ │ │ │ │ ├── SellerSendPaymentReceivedMessageToArbitrator.java │ │ │ │ │ │ ├── SellerSendPaymentReceivedMessageToBuyer.java │ │ │ │ │ │ ├── SendDepositRequest.java │ │ │ │ │ │ ├── SendDepositsConfirmedMessage.java │ │ │ │ │ │ ├── SendDepositsConfirmedMessageToArbitrator.java │ │ │ │ │ │ ├── SendDepositsConfirmedMessageToBuyer.java │ │ │ │ │ │ ├── SendDepositsConfirmedMessageToSeller.java │ │ │ │ │ │ ├── SendMailboxMessageTask.java │ │ │ │ │ │ ├── TakerReserveTradeFunds.java │ │ │ │ │ │ ├── TakerSendInitTradeRequestToArbitrator.java │ │ │ │ │ │ ├── TakerSendInitTradeRequestToMaker.java │ │ │ │ │ │ ├── TradeTask.java │ │ │ │ │ │ ├── VerifyPeersAccountAgeWitness.java │ │ │ │ │ │ └── mediation/ │ │ │ │ │ │ ├── FinalizeMediatedPayoutTx.java │ │ │ │ │ │ ├── ProcessMediatedPayoutSignatureMessage.java │ │ │ │ │ │ ├── ProcessMediatedPayoutTxPublishedMessage.java │ │ │ │ │ │ ├── SendMediatedPayoutSignatureMessage.java │ │ │ │ │ │ ├── SendMediatedPayoutTxPublishedMessage.java │ │ │ │ │ │ ├── SetupMediatedPayoutTxListener.java │ │ │ │ │ │ └── SignMediatedPayoutTx.java │ │ │ │ │ └── statistics/ │ │ │ │ │ ├── ReferralId.java │ │ │ │ │ ├── ReferralIdService.java │ │ │ │ │ ├── TradeStatistics3.java │ │ │ │ │ ├── TradeStatistics3StorageService.java │ │ │ │ │ ├── TradeStatistics3Store.java │ │ │ │ │ ├── TradeStatisticsForJson.java │ │ │ │ │ └── TradeStatisticsManager.java │ │ │ │ ├── user/ │ │ │ │ │ ├── AutoConfirmSettings.java │ │ │ │ │ ├── BlockChainExplorer.java │ │ │ │ │ ├── Cookie.java │ │ │ │ │ ├── CookieKey.java │ │ │ │ │ ├── DontShowAgainLookup.java │ │ │ │ │ ├── Preferences.java │ │ │ │ │ ├── PreferencesPayload.java │ │ │ │ │ ├── User.java │ │ │ │ │ └── UserPayload.java │ │ │ │ ├── util/ │ │ │ │ │ ├── AveragePriceUtil.java │ │ │ │ │ ├── FormattingUtils.java │ │ │ │ │ ├── GenerateKeyPairs.java │ │ │ │ │ ├── InlierUtil.java │ │ │ │ │ ├── JsonUtil.java │ │ │ │ │ ├── ParsingUtils.java │ │ │ │ │ ├── PriceUtil.java │ │ │ │ │ ├── SimpleMarkdownParser.java │ │ │ │ │ ├── Validator.java │ │ │ │ │ ├── VolumeUtil.java │ │ │ │ │ ├── coin/ │ │ │ │ │ │ ├── CoinFormatter.java │ │ │ │ │ │ ├── CoinUtil.java │ │ │ │ │ │ └── ImmutableCoinFormatter.java │ │ │ │ │ └── validation/ │ │ │ │ │ ├── AmountValidator4Decimals.java │ │ │ │ │ ├── AmountValidator8Decimals.java │ │ │ │ │ ├── BtcAddressValidator.java │ │ │ │ │ ├── HexStringValidator.java │ │ │ │ │ ├── InputValidator.java │ │ │ │ │ ├── IntegerValidator.java │ │ │ │ │ ├── MonetaryValidator.java │ │ │ │ │ ├── NumberValidator.java │ │ │ │ │ ├── RegexValidator.java │ │ │ │ │ ├── RegexValidatorFactory.java │ │ │ │ │ ├── StringValidator.java │ │ │ │ │ └── UrlInputValidator.java │ │ │ │ └── xmr/ │ │ │ │ ├── Balances.java │ │ │ │ ├── XmrConnectionModule.java │ │ │ │ ├── XmrModule.java │ │ │ │ ├── XmrNodeSettings.java │ │ │ │ ├── exceptions/ │ │ │ │ │ ├── AddressEntryException.java │ │ │ │ │ ├── InsufficientFundsException.java │ │ │ │ │ ├── InvalidHostException.java │ │ │ │ │ ├── RejectedTxException.java │ │ │ │ │ ├── SigningException.java │ │ │ │ │ ├── TransactionVerificationException.java │ │ │ │ │ ├── TxBroadcastException.java │ │ │ │ │ ├── TxBroadcastTimeoutException.java │ │ │ │ │ └── WalletException.java │ │ │ │ ├── listeners/ │ │ │ │ │ ├── AddressConfidenceListener.java │ │ │ │ │ ├── BalanceListener.java │ │ │ │ │ ├── TxConfidenceListener.java │ │ │ │ │ └── XmrBalanceListener.java │ │ │ │ ├── model/ │ │ │ │ │ ├── AddressEntry.java │ │ │ │ │ ├── AddressEntryList.java │ │ │ │ │ ├── EncryptedConnectionList.java │ │ │ │ │ ├── InputsAndChangeOutput.java │ │ │ │ │ ├── PreparedDepositTxAndMakerInputs.java │ │ │ │ │ ├── RawTransactionInput.java │ │ │ │ │ ├── XmrAddressEntry.java │ │ │ │ │ └── XmrAddressEntryList.java │ │ │ │ ├── nodes/ │ │ │ │ │ ├── ProxySocketFactory.java │ │ │ │ │ ├── SeedPeersSocks5Dns.java │ │ │ │ │ ├── XmrNetworkConfig.java │ │ │ │ │ ├── XmrNodeConverter.java │ │ │ │ │ ├── XmrNodes.java │ │ │ │ │ ├── XmrNodesRepository.java │ │ │ │ │ └── XmrNodesSetupPreferences.java │ │ │ │ ├── setup/ │ │ │ │ │ ├── DownloadListener.java │ │ │ │ │ ├── HavenoKeyChainFactory.java │ │ │ │ │ ├── HavenoKeyChainGroupStructure.java │ │ │ │ │ ├── MoneroWalletRpcManager.java │ │ │ │ │ ├── RegTestHost.java │ │ │ │ │ ├── WalletConfig.java │ │ │ │ │ └── WalletsSetup.java │ │ │ │ └── wallet/ │ │ │ │ ├── BtcCoinSelector.java │ │ │ │ ├── BtcWalletService.java │ │ │ │ ├── HavenoDefaultCoinSelector.java │ │ │ │ ├── HavenoRiskAnalysis.java │ │ │ │ ├── NonBsqCoinSelector.java │ │ │ │ ├── Restrictions.java │ │ │ │ ├── TradeWalletService.java │ │ │ │ ├── WalletService.java │ │ │ │ ├── WalletsManager.java │ │ │ │ ├── XmrWalletBase.java │ │ │ │ └── XmrWalletService.java │ │ │ └── resources/ │ │ │ ├── bip39_english.txt │ │ │ ├── haveno.policy │ │ │ ├── haveno.properties │ │ │ ├── help/ │ │ │ │ ├── canceloffer-help.txt │ │ │ │ ├── confirmpaymentreceived-help.txt │ │ │ │ ├── confirmpaymentstarted-help.txt │ │ │ │ ├── createoffer-help.txt │ │ │ │ ├── createpaymentacct-help.txt │ │ │ │ ├── getaddressbalance-help.txt │ │ │ │ ├── getbalance-help.txt │ │ │ │ ├── getfundingaddresses-help.txt │ │ │ │ ├── getmyoffer-help.txt │ │ │ │ ├── getmyoffers-help.txt │ │ │ │ ├── getoffer-help.txt │ │ │ │ ├── getoffers-help.txt │ │ │ │ ├── getpaymentacctform-help.txt │ │ │ │ ├── getpaymentaccts-help.txt │ │ │ │ ├── getpaymentmethods-help.txt │ │ │ │ ├── gettrade-help.txt │ │ │ │ ├── gettransaction-help.txt │ │ │ │ ├── gettxfeerate-help.txt │ │ │ │ ├── getversion-help.txt │ │ │ │ ├── getxmrprice-help.txt │ │ │ │ ├── keepfunds-help.txt │ │ │ │ ├── lockwallet-help.txt │ │ │ │ ├── removewalletpassword-help.txt │ │ │ │ ├── sendxmr-help.txt │ │ │ │ ├── settxfeerate-help.txt │ │ │ │ ├── setwalletpassword-help.txt │ │ │ │ ├── stop-help.txt │ │ │ │ ├── takeoffer-help.txt │ │ │ │ ├── unlockwallet-help.txt │ │ │ │ ├── unsettxfeerate-help.txt │ │ │ │ └── withdrawfunds-help.txt │ │ │ ├── i18n/ │ │ │ │ ├── displayStrings.properties │ │ │ │ ├── displayStrings_cs.properties │ │ │ │ ├── displayStrings_de.properties │ │ │ │ ├── displayStrings_es.properties │ │ │ │ ├── displayStrings_fa.properties │ │ │ │ ├── displayStrings_fr.properties │ │ │ │ ├── displayStrings_it.properties │ │ │ │ ├── displayStrings_ja.properties │ │ │ │ ├── displayStrings_pt-br.properties │ │ │ │ ├── displayStrings_pt.properties │ │ │ │ ├── displayStrings_ru.properties │ │ │ │ ├── displayStrings_th.properties │ │ │ │ ├── displayStrings_tr.properties │ │ │ │ ├── displayStrings_vi.properties │ │ │ │ ├── displayStrings_zh-hans.properties │ │ │ │ └── displayStrings_zh-hant.properties │ │ │ ├── prevent-app-nap-silent-sound.aiff │ │ │ ├── wallet/ │ │ │ │ ├── checkpoints.testnet.txt │ │ │ │ └── checkpoints.txt │ │ │ ├── xmr_local.seednodes │ │ │ ├── xmr_mainnet.seednodes │ │ │ └── xmr_stagenet.seednodes │ │ └── test/ │ │ ├── java/ │ │ │ └── haveno/ │ │ │ └── core/ │ │ │ ├── account/ │ │ │ │ ├── sign/ │ │ │ │ │ ├── SignedWitnessServiceTest.java │ │ │ │ │ └── SignedWitnessTest.java │ │ │ │ └── witness/ │ │ │ │ └── AccountAgeWitnessServiceTest.java │ │ │ ├── app/ │ │ │ │ └── HavenoHelpFormatterTest.java │ │ │ ├── arbitration/ │ │ │ │ ├── ArbitratorManagerTest.java │ │ │ │ ├── ArbitratorTest.java │ │ │ │ ├── MediatorTest.java │ │ │ │ └── TraderDataItemTest.java │ │ │ ├── crypto/ │ │ │ │ ├── EncryptionTest.java │ │ │ │ └── SigTest.java │ │ │ ├── locale/ │ │ │ │ ├── BankUtilTest.java │ │ │ │ ├── CurrencyUtilTest.java │ │ │ │ └── MockTestnetCoin.java │ │ │ ├── message/ │ │ │ │ └── MarshallerTest.java │ │ │ ├── monetary/ │ │ │ │ └── PriceTest.java │ │ │ ├── network/ │ │ │ │ └── p2p/ │ │ │ │ └── seed/ │ │ │ │ └── DefaultSeedNodeRepositoryTest.java │ │ │ ├── notifications/ │ │ │ │ └── MobileModelTest.java │ │ │ ├── offer/ │ │ │ │ ├── OfferMaker.java │ │ │ │ ├── OfferTest.java │ │ │ │ ├── OpenOfferManagerTest.java │ │ │ │ └── availability/ │ │ │ │ └── ArbitratorSelectionTest.java │ │ │ ├── payment/ │ │ │ │ ├── PaymentAccountsTest.java │ │ │ │ ├── ReceiptPredicatesTest.java │ │ │ │ ├── ReceiptValidatorTest.java │ │ │ │ ├── TradeLimitsTest.java │ │ │ │ └── validation/ │ │ │ │ └── CryptoAddressValidatorTest.java │ │ │ ├── provider/ │ │ │ │ └── price/ │ │ │ │ └── MarketPriceFeedServiceTest.java │ │ │ ├── support/ │ │ │ │ └── dispute/ │ │ │ │ └── mediation/ │ │ │ │ └── FileTransferSessionTest.java │ │ │ ├── trade/ │ │ │ │ └── TradableListTest.java │ │ │ ├── user/ │ │ │ │ ├── PreferencesTest.java │ │ │ │ └── UserPayloadModelVOTest.java │ │ │ ├── util/ │ │ │ │ ├── FormattingUtilsTest.java │ │ │ │ ├── ProtoUtilTest.java │ │ │ │ ├── RegexValidatorTest.java │ │ │ │ └── coin/ │ │ │ │ └── CoinUtilTest.java │ │ │ └── xmr/ │ │ │ ├── nodes/ │ │ │ │ ├── BtcNetworkConfigTest.java │ │ │ │ ├── XmrNodeConverterTest.java │ │ │ │ ├── XmrNodesRepositoryTest.java │ │ │ │ └── XmrNodesSetupPreferencesTest.java │ │ │ └── wallet/ │ │ │ └── RestrictionsTest.java │ │ └── resources/ │ │ ├── haveno/ │ │ │ └── core/ │ │ │ ├── app/ │ │ │ │ ├── cli-output.txt │ │ │ │ └── cli-output_windows.txt │ │ │ ├── dao/ │ │ │ │ └── node/ │ │ │ │ └── full/ │ │ │ │ └── rpc/ │ │ │ │ ├── getblock-result-verbosity-0.txt │ │ │ │ ├── getblock-result-verbosity-1.json │ │ │ │ ├── getblock-result-verbosity-2.json │ │ │ │ └── getnetworkinfo-result.json │ │ │ └── provider/ │ │ │ └── mempool/ │ │ │ ├── badOfferTestData.json │ │ │ ├── offerTestData.json │ │ │ └── txInfo.json │ │ ├── mainnet.seednodes │ │ ├── mockito-extensions/ │ │ │ └── org.mockito.plugins.MockMaker │ │ ├── regtest.seednodes │ │ └── testnet.seednodes │ └── update_translations.sh ├── daemon/ │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── haveno/ │ │ │ └── daemon/ │ │ │ ├── app/ │ │ │ │ ├── HavenoDaemon.java │ │ │ │ └── HavenoDaemonMain.java │ │ │ └── grpc/ │ │ │ ├── GrpcAccountService.java │ │ │ ├── GrpcDisputeAgentsService.java │ │ │ ├── GrpcDisputesService.java │ │ │ ├── GrpcErrorMessageHandler.java │ │ │ ├── GrpcExceptionHandler.java │ │ │ ├── GrpcGetTradeStatisticsService.java │ │ │ ├── GrpcHelpService.java │ │ │ ├── GrpcNotificationsService.java │ │ │ ├── GrpcOffersService.java │ │ │ ├── GrpcPaymentAccountsService.java │ │ │ ├── GrpcPriceService.java │ │ │ ├── GrpcServer.java │ │ │ ├── GrpcShutdownService.java │ │ │ ├── GrpcTradesService.java │ │ │ ├── GrpcVersionService.java │ │ │ ├── GrpcWalletsService.java │ │ │ ├── GrpcXmrConnectionService.java │ │ │ ├── GrpcXmrNodeService.java │ │ │ └── interceptor/ │ │ │ ├── CallRateMeteringInterceptor.java │ │ │ ├── GrpcCallRateMeter.java │ │ │ ├── GrpcServiceRateMeteringConfig.java │ │ │ └── PasswordAuthInterceptor.java │ │ └── resources/ │ │ └── logback.xml │ └── test/ │ └── java/ │ └── haveno/ │ └── daemon/ │ └── grpc/ │ └── interceptor/ │ └── GrpcServiceRateMeteringConfigTest.java ├── desktop/ │ ├── package/ │ │ ├── 29CDFD3B.asc │ │ ├── 5BC5ED73.asc │ │ ├── F379A1C6.asc │ │ ├── README.md │ │ ├── linux/ │ │ │ ├── Dockerfile │ │ │ ├── Haveno.desktop │ │ │ ├── exchange.haveno.Haveno.metainfo.xml │ │ │ ├── exchange.haveno.Haveno.yml │ │ │ └── jpackage.deb/ │ │ │ └── Haveno.desktop │ │ ├── macosx/ │ │ │ ├── Haveno-volume.icns │ │ │ ├── Haveno.icns │ │ │ ├── Info.plist │ │ │ ├── copy_dbs.sh │ │ │ ├── finalize.sh │ │ │ ├── insert_snapshot_version.sh │ │ │ ├── macos.entitlements │ │ │ └── replace_version_number.sh │ │ ├── package.gradle │ │ ├── signingkey.asc │ │ ├── tools-1.0.jar │ │ └── windows/ │ │ ├── main.wxs │ │ └── overrides.wxi │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── haveno/ │ │ │ └── desktop/ │ │ │ ├── CandleStickChart.css │ │ │ ├── DesktopModule.java │ │ │ ├── Navigation.java │ │ │ ├── app/ │ │ │ │ ├── HavenoApp.java │ │ │ │ ├── HavenoAppMain.java │ │ │ │ └── HavenoAppModule.java │ │ │ ├── common/ │ │ │ │ ├── UITimer.java │ │ │ │ ├── ViewfxException.java │ │ │ │ ├── fxml/ │ │ │ │ │ └── FxmlViewLoader.java │ │ │ │ ├── model/ │ │ │ │ │ ├── Activatable.java │ │ │ │ │ ├── ActivatableDataModel.java │ │ │ │ │ ├── ActivatableViewModel.java │ │ │ │ │ ├── ActivatableWithDataModel.java │ │ │ │ │ ├── DataModel.java │ │ │ │ │ ├── Model.java │ │ │ │ │ ├── ViewModel.java │ │ │ │ │ └── WithDataModel.java │ │ │ │ └── view/ │ │ │ │ ├── AbstractView.java │ │ │ │ ├── ActivatableView.java │ │ │ │ ├── ActivatableViewAndModel.java │ │ │ │ ├── CachingViewLoader.java │ │ │ │ ├── DefaultPathConvention.java │ │ │ │ ├── FxmlView.java │ │ │ │ ├── InitializableView.java │ │ │ │ ├── View.java │ │ │ │ ├── ViewFactory.java │ │ │ │ ├── ViewLoader.java │ │ │ │ ├── ViewPath.java │ │ │ │ └── guice/ │ │ │ │ └── InjectorViewFactory.java │ │ │ ├── components/ │ │ │ │ ├── AccountStatusTooltipLabel.java │ │ │ │ ├── AddressTextField.java │ │ │ │ ├── AddressWithIconAndDirection.java │ │ │ │ ├── AutoTooltipButton.java │ │ │ │ ├── AutoTooltipCheckBox.java │ │ │ │ ├── AutoTooltipLabel.java │ │ │ │ ├── AutoTooltipRadioButton.java │ │ │ │ ├── AutoTooltipSlideToggleButton.java │ │ │ │ ├── AutoTooltipTableColumn.java │ │ │ │ ├── AutoTooltipTextField.java │ │ │ │ ├── AutoTooltipToggleButton.java │ │ │ │ ├── AutocompleteComboBox.java │ │ │ │ ├── BalanceTextField.java │ │ │ │ ├── BusyAnimation.java │ │ │ │ ├── ColoredDecimalPlacesWithZerosText.java │ │ │ │ ├── ExplorerAddressTextField.java │ │ │ │ ├── ExternalHyperlink.java │ │ │ │ ├── FundsTextField.java │ │ │ │ ├── HavenoTextArea.java │ │ │ │ ├── HavenoTextField.java │ │ │ │ ├── HyperlinkWithIcon.java │ │ │ │ ├── InfoAutoTooltipLabel.java │ │ │ │ ├── InfoDisplay.java │ │ │ │ ├── InfoInputTextField.java │ │ │ │ ├── InfoTextField.java │ │ │ │ ├── InputTextArea.java │ │ │ │ ├── InputTextField.java │ │ │ │ ├── JFXRadioButtonSkinHavenoStyle.java │ │ │ │ ├── JFXTextAreaSkinHavenoStyle.java │ │ │ │ ├── JFXTextFieldSkinHavenoStyle.java │ │ │ │ ├── MenuItem.java │ │ │ │ ├── NewBadge.java │ │ │ │ ├── PasswordTextField.java │ │ │ │ ├── PeerInfoIcon.java │ │ │ │ ├── PeerInfoIconDispute.java │ │ │ │ ├── PeerInfoIconMap.java │ │ │ │ ├── PeerInfoIconSmall.java │ │ │ │ ├── PeerInfoIconTrading.java │ │ │ │ ├── PopOverWrapper.java │ │ │ │ ├── SimpleMarkdownLabel.java │ │ │ │ ├── TableGroupHeadline.java │ │ │ │ ├── TextFieldWithCopyIcon.java │ │ │ │ ├── TextFieldWithIcon.java │ │ │ │ ├── TitledGroupBg.java │ │ │ │ ├── TooltipUtil.java │ │ │ │ ├── TxIdTextField.java │ │ │ │ ├── chart/ │ │ │ │ │ ├── ChartDataModel.java │ │ │ │ │ ├── ChartView.java │ │ │ │ │ ├── ChartViewModel.java │ │ │ │ │ └── TemporalAdjusterModel.java │ │ │ │ ├── controlsfx/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── control/ │ │ │ │ │ │ ├── PopOver.java │ │ │ │ │ │ └── popover.css │ │ │ │ │ └── skin/ │ │ │ │ │ └── PopOverSkin.java │ │ │ │ ├── indicator/ │ │ │ │ │ ├── TxConfidenceIndicator.java │ │ │ │ │ └── skin/ │ │ │ │ │ └── StaticProgressIndicatorSkin.java │ │ │ │ ├── list/ │ │ │ │ │ └── FilterBox.java │ │ │ │ └── paymentmethods/ │ │ │ │ ├── AchTransferForm.java │ │ │ │ ├── AdvancedCashForm.java │ │ │ │ ├── AliPayForm.java │ │ │ │ ├── AmazonGiftCardForm.java │ │ │ │ ├── AssetsForm.java │ │ │ │ ├── AustraliaPayidForm.java │ │ │ │ ├── BankForm.java │ │ │ │ ├── BizumForm.java │ │ │ │ ├── CapitualForm.java │ │ │ │ ├── CashAppForm.java │ │ │ │ ├── CashAtAtmForm.java │ │ │ │ ├── CashDepositForm.java │ │ │ │ ├── CelPayForm.java │ │ │ │ ├── ChaseQuickPayForm.java │ │ │ │ ├── DomesticWireTransferForm.java │ │ │ │ ├── F2FForm.java │ │ │ │ ├── FasterPaymentsForm.java │ │ │ │ ├── GeneralAccountNumberForm.java │ │ │ │ ├── GeneralBankForm.java │ │ │ │ ├── GeneralSepaForm.java │ │ │ │ ├── GeneralUsBankForm.java │ │ │ │ ├── HalCashForm.java │ │ │ │ ├── IfscBankForm.java │ │ │ │ ├── ImpsForm.java │ │ │ │ ├── InteracETransferForm.java │ │ │ │ ├── JapanBankTransferForm.java │ │ │ │ ├── MoneseForm.java │ │ │ │ ├── MoneyBeamForm.java │ │ │ │ ├── MoneyGramForm.java │ │ │ │ ├── NationalBankForm.java │ │ │ │ ├── NeftForm.java │ │ │ │ ├── NequiForm.java │ │ │ │ ├── PaxumForm.java │ │ │ │ ├── PayByMailForm.java │ │ │ │ ├── PayPalForm.java │ │ │ │ ├── PaymentMethodForm.java │ │ │ │ ├── PaysafeForm.java │ │ │ │ ├── PayseraForm.java │ │ │ │ ├── PaytmForm.java │ │ │ │ ├── PerfectMoneyForm.java │ │ │ │ ├── PixForm.java │ │ │ │ ├── PopmoneyForm.java │ │ │ │ ├── PromptPayForm.java │ │ │ │ ├── RevolutForm.java │ │ │ │ ├── RtgsForm.java │ │ │ │ ├── SameBankForm.java │ │ │ │ ├── SatispayForm.java │ │ │ │ ├── SepaForm.java │ │ │ │ ├── SepaInstantForm.java │ │ │ │ ├── SpecificBankForm.java │ │ │ │ ├── StrikeForm.java │ │ │ │ ├── SwiftForm.java │ │ │ │ ├── SwishForm.java │ │ │ │ ├── TikkieForm.java │ │ │ │ ├── TransferwiseForm.java │ │ │ │ ├── TransferwiseUsdForm.java │ │ │ │ ├── USPostalMoneyOrderForm.java │ │ │ │ ├── UpholdForm.java │ │ │ │ ├── UpiForm.java │ │ │ │ ├── VenmoForm.java │ │ │ │ ├── VerseForm.java │ │ │ │ ├── WeChatPayForm.java │ │ │ │ ├── WesternUnionForm.java │ │ │ │ └── ZelleForm.java │ │ │ ├── haveno.css │ │ │ ├── images.css │ │ │ ├── main/ │ │ │ │ ├── MainView.fxml │ │ │ │ ├── MainView.java │ │ │ │ ├── MainViewModel.java │ │ │ │ ├── SharedPresentation.java │ │ │ │ ├── account/ │ │ │ │ │ ├── AccountView.fxml │ │ │ │ │ ├── AccountView.java │ │ │ │ │ ├── content/ │ │ │ │ │ │ ├── PaymentAccountsView.java │ │ │ │ │ │ ├── backup/ │ │ │ │ │ │ │ ├── BackupView.fxml │ │ │ │ │ │ │ └── BackupView.java │ │ │ │ │ │ ├── cryptoaccounts/ │ │ │ │ │ │ │ ├── CryptoAccountsDataModel.java │ │ │ │ │ │ │ ├── CryptoAccountsView.fxml │ │ │ │ │ │ │ ├── CryptoAccountsView.java │ │ │ │ │ │ │ └── CryptoAccountsViewModel.java │ │ │ │ │ │ ├── notifications/ │ │ │ │ │ │ │ ├── ManageMarketAlertsWindow.java │ │ │ │ │ │ │ ├── MobileNotificationsView.fxml │ │ │ │ │ │ │ ├── MobileNotificationsView.java │ │ │ │ │ │ │ └── NoWebCamFoundException.java │ │ │ │ │ │ ├── password/ │ │ │ │ │ │ │ ├── PasswordView.fxml │ │ │ │ │ │ │ └── PasswordView.java │ │ │ │ │ │ ├── seedwords/ │ │ │ │ │ │ │ ├── SeedWordsView.fxml │ │ │ │ │ │ │ └── SeedWordsView.java │ │ │ │ │ │ ├── traditionalaccounts/ │ │ │ │ │ │ │ ├── TraditionalAccountsDataModel.java │ │ │ │ │ │ │ ├── TraditionalAccountsView.fxml │ │ │ │ │ │ │ ├── TraditionalAccountsView.java │ │ │ │ │ │ │ └── TraditionalAccountsViewModel.java │ │ │ │ │ │ └── walletinfo/ │ │ │ │ │ │ ├── WalletInfoView.fxml │ │ │ │ │ │ └── WalletInfoView.java │ │ │ │ │ └── register/ │ │ │ │ │ ├── AgentRegistrationView.java │ │ │ │ │ ├── AgentRegistrationViewModel.java │ │ │ │ │ ├── arbitrator/ │ │ │ │ │ │ ├── ArbitratorRegistrationView.fxml │ │ │ │ │ │ ├── ArbitratorRegistrationView.java │ │ │ │ │ │ └── ArbitratorRegistrationViewModel.java │ │ │ │ │ ├── mediator/ │ │ │ │ │ │ ├── MediatorRegistrationView.fxml │ │ │ │ │ │ ├── MediatorRegistrationView.java │ │ │ │ │ │ └── MediatorRegistrationViewModel.java │ │ │ │ │ ├── refundagent/ │ │ │ │ │ │ ├── RefundAgentRegistrationView.fxml │ │ │ │ │ │ ├── RefundAgentRegistrationView.java │ │ │ │ │ │ └── RefundAgentRegistrationViewModel.java │ │ │ │ │ └── signing/ │ │ │ │ │ ├── SigningView.fxml │ │ │ │ │ └── SigningView.java │ │ │ │ ├── debug/ │ │ │ │ │ ├── DebugView.fxml │ │ │ │ │ └── DebugView.java │ │ │ │ ├── funds/ │ │ │ │ │ ├── FundsView.fxml │ │ │ │ │ ├── FundsView.java │ │ │ │ │ ├── deposit/ │ │ │ │ │ │ ├── DepositListItem.java │ │ │ │ │ │ ├── DepositView.fxml │ │ │ │ │ │ └── DepositView.java │ │ │ │ │ ├── locked/ │ │ │ │ │ │ ├── LockedListItem.java │ │ │ │ │ │ ├── LockedView.fxml │ │ │ │ │ │ └── LockedView.java │ │ │ │ │ ├── reserved/ │ │ │ │ │ │ ├── ReservedListItem.java │ │ │ │ │ │ ├── ReservedView.fxml │ │ │ │ │ │ └── ReservedView.java │ │ │ │ │ ├── transactions/ │ │ │ │ │ │ ├── DisplayedTransactions.java │ │ │ │ │ │ ├── DisplayedTransactionsFactory.java │ │ │ │ │ │ ├── DummyTransactionAwareTradable.java │ │ │ │ │ │ ├── ObservableListDecorator.java │ │ │ │ │ │ ├── TradableRepository.java │ │ │ │ │ │ ├── TransactionAwareOpenOffer.java │ │ │ │ │ │ ├── TransactionAwareTradable.java │ │ │ │ │ │ ├── TransactionAwareTradableFactory.java │ │ │ │ │ │ ├── TransactionAwareTrade.java │ │ │ │ │ │ ├── TransactionListItemFactory.java │ │ │ │ │ │ ├── TransactionsListItem.java │ │ │ │ │ │ ├── TransactionsView.fxml │ │ │ │ │ │ └── TransactionsView.java │ │ │ │ │ └── withdrawal/ │ │ │ │ │ ├── WithdrawalListItem.java │ │ │ │ │ ├── WithdrawalView.fxml │ │ │ │ │ └── WithdrawalView.java │ │ │ │ ├── market/ │ │ │ │ │ ├── MarketView.fxml │ │ │ │ │ ├── MarketView.java │ │ │ │ │ ├── offerbook/ │ │ │ │ │ │ ├── OfferBookChartView.fxml │ │ │ │ │ │ ├── OfferBookChartView.java │ │ │ │ │ │ ├── OfferBookChartViewModel.java │ │ │ │ │ │ └── OfferListItem.java │ │ │ │ │ ├── spread/ │ │ │ │ │ │ ├── SpreadItem.java │ │ │ │ │ │ ├── SpreadView.fxml │ │ │ │ │ │ ├── SpreadView.java │ │ │ │ │ │ ├── SpreadViewModel.java │ │ │ │ │ │ ├── SpreadViewPaymentMethod.fxml │ │ │ │ │ │ └── SpreadViewPaymentMethod.java │ │ │ │ │ └── trades/ │ │ │ │ │ ├── ChartCalculations.java │ │ │ │ │ ├── TradeStatistics3ListItem.java │ │ │ │ │ ├── TradesChartsView.fxml │ │ │ │ │ ├── TradesChartsView.java │ │ │ │ │ ├── TradesChartsViewModel.java │ │ │ │ │ └── charts/ │ │ │ │ │ ├── CandleData.java │ │ │ │ │ ├── price/ │ │ │ │ │ │ ├── Candle.java │ │ │ │ │ │ ├── CandleStickChart.java │ │ │ │ │ │ └── CandleTooltip.java │ │ │ │ │ └── volume/ │ │ │ │ │ ├── VolumeBar.java │ │ │ │ │ └── VolumeChart.java │ │ │ │ ├── offer/ │ │ │ │ │ ├── BuyOfferView.fxml │ │ │ │ │ ├── BuyOfferView.java │ │ │ │ │ ├── ClosableView.java │ │ │ │ │ ├── InitializableViewWithTakeOfferData.java │ │ │ │ │ ├── MutableOfferDataModel.java │ │ │ │ │ ├── MutableOfferView.java │ │ │ │ │ ├── MutableOfferViewModel.java │ │ │ │ │ ├── OfferDataModel.java │ │ │ │ │ ├── OfferView.java │ │ │ │ │ ├── OfferViewModelUtil.java │ │ │ │ │ ├── OfferViewUtil.java │ │ │ │ │ ├── SelectableView.java │ │ │ │ │ ├── SellOfferView.fxml │ │ │ │ │ ├── SellOfferView.java │ │ │ │ │ ├── createoffer/ │ │ │ │ │ │ ├── CreateOfferDataModel.java │ │ │ │ │ │ ├── CreateOfferView.fxml │ │ │ │ │ │ ├── CreateOfferView.java │ │ │ │ │ │ └── CreateOfferViewModel.java │ │ │ │ │ ├── offerbook/ │ │ │ │ │ │ ├── CryptoOfferBookView.fxml │ │ │ │ │ │ ├── CryptoOfferBookView.java │ │ │ │ │ │ ├── CryptoOfferBookViewModel.java │ │ │ │ │ │ ├── FiatOfferBookView.fxml │ │ │ │ │ │ ├── FiatOfferBookView.java │ │ │ │ │ │ ├── FiatOfferBookViewModel.java │ │ │ │ │ │ ├── OfferBook.java │ │ │ │ │ │ ├── OfferBookListItem.java │ │ │ │ │ │ ├── OfferBookView.fxml │ │ │ │ │ │ ├── OfferBookView.java │ │ │ │ │ │ ├── OfferBookViewModel.java │ │ │ │ │ │ ├── OtherOfferBookView.fxml │ │ │ │ │ │ ├── OtherOfferBookView.java │ │ │ │ │ │ └── OtherOfferBookViewModel.java │ │ │ │ │ ├── signedoffer/ │ │ │ │ │ │ ├── SignedOfferListItem.java │ │ │ │ │ │ ├── SignedOfferView.fxml │ │ │ │ │ │ ├── SignedOfferView.java │ │ │ │ │ │ ├── SignedOffersDataModel.java │ │ │ │ │ │ └── SignedOffersViewModel.java │ │ │ │ │ └── takeoffer/ │ │ │ │ │ ├── TakeOfferDataModel.java │ │ │ │ │ ├── TakeOfferView.fxml │ │ │ │ │ ├── TakeOfferView.java │ │ │ │ │ └── TakeOfferViewModel.java │ │ │ │ ├── overlays/ │ │ │ │ │ ├── Overlay.java │ │ │ │ │ ├── TabbedOverlay.java │ │ │ │ │ ├── editor/ │ │ │ │ │ │ ├── PasswordPopup.java │ │ │ │ │ │ └── PeerInfoWithTagEditor.java │ │ │ │ │ ├── notifications/ │ │ │ │ │ │ ├── Notification.java │ │ │ │ │ │ ├── NotificationCenter.java │ │ │ │ │ │ └── NotificationManager.java │ │ │ │ │ ├── popups/ │ │ │ │ │ │ ├── Popup.java │ │ │ │ │ │ └── PopupManager.java │ │ │ │ │ └── windows/ │ │ │ │ │ ├── ClosedTradesSummaryWindow.java │ │ │ │ │ ├── ContractWindow.java │ │ │ │ │ ├── DisplayAlertMessageWindow.java │ │ │ │ │ ├── DisputeSummaryWindow.java │ │ │ │ │ ├── EditCustomExplorerWindow.java │ │ │ │ │ ├── FilterWindow.java │ │ │ │ │ ├── GenericMessageWindow.java │ │ │ │ │ ├── OfferDetailsWindow.java │ │ │ │ │ ├── QRCodeWindow.java │ │ │ │ │ ├── SelectDepositTxWindow.java │ │ │ │ │ ├── SendAlertMessageWindow.java │ │ │ │ │ ├── SendLogFilesWindow.java │ │ │ │ │ ├── SendPrivateNotificationWindow.java │ │ │ │ │ ├── ShowWalletDataWindow.java │ │ │ │ │ ├── SignPaymentAccountsWindow.java │ │ │ │ │ ├── SignSpecificWitnessWindow.java │ │ │ │ │ ├── SignUnsignedPubKeysWindow.java │ │ │ │ │ ├── SwiftPaymentDetails.java │ │ │ │ │ ├── TacWindow.java │ │ │ │ │ ├── TorNetworkSettingsWindow.java │ │ │ │ │ ├── TradeDetailsWindow.java │ │ │ │ │ ├── TradeFeedbackWindow.java │ │ │ │ │ ├── TxDetailsWindow.java │ │ │ │ │ ├── TxWithdrawWindow.java │ │ │ │ │ ├── UnlockDisputeAgentRegistrationWindow.java │ │ │ │ │ ├── UpdateAmazonGiftCardAccountWindow.java │ │ │ │ │ ├── UpdateRevolutAccountWindow.java │ │ │ │ │ ├── VerifyDisputeResultSignatureWindow.java │ │ │ │ │ ├── WalletPasswordWindow.java │ │ │ │ │ ├── WebCamWindow.java │ │ │ │ │ └── downloadupdate/ │ │ │ │ │ ├── DisplayUpdateDownloadWindow.java │ │ │ │ │ ├── DownloadTask.java │ │ │ │ │ ├── HavenoInstaller.java │ │ │ │ │ └── VerifyTask.java │ │ │ │ ├── portfolio/ │ │ │ │ │ ├── PortfolioView.fxml │ │ │ │ │ ├── PortfolioView.java │ │ │ │ │ ├── cloneoffer/ │ │ │ │ │ │ ├── CloneOfferDataModel.java │ │ │ │ │ │ ├── CloneOfferView.fxml │ │ │ │ │ │ ├── CloneOfferView.java │ │ │ │ │ │ └── CloneOfferViewModel.java │ │ │ │ │ ├── closedtrades/ │ │ │ │ │ │ ├── ClosedTradableListItem.java │ │ │ │ │ │ ├── ClosedTradesDataModel.java │ │ │ │ │ │ ├── ClosedTradesListItem.java │ │ │ │ │ │ ├── ClosedTradesView.fxml │ │ │ │ │ │ ├── ClosedTradesView.java │ │ │ │ │ │ └── ClosedTradesViewModel.java │ │ │ │ │ ├── duplicateoffer/ │ │ │ │ │ │ ├── DuplicateOfferDataModel.java │ │ │ │ │ │ ├── DuplicateOfferView.fxml │ │ │ │ │ │ ├── DuplicateOfferView.java │ │ │ │ │ │ └── DuplicateOfferViewModel.java │ │ │ │ │ ├── editoffer/ │ │ │ │ │ │ ├── EditOfferDataModel.java │ │ │ │ │ │ ├── EditOfferView.fxml │ │ │ │ │ │ ├── EditOfferView.java │ │ │ │ │ │ └── EditOfferViewModel.java │ │ │ │ │ ├── failedtrades/ │ │ │ │ │ │ ├── FailedTradesDataModel.java │ │ │ │ │ │ ├── FailedTradesListItem.java │ │ │ │ │ │ ├── FailedTradesView.fxml │ │ │ │ │ │ ├── FailedTradesView.java │ │ │ │ │ │ └── FailedTradesViewModel.java │ │ │ │ │ ├── openoffer/ │ │ │ │ │ │ ├── OpenOfferListItem.java │ │ │ │ │ │ ├── OpenOffersDataModel.java │ │ │ │ │ │ ├── OpenOffersView.fxml │ │ │ │ │ │ ├── OpenOffersView.java │ │ │ │ │ │ └── OpenOffersViewModel.java │ │ │ │ │ ├── pendingtrades/ │ │ │ │ │ │ ├── BuyerSubView.java │ │ │ │ │ │ ├── PendingTradesDataModel.java │ │ │ │ │ │ ├── PendingTradesListItem.java │ │ │ │ │ │ ├── PendingTradesView.fxml │ │ │ │ │ │ ├── PendingTradesView.java │ │ │ │ │ │ ├── PendingTradesViewModel.java │ │ │ │ │ │ ├── SellerSubView.java │ │ │ │ │ │ ├── TradeStepInfo.java │ │ │ │ │ │ ├── TradeSubView.java │ │ │ │ │ │ └── steps/ │ │ │ │ │ │ ├── TradeStepView.java │ │ │ │ │ │ ├── TradeWizardItem.java │ │ │ │ │ │ ├── buyer/ │ │ │ │ │ │ │ ├── BuyerStep1View.java │ │ │ │ │ │ │ ├── BuyerStep2View.java │ │ │ │ │ │ │ ├── BuyerStep3View.java │ │ │ │ │ │ │ └── BuyerStep4View.java │ │ │ │ │ │ └── seller/ │ │ │ │ │ │ ├── SellerStep1View.java │ │ │ │ │ │ ├── SellerStep2View.java │ │ │ │ │ │ ├── SellerStep3View.java │ │ │ │ │ │ └── SellerStep4View.java │ │ │ │ │ └── presentation/ │ │ │ │ │ └── PortfolioUtil.java │ │ │ │ ├── presentation/ │ │ │ │ │ ├── AccountPresentation.java │ │ │ │ │ ├── MarketPricePresentation.java │ │ │ │ │ └── SettingsPresentation.java │ │ │ │ ├── settings/ │ │ │ │ │ ├── SettingsView.fxml │ │ │ │ │ ├── SettingsView.java │ │ │ │ │ ├── about/ │ │ │ │ │ │ ├── AboutView.fxml │ │ │ │ │ │ └── AboutView.java │ │ │ │ │ ├── network/ │ │ │ │ │ │ ├── MoneroNetworkListItem.java │ │ │ │ │ │ ├── NetworkSettingsView.fxml │ │ │ │ │ │ ├── NetworkSettingsView.java │ │ │ │ │ │ └── P2pNetworkListItem.java │ │ │ │ │ └── preferences/ │ │ │ │ │ ├── PreferencesView.fxml │ │ │ │ │ ├── PreferencesView.java │ │ │ │ │ └── PreferencesViewModel.java │ │ │ │ ├── shared/ │ │ │ │ │ ├── ChatView.java │ │ │ │ │ └── PriceFeedComboBoxItem.java │ │ │ │ └── support/ │ │ │ │ ├── SupportView.fxml │ │ │ │ ├── SupportView.java │ │ │ │ └── dispute/ │ │ │ │ ├── DisputeChatPopup.java │ │ │ │ ├── DisputeView.java │ │ │ │ ├── agent/ │ │ │ │ │ ├── DisputeAgentView.java │ │ │ │ │ ├── arbitration/ │ │ │ │ │ │ ├── ArbitratorView.fxml │ │ │ │ │ │ └── ArbitratorView.java │ │ │ │ │ ├── mediation/ │ │ │ │ │ │ ├── MediatorView.fxml │ │ │ │ │ │ └── MediatorView.java │ │ │ │ │ └── refund/ │ │ │ │ │ ├── RefundAgentView.fxml │ │ │ │ │ └── RefundAgentView.java │ │ │ │ └── client/ │ │ │ │ ├── DisputeClientView.java │ │ │ │ ├── arbitration/ │ │ │ │ │ ├── ArbitrationClientView.fxml │ │ │ │ │ └── ArbitrationClientView.java │ │ │ │ ├── mediation/ │ │ │ │ │ ├── MediationClientView.fxml │ │ │ │ │ └── MediationClientView.java │ │ │ │ └── refund/ │ │ │ │ ├── RefundClientView.fxml │ │ │ │ └── RefundClientView.java │ │ │ ├── setup/ │ │ │ │ └── DesktopPersistedDataHost.java │ │ │ ├── theme-dark.css │ │ │ ├── theme-dev.css │ │ │ ├── theme-light.css │ │ │ └── util/ │ │ │ ├── AxisInlierUtils.java │ │ │ ├── Colors.java │ │ │ ├── CssTheme.java │ │ │ ├── CurrencyList.java │ │ │ ├── CurrencyListItem.java │ │ │ ├── CurrencyPredicates.java │ │ │ ├── DisplayUtils.java │ │ │ ├── FormBuilder.java │ │ │ ├── GUIProfiler.java │ │ │ ├── GUIUtil.java │ │ │ ├── ImageUtil.java │ │ │ ├── Layout.java │ │ │ ├── MovingAverageUtils.java │ │ │ ├── Transitions.java │ │ │ ├── filtering/ │ │ │ │ ├── FilterableListItem.java │ │ │ │ └── FilteringUtils.java │ │ │ ├── normalization/ │ │ │ │ └── IBANNormalizer.java │ │ │ └── validation/ │ │ │ ├── JFXInputValidator.java │ │ │ └── PasswordValidator.java │ │ └── resources/ │ │ ├── fonts/ │ │ │ └── OFL.txt │ │ ├── keys/ │ │ │ ├── 29CDFD3B.asc │ │ │ ├── 5BC5ED73.asc │ │ │ └── F379A1C6.asc │ │ └── logback.xml │ └── test/ │ ├── java/ │ │ ├── haveno/ │ │ │ └── desktop/ │ │ │ ├── AwesomeFontDemo.java │ │ │ ├── AwesomeFontDemoLauncher.java │ │ │ ├── BindingTest.java │ │ │ ├── ComponentsDemo.java │ │ │ ├── ComponentsDemoLauncher.java │ │ │ ├── GuiceSetupTest.java │ │ │ ├── MarketsPrintTool.java │ │ │ ├── MaterialDesignIconDemo.java │ │ │ ├── MaterialDesignIconDemoLauncher.java │ │ │ ├── common/ │ │ │ │ ├── fxml/ │ │ │ │ │ ├── FxmlViewLoaderTests$Malformed.fxml │ │ │ │ │ ├── FxmlViewLoaderTests$MissingFxController.fxml │ │ │ │ │ ├── FxmlViewLoaderTests$MissingFxmlViewAnnotation.fxml │ │ │ │ │ ├── FxmlViewLoaderTests$WellFormed.fxml │ │ │ │ │ └── FxmlViewLoaderTests.java │ │ │ │ └── support/ │ │ │ │ └── CachingViewLoaderTests.java │ │ │ ├── components/ │ │ │ │ └── ColoredDecimalPlacesWithZerosTextTest.java │ │ │ ├── main/ │ │ │ │ ├── funds/ │ │ │ │ │ └── transactions/ │ │ │ │ │ ├── DisplayedTransactionsTest.java │ │ │ │ │ ├── ObservableListDecoratorTest.java │ │ │ │ │ ├── TransactionAwareTradableFactoryTest.java │ │ │ │ │ └── TransactionAwareTradeTest.java │ │ │ │ ├── market/ │ │ │ │ │ ├── offerbook/ │ │ │ │ │ │ └── OfferBookChartViewModelTest.java │ │ │ │ │ ├── spread/ │ │ │ │ │ │ └── SpreadViewModelTest.java │ │ │ │ │ └── trades/ │ │ │ │ │ └── TradesChartsViewModelTest.java │ │ │ │ ├── offer/ │ │ │ │ │ ├── createoffer/ │ │ │ │ │ │ ├── CreateOfferDataModelTest.java │ │ │ │ │ │ └── CreateOfferViewModelTest.java │ │ │ │ │ └── offerbook/ │ │ │ │ │ ├── OfferBookListItemMaker.java │ │ │ │ │ └── OfferBookViewModelTest.java │ │ │ │ ├── overlays/ │ │ │ │ │ ├── OverlayTest.java │ │ │ │ │ └── windows/ │ │ │ │ │ └── downloadupdate/ │ │ │ │ │ ├── HavenoInstallerTest.java │ │ │ │ │ └── VerifyTaskTest.java │ │ │ │ └── settings/ │ │ │ │ └── preferences/ │ │ │ │ └── PreferencesViewModelTest.java │ │ │ ├── maker/ │ │ │ │ ├── CurrencyListItemMakers.java │ │ │ │ ├── OfferMaker.java │ │ │ │ ├── PreferenceMakers.java │ │ │ │ ├── PriceMaker.java │ │ │ │ ├── TradeCurrencyMakers.java │ │ │ │ └── VolumeMaker.java │ │ │ └── util/ │ │ │ ├── CurrencyListTest.java │ │ │ ├── DisplayUtilsTest.java │ │ │ ├── GUIUtilTest.java │ │ │ ├── ImmutableCoinFormatterTest.java │ │ │ ├── MovingAverageUtilsTest.java │ │ │ └── validation/ │ │ │ ├── AccountNrValidatorTest.java │ │ │ ├── AdvancedCashValidatorTest.java │ │ │ ├── BranchIdValidatorTest.java │ │ │ ├── CapitualValidatorTest.java │ │ │ ├── FiatVolumeValidatorTest.java │ │ │ ├── InteracETransferAnswerValidatorTest.java │ │ │ ├── InteracETransferQuestionValidatorTest.java │ │ │ ├── InteracETransferValidatorTest.java │ │ │ ├── LengthValidatorTest.java │ │ │ ├── NationalAccountIdValidatorTest.java │ │ │ ├── PhoneNumberValidatorTest.java │ │ │ ├── RegexValidatorTest.java │ │ │ └── XmrValidatorTest.java │ │ └── org/ │ │ └── bitcoinj/ │ │ └── core/ │ │ └── CoinMaker.java │ └── resources/ │ ├── downloadUpdate/ │ │ ├── F379A1C6.asc │ │ ├── test.txt │ │ ├── test.txt.asc │ │ ├── test_bad.txt │ │ └── test_bad.txt.asc │ └── mockito-extensions/ │ └── org.mockito.plugins.MockMaker ├── docs/ │ ├── CONTRIBUTING.md │ ├── README.md │ ├── bounties.md │ ├── create-mainnet.md │ ├── data-stores.md │ ├── deployment-guide.md │ ├── developer-guide.md │ ├── external-tor-usage.md │ ├── flatpak.md │ ├── import-haveno.md │ ├── installing.md │ ├── tor-upgrade.md │ ├── trade_protocol/ │ │ ├── trade-protocol.drawio │ │ └── trade-protocol.md │ └── user-guide.md ├── gpg_keys/ │ ├── reto_public.asc │ └── woodser.asc ├── gradle/ │ ├── README.md │ ├── verification-metadata.xml │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── inventory/ │ └── src/ │ └── main/ │ └── resources/ │ ├── inv_btc_mainnet.seednodes │ └── logback.xml ├── monitor/ │ ├── README.md │ ├── collectd.conf │ ├── haveno-monitor.service │ ├── install_collectd_debian.sh │ ├── nginx.conf │ ├── src/ │ │ └── main/ │ │ └── resources/ │ │ ├── logback.xml │ │ └── metrics.properties │ └── uninstall_collectd_debian.sh ├── p2p/ │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── haveno/ │ │ └── network/ │ │ ├── DnsLookupException.java │ │ ├── DnsLookupTor.java │ │ ├── Socks5DnsDiscovery.java │ │ ├── Socks5MultiDiscovery.java │ │ ├── Socks5ProxyProvider.java │ │ ├── Socks5SeedOnionDiscovery.java │ │ ├── crypto/ │ │ │ ├── DecryptedDataTuple.java │ │ │ ├── EncryptionService.java │ │ │ └── EncryptionServiceModule.java │ │ ├── http/ │ │ │ ├── FakeDnsResolver.java │ │ │ ├── HttpClient.java │ │ │ ├── HttpClientImpl.java │ │ │ ├── HttpException.java │ │ │ ├── HttpMethod.java │ │ │ ├── SocksConnectionSocketFactory.java │ │ │ └── SocksSSLConnectionSocketFactory.java │ │ ├── p2p/ │ │ │ ├── AckMessage.java │ │ │ ├── AckMessageSourceType.java │ │ │ ├── AnonymousMessage.java │ │ │ ├── BootstrapListener.java │ │ │ ├── BundleOfEnvelopes.java │ │ │ ├── CloseConnectionMessage.java │ │ │ ├── DecryptedDirectMessageListener.java │ │ │ ├── DecryptedMessageWithPubKey.java │ │ │ ├── DirectMessage.java │ │ │ ├── ExtendedDataSizePermission.java │ │ │ ├── FileTransferPart.java │ │ │ ├── InitialDataRequest.java │ │ │ ├── InitialDataResponse.java │ │ │ ├── NetworkNodeProvider.java │ │ │ ├── NetworkNotReadyException.java │ │ │ ├── NodeAddress.java │ │ │ ├── P2PModule.java │ │ │ ├── P2PService.java │ │ │ ├── P2PServiceListener.java │ │ │ ├── PrefixedSealedAndSignedMessage.java │ │ │ ├── SendDirectMessageListener.java │ │ │ ├── SendMailboxMessageListener.java │ │ │ ├── SendersNodeAddressMessage.java │ │ │ ├── SupportedCapabilitiesMessage.java │ │ │ ├── UidMessage.java │ │ │ ├── mailbox/ │ │ │ │ ├── IgnoredMailboxMap.java │ │ │ │ ├── IgnoredMailboxService.java │ │ │ │ ├── MailboxItem.java │ │ │ │ ├── MailboxMessage.java │ │ │ │ ├── MailboxMessageList.java │ │ │ │ └── MailboxMessageService.java │ │ │ ├── messaging/ │ │ │ │ └── DecryptedMailboxListener.java │ │ │ ├── network/ │ │ │ │ ├── BanFilter.java │ │ │ │ ├── BridgeAddressProvider.java │ │ │ │ ├── CloseConnectionReason.java │ │ │ │ ├── Connection.java │ │ │ │ ├── ConnectionListener.java │ │ │ │ ├── ConnectionState.java │ │ │ │ ├── ConnectionStatistics.java │ │ │ │ ├── DefaultPluggableTransports.java │ │ │ │ ├── DirectBindTor.java │ │ │ │ ├── HavenoRuntimeException.java │ │ │ │ ├── InboundConnection.java │ │ │ │ ├── LocalhostNetworkNode.java │ │ │ │ ├── MessageListener.java │ │ │ │ ├── NetworkNode.java │ │ │ │ ├── NewTor.java │ │ │ │ ├── OutboundConnection.java │ │ │ │ ├── PeerType.java │ │ │ │ ├── ProtoOutputStream.java │ │ │ │ ├── RuleViolation.java │ │ │ │ ├── RunningTor.java │ │ │ │ ├── Server.java │ │ │ │ ├── SetupListener.java │ │ │ │ ├── Statistic.java │ │ │ │ ├── SupportedCapabilitiesListener.java │ │ │ │ ├── TorMode.java │ │ │ │ ├── TorNetworkNode.java │ │ │ │ ├── TorNetworkNodeDirectBind.java │ │ │ │ └── TorNetworkNodeNetlayer.java │ │ │ ├── peers/ │ │ │ │ ├── BroadcastHandler.java │ │ │ │ ├── Broadcaster.java │ │ │ │ ├── PeerManager.java │ │ │ │ ├── getdata/ │ │ │ │ │ ├── GetDataRequestHandler.java │ │ │ │ │ ├── RequestDataHandler.java │ │ │ │ │ ├── RequestDataManager.java │ │ │ │ │ └── messages/ │ │ │ │ │ ├── GetDataRequest.java │ │ │ │ │ ├── GetDataResponse.java │ │ │ │ │ ├── GetUpdatedDataRequest.java │ │ │ │ │ └── PreliminaryGetDataRequest.java │ │ │ │ ├── keepalive/ │ │ │ │ │ ├── KeepAliveHandler.java │ │ │ │ │ ├── KeepAliveManager.java │ │ │ │ │ └── messages/ │ │ │ │ │ ├── KeepAliveMessage.java │ │ │ │ │ ├── Ping.java │ │ │ │ │ └── Pong.java │ │ │ │ └── peerexchange/ │ │ │ │ ├── GetPeersRequestHandler.java │ │ │ │ ├── Peer.java │ │ │ │ ├── PeerExchangeHandler.java │ │ │ │ ├── PeerExchangeManager.java │ │ │ │ ├── PeerList.java │ │ │ │ └── messages/ │ │ │ │ ├── GetPeersRequest.java │ │ │ │ ├── GetPeersResponse.java │ │ │ │ └── PeerExchangeMessage.java │ │ │ ├── seed/ │ │ │ │ └── SeedNodeRepository.java │ │ │ └── storage/ │ │ │ ├── HashMapChangedListener.java │ │ │ ├── P2PDataStorage.java │ │ │ ├── messages/ │ │ │ │ ├── AddDataMessage.java │ │ │ │ ├── AddOncePayload.java │ │ │ │ ├── AddPersistableNetworkPayloadMessage.java │ │ │ │ ├── BroadcastMessage.java │ │ │ │ ├── RefreshOfferMessage.java │ │ │ │ ├── RemoveDataMessage.java │ │ │ │ └── RemoveMailboxDataMessage.java │ │ │ ├── payload/ │ │ │ │ ├── CapabilityRequiringPayload.java │ │ │ │ ├── DateSortedTruncatablePayload.java │ │ │ │ ├── DateTolerantPayload.java │ │ │ │ ├── ExpirablePayload.java │ │ │ │ ├── MailboxStoragePayload.java │ │ │ │ ├── PersistableNetworkPayload.java │ │ │ │ ├── PersistableProtectedPayload.java │ │ │ │ ├── ProcessOncePersistableNetworkPayload.java │ │ │ │ ├── ProtectedMailboxStorageEntry.java │ │ │ │ ├── ProtectedStorageEntry.java │ │ │ │ ├── ProtectedStoragePayload.java │ │ │ │ └── RequiresOwnerIsOnlinePayload.java │ │ │ └── persistence/ │ │ │ ├── AppendOnlyDataStoreListener.java │ │ │ ├── AppendOnlyDataStoreService.java │ │ │ ├── HistoricalDataStoreService.java │ │ │ ├── MapStoreService.java │ │ │ ├── PersistableNetworkPayloadStore.java │ │ │ ├── ProtectedDataStoreService.java │ │ │ ├── RemovedPayloadsMap.java │ │ │ ├── RemovedPayloadsService.java │ │ │ ├── ResourceDataStoreService.java │ │ │ ├── SequenceNumberMap.java │ │ │ └── StoreService.java │ │ └── utils/ │ │ ├── CapabilityUtils.java │ │ └── Utils.java │ └── test/ │ ├── java/ │ │ └── haveno/ │ │ └── network/ │ │ ├── crypto/ │ │ │ └── EncryptionServiceTests.java │ │ ├── p2p/ │ │ │ ├── DummySeedNode.java │ │ │ ├── MockNode.java │ │ │ ├── PeerServiceTest.java │ │ │ ├── TestUtils.java │ │ │ ├── mocks/ │ │ │ │ ├── MockMailboxPayload.java │ │ │ │ └── MockPayload.java │ │ │ ├── network/ │ │ │ │ ├── LocalhostNetworkNodeTest.java │ │ │ │ └── TorNetworkNodeTest.java │ │ │ ├── peers/ │ │ │ │ └── PeerManagerTest.java │ │ │ └── storage/ │ │ │ ├── P2PDataStorageBuildGetDataResponseTest.java │ │ │ ├── P2PDataStorageClientAPITest.java │ │ │ ├── P2PDataStorageGetDataIntegrationTest.java │ │ │ ├── P2PDataStorageOnMessageHandlerTest.java │ │ │ ├── P2PDataStoragePersistableNetworkPayloadTest.java │ │ │ ├── P2PDataStorageProcessGetDataResponse.java │ │ │ ├── P2PDataStorageProtectedStorageEntryTest.java │ │ │ ├── P2PDataStorageRemoveExpiredTest.java │ │ │ ├── P2PDataStorageRequestDataTest.java │ │ │ ├── P2PDataStoreDisconnectTest.java │ │ │ ├── TestState.java │ │ │ ├── messages/ │ │ │ │ └── AddDataMessageTest.java │ │ │ ├── mocks/ │ │ │ │ ├── AppendOnlyDataStoreServiceFake.java │ │ │ │ ├── ClockFake.java │ │ │ │ ├── DateTolerantPayloadStub.java │ │ │ │ ├── ExpirableProtectedStoragePayloadStub.java │ │ │ │ ├── MapStoreServiceFake.java │ │ │ │ ├── MockData.java │ │ │ │ ├── PersistableExpirableProtectedStoragePayloadStub.java │ │ │ │ ├── PersistableNetworkPayloadStub.java │ │ │ │ └── ProtectedStoragePayloadStub.java │ │ │ └── payload/ │ │ │ ├── ProtectedMailboxStorageEntryTest.java │ │ │ └── ProtectedStorageEntryTest.java │ │ └── utils/ │ │ └── UtilsTest.java │ └── resources/ │ └── mockito-extensions/ │ └── org.mockito.plugins.MockMaker ├── proto/ │ └── src/ │ └── main/ │ └── proto/ │ ├── grpc.proto │ └── pb.proto ├── relay/ │ ├── Procfile │ ├── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── haveno/ │ │ │ └── relay/ │ │ │ ├── RelayMain.java │ │ │ └── RelayService.java │ │ └── resources/ │ │ ├── logback.xml │ │ └── version.txt │ └── torrc ├── scripts/ │ ├── deployment/ │ │ ├── haveno-pricenode.env │ │ ├── haveno-pricenode.service │ │ ├── haveno-seednode.service │ │ ├── haveno-seednode2.service │ │ ├── monero-stagenet.service │ │ ├── private-stagenet.conf │ │ ├── private-stagenet.service │ │ ├── run-arbitrator-daemon.sh │ │ ├── run-arbitrator-gui.sh │ │ └── shared-stagenet.conf │ ├── install_java.bat │ ├── install_java.sh │ ├── install_tails/ │ │ ├── README.md │ │ ├── assets/ │ │ │ ├── exec.sh │ │ │ ├── haveno.desktop │ │ │ ├── haveno.yml │ │ │ └── install.sh │ │ ├── deprecated/ │ │ │ ├── README.md │ │ │ └── haveno-install.sh │ │ └── haveno-install.sh │ └── install_whonix_qubes/ │ ├── INSTALL.md │ ├── README.md │ └── scripts/ │ ├── 0-dom0/ │ │ ├── 0.0-dom0.sh │ │ ├── 0.1-dom0.sh │ │ ├── 0.2-dom0.sh │ │ └── 0.3-dom0.sh │ ├── 1-TemplateVM/ │ │ └── 1.0-haveno-templatevm.sh │ ├── 2-NetVM/ │ │ └── 2.0-haveno-netvm.sh │ └── 3-AppVM/ │ └── 3.0-haveno-appvm.sh ├── seednode/ │ ├── .dockerignore │ ├── README.md │ ├── blocknotify.sh │ ├── create_jar.sh │ ├── create_jaronly_archive.sh │ ├── docker/ │ │ └── Dockerfile │ ├── haveno-seednode.service │ ├── haveno.env │ ├── install_seednode_debian.sh │ ├── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── haveno/ │ │ │ │ └── seednode/ │ │ │ │ ├── SeedNode.java │ │ │ │ └── SeedNodeMain.java │ │ │ └── resources/ │ │ │ └── logback.xml │ │ └── test/ │ │ └── java/ │ │ └── haveno/ │ │ └── seednode/ │ │ └── GuiceSetupTest.java │ ├── torrc │ └── uninstall_seednode_debian.sh ├── settings.gradle ├── shell.nix └── statsnode/ └── src/ └── main/ ├── java/ │ └── haveno/ │ └── statistics/ │ ├── Statistics.java │ └── StatisticsMain.java └── resources/ └── logback.xml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = space indent_size = 4 continuation_indent_size = 8 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.diff] trim_trailing_whitespace = false [Makefile] indent_style = tab [*.bat] end_of_line = crlf [build.gradle] continuation_indent_size = 4 [.idea/codeStyles/*.xml] indent_size = 2 insert_final_newline = false ================================================ FILE: .gitattributes ================================================ # Auto detect text files and normalize line endings to LF # This will handle all files NOT found below * text=auto # These text files should retain Windows line endings (CRLF) *.bat text eol=crlf # These binary files should be left untouched # (binary is a macro for -text -diff) *.bmp binary *.gif binary *.ico binary *.jar binary *.jpg binary *.jpeg binary *.png binary ================================================ FILE: .github/CODEOWNERS ================================================ # This doc specifies who gets requested to review GitHub pull requests. # See https://help.github.com/articles/about-codeowners/. # Default * @woodser # Documentation /docs/ @haveno-dex/core-team *.md @haveno-dex/core-team ================================================ FILE: .github/workflows/build.yml ================================================ # GitHub Releases requires a tag, e.g: # git tag -s 1.0.19-1 -m "haveno-v1.0.19-1" # git push origin 1.0.19-1 name: CI on: workflow_dispatch: push: pull_request: paths-ignore: - '**/README.md' jobs: build: strategy: matrix: os: [ubuntu-24.04, ubuntu-24.04-arm, macos-15-intel, macos-15, windows-2025] include: - os: ubuntu-24.04 arch: x86_64 - os: ubuntu-24.04-arm arch: aarch64 - os: macos-15-intel arch: x86_64 - os: macos-15 arch: aarch64 - os: windows-2025 arch: x86_64 fail-fast: false runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 with: lfs: true - name: Set up JDK 21 uses: actions/setup-java@v4 with: java-version: '21' distribution: 'adopt' cache: gradle - name: Create local share directory # ubuntu-24.04 Github runners do not have `~/.local/share` directory by default. # This causes issues when testing `FileTransferSend` if: runner.os == 'Linux' run: mkdir -p ~/.local/share - name: Build with Gradle with tests if: ${{ !(runner.os == 'Linux' && matrix.arch == 'aarch64') }} run: ./gradlew build --stacktrace --scan - name: Build with Gradle, skip Desktop tests # JavaFX `21.x.x` ships with `x86_64` versions of # `libprism_es2.so` and `libprism_s2.so` shared objects. # This causes desktop tests to fail on `linux/aarch64` if: ${{ (runner.os == 'Linux' && matrix.arch == 'aarch64') }} run: | ./gradlew build --stacktrace --scan -x desktop:test echo "::warning title=Desktop Tests Skipped::Desktop tests (desktop:test) were \ intentionally skipped for linux/aarch64 builds as JavaFX 21.x.x ships with x86_64 \ shared objects, causing tests to fail. \ This should be revisited when JavaFX is next updated." - uses: actions/upload-artifact@v4 if: failure() with: name: error-reports-${{ matrix.os }} path: ${{ github.workspace }}/desktop/build/reports - name: cache nodes dependencies uses: actions/upload-artifact@v4 with: include-hidden-files: true name: cached-localnet-${{ matrix.os }} path: .localnet overwrite: true - name: Install dependencies if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y rpm libfuse2 flatpak flatpak-builder appstream flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo - name: Install WiX Toolset if: runner.os == 'Windows' run: | Invoke-WebRequest -Uri 'https://github.com/wixtoolset/wix3/releases/download/wix314rtm/wix314.exe' -OutFile wix314.exe .\wix314.exe /quiet /norestart shell: powershell - name: Build Haveno Installer with tests if: ${{ !(runner.os == 'Linux' && matrix.arch == 'aarch64') }} run: ./gradlew clean build --refresh-keys --refresh-dependencies working-directory: . - name: Build Haveno Installer, skip Desktop tests # JavaFX `21.x.x` ships with `x86_64` versions of # `libprism_es2.so` and `libprism_s2.so` shared objects. # This causes desktop tests to fail on `linux/aarch64` if: ${{ (runner.os == 'Linux' && matrix.arch == 'aarch64') }} run: | ./gradlew clean build --refresh-keys --refresh-dependencies -x desktop:test echo "::warning title=Desktop Tests Skipped::Desktop tests (desktop:test) were \ intentionally skipped for linux/aarch64 builds as JavaFX 21.x.x ships with x86_64 \ shared objects, causing tests to fail. \ This should be revisited when JavaFX is next updated." working-directory: . - name: Package Haveno Installer run: ./gradlew packageInstallers working-directory: . - name: Build Daemon JAR if: > (runner.os == 'Linux' && (matrix.arch == 'x86_64' || matrix.arch == 'aarch64')) || (runner.os == 'Windows') run: ./gradlew :daemon:shadowJar -x test -x checkstyleMain -x checkstyleTest --stacktrace working-directory: . # get version from jar - name: Set Version Unix if: runner.os != 'Windows' run: | export VERSION=$(ls desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 | grep -Eo 'desktop-[0-9]+\.[0-9]+\.[0-9]+' | sed 's/desktop-//') echo "VERSION=$VERSION" >> $GITHUB_ENV - name: Set Version Windows if: runner.os == 'Windows' run: | $VERSION = (Get-ChildItem -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256).Name -replace 'desktop-', '' -replace '-.*', '' "VERSION=$VERSION" | Out-File -FilePath $env:GITHUB_ENV -Append shell: powershell - name: Move Release Files for Linux if: runner.os == 'Linux' run: | mkdir ${{ github.workspace }}/release-linux-rpm mkdir ${{ github.workspace }}/release-linux-deb mkdir ${{ github.workspace }}/release-linux-flatpak mkdir ${{ github.workspace }}/release-linux-appimage mv desktop/build/temp-*/binaries/haveno-*.rpm ${{ github.workspace }}/release-linux-rpm/haveno-v${{ env.VERSION }}-linux-${{ matrix.arch }}-installer.rpm mv desktop/build/temp-*/binaries/haveno_*.deb ${{ github.workspace }}/release-linux-deb/haveno-v${{ env.VERSION }}-linux-${{ matrix.arch }}-installer.deb mv desktop/build/temp-*/binaries/*.flatpak ${{ github.workspace }}/release-linux-flatpak/haveno-v${{ env.VERSION }}-linux-${{ matrix.arch }}.flatpak mv desktop/build/temp-*/binaries/haveno_*.AppImage ${{ github.workspace }}/release-linux-appimage/haveno-v${{ env.VERSION }}-linux-${{ matrix.arch }}.AppImage cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-deb cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-rpm cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-appimage cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-flatpak cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-${{ matrix.arch }}-SNAPSHOT-all.jar.SHA-256 shell: bash - name: Move Release Files for macOS if: runner.os == 'MacOS' run: | mkdir ${{ github.workspace }}/release-macos-${{ matrix.arch }} mv desktop/build/temp-*/binaries/Haveno-*.dmg ${{ github.workspace }}/release-macos-${{ matrix.arch }}/haveno-v${{ env.VERSION }}-macos-${{ matrix.arch }}-installer.dmg cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-macos-${{ matrix.arch }} cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-${{ matrix.arch }}-SNAPSHOT-all.jar.SHA-256 shell: bash - name: Move Release Files on Windows if: runner.os == 'Windows' run: | mkdir ${{ github.workspace }}/release-windows Move-Item -Path desktop\build\temp-*/binaries\Haveno-*.exe -Destination ${{ github.workspace }}/release-windows/haveno-v${{ env.VERSION }}-windows-${{ matrix.arch }}-installer.exe Copy-Item -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256 -Destination ${{ github.workspace }}/release-windows Move-Item -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256 -Destination ${{ github.workspace }}/haveno-v${{ env.VERSION }}-windows-SNAPSHOT-all.jar.SHA-256 shell: powershell - name: Move Daemon JAR on Linux if: runner.os == 'Linux' run: | mkdir -p ${{ github.workspace }}/release-daemon-linux-${{ matrix.arch }} mv daemon/build/libs/daemon-all.jar ${{ github.workspace }}/release-daemon-linux-${{ matrix.arch }}/daemon-linux-${{ matrix.arch }}.jar sha256sum ${{ github.workspace }}/release-daemon-linux-${{ matrix.arch }}/daemon-linux-${{ matrix.arch }}.jar > ${{ github.workspace }}/release-daemon-linux-${{ matrix.arch }}/daemon-linux-${{ matrix.arch }}.jar.SHA-256 shell: bash - name: Move Daemon JAR on Windows if: runner.os == 'Windows' run: | mkdir ${{ github.workspace }}\release-daemon-windows Move-Item daemon\build\libs\daemon-all.jar ${{ github.workspace }}\release-daemon-windows\daemon-windows.jar Get-FileHash ${{ github.workspace }}\release-daemon-windows\daemon-windows.jar -Algorithm SHA256 | ForEach-Object { $_.Hash } > ${{ github.workspace }}\release-daemon-windows\daemon-windows.jar.SHA-256 shell: powershell - uses: actions/upload-artifact@v4 name: "Linux daemon artifacts" if: runner.os == 'Linux' with: name: daemon-linux-${{ matrix.arch }} path: ${{ github.workspace }}/release-daemon-linux-${{ matrix.arch }} - uses: actions/upload-artifact@v4 name: "Windows daemon artifacts" if: runner.os == 'Windows' with: name: daemon-windows path: ${{ github.workspace }}/release-daemon-windows # Windows artifacts - uses: actions/upload-artifact@v4 name: "Windows artifacts" if: runner.os == 'Windows' with: name: haveno-windows-${{ matrix.arch }} path: ${{ github.workspace }}/release-windows # macOS artifacts - uses: actions/upload-artifact@v4 name: "macOS artifacts" if: runner.os == 'MacOS' with: name: haveno-macos-${{ matrix.arch }} path: ${{ github.workspace }}/release-macos-${{ matrix.arch }} # Linux artifacts - uses: actions/upload-artifact@v4 name: "Linux - deb artifact" if: runner.os == 'Linux' with: name: haveno-linux-${{ matrix.arch }}-deb path: ${{ github.workspace }}/release-linux-deb - uses: actions/upload-artifact@v4 name: "Linux - rpm artifact" if: runner.os == 'Linux' with: name: haveno-linux-${{ matrix.arch }}-rpm path: ${{ github.workspace }}/release-linux-rpm - uses: actions/upload-artifact@v4 name: "Linux - AppImage artifact" if: runner.os == 'Linux' with: name: haveno-linux-${{ matrix.arch }}-appimage path: ${{ github.workspace }}/release-linux-appimage - uses: actions/upload-artifact@v4 name: "Linux - flatpak artifact" if: runner.os == 'Linux' with: name: haveno-linux-${{ matrix.arch }}-flatpak path: ${{ github.workspace }}/release-linux-flatpak - name: Release uses: softprops/action-gh-release@v2 if: startsWith(github.ref, 'refs/tags/') with: files: | # Linux x86_64 ${{ github.workspace }}/release-linux-deb/haveno-v${{ env.VERSION }}-linux-x86_64-installer.deb ${{ github.workspace }}/release-linux-rpm/haveno-v${{ env.VERSION }}-linux-x86_64-installer.rpm ${{ github.workspace }}/release-linux-appimage/haveno-v${{ env.VERSION }}-linux-x86_64.AppImage ${{ github.workspace }}/release-linux-flatpak/haveno-v${{ env.VERSION }}-linux-x86_64.flatpak ${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-x86_64-SNAPSHOT-all.jar.SHA-256 # Linux aarch64 ${{ github.workspace }}/release-linux-deb/haveno-v${{ env.VERSION }}-linux-aarch64-installer.deb ${{ github.workspace }}/release-linux-rpm/haveno-v${{ env.VERSION }}-linux-aarch64-installer.rpm ${{ github.workspace }}/release-linux-appimage/haveno-v${{ env.VERSION }}-linux-aarch64.AppImage ${{ github.workspace }}/release-linux-flatpak/haveno-v${{ env.VERSION }}-linux-aarch64.flatpak ${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-aarch64-SNAPSHOT-all.jar.SHA-256 # macOS x86_64 ${{ github.workspace }}/release-macos-x86_64/haveno-v${{ env.VERSION }}-macos-x86_64-installer.dmg ${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-x86_64-SNAPSHOT-all.jar.SHA-256 # macOS aarch64 ${{ github.workspace }}/release-macos-aarch64/haveno-v${{ env.VERSION }}-macos-aarch64-installer.dmg ${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-aarch64-SNAPSHOT-all.jar.SHA-256 # Windows ${{ github.workspace }}/release-windows/haveno-v${{ env.VERSION }}-windows-x86_64-installer.exe ${{ github.workspace }}/haveno-v${{ env.VERSION }}-windows-SNAPSHOT-all.jar.SHA-256 # Daemon JARs ${{ github.workspace }}/release-daemon-linux-x86_64/daemon-linux-x86_64.jar ${{ github.workspace }}/release-daemon-linux-x86_64/daemon-linux-x86_64.jar.SHA-256 ${{ github.workspace }}/release-daemon-linux-aarch64/daemon-linux-aarch64.jar ${{ github.workspace }}/release-daemon-linux-aarch64/daemon-linux-aarch64.jar.SHA-256 ${{ github.workspace }}/release-daemon-windows/daemon-windows.jar ${{ github.workspace }}/release-daemon-windows/daemon-windows.jar.SHA-256 # https://git-scm.com/docs/git-tag - git-tag Docu # # git tag - lists all local tags # git tag -d 1.0.19-1 - delete local tag # # git ls-remote --tags - lists all remote tags # git push origin --delete refs/tags/1.0.19-1 - delete remote tag ================================================ FILE: .github/workflows/codacy-code-reporter.yml ================================================ name: Codacy Coverage Reporter on: ["push"] permissions: contents: read jobs: build: if: github.repository == 'haveno-dex/haveno' name: Publish coverage runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Set up JDK 21 uses: actions/setup-java@v4 with: java-version: '21' distribution: 'adopt' - name: Create local share directory # ubuntu-24.04 Github runners do not have `~/.local/share` directory by default. # This causes issues when testing `FileTransferSend` run: mkdir -p ~/.local/share - name: Build with Gradle run: ./gradlew clean build -x checkstyleMain -x checkstyleTest -x shadowJar - name: Run codacy coverage reporter uses: codacy/codacy-coverage-reporter-action@v1.3.0 with: project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} coverage-reports: ${{ github.workspace }}/build/reports/jacoco/testCodeCoverageReport/testCodeCoverageReport.xml ================================================ FILE: .github/workflows/codeql-analysis.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # name: "CodeQL" on: push: branches: [ master ] pull_request: # The branches below must be a subset of the branches above branches: [ master ] schedule: - cron: '43 21 * * 0' jobs: analyze: name: Analyze runs-on: ubuntu-24.04 permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'java' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://git.io/codeql-language-support steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up JDK 21 uses: actions/setup-java@v4 with: java-version: '21' distribution: 'adopt' cache: gradle # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below). # - name: Autobuild # uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language - name: Build run: ./gradlew build --stacktrace -x test -x checkstyleMain -x checkstyleTest - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 ================================================ FILE: .github/workflows/label.yml ================================================ name: Comment to label on: issues: types: [labeled] workflow_call: jobs: issueLabeled: runs-on: ubuntu-24.04 steps: - name: Bounty explanation uses: peter-evans/create-or-update-comment@v3 if: github.event.label.name == '💰bounty' with: token: ${{ secrets.GITHUB_TOKEN }} issue-number: ${{ github.event.issue.number }} body: > There is a bounty on this issue. The amount is in the title. The reward will be awarded to the first person or group of people whose solution is accepted and merged. In some cases, we may assign the issue to specific contributors. We expect contributors to provide a PR in a reasonable time frame or, in case of an extensive work, updates on their progress. We will unassign the issue if we feel the assignee is not responsive or has abandoned the task. Read the [full conditions and details](https://github.com/haveno-dex/haveno/blob/master/docs/bounties.md) of our bounty system. ================================================ FILE: .gitignore ================================================ bin/ */docs */log */out .idea !.idea/copyright/haveno_Affero_GPLv3.xml !.idea/copyright/profiles_settings.xml !.idea/codeStyleSettings.xml *.iml *.spvchain *.wallet *.ser *.log *.sw[op] .DS_Store .gradle build .classpath .project .settings *.java.hsp *.java.hsw *.~ava /bundles /haveno-* /lib /xchange desktop.ini */target/* *.class deploy */releases/* /monitor/TorHiddenServiceStartupTimeTests/* /monitor/monitor-tor/* .java-version .localnet .vscode .vim/* */.factorypath .flatpak-builder exchange.haveno.Haveno.yaml ================================================ FILE: LICENSE ================================================ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Copyright (C) 2020 Haveno Dex Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program 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. This program 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 this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . ================================================ FILE: Makefile ================================================ # See docs/installing.md build: localnet haveno clean: ./gradlew clean clean-localnet: rm -rf .localnet localnet: mkdir -p .localnet haveno: ./gradlew build update-dependencies: ./gradlew --refresh-dependencies && ./gradlew --write-verification-metadata sha256 daemon: localnet ./gradlew :daemon:shadowJar -x test -x checkstyleMain -x checkstyleTest # build haveno without tests skip-tests: localnet ./gradlew build -x test -x checkstyleMain -x checkstyleTest # quick build desktop and daemon apps without tests haveno-apps: ./gradlew :core:compileJava :desktop:build -x test -x checkstyleMain -x checkstyleTest refresh-deps: ./gradlew --write-verification-metadata sha256 && ./gradlew build --refresh-keys --refresh-dependencies -x test -x checkstyleMain -x checkstyleTest deploy-screen: # create a new screen session named 'localnet' screen -dmS localnet # deploy each node in its own named screen window for target in \ seednode-local \ user1-desktop-local \ user2-desktop-local \ arbitrator-desktop-local; do \ screen -S localnet -X screen -t $$target; \ screen -S localnet -p $$target -X stuff "make $$target\n"; \ done; # give time to start sleep 5 deploy-tmux: # Start a new tmux session named 'localnet' (detached) tmux new-session -d -s localnet -n main "make seednode-local" # Split the window into panes and run each node in its own pane tmux split-window -h -t localnet "make user1-desktop-local" # Split horizontally for user1 tmux split-window -v -t localnet:0.0 "make user2-desktop-local" # Split vertically on the left for user2 tmux split-window -v -t localnet:0.1 "make arbitrator-desktop-local" # Split vertically on the right for arbitrator tmux select-layout -t localnet tiled # give time to start sleep 5 # Attach to the tmux session tmux attach-session -t localnet .PHONY: build seednode localnet # Local network monerod1-local: ./.localnet/monerod \ --testnet \ --no-igd \ --hide-my-port \ --data-dir .localnet/xmr_local/node1 \ --p2p-bind-ip 127.0.0.1 \ --log-level 0 \ --add-exclusive-node 127.0.0.1:48080 \ --add-exclusive-node 127.0.0.1:58080 \ --rpc-access-control-origins http://localhost:8080 \ --fixed-difficulty 500 \ --disable-rpc-ban \ --rpc-max-connections 1000 \ --max-connections-per-ip 10 \ --rpc-max-connections-per-private-ip 1000 \ monerod2-local: ./.localnet/monerod \ --testnet \ --no-igd \ --hide-my-port \ --data-dir .localnet/xmr_local/node2 \ --p2p-bind-ip 127.0.0.1 \ --p2p-bind-port 48080 \ --rpc-bind-port 48081 \ --zmq-rpc-bind-port 48082 \ --log-level 0 \ --add-exclusive-node 127.0.0.1:28080 \ --add-exclusive-node 127.0.0.1:58080 \ --rpc-access-control-origins http://localhost:8080 \ --fixed-difficulty 500 \ --disable-rpc-ban \ --rpc-max-connections 1000 \ --max-connections-per-ip 10 \ --rpc-max-connections-per-private-ip 1000 \ monerod3-local: ./.localnet/monerod \ --testnet \ --no-igd \ --hide-my-port \ --data-dir .localnet/xmr_local/node3 \ --p2p-bind-ip 127.0.0.1 \ --p2p-bind-port 58080 \ --rpc-bind-port 58081 \ --zmq-rpc-bind-port 58082 \ --log-level 0 \ --add-exclusive-node 127.0.0.1:28080 \ --add-exclusive-node 127.0.0.1:48080 \ --rpc-access-control-origins http://localhost:8080 \ --fixed-difficulty 500 \ --disable-rpc-ban \ --rpc-max-connections 1000 \ --max-connections-per-ip 10 \ --rpc-max-connections-per-private-ip 1000 \ #--proxy 127.0.0.1:49775 \ funding-wallet-local: ./.localnet/monero-wallet-rpc \ --testnet \ --daemon-address http://localhost:28081 \ --rpc-bind-port 28084 \ --rpc-login rpc_user:abc123 \ --rpc-access-control-origins http://localhost:8080 \ --wallet-dir ./.localnet \ funding-wallet-stagenet: ./.localnet/monero-wallet-rpc \ --stagenet \ --rpc-bind-port 38084 \ --rpc-login rpc_user:abc123 \ --rpc-access-control-origins http://localhost:8080 \ --wallet-dir ./.localnet \ --daemon-ssl-allow-any-cert \ --daemon-address http://127.0.0.1:38081 \ funding-wallet-mainnet: ./.localnet/monero-wallet-rpc \ --rpc-bind-port 18084 \ --rpc-login rpc_user:abc123 \ --rpc-access-control-origins http://localhost:8080 \ --wallet-dir ./.localnet \ # use .bat extension for windows binaries APP_EXT := ifeq ($(OS),Windows_NT) APP_EXT := .bat endif seednode-local: ./haveno-seednode$(APP_EXT) \ --baseCurrencyNetwork=XMR_LOCAL \ --useLocalhostForP2P=true \ --useDevPrivilegeKeys=true \ --nodePort=2002 \ --appName=haveno-XMR_LOCAL_Seed_2002 \ --xmrNode=http://localhost:28081 \ seednode2-local: ./haveno-seednode$(APP_EXT) \ --baseCurrencyNetwork=XMR_LOCAL \ --useLocalhostForP2P=true \ --useDevPrivilegeKeys=true \ --nodePort=2003 \ --appName=haveno-XMR_LOCAL_Seed_2003 \ --xmrNode=http://localhost:28081 \ arbitrator-daemon-local: # Arbitrator needs to be registered before making trades ./haveno-daemon$(APP_EXT) \ --baseCurrencyNetwork=XMR_LOCAL \ --useLocalhostForP2P=true \ --useDevPrivilegeKeys=true \ --nodePort=4444 \ --appName=haveno-XMR_LOCAL_arbitrator \ --apiPassword=apitest \ --apiPort=9998 \ --passwordRequired=false \ --useNativeXmrWallet=false \ --xmrNode=http://127.0.0.1:48081 \ arbitrator-desktop-local: # Arbitrator needs to be registered before making trades ./haveno-desktop$(APP_EXT) \ --baseCurrencyNetwork=XMR_LOCAL \ --useLocalhostForP2P=true \ --useDevPrivilegeKeys=true \ --nodePort=4444 \ --appName=haveno-XMR_LOCAL_arbitrator \ --apiPassword=apitest \ --apiPort=9998 \ --useNativeXmrWallet=false \ --xmrNode=http://127.0.0.1:48081 \ arbitrator2-daemon-local: # Arbitrator needs to be registered before making trades ./haveno-daemon$(APP_EXT) \ --baseCurrencyNetwork=XMR_LOCAL \ --useLocalhostForP2P=true \ --useDevPrivilegeKeys=true \ --nodePort=7777 \ --appName=haveno-XMR_LOCAL_arbitrator2 \ --apiPassword=apitest \ --apiPort=10001 \ --useNativeXmrWallet=false \ --xmrNode=http://127.0.0.1:48081 \ arbitrator2-desktop-local: # Arbitrator needs to be registered before making trades ./haveno-desktop$(APP_EXT) \ --baseCurrencyNetwork=XMR_LOCAL \ --useLocalhostForP2P=true \ --useDevPrivilegeKeys=true \ --nodePort=7777 \ --appName=haveno-XMR_LOCAL_arbitrator2 \ --apiPassword=apitest \ --apiPort=10001 \ --useNativeXmrWallet=false \ --xmrNode=http://127.0.0.1:48081 \ user1-daemon-local: ./haveno-daemon$(APP_EXT) \ --baseCurrencyNetwork=XMR_LOCAL \ --useLocalhostForP2P=true \ --useDevPrivilegeKeys=true \ --nodePort=5555 \ --appName=haveno-XMR_LOCAL_user1 \ --apiPassword=apitest \ --apiPort=9999 \ --walletRpcBindPort=38091 \ --passwordRequired=false \ --useNativeXmrWallet=false \ user1-desktop-local: ./haveno-desktop$(APP_EXT) \ --baseCurrencyNetwork=XMR_LOCAL \ --useLocalhostForP2P=true \ --useDevPrivilegeKeys=true \ --nodePort=5555 \ --appName=haveno-XMR_LOCAL_user1 \ --apiPassword=apitest \ --apiPort=9999 \ --walletRpcBindPort=38091 \ --logLevel=info \ --useNativeXmrWallet=false \ user2-desktop-local: ./haveno-desktop$(APP_EXT) \ --baseCurrencyNetwork=XMR_LOCAL \ --useLocalhostForP2P=true \ --useDevPrivilegeKeys=true \ --nodePort=6666 \ --appName=haveno-XMR_LOCAL_user2 \ --apiPassword=apitest \ --apiPort=10000 \ --walletRpcBindPort=38092 \ --useNativeXmrWallet=false \ user2-daemon-local: ./haveno-daemon$(APP_EXT) \ --baseCurrencyNetwork=XMR_LOCAL \ --useLocalhostForP2P=true \ --useDevPrivilegeKeys=true \ --nodePort=6666 \ --appName=haveno-XMR_LOCAL_user2 \ --apiPassword=apitest \ --apiPort=10000 \ --walletRpcBindPort=38092 \ --passwordRequired=false \ --useNativeXmrWallet=false \ user3-desktop-local: ./haveno-desktop$(APP_EXT) \ --baseCurrencyNetwork=XMR_LOCAL \ --useLocalhostForP2P=true \ --useDevPrivilegeKeys=true \ --nodePort=7778 \ --appName=haveno-XMR_LOCAL_user3 \ --apiPassword=apitest \ --apiPort=10002 \ --walletRpcBindPort=38093 \ --useNativeXmrWallet=false \ user3-daemon-local: ./haveno-daemon$(APP_EXT) \ --baseCurrencyNetwork=XMR_LOCAL \ --useLocalhostForP2P=true \ --useDevPrivilegeKeys=true \ --nodePort=7778 \ --appName=haveno-XMR_LOCAL_user3 \ --apiPassword=apitest \ --apiPort=10002 \ --walletRpcBindPort=38093 \ --passwordRequired=false \ --useNativeXmrWallet=false \ # Stagenet network monerod-stagenet: ./.localnet/monerod \ --stagenet \ --bootstrap-daemon-address auto \ --rpc-access-control-origins http://localhost:8080 \ monerod-stagenet-custom: ./.localnet/monerod \ --stagenet \ --no-zmq \ --p2p-bind-port 39080 \ --rpc-bind-port 39081 \ --bootstrap-daemon-address auto \ --rpc-access-control-origins http://localhost:8080 \ seednode-stagenet: ./haveno-seednode$(APP_EXT) \ --baseCurrencyNetwork=XMR_STAGENET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=3002 \ --appName=haveno-XMR_STAGENET_Seed_3002 \ --xmrNode=http://127.0.0.1:38081 \ seednode2-stagenet: ./haveno-seednode$(APP_EXT) \ --baseCurrencyNetwork=XMR_STAGENET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=3003 \ --appName=haveno-XMR_STAGENET_Seed_3003 \ --xmrNode=http://127.0.0.1:38081 \ arbitrator-daemon-stagenet: # Arbitrator needs to be registered before making trades ./haveno-daemon$(APP_EXT) \ --baseCurrencyNetwork=XMR_STAGENET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=9999 \ --appName=haveno-XMR_STAGENET_arbitrator \ --apiPassword=apitest \ --apiPort=3200 \ --passwordRequired=false \ --xmrNode=http://127.0.0.1:38081 \ --useNativeXmrWallet=false \ # Arbitrator needs to be registered before making trades arbitrator-desktop-stagenet: ./haveno-desktop$(APP_EXT) \ --baseCurrencyNetwork=XMR_STAGENET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=9999 \ --appName=haveno-XMR_STAGENET_arbitrator \ --apiPassword=apitest \ --apiPort=3200 \ --xmrNode=http://127.0.0.1:38081 \ --useNativeXmrWallet=false \ user1-daemon-stagenet: ./haveno-daemon$(APP_EXT) \ --baseCurrencyNetwork=XMR_STAGENET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=9999 \ --appName=haveno-XMR_STAGENET_user1 \ --apiPassword=apitest \ --apiPort=3201 \ --passwordRequired=false \ --useNativeXmrWallet=false \ user1-desktop-stagenet: ./haveno-desktop$(APP_EXT) \ --baseCurrencyNetwork=XMR_STAGENET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=9999 \ --appName=haveno-XMR_STAGENET_user1 \ --apiPassword=apitest \ --apiPort=3201 \ --useNativeXmrWallet=false \ user2-daemon-stagenet: ./haveno-daemon$(APP_EXT) \ --baseCurrencyNetwork=XMR_STAGENET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=9999 \ --appName=haveno-XMR_STAGENET_user2 \ --apiPassword=apitest \ --apiPort=3202 \ --passwordRequired=false \ --useNativeXmrWallet=false \ user2-desktop-stagenet: ./haveno-desktop$(APP_EXT) \ --baseCurrencyNetwork=XMR_STAGENET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=9999 \ --appName=haveno-XMR_STAGENET_user2 \ --apiPassword=apitest \ --apiPort=3202 \ --useNativeXmrWallet=false \ user3-desktop-stagenet: ./haveno-desktop$(APP_EXT) \ --baseCurrencyNetwork=XMR_STAGENET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=9999 \ --appName=haveno-XMR_STAGENET_user3 \ --apiPassword=apitest \ --apiPort=3203 \ --useNativeXmrWallet=false \ haveno-desktop-stagenet: ./haveno-desktop$(APP_EXT) \ --baseCurrencyNetwork=XMR_STAGENET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=9999 \ --appName=Haveno \ --apiPassword=apitest \ --apiPort=3204 \ --useNativeXmrWallet=false \ haveno-daemon-stagenet: ./haveno-daemon$(APP_EXT) \ --baseCurrencyNetwork=XMR_STAGENET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=9999 \ --appName=Haveno \ --apiPassword=apitest \ --apiPort=3204 \ --useNativeXmrWallet=false \ # Mainnet network monerod: ./.localnet/monerod \ --bootstrap-daemon-address auto \ --rpc-access-control-origins http://localhost:8080 \ --rpc-max-connections 1000 \ --max-connections-per-ip 10 \ --rpc-max-connections-per-private-ip 1000 \ seednode: ./haveno-seednode$(APP_EXT) \ --baseCurrencyNetwork=XMR_MAINNET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=1002 \ --appName=haveno-reto-XMR_MAINNET_Seed_1002 \ --xmrNode=http://127.0.0.1:18081 \ seednode2: ./haveno-seednode$(APP_EXT) \ --baseCurrencyNetwork=XMR_MAINNET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=1003 \ --appName=haveno-reto-XMR_MAINNET_Seed_1003 \ --xmrNode=http://127.0.0.1:18081 \ arbitrator-daemon-mainnet: # Arbitrator needs to be registered before making trades ./haveno-daemon$(APP_EXT) \ --baseCurrencyNetwork=XMR_MAINNET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=9999 \ --appName=haveno-reto-XMR_MAINNET_arbitrator \ --apiPassword=apitest \ --apiPort=1200 \ --passwordRequired=false \ --xmrNode=http://127.0.0.1:18081 \ --useNativeXmrWallet=false \ arbitrator-desktop-mainnet: ./haveno-desktop$(APP_EXT) \ --baseCurrencyNetwork=XMR_MAINNET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=9999 \ --appName=haveno-reto-XMR_MAINNET_arbitrator \ --apiPassword=apitest \ --apiPort=1200 \ --xmrNode=http://127.0.0.1:18081 \ --useNativeXmrWallet=false \ arbitrator2-daemon-mainnet: ./haveno-daemon$(APP_EXT) \ --baseCurrencyNetwork=XMR_MAINNET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=9999 \ --appName=haveno-XMR_MAINNET_arbitrator2 \ --apiPassword=apitest \ --apiPort=1205 \ --passwordRequired=false \ --xmrNode=http://127.0.0.1:18081 \ --useNativeXmrWallet=false \ arbitrator2-desktop-mainnet: ./haveno-desktop$(APP_EXT) \ --baseCurrencyNetwork=XMR_MAINNET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=9999 \ --appName=haveno-XMR_MAINNET_arbitrator2 \ --apiPassword=apitest \ --apiPort=1205 \ --xmrNode=http://127.0.0.1:18081 \ --useNativeXmrWallet=false \ haveno-daemon-mainnet: ./haveno-daemon$(APP_EXT) \ --baseCurrencyNetwork=XMR_MAINNET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=9999 \ --appName=Haveno-reto \ --apiPassword=apitest \ --apiPort=1201 \ --useNativeXmrWallet=false \ --ignoreLocalXmrNode=false \ haveno-desktop-mainnet: ./haveno-desktop$(APP_EXT) \ --baseCurrencyNetwork=XMR_MAINNET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=9999 \ --appName=Haveno-reto \ --apiPassword=apitest \ --apiPort=1201 \ --useNativeXmrWallet=false \ --ignoreLocalXmrNode=false \ user1-daemon-mainnet: ./haveno-daemon$(APP_EXT) \ --baseCurrencyNetwork=XMR_MAINNET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=9999 \ --appName=haveno-reto-XMR_MAINNET_user1 \ --apiPassword=apitest \ --apiPort=1202 \ --passwordRequired=false \ --useNativeXmrWallet=false \ --ignoreLocalXmrNode=false \ user1-desktop-mainnet: ./haveno-desktop$(APP_EXT) \ --baseCurrencyNetwork=XMR_MAINNET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=9999 \ --appName=haveno-reto-XMR_MAINNET_user1 \ --apiPassword=apitest \ --apiPort=1202 \ --useNativeXmrWallet=false \ --ignoreLocalXmrNode=false \ user2-daemon-mainnet: ./haveno-daemon$(APP_EXT) \ --baseCurrencyNetwork=XMR_MAINNET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=9999 \ --appName=haveno-reto-XMR_MAINNET_user2 \ --apiPassword=apitest \ --apiPort=1203 \ --passwordRequired=false \ --useNativeXmrWallet=false \ --ignoreLocalXmrNode=false \ user2-desktop-mainnet: ./haveno-desktop$(APP_EXT) \ --baseCurrencyNetwork=XMR_MAINNET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=9999 \ --appName=haveno-reto-XMR_MAINNET_user2 \ --apiPassword=apitest \ --apiPort=1203 \ --useNativeXmrWallet=false \ --ignoreLocalXmrNode=false \ user3-desktop-mainnet: ./haveno-desktop$(APP_EXT) \ --baseCurrencyNetwork=XMR_MAINNET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=9999 \ --appName=haveno-reto-XMR_MAINNET_user3 \ --apiPassword=apitest \ --apiPort=1204 \ --useNativeXmrWallet=false \ --ignoreLocalXmrNode=false \ buyer-wallet-mainnet: ./.localnet/monero-wallet-rpc \ --daemon-address http://localhost:18081 \ --rpc-bind-port 18084 \ --rpc-login rpc_user:abc123 \ --rpc-access-control-origins http://localhost:8080 \ --wallet-dir ./.localnet \ seller-wallet-mainnet: ./.localnet/monero-wallet-rpc \ --daemon-address http://localhost:18081 \ --rpc-bind-port 18085 \ --rpc-login rpc_user:abc123 \ --rpc-access-control-origins http://localhost:8080 \ --wallet-dir ./.localnet \ ================================================ FILE: README.md ================================================
Haveno logo [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/haveno-dex/haveno/build.yml?branch=master)](https://github.com/haveno-dex/haveno/actions) [![GitHub issues with bounty](https://img.shields.io/github/issues-search/haveno-dex/haveno?color=%23fef2c0&label=Issues%20with%20bounties&query=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty)](https://github.com/haveno-dex/haveno/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty) [![Twitter Follow](https://img.shields.io/twitter/follow/HavenoDEX?style=social)](https://twitter.com/havenodex) [![Matrix rooms](https://img.shields.io/badge/Matrix%20room-%23haveno-blue)](https://matrix.to/#/#haveno:monero.social) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](https://github.com/haveno-dex/.github/blob/master/CODE_OF_CONDUCT.md)
## What is Haveno? Haveno (pronounced ha‧ve‧no) is an open source platform to exchange [Monero](https://getmonero.org) for fiat currencies like USD, EUR, and GBP or other cryptocurrencies like BTC, ETH, and BCH. Main features: - Communications are routed through **Tor**, to preserve your privacy. - Trades are **peer-to-peer**: trades on Haveno happen between people only, there is no central authority. - Trades are **non-custodial**: Haveno supports arbitration in case something goes wrong during the trade, but arbitrators never have access to your funds. - There is **No token**, because it's not needed. Transactions between traders are secured by non-custodial multisignature transactions on the Monero network. See the [FAQ on our website](https://haveno.exchange/faq/) for more information. ## Haveno Demo https://github.com/user-attachments/assets/eb6b3af0-78ce-46a7-bfa1-2aacd8649d47 ## Installing Haveno Haveno can be installed on Linux, macOS, and Windows by using a third party installer and network. > [!note] > The official Haveno repository does not support making real trades directly. > > To make real trades with Haveno, first find a third party network, and then use their installer or build their repository. We do not endorse any networks at this time. A test network is also available for users to make test trades using Monero's stagenet. See the [instructions](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md) to build Haveno and connect to the test network. Alternatively, you can [create your own mainnet network](https://github.com/haveno-dex/haveno/blob/master/docs/create-mainnet.md). Note that Haveno is being actively developed. If you find issues or bugs, please let us know. ## Main repositories - **[haveno](https://github.com/haveno-dex/haveno)** - This repository. The core of Haveno. - **[haveno-ts](https://github.com/haveno-dex/haveno-ts)** - TypeScript library for using Haveno. - **[haveno-meta](https://github.com/haveno-dex/haveno-meta)** - For project-wide discussions and proposals. If you wish to help, take a look at the repositories above and look for open issues. We run a bounty program to incentivize development. See [Bounties](#bounties). ## Keep in touch and help out! Haveno is a community-driven project. For it to be successful it's fundamental to have the support and help of the community. Join the community rooms on our Matrix server: - General discussions: **Haveno** ([#haveno:monero.social](https://matrix.to/#/#haveno:monero.social)) relayed on IRC/Libera (`#haveno`) - Development discussions: **Haveno Development** ([#haveno-development:monero.social](https://matrix.to/#/#haveno-development:monero.social)) relayed on IRC/Libera (`#haveno-development`) Email: contact@haveno.exchange Website: [haveno.exchange](https://haveno.exchange) ## Contributing to Haveno See the [developer guide](docs/developer-guide.md) to get started developing for Haveno. See [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) for our styling guides. If you are not able to contribute code and want to contribute development resources, [donations](#support) fund development bounties. ## Bounties To incentivize development and reward contributors, we adopt a simple bounty system. Contributors may be awarded bounties after completing a task (resolving an issue). Take a look at the [issues labeled '💰bounty'](https://github.com/haveno-dex/haveno/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty) in the main `haveno` repository. [Details and conditions for receiving a bounty](docs/bounties.md). ## Support To bring Haveno to life, we need resources. If you have the possibility, please consider donating to the project:

Donate Monero
47fo8N5m2VVW4uojadGQVJ34LFR9yXwDrZDRugjvVSjcTWV2WFSoc1XfNpHmxwmVtfNY9wMBch6259G6BXXFmhU49YG1zfB

================================================ FILE: apitest/scripts/editf2faccountform.py ================================================ import sys, os, json # Writes a Haveno json F2F payment account form for the given country_code to the current working directory. if len(sys.argv) < 2: print("usage: editf2faccountform.py country_code") exit(1) country_code = str(sys.argv[1]).upper() acct_form = { "_COMMENTS_": [ "Do not manually edit the paymentMethodId field.", "Edit the salt field only if you are recreating a payment account on a new installation and wish to preserve the account age." ], "paymentMethodId": "F2F", "accountName": "Face to Face Payment Account", "city": "Anytown", "contact": "Me", "country": country_code, "extraInfo": "", "salt": "" } target=os.path.dirname(os.path.realpath(__file__)) + '/' + 'f2f-acct.json' with open (target, 'w') as outfile: json.dump(acct_form, outfile, indent=2) outfile.write('\n') exit(0) ================================================ FILE: apitest/scripts/get-haveno-pid.sh ================================================ #!/bin/bash # Find the pid of the java process by grepping for the mainClassName and appName, # then print the 2nd column of the output to stdout. # # Doing this from Java is problematic, probably due to limitation of the # apitest.linux.BashCommand implementation. MAIN_CLASS_NAME=$1 APP_NAME=$2 # TODO args validation ps aux | grep java | grep "${MAIN_CLASS_NAME}" | grep "${APP_NAME}" | awk '{print $2}' ================================================ FILE: apitest/scripts/limit-order-simulation.sh ================================================ #! /bin/bash # Demonstrates a way to create a limit order (offer) using the API CLI with a local regtest bitcoin node. # # A country code argument is used to create a country based face to face payment account for the simulated offer. # # Prerequisites: # # - Linux or OSX with bash, Java 11-15 (JDK language compatibility 11), and bitcoin-core (v0.19 - v22). # # - Haveno must be fully built with apitest dao setup files installed. # Build command: `./gradlew clean build :apitest:installDaoSetup` # # - All supporting nodes must be run locally, in dev/dao/regtest mode: # bitcoind, seednode, arbdaemon, alicedaemon, bobdaemon # # These should be run using the apitest harness. From the root project dir, run: # `$ ./haveno-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon --shutdownAfterTests=false` # # - Only regtest btc can be bought or sold with the test payment account. # # Usage: # # This script must be run from the root of the project, e.g.: # # `$ apitest/scripts/limit-order-simulation.sh -l 40000 -d buy -c fr -m 3.00 -a 0.125` # # Script options: -l -d -c (-m || -f ) -a [-w ] # # Example: # # Create a sell/eur offer to sell 0.125 btc at a fixed-price of 38,000 euros, using a France face to face # payment account, when the BTC market price rises to or above 40,000 EUR: # # `$ apitest/scripts/limit-order-simulation.sh -l 40000 -d sell -c fr -m 0.00 -a 0.125` APP_BASE_NAME=$(basename "$0") APP_HOME=$(pwd -P) APITEST_SCRIPTS_HOME="$APP_HOME/apitest/scripts" source "$APITEST_SCRIPTS_HOME/trade-simulation-env.sh" source "$APITEST_SCRIPTS_HOME/trade-simulation-utils.sh" checksetup parselimitorderopts "$@" printdate "Started $APP_BASE_NAME with parameters:" printscriptparams printbreak editpaymentaccountform "$COUNTRY_CODE" exitoncommandalert $? cat "$APITEST_SCRIPTS_HOME/$F2F_ACCT_FORM" printbreak # Create F2F payment accounts for $COUNTRY_CODE, and get the $CURRENCY_CODE. printdate "Creating Alice's face to face $COUNTRY_CODE payment account." CMD="$CLI_BASE --port=$ALICE_PORT createpaymentacct --payment-account-form=$APITEST_SCRIPTS_HOME/$F2F_ACCT_FORM" printdate "ALICE CLI: $CMD" CMD_OUTPUT=$(createpaymentacct "$CMD") exitoncommandalert $? echo "$CMD_OUTPUT" ALICE_ACCT_ID=$(getnewpaymentacctid "$CMD_OUTPUT") exitoncommandalert $? CURRENCY_CODE=$(getnewpaymentacctcurrency "$CMD_OUTPUT") exitoncommandalert $? printdate "ALICE F2F payment-account-id = $ALICE_ACCT_ID, currency-code = $CURRENCY_CODE." printbreak printdate "Creating Bob's face to face $COUNTRY_CODE payment account." CMD="$CLI_BASE --port=$BOB_PORT createpaymentacct --payment-account-form=$APITEST_SCRIPTS_HOME/$F2F_ACCT_FORM" printdate "BOB CLI: $CMD" CMD_OUTPUT=$(createpaymentacct "$CMD") exitoncommandalert $? echo "$CMD_OUTPUT" BOB_ACCT_ID=$(getnewpaymentacctid "$CMD_OUTPUT") exitoncommandalert $? CURRENCY_CODE=$(getnewpaymentacctcurrency "$CMD_OUTPUT") exitoncommandalert $? printdate "BOB F2F payment-account-id = $BOB_ACCT_ID, currency-code = $CURRENCY_CODE." printbreak # Bob & Alice now have matching payment accounts, now loop until the price limit is reached, then create an offer. if [ "$DIRECTION" = "BUY" ] then printdate "Create a BUY / $CURRENCY_CODE offer when the market price falls to or below $LIMIT_PRICE $CURRENCY_CODE." else printdate "Create a SELL / $CURRENCY_CODE offer when the market price rises to or above $LIMIT_PRICE $CURRENCY_CODE." fi DONE=0 while : ; do if [ "$DONE" -ne 0 ]; then break fi CURRENT_PRICE=$(getcurrentprice "$ALICE_PORT" "$CURRENCY_CODE") exitoncommandalert $? printdate "Current Market Price: $CURRENT_PRICE" if [ "$DIRECTION" = "BUY" ] && [ "$CURRENT_PRICE" -le "$LIMIT_PRICE" ]; then printdate "Limit price reached." DONE=1 break fi if [ "$DIRECTION" = "SELL" ] && [ "$CURRENT_PRICE" -ge "$LIMIT_PRICE" ]; then printdate "Limit price reached." DONE=1 break fi sleep "$WAIT" done printdate "ALICE: Creating $DIRECTION $CURRENCY_CODE offer with payment acct $ALICE_ACCT_ID." CMD="$CLI_BASE --port=$ALICE_PORT createoffer" CMD+=" --payment-account=$ALICE_ACCT_ID" CMD+=" --direction=$DIRECTION" CMD+=" --currency-code=$CURRENCY_CODE" CMD+=" --amount=$AMOUNT" if [ -z "$MKT_PRICE_MARGIN" ]; then CMD+=" --fixed-price=$FIXED_PRICE" else CMD+=" --market-price-margin=$MKT_PRICE_MARGIN" fi CMD+=" --security-deposit=50.0" printdate "ALICE CLI: $CMD" OFFER_ID=$(createoffer "$CMD") exitoncommandalert $? printdate "ALICE: Created offer with id: $OFFER_ID." printbreak sleeptraced 3 # Show Alice's new offer. printdate "ALICE: Looking at her new $DIRECTION $CURRENCY_CODE offer." CMD="$CLI_BASE --port=$ALICE_PORT getoffer --offer-id=$OFFER_ID" printdate "ALICE CLI: $CMD" OFFER=$($CMD) exitoncommandalert $? echo "$OFFER" printbreak sleeptraced 4 # Generate some btc blocks. printdate "Generating btc blocks after publishing Alice's offer." genbtcblocks 3 3 printbreak # Show Alice's offer in Bob's CLI. printdate "BOB: Looking at $DIRECTION $CURRENCY_CODE offers." CMD="$CLI_BASE --port=$BOB_PORT getoffers --direction=$DIRECTION --currency-code=$CURRENCY_CODE" printdate "BOB CLI: $CMD" OFFERS=$($CMD) exitoncommandalert $? echo "$OFFERS" exit 0 ================================================ FILE: apitest/scripts/mainnet-test.sh ================================================ #!/usr/bin/env bats # # Smoke tests for haveno-cli running against a live haveno-daemon (on mainnet) # # Prerequisites: # # - bats-core 1.2.0+ must be installed (brew install bats-core on macOS) # see https://github.com/bats-core/bats-core # # - Run `./haveno-daemon --apiPassword=xyz --appDataDir=$TESTDIR` where $TESTDIR # is empty or otherwise contains an unencrypted wallet with a 0 BTC balance # # Usage: # # This script must be run from the root of the project, e.g.: # # bats apitest/scripts/mainnet-test.sh @test "test unsupported method error" { run ./haveno-cli --password=xyz bogus [ "$status" -eq 1 ] echo "actual output: $output" >&2 # printed only on test failure [ "$output" = "Error: 'bogus' is not a supported method" ] } @test "test unrecognized option error" { run ./haveno-cli --bogus getversion [ "$status" -eq 1 ] echo "actual output: $output" >&2 [ "$output" = "Error: missing required 'password' option" ] } @test "test missing required password option error" { run ./haveno-cli getversion [ "$status" -eq 1 ] echo "actual output: $output" >&2 [ "$output" = "Error: missing required 'password' option" ] } @test "test incorrect password error" { run ./haveno-cli --password=bogus getversion [ "$status" -eq 1 ] echo "actual output: $output" >&2 [ "$output" = "Error: incorrect 'password' rpc header value" ] } @test "test getversion call with quoted password" { load 'version-parser' run ./haveno-cli --password="xyz" getversion [ "$status" -eq 0 ] echo "actual output: $output" >&2 [ "$output" = "$CURRENT_VERSION" ] } @test "test getversion" { # Wait 1 second before calling getversion again. sleep 1 load 'version-parser' run ./haveno-cli --password=xyz getversion [ "$status" -eq 0 ] echo "actual output: $output" >&2 [ "$output" = "$CURRENT_VERSION" ] } @test "test setwalletpassword \"a b c\"" { run ./haveno-cli --password=xyz setwalletpassword --wallet-password="a b c" [ "$status" -eq 0 ] echo "actual output: $output" >&2 [ "$output" = "wallet encrypted" ] sleep 1 } @test "test unlockwallet without password & timeout args" { run ./haveno-cli --password=xyz unlockwallet [ "$status" -eq 1 ] echo "actual output: $output" >&2 [ "$output" = "Error: no password specified" ] } @test "test unlockwallet without timeout arg" { run ./haveno-cli --password=xyz unlockwallet --wallet-password="a b c" [ "$status" -eq 1 ] echo "actual output: $output" >&2 [ "$output" = "Error: no unlock timeout specified" ] } @test "test unlockwallet \"a b c\" 8" { run ./haveno-cli --password=xyz unlockwallet --wallet-password="a b c" --timeout=8 [ "$status" -eq 0 ] echo "actual output: $output" >&2 [ "$output" = "wallet unlocked" ] } @test "test getbalance while wallet unlocked for 8s" { run ./haveno-cli --password=xyz getbalance [ "$status" -eq 0 ] sleep 8 } @test "test unlockwallet \"a b c\" 6" { run ./haveno-cli --password=xyz unlockwallet --wallet-password="a b c" --timeout=6 [ "$status" -eq 0 ] echo "actual output: $output" >&2 [ "$output" = "wallet unlocked" ] } @test "test lockwallet before unlockwallet timeout=6s expires" { run ./haveno-cli --password=xyz lockwallet [ "$status" -eq 0 ] echo "actual output: $output" >&2 [ "$output" = "wallet locked" ] } @test "test setwalletpassword incorrect old pwd error" { run ./haveno-cli --password=xyz setwalletpassword --wallet-password="z z z" --new-wallet-password="d e f" [ "$status" -eq 1 ] echo "actual output: $output" >&2 [ "$output" = "Error: incorrect old password" ] } @test "test setwalletpassword oldpwd newpwd" { # Wait 5 seconds before calling setwalletpassword again. sleep 5 run ./haveno-cli --password=xyz setwalletpassword --wallet-password="a b c" --new-wallet-password="d e f" [ "$status" -eq 0 ] echo "actual output: $output" >&2 [ "$output" = "wallet encrypted with new password" ] sleep 1 } @test "test getbalance wallet locked error" { run ./haveno-cli --password=xyz getbalance [ "$status" -eq 1 ] echo "actual output: $output" >&2 [ "$output" = "Error: wallet is locked" ] } @test "test removewalletpassword" { run ./haveno-cli --password=xyz removewalletpassword --wallet-password="d e f" [ "$status" -eq 0 ] echo "actual output: $output" >&2 [ "$output" = "wallet decrypted" ] sleep 3 } @test "test getbalance when wallet available & unlocked with 0 btc balance" { run ./haveno-cli --password=xyz getbalance [ "$status" -eq 0 ] } @test "test getfundingaddresses" { run ./haveno-cli --password=xyz getfundingaddresses [ "$status" -eq 0 ] } @test "test getaddressbalance missing address argument" { run ./haveno-cli --password=xyz getaddressbalance [ "$status" -eq 1 ] echo "actual output: $output" >&2 [ "$output" = "Error: no address specified" ] } @test "test getaddressbalance bogus address argument" { # Wait 1 second before calling getaddressbalance again. sleep 1 run ./haveno-cli --password=xyz getaddressbalance --address=bogus [ "$status" -eq 1 ] echo "actual output: $output" >&2 [ "$output" = "Error: address bogus not found in wallet" ] } @test "test getpaymentmethods" { run ./haveno-cli --password=xyz getpaymentmethods [ "$status" -eq 0 ] } @test "test getpaymentaccts" { run ./haveno-cli --password=xyz getpaymentaccts [ "$status" -eq 0 ] } @test "test getoffers missing direction argument" { run ./haveno-cli --password=xyz getoffers [ "$status" -eq 1 ] echo "actual output: $output" >&2 [ "$output" = "Error: no direction (buy|sell) specified" ] } @test "test getoffers sell eur check return status" { # Wait 1 second before calling getoffers again. sleep 1 run ./haveno-cli --password=xyz getoffers --direction=sell --currency-code=eur [ "$status" -eq 0 ] } @test "test getoffers buy eur check return status" { # Wait 1 second before calling getoffers again. sleep 1 run ./haveno-cli --password=xyz getoffers --direction=buy --currency-code=eur [ "$status" -eq 0 ] } @test "test getoffers sell gbp check return status" { # Wait 1 second before calling getoffers again. sleep 1 run ./haveno-cli --password=xyz getoffers --direction=sell --currency-code=gbp [ "$status" -eq 0 ] } @test "test help displayed on stderr if no options or arguments" { run ./haveno-cli [ "$status" -eq 1 ] [ "${lines[0]}" = "Haveno RPC Client" ] [ "${lines[1]}" = "Usage: haveno-cli [options] [params]" ] # TODO add asserts after help text is modified for new endpoints } @test "test --help option" { run ./haveno-cli --help [ "$status" -eq 0 ] [ "${lines[0]}" = "Haveno RPC Client" ] [ "${lines[1]}" = "Usage: haveno-cli [options] [params]" ] # TODO add asserts after help text is modified for new endpoints } @test "test takeoffer method --help" { run ./haveno-cli --password=xyz takeoffer --help [ "$status" -eq 0 ] [ "${lines[0]}" = "takeoffer" ] } ================================================ FILE: apitest/scripts/rolling-offer-simulation.sh ================================================ #! /bin/bash # Demonstrates a way to always keep one offer in the market, using the API CLI with a local regtest bitcoin node. # Alice creates an offer, waits for Bob to take it, and completes the trade protocol with him. Then Alice # creates a new offer... # # Stop the script by entering ^C. # # A country code argument is used to create a country based face to face payment account for the simulated offer. # # Prerequisites: # # - Linux or OSX with bash, Java 11-15 (JDK language compatibility 11), and bitcoin-core (v0.19 - v22). # # - Haveno must be fully built with apitest dao setup files installed. # Build command: `./gradlew clean build :apitest:installDaoSetup` # # - All supporting nodes must be run locally, in dev/dao/regtest mode: # bitcoind, seednode, arbdaemon, alicedaemon, bobdaemon # # These should be run using the apitest harness. From the root project dir, run: # `$ ./haveno-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon --shutdownAfterTests=false` # # - Only regtest btc can be bought or sold with the test payment account. # # Usage: # # This script must be run from the root of the project, e.g.: # # `$ apitest/scripts/rolling-offer-simulation.sh -d buy -c us -m 2.00 -a 0.125` # # Script options: -d -c (-m || -f ) -a # # Example: # # Create a buy/usd offer to sell 0.1 btc at 2% above market price, using a US face to face payment account: # # `$ apitest/scripts/rolling-offer-simulation.sh -d sell -c us -m 2.00 -a 0.1` APP_BASE_NAME=$(basename "$0") APP_HOME=$(pwd -P) APITEST_SCRIPTS_HOME="$APP_HOME/apitest/scripts" source "$APITEST_SCRIPTS_HOME/trade-simulation-env.sh" source "$APITEST_SCRIPTS_HOME/trade-simulation-utils.sh" checksetup parseopts "$@" printdate "Started $APP_BASE_NAME with parameters:" printscriptparams printbreak registerdisputeagents showcreatepaymentacctsteps "Alice" "$ALICE_PORT" CMD="$CLI_BASE --port=$ALICE_PORT createpaymentacct --payment-account-form=$APITEST_SCRIPTS_HOME/$F2F_ACCT_FORM" printdate "ALICE CLI: $CMD" CMD_OUTPUT=$(createpaymentacct "$CMD") echo "$CMD_OUTPUT" printbreak export ALICE_ACCT_ID=$(getnewpaymentacctid "$CMD_OUTPUT") export CURRENCY_CODE=$(getnewpaymentacctcurrency "$CMD_OUTPUT") printdate "Alice's F2F payment-account-id: $ALICE_ACCT_ID, currency-code: $CURRENCY_CODE" exitoncommandalert $? printbreak printdate "Bob creates his F2F payment account." CMD="$CLI_BASE --port=$BOB_PORT createpaymentacct --payment-account-form=$APITEST_SCRIPTS_HOME/$F2F_ACCT_FORM" printdate "BOB CLI: $CMD" CMD_OUTPUT=$(createpaymentacct "$CMD") echo "$CMD_OUTPUT" printbreak export BOB_ACCT_ID=$(getnewpaymentacctid "$CMD_OUTPUT") export CURRENCY_CODE=$(getnewpaymentacctcurrency "$CMD_OUTPUT") printdate "Bob's F2F payment-account-id: $BOB_ACCT_ID, currency-code: $CURRENCY_CODE" exitoncommandalert $? printbreak while : ; do printdate "ALICE $ALICE_ROLE: Creating $DIRECTION $CURRENCY_CODE offer with payment acct $ALICE_ACCT_ID." CURRENT_PRICE=$(getcurrentprice "$ALICE_PORT" "$CURRENCY_CODE") exitoncommandalert $? printdate "Current Market Price: $CURRENT_PRICE" CMD=$(gencreateoffercommand "$ALICE_PORT" "$ALICE_ACCT_ID") printdate "ALICE CLI: $CMD" OFFER_ID=$(createoffer "$CMD") exitoncommandalert $? printdate "ALICE $ALICE_ROLE: Created offer with id: $OFFER_ID." printbreak sleeptraced 3 # Show Alice's new offer. printdate "ALICE $ALICE_ROLE: Looking at her new $DIRECTION $CURRENCY_CODE offer." CMD="$CLI_BASE --port=$ALICE_PORT getoffer --offer-id=$OFFER_ID" printdate "ALICE CLI: $CMD" OFFER=$($CMD) exitoncommandalert $? echo "$OFFER" printbreak sleeptraced 3 # Generate some btc blocks. printdate "Generating btc blocks after publishing Alice's offer." genbtcblocks 3 2 printbreak RANDOM_WAIT=$(echo $[$RANDOM % 10 + 1]) printdate "Bob will take Alice's offer in $RANDOM_WAIT seconds..." sleeptraced "$RANDOM_WAIT" executetrade exitoncommandalert $? printbreak done exit 0 ================================================ FILE: apitest/scripts/trade-simulation-env.sh ================================================ #! /bin/bash # This file must be sourced by the main driver. export CLI_BASE="./haveno-cli --password=xyz" export ARBITRATOR_PORT=9997 export ALICE_PORT=9998 export BOB_PORT=9999 export F2F_ACCT_FORM="f2f-acct.json" checkos() { LINUX=FALSE DARWIN=FALSE UNAME=$(uname) case "$UNAME" in Linux* ) export LINUX=TRUE ;; Darwin* ) export DARWIN=TRUE ;; esac if [[ "$LINUX" == "TRUE" ]]; then printdate "Running on supported Linux OS." elif [[ "$DARWIN" == "TRUE" ]]; then printdate "Running on supported Mac OS." else printdate "Script cannot run on $OSTYPE OS, only Linux and OSX are supported." exit 1 fi } checksetup() { checkos apitestusage() { echo "The apitest harness must be running a local bitcoin regtest node, a seednode, an arbitration node," echo "Bob & Alice daemons, and bitcoin-core's bitcoin-cli must be in the system PATH." echo "" echo "From the project's root dir, start all supporting nodes from a terminal:" echo "./haveno-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon --shutdownAfterTests=false" exit 1; } printdate "Checking $APP_HOME for some expected directories and files." if [ -d "$APP_HOME/apitest" ]; then printdate "Subproject apitest exists."; else printdate "Error: Subproject apitest not found, maybe because you are not running the script from the project root dir." exit 1 fi if [ -f "$APP_HOME/haveno-cli" ]; then printdate "The haveno-cli script exists."; else printdate "Error: The haveno-cli script not found, maybe because you are not running the script from the project root dir." exit 1 fi printdate "Checking to see local bitcoind is running, and bitcoin-cli is in PATH." checkbitcoindrunning checkbitcoincliinpath printdate "Checking to see haveno servers are running." checkseednoderunning checkarbnoderunning checkalicenoderunning checkbobnoderunning } parseopts() { usage() { echo "Usage: $0 [-d buy|sell] [-c ] [-f || -m ] [-a ]" 1>&2 exit 1; } local OPTIND o d c f m a while getopts "d:c:f:m:a:" o; do case "${o}" in d) d=$(echo "${OPTARG}" | tr '[:lower:]' '[:upper:]') ((d == "BUY" || d == "SELL")) || usage export DIRECTION=${d} ;; c) c=$(echo "${OPTARG}"| tr '[:lower:]' '[:upper:]') export COUNTRY_CODE=${c} ;; f) f=${OPTARG} export FIXED_PRICE=${f} ;; m) m=${OPTARG} export MKT_PRICE_MARGIN=${m} ;; a) a=${OPTARG} export AMOUNT=${a} ;; *) usage ;; esac done shift $((OPTIND-1)) if [ -z "${d}" ] || [ -z "${c}" ] || [ -z "${a}" ]; then usage fi if [ -z "${f}" ] && [ -z "${m}" ]; then usage fi if [ -n "${f}" ] && [ -n "${m}" ]; then printdate "Must use margin-from-price param (-m) or fixed-price param (-f), not both." usage fi if [ "$DIRECTION" = "SELL" ] then export BOB_ROLE="(taker/buyer)" export ALICE_ROLE="(maker/seller)" else export BOB_ROLE="(taker/seller)" export ALICE_ROLE="(maker/buyer)" fi } parselimitorderopts() { usage() { echo "Usage: $0 [-l limit-price] [-d buy|sell] [-c ] [-f || -m ] [-a ] [-w ]" 1>&2 exit 1; } local OPTIND o l d c f m a w while getopts "l:d:c:f:m:a:w:" o; do case "${o}" in l) l=${OPTARG} export LIMIT_PRICE=${l} ;; d) d=$(echo "${OPTARG}" | tr '[:lower:]' '[:upper:]') ((d == "BUY" || d == "SELL")) || usage export DIRECTION=${d} ;; c) c=$(echo "${OPTARG}"| tr '[:lower:]' '[:upper:]') export COUNTRY_CODE=${c} ;; f) f=${OPTARG} export FIXED_PRICE=${f} ;; m) m=${OPTARG} export MKT_PRICE_MARGIN=${m} ;; a) a=${OPTARG} export AMOUNT=${a} ;; w) w=${OPTARG} export WAIT=${w} ;; *) usage ;; esac done shift $((OPTIND-1)) if [ -z "${l}" ]; then usage fi if [ -z "${d}" ] || [ -z "${c}" ] || [ -z "${a}" ]; then usage fi if [ -z "${f}" ] && [ -z "${m}" ]; then usage fi if [ -n "${f}" ] && [ -n "${m}" ]; then printdate "Must use margin-from-price param (-m) or fixed-price param (-f), not both." usage fi if [ -z "${w}" ]; then WAIT=120 elif [ "$w" -lt 20 ]; then printdate "The -w option is too low, minimum allowed is 20s. Using default 120s." WAIT=120 fi } checkbitcoindrunning() { # There may be a '+' char in the path and we have to escape it for pgrep. if [[ $APP_HOME == *"+"* ]]; then ESCAPED_APP_HOME=$(escapepluschar "$APP_HOME") else ESCAPED_APP_HOME="$APP_HOME" fi if pgrep -f "bitcoind -datadir=$ESCAPED_APP_HOME/apitest/build/resources/main/Bitcoin-regtest" > /dev/null ; then printdate "The regtest bitcoind node is running on host." else printdate "Error: regtest bitcoind node is not running on host, exiting." apitestusage fi } checkbitcoincliinpath() { if which bitcoin-cli > /dev/null ; then printdate "The bitcoin-cli binary is in the system PATH." else printdate "Error: bitcoin-cli binary is not in the system PATH, exiting." apitestusage fi } checkseednoderunning() { if [[ "$LINUX" == "TRUE" ]]; then if pgrep -f "haveno.seednode.SeedNodeMain" > /dev/null ; then printdate "The seed node is running on host." else printdate "Error: seed node is not running on host, exiting." apitestusage fi elif [[ "$DARWIN" == "TRUE" ]]; then if ps -A | awk '/[S]eedNodeMain/ {print $1}' > /dev/null ; then printdate "The seednode is running on host." else printdate "Error: seed node is not running on host, exiting." apitestusage fi else printdate "Error: seed node is not running on host, exiting." apitestusage fi } checkarbnoderunning() { if [[ "$LINUX" == "TRUE" ]]; then if pgrep -f "haveno.daemon.app.HavenoDaemonMain --appName=haveno-XMR_STAGENET_Arb" > /dev/null ; then printdate "The arbitration node is running on host." else printdate "Error: arbitration node is not running on host, exiting." apitestusage fi elif [[ "$DARWIN" == "TRUE" ]]; then if ps -A | awk '/[b]isq.daemon.app.HavenoDaemonMain --appName=haveno-XMR_STAGENET_Arb/ {print $1}' > /dev/null ; then printdate "The arbitration node is running on host." else printdate "Error: arbitration node is not running on host, exiting." apitestusage fi else printdate "Error: arbitration node is not running on host, exiting." apitestusage fi } checkalicenoderunning() { if [[ "$LINUX" == "TRUE" ]]; then if pgrep -f "haveno.daemon.app.HavenoDaemonMain --appName=haveno-XMR_STAGENET_Alice" > /dev/null ; then printdate "Alice's node is running on host." else printdate "Error: Alice's node is not running on host, exiting." apitestusage fi elif [[ "$DARWIN" == "TRUE" ]]; then if ps -A | awk '/[b]isq.daemon.app.HavenoDaemonMain --appName=haveno-XMR_STAGENET_Alice/ {print $1}' > /dev/null ; then printdate "Alice's node node is running on host." else printdate "Error: Alice's node is not running on host, exiting." apitestusage fi else printdate "Error: Alice's node is not running on host, exiting." apitestusage fi } checkbobnoderunning() { if [[ "$LINUX" == "TRUE" ]]; then if pgrep -f "haveno.daemon.app.HavenoDaemonMain --appName=haveno-XMR_STAGENET_Alice" > /dev/null ; then printdate "Bob's node is running on host." else printdate "Error: Bob's node is not running on host, exiting." apitestusage fi elif [[ "$DARWIN" == "TRUE" ]]; then if ps -A | awk '/[b]isq.daemon.app.HavenoDaemonMain --appName=haveno-XMR_STAGENET_Alice/ {print $1}' > /dev/null ; then printdate "Bob's node node is running on host." else printdate "Error: Bob's node is not running on host, exiting." apitestusage fi else printdate "Error: Bob's node is not running on host, exiting." apitestusage fi } printscriptparams() { if [ -n "${LIMIT_PRICE+1}" ]; then echo " LIMIT_PRICE = $LIMIT_PRICE" fi echo " DIRECTION = $DIRECTION" echo " COUNTRY_CODE = $COUNTRY_CODE" echo " FIXED_PRICE = $FIXED_PRICE" echo " MKT_PRICE_MARGIN = $MKT_PRICE_MARGIN" echo " AMOUNT = $AMOUNT" if [ -n "${BOB_ROLE+1}" ]; then echo " BOB_ROLE = $BOB_ROLE" fi if [ -n "${ALICE_ROLE+1}" ]; then echo " ALICE_ROLE = $ALICE_ROLE" fi if [ -n "${WAIT+1}" ]; then echo " WAIT = $WAIT" fi } ================================================ FILE: apitest/scripts/trade-simulation-utils.sh ================================================ #! /bin/bash # This file must be sourced by the main driver. source "$APITEST_SCRIPTS_HOME/trade-simulation-env.sh" printdate() { echo "[$(date)] $@" } printbreak() { echo "" echo "" } printcmd() { echo -en "$@\n" } sleeptraced() { PERIOD="$1" printdate "sleeping for $PERIOD" sleep "$PERIOD" } commandalert() { # Used in a script function when it needs to fail early with an error message, & pass the error code to the caller. # usage: commandalert <$?> if [ "$1" -ne 0 ] then printdate "Error: $2" >&2 exit "$1" fi } # TODO rename exitonalert ? exitoncommandalert() { # Used in a parent script when you need it to fail immediately, with no error message. # usage: exitoncommandalert <$?> if [ "$1" -ne 0 ] then exit "$1" fi } registerdisputeagents() { # Silently register dev dispute agents. It's easy to forget. REG_KEY="6ac43ea1df2a290c1c8391736aa42e4339c5cb4f110ff0257a13b63211977b7a" CMD="$CLI_BASE --port=$ARBITRATOR_PORT registerdisputeagent --dispute-agent-type=mediator --registration-key=$REG_KEY" SILENT=$($CMD) commandalert $? "Could not register dev/test mediator." CMD="$CLI_BASE --port=$ARBITRATOR_PORT registerdisputeagent --dispute-agent-type=refundagent --registration-key=$REG_KEY" SILENT=$($CMD) commandalert $? "Could not register dev/test refundagent." # Do something with $SILENT to keep codacy happy. echo "$SILENT" > /dev/null } getbtcoreaddress() { CMD="bitcoin-cli -regtest -rpcport=19443 -rpcuser=apitest -rpcpassword=apitest getnewaddress" NEW_ADDRESS=$($CMD) echo "$NEW_ADDRESS" } genbtcblocks() { NUM_BLOCKS="$1" SECONDS_BETWEEN_BLOCKS="$2" ADDR_PARAM="$(getbtcoreaddress)" CMD_PREFIX="bitcoin-cli -regtest -rpcport=19443 -rpcuser=apitest -rpcpassword=apitest generatetoaddress 1" # Print the generatetoaddress command with double quoted address param, to make it cut & pastable from the console. printdate "$CMD_PREFIX \"$ADDR_PARAM\"" # Now create the full generatetoaddress command to be run now. CMD="$CMD_PREFIX $ADDR_PARAM" for i in $(seq -f "%02g" 1 "$NUM_BLOCKS") do NEW_BLOCK_HASH=$(genbtcblock "$CMD") printdate "Block Hash #$i:$NEW_BLOCK_HASH" sleep "$SECONDS_BETWEEN_BLOCKS" done } genbtcblock() { CMD="$1" NEW_BLOCK_HASH=$($CMD | sed -n '2p') echo "$NEW_BLOCK_HASH" } escapepluschar() { STRING="$1" NEW_STRING=$(echo "${STRING//+/\\+}") echo "$NEW_STRING" } printbalances() { PORT="$1" printcmd "$CLI_BASE --port=$PORT getbalance" $CLI_BASE --port="$PORT" getbalance } getpaymentaccountmethods() { CMD="$1" CMD_OUTPUT=$($CMD) commandalert $? "Could not get payment method ids." printdate "Payment Method IDs:" echo "$CMD_OUTPUT" } getpaymentaccountform() { CMD="$1" CMD_OUTPUT=$($CMD) commandalert $? "Could not get new payment account form." echo "$CMD_OUTPUT" } editpaymentaccountform() { COUNTRY_CODE="$1" CMD="python3 $APITEST_SCRIPTS_HOME/editf2faccountform.py $COUNTRY_CODE" CMD_OUTPUT=$($CMD) commandalert $? "Could not edit payment account form." printdate "Saved payment account form as $F2F_ACCT_FORM." } getnewpaymentacctid() { CREATE_PAYMENT_ACCT_OUTPUT="$1" PAYMENT_ACCT_DETAIL=$(echo -e "$CREATE_PAYMENT_ACCT_OUTPUT" | sed -n '3p') ACCT_ID=$(echo -e "$PAYMENT_ACCT_DETAIL" | awk '{print $NF}') echo "$ACCT_ID" } getnewpaymentacctcurrency() { CREATE_PAYMENT_ACCT_OUTPUT="$1" PAYMENT_ACCT_DETAIL=$(echo -e "$CREATE_PAYMENT_ACCT_OUTPUT" | sed -n '3p') # This is brittle; it requires the account name field to have N words, # e.g, "Face to Face Payment Account" as defined in editf2faccountform.py. CURRENCY_CODE=$(echo -e "$PAYMENT_ACCT_DETAIL" | awk '{print $6}') echo "$CURRENCY_CODE" } createpaymentacct() { CMD="$1" CMD_OUTPUT=$($CMD) commandalert $? "Could not create new payment account." echo "$CMD_OUTPUT" } getpaymentaccounts() { PORT="$1" printcmd "$CLI_BASE --port=$PORT getpaymentaccts" CMD="$CLI_BASE --port=$PORT getpaymentaccts" CMD_OUTPUT=$($CMD) commandalert $? "Could not get payment accounts." echo "$CMD_OUTPUT" } showcreatepaymentacctsteps() { USER="$1" PORT="$2" printdate "$USER looks for the ID of the face to face payment account method (Bob will use same payment method)." CMD="$CLI_BASE --port=$PORT getpaymentmethods" printdate "$USER CLI: $CMD" PAYMENT_ACCT_METHODS=$(getpaymentaccountmethods "$CMD") echo "$PAYMENT_ACCT_METHODS" printbreak printdate "$USER uses the F2F payment method id to create a face to face payment account in country $COUNTRY_CODE." CMD="$CLI_BASE --port=$PORT getpaymentacctform --payment-method-id=F2F" printdate "$USER CLI: $CMD" getpaymentaccountform "$CMD" printbreak printdate "$USER edits the $COUNTRY_CODE payment account form, and (optionally) renames it as $F2F_ACCT_FORM" editpaymentaccountform "$COUNTRY_CODE" cat "$APITEST_SCRIPTS_HOME/$F2F_ACCT_FORM" # Remove the autogenerated json template because we are going to use one created by a python script in the next step. CMD="rm -v $APP_HOME/f2f_*.json" DELETE_JSON_TEMPLATE=$($CMD) printdate "$DELETE_JSON_TEMPLATE" printbreak } gencreateoffercommand() { PORT="$1" ACCT_ID="$2" CMD="$CLI_BASE --port=$PORT createoffer" CMD+=" --payment-account=$ACCT_ID" CMD+=" --direction=$DIRECTION" CMD+=" --currency-code=$CURRENCY_CODE" CMD+=" --amount=$AMOUNT" if [ -z "$MKT_PRICE_MARGIN" ]; then CMD+=" --fixed-price=$FIXED_PRICE" else CMD+=" --market-price-margin=$MKT_PRICE_MARGIN" fi CMD+=" --security-deposit=15.0" echo "$CMD" } createoffer() { CREATE_OFFER_CMD="$1" OFFER_DESC=$($CREATE_OFFER_CMD) # If the CLI command exited with an error, print the CLI error, and # return from this function now, passing the error status code to the caller. commandalert $? "Could not create offer." OFFER_DETAIL=$(echo -e "$OFFER_DESC" | sed -n '2p') NEW_OFFER_ID=$(echo -e "$OFFER_DETAIL" | awk '{print $NF}') echo "$NEW_OFFER_ID" } getfirstofferid() { PORT="$1" CMD="$CLI_BASE --port=$PORT getoffers --direction=$DIRECTION --currency-code=$CURRENCY_CODE" CMD_OUTPUT=$($CMD) commandalert $? "Could not get current $DIRECTION / $CURRENCY_CODE offers." FIRST_OFFER_DETAIL=$(echo -e "$CMD_OUTPUT" | sed -n '2p') FIRST_OFFER_ID=$(echo -e "$FIRST_OFFER_DETAIL" | awk '{print $NF}') commandalert $? "Could parse the offer-id from the first listed offer." echo "$FIRST_OFFER_ID" } gettrade() { GET_TRADE_CMD="$1" TRADE_DESC=$($GET_TRADE_CMD) commandalert $? "Could not get trade." echo "$TRADE_DESC" } gettradedetail() { TRADE_DESC="$1" # Get 2nd line of gettrade cmd output, and squeeze multi space delimiters into one space. TRADE_DETAIL=$(echo "$TRADE_DESC" | sed -n '2p' | tr -s ' ') commandalert $? "Could not get trade detail (line 2 of gettrade output)." echo "$TRADE_DETAIL" } istradedepositpublished() { TRADE_DETAIL="$1" ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $10}') commandalert $? "Could not parse istradedepositpublished from trade detail." echo "$ANSWER" } istradedepositconfirmed() { TRADE_DETAIL="$1" ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $11}') commandalert $? "Could not parse istradedepositconfirmed from trade detail." echo "$ANSWER" } istradepaymentsent() { TRADE_DETAIL="$1" ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $13}') commandalert $? "Could not parse istradepaymentsent from trade detail." echo "$ANSWER" } istradepaymentreceived() { TRADE_DETAIL="$1" ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $14}') commandalert $? "Could not parse istradepaymentreceived from trade detail." echo "$ANSWER" } istradepayoutpublished() { TRADE_DETAIL="$1" ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $15}') commandalert $? "Could not parse istradepayoutpublished from trade detail." echo "$ANSWER" } waitfortradedepositpublished() { # Loops until Bob's trade deposit is published. (Bob is always the trade taker.) OFFER_ID="$1" DONE=0 while : ; do if [ "$DONE" -ne 0 ]; then break fi printdate "BOB $BOB_ROLE: Looking at his trade with id $OFFER_ID." CMD="$CLI_BASE --port=$BOB_PORT gettrade --trade-id=$OFFER_ID" printdate "BOB CLI: $CMD" GETTRADE_CMD_OUTPUT=$(gettrade "$CMD") exitoncommandalert $? echo "$GETTRADE_CMD_OUTPUT" printbreak TRADE_DETAIL=$(gettradedetail "$GETTRADE_CMD_OUTPUT") exitoncommandalert $? IS_TRADE_DEPOSIT_PUBLISHED=$(istradedepositpublished "$TRADE_DETAIL") exitoncommandalert $? printdate "BOB $BOB_ROLE: Has taker's trade deposit been published? $IS_TRADE_DEPOSIT_PUBLISHED" if [ "$IS_TRADE_DEPOSIT_PUBLISHED" = "YES" ] then DONE=1 else RANDOM_WAIT=$(echo $[$RANDOM % 3 + 1]) sleeptraced "$RANDOM_WAIT" fi printbreak done } waitfortradedepositconfirmed() { # Loops until Bob's trade deposit is confirmed. (Bob is always the trade taker.) OFFER_ID="$1" DONE=0 while : ; do if [ "$DONE" -ne 0 ]; then break fi printdate "BOB $BOB_ROLE: Looking at his trade with id $OFFER_ID." CMD="$CLI_BASE --port=$BOB_PORT gettrade --trade-id=$OFFER_ID" printdate "BOB CLI: $CMD" GETTRADE_CMD_OUTPUT=$(gettrade "$CMD") exitoncommandalert $? echo "$GETTRADE_CMD_OUTPUT" printbreak TRADE_DETAIL=$(gettradedetail "$GETTRADE_CMD_OUTPUT") exitoncommandalert $? IS_TRADE_DEPOSIT_CONFIRMED=$(istradedepositconfirmed "$TRADE_DETAIL") exitoncommandalert $? printdate "BOB $BOB_ROLE: Has taker's trade deposit been confirmed? $IS_TRADE_DEPOSIT_CONFIRMED" printbreak if [ "$IS_TRADE_DEPOSIT_CONFIRMED" = "YES" ] then DONE=1 else printdate "Generating btc block while Bob waits for trade deposit to be confirmed." genbtcblocks 1 0 RANDOM_WAIT=$(echo $[$RANDOM % 3 + 1]) sleeptraced "$RANDOM_WAIT" fi done } waitfortradepaymentsent() { # Loops until buyer's trade payment has been sent. PORT="$1" SELLER="$2" OFFER_ID="$3" DONE=0 while : ; do if [ "$DONE" -ne 0 ]; then break fi printdate "$SELLER: Looking at trade with id $OFFER_ID." CMD="$CLI_BASE --port=$PORT gettrade --trade-id=$OFFER_ID" printdate "$SELLER CLI: $CMD" GETTRADE_CMD_OUTPUT=$(gettrade "$CMD") exitoncommandalert $? echo "$GETTRADE_CMD_OUTPUT" printbreak TRADE_DETAIL=$(gettradedetail "$GETTRADE_CMD_OUTPUT") exitoncommandalert $? IS_TRADE_PAYMENT_SENT=$(istradepaymentsent "$TRADE_DETAIL") exitoncommandalert $? printdate "$SELLER: Has buyer's payment been initiated? $IS_TRADE_PAYMENT_SENT" if [ "$IS_TRADE_PAYMENT_SENT" = "YES" ] then DONE=1 else RANDOM_WAIT=$(echo $[$RANDOM % 3 + 1]) sleeptraced "$RANDOM_WAIT" fi printbreak done } waitfortradepaymentreceived() { # Loops until buyer's trade payment has been received. PORT="$1" SELLER="$2" OFFER_ID="$3" DONE=0 while : ; do if [ "$DONE" -ne 0 ]; then break fi printdate "$SELLER: Looking at trade with id $OFFER_ID." CMD="$CLI_BASE --port=$PORT gettrade --trade-id=$OFFER_ID" printdate "$SELLER CLI: $CMD" GETTRADE_CMD_OUTPUT=$(gettrade "$CMD") exitoncommandalert $? echo "$GETTRADE_CMD_OUTPUT" printbreak TRADE_DETAIL=$(gettradedetail "$GETTRADE_CMD_OUTPUT") exitoncommandalert $? # When the seller receives a 'payment sent' message, it is assumed funds (fiat) have already been deposited. # In a real trade, there is usually a delay between receipt of a 'payment sent' message, and the funds deposit, # but we do not need to simulate that in this regtest script. IS_TRADE_PAYMENT_SENT=$(istradepaymentreceived "$TRADE_DETAIL") exitoncommandalert $? printdate "$SELLER: Has buyer's payment been transferred to seller's account? $IS_TRADE_PAYMENT_SENT" if [ "$IS_TRADE_PAYMENT_SENT" = "YES" ] then DONE=1 else RANDOM_WAIT=$(echo $[$RANDOM % 3 + 1]) sleeptraced "$RANDOM_WAIT" fi printbreak done } delayconfirmpaymentsent() { # Confirm payment started after a random delay. This should be run in the background # while the payee polls the trade status, waiting for the message before confirming # payment has been received. PAYER="$1" PORT="$2" OFFER_ID="$3" RANDOM_WAIT=$(echo $[$RANDOM % 5 + 1]) printdate "$PAYER: Sending 'payment sent' message to seller in $RANDOM_WAIT seconds..." sleeptraced "$RANDOM_WAIT" CMD="$CLI_BASE --port=$PORT confirmpaymentsent --trade-id=$OFFER_ID" printdate "$PAYER_CLI: $CMD" SENT_MSG=$($CMD) commandalert $? "Could not send confirmpaymentsent message." # Print the confirmpaymentsent command's console output. printdate "$SENT_MSG" printbreak } delayconfirmpaymentreceived() { # Confirm payment received after a random delay. This should be run in the background # while the payer polls the trade status, waiting for the confirmation from the seller # that funds have been received. PAYEE="$1" PORT="$2" OFFER_ID="$3" RANDOM_WAIT=$(echo $[$RANDOM % 5 + 1]) printdate "$PAYEE: Sending 'payment sent' message to seller in $RANDOM_WAIT seconds..." sleeptraced "$RANDOM_WAIT" CMD="$CLI_BASE --port=$PORT confirmpaymentreceived --trade-id=$OFFER_ID" printdate "$PAYEE_CLI: $CMD" RCVD_MSG=$($CMD) commandalert $? "Could not send confirmpaymentsent message." # Print the confirmpaymentsent command's console output. printdate "$RCVD_MSG" printbreak } # This is a large function that might be split into smaller functions. But we are not testing # api methods here, just demonstrating how to use them to get through the V1 trade protocol with # the CLI. It should work for any trade between Bob & Alice, as long as Alice is maker, Bob is # taker, and the offer to be taken is the first displayed in Bob's getoffers command output. executetrade() { # Bob list available offers. printdate "BOB $BOB_ROLE: Looking at $DIRECTION $CURRENCY_CODE offers." CMD="$CLI_BASE --port=$BOB_PORT getoffers --direction=$DIRECTION --currency-code=$CURRENCY_CODE" printdate "BOB CLI: $CMD" OFFERS=$($CMD) exitoncommandalert $? echo "$OFFERS" printbreak OFFER_ID=$(getfirstofferid "$BOB_PORT") exitoncommandalert $? printdate "First offer found: $OFFER_ID" # Take Alice's offer. CMD="$CLI_BASE --port=$BOB_PORT takeoffer --offer-id=$OFFER_ID --payment-account=$BOB_ACCT_ID" printdate "BOB CLI: $CMD" TRADE=$($CMD) commandalert $? "Could not take offer." # Print the takeoffer command's console output. printdate "$TRADE" printbreak waitfortradedepositpublished "$OFFER_ID" waitfortradedepositconfirmed "$OFFER_ID" # Send payment sent and received messages. if [ "$DIRECTION" = "BUY" ] then PAYER="ALICE $ALICE_ROLE" PAYER_PORT=$ALICE_PORT PAYER_CLI="ALICE CLI" PAYEE="BOB $BOB_ROLE" PAYEE_PORT=$BOB_PORT PAYEE_CLI="BOB CLI" else PAYER="BOB $BOB_ROLE" PAYER_PORT=$BOB_PORT PAYER_CLI="BOB CLI" PAYEE="ALICE $ALICE_ROLE" PAYEE_PORT=$ALICE_PORT PAYEE_CLI="ALICE CLI" fi # Asynchronously send a confirm payment started message after a random delay. delayconfirmpaymentsent "$PAYER" "$PAYER_PORT" "$OFFER_ID" & if [ "$DIRECTION" = "BUY" ] then # Bob waits for payment, polling status in taker specific trade detail. waitfortradepaymentsent "$PAYEE_PORT" "$PAYEE" "$OFFER_ID" else # Alice waits for payment, polling status in maker specific trade detail. waitfortradepaymentsent "$PAYEE_PORT" "$PAYEE" "$OFFER_ID" fi # Asynchronously send a confirm payment received message after a random delay. delayconfirmpaymentreceived "$PAYEE" "$PAYEE_PORT" "$OFFER_ID" & if [ "$DIRECTION" = "BUY" ] then # Alice waits for payment rcvd confirm from Bob, polling status in maker specific trade detail. waitfortradepaymentreceived "$PAYER_PORT" "$PAYER" "$OFFER_ID" else # Bob waits for payment rcvd confirm from Alice, polling status in taker specific trade detail. waitfortradepaymentreceived "$PAYER_PORT" "$PAYER" "$OFFER_ID" fi # Generate some btc blocks printdate "Generating btc blocks after payment." genbtcblocks 2 2 printbreak # Complete the trade on both sides printdate "BOB $BOB_ROLE: Closing trade and keeping funds in Haveno wallet." CMD="$CLI_BASE --port=$BOB_PORT closetrade --trade-id=$OFFER_ID" printdate "BOB CLI: $CMD" KEEP_FUNDS_MSG=$($CMD) commandalert $? "Closed trade with closetrade command." # Print the closetrade command's console output. printdate "$KEEP_FUNDS_MSG" sleeptraced 3 printbreak printdate "ALICE (taker): Closing trade and keeping funds in Haveno wallet." CMD="$CLI_BASE --port=$ALICE_PORT closetrade --trade-id=$OFFER_ID" printdate "ALICE CLI: $CMD" KEEP_FUNDS_MSG=$($CMD) commandalert $? "Closed trade with closetrade command." # Print the closetrade command's console output. printdate "$KEEP_FUNDS_MSG" sleeptraced 3 printbreak printdate "Trade $OFFER_ID complete." } getcurrentprice() { PORT="$1" CURRENCY_CODE="$2" CMD="$CLI_BASE --port=$PORT getbtcprice --currency-code=$CURRENCY_CODE" CMD_OUTPUT=$($CMD) commandalert $? "Could not get current market $CURRENCY_CODE price." FLOOR=$(echo "$CMD_OUTPUT" | cut -d'.' -f 1) commandalert $? "Could not get the floor of the current market $CURRENCY_CODE price." INTEGER=$(echo "$FLOOR" | tr -cd '[[:digit:]]') commandalert $? "Could not convert the current market $CURRENCY_CODE price string to an integer." echo "$INTEGER" } ================================================ FILE: apitest/scripts/trade-simulation.sh ================================================ #! /bin/bash # Demonstrates a fiat <-> btc trade using the API CLI with a local regtest bitcoin node. # # A country code argument is used to create a country based face to face payment account for the simulated # trade, and the maker's face to face payment account's currency code is used when creating the offer. # # Prerequisites: # # - Linux or OSX with bash, Java 11-15 (JDK language compatibility 11), and bitcoin-core (v0.19 - v22). # # - Haveno must be fully built with apitest dao setup files installed. # Build command: `./gradlew clean build :apitest:installDaoSetup` # # - All supporting nodes must be run locally, in dev/dao/regtest mode: # bitcoind, seednode, arbdaemon, alicedaemon, bobdaemon # # These should be run using the apitest harness. From the root project dir, run: # `$ ./haveno-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon --shutdownAfterTests=false` # # - Only regtest btc can be bought or sold with the test payment account. # # Usage: # # This script must be run from the root of the project, e.g.: # # `$ apitest/scripts/trade-simulation.sh -d buy -c fr -m 3.00 -a 0.125` # # Script options: -d -c -m -f -a # # Examples: # # Create and take a buy/eur offer to buy 0.125 btc at a mkt-price-margin of 0%, using an Italy face to face # payment account: # # `$ apitest/scripts/trade-simulation.sh -d buy -c it -m 0.00 -a 0.125` # # Create and take a sell/eur offer to sell 0.125 btc at a fixed-price of 38,000 euros, using a France face to face # payment account: # # `$ apitest/scripts/trade-simulation.sh -d sell -c fr -f 38000 -a 0.125` export APP_BASE_NAME=$(basename "$0") export APP_HOME=$(pwd -P) export APITEST_SCRIPTS_HOME="$APP_HOME/apitest/scripts" source "$APITEST_SCRIPTS_HOME/trade-simulation-env.sh" source "$APITEST_SCRIPTS_HOME/trade-simulation-utils.sh" checksetup parseopts "$@" printdate "Started $APP_BASE_NAME with parameters:" printscriptparams printbreak # Demonstrate how to create a country based, face to face account. showcreatepaymentacctsteps "Alice" "$ALICE_PORT" CMD="$CLI_BASE --port=$ALICE_PORT createpaymentacct --payment-account-form=$APITEST_SCRIPTS_HOME/$F2F_ACCT_FORM" printdate "ALICE CLI: $CMD" CMD_OUTPUT=$(createpaymentacct "$CMD") echo "$CMD_OUTPUT" printbreak export ALICE_ACCT_ID=$(getnewpaymentacctid "$CMD_OUTPUT") export CURRENCY_CODE=$(getnewpaymentacctcurrency "$CMD_OUTPUT") printdate "Alice's F2F payment-account-id: $ALICE_ACCT_ID, currency-code: $CURRENCY_CODE" exitoncommandalert $? printbreak printdate "Bob creates his F2F payment account." CMD="$CLI_BASE --port=$BOB_PORT createpaymentacct --payment-account-form=$APITEST_SCRIPTS_HOME/$F2F_ACCT_FORM" printdate "BOB CLI: $CMD" CMD_OUTPUT=$(createpaymentacct "$CMD") echo "$CMD_OUTPUT" printbreak export BOB_ACCT_ID=$(getnewpaymentacctid "$CMD_OUTPUT") export CURRENCY_CODE=$(getnewpaymentacctcurrency "$CMD_OUTPUT") printdate "Bob's F2F payment-account-id: $BOB_ACCT_ID, currency-code: $CURRENCY_CODE" exitoncommandalert $? printbreak # Alice creates an offer. printdate "ALICE $ALICE_ROLE: Creating $DIRECTION $CURRENCY_CODE offer with payment acct $ALICE_ACCT_ID." CURRENT_PRICE=$(getcurrentprice "$ALICE_PORT" "$CURRENCY_CODE") exitoncommandalert $? printdate "Current Market Price: $CURRENT_PRICE" CMD=$(gencreateoffercommand "$ALICE_PORT" "$ALICE_ACCT_ID") printdate "ALICE CLI: $CMD" OFFER_ID=$(createoffer "$CMD") exitoncommandalert $? printdate "ALICE $ALICE_ROLE: Created offer with id: $OFFER_ID." printbreak sleeptraced 3 # Show Alice's new offer. printdate "ALICE $ALICE_ROLE: Looking at her new $DIRECTION $CURRENCY_CODE offer." CMD="$CLI_BASE --port=$ALICE_PORT getoffer --offer-id=$OFFER_ID" printdate "ALICE CLI: $CMD" OFFER=$($CMD) exitoncommandalert $? echo "$OFFER" printbreak sleeptraced 3 # Generate some btc blocks. printdate "Generating btc blocks after publishing Alice's offer." genbtcblocks 3 1 printbreak # Go through the trade protocol. executetrade exitoncommandalert $? printbreak # Get balances after trade completion. printdate "Bob & Alice's balances after trade:" printdate "ALICE CLI:" printbalances "$ALICE_PORT" printbreak printdate "BOB CLI:" printbalances "$BOB_PORT" printbreak exit 0 ================================================ FILE: apitest/scripts/trade-xmr-simulation.sh ================================================ #! /bin/bash # Runs xmr <-> btc trading scenarios using the API CLI with a local regtest bitcoin node. # # Prerequisites: # # - Linux or OSX with bash, Java 11-15 (JDK language compatibility 11), and bitcoin-core (v0.19 - v22). # # - Haveno must be fully built with apitest dao setup files installed. # Build command: `./gradlew clean build :apitest:installDaoSetup` # # - All supporting nodes must be run locally, in dev/dao/regtest mode: # bitcoind, seednode, arbdaemon, alicedaemon, bobdaemon # # These should be run using the apitest harness. From the root project dir, run: # `$ ./haveno-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon --shutdownAfterTests=false` # # Usage: # # This script must be run from the root of the project, e.g.: # # `$ apitest/scripts/trade-xmr-simulation.sh -d buy -f 0.05 -a 0.125` # # Script options: -d -m -f -a # # Examples: # # Create a buy/xmr offer to buy 0.125 btc at an xmr fixed-price of 0.05 btc, using an xmr payment account: # # `$ apitest/scripts/trade-xmr-simulation.sh -d buy -f 0.05 -a 0.125` # # Create a sell/xmr offer to sell 0.125 btc at at an xmr mkt-price-margin of 0%, using using an xmr payment account: # # `$ apitest/scripts/trade-xmr-simulation.sh -d sell -m 0.00 -a 0.125` export APP_BASE_NAME=$(basename "$0") export APP_HOME=$(pwd -P) export APITEST_SCRIPTS_HOME="$APP_HOME/apitest/scripts" export CURRENCY_CODE="XMR" export ALICE_XMR_ADDRESS="44i8xZbd8ecaD6nQQrHjr1BwTp6QfGL22iWqHZKmU4QYSyr1F64XAxM4HgvQHxbny7ehfxemaA9LPDLz2wY3fxhB1bbMEco" export BOB_XMR_ADDRESS="48xdBkXaCosPxcWwXRZdSGc33M9tYu6k9ga56dqkNrgsjQuJX16xW2qTyWTZstJpXXj87dj5p4H3y1xAfoVjAysoAYrXh2N" source "$APITEST_SCRIPTS_HOME/trade-simulation-env.sh" source "$APITEST_SCRIPTS_HOME/trade-simulation-utils.sh" checksetup parsexmrscriptopts "$@" printdate "Started $APP_BASE_NAME with parameters:" printscriptparams printbreak registerdisputeagents # Demonstrate how to create an XMR payment account. printdate "Create Alice's XMR Trading Payment Account." # Note: Having problems passing a double quoted --account-name param to function. CMD="$CLI_BASE --port=$ALICE_PORT createcryptopaymentacct --account-name=Alice_XMR_Account" CMD+=" --currency-code=XMR --address=$ALICE_XMR_ADDRESS --trade-instant=false" printdate "ALICE CLI: $CMD" CMD_OUTPUT=$(createpaymentacct "$CMD") echo "$CMD_OUTPUT" printbreak export ALICE_ACCT_ID=$(getnewpaymentacctid "$CMD_OUTPUT") printdate "Alice's XMR payment-account-id: $ALICE_ACCT_ID" exitoncommandalert $? printbreak printdate "Create Bob's XMR Trading Payment Account." # Note: Having problems passing a double quoted --account-name param to function. CMD="$CLI_BASE --port=$BOB_PORT createcryptopaymentacct --account-name=Bob_XMR_Account" CMD+=" --currency-code=XMR --address=$BOB_XMR_ADDRESS --trade-instant=false" printdate "BOB CLI: $CMD" CMD_OUTPUT=$(createpaymentacct "$CMD") echo "$CMD_OUTPUT" printbreak export BOB_ACCT_ID=$(getnewpaymentacctid "$CMD_OUTPUT") printdate "Bob's XMR payment-account-id: $BOB_ACCT_ID" exitoncommandalert $? printbreak # Alice creates an offer. printdate "ALICE $ALICE_ROLE: Creating $DIRECTION $CURRENCY_CODE offer with payment acct $ALICE_ACCT_ID." CMD=$(gencreateoffercommand "$ALICE_PORT" "$ALICE_ACCT_ID") printdate "ALICE CLI: $CMD" OFFER_ID=$(createoffer "$CMD") exitoncommandalert $? printdate "ALICE $ALICE_ROLE: Created offer with id: $OFFER_ID." printbreak sleeptraced 3 # Show Alice's new offer. printdate "ALICE $ALICE_ROLE: Looking at her new $DIRECTION $CURRENCY_CODE offer." CMD="$CLI_BASE --port=$ALICE_PORT getoffer --offer-id=$OFFER_ID" printdate "ALICE CLI: $CMD" OFFER=$($CMD) exitoncommandalert $? echo "$OFFER" printbreak sleeptraced 3 # Generate some btc blocks. printdate "Generating btc blocks after publishing Alice's offer." genbtcblocks 3 1 printbreak # Go through the trade protocol. executetrade exitoncommandalert $? printbreak # Get balances after trade completion. printdate "Bob & Alice's balances after trade:" printdate "ALICE CLI:" printbalances "$ALICE_PORT" printbreak printdate "BOB CLI:" printbalances "$BOB_PORT" printbreak exit 0 ================================================ FILE: apitest/scripts/version-parser.bash ================================================ #!/bin/bash # Bats helper script for parsing current version from Version.java. export CURRENT_VERSION=$(grep "String VERSION =" common/src/main/java/haveno/common/app/Version.java | sed 's/[^0-9.]*//g') ================================================ FILE: apitest/src/main/java/haveno/apitest/ApiTestMain.java ================================================ /* * 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 . */ package haveno.apitest; import haveno.apitest.config.ApiTestConfig; import lombok.extern.slf4j.Slf4j; import java.io.File; import static haveno.apitest.Scaffold.EXIT_FAILURE; import static haveno.apitest.Scaffold.EXIT_SUCCESS; import static haveno.apitest.config.ApiTestRateMeterInterceptorConfig.appendCallRateMeteringConfigPathOpt; import static haveno.apitest.config.ApiTestRateMeterInterceptorConfig.getTestRateMeterInterceptorConfig; import static haveno.apitest.config.ApiTestRateMeterInterceptorConfig.hasCallRateMeteringConfigPathOpt; import static java.lang.System.err; import static java.lang.System.exit; /** * ApiTestMain is a placeholder for the gradle build file, which requires a valid * 'mainClassName' property in the :apitest subproject configuration. * * It has some uses: * * It can be used to print test scaffolding options: haveno-apitest --help. * * It can be used to smoke test your bitcoind environment: haveno-apitest. * * It can be used to run the regtest environment for release testing: * haveno-test --shutdownAfterTests=false * * All method, scenario and end-to-end tests are found in the test sources folder. * * Requires bitcoind v0.19 - v22. */ @Slf4j public class ApiTestMain { public static void main(String[] args) { if (!hasCallRateMeteringConfigPathOpt(args)) new ApiTestMain().execute(getAppendedArgs(args)); else new ApiTestMain().execute(args); } public void execute(String[] args) { try { log.info("Configuring test harness with options:\n\t{}", String.join("\n\t", args)); Scaffold scaffold = new Scaffold(args).setUp(); ApiTestConfig config = scaffold.config; if (config.skipTests) { log.info("Skipping tests ..."); } else { new SmokeTestBitcoind(config).run(); } if (config.shutdownAfterTests) { scaffold.tearDown(); exit(EXIT_SUCCESS); } else { log.info("Not shutting down scaffolding background processes will run until ^C / kill -15 is rcvd ..."); } } catch (Throwable ex) { err.println("Fault: An unexpected error occurred. " + "Please file a report at https://github.com/haveno-dex/haveno/issues"); ex.printStackTrace(err); exit(EXIT_FAILURE); } } private static String[] getAppendedArgs(String[] args) { File rateMeterInterceptorConfig = getTestRateMeterInterceptorConfig(); return appendCallRateMeteringConfigPathOpt(args, rateMeterInterceptorConfig); } } ================================================ FILE: apitest/src/main/java/haveno/apitest/Scaffold.java ================================================ /* * 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 . */ package haveno.apitest; import haveno.apitest.config.ApiTestConfig; import haveno.apitest.config.HavenoAppConfig; import haveno.apitest.linux.BashCommand; import haveno.apitest.linux.BitcoinDaemon; import haveno.apitest.linux.HavenoProcess; import haveno.apitest.linux.LinuxProcess; import haveno.cli.GrpcClient; import haveno.common.config.HavenoHelpFormatter; import haveno.common.util.Utilities; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.PosixFilePermissions; import java.util.Objects; import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import static haveno.apitest.Scaffold.BitcoinCoreApp.bitcoind; import static haveno.apitest.config.ApiTestConfig.MEDIATOR; import static haveno.apitest.config.ApiTestConfig.REFUND_AGENT; import static haveno.apitest.config.HavenoAppConfig.alicedaemon; import static haveno.apitest.config.HavenoAppConfig.alicedesktop; import static haveno.apitest.config.HavenoAppConfig.arbdaemon; import static haveno.apitest.config.HavenoAppConfig.arbdesktop; import static haveno.apitest.config.HavenoAppConfig.bobdaemon; import static haveno.apitest.config.HavenoAppConfig.bobdesktop; import static haveno.apitest.config.HavenoAppConfig.seednode; import static haveno.common.app.DevEnv.DEV_PRIVILEGE_PRIV_KEY; import static java.lang.String.format; import static java.lang.System.exit; import static java.lang.System.out; import static java.net.InetAddress.getLoopbackAddress; import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; @Slf4j public class Scaffold { public static final int EXIT_SUCCESS = 0; public static final int EXIT_FAILURE = 1; public enum BitcoinCoreApp { bitcoind } public final ApiTestConfig config; @Nullable private SetupTask bitcoindTask; @Nullable private Future bitcoindTaskFuture; @Nullable private SetupTask seedNodeTask; @Nullable private Future seedNodeTaskFuture; @Nullable private SetupTask arbNodeTask; @Nullable private Future arbNodeTaskFuture; @Nullable private SetupTask aliceNodeTask; @Nullable private Future aliceNodeTaskFuture; @Nullable private SetupTask bobNodeTask; @Nullable private Future bobNodeTaskFuture; private final ExecutorService executor; /** * Constructor for passing comma delimited list of supporting apps to * ApiTestConfig, e.g., "bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon". * * @param supportingApps String */ public Scaffold(String supportingApps) { this(new ApiTestConfig("--supportingApps", supportingApps)); } /** * Constructor for passing options accepted by ApiTestConfig. * * @param args String[] */ public Scaffold(String[] args) { this(new ApiTestConfig(args)); } /** * Constructor for passing ApiTestConfig instance. * * @param config ApiTestConfig */ public Scaffold(ApiTestConfig config) { verifyNotWindows(); this.config = config; this.executor = Executors.newFixedThreadPool(config.supportingApps.size()); if (config.helpRequested) { config.printHelp(out, new HavenoHelpFormatter( "Haveno ApiTest", "haveno-apitest", "0.1.0")); exit(EXIT_SUCCESS); } } public Scaffold setUp() throws IOException, InterruptedException, ExecutionException { // Start each background process from an executor, then add a shutdown hook. CountDownLatch countdownLatch = new CountDownLatch(config.supportingApps.size()); startBackgroundProcesses(executor, countdownLatch); installShutdownHook(); // Wait for all submitted startup tasks to decrement the count of the latch. Objects.requireNonNull(countdownLatch).await(); // Verify each startup task's future is done. verifyStartupCompleted(); maybeRegisterDisputeAgents(); return this; } public void tearDown() { if (!executor.isTerminated()) { try { log.info("Shutting down executor service ..."); executor.shutdownNow(); //noinspection ResultOfMethodCallIgnored executor.awaitTermination(config.supportingApps.size() * 2000L, MILLISECONDS); SetupTask[] orderedTasks = new SetupTask[]{ bobNodeTask, aliceNodeTask, arbNodeTask, seedNodeTask, bitcoindTask}; Optional firstException = shutDownAll(orderedTasks); if (firstException.isPresent()) throw new IllegalStateException( "There were errors shutting down one or more background instances.", firstException.get()); else log.info("Teardown complete"); } catch (Exception ex) { throw new IllegalStateException(ex); } } } private Optional shutDownAll(SetupTask[] orderedTasks) { Optional firstException = Optional.empty(); for (SetupTask t : orderedTasks) { if (t != null && t.getLinuxProcess() != null) { try { LinuxProcess p = t.getLinuxProcess(); p.shutdown(); MILLISECONDS.sleep(1000); if (p.hasShutdownExceptions()) { // We log shutdown exceptions, but do not throw any from here // because all the background instances must be shut down. p.logExceptions(p.getShutdownExceptions(), log); // We cache only the 1st shutdown exception and move on to the // next process to be shutdown. This cached exception will be the // one thrown to the calling test case (the @AfterAll method). if (!firstException.isPresent()) firstException = Optional.of(p.getShutdownExceptions().get(0)); } } catch (InterruptedException ignored) { // empty } } } return firstException; } private void installBitcoinBlocknotify() { // gradle is not working for this try { Path srcPath = Paths.get(config.baseSrcResourcesDir, "blocknotify"); Path destPath = Paths.get(config.bitcoinDatadir, "blocknotify"); Files.copy(srcPath, destPath, REPLACE_EXISTING); String chmod700Perms = "rwx------"; Files.setPosixFilePermissions(destPath, PosixFilePermissions.fromString(chmod700Perms)); log.info("Installed {} with perms {}.", destPath, chmod700Perms); } catch (IOException e) { e.printStackTrace(); } } private void installCallRateMeteringConfiguration(String dataDir) throws IOException, InterruptedException { if (config.callRateMeteringConfigPath.isEmpty()) return; File testRateMeteringFile = new File(config.callRateMeteringConfigPath); if (!testRateMeteringFile.exists()) throw new FileNotFoundException( format("Call rate metering config file '%s' not found", config.callRateMeteringConfigPath)); BashCommand copyRateMeteringConfigFile = new BashCommand( "cp -rf " + config.callRateMeteringConfigPath + " " + dataDir); if (copyRateMeteringConfigFile.run().getExitStatus() != 0) throw new IllegalStateException( format("Could not install %s file in %s", testRateMeteringFile.getAbsolutePath(), dataDir)); Path destPath = Paths.get(dataDir, testRateMeteringFile.getName()); String chmod700Perms = "rwx------"; Files.setPosixFilePermissions(destPath, PosixFilePermissions.fromString(chmod700Perms)); log.info("Installed {} with perms {}.", destPath, chmod700Perms); } private void installShutdownHook() { // Background apps can be left running until the jvm is manually shutdown, // so we add a shutdown hook for that use case. Runtime.getRuntime().addShutdownHook(new Thread(this::tearDown)); } // Starts bitcoind and haveno apps (seednode, arbnode, etc...) private void startBackgroundProcesses(ExecutorService executor, CountDownLatch countdownLatch) throws InterruptedException, IOException { log.info("Starting supporting apps {}", config.supportingApps.toString()); if (config.hasSupportingApp(bitcoind.name())) { BitcoinDaemon bitcoinDaemon = new BitcoinDaemon(config); bitcoinDaemon.verifyBitcoinPathsExist(true); bitcoindTask = new SetupTask(bitcoinDaemon, countdownLatch); bitcoindTaskFuture = executor.submit(bitcoindTask); MILLISECONDS.sleep(config.havenoAppInitTime); LinuxProcess bitcoindProcess = bitcoindTask.getLinuxProcess(); if (bitcoindProcess.hasStartupExceptions()) { bitcoindProcess.logExceptions(bitcoindProcess.getStartupExceptions(), log); throw new IllegalStateException(bitcoindProcess.getStartupExceptions().get(0)); } bitcoinDaemon.verifyBitcoindRunning(); } // Start Haveno apps defined by the supportingApps option, in the in proper order. if (config.hasSupportingApp(seednode.name())) startHavenoApp(seednode, executor, countdownLatch); if (config.hasSupportingApp(arbdaemon.name())) startHavenoApp(arbdaemon, executor, countdownLatch); else if (config.hasSupportingApp(arbdesktop.name())) startHavenoApp(arbdesktop, executor, countdownLatch); if (config.hasSupportingApp(alicedaemon.name())) startHavenoApp(alicedaemon, executor, countdownLatch); else if (config.hasSupportingApp(alicedesktop.name())) startHavenoApp(alicedesktop, executor, countdownLatch); if (config.hasSupportingApp(bobdaemon.name())) startHavenoApp(bobdaemon, executor, countdownLatch); else if (config.hasSupportingApp(bobdesktop.name())) startHavenoApp(bobdesktop, executor, countdownLatch); } private void startHavenoApp(HavenoAppConfig HavenoAppConfig, ExecutorService executor, CountDownLatch countdownLatch) throws IOException, InterruptedException { HavenoProcess HavenoProcess = createHavenoProcess(HavenoAppConfig); switch (HavenoAppConfig) { case seednode: seedNodeTask = new SetupTask(HavenoProcess, countdownLatch); seedNodeTaskFuture = executor.submit(seedNodeTask); break; case arbdaemon: case arbdesktop: arbNodeTask = new SetupTask(HavenoProcess, countdownLatch); arbNodeTaskFuture = executor.submit(arbNodeTask); break; case alicedaemon: case alicedesktop: aliceNodeTask = new SetupTask(HavenoProcess, countdownLatch); aliceNodeTaskFuture = executor.submit(aliceNodeTask); break; case bobdaemon: case bobdesktop: bobNodeTask = new SetupTask(HavenoProcess, countdownLatch); bobNodeTaskFuture = executor.submit(bobNodeTask); break; default: throw new IllegalStateException("Unknown HavenoAppConfig " + HavenoAppConfig.name()); } log.info("Giving {} ms for {} to initialize ...", config.havenoAppInitTime, HavenoAppConfig.appName); MILLISECONDS.sleep(config.havenoAppInitTime); if (HavenoProcess.hasStartupExceptions()) { HavenoProcess.logExceptions(HavenoProcess.getStartupExceptions(), log); throw new IllegalStateException(HavenoProcess.getStartupExceptions().get(0)); } } private HavenoProcess createHavenoProcess(HavenoAppConfig HavenoAppConfig) throws IOException, InterruptedException { HavenoProcess HavenoProcess = new HavenoProcess(HavenoAppConfig, config); HavenoProcess.verifyAppNotRunning(); HavenoProcess.verifyAppDataDirInstalled(); return HavenoProcess; } private void verifyStartupCompleted() throws ExecutionException, InterruptedException { if (bitcoindTaskFuture != null) verifyStartupCompleted(bitcoindTaskFuture); if (seedNodeTaskFuture != null) verifyStartupCompleted(seedNodeTaskFuture); if (arbNodeTaskFuture != null) verifyStartupCompleted(arbNodeTaskFuture); if (aliceNodeTaskFuture != null) verifyStartupCompleted(aliceNodeTaskFuture); if (bobNodeTaskFuture != null) verifyStartupCompleted(bobNodeTaskFuture); } private void verifyStartupCompleted(Future futureStatus) throws ExecutionException, InterruptedException { for (int i = 0; i < 10; i++) { if (futureStatus.isDone()) { log.info("{} completed startup at {} {}", futureStatus.get().getName(), futureStatus.get().getStartTime().toLocalDate(), futureStatus.get().getStartTime().toLocalTime()); return; } else { // We are giving the thread more time to terminate after the countdown // latch reached 0. If we are running only bitcoind, we need to be even // more lenient. SECONDS.sleep(config.supportingApps.size() == 1 ? 2 : 1); } } throw new IllegalStateException(format("%s did not complete startup", futureStatus.get().getName())); } private void verifyNotWindows() { if (Utilities.isWindows()) throw new IllegalStateException("ApiTest not supported on Windows"); } private void maybeRegisterDisputeAgents() { if (config.hasSupportingApp(arbdaemon.name()) && config.registerDisputeAgents) { log.info("Option --registerDisputeAgents=true, registering dispute agents in arbdaemon ..."); GrpcClient arbClient = new GrpcClient(getLoopbackAddress().getHostAddress(), arbdaemon.apiPort, config.apiPassword); arbClient.registerDisputeAgent(MEDIATOR, DEV_PRIVILEGE_PRIV_KEY); arbClient.registerDisputeAgent(REFUND_AGENT, DEV_PRIVILEGE_PRIV_KEY); } } } ================================================ FILE: apitest/src/main/java/haveno/apitest/SetupTask.java ================================================ /* * 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 . */ package haveno.apitest; import haveno.apitest.linux.LinuxProcess; import lombok.extern.slf4j.Slf4j; import java.time.LocalDateTime; import java.util.Objects; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import static java.lang.String.format; import static java.util.concurrent.TimeUnit.MILLISECONDS; @Slf4j public class SetupTask implements Callable { private final LinuxProcess linuxProcess; private final CountDownLatch countdownLatch; public SetupTask(LinuxProcess linuxProcess, CountDownLatch countdownLatch) { this.linuxProcess = linuxProcess; this.countdownLatch = countdownLatch; } @Override public Status call() throws Exception { try { linuxProcess.start(); // always runs in background MILLISECONDS.sleep(1000); // give 1s for bg process to init } catch (InterruptedException ex) { throw new IllegalStateException(format("Error starting %s", linuxProcess.getName()), ex); } Objects.requireNonNull(countdownLatch).countDown(); return new Status(linuxProcess.getName(), LocalDateTime.now()); } public LinuxProcess getLinuxProcess() { return linuxProcess; } public static class Status { private final String name; private final LocalDateTime startTime; public Status(String name, LocalDateTime startTime) { super(); this.name = name; this.startTime = startTime; } public String getName() { return name; } public LocalDateTime getStartTime() { return startTime; } @Override public String toString() { return "SetupTask.Status [name=" + name + ", completionTime=" + startTime + "]"; } } } ================================================ FILE: apitest/src/main/java/haveno/apitest/SmokeTestBashCommand.java ================================================ /* * 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 . */ package haveno.apitest; import haveno.apitest.linux.BashCommand; import lombok.extern.slf4j.Slf4j; import java.io.IOException; @Slf4j class SmokeTestBashCommand { public SmokeTestBashCommand() { } public void runSmokeTest() { try { BashCommand cmd = new BashCommand("ls -l").run(); log.info("$ {}\n{}", cmd.getCommand(), cmd.getOutput()); cmd = new BashCommand("free -g").run(); log.info("$ {}\n{}", cmd.getCommand(), cmd.getOutput()); cmd = new BashCommand("date").run(); log.info("$ {}\n{}", cmd.getCommand(), cmd.getOutput()); cmd = new BashCommand("netstat -a | grep localhost").run(); log.info("$ {}\n{}", cmd.getCommand(), cmd.getOutput()); } catch (IOException | InterruptedException e) { e.printStackTrace(); } } } ================================================ FILE: apitest/src/main/java/haveno/apitest/SmokeTestBitcoind.java ================================================ /* * 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 . */ package haveno.apitest; import haveno.apitest.config.ApiTestConfig; import haveno.apitest.linux.BitcoinCli; import lombok.extern.slf4j.Slf4j; import java.io.IOException; import static java.lang.String.format; @Slf4j class SmokeTestBitcoind { private final ApiTestConfig config; public SmokeTestBitcoind(ApiTestConfig config) { this.config = config; } public void run() throws IOException, InterruptedException { runBitcoinGetWalletInfo(); // smoke test bitcoin-cli String newBitcoinAddress = getNewAddress(); generateToAddress(1, newBitcoinAddress); } public void runBitcoinGetWalletInfo() throws IOException, InterruptedException { // This might be good for a sanity check to make sure the regtest data was installed. log.info("Smoke test bitcoin-cli getwalletinfo"); BitcoinCli walletInfo = new BitcoinCli(config, "getwalletinfo").run(); log.info("{}\n{}", walletInfo.getCommandWithOptions(), walletInfo.getOutput()); log.info("balance str = {}", walletInfo.getOutputValueAsString("balance")); log.info("balance dbl = {}", walletInfo.getOutputValueAsDouble("balance")); log.info("keypoololdest long = {}", walletInfo.getOutputValueAsLong("keypoololdest")); log.info("paytxfee dbl = {}", walletInfo.getOutputValueAsDouble("paytxfee")); log.info("keypoolsize_hd_internal int = {}", walletInfo.getOutputValueAsInt("keypoolsize_hd_internal")); log.info("private_keys_enabled bool = {}", walletInfo.getOutputValueAsBoolean("private_keys_enabled")); log.info("hdseedid str = {}", walletInfo.getOutputValueAsString("hdseedid")); } public String getNewAddress() throws IOException, InterruptedException { BitcoinCli newAddress = new BitcoinCli(config, "getnewaddress").run(); log.info("{}\n{}", newAddress.getCommandWithOptions(), newAddress.getOutput()); return newAddress.getOutput(); } public void generateToAddress(int blocks, String address) throws IOException, InterruptedException { String generateToAddressCmd = format("generatetoaddress %d \"%s\"", blocks, address); BitcoinCli generateToAddress = new BitcoinCli(config, generateToAddressCmd).run(); // Return value is an array of TxIDs. log.info("{}\n{}", generateToAddress.getCommandWithOptions(), generateToAddress.getOutputValueAsStringArray()); } } ================================================ FILE: apitest/src/main/java/haveno/apitest/config/ApiTestConfig.java ================================================ /* * 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 . */ package haveno.apitest.config; import haveno.common.config.CompositeOptionSet; import joptsimple.AbstractOptionSpec; import joptsimple.ArgumentAcceptingOptionSpec; import joptsimple.HelpFormatter; import joptsimple.OptionException; import joptsimple.OptionParser; import joptsimple.OptionSet; import joptsimple.OptionSpec; import lombok.extern.slf4j.Slf4j; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.OutputStream; import java.io.UncheckedIOException; import java.net.InetAddress; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.Properties; import static java.lang.String.format; import static java.lang.System.getProperty; import static java.lang.System.getenv; import static java.util.Arrays.asList; import static java.util.Arrays.stream; import static joptsimple.internal.Strings.EMPTY; @Slf4j public class ApiTestConfig { // Global constants public static final String BTC = "BTC"; public static final String EUR = "EUR"; public static final String USD = "USD"; public static final String XMR = "XMR"; public static final String ARBITRATOR = "arbitrator"; public static final String MEDIATOR = "mediator"; public static final String REFUND_AGENT = "refundagent"; // Option name constants static final String HELP = "help"; static final String BASH_PATH = "bashPath"; static final String BERKELEYDB_LIB_PATH = "berkeleyDbLibPath"; static final String BITCOIN_PATH = "bitcoinPath"; static final String BITCOIN_RPC_PORT = "bitcoinRpcPort"; static final String BITCOIN_RPC_USER = "bitcoinRpcUser"; static final String BITCOIN_RPC_PASSWORD = "bitcoinRpcPassword"; static final String BITCOIN_REGTEST_HOST = "bitcoinRegtestHost"; static final String CONFIG_FILE = "configFile"; static final String ROOT_APP_DATA_DIR = "rootAppDataDir"; static final String API_PASSWORD = "apiPassword"; static final String RUN_SUBPROJECT_JARS = "runSubprojectJars"; static final String HAVENO_APP_INIT_TIME = "havenoAppInitTime"; static final String SKIP_TESTS = "skipTests"; static final String SHUTDOWN_AFTER_TESTS = "shutdownAfterTests"; static final String SUPPORTING_APPS = "supportingApps"; static final String CALL_RATE_METERING_CONFIG_PATH = "callRateMeteringConfigPath"; static final String ENABLE_HAVENO_DEBUGGING = "enableHavenoDebugging"; static final String REGISTER_DISPUTE_AGENTS = "registerDisputeAgents"; // Default values for certain options static final String DEFAULT_CONFIG_FILE_NAME = "apitest.properties"; // Static fields that provide access to Config properties in locations where injecting // a Config instance is not feasible. public static String BASH_PATH_VALUE; public final File defaultConfigFile; // Options supported only at the command line, not within a config file. public final boolean helpRequested; public final File configFile; // Options supported at the command line and a config file. public final File rootAppDataDir; public final String bashPath; public final String berkeleyDbLibPath; public final String bitcoinPath; public final String bitcoinRegtestHost; public final int bitcoinRpcPort; public final String bitcoinRpcUser; public final String bitcoinRpcPassword; // Daemon instances can use same gRPC password, but each needs a different apiPort. public final String apiPassword; public final boolean runSubprojectJars; public final long havenoAppInitTime; public final boolean skipTests; public final boolean shutdownAfterTests; public final List supportingApps; public final String callRateMeteringConfigPath; public final boolean enableHavenoDebugging; public final boolean registerDisputeAgents; // Immutable system configurations set in the constructor. public final String bitcoinDatadir; public final String userDir; public final boolean isRunningTest; public final String rootProjectDir; public final String baseBuildResourcesDir; public final String baseSrcResourcesDir; // The parser that will be used to parse both cmd line and config file options private final OptionParser parser = new OptionParser(); public ApiTestConfig(String... args) { this.userDir = getProperty("user.dir"); // If running a @Test, the current working directory is the :apitest subproject // folder. If running ApiTestMain, the current working directory is the // haveno root project folder. this.isRunningTest = Paths.get(userDir).getFileName().toString().equals("apitest"); this.rootProjectDir = isRunningTest ? Paths.get(userDir).getParent().toFile().getAbsolutePath() : Paths.get(userDir).toFile().getAbsolutePath(); this.baseBuildResourcesDir = Paths.get(rootProjectDir, "apitest", "build", "resources", "main") .toFile().getAbsolutePath(); this.baseSrcResourcesDir = Paths.get(rootProjectDir, "apitest", "src", "main", "resources") .toFile().getAbsolutePath(); this.defaultConfigFile = absoluteConfigFile(baseBuildResourcesDir, DEFAULT_CONFIG_FILE_NAME); this.bitcoinDatadir = Paths.get(baseBuildResourcesDir, "Bitcoin-regtest").toFile().getAbsolutePath(); AbstractOptionSpec helpOpt = parser.accepts(HELP, "Print this help text") .forHelp(); ArgumentAcceptingOptionSpec configFileOpt = parser.accepts(CONFIG_FILE, format("Specify configuration file. " + "Relative paths will be prefixed by %s location.", userDir)) .withRequiredArg() .ofType(String.class) .defaultsTo(DEFAULT_CONFIG_FILE_NAME); ArgumentAcceptingOptionSpec appDataDirOpt = parser.accepts(ROOT_APP_DATA_DIR, "Application data directory") .withRequiredArg() .ofType(File.class) .defaultsTo(new File(baseBuildResourcesDir)); ArgumentAcceptingOptionSpec bashPathOpt = parser.accepts(BASH_PATH, "Bash path") .withRequiredArg() .ofType(String.class) .defaultsTo( (getenv("SHELL") == null || !getenv("SHELL").contains("bash")) ? "/bin/bash" : getenv("SHELL")); ArgumentAcceptingOptionSpec berkeleyDbLibPathOpt = parser.accepts(BERKELEYDB_LIB_PATH, "Berkeley DB lib path") .withRequiredArg() .ofType(String.class).defaultsTo(EMPTY); ArgumentAcceptingOptionSpec bitcoinPathOpt = parser.accepts(BITCOIN_PATH, "Bitcoin path") .withRequiredArg() .ofType(String.class).defaultsTo("/usr/local/bin"); ArgumentAcceptingOptionSpec bitcoinRegtestHostOpt = parser.accepts(BITCOIN_REGTEST_HOST, "Bitcoin Core regtest host") .withRequiredArg() .ofType(String.class).defaultsTo(InetAddress.getLoopbackAddress().getHostAddress()); ArgumentAcceptingOptionSpec bitcoinRpcPortOpt = parser.accepts(BITCOIN_RPC_PORT, "Bitcoin Core rpc port (non-default)") .withRequiredArg() .ofType(Integer.class).defaultsTo(19443); ArgumentAcceptingOptionSpec bitcoinRpcUserOpt = parser.accepts(BITCOIN_RPC_USER, "Bitcoin rpc user") .withRequiredArg() .ofType(String.class).defaultsTo("apitest"); ArgumentAcceptingOptionSpec bitcoinRpcPasswordOpt = parser.accepts(BITCOIN_RPC_PASSWORD, "Bitcoin rpc password") .withRequiredArg() .ofType(String.class).defaultsTo("apitest"); ArgumentAcceptingOptionSpec apiPasswordOpt = parser.accepts(API_PASSWORD, "gRPC API password") .withRequiredArg() .defaultsTo("xyz"); ArgumentAcceptingOptionSpec runSubprojectJarsOpt = parser.accepts(RUN_SUBPROJECT_JARS, "Run subproject build jars instead of full build jars") .withRequiredArg() .ofType(Boolean.class) .defaultsTo(false); ArgumentAcceptingOptionSpec havenoAppInitTimeOpt = parser.accepts(HAVENO_APP_INIT_TIME, "Amount of time (ms) to wait on a Haveno instance's initialization") .withRequiredArg() .ofType(Long.class) .defaultsTo(5000L); ArgumentAcceptingOptionSpec skipTestsOpt = parser.accepts(SKIP_TESTS, "Start apps, but skip tests") .withRequiredArg() .ofType(Boolean.class) .defaultsTo(false); ArgumentAcceptingOptionSpec shutdownAfterTestsOpt = parser.accepts(SHUTDOWN_AFTER_TESTS, "Terminate all processes after tests") .withRequiredArg() .ofType(Boolean.class) .defaultsTo(true); ArgumentAcceptingOptionSpec supportingAppsOpt = parser.accepts(SUPPORTING_APPS, "Comma delimited list of supporting apps (bitcoind,seednode,arbdaemon,...") .withRequiredArg() .ofType(String.class) .defaultsTo("bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon"); ArgumentAcceptingOptionSpec callRateMeteringConfigPathOpt = parser.accepts(CALL_RATE_METERING_CONFIG_PATH, "Install a ratemeters.json file to configure call rate metering interceptors") .withRequiredArg() .defaultsTo(EMPTY); ArgumentAcceptingOptionSpec enableHavenoDebuggingOpt = parser.accepts(ENABLE_HAVENO_DEBUGGING, "Start Haveno apps with remote debug options") .withRequiredArg() .ofType(Boolean.class) .defaultsTo(false); ArgumentAcceptingOptionSpec registerDisputeAgentsOpt = parser.accepts(REGISTER_DISPUTE_AGENTS, "Register dispute agents in arbitration daemon") .withRequiredArg() .ofType(Boolean.class) .defaultsTo(true); try { CompositeOptionSet options = new CompositeOptionSet(); // Parse command line options OptionSet cliOpts = parser.parse(args); options.addOptionSet(cliOpts); // Parse config file specified at the command line only if it was specified as // an absolute path. Otherwise, the config file will be processed later below. File configFile = null; OptionSpec[] disallowedOpts = new OptionSpec[]{helpOpt, configFileOpt}; final boolean cliHasConfigFileOpt = cliOpts.has(configFileOpt); boolean configFileHasBeenProcessed = false; if (cliHasConfigFileOpt) { configFile = new File(cliOpts.valueOf(configFileOpt)); if (configFile.isAbsolute()) { Optional configFileOpts = parseOptionsFrom(configFile, disallowedOpts); if (configFileOpts.isPresent()) { options.addOptionSet(configFileOpts.get()); configFileHasBeenProcessed = true; } } } // If the config file has not yet been processed, either because a relative // path was provided at the command line, or because no value was provided at // the command line, attempt to process the file now, falling back to the // default config file location if none was specified at the command line. if (!configFileHasBeenProcessed) { configFile = cliHasConfigFileOpt && !configFile.isAbsolute() ? absoluteConfigFile(userDir, configFile.getPath()) : defaultConfigFile; Optional configFileOpts = parseOptionsFrom(configFile, disallowedOpts); configFileOpts.ifPresent(options::addOptionSet); } // Assign all remaining properties, with command line options taking // precedence over those provided in the config file (if any) this.helpRequested = options.has(helpOpt); this.configFile = configFile; this.rootAppDataDir = options.valueOf(appDataDirOpt); bashPath = options.valueOf(bashPathOpt); this.berkeleyDbLibPath = options.valueOf(berkeleyDbLibPathOpt); this.bitcoinPath = options.valueOf(bitcoinPathOpt); this.bitcoinRegtestHost = options.valueOf(bitcoinRegtestHostOpt); this.bitcoinRpcPort = options.valueOf(bitcoinRpcPortOpt); this.bitcoinRpcUser = options.valueOf(bitcoinRpcUserOpt); this.bitcoinRpcPassword = options.valueOf(bitcoinRpcPasswordOpt); this.apiPassword = options.valueOf(apiPasswordOpt); this.runSubprojectJars = options.valueOf(runSubprojectJarsOpt); this.havenoAppInitTime = options.valueOf(havenoAppInitTimeOpt); this.skipTests = options.valueOf(skipTestsOpt); this.shutdownAfterTests = options.valueOf(shutdownAfterTestsOpt); this.supportingApps = asList(options.valueOf(supportingAppsOpt).split(",")); this.callRateMeteringConfigPath = options.valueOf(callRateMeteringConfigPathOpt); this.enableHavenoDebugging = options.valueOf(enableHavenoDebuggingOpt); this.registerDisputeAgents = options.valueOf(registerDisputeAgentsOpt); // Assign values to special-case static fields. BASH_PATH_VALUE = bashPath; } catch (OptionException ex) { throw new IllegalStateException(format("Problem parsing option '%s': %s", ex.options().get(0), ex.getCause() != null ? ex.getCause().getMessage() : ex.getMessage())); } } public boolean hasSupportingApp(String... supportingApp) { return stream(supportingApp).anyMatch(this.supportingApps::contains); } public void printHelp(OutputStream sink, HelpFormatter formatter) { try { parser.formatHelpWith(formatter); parser.printHelpOn(sink); } catch (IOException ex) { throw new UncheckedIOException(ex); } } private Optional parseOptionsFrom(File configFile, OptionSpec[] disallowedOpts) { if (!configFile.exists() && !configFile.equals(absoluteConfigFile(userDir, DEFAULT_CONFIG_FILE_NAME))) throw new IllegalStateException(format("The specified config file '%s' does not exist.", configFile)); Properties properties = getProperties(configFile); List optionLines = new ArrayList<>(); properties.forEach((k, v) -> { optionLines.add("--" + k + "=" + v); // dashes expected by jopt parser below }); OptionSet configFileOpts = parser.parse(optionLines.toArray(new String[0])); for (OptionSpec disallowedOpt : disallowedOpts) if (configFileOpts.has(disallowedOpt)) throw new IllegalStateException( format("The '%s' option is disallowed in config files", disallowedOpt.options().get(0))); return Optional.of(configFileOpts); } private Properties getProperties(File configFile) { try { Properties properties = new Properties(); properties.load(new FileInputStream(configFile.getAbsolutePath())); return properties; } catch (IOException ex) { throw new IllegalStateException( format("Could not load properties from config file %s", configFile.getAbsolutePath()), ex); } } private static File absoluteConfigFile(String parentDir, String relativeConfigFilePath) { return new File(parentDir, relativeConfigFilePath); } } ================================================ FILE: apitest/src/main/java/haveno/apitest/config/ApiTestRateMeterInterceptorConfig.java ================================================ package haveno.apitest.config; import haveno.daemon.grpc.GrpcVersionService; import haveno.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig; import java.io.File; import static haveno.apitest.config.ApiTestConfig.CALL_RATE_METERING_CONFIG_PATH; import static haveno.proto.grpc.DisputeAgentsGrpc.getRegisterDisputeAgentMethod; import static haveno.proto.grpc.GetVersionGrpc.getGetVersionMethod; import static java.lang.System.arraycopy; import static java.util.Arrays.stream; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; public class ApiTestRateMeterInterceptorConfig { public static File getTestRateMeterInterceptorConfig() { GrpcServiceRateMeteringConfig.Builder builder = new GrpcServiceRateMeteringConfig.Builder(); builder.addCallRateMeter(GrpcVersionService.class.getSimpleName(), getGetVersionMethod().getFullMethodName(), 1, SECONDS); // Only GrpcVersionService is @VisibleForTesting, so we need to // hardcode other grpcServiceClassName parameter values used in // builder.addCallRateMeter(...). builder.addCallRateMeter("GrpcDisputeAgentsService", getRegisterDisputeAgentMethod().getFullMethodName(), 10, // Same as default. SECONDS); // Define rate meters for non-existent method 'disabled', to override other grpc // services' default rate meters -- defined in their rateMeteringInterceptor() // methods. String[] serviceClassNames = new String[]{ "GrpcGetTradeStatisticsService", "GrpcHelpService", "GrpcOffersService", "GrpcPaymentAccountsService", "GrpcPriceService", "GrpcTradesService", "GrpcWalletsService" }; for (String service : serviceClassNames) { builder.addCallRateMeter(service, "disabled", 1, MILLISECONDS); } File file = builder.build(); file.deleteOnExit(); return file; } public static boolean hasCallRateMeteringConfigPathOpt(String[] args) { return stream(args).anyMatch(a -> a.contains("--" + CALL_RATE_METERING_CONFIG_PATH)); } public static String[] appendCallRateMeteringConfigPathOpt(String[] args, File rateMeterInterceptorConfig) { String[] rateMeteringConfigPathOpt = new String[]{ "--" + CALL_RATE_METERING_CONFIG_PATH + "=" + rateMeterInterceptorConfig.getAbsolutePath() }; if (args.length == 0) { return rateMeteringConfigPathOpt; } else { String[] appendedOpts = new String[args.length + 1]; arraycopy(args, 0, appendedOpts, 0, args.length); arraycopy(rateMeteringConfigPathOpt, 0, appendedOpts, args.length, rateMeteringConfigPathOpt.length); return appendedOpts; } } } ================================================ FILE: apitest/src/main/java/haveno/apitest/config/HavenoAppConfig.java ================================================ /* * 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 . */ package haveno.apitest.config; import haveno.daemon.app.HavenoDaemonMain; import haveno.desktop.app.HavenoAppMain; import haveno.seednode.SeedNodeMain; /** Some non user configurable Haveno seednode, arb node, bob and alice daemon option values. @see dev-setup.md */ public enum HavenoAppConfig { seednode("haveno-XMR_STAGENET_Seed_2002", "haveno-seednode", "-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", SeedNodeMain.class.getName(), 2002, 5120, -1, 49996), arbdaemon("haveno-XMR_STAGENET_Arb", "haveno-daemon", "-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", HavenoDaemonMain.class.getName(), 4444, 5121, 9997, 49997), arbdesktop("haveno-XMR_STAGENET_Arb", "haveno-desktop", "-XX:MaxRAM=3g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", HavenoAppMain.class.getName(), 4444, 5121, -1, 49997), alicedaemon("haveno-XMR_STAGENET_Alice", "haveno-daemon", "-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", HavenoDaemonMain.class.getName(), 7777, 5122, 9998, 49998), alicedesktop("haveno-XMR_STAGENET_Alice", "haveno-desktop", "-XX:MaxRAM=4g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", HavenoAppMain.class.getName(), 7777, 5122, -1, 49998), bobdaemon("haveno-XMR_STAGENET_Bob", "haveno-daemon", "-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", HavenoDaemonMain.class.getName(), 8888, 5123, 9999, 49999), bobdesktop("haveno-XMR_STAGENET_Bob", "haveno-desktop", "-XX:MaxRAM=4g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", HavenoAppMain.class.getName(), 8888, 5123, -1, 49999); public final String appName; public final String startupScript; public final String javaOpts; public final String mainClassName; public final int nodePort; public final int rpcBlockNotificationPort; // Daemons can use a global gRPC password, but each needs a unique apiPort. public final int apiPort; public final int remoteDebugPort; HavenoAppConfig(String appName, String startupScript, String javaOpts, String mainClassName, int nodePort, int rpcBlockNotificationPort, int apiPort, int remoteDebugPort) { this.appName = appName; this.startupScript = startupScript; this.javaOpts = javaOpts; this.mainClassName = mainClassName; this.nodePort = nodePort; this.rpcBlockNotificationPort = rpcBlockNotificationPort; this.apiPort = apiPort; this.remoteDebugPort = remoteDebugPort; } @Override public String toString() { return "HavenoAppConfig{" + "\n" + " appName='" + appName + '\'' + "\n" + ", startupScript='" + startupScript + '\'' + "\n" + ", javaOpts='" + javaOpts + '\'' + "\n" + ", mainClassName='" + mainClassName + '\'' + "\n" + ", nodePort=" + nodePort + "\n" + ", rpcBlockNotificationPort=" + rpcBlockNotificationPort + "\n" + ", apiPort=" + apiPort + "\n" + ", remoteDebugPort=" + remoteDebugPort + "\n" + '}'; } } ================================================ FILE: apitest/src/main/java/haveno/apitest/linux/AbstractLinuxProcess.java ================================================ /* * 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 . */ package haveno.apitest.linux; import haveno.apitest.config.ApiTestConfig; import lombok.extern.slf4j.Slf4j; import java.io.File; import java.io.IOException; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import static haveno.apitest.linux.BashCommand.isAlive; import static java.lang.String.format; import static joptsimple.internal.Strings.EMPTY; @Slf4j abstract class AbstractLinuxProcess implements LinuxProcess { protected final String name; protected final ApiTestConfig config; protected long pid; protected final List startupExceptions; protected final List shutdownExceptions; public AbstractLinuxProcess(String name, ApiTestConfig config) { this.name = name; this.config = config; this.startupExceptions = new ArrayList<>(); this.shutdownExceptions = new ArrayList<>(); } @Override public String getName() { return this.name; } @Override public boolean hasStartupExceptions() { return !startupExceptions.isEmpty(); } @Override public boolean hasShutdownExceptions() { return !shutdownExceptions.isEmpty(); } @Override public void logExceptions(List exceptions, org.slf4j.Logger log) { for (Throwable t : exceptions) { log.error("", t); } } @Override public List getStartupExceptions() { return startupExceptions; } @Override public List getShutdownExceptions() { return shutdownExceptions; } @SuppressWarnings("unused") public void verifyBitcoinPathsExist() { verifyBitcoinPathsExist(false); } public void verifyBitcoinPathsExist(boolean verbose) { if (verbose) log.info(format("Checking bitcoind env...%n" + "\t%-20s%s%n\t%-20s%s%n\t%-20s%s%n\t%-20s%s", "berkeleyDbLibPath", config.berkeleyDbLibPath, "bitcoinPath", config.bitcoinPath, "bitcoinDatadir", config.bitcoinDatadir, "blocknotify", config.bitcoinDatadir + "/blocknotify")); if (!config.berkeleyDbLibPath.equals(EMPTY)) { File berkeleyDbLibPath = new File(config.berkeleyDbLibPath); if (!berkeleyDbLibPath.exists() || !berkeleyDbLibPath.canExecute()) throw new IllegalStateException(berkeleyDbLibPath + " cannot be found or executed"); } File bitcoindExecutable = Paths.get(config.bitcoinPath, "bitcoind").toFile(); if (!bitcoindExecutable.exists() || !bitcoindExecutable.canExecute()) throw new IllegalStateException(format("'%s' cannot be found or executed.%n" + "A bitcoin-core v0.19 - v22 installation is required," + " and the 'bitcoinPath' must be configured in 'apitest.properties'", bitcoindExecutable.getAbsolutePath())); File bitcoindDatadir = new File(config.bitcoinDatadir); if (!bitcoindDatadir.exists() || !bitcoindDatadir.canWrite()) throw new IllegalStateException(bitcoindDatadir + " cannot be found or written to"); File blocknotify = new File(bitcoindDatadir, "blocknotify"); if (!blocknotify.exists() || !blocknotify.canExecute()) throw new IllegalStateException(blocknotify.getAbsolutePath() + " cannot be found or executed"); } public void verifyBitcoindRunning() throws IOException, InterruptedException { long bitcoindPid = BashCommand.getPid("bitcoind"); if (bitcoindPid < 0 || !isAlive(bitcoindPid)) throw new IllegalStateException("Bitcoind not running"); } } ================================================ FILE: apitest/src/main/java/haveno/apitest/linux/BashCommand.java ================================================ /* * 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 . */ package haveno.apitest.linux; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.io.IOException; import java.util.ArrayList; import java.util.List; import static haveno.apitest.config.ApiTestConfig.BASH_PATH_VALUE; import static java.lang.management.ManagementFactory.getRuntimeMXBean; @Slf4j public class BashCommand { private int exitStatus = -1; @Nullable private String output; @Nullable private String error; private final String command; private final int numResponseLines; public BashCommand(String command) { this(command, 0); } public BashCommand(String command, int numResponseLines) { this.command = command; this.numResponseLines = numResponseLines; // only want the top N lines of output } public BashCommand run() throws IOException, InterruptedException { SystemCommandExecutor commandExecutor = new SystemCommandExecutor(tokenizeSystemCommand()); exitStatus = commandExecutor.exec(); processOutput(commandExecutor); return this; } public BashCommand runInBackground() throws IOException, InterruptedException { SystemCommandExecutor commandExecutor = new SystemCommandExecutor(tokenizeSystemCommand()); exitStatus = commandExecutor.exec(false); processOutput(commandExecutor); return this; } private void processOutput(SystemCommandExecutor commandExecutor) { // Get the error status and stderr from system command. StringBuilder stderr = commandExecutor.getStandardErrorFromCommand(); if (stderr.length() > 0) error = stderr.toString(); if (exitStatus != 0) return; // Format and cache the stdout from system command. StringBuilder stdout = commandExecutor.getStandardOutputFromCommand(); String[] rawLines = stdout.toString().split("\n"); StringBuilder truncatedLines = new StringBuilder(); int limit = numResponseLines > 0 ? Math.min(numResponseLines, rawLines.length) : rawLines.length; for (int i = 0; i < limit; i++) { String line = rawLines[i].length() >= 220 ? rawLines[i].substring(0, 220) + " ..." : rawLines[i]; truncatedLines.append(line).append((i < limit - 1) ? "\n" : ""); } output = truncatedLines.toString(); } public String getCommand() { return this.command; } public int getExitStatus() { return this.exitStatus; } // TODO return Optional @Nullable public String getOutput() { return this.output; } // TODO return Optional public String getError() { return this.error; } private List tokenizeSystemCommand() { return new ArrayList<>() {{ add(BASH_PATH_VALUE); add("-c"); add(command); }}; } @SuppressWarnings("unused") // Convenience method for getting system load info. public static String printSystemLoadString(Exception tracingException) throws IOException, InterruptedException { StackTraceElement[] stackTraceElement = tracingException.getStackTrace(); StringBuilder stackTraceBuilder = new StringBuilder(tracingException.getMessage()).append("\n"); int traceLimit = Math.min(stackTraceElement.length, 4); for (int i = 0; i < traceLimit; i++) { stackTraceBuilder.append(stackTraceElement[i]).append("\n"); } stackTraceBuilder.append("..."); log.info(stackTraceBuilder.toString()); BashCommand cmd = new BashCommand("ps -aux --sort -rss --headers", 2).run(); return cmd.getOutput() + "\n" + "System load: Memory (MB): " + getUsedMemoryInMB() + " / No. of threads: " + Thread.activeCount() + " JVM uptime (ms): " + getRuntimeMXBean().getUptime(); } public static long getUsedMemoryInMB() { Runtime runtime = Runtime.getRuntime(); long free = runtime.freeMemory() / 1024 / 1024; long total = runtime.totalMemory() / 1024 / 1024; return total - free; } public static long getPid(String processName) throws IOException, InterruptedException { String psCmd = "ps aux | pgrep " + processName + " | grep -v grep"; String psCmdOutput = new BashCommand(psCmd).run().getOutput(); if (psCmdOutput == null || psCmdOutput.isEmpty()) return -1; return Long.parseLong(psCmdOutput); } @SuppressWarnings("unused") public static BashCommand grep(String processName) throws IOException, InterruptedException { String c = "ps -aux | grep " + processName + " | grep -v grep"; return new BashCommand(c).run(); } public static boolean isAlive(long pid) throws IOException, InterruptedException { String isAliveScript = "if ps -p " + pid + " > /dev/null; then echo true; else echo false; fi"; return new BashCommand(isAliveScript).run().getOutput().equals("true"); } } ================================================ FILE: apitest/src/main/java/haveno/apitest/linux/BitcoinCli.java ================================================ /* * 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 . */ package haveno.apitest.linux; import haveno.apitest.config.ApiTestConfig; import lombok.extern.slf4j.Slf4j; import java.io.IOException; @Slf4j public class BitcoinCli extends AbstractLinuxProcess implements LinuxProcess { private final String command; private String commandWithOptions; private String output; private boolean error; private String errorMessage; public BitcoinCli(ApiTestConfig config, String command) { super("bitcoin-cli", config); this.command = command; this.error = false; this.errorMessage = null; } public BitcoinCli run() throws IOException, InterruptedException { this.start(); return this; } public String getCommandWithOptions() { return commandWithOptions; } public String getOutput() { if (isError()) throw new IllegalStateException(output); // Some responses are not in json format, such as what is returned by // 'getnewaddress'. The raw output string is the value. return output; } public String[] getOutputValueAsStringArray() { if (isError()) throw new IllegalStateException(output); if (!output.startsWith("[") && !output.endsWith("]")) throw new IllegalStateException(output + "\nis not a json array"); String[] lines = output.split("\n"); String[] array = new String[lines.length - 2]; for (int i = 1; i < lines.length - 1; i++) { array[i - 1] = lines[i].replaceAll("[^a-zA-Z0-9.]", ""); } return array; } public String getOutputValueAsString(String key) { if (isError()) throw new IllegalStateException(output); // Some assumptions about bitcoin-cli json string parsing: // Every multi valued, non-error bitcoin-cli response will be a json string. // Every key/value in the json string will terminate with a newline. // Most key/value lines in json strings have a ',' char in front of the newline. // e.g., bitcoin-cli 'getwalletinfo' output: // { // "walletname": "", // "walletversion": 159900, // "balance": 527.49941568, // "unconfirmed_balance": 0.00000000, // "immature_balance": 5000.00058432, // "txcount": 114, // "keypoololdest": 1528018235, // "keypoolsize": 1000, // "keypoolsize_hd_internal": 1000, // "paytxfee": 0.00000000, // "hdseedid": "179b609a60c2769138844c3e36eb430fd758a9c6", // "private_keys_enabled": true, // "avoid_reuse": false, // "scanning": false // } int keyIdx = output.indexOf("\"" + key + "\":"); int eolIdx = output.indexOf("\n", keyIdx); String valueLine = output.substring(keyIdx, eolIdx); // "balance": 527.49941568, String[] keyValue = valueLine.split(":"); // Remove all but alphanumeric chars and decimal points from the return value, // including quotes around strings, and trailing commas. // Adjustments will be necessary as we begin to work with more complex // json values, such as arrays. return keyValue[1].replaceAll("[^a-zA-Z0-9.]", ""); } public boolean getOutputValueAsBoolean(String key) { String valueStr = getOutputValueAsString(key); return Boolean.parseBoolean(valueStr); } public int getOutputValueAsInt(String key) { String valueStr = getOutputValueAsString(key); return Integer.parseInt(valueStr); } public double getOutputValueAsDouble(String key) { String valueStr = getOutputValueAsString(key); return Double.parseDouble(valueStr); } public long getOutputValueAsLong(String key) { String valueStr = getOutputValueAsString(key); return Long.parseLong(valueStr); } public boolean isError() { return error; } public String getErrorMessage() { return errorMessage; } @Override public void start() throws InterruptedException, IOException { verifyBitcoinPathsExist(false); verifyBitcoindRunning(); commandWithOptions = config.bitcoinPath + "/bitcoin-cli -regtest " + " -rpcport=" + config.bitcoinRpcPort + " -rpcuser=" + config.bitcoinRpcUser + " -rpcpassword=" + config.bitcoinRpcPassword + " " + command; BashCommand bashCommand = new BashCommand(commandWithOptions).run(); error = bashCommand.getExitStatus() != 0; if (error) { errorMessage = bashCommand.getError(); if (errorMessage == null || errorMessage.isEmpty()) throw new IllegalStateException("bitcoin-cli returned an error without a message"); } else { output = bashCommand.getOutput(); } } @Override public long getPid() { // We don't cache the pid. The bitcoin-cli will quickly return a // response, including server error info if any. throw new UnsupportedOperationException("getPid not supported"); } @Override public void shutdown() { // We don't try to shutdown the bitcoin-cli. It will quickly return a // response, including server error info if any. throw new UnsupportedOperationException("shutdown not supported"); } } ================================================ FILE: apitest/src/main/java/haveno/apitest/linux/BitcoinDaemon.java ================================================ /* * 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 . */ package haveno.apitest.linux; import haveno.apitest.config.ApiTestConfig; import lombok.extern.slf4j.Slf4j; import java.io.IOException; import static haveno.apitest.linux.BashCommand.isAlive; import static java.lang.String.format; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static joptsimple.internal.Strings.EMPTY; @Slf4j public class BitcoinDaemon extends AbstractLinuxProcess implements LinuxProcess { public BitcoinDaemon(ApiTestConfig config) { super("bitcoind", config); } @Override public void start() throws InterruptedException, IOException { // If the bitcoind binary is dynamically linked to berkeley db libs, export the // configured berkeley-db lib path. If statically linked, the berkeley db lib // path will not be exported. String berkeleyDbLibPathExport = config.berkeleyDbLibPath.equals(EMPTY) ? EMPTY : "export LD_LIBRARY_PATH=" + config.berkeleyDbLibPath + "; "; String bitcoindCmd = berkeleyDbLibPathExport + config.bitcoinPath + "/bitcoind" + " -datadir=" + config.bitcoinDatadir + " -daemon" + " -regtest=1" + " -server=1" + " -txindex=1" + " -peerbloomfilters=1" + " -debug=net" + " -fallbackfee=0.0002" + " -rpcport=" + config.bitcoinRpcPort + " -rpcuser=" + config.bitcoinRpcUser + " -rpcpassword=" + config.bitcoinRpcPassword + " -blocknotify=" + "\"" + config.bitcoinDatadir + "/blocknotify" + " %s\""; BashCommand cmd = new BashCommand(bitcoindCmd).run(); log.info("Starting ...\n$ {}", cmd.getCommand()); if (cmd.getExitStatus() != 0) { startupExceptions.add(new IllegalStateException( format("Error starting bitcoind%nstatus: %d%nerror msg: %s", cmd.getExitStatus(), cmd.getError()))); return; } pid = BashCommand.getPid("bitcoind"); if (!isAlive(pid)) throw new IllegalStateException("Error starting regtest bitcoind daemon:\n" + cmd.getCommand()); log.info("Running with pid {}", pid); log.info("Log {}", config.bitcoinDatadir + "/regtest/debug.log"); } @Override public long getPid() { return this.pid; } @Override public void shutdown() { try { log.info("Shutting down bitcoind daemon..."); if (!isAlive(pid)) { this.shutdownExceptions.add(new IllegalStateException("Bitcoind already shut down.")); return; } if (new BashCommand("kill -15 " + pid).run().getExitStatus() != 0) { this.shutdownExceptions.add(new IllegalStateException("Could not shut down bitcoind; probably already stopped.")); return; } MILLISECONDS.sleep(2500); // allow it time to shutdown if (isAlive(pid)) { this.shutdownExceptions.add(new IllegalStateException( format("Could not kill bitcoind process with pid %d.", pid))); return; } log.info("Stopped"); } catch (InterruptedException ignored) { // empty } catch (IOException e) { this.shutdownExceptions.add(new IllegalStateException("Error shutting down bitcoind.", e)); } } } ================================================ FILE: apitest/src/main/java/haveno/apitest/linux/HavenoProcess.java ================================================ /* * 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 . */ package haveno.apitest.linux; import haveno.apitest.config.ApiTestConfig; import haveno.apitest.config.HavenoAppConfig; import haveno.daemon.app.HavenoDaemonMain; import lombok.extern.slf4j.Slf4j; import java.io.File; import java.io.IOException; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import static haveno.apitest.linux.BashCommand.isAlive; import static java.lang.String.format; import static java.util.concurrent.TimeUnit.MILLISECONDS; /** * Runs a regtest/dao Haveno application instance in the background. */ @Slf4j public class HavenoProcess extends AbstractLinuxProcess implements LinuxProcess { private final HavenoAppConfig havenoAppConfig; private final String baseCurrencyNetwork; private final String genesisTxId; private final int genesisBlockHeight; private final String seedNodes; private final boolean useLocalhostForP2P; public final boolean useDevPrivilegeKeys; private final String findHavenoPidScript; private final String debugOpts; public HavenoProcess(HavenoAppConfig havenoAppConfig, ApiTestConfig config) { super(havenoAppConfig.appName, config); this.havenoAppConfig = havenoAppConfig; this.baseCurrencyNetwork = "XMR_STAGENET"; this.genesisTxId = "30af0050040befd8af25068cc697e418e09c2d8ebd8d411d2240591b9ec203cf"; this.genesisBlockHeight = 111; this.seedNodes = "localhost:2002"; this.useLocalhostForP2P = true; this.useDevPrivilegeKeys = true; this.findHavenoPidScript = (config.isRunningTest ? "." : "./apitest") + "/scripts/get-haveno-pid.sh"; this.debugOpts = config.enableHavenoDebugging ? " -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:" + havenoAppConfig.remoteDebugPort : ""; } @Override public void start() { try { if (config.runSubprojectJars) runJar(); // run subproject/build/lib/*.jar (not full build) else runStartupScript(); // run haveno-* script for end to end test (default) } catch (Throwable t) { startupExceptions.add(t); } } @Override public long getPid() { return this.pid; } @Override public void shutdown() { try { log.info("Shutting down {} ...", havenoAppConfig.appName); if (!isAlive(pid)) { this.shutdownExceptions.add(new IllegalStateException(format("%s already shut down", havenoAppConfig.appName))); return; } String killCmd = "kill -15 " + pid; if (new BashCommand(killCmd).run().getExitStatus() != 0) { this.shutdownExceptions.add(new IllegalStateException(format("Could not shut down %s", havenoAppConfig.appName))); return; } // Be lenient about the time it takes for a java app to shut down. for (int i = 0; i < 5; i++) { if (!isAlive(pid)) { log.info("{} stopped", havenoAppConfig.appName); break; } MILLISECONDS.sleep(2500); } if (isAlive(pid)) { this.shutdownExceptions.add(new IllegalStateException(format("%s shutdown did not work", havenoAppConfig.appName))); } } catch (Exception e) { this.shutdownExceptions.add(new IllegalStateException(format("Error shutting down %s", havenoAppConfig.appName), e)); } } public void verifyAppNotRunning() throws IOException, InterruptedException { long pid = findHavenoAppPid(); if (pid >= 0) throw new IllegalStateException(format("%s %s already running with pid %d", havenoAppConfig.mainClassName, havenoAppConfig.appName, pid)); } public void verifyAppDataDirInstalled() { // If we're running an Alice or Bob daemon, make sure the dao-setup directory // are installed. switch (havenoAppConfig) { case alicedaemon: case alicedesktop: case bobdaemon: case bobdesktop: File havenoDataDir = new File(config.rootAppDataDir, havenoAppConfig.appName); if (!havenoDataDir.exists()) throw new IllegalStateException(format("Application dataDir %s/%s not found", config.rootAppDataDir, havenoAppConfig.appName)); break; default: break; } } // This is the non-default way of running a Haveno app (--runSubprojectJars=true). // It runs a java cmd, and does not depend on a full build. Haveno jars are loaded // from the :subproject/build/libs directories. private void runJar() throws IOException, InterruptedException { String java = getJavaExecutable().getAbsolutePath(); String classpath = System.getProperty("java.class.path"); String havenoCmd = getJavaOptsSpec() + " " + java + " -cp " + classpath + " " + havenoAppConfig.mainClassName + " " + String.join(" ", getOptsList()) + " &"; // run in background without nohup runBashCommand(havenoCmd); } // This is the default way of running a Haveno app (--runSubprojectJars=false). // It runs a haveno-* startup script, and depends on a full build. Haveno jars // are loaded from the root project's lib directory. private void runStartupScript() throws IOException, InterruptedException { String startupScriptPath = config.rootProjectDir + "/" + havenoAppConfig.startupScript; String havenoCmd = getJavaOptsSpec() + " " + startupScriptPath + " " + String.join(" ", getOptsList()) + " &"; // run in background without nohup runBashCommand(havenoCmd); } private void runBashCommand(String havenoCmd) throws IOException, InterruptedException { String cmdDescription = config.runSubprojectJars ? "java -> " + havenoAppConfig.mainClassName + " -> " + havenoAppConfig.appName : havenoAppConfig.startupScript + " -> " + havenoAppConfig.appName; BashCommand bashCommand = new BashCommand(havenoCmd); log.info("Starting {} ...\n$ {}", cmdDescription, bashCommand.getCommand()); bashCommand.runInBackground(); if (bashCommand.getExitStatus() != 0) throw new IllegalStateException(format("Error starting HavenoApp%n%s%nError: %s", havenoAppConfig.appName, bashCommand.getError())); // Sometimes it takes a little extra time to find the linux process id. // Wait up to two seconds before giving up and throwing an Exception. for (int i = 0; i < 4; i++) { pid = findHavenoAppPid(); if (pid != -1) break; MILLISECONDS.sleep(500L); } if (!isAlive(pid)) throw new IllegalStateException(format("Error finding pid for %s", this.name)); log.info("{} running with pid {}", cmdDescription, pid); log.info("Log {}", config.rootAppDataDir + "/" + havenoAppConfig.appName + "/haveno.log"); } private long findHavenoAppPid() throws IOException, InterruptedException { // Find the pid of the java process by grepping for the mainClassName and appName. String findPidCmd = findHavenoPidScript + " " + havenoAppConfig.mainClassName + " " + havenoAppConfig.appName; String psCmdOutput = new BashCommand(findPidCmd).run().getOutput(); return (psCmdOutput == null || psCmdOutput.isEmpty()) ? -1 : Long.parseLong(psCmdOutput); } private String getJavaOptsSpec() { return "export JAVA_OPTS=\"" + havenoAppConfig.javaOpts + debugOpts + "\"; "; } private List getOptsList() { return new ArrayList<>() {{ add("--appName=" + havenoAppConfig.appName); add("--appDataDir=" + config.rootAppDataDir.getAbsolutePath() + "/" + havenoAppConfig.appName); add("--nodePort=" + havenoAppConfig.nodePort); add("--rpcBlockNotificationPort=" + havenoAppConfig.rpcBlockNotificationPort); add("--rpcUser=" + config.bitcoinRpcUser); add("--rpcPassword=" + config.bitcoinRpcPassword); add("--rpcPort=" + config.bitcoinRpcPort); add("--seedNodes=" + seedNodes); add("--baseCurrencyNetwork=" + baseCurrencyNetwork); add("--useDevPrivilegeKeys=" + useDevPrivilegeKeys); add("--useLocalhostForP2P=" + useLocalhostForP2P); switch (havenoAppConfig) { case seednode: break; // no extra opts needed for seed node case arbdaemon: case arbdesktop: case alicedaemon: case alicedesktop: case bobdaemon: case bobdesktop: add("--genesisBlockHeight=" + genesisBlockHeight); add("--genesisTxId=" + genesisTxId); if (havenoAppConfig.mainClassName.equals(HavenoDaemonMain.class.getName())) { add("--apiPassword=" + config.apiPassword); add("--apiPort=" + havenoAppConfig.apiPort); } break; default: throw new IllegalStateException("Unknown HavenoAppConfig " + havenoAppConfig.name()); } }}; } private File getJavaExecutable() { File javaHome = Paths.get(System.getProperty("java.home")).toFile(); if (!javaHome.exists()) throw new IllegalStateException(format("$JAVA_HOME not found, cannot run %s", havenoAppConfig.mainClassName)); File javaExecutable = Paths.get(javaHome.getAbsolutePath(), "bin", "java").toFile(); if (javaExecutable.exists() || javaExecutable.canExecute()) return javaExecutable; else throw new IllegalStateException("$JAVA_HOME/bin/java not found or executable"); } } ================================================ FILE: apitest/src/main/java/haveno/apitest/linux/LinuxProcess.java ================================================ /* * 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 . */ package haveno.apitest.linux; import java.io.IOException; import java.util.List; public interface LinuxProcess { void start() throws InterruptedException, IOException; String getName(); long getPid(); boolean hasStartupExceptions(); boolean hasShutdownExceptions(); void logExceptions(List exceptions, org.slf4j.Logger log); List getStartupExceptions(); List getShutdownExceptions(); void shutdown(); } ================================================ FILE: apitest/src/main/java/haveno/apitest/linux/SystemCommandExecutor.java ================================================ /* * 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 . */ package haveno.apitest.linux; import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.io.InputStream; import java.util.List; /** * This class can be used to execute a system command from a Java application. * See the documentation for the public methods of this class for more * information. * * Documentation for this class is available at this URL: * * http://devdaily.com/java/java-processbuilder-process-system-exec * * Copyright 2010 alvin j. alexander, devdaily.com. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * This program 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 Lesser Public License for more details. * You should have received a copy of the GNU Lesser Public License * along with this program. If not, see . * * Please ee the following page for the LGPL license: * http://www.gnu.org/licenses/lgpl.txt * */ @Slf4j class SystemCommandExecutor { private final List cmdOptions; private ThreadedStreamHandler inputStreamHandler; private ThreadedStreamHandler errorStreamHandler; public SystemCommandExecutor(final List cmdOptions) { if (cmdOptions.isEmpty()) throw new IllegalStateException("No command params specified."); if (cmdOptions.contains("sudo")) throw new IllegalStateException("'sudo' commands are prohibited."); log.trace("System cmd options {}", cmdOptions); this.cmdOptions = cmdOptions; } // Execute a system command and return its status code (0 or 1). // The system command's output (stderr or stdout) can be accessed from accessors. public int exec() throws IOException, InterruptedException { return exec(true); } // Execute a system command and return its status code (0 or 1). // The system command's output (stderr or stdout) can be accessed from accessors // if the waitOnErrStream flag is true, else the method will not wait on (join) // the error stream handler thread. public int exec(boolean waitOnErrStream) throws IOException, InterruptedException { Process process = new ProcessBuilder(cmdOptions).start(); // I'm currently doing these on a separate line here in case i need to set them to null // to get the threads to stop. // see http://java.sun.com/j2se/1.5.0/docs/guide/misc/threadPrimitiveDeprecation.html InputStream inputStream = process.getInputStream(); InputStream errorStream = process.getErrorStream(); // These need to run as java threads to get the standard output and error from the command. // the inputstream handler gets a reference to our stdOutput in case we need to write // something to it. inputStreamHandler = new ThreadedStreamHandler(inputStream); errorStreamHandler = new ThreadedStreamHandler(errorStream); inputStreamHandler.start(); errorStreamHandler.start(); int exitStatus = process.waitFor(); inputStreamHandler.interrupt(); errorStreamHandler.interrupt(); inputStreamHandler.join(); if (waitOnErrStream) errorStreamHandler.join(); return exitStatus; } // Get the standard error from an executed system command. public StringBuilder getStandardErrorFromCommand() { return errorStreamHandler.getOutputBuffer(); } // Get the standard output from an executed system command. public StringBuilder getStandardOutputFromCommand() { return inputStreamHandler.getOutputBuffer(); } } ================================================ FILE: apitest/src/main/java/haveno/apitest/linux/ThreadedStreamHandler.java ================================================ /* * 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 . */ package haveno.apitest.linux; import lombok.extern.slf4j.Slf4j; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; /** * This class is intended to be used with the SystemCommandExecutor * class to let users execute system commands from Java applications. * * This class is based on work that was shared in a JavaWorld article * named "When System.exec() won't". That article is available at this * url: * * http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html * * Documentation for this class is available at this URL: * * http://devdaily.com/java/java-processbuilder-process-system-exec * * * Copyright 2010 alvin j. alexander, devdaily.com. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * This program 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 Lesser Public License for more details. * You should have received a copy of the GNU Lesser Public License * along with this program. If not, see . * * Please ee the following page for the LGPL license: * http://www.gnu.org/licenses/lgpl.txt * */ @Slf4j class ThreadedStreamHandler extends Thread { final InputStream inputStream; final StringBuilder outputBuffer = new StringBuilder(); ThreadedStreamHandler(InputStream inputStream) { this.inputStream = inputStream; } public void run() { try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) { String line; while ((line = bufferedReader.readLine()) != null) outputBuffer.append(line).append("\n"); } catch (Throwable t) { t.printStackTrace(); } } @SuppressWarnings("unused") private void doSleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException ignored) { // empty } } public StringBuilder getOutputBuffer() { return outputBuffer; } } ================================================ FILE: apitest/src/main/resources/apitest.properties ================================================ ================================================ FILE: apitest/src/main/resources/blocknotify ================================================ #!/bin/bash # Regtest ports start with 512* # To avoid pesky bitcoind io errors, do not specify ports Haveno is not listening to. # SeedNode listens on port 5120 echo $1 | nc -w 1 127.0.0.1 5120 # Arb Node listens on port 5121 echo $1 | nc -w 1 127.0.0.1 5121 # Alice Node listens on port 5122 echo $1 | nc -w 1 127.0.0.1 5122 # Bob Node listens on port 5123 echo $1 | nc -w 1 127.0.0.1 5123 # Some other node listens on port 5124, etc. # echo $1 | nc -w 1 127.0.0.1 5124 ================================================ FILE: apitest/src/main/resources/haveno.properties ================================================ # Haveno core properties file loaded by Haveno instances started by the test harness. # Normally, it would be left empty, but it is useful for ad-hoc testing with # Haveno Config options not configurable in test harness-specific apitest.properties # file. This is where you might define Haveno options such as: # dumpBlockchainData=true # dumpStatistics=true ================================================ FILE: apitest/src/main/resources/logback.xml ================================================ %highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n) ================================================ FILE: apitest/src/test/java/haveno/apitest/ApiTestCase.java ================================================ /* * 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 . */ package haveno.apitest; import haveno.apitest.config.ApiTestConfig; import haveno.apitest.method.BitcoinCliHelper; import haveno.cli.GrpcClient; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.TestInfo; import javax.annotation.Nullable; import java.io.IOException; import java.time.Duration; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; import static haveno.apitest.config.ApiTestRateMeterInterceptorConfig.getTestRateMeterInterceptorConfig; import static haveno.apitest.config.HavenoAppConfig.alicedaemon; import static haveno.apitest.config.HavenoAppConfig.arbdaemon; import static haveno.apitest.config.HavenoAppConfig.bobdaemon; import static java.net.InetAddress.getLoopbackAddress; import static java.util.Arrays.stream; /** * Base class for all test types: 'method', 'scenario' and 'e2e'. *

* During scaffold setup, various combinations of bitcoind and haveno instances * can be started in the background before test cases are run. Currently, this test * harness supports only the "Haveno DAO development environment running against a * local Bitcoin regtest network" as described in * dev-setup.md * and dao-setup.md. *

* Those documents contain information about the configurations used by this test harness: * bitcoin-core's bitcoin.conf and blocknotify values, haveno instance options, the DAO genesis * transaction id, initial BTC balances for Bob & Alice accounts, and Bob and * Alice's default payment accounts. *

* During a build, the * dao-setup.zip * file is downloaded and extracted if necessary. In each test case's @BeforeClass * method, the DAO setup files are re-installed into the run time's data directories * (each test case runs on a refreshed DAO/regtest environment setup). *

* Initial Alice balances & accounts: 10.0 BTC, USD PerfectMoney dummy *

* Initial Bob balances & accounts: 10.0 BTC, USD PerfectMoney dummy */ @Slf4j public class ApiTestCase { protected static Scaffold scaffold; protected static ApiTestConfig config; protected static BitcoinCliHelper bitcoinCli; @Nullable protected static GrpcClient arbClient; @Nullable protected static GrpcClient aliceClient; @Nullable protected static GrpcClient bobClient; public static void setUpScaffold(Enum... supportingApps) throws InterruptedException, ExecutionException, IOException { String[] params = new String[]{ "--supportingApps", stream(supportingApps).map(Enum::name).collect(Collectors.joining(",")), "--callRateMeteringConfigPath", getTestRateMeterInterceptorConfig().getAbsolutePath(), "--enableHavenoDebugging", "false" }; setUpScaffold(params); } public static void setUpScaffold(String[] params) throws InterruptedException, ExecutionException, IOException { // Test cases needing to pass more than just an ApiTestConfig // --supportingApps option will use this setup method, but the // --supportingApps option will need to be passed too, with its comma // delimited app list value, e.g., "bitcoind,seednode,arbdaemon". scaffold = new Scaffold(params).setUp(); config = scaffold.config; bitcoinCli = new BitcoinCliHelper((config)); createGrpcClients(); } public static void tearDownScaffold() { scaffold.tearDown(); } protected static void createGrpcClients() { if (config.supportingApps.contains(alicedaemon.name())) { aliceClient = new GrpcClient(getLoopbackAddress().getHostAddress(), alicedaemon.apiPort, config.apiPassword); } if (config.supportingApps.contains(bobdaemon.name())) { bobClient = new GrpcClient(getLoopbackAddress().getHostAddress(), bobdaemon.apiPort, config.apiPassword); } if (config.supportingApps.contains(arbdaemon.name())) { arbClient = new GrpcClient(getLoopbackAddress().getHostAddress(), arbdaemon.apiPort, config.apiPassword); } } protected static void genBtcBlocksThenWait(int numBlocks, long wait) { bitcoinCli.generateBlocks(numBlocks); sleep(wait); } protected static void sleep(long ms) { sleepUninterruptibly(Duration.ofMillis(ms)); } protected final String testName(TestInfo testInfo) { return testInfo.getTestMethod().isPresent() ? testInfo.getTestMethod().get().getName() : "unknown test name"; } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/BitcoinCliHelper.java ================================================ /* * 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 . */ package haveno.apitest.method; import haveno.apitest.config.ApiTestConfig; import haveno.apitest.linux.BitcoinCli; import java.io.IOException; import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.fail; public final class BitcoinCliHelper { private final ApiTestConfig config; public BitcoinCliHelper(ApiTestConfig config) { this.config = config; } // Convenience methods for making bitcoin-cli calls. public String getNewBtcAddress() { try { BitcoinCli newAddress = new BitcoinCli(config, "getnewaddress").run(); if (newAddress.isError()) fail(format("Could not generate new bitcoin address:%n%s", newAddress.getErrorMessage())); return newAddress.getOutput(); } catch (IOException | InterruptedException ex) { fail(ex); return null; } } public String[] generateToAddress(int blocks, String address) { try { String generateToAddressCmd = format("generatetoaddress %d \"%s\"", blocks, address); BitcoinCli generateToAddress = new BitcoinCli(config, generateToAddressCmd).run(); if (generateToAddress.isError()) fail(format("Could not generate bitcoin block(s):%n%s", generateToAddress.getErrorMessage())); return generateToAddress.getOutputValueAsStringArray(); } catch (IOException | InterruptedException ex) { fail(ex); return null; } } public void generateBlocks(int blocks) { generateToAddress(blocks, getNewBtcAddress()); } public String sendToAddress(String address, String amount) { // sendtoaddress "address" amount \ // ( "comment" "comment_to" subtractfeefromamount \ // replaceable conf_target "estimate_mode" ) // returns a transaction id try { String sendToAddressCmd = format("sendtoaddress \"%s\" %s \"\" \"\" false", address, amount); BitcoinCli sendToAddress = new BitcoinCli(config, sendToAddressCmd).run(); if (sendToAddress.isError()) fail(format("Could not send BTC to address:%n%s", sendToAddress.getErrorMessage())); return sendToAddress.getOutput(); } catch (IOException | InterruptedException ex) { fail(ex); return null; } } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/CallRateMeteringInterceptorTest.java ================================================ /* * 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 . */ package haveno.apitest.method; import io.grpc.StatusRuntimeException; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import static haveno.apitest.Scaffold.BitcoinCoreApp.bitcoind; import static haveno.apitest.config.HavenoAppConfig.alicedaemon; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class CallRateMeteringInterceptorTest extends MethodTest { private static final GetVersionTest getVersionTest = new GetVersionTest(); @BeforeAll public static void setUp() { startSupportingApps(false, false, bitcoind, alicedaemon); } @BeforeEach public void sleep200Milliseconds() { sleep(200); } @Test @Order(1) public void testGetVersionCall1IsAllowed() { getVersionTest.testGetVersion(); } @Test @Order(2) public void testGetVersionCall2ShouldThrowException() { Throwable exception = assertThrows(StatusRuntimeException.class, getVersionTest::testGetVersion); assertEquals("PERMISSION_DENIED: the maximum allowed number of getversion calls (1/second) has been exceeded", exception.getMessage()); } @Test @Order(3) public void testGetVersionCall3ShouldThrowException() { Throwable exception = assertThrows(StatusRuntimeException.class, getVersionTest::testGetVersion); assertEquals("PERMISSION_DENIED: the maximum allowed number of getversion calls (1/second) has been exceeded", exception.getMessage()); } @Test @Order(4) public void testGetVersionCall4IsAllowed() { sleep(1100); // Let the server's rate meter reset the call count. getVersionTest.testGetVersion(); } @AfterAll public static void tearDown() { tearDownScaffold(); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/GetMethodHelpTest.java ================================================ /* * 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 . */ package haveno.apitest.method; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import static haveno.apitest.config.HavenoAppConfig.alicedaemon; import static haveno.cli.Method.createoffer; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; @Disabled @Slf4j @TestMethodOrder(OrderAnnotation.class) public class GetMethodHelpTest extends MethodTest { @BeforeAll public static void setUp() { try { setUpScaffold(alicedaemon); } catch (Exception ex) { fail(ex); } } @Test @Order(1) public void testGetCreateOfferHelp() { var help = aliceClient.getMethodHelp(createoffer); assertNotNull(help); } @AfterAll public static void tearDown() { tearDownScaffold(); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/GetVersionTest.java ================================================ /* * 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 . */ package haveno.apitest.method; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import static haveno.apitest.config.HavenoAppConfig.alicedaemon; import static haveno.common.app.Version.VERSION; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; @Disabled @Slf4j @TestMethodOrder(OrderAnnotation.class) public class GetVersionTest extends MethodTest { @BeforeAll public static void setUp() { try { setUpScaffold(alicedaemon); } catch (Exception ex) { fail(ex); } } @Test @Order(1) public void testGetVersion() { var version = aliceClient.getVersion(); assertEquals(VERSION, version); } @AfterAll public static void tearDown() { tearDownScaffold(); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/MethodTest.java ================================================ /* * 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 . */ package haveno.apitest.method; import haveno.apitest.ApiTestCase; import haveno.apitest.linux.BashCommand; import haveno.cli.GrpcClient; import haveno.cli.table.builder.TableBuilder; import haveno.common.util.Utilities; import haveno.core.api.model.PaymentAccountForm; import haveno.core.payment.F2FAccount; import haveno.core.payment.NationalBankAccount; import haveno.core.proto.CoreProtoResolver; import haveno.proto.grpc.BalancesInfo; import io.grpc.Status; import io.grpc.StatusRuntimeException; import org.slf4j.Logger; import javax.annotation.Nullable; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.math.BigDecimal; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; import static haveno.apitest.config.ApiTestConfig.BTC; import static haveno.apitest.config.ApiTestRateMeterInterceptorConfig.getTestRateMeterInterceptorConfig; import static haveno.cli.table.builder.TableType.BTC_BALANCE_TBL; import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositPct; import static java.lang.String.format; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.stream; import static org.junit.jupiter.api.Assertions.fail; public class MethodTest extends ApiTestCase { protected static final CoreProtoResolver CORE_PROTO_RESOLVER = new CoreProtoResolver(); private static final Function[], String> toNameList = (enums) -> stream(enums).map(Enum::name).collect(Collectors.joining(",")); public static void startSupportingApps(File callRateMeteringConfigFile, boolean generateBtcBlock, boolean startSupportingAppsInDebugMode, Enum... supportingApps) { try { setUpScaffold(new String[]{ "--supportingApps", toNameList.apply(supportingApps), "--callRateMeteringConfigPath", callRateMeteringConfigFile.getAbsolutePath(), "--enableHavenoDebugging", startSupportingAppsInDebugMode ? "true" : "false" }); doPostStartup(generateBtcBlock); } catch (Exception ex) { fail(ex); } } public static void startSupportingApps(boolean generateBtcBlock, boolean startSupportingAppsInDebugMode, Enum... supportingApps) { try { // Disable call rate metering where there is no callRateMeteringConfigFile. File callRateMeteringConfigFile = getTestRateMeterInterceptorConfig(); setUpScaffold(new String[]{ "--supportingApps", toNameList.apply(supportingApps), "--callRateMeteringConfigPath", callRateMeteringConfigFile.getAbsolutePath(), "--enableHavenoDebugging", startSupportingAppsInDebugMode ? "true" : "false" }); doPostStartup(generateBtcBlock); } catch (Exception ex) { fail(ex); } } protected static void doPostStartup(boolean generateBtcBlock) { // Generate 1 regtest block for alice's and/or bob's wallet to // show 10 BTC balance, and allow time for daemons parse the new block. if (generateBtcBlock) genBtcBlocksThenWait(1, 1500); } protected final File getPaymentAccountForm(GrpcClient grpcClient, String paymentMethodId) { // We take seemingly unnecessary steps to get a File object, but the point is to // test the API, and we do not directly ask haveno.core.api.model.PaymentAccountForm // for an empty json form (file). String jsonString = grpcClient.getPaymentAcctFormAsJson(paymentMethodId); // Write the json string to a file here in the test case. File jsonFile = PaymentAccountForm.getTmpJsonFile(paymentMethodId); try (PrintWriter out = new PrintWriter(jsonFile, UTF_8)) { out.println(jsonString); } catch (IOException ex) { fail("Could not create tmp payment account form.", ex); } return jsonFile; } protected haveno.core.payment.PaymentAccount createDummyF2FAccount(GrpcClient grpcClient, String countryCode) { String f2fAccountJsonString = "{\n" + " \"_COMMENTS_\": \"This is a dummy account.\",\n" + " \"paymentMethodId\": \"F2F\",\n" + " \"accountName\": \"Dummy " + countryCode.toUpperCase() + " F2F Account\",\n" + " \"city\": \"Anytown\",\n" + " \"contact\": \"Morse Code\",\n" + " \"country\": \"" + countryCode.toUpperCase() + "\",\n" + " \"extraInfo\": \"Salt Lick #213\"\n" + "}\n"; F2FAccount f2FAccount = (F2FAccount) createPaymentAccount(grpcClient, f2fAccountJsonString); return f2FAccount; } protected haveno.core.payment.PaymentAccount createDummyBRLAccount(GrpcClient grpcClient, String holderName, String nationalAccountId, String holderTaxId) { String nationalBankAccountJsonString = "{\n" + " \"_COMMENTS_\": [ \"Dummy Account\" ],\n" + " \"paymentMethodId\": \"NATIONAL_BANK\",\n" + " \"accountName\": \"Banco do Brasil\",\n" + " \"country\": \"BR\",\n" + " \"bankName\": \"Banco do Brasil\",\n" + " \"branchId\": \"456789-10\",\n" + " \"holderName\": \"" + holderName + "\",\n" + " \"accountNr\": \"456789-87\",\n" + " \"nationalAccountId\": \"" + nationalAccountId + "\",\n" + " \"holderTaxId\": \"" + holderTaxId + "\"\n" + "}\n"; NationalBankAccount nationalBankAccount = (NationalBankAccount) createPaymentAccount(grpcClient, nationalBankAccountJsonString); return nationalBankAccount; } protected final haveno.core.payment.PaymentAccount createPaymentAccount(GrpcClient grpcClient, String jsonString) { // Normally, we do asserts on the protos from the gRPC service, but in this // case we need a haveno.core.payment.PaymentAccount so it can be cast to its // sub-type. var paymentAccount = grpcClient.createPaymentAccount(jsonString); return haveno.core.payment.PaymentAccount.fromProto(paymentAccount, CORE_PROTO_RESOLVER); } public static final Supplier defaultSecurityDepositPct = () -> { var defaultPct = BigDecimal.valueOf(getDefaultSecurityDepositPct()); if (defaultPct.precision() != 2) throw new IllegalStateException(format( "Unexpected decimal precision, expected 2 but actual is %d%n." + "Check for changes to Restrictions.getDefaultBuyerSecurityDepositAsPercent()", defaultPct.precision())); return defaultPct.movePointRight(2).doubleValue(); }; public static String formatBalancesTbls(BalancesInfo allBalances) { StringBuilder balances = new StringBuilder(BTC).append("\n"); balances.append(new TableBuilder(BTC_BALANCE_TBL, allBalances.getBtc()).build()); balances.append("\n"); return balances.toString(); } protected static String encodeToHex(String s) { return Utilities.bytesAsHexString(s.getBytes(UTF_8)); } protected static Status.Code getStatusRuntimeExceptionStatusCode(Exception grpcException) { if (grpcException instanceof StatusRuntimeException) return ((StatusRuntimeException) grpcException).getStatus().getCode(); else throw new IllegalArgumentException( format("Expected a io.grpc.StatusRuntimeException argument, but got a %s", grpcException.getClass().getName())); } protected void verifyNoLoggedNodeExceptions() { var loggedExceptions = getNodeExceptionMessages(); if (loggedExceptions != null) { String err = format("Exception(s) found in daemon log(s):%n%s", loggedExceptions); fail(err); } } protected void printNodeExceptionMessages(Logger log) { var loggedExceptions = getNodeExceptionMessages(); if (loggedExceptions != null) log.error("Exception(s) found in daemon log(s):\n{}", loggedExceptions); } @Nullable protected static String getNodeExceptionMessages() { var nodeLogsSpec = config.rootAppDataDir.getAbsolutePath() + "/haveno-BTC_REGTEST_*_dao/haveno.log"; var grep = "grep Exception " + nodeLogsSpec; var bashCommand = new BashCommand(grep); try { bashCommand.run(); } catch (IOException | InterruptedException ex) { fail("Bash command execution error: " + ex); } if (bashCommand.getError() == null) return bashCommand.getOutput(); else throw new IllegalStateException("Bash command execution error: " + bashCommand.getError()); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/RegisterDisputeAgentsTest.java ================================================ /* * 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 . */ package haveno.apitest.method; import io.grpc.StatusRuntimeException; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import static haveno.apitest.Scaffold.BitcoinCoreApp.bitcoind; import static haveno.apitest.config.ApiTestConfig.ARBITRATOR; import static haveno.apitest.config.ApiTestConfig.MEDIATOR; import static haveno.apitest.config.ApiTestConfig.REFUND_AGENT; import static haveno.apitest.config.HavenoAppConfig.arbdaemon; import static haveno.apitest.config.HavenoAppConfig.seednode; import static haveno.common.app.DevEnv.DEV_PRIVILEGE_PRIV_KEY; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; @SuppressWarnings("ResultOfMethodCallIgnored") @Disabled @Slf4j @TestMethodOrder(OrderAnnotation.class) public class RegisterDisputeAgentsTest extends MethodTest { @BeforeAll public static void setUp() { try { setUpScaffold(bitcoind, seednode, arbdaemon); } catch (Exception ex) { fail(ex); } } @Test @Order(1) public void testRegisterArbitratorShouldThrowException() { Throwable exception = assertThrows(StatusRuntimeException.class, () -> arbClient.registerDisputeAgent(ARBITRATOR, DEV_PRIVILEGE_PRIV_KEY)); assertEquals("UNIMPLEMENTED: arbitrators must be registered in a Haveno UI", exception.getMessage()); } @Test @Order(2) public void testInvalidDisputeAgentTypeArgShouldThrowException() { Throwable exception = assertThrows(StatusRuntimeException.class, () -> arbClient.registerDisputeAgent("badagent", DEV_PRIVILEGE_PRIV_KEY)); assertEquals("INVALID_ARGUMENT: unknown dispute agent type 'badagent'", exception.getMessage()); } @Test @Order(3) public void testInvalidRegistrationKeyArgShouldThrowException() { Throwable exception = assertThrows(StatusRuntimeException.class, () -> arbClient.registerDisputeAgent(REFUND_AGENT, "invalid" + DEV_PRIVILEGE_PRIV_KEY)); assertEquals("INVALID_ARGUMENT: invalid registration key", exception.getMessage()); } @Test @Order(4) public void testRegisterMediator() { arbClient.registerDisputeAgent(MEDIATOR, DEV_PRIVILEGE_PRIV_KEY); } @Test @Order(5) public void testRegisterRefundAgent() { arbClient.registerDisputeAgent(REFUND_AGENT, DEV_PRIVILEGE_PRIV_KEY); } @AfterAll public static void tearDown() { tearDownScaffold(); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/offer/AbstractOfferTest.java ================================================ /* * 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 . */ package haveno.apitest.method.offer; import haveno.apitest.method.MethodTest; import haveno.cli.CliMain; import haveno.cli.table.builder.TableBuilder; import haveno.proto.grpc.OfferInfo; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import protobuf.PaymentAccount; import java.math.BigDecimal; import java.math.MathContext; import java.util.List; import java.util.function.Function; import static haveno.apitest.Scaffold.BitcoinCoreApp.bitcoind; import static haveno.apitest.config.ApiTestConfig.XMR; import static haveno.apitest.config.HavenoAppConfig.alicedaemon; import static haveno.apitest.config.HavenoAppConfig.arbdaemon; import static haveno.apitest.config.HavenoAppConfig.bobdaemon; import static haveno.apitest.config.HavenoAppConfig.seednode; import static haveno.cli.table.builder.TableType.OFFER_TBL; import static java.lang.String.format; import static java.lang.System.out; @Slf4j public abstract class AbstractOfferTest extends MethodTest { protected static final int ACTIVATE_OFFER = 1; protected static final int DEACTIVATE_OFFER = 0; protected static final String NO_TRIGGER_PRICE = "0"; @Setter protected static boolean isLongRunningTest; protected static PaymentAccount alicesBtcAcct; protected static PaymentAccount bobsBtcAcct; protected static PaymentAccount alicesXmrAcct; protected static PaymentAccount bobsXmrAcct; @BeforeAll public static void setUp() { setUp(false); } public static void setUp(boolean startSupportingAppsInDebugMode) { startSupportingApps(true, startSupportingAppsInDebugMode, bitcoind, seednode, arbdaemon, alicedaemon, bobdaemon); initPaymentAccounts(); } protected static final Function toOfferTable = (offer) -> new TableBuilder(OFFER_TBL, offer).build().toString(); protected static final Function, String> toOffersTable = (offers) -> new TableBuilder(OFFER_TBL, offers).build().toString(); protected static String calcPriceAsString(double base, double delta, int precision) { var mathContext = new MathContext(precision); var priceAsBigDecimal = new BigDecimal(Double.toString(base), mathContext) .add(new BigDecimal(Double.toString(delta), mathContext)) .round(mathContext); return format("%." + precision + "f", priceAsBigDecimal.doubleValue()); } @SuppressWarnings("ConstantConditions") public static void initPaymentAccounts() { alicesBtcAcct = aliceClient.getPaymentAccount("BTC"); bobsBtcAcct = bobClient.getPaymentAccount("BTC"); } @SuppressWarnings("ConstantConditions") public static void createXmrPaymentAccounts() { alicesXmrAcct = aliceClient.createCryptoCurrencyPaymentAccount("Alice's XMR Account", XMR, "44G4jWmSvTEfifSUZzTDnJVLPvYATmq9XhhtDqUof1BGCLceG82EQsVYG9Q9GN4bJcjbAJEc1JD1m5G7iK4UPZqACubV4Mq", false); log.trace("Alices XMR Account: {}", alicesXmrAcct); bobsXmrAcct = bobClient.createCryptoCurrencyPaymentAccount("Bob's XMR Account", XMR, "4BDRhdSBKZqAXs3PuNTbMtaXBNqFj5idC2yMVnQj8Rm61AyKY8AxLTt9vGRJ8pwcG4EtpyD8YpGqdZWCZ2VZj6yVBN2RVKs", false); log.trace("Bob's XMR Account: {}", bobsXmrAcct); } @AfterAll public static void tearDown() { tearDownScaffold(); } protected static void runCliGetOffer(String offerId) { out.println("Alice's CLI 'getmyoffer' response:"); CliMain.main(new String[]{"--password=xyz", "--port=9998", "getmyoffer", "--offer-id=" + offerId}); out.println("Bob's CLI 'getoffer' response:"); CliMain.main(new String[]{"--password=xyz", "--port=9999", "getoffer", "--offer-id=" + offerId}); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/offer/CancelOfferTest.java ================================================ /* * 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 . */ package haveno.apitest.method.offer; import haveno.core.payment.PaymentAccount; import haveno.proto.grpc.OfferInfo; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import java.util.List; import java.util.function.Consumer; import static org.junit.jupiter.api.Assertions.assertEquals; import static protobuf.OfferDirection.BUY; @Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class CancelOfferTest extends AbstractOfferTest { private static final String DIRECTION = BUY.name(); private static final String CURRENCY_CODE = "cad"; private static final int MAX_OFFERS = 3; private final Consumer createOfferToCancel = (paymentAccountId) -> { aliceClient.createMarketBasedPricedOffer(DIRECTION, CURRENCY_CODE, 10000000L, 10000000L, 0.00, defaultSecurityDepositPct.get(), paymentAccountId, NO_TRIGGER_PRICE); }; @Test @Order(1) public void testCancelOffer() { PaymentAccount cadAccount = createDummyF2FAccount(aliceClient, "CA"); // Create some offers. for (int i = 1; i <= MAX_OFFERS; i++) { createOfferToCancel.accept(cadAccount.getId()); // Wait for Alice's AddToOfferBook task. // Wait times vary; my logs show >= 2 second delay. sleep(2500); } List offers = aliceClient.getMyOffersSortedByDate(DIRECTION, CURRENCY_CODE); assertEquals(MAX_OFFERS, offers.size()); // Cancel the offers, checking the open offer count after each offer removal. for (int i = 1; i <= MAX_OFFERS; i++) { aliceClient.cancelOffer(offers.remove(0).getId()); offers = aliceClient.getMyOffersSortedByDate(DIRECTION, CURRENCY_CODE); assertEquals(MAX_OFFERS - i, offers.size()); } sleep(1000); // wait for offer removal offers = aliceClient.getMyOffersSortedByDate(DIRECTION, CURRENCY_CODE); assertEquals(0, offers.size()); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingFixedPriceTest.java ================================================ /* * 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 . */ package haveno.apitest.method.offer; import haveno.core.payment.PaymentAccount; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import static haveno.apitest.config.ApiTestConfig.EUR; import static haveno.apitest.config.ApiTestConfig.USD; import static haveno.apitest.config.ApiTestConfig.XMR; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static protobuf.OfferDirection.BUY; import static protobuf.OfferDirection.SELL; @Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest { @Test @Order(1) public void testCreateAUDBTCBuyOfferUsingFixedPrice16000() { PaymentAccount audAccount = createDummyF2FAccount(aliceClient, "AU"); var newOffer = aliceClient.createFixedPricedOffer(BUY.name(), "aud", 10_000_000L, 10_000_000L, "36000", defaultSecurityDepositPct.get(), audAccount.getId()); log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); assertFalse(newOffer.getIsActivated()); String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); assertEquals(BUY.name(), newOffer.getDirection()); assertFalse(newOffer.getUseMarketBasedPrice()); assertEquals("36000.0000", newOffer.getPrice()); assertEquals(10_000_000, newOffer.getAmount()); assertEquals(10_000_000, newOffer.getMinAmount()); assertEquals("3600", newOffer.getVolume()); assertEquals("3600", newOffer.getMinVolume()); assertEquals(.15, newOffer.getBuyerSecurityDepositPct()); assertEquals(.15, newOffer.getSellerSecurityDepositPct()); assertEquals(audAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals("AUD", newOffer.getCounterCurrencyCode()); newOffer = aliceClient.getOffer(newOfferId); assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsActivated()); assertEquals(newOfferId, newOffer.getId()); assertEquals(BUY.name(), newOffer.getDirection()); assertFalse(newOffer.getUseMarketBasedPrice()); assertEquals("36000.0000", newOffer.getPrice()); assertEquals(10_000_000, newOffer.getAmount()); assertEquals(10_000_000, newOffer.getMinAmount()); assertEquals("3600", newOffer.getVolume()); assertEquals("3600", newOffer.getMinVolume()); assertEquals(.15, newOffer.getBuyerSecurityDepositPct()); assertEquals(.15, newOffer.getSellerSecurityDepositPct()); assertEquals(audAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals("AUD", newOffer.getCounterCurrencyCode()); } @Test @Order(2) public void testCreateUSDBTCBuyOfferUsingFixedPrice100001234() { PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US"); var newOffer = aliceClient.createFixedPricedOffer(BUY.name(), "usd", 10_000_000L, 10_000_000L, "30000.1234", defaultSecurityDepositPct.get(), usdAccount.getId()); log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); assertFalse(newOffer.getIsActivated()); String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); assertEquals(BUY.name(), newOffer.getDirection()); assertFalse(newOffer.getUseMarketBasedPrice()); assertEquals("30000.1234", newOffer.getPrice()); assertEquals(10_000_000, newOffer.getAmount()); assertEquals(10_000_000, newOffer.getMinAmount()); assertEquals("3000", newOffer.getVolume()); assertEquals("3000", newOffer.getMinVolume()); assertEquals(.15, newOffer.getBuyerSecurityDepositPct()); assertEquals(.15, newOffer.getSellerSecurityDepositPct()); assertEquals(usdAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(USD, newOffer.getCounterCurrencyCode()); newOffer = aliceClient.getOffer(newOfferId); assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsActivated()); assertEquals(newOfferId, newOffer.getId()); assertEquals(BUY.name(), newOffer.getDirection()); assertFalse(newOffer.getUseMarketBasedPrice()); assertEquals("30000.1234", newOffer.getPrice()); assertEquals(10_000_000, newOffer.getAmount()); assertEquals(10_000_000, newOffer.getMinAmount()); assertEquals("3000", newOffer.getVolume()); assertEquals("3000", newOffer.getMinVolume()); assertEquals(.15, newOffer.getBuyerSecurityDepositPct()); assertEquals(.15, newOffer.getSellerSecurityDepositPct()); assertEquals(usdAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(USD, newOffer.getCounterCurrencyCode()); } @Test @Order(3) public void testCreateEURBTCSellOfferUsingFixedPrice95001234() { PaymentAccount eurAccount = createDummyF2FAccount(aliceClient, "FR"); var newOffer = aliceClient.createFixedPricedOffer(SELL.name(), "eur", 10_000_000L, 5_000_000L, "29500.1234", defaultSecurityDepositPct.get(), eurAccount.getId()); log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); assertFalse(newOffer.getIsActivated()); String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); assertEquals(SELL.name(), newOffer.getDirection()); assertFalse(newOffer.getUseMarketBasedPrice()); assertEquals("29500.1234", newOffer.getPrice()); assertEquals(10_000_000, newOffer.getAmount()); assertEquals(5_000_000, newOffer.getMinAmount()); assertEquals("2950", newOffer.getVolume()); assertEquals("1475", newOffer.getMinVolume()); assertEquals(.15, newOffer.getBuyerSecurityDepositPct()); assertEquals(.15, newOffer.getSellerSecurityDepositPct()); assertEquals(eurAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(EUR, newOffer.getCounterCurrencyCode()); newOffer = aliceClient.getOffer(newOfferId); assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsActivated()); assertEquals(newOfferId, newOffer.getId()); assertEquals(SELL.name(), newOffer.getDirection()); assertFalse(newOffer.getUseMarketBasedPrice()); assertEquals("29500.1234", newOffer.getPrice()); assertEquals(10_000_000, newOffer.getAmount()); assertEquals(5_000_000, newOffer.getMinAmount()); assertEquals("2950", newOffer.getVolume()); assertEquals("1475", newOffer.getMinVolume()); assertEquals(.15, newOffer.getBuyerSecurityDepositPct()); assertEquals(.15, newOffer.getSellerSecurityDepositPct()); assertEquals(eurAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(EUR, newOffer.getCounterCurrencyCode()); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java ================================================ /* * 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 . */ package haveno.apitest.method.offer; import haveno.core.payment.PaymentAccount; import haveno.proto.grpc.OfferInfo; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import java.math.BigDecimal; import java.text.DecimalFormat; import static haveno.apitest.config.ApiTestConfig.BTC; import static haveno.apitest.config.ApiTestConfig.USD; import static haveno.common.util.MathUtils.roundDouble; import static haveno.common.util.MathUtils.scaleDownByPowerOf10; import static haveno.common.util.MathUtils.scaleUpByPowerOf10; import static java.lang.Math.abs; import static java.lang.String.format; import static java.math.RoundingMode.HALF_UP; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static protobuf.OfferDirection.BUY; import static protobuf.OfferDirection.SELL; @SuppressWarnings("ConstantConditions") @Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest { private static final DecimalFormat PCT_FORMAT = new DecimalFormat("##0.00"); private static final double MKT_PRICE_MARGIN_ERROR_TOLERANCE = 0.0050; // 0.50% private static final double MKT_PRICE_MARGIN_WARNING_TOLERANCE = 0.0001; // 0.01% private static final String MAKER_FEE_CURRENCY_CODE = BTC; @Test @Order(1) public void testCreateUSDBTCBuyOffer5PctPriceMargin() { PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US"); double priceMarginPctInput = 5.00d; var newOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(), "usd", 10_000_000L, 10_000_000L, priceMarginPctInput, defaultSecurityDepositPct.get(), usdAccount.getId(), NO_TRIGGER_PRICE); log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); assertFalse(newOffer.getIsActivated()); String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); assertEquals(BUY.name(), newOffer.getDirection()); assertTrue(newOffer.getUseMarketBasedPrice()); assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct()); assertEquals(10_000_000, newOffer.getAmount()); assertEquals(10_000_000, newOffer.getMinAmount()); assertEquals(.15, newOffer.getBuyerSecurityDepositPct()); assertEquals(.15, newOffer.getSellerSecurityDepositPct()); assertEquals(usdAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(BTC, newOffer.getBaseCurrencyCode()); assertEquals(USD, newOffer.getCounterCurrencyCode()); newOffer = aliceClient.getOffer(newOfferId); assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsActivated()); assertEquals(newOfferId, newOffer.getId()); assertEquals(BUY.name(), newOffer.getDirection()); assertTrue(newOffer.getUseMarketBasedPrice()); assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct()); assertEquals(10_000_000, newOffer.getAmount()); assertEquals(10_000_000, newOffer.getMinAmount()); assertEquals(.15, newOffer.getBuyerSecurityDepositPct()); assertEquals(.15, newOffer.getSellerSecurityDepositPct()); assertEquals(usdAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(BTC, newOffer.getBaseCurrencyCode()); assertEquals(USD, newOffer.getCounterCurrencyCode()); assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput); } @Test @Order(2) public void testCreateNZDBTCBuyOfferMinus2PctPriceMargin() { PaymentAccount nzdAccount = createDummyF2FAccount(aliceClient, "NZ"); double priceMarginPctInput = -2.00d; // -2% var newOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(), "nzd", 10_000_000L, 10_000_000L, priceMarginPctInput, defaultSecurityDepositPct.get(), nzdAccount.getId(), NO_TRIGGER_PRICE); log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); assertFalse(newOffer.getIsActivated()); String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); assertEquals(BUY.name(), newOffer.getDirection()); assertTrue(newOffer.getUseMarketBasedPrice()); assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct()); assertEquals(10_000_000, newOffer.getAmount()); assertEquals(10_000_000, newOffer.getMinAmount()); assertEquals(.15, newOffer.getBuyerSecurityDepositPct()); assertEquals(.15, newOffer.getSellerSecurityDepositPct()); assertEquals(nzdAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(BTC, newOffer.getBaseCurrencyCode()); assertEquals("NZD", newOffer.getCounterCurrencyCode()); newOffer = aliceClient.getOffer(newOfferId); assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsActivated()); assertEquals(newOfferId, newOffer.getId()); assertEquals(BUY.name(), newOffer.getDirection()); assertTrue(newOffer.getUseMarketBasedPrice()); assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct()); assertEquals(10_000_000, newOffer.getAmount()); assertEquals(10_000_000, newOffer.getMinAmount()); assertEquals(.15, newOffer.getBuyerSecurityDepositPct()); assertEquals(.15, newOffer.getSellerSecurityDepositPct()); assertEquals(nzdAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(BTC, newOffer.getBaseCurrencyCode()); assertEquals("NZD", newOffer.getCounterCurrencyCode()); assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput); } @Test @Order(3) public void testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin() { PaymentAccount gbpAccount = createDummyF2FAccount(aliceClient, "GB"); double priceMarginPctInput = -1.5; var newOffer = aliceClient.createMarketBasedPricedOffer(SELL.name(), "gbp", 10_000_000L, 5_000_000L, priceMarginPctInput, defaultSecurityDepositPct.get(), gbpAccount.getId(), NO_TRIGGER_PRICE); log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); assertFalse(newOffer.getIsActivated()); String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); assertEquals(SELL.name(), newOffer.getDirection()); assertTrue(newOffer.getUseMarketBasedPrice()); assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct()); assertEquals(10_000_000, newOffer.getAmount()); assertEquals(5_000_000, newOffer.getMinAmount()); assertEquals(.15, newOffer.getBuyerSecurityDepositPct()); assertEquals(.15, newOffer.getSellerSecurityDepositPct()); assertEquals(gbpAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(BTC, newOffer.getBaseCurrencyCode()); assertEquals("GBP", newOffer.getCounterCurrencyCode()); newOffer = aliceClient.getOffer(newOfferId); assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsActivated()); assertEquals(newOfferId, newOffer.getId()); assertEquals(SELL.name(), newOffer.getDirection()); assertTrue(newOffer.getUseMarketBasedPrice()); assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct()); assertEquals(10_000_000, newOffer.getAmount()); assertEquals(5_000_000, newOffer.getMinAmount()); assertEquals(.15, newOffer.getBuyerSecurityDepositPct()); assertEquals(.15, newOffer.getSellerSecurityDepositPct()); assertEquals(gbpAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(BTC, newOffer.getBaseCurrencyCode()); assertEquals("GBP", newOffer.getCounterCurrencyCode()); assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput); } @Test @Order(4) public void testCreateBRLBTCSellOffer6Point55PctPriceMargin() { PaymentAccount brlAccount = createDummyF2FAccount(aliceClient, "BR"); double priceMarginPctInput = 6.55; var newOffer = aliceClient.createMarketBasedPricedOffer(SELL.name(), "brl", 10_000_000L, 5_000_000L, priceMarginPctInput, defaultSecurityDepositPct.get(), brlAccount.getId(), NO_TRIGGER_PRICE); log.debug("Offer #4:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); assertFalse(newOffer.getIsActivated()); String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); assertEquals(SELL.name(), newOffer.getDirection()); assertTrue(newOffer.getUseMarketBasedPrice()); assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct()); assertEquals(10_000_000, newOffer.getAmount()); assertEquals(5_000_000, newOffer.getMinAmount()); assertEquals(.15, newOffer.getBuyerSecurityDepositPct()); assertEquals(.15, newOffer.getSellerSecurityDepositPct()); assertEquals(brlAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(BTC, newOffer.getBaseCurrencyCode()); assertEquals("BRL", newOffer.getCounterCurrencyCode()); newOffer = aliceClient.getOffer(newOfferId); assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsActivated()); assertEquals(newOfferId, newOffer.getId()); assertEquals(SELL.name(), newOffer.getDirection()); assertTrue(newOffer.getUseMarketBasedPrice()); assertEquals(priceMarginPctInput, newOffer.getMarketPriceMarginPct()); assertEquals(10_000_000, newOffer.getAmount()); assertEquals(5_000_000, newOffer.getMinAmount()); assertEquals(.15, newOffer.getBuyerSecurityDepositPct()); assertEquals(.15, newOffer.getSellerSecurityDepositPct()); assertEquals(brlAccount.getId(), newOffer.getPaymentAccountId()); assertEquals(BTC, newOffer.getBaseCurrencyCode()); assertEquals("BRL", newOffer.getCounterCurrencyCode()); assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput); } @Test @Order(5) public void testCreateUSDBTCBuyOfferWithTriggerPrice() { PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US"); double mktPriceAsDouble = aliceClient.getBtcPrice("usd"); String triggerPrice = calcPriceAsString(mktPriceAsDouble, Double.parseDouble("1000.9999"), 4); var newOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(), "usd", 10_000_000L, 5_000_000L, 0.0, defaultSecurityDepositPct.get(), usdAccount.getId(), triggerPrice); assertTrue(newOffer.getIsMyOffer()); assertFalse(newOffer.getIsActivated()); genBtcBlocksThenWait(1, 4000); // give time to add to offer book newOffer = aliceClient.getOffer(newOffer.getId()); log.debug("Offer #5:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsActivated()); assertEquals(triggerPrice, newOffer.getTriggerPrice()); } private void assertCalculatedPriceIsCorrect(OfferInfo offer, double priceMarginPctInput) { assertTrue(() -> { String counterCurrencyCode = offer.getCounterCurrencyCode(); double mktPrice = aliceClient.getBtcPrice(counterCurrencyCode); double priceAsDouble = Double.parseDouble(offer.getPrice()); double expectedDiffPct = scaleDownByPowerOf10(priceMarginPctInput, 2); double actualDiffPct = offer.getDirection().equals(BUY.name()) ? getPercentageDifference(priceAsDouble, mktPrice) : getPercentageDifference(mktPrice, priceAsDouble); double pctDiffDelta = abs(expectedDiffPct) - abs(actualDiffPct); return isCalculatedPriceWithinErrorTolerance(pctDiffDelta, expectedDiffPct, actualDiffPct, mktPrice, priceAsDouble, offer); }); } private double getPercentageDifference(double price1, double price2) { return BigDecimal.valueOf(roundDouble((1 - (price1 / price2)), 5)) .setScale(4, HALF_UP) .doubleValue(); } private boolean isCalculatedPriceWithinErrorTolerance(double delta, double expectedDiffPct, double actualDiffPct, double mktPrice, double scaledOfferPrice, OfferInfo offer) { if (abs(delta) > MKT_PRICE_MARGIN_ERROR_TOLERANCE) { logCalculatedPricePoppedErrorTolerance(expectedDiffPct, actualDiffPct, mktPrice, scaledOfferPrice); log.error(offer.toString()); return false; } if (abs(delta) >= MKT_PRICE_MARGIN_WARNING_TOLERANCE) { logCalculatedPricePoppedWarningTolerance(expectedDiffPct, actualDiffPct, mktPrice, scaledOfferPrice); log.trace(offer.toString()); } return true; } private void logCalculatedPricePoppedWarningTolerance(double expectedDiffPct, double actualDiffPct, double mktPrice, double scaledOfferPrice) { log.warn(format("Calculated price %.4f & mkt price %.4f differ by ~ %s%s," + " not by %s%s, outside the %s%s warning tolerance," + " but within the %s%s error tolerance.", scaledOfferPrice, mktPrice, PCT_FORMAT.format(scaleUpByPowerOf10(actualDiffPct, 2)), "%", PCT_FORMAT.format(scaleUpByPowerOf10(expectedDiffPct, 2)), "%", PCT_FORMAT.format(scaleUpByPowerOf10(MKT_PRICE_MARGIN_WARNING_TOLERANCE, 2)), "%", PCT_FORMAT.format(scaleUpByPowerOf10(MKT_PRICE_MARGIN_ERROR_TOLERANCE, 2)), "%")); } private void logCalculatedPricePoppedErrorTolerance(double expectedDiffPct, double actualDiffPct, double mktPrice, double scaledOfferPrice) { log.error(format("Calculated price %.4f & mkt price %.4f differ by ~ %s%s," + " not by %s%s, outside the %s%s error tolerance.", scaledOfferPrice, mktPrice, PCT_FORMAT.format(scaleUpByPowerOf10(actualDiffPct, 2)), "%", PCT_FORMAT.format(scaleUpByPowerOf10(expectedDiffPct, 2)), "%", PCT_FORMAT.format(scaleUpByPowerOf10(MKT_PRICE_MARGIN_ERROR_TOLERANCE, 2)), "%")); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/offer/CreateXMROffersTest.java ================================================ /* * 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 . */ package haveno.apitest.method.offer; import haveno.proto.grpc.OfferInfo; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import java.util.List; import static haveno.apitest.config.ApiTestConfig.BTC; import static haveno.apitest.config.ApiTestConfig.XMR; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static protobuf.OfferDirection.BUY; import static protobuf.OfferDirection.SELL; @SuppressWarnings("ConstantConditions") @Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class CreateXMROffersTest extends AbstractOfferTest { private static final String MAKER_FEE_CURRENCY_CODE = BTC; @BeforeAll public static void setUp() { AbstractOfferTest.setUp(); createXmrPaymentAccounts(); } @Test @Order(1) public void testCreateFixedPriceBuy1BTCFor200KXMROffer() { // Remember alt coin trades are BTC trades. When placing an offer, you are // offering to buy or sell BTC, not ETH, XMR, etc. In this test case, // Alice places an offer to BUY BTC. var newOffer = aliceClient.createFixedPricedOffer(BUY.name(), XMR, 100_000_000L, 75_000_000L, "0.005", // FIXED PRICE IN BTC FOR 1 XMR defaultSecurityDepositPct.get(), alicesXmrAcct.getId()); log.debug("Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); assertFalse(newOffer.getIsActivated()); String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); assertEquals(BUY.name(), newOffer.getDirection()); assertFalse(newOffer.getUseMarketBasedPrice()); assertEquals("0.00500000", newOffer.getPrice()); assertEquals(100_000_000L, newOffer.getAmount()); assertEquals(75_000_000L, newOffer.getMinAmount()); assertEquals(.15, newOffer.getBuyerSecurityDepositPct()); assertEquals(.15, newOffer.getSellerSecurityDepositPct()); assertEquals(alicesXmrAcct.getId(), newOffer.getPaymentAccountId()); assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(BTC, newOffer.getCounterCurrencyCode()); genBtcBlockAndWaitForOfferPreparation(); newOffer = aliceClient.getOffer(newOfferId); assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsActivated()); assertEquals(newOfferId, newOffer.getId()); assertEquals(BUY.name(), newOffer.getDirection()); assertFalse(newOffer.getUseMarketBasedPrice()); assertEquals("0.00500000", newOffer.getPrice()); assertEquals(100_000_000L, newOffer.getAmount()); assertEquals(75_000_000L, newOffer.getMinAmount()); assertEquals(.15, newOffer.getBuyerSecurityDepositPct()); assertEquals(.15, newOffer.getSellerSecurityDepositPct()); assertEquals(alicesXmrAcct.getId(), newOffer.getPaymentAccountId()); assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(BTC, newOffer.getCounterCurrencyCode()); } @Test @Order(2) public void testCreateFixedPriceSell1BTCFor200KXMROffer() { // Alice places an offer to SELL BTC for XMR. var newOffer = aliceClient.createFixedPricedOffer(SELL.name(), XMR, 100_000_000L, 50_000_000L, "0.005", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR defaultSecurityDepositPct.get(), alicesXmrAcct.getId()); log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); assertFalse(newOffer.getIsActivated()); String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); assertEquals(SELL.name(), newOffer.getDirection()); assertFalse(newOffer.getUseMarketBasedPrice()); assertEquals("0.00500000", newOffer.getPrice()); assertEquals(100_000_000L, newOffer.getAmount()); assertEquals(50_000_000L, newOffer.getMinAmount()); assertEquals(.15, newOffer.getBuyerSecurityDepositPct()); assertEquals(.15, newOffer.getSellerSecurityDepositPct()); assertEquals(alicesXmrAcct.getId(), newOffer.getPaymentAccountId()); assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(BTC, newOffer.getCounterCurrencyCode()); genBtcBlockAndWaitForOfferPreparation(); newOffer = aliceClient.getOffer(newOfferId); assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsActivated()); assertEquals(newOfferId, newOffer.getId()); assertEquals(SELL.name(), newOffer.getDirection()); assertFalse(newOffer.getUseMarketBasedPrice()); assertEquals("0.00500000", newOffer.getPrice()); assertEquals(100_000_000L, newOffer.getAmount()); assertEquals(50_000_000L, newOffer.getMinAmount()); assertEquals(.15, newOffer.getBuyerSecurityDepositPct()); assertEquals(.15, newOffer.getSellerSecurityDepositPct()); assertEquals(alicesXmrAcct.getId(), newOffer.getPaymentAccountId()); assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(BTC, newOffer.getCounterCurrencyCode()); } @Test @Order(3) public void testCreatePriceMarginBasedBuy1BTCOfferWithTriggerPrice() { double priceMarginPctInput = 1.00; double mktPriceAsDouble = aliceClient.getBtcPrice(XMR); String triggerPrice = calcPriceAsString(mktPriceAsDouble, Double.parseDouble("-0.001"), 8); var newOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(), XMR, 100_000_000L, 75_000_000L, priceMarginPctInput, defaultSecurityDepositPct.get(), alicesXmrAcct.getId(), triggerPrice); log.debug("Pending Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); assertFalse(newOffer.getIsActivated()); String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); assertEquals(BUY.name(), newOffer.getDirection()); assertTrue(newOffer.getUseMarketBasedPrice()); // There is no trigger price while offer is pending. assertEquals(NO_TRIGGER_PRICE, newOffer.getTriggerPrice()); assertEquals(100_000_000L, newOffer.getAmount()); assertEquals(75_000_000L, newOffer.getMinAmount()); assertEquals(.15, newOffer.getBuyerSecurityDepositPct()); assertEquals(.15, newOffer.getSellerSecurityDepositPct()); assertEquals(alicesXmrAcct.getId(), newOffer.getPaymentAccountId()); assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(BTC, newOffer.getCounterCurrencyCode()); genBtcBlockAndWaitForOfferPreparation(); newOffer = aliceClient.getOffer(newOfferId); log.debug("Available Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsActivated()); assertEquals(newOfferId, newOffer.getId()); assertEquals(BUY.name(), newOffer.getDirection()); assertTrue(newOffer.getUseMarketBasedPrice()); // The trigger price should exist on the prepared offer. assertEquals(triggerPrice, newOffer.getTriggerPrice()); assertEquals(100_000_000L, newOffer.getAmount()); assertEquals(75_000_000L, newOffer.getMinAmount()); assertEquals(.15, newOffer.getBuyerSecurityDepositPct()); assertEquals(.15, newOffer.getSellerSecurityDepositPct()); assertEquals(alicesXmrAcct.getId(), newOffer.getPaymentAccountId()); assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(BTC, newOffer.getCounterCurrencyCode()); } @Test @Order(4) public void testCreatePriceMarginBasedSell1BTCOffer() { // Alice places an offer to SELL BTC for XMR. double priceMarginPctInput = 0.50; var newOffer = aliceClient.createMarketBasedPricedOffer(SELL.name(), XMR, 100_000_000L, 50_000_000L, priceMarginPctInput, defaultSecurityDepositPct.get(), alicesXmrAcct.getId(), NO_TRIGGER_PRICE); log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); assertFalse(newOffer.getIsActivated()); String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); assertEquals(SELL.name(), newOffer.getDirection()); assertTrue(newOffer.getUseMarketBasedPrice()); assertEquals(100_000_000L, newOffer.getAmount()); assertEquals(50_000_000L, newOffer.getMinAmount()); assertEquals(.15, newOffer.getBuyerSecurityDepositPct()); assertEquals(.15, newOffer.getSellerSecurityDepositPct()); assertEquals(alicesXmrAcct.getId(), newOffer.getPaymentAccountId()); assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(BTC, newOffer.getCounterCurrencyCode()); genBtcBlockAndWaitForOfferPreparation(); newOffer = aliceClient.getOffer(newOfferId); assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsActivated()); assertEquals(newOfferId, newOffer.getId()); assertEquals(SELL.name(), newOffer.getDirection()); assertTrue(newOffer.getUseMarketBasedPrice()); assertEquals(100_000_000L, newOffer.getAmount()); assertEquals(50_000_000L, newOffer.getMinAmount()); assertEquals(.15, newOffer.getBuyerSecurityDepositPct()); assertEquals(.15, newOffer.getSellerSecurityDepositPct()); assertEquals(alicesXmrAcct.getId(), newOffer.getPaymentAccountId()); assertEquals(XMR, newOffer.getBaseCurrencyCode()); assertEquals(BTC, newOffer.getCounterCurrencyCode()); } @Test @Order(5) public void testGetAllMyXMROffers() { List offers = aliceClient.getMyOffersSortedByDate(XMR); log.debug("All of Alice's XMR offers:\n{}", toOffersTable.apply(offers)); assertEquals(4, offers.size()); log.debug("Alice's balances\n{}", formatBalancesTbls(aliceClient.getBalances())); } @Test @Order(6) public void testGetAvailableXMROffers() { List offers = bobClient.getOffersSortedByDate(XMR); log.debug("All of Bob's available XMR offers:\n{}", toOffersTable.apply(offers)); assertEquals(4, offers.size()); log.debug("Bob's balances\n{}", formatBalancesTbls(bobClient.getBalances())); } private void genBtcBlockAndWaitForOfferPreparation() { genBtcBlocksThenWait(1, 5000); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/offer/ValidateCreateOfferTest.java ================================================ /* * 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 . */ package haveno.apitest.method.offer; import haveno.core.payment.PaymentAccount; import io.grpc.StatusRuntimeException; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static protobuf.OfferDirection.BUY; @Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class ValidateCreateOfferTest extends AbstractOfferTest { @Test @Order(1) public void testAmtTooLargeShouldThrowException() { PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US"); @SuppressWarnings("ResultOfMethodCallIgnored") Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.createFixedPricedOffer(BUY.name(), "usd", 100000000000L, // exceeds amount limit 100000000000L, "10000.0000", defaultSecurityDepositPct.get(), usdAccount.getId())); assertEquals("UNKNOWN: An error occurred at task: ValidateOffer", exception.getMessage()); } @Test @Order(2) public void testNoMatchingEURPaymentAccountShouldThrowException() { PaymentAccount chfAccount = createDummyF2FAccount(aliceClient, "ch"); @SuppressWarnings("ResultOfMethodCallIgnored") Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.createFixedPricedOffer(BUY.name(), "eur", 10000000L, 10000000L, "40000.0000", defaultSecurityDepositPct.get(), chfAccount.getId())); String expectedError = format("UNKNOWN: cannot create EUR offer with payment account %s", chfAccount.getId()); assertEquals(expectedError, exception.getMessage()); } @Test @Order(2) public void testNoMatchingCADPaymentAccountShouldThrowException() { PaymentAccount audAccount = createDummyF2FAccount(aliceClient, "au"); @SuppressWarnings("ResultOfMethodCallIgnored") Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.createFixedPricedOffer(BUY.name(), "cad", 10000000L, 10000000L, "63000.0000", defaultSecurityDepositPct.get(), audAccount.getId())); String expectedError = format("UNKNOWN: cannot create CAD offer with payment account %s", audAccount.getId()); assertEquals(expectedError, exception.getMessage()); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/payment/AbstractPaymentAccountTest.java ================================================ package haveno.apitest.method.payment; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.stream.JsonWriter; import haveno.apitest.method.MethodTest; import haveno.cli.GrpcClient; import haveno.core.api.model.PaymentAccountForm; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.PaymentAccount; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInfo; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import static java.lang.String.format; import static java.lang.System.getProperty; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @Slf4j public class AbstractPaymentAccountTest extends MethodTest { static final String PROPERTY_NAME_JSON_COMMENTS = "_COMMENTS_"; static final List PROPERTY_VALUE_JSON_COMMENTS = new ArrayList<>() {{ add("Do not manually edit the paymentMethodId field."); add("Edit the salt field only if you are recreating a payment" + " account on a new installation and wish to preserve the account age."); }}; static final String PROPERTY_NAME_PAYMENT_METHOD_ID = "paymentMethodId"; static final String PROPERTY_NAME_ACCOUNT_ID = "accountId"; static final String PROPERTY_NAME_ACCOUNT_NAME = "accountName"; static final String PROPERTY_NAME_ACCOUNT_NR = "accountNr"; static final String PROPERTY_NAME_ACCOUNT_TYPE = "accountType"; static final String PROPERTY_NAME_ANSWER = "answer"; static final String PROPERTY_NAME_BANK_ACCOUNT_NAME = "bankAccountName"; static final String PROPERTY_NAME_BANK_ACCOUNT_NUMBER = "bankAccountNumber"; static final String PROPERTY_NAME_BANK_ACCOUNT_TYPE = "bankAccountType"; static final String PROPERTY_NAME_BANK_ADDRESS = "bankAddress"; static final String PROPERTY_NAME_BANK_BRANCH = "bankBranch"; static final String PROPERTY_NAME_BANK_BRANCH_CODE = "bankBranchCode"; static final String PROPERTY_NAME_BANK_BRANCH_NAME = "bankBranchName"; static final String PROPERTY_NAME_BANK_CODE = "bankCode"; static final String PROPERTY_NAME_BANK_COUNTRY_CODE = "bankCountryCode"; @SuppressWarnings("unused") static final String PROPERTY_NAME_BANK_ID = "bankId"; static final String PROPERTY_NAME_BANK_NAME = "bankName"; static final String PROPERTY_NAME_BANK_SWIFT_CODE = "bankSwiftCode"; static final String PROPERTY_NAME_BRANCH_ID = "branchId"; static final String PROPERTY_NAME_BIC = "bic"; static final String PROPERTY_NAME_BENEFICIARY_NAME = "beneficiaryName"; static final String PROPERTY_NAME_BENEFICIARY_ACCOUNT_NR = "beneficiaryAccountNr"; static final String PROPERTY_NAME_BENEFICIARY_ADDRESS = "beneficiaryAddress"; static final String PROPERTY_NAME_BENEFICIARY_CITY = "beneficiaryCity"; static final String PROPERTY_NAME_BENEFICIARY_PHONE = "beneficiaryPhone"; static final String PROPERTY_NAME_COUNTRY = "country"; static final String PROPERTY_NAME_CITY = "city"; static final String PROPERTY_NAME_CONTACT = "contact"; static final String PROPERTY_NAME_EMAIL = "email"; static final String PROPERTY_NAME_EMAIL_OR_MOBILE_NR = "emailOrMobileNr"; static final String PROPERTY_NAME_EXTRA_INFO = "extraInfo"; static final String PROPERTY_NAME_HOLDER_EMAIL = "holderEmail"; static final String PROPERTY_NAME_HOLDER_NAME = "holderName"; static final String PROPERTY_NAME_HOLDER_TAX_ID = "holderTaxId"; static final String PROPERTY_NAME_IBAN = "iban"; static final String PROPERTY_NAME_INTERMEDIARY_ADDRESS = "intermediaryAddress"; static final String PROPERTY_NAME_INTERMEDIARY_BRANCH = "intermediaryBranch"; static final String PROPERTY_NAME_INTERMEDIARY_COUNTRY_CODE = "intermediaryCountryCode"; static final String PROPERTY_NAME_INTERMEDIARY_NAME = "intermediaryName"; static final String PROPERTY_NAME_INTERMEDIARY_SWIFT_CODE = "intermediarySwiftCode"; static final String PROPERTY_NAME_MOBILE_NR = "mobileNr"; static final String PROPERTY_NAME_NATIONAL_ACCOUNT_ID = "nationalAccountId"; static final String PROPERTY_NAME_PAY_ID = "payid"; static final String PROPERTY_NAME_POSTAL_ADDRESS = "postalAddress"; static final String PROPERTY_NAME_PROMPT_PAY_ID = "promptPayId"; static final String PROPERTY_NAME_QUESTION = "question"; static final String PROPERTY_NAME_REQUIREMENTS = "requirements"; static final String PROPERTY_NAME_SALT = "salt"; static final String PROPERTY_NAME_SELECTED_TRADE_CURRENCY = "selectedTradeCurrency"; static final String PROPERTY_NAME_SORT_CODE = "sortCode"; static final String PROPERTY_NAME_SPECIAL_INSTRUCTIONS = "specialInstructions"; static final String PROPERTY_NAME_STATE = "state"; static final String PROPERTY_NAME_TRADE_CURRENCIES = "tradeCurrencies"; static final String PROPERTY_NAME_USERNAME = "userName"; static final Gson GSON = new GsonBuilder() .setPrettyPrinting() .serializeNulls() .create(); static final Map COMPLETED_FORM_MAP = new HashMap<>(); @BeforeEach public void setup() { Res.setup(); } protected final File getEmptyForm(TestInfo testInfo, String paymentMethodId) { // This would normally be done in @BeforeEach, but these test cases might be // called from a single 'scenario' test case, and the @BeforeEach -> clear() // would be skipped. COMPLETED_FORM_MAP.clear(); File emptyForm = getPaymentAccountForm(aliceClient, paymentMethodId); // A shortcut over the API: // File emptyForm = PAYMENT_ACCOUNT_FORM.getPaymentAccountForm(paymentMethodId); log.debug("{} Empty form saved to {}", testName(testInfo), PaymentAccountForm.getClickableURI(emptyForm)); emptyForm.deleteOnExit(); return emptyForm; } protected final void verifyEmptyForm(File jsonForm, String paymentMethodId, String... fields) { @SuppressWarnings("unchecked") Map emptyForm = (Map) GSON.fromJson( PaymentAccountForm.toJsonString(jsonForm), Object.class); assertNotNull(emptyForm); if (paymentMethodId.equals("SWIFT_ID")) { assertEquals(getSwiftFormComments(), emptyForm.get(PROPERTY_NAME_JSON_COMMENTS)); } else { assertEquals(PROPERTY_VALUE_JSON_COMMENTS, emptyForm.get(PROPERTY_NAME_JSON_COMMENTS)); } assertEquals(paymentMethodId, emptyForm.get(PROPERTY_NAME_PAYMENT_METHOD_ID)); assertEquals("your accountname", emptyForm.get(PROPERTY_NAME_ACCOUNT_NAME)); for (String field : fields) { if (field.equals("country")) assertEquals("your two letter country code", emptyForm.get(field)); else assertEquals("your " + field.toLowerCase(), emptyForm.get(field)); } } protected final void verifyCommonFormEntries(PaymentAccount paymentAccount) { // All PaymentAccount subclasses have paymentMethodId and an accountName fields. assertNotNull(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_PAYMENT_METHOD_ID), paymentAccount.getPaymentMethod().getId()); assertTrue(paymentAccount.getCreationDate().getTime() > 0); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NAME), paymentAccount.getAccountName()); } protected final void verifyAccountSingleTradeCurrency(String expectedCurrencyCode, PaymentAccount paymentAccount) { assertNotNull(paymentAccount.getSingleTradeCurrency()); assertEquals(expectedCurrencyCode, paymentAccount.getSingleTradeCurrency().getCode()); } protected final void verifyAccountTradeCurrencies(Collection expectedTraditionalCurrencies, PaymentAccount paymentAccount) { assertNotNull(paymentAccount.getTradeCurrencies()); List expectedTradeCurrencies = new ArrayList<>() {{ addAll(expectedTraditionalCurrencies); }}; assertArrayEquals(expectedTradeCurrencies.toArray(), paymentAccount.getTradeCurrencies().toArray()); } protected final void verifyAccountTradeCurrencies(List expectedTradeCurrencies, PaymentAccount paymentAccount) { assertNotNull(paymentAccount.getTradeCurrencies()); assertArrayEquals(expectedTradeCurrencies.toArray(), paymentAccount.getTradeCurrencies().toArray()); } protected final void verifyUserPayloadHasPaymentAccountWithId(GrpcClient grpcClient, String paymentAccountId) { Optional paymentAccount = grpcClient.getPaymentAccounts() .stream() .filter(a -> a.getId().equals(paymentAccountId)) .findFirst(); assertTrue(paymentAccount.isPresent()); } protected final String getCompletedFormAsJsonString(List comments) { File completedForm = fillPaymentAccountForm(comments); String jsonString = PaymentAccountForm.toJsonString(completedForm); log.debug("Completed form: {}", jsonString); return jsonString; } protected final String getCompletedFormAsJsonString() { File completedForm = fillPaymentAccountForm(PROPERTY_VALUE_JSON_COMMENTS); String jsonString = PaymentAccountForm.toJsonString(completedForm); log.debug("Completed form: {}", jsonString); return jsonString; } protected final String getCommaDelimitedTraditionalCurrencyCodes(Collection traditionalCurrencies) { return traditionalCurrencies.stream() .sorted(Comparator.comparing(TradeCurrency::getCode)) .map(c -> c.getCode()) .collect(Collectors.joining(",")); } protected final List getSwiftFormComments() { List comments = new ArrayList<>(); comments.addAll(PROPERTY_VALUE_JSON_COMMENTS); List wrappedSwiftComments = Res.getWrappedAsList("payment.swift.info.account", 110); comments.addAll(wrappedSwiftComments); return comments; } private File fillPaymentAccountForm(List comments) { File tmpJsonForm = null; try { tmpJsonForm = File.createTempFile("temp_acct_form_", ".json", Paths.get(getProperty("java.io.tmpdir")).toFile()); JsonWriter writer = new JsonWriter(new OutputStreamWriter(new FileOutputStream(tmpJsonForm), UTF_8)); writer.beginObject(); writer.name(PROPERTY_NAME_JSON_COMMENTS); writer.beginArray(); for (String s : comments) { writer.value(s); } writer.endArray(); for (Map.Entry entry : COMPLETED_FORM_MAP.entrySet()) { String k = entry.getKey(); Object v = entry.getValue(); writer.name(k); writer.value(v.toString()); } writer.endObject(); writer.close(); } catch (IOException ex) { log.error("", ex); fail(format("Could not write json file from form entries %s", COMPLETED_FORM_MAP)); } tmpJsonForm.deleteOnExit(); return tmpJsonForm; } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/payment/CreatePaymentAccountTest.java ================================================ /* * 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 . */ package haveno.apitest.method.payment; import haveno.cli.table.builder.TableBuilder; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.AdvancedCashAccount; import haveno.core.payment.AliPayAccount; import haveno.core.payment.AustraliaPayidAccount; import haveno.core.payment.CapitualAccount; import haveno.core.payment.CashDepositAccount; import haveno.core.payment.ZelleAccount; import haveno.core.payment.F2FAccount; import haveno.core.payment.FasterPaymentsAccount; import haveno.core.payment.HalCashAccount; import haveno.core.payment.InteracETransferAccount; import haveno.core.payment.JapanBankAccount; import haveno.core.payment.MoneyBeamAccount; import haveno.core.payment.MoneyGramAccount; import haveno.core.payment.NationalBankAccount; import haveno.core.payment.PaxumAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PayseraAccount; import haveno.core.payment.PerfectMoneyAccount; import haveno.core.payment.PopmoneyAccount; import haveno.core.payment.PromptPayAccount; import haveno.core.payment.RevolutAccount; import haveno.core.payment.SameBankAccount; import haveno.core.payment.SepaAccount; import haveno.core.payment.SepaInstantAccount; import haveno.core.payment.SpecificBanksAccount; import haveno.core.payment.SwiftAccount; import haveno.core.payment.SwishAccount; import haveno.core.payment.TransferwiseAccount; import haveno.core.payment.USPostalMoneyOrderAccount; import haveno.core.payment.UpholdAccount; import haveno.core.payment.WeChatPayAccount; import haveno.core.payment.WesternUnionAccount; import haveno.core.payment.payload.BankAccountPayload; import haveno.core.payment.payload.CashDepositAccountPayload; import haveno.core.payment.payload.SameBankAccountPayload; import haveno.core.payment.payload.SpecificBanksAccountPayload; import haveno.core.payment.payload.SwiftAccountPayload; import io.grpc.StatusRuntimeException; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import static haveno.apitest.Scaffold.BitcoinCoreApp.bitcoind; import static haveno.apitest.config.ApiTestConfig.EUR; import static haveno.apitest.config.ApiTestConfig.USD; import static haveno.apitest.config.HavenoAppConfig.alicedaemon; import static haveno.cli.table.builder.TableType.PAYMENT_ACCOUNT_TBL; import static haveno.core.locale.CurrencyUtil.getAllSortedTraditionalCurrencies; import static haveno.core.locale.CurrencyUtil.getTradeCurrency; import static haveno.core.payment.payload.PaymentMethod.ADVANCED_CASH_ID; import static haveno.core.payment.payload.PaymentMethod.ALI_PAY_ID; import static haveno.core.payment.payload.PaymentMethod.AUSTRALIA_PAYID_ID; import static haveno.core.payment.payload.PaymentMethod.CAPITUAL_ID; import static haveno.core.payment.payload.PaymentMethod.CASH_DEPOSIT_ID; import static haveno.core.payment.payload.PaymentMethod.ZELLE_ID; import static haveno.core.payment.payload.PaymentMethod.F2F_ID; import static haveno.core.payment.payload.PaymentMethod.FASTER_PAYMENTS_ID; import static haveno.core.payment.payload.PaymentMethod.HAL_CASH_ID; import static haveno.core.payment.payload.PaymentMethod.INTERAC_E_TRANSFER_ID; import static haveno.core.payment.payload.PaymentMethod.JAPAN_BANK_ID; import static haveno.core.payment.payload.PaymentMethod.MONEY_BEAM_ID; import static haveno.core.payment.payload.PaymentMethod.MONEY_GRAM_ID; import static haveno.core.payment.payload.PaymentMethod.NATIONAL_BANK_ID; import static haveno.core.payment.payload.PaymentMethod.PAXUM_ID; import static haveno.core.payment.payload.PaymentMethod.PAYSERA_ID; import static haveno.core.payment.payload.PaymentMethod.PERFECT_MONEY_ID; import static haveno.core.payment.payload.PaymentMethod.POPMONEY_ID; import static haveno.core.payment.payload.PaymentMethod.PROMPT_PAY_ID; import static haveno.core.payment.payload.PaymentMethod.REVOLUT_ID; import static haveno.core.payment.payload.PaymentMethod.SAME_BANK_ID; import static haveno.core.payment.payload.PaymentMethod.SEPA_ID; import static haveno.core.payment.payload.PaymentMethod.SEPA_INSTANT_ID; import static haveno.core.payment.payload.PaymentMethod.SPECIFIC_BANKS_ID; import static haveno.core.payment.payload.PaymentMethod.SWIFT_ID; import static haveno.core.payment.payload.PaymentMethod.SWISH_ID; import static haveno.core.payment.payload.PaymentMethod.TRANSFERWISE_ID; import static haveno.core.payment.payload.PaymentMethod.UPHOLD_ID; import static haveno.core.payment.payload.PaymentMethod.US_POSTAL_MONEY_ORDER_ID; import static haveno.core.payment.payload.PaymentMethod.WECHAT_PAY_ID; import static haveno.core.payment.payload.PaymentMethod.WESTERN_UNION_ID; import static java.util.Comparator.comparing; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; @SuppressWarnings({"OptionalGetWithoutIsPresent", "ConstantConditions"}) @Disabled @Slf4j @TestMethodOrder(OrderAnnotation.class) public class CreatePaymentAccountTest extends AbstractPaymentAccountTest { @BeforeAll public static void setUp() { try { setUpScaffold(bitcoind, alicedaemon); } catch (Exception ex) { fail(ex); } } @Test public void testCreateAdvancedCashAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, ADVANCED_CASH_ID); verifyEmptyForm(emptyForm, ADVANCED_CASH_ID, PROPERTY_NAME_ACCOUNT_NR); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, ADVANCED_CASH_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Advanced Cash Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "0000 1111 2222"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, AdvancedCashAccount.SUPPORTED_CURRENCIES .stream() .map(TradeCurrency::getCode) .collect(Collectors.joining(","))); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "RUB"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Advanced Cash Acct Salt")); String jsonString = getCompletedFormAsJsonString(); AdvancedCashAccount paymentAccount = (AdvancedCashAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountTradeCurrencies(AdvancedCashAccount.SUPPORTED_CURRENCIES, paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY), paymentAccount.getSelectedTradeCurrency().getCode()); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); print(paymentAccount); } @Test public void testCreateAliPayAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, ALI_PAY_ID); verifyEmptyForm(emptyForm, ALI_PAY_ID, PROPERTY_NAME_ACCOUNT_NR); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, ALI_PAY_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Ali Pay Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "2222 3333 4444"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); String jsonString = getCompletedFormAsJsonString(); AliPayAccount paymentAccount = (AliPayAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountSingleTradeCurrency("CNY", paymentAccount); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr()); print(paymentAccount); } @Test public void testCreateAustraliaPayidAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, AUSTRALIA_PAYID_ID); verifyEmptyForm(emptyForm, AUSTRALIA_PAYID_ID, PROPERTY_NAME_BANK_ACCOUNT_NAME); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, AUSTRALIA_PAYID_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Australia Pay ID Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAY_ID, "123 456 789"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_NAME, "Credit Union Australia"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Australia Pay ID Acct Salt")); String jsonString = getCompletedFormAsJsonString(); AustraliaPayidAccount paymentAccount = (AustraliaPayidAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountSingleTradeCurrency("AUD", paymentAccount); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_PAY_ID), paymentAccount.getPayid()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ACCOUNT_NAME), paymentAccount.getBankAccountName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); print(paymentAccount); } @Test public void testCreateCapitualAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, CAPITUAL_ID); verifyEmptyForm(emptyForm, CAPITUAL_ID, PROPERTY_NAME_ACCOUNT_NR); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, CAPITUAL_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Capitual Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "1111 2222 3333-4"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, CapitualAccount.SUPPORTED_CURRENCIES .stream() .map(TradeCurrency::getCode) .collect(Collectors.joining(","))); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "BRL"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Capitual Acct Salt")); String jsonString = getCompletedFormAsJsonString(); CapitualAccount paymentAccount = (CapitualAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountTradeCurrencies(CapitualAccount.SUPPORTED_CURRENCIES, paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY), paymentAccount.getSelectedTradeCurrency().getCode()); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); print(paymentAccount); } @Test public void testCreateCashDepositAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, CASH_DEPOSIT_ID); verifyEmptyForm(emptyForm, CASH_DEPOSIT_ID, PROPERTY_NAME_ACCOUNT_NR, PROPERTY_NAME_ACCOUNT_TYPE, PROPERTY_NAME_BANK_ID, PROPERTY_NAME_BANK_NAME, PROPERTY_NAME_BRANCH_ID, PROPERTY_NAME_COUNTRY, PROPERTY_NAME_HOLDER_EMAIL, PROPERTY_NAME_HOLDER_NAME, PROPERTY_NAME_HOLDER_TAX_ID, PROPERTY_NAME_NATIONAL_ACCOUNT_ID, PROPERTY_NAME_REQUIREMENTS); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, CASH_DEPOSIT_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Cash Deposit Account"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "4444 5555 6666"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_TYPE, "Checking"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ID, "0001"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "BoF"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BRANCH_ID, "99-8888-7654"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "FR"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_EMAIL, "jean@johnson.info"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jean Johnson"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_REQUIREMENTS, "Requirements..."); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); String jsonString = getCompletedFormAsJsonString(); CashDepositAccount paymentAccount = (CashDepositAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountSingleTradeCurrency(EUR, paymentAccount); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), Objects.requireNonNull(paymentAccount.getCountry()).code); CashDepositAccountPayload payload = (CashDepositAccountPayload) paymentAccount.getPaymentAccountPayload(); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_TYPE), payload.getAccountType()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ID), payload.getBankId()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_EMAIL), payload.getHolderEmail()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_REQUIREMENTS), payload.getRequirements()); print(paymentAccount); } @Test public void testCreateBrazilNationalBankAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, NATIONAL_BANK_ID); verifyEmptyForm(emptyForm, NATIONAL_BANK_ID, PROPERTY_NAME_ACCOUNT_NR, PROPERTY_NAME_ACCOUNT_TYPE, PROPERTY_NAME_BANK_NAME, PROPERTY_NAME_BRANCH_ID, PROPERTY_NAME_COUNTRY, PROPERTY_NAME_HOLDER_NAME, PROPERTY_NAME_HOLDER_TAX_ID, PROPERTY_NAME_NATIONAL_ACCOUNT_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, NATIONAL_BANK_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Banco do Brasil"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "456789-87"); // No BankId is required for BR. COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "Banco do Brasil"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BRANCH_ID, "456789-10"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "BR"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Joao da Silva"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Banco do Brasil Acct Salt")); String jsonString = getCompletedFormAsJsonString(); NationalBankAccount paymentAccount = (NationalBankAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountSingleTradeCurrency("BRL", paymentAccount); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), Objects.requireNonNull(paymentAccount.getCountry()).code); BankAccountPayload payload = (BankAccountPayload) paymentAccount.getPaymentAccountPayload(); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr()); // When no BankId is required, getBankId() returns bankName. assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankId()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); print(paymentAccount); } @Test public void testCreateZelleAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, ZELLE_ID); verifyEmptyForm(emptyForm, ZELLE_ID, PROPERTY_NAME_EMAIL_OR_MOBILE_NR, PROPERTY_NAME_HOLDER_NAME); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, ZELLE_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "USD Zelle Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL_OR_MOBILE_NR, "jane@doe.com"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Zelle Acct Salt")); String jsonString = getCompletedFormAsJsonString(); ZelleAccount paymentAccount = (ZelleAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountSingleTradeCurrency(USD, paymentAccount); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL_OR_MOBILE_NR), paymentAccount.getEmailOrMobileNr()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); print(paymentAccount); } @Test public void testCreateF2FAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, F2F_ID); verifyEmptyForm(emptyForm, F2F_ID, PROPERTY_NAME_COUNTRY, PROPERTY_NAME_CITY, PROPERTY_NAME_CONTACT, PROPERTY_NAME_EXTRA_INFO); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, F2F_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Conta Cara a Cara"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "BR"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_CITY, "Rio de Janeiro"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_CONTACT, "Freddy Beira Mar"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_EXTRA_INFO, "So fim de semana"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); String jsonString = getCompletedFormAsJsonString(); F2FAccount paymentAccount = (F2FAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountSingleTradeCurrency("BRL", paymentAccount); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), Objects.requireNonNull(paymentAccount.getCountry()).code); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_CITY), paymentAccount.getCity()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_CONTACT), paymentAccount.getContact()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EXTRA_INFO), paymentAccount.getExtraInfo()); print(paymentAccount); } @Test public void testCreateFasterPaymentsAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, FASTER_PAYMENTS_ID); verifyEmptyForm(emptyForm, FASTER_PAYMENTS_ID, PROPERTY_NAME_ACCOUNT_NR, PROPERTY_NAME_SORT_CODE); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, FASTER_PAYMENTS_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Faster Payments Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "9999 8888 7777"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SORT_CODE, "3127"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Faster Payments Acct Salt")); String jsonString = getCompletedFormAsJsonString(); FasterPaymentsAccount paymentAccount = (FasterPaymentsAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountSingleTradeCurrency("GBP", paymentAccount); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SORT_CODE), paymentAccount.getSortCode()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); print(paymentAccount); } @Test public void testCreateHalCashAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, HAL_CASH_ID); verifyEmptyForm(emptyForm, HAL_CASH_ID, PROPERTY_NAME_MOBILE_NR); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, HAL_CASH_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Hal Cash Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_MOBILE_NR, "798 123 456"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); String jsonString = getCompletedFormAsJsonString(); HalCashAccount paymentAccount = (HalCashAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountSingleTradeCurrency(EUR, paymentAccount); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_MOBILE_NR), paymentAccount.getMobileNr()); print(paymentAccount); } @Test public void testCreateInteracETransferAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, INTERAC_E_TRANSFER_ID); verifyEmptyForm(emptyForm, INTERAC_E_TRANSFER_ID, PROPERTY_NAME_HOLDER_NAME, PROPERTY_NAME_EMAIL, PROPERTY_NAME_QUESTION, PROPERTY_NAME_ANSWER); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, INTERAC_E_TRANSFER_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Interac Transfer Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "John Doe"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "john@doe.info"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_QUESTION, "What is my dog's name?"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ANSWER, "Fido"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Interac Transfer Acct Salt")); String jsonString = getCompletedFormAsJsonString(); InteracETransferAccount paymentAccount = (InteracETransferAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountSingleTradeCurrency("CAD", paymentAccount); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_QUESTION), paymentAccount.getQuestion()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ANSWER), paymentAccount.getAnswer()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); print(paymentAccount); } @Test public void testCreateJapanBankAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, JAPAN_BANK_ID); verifyEmptyForm(emptyForm, JAPAN_BANK_ID, PROPERTY_NAME_BANK_NAME, PROPERTY_NAME_BANK_CODE, PROPERTY_NAME_BANK_BRANCH_CODE, PROPERTY_NAME_BANK_BRANCH_NAME, PROPERTY_NAME_BANK_ACCOUNT_NAME, PROPERTY_NAME_BANK_ACCOUNT_TYPE, PROPERTY_NAME_BANK_ACCOUNT_NUMBER); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, JAPAN_BANK_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Fukuoka Account"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "Bank of Kyoto"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_CODE, "FKBKJPJT"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_BRANCH_CODE, "8100-8727"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_BRANCH_NAME, "Fukuoka Branch"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_NAME, "Fukuoka Account"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_TYPE, "Yen Account"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_NUMBER, "8100-8727-0000"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); String jsonString = getCompletedFormAsJsonString(); JapanBankAccount paymentAccount = (JapanBankAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountSingleTradeCurrency("JPY", paymentAccount); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_CODE), paymentAccount.getBankCode()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), paymentAccount.getBankName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_BRANCH_CODE), paymentAccount.getBankBranchCode()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_BRANCH_NAME), paymentAccount.getBankBranchName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ACCOUNT_NAME), paymentAccount.getBankAccountName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ACCOUNT_TYPE), paymentAccount.getBankAccountType()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ACCOUNT_NUMBER), paymentAccount.getBankAccountNumber()); print(paymentAccount); } @Test public void testCreateMoneyBeamAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, MONEY_BEAM_ID); verifyEmptyForm(emptyForm, MONEY_BEAM_ID, PROPERTY_NAME_ACCOUNT_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, MONEY_BEAM_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Money Beam Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_ID, "MB 0000 1111"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Money Beam Acct Salt")); String jsonString = getCompletedFormAsJsonString(); MoneyBeamAccount paymentAccount = (MoneyBeamAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountSingleTradeCurrency(EUR, paymentAccount); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); print(paymentAccount); } @Test public void testCreateMoneyGramAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, MONEY_GRAM_ID); verifyEmptyForm(emptyForm, MONEY_GRAM_ID, PROPERTY_NAME_HOLDER_NAME, PROPERTY_NAME_EMAIL, PROPERTY_NAME_COUNTRY, PROPERTY_NAME_STATE); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, MONEY_GRAM_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Money Gram Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, MoneyGramAccount.SUPPORTED_CURRENCIES .stream() .map(TradeCurrency::getCode) .collect(Collectors.joining(","))); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "INR"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "John Doe"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "john@doe.info"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "US"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_STATE, "NY"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); String jsonString = getCompletedFormAsJsonString(); MoneyGramAccount paymentAccount = (MoneyGramAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountTradeCurrencies(MoneyGramAccount.SUPPORTED_CURRENCIES, paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY), paymentAccount.getSelectedTradeCurrency().getCode()); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getFullName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), Objects.requireNonNull(paymentAccount.getCountry()).code); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_STATE), paymentAccount.getState()); print(paymentAccount); } @Test public void testCreatePerfectMoneyAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, PERFECT_MONEY_ID); verifyEmptyForm(emptyForm, PERFECT_MONEY_ID, PROPERTY_NAME_ACCOUNT_NR); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, PERFECT_MONEY_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Perfect Money Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "PM 0000 1111"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Perfect Money Acct Salt")); String jsonString = getCompletedFormAsJsonString(); PerfectMoneyAccount paymentAccount = (PerfectMoneyAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountSingleTradeCurrency(USD, paymentAccount); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); print(paymentAccount); } @Test public void testCreatePaxumAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, PAXUM_ID); verifyEmptyForm(emptyForm, PAXUM_ID, PROPERTY_NAME_EMAIL); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, PAXUM_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Paxum Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, PaxumAccount.SUPPORTED_CURRENCIES .stream() .map(TradeCurrency::getCode) .collect(Collectors.joining(","))); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "SEK"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.net"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); String jsonString = getCompletedFormAsJsonString(); PaxumAccount paymentAccount = (PaxumAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountTradeCurrencies(PaxumAccount.SUPPORTED_CURRENCIES, paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY), paymentAccount.getSelectedTradeCurrency().getCode()); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail()); print(paymentAccount); } @Test public void testCreatePayseraAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, PAYSERA_ID); verifyEmptyForm(emptyForm, PAYSERA_ID, PROPERTY_NAME_EMAIL); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, PAYSERA_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Paysera Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, PayseraAccount.SUPPORTED_CURRENCIES .stream() .map(TradeCurrency::getCode) .collect(Collectors.joining(","))); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "ZAR"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.net"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); String jsonString = getCompletedFormAsJsonString(); PayseraAccount paymentAccount = (PayseraAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountTradeCurrencies(PayseraAccount.SUPPORTED_CURRENCIES, paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY), paymentAccount.getSelectedTradeCurrency().getCode()); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail()); print(paymentAccount); } @Test public void testCreatePopmoneyAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, POPMONEY_ID); verifyEmptyForm(emptyForm, POPMONEY_ID, PROPERTY_NAME_ACCOUNT_ID, PROPERTY_NAME_HOLDER_NAME); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, POPMONEY_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Pop Money Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_ID, "POPMONEY 0000 1111"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); String jsonString = getCompletedFormAsJsonString(); PopmoneyAccount paymentAccount = (PopmoneyAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountSingleTradeCurrency(USD, paymentAccount); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); print(paymentAccount); } @Test public void testCreatePromptPayAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, PROMPT_PAY_ID); verifyEmptyForm(emptyForm, PROMPT_PAY_ID, PROPERTY_NAME_PROMPT_PAY_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, PROMPT_PAY_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Prompt Pay Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PROMPT_PAY_ID, "PP 0000 1111"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Prompt Pay Acct Salt")); String jsonString = getCompletedFormAsJsonString(); PromptPayAccount paymentAccount = (PromptPayAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountSingleTradeCurrency("THB", paymentAccount); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_PROMPT_PAY_ID), paymentAccount.getPromptPayId()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); print(paymentAccount); } @Test public void testCreateRevolutAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, REVOLUT_ID); verifyEmptyForm(emptyForm, REVOLUT_ID, PROPERTY_NAME_USERNAME); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, REVOLUT_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Revolut Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, RevolutAccount.SUPPORTED_CURRENCIES .stream() .map(TradeCurrency::getCode) .collect(Collectors.joining(","))); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "QAR"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_USERNAME, "revolut123"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); String jsonString = getCompletedFormAsJsonString(); RevolutAccount paymentAccount = (RevolutAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountTradeCurrencies(RevolutAccount.SUPPORTED_CURRENCIES, paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY), paymentAccount.getSelectedTradeCurrency().getCode()); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_USERNAME), paymentAccount.getUsername()); print(paymentAccount); } @Test public void testCreateSameBankAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, SAME_BANK_ID); verifyEmptyForm(emptyForm, SAME_BANK_ID, PROPERTY_NAME_ACCOUNT_NR, PROPERTY_NAME_ACCOUNT_TYPE, PROPERTY_NAME_BANK_NAME, PROPERTY_NAME_BRANCH_ID, PROPERTY_NAME_COUNTRY, PROPERTY_NAME_HOLDER_NAME, PROPERTY_NAME_HOLDER_TAX_ID, PROPERTY_NAME_NATIONAL_ACCOUNT_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SAME_BANK_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Same Bank Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "000 1 4567"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_TYPE, "Checking"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "HSBC"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BRANCH_ID, "111"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "GB"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Same Bank Acct Salt")); String jsonString = getCompletedFormAsJsonString(); SameBankAccount paymentAccount = (SameBankAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountSingleTradeCurrency("GBP", paymentAccount); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), Objects.requireNonNull(paymentAccount.getCountry()).code); SameBankAccountPayload payload = (SameBankAccountPayload) paymentAccount.getPaymentAccountPayload(); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_TYPE), payload.getAccountType()); // The bankId == bankName because bank id is not required in the UK. assertEquals(payload.getBankId(), payload.getBankName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); print(paymentAccount); } @Test public void testCreateSepaInstantAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, SEPA_INSTANT_ID); verifyEmptyForm(emptyForm, SEPA_INSTANT_ID, PROPERTY_NAME_COUNTRY, PROPERTY_NAME_HOLDER_NAME, PROPERTY_NAME_IBAN, PROPERTY_NAME_BIC); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SEPA_INSTANT_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Conta Sepa Instant"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "PT"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jose da Silva"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_IBAN, "909-909"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BIC, "909"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); String jsonString = getCompletedFormAsJsonString(); SepaInstantAccount paymentAccount = (SepaInstantAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), Objects.requireNonNull(paymentAccount.getCountry()).code); verifyAccountSingleTradeCurrency(EUR, paymentAccount); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_IBAN), paymentAccount.getIban()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBic()); // bankId == bic assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBankId()); print(paymentAccount); } @Test public void testCreateSepaAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, SEPA_ID); verifyEmptyForm(emptyForm, SEPA_ID, PROPERTY_NAME_COUNTRY, PROPERTY_NAME_HOLDER_NAME, PROPERTY_NAME_IBAN, PROPERTY_NAME_BIC); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SEPA_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Conta Sepa"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "PT"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jose da Silva"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_IBAN, "909-909"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BIC, "909"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Conta Sepa Salt")); String jsonString = getCompletedFormAsJsonString(); SepaAccount paymentAccount = (SepaAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), Objects.requireNonNull(paymentAccount.getCountry()).code); verifyAccountSingleTradeCurrency(EUR, paymentAccount); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_IBAN), paymentAccount.getIban()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBic()); // bankId == bic assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBankId()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); print(paymentAccount); } @Test public void testCreateSpecificBanksAccount(TestInfo testInfo) { // TODO Supporting set of accepted banks may require some refactoring // of the SpecificBanksAccount and SpecificBanksAccountPayload classes, i.e., // public void setAcceptedBanks(String... bankNames) { ... } File emptyForm = getEmptyForm(testInfo, SPECIFIC_BANKS_ID); verifyEmptyForm(emptyForm, SPECIFIC_BANKS_ID, PROPERTY_NAME_ACCOUNT_NR, PROPERTY_NAME_ACCOUNT_TYPE, PROPERTY_NAME_BANK_NAME, PROPERTY_NAME_BRANCH_ID, PROPERTY_NAME_COUNTRY, PROPERTY_NAME_HOLDER_NAME, PROPERTY_NAME_HOLDER_TAX_ID, PROPERTY_NAME_NATIONAL_ACCOUNT_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SPECIFIC_BANKS_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Specific Banks Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "000 1 4567"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_TYPE, "Checking"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "HSBC"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BRANCH_ID, "111"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "GB"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); String jsonString = getCompletedFormAsJsonString(); SpecificBanksAccount paymentAccount = (SpecificBanksAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountSingleTradeCurrency("GBP", paymentAccount); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), Objects.requireNonNull(paymentAccount.getCountry()).code); SpecificBanksAccountPayload payload = (SpecificBanksAccountPayload) paymentAccount.getPaymentAccountPayload(); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_TYPE), payload.getAccountType()); // The bankId == bankName because bank id is not required in the UK. assertEquals(payload.getBankId(), payload.getBankName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId()); print(paymentAccount); } @Test public void testCreateSwiftAccount(TestInfo testInfo) { // https://www.theswiftcodes.com File emptyForm = getEmptyForm(testInfo, SWIFT_ID); verifyEmptyForm(emptyForm, SWIFT_ID, PROPERTY_NAME_BANK_SWIFT_CODE); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SWIFT_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "IT Swift Acct w/ DE Intermediary"); Collection swiftCurrenciesSortedByCode = getAllSortedTraditionalCurrencies(comparing(TradeCurrency::getCode)); String allTraditionalCodes = getCommaDelimitedTraditionalCurrencyCodes(swiftCurrenciesSortedByCode); COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, allTraditionalCodes); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, EUR); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_SWIFT_CODE, "PASCITMMFIR"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_COUNTRY_CODE, "IT"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "BANCA MONTE DEI PASCHI DI SIENA S.P.A."); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_BRANCH, "SUCC. DI FIRENZE"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ADDRESS, "Via dei Pecori, 8, 50123 Firenze FI, Italy"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BENEFICIARY_NAME, "Vito de' Medici"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BENEFICIARY_ACCOUNT_NR, "0000 1111 2222 3333"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BENEFICIARY_ADDRESS, "Via dei Pecori, 1, 50123 Firenze FI, Italy"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BENEFICIARY_CITY, "Firenze"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_BENEFICIARY_PHONE, "+39 055 222222"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SPECIAL_INSTRUCTIONS, "N/A"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_INTERMEDIARY_SWIFT_CODE, "DEUTDEFFXXX"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_INTERMEDIARY_COUNTRY_CODE, "DE"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_INTERMEDIARY_NAME, "Kosmo Krump"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_INTERMEDIARY_ADDRESS, "TAUNUSANLAGE 12, FRANKFURT AM MAIN, 60262"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_INTERMEDIARY_BRANCH, "Deutsche Bank Frankfurt F"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Swift Acct Salt")); String jsonString = getCompletedFormAsJsonString(getSwiftFormComments()); SwiftAccount paymentAccount = (SwiftAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountTradeCurrencies(swiftCurrenciesSortedByCode, paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY), paymentAccount.getSelectedTradeCurrency().getCode()); verifyCommonFormEntries(paymentAccount); SwiftAccountPayload payload = (SwiftAccountPayload) paymentAccount.getPaymentAccountPayload(); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_SWIFT_CODE), payload.getBankSwiftCode()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_COUNTRY_CODE), payload.getBankCountryCode()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_BRANCH), payload.getBankBranch()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ADDRESS), payload.getBankAddress()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BENEFICIARY_NAME), payload.getBeneficiaryName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BENEFICIARY_ACCOUNT_NR), payload.getBeneficiaryAccountNr()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BENEFICIARY_ADDRESS), payload.getBeneficiaryAddress()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BENEFICIARY_CITY), payload.getBeneficiaryCity()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BENEFICIARY_PHONE), payload.getBeneficiaryPhone()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SPECIAL_INSTRUCTIONS), payload.getSpecialInstructions()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_INTERMEDIARY_SWIFT_CODE), payload.getIntermediarySwiftCode()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_INTERMEDIARY_COUNTRY_CODE), payload.getIntermediaryCountryCode()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_INTERMEDIARY_NAME), payload.getIntermediaryName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_INTERMEDIARY_BRANCH), payload.getIntermediaryBranch()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_INTERMEDIARY_ADDRESS), payload.getIntermediaryAddress()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); print(paymentAccount); } @Test public void testCreateSwishAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, SWISH_ID); verifyEmptyForm(emptyForm, SWISH_ID, PROPERTY_NAME_MOBILE_NR, PROPERTY_NAME_HOLDER_NAME); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SWISH_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Swish Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_MOBILE_NR, "+46 7 6060 0101"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Swish Acct Holder"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Swish Acct Salt")); String jsonString = getCompletedFormAsJsonString(); SwishAccount paymentAccount = (SwishAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountSingleTradeCurrency("SEK", paymentAccount); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_MOBILE_NR), paymentAccount.getMobileNr()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); print(paymentAccount); } @Test public void testCreateTransferwiseAccountWith1TradeCurrency(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, TRANSFERWISE_ID); verifyEmptyForm(emptyForm, TRANSFERWISE_ID, PROPERTY_NAME_EMAIL); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, TRANSFERWISE_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Transferwise Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, "NZD"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "NZD"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); String jsonString = getCompletedFormAsJsonString(); TransferwiseAccount paymentAccount = (TransferwiseAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); assertEquals(1, paymentAccount.getTradeCurrencies().size()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY), paymentAccount.getSelectedTradeCurrency().getCode()); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail()); print(paymentAccount); } @Test public void testCreateTransferwiseAccountWith10TradeCurrencies(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, TRANSFERWISE_ID); verifyEmptyForm(emptyForm, TRANSFERWISE_ID, PROPERTY_NAME_EMAIL); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, TRANSFERWISE_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Transferwise Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, "ARS,CAD,HRK,CZK,EUR,HKD,IDR,JPY,CHF,NZD"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "CHF"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); String jsonString = getCompletedFormAsJsonString(); TransferwiseAccount paymentAccount = (TransferwiseAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); assertEquals(10, paymentAccount.getTradeCurrencies().size()); List expectedTradeCurrencies = new ArrayList<>() {{ add(getTradeCurrency("ARS").get()); // 1st in list = selected ccy add(getTradeCurrency("CAD").get()); add(getTradeCurrency("CZK").get()); add(getTradeCurrency(EUR).get()); add(getTradeCurrency("HKD").get()); add(getTradeCurrency("IDR").get()); add(getTradeCurrency("JPY").get()); add(getTradeCurrency("CHF").get()); add(getTradeCurrency("NZD").get()); }}; verifyAccountTradeCurrencies(expectedTradeCurrencies, paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY), paymentAccount.getSelectedTradeCurrency().getCode()); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail()); print(paymentAccount); } @Test public void testCreateTransferwiseAccountWithSupportedTradeCurrencies(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, TRANSFERWISE_ID); verifyEmptyForm(emptyForm, TRANSFERWISE_ID, PROPERTY_NAME_EMAIL); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, TRANSFERWISE_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Transferwise Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, TransferwiseAccount.SUPPORTED_CURRENCIES .stream() .map(TradeCurrency::getCode) .collect(Collectors.joining(","))); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "AUD"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); String jsonString = getCompletedFormAsJsonString(); TransferwiseAccount paymentAccount = (TransferwiseAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountTradeCurrencies(TransferwiseAccount.SUPPORTED_CURRENCIES, paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY), paymentAccount.getSelectedTradeCurrency().getCode()); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail()); print(paymentAccount); } @Test public void testCreateTransferwiseAccountWithInvalidBrlTradeCurrencyShouldThrowException(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, TRANSFERWISE_ID); verifyEmptyForm(emptyForm, TRANSFERWISE_ID, PROPERTY_NAME_EMAIL); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, TRANSFERWISE_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Transferwise Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, "eur, hkd, idr, jpy, chf, nzd, brl, gbp"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); String jsonString = getCompletedFormAsJsonString(); Throwable exception = assertThrows(StatusRuntimeException.class, () -> createPaymentAccount(aliceClient, jsonString)); assertEquals("INVALID_ARGUMENT: BRL is not a member of valid currencies list", exception.getMessage()); } @Test public void testCreateTransferwiseAccountWithoutTradeCurrenciesShouldThrowException(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, TRANSFERWISE_ID); verifyEmptyForm(emptyForm, TRANSFERWISE_ID, PROPERTY_NAME_EMAIL); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, TRANSFERWISE_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Transferwise Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, ""); COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); String jsonString = getCompletedFormAsJsonString(); Throwable exception = assertThrows(StatusRuntimeException.class, () -> createPaymentAccount(aliceClient, jsonString)); assertEquals("INVALID_ARGUMENT: no trade currency defined for transferwise payment account", exception.getMessage()); } @Test public void testCreateUpholdAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, UPHOLD_ID); verifyEmptyForm(emptyForm, UPHOLD_ID, PROPERTY_NAME_ACCOUNT_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, UPHOLD_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Uphold Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_ID, "UA 9876"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_TRADE_CURRENCIES, UpholdAccount.SUPPORTED_CURRENCIES .stream() .map(TradeCurrency::getCode) .collect(Collectors.joining(","))); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SELECTED_TRADE_CURRENCY, "MXN"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Uphold Acct Salt")); String jsonString = getCompletedFormAsJsonString(); UpholdAccount paymentAccount = (UpholdAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountTradeCurrencies(UpholdAccount.SUPPORTED_CURRENCIES, paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY), paymentAccount.getSelectedTradeCurrency().getCode()); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); print(paymentAccount); } @Test public void testCreateUSPostalMoneyOrderAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, US_POSTAL_MONEY_ORDER_ID); verifyEmptyForm(emptyForm, US_POSTAL_MONEY_ORDER_ID, PROPERTY_NAME_HOLDER_NAME, PROPERTY_NAME_POSTAL_ADDRESS); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, US_POSTAL_MONEY_ORDER_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Bubba's Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Bubba"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_POSTAL_ADDRESS, "000 Westwood Terrace Austin, TX 78700"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); String jsonString = getCompletedFormAsJsonString(); USPostalMoneyOrderAccount paymentAccount = (USPostalMoneyOrderAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountSingleTradeCurrency(USD, paymentAccount); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_POSTAL_ADDRESS), paymentAccount.getPostalAddress()); print(paymentAccount); } @Test public void testCreateWeChatPayAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, WECHAT_PAY_ID); verifyEmptyForm(emptyForm, WECHAT_PAY_ID, PROPERTY_NAME_ACCOUNT_NR); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, WECHAT_PAY_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "WeChat Pay Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "WC 1234"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored WeChat Pay Acct Salt")); String jsonString = getCompletedFormAsJsonString(); WeChatPayAccount paymentAccount = (WeChatPayAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountSingleTradeCurrency("CNY", paymentAccount); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex()); print(paymentAccount); } @Test public void testCreateWesternUnionAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, WESTERN_UNION_ID); verifyEmptyForm(emptyForm, WESTERN_UNION_ID, PROPERTY_NAME_HOLDER_NAME, PROPERTY_NAME_CITY, PROPERTY_NAME_STATE, PROPERTY_NAME_COUNTRY, PROPERTY_NAME_EMAIL); COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, WESTERN_UNION_ID); COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Western Union Acct"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_CITY, "Fargo"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_STATE, "North Dakota"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "US"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info"); COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); String jsonString = getCompletedFormAsJsonString(); WesternUnionAccount paymentAccount = (WesternUnionAccount) createPaymentAccount(aliceClient, jsonString); verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); verifyAccountSingleTradeCurrency(USD, paymentAccount); verifyCommonFormEntries(paymentAccount); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getFullName()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_CITY), paymentAccount.getCity()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_STATE), paymentAccount.getState()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail()); assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY), Objects.requireNonNull(paymentAccount.getCountry()).code); print(paymentAccount); } @AfterAll public static void tearDown() { tearDownScaffold(); } private void print(PaymentAccount paymentAccount) { if (log.isDebugEnabled()) { log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount); log.debug("\n{}", new TableBuilder(PAYMENT_ACCOUNT_TBL, paymentAccount.toProtoMessage()).build()); } } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/payment/GetPaymentMethodsTest.java ================================================ package haveno.apitest.method.payment; import haveno.apitest.method.MethodTest; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import protobuf.PaymentMethod; import java.util.List; import java.util.stream.Collectors; import static haveno.apitest.Scaffold.BitcoinCoreApp.bitcoind; import static haveno.apitest.config.HavenoAppConfig.alicedaemon; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class GetPaymentMethodsTest extends MethodTest { @BeforeAll public static void setUp() { try { setUpScaffold(bitcoind, alicedaemon); } catch (Exception ex) { fail(ex); } } @Test @Order(1) public void testGetPaymentMethods() { List paymentMethodIds = aliceClient.getPaymentMethods() .stream() .map(PaymentMethod::getId) .collect(Collectors.toList()); assertTrue(paymentMethodIds.size() >= 20); } @AfterAll public static void tearDown() { tearDownScaffold(); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/trade/AbstractTradeTest.java ================================================ package haveno.apitest.method.trade; import haveno.apitest.method.offer.AbstractOfferTest; import haveno.cli.CliMain; import haveno.cli.GrpcClient; import haveno.cli.table.builder.TableBuilder; import haveno.proto.grpc.TradeInfo; import lombok.Getter; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.TestInfo; import org.slf4j.Logger; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import static haveno.cli.table.builder.TableType.TRADE_DETAIL_TBL; import static haveno.core.trade.Trade.Phase.DEPOSITS_UNLOCKED; import static haveno.core.trade.Trade.Phase.PAYMENT_SENT; import static haveno.core.trade.Trade.State.BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG; import static haveno.core.trade.Trade.State.BUYER_SENT_PAYMENT_SENT_MSG; import static haveno.core.trade.Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN; import static java.lang.String.format; import static java.lang.System.out; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; public class AbstractTradeTest extends AbstractOfferTest { public static final ExpectedProtocolStatus EXPECTED_PROTOCOL_STATUS = new ExpectedProtocolStatus(); // A Trade ID cache for use in @Test sequences. @Getter protected static String tradeId; protected final Supplier maxTradeStateAndPhaseChecks = () -> isLongRunningTest ? 10 : 2; protected final Function toTradeDetailTable = (trade) -> new TableBuilder(TRADE_DETAIL_TBL, trade).build().toString(); protected final Function toUserName = (client) -> client.equals(aliceClient) ? "Alice" : "Bob"; @BeforeAll public static void initStaticFixtures() { EXPECTED_PROTOCOL_STATUS.init(); } protected final TradeInfo takeAlicesOffer(String offerId, String paymentAccountId) { return takeAlicesOffer(offerId, paymentAccountId, true); } protected final TradeInfo takeAlicesOffer(String offerId, String paymentAccountId, boolean generateBtcBlock) { @SuppressWarnings("ConstantConditions") var trade = bobClient.takeOffer(offerId, paymentAccountId); assertNotNull(trade); assertEquals(offerId, trade.getTradeId()); // Cache the trade id for the other tests. tradeId = trade.getTradeId(); if (generateBtcBlock) genBtcBlocksThenWait(1, 6_000); return trade; } protected final void waitForDepositUnlocked(Logger log, TestInfo testInfo, GrpcClient grpcClient, String tradeId) { Predicate isTradeInDepositUnlockedStateAndPhase = (t) -> t.getState().equals(DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN.name()) && t.getPhase().equals(DEPOSITS_UNLOCKED.name()); String userName = toUserName.apply(grpcClient); for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { TradeInfo trade = grpcClient.getTrade(tradeId); if (!isTradeInDepositUnlockedStateAndPhase.test(trade)) { log.warn("{} still waiting on trade {} tx {}: DEPOSIT_UNLOCKED_IN_BLOCK_CHAIN, attempt # {}", userName, trade.getShortId(), trade.getMakerDepositTxId(), trade.getTakerDepositTxId(), i); genBtcBlocksThenWait(1, 4_000); } else { EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN) .setPhase(DEPOSITS_UNLOCKED) .setDepositPublished(true) .setDepositConfirmed(true); verifyExpectedProtocolStatus(trade); logTrade(log, testInfo, userName + "'s view after deposit is confirmed", trade); break; } } } protected final void verifyTakerDepositConfirmed(TradeInfo trade) { if (!trade.getIsDepositsUnlocked()) { fail(format("INVALID_PHASE for trade %s in STATE=%s PHASE=%s, deposit tx never unlocked.", trade.getShortId(), trade.getState(), trade.getPhase())); } } protected final void waitForBuyerSeesPaymentInitiatedMessage(Logger log, TestInfo testInfo, GrpcClient grpcClient, String tradeId) { String userName = toUserName.apply(grpcClient); for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { TradeInfo trade = grpcClient.getTrade(tradeId); if (!trade.getIsPaymentSent()) { log.warn("{} still waiting for trade {} {}, attempt # {}", userName, trade.getShortId(), BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG, i); sleep(5_000); } else { // Do not check trade.getOffer().getState() here because // it might be AVAILABLE, not OFFER_FEE_RESERVED. EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG) .setPhase(PAYMENT_SENT) .setPaymentSentMessageSent(true); verifyExpectedProtocolStatus(trade); logTrade(log, testInfo, userName + "'s view after confirming trade payment sent", trade); break; } } } protected final void waitForSellerSeesPaymentInitiatedMessage(Logger log, TestInfo testInfo, GrpcClient grpcClient, String tradeId) { Predicate isTradeInPaymentReceiptConfirmedStateAndPhase = (t) -> t.getState().equals(BUYER_SENT_PAYMENT_SENT_MSG.name()) && t.getPhase().equals(PAYMENT_SENT.name()); String userName = toUserName.apply(grpcClient); for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { TradeInfo trade = grpcClient.getTrade(tradeId); if (!isTradeInPaymentReceiptConfirmedStateAndPhase.test(trade)) { log.warn("INVALID_PHASE for {}'s trade {} in STATE={} PHASE={}, cannot confirm payment received yet.", userName, trade.getShortId(), trade.getState(), trade.getPhase()); sleep(10_000); } else { break; } } TradeInfo trade = grpcClient.getTrade(tradeId); if (!isTradeInPaymentReceiptConfirmedStateAndPhase.test(trade)) { fail(format("INVALID_PHASE for %s's trade %s in STATE=%s PHASE=%s, cannot confirm payment received.", userName, trade.getShortId(), trade.getState(), trade.getPhase())); } } protected final void verifyExpectedProtocolStatus(TradeInfo trade) { assertNotNull(trade); assertEquals(EXPECTED_PROTOCOL_STATUS.state.name(), trade.getState()); assertEquals(EXPECTED_PROTOCOL_STATUS.phase.name(), trade.getPhase()); if (!isLongRunningTest) assertEquals(EXPECTED_PROTOCOL_STATUS.isDepositPublished, trade.getIsDepositsPublished()); assertEquals(EXPECTED_PROTOCOL_STATUS.isDepositConfirmed, trade.getIsDepositsUnlocked()); assertEquals(EXPECTED_PROTOCOL_STATUS.isPaymentSentMessageSent, trade.getIsPaymentSent()); assertEquals(EXPECTED_PROTOCOL_STATUS.isPaymentReceivedMessageSent, trade.getIsPaymentReceived()); assertEquals(EXPECTED_PROTOCOL_STATUS.isPayoutPublished, trade.getIsPayoutPublished()); assertEquals(EXPECTED_PROTOCOL_STATUS.isCompleted, trade.getIsCompleted()); } protected final void logBalances(Logger log, TestInfo testInfo) { var alicesBalances = aliceClient.getBalances(); log.debug("{} Alice's Current Balances:\n{}", testName(testInfo), formatBalancesTbls(alicesBalances)); var bobsBalances = bobClient.getBalances(); log.debug("{} Bob's Current Balances:\n{}", testName(testInfo), formatBalancesTbls(bobsBalances)); } protected final void logTrade(Logger log, TestInfo testInfo, String description, TradeInfo trade) { if (log.isDebugEnabled()) { log.debug(format("%s %s%n%s", testName(testInfo), description, new TableBuilder(TRADE_DETAIL_TBL, trade).build())); } } protected static void runCliGetTrade(String tradeId) { out.println("Alice's CLI 'gettrade' response:"); CliMain.main(new String[]{"--password=xyz", "--port=9998", "gettrade", "--trade-id=" + tradeId}); out.println("Bob's CLI 'gettrade' response:"); CliMain.main(new String[]{"--password=xyz", "--port=9999", "gettrade", "--trade-id=" + tradeId}); } protected static void runCliGetOpenTrades() { out.println("Alice's CLI 'gettrades --category=open' response:"); CliMain.main(new String[]{"--password=xyz", "--port=9998", "gettrades", "--category=open"}); out.println("Bob's CLI 'gettrades --category=open' response:"); CliMain.main(new String[]{"--password=xyz", "--port=9999", "gettrades", "--category=open"}); } protected static void runCliGetClosedTrades() { out.println("Alice's CLI 'gettrades --category=closed' response:"); CliMain.main(new String[]{"--password=xyz", "--port=9998", "gettrades", "--category=closed"}); out.println("Bob's CLI 'gettrades --category=closed' response:"); CliMain.main(new String[]{"--password=xyz", "--port=9999", "gettrades", "--category=closed"}); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/trade/ExpectedProtocolStatus.java ================================================ package haveno.apitest.method.trade; import haveno.core.trade.Trade; /** * A test fixture encapsulating expected trade protocol status. * Status flags should be cleared via init() before starting a new trade protocol. */ public class ExpectedProtocolStatus { Trade.State state; Trade.Phase phase; boolean isDepositPublished; boolean isDepositConfirmed; boolean isPaymentSentMessageSent; boolean isPaymentReceivedMessageSent; boolean isPayoutPublished; boolean isCompleted; public ExpectedProtocolStatus setState(Trade.State state) { this.state = state; return this; } public ExpectedProtocolStatus setPhase(Trade.Phase phase) { this.phase = phase; return this; } public ExpectedProtocolStatus setDepositPublished(boolean depositPublished) { isDepositPublished = depositPublished; return this; } public ExpectedProtocolStatus setDepositConfirmed(boolean depositConfirmed) { isDepositConfirmed = depositConfirmed; return this; } public ExpectedProtocolStatus setPaymentSentMessageSent(boolean paymentSentMessageSent) { isPaymentSentMessageSent = paymentSentMessageSent; return this; } public ExpectedProtocolStatus setPaymentReceivedMessageSent(boolean paymentReceivedMessageSent) { isPaymentReceivedMessageSent = paymentReceivedMessageSent; return this; } public ExpectedProtocolStatus setPayoutPublished(boolean payoutPublished) { isPayoutPublished = payoutPublished; return this; } public ExpectedProtocolStatus setCompleted(boolean completed) { isCompleted = completed; return this; } public void init() { state = null; phase = null; isDepositPublished = false; isDepositConfirmed = false; isPaymentSentMessageSent = false; isPaymentReceivedMessageSent = false; isPayoutPublished = false; isCompleted = false; } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferTest.java ================================================ /* * 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 . */ package haveno.apitest.method.trade; import haveno.core.payment.PaymentAccount; import io.grpc.StatusRuntimeException; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; import static haveno.apitest.config.ApiTestConfig.USD; import static haveno.core.trade.Trade.Phase.PAYMENT_RECEIVED; import static haveno.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; import static protobuf.OfferDirection.BUY; import static protobuf.OpenOffer.State.AVAILABLE; @Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class TakeBuyBTCOfferTest extends AbstractTradeTest { // Alice is maker/buyer, Bob is taker/seller. @Test @Order(1) public void testTakeAlicesBuyOffer(final TestInfo testInfo) { try { PaymentAccount alicesUsdAccount = createDummyF2FAccount(aliceClient, "US"); var alicesOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(), USD, 12_500_000L, 12_500_000L, // min-amount = amount 0.00, defaultSecurityDepositPct.get(), alicesUsdAccount.getId(), NO_TRIGGER_PRICE); var offerId = alicesOffer.getId(); // Wait for Alice's AddToOfferBook task. // Wait times vary; my logs show >= 2-second delay. sleep(3_000); // TODO loop instead of hard code a wait time var alicesUsdOffers = aliceClient.getMyOffersSortedByDate(BUY.name(), USD); assertEquals(1, alicesUsdOffers.size()); PaymentAccount bobsUsdAccount = createDummyF2FAccount(bobClient, "US"); var trade = takeAlicesOffer(offerId, bobsUsdAccount.getId(), false); sleep(2_500); // Allow available offer to be removed from offer book. alicesUsdOffers = aliceClient.getMyOffersSortedByDate(BUY.name(), USD); assertEquals(0, alicesUsdOffers.size()); genBtcBlocksThenWait(1, 2_500); waitForDepositUnlocked(log, testInfo, bobClient, trade.getTradeId()); trade = bobClient.getTrade(tradeId); verifyTakerDepositConfirmed(trade); logTrade(log, testInfo, "Alice's Maker/Buyer View", aliceClient.getTrade(tradeId)); logTrade(log, testInfo, "Bob's Taker/Seller View", bobClient.getTrade(tradeId)); } catch (StatusRuntimeException e) { fail(e); } } @Test @Order(2) public void testAlicesConfirmPaymentSent(final TestInfo testInfo) { try { var trade = aliceClient.getTrade(tradeId); waitForDepositUnlocked(log, testInfo, aliceClient, trade.getTradeId()); aliceClient.confirmPaymentSent(trade.getTradeId()); sleep(6_000); waitForBuyerSeesPaymentInitiatedMessage(log, testInfo, aliceClient, tradeId); } catch (StatusRuntimeException e) { fail(e); } } @Test @Order(3) public void testBobsConfirmPaymentReceived(final TestInfo testInfo) { try { waitForSellerSeesPaymentInitiatedMessage(log, testInfo, bobClient, tradeId); var trade = bobClient.getTrade(tradeId); bobClient.confirmPaymentReceived(trade.getTradeId()); sleep(3_000); trade = bobClient.getTrade(tradeId); // Note: offer.state == available assertEquals(AVAILABLE.name(), trade.getOffer().getState()); EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG) .setPhase(PAYMENT_RECEIVED) .setPayoutPublished(true) .setPaymentReceivedMessageSent(true); verifyExpectedProtocolStatus(trade); logTrade(log, testInfo, "Bob's view after confirming fiat payment received", trade); } catch (StatusRuntimeException e) { fail(e); } } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java ================================================ /* * 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 . */ /* * 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 . */ package haveno.apitest.method.trade; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.NationalBankAccountPayload; import io.grpc.StatusRuntimeException; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; import static haveno.core.trade.Trade.Phase.PAYMENT_RECEIVED; import static haveno.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static protobuf.Offer.State.OFFER_FEE_RESERVED; import static protobuf.OfferDirection.BUY; import static protobuf.OpenOffer.State.AVAILABLE; /** * Test case verifies trade can be made with national bank payment method, * and json contracts exclude bank acct details until deposit tx is confirmed. */ @SuppressWarnings("ConstantConditions") @Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class TakeBuyBTCOfferWithNationalBankAcctTest extends AbstractTradeTest { // Alice is maker/buyer, Bob is taker/seller. private static final String BRL = "BRL"; private static PaymentAccount alicesPaymentAccount; private static PaymentAccount bobsPaymentAccount; @BeforeAll public static void setUp() { setUp(false); } @Test @Order(1) public void testTakeAlicesBuyOffer(final TestInfo testInfo) { try { alicesPaymentAccount = createDummyBRLAccount(aliceClient, "Alicia da Silva", String.valueOf(System.currentTimeMillis()), "123.456.789-01"); bobsPaymentAccount = createDummyBRLAccount(bobClient, "Roberto da Silva", String.valueOf(System.currentTimeMillis()), "123.456.789-02"); var alicesOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(), BRL, 1_000_000L, 1_000_000L, // min-amount = amount 0.00, defaultSecurityDepositPct.get(), alicesPaymentAccount.getId(), NO_TRIGGER_PRICE); var offerId = alicesOffer.getId(); // Wait for Alice's AddToOfferBook task. // Wait times vary; my logs show >= 2 second delay. sleep(3_000); // TODO loop instead of hard code wait time var alicesOffers = aliceClient.getMyOffersSortedByDate(BUY.name(), BRL); assertEquals(1, alicesOffers.size()); var trade = takeAlicesOffer(offerId, bobsPaymentAccount.getId(), false); // Before generating a blk and confirming deposit tx, make sure there // are no bank acct details in the either side's contract. while (true) { try { var alicesContract = aliceClient.getTrade(trade.getTradeId()).getContractAsJson(); var bobsContract = bobClient.getTrade(trade.getTradeId()).getContractAsJson(); verifyJsonContractExcludesBankAccountDetails(alicesContract, alicesPaymentAccount); verifyJsonContractExcludesBankAccountDetails(alicesContract, bobsPaymentAccount); verifyJsonContractExcludesBankAccountDetails(bobsContract, alicesPaymentAccount); verifyJsonContractExcludesBankAccountDetails(bobsContract, bobsPaymentAccount); break; } catch (StatusRuntimeException ex) { if (ex.getMessage() == null) { String message = ex.getMessage().replaceFirst("^[A-Z_]+: ", ""); if (message.contains("trade") && message.contains("not found")) { fail(ex); } } else { sleep(500); } } } genBtcBlocksThenWait(1, 4000); alicesOffers = aliceClient.getMyOffersSortedByDate(BUY.name(), BRL); assertEquals(0, alicesOffers.size()); genBtcBlocksThenWait(1, 2_500); waitForDepositUnlocked(log, testInfo, bobClient, trade.getTradeId()); trade = bobClient.getTrade(tradeId); verifyTakerDepositConfirmed(trade); logTrade(log, testInfo, "Alice's Maker/Buyer View", aliceClient.getTrade(tradeId)); logTrade(log, testInfo, "Bob's Taker/Seller View", bobClient.getTrade(tradeId)); } catch (StatusRuntimeException e) { fail(e); } } @Test @Order(2) public void testBankAcctDetailsIncludedInContracts(final TestInfo testInfo) { assertNotNull(alicesPaymentAccount); assertNotNull(bobsPaymentAccount); var alicesTrade = aliceClient.getTrade(tradeId); assertNotEquals(null, alicesTrade.getContract().getMakerPaymentAccountPayload()); assertNotEquals(null, alicesTrade.getContract().getTakerPaymentAccountPayload()); var alicesContractJson = alicesTrade.getContractAsJson(); verifyJsonContractIncludesBankAccountDetails(alicesContractJson, alicesPaymentAccount); verifyJsonContractIncludesBankAccountDetails(alicesContractJson, bobsPaymentAccount); var bobsTrade = bobClient.getTrade(tradeId); assertNotEquals(null, bobsTrade.getContract().getMakerPaymentAccountPayload()); assertNotEquals(null, bobsTrade.getContract().getTakerPaymentAccountPayload()); var bobsContractJson = bobsTrade.getContractAsJson(); verifyJsonContractIncludesBankAccountDetails(bobsContractJson, alicesPaymentAccount); verifyJsonContractIncludesBankAccountDetails(bobsContractJson, bobsPaymentAccount); } @Test @Order(3) public void testAlicesConfirmPaymentSent(final TestInfo testInfo) { try { var trade = aliceClient.getTrade(tradeId); waitForDepositUnlocked(log, testInfo, aliceClient, trade.getTradeId()); aliceClient.confirmPaymentSent(trade.getTradeId()); sleep(6_000); waitForBuyerSeesPaymentInitiatedMessage(log, testInfo, aliceClient, tradeId); trade = aliceClient.getTrade(tradeId); assertEquals(OFFER_FEE_RESERVED.name(), trade.getOffer().getState()); logTrade(log, testInfo, "Alice's Maker/Buyer View (Payment Sent)", aliceClient.getTrade(tradeId)); logTrade(log, testInfo, "Bob's Taker/Seller View (Payment Sent)", bobClient.getTrade(tradeId)); } catch (StatusRuntimeException e) { fail(e); } } @Test @Order(4) public void testBobsConfirmPaymentReceived(final TestInfo testInfo) { try { waitForSellerSeesPaymentInitiatedMessage(log, testInfo, bobClient, tradeId); var trade = bobClient.getTrade(tradeId); bobClient.confirmPaymentReceived(trade.getTradeId()); sleep(3_000); trade = bobClient.getTrade(tradeId); // Note: offer.state == available assertEquals(AVAILABLE.name(), trade.getOffer().getState()); EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG) .setPhase(PAYMENT_RECEIVED) .setPayoutPublished(true) .setPaymentReceivedMessageSent(true); verifyExpectedProtocolStatus(trade); logTrade(log, testInfo, "Bob's view after confirming fiat payment received", trade); } catch (StatusRuntimeException e) { fail(e); } } private void verifyJsonContractExcludesBankAccountDetails(String jsonContract, PaymentAccount paymentAccount) { NationalBankAccountPayload nationalBankAccountPayload = (NationalBankAccountPayload) paymentAccount.getPaymentAccountPayload(); // The client cannot know exactly when payment acct payloads are added to a contract, // so auto-failing here results in a flaky test. // assertFalse(jsonContract.contains(nationalBankAccountPayload.getNationalAccountId())); // assertFalse(jsonContract.contains(nationalBankAccountPayload.getBranchId())); // assertFalse(jsonContract.contains(nationalBankAccountPayload.getAccountNr())); // assertFalse(jsonContract.contains(nationalBankAccountPayload.getHolderName())); // assertFalse(jsonContract.contains(nationalBankAccountPayload.getHolderTaxId())); // Log warning if bank acct details are found in json contract. if (jsonContract.contains(nationalBankAccountPayload.getNationalAccountId())) log.warn("Could not check json contract soon enough; it contains national bank acct id"); if (jsonContract.contains(nationalBankAccountPayload.getBranchId())) log.warn("Could not check json contract soon enough; it contains natl bank branch id"); if (jsonContract.contains(nationalBankAccountPayload.getAccountNr())) log.warn("Could not check json contract soon enough; it contains natl bank acct #"); if (jsonContract.contains(nationalBankAccountPayload.getHolderName())) log.warn("Could not check json contract soon enough; it contains natl bank acct holder name"); if (jsonContract.contains(nationalBankAccountPayload.getHolderTaxId())) log.warn("Could not check json contract soon enough; it contains natl bank acct holder tax id"); } private void verifyJsonContractIncludesBankAccountDetails(String jsonContract, PaymentAccount paymentAccount) { NationalBankAccountPayload nationalBankAccountPayload = (NationalBankAccountPayload) paymentAccount.getPaymentAccountPayload(); assertTrue(jsonContract.contains(nationalBankAccountPayload.getNationalAccountId())); assertTrue(jsonContract.contains(nationalBankAccountPayload.getBranchId())); assertTrue(jsonContract.contains(nationalBankAccountPayload.getAccountNr())); assertTrue(jsonContract.contains(nationalBankAccountPayload.getHolderName())); assertTrue(jsonContract.contains(nationalBankAccountPayload.getHolderTaxId())); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/trade/TakeBuyXMROfferTest.java ================================================ /* * 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 . */ package haveno.apitest.method.trade; import haveno.apitest.method.offer.AbstractOfferTest; import haveno.cli.table.builder.TableBuilder; import io.grpc.StatusRuntimeException; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; import static haveno.apitest.config.ApiTestConfig.XMR; import static haveno.cli.table.builder.TableType.OFFER_TBL; import static haveno.core.trade.Trade.Phase.PAYMENT_RECEIVED; import static haveno.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; import static protobuf.Offer.State.OFFER_FEE_RESERVED; import static protobuf.OfferDirection.SELL; @Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class TakeBuyXMROfferTest extends AbstractTradeTest { // Alice is maker / xmr buyer (btc seller), Bob is taker / xmr seller (btc buyer). @BeforeAll public static void setUp() { AbstractOfferTest.setUp(); createXmrPaymentAccounts(); EXPECTED_PROTOCOL_STATUS.init(); } @Test @Order(1) public void testTakeAlicesSellBTCForXMROffer(final TestInfo testInfo) { try { // Alice is going to BUY XMR, but the Offer direction = SELL because it is a // BTC trade; Alice will SELL BTC for XMR. Bob will send Alice XMR. // Confused me, but just need to remember there are only BTC offers. var btcTradeDirection = SELL.name(); var alicesOffer = aliceClient.createFixedPricedOffer(btcTradeDirection, XMR, 15_000_000L, 7_500_000L, "0.00455500", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR defaultSecurityDepositPct.get(), alicesXmrAcct.getId()); log.debug("Alice's BUY XMR (SELL BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build()); genBtcBlocksThenWait(1, 5000); var offerId = alicesOffer.getId(); var alicesXmrOffers = aliceClient.getMyOffers(btcTradeDirection, XMR); assertEquals(1, alicesXmrOffers.size()); var trade = takeAlicesOffer(offerId, bobsXmrAcct.getId()); alicesXmrOffers = aliceClient.getMyOffersSortedByDate(XMR); assertEquals(0, alicesXmrOffers.size()); genBtcBlocksThenWait(1, 2_500); waitForDepositUnlocked(log, testInfo, bobClient, trade.getTradeId()); trade = bobClient.getTrade(tradeId); verifyTakerDepositConfirmed(trade); logTrade(log, testInfo, "Alice's Maker/Buyer View", aliceClient.getTrade(tradeId)); logTrade(log, testInfo, "Bob's Taker/Seller View", bobClient.getTrade(tradeId)); } catch (StatusRuntimeException e) { fail(e); } } @Test @Order(2) public void testBobsConfirmPaymentSent(final TestInfo testInfo) { try { var trade = bobClient.getTrade(tradeId); verifyTakerDepositConfirmed(trade); log.debug("Bob sends XMR payment to Alice for trade {}", trade.getTradeId()); bobClient.confirmPaymentSent(trade.getTradeId()); sleep(3500); waitForBuyerSeesPaymentInitiatedMessage(log, testInfo, bobClient, tradeId); logTrade(log, testInfo, "Alice's Maker/Buyer View (Payment Sent)", aliceClient.getTrade(tradeId)); logTrade(log, testInfo, "Bob's Taker/Seller View (Payment Sent)", bobClient.getTrade(tradeId)); } catch (StatusRuntimeException e) { fail(e); } } @Test @Order(3) public void testAlicesConfirmPaymentReceived(final TestInfo testInfo) { try { waitForSellerSeesPaymentInitiatedMessage(log, testInfo, aliceClient, tradeId); sleep(2_000); var trade = aliceClient.getTrade(tradeId); // If we were trading BSQ, Alice would verify payment has been sent to her // Haveno wallet, but we can do no such checks for XMR payments. // All XMR transfers are done outside Haveno. log.debug("Alice verifies XMR payment was received from Bob, for trade {}", trade.getTradeId()); aliceClient.confirmPaymentReceived(trade.getTradeId()); sleep(3_000); trade = aliceClient.getTrade(tradeId); assertEquals(OFFER_FEE_RESERVED.name(), trade.getOffer().getState()); EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG) .setPhase(PAYMENT_RECEIVED) .setPayoutPublished(true) .setPaymentReceivedMessageSent(true); verifyExpectedProtocolStatus(trade); logTrade(log, testInfo, "Alice's Maker/Buyer View (Payment Received)", aliceClient.getTrade(tradeId)); logTrade(log, testInfo, "Bob's Taker/Seller View (Payment Received)", bobClient.getTrade(tradeId)); } catch (StatusRuntimeException e) { fail(e); } } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/trade/TakeSellBTCOfferTest.java ================================================ /* * 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 . */ package haveno.apitest.method.trade; import haveno.core.payment.PaymentAccount; import io.grpc.StatusRuntimeException; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; import static haveno.apitest.config.ApiTestConfig.BTC; import static haveno.apitest.config.ApiTestConfig.USD; import static haveno.core.trade.Trade.Phase.PAYMENT_RECEIVED; import static haveno.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; import static protobuf.Offer.State.OFFER_FEE_RESERVED; import static protobuf.OfferDirection.SELL; @Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class TakeSellBTCOfferTest extends AbstractTradeTest { // Alice is maker/seller, Bob is taker/buyer. // Maker and Taker fees are in BTC. private static final String TRADE_FEE_CURRENCY_CODE = BTC; private static final String WITHDRAWAL_TX_MEMO = "Bob's trade withdrawal"; @Test @Order(1) public void testTakeAlicesSellOffer(final TestInfo testInfo) { try { PaymentAccount alicesUsdAccount = createDummyF2FAccount(aliceClient, "US"); var alicesOffer = aliceClient.createMarketBasedPricedOffer(SELL.name(), USD, 12_500_000L, 12_500_000L, // min-amount = amount 0.00, defaultSecurityDepositPct.get(), alicesUsdAccount.getId(), NO_TRIGGER_PRICE); var offerId = alicesOffer.getId(); // Wait for Alice's AddToOfferBook task. // Wait times vary; my logs show >= 2-second delay, but taking sell offers // seems to require more time to prepare. sleep(3_000); // TODO loop instead of hard code a wait time var alicesUsdOffers = aliceClient.getMyOffersSortedByDate(SELL.name(), USD); assertEquals(1, alicesUsdOffers.size()); PaymentAccount bobsUsdAccount = createDummyF2FAccount(bobClient, "US"); var trade = takeAlicesOffer(offerId, bobsUsdAccount.getId(), false); sleep(2_500); // Allow available offer to be removed from offer book. var takeableUsdOffers = bobClient.getOffersSortedByDate(SELL.name(), USD); assertEquals(0, takeableUsdOffers.size()); genBtcBlocksThenWait(1, 2_500); waitForDepositUnlocked(log, testInfo, bobClient, trade.getTradeId()); trade = bobClient.getTrade(tradeId); verifyTakerDepositConfirmed(trade); logTrade(log, testInfo, "Alice's Maker/Buyer View", aliceClient.getTrade(tradeId)); logTrade(log, testInfo, "Bob's Taker/Seller View", bobClient.getTrade(tradeId)); } catch (StatusRuntimeException e) { fail(e); } } @Test @Order(2) public void testBobsConfirmPaymentSent(final TestInfo testInfo) { try { var trade = bobClient.getTrade(tradeId); verifyTakerDepositConfirmed(trade); bobClient.confirmPaymentSent(tradeId); sleep(6_000); waitForBuyerSeesPaymentInitiatedMessage(log, testInfo, bobClient, tradeId); } catch (StatusRuntimeException e) { fail(e); } } @Test @Order(3) public void testAlicesConfirmPaymentReceived(final TestInfo testInfo) { try { waitForSellerSeesPaymentInitiatedMessage(log, testInfo, aliceClient, tradeId); var trade = aliceClient.getTrade(tradeId); aliceClient.confirmPaymentReceived(trade.getTradeId()); sleep(3_000); trade = aliceClient.getTrade(tradeId); assertEquals(OFFER_FEE_RESERVED.name(), trade.getOffer().getState()); EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG) .setPhase(PAYMENT_RECEIVED) .setPayoutPublished(true) .setPaymentReceivedMessageSent(true); verifyExpectedProtocolStatus(trade); logTrade(log, testInfo, "Alice's view after confirming fiat payment received", trade); } catch (StatusRuntimeException e) { fail(e); } } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/trade/TakeSellXMROfferTest.java ================================================ /* * 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 . */ package haveno.apitest.method.trade; import haveno.apitest.method.offer.AbstractOfferTest; import haveno.cli.table.builder.TableBuilder; import io.grpc.StatusRuntimeException; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; import static haveno.apitest.config.ApiTestConfig.BTC; import static haveno.apitest.config.ApiTestConfig.XMR; import static haveno.cli.table.builder.TableType.OFFER_TBL; import static haveno.core.trade.Trade.Phase.PAYMENT_RECEIVED; import static haveno.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; import static protobuf.OfferDirection.BUY; @Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class TakeSellXMROfferTest extends AbstractTradeTest { // Alice is maker / xmr seller (btc buyer), Bob is taker / xmr buyer (btc seller). // Maker and Taker fees are in BTC. private static final String TRADE_FEE_CURRENCY_CODE = BTC; private static final String WITHDRAWAL_TX_MEMO = "Bob's trade withdrawal"; @BeforeAll public static void setUp() { AbstractOfferTest.setUp(); createXmrPaymentAccounts(); EXPECTED_PROTOCOL_STATUS.init(); } @Test @Order(1) public void testTakeAlicesBuyBTCForXMROffer(final TestInfo testInfo) { try { // Alice is going to SELL XMR, but the Offer direction = BUY because it is a // BTC trade; Alice will BUY BTC for XMR. Alice will send Bob XMR. // Confused me, but just need to remember there are only BTC offers. var btcTradeDirection = BUY.name(); double priceMarginPctInput = 1.50; var alicesOffer = aliceClient.createMarketBasedPricedOffer(btcTradeDirection, XMR, 20_000_000L, 10_500_000L, priceMarginPctInput, defaultSecurityDepositPct.get(), alicesXmrAcct.getId(), NO_TRIGGER_PRICE); log.debug("Alice's SELL XMR (BUY BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build()); genBtcBlocksThenWait(1, 4000); var offerId = alicesOffer.getId(); var alicesXmrOffers = aliceClient.getMyOffers(btcTradeDirection, XMR); assertEquals(1, alicesXmrOffers.size()); var trade = takeAlicesOffer(offerId, bobsXmrAcct.getId()); alicesXmrOffers = aliceClient.getMyOffersSortedByDate(XMR); assertEquals(0, alicesXmrOffers.size()); genBtcBlocksThenWait(1, 2_500); waitForDepositUnlocked(log, testInfo, bobClient, trade.getTradeId()); trade = bobClient.getTrade(tradeId); verifyTakerDepositConfirmed(trade); logTrade(log, testInfo, "Alice's Maker/Seller View", aliceClient.getTrade(tradeId)); logTrade(log, testInfo, "Bob's Taker/Buyer View", bobClient.getTrade(tradeId)); } catch (StatusRuntimeException e) { fail(e); } } @Test @Order(2) public void testAlicesConfirmPaymentSent(final TestInfo testInfo) { try { var trade = aliceClient.getTrade(tradeId); waitForDepositUnlocked(log, testInfo, aliceClient, trade.getTradeId()); log.debug("Alice sends XMR payment to Bob for trade {}", trade.getTradeId()); aliceClient.confirmPaymentSent(trade.getTradeId()); sleep(3500); waitForBuyerSeesPaymentInitiatedMessage(log, testInfo, aliceClient, tradeId); logTrade(log, testInfo, "Alice's Maker/Seller View (Payment Sent)", aliceClient.getTrade(tradeId)); logTrade(log, testInfo, "Bob's Taker/Buyer View (Payment Sent)", bobClient.getTrade(tradeId)); } catch (StatusRuntimeException e) { fail(e); } } @Test @Order(3) public void testBobsConfirmPaymentReceived(final TestInfo testInfo) { try { waitForSellerSeesPaymentInitiatedMessage(log, testInfo, bobClient, tradeId); var trade = bobClient.getTrade(tradeId); sleep(2_000); // If we were trading BTC, Bob would verify payment has been sent to his // Haveno wallet, but we can do no such checks for XMR payments. // All XMR transfers are done outside Haveno. log.debug("Bob verifies XMR payment was received from Alice, for trade {}", trade.getTradeId()); bobClient.confirmPaymentReceived(trade.getTradeId()); sleep(3_000); trade = bobClient.getTrade(tradeId); // Warning: trade.getOffer().getState() might be AVAILABLE, not OFFER_FEE_RESERVED. EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG) .setPhase(PAYMENT_RECEIVED) .setPayoutPublished(true) .setPaymentReceivedMessageSent(true); verifyExpectedProtocolStatus(trade); logTrade(log, testInfo, "Alice's Maker/Seller View (Payment Received)", aliceClient.getTrade(tradeId)); logTrade(log, testInfo, "Bob's Taker/Buyer View (Payment Received)", bobClient.getTrade(tradeId)); } catch (StatusRuntimeException e) { fail(e); } } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/wallet/BtcWalletTest.java ================================================ package haveno.apitest.method.wallet; import haveno.apitest.method.MethodTest; import haveno.cli.table.builder.TableBuilder; import haveno.proto.grpc.BtcBalanceInfo; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; import static haveno.apitest.Scaffold.BitcoinCoreApp.bitcoind; import static haveno.apitest.config.HavenoAppConfig.alicedaemon; import static haveno.apitest.config.HavenoAppConfig.bobdaemon; import static haveno.apitest.config.HavenoAppConfig.seednode; import static haveno.apitest.method.wallet.WalletTestUtil.INITIAL_BTC_BALANCES; import static haveno.apitest.method.wallet.WalletTestUtil.verifyBtcBalances; import static haveno.cli.table.builder.TableType.ADDRESS_BALANCE_TBL; import static haveno.cli.table.builder.TableType.BTC_BALANCE_TBL; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; @Disabled @Slf4j @TestMethodOrder(OrderAnnotation.class) public class BtcWalletTest extends MethodTest { private static final String TX_MEMO = "tx memo"; @BeforeAll public static void setUp() { startSupportingApps(false, true, bitcoind, seednode, alicedaemon, bobdaemon); } @Test @Order(1) public void testInitialBtcBalances(final TestInfo testInfo) { // Bob & Alice's regtest Haveno wallets were initialized with 10 BTC. BtcBalanceInfo alicesBalances = aliceClient.getBtcBalances(); log.debug("{} Alice's BTC Balances:\n{}", testName(testInfo), new TableBuilder(BTC_BALANCE_TBL, alicesBalances).build()); BtcBalanceInfo bobsBalances = bobClient.getBtcBalances(); log.debug("{} Bob's BTC Balances:\n{}", testName(testInfo), new TableBuilder(BTC_BALANCE_TBL, bobsBalances).build()); assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), alicesBalances.getAvailableBalance()); assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), bobsBalances.getAvailableBalance()); } @Test @Order(2) public void testFundAlicesBtcWallet(final TestInfo testInfo) { String newAddress = aliceClient.getUnusedBtcAddress(); bitcoinCli.sendToAddress(newAddress, "2.5"); genBtcBlocksThenWait(1, 1000); BtcBalanceInfo btcBalanceInfo = aliceClient.getBtcBalances(); // New balance is 12.5 BTC assertEquals(1250000000, btcBalanceInfo.getAvailableBalance()); log.debug("{} -> Alice's Funded Address Balance -> \n{}", testName(testInfo), new TableBuilder(ADDRESS_BALANCE_TBL, aliceClient.getAddressBalance(newAddress))); // New balance is 12.5 BTC btcBalanceInfo = aliceClient.getBtcBalances(); haveno.core.api.model.BtcBalanceInfo alicesExpectedBalances = haveno.core.api.model.BtcBalanceInfo.valueOf(1250000000, 0, 1250000000, 0); verifyBtcBalances(alicesExpectedBalances, btcBalanceInfo); log.debug("{} -> Alice's BTC Balances After Sending 2.5 BTC -> \n{}", testName(testInfo), new TableBuilder(BTC_BALANCE_TBL, btcBalanceInfo).build()); } @AfterAll public static void tearDown() { tearDownScaffold(); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/wallet/WalletProtectionTest.java ================================================ package haveno.apitest.method.wallet; import haveno.apitest.method.MethodTest; import io.grpc.StatusRuntimeException; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import static haveno.apitest.config.HavenoAppConfig.alicedaemon; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; @SuppressWarnings("ResultOfMethodCallIgnored") @Disabled @Slf4j @TestMethodOrder(OrderAnnotation.class) public class WalletProtectionTest extends MethodTest { @BeforeAll public static void setUp() { try { setUpScaffold(alicedaemon); MILLISECONDS.sleep(2000); } catch (Exception ex) { fail(ex); } } @Test @Order(1) public void testSetWalletPassword() { aliceClient.setWalletPassword("first-password"); } @Test @Order(2) public void testGetBalanceOnEncryptedWalletShouldThrowException() { Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances()); assertEquals("FAILED_PRECONDITION: wallet is locked", exception.getMessage()); } @Test @Order(3) public void testUnlockWalletFor4Seconds() { aliceClient.unlockWallet("first-password", 4); aliceClient.getBtcBalances(); // should not throw 'wallet locked' exception sleep(4500); // let unlock timeout expire Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances()); assertEquals("FAILED_PRECONDITION: wallet is locked", exception.getMessage()); } @Test @Order(4) public void testGetBalanceAfterUnlockTimeExpiryShouldThrowException() { aliceClient.unlockWallet("first-password", 3); sleep(4000); // let unlock timeout expire Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances()); assertEquals("FAILED_PRECONDITION: wallet is locked", exception.getMessage()); } @Test @Order(5) public void testLockWalletBeforeUnlockTimeoutExpiry() { aliceClient.unlockWallet("first-password", 60); aliceClient.lockWallet(); Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances()); assertEquals("FAILED_PRECONDITION: wallet is locked", exception.getMessage()); } @Test @Order(6) public void testLockWalletWhenWalletAlreadyLockedShouldThrowException() { Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.lockWallet()); assertEquals("ALREADY_EXISTS: wallet is already locked", exception.getMessage()); } @Test @Order(7) public void testUnlockWalletTimeoutOverride() { aliceClient.unlockWallet("first-password", 2); sleep(500); // override unlock timeout after 0.5s aliceClient.unlockWallet("first-password", 6); sleep(5000); aliceClient.getBtcBalances(); // getbalance 5s after overriding timeout to 6s } @Test @Order(8) public void testSetNewWalletPassword() { aliceClient.setWalletPassword("first-password", "second-password"); sleep(2500); // allow time for wallet save aliceClient.unlockWallet("second-password", 2); aliceClient.getBtcBalances(); } @Test @Order(9) public void testSetNewWalletPasswordWithIncorrectNewPasswordShouldThrowException() { Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.setWalletPassword("bad old password", "irrelevant")); assertEquals("INVALID_ARGUMENT: incorrect old password", exception.getMessage()); } @Test @Order(10) public void testRemoveNewWalletPassword() { aliceClient.removeWalletPassword("second-password"); aliceClient.getBtcBalances(); // should not throw 'wallet locked' exception } @AfterAll public static void tearDown() { tearDownScaffold(); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/method/wallet/WalletTestUtil.java ================================================ package haveno.apitest.method.wallet; import haveno.proto.grpc.BtcBalanceInfo; import lombok.extern.slf4j.Slf4j; import static org.junit.jupiter.api.Assertions.assertEquals; @Slf4j public class WalletTestUtil { // All api tests depend on the DAO / regtest environment, and Bob & Alice's wallets // are initialized with 10 BTC during the scaffolding setup. public static final haveno.core.api.model.BtcBalanceInfo INITIAL_BTC_BALANCES = haveno.core.api.model.BtcBalanceInfo.valueOf(1000000000, 0, 1000000000, 0); public static void verifyBtcBalances(haveno.core.api.model.BtcBalanceInfo expected, BtcBalanceInfo actual) { assertEquals(expected.getAvailableBalance(), actual.getAvailableBalance()); assertEquals(expected.getReservedBalance(), actual.getReservedBalance()); assertEquals(expected.getTotalAvailableBalance(), actual.getTotalAvailableBalance()); assertEquals(expected.getLockedBalance(), actual.getLockedBalance()); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/LongRunningOfferDeactivationTest.java ================================================ /* * 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 . */ package haveno.apitest.scenario; import haveno.apitest.method.offer.AbstractOfferTest; import haveno.core.payment.PaymentAccount; import haveno.proto.grpc.OfferInfo; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.condition.EnabledIf; import static java.lang.System.getenv; import static org.junit.jupiter.api.Assertions.fail; import static protobuf.OfferDirection.BUY; import static protobuf.OfferDirection.SELL; /** * Used to verify trigger based, automatic offer deactivation works. * Disabled by default. * Set ENV or IDE-ENV LONG_RUNNING_OFFER_DEACTIVATION_TEST_ENABLED=true to run. */ @EnabledIf("envLongRunningTestEnabled") @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class LongRunningOfferDeactivationTest extends AbstractOfferTest { private static final int MAX_ITERATIONS = 500; @Test @Order(1) public void testSellOfferAutoDisable(final TestInfo testInfo) { PaymentAccount paymentAcct = createDummyF2FAccount(aliceClient, "US"); double mktPriceAsDouble = aliceClient.getBtcPrice("USD"); String triggerPrice = calcPriceAsString(mktPriceAsDouble, -50.0000, 4); log.info("Current USD mkt price = {} Trigger Price = {}", mktPriceAsDouble, triggerPrice); OfferInfo offer = aliceClient.createMarketBasedPricedOffer(SELL.name(), "USD", 1_000_000, 1_000_000, 0.00, defaultSecurityDepositPct.get(), paymentAcct.getId(), triggerPrice); log.info("SELL offer {} created with margin based price {}.", offer.getId(), offer.getPrice()); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. offer = aliceClient.getOffer(offer.getId()); // Offer has trigger price now. log.info("SELL offer should be automatically disabled when mkt price falls below {}.", offer.getTriggerPrice()); int numIterations = 0; while (++numIterations < MAX_ITERATIONS) { offer = aliceClient.getOffer(offer.getId()); var mktPrice = aliceClient.getBtcPrice("USD"); if (offer.getIsActivated()) { log.info("Offer still enabled at mkt price {} > {} trigger price", mktPrice, offer.getTriggerPrice()); sleep(1000 * 60); // 60s } else { log.info("Successful test completion after offer disabled at mkt price {} < {} trigger price.", mktPrice, offer.getTriggerPrice()); break; } if (numIterations == MAX_ITERATIONS) fail("Offer never disabled"); genBtcBlocksThenWait(1, 0); } } @Test @Order(2) public void testBuyOfferAutoDisable(final TestInfo testInfo) { PaymentAccount paymentAcct = createDummyF2FAccount(aliceClient, "US"); double mktPriceAsDouble = aliceClient.getBtcPrice("USD"); String triggerPrice = calcPriceAsString(mktPriceAsDouble, 50.0000, 4); log.info("Current USD mkt price = {} Trigger Price = {}", mktPriceAsDouble, triggerPrice); OfferInfo offer = aliceClient.createMarketBasedPricedOffer(BUY.name(), "USD", 1_000_000, 1_000_000, 0.00, defaultSecurityDepositPct.get(), paymentAcct.getId(), triggerPrice); log.info("BUY offer {} created with margin based price {}.", offer.getId(), offer.getPrice()); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. offer = aliceClient.getOffer(offer.getId()); // Offer has trigger price now. log.info("BUY offer should be automatically disabled when mkt price rises above {}.", offer.getTriggerPrice()); int numIterations = 0; while (++numIterations < MAX_ITERATIONS) { offer = aliceClient.getOffer(offer.getId()); var mktPrice = aliceClient.getBtcPrice("USD"); if (offer.getIsActivated()) { log.info("Offer still enabled at mkt price {} < {} trigger price", mktPrice, offer.getTriggerPrice()); sleep(1000 * 60); // 60s } else { log.info("Successful test completion after offer disabled at mkt price {} > {} trigger price.", mktPrice, offer.getTriggerPrice()); break; } if (numIterations == MAX_ITERATIONS) fail("Offer never disabled"); genBtcBlocksThenWait(1, 0); } } protected static boolean envLongRunningTestEnabled() { String envName = "LONG_RUNNING_OFFER_DEACTIVATION_TEST_ENABLED"; String envX = getenv(envName); if (envX != null) { log.info("Enabled, found {}.", envName); return true; } else { log.info("Skipped, no environment variable {} defined.", envName); log.info("To enable on Mac OS or Linux:" + "\tIf running in terminal, export LONG_RUNNING_OFFER_DEACTIVATION_TEST_ENABLED=true in bash shell." + "\tIf running in Intellij, set LONG_RUNNING_OFFER_DEACTIVATION_TEST_ENABLED=true in launcher's Environment variables field."); return false; } } } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/LongRunningTradesTest.java ================================================ /* * 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 . */ package haveno.apitest.scenario; import haveno.apitest.method.trade.AbstractTradeTest; import haveno.apitest.method.trade.TakeBuyBTCOfferTest; import haveno.apitest.method.trade.TakeSellBTCOfferTest; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.condition.EnabledIf; import static java.lang.System.getenv; @EnabledIf("envLongRunningTestEnabled") @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class LongRunningTradesTest extends AbstractTradeTest { @Test @Order(1) public void TradeLoop(final TestInfo testInfo) { int numTrades = 0; while (numTrades < 50) { log.info("*******************************************************************"); log.info("Trade # {}", ++numTrades); log.info("*******************************************************************"); EXPECTED_PROTOCOL_STATUS.init(); testTakeBuyBTCOffer(testInfo); genBtcBlocksThenWait(1, 1000 * 15); log.info("*******************************************************************"); log.info("Trade # {}", ++numTrades); log.info("*******************************************************************"); EXPECTED_PROTOCOL_STATUS.init(); testTakeSellBTCOffer(testInfo); genBtcBlocksThenWait(1, 1000 * 15); } } public void testTakeBuyBTCOffer(final TestInfo testInfo) { TakeBuyBTCOfferTest test = new TakeBuyBTCOfferTest(); setLongRunningTest(true); test.testTakeAlicesBuyOffer(testInfo); test.testAlicesConfirmPaymentSent(testInfo); test.testBobsConfirmPaymentReceived(testInfo); } public void testTakeSellBTCOffer(final TestInfo testInfo) { TakeSellBTCOfferTest test = new TakeSellBTCOfferTest(); setLongRunningTest(true); test.testTakeAlicesSellOffer(testInfo); test.testBobsConfirmPaymentSent(testInfo); test.testAlicesConfirmPaymentReceived(testInfo); } protected static boolean envLongRunningTestEnabled() { String envName = "LONG_RUNNING_TRADES_TEST_ENABLED"; String envX = getenv(envName); if (envX != null) { log.info("Enabled, found {}.", envName); return true; } else { log.info("Skipped, no environment variable {} defined.", envName); log.info("To enable on Mac OS or Linux:" + "\tIf running in terminal, export LONG_RUNNING_TRADES_TEST_ENABLED=true in bash shell." + "\tIf running in Intellij, set LONG_RUNNING_TRADES_TEST_ENABLED=true in launcher's Environment variables field."); return false; } } } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/OfferTest.java ================================================ /* * 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 . */ package haveno.apitest.scenario; import haveno.apitest.method.offer.AbstractOfferTest; import haveno.apitest.method.offer.CancelOfferTest; import haveno.apitest.method.offer.CreateOfferUsingFixedPriceTest; import haveno.apitest.method.offer.CreateOfferUsingMarketPriceMarginTest; import haveno.apitest.method.offer.CreateXMROffersTest; import haveno.apitest.method.offer.ValidateCreateOfferTest; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class OfferTest extends AbstractOfferTest { @BeforeAll public static void setUp() { setUp(false); // Use setUp(true) for running API daemons in remote debug mode. } @Test @Order(1) public void testCreateOfferValidation() { ValidateCreateOfferTest test = new ValidateCreateOfferTest(); test.testAmtTooLargeShouldThrowException(); test.testNoMatchingEURPaymentAccountShouldThrowException(); test.testNoMatchingCADPaymentAccountShouldThrowException(); } @Test @Order(2) public void testCancelOffer() { CancelOfferTest test = new CancelOfferTest(); test.testCancelOffer(); } @Test @Order(3) public void testCreateOfferUsingFixedPrice() { CreateOfferUsingFixedPriceTest test = new CreateOfferUsingFixedPriceTest(); test.testCreateAUDBTCBuyOfferUsingFixedPrice16000(); test.testCreateUSDBTCBuyOfferUsingFixedPrice100001234(); test.testCreateEURBTCSellOfferUsingFixedPrice95001234(); } @Test @Order(4) public void testCreateOfferUsingMarketPriceMarginPct() { CreateOfferUsingMarketPriceMarginTest test = new CreateOfferUsingMarketPriceMarginTest(); test.testCreateUSDBTCBuyOffer5PctPriceMargin(); test.testCreateNZDBTCBuyOfferMinus2PctPriceMargin(); test.testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin(); test.testCreateBRLBTCSellOffer6Point55PctPriceMargin(); test.testCreateUSDBTCBuyOfferWithTriggerPrice(); } @Test @Order(6) public void testCreateXMROffers() { CreateXMROffersTest test = new CreateXMROffersTest(); CreateXMROffersTest.createXmrPaymentAccounts(); test.testCreateFixedPriceBuy1BTCFor200KXMROffer(); test.testCreateFixedPriceSell1BTCFor200KXMROffer(); test.testCreatePriceMarginBasedBuy1BTCOfferWithTriggerPrice(); test.testCreatePriceMarginBasedSell1BTCOffer(); test.testGetAllMyXMROffers(); test.testGetAvailableXMROffers(); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/PaymentAccountTest.java ================================================ package haveno.apitest.scenario; import haveno.apitest.method.payment.AbstractPaymentAccountTest; import haveno.apitest.method.payment.CreatePaymentAccountTest; import haveno.apitest.method.payment.GetPaymentMethodsTest; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; import static haveno.apitest.Scaffold.BitcoinCoreApp.bitcoind; import static haveno.apitest.config.HavenoAppConfig.alicedaemon; import static haveno.apitest.config.HavenoAppConfig.seednode; import static org.junit.jupiter.api.Assertions.fail; @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class PaymentAccountTest extends AbstractPaymentAccountTest { @BeforeAll public static void setUp() { try { setUpScaffold(bitcoind, seednode, alicedaemon); } catch (Exception ex) { fail(ex); } } @Test @Order(1) public void testGetPaymentMethods() { GetPaymentMethodsTest test = new GetPaymentMethodsTest(); test.testGetPaymentMethods(); } @Test @Order(2) public void testCreatePaymentAccount(TestInfo testInfo) { CreatePaymentAccountTest test = new CreatePaymentAccountTest(); test.testCreateAdvancedCashAccount(testInfo); test.testCreateAliPayAccount(testInfo); test.testCreateAustraliaPayidAccount(testInfo); test.testCreateCapitualAccount(testInfo); test.testCreateCashDepositAccount(testInfo); test.testCreateBrazilNationalBankAccount(testInfo); test.testCreateZelleAccount(testInfo); test.testCreateF2FAccount(testInfo); test.testCreateFasterPaymentsAccount(testInfo); test.testCreateHalCashAccount(testInfo); test.testCreateInteracETransferAccount(testInfo); test.testCreateJapanBankAccount(testInfo); test.testCreateMoneyBeamAccount(testInfo); test.testCreateMoneyGramAccount(testInfo); test.testCreatePerfectMoneyAccount(testInfo); test.testCreatePaxumAccount(testInfo); test.testCreatePayseraAccount(testInfo); test.testCreatePopmoneyAccount(testInfo); test.testCreatePromptPayAccount(testInfo); test.testCreateRevolutAccount(testInfo); test.testCreateSameBankAccount(testInfo); test.testCreateSepaInstantAccount(testInfo); test.testCreateSepaAccount(testInfo); test.testCreateSpecificBanksAccount(testInfo); test.testCreateSwiftAccount(testInfo); test.testCreateSwishAccount(testInfo); test.testCreateTransferwiseAccountWith1TradeCurrency(testInfo); test.testCreateTransferwiseAccountWith10TradeCurrencies(testInfo); test.testCreateTransferwiseAccountWithSupportedTradeCurrencies(testInfo); test.testCreateTransferwiseAccountWithInvalidBrlTradeCurrencyShouldThrowException(testInfo); test.testCreateTransferwiseAccountWithoutTradeCurrenciesShouldThrowException(testInfo); test.testCreateUpholdAccount(testInfo); test.testCreateUSPostalMoneyOrderAccount(testInfo); test.testCreateWeChatPayAccount(testInfo); test.testCreateWesternUnionAccount(testInfo); } @AfterAll public static void tearDown() { tearDownScaffold(); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/ScriptedBotTest.java ================================================ /* * 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 . */ package haveno.apitest.scenario; import haveno.apitest.config.ApiTestConfig; import haveno.apitest.method.BitcoinCliHelper; import haveno.apitest.scenario.bot.AbstractBotTest; import haveno.apitest.scenario.bot.BotClient; import haveno.apitest.scenario.bot.RobotBob; import haveno.apitest.scenario.bot.script.BashScriptGenerator; import haveno.apitest.scenario.bot.shutdown.ManualBotShutdownException; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.condition.EnabledIf; import static haveno.apitest.Scaffold.BitcoinCoreApp.bitcoind; import static haveno.apitest.config.HavenoAppConfig.alicedaemon; import static haveno.apitest.config.HavenoAppConfig.arbdaemon; import static haveno.apitest.config.HavenoAppConfig.bobdaemon; import static haveno.apitest.config.HavenoAppConfig.seednode; import static haveno.apitest.scenario.bot.shutdown.ManualShutdown.startShutdownTimer; import static org.junit.jupiter.api.Assertions.fail; // The test case is enabled if AbstractBotTest#botScriptExists() returns true. @EnabledIf("botScriptExists") @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class ScriptedBotTest extends AbstractBotTest { private RobotBob robotBob; @BeforeAll public static void startTestHarness() { botScript = deserializeBotScript(); if (botScript.isUseTestHarness()) { startSupportingApps(true, true, bitcoind, seednode, arbdaemon, alicedaemon, bobdaemon); } else { // We need just enough configurations to make sure Bob and testers use // the right apiPassword, to create a bitcoin-cli helper, and RobotBob's // gRPC stubs. But the user will have to register dispute agents before // an offer can be taken. config = new ApiTestConfig("--apiPassword", "xyz"); bitcoinCli = new BitcoinCliHelper(config); log.warn("Don't forget to register dispute agents before trying to trade with me."); } botClient = new BotClient(bobClient); } @BeforeEach public void initRobotBob() { try { BashScriptGenerator bashScriptGenerator = getBashScriptGenerator(); robotBob = new RobotBob(botClient, botScript, bitcoinCli, bashScriptGenerator); } catch (Exception ex) { fail(ex); } } @Test @Order(1) public void runRobotBob() { try { startShutdownTimer(); robotBob.run(); } catch (ManualBotShutdownException ex) { // This exception is thrown if a /tmp/bottest-shutdown file was found. // You can also kill -15 // of worker.org.gradle.process.internal.worker.GradleWorkerMain 'Gradle Test Executor #' // // This will cleanly shut everything down as well, but you will see a // Process 'Gradle Test Executor #' finished with non-zero exit value 143 error, // which you may think is a test failure. log.warn("{} Shutting down test case before test completion;" + " this is not a test failure.", ex.getMessage()); } catch (Throwable throwable) { fail(throwable); } } @AfterAll public static void tearDown() { if (botScript.isUseTestHarness()) tearDownScaffold(); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/StartupTest.java ================================================ /* * 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 . */ package haveno.apitest.scenario; import haveno.apitest.method.CallRateMeteringInterceptorTest; import haveno.apitest.method.GetMethodHelpTest; import haveno.apitest.method.GetVersionTest; import haveno.apitest.method.MethodTest; import haveno.apitest.method.RegisterDisputeAgentsTest; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import java.io.File; import java.io.IOException; import static haveno.apitest.Scaffold.BitcoinCoreApp.bitcoind; import static haveno.apitest.config.ApiTestRateMeterInterceptorConfig.getTestRateMeterInterceptorConfig; import static haveno.apitest.config.HavenoAppConfig.alicedaemon; import static haveno.apitest.config.HavenoAppConfig.arbdaemon; import static haveno.apitest.config.HavenoAppConfig.seednode; import static haveno.common.file.FileUtil.deleteFileIfExists; import static org.junit.jupiter.api.Assertions.fail; @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class StartupTest extends MethodTest { private static File callRateMeteringConfigFile; @BeforeAll public static void setUp() { try { callRateMeteringConfigFile = getTestRateMeterInterceptorConfig(); startSupportingApps(callRateMeteringConfigFile, false, false, bitcoind, seednode, arbdaemon, alicedaemon); } catch (Exception ex) { fail(ex); } } @Test @Order(1) public void testCallRateMeteringInterceptor() { CallRateMeteringInterceptorTest test = new CallRateMeteringInterceptorTest(); test.testGetVersionCall1IsAllowed(); test.sleep200Milliseconds(); test.testGetVersionCall2ShouldThrowException(); test.sleep200Milliseconds(); test.testGetVersionCall3ShouldThrowException(); test.sleep200Milliseconds(); test.testGetVersionCall4IsAllowed(); sleep(1000); // Wait 1 second before calling getversion in next test. } @Test @Order(2) public void testGetVersion() { GetVersionTest test = new GetVersionTest(); test.testGetVersion(); } @Test @Order(3) public void testRegisterDisputeAgents() { RegisterDisputeAgentsTest test = new RegisterDisputeAgentsTest(); test.testRegisterArbitratorShouldThrowException(); test.testInvalidDisputeAgentTypeArgShouldThrowException(); test.testInvalidRegistrationKeyArgShouldThrowException(); test.testRegisterMediator(); test.testRegisterRefundAgent(); } @Test @Order(4) public void testGetCreateOfferHelp() { GetMethodHelpTest test = new GetMethodHelpTest(); test.testGetCreateOfferHelp(); } @AfterAll public static void tearDown() { try { deleteFileIfExists(callRateMeteringConfigFile); } catch (IOException ex) { log.error(ex.getMessage()); } tearDownScaffold(); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/TradeTest.java ================================================ /* * 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 . */ package haveno.apitest.scenario; import haveno.apitest.method.trade.AbstractTradeTest; import haveno.apitest.method.trade.TakeBuyBTCOfferTest; import haveno.apitest.method.trade.TakeBuyBTCOfferWithNationalBankAcctTest; import haveno.apitest.method.trade.TakeBuyXMROfferTest; import haveno.apitest.method.trade.TakeSellBTCOfferTest; import haveno.apitest.method.trade.TakeSellXMROfferTest; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class TradeTest extends AbstractTradeTest { @BeforeEach public void init() { EXPECTED_PROTOCOL_STATUS.init(); } @Test @Order(1) public void testTakeBuyBTCOffer(final TestInfo testInfo) { TakeBuyBTCOfferTest test = new TakeBuyBTCOfferTest(); test.testTakeAlicesBuyOffer(testInfo); test.testAlicesConfirmPaymentSent(testInfo); test.testBobsConfirmPaymentReceived(testInfo); } @Test @Order(2) public void testTakeSellBTCOffer(final TestInfo testInfo) { TakeSellBTCOfferTest test = new TakeSellBTCOfferTest(); test.testTakeAlicesSellOffer(testInfo); test.testBobsConfirmPaymentSent(testInfo); test.testAlicesConfirmPaymentReceived(testInfo); } @Test @Order(4) public void testTakeBuyBTCOfferWithNationalBankAcct(final TestInfo testInfo) { TakeBuyBTCOfferWithNationalBankAcctTest test = new TakeBuyBTCOfferWithNationalBankAcctTest(); test.testTakeAlicesBuyOffer(testInfo); test.testBankAcctDetailsIncludedInContracts(testInfo); test.testAlicesConfirmPaymentSent(testInfo); test.testBobsConfirmPaymentReceived(testInfo); } @Test @Order(6) public void testTakeBuyXMROffer(final TestInfo testInfo) { TakeBuyXMROfferTest test = new TakeBuyXMROfferTest(); TakeBuyXMROfferTest.createXmrPaymentAccounts(); test.testTakeAlicesSellBTCForXMROffer(testInfo); test.testBobsConfirmPaymentSent(testInfo); test.testAlicesConfirmPaymentReceived(testInfo); } @Test @Order(7) public void testTakeSellXMROffer(final TestInfo testInfo) { TakeSellXMROfferTest test = new TakeSellXMROfferTest(); TakeBuyXMROfferTest.createXmrPaymentAccounts(); test.testTakeAlicesBuyBTCForXMROffer(testInfo); test.testAlicesConfirmPaymentSent(testInfo); test.testBobsConfirmPaymentReceived(testInfo); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/WalletTest.java ================================================ /* * 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 . */ package haveno.apitest.scenario; import haveno.apitest.method.MethodTest; import haveno.apitest.method.wallet.BtcWalletTest; import haveno.apitest.method.wallet.WalletProtectionTest; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; import static haveno.apitest.Scaffold.BitcoinCoreApp.bitcoind; import static haveno.apitest.config.HavenoAppConfig.alicedaemon; import static haveno.apitest.config.HavenoAppConfig.arbdaemon; import static haveno.apitest.config.HavenoAppConfig.bobdaemon; import static haveno.apitest.config.HavenoAppConfig.seednode; @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class WalletTest extends MethodTest { // Batching all wallet tests in this test case reduces scaffold setup // time. Here, we create a method WalletProtectionTest instance and run each // test in declared order. @BeforeAll public static void setUp() { startSupportingApps(true, false, bitcoind, seednode, arbdaemon, alicedaemon, bobdaemon); } @Test @Order(1) public void testBtcWalletFunding(final TestInfo testInfo) { BtcWalletTest btcWalletTest = new BtcWalletTest(); btcWalletTest.testInitialBtcBalances(testInfo); btcWalletTest.testFundAlicesBtcWallet(testInfo); } @Test @Order(3) public void testWalletProtection() { WalletProtectionTest walletProtectionTest = new WalletProtectionTest(); walletProtectionTest.testSetWalletPassword(); walletProtectionTest.testGetBalanceOnEncryptedWalletShouldThrowException(); walletProtectionTest.testUnlockWalletFor4Seconds(); walletProtectionTest.testGetBalanceAfterUnlockTimeExpiryShouldThrowException(); walletProtectionTest.testLockWalletBeforeUnlockTimeoutExpiry(); walletProtectionTest.testLockWalletWhenWalletAlreadyLockedShouldThrowException(); walletProtectionTest.testUnlockWalletTimeoutOverride(); walletProtectionTest.testSetNewWalletPassword(); walletProtectionTest.testSetNewWalletPasswordWithIncorrectNewPasswordShouldThrowException(); walletProtectionTest.testRemoveNewWalletPassword(); } @AfterAll public static void tearDown() { tearDownScaffold(); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/bot/AbstractBotTest.java ================================================ /* * 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 . */ package haveno.apitest.scenario.bot; import com.google.gson.GsonBuilder; import haveno.apitest.method.MethodTest; import haveno.apitest.scenario.bot.script.BashScriptGenerator; import haveno.apitest.scenario.bot.script.BotScript; import haveno.core.locale.Country; import lombok.extern.slf4j.Slf4j; import protobuf.PaymentAccount; import java.io.File; import java.io.IOException; import java.nio.file.Paths; import static haveno.core.locale.CountryUtil.findCountryByCode; import static haveno.core.payment.payload.PaymentMethod.ZELLE_ID; import static haveno.core.payment.payload.PaymentMethod.getPaymentMethod; import static java.lang.String.format; import static java.lang.System.getProperty; import static java.nio.file.Files.readAllBytes; @Slf4j public abstract class AbstractBotTest extends MethodTest { protected static final String BOT_SCRIPT_NAME = "bot-script.json"; protected static BotScript botScript; protected static BotClient botClient; protected BashScriptGenerator getBashScriptGenerator() { if (botScript.isUseTestHarness()) { PaymentAccount alicesAccount = createAlicesPaymentAccount(); botScript.setPaymentAccountIdForCliScripts(alicesAccount.getId()); } return new BashScriptGenerator(config.apiPassword, botScript.getApiPortForCliScripts(), botScript.getPaymentAccountIdForCliScripts(), botScript.isPrintCliScripts()); } private PaymentAccount createAlicesPaymentAccount() { BotPaymentAccountGenerator accountGenerator = new BotPaymentAccountGenerator(new BotClient(aliceClient)); String paymentMethodId = botScript.getBotPaymentMethodId(); if (paymentMethodId != null) { if (paymentMethodId.equals(ZELLE_ID)) { // Only Zelle test accts are supported now. return accountGenerator.createZellePaymentAccount( "Alice's Zelle Account", "Alice"); } else { throw new UnsupportedOperationException( format("This test harness bot does not work with %s payment accounts yet.", getPaymentMethod(paymentMethodId).getDisplayString())); } } else { String countryCode = botScript.getCountryCode(); Country country = findCountryByCode(countryCode).orElseThrow(() -> new IllegalArgumentException(countryCode + " is not a valid iso country code.")); return accountGenerator.createF2FPaymentAccount(country, "Alice's " + country.name + " F2F Account"); } } protected static BotScript deserializeBotScript() { try { File botScriptFile = new File(getProperty("java.io.tmpdir"), BOT_SCRIPT_NAME); String json = new String(readAllBytes(Paths.get(botScriptFile.getPath()))); return new GsonBuilder().setPrettyPrinting().create().fromJson(json, BotScript.class); } catch (IOException ex) { throw new IllegalStateException("Error reading script bot file contents.", ex); } } @SuppressWarnings("unused") // This is used by the jupiter framework. protected static boolean botScriptExists() { File botScriptFile = new File(getProperty("java.io.tmpdir"), BOT_SCRIPT_NAME); if (botScriptFile.exists()) { botScriptFile.deleteOnExit(); log.info("Enabled, found {}.", botScriptFile.getPath()); return true; } else { log.info("Skipped, no bot script.\n\tTo generate a bot-script.json file, see BotScriptGenerator."); return false; } } } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/bot/Bot.java ================================================ package haveno.apitest.scenario.bot; import haveno.apitest.method.BitcoinCliHelper; import haveno.apitest.scenario.bot.script.BashScriptGenerator; import haveno.apitest.scenario.bot.script.BotScript; import haveno.core.locale.Country; import lombok.extern.slf4j.Slf4j; import protobuf.PaymentAccount; import static haveno.core.locale.CountryUtil.findCountryByCode; import static haveno.core.payment.payload.PaymentMethod.ZELLE_ID; import static haveno.core.payment.payload.PaymentMethod.getPaymentMethod; import static java.lang.String.format; import static java.util.concurrent.TimeUnit.MINUTES; @Slf4j public class Bot { static final String MAKE = "MAKE"; static final String TAKE = "TAKE"; protected final BotClient botClient; protected final BitcoinCliHelper bitcoinCli; protected final BashScriptGenerator bashScriptGenerator; protected final String[] actions; protected final long protocolStepTimeLimitInMs; protected final boolean stayAlive; protected final boolean isUsingTestHarness; protected final PaymentAccount paymentAccount; public Bot(BotClient botClient, BotScript botScript, BitcoinCliHelper bitcoinCli, BashScriptGenerator bashScriptGenerator) { this.botClient = botClient; this.bitcoinCli = bitcoinCli; this.bashScriptGenerator = bashScriptGenerator; this.actions = botScript.getActions(); this.protocolStepTimeLimitInMs = MINUTES.toMillis(botScript.getProtocolStepTimeLimitInMinutes()); this.stayAlive = botScript.isStayAlive(); this.isUsingTestHarness = botScript.isUseTestHarness(); if (isUsingTestHarness) this.paymentAccount = createBotPaymentAccount(botScript); else this.paymentAccount = botClient.getPaymentAccount(botScript.getPaymentAccountIdForBot()); } private PaymentAccount createBotPaymentAccount(BotScript botScript) { BotPaymentAccountGenerator accountGenerator = new BotPaymentAccountGenerator(botClient); String paymentMethodId = botScript.getBotPaymentMethodId(); if (paymentMethodId != null) { if (paymentMethodId.equals(ZELLE_ID)) { return accountGenerator.createZellePaymentAccount("Bob's Zelle Account", "Bob"); } else { throw new UnsupportedOperationException( format("This bot test does not work with %s payment accounts yet.", getPaymentMethod(paymentMethodId).getDisplayString())); } } else { Country country = findCountry(botScript.getCountryCode()); return accountGenerator.createF2FPaymentAccount(country, country.name + " F2F Account"); } } private Country findCountry(String countryCode) { return findCountryByCode(countryCode).orElseThrow(() -> new IllegalArgumentException(countryCode + " is not a valid iso country code.")); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/bot/BotClient.java ================================================ /* * 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 . */ package haveno.apitest.scenario.bot; import haveno.cli.GrpcClient; import haveno.proto.grpc.BalancesInfo; import haveno.proto.grpc.GetPaymentAccountsRequest; import haveno.proto.grpc.OfferInfo; import haveno.proto.grpc.TradeInfo; import lombok.extern.slf4j.Slf4j; import protobuf.PaymentAccount; import java.text.DecimalFormat; import java.util.List; import java.util.function.BiPredicate; import static org.apache.commons.lang3.StringUtils.capitalize; /** * Convenience GrpcClient wrapper for bots using gRPC services. */ @SuppressWarnings({"unused"}) @Slf4j public class BotClient { private static final DecimalFormat FIXED_PRICE_FMT = new DecimalFormat("###########0"); private final GrpcClient grpcClient; public BotClient(GrpcClient grpcClient) { this.grpcClient = grpcClient; } /** * Returns current balance information. * @return BalancesInfo */ public BalancesInfo getBalance() { return grpcClient.getBalances(); } /** * Return the most recent BTC market price for the given currencyCode. * @param currencyCode * @return double */ public double getCurrentBTCMarketPrice(String currencyCode) { return grpcClient.getBtcPrice(currencyCode); } /** * Return the most recent BTC market price for the given currencyCode as an integer string. * @param currencyCode * @return String */ public String getCurrentBTCMarketPriceAsIntegerString(String currencyCode) { return FIXED_PRICE_FMT.format(getCurrentBTCMarketPrice(currencyCode)); } /** * Return all BUY and SELL offers for the given currencyCode. * @param currencyCode * @return List */ public List getOffers(String currencyCode) { var buyOffers = getBuyOffers(currencyCode); if (buyOffers.size() > 0) { return buyOffers; } else { return getSellOffers(currencyCode); } } /** * Return BUY offers for the given currencyCode. * @param currencyCode * @return List */ public List getBuyOffers(String currencyCode) { return grpcClient.getOffers("BUY", currencyCode); } /** * Return SELL offers for the given currencyCode. * @param currencyCode * @return List */ public List getSellOffers(String currencyCode) { return grpcClient.getOffers("SELL", currencyCode); } /** * Create and return a new Offer using a market based price. * @param paymentAccount * @param direction * @param currencyCode * @param amountInSatoshis * @param minAmountInSatoshis * @param priceMarginAsPercent * @param securityDepositAsPercent * @param feeCurrency * @param triggerPrice * @return OfferInfo */ public OfferInfo createOfferAtMarketBasedPrice(PaymentAccount paymentAccount, String direction, String currencyCode, long amountInSatoshis, long minAmountInSatoshis, double priceMarginAsPercent, double securityDepositAsPercent, String triggerPrice) { return grpcClient.createMarketBasedPricedOffer(direction, currencyCode, amountInSatoshis, minAmountInSatoshis, priceMarginAsPercent, securityDepositAsPercent, paymentAccount.getId(), triggerPrice); } /** * Create and return a new Offer using a fixed price. * @param paymentAccount * @param direction * @param currencyCode * @param amountInSatoshis * @param minAmountInSatoshis * @param fixedOfferPriceAsString * @param securityDepositAsPercent * @param feeCurrency * @return OfferInfo */ public OfferInfo createOfferAtFixedPrice(PaymentAccount paymentAccount, String direction, String currencyCode, long amountInSatoshis, long minAmountInSatoshis, String fixedOfferPriceAsString, double securityDepositAsPercent) { return grpcClient.createFixedPricedOffer(direction, currencyCode, amountInSatoshis, minAmountInSatoshis, fixedOfferPriceAsString, securityDepositAsPercent, paymentAccount.getId()); } public TradeInfo takeOffer(String offerId, PaymentAccount paymentAccount) { return grpcClient.takeOffer(offerId, paymentAccount.getId()); } /** * Returns a persisted Trade with the given tradeId, or throws an exception. * @param tradeId * @return TradeInfo */ public TradeInfo getTrade(String tradeId) { return grpcClient.getTrade(tradeId); } /** * Predicate returns true if the given exception indicates the trade with the given * tradeId exists, but the trade's contract has not been fully prepared. */ public final BiPredicate tradeContractIsNotReady = (exception, tradeId) -> { if (exception.getMessage().contains("no contract was found")) { log.warn("Trade {} exists but is not fully prepared: {}.", tradeId, toCleanGrpcExceptionMessage(exception)); return true; } else { return false; } }; /** * Returns a trade's contract as a Json string, or null if the trade exists * but the contract is not ready. * @param tradeId * @return String */ public String getTradeContract(String tradeId) { try { var trade = grpcClient.getTrade(tradeId); return trade.getContractAsJson(); } catch (Exception ex) { if (tradeContractIsNotReady.test(ex, tradeId)) return null; else throw ex; } } /** * Returns true if the trade's taker deposit fee transaction has been published. * @param tradeId a valid trade id * @return boolean */ public boolean isTakerDepositFeeTxPublished(String tradeId) { return grpcClient.getTrade(tradeId).getIsPayoutPublished(); } /** * Returns true if the trade's taker deposit fee transaction has been confirmed. * @param tradeId a valid trade id * @return boolean */ public boolean isTakerDepositFeeTxConfirmed(String tradeId) { return grpcClient.getTrade(tradeId).getIsDepositsUnlocked(); } /** * Returns true if the trade's 'start payment' message has been sent by the buyer. * @param tradeId a valid trade id * @return boolean */ public boolean isTradePaymentSentSent(String tradeId) { return grpcClient.getTrade(tradeId).getIsPaymentSent(); } /** * Returns true if the trade's 'payment received' message has been sent by the seller. * @param tradeId a valid trade id * @return boolean */ public boolean isTradePaymentReceivedConfirmationSent(String tradeId) { return grpcClient.getTrade(tradeId).getIsPaymentReceived(); } /** * Returns true if the trade's payout transaction has been published. * @param tradeId a valid trade id * @return boolean */ public boolean isTradePayoutTxPublished(String tradeId) { return grpcClient.getTrade(tradeId).getIsPayoutPublished(); } /** * Sends a 'confirm payment started message' for a trade with the given tradeId, * or throws an exception. * @param tradeId */ public void sendConfirmPaymentSentMessage(String tradeId) { grpcClient.confirmPaymentSent(tradeId); } /** * Sends a 'confirm payment received message' for a trade with the given tradeId, * or throws an exception. * @param tradeId */ public void sendConfirmPaymentReceivedMessage(String tradeId) { grpcClient.confirmPaymentReceived(tradeId); } /** * Create and save a new PaymentAccount with details in the given json. * @param json * @return PaymentAccount */ public PaymentAccount createNewPaymentAccount(String json) { return grpcClient.createPaymentAccount(json); } /** * Returns a persisted PaymentAccount with the given paymentAccountId, or throws * an exception. * @param paymentAccountId The id of the PaymentAccount being looked up. * @return PaymentAccount */ public PaymentAccount getPaymentAccount(String paymentAccountId) { return grpcClient.getPaymentAccounts().stream() .filter(a -> (a.getId().equals(paymentAccountId))) .findFirst() .orElseThrow(() -> new PaymentAccountNotFoundException("Could not find a payment account with id " + paymentAccountId + ".")); } /** * Returns a persisted PaymentAccount with the given accountName, or throws * an exception. * @param accountName * @return PaymentAccount */ public PaymentAccount getPaymentAccountWithName(String accountName) { var req = GetPaymentAccountsRequest.newBuilder().build(); return grpcClient.getPaymentAccounts().stream() .filter(a -> (a.getAccountName().equals(accountName))) .findFirst() .orElseThrow(() -> new PaymentAccountNotFoundException("Could not find a payment account with name " + accountName + ".")); } public String toCleanGrpcExceptionMessage(Exception ex) { return capitalize(ex.getMessage().replaceFirst("^[A-Z_]+: ", "")); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/bot/BotPaymentAccountGenerator.java ================================================ package haveno.apitest.scenario.bot; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import haveno.core.api.model.PaymentAccountForm; import haveno.core.locale.Country; import lombok.extern.slf4j.Slf4j; import protobuf.PaymentAccount; import java.io.File; import java.util.Map; import static haveno.core.payment.payload.PaymentMethod.ZELLE_ID; import static haveno.core.payment.payload.PaymentMethod.F2F_ID; @Slf4j public class BotPaymentAccountGenerator { private final Gson gson = new GsonBuilder().setPrettyPrinting().serializeNulls().create(); private final BotClient botClient; public BotPaymentAccountGenerator(BotClient botClient) { this.botClient = botClient; } public PaymentAccount createF2FPaymentAccount(Country country, String accountName) { try { return botClient.getPaymentAccountWithName(accountName); } catch (PaymentAccountNotFoundException ignored) { // Ignore not found exception, create a new account. } Map p = getPaymentAccountFormMap(F2F_ID); p.put("accountName", accountName); p.put("city", country.name + " City"); p.put("country", country.code); p.put("contact", "By Semaphore"); p.put("extraInfo", ""); // Convert the map back to a json string and create the payment account over gRPC. return botClient.createNewPaymentAccount(gson.toJson(p)); } public PaymentAccount createZellePaymentAccount(String accountName, String holderName) { try { return botClient.getPaymentAccountWithName(accountName); } catch (PaymentAccountNotFoundException ignored) { // Ignore not found exception, create a new account. } Map p = getPaymentAccountFormMap(ZELLE_ID); p.put("accountName", accountName); p.put("emailOrMobileNr", holderName + "@zelle.com"); p.put("holderName", holderName); return botClient.createNewPaymentAccount(gson.toJson(p)); } private Map getPaymentAccountFormMap(String paymentMethodId) { File jsonFormTemplate = PaymentAccountForm.getPaymentAccountForm(paymentMethodId); jsonFormTemplate.deleteOnExit(); String jsonString = PaymentAccountForm.toJsonString(jsonFormTemplate); //noinspection unchecked return (Map) gson.fromJson(jsonString, Object.class); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/bot/InvalidRandomOfferException.java ================================================ /* * 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 . */ package haveno.apitest.scenario.bot; import haveno.common.HavenoException; @SuppressWarnings("unused") public class InvalidRandomOfferException extends HavenoException { public InvalidRandomOfferException(Throwable cause) { super(cause); } public InvalidRandomOfferException(String format, Object... args) { super(format, args); } public InvalidRandomOfferException(Throwable cause, String format, Object... args) { super(cause, format, args); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/bot/PaymentAccountNotFoundException.java ================================================ /* * 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 . */ package haveno.apitest.scenario.bot; import haveno.common.HavenoException; @SuppressWarnings("unused") public class PaymentAccountNotFoundException extends HavenoException { public PaymentAccountNotFoundException(Throwable cause) { super(cause); } public PaymentAccountNotFoundException(String format, Object... args) { super(format, args); } public PaymentAccountNotFoundException(Throwable cause, String format, Object... args) { super(cause, format, args); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/bot/RandomOffer.java ================================================ /* * 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 . */ package haveno.apitest.scenario.bot; import haveno.proto.grpc.OfferInfo; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import protobuf.PaymentAccount; import java.math.BigDecimal; import java.security.SecureRandom; import java.text.DecimalFormat; import java.util.Objects; import java.util.function.Supplier; import static haveno.apitest.method.offer.AbstractOfferTest.defaultSecurityDepositPct; import static haveno.cli.CurrencyFormat.formatInternalFiatPrice; import static haveno.cli.CurrencyFormat.formatSatoshis; import static haveno.common.util.MathUtils.scaleDownByPowerOf10; import static haveno.core.payment.payload.PaymentMethod.F2F_ID; import static java.lang.String.format; import static java.math.RoundingMode.HALF_UP; @Slf4j public class RandomOffer { private static final SecureRandom RANDOM = new SecureRandom(); private static final DecimalFormat FIXED_PRICE_FMT = new DecimalFormat("###########0"); @SuppressWarnings("FieldCanBeLocal") // If not an F2F account, keep amount <= 0.01 BTC to avoid hitting unsigned // acct trading limit. private final Supplier nextAmount = () -> this.getPaymentAccount().getPaymentMethod().getId().equals(F2F_ID) ? (long) (10000000 + RANDOM.nextInt(2500000)) : (long) (750000 + RANDOM.nextInt(250000)); @SuppressWarnings("FieldCanBeLocal") private final Supplier nextMinAmount = () -> { boolean useMinAmount = RANDOM.nextBoolean(); if (useMinAmount) { return this.getPaymentAccount().getPaymentMethod().getId().equals(F2F_ID) ? this.getAmount() - 5000000L : this.getAmount() - 50000L; } else { return this.getAmount(); } }; @SuppressWarnings("FieldCanBeLocal") private final Supplier nextPriceMargin = () -> { boolean useZeroMargin = RANDOM.nextBoolean(); if (useZeroMargin) { return 0.00; } else { BigDecimal min = BigDecimal.valueOf(-5.0).setScale(2, HALF_UP); BigDecimal max = BigDecimal.valueOf(5.0).setScale(2, HALF_UP); BigDecimal randomBigDecimal = min.add(BigDecimal.valueOf(RANDOM.nextDouble()).multiply(max.subtract(min))); return randomBigDecimal.setScale(2, HALF_UP).doubleValue(); } }; private final BotClient botClient; @Getter private final PaymentAccount paymentAccount; @Getter private final String direction; @Getter private final String currencyCode; @Getter private final long amount; @Getter private final long minAmount; @Getter private final boolean useMarketBasedPrice; @Getter private final double priceMargin; @Getter private String fixedOfferPrice = "0"; @Getter private OfferInfo offer; @Getter private String id; public RandomOffer(BotClient botClient, PaymentAccount paymentAccount) { this.botClient = botClient; this.paymentAccount = paymentAccount; this.direction = RANDOM.nextBoolean() ? "BUY" : "SELL"; this.currencyCode = Objects.requireNonNull(paymentAccount.getSelectedTradeCurrency()).getCode(); this.amount = nextAmount.get(); this.minAmount = nextMinAmount.get(); this.useMarketBasedPrice = RANDOM.nextBoolean(); this.priceMargin = nextPriceMargin.get(); } public RandomOffer create() throws InvalidRandomOfferException { try { printDescription(); if (useMarketBasedPrice) { this.offer = botClient.createOfferAtMarketBasedPrice(paymentAccount, direction, currencyCode, amount, minAmount, priceMargin, defaultSecurityDepositPct.get(), "0" /*no trigger price*/); } else { this.offer = botClient.createOfferAtFixedPrice(paymentAccount, direction, currencyCode, amount, minAmount, fixedOfferPrice, defaultSecurityDepositPct.get()); } this.id = offer.getId(); return this; } catch (Exception ex) { String error = format("Could not create valid %s offer for %s BTC: %s", currencyCode, formatSatoshis(amount), ex.getMessage()); throw new InvalidRandomOfferException(error, ex); } } private void printDescription() { double currentMarketPrice = botClient.getCurrentBTCMarketPrice(currencyCode); // Calculate a fixed price based on the random mkt price margin, even if we don't use it. double differenceFromMarketPrice = currentMarketPrice * scaleDownByPowerOf10(priceMargin, 2); double fixedOfferPriceAsDouble = direction.equals("BUY") ? currentMarketPrice - differenceFromMarketPrice : currentMarketPrice + differenceFromMarketPrice; this.fixedOfferPrice = FIXED_PRICE_FMT.format(fixedOfferPriceAsDouble); String description = format("Creating new %s %s / %s offer for amount = %s BTC, min-amount = %s BTC.", useMarketBasedPrice ? "mkt-based-price" : "fixed-priced", direction, currencyCode, formatSatoshis(amount), formatSatoshis(minAmount)); log.info(description); if (useMarketBasedPrice) { log.info("Offer Price Margin = {}%", priceMargin); log.info("Expected Offer Price = {} {}", formatInternalFiatPrice(Double.parseDouble(fixedOfferPrice)), currencyCode); } else { log.info("Fixed Offer Price = {} {}", fixedOfferPrice, currencyCode); } log.info("Current Market Price = {} {}", formatInternalFiatPrice(currentMarketPrice), currencyCode); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/bot/RobotBob.java ================================================ /* * 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 . */ package haveno.apitest.scenario.bot; import haveno.apitest.method.BitcoinCliHelper; import haveno.apitest.scenario.bot.protocol.BotProtocol; import haveno.apitest.scenario.bot.protocol.MakerBotProtocol; import haveno.apitest.scenario.bot.protocol.TakerBotProtocol; import haveno.apitest.scenario.bot.script.BashScriptGenerator; import haveno.apitest.scenario.bot.script.BotScript; import haveno.apitest.scenario.bot.shutdown.ManualBotShutdownException; import haveno.cli.table.builder.TableBuilder; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import static haveno.apitest.scenario.bot.protocol.ProtocolStep.DONE; import static haveno.apitest.scenario.bot.shutdown.ManualShutdown.isShutdownCalled; import static haveno.cli.table.builder.TableType.BTC_BALANCE_TBL; import static java.util.concurrent.TimeUnit.SECONDS; @Slf4j public class RobotBob extends Bot { @Getter private int numTrades; public RobotBob(BotClient botClient, BotScript botScript, BitcoinCliHelper bitcoinCli, BashScriptGenerator bashScriptGenerator) { super(botClient, botScript, bitcoinCli, bashScriptGenerator); } public void run() { for (String action : actions) { checkActionIsValid(action); BotProtocol botProtocol; if (action.equalsIgnoreCase(MAKE)) { botProtocol = new MakerBotProtocol(botClient, paymentAccount, protocolStepTimeLimitInMs, bitcoinCli, bashScriptGenerator); } else { botProtocol = new TakerBotProtocol(botClient, paymentAccount, protocolStepTimeLimitInMs, bitcoinCli, bashScriptGenerator); } botProtocol.run(); if (!botProtocol.getCurrentProtocolStep().equals(DONE)) { throw new IllegalStateException(botProtocol.getClass().getSimpleName() + " failed to complete."); } StringBuilder balancesBuilder = new StringBuilder(); balancesBuilder.append("BTC").append("\n"); balancesBuilder.append(new TableBuilder(BTC_BALANCE_TBL, botClient.getBalance().getBtc()).build().toString()).append("\n"); log.info("Completed {} successful trade{}. Current Balance:\n{}", ++numTrades, numTrades == 1 ? "" : "s", balancesBuilder); if (numTrades < actions.length) { try { SECONDS.sleep(20); } catch (InterruptedException ignored) { // empty } } } // end of actions loop if (stayAlive) waitForManualShutdown(); else warnCLIUserBeforeShutdown(); } private void checkActionIsValid(String action) { if (!action.equalsIgnoreCase(MAKE) && !action.equalsIgnoreCase(TAKE)) throw new IllegalStateException(action + " is not a valid bot action; must be 'make' or 'take'"); } private void waitForManualShutdown() { String harnessOrCase = isUsingTestHarness ? "harness" : "case"; log.info("All script actions have been completed, but the test {} will stay alive" + " until a /tmp/bottest-shutdown file is detected.", harnessOrCase); log.info("When ready to shutdown the test {}, run '$ touch /tmp/bottest-shutdown'.", harnessOrCase); if (!isUsingTestHarness) { log.warn("You will have to manually shutdown the bitcoind and Haveno nodes" + " running outside of the test harness."); } try { while (!isShutdownCalled()) { SECONDS.sleep(10); } log.warn("Manual shutdown signal received."); } catch (ManualBotShutdownException ex) { log.warn(ex.getMessage()); } catch (InterruptedException ignored) { // empty } } private void warnCLIUserBeforeShutdown() { if (isUsingTestHarness) { long delayInSeconds = 30; log.warn("All script actions have been completed. You have {} seconds to complete any" + " remaining tasks before the test harness shuts down.", delayInSeconds); try { SECONDS.sleep(delayInSeconds); } catch (InterruptedException ignored) { // empty } } else { log.info("Shutting down test case"); } } } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/bot/protocol/BotProtocol.java ================================================ /* * 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 . */ package haveno.apitest.scenario.bot.protocol; import haveno.apitest.method.BitcoinCliHelper; import haveno.apitest.scenario.bot.BotClient; import haveno.apitest.scenario.bot.script.BashScriptGenerator; import haveno.apitest.scenario.bot.shutdown.ManualBotShutdownException; import haveno.cli.table.builder.TableBuilder; import haveno.proto.grpc.TradeInfo; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import protobuf.PaymentAccount; import java.io.File; import java.security.SecureRandom; import java.util.Objects; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; import static haveno.apitest.scenario.bot.protocol.ProtocolStep.SEND_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE; import static haveno.apitest.scenario.bot.protocol.ProtocolStep.SEND_PAYMENT_SENT_MESSAGE; import static haveno.apitest.scenario.bot.protocol.ProtocolStep.START; import static haveno.apitest.scenario.bot.protocol.ProtocolStep.WAIT_FOR_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE; import static haveno.apitest.scenario.bot.protocol.ProtocolStep.WAIT_FOR_PAYMENT_SENT_MESSAGE; import static haveno.apitest.scenario.bot.protocol.ProtocolStep.WAIT_FOR_PAYOUT_TX; import static haveno.apitest.scenario.bot.protocol.ProtocolStep.WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED; import static haveno.apitest.scenario.bot.protocol.ProtocolStep.WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED; import static haveno.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled; import static haveno.cli.table.builder.TableType.TRADE_DETAIL_TBL; import static java.lang.String.format; import static java.lang.System.currentTimeMillis; import static java.util.Arrays.stream; import static java.util.concurrent.TimeUnit.MILLISECONDS; @Slf4j public abstract class BotProtocol { static final SecureRandom RANDOM = new SecureRandom(); static final String BUY = "BUY"; static final String SELL = "SELL"; protected final Supplier randomDelay = () -> (long) (2000 + RANDOM.nextInt(5000)); protected final AtomicLong protocolStepStartTime = new AtomicLong(0); protected final Consumer initProtocolStep = (step) -> { currentProtocolStep = step; printBotProtocolStep(); protocolStepStartTime.set(currentTimeMillis()); }; @Getter protected ProtocolStep currentProtocolStep; @Getter // Functions within 'this' need the @Getter. protected final BotClient botClient; protected final PaymentAccount paymentAccount; protected final String currencyCode; protected final long protocolStepTimeLimitInMs; protected final BitcoinCliHelper bitcoinCli; @Getter protected final BashScriptGenerator bashScriptGenerator; public BotProtocol(BotClient botClient, PaymentAccount paymentAccount, long protocolStepTimeLimitInMs, BitcoinCliHelper bitcoinCli, BashScriptGenerator bashScriptGenerator) { this.botClient = botClient; this.paymentAccount = paymentAccount; this.currencyCode = Objects.requireNonNull(paymentAccount.getSelectedTradeCurrency()).getCode(); this.protocolStepTimeLimitInMs = protocolStepTimeLimitInMs; this.bitcoinCli = bitcoinCli; this.bashScriptGenerator = bashScriptGenerator; this.currentProtocolStep = START; } public abstract void run(); protected boolean isWithinProtocolStepTimeLimit() { return (currentTimeMillis() - protocolStepStartTime.get()) < protocolStepTimeLimitInMs; } protected void checkIsStartStep() { if (currentProtocolStep != START) { throw new IllegalStateException("First bot protocol step must be " + START.name()); } } protected void printBotProtocolStep() { log.info("Starting protocol step {}. Bot will shutdown if step not completed within {} minutes.", currentProtocolStep.name(), MILLISECONDS.toMinutes(protocolStepTimeLimitInMs)); if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED)) { log.info("Generate a btc block to trigger taker's deposit fee tx confirmation."); createGenerateBtcBlockScript(); } } protected final Function waitForTakerFeeTxConfirm = (trade) -> { sleep(5000); waitForTakerFeeTxPublished(trade.getTradeId()); waitForTakerFeeTxConfirmed(trade.getTradeId()); return trade; }; protected final Function waitForPaymentSentMessage = (trade) -> { initProtocolStep.accept(WAIT_FOR_PAYMENT_SENT_MESSAGE); try { createPaymentSentScript(trade); log.info(" Waiting for a 'payment started' message from buyer for trade with id {}.", trade.getTradeId()); while (isWithinProtocolStepTimeLimit()) { checkIfShutdownCalled("Interrupted before checking if 'payment started' message has been sent."); try { var t = this.getBotClient().getTrade(trade.getTradeId()); if (t.getIsPaymentSent()) { log.info("Buyer has started payment for trade:\n{}", new TableBuilder(TRADE_DETAIL_TBL, t).build().toString()); return t; } } catch (Exception ex) { throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex)); } sleep(randomDelay.get()); } // end while throw new IllegalStateException("Payment was never sent; we won't wait any longer."); } catch (ManualBotShutdownException ex) { throw ex; // not an error, tells bot to shutdown } catch (Exception ex) { throw new IllegalStateException("Error while waiting payment sent message.", ex); } }; protected final Function sendPaymentSentMessage = (trade) -> { initProtocolStep.accept(SEND_PAYMENT_SENT_MESSAGE); checkIfShutdownCalled("Interrupted before sending 'payment started' message."); this.getBotClient().sendConfirmPaymentSentMessage(trade.getTradeId()); return trade; }; protected final Function waitForPaymentReceivedConfirmation = (trade) -> { initProtocolStep.accept(WAIT_FOR_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE); createPaymentReceivedScript(trade); try { log.info("Waiting for a 'payment received confirmation' message from seller for trade with id {}.", trade.getTradeId()); while (isWithinProtocolStepTimeLimit()) { checkIfShutdownCalled("Interrupted before checking if 'payment received confirmation' message has been sent."); try { var t = this.getBotClient().getTrade(trade.getTradeId()); if (t.getIsPaymentReceived()) { log.info("Seller has received payment for trade:\n{}", new TableBuilder(TRADE_DETAIL_TBL, t).build().toString()); return t; } } catch (Exception ex) { throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex)); } sleep(randomDelay.get()); } // end while throw new IllegalStateException("Payment was never received; we won't wait any longer."); } catch (ManualBotShutdownException ex) { throw ex; // not an error, tells bot to shutdown } catch (Exception ex) { throw new IllegalStateException("Error while waiting payment received confirmation message.", ex); } }; protected final Function sendPaymentReceivedMessage = (trade) -> { initProtocolStep.accept(SEND_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE); checkIfShutdownCalled("Interrupted before sending 'payment received confirmation' message."); this.getBotClient().sendConfirmPaymentReceivedMessage(trade.getTradeId()); return trade; }; protected final Function waitForPayoutTx = (trade) -> { initProtocolStep.accept(WAIT_FOR_PAYOUT_TX); try { log.info("Waiting on the 'payout tx published confirmation' for trade with id {}.", trade.getTradeId()); while (isWithinProtocolStepTimeLimit()) { checkIfShutdownCalled("Interrupted before checking if payout tx has been published."); try { var t = this.getBotClient().getTrade(trade.getTradeId()); if (t.getIsPayoutPublished()) { log.info("Payout tx {} has been published for trade:\n{}", t.getPayoutTxId(), new TableBuilder(TRADE_DETAIL_TBL, t).build().toString()); return t; } } catch (Exception ex) { throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex)); } sleep(randomDelay.get()); } // end while throw new IllegalStateException("Payout tx was never published; we won't wait any longer."); } catch (ManualBotShutdownException ex) { throw ex; // not an error, tells bot to shutdown } catch (Exception ex) { throw new IllegalStateException("Error while waiting for published payout tx.", ex); } }; protected void createPaymentSentScript(TradeInfo trade) { File script = bashScriptGenerator.createPaymentSentScript(trade); printCliHintAndOrScript(script, "The manual CLI side can send a 'payment started' message"); } protected void createPaymentReceivedScript(TradeInfo trade) { File script = bashScriptGenerator.createPaymentReceivedScript(trade); printCliHintAndOrScript(script, "The manual CLI side can sent a 'payment received confirmation' message"); } protected void createKeepFundsScript(TradeInfo trade) { File script = bashScriptGenerator.createKeepFundsScript(trade); printCliHintAndOrScript(script, "The manual CLI side can close the trade"); } protected void createGetBalanceScript() { File script = bashScriptGenerator.createGetBalanceScript(); printCliHintAndOrScript(script, "The manual CLI side can view current balances"); } protected void createGenerateBtcBlockScript() { String newBitcoinCoreAddress = bitcoinCli.getNewBtcAddress(); File script = bashScriptGenerator.createGenerateBtcBlockScript(newBitcoinCoreAddress); printCliHintAndOrScript(script, "The manual CLI side can generate 1 btc block"); } protected void printCliHintAndOrScript(File script, String hint) { log.info("{} by running bash script '{}'.", hint, script.getAbsolutePath()); if (this.getBashScriptGenerator().isPrintCliScripts()) this.getBashScriptGenerator().printCliScript(script, log); sleep(5000); // Allow 5s for CLI user to read the hint. } protected void sleep(long ms) { try { MILLISECONDS.sleep(ms); } catch (InterruptedException ignored) { // empty } } private void waitForTakerFeeTxPublished(String tradeId) { waitForTakerDepositFee(tradeId, WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED); } private void waitForTakerFeeTxConfirmed(String tradeId) { waitForTakerDepositFee(tradeId, WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED); } private void waitForTakerDepositFee(String tradeId, ProtocolStep depositTxProtocolStep) { initProtocolStep.accept(depositTxProtocolStep); validateCurrentProtocolStep(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED, WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED); try { log.info(waitingForDepositFeeTxMsg(tradeId)); while (isWithinProtocolStepTimeLimit()) { checkIfShutdownCalled("Interrupted before checking taker deposit fee tx is published and confirmed."); try { var trade = this.getBotClient().getTrade(tradeId); if (isDepositFeeTxStepComplete.test(trade)) return; else sleep(randomDelay.get()); } catch (Exception ex) { if (this.getBotClient().tradeContractIsNotReady.test(ex, tradeId)) sleep(randomDelay.get()); else throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex)); } } // end while throw new IllegalStateException(stoppedWaitingForDepositFeeTxMsg(this.getBotClient().getTrade(tradeId).getTakerDepositTxId())); } catch (ManualBotShutdownException ex) { throw ex; // not an error, tells bot to shutdown } catch (Exception ex) { throw new IllegalStateException("Error while waiting for taker deposit tx to be published or confirmed.", ex); } } private final Predicate isDepositFeeTxStepComplete = (trade) -> { if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED) && trade.getIsDepositsPublished()) { log.info("Taker deposit fee tx {} has been published.", trade.getTakerDepositTxId()); return true; } else if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED) && trade.getIsDepositsUnlocked()) { log.info("Taker deposit fee tx {} has been confirmed.", trade.getTakerDepositTxId()); return true; } else { return false; } }; private void validateCurrentProtocolStep(Enum... validBotSteps) { for (Enum validBotStep : validBotSteps) { if (currentProtocolStep.equals(validBotStep)) return; } throw new IllegalStateException("Unexpected bot step: " + currentProtocolStep.name() + ".\n" + "Must be one of " + stream(validBotSteps).map((Enum::name)).collect(Collectors.joining(",")) + "."); } private String waitingForDepositFeeTxMsg(String tradeId) { return format("Waiting for taker deposit fee tx for trade %s to be %s.", tradeId, currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED) ? "published" : "confirmed"); } private String stoppedWaitingForDepositFeeTxMsg(String txId) { return format("Taker deposit fee tx %s is took too long to be %s; we won't wait any longer.", txId, currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED) ? "published" : "confirmed"); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/bot/protocol/MakerBotProtocol.java ================================================ package haveno.apitest.scenario.bot.protocol; import haveno.apitest.method.BitcoinCliHelper; import haveno.apitest.scenario.bot.BotClient; import haveno.apitest.scenario.bot.RandomOffer; import haveno.apitest.scenario.bot.script.BashScriptGenerator; import haveno.apitest.scenario.bot.shutdown.ManualBotShutdownException; import haveno.cli.table.builder.TableBuilder; import haveno.proto.grpc.OfferInfo; import haveno.proto.grpc.TradeInfo; import lombok.extern.slf4j.Slf4j; import protobuf.PaymentAccount; import java.io.File; import java.util.Optional; import java.util.function.Function; import java.util.function.Supplier; import static haveno.apitest.scenario.bot.protocol.ProtocolStep.DONE; import static haveno.apitest.scenario.bot.protocol.ProtocolStep.WAIT_FOR_OFFER_TAKER; import static haveno.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled; import static haveno.cli.table.builder.TableType.OFFER_TBL; import static haveno.cli.table.builder.TableType.TRADE_DETAIL_TBL; @Slf4j public class MakerBotProtocol extends BotProtocol { public MakerBotProtocol(BotClient botClient, PaymentAccount paymentAccount, long protocolStepTimeLimitInMs, BitcoinCliHelper bitcoinCli, BashScriptGenerator bashScriptGenerator) { super(botClient, paymentAccount, protocolStepTimeLimitInMs, bitcoinCli, bashScriptGenerator); } @Override public void run() { checkIsStartStep(); Function, TradeInfo> makeTrade = waitForNewTrade.andThen(waitForTakerFeeTxConfirm); var trade = makeTrade.apply(randomOffer); var makerIsBuyer = trade.getOffer().getDirection().equalsIgnoreCase(BUY); Function completeTraditionalTransaction = makerIsBuyer ? sendPaymentSentMessage.andThen(waitForPaymentReceivedConfirmation) : waitForPaymentSentMessage.andThen(sendPaymentReceivedMessage); completeTraditionalTransaction.apply(trade); currentProtocolStep = DONE; } private final Supplier randomOffer = () -> { checkIfShutdownCalled("Interrupted before creating random offer."); OfferInfo offer = new RandomOffer(botClient, paymentAccount).create().getOffer(); log.info("Created random {} offer\n{}", currencyCode, new TableBuilder(OFFER_TBL, offer).build()); return offer; }; private final Function, TradeInfo> waitForNewTrade = (randomOffer) -> { initProtocolStep.accept(WAIT_FOR_OFFER_TAKER); OfferInfo offer = randomOffer.get(); createTakeOfferCliScript(offer); try { log.info("Impatiently waiting for offer {} to be taken, repeatedly calling gettrade.", offer.getId()); while (isWithinProtocolStepTimeLimit()) { checkIfShutdownCalled("Interrupted while waiting for offer to be taken."); try { var trade = getNewTrade(offer.getId()); if (trade.isPresent()) return trade.get(); else sleep(randomDelay.get()); } catch (Exception ex) { throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex), ex); } } // end while throw new IllegalStateException("Offer was never taken; we won't wait any longer."); } catch (ManualBotShutdownException ex) { throw ex; // not an error, tells bot to shutdown } catch (Exception ex) { throw new IllegalStateException("Error while waiting for offer to be taken.", ex); } }; private Optional getNewTrade(String offerId) { try { var trade = botClient.getTrade(offerId); log.info("Offer {} was taken, new trade:\n{}", offerId, new TableBuilder(TRADE_DETAIL_TBL, trade).build().toString()); return Optional.of(trade); } catch (Exception ex) { // Get trade will throw a non-fatal gRPC exception if not found. log.info(this.getBotClient().toCleanGrpcExceptionMessage(ex)); return Optional.empty(); } } private void createTakeOfferCliScript(OfferInfo offer) { File script = bashScriptGenerator.createTakeOfferScript(offer); printCliHintAndOrScript(script, "The manual CLI side can take the offer"); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/bot/protocol/ProtocolStep.java ================================================ package haveno.apitest.scenario.bot.protocol; public enum ProtocolStep { START, FIND_OFFER, TAKE_OFFER, WAIT_FOR_OFFER_TAKER, WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED, WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED, SEND_PAYMENT_SENT_MESSAGE, WAIT_FOR_PAYMENT_SENT_MESSAGE, SEND_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE, WAIT_FOR_PAYMENT_RECEIVED_CONFIRMATION_MESSAGE, WAIT_FOR_PAYOUT_TX, CLOSE_TRADE, DONE } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/bot/protocol/TakerBotProtocol.java ================================================ package haveno.apitest.scenario.bot.protocol; import haveno.apitest.method.BitcoinCliHelper; import haveno.apitest.scenario.bot.BotClient; import haveno.apitest.scenario.bot.script.BashScriptGenerator; import haveno.apitest.scenario.bot.shutdown.ManualBotShutdownException; import haveno.cli.table.builder.TableBuilder; import haveno.proto.grpc.OfferInfo; import haveno.proto.grpc.TradeInfo; import lombok.extern.slf4j.Slf4j; import protobuf.PaymentAccount; import java.io.File; import java.util.Optional; import java.util.function.Function; import java.util.function.Supplier; import static haveno.apitest.scenario.bot.protocol.ProtocolStep.DONE; import static haveno.apitest.scenario.bot.protocol.ProtocolStep.FIND_OFFER; import static haveno.apitest.scenario.bot.protocol.ProtocolStep.TAKE_OFFER; import static haveno.apitest.scenario.bot.shutdown.ManualShutdown.checkIfShutdownCalled; import static haveno.cli.table.builder.TableType.OFFER_TBL; import static haveno.core.payment.payload.PaymentMethod.F2F_ID; @Slf4j public class TakerBotProtocol extends BotProtocol { public TakerBotProtocol(BotClient botClient, PaymentAccount paymentAccount, long protocolStepTimeLimitInMs, BitcoinCliHelper bitcoinCli, BashScriptGenerator bashScriptGenerator) { super(botClient, paymentAccount, protocolStepTimeLimitInMs, bitcoinCli, bashScriptGenerator); } @Override public void run() { checkIsStartStep(); Function takeTrade = takeOffer.andThen(waitForTakerFeeTxConfirm); var trade = takeTrade.apply(findOffer.get()); var takerIsSeller = trade.getOffer().getDirection().equalsIgnoreCase(BUY); Function completeTraditionalTransaction = takerIsSeller ? waitForPaymentSentMessage.andThen(sendPaymentReceivedMessage) : sendPaymentSentMessage.andThen(waitForPaymentReceivedConfirmation); completeTraditionalTransaction.apply(trade); currentProtocolStep = DONE; } private final Supplier> firstOffer = () -> { var offers = botClient.getOffers(currencyCode); if (offers.size() > 0) { log.info("Offers found:\n{}", new TableBuilder(OFFER_TBL, offers).build()); OfferInfo offer = offers.get(0); log.info("Will take first offer {}", offer.getId()); return Optional.of(offer); } else { log.info("No buy or sell {} offers found.", currencyCode); return Optional.empty(); } }; private final Supplier findOffer = () -> { initProtocolStep.accept(FIND_OFFER); createMakeOfferScript(); try { log.info("Impatiently waiting for at least one {} offer to be created, repeatedly calling getoffers.", currencyCode); while (isWithinProtocolStepTimeLimit()) { checkIfShutdownCalled("Interrupted while checking offers."); try { Optional offer = firstOffer.get(); if (offer.isPresent()) return offer.get(); else sleep(randomDelay.get()); } catch (Exception ex) { throw new IllegalStateException(this.getBotClient().toCleanGrpcExceptionMessage(ex), ex); } } // end while throw new IllegalStateException("Offer was never created; we won't wait any longer."); } catch (ManualBotShutdownException ex) { throw ex; // not an error, tells bot to shutdown } catch (Exception ex) { throw new IllegalStateException("Error while waiting for a new offer.", ex); } }; private final Function takeOffer = (offer) -> { initProtocolStep.accept(TAKE_OFFER); checkIfShutdownCalled("Interrupted before taking offer."); return botClient.takeOffer(offer.getId(), paymentAccount); }; private void createMakeOfferScript() { String direction = RANDOM.nextBoolean() ? "BUY" : "SELL"; boolean createMarginPricedOffer = RANDOM.nextBoolean(); // If not using an F2F account, don't go over possible 0.01 BTC // limit if account is not signed. String amount = paymentAccount.getPaymentMethod().getId().equals(F2F_ID) ? "0.25" : "0.01"; File script; if (createMarginPricedOffer) { script = bashScriptGenerator.createMakeMarginPricedOfferScript(direction, currencyCode, amount, "0.0", "15.0"); } else { script = bashScriptGenerator.createMakeFixedPricedOfferScript(direction, currencyCode, amount, botClient.getCurrentBTCMarketPriceAsIntegerString(currencyCode), "15.0"); } printCliHintAndOrScript(script, "The manual CLI side can create an offer"); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/bot/script/BashScriptGenerator.java ================================================ /* * 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 . */ package haveno.apitest.scenario.bot.script; import com.google.common.io.Files; import haveno.common.file.FileUtil; import haveno.proto.grpc.OfferInfo; import haveno.proto.grpc.TradeInfo; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.io.File; import java.io.IOException; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.List; import static com.google.common.io.FileWriteMode.APPEND; import static java.lang.String.format; import static java.lang.System.getProperty; import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.file.Files.readAllBytes; @Slf4j @Getter public class BashScriptGenerator { private final int apiPort; private final String apiPassword; private final String paymentAccountId; private final String cliBase; private final boolean printCliScripts; public BashScriptGenerator(String apiPassword, int apiPort, String paymentAccountId, boolean printCliScripts) { this.apiPassword = apiPassword; this.apiPort = apiPort; this.paymentAccountId = paymentAccountId; this.printCliScripts = printCliScripts; this.cliBase = format("./haveno-cli --password=%s --port=%d", apiPassword, apiPort); } public File createMakeMarginPricedOfferScript(String direction, String currencyCode, String amount, String marketPriceMargin, String securityDeposit) { String makeOfferCmd = format("%s createoffer --payment-account=%s " + " --direction=%s" + " --currency-code=%s" + " --amount=%s" + " --market-price-margin=%s" + " --security-deposit=%s" + " --fee-currency=%s", cliBase, this.getPaymentAccountId(), direction, currencyCode, amount, marketPriceMargin, securityDeposit); String getOffersCmd = format("%s getmyoffers --direction=%s --currency-code=%s", cliBase, direction, currencyCode); return createCliScript("createoffer.sh", makeOfferCmd, "sleep 2", getOffersCmd); } public File createMakeFixedPricedOfferScript(String direction, String currencyCode, String amount, String fixedPrice, String securityDeposit) { String makeOfferCmd = format("%s createoffer --payment-account=%s " + " --direction=%s" + " --currency-code=%s" + " --amount=%s" + " --fixed-price=%s" + " --security-deposit=%s" + " --fee-currency=%s", cliBase, this.getPaymentAccountId(), direction, currencyCode, amount, fixedPrice, securityDeposit); String getOffersCmd = format("%s getmyoffers --direction=%s --currency-code=%s", cliBase, direction, currencyCode); return createCliScript("createoffer.sh", makeOfferCmd, "sleep 2", getOffersCmd); } public File createTakeOfferScript(OfferInfo offer) { String getOffersCmd = format("%s getoffers --direction=%s --currency-code=%s", cliBase, offer.getDirection(), offer.getCounterCurrencyCode()); String takeOfferCmd = format("%s takeoffer --offer-id=%s --payment-account=%s", cliBase, offer.getId(), this.getPaymentAccountId()); String getTradeCmd = format("%s gettrade --trade-id=%s", cliBase, offer.getId()); return createCliScript("takeoffer.sh", getOffersCmd, takeOfferCmd, "sleep 5", getTradeCmd); } public File createPaymentSentScript(TradeInfo trade) { String paymentSentCmd = format("%s confirmpaymentsent --trade-id=%s", cliBase, trade.getTradeId()); String getTradeCmd = format("%s gettrade --trade-id=%s", cliBase, trade.getTradeId()); return createCliScript("confirmpaymentsent.sh", paymentSentCmd, "sleep 2", getTradeCmd); } public File createPaymentReceivedScript(TradeInfo trade) { String paymentSentCmd = format("%s confirmpaymentreceived --trade-id=%s", cliBase, trade.getTradeId()); String getTradeCmd = format("%s gettrade --trade-id=%s", cliBase, trade.getTradeId()); return createCliScript("confirmpaymentreceived.sh", paymentSentCmd, "sleep 2", getTradeCmd); } public File createKeepFundsScript(TradeInfo trade) { String paymentSentCmd = format("%s closetrade --trade-id=%s", cliBase, trade.getTradeId()); String getTradeCmd = format("%s gettrade --trade-id=%s", cliBase, trade.getTradeId()); String getBalanceCmd = format("%s getbalance", cliBase); return createCliScript("closetrade.sh", paymentSentCmd, "sleep 2", getTradeCmd, getBalanceCmd); } public File createGetBalanceScript() { String getBalanceCmd = format("%s getbalance", cliBase); return createCliScript("getbalance.sh", getBalanceCmd); } public File createGenerateBtcBlockScript(String address) { String bitcoinCliCmd = format("bitcoin-cli -regtest -rpcport=19443 -rpcuser=apitest" + " -rpcpassword=apitest generatetoaddress 1 \"%s\"", address); return createCliScript("genbtcblk.sh", bitcoinCliCmd); } public File createCliScript(String scriptName, String... commands) { String filename = getProperty("java.io.tmpdir") + File.separator + scriptName; File oldScript = new File(filename); if (oldScript.exists()) { try { FileUtil.deleteFileIfExists(oldScript); } catch (IOException ex) { throw new IllegalStateException("Unable to delete old script.", ex); } } File script = new File(filename); try { List lines = new ArrayList<>(); lines.add("#!/bin/bash"); lines.add("############################################################"); lines.add("# This example CLI script may be overwritten during the test"); lines.add("# run, and will be deleted when the test harness shuts down."); lines.add("# Make a copy if you want to save it."); lines.add("############################################################"); lines.add("set -x"); Collections.addAll(lines, commands); Files.asCharSink(script, UTF_8, APPEND).writeLines(lines); if (!script.setExecutable(true)) throw new IllegalStateException("Unable to set script owner's execute permission."); } catch (IOException ex) { log.error("", ex); throw new IllegalStateException(ex); } finally { script.deleteOnExit(); } return script; } public void printCliScript(File cliScript, org.slf4j.Logger logger) { try { String contents = new String(readAllBytes(Paths.get(cliScript.getPath()))); logger.info("CLI script {}:\n{}", cliScript.getAbsolutePath(), contents); } catch (IOException ex) { throw new IllegalStateException("Error reading CLI script contents.", ex); } } } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/bot/script/BotScript.java ================================================ /* * 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 . */ package haveno.apitest.scenario.bot.script; import lombok.Getter; import lombok.Setter; import lombok.ToString; import javax.annotation.Nullable; @Getter @ToString public class BotScript { // Common, default is true. private final boolean useTestHarness; // Used only with test harness. Mutually exclusive, but if both are not null, // the botPaymentMethodId takes precedence over countryCode. @Nullable private final String botPaymentMethodId; @Nullable private final String countryCode; // Used only without test harness. @Nullable @Setter private String paymentAccountIdForBot; @Nullable @Setter private String paymentAccountIdForCliScripts; // Common, used with or without test harness. private final int apiPortForCliScripts; private final String[] actions; private final long protocolStepTimeLimitInMinutes; private final boolean printCliScripts; private final boolean stayAlive; @SuppressWarnings("NullableProblems") BotScript(boolean useTestHarness, String botPaymentMethodId, String countryCode, String paymentAccountIdForBot, String paymentAccountIdForCliScripts, String[] actions, int apiPortForCliScripts, long protocolStepTimeLimitInMinutes, boolean printCliScripts, boolean stayAlive) { this.useTestHarness = useTestHarness; this.botPaymentMethodId = botPaymentMethodId; this.countryCode = countryCode != null ? countryCode.toUpperCase() : null; this.paymentAccountIdForBot = paymentAccountIdForBot; this.paymentAccountIdForCliScripts = paymentAccountIdForCliScripts; this.apiPortForCliScripts = apiPortForCliScripts; this.actions = actions; this.protocolStepTimeLimitInMinutes = protocolStepTimeLimitInMinutes; this.printCliScripts = printCliScripts; this.stayAlive = stayAlive; } } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/bot/script/BotScriptGenerator.java ================================================ /* * 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 . */ package haveno.apitest.scenario.bot.script; import haveno.common.file.JsonFileManager; import haveno.core.util.JsonUtil; import joptsimple.BuiltinHelpFormatter; import joptsimple.OptionParser; import joptsimple.OptionSet; import joptsimple.OptionSpec; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.io.File; import java.io.IOException; import java.io.PrintStream; import static java.lang.System.err; import static java.lang.System.exit; import static java.lang.System.getProperty; import static java.lang.System.out; @Slf4j public class BotScriptGenerator { private final boolean useTestHarness; @Nullable private final String countryCode; @Nullable private final String botPaymentMethodId; @Nullable private final String paymentAccountIdForBot; @Nullable private final String paymentAccountIdForCliScripts; private final int apiPortForCliScripts; private final String actions; private final int protocolStepTimeLimitInMinutes; private final boolean printCliScripts; private final boolean stayAlive; public BotScriptGenerator(String[] args) { OptionParser parser = new OptionParser(); var helpOpt = parser.accepts("help", "Print this help text.") .forHelp(); OptionSpec useTestHarnessOpt = parser .accepts("use-testharness", "Use the test harness, or manually start your own nodes.") .withRequiredArg() .ofType(Boolean.class) .defaultsTo(true); OptionSpec actionsOpt = parser .accepts("actions", "A comma delimited list with no spaces, e.g., make,take,take,make,...") .withRequiredArg(); OptionSpec botPaymentMethodIdOpt = parser .accepts("bot-payment-method", "The bot's (Bob) payment method id. If using the test harness," + " the id will be used to automatically create a payment account.") .withRequiredArg(); OptionSpec countryCodeOpt = parser .accepts("country-code", "The two letter country-code for an F2F payment account if using the test harness," + " but the bot-payment-method option takes precedence.") .withRequiredArg(); OptionSpec apiPortForCliScriptsOpt = parser .accepts("api-port-for-cli-scripts", "The api port used in bot generated bash/cli scripts.") .withRequiredArg() .ofType(Integer.class) .defaultsTo(9998); OptionSpec paymentAccountIdForBotOpt = parser .accepts("payment-account-for-bot", "The bot side's payment account id, when the test harness is not used," + " and Bob & Alice accounts are not automatically created.") .withRequiredArg(); OptionSpec paymentAccountIdForCliScriptsOpt = parser .accepts("payment-account-for-cli-scripts", "The other side's payment account id, used in generated bash/cli scripts when" + " the test harness is not used, and Bob & Alice accounts are not automatically created.") .withRequiredArg(); OptionSpec protocolStepTimeLimitInMinutesOpt = parser .accepts("step-time-limit", "Each protocol step's time limit in minutes") .withRequiredArg() .ofType(Integer.class) .defaultsTo(60); OptionSpec printCliScriptsOpt = parser .accepts("print-cli-scripts", "Print the generated CLI scripts from bot") .withRequiredArg() .ofType(Boolean.class) .defaultsTo(false); OptionSpec stayAliveOpt = parser .accepts("stay-alive", "Leave test harness nodes running after the last action.") .withRequiredArg() .ofType(Boolean.class) .defaultsTo(true); OptionSet options = parser.parse(args); if (options.has(helpOpt)) { printHelp(parser, out); exit(0); } if (!options.has(actionsOpt)) { printHelp(parser, err); exit(1); } this.useTestHarness = options.has(useTestHarnessOpt) ? options.valueOf(useTestHarnessOpt) : true; this.actions = options.valueOf(actionsOpt); this.apiPortForCliScripts = options.has(apiPortForCliScriptsOpt) ? options.valueOf(apiPortForCliScriptsOpt) : 9998; this.botPaymentMethodId = options.has(botPaymentMethodIdOpt) ? options.valueOf(botPaymentMethodIdOpt) : null; this.countryCode = options.has(countryCodeOpt) ? options.valueOf(countryCodeOpt) : null; this.paymentAccountIdForBot = options.has(paymentAccountIdForBotOpt) ? options.valueOf(paymentAccountIdForBotOpt) : null; this.paymentAccountIdForCliScripts = options.has(paymentAccountIdForCliScriptsOpt) ? options.valueOf(paymentAccountIdForCliScriptsOpt) : null; this.protocolStepTimeLimitInMinutes = options.valueOf(protocolStepTimeLimitInMinutesOpt); this.printCliScripts = options.valueOf(printCliScriptsOpt); this.stayAlive = options.valueOf(stayAliveOpt); var noPaymentAccountCountryOrMethodForTestHarness = useTestHarness && (!options.has(countryCodeOpt) && !options.has(botPaymentMethodIdOpt)); if (noPaymentAccountCountryOrMethodForTestHarness) { log.error("When running the test harness, payment accounts are automatically generated,"); log.error("and you must provide one of the following options:"); log.error(" \t\t(1) --bot-payment-method= OR"); log.error(" \t\t(2) --country-code="); log.error("If the bot-payment-method option is not present, the bot will create" + " a country based F2F account using the country-code."); log.error("If both are present, the bot-payment-method will take precedence. " + "Currently, only the ZELLE_ID bot-payment-method is supported."); printHelp(parser, err); exit(1); } var noPaymentAccountIdOrApiPortForCliScripts = !useTestHarness && (!options.has(paymentAccountIdForCliScriptsOpt) || !options.has(paymentAccountIdForBotOpt)); if (noPaymentAccountIdOrApiPortForCliScripts) { log.error("If not running the test harness, payment accounts are not automatically generated,"); log.error("and you must provide three options:"); log.error(" \t\t(1) --api-port-for-cli-scripts="); log.error(" \t\t(2) --payment-account-for-bot="); log.error(" \t\t(3) --payment-account-for-cli-scripts="); log.error("These will be used by the bot and in CLI scripts the bot will generate when creating an offer."); printHelp(parser, err); exit(1); } } private void printHelp(OptionParser parser, PrintStream stream) { try { String usage = "Examples\n--------\n" + examplesUsingTestHarness() + examplesNotUsingTestHarness(); stream.println(); parser.formatHelpWith(new HelpFormatter()); parser.printHelpOn(stream); stream.println(); stream.println(usage); stream.println(); } catch (IOException ex) { log.error("", ex); } } private String examplesUsingTestHarness() { @SuppressWarnings("StringBufferReplaceableByString") StringBuilder builder = new StringBuilder(); builder.append("To generate a bot-script.json file that will start the test harness,"); builder.append(" create F2F accounts for Bob and Alice,"); builder.append(" and take an offer created by Alice's CLI:").append("\n"); builder.append("\tUsage: BotScriptGenerator").append("\n"); builder.append("\t\t").append("--use-testharness=true").append("\n"); builder.append("\t\t").append("--country-code=").append("\n"); builder.append("\t\t").append("--actions=take").append("\n"); builder.append("\n"); builder.append("To generate a bot-script.json file that will start the test harness,"); builder.append(" create Zelle accounts for Bob and Alice,"); builder.append(" and create an offer to be taken by Alice's CLI:").append("\n"); builder.append("\tUsage: BotScriptGenerator").append("\n"); builder.append("\t\t").append("--use-testharness=true").append("\n"); builder.append("\t\t").append("--bot-payment-method=ZELLE").append("\n"); builder.append("\t\t").append("--actions=make").append("\n"); builder.append("\n"); return builder.toString(); } private String examplesNotUsingTestHarness() { @SuppressWarnings("StringBufferReplaceableByString") StringBuilder builder = new StringBuilder(); builder.append("To generate a bot-script.json file that will not start the test harness,"); builder.append(" but will create useful bash scripts for the CLI user,"); builder.append(" and make two offers, then take two offers:").append("\n"); builder.append("\tUsage: BotScriptGenerator").append("\n"); builder.append("\t\t").append("--use-testharness=false").append("\n"); builder.append("\t\t").append("--api-port-for-cli-scripts=").append("\n"); builder.append("\t\t").append("--payment-account-for-bot=").append("\n"); builder.append("\t\t").append("--payment-account-for-cli-scripts=").append("\n"); builder.append("\t\t").append("--actions=make,make,take,take").append("\n"); builder.append("\n"); return builder.toString(); } private String generateBotScriptTemplate() { return JsonUtil.objectToJson(new BotScript( useTestHarness, botPaymentMethodId, countryCode, paymentAccountIdForBot, paymentAccountIdForCliScripts, actions.split("\\s*,\\s*").clone(), apiPortForCliScripts, protocolStepTimeLimitInMinutes, printCliScripts, stayAlive)); } public static void main(String[] args) { BotScriptGenerator generator = new BotScriptGenerator(args); String json = generator.generateBotScriptTemplate(); String destDir = getProperty("java.io.tmpdir"); JsonFileManager jsonFileManager = new JsonFileManager(new File(destDir)); jsonFileManager.writeToDisc(json, "bot-script"); JsonFileManager.shutDownAllInstances(); log.info("Saved {}/bot-script.json", destDir); log.info("bot-script.json contents\n{}", json); } // Makes a formatter with a given overall row width of 120 and column separator width of 2. private static class HelpFormatter extends BuiltinHelpFormatter { public HelpFormatter() { super(120, 2); } } } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/bot/shutdown/ManualBotShutdownException.java ================================================ /* * 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 . */ package haveno.apitest.scenario.bot.shutdown; import haveno.common.HavenoException; @SuppressWarnings("unused") public class ManualBotShutdownException extends HavenoException { public ManualBotShutdownException(Throwable cause) { super(cause); } public ManualBotShutdownException(String format, Object... args) { super(format, args); } public ManualBotShutdownException(Throwable cause, String format, Object... args) { super(cause, format, args); } } ================================================ FILE: apitest/src/test/java/haveno/apitest/scenario/bot/shutdown/ManualShutdown.java ================================================ package haveno.apitest.scenario.bot.shutdown; import haveno.common.UserThread; import lombok.extern.slf4j.Slf4j; import java.io.File; import java.io.IOException; import java.util.concurrent.atomic.AtomicBoolean; import static haveno.common.file.FileUtil.deleteFileIfExists; import static java.util.concurrent.TimeUnit.MILLISECONDS; @Slf4j public class ManualShutdown { public static final String SHUTDOWN_FILENAME = "/tmp/bottest-shutdown"; private static final AtomicBoolean SHUTDOWN_CALLED = new AtomicBoolean(false); /** * Looks for a /tmp/bottest-shutdown file and throws a BotShutdownException if found. * * Running '$ touch /tmp/bottest-shutdown' could be used to trigger a scaffold teardown. * * This is much easier than manually shutdown down haveno apps & bitcoind. */ public static void startShutdownTimer() { deleteStaleShutdownFile(); UserThread.runPeriodically(() -> { File shutdownFile = new File(SHUTDOWN_FILENAME); if (shutdownFile.exists()) { log.warn("Caught manual shutdown signal: /tmp/bottest-shutdown file exists."); try { deleteFileIfExists(shutdownFile); } catch (IOException ex) { log.error("", ex); throw new IllegalStateException(ex); } SHUTDOWN_CALLED.set(true); } }, 2000, MILLISECONDS); } public static boolean isShutdownCalled() { return SHUTDOWN_CALLED.get(); } public static void checkIfShutdownCalled(String warning) throws ManualBotShutdownException { if (isShutdownCalled()) throw new ManualBotShutdownException(warning); } private static void deleteStaleShutdownFile() { try { deleteFileIfExists(new File(SHUTDOWN_FILENAME)); } catch (IOException ex) { log.error("", ex); throw new IllegalStateException(ex); } } } ================================================ FILE: apitest/src/test/resources/logback.xml ================================================ %highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n) ================================================ FILE: assets/src/main/java/haveno/asset/AbstractAsset.java ================================================ /* * 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 . */ package haveno.asset; import static org.apache.commons.lang3.Validate.notBlank; import static org.apache.commons.lang3.Validate.notNull; /** * Abstract base class for {@link Asset} implementations. Most implementations should not * extend this class directly, but should rather extend {@link Coin}, {@link Token} or one * of their subtypes. * * @author Chris Beams * @since 0.7.0 */ public abstract class AbstractAsset implements Asset { private final String name; private final String tickerSymbol; private final AddressValidator addressValidator; public AbstractAsset(String name, String tickerSymbol, AddressValidator addressValidator) { this.name = notBlank(name); this.tickerSymbol = notBlank(tickerSymbol); this.addressValidator = notNull(addressValidator); } @Override public final String getName() { return name; } @Override public final String getTickerSymbol() { return tickerSymbol; } @Override public final AddressValidationResult validateAddress(String address) { return addressValidator.validate(address); } @Override public String toString() { return getClass().getName(); } } ================================================ FILE: assets/src/main/java/haveno/asset/AddressValidationResult.java ================================================ /* * 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 . */ package haveno.asset; /** * Value object representing the result of validating an {@link Asset} address. Various * factory methods are provided for typical use cases. * * @author Chris Beams * @since 0.7.0 * @see Asset#validateAddress(String) */ public class AddressValidationResult { private static final AddressValidationResult VALID_ADDRESS = new AddressValidationResult(true, "", ""); private final boolean isValid; private final String message; private final String i18nKey; private AddressValidationResult(boolean isValid, String message, String i18nKey) { this.isValid = isValid; this.message = message; this.i18nKey = i18nKey; } public boolean isValid() { return isValid; } public String getI18nKey() { return i18nKey; } public String getMessage() { return message; } public static AddressValidationResult validAddress() { return VALID_ADDRESS; } public static AddressValidationResult invalidAddress(Throwable cause) { return invalidAddress(cause.getMessage()); } public static AddressValidationResult invalidAddress(String cause) { return invalidAddress(cause, "validation.crypto.invalidAddress"); } public static AddressValidationResult invalidAddress(String cause, String i18nKey) { return new AddressValidationResult(false, cause, i18nKey); } public static AddressValidationResult invalidStructure() { return invalidAddress("", "validation.crypto.wrongStructure"); } } ================================================ FILE: assets/src/main/java/haveno/asset/AddressValidator.java ================================================ /* * 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 . */ package haveno.asset; /** * An {@link Asset} address validation function. * * @author Chris Beams * @since 0.7.0 */ public interface AddressValidator { AddressValidationResult validate(String address); } ================================================ FILE: assets/src/main/java/haveno/asset/Asset.java ================================================ /* * 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 . */ package haveno.asset; /** * Interface representing a given ("crypto") asset in its most abstract form, having a * {@link #getName() name}, eg "Bitcoin", a {@link #getTickerSymbol() ticker symbol}, * eg "BTC", and an address validation function. Together, these properties represent * the minimum information and functionality required to register and trade an asset on * the Haveno network. *

* Implementations typically extend either the {@link Coin} or {@link Token} base * classes, and must be registered in the {@code META-INF/services/haveno.asset.Asset} file * in order to be available in the {@link AssetRegistry} at runtime. * * @author Chris Beams * @since 0.7.0 * @see AbstractAsset * @see Coin * @see Token * @see Erc20Token * @see AssetRegistry */ public interface Asset { String getName(); String getTickerSymbol(); AddressValidationResult validateAddress(String address); } ================================================ FILE: assets/src/main/java/haveno/asset/AssetRegistry.java ================================================ /* * 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 . */ package haveno.asset; import java.util.ArrayList; import java.util.List; import java.util.ServiceLoader; import java.util.stream.Stream; /** * Provides {@link Stream}-based access to {@link Asset} implementations registered in * the {@code META-INF/services/haveno.asset.Asset} provider-configuration file. * * @author Chris Beams * @since 0.7.0 * @see ServiceLoader */ public class AssetRegistry { private static final List registeredAssets = new ArrayList<>(); static { for (Asset asset : ServiceLoader.load(Asset.class)) { registeredAssets.add(asset); } } public Stream stream() { return registeredAssets.stream(); } } ================================================ FILE: assets/src/main/java/haveno/asset/Base58AddressValidator.java ================================================ /* * 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 . */ package haveno.asset; import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.params.MainNetParams; /** * {@link AddressValidator} for Base58-encoded addresses. * * @author Chris Beams * @since 0.7.0 * @see org.bitcoinj.core.LegacyAddress#fromBase58(NetworkParameters, String) */ public class Base58AddressValidator implements AddressValidator { private final NetworkParameters networkParameters; public Base58AddressValidator() { this(MainNetParams.get()); } public Base58AddressValidator(NetworkParameters networkParameters) { this.networkParameters = networkParameters; } @Override public AddressValidationResult validate(String address) { try { LegacyAddress.fromBase58(networkParameters, address); } catch (AddressFormatException ex) { return AddressValidationResult.invalidAddress(ex); } return AddressValidationResult.validAddress(); } } ================================================ FILE: assets/src/main/java/haveno/asset/BitcoinAddressValidator.java ================================================ /* * 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 . */ package haveno.asset; import org.bitcoinj.core.Address; import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.params.MainNetParams; /** * {@link AddressValidator} for Bitcoin addresses. * * @author Oscar Guindzberg */ public class BitcoinAddressValidator implements AddressValidator { private final NetworkParameters networkParameters; public BitcoinAddressValidator() { this(MainNetParams.get()); } public BitcoinAddressValidator(NetworkParameters networkParameters) { this.networkParameters = networkParameters; } @Override public AddressValidationResult validate(String address) { try { Address.fromString(networkParameters, address); } catch (AddressFormatException ex) { return AddressValidationResult.invalidAddress(ex); } return AddressValidationResult.validAddress(); } } ================================================ FILE: assets/src/main/java/haveno/asset/BitcoinCashAddressValidator.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.asset; /** * Validates a Bitcoin Cash address. */ public class BitcoinCashAddressValidator extends RegexAddressValidator { public BitcoinCashAddressValidator() { super("^([13][a-km-zA-HJ-NP-Z1-9]{25,34})|^((bitcoincash:)?(q|p)[a-z0-9]{41})|^((BITCOINCASH:)?(Q|P)[A-Z0-9]{41})$"); } public BitcoinCashAddressValidator(String errorMessageI18nKey) { super("^([13][a-km-zA-HJ-NP-Z1-9]{25,34})|^((bitcoincash:)?(q|p)[a-z0-9]{41})|^((BITCOINCASH:)?(Q|P)[A-Z0-9]{41})$", errorMessageI18nKey); } } ================================================ FILE: assets/src/main/java/haveno/asset/CardanoAddressValidator.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.asset; /** * Validates a Shelley-era mainnet Cardano address. */ public class CardanoAddressValidator extends RegexAddressValidator { private static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; private static final int BECH32_CONST = 1; private static final int BECH32M_CONST = 0x2bc830a3; private static final int MAX_LEN = 104; // bech32 / bech32m max for Cardano public CardanoAddressValidator() { super("^addr1[0-9a-z]{20,98}$"); } public CardanoAddressValidator(String errorMessageI18nKey) { super("^addr1[0-9a-z]{20,98}$", errorMessageI18nKey); } @Override public AddressValidationResult validate(String address) { if (!isValidShelleyMainnet(address)) { return AddressValidationResult.invalidStructure(); } return super.validate(address); } /** * Checks if the given address is a valid Shelley-era mainnet Cardano address. * * This code is AI-generated and has been tested with a variety of addresses. * * @param addr the address to validate * @return true if the address is valid, false otherwise */ private static boolean isValidShelleyMainnet(String addr) { if (addr == null) return false; String lower = addr.toLowerCase(); // must start addr1 and not be absurdly long if (!lower.startsWith("addr1") || lower.length() > MAX_LEN) return false; int sep = lower.lastIndexOf('1'); if (sep < 1) return false; // no separator or empty HRP String hrp = lower.substring(0, sep); if (!"addr".equals(hrp)) return false; // mainnet only String dataPart = lower.substring(sep + 1); if (dataPart.length() < 6) return false; // checksum is 6 chars minimum int[] data = new int[dataPart.length()]; for (int i = 0; i < dataPart.length(); i++) { int v = CHARSET.indexOf(dataPart.charAt(i)); if (v == -1) return false; data[i] = v; } int[] hrpExp = hrpExpand(hrp); int[] combined = new int[hrpExp.length + data.length]; System.arraycopy(hrpExp, 0, combined, 0, hrpExp.length); System.arraycopy(data, 0, combined, hrpExp.length, data.length); int chk = polymod(combined); return chk == BECH32_CONST || chk == BECH32M_CONST; // accept either legacy Bech32 (1) or Bech32m (0x2bc830a3) } private static int[] hrpExpand(String hrp) { int[] ret = new int[hrp.length() * 2 + 1]; int idx = 0; for (char c : hrp.toCharArray()) ret[idx++] = c >> 5; ret[idx++] = 0; for (char c : hrp.toCharArray()) ret[idx++] = c & 31; return ret; } private static int polymod(int[] values) { int chk = 1; int[] GEN = {0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3}; for (int v : values) { int b = chk >>> 25; chk = ((chk & 0x1ffffff) << 5) ^ v; for (int i = 0; i < 5; i++) { if (((b >>> i) & 1) != 0) chk ^= GEN[i]; } } return chk; } } ================================================ FILE: assets/src/main/java/haveno/asset/Coin.java ================================================ /* * 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 . */ package haveno.asset; /** * Abstract base class for {@link Asset}s with their own dedicated blockchain, such as * {@link haveno.asset.coins.Bitcoin}, {@link haveno.asset.coins.Ether}, and {@link haveno.asset.coins.Monero}. *

* In addition to the usual {@code Asset} properties, a {@code Coin} maintains information * about which {@link Network} it may be used on. By default, coins are constructed with * the assumption they are for use on that coin's "main network", or "main blockchain", * i.e. that they are "real" coins for use in a production environment. In testing * scenarios, however, a coin may be constructed for use only on "testnet" or "stagenet" * networks. * * @author Chris Beams * @since 0.7.0 */ public abstract class Coin extends AbstractAsset { public enum Network { MAINNET, TESTNET, STAGENET } private final Network network; public Coin(String name, String tickerSymbol, AddressValidator addressValidator) { this(name, tickerSymbol, addressValidator, Network.MAINNET); } public Coin(String name, String tickerSymbol, AddressValidator addressValidator, Network network) { super(name, tickerSymbol, addressValidator); this.network = network; } public Network getNetwork() { return network; } } ================================================ FILE: assets/src/main/java/haveno/asset/CryptoAccountDisclaimer.java ================================================ package haveno.asset; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * When a new PaymentAccount is created for given asset, this annotation tells UI to show user a disclaimer message * with requirements needed to be fulfilled when conducting trade given payment method. * * I.e. in case of Monero user must use official Monero GUI wallet or Monero CLI wallet with certain options enabled, * user needs to keep tx private key, tx hash, recipient's address, etc. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface CryptoAccountDisclaimer { /** * Translation key of the message to show, i.e. "account.crypto.popup.xmr.msg" * @return translation key */ String value(); } ================================================ FILE: assets/src/main/java/haveno/asset/CryptoNoteAddressValidator.java ================================================ /* * 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 . */ package haveno.asset; /** * {@link AddressValidator} for Base58-encoded Cryptonote addresses. * * @author Xiphon */ public class CryptoNoteAddressValidator implements AddressValidator { private final long[] validPrefixes; private final boolean validateChecksum; public CryptoNoteAddressValidator(boolean validateChecksum, long... validPrefixes) { this.validPrefixes = validPrefixes; this.validateChecksum = validateChecksum; } public CryptoNoteAddressValidator(long... validPrefixes) { this(true, validPrefixes); } @Override public AddressValidationResult validate(String address) { try { long prefix = CryptoNoteUtils.MoneroBase58.decodeAddress(address, this.validateChecksum); for (long validPrefix : this.validPrefixes) { if (prefix == validPrefix) { return AddressValidationResult.validAddress(); } } return AddressValidationResult.invalidAddress(String.format("invalid address prefix %x", prefix)); } catch (Exception e) { return AddressValidationResult.invalidStructure(); } } } ================================================ FILE: assets/src/main/java/haveno/asset/CryptoNoteUtils.java ================================================ /* * 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 . */ package haveno.asset; import org.bitcoinj.core.Utils; import java.math.BigInteger; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; import java.util.Map; public class CryptoNoteUtils { public static String getRawSpendKeyAndViewKey(String address) throws CryptoNoteUtils.CryptoNoteException { try { // See https://monerodocs.org/public-address/standard-address/ byte[] decoded = CryptoNoteUtils.MoneroBase58.decode(address); // Standard addresses are of length 69 and addresses with integrated payment ID of length 77. if (decoded.length <= 65) { throw new CryptoNoteUtils.CryptoNoteException("The address we received is too short. address=" + address); } // If the length is not as expected but still can be truncated we log an error and continue. if (decoded.length != 69 && decoded.length != 77) { System.out.println("The address we received is not in the expected format. address=" + address); } // We remove the network type byte, the checksum (4 bytes) and optionally the payment ID (8 bytes if present) // So extract the 64 bytes after the first byte, which are the 32 byte public spend key + the 32 byte public view key byte[] slice = Arrays.copyOfRange(decoded, 1, 65); return Utils.HEX.encode(slice); } catch (CryptoNoteUtils.CryptoNoteException e) { throw new CryptoNoteUtils.CryptoNoteException(e); } } public static class CryptoNoteException extends Exception { CryptoNoteException(String msg) { super(msg); } public CryptoNoteException(CryptoNoteException exception) { super(exception); } } static class Keccak { private static final int BLOCK_SIZE = 136; private static final int LONGS_PER_BLOCK = BLOCK_SIZE / 8; private static final int KECCAK_ROUNDS = 24; private static final long[] KECCAKF_RNDC = { 0x0000000000000001L, 0x0000000000008082L, 0x800000000000808aL, 0x8000000080008000L, 0x000000000000808bL, 0x0000000080000001L, 0x8000000080008081L, 0x8000000000008009L, 0x000000000000008aL, 0x0000000000000088L, 0x0000000080008009L, 0x000000008000000aL, 0x000000008000808bL, 0x800000000000008bL, 0x8000000000008089L, 0x8000000000008003L, 0x8000000000008002L, 0x8000000000000080L, 0x000000000000800aL, 0x800000008000000aL, 0x8000000080008081L, 0x8000000000008080L, 0x0000000080000001L, 0x8000000080008008L }; private static final int[] KECCAKF_ROTC = { 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44 }; private static final int[] KECCAKF_PILN = { 10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1 }; private static long rotateLeft(long value, int shift) { return (value << shift) | (value >>> (64 - shift)); } private static void keccakf(long[] st, int rounds) { long[] bc = new long[5]; for (int round = 0; round < rounds; ++round) { for (int i = 0; i < 5; ++i) { bc[i] = st[i] ^ st[i + 5] ^ st[i + 10] ^ st[i + 15] ^ st[i + 20]; } for (int i = 0; i < 5; i++) { long t = bc[(i + 4) % 5] ^ rotateLeft(bc[(i + 1) % 5], 1); for (int j = 0; j < 25; j += 5) { st[j + i] ^= t; } } long t = st[1]; for (int i = 0; i < 24; ++i) { int j = KECCAKF_PILN[i]; bc[0] = st[j]; st[j] = rotateLeft(t, KECCAKF_ROTC[i]); t = bc[0]; } for (int j = 0; j < 25; j += 5) { for (int i = 0; i < 5; i++) { bc[i] = st[j + i]; } for (int i = 0; i < 5; i++) { st[j + i] ^= (~bc[(i + 1) % 5]) & bc[(i + 2) % 5]; } } st[0] ^= KECCAKF_RNDC[round]; } } static ByteBuffer keccak1600(ByteBuffer input) { input.order(ByteOrder.LITTLE_ENDIAN); int fullBlocks = input.remaining() / BLOCK_SIZE; long[] st = new long[25]; for (int block = 0; block < fullBlocks; ++block) { for (int index = 0; index < LONGS_PER_BLOCK; ++index) { st[index] ^= input.getLong(); } keccakf(st, KECCAK_ROUNDS); } ByteBuffer lastBlock = ByteBuffer.allocate(144).order(ByteOrder.LITTLE_ENDIAN); lastBlock.put(input); lastBlock.put((byte) 1); int paddingOffset = BLOCK_SIZE - 1; lastBlock.put(paddingOffset, (byte) (lastBlock.get(paddingOffset) | 0x80)); lastBlock.rewind(); for (int index = 0; index < LONGS_PER_BLOCK; ++index) { st[index] ^= lastBlock.getLong(); } keccakf(st, KECCAK_ROUNDS); ByteBuffer result = ByteBuffer.allocate(32); result.slice().order(ByteOrder.LITTLE_ENDIAN).asLongBuffer().put(st, 0, 4); return result; } } static class MoneroBase58 { private static final String ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; private static final BigInteger ALPHABET_SIZE = BigInteger.valueOf(ALPHABET.length()); private static final int FULL_DECODED_BLOCK_SIZE = 8; private static final int FULL_ENCODED_BLOCK_SIZE = 11; private static final BigInteger UINT64_MAX = new BigInteger("18446744073709551615"); private static final Map DECODED_CHUNK_LENGTH = Map.of(2, 1, 3, 2, 5, 3, 6, 4, 7, 5, 9, 6, 10, 7, 11, 8); private static void decodeChunk(String input, int inputOffset, int inputLength, byte[] decoded, int decodedOffset, int decodedLength) throws CryptoNoteException { BigInteger result = BigInteger.ZERO; BigInteger order = BigInteger.ONE; for (int index = inputOffset + inputLength; index != inputOffset; order = order.multiply(ALPHABET_SIZE)) { char character = input.charAt(--index); int digit = ALPHABET.indexOf(character); if (digit == -1) { throw new CryptoNoteException("invalid character " + character); } result = result.add(order.multiply(BigInteger.valueOf(digit))); if (result.compareTo(UINT64_MAX) > 0) { throw new CryptoNoteException("64-bit unsigned integer overflow " + result.toString()); } } BigInteger maxCapacity = BigInteger.ONE.shiftLeft(8 * decodedLength); if (result.compareTo(maxCapacity) >= 0) { throw new CryptoNoteException("capacity overflow " + result.toString()); } for (int index = decodedOffset + decodedLength; index != decodedOffset; result = result.shiftRight(8)) { decoded[--index] = result.byteValue(); } } public static byte[] decode(String input) throws CryptoNoteException { if (input.length() == 0) { return new byte[0]; } int chunks = input.length() / FULL_ENCODED_BLOCK_SIZE; int lastEncodedSize = input.length() % FULL_ENCODED_BLOCK_SIZE; int lastChunkSize = lastEncodedSize > 0 ? DECODED_CHUNK_LENGTH.get(lastEncodedSize) : 0; byte[] result = new byte[chunks * FULL_DECODED_BLOCK_SIZE + lastChunkSize]; int inputOffset = 0; int resultOffset = 0; for (int chunk = 0; chunk < chunks; ++chunk, inputOffset += FULL_ENCODED_BLOCK_SIZE, resultOffset += FULL_DECODED_BLOCK_SIZE) { decodeChunk(input, inputOffset, FULL_ENCODED_BLOCK_SIZE, result, resultOffset, FULL_DECODED_BLOCK_SIZE); } if (lastChunkSize > 0) { decodeChunk(input, inputOffset, lastEncodedSize, result, resultOffset, lastChunkSize); } return result; } private static long readVarInt(ByteBuffer buffer) { long result = 0; for (int shift = 0; ; shift += 7) { byte current = buffer.get(); result += (current & 0x7fL) << shift; if ((current & 0x80L) == 0) { break; } } return result; } static long decodeAddress(String address, boolean validateChecksum) throws CryptoNoteException { byte[] decoded = decode(address); int checksumSize = 4; if (decoded.length < checksumSize) { throw new CryptoNoteException("invalid length"); } ByteBuffer decodedAddress = ByteBuffer.wrap(decoded, 0, decoded.length - checksumSize); long prefix = readVarInt(decodedAddress.slice()); if (!validateChecksum) { return prefix; } ByteBuffer fastHash = Keccak.keccak1600(decodedAddress.slice()); int checksum = fastHash.getInt(); int expected = ByteBuffer.wrap(decoded, decoded.length - checksumSize, checksumSize).getInt(); if (checksum != expected) { throw new CryptoNoteException(String.format("invalid checksum %08X, expected %08X", checksum, expected)); } return prefix; } } } ================================================ FILE: assets/src/main/java/haveno/asset/Erc20Token.java ================================================ /* * 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 . */ package haveno.asset; /** * Abstract base class for Ethereum-based {@link Token}s that implement the * ERC-20 Token * Standard. * * @author Chris Beams * @since 0.7.0 */ public abstract class Erc20Token extends Token { public Erc20Token(String name, String tickerSymbol) { super(name, tickerSymbol, new EtherAddressValidator()); } } ================================================ FILE: assets/src/main/java/haveno/asset/EtherAddressValidator.java ================================================ /* * 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 . */ package haveno.asset; /** * Validates an Ethereum address using the regular expression on record in the * * ethereum/web3.js project. Note that this implementation is widely used, not just * for actual {@link haveno.asset.coins.Ether} address validation, but also for * {@link Erc20Token} implementations and other Ethereum-based {@link Asset} * implementations. * * @author Chris Beams * @since 0.7.0 */ public class EtherAddressValidator extends RegexAddressValidator { public EtherAddressValidator() { super("^(0x)?[0-9a-fA-F]{40}$"); } public EtherAddressValidator(String errorMessageI18nKey) { super("^(0x)?[0-9a-fA-F]{40}$", errorMessageI18nKey); } } ================================================ FILE: assets/src/main/java/haveno/asset/GrinAddressValidator.java ================================================ /* * 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 . */ package haveno.asset; /** * We only support the grinbox format as it is currently the only tool which offers a validation options of sender. * Beside that is the IP:port format very insecure with MITM attacks. * * Here is the information from a conversation with the Grinbox developer regarding the Grinbox address format. * A Grinbox address is of the format: grinbox://@domain.com:port where everything besides is optional. If no domain is specified, the default relay grinbox.io will be used. The is a base58check encoded value (like in Bitcoin). For Grin mainnet, the first 2 bytes will be [1, 11] and the following 33 bytes should be a valid secp256k1 compressed public key. Some examples of valid addresses are: gVvRNiuopubvxPrs1BzJdQjVdFAxmkLzMqiVJzUZ7ubznhdtNTGB gVvUcSafSTD3YTSqgNf9ojEYWkz3zMZNfsjdpdb9en5mxc6gmja6 gVvk7rLBg3r3qoWYL3VsREnBbooT7nynxx5HtDvUWCJUaNCnddvY grinbox://gVtWzX5NTLCBkyNV19QVdnLXue13heAVRD36sfkGD6xpqy7k7e4a gVw9TWimGFXRjoDXWhWxeNQbu84ZpLkvnenkKvA5aJeDo31eM5tC@somerelay.com grinbox://gVwjSsYW5vvHpK4AunJ5piKhhQTV6V3Jb818Uqs6PdC3SsB36AsA@somerelay.com:1220 Some examples of invalid addresses are: gVuBJDKcWkhueMfBLAbFwV4ax55YXPeinWXdRME1Zi3eiC6sFNye (invalid checksum) geWGCMQjxZMHG3EtTaRbR7rH9rE4DsmLfpm1iiZEa7HFKjjkgpf2 (wrong version bytes) gVvddC2jYAfxTxnikcbTEQKLjhJZpqpBg39tXkwAKnD2Pys2mWiK (invalid public key) We only add the basic validation without checksum, version byte and pubkey validation as that would require much more effort. Any Grin developer is welcome to add that though! */ public class GrinAddressValidator implements AddressValidator { // A Grin Wallet URL (address is not the correct term) can be in the form IP:port or a grinbox format. // The grinbox has the format grinbox://@domain.com:port where everything beside the key is optional. // Regex for IP validation borrowed from https://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses private static final String PORT = "((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$"; private static final String DOMAIN = "[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\\.[a-zA-Z]{2,}$"; private static final String KEY = "[a-km-zA-HJ-NP-Z1-9]{52}$"; public GrinAddressValidator() { } @Override public AddressValidationResult validate(String address) { if (address == null || address.length() == 0) return AddressValidationResult.invalidAddress("Address may not be empty (only Grinbox format is supported)"); // We only support grinbox address String key; String domain = null; String port = null; address = address.replace("grinbox://", ""); if (address.contains("@")) { String[] keyAndDomain = address.split("@"); key = keyAndDomain[0]; if (keyAndDomain.length > 1) { domain = keyAndDomain[1]; if (domain.contains(":")) { String[] domainAndPort = domain.split(":"); domain = domainAndPort[0]; if (domainAndPort.length > 1) port = domainAndPort[1]; } } } else { key = address; } if (!key.matches("^" + KEY)) return AddressValidationResult.invalidAddress("Invalid key (only Grinbox format is supported)"); if (domain != null && !domain.matches("^" + DOMAIN)) return AddressValidationResult.invalidAddress("Invalid domain (only Grinbox format is supported)"); if (port != null && !port.matches("^" + PORT)) return AddressValidationResult.invalidAddress("Invalid port (only Grinbox format is supported)"); return AddressValidationResult.validAddress(); } } ================================================ FILE: assets/src/main/java/haveno/asset/I18n.java ================================================ package haveno.asset; import java.util.ResourceBundle; public class I18n { public static ResourceBundle DISPLAY_STRINGS = ResourceBundle.getBundle("i18n.displayStrings-assets"); } ================================================ FILE: assets/src/main/java/haveno/asset/LiquidBitcoinAddressValidator.java ================================================ package haveno.asset; public class LiquidBitcoinAddressValidator extends RegexAddressValidator { static private final String REGEX = "^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,87}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,87})$"; public LiquidBitcoinAddressValidator() { super(REGEX, "validation.crypto.liquidBitcoin.invalidAddress"); } } ================================================ FILE: assets/src/main/java/haveno/asset/NetworkParametersAdapter.java ================================================ /* * 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 . */ package haveno.asset; import org.bitcoinj.core.BitcoinSerializer; import org.bitcoinj.core.Block; import org.bitcoinj.core.Coin; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.StoredBlock; import org.bitcoinj.core.VerificationException; import org.bitcoinj.store.BlockStore; import org.bitcoinj.utils.MonetaryFormat; /** * Convenient abstract {@link NetworkParameters} base class providing no-op * implementations of all methods that are not required for address validation * purposes. * * @author Chris Beams * @since 0.7.0 */ public abstract class NetworkParametersAdapter extends NetworkParameters { @Override public String getPaymentProtocolId() { return PAYMENT_PROTOCOL_ID_MAINNET; } @Override public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException { } @Override public Coin getMaxMoney() { return null; } @Override public Coin getMinNonDustOutput() { return null; } @Override public MonetaryFormat getMonetaryFormat() { return null; } @Override public String getUriScheme() { return null; } @Override public boolean hasMaxMoney() { return false; } @Override public BitcoinSerializer getSerializer(boolean parseRetain) { return null; } @Override public int getProtocolVersionNum(ProtocolVersion version) { return 0; } } ================================================ FILE: assets/src/main/java/haveno/asset/PrintTool.java ================================================ /* * 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 . */ package haveno.asset; import java.util.Comparator; public class PrintTool { public static void main(String[] args) { // Prints out all coins in the format used in the FAQ webpage. // Run that and copy paste the result to the FAQ webpage at new releases. StringBuilder sb = new StringBuilder(); new AssetRegistry().stream() .sorted(Comparator.comparing(o -> o.getName().toLowerCase())) .filter(e -> !e.getTickerSymbol().equals("BTC")) .map(e -> new Pair(e.getName(), e.getTickerSymbol())) // We want to get rid of duplicated entries for regtest/testnet... .distinct() .forEach(e -> sb.append("

  • “") .append(e.right) .append("”, “") .append(e.left) .append("”
  • ") .append("\n")); System.out.println(sb.toString()); } private static class Pair { final String left; final String right; Pair(String left, String right) { this.left = left; this.right = right; } } } ================================================ FILE: assets/src/main/java/haveno/asset/RegexAddressValidator.java ================================================ /* * 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 . */ package haveno.asset; /** * Validates an {@link Asset} address against a given regular expression. * * @author Chris Beams * @since 0.7.0 */ public class RegexAddressValidator implements AddressValidator { private final String regex; private final String errorMessageI18nKey; public RegexAddressValidator(String regex) { this(regex, null); } public RegexAddressValidator(String regex, String errorMessageI18nKey) { this.regex = regex; this.errorMessageI18nKey = errorMessageI18nKey; } @Override public AddressValidationResult validate(String address) { if (!address.matches(regex)) if (errorMessageI18nKey == null) return AddressValidationResult.invalidStructure(); else return AddressValidationResult.invalidAddress("", errorMessageI18nKey); return AddressValidationResult.validAddress(); } } ================================================ FILE: assets/src/main/java/haveno/asset/RippleAddressValidator.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.asset; /** * Validates a Ripple address using a regular expression. */ public class RippleAddressValidator extends RegexAddressValidator { public RippleAddressValidator() { super("^r[1-9A-HJ-NP-Za-km-z]{25,34}$"); } public RippleAddressValidator(String errorMessageI18nKey) { super("^r[1-9A-HJ-NP-Za-km-z]{25,34}$", errorMessageI18nKey); } } ================================================ FILE: assets/src/main/java/haveno/asset/SolanaAddressValidator.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.asset; import java.math.BigInteger; /** * Validates a Solana address. */ public class SolanaAddressValidator implements AddressValidator { private static final String BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; public SolanaAddressValidator() { } @Override public AddressValidationResult validate(String address) { if (!isValidSolanaAddress(address)) { return AddressValidationResult.invalidStructure(); } return AddressValidationResult.validAddress(); } /** * Checks if the given address is a valid Solana address. * * This code is AI-generated and has been tested with a variety of addresses. * * @param addr the address to validate * @return true if the address is valid, false otherwise */ private static boolean isValidSolanaAddress(String address) { if (address == null) return false; if (address.length() < 32 || address.length() > 44) return false; // typical Solana length range // Check all chars are base58 valid for (char c : address.toCharArray()) { if (BASE58_ALPHABET.indexOf(c) == -1) return false; } // Decode from base58 and ensure exactly 32 bytes byte[] decoded = decodeBase58(address); return decoded != null && decoded.length == 32; } private static byte[] decodeBase58(String input) { BigInteger num = BigInteger.ZERO; BigInteger base = BigInteger.valueOf(58); for (char c : input.toCharArray()) { int digit = BASE58_ALPHABET.indexOf(c); if (digit < 0) return null; // invalid char num = num.multiply(base).add(BigInteger.valueOf(digit)); } // Convert BigInteger to byte array byte[] bytes = num.toByteArray(); // Remove sign byte if present if (bytes.length > 1 && bytes[0] == 0) { byte[] tmp = new byte[bytes.length - 1]; System.arraycopy(bytes, 1, tmp, 0, tmp.length); bytes = tmp; } // Count leading '1's and add leading zero bytes int leadingZeros = 0; for (char c : input.toCharArray()) { if (c == '1') leadingZeros++; else break; } byte[] result = new byte[leadingZeros + bytes.length]; System.arraycopy(bytes, 0, result, leadingZeros, bytes.length); return result; } } ================================================ FILE: assets/src/main/java/haveno/asset/Token.java ================================================ /* * 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 . */ package haveno.asset; /** * Abstract base class for {@link Asset}s that do not have their own dedicated blockchain, * but are rather based on or derived from another blockchain. Contrast with {@link Coin}. * Note that this is essentially a "marker" base class in the sense that it (currently) * exposes no additional information or functionality beyond that found in * {@link AbstractAsset}, but it is nevertheless useful in distinguishing between major * different {@code Asset} types. * * @author Chris Beams * @since 0.7.0 * @see Erc20Token */ public abstract class Token extends AbstractAsset { public Token(String name, String tickerSymbol, AddressValidator addressValidator) { super(name, tickerSymbol, addressValidator); } } ================================================ FILE: assets/src/main/java/haveno/asset/Trc20Token.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.asset; /** * Abstract base class for Tron-based {@link Token}s that implement the * TRC-20 Token Standard. */ public abstract class Trc20Token extends Token { public Trc20Token(String name, String tickerSymbol) { super(name, tickerSymbol, new RegexAddressValidator("T[A-Za-z1-9]{33}")); } } ================================================ FILE: assets/src/main/java/haveno/asset/TronAddressValidator.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.asset; import java.math.BigInteger; import java.security.MessageDigest; import java.util.Arrays; /** * Validates a Tron address. */ public class TronAddressValidator implements AddressValidator { private static final String BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; private static final byte MAINNET_PREFIX = 0x41; public TronAddressValidator() { } @Override public AddressValidationResult validate(String address) { if (!isValidTronAddress(address)) { return AddressValidationResult.invalidStructure(); } return AddressValidationResult.validAddress(); } /** * Checks if the given address is a valid Solana address. * * This code is AI-generated and has been tested with a variety of addresses. * * @param addr the address to validate * @return true if the address is valid, false otherwise */ private static boolean isValidTronAddress(String address) { if (address == null || address.length() != 34) return false; byte[] decoded = decodeBase58(address); if (decoded == null || decoded.length != 25) return false; // 21 bytes data + 4 bytes checksum // Check checksum byte[] data = Arrays.copyOfRange(decoded, 0, 21); byte[] checksum = Arrays.copyOfRange(decoded, 21, 25); byte[] calculatedChecksum = Arrays.copyOfRange(doubleSHA256(data), 0, 4); if (!Arrays.equals(checksum, calculatedChecksum)) return false; // Check mainnet prefix return data[0] == MAINNET_PREFIX; } private static byte[] decodeBase58(String input) { BigInteger num = BigInteger.ZERO; BigInteger base = BigInteger.valueOf(58); for (char c : input.toCharArray()) { int digit = BASE58_ALPHABET.indexOf(c); if (digit < 0) return null; num = num.multiply(base).add(BigInteger.valueOf(digit)); } // Convert BigInteger to byte array byte[] bytes = num.toByteArray(); if (bytes.length > 1 && bytes[0] == 0) { bytes = Arrays.copyOfRange(bytes, 1, bytes.length); } // Add leading zero bytes for '1's int leadingZeros = 0; for (char c : input.toCharArray()) { if (c == '1') leadingZeros++; else break; } byte[] result = new byte[leadingZeros + bytes.length]; System.arraycopy(bytes, 0, result, leadingZeros, bytes.length); return result; } private static byte[] doubleSHA256(byte[] data) { try { MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); return sha256.digest(sha256.digest(data)); } catch (Exception e) { throw new RuntimeException(e); } } } ================================================ FILE: assets/src/main/java/haveno/asset/coins/Bitcoin.java ================================================ /* * 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 . */ package haveno.asset.coins; import haveno.asset.BitcoinAddressValidator; import haveno.asset.Coin; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.params.MainNetParams; import org.bitcoinj.params.RegTestParams; import org.bitcoinj.params.TestNet3Params; public abstract class Bitcoin extends Coin { public Bitcoin(Network network, NetworkParameters networkParameters) { super("Bitcoin", "BTC", new BitcoinAddressValidator(networkParameters), network); } public static class Mainnet extends Bitcoin { public Mainnet() { super(Network.MAINNET, MainNetParams.get()); } } public static class Testnet extends Bitcoin { public Testnet() { super(Network.TESTNET, TestNet3Params.get()); } } public static class Regtest extends Bitcoin { public Regtest() { super(Network.STAGENET, RegTestParams.get()); } } } ================================================ FILE: assets/src/main/java/haveno/asset/coins/BitcoinCash.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.asset.coins; import haveno.asset.BitcoinCashAddressValidator; import haveno.asset.Coin; public class BitcoinCash extends Coin { public BitcoinCash() { super("Bitcoin Cash", "BCH", new BitcoinCashAddressValidator()); } } ================================================ FILE: assets/src/main/java/haveno/asset/coins/Cardano.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.asset.coins; import haveno.asset.CardanoAddressValidator; import haveno.asset.Coin; public class Cardano extends Coin { public Cardano() { super("Cardano", "ADA", new CardanoAddressValidator()); } } ================================================ FILE: assets/src/main/java/haveno/asset/coins/Dogecoin.java ================================================ /* * 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 . */ package haveno.asset.coins; import haveno.asset.Base58AddressValidator; import haveno.asset.Coin; import haveno.asset.NetworkParametersAdapter; public class Dogecoin extends Coin { public Dogecoin() { super("Dogecoin", "DOGE", new Base58AddressValidator(new DogecoinMainNetParams()), Network.MAINNET); } public static class DogecoinMainNetParams extends NetworkParametersAdapter { public DogecoinMainNetParams() { this.addressHeader = 30; this.p2shHeader = 22; } } } ================================================ FILE: assets/src/main/java/haveno/asset/coins/Ether.java ================================================ /* * 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 . */ package haveno.asset.coins; import haveno.asset.Coin; import haveno.asset.EtherAddressValidator; public class Ether extends Coin { public Ether() { super("Ether", "ETH", new EtherAddressValidator()); } } ================================================ FILE: assets/src/main/java/haveno/asset/coins/Litecoin.java ================================================ /* * 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 . */ package haveno.asset.coins; import haveno.asset.BitcoinAddressValidator; import haveno.asset.Coin; import haveno.asset.NetworkParametersAdapter; public class Litecoin extends Coin { public Litecoin() { super("Litecoin", "LTC", new BitcoinAddressValidator(new LitecoinMainNetParams()), Network.MAINNET); } public static class LitecoinMainNetParams extends NetworkParametersAdapter { public LitecoinMainNetParams() { this.addressHeader = 48; this.p2shHeader = 50; this.segwitAddressHrp = "ltc"; } } } ================================================ FILE: assets/src/main/java/haveno/asset/coins/Monero.java ================================================ /* * 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 . */ package haveno.asset.coins; import haveno.asset.CryptoAccountDisclaimer; import haveno.asset.Coin; import haveno.asset.CryptoNoteAddressValidator; @CryptoAccountDisclaimer("account.crypto.popup.xmr.msg") public class Monero extends Coin { public Monero() { super("Monero", "XMR", new CryptoNoteAddressValidator(18, 19, 42)); } } ================================================ FILE: assets/src/main/java/haveno/asset/coins/Ripple.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.asset.coins; import haveno.asset.Coin; import haveno.asset.RippleAddressValidator; public class Ripple extends Coin { public Ripple() { super("Ripple", "XRP", new RippleAddressValidator()); } } ================================================ FILE: assets/src/main/java/haveno/asset/coins/Solana.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.asset.coins; import haveno.asset.Coin; import haveno.asset.SolanaAddressValidator; public class Solana extends Coin { public Solana() { super("Solana", "SOL", new SolanaAddressValidator()); } } ================================================ FILE: assets/src/main/java/haveno/asset/coins/Tron.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.asset.coins; import haveno.asset.Coin; import haveno.asset.TronAddressValidator; public class Tron extends Coin { public Tron() { super("Tron", "TRX", new TronAddressValidator()); } } ================================================ FILE: assets/src/main/java/haveno/asset/package-info.java ================================================ /* * 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 . */ /** * Haveno's family of abstractions representing different ("crypto") * {@link haveno.asset.Asset} types such as {@link haveno.asset.Coin}, * {@link haveno.asset.Token} and {@link haveno.asset.Erc20Token}, as well as concrete * implementations of each, such as {@link haveno.asset.coins.Bitcoin} itself, cryptos like * {@link haveno.asset.coins.Litecoin} and {@link haveno.asset.coins.Ether} and tokens like * {@link haveno.asset.tokens.DaiStablecoinERC20}. *

    * The purpose of this package is to provide everything necessary for registering * ("listing") new assets and managing / accessing those assets within, e.g. the Haveno * Desktop UI. *

    * Note that everything within this package is intentionally designed to be simple and * low-level with no dependencies on any other Haveno packages or components. * * @author Chris Beams * @since 0.7.0 */ package haveno.asset; ================================================ FILE: assets/src/main/java/haveno/asset/tokens/AugmintEuro.java ================================================ /* * 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 . */ package haveno.asset.tokens; import haveno.asset.Erc20Token; public class AugmintEuro extends Erc20Token { public AugmintEuro() { super("Augmint Euro", "AEUR"); } } ================================================ FILE: assets/src/main/java/haveno/asset/tokens/DaiStablecoinERC20.java ================================================ /* * 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 . */ package haveno.asset.tokens; import haveno.asset.Erc20Token; public class DaiStablecoinERC20 extends Erc20Token { public DaiStablecoinERC20() { super("Dai Stablecoin", "DAI-ERC20"); } } ================================================ FILE: assets/src/main/java/haveno/asset/tokens/EtherStone.java ================================================ /* * 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 . */ package haveno.asset.tokens; import haveno.asset.Erc20Token; public class EtherStone extends Erc20Token { public EtherStone() { super("EtherStone", "ETHS"); } } ================================================ FILE: assets/src/main/java/haveno/asset/tokens/TetherUSDERC20.java ================================================ package haveno.asset.tokens; import haveno.asset.Erc20Token; public class TetherUSDERC20 extends Erc20Token { public TetherUSDERC20() { // If you add a new USDT variant or want to change this ticker symbol you should also look here: // core/src/main/java/haveno/core/provider/price/PriceProvider.java:getAll() super("Tether USD", "USDT-ERC20"); } } ================================================ FILE: assets/src/main/java/haveno/asset/tokens/TetherUSDTRC20.java ================================================ package haveno.asset.tokens; import haveno.asset.Trc20Token; public class TetherUSDTRC20 extends Trc20Token { public TetherUSDTRC20() { // If you add a new USDT variant or want to change this ticker symbol you should also look here: // core/src/main/java/haveno/core/provider/price/PriceProvider.java:getAll() super("Tether USD", "USDT-TRC20"); } } ================================================ FILE: assets/src/main/java/haveno/asset/tokens/TrueUSD.java ================================================ /* * 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 . */ package haveno.asset.tokens; import haveno.asset.Erc20Token; public class TrueUSD extends Erc20Token { public TrueUSD() { super("TrueUSD", "TUSD"); } } ================================================ FILE: assets/src/main/java/haveno/asset/tokens/USDCoinERC20.java ================================================ /* * 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 . */ package haveno.asset.tokens; import haveno.asset.Erc20Token; public class USDCoinERC20 extends Erc20Token { public USDCoinERC20() { super("USD Coin", "USDC-ERC20"); } } ================================================ FILE: assets/src/main/java/haveno/asset/tokens/VectorspaceAI.java ================================================ /* * 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 . */ package haveno.asset.tokens; import haveno.asset.Erc20Token; public class VectorspaceAI extends Erc20Token { public VectorspaceAI() { super("VectorspaceAI", "VXV"); } } ================================================ FILE: assets/src/main/resources/META-INF/services/haveno.asset.Asset ================================================ # All assets available for trading on the Haveno network. # Contents are sorted according to the output of `sort --ignore-case --dictionary-order`. # See haveno.asset.Asset and haveno.asset.AssetRegistry for further details. # See https://haveno.exchange/list-asset for complete instructions. haveno.asset.coins.Bitcoin$Mainnet haveno.asset.coins.BitcoinCash haveno.asset.coins.Cardano haveno.asset.coins.Dogecoin haveno.asset.coins.Ether haveno.asset.coins.Litecoin haveno.asset.coins.Monero haveno.asset.coins.Ripple haveno.asset.coins.Solana haveno.asset.coins.Tron haveno.asset.tokens.TetherUSDERC20 haveno.asset.tokens.TetherUSDTRC20 haveno.asset.tokens.USDCoinERC20 haveno.asset.tokens.DaiStablecoinERC20 ================================================ FILE: assets/src/main/resources/i18n/displayStrings-assets.properties ================================================ # Keep display strings organized by domain # Naming convention: We use camelCase and dot separated name spaces. # Use as many sub spaces as required to make the structure clear, but as little as possible. # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break # in the display but only in the editor. # Please use in all language files the exact same order of the entries, that way a comparison is easier. # Please try to keep the length of the translated string similar to English. If it is longer it might break layout or # get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. account.crypto.popup.validation.XCP=XCP address must start with '1' and must have 34 characters. account.crypto.popup.validation.DCR=DCR address must start with 'Dk' or 'Ds' or 'De' or 'DS' or 'Dc' or 'Pm' and must have 34 characters. account.crypto.popup.validation.ETC=ETC address must start with '0x' and made up of letters A to F and numbers which are 40 characters long. account.crypto.popup.validation.NMC=NMC address must start with 'N' or 'M' and must be 34 characters long. account.crypto.popup.validation.SF= Siafund address must be made up of letters A to F and numbers which are 76 characters long. account.crypto.popup.validation.UNO=UNO address must start with 'u' and must have 34 characters. account.crypto.popup.validation.XZC=XZC address must start with 'a' and must have 34 characters. ================================================ FILE: assets/src/test/java/haveno/asset/AbstractAssetTest.java ================================================ /* * 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 . */ package haveno.asset; import org.junit.jupiter.api.Test; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; /** * Abstract base class for all {@link Asset} unit tests. Subclasses must implement the * {@link #testValidAddresses()} and {@link #testInvalidAddresses()} methods, and are * expected to use the convenient {@link #assertValidAddress(String)} and * {@link #assertInvalidAddress(String)} assertions when doing so. *

    * Blank / empty addresses are tested automatically by this base class and are always * considered invalid. *

    * This base class also serves as a kind of integration test for {@link AssetRegistry}, in * that all assets tested through subclasses are tested to make sure they are also * properly registered and available there. * * @author Chris Beams * @author Bernard Labno * @since 0.7.0 */ public abstract class AbstractAssetTest { private final AssetRegistry assetRegistry = new AssetRegistry(); protected final Asset asset; public AbstractAssetTest(Asset asset) { this.asset = asset; } @Test public void testPresenceInAssetRegistry() { assertThat(asset + " is not registered in META-INF/services/" + Asset.class.getName(), assetRegistry.stream().anyMatch(this::hasSameTickerSymbol), is(true)); } @Test public void testBlank() { assertInvalidAddress(""); } @Test public abstract void testValidAddresses(); @Test public abstract void testInvalidAddresses(); protected void assertValidAddress(String address) { AddressValidationResult result = asset.validateAddress(address); assertThat(result.getMessage(), result.isValid(), is(true)); } protected void assertInvalidAddress(String address) { assertThat(asset.validateAddress(address).isValid(), is(false)); } private boolean hasSameTickerSymbol(Asset asset) { return this.asset.getTickerSymbol().equals(asset.getTickerSymbol()); } } ================================================ FILE: assets/src/test/java/haveno/asset/coins/BitcoinCashTest.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.asset.coins; import haveno.asset.AbstractAssetTest; import org.junit.jupiter.api.Test; public class BitcoinCashTest extends AbstractAssetTest { public BitcoinCashTest() { super(new BitcoinCash()); } @Test public void testValidAddresses() { assertValidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem"); assertValidAddress("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX"); assertValidAddress("1111111111111111111114oLvT2"); assertValidAddress("1BitcoinEaterAddressDontSendf59kuE"); } @Test public void testInvalidAddresses() { assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhemqq"); assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYheO"); assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhek#"); } } ================================================ FILE: assets/src/test/java/haveno/asset/coins/BitcoinTest.java ================================================ /* * 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 . */ package haveno.asset.coins; import haveno.asset.AbstractAssetTest; import org.junit.jupiter.api.Test; public class BitcoinTest extends AbstractAssetTest { public BitcoinTest() { super(new Bitcoin.Mainnet()); } @Test public void testValidAddresses() { assertValidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem"); assertValidAddress("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX"); assertValidAddress("1111111111111111111114oLvT2"); assertValidAddress("1BitcoinEaterAddressDontSendf59kuE"); assertValidAddress("bc1qj89046x7zv6pm4n00qgqp505nvljnfp6xfznyw"); } @Test public void testInvalidAddresses() { assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhemqq"); assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYheO"); assertInvalidAddress("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhek#"); } } ================================================ FILE: assets/src/test/java/haveno/asset/coins/CardanoTest.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.asset.coins; import haveno.asset.AbstractAssetTest; import org.junit.jupiter.api.Test; public class CardanoTest extends AbstractAssetTest { public CardanoTest() { super(new Cardano()); } @Test public void testValidAddresses() { assertValidAddress("addr1vpu5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5eg0yu80w"); assertValidAddress("addr1q8gg2r3vf9zggn48g7m8vx62rwf6warcs4k7ej8mdzmqmesj30jz7psduyk6n4n2qrud2xlv9fgj53n6ds3t8cs4fvzs05yzmz"); } @Test public void testInvalidAddresses() { assertInvalidAddress("addr1Q9r4y0gx0m4hd5s2u3pnj7ufc4s0ghqzj7u6czxyfks5cty5k5yq5qp6gmw5v7uqvx2g4kw6zjhx4l6fnhcey9lg9nys6v2mpu"); assertInvalidAddress("addr2q9r4y0gx0m4hd5s2u3pnj7ufc4s0ghqzj7u6czxyfks5cty5k5yq5qp6gmw5v7uqvx2g4kw6zjhx4l6fnhcey9lg9nys6v2mpu"); assertInvalidAddress("addr2vpu5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5eg0yu80w"); assertInvalidAddress("Ae2tdPwUPEYxkYw5GrFyqb4Z9TzXo8f1WnWpPZP1sXrEn1pz2VU3CkJ8aTQ"); } } ================================================ FILE: assets/src/test/java/haveno/asset/coins/DogecoinTest.java ================================================ /* * 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 . */ package haveno.asset.coins; import haveno.asset.AbstractAssetTest; import org.junit.jupiter.api.Test; public class DogecoinTest extends AbstractAssetTest { public DogecoinTest() { super(new Dogecoin()); } @Test public void testValidAddresses() { assertValidAddress("DEa7damK8MsbdCJztidBasZKVsDLJifWfE"); assertValidAddress("DNkkfdUvkCDiywYE98MTVp9nQJTgeZAiFr"); assertValidAddress("DDWUYQ3GfMDj8hkx8cbnAMYkTzzAunAQxg"); } @Test public void testInvalidAddresses() { assertInvalidAddress("1DDWUYQ3GfMDj8hkx8cbnAMYkTzzAunAQxg"); assertInvalidAddress("DDWUYQ3GfMDj8hkx8cbnAMYkTzzAunAQxgs"); assertInvalidAddress("DDWUYQ3GfMDj8hkx8cbnAMYkTzzAunAQxg#"); } } ================================================ FILE: assets/src/test/java/haveno/asset/coins/LitecoinTest.java ================================================ /* * 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 . */ package haveno.asset.coins; import haveno.asset.AbstractAssetTest; import org.junit.jupiter.api.Test; public class LitecoinTest extends AbstractAssetTest { public LitecoinTest() { super(new Litecoin()); } @Test public void testValidAddresses() { assertValidAddress("Lg3PX8wRWmApFCoCMAsPF5P9dPHYQHEWKW"); assertValidAddress("LTuoeY6RBHV3n3cfhXVVTbJbxzxnXs9ofm"); assertValidAddress("LgfapHEPhZbRF9pMd5WPT35hFXcZS1USrW"); assertValidAddress("M8T1B2Z97gVdvmfkQcAtYbEepune1tzGua"); assertValidAddress("ltc1qr07zu594qf63xm7l7x6pu3a2v39m2z6hh5pp4t"); assertValidAddress("ltc1qzvcgmntglcuv4smv3lzj6k8szcvsrmvk0phrr9wfq8w493r096ssm2fgsw"); assertValidAddress("MESruSiB2uC9i7tMU6VMUVom91ohM7Rnbd"); assertValidAddress("ltc1q2a0laq2jg2gntzhfs43qptajd325kkx7hrq9cs"); assertValidAddress("ltc1qd6d54mt8xxcg0xg3l0vh6fymdfvd2tv0vnwyrv"); assertValidAddress("ltc1gmay6ht028aurcm680f8e8wxdup07y2tq46f6z2d4v8rutewqmmcqk29jtm"); assertValidAddress("MTf4tP1TCNBn8dNkyxeBVoPrFCcVzxJvvh"); assertValidAddress("LaRoRBC6utQtY3U2FbHwhmhhDPyxodDeKA"); assertValidAddress("MDMFP9Dx84tyaxiYksjvkG1jymBdqCuHGA"); //assertValidAddress("3MSvaVbVFFLML86rt5eqgA9SvW23upaXdY"); // deprecated } @Test public void testInvalidAddresses() { assertInvalidAddress("1LgfapHEPhZbRF9pMd5WPT35hFXcZS1USrW"); assertInvalidAddress("LgfapHEPhZbdRF9pMd5WPT35hFXcZS1USrW"); assertInvalidAddress("LgfapHEPhZbRF9pMd5WPT35hFXcZS1USrW#"); assertInvalidAddress("3MSvaVbVFFLML86rt5eqgl9SvW23upaXdY"); // contains lowercase l assertInvalidAddress("LURw7hYhREXjWHyiXhQNsKInWtPezwNe98"); // contains uppercase I assertInvalidAddress("LM4ch8ZtAowdiGLSnf92MrMOC9dVmve2hr"); // contains uppercase O assertInvalidAddress("MArsfeyS7P0HzsqLpAFGC9pFdhuqHgdL2R"); // contains number 0 assertInvalidAddress("ltc1qr6quwn3v2gxpadd0cu040r9385gayk5vdcyl5"); // too short assertInvalidAddress("ltc1q5det08ke2gpet06wczcdfs2v3hgfqllxw28uln8vxxx82qlue6uswceljma"); // too long assertInvalidAddress("MADpfTtabZ6pDjms4pMd3ZmnrgyhTCo4N8?time=1708476729&exp=86400"); // additional information assertInvalidAddress("ltc1q8tk47lvgqu55h4pfast39r3t9360gmll5z9m6z?time=1708476604&exp=600"); // additional information assertInvalidAddress("ltc1q026xyextkwhmveh7rpf6v6mp5p88vwc25aynxr?time=1708476626"); // additional information } } ================================================ FILE: assets/src/test/java/haveno/asset/coins/MoneroTest.java ================================================ /* * 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 . */ package haveno.asset.coins; import haveno.asset.AbstractAssetTest; import org.junit.jupiter.api.Test; public class MoneroTest extends AbstractAssetTest { public MoneroTest() { super(new Monero()); } @Test public void testValidAddresses() { assertValidAddress("4BJHitCigGy6giuYsJFP26KGkTKiQDJ6HJP1pan2ir2CCV8Twc2WWmo4fu1NVXt8XLGYAkjo5cJ3yH68Lfz9ZXEUJ9MeqPW"); assertValidAddress("46tM15KsogEW5MiVmBn7waPF8u8ZsB6aHjJk7BAv1wvMKfWhQ2h2so5BCJ9cRakfPt5BFo452oy3K8UK6L2u2v7aJ3Nf7P2"); assertValidAddress("86iQTnEqQ9mXJFvBvbY3KU5do5Jh2NCkpTcZsw3TMZ6oKNJhELvAreZFQ1p8EknRRTKPp2vg9fJvy47Q4ARVChjLMuUAFQJ"); // integrated addresses assertValidAddress("4LL9oSLmtpccfufTMvppY6JwXNouMBzSkbLYfpAV5Usx3skxNgYeYTRj5UzqtReoS44qo9mtmXCqY45DJ852K5Jv2bYXZKKQePHES9khPK"); assertValidAddress("4GdoN7NCTi8a5gZug7PrwZNKjvHFmKeV11L6pNJPgj5QNEHsN6eeX3DaAQFwZ1ufD4LYCZKArktt113W7QjWvQ7CWD1FFMXoYHeE6M55P9"); assertValidAddress("4GdoN7NCTi8a5gZug7PrwZNKjvHFmKeV11L6pNJPgj5QNEHsN6eeX3DaAQFwZ1ufD4LYCZKArktt113W7QjWvQ7CW82yHFEGvSG3NJRNtH"); } @Test public void testInvalidAddresses() { assertInvalidAddress(""); assertInvalidAddress("4BJHitCigGy6giuYsJFP26KGkTKiQDJ6HJP1pan2ir2CCV8Twc2WWmo4fu1NVXt8XLGYAkjo5cJ3yH68Lfz9ZXEUJ9MeqP"); assertInvalidAddress("4BJHitCigGy6giuYsJFP26KGkTKiQDJ6HJP1pan2ir2CCV8Twc2WWmo4fu1NVXt8XLGYAkjo5cJ3yH68Lfz9ZXEUJ9MeqPWW"); assertInvalidAddress("86iQTnEqQ9mXJFvBvbY3KU5do5Jh2NCkpTcZsw3TMZ6oKNJhELvAreZFQ1p8EknRRTKPp2vg9fJvy47Q4ARVChjLMuUAFQ!"); assertInvalidAddress("76iQTnEqQ9mXJFvBvbY3KU5do5Jh2NCkpTcZsw3TMZ6oKNJhELvAreZFQ1p8EknRRTKPp2vg9fJvy47Q4ARVChjLMuUAFQJ"); } } ================================================ FILE: assets/src/test/java/haveno/asset/coins/RippleTest.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.asset.coins; import haveno.asset.AbstractAssetTest; import org.junit.jupiter.api.Test; public class RippleTest extends AbstractAssetTest { public RippleTest() { super(new Ripple()); } @Test public void testValidAddresses() { assertValidAddress("r9CxAMAoZAgyVGP8CY9F1arzf9bJg3Y7U8"); assertValidAddress("rsXMbDtCAmzSWajWiii7ffWygAjYVNDxY7"); assertValidAddress("rE3nYkQy121JEVb37JKX8LSH6wUBnNvNo2"); assertValidAddress("rMzucuWFUEE6aM9DC992BqqMgZNPrv4kvi"); assertValidAddress("rJUmAFPWE36cpdbN4DUEAFBLtG2xkEavY8"); } @Test public void testInvalidAddresses() { assertInvalidAddress("RJUmAFPWE36cpdbN4DUEAFBLtG2xkEavY8"); assertInvalidAddress("zJUmAFPWE36cpdbN4DUEAFBLtG2xkEavY8"); assertInvalidAddress("1LgfapHEPhZbRF9pMd5WPT35hFXcZS1USrW"); } } ================================================ FILE: assets/src/test/java/haveno/asset/coins/SolanaTest.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.asset.coins; import haveno.asset.AbstractAssetTest; import org.junit.jupiter.api.Test; public class SolanaTest extends AbstractAssetTest { public SolanaTest() { super(new Solana()); } @Test public void testValidAddresses() { assertValidAddress("4Nd1mYZbtJbHkj9QwxAXWah8X9M8vZ9H1fsn6uhPW33k"); assertValidAddress("8HoQnePLqPj4M7PUDzfw8e3Ymdwgc7NqAcoH7okh4wz7"); assertValidAddress("H3C5pGrMmD8FrGd9VRtNVbY3tWusJX3A1u33f9bdBpsk"); assertValidAddress("7zVhJcA5s8zfg3UoDUuG4zmnqaVmLqj6L6F6L8WPLnYw"); assertValidAddress("AVHUu155WoNexeNCGce8mrb8hvg8pBgvCJh4vtd3Q1RV"); assertValidAddress("8HoQnePLqPj4M7PUDzfw8e3Ymdwgc7NqAcoH7okh4wz"); } @Test public void testInvalidAddresses() { assertInvalidAddress("4Nd1mYZbtJbHkj9QwxAXWah8X9M8vZ9H1fsn6uhPW33O"); assertInvalidAddress("H3C5pGrMmD8FrGd9VRtNVbY3tWusJX3A1u33f9bdBpskAAA"); assertInvalidAddress("1"); assertInvalidAddress("abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ123456789"); } } ================================================ FILE: assets/src/test/java/haveno/asset/coins/TetherUSDERC20Test.java ================================================ /* * 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 . */ package haveno.asset.coins; import haveno.asset.AbstractAssetTest; import haveno.asset.tokens.TetherUSDERC20; import org.junit.jupiter.api.Test; public class TetherUSDERC20Test extends AbstractAssetTest { public TetherUSDERC20Test() { super(new TetherUSDERC20()); } @Test public void testValidAddresses() { assertValidAddress("0x2a65Aca4D5fC5B5C859090a6c34d164135398226"); assertValidAddress("2a65Aca4D5fC5B5C859090a6c34d164135398226"); } @Test public void testInvalidAddresses() { assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d1641353982266"); assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d16413539822g"); assertInvalidAddress("2a65Aca4D5fC5B5C859090a6c34d16413539822g"); } } ================================================ FILE: assets/src/test/java/haveno/asset/coins/TetherUSDTRC20Test.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.asset.coins; import haveno.asset.AbstractAssetTest; import haveno.asset.tokens.TetherUSDTRC20; import org.junit.jupiter.api.Test; public class TetherUSDTRC20Test extends AbstractAssetTest { public TetherUSDTRC20Test() { super(new TetherUSDTRC20()); } @Test public void testValidAddresses() { assertValidAddress("TVnmu3E6DYVL4bpAoZnPNEPVUrgC7eSWaX"); } @Test public void testInvalidAddresses() { assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d1641353982266"); assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d16413539822g"); assertInvalidAddress("2a65Aca4D5fC5B5C859090a6c34d16413539822g"); } } ================================================ FILE: assets/src/test/java/haveno/asset/coins/TronTest.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.asset.coins; import haveno.asset.AbstractAssetTest; import org.junit.jupiter.api.Test; public class TronTest extends AbstractAssetTest { public TronTest() { super(new Tron()); } @Test public void testValidAddresses() { assertValidAddress("TRjE1H8dxypKM1NZRdysbs9wo7huR4bdNz"); assertValidAddress("THdUXD3mZqT5aMnPQMtBSJX9ANGjaeUwQK"); assertValidAddress("THUE6WTLaEGytFyuGJQUcKc3r245UKypoi"); assertValidAddress("TH7vVF9RTMXM9x7ZnPnbNcEph734hpu8cf"); assertValidAddress("TJNtFduS4oebw3jgGKCYmgSpTdyPieb6Ha"); } @Test public void testInvalidAddresses() { assertInvalidAddress("TJRyWwFs9wTFGZg3L8nL62xwP9iK8QdK9R"); assertInvalidAddress("TJRyWwFs9wTFGZg3L8nL62xwP9iK8QdK9X"); assertInvalidAddress("1JRyWwFs9wTFGZg3L8nL62xwP9iK8QdK9R"); assertInvalidAddress("TGzz8gjYiYRqpfmDwnLxfgPuLVNmpCswVo"); } } ================================================ FILE: build.gradle ================================================ import org.apache.tools.ant.taskdefs.condition.Os import org.gradle.internal.logging.ConsoleRenderer buildscript { repositories { gradlePluginPortal() } dependencies { classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.17' classpath 'com.google.gradle:osdetector-gradle-plugin:1.7.3' classpath 'com.github.johnrengelman:shadow:8.1.1' classpath 'org.springframework.boot:spring-boot-gradle-plugin:3.2.3' classpath 'com.gradle:gradle-enterprise-gradle-plugin:3.16.1' // added for windows build verification-metadata.xml error } } configure(rootProject) { // remove the 'haveno-*' scripts and 'lib' dir generated by the 'installDist' task task clean { doLast { delete fileTree(dir: rootProject.projectDir, include: 'haveno-*'), 'lib' } } } configure(subprojects) { apply plugin: 'java' apply plugin: 'com.google.osdetector' // Apply the jacoco plugin to add support for test coverage apply plugin: 'jacoco' apply plugin: 'jacoco-report-aggregation' apply plugin: 'checkstyle' sourceCompatibility = JavaVersion.VERSION_21 ext { // in alphabetical order bcVersion = '1.63' bitcoinjVersion = '2a80db4' codecVersion = '1.13' cowwocVersion = '1.2' easybindVersion = '1.0.3' easyVersion = '4.0.1' findbugsVersion = '3.0.2' firebaseVersion = '6.2.0' fontawesomefxVersion = '8.0.0' fontawesomefxCommonsVersion = '9.1.2' fontawesomefxMaterialdesignfontVersion = '2.0.26-9.1.2' grpcVersion = '1.42.1' gsonVersion = '2.8.5' guavaVersion = '32.1.1-jre' guiceVersion = '7.0.0' moneroJavaVersion = '0.8.43' httpclient5Version = '5.0' hamcrestVersion = '2.2' httpclientVersion = '4.5.12' httpcoreVersion = '4.4.13' ioVersion = '2.6' jacksonVersion = '2.12.1' javafxVersion = '21.0.9' javaxAnnotationVersion = '1.2' jcsvVersion = '1.4.0' jetbrainsAnnotationsVersion = '13.0' jfoenixVersion = '9.0.10' joptVersion = '5.0.4' jsonsimpleVersion = '1.1.1' jsonrpc4jVersion = '1.6.0.bisq.1' jupiterVersion = '5.9.2' kotlinVersion = '1.3.41' langVersion = '3.11' logbackVersion = '1.1.11' loggingVersion = '1.2' lombokVersion = '1.18.30' mockitoVersion = '5.10.0' netlayerVersion = 'd9c60be46d' // Tor browser version 14.0.7 and tor binary version: 0.4.8.14 protobufVersion = '3.19.1' protocVersion = protobufVersion pushyVersion = '0.13.2' qrgenVersion = '1.3' slf4jVersion = '1.7.30' sparkVersion = '2.5.2' def osName = osdetector.os == 'osx' ? 'mac' : osdetector.os == 'windows' ? 'win' : osdetector.os def osArch = System.getProperty("os.arch").toLowerCase() os = (osName == 'mac' && (osArch.contains('aarch64') || osArch.contains('arm'))) ? 'mac-aarch64' : osName } configurations { javafxVerification } repositories { mavenCentral() //mavenLocal() maven { url 'https://jitpack.io' } maven { url 'https://mvnrepository.com' } } tasks.withType(JavaCompile) { options.encoding = 'UTF-8' } checkstyle { toolVersion = '10.8.1' // https://raw.githubusercontent.com/checkstyle/checkstyle/checkstyle-10.8.1/src/main/resources/google_checks.xml configFile = rootProject.file("$rootDir/config/checkstyle/checkstyle.xml") } tasks.withType(Checkstyle) { minHeapSize.set('200m') maxHeapSize.set('1g') } jacoco { toolVersion = "0.8.10" reportsDirectory = file("$rootDir/build/reports/jacoco") } test.finalizedBy { testCodeCoverageReport { // tests are required to run before generating the report reports { xml.required.set(true) html.required.set(false) } } } test { useJUnitPlatform() } } configure([project(':cli'), project(':daemon'), project(':desktop'), project(':monitor'), project(':relay'), project(':seednode'), project(':statsnode'), project(':inventory'), project(':apitest')]) { apply plugin: 'application' build.dependsOn installDist installDist.destinationDir = file('build/app') distZip.enabled = false // the 'installDist' and 'startScripts' blocks below configure haveno executables to put // generated shell scripts in the root project directory, such that users can easily // discover and invoke e.g. ./haveno-desktop, ./haveno-seednode, etc. // See https://stackoverflow.com/q/46327736 for details. installDist { doLast { // copy generated shell scripts, e.g. `haveno-desktop` directly to the project // root directory for discoverability and ease of use copy { from "$destinationDir/bin" into rootProject.projectDir } // copy libs required for generated shell script classpaths to 'lib' dir under // the project root directory copy { from "$destinationDir/lib" into "${rootProject.projectDir}/lib" } // edit generated shell scripts such that they expect to be executed in the // project root dir as opposed to a 'bin' subdirectory if (osdetector.os == 'windows') { def windowsScriptFile = file("${rootProject.projectDir}/haveno-${applicationName}.bat") windowsScriptFile.text = windowsScriptFile.text.replace( 'set APP_HOME=%DIRNAME%..', 'set APP_HOME=%DIRNAME%') if (applicationName == 'desktop') { windowsScriptFile.text = windowsScriptFile.text.replace( 'DEFAULT_JVM_OPTS=', 'DEFAULT_JVM_OPTS=-XX:MaxRAM=4g ' + '--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED ' + '--add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED ' + '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED ' + '--add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED') } } else { def unixScriptFile = file("${rootProject.projectDir}/haveno-$applicationName") unixScriptFile.text = unixScriptFile.text.replace( 'APP_HOME=$( cd "${APP_HOME:-./}.." > /dev/null && pwd -P ) || exit', 'APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit') if (applicationName == 'desktop') { unixScriptFile.text = unixScriptFile.text.replace( 'DEFAULT_JVM_OPTS=""', 'DEFAULT_JVM_OPTS="-XX:MaxRAM=4g ' + '--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED ' + '--add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED ' + '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED ' + '--add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED"') } } if (applicationName == 'apitest') { // Pass the logback config file as a system property to avoid chatty // logback startup due to multiple logback.xml files in the classpath // (:daemon & :cli). def script = file("${rootProject.projectDir}/haveno-$applicationName") script.text = script.text.replace( 'DEFAULT_JVM_OPTS=""', 'DEFAULT_JVM_OPTS="' + '-Dlogback.configurationFile=apitest/build/resources/main/logback.xml"') } if (osdetector.os != 'windows') delete fileTree(dir: rootProject.projectDir, include: 'haveno-*.bat') else delete fileTree(dir: rootProject.projectDir, include: 'haveno-*', exclude: '*.bat') } } startScripts { // rename scripts from, e.g. `desktop` to `haveno-desktop` applicationName = "haveno-$applicationName" } } configure(project(':proto')) { apply plugin: 'com.google.protobuf' dependencies { annotationProcessor "org.projectlombok:lombok:$lombokVersion" compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion" compileOnly "org.projectlombok:lombok:$lombokVersion" implementation "ch.qos.logback:logback-classic:$logbackVersion" implementation "ch.qos.logback:logback-core:$logbackVersion" implementation "com.google.guava:guava:$guavaVersion" implementation "com.google.protobuf:protobuf-java:$protobufVersion" implementation "org.slf4j:slf4j-api:$slf4jVersion" implementation("io.grpc:grpc-protobuf:$grpcVersion") { exclude(module: 'animal-sniffer-annotations') exclude(module: 'guava') } implementation("io.grpc:grpc-stub:$grpcVersion") { exclude(module: 'animal-sniffer-annotations') exclude(module: 'guava') } } sourceSets.main.java.srcDirs += [ 'build/generated/source/proto/main/grpc', 'build/generated/source/proto/main/java' ] protobuf { protoc { artifact = "com.google.protobuf:protoc:${protocVersion}" } plugins { grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" } } generateProtoTasks { all()*.plugins { grpc {} } } } } configure(project(':assets')) { dependencies { implementation("com.github.bisq-network:bitcoinj:$bitcoinjVersion") { exclude(module: 'bcprov-jdk15on') exclude(module: 'guava') exclude(module: 'jsr305') exclude(module: 'okhttp') exclude(module: 'okio') exclude(module: 'protobuf-java') exclude(module: 'slf4j-api') } implementation "com.google.guava:guava:$guavaVersion" implementation "org.apache.commons:commons-lang3:$langVersion" implementation "org.slf4j:slf4j-api:$slf4jVersion" testImplementation "org.hamcrest:hamcrest:$hamcrestVersion" testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$jupiterVersion" } } configure(project(':common')) { dependencies { implementation project(':proto') annotationProcessor "org.projectlombok:lombok:$lombokVersion" compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion" compileOnly "org.projectlombok:lombok:$lombokVersion" implementation "ch.qos.logback:logback-classic:$logbackVersion" implementation "ch.qos.logback:logback-core:$logbackVersion" implementation("com.github.bisq-network:bitcoinj:$bitcoinjVersion") { exclude(module: 'jsr305') exclude(module: 'slf4j-api') exclude(module: 'guava') exclude(module: 'protobuf-java') exclude(module: 'bcprov-jdk15on') exclude(module: 'okhttp') exclude(module: 'okio') } implementation "com.google.code.findbugs:jsr305:$findbugsVersion" implementation "com.google.code.gson:gson:$gsonVersion" implementation "com.google.guava:guava:$guavaVersion" implementation("com.google.inject:guice:$guiceVersion") { exclude(module: 'guava') } implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" implementation "com.google.protobuf:protobuf-java:$protobufVersion" implementation "commons-io:commons-io:$ioVersion" implementation "net.sf.jopt-simple:jopt-simple:$joptVersion" implementation "org.apache.commons:commons-lang3:$langVersion" implementation "org.bouncycastle:bcpg-jdk15on:$bcVersion" implementation "org.jetbrains:annotations:$jetbrainsAnnotationsVersion" implementation "org.slf4j:slf4j-api:$slf4jVersion" testImplementation "org.hamcrest:hamcrest:$hamcrestVersion" testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$jupiterVersion" runtimeOnly("io.grpc:grpc-netty-shaded:$grpcVersion") { exclude(module: 'guava') exclude(module: 'animal-sniffer-annotations') } // override transitive dependency and use latest version from bisq implementation(group: 'com.github.bisq-network', name: 'jtorctl') { version { strictly "[b2a172f44edcd8deaa5ed75d936dcbb007f0d774]" } } implementation "org.openjfx:javafx-base:$javafxVersion:$os" implementation "org.openjfx:javafx-graphics:$javafxVersion:$os" } } configure(project(':p2p')) { dependencies { implementation project(':proto') implementation project(':common') annotationProcessor "org.projectlombok:lombok:$lombokVersion" compileOnly "org.projectlombok:lombok:$lombokVersion" implementation "com.google.guava:guava:$guavaVersion" implementation "com.google.protobuf:protobuf-java:$protobufVersion" implementation "org.fxmisc.easybind:easybind:$easybindVersion" implementation "org.slf4j:slf4j-api:$slf4jVersion" implementation "org.apache.commons:commons-lang3:$langVersion" implementation("com.github.haveno-dex.netlayer:tor.external:$netlayerVersion") { exclude(module: 'slf4j-api') } implementation("com.github.haveno-dex.netlayer:tor.native:$netlayerVersion") { exclude(module: 'slf4j-api') } implementation("com.github.bisq-network:bitcoinj:$bitcoinjVersion") { exclude(module: 'bcprov-jdk15on') exclude(module: 'guava') exclude(module: 'jsr305') exclude(module: 'okhttp') exclude(module: 'okio') exclude(module: 'protobuf-java') exclude(module: 'slf4j-api') } implementation("com.google.inject:guice:$guiceVersion") { exclude(module: 'guava') } implementation("org.apache.httpcomponents:httpclient:$httpclientVersion") { exclude(module: 'commons-codec') } testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion" testCompileOnly "org.projectlombok:lombok:$lombokVersion" testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion" testImplementation "org.junit.jupiter:junit-jupiter-params:$jupiterVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$jupiterVersion" testImplementation "ch.qos.logback:logback-classic:$logbackVersion" testImplementation "ch.qos.logback:logback-core:$logbackVersion" testImplementation "org.apache.commons:commons-lang3:$langVersion" testImplementation("org.mockito:mockito-core:$mockitoVersion") testImplementation("org.mockito:mockito-junit-jupiter:$mockitoVersion") implementation "org.openjfx:javafx-base:$javafxVersion:$os" implementation "org.openjfx:javafx-graphics:$javafxVersion:$os" } } configure(project(':core')) { dependencies { implementation project(':proto') implementation project(':assets') implementation project(':common') implementation project(':p2p') annotationProcessor "org.projectlombok:lombok:$lombokVersion" compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion" compileOnly "org.projectlombok:lombok:$lombokVersion" implementation "ch.qos.logback:logback-classic:$logbackVersion" implementation "ch.qos.logback:logback-core:$logbackVersion" implementation "com.fasterxml.jackson.core:jackson-core:$jacksonVersion" implementation "com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion" implementation "com.google.code.findbugs:jsr305:$findbugsVersion" implementation "com.google.code.gson:gson:$gsonVersion" implementation "com.google.guava:guava:$guavaVersion" implementation "com.google.protobuf:protobuf-java:$protobufVersion" implementation "commons-codec:commons-codec:$codecVersion" implementation "commons-io:commons-io:$ioVersion" implementation "net.sf.jopt-simple:jopt-simple:$joptVersion" implementation "org.apache.commons:commons-lang3:$langVersion" implementation "org.apache.httpcomponents:httpcore:$httpcoreVersion" implementation "org.fxmisc.easybind:easybind:$easybindVersion" implementation "org.jetbrains:annotations:$jetbrainsAnnotationsVersion" implementation "org.slf4j:slf4j-api:$slf4jVersion" implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") { exclude(module: 'jackson-annotations') } implementation("com.github.haveno-dex.netlayer:tor.native:$netlayerVersion") { exclude(module: 'slf4j-api') } implementation("com.github.haveno-dex.netlayer:tor.external:$netlayerVersion") { exclude(module: 'slf4j-api') } implementation("com.github.bisq-network:bitcoinj:$bitcoinjVersion") { exclude(module: 'bcprov-jdk15on') exclude(module: 'guava') exclude(module: 'jsr305') exclude(module: 'okhttp') exclude(module: 'okio') exclude(module: 'protobuf-java') exclude(module: 'slf4j-api') } implementation("com.github.bisq-network:jsonrpc4j:$jsonrpc4jVersion") { exclude(module: 'base64') exclude(module: 'httpcore-nio') } implementation("com.google.inject:guice:$guiceVersion") { exclude(module: 'guava') } implementation("org.apache.httpcomponents:httpclient:$httpclientVersion") { exclude(module: 'commons-codec') } testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion" testCompileOnly "org.projectlombok:lombok:$lombokVersion" testImplementation "com.natpryce:make-it-easy:$easyVersion" testImplementation "org.hamcrest:hamcrest:$hamcrestVersion" testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$jupiterVersion" testImplementation "org.mockito:mockito-core:$mockitoVersion" implementation "org.openjfx:javafx-base:$javafxVersion:$os" implementation "org.openjfx:javafx-graphics:$javafxVersion:$os" implementation("io.github.woodser:monero-java:$moneroJavaVersion") { exclude(module: 'jackson-core') exclude(module: 'jackson-annotations') exclude(module: 'jackson-databind') exclude(module: 'bcprov-jdk15on') exclude(group: 'org.slf4j', module: 'slf4j-simple') } } test { systemProperty 'jdk.attach.allowAttachSelf', true } task generateKeypairs(type: JavaExec) { mainClass = 'haveno.core.util.GenerateKeyPairs' classpath = sourceSets.main.runtimeClasspath } task havenoDeps { doLast { // get monero binaries download url Map moneroBinaries = [ 'linux-x86_64' : 'https://github.com/haveno-dex/monero/releases/download/release9/monero-bins-haveno-linux-x86_64.tar.gz', 'linux-x86_64-sha256' : 'e1df7c2789472ece060619bcfe1a8a6e792799c4cd963433226e12b265ca0c9f', 'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release9/monero-bins-haveno-linux-aarch64.tar.gz', 'linux-aarch64-sha256' : 'b197c46b08780c27ccd05d8123207af25211dd59edff0a63d73e93aa88d16892', 'mac' : 'https://github.com/haveno-dex/monero/releases/download/release9/monero-bins-haveno-mac.tar.gz', 'mac-sha256' : '1f39647d686a7a15b72ecff594069d90f047ed2f6df1817165ef1abc5419ec43', 'windows' : 'https://github.com/haveno-dex/monero/releases/download/release9/monero-bins-haveno-windows.zip', 'windows-sha256' : '10f7b38ebc1679ad9d25ac72a930141a082e6e502d7a5b06816ad770fe23f27c' ] String osKey if (Os.isFamily(Os.FAMILY_WINDOWS)) { osKey = 'windows' } else if (Os.isFamily(Os.FAMILY_MAC)) { osKey = 'mac' } else { String architecture = System.getProperty("os.arch").toLowerCase() if (architecture.contains('aarch64') || architecture.contains('arm')) { osKey = 'linux-aarch64' } else { osKey = 'linux-x86_64' } } String moneroDownloadUrl = moneroBinaries[osKey] String moneroSHA256Hash = moneroBinaries[osKey + '-sha256'] String moneroArchiveFileName = moneroDownloadUrl.tokenize('/').last() String localnetDirName = '.localnet' File localnetDir = new File(project.rootDir, localnetDirName) localnetDir.mkdirs() File moneroArchiveFile = new File(localnetDir, moneroArchiveFileName) ext.downloadAndVerifyDependencies(moneroDownloadUrl, moneroSHA256Hash, moneroArchiveFile) // extract if dependencies are missing or if archive was updated File monerodFile File moneroRpcFile if (Os.isFamily(Os.FAMILY_WINDOWS)) { monerodFile = new File(localnetDir, 'monerod.exe') moneroRpcFile = new File(localnetDir, 'monero-wallet-rpc.exe') } else { monerodFile = new File(localnetDir, 'monerod') moneroRpcFile = new File(localnetDir, 'monero-wallet-rpc') } if (ext.dependencyDownloadedAndVerified || !monerodFile.exists() || !moneroRpcFile.exists()) { if (Os.isFamily(Os.FAMILY_WINDOWS)) { ext.extractArchiveZip(moneroArchiveFile, localnetDir) } else { ext.extractArchiveTarGz(moneroArchiveFile, localnetDir) } } // add the current platform's monero dependencies into the resources folder for installation copy { from "${monerodFile}" into "${project(':core').projectDir}/src/main/resources/bin" } copy { from "${moneroRpcFile}" into "${project(':core').projectDir}/src/main/resources/bin" } } ext.extractArchiveTarGz = { File tarGzFile, File destinationDir -> println "Extracting tar.gz ${tarGzFile}" // Gradle's tar extraction preserves permissions (crucial for jpackage to function correctly) copy { from tarTree(resources.gzip(tarGzFile)) into destinationDir } println "Extracted to ${destinationDir}" } ext.extractArchiveZip = { File zipFile, File destinationDir -> println "Extracting zip ${zipFile}..." ant.unzip(src: zipFile, dest: destinationDir) println "Extracted to ${destinationDir}" } ext.downloadAndVerifyDependencies = { String archiveURL, String archiveSHA256, File destinationArchiveFile -> ext.dependencyDownloadedAndVerified = false // if archive exists, check to see if its already up to date if (destinationArchiveFile.exists()) { println "Verifying existing archive ${destinationArchiveFile}" ant.archiveHash = archiveSHA256 ant.checksum(file: destinationArchiveFile, algorithm: 'SHA-256', property: '${archiveHash}', verifyProperty: 'existingHashMatches') if (ant.properties['existingHashMatches'] != 'true') { println "Existing archive does not match hash ${archiveSHA256}" } else { println "Existing archive matches hash" return } } // download archives println "Downloading ${archiveURL}" ant.get(src: archiveURL, dest: destinationArchiveFile) println 'Download saved to ' + destinationArchiveFile // verify checksum println 'Verifying checksum for downloaded binary ...' ant.archiveHash = archiveSHA256 ant.checksum(file: destinationArchiveFile, algorithm: 'SHA-256', property: '${archiveHash}', verifyProperty: 'downloadedHashMatches') // use a different verifyProperty name from existing verification or it will always fail if (ant.properties['downloadedHashMatches'] != 'true') { ant.fail('Checksum mismatch: Downloaded archive has a different checksum than expected') } println 'Checksum verified' ext.dependencyDownloadedAndVerified = true } } processResources.dependsOn havenoDeps // before both test and build } configure(project(':cli')) { mainClassName = 'haveno.cli.CliMain' dependencies { implementation project(':proto') annotationProcessor "org.projectlombok:lombok:$lombokVersion" compileOnly "org.projectlombok:lombok:$lombokVersion" implementation "ch.qos.logback:logback-classic:$logbackVersion" implementation "ch.qos.logback:logback-core:$logbackVersion" implementation "com.google.guava:guava:$guavaVersion" implementation "com.google.protobuf:protobuf-java:$protobufVersion" implementation "net.sf.jopt-simple:jopt-simple:$joptVersion" implementation "org.slf4j:slf4j-api:$slf4jVersion" implementation("io.grpc:grpc-core:$grpcVersion") { exclude(module: 'animal-sniffer-annotations') exclude(module: 'guava') } implementation("io.grpc:grpc-stub:$grpcVersion") { exclude(module: 'animal-sniffer-annotations') exclude(module: 'guava') } runtimeOnly("io.grpc:grpc-netty-shaded:$grpcVersion") { exclude(module: 'animal-sniffer-annotations') exclude(module: 'guava') } testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion" testCompileOnly "org.projectlombok:lombok:$lombokVersion" testImplementation "org.bitbucket.cowwoc:diff-match-patch:$cowwocVersion" testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion" testImplementation "org.junit.jupiter:junit-jupiter-params:$jupiterVersion" testRuntimeOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$jupiterVersion" } } configure(project(':desktop')) { apply plugin: 'com.github.johnrengelman.shadow' apply from: 'package/package.gradle' version = '1.2.3-SNAPSHOT' jar.manifest.attributes( "Implementation-Title": project.name, "Implementation-Version": version, "CFBundleVersion": version) mainClassName = 'haveno.desktop.app.HavenoAppMain' tasks.withType(AbstractArchiveTask) { preserveFileTimestamps = false reproducibleFileOrder = true } sourceSets.main.resources.srcDirs += ['src/main/java'] // to copy fxml and css files dependencies { implementation project(':assets') implementation project(':common') implementation project(':proto') implementation project(':p2p') implementation project(':core') annotationProcessor "org.projectlombok:lombok:$lombokVersion" compileOnly "org.projectlombok:lombok:$lombokVersion" implementation "ch.qos.logback:logback-classic:$logbackVersion" implementation "ch.qos.logback:logback-core:$logbackVersion" implementation "com.google.code.gson:gson:$gsonVersion" implementation "com.google.guava:guava:$guavaVersion" implementation "com.google.protobuf:protobuf-java:$protobufVersion" implementation "com.googlecode.jcsv:jcsv:$jcsvVersion" implementation "com.jfoenix:jfoenix:$jfoenixVersion" implementation "commons-io:commons-io:$ioVersion" implementation "de.jensd:fontawesomefx-commons:$fontawesomefxCommonsVersion" implementation "de.jensd:fontawesomefx-materialdesignfont:$fontawesomefxMaterialdesignfontVersion" implementation "de.jensd:fontawesomefx:$fontawesomefxVersion" implementation "net.glxn:qrgen:$qrgenVersion" implementation "org.apache.commons:commons-lang3:$langVersion" implementation "org.bouncycastle:bcpg-jdk15on:$bcVersion" implementation "org.fxmisc.easybind:easybind:$easybindVersion" implementation "org.jetbrains:annotations:$jetbrainsAnnotationsVersion" implementation "org.slf4j:slf4j-api:$slf4jVersion" implementation("com.github.bisq-network:bitcoinj:$bitcoinjVersion") { exclude(module: 'bcprov-jdk15on') exclude(module: 'guava') exclude(module: 'jsr305') exclude(module: 'okhttp') exclude(module: 'okio') exclude(module: 'protobuf-java') exclude(module: 'slf4j-api') } implementation("com.google.inject:guice:$guiceVersion") { exclude(module: 'guava') } testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion" testCompileOnly "org.projectlombok:lombok:$lombokVersion" testImplementation "com.natpryce:make-it-easy:$easyVersion" testImplementation "org.mockito:mockito-core:$mockitoVersion" testImplementation "org.hamcrest:hamcrest:$hamcrestVersion" testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$jupiterVersion" implementation("io.github.woodser:monero-java:$moneroJavaVersion") { exclude(module: 'jackson-core') exclude(module: 'jackson-annotations') exclude(module: 'jackson-databind') exclude(module: 'bcprov-jdk15on') exclude(group: 'org.slf4j', module: 'slf4j-simple') } implementation "org.openjfx:javafx-controls:$javafxVersion:$os" implementation "org.openjfx:javafx-fxml:$javafxVersion:$os" implementation "org.openjfx:javafx-swing:$javafxVersion:$os" implementation "org.openjfx:javafx-base:$javafxVersion:$os" implementation "org.openjfx:javafx-graphics:$javafxVersion:$os" // verification-only dependencies javafxVerification "org.openjfx:javafx-controls:$javafxVersion:mac" javafxVerification "org.openjfx:javafx-controls:$javafxVersion:mac-aarch64" javafxVerification "org.openjfx:javafx-controls:$javafxVersion:win" javafxVerification "org.openjfx:javafx-controls:$javafxVersion:linux" javafxVerification "org.openjfx:javafx-fxml:$javafxVersion:mac" javafxVerification "org.openjfx:javafx-fxml:$javafxVersion:mac-aarch64" javafxVerification "org.openjfx:javafx-fxml:$javafxVersion:win" javafxVerification "org.openjfx:javafx-fxml:$javafxVersion:linux" javafxVerification "org.openjfx:javafx-swing:$javafxVersion:mac" javafxVerification "org.openjfx:javafx-swing:$javafxVersion:mac-aarch64" javafxVerification "org.openjfx:javafx-swing:$javafxVersion:win" javafxVerification "org.openjfx:javafx-swing:$javafxVersion:linux" javafxVerification "org.openjfx:javafx-base:$javafxVersion:mac" javafxVerification "org.openjfx:javafx-base:$javafxVersion:mac-aarch64" javafxVerification "org.openjfx:javafx-base:$javafxVersion:win" javafxVerification "org.openjfx:javafx-base:$javafxVersion:linux" javafxVerification "org.openjfx:javafx-graphics:$javafxVersion:mac" javafxVerification "org.openjfx:javafx-graphics:$javafxVersion:mac-aarch64" javafxVerification "org.openjfx:javafx-graphics:$javafxVersion:win" javafxVerification "org.openjfx:javafx-graphics:$javafxVersion:linux" } test { systemProperty 'jdk.attach.allowAttachSelf', true } } configure(project(':monitor')) { mainClassName = 'haveno.monitor.Monitor' dependencies { implementation project(':assets') implementation project(':common') implementation project(':core') implementation project(':p2p') annotationProcessor "org.projectlombok:lombok:$lombokVersion" compileOnly "org.projectlombok:lombok:$lombokVersion" implementation "ch.qos.logback:logback-classic:$logbackVersion" implementation "ch.qos.logback:logback-core:$logbackVersion" implementation "com.google.guava:guava:$guavaVersion" implementation "org.slf4j:slf4j-api:$slf4jVersion" implementation("com.github.haveno-dex.netlayer:tor.external:$netlayerVersion") { exclude(module: 'slf4j-api') } implementation("com.github.haveno-dex.netlayer:tor.native:$netlayerVersion") { exclude(module: 'slf4j-api') } implementation("com.google.inject:guice:$guiceVersion") { exclude(module: 'guava') } testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion" testCompileOnly "org.projectlombok:lombok:$lombokVersion" testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion" testImplementation "org.junit.jupiter:junit-jupiter-params:$jupiterVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$jupiterVersion" implementation "org.openjfx:javafx-base:$javafxVersion:$os" implementation "org.openjfx:javafx-graphics:$javafxVersion:$os" } } configure(project(':relay')) { mainClassName = 'haveno.relay.RelayMain' dependencies { implementation project(':common') implementation "ch.qos.logback:logback-classic:$logbackVersion" implementation "ch.qos.logback:logback-core:$logbackVersion" implementation("com.google.firebase:firebase-admin:$firebaseVersion") { exclude(module: 'commons-logging') exclude(module: 'grpc-auth') exclude(module: 'httpclient') exclude(module: 'httpcore') } implementation "com.sparkjava:spark-core:$sparkVersion" implementation "com.turo:pushy:$pushyVersion" implementation "commons-codec:commons-codec:$codecVersion" implementation "io.grpc:grpc-auth:$grpcVersion" } } configure(project(':seednode')) { apply plugin: 'com.github.johnrengelman.shadow' mainClassName = 'haveno.seednode.SeedNodeMain' dependencies { implementation project(':common') implementation project(':p2p') implementation project(':core') annotationProcessor "org.projectlombok:lombok:$lombokVersion" compileOnly "org.projectlombok:lombok:$lombokVersion" implementation "com.google.guava:guava:$guavaVersion" implementation "org.slf4j:slf4j-api:$slf4jVersion" implementation("com.google.inject:guice:$guiceVersion") { exclude(module: 'guava') } testImplementation "org.mockito:mockito-core:$mockitoVersion" testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$jupiterVersion" } } configure(project(':statsnode')) { mainClassName = 'haveno.statistics.StatisticsMain' dependencies { implementation project(':common') implementation project(':p2p') implementation project(':core') annotationProcessor "org.projectlombok:lombok:$lombokVersion" compileOnly "org.projectlombok:lombok:$lombokVersion" implementation "org.slf4j:slf4j-api:$slf4jVersion" implementation("com.google.inject:guice:$guiceVersion") { exclude(module: 'guava') } } } configure(project(':daemon')) { mainClassName = 'haveno.daemon.app.HavenoDaemonMain' apply plugin: 'com.github.johnrengelman.shadow' dependencies { implementation project(':proto') implementation project(':common') implementation project(':p2p') implementation project(':core') annotationProcessor "org.projectlombok:lombok:$lombokVersion" compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion" compileOnly "org.projectlombok:lombok:$lombokVersion" implementation "ch.qos.logback:logback-classic:$logbackVersion" implementation "ch.qos.logback:logback-core:$logbackVersion" implementation "com.google.code.gson:gson:$gsonVersion" implementation "com.google.guava:guava:$guavaVersion" implementation "com.google.protobuf:protobuf-java:$protobufVersion" implementation "org.apache.commons:commons-lang3:$langVersion" implementation "org.jetbrains:annotations:$jetbrainsAnnotationsVersion" implementation "org.slf4j:slf4j-api:$slf4jVersion" implementation("com.github.bisq-network:bitcoinj:$bitcoinjVersion") { exclude(module: 'bcprov-jdk15on') exclude(module: 'guava') exclude(module: 'jsr305') exclude(module: 'okhttp') exclude(module: 'okio') exclude(module: 'protobuf-java') exclude(module: 'slf4j-api') } implementation("com.google.inject:guice:$guiceVersion") { exclude(module: 'guava') } implementation("io.grpc:grpc-protobuf:$grpcVersion") { exclude(module: 'animal-sniffer-annotations') exclude(module: 'guava') } implementation("io.grpc:grpc-stub:$grpcVersion") { exclude(module: 'animal-sniffer-annotations') exclude(module: 'guava') } runtimeOnly("io.grpc:grpc-netty-shaded:$grpcVersion") { exclude(module: 'animal-sniffer-annotations') exclude(module: 'guava') } testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion" testCompileOnly "org.projectlombok:lombok:$lombokVersion" testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion" testImplementation "org.junit.jupiter:junit-jupiter-params:$jupiterVersion" testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion") implementation("io.github.woodser:monero-java:$moneroJavaVersion") { exclude(module: 'jackson-core') exclude(module: 'jackson-annotations') exclude(module: 'jackson-databind') exclude(module: 'bcprov-jdk15on') exclude(group: 'org.slf4j', module: 'slf4j-simple') } implementation "org.openjfx:javafx-base:$javafxVersion:$os" implementation "org.openjfx:javafx-graphics:$javafxVersion:$os" } } configure(project(':inventory')) { apply plugin: 'com.github.johnrengelman.shadow' mainClassName = 'haveno.inventory.InventoryMonitorMain' dependencies { implementation project(':common') implementation project(':p2p') implementation project(':core') annotationProcessor "org.projectlombok:lombok:$lombokVersion" compileOnly "org.projectlombok:lombok:$lombokVersion" implementation "ch.qos.logback:logback-classic:$logbackVersion" implementation "ch.qos.logback:logback-core:$logbackVersion" implementation "com.google.guava:guava:$guavaVersion" implementation "com.sparkjava:spark-core:$sparkVersion" implementation "org.jetbrains:annotations:$jetbrainsAnnotationsVersion" implementation("com.google.inject:guice:$guiceVersion") { exclude(module: 'guava') } } } configure(project(':apitest')) { mainClassName = 'haveno.apitest.ApiTestMain' // We have to disable the :apitest 'test' task by default because we do not want // to interfere with normal builds. To run JUnit tests in this subproject: // Run a normal build and install dao-setup files first, then run: // 'gradle :apitest:test -DrunApiTests=true' test.enabled = System.getProperty("runApiTests") == "true" test { outputs.upToDateWhen { false } // Don't use previously cached test outputs. testLogging { showStackTraces = true // Show full stack traces in the console. exceptionFormat = "full" // Show passed & failed tests, and anything printed to stderr by the tests in the console. // Do not show skipped tests in the console; they are shown in the html report. events "passed", "failed", "standardError" } afterSuite { desc, result -> if (!desc.parent) { println("${result.resultType} " + "[${result.testCount} tests, " + "${result.successfulTestCount} passed, " + "${result.failedTestCount} failed, " + "${result.skippedTestCount} skipped] html report contains skipped test info") // Show report link if all tests passed in case you want to see more detail, stdout, skipped, etc. if (result.resultType == TestResult.ResultType.SUCCESS) { DirectoryReport htmlReport = getReports().getHtml() String reportUrl = new ConsoleRenderer() .asClickableFileUrl(htmlReport.getEntryPoint()) println("REPORT " + reportUrl) } } } } dependencies { implementation project(':proto') implementation project(':common') implementation project(':core') implementation project(':seednode') implementation project(':desktop') implementation project(':daemon') implementation project(':cli') annotationProcessor "org.projectlombok:lombok:$lombokVersion" compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion" compileOnly "org.projectlombok:lombok:$lombokVersion" implementation "ch.qos.logback:logback-classic:$logbackVersion" implementation "ch.qos.logback:logback-core:$logbackVersion" implementation "com.google.code.gson:gson:$gsonVersion" implementation "com.google.guava:guava:$guavaVersion" implementation "com.google.protobuf:protobuf-java:$protobufVersion" implementation "net.sf.jopt-simple:jopt-simple:$joptVersion" implementation "org.apache.commons:commons-lang3:$langVersion" implementation "org.slf4j:slf4j-api:$slf4jVersion" implementation("com.github.bisq-network:bitcoinj:$bitcoinjVersion") { exclude(module: 'bcprov-jdk15on') exclude(module: 'guava') exclude(module: 'jsr305') exclude(module: 'okhttp') exclude(module: 'okio') exclude(module: 'protobuf-java') exclude(module: 'slf4j-api') } implementation("io.grpc:grpc-protobuf:$grpcVersion") { exclude(module: 'animal-sniffer-annotations') exclude(module: 'guava') } implementation("io.grpc:grpc-stub:$grpcVersion") { exclude(module: 'animal-sniffer-annotations') exclude(module: 'guava') } testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion" testCompileOnly "org.projectlombok:lombok:$lombokVersion" testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion" testImplementation "org.junit.jupiter:junit-jupiter-params:$jupiterVersion" testRuntimeOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$jupiterVersion" } } ================================================ FILE: cli/package/create-cli-dist.sh ================================================ #! /bin/bash VERSION="$1" if [[ -z "$VERSION" ]]; then VERSION="SNAPSHOT" fi export HAVENO_RELEASE_NAME="haveno-cli-$VERSION" export HAVENO_RELEASE_ZIP_NAME="$HAVENO_RELEASE_NAME.zip" export GRADLE_DIST_NAME="cli.tar" export GRADLE_DIST_PATH="../build/distributions/$GRADLE_DIST_NAME" arrangegradledist() { # Arrange $HAVENO_RELEASE_NAME directory structure to contain a runnable # jar at the top-level, and a lib dir containing dependencies: # . # | # |__ cli.jar # |__ lib # |__ |__ dep1.jar # |__ |__ dep2.jar # |__ |__ ... # Copy the build's distribution tarball to this directory. cp -v $GRADLE_DIST_PATH . # Create a clean directory to hold the tarball's content. rm -rf $HAVENO_RELEASE_NAME mkdir $HAVENO_RELEASE_NAME # Extract the tarball's content into $HAVENO_RELEASE_NAME. tar -xf $GRADLE_DIST_NAME -C $HAVENO_RELEASE_NAME cd $HAVENO_RELEASE_NAME # Rearrange $HAVENO_RELEASE_NAME contents: move the lib directory up one level. mv -v cli/lib . # Rearrange $HAVENO_RELEASE_NAME contents: remove the cli/bin and cli directories. rm -rf cli # Rearrange $HAVENO_RELEASE_NAME contents: move the lib/cli.jar up one level. mv -v lib/cli.jar . } writemanifest() { # Make the cli.jar runnable, and define its dependencies in a MANIFEST.MF update. echo "Main-Class: haveno.cli.CliMain" > manifest-update.txt printf "Class-Path: " >> manifest-update.txt for file in lib/* do # Each new line in the classpath must be preceded by two spaces. printf " %s\n" "$file" >> manifest-update.txt done } updatemanifest() { # Append contents of to cli.jar's MANIFEST.MF. jar uvfm cli.jar manifest-update.txt } ziprelease() { cd .. zip -r $HAVENO_RELEASE_ZIP_NAME $HAVENO_RELEASE_NAME/lib $HAVENO_RELEASE_NAME/cli.jar } cleanup() { rm -v ./$GRADLE_DIST_NAME rm -r ./$HAVENO_RELEASE_NAME } arrangegradledist writemanifest updatemanifest ziprelease cleanup ================================================ FILE: cli/src/main/java/haveno/cli/CliMain.java ================================================ /* * 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 . */ package haveno.cli; import haveno.cli.opts.ArgumentList; import haveno.cli.opts.CancelOfferOptionParser; import haveno.cli.opts.CreateCryptoCurrencyPaymentAcctOptionParser; import haveno.cli.opts.CreateOfferOptionParser; import haveno.cli.opts.CreatePaymentAcctOptionParser; import haveno.cli.opts.GetAddressBalanceOptionParser; import haveno.cli.opts.GetBTCMarketPriceOptionParser; import haveno.cli.opts.GetBalanceOptionParser; import haveno.cli.opts.GetOffersOptionParser; import haveno.cli.opts.GetPaymentAcctFormOptionParser; import haveno.cli.opts.GetTradeOptionParser; import haveno.cli.opts.GetTradesOptionParser; import haveno.cli.opts.OfferIdOptionParser; import haveno.cli.opts.RegisterDisputeAgentOptionParser; import haveno.cli.opts.RemoveWalletPasswordOptionParser; import haveno.cli.opts.SendBtcOptionParser; import haveno.cli.opts.SetWalletPasswordOptionParser; import haveno.cli.opts.SimpleMethodOptionParser; import haveno.cli.opts.TakeOfferOptionParser; import haveno.cli.opts.UnlockWalletOptionParser; import haveno.cli.opts.WithdrawFundsOptionParser; import haveno.cli.table.builder.TableBuilder; import haveno.proto.grpc.OfferInfo; import io.grpc.StatusRuntimeException; import joptsimple.OptionParser; import joptsimple.OptionSet; import lombok.extern.slf4j.Slf4j; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintStream; import java.io.PrintWriter; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Date; import java.util.List; import static haveno.cli.CurrencyFormat.formatInternalFiatPrice; import static haveno.cli.CurrencyFormat.toSatoshis; import static haveno.cli.Method.canceloffer; import static haveno.cli.Method.closetrade; import static haveno.cli.Method.confirmpaymentreceived; import static haveno.cli.Method.confirmpaymentsent; import static haveno.cli.Method.createcryptopaymentacct; import static haveno.cli.Method.createoffer; import static haveno.cli.Method.createpaymentacct; import static haveno.cli.Method.editoffer; import static haveno.cli.Method.failtrade; import static haveno.cli.Method.getaddressbalance; import static haveno.cli.Method.getbalance; import static haveno.cli.Method.getbtcprice; import static haveno.cli.Method.getfundingaddresses; import static haveno.cli.Method.getmyoffer; import static haveno.cli.Method.getmyoffers; import static haveno.cli.Method.getoffer; import static haveno.cli.Method.getoffers; import static haveno.cli.Method.getpaymentacctform; import static haveno.cli.Method.getpaymentaccts; import static haveno.cli.Method.getpaymentmethods; import static haveno.cli.Method.gettrade; import static haveno.cli.Method.gettrades; import static haveno.cli.Method.gettransaction; import static haveno.cli.Method.gettxfeerate; import static haveno.cli.Method.getunusedbsqaddress; import static haveno.cli.Method.getversion; import static haveno.cli.Method.lockwallet; import static haveno.cli.Method.sendbtc; import static haveno.cli.Method.settxfeerate; import static haveno.cli.Method.setwalletpassword; import static haveno.cli.Method.stop; import static haveno.cli.Method.takeoffer; import static haveno.cli.Method.unfailtrade; import static haveno.cli.Method.unlockwallet; import static haveno.cli.Method.unsettxfeerate; import static haveno.cli.Method.withdrawfunds; import static haveno.cli.opts.OptLabel.OPT_AMOUNT; import static haveno.cli.opts.OptLabel.OPT_HELP; import static haveno.cli.opts.OptLabel.OPT_HOST; import static haveno.cli.opts.OptLabel.OPT_PASSWORD; import static haveno.cli.opts.OptLabel.OPT_PORT; import static haveno.cli.opts.OptLabel.OPT_TX_FEE_RATE; import static haveno.cli.table.builder.TableType.ADDRESS_BALANCE_TBL; import static haveno.cli.table.builder.TableType.BTC_BALANCE_TBL; import static haveno.cli.table.builder.TableType.CLOSED_TRADES_TBL; import static haveno.cli.table.builder.TableType.FAILED_TRADES_TBL; import static haveno.cli.table.builder.TableType.OFFER_TBL; import static haveno.cli.table.builder.TableType.OPEN_TRADES_TBL; import static haveno.cli.table.builder.TableType.PAYMENT_ACCOUNT_TBL; import static haveno.cli.table.builder.TableType.TRADE_DETAIL_TBL; import static haveno.proto.grpc.GetTradesRequest.Category.CLOSED; import static haveno.proto.grpc.GetTradesRequest.Category.OPEN; import static java.lang.String.format; import static java.lang.System.err; import static java.lang.System.exit; import static java.lang.System.out; /** * A command-line client for the Haveno gRPC API. */ @Slf4j public class CliMain { public static void main(String[] args) { try { run(args); } catch (Throwable t) { err.println("Error: " + t.getMessage()); exit(1); } } public static void run(String[] args) { var parser = new OptionParser(); var helpOpt = parser.accepts(OPT_HELP, "Print this help text") .forHelp(); var hostOpt = parser.accepts(OPT_HOST, "rpc server hostname or ip") .withRequiredArg() .defaultsTo("localhost"); var portOpt = parser.accepts(OPT_PORT, "rpc server port") .withRequiredArg() .ofType(Integer.class) .defaultsTo(9998); var passwordOpt = parser.accepts(OPT_PASSWORD, "rpc server password") .withRequiredArg(); // Parse the CLI opts host, port, password, method name, and help. The help opt // may indicate the user is asking for method level help, and will be excluded // from the parsed options if a method opt is present in String[] args. OptionSet options = parser.parse(new ArgumentList(args).getCLIArguments()); @SuppressWarnings("unchecked") var nonOptionArgs = (List) options.nonOptionArguments(); // If neither the help opt nor a method name is present, print CLI level help // to stderr and throw an exception. if (!options.has(helpOpt) && nonOptionArgs.isEmpty()) { printHelp(parser, err); throw new IllegalArgumentException("no method specified"); } // If the help opt is present, but not a method name, print CLI level help // to stdout. if (options.has(helpOpt) && nonOptionArgs.isEmpty()) { printHelp(parser, out); return; } var host = options.valueOf(hostOpt); var port = options.valueOf(portOpt); var password = options.valueOf(passwordOpt); if (password == null) throw new IllegalArgumentException("missing required 'password' option"); var methodName = nonOptionArgs.get(0); Method method; try { method = getMethodFromCmd(methodName); } catch (IllegalArgumentException ex) { throw new IllegalArgumentException(format("'%s' is not a supported method", methodName)); } GrpcClient client = new GrpcClient(host, port, password); try { switch (method) { case getversion: { if (new SimpleMethodOptionParser(args).parse().isForHelp()) { out.println(client.getMethodHelp(method)); return; } var version = client.getVersion(); out.println(version); return; } case getbalance: { var opts = new GetBalanceOptionParser(args).parse(); if (opts.isForHelp()) { out.println(client.getMethodHelp(method)); return; } var currencyCode = opts.getCurrencyCode(); var balances = client.getBalances(currencyCode); switch (currencyCode.toUpperCase()) { case "BTC": new TableBuilder(BTC_BALANCE_TBL, balances.getBtc()).build().print(out); break; case "": default: { out.println("BTC"); new TableBuilder(BTC_BALANCE_TBL, balances.getBtc()).build().print(out); break; } } return; } case getaddressbalance: { var opts = new GetAddressBalanceOptionParser(args).parse(); if (opts.isForHelp()) { out.println(client.getMethodHelp(method)); return; } var address = opts.getAddress(); var addressBalance = client.getAddressBalance(address); new TableBuilder(ADDRESS_BALANCE_TBL, addressBalance).build().print(out); return; } case getbtcprice: { var opts = new GetBTCMarketPriceOptionParser(args).parse(); if (opts.isForHelp()) { out.println(client.getMethodHelp(method)); return; } var currencyCode = opts.getCurrencyCode(); var price = client.getBtcPrice(currencyCode); out.println(formatInternalFiatPrice(price)); return; } case getfundingaddresses: { if (new SimpleMethodOptionParser(args).parse().isForHelp()) { out.println(client.getMethodHelp(method)); return; } var fundingAddresses = client.getFundingAddresses(); new TableBuilder(ADDRESS_BALANCE_TBL, fundingAddresses).build().print(out); return; } case sendbtc: { var opts = new SendBtcOptionParser(args).parse(); if (opts.isForHelp()) { out.println(client.getMethodHelp(method)); return; } var address = opts.getAddress(); var amount = opts.getAmount(); verifyStringIsValidDecimal(OPT_AMOUNT, amount); var txFeeRate = opts.getFeeRate(); if (!txFeeRate.isEmpty()) verifyStringIsValidLong(OPT_TX_FEE_RATE, txFeeRate); var memo = opts.getMemo(); throw new RuntimeException("Send BTC not implemented"); } case createoffer: { var opts = new CreateOfferOptionParser(args).parse(); if (opts.isForHelp()) { out.println(client.getMethodHelp(method)); return; } var paymentAcctId = opts.getPaymentAccountId(); var direction = opts.getDirection(); var currencyCode = opts.getCurrencyCode(); var amount = toSatoshis(opts.getAmount()); var minAmount = toSatoshis(opts.getMinAmount()); var useMarketBasedPrice = opts.isUsingMktPriceMargin(); var fixedPrice = opts.getFixedPrice(); var marketPriceMarginPct = opts.getMktPriceMarginPct(); var securityDepositPct = opts.getSecurityDepositPct(); var triggerPrice = "0"; // Cannot be defined until the new offer is added to book. OfferInfo offer; offer = client.createOffer(direction, currencyCode, amount, minAmount, useMarketBasedPrice, fixedPrice, marketPriceMarginPct, securityDepositPct, paymentAcctId, triggerPrice); new TableBuilder(OFFER_TBL, offer).build().print(out); return; } case canceloffer: { var opts = new CancelOfferOptionParser(args).parse(); if (opts.isForHelp()) { out.println(client.getMethodHelp(method)); return; } var offerId = opts.getOfferId(); client.cancelOffer(offerId); out.println("offer canceled and removed from offer book"); return; } case getoffer: { var opts = new OfferIdOptionParser(args).parse(); if (opts.isForHelp()) { out.println(client.getMethodHelp(method)); return; } var offerId = opts.getOfferId(); var offer = client.getOffer(offerId); new TableBuilder(OFFER_TBL, offer).build().print(out); return; } case getmyoffer: { var opts = new OfferIdOptionParser(args).parse(); if (opts.isForHelp()) { out.println(client.getMethodHelp(method)); return; } var offerId = opts.getOfferId(); var offer = client.getMyOffer(offerId); new TableBuilder(OFFER_TBL, offer).build().print(out); return; } case getoffers: { var opts = new GetOffersOptionParser(args).parse(); if (opts.isForHelp()) { out.println(client.getMethodHelp(method)); return; } var direction = opts.getDirection(); var currencyCode = opts.getCurrencyCode(); List offers = client.getOffers(direction, currencyCode); if (offers.isEmpty()) out.printf("no %s %s offers found%n", direction, currencyCode); else new TableBuilder(OFFER_TBL, offers).build().print(out); return; } case getmyoffers: { var opts = new GetOffersOptionParser(args).parse(); if (opts.isForHelp()) { out.println(client.getMethodHelp(method)); return; } var direction = opts.getDirection(); var currencyCode = opts.getCurrencyCode(); List offers = client.getMyOffers(direction, currencyCode); if (offers.isEmpty()) out.printf("no %s %s offers found%n", direction, currencyCode); else new TableBuilder(OFFER_TBL, offers).build().print(out); return; } case takeoffer: { var opts = new TakeOfferOptionParser(args).parse(); if (opts.isForHelp()) { out.println(client.getMethodHelp(method)); return; } var offerId = opts.getOfferId(); var paymentAccountId = opts.getPaymentAccountId(); var trade = client.takeOffer(offerId, paymentAccountId); out.printf("trade %s successfully taken%n", trade.getTradeId()); return; } case gettrade: { // TODO make short-id a valid argument? var opts = new GetTradeOptionParser(args).parse(); if (opts.isForHelp()) { out.println(client.getMethodHelp(method)); return; } var tradeId = opts.getTradeId(); var showContract = opts.getShowContract(); var trade = client.getTrade(tradeId); if (showContract) out.println(trade.getContractAsJson()); else new TableBuilder(TRADE_DETAIL_TBL, trade).build().print(out); return; } case gettrades: { var opts = new GetTradesOptionParser(args).parse(); if (opts.isForHelp()) { out.println(client.getMethodHelp(method)); return; } var category = opts.getCategory(); var trades = category.equals(OPEN) ? client.getOpenTrades() : client.getTradeHistory(category); if (trades.isEmpty()) { out.printf("no %s trades found%n", category.name().toLowerCase()); } else { var tableType = category.equals(OPEN) ? OPEN_TRADES_TBL : category.equals(CLOSED) ? CLOSED_TRADES_TBL : FAILED_TRADES_TBL; new TableBuilder(tableType, trades).build().print(out); } return; } case confirmpaymentsent: { var opts = new GetTradeOptionParser(args).parse(); if (opts.isForHelp()) { out.println(client.getMethodHelp(method)); return; } var tradeId = opts.getTradeId(); client.confirmPaymentSent(tradeId); out.printf("trade %s payment started message sent%n", tradeId); return; } case confirmpaymentreceived: { var opts = new GetTradeOptionParser(args).parse(); if (opts.isForHelp()) { out.println(client.getMethodHelp(method)); return; } var tradeId = opts.getTradeId(); client.confirmPaymentReceived(tradeId); out.printf("trade %s payment received message sent%n", tradeId); return; } case withdrawfunds: { var opts = new WithdrawFundsOptionParser(args).parse(); if (opts.isForHelp()) { out.println(client.getMethodHelp(method)); return; } var tradeId = opts.getTradeId(); var address = opts.getAddress(); // Multi-word memos must be double-quoted. var memo = opts.getMemo(); client.withdrawFunds(tradeId, address, memo); out.printf("trade %s funds sent to btc address %s%n", tradeId, address); return; } case getpaymentmethods: { if (new SimpleMethodOptionParser(args).parse().isForHelp()) { out.println(client.getMethodHelp(method)); return; } var paymentMethods = client.getPaymentMethods(); paymentMethods.forEach(p -> out.println(p.getId())); return; } case getpaymentacctform: { var opts = new GetPaymentAcctFormOptionParser(args).parse(); if (opts.isForHelp()) { out.println(client.getMethodHelp(method)); return; } var paymentMethodId = opts.getPaymentMethodId(); String jsonString = client.getPaymentAcctFormAsJson(paymentMethodId); File jsonFile = saveFileToDisk(paymentMethodId.toLowerCase(), ".json", jsonString); out.printf("payment account form %s%nsaved to %s%n", jsonString, jsonFile.getAbsolutePath()); out.println("Edit the file, and use as the argument to a 'createpaymentacct' command."); return; } case createpaymentacct: { var opts = new CreatePaymentAcctOptionParser(args).parse(); if (opts.isForHelp()) { out.println(client.getMethodHelp(method)); return; } var paymentAccountForm = opts.getPaymentAcctForm(); String jsonString; try { jsonString = new String(Files.readAllBytes(paymentAccountForm)); } catch (IOException e) { throw new IllegalStateException( format("could not read %s", paymentAccountForm)); } var paymentAccount = client.createPaymentAccount(jsonString); out.println("payment account saved"); new TableBuilder(PAYMENT_ACCOUNT_TBL, paymentAccount).build().print(out); return; } case createcryptopaymentacct: { var opts = new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse(); if (opts.isForHelp()) { out.println(client.getMethodHelp(method)); return; } var accountName = opts.getAccountName(); var currencyCode = opts.getCurrencyCode(); var address = opts.getAddress(); var isTradeInstant = opts.getIsTradeInstant(); var paymentAccount = client.createCryptoCurrencyPaymentAccount(accountName, currencyCode, address, isTradeInstant); out.println("payment account saved"); new TableBuilder(PAYMENT_ACCOUNT_TBL, paymentAccount).build().print(out); return; } case getpaymentaccts: { if (new SimpleMethodOptionParser(args).parse().isForHelp()) { out.println(client.getMethodHelp(method)); return; } var paymentAccounts = client.getPaymentAccounts(); if (paymentAccounts.size() > 0) new TableBuilder(PAYMENT_ACCOUNT_TBL, paymentAccounts).build().print(out); else out.println("no payment accounts are saved"); return; } case lockwallet: { if (new SimpleMethodOptionParser(args).parse().isForHelp()) { out.println(client.getMethodHelp(method)); return; } client.lockWallet(); out.println("wallet locked"); return; } case unlockwallet: { var opts = new UnlockWalletOptionParser(args).parse(); if (opts.isForHelp()) { out.println(client.getMethodHelp(method)); return; } var walletPassword = opts.getPassword(); var timeout = opts.getUnlockTimeout(); client.unlockWallet(walletPassword, timeout); out.println("wallet unlocked"); return; } case removewalletpassword: { var opts = new RemoveWalletPasswordOptionParser(args).parse(); if (opts.isForHelp()) { out.println(client.getMethodHelp(method)); return; } var walletPassword = opts.getPassword(); client.removeWalletPassword(walletPassword); out.println("wallet decrypted"); return; } case setwalletpassword: { var opts = new SetWalletPasswordOptionParser(args).parse(); if (opts.isForHelp()) { out.println(client.getMethodHelp(method)); return; } var walletPassword = opts.getPassword(); var newWalletPassword = opts.getNewPassword(); client.setWalletPassword(walletPassword, newWalletPassword); out.println("wallet encrypted" + (!newWalletPassword.isEmpty() ? " with new password" : "")); return; } case registerdisputeagent: { var opts = new RegisterDisputeAgentOptionParser(args).parse(); if (opts.isForHelp()) { out.println(client.getMethodHelp(method)); return; } var disputeAgentType = opts.getDisputeAgentType(); var registrationKey = opts.getRegistrationKey(); client.registerDisputeAgent(disputeAgentType, registrationKey); out.println(disputeAgentType + " registered"); return; } case stop: { if (new SimpleMethodOptionParser(args).parse().isForHelp()) { out.println(client.getMethodHelp(method)); return; } client.stopServer(); out.println("server shutdown signal received"); return; } default: { throw new RuntimeException(format("unhandled method '%s'", method)); } } } catch (StatusRuntimeException ex) { // Remove the leading gRPC status code, e.g., INVALID_ARGUMENT, // NOT_FOUND, ..., UNKNOWN from the exception message. String message = ex.getMessage().replaceFirst("^[A-Z_]+: ", ""); if (message.equals("io exception")) throw new RuntimeException(message + ", server may not be running", ex); else throw new RuntimeException(message, ex); } } private static Method getMethodFromCmd(String methodName) { // TODO if we use const type for enum we need add some mapping. Even if we don't // change now it is handy to have flexibility in case we change internal code // and don't want to break user commands. return Method.valueOf(methodName.toLowerCase()); } @SuppressWarnings("SameParameterValue") private static void verifyStringIsValidDecimal(String optionLabel, String optionValue) { try { Double.parseDouble(optionValue); } catch (NumberFormatException ex) { throw new IllegalArgumentException(format("--%s=%s, '%s' is not a number", optionLabel, optionValue, optionValue)); } } @SuppressWarnings("SameParameterValue") private static void verifyStringIsValidLong(String optionLabel, String optionValue) { try { Long.parseLong(optionValue); } catch (NumberFormatException ex) { throw new IllegalArgumentException(format("--%s=%s, '%s' is not a number", optionLabel, optionValue, optionValue)); } } private static long toLong(String param) { try { return Long.parseLong(param); } catch (NumberFormatException ex) { throw new IllegalArgumentException(format("'%s' is not a number", param)); } } private static File saveFileToDisk(String prefix, @SuppressWarnings("SameParameterValue") String suffix, String text) { String timestamp = Long.toUnsignedString(new Date().getTime()); String relativeFileName = prefix + "_" + timestamp + suffix; try { Path path = Paths.get(relativeFileName); if (!Files.exists(path)) { try (PrintWriter out = new PrintWriter(path.toString())) { out.println(text); } return path.toAbsolutePath().toFile(); } else { throw new IllegalStateException(format("could not overwrite existing file '%s'", relativeFileName)); } } catch (FileNotFoundException e) { throw new IllegalStateException(format("could not create file '%s'", relativeFileName)); } } private static void printHelp(OptionParser parser, @SuppressWarnings("SameParameterValue") PrintStream stream) { try { stream.println("Haveno RPC Client"); stream.println(); stream.println("Usage: haveno-cli [options] [params]"); stream.println(); parser.printHelpOn(stream); stream.println(); String rowFormat = "%-25s%-52s%s%n"; stream.format(rowFormat, "Method", "Params", "Description"); stream.format(rowFormat, "------", "------", "------------"); stream.format(rowFormat, getversion.name(), "", "Get server version"); stream.println(); stream.format(rowFormat, getbalance.name(), "[--currency-code=]", "Get server wallet balances"); stream.println(); stream.format(rowFormat, getaddressbalance.name(), "--address=", "Get server wallet address balance"); stream.println(); stream.format(rowFormat, getbtcprice.name(), "--currency-code=", "Get current market btc price"); stream.println(); stream.format(rowFormat, getfundingaddresses.name(), "", "Get BTC funding addresses"); stream.println(); stream.format(rowFormat, getunusedbsqaddress.name(), "", "Get unused BSQ address"); stream.println(); stream.format(rowFormat, "", "[--tx-fee-rate=]", ""); stream.println(); stream.format(rowFormat, sendbtc.name(), "--address= --amount= \\", "Send BTC"); stream.format(rowFormat, "", "[--tx-fee-rate=]", ""); stream.format(rowFormat, "", "[--memo=<\"memo\">]", ""); stream.println(); stream.format(rowFormat, gettxfeerate.name(), "", "Get current tx fee rate in sats/byte"); stream.println(); stream.format(rowFormat, settxfeerate.name(), "--tx-fee-rate=", "Set custom tx fee rate in sats/byte"); stream.println(); stream.format(rowFormat, unsettxfeerate.name(), "", "Unset custom tx fee rate"); stream.println(); stream.format(rowFormat, gettransaction.name(), "--transaction-id=", "Get transaction with id"); stream.println(); stream.format(rowFormat, createoffer.name(), "--payment-account= \\", "Create and place an offer"); stream.format(rowFormat, "", "--direction= \\", ""); stream.format(rowFormat, "", "--currency-code= \\", ""); stream.format(rowFormat, "", "--amount= \\", ""); stream.format(rowFormat, "", "[--min-amount=] \\", ""); stream.format(rowFormat, "", "--fixed-price= | --market-price-margin= \\", ""); stream.format(rowFormat, "", "--security-deposit= \\", ""); stream.format(rowFormat, "", "[--fee-currency=]", ""); stream.format(rowFormat, "", "[--trigger-price=]", ""); stream.format(rowFormat, "", "[--swap=]", ""); stream.println(); stream.format(rowFormat, editoffer.name(), "--offer-id= \\", "Edit offer with id"); stream.format(rowFormat, "", "[--fixed-price=] \\", ""); stream.format(rowFormat, "", "[--market-price-margin=] \\", ""); stream.format(rowFormat, "", "[--trigger-price=] \\", ""); stream.format(rowFormat, "", "[--enabled=]", ""); stream.println(); stream.format(rowFormat, canceloffer.name(), "--offer-id=", "Cancel offer with id"); stream.println(); stream.format(rowFormat, getoffer.name(), "--offer-id=", "Get current offer with id"); stream.println(); stream.format(rowFormat, getmyoffer.name(), "--offer-id=", "Get my current offer with id"); stream.println(); stream.format(rowFormat, getoffers.name(), "--direction= \\", "Get current offers"); stream.format(rowFormat, "", "--currency-code=", ""); stream.println(); stream.format(rowFormat, getmyoffers.name(), "--direction= \\", "Get my current offers"); stream.format(rowFormat, "", "--currency-code=", ""); stream.println(); stream.format(rowFormat, takeoffer.name(), "--offer-id= \\", "Take offer with id"); stream.format(rowFormat, "", "[--payment-account=]", ""); stream.format(rowFormat, "", "[--fee-currency=]", ""); stream.println(); stream.format(rowFormat, gettrade.name(), "--trade-id= \\", "Get trade summary or full contract"); stream.format(rowFormat, "", "[--show-contract=]", ""); stream.println(); stream.format(rowFormat, gettrades.name(), "[--category=]", "Get open (default), closed, or failed trades"); stream.println(); stream.format(rowFormat, confirmpaymentsent.name(), "--trade-id=", "Confirm payment started"); stream.println(); stream.format(rowFormat, confirmpaymentreceived.name(), "--trade-id=", "Confirm payment received"); stream.println(); stream.format(rowFormat, closetrade.name(), "--trade-id=", "Close completed trade"); stream.println(); stream.format(rowFormat, withdrawfunds.name(), "--trade-id= --address= \\", "Withdraw received trade funds to external wallet address"); stream.format(rowFormat, "", "[--memo=<\"memo\">]", ""); stream.println(); stream.format(rowFormat, failtrade.name(), "--trade-id=", "Change open trade to failed trade"); stream.println(); stream.format(rowFormat, unfailtrade.name(), "--trade-id=", "Change failed trade to open trade"); stream.println(); stream.format(rowFormat, getpaymentmethods.name(), "", "Get list of supported payment account method ids"); stream.println(); stream.format(rowFormat, getpaymentacctform.name(), "--payment-method-id=", "Get a new payment account form"); stream.println(); stream.format(rowFormat, createpaymentacct.name(), "--payment-account-form=", "Create a new payment account"); stream.println(); stream.format(rowFormat, createcryptopaymentacct.name(), "--account-name= \\", "Create a new cryptocurrency payment account"); stream.format(rowFormat, "", "--currency-code= \\", ""); stream.format(rowFormat, "", "--address=", ""); stream.format(rowFormat, "", "--trade-instant=", ""); stream.println(); stream.format(rowFormat, getpaymentaccts.name(), "", "Get user payment accounts"); stream.println(); stream.format(rowFormat, lockwallet.name(), "", "Remove wallet password from memory, locking the wallet"); stream.println(); stream.format(rowFormat, unlockwallet.name(), "--wallet-password= --timeout=", "Store wallet password in memory for timeout seconds"); stream.println(); stream.format(rowFormat, setwalletpassword.name(), "--wallet-password= \\", "Encrypt wallet with password, or set new password on encrypted wallet"); stream.format(rowFormat, "", "[--new-wallet-password=]", ""); stream.println(); stream.format(rowFormat, stop.name(), "", "Shut down the server"); stream.println(); stream.println("Method Help Usage: haveno-cli [options] --help"); stream.println(); } catch (IOException ex) { ex.printStackTrace(stream); } } } ================================================ FILE: cli/src/main/java/haveno/cli/ColumnHeaderConstants.java ================================================ /* * 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 . */ package haveno.cli; import static com.google.common.base.Strings.padEnd; import static com.google.common.base.Strings.padStart; class ColumnHeaderConstants { // For inserting 2 spaces between column headers. static final String COL_HEADER_DELIMITER = " "; // Table column header format specs, right padded with two spaces. In some cases // such as COL_HEADER_CREATION_DATE, COL_HEADER_VOLUME and COL_HEADER_UUID, the // expected max data string length is accounted for. In others, column header // lengths are expected to be greater than any column value length. static final String COL_HEADER_ADDRESS = padEnd("%-3s Address", 52, ' '); static final String COL_HEADER_AMOUNT = "BTC(min - max)"; static final String COL_HEADER_BALANCE = "Balance"; static final String COL_HEADER_AVAILABLE_BALANCE = "Available Balance"; static final String COL_HEADER_AVAILABLE_CONFIRMED_BALANCE = "Available Confirmed Balance"; static final String COL_HEADER_UNCONFIRMED_CHANGE_BALANCE = "Unconfirmed Change Balance"; static final String COL_HEADER_RESERVED_BALANCE = "Reserved Balance"; static final String COL_HEADER_TOTAL_AVAILABLE_BALANCE = "Total Available Balance"; static final String COL_HEADER_LOCKED_BALANCE = "Locked Balance"; static final String COL_HEADER_RESERVED_OFFER_BALANCE = "Reserved Offer Balance"; static final String COL_HEADER_RESERVED_TRADE_BALANCE = "Reserved Trade Balance"; static final String COL_HEADER_LOCKED_FOR_VOTING_BALANCE = "Locked For Voting Balance"; static final String COL_HEADER_LOCKUP_BONDS_BALANCE = "Lockup Bonds Balance"; static final String COL_HEADER_UNLOCKING_BONDS_BALANCE = "Unlocking Bonds Balance"; static final String COL_HEADER_UNVERIFIED_BALANCE = "Unverified Balance"; static final String COL_HEADER_CONFIRMATIONS = "Confirmations"; static final String COL_HEADER_IS_USED_ADDRESS = "Is Used"; static final String COL_HEADER_CREATION_DATE = padEnd("Creation Date (UTC)", 20, ' '); static final String COL_HEADER_CURRENCY = "Currency"; static final String COL_HEADER_DIRECTION = "Buy/Sell"; static final String COL_HEADER_NAME = "Name"; static final String COL_HEADER_PAYMENT_METHOD = "Payment Method"; static final String COL_HEADER_PRICE = "Price in %-3s for 1 BTC"; static final String COL_HEADER_PRICE_OF_CRYPTO = "Price in BTC for 1 %-3s"; static final String COL_HEADER_TRADE_AMOUNT = padStart("Amount(%-3s)", 12, ' '); static final String COL_HEADER_TRADE_BUYER_COST = padEnd("Buyer Cost(%-3s)", 15, ' '); static final String COL_HEADER_TRADE_DEPOSIT_CONFIRMED = "Deposit Confirmed"; static final String COL_HEADER_TRADE_DEPOSIT_PUBLISHED = "Deposit Published"; static final String COL_HEADER_TRADE_PAYMENT_SENT = padEnd("%-3s Sent", 8, ' '); static final String COL_HEADER_TRADE_PAYMENT_RECEIVED = padEnd("%-3s Received", 12, ' '); static final String COL_HEADER_TRADE_PAYOUT_PUBLISHED = "Payout Published"; static final String COL_HEADER_TRADE_WITHDRAWN = "Withdrawn"; static final String COL_HEADER_TRADE_ROLE = "My Role"; static final String COL_HEADER_TRADE_SHORT_ID = "ID"; static final String COL_HEADER_TRADE_TX_FEE = padEnd("Tx Fee(BTC)", 12, ' '); static final String COL_HEADER_TRADE_MAKER_FEE = padEnd("Maker Fee(%-3s)", 12, ' '); // "Maker Fee(%-3s)"; static final String COL_HEADER_TRADE_TAKER_FEE = padEnd("Taker Fee(%-3s)", 12, ' '); // "Taker Fee(%-3s)"; static final String COL_HEADER_TX_ID = "Tx ID"; static final String COL_HEADER_TX_INPUT_SUM = "Tx Inputs (BTC)"; static final String COL_HEADER_TX_OUTPUT_SUM = "Tx Outputs (BTC)"; static final String COL_HEADER_TX_FEE = "Tx Fee (BTC)"; static final String COL_HEADER_TX_SIZE = "Tx Size (Bytes)"; static final String COL_HEADER_TX_IS_CONFIRMED = "Is Confirmed"; static final String COL_HEADER_TX_MEMO = "Memo"; static final String COL_HEADER_VOLUME = padEnd("%-3s(min - max)", 15, ' '); static final String COL_HEADER_UUID = padEnd("ID", 52, ' '); } ================================================ FILE: cli/src/main/java/haveno/cli/CryptoCurrencyUtil.java ================================================ /* * 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 . */ package haveno.cli; import java.util.ArrayList; import java.util.List; public class CryptoCurrencyUtil { public static boolean apiDoesSupportCryptoCurrency(String currencyCode) { return getSupportedCryptoCurrencies().contains(currencyCode.toUpperCase()); } public static List getSupportedCryptoCurrencies() { final List result = new ArrayList<>(); result.add("BCH"); result.sort(String::compareTo); return result; } } ================================================ FILE: cli/src/main/java/haveno/cli/CurrencyFormat.java ================================================ /* * 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 . */ package haveno.cli; import com.google.common.annotations.VisibleForTesting; import java.math.BigDecimal; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; import java.util.Locale; import static java.lang.String.format; import static java.math.RoundingMode.HALF_UP; import static java.math.RoundingMode.UNNECESSARY; /** * Utility for formatting amounts, volumes and fees; there is no i18n support in the CLI. */ @VisibleForTesting public class CurrencyFormat { // Use the US locale as a base for all DecimalFormats, but commas should be omitted from number strings. private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = DecimalFormatSymbols.getInstance(Locale.US); // Use the US locale as a base for all NumberFormats, but commas should be omitted from number strings. private static final NumberFormat US_LOCALE_NUMBER_FORMAT = NumberFormat.getInstance(Locale.US); // Formats numbers for internal use, i.e., grpc request parameters. private static final DecimalFormat INTERNAL_FIAT_DECIMAL_FORMAT = new DecimalFormat("##############0.0000"); static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100_000_000); static final DecimalFormat SATOSHI_FORMAT = new DecimalFormat("###,##0.00000000", DECIMAL_FORMAT_SYMBOLS); static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.########", DECIMAL_FORMAT_SYMBOLS); static final DecimalFormat BTC_TX_FEE_FORMAT = new DecimalFormat("###,###,##0", DECIMAL_FORMAT_SYMBOLS); static final BigDecimal BSQ_SATOSHI_DIVISOR = new BigDecimal(100); static final DecimalFormat BSQ_FORMAT = new DecimalFormat("###,###,###,##0.00", DECIMAL_FORMAT_SYMBOLS); public static String formatSatoshis(String sats) { //noinspection BigDecimalMethodWithoutRoundingCalled return SATOSHI_FORMAT.format(new BigDecimal(sats).divide(SATOSHI_DIVISOR)); } @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") public static String formatSatoshis(long sats) { return SATOSHI_FORMAT.format(new BigDecimal(sats).divide(SATOSHI_DIVISOR)); } @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") public static String formatBtc(long sats) { return BTC_FORMAT.format(new BigDecimal(sats).divide(SATOSHI_DIVISOR)); } @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") public static String formatBsq(long sats) { return BSQ_FORMAT.format(new BigDecimal(sats).divide(BSQ_SATOSHI_DIVISOR)); } public static String formatInternalFiatPrice(BigDecimal price) { INTERNAL_FIAT_DECIMAL_FORMAT.setMinimumFractionDigits(4); INTERNAL_FIAT_DECIMAL_FORMAT.setMaximumFractionDigits(4); return INTERNAL_FIAT_DECIMAL_FORMAT.format(price); } public static String formatInternalFiatPrice(double price) { US_LOCALE_NUMBER_FORMAT.setMinimumFractionDigits(4); US_LOCALE_NUMBER_FORMAT.setMaximumFractionDigits(4); return US_LOCALE_NUMBER_FORMAT.format(price); } public static String formatPrice(long price) { US_LOCALE_NUMBER_FORMAT.setMinimumFractionDigits(4); US_LOCALE_NUMBER_FORMAT.setMaximumFractionDigits(4); US_LOCALE_NUMBER_FORMAT.setRoundingMode(UNNECESSARY); return US_LOCALE_NUMBER_FORMAT.format((double) price / 10_000); } public static String formatFiatVolume(long volume) { US_LOCALE_NUMBER_FORMAT.setMinimumFractionDigits(0); US_LOCALE_NUMBER_FORMAT.setMaximumFractionDigits(0); US_LOCALE_NUMBER_FORMAT.setRoundingMode(HALF_UP); return US_LOCALE_NUMBER_FORMAT.format((double) volume / 10_000); } public static long toSatoshis(String btc) { if (btc.startsWith("-")) throw new IllegalArgumentException(format("'%s' is not a positive number", btc)); try { return new BigDecimal(btc).multiply(SATOSHI_DIVISOR).longValue(); } catch (NumberFormatException e) { throw new IllegalArgumentException(format("'%s' is not a number", btc)); } } public static String formatFeeSatoshis(long sats) { return BTC_TX_FEE_FORMAT.format(BigDecimal.valueOf(sats)); } } ================================================ FILE: cli/src/main/java/haveno/cli/DirectionFormat.java ================================================ /* * 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 . */ package haveno.cli; import haveno.proto.grpc.OfferInfo; import java.util.List; import java.util.function.Function; import static haveno.cli.ColumnHeaderConstants.COL_HEADER_DIRECTION; import static java.lang.String.format; import static protobuf.OfferDirection.BUY; import static protobuf.OfferDirection.SELL; class DirectionFormat { static int getLongestDirectionColWidth(List offers) { if (offers.isEmpty() || offers.get(0).getBaseCurrencyCode().equals("XMR")) return COL_HEADER_DIRECTION.length(); else return 18; // .e.g., "Sell BSQ (Buy XMR)".length() } static final Function directionFormat = (offer) -> { String baseCurrencyCode = offer.getBaseCurrencyCode(); boolean isCryptoCurrencyOffer = !baseCurrencyCode.equals("XMR"); if (!isCryptoCurrencyOffer) { return baseCurrencyCode; } else { // Return "Sell BSQ (Buy XMR)", or "Buy BSQ (Sell XMR)". String direction = offer.getDirection(); String mirroredDirection = getMirroredDirection(direction); Function mixedCase = (word) -> word.charAt(0) + word.substring(1).toLowerCase(); return format("%s %s (%s %s)", mixedCase.apply(mirroredDirection), baseCurrencyCode, mixedCase.apply(direction), offer.getCounterCurrencyCode()); } }; static String getMirroredDirection(String directionAsString) { return directionAsString.equalsIgnoreCase(BUY.name()) ? SELL.name() : BUY.name(); } } ================================================ FILE: cli/src/main/java/haveno/cli/GrpcClient.java ================================================ /* * 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 . */ package haveno.cli; import haveno.cli.request.OffersServiceRequest; import haveno.cli.request.PaymentAccountsServiceRequest; import haveno.cli.request.TradesServiceRequest; import haveno.cli.request.WalletsServiceRequest; import haveno.proto.grpc.AddressBalanceInfo; import haveno.proto.grpc.BalancesInfo; import haveno.proto.grpc.BtcBalanceInfo; import haveno.proto.grpc.GetMethodHelpRequest; import haveno.proto.grpc.GetTradesRequest; import haveno.proto.grpc.GetVersionRequest; import haveno.proto.grpc.OfferInfo; import haveno.proto.grpc.RegisterDisputeAgentRequest; import haveno.proto.grpc.StopRequest; import haveno.proto.grpc.TradeInfo; import lombok.extern.slf4j.Slf4j; import protobuf.PaymentAccount; import protobuf.PaymentMethod; import java.util.List; @SuppressWarnings("ResultOfMethodCallIgnored") @Slf4j public final class GrpcClient { private final GrpcStubs grpcStubs; private final OffersServiceRequest offersServiceRequest; private final TradesServiceRequest tradesServiceRequest; private final WalletsServiceRequest walletsServiceRequest; private final PaymentAccountsServiceRequest paymentAccountsServiceRequest; public GrpcClient(String apiHost, int apiPort, String apiPassword) { this.grpcStubs = new GrpcStubs(apiHost, apiPort, apiPassword); this.offersServiceRequest = new OffersServiceRequest(grpcStubs); this.tradesServiceRequest = new TradesServiceRequest(grpcStubs); this.walletsServiceRequest = new WalletsServiceRequest(grpcStubs); this.paymentAccountsServiceRequest = new PaymentAccountsServiceRequest(grpcStubs); } public String getVersion() { var request = GetVersionRequest.newBuilder().build(); return grpcStubs.versionService.getVersion(request).getVersion(); } public BalancesInfo getBalances() { return walletsServiceRequest.getBalances(); } public BtcBalanceInfo getBtcBalances() { return walletsServiceRequest.getBtcBalances(); } public BalancesInfo getBalances(String currencyCode) { return walletsServiceRequest.getBalances(currencyCode); } public AddressBalanceInfo getAddressBalance(String address) { return walletsServiceRequest.getAddressBalance(address); } public double getBtcPrice(String currencyCode) { return walletsServiceRequest.getBtcPrice(currencyCode); } public List getFundingAddresses() { return walletsServiceRequest.getFundingAddresses(); } public String getUnusedBtcAddress() { return walletsServiceRequest.getUnusedBtcAddress(); } public OfferInfo createFixedPricedOffer(String direction, String currencyCode, long amount, long minAmount, String fixedPrice, double securityDepositPct, String paymentAcctId) { return offersServiceRequest.createOffer(direction, currencyCode, amount, minAmount, false, fixedPrice, 0.00, securityDepositPct, paymentAcctId, "0" /* no trigger price */); } public OfferInfo createMarketBasedPricedOffer(String direction, String currencyCode, long amount, long minAmount, double marketPriceMarginPct, double securityDepositPct, String paymentAcctId, String triggerPrice) { return offersServiceRequest.createOffer(direction, currencyCode, amount, minAmount, true, "0", marketPriceMarginPct, securityDepositPct, paymentAcctId, triggerPrice); } public OfferInfo createOffer(String direction, String currencyCode, long amount, long minAmount, boolean useMarketBasedPrice, String fixedPrice, double marketPriceMarginPct, double securityDepositPct, String paymentAcctId, String triggerPrice) { return offersServiceRequest.createOffer(direction, currencyCode, amount, minAmount, useMarketBasedPrice, fixedPrice, marketPriceMarginPct, securityDepositPct, paymentAcctId, triggerPrice); } public void cancelOffer(String offerId) { offersServiceRequest.cancelOffer(offerId); } public OfferInfo getOffer(String offerId) { return offersServiceRequest.getOffer(offerId); } @Deprecated // Since 5-Dec-2021. // Endpoint to be removed from future version. Use getOffer service method instead. public OfferInfo getMyOffer(String offerId) { return offersServiceRequest.getMyOffer(offerId); } public List getOffers(String direction, String currencyCode) { return offersServiceRequest.getOffers(direction, currencyCode); } public List getOffersSortedByDate(String currencyCode) { return offersServiceRequest.getOffersSortedByDate(currencyCode); } public List getOffersSortedByDate(String direction, String currencyCode) { return offersServiceRequest.getOffersSortedByDate(direction, currencyCode); } public List getMyOffers(String direction, String currencyCode) { return offersServiceRequest.getMyOffers(direction, currencyCode); } public List getMyOffersSortedByDate(String currencyCode) { return offersServiceRequest.getMyOffersSortedByDate(currencyCode); } public List getMyOffersSortedByDate(String direction, String currencyCode) { return offersServiceRequest.getMyOffersSortedByDate(direction, currencyCode); } public TradeInfo takeOffer(String offerId, String paymentAccountId) { return tradesServiceRequest.takeOffer(offerId, paymentAccountId); } public TradeInfo getTrade(String tradeId) { return tradesServiceRequest.getTrade(tradeId); } public List getOpenTrades() { return tradesServiceRequest.getOpenTrades(); } public List getTradeHistory(GetTradesRequest.Category category) { return tradesServiceRequest.getTradeHistory(category); } public void confirmPaymentSent(String tradeId) { tradesServiceRequest.confirmPaymentSent(tradeId); } public void confirmPaymentReceived(String tradeId) { tradesServiceRequest.confirmPaymentReceived(tradeId); } public void withdrawFunds(String tradeId, String address, String memo) { tradesServiceRequest.withdrawFunds(tradeId, address, memo); } public List getPaymentMethods() { return paymentAccountsServiceRequest.getPaymentMethods(); } public String getPaymentAcctFormAsJson(String paymentMethodId) { return paymentAccountsServiceRequest.getPaymentAcctFormAsJson(paymentMethodId); } public PaymentAccount createPaymentAccount(String json) { return paymentAccountsServiceRequest.createPaymentAccount(json); } public List getPaymentAccounts() { return paymentAccountsServiceRequest.getPaymentAccounts(); } public PaymentAccount getPaymentAccount(String accountName) { return paymentAccountsServiceRequest.getPaymentAccount(accountName); } public PaymentAccount createCryptoCurrencyPaymentAccount(String accountName, String currencyCode, String address, boolean tradeInstant) { return paymentAccountsServiceRequest.createCryptoCurrencyPaymentAccount(accountName, currencyCode, address, tradeInstant); } public List getCryptoPaymentMethods() { return paymentAccountsServiceRequest.getCryptoPaymentMethods(); } public void lockWallet() { walletsServiceRequest.lockWallet(); } public void unlockWallet(String walletPassword, long timeout) { walletsServiceRequest.unlockWallet(walletPassword, timeout); } public void removeWalletPassword(String walletPassword) { walletsServiceRequest.removeWalletPassword(walletPassword); } public void setWalletPassword(String walletPassword) { walletsServiceRequest.setWalletPassword(walletPassword); } public void setWalletPassword(String oldWalletPassword, String newWalletPassword) { walletsServiceRequest.setWalletPassword(oldWalletPassword, newWalletPassword); } public void registerDisputeAgent(String disputeAgentType, String registrationKey) { var request = RegisterDisputeAgentRequest.newBuilder() .setDisputeAgentType(disputeAgentType).setRegistrationKey(registrationKey).build(); grpcStubs.disputeAgentsService.registerDisputeAgent(request); } public void stopServer() { var request = StopRequest.newBuilder().build(); grpcStubs.shutdownService.stop(request); } public String getMethodHelp(Method method) { var request = GetMethodHelpRequest.newBuilder().setMethodName(method.name()).build(); return grpcStubs.helpService.getMethodHelp(request).getMethodHelp(); } } ================================================ FILE: cli/src/main/java/haveno/cli/GrpcStubs.java ================================================ /* * 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 . */ package haveno.cli; import haveno.proto.grpc.DisputeAgentsGrpc; import haveno.proto.grpc.GetVersionGrpc; import haveno.proto.grpc.HelpGrpc; import haveno.proto.grpc.OffersGrpc; import haveno.proto.grpc.PaymentAccountsGrpc; import haveno.proto.grpc.PriceGrpc; import haveno.proto.grpc.ShutdownServerGrpc; import haveno.proto.grpc.TradesGrpc; import haveno.proto.grpc.WalletsGrpc; import io.grpc.CallCredentials; import io.grpc.ManagedChannelBuilder; import static java.util.concurrent.TimeUnit.SECONDS; public final class GrpcStubs { public final DisputeAgentsGrpc.DisputeAgentsBlockingStub disputeAgentsService; public final HelpGrpc.HelpBlockingStub helpService; public final GetVersionGrpc.GetVersionBlockingStub versionService; public final OffersGrpc.OffersBlockingStub offersService; public final PaymentAccountsGrpc.PaymentAccountsBlockingStub paymentAccountsService; public final PriceGrpc.PriceBlockingStub priceService; public final ShutdownServerGrpc.ShutdownServerBlockingStub shutdownService; public final TradesGrpc.TradesBlockingStub tradesService; public final WalletsGrpc.WalletsBlockingStub walletsService; public GrpcStubs(String apiHost, int apiPort, String apiPassword) { CallCredentials credentials = new PasswordCallCredentials(apiPassword); var channel = ManagedChannelBuilder.forAddress(apiHost, apiPort).usePlaintext().build(); Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { channel.shutdown().awaitTermination(1, SECONDS); } catch (InterruptedException ex) { throw new IllegalStateException(ex); } })); this.disputeAgentsService = DisputeAgentsGrpc.newBlockingStub(channel).withCallCredentials(credentials); this.helpService = HelpGrpc.newBlockingStub(channel).withCallCredentials(credentials); this.versionService = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); this.offersService = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); this.paymentAccountsService = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials); this.priceService = PriceGrpc.newBlockingStub(channel).withCallCredentials(credentials); this.shutdownService = ShutdownServerGrpc.newBlockingStub(channel).withCallCredentials(credentials); this.tradesService = TradesGrpc.newBlockingStub(channel).withCallCredentials(credentials); this.walletsService = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); } } ================================================ FILE: cli/src/main/java/haveno/cli/Method.java ================================================ /* * 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 . */ package haveno.cli; /** * Currently supported api methods. */ public enum Method { canceloffer, closetrade, confirmpaymentreceived, confirmpaymentsent, createoffer, editoffer, createpaymentacct, createcryptopaymentacct, getaddressbalance, getbalance, getbtcprice, getfundingaddresses, @Deprecated // Since 27-Dec-2021. getmyoffer, // Endpoint to be removed from future version. Use getoffer instead. getmyoffers, getoffer, getoffers, getpaymentacctform, getpaymentaccts, getpaymentmethods, gettrade, gettrades, failtrade, unfailtrade, gettransaction, gettxfeerate, getunusedbsqaddress, getversion, lockwallet, registerdisputeagent, removewalletpassword, sendbtc, settxfeerate, setwalletpassword, takeoffer, unlockwallet, unsettxfeerate, withdrawfunds, stop } ================================================ FILE: cli/src/main/java/haveno/cli/PasswordCallCredentials.java ================================================ /* * 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 . */ package haveno.cli; import io.grpc.CallCredentials; import io.grpc.Metadata; import io.grpc.Metadata.Key; import java.util.concurrent.Executor; import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; import static io.grpc.Status.UNAUTHENTICATED; import static java.lang.String.format; /** * Sets the {@value PASSWORD_KEY} rpc call header to a given value. */ class PasswordCallCredentials extends CallCredentials { public static final String PASSWORD_KEY = "password"; private final String passwordValue; public PasswordCallCredentials(String passwordValue) { if (passwordValue == null) throw new IllegalArgumentException(format("'%s' value must not be null", PASSWORD_KEY)); this.passwordValue = passwordValue; } @Override public void applyRequestMetadata(RequestInfo requestInfo, Executor appExecutor, MetadataApplier metadataApplier) { appExecutor.execute(() -> { try { var headers = new Metadata(); var passwordKey = Key.of(PASSWORD_KEY, ASCII_STRING_MARSHALLER); headers.put(passwordKey, passwordValue); metadataApplier.apply(headers); } catch (Throwable ex) { metadataApplier.fail(UNAUTHENTICATED.withCause(ex)); } }); } @Override public void thisUsesUnstableApi() { } } ================================================ FILE: cli/src/main/java/haveno/cli/TransactionFormat.java ================================================ /* * 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 . */ package haveno.cli; import com.google.common.annotations.VisibleForTesting; @VisibleForTesting public class TransactionFormat { } ================================================ FILE: cli/src/main/java/haveno/cli/opts/AbstractMethodOptionParser.java ================================================ /* * 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 . */ package haveno.cli.opts; import joptsimple.OptionException; import joptsimple.OptionParser; import joptsimple.OptionSet; import joptsimple.OptionSpec; import lombok.Getter; import java.util.List; import java.util.function.Function; import java.util.function.Predicate; import static haveno.cli.opts.OptLabel.OPT_HELP; import static java.lang.String.format; @SuppressWarnings("unchecked") abstract class AbstractMethodOptionParser implements MethodOpts { // The full command line args passed to CliMain.main(String[] args). // CLI and Method level arguments are derived from args by an ArgumentList(args). protected final String[] args; protected final OptionParser parser = new OptionParser(); // The help option for a specific api method, e.g., takeoffer --help. protected final OptionSpec helpOpt = parser.accepts(OPT_HELP, "Print method help").forHelp(); @Getter protected OptionSet options; @Getter protected List nonOptionArguments; protected AbstractMethodOptionParser(String[] args) { this.args = args; } public AbstractMethodOptionParser parse() { try { options = parser.parse(new ArgumentList(args).getMethodArguments()); nonOptionArguments = (List) options.nonOptionArguments(); return this; } catch (OptionException ex) { throw new IllegalArgumentException(cliExceptionMessageStyle.apply(ex), ex); } } public boolean isForHelp() { return options.has(helpOpt); } protected void verifyStringIsValidDouble(String string) { try { Double.valueOf(string); } catch (NumberFormatException ex) { throw new IllegalArgumentException(format("%s is not a number", string)); } } protected final Predicate> valueNotSpecified = (opt) -> !options.hasArgument(opt) || options.valueOf(opt).isEmpty(); private final Function cliExceptionMessageStyle = (ex) -> { if (ex.getMessage() == null) return null; var optionToken = "option "; var cliMessage = ex.getMessage().toLowerCase(); if (cliMessage.startsWith(optionToken) && cliMessage.length() > optionToken.length()) { cliMessage = cliMessage.substring(cliMessage.indexOf(" ") + 1); } return cliMessage; }; } ================================================ FILE: cli/src/main/java/haveno/cli/opts/ArgumentList.java ================================================ /* * 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 . */ package haveno.cli.opts; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.function.Predicate; /** * Wrapper for an array of command line arguments. * * Used to extract CLI connection and authentication arguments, or method arguments * before parsing CLI or method opts * */ public class ArgumentList { private final Predicate isCliOpt = (o) -> o.startsWith("--password") || o.startsWith("-password") || o.startsWith("--port") || o.startsWith("-port") || o.startsWith("--host") || o.startsWith("-host"); // The method name is the only positional opt in a command (easy to identify). // If the positional argument does not match a Method, or there are more than one // positional arguments, the joptsimple parser or CLI will fail as expected. private final Predicate isMethodNameOpt = (o) -> !o.startsWith("-"); private final Predicate isHelpOpt = (o) -> o.startsWith("--help") || o.startsWith("-help"); private final String[] arguments; private int currentIndex; public ArgumentList(String... arguments) { this.arguments = arguments.clone(); } /** * Returns only the CLI connection & authentication, and method name args * (--password, --host, --port, --help, method name) contained in the original * String[] args; excludes the method specific arguments. * * If String[] args contains both a method name (the only positional opt) and a help * argument (--help, -help), it is assumed the user wants method help, not CLI help, * and the help argument is not included in the returned String[]. */ public String[] getCLIArguments() { currentIndex = 0; Optional methodNameArgument = Optional.empty(); Optional helpArgument = Optional.empty(); List prunedArguments = new ArrayList<>(); while (hasMore()) { String arg = peek(); if (isMethodNameOpt.test(arg)) { methodNameArgument = Optional.of(arg); prunedArguments.add(arg); } if (isCliOpt.test(arg)) prunedArguments.add(arg); if (isHelpOpt.test(arg)) helpArgument = Optional.of(arg); next(); } // Include the saved CLI help argument if the positional method name argument // was not found. if (!methodNameArgument.isPresent() && helpArgument.isPresent()) prunedArguments.add(helpArgument.get()); return prunedArguments.toArray(new String[0]); } /** * Returns only the method args contained in the original String[] args; excludes the * CLI connection & authentication opts (--password, --host, --port), plus the * positional method name arg. */ public String[] getMethodArguments() { List prunedArguments = new ArrayList<>(); currentIndex = 0; while (hasMore()) { String arg = peek(); if (!isCliOpt.test(arg) && !isMethodNameOpt.test(arg)) { prunedArguments.add(arg); } next(); } return prunedArguments.toArray(new String[0]); } boolean hasMore() { return currentIndex < arguments.length; } @SuppressWarnings("UnusedReturnValue") String next() { return arguments[currentIndex++]; } String peek() { return arguments[currentIndex]; } } ================================================ FILE: cli/src/main/java/haveno/cli/opts/CancelOfferOptionParser.java ================================================ /* * 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 . */ package haveno.cli.opts; public class CancelOfferOptionParser extends OfferIdOptionParser implements MethodOpts { public CancelOfferOptionParser(String[] args) { super(args); } public CancelOfferOptionParser parse() { super.parse(); // Super class will short-circuit parsing if help option is present. return this; } public String getOfferId() { return options.valueOf(offerIdOpt); } } ================================================ FILE: cli/src/main/java/haveno/cli/opts/CreateCryptoCurrencyPaymentAcctOptionParser.java ================================================ /* * 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 . */ package haveno.cli.opts; import joptsimple.OptionSpec; import static haveno.cli.CryptoCurrencyUtil.apiDoesSupportCryptoCurrency; import static haveno.cli.opts.OptLabel.OPT_ACCOUNT_NAME; import static haveno.cli.opts.OptLabel.OPT_ADDRESS; import static haveno.cli.opts.OptLabel.OPT_CURRENCY_CODE; import static haveno.cli.opts.OptLabel.OPT_TRADE_INSTANT; import static java.lang.String.format; public class CreateCryptoCurrencyPaymentAcctOptionParser extends AbstractMethodOptionParser implements MethodOpts { final OptionSpec accountNameOpt = parser.accepts(OPT_ACCOUNT_NAME, "crypto currency account name") .withRequiredArg(); final OptionSpec currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "crypto currency code (xmr)") .withRequiredArg(); final OptionSpec addressOpt = parser.accepts(OPT_ADDRESS, "crypto address") .withRequiredArg(); final OptionSpec tradeInstantOpt = parser.accepts(OPT_TRADE_INSTANT, "create trade instant account") .withOptionalArg() .ofType(boolean.class) .defaultsTo(Boolean.FALSE); public CreateCryptoCurrencyPaymentAcctOptionParser(String[] args) { super(args); } public CreateCryptoCurrencyPaymentAcctOptionParser parse() { super.parse(); // Short circuit opt validation if user just wants help. if (options.has(helpOpt)) return this; if (!options.has(accountNameOpt) || options.valueOf(accountNameOpt).isEmpty()) throw new IllegalArgumentException("no payment account name specified"); if (!options.has(currencyCodeOpt) || options.valueOf(currencyCodeOpt).isEmpty()) throw new IllegalArgumentException("no currency code specified"); String cryptoCurrencyCode = options.valueOf(currencyCodeOpt); if (!apiDoesSupportCryptoCurrency(cryptoCurrencyCode)) throw new IllegalArgumentException(format("api does not support %s payment accounts", cryptoCurrencyCode.toLowerCase())); if (!options.has(addressOpt) || options.valueOf(addressOpt).isEmpty()) throw new IllegalArgumentException(format("no %s address specified", cryptoCurrencyCode.toLowerCase())); return this; } public String getAccountName() { return options.valueOf(accountNameOpt); } public String getCurrencyCode() { return options.valueOf(currencyCodeOpt); } public String getAddress() { return options.valueOf(addressOpt); } public boolean getIsTradeInstant() { return options.valueOf(tradeInstantOpt); } } ================================================ FILE: cli/src/main/java/haveno/cli/opts/CreateOfferOptionParser.java ================================================ /* * 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 . */ package haveno.cli.opts; import joptsimple.OptionSpec; import static haveno.cli.opts.OptLabel.OPT_AMOUNT; import static haveno.cli.opts.OptLabel.OPT_CURRENCY_CODE; import static haveno.cli.opts.OptLabel.OPT_DIRECTION; import static haveno.cli.opts.OptLabel.OPT_FIXED_PRICE; import static haveno.cli.opts.OptLabel.OPT_MIN_AMOUNT; import static haveno.cli.opts.OptLabel.OPT_MKT_PRICE_MARGIN; import static haveno.cli.opts.OptLabel.OPT_PAYMENT_ACCOUNT_ID; import static haveno.cli.opts.OptLabel.OPT_SECURITY_DEPOSIT; import static joptsimple.internal.Strings.EMPTY; public class CreateOfferOptionParser extends AbstractMethodOptionParser implements MethodOpts { final OptionSpec paymentAccountIdOpt = parser.accepts(OPT_PAYMENT_ACCOUNT_ID, "id of payment account used for offer") .withRequiredArg() .defaultsTo(EMPTY); final OptionSpec directionOpt = parser.accepts(OPT_DIRECTION, "offer direction (buy|sell)") .withRequiredArg(); final OptionSpec currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "currency code (xmr|eur|usd|...)") .withRequiredArg(); final OptionSpec amountOpt = parser.accepts(OPT_AMOUNT, "amount of btc to buy or sell") .withRequiredArg(); final OptionSpec minAmountOpt = parser.accepts(OPT_MIN_AMOUNT, "minimum amount of btc to buy or sell") .withOptionalArg(); final OptionSpec mktPriceMarginPctOpt = parser.accepts(OPT_MKT_PRICE_MARGIN, "market btc price margin (%)") .withOptionalArg() .defaultsTo("0.00"); final OptionSpec fixedPriceOpt = parser.accepts(OPT_FIXED_PRICE, "fixed btc price") .withOptionalArg() .defaultsTo("0"); final OptionSpec securityDepositPctOpt = parser.accepts(OPT_SECURITY_DEPOSIT, "maker security deposit (%)") .withRequiredArg(); public CreateOfferOptionParser(String[] args) { super(args); } @Override public CreateOfferOptionParser parse() { super.parse(); // Short circuit opt validation if user just wants help. if (options.has(helpOpt)) return this; if (!options.has(directionOpt) || options.valueOf(directionOpt).isEmpty()) throw new IllegalArgumentException("no direction (buy|sell) specified"); if (!options.has(currencyCodeOpt) || options.valueOf(currencyCodeOpt).isEmpty()) throw new IllegalArgumentException("no currency code specified"); if (!options.has(amountOpt) || options.valueOf(amountOpt).isEmpty()) throw new IllegalArgumentException("no btc amount specified"); if (!options.has(paymentAccountIdOpt) || options.valueOf(paymentAccountIdOpt).isEmpty()) throw new IllegalArgumentException("no payment account id specified"); if (!options.has(mktPriceMarginPctOpt) && !options.has(fixedPriceOpt)) throw new IllegalArgumentException("no market price margin or fixed price specified"); if (options.has(mktPriceMarginPctOpt)) { var mktPriceMarginPctString = options.valueOf(mktPriceMarginPctOpt); if (mktPriceMarginPctString.isEmpty()) throw new IllegalArgumentException("no market price margin specified"); else verifyStringIsValidDouble(mktPriceMarginPctString); } if (options.has(fixedPriceOpt) && options.valueOf(fixedPriceOpt).isEmpty()) throw new IllegalArgumentException("no fixed price specified"); if (!options.has(securityDepositPctOpt) || options.valueOf(securityDepositPctOpt).isEmpty()) throw new IllegalArgumentException("no security deposit specified"); else verifyStringIsValidDouble(options.valueOf(securityDepositPctOpt)); return this; } public String getPaymentAccountId() { return options.valueOf(paymentAccountIdOpt); } public String getDirection() { return options.valueOf(directionOpt); } public String getCurrencyCode() { return options.valueOf(currencyCodeOpt); } public String getAmount() { return options.valueOf(amountOpt); } public String getMinAmount() { return options.has(minAmountOpt) ? options.valueOf(minAmountOpt) : getAmount(); } public boolean isUsingMktPriceMargin() { return options.has(mktPriceMarginPctOpt); } public double getMktPriceMarginPct() { return isUsingMktPriceMargin() ? Double.parseDouble(options.valueOf(mktPriceMarginPctOpt)) : 0.00d; } public String getFixedPrice() { return options.has(fixedPriceOpt) ? options.valueOf(fixedPriceOpt) : "0.00"; } public double getSecurityDepositPct() { return Double.valueOf(options.valueOf(securityDepositPctOpt)); } } ================================================ FILE: cli/src/main/java/haveno/cli/opts/CreatePaymentAcctOptionParser.java ================================================ /* * 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 . */ package haveno.cli.opts; import joptsimple.OptionSpec; import java.nio.file.Path; import java.nio.file.Paths; import static haveno.cli.opts.OptLabel.OPT_PAYMENT_ACCOUNT_FORM; import static java.lang.String.format; public class CreatePaymentAcctOptionParser extends AbstractMethodOptionParser implements MethodOpts { final OptionSpec paymentAcctFormPathOpt = parser.accepts(OPT_PAYMENT_ACCOUNT_FORM, "path to json payment account form") .withRequiredArg(); public CreatePaymentAcctOptionParser(String[] args) { super(args); } public CreatePaymentAcctOptionParser parse() { super.parse(); // Short circuit opt validation if user just wants help. if (options.has(helpOpt)) return this; if (!options.has(paymentAcctFormPathOpt) || options.valueOf(paymentAcctFormPathOpt).isEmpty()) throw new IllegalArgumentException("no path to json payment account form specified"); Path path = Paths.get(options.valueOf(paymentAcctFormPathOpt)); if (!path.toFile().exists()) throw new IllegalStateException( format("json payment account form '%s' could not be found", path)); return this; } public Path getPaymentAcctForm() { return Paths.get(options.valueOf(paymentAcctFormPathOpt)); } } ================================================ FILE: cli/src/main/java/haveno/cli/opts/GetAddressBalanceOptionParser.java ================================================ /* * 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 . */ package haveno.cli.opts; import joptsimple.OptionSpec; import static haveno.cli.opts.OptLabel.OPT_ADDRESS; public class GetAddressBalanceOptionParser extends AbstractMethodOptionParser implements MethodOpts { final OptionSpec addressOpt = parser.accepts(OPT_ADDRESS, "wallet btc address") .withRequiredArg(); public GetAddressBalanceOptionParser(String[] args) { super(args); } public GetAddressBalanceOptionParser parse() { super.parse(); // Short circuit opt validation if user just wants help. if (options.has(helpOpt)) return this; if (!options.has(addressOpt) || options.valueOf(addressOpt).isEmpty()) throw new IllegalArgumentException("no address specified"); return this; } public String getAddress() { return options.valueOf(addressOpt); } } ================================================ FILE: cli/src/main/java/haveno/cli/opts/GetBTCMarketPriceOptionParser.java ================================================ /* * 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 . */ package haveno.cli.opts; import joptsimple.OptionSpec; import static haveno.cli.opts.OptLabel.OPT_CURRENCY_CODE; public class GetBTCMarketPriceOptionParser extends AbstractMethodOptionParser implements MethodOpts { final OptionSpec currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "currency-code") .withRequiredArg(); public GetBTCMarketPriceOptionParser(String[] args) { super(args); } public GetBTCMarketPriceOptionParser parse() { super.parse(); // Short circuit opt validation if user just wants help. if (options.has(helpOpt)) return this; if (!options.has(currencyCodeOpt) || options.valueOf(currencyCodeOpt).isEmpty()) throw new IllegalArgumentException("no currency code specified"); return this; } public String getCurrencyCode() { return options.valueOf(currencyCodeOpt); } } ================================================ FILE: cli/src/main/java/haveno/cli/opts/GetBalanceOptionParser.java ================================================ /* * 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 . */ package haveno.cli.opts; import joptsimple.OptionSpec; import static haveno.cli.opts.OptLabel.OPT_CURRENCY_CODE; import static joptsimple.internal.Strings.EMPTY; public class GetBalanceOptionParser extends AbstractMethodOptionParser implements MethodOpts { final OptionSpec currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "wallet currency code (btc)") .withOptionalArg() .defaultsTo(EMPTY); public GetBalanceOptionParser(String[] args) { super(args); } public GetBalanceOptionParser parse() { return (GetBalanceOptionParser) super.parse(); } public String getCurrencyCode() { return options.has(currencyCodeOpt) ? options.valueOf(currencyCodeOpt) : ""; } } ================================================ FILE: cli/src/main/java/haveno/cli/opts/GetOfferOptionParser.java ================================================ /* * 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 . */ package haveno.cli.opts; import joptsimple.OptionSpec; import static haveno.cli.opts.OptLabel.OPT_OFFER_ID; public class GetOfferOptionParser extends AbstractMethodOptionParser implements MethodOpts { final OptionSpec offerIdOpt = parser.accepts(OPT_OFFER_ID, "id of offer to get") .withRequiredArg(); public GetOfferOptionParser(String[] args) { super(args); } public GetOfferOptionParser parse() { super.parse(); // Short circuit opt validation if user just wants help. if (options.has(helpOpt)) return this; if (!options.has(offerIdOpt) || options.valueOf(offerIdOpt).isEmpty()) throw new IllegalArgumentException("no offer id specified"); return this; } public String getOfferId() { return options.valueOf(offerIdOpt); } } ================================================ FILE: cli/src/main/java/haveno/cli/opts/GetOffersOptionParser.java ================================================ /* * 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 . */ package haveno.cli.opts; import joptsimple.OptionSpec; import static haveno.cli.opts.OptLabel.OPT_CURRENCY_CODE; import static haveno.cli.opts.OptLabel.OPT_DIRECTION; public class GetOffersOptionParser extends AbstractMethodOptionParser implements MethodOpts { final OptionSpec directionOpt = parser.accepts(OPT_DIRECTION, "offer direction (buy|sell)") .withRequiredArg(); final OptionSpec currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "currency code (xmr|eur|usd|...)") .withRequiredArg(); public GetOffersOptionParser(String[] args) { super(args); } public GetOffersOptionParser parse() { super.parse(); // Short circuit opt validation if user just wants help. if (options.has(helpOpt)) return this; if (!options.has(directionOpt) || options.valueOf(directionOpt).isEmpty()) throw new IllegalArgumentException("no direction (buy|sell) specified"); if (!options.has(currencyCodeOpt) || options.valueOf(currencyCodeOpt).isEmpty()) throw new IllegalArgumentException("no currency code specified"); return this; } public String getDirection() { return options.valueOf(directionOpt); } public String getCurrencyCode() { return options.valueOf(currencyCodeOpt); } } ================================================ FILE: cli/src/main/java/haveno/cli/opts/GetPaymentAcctFormOptionParser.java ================================================ /* * 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 . */ package haveno.cli.opts; import joptsimple.OptionSpec; import static haveno.cli.opts.OptLabel.OPT_PAYMENT_METHOD_ID; public class GetPaymentAcctFormOptionParser extends AbstractMethodOptionParser implements MethodOpts { final OptionSpec paymentMethodIdOpt = parser.accepts(OPT_PAYMENT_METHOD_ID, "id of payment method type used by a payment account") .withRequiredArg(); public GetPaymentAcctFormOptionParser(String[] args) { super(args); } public GetPaymentAcctFormOptionParser parse() { super.parse(); // Short circuit opt validation if user just wants help. if (options.has(helpOpt)) return this; if (!options.has(paymentMethodIdOpt) || options.valueOf(paymentMethodIdOpt).isEmpty()) throw new IllegalArgumentException("no payment method id specified"); return this; } public String getPaymentMethodId() { return options.valueOf(paymentMethodIdOpt); } } ================================================ FILE: cli/src/main/java/haveno/cli/opts/GetTradeOptionParser.java ================================================ /* * 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 . */ package haveno.cli.opts; import joptsimple.OptionSpec; import static haveno.cli.opts.OptLabel.OPT_SHOW_CONTRACT; import static haveno.cli.opts.OptLabel.OPT_TRADE_ID; public class GetTradeOptionParser extends AbstractMethodOptionParser implements MethodOpts { final OptionSpec tradeIdOpt = parser.accepts(OPT_TRADE_ID, "id of trade") .withRequiredArg(); final OptionSpec showContractOpt = parser.accepts(OPT_SHOW_CONTRACT, "show trade's json contract") .withOptionalArg() .ofType(boolean.class) .defaultsTo(Boolean.FALSE); public GetTradeOptionParser(String[] args) { super(args); } public GetTradeOptionParser parse() { super.parse(); // Short circuit opt validation if user just wants help. if (options.has(helpOpt)) return this; if (!options.has(tradeIdOpt) || options.valueOf(tradeIdOpt).isEmpty()) throw new IllegalArgumentException("no trade id specified"); return this; } public String getTradeId() { return options.valueOf(tradeIdOpt); } public boolean getShowContract() { return options.has(showContractOpt) ? options.valueOf(showContractOpt) : false; } } ================================================ FILE: cli/src/main/java/haveno/cli/opts/GetTradesOptionParser.java ================================================ /* * 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 . */ package haveno.cli.opts; import haveno.proto.grpc.GetTradesRequest; import joptsimple.OptionSpec; import java.util.function.Predicate; import static haveno.cli.opts.OptLabel.OPT_CATEGORY; import static haveno.proto.grpc.GetTradesRequest.Category.CLOSED; import static haveno.proto.grpc.GetTradesRequest.Category.FAILED; import static haveno.proto.grpc.GetTradesRequest.Category.OPEN; import static java.util.Arrays.stream; public class GetTradesOptionParser extends AbstractMethodOptionParser implements MethodOpts { // Map valid CLI option values to gRPC request parameters. private enum CATEGORY { // Lower case enum fits CLI method and parameter style. open(OPEN), closed(CLOSED), failed(FAILED); private final GetTradesRequest.Category grpcRequestCategory; CATEGORY(GetTradesRequest.Category grpcRequestCategory) { this.grpcRequestCategory = grpcRequestCategory; } } final OptionSpec categoryOpt = parser.accepts(OPT_CATEGORY, "category of trades (open|closed|failed)") .withRequiredArg() .defaultsTo(CATEGORY.open.name()); private final Predicate isValidCategory = (c) -> stream(CATEGORY.values()).anyMatch(v -> v.name().equalsIgnoreCase(c)); public GetTradesOptionParser(String[] args) { super(args); } public GetTradesOptionParser parse() { super.parse(); // Short circuit opt validation if user just wants help. if (options.has(helpOpt)) return this; if (options.has(categoryOpt)) { String category = options.valueOf(categoryOpt); if (category.isEmpty()) throw new IllegalArgumentException("no category (open|closed|failed) specified"); if (!isValidCategory.test(category)) throw new IllegalArgumentException("category must be open|closed|failed"); } return this; } public GetTradesRequest.Category getCategory() { String categoryOpt = options.valueOf(this.categoryOpt).toLowerCase(); return CATEGORY.valueOf(categoryOpt).grpcRequestCategory; } } ================================================ FILE: cli/src/main/java/haveno/cli/opts/GetTransactionOptionParser.java ================================================ /* * 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 . */ package haveno.cli.opts; import joptsimple.OptionSpec; import static haveno.cli.opts.OptLabel.OPT_TRANSACTION_ID; public class GetTransactionOptionParser extends AbstractMethodOptionParser implements MethodOpts { final OptionSpec txIdOpt = parser.accepts(OPT_TRANSACTION_ID, "id of transaction") .withRequiredArg(); public GetTransactionOptionParser(String[] args) { super(args); } public GetTransactionOptionParser parse() { super.parse(); // Short circuit opt validation if user just wants help. if (options.has(helpOpt)) return this; if (!options.has(txIdOpt) || options.valueOf(txIdOpt).isEmpty()) throw new IllegalArgumentException("no tx id specified"); return this; } public String getTxId() { return options.valueOf(txIdOpt); } } ================================================ FILE: cli/src/main/java/haveno/cli/opts/MethodOpts.java ================================================ /* * 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 . */ package haveno.cli.opts; public interface MethodOpts { MethodOpts parse(); boolean isForHelp(); } ================================================ FILE: cli/src/main/java/haveno/cli/opts/OfferIdOptionParser.java ================================================ /* * 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 . */ package haveno.cli.opts; import joptsimple.OptionSpec; import static haveno.cli.opts.OptLabel.OPT_OFFER_ID; /** * Superclass for option parsers requiring an offer-id. Avoids a small amount of * duplicated boilerplate. */ public class OfferIdOptionParser extends AbstractMethodOptionParser implements MethodOpts { final OptionSpec offerIdOpt = parser.accepts(OPT_OFFER_ID, "id of offer") .withRequiredArg(); public OfferIdOptionParser(String[] args) { this(args, false); } public OfferIdOptionParser(String[] args, boolean allowsUnrecognizedOptions) { super(args); if (allowsUnrecognizedOptions) this.parser.allowsUnrecognizedOptions(); } public OfferIdOptionParser parse() { super.parse(); // Short circuit opt validation if user just wants help. if (options.has(helpOpt)) return this; if (!options.has(offerIdOpt) || options.valueOf(offerIdOpt).isEmpty()) throw new IllegalArgumentException("no offer id specified"); return this; } public String getOfferId() { return options.valueOf(offerIdOpt); } } ================================================ FILE: cli/src/main/java/haveno/cli/opts/OptLabel.java ================================================ /* * 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 . */ package haveno.cli.opts; /** * CLI opt label definitions. */ public class OptLabel { public final static String OPT_ACCOUNT_NAME = "account-name"; public final static String OPT_ADDRESS = "address"; public final static String OPT_AMOUNT = "amount"; public final static String OPT_CATEGORY = "category"; public final static String OPT_CURRENCY_CODE = "currency-code"; public final static String OPT_DIRECTION = "direction"; public final static String OPT_DISPUTE_AGENT_TYPE = "dispute-agent-type"; public final static String OPT_ENABLE = "enable"; public final static String OPT_FEE_CURRENCY = "fee-currency"; public final static String OPT_FIXED_PRICE = "fixed-price"; public final static String OPT_HELP = "help"; public final static String OPT_HOST = "host"; public final static String OPT_MEMO = "memo"; public final static String OPT_MKT_PRICE_MARGIN = "market-price-margin"; public final static String OPT_MIN_AMOUNT = "min-amount"; public final static String OPT_OFFER_ID = "offer-id"; public final static String OPT_PASSWORD = "password"; public final static String OPT_PAYMENT_ACCOUNT_ID = "payment-account-id"; public final static String OPT_PAYMENT_ACCOUNT_FORM = "payment-account-form"; public final static String OPT_PAYMENT_METHOD_ID = "payment-method-id"; public final static String OPT_PORT = "port"; public final static String OPT_REGISTRATION_KEY = "registration-key"; public final static String OPT_SECURITY_DEPOSIT = "security-deposit"; public final static String OPT_SHOW_CONTRACT = "show-contract"; public final static String OPT_TRADE_ID = "trade-id"; public final static String OPT_TRADE_INSTANT = "trade-instant"; public final static String OPT_TIMEOUT = "timeout"; public final static String OPT_TRANSACTION_ID = "transaction-id"; public final static String OPT_TRIGGER_PRICE = "trigger-price"; public final static String OPT_TX_FEE_RATE = "tx-fee-rate"; public final static String OPT_WALLET_PASSWORD = "wallet-password"; public final static String OPT_NEW_WALLET_PASSWORD = "new-wallet-password"; } ================================================ FILE: cli/src/main/java/haveno/cli/opts/RegisterDisputeAgentOptionParser.java ================================================ /* * 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 . */ package haveno.cli.opts; import joptsimple.OptionSpec; import static haveno.cli.opts.OptLabel.OPT_DISPUTE_AGENT_TYPE; import static haveno.cli.opts.OptLabel.OPT_REGISTRATION_KEY; public class RegisterDisputeAgentOptionParser extends AbstractMethodOptionParser implements MethodOpts { final OptionSpec disputeAgentTypeOpt = parser.accepts(OPT_DISPUTE_AGENT_TYPE, "dispute agent type") .withRequiredArg(); final OptionSpec registrationKeyOpt = parser.accepts(OPT_REGISTRATION_KEY, "registration key") .withRequiredArg(); public RegisterDisputeAgentOptionParser(String[] args) { super(args); } public RegisterDisputeAgentOptionParser parse() { super.parse(); // Short circuit opt validation if user just wants help. if (options.has(helpOpt)) return this; if (!options.has(disputeAgentTypeOpt) || options.valueOf(disputeAgentTypeOpt).isEmpty()) throw new IllegalArgumentException("no dispute agent type specified"); if (!options.has(registrationKeyOpt) || options.valueOf(registrationKeyOpt).isEmpty()) throw new IllegalArgumentException("no registration key specified"); return this; } public String getDisputeAgentType() { return options.valueOf(disputeAgentTypeOpt); } public String getRegistrationKey() { return options.valueOf(registrationKeyOpt); } } ================================================ FILE: cli/src/main/java/haveno/cli/opts/RemoveWalletPasswordOptionParser.java ================================================ /* * 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 . */ package haveno.cli.opts; import joptsimple.OptionSpec; import static haveno.cli.opts.OptLabel.OPT_WALLET_PASSWORD; public class RemoveWalletPasswordOptionParser extends AbstractMethodOptionParser implements MethodOpts { final OptionSpec passwordOpt = parser.accepts(OPT_WALLET_PASSWORD, "haveno wallet password") .withRequiredArg(); public RemoveWalletPasswordOptionParser(String[] args) { super(args); } public RemoveWalletPasswordOptionParser parse() { super.parse(); // Short circuit opt validation if user just wants help. if (options.has(helpOpt)) return this; if (!options.has(passwordOpt) || options.valueOf(passwordOpt).isEmpty()) throw new IllegalArgumentException("no password specified"); return this; } public String getPassword() { return options.valueOf(passwordOpt); } } ================================================ FILE: cli/src/main/java/haveno/cli/opts/SendBtcOptionParser.java ================================================ /* * 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 . */ package haveno.cli.opts; import joptsimple.OptionSpec; import static haveno.cli.opts.OptLabel.OPT_ADDRESS; import static haveno.cli.opts.OptLabel.OPT_AMOUNT; import static haveno.cli.opts.OptLabel.OPT_MEMO; import static haveno.cli.opts.OptLabel.OPT_TX_FEE_RATE; import static joptsimple.internal.Strings.EMPTY; public class SendBtcOptionParser extends AbstractMethodOptionParser implements MethodOpts { final OptionSpec addressOpt = parser.accepts(OPT_ADDRESS, "destination btc address") .withRequiredArg(); final OptionSpec amountOpt = parser.accepts(OPT_AMOUNT, "amount of btc to send") .withRequiredArg(); final OptionSpec feeRateOpt = parser.accepts(OPT_TX_FEE_RATE, "optional tx fee rate (sats/byte)") .withOptionalArg() .defaultsTo(EMPTY); final OptionSpec memoOpt = parser.accepts(OPT_MEMO, "optional tx memo") .withOptionalArg() .defaultsTo(EMPTY); public SendBtcOptionParser(String[] args) { super(args); } public SendBtcOptionParser parse() { super.parse(); // Short circuit opt validation if user just wants help. if (options.has(helpOpt)) return this; if (!options.has(addressOpt) || options.valueOf(addressOpt).isEmpty()) throw new IllegalArgumentException("no btc address specified"); if (!options.has(amountOpt) || options.valueOf(amountOpt).isEmpty()) throw new IllegalArgumentException("no btc amount specified"); return this; } public String getAddress() { return options.valueOf(addressOpt); } public String getAmount() { return options.valueOf(amountOpt); } public String getFeeRate() { return options.has(feeRateOpt) ? options.valueOf(feeRateOpt) : ""; } public String getMemo() { return options.has(memoOpt) ? options.valueOf(memoOpt) : ""; } } ================================================ FILE: cli/src/main/java/haveno/cli/opts/SetTxFeeRateOptionParser.java ================================================ /* * 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 . */ package haveno.cli.opts; import joptsimple.OptionSpec; import static haveno.cli.opts.OptLabel.OPT_TX_FEE_RATE; public class SetTxFeeRateOptionParser extends AbstractMethodOptionParser implements MethodOpts { final OptionSpec feeRateOpt = parser.accepts(OPT_TX_FEE_RATE, "tx fee rate preference (sats/byte)") .withRequiredArg(); public SetTxFeeRateOptionParser(String[] args) { super(args); } public SetTxFeeRateOptionParser parse() { super.parse(); // Short circuit opt validation if user just wants help. if (options.has(helpOpt)) return this; if (!options.has(feeRateOpt) || options.valueOf(feeRateOpt).isEmpty()) throw new IllegalArgumentException("no tx fee rate specified"); return this; } public String getFeeRate() { return options.valueOf(feeRateOpt); } } ================================================ FILE: cli/src/main/java/haveno/cli/opts/SetWalletPasswordOptionParser.java ================================================ /* * 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 . */ package haveno.cli.opts; import joptsimple.OptionSpec; import static haveno.cli.opts.OptLabel.OPT_NEW_WALLET_PASSWORD; import static haveno.cli.opts.OptLabel.OPT_WALLET_PASSWORD; import static joptsimple.internal.Strings.EMPTY; public class SetWalletPasswordOptionParser extends AbstractMethodOptionParser implements MethodOpts { final OptionSpec passwordOpt = parser.accepts(OPT_WALLET_PASSWORD, "haveno wallet password") .withRequiredArg(); final OptionSpec newPasswordOpt = parser.accepts(OPT_NEW_WALLET_PASSWORD, "new haveno wallet password") .withOptionalArg() .defaultsTo(EMPTY); public SetWalletPasswordOptionParser(String[] args) { super(args); } public SetWalletPasswordOptionParser parse() { super.parse(); // Short circuit opt validation if user just wants help. if (options.has(helpOpt)) return this; if (!options.has(passwordOpt) || options.valueOf(passwordOpt).isEmpty()) throw new IllegalArgumentException("no password specified"); return this; } public String getPassword() { return options.valueOf(passwordOpt); } public String getNewPassword() { return options.has(newPasswordOpt) ? options.valueOf(newPasswordOpt) : ""; } } ================================================ FILE: cli/src/main/java/haveno/cli/opts/SimpleMethodOptionParser.java ================================================ /* * 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 . */ package haveno.cli.opts; public class SimpleMethodOptionParser extends AbstractMethodOptionParser implements MethodOpts { public SimpleMethodOptionParser(String[] args) { super(args); } public SimpleMethodOptionParser parse() { return (SimpleMethodOptionParser) super.parse(); } } ================================================ FILE: cli/src/main/java/haveno/cli/opts/TakeOfferOptionParser.java ================================================ /* * 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 . */ package haveno.cli.opts; import joptsimple.OptionSpec; import static haveno.cli.opts.OptLabel.OPT_PAYMENT_ACCOUNT_ID; public class TakeOfferOptionParser extends OfferIdOptionParser implements MethodOpts { final OptionSpec paymentAccountIdOpt = parser.accepts(OPT_PAYMENT_ACCOUNT_ID, "id of payment account used for trade") .withRequiredArg(); public TakeOfferOptionParser(String[] args) { super(args, true); } public TakeOfferOptionParser parse() { super.parse(); // Super class will short-circuit parsing if help option is present. if (!options.has(paymentAccountIdOpt) || options.valueOf(paymentAccountIdOpt).isEmpty()) throw new IllegalArgumentException("no payment account id specified"); return this; } public String getPaymentAccountId() { return options.valueOf(paymentAccountIdOpt); } } ================================================ FILE: cli/src/main/java/haveno/cli/opts/UnlockWalletOptionParser.java ================================================ /* * 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 . */ package haveno.cli.opts; import joptsimple.OptionSpec; import static haveno.cli.opts.OptLabel.OPT_TIMEOUT; import static haveno.cli.opts.OptLabel.OPT_WALLET_PASSWORD; public class UnlockWalletOptionParser extends AbstractMethodOptionParser implements MethodOpts { final OptionSpec passwordOpt = parser.accepts(OPT_WALLET_PASSWORD, "haveno wallet password") .withRequiredArg(); final OptionSpec unlockTimeoutOpt = parser.accepts(OPT_TIMEOUT, "wallet unlock timeout (s)") .withRequiredArg() .ofType(long.class) .defaultsTo(0L); public UnlockWalletOptionParser(String[] args) { super(args); } public UnlockWalletOptionParser parse() { super.parse(); // Short circuit opt validation if user just wants help. if (options.has(helpOpt)) return this; if (!options.has(passwordOpt) || options.valueOf(passwordOpt).isEmpty()) throw new IllegalArgumentException("no password specified"); if (!options.has(unlockTimeoutOpt) || options.valueOf(unlockTimeoutOpt) <= 0) throw new IllegalArgumentException("no unlock timeout specified"); return this; } public String getPassword() { return options.valueOf(passwordOpt); } public long getUnlockTimeout() { return options.valueOf(unlockTimeoutOpt); } } ================================================ FILE: cli/src/main/java/haveno/cli/opts/WithdrawFundsOptionParser.java ================================================ /* * 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 . */ package haveno.cli.opts; import joptsimple.OptionSpec; import static haveno.cli.opts.OptLabel.OPT_ADDRESS; import static haveno.cli.opts.OptLabel.OPT_MEMO; import static haveno.cli.opts.OptLabel.OPT_TRADE_ID; import static joptsimple.internal.Strings.EMPTY; public class WithdrawFundsOptionParser extends AbstractMethodOptionParser implements MethodOpts { final OptionSpec tradeIdOpt = parser.accepts(OPT_TRADE_ID, "id of trade") .withRequiredArg(); final OptionSpec addressOpt = parser.accepts(OPT_ADDRESS, "destination btc address") .withRequiredArg(); final OptionSpec memoOpt = parser.accepts(OPT_MEMO, "optional tx memo") .withOptionalArg() .defaultsTo(EMPTY); public WithdrawFundsOptionParser(String[] args) { super(args); } public WithdrawFundsOptionParser parse() { super.parse(); // Short circuit opt validation if user just wants help. if (options.has(helpOpt)) return this; if (!options.has(tradeIdOpt) || options.valueOf(tradeIdOpt).isEmpty()) throw new IllegalArgumentException("no trade id specified"); if (!options.has(addressOpt) || options.valueOf(addressOpt).isEmpty()) throw new IllegalArgumentException("no destination address specified"); return this; } public String getTradeId() { return options.valueOf(tradeIdOpt); } public String getAddress() { return options.valueOf(addressOpt); } public String getMemo() { return options.has(memoOpt) ? options.valueOf(memoOpt) : ""; } } ================================================ FILE: cli/src/main/java/haveno/cli/request/OffersServiceRequest.java ================================================ /* * 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 . */ package haveno.cli.request; import haveno.cli.GrpcStubs; import haveno.proto.grpc.CancelOfferRequest; import haveno.proto.grpc.GetMyOfferRequest; import haveno.proto.grpc.GetMyOffersRequest; import haveno.proto.grpc.GetOfferRequest; import haveno.proto.grpc.GetOffersRequest; import haveno.proto.grpc.OfferInfo; import haveno.proto.grpc.PostOfferRequest; import java.util.ArrayList; import java.util.List; import static java.util.Comparator.comparing; import static java.util.stream.Collectors.toList; import static protobuf.OfferDirection.BUY; import static protobuf.OfferDirection.SELL; public class OffersServiceRequest { private final GrpcStubs grpcStubs; public OffersServiceRequest(GrpcStubs grpcStubs) { this.grpcStubs = grpcStubs; } @SuppressWarnings("unused") public OfferInfo createFixedPricedOffer(String direction, String currencyCode, long amount, long minAmount, String fixedPrice, double securityDepositPct, String paymentAcctId, String makerFeeCurrencyCode) { return createOffer(direction, currencyCode, amount, minAmount, false, fixedPrice, 0.00, securityDepositPct, paymentAcctId, "0" /* no trigger price */); } public OfferInfo createOffer(String direction, String currencyCode, long amount, long minAmount, boolean useMarketBasedPrice, String fixedPrice, double marketPriceMarginPct, double securityDepositPct, String paymentAcctId, String triggerPrice) { var request = PostOfferRequest.newBuilder() .setDirection(direction) .setCurrencyCode(currencyCode) .setAmount(amount) .setMinAmount(minAmount) .setUseMarketBasedPrice(useMarketBasedPrice) .setPrice(fixedPrice) .setMarketPriceMarginPct(marketPriceMarginPct) .setSecurityDepositPct(securityDepositPct) .setPaymentAccountId(paymentAcctId) .setTriggerPrice(triggerPrice) .build(); return grpcStubs.offersService.postOffer(request).getOffer(); } public void cancelOffer(String offerId) { var request = CancelOfferRequest.newBuilder() .setId(offerId) .build(); //noinspection ResultOfMethodCallIgnored grpcStubs.offersService.cancelOffer(request); } public OfferInfo getOffer(String offerId) { var request = GetOfferRequest.newBuilder() .setId(offerId) .build(); return grpcStubs.offersService.getOffer(request).getOffer(); } public OfferInfo getMyOffer(String offerId) { var request = GetMyOfferRequest.newBuilder() .setId(offerId) .build(); return grpcStubs.offersService.getMyOffer(request).getOffer(); } public List getOffers(String direction, String currencyCode) { var request = GetOffersRequest.newBuilder() .setDirection(direction) .setCurrencyCode(currencyCode) .build(); return grpcStubs.offersService.getOffers(request).getOffersList(); } public List getOffersSortedByDate(String currencyCode) { ArrayList offers = new ArrayList<>(); offers.addAll(getOffers(BUY.name(), currencyCode)); offers.addAll(getOffers(SELL.name(), currencyCode)); return offers.isEmpty() ? offers : sortOffersByDate(offers); } public List getOffersSortedByDate(String direction, String currencyCode) { var offers = getOffers(direction, currencyCode); return offers.isEmpty() ? offers : sortOffersByDate(offers); } public List getMyOffers(String direction, String currencyCode) { var request = GetMyOffersRequest.newBuilder() .setDirection(direction) .setCurrencyCode(currencyCode) .build(); return grpcStubs.offersService.getMyOffers(request).getOffersList(); } public List getMyOffersSortedByDate(String currencyCode) { ArrayList offers = new ArrayList<>(); offers.addAll(getMyOffers(BUY.name(), currencyCode)); offers.addAll(getMyOffers(SELL.name(), currencyCode)); return offers.isEmpty() ? offers : sortOffersByDate(offers); } public List getMyOffersSortedByDate(String direction, String currencyCode) { var offers = getMyOffers(direction, currencyCode); return offers.isEmpty() ? offers : sortOffersByDate(offers); } public OfferInfo getMostRecentOffer(String direction, String currencyCode) { List offers = getOffersSortedByDate(direction, currencyCode); return offers.isEmpty() ? null : offers.get(offers.size() - 1); } public List sortOffersByDate(List offerInfoList) { return offerInfoList.stream() .sorted(comparing(OfferInfo::getDate)) .collect(toList()); } } ================================================ FILE: cli/src/main/java/haveno/cli/request/PaymentAccountsServiceRequest.java ================================================ /* * 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 . */ package haveno.cli.request; import haveno.cli.GrpcStubs; import haveno.proto.grpc.CreateCryptoCurrencyPaymentAccountRequest; import haveno.proto.grpc.CreatePaymentAccountRequest; import haveno.proto.grpc.GetCryptoCurrencyPaymentMethodsRequest; import haveno.proto.grpc.GetPaymentAccountFormAsJsonRequest; import haveno.proto.grpc.GetPaymentAccountsRequest; import haveno.proto.grpc.GetPaymentMethodsRequest; import protobuf.PaymentAccount; import protobuf.PaymentMethod; import java.util.List; import static java.lang.String.format; public class PaymentAccountsServiceRequest { private final GrpcStubs grpcStubs; public PaymentAccountsServiceRequest(GrpcStubs grpcStubs) { this.grpcStubs = grpcStubs; } public List getPaymentMethods() { var request = GetPaymentMethodsRequest.newBuilder().build(); return grpcStubs.paymentAccountsService.getPaymentMethods(request).getPaymentMethodsList(); } public String getPaymentAcctFormAsJson(String paymentMethodId) { var request = GetPaymentAccountFormAsJsonRequest.newBuilder() .setPaymentMethodId(paymentMethodId) .build(); return grpcStubs.paymentAccountsService.getPaymentAccountFormAsJson(request).getPaymentAccountFormAsJson(); } public PaymentAccount createPaymentAccount(String json) { var request = CreatePaymentAccountRequest.newBuilder() .setPaymentAccountFormAsJson(json) .build(); return grpcStubs.paymentAccountsService.createPaymentAccount(request).getPaymentAccount(); } public List getPaymentAccounts() { var request = GetPaymentAccountsRequest.newBuilder().build(); return grpcStubs.paymentAccountsService.getPaymentAccounts(request).getPaymentAccountsList(); } /** * Returns the first PaymentAccount found with the given name, or throws an * IllegalArgumentException if not found. This method should be used with care; * it will only return one PaymentAccount, and the account name must be an exact * match on the name argument. * @param accountName the name of the stored PaymentAccount to retrieve * @return PaymentAccount with given name */ public PaymentAccount getPaymentAccount(String accountName) { return getPaymentAccounts().stream() .filter(a -> a.getAccountName().equals(accountName)).findFirst() .orElseThrow(() -> new IllegalArgumentException(format("payment account with name '%s' not found", accountName))); } public PaymentAccount createCryptoCurrencyPaymentAccount(String accountName, String currencyCode, String address, boolean tradeInstant) { var request = CreateCryptoCurrencyPaymentAccountRequest.newBuilder() .setAccountName(accountName) .setCurrencyCode(currencyCode) .setAddress(address) .setTradeInstant(tradeInstant) .build(); return grpcStubs.paymentAccountsService.createCryptoCurrencyPaymentAccount(request).getPaymentAccount(); } public List getCryptoPaymentMethods() { var request = GetCryptoCurrencyPaymentMethodsRequest.newBuilder().build(); return grpcStubs.paymentAccountsService.getCryptoCurrencyPaymentMethods(request).getPaymentMethodsList(); } } ================================================ FILE: cli/src/main/java/haveno/cli/request/TradesServiceRequest.java ================================================ /* * 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 . */ package haveno.cli.request; import haveno.cli.GrpcStubs; import haveno.proto.grpc.ConfirmPaymentReceivedRequest; import haveno.proto.grpc.ConfirmPaymentSentRequest; import haveno.proto.grpc.GetTradeRequest; import haveno.proto.grpc.GetTradesRequest; import haveno.proto.grpc.TakeOfferReply; import haveno.proto.grpc.TakeOfferRequest; import haveno.proto.grpc.TradeInfo; import haveno.proto.grpc.WithdrawFundsRequest; import java.util.List; import static haveno.proto.grpc.GetTradesRequest.Category.CLOSED; import static haveno.proto.grpc.GetTradesRequest.Category.FAILED; public class TradesServiceRequest { private final GrpcStubs grpcStubs; public TradesServiceRequest(GrpcStubs grpcStubs) { this.grpcStubs = grpcStubs; } public TakeOfferReply getTakeOfferReply(String offerId, String paymentAccountId) { var request = TakeOfferRequest.newBuilder() .setOfferId(offerId) .setPaymentAccountId(paymentAccountId) .build(); return grpcStubs.tradesService.takeOffer(request); } public TradeInfo takeOffer(String offerId, String paymentAccountId) { var reply = getTakeOfferReply(offerId, paymentAccountId); if (reply.hasTrade()) return reply.getTrade(); else throw new IllegalStateException(reply.getFailureReason().getDescription()); } public TradeInfo getTrade(String tradeId) { var request = GetTradeRequest.newBuilder() .setTradeId(tradeId) .build(); return grpcStubs.tradesService.getTrade(request).getTrade(); } public List getOpenTrades() { var request = GetTradesRequest.newBuilder() .build(); return grpcStubs.tradesService.getTrades(request).getTradesList(); } public List getTradeHistory(GetTradesRequest.Category category) { if (!category.equals(CLOSED) && !category.equals(FAILED)) throw new IllegalStateException("unrecognized gettrades category parameter " + category.name()); var request = GetTradesRequest.newBuilder() .setCategory(category) .build(); return grpcStubs.tradesService.getTrades(request).getTradesList(); } public void confirmPaymentSent(String tradeId) { var request = ConfirmPaymentSentRequest.newBuilder() .setTradeId(tradeId) .build(); //noinspection ResultOfMethodCallIgnored grpcStubs.tradesService.confirmPaymentSent(request); } public void confirmPaymentReceived(String tradeId) { var request = ConfirmPaymentReceivedRequest.newBuilder() .setTradeId(tradeId) .build(); //noinspection ResultOfMethodCallIgnored grpcStubs.tradesService.confirmPaymentReceived(request); } public void withdrawFunds(String tradeId, String address, String memo) { var request = WithdrawFundsRequest.newBuilder() .setTradeId(tradeId) .setAddress(address) .setMemo(memo) .build(); //noinspection ResultOfMethodCallIgnored grpcStubs.tradesService.withdrawFunds(request); } } ================================================ FILE: cli/src/main/java/haveno/cli/request/WalletsServiceRequest.java ================================================ /* * 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 . */ package haveno.cli.request; import haveno.cli.GrpcStubs; import haveno.proto.grpc.AddressBalanceInfo; import haveno.proto.grpc.BalancesInfo; import haveno.proto.grpc.BtcBalanceInfo; import haveno.proto.grpc.GetAddressBalanceRequest; import haveno.proto.grpc.GetBalancesRequest; import haveno.proto.grpc.GetFundingAddressesRequest; import haveno.proto.grpc.LockWalletRequest; import haveno.proto.grpc.MarketPriceRequest; import haveno.proto.grpc.RemoveWalletPasswordRequest; import haveno.proto.grpc.SetWalletPasswordRequest; import haveno.proto.grpc.UnlockWalletRequest; import java.util.List; public class WalletsServiceRequest { private final GrpcStubs grpcStubs; public WalletsServiceRequest(GrpcStubs grpcStubs) { this.grpcStubs = grpcStubs; } public BalancesInfo getBalances() { return getBalances(""); } public BtcBalanceInfo getBtcBalances() { return getBalances("BTC").getBtc(); } public BalancesInfo getBalances(String currencyCode) { var request = GetBalancesRequest.newBuilder() .setCurrencyCode(currencyCode) .build(); return grpcStubs.walletsService.getBalances(request).getBalances(); } public AddressBalanceInfo getAddressBalance(String address) { var request = GetAddressBalanceRequest.newBuilder() .setAddress(address).build(); return grpcStubs.walletsService.getAddressBalance(request).getAddressBalanceInfo(); } public double getBtcPrice(String currencyCode) { var request = MarketPriceRequest.newBuilder() .setCurrencyCode(currencyCode) .build(); return grpcStubs.priceService.getMarketPrice(request).getPrice(); } public List getFundingAddresses() { var request = GetFundingAddressesRequest.newBuilder().build(); return grpcStubs.walletsService.getFundingAddresses(request).getAddressBalanceInfoList(); } public String getUnusedBtcAddress() { var request = GetFundingAddressesRequest.newBuilder().build(); var addressBalances = grpcStubs.walletsService.getFundingAddresses(request) .getAddressBalanceInfoList(); //noinspection OptionalGetWithoutIsPresent return addressBalances.stream() .filter(AddressBalanceInfo::getIsAddressUnused) .findFirst() .get() .getAddress(); } public void lockWallet() { var request = LockWalletRequest.newBuilder().build(); //noinspection ResultOfMethodCallIgnored grpcStubs.walletsService.lockWallet(request); } public void unlockWallet(String walletPassword, long timeout) { var request = UnlockWalletRequest.newBuilder() .setPassword(walletPassword) .setTimeout(timeout).build(); //noinspection ResultOfMethodCallIgnored grpcStubs.walletsService.unlockWallet(request); } public void removeWalletPassword(String walletPassword) { var request = RemoveWalletPasswordRequest.newBuilder() .setPassword(walletPassword).build(); //noinspection ResultOfMethodCallIgnored grpcStubs.walletsService.removeWalletPassword(request); } public void setWalletPassword(String walletPassword) { var request = SetWalletPasswordRequest.newBuilder() .setPassword(walletPassword).build(); //noinspection ResultOfMethodCallIgnored grpcStubs.walletsService.setWalletPassword(request); } public void setWalletPassword(String oldWalletPassword, String newWalletPassword) { var request = SetWalletPasswordRequest.newBuilder() .setPassword(oldWalletPassword) .setNewPassword(newWalletPassword).build(); //noinspection ResultOfMethodCallIgnored grpcStubs.walletsService.setWalletPassword(request); } } ================================================ FILE: cli/src/main/java/haveno/cli/table/Table.java ================================================ /* * 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 . */ package haveno.cli.table; import haveno.cli.table.column.Column; import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.util.stream.IntStream; import static com.google.common.base.Strings.padStart; import static haveno.cli.table.column.Column.JUSTIFICATION.RIGHT; import static java.lang.String.format; import static java.nio.charset.StandardCharsets.UTF_8; /** * A simple table of formatted data for the CLI's output console. A table must be * created with at least one populated column, and each column passed to the constructor * must contain the same number of rows. Null checking is omitted because tables are * populated by protobuf message fields which cannot be null. * * All data in a column has the same type: long, string, etc., but a table * may contain an arbitrary number of columns of any type. For output formatting * purposes, numeric and date columns should be transformed to a StringColumn type with * formatted and justified string values before being passed to the constructor. * * This is not a relational, rdbms table. */ public class Table { public final Column[] columns; public final int rowCount; // Each printed column is delimited by two spaces. private final int columnDelimiterLength = 2; /** * Default constructor. Takes populated Columns. * * @param columns containing the same number of rows */ public Table(Column... columns) { this.columns = columns; this.rowCount = columns.length > 0 ? columns[0].rowCount() : 0; validateStructure(); } /** * Print table data to a PrintStream. * * @param printStream the target output stream */ public void print(PrintStream printStream) { printColumnNames(printStream); for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) { printRow(printStream, rowIndex); } } /** * Print table column names to a PrintStream. * * @param printStream the target output stream */ private void printColumnNames(PrintStream printStream) { IntStream.range(0, columns.length).forEachOrdered(colIndex -> { var c = columns[colIndex]; var justifiedName = c.getJustification().equals(RIGHT) ? padStart(c.getName(), c.getWidth(), ' ') : c.getName(); var paddedWidth = colIndex == columns.length - 1 ? c.getName().length() : c.getWidth() + columnDelimiterLength; printStream.printf("%-" + paddedWidth + "s", justifiedName); }); printStream.println(); } /** * Print a table row to a PrintStream. * * @param printStream the target output stream */ private void printRow(PrintStream printStream, int rowIndex) { IntStream.range(0, columns.length).forEachOrdered(colIndex -> { var c = columns[colIndex]; var paddedWidth = colIndex == columns.length - 1 ? c.getWidth() : c.getWidth() + columnDelimiterLength; printStream.printf("%-" + paddedWidth + "s", c.getRow(rowIndex)); if (colIndex == columns.length - 1) printStream.println(); }); } /** * Returns the table's formatted output as a String. * @return String */ @Override public String toString() { ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (PrintStream ps = new PrintStream(baos, true, UTF_8)) { print(ps); } return baos.toString(); } /** * Verifies the table has columns, and each column has the same number of rows. */ private void validateStructure() { if (columns.length == 0) throw new IllegalArgumentException("Table has no columns."); if (columns[0].isEmpty()) throw new IllegalArgumentException( format("Table's 1st column (%s) has no data.", columns[0].getName())); IntStream.range(1, columns.length).forEachOrdered(colIndex -> { var c = columns[colIndex]; if (c.isEmpty()) throw new IllegalStateException( format("Table column # %d (%s) does not have any data.", colIndex + 1, c.getName())); if (this.rowCount != c.rowCount()) throw new IllegalStateException( format("Table column # %d (%s) does not have same number of rows as 1st column.", colIndex + 1, c.getName())); }); } } ================================================ FILE: cli/src/main/java/haveno/cli/table/builder/AbstractTableBuilder.java ================================================ /* * 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 . */ package haveno.cli.table.builder; import haveno.cli.table.Table; import haveno.proto.grpc.OfferInfo; import java.util.List; import java.util.function.Predicate; /** * Abstract superclass for TableBuilder implementations. */ abstract class AbstractTableBuilder { protected final Predicate isTraditionalOffer = (o) -> o.getBaseCurrencyCode().equals("XMR"); protected final TableType tableType; protected final List protos; AbstractTableBuilder(TableType tableType, List protos) { this.tableType = tableType; this.protos = protos; if (protos.isEmpty()) throw new IllegalArgumentException("cannot build a table without rows"); } public abstract Table build(); } ================================================ FILE: cli/src/main/java/haveno/cli/table/builder/AbstractTradeListBuilder.java ================================================ /* * 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 . */ package haveno.cli.table.builder; import haveno.cli.table.column.Column; import haveno.cli.table.column.MixedTradeFeeColumn; import haveno.proto.grpc.ContractInfo; import haveno.proto.grpc.TradeInfo; import javax.annotation.Nullable; import java.math.BigDecimal; import java.util.List; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; import static haveno.cli.CurrencyFormat.formatSatoshis; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_BUYER_DEPOSIT; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_SELLER_DEPOSIT; import static haveno.cli.table.builder.TableType.TRADE_DETAIL_TBL; import static java.lang.String.format; import static protobuf.OfferDirection.SELL; abstract class AbstractTradeListBuilder extends AbstractTableBuilder { protected final List trades; protected final TradeTableColumnSupplier colSupplier; protected final Column colTradeId; @Nullable protected final Column colCreateDate; @Nullable protected final Column colMarket; protected final Column colPrice; @Nullable protected final Column colPriceDeviation; @Nullable protected final Column colCurrency; @Nullable protected final Column colAmount; @Nullable protected final Column colMixedAmount; @Nullable protected final MixedTradeFeeColumn colMixedTradeFee; @Nullable protected final Column colBuyerDeposit; @Nullable protected final Column colSellerDeposit; @Nullable protected final Column colPaymentMethod; @Nullable protected final Column colRole; @Nullable protected final Column colOfferType; @Nullable protected final Column colClosingStatus; // Trade detail tbl specific columns @Nullable protected final Column colIsDepositPublished; @Nullable protected final Column colIsDepositConfirmed; @Nullable protected final Column colIsPayoutPublished; @Nullable protected final Column colIsCompleted; @Nullable protected final Column colHavenoTradeFee; @Nullable protected final Column colTradeCost; @Nullable protected final Column colIsPaymentSentMessageSent; @Nullable protected final Column colIsPaymentReceivedMessageSent; @Nullable protected final Column colCryptoReceiveAddressColumn; AbstractTradeListBuilder(TableType tableType, List protos) { super(tableType, protos); validate(); this.trades = protos.stream().map(p -> (TradeInfo) p).collect(Collectors.toList()); this.colSupplier = new TradeTableColumnSupplier(tableType, trades); this.colTradeId = colSupplier.tradeIdColumn.get(); this.colCreateDate = colSupplier.createDateColumn.get(); this.colMarket = colSupplier.marketColumn.get(); this.colPrice = colSupplier.priceColumn.get(); this.colPriceDeviation = colSupplier.priceDeviationColumn.get(); this.colCurrency = colSupplier.currencyColumn.get(); this.colAmount = colSupplier.amountColumn.get(); this.colMixedAmount = colSupplier.mixedAmountColumn.get(); this.colMixedTradeFee = colSupplier.mixedTradeFeeColumn.get(); this.colBuyerDeposit = colSupplier.toSecurityDepositColumn.apply(COL_HEADER_BUYER_DEPOSIT); this.colSellerDeposit = colSupplier.toSecurityDepositColumn.apply(COL_HEADER_SELLER_DEPOSIT); this.colPaymentMethod = colSupplier.paymentMethodColumn.get(); this.colRole = colSupplier.roleColumn.get(); this.colOfferType = colSupplier.offerTypeColumn.get(); this.colClosingStatus = colSupplier.statusDescriptionColumn.get(); // Trade detail specific columns, some in common with BSQ swap trades detail. this.colIsDepositPublished = colSupplier.depositPublishedColumn.get(); this.colIsDepositConfirmed = colSupplier.depositConfirmedColumn.get(); this.colIsPayoutPublished = colSupplier.payoutPublishedColumn.get(); this.colIsCompleted = colSupplier.fundsWithdrawnColumn.get(); this.colHavenoTradeFee = colSupplier.havenoTradeDetailFeeColumn.get(); this.colTradeCost = colSupplier.tradeCostColumn.get(); this.colIsPaymentSentMessageSent = colSupplier.paymentSentMessageSentColumn.get(); this.colIsPaymentReceivedMessageSent = colSupplier.paymentReceivedMessageSentColumn.get(); //noinspection ConstantConditions this.colCryptoReceiveAddressColumn = colSupplier.cryptoReceiveAddressColumn.get(); } protected void validate() { if (isTradeDetailTblBuilder.get()) { if (protos.size() != 1) throw new IllegalArgumentException("trade detail tbl can have only one row"); } else if (protos.isEmpty()) { throw new IllegalArgumentException("trade tbl has no rows"); } } // Helper Functions private final Supplier isTradeDetailTblBuilder = () -> tableType.equals(TRADE_DETAIL_TBL); protected final Predicate isTraditionalTrade = (t) -> isTraditionalOffer.test(t.getOffer()); protected final Predicate isMyOffer = (t) -> t.getOffer().getIsMyOffer(); protected final Predicate isTaker = (t) -> t.getRole().toLowerCase().contains("taker"); protected final Predicate isSellOffer = (t) -> t.getOffer().getDirection().equals(SELL.name()); protected final Predicate isBtcSeller = (t) -> (isMyOffer.test(t) && isSellOffer.test(t)) || (!isMyOffer.test(t) && !isSellOffer.test(t)); // Column Value Functions // Crypto volumes from server are string representations of decimals. // Converting them to longs ("sats") requires shifting the decimal points // to left: 2 for BSQ, 8 for other cryptos. protected final Function toCryptoTradeVolumeAsLong = (t) -> new BigDecimal(t.getTradeVolume()).movePointRight(8).longValue(); protected final Function toTradeVolumeAsString = (t) -> isTraditionalTrade.test(t) ? t.getTradeVolume() : formatSatoshis(t.getAmount()); protected final Function toTradeVolumeAsLong = (t) -> isTraditionalTrade.test(t) ? Long.parseLong(t.getTradeVolume()) : toCryptoTradeVolumeAsLong.apply(t); protected final Function toTradeAmount = (t) -> isTraditionalTrade.test(t) ? t.getAmount() : toTradeVolumeAsLong.apply(t); protected final Function toMarket = (t) -> t.getOffer().getBaseCurrencyCode() + "/" + t.getOffer().getCounterCurrencyCode(); protected final Function toPaymentCurrencyCode = (t) -> isTraditionalTrade.test(t) ? t.getOffer().getCounterCurrencyCode() : t.getOffer().getBaseCurrencyCode(); protected final Function toPriceDeviation = (t) -> t.getOffer().getUseMarketBasedPrice() ? format("%.2f%s", t.getOffer().getMarketPriceMarginPct(), "%") : "N/A"; protected final Function toTradeFeeBtc = (t) -> { var isMyOffer = t.getOffer().getIsMyOffer(); if (isMyOffer) { return t.getMakerFee(); } else { return t.getTakerFee(); } }; protected final Function toMyMakerOrTakerFee = (t) -> { return isTaker.test(t) ? t.getTakerFee() : t.getMakerFee(); }; protected final Function toOfferType = (t) -> { if (isTraditionalTrade.test(t)) { return t.getOffer().getDirection() + " " + t.getOffer().getBaseCurrencyCode(); } else { if (t.getOffer().getDirection().equals("BUY")) { return "SELL " + t.getOffer().getBaseCurrencyCode(); } else { return "BUY " + t.getOffer().getBaseCurrencyCode(); } } }; protected final Predicate showCryptoBuyerAddress = (t) -> { if (isTraditionalTrade.test(t)) { return false; } else { ContractInfo contract = t.getContract(); boolean isBuyerMakerAndSellerTaker = contract.getIsBuyerMakerAndSellerTaker(); if (isTaker.test(t)) { return !isBuyerMakerAndSellerTaker; } else { return isBuyerMakerAndSellerTaker; } } }; protected final Function toCryptoReceiveAddress = (t) -> { if (showCryptoBuyerAddress.test(t)) { ContractInfo contract = t.getContract(); boolean isBuyerMakerAndSellerTaker = contract.getIsBuyerMakerAndSellerTaker(); return isBuyerMakerAndSellerTaker // (is BTC buyer / maker) ? contract.getTakerPaymentAccountPayload().getCryptoCurrencyAccountPayload().getAddress() : contract.getMakerPaymentAccountPayload().getCryptoCurrencyAccountPayload().getAddress(); } else { return ""; } }; } ================================================ FILE: cli/src/main/java/haveno/cli/table/builder/AddressBalanceTableBuilder.java ================================================ /* * 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 . */ package haveno.cli.table.builder; import haveno.cli.table.Table; import haveno.cli.table.column.BooleanColumn; import haveno.cli.table.column.Column; import haveno.cli.table.column.LongColumn; import haveno.cli.table.column.SatoshiColumn; import haveno.cli.table.column.StringColumn; import haveno.proto.grpc.AddressBalanceInfo; import java.util.List; import java.util.stream.Collectors; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_ADDRESS; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_AVAILABLE_BALANCE; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_CONFIRMATIONS; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_IS_USED_ADDRESS; import static haveno.cli.table.builder.TableType.ADDRESS_BALANCE_TBL; import static java.lang.String.format; /** * Builds a {@code haveno.cli.table.Table} from a List of * {@code haveno.proto.grpc.AddressBalanceInfo} objects. */ class AddressBalanceTableBuilder extends AbstractTableBuilder { // Default columns not dynamically generated with address info. private final Column colAddress; private final Column colAvailableBalance; private final Column colConfirmations; private final Column colIsUsed; AddressBalanceTableBuilder(List protos) { super(ADDRESS_BALANCE_TBL, protos); colAddress = new StringColumn(format(COL_HEADER_ADDRESS, "BTC")); this.colAvailableBalance = new SatoshiColumn(COL_HEADER_AVAILABLE_BALANCE); this.colConfirmations = new LongColumn(COL_HEADER_CONFIRMATIONS); this.colIsUsed = new BooleanColumn(COL_HEADER_IS_USED_ADDRESS); } public Table build() { List addresses = protos.stream() .map(a -> (AddressBalanceInfo) a) .collect(Collectors.toList()); // Populate columns with address info. //noinspection SimplifyStreamApiCallChains addresses.stream().forEachOrdered(a -> { colAddress.addRow(a.getAddress()); colAvailableBalance.addRow(a.getBalance()); colConfirmations.addRow(a.getNumConfirmations()); colIsUsed.addRow(!a.getIsAddressUnused()); }); // Define and return the table instance with populated columns. return new Table(colAddress, colAvailableBalance.asStringColumn(), colConfirmations.asStringColumn(), colIsUsed.asStringColumn()); } } ================================================ FILE: cli/src/main/java/haveno/cli/table/builder/BtcBalanceTableBuilder.java ================================================ /* * 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 . */ package haveno.cli.table.builder; import haveno.cli.table.Table; import haveno.cli.table.column.Column; import haveno.cli.table.column.SatoshiColumn; import haveno.proto.grpc.BtcBalanceInfo; import java.util.List; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_AVAILABLE_BALANCE; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_LOCKED_BALANCE; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_RESERVED_BALANCE; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_TOTAL_AVAILABLE_BALANCE; import static haveno.cli.table.builder.TableType.BTC_BALANCE_TBL; /** * Builds a {@code haveno.cli.table.Table} from a * {@code haveno.proto.grpc.BtcBalanceInfo} object. */ class BtcBalanceTableBuilder extends AbstractTableBuilder { // Default columns not dynamically generated with btc balance info. private final Column colAvailableBalance; private final Column colReservedBalance; private final Column colTotalAvailableBalance; private final Column colLockedBalance; BtcBalanceTableBuilder(List protos) { super(BTC_BALANCE_TBL, protos); this.colAvailableBalance = new SatoshiColumn(COL_HEADER_AVAILABLE_BALANCE); this.colReservedBalance = new SatoshiColumn(COL_HEADER_RESERVED_BALANCE); this.colTotalAvailableBalance = new SatoshiColumn(COL_HEADER_TOTAL_AVAILABLE_BALANCE); this.colLockedBalance = new SatoshiColumn(COL_HEADER_LOCKED_BALANCE); } @Override public Table build() { BtcBalanceInfo balance = (BtcBalanceInfo) protos.get(0); // Populate columns with btc balance info. colAvailableBalance.addRow(balance.getAvailableBalance()); colReservedBalance.addRow(balance.getReservedBalance()); colTotalAvailableBalance.addRow(balance.getTotalAvailableBalance()); colLockedBalance.addRow(balance.getLockedBalance()); // Define and return the table instance with populated columns. return new Table(colAvailableBalance.asStringColumn(), colReservedBalance.asStringColumn(), colTotalAvailableBalance.asStringColumn(), colLockedBalance.asStringColumn()); } } ================================================ FILE: cli/src/main/java/haveno/cli/table/builder/ClosedTradeTableBuilder.java ================================================ /* * 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 . */ package haveno.cli.table.builder; import haveno.cli.table.Table; import java.util.List; import static haveno.cli.table.builder.TableType.CLOSED_TRADES_TBL; @SuppressWarnings("ConstantConditions") class ClosedTradeTableBuilder extends AbstractTradeListBuilder { ClosedTradeTableBuilder(List protos) { super(CLOSED_TRADES_TBL, protos); } @Override public Table build() { populateColumns(); return new Table(colTradeId, colCreateDate.asStringColumn(), colMarket, colPrice.justify(), colPriceDeviation.justify(), colAmount.asStringColumn(), colMixedAmount.justify(), colCurrency, colMixedTradeFee.asStringColumn(), colBuyerDeposit.asStringColumn(), colSellerDeposit.asStringColumn(), colOfferType, colClosingStatus); } private void populateColumns() { trades.forEach(t -> { colTradeId.addRow(t.getTradeId()); colCreateDate.addRow(t.getDate()); colMarket.addRow(toMarket.apply(t)); colPrice.addRow(t.getPrice()); colPriceDeviation.addRow(toPriceDeviation.apply(t)); colAmount.addRow(t.getAmount()); colMixedAmount.addRow(t.getTradeVolume()); colCurrency.addRow(toPaymentCurrencyCode.apply(t)); colMixedTradeFee.addRow(toTradeFeeBtc.apply(t), false); colBuyerDeposit.addRow(t.getOffer().getBuyerSecurityDepositPct()); colSellerDeposit.addRow(t.getOffer().getSellerSecurityDepositPct()); colOfferType.addRow(toOfferType.apply(t)); }); } } ================================================ FILE: cli/src/main/java/haveno/cli/table/builder/FailedTradeTableBuilder.java ================================================ /* * 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 . */ package haveno.cli.table.builder; import haveno.cli.table.Table; import java.util.List; import static haveno.cli.table.builder.TableType.FAILED_TRADES_TBL; /** * Builds a {@code haveno.cli.table.Table} from a list of {@code haveno.proto.grpc.TradeInfo} objects. */ @SuppressWarnings("ConstantConditions") class FailedTradeTableBuilder extends AbstractTradeListBuilder { FailedTradeTableBuilder(List protos) { super(FAILED_TRADES_TBL, protos); } public Table build() { populateColumns(); return new Table(colTradeId, colCreateDate.asStringColumn(), colMarket, colPrice.justify(), colAmount.asStringColumn(), colMixedAmount.justify(), colCurrency, colOfferType, colRole, colClosingStatus); } private void populateColumns() { trades.forEach(t -> { colTradeId.addRow(t.getTradeId()); colCreateDate.addRow(t.getDate()); colMarket.addRow(toMarket.apply(t)); colPrice.addRow(t.getPrice()); colAmount.addRow(t.getAmount()); colMixedAmount.addRow(t.getTradeVolume()); colCurrency.addRow(toPaymentCurrencyCode.apply(t)); colOfferType.addRow(toOfferType.apply(t)); colRole.addRow(t.getRole()); colClosingStatus.addRow("Failed"); }); } } ================================================ FILE: cli/src/main/java/haveno/cli/table/builder/OfferTableBuilder.java ================================================ /* * 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 . */ package haveno.cli.table.builder; import haveno.cli.table.Table; import haveno.cli.table.column.Column; import haveno.cli.table.column.Iso8601DateTimeColumn; import haveno.cli.table.column.SatoshiColumn; import haveno.cli.table.column.StringColumn; import haveno.cli.table.column.ZippedStringColumns; import haveno.proto.grpc.OfferInfo; import javax.annotation.Nullable; import java.util.List; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_AMOUNT_RANGE; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_CREATION_DATE; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_DETAILED_PRICE; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_DETAILED_PRICE_OF_CRYPTO; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_DIRECTION; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_ENABLED; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_PAYMENT_METHOD; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_TRIGGER_PRICE; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_UUID; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_VOLUME_RANGE; import static haveno.cli.table.builder.TableType.OFFER_TBL; import static haveno.cli.table.column.Column.JUSTIFICATION.LEFT; import static haveno.cli.table.column.Column.JUSTIFICATION.NONE; import static haveno.cli.table.column.Column.JUSTIFICATION.RIGHT; import static haveno.cli.table.column.ZippedStringColumns.DUPLICATION_MODE.EXCLUDE_DUPLICATES; import static java.lang.String.format; import static protobuf.OfferDirection.BUY; import static protobuf.OfferDirection.SELL; /** * Builds a {@code haveno.cli.table.Table} from a List of * {@code haveno.proto.grpc.OfferInfo} objects. */ class OfferTableBuilder extends AbstractTableBuilder { // Columns common to both traditional and cryptocurrency offers. private final Column colOfferId = new StringColumn(COL_HEADER_UUID, LEFT); private final Column colDirection = new StringColumn(COL_HEADER_DIRECTION, LEFT); private final Column colAmount = new SatoshiColumn("Temp Amount", NONE); private final Column colMinAmount = new SatoshiColumn("Temp Min Amount", NONE); private final Column colPaymentMethod = new StringColumn(COL_HEADER_PAYMENT_METHOD, LEFT); private final Column colCreateDate = new Iso8601DateTimeColumn(COL_HEADER_CREATION_DATE); OfferTableBuilder(List protos) { super(OFFER_TBL, protos); } @Override public Table build() { List offers = protos.stream().map(p -> (OfferInfo) p).collect(Collectors.toList()); return isShowingTraditionalOffers.get() ? buildTraditionalOfferTable(offers) : buildCryptoCurrencyOfferTable(offers); } @SuppressWarnings("ConstantConditions") public Table buildTraditionalOfferTable(List offers) { @Nullable Column colEnabled = enabledColumn.get(); // Not boolean: "YES", "NO", or "PENDING" Column colTraditionalPrice = new StringColumn(format(COL_HEADER_DETAILED_PRICE, traditionalTradeCurrency.get()), RIGHT); Column colVolume = new StringColumn(format("Temp Volume (%s)", traditionalTradeCurrency.get()), NONE); Column colMinVolume = new StringColumn(format("Temp Min Volume (%s)", traditionalTradeCurrency.get()), NONE); @Nullable Column colTriggerPrice = traditionalTriggerPriceColumn.get(); // Populate columns with offer info. offers.forEach(o -> { if (colEnabled != null) colEnabled.addRow(toEnabled.apply(o)); colDirection.addRow(o.getDirection()); colTraditionalPrice.addRow(o.getPrice()); colMinAmount.addRow(o.getMinAmount()); colAmount.addRow(o.getAmount()); colVolume.addRow(o.getVolume()); colMinVolume.addRow(o.getMinVolume()); if (colTriggerPrice != null) colTriggerPrice.addRow(toBlankOrNonZeroValue.apply(o.getTriggerPrice())); colPaymentMethod.addRow(o.getPaymentMethodShortName()); colCreateDate.addRow(o.getDate()); colOfferId.addRow(o.getId()); }); ZippedStringColumns amountRange = zippedAmountRangeColumns.get(); ZippedStringColumns volumeRange = new ZippedStringColumns(format(COL_HEADER_VOLUME_RANGE, traditionalTradeCurrency.get()), RIGHT, " - ", colMinVolume.asStringColumn(), colVolume.asStringColumn()); // Define and return the table instance with populated columns. if (isShowingMyOffers.get()) { return new Table(colEnabled.asStringColumn(), colDirection, colTraditionalPrice.justify(), amountRange.asStringColumn(EXCLUDE_DUPLICATES), volumeRange.asStringColumn(EXCLUDE_DUPLICATES), colTriggerPrice.justify(), colPaymentMethod, colCreateDate.asStringColumn(), colOfferId); } else { return new Table(colDirection, colTraditionalPrice.justify(), amountRange.asStringColumn(EXCLUDE_DUPLICATES), volumeRange.asStringColumn(EXCLUDE_DUPLICATES), colPaymentMethod, colCreateDate.asStringColumn(), colOfferId); } } @SuppressWarnings("ConstantConditions") public Table buildCryptoCurrencyOfferTable(List offers) { @Nullable Column colEnabled = enabledColumn.get(); // Not boolean: YES, NO, or PENDING Column colBtcPrice = new StringColumn(format(COL_HEADER_DETAILED_PRICE_OF_CRYPTO, cryptoTradeCurrency.get()), RIGHT); Column colVolume = new StringColumn(format("Temp Volume (%s)", cryptoTradeCurrency.get()), NONE); Column colMinVolume = new StringColumn(format("Temp Min Volume (%s)", cryptoTradeCurrency.get()), NONE); @Nullable Column colTriggerPrice = cryptoTriggerPriceColumn.get(); // Populate columns with offer info. offers.forEach(o -> { if (colEnabled != null) colEnabled.addRow(toEnabled.apply(o)); colDirection.addRow(directionFormat.apply(o)); colBtcPrice.addRow(o.getPrice()); colAmount.addRow(o.getAmount()); colMinAmount.addRow(o.getMinAmount()); colVolume.addRow(o.getVolume()); colMinVolume.addRow(o.getMinVolume()); if (colTriggerPrice != null) colTriggerPrice.addRow(toBlankOrNonZeroValue.apply(o.getTriggerPrice())); colPaymentMethod.addRow(o.getPaymentMethodShortName()); colCreateDate.addRow(o.getDate()); colOfferId.addRow(o.getId()); }); ZippedStringColumns amountRange = zippedAmountRangeColumns.get(); ZippedStringColumns volumeRange = new ZippedStringColumns(format(COL_HEADER_VOLUME_RANGE, cryptoTradeCurrency.get()), RIGHT, " - ", colMinVolume.asStringColumn(), colVolume.asStringColumn()); // Define and return the table instance with populated columns. if (isShowingMyOffers.get()) { if (isShowingBsqOffers.get()) { return new Table(colEnabled.asStringColumn(), colDirection, colBtcPrice.justify(), amountRange.asStringColumn(EXCLUDE_DUPLICATES), volumeRange.asStringColumn(EXCLUDE_DUPLICATES), colPaymentMethod, colCreateDate.asStringColumn(), colOfferId); } else { return new Table(colEnabled.asStringColumn(), colDirection, colBtcPrice.justify(), amountRange.asStringColumn(EXCLUDE_DUPLICATES), volumeRange.asStringColumn(EXCLUDE_DUPLICATES), colTriggerPrice.justify(), colPaymentMethod, colCreateDate.asStringColumn(), colOfferId); } } else { return new Table(colDirection, colBtcPrice.justify(), amountRange.asStringColumn(EXCLUDE_DUPLICATES), volumeRange.asStringColumn(EXCLUDE_DUPLICATES), colPaymentMethod, colCreateDate.asStringColumn(), colOfferId); } } private final Function toBlankOrNonZeroValue = (s) -> s.trim().equals("0") ? "" : s; private final Supplier firstOfferInList = () -> (OfferInfo) protos.get(0); private final Supplier isShowingMyOffers = () -> firstOfferInList.get().getIsMyOffer(); private final Supplier isShowingTraditionalOffers = () -> isTraditionalOffer.test(firstOfferInList.get()); private final Supplier traditionalTradeCurrency = () -> firstOfferInList.get().getCounterCurrencyCode(); private final Supplier cryptoTradeCurrency = () -> firstOfferInList.get().getBaseCurrencyCode(); private final Supplier isShowingBsqOffers = () -> !isTraditionalOffer.test(firstOfferInList.get()) && cryptoTradeCurrency.get().equals("BSQ"); @Nullable // Not a boolean column: YES, NO, or PENDING. private final Supplier enabledColumn = () -> isShowingMyOffers.get() ? new StringColumn(COL_HEADER_ENABLED, LEFT) : null; @Nullable private final Supplier traditionalTriggerPriceColumn = () -> isShowingMyOffers.get() ? new StringColumn(format(COL_HEADER_TRIGGER_PRICE, traditionalTradeCurrency.get()), RIGHT) : null; @Nullable private final Supplier cryptoTriggerPriceColumn = () -> isShowingMyOffers.get() && !isShowingBsqOffers.get() ? new StringColumn(format(COL_HEADER_TRIGGER_PRICE, cryptoTradeCurrency.get()), RIGHT) : null; private final Function toEnabled = (o) -> { return o.getIsActivated() ? "YES" : "NO"; }; private final Function toMirroredDirection = (d) -> d.equalsIgnoreCase(BUY.name()) ? SELL.name() : BUY.name(); private final Function directionFormat = (o) -> { if (isTraditionalOffer.test(o)) { return o.getBaseCurrencyCode(); } else { // Return "Sell BSQ (Buy BTC)", or "Buy BSQ (Sell BTC)". String direction = o.getDirection(); String mirroredDirection = toMirroredDirection.apply(direction); Function mixedCase = (word) -> word.charAt(0) + word.substring(1).toLowerCase(); return format("%s %s (%s %s)", mixedCase.apply(mirroredDirection), o.getBaseCurrencyCode(), mixedCase.apply(direction), o.getCounterCurrencyCode()); } }; private final Supplier zippedAmountRangeColumns = () -> { if (colMinAmount.isEmpty() || colAmount.isEmpty()) throw new IllegalStateException("amount columns must have data"); return new ZippedStringColumns(COL_HEADER_AMOUNT_RANGE, RIGHT, " - ", colMinAmount.asStringColumn(), colAmount.asStringColumn()); }; } ================================================ FILE: cli/src/main/java/haveno/cli/table/builder/OpenTradeTableBuilder.java ================================================ /* * 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 . */ package haveno.cli.table.builder; import haveno.cli.table.Table; import java.util.List; import static haveno.cli.table.builder.TableType.OPEN_TRADES_TBL; /** * Builds a {@code haveno.cli.table.Table} from a list of {@code haveno.proto.grpc.TradeInfo} objects. */ @SuppressWarnings("ConstantConditions") class OpenTradeTableBuilder extends AbstractTradeListBuilder { OpenTradeTableBuilder(List protos) { super(OPEN_TRADES_TBL, protos); } public Table build() { populateColumns(); return new Table(colTradeId, colCreateDate.asStringColumn(), colMarket, colPrice.justify(), colAmount.asStringColumn(), colMixedAmount.justify(), colCurrency, colPaymentMethod, colRole); } private void populateColumns() { trades.forEach(t -> { colTradeId.addRow(t.getTradeId()); colCreateDate.addRow(t.getDate()); colMarket.addRow(toMarket.apply(t)); colPrice.addRow(t.getPrice()); colAmount.addRow(t.getAmount()); colMixedAmount.addRow(t.getTradeVolume()); colCurrency.addRow(toPaymentCurrencyCode.apply(t)); colPaymentMethod.addRow(t.getOffer().getPaymentMethodShortName()); colRole.addRow(t.getRole()); }); } } ================================================ FILE: cli/src/main/java/haveno/cli/table/builder/PaymentAccountTableBuilder.java ================================================ /* * 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 . */ package haveno.cli.table.builder; import haveno.cli.table.Table; import haveno.cli.table.column.Column; import haveno.cli.table.column.StringColumn; import protobuf.PaymentAccount; import java.util.List; import java.util.stream.Collectors; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_CURRENCY; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_NAME; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_PAYMENT_METHOD; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_UUID; import static haveno.cli.table.builder.TableType.PAYMENT_ACCOUNT_TBL; /** * Builds a {@code haveno.cli.table.Table} from a List of * {@code protobuf.PaymentAccount} objects. */ class PaymentAccountTableBuilder extends AbstractTableBuilder { // Default columns not dynamically generated with payment account info. private final Column colName; private final Column colCurrency; private final Column colPaymentMethod; private final Column colId; PaymentAccountTableBuilder(List protos) { super(PAYMENT_ACCOUNT_TBL, protos); this.colName = new StringColumn(COL_HEADER_NAME); this.colCurrency = new StringColumn(COL_HEADER_CURRENCY); this.colPaymentMethod = new StringColumn(COL_HEADER_PAYMENT_METHOD); this.colId = new StringColumn(COL_HEADER_UUID); } public Table build() { List paymentAccounts = protos.stream() .map(a -> (PaymentAccount) a) .collect(Collectors.toList()); // Populate columns with payment account info. //noinspection SimplifyStreamApiCallChains paymentAccounts.stream().forEachOrdered(a -> { colName.addRow(a.getAccountName()); colCurrency.addRow(a.getSelectedTradeCurrency().getCode()); colPaymentMethod.addRow(a.getPaymentMethod().getId()); colId.addRow(a.getId()); }); // Define and return the table instance with populated columns. return new Table(colName, colCurrency, colPaymentMethod, colId); } } ================================================ FILE: cli/src/main/java/haveno/cli/table/builder/TableBuilder.java ================================================ /* * 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 . */ package haveno.cli.table.builder; import haveno.cli.table.Table; import java.util.List; import static java.util.Collections.singletonList; /** * Table builder factory. It is not conventionally named TableBuilderFactory because * it has no static factory methods. The number of static fields and methods in the * {@code haveno.cli.table} are kept to a minimum in an effort o reduce class load time * in the session-less CLI. */ public class TableBuilder extends AbstractTableBuilder { public TableBuilder(TableType tableType, Object proto) { this(tableType, singletonList(proto)); } public TableBuilder(TableType tableType, List protos) { super(tableType, protos); } @Override public Table build() { switch (tableType) { case ADDRESS_BALANCE_TBL: return new AddressBalanceTableBuilder(protos).build(); case BTC_BALANCE_TBL: return new BtcBalanceTableBuilder(protos).build(); case CLOSED_TRADES_TBL: return new ClosedTradeTableBuilder(protos).build(); case FAILED_TRADES_TBL: return new FailedTradeTableBuilder(protos).build(); case OFFER_TBL: return new OfferTableBuilder(protos).build(); case OPEN_TRADES_TBL: return new OpenTradeTableBuilder(protos).build(); case PAYMENT_ACCOUNT_TBL: return new PaymentAccountTableBuilder(protos).build(); case TRADE_DETAIL_TBL: return new TradeDetailTableBuilder(protos).build(); case TRANSACTION_TBL: return new TransactionTableBuilder(protos).build(); default: throw new IllegalArgumentException("invalid cli table type " + tableType.name()); } } } ================================================ FILE: cli/src/main/java/haveno/cli/table/builder/TableBuilderConstants.java ================================================ /* * 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 . */ package haveno.cli.table.builder; /** * Table column name constants. */ class TableBuilderConstants { static final String COL_HEADER_ADDRESS = "%-3s Address"; static final String COL_HEADER_AMOUNT = "Amount"; static final String COL_HEADER_AMOUNT_IN_BTC = "Amount in BTC"; static final String COL_HEADER_AMOUNT_RANGE = "BTC(min - max)"; static final String COL_HEADER_AVAILABLE_BALANCE = "Available Balance"; static final String COL_HEADER_AVAILABLE_CONFIRMED_BALANCE = "Available Confirmed Balance"; static final String COL_HEADER_UNCONFIRMED_CHANGE_BALANCE = "Unconfirmed Change Balance"; static final String COL_HEADER_RESERVED_BALANCE = "Reserved Balance"; static final String COL_HEADER_TOTAL_AVAILABLE_BALANCE = "Total Available Balance"; static final String COL_HEADER_LOCKED_BALANCE = "Locked Balance"; static final String COL_HEADER_LOCKED_FOR_VOTING_BALANCE = "Locked For Voting Balance"; static final String COL_HEADER_LOCKUP_BONDS_BALANCE = "Lockup Bonds Balance"; static final String COL_HEADER_UNLOCKING_BONDS_BALANCE = "Unlocking Bonds Balance"; static final String COL_HEADER_UNVERIFIED_BALANCE = "Unverified Balance"; static final String COL_HEADER_BSQ_SWAP_TRADE_ROLE = "My BSQ Swap Role"; static final String COL_HEADER_BUYER_DEPOSIT = "Buyer Deposit (BTC)"; static final String COL_HEADER_SELLER_DEPOSIT = "Seller Deposit (BTC)"; static final String COL_HEADER_CONFIRMATIONS = "Confirmations"; static final String COL_HEADER_DEVIATION = "Deviation"; static final String COL_HEADER_IS_USED_ADDRESS = "Is Used"; static final String COL_HEADER_CREATION_DATE = "Creation Date (UTC)"; static final String COL_HEADER_CURRENCY = "Currency"; static final String COL_HEADER_DATE_TIME = "Date/Time (UTC)"; static final String COL_HEADER_DETAILED_AMOUNT = "Amount(%-3s)"; static final String COL_HEADER_DETAILED_PRICE = "Price in %-3s for 1 BTC"; static final String COL_HEADER_DETAILED_PRICE_OF_CRYPTO = "Price in BTC for 1 %-3s"; static final String COL_HEADER_DIRECTION = "Buy/Sell"; static final String COL_HEADER_ENABLED = "Enabled"; static final String COL_HEADER_MARKET = "Market"; static final String COL_HEADER_NAME = "Name"; static final String COL_HEADER_OFFER_TYPE = "Offer Type"; static final String COL_HEADER_PAYMENT_METHOD = "Payment Method"; static final String COL_HEADER_PRICE = "Price"; static final String COL_HEADER_STATUS = "Status"; static final String COL_HEADER_TRADE_CRYPTO_BUYER_ADDRESS = "%-3s Buyer Address"; static final String COL_HEADER_TRADE_BUYER_COST = "Buyer Cost(%-3s)"; static final String COL_HEADER_TRADE_DEPOSIT_CONFIRMED = "Deposit Confirmed"; static final String COL_HEADER_TRADE_DEPOSIT_PUBLISHED = "Deposit Published"; static final String COL_HEADER_TRADE_PAYMENT_SENT = "%-3s Sent"; static final String COL_HEADER_TRADE_PAYMENT_RECEIVED = "%-3s Received"; static final String COL_HEADER_TRADE_PAYOUT_PUBLISHED = "Payout Published"; static final String COL_HEADER_TRADE_WITHDRAWN = "Withdrawn"; static final String COL_HEADER_TRADE_ID = "Trade ID"; static final String COL_HEADER_TRADE_ROLE = "My Role"; static final String COL_HEADER_TRADE_SHORT_ID = "ID"; static final String COL_HEADER_TRADE_MAKER_FEE = "Maker Fee(%-3s)"; static final String COL_HEADER_TRADE_TAKER_FEE = "Taker Fee(%-3s)"; static final String COL_HEADER_TRADE_FEE = "Trade Fee"; static final String COL_HEADER_TRIGGER_PRICE = "Trigger Price(%-3s)"; static final String COL_HEADER_TX_ID = "Tx ID"; static final String COL_HEADER_TX_INPUT_SUM = "Tx Inputs (BTC)"; static final String COL_HEADER_TX_OUTPUT_SUM = "Tx Outputs (BTC)"; static final String COL_HEADER_TX_FEE = "Tx Fee (BTC)"; static final String COL_HEADER_TX_SIZE = "Tx Size (Bytes)"; static final String COL_HEADER_TX_IS_CONFIRMED = "Is Confirmed"; static final String COL_HEADER_TX_MEMO = "Memo"; static final String COL_HEADER_VOLUME_RANGE = "%-3s(min - max)"; static final String COL_HEADER_UUID = "ID"; } ================================================ FILE: cli/src/main/java/haveno/cli/table/builder/TableType.java ================================================ /* * 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 . */ package haveno.cli.table.builder; /** * Used as param in TableBuilder constructor instead of inspecting * protos to find out what kind of CLI output table should be built. */ public enum TableType { ADDRESS_BALANCE_TBL, BTC_BALANCE_TBL, CLOSED_TRADES_TBL, FAILED_TRADES_TBL, OFFER_TBL, OPEN_TRADES_TBL, PAYMENT_ACCOUNT_TBL, TRADE_DETAIL_TBL, TRANSACTION_TBL } ================================================ FILE: cli/src/main/java/haveno/cli/table/builder/TradeDetailTableBuilder.java ================================================ /* * 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 . */ package haveno.cli.table.builder; import haveno.cli.table.Table; import haveno.cli.table.column.Column; import haveno.proto.grpc.TradeInfo; import java.util.ArrayList; import java.util.List; import static haveno.cli.table.builder.TableType.TRADE_DETAIL_TBL; /** * Builds a {@code haveno.cli.table.Table} from a {@code haveno.proto.grpc.TradeInfo} object. */ @SuppressWarnings("ConstantConditions") class TradeDetailTableBuilder extends AbstractTradeListBuilder { TradeDetailTableBuilder(List protos) { super(TRADE_DETAIL_TBL, protos); } /** * Build a single row trade detail table. * @return Table containing one row */ @Override public Table build() { // A trade detail table only has one row. var trade = trades.get(0); populateColumns(trade); List> columns = defineColumnList(trade); return new Table(columns.toArray(new Column[0])); } private void populateColumns(TradeInfo trade) { populateHavenoV1TradeColumns(trade); } private void populateHavenoV1TradeColumns(TradeInfo trade) { colTradeId.addRow(trade.getShortId()); colRole.addRow(trade.getRole()); colPrice.addRow(trade.getPrice()); colAmount.addRow(toTradeAmount.apply(trade)); colHavenoTradeFee.addRow(toMyMakerOrTakerFee.apply(trade)); colIsDepositPublished.addRow(trade.getIsDepositsPublished()); colIsDepositConfirmed.addRow(trade.getIsDepositsUnlocked()); colTradeCost.addRow(toTradeVolumeAsString.apply(trade)); colIsPaymentSentMessageSent.addRow(trade.getIsPaymentSent()); colIsPaymentReceivedMessageSent.addRow(trade.getIsPaymentReceived()); colIsPayoutPublished.addRow(trade.getIsPayoutPublished()); colIsCompleted.addRow(trade.getIsCompleted()); if (colCryptoReceiveAddressColumn != null) colCryptoReceiveAddressColumn.addRow(toCryptoReceiveAddress.apply(trade)); } private List> defineColumnList(TradeInfo trade) { return getHavenoV1TradeColumnList(); } private List> getHavenoV1TradeColumnList() { List> columns = new ArrayList<>() {{ add(colTradeId); add(colRole); add(colPrice.justify()); add(colAmount.asStringColumn()); add(colHavenoTradeFee.asStringColumn()); add(colIsDepositPublished.asStringColumn()); add(colIsDepositConfirmed.asStringColumn()); add(colTradeCost.justify()); add(colIsPaymentSentMessageSent.asStringColumn()); add(colIsPaymentReceivedMessageSent.asStringColumn()); add(colIsPayoutPublished.asStringColumn()); add(colIsCompleted.asStringColumn()); }}; if (colCryptoReceiveAddressColumn != null) columns.add(colCryptoReceiveAddressColumn); return columns; } } ================================================ FILE: cli/src/main/java/haveno/cli/table/builder/TradeTableColumnSupplier.java ================================================ /* * 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 . */ package haveno.cli.table.builder; import haveno.cli.table.column.CryptoVolumeColumn; import haveno.cli.table.column.DoubleColumn; import haveno.cli.table.column.BooleanColumn; import haveno.cli.table.column.BtcColumn; import haveno.cli.table.column.Column; import haveno.cli.table.column.Iso8601DateTimeColumn; import haveno.cli.table.column.MixedTradeFeeColumn; import haveno.cli.table.column.SatoshiColumn; import haveno.cli.table.column.StringColumn; import haveno.proto.grpc.ContractInfo; import haveno.proto.grpc.OfferInfo; import haveno.proto.grpc.TradeInfo; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.util.List; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_AMOUNT; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_AMOUNT_IN_BTC; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_CURRENCY; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_DATE_TIME; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_DETAILED_AMOUNT; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_DETAILED_PRICE; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_DETAILED_PRICE_OF_CRYPTO; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_DEVIATION; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_MARKET; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_OFFER_TYPE; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_PAYMENT_METHOD; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_PRICE; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_STATUS; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_TRADE_CRYPTO_BUYER_ADDRESS; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_TRADE_BUYER_COST; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_TRADE_DEPOSIT_CONFIRMED; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_TRADE_DEPOSIT_PUBLISHED; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_TRADE_FEE; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_TRADE_ID; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_TRADE_MAKER_FEE; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_TRADE_PAYMENT_RECEIVED; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_TRADE_PAYMENT_SENT; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_TRADE_PAYOUT_PUBLISHED; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_TRADE_ROLE; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_TRADE_SHORT_ID; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_TRADE_TAKER_FEE; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_TRADE_WITHDRAWN; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_TX_FEE; import static haveno.cli.table.builder.TableType.CLOSED_TRADES_TBL; import static haveno.cli.table.builder.TableType.FAILED_TRADES_TBL; import static haveno.cli.table.builder.TableType.OPEN_TRADES_TBL; import static haveno.cli.table.builder.TableType.TRADE_DETAIL_TBL; import static haveno.cli.table.column.CryptoVolumeColumn.DISPLAY_MODE.CRYPTO_VOLUME; import static haveno.cli.table.column.CryptoVolumeColumn.DISPLAY_MODE.BSQ_VOLUME; import static haveno.cli.table.column.Column.JUSTIFICATION.LEFT; import static haveno.cli.table.column.Column.JUSTIFICATION.RIGHT; import static java.lang.String.format; /** * Convenience for supplying column definitions to * open/closed/failed/detail trade table builders. */ @Slf4j class TradeTableColumnSupplier { @Getter private final TableType tableType; @Getter private final List trades; public TradeTableColumnSupplier(TableType tableType, List trades) { this.tableType = tableType; this.trades = trades; } private final Supplier isTradeDetailTblBuilder = () -> getTableType().equals(TRADE_DETAIL_TBL); private final Supplier isOpenTradeTblBuilder = () -> getTableType().equals(OPEN_TRADES_TBL); private final Supplier isClosedTradeTblBuilder = () -> getTableType().equals(CLOSED_TRADES_TBL); private final Supplier isFailedTradeTblBuilder = () -> getTableType().equals(FAILED_TRADES_TBL); private final Supplier firstRow = () -> getTrades().get(0); private final Predicate isTraditionalOffer = (o) -> o.getBaseCurrencyCode().equals("XMR"); private final Predicate isTraditionalTrade = (t) -> isTraditionalOffer.test(t.getOffer()); private final Predicate isTaker = (t) -> t.getRole().toLowerCase().contains("taker"); final Supplier tradeIdColumn = () -> isTradeDetailTblBuilder.get() ? new StringColumn(COL_HEADER_TRADE_SHORT_ID) : new StringColumn(COL_HEADER_TRADE_ID); final Supplier createDateColumn = () -> isTradeDetailTblBuilder.get() ? null : new Iso8601DateTimeColumn(COL_HEADER_DATE_TIME); final Supplier marketColumn = () -> isTradeDetailTblBuilder.get() ? null : new StringColumn(COL_HEADER_MARKET); private final Function> toDetailedPriceColumn = (t) -> { String colHeader = isTraditionalTrade.test(t) ? format(COL_HEADER_DETAILED_PRICE, t.getOffer().getCounterCurrencyCode()) : format(COL_HEADER_DETAILED_PRICE_OF_CRYPTO, t.getOffer().getBaseCurrencyCode()); return new StringColumn(colHeader, RIGHT); }; final Supplier> priceColumn = () -> isTradeDetailTblBuilder.get() ? toDetailedPriceColumn.apply(firstRow.get()) : new StringColumn(COL_HEADER_PRICE, RIGHT); final Supplier> priceDeviationColumn = () -> isTradeDetailTblBuilder.get() ? null : new StringColumn(COL_HEADER_DEVIATION, RIGHT); final Supplier currencyColumn = () -> isTradeDetailTblBuilder.get() ? null : new StringColumn(COL_HEADER_CURRENCY); private final Function> toDetailedAmountColumn = (t) -> { String headerCurrencyCode = t.getOffer().getBaseCurrencyCode(); String colHeader = format(COL_HEADER_DETAILED_AMOUNT, headerCurrencyCode); CryptoVolumeColumn.DISPLAY_MODE displayMode = headerCurrencyCode.equals("BSQ") ? BSQ_VOLUME : CRYPTO_VOLUME; return isTraditionalTrade.test(t) ? new SatoshiColumn(colHeader) : new CryptoVolumeColumn(colHeader, displayMode); }; // Can be tradional or crypto amount represented as longs. Placing the decimal // in the displayed string representation is done in the Column implementation. final Supplier> amountColumn = () -> isTradeDetailTblBuilder.get() ? toDetailedAmountColumn.apply(firstRow.get()) : new BtcColumn(COL_HEADER_AMOUNT_IN_BTC); final Supplier mixedAmountColumn = () -> isTradeDetailTblBuilder.get() ? null : new StringColumn(COL_HEADER_AMOUNT, RIGHT); final Supplier> minerTxFeeColumn = () -> isTradeDetailTblBuilder.get() || isClosedTradeTblBuilder.get() ? new SatoshiColumn(COL_HEADER_TX_FEE) : null; final Supplier mixedTradeFeeColumn = () -> isTradeDetailTblBuilder.get() ? null : new MixedTradeFeeColumn(COL_HEADER_TRADE_FEE); final Supplier paymentMethodColumn = () -> isTradeDetailTblBuilder.get() || isClosedTradeTblBuilder.get() ? null : new StringColumn(COL_HEADER_PAYMENT_METHOD, LEFT); final Supplier roleColumn = () -> { return isTradeDetailTblBuilder.get() || isOpenTradeTblBuilder.get() || isFailedTradeTblBuilder.get() ? new StringColumn(COL_HEADER_TRADE_ROLE) : null; }; final Function> toSecurityDepositColumn = (name) -> isClosedTradeTblBuilder.get() ? new DoubleColumn(name) : null; final Supplier offerTypeColumn = () -> isTradeDetailTblBuilder.get() ? null : new StringColumn(COL_HEADER_OFFER_TYPE); final Supplier statusDescriptionColumn = () -> isTradeDetailTblBuilder.get() ? null : new StringColumn(COL_HEADER_STATUS); private final Function> toBooleanColumn = BooleanColumn::new; final Supplier> depositPublishedColumn = () -> { return isTradeDetailTblBuilder.get() ? toBooleanColumn.apply(COL_HEADER_TRADE_DEPOSIT_PUBLISHED) : null; }; final Supplier> depositConfirmedColumn = () -> { return isTradeDetailTblBuilder.get() ? toBooleanColumn.apply(COL_HEADER_TRADE_DEPOSIT_CONFIRMED) : null; }; final Supplier> payoutPublishedColumn = () -> { return isTradeDetailTblBuilder.get() ? toBooleanColumn.apply(COL_HEADER_TRADE_PAYOUT_PUBLISHED) : null; }; final Supplier> fundsWithdrawnColumn = () -> { return isTradeDetailTblBuilder.get() ? toBooleanColumn.apply(COL_HEADER_TRADE_WITHDRAWN) : null; }; final Supplier> havenoTradeDetailFeeColumn = () -> { if (isTradeDetailTblBuilder.get()) { TradeInfo t = firstRow.get(); String headerCurrencyCode = "XMR"; String colHeader = isTaker.test(t) ? format(COL_HEADER_TRADE_TAKER_FEE, headerCurrencyCode) : format(COL_HEADER_TRADE_MAKER_FEE, headerCurrencyCode); return new SatoshiColumn(colHeader, false); } else { return null; } }; final Function toPaymentCurrencyCode = (t) -> isTraditionalTrade.test(t) ? t.getOffer().getCounterCurrencyCode() : t.getOffer().getBaseCurrencyCode(); final Supplier> paymentSentMessageSentColumn = () -> { if (isTradeDetailTblBuilder.get()) { String headerCurrencyCode = toPaymentCurrencyCode.apply(firstRow.get()); String colHeader = format(COL_HEADER_TRADE_PAYMENT_SENT, headerCurrencyCode); return new BooleanColumn(colHeader); } else { return null; } }; final Supplier> paymentReceivedMessageSentColumn = () -> { if (isTradeDetailTblBuilder.get()) { String headerCurrencyCode = toPaymentCurrencyCode.apply(firstRow.get()); String colHeader = format(COL_HEADER_TRADE_PAYMENT_RECEIVED, headerCurrencyCode); return new BooleanColumn(colHeader); } else { return null; } }; final Supplier> tradeCostColumn = () -> { if (isTradeDetailTblBuilder.get()) { TradeInfo t = firstRow.get(); String headerCurrencyCode = t.getOffer().getCounterCurrencyCode(); String colHeader = format(COL_HEADER_TRADE_BUYER_COST, headerCurrencyCode); return new StringColumn(colHeader, RIGHT); } else { return null; } }; final Predicate showCryptoBuyerAddress = (t) -> { if (isTraditionalTrade.test(t)) { return false; } else { ContractInfo contract = t.getContract(); boolean isBuyerMakerAndSellerTaker = contract.getIsBuyerMakerAndSellerTaker(); if (isTaker.test(t)) { return !isBuyerMakerAndSellerTaker; } else { return isBuyerMakerAndSellerTaker; } } }; @Nullable final Supplier> cryptoReceiveAddressColumn = () -> { if (isTradeDetailTblBuilder.get()) { TradeInfo t = firstRow.get(); if (showCryptoBuyerAddress.test(t)) { String headerCurrencyCode = toPaymentCurrencyCode.apply(t); String colHeader = format(COL_HEADER_TRADE_CRYPTO_BUYER_ADDRESS, headerCurrencyCode); return new StringColumn(colHeader); } else { return null; } } else { return null; } }; } ================================================ FILE: cli/src/main/java/haveno/cli/table/builder/TransactionTableBuilder.java ================================================ /* * 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 . */ package haveno.cli.table.builder; import haveno.cli.table.Table; import haveno.cli.table.column.BooleanColumn; import haveno.cli.table.column.Column; import haveno.cli.table.column.LongColumn; import haveno.cli.table.column.SatoshiColumn; import haveno.cli.table.column.StringColumn; import javax.annotation.Nullable; import java.util.List; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_TX_FEE; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_TX_ID; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_TX_INPUT_SUM; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_TX_IS_CONFIRMED; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_TX_OUTPUT_SUM; import static haveno.cli.table.builder.TableBuilderConstants.COL_HEADER_TX_SIZE; import static haveno.cli.table.builder.TableType.TRANSACTION_TBL; /** * Builds a {@code haveno.cli.table.Table} from a {@code haveno.proto.grpc.TxInfo} object. */ class TransactionTableBuilder extends AbstractTableBuilder { // Default columns not dynamically generated with tx info. private final Column colTxId; private final Column colIsConfirmed; private final Column colInputSum; private final Column colOutputSum; private final Column colTxFee; private final Column colTxSize; TransactionTableBuilder(List protos) { super(TRANSACTION_TBL, protos); this.colTxId = new StringColumn(COL_HEADER_TX_ID); this.colIsConfirmed = new BooleanColumn(COL_HEADER_TX_IS_CONFIRMED); this.colInputSum = new SatoshiColumn(COL_HEADER_TX_INPUT_SUM); this.colOutputSum = new SatoshiColumn(COL_HEADER_TX_OUTPUT_SUM); this.colTxFee = new SatoshiColumn(COL_HEADER_TX_FEE); this.colTxSize = new LongColumn(COL_HEADER_TX_SIZE); } public Table build() { // TODO Add 'gettransactions' api method & show multiple tx in the console. // For now, a tx tbl is only one row. // Declare the columns derived from tx info. @Nullable Column colMemo = null; // Populate columns with tx info. colTxId.addRow(null); colIsConfirmed.addRow(null); colInputSum.addRow(null); colOutputSum.addRow(null); colTxFee.addRow(null); colTxSize.addRow(null); if (colMemo != null) colMemo.addRow(null); // Define and return the table instance with populated columns. if (colMemo != null) { return new Table(colTxId, colIsConfirmed.asStringColumn(), colInputSum.asStringColumn(), colOutputSum.asStringColumn(), colTxFee.asStringColumn(), colTxSize.asStringColumn(), colMemo); } else { return new Table(colTxId, colIsConfirmed.asStringColumn(), colInputSum.asStringColumn(), colOutputSum.asStringColumn(), colTxFee.asStringColumn(), colTxSize.asStringColumn()); } } } ================================================ FILE: cli/src/main/java/haveno/cli/table/column/AbstractColumn.java ================================================ /* * 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 . */ package haveno.cli.table.column; import static com.google.common.base.Strings.padEnd; import static com.google.common.base.Strings.padStart; import static haveno.cli.table.column.Column.JUSTIFICATION.RIGHT; /** * Partial implementation of the {@link Column} interface. */ abstract class AbstractColumn, T> implements Column { // We create an encapsulated StringColumn up front to populate with formatted // strings in each this.addRow(Long value) call. But we will not know how // to justify the cached, formatted string until the column is fully populated. protected final StringColumn stringColumn; // The name field is not final, so it can be re-set for column alignment. protected String name; protected final JUSTIFICATION justification; // The max width is not known until after column is fully populated. protected int maxWidth; public AbstractColumn(String name, JUSTIFICATION justification) { this.name = name; this.justification = justification; this.stringColumn = this instanceof StringColumn ? null : new StringColumn(name, justification); } @Override public String getName() { return this.name; } @Override public void setName(String name) { this.name = name; } @Override public int getWidth() { return maxWidth; } @Override public JUSTIFICATION getJustification() { return this.justification; } @Override public Column justify() { if (this instanceof StringColumn && this.justification.equals(RIGHT)) return this.justify(); else return this; // no-op } protected final String toJustifiedString(String s) { switch (justification) { case LEFT: return padEnd(s, maxWidth, ' '); case RIGHT: return padStart(s, maxWidth, ' '); case NONE: default: return s; } } } ================================================ FILE: cli/src/main/java/haveno/cli/table/column/BooleanColumn.java ================================================ /* * 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 . */ package haveno.cli.table.column; import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; import java.util.stream.IntStream; import static haveno.cli.table.column.Column.JUSTIFICATION.LEFT; /** * For displaying boolean values as YES, NO, or user's choice for 'true' and 'false'. */ public class BooleanColumn extends AbstractColumn { private static final String DEFAULT_TRUE_AS_STRING = "YES"; private static final String DEFAULT_FALSE_AS_STRING = "NO"; private final List rows = new ArrayList<>(); private final Predicate isNewMaxWidth = (s) -> s != null && !s.isEmpty() && s.length() > maxWidth; private final String trueAsString; private final String falseAsString; // The default BooleanColumn JUSTIFICATION is LEFT. // The default BooleanColumn True AsString value is YES. // The default BooleanColumn False AsString value is NO. public BooleanColumn(String name) { this(name, LEFT, DEFAULT_TRUE_AS_STRING, DEFAULT_FALSE_AS_STRING); } // Use this constructor to override default LEFT justification. @SuppressWarnings("unused") public BooleanColumn(String name, JUSTIFICATION justification) { this(name, justification, DEFAULT_TRUE_AS_STRING, DEFAULT_FALSE_AS_STRING); } // Use this constructor to override default true/false as string defaults. public BooleanColumn(String name, String trueAsString, String falseAsString) { this(name, LEFT, trueAsString, falseAsString); } // Use this constructor to override default LEFT justification. public BooleanColumn(String name, JUSTIFICATION justification, String trueAsString, String falseAsString) { super(name, justification); this.trueAsString = trueAsString; this.falseAsString = falseAsString; this.maxWidth = name.length(); } @Override public void addRow(Boolean value) { rows.add(value); // We do not know how much padding each StringColumn value needs until it has all the values. String s = asString(value); stringColumn.addRow(s); if (isNewMaxWidth.test(s)) maxWidth = s.length(); } @Override public List getRows() { return rows; } @Override public int rowCount() { return rows.size(); } @Override public boolean isEmpty() { return rows.isEmpty(); } @Override public Boolean getRow(int rowIndex) { return rows.get(rowIndex); } @Override public void updateRow(int rowIndex, Boolean newValue) { rows.set(rowIndex, newValue); } @Override public String getRowAsFormattedString(int rowIndex) { return getRow(rowIndex) ? trueAsString : falseAsString; } @Override public StringColumn asStringColumn() { // We cached the formatted satoshi strings, but we did // not know how much padding each string needed until now. IntStream.range(0, stringColumn.getRows().size()).forEach(rowIndex -> { String unjustified = stringColumn.getRow(rowIndex); String justified = stringColumn.toJustifiedString(unjustified); stringColumn.updateRow(rowIndex, justified); }); return stringColumn; } private String asString(boolean value) { return value ? trueAsString : falseAsString; } } ================================================ FILE: cli/src/main/java/haveno/cli/table/column/BtcColumn.java ================================================ package haveno.cli.table.column; import java.util.stream.IntStream; import static com.google.common.base.Strings.padEnd; import static haveno.cli.CurrencyFormat.formatBtc; import static java.util.Comparator.comparingInt; public class BtcColumn extends SatoshiColumn { public BtcColumn(String name) { super(name); } @Override public void addRow(Long value) { rows.add(value); String s = formatBtc(value); stringColumn.addRow(s); if (isNewMaxWidth.test(s)) maxWidth = s.length(); } @Override public String getRowAsFormattedString(int rowIndex) { return formatBtc(getRow(rowIndex)); } @Override public StringColumn asStringColumn() { // We cached the formatted satoshi strings, but we did // not know how much zero padding each string needed until now. int maxColumnValueWidth = stringColumn.getRows().stream() .max(comparingInt(String::length)) .get() .length(); IntStream.range(0, stringColumn.getRows().size()).forEach(rowIndex -> { String btcString = stringColumn.getRow(rowIndex); if (btcString.length() < maxColumnValueWidth) { String paddedBtcString = padEnd(btcString, maxColumnValueWidth, '0'); stringColumn.updateRow(rowIndex, paddedBtcString); } }); return stringColumn.justify(); } } ================================================ FILE: cli/src/main/java/haveno/cli/table/column/Column.java ================================================ /* * 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 . */ package haveno.cli.table.column; import java.util.List; public interface Column { enum JUSTIFICATION { LEFT, RIGHT, NONE } /** * Returns the column's name. * * @return name as String */ String getName(); /** * Sets the column name. * * @param name of the column */ void setName(String name); /** * Add column value. * * @param value added to column's data (row) */ void addRow(T value); /** * Returns the column data. * * @return rows as List */ List getRows(); /** * Returns the maximum width of the column name, or longest, * formatted string value -- whichever is greater. * * @return width of the populated column as int */ int getWidth(); /** * Returns the number of rows in the column. * * @return number of rows in the column as int. */ int rowCount(); /** * Returns true if the column has no data. * * @return true if empty, false if not */ boolean isEmpty(); /** * Returns the column value (data) at given row index. * * @return value object */ T getRow(int rowIndex); /** * Update an existing value at the given row index to a new value. * * @param rowIndex row index of value to be updated * @param newValue new value */ void updateRow(int rowIndex, T newValue); /** * Returns the row value as a formatted String. * * @return a row value as formatted String */ String getRowAsFormattedString(int rowIndex); /** * Return the column with all of its data as a StringColumn with all of its * formatted string data. * * @return StringColumn */ StringColumn asStringColumn(); /** * Convenience for justifying populated StringColumns before being displayed. * Is only useful for StringColumn instances. */ Column justify(); /** * Returns JUSTIFICATION value (RIGHT|LEFT|NONE) for the column. * * @return column JUSTIFICATION */ JUSTIFICATION getJustification(); } ================================================ FILE: cli/src/main/java/haveno/cli/table/column/CryptoVolumeColumn.java ================================================ /* * 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 . */ package haveno.cli.table.column; import java.math.BigDecimal; import java.util.function.BiFunction; import java.util.stream.IntStream; import static haveno.cli.table.column.Column.JUSTIFICATION.RIGHT; /** * For displaying crypto volume with appropriate precision. */ public class CryptoVolumeColumn extends LongColumn { public enum DISPLAY_MODE { CRYPTO_VOLUME, BSQ_VOLUME, } private final DISPLAY_MODE displayMode; // The default CryptoVolumeColumn JUSTIFICATION is RIGHT. public CryptoVolumeColumn(String name, DISPLAY_MODE displayMode) { this(name, RIGHT, displayMode); } public CryptoVolumeColumn(String name, JUSTIFICATION justification, DISPLAY_MODE displayMode) { super(name, justification); this.displayMode = displayMode; } @Override public void addRow(Long value) { rows.add(value); String s = toFormattedString.apply(value, displayMode); stringColumn.addRow(s); if (isNewMaxWidth.test(s)) maxWidth = s.length(); } @Override public String getRowAsFormattedString(int rowIndex) { return toFormattedString.apply(getRow(rowIndex), displayMode); } @Override public StringColumn asStringColumn() { // We cached the formatted crypto value strings, but we did // not know how much padding each string needed until now. IntStream.range(0, stringColumn.getRows().size()).forEach(rowIndex -> { String unjustified = stringColumn.getRow(rowIndex); String justified = stringColumn.toJustifiedString(unjustified); stringColumn.updateRow(rowIndex, justified); }); return this.stringColumn; } private final BiFunction toFormattedString = (value, displayMode) -> { switch (displayMode) { case CRYPTO_VOLUME: return value > 0 ? new BigDecimal(value).movePointLeft(8).toString() : ""; case BSQ_VOLUME: return value > 0 ? new BigDecimal(value).movePointLeft(2).toString() : ""; default: throw new IllegalStateException("invalid display mode: " + displayMode); } }; } ================================================ FILE: cli/src/main/java/haveno/cli/table/column/DoubleColumn.java ================================================ /* * 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 . */ package haveno.cli.table.column; import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; import java.util.stream.IntStream; import static haveno.cli.table.column.Column.JUSTIFICATION.RIGHT; /** * For displaying Double values. */ public class DoubleColumn extends NumberColumn { protected final List rows = new ArrayList<>(); protected final Predicate isNewMaxWidth = (s) -> s != null && !s.isEmpty() && s.length() > maxWidth; // The default DoubleColumn JUSTIFICATION is RIGHT. public DoubleColumn(String name) { this(name, RIGHT); } public DoubleColumn(String name, JUSTIFICATION justification) { super(name, justification); this.maxWidth = name.length(); } @Override public void addRow(Double value) { rows.add(value); String s = String.valueOf(value); if (isNewMaxWidth.test(s)) maxWidth = s.length(); } @Override public List getRows() { return rows; } @Override public int rowCount() { return rows.size(); } @Override public boolean isEmpty() { return rows.isEmpty(); } @Override public Double getRow(int rowIndex) { return rows.get(rowIndex); } @Override public void updateRow(int rowIndex, Double newValue) { rows.set(rowIndex, newValue); } @Override public String getRowAsFormattedString(int rowIndex) { String s = String.valueOf(getRow(rowIndex)); return toJustifiedString(s); } @Override public StringColumn asStringColumn() { IntStream.range(0, rows.size()).forEachOrdered(rowIndex -> stringColumn.addRow(getRowAsFormattedString(rowIndex))); return stringColumn; } } ================================================ FILE: cli/src/main/java/haveno/cli/table/column/IntegerColumn.java ================================================ /* * 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 . */ package haveno.cli.table.column; import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; import java.util.stream.IntStream; import static haveno.cli.table.column.Column.JUSTIFICATION.RIGHT; /** * For displaying Integer values. */ public class IntegerColumn extends NumberColumn { protected final List rows = new ArrayList<>(); protected final Predicate isNewMaxWidth = (s) -> s != null && !s.isEmpty() && s.length() > maxWidth; // The default IntegerColumn JUSTIFICATION is RIGHT. public IntegerColumn(String name) { this(name, RIGHT); } public IntegerColumn(String name, JUSTIFICATION justification) { super(name, justification); this.maxWidth = name.length(); } @Override public void addRow(Integer value) { rows.add(value); String s = String.valueOf(value); if (isNewMaxWidth.test(s)) maxWidth = s.length(); } @Override public List getRows() { return rows; } @Override public int rowCount() { return rows.size(); } @Override public boolean isEmpty() { return rows.isEmpty(); } @Override public Integer getRow(int rowIndex) { return rows.get(rowIndex); } @Override public void updateRow(int rowIndex, Integer newValue) { rows.set(rowIndex, newValue); } @Override public String getRowAsFormattedString(int rowIndex) { String s = String.valueOf(getRow(rowIndex)); return toJustifiedString(s); } @Override public StringColumn asStringColumn() { IntStream.range(0, rows.size()).forEachOrdered(rowIndex -> stringColumn.addRow(getRowAsFormattedString(rowIndex))); return stringColumn; } } ================================================ FILE: cli/src/main/java/haveno/cli/table/column/Iso8601DateTimeColumn.java ================================================ /* * 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 . */ package haveno.cli.table.column; import java.text.SimpleDateFormat; import java.util.Date; import java.util.stream.IntStream; import static com.google.common.base.Strings.padEnd; import static com.google.common.base.Strings.padStart; import static haveno.cli.table.column.Column.JUSTIFICATION.LEFT; import static java.lang.System.currentTimeMillis; import static java.util.TimeZone.getTimeZone; /** * For displaying (long) timestamp values as ISO-8601 dates in UTC time zone. */ public class Iso8601DateTimeColumn extends LongColumn { protected final SimpleDateFormat iso8601DateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); // The default Iso8601DateTimeColumn JUSTIFICATION is LEFT. public Iso8601DateTimeColumn(String name) { this(name, LEFT); } public Iso8601DateTimeColumn(String name, JUSTIFICATION justification) { super(name, justification); iso8601DateFormat.setTimeZone(getTimeZone("UTC")); this.maxWidth = Math.max(name.length(), String.valueOf(currentTimeMillis()).length()); } @Override public String getRowAsFormattedString(int rowIndex) { long time = getRow(rowIndex); return justification.equals(LEFT) ? padEnd(iso8601DateFormat.format(new Date(time)), maxWidth, ' ') : padStart(iso8601DateFormat.format(new Date(time)), maxWidth, ' '); } @Override public StringColumn asStringColumn() { IntStream.range(0, rows.size()).forEachOrdered(rowIndex -> stringColumn.addRow(getRowAsFormattedString(rowIndex))); return stringColumn; } } ================================================ FILE: cli/src/main/java/haveno/cli/table/column/LongColumn.java ================================================ /* * 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 . */ package haveno.cli.table.column; import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; import java.util.stream.IntStream; import static haveno.cli.table.column.Column.JUSTIFICATION.RIGHT; /** * For displaying Long values. */ public class LongColumn extends NumberColumn { protected final List rows = new ArrayList<>(); protected final Predicate isNewMaxWidth = (s) -> s != null && !s.isEmpty() && s.length() > maxWidth; // The default LongColumn JUSTIFICATION is RIGHT. public LongColumn(String name) { this(name, RIGHT); } public LongColumn(String name, JUSTIFICATION justification) { super(name, justification); this.maxWidth = name.length(); } @Override public void addRow(Long value) { rows.add(value); String s = String.valueOf(value); if (isNewMaxWidth.test(s)) maxWidth = s.length(); } @Override public List getRows() { return rows; } @Override public int rowCount() { return rows.size(); } @Override public boolean isEmpty() { return rows.isEmpty(); } @Override public Long getRow(int rowIndex) { return rows.get(rowIndex); } @Override public void updateRow(int rowIndex, Long newValue) { rows.set(rowIndex, newValue); } @Override public String getRowAsFormattedString(int rowIndex) { String s = String.valueOf(getRow(rowIndex)); return toJustifiedString(s); } @Override public StringColumn asStringColumn() { IntStream.range(0, rows.size()).forEachOrdered(rowIndex -> stringColumn.addRow(getRowAsFormattedString(rowIndex))); return stringColumn; } } ================================================ FILE: cli/src/main/java/haveno/cli/table/column/MixedTradeFeeColumn.java ================================================ /* * 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 . */ package haveno.cli.table.column; import static haveno.cli.CurrencyFormat.formatBsq; import static haveno.cli.CurrencyFormat.formatSatoshis; import static haveno.cli.table.column.Column.JUSTIFICATION.RIGHT; /** * For displaying a mix of BSQ and BTC trade fees with appropriate precision. */ public class MixedTradeFeeColumn extends LongColumn { public MixedTradeFeeColumn(String name) { super(name, RIGHT); } @Override public void addRow(Long value) { throw new UnsupportedOperationException("use public void addRow(Long value, boolean isBsq) instead"); } public void addRow(Long value, boolean isBsq) { rows.add(value); String s = isBsq ? formatBsq(value) + " BSQ" : formatSatoshis(value) + " BTC"; stringColumn.addRow(s); if (isNewMaxWidth.test(s)) maxWidth = s.length(); } @Override public String getRowAsFormattedString(int rowIndex) { return getRow(rowIndex).toString(); } @Override public StringColumn asStringColumn() { return stringColumn.justify(); } } ================================================ FILE: cli/src/main/java/haveno/cli/table/column/NumberColumn.java ================================================ /* * 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 . */ package haveno.cli.table.column; /** * Abstract superclass for numeric Columns. * * @param the subclass column's type (LongColumn, IntegerColumn, ...) * @param the subclass column's numeric Java type (Long, Integer, ...) */ abstract class NumberColumn, T extends Number> extends AbstractColumn implements Column { public NumberColumn(String name, JUSTIFICATION justification) { super(name, justification); } } ================================================ FILE: cli/src/main/java/haveno/cli/table/column/SatoshiColumn.java ================================================ /* * 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 . */ package haveno.cli.table.column; import static haveno.cli.CurrencyFormat.formatBsq; import static haveno.cli.CurrencyFormat.formatSatoshis; import static haveno.cli.table.column.Column.JUSTIFICATION.RIGHT; /** * For displaying BTC or BSQ satoshi values with appropriate precision. */ public class SatoshiColumn extends LongColumn { protected final boolean isBsqSatoshis; // The default SatoshiColumn JUSTIFICATION is RIGHT. public SatoshiColumn(String name) { this(name, RIGHT, false); } public SatoshiColumn(String name, boolean isBsqSatoshis) { this(name, RIGHT, isBsqSatoshis); } public SatoshiColumn(String name, JUSTIFICATION justification) { this(name, justification, false); } public SatoshiColumn(String name, JUSTIFICATION justification, boolean isBsqSatoshis) { super(name, justification); this.isBsqSatoshis = isBsqSatoshis; } @Override public void addRow(Long value) { rows.add(value); // We do not know how much padding each StringColumn value needs until it has all the values. String s = isBsqSatoshis ? formatBsq(value) : formatSatoshis(value); stringColumn.addRow(s); if (isNewMaxWidth.test(s)) maxWidth = s.length(); } @Override public String getRowAsFormattedString(int rowIndex) { return isBsqSatoshis ? formatBsq(getRow(rowIndex)) : formatSatoshis(getRow(rowIndex)); } @Override public StringColumn asStringColumn() { return stringColumn.justify(); } } ================================================ FILE: cli/src/main/java/haveno/cli/table/column/StringColumn.java ================================================ /* * 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 . */ package haveno.cli.table.column; import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; import java.util.stream.IntStream; import static haveno.cli.table.column.Column.JUSTIFICATION.LEFT; import static haveno.cli.table.column.Column.JUSTIFICATION.RIGHT; /** * For displaying justified string values. */ public class StringColumn extends AbstractColumn { private final List rows = new ArrayList<>(); private final Predicate isNewMaxWidth = (s) -> s != null && !s.isEmpty() && s.length() > maxWidth; // The default StringColumn JUSTIFICATION is LEFT. public StringColumn(String name) { this(name, LEFT); } // Use this constructor to override default LEFT justification. public StringColumn(String name, JUSTIFICATION justification) { super(name, justification); this.maxWidth = name.length(); } @Override public void addRow(String value) { rows.add(value); if (isNewMaxWidth.test(value)) maxWidth = value.length(); } @Override public List getRows() { return rows; } @Override public int rowCount() { return rows.size(); } @Override public boolean isEmpty() { return rows.isEmpty(); } @Override public String getRow(int rowIndex) { return rows.get(rowIndex); } @Override public void updateRow(int rowIndex, String newValue) { rows.set(rowIndex, newValue); } @Override public String getRowAsFormattedString(int rowIndex) { return getRow(rowIndex); } @Override public StringColumn asStringColumn() { return this; } @Override public StringColumn justify() { if (justification.equals(RIGHT)) { IntStream.range(0, getRows().size()).forEach(rowIndex -> { String unjustified = getRow(rowIndex); String justified = toJustifiedString(unjustified); updateRow(rowIndex, justified); }); } return this; } } ================================================ FILE: cli/src/main/java/haveno/cli/table/column/ZippedStringColumns.java ================================================ /* * 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 . */ package haveno.cli.table.column; import haveno.cli.table.column.Column.JUSTIFICATION; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; import java.util.stream.IntStream; import static haveno.cli.table.column.ZippedStringColumns.DUPLICATION_MODE.EXCLUDE_DUPLICATES; import static haveno.cli.table.column.ZippedStringColumns.DUPLICATION_MODE.INCLUDE_DUPLICATES; /** * For zipping multiple StringColumns into a single StringColumn. * Useful for displaying amount and volume range values. */ public class ZippedStringColumns { public enum DUPLICATION_MODE { EXCLUDE_DUPLICATES, INCLUDE_DUPLICATES } private final String name; private final JUSTIFICATION justification; private final String delimiter; private final StringColumn[] columns; public ZippedStringColumns(String name, JUSTIFICATION justification, String delimiter, StringColumn... columns) { this.name = name; this.justification = justification; this.delimiter = delimiter; this.columns = columns; validateColumnData(); } public StringColumn asStringColumn(DUPLICATION_MODE duplicationMode) { StringColumn stringColumn = new StringColumn(name, justification); buildRows(stringColumn, duplicationMode); // Re-set the column name field to its justified value, in case any of the column // values are longer than the name passed to this constructor. stringColumn.setName(stringColumn.toJustifiedString(name)); return stringColumn; } private void buildRows(StringColumn stringColumn, DUPLICATION_MODE duplicationMode) { // Populate the StringColumn with unjustified zipped values; we cannot justify // the zipped values until stringColumn knows its final maxWidth. IntStream.range(0, columns[0].getRows().size()).forEach(rowIndex -> { String row = buildRow(rowIndex, duplicationMode); stringColumn.addRow(row); }); formatRows(stringColumn); } private String buildRow(int rowIndex, DUPLICATION_MODE duplicationMode) { StringBuilder rowBuilder = new StringBuilder(); @Nullable List processedValues = duplicationMode.equals(EXCLUDE_DUPLICATES) ? new ArrayList<>() : null; IntStream.range(0, columns.length).forEachOrdered(colIndex -> { // For each column @ rowIndex ... var value = columns[colIndex].getRows().get(rowIndex); if (duplicationMode.equals(INCLUDE_DUPLICATES)) { if (rowBuilder.length() > 0) rowBuilder.append(delimiter); rowBuilder.append(value); } else if (!processedValues.contains(value)) { if (rowBuilder.length() > 0) rowBuilder.append(delimiter); rowBuilder.append(value); processedValues.add(value); } }); return rowBuilder.toString(); } private void formatRows(StringColumn stringColumn) { // Now we can justify the zipped string values in the new StringColumn. IntStream.range(0, stringColumn.getRows().size()).forEach(rowIndex -> { String unjustified = stringColumn.getRow(rowIndex); String justified = stringColumn.toJustifiedString(unjustified); stringColumn.updateRow(rowIndex, justified); }); } private void validateColumnData() { if (columns.length == 0) throw new IllegalStateException("cannot zip columns because they do not have any data"); StringColumn firstColumn = columns[0]; if (firstColumn.getRows().isEmpty()) throw new IllegalStateException("1st column has no data"); IntStream.range(1, columns.length).forEach(colIndex -> { if (columns[colIndex].getRows().size() != firstColumn.getRows().size()) throw new IllegalStateException("columns do not have same number of rows"); }); } } ================================================ FILE: cli/src/main/resources/logback.xml ================================================ %highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n) ================================================ FILE: cli/src/test/java/haveno/cli/AbstractCliTest.java ================================================ package haveno.cli; import haveno.cli.opts.ArgumentList; import haveno.proto.grpc.OfferInfo; import joptsimple.OptionParser; import joptsimple.OptionSet; import lombok.extern.slf4j.Slf4j; import org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch; import java.math.BigDecimal; import java.util.LinkedList; import java.util.List; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.function.BiFunction; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.IntStream; import static haveno.cli.opts.OptLabel.OPT_HOST; import static haveno.cli.opts.OptLabel.OPT_PASSWORD; import static haveno.cli.opts.OptLabel.OPT_PORT; import static java.lang.System.out; import static java.math.RoundingMode.HALF_UP; import static java.util.Arrays.stream; import static org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch.Operation.DELETE; import static org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch.Operation.INSERT; /** * Parent class for CLI smoke tests. Useful for examining the format of the console * output, and checking for diffs while making changes to console output formatters. * * Tests that create offers or trades should not be run on mainnet. */ @Slf4j public abstract class AbstractCliTest { static final String PASSWORD_OPT = "--password=xyz"; // Both daemons' password. static final String ALICE_PORT_OPT = "--port=" + 9998; // Alice's daemon port. static final String BOB_PORT_OPT = "--port=" + 9999; // Bob's daemon port. static final String[] BASE_ALICE_CLIENT_OPTS = new String[]{PASSWORD_OPT, ALICE_PORT_OPT}; static final String[] BASE_BOB_CLIENT_OPTS = new String[]{PASSWORD_OPT, BOB_PORT_OPT}; protected final BiFunction> randomMarginBasedPrices = (min, max) -> IntStream.range(min, max).asDoubleStream() .boxed() .map(d -> d / 100) .map(Object::toString) .collect(Collectors.toList()); protected final BiFunction randomFixedCryptoPrice = (min, max) -> { String random = Double.valueOf(ThreadLocalRandom.current().nextDouble(min, max)).toString(); BigDecimal bd = new BigDecimal(random).setScale(8, HALF_UP); return bd.toPlainString(); }; protected final GrpcClient aliceClient; protected final GrpcClient bobClient; public AbstractCliTest() { this.aliceClient = getGrpcClient(BASE_ALICE_CLIENT_OPTS); this.bobClient = getGrpcClient(BASE_BOB_CLIENT_OPTS); } protected GrpcClient getGrpcClient(String[] args) { var parser = new OptionParser(); var hostOpt = parser.accepts(OPT_HOST, "rpc server hostname or ip") .withRequiredArg() .defaultsTo("localhost"); var portOpt = parser.accepts(OPT_PORT, "rpc server port") .withRequiredArg() .ofType(Integer.class) .defaultsTo(9998); var passwordOpt = parser.accepts(OPT_PASSWORD, "rpc server password") .withRequiredArg(); OptionSet options = parser.parse(new ArgumentList(args).getCLIArguments()); var host = options.valueOf(hostOpt); var port = options.valueOf(portOpt); var password = options.valueOf(passwordOpt); if (password == null) throw new IllegalArgumentException("missing required 'password' option"); return new GrpcClient(host, port, password); } protected void checkDiffsIgnoreWhitespace(String oldOutput, String newOutput) { Predicate isInsertOrDelete = (operation) -> operation.equals(INSERT) || operation.equals(DELETE); Predicate isWhitespace = (text) -> text.trim().isEmpty(); boolean hasNonWhitespaceDiffs = false; if (!oldOutput.equals(newOutput)) { DiffMatchPatch dmp = new DiffMatchPatch(); LinkedList diff = dmp.diffMain(oldOutput, newOutput, true); for (DiffMatchPatch.Diff d : diff) { if (isInsertOrDelete.test(d.operation) && !isWhitespace.test(d.text)) { hasNonWhitespaceDiffs = true; log.error(">>> DIFF {}", d); } } } if (hasNonWhitespaceDiffs) log.error("FAIL: There were diffs"); else log.info("PASS: No diffs"); } protected void printOldTbl(String tbl) { log.info("OLD Console OUT:\n{}", tbl); } protected void printNewTbl(String tbl) { log.info("NEW Console OUT:\n{}", tbl); } protected List getMyCryptoOffers(String currencyCode) { String[] args = getMyOffersCommand("buy", currencyCode); out.print(">>>>> haveno-cli "); stream(args).forEach(a -> out.print(a + " ")); out.println(); CliMain.main(args); out.println("<<<<<"); args = getMyOffersCommand("sell", currencyCode); out.print(">>>>> haveno-cli "); stream(args).forEach(a -> out.print(a + " ")); out.println(); CliMain.main(args); out.println("<<<<<"); return aliceClient.getMyOffersSortedByDate(currencyCode); } protected String[] getMyOffersCommand(String direction, String currencyCode) { return new String[]{ PASSWORD_OPT, ALICE_PORT_OPT, "getmyoffers", "--direction=" + direction, "--currency-code=" + currencyCode }; } protected String[] getAvailableOffersCommand(String direction, String currencyCode) { return new String[]{ PASSWORD_OPT, BOB_PORT_OPT, "getoffers", "--direction=" + direction, "--currency-code=" + currencyCode }; } protected void editOfferPriceMargin(OfferInfo offer, String priceMargin, boolean enable) { String[] args = new String[]{ PASSWORD_OPT, ALICE_PORT_OPT, "editoffer", "--offer-id=" + offer.getId(), "--market-price-margin=" + priceMargin, "--enable=" + enable }; out.print(">>>>> haveno-cli "); stream(args).forEach(a -> out.print(a + " ")); out.println(); CliMain.main(args); out.println("<<<<<"); } protected void editOfferTriggerPrice(OfferInfo offer, String triggerPrice, boolean enable) { String[] args = new String[]{ PASSWORD_OPT, ALICE_PORT_OPT, "editoffer", "--offer-id=" + offer.getId(), "--trigger-price=" + triggerPrice, "--enable=" + enable }; out.print(">>>>> haveno-cli "); stream(args).forEach(a -> out.print(a + " ")); out.println(); CliMain.main(args); out.println("<<<<<"); } protected void editOfferPriceMarginAndTriggerPrice(OfferInfo offer, String priceMargin, String triggerPrice, boolean enable) { String[] args = new String[]{ PASSWORD_OPT, ALICE_PORT_OPT, "editoffer", "--offer-id=" + offer.getId(), "--market-price-margin=" + priceMargin, "--trigger-price=" + triggerPrice, "--enable=" + enable }; out.print(">>>>> haveno-cli "); stream(args).forEach(a -> out.print(a + " ")); out.println(); CliMain.main(args); out.println("<<<<<"); } protected void editOfferFixedPrice(OfferInfo offer, String fixedPrice, boolean enable) { String[] args = new String[]{ PASSWORD_OPT, ALICE_PORT_OPT, "editoffer", "--offer-id=" + offer.getId(), "--fixed-price=" + fixedPrice, "--enable=" + enable }; out.print(">>>>> haveno-cli "); stream(args).forEach(a -> out.print(a + " ")); out.println(); CliMain.main(args); out.println("<<<<<"); } protected void disableOffers(List offers) { out.println("Disable Offers"); for (OfferInfo offer : offers) { editOfferEnable(offer, false); sleep(5); } } protected void enableOffers(List offers) { out.println("Enable Offers"); for (OfferInfo offer : offers) { editOfferEnable(offer, true); sleep(5); } } protected void editOfferEnable(OfferInfo offer, boolean enable) { String[] args = new String[]{ PASSWORD_OPT, ALICE_PORT_OPT, "editoffer", "--offer-id=" + offer.getId(), "--enable=" + enable }; out.print(">>>>> haveno-cli "); stream(args).forEach(a -> out.print(a + " ")); out.println(); CliMain.main(args); out.println("<<<<<"); } protected void sleep(long seconds) { try { TimeUnit.SECONDS.sleep(seconds); } catch (InterruptedException e) { e.printStackTrace(); } } } ================================================ FILE: cli/src/test/java/haveno/cli/CreateOfferSmokeTest.java ================================================ package haveno.cli; import static java.lang.System.out; import static java.util.Arrays.stream; /** Smoke tests for createoffer method. Useful for testing CLI command and examining the format of its console output. Prerequisites: - Run `./haveno-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon --shutdownAfterTests=false --enableHavenoDebugging=false` Note: Test harness will not automatically generate BTC blocks to confirm transactions. Never run on mainnet! */ @SuppressWarnings({"CommentedOutCode", "unused"}) public class CreateOfferSmokeTest extends AbstractCliTest { public static void main(String[] args) { CreateOfferSmokeTest test = new CreateOfferSmokeTest(); test.createBsqSwapOffer("buy"); test.createBsqSwapOffer("sell"); } private void createBsqSwapOffer(String direction) { String[] args = createBsqSwapOfferCommand(direction, "0.01", "0.005", "0.00005"); out.print(">>>>> haveno-cli "); stream(args).forEach(a -> out.print(a + " ")); out.println(); CliMain.main(args); out.println("<<<<<"); args = getMyOffersCommand(direction, "bsq"); out.print(">>>>> haveno-cli "); stream(args).forEach(a -> out.print(a + " ")); out.println(); CliMain.main(args); out.println("<<<<<"); args = getAvailableOffersCommand(direction, "bsq"); out.print(">>>>> haveno-cli "); stream(args).forEach(a -> out.print(a + " ")); out.println(); CliMain.main(args); out.println("<<<<<"); } private String[] createBsqSwapOfferCommand(String direction, String amount, String minAmount, String fixedPrice) { return new String[]{ PASSWORD_OPT, ALICE_PORT_OPT, "createoffer", "--swap=true", "--direction=" + direction, "--currency-code=bsq", "--amount=" + amount, "--min-amount=" + minAmount, "--fixed-price=" + fixedPrice }; } } ================================================ FILE: cli/src/test/java/haveno/cli/EditXmrOffersSmokeTest.java ================================================ package haveno.cli; import haveno.proto.grpc.OfferInfo; import java.util.List; import java.util.Random; import static java.lang.System.out; import static protobuf.OfferDirection.BUY; /** Smoke tests for the editoffer method. Prerequisites: - Run `./haveno-apitest --apiPassword=xyz --supportingApps=bitcoind,seednode,arbdaemon,alicedaemon,bobdesktop --shutdownAfterTests=false --enableHavenoDebugging=false` - Create some XMR offers with Alice's UI or CLI. - Watch Alice's offers being edited in Bob's UI. Never run on mainnet. */ public class EditXmrOffersSmokeTest extends AbstractCliTest { public static void main(String[] args) { var test = new EditXmrOffersSmokeTest(); test.doOfferPriceEdits(); List offers = test.getMyCryptoOffers("xmr"); test.disableOffers(offers); test.sleep(6); offers = test.getMyCryptoOffers("xmr"); test.enableOffers(offers); // A final look after last edit. test.getMyCryptoOffers("xmr"); } private void doOfferPriceEdits() { editPriceMargin(); editTriggerPrice(); editPriceMarginAndTriggerPrice(); editFixedPrice(); } private void editPriceMargin() { var offers = getMyCryptoOffers("xmr"); out.println("Edit XMR offers' price margin"); var margins = randomMarginBasedPrices.apply(-301, 300); for (int i = 0; i < offers.size(); i++) { String randomMargin = margins.get(new Random().nextInt(margins.size())); editOfferPriceMargin(offers.get(i), randomMargin, new Random().nextBoolean()); sleep(5); } } private void editTriggerPrice() { var offers = getMyCryptoOffers("xmr"); out.println("Edit XMR offers' trigger price"); for (int i = 0; i < offers.size(); i++) { var offer = offers.get(i); if (offer.getUseMarketBasedPrice()) { // Trigger price is hardcode to be a bit above or below xmr mkt price at runtime. // It could be looked up and calculated instead. var newTriggerPrice = offer.getDirection().equals(BUY.name()) ? "0.0039" : "0.005"; editOfferTriggerPrice(offer, newTriggerPrice, true); sleep(5); } } } private void editPriceMarginAndTriggerPrice() { var offers = getMyCryptoOffers("xmr"); out.println("Edit XMR offers' price margin and trigger price"); for (int i = 0; i < offers.size(); i++) { var offer = offers.get(i); if (offer.getUseMarketBasedPrice()) { // Trigger price is hardcode to be a bit above or below xmr mkt price at runtime. // It could be looked up and calculated instead. var newTriggerPrice = offer.getDirection().equals(BUY.name()) ? "0.0038" : "0.0051"; editOfferPriceMarginAndTriggerPrice(offer, "0.05", newTriggerPrice, true); sleep(5); } } } private void editFixedPrice() { var offers = getMyCryptoOffers("xmr"); out.println("Edit XMR offers' fixed price"); for (int i = 0; i < offers.size(); i++) { String randomFixedPrice = randomFixedCryptoPrice.apply(0.004, 0.0075); editOfferFixedPrice(offers.get(i), randomFixedPrice, new Random().nextBoolean()); sleep(5); } } } ================================================ FILE: cli/src/test/java/haveno/cli/GetOffersSmokeTest.java ================================================ package haveno.cli; import static java.lang.System.out; /** Smoke tests for getoffers method. Useful for examining the format of the console output. Prerequisites: - Run `./haveno-daemon --apiPassword=xyz --appDataDir=$TESTDIR` This can be run on mainnet. */ @SuppressWarnings({"CommentedOutCode", "unused"}) public class GetOffersSmokeTest extends AbstractCliTest { // TODO use the static password and port opt definitions in superclass public static void main(String[] args) { getMyBsqOffers(); // getAvailableBsqOffers(); // getMyUsdOffers(); // getAvailableUsdOffers(); } private static void getMyBsqOffers() { out.println(">>> getmyoffers buy bsq"); CliMain.main(new String[]{"--password=xyz", "--port=9998", "getmyoffers", "--direction=buy", "--currency-code=bsq"}); out.println(">>> getmyoffers sell bsq"); CliMain.main(new String[]{"--password=xyz", "--port=9998", "getmyoffers", "--direction=sell", "--currency-code=bsq"}); out.println(">>> getmyoffer --offer-id=KRONTTMO-11cef1a9-c636-4dc7-b3f2-1616e4960c28-175"); CliMain.main(new String[]{"--password=xyz", "--port=9998", "getmyoffer", "--offer-id=KRONTTMO-11cef1a9-c636-4dc7-b3f2-1616e4960c28-175"}); } private static void getAvailableBsqOffers() { out.println(">>> getoffers buy bsq"); CliMain.main(new String[]{"--password=xyz", "--port=9998", "getoffers", "--direction=buy", "--currency-code=bsq"}); out.println(">>> getoffers sell bsq"); CliMain.main(new String[]{"--password=xyz", "--port=9998", "getoffers", "--direction=sell", "--currency-code=bsq"}); } private static void getMyUsdOffers() { out.println(">>> getmyoffers buy usd"); CliMain.main(new String[]{"--password=xyz", "--port=9998", "getmyoffers", "--direction=buy", "--currency-code=usd"}); out.println(">>> getmyoffers sell usd"); CliMain.main(new String[]{"--password=xyz", "--port=9998", "getmyoffers", "--direction=sell", "--currency-code=usd"}); } private static void getAvailableUsdOffers() { out.println(">>> getoffers buy usd"); CliMain.main(new String[]{"--password=xyz", "--port=9998", "getoffers", "--direction=buy", "--currency-code=usd"}); out.println(">>> getoffers sell usd"); CliMain.main(new String[]{"--password=xyz", "--port=9998", "getoffers", "--direction=sell", "--currency-code=usd"}); } private static void TODO() { out.println(">>> getoffers buy eur"); CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=buy", "--currency-code=eur"}); out.println(">>> getoffers sell eur"); CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=sell", "--currency-code=eur"}); out.println(">>> getoffers buy gbp"); CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=buy", "--currency-code=gbp"}); out.println(">>> getoffers sell gbp"); CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=sell", "--currency-code=gbp"}); out.println(">>> getoffers buy brl"); CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=buy", "--currency-code=brl"}); out.println(">>> getoffers sell brl"); CliMain.main(new String[]{"--password=xyz", "getoffers", "--direction=sell", "--currency-code=brl"}); } } ================================================ FILE: cli/src/test/java/haveno/cli/GetTradesSmokeTest.java ================================================ package haveno.cli; import haveno.cli.table.builder.TableBuilder; import haveno.proto.grpc.TradeInfo; import java.util.List; import static haveno.cli.table.builder.TableType.TRADE_DETAIL_TBL; import static haveno.proto.grpc.GetTradesRequest.Category.CLOSED; import static java.lang.System.out; @SuppressWarnings("unused") public class GetTradesSmokeTest extends AbstractCliTest { public static void main(String[] args) { GetTradesSmokeTest test = new GetTradesSmokeTest(); test.printAlicesTrades(); test.printBobsTrades(); } private final List openTrades; private final List closedTrades; public GetTradesSmokeTest() { super(); this.openTrades = aliceClient.getOpenTrades(); this.closedTrades = aliceClient.getTradeHistory(CLOSED); } private void printAlicesTrades() { out.println("ALICE'S OPEN TRADES"); openTrades.stream().forEachOrdered(t -> printTrade(aliceClient, t.getTradeId())); out.println("ALICE'S CLOSED TRADES"); closedTrades.stream().forEachOrdered(t -> printTrade(aliceClient, t.getTradeId())); } private void printBobsTrades() { out.println("BOB'S OPEN TRADES"); openTrades.stream().forEachOrdered(t -> printTrade(bobClient, t.getTradeId())); out.println("BOB'S CLOSED TRADES"); closedTrades.stream().forEachOrdered(t -> printTrade(bobClient, t.getTradeId())); } private void printTrade(GrpcClient client, String tradeId) { var trade = client.getTrade(tradeId); var tbl = new TableBuilder(TRADE_DETAIL_TBL, trade).build().toString(); out.println(tbl); } } ================================================ FILE: cli/src/test/java/haveno/cli/opts/OptionParsersTest.java ================================================ package haveno.cli.opts; import org.junit.jupiter.api.Test; import static haveno.cli.Method.canceloffer; import static haveno.cli.Method.createcryptopaymentacct; import static haveno.cli.Method.createoffer; import static haveno.cli.Method.createpaymentacct; import static haveno.cli.opts.OptLabel.OPT_ACCOUNT_NAME; import static haveno.cli.opts.OptLabel.OPT_ADDRESS; import static haveno.cli.opts.OptLabel.OPT_AMOUNT; import static haveno.cli.opts.OptLabel.OPT_CURRENCY_CODE; import static haveno.cli.opts.OptLabel.OPT_DIRECTION; import static haveno.cli.opts.OptLabel.OPT_MKT_PRICE_MARGIN; import static haveno.cli.opts.OptLabel.OPT_OFFER_ID; import static haveno.cli.opts.OptLabel.OPT_PASSWORD; import static haveno.cli.opts.OptLabel.OPT_PAYMENT_ACCOUNT_FORM; import static haveno.cli.opts.OptLabel.OPT_PAYMENT_ACCOUNT_ID; import static haveno.cli.opts.OptLabel.OPT_SECURITY_DEPOSIT; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; public class OptionParsersTest { private static final String PASSWORD_OPT = "--" + OPT_PASSWORD + "=" + "xyz"; // canceloffer opt parser tests @Test public void testCancelOfferWithMissingOfferIdOptShouldThrowException() { String[] args = new String[]{ PASSWORD_OPT, canceloffer.name() }; Throwable exception = assertThrows(RuntimeException.class, () -> new CancelOfferOptionParser(args).parse()); assertEquals("no offer id specified", exception.getMessage()); } @Test public void testCancelOfferWithEmptyOfferIdOptShouldThrowException() { String[] args = new String[]{ PASSWORD_OPT, canceloffer.name(), "--" + OPT_OFFER_ID + "=" // missing opt value }; Throwable exception = assertThrows(RuntimeException.class, () -> new CancelOfferOptionParser(args).parse()); assertEquals("no offer id specified", exception.getMessage()); } @Test public void testCancelOfferWithMissingOfferIdValueShouldThrowException() { String[] args = new String[]{ PASSWORD_OPT, canceloffer.name(), "--" + OPT_OFFER_ID // missing equals sign & opt value }; Throwable exception = assertThrows(RuntimeException.class, () -> new CancelOfferOptionParser(args).parse()); assertEquals("offer-id requires an argument", exception.getMessage()); } @Test public void testValidCancelOfferOpts() { String[] args = new String[]{ PASSWORD_OPT, canceloffer.name(), "--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID" }; new CancelOfferOptionParser(args).parse(); } // createoffer (v1) opt parser tests @Test public void testCreateOfferWithMissingPaymentAccountIdOptShouldThrowException() { String[] args = new String[]{ PASSWORD_OPT, createoffer.name(), "--" + OPT_DIRECTION + "=" + "SELL", "--" + OPT_CURRENCY_CODE + "=" + "JPY", "--" + OPT_AMOUNT + "=" + "0.1" }; Throwable exception = assertThrows(RuntimeException.class, () -> new CreateOfferOptionParser(args).parse()); assertEquals("no payment account id specified", exception.getMessage()); } @Test public void testCreateOfferWithEmptyPaymentAccountIdOptShouldThrowException() { String[] args = new String[]{ PASSWORD_OPT, createoffer.name(), "--" + OPT_PAYMENT_ACCOUNT_ID }; Throwable exception = assertThrows(RuntimeException.class, () -> new CreateOfferOptionParser(args).parse()); assertEquals("payment-account-id requires an argument", exception.getMessage()); } @Test public void testCreateOfferWithMissingDirectionOptShouldThrowException() { String[] args = new String[]{ PASSWORD_OPT, createoffer.name(), "--" + OPT_PAYMENT_ACCOUNT_ID + "=" + "abc-payment-acct-id-123" }; Throwable exception = assertThrows(RuntimeException.class, () -> new CreateOfferOptionParser(args).parse()); assertEquals("no direction (buy|sell) specified", exception.getMessage()); } @Test public void testCreateOfferWithMissingDirectionOptValueShouldThrowException() { String[] args = new String[]{ PASSWORD_OPT, createoffer.name(), "--" + OPT_PAYMENT_ACCOUNT_ID + "=" + "abc-payment-acct-id-123", "--" + OPT_DIRECTION + "=" + "" }; Throwable exception = assertThrows(RuntimeException.class, () -> new CreateOfferOptionParser(args).parse()); assertEquals("no direction (buy|sell) specified", exception.getMessage()); } @Test public void testValidCreateOfferOpts() { String[] args = new String[]{ PASSWORD_OPT, createoffer.name(), "--" + OPT_PAYMENT_ACCOUNT_ID + "=" + "abc-payment-acct-id-123", "--" + OPT_DIRECTION + "=" + "BUY", "--" + OPT_CURRENCY_CODE + "=" + "EUR", "--" + OPT_AMOUNT + "=" + "0.125", "--" + OPT_MKT_PRICE_MARGIN + "=" + "3.15", "--" + OPT_SECURITY_DEPOSIT + "=" + "25.0" }; CreateOfferOptionParser parser = new CreateOfferOptionParser(args).parse(); assertEquals("abc-payment-acct-id-123", parser.getPaymentAccountId()); assertEquals("BUY", parser.getDirection()); assertEquals("EUR", parser.getCurrencyCode()); assertEquals("0.125", parser.getAmount()); assertEquals(3.15d, parser.getMktPriceMarginPct()); assertEquals(25.0, parser.getSecurityDepositPct()); } // createpaymentacct opt parser tests @Test public void testCreatePaymentAcctWithMissingPaymentFormOptShouldThrowException() { String[] args = new String[]{ PASSWORD_OPT, createpaymentacct.name() // OPT_PAYMENT_ACCOUNT_FORM }; Throwable exception = assertThrows(RuntimeException.class, () -> new CreatePaymentAcctOptionParser(args).parse()); assertEquals("no path to json payment account form specified", exception.getMessage()); } @Test public void testCreatePaymentAcctWithMissingPaymentFormOptValueShouldThrowException() { String[] args = new String[]{ PASSWORD_OPT, createpaymentacct.name(), "--" + OPT_PAYMENT_ACCOUNT_FORM + "=" }; Throwable exception = assertThrows(RuntimeException.class, () -> new CreatePaymentAcctOptionParser(args).parse()); assertEquals("no path to json payment account form specified", exception.getMessage()); } @Test public void testCreatePaymentAcctWithInvalidPaymentFormOptValueShouldThrowException() { String[] args = new String[]{ PASSWORD_OPT, createpaymentacct.name(), "--" + OPT_PAYMENT_ACCOUNT_FORM + "=" + "/tmp/milkyway/solarsystem/mars" }; Throwable exception = assertThrows(RuntimeException.class, () -> new CreatePaymentAcctOptionParser(args).parse()); if (System.getProperty("os.name").toLowerCase().indexOf("win") >= 0) assertEquals("json payment account form '\\tmp\\milkyway\\solarsystem\\mars' could not be found", exception.getMessage()); else assertEquals("json payment account form '/tmp/milkyway/solarsystem/mars' could not be found", exception.getMessage()); } // createcryptopaymentacct parser tests @Test public void testCreateCryptoCurrencyPaymentAcctWithMissingAcctNameOptShouldThrowException() { String[] args = new String[]{ PASSWORD_OPT, createcryptopaymentacct.name() }; Throwable exception = assertThrows(RuntimeException.class, () -> new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse()); assertEquals("no payment account name specified", exception.getMessage()); } @Test public void testCreateCryptoCurrencyPaymentAcctWithEmptyAcctNameOptShouldThrowException() { String[] args = new String[]{ PASSWORD_OPT, createcryptopaymentacct.name(), "--" + OPT_ACCOUNT_NAME }; Throwable exception = assertThrows(RuntimeException.class, () -> new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse()); assertEquals("account-name requires an argument", exception.getMessage()); } @Test public void testCreateCryptoCurrencyPaymentAcctWithInvalidCurrencyCodeOptShouldThrowException() { String[] args = new String[]{ PASSWORD_OPT, createcryptopaymentacct.name(), "--" + OPT_ACCOUNT_NAME + "=" + "bsq payment account", "--" + OPT_CURRENCY_CODE + "=" + "bsq" }; Throwable exception = assertThrows(RuntimeException.class, () -> new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse()); assertEquals("api does not support bsq payment accounts", exception.getMessage()); } @Test public void testCreateBchPaymentAcct() { var acctName = "bch payment account"; var currencyCode = "bch"; var address = "B1nXyZ46XXX"; // address is validated on server String[] args = new String[]{ PASSWORD_OPT, createcryptopaymentacct.name(), "--" + OPT_ACCOUNT_NAME + "=" + acctName, "--" + OPT_CURRENCY_CODE + "=" + currencyCode, "--" + OPT_ADDRESS + "=" + address }; var parser = new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse(); assertEquals(acctName, parser.getAccountName()); assertEquals(currencyCode, parser.getCurrencyCode()); assertEquals(address, parser.getAddress()); } } ================================================ FILE: cli/src/test/java/haveno/cli/table/AddressCliOutputDiffTest.java ================================================ package haveno.cli.table; import haveno.cli.AbstractCliTest; import haveno.cli.table.builder.TableBuilder; import haveno.proto.grpc.AddressBalanceInfo; import java.util.List; import static haveno.cli.table.builder.TableType.ADDRESS_BALANCE_TBL; import static java.lang.System.err; import static java.util.Collections.singletonList; @SuppressWarnings("unused") public class AddressCliOutputDiffTest extends AbstractCliTest { public static void main(String[] args) { AddressCliOutputDiffTest test = new AddressCliOutputDiffTest(); test.getFundingAddresses(); test.getAddressBalance(); } public AddressCliOutputDiffTest() { super(); } private void getFundingAddresses() { var fundingAddresses = aliceClient.getFundingAddresses(); if (fundingAddresses.size() > 0) { // TableFormat class had been deprecated, then deleted on 17-Feb-2022, but // these diff tests can be useful for testing changes to the current tbl formatting api. // var oldTbl = TableFormat.formatAddressBalanceTbl(fundingAddresses); var newTbl = new TableBuilder(ADDRESS_BALANCE_TBL, fundingAddresses).build().toString(); // printOldTbl(oldTbl); printNewTbl(newTbl); // checkDiffsIgnoreWhitespace(oldTbl, newTbl); } else { err.println("no funding addresses found"); } } private void getAddressBalance() { List addresses = aliceClient.getFundingAddresses(); int numAddresses = addresses.size(); // Check output for last 2 addresses. for (int i = numAddresses - 2; i < addresses.size(); i++) { var addressBalanceInfo = addresses.get(i); getAddressBalance(addressBalanceInfo.getAddress()); } } private void getAddressBalance(String address) { var addressBalance = singletonList(aliceClient.getAddressBalance(address)); // TableFormat class had been deprecated, then deleted on 17-Feb-2022, but these // diff tests can be useful for testing changes to the current tbl formatting api. // var oldTbl = TableFormat.formatAddressBalanceTbl(addressBalance); var newTbl = new TableBuilder(ADDRESS_BALANCE_TBL, addressBalance).build().toString(); // printOldTbl(oldTbl); printNewTbl(newTbl); // checkDiffsIgnoreWhitespace(oldTbl, newTbl); } } ================================================ FILE: cli/src/test/java/haveno/cli/table/GetBalanceCliOutputDiffTest.java ================================================ package haveno.cli.table; import haveno.cli.AbstractCliTest; import haveno.cli.table.builder.TableBuilder; import static haveno.cli.table.builder.TableType.BTC_BALANCE_TBL; @SuppressWarnings("unused") public class GetBalanceCliOutputDiffTest extends AbstractCliTest { public static void main(String[] args) { GetBalanceCliOutputDiffTest test = new GetBalanceCliOutputDiffTest(); test.getBtcBalance(); } public GetBalanceCliOutputDiffTest() { super(); } private void getBtcBalance() { var balance = aliceClient.getBtcBalances(); // TableFormat class had been deprecated, then deleted on 17-Feb-2022, but these // diff tests can be useful for testing changes to the current tbl formatting api. // var oldTbl = TableFormat.formatBtcBalanceInfoTbl(balance); var newTbl = new TableBuilder(BTC_BALANCE_TBL, balance).build().toString(); // printOldTbl(oldTbl); printNewTbl(newTbl); // checkDiffsIgnoreWhitespace(oldTbl, newTbl); } } ================================================ FILE: cli/src/test/java/haveno/cli/table/GetOffersCliOutputDiffTest.java ================================================ package haveno.cli.table; import haveno.cli.AbstractCliTest; import haveno.cli.table.builder.TableBuilder; import haveno.proto.grpc.OfferInfo; import lombok.extern.slf4j.Slf4j; import java.util.List; import static haveno.cli.table.builder.TableType.OFFER_TBL; import static protobuf.OfferDirection.BUY; import static protobuf.OfferDirection.SELL; @SuppressWarnings("unused") @Slf4j public class GetOffersCliOutputDiffTest extends AbstractCliTest { // "My" offers are always Alice's offers. // "Available" offers are always Alice's offers available to Bob. public static void main(String[] args) { GetOffersCliOutputDiffTest test = new GetOffersCliOutputDiffTest(); test.getMyBuyUsdOffers(); test.getMySellUsdOffers(); test.getAvailableBuyUsdOffers(); test.getAvailableSellUsdOffers(); /* // TODO Uncomment when XMR support is added. test.getMyBuyXmrOffers(); test.getMySellXmrOffers(); test.getAvailableBuyXmrOffers(); test.getAvailableSellXmrOffers(); */ test.getMyBuyBsqOffers(); test.getMySellBsqOffers(); test.getAvailableBuyBsqOffers(); test.getAvailableSellBsqOffers(); } public GetOffersCliOutputDiffTest() { super(); } private void getMyBuyUsdOffers() { var myOffers = aliceClient.getMyOffers(BUY.name(), "USD"); printAndCheckDiffs(myOffers, BUY.name(), "USD"); } private void getMySellUsdOffers() { var myOffers = aliceClient.getMyOffers(SELL.name(), "USD"); printAndCheckDiffs(myOffers, SELL.name(), "USD"); } private void getAvailableBuyUsdOffers() { var offers = bobClient.getOffers(BUY.name(), "USD"); printAndCheckDiffs(offers, BUY.name(), "USD"); } private void getAvailableSellUsdOffers() { var offers = bobClient.getOffers(SELL.name(), "USD"); printAndCheckDiffs(offers, SELL.name(), "USD"); } private void getMyBuyXmrOffers() { var myOffers = aliceClient.getMyOffers(BUY.name(), "XMR"); printAndCheckDiffs(myOffers, BUY.name(), "XMR"); } private void getMySellXmrOffers() { var myOffers = aliceClient.getMyOffers(SELL.name(), "XMR"); printAndCheckDiffs(myOffers, SELL.name(), "XMR"); } private void getAvailableBuyXmrOffers() { var offers = bobClient.getOffers(BUY.name(), "XMR"); printAndCheckDiffs(offers, BUY.name(), "XMR"); } private void getAvailableSellXmrOffers() { var offers = bobClient.getOffers(SELL.name(), "XMR"); printAndCheckDiffs(offers, SELL.name(), "XMR"); } private void getMyBuyBsqOffers() { var myOffers = aliceClient.getMyOffers(BUY.name(), "BSQ"); printAndCheckDiffs(myOffers, BUY.name(), "BSQ"); } private void getMySellBsqOffers() { var myOffers = aliceClient.getMyOffers(SELL.name(), "BSQ"); printAndCheckDiffs(myOffers, SELL.name(), "BSQ"); } private void getAvailableBuyBsqOffers() { var offers = bobClient.getOffers(BUY.name(), "BSQ"); printAndCheckDiffs(offers, BUY.name(), "BSQ"); } private void getAvailableSellBsqOffers() { var offers = bobClient.getOffers(SELL.name(), "BSQ"); printAndCheckDiffs(offers, SELL.name(), "BSQ"); } private void printAndCheckDiffs(List offers, String direction, String currencyCode) { if (offers.isEmpty()) { log.warn("No {} {} offers to print.", direction, currencyCode); } else { log.info("Checking for diffs in {} {} offers.", direction, currencyCode); // OfferFormat class had been deprecated, then deleted on 17-Feb-2022, but // these diff tests can be useful for testing changes to the current tbl formatting api. // var oldTbl = OfferFormat.formatOfferTable(offers, currencyCode); var newTbl = new TableBuilder(OFFER_TBL, offers).build().toString(); // printOldTbl(oldTbl); printNewTbl(newTbl); // checkDiffsIgnoreWhitespace(oldTbl, newTbl); } } } ================================================ FILE: cli/src/test/java/haveno/cli/table/GetTradeCliOutputDiffTest.java ================================================ package haveno.cli.table; import haveno.cli.AbstractCliTest; import haveno.cli.GrpcClient; import haveno.cli.table.builder.TableBuilder; import lombok.extern.slf4j.Slf4j; import static haveno.cli.table.builder.TableType.TRADE_DETAIL_TBL; import static java.lang.System.out; @SuppressWarnings("unused") @Slf4j public class GetTradeCliOutputDiffTest extends AbstractCliTest { public static void main(String[] args) { if (args.length == 0) throw new IllegalStateException("Need a single trade-id program argument."); GetTradeCliOutputDiffTest test = new GetTradeCliOutputDiffTest(args[0]); test.getAlicesTrade(); out.println(); test.getBobsTrade(); } private final String tradeId; public GetTradeCliOutputDiffTest(String tradeId) { super(); this.tradeId = tradeId; } private void getAlicesTrade() { getTrade(aliceClient); } private void getBobsTrade() { getTrade(bobClient); } private void getTrade(GrpcClient client) { var trade = client.getTrade(tradeId); // TradeFormat class had been deprecated, then deleted on 17-Feb-2022, but these // diff tests can be useful for testing changes to the current tbl formatting api. // var oldTbl = TradeFormat.format(trade); var newTbl = new TableBuilder(TRADE_DETAIL_TBL, trade).build().toString(); // printOldTbl(oldTbl); printNewTbl(newTbl); // checkDiffsIgnoreWhitespace(oldTbl, newTbl); } } ================================================ FILE: cli/src/test/java/haveno/cli/table/GetTransactionCliOutputDiffTest.java ================================================ package haveno.cli.table; import haveno.cli.AbstractCliTest; import lombok.extern.slf4j.Slf4j; @SuppressWarnings("unused") @Slf4j public class GetTransactionCliOutputDiffTest extends AbstractCliTest { public static void main(String[] args) { if (args.length == 0) throw new IllegalStateException("Need a single transaction-id program argument."); GetTransactionCliOutputDiffTest test = new GetTransactionCliOutputDiffTest(args[0]); } private final String transactionId; public GetTransactionCliOutputDiffTest(String transactionId) { super(); this.transactionId = transactionId; } } ================================================ FILE: cli/src/test/java/haveno/cli/table/PaymentAccountsCliOutputDiffTest.java ================================================ package haveno.cli.table; import haveno.cli.AbstractCliTest; import haveno.cli.table.builder.TableBuilder; import lombok.extern.slf4j.Slf4j; import static haveno.cli.table.builder.TableType.PAYMENT_ACCOUNT_TBL; @SuppressWarnings("unused") @Slf4j public class PaymentAccountsCliOutputDiffTest extends AbstractCliTest { public static void main(String[] args) { PaymentAccountsCliOutputDiffTest test = new PaymentAccountsCliOutputDiffTest(); test.getPaymentAccounts(); } public PaymentAccountsCliOutputDiffTest() { super(); } private void getPaymentAccounts() { var paymentAccounts = aliceClient.getPaymentAccounts(); if (paymentAccounts.size() > 0) { // The formatPaymentAcctTbl method had been deprecated, then deleted on 17-Feb-2022, // but these diff tests can be useful for testing changes to the current tbl formatting api. // var oldTbl = formatPaymentAcctTbl(paymentAccounts); var newTbl = new TableBuilder(PAYMENT_ACCOUNT_TBL, paymentAccounts).build().toString(); // printOldTbl(oldTbl); printNewTbl(newTbl); // checkDiffsIgnoreWhitespace(oldTbl, newTbl); } else { log.warn("no payment accounts found"); } } } ================================================ FILE: common/src/main/java/haveno/common/ClockWatcher.java ================================================ /* * 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 . */ package haveno.common; import com.google.inject.Singleton; import java.util.LinkedList; import java.util.List; import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; // Helps configure listener objects that are run by the `UserThread` each second // and can do per second, per minute and delayed second actions. Also detects when we were in standby, and logs it. @Slf4j @Singleton public class ClockWatcher { public static final int IDLE_TOLERANCE_MS = 20000; public interface Listener { void onSecondTick(); void onMinuteTick(); default void onMissedSecondTick(long missedMs) { } default void onAwakeFromStandby(long missedMs) { } } private Timer timer; private final List listeners = new LinkedList<>(); private long counter = 0; private long lastSecondTick; public ClockWatcher() { } public void start() { if (timer == null) { lastSecondTick = System.currentTimeMillis(); timer = UserThread.runPeriodically(() -> { synchronized (listeners) { listeners.forEach(Listener::onSecondTick); counter++; if (counter >= 60) { counter = 0; listeners.forEach(Listener::onMinuteTick); } long currentTimeMillis = System.currentTimeMillis(); long diff = currentTimeMillis - lastSecondTick; if (diff > 1000) { long missedMs = diff - 1000; listeners.forEach(listener -> listener.onMissedSecondTick(missedMs)); if (missedMs > ClockWatcher.IDLE_TOLERANCE_MS) { log.warn("We have been in standby mode for {} sec", missedMs / 1000); listeners.forEach(listener -> listener.onAwakeFromStandby(missedMs)); } } lastSecondTick = currentTimeMillis; } }, 1, TimeUnit.SECONDS); } } public void stop() { timer.stop(); timer = null; counter = 0; } public void addListener(Listener listener) { synchronized (listeners) { listeners.add(listener); } } public void removeListener(Listener listener) { synchronized (listeners) { listeners.remove(listener); } } } ================================================ FILE: common/src/main/java/haveno/common/Envelope.java ================================================ /* * 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 . */ package haveno.common; /** * Interface for the outside envelope object sent over the network or persisted to disk. */ public interface Envelope extends Proto { } ================================================ FILE: common/src/main/java/haveno/common/FrameRateTimer.java ================================================ /* * 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 . */ package haveno.common; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.Duration; import java.util.UUID; /** * We simulate a global frame rate timer similar to FXTimer to avoid creation of threads for each timer call. * Used only in headless apps like the seed node. */ public class FrameRateTimer implements Timer, Runnable { private final Logger log = LoggerFactory.getLogger(FrameRateTimer.class); private long interval; private Runnable runnable; private long startTs; private boolean isPeriodically; private final String uid = UUID.randomUUID().toString(); private volatile boolean stopped; public FrameRateTimer() { } @Override public void run() { if (!stopped) { try { long currentTimeMillis = System.currentTimeMillis(); if ((currentTimeMillis - startTs) >= interval) { runnable.run(); if (isPeriodically) startTs = currentTimeMillis; else stop(); } } catch (Throwable t) { log.error("exception in FrameRateTimer", t); stop(); throw t; } } } @Override public Timer runLater(Duration delay, Runnable runnable) { this.interval = delay.toMillis(); this.runnable = runnable; startTs = System.currentTimeMillis(); MasterTimer.addListener(this); return this; } @Override public Timer runPeriodically(Duration interval, Runnable runnable) { this.interval = interval.toMillis(); isPeriodically = true; this.runnable = runnable; startTs = System.currentTimeMillis(); MasterTimer.addListener(this); return this; } @Override public void stop() { stopped = true; MasterTimer.removeListener(this); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof FrameRateTimer)) return false; FrameRateTimer that = (FrameRateTimer) o; return !(uid != null ? !uid.equals(that.uid) : that.uid != null); } @Override public int hashCode() { return uid != null ? uid.hashCode() : 0; } } ================================================ FILE: common/src/main/java/haveno/common/HavenoException.java ================================================ /* * 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 . */ package haveno.common; public class HavenoException extends RuntimeException { public HavenoException(Throwable cause) { super(cause); } public HavenoException(String format, Object... args) { super(String.format(format, args)); } public HavenoException(Throwable cause, String format, Object... args) { super(String.format(format, args), cause); } } ================================================ FILE: common/src/main/java/haveno/common/MasterTimer.java ================================================ /* * 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 . */ package haveno.common; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Set; import java.util.TimerTask; import java.util.concurrent.CopyOnWriteArraySet; // Runs all listener objects periodically in a short interval. public class MasterTimer { private final static Logger log = LoggerFactory.getLogger(MasterTimer.class); private static final java.util.Timer timer = new java.util.Timer(); // frame rate of 60 fps is about 16 ms but we don't need such a short interval, 100 ms should be good enough public static final long FRAME_INTERVAL_MS = 100; static { timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { UserThread.execute(() -> listeners.forEach(Runnable::run)); } }, FRAME_INTERVAL_MS, FRAME_INTERVAL_MS); } private static final Set listeners = new CopyOnWriteArraySet<>(); public static void addListener(Runnable runnable) { listeners.add(runnable); } public static void removeListener(Runnable runnable) { listeners.remove(runnable); } } ================================================ FILE: common/src/main/java/haveno/common/Payload.java ================================================ /* * 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 . */ package haveno.common; /** * Interface for objects used inside an Envelope or other Payloads. */ public interface Payload extends Proto { } ================================================ FILE: common/src/main/java/haveno/common/Proto.java ================================================ /* * 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 . */ package haveno.common; import com.google.protobuf.Message; /** * Base interface for Envelope and Payload. */ public interface Proto { Message toProtoMessage(); } ================================================ FILE: common/src/main/java/haveno/common/ThreadUtils.java ================================================ /* * 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 . */ package haveno.common; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; @Slf4j public class ThreadUtils { private static final Map EXECUTORS = new HashMap<>(); private static final Map THREADS = new HashMap<>(); private static final int POOL_SIZE = 10; private static final ExecutorService POOL = Executors.newFixedThreadPool(POOL_SIZE); /** * Execute the given command in a thread with the given id. * * @param command the command to execute * @param threadId the thread id */ public static Future execute(Runnable command, String threadId) { synchronized (EXECUTORS) { if (!EXECUTORS.containsKey(threadId)) EXECUTORS.put(threadId, Executors.newFixedThreadPool(1)); ExecutorService executor = EXECUTORS.get(threadId); if (executor.isShutdown()) throw new IllegalStateException("Cannot execute thread because it's shut down: " + threadId); return EXECUTORS.get(threadId).submit(() -> { synchronized (THREADS) { THREADS.put(threadId, Thread.currentThread()); } Thread.currentThread().setName(threadId); command.run(); }); } } /** * Awaits execution of the given command, but does not throw its exception. * * @param command the command to execute * @param threadId the thread id */ public static void await(Runnable command, String threadId) { try { execute(command, threadId).get(); } catch (Exception e) { throw new RuntimeException(e); } } public static void shutDown(String threadId) { shutDown(threadId, null); } public static void shutDown(String threadId, Long timeoutMs) { if (timeoutMs == null) timeoutMs = Long.MAX_VALUE; ExecutorService pool = null; synchronized (EXECUTORS) { pool = EXECUTORS.get(threadId); if (pool == null) return; // thread not found if (pool.isShutdown()) return; // already shut down pool.shutdown(); } try { if (!pool.awaitTermination(timeoutMs, TimeUnit.MILLISECONDS)) pool.shutdownNow(); } catch (InterruptedException e) { pool.shutdownNow(); throw new RuntimeException(e); } } /** * Reset the thread with the given id so it's ready for use. Does not shut it down. * * @param threadId the thread id */ public static void reset(String threadId) { remove(threadId); } public static void remove(String threadId) { synchronized (EXECUTORS) { EXECUTORS.remove(threadId); } synchronized (THREADS) { THREADS.remove(threadId); } } public static boolean isShutDown(String threadId) { synchronized (EXECUTORS) { if (!EXECUTORS.containsKey(threadId)) return false; ExecutorService executor = EXECUTORS.get(threadId); return executor.isShutdown(); } } // TODO: consolidate and cleanup apis public static Future submitToPool(Runnable task) { return submitToPool(Arrays.asList(task)).get(0); } public static List> submitToPool(List tasks) { List> futures = new ArrayList<>(); for (Runnable task : tasks) futures.add(POOL.submit(task)); return futures; } public static Future awaitTask(Runnable task) { return awaitTask(task, null); } public static Future awaitTask(Runnable task, Long timeoutMs) { return awaitTasks(Arrays.asList(task), 1, timeoutMs).get(0); } public static List> awaitTasks(Collection tasks) { return awaitTasks(tasks, tasks.size()); } public static List> awaitTasks(Collection tasks, int maxConcurrency) { return awaitTasks(tasks, maxConcurrency, null); } public static List> awaitTasks(Collection tasks, int maxConcurrency, Long timeoutMs) { if (timeoutMs == null) timeoutMs = Long.MAX_VALUE; if (tasks.isEmpty()) return new ArrayList<>(); ExecutorService executorService = Executors.newFixedThreadPool(tasks.size()); try { List> futures = new ArrayList<>(); for (Runnable task : tasks) futures.add(executorService.submit(task, null)); for (Future future : futures) future.get(timeoutMs, TimeUnit.MILLISECONDS); return futures; } catch (Exception e) { throw new RuntimeException(e); } finally { executorService.shutdownNow(); } } private static boolean isCurrentThread(Thread thread, String threadId) { synchronized (THREADS) { if (!THREADS.containsKey(threadId)) return false; return thread == THREADS.get(threadId); } } } ================================================ FILE: common/src/main/java/haveno/common/Timer.java ================================================ /* * 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 . */ package haveno.common; import java.time.Duration; public interface Timer { Timer runLater(java.time.Duration delay, Runnable action); Timer runPeriodically(Duration interval, Runnable runnable); void stop(); } ================================================ FILE: common/src/main/java/haveno/common/UserThread.java ================================================ /* * 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 . */ package haveno.common; import com.google.common.util.concurrent.MoreExecutors; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import java.lang.reflect.InvocationTargetException; import java.time.Duration; import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; /** * Defines which thread is used as user thread. The user thread is the the main thread in the single threaded context. * For JavaFX it is usually the Platform::RunLater executor, for a headless application it is any single threaded * executor. * Additionally sets a timer class so JavaFX and headless applications can set different timers (UITimer for JavaFX * otherwise we use the default FrameRateTimer). *

    * Provides also methods for delayed and periodic executions. */ @Slf4j public class UserThread { private static Class timerClass; @Getter @Setter private static Executor executor; private static Thread USER_THREAD; public static void setTimerClass(Class timerClass) { UserThread.timerClass = timerClass; } static { // If not defined we use same thread as caller thread executor = MoreExecutors.directExecutor(); timerClass = FrameRateTimer.class; } public static void execute(Runnable command) { executor.execute(() -> { synchronized (executor) { USER_THREAD = Thread.currentThread(); command.run(); } }); } public static void await(Runnable command) { if (isUserThread(Thread.currentThread())) { command.run(); } else { CountDownLatch latch = new CountDownLatch(1); execute(() -> { try { command.run(); } catch (Exception e) { throw e; } finally { latch.countDown(); } }); try { latch.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } public static boolean isUserThread(Thread thread) { return thread == USER_THREAD; } // Prefer FxTimer if a delay is needed in a JavaFx class (gui module) public static Timer runAfterRandomDelay(Runnable runnable, long minDelayInSec, long maxDelayInSec) { return UserThread.runAfterRandomDelay(runnable, minDelayInSec, maxDelayInSec, TimeUnit.SECONDS); } @SuppressWarnings("WeakerAccess") public static Timer runAfterRandomDelay(Runnable runnable, long minDelay, long maxDelay, TimeUnit timeUnit) { return UserThread.runAfter(runnable, new Random().nextInt((int) (maxDelay - minDelay)) + minDelay, timeUnit); } public static Timer runAfter(Runnable runnable, long delayInSec) { return UserThread.runAfter(runnable, delayInSec, TimeUnit.SECONDS); } public static Timer runAfter(Runnable runnable, long delay, TimeUnit timeUnit) { return getTimer().runLater(Duration.ofMillis(timeUnit.toMillis(delay)), () -> execute(runnable)); } public static Timer runPeriodically(Runnable runnable, long intervalInSec) { return UserThread.runPeriodically(runnable, intervalInSec, TimeUnit.SECONDS); } public static Timer runPeriodically(Runnable runnable, long interval, TimeUnit timeUnit) { return getTimer().runPeriodically(Duration.ofMillis(timeUnit.toMillis(interval)), () -> execute(runnable)); } private static Timer getTimer() { try { return timerClass.getDeclaredConstructor().newInstance(); } catch (InstantiationException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { String message = "Could not instantiate timer bsTimerClass=" + timerClass; log.error(message, e); throw new RuntimeException(message); } } } ================================================ FILE: common/src/main/java/haveno/common/app/AppModule.java ================================================ /* * 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 . */ package haveno.common.app; import com.google.inject.AbstractModule; import com.google.inject.Injector; import haveno.common.config.Config; import java.util.ArrayList; import java.util.List; public abstract class AppModule extends AbstractModule { protected final Config config; private final List modules = new ArrayList<>(); protected AppModule(Config config) { this.config = config; } protected void install(AppModule module) { super.install(module); modules.add(module); } /** * Close any instances this module is responsible for and recursively close any * sub-modules installed via {@link #install(AppModule)}. This method * must be called manually, e.g. at the end of a main() method or in the stop() method * of a JavaFX Application; alternatively it may be registered as a JVM shutdown hook. * * @param injector the Injector originally initialized with this module * @see #doClose(com.google.inject.Injector) */ public final void close(Injector injector) { modules.forEach(module -> module.close(injector)); doClose(injector); } /** * Actually perform closing of any instances this module is responsible for. Called by * {@link #close(Injector)}. * * @param injector the Injector originally initialized with this module */ @SuppressWarnings({"WeakerAccess", "EmptyMethod", "UnusedParameters"}) protected void doClose(Injector injector) { } } ================================================ FILE: common/src/main/java/haveno/common/app/AsciiLogo.java ================================================ /* * 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 . */ package haveno.common.app; import lombok.extern.slf4j.Slf4j; @Slf4j public class AsciiLogo { public static void showAsciiLogo() { String ls = System.lineSeparator(); log.info(ls + ls + " " + ls + " " + ls + " " + ls + " 0X " + ls + " OOdolcck " + ls + " KXKNN0occcccccck: :Kxxk0d " + ls + " klccccccccccccccccck0xcccccccxK' " + ls + " xccccccccccccccclOKKOocccccccccclxK " + ls + " .xccccccccccccccclWMMMMMd:::::::::ccco " + ls + " 'dccccccc:::cccccclWMMMMMo:::::::::::cc; " + ls + " ,occccc:::::::::::::cxO0kl:::::::::::::cd " + ls + " ;occccc:::::cddddddc;;;;;;;;:ddddddl:::::coldOK " + ls + " :occccc::::::xMMMMMMo,,,,,,,,cMMMMMMk::::::cccccoOc " + ls + " llccccc:::::;;dMMMMMMo,,,,,,,,cMMMMMMk:::::::cccccc, " + ls + " 'cccccc::::;,,dMMMMMMl'''''',,cMMMMMMk::::::::ccccc. " + ls + " .cccccc::::,,,dMMMMMMo'''''',,cMMMMMMk::::::::ccccc " + ls + " :ccccc:::::;;dMMMMMM0xxxxxxxxOMMMMMMk::::::::ccc' " + ls + " ;ccccc:::::::xMMMMMMMMMMMMMMMMMMMMMMk::::::ccccco " + ls + " 'ccccc:::::::xMMMMMMMMMMMMMMMMMMMMMMk:::::ccccccco " + ls + " .ccccccc:::::xMMMMMMd::::::::oMMMMMMk:::::ccccccc " + ls + " :cccccccc:::xMMMMMMo,,,,,,,,cMMMMMMk:::::cccccc " + ls + " cccccccccc:xMMMMMMo,,,,,,,,cMMMMMMk::::cccccc " + ls + " :ccccccccccxMMMMMMo,,,,,,,,cMMMMMMO:cccccccc " + ls + " ccccccccccxMMMMMMd;;;;;;;:lMMMMMMOcccccccc " + ls + " ccccccclooooooc::::::::cddddddlcccccc: " + ls + " .ccccc::::::::::::::ccccccccccccccc " + ls + " :cccc:::::::::::ccccccccccccc " + ls + " .cccc:::::::ccccccccccc, " + ls + " 'ccccccccccccc. " + ls + " ;ccccc: " + ls + " " + ls + " " + ls + " " + ls + " " + ls + " .XXX. .XXX. .XXXk dXX0 ;XXX.KXXXXXX, xXX0 :XX0 ,XK000KK " + ls + " .ccc. .ccc. xccccc ;cco .occ. ccccccc. :cccdo ;cc: oxlccccccco0. " + ls + " .ccc. .ccc. dcc'ccl. :ccl dcc. ccc' :cccccO. ;cc: lccc 'ccd " + ls + " .ccclllllccc. ccc: .ccx .ccl,dcc' cccllll. :cc, ccox;cc: :cc: .ccc. " + ls + " .ccc ccc. 'lccl0kcccd .cclcc, ccc. :cc; .ccocc: .ccco kcc: " + ls + " .ccc. .ccc. dcc. :ccl .ccc; cccd000' :cc; cccc: cccxO0kocc, " + ls + " :ccc. " + ls + " " + ls + " " + ls + " " + ls + " " + ls + ls + ls); } } ================================================ FILE: common/src/main/java/haveno/common/app/Capabilities.java ================================================ /* * 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 . */ package haveno.common.app; import com.google.common.base.Joiner; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; /** * hold a set of capabilities and offers appropriate comparison methods. * * @author Florian Reimair */ @EqualsAndHashCode @Slf4j public class Capabilities { /** * The global set of capabilities, i.e. the capabilities if the local app. */ public static final Capabilities app = new Capabilities(); // Defines which most recent capability any node need to support. // This helps to clean network from very old inactive but still running nodes. private static final Capability MANDATORY_CAPABILITY = Capability.TRADE_STATISTICS_3; protected final Set capabilities = new HashSet<>(); public Capabilities(Capability... capabilities) { this(Arrays.asList(capabilities)); } public Capabilities(Capabilities capabilities) { this(capabilities.capabilities); } public Capabilities(Collection capabilities) { synchronized (capabilities) { synchronized (this.capabilities) { this.capabilities.addAll(capabilities); } } } public void set(Capability... capabilities) { set(Arrays.asList(capabilities)); } public void set(Capabilities capabilities) { set(capabilities.capabilities); } public void set(Collection capabilities) { synchronized (capabilities) { synchronized (this.capabilities) { this.capabilities.clear(); this.capabilities.addAll(capabilities); } } } public void addAll(Capability... capabilities) { synchronized (this.capabilities) { this.capabilities.addAll(Arrays.asList(capabilities)); } } public void addAll(Capabilities capabilities) { if (capabilities != null) { synchronized (capabilities.capabilities) { synchronized (this.capabilities) { this.capabilities.addAll(capabilities.capabilities); } } } } public boolean containsAll(final Set requiredItems) { synchronized(requiredItems) { synchronized (this.capabilities) { return capabilities.containsAll(requiredItems); } } } public boolean containsAll(final Capabilities capabilities) { return containsAll(capabilities.capabilities); } public boolean containsAll(Capability... capabilities) { synchronized (this.capabilities) { return this.capabilities.containsAll(Arrays.asList(capabilities)); } } public boolean contains(Capability capability) { synchronized (this.capabilities) { return this.capabilities.contains(capability); } } public boolean isEmpty() { synchronized (this.capabilities) { return capabilities.isEmpty(); } } /** * helper for protobuffer stuff * * @param capabilities * @return int list of Capability ordinals */ public static List toIntList(Capabilities capabilities) { synchronized (capabilities.capabilities) { return capabilities.capabilities.stream().map(Enum::ordinal).sorted().collect(Collectors.toList()); } } /** * helper for protobuffer stuff * * @param capabilities a list of Capability ordinals * @return a {@link Capabilities} object */ public static Capabilities fromIntList(List capabilities) { synchronized (capabilities) { return new Capabilities(capabilities.stream() .filter(integer -> integer < Capability.values().length) .filter(integer -> integer >= 0) .map(integer -> Capability.values()[integer]) .collect(Collectors.toSet())); } } /** * * @param list Comma separated list of Capability ordinals. * @return Capabilities */ public static Capabilities fromStringList(String list) { if (list == null || list.isEmpty()) return new Capabilities(); List entries = List.of(list.replace(" ", "").split(",")); List capabilitiesList = entries.stream() .map(c -> { try { return Integer.parseInt(c); } catch (Throwable e) { return null; } }) .filter(Objects::nonNull) .collect(Collectors.toList()); return Capabilities.fromIntList(capabilitiesList); } /** * @return Converts capabilities to list of ordinals as comma separated strings */ public String toStringList() { return Joiner.on(", ").join(Capabilities.toIntList(this)); } public static boolean hasMandatoryCapability(Capabilities capabilities) { return hasMandatoryCapability(capabilities, MANDATORY_CAPABILITY); } public static boolean hasMandatoryCapability(Capabilities capabilities, Capability mandatoryCapability) { synchronized (capabilities.capabilities) { return capabilities.capabilities.stream().anyMatch(c -> c == mandatoryCapability); } } @Override public String toString() { return Arrays.toString(Capabilities.toIntList(this).toArray()); } public String prettyPrint() { synchronized (capabilities) { return capabilities.stream() .sorted(Comparator.comparingInt(Enum::ordinal)) .map(e -> e.name() + " [" + e.ordinal() + "]") .collect(Collectors.joining(", ")); } } public int size() { return capabilities.size(); } // We return true if our capabilities have less capabilities than the parameter value public boolean hasLess(Capabilities other) { return findHighestCapability(this) < findHighestCapability(other); } // We use the sum of all capabilities. Alternatively we could use the highest entry. // Neither would support removal of past capabilities, a use case we never had so far and which might have // backward compatibility issues, so we should treat capabilities as an append-only data structure. public int findHighestCapability(Capabilities capabilities) { synchronized (capabilities.capabilities) { return (int) capabilities.capabilities.stream() .mapToLong(e -> (long) e.ordinal()) .sum(); } } } ================================================ FILE: common/src/main/java/haveno/common/app/Capability.java ================================================ /* * 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 . */ package haveno.common.app; // We can define here special features the client is supporting. // Useful for updates to new versions where a new data type would break backwards compatibility or to // limit a node to certain behaviour and roles like the seed nodes. // We don't use the Enum in any serialized data, as changes in the enum would break backwards compatibility. // We use the ordinal integer instead. // Sequence in the enum must not be changed (append only). public enum Capability { @Deprecated TRADE_STATISTICS, // Not required anymore as no old clients out there not having that support @Deprecated TRADE_STATISTICS_2, // Not required anymore as no old clients out there not having that support @Deprecated ACCOUNT_AGE_WITNESS, // Not required anymore as no old clients out there not having that support SEED_NODE, // Node is a seed node @Deprecated DAO_FULL_NODE, // DAO full node can deliver BSQ blocks @Deprecated PROPOSAL, // Not required anymore as no old clients out there not having that support @Deprecated BLIND_VOTE, // Not required anymore as no old clients out there not having that support @Deprecated ACK_MSG, // Not required anymore as no old clients out there not having that support @Deprecated RECEIVE_BSQ_BLOCK, // Signaling that node which wants to receive BSQ blocks (DAO lite node) @Deprecated DAO_STATE, // Not required anymore as no old clients out there not having that support @Deprecated BUNDLE_OF_ENVELOPES, // Supports bundling of messages if many messages are sent in short interval SIGNED_ACCOUNT_AGE_WITNESS, // Supports the signed account age witness feature MEDIATION, // Supports mediation feature REFUND_AGENT, // Supports refund agents TRADE_STATISTICS_HASH_UPDATE, // We changed the hash method in 1.2.0 and that requires update to 1.2.2 for handling it correctly, otherwise the seed nodes have to process too much data. NO_ADDRESS_PRE_FIX, // At 1.4.0 we removed the prefix filter for mailbox messages. If a peer has that capability we do not sent the prefix. TRADE_STATISTICS_3 // We used a new reduced trade statistics model from v1.4.0 on } ================================================ FILE: common/src/main/java/haveno/common/app/DevEnv.java ================================================ /* * 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 . */ package haveno.common.app; import haveno.common.config.Config; import lombok.extern.slf4j.Slf4j; @Slf4j public class DevEnv { // The UI got set the private dev key so the developer does not need to do anything and can test those features. // Features: Arbitration registration (alt+R at account), Alert/Update (alt+m), private message to a // peer (click user icon and alt+r), filter/block offers by various data like offer ID (cmd + f). // The user can set a program argument to ignore all of those privileged network_messages. They are intended for // emergency cases only (beside update message and arbitrator registration). public static final String DEV_PRIVILEGE_PUB_KEY = "027a381b5333a56e1cc3d90d3a7d07f26509adf7029ed06fc997c656621f8da1ee"; public static final String DEV_PRIVILEGE_PRIV_KEY = "6ac43ea1df2a290c1c8391736aa42e4339c5cb4f110ff0257a13b63211977b7a"; public static void setup(Config config) { DevEnv.setDevMode(config.useDevMode); } // If set to true we ignore several UI behavior like confirmation popups as well dummy accounts are created and // offers are filled with default values. Intended to make dev testing faster. private static boolean devMode = false; public static boolean isDevMode() { return devMode; } public static void setDevMode(boolean devMode) { DevEnv.devMode = devMode; } public static void logErrorAndThrowIfDevMode(String msg) { log.error(msg); if (devMode) throw new RuntimeException(msg); } } ================================================ FILE: common/src/main/java/haveno/common/app/HasCapabilities.java ================================================ /* * 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 . */ package haveno.common.app; /** * Holds a set of {@link Capabilities}. * * @author Florian Reimair */ public interface HasCapabilities { Capabilities getCapabilities(); } ================================================ FILE: common/src/main/java/haveno/common/app/Log.java ================================================ /* * 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 . */ package haveno.common.app; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.encoder.PatternLayoutEncoder; import ch.qos.logback.classic.filter.ThresholdFilter; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.FileAppender; import ch.qos.logback.core.rolling.FixedWindowRollingPolicy; import ch.qos.logback.core.rolling.RollingFileAppender; import ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy; import ch.qos.logback.core.util.FileSize; import org.slf4j.LoggerFactory; public class Log { private static Logger logbackLogger; public static void setLevel(Level logLevel) { logbackLogger.setLevel(logLevel); } public static void setup(String fileName) { if (logbackLogger != null) { stopFileLogging(); } LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); RollingFileAppender appender = new RollingFileAppender<>(); appender.setContext(loggerContext); appender.setFile(fileName + ".log"); FixedWindowRollingPolicy rollingPolicy = new FixedWindowRollingPolicy(); rollingPolicy.setContext(loggerContext); rollingPolicy.setParent(appender); rollingPolicy.setFileNamePattern(fileName + "_%i.log"); rollingPolicy.setMinIndex(1); rollingPolicy.setMaxIndex(20); rollingPolicy.start(); SizeBasedTriggeringPolicy triggeringPolicy = new SizeBasedTriggeringPolicy<>(); triggeringPolicy.setMaxFileSize(FileSize.valueOf("10MB")); triggeringPolicy.setContext(loggerContext); triggeringPolicy.start(); PatternLayoutEncoder encoder = new PatternLayoutEncoder(); encoder.setContext(loggerContext); encoder.setPattern("%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{15}: %msg%n"); encoder.start(); appender.setEncoder(encoder); appender.setRollingPolicy(rollingPolicy); appender.setTriggeringPolicy(triggeringPolicy); appender.start(); // log errors in separate file PatternLayoutEncoder errorEncoder = new PatternLayoutEncoder(); errorEncoder.setContext(loggerContext); errorEncoder.setPattern("%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger: %msg%n%ex"); errorEncoder.start(); RollingFileAppender errorAppender = new RollingFileAppender<>(); errorAppender.setEncoder(errorEncoder); errorAppender.setName("Error"); errorAppender.setContext(loggerContext); errorAppender.setFile(fileName + "_error.log"); FixedWindowRollingPolicy errorRollingPolicy = new FixedWindowRollingPolicy(); errorRollingPolicy.setContext(loggerContext); errorRollingPolicy.setParent(errorAppender); errorRollingPolicy.setFileNamePattern(fileName + "_error_%i.log"); errorRollingPolicy.setMinIndex(1); errorRollingPolicy.setMaxIndex(20); errorRollingPolicy.start(); SizeBasedTriggeringPolicy errorTriggeringPolicy = new SizeBasedTriggeringPolicy<>(); errorTriggeringPolicy.setMaxFileSize(FileSize.valueOf("10MB")); errorTriggeringPolicy.start(); ThresholdFilter thresholdFilter = new ThresholdFilter(); thresholdFilter.setLevel("WARN"); thresholdFilter.start(); errorAppender.setRollingPolicy(errorRollingPolicy); errorAppender.setTriggeringPolicy(errorTriggeringPolicy); errorAppender.addFilter(thresholdFilter); errorAppender.start(); logbackLogger = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); logbackLogger.addAppender(errorAppender); logbackLogger.addAppender(appender); logbackLogger.setLevel(Level.INFO); } public static void setCustomLogLevel(String pattern, Level logLevel) { ((Logger) LoggerFactory.getLogger(pattern)).setLevel(logLevel); } public static void stopFileLogging() { var context = logbackLogger.getLoggerContext(); for (Logger logger : context.getLoggerList()) { var iteratorForAppenders = logger.iteratorForAppenders(); while (iteratorForAppenders.hasNext()) { var appender = iteratorForAppenders.next(); if (appender instanceof FileAppender) { logger.detachAppender(appender); appender.stop(); System.out.println("Released: " + ((FileAppender) appender).getFile()); } } } } } ================================================ FILE: common/src/main/java/haveno/common/app/LogHighlighter.java ================================================ /** * Logback: the reliable, generic, fast and flexible logging framework. * Copyright (C) 1999-2015, QOS.ch. All rights reserved. * * This program and the accompanying materials are dual-licensed under * either the terms of the Eclipse Public License v1.0 as published by * the Eclipse Foundation * * or (per the licensee's choosing) * * under the terms of the GNU Lesser General Public License version 2.1 * as published by the Free Software Foundation. */ /* Derived from https://logback.qos.ch/xref/ch/qos/logback/classic/pattern/color/HighlightingCompositeConverter.html */ package haveno.common.app; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.spi.ILoggingEvent; import static ch.qos.logback.core.pattern.color.ANSIConstants.BOLD; import static ch.qos.logback.core.pattern.color.ANSIConstants.DEFAULT_FG; import static ch.qos.logback.core.pattern.color.ANSIConstants.RED_FG; import ch.qos.logback.core.pattern.color.ForegroundCompositeConverterBase; /** * Highlights inner-text depending on the level, in bold red for events of level * ERROR, in red for WARN, in the default color for INFO, and in the default color for other * levels. */ public class LogHighlighter extends ForegroundCompositeConverterBase { @Override protected String getForegroundColorCode(ILoggingEvent event) { Level level = event.getLevel(); switch (level.toInt()) { case Level.ERROR_INT: return BOLD + RED_FG; case Level.WARN_INT: return RED_FG; case Level.INFO_INT: return DEFAULT_FG; default: return DEFAULT_FG; } } } ================================================ FILE: common/src/main/java/haveno/common/app/Version.java ================================================ /* * 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 . */ package haveno.common.app; import lombok.extern.slf4j.Slf4j; import java.util.Arrays; import java.util.List; import static com.google.common.base.Preconditions.checkArgument; @Slf4j public class Version { // The application version. // We use semantic versioning with major, minor and patch. // Optionally supports a fourth digit for fork-specific build versions (e.g. 1.2.3.0). public static final String VERSION = "1.2.3.1"; /** * 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 HISTORICAL_RESOURCE_FILE_VERSION_TAGS = Arrays.asList("0.0.1"); public static int getMajorVersion(String version) { return getSubVersion(version, 0); } public static int getMinorVersion(String version) { return getSubVersion(version, 1); } public static int getPatchVersion(String version) { return getSubVersion(version, 2); } public static int getBuildVersion(String version) { return getSubVersion(version, 3); } public static boolean isNewVersion(String newVersion) { return isNewVersion(newVersion, VERSION); } public static boolean isNewVersion(String newVersion, String currentVersion) { if (newVersion.equals(currentVersion)) return false; else if (getMajorVersion(newVersion) > getMajorVersion(currentVersion)) return true; else if (getMajorVersion(newVersion) < getMajorVersion(currentVersion)) return false; else if (getMinorVersion(newVersion) > getMinorVersion(currentVersion)) return true; else if (getMinorVersion(newVersion) < getMinorVersion(currentVersion)) return false; else if (getPatchVersion(newVersion) > getPatchVersion(currentVersion)) return true; else if (getPatchVersion(newVersion) < getPatchVersion(currentVersion)) return false; else if (getBuildVersion(newVersion) > getBuildVersion(currentVersion)) return true; else if (getBuildVersion(newVersion) < getBuildVersion(currentVersion)) return false; else return false; } public static int compare(String version1, String version2) { if (version1.equals(version2)) return 0; else if (getMajorVersion(version1) > getMajorVersion(version2)) return 1; else if (getMajorVersion(version1) < getMajorVersion(version2)) return -1; else if (getMinorVersion(version1) > getMinorVersion(version2)) return 1; else if (getMinorVersion(version1) < getMinorVersion(version2)) return -1; else if (getPatchVersion(version1) > getPatchVersion(version2)) return 1; else if (getPatchVersion(version1) < getPatchVersion(version2)) return -1; else if (getBuildVersion(version1) > getBuildVersion(version2)) return 1; else if (getBuildVersion(version1) < getBuildVersion(version2)) return -1; else return 0; } private static int getSubVersion(String version, int index) { final String[] split = version.split("\\."); checkArgument(split.length == 3 || split.length == 4, "Version number must be in semantic version format (contain 2 '.') with optional fourth digit for fork-specific builds. Version=" + version); if (split.length == 3 && index == 3) return 0; // no build version specified return Integer.parseInt(split[index]); } // The version no. for the objects sent over the network. A change will break the serialization of old objects. // If objects are used for both network and database the network version is applied. public static final String P2P_NETWORK_VERSION = "A"; // The version no. of the serialized data stored to disc. A change will break the serialization of old objects. // VERSION = 0.5.0 -> LOCAL_DB_VERSION = 1 public static final int LOCAL_DB_VERSION = 1; // 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. // the Haveno app. // Version = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1 // Version = 1.0.19 -> TRADE_PROTOCOL_VERSION = 2 // Version = 1.2.0 -> TRADE_PROTOCOL_VERSION = 3 public static final int TRADE_PROTOCOL_VERSION = 3; private static String p2pMessageVersion; public static String getP2PMessageVersion() { return p2pMessageVersion; } // The version for the crypto network (XMR_Mainnet = 0, XMR_LOCAL = 1, XMR_Regtest = 2, ...) private static int BASE_CURRENCY_NETWORK; public static void setBaseCryptoNetworkId(int baseCryptoNetworkId) { BASE_CURRENCY_NETWORK = baseCryptoNetworkId; // CRYPTO_NETWORK_ID is ordinal of enum. We use for changes at NETWORK_PROTOCOL_VERSION a multiplication with 10 // to not mix up networks: if (BASE_CURRENCY_NETWORK == 0) p2pMessageVersion = "0" + P2P_NETWORK_VERSION; if (BASE_CURRENCY_NETWORK == 1) p2pMessageVersion = "1" + P2P_NETWORK_VERSION; if (BASE_CURRENCY_NETWORK == 2) p2pMessageVersion = "2" + P2P_NETWORK_VERSION; } public static int getBaseCurrencyNetwork() { return BASE_CURRENCY_NETWORK; } public static void printVersion() { log.info("Version{" + "VERSION=" + VERSION + ", P2P_NETWORK_VERSION=" + P2P_NETWORK_VERSION + ", LOCAL_DB_VERSION=" + LOCAL_DB_VERSION + ", TRADE_PROTOCOL_VERSION=" + TRADE_PROTOCOL_VERSION + ", BASE_CURRENCY_NETWORK=" + BASE_CURRENCY_NETWORK + ", getP2PNetworkId()=" + getP2PMessageVersion() + '}'); } public static final byte COMPENSATION_REQUEST = (byte) 0x01; public static final byte REIMBURSEMENT_REQUEST = (byte) 0x01; public static final byte PROPOSAL = (byte) 0x01; public static final byte BLIND_VOTE = (byte) 0x01; public static final byte VOTE_REVEAL = (byte) 0x01; public static final byte LOCKUP = (byte) 0x01; public static final byte ASSET_LISTING_FEE = (byte) 0x01; public static final byte PROOF_OF_BURN = (byte) 0x01; } ================================================ FILE: common/src/main/java/haveno/common/config/BaseCurrencyNetwork.java ================================================ /* * 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 . */ package haveno.common.config; import lombok.Getter; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.params.MainNetParams; import org.bitcoinj.params.RegTestParams; import org.bitcoinj.utils.MonetaryFormat; public enum BaseCurrencyNetwork { XMR_MAINNET(new XmrMainNetParams(), "XMR", "MAINNET", "Monero"), // TODO (woodser): network params are part of bitcoinj and shouldn't be needed. only used to get MonetaryFormat? replace with MonetaryFormat if so XMR_STAGENET(new XmrStageNetParams(), "XMR", "STAGENET", "Monero"), XMR_LOCAL(new XmrTestNetParams(), "XMR", "TESTNET", "Monero"); @Getter private final NetworkParameters parameters; @Getter private final String currencyCode; @Getter private final String network; @Getter private final String currencyName; BaseCurrencyNetwork(NetworkParameters parameters, String currencyCode, String network, String currencyName) { this.parameters = parameters; this.currencyCode = currencyCode; this.network = network; this.currencyName = currencyName; } public boolean isMainnet() { return "XMR_MAINNET".equals(name()); } public boolean isTestnet() { return "XMR_LOCAL".equals(name()); } public boolean isStagenet() { return "XMR_STAGENET".equals(name()); } public long getDefaultMinFeePerVbyte() { return 15; // 2021-02-22 due to mempool congestion, increased from 2 } private static final MonetaryFormat XMR_MONETARY_FORMAT = new MonetaryFormat().minDecimals(2).repeatOptionalDecimals(2, 3).noCode().code(0, "XMR"); private static class XmrMainNetParams extends MainNetParams { @Override public MonetaryFormat getMonetaryFormat() { return XMR_MONETARY_FORMAT; } } private static class XmrTestNetParams extends RegTestParams { @Override public MonetaryFormat getMonetaryFormat() { return XMR_MONETARY_FORMAT; } } private static class XmrStageNetParams extends MainNetParams { @Override public MonetaryFormat getMonetaryFormat() { return XMR_MONETARY_FORMAT; } } } ================================================ FILE: common/src/main/java/haveno/common/config/CompositeOptionSet.java ================================================ package haveno.common.config; import com.google.common.annotations.VisibleForTesting; import joptsimple.ArgumentAcceptingOptionSpec; import joptsimple.OptionSet; import joptsimple.OptionSpec; import java.util.ArrayList; import java.util.List; /** * Composes multiple JOptSimple {@link OptionSet} instances such that calls to * {@link #valueOf(OptionSpec)} and co will search all instances in the order they were * added and return any value explicitly set, otherwise returning the default value for * the given option or null if no default has been set. The API found here loosely * emulates the {@link OptionSet} API without going through the unnecessary work of * actually extending it. In practice, this class is used to compose options provided at * the command line with those provided via config file, such that those provided at the * command line take precedence over those provided in the config file. */ @VisibleForTesting public class CompositeOptionSet { private final List optionSets = new ArrayList<>(); public void addOptionSet(OptionSet optionSet) { optionSets.add(optionSet); } public boolean has(OptionSpec option) { for (OptionSet optionSet : optionSets) if (optionSet.has(option)) return true; return false; } public V valueOf(OptionSpec option) { for (OptionSet optionSet : optionSets) if (optionSet.has(option)) return optionSet.valueOf(option); // None of the provided option sets specified the given option so fall back to // the default value (if any) provided by the first specified OptionSet return optionSets.get(0).valueOf(option); } public List valuesOf(ArgumentAcceptingOptionSpec option) { for (OptionSet optionSet : optionSets) if (optionSet.has(option)) return optionSet.valuesOf(option); // None of the provided option sets specified the given option so fall back to // the default value (if any) provided by the first specified OptionSet return optionSets.get(0).valuesOf(option); } } ================================================ FILE: common/src/main/java/haveno/common/config/Config.java ================================================ package haveno.common.config; import ch.qos.logback.classic.Level; import joptsimple.AbstractOptionSpec; import joptsimple.ArgumentAcceptingOptionSpec; import joptsimple.HelpFormatter; import joptsimple.OptionException; import joptsimple.OptionParser; import joptsimple.OptionSet; import joptsimple.OptionSpec; import joptsimple.OptionSpecBuilder; import joptsimple.util.PathConverter; import joptsimple.util.PathProperties; import joptsimple.util.RegexMatcher; import org.bitcoinj.core.NetworkParameters; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.Optional; import static com.google.common.base.Preconditions.checkNotNull; import static java.lang.String.format; import static java.util.stream.Collectors.toList; /** * Parses and provides access to all Haveno configuration options specified at the command * line and/or via the {@value DEFAULT_CONFIG_FILE_NAME} config file, including any * default values. Constructing a {@link Config} instance is generally side-effect free, * with one key exception being that {@value APP_DATA_DIR} and its subdirectories will * be created if they do not already exist. Care is taken to avoid inadvertent creation or * modification of the actual system user data directory and/or the production Haveno * application data directory. Calling code must explicitly specify these values; they are * never assumed. *

    * Note that this class deviates from typical JavaBean conventions in that fields * representing configuration options are public and have no corresponding accessor * ("getter") method. This is because all such fields are final and therefore not subject * to modification by calling code and because eliminating the accessor methods means * eliminating hundreds of lines of boilerplate code and one less touchpoint to deal with * when adding or modifying options. Furthermore, while accessor methods are often useful * when mocking an object in a testing context, this class is designed for testability * without needing to be mocked. See {@code ConfigTests} for examples. * @see #Config(String...) * @see #Config(String, File, String...) */ public class Config { // Option name constants public static final String HELP = "help"; public static final String APP_NAME = "appName"; public static final String USER_DATA_DIR = "userDataDir"; public static final String APP_DATA_DIR = "appDataDir"; public static final String CONFIG_FILE = "configFile"; public static final String MAX_MEMORY = "maxMemory"; public static final String LOG_LEVEL = "logLevel"; public static final String BANNED_XMR_NODES = "bannedXmrNodes"; public static final String BANNED_PRICE_RELAY_NODES = "bannedPriceRelayNodes"; public static final String BANNED_SEED_NODES = "bannedSeedNodes"; public static final String BASE_CURRENCY_NETWORK = "baseCurrencyNetwork"; public static final String REFERRAL_ID = "referralId"; public static final String USE_DEV_MODE = "useDevMode"; public static final String USE_DEV_MODE_HEADER = "useDevModeHeader"; public static final String TOR_DIR = "torDir"; public static final String STORAGE_DIR = "storageDir"; public static final String KEY_STORAGE_DIR = "keyStorageDir"; public static final String WALLET_DIR = "walletDir"; public static final String WALLET_RPC_BIND_PORT = "walletRpcBindPort"; public static final String USE_DEV_PRIVILEGE_KEYS = "useDevPrivilegeKeys"; public static final String DUMP_STATISTICS = "dumpStatistics"; public static final String IGNORE_DEV_MSG = "ignoreDevMsg"; public static final String PROVIDERS = "providers"; public static final String SEED_NODES = "seedNodes"; public static final String BAN_LIST = "banList"; public static final String NODE_PORT = "nodePort"; public static final String HIDDEN_SERVICE_ADDRESS = "hiddenServiceAddress"; public static final String USE_LOCALHOST_FOR_P2P = "useLocalhostForP2P"; public static final String MAX_CONNECTIONS = "maxConnections"; public static final String SOCKS_5_PROXY_XMR_ADDRESS = "socks5ProxyXmrAddress"; public static final String SOCKS_5_PROXY_HTTP_ADDRESS = "socks5ProxyHttpAddress"; public static final String USE_TOR_FOR_XMR = "useTorForXmr"; public static final String TORRC_FILE = "torrcFile"; public static final String TORRC_OPTIONS = "torrcOptions"; public static final String TOR_CONTROL_HOST = "torControlHost"; public static final String TOR_CONTROL_PORT = "torControlPort"; public static final String TOR_CONTROL_PASSWORD = "torControlPassword"; public static final String TOR_CONTROL_COOKIE_FILE = "torControlCookieFile"; public static final String TOR_CONTROL_USE_SAFE_COOKIE_AUTH = "torControlUseSafeCookieAuth"; public static final String TOR_STREAM_ISOLATION = "torStreamIsolation"; public static final String MSG_THROTTLE_PER_SEC = "msgThrottlePerSec"; public static final String MSG_THROTTLE_PER_10_SEC = "msgThrottlePer10Sec"; public static final String SEND_MSG_THROTTLE_TRIGGER = "sendMsgThrottleTrigger"; public static final String SEND_MSG_THROTTLE_SLEEP = "sendMsgThrottleSleep"; public static final String IGNORE_LOCAL_XMR_NODE = "ignoreLocalXmrNode"; public static final String BITCOIN_REGTEST_HOST = "bitcoinRegtestHost"; public static final String XMR_NODE = "xmrNode"; public static final String XMR_NODE_USERNAME = "xmrNodeUsername"; public static final String XMR_NODE_PASSWORD = "xmrNodePassword"; public static final String XMR_NODES = "xmrNodes"; public static final String USE_NATIVE_XMR_WALLET = "useNativeXmrWallet"; public static final String SOCKS5_DISCOVER_MODE = "socks5DiscoverMode"; public static final String USE_ALL_PROVIDED_NODES = "useAllProvidedNodes"; public static final String USER_AGENT = "userAgent"; public static final String NUM_CONNECTIONS_FOR_BTC = "numConnectionsForBtc"; public static final String API_PASSWORD = "apiPassword"; public static final String API_PORT = "apiPort"; public static final String PREVENT_PERIODIC_SHUTDOWN_AT_SEED_NODE = "preventPeriodicShutdownAtSeedNode"; public static final String REPUBLISH_MAILBOX_ENTRIES = "republishMailboxEntries"; public static final String LEGACY_FEE_DATAMAP = "dataMap"; public static final String BTC_TX_FEE = "btcTxFee"; public static final String BTC_MIN_TX_FEE = "btcMinTxFee"; public static final String BTC_FEES_TS = "bitcoinFeesTs"; public static final String BTC_FEE_INFO = "bitcoinFeeInfo"; public static final String BYPASS_MEMPOOL_VALIDATION = "bypassMempoolValidation"; public static final String PASSWORD_REQUIRED = "passwordRequired"; public static final String UPDATE_XMR_BINARIES = "updateXmrBinaries"; public static final String XMR_BLOCKCHAIN_PATH = "xmrBlockchainPath"; public static final String DISABLE_RATE_LIMITS = "disableRateLimits"; // Default values for certain options public static final int UNSPECIFIED_PORT = -1; public static final String DEFAULT_REGTEST_HOST = "none"; public static final int DEFAULT_NUM_CONNECTIONS_FOR_BTC = 9; // down from BitcoinJ default of 12 static final String DEFAULT_CONFIG_FILE_NAME = "haveno.properties"; // Static fields that provide access to Config properties in locations where injecting // a Config instance is not feasible. See Javadoc for corresponding static accessors. private static File APP_DATA_DIR_VALUE; private static BaseCurrencyNetwork BASE_CURRENCY_NETWORK_VALUE = BaseCurrencyNetwork.XMR_MAINNET; // Default "data dir properties", i.e. properties that can determine the location of // Haveno's application data directory (appDataDir) public final String defaultAppName; public final File defaultUserDataDir; public final File defaultAppDataDir; public final File defaultConfigFile; // Options supported only at the command-line interface (cli) public final boolean helpRequested; public final File configFile; public enum UseTorForXmr { AFTER_SYNC, OFF, ON } // Options supported on cmd line and in the config file public final String appName; public final File userDataDir; public final File appDataDir; public final int walletRpcBindPort; public final int nodePort; public final String hiddenServiceAddress; public final int maxMemory; public final String logLevel; public final List bannedXmrNodes; public final List bannedPriceRelayNodes; public final List bannedSeedNodes; public final BaseCurrencyNetwork baseCurrencyNetwork; public final NetworkParameters networkParameters; public final boolean ignoreLocalXmrNode; public final String bitcoinRegtestHost; public final String referralId; public final boolean useDevMode; public final boolean useDevModeHeader; public final boolean useDevPrivilegeKeys; public final boolean dumpStatistics; public final boolean ignoreDevMsg; public final List providers; public final List seedNodes; public final List banList; public final boolean useLocalhostForP2P; public final int maxConnections; public final String socks5ProxyXmrAddress; public final String socks5ProxyHttpAddress; public final File torrcFile; public final String torrcOptions; public final String torControlHost; public final int torControlPort; public final String torControlPassword; public final File torControlCookieFile; public final boolean useTorControlSafeCookieAuth; public final boolean torStreamIsolation; public final int msgThrottlePerSec; public final int msgThrottlePer10Sec; public final int sendMsgThrottleTrigger; public final int sendMsgThrottleSleep; public final String xmrNode; public final String xmrNodeUsername; public final String xmrNodePassword; public final String xmrNodes; public final boolean useNativeXmrWallet; public final UseTorForXmr useTorForXmr; public final boolean useTorForXmrOptionSetExplicitly; public final String socks5DiscoverMode; public final boolean useAllProvidedNodes; public final String userAgent; public final int numConnectionsForBtc; public final String apiPassword; public final int apiPort; public final boolean preventPeriodicShutdownAtSeedNode; public final boolean republishMailboxEntries; public final boolean bypassMempoolValidation; public final boolean passwordRequired; public final boolean updateXmrBinaries; public final String xmrBlockchainPath; public final boolean disableRateLimits; // Properties derived from options but not exposed as options themselves public final File torDir; public final File walletDir; public final File storageDir; public final File keyStorageDir; // The parser that will be used to parse both cmd line and config file options private final OptionParser parser = new OptionParser(); /** * Create a new {@link Config} instance using a randomly-generated default * {@value APP_NAME} and a newly-created temporary directory as the default * {@value USER_DATA_DIR} along with any command line arguments. This constructor is * primarily useful in test code, where no references or modifications should be made * to the actual system user data directory and/or real Haveno application data * directory. Most production use cases will favor calling the * {@link #Config(String, File, String...)} constructor directly. * @param args zero or more command line arguments in the form "--optName=optValue" * @throws ConfigException if any problems are encountered during option parsing * @see #Config(String, File, String...) */ public Config(String... args) { this(randomAppName(), tempUserDataDir(), args); } /** * Create a new {@link Config} instance with the given default {@value APP_NAME} and * {@value USER_DATA_DIR} values along with any command line arguments, typically * those supplied via a Haveno application's main() method. *

    * This constructor performs all parsing of command line options and config file * options, assuming the default config file exists or a custom config file has been * specified via the {@value CONFIG_FILE} option and exists. For any options that * are present both at the command line and in the config file, the command line value * will take precedence. Note that the {@value HELP} and {@value CONFIG_FILE} options * are supported only at the command line and are disallowed within the config file * itself. * @param defaultAppName typically "Haveno" or similar * @param defaultUserDataDir typically the OS-specific user data directory location * @param args zero or more command line arguments in the form "--optName=optValue" * @throws ConfigException if any problems are encountered during option parsing */ public Config(String defaultAppName, File defaultUserDataDir, String... args) { this.defaultAppName = defaultAppName; this.defaultUserDataDir = defaultUserDataDir; this.defaultAppDataDir = new File(defaultUserDataDir, defaultAppName); this.defaultConfigFile = absoluteConfigFile(defaultAppDataDir, DEFAULT_CONFIG_FILE_NAME); AbstractOptionSpec helpOpt = parser.accepts(HELP, "Print this help text") .forHelp(); ArgumentAcceptingOptionSpec configFileOpt = parser.accepts(CONFIG_FILE, format("Specify configuration file. " + "Relative paths will be prefixed by %s location.", APP_DATA_DIR)) .withRequiredArg() .ofType(String.class) .defaultsTo(DEFAULT_CONFIG_FILE_NAME); ArgumentAcceptingOptionSpec appNameOpt = parser.accepts(APP_NAME, "Application name") .withRequiredArg() .ofType(String.class) .defaultsTo(this.defaultAppName); ArgumentAcceptingOptionSpec userDataDirOpt = parser.accepts(USER_DATA_DIR, "User data directory") .withRequiredArg() .ofType(File.class) .defaultsTo(this.defaultUserDataDir); ArgumentAcceptingOptionSpec appDataDirOpt = parser.accepts(APP_DATA_DIR, "Application data directory") .withRequiredArg() .ofType(File.class) .defaultsTo(defaultAppDataDir); ArgumentAcceptingOptionSpec nodePortOpt = parser.accepts(NODE_PORT, "Port to listen on") .withRequiredArg() .ofType(Integer.class) .defaultsTo(9999); ArgumentAcceptingOptionSpec hiddenServiceAddressOpt = parser.accepts(HIDDEN_SERVICE_ADDRESS, "Hidden Service Address to listen on") .withRequiredArg() .ofType(String.class) .defaultsTo(""); ArgumentAcceptingOptionSpec walletRpcBindPortOpt = parser.accepts(WALLET_RPC_BIND_PORT, "Port to bind the wallet RPC on") .withRequiredArg() .ofType(int.class) .defaultsTo(UNSPECIFIED_PORT); ArgumentAcceptingOptionSpec maxMemoryOpt = parser.accepts(MAX_MEMORY, "Max. permitted memory (used only by headless versions)") .withRequiredArg() .ofType(int.class) .defaultsTo(1200); ArgumentAcceptingOptionSpec logLevelOpt = parser.accepts(LOG_LEVEL, "Set logging level") .withRequiredArg() .ofType(String.class) .describedAs("OFF|ALL|ERROR|WARN|INFO|DEBUG|TRACE") .defaultsTo(Level.INFO.levelStr); ArgumentAcceptingOptionSpec bannedXmrNodesOpt = parser.accepts(BANNED_XMR_NODES, "List Bitcoin nodes to ban") .withRequiredArg() .ofType(String.class) .withValuesSeparatedBy(',') .describedAs("host:port[,...]"); ArgumentAcceptingOptionSpec bannedPriceRelayNodesOpt = parser.accepts(BANNED_PRICE_RELAY_NODES, "List Haveno price nodes to ban") .withRequiredArg() .ofType(String.class) .withValuesSeparatedBy(',') .describedAs("host:port[,...]"); ArgumentAcceptingOptionSpec bannedSeedNodesOpt = parser.accepts(BANNED_SEED_NODES, "List Haveno seed nodes to ban") .withRequiredArg() .ofType(String.class) .withValuesSeparatedBy(',') .describedAs("host:port[,...]"); //noinspection rawtypes ArgumentAcceptingOptionSpec baseCurrencyNetworkOpt = parser.accepts(BASE_CURRENCY_NETWORK, "Base currency network") .withRequiredArg() .ofType(BaseCurrencyNetwork.class) .withValuesConvertedBy(new EnumValueConverter(BaseCurrencyNetwork.class)) .defaultsTo(BaseCurrencyNetwork.XMR_MAINNET); ArgumentAcceptingOptionSpec ignoreLocalXmrNodeOpt = // TODO: update this to ignore local XMR node parser.accepts(IGNORE_LOCAL_XMR_NODE, "If set to true a Monero node running locally will be ignored") .withRequiredArg() .ofType(Boolean.class) .defaultsTo(false); ArgumentAcceptingOptionSpec bitcoinRegtestHostOpt = // TODO: remove? parser.accepts(BITCOIN_REGTEST_HOST, "Bitcoin Core node when using XMR_STAGENET network") .withRequiredArg() .ofType(String.class) .describedAs("host[:port]") .defaultsTo(""); ArgumentAcceptingOptionSpec referralIdOpt = parser.accepts(REFERRAL_ID, "Optional Referral ID (e.g. for API users or pro market makers)") .withRequiredArg() .ofType(String.class) .defaultsTo(""); ArgumentAcceptingOptionSpec useDevModeOpt = parser.accepts(USE_DEV_MODE, "Enables dev mode which is used for convenience for developer testing") .withRequiredArg() .ofType(boolean.class) .defaultsTo(false); ArgumentAcceptingOptionSpec useDevModeHeaderOpt = parser.accepts(USE_DEV_MODE_HEADER, "Use dev mode css scheme to distinguish dev instances.") .withRequiredArg() .ofType(boolean.class) .defaultsTo(false); ArgumentAcceptingOptionSpec useDevPrivilegeKeysOpt = parser.accepts(USE_DEV_PRIVILEGE_KEYS, "If set to true all privileged features requiring a private " + "key to be enabled are overridden by a dev key pair (This is for developers only!)") .withRequiredArg() .ofType(boolean.class) .defaultsTo(false); ArgumentAcceptingOptionSpec dumpStatisticsOpt = parser.accepts(DUMP_STATISTICS, "If set to true dump trade statistics to a json file in appDataDir") .withRequiredArg() .ofType(boolean.class) .defaultsTo(false); ArgumentAcceptingOptionSpec ignoreDevMsgOpt = parser.accepts(IGNORE_DEV_MSG, "If set to true all signed " + "network_messages from haveno developers are ignored (Global " + "alert, Version update alert, Filters for offers, nodes or " + "trading account data)") .withRequiredArg() .ofType(boolean.class) .defaultsTo(false); ArgumentAcceptingOptionSpec providersOpt = parser.accepts(PROVIDERS, "List custom pricenodes") .withRequiredArg() .withValuesSeparatedBy(',') .describedAs("host:port[,...]"); ArgumentAcceptingOptionSpec seedNodesOpt = parser.accepts(SEED_NODES, "Override hard coded seed nodes as comma separated list e.g. " + "'rxdkppp3vicnbgqt.onion:8002,mfla72c4igh5ta2t.onion:8002'") .withRequiredArg() .withValuesSeparatedBy(',') .describedAs("host:port[,...]"); ArgumentAcceptingOptionSpec banListOpt = parser.accepts(BAN_LIST, "Nodes to exclude from network connections.") .withRequiredArg() .withValuesSeparatedBy(',') .describedAs("host:port[,...]"); ArgumentAcceptingOptionSpec useLocalhostForP2POpt = parser.accepts(USE_LOCALHOST_FOR_P2P, "Use localhost P2P network for development. Only available for non-XMR_MAINNET configuration.") .availableIf(BASE_CURRENCY_NETWORK) .withRequiredArg() .ofType(boolean.class) .defaultsTo(false); ArgumentAcceptingOptionSpec maxConnectionsOpt = parser.accepts(MAX_CONNECTIONS, "Max. connections a peer will try to keep") .withRequiredArg() .ofType(int.class) .defaultsTo(12); ArgumentAcceptingOptionSpec socks5ProxyXmrAddressOpt = parser.accepts(SOCKS_5_PROXY_XMR_ADDRESS, "A proxy address to be used for Bitcoin network.") .withRequiredArg() .describedAs("host:port") .defaultsTo(""); ArgumentAcceptingOptionSpec socks5ProxyHttpAddressOpt = parser.accepts(SOCKS_5_PROXY_HTTP_ADDRESS, "A proxy address to be used for Http requests (should be non-Tor)") .withRequiredArg() .describedAs("host:port") .defaultsTo(""); ArgumentAcceptingOptionSpec torrcFileOpt = parser.accepts(TORRC_FILE, "An existing torrc-file to be sourced for Tor. Note that torrc-entries, " + "which are critical to Haveno's correct operation, cannot be overwritten.") .withRequiredArg() .describedAs("File") .withValuesConvertedBy(new PathConverter(PathProperties.FILE_EXISTING, PathProperties.READABLE)); ArgumentAcceptingOptionSpec torrcOptionsOpt = parser.accepts(TORRC_OPTIONS, "A list of torrc-entries to amend to Haveno's torrc. Note that " + "torrc-entries, which are critical to Haveno's flawless operation, cannot be overwritten. " + "[torrc options line, torrc option, ...]") .withRequiredArg() .withValuesConvertedBy(RegexMatcher.regex("^([^\\s,]+\\s[^,]+,?\\s*)+$")) .defaultsTo(""); ArgumentAcceptingOptionSpec torControlHostOpt = parser.accepts(TOR_CONTROL_HOST, "The control hostname of an already running Tor service to be used by Haveno.") .withRequiredArg() .defaultsTo("127.0.0.1"); ArgumentAcceptingOptionSpec torControlPortOpt = parser.accepts(TOR_CONTROL_PORT, "The control port of an already running Tor service to be used by Haveno.") .availableUnless(TORRC_FILE, TORRC_OPTIONS) .withRequiredArg() .ofType(int.class) .describedAs("port") .defaultsTo(UNSPECIFIED_PORT); ArgumentAcceptingOptionSpec torControlPasswordOpt = parser.accepts(TOR_CONTROL_PASSWORD, "The password for controlling the already running Tor service.") .availableIf(TOR_CONTROL_PORT) .withRequiredArg() .defaultsTo(""); ArgumentAcceptingOptionSpec torControlCookieFileOpt = parser.accepts(TOR_CONTROL_COOKIE_FILE, "The cookie file for authenticating against the already " + "running Tor service. Use in conjunction with --" + TOR_CONTROL_USE_SAFE_COOKIE_AUTH) .availableIf(TOR_CONTROL_PORT) .availableUnless(TOR_CONTROL_PASSWORD) .withRequiredArg() .describedAs("File") .withValuesConvertedBy(new PathConverter(PathProperties.FILE_EXISTING, PathProperties.READABLE)); OptionSpecBuilder torControlUseSafeCookieAuthOpt = parser.accepts(TOR_CONTROL_USE_SAFE_COOKIE_AUTH, "Use the SafeCookie method when authenticating to the already running Tor service.") .availableIf(TOR_CONTROL_COOKIE_FILE); OptionSpecBuilder torStreamIsolationOpt = parser.accepts(TOR_STREAM_ISOLATION, "Use stream isolation for Tor [experimental!]."); ArgumentAcceptingOptionSpec msgThrottlePerSecOpt = parser.accepts(MSG_THROTTLE_PER_SEC, "Message throttle per sec for connection class") .withRequiredArg() .ofType(int.class) // With PERMITTED_MESSAGE_SIZE of 200kb results in bandwidth of 40MB/sec or 5 mbit/sec .defaultsTo(200); ArgumentAcceptingOptionSpec msgThrottlePer10SecOpt = parser.accepts(MSG_THROTTLE_PER_10_SEC, "Message throttle per 10 sec for connection class") .withRequiredArg() .ofType(int.class) // With PERMITTED_MESSAGE_SIZE of 200kb results in bandwidth of 20MB/sec or 2.5 mbit/sec .defaultsTo(1000); ArgumentAcceptingOptionSpec sendMsgThrottleTriggerOpt = parser.accepts(SEND_MSG_THROTTLE_TRIGGER, "Time in ms when we trigger a sleep if 2 messages are sent") .withRequiredArg() .ofType(int.class) .defaultsTo(20); // Time in ms when we trigger a sleep if 2 messages are sent ArgumentAcceptingOptionSpec sendMsgThrottleSleepOpt = parser.accepts(SEND_MSG_THROTTLE_SLEEP, "Pause in ms to sleep if we get too many messages to send") .withRequiredArg() .ofType(int.class) .defaultsTo(50); // Pause in ms to sleep if we get too many messages to send ArgumentAcceptingOptionSpec xmrNodeOpt = parser.accepts(XMR_NODE, "URI of custom Monero node to use") .withRequiredArg() .defaultsTo(""); ArgumentAcceptingOptionSpec xmrNodeUsernameOpt = parser.accepts(XMR_NODE_USERNAME, "Username of custom Monero node to use") .withRequiredArg() .defaultsTo(""); ArgumentAcceptingOptionSpec xmrNodePasswordOpt = parser.accepts(XMR_NODE_PASSWORD, "Password of custom Monero node to use") .withRequiredArg() .defaultsTo(""); ArgumentAcceptingOptionSpec xmrNodesOpt = parser.accepts(XMR_NODES, "Custom nodes used for Monero as comma separated IP addresses.") .withRequiredArg() .describedAs("ip[,...]") .defaultsTo(""); ArgumentAcceptingOptionSpec useNativeXmrWalletOpt = parser.accepts(USE_NATIVE_XMR_WALLET, "Use native wallet libraries instead of monero-wallet-rpc server") .withRequiredArg() .ofType(boolean.class) .defaultsTo(false); //noinspection rawtypes ArgumentAcceptingOptionSpec useTorForXmrOpt = parser.accepts(USE_TOR_FOR_XMR, "Configure TOR for Monero connections, one of: after_sync, off, or on.") .withRequiredArg() .ofType(UseTorForXmr.class) .withValuesConvertedBy(new EnumValueConverter(UseTorForXmr.class)) .defaultsTo(UseTorForXmr.AFTER_SYNC); ArgumentAcceptingOptionSpec socks5DiscoverModeOpt = parser.accepts(SOCKS5_DISCOVER_MODE, "Specify discovery mode for Bitcoin nodes. " + "One or more of: [ADDR, DNS, ONION, ALL] (comma separated, they get OR'd together).") .withRequiredArg() .describedAs("mode[,...]") .defaultsTo("ALL"); ArgumentAcceptingOptionSpec useAllProvidedNodesOpt = parser.accepts(USE_ALL_PROVIDED_NODES, "Set to true if connection of bitcoin nodes should include clear net nodes") .withRequiredArg() .ofType(boolean.class) .defaultsTo(false); ArgumentAcceptingOptionSpec userAgentOpt = parser.accepts(USER_AGENT, "User agent at btc node connections") .withRequiredArg() .defaultsTo("Haveno"); ArgumentAcceptingOptionSpec numConnectionsForBtcOpt = parser.accepts(NUM_CONNECTIONS_FOR_BTC, "Number of connections to the Bitcoin network") .withRequiredArg() .ofType(int.class) .defaultsTo(DEFAULT_NUM_CONNECTIONS_FOR_BTC); ArgumentAcceptingOptionSpec apiPasswordOpt = parser.accepts(API_PASSWORD, "gRPC API password") .withRequiredArg() .defaultsTo(""); ArgumentAcceptingOptionSpec apiPortOpt = parser.accepts(API_PORT, "gRPC API port") .withRequiredArg() .ofType(Integer.class) .defaultsTo(9998); ArgumentAcceptingOptionSpec preventPeriodicShutdownAtSeedNodeOpt = parser.accepts(PREVENT_PERIODIC_SHUTDOWN_AT_SEED_NODE, "Prevents periodic shutdown at seed nodes") .withRequiredArg() .ofType(boolean.class) .defaultsTo(false); ArgumentAcceptingOptionSpec republishMailboxEntriesOpt = parser.accepts(REPUBLISH_MAILBOX_ENTRIES, "Republish mailbox messages at startup") .withRequiredArg() .ofType(boolean.class) .defaultsTo(false); ArgumentAcceptingOptionSpec bypassMempoolValidationOpt = parser.accepts(BYPASS_MEMPOOL_VALIDATION, "Prevents mempool check of trade parameters") .withRequiredArg() .ofType(boolean.class) .defaultsTo(false); ArgumentAcceptingOptionSpec passwordRequiredOpt = parser.accepts(PASSWORD_REQUIRED, "Requires a password for creating a Haveno account") .withRequiredArg() .ofType(boolean.class) .defaultsTo(false); ArgumentAcceptingOptionSpec updateXmrBinariesOpt = parser.accepts(UPDATE_XMR_BINARIES, "Update Monero binaries if applicable") .withRequiredArg() .ofType(boolean.class) .defaultsTo(true); ArgumentAcceptingOptionSpec xmrBlockchainPathOpt = parser.accepts(XMR_BLOCKCHAIN_PATH, "Path to Monero blockchain when using local Monero node") .withRequiredArg() .ofType(String.class) .defaultsTo(""); ArgumentAcceptingOptionSpec disableRateLimits = parser.accepts(DISABLE_RATE_LIMITS, "Disables all API rate limits") .withRequiredArg() .ofType(boolean.class) .defaultsTo(false); try { CompositeOptionSet options = new CompositeOptionSet(); // Parse command line options OptionSet cliOpts = parser.parse(args); options.addOptionSet(cliOpts); // Option parsing is strict at the command line, but we relax it now for any // subsequent config file processing. This is for compatibility with pre-1.2.6 // versions that allowed unrecognized options in the haveno.properties config // file and because it follows suit with Bitcoin Core's config file behavior. parser.allowsUnrecognizedOptions(); // Parse config file specified at the command line only if it was specified as // an absolute path. Otherwise, the config file will be processed later below. File configFile = null; OptionSpec[] disallowedOpts = new OptionSpec[]{helpOpt, configFileOpt}; final boolean cliHasConfigFileOpt = cliOpts.has(configFileOpt); boolean configFileHasBeenProcessed = false; if (cliHasConfigFileOpt) { configFile = new File(cliOpts.valueOf(configFileOpt)); if (configFile.isAbsolute()) { Optional configFileOpts = parseOptionsFrom(configFile, disallowedOpts); if (configFileOpts.isPresent()) { options.addOptionSet(configFileOpts.get()); configFileHasBeenProcessed = true; } } } // Assign values to the following "data dir properties". If a // relatively-pathed config file was specified at the command line, any // entries it has for these options will be ignored, as it has not been // processed yet. this.appName = options.valueOf(appNameOpt); this.userDataDir = options.valueOf(userDataDirOpt); this.appDataDir = mkAppDataDir(options.has(appDataDirOpt) ? options.valueOf(appDataDirOpt) : new File(userDataDir, appName)); // If the config file has not yet been processed, either because a relative // path was provided at the command line, or because no value was provided at // the command line, attempt to process the file now, falling back to the // default config file location if none was specified at the command line. if (!configFileHasBeenProcessed) { configFile = cliHasConfigFileOpt && !configFile.isAbsolute() ? absoluteConfigFile(appDataDir, configFile.getPath()) : absoluteConfigFile(appDataDir, DEFAULT_CONFIG_FILE_NAME); Optional configFileOpts = parseOptionsFrom(configFile, disallowedOpts); configFileOpts.ifPresent(options::addOptionSet); } // Assign all remaining properties, with command line options taking // precedence over those provided in the config file (if any) this.helpRequested = options.has(helpOpt); this.configFile = configFile; this.nodePort = options.valueOf(nodePortOpt); this.hiddenServiceAddress = options.valueOf(hiddenServiceAddressOpt); this.walletRpcBindPort = options.valueOf(walletRpcBindPortOpt); this.maxMemory = options.valueOf(maxMemoryOpt); this.logLevel = options.valueOf(logLevelOpt); this.bannedXmrNodes = options.valuesOf(bannedXmrNodesOpt); this.bannedPriceRelayNodes = options.valuesOf(bannedPriceRelayNodesOpt); this.bannedSeedNodes = options.valuesOf(bannedSeedNodesOpt); this.baseCurrencyNetwork = (BaseCurrencyNetwork) options.valueOf(baseCurrencyNetworkOpt); this.networkParameters = baseCurrencyNetwork.getParameters(); this.ignoreLocalXmrNode = options.valueOf(ignoreLocalXmrNodeOpt); this.bitcoinRegtestHost = options.valueOf(bitcoinRegtestHostOpt); this.torrcFile = options.has(torrcFileOpt) ? options.valueOf(torrcFileOpt).toFile() : null; this.torrcOptions = options.valueOf(torrcOptionsOpt); this.torControlHost = options.valueOf(torControlHostOpt); this.torControlPort = options.valueOf(torControlPortOpt); this.torControlPassword = options.valueOf(torControlPasswordOpt); this.torControlCookieFile = options.has(torControlCookieFileOpt) ? options.valueOf(torControlCookieFileOpt).toFile() : null; this.useTorControlSafeCookieAuth = options.has(torControlUseSafeCookieAuthOpt); this.torStreamIsolation = options.has(torStreamIsolationOpt); this.referralId = options.valueOf(referralIdOpt); this.useDevMode = options.valueOf(useDevModeOpt); this.useDevModeHeader = options.valueOf(useDevModeHeaderOpt); this.useDevPrivilegeKeys = options.valueOf(useDevPrivilegeKeysOpt); this.dumpStatistics = options.valueOf(dumpStatisticsOpt); this.ignoreDevMsg = options.valueOf(ignoreDevMsgOpt); this.providers = options.valuesOf(providersOpt); this.seedNodes = options.valuesOf(seedNodesOpt); this.banList = options.valuesOf(banListOpt); this.useLocalhostForP2P = !this.baseCurrencyNetwork.isMainnet() && options.valueOf(useLocalhostForP2POpt); this.maxConnections = options.valueOf(maxConnectionsOpt); this.socks5ProxyXmrAddress = options.valueOf(socks5ProxyXmrAddressOpt); this.socks5ProxyHttpAddress = options.valueOf(socks5ProxyHttpAddressOpt); this.msgThrottlePerSec = options.valueOf(msgThrottlePerSecOpt); this.msgThrottlePer10Sec = options.valueOf(msgThrottlePer10SecOpt); this.sendMsgThrottleTrigger = options.valueOf(sendMsgThrottleTriggerOpt); this.sendMsgThrottleSleep = options.valueOf(sendMsgThrottleSleepOpt); this.xmrNode = options.valueOf(xmrNodeOpt); this.xmrNodeUsername = options.valueOf(xmrNodeUsernameOpt); this.xmrNodePassword = options.valueOf(xmrNodePasswordOpt); this.xmrNodes = options.valueOf(xmrNodesOpt); this.useNativeXmrWallet = options.valueOf(useNativeXmrWalletOpt); this.useTorForXmr = (UseTorForXmr) options.valueOf(useTorForXmrOpt); this.useTorForXmrOptionSetExplicitly = options.has(useTorForXmrOpt); this.socks5DiscoverMode = options.valueOf(socks5DiscoverModeOpt); this.useAllProvidedNodes = options.valueOf(useAllProvidedNodesOpt); this.userAgent = options.valueOf(userAgentOpt); this.numConnectionsForBtc = options.valueOf(numConnectionsForBtcOpt); this.apiPassword = options.valueOf(apiPasswordOpt); this.apiPort = options.valueOf(apiPortOpt); this.preventPeriodicShutdownAtSeedNode = options.valueOf(preventPeriodicShutdownAtSeedNodeOpt); this.republishMailboxEntries = options.valueOf(republishMailboxEntriesOpt); this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt); this.passwordRequired = options.valueOf(passwordRequiredOpt); this.updateXmrBinaries = options.valueOf(updateXmrBinariesOpt); this.xmrBlockchainPath = options.valueOf(xmrBlockchainPathOpt); this.disableRateLimits = options.valueOf(disableRateLimits); } catch (OptionException ex) { throw new ConfigException("problem parsing option '%s': %s", ex.options().get(0), ex.getCause() != null ? ex.getCause().getMessage() : ex.getMessage()); } // Create all appDataDir subdirectories and assign to their respective properties File xmrNetworkDir = mkdir(appDataDir, baseCurrencyNetwork.name().toLowerCase()); this.keyStorageDir = mkdir(xmrNetworkDir, "keys"); this.storageDir = mkdir(xmrNetworkDir, "db"); this.torDir = mkdir(xmrNetworkDir, "tor"); this.walletDir = mkdir(xmrNetworkDir, "wallet"); // Assign values to special-case static fields APP_DATA_DIR_VALUE = appDataDir; BASE_CURRENCY_NETWORK_VALUE = baseCurrencyNetwork; } private static File absoluteConfigFile(File parentDir, String relativeConfigFilePath) { return new File(parentDir, relativeConfigFilePath); } private Optional parseOptionsFrom(File configFile, OptionSpec[] disallowedOpts) { if (!configFile.exists()) { if (!configFile.equals(absoluteConfigFile(appDataDir, DEFAULT_CONFIG_FILE_NAME))) throw new ConfigException("The specified config file '%s' does not exist.", configFile); return Optional.empty(); } ConfigFileReader configFileReader = new ConfigFileReader(configFile); String[] optionLines = configFileReader.getOptionLines().stream() .map(o -> "--" + o) // prepend dashes expected by jopt parser below .collect(toList()) .toArray(new String[]{}); OptionSet configFileOpts = parser.parse(optionLines); for (OptionSpec disallowedOpt : disallowedOpts) if (configFileOpts.has(disallowedOpt)) throw new ConfigException("The '%s' option is disallowed in config files", disallowedOpt.options().get(0)); return Optional.of(configFileOpts); } public void printHelp(OutputStream sink, HelpFormatter formatter) { try { parser.formatHelpWith(formatter); parser.printHelpOn(sink); } catch (IOException ex) { throw new UncheckedIOException(ex); } } // == STATIC UTILS =================================================================== private static String randomAppName() { try { File file = Files.createTempFile("Haveno", "Temp").toFile(); //noinspection ResultOfMethodCallIgnored file.delete(); return file.toPath().getFileName().toString(); } catch (IOException ex) { throw new UncheckedIOException(ex); } } private static File tempUserDataDir() { try { return Files.createTempDirectory("HavenoTempUserData").toFile(); } catch (IOException ex) { throw new UncheckedIOException(ex); } } /** * Creates {@value APP_DATA_DIR} including any nonexistent parent directories. Does * nothing if the directory already exists. * @return the given directory, now guaranteed to exist */ private static File mkAppDataDir(File dir) { if (!dir.exists()) { try { Files.createDirectories(dir.toPath()); } catch (IOException ex) { throw new UncheckedIOException(format("Application data directory '%s' could not be created", dir), ex); } } return dir; } /** * Creates child directory assuming parent directories already exist. Does nothing if * the directory already exists. * @return the child directory, now guaranteed to exist */ private static File mkdir(File parent, String child) { File dir = new File(parent, child); if (!dir.exists()) { try { Files.createDirectory(dir.toPath()); } catch (IOException ex) { throw new UncheckedIOException(format("Directory '%s' could not be created", dir), ex); } } return dir; } // == STATIC ACCESSORS ====================================================================== /** * Static accessor that returns the same value as the non-static * {@link #appDataDir} property. For use only in the {@code Overlay} class, where * because of its large number of subclasses, injecting the Guice-managed * {@link Config} class is not worth the effort. {@link #appDataDir} should be * favored in all other cases. * @throws NullPointerException if the static value has not yet been assigned, i.e. if * the Guice-managed {@link Config} class has not yet been instantiated elsewhere. * This should never be the case, as Guice wiring always happens before any * {@code Overlay} class is instantiated. */ public static File appDataDir() { return checkNotNull(APP_DATA_DIR_VALUE, "The static appDataDir has not yet " + "been assigned. A Config instance must be instantiated (usually by " + "Guice) before calling this method."); } /** * Static accessor that returns either the default base currency network value of * {@link BaseCurrencyNetwork#XMR_MAINNET} or the value assigned via the * {@value BASE_CURRENCY_NETWORK} option. The non-static * {@link #baseCurrencyNetwork} property should be favored whenever possible and * this static accessor should be used only in code locations where it is infeasible * or too cumbersome to inject the normal Guice-managed singleton {@link Config} * instance. */ public static BaseCurrencyNetwork baseCurrencyNetwork() { return BASE_CURRENCY_NETWORK_VALUE; } /** * Static accessor that returns the value of * {@code baseCurrencyNetwork().getParameters()} for convenience and to avoid violating * the Law of Demeter. The * non-static {@link #baseCurrencyNetwork} property should be favored whenever * possible. * @see #baseCurrencyNetwork() */ public static NetworkParameters baseCurrencyNetworkParameters() { return BASE_CURRENCY_NETWORK_VALUE.getParameters(); } } ================================================ FILE: common/src/main/java/haveno/common/config/ConfigException.java ================================================ package haveno.common.config; import haveno.common.HavenoException; public class ConfigException extends HavenoException { public ConfigException(String format, Object... args) { super(format, args); } } ================================================ FILE: common/src/main/java/haveno/common/config/ConfigFileEditor.java ================================================ package haveno.common.config; import ch.qos.logback.classic.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintWriter; import java.io.UncheckedIOException; import java.util.List; public class ConfigFileEditor { private static final Logger log = (Logger) LoggerFactory.getLogger(ConfigFileEditor.class); private final File file; private final ConfigFileReader reader; public ConfigFileEditor(File file) { this.file = file; this.reader = new ConfigFileReader(file); } public void setOption(String name) { setOption(name, null); } public void setOption(String name, String arg) { tryCreate(file); List lines = reader.getLines(); try (PrintWriter writer = new PrintWriter(file)) { boolean fileAlreadyContainsTargetOption = false; for (String line : lines) { if (ConfigFileOption.isOption(line)) { ConfigFileOption existingOption = ConfigFileOption.parse(line); if (existingOption.name.equals(name)) { fileAlreadyContainsTargetOption = true; if (!existingOption.arg.equals(arg)) { ConfigFileOption newOption = new ConfigFileOption(name, arg); writer.println(newOption); log.warn("Overwrote existing config file option '{}' as '{}'", existingOption, newOption); continue; } } } writer.println(line); } if (!fileAlreadyContainsTargetOption) writer.println(new ConfigFileOption(name, arg)); } catch (FileNotFoundException ex) { throw new UncheckedIOException(ex); } } public void clearOption(String name) { if (!file.exists()) return; List lines = reader.getLines(); try (PrintWriter writer = new PrintWriter(file)) { for (String line : lines) { if (ConfigFileOption.isOption(line)) { ConfigFileOption option = ConfigFileOption.parse(line); if (option.name.equals(name)) { log.warn("Cleared existing config file option '{}'", option); continue; } } writer.println(line); } } catch (FileNotFoundException ex) { throw new UncheckedIOException(ex); } } private void tryCreate(File file) { try { if (file.createNewFile()) log.info("Created config file '{}'", file); } catch (IOException ex) { throw new UncheckedIOException(ex); } } } ================================================ FILE: common/src/main/java/haveno/common/config/ConfigFileOption.java ================================================ package haveno.common.config; class ConfigFileOption { public final String name; public final String arg; public ConfigFileOption(String name, String arg) { this.name = name; this.arg = arg; } public static boolean isOption(String line) { return !line.isEmpty() && !line.startsWith("#"); } public static ConfigFileOption parse(String option) { if (!option.contains("=")) return new ConfigFileOption(option, null); String[] tokens = clean(option).split("="); String name = tokens[0].trim(); String arg = tokens.length > 1 ? tokens[1].trim() : ""; return new ConfigFileOption(name, arg); } public String toString() { return String.format("%s%s", name, arg != null ? ('=' + arg) : ""); } public static String clean(String option) { return option .trim() .replace("\\:", ":"); } } ================================================ FILE: common/src/main/java/haveno/common/config/ConfigFileReader.java ================================================ package haveno.common.config; import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; import java.util.List; import static java.util.stream.Collectors.toList; class ConfigFileReader { private final File file; public ConfigFileReader(File file) { this.file = file; } public List getLines() { if (!file.exists()) throw new ConfigException("Config file %s does not exist", file); if (!file.canRead()) throw new ConfigException("Config file %s is not readable", file); try { return Files.readAllLines(file.toPath()).stream() .map(ConfigFileReader::cleanLine) .collect(toList()); } catch (IOException ex) { throw new UncheckedIOException(ex); } } public List getOptionLines() { return getLines().stream() .filter(ConfigFileOption::isOption) .collect(toList()); } private static String cleanLine(String line) { return ConfigFileOption.isOption(line) ? ConfigFileOption.clean(line) : line; } } ================================================ FILE: common/src/main/java/haveno/common/config/EnumValueConverter.java ================================================ /* * 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 . */ package haveno.common.config; import com.google.common.base.Enums; import com.google.common.base.Optional; import com.google.common.collect.Sets; import joptsimple.ValueConverter; import java.util.Set; /** * A {@link joptsimple.ValueConverter} that supports case-insensitive conversion from * String to an enum label. Useful in conjunction with * {@link joptsimple.ArgumentAcceptingOptionSpec#ofType(Class)} when the type in question * is an enum. */ class EnumValueConverter implements ValueConverter { private final Class enumType; public EnumValueConverter(Class enumType) { this.enumType = enumType; } /** * Attempt to resolve an enum of the specified type by looking for a label with the * given value, trying all case variations in the process. * * @return the matching enum label (if any) * @throws ConfigException if no such label matching the given value is found. */ @Override public Enum convert(String value) { Set candidates = Sets.newHashSet(value, value.toUpperCase(), value.toLowerCase()); for (String candidate : candidates) { Optional result = Enums.getIfPresent(enumType, candidate); if (result.isPresent()) return result.get(); } throw new ConfigException("Enum label %s.{%s} does not exist", enumType.getSimpleName(), String.join("|", candidates)); } @Override public Class valueType() { return enumType; } @Override public String valuePattern() { return null; } } ================================================ FILE: common/src/main/java/haveno/common/config/HavenoHelpFormatter.java ================================================ /* * 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 . */ package haveno.common.config; import joptsimple.HelpFormatter; import joptsimple.OptionDescriptor; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.stream.Collectors; public class HavenoHelpFormatter implements HelpFormatter { private final String fullName; private final String scriptName; private final String version; public HavenoHelpFormatter(String fullName, String scriptName, String version) { this.fullName = fullName; this.scriptName = scriptName; this.version = version; } public String format(Map descriptors) { StringBuilder output = new StringBuilder(); output.append(String.format("%s version %s\n\n", fullName, version)); output.append(String.format("Usage: %s [options]\n\n", scriptName)); output.append("Options:\n\n"); for (Map.Entry entry : descriptors.entrySet()) { String optionName = entry.getKey(); OptionDescriptor optionDesc = entry.getValue(); if (optionDesc.representsNonOptions()) continue; output.append(String.format("%s\n", formatOptionSyntax(optionName, optionDesc))); output.append(String.format("%s\n", formatOptionDescription(optionDesc))); } return output.toString(); } private String formatOptionSyntax(String optionName, OptionDescriptor optionDesc) { StringBuilder result = new StringBuilder(String.format(" --%s", optionName)); if (optionDesc.acceptsArguments()) result.append(String.format("=<%s>", formatArgDescription(optionDesc))); List defaultValues = optionDesc.defaultValues(); if (defaultValues.size() > 0) result.append(String.format(" (default: %s)", formatDefaultValues(defaultValues))); return result.toString(); } private String formatArgDescription(OptionDescriptor optionDesc) { String argDescription = optionDesc.argumentDescription(); if (argDescription.length() > 0) return argDescription; String typeIndicator = optionDesc.argumentTypeIndicator(); if (typeIndicator == null) return "value"; try { Class type = Class.forName(typeIndicator); return type.isEnum() ? Arrays.stream(type.getEnumConstants()).map(Object::toString).collect(Collectors.joining("|")) : typeIndicator.substring(typeIndicator.lastIndexOf('.') + 1); } catch (ClassNotFoundException ex) { // typeIndicator is something other than a class name, which can occur // in certain cases e.g. where OptionParser.withValuesConvertedBy is used. return typeIndicator; } } private Object formatDefaultValues(List defaultValues) { return defaultValues.size() == 1 ? defaultValues.get(0) : defaultValues.toString(); } private String formatOptionDescription(OptionDescriptor optionDesc) { StringBuilder output = new StringBuilder(); String remainder = optionDesc.description().trim(); // Wrap description text at 80 characters with 8 spaces of indentation and a // maximum of 72 chars of text, wrapping on spaces. Strings longer than 72 chars // without any spaces (e.g. a URL) are allowed to overflow the 80-char margin. while (remainder.length() > 72) { int idxFirstSpace = remainder.indexOf(' '); int chunkLen = idxFirstSpace == -1 ? remainder.length() : Math.max(idxFirstSpace, 73); String chunk = remainder.substring(0, chunkLen); int idxLastSpace = chunk.lastIndexOf(' '); int idxBreak = idxLastSpace > 0 ? idxLastSpace : chunk.length(); String line = remainder.substring(0, idxBreak); output.append(formatLine(line)); remainder = remainder.substring(chunk.length() - (chunk.length() - idxBreak)).trim(); } if (remainder.length() > 0) output.append(formatLine(remainder)); return output.toString(); } private String formatLine(String line) { return String.format(" %s\n", line.trim()); } } ================================================ FILE: common/src/main/java/haveno/common/consensus/UsedForTradeContractJson.java ================================================ /* * 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 . */ package haveno.common.consensus; /** * Marker interface for classes which are used in the trade contract. * Any change of the class fields would breaking backward compatibility. * If a field needs to get added it needs to be annotated with @JsonExclude (thus excluded from the contract JSON). * Better to use the excludeFromJsonDataMap (annotated with @JsonExclude; used in PaymentAccountPayload) to * add a key/value pair. */ public interface UsedForTradeContractJson { } ================================================ FILE: common/src/main/java/haveno/common/crypto/CryptoException.java ================================================ /* * 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 . */ package haveno.common.crypto; public class CryptoException extends Exception { public CryptoException(String message) { super(message); } public CryptoException(String message, Throwable cause) { super(message, cause); } public CryptoException(Throwable cause) { super(cause); } } ================================================ FILE: common/src/main/java/haveno/common/crypto/CryptoUtils.java ================================================ /* * 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 . */ package haveno.common.crypto; import lombok.extern.slf4j.Slf4j; import java.security.PublicKey; import java.security.SecureRandom; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; @Slf4j public class CryptoUtils { public static String pubKeyToString(PublicKey publicKey) { final X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKey.getEncoded()); return Base64.getEncoder().encodeToString(x509EncodedKeySpec.getEncoded()); } public static byte[] getRandomBytes(int size) { byte[] bytes = new byte[size]; new SecureRandom().nextBytes(bytes); return bytes; } } ================================================ FILE: common/src/main/java/haveno/common/crypto/Encryption.java ================================================ /* * 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 . */ package haveno.common.crypto; import haveno.common.util.Hex; import haveno.common.util.Utilities; import java.io.ByteArrayOutputStream; import java.security.InvalidKeyException; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.MGF1ParameterSpec; import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.Mac; import javax.crypto.SecretKey; import javax.crypto.spec.OAEPParameterSpec; import javax.crypto.spec.PSource; import javax.crypto.spec.SecretKeySpec; import lombok.extern.slf4j.Slf4j; @Slf4j public class Encryption { public static final String ASYM_KEY_ALGO = "RSA"; private static final String ASYM_CIPHER = "RSA/ECB/OAEPWithSHA-256AndMGF1PADDING"; public static final String SYM_KEY_ALGO = "AES"; private static final String SYM_CIPHER = "AES"; private static final String HMAC = "HmacSHA256"; public static final String HMAC_ERROR_MSG = "Hmac does not match."; public static KeyPair generateKeyPair() { long ts = System.currentTimeMillis(); try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ASYM_KEY_ALGO); keyPairGenerator.initialize(2048); return keyPairGenerator.genKeyPair(); } catch (Throwable e) { log.error("Could not create key.", e); throw new RuntimeException("Could not create key."); } } /////////////////////////////////////////////////////////////////////////////////////////// // Symmetric /////////////////////////////////////////////////////////////////////////////////////////// public static byte[] encrypt(byte[] payload, SecretKey secretKey) throws CryptoException { try { Cipher cipher = Cipher.getInstance(SYM_CIPHER); cipher.init(Cipher.ENCRYPT_MODE, secretKey); return cipher.doFinal(payload); } catch (Throwable e) { log.error("error in encrypt", e); throw new CryptoException(e); } } public static byte[] decrypt(byte[] encryptedPayload, SecretKey secretKey) throws CryptoException { try { Cipher cipher = Cipher.getInstance(SYM_CIPHER); cipher.init(Cipher.DECRYPT_MODE, secretKey); return cipher.doFinal(encryptedPayload); } catch (Throwable e) { throw new CryptoException(e); } } public static SecretKey getSecretKeyFromBytes(byte[] secretKeyBytes) { return new SecretKeySpec(secretKeyBytes, 0, secretKeyBytes.length, SYM_KEY_ALGO); } /////////////////////////////////////////////////////////////////////////////////////////// // Hmac /////////////////////////////////////////////////////////////////////////////////////////// private static byte[] getPayloadWithHmac(byte[] payload, SecretKey secretKey) { try { byte[] hmac = getHmac(payload, secretKey); try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(payload.length + hmac.length)) { outputStream.write(payload); outputStream.write(hmac); return outputStream.toByteArray(); } } catch (Throwable e) { log.error("Could not create hmac", e); throw new RuntimeException("Could not create hmac", e); } } private static boolean verifyHmac(byte[] message, byte[] hmac, SecretKey secretKey) { try { byte[] hmacTest = getHmac(message, secretKey); return Arrays.equals(hmacTest, hmac); } catch (Throwable e) { log.error("Could not create cipher", e); throw new RuntimeException("Could not create cipher"); } } private static byte[] getHmac(byte[] payload, SecretKey secretKey) throws NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { Mac mac = Mac.getInstance(HMAC); mac.init(secretKey); return mac.doFinal(payload); } /////////////////////////////////////////////////////////////////////////////////////////// // Symmetric with Hmac /////////////////////////////////////////////////////////////////////////////////////////// public static byte[] encryptPayloadWithHmac(byte[] payload, SecretKey secretKey) throws CryptoException { return encrypt(getPayloadWithHmac(payload, secretKey), secretKey); } public static byte[] decryptPayloadWithHmac(byte[] encryptedPayloadWithHmac, SecretKey secretKey) throws CryptoException { byte[] payloadWithHmac = decrypt(encryptedPayloadWithHmac, secretKey); String payloadWithHmacAsHex = Hex.encode(payloadWithHmac); // first part is raw message int length = payloadWithHmacAsHex.length(); int sep = length - 64; String payloadAsHex = payloadWithHmacAsHex.substring(0, sep); // last 64 bytes is hmac String hmacAsHex = payloadWithHmacAsHex.substring(sep, length); if (verifyHmac(Hex.decode(payloadAsHex), Hex.decode(hmacAsHex), secretKey)) { return Hex.decode(payloadAsHex); } else { throw new CryptoException(HMAC_ERROR_MSG); } } /////////////////////////////////////////////////////////////////////////////////////////// // Asymmetric /////////////////////////////////////////////////////////////////////////////////////////// public static byte[] encryptSecretKey(SecretKey secretKey, PublicKey publicKey) throws CryptoException { try { Cipher cipher = Cipher.getInstance(ASYM_CIPHER); OAEPParameterSpec oaepParameterSpec = new OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT); cipher.init(Cipher.WRAP_MODE, publicKey, oaepParameterSpec); return cipher.wrap(secretKey); } catch (Throwable e) { log.error("Couldn't encrypt payload", e); throw new CryptoException("Couldn't encrypt payload"); } } public static SecretKey decryptSecretKey(byte[] encryptedSecretKey, PrivateKey privateKey) throws CryptoException { try { Cipher cipher = Cipher.getInstance(ASYM_CIPHER); OAEPParameterSpec oaepParameterSpec = new OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT); cipher.init(Cipher.UNWRAP_MODE, privateKey, oaepParameterSpec); return (SecretKey) cipher.unwrap(encryptedSecretKey, "AES", Cipher.SECRET_KEY); } catch (Throwable e) { // errors when trying to decrypt foreign network_messages are normal throw new CryptoException(e); } } /////////////////////////////////////////////////////////////////////////////////////////// // Hybrid with signature of asymmetric key /////////////////////////////////////////////////////////////////////////////////////////// public static SecretKey generateSecretKey(int bits) { try { KeyGenerator keyGenerator = KeyGenerator.getInstance(SYM_KEY_ALGO); keyGenerator.init(bits); return keyGenerator.generateKey(); } catch (Throwable e) { log.error("Couldn't generate key", e); throw new RuntimeException("Couldn't generate key"); } } public static byte[] getPublicKeyBytes(PublicKey encryptionPubKey) { return new X509EncodedKeySpec(encryptionPubKey.getEncoded()).getEncoded(); } /** * @param encryptionPubKeyBytes * @return */ public static PublicKey getPublicKeyFromBytes(byte[] encryptionPubKeyBytes) { try { return KeyFactory.getInstance(Encryption.ASYM_KEY_ALGO).generatePublic(new X509EncodedKeySpec(encryptionPubKeyBytes)); } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { log.error("Error creating sigPublicKey from bytes. sigPublicKeyBytes as hex={}, error={}", Utilities.bytesAsHexString(encryptionPubKeyBytes), e); throw new KeyConversionException(e); } } } ================================================ FILE: common/src/main/java/haveno/common/crypto/Hash.java ================================================ /* * 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 . */ package haveno.common.crypto; import com.google.common.base.Charsets; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.Utils; import org.bouncycastle.crypto.digests.RIPEMD160Digest; import java.nio.ByteBuffer; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @Slf4j public class Hash { /** * @param data Data as byte array * @return Hash of data */ public static byte[] getSha256Hash(byte[] data) { try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); digest.update(data, 0, data.length); return digest.digest(); } catch (NoSuchAlgorithmException e) { log.error("Could not create MessageDigest for hash. ", e); throw new RuntimeException(e); } } /** * @param message UTF-8 encoded message * @return Hash of data */ public static byte[] getSha256Hash(String message) { return getSha256Hash(message.getBytes(Charsets.UTF_8)); } /** * @param data data as Integer * @return Hash of data */ public static byte[] getSha256Hash(Integer data) { return getSha256Hash(ByteBuffer.allocate(4).putInt(data).array()); } /** * Calculates RIPEMD160(SHA256(data)). */ public static byte[] getSha256Ripemd160hash(byte[] data) { return Utils.sha256hash160(data); } /** * Calculates RIPEMD160(data). */ public static byte[] getRipemd160hash(byte[] data) { RIPEMD160Digest digest = new RIPEMD160Digest(); digest.update(data, 0, data.length); byte[] out = new byte[20]; digest.doFinal(out, 0); return out; } } ================================================ FILE: common/src/main/java/haveno/common/crypto/IncorrectPasswordException.java ================================================ /* * 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 . */ package haveno.common.crypto; public class IncorrectPasswordException extends Exception { public IncorrectPasswordException(String message) { super(message); } } ================================================ FILE: common/src/main/java/haveno/common/crypto/KeyConversionException.java ================================================ /* * 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 . */ package haveno.common.crypto; public class KeyConversionException extends RuntimeException { public KeyConversionException(Throwable cause) { super(cause); } public KeyConversionException(String msg) { super(msg); } } ================================================ FILE: common/src/main/java/haveno/common/crypto/KeyRing.java ================================================ /* * 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 . */ package haveno.common.crypto; import com.google.inject.Inject; import com.google.inject.Singleton; import java.security.KeyPair; import javax.annotation.Nullable; import javax.crypto.SecretKey; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @Getter @EqualsAndHashCode @Slf4j @Singleton public final class KeyRing { private final KeyStorage keyStorage; private SecretKey symmetricKey; private KeyPair signatureKeyPair; private KeyPair encryptionKeyPair; private PubKeyRing pubKeyRing; /** * Creates the KeyRing. Unlocks if not encrypted. Does not generate keys. * * @param keyStorage Persisted storage */ @Inject public KeyRing(KeyStorage keyStorage) { this(keyStorage, null, false); } /** * Creates KeyRing with a password. Attempts to generate keys if they don't exist. * * @param keyStorage Persisted storage * @param password The password to unlock the keys or to generate new keys, nullable. * @param generateKeys Generate new keys with password if not created yet. */ public KeyRing(KeyStorage keyStorage, String password, boolean generateKeys) { this.keyStorage = keyStorage; try { unlockKeys(password, generateKeys); } catch(IncorrectPasswordException ex) { // no action } } public boolean isUnlocked() { boolean isUnlocked = this.symmetricKey != null && this.signatureKeyPair != null && this.encryptionKeyPair != null && this.pubKeyRing != null; return isUnlocked; } /** * Locks the keyring disabling access to the keys until unlock is called. * If the keys are never persisted then the keys are lost and will be regenerated. */ public void lockKeys() { signatureKeyPair = null; encryptionKeyPair = null; symmetricKey = null; pubKeyRing = null; } /** * Unlocks the keyring with a given password if required. If the keyring is already * unlocked, do nothing. * * @param password Decrypts the or encrypts newly generated keys with the given password. * @return Whether KeyRing is unlocked */ public boolean unlockKeys(@Nullable String password, boolean generateKeys) throws IncorrectPasswordException { if (isUnlocked()) return true; if (keyStorage.allKeyFilesExist()) { symmetricKey = keyStorage.loadSecretKey(KeyStorage.KeyEntry.SYM_ENCRYPTION, password); signatureKeyPair = keyStorage.loadKeyPair(KeyStorage.KeyEntry.MSG_SIGNATURE, symmetricKey); encryptionKeyPair = keyStorage.loadKeyPair(KeyStorage.KeyEntry.MSG_ENCRYPTION, symmetricKey); if (signatureKeyPair != null && encryptionKeyPair != null) pubKeyRing = new PubKeyRing(signatureKeyPair.getPublic(), encryptionKeyPair.getPublic()); } else if (generateKeys) { generateKeys(password); } return isUnlocked(); } /** * Generates a new set of keys if the current keyring is closed. * * @param password The password to unlock the keys or to generate new keys, nullable. */ public void generateKeys(String password) { if (isUnlocked()) throw new IllegalStateException("Current keyring must be closed to generate new keys"); symmetricKey = Encryption.generateSecretKey(256); signatureKeyPair = Sig.generateKeyPair(); encryptionKeyPair = Encryption.generateKeyPair(); pubKeyRing = new PubKeyRing(signatureKeyPair.getPublic(), encryptionKeyPair.getPublic()); keyStorage.saveKeyRing(this, null, password); } // Don't print keys for security reasons @Override public String toString() { return "KeyRing{" + "symmetricKey.hashCode()=" + symmetricKey.hashCode() + ", signatureKeyPair.hashCode()=" + signatureKeyPair.hashCode() + ", encryptionKeyPair.hashCode()=" + encryptionKeyPair.hashCode() + ", pubKeyRing.hashCode()=" + pubKeyRing.hashCode() + '}'; } } ================================================ FILE: common/src/main/java/haveno/common/crypto/KeyStorage.java ================================================ /* * 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 . */ package haveno.common.crypto; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.common.file.FileUtil; import static haveno.common.util.Preconditions.checkDir; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Path; import java.security.Key; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyStore; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.UnrecoverableKeyException; import java.security.interfaces.DSAParams; import java.security.interfaces.DSAPrivateKey; import java.security.interfaces.RSAPrivateCrtKey; import java.security.spec.DSAPublicKeySpec; import java.security.spec.InvalidKeySpecException; import java.security.spec.KeySpec; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.RSAPublicKeySpec; import javax.crypto.SecretKey; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * KeyStorage uses password protection to save a symmetric key in PKCS#12 format. * The symmetric key is used to encrypt and decrypt other keys in the key ring and other types of persistence. */ @Singleton public class KeyStorage { private static final Logger log = LoggerFactory.getLogger(KeyStorage.class); public enum KeyEntry { SYM_ENCRYPTION("sym.p12", Encryption.SYM_KEY_ALGO, "sym"), // symmetric encryption for persistence MSG_SIGNATURE("sig.key", Sig.KEY_ALGO, "sig"), MSG_ENCRYPTION("enc.key", Encryption.ASYM_KEY_ALGO, "enc"); private final String fileName; private final String algorithm; private final String alias; KeyEntry(String fileName, String algorithm, String alias) { this.fileName = fileName; this.algorithm = algorithm; this.alias = alias; } public String getFileName() { return fileName; } public String getAlgorithm() { return algorithm; } public String getAlias() { return alias; } @NotNull @Override public String toString() { return "Key{" + "fileName='" + fileName + '\'' + ", algorithm='" + algorithm + '\'' + '}'; } } private final File storageDir; @Inject public KeyStorage(@Named(Config.KEY_STORAGE_DIR) File storageDir) { this.storageDir = checkDir(storageDir); } public boolean allKeyFilesExist() { return fileExists(KeyEntry.MSG_SIGNATURE) && fileExists(KeyEntry.MSG_ENCRYPTION) && fileExists(KeyEntry.SYM_ENCRYPTION); } private boolean fileExists(KeyEntry keyEntry) { return new File(storageDir + "/" + keyEntry.getFileName()).exists(); } private byte[] loadKeyBytes(KeyEntry keyEntry, SecretKey secretKey) { File keyFile = new File(storageDir + "/" + keyEntry.getFileName()); try (FileInputStream fis = new FileInputStream(keyFile.getPath())) { byte[] encodedKey = new byte[(int) keyFile.length()]; //noinspection ResultOfMethodCallIgnored fis.read(encodedKey); encodedKey = Encryption.decryptPayloadWithHmac(encodedKey, secretKey); return encodedKey; } catch (IOException | CryptoException e) { log.error("Could not load key " + keyEntry.toString(), e.getMessage()); throw new RuntimeException("Could not load key " + keyEntry.toString(), e); } } /** * Loads the public private KeyPair from a key file. * * @param keyEntry The key entry that defines the public private key * @param secretKey The symmetric key that protects the key entry file */ public KeyPair loadKeyPair(KeyEntry keyEntry, SecretKey secretKey) { FileUtil.rollingBackup(storageDir, keyEntry.getFileName(), 20); try { KeyFactory keyFactory = KeyFactory.getInstance(keyEntry.getAlgorithm()); byte[] encodedPrivateKey = loadKeyBytes(keyEntry, secretKey); PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(encodedPrivateKey); PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec); PublicKey publicKey; if (privateKey instanceof RSAPrivateCrtKey) { RSAPrivateCrtKey rsaPrivateKey = (RSAPrivateCrtKey) privateKey; RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(rsaPrivateKey.getModulus(), rsaPrivateKey.getPublicExponent()); publicKey = keyFactory.generatePublic(publicKeySpec); } else if (privateKey instanceof DSAPrivateKey) { DSAPrivateKey dsaPrivateKey = (DSAPrivateKey) privateKey; DSAParams dsaParams = dsaPrivateKey.getParams(); BigInteger p = dsaParams.getP(); BigInteger q = dsaParams.getQ(); BigInteger g = dsaParams.getG(); BigInteger y = g.modPow(dsaPrivateKey.getX(), p); KeySpec publicKeySpec = new DSAPublicKeySpec(y, p, q, g); publicKey = keyFactory.generatePublic(publicKeySpec); } else { throw new RuntimeException("Unsupported key algo" + keyEntry.getAlgorithm()); } return new KeyPair(publicKey, privateKey); } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { log.error("Could not load key " + keyEntry.toString(), e); throw new RuntimeException("Could not load key " + keyEntry.toString(), e); } } /** * Loads the password protected symmetric secret key for this key ring. * * @param keyEntry The key entry that defines the symmetric key * @param password Optional password that protects the key */ public SecretKey loadSecretKey(KeyEntry keyEntry, String password) throws IncorrectPasswordException { FileUtil.rollingBackup(storageDir, keyEntry.getFileName(), 20); char[] passwordChars = password == null ? new char[0] : password.toCharArray(); try { KeyStore keyStore = KeyStore.getInstance("PKCS12"); try (FileInputStream fileInputStream = new FileInputStream(storageDir + "/" + keyEntry.getFileName())) { keyStore.load(fileInputStream, passwordChars); } Key key = keyStore.getKey(keyEntry.getAlias(), passwordChars); return (SecretKey) key; } catch (UnrecoverableKeyException e) { // null password when password is required throw new IncorrectPasswordException("Incorrect password"); } catch (IOException e) { // incorrect password if (e.getCause() instanceof UnrecoverableKeyException) { throw new IncorrectPasswordException("Incorrect password"); } else { log.error("Could not load key " + keyEntry.toString(), e); throw new RuntimeException("Could not load key " + keyEntry.toString(), e); } } catch (Exception e) { log.error("Could not load key " + keyEntry.toString(), e); throw new RuntimeException("Could not load key " + keyEntry.toString(), e); } } /** * Saves the key ring to the key storage directory. * * @param keyRing The key ring * @param password Optional password */ public void saveKeyRing(KeyRing keyRing, String oldPassword, String password) { SecretKey symmetric = keyRing.getSymmetricKey(); // password protect the symmetric key saveKey(symmetric, KeyEntry.SYM_ENCRYPTION.getAlias(), KeyEntry.SYM_ENCRYPTION.getFileName(), oldPassword, password); // use symmetric encryption to encrypt the key pairs saveKey(keyRing.getSignatureKeyPair().getPrivate(), KeyEntry.MSG_SIGNATURE.getFileName(), symmetric); saveKey(keyRing.getEncryptionKeyPair().getPrivate(), KeyEntry.MSG_ENCRYPTION.getFileName(), symmetric); } /** * Saves private key in PKCS#8 to a file and encrypts using the symmetric key. * * @param key The key pair * @param fileName File name to save * @param secretKey Secret key to encrypt the key pair */ private void saveKey(PrivateKey key, String fileName, SecretKey secretKey) { if (!storageDir.exists()) //noinspection ResultOfMethodCallIgnored storageDir.mkdirs(); PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(key.getEncoded()); byte[] keyBytes = pkcs8EncodedKeySpec.getEncoded(); try (FileOutputStream fos = new FileOutputStream(storageDir + "/" + fileName)) { keyBytes = Encryption.encryptPayloadWithHmac(keyBytes, secretKey); fos.write(keyBytes); } catch (Exception e) { log.error("Could not save key " + fileName, e); throw new RuntimeException("Could not save key " + fileName, e); } } /** * Saves a SecretKey to a PKCS12 file. * * @param key The symmetric key * @param alias Alias of the key entry in the key store * @param fileName Filename of the key store * @param oldPassword Optional password to decrypt existing key store * @param password Optional password to encrypt the key store */ private void saveKey(SecretKey key, String alias, String fileName, String oldPassword, String password) { if (!storageDir.exists()) //noinspection ResultOfMethodCallIgnored storageDir.mkdirs(); // password must be ascii if (password != null && !password.matches("\\p{ASCII}*")) { throw new IllegalArgumentException("Password must be ASCII."); } var oldPasswordChars = oldPassword == null ? new char[0] : oldPassword.toCharArray(); var passwordChars = password == null ? new char[0] : password.toCharArray(); try { var path = storageDir + "/" + fileName; KeyStore keyStore = KeyStore.getInstance("PKCS12"); // load from existing file or initialize new if (Files.exists(Path.of(path))) { try (FileInputStream fileInputStream = new FileInputStream(path)) { keyStore.load(fileInputStream, oldPasswordChars); } } else { keyStore.load(null, null); } // store in the keystore keyStore.setKeyEntry(alias, key, passwordChars, null); try (FileOutputStream fileOutputStream = new FileOutputStream(path)) { // save the keystore keyStore.store(fileOutputStream, passwordChars); } } catch (Exception e) { throw new RuntimeException("Could not save key " + alias, e); } } } ================================================ FILE: common/src/main/java/haveno/common/crypto/PubKeyRing.java ================================================ /* * 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 . */ package haveno.common.crypto; import com.google.common.annotations.VisibleForTesting; import com.google.protobuf.ByteString; import haveno.common.consensus.UsedForTradeContractJson; import haveno.common.proto.network.NetworkPayload; import haveno.common.util.Utilities; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.security.PublicKey; /** * Same as KeyRing but with public keys only. * Used to send public keys over the wire to other peer. */ @Slf4j @EqualsAndHashCode @Getter public final class PubKeyRing implements NetworkPayload, UsedForTradeContractJson { private final byte[] signaturePubKeyBytes; private final byte[] encryptionPubKeyBytes; private transient PublicKey signaturePubKey; private transient PublicKey encryptionPubKey; public PubKeyRing(PublicKey signaturePubKey, PublicKey encryptionPubKey) { this.signaturePubKeyBytes = Sig.getPublicKeyBytes(signaturePubKey); this.encryptionPubKeyBytes = Encryption.getPublicKeyBytes(encryptionPubKey); this.signaturePubKey = signaturePubKey; this.encryptionPubKey = encryptionPubKey; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @VisibleForTesting public PubKeyRing(byte[] signaturePubKeyBytes, byte[] encryptionPubKeyBytes) { this.signaturePubKeyBytes = signaturePubKeyBytes; this.encryptionPubKeyBytes = encryptionPubKeyBytes; signaturePubKey = Sig.getPublicKeyFromBytes(signaturePubKeyBytes); encryptionPubKey = Encryption.getPublicKeyFromBytes(encryptionPubKeyBytes); } @Override public protobuf.PubKeyRing toProtoMessage() { return protobuf.PubKeyRing.newBuilder() .setSignaturePubKeyBytes(ByteString.copyFrom(signaturePubKeyBytes)) .setEncryptionPubKeyBytes(ByteString.copyFrom(encryptionPubKeyBytes)) .build(); } public static PubKeyRing fromProto(protobuf.PubKeyRing proto) { return new PubKeyRing( proto.getSignaturePubKeyBytes().toByteArray(), proto.getEncryptionPubKeyBytes().toByteArray()); } @Override public String toString() { return "PubKeyRing{" + "signaturePubKeyHex=" + Utilities.bytesAsHexString(signaturePubKeyBytes) + ", encryptionPubKeyHex=" + Utilities.bytesAsHexString(encryptionPubKeyBytes) + "}"; } } ================================================ FILE: common/src/main/java/haveno/common/crypto/PubKeyRingProvider.java ================================================ package haveno.common.crypto; import com.google.inject.Inject; import com.google.inject.Provider; /** * Allows User's static PubKeyRing to be injected into constructors without having to * open the account yet. Once its opened, PubKeyRingProvider will return non-null PubKeyRing. * Originally used via bind(PubKeyRing.class).toProvider(PubKeyRingProvider.class); */ public class PubKeyRingProvider implements Provider { private final KeyRing keyRing; @Inject public PubKeyRingProvider(KeyRing keyRing) { this.keyRing = keyRing; } @Override public PubKeyRing get() { return keyRing.getPubKeyRing(); } } ================================================ FILE: common/src/main/java/haveno/common/crypto/ScryptUtil.java ================================================ /* * 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 . */ package haveno.common.crypto; import com.google.protobuf.ByteString; import haveno.common.UserThread; import haveno.common.util.Utilities; import org.bitcoinj.crypto.KeyCrypterScrypt; import org.bitcoinj.wallet.Protos; import org.bouncycastle.crypto.params.KeyParameter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; //TODO: Borrowed form BitcoinJ/Lighthouse. Remove Protos dependency, check complete code logic. public class ScryptUtil { private static final Logger log = LoggerFactory.getLogger(ScryptUtil.class); public interface DeriveKeyResultHandler { void handleResult(KeyParameter aesKey); } public static KeyCrypterScrypt getKeyCrypterScrypt() { return getKeyCrypterScrypt(KeyCrypterScrypt.randomSalt()); } public static KeyCrypterScrypt getKeyCrypterScrypt(byte[] salt) { Protos.ScryptParameters scryptParameters = Protos.ScryptParameters.newBuilder() .setP(6) .setR(8) .setN(32768) .setSalt(ByteString.copyFrom(salt)) .build(); return new KeyCrypterScrypt(scryptParameters); } public static KeyParameter deriveKeyWithScrypt(KeyCrypterScrypt keyCrypterScrypt, String password) { try { log.debug("Doing key derivation"); long start = System.currentTimeMillis(); KeyParameter aesKey = keyCrypterScrypt.deriveKey(password); long duration = System.currentTimeMillis() - start; log.debug("Key derivation took {} msec", duration); return aesKey; } catch (Throwable t) { t.printStackTrace(); log.error("Key derivation failed. " + t.getMessage()); throw t; } } public static void deriveKeyWithScrypt(KeyCrypterScrypt keyCrypterScrypt, String password, DeriveKeyResultHandler resultHandler) { Utilities.getThreadPoolExecutor("ScryptUtil:deriveKeyWithScrypt-%d", 1, 2, 5L).submit(() -> { try { KeyParameter aesKey = deriveKeyWithScrypt(keyCrypterScrypt, password); UserThread.execute(() -> { try { resultHandler.handleResult(aesKey); } catch (Throwable t) { t.printStackTrace(); log.error("Executing task failed. " + t.getMessage()); throw t; } }); } catch (Throwable t) { t.printStackTrace(); log.error("Executing task failed. " + t.getMessage()); throw t; } }); } } ================================================ FILE: common/src/main/java/haveno/common/crypto/SealedAndSigned.java ================================================ /* * 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 . */ package haveno.common.crypto; import com.google.protobuf.ByteString; import haveno.common.proto.network.NetworkPayload; import lombok.Value; import java.security.PublicKey; @Value public final class SealedAndSigned implements NetworkPayload { private final byte[] encryptedSecretKey; private final byte[] encryptedPayloadWithHmac; private final byte[] signature; private final byte[] sigPublicKeyBytes; transient private final PublicKey sigPublicKey; public SealedAndSigned(byte[] encryptedSecretKey, byte[] encryptedPayloadWithHmac, byte[] signature, PublicKey sigPublicKey) { this.encryptedSecretKey = encryptedSecretKey; this.encryptedPayloadWithHmac = encryptedPayloadWithHmac; this.signature = signature; this.sigPublicKey = sigPublicKey; sigPublicKeyBytes = Sig.getPublicKeyBytes(sigPublicKey); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private SealedAndSigned(byte[] encryptedSecretKey, byte[] encryptedPayloadWithHmac, byte[] signature, byte[] sigPublicKeyBytes) { this.encryptedSecretKey = encryptedSecretKey; this.encryptedPayloadWithHmac = encryptedPayloadWithHmac; this.signature = signature; this.sigPublicKeyBytes = sigPublicKeyBytes; sigPublicKey = Sig.getPublicKeyFromBytes(sigPublicKeyBytes); } public protobuf.SealedAndSigned toProtoMessage() { return protobuf.SealedAndSigned.newBuilder() .setEncryptedSecretKey(ByteString.copyFrom(encryptedSecretKey)) .setEncryptedPayloadWithHmac(ByteString.copyFrom(encryptedPayloadWithHmac)) .setSignature(ByteString.copyFrom(signature)) .setSigPublicKeyBytes(ByteString.copyFrom(sigPublicKeyBytes)) .build(); } public static SealedAndSigned fromProto(protobuf.SealedAndSigned proto) { return new SealedAndSigned(proto.getEncryptedSecretKey().toByteArray(), proto.getEncryptedPayloadWithHmac().toByteArray(), proto.getSignature().toByteArray(), proto.getSigPublicKeyBytes().toByteArray()); } } ================================================ FILE: common/src/main/java/haveno/common/crypto/Sig.java ================================================ /* * 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 . */ package haveno.common.crypto; import com.google.common.base.Charsets; import haveno.common.util.Base64; import haveno.common.util.Utilities; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.security.InvalidKeyException; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; /** * StorageSignatureKeyPair/STORAGE_SIGN_KEY_ALGO: That is used for signing the data to be stored to the P2P network (by flooding). * The algo is selected because it originated from the TomP2P version which used DSA. * Changing to EC keys might be considered. *

    * MsgSignatureKeyPair/MSG_SIGN_KEY_ALGO/MSG_SIGN_ALGO: That is used when sending a message to a peer which is encrypted and signed. * Changing to EC keys might be considered. */ public class Sig { private static final Logger log = LoggerFactory.getLogger(Sig.class); public static final String KEY_ALGO = "DSA"; private static final String ALGO = "SHA256withDSA"; /** * @return keyPair */ public static KeyPair generateKeyPair() { long ts = System.currentTimeMillis(); try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGO); keyPairGenerator.initialize(2048); return keyPairGenerator.genKeyPair(); } catch (NoSuchAlgorithmException e) { log.error("Could not create key.", e); throw new RuntimeException("Could not create key."); } } /** * @param privateKey * @param data * @return */ public static byte[] sign(PrivateKey privateKey, byte[] data) throws CryptoException { try { Signature sig = Signature.getInstance(ALGO); sig.initSign(privateKey); sig.update(data); return sig.sign(); } catch (SignatureException | InvalidKeyException | NoSuchAlgorithmException e) { throw new CryptoException("Signing failed. " + e.getMessage()); } } /** * @param privateKey * @param message UTF-8 encoded message to sign * @return Base64 encoded signature */ public static String sign(PrivateKey privateKey, String message) throws CryptoException { byte[] sigAsBytes = sign(privateKey, message.getBytes(Charsets.UTF_8)); return Base64.encode(sigAsBytes); } /** * @param publicKey * @param data * @param signature * @return */ public static boolean verify(PublicKey publicKey, byte[] data, byte[] signature) throws CryptoException { try { Signature sig = Signature.getInstance(ALGO); sig.initVerify(publicKey); sig.update(data); return sig.verify(signature); } catch (SignatureException | InvalidKeyException | NoSuchAlgorithmException e) { throw new CryptoException("Signature verification failed", e); } } /** * @param publicKey * @param message UTF-8 encoded message * @param signature Base64 encoded signature * @return */ public static boolean verify(PublicKey publicKey, String message, String signature) throws CryptoException { return verify(publicKey, message.getBytes(Charsets.UTF_8), Base64.decode(signature)); } /** * @param sigPublicKeyBytes * @return */ public static PublicKey getPublicKeyFromBytes(byte[] sigPublicKeyBytes) { try { return KeyFactory.getInstance(Sig.KEY_ALGO).generatePublic(new X509EncodedKeySpec(sigPublicKeyBytes)); } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { log.error("Error creating sigPublicKey from bytes. sigPublicKeyBytes as hex={}, error={}", Utilities.bytesAsHexString(sigPublicKeyBytes), e); e.printStackTrace(); throw new KeyConversionException(e); } } public static byte[] getPublicKeyBytes(PublicKey sigPublicKey) { return new X509EncodedKeySpec(sigPublicKey.getEncoded()).getEncoded(); } } ================================================ FILE: common/src/main/java/haveno/common/file/CorruptedStorageFileHandler.java ================================================ /* * 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 . */ package haveno.common.file; import com.google.inject.Inject; import com.google.inject.Singleton; import java.util.ArrayList; import java.util.List; import java.util.Optional; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton public class CorruptedStorageFileHandler { private final List files = new ArrayList<>(); @Inject public CorruptedStorageFileHandler() { } public void addFile(String fileName) { files.add(fileName); } public Optional> getFiles() { if (files.isEmpty()) { return Optional.empty(); } if (files.size() == 1 && files.get(0).equals("ViewPathAsString")) { log.debug("We detected incompatible data base file for Navigation. " + "That is a minor issue happening with refactoring of UI classes " + "and we don't display a warning popup to the user."); return Optional.empty(); } return Optional.of(files); } } ================================================ FILE: common/src/main/java/haveno/common/file/FileUtil.java ================================================ /* * 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 . */ package haveno.common.file; import com.google.common.io.Files; import haveno.common.util.Utilities; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import javax.annotation.Nullable; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Scanner; @Slf4j public class FileUtil { private static final String BACKUP_DIR = "backup"; public static void rollingBackup(File dir, String fileName, int numMaxBackupFiles) { if (numMaxBackupFiles <= 0) return; if (dir.exists()) { File backupDir = new File(Paths.get(dir.getAbsolutePath(), BACKUP_DIR).toString()); if (!backupDir.exists()) if (!backupDir.mkdir()) log.warn("make dir failed.\nBackupDir=" + backupDir.getAbsolutePath()); File origFile = new File(Paths.get(dir.getAbsolutePath(), fileName).toString()); if (origFile.exists()) { String dirName = "backups_" + fileName; if (dirName.contains(".")) dirName = dirName.replace(".", "_"); File backupFileDir = new File(Paths.get(backupDir.getAbsolutePath(), dirName).toString()); if (!backupFileDir.exists()) if (!backupFileDir.mkdir()) log.warn("make backupFileDir failed.\nBackupFileDir=" + backupFileDir.getAbsolutePath()); File backupFile = new File(Paths.get(backupFileDir.getAbsolutePath(), new Date().getTime() + "_" + fileName).toString()); try { Files.copy(origFile, backupFile); pruneBackup(backupFileDir, numMaxBackupFiles); } catch (IOException e) { log.error("Backup key failed: {}\n", e.getMessage(), e); } } } } public static List getBackupFiles(File dir, String fileName) { File backupDir = new File(Paths.get(dir.getAbsolutePath(), BACKUP_DIR).toString()); if (!backupDir.exists()) return new ArrayList(); String dirName = "backups_" + fileName; if (dirName.contains(".")) dirName = dirName.replace(".", "_"); File backupFileDir = new File(Paths.get(backupDir.getAbsolutePath(), dirName).toString()); if (!backupFileDir.exists()) return new ArrayList(); File[] files = backupFileDir.listFiles(); return Arrays.asList(files); } public static File getLatestBackupFile(File dir, String fileName) { List files = getBackupFiles(dir, fileName); if (files.isEmpty()) return null; files.sort(Comparator.comparing(File::getName)); return files.get(files.size() - 1); } public static void deleteRollingBackup(File dir, String fileName) { File backupDir = new File(Paths.get(dir.getAbsolutePath(), BACKUP_DIR).toString()); if (!backupDir.exists()) return; String dirName = "backups_" + fileName; if (dirName.contains(".")) dirName = dirName.replace(".", "_"); File backupFileDir = new File(Paths.get(backupDir.getAbsolutePath(), dirName).toString()); try { FileUtils.deleteDirectory(backupFileDir); } catch (IOException e) { log.error("Delete backup key failed: {}\n", e.getMessage(), e); } } private static void pruneBackup(File backupDir, int numMaxBackupFiles) { if (backupDir.isDirectory()) { File[] files = backupDir.listFiles(); if (files != null) { List filesList = Arrays.asList(files); if (filesList.size() > numMaxBackupFiles) { filesList.sort(Comparator.comparing(File::getName)); File file = filesList.get(0); if (file.isFile()) { if (!file.delete()) log.error("Failed to delete file: " + file); else pruneBackup(backupDir, numMaxBackupFiles); } else { pruneBackup(new File(Paths.get(backupDir.getAbsolutePath(), file.getName()).toString()), numMaxBackupFiles); } } } } } public static void deleteDirectory(File file) throws IOException { deleteDirectory(file, null, true); } public static void deleteDirectory(File file, @Nullable File exclude, boolean ignoreLockedFiles) throws IOException { boolean excludeFileFound = false; if (file.isDirectory()) { File[] files = file.listFiles(); if (files != null) for (File f : files) { boolean excludeFileFoundLocal = exclude != null && f.getAbsolutePath().equals(exclude.getAbsolutePath()); excludeFileFound |= excludeFileFoundLocal; if (!excludeFileFoundLocal) deleteDirectory(f, exclude, ignoreLockedFiles); } } // Finally delete main file/dir if exclude file was not found in directory if (!excludeFileFound && !(exclude != null && file.getAbsolutePath().equals(exclude.getAbsolutePath()))) { try { deleteFileIfExists(file, ignoreLockedFiles); } catch (Throwable t) { log.error("Could not delete file. Error=" + t.toString()); throw new IOException(t); } } } public static void deleteFileIfExists(File file) throws IOException { deleteFileIfExists(file, true); } public static void deleteFileIfExists(File file, boolean ignoreLockedFiles) throws IOException { try { if (Utilities.isWindows()) file = file.getCanonicalFile(); if (file.exists() && !file.delete()) { if (ignoreLockedFiles) { // We check if file is locked. On Windows all open files are locked by the OS, so we if (isFileLocked(file)) log.info("Failed to delete locked file: " + file.getAbsolutePath()); } else { final String message = "Failed to delete file: " + file.getAbsolutePath(); log.error(message); throw new IOException(message); } } } catch (Throwable t) { log.error("Could not delete file, error={}\n", t.getMessage(), t); throw new IOException(t); } } private static boolean isFileLocked(File file) { return !file.canWrite(); } public static void resourceToFile(String resourcePath, File destinationFile) throws ResourceNotFoundException, IOException { try (InputStream inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(resourcePath)) { if (inputStream == null) { throw new ResourceNotFoundException(resourcePath); } try (FileOutputStream fileOutputStream = new FileOutputStream(destinationFile)) { IOUtils.copy(inputStream, fileOutputStream); } } } public static boolean resourceEqualToFile(String resourcePath, File destinationFile) throws ResourceNotFoundException, IOException { try (InputStream inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(resourcePath)) { if (inputStream == null) { throw new ResourceNotFoundException(resourcePath); } return IOUtils.contentEquals(inputStream, new FileInputStream(destinationFile)); } } public static void renameFile(File oldFile, File newFile) throws IOException { if (Utilities.isWindows()) { // Work around an issue on Windows whereby you can't rename over existing files. final File canonical = newFile.getCanonicalFile(); if (canonical.exists() && !canonical.delete()) { throw new IOException("Failed to delete canonical file for replacement with save"); } if (!oldFile.renameTo(canonical)) { throw new IOException("Failed to rename " + oldFile + " to " + canonical); } } else if (!oldFile.renameTo(newFile)) { throw new IOException("Failed to rename " + oldFile + " to " + newFile); } } public static void copyFile(File origin, File target) throws IOException { if (!origin.exists()) { return; } try { Files.copy(origin, target); } catch (IOException e) { log.error("Copy file failed", e); throw new IOException("Failed to copy " + origin + " to " + target); } } public static void copyDirectory(File source, File destination) throws IOException { FileUtils.copyDirectory(source, destination); } public static File createNewFile(Path path) throws IOException { File file = path.toFile(); if (!file.createNewFile()) { throw new IOException("There already exists a file with path: " + path); } return file; } public static void removeAndBackupFile(File dbDir, File storageFile, String fileName, String backupFolderName) throws IOException { File corruptedBackupDir = new File(Paths.get(dbDir.getAbsolutePath(), backupFolderName).toString()); if (!corruptedBackupDir.exists() && !corruptedBackupDir.mkdir()) { log.warn("make dir failed"); } File corruptedFile = new File(Paths.get(dbDir.getAbsolutePath(), backupFolderName, fileName).toString()); if (storageFile.exists()) { renameFile(storageFile, corruptedFile); } } public static boolean doesFileContainKeyword(File file, String keyword) throws FileNotFoundException { Scanner s = new Scanner(file); while (s.hasNextLine()) { if (s.nextLine().contains(keyword)) { return true; } } return false; } } ================================================ FILE: common/src/main/java/haveno/common/file/JsonFileManager.java ================================================ /* * 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 . */ package haveno.common.file; import haveno.common.util.Utilities; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.PrintWriter; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ThreadPoolExecutor; @Slf4j public class JsonFileManager { private final static List INSTANCES = new ArrayList<>(); public static void shutDownAllInstances() { INSTANCES.forEach(JsonFileManager::shutDown); } @Nullable private ThreadPoolExecutor executor; private final File dir; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public JsonFileManager(File dir) { this.dir = dir; if (!dir.exists() && !dir.mkdir()) { log.warn("make dir failed"); } INSTANCES.add(this); } @NotNull protected ThreadPoolExecutor getExecutor() { if (executor == null) { executor = Utilities.getThreadPoolExecutor("JsonFileManagerExecutor", 5, 50, 60); } return executor; } public void shutDown() { if (executor != null) { executor.shutdown(); } } public void writeToDiscThreaded(String json, String fileName) { getExecutor().execute(() -> writeToDisc(json, fileName)); } public void writeToDisc(String json, String fileName) { File jsonFile = new File(Paths.get(dir.getAbsolutePath(), fileName + ".json").toString()); File tempFile = null; PrintWriter printWriter = null; try { tempFile = File.createTempFile("temp", null, dir); tempFile.deleteOnExit(); printWriter = new PrintWriter(tempFile); printWriter.println(json); // This close call and comment is borrowed from FileManager. Not 100% sure it that is really needed but // seems that had fixed in the past and we got reported issues on Windows so that fix might be still // required. // Close resources before replacing file with temp file because otherwise it causes problems on windows // when rename temp file printWriter.close(); FileUtil.renameFile(tempFile, jsonFile); } catch (Throwable t) { log.error("storageFile " + jsonFile.toString()); t.printStackTrace(); } finally { if (tempFile != null && tempFile.exists()) { log.warn("Temp file still exists after failed save. We will delete it now. storageFile=" + fileName); if (!tempFile.delete()) log.error("Cannot delete temp file."); } if (printWriter != null) printWriter.close(); } } } ================================================ FILE: common/src/main/java/haveno/common/file/ResourceNotFoundException.java ================================================ /* * 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 . */ package haveno.common.file; public class ResourceNotFoundException extends Exception { public ResourceNotFoundException(String path) { super("Resource not found: path = " + path); } } ================================================ FILE: common/src/main/java/haveno/common/handlers/ErrorMessageHandler.java ================================================ /* * 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 . */ package haveno.common.handlers; /** * For reporting error message only (UI) */ public interface ErrorMessageHandler { void handleErrorMessage(String errorMessage); } ================================================ FILE: common/src/main/java/haveno/common/handlers/ExceptionHandler.java ================================================ /* * 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 . */ package haveno.common.handlers; /** * For reporting throwable objects only */ public interface ExceptionHandler { void handleException(Throwable throwable); } ================================================ FILE: common/src/main/java/haveno/common/handlers/FaultHandler.java ================================================ /* * 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 . */ package haveno.common.handlers; /** * For reporting a description message and throwable */ public interface FaultHandler { void handleFault(String errorMessage, Throwable throwable); } ================================================ FILE: common/src/main/java/haveno/common/handlers/ResultHandler.java ================================================ /* * 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 . */ package haveno.common.handlers; public interface ResultHandler { void handleResult(); } ================================================ FILE: common/src/main/java/haveno/common/persistence/PersistenceManager.java ================================================ /* * 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 . */ package haveno.common.persistence; import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.config.Config; import haveno.common.crypto.CryptoException; import haveno.common.crypto.Encryption; import haveno.common.crypto.KeyRing; import haveno.common.file.CorruptedStorageFileHandler; import haveno.common.file.FileUtil; import haveno.common.handlers.ResultHandler; import haveno.common.proto.persistable.PersistableEnvelope; import haveno.common.proto.persistable.PersistenceProtoResolver; import haveno.common.util.GcUtil; import static haveno.common.util.Preconditions.checkDir; import haveno.common.util.SingleThreadExecutorUtils; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Path; import java.util.HashMap; 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; import javax.annotation.Nullable; import lombok.Getter; import lombok.extern.slf4j.Slf4j; /** * Responsible for reading persisted data and writing it on disk. We read usually only at start-up and keep data in RAM. * We write all data which got a request for persistence at shut down at the very last moment when all other services * are shut down, so allowing changes to the data in the very last moment. For critical data we set {@link Source} * to HIGH which causes a timer to trigger a write to disk after 1 minute. We use that for not very frequently altered * data and data which cannot be recovered from the network. * * We decided to not use threading (as it was in previous versions) as the read operation happens only at start-up and * with the modified model that data is written at shut down we eliminate frequent and expensive disk I/O. Risks of * deadlock or data inconsistency and a more complex model have been a further argument for that model. In fact * previously we wasted a lot of resources as way too many threads have been created without doing actual work as well * the write operations got triggered way too often specially for the very frequent changes at SequenceNumberMap * * * @param The type of the {@link PersistableEnvelope} to be written or read from disk */ @Slf4j public class PersistenceManager { /////////////////////////////////////////////////////////////////////////////////////////// // Static /////////////////////////////////////////////////////////////////////////////////////////// public static final Map> ALL_PERSISTENCE_MANAGERS = new HashMap<>(); private static boolean flushAtShutdownCalled; public static final AtomicBoolean allServicesInitialized = new AtomicBoolean(false); public static void onAllServicesInitialized() { allServicesInitialized.set(true); ALL_PERSISTENCE_MANAGERS.values().forEach(persistenceManager -> { // In case we got a requestPersistence call before we got initialized we trigger // the timer for the persist call if (persistenceManager.persistenceRequested) { persistenceManager.maybeStartTimerForPersistence(); } }); } public static void flushAllDataToDiskAtBackup(ResultHandler completeHandler) { flushAllDataToDisk(completeHandler, false); } public static void flushAllDataToDiskAtShutdown(ResultHandler completeHandler) { flushAllDataToDisk(completeHandler, true); } /** * Resets the static members of PersistenceManager to restart the application. */ public static void reset() { ALL_PERSISTENCE_MANAGERS.clear(); flushAtShutdownCalled = false; allServicesInitialized.set(false); } // 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. private static void flushAllDataToDisk(ResultHandler completeHandler, boolean doShutdown) { if (!allServicesInitialized.get()) { log.warn("Application has not completed start up yet so we do not flush data to disk."); completeHandler.handleResult(); return; } // We don't know from which thread we are called so we map to user thread UserThread.execute(() -> { if (doShutdown) { if (flushAtShutdownCalled) { log.warn("We got flushAllDataToDisk called again. This can happen in some rare cases. We ignore the repeated call."); return; } flushAtShutdownCalled = true; } log.info("Start flushAllDataToDisk"); 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. // We also check if we have called read already to avoid a very early write attempt before we have ever // read the data, which would lead to a write of empty data // (fixes https://github.com/bisq-network/bisq/issues/4844). if (persistenceManager.readCalled.get() && (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. try { persistenceManager.persistNow(() -> UserThread.execute(() -> onWriteCompleted(completeHandler, openInstances, persistenceManager, doShutdown))); } catch (Exception e) { if (!doShutdown) throw e; // only complete if shutting down log.warn("Error flushing data to disk on shut down. Calling completeHandler."); UserThread.execute(() -> onWriteCompleted(completeHandler, openInstances, persistenceManager, doShutdown)); } } else { onWriteCompleted(completeHandler, openInstances, persistenceManager, doShutdown); } }); }); } // We get called always from user thread here. private static void onWriteCompleted(ResultHandler completeHandler, AtomicInteger openInstances, PersistenceManager persistenceManager, boolean doShutdown) { if (doShutdown) { persistenceManager.shutdown(); } if (openInstances.decrementAndGet() == 0) { log.info("flushAllDataToDisk completed"); completeHandler.handleResult(); } } /////////////////////////////////////////////////////////////////////////////////////////// // Enum /////////////////////////////////////////////////////////////////////////////////////////// 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.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, 200, true), // 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 delay; @Getter private final boolean flushAtShutDown; Source(int numMaxBackupFiles, long delay, boolean flushAtShutDown) { this.numMaxBackupFiles = numMaxBackupFiles; this.delay = delay; this.flushAtShutDown = flushAtShutDown; } } /////////////////////////////////////////////////////////////////////////////////////////// // Class fields /////////////////////////////////////////////////////////////////////////////////////////// private final File dir; private final PersistenceProtoResolver persistenceProtoResolver; private final CorruptedStorageFileHandler corruptedStorageFileHandler; @Nullable private final KeyRing keyRing; private File storageFile; private T persistable; private String fileName; private Source source = Source.PRIVATE_LOW_PRIO; private Path usedTempFilePath; private volatile boolean persistenceRequested; @Nullable private Timer timer; private ExecutorService writeToDiskExecutor; public final AtomicBoolean initCalled = new AtomicBoolean(false); public final AtomicBoolean readCalled = new AtomicBoolean(false); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public PersistenceManager(@Named(Config.STORAGE_DIR) File dir, PersistenceProtoResolver persistenceProtoResolver, CorruptedStorageFileHandler corruptedStorageFileHandler, @Nullable KeyRing keyRing) { this.dir = checkDir(dir); this.persistenceProtoResolver = persistenceProtoResolver; this.corruptedStorageFileHandler = corruptedStorageFileHandler; this.keyRing = keyRing; } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void initialize(T persistable, Source source) { this.initialize(persistable, persistable.getDefaultStorageFileName(), source); } public void initialize(T persistable, String fileName, Source source) { if (flushAtShutdownCalled) { 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; storageFile = new File(dir, fileName); ALL_PERSISTENCE_MANAGERS.put(fileName, this); } public void shutdown() { ALL_PERSISTENCE_MANAGERS.remove(fileName); if (timer != null) { timer.stop(); } if (writeToDiskExecutor != null) { writeToDiskExecutor.shutdown(); } } /////////////////////////////////////////////////////////////////////////////////////////// // Reading file /////////////////////////////////////////////////////////////////////////////////////////// /** * Read persisted file in a thread. * * @param resultHandler Consumer of persisted data once it was read from disk. * @param orElse Called if no file exists or reading of file failed. */ public void readPersisted(Consumer resultHandler, Runnable orElse) { readPersisted(checkNotNull(fileName), resultHandler, orElse); } /** * Read persisted file in a thread. * We map result handler calls to UserThread, so clients don't need to worry about threading * * @param fileName File name of our persisted data. * @param resultHandler Consumer of persisted data once it was read from disk. * @param orElse Called if no file exists or reading of file failed. */ public void readPersisted(String fileName, Consumer resultHandler, Runnable orElse) { if (flushAtShutdownCalled) { 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) { UserThread.execute(() -> { resultHandler.accept(persisted); GcUtil.maybeReleaseMemory(); }); } else { UserThread.execute(orElse); } }, "PersistenceManager-read-" + fileName).start(); } // API for synchronous reading of data. Not recommended to be used in application code. // Currently used by tests and monitor. Should be converted to the threaded API as well. @Nullable public T getPersisted() { return getPersisted(checkNotNull(fileName)); } @Nullable public T getPersisted(String fileName) { if (flushAtShutdownCalled) { log.warn("We have started the shut down routine already. We ignore that getPersisted call."); return null; } if (keyRing != null && !keyRing.isUnlocked()) { log.warn("Account is not open yet, ignoring getPersisted."); return null; } readCalled.set(true); File storageFile = new File(dir, fileName); if (!storageFile.exists()) { return null; } long ts = System.currentTimeMillis(); try (FileInputStream fileInputStream = new FileInputStream(storageFile)) { protobuf.PersistableEnvelope proto; if (keyRing != null) { byte[] encryptedBytes = fileInputStream.readAllBytes(); try { byte[] decryptedBytes = Encryption.decryptPayloadWithHmac(encryptedBytes, keyRing.getSymmetricKey()); proto = protobuf.PersistableEnvelope.parseFrom(decryptedBytes); } catch (CryptoException ce) { log.warn("Expected encrypted persisted file, attempting to getPersisted without decryption"); ByteArrayInputStream bs = new ByteArrayInputStream(encryptedBytes); proto = protobuf.PersistableEnvelope.parseDelimitedFrom(bs); } } else { proto = protobuf.PersistableEnvelope.parseDelimitedFrom(fileInputStream); } //noinspection unchecked T persistableEnvelope = (T) persistenceProtoResolver.fromProto(proto); log.info("Reading {} completed in {} ms", fileName, System.currentTimeMillis() - ts); return persistableEnvelope; } catch (Throwable t) { log.error("Reading {} failed with {}.", fileName, t.getMessage(), t); try { // We keep a backup which might be used for recovery FileUtil.removeAndBackupFile(dir, storageFile, fileName, "backup_of_corrupted_data"); DevEnv.logErrorAndThrowIfDevMode(t.toString()); } catch (IOException e1) { e1.printStackTrace(); log.error(e1.getMessage()); // We swallow Exception if backup fails } if (corruptedStorageFileHandler != null) { corruptedStorageFileHandler.addFile(storageFile.getName()); } } return null; } /////////////////////////////////////////////////////////////////////////////////////////// // Write file to disk /////////////////////////////////////////////////////////////////////////////////////////// public void requestPersistence() { if (flushAtShutdownCalled) { log.warn("We have started the shut down routine already. We ignore that requestPersistence call."); try { throw new RuntimeException("We have started the shut down routine already. We ignore that requestPersistence call."); } catch (Exception e) { e.printStackTrace(); } return; } persistenceRequested = true; // If we have not initialized yet we postpone the start of the timer and call maybeStartTimerForPersistence at // onAllServicesInitialized if (!allServicesInitialized.get()) { return; } maybeStartTimerForPersistence(); } private void maybeStartTimerForPersistence() { // We write to disk with a delay to avoid frequent write operations. Depending on the priority those delays // can be rather long. UserThread.execute(() -> { if (timer == null) { timer = UserThread.runAfter(() -> { persistNow(null); UserThread.execute(() -> timer = null); }, source.delay, TimeUnit.MILLISECONDS); } }); } public void forcePersistNow() { // Tor Bridges and other settings are edited before app init completes, require persistNow to be forced, see writeToDisk() persistNow(null, true); } public void persistNow(@Nullable Runnable completeHandler) { persistNow(completeHandler, false); } private synchronized void persistNow(@Nullable Runnable completeHandler, boolean force) { long ts = System.currentTimeMillis(); try { // The serialisation is done on the user thread to avoid threading issue with potential mutations of the // persistable object. Keeping it on the user thread we are in a synchronize model. protobuf.PersistableEnvelope serialized = (protobuf.PersistableEnvelope) persistable.toPersistableMessage(); // For the write to disk task we use a thread. We do not have any issues anymore if the persistable objects // gets mutated while the thread is running as we have serialized it already and do not operate on the // reference to the persistable object. getWriteToDiskExecutor().execute(() -> writeToDisk(serialized, completeHandler, force)); long duration = System.currentTimeMillis() - ts; if (duration > 100) { log.info("Serializing {} took {} msec", fileName, duration); } } catch (Throwable e) { log.error("Error in saveToFile toProtoMessage: {}, {}", persistable.getClass().getSimpleName(), fileName); e.printStackTrace(); throw new RuntimeException(e); } } private void writeToDisk(protobuf.PersistableEnvelope serialized, @Nullable Runnable completeHandler, boolean force) { if (!allServicesInitialized.get() && !force) { log.warn("Application has not completed start up yet so we do not permit writing data to disk."); if (completeHandler != null) { UserThread.execute(completeHandler); } return; } if (keyRing != null && !keyRing.isUnlocked()) { log.warn("Account is not open, ignoring writeToDisk."); if (completeHandler != null) { UserThread.execute(completeHandler); } return; } long ts = System.currentTimeMillis(); File tempFile = null; FileOutputStream fileOutputStream = null; try { // Before we write we backup existing file FileUtil.rollingBackup(dir, fileName, source.getNumMaxBackupFiles()); if (!dir.exists() && !dir.mkdir()) log.warn("make dir failed {}", fileName); tempFile = usedTempFilePath != null ? FileUtil.createNewFile(usedTempFilePath) : File.createTempFile("temp_" + fileName, null, dir); // Don't use a new temp file path each time, as that causes the delete-on-exit hook to leak memory: tempFile.deleteOnExit(); fileOutputStream = new FileOutputStream(tempFile); if (keyRing != null) { byte[] encryptedBytes = Encryption.encryptPayloadWithHmac(serialized.toByteArray(), keyRing.getSymmetricKey()); fileOutputStream.write(encryptedBytes); } else { serialized.writeDelimitedTo(fileOutputStream); } // Attempt to force the bits to hit the disk. In reality the OS or hard disk itself may still decide // to not write through to physical media for at least a few seconds, but this is the best we can do. fileOutputStream.flush(); fileOutputStream.getFD().sync(); // Close resources before replacing file with temp file because otherwise it causes problems on windows // when rename temp file fileOutputStream.close(); FileUtil.renameFile(tempFile, storageFile); usedTempFilePath = tempFile.toPath(); } catch (Throwable t) { // If an error occurred, don't attempt to reuse this path again, in case temp file cleanup fails. usedTempFilePath = null; log.error("Error at saveToFile, storageFile={}", fileName, t); } finally { if (tempFile != null && tempFile.exists()) { log.warn("Temp file still exists after failed save. We will delete it now. storageFile={}", fileName); if (!tempFile.delete()) { log.error("Cannot delete temp file."); } } try { if (fileOutputStream != null) { fileOutputStream.close(); } } catch (IOException e) { // We swallow that e.printStackTrace(); log.error("Cannot close resources." + e.getMessage()); } long duration = System.currentTimeMillis() - ts; if (duration > 100) { log.info("Writing the serialized {} completed in {} msec", fileName, duration); } persistenceRequested = false; if (completeHandler != null) { UserThread.execute(completeHandler); } } } private ExecutorService getWriteToDiskExecutor() { if (writeToDiskExecutor == null) { String name = "Write-" + fileName + "_to-disk"; writeToDiskExecutor = SingleThreadExecutorUtils.getSingleThreadExecutor(name); } return writeToDiskExecutor; } @Override public String toString() { return "PersistenceManager{" + "\n fileName='" + fileName + '\'' + ",\n dir=" + dir + ",\n storageFile=" + storageFile + ",\n persistable=" + persistable + ",\n source=" + source + ",\n usedTempFilePath=" + usedTempFilePath + ",\n persistenceRequested=" + persistenceRequested + "\n}"; } } ================================================ FILE: common/src/main/java/haveno/common/proto/ProtoResolver.java ================================================ /* * 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 . */ package haveno.common.proto; import haveno.common.Payload; import haveno.common.proto.persistable.PersistablePayload; public interface ProtoResolver { Payload fromProto(protobuf.PaymentAccountPayload proto); PersistablePayload fromProto(protobuf.PersistableNetworkPayload proto); } ================================================ FILE: common/src/main/java/haveno/common/proto/ProtoUtil.java ================================================ /* * 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 . */ package haveno.common.proto; import com.google.common.base.Enums; import com.google.protobuf.ByteString; import com.google.protobuf.Message; import com.google.protobuf.ProtocolStringList; import haveno.common.Proto; import haveno.common.util.CollectionUtils; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @Slf4j public class ProtoUtil { public static Set byteSetFromProtoByteStringList(List byteStringList) { return byteStringList.stream().map(ByteString::toByteArray).collect(Collectors.toSet()); } /** * Returns the input String, except when it's the empty string: "", then null is returned. * Note: "" is the default value for a protobuffer string, so this means it's not filled in. */ @Nullable public static String stringOrNullFromProto(String proto) { return "".equals(proto) ? null : proto; } @Nullable public static byte[] byteArrayOrNullFromProto(ByteString proto) { return proto.isEmpty() ? null : proto.toByteArray(); } /** * Get a Java enum from a Protobuf enum in a safe way. * * @param enumType the class of the enum, e.g: BlaEnum.class * @param name the name of the enum entry, e.g: proto.getWinner().name() * @param the enum Type * @return an enum */ @Nullable public static > E enumFromProto(Class enumType, String name) { String enumName = name != null ? name : "UNDEFINED"; E result = Enums.getIfPresent(enumType, enumName).orNull(); if (result == null) { result = Enums.getIfPresent(enumType, "UNDEFINED").orNull(); log.debug("We try to lookup for an enum entry with name 'UNDEFINED' and use that if available, " + "otherwise the enum is null. enum={}", result); return result; } return result; } public static Iterable collectionToProto(Collection collection, Class messageType) { return collection.stream() .map(e -> { final Message message = e.toProtoMessage(); try { return messageType.cast(message); } catch (ClassCastException t) { log.error("Message could not be cast. message={}, messageType={}", message, messageType); return null; } }) .filter(Objects::nonNull) .collect(Collectors.toList()); } public static Iterable collectionToProto(Collection collection, Function extra) { return collection.stream().map(o -> extra.apply(o.toProtoMessage())).collect(Collectors.toList()); } public static List protocolStringListToList(ProtocolStringList protocolStringList) { return CollectionUtils.isEmpty(protocolStringList) ? new ArrayList<>() : new ArrayList<>(protocolStringList); } public static Set protocolStringListToSet(ProtocolStringList protocolStringList) { return CollectionUtils.isEmpty(protocolStringList) ? new HashSet<>() : new HashSet<>(protocolStringList); } } ================================================ FILE: common/src/main/java/haveno/common/proto/ProtobufferException.java ================================================ /* * 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 . */ package haveno.common.proto; import java.io.IOException; public class ProtobufferException extends IOException { public ProtobufferException(String message) { super(message); } public ProtobufferException(String message, Throwable e) { super(message, e); } } ================================================ FILE: common/src/main/java/haveno/common/proto/ProtobufferRuntimeException.java ================================================ /* * 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 . */ package haveno.common.proto; public class ProtobufferRuntimeException extends RuntimeException { public ProtobufferRuntimeException(String message) { super(message); } public ProtobufferRuntimeException(String message, Throwable e) { super(message, e); } } ================================================ FILE: common/src/main/java/haveno/common/proto/network/GetDataResponsePriority.java ================================================ /* * 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 . */ package haveno.common.proto.network; /** * Represents priority used at truncating data set at getDataResponse if total data exceeds limits. */ public enum GetDataResponsePriority { LOW, MID, HIGH } ================================================ FILE: common/src/main/java/haveno/common/proto/network/NetworkEnvelope.java ================================================ /* * 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 . */ package haveno.common.proto.network; import com.google.protobuf.Message; import haveno.common.Envelope; import lombok.EqualsAndHashCode; import static com.google.common.base.Preconditions.checkArgument; @EqualsAndHashCode public abstract class NetworkEnvelope implements Envelope { protected final String messageVersion; /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// protected NetworkEnvelope(String messageVersion) { this.messageVersion = messageVersion; } public protobuf.NetworkEnvelope.Builder getNetworkEnvelopeBuilder() { return protobuf.NetworkEnvelope.newBuilder().setMessageVersion(messageVersion); } @Override public Message toProtoMessage() { return getNetworkEnvelopeBuilder().build(); } public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { return getNetworkEnvelopeBuilder().build(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public String getMessageVersion() { // -1 is used for the case that we use an envelope message as payload (mailbox) // so we check only against 0 which is the default value if not set checkArgument(!messageVersion.equals("0"), "messageVersion is not set (0)."); return messageVersion; } @Override public String toString() { return "NetworkEnvelope{" + "\n messageVersion=" + messageVersion + "\n}"; } } ================================================ FILE: common/src/main/java/haveno/common/proto/network/NetworkPayload.java ================================================ /* * 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 . */ package haveno.common.proto.network; import haveno.common.Payload; /** * Interface for objects used inside WireEnvelope or other WirePayloads. */ public interface NetworkPayload extends Payload { default GetDataResponsePriority getGetDataResponsePriority() { return GetDataResponsePriority.LOW; } } ================================================ FILE: common/src/main/java/haveno/common/proto/network/NetworkProtoResolver.java ================================================ /* * 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 . */ package haveno.common.proto.network; import haveno.common.proto.ProtoResolver; import haveno.common.proto.ProtobufferException; import java.time.Clock; public interface NetworkProtoResolver extends ProtoResolver { NetworkEnvelope fromProto(protobuf.NetworkEnvelope proto) throws ProtobufferException; NetworkPayload fromProto(protobuf.StoragePayload proto); NetworkPayload fromProto(protobuf.StorageEntryWrapper proto); Clock getClock(); } ================================================ FILE: common/src/main/java/haveno/common/proto/persistable/NavigationPath.java ================================================ /* * 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 . */ package haveno.common.proto.persistable; import com.google.protobuf.Message; import haveno.common.util.CollectionUtils; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import java.util.List; @EqualsAndHashCode @AllArgsConstructor @NoArgsConstructor @Getter @Setter public class NavigationPath implements PersistableEnvelope { private List path = List.of(); @Override public Message toProtoMessage() { final protobuf.NavigationPath.Builder builder = protobuf.NavigationPath.newBuilder(); if (!CollectionUtils.isEmpty(path)) builder.addAllPath(path); return protobuf.PersistableEnvelope.newBuilder().setNavigationPath(builder).build(); } public static NavigationPath fromProto(protobuf.NavigationPath proto) { return new NavigationPath(List.copyOf(proto.getPathList())); } } ================================================ FILE: common/src/main/java/haveno/common/proto/persistable/PersistableEnvelope.java ================================================ /* * 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 . */ package haveno.common.proto.persistable; import com.google.protobuf.Message; import haveno.common.Envelope; /** * Interface for the outside envelope object persisted to disk. */ public interface PersistableEnvelope extends Envelope { default Message toPersistableMessage() { return toProtoMessage(); } default String getDefaultStorageFileName() { return this.getClass().getSimpleName(); } } ================================================ FILE: common/src/main/java/haveno/common/proto/persistable/PersistableList.java ================================================ /* * 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 . */ package haveno.common.proto.persistable; import lombok.Getter; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.function.Consumer; import java.util.stream.Stream; public abstract class PersistableList implements PersistableEnvelope { @Getter public final List list = createList(); protected List createList() { return new ArrayList<>(); } public PersistableList() { } protected PersistableList(Collection collection) { setAll(collection); } public void setAll(Collection collection) { synchronized (this.list) { this.list.clear(); this.list.addAll(collection); } } public boolean add(T item) { synchronized (list) { if (!list.contains(item)) { list.add(item); return true; } return false; } } public boolean remove(T item) { synchronized (list) { return list.remove(item); } } public Stream stream() { synchronized (list) { return list.stream(); } } public int size() { synchronized (list) { return list.size(); } } public boolean contains(T item) { synchronized (list) { return list.contains(item); } } public boolean isEmpty() { synchronized (list) { return list.isEmpty(); } } public void forEach(Consumer action) { synchronized (list) { list.forEach(action); } } public void clear() { synchronized (list) { list.clear(); } } } ================================================ FILE: common/src/main/java/haveno/common/proto/persistable/PersistableListAsObservable.java ================================================ /* * 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 . */ package haveno.common.proto.persistable; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import java.util.Collection; import java.util.List; public abstract class PersistableListAsObservable extends PersistableList { public PersistableListAsObservable() { } protected PersistableListAsObservable(Collection collection) { super(collection); } protected List createList() { return FXCollections.observableArrayList(); } public ObservableList getObservableList() { return (ObservableList) getList(); } public void addListener(ListChangeListener listener) { ((ObservableList) getList()).addListener(listener); } public void removeListener(ListChangeListener listener) { ((ObservableList) getList()).removeListener(listener); } } ================================================ FILE: common/src/main/java/haveno/common/proto/persistable/PersistablePayload.java ================================================ /* * 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 . */ package haveno.common.proto.persistable; import haveno.common.Payload; /** * Interface for objects used inside Envelope or other Payloads. */ public interface PersistablePayload extends Payload { } ================================================ FILE: common/src/main/java/haveno/common/proto/persistable/PersistedDataHost.java ================================================ /* * 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 . */ package haveno.common.proto.persistable; public interface PersistedDataHost { void readPersisted(Runnable completeHandler); } ================================================ FILE: common/src/main/java/haveno/common/proto/persistable/PersistenceProtoResolver.java ================================================ /* * 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 . */ package haveno.common.proto.persistable; import haveno.common.proto.ProtoResolver; public interface PersistenceProtoResolver extends ProtoResolver { PersistableEnvelope fromProto(protobuf.PersistableEnvelope persistable); } ================================================ FILE: common/src/main/java/haveno/common/reactfx/FxTimer.java ================================================ package haveno.common.reactfx; import javafx.animation.Animation; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.util.Duration; /** * Provides factory methods for timers that are manipulated from and execute * their action on the JavaFX application thread. * * Copied from: * https://github.com/TomasMikula/ReactFX/blob/537fffdbb2958a77dfbca08b712bb2192862e960/reactfx/src/main/java/org/reactfx/util/FxTimer.java * */ public class FxTimer implements Timer { /** * Prepares a (stopped) timer that lasts for {@code delay} and whose action runs when timer ends. */ public static Timer create(java.time.Duration delay, Runnable action) { return new FxTimer(delay, delay, action, 1); } /** * Equivalent to {@code create(delay, action).restart()}. */ public static Timer runLater(java.time.Duration delay, Runnable action) { Timer timer = create(delay, action); timer.restart(); return timer; } /** * Prepares a (stopped) timer that lasts for {@code interval} and that executes the given action periodically * when the timer ends. */ public static Timer createPeriodic(java.time.Duration interval, Runnable action) { return new FxTimer(interval, interval, action, Animation.INDEFINITE); } /** * Equivalent to {@code createPeriodic(interval, action).restart()}. */ public static Timer runPeriodically(java.time.Duration interval, Runnable action) { Timer timer = createPeriodic(interval, action); timer.restart(); return timer; } /** * Prepares a (stopped) timer that lasts for {@code interval} and that executes the given action periodically * when the timer starts. */ public static Timer createPeriodic0(java.time.Duration interval, Runnable action) { return new FxTimer(java.time.Duration.ZERO, interval, action, Animation.INDEFINITE); } /** * Equivalent to {@code createPeriodic0(interval, action).restart()}. */ public static Timer runPeriodically0(java.time.Duration interval, Runnable action) { Timer timer = createPeriodic0(interval, action); timer.restart(); return timer; } private final Duration actionTime; private final Timeline timeline; private final Runnable action; private long seq = 0; private FxTimer(java.time.Duration actionTime, java.time.Duration period, Runnable action, int cycles) { this.actionTime = Duration.millis(actionTime.toMillis()); this.timeline = new Timeline(); this.action = action; timeline.getKeyFrames().add(new KeyFrame(this.actionTime)); // used as placeholder if (period != actionTime) { timeline.getKeyFrames().add(new KeyFrame(Duration.millis(period.toMillis()))); } timeline.setCycleCount(cycles); } @Override public void restart() { stop(); long expected = seq; timeline.getKeyFrames().set(0, new KeyFrame(actionTime, ae -> { if(seq == expected) { action.run(); } })); timeline.play(); } @Override public void stop() { timeline.stop(); ++seq; } } ================================================ FILE: common/src/main/java/haveno/common/reactfx/LICENSE ================================================ Copyright (c) 2013-2014, Tomas Mikula All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: common/src/main/java/haveno/common/reactfx/README.md ================================================ This package is a very minimal subset of the external library `org.reactfx`. Two small files from `org.reactfx` were embedded into the project to avoid having it as dependency: [https://github.com/TomasMikula/ReactFX] ================================================ FILE: common/src/main/java/haveno/common/reactfx/Timer.java ================================================ package haveno.common.reactfx; /** * Timer represents a delayed action. This means that every timer has an * associated action and an associated delay. Action and delay are specified * on timer creation. * *

    Every timer also has an associated thread (such as JavaFX application * thread or a single-thread executor's thread). Timer may only be accessed * from its associated thread. Timer's action is executed on its associated * thread, too. This design allows to implement guarantees provided by * {@link #stop()}. * * Copied from: * https://raw.githubusercontent.com/TomasMikula/ReactFX/537fffdbb2958a77dfbca08b712bb2192862e960/reactfx/src/main/java/org/reactfx/util/Timer.java* */ public interface Timer { /** * Schedules the associated action to be executed after the associated * delay. If the action is already scheduled but hasn't been executed yet, * the timeout is reset, so that the action won't be executed before the * full delay from now. */ void restart(); /** * If the associated action has been scheduled for execution but not yet * executed, this method prevents it from being executed at all. This is * also true in case the timer's timeout has already expired, but the * associated action hasn't had a chance to be executed on the associated * thread. Note that this is a stronger guarantee than the one given by * {@link javafx.animation.Animation#stop()}: * *

         * {@code
         * Timeline timeline = new Timeline(new KeyFrame(
         *         Duration.millis(1000),
         *         ae -> System.out.println("FIRED ANYWAY")));
         * timeline.play();
         *
         * // later on the JavaFX application thread,
         * // but still before the action has been executed
         * timeline.stop();
         *
         * // later, "FIRED ANYWAY" may still be printed
         * }
         * 
    * * In contrast, using the {@link FxTimer}, the action is guaranteed not to * be executed after {@code stop()}: *
         * {@code
         * Timer timer = FxTimer.runLater(
         *         Duration.ofMillis(1000),
         *         () -> System.out.println("FIRED"));
         *
         * // later on the JavaFX application thread,
         * // but still before the action has been executed
         * timer.stop();
         *
         * // "FIRED" is guaranteed *not* to be printed
         * }
         * 
    */ void stop(); } ================================================ FILE: common/src/main/java/haveno/common/setup/CommonSetup.java ================================================ /* * 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 . */ package haveno.common.setup; import ch.qos.logback.classic.Level; import haveno.common.UserThread; import haveno.common.app.AsciiLogo; import haveno.common.app.DevEnv; import haveno.common.app.Log; import haveno.common.app.Version; import haveno.common.config.Config; import haveno.common.util.Profiler; import haveno.common.util.Utilities; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; import org.bitcoinj.store.BlockStoreException; import sun.misc.Signal; import java.net.URISyntaxException; import java.nio.file.Paths; import java.util.concurrent.TimeUnit; @Slf4j public class CommonSetup { public static void setup(Config config, GracefulShutDownHandler gracefulShutDownHandler) { setupLog(config); AsciiLogo.showAsciiLogo(); Version.setBaseCryptoNetworkId(config.baseCurrencyNetwork.ordinal()); Version.printVersion(); maybePrintPathOfCodeSource(); Profiler.printSystemLoad(); setSystemProperties(); setupSigIntHandlers(gracefulShutDownHandler); DevEnv.setup(config); } public static void printSystemLoadPeriodically(int delayMin) { UserThread.runPeriodically(Profiler::printSystemLoad, delayMin, TimeUnit.MINUTES); } public static void setupUncaughtExceptionHandler(UncaughtExceptionHandler uncaughtExceptionHandler) { Thread.UncaughtExceptionHandler handler = (thread, throwable) -> { // Might come from another thread if (throwable.getCause() != null && throwable.getCause().getCause() != null && throwable.getCause().getCause() instanceof BlockStoreException) { log.error(throwable.getMessage()); } else if (throwable instanceof ClassCastException && "sun.awt.image.BufImgSurfaceData cannot be cast to sun.java2d.xr.XRSurfaceData".equals(throwable.getMessage())) { log.warn(throwable.getMessage()); } else if (throwable instanceof UnsupportedOperationException && "The system tray is not supported on the current platform.".equals(throwable.getMessage())) { log.warn(throwable.getMessage()); } else { log.error("Uncaught Exception from thread {}, throwableClass={}, throwableMessage={}", Thread.currentThread().getName(), throwable.getClass(), throwable.getMessage()); log.error("Stack trace:\n" + ExceptionUtils.getStackTrace(throwable)); throwable.printStackTrace(); UserThread.execute(() -> uncaughtExceptionHandler.handleUncaughtException(throwable, false)); } }; Thread.setDefaultUncaughtExceptionHandler(handler); Thread.currentThread().setUncaughtExceptionHandler(handler); } private static void setupLog(Config config) { String logPath = Paths.get(config.appDataDir.getPath(), "haveno").toString(); Log.setup(logPath); Utilities.printSysInfo(); Log.setLevel(Level.toLevel(config.logLevel)); } protected static void setSystemProperties() { if (Utilities.isLinux()) System.setProperty("prism.lcdtext", "false"); } protected static void setupSigIntHandlers(GracefulShutDownHandler gracefulShutDownHandler) { Signal.handle(new Signal("INT"), signal -> { log.info("Received {}", signal); UserThread.execute(() -> gracefulShutDownHandler.gracefulShutDown(() -> { })); }); Signal.handle(new Signal("TERM"), signal -> { log.info("Received {}", signal); UserThread.execute(() -> gracefulShutDownHandler.gracefulShutDown(() -> { })); }); } protected static void maybePrintPathOfCodeSource() { try { final String pathOfCodeSource = Utilities.getPathOfCodeSource(); if (!pathOfCodeSource.endsWith("classes")) log.info("Path to Haveno jar file: " + pathOfCodeSource); } catch (URISyntaxException e) { log.error(ExceptionUtils.getStackTrace(e)); } } } ================================================ FILE: common/src/main/java/haveno/common/setup/GracefulShutDownHandler.java ================================================ /* * 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 . */ package haveno.common.setup; import haveno.common.handlers.ResultHandler; public interface GracefulShutDownHandler { void gracefulShutDown(ResultHandler resultHandler); void gracefulShutDown(ResultHandler resultHandler, boolean systemExit); } ================================================ FILE: common/src/main/java/haveno/common/setup/UncaughtExceptionHandler.java ================================================ /* * 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 . */ package haveno.common.setup; public interface UncaughtExceptionHandler { void handleUncaughtException(Throwable throwable, boolean doShutDown); } ================================================ FILE: common/src/main/java/haveno/common/taskrunner/InterceptTaskException.java ================================================ /* * 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 . */ package haveno.common.taskrunner; public class InterceptTaskException extends RuntimeException { public InterceptTaskException(String message) { super(message); } } ================================================ FILE: common/src/main/java/haveno/common/taskrunner/Model.java ================================================ /* * 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 . */ package haveno.common.taskrunner; public interface Model { void onComplete(); } ================================================ FILE: common/src/main/java/haveno/common/taskrunner/Task.java ================================================ /* * 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 . */ package haveno.common.taskrunner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public abstract class Task { private static final Logger log = LoggerFactory.getLogger(Task.class); public static Class taskToIntercept; protected final TaskRunner taskHandler; protected final T model; protected String errorMessage = "An error occurred at task: " + getClass().getSimpleName(); protected boolean completed; public Task(TaskRunner taskHandler, T model) { this.taskHandler = taskHandler; this.model = model; } protected abstract void run(); protected void runInterceptHook() { if (getClass() == taskToIntercept) throw new InterceptTaskException("Task intercepted for testing purpose. Task = " + getClass().getSimpleName()); } protected void appendToErrorMessage(String message) { errorMessage += "\n" + message; } protected void appendExceptionToErrorMessage(Throwable t) { if (t.getMessage() != null) errorMessage += "\nException message: " + t.getMessage(); else errorMessage += "\nException: " + t.toString(); } protected void complete() { completed = true; taskHandler.handleComplete(); } public boolean isCompleted() { return completed; } protected void failed(String message) { appendToErrorMessage(message); failed(); } protected void failed(Throwable t) { // // append stacktrace to error message (only for development) // StringWriter sw = new StringWriter(); // PrintWriter pw = new PrintWriter(sw); // t.printStackTrace(pw); // errorMessage = sw.toString(); if (taskHandler.isCanceled()) return; errorMessage = t.getMessage() + " (task " + getClass().getSimpleName() + ")"; log.error(errorMessage, t); taskHandler.handleErrorMessage(errorMessage); } protected void failed() { log.error(errorMessage); taskHandler.handleErrorMessage(errorMessage); } } ================================================ FILE: common/src/main/java/haveno/common/taskrunner/TaskRunner.java ================================================ /* * 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 . */ package haveno.common.taskrunner; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import lombok.extern.slf4j.Slf4j; import java.util.Arrays; import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; import org.apache.commons.lang3.exception.ExceptionUtils; @Slf4j public class TaskRunner { private final Queue>> tasks = new LinkedBlockingQueue<>(); private final T sharedModel; private final Class sharedModelClass; private final ResultHandler resultHandler; private final ErrorMessageHandler errorMessageHandler; private boolean failed = false; private boolean isCanceled; private Class> currentTask; public TaskRunner(T sharedModel, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { //noinspection unchecked this(sharedModel, (Class) sharedModel.getClass(), resultHandler, errorMessageHandler); } public TaskRunner(T sharedModel, Class sharedModelClass, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { this.sharedModel = sharedModel; this.resultHandler = resultHandler; this.errorMessageHandler = errorMessageHandler; this.sharedModelClass = sharedModelClass; } @SafeVarargs public final void addTasks(Class>... items) { tasks.addAll(Arrays.asList(items)); } public void run() { next(); } private void next() { if (!failed && !isCanceled) { if (tasks.size() > 0) { try { currentTask = tasks.poll(); log.info("Run task: " + currentTask.getSimpleName()); currentTask.getDeclaredConstructor(TaskRunner.class, sharedModelClass).newInstance(this, sharedModel).run(); } catch (Throwable throwable) { log.error(ExceptionUtils.getStackTrace(throwable)); handleErrorMessage("Error at taskRunner, error=" + throwable.getMessage()); } } else { resultHandler.handleResult(); } } } public void cancel() { isCanceled = true; } public boolean isCanceled() { return isCanceled; } void handleComplete() { next(); } void handleErrorMessage(String errorMessage) { if (isCanceled) return; log.error("Task failed: " + currentTask.getSimpleName() + " / errorMessage: " + errorMessage); failed = true; errorMessageHandler.handleErrorMessage(errorMessage); } } ================================================ FILE: common/src/main/java/haveno/common/util/Base64.java ================================================ /* * 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 . */ package haveno.common.util; /** * We use Java 8 builtin Base64 because it is much faster than Guava and Apache versions: * http://java-performance.info/base64-encoding-and-decoding-performance/ */ public class Base64 { public static byte[] decode(String base64) { return java.util.Base64.getDecoder().decode(base64); } public static String encode(byte[] bytes) { return java.util.Base64.getEncoder().encodeToString(bytes); } } ================================================ FILE: common/src/main/java/haveno/common/util/CollectionUtils.java ================================================ package haveno.common.util; import java.util.Collection; import java.util.Map; /** * Collection utility methods copied from Spring Framework v4.3.6's * {@code org.springframework.util.CollectionUtils} class in order to make it possible to * drop Haveno's dependency on Spring altogether. The name of the class and methods have * been preserved here to minimize the impact to the Haveno codebase of making this change. * All that is necessary to swap this implementation in is to change the CollectionUtils * import statement. */ public class CollectionUtils { /** * Return {@code true} if the supplied Collection is {@code null} or empty. * Otherwise, return {@code false}. * @param collection the Collection to check * @return whether the given Collection is empty */ public static boolean isEmpty(Collection collection) { return (collection == null || collection.isEmpty()); } /** * Return {@code true} if the supplied Map is {@code null} or empty. * Otherwise, return {@code false}. * @param map the Map to check * @return whether the given Map is empty */ public static boolean isEmpty(Map map) { return (map == null || map.isEmpty()); } } ================================================ FILE: common/src/main/java/haveno/common/util/CompletableFutureUtils.java ================================================ /* * 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 . */ package haveno.common.util; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; public class CompletableFutureUtils { /** * @param list List of futures * @param The generic type of the future * @return Returns a CompletableFuture with a list of the futures we got as parameter once all futures * are completed (incl. exceptionally completed). */ public static CompletableFuture> allOf(List> list) { CompletableFuture allFuturesResult = CompletableFuture.allOf(list.toArray(new CompletableFuture[list.size()])); return allFuturesResult.thenApply(v -> list.stream(). map(CompletableFuture::join). collect(Collectors.toList()) ); } } ================================================ FILE: common/src/main/java/haveno/common/util/DesktopUtil.java ================================================ /* * 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 . */ package haveno.common.util; import lombok.extern.slf4j.Slf4j; import java.io.File; import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.List; // Taken form https://stackoverflow.com/questions/18004150/desktop-api-is-not-supported-on-the-current-platform, // originally net.mightypork.rpack.utils.DesktopApi @Slf4j class DesktopUtil { public static boolean browse(URI uri) { return openSystemSpecific(uri.toString()); } public static boolean open(File file) { return openSystemSpecific(file.getPath()); } public static boolean edit(File file) { // you can try something like // runCommand("gimp", "%s", file.getPath()) // based on user preferences. return openSystemSpecific(file.getPath()); } private static boolean openSystemSpecific(String what) { EnumOS os = getOs(); if (os.isLinux()) { if (runCommand("kde-open", "%s", what)) return true; if (runCommand("gnome-open", "%s", what)) return true; if (runCommand("xdg-open", "%s", what)) return true; } if (os.isMac()) { if (runCommand("open", "%s", what)) return true; } if (os.isWindows()) { return runCommand("explorer", "%s", "\"" + what + "\""); } return false; } @SuppressWarnings("SameParameterValue") private static boolean runCommand(String command, String args, String file) { log.info("Trying to exec: cmd = {} args = {} file = {}", command, args, file); String[] parts = prepareCommand(command, args, file); try { Process p = Runtime.getRuntime().exec(parts); if (p == null) return false; try { int value = p.exitValue(); if (value == 0) { log.warn("Process ended immediately."); } else { log.warn("Process crashed."); } return false; } catch (IllegalThreadStateException e) { log.info("Process is running."); return true; } } catch (IOException e) { log.warn("Error running command. {}", e.toString()); return false; } } private static String[] prepareCommand(String command, String args, String file) { List parts = new ArrayList<>(); parts.add(command); if (args != null) { for (String s : args.split(" ")) { s = String.format(s, file); // put in the filename thing parts.add(s.trim()); } } return parts.toArray(new String[parts.size()]); } public enum EnumOS { linux, macos, solaris, unknown, windows; public boolean isLinux() { return this == linux || this == solaris; } public boolean isMac() { return this == macos; } public boolean isWindows() { return this == windows; } } private static EnumOS getOs() { String s = System.getProperty("os.name").toLowerCase(); if (s.contains("win")) { return EnumOS.windows; } if (s.contains("mac")) { return EnumOS.macos; } if (s.contains("solaris")) { return EnumOS.solaris; } if (s.contains("sunos")) { return EnumOS.solaris; } if (s.contains("linux")) { return EnumOS.linux; } if (s.contains("unix")) { return EnumOS.linux; } else { return EnumOS.unknown; } } } ================================================ FILE: common/src/main/java/haveno/common/util/DoubleSummaryStatisticsWithStdDev.java ================================================ /* * 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 . */ package haveno.common.util; import java.util.DoubleSummaryStatistics; /* Adds logic to DoubleSummaryStatistics for keeping track of sum of squares * and computing population variance and population standard deviation. * Kahan summation algorithm (for `getSumOfSquares`) sourced from the DoubleSummaryStatistics class. * Incremental variance algorithm sourced from https://math.stackexchange.com/a/1379804/316756 */ public class DoubleSummaryStatisticsWithStdDev extends DoubleSummaryStatistics { private double sumOfSquares; private double sumOfSquaresCompensation; // Low order bits of sum of squares private double simpleSumOfSquares; // Used to compute right sum of squares for non-finite inputs @Override public void accept(double value) { super.accept(value); double valueSquared = value * value; simpleSumOfSquares += valueSquared; sumOfSquaresWithCompensation(valueSquared); } public void combine(DoubleSummaryStatisticsWithStdDev other) { super.combine(other); simpleSumOfSquares += other.simpleSumOfSquares; sumOfSquaresWithCompensation(other.sumOfSquares); sumOfSquaresWithCompensation(other.sumOfSquaresCompensation); } /* Incorporate a new squared double value using Kahan summation / * compensated summation. */ private void sumOfSquaresWithCompensation(double valueSquared) { double tmp = valueSquared - sumOfSquaresCompensation; double velvel = sumOfSquares + tmp; // Little wolf of rounding error sumOfSquaresCompensation = (velvel - sumOfSquares) - tmp; sumOfSquares = velvel; } private double getSumOfSquares() { // Better error bounds to add both terms as the final sum of squares double tmp = sumOfSquares + sumOfSquaresCompensation; if (Double.isNaN(tmp) && Double.isInfinite(simpleSumOfSquares)) // If the compensated sum of squares is spuriously NaN from // accumulating one or more same-signed infinite values, // return the correctly-signed infinity stored in // simpleSumOfSquares. return simpleSumOfSquares; else return tmp; } private double getVariance() { double sumOfSquares = getSumOfSquares(); long count = getCount(); double mean = getAverage(); return (sumOfSquares / count) - (mean * mean); } public final double getStandardDeviation() { double variance = getVariance(); return Math.sqrt(variance); } } ================================================ FILE: common/src/main/java/haveno/common/util/ExtraDataMapValidator.java ================================================ /* * 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 . */ package haveno.common.util; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.util.HashMap; import java.util.Map; import static com.google.common.base.Preconditions.checkArgument; /** * Validator for extraDataMap fields used in network payloads. * Ensures that we don't get the network attacked by huge data inserted there. */ @Slf4j public class ExtraDataMapValidator { // ExtraDataMap is only used for exceptional cases to not break backward compatibility. // We don't expect many entries there. public final static int MAX_SIZE = 10; public final static int MAX_KEY_LENGTH = 100; public final static int MAX_VALUE_LENGTH = 100000; // 100 kb public static Map getValidatedExtraDataMap(@Nullable Map extraDataMap) { return getValidatedExtraDataMap(extraDataMap, MAX_SIZE, MAX_KEY_LENGTH, MAX_VALUE_LENGTH); } public static Map getValidatedExtraDataMap(@Nullable Map extraDataMap, int maxSize, int maxKeyLength, int maxValueLength) { if (extraDataMap == null) return null; try { checkArgument(extraDataMap.entrySet().size() <= maxSize, "Size of map must not exceed " + maxSize); extraDataMap.forEach((key, value) -> { checkArgument(key.length() <= maxKeyLength, "Length of key must not exceed " + maxKeyLength); checkArgument(value.length() <= maxValueLength, "Length of value must not exceed " + maxValueLength); }); return extraDataMap; } catch (Throwable t) { return new HashMap<>(); } } public static void validate(@Nullable Map extraDataMap) { validate(extraDataMap, MAX_SIZE, MAX_KEY_LENGTH, MAX_VALUE_LENGTH); } public static void validate(@Nullable Map extraDataMap, int maxSize, int maxKeyLength, int maxValueLength) { if (extraDataMap == null) return; checkArgument(extraDataMap.entrySet().size() <= maxSize, "Size of map must not exceed " + maxSize); extraDataMap.forEach((key, value) -> { checkArgument(key.length() <= maxKeyLength, "Length of key must not exceed " + maxKeyLength); checkArgument(value.length() <= maxValueLength, "Length of value must not exceed " + maxValueLength); }); } } ================================================ FILE: common/src/main/java/haveno/common/util/GcUtil.java ================================================ /* * 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 . */ package haveno.common.util; import haveno.common.UserThread; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @Slf4j public class GcUtil { @Setter private static boolean DISABLE_GC_CALLS = false; private static final int TRIGGER_MEM = 1000; private static final int TRIGGER_MAX_MEM = 3000; private static int totalInvocations; private static long totalGCTime; public static void autoReleaseMemory() { if (DISABLE_GC_CALLS) return; autoReleaseMemory(TRIGGER_MEM); } public static void maybeReleaseMemory() { if (DISABLE_GC_CALLS) return; maybeReleaseMemory(TRIGGER_MAX_MEM); } /** * @param trigger Threshold for free memory in MB when we invoke the garbage collector */ private static void autoReleaseMemory(long trigger) { UserThread.runPeriodically(() -> maybeReleaseMemory(trigger), 120); } /** * @param trigger Threshold for free memory in MB when we invoke the garbage collector */ private static void maybeReleaseMemory(long trigger) { long ts = System.currentTimeMillis(); long preGcMemory = Runtime.getRuntime().totalMemory(); if (preGcMemory > trigger * 1024 * 1024) { System.gc(); totalInvocations++; long postGcMemory = Runtime.getRuntime().totalMemory(); long duration = System.currentTimeMillis() - ts; totalGCTime += duration; log.info("GC reduced memory by {}. Total memory before/after: {}/{}. Free memory: {}. Took {} ms. Total GC invocations: {} / Total GC time {} sec", Utilities.readableFileSize(preGcMemory - postGcMemory), Utilities.readableFileSize(preGcMemory), Utilities.readableFileSize(postGcMemory), Utilities.readableFileSize(Runtime.getRuntime().freeMemory()), duration, totalInvocations, totalGCTime / 1000d); /* if (DevEnv.isDevMode()) { try { // To see from where we got called throw new RuntimeException("Dummy Exception for print stacktrace at maybeReleaseMemory"); } catch (Throwable t) { t.printStackTrace(); } }*/ } } } ================================================ FILE: common/src/main/java/haveno/common/util/Hex.java ================================================ /* * 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 . */ package haveno.common.util; import com.google.common.io.BaseEncoding; public class Hex { public static byte[] decode(String hex) { return BaseEncoding.base16().lowerCase().decode(hex.toLowerCase()); } public static String encode(byte[] bytes) { return BaseEncoding.base16().lowerCase().encode(bytes); } } ================================================ FILE: common/src/main/java/haveno/common/util/InvalidVersionException.java ================================================ /* * 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 . */ package haveno.common.util; public class InvalidVersionException extends Exception { public InvalidVersionException(String msg) { super(msg); } } ================================================ FILE: common/src/main/java/haveno/common/util/JsonExclude.java ================================================ /* * 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 . */ package haveno.common.util; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface JsonExclude { } ================================================ FILE: common/src/main/java/haveno/common/util/MathUtils.java ================================================ /* * 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 . */ package haveno.common.util; import com.google.common.math.DoubleMath; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; import java.util.ArrayDeque; import java.util.Deque; import java.util.Optional; import static com.google.common.base.Preconditions.checkArgument; public class MathUtils { private static final Logger log = LoggerFactory.getLogger(MathUtils.class); public static double roundDouble(double value, int precision) { return roundDouble(value, precision, RoundingMode.HALF_UP); } @SuppressWarnings("SameParameterValue") public static double roundDouble(double value, int precision, RoundingMode roundingMode) { if (precision < 0) throw new IllegalArgumentException(); if (!Double.isFinite(value)) throw new IllegalArgumentException("Expected a finite double, but found " + value); try { BigDecimal bd = BigDecimal.valueOf(value); bd = bd.setScale(precision, roundingMode); return bd.doubleValue(); } catch (Throwable t) { log.error(t.toString()); return 0; } } public static long roundDoubleToLong(double value) { return roundDoubleToLong(value, RoundingMode.HALF_UP); } @SuppressWarnings("SameParameterValue") public static long roundDoubleToLong(double value, RoundingMode roundingMode) { return DoubleMath.roundToLong(value, roundingMode); } public static int roundDoubleToInt(double value) { return roundDoubleToInt(value, RoundingMode.HALF_UP); } @SuppressWarnings("SameParameterValue") public static int roundDoubleToInt(double value, RoundingMode roundingMode) { return DoubleMath.roundToInt(value, roundingMode); } public static long doubleToLong(double value) { return Double.valueOf(value).longValue(); } public static double scaleUpByPowerOf10(double value, int exponent) { double factor = Math.pow(10, exponent); return value * factor; } public static double scaleUpByPowerOf10(long value, int exponent) { double factor = Math.pow(10, exponent); return ((double) value) * factor; } public static BigInteger scaleUpByPowerOf10(BigInteger value, int exponent) { BigInteger factor = BigInteger.TEN.pow(exponent); return value.multiply(factor); } public static double scaleDownByPowerOf10(double value, int exponent) { double factor = Math.pow(10, exponent); return value / factor; } public static double scaleDownByPowerOf10(long value, int exponent) { double factor = Math.pow(10, exponent); return ((double) value) / factor; } public static BigInteger scaleDownByPowerOf10(BigInteger value, int exponent) { BigInteger factor = BigInteger.TEN.pow(exponent); return value.divide(factor); } public static double exactMultiply(double value1, double value2) { return BigDecimal.valueOf(value1).multiply(BigDecimal.valueOf(value2)).doubleValue(); } public static long getMedian(Long[] list) { if (list.length == 0) { return 0L; } int middle = list.length / 2; long median; if (list.length % 2 == 1) { median = list[middle]; } else { median = MathUtils.roundDoubleToLong((list[middle - 1] + list[middle]) / 2.0); } return median; } public static class MovingAverage { final Deque window; private final int size; private long sum; private final double outlier; // Outlier as ratio public MovingAverage(int size, double outlier) { this.size = size; window = new ArrayDeque<>(size); this.outlier = outlier; sum = 0; } public Optional next(long val) { try { var fullAtStart = isFull(); if (fullAtStart) { if (outlier > 0) { // Return early if it's an outlier checkArgument(size != 0); var avg = (double) sum / size; if (Math.abs(avg - val) / avg > outlier) { return Optional.empty(); } } sum -= window.remove(); } window.add(val); sum += val; if (!fullAtStart && isFull() && outlier != 0) { removeInitialOutlier(); } // When discarding outliers, the first n non discarded elements return Optional.empty() return outlier > 0 && !isFull() ? Optional.empty() : current(); } catch (Throwable t) { log.error(t.toString()); return Optional.empty(); } } boolean isFull() { return window.size() == size; } private void removeInitialOutlier() { var element = window.iterator(); while (element.hasNext()) { var val = element.next(); int div = size - 1; checkArgument(div != 0); var avgExVal = (double) (sum - val) / div; if (Math.abs(avgExVal - val) / avgExVal > outlier) { element.remove(); break; } } } public Optional current() { return window.size() == 0 ? Optional.empty() : Optional.of((double) sum / window.size()); } } } ================================================ FILE: common/src/main/java/haveno/common/util/PermutationUtil.java ================================================ /* * 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 . */ package haveno.common.util; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiPredicate; @Slf4j public class PermutationUtil { /** * @param list Original list * @param indicesToRemove List of indices to remove * @param Type of List items * @return Partial list where items at indices of indicesToRemove have been removed */ public static List getPartialList(List list, List indicesToRemove) { List altered = new ArrayList<>(list); // Eliminate duplicates indicesToRemove = new ArrayList<>(new HashSet<>(indicesToRemove)); // Sort Collections.sort(indicesToRemove); // Reverse list. // We need to remove from highest index downwards to not change order of remaining indices Collections.reverse(indicesToRemove); indicesToRemove.forEach(index -> { if (altered.size() > index && index >= 0) altered.remove((int) index); }); return altered; } public static List findMatchingPermutation(R targetValue, List list, BiPredicate> predicate, int maxIterations) { if (predicate.test(targetValue, list)) { return list; } else { return findMatchingPermutation(targetValue, list, new ArrayList<>(), predicate, new AtomicInteger(maxIterations)); } } private static List findMatchingPermutation(R targetValue, List list, List> lists, BiPredicate> predicate, AtomicInteger maxIterations) { for (int level = 0; level < list.size(); level++) { // Test one level at a time var result = checkLevel(targetValue, list, predicate, level, 0, maxIterations); if (!result.isEmpty()) { return result; } } return new ArrayList<>(); } @NonNull private static List checkLevel(R targetValue, List previousLevel, BiPredicate> predicate, int level, int permutationIndex, AtomicInteger maxIterations) { if (previousLevel.size() == 1) { return new ArrayList<>(); } for (int i = permutationIndex; i < previousLevel.size(); i++) { if (maxIterations.get() <= 0) { return new ArrayList<>(); } List newList = new ArrayList<>(previousLevel); newList.remove(i); if (level == 0) { maxIterations.decrementAndGet(); // Check all permutations on this level if (predicate.test(targetValue, newList)) { return newList; } } else { // Test next level var result = checkLevel(targetValue, newList, predicate, level - 1, i, maxIterations); if (!result.isEmpty()) { return result; } } } return new ArrayList<>(); } //TODO optimize algorithm so that it starts from all objects and goes down instead starting with from the bottom. // That should help that we are not hitting the iteration limit so easily. /** * Returns a list of all possible permutations of a give sorted list ignoring duplicates. * E.g. List [A,B,C] results in this list of permutations: [[A], [B], [A,B], [C], [A,C], [B,C], [A,B,C]] * Number of variations and iterations grows with 2^n - 1 where n is the number of items in the list. * With 20 items we reach about 1 million iterations and it takes about 0.5 sec. * To avoid performance issues we added the maxIterations parameter to stop once the number of iterations has * reached the maxIterations and return in such a case the list of permutations we have been able to create. * Depending on the type of object which is stored in the list the memory usage should be considered as well for * choosing the right maxIterations value. * * @param list List from which we create permutations * @param maxIterations Max. number of iterations including inner iterations * @param Type of list items * @return List of possible permutations of the original list */ public static List> findAllPermutations(List list, int maxIterations) { List> result = new ArrayList<>(); int counter = 0; long ts = System.currentTimeMillis(); for (T item : list) { counter++; if (counter > maxIterations) { log.warn("We reached maxIterations of our allowed iterations and return current state of the result. " + "counter={}", counter); return result; } List> subLists = new ArrayList<>(); for (int n = 0; n < result.size(); n++) { counter++; if (counter > maxIterations) { log.warn("We reached maxIterations of our allowed iterations and return current state of the result. " + "counter={}", counter); return result; } List subList = new ArrayList<>(result.get(n)); subList.add(item); subLists.add(subList); } // add single item result.add(new ArrayList<>(Collections.singletonList(item))); // add subLists result.addAll(subLists); } log.info("findAllPermutations took {} ms for {} items and {} iterations. Heap size used: {} MB", (System.currentTimeMillis() - ts), list.size(), counter, Profiler.getUsedMemoryInMB()); return result; } } ================================================ FILE: common/src/main/java/haveno/common/util/Preconditions.java ================================================ package haveno.common.util; import java.io.File; import static java.lang.String.format; /** * Custom preconditions similar to those found in * {@link com.google.common.base.Preconditions}. */ public class Preconditions { /** * Ensures that {@code dir} is a non-null, existing and read-writeable directory. * @param dir the directory to check * @return the given directory, now validated */ public static File checkDir(File dir) { if (dir == null) throw new IllegalArgumentException("Directory must not be null"); if (!dir.exists()) throw new IllegalArgumentException(format("Directory '%s' does not exist", dir)); if (!dir.isDirectory()) throw new IllegalArgumentException(format("Directory '%s' is not a directory", dir)); if (!dir.canRead()) throw new IllegalArgumentException(format("Directory '%s' is not readable", dir)); if (!dir.canWrite()) throw new IllegalArgumentException(format("Directory '%s' is not writeable", dir)); return dir; } } ================================================ FILE: common/src/main/java/haveno/common/util/Profiler.java ================================================ /* * 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 . */ package haveno.common.util; import haveno.common.UserThread; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.TimeUnit; @Slf4j public class Profiler { public static void printSystemLoadPeriodically(long delay, TimeUnit timeUnit) { UserThread.runPeriodically(Profiler::printSystemLoad, delay, timeUnit); } public static void printSystemLoad() { Runtime runtime = Runtime.getRuntime(); long free = runtime.freeMemory(); long total = runtime.totalMemory(); long used = total - free; log.info("Total memory: {}; Used memory: {}; Free memory: {}; Max memory: {}; No. of threads: {}", Utilities.readableFileSize(total), Utilities.readableFileSize(used), Utilities.readableFileSize(free), Utilities.readableFileSize(runtime.maxMemory()), Thread.activeCount()); } public static long getUsedMemoryInMB() { return getUsedMemoryInBytes() / 1024 / 1024; } public static long getUsedMemoryInBytes() { Runtime runtime = Runtime.getRuntime(); long free = runtime.freeMemory(); long total = runtime.totalMemory(); return total - free; } public static long getFreeMemoryInMB() { return Runtime.getRuntime().freeMemory() / 1024 / 1024; } public static long getTotalMemoryInMB() { return Runtime.getRuntime().totalMemory() / 1024 / 1024; } } ================================================ FILE: common/src/main/java/haveno/common/util/ReflectionUtils.java ================================================ /* * 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 . */ package haveno.common.util; import lombok.extern.slf4j.Slf4j; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Collectors; import static java.lang.String.format; import static java.util.Arrays.stream; import static org.apache.commons.lang3.StringUtils.capitalize; @Slf4j public class ReflectionUtils { /** * Recursively loads a list of fields for a given class and its superclasses, * using a filter predicate to exclude any unwanted fields. * * @param fields The list of fields being loaded for a class hierarchy. * @param clazz The lowest level class in a hierarchy; excluding Object.class. * @param isExcludedField The field exclusion predicate. */ public static void loadFieldListForClassHierarchy(List fields, Class clazz, Predicate isExcludedField) { fields.addAll(stream(clazz.getDeclaredFields()) .filter(f -> !isExcludedField.test(f)) .collect(Collectors.toList())); Class superclass = clazz.getSuperclass(); if (!Objects.equals(superclass, Object.class)) loadFieldListForClassHierarchy(fields, superclass, isExcludedField); } /** * Returns an Optional of a setter method for a given field and a class hierarchy, * or Optional.empty() if it does not exist. * * @param field The field used to find a setter method. * @param clazz The lowest level class in a hierarchy; excluding Object.class. * @return Optional of the setter method for a field in the class hierarchy, * or Optional.empty() if it does not exist. */ public static Optional getSetterMethodForFieldInClassHierarchy(Field field, Class clazz) { Optional setter = stream(clazz.getDeclaredMethods()) .filter((m) -> isSetterForField(m, field)) .findFirst(); if (setter.isPresent()) return setter; Class superclass = clazz.getSuperclass(); if (!Objects.equals(superclass, Object.class)) { setter = getSetterMethodForFieldInClassHierarchy(field, superclass); if (setter.isPresent()) return setter; } return Optional.empty(); } public static boolean isSetterForField(Method m, Field f) { return m.getName().startsWith("set") && m.getName().endsWith(capitalize(f.getName())) && m.getReturnType().getName().equals("void") && m.getParameterCount() == 1 && m.getParameterTypes()[0].getName().equals(f.getType().getName()); } public static boolean isSetterOnClass(Method setter, Class clazz) { return setter.getDeclaringClass().isAssignableFrom(clazz); } public static String getVisibilityModifierAsString(Field field) { if (Modifier.isPrivate(field.getModifiers())) return "private"; else if (Modifier.isProtected(field.getModifiers())) return "protected"; else if (Modifier.isPublic(field.getModifiers())) return "public"; else return ""; } public static Field getField(String name, Class clazz) { Optional field = stream(clazz.getDeclaredFields()) .filter(f -> f.getName().equals(name)).findFirst(); return field.orElseThrow(() -> new IllegalArgumentException(format("field %s not found in class %s", name, clazz.getSimpleName()))); } public static Method getMethod(String name, Class clazz) { Optional method = stream(clazz.getDeclaredMethods()) .filter(m -> m.getName().equals(name)).findFirst(); return method.orElseThrow(() -> new IllegalArgumentException(format("method %s not found in class %s", name, clazz.getSimpleName()))); } public static void handleSetFieldValueError(Object object, Field field, ReflectiveOperationException ex) { String errMsg = format("cannot set value of field %s, on class %s", field.getName(), object.getClass().getSimpleName()); log.error(capitalize(errMsg) + ".", ex); throw new IllegalStateException("programmer error: " + errMsg); } } ================================================ FILE: common/src/main/java/haveno/common/util/RestartUtil.java ================================================ /* * 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 . */ package haveno.common.util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.lang.management.ManagementFactory; import java.util.List; // Borrowed from: https://dzone.com/articles/programmatically-restart-java public class RestartUtil { private static final Logger log = LoggerFactory.getLogger(RestartUtil.class); /** * Sun property pointing the main class and its arguments. * Might not be defined on non Hotspot VM implementations. */ public static final String SUN_JAVA_COMMAND = "sun.java.command"; public static void restartApplication(String logPath) throws IOException { try { String java = System.getProperty("java.home") + "/bin/java"; List vmArguments = ManagementFactory.getRuntimeMXBean().getInputArguments(); StringBuilder vmArgsOneLine = new StringBuilder(); // if it's the agent argument : we ignore it otherwise the // address of the old application and the new one will be in conflict vmArguments.stream().filter(arg -> !arg.contains("-agentlib")).forEach(arg -> { vmArgsOneLine.append(arg); vmArgsOneLine.append(" "); }); // init the command to execute, add the vm args final StringBuilder cmd = new StringBuilder(java + " " + vmArgsOneLine); // program main and program arguments String[] mainCommand = System.getProperty(SUN_JAVA_COMMAND).split(" "); // program main is a jar if (mainCommand[0].endsWith(".jar")) { // if it's a jar, add -jar mainJar cmd.append("-jar ").append(new File(mainCommand[0]).getPath()); } else { // else it's a .class, add the classpath and mainClass cmd.append("-cp \"").append(System.getProperty("java.class.path")).append("\" ").append(mainCommand[0]); } // finally add program arguments for (int i = 1; i < mainCommand.length; i++) { cmd.append(" "); cmd.append(mainCommand[i]); } try { final String command = "nohup " + cmd.toString() + " >/dev/null 2>" + logPath + " &"; log.warn("\n\n############################################################\n" + "Executing cmd for restart: {}" + "\n############################################################\n\n", command); Runtime.getRuntime().exec(command); } catch (IOException e) { e.printStackTrace(); } } catch (Exception e) { throw new IOException("Error while trying to restart the application", e); } } } ================================================ FILE: common/src/main/java/haveno/common/util/SingleThreadExecutorUtils.java ================================================ /* * 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 . */ package haveno.common.util; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; /** * Utility class for creating single-threaded executors. */ public class SingleThreadExecutorUtils { private SingleThreadExecutorUtils() { // Prevent instantiation } public static ExecutorService getSingleThreadExecutor(Class aClass) { validateClass(aClass); return getSingleThreadExecutor(aClass.getSimpleName()); } public static ExecutorService getNonDaemonSingleThreadExecutor(Class aClass) { validateClass(aClass); return getSingleThreadExecutor(aClass.getSimpleName(), false); } public static ExecutorService getSingleThreadExecutor(String name) { validateName(name); return getSingleThreadExecutor(name, true); } public static ListeningExecutorService getSingleThreadListeningExecutor(String name) { validateName(name); return MoreExecutors.listeningDecorator(getSingleThreadExecutor(name)); } public static ExecutorService getSingleThreadExecutor(ThreadFactory threadFactory) { validateThreadFactory(threadFactory); return Executors.newSingleThreadExecutor(threadFactory); } private static ExecutorService getSingleThreadExecutor(String name, boolean isDaemonThread) { ThreadFactory threadFactory = getThreadFactory(name, isDaemonThread); return Executors.newSingleThreadExecutor(threadFactory); } private static ThreadFactory getThreadFactory(String name, boolean isDaemonThread) { return new ThreadFactoryBuilder() .setNameFormat(name + "-%d") .setDaemon(isDaemonThread) .build(); } private static void validateClass(Class aClass) { if (aClass == null) { throw new IllegalArgumentException("Class must not be null."); } } private static void validateName(String name) { if (name == null || name.isEmpty()) { throw new IllegalArgumentException("Name must not be null or empty."); } } private static void validateThreadFactory(ThreadFactory threadFactory) { if (threadFactory == null) { throw new IllegalArgumentException("ThreadFactory must not be null."); } } } ================================================ FILE: common/src/main/java/haveno/common/util/Tuple2.java ================================================ /* * 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 . */ package haveno.common.util; import java.io.Serializable; import java.util.Objects; public class Tuple2 implements Serializable { private static final long serialVersionUID = 1; final public A first; final public B second; public Tuple2(A first, B second) { this.first = first; this.second = second; } @SuppressWarnings("SimplifiableIfStatement") @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Tuple2)) return false; Tuple2 tuple2 = (Tuple2) o; if (!Objects.equals(first, tuple2.first)) return false; return Objects.equals(second, tuple2.second); } @Override public int hashCode() { int result = first != null ? first.hashCode() : 0; result = 31 * result + (second != null ? second.hashCode() : 0); return result; } @Override public String toString() { return "Tuple2{" + "\n first=" + first + ",\n second=" + second + "\n}"; } } ================================================ FILE: common/src/main/java/haveno/common/util/Tuple3.java ================================================ /* * 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 . */ package haveno.common.util; import java.util.Objects; public class Tuple3 { final public A first; final public B second; final public C third; public Tuple3(A first, B second, C third) { this.first = first; this.second = second; this.third = third; } @SuppressWarnings("SimplifiableIfStatement") @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Tuple3)) return false; Tuple3 tuple3 = (Tuple3) o; if (!Objects.equals(first, tuple3.first)) return false; if (!Objects.equals(second, tuple3.second)) return false; return Objects.equals(third, tuple3.third); } @Override public int hashCode() { int result = first != null ? first.hashCode() : 0; result = 31 * result + (second != null ? second.hashCode() : 0); result = 31 * result + (third != null ? third.hashCode() : 0); return result; } } ================================================ FILE: common/src/main/java/haveno/common/util/Tuple4.java ================================================ /* * 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 . */ package haveno.common.util; import java.util.Objects; public class Tuple4 { final public A first; final public B second; final public C third; final public D fourth; public Tuple4(A first, B second, C third, D fourth) { this.first = first; this.second = second; this.third = third; this.fourth = fourth; } @SuppressWarnings("SimplifiableIfStatement") @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Tuple4)) return false; Tuple4 tuple4 = (Tuple4) o; if (!Objects.equals(first, tuple4.first)) return false; if (!Objects.equals(second, tuple4.second)) return false; if (!Objects.equals(third, tuple4.third)) return false; return Objects.equals(fourth, tuple4.fourth); } @Override public int hashCode() { int result = first != null ? first.hashCode() : 0; result = 31 * result + (second != null ? second.hashCode() : 0); result = 31 * result + (third != null ? third.hashCode() : 0); result = 31 * result + (fourth != null ? fourth.hashCode() : 0); return result; } } ================================================ FILE: common/src/main/java/haveno/common/util/Tuple5.java ================================================ /* * 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 . */ package haveno.common.util; import java.util.Objects; public class Tuple5 { final public A first; final public B second; final public C third; final public D fourth; final public E fifth; public Tuple5(A first, B second, C third, D fourth, E fifth) { this.first = first; this.second = second; this.third = third; this.fourth = fourth; this.fifth = fifth; } @SuppressWarnings("SimplifiableIfStatement") @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Tuple5)) return false; Tuple5 tuple5 = (Tuple5) o; if (!Objects.equals(first, tuple5.first)) return false; if (!Objects.equals(second, tuple5.second)) return false; if (!Objects.equals(third, tuple5.third)) return false; if (!Objects.equals(fourth, tuple5.fourth)) return false; return Objects.equals(fifth, tuple5.fifth); } @Override public int hashCode() { int result = first != null ? first.hashCode() : 0; result = 31 * result + (second != null ? second.hashCode() : 0); result = 31 * result + (third != null ? third.hashCode() : 0); result = 31 * result + (fourth != null ? fourth.hashCode() : 0); result = 31 * result + (fifth != null ? fifth.hashCode() : 0); return result; } } ================================================ FILE: common/src/main/java/haveno/common/util/Utilities.java ================================================ /* * 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 . */ package haveno.common.util; import org.bitcoinj.core.Utils; import com.google.common.base.Splitter; import com.google.common.primitives.Ints; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.ThreadFactoryBuilder; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DurationFormatUtils; import javafx.scene.input.Clipboard; import javafx.scene.input.ClipboardContent; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; import java.text.DecimalFormat; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Paths; import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.TimeZone; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class Utilities { public static ExecutorService getFixedThreadPoolExecutor(int nThreads, ThreadFactory threadFactory) { return Executors.newFixedThreadPool(nThreads, threadFactory); } public static ListeningExecutorService getListeningExecutorService(String name, int corePoolSize, int maximumPoolSize, long keepAliveTimeInSec) { return getListeningExecutorService(name, corePoolSize, maximumPoolSize, maximumPoolSize, keepAliveTimeInSec); } public static ListeningExecutorService getListeningExecutorService(String name, int corePoolSize, int maximumPoolSize, int queueCapacity, long keepAliveTimeInSec) { return MoreExecutors.listeningDecorator(getThreadPoolExecutor(name, corePoolSize, maximumPoolSize, queueCapacity, keepAliveTimeInSec)); } public static ListeningExecutorService getListeningExecutorService(String name, int corePoolSize, int maximumPoolSize, long keepAliveTimeInSec, BlockingQueue workQueue) { return MoreExecutors.listeningDecorator(getThreadPoolExecutor(name, corePoolSize, maximumPoolSize, keepAliveTimeInSec, workQueue)); } public static ThreadPoolExecutor getThreadPoolExecutor(String name, int corePoolSize, int maximumPoolSize, long keepAliveTimeInSec) { return getThreadPoolExecutor(name, corePoolSize, maximumPoolSize, maximumPoolSize, keepAliveTimeInSec); } public static ThreadPoolExecutor getThreadPoolExecutor(String name, int corePoolSize, int maximumPoolSize, int queueCapacity, long keepAliveTimeInSec) { return getThreadPoolExecutor(name, corePoolSize, maximumPoolSize, keepAliveTimeInSec, new ArrayBlockingQueue<>(queueCapacity)); } private static ThreadPoolExecutor getThreadPoolExecutor(String name, int corePoolSize, int maximumPoolSize, long keepAliveTimeInSec, BlockingQueue workQueue) { ThreadFactory threadFactory = new ThreadFactoryBuilder() .setNameFormat(name + "-%d") .setDaemon(true) .build(); ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTimeInSec, TimeUnit.SECONDS, workQueue, threadFactory); executor.allowCoreThreadTimeOut(true); return executor; } public static void shutdownAndAwaitTermination(ExecutorService executor, long timeout, TimeUnit unit) { // noinspection UnstableApiUsage MoreExecutors.shutdownAndAwaitTermination(executor, timeout, unit); } public static FutureCallback failureCallback(Consumer errorHandler) { return new FutureCallback<>() { @Override public void onSuccess(V result) { } @Override public void onFailure(@NotNull Throwable t) { errorHandler.accept(t); } }; } /** * @return true if defaults read -g AppleInterfaceStyle has an exit status of 0 (i.e. _not_ returning "key not found"). */ public static boolean isMacMenuBarDarkMode() { try { // check for exit status only. Once there are more modes than "dark" and "default", we might need to analyze string contents.. Process process = Runtime.getRuntime().exec(new String[] { "defaults", "read", "-g", "AppleInterfaceStyle" }); process.waitFor(100, TimeUnit.MILLISECONDS); return process.exitValue() == 0; } catch (IOException | InterruptedException | IllegalThreadStateException ex) { // IllegalThreadStateException thrown by proc.exitValue(), if process didn't terminate return false; } } public static boolean isUnix() { return isOSX() || isLinux() || getOSName().contains("freebsd"); } public static boolean isWindows() { return getOSName().contains("win"); } /** * @return True, if Haveno is running on a virtualized OS within Qubes, false otherwise */ public static boolean isQubesOS() { // For Linux qubes, "os.version" looks like "4.19.132-1.pvops.qubes.x86_64" // The presence of the "qubes" substring indicates this Linux is running as a qube // This is the case for all 3 virtualization modes (PV, PVH, HVM) // In addition, this works for both simple AppVMs, as well as for StandaloneVMs // TODO This might not work for detecting Qubes virtualization for other OSes // like Windows return getOSVersion().contains("qubes"); } public static boolean isOSX() { return getOSName().contains("mac") || getOSName().contains("darwin"); } public static boolean isLinux() { return getOSName().contains("linux"); } public static boolean isDebianLinux() { return isLinux() && new File("/etc/debian_version").isFile(); } public static boolean isRedHatLinux() { return isLinux() && new File("/etc/redhat-release").isFile(); } private static String getOSName() { return System.getProperty("os.name").toLowerCase(Locale.US); } public static String getOSVersion() { return System.getProperty("os.version").toLowerCase(Locale.US); } /** * Returns the well-known "user data directory" for the current operating system. */ public static File getUserDataDir() { if (Utilities.isWindows()) return new File(System.getenv("APPDATA")); if (Utilities.isOSX()) return Paths.get(System.getProperty("user.home"), "Library", "Application Support").toFile(); // *nix return Paths.get(System.getProperty("user.home"), ".local", "share").toFile(); } public static int getMinorVersion() throws InvalidVersionException { String version = getOSVersion(); String[] tokens = version.split("\\."); try { checkArgument(tokens.length > 1); return Integer.parseInt(tokens[1]); } catch (IllegalArgumentException e) { printSysInfo(); throw new InvalidVersionException("Version is not in expected format. Version=" + version); } } public static int getMajorVersion() throws InvalidVersionException { String version = getOSVersion(); String[] tokens = version.split("\\."); try { checkArgument(tokens.length > 0); return Integer.parseInt(tokens[0]); } catch (IllegalArgumentException e) { printSysInfo(); throw new InvalidVersionException("Version is not in expected format. Version=" + version); } } public static String getOSArchitecture() { String osArch = System.getProperty("os.arch"); if (isWindows()) { // See: Like always windows needs extra treatment // https://stackoverflow.com/questions/20856694/how-to-find-the-os-bit-type String arch = System.getenv("PROCESSOR_ARCHITECTURE"); String wow64Arch = System.getenv("PROCESSOR_ARCHITEW6432"); return arch.endsWith("64") || wow64Arch != null && wow64Arch.endsWith("64") ? "64" : "32"; } else if (osArch.contains("arm")) { // armv8 is 64 bit, armv7l is 32 bit return osArch.contains("64") || osArch.contains("v8") ? "64" : "32"; } else if (isLinux()) { return osArch.startsWith("i") ? "32" : "64"; } else { return osArch.contains("64") ? "64" : osArch; } } public static void printSysInfo() { log.info("System info: os.name={}; os.version={}; os.arch={}; sun.arch.data.model={}; JRE={}; JVM={}", System.getProperty("os.name"), System.getProperty("os.version"), System.getProperty("os.arch"), getJVMArchitecture(), (System.getProperty("java.runtime.version", "-") + " (" + System.getProperty("java.vendor", "-") + ")"), (System.getProperty("java.vm.version", "-") + " (" + System.getProperty("java.vm.name", "-") + ")")); } public static String getJVMArchitecture() { return System.getProperty("sun.arch.data.model"); } public static boolean isCorrectOSArchitecture() { boolean result = getOSArchitecture().endsWith(getJVMArchitecture()); if (!result) { log.warn("System.getProperty(\"os.arch\") " + System.getProperty("os.arch")); log.warn("System.getenv(\"ProgramFiles(x86)\") " + System.getenv("ProgramFiles(x86)")); log.warn("System.getenv(\"PROCESSOR_ARCHITECTURE\")" + System.getenv("PROCESSOR_ARCHITECTURE")); log.warn("System.getenv(\"PROCESSOR_ARCHITEW6432\") " + System.getenv("PROCESSOR_ARCHITEW6432")); log.warn("System.getProperty(\"sun.arch.data.model\") " + System.getProperty("sun.arch.data.model")); } return result; } public static void openURI(URI uri) throws IOException { if (!DesktopUtil.browse(uri)) throw new IOException("Failed to open URI: " + uri); } public static void openFile(File file) throws IOException { if (!DesktopUtil.open(file)) throw new IOException("Failed to open file: " + file); } public static String getDownloadOfHomeDir() { File file = new File(getSystemHomeDirectory() + "/Downloads"); if (file.exists()) return file.getAbsolutePath(); else return getSystemHomeDirectory(); } public static void copyToClipboard(String content) { try { if (content != null && content.length() > 0) { Clipboard clipboard = Clipboard.getSystemClipboard(); ClipboardContent clipboardContent = new ClipboardContent(); clipboardContent.putString(content); clipboard.setContent(clipboardContent); } } catch (Throwable e) { log.error("copyToClipboard failed: {}\n", e.getMessage(), e); } } public static void setThreadName(String name) { Thread.currentThread().setName(name + "-" + new Random().nextInt(10000)); } public static boolean isDirectory(String path) { return new File(path).isDirectory(); } public static String getSystemHomeDirectory() { return Utilities.isWindows() ? System.getenv("USERPROFILE") : System.getProperty("user.home"); } public static String encodeToHex(@Nullable byte[] bytes, boolean allowNullable) { if (allowNullable) return bytes != null ? Utils.HEX.encode(bytes) : "null"; else return Utils.HEX.encode(checkNotNull(bytes, "bytes must not be null at encodeToHex")); } public static String bytesAsHexString(@Nullable byte[] bytes) { return encodeToHex(bytes, true); } public static String encodeToHex(@Nullable byte[] bytes) { return encodeToHex(bytes, false); } public static byte[] decodeFromHex(String encoded) { return Utils.HEX.decode(encoded); } public static boolean isAltOrCtrlPressed(KeyCode keyCode, KeyEvent keyEvent) { return isAltPressed(keyCode, keyEvent) || isCtrlPressed(keyCode, keyEvent); } public static boolean isCtrlPressed(KeyCode keyCode, KeyEvent keyEvent) { return new KeyCodeCombination(keyCode, KeyCombination.SHORTCUT_DOWN).match(keyEvent) || new KeyCodeCombination(keyCode, KeyCombination.CONTROL_DOWN).match(keyEvent); } public static boolean isAltPressed(KeyCode keyCode, KeyEvent keyEvent) { return new KeyCodeCombination(keyCode, KeyCombination.ALT_DOWN).match(keyEvent); } public static boolean isCtrlShiftPressed(KeyCode keyCode, KeyEvent keyEvent) { return new KeyCodeCombination(keyCode, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN).match(keyEvent); } public static byte[] concatenateByteArrays(byte[] array1, byte[] array2) { return ArrayUtils.addAll(array1, array2); } public static Date getUTCDate(int year, int month, int dayOfMonth) { GregorianCalendar calendar = new GregorianCalendar(year, month, dayOfMonth); calendar.setTimeZone(TimeZone.getTimeZone("UTC")); return calendar.getTime(); } /** * @param stringList String of comma separated tokens. * @param allowWhitespace If white space inside the list tokens is allowed. If not the token will be ignored. * @return Set of tokens */ public static Set commaSeparatedListToSet(String stringList, boolean allowWhitespace) { if (stringList != null) { return Splitter.on(",") .splitToList(allowWhitespace ? stringList : StringUtils.deleteWhitespace(stringList)) .stream() .filter(e -> !e.isEmpty()) .collect(Collectors.toSet()); } else { return new HashSet<>(); } } public static String getPathOfCodeSource() throws URISyntaxException { return new File(Utilities.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getPath(); } public static String toTruncatedString(Object message) { return toTruncatedString(message, 200, true); } public static String toTruncatedString(Object message, int maxLength) { return toTruncatedString(message, maxLength, true); } public static String toTruncatedString(Object message, int maxLength, boolean removeLineBreaks) { if (message == null) return "null"; String result = StringUtils.abbreviate(message.toString(), maxLength); if (removeLineBreaks) return result.replace("\n", ""); return result; } public static List toListOfWrappedStrings(String s, int wrapLength) { StringBuilder sb = new StringBuilder(s); int i = 0; while (i + wrapLength < sb.length() && (i = sb.lastIndexOf(" ", i + wrapLength)) != -1) { sb.replace(i, i + 1, "\n"); } String[] splitLine = sb.toString().split("\n"); return Arrays.asList(splitLine); } public static String getRandomPrefix(int minLength, int maxLength) { int length = minLength + new Random().nextInt(maxLength - minLength + 1); String result; switch (new Random().nextInt(3)) { case 0: result = RandomStringUtils.randomAlphabetic(length); break; case 1: result = RandomStringUtils.randomNumeric(length); break; case 2: default: result = RandomStringUtils.randomAlphanumeric(length); } switch (new Random().nextInt(3)) { case 0: result = result.toUpperCase(); break; case 1: result = result.toLowerCase(); break; case 2: default: } return result; } public static String getShortId(String id) { return getShortId(id, "-"); } @SuppressWarnings("SameParameterValue") public static String getShortId(String id, String sep) { String[] chunks = id.split(sep); if (chunks.length > 0) return chunks[0]; else return id.substring(0, Math.min(8, id.length())); } public static byte[] integerToByteArray(int intValue, int numBytes) { byte[] bytes = new byte[numBytes]; for (int i = numBytes - 1; i >= 0; i--) { bytes[i] = ((byte) (intValue & 0xFF)); intValue >>>= 8; } return bytes; } public static int byteArrayToInteger(byte[] bytes) { int result = 0; for (byte aByte : bytes) { result = result << 8 | aByte & 0xff; } return result; } public static byte[] copyRightAligned(byte[] src, int newLength) { byte[] dest = new byte[newLength]; int srcPos = Math.max(src.length - newLength, 0); int destPos = Math.max(newLength - src.length, 0); System.arraycopy(src, srcPos, dest, destPos, newLength - destPos); return dest; } public static byte[] intsToBytesBE(int[] ints) { byte[] bytes = new byte[ints.length * 4]; int i = 0; for (int v : ints) { bytes[i++] = (byte) (v >> 24); bytes[i++] = (byte) (v >> 16); bytes[i++] = (byte) (v >> 8); bytes[i++] = (byte) v; } return bytes; } public static int[] bytesToIntsBE(byte[] bytes) { int[] ints = new int[bytes.length / 4]; for (int i = 0, j = 0; i < bytes.length / 4; i++) { ints[i] = Ints.fromBytes(bytes[j++], bytes[j++], bytes[j++], bytes[j++]); } return ints; } // Helper to filter unique elements by key public static Predicate distinctByKey(Function keyExtractor) { Map map = new ConcurrentHashMap<>(); return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; } public static String readableFileSize(long size) { if (size <= 0) return "0"; String[] units = new String[]{"B", "kB", "MB", "GB", "TB"}; int digitGroups = (int) (Math.log10(size) / Math.log10(1024)); return new DecimalFormat("#,##0.###").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups]; } // Substitute for FormattingUtils if there is no dependency to core public static String formatDurationAsWords(long durationMillis) { String format = ""; String second = "second"; String minute = "minute"; String hour = "hour"; String day = "day"; String days = "days"; String hours = "hours"; String minutes = "minutes"; String seconds = "seconds"; if (durationMillis >= TimeUnit.DAYS.toMillis(1)) { format = "d\' " + days + ", \'"; } format += "H\' " + hours + ", \'m\' " + minutes + ", \'s\'.\'S\' " + seconds + "\'"; String duration = durationMillis > 0 ? DurationFormatUtils.formatDuration(durationMillis, format) : ""; duration = StringUtils.replacePattern(duration, "^1 " + seconds + "|\\b1 " + seconds, "1 " + second); duration = StringUtils.replacePattern(duration, "^1 " + minutes + "|\\b1 " + minutes, "1 " + minute); duration = StringUtils.replacePattern(duration, "^1 " + hours + "|\\b1 " + hours, "1 " + hour); duration = StringUtils.replacePattern(duration, "^1 " + days + "|\\b1 " + days, "1 " + day); duration = duration.replace(", 0 seconds", ""); duration = duration.replace(", 0 minutes", ""); duration = duration.replace(", 0 hours", ""); duration = StringUtils.replacePattern(duration, "^0 days, ", ""); duration = StringUtils.replacePattern(duration, "^0 hours, ", ""); duration = StringUtils.replacePattern(duration, "^0 minutes, ", ""); duration = StringUtils.replacePattern(duration, "^0 seconds, ", ""); String result = duration.trim(); if (result.isEmpty()) { result = "0.000 seconds"; } return result; } public static String cleanString(String string) { return string.replaceAll("[\\t\\n\\r]+", " "); } } ================================================ FILE: common/src/main/java/haveno/common/util/ZipUtils.java ================================================ /* * 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 . */ package haveno.common.util; import lombok.extern.slf4j.Slf4j; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; @Slf4j public class ZipUtils { /** * Zips directory into the output stream. Empty directories are not included. * * @param dir The directory to create the zip from. * @param out The stream to write to. */ public static void zipDirToStream(File dir, OutputStream out, int bufferSize, Collection excludedFiles) throws Exception { // Get all files in directory and subdirectories. List fileList = new ArrayList<>(); getFilesRecursive(dir, fileList, excludedFiles); try (ZipOutputStream zos = new ZipOutputStream(out)) { for (File file : fileList) { String filePath = file.getAbsolutePath(); log.info("Compressing: " + filePath); // Creates a zip entry. String name = filePath.substring(dir.getAbsolutePath().length() + 1); ZipEntry zipEntry = new ZipEntry(name); zos.putNextEntry(zipEntry); // Read file content and write to zip output stream. try (FileInputStream fis = new FileInputStream(filePath)) { byte[] buffer = new byte[bufferSize]; int length; while ((length = fis.read(buffer)) > 0) { zos.write(buffer, 0, length); } // Close the zip entry. zos.closeEntry(); } catch (Exception e) { log.warn(e.getMessage()); } } } } /** * Get files list from the directory recursive to the subdirectory. */ public static void getFilesRecursive(File directory, List fileList, Collection excludedFiles) { File[] files = directory.listFiles(); if (files != null && files.length > 0) { for (File file : files) { if (excludedFiles != null && excludedFiles.contains(file)) continue; if (file.isFile()) { fileList.add(file); } else { getFilesRecursive(file, fileList, excludedFiles); } } } } /** * Unzips the zipStream into the specified directory, overwriting any files. * Existing files are preserved. * * @param dir The directory to write to. * @param inputStream The raw stream assumed to be in zip format. * @param bufferSize The buffer used to read from efficiently. */ public static void unzipToDir(File dir, InputStream inputStream, int bufferSize) throws Exception { try (ZipInputStream zipStream = new ZipInputStream(inputStream)) { ZipEntry entry; byte[] buffer = new byte[bufferSize]; int count; while ((entry = zipStream.getNextEntry()) != null) { File file = new File(dir, entry.getName()); if (entry.isDirectory()) { file.mkdirs(); } else { // Make sure folder exists. file.getParentFile().mkdirs(); log.info("Unzipped file: " + file.getAbsolutePath()); // Don't overwrite the current logs if ("haveno.log".equals(file.getName())) { file = new File(file.getParent() + "/" + "haveno.backup.log"); log.info("Unzipped logfile to backup path: " + file.getAbsolutePath()); } try (FileOutputStream fileOutput = new FileOutputStream(file)) { while ((count = zipStream.read(buffer)) != -1) { fileOutput.write(buffer, 0, count); } } } zipStream.closeEntry(); } } } } ================================================ FILE: common/src/test/java/haveno/common/app/CapabilitiesTest.java ================================================ /* * 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 . */ package haveno.common.app; import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import static haveno.common.app.Capability.SEED_NODE; import static haveno.common.app.Capability.TRADE_STATISTICS; import static haveno.common.app.Capability.TRADE_STATISTICS_2; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class CapabilitiesTest { @Test public void testNoCapabilitiesAvailable() { Capabilities DUT = new Capabilities(); assertTrue(DUT.containsAll(new HashSet<>())); assertFalse(DUT.containsAll(new Capabilities(SEED_NODE))); } @Test public void testHasLess() { assertTrue(new Capabilities().hasLess(new Capabilities(SEED_NODE))); assertFalse(new Capabilities().hasLess(new Capabilities())); assertFalse(new Capabilities(SEED_NODE).hasLess(new Capabilities())); Capabilities all = new Capabilities( TRADE_STATISTICS, TRADE_STATISTICS_2, Capability.ACCOUNT_AGE_WITNESS, Capability.ACK_MSG, Capability.BUNDLE_OF_ENVELOPES, Capability.MEDIATION, Capability.SIGNED_ACCOUNT_AGE_WITNESS, Capability.REFUND_AGENT, Capability.TRADE_STATISTICS_HASH_UPDATE ); Capabilities other = new Capabilities( TRADE_STATISTICS, TRADE_STATISTICS_2, Capability.ACCOUNT_AGE_WITNESS, Capability.ACK_MSG, Capability.BUNDLE_OF_ENVELOPES, Capability.MEDIATION, Capability.SIGNED_ACCOUNT_AGE_WITNESS, Capability.REFUND_AGENT, Capability.TRADE_STATISTICS_HASH_UPDATE, Capability.NO_ADDRESS_PRE_FIX ); assertTrue(all.hasLess(other)); } @Test public void testO() { Capabilities DUT = new Capabilities(TRADE_STATISTICS); assertTrue(DUT.containsAll(new HashSet<>())); } @Test public void testSingleMatch() { Capabilities DUT = new Capabilities(TRADE_STATISTICS); // single match assertTrue(DUT.containsAll(new Capabilities(TRADE_STATISTICS))); assertFalse(DUT.containsAll(new Capabilities(SEED_NODE))); } @Test public void testMultiMatch() { Capabilities DUT = new Capabilities(TRADE_STATISTICS, TRADE_STATISTICS_2); assertTrue(DUT.containsAll(new Capabilities(TRADE_STATISTICS))); assertFalse(DUT.containsAll(new Capabilities(SEED_NODE))); assertTrue(DUT.containsAll(new Capabilities(TRADE_STATISTICS, TRADE_STATISTICS_2))); assertFalse(DUT.containsAll(new Capabilities(SEED_NODE, TRADE_STATISTICS_2))); } @Test public void testToIntList() { assertEquals(Collections.emptyList(), Capabilities.toIntList(new Capabilities())); assertEquals(Collections.singletonList(12), Capabilities.toIntList(new Capabilities(Capability.MEDIATION))); assertEquals(Arrays.asList(6, 12), Capabilities.toIntList(new Capabilities(Capability.MEDIATION, Capability.BLIND_VOTE))); } @Test public void testFromIntList() { assertEquals(new Capabilities(), Capabilities.fromIntList(Collections.emptyList())); assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromIntList(Collections.singletonList(12))); assertEquals(new Capabilities(Capability.BLIND_VOTE, Capability.MEDIATION), Capabilities.fromIntList(Arrays.asList(6, 12))); assertEquals(new Capabilities(), Capabilities.fromIntList(Collections.singletonList(-1))); assertEquals(new Capabilities(), Capabilities.fromIntList(Collections.singletonList(99))); assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromIntList(Arrays.asList(-6, 12))); assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromIntList(Arrays.asList(12, 99))); } @Test public void testToStringList() { assertEquals("", new Capabilities().toStringList()); assertEquals("12", new Capabilities(Capability.MEDIATION).toStringList()); assertEquals("6, 12", new Capabilities(Capability.BLIND_VOTE, Capability.MEDIATION).toStringList()); // capabilities gets sorted, independent of our order assertEquals("6, 12", new Capabilities(Capability.MEDIATION, Capability.BLIND_VOTE).toStringList()); } @Test public void testFromStringList() { assertEquals(new Capabilities(), Capabilities.fromStringList(null)); assertEquals(new Capabilities(), Capabilities.fromStringList("")); assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromStringList("12")); assertEquals(new Capabilities(Capability.BLIND_VOTE, Capability.MEDIATION), Capabilities.fromStringList("6,12")); assertEquals(new Capabilities(Capability.BLIND_VOTE, Capability.MEDIATION), Capabilities.fromStringList("12, 6")); assertEquals(new Capabilities(), Capabilities.fromStringList("a")); assertEquals(new Capabilities(), Capabilities.fromStringList("99")); assertEquals(new Capabilities(), Capabilities.fromStringList("-1")); assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromStringList("12, a")); assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromStringList("12, 99")); assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromStringList("a,12, 99")); } } ================================================ FILE: common/src/test/java/haveno/common/app/VersionTest.java ================================================ /* * 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 . */ package haveno.common.app; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class VersionTest { @Test public void testVersionNumber() { assertEquals(0, Version.getMajorVersion("0.0.0")); assertEquals(1, Version.getMajorVersion("1.0.0")); assertEquals(0, Version.getMinorVersion("0.0.0")); assertEquals(5, Version.getMinorVersion("0.5.0")); assertEquals(0, Version.getPatchVersion("0.0.0")); assertEquals(5, Version.getPatchVersion("0.0.5")); } @Test public void testIsNewVersion() { assertFalse(Version.isNewVersion("0.0.0", "0.0.0")); assertTrue(Version.isNewVersion("0.1.0", "0.0.0")); assertTrue(Version.isNewVersion("0.0.1", "0.0.0")); assertTrue(Version.isNewVersion("1.0.0", "0.0.0")); assertTrue(Version.isNewVersion("0.5.1", "0.5.0")); assertFalse(Version.isNewVersion("0.5.0", "0.5.1")); assertTrue(Version.isNewVersion("0.6.0", "0.5.0")); assertTrue(Version.isNewVersion("0.6.0", "0.5.1")); assertFalse(Version.isNewVersion("0.5.0", "1.5.0")); assertFalse(Version.isNewVersion("0.4.9", "0.5.0")); } } ================================================ FILE: common/src/test/java/haveno/common/config/ConfigFileEditorTests.java ================================================ package haveno.common.config; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class ConfigFileEditorTests { private File file; private PrintWriter writer; private ConfigFileReader reader; private ConfigFileEditor editor; @BeforeEach public void setUp() throws IOException { file = File.createTempFile("haveno", "properties"); reader = new ConfigFileReader(file); editor = new ConfigFileEditor(file); writer = new PrintWriter(file); } @Test public void whenFileDoesNotExist_thenSetOptionCreatesItAndAppendsOneLine() { writer.close(); assertTrue(file.delete()); editor.setOption("opt1", "val1"); assertThat(reader.getLines(), contains("opt1=val1")); } @Test public void whenFileContainsOptionBeingSet_thenSetOptionOverwritesIt() { writer.println("opt1=val1"); writer.println("opt2=val2"); writer.println("opt3=val3"); writer.flush(); editor.setOption("opt2", "newval2"); assertThat(reader.getLines(), contains( "opt1=val1", "opt2=newval2", "opt3=val3")); } @Test public void whenOptionBeingSetHasNoArg_thenSetOptionWritesItWithNoEqualsSign() { writer.println("opt1=val1"); writer.println("opt2=val2"); writer.flush(); editor.setOption("opt3"); assertThat(reader.getLines(), contains( "opt1=val1", "opt2=val2", "opt3")); } @Test public void whenFileHasBlankOrCommentLines_thenTheyArePreserved() { writer.println("# Comment 1"); writer.println("opt1=val1"); writer.println(); writer.println("# Comment 2"); writer.println("opt2=val2"); writer.flush(); editor.setOption("opt3=val3"); assertThat(reader.getLines(), contains( "# Comment 1", "opt1=val1", "", "# Comment 2", "opt2=val2", "opt3=val3")); } @Test public void whenFileContainsOptionBeingCleared_thenClearOptionRemovesIt() { writer.println("opt1=val1"); writer.println("opt2=val2"); writer.flush(); editor.clearOption("opt2"); assertThat(reader.getLines(), contains("opt1=val1")); } @Test public void whenFileDoesNotContainOptionBeingCleared_thenClearOptionIsNoOp() { writer.println("opt1=val1"); writer.println("opt2=val2"); writer.flush(); editor.clearOption("opt3"); assertThat(reader.getLines(), contains( "opt1=val1", "opt2=val2")); } @Test public void whenFileDoesNotExist_thenClearOptionIsNoOp() { writer.close(); assertTrue(file.delete()); editor.clearOption("opt1"); assertFalse(file.exists()); } } ================================================ FILE: common/src/test/java/haveno/common/config/ConfigFileOptionTests.java ================================================ package haveno.common.config; import org.junit.jupiter.api.Test; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; public class ConfigFileOptionTests { @Test public void whenOptionHasWhitespaceAroundEqualsSign_thenItGetsTrimmed() { String value = "name1 = arg1"; ConfigFileOption option = ConfigFileOption.parse(value); assertThat(option.name, equalTo("name1")); assertThat(option.arg, equalTo("arg1")); assertThat(option.toString(), equalTo("name1=arg1")); } @Test public void whenOptionHasLeadingOrTrailingWhitespace_thenItGetsTrimmed() { String value = " name1=arg1 "; ConfigFileOption option = ConfigFileOption.parse(value); assertThat(option.name, equalTo("name1")); assertThat(option.arg, equalTo("arg1")); assertThat(option.toString(), equalTo("name1=arg1")); } @Test public void whenOptionHasEscapedColons_thenTheyGetUnescaped() { String value = "host1=example.com\\:8080"; ConfigFileOption option = ConfigFileOption.parse(value); assertThat(option.name, equalTo("host1")); assertThat(option.arg, equalTo("example.com:8080")); assertThat(option.toString(), equalTo("host1=example.com:8080")); } @Test public void whenOptionHasNoValue_thenItSetsEmptyValue() { String value = "host1="; ConfigFileOption option = ConfigFileOption.parse(value); assertThat(option.name, equalTo("host1")); assertThat(option.arg, equalTo("")); assertThat(option.toString(), equalTo("host1=")); } } ================================================ FILE: common/src/test/java/haveno/common/config/ConfigFileReaderTests.java ================================================ package haveno.common.config; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; public class ConfigFileReaderTests { private File file; private PrintWriter writer; private ConfigFileReader reader; @BeforeEach public void setUp() throws IOException { file = File.createTempFile("haveno", "properties"); reader = new ConfigFileReader(file); writer = new PrintWriter(file); } @Test public void whenFileDoesNotExist_thenGetLinesThrows() { writer.close(); assertTrue(file.delete()); Exception exception = assertThrows(ConfigException.class, () -> reader.getLines()); String expectedMessage = "does not exist"; String actualMessage = exception.getMessage(); assertTrue(actualMessage.contains(expectedMessage)); } @Test public void whenOptionHasWhitespaceAroundEqualsSign_thenGetLinesPreservesIt() { writer.println("name1 =arg1"); writer.println("name2= arg2"); writer.println("name3 = arg3"); writer.flush(); assertThat(reader.getLines(), contains( "name1 =arg1", "name2= arg2", "name3 = arg3")); } @Test public void whenOptionHasEscapedColons_thenTheyGetUnescaped() { writer.println("host1=example.com\\:8080"); writer.println("host2=example.org:8080"); writer.flush(); assertThat(reader.getLines(), contains( "host1=example.com:8080", "host2=example.org:8080")); } @Test public void whenFileContainsNonOptionLines_getOptionLinesReturnsOnlyOptionLines() { writer.println("# Comment"); writer.println(""); writer.println("name1=arg1"); writer.println("noArgOpt"); writer.flush(); assertThat(reader.getOptionLines(), contains("name1=arg1", "noArgOpt")); } } ================================================ FILE: common/src/test/java/haveno/common/config/ConfigTests.java ================================================ package haveno.common.config; import org.junit.jupiter.api.Test; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.io.PrintWriter; import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; import static haveno.common.config.Config.APP_DATA_DIR; import static haveno.common.config.Config.APP_NAME; import static haveno.common.config.Config.BANNED_XMR_NODES; import static haveno.common.config.Config.CONFIG_FILE; import static haveno.common.config.Config.DEFAULT_CONFIG_FILE_NAME; import static haveno.common.config.Config.HELP; import static haveno.common.config.Config.TORRC_FILE; import static haveno.common.config.Config.USER_DATA_DIR; import static java.io.File.createTempFile; import static java.lang.String.format; import static java.lang.System.getProperty; import static java.nio.file.Files.createTempDirectory; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.emptyString; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; public class ConfigTests { // Note: "DataDirProperties" in the test method names below represent the group of // configuration options that influence the location of a Haveno node's data directory. // These options include appName, userDataDir, appDataDir and configFile @Test public void whenNoArgCtorIsCalled_thenDefaultAppNameIsSetToTempValue() { Config config = new Config(); String defaultAppName = config.defaultAppName; String regex = "Haveno\\d{2,}Temp"; assertTrue(defaultAppName.matches(regex), format("Temp app name '%s' failed to match '%s'", defaultAppName, regex)); } @Test public void whenAppNameOptionIsSet_thenAppNamePropertyDiffersFromDefaultAppNameProperty() { Config config = configWithOpts(opt(APP_NAME, "My-Haveno")); assertThat(config.appName, equalTo("My-Haveno")); assertThat(config.appName, not(equalTo(config.defaultAppName))); } @Test public void whenNoOptionsAreSet_thenDataDirPropertiesEqualDefaultValues() { Config config = new Config(); assertThat(config.appName, equalTo(config.defaultAppName)); assertThat(config.userDataDir, equalTo(config.defaultUserDataDir)); assertThat(config.appDataDir, equalTo(config.defaultAppDataDir)); assertThat(config.configFile, equalTo(config.defaultConfigFile)); } @Test public void whenAppNameOptionIsSet_thenDataDirPropertiesReflectItsValue() { Config config = configWithOpts(opt(APP_NAME, "My-Haveno")); assertThat(config.appName, equalTo("My-Haveno")); assertThat(config.userDataDir, equalTo(config.defaultUserDataDir)); assertThat(config.appDataDir, equalTo(new File(config.userDataDir, "My-Haveno"))); assertThat(config.configFile, equalTo(new File(config.appDataDir, DEFAULT_CONFIG_FILE_NAME))); } @Test public void whenAppDataDirOptionIsSet_thenDataDirPropertiesReflectItsValue() throws IOException { File appDataDir = createTempDirectory("myapp").toFile(); Config config = configWithOpts(opt(APP_DATA_DIR, appDataDir)); assertThat(config.appName, equalTo(config.defaultAppName)); assertThat(config.userDataDir, equalTo(config.defaultUserDataDir)); assertThat(config.appDataDir, equalTo(appDataDir)); assertThat(config.configFile, equalTo(new File(config.appDataDir, DEFAULT_CONFIG_FILE_NAME))); } @Test public void whenUserDataDirOptionIsSet_thenDataDirPropertiesReflectItsValue() throws IOException { File userDataDir = createTempDirectory("myuserdata").toFile(); Config config = configWithOpts(opt(USER_DATA_DIR, userDataDir)); assertThat(config.appName, equalTo(config.defaultAppName)); assertThat(config.userDataDir, equalTo(userDataDir)); assertThat(config.appDataDir, equalTo(new File(userDataDir, config.defaultAppName))); assertThat(config.configFile, equalTo(new File(config.appDataDir, DEFAULT_CONFIG_FILE_NAME))); } @Test public void whenAppNameAndAppDataDirOptionsAreSet_thenDataDirPropertiesReflectTheirValues() throws IOException { File appDataDir = createTempDirectory("myapp").toFile(); Config config = configWithOpts(opt(APP_NAME, "My-Haveno"), opt(APP_DATA_DIR, appDataDir)); assertThat(config.appName, equalTo("My-Haveno")); assertThat(config.userDataDir, equalTo(config.defaultUserDataDir)); assertThat(config.appDataDir, equalTo(appDataDir)); assertThat(config.configFile, equalTo(new File(config.appDataDir, DEFAULT_CONFIG_FILE_NAME))); } @Test public void whenOptionIsSetAtCommandLineAndInConfigFile_thenCommandLineValueTakesPrecedence() throws IOException { File configFile = createTempFile("haveno", "properties"); try (PrintWriter writer = new PrintWriter(configFile)) { writer.println(new ConfigFileOption(APP_NAME, "Haveno-configFileValue")); } Config config = configWithOpts(opt(APP_NAME, "Haveno-commandLineValue")); assertThat(config.appName, equalTo("Haveno-commandLineValue")); } @Test public void whenUnrecognizedOptionIsSet_thenConfigExceptionIsThrown() { Exception exception = assertThrows(ConfigException.class, () -> configWithOpts(opt("bogus"))); String expectedMessage = "problem parsing option 'bogus': bogus is not a recognized option"; String actualMessage = exception.getMessage(); assertTrue(actualMessage.contains(expectedMessage)); } @Test public void whenUnrecognizedOptionIsSetInConfigFile_thenNoExceptionIsThrown() throws IOException { File configFile = createTempFile("haveno", "properties"); try (PrintWriter writer = new PrintWriter(configFile)) { writer.println(new ConfigFileOption("bogusOption", "bogusValue")); writer.println(new ConfigFileOption(APP_NAME, "HavenoTest")); } Config config = configWithOpts(opt(CONFIG_FILE, configFile.getAbsolutePath())); assertThat(config.appName, equalTo("HavenoTest")); } @Test public void whenOptionFileArgumentDoesNotExist_thenConfigExceptionIsThrown() { String filepath = getProperty("os.name").startsWith("Windows") ? "C:\\does\\not\\exist" : "/does/not/exist"; Exception exception = assertThrows(ConfigException.class, () -> configWithOpts(opt(TORRC_FILE, filepath))); String expectedMessage = format("problem parsing option 'torrcFile': File [%s] does not exist", filepath); String actualMessage = exception.getMessage(); assertTrue(actualMessage.contains(expectedMessage)); } @Test public void whenConfigFileOptionIsSetToNonExistentFile_thenConfigExceptionIsThrown() { String filepath = getProperty("os.name").startsWith("Windows") ? "C:\\no\\such\\haveno.properties" : "/no/such/haveno.properties"; Exception exception = assertThrows(ConfigException.class, () -> configWithOpts(opt(CONFIG_FILE, filepath))); String expectedMessage = format("The specified config file '%s' does not exist", filepath); String actualMessage = exception.getMessage(); assertTrue(actualMessage.contains(expectedMessage)); } @Test public void whenConfigFileOptionIsSetInConfigFile_thenConfigExceptionIsThrown() throws IOException { File configFile = createTempFile("haveno", "properties"); try (PrintWriter writer = new PrintWriter(configFile)) { writer.println(new ConfigFileOption(CONFIG_FILE, "/tmp/other.haveno.properties")); } Exception exception = assertThrows(ConfigException.class, () -> configWithOpts(opt(CONFIG_FILE, configFile.getAbsolutePath()))); String expectedMessage = format("The '%s' option is disallowed in config files", CONFIG_FILE); String actualMessage = exception.getMessage(); assertTrue(actualMessage.contains(expectedMessage)); } @Test public void whenConfigFileOptionIsSetToExistingFile_thenConfigFilePropertyReflectsItsValue() throws IOException { File configFile = createTempFile("haveno", "properties"); Config config = configWithOpts(opt(CONFIG_FILE, configFile.getAbsolutePath())); assertThat(config.configFile, equalTo(configFile)); } @Test public void whenConfigFileOptionIsSetToRelativePath_thenThePathIsPrefixedByAppDataDir() throws IOException { File configFile = Files.createTempFile("my-haveno", ".properties").toFile(); File appDataDir = configFile.getParentFile(); String relativeConfigFilePath = configFile.getName(); Config config = configWithOpts(opt(APP_DATA_DIR, appDataDir), opt(CONFIG_FILE, relativeConfigFilePath)); assertThat(config.configFile, equalTo(configFile)); } @Test public void whenAppNameIsSetInConfigFile_thenDataDirPropertiesReflectItsValue() throws IOException { File configFile = createTempFile("haveno", "properties"); try (PrintWriter writer = new PrintWriter(configFile)) { writer.println(new ConfigFileOption(APP_NAME, "My-Haveno")); } Config config = configWithOpts(opt(CONFIG_FILE, configFile.getAbsolutePath())); assertThat(config.appName, equalTo("My-Haveno")); assertThat(config.userDataDir, equalTo(config.defaultUserDataDir)); assertThat(config.appDataDir, equalTo(new File(config.userDataDir, config.appName))); assertThat(config.configFile, equalTo(configFile)); } @Test public void whenBannedXmrNodesOptionIsSet_thenBannedXmrNodesPropertyReturnsItsValue() { Config config = configWithOpts(opt(BANNED_XMR_NODES, "foo.onion:8333,bar.onion:8333")); assertThat(config.bannedXmrNodes, contains("foo.onion:8333", "bar.onion:8333")); } @Test public void whenHelpOptionIsSet_thenIsHelpRequestedIsTrue() { assertFalse(new Config().helpRequested); assertTrue(configWithOpts(opt(HELP)).helpRequested); } @Test public void whenConfigIsConstructed_thenNoConsoleOutputSideEffectsShouldOccur() { PrintStream outOrig = System.out; PrintStream errOrig = System.err; ByteArrayOutputStream outBytes = new ByteArrayOutputStream(); ByteArrayOutputStream errBytes = new ByteArrayOutputStream(); try (PrintStream outTest = new PrintStream(outBytes); PrintStream errTest = new PrintStream(errBytes)) { System.setOut(outTest); System.setErr(errTest); new Config(); assertThat(outBytes.toString(), is(emptyString())); assertThat(errBytes.toString(), is(emptyString())); } finally { System.setOut(outOrig); System.setErr(errOrig); } } @Test public void whenConfigIsConstructed_thenAppDataDirAndSubdirsAreCreated() { Config config = new Config(); assertTrue(config.appDataDir.exists()); assertTrue(config.keyStorageDir.exists()); assertTrue(config.storageDir.exists()); assertTrue(config.torDir.exists()); assertTrue(config.walletDir.exists()); } @Test public void whenAppDataDirCannotBeCreatedThenUncheckedIoExceptionIsThrown() { // set a userDataDir that is actually a file so appDataDir cannot be created Exception exception = assertThrows(UncheckedIOException.class, () -> { File aFile = Files.createTempFile("A", "File").toFile(); configWithOpts(opt(USER_DATA_DIR, aFile)); }); String expectedMessage = "could not be created"; String actualMessage = exception.getMessage(); assertTrue(actualMessage.contains(expectedMessage)); } @Test public void whenAppDataDirIsSymbolicLink_thenAppDataDirCreationIsNoOp() throws IOException { Path parentDir = createTempDirectory("parent"); Path targetDir = parentDir.resolve("target"); Path symlink = parentDir.resolve("symlink"); Files.createDirectory(targetDir); try { Files.createSymbolicLink(symlink, targetDir); } catch (Throwable ex) { // An error occurred trying to create a symbolic link, likely because the // operating system (e.g. Windows) does not support it, so we abort the test. return; } configWithOpts(opt(APP_DATA_DIR, symlink)); } // == TEST SUPPORT FACILITIES ======================================================== static Config configWithOpts(Opt... opts) { String[] args = new String[opts.length]; for (int i = 0; i < opts.length; i++) args[i] = opts[i].toString(); return new Config(args); } static Opt opt(String name) { return new Opt(name); } static Opt opt(String name, Object arg) { return new Opt(name, arg.toString()); } static class Opt { private final String name; private final String arg; public Opt(String name) { this(name, null); } public Opt(String name, String arg) { this.name = name; this.arg = arg; } @Override public String toString() { return format("--%s%s", name, arg != null ? ("=" + arg) : ""); } } } ================================================ FILE: common/src/test/java/haveno/common/util/MathUtilsTest.java ================================================ /* * 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 . */ package haveno.common.util; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; public class MathUtilsTest { @Test public void testRoundDoubleWithInfiniteArg() { assertThrows(IllegalArgumentException.class, () -> MathUtils.roundDouble(Double.POSITIVE_INFINITY, 2)); } @Test public void testRoundDoubleWithNaNArg() { assertThrows(IllegalArgumentException.class, () ->MathUtils.roundDouble(Double.NaN, 2)); } @Test public void testRoundDoubleWithNegativePrecision() { assertThrows(IllegalArgumentException.class, () ->MathUtils.roundDouble(3, -1)); } @SuppressWarnings("OptionalGetWithoutIsPresent") @Test public void testMovingAverageWithoutOutlierExclusion() { var values = new int[]{4, 5, 3, 1, 2, 4}; // Moving average = 4, 4.5, 4, 3, 2, 7/3 var movingAverage = new MathUtils.MovingAverage(3, 0); int i = 0; assertEquals(4, movingAverage.next(values[i++]).get(),0.001); assertEquals(4.5, movingAverage.next(values[i++]).get(),0.001); assertEquals(4, movingAverage.next(values[i++]).get(),0.001); assertEquals(3, movingAverage.next(values[i++]).get(),0.001); assertEquals(2, movingAverage.next(values[i++]).get(),0.001); assertEquals((double) 7 / 3, movingAverage.next(values[i]).get(),0.001); } @SuppressWarnings("OptionalGetWithoutIsPresent") @Test public void testMovingAverageWithOutlierExclusion() { var values = new int[]{100, 102, 95, 101, 120, 115}; // Moving average = N/A, N/A, 99, 99.333..., N/A, 103.666... var movingAverage = new MathUtils.MovingAverage(3, 0.2); int i = 0; assertFalse(movingAverage.next(values[i++]).isPresent()); assertFalse(movingAverage.next(values[i++]).isPresent()); assertEquals(99, movingAverage.next(values[i++]).get(),0.001); assertEquals(99.333, movingAverage.next(values[i++]).get(),0.001); assertFalse(movingAverage.next(values[i++]).isPresent()); assertEquals(103.666, movingAverage.next(values[i]).get(),0.001); } } ================================================ FILE: common/src/test/java/haveno/common/util/PermutationTest.java ================================================ /* * 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 . */ package haveno.common.util; import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.function.BiPredicate; import static org.junit.jupiter.api.Assertions.assertTrue; public class PermutationTest { // @Test public void testGetPartialList() { String blindVote0 = "blindVote0"; String blindVote1 = "blindVote1"; String blindVote2 = "blindVote2"; String blindVote3 = "blindVote3"; String blindVote4 = "blindVote4"; String blindVote5 = "blindVote5"; List list = new ArrayList<>(Arrays.asList(blindVote0, blindVote1, blindVote2, blindVote3, blindVote4, blindVote5)); List indicesToRemove = Arrays.asList(0, 3); List expected = new ArrayList<>(Arrays.asList(blindVote1, blindVote2, blindVote4, blindVote5)); List result = PermutationUtil.getPartialList(list, indicesToRemove); assertTrue(expected.toString().equals(result.toString())); // remove nothing indicesToRemove = new ArrayList<>(); expected = new ArrayList<>(list); result = PermutationUtil.getPartialList(list, indicesToRemove); assertTrue(expected.toString().equals(result.toString())); // remove first indicesToRemove = Collections.singletonList(0); expected = new ArrayList<>(list); expected.remove(0); result = PermutationUtil.getPartialList(list, indicesToRemove); assertTrue(expected.toString().equals(result.toString())); // remove last indicesToRemove = Collections.singletonList(5); expected = new ArrayList<>(list); expected.remove(5); result = PermutationUtil.getPartialList(list, indicesToRemove); assertTrue(expected.toString().equals(result.toString())); // remove all indicesToRemove = Arrays.asList(0, 1, 2, 3, 4, 5); expected = new ArrayList<>(); result = PermutationUtil.getPartialList(list, indicesToRemove); assertTrue(expected.toString().equals(result.toString())); // wrong sorting of indices indicesToRemove = Arrays.asList(4, 0, 1); expected = expected = new ArrayList<>(Arrays.asList(blindVote2, blindVote3, blindVote5)); result = PermutationUtil.getPartialList(list, indicesToRemove); assertTrue(expected.toString().equals(result.toString())); // wrong sorting of indices indicesToRemove = Arrays.asList(0, 0); expected = new ArrayList<>(Arrays.asList(blindVote1, blindVote2, blindVote3, blindVote4, blindVote5)); result = PermutationUtil.getPartialList(list, indicesToRemove); assertTrue(expected.toString().equals(result.toString())); // don't remove as invalid index indicesToRemove = Collections.singletonList(9); expected = new ArrayList<>(list); result = PermutationUtil.getPartialList(list, indicesToRemove); assertTrue(expected.toString().equals(result.toString())); // don't remove as invalid index indicesToRemove = Collections.singletonList(-2); expected = new ArrayList<>(list); result = PermutationUtil.getPartialList(list, indicesToRemove); assertTrue(expected.toString().equals(result.toString())); } @Test public void testFindMatchingPermutation() { String a = "A"; String b = "B"; String c = "C"; String d = "D"; String e = "E"; int limit = 1048575; List result; List list; List expected; BiPredicate> predicate = (target, variationList) -> variationList.toString().equals(target); list = Arrays.asList(a, b, c, d, e); expected = Arrays.asList(a); result = PermutationUtil.findMatchingPermutation(expected.toString(), list, predicate, limit); assertTrue(expected.toString().equals(result.toString())); expected = Arrays.asList(a, c, e); result = PermutationUtil.findMatchingPermutation(expected.toString(), list, predicate, limit); assertTrue(expected.toString().equals(result.toString())); } @Test public void testBreakAtLimit() { BiPredicate> predicate = (target, variationList) -> variationList.toString().equals(target); var list = Arrays.asList("a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o"); var expected = Arrays.asList("b", "g", "m"); // Takes around 32508 tries starting from longer strings var limit = 100000; var result = PermutationUtil.findMatchingPermutation(expected.toString(), list, predicate, limit); assertTrue(expected.toString().equals(result.toString())); limit = 1000; result = PermutationUtil.findMatchingPermutation(expected.toString(), list, predicate, limit); assertTrue(result.isEmpty()); } // @Test public void testFindAllPermutations() { String blindVote0 = "blindVote0"; String blindVote1 = "blindVote1"; String blindVote2 = "blindVote2"; String blindVote3 = "blindVote3"; String blindVote4 = "blindVote4"; // Up to about 1M iterations performance is acceptable (0.5 sec) // findAllPermutations took 580 ms for 20 items and 1048575 iterations // findAllPermutations took 10 ms for 15 items and 32767 iterations // findAllPermutations took 0 ms for 10 items and 1023 iterations // int limit = 1048575; int limit = 1048575000; List list; List> expected; List> result; List subList; list = new ArrayList<>(); /* for (int i = 0; i < 4; i++) { list.add("blindVote" + i); }*/ PermutationUtil.findAllPermutations(list, limit); list = new ArrayList<>(); expected = new ArrayList<>(); result = PermutationUtil.findAllPermutations(list, limit); assertTrue(expected.toString().equals(result.toString())); list = new ArrayList<>(Arrays.asList(blindVote0)); expected = new ArrayList<>(); expected.add(list); result = PermutationUtil.findAllPermutations(list, limit); assertTrue(expected.toString().equals(result.toString())); // 2 items -> 3 variations list = new ArrayList<>(Arrays.asList(blindVote0, blindVote1)); expected = new ArrayList<>(); expected.add(Arrays.asList(list.get(0))); expected.add(Arrays.asList(list.get(1))); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(1)); expected.add(subList); result = PermutationUtil.findAllPermutations(list, limit); assertTrue(expected.toString().equals(result.toString())); // 3 items -> 7 variations list = new ArrayList<>(Arrays.asList(blindVote0, blindVote1, blindVote2)); expected = new ArrayList<>(); expected.add(Arrays.asList(list.get(0))); expected.add(Arrays.asList(list.get(1))); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(1)); expected.add(subList); expected.add(Arrays.asList(list.get(2))); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(2)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(1)); subList.add(list.get(2)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(1)); subList.add(list.get(2)); expected.add(subList); result = PermutationUtil.findAllPermutations(list, limit); assertTrue(expected.toString().equals(result.toString())); // 4 items -> 15 variations list = new ArrayList<>(Arrays.asList(blindVote0, blindVote1, blindVote2, blindVote3)); expected = new ArrayList<>(); expected.add(Arrays.asList(list.get(0))); expected.add(Arrays.asList(list.get(1))); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(1)); expected.add(subList); expected.add(Arrays.asList(list.get(2))); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(2)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(1)); subList.add(list.get(2)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(1)); subList.add(list.get(2)); expected.add(subList); expected.add(Arrays.asList(list.get(3))); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(3)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(1)); subList.add(list.get(3)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(1)); subList.add(list.get(3)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(2)); subList.add(list.get(3)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(2)); subList.add(list.get(3)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(1)); subList.add(list.get(2)); subList.add(list.get(3)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(1)); subList.add(list.get(2)); subList.add(list.get(3)); expected.add(subList); result = PermutationUtil.findAllPermutations(list, limit); assertTrue(expected.toString().equals(result.toString())); // 5 items -> 31 variations list = new ArrayList<>(Arrays.asList(blindVote0, blindVote1, blindVote2, blindVote3, blindVote4)); expected = new ArrayList<>(); expected.add(Arrays.asList(list.get(0))); expected.add(Arrays.asList(list.get(1))); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(1)); expected.add(subList); expected.add(Arrays.asList(list.get(2))); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(2)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(1)); subList.add(list.get(2)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(1)); subList.add(list.get(2)); expected.add(subList); expected.add(Arrays.asList(list.get(3))); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(3)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(1)); subList.add(list.get(3)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(1)); subList.add(list.get(3)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(2)); subList.add(list.get(3)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(2)); subList.add(list.get(3)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(1)); subList.add(list.get(2)); subList.add(list.get(3)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(1)); subList.add(list.get(2)); subList.add(list.get(3)); expected.add(subList); expected.add(Arrays.asList(list.get(4))); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(4)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(1)); subList.add(list.get(4)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(1)); subList.add(list.get(4)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(2)); subList.add(list.get(4)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(2)); subList.add(list.get(4)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(1)); subList.add(list.get(2)); subList.add(list.get(4)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(1)); subList.add(list.get(2)); subList.add(list.get(4)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(3)); subList.add(list.get(4)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(3)); subList.add(list.get(4)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(1)); subList.add(list.get(3)); subList.add(list.get(4)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(1)); subList.add(list.get(3)); subList.add(list.get(4)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(2)); subList.add(list.get(3)); subList.add(list.get(4)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(2)); subList.add(list.get(3)); subList.add(list.get(4)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(1)); subList.add(list.get(2)); subList.add(list.get(3)); subList.add(list.get(4)); expected.add(subList); subList = new ArrayList<>(); subList.add(list.get(0)); subList.add(list.get(1)); subList.add(list.get(2)); subList.add(list.get(3)); subList.add(list.get(4)); expected.add(subList); result = PermutationUtil.findAllPermutations(list, limit); assertTrue(expected.toString().equals(result.toString())); } } ================================================ FILE: common/src/test/java/haveno/common/util/PreconditionsTests.java ================================================ package haveno.common.util; import org.junit.jupiter.api.Test; import java.io.File; import java.io.IOException; import java.nio.file.Files; import static haveno.common.util.Preconditions.checkDir; import static java.lang.String.format; import static java.lang.System.getProperty; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; public class PreconditionsTests { @Test public void whenDirIsValid_thenDirIsReturned() throws IOException { File dir = Files.createTempDirectory("TestDir").toFile(); File ret = checkDir(dir); assertSame(dir, ret); } @Test public void whenDirDoesNotExist_thenThrow() { String filepath = getProperty("os.name").startsWith("Windows") ? "C:\\does\\not\\exist" : "/does/not/exist"; Exception exception = assertThrows(IllegalArgumentException.class, () -> checkDir(new File(filepath))); String expectedMessage = format("Directory '%s' does not exist", filepath); String actualMessage = exception.getMessage(); assertTrue(actualMessage.contains(expectedMessage)); } } ================================================ FILE: common/src/test/java/haveno/common/util/UtilitiesTest.java ================================================ /* * 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 . */ package haveno.common.util; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; public class UtilitiesTest { @Test public void testToStringList() { assertTrue(Utilities.commaSeparatedListToSet(null, false).isEmpty()); assertTrue(Utilities.commaSeparatedListToSet(null, true).isEmpty()); assertTrue(Utilities.commaSeparatedListToSet("", false).isEmpty()); assertTrue(Utilities.commaSeparatedListToSet("", true).isEmpty()); assertTrue(Utilities.commaSeparatedListToSet(" ", false).isEmpty()); assertEquals(1, Utilities.commaSeparatedListToSet(" ", true).size()); assertTrue(Utilities.commaSeparatedListToSet(",", false).isEmpty()); assertTrue(Utilities.commaSeparatedListToSet(",", true).isEmpty()); assertEquals(1, Utilities.commaSeparatedListToSet(",test1", false).size()); assertEquals(1, Utilities.commaSeparatedListToSet(", , test1", false).size()); assertEquals(2, Utilities.commaSeparatedListToSet(", , test1", true).size()); assertEquals(1, Utilities.commaSeparatedListToSet("test1,", false).size()); assertEquals(1, Utilities.commaSeparatedListToSet("test1, ,", false).size()); assertEquals(1, Utilities.commaSeparatedListToSet("test1", false).size()); assertEquals(2, Utilities.commaSeparatedListToSet("test1, test2", false).size()); } @Test public void testIntegerToByteArray() { assertEquals("0000", Utilities.bytesAsHexString(Utilities.integerToByteArray(0, 2))); assertEquals("ffff", Utilities.bytesAsHexString(Utilities.integerToByteArray(65535, 2))); assertEquals("0011", Utilities.bytesAsHexString(Utilities.integerToByteArray(17, 2))); assertEquals("1100", Utilities.bytesAsHexString(Utilities.integerToByteArray(4352, 2))); assertEquals("dd22", Utilities.bytesAsHexString(Utilities.integerToByteArray(56610, 2))); assertEquals("7fffffff", Utilities.bytesAsHexString(Utilities.integerToByteArray(2147483647, 4))); // Integer.MAX_VALUE assertEquals("80000000", Utilities.bytesAsHexString(Utilities.integerToByteArray(-2147483648, 4))); // Integer.MIN_VALUE assertEquals("00110011", Utilities.bytesAsHexString(Utilities.integerToByteArray(1114129, 4))); assertEquals("ffeeffef", Utilities.bytesAsHexString(Utilities.integerToByteArray(-1114129, 4))); } @Test public void testByteArrayToInteger() { assertEquals(0, Utilities.byteArrayToInteger(Utilities.decodeFromHex("0000"))); assertEquals(65535, Utilities.byteArrayToInteger(Utilities.decodeFromHex("ffff"))); assertEquals(4352, Utilities.byteArrayToInteger(Utilities.decodeFromHex("1100"))); assertEquals(17, Utilities.byteArrayToInteger(Utilities.decodeFromHex("0011"))); assertEquals(56610, Utilities.byteArrayToInteger(Utilities.decodeFromHex("dd22"))); assertEquals(2147483647, Utilities.byteArrayToInteger(Utilities.decodeFromHex("7fffffff"))); assertEquals(-2147483648, Utilities.byteArrayToInteger(Utilities.decodeFromHex("80000000"))); assertEquals(1114129, Utilities.byteArrayToInteger(Utilities.decodeFromHex("00110011"))); assertEquals(-1114129, Utilities.byteArrayToInteger(Utilities.decodeFromHex("ffeeffef"))); } } ================================================ FILE: config/checkstyle/checkstyle.xml ================================================ ================================================ FILE: core/.tx/config ================================================ [main] host = https://www.transifex.com [haveno-desktop.displaystringsproperties] file_filter = translations/haveno-desktop.displaystringsproperties/.properties source_lang = en type = UNICODEPROPERTIES ================================================ FILE: core/src/main/java/haveno/core/account/sign/SignedWitness.java ================================================ /* * 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 . */ package haveno.core.account.sign; import com.google.protobuf.ByteString; import haveno.common.app.Capabilities; import haveno.common.app.Capability; import haveno.common.crypto.Hash; import haveno.common.proto.ProtoUtil; import haveno.common.util.Utilities; import haveno.core.trade.HavenoUtils; import haveno.network.p2p.storage.P2PDataStorage; import haveno.network.p2p.storage.payload.CapabilityRequiringPayload; import haveno.network.p2p.storage.payload.DateTolerantPayload; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import haveno.network.p2p.storage.payload.ProcessOncePersistableNetworkPayload; import lombok.Value; import lombok.extern.slf4j.Slf4j; import java.time.Clock; import java.time.Instant; import java.util.concurrent.TimeUnit; // Supports signatures made from EC key (arbitrators) and signature created with DSA key. @Slf4j @Value public class SignedWitness implements ProcessOncePersistableNetworkPayload, PersistableNetworkPayload, DateTolerantPayload, CapabilityRequiringPayload { public enum VerificationMethod { ARBITRATOR, TRADE; public static SignedWitness.VerificationMethod fromProto(protobuf.SignedWitness.VerificationMethod method) { return ProtoUtil.enumFromProto(SignedWitness.VerificationMethod.class, method.name()); } public static protobuf.SignedWitness.VerificationMethod toProtoMessage(SignedWitness.VerificationMethod method) { return protobuf.SignedWitness.VerificationMethod.valueOf(method.name()); } } private static final long TOLERANCE = TimeUnit.DAYS.toMillis(1); private final VerificationMethod verificationMethod; private final byte[] accountAgeWitnessHash; private final byte[] signature; private final byte[] signerPubKey; private final byte[] witnessOwnerPubKey; private final long date; private final long tradeAmount; transient private final byte[] hash; public SignedWitness(VerificationMethod verificationMethod, byte[] accountAgeWitnessHash, byte[] signature, byte[] signerPubKey, byte[] witnessOwnerPubKey, long date, long tradeAmount) { this.verificationMethod = verificationMethod; this.accountAgeWitnessHash = accountAgeWitnessHash.clone(); this.signature = signature.clone(); this.signerPubKey = signerPubKey.clone(); this.witnessOwnerPubKey = witnessOwnerPubKey.clone(); this.date = date; this.tradeAmount = tradeAmount; // The hash is only using the data which does not change in repeated trades between identical users (no date or amount). // We only want to store the first and oldest one and will ignore others. That will also help to protect privacy // so that the total number of trades is not revealed. We use putIfAbsent when we store the data so first // object will win. We consider one signed trade with one peer enough and do not consider repeated trades with // same peer to add more security as if that one would be colluding it would be not detected anyway. The total // number of signed trades with different peers is still available and can be considered more valuable data for // security. byte[] data = Utilities.concatenateByteArrays(accountAgeWitnessHash, signature); data = Utilities.concatenateByteArrays(data, signerPubKey); hash = Hash.getSha256Ripemd160hash(data); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTOBUF /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.PersistableNetworkPayload toProtoMessage() { final protobuf.SignedWitness.Builder builder = protobuf.SignedWitness.newBuilder() .setVerificationMethod(VerificationMethod.toProtoMessage(verificationMethod)) .setAccountAgeWitnessHash(ByteString.copyFrom(accountAgeWitnessHash)) .setSignature(ByteString.copyFrom(signature)) .setSignerPubKey(ByteString.copyFrom(signerPubKey)) .setWitnessOwnerPubKey(ByteString.copyFrom(witnessOwnerPubKey)) .setDate(date) .setTradeAmount(tradeAmount); return protobuf.PersistableNetworkPayload.newBuilder().setSignedWitness(builder).build(); } public protobuf.SignedWitness toProtoSignedWitness() { return toProtoMessage().getSignedWitness(); } public static SignedWitness fromProto(protobuf.SignedWitness proto) { return new SignedWitness( SignedWitness.VerificationMethod.fromProto(proto.getVerificationMethod()), proto.getAccountAgeWitnessHash().toByteArray(), proto.getSignature().toByteArray(), proto.getSignerPubKey().toByteArray(), proto.getWitnessOwnerPubKey().toByteArray(), proto.getDate(), proto.getTradeAmount()); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public boolean isDateInTolerance(Clock clock) { // We don't allow older or newer than 1 day. // Preventing forward dating is also important to protect against a sophisticated attack return Math.abs(clock.millis() - date) <= TOLERANCE; } @Override public boolean verifyHashSize() { return hash.length == 20; } // Pre 1.0.1 version don't know the new message type and throw an error which leads to disconnecting the peer. @Override public Capabilities getRequiredCapabilities() { return new Capabilities(Capability.SIGNED_ACCOUNT_AGE_WITNESS); } @Override public byte[] getHash() { return hash; } public boolean isSignedByArbitrator() { return verificationMethod == VerificationMethod.ARBITRATOR; } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// public P2PDataStorage.ByteArray getHashAsByteArray() { return new P2PDataStorage.ByteArray(hash); } @Override public String toString() { return "SignedWitness{" + "\n verificationMethod=" + verificationMethod + ",\n witnessHash=" + Utilities.bytesAsHexString(accountAgeWitnessHash) + ",\n signature=" + Utilities.bytesAsHexString(signature) + ",\n signerPubKey=" + Utilities.bytesAsHexString(signerPubKey) + ",\n witnessOwnerPubKey=" + Utilities.bytesAsHexString(witnessOwnerPubKey) + ",\n date=" + Instant.ofEpochMilli(date) + ",\n tradeAmount=" + HavenoUtils.formatXmr(tradeAmount, true) + ",\n hash=" + Utilities.bytesAsHexString(hash) + "\n}"; } } ================================================ FILE: core/src/main/java/haveno/core/account/sign/SignedWitnessService.java ================================================ /* * 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 . */ package haveno.core.account.sign; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Charsets; import com.google.inject.Inject; import haveno.common.UserThread; import haveno.common.crypto.CryptoException; import haveno.common.crypto.Hash; import haveno.common.crypto.KeyRing; import haveno.common.crypto.Sig; import haveno.common.util.Utilities; import haveno.core.account.witness.AccountAgeWitness; import haveno.core.filter.FilterManager; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.user.User; import haveno.core.xmr.wallet.Restrictions; import haveno.network.p2p.BootstrapListener; import haveno.network.p2p.P2PService; import haveno.network.p2p.storage.P2PDataStorage; import haveno.network.p2p.storage.persistence.AppendOnlyDataStoreService; import java.math.BigInteger; import java.security.PublicKey; import java.security.SignatureException; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.Stack; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.Utils; @Slf4j public class SignedWitnessService { public static final long SIGNER_AGE_DAYS = 30; private static final long SIGNER_AGE = SIGNER_AGE_DAYS * ChronoUnit.DAYS.getDuration().toMillis(); public static final BigInteger MINIMUM_TRADE_AMOUNT_FOR_SIGNING = Restrictions.getMinTradeAmount(); private final KeyRing keyRing; private final P2PService p2PService; private final ArbitratorManager arbitratorManager; private final SignedWitnessStorageService signedWitnessStorageService; private final User user; private final FilterManager filterManager; private final Map signedWitnessMap = new HashMap<>(); // This map keeps all SignedWitnesses with the same AccountAgeWitnessHash in a Set. // This avoids iterations over the signedWitnessMap for getting the set of such SignedWitnesses. private final Map> signedWitnessSetByAccountAgeWitnessHash = new HashMap<>(); // Iterating over all SignedWitnesses and do a byte array comparison is a bit expensive and // it is called at filtering the offer book many times, so we use a lookup map for fast // access to the set of SignedWitness which match the ownerPubKey. private final Map> signedWitnessSetByOwnerPubKey = new HashMap<>(); // The signature verification calls are rather expensive and called at filtering the offer book many times, // so we cache the results using the hash as key. The hash is created from the accountAgeWitnessHash and the // signature. private final Map verifySignatureWithDSAKeyResultCache = new HashMap<>(); private final Map verifySignatureWithECKeyResultCache = new HashMap<>(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public SignedWitnessService(KeyRing keyRing, P2PService p2PService, ArbitratorManager arbitratorManager, SignedWitnessStorageService signedWitnessStorageService, AppendOnlyDataStoreService appendOnlyDataStoreService, User user, FilterManager filterManager) { this.keyRing = keyRing; this.p2PService = p2PService; this.arbitratorManager = arbitratorManager; this.signedWitnessStorageService = signedWitnessStorageService; this.user = user; this.filterManager = filterManager; // We need to add that early (before onAllServicesInitialized) as it will be used at startup. appendOnlyDataStoreService.addService(signedWitnessStorageService); } /////////////////////////////////////////////////////////////////////////////////////////// // Lifecycle /////////////////////////////////////////////////////////////////////////////////////////// public void onAllServicesInitialized() { p2PService.getP2PDataStorage().addAppendOnlyDataStoreListener(payload -> { if (payload instanceof SignedWitness) addToMap((SignedWitness) payload); }); // At startup the P2PDataStorage initializes earlier, otherwise we get the listener called. signedWitnessStorageService.getMap().values().forEach(e -> { if (e instanceof SignedWitness) addToMap((SignedWitness) e); }); if (p2PService.isBootstrapped()) { onBootstrapComplete(); } else { p2PService.addP2PServiceListener(new BootstrapListener() { @Override public void onDataReceived() { onBootstrapComplete(); } }); } // TODO: Enable cleaning of signed witness list when necessary // cleanSignedWitnesses(); } private void onBootstrapComplete() { if (user.getRegisteredArbitrator() != null) { UserThread.runAfter(this::doRepublishAllSignedWitnesses, 60); } } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public Collection getSignedWitnessMapValues() { return signedWitnessMap.values(); } /** * List of dates as long when accountAgeWitness was signed * * Witnesses that were added but are no longer considered signed won't be shown */ public List getVerifiedWitnessDateList(AccountAgeWitness accountAgeWitness) { if (!isSignedAccountAgeWitness(accountAgeWitness)) { return new ArrayList<>(); } return getSignedWitnessSet(accountAgeWitness).stream() .filter(this::verifySignature) .map(SignedWitness::getDate) .sorted() .collect(Collectors.toList()); } /** * List of dates as long when accountAgeWitness was signed * Not verifying that signatures are correct */ public List getWitnessDateList(AccountAgeWitness accountAgeWitness) { // We do not validate as it would not make sense to cheat one self... return getSignedWitnessSet(accountAgeWitness).stream() .map(SignedWitness::getDate) .sorted() .collect(Collectors.toList()); } public boolean isSignedByArbitrator(AccountAgeWitness accountAgeWitness) { return getSignedWitnessSet(accountAgeWitness).stream() .map(SignedWitness::isSignedByArbitrator) .findAny() .orElse(false); } public boolean isFilteredWitness(AccountAgeWitness accountAgeWitness) { return getSignedWitnessSet(accountAgeWitness).stream() .map(SignedWitness::getWitnessOwnerPubKey) .anyMatch(ownerPubKey -> filterManager.isWitnessSignerPubKeyBanned(Utils.HEX.encode(ownerPubKey))); } private byte[] ownerPubKey(AccountAgeWitness accountAgeWitness) { return getSignedWitnessSet(accountAgeWitness).stream() .map(SignedWitness::getWitnessOwnerPubKey) .findFirst() .orElse(null); } public String ownerPubKeyAsString(AccountAgeWitness accountAgeWitness) { return getSignedWitnessSet(accountAgeWitness).stream() .map(signedWitness -> Utils.HEX.encode(signedWitness.getWitnessOwnerPubKey())) .findFirst() .orElse(""); } @VisibleForTesting public Set getSignedWitnessSetByOwnerPubKey(byte[] ownerPubKey) { return getSignedWitnessMapValues().stream() .filter(e -> Arrays.equals(e.getWitnessOwnerPubKey(), ownerPubKey)) .collect(Collectors.toSet()); } public boolean publishOwnSignedWitness(SignedWitness signedWitness) { if (!Arrays.equals(signedWitness.getWitnessOwnerPubKey(), keyRing.getPubKeyRing().getSignaturePubKeyBytes()) || !verifySigner(signedWitness)) { return false; } log.info("Publish own signedWitness {}", signedWitness); publishSignedWitness(signedWitness); return true; } // Arbitrators sign with EC key public void signAndPublishAccountAgeWitness(BigInteger tradeAmount, AccountAgeWitness accountAgeWitness, ECKey key, PublicKey peersPubKey) { signAndPublishAccountAgeWitness(tradeAmount, accountAgeWitness, key, peersPubKey.getEncoded(), new Date().getTime()); } // Arbitrators sign with EC key public String signAndPublishAccountAgeWitness(AccountAgeWitness accountAgeWitness, ECKey key, byte[] peersPubKey, long time) { var witnessPubKey = peersPubKey == null ? ownerPubKey(accountAgeWitness) : peersPubKey; return signAndPublishAccountAgeWitness(MINIMUM_TRADE_AMOUNT_FOR_SIGNING, accountAgeWitness, key, witnessPubKey, time); } // Arbitrators sign with EC key public String signTraderPubKey(ECKey key, byte[] peersPubKey, long childSignTime) { var time = childSignTime - SIGNER_AGE - 1; var dummyAccountAgeWitness = new AccountAgeWitness(Hash.getRipemd160hash(peersPubKey), time); return signAndPublishAccountAgeWitness(MINIMUM_TRADE_AMOUNT_FOR_SIGNING, dummyAccountAgeWitness, key, peersPubKey, time); } // Arbitrators sign with EC key private String signAndPublishAccountAgeWitness(BigInteger tradeAmount, AccountAgeWitness accountAgeWitness, ECKey key, byte[] peersPubKey, long time) { if (isSignedAccountAgeWitness(accountAgeWitness)) { var err = "Arbitrator trying to sign already signed accountagewitness " + accountAgeWitness.toString(); log.warn(err); return err; } if (peersPubKey == null) { var err = "Trying to sign accountAgeWitness " + accountAgeWitness.toString() + "\nwith owner pubkey=null"; log.warn(err); return err; } String accountAgeWitnessHashAsHex = Utilities.encodeToHex(accountAgeWitness.getHash()); String signatureBase64 = key.signMessage(accountAgeWitnessHashAsHex); SignedWitness signedWitness = new SignedWitness(SignedWitness.VerificationMethod.ARBITRATOR, accountAgeWitness.getHash(), signatureBase64.getBytes(Charsets.UTF_8), key.getPubKey(), peersPubKey, time, tradeAmount.longValueExact()); publishSignedWitness(signedWitness); log.info("Arbitrator signed witness {}", signedWitness.toString()); return ""; } public void selfSignAndPublishAccountAgeWitness(AccountAgeWitness accountAgeWitness) throws CryptoException { log.info("Sign own accountAgeWitness {}", accountAgeWitness); signAndPublishAccountAgeWitness(MINIMUM_TRADE_AMOUNT_FOR_SIGNING, accountAgeWitness, keyRing.getSignatureKeyPair().getPublic()); } // Any peer can sign with DSA key public Optional signAndPublishAccountAgeWitness(BigInteger tradeAmount, AccountAgeWitness accountAgeWitness, PublicKey peersPubKey) throws CryptoException { if (isSignedAccountAgeWitness(accountAgeWitness)) { log.warn("Trader trying to sign already signed accountagewitness {}", accountAgeWitness.toString()); return Optional.empty(); } if (!isSufficientTradeAmountForSigning(tradeAmount)) { log.warn("Trader tried to sign account with too little trade amount"); return Optional.empty(); } byte[] signature = Sig.sign(keyRing.getSignatureKeyPair().getPrivate(), accountAgeWitness.getHash()); SignedWitness signedWitness = new SignedWitness(SignedWitness.VerificationMethod.TRADE, accountAgeWitness.getHash(), signature, keyRing.getSignatureKeyPair().getPublic().getEncoded(), peersPubKey.getEncoded(), new Date().getTime(), tradeAmount.longValueExact()); publishSignedWitness(signedWitness); log.info("Trader signed witness {}", signedWitness.toString()); return Optional.of(signedWitness); } public boolean verifySignature(SignedWitness signedWitness) { if (signedWitness.isSignedByArbitrator()) { return verifySignatureWithECKey(signedWitness); } else { return verifySignatureWithDSAKey(signedWitness); } } private boolean verifySignatureWithECKey(SignedWitness signedWitness) { P2PDataStorage.ByteArray hash = new P2PDataStorage.ByteArray(signedWitness.getHash()); if (verifySignatureWithECKeyResultCache.containsKey(hash)) { return verifySignatureWithECKeyResultCache.get(hash); } try { String message = Utilities.encodeToHex(signedWitness.getAccountAgeWitnessHash()); String signatureBase64 = new String(signedWitness.getSignature(), Charsets.UTF_8); ECKey key = ECKey.fromPublicOnly(signedWitness.getSignerPubKey()); String pubKeyHex = Utilities.encodeToHex(key.getPubKey()); if (arbitratorManager.isPublicKeyInList(pubKeyHex)) { key.verifyMessage(message, signatureBase64); verifySignatureWithECKeyResultCache.put(hash, true); return true; } else { log.warn("Provided EC key is not in list of valid arbitrators: " + pubKeyHex); verifySignatureWithECKeyResultCache.put(hash, false); return false; } } catch (SignatureException e) { log.warn("verifySignature signedWitness failed. signedWitness={}", signedWitness); log.warn("Caused by ", e); verifySignatureWithECKeyResultCache.put(hash, false); return false; } } private boolean verifySignatureWithDSAKey(SignedWitness signedWitness) { P2PDataStorage.ByteArray hash = new P2PDataStorage.ByteArray(signedWitness.getHash()); if (verifySignatureWithDSAKeyResultCache.containsKey(hash)) { return verifySignatureWithDSAKeyResultCache.get(hash); } try { PublicKey signaturePubKey = Sig.getPublicKeyFromBytes(signedWitness.getSignerPubKey()); Sig.verify(signaturePubKey, signedWitness.getAccountAgeWitnessHash(), signedWitness.getSignature()); verifySignatureWithDSAKeyResultCache.put(hash, true); return true; } catch (CryptoException e) { log.warn("verifySignature signedWitness failed. signedWitness={}", signedWitness); log.warn("Caused by ", e); verifySignatureWithDSAKeyResultCache.put(hash, false); return false; } } public Set getSignedWitnessSet(AccountAgeWitness accountAgeWitness) { P2PDataStorage.ByteArray key = new P2PDataStorage.ByteArray(accountAgeWitness.getHash()); return signedWitnessSetByAccountAgeWitnessHash.getOrDefault(key, new HashSet<>()); } // SignedWitness objects signed by arbitrators public Set getArbitratorsSignedWitnessSet(AccountAgeWitness accountAgeWitness) { return getSignedWitnessSet(accountAgeWitness).stream() .filter(SignedWitness::isSignedByArbitrator) .collect(Collectors.toSet()); } // SignedWitness objects signed by any other peer public Set getTrustedPeerSignedWitnessSet(AccountAgeWitness accountAgeWitness) { return getSignedWitnessSet(accountAgeWitness).stream() .filter(e -> !e.isSignedByArbitrator()) .collect(Collectors.toSet()); } public Set getRootSignedWitnessSet(boolean includeSignedByArbitrator) { return getSignedWitnessMapValues().stream() .filter(witness -> getSignedWitnessSetByOwnerPubKey(witness.getSignerPubKey(), new Stack<>()).isEmpty()) .filter(witness -> includeSignedByArbitrator || witness.getVerificationMethod() != SignedWitness.VerificationMethod.ARBITRATOR) .collect(Collectors.toSet()); } // Find first (in time) SignedWitness per missing signer public Set getUnsignedSignerPubKeys() { var oldestUnsignedSigners = new HashMap(); getRootSignedWitnessSet(false).forEach(signedWitness -> oldestUnsignedSigners.compute(new P2PDataStorage.ByteArray(signedWitness.getSignerPubKey()), (key, oldValue) -> oldValue == null ? signedWitness : oldValue.getDate() > signedWitness.getDate() ? signedWitness : oldValue)); return new HashSet<>(oldestUnsignedSigners.values()); } // We go one level up by using the signer Key to lookup for SignedWitness objects which contain the signerKey as // witnessOwnerPubKey private Set getSignedWitnessSetByOwnerPubKey(byte[] ownerPubKey, Stack excluded) { P2PDataStorage.ByteArray key = new P2PDataStorage.ByteArray(ownerPubKey); if (signedWitnessSetByOwnerPubKey.containsKey(key)) { return signedWitnessSetByOwnerPubKey.get(key).stream() .filter(e -> !excluded.contains(new P2PDataStorage.ByteArray(e.getSignerPubKey()))) .collect(Collectors.toSet()); } else { return new HashSet<>(); } } public boolean isSignedAccountAgeWitness(AccountAgeWitness accountAgeWitness) { return isSignerAccountAgeWitness(accountAgeWitness, new Date().getTime() + SIGNER_AGE); } public boolean isSignerAccountAgeWitness(AccountAgeWitness accountAgeWitness) { return isSignerAccountAgeWitness(accountAgeWitness, new Date().getTime()); } public boolean isSufficientTradeAmountForSigning(BigInteger tradeAmount) { return tradeAmount.compareTo(MINIMUM_TRADE_AMOUNT_FOR_SIGNING) >= 0; } private boolean verifySigner(SignedWitness signedWitness) { return getSignedWitnessSetByOwnerPubKey(signedWitness.getWitnessOwnerPubKey(), new Stack<>()).stream() .anyMatch(w -> isValidSignerWitnessInternal(w, signedWitness.getDate(), new Stack<>())); } /** * Checks whether the accountAgeWitness has a valid signature from a peer/arbitrator and is allowed to sign * other accounts. * * @param accountAgeWitness accountAgeWitness * @param time time of signing * @return true if accountAgeWitness is allowed to sign at time, false otherwise. */ private boolean isSignerAccountAgeWitness(AccountAgeWitness accountAgeWitness, long time) { Stack excludedPubKeys = new Stack<>(); Set signedWitnessSet = getSignedWitnessSet(accountAgeWitness); for (SignedWitness signedWitness : signedWitnessSet) { if (isValidSignerWitnessInternal(signedWitness, time, excludedPubKeys)) { return true; } } // If we have not returned in the loops or they have been empty we have not found a valid signer. return false; } /** * Helper to isValidAccountAgeWitness(accountAgeWitness) * * @param signedWitness the signedWitness to validate * @param childSignedWitnessDateMillis the date the child SignedWitness was signed or current time if it is a leaf. * @param excludedPubKeys stack to prevent recursive loops * @return true if signedWitness is valid, false otherwise. */ private boolean isValidSignerWitnessInternal(SignedWitness signedWitness, long childSignedWitnessDateMillis, Stack excludedPubKeys) { if (filterManager.isWitnessSignerPubKeyBanned(Utils.HEX.encode(signedWitness.getWitnessOwnerPubKey()))) { return false; } if (!verifySignature(signedWitness)) { return false; } if (signedWitness.isSignedByArbitrator()) { // If signed by an arbitrator we don't have to check anything else. return true; } else { if (!verifyDate(signedWitness, childSignedWitnessDateMillis)) { return false; } if (excludedPubKeys.size() >= 2000) { // Prevent DoS attack: an attacker floods the SignedWitness db with a long chain that takes lots of time to verify. return false; } excludedPubKeys.push(new P2PDataStorage.ByteArray(signedWitness.getSignerPubKey())); excludedPubKeys.push(new P2PDataStorage.ByteArray(signedWitness.getWitnessOwnerPubKey())); // Iterate over signedWitness signers Set signerSignedWitnessSet = getSignedWitnessSetByOwnerPubKey(signedWitness.getSignerPubKey(), excludedPubKeys); for (SignedWitness signerSignedWitness : signerSignedWitnessSet) { if (isValidSignerWitnessInternal(signerSignedWitness, signedWitness.getDate(), excludedPubKeys)) { return true; } } excludedPubKeys.pop(); excludedPubKeys.pop(); } // If we have not returned in the loops or they have been empty we have not found a valid signer. return false; } private boolean verifyDate(SignedWitness signedWitness, long childSignedWitnessDateMillis) { long childSignedWitnessDateMinusChargebackPeriodMillis = Instant.ofEpochMilli( childSignedWitnessDateMillis).minus(SIGNER_AGE, ChronoUnit.MILLIS).toEpochMilli(); long signedWitnessDateMillis = signedWitness.getDate(); return signedWitnessDateMillis <= childSignedWitnessDateMinusChargebackPeriodMillis; } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// @VisibleForTesting public void addToMap(SignedWitness signedWitness) { signedWitnessMap.putIfAbsent(signedWitness.getHashAsByteArray(), signedWitness); P2PDataStorage.ByteArray accountAgeWitnessHash = new P2PDataStorage.ByteArray(signedWitness.getAccountAgeWitnessHash()); signedWitnessSetByAccountAgeWitnessHash.putIfAbsent(accountAgeWitnessHash, new HashSet<>()); signedWitnessSetByAccountAgeWitnessHash.get(accountAgeWitnessHash).add(signedWitness); P2PDataStorage.ByteArray ownerPubKey = new P2PDataStorage.ByteArray(signedWitness.getWitnessOwnerPubKey()); signedWitnessSetByOwnerPubKey.putIfAbsent(ownerPubKey, new HashSet<>()); signedWitnessSetByOwnerPubKey.get(ownerPubKey).add(signedWitness); } private void publishSignedWitness(SignedWitness signedWitness) { if (!signedWitnessMap.containsKey(signedWitness.getHashAsByteArray())) { log.info("broadcast signed witness {}", signedWitness.toString()); // We set reBroadcast to true to achieve better resilience. p2PService.addPersistableNetworkPayload(signedWitness, true); addToMap(signedWitness); } } private void doRepublishAllSignedWitnesses() { getSignedWitnessMapValues() .forEach(signedWitness -> p2PService.addPersistableNetworkPayload(signedWitness, true)); } @VisibleForTesting public void removeSignedWitness(SignedWitness signedWitness) { signedWitnessMap.remove(signedWitness.getHashAsByteArray()); P2PDataStorage.ByteArray accountAgeWitnessHash = new P2PDataStorage.ByteArray(signedWitness.getAccountAgeWitnessHash()); if (signedWitnessSetByAccountAgeWitnessHash.containsKey(accountAgeWitnessHash)) { Set set = signedWitnessSetByAccountAgeWitnessHash.get(accountAgeWitnessHash); set.remove(signedWitness); if (set.isEmpty()) { signedWitnessSetByAccountAgeWitnessHash.remove(accountAgeWitnessHash); } } P2PDataStorage.ByteArray ownerPubKey = new P2PDataStorage.ByteArray(signedWitness.getWitnessOwnerPubKey()); if (signedWitnessSetByOwnerPubKey.containsKey(ownerPubKey)) { Set set = signedWitnessSetByOwnerPubKey.get(ownerPubKey); set.remove(signedWitness); if (set.isEmpty()) { signedWitnessSetByOwnerPubKey.remove(ownerPubKey); } } } // Remove SignedWitnesses that are signed by TRADE that also have an ARBITRATOR signature // for the same ownerPubKey and AccountAgeWitnessHash // private void cleanSignedWitnesses() { // var orphans = getRootSignedWitnessSet(false); // var signedWitnessesCopy = new HashSet<>(signedWitnessMap.values()); // signedWitnessesCopy.forEach(sw -> orphans.forEach(orphan -> { // if (sw.getVerificationMethod() == SignedWitness.VerificationMethod.ARBITRATOR && // Arrays.equals(sw.getWitnessOwnerPubKey(), orphan.getWitnessOwnerPubKey()) && // Arrays.equals(sw.getAccountAgeWitnessHash(), orphan.getAccountAgeWitnessHash())) { // signedWitnessMap.remove(orphan.getHashAsByteArray()); // log.info("Remove duplicate SignedWitness: {}", orphan.toString()); // } // })); // } } ================================================ FILE: core/src/main/java/haveno/core/account/sign/SignedWitnessStorageService.java ================================================ /* * 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 . */ package haveno.core.account.sign; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.common.persistence.PersistenceManager; import haveno.network.p2p.storage.P2PDataStorage; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import haveno.network.p2p.storage.persistence.MapStoreService; import java.io.File; import java.util.Map; import lombok.extern.slf4j.Slf4j; @Slf4j public class SignedWitnessStorageService extends MapStoreService { private static final String FILE_NAME = "SignedWitnessStore"; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public SignedWitnessStorageService(@Named(Config.STORAGE_DIR) File storageDir, PersistenceManager persistenceManager) { super(storageDir, persistenceManager); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void initializePersistenceManager() { persistenceManager.initialize(store, PersistenceManager.Source.NETWORK); } @Override public String getFileName() { return FILE_NAME; } @Override public Map getMap() { return store.getMap(); } @Override public boolean canHandle(PersistableNetworkPayload payload) { return payload instanceof SignedWitness; } /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// @Override protected SignedWitnessStore createStore() { return new SignedWitnessStore(); } } ================================================ FILE: core/src/main/java/haveno/core/account/sign/SignedWitnessStore.java ================================================ /* * 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 . */ package haveno.core.account.sign; import com.google.protobuf.Message; import haveno.network.p2p.storage.persistence.PersistableNetworkPayloadStore; import lombok.extern.slf4j.Slf4j; import java.util.List; import java.util.stream.Collectors; /** * We store only the payload in the PB file to save disc space. The hash of the payload can be created anyway and * is only used as key in the map. So we have a hybrid data structure which is represented as list in the protobuf * definition and provide a hashMap for the domain access. */ @Slf4j public class SignedWitnessStore extends PersistableNetworkPayloadStore { SignedWitnessStore() { } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private SignedWitnessStore(List list) { super(list); } public Message toProtoMessage() { return protobuf.PersistableEnvelope.newBuilder() .setSignedWitnessStore(getBuilder()) .build(); } private protobuf.SignedWitnessStore.Builder getBuilder() { final List protoList = map.values().stream() .map(payload -> (SignedWitness) payload) .map(SignedWitness::toProtoSignedWitness) .collect(Collectors.toList()); return protobuf.SignedWitnessStore.newBuilder().addAllItems(protoList); } public static SignedWitnessStore fromProto(protobuf.SignedWitnessStore proto) { List list = proto.getItemsList().stream() .map(SignedWitness::fromProto).collect(Collectors.toList()); return new SignedWitnessStore(list); } } ================================================ FILE: core/src/main/java/haveno/core/account/witness/AccountAgeWitness.java ================================================ /* * 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 . */ package haveno.core.account.witness; import com.google.protobuf.ByteString; import haveno.common.util.Utilities; import haveno.network.p2p.storage.P2PDataStorage; import haveno.network.p2p.storage.payload.DateTolerantPayload; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import haveno.network.p2p.storage.payload.ProcessOncePersistableNetworkPayload; import lombok.Value; import lombok.extern.slf4j.Slf4j; import java.time.Clock; import java.time.Instant; import java.util.concurrent.TimeUnit; // Object has 28 raw bytes (33 bytes is size of ProtoBuffer object in storage list, 5 byte extra for list -> totalBytes = 5 + n*33) // With 1 000 000 entries we get about 33 MB of data. Old entries will be shipped with the resource file, // so only the newly added objects since the last release will be retrieved over the P2P network. @Slf4j @Value public class AccountAgeWitness implements ProcessOncePersistableNetworkPayload, PersistableNetworkPayload, DateTolerantPayload { private static final long TOLERANCE = TimeUnit.DAYS.toMillis(1); private final byte[] hash; // Ripemd160(Sha256(concatenated accountHash, signature and sigPubKey)); 20 bytes private final long date; // 8 byte public AccountAgeWitness(byte[] hash, long date) { this.hash = hash; this.date = date; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.PersistableNetworkPayload toProtoMessage() { final protobuf.AccountAgeWitness.Builder builder = protobuf.AccountAgeWitness.newBuilder() .setHash(ByteString.copyFrom(hash)) .setDate(date); return protobuf.PersistableNetworkPayload.newBuilder().setAccountAgeWitness(builder).build(); } public protobuf.AccountAgeWitness toProtoAccountAgeWitness() { return toProtoMessage().getAccountAgeWitness(); } public static AccountAgeWitness fromProto(protobuf.AccountAgeWitness proto) { byte[] hash = proto.getHash().toByteArray(); if (hash.length != 20) { log.warn("We got a a hash which is not 20 bytes"); hash = new byte[0]; } return new AccountAgeWitness( hash, proto.getDate()); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public boolean isDateInTolerance(Clock clock) { // We don't allow older or newer than 1 day. // Preventing forward dating is also important to protect against a sophisticated attack return Math.abs(clock.millis() - date) <= TOLERANCE; } @Override public boolean verifyHashSize() { return hash.length == 20; } @Override public byte[] getHash() { return hash; } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// P2PDataStorage.ByteArray getHashAsByteArray() { return new P2PDataStorage.ByteArray(hash); } @Override public String toString() { return "AccountAgeWitness{" + "\n hash=" + Utilities.bytesAsHexString(hash) + ",\n date=" + Instant.ofEpochMilli(date) + "\n}"; } } ================================================ FILE: core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java ================================================ /* * 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 . */ package haveno.core.account.witness; import com.google.common.annotations.VisibleForTesting; import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.Inject; import haveno.common.UserThread; import haveno.common.crypto.CryptoException; import haveno.common.crypto.Hash; import haveno.common.crypto.KeyRing; import haveno.common.crypto.PubKeyRing; import haveno.common.crypto.Sig; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.util.MathUtils; import haveno.common.util.Tuple2; import haveno.common.util.Utilities; import haveno.core.account.sign.SignedWitness; import haveno.core.account.sign.SignedWitnessService; import haveno.core.filter.FilterManager; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OfferRestrictions; import haveno.core.payment.ChargeBackRisk; import haveno.core.payment.PaymentAccount; import haveno.core.payment.TradeLimits; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeResult; import haveno.core.support.dispute.arbitration.TraderDataItem; import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.Trade; import haveno.core.trade.protocol.TradePeer; import haveno.core.user.User; import haveno.network.p2p.BootstrapListener; import haveno.network.p2p.P2PService; import haveno.network.p2p.storage.P2PDataStorage; import haveno.network.p2p.storage.persistence.AppendOnlyDataStoreService; import java.math.BigInteger; import java.security.PublicKey; import java.time.Clock; import java.util.Arrays; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Random; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.Utils; @Slf4j public class AccountAgeWitnessService { private static final Date RELEASE = Utilities.getUTCDate(2017, GregorianCalendar.NOVEMBER, 11); private static final long SAFE_ACCOUNT_AGE_DATE = Utilities.getUTCDate(2019, GregorianCalendar.MARCH, 1).getTime(); public enum AccountAge { UNVERIFIED, LESS_ONE_MONTH, ONE_TO_TWO_MONTHS, TWO_MONTHS_OR_MORE } public enum SignState { UNSIGNED(Res.get("offerbook.timeSinceSigning.notSigned")), ARBITRATOR(Res.get("offerbook.timeSinceSigning.info.arbitrator")), PEER_INITIAL(Res.get("offerbook.timeSinceSigning.info.peer")), PEER_LIMIT_LIFTED(Res.get("offerbook.timeSinceSigning.info.peerLimitLifted")), PEER_SIGNER(Res.get("offerbook.timeSinceSigning.info.signer")), BANNED(Res.get("offerbook.timeSinceSigning.info.banned")); private String displayString; private String hash = ""; private long daysUntilLimitLifted = 0; SignState(String displayString) { this.displayString = displayString; } public SignState addHash(String hash) { this.hash = hash; return this; } public SignState setDaysUntilLimitLifted(long days) { this.daysUntilLimitLifted = days; return this; } public String getDisplayString() { if (!hash.isEmpty()) { // Only showing in DEBUG mode return displayString + " " + hash; } return String.format(displayString, daysUntilLimitLifted); } public boolean isLimitLifted() { return this == PEER_LIMIT_LIFTED || this == PEER_SIGNER || this == ARBITRATOR; } } private final KeyRing keyRing; private final P2PService p2PService; private final User user; private final SignedWitnessService signedWitnessService; private final ChargeBackRisk chargeBackRisk; private final AccountAgeWitnessStorageService accountAgeWitnessStorageService; private final Clock clock; private final FilterManager filterManager; @Getter private final AccountAgeWitnessUtils accountAgeWitnessUtils; private final Map accountAgeWitnessMap = new HashMap<>(); // The accountAgeWitnessMap is very large (70k items) and access is a bit expensive. We usually only access less // than 100 items, those who have offers online. So we use a cache for a fast lookup and only if // not found there we use the accountAgeWitnessMap and put then the new item into our cache. private final Map accountAgeWitnessCache = new ConcurrentHashMap<>(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public AccountAgeWitnessService(KeyRing keyRing, P2PService p2PService, User user, SignedWitnessService signedWitnessService, ChargeBackRisk chargeBackRisk, AccountAgeWitnessStorageService accountAgeWitnessStorageService, AppendOnlyDataStoreService appendOnlyDataStoreService, Clock clock, FilterManager filterManager) { this.keyRing = keyRing; this.p2PService = p2PService; this.user = user; this.signedWitnessService = signedWitnessService; this.chargeBackRisk = chargeBackRisk; this.accountAgeWitnessStorageService = accountAgeWitnessStorageService; this.clock = clock; this.filterManager = filterManager; accountAgeWitnessUtils = new AccountAgeWitnessUtils( this, signedWitnessService, keyRing); // We need to add that early (before onAllServicesInitialized) as it will be used at startup. appendOnlyDataStoreService.addService(accountAgeWitnessStorageService); } /////////////////////////////////////////////////////////////////////////////////////////// // Lifecycle /////////////////////////////////////////////////////////////////////////////////////////// public void onAllServicesInitialized() { p2PService.getP2PDataStorage().addAppendOnlyDataStoreListener(payload -> { if (payload instanceof AccountAgeWitness) addToMap((AccountAgeWitness) payload); }); // At startup the P2PDataStorage initializes earlier, otherwise we get the listener called. accountAgeWitnessStorageService.getMapOfAllData().values().stream() .filter(e -> e instanceof AccountAgeWitness) .map(e -> (AccountAgeWitness) e) .forEach(this::addToMap); if (p2PService.isBootstrapped()) { onBootStrapped(); } else { p2PService.addP2PServiceListener(new BootstrapListener() { @Override public void onDataReceived() { onBootStrapped(); } }); } } private void onBootStrapped() { republishAllTraditionalAccounts(); signAndPublishSameNameAccounts(); } // At startup we re-publish the witness data of all traditional accounts to ensure we got our data well distributed. private void republishAllTraditionalAccounts() { if (user.getPaymentAccounts() != null) user.getPaymentAccounts().stream() .filter(account -> account.getPaymentMethod().isTraditional()) .forEach(account -> { AccountAgeWitness myWitness = getMyWitness(account.getPaymentAccountPayload()); // We only publish if the date of our witness is inside the date tolerance. // It would be rejected otherwise from the peers. if (myWitness.isDateInTolerance(clock)) { // We delay with a random interval of 20-60 sec to ensure to be better connected and don't // stress the P2P network with publishing all at once at startup time. int delayInSec = 20 + new Random().nextInt(40); UserThread.runAfter(() -> p2PService.addPersistableNetworkPayload(myWitness, true), delayInSec); } }); } @VisibleForTesting public void addToMap(AccountAgeWitness accountAgeWitness) { synchronized (this) { accountAgeWitnessMap.putIfAbsent(accountAgeWitness.getHashAsByteArray(), accountAgeWitness); } } /////////////////////////////////////////////////////////////////////////////////////////// // Generic /////////////////////////////////////////////////////////////////////////////////////////// public void publishMyAccountAgeWitness(PaymentAccountPayload paymentAccountPayload) { synchronized (this) { AccountAgeWitness accountAgeWitness = getMyWitness(paymentAccountPayload); P2PDataStorage.ByteArray hash = accountAgeWitness.getHashAsByteArray(); // We use first our fast lookup cache. If its in accountAgeWitnessCache it is also in accountAgeWitnessMap // and we do not publish. if (accountAgeWitnessCache.containsKey(hash)) { return; } if (!accountAgeWitnessMap.containsKey(hash)) { p2PService.addPersistableNetworkPayload(accountAgeWitness, false); } } } public byte[] getPeerAccountAgeWitnessHash(Trade trade) { return findTradePeerWitness(trade) .map(AccountAgeWitness::getHash) .orElse(null); } byte[] getAccountInputDataWithSalt(PaymentAccountPayload paymentAccountPayload) { return Utilities.concatenateByteArrays(paymentAccountPayload.getAgeWitnessInputData(), paymentAccountPayload.getSalt()); } @VisibleForTesting public AccountAgeWitness getNewWitness(PaymentAccountPayload paymentAccountPayload, PubKeyRing pubKeyRing) { byte[] accountInputDataWithSalt = getAccountInputDataWithSalt(paymentAccountPayload); byte[] hash = Hash.getSha256Ripemd160hash(Utilities.concatenateByteArrays(accountInputDataWithSalt, pubKeyRing.getSignaturePubKeyBytes())); return new AccountAgeWitness(hash, new Date().getTime()); } public Optional findWitness(PaymentAccountPayload paymentAccountPayload, PubKeyRing pubKeyRing) { if (paymentAccountPayload == null) { return Optional.empty(); } byte[] accountInputDataWithSalt = getAccountInputDataWithSalt(paymentAccountPayload); byte[] hash = Hash.getSha256Ripemd160hash(Utilities.concatenateByteArrays(accountInputDataWithSalt, pubKeyRing.getSignaturePubKeyBytes())); return getWitnessByHash(hash); } public Optional findWitness(Offer offer) { final Optional accountAgeWitnessHash = offer.getAccountAgeWitnessHashAsHex(); return accountAgeWitnessHash.isPresent() ? getWitnessByHashAsHex(accountAgeWitnessHash.get()) : Optional.empty(); } private Optional findTradePeerWitness(Trade trade) { if (trade instanceof ArbitratorTrade) return Optional.empty(); // TODO (woodser): arbitrator trade has two peers TradePeer tradePeer = trade.getTradePeer(); return (tradePeer == null || tradePeer.getPaymentAccountPayload() == null || tradePeer.getPubKeyRing() == null) ? Optional.empty() : findWitness(tradePeer.getPaymentAccountPayload(), tradePeer.getPubKeyRing()); } private Optional getWitnessByHash(byte[] hash) { P2PDataStorage.ByteArray hashAsByteArray = new P2PDataStorage.ByteArray(hash); synchronized (this) { // First we look up in our fast lookup cache if (accountAgeWitnessCache.containsKey(hashAsByteArray)) { return Optional.of(accountAgeWitnessCache.get(hashAsByteArray)); } if (accountAgeWitnessMap.containsKey(hashAsByteArray)) { AccountAgeWitness accountAgeWitness = accountAgeWitnessMap.get(hashAsByteArray); // We add it to our fast lookup cache accountAgeWitnessCache.put(hashAsByteArray, accountAgeWitness); return Optional.of(accountAgeWitness); } return Optional.empty(); } } private Optional getWitnessByHashAsHex(String hashAsHex) { return getWitnessByHash(Utilities.decodeFromHex(hashAsHex)); } /////////////////////////////////////////////////////////////////////////////////////////// // Witness age /////////////////////////////////////////////////////////////////////////////////////////// public long getAccountAge(AccountAgeWitness accountAgeWitness, Date now) { log.debug("getAccountAge now={}, accountAgeWitness.getDate()={}", now.getTime(), accountAgeWitness.getDate()); return now.getTime() - accountAgeWitness.getDate(); } // Return -1 if no witness found public long getAccountAge(PaymentAccountPayload paymentAccountPayload, PubKeyRing pubKeyRing) { return findWitness(paymentAccountPayload, pubKeyRing) .map(accountAgeWitness -> getAccountAge(accountAgeWitness, new Date())) .orElse(-1L); } public long getAccountAge(Offer offer) { return findWitness(offer) .map(accountAgeWitness -> getAccountAge(accountAgeWitness, new Date())) .orElse(-1L); } public long getAccountAge(Trade trade) { return findTradePeerWitness(trade) .map(accountAgeWitness -> getAccountAge(accountAgeWitness, new Date())) .orElse(-1L); } /////////////////////////////////////////////////////////////////////////////////////////// // Signed age /////////////////////////////////////////////////////////////////////////////////////////// // Return -1 if not signed public long getWitnessSignAge(AccountAgeWitness accountAgeWitness, Date now) { List dates = signedWitnessService.getVerifiedWitnessDateList(accountAgeWitness); if (dates.isEmpty()) { return -1L; } else { return now.getTime() - dates.get(0); } } // Return -1 if not signed public long getWitnessSignAge(Offer offer, Date now) { return findWitness(offer) .map(witness -> getWitnessSignAge(witness, now)) .orElse(-1L); } public long getWitnessSignAge(Trade trade, Date now) { return findTradePeerWitness(trade) .map(witness -> getWitnessSignAge(witness, now)) .orElse(-1L); } public AccountAge getPeersAccountAgeCategory(long peersAccountAge) { return getAccountAgeCategory(peersAccountAge); } private AccountAge getAccountAgeCategory(long accountAge) { if (accountAge < 0) { return AccountAge.UNVERIFIED; } else if (accountAge < TimeUnit.DAYS.toMillis(30)) { return AccountAge.LESS_ONE_MONTH; } else if (accountAge < TimeUnit.DAYS.toMillis(60)) { return AccountAge.ONE_TO_TWO_MONTHS; } else { return AccountAge.TWO_MONTHS_OR_MORE; } } // Get trade limit based on a time schedule // Buying of BTC with a payment method that has chargeback risk will use a low trade limit schedule // All selling and all other fiat payment methods use the normal trade limit schedule // Non fiat always has max limit // Account types that can get signed will use time since signing, other methods use time since account age creation // when measuring account age private BigInteger getTradeLimit(BigInteger maxTradeLimit, String currencyCode, AccountAgeWitness accountAgeWitness, AccountAge accountAgeCategory, OfferDirection direction, PaymentMethod paymentMethod) { if (CurrencyUtil.isCryptoCurrency(currencyCode) || !PaymentMethod.hasChargebackRisk(paymentMethod, currencyCode) || direction == OfferDirection.SELL) { return maxTradeLimit; } BigInteger limit = OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT; var factor = signedBuyFactor(accountAgeCategory); if (factor > 0) { limit = BigInteger.valueOf(MathUtils.roundDoubleToLong(maxTradeLimit.longValueExact() * factor)); } if (accountAgeWitness != null) { log.debug("limit={}, factor={}, accountAgeWitnessHash={}", limit, factor, Utilities.bytesAsHexString(accountAgeWitness.getHash())); } return limit; } private double signedBuyFactor(AccountAge accountAgeCategory) { switch (accountAgeCategory) { case TWO_MONTHS_OR_MORE: return 1; case ONE_TO_TWO_MONTHS: return 0.5; case LESS_ONE_MONTH: case UNVERIFIED: default: return 0; } } private double normalFactor() { return 1; } /////////////////////////////////////////////////////////////////////////////////////////// // Trade limit exceptions /////////////////////////////////////////////////////////////////////////////////////////// private boolean isImmature(AccountAgeWitness accountAgeWitness) { return accountAgeWitness.getDate() > SAFE_ACCOUNT_AGE_DATE; } public boolean myHasTradeLimitException(PaymentAccount myPaymentAccount) { return hasTradeLimitException(getMyWitness(myPaymentAccount.getPaymentAccountPayload())); } // There are no trade limits on accounts that // - are mature // - were signed by an arbitrator private boolean hasTradeLimitException(AccountAgeWitness accountAgeWitness) { return !isImmature(accountAgeWitness) || signedWitnessService.isSignedByArbitrator(accountAgeWitness); } /////////////////////////////////////////////////////////////////////////////////////////// // My witness /////////////////////////////////////////////////////////////////////////////////////////// public AccountAgeWitness getMyWitness(PaymentAccountPayload paymentAccountPayload) { final Optional accountAgeWitnessOptional = findWitness(paymentAccountPayload, keyRing.getPubKeyRing()); return accountAgeWitnessOptional.orElseGet(() -> getNewWitness(paymentAccountPayload, keyRing.getPubKeyRing())); } private byte[] getMyWitnessHash(PaymentAccountPayload paymentAccountPayload) { return getMyWitness(paymentAccountPayload).getHash(); } public String getMyWitnessHashAsHex(PaymentAccountPayload paymentAccountPayload) { return Utilities.bytesAsHexString(getMyWitnessHash(paymentAccountPayload)); } public long getMyAccountAge(PaymentAccountPayload paymentAccountPayload) { return getAccountAge(getMyWitness(paymentAccountPayload), new Date()); } public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferDirection direction, boolean buyerAsTakerWithoutDeposit) { if (paymentAccount == null) return 0; if (buyerAsTakerWithoutDeposit) { TradeLimits tradeLimits = new TradeLimits(); return tradeLimits.getMaxTradeLimitBuyerAsTakerWithoutDeposit().longValueExact(); } AccountAgeWitness accountAgeWitness = getMyWitness(paymentAccount.getPaymentAccountPayload()); BigInteger maxTradeLimit = paymentAccount.getPaymentMethod().getMaxTradeLimit(currencyCode); if (hasTradeLimitException(accountAgeWitness)) { return maxTradeLimit.longValueExact(); } final long accountSignAge = getWitnessSignAge(accountAgeWitness, new Date()); AccountAge accountAgeCategory = getAccountAgeCategory(accountSignAge); return getTradeLimit(maxTradeLimit, currencyCode, accountAgeWitness, accountAgeCategory, direction, paymentAccount.getPaymentMethod()).longValueExact(); } public long getUnsignedTradeLimit(PaymentMethod paymentMethod, String currencyCode, OfferDirection direction) { return getTradeLimit(paymentMethod.getMaxTradeLimit(currencyCode), currencyCode, null, AccountAge.UNVERIFIED, direction, paymentMethod).longValueExact(); } /////////////////////////////////////////////////////////////////////////////////////////// // Verification /////////////////////////////////////////////////////////////////////////////////////////// public boolean verifyAccountAgeWitness(Trade trade, PaymentAccountPayload peersPaymentAccountPayload, PubKeyRing peersPubKeyRing, byte[] nonce, byte[] signature, ErrorMessageHandler errorMessageHandler) { log.info("Verifying account age witness for {} {}, payment account payload hash={}, nonce={}, signature={}", trade.getClass().getSimpleName(), trade.getId(), Utilities.bytesAsHexString(peersPaymentAccountPayload.getHash()), Utilities.bytesAsHexString(nonce), Utilities.bytesAsHexString(signature)); final Optional accountAgeWitnessOptional = findWitness(peersPaymentAccountPayload, peersPubKeyRing); // If we don't find a stored witness data we create a new dummy object which makes is easier to reuse the // below validation methods. This peersWitness object is not used beside for validation. Some of the // validation calls are pointless in the case we create a new Witness ourselves but the verifyPeersTradeLimit // need still be called, so we leave also the rest for sake of simplicity. AccountAgeWitness peersWitness; if (accountAgeWitnessOptional.isPresent()) { peersWitness = accountAgeWitnessOptional.get(); } else { log.warn("We did not find the peers witness data. That is expected with peers using an older version."); peersWitness = getNewWitness(peersPaymentAccountPayload, peersPubKeyRing); } // Check if date in witness is not older than the release date of that feature (was added in v0.6) if (!isDateAfterReleaseDate(peersWitness.getDate(), RELEASE, errorMessageHandler)) return false; final byte[] peersAccountInputDataWithSalt = Utilities.concatenateByteArrays( peersPaymentAccountPayload.getAgeWitnessInputData(), peersPaymentAccountPayload.getSalt()); byte[] hash = Hash.getSha256Ripemd160hash(Utilities.concatenateByteArrays(peersAccountInputDataWithSalt, peersPubKeyRing.getSignaturePubKeyBytes())); // Check if the hash in the witness data matches the hash derived from the data provided by the peer final byte[] peersWitnessHash = peersWitness.getHash(); if (!verifyWitnessHash(peersWitnessHash, hash, errorMessageHandler)) return false; // Check if the peers trade limit is not less than the trade amount if (!verifyPeersTradeLimit(trade.getOffer(), trade.getAmount(), peersWitness, new Date(), errorMessageHandler)) { log.error("verifyPeersTradeLimit failed: peersPaymentAccountPayload {}", peersPaymentAccountPayload); return false; } // Check if the signature is correct return verifySignature(peersPubKeyRing.getSignaturePubKey(), nonce, signature, errorMessageHandler); } public boolean verifyPeersTradeAmount(Offer offer, BigInteger tradeAmount, ErrorMessageHandler errorMessageHandler) { checkNotNull(offer); // In case we don't find the witness we check if the trade amount is above the // TOLERATED_SMALL_TRADE_AMOUNT (0.01 BTC) and only in that case return false. return findWitness(offer) .map(witness -> verifyPeersTradeLimit(offer, tradeAmount, witness, new Date(), errorMessageHandler)) .orElse(isToleratedSmalleAmount(tradeAmount)); } private boolean isToleratedSmalleAmount(BigInteger tradeAmount) { return tradeAmount.longValueExact() <= OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT.longValueExact(); } /////////////////////////////////////////////////////////////////////////////////////////// // Package scope verification subroutines /////////////////////////////////////////////////////////////////////////////////////////// boolean isDateAfterReleaseDate(long witnessDateAsLong, Date ageWitnessReleaseDate, ErrorMessageHandler errorMessageHandler) { // Release date minus 1 day as tolerance for not synced clocks Date releaseDateWithTolerance = new Date(ageWitnessReleaseDate.getTime() - TimeUnit.DAYS.toMillis(1)); final Date witnessDate = new Date(witnessDateAsLong); final boolean result = witnessDate.after(releaseDateWithTolerance); if (!result) { final String msg = "Witness date is set earlier than release date of ageWitness feature. " + "ageWitnessReleaseDate=" + ageWitnessReleaseDate + ", witnessDate=" + witnessDate; log.warn(msg); errorMessageHandler.handleErrorMessage(msg); } return result; } public boolean verifyPeersCurrentDate(Date peersCurrentDate) { boolean result = Math.abs(peersCurrentDate.getTime() - new Date().getTime()) <= TimeUnit.DAYS.toMillis(1); if (!result) { String msg = "Peers current date is further than 1 day off to our current date. " + "PeersCurrentDate=" + peersCurrentDate + "; myCurrentDate=" + new Date(); throw new RuntimeException(msg); } return result; } private boolean verifyWitnessHash(byte[] witnessHash, byte[] hash, ErrorMessageHandler errorMessageHandler) { final boolean result = Arrays.equals(witnessHash, hash); if (!result) { final String msg = "witnessHash is not matching peers hash. " + "witnessHash=" + Utilities.bytesAsHexString(witnessHash) + ", hash=" + Utilities.bytesAsHexString(hash); log.warn(msg); errorMessageHandler.handleErrorMessage(msg); } return result; } private boolean verifyPeersTradeLimit(Offer offer, BigInteger tradeAmount, AccountAgeWitness peersWitness, Date peersCurrentDate, ErrorMessageHandler errorMessageHandler) { checkNotNull(offer); final String currencyCode = offer.getCounterCurrencyCode(); final BigInteger defaultMaxTradeLimit = offer.getPaymentMethod().getMaxTradeLimit(currencyCode); BigInteger peersCurrentTradeLimit = defaultMaxTradeLimit; if (!hasTradeLimitException(peersWitness)) { final long accountSignAge = getWitnessSignAge(peersWitness, peersCurrentDate); AccountAge accountAgeCategory = getPeersAccountAgeCategory(accountSignAge); OfferDirection direction = offer.isMyOffer(keyRing) ? offer.getMirroredDirection() : offer.getDirection(); peersCurrentTradeLimit = getTradeLimit(defaultMaxTradeLimit, currencyCode, peersWitness, accountAgeCategory, direction, offer.getPaymentMethod()); } // Makers current trade limit cannot be smaller than that in the offer boolean result = tradeAmount.longValueExact() <= peersCurrentTradeLimit.longValueExact(); if (!result) { String msg = "The peers trade limit is less than the traded amount.\n" + "tradeAmount=" + tradeAmount + "\nPeers trade limit=" + peersCurrentTradeLimit + "\nOffer ID=" + offer.getShortId() + "\nPaymentMethod=" + offer.getPaymentMethod().getId() + "\nCurrencyCode=" + offer.getCounterCurrencyCode(); log.warn(msg); errorMessageHandler.handleErrorMessage(msg); } return result; } boolean verifySignature(PublicKey peersPublicKey, byte[] nonce, byte[] signature, ErrorMessageHandler errorMessageHandler) { boolean result; try { result = Sig.verify(peersPublicKey, nonce, signature); } catch (CryptoException e) { log.warn(e.toString()); result = false; } if (!result) { final String msg = "Signature of nonce is not correct. " + "peersPublicKey=" + peersPublicKey + ", nonce(hex)=" + Utilities.bytesAsHexString(nonce) + ", signature=" + Utilities.bytesAsHexString(signature); log.warn(msg); errorMessageHandler.handleErrorMessage(msg); } return result; } /////////////////////////////////////////////////////////////////////////////////////////// // Witness signing /////////////////////////////////////////////////////////////////////////////////////////// public void arbitratorSignAccountAgeWitness(BigInteger tradeAmount, AccountAgeWitness accountAgeWitness, ECKey key, PublicKey peersPubKey) { signedWitnessService.signAndPublishAccountAgeWitness(tradeAmount, accountAgeWitness, key, peersPubKey); } public String arbitratorSignOrphanWitness(AccountAgeWitness accountAgeWitness, ECKey ecKey, long time) { // TODO Is not found signedWitness considered an error case? // Previous code version was throwing an exception in case no signedWitness was found... // signAndPublishAccountAgeWitness returns an empty string in success case and error otherwise return signedWitnessService.getSignedWitnessSet(accountAgeWitness).stream() .findAny() .map(SignedWitness::getWitnessOwnerPubKey) .map(witnessOwnerPubKey -> signedWitnessService.signAndPublishAccountAgeWitness(accountAgeWitness, ecKey, witnessOwnerPubKey, time) ) .orElse("No signedWitness found"); } public String arbitratorSignOrphanPubKey(ECKey key, byte[] peersPubKey, long childSignTime) { return signedWitnessService.signTraderPubKey(key, peersPubKey, childSignTime); } public void arbitratorSignAccountAgeWitness(AccountAgeWitness accountAgeWitness, ECKey key, byte[] tradersPubKey, long time) { signedWitnessService.signAndPublishAccountAgeWitness(accountAgeWitness, key, tradersPubKey, time); } public Optional traderSignAndPublishPeersAccountAgeWitness(Trade trade) { checkNotNull(trade.getTradePeer().getPubKeyRing(), "Peer must have a keyring"); PublicKey peersPubKey = trade.getTradePeer().getPubKeyRing().getSignaturePubKey(); checkNotNull(peersPubKey, "Peers pub key must not be null"); AccountAgeWitness peersWitness = findTradePeerWitness(trade).orElse(null); checkNotNull(peersWitness, "Not able to find peers witness, unable to sign for trade " + trade.toString()); BigInteger tradeAmount = trade.getAmount(); checkNotNull(tradeAmount, "Trade amount must not be null"); try { return signedWitnessService.signAndPublishAccountAgeWitness(tradeAmount, peersWitness, peersPubKey); } catch (CryptoException e) { log.warn("Trader failed to sign witness, exception {}", e.toString()); } return Optional.empty(); } public boolean publishOwnSignedWitness(SignedWitness signedWitness) { return signedWitnessService.publishOwnSignedWitness(signedWitness); } // Arbitrator signing public List getTraderPaymentAccounts(long safeDate, PaymentMethod paymentMethod, List disputes) { return disputes.stream() .filter(dispute -> dispute.getContract().getPaymentMethodId().equals(paymentMethod.getId())) .filter(this::isNotFiltered) .filter(this::hasChargebackRisk) .filter(this::isBuyerWinner) .flatMap(this::getTraderData) .filter(Objects::nonNull) .filter(traderDataItem -> !signedWitnessService.isSignedAccountAgeWitness(traderDataItem.getAccountAgeWitness())) .filter(traderDataItem -> traderDataItem.getAccountAgeWitness().getDate() < safeDate) .distinct() .collect(Collectors.toList()); } private boolean isNotFiltered(Dispute dispute) { boolean isFiltered = filterManager.isNodeAddressBanned(dispute.getContract().getBuyerNodeAddress()) || filterManager.isNodeAddressBanned(dispute.getContract().getSellerNodeAddress()) || filterManager.isCurrencyBanned(dispute.getContract().getOfferPayload().getCurrencyCode()) || filterManager.isPaymentMethodBanned(PaymentMethod.getPaymentMethodOrNA(dispute.getContract().getPaymentMethodId())) || filterManager.arePeersPaymentAccountDataBanned(dispute.getBuyerPaymentAccountPayload()) || filterManager.arePeersPaymentAccountDataBanned(dispute.getSellerPaymentAccountPayload()) || filterManager.isWitnessSignerPubKeyBanned(Utils.HEX.encode(dispute.getContract().getBuyerPubKeyRing().getSignaturePubKeyBytes())) || filterManager.isWitnessSignerPubKeyBanned(Utils.HEX.encode(dispute.getContract().getSellerPubKeyRing().getSignaturePubKeyBytes())); return !isFiltered; } @VisibleForTesting public boolean hasChargebackRisk(Dispute dispute) { return chargeBackRisk.hasChargebackRisk(dispute.getContract().getPaymentMethodId(), dispute.getContract().getOfferPayload().getCurrencyCode()); } private boolean isBuyerWinner(Dispute dispute) { if (!dispute.isClosed() || dispute.getDisputeResultProperty() == null) return false; return dispute.getDisputeResultProperty().get().getWinner() == DisputeResult.Winner.BUYER; } private Stream getTraderData(Dispute dispute) { BigInteger tradeAmount = dispute.getContract().getTradeAmount(); PubKeyRing buyerPubKeyRing = dispute.getContract().getBuyerPubKeyRing(); PubKeyRing sellerPubKeyRing = dispute.getContract().getSellerPubKeyRing(); PaymentAccountPayload buyerPaymentAccountPaload = dispute.getBuyerPaymentAccountPayload(); PaymentAccountPayload sellerPaymentAccountPaload = dispute.getSellerPaymentAccountPayload(); TraderDataItem buyerData = findWitness(buyerPaymentAccountPaload, buyerPubKeyRing) .map(witness -> new TraderDataItem( buyerPaymentAccountPaload, witness, tradeAmount, buyerPubKeyRing.getSignaturePubKey())) .orElse(null); TraderDataItem sellerData = findWitness(sellerPaymentAccountPaload, sellerPubKeyRing) .map(witness -> new TraderDataItem( sellerPaymentAccountPaload, witness, tradeAmount, sellerPubKeyRing.getSignaturePubKey())) .orElse(null); return Stream.of(buyerData, sellerData); } public boolean hasSignedWitness(Offer offer) { return findWitness(offer) .map(signedWitnessService::isSignedAccountAgeWitness) .orElse(false); } public boolean peerHasSignedWitness(Trade trade) { return findTradePeerWitness(trade) .map(signedWitnessService::isSignedAccountAgeWitness) .orElse(false); } public boolean accountIsSigner(AccountAgeWitness accountAgeWitness) { return signedWitnessService.isSignerAccountAgeWitness(accountAgeWitness); } public boolean tradeAmountIsSufficient(BigInteger tradeAmount) { return signedWitnessService.isSufficientTradeAmountForSigning(tradeAmount); } public SignState getSignState(Offer offer) { return findWitness(offer) .map(this::getSignState) .orElse(SignState.UNSIGNED); } public SignState getSignState(Trade trade) { if (trade instanceof ArbitratorTrade) return SignState.UNSIGNED; return findTradePeerWitness(trade) .map(this::getSignState) .orElse(SignState.UNSIGNED); } public SignState getSignState(AccountAgeWitness accountAgeWitness) { // Add hash to sign state info when running in debug mode String hash = log.isDebugEnabled() ? Utilities.bytesAsHexString(accountAgeWitness.getHash()) + "\n" + signedWitnessService.ownerPubKeyAsString(accountAgeWitness) : ""; if (signedWitnessService.isFilteredWitness(accountAgeWitness)) { return SignState.BANNED.addHash(hash); } if (signedWitnessService.isSignedByArbitrator(accountAgeWitness)) { return SignState.ARBITRATOR.addHash(hash); } else { final long accountSignAge = getWitnessSignAge(accountAgeWitness, new Date()); switch (getAccountAgeCategory(accountSignAge)) { case TWO_MONTHS_OR_MORE: case ONE_TO_TWO_MONTHS: return SignState.PEER_SIGNER.addHash(hash); case LESS_ONE_MONTH: return SignState.PEER_INITIAL.addHash(hash) .setDaysUntilLimitLifted(30 - TimeUnit.MILLISECONDS.toDays(accountSignAge)); case UNVERIFIED: default: return SignState.UNSIGNED.addHash(hash); } } } public Set getOrphanSignedWitnesses() { return signedWitnessService.getRootSignedWitnessSet(false).stream() .map(signedWitness -> getWitnessByHash(signedWitness.getAccountAgeWitnessHash()).orElse(null)) .filter(Objects::nonNull) .collect(Collectors.toSet()); } public void signAndPublishSameNameAccounts() { // Collect accounts that have ownerId to sign unsigned accounts with the same ownderId var signerAccounts = Objects.requireNonNull(user.getPaymentAccounts()).stream() .filter(account -> account.getOwnerId() != null && accountIsSigner(getMyWitness(account.getPaymentAccountPayload()))) .collect(Collectors.toSet()); var unsignedAccounts = user.getPaymentAccounts().stream() .filter(account -> account.getOwnerId() != null && !signedWitnessService.isSignedAccountAgeWitness( getMyWitness(account.getPaymentAccountPayload()))) .collect(Collectors.toSet()); signerAccounts.forEach(signer -> unsignedAccounts.forEach(unsigned -> { if (signer.getOwnerId().equals(unsigned.getOwnerId())) { try { signedWitnessService.selfSignAndPublishAccountAgeWitness( getMyWitness(unsigned.getPaymentAccountPayload())); } catch (CryptoException e) { log.warn("Self signing failed, exception {}", e.toString()); } } })); } public Set getUnsignedSignerPubKeys() { return signedWitnessService.getUnsignedSignerPubKeys(); } public boolean isSignWitnessTrade(Trade trade) { checkNotNull(trade, "trade must not be null"); checkNotNull(trade.getOffer(), "offer must not be null"); PaymentAccountPayload sellerPaymentAccountPayload = trade.getSeller().getPaymentAccountPayload(); AccountAgeWitness myWitness = getMyWitness(sellerPaymentAccountPayload); getAccountAgeWitnessUtils().witnessDebugLog(trade, myWitness); return accountIsSigner(myWitness) && !peerHasSignedWitness(trade) && tradeAmountIsSufficient(trade.getAmount()); } public String getSignInfoFromAccount(PaymentAccount paymentAccount) { var pubKey = keyRing.getSignatureKeyPair().getPublic(); var witness = getMyWitness(paymentAccount.getPaymentAccountPayload()); return Utilities.bytesAsHexString(witness.getHash()) + "," + Utilities.bytesAsHexString(pubKey.getEncoded()); } public Tuple2 getSignInfoFromString(String signInfo) { var parts = signInfo.split(","); if (parts.length != 2) { return null; } byte[] pubKeyHash; Optional accountAgeWitness; try { var accountAgeWitnessHash = Utilities.decodeFromHex(parts[0]); pubKeyHash = Utilities.decodeFromHex(parts[1]); accountAgeWitness = getWitnessByHash(accountAgeWitnessHash); return accountAgeWitness .map(ageWitness -> new Tuple2<>(ageWitness, pubKeyHash)) .orElse(null); } catch (Exception e) { return null; } } } ================================================ FILE: core/src/main/java/haveno/core/account/witness/AccountAgeWitnessStorageService.java ================================================ /* * 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 . */ package haveno.core.account.witness; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.common.persistence.PersistenceManager; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import haveno.network.p2p.storage.persistence.HistoricalDataStoreService; import java.io.File; import lombok.extern.slf4j.Slf4j; @Slf4j public class AccountAgeWitnessStorageService extends HistoricalDataStoreService { private static final String FILE_NAME = "AccountAgeWitnessStore"; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public AccountAgeWitnessStorageService(@Named(Config.STORAGE_DIR) File storageDir, PersistenceManager persistenceManager) { super(storageDir, persistenceManager); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getFileName() { return FILE_NAME; } @Override protected void initializePersistenceManager() { persistenceManager.initialize(store, PersistenceManager.Source.NETWORK); } @Override public boolean canHandle(PersistableNetworkPayload payload) { return payload instanceof AccountAgeWitness; } /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// @Override protected AccountAgeWitnessStore createStore() { return new AccountAgeWitnessStore(); } } ================================================ FILE: core/src/main/java/haveno/core/account/witness/AccountAgeWitnessStore.java ================================================ /* * 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 . */ package haveno.core.account.witness; import com.google.protobuf.Message; import haveno.network.p2p.storage.persistence.PersistableNetworkPayloadStore; import lombok.extern.slf4j.Slf4j; import java.util.List; import java.util.stream.Collectors; /** * We store only the payload in the PB file to save disc space. The hash of the payload can be created anyway and * is only used as key in the map. So we have a hybrid data structure which is represented as list in the protobuffer * definition and provide a hashMap for the domain access. */ @Slf4j public class AccountAgeWitnessStore extends PersistableNetworkPayloadStore { public AccountAgeWitnessStore() { } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private AccountAgeWitnessStore(List list) { super(list); } public Message toProtoMessage() { return protobuf.PersistableEnvelope.newBuilder() .setAccountAgeWitnessStore(getBuilder()) .build(); } private protobuf.AccountAgeWitnessStore.Builder getBuilder() { final List protoList = map.values().stream() .map(payload -> (AccountAgeWitness) payload) .map(AccountAgeWitness::toProtoAccountAgeWitness) .collect(Collectors.toList()); return protobuf.AccountAgeWitnessStore.newBuilder().addAllItems(protoList); } public static AccountAgeWitnessStore fromProto(protobuf.AccountAgeWitnessStore proto) { List list = proto.getItemsList().stream() .map(AccountAgeWitness::fromProto).collect(Collectors.toList()); return new AccountAgeWitnessStore(list); } } ================================================ FILE: core/src/main/java/haveno/core/account/witness/AccountAgeWitnessUtils.java ================================================ /* * 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 . */ package haveno.core.account.witness; import haveno.common.crypto.Hash; import haveno.common.crypto.KeyRing; import haveno.common.crypto.PubKeyRing; import haveno.common.util.Utilities; import haveno.core.account.sign.SignedWitness; import haveno.core.account.sign.SignedWitnessService; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.trade.Trade; import haveno.network.p2p.storage.P2PDataStorage; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.util.Arrays; import java.util.Collection; import java.util.Optional; import java.util.Stack; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class AccountAgeWitnessUtils { private final AccountAgeWitnessService accountAgeWitnessService; private final SignedWitnessService signedWitnessService; private final KeyRing keyRing; AccountAgeWitnessUtils(AccountAgeWitnessService accountAgeWitnessService, SignedWitnessService signedWitnessService, KeyRing keyRing) { this.accountAgeWitnessService = accountAgeWitnessService; this.signedWitnessService = signedWitnessService; this.keyRing = keyRing; } // Log tree of signed witnesses public void logSignedWitnesses() { var orphanSigners = signedWitnessService.getRootSignedWitnessSet(true); log.info("Orphaned signed account age witnesses:"); orphanSigners.forEach(w -> { log.info("{}: Signer PKH: {} Owner PKH: {} time: {}", w.getVerificationMethod().toString(), Utilities.bytesAsHexString(Hash.getRipemd160hash(w.getSignerPubKey())).substring(0, 7), Utilities.bytesAsHexString(Hash.getRipemd160hash(w.getWitnessOwnerPubKey())).substring(0, 7), w.getDate()); logChild(w, " ", new Stack<>()); }); } private void logChild(SignedWitness sigWit, String initString, Stack excluded) { log.info("{}AEW: {} PKH: {} time: {}", initString, Utilities.bytesAsHexString(sigWit.getAccountAgeWitnessHash()).substring(0, 7), Utilities.bytesAsHexString(Hash.getRipemd160hash(sigWit.getWitnessOwnerPubKey())).substring(0, 7), sigWit.getDate()); signedWitnessService.getSignedWitnessMapValues().forEach(w -> { if (!excluded.contains(new P2PDataStorage.ByteArray(w.getWitnessOwnerPubKey())) && Arrays.equals(w.getSignerPubKey(), sigWit.getWitnessOwnerPubKey())) { excluded.push(new P2PDataStorage.ByteArray(w.getWitnessOwnerPubKey())); logChild(w, initString + " ", excluded); excluded.pop(); } }); } // Log signers per public void logSigners() { log.info("Signers per AEW"); Collection signedWitnessMapValues = signedWitnessService.getSignedWitnessMapValues(); signedWitnessMapValues.forEach(w -> { log.info("AEW {}", Utilities.bytesAsHexString(w.getAccountAgeWitnessHash())); signedWitnessMapValues.forEach(ww -> { if (Arrays.equals(w.getSignerPubKey(), ww.getWitnessOwnerPubKey())) { log.info(" {}", Utilities.bytesAsHexString(ww.getAccountAgeWitnessHash())); } }); } ); } public void logUnsignedSignerPubKeys() { log.info("Unsigned signer pubkeys"); signedWitnessService.getUnsignedSignerPubKeys().forEach(signedWitness -> log.info("PK hash {} date {}", Utilities.bytesAsHexString(Hash.getRipemd160hash(signedWitness.getSignerPubKey())), signedWitness.getDate())); } /////////////////////////////////////////////////////////////////////////////////////////// // Debug logs /////////////////////////////////////////////////////////////////////////////////////////// private String getWitnessDebugLog(PaymentAccountPayload paymentAccountPayload, PubKeyRing pubKeyRing) { Optional accountAgeWitness = accountAgeWitnessService.findWitness(paymentAccountPayload, pubKeyRing); if (!accountAgeWitness.isPresent()) { byte[] accountInputDataWithSalt = accountAgeWitnessService.getAccountInputDataWithSalt(paymentAccountPayload); byte[] hash = Hash.getSha256Ripemd160hash(Utilities.concatenateByteArrays(accountInputDataWithSalt, pubKeyRing.getSignaturePubKeyBytes())); return "No accountAgeWitness found for paymentAccountPayload with hash " + Utilities.bytesAsHexString(hash); } AccountAgeWitnessService.SignState signState = accountAgeWitnessService.getSignState(accountAgeWitness.get()); return signState.name() + " " + signState.getDisplayString() + "\n" + accountAgeWitness.toString(); } public void witnessDebugLog(Trade trade, @Nullable AccountAgeWitness myWitness) { // Log to find why accounts sometimes don't get signed as expected // TODO: Demote to debug or remove once account signing is working ok checkNotNull(trade.getContract()); checkNotNull(trade.getBuyer().getPaymentAccountPayload()); boolean checkingSignTrade = true; boolean isBuyer = trade.getContract().isMyRoleBuyer(keyRing.getPubKeyRing()); AccountAgeWitness witness = myWitness; if (witness == null) { witness = isBuyer ? accountAgeWitnessService.getMyWitness(trade.getBuyer().getPaymentAccountPayload()) : accountAgeWitnessService.getMyWitness(trade.getSeller().getPaymentAccountPayload()); checkingSignTrade = false; } boolean isSignWitnessTrade = accountAgeWitnessService.accountIsSigner(witness) && !accountAgeWitnessService.peerHasSignedWitness(trade) && accountAgeWitnessService.tradeAmountIsSufficient(trade.getAmount()); log.debug("AccountSigning debug log: " + "\ntradeId: {}" + "\nis buyer: {}" + "\nbuyer account age witness info: {}" + "\nseller account age witness info: {}" + "\nchecking for sign trade: {}" + "\nis myWitness signer: {}" + "\npeer has signed witness: {}" + "\ntrade amount: {}" + "\ntrade amount is sufficient: {}" + "\nisSignWitnessTrade: {}", trade.getId(), isBuyer, getWitnessDebugLog(trade.getBuyer().getPaymentAccountPayload(), trade.getContract().getBuyerPubKeyRing()), getWitnessDebugLog(trade.getSeller().getPaymentAccountPayload(), trade.getContract().getSellerPubKeyRing()), checkingSignTrade, // Following cases added to use same logic as in seller signing check accountAgeWitnessService.accountIsSigner(witness), accountAgeWitnessService.peerHasSignedWitness(trade), trade.getAmount(), accountAgeWitnessService.tradeAmountIsSufficient(trade.getAmount()), isSignWitnessTrade); } } ================================================ FILE: core/src/main/java/haveno/core/alert/Alert.java ================================================ /* * 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 . */ package haveno.core.alert; import com.google.protobuf.ByteString; import haveno.common.app.Version; import haveno.common.crypto.Sig; import haveno.common.util.CollectionUtils; import haveno.common.util.ExtraDataMapValidator; import haveno.core.user.Preferences; import haveno.network.p2p.storage.payload.ExpirablePayload; import haveno.network.p2p.storage.payload.ProtectedStoragePayload; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.security.PublicKey; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import static com.google.common.base.Preconditions.checkNotNull; @EqualsAndHashCode @Getter @ToString @Slf4j public final class Alert implements ProtectedStoragePayload, ExpirablePayload { public static final long TTL = TimeUnit.DAYS.toMillis(90); private final String message; private final boolean isUpdateInfo; private final boolean isPreReleaseInfo; private final String version; @Nullable private byte[] ownerPubKeyBytes; @Nullable private String signatureAsBase64; @Nullable private PublicKey ownerPubKey; // Should be only used in emergency case if we need to add data but do not want to break backward compatibility // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new // field in a class would break that hash and therefore break the storage mechanism. @Nullable private Map extraDataMap; public Alert(String message, boolean isUpdateInfo, boolean isPreReleaseInfo, String version) { this.message = message; this.isUpdateInfo = isUpdateInfo; this.isPreReleaseInfo = isPreReleaseInfo; this.version = version; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @SuppressWarnings("NullableProblems") public Alert(String message, boolean isUpdateInfo, boolean isPreReleaseInfo, String version, byte[] ownerPubKeyBytes, String signatureAsBase64, Map extraDataMap) { this.message = message; this.isUpdateInfo = isUpdateInfo; this.isPreReleaseInfo = isPreReleaseInfo; this.version = version; this.ownerPubKeyBytes = ownerPubKeyBytes; this.signatureAsBase64 = signatureAsBase64; this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); ownerPubKey = Sig.getPublicKeyFromBytes(ownerPubKeyBytes); } @Override public protobuf.StoragePayload toProtoMessage() { checkNotNull(ownerPubKeyBytes, "storagePublicKeyBytes must not be null"); checkNotNull(signatureAsBase64, "signatureAsBase64 must not be null"); protobuf.Alert.Builder builder = protobuf.Alert.newBuilder() .setMessage(message) .setIsUpdateInfo(isUpdateInfo) .setIsPreReleaseInfo(isPreReleaseInfo) .setVersion(version) .setOwnerPubKeyBytes(ByteString.copyFrom(ownerPubKeyBytes)) .setSignatureAsBase64(signatureAsBase64); Optional.ofNullable(getExtraDataMap()).ifPresent(builder::putAllExtraData); return protobuf.StoragePayload.newBuilder().setAlert(builder).build(); } @Nullable public static Alert fromProto(protobuf.Alert proto) { // We got in dev testing sometimes an empty protobuf Alert. Not clear why that happened but as it causes an // exception and corrupted user db file we prefer to set it to null. if (proto.getSignatureAsBase64().isEmpty()) return null; return new Alert(proto.getMessage(), proto.getIsUpdateInfo(), proto.getIsPreReleaseInfo(), proto.getVersion(), proto.getOwnerPubKeyBytes().toByteArray(), proto.getSignatureAsBase64(), CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap()); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public long getTTL() { return TTL; } public void setSigAndPubKey(String signatureAsBase64, PublicKey ownerPubKey) { this.signatureAsBase64 = signatureAsBase64; this.ownerPubKey = ownerPubKey; ownerPubKeyBytes = Sig.getPublicKeyBytes(ownerPubKey); } public boolean isNewVersion(Preferences preferences) { // regular release: always notify user // pre-release: if user has set preference to receive pre-release notification if (isUpdateInfo || (isPreReleaseInfo && preferences.isNotifyOnPreRelease())) { return Version.isNewVersion(version); } return false; } public boolean isSoftwareUpdateNotification() { return (isUpdateInfo || isPreReleaseInfo); } public boolean canShowPopup(Preferences preferences) { // only show popup if its version is newer than current // and only if user has not checked "don't show again" return isNewVersion(preferences) && preferences.showAgain(showAgainKey()); } public String showAgainKey() { return "Update_" + version; } } ================================================ FILE: core/src/main/java/haveno/core/alert/AlertManager.java ================================================ /* * 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 . */ package haveno.core.alert; import com.google.common.base.Charsets; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.app.DevEnv; import haveno.common.config.Config; import haveno.common.crypto.KeyRing; import haveno.core.user.User; import haveno.network.p2p.P2PService; import haveno.network.p2p.storage.HashMapChangedListener; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import haveno.network.p2p.storage.payload.ProtectedStoragePayload; import java.math.BigInteger; import java.security.SignatureException; import java.util.Collection; import java.util.List; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleObjectProperty; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.Utils; import static org.bitcoinj.core.Utils.HEX; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class AlertManager { private static final Logger log = LoggerFactory.getLogger(AlertManager.class); private final P2PService p2PService; private final KeyRing keyRing; private final User user; private final ObjectProperty alertMessageProperty = new SimpleObjectProperty<>(); private final boolean useDevPrivilegeKeys; private ECKey alertSigningKey; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialization /////////////////////////////////////////////////////////////////////////////////////////// @Inject public AlertManager(P2PService p2PService, KeyRing keyRing, User user, @Named(Config.IGNORE_DEV_MSG) boolean ignoreDevMsg, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { this.p2PService = p2PService; this.keyRing = keyRing; this.user = user; this.useDevPrivilegeKeys = useDevPrivilegeKeys; if (!ignoreDevMsg) { p2PService.addHashSetChangedListener(new HashMapChangedListener() { @Override public void onAdded(Collection protectedStorageEntries) { protectedStorageEntries.forEach(protectedStorageEntry -> { final ProtectedStoragePayload protectedStoragePayload = protectedStorageEntry.getProtectedStoragePayload(); if (protectedStoragePayload instanceof Alert) { Alert alert = (Alert) protectedStoragePayload; if (verifySignature(alert)) alertMessageProperty.set(alert); } }); } @Override public void onRemoved(Collection protectedStorageEntries) { protectedStorageEntries.forEach(protectedStorageEntry -> { final ProtectedStoragePayload protectedStoragePayload = protectedStorageEntry.getProtectedStoragePayload(); if (protectedStoragePayload instanceof Alert) { if (verifySignature((Alert) protectedStoragePayload)) alertMessageProperty.set(null); } }); } }); } } protected List getPubKeyList() { if (useDevPrivilegeKeys) return List.of(DevEnv.DEV_PRIVILEGE_PUB_KEY); switch (Config.baseCurrencyNetwork()) { case XMR_LOCAL: return List.of( "027a381b5333a56e1cc3d90d3a7d07f26509adf7029ed06fc997c656621f8da1ee", "024baabdba90e7cc0dc4626ef73ea9d722ea7085d1104491da8c76f28187513492"); case XMR_STAGENET: return List.of( "03aa23e062afa0dda465f46986f8aa8d0374ad3e3f256141b05681dcb1e39c3859", "02d3beb1293ca2ca14e6d42ca8bd18089a62aac62fd6bb23923ee6ead46ac60fba", "0374dd70f3fa6e47ec5ab97932e1cec6233e98e6ae3129036b17118650c44fd3de"); case XMR_MAINNET: return List.of( "02d8ac0fbe4e25f4a1d68b95936f25fc2e1b218e161cb5ed6661c7ab4c85f1fd4f", "02e9dc14edddde19cc9f829a0739d0ab0c7310154ad94a15d477b51d85991b5a8a", "03c8efdf81287ce8b3212241e6aa7cdf094ecbed2d2f119730a3e4d596a764106a"); default: throw new RuntimeException("Unhandled base currency network: " + Config.baseCurrencyNetwork()); } } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public ReadOnlyObjectProperty alertMessageProperty() { return alertMessageProperty; } public boolean addAlertMessageIfKeyIsValid(Alert alert, String privKeyString) { // if there is a previous message we remove that first if (user.getDevelopersAlert() != null) removeAlertMessageIfKeyIsValid(privKeyString); boolean isKeyValid = isKeyValid(privKeyString); if (isKeyValid) { signAndAddSignatureToAlertMessage(alert); user.setDevelopersAlert(alert); boolean result = p2PService.addProtectedStorageEntry(alert); if (result) { log.trace("Add alertMessage to network was successful. AlertMessage={}", alert); } } return isKeyValid; } public boolean removeAlertMessageIfKeyIsValid(String privKeyString) { Alert alert = user.getDevelopersAlert(); if (isKeyValid(privKeyString) && alert != null) { if (p2PService.removeData(alert)) log.trace("Remove alertMessage from network was successful. AlertMessage={}", alert); user.setDevelopersAlert(null); return true; } else { return false; } } private boolean isKeyValid(String privKeyString) { try { alertSigningKey = ECKey.fromPrivate(new BigInteger(1, HEX.decode(privKeyString))); return getPubKeyList().contains(Utils.HEX.encode(alertSigningKey.getPubKey())); } catch (Throwable t) { return false; } } private void signAndAddSignatureToAlertMessage(Alert alert) { String alertMessageAsHex = Utils.HEX.encode(alert.getMessage().getBytes(Charsets.UTF_8)); String signatureAsBase64 = alertSigningKey.signMessage(alertMessageAsHex); alert.setSigAndPubKey(signatureAsBase64, keyRing.getSignatureKeyPair().getPublic()); } private boolean verifySignature(Alert alert) { String alertMessageAsHex = Utils.HEX.encode(alert.getMessage().getBytes(Charsets.UTF_8)); for (String pubKeyAsHex : getPubKeyList()) { try { ECKey.fromPublicOnly(HEX.decode(pubKeyAsHex)).verifyMessage(alertMessageAsHex, alert.getSignatureAsBase64()); return true; } catch (SignatureException e) { // ignore } } log.warn("verifySignature failed"); return false; } } ================================================ FILE: core/src/main/java/haveno/core/alert/AlertModule.java ================================================ /* * 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 . */ package haveno.core.alert; import com.google.inject.Singleton; import haveno.common.app.AppModule; import haveno.common.config.Config; import static com.google.inject.name.Names.named; import static haveno.common.config.Config.IGNORE_DEV_MSG; public class AlertModule extends AppModule { public AlertModule(Config config) { super(config); } @Override protected final void configure() { bind(AlertManager.class).in(Singleton.class); bind(PrivateNotificationManager.class).in(Singleton.class); bindConstant().annotatedWith(named(IGNORE_DEV_MSG)).to(config.ignoreDevMsg); } } ================================================ FILE: core/src/main/java/haveno/core/alert/PrivateNotificationManager.java ================================================ /* * 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 . */ package haveno.core.alert; import com.google.common.base.Charsets; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.app.DevEnv; import haveno.common.config.Config; import haveno.common.crypto.KeyRing; import haveno.common.crypto.PubKeyRing; import haveno.common.proto.network.NetworkEnvelope; import haveno.network.p2p.DecryptedMessageWithPubKey; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import haveno.network.p2p.SendMailboxMessageListener; import haveno.network.p2p.mailbox.MailboxMessageService; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.MessageListener; import haveno.network.p2p.network.NetworkNode; import haveno.network.p2p.peers.keepalive.messages.Ping; import haveno.network.p2p.peers.keepalive.messages.Pong; import java.math.BigInteger; import java.security.SignatureException; import java.util.List; import java.util.Random; import java.util.UUID; import java.util.function.Consumer; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javax.annotation.Nullable; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.Utils; import static org.bitcoinj.core.Utils.HEX; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class PrivateNotificationManager implements MessageListener { private static final Logger log = LoggerFactory.getLogger(PrivateNotificationManager.class); private final P2PService p2PService; private final MailboxMessageService mailboxMessageService; private final KeyRing keyRing; private final ObjectProperty privateNotificationMessageProperty = new SimpleObjectProperty<>(); private final boolean useDevPrivilegeKeys; private ECKey privateNotificationSigningKey; @Nullable private PrivateNotificationMessage privateNotificationMessage; private final NetworkNode networkNode; private Consumer pingResponseHandler = null; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialization /////////////////////////////////////////////////////////////////////////////////////////// @Inject public PrivateNotificationManager(P2PService p2PService, NetworkNode networkNode, MailboxMessageService mailboxMessageService, KeyRing keyRing, @Named(Config.IGNORE_DEV_MSG) boolean ignoreDevMsg, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { this.p2PService = p2PService; this.networkNode = networkNode; this.mailboxMessageService = mailboxMessageService; this.keyRing = keyRing; this.useDevPrivilegeKeys = useDevPrivilegeKeys; if (!ignoreDevMsg) { this.p2PService.addDecryptedDirectMessageListener(this::handleMessage); this.mailboxMessageService.addDecryptedMailboxListener(this::handleMessage); } } protected List getPubKeyList() { if (useDevPrivilegeKeys) return List.of(DevEnv.DEV_PRIVILEGE_PUB_KEY); switch (Config.baseCurrencyNetwork()) { case XMR_LOCAL: return List.of( "027a381b5333a56e1cc3d90d3a7d07f26509adf7029ed06fc997c656621f8da1ee", "024baabdba90e7cc0dc4626ef73ea9d722ea7085d1104491da8c76f28187513492"); case XMR_STAGENET: return List.of( "03aa23e062afa0dda465f46986f8aa8d0374ad3e3f256141b05681dcb1e39c3859", "02d3beb1293ca2ca14e6d42ca8bd18089a62aac62fd6bb23923ee6ead46ac60fba", "0374dd70f3fa6e47ec5ab97932e1cec6233e98e6ae3129036b17118650c44fd3de"); case XMR_MAINNET: return List.of( "02d8ac0fbe4e25f4a1d68b95936f25fc2e1b218e161cb5ed6661c7ab4c85f1fd4f", "02e9dc14edddde19cc9f829a0739d0ab0c7310154ad94a15d477b51d85991b5a8a", "03c8efdf81287ce8b3212241e6aa7cdf094ecbed2d2f119730a3e4d596a764106a"); default: throw new RuntimeException("Unhandled base currency network: " + Config.baseCurrencyNetwork()); } } private void handleMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress senderNodeAddress) { NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); if (networkEnvelope instanceof PrivateNotificationMessage) { privateNotificationMessage = (PrivateNotificationMessage) networkEnvelope; log.info("Received PrivateNotificationMessage from {} with uid={}", senderNodeAddress, privateNotificationMessage.getUid()); if (privateNotificationMessage.getSenderNodeAddress().equals(senderNodeAddress)) { final PrivateNotificationPayload privateNotification = privateNotificationMessage.getPrivateNotificationPayload(); if (verifySignature(privateNotification)) privateNotificationMessageProperty.set(privateNotification); } else { log.warn("Peer address not matching for privateNotificationMessage"); } } } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public ReadOnlyObjectProperty privateNotificationProperty() { return privateNotificationMessageProperty; } public boolean sendPrivateNotificationMessageIfKeyIsValid(PrivateNotificationPayload privateNotification, PubKeyRing pubKeyRing, NodeAddress peersNodeAddress, String privKeyString, SendMailboxMessageListener sendMailboxMessageListener) { boolean isKeyValid = isKeyValid(privKeyString); if (isKeyValid) { signAndAddSignatureToPrivateNotificationMessage(privateNotification); PrivateNotificationMessage message = new PrivateNotificationMessage(privateNotification, p2PService.getNetworkNode().getNodeAddress(), UUID.randomUUID().toString()); log.info("Send {} to peer {}. uid={}", message.getClass().getSimpleName(), peersNodeAddress, message.getUid()); mailboxMessageService.sendEncryptedMailboxMessage(peersNodeAddress, pubKeyRing, message, sendMailboxMessageListener); } return isKeyValid; } public void removePrivateNotification() { if (privateNotificationMessage != null) { mailboxMessageService.removeMailboxMsg(privateNotificationMessage); } } private boolean isKeyValid(String privKeyString) { try { privateNotificationSigningKey = ECKey.fromPrivate(new BigInteger(1, HEX.decode(privKeyString))); return getPubKeyList().contains(Utils.HEX.encode(privateNotificationSigningKey.getPubKey())); } catch (Throwable t) { return false; } } private void signAndAddSignatureToPrivateNotificationMessage(PrivateNotificationPayload privateNotification) { String privateNotificationMessageAsHex = Utils.HEX.encode(privateNotification.getMessage().getBytes(Charsets.UTF_8)); String signatureAsBase64 = privateNotificationSigningKey.signMessage(privateNotificationMessageAsHex); privateNotification.setSigAndPubKey(signatureAsBase64, keyRing.getSignatureKeyPair().getPublic()); } private boolean verifySignature(PrivateNotificationPayload privateNotification) { String privateNotificationMessageAsHex = Utils.HEX.encode(privateNotification.getMessage().getBytes(Charsets.UTF_8)); for (String pubKeyAsHex : getPubKeyList()) { try { ECKey.fromPublicOnly(HEX.decode(pubKeyAsHex)).verifyMessage(privateNotificationMessageAsHex, privateNotification.getSignatureAsBase64()); return true; } catch (SignatureException e) { // ignore } } log.warn("verifySignature failed"); return false; } public void sendPing(NodeAddress peersNodeAddress, Consumer resultHandler) { Ping ping = new Ping(new Random().nextInt(), 0); log.info("Send Ping to peer {}, nonce={}", peersNodeAddress, ping.getNonce()); SettableFuture future = networkNode.sendMessage(peersNodeAddress, ping); Futures.addCallback(future, new FutureCallback<>() { @Override public void onSuccess(Connection connection) { connection.addMessageListener(PrivateNotificationManager.this); pingResponseHandler = resultHandler; } @Override public void onFailure(@NotNull Throwable throwable) { String errorMessage = "Sending ping to " + peersNodeAddress.getAddressForDisplay() + " failed. That is expected if the peer is offline.\n\tping=" + ping + ".\n\tException=" + throwable.getMessage(); log.info(errorMessage); resultHandler.accept(errorMessage); } }, MoreExecutors.directExecutor()); } @Override public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { if (networkEnvelope instanceof Pong) { Pong pong = (Pong) networkEnvelope; String key = connection.getPeersNodeAddressOptional().get().getFullAddress(); log.info("Received Pong! {} from {}", pong, key); connection.removeMessageListener(this); if (pingResponseHandler != null) { pingResponseHandler.accept("SUCCESS"); } } } } ================================================ FILE: core/src/main/java/haveno/core/alert/PrivateNotificationMessage.java ================================================ /* * 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 . */ package haveno.core.alert; import haveno.common.app.Version; import haveno.common.proto.network.NetworkEnvelope; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.mailbox.MailboxMessage; import lombok.EqualsAndHashCode; import lombok.Value; import java.util.concurrent.TimeUnit; @EqualsAndHashCode(callSuper = true) @Value public class PrivateNotificationMessage extends NetworkEnvelope implements MailboxMessage { public static final long TTL = TimeUnit.DAYS.toMillis(30); private final PrivateNotificationPayload privateNotificationPayload; private final NodeAddress senderNodeAddress; private final String uid; public PrivateNotificationMessage(PrivateNotificationPayload privateNotificationPayload, NodeAddress senderNodeAddress, String uid) { this(privateNotificationPayload, senderNodeAddress, uid, Version.getP2PMessageVersion()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private PrivateNotificationMessage(PrivateNotificationPayload privateNotificationPayload, NodeAddress senderNodeAddress, String uid, String messageVersion) { super(messageVersion); this.privateNotificationPayload = privateNotificationPayload; this.senderNodeAddress = senderNodeAddress; this.uid = uid; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { return getNetworkEnvelopeBuilder() .setPrivateNotificationMessage(protobuf.PrivateNotificationMessage.newBuilder() .setPrivateNotificationPayload(privateNotificationPayload.toProtoMessage()) .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setUid(uid)) .build(); } public static PrivateNotificationMessage fromProto(protobuf.PrivateNotificationMessage proto, String messageVersion) { return new PrivateNotificationMessage(PrivateNotificationPayload.fromProto(proto.getPrivateNotificationPayload()), NodeAddress.fromProto(proto.getSenderNodeAddress()), proto.getUid(), messageVersion); } @Override public long getTTL() { return TTL; } } ================================================ FILE: core/src/main/java/haveno/core/alert/PrivateNotificationPayload.java ================================================ /* * 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 . */ package haveno.core.alert; import com.google.protobuf.ByteString; import haveno.common.crypto.Sig; import haveno.common.proto.network.NetworkPayload; import haveno.common.util.Utilities; import lombok.EqualsAndHashCode; import lombok.Getter; import javax.annotation.Nullable; import java.security.PublicKey; import static com.google.common.base.Preconditions.checkNotNull; @EqualsAndHashCode @Getter public final class PrivateNotificationPayload implements NetworkPayload { private final String message; @Nullable private String signatureAsBase64; @Nullable private byte[] sigPublicKeyBytes; @Nullable private PublicKey sigPublicKey; public PrivateNotificationPayload(String message) { this.message = message; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @SuppressWarnings("NullableProblems") private PrivateNotificationPayload(String message, String signatureAsBase64, byte[] sigPublicKeyBytes) { this(message); this.signatureAsBase64 = signatureAsBase64; this.sigPublicKeyBytes = sigPublicKeyBytes; sigPublicKey = Sig.getPublicKeyFromBytes(sigPublicKeyBytes); } public static PrivateNotificationPayload fromProto(protobuf.PrivateNotificationPayload proto) { return new PrivateNotificationPayload(proto.getMessage(), proto.getSignatureAsBase64(), proto.getSigPublicKeyBytes().toByteArray()); } @Override public protobuf.PrivateNotificationPayload toProtoMessage() { checkNotNull(sigPublicKeyBytes, "sigPublicKeyBytes must not be null"); checkNotNull(signatureAsBase64, "signatureAsBase64 must not be null"); return protobuf.PrivateNotificationPayload.newBuilder() .setMessage(message) .setSignatureAsBase64(signatureAsBase64) .setSigPublicKeyBytes(ByteString.copyFrom(sigPublicKeyBytes)) .build(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void setSigAndPubKey(String signatureAsBase64, PublicKey sigPublicKey) { this.signatureAsBase64 = signatureAsBase64; this.sigPublicKey = sigPublicKey; sigPublicKeyBytes = Sig.getPublicKeyBytes(sigPublicKey); } // Hex @Override public String toString() { return "PrivateNotification{" + "message='" + message + '\'' + ", signatureAsBase64='" + signatureAsBase64 + '\'' + ", publicKeyBytes=" + Utilities.bytesAsHexString(sigPublicKeyBytes) + '}'; } } ================================================ FILE: core/src/main/java/haveno/core/api/AccountServiceListener.java ================================================ package haveno.core.api; /** * Default account listener (takes no action). */ public class AccountServiceListener { public void onAppInitialized() {} public void onAccountCreated() {} public void onAccountOpened() {} public void onAccountClosed() {} public void onAccountRestored(Runnable onShutDown) {} public void onAccountDeleted(Runnable onShutDown) {} public void onPasswordChanged(String oldPassword, String newPassword) {} } ================================================ FILE: core/src/main/java/haveno/core/api/CoreAccountService.java ================================================ /* * 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 . */ package haveno.core.api; import static com.google.common.base.Preconditions.checkState; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.app.Log; import haveno.common.config.Config; import haveno.common.crypto.IncorrectPasswordException; import haveno.common.crypto.KeyRing; import haveno.common.crypto.KeyStorage; import haveno.common.file.FileUtil; import haveno.common.persistence.PersistenceManager; import haveno.common.util.ZipUtils; import haveno.core.xmr.wallet.XmrWalletService; import java.io.File; import java.io.InputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.function.Consumer; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; /** * Manages the account state. A created account must have a password which encrypts * all persistence in the PersistenceManager. As a result, opening the account requires * a correct password to be passed in to deserialize the account properties that are * persisted. It is possible to persist the objects without a password (legacy). * * Backup and restore flushes the persistence objects in the app folder and sends or * restores a zip stream. */ @Singleton @Slf4j public class CoreAccountService { private final Config config; private final KeyStorage keyStorage; private final KeyRing keyRing; @Getter private String password; private List listeners = new ArrayList(); @Inject public CoreAccountService(Config config, KeyStorage keyStorage, KeyRing keyRing) { this.config = config; this.keyStorage = keyStorage; this.keyRing = keyRing; } public void addListener(AccountServiceListener listener) { synchronized (listeners) { listeners.add(listener); } } public boolean removeListener(AccountServiceListener listener) { synchronized (listeners) { return listeners.remove(listener); } } public boolean accountExists() { return keyStorage.allKeyFilesExist(); // public and private key pair indicate the existence of the account } public boolean isAccountOpen() { return keyRing.isUnlocked() && accountExists(); } public void checkAccountOpen() { checkState(isAccountOpen(), "Account not open"); } public void createAccount(String password) { if (accountExists()) throw new IllegalStateException("Cannot create account if account already exists"); keyRing.generateKeys(password); this.password = password; synchronized (listeners) { for (AccountServiceListener listener : new ArrayList<>(listeners)) listener.onAccountCreated(); } } public void openAccount(String password) throws IncorrectPasswordException { if (!accountExists()) throw new IllegalStateException("Cannot open account if account does not exist"); if (keyRing.unlockKeys(password, false)) { this.password = password; synchronized (listeners) { for (AccountServiceListener listener : new ArrayList<>(listeners)) listener.onAccountOpened(); } } else { throw new IllegalStateException("keyRing.unlockKeys() returned false, that should never happen"); } } public void changePassword(String oldPassword, String newPassword) { if (!isAccountOpen()) throw new IllegalStateException("Cannot change password on unopened account"); if ("".equals(oldPassword)) oldPassword = null; // normalize to null if (!StringUtils.equals(this.password, oldPassword)) throw new IllegalStateException("Incorrect password"); if (newPassword != null && newPassword.length() < 8) throw new IllegalStateException("Password must be at least 8 characters"); keyStorage.saveKeyRing(keyRing, oldPassword, newPassword); this.password = newPassword; synchronized (listeners) { for (AccountServiceListener listener : new ArrayList<>(listeners)) listener.onPasswordChanged(oldPassword, newPassword); } } public void verifyPassword(String password) throws IncorrectPasswordException { if (!StringUtils.equals(this.password, password)) { throw new IncorrectPasswordException("Incorrect password"); } } public void closeAccount() { if (!isAccountOpen()) throw new IllegalStateException("Cannot close unopened account"); keyRing.lockKeys(); // closed account means the keys are locked synchronized (listeners) { for (AccountServiceListener listener : new ArrayList<>(listeners)) listener.onAccountClosed(); } } // TODO: share common code with BackupView to backup public void backupAccount(int bufferSize, Consumer consume, Consumer error) { if (!accountExists()) throw new IllegalStateException("Cannot backup non existing account"); var accountWasOpen = isAccountOpen(); // Needed to unlock haveno_XMR.keys if (accountWasOpen) closeAccount(); // flush all known persistence objects to disk PersistenceManager.flushAllDataToDiskAtBackup(() -> { try { File dataDir = new File(config.appDataDir.getPath()); PipedInputStream in = new PipedInputStream(bufferSize); // pipe the serialized account object to stream which will be read by the consumer PipedOutputStream out = new PipedOutputStream(in); log.info("Zipping directory " + dataDir); // exclude monero binaries from backup so they're reinstalled with permissions List excludedFiles = Arrays.asList( new File(XmrWalletService.MONERO_WALLET_RPC_PATH), new File(XmrLocalNode.MONEROD_PATH) ); new Thread(() -> { try { ZipUtils.zipDirToStream(dataDir, out, bufferSize, excludedFiles); } catch (Exception ex) { error.accept(ex); } }).start(); consume.accept(in); } catch (java.io.IOException err) { error.accept(err); } }); if (accountWasOpen) { try { openAccount(password); } catch (Exception ex){ throw new RuntimeException(ex); } } } public void restoreAccount(InputStream inputStream, int bufferSize, Runnable onShutdown) throws Exception { if (accountExists()) throw new IllegalStateException("Cannot restore account if there is an existing account"); File dataDir = new File(config.appDataDir.getPath()); ZipUtils.unzipToDir(dataDir, inputStream, bufferSize); synchronized (listeners) { for (AccountServiceListener listener : new ArrayList<>(listeners)) listener.onAccountRestored(onShutdown); } } public void deleteAccount(Runnable onShutdown) { try { if (isAccountOpen()) closeAccount(); synchronized (listeners) { for (AccountServiceListener listener : new ArrayList<>(listeners)) listener.onAccountDeleted(onShutdown); } // Log files are locked on Windows so we need to release them. Logging resumes on automatic restart Log.stopFileLogging(); File dataDir = new File(config.appDataDir.getPath()); // TODO (woodser): deleting directory after gracefulShutdown() so services don't throw when they try to persist (e.g. XmrTxProofService), but gracefulShutdown() should honor read-only shutdown FileUtil.deleteDirectory(dataDir, null, false); } catch (Exception err) { throw new RuntimeException(err); } } } ================================================ FILE: core/src/main/java/haveno/core/api/CoreApi.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.api; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.app.Version; import haveno.common.config.Config; import haveno.common.crypto.IncorrectPasswordException; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.FaultHandler; import haveno.common.handlers.ResultHandler; import haveno.core.api.model.AddressBalanceInfo; import haveno.core.api.model.BalancesInfo; import haveno.core.api.model.MarketDepthInfo; import haveno.core.api.model.MarketPriceInfo; import haveno.core.api.model.PaymentAccountForm; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.app.AppStartupState; import haveno.core.offer.Offer; import haveno.core.offer.OpenOffer; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentMethod; import haveno.core.support.dispute.Attachment; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeResult; import haveno.core.support.messages.ChatMessage; import haveno.core.trade.Trade; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.xmr.XmrNodeSettings; import haveno.proto.grpc.NotificationMessage; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import monero.common.MoneroRpcConnection; import monero.wallet.model.MoneroDestination; import monero.wallet.model.MoneroTxWallet; import org.bitcoinj.core.Transaction; /** * Provides high level interface to functionality of core Haveno features. * E.g. useful for different APIs to access data of different domains of Haveno. */ @Singleton @Slf4j public class CoreApi { @Getter private final Config config; private final AppStartupState appStartupState; private final CoreAccountService coreAccountService; private final CoreDisputeAgentsService coreDisputeAgentsService; private final CoreDisputesService coreDisputeService; private final CoreHelpService coreHelpService; private final CoreOffersService coreOffersService; private final CorePaymentAccountsService paymentAccountsService; private final CorePriceService corePriceService; private final CoreTradesService coreTradesService; private final CoreWalletsService walletsService; private final TradeStatisticsManager tradeStatisticsManager; private final CoreNotificationService notificationService; private final XmrConnectionService xmrConnectionService; private final XmrLocalNode xmrLocalNode; @Inject public CoreApi(Config config, AppStartupState appStartupState, CoreAccountService coreAccountService, CoreDisputeAgentsService coreDisputeAgentsService, CoreDisputesService coreDisputeService, CoreHelpService coreHelpService, CoreOffersService coreOffersService, CorePaymentAccountsService paymentAccountsService, CorePriceService corePriceService, CoreTradesService coreTradesService, CoreWalletsService walletsService, TradeStatisticsManager tradeStatisticsManager, CoreNotificationService notificationService, XmrConnectionService xmrConnectionService, XmrLocalNode xmrLocalNode) { this.config = config; this.appStartupState = appStartupState; this.coreAccountService = coreAccountService; this.coreDisputeAgentsService = coreDisputeAgentsService; this.coreDisputeService = coreDisputeService; this.coreHelpService = coreHelpService; this.coreOffersService = coreOffersService; this.paymentAccountsService = paymentAccountsService; this.coreTradesService = coreTradesService; this.corePriceService = corePriceService; this.walletsService = walletsService; this.tradeStatisticsManager = tradeStatisticsManager; this.notificationService = notificationService; this.xmrConnectionService = xmrConnectionService; this.xmrLocalNode = xmrLocalNode; } @SuppressWarnings("SameReturnValue") public String getVersion() { return Version.VERSION; } /////////////////////////////////////////////////////////////////////////////////////////// // Help /////////////////////////////////////////////////////////////////////////////////////////// public String getMethodHelp(String methodName) { return coreHelpService.getMethodHelp(methodName); } /////////////////////////////////////////////////////////////////////////////////////////// // Account Service /////////////////////////////////////////////////////////////////////////////////////////// public boolean accountExists() { return coreAccountService.accountExists(); } public boolean isAccountOpen() { return coreAccountService.isAccountOpen(); } public void createAccount(String password) { coreAccountService.createAccount(password); } public void openAccount(String password) throws IncorrectPasswordException { coreAccountService.openAccount(password); } public boolean isAppInitialized() { return appStartupState.isApplicationFullyInitialized(); } public void changePassword(String oldPassword, String newPassword) { coreAccountService.changePassword(oldPassword, newPassword); } public void closeAccount() { coreAccountService.closeAccount(); } public void deleteAccount(Runnable onShutdown) { coreAccountService.deleteAccount(onShutdown); } public void backupAccount(int bufferSize, Consumer consume, Consumer error) { coreAccountService.backupAccount(bufferSize, consume, error); } public void restoreAccount(InputStream zipStream, int bufferSize, Runnable onShutdown) throws Exception { coreAccountService.restoreAccount(zipStream, bufferSize, onShutdown); } /////////////////////////////////////////////////////////////////////////////////////////// // Monero Connections /////////////////////////////////////////////////////////////////////////////////////////// public void addXmrConnection(MoneroRpcConnection connection) { xmrConnectionService.addConnection(connection); } public void removeXmrConnection(String connectionUri) { xmrConnectionService.removeConnection(connectionUri); } public MoneroRpcConnection getXmrConnection() { return xmrConnectionService.getConnection(); } public List getXmrConnections() { return xmrConnectionService.getConnections(); } public void setXmrConnection(String connectionUri) { xmrConnectionService.setConnection(connectionUri); } public void setXmrConnection(MoneroRpcConnection connection) { xmrConnectionService.setConnection(connection); } public MoneroRpcConnection checkXmrConnection() { return xmrConnectionService.checkConnection(); } public MoneroRpcConnection getBestXmrConnection() { return xmrConnectionService.getBestConnection(); } public void setXmrConnectionAutoSwitch(boolean autoSwitch) { xmrConnectionService.setAutoSwitch(autoSwitch); } public boolean getXmrConnectionAutoSwitch() { return xmrConnectionService.getAutoSwitch(); } /////////////////////////////////////////////////////////////////////////////////////////// // Monero node /////////////////////////////////////////////////////////////////////////////////////////// public boolean isXmrNodeOnline() { return xmrLocalNode.isDetected(); } public XmrNodeSettings getXmrNodeSettings() { return xmrLocalNode.getNodeSettings(); } public void startXmrNode(XmrNodeSettings settings) throws IOException { xmrLocalNode.start(settings); } public void stopXmrNode() { xmrLocalNode.stop(); } /////////////////////////////////////////////////////////////////////////////////////////// // Wallets /////////////////////////////////////////////////////////////////////////////////////////// public BalancesInfo getBalances(String currencyCode) { return walletsService.getBalances(currencyCode); } public String getXmrSeed() { return walletsService.getXmrSeed(); } public String getXmrPrimaryAddress() { return walletsService.getXmrPrimaryAddress(); } public String getXmrNewSubaddress() { return walletsService.getXmrNewSubaddress(); } public List getXmrTxs() { return walletsService.getXmrTxs(); } public MoneroTxWallet createXmrTx(List destinations) { return walletsService.createXmrTx(destinations); } public List createXmrSweepTxs(String address) { return walletsService.createXmrSweepTxs(address); } public List relayXmrTxs(List metadatas) { return walletsService.relayXmrTxs(metadatas); } public long getAddressBalance(String addressString) { return walletsService.getAddressBalance(addressString); } public AddressBalanceInfo getAddressBalanceInfo(String addressString) { return walletsService.getAddressBalanceInfo(addressString); } public List getFundingAddresses() { return walletsService.getFundingAddresses(); } public Transaction getTransaction(String txId) { return walletsService.getTransaction(txId); } public void setWalletPassword(String password, String newPassword) { walletsService.setWalletPassword(password, newPassword); } public void lockWallet() { walletsService.lockWallet(); } public void unlockWallet(String password, long timeout) { walletsService.unlockWallet(password, timeout); } public void removeWalletPassword(String password) { walletsService.removeWalletPassword(password); } public int getNumConfirmationsForMostRecentTransaction(String addressString) { return walletsService.getNumConfirmationsForMostRecentTransaction(addressString); } public long getHeight() { return walletsService.getHeight(); } public long getTargetHeight() { return Objects.requireNonNullElse(xmrConnectionService.getTargetHeight(), 0L); } /////////////////////////////////////////////////////////////////////////////////////////// // Notifications /////////////////////////////////////////////////////////////////////////////////////////// public void addNotificationListener(NotificationListener listener) { notificationService.addListener(listener); } public void sendNotification(NotificationMessage notification) { notificationService.sendNotification(notification); } /////////////////////////////////////////////////////////////////////////////////////////// // Disputes /////////////////////////////////////////////////////////////////////////////////////////// public List getDisputes() { return coreDisputeService.getDisputes(); } public Dispute getDispute(String tradeId) { return coreDisputeService.getDispute(tradeId); } public void openDispute(String tradeId, ResultHandler resultHandler, FaultHandler faultHandler) { coreDisputeService.openDispute(tradeId, resultHandler, faultHandler); } public void resolveDispute(String tradeId, DisputeResult.Winner winner, DisputeResult.Reason reason, String summaryNotes, long customPayoutAmount) { coreDisputeService.resolveDispute(tradeId, winner, reason, summaryNotes, customPayoutAmount); } public void sendDisputeChatMessage(String disputeId, String message, ArrayList attachments) { coreDisputeService.sendDisputeChatMessage(disputeId, message, attachments); } /////////////////////////////////////////////////////////////////////////////////////////// // Dispute Agents /////////////////////////////////////////////////////////////////////////////////////////// public void registerDisputeAgent(String disputeAgentType, String registrationKey, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { coreDisputeAgentsService.registerDisputeAgent(disputeAgentType, registrationKey, resultHandler, errorMessageHandler); } public void unregisterDisputeAgent(String disputeAgentType, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { coreDisputeAgentsService.unregisterDisputeAgent(disputeAgentType, resultHandler, errorMessageHandler); } /////////////////////////////////////////////////////////////////////////////////////////// // Offers /////////////////////////////////////////////////////////////////////////////////////////// public Offer getOffer(String id) { return coreOffersService.getOffer(id); } public List getOffers(String direction, String currencyCode) { return coreOffersService.getOffers(direction, currencyCode); } public List getMyOffers(String direction, String currencyCode) { return coreOffersService.getMyOffers(direction, currencyCode); } public OpenOffer getMyOffer(String id) { return coreOffersService.getMyOffer(id); } public void postOffer(String currencyCode, String directionAsString, String priceAsString, boolean useMarketBasedPrice, double marketPriceMarginPct, long amountAsLong, long minAmountAsLong, double securityDepositPct, String triggerPriceAsString, boolean reserveExactAmount, String paymentAccountId, boolean isPrivateOffer, boolean buyerAsTakerWithoutDeposit, String extraInfo, String sourceOfferId, Consumer resultHandler, ErrorMessageHandler errorMessageHandler) { coreOffersService.postOffer(currencyCode, directionAsString, priceAsString, useMarketBasedPrice, marketPriceMarginPct, amountAsLong, minAmountAsLong, securityDepositPct, triggerPriceAsString, reserveExactAmount, paymentAccountId, isPrivateOffer, buyerAsTakerWithoutDeposit, extraInfo, sourceOfferId, resultHandler, errorMessageHandler); } public void editOffer(String offerId, String currencyCode, String priceAsString, boolean useMarketBasedPrice, double marketPriceMarginPct, String triggerPriceAsString, String paymentAccountId, String extraInfo, Consumer resultHandler, ErrorMessageHandler errorMessageHandler) { coreOffersService.editOffer(offerId, currencyCode, priceAsString, useMarketBasedPrice, marketPriceMarginPct, triggerPriceAsString, paymentAccountId, extraInfo, resultHandler, errorMessageHandler); } public void deactivateOffer(String offerId, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { coreOffersService.deactivateOffer(offerId, resultHandler, errorMessageHandler); } public void activateOffer(String offerId, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { coreOffersService.activateOffer(offerId, resultHandler, errorMessageHandler); } public void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { coreOffersService.cancelOffer(id, resultHandler, errorMessageHandler); } /////////////////////////////////////////////////////////////////////////////////////////// // PaymentAccounts /////////////////////////////////////////////////////////////////////////////////////////// public PaymentAccount createPaymentAccount(PaymentAccountForm form) { return paymentAccountsService.createPaymentAccount(form); } public Set getPaymentAccounts() { return paymentAccountsService.getPaymentAccounts(); } public List getPaymentMethods() { return paymentAccountsService.getPaymentMethods(); } public PaymentAccountForm getPaymentAccountForm(String paymentMethodId) { return paymentAccountsService.getPaymentAccountForm(paymentMethodId); } public PaymentAccountForm getPaymentAccountForm(PaymentAccount paymentAccount) { return paymentAccountsService.getPaymentAccountForm(paymentAccount); } public PaymentAccount createCryptoCurrencyPaymentAccount(String accountName, String currencyCode, String address, boolean tradeInstant) { return paymentAccountsService.createCryptoCurrencyPaymentAccount(accountName, currencyCode, address, tradeInstant); } public void deletePaymentAccount(String paymentAccountId) { paymentAccountsService.deletePaymentAccount(paymentAccountId); } public List getCryptoCurrencyPaymentMethods() { return paymentAccountsService.getCryptoCurrencyPaymentMethods(); } public void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) { paymentAccountsService.validateFormField(form, fieldId, value); } /////////////////////////////////////////////////////////////////////////////////////////// // Prices /////////////////////////////////////////////////////////////////////////////////////////// public double getMarketPrice(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException { return corePriceService.getMarketPrice(currencyCode); } public List getMarketPrices() throws ExecutionException, InterruptedException, TimeoutException { return corePriceService.getMarketPrices(); } public MarketDepthInfo getMarketDepth(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException { return corePriceService.getMarketDepth(currencyCode); } /////////////////////////////////////////////////////////////////////////////////////////// // Trades /////////////////////////////////////////////////////////////////////////////////////////// public void takeOffer(String offerId, String paymentAccountId, long amountAsLong, String challenge, Consumer resultHandler, ErrorMessageHandler errorMessageHandler) { Offer offer = coreOffersService.getOffer(offerId); offer.setChallenge(challenge); coreTradesService.takeOffer(offer, paymentAccountId, amountAsLong, resultHandler, errorMessageHandler); } public void confirmPaymentSent(String tradeId, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { coreTradesService.confirmPaymentSent(tradeId, resultHandler, errorMessageHandler); } public void confirmPaymentReceived(String tradeId, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { coreTradesService.confirmPaymentReceived(tradeId, resultHandler, errorMessageHandler); } public void closeTrade(String tradeId) { coreTradesService.closeTrade(tradeId); } public Trade getTrade(String tradeId) { return coreTradesService.getTrade(tradeId); } public List getTrades() { return coreTradesService.getTrades(); } public List getChatMessages(String tradeId) { return coreTradesService.getChatMessages(tradeId); } public void sendChatMessage(String tradeId, String message) { coreTradesService.sendChatMessage(tradeId, message); } } ================================================ FILE: core/src/main/java/haveno/core/api/CoreContext.java ================================================ /* * 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 . */ package haveno.core.api; import com.google.inject.Inject; import com.google.inject.Singleton; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @Singleton @Slf4j public class CoreContext { @Getter @Setter private boolean isApiUser; @Inject public CoreContext() { } } ================================================ FILE: core/src/main/java/haveno/core/api/CoreDisputeAgentsService.java ================================================ /* * 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 . */ package haveno.core.api; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.config.Config; import haveno.common.crypto.KeyRing; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.core.support.SupportType; import static haveno.core.support.SupportType.ARBITRATION; import static haveno.core.support.SupportType.MEDIATION; import static haveno.core.support.SupportType.REFUND; import static haveno.core.support.SupportType.TRADE; import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.support.dispute.mediation.mediator.Mediator; import haveno.core.support.dispute.mediation.mediator.MediatorManager; import haveno.core.support.dispute.refund.refundagent.RefundAgent; import haveno.core.support.dispute.refund.refundagent.RefundAgentManager; import haveno.core.user.User; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import static java.lang.String.format; import static java.net.InetAddress.getLoopbackAddress; import java.util.ArrayList; import static java.util.Arrays.asList; import java.util.Date; import java.util.List; import java.util.Objects; import java.util.Optional; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.ECKey; @Singleton @Slf4j class CoreDisputeAgentsService { private final User user; private final Config config; private final KeyRing keyRing; private final XmrWalletService xmrWalletService; private final ArbitratorManager arbitratorManager; private final MediatorManager mediatorManager; private final RefundAgentManager refundAgentManager; private final P2PService p2PService; private final NodeAddress nodeAddress; private final List languageCodes; @Inject public CoreDisputeAgentsService(User user, Config config, KeyRing keyRing, XmrWalletService xmrWalletService, ArbitratorManager arbitratorManager, MediatorManager mediatorManager, RefundAgentManager refundAgentManager, P2PService p2PService) { this.user = user; this.config = config; this.keyRing = keyRing; this.xmrWalletService = xmrWalletService; this.arbitratorManager = arbitratorManager; this.mediatorManager = mediatorManager; this.refundAgentManager = refundAgentManager; this.p2PService = p2PService; this.nodeAddress = new NodeAddress(getLoopbackAddress().getHostName(), config.nodePort); this.languageCodes = asList("de", "en", "es", "fr"); } void registerDisputeAgent(String disputeAgentType, String registrationKey, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { if (!p2PService.isBootstrapped()) throw new IllegalStateException("p2p service is not bootstrapped yet"); Optional supportType = getSupportType(disputeAgentType); if (supportType.isPresent()) { ECKey ecKey; String signature; switch (supportType.get()) { case ARBITRATION: if (user.getRegisteredArbitrator() != null) { log.warn("ignoring request to re-register as arbitrator"); resultHandler.handleResult(); return; } ecKey = arbitratorManager.getRegistrationKey(registrationKey); if (ecKey == null) throw new IllegalStateException("invalid registration key"); signature = arbitratorManager.signStorageSignaturePubKey(Objects.requireNonNull(ecKey)); registerArbitrator(nodeAddress, languageCodes, ecKey, signature, resultHandler, errorMessageHandler); return; case MEDIATION: if (user.getRegisteredMediator() != null) { log.warn("ignoring request to re-register as mediator"); resultHandler.handleResult(); return; } ecKey = mediatorManager.getRegistrationKey(registrationKey); if (ecKey == null) throw new IllegalStateException("invalid registration key"); signature = mediatorManager.signStorageSignaturePubKey(Objects.requireNonNull(ecKey)); registerMediator(nodeAddress, languageCodes, ecKey, signature); return; case REFUND: if (user.getRegisteredRefundAgent() != null) { log.warn("ignoring request to re-register as refund agent"); resultHandler.handleResult(); return; } ecKey = refundAgentManager.getRegistrationKey(registrationKey); if (ecKey == null) throw new IllegalStateException("invalid registration key"); signature = refundAgentManager.signStorageSignaturePubKey(Objects.requireNonNull(ecKey)); registerRefundAgent(nodeAddress, languageCodes, ecKey, signature); return; case TRADE: throw new IllegalArgumentException("trade agent registration not supported"); } } else { throw new IllegalArgumentException(format("unknown dispute agent type '%s'", disputeAgentType)); } } void unregisterDisputeAgent(String disputeAgentType, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { if (!p2PService.isBootstrapped()) throw new IllegalStateException("p2p service is not bootstrapped yet"); Optional supportType = getSupportType(disputeAgentType); if (supportType.isPresent()) { switch (supportType.get()) { case ARBITRATION: if (user.getRegisteredArbitrator() == null) { errorMessageHandler.handleErrorMessage("User is not arbitrator"); return; } unregisterDisputeAgent(resultHandler, errorMessageHandler); return; case MEDIATION: throw new IllegalStateException("unregister mediator not implemented"); case REFUND: throw new IllegalStateException("unregister refund agent not implemented"); case TRADE: throw new IllegalArgumentException("trade agent registration not supported"); } } else { throw new IllegalArgumentException(format("unknown dispute agent type '%s'", disputeAgentType)); } } private void registerArbitrator(NodeAddress nodeAddress, List languageCodes, ECKey ecKey, String signature, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { Arbitrator arbitrator = new Arbitrator( p2PService.getAddress(), keyRing.getPubKeyRing(), new ArrayList<>(languageCodes), new Date().getTime(), ecKey.getPubKey(), signature, "", null, null); arbitratorManager.addDisputeAgent(arbitrator, () -> { if (!arbitratorManager.getDisputeAgentByNodeAddress(nodeAddress).isPresent()) errorMessageHandler.handleErrorMessage("could not register arbitrator"); else resultHandler.handleResult(); }, errorMessageHandler); } private void registerMediator(NodeAddress nodeAddress, List languageCodes, ECKey ecKey, String signature) { Mediator mediator = new Mediator(nodeAddress, keyRing.getPubKeyRing(), new ArrayList<>(languageCodes), new Date().getTime(), ecKey.getPubKey(), signature, null, null, null ); mediatorManager.addDisputeAgent(mediator, () -> { }, errorMessage -> { }); mediatorManager.getDisputeAgentByNodeAddress(nodeAddress).orElseThrow(() -> new IllegalStateException("could not register mediator")); } private void registerRefundAgent(NodeAddress nodeAddress, List languageCodes, ECKey ecKey, String signature) { RefundAgent refundAgent = new RefundAgent(nodeAddress, keyRing.getPubKeyRing(), new ArrayList<>(languageCodes), new Date().getTime(), ecKey.getPubKey(), signature, null, null, null ); refundAgentManager.addDisputeAgent(refundAgent, () -> { }, errorMessage -> { }); refundAgentManager.getDisputeAgentByNodeAddress(nodeAddress).orElseThrow(() -> new IllegalStateException("could not register refund agent")); } private Optional getSupportType(String disputeAgentType) { switch (disputeAgentType.toLowerCase()) { case "arbitrator": return Optional.of(ARBITRATION); case "mediator": return Optional.of(MEDIATION); case "refundagent": case "refund_agent": return Optional.of(REFUND); case "tradeagent": case "trade_agent": return Optional.of(TRADE); default: return Optional.empty(); } } private void unregisterDisputeAgent(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { arbitratorManager.removeDisputeAgent(resultHandler, errorMesage -> { errorMessageHandler.handleErrorMessage("Error unregistering dispute agent: " + errorMesage); }); } } ================================================ FILE: core/src/main/java/haveno/core/api/CoreDisputesService.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.api; import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.inject.name.Named; import haveno.common.crypto.KeyRing; import haveno.common.crypto.PubKeyRing; import haveno.common.handlers.FaultHandler; import haveno.common.handlers.ResultHandler; import haveno.core.locale.Res; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.support.SupportType; import haveno.core.support.dispute.Attachment; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeManager; import haveno.core.support.dispute.DisputeResult; import haveno.core.support.dispute.DisputeSummaryVerification; import haveno.core.support.dispute.DisputeResult.SubtractFeeFrom; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.messages.ChatMessage; import haveno.core.trade.Contract; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.core.xmr.wallet.XmrWalletService; import static java.lang.String.format; import java.math.BigInteger; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Optional; import org.apache.commons.lang3.exception.ExceptionUtils; import lombok.extern.slf4j.Slf4j; @Singleton @Slf4j public class CoreDisputesService { // TODO: persist in DisputeResult? public enum PayoutSuggestion { BUYER_GETS_TRADE_AMOUNT, BUYER_GETS_ALL, SELLER_GETS_TRADE_AMOUNT, SELLER_GETS_ALL, CUSTOM } private final ArbitrationManager arbitrationManager; private final CoinFormatter formatter; private final KeyRing keyRing; private final TradeManager tradeManager; @Inject public CoreDisputesService(ArbitrationManager arbitrationManager, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, // TODO: XMR? KeyRing keyRing, TradeManager tradeManager, XmrWalletService xmrWalletService) { this.arbitrationManager = arbitrationManager; this.formatter = formatter; this.keyRing = keyRing; this.tradeManager = tradeManager; } public List getDisputes() { return new ArrayList<>(arbitrationManager.getDisputesAsObservableList()); } public Dispute getDispute(String tradeId) { Optional dispute = arbitrationManager.findDispute(tradeId); if (dispute.isPresent()) return dispute.get(); else throw new IllegalStateException(format("dispute for trade id '%s' not found", tradeId)); } public void openDispute(String tradeId, ResultHandler resultHandler, FaultHandler faultHandler) { Trade trade = tradeManager.getOpenTrade(tradeId).orElseThrow(() -> new IllegalArgumentException(format("trade with id '%s' not found", tradeId))); Offer offer = trade.getOffer(); if (offer == null) throw new IllegalStateException(format("offer with tradeId '%s' is null", tradeId)); // Dispute agents are registered as mediators and refund agents, but current UI appears to be hardcoded // to reference the arbitrator. Reference code is in desktop PendingTradesDataModel.java and could be refactored. var disputeManager = arbitrationManager; var isSupportTicket = false; var isMaker = tradeManager.isMyOffer(offer); var dispute = createDisputeForTrade(trade, offer, keyRing.getPubKeyRing(), isMaker, isSupportTicket); // Sends the openNewDisputeMessage to arbitrator, who will then create 2 disputes // one for the opener, the other for the peer, see sendPeerOpenedDisputeMessage. disputeManager.sendDisputeOpenedMessage(dispute, resultHandler, faultHandler); tradeManager.requestPersistence(); } public Dispute createDisputeForTrade(Trade trade, Offer offer, PubKeyRing pubKey, boolean isMaker, boolean isSupportTicket) { byte[] payoutTxSerialized = null; String payoutTxHashAsString = null; PubKeyRing arbitratorPubKeyRing = trade.getArbitrator().getPubKeyRing(); checkNotNull(arbitratorPubKeyRing, "arbitratorPubKeyRing must not be null"); Dispute dispute = new Dispute(new Date().getTime(), trade.getId(), pubKey.hashCode(), // trader id, true, (offer.getDirection() == OfferDirection.BUY) == isMaker, isMaker, pubKey, trade.getDate().getTime(), trade.getMaxTradePeriodDate().getTime(), trade.getContract(), trade.getContractHash(), payoutTxSerialized, payoutTxHashAsString, trade.getContractAsJson(), trade.getMaker().getContractSignature(), trade.getTaker().getContractSignature(), trade.getMaker().getPaymentAccountPayload(), trade.getTaker().getPaymentAccountPayload(), arbitratorPubKeyRing, isSupportTicket, SupportType.ARBITRATION); return dispute; } // TODO: does not wait for success or error response public void resolveDispute(String tradeId, DisputeResult.Winner winner, DisputeResult.Reason reason, String summaryNotes, long customWinnerAmount) { // get winning dispute Dispute winningDispute; Trade trade = tradeManager.getTrade(tradeId); var winningDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream() // TODO (woodser): use getDispute() .filter(d -> tradeId.equals(d.getTradeId())) .filter(d -> trade.getTradePeer(d.getTraderPubKeyRing()) == (winner == DisputeResult.Winner.BUYER ? trade.getBuyer() : trade.getSeller())) .findFirst(); if (winningDisputeOptional.isPresent()) winningDispute = winningDisputeOptional.get(); else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId)); synchronized (trade.getLock()) { try { // create dispute result var closeDate = new Date(); var winnerDisputeResult = createDisputeResult(winningDispute, winner, reason, summaryNotes, closeDate); PayoutSuggestion payoutSuggestion; if (customWinnerAmount > 0) { payoutSuggestion = PayoutSuggestion.CUSTOM; } else if (winner == DisputeResult.Winner.BUYER) { payoutSuggestion = PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT; } else if (winner == DisputeResult.Winner.SELLER) { payoutSuggestion = PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT; } else { throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner); } applyPayoutAmountsToDisputeResult(payoutSuggestion, winningDispute, winnerDisputeResult, customWinnerAmount); // close winning dispute ticket closeDisputeTicket(arbitrationManager, winningDispute, winnerDisputeResult, () -> { arbitrationManager.requestPersistence(); }, (errMessage, err) -> { throw new IllegalStateException(errMessage, err); }); // close loser's dispute ticket var loserDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream() .filter(d -> tradeId.equals(d.getTradeId()) && winningDispute.getTraderId() != d.getTraderId()) .findFirst(); if (!loserDisputeOptional.isPresent()) throw new IllegalStateException("could not find peer dispute"); var loserDispute = loserDisputeOptional.get(); var loserDisputeResult = createDisputeResult(loserDispute, winner, reason, summaryNotes, closeDate); loserDisputeResult.setBuyerPayoutAmountBeforeCost(winnerDisputeResult.getBuyerPayoutAmountBeforeCost()); loserDisputeResult.setSellerPayoutAmountBeforeCost(winnerDisputeResult.getSellerPayoutAmountBeforeCost()); loserDisputeResult.setSubtractFeeFrom(winnerDisputeResult.getSubtractFeeFrom()); closeDisputeTicket(arbitrationManager, loserDispute, loserDisputeResult, () -> { arbitrationManager.requestPersistence(); }, (errMessage, err) -> { throw new IllegalStateException(errMessage, err); }); } catch (Exception e) { log.error(ExceptionUtils.getStackTrace(e)); throw new IllegalStateException(e.getMessage() == null ? ("Error resolving dispute for trade " + trade.getId()) : e.getMessage()); } } } private DisputeResult createDisputeResult(Dispute dispute, DisputeResult.Winner winner, DisputeResult.Reason reason, String summaryNotes, Date closeDate) { var disputeResult = new DisputeResult(dispute.getTradeId(), dispute.getTraderId()); disputeResult.setWinner(winner); disputeResult.setReason(reason); disputeResult.setSummaryNotes(summaryNotes); disputeResult.setCloseDate(closeDate); return disputeResult; } /** * Sets payout amounts given a payout type. If custom is selected, the winner gets a custom amount, and the peer * receives the remaining amount minus the mining fee. */ public void applyPayoutAmountsToDisputeResult(PayoutSuggestion payoutSuggestion, Dispute dispute, DisputeResult disputeResult, long customWinnerAmount) { Contract contract = dispute.getContract(); Trade trade = tradeManager.getTrade(dispute.getTradeId()); BigInteger buyerSecurityDeposit = trade.getBuyer().getSecurityDeposit(); BigInteger sellerSecurityDeposit = trade.getSeller().getSecurityDeposit(); BigInteger tradeAmount = contract.getTradeAmount(); BigInteger tradeWalletBalance = trade.getWalletBalance(); disputeResult.setSubtractFeeFrom(DisputeResult.SubtractFeeFrom.BUYER_AND_SELLER); if (payoutSuggestion == PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT) { disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit)); disputeResult.setSellerPayoutAmountBeforeCost(sellerSecurityDeposit); } else if (payoutSuggestion == PayoutSuggestion.BUYER_GETS_ALL) { disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)); // TODO (woodser): apply min payout to incentivize loser? (see post v1.1.7) disputeResult.setSellerPayoutAmountBeforeCost(BigInteger.ZERO); if (!trade.isPayoutPublished() && disputeResult.getBuyerPayoutAmountBeforeCost().compareTo(tradeWalletBalance) > 0) { // in case peer's deposit transaction is not confirmed log.warn("Payout amount for buyer is more than wallet's balance. This can happen if a deposit tx is dropped. Decreasing payout amount from {} to {}", HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmountBeforeCost()), HavenoUtils.formatXmr(tradeWalletBalance)); disputeResult.setBuyerPayoutAmountBeforeCost(tradeWalletBalance); } } else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT) { disputeResult.setBuyerPayoutAmountBeforeCost(buyerSecurityDeposit); disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit)); } else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_ALL) { disputeResult.setBuyerPayoutAmountBeforeCost(BigInteger.ZERO); disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit).add(buyerSecurityDeposit)); if (!trade.isPayoutPublished() && disputeResult.getSellerPayoutAmountBeforeCost().compareTo(tradeWalletBalance) > 0) { // in case peer's deposit transaction is not confirmed log.warn("Payout amount for seller is more than wallet's balance. This can happen if a deposit tx is dropped. Decreasing payout amount from {} to {}", HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmountBeforeCost()), HavenoUtils.formatXmr(tradeWalletBalance)); disputeResult.setSellerPayoutAmountBeforeCost(tradeWalletBalance); } } else if (payoutSuggestion == PayoutSuggestion.CUSTOM) { if (!trade.isPayoutPublished() && customWinnerAmount > tradeWalletBalance.longValueExact()) throw new RuntimeException("Winner payout is more than the trade wallet's balance"); long loserAmount = tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit).subtract(BigInteger.valueOf(customWinnerAmount)).longValueExact(); if (loserAmount < 0) throw new RuntimeException("Loser payout cannot be negative"); disputeResult.setBuyerPayoutAmountBeforeCost(BigInteger.valueOf(disputeResult.getWinner() == DisputeResult.Winner.BUYER ? customWinnerAmount : loserAmount)); disputeResult.setSellerPayoutAmountBeforeCost(BigInteger.valueOf(disputeResult.getWinner() == DisputeResult.Winner.BUYER ? loserAmount : customWinnerAmount)); disputeResult.setSubtractFeeFrom(disputeResult.getWinner() == DisputeResult.Winner.BUYER ? SubtractFeeFrom.SELLER_ONLY : SubtractFeeFrom.BUYER_ONLY); // winner gets exact amount, loser pays mining fee } } public void closeDisputeTicket(DisputeManager disputeManager, Dispute dispute, DisputeResult disputeResult, ResultHandler resultHandler, FaultHandler faultHandler) { DisputeResult.Reason reason = disputeResult.getReason(); String role = Res.get("shared.arbitrator"); String agentNodeAddress = checkNotNull(disputeManager.getAgentNodeAddress(dispute)).getFullAddress(); Contract contract = dispute.getContract(); String currencyCode = contract.getOfferPayload().getCurrencyCode(); String amount = HavenoUtils.formatXmr(contract.getTradeAmount(), true); String textToSign = Res.get("disputeSummaryWindow.close.msg", FormattingUtils.formatDateTime(disputeResult.getCloseDate(), true), role, agentNodeAddress, dispute.getShortTradeId(), currencyCode, Res.get("disputeSummaryWindow.reason." + reason.name()), amount, HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmountBeforeCost(), true), HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmountBeforeCost(), true), disputeResult.summaryNotesProperty().get() ); synchronized (dispute.getChatMessages()) { if (reason == DisputeResult.Reason.OPTION_TRADE && dispute.getChatMessages().size() > 1 && dispute.getChatMessages().get(1).isSystemMessage()) { textToSign += "\n" + dispute.getChatMessages().get(1).getMessage() + "\n"; } } String summaryText = DisputeSummaryVerification.signAndApply(disputeManager, disputeResult, textToSign); disputeManager.closeDisputeTicket(disputeResult, dispute, summaryText, () -> { dispute.setDisputeResult(disputeResult); dispute.setIsClosed(); resultHandler.handleResult(); }, faultHandler); } public void sendDisputeChatMessage(String disputeId, String message, ArrayList attachments) { var disputeOptional = arbitrationManager.findDisputeById(disputeId); Dispute dispute; if (disputeOptional.isPresent()) dispute = disputeOptional.get(); else throw new IllegalStateException(format("dispute with id '%s' not found", disputeId)); if (!arbitrationManager.canSendChatMessages(dispute)) throw new IllegalStateException(format("dispute with id '%s' cannot send chat messages (must be open or stored to mailbox)", disputeId)); ChatMessage chatMessage = new ChatMessage( arbitrationManager.getSupportType(), dispute.getTradeId(), dispute.getTraderId(), arbitrationManager.isTrader(dispute), message, arbitrationManager.getMyAddress(), attachments); dispute.addAndPersistChatMessage(chatMessage); arbitrationManager.sendChatMessage(chatMessage); } } ================================================ FILE: core/src/main/java/haveno/core/api/CoreHelpService.java ================================================ /* * 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 . */ package haveno.core.api; import com.google.inject.Inject; import com.google.inject.Singleton; import java.io.BufferedReader; import static java.io.File.separator; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import static java.lang.String.format; import static java.lang.System.out; import lombok.extern.slf4j.Slf4j; @Singleton @Slf4j class CoreHelpService { @Inject public CoreHelpService() { } public String getMethodHelp(String methodName) { String resourceFile = "/help" + separator + methodName + "-" + "help.txt"; try { return readHelpFile(resourceFile); } catch (NullPointerException ex) { log.error("", ex); throw new IllegalStateException(format("no help found for api method %s", methodName)); } catch (IOException ex) { log.error("", ex); throw new IllegalStateException(format("could not read %s help doc", methodName)); } } private String readHelpFile(String resourceFile) throws NullPointerException, IOException { // The deployed text file is in the core.jar file, so use // Class.getResourceAsStream to read it. InputStream is = getClass().getResourceAsStream(resourceFile); BufferedReader br = new BufferedReader(new InputStreamReader(is)); String line; StringBuilder builder = new StringBuilder(); while ((line = br.readLine()) != null) builder.append(line).append("\n"); return builder.toString(); } // Main method for devs to view help text without running the server. @SuppressWarnings("CommentedOutCode") public static void main(String[] args) { CoreHelpService coreHelpService = new CoreHelpService(); out.println(coreHelpService.getMethodHelp("getversion")); // out.println(coreHelpService.getMethodHelp("getfundingaddresses")); // out.println(coreHelpService.getMethodHelp("getfundingaddresses")); // out.println(coreHelpService.getMethodHelp("unsettxfeerate")); // out.println(coreHelpService.getMethodHelp("getpaymentmethods")); // out.println(coreHelpService.getMethodHelp("getpaymentaccts")); // out.println(coreHelpService.getMethodHelp("lockwallet")); // out.println(coreHelpService.getMethodHelp("gettxfeerate")); // out.println(coreHelpService.getMethodHelp("createoffer")); // out.println(coreHelpService.getMethodHelp("takeoffer")); // out.println(coreHelpService.getMethodHelp("garbage")); // out.println(coreHelpService.getMethodHelp("")); // out.println(coreHelpService.getMethodHelp(null)); } } ================================================ FILE: core/src/main/java/haveno/core/api/CoreNotificationService.java ================================================ package haveno.core.api; import com.google.inject.Singleton; import haveno.core.api.model.TradeInfo; import haveno.core.support.messages.ChatMessage; import haveno.core.trade.BuyerTrade; import haveno.core.trade.HavenoUtils; import haveno.core.trade.MakerTrade; import haveno.core.trade.SellerTrade; import haveno.core.trade.Trade; import haveno.core.trade.Trade.Phase; import haveno.proto.grpc.NotificationMessage; import haveno.proto.grpc.NotificationMessage.NotificationType; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; @Singleton @Slf4j public class CoreNotificationService { private final Object lock = new Object(); private final List listeners = new LinkedList<>(); public void addListener(@NonNull NotificationListener listener) { synchronized (lock) { listeners.add(listener); } } public void sendNotification(@NonNull NotificationMessage notification) { synchronized (lock) { for (Iterator iter = listeners.iterator(); iter.hasNext(); ) { NotificationListener listener = iter.next(); try { listener.onMessage(notification); } catch (RuntimeException e) { log.warn("Failed to send notification to listener {}: {}", listener, e.getMessage()); iter.remove(); } } } } public void sendAppInitializedNotification() { sendNotification(NotificationMessage.newBuilder() .setType(NotificationType.APP_INITIALIZED) .setTimestamp(System.currentTimeMillis()) .build()); } public void sendTradeNotification(Trade trade, Phase phase, String title, String message) { // play chime when maker's trade is taken if (trade instanceof MakerTrade && phase == Trade.Phase.DEPOSITS_PUBLISHED) HavenoUtils.playChimeSound(); // play chime when buyer can confirm payment sent if (trade instanceof BuyerTrade && phase == Trade.Phase.DEPOSITS_UNLOCKED) HavenoUtils.playChimeSound(); // play chime when seller sees buyer confirm payment sent if (trade instanceof SellerTrade && phase == Trade.Phase.PAYMENT_SENT) HavenoUtils.playChimeSound(); // send notification sendNotification(NotificationMessage.newBuilder() .setType(NotificationType.TRADE_UPDATE) .setTrade(TradeInfo.toTradeInfo(trade).toProtoMessage()) .setTimestamp(System.currentTimeMillis()) .setTitle(title) .setMessage(message) .build()); } public void sendChatNotification(ChatMessage chatMessage) { HavenoUtils.playChimeSound(); sendNotification(NotificationMessage.newBuilder() .setType(NotificationType.CHAT_MESSAGE) .setTimestamp(System.currentTimeMillis()) .setChatMessage(chatMessage.toProtoNetworkEnvelope().getChatMessage()) .build()); } public void sendErrorNotification(String title, String errorMessage) { sendNotification(NotificationMessage.newBuilder() .setType(NotificationType.ERROR) .setTimestamp(System.currentTimeMillis()) .setTitle(title) .setMessage(errorMessage == null ? "null" : errorMessage) .build()); } } ================================================ FILE: core/src/main/java/haveno/core/api/CoreOffersService.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.api; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.crypto.KeyRing; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import static haveno.common.util.MathUtils.roundDoubleToLong; import static haveno.common.util.MathUtils.scaleUpByPowerOf10; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TradeCurrency; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.Price; import haveno.core.monetary.TraditionalMoney; import haveno.core.offer.CreateOfferService; import haveno.core.offer.Offer; import haveno.core.offer.OfferBookService; import haveno.core.offer.OfferDirection; import static haveno.core.offer.OfferDirection.BUY; import haveno.core.offer.OfferFilterService; import haveno.core.offer.OfferFilterService.Result; import haveno.core.offer.OfferPayload; import haveno.core.offer.OfferUtil; import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.PaymentAccount; import haveno.core.proto.persistable.CorePersistenceProtoResolver; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.HavenoUtils; import static haveno.core.payment.PaymentAccountUtil.isPaymentAccountValidForOffer; import haveno.core.user.User; import haveno.core.util.PriceUtil; import static java.lang.String.format; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.Transaction; @Singleton @Slf4j public class CoreOffersService { private static final long WAIT_FOR_EDIT_REMOVAL_MS = 5000; private final Supplier> priceComparator = () -> Comparator.comparing( Offer::getPrice, Comparator.nullsLast(Comparator.naturalOrder()) ); private final Supplier> openOfferPriceComparator = () -> Comparator.comparing( openOffer -> openOffer.getOffer().getPrice(), Comparator.nullsLast(Comparator.naturalOrder()) ); private final Supplier> reversePriceComparator = () -> Comparator.comparing( Offer::getPrice, Comparator.nullsLast(Comparator.naturalOrder()) ).reversed(); private final CoreContext coreContext; private final KeyRing keyRing; // Dependencies on core api services in this package must be kept to an absolute // minimum, but some trading functions require an unlocked wallet's key, so an // exception is made in this case. private final CoreWalletsService coreWalletsService; private final CreateOfferService createOfferService; private final OfferBookService offerBookService; private final OfferFilterService offerFilter; private final OpenOfferManager openOfferManager; private final User user; private final PriceFeedService priceFeedService; private final CorePersistenceProtoResolver corePersistenceProtoResolver; @Inject public CoreOffersService(CoreContext coreContext, KeyRing keyRing, CoreWalletsService coreWalletsService, CreateOfferService createOfferService, OfferBookService offerBookService, OfferFilterService offerFilter, OpenOfferManager openOfferManager, OfferUtil offerUtil, User user, PriceFeedService priceFeedService, CorePersistenceProtoResolver corePersistenceProtoResolver) { this.coreContext = coreContext; this.keyRing = keyRing; this.coreWalletsService = coreWalletsService; this.createOfferService = createOfferService; this.offerBookService = offerBookService; this.offerFilter = offerFilter; this.openOfferManager = openOfferManager; this.user = user; this.priceFeedService = priceFeedService; this.corePersistenceProtoResolver = corePersistenceProtoResolver; } // excludes my offers List getOffers() { List offers = new ArrayList<>(offerBookService.getOffers()).stream() .filter(o -> !o.isMyOffer(keyRing)) .filter(o -> { Result result = offerFilter.canTakeOffer(o, coreContext.isApiUser()); return result.isValid() || result == Result.HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER; }) .collect(Collectors.toList()); return offers; } List getOffers(String direction, String currencyCode) { return getOffers().stream() .filter(o -> offerMatchesDirectionAndCurrency(o, direction, currencyCode)) .sorted(priceComparator(direction)) .collect(Collectors.toList()); } Offer getOffer(String id) { return getOffers().stream() .filter(o -> o.getId().equals(id)) .findAny().orElseThrow(() -> new IllegalStateException(format("offer with id '%s' not found", id))); } List getMyOffers() { return openOfferManager.getOpenOffers().stream() .filter(o -> o.getOffer().isMyOffer(keyRing)) .collect(Collectors.toList()); }; List getMyOffers(String direction, String currencyCode) { return getMyOffers().stream() .filter(o -> offerMatchesDirectionAndCurrency(o.getOffer(), direction, currencyCode)) .sorted(openOfferPriceComparator(direction)) .collect(Collectors.toList()); } OpenOffer getMyOffer(String id) { return openOfferManager.getOpenOffer(id) .filter(open -> open.getOffer().isMyOffer(keyRing)) .orElseThrow(() -> new IllegalStateException(format("openoffer with id '%s' not found", id))); } void postOffer(String currencyCode, String directionAsString, String priceAsString, boolean useMarketBasedPrice, double marketPriceMarginPct, long amountAsLong, long minAmountAsLong, double securityDepositPct, String triggerPriceAsString, boolean reserveExactAmount, String paymentAccountId, boolean isPrivateOffer, boolean buyerAsTakerWithoutDeposit, String extraInfo, String sourceOfferId, Consumer resultHandler, ErrorMessageHandler errorMessageHandler) { coreWalletsService.verifyWalletsAreAvailable(); coreWalletsService.verifyEncryptedWalletIsUnlocked(); // clone offer if sourceOfferId given if (!sourceOfferId.isEmpty()) { cloneOffer(sourceOfferId, currencyCode, priceAsString, useMarketBasedPrice, marketPriceMarginPct, triggerPriceAsString, paymentAccountId, extraInfo, resultHandler, errorMessageHandler); return; } // get payment account PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId); if (paymentAccount == null) throw new IllegalArgumentException(format("Payment account with id %s not found", paymentAccountId)); // get trade currency (default to payment account's single trade currency) if (currencyCode.isEmpty() && paymentAccount.getSingleTradeCurrency() != null) { currencyCode = paymentAccount.getSingleTradeCurrency().getCode(); } // create new offer String upperCaseCurrencyCode = currencyCode.toUpperCase(); String offerId = createOfferService.getRandomOfferId(); OfferDirection direction = OfferDirection.valueOf(directionAsString.toUpperCase()); Price price = priceAsString.isEmpty() ? null : Price.valueOf(upperCaseCurrencyCode, priceStringToLong(priceAsString, upperCaseCurrencyCode)); BigInteger amount = BigInteger.valueOf(amountAsLong); BigInteger minAmount = BigInteger.valueOf(minAmountAsLong); Offer offer = createOfferService.createAndGetOffer(offerId, direction, upperCaseCurrencyCode, amount, minAmount, price, useMarketBasedPrice, marketPriceMarginPct, securityDepositPct, paymentAccount, isPrivateOffer, buyerAsTakerWithoutDeposit, extraInfo); verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount); placeOffer(offer, triggerPriceAsString, true, reserveExactAmount, null, transaction -> resultHandler.accept(offer), errorMessageHandler); } private void cloneOffer(String sourceOfferId, String currencyCode, String priceAsString, boolean useMarketBasedPrice, double marketPriceMarginPct, String triggerPriceAsString, String paymentAccountId, String extraInfo, Consumer resultHandler, ErrorMessageHandler errorMessageHandler) { // get source offer OpenOffer sourceOpenOffer = getMyOffer(sourceOfferId); Offer sourceOffer = sourceOpenOffer.getOffer(); // get payment account if (paymentAccountId.isEmpty()) paymentAccountId = sourceOffer.getOfferPayload().getMakerPaymentAccountId(); PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId); if (paymentAccount == null) throw new IllegalArgumentException(format("Payment account with id %s not found", paymentAccountId)); // get trade currency if (currencyCode.isEmpty()) { if (paymentAccountId.equals(sourceOffer.getOfferPayload().getMakerPaymentAccountId())) { currencyCode = sourceOffer.getOfferPayload().getCurrencyCode(); } else if (paymentAccount.getSingleTradeCurrency() != null) { currencyCode = paymentAccount.getSingleTradeCurrency().getCode(); } } if (currencyCode.isEmpty()) throw new IllegalArgumentException("Must provide currency code"); String upperCaseCurrencyCode = currencyCode.toUpperCase(); // get price (default to source price) Price price = useMarketBasedPrice ? null : priceAsString.isEmpty() ? sourceOffer.isUseMarketBasedPrice() ? null : sourceOffer.getPrice() : Price.parse(upperCaseCurrencyCode, priceAsString); if (price == null) useMarketBasedPrice = true; // get extra info if (extraInfo.isEmpty()) extraInfo = sourceOffer.getOfferPayload().getExtraInfo(); // create cloned offer Offer offer = createOfferService.createClonedOffer(sourceOffer, upperCaseCurrencyCode, price, useMarketBasedPrice, marketPriceMarginPct, paymentAccount, extraInfo); // verify cloned offer verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount); // place offer placeOffer(offer, triggerPriceAsString, true, false, // ignored when cloning sourceOfferId, transaction -> resultHandler.accept(offer), errorMessageHandler); } void editOffer(String offerId, String currencyCode, String priceAsString, boolean useMarketBasedPrice, double marketPriceMarginPct, String triggerPriceAsString, String paymentAccountId, String extraInfo, Consumer resultHandler, ErrorMessageHandler errorMessageHandler) { try { // collect offer info final OpenOffer openOffer = getMyOffer(offerId); final Offer offer = openOffer.getOffer(); final OfferPayload offerPayload = openOffer.getOffer().getOfferPayload(); // cannot edit reserved offer if (openOffer.isReserved()) { throw new IllegalStateException("Cannot edit offer " + offer.getId() + " because it's reserved"); } // get payment account if (paymentAccountId.isEmpty()) paymentAccountId = offer.getOfferPayload().getMakerPaymentAccountId(); PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId); if (paymentAccount == null) throw new IllegalArgumentException(format("payment account with id %s not found", paymentAccountId)); // TODO: invoke error handler for this and other offer methods // get trade currency if (currencyCode.isEmpty()) { if (paymentAccountId.equals(offer.getOfferPayload().getMakerPaymentAccountId())) { currencyCode = offer.getOfferPayload().getCurrencyCode(); } else if (paymentAccount.getSingleTradeCurrency() != null) { currencyCode = paymentAccount.getSingleTradeCurrency().getCode(); } } if (currencyCode.isEmpty()) throw new IllegalArgumentException("Must provide currency code"); String upperCaseCurrencyCode = currencyCode.toUpperCase(); // start edit offer OpenOffer.State initialState = openOffer.getState(); openOfferManager.editOpenOfferStart(openOffer, () -> { try { // wait for remove offer to propagate // TODO: if offer edit is published too quickly, the remove message can be received after the add message, in which case the offer will be offline until the next offer refresh HavenoUtils.waitFor(WAIT_FOR_EDIT_REMOVAL_MS); // create edited offer Price price = priceAsString.isEmpty() ? null : Price.valueOf(upperCaseCurrencyCode, priceStringToLong(priceAsString, upperCaseCurrencyCode)); final OfferPayload newOfferPayload = createOfferService.createAndGetOffer(offerId, offer.getDirection(), upperCaseCurrencyCode, offer.getAmount(), offer.getMinAmount(), price, useMarketBasedPrice, marketPriceMarginPct, offerPayload.getBuyerSecurityDepositPct(), paymentAccount, offerPayload.isPrivateOffer(), offer.hasBuyerAsTakerWithoutDeposit(), extraInfo).getOfferPayload(); Offer editedOffer = getEditedOffer(openOffer, newOfferPayload); // publish edited offer long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, upperCaseCurrencyCode); openOfferManager.editOpenOfferPublish(editedOffer, triggerPriceAsLong, initialState, () -> { Offer updatedEditedOffer = openOfferManager.getOpenOffer(offerId).get().getOffer(); // get latest offer resultHandler.accept(updatedEditedOffer); }, (errorMsg) -> { errorMessageHandler.handleErrorMessage(errorMsg); }); } catch (Exception e) { errorMessageHandler.handleErrorMessage(format("Error editing offer %s: %s", offerId, e.getMessage())); return; } }, errorMessageHandler); } catch (Exception e) { errorMessageHandler.handleErrorMessage(format("Error editing offer %s: %s", offerId, e.getMessage())); return; } } private PaymentAccount getPreselectedPaymentAccount(PaymentAccount paymentAccount, String currencyCode) { if (paymentAccount == null) throw new IllegalArgumentException("payment account cannot be null"); if (currencyCode == null || currencyCode.isEmpty()) throw new IllegalArgumentException("currency code cannot be null or empty"); Optional optionalTradeCurrency = CurrencyUtil.getTradeCurrency(currencyCode); if (!optionalTradeCurrency.isPresent()) throw new IllegalArgumentException(format("cannot get trade currency for currency code %s", currencyCode)); TradeCurrency selectedTradeCurrency = optionalTradeCurrency.get(); PaymentAccount preselectedPaymentAccount = PaymentAccount.fromProto(paymentAccount.toProtoMessage(), corePersistenceProtoResolver); if (paymentAccount.getSingleTradeCurrency() != null) preselectedPaymentAccount.setSingleTradeCurrency(selectedTradeCurrency); else preselectedPaymentAccount.setSelectedTradeCurrency(selectedTradeCurrency); return preselectedPaymentAccount; } public Offer getEditedOffer(OpenOffer openOffer, OfferPayload newOfferPayload) { // editedPayload is a merge of the original offerPayload and newOfferPayload // fields which are editable are merged in from newOfferPayload (such as payment account details) // fields which cannot change (most importantly XMR amount) are sourced from the original offerPayload final OfferPayload offerPayload = openOffer.getOffer().getOfferPayload(); // maker fee cannot change double newMakerFee = HavenoUtils.getMakerFeePct(newOfferPayload.getCurrencyCode(), newOfferPayload.isBuyerAsTakerWithoutDeposit()); if (openOffer.getOffer().getOfferPayload().getMakerFeePct() != newMakerFee) { throw new IllegalArgumentException("Cannot edit offer with different maker fee, original maker fee: " + openOffer.getOffer().getOfferPayload().getMakerFeePct() + ", new maker fee: " + newMakerFee); } final OfferPayload editedPayload = new OfferPayload(offerPayload.getId(), offerPayload.getDate(), offerPayload.getOwnerNodeAddress(), offerPayload.getPubKeyRing(), offerPayload.getDirection(), newOfferPayload.getPrice(), newOfferPayload.getMarketPriceMarginPct(), newOfferPayload.isUseMarketBasedPrice(), offerPayload.getAmount(), offerPayload.getMinAmount(), offerPayload.getMakerFeePct(), HavenoUtils.getTakerFeePct(newOfferPayload.getCurrencyCode(), newOfferPayload.isBuyerAsTakerWithoutDeposit()), offerPayload.getPenaltyFeePct(), offerPayload.getBuyerSecurityDepositPct(), offerPayload.getSellerSecurityDepositPct(), newOfferPayload.getBaseCurrencyCode(), newOfferPayload.getCounterCurrencyCode(), newOfferPayload.getPaymentMethodId(), newOfferPayload.getMakerPaymentAccountId(), newOfferPayload.getCountryCode(), newOfferPayload.getAcceptedCountryCodes(), newOfferPayload.getBankId(), newOfferPayload.getAcceptedBankIds(), offerPayload.getVersionNr(), offerPayload.getBlockHeightAtOfferCreation(), offerPayload.getMaxTradeLimit(), offerPayload.getMaxTradePeriod(), offerPayload.isUseAutoClose(), offerPayload.isUseReOpenAfterAutoClose(), offerPayload.getLowerClosePrice(), offerPayload.getUpperClosePrice(), offerPayload.isPrivateOffer(), offerPayload.getChallengeHash(), offerPayload.getExtraDataMap(), offerPayload.getProtocolVersion(), offerPayload.getArbitratorSigner(), offerPayload.getArbitratorSignature(), offerPayload.getReserveTxKeyImages(), newOfferPayload.getExtraInfo()); Offer editedOffer = new Offer(editedPayload); editedOffer.setPriceFeedService(priceFeedService); editedOffer.setState(Offer.State.AVAILABLE); return editedOffer; } void deactivateOffer(String offerId, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { openOfferManager.deactivateOpenOffer(getMyOffer(offerId), false, resultHandler, errorMessageHandler); } void activateOffer(String offerId, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { openOfferManager.activateOpenOffer(getMyOffer(offerId), resultHandler, errorMessageHandler); } void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { Offer offer = getMyOffer(id).getOffer(); openOfferManager.removeOffer(offer, resultHandler, errorMessageHandler); } // -------------------------- PRIVATE HELPERS ----------------------------- private void verifyPaymentAccountIsValidForNewOffer(Offer offer, PaymentAccount paymentAccount) { if (!isPaymentAccountValidForOffer(offer, paymentAccount)) { String error = format("cannot create %s offer with payment account %s", offer.getOfferPayload().getCounterCurrencyCode(), paymentAccount.getId()); throw new IllegalStateException(error); } } private void placeOffer(Offer offer, String triggerPriceAsString, boolean useSavingsWallet, boolean reserveExactAmount, String sourceOfferId, Consumer resultHandler, ErrorMessageHandler errorMessageHandler) { long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, offer.getCounterCurrencyCode()); openOfferManager.placeOffer(offer, useSavingsWallet, triggerPriceAsLong, reserveExactAmount, true, sourceOfferId, resultHandler::accept, errorMessageHandler); } private boolean offerMatchesDirectionAndCurrency(Offer offer, String direction, String currencyCode) { if ("".equals(direction)) direction = null; if ("".equals(currencyCode)) currencyCode = null; var offerOfWantedDirection = direction == null || offer.getDirection().name().equalsIgnoreCase(direction); var offerInWantedCurrency = currencyCode == null || offer.getCounterCurrencyCode().equalsIgnoreCase(currencyCode); return offerOfWantedDirection && offerInWantedCurrency; } private Comparator priceComparator(String direction) { // A buyer probably wants to see sell orders in price ascending order. // A seller probably wants to see buy orders in price descending order. return direction.equalsIgnoreCase(BUY.name()) ? reversePriceComparator.get() : priceComparator.get(); } private Comparator openOfferPriceComparator(String direction) { // A buyer probably wants to see sell orders in price ascending order. // A seller probably wants to see buy orders in price descending order. return direction.equalsIgnoreCase(OfferDirection.BUY.name()) ? openOfferPriceComparator.get().reversed() : openOfferPriceComparator.get(); } private long priceStringToLong(String priceAsString, String currencyCode) { int precision = CurrencyUtil.isTraditionalCurrency(currencyCode) ? TraditionalMoney.SMALLEST_UNIT_EXPONENT : CryptoMoney.SMALLEST_UNIT_EXPONENT; double priceAsDouble = new BigDecimal(priceAsString).doubleValue(); double scaled = scaleUpByPowerOf10(priceAsDouble, precision); return roundDoubleToLong(scaled); } } ================================================ FILE: core/src/main/java/haveno/core/api/CorePaymentAccountsService.java ================================================ /* * 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 . */ package haveno.core.api; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.asset.Asset; import haveno.asset.AssetRegistry; import static haveno.common.config.Config.baseCurrencyNetwork; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.api.model.PaymentAccountForm; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.CurrencyUtil; import static haveno.core.locale.CurrencyUtil.findAsset; import static haveno.core.locale.CurrencyUtil.getCryptoCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.AssetAccount; import haveno.core.payment.CryptoCurrencyAccount; import haveno.core.payment.InstantCryptoCurrencyAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccountFactory; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.validation.InteracETransferValidator; import haveno.core.trade.HavenoUtils; import haveno.core.user.User; import java.io.File; import static java.lang.String.format; import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @Singleton @Slf4j public class CorePaymentAccountsService { private final CoreAccountService accountService; private final AccountAgeWitnessService accountAgeWitnessService; private final User user; public final InteracETransferValidator interacETransferValidator; @Inject public CorePaymentAccountsService(CoreAccountService accountService, AccountAgeWitnessService accountAgeWitnessService, User user, InteracETransferValidator interacETransferValidator) { this.accountService = accountService; this.accountAgeWitnessService = accountAgeWitnessService; this.user = user; this.interacETransferValidator = interacETransferValidator; HavenoUtils.corePaymentAccountService = this; } PaymentAccount createPaymentAccount(PaymentAccountForm form) { validateFormFields(form); PaymentAccount paymentAccount = form.toPaymentAccount(); setSelectedTradeCurrency(paymentAccount); // TODO: selected trade currency is function of offer, not payment account payload verifyPaymentAccountHasRequiredFields(paymentAccount); if (paymentAccount instanceof CryptoCurrencyAccount) { CryptoCurrencyAccount cryptoAccount = (CryptoCurrencyAccount) paymentAccount; verifyCryptoCurrencyAddress(cryptoAccount.getSingleTradeCurrency().getCode(), cryptoAccount.getAddress()); } user.addPaymentAccountIfNotExists(paymentAccount); accountAgeWitnessService.publishMyAccountAgeWitness(paymentAccount.getPaymentAccountPayload()); log.info("Saved payment account with id {} and payment method {}.", paymentAccount.getId(), paymentAccount.getPaymentAccountPayload().getPaymentMethodId()); return paymentAccount; } private static void setSelectedTradeCurrency(PaymentAccount paymentAccount) { TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency(); List tradeCurrencies = paymentAccount.getTradeCurrencies(); if (singleTradeCurrency != null) { paymentAccount.setSelectedTradeCurrency(singleTradeCurrency); } else if (tradeCurrencies != null && !tradeCurrencies.isEmpty()) { if (tradeCurrencies.contains(CurrencyUtil.getDefaultTradeCurrency())) { paymentAccount.setSelectedTradeCurrency(CurrencyUtil.getDefaultTradeCurrency()); } else { paymentAccount.setSelectedTradeCurrency(tradeCurrencies.get(0)); } } } PaymentAccount getPaymentAccount(String paymentAccountId) { return user.getPaymentAccount(paymentAccountId); } Set getPaymentAccounts() { return user.getPaymentAccounts(); } List getPaymentMethods() { return PaymentMethod.getPaymentMethods().stream() .sorted(Comparator.comparing(PaymentMethod::getId)) .collect(Collectors.toList()); } PaymentAccountForm getPaymentAccountForm(String paymentMethodId) { return PaymentAccountForm.getForm(paymentMethodId.toUpperCase()); } PaymentAccountForm getPaymentAccountForm(PaymentAccount paymentAccount) { return paymentAccount.toForm(); } String getPaymentAccountFormAsString(String paymentMethodId) { File jsonForm = getPaymentAccountFormFile(paymentMethodId); jsonForm.deleteOnExit(); // If just asking for a string, delete the form file. return PaymentAccountForm.toJsonString(jsonForm); } File getPaymentAccountFormFile(String paymentMethodId) { return PaymentAccountForm.getPaymentAccountForm(paymentMethodId); } // Crypto Currency Accounts synchronized PaymentAccount createCryptoCurrencyPaymentAccount(String accountName, String currencyCode, String address, boolean tradeInstant) { accountService.checkAccountOpen(); verifyAccountNameUnique(accountName); verifyCryptoCurrencyAddress(currencyCode.toUpperCase(), address); AssetAccount cryptoCurrencyAccount = tradeInstant ? (InstantCryptoCurrencyAccount) PaymentAccountFactory.getPaymentAccount(PaymentMethod.BLOCK_CHAINS_INSTANT) : (CryptoCurrencyAccount) PaymentAccountFactory.getPaymentAccount(PaymentMethod.BLOCK_CHAINS); cryptoCurrencyAccount.init(); cryptoCurrencyAccount.setAccountName(accountName); cryptoCurrencyAccount.setAddress(address); Optional cryptoCurrency = getCryptoCurrency(currencyCode.toUpperCase()); cryptoCurrency.ifPresent(cryptoCurrencyAccount::setSingleTradeCurrency); user.addPaymentAccount(cryptoCurrencyAccount); log.info("Saved crypto payment account with id {} and payment method {}.", cryptoCurrencyAccount.getId(), cryptoCurrencyAccount.getPaymentAccountPayload().getPaymentMethodId()); return cryptoCurrencyAccount; } synchronized void deletePaymentAccount(String paymentAccountId) { accountService.checkAccountOpen(); PaymentAccount paymentAccount = getPaymentAccount(paymentAccountId); if (paymentAccount == null) throw new IllegalArgumentException(format("Payment account with id %s not found", paymentAccountId)); user.removePaymentAccount(paymentAccount); log.info("Deleted payment account with id {} and payment method {}.", paymentAccount.getId(), paymentAccount.getPaymentAccountPayload().getPaymentMethodId()); } // TODO Support all alt coin payment methods supported by UI. // The getCryptoCurrencyPaymentMethods method below will be // callable from the CLI when more are supported. List getCryptoCurrencyPaymentMethods() { return PaymentMethod.getPaymentMethods().stream() .filter(PaymentMethod::isCrypto) .sorted(Comparator.comparing(PaymentMethod::getId)) .collect(Collectors.toList()); } private void validateFormFields(PaymentAccountForm form) { for (PaymentAccountFormField field : form.getFields()) { validateFormField(form, field.getId(), field.getValue()); } } void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) { // deserialize the payment account for context PaymentAccount paymentAccount = PaymentAccount.fromJson(form.toPaymentAccountJsonString()); // validate form field paymentAccount.validateFormField(form, fieldId, value); } private void verifyAccountNameUnique(String accountName) { if (getPaymentAccounts().stream().anyMatch(e -> e.getAccountName() != null && e.getAccountName().equals(accountName))) throw new IllegalArgumentException(format("Account '%s' is already taken", accountName)); } private void verifyCryptoCurrencyAddress(String cryptoCurrencyCode, String address) { Asset asset = getAsset(cryptoCurrencyCode); if (!asset.validateAddress(address).isValid()) throw new IllegalArgumentException( format("%s is not a valid %s address", address, cryptoCurrencyCode.toLowerCase())); } private Asset getAsset(String cryptoCurrencyCode) { return findAsset(new AssetRegistry(), cryptoCurrencyCode, baseCurrencyNetwork()) .orElseThrow(() -> new IllegalStateException( format("crypto currency with code '%s' not found", cryptoCurrencyCode.toLowerCase()))); } private void verifyPaymentAccountHasRequiredFields(PaymentAccount paymentAccount) { if (!paymentAccount.hasMultipleCurrencies() && paymentAccount.getSingleTradeCurrency() == null) throw new IllegalArgumentException(format("no trade currency defined for %s payment account", paymentAccount.getPaymentMethod().getDisplayString().toLowerCase())); } } ================================================ FILE: core/src/main/java/haveno/core/api/CorePriceService.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.api; import com.google.common.math.LongMath; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.core.api.model.MarketDepthInfo; import haveno.core.api.model.MarketPriceInfo; import haveno.core.locale.CurrencyUtil; import haveno.core.monetary.Price; import haveno.core.offer.Offer; import haveno.core.offer.OfferBookService; import haveno.core.offer.OfferDirection; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.HavenoUtils; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @Singleton @Slf4j class CorePriceService { private final PriceFeedService priceFeedService; private final OfferBookService offerBookService; @Inject public CorePriceService(PriceFeedService priceFeedService, OfferBookService offerBookService) { this.priceFeedService = priceFeedService; this.offerBookService = offerBookService; } /** * @return Price per 1 XMR in the given currency (traditional or crypto) */ public double getMarketPrice(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException, IllegalArgumentException { var marketPrice = priceFeedService.requestAllPrices().get(CurrencyUtil.getCurrencyCodeBase(currencyCode)); if (marketPrice == null) { throw new IllegalArgumentException("Currency not found: " + currencyCode); // TODO: do not use IllegalArgumentException as message sent to client, return undefined? } else if (!marketPrice.isExternallyProvidedPrice()) { throw new IllegalArgumentException("Price is not available externally: " + currencyCode); // TODO: return more complex Price type including price double and isExternal boolean } return marketPrice.getPrice(); } /** * @return Price per 1 XMR in all supported currencies (traditional & crypto) */ public List getMarketPrices() throws ExecutionException, InterruptedException, TimeoutException { return priceFeedService.requestAllPrices().values().stream() .map(marketPrice -> { return new MarketPriceInfo(marketPrice.getCurrencyCode(), marketPrice.getPrice()); }) .collect(Collectors.toList()); } /** * @return Data for market depth chart */ public MarketDepthInfo getMarketDepth(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException, IllegalArgumentException { if (priceFeedService.requestAllPrices().get(currencyCode.toUpperCase()) == null) throw new IllegalArgumentException("Currency not found: " + currencyCode) ; // Offer price can be null (if price feed unavailable), thus a null-tolerant comparator is used. Comparator offerPriceComparator = Comparator.comparing(Offer::getPrice, Comparator.nullsLast(Comparator.naturalOrder())); // Offer amounts are used for the secondary sort. They are sorted from high to low. Comparator offerAmountComparator = Comparator.comparing(Offer::getAmount).reversed(); var buyOfferSortComparator = offerPriceComparator.reversed() // Buy offers, as opposed to sell offers, are primarily sorted from high price to low. .thenComparing(offerAmountComparator); var sellOfferSortComparator = offerPriceComparator .thenComparing(offerAmountComparator); List buyOffers = offerBookService.getOffersByCurrency(OfferDirection.BUY.name(), currencyCode).stream().sorted(buyOfferSortComparator).collect(Collectors.toList()); List sellOffers = offerBookService.getOffersByCurrency(OfferDirection.SELL.name(), currencyCode).stream().sorted(sellOfferSortComparator).collect(Collectors.toList()); // Create buyer hashmap {key:price, value:count}, uses LinkedHashMap to maintain insertion order double accumulatedAmount = 0; LinkedHashMap buyTM = new LinkedHashMap(); for(Offer offer: buyOffers) { Price price = offer.getPrice(); if (price != null) { double amount = (double) offer.getAmount().longValueExact() / LongMath.pow(10, HavenoUtils.XMR_SMALLEST_UNIT_EXPONENT); accumulatedAmount += amount; double priceAsDouble = (double) price.getValue() / LongMath.pow(10, price.smallestUnitExponent()); buyTM.put(priceAsDouble, accumulatedAmount); } }; // Create seller hashmap {key:price, value:count}, uses TreeMap to sort by key (asc) accumulatedAmount = 0; LinkedHashMap sellTM = new LinkedHashMap(); for(Offer offer: sellOffers){ Price price = offer.getPrice(); if (price != null) { double amount = (double) offer.getAmount().longValueExact() / LongMath.pow(10, HavenoUtils.XMR_SMALLEST_UNIT_EXPONENT); accumulatedAmount += amount; double priceAsDouble = (double) price.getValue() / LongMath.pow(10, price.smallestUnitExponent()); sellTM.put(priceAsDouble, accumulatedAmount); } }; // Make array of buyPrices and buyDepth Double[] buyDepth = buyTM.values().toArray(new Double[buyTM.size()]); Double[] buyPrices = buyTM.keySet().toArray(new Double[buyTM.size()]); // Make array of sellPrices and sellDepth Double[] sellDepth = sellTM.values().toArray(new Double[sellTM.size()]); Double[] sellPrices = sellTM.keySet().toArray(new Double[sellTM.size()]); return new MarketDepthInfo(currencyCode, buyPrices, buyDepth, sellPrices, sellDepth); } } ================================================ FILE: core/src/main/java/haveno/core/api/CoreTradesService.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.api; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OfferUtil; import haveno.core.offer.takeoffer.TakeOfferModel; import haveno.core.payment.payload.PaymentMethod; import haveno.core.support.messages.ChatMessage; import haveno.core.support.traderchat.TradeChatSession; import haveno.core.support.traderchat.TraderChatManager; import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.trade.TradeUtil; import haveno.core.trade.protocol.BuyerProtocol; import haveno.core.trade.protocol.SellerProtocol; import haveno.core.user.User; import haveno.core.util.coin.CoinUtil; import haveno.core.xmr.wallet.BtcWalletService; import static java.lang.String.format; import java.math.BigInteger; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.function.Consumer; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; @Singleton @Slf4j class CoreTradesService { private final CoreContext coreContext; // Dependencies on core api services in this package must be kept to an absolute // minimum, but some trading functions require an unlocked wallet's key, so an // exception is made in this case. private final CoreWalletsService coreWalletsService; private final BtcWalletService btcWalletService; private final ClosedTradableManager closedTradableManager; private final TakeOfferModel takeOfferModel; private final TradeManager tradeManager; private final TraderChatManager traderChatManager; private final OfferUtil offerUtil; private final User user; @Inject public CoreTradesService(CoreContext coreContext, CoreWalletsService coreWalletsService, BtcWalletService btcWalletService, ClosedTradableManager closedTradableManager, TakeOfferModel takeOfferModel, TradeManager tradeManager, TraderChatManager traderChatManager, TradeUtil tradeUtil, OfferUtil offerUtil, User user) { this.coreContext = coreContext; this.coreWalletsService = coreWalletsService; this.btcWalletService = btcWalletService; this.closedTradableManager = closedTradableManager; this.takeOfferModel = takeOfferModel; this.tradeManager = tradeManager; this.traderChatManager = traderChatManager; this.offerUtil = offerUtil; this.user = user; } void takeOffer(Offer offer, String paymentAccountId, long tradeAmountAsLong, Consumer resultHandler, ErrorMessageHandler errorMessageHandler) { try { coreWalletsService.verifyWalletsAreAvailable(); coreWalletsService.verifyEncryptedWalletIsUnlocked(); var paymentAccount = user.getPaymentAccount(paymentAccountId); if (paymentAccount == null) throw new IllegalArgumentException(format("payment account with id '%s' not found", paymentAccountId)); var useSavingsWallet = true; // default to offer amount BigInteger tradeAmount = tradeAmountAsLong == 0 ? offer.getAmount() : BigInteger.valueOf(tradeAmountAsLong); // validate trade amount if (tradeAmount.compareTo(offer.getAmount()) > 0) throw new RuntimeException("Trade amount exceeds offer amount"); if (tradeAmount.compareTo(offer.getMinAmount()) < 0) throw new RuntimeException("Trade amount is less than minimum offer amount"); // apply rounding (based on TakeOfferViewModel) String currencyCode = offer.getCounterCurrencyCode(); OfferDirection direction = offer.getOfferPayload().getDirection(); BigInteger maxAmount = offer.getAmount().min(offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, offer.hasBuyerAsTakerWithoutDeposit())); if (offer.getPrice() != null) { if (PaymentMethod.isRoundedForAtmCash(paymentAccount.getPaymentMethod().getId())) { tradeAmount = CoinUtil.getRoundedAtmCashAmount(tradeAmount, offer.getPrice(), offer.getMinAmount(), maxAmount); } else if (offer.isTraditionalOffer() && offer.isRange()) { tradeAmount = CoinUtil.getRoundedAmount(tradeAmount, offer.getPrice(), offer.getMinAmount(), maxAmount, offer.getCounterCurrencyCode(), offer.getPaymentMethodId()); } } // synchronize access to take offer model // TODO (woodser): to avoid synchronizing, don't use stateful model BigInteger fundsNeededForTrade; synchronized (takeOfferModel) { takeOfferModel.initModel(offer, paymentAccount, tradeAmount, useSavingsWallet); fundsNeededForTrade = takeOfferModel.getFundsNeededForTrade(); log.debug("Initiating take {} offer, {}", offer.isBuyOffer() ? "buy" : "sell", takeOfferModel); } // take offer tradeManager.onTakeOffer(tradeAmount, fundsNeededForTrade, offer, paymentAccountId, useSavingsWallet, coreContext.isApiUser(), resultHandler::accept, errorMessageHandler ); } catch (Exception e) { log.error(ExceptionUtils.getStackTrace(e)); errorMessageHandler.handleErrorMessage(e.getMessage()); } } void confirmPaymentSent(String tradeId, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { var trade = getTrade(tradeId); if (isFollowingBuyerProtocol(trade)) { var tradeProtocol = tradeManager.getTradeProtocol(trade); ((BuyerProtocol) tradeProtocol).onPaymentSent(resultHandler, errorMessageHandler); } else { throw new IllegalStateException("you are the seller and not sending payment"); } } void confirmPaymentReceived(String tradeId, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { var trade = getTrade(tradeId); if (isFollowingBuyerProtocol(trade)) { throw new IllegalStateException("you are the buyer, and not receiving payment"); } else { var tradeProtocol = tradeManager.getTradeProtocol(trade); ((SellerProtocol) tradeProtocol).onPaymentReceived(resultHandler, errorMessageHandler); } } void closeTrade(String tradeId) { coreWalletsService.verifyWalletsAreAvailable(); coreWalletsService.verifyEncryptedWalletIsUnlocked(); verifyTradeIsNotClosed(tradeId); var trade = getOpenTrade(tradeId).orElseThrow(() -> new IllegalArgumentException(format("trade with id '%s' not found", tradeId))); tradeManager.onTradeCompleted(trade); } String getTradeRole(String tradeId) { coreWalletsService.verifyWalletsAreAvailable(); coreWalletsService.verifyEncryptedWalletIsUnlocked(); return TradeUtil.getRole(getTrade(tradeId)); } Trade getTrade(String tradeId) { coreWalletsService.verifyWalletsAreAvailable(); coreWalletsService.verifyEncryptedWalletIsUnlocked(); return getOpenTrade(tradeId).orElseGet(() -> getClosedTrade(tradeId).orElseThrow(() -> new IllegalArgumentException(format("trade with id '%s' not found", tradeId)) )); } private Optional getOpenTrade(String tradeId) { return tradeManager.getOpenTrade(tradeId); } private Optional getClosedTrade(String tradeId) { return closedTradableManager.getTradeById(tradeId); } List getTrades() { coreWalletsService.verifyWalletsAreAvailable(); coreWalletsService.verifyEncryptedWalletIsUnlocked(); List trades = new ArrayList(tradeManager.getOpenTrades()); trades.addAll(closedTradableManager.getClosedTrades()); return trades; } List getChatMessages(String tradeId) { Trade trade; var tradeOptional = tradeManager.getOpenTrade(tradeId); if (tradeOptional.isPresent()) trade = tradeOptional.get(); else throw new IllegalStateException(format("trade with id '%s' not found", tradeId)); boolean isMaker = tradeManager.isMyOffer(trade.getOffer()); TradeChatSession tradeChatSession = new TradeChatSession(trade, !isMaker); return tradeChatSession.getObservableChatMessageList(); } void sendChatMessage(String tradeId, String message) { Trade trade; var tradeOptional = tradeManager.getOpenTrade(tradeId); if (tradeOptional.isPresent()) trade = tradeOptional.get(); else throw new IllegalStateException(format("trade with id '%s' not found", tradeId)); boolean isMaker = tradeManager.isMyOffer(trade.getOffer()); TradeChatSession tradeChatSession = new TradeChatSession(trade, !isMaker); ChatMessage chatMessage = new ChatMessage( traderChatManager.getSupportType(), tradeChatSession.getTradeId(), tradeChatSession.getClientId(), tradeChatSession.isClient(), message, traderChatManager.getMyAddress()); traderChatManager.addAndPersistChatMessage(chatMessage); traderChatManager.sendChatMessage(chatMessage); } private boolean isFollowingBuyerProtocol(Trade trade) { return tradeManager.getTradeProtocol(trade) instanceof BuyerProtocol; } // Throws a RuntimeException trade is already closed. private void verifyTradeIsNotClosed(String tradeId) { if (getClosedTrade(tradeId).isPresent()) throw new IllegalArgumentException(format("trade '%s' is already closed", tradeId)); } } ================================================ FILE: core/src/main/java/haveno/core/api/CoreWalletsService.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.api; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.inject.name.Named; import haveno.common.Timer; import haveno.common.UserThread; import haveno.core.api.model.AddressBalanceInfo; import haveno.core.api.model.BalancesInfo; import haveno.core.api.model.BtcBalanceInfo; import haveno.core.api.model.XmrBalanceInfo; import haveno.core.app.AppStartupState; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import static haveno.core.util.ParsingUtils.parseToCoin; import haveno.core.util.coin.CoinFormatter; import haveno.core.xmr.Balances; import haveno.core.xmr.model.AddressEntry; import haveno.core.xmr.setup.WalletsSetup; import haveno.core.xmr.wallet.BtcWalletService; import static haveno.core.xmr.wallet.Restrictions.getMinNonDustOutput; import haveno.core.xmr.wallet.WalletsManager; import haveno.core.xmr.wallet.XmrWalletService; import static java.lang.String.format; import java.util.List; import java.util.Optional; import static java.util.concurrent.TimeUnit.SECONDS; import java.util.function.Function; import java.util.stream.Collectors; import javax.annotation.Nullable; import lombok.extern.slf4j.Slf4j; import monero.wallet.model.MoneroDestination; import monero.wallet.model.MoneroTxWallet; import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionConfidence; import org.bitcoinj.crypto.KeyCrypterScrypt; import org.bouncycastle.crypto.params.KeyParameter; @Singleton @Slf4j class CoreWalletsService { private final AppStartupState appStartupState; private final CoreAccountService accountService; private final CoreContext coreContext; private final Balances balances; private final WalletsManager walletsManager; private final WalletsSetup walletsSetup; private final BtcWalletService btcWalletService; private final XmrWalletService xmrWalletService; private final CoinFormatter btcFormatter; @Nullable private Timer lockTimer; @Nullable private KeyParameter tempAesKey; @Inject public CoreWalletsService(AppStartupState appStartupState, CoreContext coreContext, CoreAccountService accountService, Balances balances, WalletsManager walletsManager, WalletsSetup walletsSetup, BtcWalletService btcWalletService, XmrWalletService xmrWalletService, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, Preferences preferences) { this.appStartupState = appStartupState; this.coreContext = coreContext; this.accountService = accountService; this.balances = balances; this.walletsManager = walletsManager; this.walletsSetup = walletsSetup; this.btcWalletService = btcWalletService; this.xmrWalletService = xmrWalletService; this.btcFormatter = btcFormatter; } @Nullable KeyParameter getKey() { verifyEncryptedWalletIsUnlocked(); return tempAesKey; } NetworkParameters getNetworkParameters() { return btcWalletService.getWallet().getContext().getParams(); } BalancesInfo getBalances(String currencyCode) { accountService.checkAccountOpen(); verifyWalletCurrencyCodeIsValid(currencyCode); verifyWalletsAreAvailable(); verifyEncryptedWalletIsUnlocked(); switch (currencyCode.trim().toUpperCase()) { case "": case "XMR": return new BalancesInfo(BtcBalanceInfo.EMPTY, getXmrBalances()); default: throw new IllegalStateException("Unsupported currency code: " + currencyCode.trim().toUpperCase()); } } String getXmrSeed() { return xmrWalletService.getWallet().getSeed(); } String getXmrPrimaryAddress() { return xmrWalletService.getWallet().getPrimaryAddress(); } String getXmrNewSubaddress() { accountService.checkAccountOpen(); return xmrWalletService.getNewAddressEntry().getAddressString(); } List getXmrTxs() { accountService.checkAccountOpen(); return xmrWalletService.getTxs(); } MoneroTxWallet createXmrTx(List destinations) { accountService.checkAccountOpen(); verifyWalletsAreAvailable(); verifyEncryptedWalletIsUnlocked(); try { return xmrWalletService.createTx(destinations); } catch (Exception ex) { log.error("", ex); throw new IllegalStateException(ex); } } List createXmrSweepTxs(String address) { accountService.checkAccountOpen(); verifyWalletsAreAvailable(); verifyEncryptedWalletIsUnlocked(); try { return xmrWalletService.createSweepTxs(address); } catch (Exception ex) { log.error("", ex); throw new IllegalStateException(ex); } } List relayXmrTxs(List metadatas) { accountService.checkAccountOpen(); verifyWalletsAreAvailable(); verifyEncryptedWalletIsUnlocked(); try { return xmrWalletService.relayTxs(metadatas); } catch (Exception ex) { log.error("", ex); throw new IllegalStateException(ex); } } long getAddressBalance(String addressString) { Address address = getAddressEntry(addressString).getAddress(); return btcWalletService.getBalanceForAddress(address).value; } AddressBalanceInfo getAddressBalanceInfo(String addressString) { var satoshiBalance = getAddressBalance(addressString); var numConfirmations = getNumConfirmationsForMostRecentTransaction(addressString); Address address = getAddressEntry(addressString).getAddress(); return new AddressBalanceInfo(addressString, satoshiBalance, numConfirmations, btcWalletService.isAddressUnused(address)); } List getFundingAddresses() { verifyWalletsAreAvailable(); verifyEncryptedWalletIsUnlocked(); // Create a new unused funding address if none exists. boolean unusedAddressExists = btcWalletService.getAvailableAddressEntries() .stream() .anyMatch(a -> btcWalletService.isAddressUnused(a.getAddress())); if (!unusedAddressExists) btcWalletService.getFreshAddressEntry(); List addressStrings = btcWalletService .getAvailableAddressEntries() .stream() .map(AddressEntry::getAddressString) .collect(Collectors.toList()); // getAddressBalance is memoized, because we'll map it over addresses twice. // To get the balances, we'll be using .getUnchecked, because we know that // this::getAddressBalance cannot return null. var balances = memoize(this::getAddressBalance); boolean noAddressHasZeroBalance = addressStrings.stream() .allMatch(addressString -> balances.getUnchecked(addressString) != 0); if (noAddressHasZeroBalance) { var newZeroBalanceAddress = btcWalletService.getFreshAddressEntry(); addressStrings.add(newZeroBalanceAddress.getAddressString()); } return addressStrings.stream().map(address -> new AddressBalanceInfo(address, balances.getUnchecked(address), getNumConfirmationsForMostRecentTransaction(address), btcWalletService.isAddressUnused(getAddressEntry(address).getAddress()))) .collect(Collectors.toList()); } Transaction getTransaction(String txId) { if (txId.length() != 64) throw new IllegalArgumentException(format("%s is not a transaction id", txId)); try { Transaction tx = btcWalletService.getTransaction(txId); if (tx == null) throw new IllegalArgumentException(format("tx with id %s not found", txId)); else return tx; } catch (IllegalArgumentException ex) { log.error("", ex); throw new IllegalArgumentException( format("could not get transaction with id %s%ncause: %s", txId, ex.getMessage().toLowerCase())); } } int getNumConfirmationsForMostRecentTransaction(String addressString) { Address address = getAddressEntry(addressString).getAddress(); TransactionConfidence confidence = btcWalletService.getConfidenceForAddress(address); return confidence == null ? 0 : confidence.getDepthInBlocks(); } void setWalletPassword(String password, String newPassword) { verifyWalletsAreAvailable(); KeyCrypterScrypt keyCrypterScrypt = getKeyCrypterScrypt(); if (newPassword != null && !newPassword.isEmpty()) { // TODO Validate new password before replacing old password. if (!walletsManager.areWalletsEncrypted()) throw new IllegalStateException("wallet is not encrypted with a password"); KeyParameter aesKey = keyCrypterScrypt.deriveKey(password); if (!walletsManager.checkAESKey(aesKey)) throw new IllegalStateException("incorrect old password"); walletsManager.decryptWallets(aesKey); aesKey = keyCrypterScrypt.deriveKey(newPassword); walletsManager.encryptWallets(keyCrypterScrypt, aesKey); walletsManager.backupWallets(); return; } if (walletsManager.areWalletsEncrypted()) throw new IllegalStateException("wallet is encrypted with a password"); // TODO Validate new password. KeyParameter aesKey = keyCrypterScrypt.deriveKey(password); walletsManager.encryptWallets(keyCrypterScrypt, aesKey); walletsManager.backupWallets(); } void lockWallet() { if (!walletsManager.areWalletsEncrypted()) throw new IllegalStateException("wallet is not encrypted with a password"); if (tempAesKey == null) throw new IllegalStateException("wallet is already locked"); tempAesKey = null; } void unlockWallet(String password, long timeout) { verifyWalletIsAvailableAndEncrypted(); KeyCrypterScrypt keyCrypterScrypt = getKeyCrypterScrypt(); // The aesKey is also cached for timeout (secs) after being used to decrypt the // wallet, in case the user wants to manually lock the wallet before the timeout. tempAesKey = keyCrypterScrypt.deriveKey(password); if (!walletsManager.checkAESKey(tempAesKey)) throw new IllegalStateException("incorrect password"); if (lockTimer != null) { // The user has called unlockwallet again, before the prior unlockwallet // timeout has expired. He's overriding it with a new timeout value. // Remove the existing lock timer to prevent it from calling lockwallet // before or after the new one does. lockTimer.stop(); lockTimer = null; } if (coreContext.isApiUser()) maybeSetWalletsManagerKey(); lockTimer = UserThread.runAfter(() -> { if (tempAesKey != null) { // The unlockwallet timeout has expired; re-lock the wallet. log.info("Locking wallet after {} second timeout expired.", timeout); tempAesKey = null; } }, timeout, SECONDS); } // Provided for automated wallet protection method testing, despite the // security risks exposed by providing users the ability to decrypt their wallets. void removeWalletPassword(String password) { verifyWalletIsAvailableAndEncrypted(); KeyCrypterScrypt keyCrypterScrypt = getKeyCrypterScrypt(); KeyParameter aesKey = keyCrypterScrypt.deriveKey(password); if (!walletsManager.checkAESKey(aesKey)) throw new IllegalStateException("incorrect password"); walletsManager.decryptWallets(aesKey); walletsManager.backupWallets(); } // Throws a RuntimeException if wallets are not available (encrypted or not). void verifyWalletsAreAvailable() { verifyWalletAndNetworkIsReady(); // TODO This check may be redundant, but the AppStartupState is new and unused // prior to commit 838595cb03886c3980c40df9cfe5f19e9f8a0e39. I would prefer // to leave this check in place until certain AppStartupState will always work // as expected. if (!walletsManager.areWalletsAvailable()) throw new IllegalStateException("wallet is not yet available"); } // Throws a RuntimeException if wallets are not available or not encrypted. void verifyWalletIsAvailableAndEncrypted() { verifyWalletAndNetworkIsReady(); if (!walletsManager.areWalletsAvailable()) throw new IllegalStateException("wallet is not yet available"); if (!walletsManager.areWalletsEncrypted()) throw new IllegalStateException("wallet is not encrypted with a password"); } // Throws a RuntimeException if wallets are encrypted and locked. void verifyEncryptedWalletIsUnlocked() { if (walletsManager.areWalletsEncrypted() && !accountService.isAccountOpen()) throw new IllegalStateException("wallet is locked"); } // Throws a RuntimeException if wallets and network are not ready. void verifyWalletAndNetworkIsReady() { if (!appStartupState.isWalletAndNetworkReady()) throw new IllegalStateException("wallet and network is not yet initialized"); } // Throws a RuntimeException if application is not fully initialized. void verifyApplicationIsFullyInitialized() { if (!appStartupState.isApplicationFullyInitialized()) throw new IllegalStateException("server is not fully initialized"); } // Throws a RuntimeException if wallet currency code is not BTC or XMR. private void verifyWalletCurrencyCodeIsValid(String currencyCode) { if (currencyCode == null || currencyCode.isEmpty()) return; if (!currencyCode.equalsIgnoreCase("BTC") && !currencyCode.equalsIgnoreCase("XMR")) throw new IllegalStateException(format("wallet does not support %s", currencyCode)); } private void maybeSetWalletsManagerKey() { // Unlike the UI, a daemon cannot capture the user's wallet encryption password // during startup. This method will set the wallet service's aesKey if necessary. if (tempAesKey == null) throw new IllegalStateException("cannot use null key, unlockwallet timeout may have expired"); if (btcWalletService.getAesKey() == null) { KeyParameter aesKey = new KeyParameter(tempAesKey.getKey()); walletsManager.setAesKey(aesKey); walletsSetup.getWalletConfig().maybeAddSegwitKeychain(walletsSetup.getWalletConfig().btcWallet(), aesKey); } } private XmrBalanceInfo getXmrBalances() { verifyWalletsAreAvailable(); verifyEncryptedWalletIsUnlocked(); if (balances.getAvailableBalance() == null) throw new IllegalStateException("Balances are not yet available"); return balances.getBalances(); } // Returns a Coin for the transfer amount string, or a RuntimeException if invalid. private Coin getValidTransferAmount(String amount, CoinFormatter coinFormatter) { Coin amountAsCoin = parseToCoin(amount, coinFormatter); if (amountAsCoin.isLessThan(getMinNonDustOutput())) throw new IllegalStateException(format("%s is an invalid transfer amount", amount)); return amountAsCoin; } private Coin getTxFeeRateFromParamOrPreferenceOrFeeService(String txFeeRate) { // A non txFeeRate String value overrides the fee service and custom fee. return txFeeRate.isEmpty() ? btcWalletService.getTxFeeForWithdrawalPerVbyte() : Coin.valueOf(Long.parseLong(txFeeRate)); } private KeyCrypterScrypt getKeyCrypterScrypt() { KeyCrypterScrypt keyCrypterScrypt = walletsManager.getKeyCrypterScrypt(); if (keyCrypterScrypt == null) throw new IllegalStateException("wallet encrypter is not available"); return keyCrypterScrypt; } private AddressEntry getAddressEntry(String addressString) { Optional addressEntry = btcWalletService.getAddressEntryListAsImmutableList().stream() .filter(e -> addressString.equals(e.getAddressString())) .findFirst(); if (!addressEntry.isPresent()) throw new IllegalStateException(format("address %s not found in wallet", addressString)); return addressEntry.get(); } public long getHeight() { return xmrWalletService.getHeight(); } /** * Memoization stores the results of expensive function calls and returns * the cached result when the same input occurs again. * * Resulting LoadingCache is used by calling `.get(input I)` or * `.getUnchecked(input I)`, depending on whether or not `f` can return null. * That's because CacheLoader throws an exception on null output from `f`. */ private static LoadingCache memoize(Function f) { // f::apply is used, because Guava 20.0 Function doesn't yet extend // Java Function. return CacheBuilder.newBuilder().build(CacheLoader.from(f::apply)); } } ================================================ FILE: core/src/main/java/haveno/core/api/NotificationListener.java ================================================ package haveno.core.api; import haveno.proto.grpc.NotificationMessage; import lombok.NonNull; /* * 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 . */ public interface NotificationListener { void onMessage(@NonNull NotificationMessage message); } ================================================ FILE: core/src/main/java/haveno/core/api/XmrConnectionService.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.api; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.ThreadUtils; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; import haveno.core.user.Preferences; import haveno.core.xmr.model.EncryptedConnectionList; import haveno.core.xmr.nodes.XmrNodes; import haveno.core.xmr.nodes.XmrNodes.XmrNode; import haveno.core.xmr.nodes.XmrNodesSetupPreferences; import haveno.core.xmr.setup.DownloadListener; import haveno.core.xmr.setup.WalletsSetup; import haveno.network.Socks5ProxyProvider; import haveno.network.p2p.P2PService; import haveno.network.p2p.P2PServiceListener; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.CompletionService; import java.util.concurrent.ExecutorCompletionService; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import org.apache.commons.lang3.exception.ExceptionUtils; import javafx.beans.property.IntegerProperty; import javafx.beans.property.LongProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.ReadOnlyIntegerProperty; import javafx.beans.property.ReadOnlyLongProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleLongProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import monero.common.MoneroConnectionManager; import monero.common.MoneroConnectionManagerListener; import monero.common.MoneroError; import monero.common.MoneroRpcConnection; import monero.common.MoneroRpcError; import monero.common.TaskLooper; import monero.daemon.MoneroDaemonRpc; import monero.daemon.model.MoneroDaemonInfo; import monero.daemon.model.MoneroTx; @Slf4j @Singleton public final class XmrConnectionService { private static final int MIN_BROADCAST_CONNECTIONS = 0; // TODO: 0 for stagenet, 5+ for mainnet private static final long REFRESH_PERIOD_HTTP_MS = 20000; // refresh period when connected to remote node over http private static final long REFRESH_PERIOD_ONION_MS = 30000; // refresh period when connected to remote node over tor private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes private static final int SYNC_TOLERANCE_NUM_BLOCKS = 3; private static final boolean USE_BOOTSTRAP_HEIGHT = false; private static final long STAGGER_MS = 1500; // stagger connection checks private static final String LAST_INFO_KEY = "lastInfo"; private static int numConsecutiveErrors = 0; public enum XmrConnectionFallbackType { LOCAL, CUSTOM, PROVIDED } private final Object lock = new Object(); private final Object pollLock = new Object(); private final Object listenerLock = new Object(); private final Config config; private final CoreContext coreContext; private final Preferences preferences; private final CoreAccountService accountService; private final XmrNodes xmrNodes; private final XmrLocalNode xmrLocalNode; private final EncryptedConnectionList connectionList; private List connections = new ArrayList(); private final ObjectProperty connectionProperty = new SimpleObjectProperty<>(); private final ObjectProperty> connectionsProperty = new SimpleObjectProperty<>(); // TODO: redundant with connections private final IntegerProperty numConnections = new SimpleIntegerProperty(-1); private final LongProperty chainHeight = new SimpleLongProperty(0); private final DownloadListener downloadListener = new DownloadListener(); @Getter private final ObjectProperty connectionServiceFallbackType = new SimpleObjectProperty<>(); @Getter private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty(); private final LongProperty numUpdates = new SimpleLongProperty(0); private Socks5ProxyProvider socks5ProxyProvider; private boolean isInitialized; private boolean pollInProgress; private MoneroDaemonRpc monerod; private Long connectionTimeout; private Boolean isConnected = false; @Getter private MoneroDaemonInfo lastInfo; private Long lastFallbackInvocation; private Long lastLogPollErrorTimestamp; private long lastLogMonerodNotSyncedTimestamp; private Long syncStartHeight; private TaskLooper monerodPoller; private long lastRefreshPeriodMs; private boolean wasMonerodSynced; @Getter private boolean isShutDownStarted; private List listeners = new ArrayList<>(); private XmrKeyImagePoller keyImagePoller; private final Map> txCache = new HashMap>(); // connection switching private static final int EXCLUDE_CONNECTION_SECONDS = 180; private static final int MAX_SWITCH_REQUESTS_PER_MINUTE = 2; private static final int SKIP_SWITCH_WITHIN_MS = 10000; private int numRequestsLastMinute; private long lastSwitchTimestamp; private Set excludedConnections = new HashSet<>(); private static final long FALLBACK_INVOCATION_PERIOD_MS = 1000 * 30 * 1; // offer to fallback up to once every 30s private boolean fallbackApplied; private boolean usedSyncingLocalNodeBeforeStartup; private boolean localNodeStartedFromPrompt = false; @Inject public XmrConnectionService(P2PService p2PService, Config config, CoreContext coreContext, Preferences preferences, WalletsSetup walletsSetup, CoreAccountService accountService, XmrNodes xmrNodes, XmrLocalNode xmrLocalNode, MoneroConnectionManager connectionManager, EncryptedConnectionList connectionList, Socks5ProxyProvider socks5ProxyProvider) { this.config = config; this.coreContext = coreContext; this.preferences = preferences; this.accountService = accountService; this.xmrNodes = xmrNodes; this.xmrLocalNode = xmrLocalNode; this.connectionList = connectionList; this.socks5ProxyProvider = socks5ProxyProvider; // set static references in HavenoUtils // TODO: better way? HavenoUtils.xmrConnectionService = this; HavenoUtils.preferences = preferences; // initialize when connected to p2p network p2PService.addP2PServiceListener(new P2PServiceListener() { @Override public void onTorNodeReady() { ThreadUtils.submitToPool(() -> { try { initialize(); } catch (Exception e) { log.warn("Error initializing connection service, error={}\n", e.getMessage(), e); } }); } @Override public void onHiddenServicePublished() {} @Override public void onDataReceived() {} @Override public void onNoSeedNodeAvailable() {} @Override public void onNoPeersAvailable() {} @Override public void onUpdatedDataReceived() {} }); } public void onShutDownStarted() { log.info("{}.onShutDownStarted()", getClass().getSimpleName()); isShutDownStarted = true; } public void shutDown() { log.info("Shutting down {}", getClass().getSimpleName()); isInitialized = false; synchronized (lock) { if (monerodPoller != null) monerodPoller.stop(); monerod = null; } } // ------------------------ CONNECTION MANAGEMENT ------------------------- public MoneroDaemonRpc getMonerod() { accountService.checkAccountOpen(); return this.monerod; } public String getProxyUri() { return socks5ProxyProvider.getSocks5Proxy() == null ? null : socks5ProxyProvider.getSocks5Proxy().getInetAddress().getHostAddress() + ":" + socks5ProxyProvider.getSocks5Proxy().getPort(); } public void addConnectionListener(MoneroConnectionManagerListener listener) { synchronized (listenerLock) { listeners.add(listener); } } public Boolean isConnected() { return isConnected; } public void addConnection(MoneroRpcConnection connection) { addConnection(connection, true); } private void addConnection(MoneroRpcConnection connection, boolean addToEncryptedList) { accountService.checkAccountOpen(); synchronized (connections) { if (getConnection(connection.getUri()) != null) throw new IllegalArgumentException("Connection already exists with URI: " + connection.getUri()); connections.add(connection); } if (addToEncryptedList && coreContext.isApiUser()) connectionList.addConnection(connection); } public void removeConnection(String uri) { accountService.checkAccountOpen(); connectionList.removeConnection(uri); synchronized (connections) { MoneroRpcConnection toRemove = null; for (MoneroRpcConnection connection : connections) { if (connection.getUri().equals(uri)) { toRemove = connection; break; } } if (toRemove != null) connections.remove(toRemove); } } public MoneroRpcConnection getConnection() { accountService.checkAccountOpen(); Optional currentConnectionUri = connectionList.getCurrentConnectionUri(); return currentConnectionUri.isPresent() ? getConnection(currentConnectionUri.get()) : null; } public boolean hasConnection(String uri) { return getConnection(uri) != null; } public MoneroRpcConnection getConnection(String connectionUri) { accountService.checkAccountOpen(); synchronized (connections) { for (MoneroRpcConnection connection : connections) { if (connection.getUri().equals(connectionUri)) { return connection; } } return null; } } public List getConnections() { accountService.checkAccountOpen(); synchronized (connections) { return connections; } } public void setConnection(String connectionUri) { accountService.checkAccountOpen(); MoneroRpcConnection connection = getConnection(connectionUri); if (connection == null) connection = new MoneroRpcConnection(connectionUri); setConnection(connection, null); } public void setConnection(MoneroRpcConnection connection) { accountService.checkAccountOpen(); setConnection(connection, null); } private void setConnection(MoneroRpcConnection connection, MoneroDaemonInfo info) { log.info("XmrConnectionService.setConnection() called with connection: " + connection); if (isShutDownStarted || !accountService.isAccountOpen()) return; if (connection == null) { log.warn("Setting monerod connection to null", new Throwable("Stack trace")); } // update internals if applicable boolean isInitializing = monerod == null && connection != null; if (isInitializing || !HavenoUtils.connectionConfigsEqual(connection, getConnection()) || !isPolling() || lastRefreshPeriodMs != getRefreshPeriodMs()) { synchronized (lock) { if (connection == null) { monerod = null; isConnected = false; connectionList.setCurrentConnectionUri(null); } else { monerod = new MoneroDaemonRpc(connection); isConnected = connection.isConnected(); synchronized (connections) { removeConnection(connection.getUri()); addConnection(connection); } connectionList.setCurrentConnectionUri(connection.getUri()); } // set connection property on user thread UserThread.execute(() -> { connectionProperty.set(connection); numUpdates.set(numUpdates.get() + 1); }); } // update key image poller keyImagePoller.setMonerod(getMonerod()); keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs()); // restart polling lastRefreshPeriodMs = getRefreshPeriodMs(); if (wasMonerodSynced) { updatePolling(info); // restart polling off thread after connection established } else { tryPollMonerod(info); // poll immediately before connection established if (connection != getConnection()) return; // polling can change connection UserThread.runAfter(() -> updatePolling(null), getInternalRefreshPeriodMs() / 1000); } } // notify listeners in parallel synchronized (listenerLock) { for (MoneroConnectionManagerListener listener : listeners) { ThreadUtils.submitToPool(() -> listener.onConnectionChanged(connection)); } } } public MoneroRpcConnection checkConnection() { accountService.checkAccountOpen(); MoneroRpcConnection connection = getConnection(); checkConnection(connection); return connection; } public MoneroRpcConnection getBestConnection() { return getBestConnection(null); } private MoneroRpcConnection getBestConnection(Collection ignoredConnections) { accountService.checkAccountOpen(); // skip if user needs prompted if (promptToStartLocalNode()) { log.warn("Cannot get best connection on startup because user needs to be prompted"); return null; } // copy connections for thread safety List connectionsCopy; synchronized (connections) { connectionsCopy = new ArrayList(getConnections()); } // get best connection Set ignoredConnectionsSet = new HashSet<>(); if (ignoredConnections != null) ignoredConnectionsSet.addAll(ignoredConnections); addLocalNodeIfIgnored(ignoredConnectionsSet); MoneroRpcConnection bestConnection = getBestConnection(connectionsCopy, ignoredConnectionsSet); // return only connection if no best connection if (bestConnection == null && connectionsCopy.size() == 1 && !ignoredConnectionsSet.contains(connectionsCopy.get(0))) { return connectionsCopy.get(0); } return bestConnection; } private static MoneroRpcConnection getBestConnection(Collection connections, Collection ignoredConnections) { log.info("Getting best Monero connection, ignoring " + (ignoredConnections == null ? 0 : ignoredConnections.size()) + " connections"); // try connections within each ascending priority AtomicReference bestConnection = new AtomicReference<>(); MoneroRpcConnection bestUnsyncedConnection = null; for (List prioritizedConnections : getConnectionsInAscendingPriority(connections)) { try { // shuffle connections within same priority Collections.shuffle(prioritizedConnections); // check connections staggered int numTasks = 0; ExecutorService pool = Executors.newFixedThreadPool(prioritizedConnections.size()); CompletionService completionService = new ExecutorCompletionService(pool); for (int i = 0; i < prioritizedConnections.size(); i++) { MoneroRpcConnection connection = prioritizedConnections.get(i); if (ignoredConnections != null && ignoredConnections.contains(connection)) continue; numTasks++; final int delay = i; completionService.submit(() -> { if (delay > 0) Thread.sleep(delay * STAGGER_MS); // stagger start if (bestConnection.get() == null) checkConnection(connection); // check connection if best not found return connection; }); } // use first available and synced connection pool.shutdown(); for (int i = 0; i < numTasks; i++) { try { MoneroRpcConnection connection = completionService.take().get(); if (Boolean.TRUE.equals(connection.isConnected())) { if (isSyncedWithinTolerance(getCachedDaemonInfo(connection))) { bestConnection.set(connection); return connection; } else if (bestUnsyncedConnection == null) { bestUnsyncedConnection = connection; } } } catch (Exception e) { // ignore error connecting } } } catch (Exception e) { log.warn("Error checking prioritized connections: " + e.getMessage() + ". That should never happen."); throw new MoneroError(e); } } if (bestUnsyncedConnection == null) { log.warn("There is no best Monero connection detected"); } else { log.warn("The best Monero connection is not synced"); } return bestUnsyncedConnection; } private static List> getConnectionsInAscendingPriority(Collection connections) { Map> connectionPriorities = new TreeMap>(); for (MoneroRpcConnection connection : connections) { if (!connectionPriorities.containsKey(connection.getPriority())) connectionPriorities.put(connection.getPriority(), new ArrayList()); connectionPriorities.get(connection.getPriority()).add(connection); } List> prioritizedConnections = new ArrayList>(); for (List priorityConnections : connectionPriorities.values()) prioritizedConnections.add(priorityConnections); if (connectionPriorities.containsKey(0)) prioritizedConnections.add(prioritizedConnections.remove(0)); // move priority 0 to end return prioritizedConnections; } private boolean promptToStartLocalNode() { return !wasMonerodSynced && !fallbackApplied && usedSyncingLocalNodeBeforeStartup && !xmrLocalNode.isDetected(); // we give user the chance to start local node on startup if previously used } private void addLocalNodeIfIgnored(Collection ignoredConnections) { synchronized (connections) { if (xmrLocalNode.shouldBeIgnored() && hasConnection(xmrLocalNode.getUri())) ignoredConnections.add(getConnection(xmrLocalNode.getUri())); } } private boolean canSwitchToBestConnection() { return !isFixedConnection() && isAutoSwitch() && !promptToStartLocalNode(); } private MoneroRpcConnection switchToBestConnection() { return switchToBestConnection(null, true); } private MoneroRpcConnection switchToBestConnection(Collection ignoredConnections, boolean logWarning) { if (!canSwitchToBestConnection()) { if (logWarning) log.warn("Cannot switch to best Monero connection because connection is fixed, auto switch is disabled, or fallback is required"); return null; } MoneroRpcConnection bestConnection = getBestConnection(ignoredConnections); if (bestConnection != null) setConnection(bestConnection, getCachedDaemonInfo(bestConnection)); return bestConnection; } public synchronized boolean requestSwitchToNextBestConnection() { return requestSwitchToNextBestConnection(null); } public synchronized boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) { log.warn("Requesting switch to next best monerod, source monerod={}, proxyUri={}", sourceConnection == null ? null : sourceConnection.getUri(), sourceConnection == null ? null : sourceConnection.getProxyUri()); if (Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_LOCAL) { log.warn("Requesting connection switch on testnet", new RuntimeException("Stack trace")); } // skip if shut down started if (isShutDownStarted) { log.warn("Skipping switch to next best Monero connection because shut down has started"); return false; } // skip if connection is already switched if (sourceConnection != null && sourceConnection != getConnection()) { log.warn("Skipping switch to next best Monero connection because source connection is not current connection"); return false; } // skip if connection is fixed if (isFixedConnection() || !isAutoSwitch()) { log.warn("Skipping switch to next best Monero connection because connection is fixed or auto switch is disabled"); return false; } // skip if last switch was too recent boolean skipSwitch = System.currentTimeMillis() - lastSwitchTimestamp < SKIP_SWITCH_WITHIN_MS; if (skipSwitch) { log.warn("Skipping switch to next best Monero connection because last switch was less than {} seconds ago", SKIP_SWITCH_WITHIN_MS / 1000); return false; } // skip if too many requests in the last minute if (numRequestsLastMinute > MAX_SWITCH_REQUESTS_PER_MINUTE) { log.warn("Skipping switch to next best Monero connection because more than {} requests were made in the last minute", MAX_SWITCH_REQUESTS_PER_MINUTE); return false; } // increment request count numRequestsLastMinute++; UserThread.runAfter(() -> numRequestsLastMinute--, 60); // decrement after one minute // exclude current connection MoneroRpcConnection currentConnection = getConnection(); if (currentConnection != null) excludedConnections.add(currentConnection); // get connection to switch to MoneroRpcConnection bestConnection = getBestConnection(excludedConnections); // remove from excluded connections after period UserThread.runAfter(() -> { if (currentConnection != null) excludedConnections.remove(currentConnection); }, EXCLUDE_CONNECTION_SECONDS); // return if no connection to switch to if (bestConnection == null || !Boolean.TRUE.equals(bestConnection.isConnected())) { return false; } // switch to best connection lastSwitchTimestamp = System.currentTimeMillis(); setConnection(bestConnection, getCachedDaemonInfo(bestConnection)); return true; } public void setAutoSwitch(boolean autoSwitch) { accountService.checkAccountOpen(); connectionList.setAutoSwitch(autoSwitch); } public boolean getAutoSwitch() { accountService.checkAccountOpen(); return connectionList.getAutoSwitch(); } public boolean isConnectionLocalHost() { return isConnectionLocalHost(getConnection()); } public boolean isProxyApplied() { return isProxyApplied(getConnection()); } public long getRefreshPeriodMs() { return connectionList.getRefreshPeriod() > 0 ? connectionList.getRefreshPeriod() : getDefaultRefreshPeriodMs(false); } private long getInternalRefreshPeriodMs() { return connectionList.getRefreshPeriod() > 0 ? connectionList.getRefreshPeriod() : getDefaultRefreshPeriodMs(true); } public void verifyConnection() { if (monerod == null) throw new RuntimeException("No connection to Monero node"); if (!Boolean.TRUE.equals(isConnected())) throw new RuntimeException("No connection to Monero node"); if (!isSyncedWithinTolerance()) throw new RuntimeException("Monero node is not synced"); } public Long getHeight() { return getHeight(lastInfo); } private static Long getHeight(MoneroDaemonInfo info) { if (info == null) return null; if (USE_BOOTSTRAP_HEIGHT) return info.getHeight(); return info.getHeightWithoutBootstrap() == null || info.getHeightWithoutBootstrap() == 0 ? info.getHeight() : info.getHeightWithoutBootstrap(); } public Long getTargetHeight() { return getTargetHeight(lastInfo); } private static Long getTargetHeight(MoneroDaemonInfo info) { if (info == null) return null; return info.getTargetHeight() == 0 ? info.getHeight() : info.getTargetHeight(); } private static int getNumOutgoingConnections(MoneroDaemonInfo info) { return info == null || Boolean.TRUE.equals(info.isRestricted()) ? -1 : info.getNumOutgoingConnections(); } public boolean isSyncedWithinTolerance() { return isSyncedWithinTolerance(lastInfo); } public XmrKeyImagePoller getKeyImagePoller() { synchronized (lock) { if (keyImagePoller == null) keyImagePoller = new XmrKeyImagePoller(); return keyImagePoller; } } private long getKeyImageRefreshPeriodMs() { return isConnectionLocalHost() ? KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL : KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE; } // ----------------------------- APP METHODS ------------------------------ public ReadOnlyIntegerProperty numConnectionsProperty() { return numConnections; } public ReadOnlyObjectProperty> connectionsProperty() { return connectionsProperty; } public ReadOnlyObjectProperty connectionProperty() { return connectionProperty; } public boolean hasSufficientPeersForBroadcast() { if (numConnections.get() < 0) return true; // we don't know how many connections we have, but that's expected with restricted node return numConnections.get() >= getMinBroadcastConnections(); } public LongProperty chainHeightProperty() { return chainHeight; } public ReadOnlyDoubleProperty downloadPercentageProperty() { return downloadListener.percentageProperty(); } public int getMinBroadcastConnections() { return MIN_BROADCAST_CONNECTIONS; } public boolean isDownloadComplete() { return downloadPercentageProperty().get() == 1d; } public ReadOnlyLongProperty numUpdatesProperty() { return numUpdates; } public void fallbackToBestConnection() { stopPolling(); if (isShutDownStarted) return; fallbackApplied = true; if (isProvidedConnections() || xmrNodes.getProvidedXmrNodes().isEmpty()) { log.warn("Falling back to public nodes"); preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal()); initializeConnections(); } else { log.warn("Falling back to provided nodes"); preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal()); initializeConnections(); if (getConnection() == null) { log.warn("No provided nodes available, falling back to public nodes"); fallbackToBestConnection(); } } } public MoneroTx getTx(String txHash) { List txs = getTxs(Arrays.asList(txHash)); return txs.isEmpty() ? null : txs.get(0); } public List getTxs(List txHashes) { synchronized (txCache) { // fetch txs if (getMonerod() == null) verifyConnection(); // will throw List txs = getMonerod().getTxs(txHashes, true); // store to cache for (MoneroTx tx : txs) txCache.put(tx.getHash(), Optional.of(tx)); // schedule txs to be removed from cache UserThread.runAfter(() -> { synchronized (txCache) { for (MoneroTx tx : txs) txCache.remove(tx.getHash()); } }, getRefreshPeriodMs() / 1000); return txs; } } public MoneroTx getTxWithCache(String txHash) { List cachedTxs = getTxsWithCache(Arrays.asList(txHash)); return cachedTxs.isEmpty() ? null : cachedTxs.get(0); } public List getTxsWithCache(List txHashes) { synchronized (txCache) { try { // get cached txs List cachedTxs = new ArrayList(); List uncachedTxHashes = new ArrayList(); for (int i = 0; i < txHashes.size(); i++) { if (txCache.containsKey(txHashes.get(i))) cachedTxs.add(txCache.get(txHashes.get(i)).orElse(null)); else uncachedTxHashes.add(txHashes.get(i)); } // return txs from cache if available, otherwise fetch return uncachedTxHashes.isEmpty() ? cachedTxs : getTxs(txHashes); } catch (Exception e) { if (!isShutDownStarted) throw e; return null; } } } // ---------------------------- STATIC UTILS ----------------------------- protected static boolean isProxyApplied(MoneroRpcConnection connection) { if (connection == null) return false; return connection.isOnion() || (HavenoUtils.preferences.getUseTorForXmr().isUseTorForXmr() && !HavenoUtils.isPrivateIp(connection.getUri())); } protected static void checkConnection(MoneroRpcConnection connection) { long startTime = System.currentTimeMillis(); try { connection.setTimeout(getTimeoutMs(connection)); MoneroDaemonRpc monerod = new MoneroDaemonRpc(connection); MoneroDaemonInfo info = monerod.getInfo(); connection.setOnline(getNumOutgoingConnections(info) != 0); connection.setAuthenticated(true); connection.setAttribute(LAST_INFO_KEY, info); } catch (Exception e) { connection.setOnline(false); if (e instanceof MoneroRpcError) { if (((MoneroRpcError) e).getCode() == 401) { connection.setOnline(true); connection.setAuthenticated(false); } } else { connection.setOnline(false); connection.setAuthenticated(null); } connection.setAttribute(LAST_INFO_KEY, null); } if (Boolean.TRUE.equals(connection.isOnline())) { connection.setResponseTime(System.currentTimeMillis() - startTime); } } protected static long getTimeoutMs(MoneroRpcConnection connection) { if (HavenoUtils.isLocalHost(connection.getUri())) { return XmrLocalNode.REFRESH_PERIOD_LOCAL_MS; } else if (isProxyApplied(connection)) { return REFRESH_PERIOD_ONION_MS; } else { return REFRESH_PERIOD_HTTP_MS; } } protected static MoneroDaemonInfo getCachedDaemonInfo(MoneroRpcConnection connection) { return (MoneroDaemonInfo) connection.getAttribute(LAST_INFO_KEY); } protected static boolean isSyncedWithinTolerance(MoneroDaemonInfo info) { Long targetHeight = getTargetHeight(info); if (targetHeight == null) return false; if (targetHeight - getHeight(info) <= SYNC_TOLERANCE_NUM_BLOCKS) return true; // synced if within 3 blocks of target height return false; } // ------------------------------- HELPERS -------------------------------- private void doneDownload() { wasMonerodSynced = true; downloadListener.doneDownload(); } private boolean isConnectionLocalHost(MoneroRpcConnection connection) { return connection != null && HavenoUtils.isLocalHost(connection.getUri()); } private long getDefaultRefreshPeriodMs(boolean internal) { MoneroRpcConnection connection = getConnection(); if (connection == null) return XmrLocalNode.REFRESH_PERIOD_LOCAL_MS; if (isConnectionLocalHost(connection)) { if (internal) return XmrLocalNode.REFRESH_PERIOD_LOCAL_MS; if (lastInfo != null && (lastInfo.getHeightWithoutBootstrap() != null && lastInfo.getHeightWithoutBootstrap() > 0 && lastInfo.getHeightWithoutBootstrap() < lastInfo.getHeight())) { return REFRESH_PERIOD_HTTP_MS; // refresh slower if syncing or bootstrapped } else { return XmrLocalNode.REFRESH_PERIOD_LOCAL_MS; // TODO: announce faster refresh after done syncing } } else if (isProxyApplied(connection)) { return REFRESH_PERIOD_ONION_MS; } else { return REFRESH_PERIOD_HTTP_MS; } } private void initialize() { // initialize key image poller getKeyImagePoller(); new Thread(() -> { HavenoUtils.waitFor(20000); keyImagePoller.poll(); // TODO: keep or remove first poll?s }).start(); // listen for account to be opened or password changed if (!isInitialized) { accountService.addListener(new AccountServiceListener() { @Override public void onAccountOpened() { try { log.info(getClass() + ".onAccountOpened() called"); initialize(); } catch (Exception e) { log.error("Error initializing connection service after account opened, error={}\n", e.getMessage(), e); throw new RuntimeException(e); } } @Override public void onPasswordChanged(String oldPassword, String newPassword) { log.info(getClass() + ".onPasswordChanged({}, {}) called", oldPassword == null ? null : "***", newPassword == null ? null : "***"); connectionList.changePassword(oldPassword, newPassword); } }); } // initialize connections initializeConnections(); } private void initializeConnections() { MoneroRpcConnection initialConnection = null; MoneroDaemonInfo initialInfo = null; synchronized (lock) { // reset connection manager synchronized (connections) { connections.clear(); } // run once if (!isInitialized) { // register local node listener xmrLocalNode.addListener(new XmrLocalNodeListener() { @Override public void onNodeStarted(MoneroDaemonRpc monerod) { log.info("Local monero node started, height={}", monerod.getHeight()); } @Override public void onNodeStopped() { log.info("Local monero node stopped"); } @Override public void onConnectionChanged(MoneroRpcConnection connection) { log.info("Local monerod connection changed: " + connection); // skip if ignored if (isShutDownStarted || !isAutoSwitch() || !accountService.isAccountOpen() || !hasConnection(connection.getUri()) || xmrLocalNode.shouldBeIgnored()) return; // check connection boolean isConnectedAndSynced = false; if (xmrLocalNode.isConnected()) { MoneroRpcConnection conn = getConnection(connection.getUri()); checkConnection(conn); isConnectedAndSynced = Boolean.TRUE.equals(conn.isConnected()) && isSyncedWithinTolerance(getCachedDaemonInfo(conn)); } // update connection boolean isCurrentConnection = getConnection() != null && getConnection().getUri().equals(connection.getUri()); if (isConnectedAndSynced) { setConnection(connection.getUri()); // reset error connecting to local node if (connectionServiceFallbackType.get() == XmrConnectionFallbackType.LOCAL && isConnectionLocalHost()) { connectionServiceFallbackType.set(null); } } else if (isCurrentConnection && !promptToStartLocalNode() && !localNodeStartedFromPrompt) { switchToBestConnection(); // TODO: what if this is called before initialized? } } }); } // set if last node was locally syncing if (!isInitialized) { usedSyncingLocalNodeBeforeStartup = connectionList.getCurrentConnectionUri().isPresent() && xmrLocalNode.equalsUri(connectionList.getCurrentConnectionUri().get()) && Boolean.TRUE.equals(preferences.getXmrNodeSettings().getSyncBlockchain()); } // restore connections if (!isFixedConnection()) { // load previous or default connections if (coreContext.isApiUser()) { // load previous connections for (MoneroRpcConnection connection : connectionList.getConnections()) addConnection(connection, false); log.info("Read " + connectionList.getConnections().size() + " previous connections from disk"); // add default connections for (XmrNode node : xmrNodes.getAllXmrNodes()) { if (node.hasClearNetAddress()) { if (!(xmrLocalNode.equalsUri(node.getClearNetUri()) && xmrLocalNode.shouldBeIgnored())) { MoneroRpcConnection connection = new MoneroRpcConnection(node.getHostNameOrAddress() + ":" + node.getPort()).setPriority(node.getPriority()); if (!connectionList.hasConnection(connection.getUri())) addConnection(connection); } } if (node.hasOnionAddress()) { MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority()); if (!connectionList.hasConnection(connection.getUri())) addConnection(connection); } } } else { // add default connections for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) { if (node.hasClearNetAddress()) { if (!(xmrLocalNode.equalsUri(node.getClearNetUri()) && xmrLocalNode.shouldBeIgnored())) { MoneroRpcConnection connection = new MoneroRpcConnection(node.getHostNameOrAddress() + ":" + node.getPort()).setPriority(node.getPriority()); addConnection(connection); } } if (node.hasOnionAddress()) { MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority()); addConnection(connection); } } } // restore last connection if (connectionList.getCurrentConnectionUri().isPresent() && hasConnection(connectionList.getCurrentConnectionUri().get())) { if (!(xmrLocalNode.equalsUri(connectionList.getCurrentConnectionUri().get()) && xmrLocalNode.shouldBeIgnored())) { initialConnection = getConnection(connectionList.getCurrentConnectionUri().get()); } } // set connection proxies log.info("TOR proxy URI: " + getProxyUri()); synchronized (connections) { for (MoneroRpcConnection connection : connections) { if (isProxyApplied(connection)) connection.setProxyUri(getProxyUri()); } } // update connection if (canSwitchToBestConnection()) { MoneroRpcConnection bestConnection = getBestConnection(); if (bestConnection != null) initialConnection = bestConnection; if (initialConnection != null) initialInfo = getCachedDaemonInfo(initialConnection); } } else if (!isInitialized) { // set connection from startup argument if given MoneroRpcConnection connection = new MoneroRpcConnection(config.xmrNode, config.xmrNodeUsername, config.xmrNodePassword).setPriority(1); if (isProxyApplied(connection)) connection.setProxyUri(getProxyUri()); initialConnection = connection; } // register connection listener isInitialized = true; } // notify initial connection lastRefreshPeriodMs = getRefreshPeriodMs(); setConnection(initialConnection, initialInfo); // start background polling local node manager xmrLocalNode.startPolling(); } public boolean isAutoSwitch() { return connectionList.getAutoSwitch() || !coreContext.isApiUser(); // auto switch always enabled on desktop ui } public void startLocalNode() throws Exception { // cannot start local node as seed node if (HavenoUtils.isSeedNode()) { throw new RuntimeException("Cannot start local node on seed node"); } // start local node log.info("Starting local node"); localNodeStartedFromPrompt = true; xmrLocalNode.start(); } private void updatePolling(MoneroDaemonInfo applyInfo) { synchronized (lock) { stopPolling(); if (connectionList.getRefreshPeriod() >= 0) startPolling(applyInfo); // 0 means default refresh poll } } private void startPolling(MoneroDaemonInfo applyInfo) { synchronized (lock) { stopPolling(); numConsecutiveErrors = 0; AtomicBoolean firstPoll = new AtomicBoolean(true); monerodPoller = new TaskLooper(() -> { if (!pollInProgress) { tryPollMonerod(firstPoll.get() ? applyInfo : null); } firstPoll.set(false); }); monerodPoller.start(getInternalRefreshPeriodMs()); } } private void stopPolling() { synchronized (lock) { if (monerodPoller != null) { monerodPoller.stop(); monerodPoller = null; } } } private boolean isPolling() { synchronized (lock) { return monerodPoller != null; } } private void tryPollMonerod(MoneroDaemonInfo applyInfo) { try { doPollMonerod(applyInfo); } catch (Exception e) { // error is already handled } } /** * Polls monerod for the latest info and updates the connection if necessary. * * @param applyInfo applies the given info instead of fetching from monerod */ private void doPollMonerod(MoneroDaemonInfo applyInfo) { synchronized (pollLock) { if (isShutDownStarted) return; pollInProgress = true; try { // check monero connection with error tolerance MoneroRpcConnection connection = getConnection(); try { // throw if no monerod if (monerod == null) throw new RuntimeException("No connection to Monero node."); // check the monero connection or use applied info if (applyInfo == null) { checkConnection(connection); MoneroDaemonInfo info = getCachedDaemonInfo(connection); if (info == null) throw new RuntimeException("Could not get latest info from the Monero node."); lastInfo = info; } else { lastInfo = applyInfo; } // throw if no peer connections if (getNumOutgoingConnections(lastInfo) == 0) { throw new RuntimeException("The Monero node has no connected peers. It may be experiencing a network connectivity issue."); } // reset error count on success numConsecutiveErrors = 0; } catch (Exception e) { // skip handling if shutting down if (isShutDownStarted) return; // skip error handling up to max attempts numConsecutiveErrors++; if (numConsecutiveErrors < getMaxConsecutiveErrors()) { // attempt to switch if never synced unless polling stopped if (!wasMonerodSynced && canSwitchToBestConnection() && isPolling()) { switchToBestConnection(); } return; } else { numConsecutiveErrors = 0; // reset error count } // log error message periodically boolean lastWarningOutsidePeriod = lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS; if (lastWarningOutsidePeriod) { log.warn("Error fetching daemon info after max attempts ({}). monerod={}, error={}", getMaxConsecutiveErrors(), connection == null ? "null" : connection.getUri(), e.getMessage()); if (DevEnv.isDevMode()) log.error(ExceptionUtils.getStackTrace(e)); lastLogPollErrorTimestamp = System.currentTimeMillis(); } // invoke fallback handling on startup error boolean canFallback = !wasMonerodSynced && (isFixedConnection() || isProvidedConnections() || isCustomConnections() || usedSyncingLocalNodeBeforeStartup); if (!fallbackApplied && canFallback && connectionServiceFallbackType.get() == null && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) { lastFallbackInvocation = System.currentTimeMillis(); if (usedSyncingLocalNodeBeforeStartup && !xmrLocalNode.isDetected()) { log.warn("Could not get monerod info from local connection on startup: " + e.getMessage()); connectionServiceFallbackType.set(XmrConnectionFallbackType.LOCAL); return; } else if (isProvidedConnections()) { log.warn("Could not get monerod info from provided connections on startup: " + e.getMessage()); connectionServiceFallbackType.set(XmrConnectionFallbackType.PROVIDED); return; } else if (isCustomConnections()) { log.warn("Could not get monerod info from custom connection on startup: " + e.getMessage()); connectionServiceFallbackType.set(XmrConnectionFallbackType.CUSTOM); return; } } // skip further error handling if awaiting prompt if (connectionServiceFallbackType.get() != null) return; // try switching to next best connection unless polling stopped if (canSwitchToBestConnection() && isPolling()) { MoneroRpcConnection newConnection = switchToBestConnection(Arrays.asList(connection), lastWarningOutsidePeriod); if (newConnection == null) throw e; return; } else { throw e; } } // connected to monerod isConnected = true; connectionServiceFallbackType.set(null); // set chain height chainHeight.set(getHeight()); // save if blockchain is syncing locally boolean blockchainSyncingLocally = isConnectionLocalHost() && lastInfo.getHeight().equals(lastInfo.getHeightWithoutBootstrap()) || (lastInfo.getTargetHeight().equals(0l) && lastInfo.getHeightWithoutBootstrap().equals(0l)); // blockchain is syncing if height equals height without bootstrap, or target height and height without bootstrap both equal 0 preferences.getXmrNodeSettings().setSyncBlockchain(blockchainSyncingLocally); // TODO: this isn't saved until all services initialized // throttle warnings if monerod not synced if (!isSyncedWithinTolerance() && System.currentTimeMillis() - lastLogMonerodNotSyncedTimestamp > HavenoUtils.LOG_MONEROD_NOT_SYNCED_WARN_PERIOD_MS) { log.warn("Our chain height: {} is out of sync with peer nodes chain height: {}", getHeight(), getTargetHeight()); lastLogMonerodNotSyncedTimestamp = System.currentTimeMillis(); } // announce connection change if refresh period changes if (getRefreshPeriodMs() != lastRefreshPeriodMs) { pollInProgress = false; setConnection(getConnection(), lastInfo); // resets polling return; } // handle error recovery if (lastLogPollErrorTimestamp != null) { log.info("Successfully fetched monerod info after previous error"); lastLogPollErrorTimestamp = null; } // clear error message getConnectionServiceErrorMsg().set(null); } catch (Exception e) { // not connected to monerod isConnected = false; // skip if shut down if (isShutDownStarted) return; // format error message String errorMsg = e.getMessage(); if (errorMsg != null && errorMsg.contains(": ")) { errorMsg = errorMsg.substring(errorMsg.indexOf(": ") + 2); // strip exception class } errorMsg = Res.get("popup.warning.moneroConnection", errorMsg); // set error message unless fallback prompt is shown getConnectionServiceErrorMsg().set(errorMsg); throw e; } finally { pollInProgress = false; updateProperties(); } } } private boolean isFixedConnection() { return !"".equals(config.xmrNode) && !(HavenoUtils.isLocalHost(config.xmrNode) && xmrLocalNode.shouldBeIgnored()) && !fallbackApplied; } private boolean isCustomConnections() { return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM; } private boolean isProvidedConnections() { return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.PROVIDED; } private int getMaxConsecutiveErrors() { return isConnectionLocalHost() ? 3 : 4; // allow more errors on remote connections } private void updateProperties() { UserThread.execute(() -> { // update sync progress if (lastInfo != null) { long height = getHeight(); long targetHeight = getTargetHeight(); if (height >= targetHeight) doneDownload(); else { long blocksRemaining = targetHeight - height; if (syncStartHeight == null) syncStartHeight = height; double percent = Math.min(1.0, targetHeight == syncStartHeight ? 1.0 : ((double) Math.max(1, height - syncStartHeight) / (double) (targetHeight - syncStartHeight))); // grant at least 1 block to show progress downloadListener.progress(percent, blocksRemaining); } } // set available connections List availableConnections = new ArrayList<>(); synchronized (connections) { for (MoneroRpcConnection connection : connections) { if (Boolean.TRUE.equals(connection.isOnline()) && Boolean.TRUE.equals(connection.isAuthenticated())) { availableConnections.add(connection); } } } connectionsProperty.set(availableConnections); numConnections.set(getNumOutgoingConnections(lastInfo)); // notify update numUpdates.set(numUpdates.get() + 1); }); } } ================================================ FILE: core/src/main/java/haveno/core/api/XmrKeyImageListener.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.api; import monero.daemon.model.MoneroKeyImageSpentStatus; import java.util.Map; public interface XmrKeyImageListener { /** * Called with changes to the spent status of key images. */ public void onSpentStatusChanged(Map spentStatuses); } ================================================ FILE: core/src/main/java/haveno/core/api/XmrKeyImagePoller.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.api; import lombok.extern.slf4j.Slf4j; import monero.common.MoneroError; import monero.common.TaskLooper; import monero.daemon.MoneroDaemonRpc; import monero.daemon.model.MoneroKeyImageSpentStatus; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import haveno.core.trade.HavenoUtils; /** * Poll for changes to the spent status of key images. */ @Slf4j public class XmrKeyImagePoller { private MoneroDaemonRpc monerod; private long refreshPeriodMs; private Object lock = new Object(); private Map> keyImageGroups = new HashMap>(); private LinkedHashSet keyImagePollQueue = new LinkedHashSet<>(); private Set listeners = new HashSet(); private TaskLooper looper; private Map lastStatuses = new HashMap(); private boolean isPolling = false; private Long lastLogPollErrorTimestamp; private static final int MAX_POLL_SIZE = 200; /** * Construct the listener. */ public XmrKeyImagePoller() { looper = new TaskLooper(() -> poll()); } /** * Construct the listener. * * @param monerod - the Monero daemon to poll * @param refreshPeriodMs - refresh period in milliseconds */ public XmrKeyImagePoller(MoneroDaemonRpc monerod, long refreshPeriodMs) { looper = new TaskLooper(() -> poll()); setMonerod(monerod); setRefreshPeriodMs(refreshPeriodMs); } /** * Add a listener to receive notifications. * * @param listener - the listener to add */ public void addListener(XmrKeyImageListener listener) { synchronized (lock) { listeners.add(listener); refreshPolling(); } } /** * Remove a listener to receive notifications. * * @param listener - the listener to remove */ public void removeListener(XmrKeyImageListener listener) { synchronized (lock) { if (!listeners.contains(listener)) throw new MoneroError("Listener is not registered"); listeners.remove(listener); refreshPolling(); } } /** * Set the Monero daemon to fetch key images from. * * @param monerod - the daemon to fetch key images from */ public void setMonerod(MoneroDaemonRpc monerod) { this.monerod = monerod; } /** * Get the Monero daemon to fetch key images from. * * @return the daemon to fetch key images from */ public MoneroDaemonRpc getMonerod() { return monerod; } /** * Set the refresh period in milliseconds. * * @param refreshPeriodMs - the refresh period in milliseconds */ public void setRefreshPeriodMs(long refreshPeriodMs) { this.refreshPeriodMs = refreshPeriodMs; } /** * Get the refresh period in milliseconds * * @return the refresh period in milliseconds */ public long getRefreshPeriodMs() { return refreshPeriodMs; } /** * Add a key image to listen to. * * @param keyImage - the key image to listen to */ public void addKeyImage(String keyImage, String groupId) { addKeyImages(Arrays.asList(keyImage), groupId); } /** * Add key images to listen to. * * @param keyImages - key images to listen to */ public void addKeyImages(Collection keyImages, String groupId) { synchronized (lock) { if (!keyImageGroups.containsKey(groupId)) keyImageGroups.put(groupId, new HashSet()); Set keyImagesGroup = keyImageGroups.get(groupId); keyImagesGroup.addAll(keyImages); keyImagePollQueue.addAll(keyImages); refreshPolling(); } } /** * Remove key images to listen to. * * @param keyImages - key images to unlisten to */ public void removeKeyImages(Collection keyImages, String groupId) { synchronized (lock) { Set keyImagesGroup = keyImageGroups.get(groupId); if (keyImagesGroup == null) return; keyImagesGroup.removeAll(keyImages); if (keyImagesGroup.isEmpty()) keyImageGroups.remove(groupId); Set allKeyImages = getKeyImages(); for (String keyImage : keyImages) { if (!allKeyImages.contains(keyImage)) { keyImagePollQueue.remove(keyImage); lastStatuses.remove(keyImage); } } refreshPolling(); } } public void removeKeyImages(String groupId) { synchronized (lock) { Set keyImagesGroup = keyImageGroups.get(groupId); if (keyImagesGroup == null) return; keyImageGroups.remove(groupId); Set allKeyImages = getKeyImages(); for (String keyImage : keyImagesGroup) { if (!allKeyImages.contains(keyImage)) { keyImagePollQueue.remove(keyImage); lastStatuses.remove(keyImage); } } refreshPolling(); } } /** * Clear the key images which stops polling. */ public void clearKeyImages() { synchronized (lock) { keyImageGroups.clear(); keyImagePollQueue.clear(); lastStatuses.clear(); refreshPolling(); } } /** * Indicates if the given key image is spent. * * @param keyImage - the key image to check * @return true if the key is spent, false if unspent, null if unknown */ public Boolean isSpent(String keyImage) { synchronized (lock) { if (!lastStatuses.containsKey(keyImage)) return null; return XmrKeyImagePoller.isSpent(lastStatuses.get(keyImage)); } } /** * Indicates if the given key image spent status is spent. * * @param status the key image spent status to check * @return true if the key image is spent, false if unspent */ public static boolean isSpent(MoneroKeyImageSpentStatus status) { return status != MoneroKeyImageSpentStatus.NOT_SPENT; } /** * Get the last known spent status for the given key image. * * @param keyImage the key image to get the spent status for * @return the last known spent status of the key image */ public MoneroKeyImageSpentStatus getLastSpentStatus(String keyImage) { synchronized (lock) { return lastStatuses.get(keyImage); } } public void poll() { // skip until all services initialized if (!HavenoUtils.isAllDomainServicesInitialized()) { return; } // skip if monerod is null if (monerod == null) { log.warn("Cannot poll key images because monerod is null"); return; } // fetch spent statuses List spentStatuses = null; List keyImages = new ArrayList(getNextKeyImageBatch()); try { // update connection timeout if (monerod.getRpcConnection() != null) { monerod.getRpcConnection().setTimeout(XmrConnectionService.getTimeoutMs(monerod.getRpcConnection())); } // query key images spentStatuses = keyImages.isEmpty() ? new ArrayList() : monerod.getKeyImageSpentStatuses(keyImages); // TODO monero-java: if order of getKeyImageSpentStatuses is guaranteed, then it should take list parameter } catch (Exception e) { // limit error logging if (lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS) { log.warn("Error polling spent status of key images: " + e.getMessage()); lastLogPollErrorTimestamp = System.currentTimeMillis(); } return; } // process spent statuses Map changedStatuses = new HashMap(); synchronized (lock) { Set allKeyImages = getKeyImages(); for (int i = 0; i < keyImages.size(); i++) { // skip if key image is removed if (!allKeyImages.contains(keyImages.get(i))) continue; // move key image to the end of the queue keyImagePollQueue.remove(keyImages.get(i)); keyImagePollQueue.add(keyImages.get(i)); // update spent status if (spentStatuses.get(i) != lastStatuses.get(keyImages.get(i))) { lastStatuses.put(keyImages.get(i), spentStatuses.get(i)); changedStatuses.put(keyImages.get(i), spentStatuses.get(i)); } } } // announce changes if (!changedStatuses.isEmpty()) { List listeners; synchronized (lock) { listeners = new ArrayList(this.listeners); } for (XmrKeyImageListener listener : listeners) { listener.onSpentStatusChanged(changedStatuses); } } } private void refreshPolling() { synchronized (lock) { setIsPolling(!getKeyImages().isEmpty() && listeners.size() > 0); } } private synchronized void setIsPolling(boolean enabled) { if (enabled) { if (!isPolling) { isPolling = true; // TODO: use looper.isStarted(), synchronize looper.start(refreshPeriodMs); } } else { isPolling = false; looper.stop(); } } private Set getKeyImages() { Set allKeyImages = new HashSet(); synchronized (lock) { for (Set keyImagesGroup : keyImageGroups.values()) { allKeyImages.addAll(keyImagesGroup); } } return allKeyImages; } private List getNextKeyImageBatch() { synchronized (lock) { List keyImageBatch = new ArrayList<>(); int count = 0; for (String keyImage : keyImagePollQueue) { if (count >= MAX_POLL_SIZE) break; keyImageBatch.add(keyImage); count++; } return keyImageBatch; } } } ================================================ FILE: core/src/main/java/haveno/core/api/XmrLocalNode.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.api; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; import haveno.common.util.Utilities; import haveno.core.trade.HavenoUtils; import haveno.core.user.Preferences; import haveno.core.xmr.XmrNodeSettings; import haveno.core.xmr.nodes.XmrNodes; import haveno.core.xmr.nodes.XmrNodes.XmrNode; import haveno.core.xmr.nodes.XmrNodesSetupPreferences; import haveno.core.xmr.wallet.XmrWalletService; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import lombok.extern.slf4j.Slf4j; import monero.common.MoneroRpcConnection; import monero.common.MoneroUtils; import monero.common.TaskLooper; import monero.daemon.MoneroDaemonRpc; import monero.daemon.model.MoneroDaemonInfo; /** * Start and stop or connect to a local Monero node. */ @Slf4j @Singleton public class XmrLocalNode { // constants public static final long REFRESH_PERIOD_LOCAL_MS = 5000; // refresh period for local node public static final String MONEROD_NAME = Utilities.isWindows() ? "monerod.exe" : "monerod"; public static final String MONEROD_PATH = XmrWalletService.MONERO_BINS_DIR + File.separator + MONEROD_NAME; private static final String MONEROD_DATADIR = Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_LOCAL ? XmrWalletService.MONERO_BINS_DIR + File.separator + Config.baseCurrencyNetwork().toString().toLowerCase() + File.separator + "node1" : null; // use default directory unless local // instance fields private MoneroDaemonRpc daemon; private final Config config; private final Preferences preferences; private final XmrNodes xmrNodes; private final List listeners = new ArrayList<>(); private TaskLooper monerodPoller; private Boolean lastOnline = null; private Boolean lastAuthenticated = null; private Boolean lastSyncedWithinTolerance = null; // required arguments private static final List MONEROD_ARGS = new ArrayList(); static { MONEROD_ARGS.add(MONEROD_PATH); MONEROD_ARGS.add("--no-igd"); MONEROD_ARGS.add("--hide-my-port"); MONEROD_ARGS.add("--p2p-bind-ip"); MONEROD_ARGS.add(HavenoUtils.LOOPBACK_HOST); if (!Config.baseCurrencyNetwork().isMainnet()) MONEROD_ARGS.add("--" + Config.baseCurrencyNetwork().getNetwork().toLowerCase()); } @Inject public XmrLocalNode(Config config, Preferences preferences, XmrNodes xmrNodes) { this.config = config; this.preferences = preferences; this.xmrNodes = xmrNodes; this.daemon = new MoneroDaemonRpc(getUri()); } public void startPolling() { if (monerodPoller != null) monerodPoller.stop(); monerodPoller = new TaskLooper(() -> pollMonerod()); monerodPoller.start(REFRESH_PERIOD_LOCAL_MS); } private void pollMonerod() { // skip if connection manager is already polling default local connection MoneroRpcConnection connection = HavenoUtils.xmrConnectionService.getConnection(); if (connection != null && equalsUri(connection.getUri())) { return; } // check connection checkConnection(); // determine if connection changed Boolean isOnline = daemon.getRpcConnection().isOnline(); Boolean isAuthenticated = daemon.getRpcConnection().isAuthenticated(); MoneroDaemonInfo lastInfo = XmrConnectionService.getCachedDaemonInfo(daemon.getRpcConnection()); Boolean isSyncedWithinTolerance = null; if (lastInfo != null) isSyncedWithinTolerance = XmrConnectionService.isSyncedWithinTolerance(lastInfo); boolean change = lastOnline != isOnline || lastAuthenticated != isAuthenticated || lastSyncedWithinTolerance != isSyncedWithinTolerance; // update cached state lastOnline = isOnline; lastAuthenticated = isAuthenticated; lastSyncedWithinTolerance = isSyncedWithinTolerance; // announce if connection changed if (change) { for (var listener : listeners) listener.onConnectionChanged(daemon.getRpcConnection()); } } public String getUri() { return "http://" + HavenoUtils.LOOPBACK_HOST + ":" + HavenoUtils.getDefaultMoneroPort(); } /** * Returns whether Haveno should use a local Monero node, meaning that a node was * detected and conditions under which it should be ignored have not been met. If * the local node should be ignored, a call to this method will not trigger an * unnecessary detection attempt. */ public boolean shouldBeUsed() { return !shouldBeIgnored() && isDetected(); } /** * Returns whether Haveno should ignore a local Monero node even if it is usable. */ public boolean shouldBeIgnored() { if (config.ignoreLocalXmrNode) return true; // ignore if fixed connection is not local if (!"".equals(config.xmrNode)) return !HavenoUtils.isLocalHost(config.xmrNode); // check if local node is within configuration boolean hasConfiguredLocalNode = false; for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) { if (node.hasClearNetAddress() && equalsUri(node.getClearNetUri())) { hasConfiguredLocalNode = true; break; } } return !hasConfiguredLocalNode; } public void addListener(XmrLocalNodeListener listener) { listeners.add(listener); } public boolean removeListener(XmrLocalNodeListener listener) { return listeners.remove(listener); } /** * Return the client of the local Monero node. */ public MoneroDaemonRpc getDaemon() { return daemon; } public boolean equalsUri(String uri) { try { return HavenoUtils.isLocalHost(uri) && MoneroUtils.parseUri(uri).getPort() == HavenoUtils.getDefaultMoneroPort(); } catch (Exception e) { return false; } } /** * Check if local Monero node is detected. */ public boolean isDetected() { checkConnection(); return Boolean.TRUE.equals(daemon.getRpcConnection().isOnline()); } /** * Check if connected to local Monero node. */ public boolean isConnected() { return Boolean.TRUE.equals(daemon.getRpcConnection().isConnected()); } private void checkConnection() { XmrConnectionService.checkConnection(daemon.getRpcConnection()); } public XmrNodeSettings getNodeSettings() { return preferences.getXmrNodeSettings(); } /** * Start a local Monero node from settings. */ public void start() throws IOException { var settings = preferences.getXmrNodeSettings(); this.start(settings); } /** * Start local Monero node. Throws MoneroError if the node cannot be started. * Persist the settings to preferences if the node started successfully. */ public void start(XmrNodeSettings settings) throws IOException { if (isDetected()) throw new IllegalStateException("Local Monero node already online"); log.info("Starting local Monero node: " + settings); var args = new ArrayList<>(MONEROD_ARGS); var dataDir = ""; if (config.xmrBlockchainPath == null || config.xmrBlockchainPath.isEmpty()) { dataDir = settings.getBlockchainPath(); if (dataDir == null || dataDir.isEmpty()) { dataDir = MONEROD_DATADIR; } } else { dataDir = config.xmrBlockchainPath; // startup config overrides settings } if (dataDir != null && !dataDir.isEmpty()) { args.add("--data-dir=" + dataDir); } var bootstrapUrl = settings.getBootstrapUrl(); if (bootstrapUrl != null && !bootstrapUrl.isEmpty()) { args.add("--bootstrap-daemon-address=" + bootstrapUrl); } var syncBlockchain = settings.getSyncBlockchain(); if (syncBlockchain != null && !syncBlockchain) { args.add("--no-sync"); } var flags = settings.getStartupFlags(); if (flags != null) { args.addAll(flags); } daemon = new MoneroDaemonRpc(args); // start daemon as process and re-assign client preferences.setXmrNodeSettings(settings); for (var listener : listeners) listener.onNodeStarted(daemon); } /** * Stop the current local Monero node if we own its process. * Does not remove the last XmrNodeSettings. */ public void stop() { if (!isDetected()) throw new IllegalStateException("Local Monero node is not running"); if (daemon.getProcess() == null || !daemon.getProcess().isAlive()) throw new IllegalStateException("Cannot stop local Monero node because we don't own its process"); // TODO (woodser): remove isAlive() check after monero-java 0.5.4 which nullifies internal process daemon.stopProcess(); for (var listener : listeners) listener.onNodeStopped(); } } ================================================ FILE: core/src/main/java/haveno/core/api/XmrLocalNodeListener.java ================================================ /* * 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 . */ package haveno.core.api; import monero.common.MoneroRpcConnection; import monero.daemon.MoneroDaemonRpc; public class XmrLocalNodeListener { public void onNodeStarted(MoneroDaemonRpc daemon) {} public void onNodeStopped() {} public void onConnectionChanged(MoneroRpcConnection connection) {} } ================================================ FILE: core/src/main/java/haveno/core/api/model/AddressBalanceInfo.java ================================================ /* * 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 . */ package haveno.core.api.model; import haveno.common.Payload; public class AddressBalanceInfo implements Payload { private final String address; private final long balance; // address' balance in satoshis private final long numConfirmations; // # confirmations for address' most recent tx private final boolean isAddressUnused; public AddressBalanceInfo(String address, long balance, long numConfirmations, boolean isAddressUnused) { this.address = address; this.balance = balance; this.numConfirmations = numConfirmations; this.isAddressUnused = isAddressUnused; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public haveno.proto.grpc.AddressBalanceInfo toProtoMessage() { return haveno.proto.grpc.AddressBalanceInfo.newBuilder() .setAddress(address) .setBalance(balance) .setNumConfirmations(numConfirmations) .setIsAddressUnused(isAddressUnused) .build(); } public static AddressBalanceInfo fromProto(haveno.proto.grpc.AddressBalanceInfo proto) { return new AddressBalanceInfo(proto.getAddress(), proto.getBalance(), proto.getNumConfirmations(), proto.getIsAddressUnused()); } @Override public String toString() { return "AddressBalanceInfo{" + "address='" + address + '\'' + ", balance=" + balance + ", numConfirmations=" + numConfirmations + ", isAddressUnused=" + isAddressUnused + '}'; } } ================================================ FILE: core/src/main/java/haveno/core/api/model/BalancesInfo.java ================================================ package haveno.core.api.model; import haveno.common.Payload; import lombok.Getter; @Getter public class BalancesInfo implements Payload { // Getter names are shortened for readability's sake, i.e., // balancesInfo.getBtc().getAvailableBalance() is cleaner than // balancesInfo.getBtcBalanceInfo().getAvailableBalance(). private final BtcBalanceInfo btc; private final XmrBalanceInfo xmr; public BalancesInfo(BtcBalanceInfo btc, XmrBalanceInfo xmr) { this.btc = btc; this.xmr = xmr; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public haveno.proto.grpc.BalancesInfo toProtoMessage() { return haveno.proto.grpc.BalancesInfo.newBuilder() .setBtc(btc.toProtoMessage()) .setXmr(xmr.toProtoMessage()) .build(); } public static BalancesInfo fromProto(haveno.proto.grpc.BalancesInfo proto) { return new BalancesInfo( BtcBalanceInfo.fromProto(proto.getBtc()), XmrBalanceInfo.fromProto(proto.getXmr())); } @Override public String toString() { return "BalancesInfo{" + "\n" + " " + btc.toString() + "\n" + ", " + xmr.toString() + "\n" + '}'; } } ================================================ FILE: core/src/main/java/haveno/core/api/model/BtcBalanceInfo.java ================================================ package haveno.core.api.model; import com.google.common.annotations.VisibleForTesting; import haveno.common.Payload; import lombok.Getter; @Getter public class BtcBalanceInfo implements Payload { public static final BtcBalanceInfo EMPTY = new BtcBalanceInfo(-1, -1, -1, -1); // All balances are in BTC satoshis. private final long availableBalance; private final long reservedBalance; private final long totalAvailableBalance; // available + reserved private final long lockedBalance; public BtcBalanceInfo(long availableBalance, long reservedBalance, long totalAvailableBalance, long lockedBalance) { this.availableBalance = availableBalance; this.reservedBalance = reservedBalance; this.totalAvailableBalance = totalAvailableBalance; this.lockedBalance = lockedBalance; } @VisibleForTesting public static BtcBalanceInfo valueOf(long availableBalance, long reservedBalance, long totalAvailableBalance, long lockedBalance) { // Convenience for creating a model instance instead of a proto. return new BtcBalanceInfo(availableBalance, reservedBalance, totalAvailableBalance, lockedBalance); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public haveno.proto.grpc.BtcBalanceInfo toProtoMessage() { return haveno.proto.grpc.BtcBalanceInfo.newBuilder() .setAvailableBalance(availableBalance) .setReservedBalance(reservedBalance) .setTotalAvailableBalance(totalAvailableBalance) .setLockedBalance(lockedBalance) .build(); } public static BtcBalanceInfo fromProto(haveno.proto.grpc.BtcBalanceInfo proto) { return new BtcBalanceInfo(proto.getAvailableBalance(), proto.getReservedBalance(), proto.getTotalAvailableBalance(), proto.getLockedBalance()); } @Override public String toString() { return "BtcBalanceInfo{" + "availableBalance=" + availableBalance + ", reservedBalance=" + reservedBalance + ", totalAvailableBalance=" + totalAvailableBalance + ", lockedBalance=" + lockedBalance + '}'; } } ================================================ FILE: core/src/main/java/haveno/core/api/model/ContractInfo.java ================================================ /* * 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 . */ package haveno.core.api.model; import haveno.common.Payload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.proto.CoreProtoResolver; import lombok.Getter; import java.util.function.Supplier; /** * A lightweight Trade Contract constructed from a trade's json contract. * Many fields in the core Contract are ignored, but can be added as needed. */ @Getter public class ContractInfo implements Payload { private final String buyerNodeAddress; private final String sellerNodeAddress; private final String arbitratorNodeAddress; private final boolean isBuyerMakerAndSellerTaker; private final String makerAccountId; private final String takerAccountId; private final PaymentAccountPayload makerPaymentAccountPayload; private final PaymentAccountPayload takerPaymentAccountPayload; private final String makerPayoutAddressString; private final String takerPayoutAddressString; public ContractInfo(String buyerNodeAddress, String sellerNodeAddress, String arbitratorNodeAddress, boolean isBuyerMakerAndSellerTaker, String makerAccountId, String takerAccountId, PaymentAccountPayload makerPaymentAccountPayload, PaymentAccountPayload takerPaymentAccountPayload, String makerPayoutAddressString, String takerPayoutAddressString) { this.buyerNodeAddress = buyerNodeAddress; this.sellerNodeAddress = sellerNodeAddress; this.arbitratorNodeAddress = arbitratorNodeAddress; this.isBuyerMakerAndSellerTaker = isBuyerMakerAndSellerTaker; this.makerAccountId = makerAccountId; this.takerAccountId = takerAccountId; this.makerPaymentAccountPayload = makerPaymentAccountPayload; this.takerPaymentAccountPayload = takerPaymentAccountPayload; this.makerPayoutAddressString = makerPayoutAddressString; this.takerPayoutAddressString = takerPayoutAddressString; } // For transmitting TradeInfo messages when no contract is available. public static Supplier emptyContract = () -> new ContractInfo("", "", "", false, "", "", null, null, "", ""); /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// public static ContractInfo fromProto(haveno.proto.grpc.ContractInfo proto) { CoreProtoResolver coreProtoResolver = new CoreProtoResolver(); return new ContractInfo(proto.getBuyerNodeAddress(), proto.getSellerNodeAddress(), proto.getArbitratorNodeAddress(), proto.getIsBuyerMakerAndSellerTaker(), proto.getMakerAccountId(), proto.getTakerAccountId(), proto.getMakerPaymentAccountPayload() == null ? null : PaymentAccountPayload.fromProto(proto.getMakerPaymentAccountPayload(), coreProtoResolver), proto.getTakerPaymentAccountPayload() == null ? null : PaymentAccountPayload.fromProto(proto.getTakerPaymentAccountPayload(), coreProtoResolver), proto.getMakerPayoutAddressString(), proto.getTakerPayoutAddressString()); } @Override public haveno.proto.grpc.ContractInfo toProtoMessage() { haveno.proto.grpc.ContractInfo.Builder builder = haveno.proto.grpc.ContractInfo.newBuilder() .setBuyerNodeAddress(buyerNodeAddress) .setSellerNodeAddress(sellerNodeAddress) .setArbitratorNodeAddress(arbitratorNodeAddress) .setIsBuyerMakerAndSellerTaker(isBuyerMakerAndSellerTaker) .setMakerAccountId(makerAccountId) .setTakerAccountId(takerAccountId) .setMakerPayoutAddressString(makerPayoutAddressString) .setTakerPayoutAddressString(takerPayoutAddressString); if (makerPaymentAccountPayload != null) builder.setMakerPaymentAccountPayload((protobuf.PaymentAccountPayload) makerPaymentAccountPayload.toProtoMessage()); if (takerPaymentAccountPayload != null) builder.setTakerPaymentAccountPayload((protobuf.PaymentAccountPayload) takerPaymentAccountPayload.toProtoMessage()); return builder.build(); } } ================================================ FILE: core/src/main/java/haveno/core/api/model/EncryptedConnection.java ================================================ package haveno.core.api.model; import com.google.protobuf.ByteString; import haveno.common.proto.persistable.PersistablePayload; import lombok.Builder; import lombok.Value; @Value @Builder(toBuilder = true) public class EncryptedConnection implements PersistablePayload { String url; String username; byte[] encryptedPassword; byte[] encryptionSalt; int priority; @Override public protobuf.EncryptedConnection toProtoMessage() { return protobuf.EncryptedConnection.newBuilder() .setUrl(url) .setUsername(username) .setEncryptedPassword(ByteString.copyFrom(encryptedPassword)) .setEncryptionSalt(ByteString.copyFrom(encryptionSalt)) .setPriority(priority) .build(); } public static EncryptedConnection fromProto(protobuf.EncryptedConnection encryptedConnection) { return new EncryptedConnection( encryptedConnection.getUrl(), encryptedConnection.getUsername(), encryptedConnection.getEncryptedPassword().toByteArray(), encryptedConnection.getEncryptionSalt().toByteArray(), encryptedConnection.getPriority()); } } ================================================ FILE: core/src/main/java/haveno/core/api/model/MarketDepthInfo.java ================================================ /* * 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 . */ package haveno.core.api.model; import lombok.AllArgsConstructor; import lombok.ToString; import java.util.Arrays; @ToString @AllArgsConstructor public class MarketDepthInfo { public final String currencyCode; public final Double[] buyPrices; public final Double[] buyDepth; public final Double[] sellPrices; public final Double[] sellDepth; /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// // @Override public haveno.proto.grpc.MarketDepthInfo toProtoMessage() { return haveno.proto.grpc.MarketDepthInfo.newBuilder() .setCurrencyCode(currencyCode) .addAllBuyPrices(Arrays.asList(buyPrices)) .addAllBuyDepth(Arrays.asList((buyDepth))) .addAllSellPrices(Arrays.asList(sellPrices)) .addAllSellDepth(Arrays.asList(sellDepth)) .build(); } } ================================================ FILE: core/src/main/java/haveno/core/api/model/MarketPriceInfo.java ================================================ /* * 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 . */ package haveno.core.api.model; import haveno.common.Payload; import lombok.AllArgsConstructor; import lombok.ToString; @ToString @AllArgsConstructor public class MarketPriceInfo implements Payload { private final String currencyCode; private final double price; /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public haveno.proto.grpc.MarketPriceInfo toProtoMessage() { return haveno.proto.grpc.MarketPriceInfo.newBuilder() .setPrice(price) .setCurrencyCode(currencyCode) .build(); } public static MarketPriceInfo fromProto(haveno.proto.grpc.MarketPriceInfo proto) { return new MarketPriceInfo(proto.getCurrencyCode(), proto.getPrice()); } } ================================================ FILE: core/src/main/java/haveno/core/api/model/OfferInfo.java ================================================ /* * 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 . */ package haveno.core.api.model; import haveno.common.Payload; import haveno.core.api.model.builder.OfferInfoBuilder; import haveno.core.locale.CountryUtil; import haveno.core.monetary.Price; import haveno.core.offer.Offer; import haveno.core.offer.OpenOffer; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; import javax.annotation.Nullable; import java.util.Optional; import static haveno.core.util.PriceUtil.reformatMarketPrice; import static haveno.core.util.VolumeUtil.formatVolume; import static java.util.Objects.requireNonNull; import java.util.List; @EqualsAndHashCode @ToString @Getter public class OfferInfo implements Payload { // The client cannot see haveno.core.Offer or its fromProto method. We use the lighter // weight OfferInfo proto wrapper instead, containing just enough fields to view, // create and take offers. private final String id; private final String direction; private final String price; private final boolean useMarketBasedPrice; private final double marketPriceMarginPct; private final long amount; private final long minAmount; private final String volume; private final String minVolume; private final double makerFeePct; private final double takerFeePct; private final double penaltyFeePct; private final double buyerSecurityDepositPct; private final double sellerSecurityDepositPct; private final String triggerPrice; private final String paymentAccountId; private final String paymentMethodId; private final String paymentMethodShortName; private final String baseCurrencyCode; private final String counterCurrencyCode; private final long date; private final String state; private final boolean isActivated; private final boolean isMyOffer; private final String ownerNodeAddress; private final String pubKeyRing; private final String versionNumber; private final int protocolVersion; @Nullable private final String arbitratorSigner; @Nullable private final String splitOutputTxHash; private final long splitOutputTxFee; private final boolean isPrivateOffer; private final String challenge; private final String extraInfo; private final List acceptedCountryCodes; private final String acceptedCountriesString; private final String city; public OfferInfo(OfferInfoBuilder builder) { this.id = builder.getId(); this.direction = builder.getDirection(); this.price = builder.getPrice(); this.useMarketBasedPrice = builder.isUseMarketBasedPrice(); this.marketPriceMarginPct = builder.getMarketPriceMarginPct(); this.amount = builder.getAmount(); this.minAmount = builder.getMinAmount(); this.makerFeePct = builder.getMakerFeePct(); this.takerFeePct = builder.getTakerFeePct(); this.penaltyFeePct = builder.getPenaltyFeePct(); this.buyerSecurityDepositPct = builder.getBuyerSecurityDepositPct(); this.sellerSecurityDepositPct = builder.getSellerSecurityDepositPct(); this.volume = builder.getVolume(); this.minVolume = builder.getMinVolume(); this.triggerPrice = builder.getTriggerPrice(); this.paymentAccountId = builder.getPaymentAccountId(); this.paymentMethodId = builder.getPaymentMethodId(); this.paymentMethodShortName = builder.getPaymentMethodShortName(); this.baseCurrencyCode = builder.getBaseCurrencyCode(); this.counterCurrencyCode = builder.getCounterCurrencyCode(); this.date = builder.getDate(); this.state = builder.getState(); this.isActivated = builder.isActivated(); this.isMyOffer = builder.isMyOffer(); this.ownerNodeAddress = builder.getOwnerNodeAddress(); this.pubKeyRing = builder.getPubKeyRing(); this.versionNumber = builder.getVersionNumber(); this.protocolVersion = builder.getProtocolVersion(); this.arbitratorSigner = builder.getArbitratorSigner(); this.splitOutputTxHash = builder.getSplitOutputTxHash(); this.splitOutputTxFee = builder.getSplitOutputTxFee(); this.isPrivateOffer = builder.isPrivateOffer(); this.challenge = builder.getChallenge(); this.extraInfo = builder.getExtraInfo(); this.acceptedCountryCodes = builder.getAcceptedCountryCodes(); this.acceptedCountriesString = builder.getAcceptedCountriesString(); this.city = builder.getCity(); } public static OfferInfo toOfferInfo(Offer offer) { return getBuilder(offer) .withIsMyOffer(false) .withIsActivated(true) .build(); } public static OfferInfo toMyOfferInfo(OpenOffer openOffer) { // An OpenOffer is always my offer. var offer = openOffer.getOffer(); var currencyCode = offer.getCounterCurrencyCode(); var isActivated = !openOffer.isDeactivated(); Optional optionalTriggerPrice = openOffer.getTriggerPrice() > 0 ? Optional.of(Price.valueOf(currencyCode, openOffer.getTriggerPrice())) : Optional.empty(); var preciseTriggerPrice = optionalTriggerPrice .map(value -> reformatMarketPrice(value.toPlainString(), currencyCode)) .orElse("0"); return getBuilder(offer) .withTriggerPrice(preciseTriggerPrice) .withState(openOffer.getState().name()) .withIsActivated(isActivated) .withSplitOutputTxHash(openOffer.getSplitOutputTxHash()) .withSplitOutputTxFee(openOffer.getSplitOutputTxFee()) .withChallenge(openOffer.getChallenge()) .withIsMyOffer(true) .build(); } private static OfferInfoBuilder getBuilder(Offer offer) { // OfferInfo protos are passed to API client, and some field // values are converted to displayable, unambiguous form. var currencyCode = offer.getCounterCurrencyCode(); var preciseOfferPrice = reformatMarketPrice( requireNonNull(offer.getPrice()).toPlainString(), currencyCode); var roundedVolume = formatVolume(requireNonNull(offer.getVolume())); var roundedMinVolume = formatVolume(requireNonNull(offer.getMinVolume())); boolean hasAcceptedCountries = offer.getAcceptedCountryCodes() != null && !offer.getAcceptedCountryCodes().isEmpty(); String city = offer.getF2FCity(); return new OfferInfoBuilder() .withId(offer.getId()) .withDirection(offer.getDirection().name()) .withPrice(preciseOfferPrice) .withUseMarketBasedPrice(offer.isUseMarketBasedPrice()) .withMarketPriceMarginPct(offer.getMarketPriceMarginPct()) .withAmount(offer.getAmount().longValueExact()) .withMinAmount(offer.getMinAmount().longValueExact()) .withMakerFeePct(offer.getMakerFeePct()) .withTakerFeePct(offer.getTakerFeePct()) .withPenaltyFeePct(offer.getPenaltyFeePct()) .withSellerSecurityDepositPct(offer.getSellerSecurityDepositPct()) .withBuyerSecurityDepositPct(offer.getBuyerSecurityDepositPct()) .withSellerSecurityDepositPct(offer.getSellerSecurityDepositPct()) .withVolume(roundedVolume) .withMinVolume(roundedMinVolume) .withPaymentAccountId(offer.getMakerPaymentAccountId()) .withPaymentMethodId(offer.getPaymentMethod().getId()) .withPaymentMethodShortName(offer.getPaymentMethod().getShortName()) .withBaseCurrencyCode(offer.getBaseCurrencyCode()) .withCounterCurrencyCode(offer.getCounterCurrencyCode()) .withDate(offer.getDate().getTime()) .withState(offer.getState().name()) .withOwnerNodeAddress(offer.getOfferPayload().getOwnerNodeAddress().getFullAddress()) .withPubKeyRing(offer.getOfferPayload().getPubKeyRing().toString()) .withVersionNumber(offer.getOfferPayload().getVersionNr()) .withProtocolVersion(offer.getOfferPayload().getProtocolVersion()) .withArbitratorSigner(offer.getOfferPayload().getArbitratorSigner() == null ? null : offer.getOfferPayload().getArbitratorSigner().getFullAddress()) .withIsPrivateOffer(offer.isPrivateOffer()) .withChallenge(offer.getChallenge()) .withExtraInfo(offer.getCombinedExtraInfo()) .withAcceptedCountryCodes(hasAcceptedCountries ? offer.getAcceptedCountryCodes() : null) .withAcceptedCountriesString(hasAcceptedCountries ? CountryUtil.getCountriesString(offer.getAcceptedCountryCodes()) : null) .withCity(city == null || city.isEmpty() ? null : city); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public haveno.proto.grpc.OfferInfo toProtoMessage() { haveno.proto.grpc.OfferInfo.Builder builder = haveno.proto.grpc.OfferInfo.newBuilder() .setId(id) .setDirection(direction) .setPrice(price) .setUseMarketBasedPrice(useMarketBasedPrice) .setMarketPriceMarginPct(marketPriceMarginPct) .setAmount(amount) .setMinAmount(minAmount) .setVolume(volume) .setMinVolume(minVolume) .setMakerFeePct(makerFeePct) .setTakerFeePct(takerFeePct) .setPenaltyFeePct(penaltyFeePct) .setBuyerSecurityDepositPct(buyerSecurityDepositPct) .setSellerSecurityDepositPct(sellerSecurityDepositPct) .setTriggerPrice(triggerPrice == null ? "0" : triggerPrice) .setPaymentAccountId(paymentAccountId) .setPaymentMethodId(paymentMethodId) .setPaymentMethodShortName(paymentMethodShortName) .setBaseCurrencyCode(baseCurrencyCode) .setCounterCurrencyCode(counterCurrencyCode) .setDate(date) .setState(state) .setIsActivated(isActivated) .setIsMyOffer(isMyOffer) .setOwnerNodeAddress(ownerNodeAddress) .setPubKeyRing(pubKeyRing) .setVersionNr(versionNumber) .setProtocolVersion(protocolVersion) .setSplitOutputTxFee(splitOutputTxFee) .setIsPrivateOffer(isPrivateOffer); Optional.ofNullable(arbitratorSigner).ifPresent(builder::setArbitratorSigner); Optional.ofNullable(splitOutputTxHash).ifPresent(builder::setSplitOutputTxHash); Optional.ofNullable(challenge).ifPresent(builder::setChallenge); Optional.ofNullable(extraInfo).ifPresent(builder::setExtraInfo); Optional.ofNullable(acceptedCountryCodes).ifPresent(e -> builder.addAllAcceptedCountryCodes(acceptedCountryCodes)); Optional.ofNullable(acceptedCountriesString).ifPresent(builder::setAcceptedCountriesString); Optional.ofNullable(city).ifPresent(builder::setCity); return builder.build(); } @SuppressWarnings("unused") public static OfferInfo fromProto(haveno.proto.grpc.OfferInfo proto) { return new OfferInfoBuilder() .withId(proto.getId()) .withDirection(proto.getDirection()) .withPrice(proto.getPrice()) .withUseMarketBasedPrice(proto.getUseMarketBasedPrice()) .withMarketPriceMarginPct(proto.getMarketPriceMarginPct()) .withAmount(proto.getAmount()) .withMinAmount(proto.getMinAmount()) .withVolume(proto.getVolume()) .withMinVolume(proto.getMinVolume()) .withMakerFeePct(proto.getMakerFeePct()) .withTakerFeePct(proto.getTakerFeePct()) .withPenaltyFeePct(proto.getPenaltyFeePct()) .withBuyerSecurityDepositPct(proto.getBuyerSecurityDepositPct()) .withSellerSecurityDepositPct(proto.getSellerSecurityDepositPct()) .withTriggerPrice(proto.getTriggerPrice()) .withPaymentAccountId(proto.getPaymentAccountId()) .withPaymentMethodId(proto.getPaymentMethodId()) .withPaymentMethodShortName(proto.getPaymentMethodShortName()) .withBaseCurrencyCode(proto.getBaseCurrencyCode()) .withCounterCurrencyCode(proto.getCounterCurrencyCode()) .withDate(proto.getDate()) .withState(proto.getState()) .withIsActivated(proto.getIsActivated()) .withIsMyOffer(proto.getIsMyOffer()) .withOwnerNodeAddress(proto.getOwnerNodeAddress()) .withPubKeyRing(proto.getPubKeyRing()) .withVersionNumber(proto.getVersionNr()) .withProtocolVersion(proto.getProtocolVersion()) .withArbitratorSigner(proto.getArbitratorSigner()) .withSplitOutputTxHash(proto.getSplitOutputTxHash()) .withSplitOutputTxFee(proto.getSplitOutputTxFee()) .withIsPrivateOffer(proto.getIsPrivateOffer()) .withChallenge(proto.getChallenge()) .withExtraInfo(proto.getExtraInfo()) .withAcceptedCountryCodes(proto.getAcceptedCountryCodesList()) .withAcceptedCountriesString(proto.getAcceptedCountriesString()) .withCity(proto.getCity()) .build(); } } ================================================ FILE: core/src/main/java/haveno/core/api/model/PaymentAccountForm.java ================================================ /* * 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 . */ package haveno.core.api.model; import com.google.common.annotations.VisibleForTesting; import com.google.gson.Gson; import haveno.common.proto.ProtoUtil; import haveno.common.proto.persistable.PersistablePayload; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccountFactory; import haveno.core.payment.payload.PaymentMethod; import haveno.core.trade.HavenoUtils; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import javax.annotation.concurrent.Immutable; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkNotNull; import static java.lang.String.format; import static java.lang.System.getProperty; import static java.nio.charset.StandardCharsets.UTF_8; @Getter @Immutable @EqualsAndHashCode @ToString @Slf4j public final class PaymentAccountForm implements PersistablePayload { public enum FormId { BLOCK_CHAINS, CASH_AT_ATM, FASTER_PAYMENTS, F2F, MONEY_GRAM, PAXUM, PAY_BY_MAIL, REVOLUT, SEPA, SEPA_INSTANT, STRIKE, SWIFT, TRANSFERWISE, UPHOLD, ZELLE, AUSTRALIA_PAYID, CASH_APP, PAYPAL, VENMO, PAYSAFE, WECHAT_PAY, ALI_PAY, SWISH, TRANSFERWISE_USD, AMAZON_GIFT_CARD, ACH_TRANSFER, INTERAC_E_TRANSFER, US_POSTAL_MONEY_ORDER, PIX; public static PaymentAccountForm.FormId fromProto(protobuf.PaymentAccountForm.FormId formId) { return ProtoUtil.enumFromProto(PaymentAccountForm.FormId.class, formId.name()); } public static protobuf.PaymentAccountForm.FormId toProtoMessage(PaymentAccountForm.FormId formId) { return protobuf.PaymentAccountForm.FormId.valueOf(formId.name()); } } private final FormId id; private final List fields; public PaymentAccountForm(FormId id) { this.id = id; this.fields = new ArrayList(); } public PaymentAccountForm(FormId id, List fields) { this.id = id; this.fields = fields; } @Override public protobuf.PaymentAccountForm toProtoMessage() { return protobuf.PaymentAccountForm.newBuilder() .setId(PaymentAccountForm.FormId.toProtoMessage(id)) .addAllFields(fields.stream().map(field -> field.toProtoMessage()).collect(Collectors.toList())) .build(); } public static PaymentAccountForm fromProto(protobuf.PaymentAccountForm proto) { List fields = proto.getFieldsList().isEmpty() ? null : proto.getFieldsList().stream().map(PaymentAccountFormField::fromProto).collect(Collectors.toList()); return new PaymentAccountForm(FormId.fromProto(proto.getId()), fields); } public void addField(PaymentAccountFormField field) { fields.add(field); } public String getValue(PaymentAccountFormField.FieldId fieldId) { for (PaymentAccountFormField field : fields) { if (field.getId() == fieldId) { return field.getValue(); } } throw new IllegalArgumentException("Form does not contain field " + fieldId); } /** * Convert this form to a PaymentAccount json string. */ public String toPaymentAccountJsonString() { Map formMap = new HashMap(); formMap.put("paymentMethodId", getId().toString()); for (PaymentAccountFormField field : getFields()) { formMap.put(HavenoUtils.toCamelCase(field.getId().toString()), field.getValue()); } return new Gson().toJson(formMap); } /** * Convert this form to a PaymentAccount. */ public PaymentAccount toPaymentAccount() { return PaymentAccount.fromJson(toPaymentAccountJsonString()); } /** * Get a structured form for the given payment method. */ public static PaymentAccountForm getForm(String paymentMethodId) { PaymentAccount paymentAccount = PaymentAccountFactory.getPaymentAccount(PaymentMethod.getPaymentMethod(paymentMethodId)); return paymentAccount.toForm(); } // ----------------------------- OLD FORM API ----------------------------- /** * Returns a blank payment account form (json) for the given paymentMethodId. * * @param paymentMethodId Determines what kind of json form to return. * @return A uniquely named tmp file used to define new payment account details. */ public static File getPaymentAccountForm(String paymentMethodId) { PaymentMethod paymentMethod = PaymentMethod.getPaymentMethod(paymentMethodId); File file = getTmpJsonFile(paymentMethodId); try (OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(checkNotNull(file), false), UTF_8)) { PaymentAccount paymentAccount = PaymentAccountFactory.getPaymentAccount(paymentMethod); String json = paymentAccount.toForm().toPaymentAccountJsonString(); outputStreamWriter.write(json); } catch (Exception ex) { String errMsg = format("cannot create a payment account form for a %s payment method", paymentMethodId); log.error(StringUtils.capitalize(errMsg) + ".", ex); throw new IllegalStateException(errMsg); } return file; } /** * De-serialize a PaymentAccount json form into a new PaymentAccount instance. * * @param jsonForm The file representing a new payment account form. * @return A populated PaymentAccount subclass instance. */ @SuppressWarnings("unused") @VisibleForTesting public static PaymentAccount toPaymentAccount(File jsonForm) { return PaymentAccount.fromJson(toJsonString(jsonForm)); } public static String toJsonString(File jsonFile) { try { checkNotNull(jsonFile, "json file cannot be null"); return new String(Files.readAllBytes(Paths.get(jsonFile.getAbsolutePath()))); } catch (IOException ex) { String errMsg = format("cannot read json string from file '%s'", jsonFile.getAbsolutePath()); log.error(StringUtils.capitalize(errMsg) + ".", ex); throw new IllegalStateException(errMsg); } } @VisibleForTesting public static URI getClickableURI(File jsonFile) { try { return new URI("file", "", jsonFile.toURI().getPath(), null, null); } catch (URISyntaxException ex) { String errMsg = format("cannot create clickable url to file '%s'", jsonFile.getAbsolutePath()); log.error(StringUtils.capitalize(errMsg) + ".", ex); throw new IllegalStateException(errMsg); } } @VisibleForTesting public static File getTmpJsonFile(String paymentMethodId) { File file; try { // Creates a tmp file that includes a random number string between the // prefix and suffix, i.e., sepa_form_13243546575879.json, so there is // little chance this will fail because the tmp file already exists. file = File.createTempFile(paymentMethodId.toLowerCase() + "_form_", ".json", Paths.get(getProperty("java.io.tmpdir")).toFile()); } catch (IOException ex) { String errMsg = format("cannot create json file for a %s payment method", paymentMethodId); log.error(StringUtils.capitalize(errMsg) + ".", ex); throw new IllegalStateException(errMsg); } return file; } } ================================================ FILE: core/src/main/java/haveno/core/api/model/PaymentAccountFormField.java ================================================ /* * 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 . */ package haveno.core.api.model; import haveno.common.proto.ProtoUtil; import haveno.common.proto.persistable.PersistablePayload; import haveno.core.locale.Country; import haveno.core.locale.TradeCurrency; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @Getter @Setter @Immutable @EqualsAndHashCode @ToString public final class PaymentAccountFormField implements PersistablePayload { public enum FieldId { ADDRESS, ACCEPTED_COUNTRY_CODES, ACCOUNT_ID, ACCOUNT_NAME, ACCOUNT_NR, ACCOUNT_OWNER, ACCOUNT_TYPE, ANSWER, BANK_ACCOUNT_NAME, BANK_ACCOUNT_NUMBER, BANK_ACCOUNT_TYPE, BANK_ADDRESS, BANK_BRANCH, BANK_BRANCH_CODE, BANK_BRANCH_NAME, BANK_CODE, BANK_COUNTRY_CODE, BANK_ID, BANK_NAME, BANK_SWIFT_CODE, BENEFICIARY_ACCOUNT_NR, BENEFICIARY_ADDRESS, BENEFICIARY_CITY, BENEFICIARY_NAME, BENEFICIARY_PHONE, BIC, BRANCH_ID, CITY, CONTACT, COUNTRY, EMAIL, EMAIL_OR_MOBILE_NR, EXTRA_INFO, HOLDER_ADDRESS, HOLDER_EMAIL, HOLDER_NAME, HOLDER_TAX_ID, IBAN, IFSC, INTERMEDIARY_ADDRESS, INTERMEDIARY_BRANCH, INTERMEDIARY_COUNTRY_CODE, INTERMEDIARY_NAME, INTERMEDIARY_SWIFT_CODE, MOBILE_NR, NATIONAL_ACCOUNT_ID, PAYID, PIX_KEY, POSTAL_ADDRESS, PROMPT_PAY_ID, QUESTION, REQUIREMENTS, SALT, SORT_CODE, SPECIAL_INSTRUCTIONS, STATE, TRADE_CURRENCIES, USERNAME, EMAIL_OR_MOBILE_NR_OR_USERNAME, EMAIL_OR_MOBILE_NR_OR_CASHTAG; public static PaymentAccountFormField.FieldId fromProto(protobuf.PaymentAccountFormField.FieldId fieldId) { return ProtoUtil.enumFromProto(PaymentAccountFormField.FieldId.class, fieldId.name()); } public static protobuf.PaymentAccountFormField.FieldId toProtoMessage(PaymentAccountFormField.FieldId fieldId) { return protobuf.PaymentAccountFormField.FieldId.valueOf(fieldId.name()); } } public enum Component { TEXT, TEXTAREA, SELECT_ONE, SELECT_MULTIPLE; public static PaymentAccountFormField.Component fromProto(protobuf.PaymentAccountFormField.Component component) { return ProtoUtil.enumFromProto(PaymentAccountFormField.Component.class, component.name()); } public static protobuf.PaymentAccountFormField.Component toProtoMessage(PaymentAccountFormField.Component component) { return protobuf.PaymentAccountFormField.Component.valueOf(component.name()); } } private FieldId id; private Component component; @Nullable private String type; private String label; private String value; private int minLength; private int maxLength; private List supportedCurrencies; private List supportedCountries; private List supportedSepaEuroCountries; private List supportedSepaNonEuroCountries; private List requiredForCountries; public PaymentAccountFormField(FieldId id) { this.id = id; } @Override public protobuf.PaymentAccountFormField toProtoMessage() { protobuf.PaymentAccountFormField.Builder builder = protobuf.PaymentAccountFormField.newBuilder() .setId(PaymentAccountFormField.FieldId.toProtoMessage(id)) .setComponent(PaymentAccountFormField.Component.toProtoMessage(component)) .setMinLength(minLength) .setMaxLength(maxLength); Optional.ofNullable(type).ifPresent(builder::setType); Optional.ofNullable(label).ifPresent(builder::setLabel); Optional.ofNullable(value).ifPresent(builder::setValue); Optional.ofNullable(supportedCurrencies).ifPresent(e -> builder.addAllSupportedCurrencies(ProtoUtil.collectionToProto(supportedCurrencies, protobuf.TradeCurrency.class))); Optional.ofNullable(supportedCountries).ifPresent(e -> builder.addAllSupportedCountries(ProtoUtil.collectionToProto(supportedCountries, protobuf.Country.class))); Optional.ofNullable(supportedSepaEuroCountries).ifPresent(e -> builder.addAllSupportedSepaEuroCountries(ProtoUtil.collectionToProto(supportedSepaEuroCountries, protobuf.Country.class))); Optional.ofNullable(supportedSepaNonEuroCountries).ifPresent(e -> builder.addAllSupportedSepaNonEuroCountries(ProtoUtil.collectionToProto(supportedSepaNonEuroCountries, protobuf.Country.class))); Optional.ofNullable(requiredForCountries).ifPresent(builder::addAllRequiredForCountries); return builder.build(); } public static PaymentAccountFormField fromProto(protobuf.PaymentAccountFormField proto) { PaymentAccountFormField formField = new PaymentAccountFormField(FieldId.fromProto(proto.getId())); formField.type = proto.getType(); formField.label = proto.getLabel(); formField.value = proto.getValue(); formField.minLength = proto.getMinLength(); formField.maxLength = proto.getMaxLength(); formField.supportedCountries = proto.getSupportedCountriesList().isEmpty() ? null : proto.getSupportedCountriesList().stream().map(Country::fromProto).collect(Collectors.toList()); formField.supportedSepaEuroCountries = proto.getSupportedSepaEuroCountriesList().isEmpty() ? null : proto.getSupportedSepaEuroCountriesList().stream().map(Country::fromProto).collect(Collectors.toList()); formField.supportedSepaNonEuroCountries = proto.getSupportedSepaNonEuroCountriesList().isEmpty() ? null : proto.getSupportedSepaNonEuroCountriesList().stream().map(Country::fromProto).collect(Collectors.toList()); formField.requiredForCountries = proto.getRequiredForCountriesList() == null ? null : new ArrayList(proto.getRequiredForCountriesList()); return formField; } } ================================================ FILE: core/src/main/java/haveno/core/api/model/TradeInfo.java ================================================ /* * 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 . */ package haveno.core.api.model; import haveno.common.Payload; import haveno.core.api.model.builder.TradeInfoV1Builder; import haveno.core.trade.Contract; import haveno.core.trade.Trade; import haveno.core.trade.TradeUtil; import lombok.EqualsAndHashCode; import lombok.Getter; import java.util.function.Function; import static haveno.core.api.model.OfferInfo.toOfferInfo; import static haveno.core.util.PriceUtil.reformatMarketPrice; import static haveno.core.util.VolumeUtil.formatVolume; import static java.util.Objects.requireNonNull; @EqualsAndHashCode @Getter public class TradeInfo implements Payload { // The client cannot see haveno.core.trade.Trade or its fromProto method. We use the // lighter weight TradeInfo proto wrapper instead, containing just enough fields to // view and interact with trades. private static final Function toPeerNodeAddress = (trade) -> trade.getTradePeerNodeAddress() == null ? "" : trade.getTradePeerNodeAddress().getFullAddress(); private static final Function toArbitratorNodeAddress = (trade) -> trade.getArbitratorNodeAddress() == null ? "" : trade.getArbitratorNodeAddress().getFullAddress(); private static final Function toRoundedVolume = (trade) -> trade.getVolume() == null ? "" : formatVolume(requireNonNull(trade.getVolume())); private static final Function toPreciseTradePrice = (trade) -> reformatMarketPrice(requireNonNull(trade.getPrice()).toPlainString(), trade.getOffer().getCounterCurrencyCode()); // Haveno v1 trade protocol fields (some are in common with the BSQ Swap protocol). private final OfferInfo offer; private final String tradeId; private final String shortId; private final long date; private final String role; private final String makerDepositTxId; private final String takerDepositTxId; private final String payoutTxId; private final long amount; private final long makerFee; private final long takerFee; private final long buyerSecurityDeposit; private final long sellerSecurityDeposit; private final long buyerDepositTxFee; private final long sellerDepositTxFee; private final long buyerPayoutTxFee; private final long sellerPayoutTxFee; private final long buyerPayoutAmount; private final long sellerPayoutAmount; private final String price; private final String volume; private final String arbitratorNodeAddress; private final String tradePeerNodeAddress; private final String state; private final String phase; private final String periodState; private final String payoutState; private final String disputeState; private final boolean isDepositsPublished; private final boolean isDepositsConfirmed; private final boolean isDepositsUnlocked; private final boolean isDepositsFinalized; private final boolean isPaymentSent; private final boolean isPaymentReceived; private final boolean isPayoutPublished; private final boolean isPayoutConfirmed; private final boolean isPayoutUnlocked; private final boolean isPayoutFinalized; private final boolean isCompleted; private final String contractAsJson; private final ContractInfo contract; private final long startTime; private final long maxDurationMs; private final long deadlineTime; public TradeInfo(TradeInfoV1Builder builder) { this.offer = builder.getOffer(); this.tradeId = builder.getTradeId(); this.shortId = builder.getShortId(); this.date = builder.getDate(); this.role = builder.getRole(); this.makerDepositTxId = builder.getMakerDepositTxId(); this.takerDepositTxId = builder.getTakerDepositTxId(); this.payoutTxId = builder.getPayoutTxId(); this.amount = builder.getAmount(); this.makerFee = builder.getMakerFee(); this.takerFee = builder.getTakerFee(); this.buyerSecurityDeposit = builder.getBuyerSecurityDeposit(); this.sellerSecurityDeposit = builder.getSellerSecurityDeposit(); this.buyerDepositTxFee = builder.getBuyerDepositTxFee(); this.sellerDepositTxFee = builder.getSellerDepositTxFee(); this.buyerPayoutTxFee = builder.getBuyerPayoutTxFee(); this.sellerPayoutTxFee = builder.getSellerPayoutTxFee(); this.buyerPayoutAmount = builder.getBuyerPayoutAmount(); this.sellerPayoutAmount = builder.getSellerPayoutAmount(); this.price = builder.getPrice(); this.volume = builder.getVolume(); this.arbitratorNodeAddress = builder.getArbitratorNodeAddress(); this.tradePeerNodeAddress = builder.getTradePeerNodeAddress(); this.state = builder.getState(); this.phase = builder.getPhase(); this.periodState = builder.getPeriodState(); this.payoutState = builder.getPayoutState(); this.disputeState = builder.getDisputeState(); this.isDepositsPublished = builder.isDepositsPublished(); this.isDepositsConfirmed = builder.isDepositsConfirmed(); this.isDepositsUnlocked = builder.isDepositsUnlocked(); this.isDepositsFinalized = builder.isDepositsFinalized(); this.isPaymentSent = builder.isPaymentSent(); this.isPaymentReceived = builder.isPaymentReceived(); this.isPayoutPublished = builder.isPayoutPublished(); this.isPayoutConfirmed = builder.isPayoutConfirmed(); this.isPayoutUnlocked = builder.isPayoutUnlocked(); this.isPayoutFinalized = builder.isPayoutFinalized(); this.isCompleted = builder.isCompleted(); this.contractAsJson = builder.getContractAsJson(); this.contract = builder.getContract(); this.startTime = builder.getStartTime(); this.maxDurationMs = builder.getMaxDurationMs(); this.deadlineTime = builder.getDeadlineTime(); } public static TradeInfo toTradeInfo(Trade trade) { String role = TradeUtil.getRole(trade); ContractInfo contractInfo; if (trade.getContract() != null) { Contract contract = trade.getContract(); contractInfo = new ContractInfo(contract.getBuyerPayoutAddressString(), contract.getSellerPayoutAddressString(), contract.getArbitratorNodeAddress().getFullAddress(), contract.isBuyerMakerAndSellerTaker(), contract.getMakerAccountId(), contract.getTakerAccountId(), trade.getMaker().getPaymentAccountPayload(), trade.getTaker().getPaymentAccountPayload(), contract.getMakerPayoutAddressString(), contract.getTakerPayoutAddressString()); } else { contractInfo = ContractInfo.emptyContract.get(); } return new TradeInfoV1Builder() .withTradeId(trade.getId()) .withShortId(trade.getShortId()) .withDate(trade.getDate().getTime()) .withRole(role == null ? "" : role) .withMakerDepositTxId(trade.getMaker().getDepositTxHash()) .withTakerDepositTxId(trade.getTaker().getDepositTxHash()) .withPayoutTxId(trade.getPayoutTxId()) .withAmount(trade.getAmount().longValueExact()) .withMakerFee(trade.getMakerFee().longValueExact()) .withTakerFee(trade.getTakerFee().longValueExact()) .withBuyerSecurityDeposit(trade.getBuyer().getSecurityDeposit().longValueExact()) .withSellerSecurityDeposit(trade.getSeller().getSecurityDeposit().longValueExact()) .withBuyerDepositTxFee(trade.getBuyer().getDepositTxFee().longValueExact()) .withSellerDepositTxFee(trade.getSeller().getDepositTxFee().longValueExact()) .withBuyerPayoutTxFee(trade.getBuyer().getPayoutTxFee().longValueExact()) .withSellerPayoutTxFee(trade.getSeller().getPayoutTxFee().longValueExact()) .withBuyerPayoutAmount(trade.getBuyer().getPayoutAmount().longValueExact()) .withSellerPayoutAmount(trade.getSeller().getPayoutAmount().longValueExact()) .withTotalTxFee(trade.getTotalTxFee().longValueExact()) .withPrice(toPreciseTradePrice.apply(trade)) .withVolume(toRoundedVolume.apply(trade)) .withArbitratorNodeAddress(toArbitratorNodeAddress.apply(trade)) .withTradePeerNodeAddress(toPeerNodeAddress.apply(trade)) .withState(trade.getState().name()) .withPhase(trade.getPhase().name()) .withPeriodState(trade.getPeriodState().name()) .withPayoutState(trade.getPayoutState().name()) .withDisputeState(trade.getDisputeState().name()) .withIsDepositsPublished(trade.isDepositsPublished()) .withIsDepositsConfirmed(trade.isDepositsConfirmed()) .withIsDepositsUnlocked(trade.isDepositsUnlocked()) .withIsDepositsFinalized(trade.isDepositsFinalized()) .withIsPaymentSent(trade.isPaymentSent()) .withIsPaymentReceived(trade.isPaymentReceived()) .withIsPayoutPublished(trade.isPayoutPublished()) .withIsPayoutConfirmed(trade.isPayoutConfirmed()) .withIsPayoutUnlocked(trade.isPayoutUnlocked()) .withIsPayoutFinalized(trade.isPayoutFinalized()) .withIsCompleted(trade.isCompleted()) .withContractAsJson(trade.getContractAsJson()) .withContract(contractInfo) .withOffer(toOfferInfo(trade.getOffer())) .withStartTime(trade.getStartDate().getTime()) .withMaxDurationMs(trade.getMaxTradePeriod()) .withDeadlineTime(trade.getMaxTradePeriodDate().getTime()) .build(); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public haveno.proto.grpc.TradeInfo toProtoMessage() { return haveno.proto.grpc.TradeInfo.newBuilder() .setOffer(offer.toProtoMessage()) .setTradeId(tradeId) .setShortId(shortId) .setDate(date) .setRole(role) .setMakerDepositTxId(makerDepositTxId == null ? "" : makerDepositTxId) .setTakerDepositTxId(takerDepositTxId == null ? "" : takerDepositTxId) .setPayoutTxId(payoutTxId == null ? "" : payoutTxId) .setAmount(amount) .setMakerFee(makerFee) .setTakerFee(takerFee) .setBuyerSecurityDeposit(buyerSecurityDeposit) .setSellerSecurityDeposit(sellerSecurityDeposit) .setBuyerDepositTxFee(buyerDepositTxFee) .setSellerDepositTxFee(sellerDepositTxFee) .setBuyerPayoutTxFee(buyerPayoutTxFee) .setSellerPayoutTxFee(sellerPayoutTxFee) .setBuyerPayoutAmount(buyerPayoutAmount) .setSellerPayoutAmount(sellerPayoutAmount) .setPrice(price) .setTradeVolume(volume) .setArbitratorNodeAddress(arbitratorNodeAddress) .setTradePeerNodeAddress(tradePeerNodeAddress) .setState(state) .setPhase(phase) .setPeriodState(periodState) .setPayoutState(payoutState) .setDisputeState(disputeState) .setIsDepositsPublished(isDepositsPublished) .setIsDepositsConfirmed(isDepositsConfirmed) .setIsDepositsUnlocked(isDepositsUnlocked) .setIsDepositsFinalized(isDepositsFinalized) .setIsPaymentSent(isPaymentSent) .setIsPaymentReceived(isPaymentReceived) .setIsCompleted(isCompleted) .setIsPayoutPublished(isPayoutPublished) .setIsPayoutConfirmed(isPayoutConfirmed) .setIsPayoutUnlocked(isPayoutUnlocked) .setIsPayoutFinalized(isPayoutFinalized) .setContractAsJson(contractAsJson == null ? "" : contractAsJson) .setContract(contract.toProtoMessage()) .setStartTime(startTime) .setMaxDurationMs(maxDurationMs) .setDeadlineTime(deadlineTime) .build(); } public static TradeInfo fromProto(haveno.proto.grpc.TradeInfo proto) { return new TradeInfoV1Builder() .withOffer(OfferInfo.fromProto(proto.getOffer())) .withTradeId(proto.getTradeId()) .withShortId(proto.getShortId()) .withDate(proto.getDate()) .withRole(proto.getRole()) .withMakerDepositTxId(proto.getMakerDepositTxId()) .withTakerDepositTxId(proto.getTakerDepositTxId()) .withPayoutTxId(proto.getPayoutTxId()) .withAmount(proto.getAmount()) .withMakerFee(proto.getMakerFee()) .withTakerFee(proto.getTakerFee()) .withBuyerSecurityDeposit(proto.getBuyerSecurityDeposit()) .withSellerSecurityDeposit(proto.getSellerSecurityDeposit()) .withBuyerDepositTxFee(proto.getBuyerDepositTxFee()) .withSellerDepositTxFee(proto.getSellerDepositTxFee()) .withBuyerPayoutTxFee(proto.getBuyerPayoutTxFee()) .withSellerPayoutTxFee(proto.getSellerPayoutTxFee()) .withBuyerPayoutAmount(proto.getBuyerPayoutAmount()) .withSellerPayoutAmount(proto.getSellerPayoutAmount()) .withPrice(proto.getPrice()) .withVolume(proto.getTradeVolume()) .withPeriodState(proto.getPeriodState()) .withPayoutState(proto.getPayoutState()) .withDisputeState(proto.getDisputeState()) .withState(proto.getState()) .withPhase(proto.getPhase()) .withArbitratorNodeAddress(proto.getArbitratorNodeAddress()) .withTradePeerNodeAddress(proto.getTradePeerNodeAddress()) .withIsDepositsPublished(proto.getIsDepositsPublished()) .withIsDepositsConfirmed(proto.getIsDepositsConfirmed()) .withIsDepositsUnlocked(proto.getIsDepositsUnlocked()) .withIsDepositsFinalized(proto.getIsDepositsFinalized()) .withIsPaymentSent(proto.getIsPaymentSent()) .withIsPaymentReceived(proto.getIsPaymentReceived()) .withIsCompleted(proto.getIsCompleted()) .withIsPayoutPublished(proto.getIsPayoutPublished()) .withIsPayoutConfirmed(proto.getIsPayoutConfirmed()) .withIsPayoutUnlocked(proto.getIsPayoutUnlocked()) .withIsPayoutFinalized(proto.getIsPayoutFinalized()) .withContractAsJson(proto.getContractAsJson()) .withContract((ContractInfo.fromProto(proto.getContract()))) .withStartTime(proto.getStartTime()) .withMaxDurationMs(proto.getMaxDurationMs()) .withDeadlineTime(proto.getDeadlineTime()) .build(); } @Override public String toString() { return "TradeInfo{" + " tradeId='" + tradeId + '\'' + "\n" + ", shortId='" + shortId + '\'' + "\n" + ", date='" + date + '\'' + "\n" + ", role='" + role + '\'' + "\n" + ", makerDepositTxId='" + makerDepositTxId + '\'' + "\n" + ", takerDepositTxId='" + takerDepositTxId + '\'' + "\n" + ", payoutTxId='" + payoutTxId + '\'' + "\n" + ", amount='" + amount + '\'' + "\n" + ", makerFee='" + makerFee + '\'' + "\n" + ", takerFee='" + takerFee + '\'' + "\n" + ", buyerSecurityDeposit='" + buyerSecurityDeposit + '\'' + "\n" + ", sellerSecurityDeposit='" + sellerSecurityDeposit + '\'' + "\n" + ", buyerDepositTxFee='" + buyerDepositTxFee + '\'' + "\n" + ", sellerDepositTxFee='" + sellerDepositTxFee + '\'' + "\n" + ", buyerPayoutTxFee='" + buyerPayoutTxFee + '\'' + "\n" + ", sellerPayoutTxFee='" + sellerPayoutTxFee + '\'' + "\n" + ", buyerPayoutAmount='" + buyerPayoutAmount + '\'' + "\n" + ", sellerPayoutAmount='" + sellerPayoutAmount + '\'' + "\n" + ", price='" + price + '\'' + "\n" + ", arbitratorNodeAddress='" + arbitratorNodeAddress + '\'' + "\n" + ", tradePeerNodeAddress='" + tradePeerNodeAddress + '\'' + "\n" + ", state='" + state + '\'' + "\n" + ", phase='" + phase + '\'' + "\n" + ", periodState='" + periodState + '\'' + "\n" + ", payoutState='" + payoutState + '\'' + "\n" + ", disputeState='" + disputeState + '\'' + "\n" + ", isDepositsPublished=" + isDepositsPublished + "\n" + ", isDepositsConfirmed=" + isDepositsConfirmed + "\n" + ", isDepositsUnlocked=" + isDepositsUnlocked + "\n" + ", isDepositsFinalized=" + isDepositsFinalized + "\n" + ", isPaymentSent=" + isPaymentSent + "\n" + ", isPaymentReceived=" + isPaymentReceived + "\n" + ", isPayoutPublished=" + isPayoutPublished + "\n" + ", isPayoutConfirmed=" + isPayoutConfirmed + "\n" + ", isPayoutUnlocked=" + isPayoutUnlocked + "\n" + ", isPayoutFinalized=" + isPayoutFinalized + "\n" + ", isCompleted=" + isCompleted + "\n" + ", offer=" + offer + "\n" + ", contractAsJson=" + contractAsJson + "\n" + ", contract=" + contract + "\n" + ", startTime=" + startTime + "\n" + ", maxDurationMs=" + maxDurationMs + "\n" + ", deadlineTime=" + deadlineTime + "\n" + '}'; } } ================================================ FILE: core/src/main/java/haveno/core/api/model/XmrBalanceInfo.java ================================================ package haveno.core.api.model; import java.math.BigInteger; import com.google.common.annotations.VisibleForTesting; import haveno.common.Payload; public class XmrBalanceInfo implements Payload { public static final XmrBalanceInfo EMPTY = new XmrBalanceInfo(-1, -1, -1, -1, -1); // all balances are in atomic units private final long balance; private final long availableBalance; private final long pendingBalance; private final long reservedOfferBalance; private final long reservedTradeBalance; private final long reservedBalance; public XmrBalanceInfo(long balance, long unlockedBalance, long pendingBalance, long reservedOfferBalance, long reservedTradeBalance) { this.balance = balance; this.availableBalance = unlockedBalance; this.pendingBalance = pendingBalance; this.reservedOfferBalance = reservedOfferBalance; this.reservedTradeBalance = reservedTradeBalance; this.reservedBalance = reservedOfferBalance + reservedTradeBalance; } @VisibleForTesting public static XmrBalanceInfo valueOf(long balance, long availableBalance, long pendingBalance, long reservedOfferBalance, long reservedTradeBalance) { return new XmrBalanceInfo(balance, availableBalance, pendingBalance, reservedOfferBalance, reservedTradeBalance); } public BigInteger getBalance() { return BigInteger.valueOf(balance); } public BigInteger getAvailableBalance() { return BigInteger.valueOf(availableBalance); } public BigInteger getPendingBalance() { return BigInteger.valueOf(pendingBalance); } public BigInteger getReservedOfferBalance() { return BigInteger.valueOf(reservedOfferBalance); } public BigInteger getReservedTradeBalance() { return BigInteger.valueOf(reservedTradeBalance); } public BigInteger getReservedBalance() { return BigInteger.valueOf(reservedBalance); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public haveno.proto.grpc.XmrBalanceInfo toProtoMessage() { return haveno.proto.grpc.XmrBalanceInfo.newBuilder() .setBalance(balance) .setAvailableBalance(availableBalance) .setPendingBalance(pendingBalance) .setReservedOfferBalance(reservedOfferBalance) .setReservedTradeBalance(reservedTradeBalance) .build(); } public static XmrBalanceInfo fromProto(haveno.proto.grpc.XmrBalanceInfo proto) { return new XmrBalanceInfo(proto.getBalance(), proto.getAvailableBalance(), proto.getPendingBalance(), proto.getReservedOfferBalance(), proto.getReservedTradeBalance()); } @Override public String toString() { return "XmrBalanceInfo{" + "balance=" + balance + ", unlockedBalance=" + availableBalance + ", lockedBalance=" + pendingBalance + ", reservedOfferBalance=" + reservedOfferBalance + ", reservedTradeBalance=" + reservedTradeBalance + '}'; } } ================================================ FILE: core/src/main/java/haveno/core/api/model/XmrDestination.java ================================================ package haveno.core.api.model; import haveno.common.Payload; import lombok.Getter; import monero.wallet.model.MoneroDestination; import java.math.BigInteger; @Getter public class XmrDestination implements Payload { private final String address; private final BigInteger amount; public XmrDestination(XmrDestinationBuilder builder) { this.address = builder.address; this.amount = builder.amount; } public static XmrDestination toXmrDestination(MoneroDestination dst) { return new XmrDestinationBuilder() .withAddress(dst.getAddress()) .withAmount(dst.getAmount()) .build(); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public haveno.proto.grpc.XmrDestination toProtoMessage() { return haveno.proto.grpc.XmrDestination.newBuilder() .setAddress(address) .setAmount(amount.toString()) .build(); } public static XmrDestination fromProto(haveno.proto.grpc.XmrDestination proto) { return new XmrDestinationBuilder() .withAddress(proto.getAddress()) .withAmount(new BigInteger(proto.getAmount())) .build(); } public static class XmrDestinationBuilder { private String address; private BigInteger amount; public XmrDestinationBuilder withAddress(String address) { this.address = address; return this; } public XmrDestinationBuilder withAmount(BigInteger amount) { this.amount = amount; return this; } public XmrDestination build() { return new XmrDestination(this); } } @Override public String toString() { return "XmrDestination{" + "address=" + address + ", amount" + amount + '}'; } } ================================================ FILE: core/src/main/java/haveno/core/api/model/XmrIncomingTransfer.java ================================================ package haveno.core.api.model; import haveno.common.Payload; import lombok.Getter; import monero.wallet.model.MoneroIncomingTransfer; import java.math.BigInteger; @Getter public class XmrIncomingTransfer implements Payload { private final BigInteger amount; private final Integer accountIndex; private final Integer subaddressIndex; private final String address; private final Long numSuggestedConfirmations; public XmrIncomingTransfer(XmrIncomingTransferBuilder builder) { this.amount = builder.amount; this.accountIndex = builder.accountIndex; this.subaddressIndex = builder.subaddressIndex; this.address = builder.address; this.numSuggestedConfirmations = builder.numSuggestedConfirmations; } public static XmrIncomingTransfer toXmrIncomingTransfer(MoneroIncomingTransfer transfer) { return new XmrIncomingTransferBuilder() .withAmount(transfer.getAmount()) .withAccountIndex(transfer.getAccountIndex()) .withSubaddressIndex(transfer.getSubaddressIndex()) .withAddress(transfer.getAddress()) .withNumSuggestedConfirmations(transfer.getNumSuggestedConfirmations()) .build(); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public haveno.proto.grpc.XmrIncomingTransfer toProtoMessage() { return haveno.proto.grpc.XmrIncomingTransfer.newBuilder() .setAmount(amount.toString()) .setAccountIndex(accountIndex) .setSubaddressIndex(subaddressIndex) .setAddress(address) .setNumSuggestedConfirmations(numSuggestedConfirmations) .build(); } public static XmrIncomingTransfer fromProto(haveno.proto.grpc.XmrIncomingTransfer proto) { return new XmrIncomingTransferBuilder() .withAmount(new BigInteger(proto.getAmount())) .withAccountIndex(proto.getAccountIndex()) .withSubaddressIndex(proto.getSubaddressIndex()) .withAddress(proto.getAddress()) .withNumSuggestedConfirmations(proto.getNumSuggestedConfirmations()) .build(); } public static class XmrIncomingTransferBuilder { private BigInteger amount; private Integer accountIndex; private Integer subaddressIndex; private String address; private Long numSuggestedConfirmations; public XmrIncomingTransferBuilder withAmount(BigInteger amount) { this.amount = amount; return this; } public XmrIncomingTransferBuilder withAccountIndex(Integer accountIndex) { this.accountIndex = accountIndex; return this; } public XmrIncomingTransferBuilder withSubaddressIndex(Integer subaddressIndex) { this.subaddressIndex = subaddressIndex; return this; } public XmrIncomingTransferBuilder withAddress(String address) { this.address = address; return this; } public XmrIncomingTransferBuilder withNumSuggestedConfirmations(Long numSuggestedConfirmations) { this.numSuggestedConfirmations = numSuggestedConfirmations; return this; } public XmrIncomingTransfer build() { return new XmrIncomingTransfer(this); } } @Override public String toString() { return "XmrIncomingTransfer{" + "amount=" + amount + ", accountIndex=" + accountIndex + ", subaddressIndex=" + subaddressIndex + ", address=" + address + ", numSuggestedConfirmations=" + numSuggestedConfirmations + '}'; } } ================================================ FILE: core/src/main/java/haveno/core/api/model/XmrOutgoingTransfer.java ================================================ package haveno.core.api.model; import haveno.common.Payload; import haveno.common.proto.ProtoUtil; import lombok.Getter; import monero.wallet.model.MoneroOutgoingTransfer; import javax.annotation.Nullable; import java.math.BigInteger; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import static haveno.core.api.model.XmrDestination.toXmrDestination; @Getter public class XmrOutgoingTransfer implements Payload { private final BigInteger amount; private final Integer accountIndex; @Nullable private final List subaddressIndices; @Nullable private final List destinations; public XmrOutgoingTransfer(XmrOutgoingTransferBuilder builder) { this.amount = builder.amount; this.accountIndex = builder.accountIndex; this.subaddressIndices = builder.subaddressIndices; this.destinations = builder.destinations; } public static XmrOutgoingTransfer toXmrOutgoingTransfer(MoneroOutgoingTransfer transfer) { List destinations = transfer.getDestinations() == null ? null : transfer.getDestinations().stream() .map(s -> toXmrDestination(s)) .collect(Collectors.toList()); XmrOutgoingTransferBuilder builder = new XmrOutgoingTransferBuilder() .withAmount(transfer.getAmount()) .withAccountIndex(transfer.getAccountIndex()) .withSubaddressIndices(transfer.getSubaddressIndices()) .withDestinations(destinations); return builder.build(); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public haveno.proto.grpc.XmrOutgoingTransfer toProtoMessage() { var builder = haveno.proto.grpc.XmrOutgoingTransfer.newBuilder() .setAmount(amount.toString()) .setAccountIndex(accountIndex); Optional.ofNullable(subaddressIndices).ifPresent(e -> builder.addAllSubaddressIndices(subaddressIndices)); Optional.ofNullable(destinations).ifPresent(e -> builder.addAllDestinations(ProtoUtil.collectionToProto(destinations, haveno.proto.grpc.XmrDestination.class))); return builder.build(); } public static XmrOutgoingTransfer fromProto(haveno.proto.grpc.XmrOutgoingTransfer proto) { List destinations = proto.getDestinationsList().isEmpty() ? null : proto.getDestinationsList().stream() .map(XmrDestination::fromProto).collect(Collectors.toList()); return new XmrOutgoingTransferBuilder() .withAmount(new BigInteger(proto.getAmount())) .withAccountIndex(proto.getAccountIndex()) .withSubaddressIndices(proto.getSubaddressIndicesList()) .withDestinations(destinations) .build(); } public static class XmrOutgoingTransferBuilder { private BigInteger amount; private Integer accountIndex; private List subaddressIndices; private List destinations; public XmrOutgoingTransferBuilder withAmount(BigInteger amount) { this.amount = amount; return this; } public XmrOutgoingTransferBuilder withAccountIndex(Integer accountIndex) { this.accountIndex = accountIndex; return this; } public XmrOutgoingTransferBuilder withSubaddressIndices(List subaddressIndices) { this.subaddressIndices = subaddressIndices; return this; } public XmrOutgoingTransferBuilder withDestinations(List destinations) { this.destinations = destinations; return this; } public XmrOutgoingTransfer build() { return new XmrOutgoingTransfer(this); } } @Override public String toString() { return "XmrOutgoingTransfer{" + "amount=" + amount + ", accountIndex=" + accountIndex + ", subaddressIndices=" + subaddressIndices + ", destinations=" + destinations + '}'; } } ================================================ FILE: core/src/main/java/haveno/core/api/model/XmrTx.java ================================================ package haveno.core.api.model; import haveno.common.Payload; import haveno.common.proto.ProtoUtil; import lombok.Getter; import monero.wallet.model.MoneroTxWallet; import javax.annotation.Nullable; import java.math.BigInteger; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import static haveno.core.api.model.XmrIncomingTransfer.toXmrIncomingTransfer; import static haveno.core.api.model.XmrOutgoingTransfer.toXmrOutgoingTransfer; @Getter public class XmrTx implements Payload { private final String hash; private final BigInteger fee; private final boolean isConfirmed; private final boolean isLocked; @Nullable private final Long height; @Nullable private final Long timestamp; @Nullable private final List incomingTransfers; @Nullable private final XmrOutgoingTransfer outgoingTransfer; @Nullable private final String metadata; public XmrTx(XmrTxBuilder builder) { this.hash = builder.hash; this.fee = builder.fee; this.isConfirmed = builder.isConfirmed; this.isLocked = builder.isLocked; this.height = builder.height; this.timestamp = builder.timestamp; this.incomingTransfers = builder.incomingTransfers; this.outgoingTransfer = builder.outgoingTransfer; this.metadata = builder.metadata; } public static XmrTx toXmrTx(MoneroTxWallet tx){ Long timestamp = tx.getBlock() == null ? null : tx.getBlock().getTimestamp(); List incomingTransfers = tx.getIncomingTransfers() == null ? null : tx.getIncomingTransfers().stream() .map(s -> toXmrIncomingTransfer(s)) .collect(Collectors.toList()); XmrOutgoingTransfer outgoingTransfer = tx.getOutgoingTransfer() == null ? null : toXmrOutgoingTransfer(tx.getOutgoingTransfer()); XmrTxBuilder builder = new XmrTxBuilder() .withHash(tx.getHash()) .withFee(tx.getFee()) .withIsConfirmed(tx.isConfirmed()) .withIsLocked(tx.isLocked()); Optional.ofNullable(tx.getHeight()).ifPresent(e ->builder.withHeight(tx.getHeight())); Optional.ofNullable(timestamp).ifPresent(e ->builder.withTimestamp(timestamp)); Optional.ofNullable(outgoingTransfer).ifPresent(e ->builder.withOutgoingTransfer(outgoingTransfer)); Optional.ofNullable(incomingTransfers).ifPresent(e ->builder.withIncomingTransfers(incomingTransfers)); Optional.ofNullable(tx.getMetadata()).ifPresent(e ->builder.withMetadata(tx.getMetadata())); return builder.build(); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public haveno.proto.grpc.XmrTx toProtoMessage() { haveno.proto.grpc.XmrTx.Builder builder = haveno.proto.grpc.XmrTx.newBuilder() .setHash(hash) .setFee(fee.toString()) .setIsConfirmed(isConfirmed) .setIsLocked(isLocked); Optional.ofNullable(height).ifPresent(e -> builder.setHeight(height)); Optional.ofNullable(timestamp).ifPresent(e -> builder.setTimestamp(timestamp)); Optional.ofNullable(outgoingTransfer).ifPresent(e -> builder.setOutgoingTransfer(outgoingTransfer.toProtoMessage())); Optional.ofNullable(incomingTransfers).ifPresent(e -> builder.addAllIncomingTransfers(ProtoUtil.collectionToProto(incomingTransfers, haveno.proto.grpc.XmrIncomingTransfer.class))); Optional.ofNullable(metadata).ifPresent(e -> builder.setMetadata(metadata)); return builder.build(); } public static XmrTx fromProto(haveno.proto.grpc.XmrTx proto) { return new XmrTxBuilder() .withHash(proto.getHash()) .withFee(new BigInteger(proto.getFee())) .withIsConfirmed(proto.getIsConfirmed()) .withIsLocked(proto.getIsLocked()) .withHeight(proto.getHeight()) .withTimestamp(proto.getTimestamp()) .withIncomingTransfers( proto.getIncomingTransfersList().stream() .map(XmrIncomingTransfer::fromProto) .collect(Collectors.toList())) .withOutgoingTransfer(XmrOutgoingTransfer.fromProto(proto.getOutgoingTransfer())) .withMetadata(proto.getMetadata()) .build(); } public static class XmrTxBuilder { private String hash; private BigInteger fee; private boolean isConfirmed; private boolean isLocked; private Long height; private Long timestamp; private List incomingTransfers; private XmrOutgoingTransfer outgoingTransfer; private String metadata; public XmrTxBuilder withHash(String hash) { this.hash = hash; return this; } public XmrTxBuilder withFee(BigInteger fee) { this.fee = fee; return this; } public XmrTxBuilder withIsConfirmed(boolean isConfirmed) { this.isConfirmed = isConfirmed; return this; } public XmrTxBuilder withIsLocked(boolean isLocked) { this.isLocked = isLocked; return this; } public XmrTxBuilder withHeight(Long height) { this.height = height; return this; } public XmrTxBuilder withTimestamp(Long timestamp) { this.timestamp = timestamp; return this; } public XmrTxBuilder withIncomingTransfers(List incomingTransfers) { this.incomingTransfers = incomingTransfers; return this; } public XmrTxBuilder withOutgoingTransfer(XmrOutgoingTransfer outgoingTransfer) { this.outgoingTransfer = outgoingTransfer; return this; } public XmrTxBuilder withMetadata(String metadata) { this.metadata = metadata; return this; } public XmrTx build() { return new XmrTx(this); } } @Override public String toString() { return "XmrTx{" + "hash=" + hash + ", fee=" + timestamp + ", isConfirmed=" + isConfirmed + ", isLocked=" + isLocked + ", height=" + height + ", timestamp=" + timestamp + ", incomingTransfers=" + incomingTransfers + ", outgoingTransfer=" + outgoingTransfer + ", metadata=" + metadata + '}'; } } ================================================ FILE: core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java ================================================ /* * 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 . */ package haveno.core.api.model.builder; import java.util.List; import haveno.core.api.model.OfferInfo; import lombok.Getter; /* * A builder helps avoid bungling use of a large OfferInfo constructor * argument list. If consecutive argument values of the same type are not * ordered correctly, the compiler won't complain but the resulting bugs could * be hard to find and fix. */ @Getter public final class OfferInfoBuilder { private String id; private String direction; private String price; private boolean useMarketBasedPrice; private double marketPriceMarginPct; private long amount; private long minAmount; private String volume; private String minVolume; private double makerFeePct; private double takerFeePct; private double penaltyFeePct; private double buyerSecurityDepositPct; private double sellerSecurityDepositPct; private String triggerPrice; private boolean isCurrencyForMakerFeeBtc; private String paymentAccountId; private String paymentMethodId; private String paymentMethodShortName; private String baseCurrencyCode; private String counterCurrencyCode; private long date; private String state; private boolean isActivated; private boolean isMyOffer; private boolean isMyPendingOffer; private boolean isBsqSwapOffer; private String ownerNodeAddress; private String pubKeyRing; private String versionNumber; private int protocolVersion; private String arbitratorSigner; private String splitOutputTxHash; private long splitOutputTxFee; private boolean isPrivateOffer; private String challenge; private String extraInfo; private List acceptedCountryCodes; private String acceptedCountriesString; private String city; public OfferInfoBuilder withId(String id) { this.id = id; return this; } public OfferInfoBuilder withDirection(String direction) { this.direction = direction; return this; } public OfferInfoBuilder withPrice(String price) { this.price = price; return this; } public OfferInfoBuilder withUseMarketBasedPrice(boolean useMarketBasedPrice) { this.useMarketBasedPrice = useMarketBasedPrice; return this; } public OfferInfoBuilder withMarketPriceMarginPct(double marketPriceMarginPct) { this.marketPriceMarginPct = marketPriceMarginPct; return this; } public OfferInfoBuilder withAmount(long amount) { this.amount = amount; return this; } public OfferInfoBuilder withMinAmount(long minAmount) { this.minAmount = minAmount; return this; } public OfferInfoBuilder withMakerFeePct(double makerFeePct) { this.makerFeePct = makerFeePct; return this; } public OfferInfoBuilder withTakerFeePct(double takerFeePct) { this.takerFeePct = takerFeePct; return this; } public OfferInfoBuilder withPenaltyFeePct(double penaltyFeePct) { this.penaltyFeePct = penaltyFeePct; return this; } public OfferInfoBuilder withBuyerSecurityDepositPct(double buyerSecurityDepositPct) { this.buyerSecurityDepositPct = buyerSecurityDepositPct; return this; } public OfferInfoBuilder withSellerSecurityDepositPct(double sellerSecurityDepositPct) { this.sellerSecurityDepositPct = sellerSecurityDepositPct; return this; } public OfferInfoBuilder withVolume(String volume) { this.volume = volume; return this; } public OfferInfoBuilder withMinVolume(String minVolume) { this.minVolume = minVolume; return this; } public OfferInfoBuilder withTriggerPrice(String triggerPrice) { this.triggerPrice = triggerPrice; return this; } public OfferInfoBuilder withIsCurrencyForMakerFeeBtc(boolean isCurrencyForMakerFeeBtc) { this.isCurrencyForMakerFeeBtc = isCurrencyForMakerFeeBtc; return this; } public OfferInfoBuilder withPaymentAccountId(String paymentAccountId) { this.paymentAccountId = paymentAccountId; return this; } public OfferInfoBuilder withPaymentMethodId(String paymentMethodId) { this.paymentMethodId = paymentMethodId; return this; } public OfferInfoBuilder withPaymentMethodShortName(String paymentMethodShortName) { this.paymentMethodShortName = paymentMethodShortName; return this; } public OfferInfoBuilder withBaseCurrencyCode(String baseCurrencyCode) { this.baseCurrencyCode = baseCurrencyCode; return this; } public OfferInfoBuilder withCounterCurrencyCode(String counterCurrencyCode) { this.counterCurrencyCode = counterCurrencyCode; return this; } public OfferInfoBuilder withDate(long date) { this.date = date; return this; } public OfferInfoBuilder withState(String state) { this.state = state; return this; } public OfferInfoBuilder withIsActivated(boolean isActivated) { this.isActivated = isActivated; return this; } public OfferInfoBuilder withIsMyOffer(boolean isMyOffer) { this.isMyOffer = isMyOffer; return this; } public OfferInfoBuilder withIsMyPendingOffer(boolean isMyPendingOffer) { this.isMyPendingOffer = isMyPendingOffer; return this; } public OfferInfoBuilder withIsBsqSwapOffer(boolean isBsqSwapOffer) { this.isBsqSwapOffer = isBsqSwapOffer; return this; } public OfferInfoBuilder withOwnerNodeAddress(String ownerNodeAddress) { this.ownerNodeAddress = ownerNodeAddress; return this; } public OfferInfoBuilder withPubKeyRing(String pubKeyRing) { this.pubKeyRing = pubKeyRing; return this; } public OfferInfoBuilder withVersionNumber(String versionNumber) { this.versionNumber = versionNumber; return this; } public OfferInfoBuilder withProtocolVersion(int protocolVersion) { this.protocolVersion = protocolVersion; return this; } public OfferInfoBuilder withArbitratorSigner(String arbitratorSigner) { this.arbitratorSigner = arbitratorSigner; return this; } public OfferInfoBuilder withSplitOutputTxHash(String splitOutputTxHash) { this.splitOutputTxHash = splitOutputTxHash; return this; } public OfferInfoBuilder withSplitOutputTxFee(long splitOutputTxFee) { this.splitOutputTxFee = splitOutputTxFee; return this; } public OfferInfoBuilder withIsPrivateOffer(boolean isPrivateOffer) { this.isPrivateOffer = isPrivateOffer; return this; } public OfferInfoBuilder withChallenge(String challenge) { this.challenge = challenge; return this; } public OfferInfoBuilder withExtraInfo(String extraInfo) { this.extraInfo = extraInfo; return this; } public OfferInfoBuilder withAcceptedCountryCodes(List acceptedCountryCodes) { this.acceptedCountryCodes = acceptedCountryCodes; return this; } public OfferInfoBuilder withAcceptedCountriesString(String acceptedCountriesString) { this.acceptedCountriesString = acceptedCountriesString; return this; } public OfferInfoBuilder withCity(String city) { this.city = city; return this; } public OfferInfo build() { return new OfferInfo(this); } } ================================================ FILE: core/src/main/java/haveno/core/api/model/builder/TradeInfoV1Builder.java ================================================ /* * 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 . */ package haveno.core.api.model.builder; import haveno.core.api.model.ContractInfo; import haveno.core.api.model.OfferInfo; import haveno.core.api.model.TradeInfo; import lombok.Getter; /** * A builder helps avoid bungling use of a large TradeInfo constructor * argument list. If consecutive argument values of the same type are not * ordered correctly, the compiler won't complain but the resulting bugs could * be hard to find and fix. */ @Getter public final class TradeInfoV1Builder { private OfferInfo offer; private String tradeId; private String shortId; private long date; private String role; private boolean isCurrencyForTakerFeeBtc; private long totalTxFee; private long makerFee; private long takerFee; private long buyerSecurityDeposit; private long sellerSecurityDeposit; private long buyerDepositTxFee; private long sellerDepositTxFee; private long buyerPayoutTxFee; private long sellerPayoutTxFee; private long buyerPayoutAmount; private long sellerPayoutAmount; private String makerDepositTxId; private String takerDepositTxId; private String payoutTxId; private long amount; private String price; private String volume; private String arbitratorNodeAddress; private String tradePeerNodeAddress; private String state; private String phase; private String periodState; private String payoutState; private String disputeState; private boolean isDepositsPublished; private boolean isDepositsConfirmed; private boolean isDepositsUnlocked; private boolean isDepositsFinalized; private boolean isPaymentSent; private boolean isPaymentReceived; private boolean isPayoutPublished; private boolean isPayoutConfirmed; private boolean isPayoutUnlocked; private boolean isPayoutFinalized; private boolean isCompleted; private String contractAsJson; private ContractInfo contract; private String closingStatus; private long startTime; private long maxDurationMs; private long deadlineTime; public TradeInfoV1Builder withOffer(OfferInfo offer) { this.offer = offer; return this; } public TradeInfoV1Builder withTradeId(String tradeId) { this.tradeId = tradeId; return this; } public TradeInfoV1Builder withShortId(String shortId) { this.shortId = shortId; return this; } public TradeInfoV1Builder withDate(long date) { this.date = date; return this; } public TradeInfoV1Builder withRole(String role) { this.role = role; return this; } public TradeInfoV1Builder withIsCurrencyForTakerFeeBtc(boolean isCurrencyForTakerFeeBtc) { this.isCurrencyForTakerFeeBtc = isCurrencyForTakerFeeBtc; return this; } public TradeInfoV1Builder withTotalTxFee(long totalTxFee) { this.totalTxFee = totalTxFee; return this; } public TradeInfoV1Builder withMakerFee(long makerFee) { this.makerFee = makerFee; return this; } public TradeInfoV1Builder withTakerFee(long takerFee) { this.takerFee = takerFee; return this; } public TradeInfoV1Builder withBuyerSecurityDeposit(long buyerSecurityDeposit) { this.buyerSecurityDeposit = buyerSecurityDeposit; return this; } public TradeInfoV1Builder withSellerSecurityDeposit(long sellerSecurityDeposit) { this.sellerSecurityDeposit = sellerSecurityDeposit; return this; } public TradeInfoV1Builder withBuyerDepositTxFee(long buyerDepositTxFee) { this.buyerDepositTxFee = buyerDepositTxFee; return this; } public TradeInfoV1Builder withSellerDepositTxFee(long sellerDepositTxFee) { this.sellerDepositTxFee = sellerDepositTxFee; return this; } public TradeInfoV1Builder withBuyerPayoutTxFee(long buyerPayoutTxFee) { this.buyerPayoutTxFee = buyerPayoutTxFee; return this; } public TradeInfoV1Builder withSellerPayoutTxFee(long sellerPayoutTxFee) { this.sellerPayoutTxFee = sellerPayoutTxFee; return this; } public TradeInfoV1Builder withBuyerPayoutAmount(long buyerPayoutAmount) { this.buyerPayoutAmount = buyerPayoutAmount; return this; } public TradeInfoV1Builder withSellerPayoutAmount(long sellerPayoutAmount) { this.sellerPayoutAmount = sellerPayoutAmount; return this; } public TradeInfoV1Builder withMakerDepositTxId(String makerDepositTxId) { this.makerDepositTxId = makerDepositTxId; return this; } public TradeInfoV1Builder withTakerDepositTxId(String takerDepositTxId) { this.takerDepositTxId = takerDepositTxId; return this; } public TradeInfoV1Builder withPayoutTxId(String payoutTxId) { this.payoutTxId = payoutTxId; return this; } public TradeInfoV1Builder withAmount(long amount) { this.amount = amount; return this; } public TradeInfoV1Builder withPrice(String price) { this.price = price; return this; } public TradeInfoV1Builder withVolume(String volume) { this.volume = volume; return this; } public TradeInfoV1Builder withState(String state) { this.state = state; return this; } public TradeInfoV1Builder withPhase(String phase) { this.phase = phase; return this; } public TradeInfoV1Builder withPeriodState(String periodState) { this.periodState = periodState; return this; } public TradeInfoV1Builder withPayoutState(String payoutState) { this.payoutState = payoutState; return this; } public TradeInfoV1Builder withDisputeState(String disputeState) { this.disputeState = disputeState; return this; } public TradeInfoV1Builder withArbitratorNodeAddress(String arbitratorNodeAddress) { this.arbitratorNodeAddress = arbitratorNodeAddress; return this; } public TradeInfoV1Builder withTradePeerNodeAddress(String tradePeerNodeAddress) { this.tradePeerNodeAddress = tradePeerNodeAddress; return this; } public TradeInfoV1Builder withIsDepositsPublished(boolean isDepositsPublished) { this.isDepositsPublished = isDepositsPublished; return this; } public TradeInfoV1Builder withIsDepositsConfirmed(boolean isDepositsConfirmed) { this.isDepositsConfirmed = isDepositsConfirmed; return this; } public TradeInfoV1Builder withIsDepositsUnlocked(boolean isDepositsUnlocked) { this.isDepositsUnlocked = isDepositsUnlocked; return this; } public TradeInfoV1Builder withIsDepositsFinalized(boolean isDepositsFinalized) { this.isDepositsFinalized = isDepositsFinalized; return this; } public TradeInfoV1Builder withIsPaymentSent(boolean isPaymentSent) { this.isPaymentSent = isPaymentSent; return this; } public TradeInfoV1Builder withIsPaymentReceived(boolean isPaymentReceived) { this.isPaymentReceived = isPaymentReceived; return this; } public TradeInfoV1Builder withIsPayoutPublished(boolean isPayoutPublished) { this.isPayoutPublished = isPayoutPublished; return this; } public TradeInfoV1Builder withIsPayoutConfirmed(boolean isPayoutConfirmed) { this.isPayoutConfirmed = isPayoutConfirmed; return this; } public TradeInfoV1Builder withIsPayoutUnlocked(boolean isPayoutUnlocked) { this.isPayoutUnlocked = isPayoutUnlocked; return this; } public TradeInfoV1Builder withIsPayoutFinalized(boolean isPayoutFinalized) { this.isPayoutFinalized = isPayoutFinalized; return this; } public TradeInfoV1Builder withIsCompleted(boolean isCompleted) { this.isCompleted = isCompleted; return this; } public TradeInfoV1Builder withContractAsJson(String contractAsJson) { this.contractAsJson = contractAsJson; return this; } public TradeInfoV1Builder withContract(ContractInfo contract) { this.contract = contract; return this; } public TradeInfoV1Builder withClosingStatus(String closingStatus) { this.closingStatus = closingStatus; return this; } public TradeInfoV1Builder withStartTime(long startTime) { this.startTime = startTime; return this; } public TradeInfoV1Builder withMaxDurationMs(long maxDurationMs) { this.maxDurationMs = maxDurationMs; return this; } public TradeInfoV1Builder withDeadlineTime(long deadlineTime) { this.deadlineTime = deadlineTime; return this; } public TradeInfo build() { return new TradeInfo(this); } } ================================================ FILE: core/src/main/java/haveno/core/app/AppStartupState.java ================================================ /* * 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 . */ package haveno.core.app; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.core.api.XmrConnectionService; import haveno.core.api.CoreNotificationService; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.BootstrapListener; import haveno.network.p2p.P2PService; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import lombok.extern.slf4j.Slf4j; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.monadic.MonadicBinding; /** * We often need to wait until network and wallet is ready or other combination of startup states. * To avoid those repeated checks for the state or setting of listeners on different domains we provide here a * collection of useful states. */ @Slf4j @Singleton public class AppStartupState { // Do not convert to local field as there have been issues observed that the object got GC'ed. private final MonadicBinding p2pNetworkAndWalletInitialized; private final BooleanProperty walletAndNetworkReady = new SimpleBooleanProperty(); private final BooleanProperty allDomainServicesInitialized = new SimpleBooleanProperty(); private final BooleanProperty applicationFullyInitialized = new SimpleBooleanProperty(); private final BooleanProperty updatedDataReceived = new SimpleBooleanProperty(); private final BooleanProperty isBlockDownloadComplete = new SimpleBooleanProperty(); private final BooleanProperty wasWalletSynced = new SimpleBooleanProperty(); private final BooleanProperty hasSufficientPeersForBroadcast = new SimpleBooleanProperty(); @Inject public AppStartupState(CoreNotificationService notificationService, XmrConnectionService xmrConnectionService, XmrWalletService xmrWalletService, P2PService p2PService) { p2PService.addP2PServiceListener(new BootstrapListener() { @Override public void onDataReceived() { updatedDataReceived.set(true); } }); xmrConnectionService.downloadPercentageProperty().addListener((observable, oldValue, newValue) -> { if (xmrConnectionService.isDownloadComplete()) isBlockDownloadComplete.set(true); }); xmrWalletService.downloadPercentageProperty().addListener((observable, oldValue, newValue) -> { wasWalletSynced.set(xmrWalletService.wasWalletSynced()); }); xmrConnectionService.numConnectionsProperty().addListener((observable, oldValue, newValue) -> { if (xmrConnectionService.hasSufficientPeersForBroadcast()) hasSufficientPeersForBroadcast.set(true); }); p2pNetworkAndWalletInitialized = EasyBind.combine(updatedDataReceived, isBlockDownloadComplete, wasWalletSynced, hasSufficientPeersForBroadcast, // TODO: consider sufficient number of peers? allDomainServicesInitialized, (a, b, c, d, e) -> { log.info("Combined initialized state = {} = updatedDataReceived={} && isBlockDownloadComplete={} && isWalletSynced={} && hasSufficientPeersForBroadcast={} && allDomainServicesInitialized={}", (a && b && c && d && e), updatedDataReceived.get(), isBlockDownloadComplete.get(), wasWalletSynced.get(), hasSufficientPeersForBroadcast.get(), allDomainServicesInitialized.get()); if (a && b && c) { walletAndNetworkReady.set(true); } else if (!wasWalletSynced()) { walletAndNetworkReady.set(false); } return a && c && e; }); p2pNetworkAndWalletInitialized.subscribe((observable, oldValue, newValue) -> { if (newValue) { applicationFullyInitialized.set(true); notificationService.sendAppInitializedNotification(); log.info("Application fully initialized"); } else { applicationFullyInitialized.set(false); notificationService.sendAppInitializedNotification(); log.info("Application is not fully initialized"); } }); } public void onDomainServicesInitialized() { allDomainServicesInitialized.set(true); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// public boolean isWalletAndNetworkReady() { return walletAndNetworkReady.get(); } public ReadOnlyBooleanProperty walletAndNetworkReadyProperty() { return walletAndNetworkReady; } public boolean isAllDomainServicesInitialized() { return allDomainServicesInitialized.get(); } public ReadOnlyBooleanProperty allDomainServicesInitializedProperty() { return allDomainServicesInitialized; } public boolean isApplicationFullyInitialized() { return applicationFullyInitialized.get(); } public ReadOnlyBooleanProperty applicationFullyInitializedProperty() { return applicationFullyInitialized; } public boolean isUpdatedDataReceived() { return updatedDataReceived.get(); } public ReadOnlyBooleanProperty updatedDataReceivedProperty() { return updatedDataReceived; } public boolean isBlockDownloadComplete() { return isBlockDownloadComplete.get(); } public boolean wasWalletSynced() { return wasWalletSynced.get(); } public ReadOnlyBooleanProperty isBlockDownloadCompleteProperty() { return isBlockDownloadComplete; } public boolean isHasSufficientPeersForBroadcast() { return hasSufficientPeersForBroadcast.get(); } public ReadOnlyBooleanProperty hasSufficientPeersForBroadcastProperty() { return hasSufficientPeersForBroadcast; } } ================================================ FILE: core/src/main/java/haveno/core/app/AvoidStandbyModeService.java ================================================ /* * 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 . */ package haveno.core.app; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.config.Config; import haveno.common.file.FileUtil; import haveno.common.file.ResourceNotFoundException; import haveno.common.util.Utilities; import haveno.core.user.Preferences; import java.io.File; import java.io.IOException; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.DataLine; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.SourceDataLine; import lombok.extern.slf4j.Slf4j; /** * Prevents that Haveno gets hibernated from the OS. On OSX there is a tool called caffeinate but it seems it does not * provide the behaviour we need, thus we use the trick to play a almost silent sound file in a loop. This keeps the * application active even if the OS has moved to hibernate. Hibernating Haveno would cause network degradations and other * resource limitations which would lead to offers not published or if a taker takes an offer that the trade process is * at risk to fail due too slow response time. */ @Slf4j @Singleton public class AvoidStandbyModeService { private final Preferences preferences; private final Config config; private final Optional inhibitorPathSpec; private CountDownLatch stopLinuxInhibitorCountdownLatch; private volatile boolean isStopped; @Inject public AvoidStandbyModeService(Preferences preferences, Config config) { this.preferences = preferences; this.config = config; this.inhibitorPathSpec = inhibitorPath(); preferences.getUseStandbyModeProperty().addListener((observable, oldValue, newValue) -> { if (newValue) { isStopped = true; log.info("AvoidStandbyModeService stopped"); if (Utilities.isLinux() && runningInhibitorProcess().isPresent()) { Objects.requireNonNull(stopLinuxInhibitorCountdownLatch).countDown(); } } else { start(); } }); } public void init() { isStopped = preferences.isUseStandbyMode(); if (!isStopped) { start(); } } private void start() { isStopped = false; if (Utilities.isLinux()) { startInhibitor(); } else { new Thread(this::playSilentAudioFile, "AvoidStandbyModeService-thread").start(); } } public void shutDown() { isStopped = true; stopInhibitor(); } private void startInhibitor() { try { if (runningInhibitorProcess().isPresent()) { log.info("Inhibitor already started"); return; } inhibitCommand().ifPresent(cmd -> { try { new ProcessBuilder(cmd).start(); log.info("Started -- disabled power management via {}", String.join(" ", cmd)); if (Utilities.isLinux()) { stopLinuxInhibitorCountdownLatch = new CountDownLatch(1); new Thread(this::stopInhibitor, "StopAvoidStandbyModeService-thread").start(); } } catch (Exception e) { e.printStackTrace(); } }); } catch (Exception e) { log.error("Cannot avoid standby mode", e); } } private void stopInhibitor() { try { if (Utilities.isLinux()) { if (!isStopped) { Objects.requireNonNull(stopLinuxInhibitorCountdownLatch).await(); } Optional runningInhibitor = runningInhibitorProcess(); runningInhibitor.ifPresent(processHandle -> { processHandle.destroy(); log.info("Stopped"); }); } } catch (Exception e) { log.error("Stop inhibitor thread interrupted", e); } } private void playSilentAudioFile() { try { log.info("Started"); while (!isStopped) { try (AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(getSoundFile()); SourceDataLine sourceDataLine = getSourceDataLine(audioInputStream.getFormat())) { byte[] tempBuffer = new byte[10000]; sourceDataLine.open(audioInputStream.getFormat()); sourceDataLine.start(); int cnt; while ((cnt = audioInputStream.read(tempBuffer, 0, tempBuffer.length)) != -1 && !isStopped) { if (cnt > 0) { sourceDataLine.write(tempBuffer, 0, cnt); } } sourceDataLine.drain(); } } } catch (Exception e) { log.error(e.toString()); e.printStackTrace(); } } private File getSoundFile() throws IOException, ResourceNotFoundException { File soundFile = new File(config.appDataDir, "prevent-app-nap-silent-sound.aiff"); // We replaced the old file which was 42 MB with a smaller file of 0.8 MB. To enforce replacement we check for // the size... if (!soundFile.exists() || soundFile.length() > 42000000) { FileUtil.resourceToFile("prevent-app-nap-silent-sound.aiff", soundFile); } return soundFile; } private SourceDataLine getSourceDataLine(AudioFormat audioFormat) throws LineUnavailableException { DataLine.Info dataLineInfo = new DataLine.Info(SourceDataLine.class, audioFormat); return (SourceDataLine) AudioSystem.getLine(dataLineInfo); } private Optional inhibitorPath() { for (Optional installedInhibitor : installedInhibitors.get()) { if (installedInhibitor.isPresent()) { return installedInhibitor; } } return Optional.empty(); // falling back to silent audio file player } private Optional inhibitCommand() { final String[] params; if (inhibitorPathSpec.isPresent()) { String cmd = inhibitorPathSpec.get(); if (Utilities.isLinux()) { params = cmd.contains("gnome-session-inhibit") ? new String[]{cmd, "--app-id", "Haveno", "--inhibit", "suspend", "--reason", "Avoid Standby", "--inhibit-only"} : new String[]{cmd, "--who", "Haveno", "--what", "sleep", "--why", "Avoid Standby", "--mode", "block", "tail", "-f", "/dev/null"}; } else { params = null; } } else { params = null; // fall back to silent audio file player } return params == null ? Optional.empty() : Optional.of(params); } private Optional runningInhibitorProcess() { final ProcessHandle[] inhibitorProc = new ProcessHandle[1]; inhibitorPathSpec.ifPresent(cmd -> { Optional jvmProc = ProcessHandle.of(ProcessHandle.current().pid()); jvmProc.ifPresent(proc -> proc.children().forEach(childProc -> childProc.info().command().ifPresent(command -> { if (command.equals(cmd) && childProc.isAlive()) { inhibitorProc[0] = childProc; } }))); }); return inhibitorProc[0] == null ? Optional.empty() : Optional.of(inhibitorProc[0]); } private final Predicate isCmdInstalled = (p) -> { File executable = Paths.get(p).toFile(); return executable.exists() && executable.canExecute(); }; private final Function> cmdPath = (possiblePaths) -> { for (String path : possiblePaths) { if (isCmdInstalled.test(path)) { return Optional.of(path); } } return Optional.empty(); }; private final Supplier>> installedInhibitors = () -> new ArrayList<>() {{ add(gnomeSessionInhibitPathSpec.get()); // On linux, preferred inhibitor is gnome-session-inhibit, add(systemdInhibitPathSpec.get()); // then fall back to systemd-inhibit if it is installed. }}; private final Supplier> gnomeSessionInhibitPathSpec = () -> cmdPath.apply(new String[]{"/usr/bin/gnome-session-inhibit", "/bin/gnome-session-inhibit"}); private final Supplier> systemdInhibitPathSpec = () -> cmdPath.apply(new String[]{"/usr/bin/systemd-inhibit", "/bin/systemd-inhibit"}); } ================================================ FILE: core/src/main/java/haveno/core/app/ConsoleInput.java ================================================ /* * 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 . */ package haveno.core.app; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; /** * A cancellable console input reader. * Derived from https://www.javaspecialists.eu/archive/Issue153-Timeout-on-Console-Input.html */ public class ConsoleInput { private final int tries; private final int timeout; private final TimeUnit unit; private Future future; public ConsoleInput(int tries, int timeout, TimeUnit unit) { this.tries = tries; this.timeout = timeout; this.unit = unit; } public void cancel() { if (future != null) future.cancel(true); } public String readLine() throws InterruptedException { ExecutorService ex = Executors.newSingleThreadExecutor(); String input = null; try { for (int i = 0; i < tries; i++) { future = ex.submit(new ConsoleInputReadTask()); try { input = future.get(timeout, unit); break; } catch (ExecutionException e) { e.getCause().printStackTrace(); } catch (TimeoutException e) { future.cancel(true); } finally { future = null; } } } finally { ex.shutdownNow(); } return input; } } ================================================ FILE: core/src/main/java/haveno/core/app/ConsoleInputReadTask.java ================================================ /* * 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 . */ package haveno.core.app; import lombok.extern.slf4j.Slf4j; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.concurrent.Callable; @Slf4j public class ConsoleInputReadTask implements Callable { public String call() throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); log.debug("ConsoleInputReadTask run() called."); String input; do { try { // wait until we have data to complete a readLine() while (!br.ready()) { Thread.sleep(100); } // readline will always block until an input exists. input = br.readLine(); } catch (InterruptedException e) { log.debug("ConsoleInputReadTask() cancelled"); return null; } } while ("".equals(input)); return input; } } ================================================ FILE: core/src/main/java/haveno/core/app/CoreModule.java ================================================ /* * 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 . */ package haveno.core.app; import com.google.inject.Singleton; import haveno.common.app.AppModule; import haveno.common.config.Config; import haveno.common.proto.network.NetworkProtoResolver; import haveno.common.proto.persistable.PersistenceProtoResolver; import haveno.core.alert.AlertModule; import haveno.core.filter.FilterModule; import haveno.core.network.CoreBanFilter; import haveno.core.network.p2p.seed.DefaultSeedNodeRepository; import haveno.core.offer.OfferModule; import haveno.core.presentation.CorePresentationModule; import haveno.core.proto.network.CoreNetworkProtoResolver; import haveno.core.proto.persistable.CorePersistenceProtoResolver; import haveno.core.trade.TradeModule; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.coin.ImmutableCoinFormatter; import haveno.core.xmr.XmrConnectionModule; import haveno.core.xmr.XmrModule; import haveno.network.crypto.EncryptionServiceModule; import haveno.network.p2p.P2PModule; import haveno.network.p2p.network.BanFilter; import haveno.network.p2p.network.BridgeAddressProvider; import haveno.network.p2p.seed.SeedNodeRepository; import java.io.File; import static com.google.inject.name.Names.named; import static haveno.common.config.Config.KEY_STORAGE_DIR; import static haveno.common.config.Config.REFERRAL_ID; import static haveno.common.config.Config.STORAGE_DIR; import static haveno.common.config.Config.USE_DEV_MODE; import static haveno.common.config.Config.USE_DEV_MODE_HEADER; import static haveno.common.config.Config.USE_DEV_PRIVILEGE_KEYS; public class CoreModule extends AppModule { public CoreModule(Config config) { super(config); } @Override protected void configure() { bind(Config.class).toInstance(config); bind(BridgeAddressProvider.class).to(Preferences.class); bind(SeedNodeRepository.class).to(DefaultSeedNodeRepository.class); bind(BanFilter.class).to(CoreBanFilter.class).in(Singleton.class); bind(File.class).annotatedWith(named(STORAGE_DIR)).toInstance(config.storageDir); CoinFormatter btcFormatter = new ImmutableCoinFormatter(config.networkParameters.getMonetaryFormat()); bind(CoinFormatter.class).annotatedWith(named(FormattingUtils.BTC_FORMATTER_KEY)).toInstance(btcFormatter); bind(File.class).annotatedWith(named(KEY_STORAGE_DIR)).toInstance(config.keyStorageDir); bind(NetworkProtoResolver.class).to(CoreNetworkProtoResolver.class); bind(PersistenceProtoResolver.class).to(CorePersistenceProtoResolver.class); bindConstant().annotatedWith(named(USE_DEV_PRIVILEGE_KEYS)).to(config.useDevPrivilegeKeys); bindConstant().annotatedWith(named(USE_DEV_MODE)).to(config.useDevMode); bindConstant().annotatedWith(named(USE_DEV_MODE_HEADER)).to(config.useDevModeHeader); bindConstant().annotatedWith(named(REFERRAL_ID)).to(config.referralId); // ordering is used for shut down sequence install(new TradeModule(config)); install(new EncryptionServiceModule(config)); install(new OfferModule(config)); install(new P2PModule(config)); install(new XmrModule(config)); install(new AlertModule(config)); install(new FilterModule(config)); install(new CorePresentationModule(config)); install(new XmrConnectionModule(config)); } } ================================================ FILE: core/src/main/java/haveno/core/app/DomainInitialisation.java ================================================ /* * 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 . */ package haveno.core.app; import com.google.inject.Inject; import haveno.common.ClockWatcher; import haveno.common.persistence.PersistenceManager; import haveno.core.account.sign.SignedWitnessService; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.alert.PrivateNotificationManager; import haveno.core.alert.PrivateNotificationPayload; import haveno.core.filter.FilterManager; import haveno.core.notifications.MobileNotificationService; import haveno.core.notifications.alerts.DisputeMsgEvents; import haveno.core.notifications.alerts.MyOfferTakenEvents; import haveno.core.notifications.alerts.TradeEvents; import haveno.core.notifications.alerts.market.MarketAlerts; import haveno.core.notifications.alerts.price.PriceAlert; import haveno.core.offer.OpenOfferManager; import haveno.core.offer.TriggerPriceService; import haveno.core.payment.AmazonGiftCardAccount; import haveno.core.payment.RevolutAccount; import haveno.core.provider.price.PriceFeedService; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.support.dispute.mediation.MediationManager; import haveno.core.support.dispute.mediation.mediator.MediatorManager; import haveno.core.support.dispute.refund.RefundManager; import haveno.core.support.dispute.refund.refundagent.RefundAgentManager; import haveno.core.support.traderchat.TraderChatManager; import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.TradeManager; import haveno.core.trade.failed.FailedTradesManager; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.User; import haveno.core.xmr.Balances; import haveno.network.p2p.P2PService; import haveno.network.p2p.mailbox.MailboxMessageService; import java.util.List; import java.util.function.Consumer; import java.util.stream.Collectors; /** * Handles the initialisation of domain classes. We should refactor to the model that the domain classes listen on the * relevant start up state from AppStartupState instead to get called. Only for initialisation which has a required * order we will still need this class. For now it helps to keep HavenoSetup more focussed on the process and not getting * overloaded with domain initialisation code. */ public class DomainInitialisation { private final ClockWatcher clockWatcher; private final ArbitrationManager arbitrationManager; private final MediationManager mediationManager; private final RefundManager refundManager; private final TraderChatManager traderChatManager; private final TradeManager tradeManager; private final ClosedTradableManager closedTradableManager; private final FailedTradesManager failedTradesManager; private final OpenOfferManager openOfferManager; private final Balances balances; private final WalletAppSetup walletAppSetup; private final ArbitratorManager arbitratorManager; private final MediatorManager mediatorManager; private final RefundAgentManager refundAgentManager; private final PrivateNotificationManager privateNotificationManager; private final P2PService p2PService; private final TradeStatisticsManager tradeStatisticsManager; private final AccountAgeWitnessService accountAgeWitnessService; private final SignedWitnessService signedWitnessService; private final PriceFeedService priceFeedService; private final FilterManager filterManager; private final MobileNotificationService mobileNotificationService; private final MyOfferTakenEvents myOfferTakenEvents; private final TradeEvents tradeEvents; private final DisputeMsgEvents disputeMsgEvents; private final PriceAlert priceAlert; private final MarketAlerts marketAlerts; private final User user; private final TriggerPriceService triggerPriceService; private final MailboxMessageService mailboxMessageService; @Inject public DomainInitialisation(ClockWatcher clockWatcher, ArbitrationManager arbitrationManager, MediationManager mediationManager, RefundManager refundManager, TraderChatManager traderChatManager, TradeManager tradeManager, ClosedTradableManager closedTradableManager, FailedTradesManager failedTradesManager, OpenOfferManager openOfferManager, Balances balances, WalletAppSetup walletAppSetup, ArbitratorManager arbitratorManager, MediatorManager mediatorManager, RefundAgentManager refundAgentManager, PrivateNotificationManager privateNotificationManager, P2PService p2PService, TradeStatisticsManager tradeStatisticsManager, AccountAgeWitnessService accountAgeWitnessService, SignedWitnessService signedWitnessService, PriceFeedService priceFeedService, FilterManager filterManager, MobileNotificationService mobileNotificationService, MyOfferTakenEvents myOfferTakenEvents, TradeEvents tradeEvents, DisputeMsgEvents disputeMsgEvents, PriceAlert priceAlert, MarketAlerts marketAlerts, User user, TriggerPriceService triggerPriceService, MailboxMessageService mailboxMessageService) { this.clockWatcher = clockWatcher; this.arbitrationManager = arbitrationManager; this.mediationManager = mediationManager; this.refundManager = refundManager; this.traderChatManager = traderChatManager; this.tradeManager = tradeManager; this.closedTradableManager = closedTradableManager; this.failedTradesManager = failedTradesManager; this.openOfferManager = openOfferManager; this.balances = balances; this.walletAppSetup = walletAppSetup; this.arbitratorManager = arbitratorManager; this.mediatorManager = mediatorManager; this.refundAgentManager = refundAgentManager; this.privateNotificationManager = privateNotificationManager; this.p2PService = p2PService; this.tradeStatisticsManager = tradeStatisticsManager; this.accountAgeWitnessService = accountAgeWitnessService; this.signedWitnessService = signedWitnessService; this.priceFeedService = priceFeedService; this.filterManager = filterManager; this.mobileNotificationService = mobileNotificationService; this.myOfferTakenEvents = myOfferTakenEvents; this.tradeEvents = tradeEvents; this.disputeMsgEvents = disputeMsgEvents; this.priceAlert = priceAlert; this.marketAlerts = marketAlerts; this.user = user; this.triggerPriceService = triggerPriceService; this.mailboxMessageService = mailboxMessageService; } public void initDomainServices(Consumer rejectedTxErrorMessageHandler, Consumer displayPrivateNotificationHandler, Consumer filterWarningHandler, Consumer> revolutAccountsUpdateHandler, Consumer> amazonGiftCardAccountsUpdateHandler) { clockWatcher.start(); PersistenceManager.onAllServicesInitialized(); p2PService.onAllServicesInitialized(); arbitratorManager.onAllServicesInitialized(); mediatorManager.onAllServicesInitialized(); refundAgentManager.onAllServicesInitialized(); tradeManager.onAllServicesInitialized(); arbitrationManager.onAllServicesInitialized(); mediationManager.onAllServicesInitialized(); refundManager.onAllServicesInitialized(); traderChatManager.onAllServicesInitialized(); closedTradableManager.onAllServicesInitialized(); failedTradesManager.onAllServicesInitialized(); filterManager.setFilterWarningHandler(filterWarningHandler); filterManager.onAllServicesInitialized(); openOfferManager.onAllServicesInitialized(); balances.onAllServicesInitialized(); walletAppSetup.setRejectedTxErrorMessageHandler(rejectedTxErrorMessageHandler, openOfferManager, tradeManager); privateNotificationManager.privateNotificationProperty().addListener((observable, oldValue, newValue) -> { if (displayPrivateNotificationHandler != null) displayPrivateNotificationHandler.accept(newValue); }); tradeStatisticsManager.onAllServicesInitialized(); accountAgeWitnessService.onAllServicesInitialized(); signedWitnessService.onAllServicesInitialized(); priceFeedService.setCurrencyCodeOnInit(); priceFeedService.startRequestingPrices(); mobileNotificationService.onAllServicesInitialized(); myOfferTakenEvents.onAllServicesInitialized(); tradeEvents.onAllServicesInitialized(); disputeMsgEvents.onAllServicesInitialized(); priceAlert.onAllServicesInitialized(); marketAlerts.onAllServicesInitialized(); triggerPriceService.onAllServicesInitialized(); mailboxMessageService.onAllServicesInitialized(); if (revolutAccountsUpdateHandler != null && user.getPaymentAccountsAsObservable() != null) { revolutAccountsUpdateHandler.accept(user.getPaymentAccountsAsObservable().stream() .filter(paymentAccount -> paymentAccount instanceof RevolutAccount) .map(paymentAccount -> (RevolutAccount) paymentAccount) .filter(RevolutAccount::usernameNotSet) .collect(Collectors.toList())); } if (amazonGiftCardAccountsUpdateHandler != null && user.getPaymentAccountsAsObservable() != null) { amazonGiftCardAccountsUpdateHandler.accept(user.getPaymentAccountsAsObservable().stream() .filter(paymentAccount -> paymentAccount instanceof AmazonGiftCardAccount) .map(paymentAccount -> (AmazonGiftCardAccount) paymentAccount) .filter(AmazonGiftCardAccount::countryNotSet) .collect(Collectors.toList())); } } } ================================================ FILE: core/src/main/java/haveno/core/app/HavenoExecutable.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.app; import com.google.inject.Guice; import com.google.inject.Injector; import haveno.common.ThreadUtils; import haveno.common.UserThread; import haveno.common.app.AppModule; import haveno.common.config.Config; import haveno.common.config.ConfigException; import haveno.common.config.HavenoHelpFormatter; import haveno.common.crypto.IncorrectPasswordException; import haveno.common.handlers.ResultHandler; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.persistable.PersistedDataHost; import haveno.common.setup.CommonSetup; import haveno.common.setup.GracefulShutDownHandler; import haveno.common.setup.UncaughtExceptionHandler; import haveno.common.util.Utilities; import haveno.core.api.AccountServiceListener; import haveno.core.api.CoreAccountService; import haveno.core.api.XmrConnectionService; import haveno.core.offer.OfferBookService; import haveno.core.offer.OpenOfferManager; import haveno.core.provider.price.PriceFeedService; import haveno.core.setup.CorePersistedDataHost; import haveno.core.setup.CoreSetup; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.trade.TradeManager; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.xmr.setup.WalletsSetup; import haveno.core.xmr.wallet.BtcWalletService; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.P2PService; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.io.Console; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @Slf4j public abstract class HavenoExecutable implements GracefulShutDownHandler, HavenoSetup.HavenoSetupListener, UncaughtExceptionHandler { public static final String DEFAULT_APP_NAME = "Haveno-reto"; public static final int EXIT_SUCCESS = 0; public static final int EXIT_FAILURE = 1; public static final int EXIT_RESTART = 2; private final String fullName; private final String scriptName; private final String appName; private final String version; protected CoreAccountService accountService; protected Injector injector; protected AppModule module; protected Config config; @Getter protected boolean isShutDownStarted; private boolean isReadOnly; private Thread keepRunningThread; private AtomicInteger keepRunningResult = new AtomicInteger(EXIT_SUCCESS); private Runnable shutdownCompletedHandler; public HavenoExecutable(String fullName, String scriptName, String appName, String version) { this.fullName = fullName; this.scriptName = scriptName; this.appName = appName; this.version = version; } public int execute(String[] args) { try { config = new Config(appName, Utilities.getUserDataDir(), args); if (config.helpRequested) { config.printHelp(System.out, new HavenoHelpFormatter(fullName, scriptName, version)); System.exit(EXIT_SUCCESS); } } catch (ConfigException ex) { ex.printStackTrace(); System.err.println("error: " + ex.getMessage()); System.exit(EXIT_FAILURE); } catch (Throwable ex) { System.err.println("fault: An unexpected error occurred. " + "Please file a report at https://github.com/haveno-dex/haveno/issues"); ex.printStackTrace(System.err); System.exit(EXIT_FAILURE); } return doExecute(); } /////////////////////////////////////////////////////////////////////////////////////////// // First synchronous execution tasks /////////////////////////////////////////////////////////////////////////////////////////// protected int doExecute() { CommonSetup.setup(config, this); CoreSetup.setup(config); addCapabilities(); // If application is JavaFX application we need to wait until it is initialized launchApplication(); return EXIT_SUCCESS; } protected abstract void configUserThread(); protected void addCapabilities() { } // The onApplicationLaunched call must map to UserThread, so that all following methods are running in the // thread the application is running and we don't run into thread interference. protected abstract void launchApplication(); /////////////////////////////////////////////////////////////////////////////////////////// // If application is a JavaFX application we need wait for onApplicationLaunched /////////////////////////////////////////////////////////////////////////////////////////// // Headless versions can call inside launchApplication the onApplicationLaunched() manually protected void onApplicationLaunched() { configUserThread(); CommonSetup.printSystemLoadPeriodically(10); // As the handler method might be overwritten by subclasses and they use the application as handler // we need to setup the handler after the application is created. CommonSetup.setupUncaughtExceptionHandler(this); setupGuice(); setupAvoidStandbyMode(); // 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 HavenoSetup to go further // in the process and require a shut down. isReadOnly = HavenoSetup.hasDowngraded(); // Account service should be available before attempting to login. accountService = injector.getInstance(CoreAccountService.class); // Application needs to restart on delete and restore of account. accountService.addListener(new AccountServiceListener() { @Override public void onAccountDeleted(Runnable onShutdown) { shutDownNoPersist(onShutdown, true); } @Override public void onAccountRestored(Runnable onShutdown) { shutDownNoPersist(onShutdown, true); } }); // Attempt to login, subclasses should implement interactive login and or rpc login. CompletableFuture loginFuture = loginAccount(); loginFuture.whenComplete((result, throwable) -> { if (throwable != null) { log.error("Error logging in to account", throwable); shutDownNoPersist(null, false); return; } try { if (!isReadOnly && loginFuture.get()) { readAllPersisted(this::startApplication); } else { log.warn("Running application in readonly mode"); startApplication(); } } catch (InterruptedException | ExecutionException e) { log.error("An error occurred: {}\n", e.getMessage(), e); } }); } /** * Do not persist when shutting down after account restore and restarts since * that causes the current persistables to overwrite the restored or deleted state. * * If restart is specified, initiates an in-process asynchronous restart of the * application by interrupting the keepRunningThread. */ protected void shutDownNoPersist(Runnable onShutdown, boolean restart) { this.isReadOnly = true; if (restart) { shutdownCompletedHandler = onShutdown; keepRunningResult.set(EXIT_RESTART); keepRunningThread.interrupt(); } else { gracefulShutDown(() -> { log.info("Shutdown without persisting"); if (onShutdown != null) onShutdown.run(); }); } } /** * Attempt to login. TODO: supply a password in config or args * * @return true if account is opened successfully. */ protected CompletableFuture loginAccount() { CompletableFuture result = new CompletableFuture<>(); if (accountService.accountExists()) { log.info("Account already exists, attempting to open"); try { accountService.openAccount(null); result.complete(accountService.isAccountOpen()); } catch (IncorrectPasswordException ipe) { log.info("Account password protected, password required"); result.complete(false); } } else if (!config.passwordRequired) { log.info("Creating Haveno account with null password"); accountService.createAccount(null); result.complete(accountService.isAccountOpen()); } else { log.info("Account does not exist and password is required"); result.complete(false); } return result; } /////////////////////////////////////////////////////////////////////////////////////////// // We continue with a series of synchronous execution tasks /////////////////////////////////////////////////////////////////////////////////////////// protected void setupGuice() { module = getModule(); injector = getInjector(); applyInjector(); } protected abstract AppModule getModule(); protected Injector getInjector() { return Guice.createInjector(module); } protected void applyInjector() { // Subclasses might configure classes with the injector here } protected void readAllPersisted(Runnable completeHandler) { readAllPersisted(null, completeHandler); } protected void readAllPersisted(@Nullable List additionalHosts, Runnable completeHandler) { List hosts = CorePersistedDataHost.getPersistedDataHosts(injector); if (additionalHosts != null) { hosts.addAll(additionalHosts); } AtomicInteger remaining = new AtomicInteger(hosts.size()); hosts.forEach(host -> { host.readPersisted(() -> { if (remaining.decrementAndGet() == 0) { UserThread.execute(completeHandler); } }); }); } protected void setupAvoidStandbyMode() { } protected abstract void startApplication(); // Once the application is ready we get that callback and we start the setup protected void onApplicationStarted() { runHavenoSetup(); } protected void runHavenoSetup() { HavenoSetup havenoSetup = injector.getInstance(HavenoSetup.class); havenoSetup.addHavenoSetupListener(this); havenoSetup.start(); } @Override public abstract void onSetupComplete(); /////////////////////////////////////////////////////////////////////////////////////////// // GracefulShutDownHandler implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void gracefulShutDown(ResultHandler resultHandler) { gracefulShutDown(resultHandler, true); } // This might need to be overwritten in case the application is not using all modules @Override public void gracefulShutDown(ResultHandler onShutdown, boolean systemExit) { log.info("Starting graceful shut down of {}", getClass().getSimpleName()); // ignore if shut down started if (isShutDownStarted) { log.info("Ignoring call to gracefulShutDown, already started"); return; } isShutDownStarted = true; ResultHandler resultHandler; if (shutdownCompletedHandler != null) { resultHandler = () -> { shutdownCompletedHandler.run(); onShutdown.handleResult(); }; } else { resultHandler = onShutdown; } if (injector == null) { log.info("Shut down called before injector was created"); resultHandler.handleResult(); System.exit(EXIT_SUCCESS); } try { // notify trade protocols and wallets to prepare for shut down before shutting down Set tasks = new HashSet(); tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted()); tasks.add(() -> injector.getInstance(XmrWalletService.class).onShutDownStarted()); tasks.add(() -> injector.getInstance(XmrConnectionService.class).onShutDownStarted()); try { ThreadUtils.awaitTasks(tasks, tasks.size(), 90000l); // run in parallel with timeout } catch (Exception e) { log.error("Failed to notify all services to prepare for shutdown: {}\n", e.getMessage(), e); } injector.getInstance(PriceFeedService.class).shutDown(); injector.getInstance(ArbitratorManager.class).shutDown(); injector.getInstance(TradeStatisticsManager.class).shutDown(); injector.getInstance(AvoidStandbyModeService.class).shutDown(); // shut down open offer manager log.info("Shutting down OpenOfferManager"); injector.getInstance(OpenOfferManager.class).shutDown(() -> { // listen for shut down of wallets setup injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> { // shut down p2p service log.info("Shutting down P2P service"); injector.getInstance(P2PService.class).shutDown(() -> { // done shutting down log.info("Graceful shutdown completed. Exiting now."); module.close(injector); completeShutdown(resultHandler, EXIT_SUCCESS, systemExit); }); }); // shut down trade and wallet services log.info("Shutting down trade and wallet services"); injector.getInstance(OfferBookService.class).shutDown(); injector.getInstance(TradeManager.class).shutDown(); injector.getInstance(BtcWalletService.class).shutDown(); injector.getInstance(XmrWalletService.class).shutDown(); injector.getInstance(XmrConnectionService.class).shutDown(); injector.getInstance(WalletsSetup.class).shutDown(); }); } catch (Throwable t) { log.error("App shutdown failed with exception: {}\n", t.getMessage(), t); completeShutdown(resultHandler, EXIT_FAILURE, systemExit); } } private void completeShutdown(ResultHandler resultHandler, int exitCode, boolean systemExit) { if (!isReadOnly) { // If user tried to downgrade we do not write the persistable data to avoid data corruption PersistenceManager.flushAllDataToDiskAtShutdown(() -> { log.info("Graceful shutdown flushed persistence. Exiting now."); resultHandler.handleResult(); if (systemExit) UserThread.runAfter(() -> System.exit(exitCode), 1); }); } else { resultHandler.handleResult(); if (systemExit) UserThread.runAfter(() -> System.exit(exitCode), 1); } } /////////////////////////////////////////////////////////////////////////////////////////// // UncaughtExceptionHandler implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void handleUncaughtException(Throwable throwable, boolean doShutDown) { log.error(throwable.toString()); if (doShutDown) gracefulShutDown(() -> log.info("gracefulShutDown complete")); } /** * Runs until a command interrupts the application and returns the desired command behavior. * @return EXIT_SUCCESS to initiate a shutdown, EXIT_RESTART to initiate an in process restart. */ protected int keepRunning() { keepRunningThread = new Thread(() -> { ConsoleInput reader = new ConsoleInput(Integer.MAX_VALUE, Integer.MAX_VALUE, TimeUnit.MILLISECONDS); while (true) { Console console = System.console(); try { if (console == null) { Thread.sleep(Long.MAX_VALUE); } else { var cmd = reader.readLine(); if ("exit".equals(cmd)) { keepRunningResult.set(EXIT_SUCCESS); break; } else if ("restart".equals(cmd)) { keepRunningResult.set(EXIT_RESTART); break; } else if ("help".equals(cmd)) { System.out.println("Commands: restart, exit, help"); } else { System.out.println("Unknown command, use: restart, exit, help"); } } } catch (InterruptedException e) { break; } } }); keepRunningThread.start(); try { keepRunningThread.join(); } catch (InterruptedException ie) { System.out.println(ie); } return keepRunningResult.get(); } } ================================================ FILE: core/src/main/java/haveno/core/app/HavenoHeadlessApp.java ================================================ /* * 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 . */ package haveno.core.app; import com.google.inject.Injector; import haveno.common.UserThread; import haveno.common.app.Version; import haveno.common.file.CorruptedStorageFileHandler; import haveno.common.setup.GracefulShutDownHandler; import haveno.core.trade.TradeManager; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.TimeUnit; @Slf4j public class HavenoHeadlessApp implements HeadlessApp { @Getter private static Runnable shutDownHandler; @Setter public static Runnable onGracefulShutDownHandler; @Setter protected Injector injector; @Setter private GracefulShutDownHandler gracefulShutDownHandler; private boolean shutDownRequested; protected HavenoSetup havenoSetup; private CorruptedStorageFileHandler corruptedStorageFileHandler; private TradeManager tradeManager; public HavenoHeadlessApp() { shutDownHandler = this::stop; } @Override public void startApplication() { try { havenoSetup = injector.getInstance(HavenoSetup.class); havenoSetup.addHavenoSetupListener(this); corruptedStorageFileHandler = injector.getInstance(CorruptedStorageFileHandler.class); tradeManager = injector.getInstance(TradeManager.class); setupHandlers(); } catch (Throwable throwable) { log.error("Error during app init", throwable); handleUncaughtException(throwable, false); } } @Override public void onSetupComplete() { log.info("onSetupComplete"); } protected void setupHandlers() { havenoSetup.setDisplayTacHandler(acceptedHandler -> { log.info("onDisplayTacHandler: We accept the tacs automatically in headless mode"); acceptedHandler.run(); }); havenoSetup.setDisplayMoneroConnectionFallbackHandler(show -> log.warn("onDisplayMoneroConnectionFallbackHandler: show={}", show)); havenoSetup.setDisplayTorNetworkSettingsHandler(show -> log.info("onDisplayTorNetworkSettingsHandler: show={}", show)); havenoSetup.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg)); tradeManager.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg)); havenoSetup.setShowFirstPopupIfResyncSPVRequestedHandler(() -> log.info("onShowFirstPopupIfResyncSPVRequestedHandler")); havenoSetup.setDisplayUpdateHandler((alert, key) -> log.info("onDisplayUpdateHandler")); havenoSetup.setDisplayAlertHandler(alert -> log.info("onDisplayAlertHandler. alert={}", alert)); havenoSetup.setDisplayPrivateNotificationHandler(privateNotification -> log.info("onDisplayPrivateNotificationHandler. privateNotification={}", privateNotification)); havenoSetup.setDisplaySecurityRecommendationHandler(key -> log.info("onDisplaySecurityRecommendationHandler")); havenoSetup.setWrongOSArchitectureHandler(msg -> log.error("onWrongOSArchitectureHandler. msg={}", msg)); havenoSetup.setRejectedTxErrorMessageHandler(errorMessage -> log.warn("setRejectedTxErrorMessageHandler. errorMessage={}", errorMessage)); havenoSetup.setShowPopupIfInvalidXmrConfigHandler(() -> log.error("onShowPopupIfInvalidXmrConfigHandler")); havenoSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> log.info("setRevolutAccountsUpdateHandler: revolutAccountList={}", revolutAccountList)); havenoSetup.setOsxKeyLoggerWarningHandler(() -> log.info("setOsxKeyLoggerWarningHandler")); havenoSetup.setQubesOSInfoHandler(() -> log.info("setQubesOSInfoHandler")); havenoSetup.setDownGradePreventionHandler(lastVersion -> log.info("Downgrade from version {} to version {} is not supported", lastVersion, Version.VERSION)); havenoSetup.setTorAddressUpgradeHandler(() -> log.info("setTorAddressUpgradeHandler")); corruptedStorageFileHandler.getFiles().ifPresent(files -> log.warn("getCorruptedDatabaseFiles. files={}", files)); } public void stop() { if (!shutDownRequested) { UserThread.runAfter(() -> { if (gracefulShutDownHandler != null) { gracefulShutDownHandler.gracefulShutDown(() -> { log.debug("App shutdown complete"); if (onGracefulShutDownHandler != null) onGracefulShutDownHandler.run(); }); } else if (onGracefulShutDownHandler != null) { onGracefulShutDownHandler.run(); } }, 200, TimeUnit.MILLISECONDS); shutDownRequested = true; } } /////////////////////////////////////////////////////////////////////////////////////////// // UncaughtExceptionHandler implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void handleUncaughtException(Throwable throwable, boolean doShutDown) { if (!shutDownRequested) { try { try { log.error(throwable.getMessage()); } catch (Throwable throwable3) { log.error("Error at displaying Throwable."); throwable3.printStackTrace(); } if (doShutDown) stop(); } catch (Throwable throwable2) { // If printStackTrace cause a further exception we don't pass the throwable to the Popup. log.error(throwable2.toString()); if (doShutDown) stop(); } } } } ================================================ FILE: core/src/main/java/haveno/core/app/HavenoHeadlessAppMain.java ================================================ /* * 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 . */ package haveno.core.app; import com.google.common.util.concurrent.ThreadFactoryBuilder; import haveno.common.UserThread; import haveno.common.app.AppModule; import haveno.common.app.Version; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; @Slf4j public class HavenoHeadlessAppMain extends HavenoExecutable { protected HeadlessApp headlessApp; public HavenoHeadlessAppMain() { super("Haveno Daemon", "havenod", HavenoExecutable.DEFAULT_APP_NAME, Version.VERSION); } public static void main(String[] args) throws Exception { // For some reason the JavaFX launch process results in us losing the thread // context class loader: reset it. In order to work around a bug in JavaFX 8u25 // and below, you must include the following code as the first line of your // realMain method: Thread.currentThread().setContextClassLoader(HavenoHeadlessAppMain.class.getClassLoader()); new HavenoHeadlessAppMain().execute(args); } @Override protected int doExecute() { super.doExecute(); return keepRunning(); } /////////////////////////////////////////////////////////////////////////////////////////// // First synchronous execution tasks /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void configUserThread() { final ThreadFactory threadFactory = new ThreadFactoryBuilder() .setNameFormat(this.getClass().getSimpleName()) .setDaemon(true) .build(); UserThread.setExecutor(Executors.newSingleThreadExecutor(threadFactory)); } @Override protected void launchApplication() { headlessApp = new HavenoHeadlessApp(); UserThread.execute(this::onApplicationLaunched); } @Override protected void onApplicationLaunched() { super.onApplicationLaunched(); headlessApp.setGracefulShutDownHandler(this); } @Override public void handleUncaughtException(Throwable throwable, boolean doShutDown) { headlessApp.handleUncaughtException(throwable, doShutDown); } @Override public void onSetupComplete() { log.info("onSetupComplete"); } /////////////////////////////////////////////////////////////////////////////////////////// // We continue with a series of synchronous execution tasks /////////////////////////////////////////////////////////////////////////////////////////// @Override protected AppModule getModule() { return new CoreModule(config); } @Override protected void applyInjector() { super.applyInjector(); headlessApp.setInjector(injector); } @Override protected void startApplication() { // We need to be in user thread! We mapped at launchApplication already... headlessApp.startApplication(); // In headless mode we don't have an async behaviour so we trigger the setup by calling onApplicationStarted onApplicationStarted(); } } ================================================ FILE: core/src/main/java/haveno/core/app/HavenoSetup.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.app; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.inject.name.Named; import haveno.common.ThreadUtils; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.app.Version; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; import haveno.common.file.FileUtil; import haveno.common.util.InvalidVersionException; import haveno.common.util.Utilities; import haveno.core.account.sign.SignedWitness; import haveno.core.account.sign.SignedWitnessStorageService; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.alert.Alert; import haveno.core.alert.AlertManager; import haveno.core.alert.PrivateNotificationManager; import haveno.core.alert.PrivateNotificationPayload; import haveno.core.api.CoreContext; import haveno.core.api.XmrConnectionService; import haveno.core.api.XmrConnectionService.XmrConnectionFallbackType; import haveno.core.api.XmrLocalNode; import haveno.core.locale.Res; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.AmazonGiftCardAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.RevolutAccount; import haveno.core.payment.payload.PaymentMethod; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.mediation.MediationManager; import haveno.core.support.dispute.refund.RefundManager; import haveno.core.trade.HavenoUtils; import haveno.core.trade.TradeManager; import haveno.core.user.Preferences; import haveno.core.user.Preferences.UseTorForXmr; import haveno.core.user.User; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.core.xmr.setup.WalletsSetup; import haveno.core.xmr.wallet.WalletsManager; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.Socks5ProxyProvider; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import haveno.network.utils.Utils; 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.Objects; import java.util.Random; import java.util.Scanner; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import javafx.collections.ListChangeListener; import javax.annotation.Nullable; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.monadic.MonadicBinding; @Slf4j @Singleton public class HavenoSetup { private static final String VERSION_FILE_NAME = "version"; private static final int STARTUP_TIMEOUT_MINUTES = 5; private final DomainInitialisation domainInitialisation; private final P2PNetworkSetup p2PNetworkSetup; private final WalletAppSetup walletAppSetup; private final WalletsManager walletsManager; private final WalletsSetup walletsSetup; private final XmrConnectionService xmrConnectionService; @Getter private final XmrWalletService xmrWalletService; private final P2PService p2PService; private final PrivateNotificationManager privateNotificationManager; private final SignedWitnessStorageService signedWitnessStorageService; private final TradeManager tradeManager; private final OpenOfferManager openOfferManager; private final Preferences preferences; private final User user; private final AlertManager alertManager; @Getter private final Config config; @Getter private final CoreContext coreContext; private final AccountAgeWitnessService accountAgeWitnessService; private final TorSetup torSetup; private final CoinFormatter formatter; private final XmrLocalNode xmrLocalNode; private final AppStartupState appStartupState; private final MediationManager mediationManager; private final RefundManager refundManager; private final ArbitrationManager arbitrationManager; private final StringProperty topErrorMsg = new SimpleStringProperty(); @Setter @Nullable private Consumer displayTacHandler; @Setter @Nullable private Consumer chainFileLockedExceptionHandler, lockedUpFundsHandler, filterWarningHandler, displaySecurityRecommendationHandler, displayLocalhostHandler, wrongOSArchitectureHandler, displaySignedByArbitratorHandler, displaySignedByPeerHandler, displayPeerLimitLiftedHandler, displayPeerSignerHandler, rejectedTxErrorMessageHandler; @Setter @Nullable private Consumer displayMoneroConnectionFallbackHandler; @Setter @Nullable private Consumer displayTorNetworkSettingsHandler; @Setter @Nullable private Runnable showFirstPopupIfResyncSPVRequestedHandler; @Setter @Nullable private Consumer displayAlertHandler; @Setter @Nullable private BiConsumer displayUpdateHandler; @Setter @Nullable private Consumer displayPrivateNotificationHandler; @Setter @Nullable private Runnable showPopupIfInvalidXmrConfigHandler; @Setter @Nullable private Consumer> revolutAccountsUpdateHandler; @Setter @Nullable private Consumer> amazonGiftCardAccountsUpdateHandler; @Setter @Nullable private Runnable osxKeyLoggerWarningHandler; @Setter @Nullable private Runnable qubesOSInfoHandler; @Setter @Nullable private Runnable torAddressUpgradeHandler; @Setter @Nullable private Consumer downGradePreventionHandler; @Getter final BooleanProperty newVersionAvailableProperty = new SimpleBooleanProperty(false); private BooleanProperty p2pNetworkReady; private final BooleanProperty walletInitialized = new SimpleBooleanProperty(); private boolean allBasicServicesInitialized; @SuppressWarnings("FieldCanBeLocal") private MonadicBinding p2pNetworkAndWalletInitialized; private Timer startupTimeout; private final List havenoSetupListeners = new ArrayList<>(); public interface HavenoSetupListener { default void onInitP2pNetwork() { } default void onInitWallet() { } default void onRequestWalletPassword() { } void onSetupComplete(); } @Inject public HavenoSetup(DomainInitialisation domainInitialisation, P2PNetworkSetup p2PNetworkSetup, WalletAppSetup walletAppSetup, WalletsManager walletsManager, WalletsSetup walletsSetup, XmrConnectionService xmrConnectionService, XmrWalletService xmrWalletService, P2PService p2PService, PrivateNotificationManager privateNotificationManager, SignedWitnessStorageService signedWitnessStorageService, TradeManager tradeManager, OpenOfferManager openOfferManager, Preferences preferences, User user, AlertManager alertManager, Config config, CoreContext coreContext, AccountAgeWitnessService accountAgeWitnessService, TorSetup torSetup, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, XmrLocalNode xmrLocalNode, AppStartupState appStartupState, Socks5ProxyProvider socks5ProxyProvider, MediationManager mediationManager, RefundManager refundManager, ArbitrationManager arbitrationManager) { this.domainInitialisation = domainInitialisation; this.p2PNetworkSetup = p2PNetworkSetup; this.walletAppSetup = walletAppSetup; this.walletsManager = walletsManager; this.walletsSetup = walletsSetup; this.xmrConnectionService = xmrConnectionService; this.xmrWalletService = xmrWalletService; this.p2PService = p2PService; this.privateNotificationManager = privateNotificationManager; this.signedWitnessStorageService = signedWitnessStorageService; this.tradeManager = tradeManager; this.openOfferManager = openOfferManager; this.preferences = preferences; this.user = user; this.alertManager = alertManager; this.config = config; this.coreContext = coreContext; this.accountAgeWitnessService = accountAgeWitnessService; this.torSetup = torSetup; this.formatter = formatter; this.xmrLocalNode = xmrLocalNode; this.appStartupState = appStartupState; this.mediationManager = mediationManager; this.refundManager = refundManager; this.arbitrationManager = arbitrationManager; HavenoUtils.havenoSetup = this; HavenoUtils.preferences = preferences; } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void displayAlertIfPresent(Alert alert, boolean openNewVersionPopup) { if (alert == null) return; if (alert.isSoftwareUpdateNotification()) { // only process if the alert version is "newer" than ours if (alert.isNewVersion(preferences)) { user.setDisplayedAlert(alert); // save context to compare later newVersionAvailableProperty.set(true); // shows link in footer bar if ((alert.canShowPopup(preferences) || openNewVersionPopup) && displayUpdateHandler != null) { displayUpdateHandler.accept(alert, alert.showAgainKey()); } } } else { // it is a normal message alert final Alert displayedAlert = user.getDisplayedAlert(); if ((displayedAlert == null || !displayedAlert.equals(alert)) && displayAlertHandler != null) displayAlertHandler.accept(alert); } } /////////////////////////////////////////////////////////////////////////////////////////// // Main startup tasks /////////////////////////////////////////////////////////////////////////////////////////// public void addHavenoSetupListener(HavenoSetupListener listener) { havenoSetupListeners.add(listener); } public void start() { // If user tried to downgrade we require a shutdown if ((Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_MAINNET || Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_STAGENET) && hasDowngraded(downGradePreventionHandler)) { return; } persistHavenoVersion(); maybeShowTac(this::step2); } private void step2() { readMapsFromResources(this::step3); checkForCorrectOSArchitecture(); checkOSXVersion(); checkIfRunningOnQubesOS(); } private void step3() { maybeInstallDependencies(); startP2pNetworkAndWallet(this::step4); } private void step4() { // run off main thread so domain initialization does not block UI ThreadUtils.submitToPool(() -> { initDomainServices(); havenoSetupListeners.forEach(HavenoSetupListener::onSetupComplete); // We set that after calling the setupCompleteHandler to not trigger a popup from the dev dummy accounts // in MainViewModel maybeShowSecurityRecommendation(); maybeShowLocalhostRunningInfo(); maybeShowAccountSigningStateInfo(); maybeShowTorAddressUpgradeInformation(); checkInboundConnections(); }); } /////////////////////////////////////////////////////////////////////////////////////////// // Sub tasks /////////////////////////////////////////////////////////////////////////////////////////// private void maybeShowTac(Runnable nextStep) { if (!preferences.isTacAcceptedV120() && !DevEnv.isDevMode()) { if (displayTacHandler != null) displayTacHandler.accept(() -> { preferences.setTacAcceptedV120(true); nextStep.run(); }); } else { nextStep.run(); } } private void maybeInstallDependencies() { try { // install monerod File monerodFile = new File(XmrLocalNode.MONEROD_PATH); String monerodResourcePath = "bin/" + XmrLocalNode.MONEROD_NAME; if (!monerodFile.exists() || (config.updateXmrBinaries && !FileUtil.resourceEqualToFile(monerodResourcePath, monerodFile))) { log.info("Installing monerod"); monerodFile.getParentFile().mkdirs(); FileUtil.resourceToFile("bin/" + XmrLocalNode.MONEROD_NAME, monerodFile); monerodFile.setExecutable(true); } // install monero-wallet-rpc File moneroWalletRpcFile = new File(XmrWalletService.MONERO_WALLET_RPC_PATH); String moneroWalletRpcResourcePath = "bin/" + XmrWalletService.MONERO_WALLET_RPC_NAME; if (!moneroWalletRpcFile.exists() || (config.updateXmrBinaries && !FileUtil.resourceEqualToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile))) { log.info("Installing monero-wallet-rpc"); moneroWalletRpcFile.getParentFile().mkdirs(); FileUtil.resourceToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile); moneroWalletRpcFile.setExecutable(true); } } catch (Exception e) { log.warn("Failed to install Monero binaries: {}\n", e.getMessage(), e); } } private void readMapsFromResources(Runnable completeHandler) { String postFix = "_" + config.baseCurrencyNetwork.name(); p2PService.getP2PDataStorage().readFromResources(postFix, completeHandler); } private synchronized void resetStartupTimeout() { if (p2pNetworkAndWalletInitialized != null && p2pNetworkAndWalletInitialized.get()) return; // skip if already initialized if (startupTimeout != null) startupTimeout.stop(); startupTimeout = UserThread.runAfter(() -> { if (p2PNetworkSetup.p2pNetworkFailed.get() || walletsSetup.walletsSetupFailed.get()) { // Skip this timeout action if the p2p network or wallet setup failed // since an error prompt will be shown containing the error message return; } log.warn("startupTimeout called"); if (displayTorNetworkSettingsHandler != null) displayTorNetworkSettingsHandler.accept(true); // log.info("Set log level for org.berndpruenster.netlayer classes to DEBUG to show more details for " + // "Tor network connection issues"); // Log.setCustomLogLevel("org.berndpruenster.netlayer", Level.DEBUG); }, STARTUP_TIMEOUT_MINUTES, TimeUnit.MINUTES); } private void startP2pNetworkAndWallet(Runnable nextStep) { ChangeListener walletInitializedListener = (observable, oldValue, newValue) -> { // TODO that seems to be called too often if Tor takes longer to start up... if (newValue && !p2pNetworkReady.get() && displayTorNetworkSettingsHandler != null) displayTorNetworkSettingsHandler.accept(true); }; // start startup timeout resetStartupTimeout(); // reset startup timeout on progress getXmrDaemonSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout()); getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout()); // listen for fallback handling getConnectionServiceFallbackType().addListener((observable, oldValue, newValue) -> { if (displayMoneroConnectionFallbackHandler == null) return; displayMoneroConnectionFallbackHandler.accept(newValue); }); log.info("Init P2P network"); havenoSetupListeners.forEach(HavenoSetupListener::onInitP2pNetwork); p2pNetworkReady = p2PNetworkSetup.init(this::initWallet, displayTorNetworkSettingsHandler); // need to store it to not get garbage collected p2pNetworkAndWalletInitialized = EasyBind.combine(walletInitialized, p2pNetworkReady, (a, b) -> { log.info("walletInitialized={}, p2pNetWorkReady={}", a, b); return a && b; }); p2pNetworkAndWalletInitialized.subscribe((observable, oldValue, newValue) -> { if (newValue) { startupTimeout.stop(); walletInitialized.removeListener(walletInitializedListener); if (displayTorNetworkSettingsHandler != null) displayTorNetworkSettingsHandler.accept(false); nextStep.run(); } }); } private void initWallet() { log.info("Init wallet"); havenoSetupListeners.forEach(HavenoSetupListener::onInitWallet); walletAppSetup.init(chainFileLockedExceptionHandler, showFirstPopupIfResyncSPVRequestedHandler, showPopupIfInvalidXmrConfigHandler, () -> {}, () -> {}); } private void initDomainServices() { log.info("initDomainServices"); domainInitialisation.initDomainServices(rejectedTxErrorMessageHandler, displayPrivateNotificationHandler, filterWarningHandler, revolutAccountsUpdateHandler, amazonGiftCardAccountsUpdateHandler); alertManager.alertMessageProperty().addListener((observable, oldValue, newValue) -> displayAlertIfPresent(newValue, false)); displayAlertIfPresent(alertManager.alertMessageProperty().get(), false); allBasicServicesInitialized = true; appStartupState.onDomainServicesInitialized(); } /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// @Nullable public static String getLastHavenoVersion() { 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(getLastHavenoVersion()); } public static boolean hasDowngraded(String lastVersion) { return lastVersion != null && Version.isNewVersion(lastVersion, Version.VERSION); } public static boolean hasDowngraded(@Nullable Consumer downGradePreventionHandler) { String lastVersion = getLastHavenoVersion(); 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 persistHavenoVersion() { 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(); // We don't force a shutdown as the osArchitecture might in strange cases return a wrong value. // Needs at least more testing on different machines... wrongOSArchitectureHandler.accept(Res.get("popup.warning.wrongVersion", osArchitecture, Utilities.getJVMArchitecture(), osArchitecture)); } } private void checkOSXVersion() { if (Utilities.isOSX() && osxKeyLoggerWarningHandler != null) { try { // Seems it was introduced at 10.14: https://github.com/wesnoth/wesnoth/issues/4109 if (Utilities.getMajorVersion() >= 10 && Utilities.getMinorVersion() >= 14) { osxKeyLoggerWarningHandler.run(); } } catch (InvalidVersionException | NumberFormatException e) { log.warn(e.getMessage()); } } } /** * If Haveno is running on an OS that is virtualized under Qubes, show info popup with * link to the Setup Guide. The guide documents what other steps are needed, in * addition to installing the Linux package (qube sizing, etc) */ private void checkIfRunningOnQubesOS() { if (Utilities.isQubesOS() && qubesOSInfoHandler != null) { qubesOSInfoHandler.run(); } } /** * Check if we have inbound connections. If not, try to ping ourselves. * If Haveno cannot connect to its own onion address through Tor, display * an informative message to let the user know to configure their firewall else * their offers will not be reachable. * Repeat this test hourly. */ private void checkInboundConnections() { NodeAddress onionAddress = p2PService.getNetworkNode().nodeAddressProperty().get(); if (onionAddress == null || !onionAddress.getFullAddress().contains("onion")) { return; } if (p2PService.getNetworkNode().upTime() > TimeUnit.HOURS.toMillis(1) && p2PService.getNetworkNode().getInboundConnectionCount() == 0) { // we've been online a while and did not find any inbound connections; lets try the self-ping check log.info("no recent inbound connections found, starting the self-ping test"); privateNotificationManager.sendPing(onionAddress, stringResult -> { log.info(stringResult); if (stringResult.contains("failed")) { getP2PNetworkStatusIconId().set("flashing:image-yellow_circle"); } }); } // schedule another inbound connection check for later int nextCheckInMinutes = 30 + new Random().nextInt(30); log.debug("next inbound connections check in {} minutes", nextCheckInMinutes); UserThread.runAfter(this::checkInboundConnections, nextCheckInMinutes, TimeUnit.MINUTES); } private void maybeShowSecurityRecommendation() { if (user.getPaymentAccountsAsObservable() == null) return; String key = "remindPasswordAndBackup"; user.getPaymentAccountsAsObservable().addListener((ListChangeListener) change -> { if (!walletsManager.areWalletsEncrypted() && !user.isPaymentAccountImport() && preferences.showAgain(key) && change.next() && change.wasAdded() && displaySecurityRecommendationHandler != null) displaySecurityRecommendationHandler.accept(key); }); } private void maybeShowLocalhostRunningInfo() { maybeTriggerDisplayHandler("xmrLocalNode", displayLocalhostHandler, xmrLocalNode.shouldBeUsed()); } private void maybeShowAccountSigningStateInfo() { String keySignedByArbitrator = "accountSignedByArbitrator"; String keySignedByPeer = "accountSignedByPeer"; String keyPeerLimitedLifted = "accountLimitLifted"; String keyPeerSigner = "accountPeerSigner"; // check signed witness on startup checkSigningState(AccountAgeWitnessService.SignState.ARBITRATOR, keySignedByArbitrator, displaySignedByArbitratorHandler); checkSigningState(AccountAgeWitnessService.SignState.PEER_INITIAL, keySignedByPeer, displaySignedByPeerHandler); checkSigningState(AccountAgeWitnessService.SignState.PEER_LIMIT_LIFTED, keyPeerLimitedLifted, displayPeerLimitLiftedHandler); checkSigningState(AccountAgeWitnessService.SignState.PEER_SIGNER, keyPeerSigner, displayPeerSignerHandler); // check signed witness during runtime p2PService.getP2PDataStorage().addAppendOnlyDataStoreListener( payload -> { maybeTriggerDisplayHandler(keySignedByArbitrator, displaySignedByArbitratorHandler, isSignedWitnessOfMineWithState(payload, AccountAgeWitnessService.SignState.ARBITRATOR)); maybeTriggerDisplayHandler(keySignedByPeer, displaySignedByPeerHandler, isSignedWitnessOfMineWithState(payload, AccountAgeWitnessService.SignState.PEER_INITIAL)); maybeTriggerDisplayHandler(keyPeerLimitedLifted, displayPeerLimitLiftedHandler, isSignedWitnessOfMineWithState(payload, AccountAgeWitnessService.SignState.PEER_LIMIT_LIFTED)); maybeTriggerDisplayHandler(keyPeerSigner, displayPeerSignerHandler, isSignedWitnessOfMineWithState(payload, AccountAgeWitnessService.SignState.PEER_SIGNER)); }); } private void checkSigningState(AccountAgeWitnessService.SignState state, String key, Consumer displayHandler) { boolean signingStateFound = signedWitnessStorageService.getMap().values().stream() .anyMatch(payload -> isSignedWitnessOfMineWithState(payload, state)); maybeTriggerDisplayHandler(key, displayHandler, signingStateFound); } private boolean isSignedWitnessOfMineWithState(PersistableNetworkPayload payload, AccountAgeWitnessService.SignState state) { if (payload instanceof SignedWitness && user.getPaymentAccounts() != null) { // We know at this point that it is already added to the signed witness list // Check if new signed witness is for one of my own accounts return user.getPaymentAccounts().stream() .filter(a -> PaymentMethod.hasChargebackRisk(a.getPaymentMethod(), a.getTradeCurrencies())) .filter(a -> Arrays.equals(((SignedWitness) payload).getAccountAgeWitnessHash(), accountAgeWitnessService.getMyWitness(a.getPaymentAccountPayload()).getHash())) .anyMatch(a -> accountAgeWitnessService.getSignState(accountAgeWitnessService.getMyWitness( a.getPaymentAccountPayload())).equals(state)); } return false; } private void maybeTriggerDisplayHandler(String key, Consumer displayHandler, boolean signingStateFound) { if (signingStateFound && preferences.showAgain(key) && displayHandler != null) { displayHandler.accept(key); } } private void maybeShowTorAddressUpgradeInformation() { if (Config.baseCurrencyNetwork().isTestnet() || Utils.isV3Address(Objects.requireNonNull(p2PService.getNetworkNode().getNodeAddress()).getHostName())) { return; } maybeRunTorNodeAddressUpgradeHandler(); tradeManager.getNumPendingTrades().addListener((observable, oldValue, newValue) -> { long numPendingTrades = (long) newValue; if (numPendingTrades == 0) { maybeRunTorNodeAddressUpgradeHandler(); } }); } private void maybeRunTorNodeAddressUpgradeHandler() { if (mediationManager.getDisputesAsObservableList().stream().allMatch(Dispute::isClosed) && refundManager.getDisputesAsObservableList().stream().allMatch(Dispute::isClosed) && arbitrationManager.getDisputesAsObservableList().stream().allMatch(Dispute::isClosed) && tradeManager.getNumPendingTrades().isEqualTo(0).get()) { Objects.requireNonNull(torAddressUpgradeHandler).run(); } } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// // Wallet public StringProperty getXmrInfo() { return walletAppSetup.getXmrInfo(); } public DoubleProperty getXmrDaemonSyncProgress() { return walletAppSetup.getXmrDaemonSyncProgress(); } public DoubleProperty getXmrWalletSyncProgress() { return walletAppSetup.getXmrWalletSyncProgress(); } public StringProperty getConnectionServiceErrorMsg() { return xmrConnectionService.getConnectionServiceErrorMsg(); } public ObjectProperty getConnectionServiceFallbackType() { return xmrConnectionService.getConnectionServiceFallbackType(); } public StringProperty getTopErrorMsg() { return topErrorMsg; } public StringProperty getXmrSplashSyncIconId() { return walletAppSetup.getXmrSplashSyncIconId(); } public ObjectProperty getUseTorForXmr() { return walletAppSetup.getUseTorForXmr(); } // P2P public StringProperty getP2PNetworkInfo() { return p2PNetworkSetup.getP2PNetworkInfo(); } public BooleanProperty getSplashP2PNetworkAnimationVisible() { return p2PNetworkSetup.getSplashP2PNetworkAnimationVisible(); } public StringProperty getP2pNetworkWarnMsg() { return p2PNetworkSetup.getP2pNetworkWarnMsg(); } public StringProperty getP2PNetworkIconId() { return p2PNetworkSetup.getP2PNetworkIconId(); } public StringProperty getP2PNetworkStatusIconId() { return p2PNetworkSetup.getP2PNetworkStatusIconId(); } public BooleanProperty getUpdatedDataReceived() { return p2PNetworkSetup.getUpdatedDataReceived(); } public StringProperty getP2pNetworkLabelId() { return p2PNetworkSetup.getP2pNetworkLabelId(); } public BooleanProperty getWalletInitialized() { return walletInitialized; } public AppStartupState getAppStartupState() { return appStartupState; } } ================================================ FILE: core/src/main/java/haveno/core/app/HeadlessApp.java ================================================ /* * 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 . */ package haveno.core.app; import com.google.inject.Injector; import haveno.common.setup.GracefulShutDownHandler; import haveno.common.setup.UncaughtExceptionHandler; public interface HeadlessApp extends UncaughtExceptionHandler, HavenoSetup.HavenoSetupListener { void setGracefulShutDownHandler(GracefulShutDownHandler gracefulShutDownHandler); void setInjector(Injector injector); void startApplication(); } ================================================ FILE: core/src/main/java/haveno/core/app/P2PNetworkSetup.java ================================================ /* * 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 . */ package haveno.core.app; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.UserThread; import haveno.core.api.XmrConnectionService; import haveno.core.locale.Res; import haveno.core.provider.price.PriceFeedService; import haveno.core.user.Preferences; import haveno.network.p2p.P2PService; import haveno.network.p2p.P2PServiceListener; import haveno.network.p2p.network.CloseConnectionReason; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.ConnectionListener; import java.util.function.Consumer; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javax.annotation.Nullable; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.monadic.MonadicBinding; @Singleton @Slf4j public class P2PNetworkSetup { private final PriceFeedService priceFeedService; private final P2PService p2PService; private final XmrConnectionService xmrConnectionService; private final Preferences preferences; @SuppressWarnings("FieldCanBeLocal") private MonadicBinding p2PNetworkInfoBinding; @Getter final StringProperty p2PNetworkInfo = new SimpleStringProperty(); @Getter final StringProperty p2PNetworkIconId = new SimpleStringProperty(); @Getter final StringProperty p2PNetworkStatusIconId = new SimpleStringProperty(); @Getter final BooleanProperty splashP2PNetworkAnimationVisible = new SimpleBooleanProperty(true); @Getter final StringProperty p2pNetworkLabelId = new SimpleStringProperty("footer-pane"); @Getter final StringProperty p2pNetworkWarnMsg = new SimpleStringProperty(); @Getter final BooleanProperty updatedDataReceived = new SimpleBooleanProperty(); @Getter final BooleanProperty p2pNetworkFailed = new SimpleBooleanProperty(); @Inject public P2PNetworkSetup(PriceFeedService priceFeedService, P2PService p2PService, XmrConnectionService xmrConnectionService, Preferences preferences) { this.priceFeedService = priceFeedService; this.p2PService = p2PService; this.xmrConnectionService = xmrConnectionService; this.preferences = preferences; } BooleanProperty init(Runnable onReadyHandler, @Nullable Consumer displayTorNetworkSettingsHandler) { StringProperty bootstrapState = new SimpleStringProperty(); StringProperty bootstrapWarning = new SimpleStringProperty(); BooleanProperty hiddenServicePublished = new SimpleBooleanProperty(); BooleanProperty initialP2PNetworkDataReceived = new SimpleBooleanProperty(); p2PNetworkInfoBinding = EasyBind.combine(bootstrapState, bootstrapWarning, p2PService.getNumConnectedPeers(), xmrConnectionService.numConnectionsProperty(), hiddenServicePublished, initialP2PNetworkDataReceived, (state, warning, numP2pPeers, numXmrPeers, hiddenService, dataReceived) -> { String result; int p2pPeers = (int) numP2pPeers; if (warning != null && p2pPeers == 0) { result = warning; } else { String p2pInfo = ((int) numXmrPeers >= 0 ? Res.get("mainView.footer.xmrPeers", numXmrPeers) + " / " : "") + Res.get("mainView.footer.p2pPeers", numP2pPeers); if (dataReceived && hiddenService) { result = p2pInfo; } else if (p2pPeers == 0) result = state; else result = state + " / " + p2pInfo; } return result; }); p2PNetworkInfoBinding.subscribe((observable, oldValue, newValue) -> { UserThread.execute(() -> p2PNetworkInfo.set(newValue)); }); bootstrapState.set(Res.get("mainView.bootstrapState.connectionToTorNetwork")); p2PService.getNetworkNode().addConnectionListener(new ConnectionListener() { @Override public void onConnection(Connection connection) { updateNetworkStatusIndicator(); } @Override public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { updateNetworkStatusIndicator(); // We only check at seed nodes as they are running the latest version // Other disconnects might be caused by peers running an older version if (connection.getConnectionState().isSeedNode() && closeConnectionReason == CloseConnectionReason.RULE_VIOLATION) { log.warn("RULE_VIOLATION onDisconnect closeConnectionReason={}, connection={}", closeConnectionReason, connection); } } }); updateNetworkStatusIndicator(); final BooleanProperty p2pNetworkInitialized = new SimpleBooleanProperty(); p2PService.start(new P2PServiceListener() { @Override public void onTorNodeReady() { log.debug("onTorNodeReady"); bootstrapState.set(Res.get("mainView.bootstrapState.torNodeCreated")); p2PNetworkIconId.set("image-connection-tor"); // We want to get early connected to the price relay so we call it already now priceFeedService.setCurrencyCodeOnInit(); priceFeedService.requestPrices(); // invoke handler when network ready onReadyHandler.run(); } @Override public void onHiddenServicePublished() { log.debug("onHiddenServicePublished"); hiddenServicePublished.set(true); bootstrapState.set(Res.get("mainView.bootstrapState.hiddenServicePublished")); } @Override public void onDataReceived() { log.debug("onRequestingDataCompleted"); initialP2PNetworkDataReceived.set(true); bootstrapState.set(Res.get("mainView.bootstrapState.initialDataReceived")); splashP2PNetworkAnimationVisible.set(false); p2pNetworkInitialized.set(true); } @Override public void onNoSeedNodeAvailable() { log.warn("onNoSeedNodeAvailable"); if (p2PService.getNumConnectedPeers().get() == 0) bootstrapWarning.set(Res.get("mainView.bootstrapWarning.noSeedNodesAvailable")); else bootstrapWarning.set(null); splashP2PNetworkAnimationVisible.set(false); p2pNetworkInitialized.set(true); } @Override public void onNoPeersAvailable() { log.warn("onNoPeersAvailable"); if (p2PService.getNumConnectedPeers().get() == 0) { p2pNetworkWarnMsg.set(Res.get("mainView.p2pNetworkWarnMsg.noNodesAvailable")); bootstrapWarning.set(Res.get("mainView.bootstrapWarning.noNodesAvailable")); p2pNetworkLabelId.set("splash-error-state-msg"); } else { bootstrapWarning.set(null); p2pNetworkLabelId.set("footer-pane"); } splashP2PNetworkAnimationVisible.set(false); p2pNetworkInitialized.set(true); } @Override public void onUpdatedDataReceived() { log.debug("onUpdatedDataReceived"); splashP2PNetworkAnimationVisible.set(false); updatedDataReceived.set(true); } @Override public void onSetupFailed(Throwable throwable) { log.error("onSetupFailed"); p2pNetworkWarnMsg.set(Res.get("mainView.p2pNetworkWarnMsg.connectionToP2PFailed", throwable.getMessage())); splashP2PNetworkAnimationVisible.set(false); bootstrapWarning.set(Res.get("mainView.bootstrapWarning.bootstrappingToP2PFailed")); p2pNetworkLabelId.set("splash-error-state-msg"); p2pNetworkFailed.set(true); } @Override public void onRequestCustomBridges() { if (displayTorNetworkSettingsHandler != null) displayTorNetworkSettingsHandler.accept(true); } }); return p2pNetworkInitialized; } public void setSplashP2PNetworkAnimationVisible(boolean value) { splashP2PNetworkAnimationVisible.set(value); } private void updateNetworkStatusIndicator() { if (p2PService.getNetworkNode().getInboundConnectionCount() > 0) { p2PNetworkStatusIconId.set("image-green_circle"); } else if (p2PService.getNetworkNode().getOutboundConnectionCount() > 0) { p2PNetworkStatusIconId.set("image-yellow_circle"); } else { p2PNetworkStatusIconId.set("image-alert-round"); } } } ================================================ FILE: core/src/main/java/haveno/core/app/TorSetup.java ================================================ /* * 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 . */ package haveno.core.app; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.common.file.FileUtil; import haveno.common.handlers.ErrorMessageHandler; import static haveno.common.util.Preconditions.checkDir; import java.io.File; import java.io.IOException; import java.nio.file.Paths; import javax.annotation.Nullable; import org.apache.commons.lang3.exception.ExceptionUtils; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton public class TorSetup { private final File torDir; @Inject public TorSetup(@Named(Config.TOR_DIR) File torDir) { this.torDir = checkDir(torDir); } // Should only be called if needed. Slows down Tor startup from about 5 sec. to 30 sec. if it gets deleted. public void cleanupTorFiles(@Nullable Runnable resultHandler, @Nullable ErrorMessageHandler errorMessageHandler) { File hiddenservice = new File(Paths.get(torDir.getAbsolutePath(), "hiddenservice").toString()); try { FileUtil.deleteDirectory(torDir, hiddenservice, true); if (resultHandler != null) resultHandler.run(); } catch (IOException e) { log.error(ExceptionUtils.getStackTrace(e)); if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage(e.toString()); } } } ================================================ FILE: core/src/main/java/haveno/core/app/WalletAppSetup.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.app; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.UserThread; import haveno.common.config.Config; import haveno.core.api.CoreContext; import haveno.core.api.XmrConnectionService; import haveno.core.locale.Res; import haveno.core.offer.OpenOfferManager; import haveno.core.trade.TradeManager; import haveno.core.user.Preferences; import haveno.core.user.Preferences.UseTorForXmr; import haveno.core.util.FormattingUtils; import haveno.core.xmr.exceptions.InvalidHostException; import haveno.core.xmr.exceptions.RejectedTxException; import haveno.core.xmr.setup.WalletsSetup; import haveno.core.xmr.wallet.WalletsManager; import haveno.core.xmr.wallet.XmrWalletService; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javax.annotation.Nullable; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import monero.common.MoneroUtils; import org.bitcoinj.core.RejectMessage; import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.store.ChainFileLockedException; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.monadic.MonadicBinding; @Slf4j @Singleton public class WalletAppSetup { private final CoreContext coreContext; private final WalletsManager walletsManager; private final WalletsSetup walletsSetup; private final XmrConnectionService xmrConnectionService; private final XmrWalletService xmrWalletService; private final Config config; private final Preferences preferences; @SuppressWarnings("FieldCanBeLocal") private MonadicBinding xmrInfoBinding; @Getter private final DoubleProperty xmrDaemonSyncProgress = new SimpleDoubleProperty(-1); @Getter private final DoubleProperty xmrWalletSyncProgress = new SimpleDoubleProperty(-1); @Getter private final StringProperty xmrSplashSyncIconId = new SimpleStringProperty(); @Getter private final StringProperty xmrInfo = new SimpleStringProperty(Res.get("mainView.footer.xmrInfo.initializing")); @Getter private final ObjectProperty rejectedTxException = new SimpleObjectProperty<>(); @Getter private final ObjectProperty useTorForXmr = new SimpleObjectProperty(); @Inject public WalletAppSetup(CoreContext coreContext, WalletsManager walletsManager, WalletsSetup walletsSetup, XmrConnectionService xmrConnectionService, XmrWalletService xmrWalletService, Config config, Preferences preferences) { this.coreContext = coreContext; this.walletsManager = walletsManager; this.walletsSetup = walletsSetup; this.xmrConnectionService = xmrConnectionService; this.xmrWalletService = xmrWalletService; this.config = config; this.preferences = preferences; this.useTorForXmr.set(preferences.getUseTorForXmr()); } void init(@Nullable Consumer chainFileLockedExceptionHandler, @Nullable Runnable showFirstPopupIfResyncSPVRequestedHandler, @Nullable Runnable showPopupIfInvalidXmrConfigHandler, Runnable downloadCompleteHandler, Runnable walletInitializedHandler) { log.info("Initialize WalletAppSetup with monero-java v{}", MoneroUtils.getVersion()); ObjectProperty walletServiceException = new SimpleObjectProperty<>(); xmrInfoBinding = EasyBind.combine( xmrConnectionService.numUpdatesProperty(), // receives notification of any connection update xmrWalletService.downloadPercentageProperty(), xmrWalletService.walletHeightProperty(), walletServiceException, xmrConnectionService.getConnectionServiceErrorMsg(), (numConnectionUpdates, walletDownloadPercentage, walletHeight, exception, errorMsg) -> { String result; if (exception == null && errorMsg == null) { // update daemon sync progress double chainDownloadPercentageD = xmrConnectionService.downloadPercentageProperty().doubleValue(); Long bestChainHeight = xmrConnectionService.chainHeightProperty().get(); String chainHeightAsString = bestChainHeight != null && bestChainHeight > 0 ? String.valueOf(bestChainHeight) : ""; if (chainDownloadPercentageD < 1 && !xmrWalletService.wasWalletSynced()) { xmrDaemonSyncProgress.set(chainDownloadPercentageD); if (chainDownloadPercentageD > 0.0) { String synchronizingWith = Res.get("mainView.footer.xmrInfo.synchronizingWith", getXmrDaemonNetworkAsString(), chainHeightAsString, FormattingUtils.formatToClampedRoundedPercentWithSymbol(chainDownloadPercentageD)); result = Res.get("mainView.footer.xmrInfo", synchronizingWith, ""); } else { result = Res.get("mainView.footer.xmrInfo", Res.get("mainView.footer.xmrInfo.connectingTo"), getXmrDaemonNetworkAsString()); } } else { // update wallet sync progress double walletDownloadPercentageD = (double) walletDownloadPercentage; xmrWalletSyncProgress.set(walletDownloadPercentageD); Long appliedWalletHeight = walletHeight == null || ((Long) walletHeight) <= 1 ? 0 : (Long) walletHeight; String walletHeightAsString = String.valueOf(appliedWalletHeight); if (walletDownloadPercentageD >= 1 || xmrWalletService.wasWalletSynced()) { String synchronizedWith = Res.get("mainView.footer.xmrInfo.syncedWith", getXmrWalletNetworkAsString(), walletHeightAsString); String feeInfo = ""; // TODO: feeService.isFeeAvailable() returns true, disable result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo); getXmrSplashSyncIconId().set("image-connection-synced"); downloadCompleteHandler.run(); } else if (walletDownloadPercentageD >= 0) { String synchronizingWith = Res.get("mainView.footer.xmrInfo.synchronizingWalletWith", getXmrWalletNetworkAsString(), walletHeightAsString, FormattingUtils.formatToClampedRoundedPercentWithSymbol(walletDownloadPercentageD)); result = Res.get("mainView.footer.xmrInfo", synchronizingWith, ""); getXmrSplashSyncIconId().set(""); // clear synced icon } else { String synchronizedWith = Res.get("mainView.footer.xmrInfo.connectedTo", getXmrDaemonNetworkAsString(), chainHeightAsString); String feeInfo = ""; // TODO: feeService.isFeeAvailable() returns true, disable result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo); getXmrSplashSyncIconId().set("image-connection-synced"); } } } else { result = Res.get("mainView.footer.xmrInfo", Res.get("mainView.footer.xmrInfo.connectionFailed"), getXmrDaemonNetworkAsString()); if (exception != null) { if (exception instanceof TimeoutException) { xmrConnectionService.getConnectionServiceErrorMsg().set(Res.get("mainView.walletServiceErrorMsg.timeout")); } else if (exception.getCause() instanceof BlockStoreException) { if (exception.getCause().getCause() instanceof ChainFileLockedException && chainFileLockedExceptionHandler != null) { chainFileLockedExceptionHandler.accept(Res.get("popup.warning.startupFailed.twoInstances")); } } else if (exception instanceof RejectedTxException) { rejectedTxException.set((RejectedTxException) exception); xmrConnectionService.getConnectionServiceErrorMsg().set(Res.get("mainView.walletServiceErrorMsg.rejectedTxException", exception.getMessage())); } else { xmrConnectionService.getConnectionServiceErrorMsg().set(Res.get("mainView.walletServiceErrorMsg.connectionError", exception.getMessage())); } } else { xmrConnectionService.getConnectionServiceErrorMsg().set(errorMsg); } } return result; }); xmrInfoBinding.subscribe((observable, oldValue, newValue) -> UserThread.execute(() -> xmrInfo.set(newValue))); walletsSetup.initialize(null, () -> { walletInitializedHandler.run(); }, exception -> { if (exception instanceof InvalidHostException && showPopupIfInvalidXmrConfigHandler != null) { showPopupIfInvalidXmrConfigHandler.run(); } else { walletServiceException.set(exception); } }); } void setRejectedTxErrorMessageHandler(Consumer rejectedTxErrorMessageHandler, OpenOfferManager openOfferManager, TradeManager tradeManager) { getRejectedTxException().addListener((observable, oldValue, newValue) -> { if (newValue == null || newValue.getTxId() == null) { return; } RejectMessage rejectMessage = newValue.getRejectMessage(); log.warn("We received reject message: {}", rejectMessage); // TODO: Find out which reject messages are critical and which not. // We got a report where a "tx already known" message caused a failed trade but the deposit tx was valid. // To avoid such false positives we only handle reject messages which we consider clearly critical. switch (rejectMessage.getReasonCode()) { case OBSOLETE: case DUPLICATE: case NONSTANDARD: case CHECKPOINT: case OTHER: // We ignore those cases to avoid that not critical reject messages trigger a failed trade. log.warn("We ignore that reject message as it is likely not critical."); break; case MALFORMED: case INVALID: case DUST: case INSUFFICIENTFEE: // We delay as we might get the rejected tx error before we have completed the create offer protocol log.warn("We handle that reject message as it is likely critical."); UserThread.runAfter(() -> { String txId = newValue.getTxId(); tradeManager.getObservableList().stream() .filter(trade -> trade.getOffer() != null) .forEach(trade -> { String details = null; if (txId.equals(trade.getMaker().getDepositTxHash())) { details = Res.get("popup.warning.trade.txRejected.deposit"); // TODO (woodser): txRejected.maker_deposit, txRejected.taker_deposit } if (txId.equals(trade.getTaker().getDepositTxHash())) { details = Res.get("popup.warning.trade.txRejected.deposit"); } if (details != null) { // We delay to avoid concurrent modification exceptions 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)); } }, 1); } }); }, 3); } }); } private String getXmrDaemonNetworkAsString() { String postFix; if (xmrConnectionService.isConnectionLocalHost()) postFix = " " + Res.get("mainView.footer.localhostMoneroNode"); else if (xmrConnectionService.isProxyApplied()) postFix = " " + Res.get("mainView.footer.usingTor"); else postFix = ""; return Res.get(config.baseCurrencyNetwork.name()) + postFix; } private String getXmrWalletNetworkAsString() { String postFix; if (xmrConnectionService.isConnectionLocalHost()) postFix = " " + Res.get("mainView.footer.localhostMoneroNode"); else if (xmrWalletService.isProxyApplied()) postFix = " " + Res.get("mainView.footer.usingTor"); else postFix = " " + Res.get("mainView.footer.clearnet"); return Res.get(config.baseCurrencyNetwork.name()) + postFix; } } ================================================ FILE: core/src/main/java/haveno/core/app/misc/AppSetup.java ================================================ /* * 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 . */ package haveno.core.app.misc; import com.google.inject.Inject; import haveno.common.app.Version; import haveno.common.config.Config; import lombok.extern.slf4j.Slf4j; @Slf4j public abstract class AppSetup { protected final Config config; @Inject public AppSetup(Config config) { // we need to reference it so the seed node stores tradeStatistics this.config = config; Version.setBaseCryptoNetworkId(this.config.baseCurrencyNetwork.ordinal()); Version.printVersion(); } public void start() { initPersistedDataHosts(); initBasicServices(); } abstract void initPersistedDataHosts(); abstract void initBasicServices(); } ================================================ FILE: core/src/main/java/haveno/core/app/misc/AppSetupWithP2P.java ================================================ /* * 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 . */ package haveno.core.app.misc; import com.google.inject.Inject; import haveno.common.config.Config; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.persistable.PersistedDataHost; import haveno.core.account.sign.SignedWitnessService; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.filter.FilterManager; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.network.p2p.P2PService; import haveno.network.p2p.P2PServiceListener; import haveno.network.p2p.network.CloseConnectionReason; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.ConnectionListener; import haveno.network.p2p.peers.PeerManager; import haveno.network.p2p.storage.P2PDataStorage; import java.util.ArrayList; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import lombok.extern.slf4j.Slf4j; @Slf4j public class AppSetupWithP2P extends AppSetup { protected final P2PService p2PService; protected final AccountAgeWitnessService accountAgeWitnessService; private final SignedWitnessService signedWitnessService; protected final FilterManager filterManager; private final P2PDataStorage p2PDataStorage; private final PeerManager peerManager; protected final TradeStatisticsManager tradeStatisticsManager; protected ArrayList persistedDataHosts; protected BooleanProperty p2pNetWorkReady; @Inject public AppSetupWithP2P(P2PService p2PService, P2PDataStorage p2PDataStorage, PeerManager peerManager, TradeStatisticsManager tradeStatisticsManager, AccountAgeWitnessService accountAgeWitnessService, SignedWitnessService signedWitnessService, FilterManager filterManager, Config config) { super(config); this.p2PService = p2PService; this.p2PDataStorage = p2PDataStorage; this.peerManager = peerManager; this.tradeStatisticsManager = tradeStatisticsManager; this.accountAgeWitnessService = accountAgeWitnessService; this.signedWitnessService = signedWitnessService; this.filterManager = filterManager; this.persistedDataHosts = new ArrayList<>(); } @Override public void initPersistedDataHosts() { persistedDataHosts.add(p2PDataStorage); persistedDataHosts.add(peerManager); // we apply at startup the reading of persisted data but don't want to get it triggered in the constructor persistedDataHosts.forEach(e -> { try { e.readPersisted(() -> { }); } catch (Throwable e1) { log.error("readPersisted error", e1); } }); } @Override protected void initBasicServices() { String postFix = "_" + config.baseCurrencyNetwork.name(); p2PDataStorage.readFromResources(postFix, this::startInitP2PNetwork); } private void startInitP2PNetwork() { p2pNetWorkReady = initP2PNetwork(); p2pNetWorkReady.addListener((observable, oldValue, newValue) -> { if (newValue) onBasicServicesInitialized(); }); } /////////////////////////////////////////////////////////////////////////////////////////// // Initialisation /////////////////////////////////////////////////////////////////////////////////////////// private BooleanProperty initP2PNetwork() { log.info("initP2PNetwork"); p2PService.getNetworkNode().addConnectionListener(new ConnectionListener() { @Override public void onConnection(Connection connection) { } @Override public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { // We only check at seed nodes as they are running the latest version // Other disconnects might be caused by peers running an older version if (connection.getConnectionState().isSeedNode() && closeConnectionReason == CloseConnectionReason.RULE_VIOLATION) { log.warn("RULE_VIOLATION onDisconnect closeConnectionReason={}. connection={}", closeConnectionReason, connection); } } }); final BooleanProperty p2pNetworkInitialized = new SimpleBooleanProperty(); p2PService.start(new P2PServiceListener() { @Override public void onTorNodeReady() { } @Override public void onHiddenServicePublished() { log.info("onHiddenServicePublished"); } @Override public void onDataReceived() { log.info("onRequestingDataCompleted"); p2pNetworkInitialized.set(true); } @Override public void onNoSeedNodeAvailable() { log.info("onNoSeedNodeAvailable"); p2pNetworkInitialized.set(true); } @Override public void onNoPeersAvailable() { log.info("onNoPeersAvailable"); p2pNetworkInitialized.set(true); } @Override public void onUpdatedDataReceived() { log.info("onUpdatedDataReceived"); } @Override public void onSetupFailed(Throwable throwable) { log.error(throwable.toString()); } @Override public void onRequestCustomBridges() { } }); return p2pNetworkInitialized; } protected void onBasicServicesInitialized() { log.info("onBasicServicesInitialized"); PersistenceManager.onAllServicesInitialized(); p2PService.onAllServicesInitialized(); tradeStatisticsManager.onAllServicesInitialized(); accountAgeWitnessService.onAllServicesInitialized(); signedWitnessService.onAllServicesInitialized(); filterManager.onAllServicesInitialized(); } } ================================================ FILE: core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.app.misc; import com.google.common.util.concurrent.ThreadFactoryBuilder; import haveno.common.ThreadUtils; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.config.Config; import haveno.common.file.JsonFileManager; import haveno.common.handlers.ResultHandler; import haveno.common.persistence.PersistenceManager; import haveno.common.setup.GracefulShutDownHandler; import haveno.common.util.Profiler; import haveno.core.api.XmrConnectionService; import haveno.core.app.AvoidStandbyModeService; import haveno.core.app.HavenoExecutable; import haveno.core.offer.OfferBookService; import haveno.core.offer.OpenOfferManager; import haveno.core.provider.price.PriceFeedService; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.trade.TradeManager; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.xmr.setup.WalletsSetup; import haveno.core.xmr.wallet.BtcWalletService; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import haveno.network.p2p.seed.SeedNodeRepository; import lombok.extern.slf4j.Slf4j; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; @Slf4j public abstract class ExecutableForAppWithP2p extends HavenoExecutable { private static final long CHECK_MEMORY_PERIOD_SEC = 300; private static final long CHECK_SHUTDOWN_SEC = TimeUnit.HOURS.toSeconds(1); private static final long SHUTDOWN_INTERVAL = TimeUnit.HOURS.toMillis(24); private volatile boolean stopped; private final long startTime = System.currentTimeMillis(); public ExecutableForAppWithP2p(String fullName, String scriptName, String appName, String version) { super(fullName, scriptName, appName, version); } @Override protected void configUserThread() { final ThreadFactory threadFactory = new ThreadFactoryBuilder() .setNameFormat(this.getClass().getSimpleName()) .setDaemon(true) .build(); UserThread.setExecutor(Executors.newSingleThreadExecutor(threadFactory)); } @Override public void onSetupComplete() { log.info("onSetupComplete"); } // We don't use the gracefulShutDown implementation of the super class as we have a limited set of modules @Override public void gracefulShutDown(ResultHandler resultHandler) { log.info("Starting graceful shut down of {}", getClass().getSimpleName()); // ignore if shut down started if (isShutDownStarted) { log.info("Ignoring call to gracefulShutDown, already started"); return; } isShutDownStarted = true; try { if (injector != null) { // notify trade protocols and wallets to prepare for shut down Set tasks = new HashSet(); tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted()); tasks.add(() -> injector.getInstance(XmrWalletService.class).onShutDownStarted()); tasks.add(() -> injector.getInstance(XmrConnectionService.class).onShutDownStarted()); try { ThreadUtils.awaitTasks(tasks, tasks.size(), 120000l); // run in parallel with timeout } catch (Exception e) { log.error("Error awaiting tasks to complete: {}\n", e.getMessage(), e); } JsonFileManager.shutDownAllInstances(); injector.getInstance(PriceFeedService.class).shutDown(); injector.getInstance(ArbitratorManager.class).shutDown(); injector.getInstance(TradeStatisticsManager.class).shutDown(); injector.getInstance(AvoidStandbyModeService.class).shutDown(); // shut down open offer manager log.info("Shutting down OpenOfferManager"); injector.getInstance(OpenOfferManager.class).shutDown(() -> { // listen for shut down of wallets setup injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> { // shut down p2p service log.info("Shutting down P2P service"); injector.getInstance(P2PService.class).shutDown(() -> { module.close(injector); PersistenceManager.flushAllDataToDiskAtShutdown(() -> { // done shutting down log.info("Graceful shutdown completed. Exiting now."); resultHandler.handleResult(); UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_SUCCESS), 1); }); }); }); // shut down trade and wallet services log.info("Shutting down trade and wallet services"); injector.getInstance(OfferBookService.class).shutDown(); injector.getInstance(TradeManager.class).shutDown(); injector.getInstance(BtcWalletService.class).shutDown(); injector.getInstance(XmrWalletService.class).shutDown(); injector.getInstance(XmrConnectionService.class).shutDown(); injector.getInstance(WalletsSetup.class).shutDown(); }); // we wait max 5 sec. UserThread.runAfter(() -> { PersistenceManager.flushAllDataToDiskAtShutdown(() -> { resultHandler.handleResult(); log.warn("Graceful shutdown caused a timeout. Exiting now."); UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_SUCCESS), 1); }); }, 5); } else { UserThread.runAfter(() -> { resultHandler.handleResult(); System.exit(HavenoExecutable.EXIT_SUCCESS); }, 1); } } catch (Throwable t) { log.info("App shutdown failed with exception: {}\n", t.getMessage(), t); PersistenceManager.flushAllDataToDiskAtShutdown(() -> { resultHandler.handleResult(); log.info("Graceful shutdown resulted in an error. Exiting now."); UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_FAILURE), 1); }); } } public void startShutDownInterval(GracefulShutDownHandler gracefulShutDownHandler) { if (DevEnv.isDevMode() || injector.getInstance(Config.class).useLocalhostForP2P) { return; } List seedNodeAddresses = new ArrayList<>(injector.getInstance(SeedNodeRepository.class).getSeedNodeAddresses()); seedNodeAddresses.sort(Comparator.comparing(NodeAddress::getFullAddress)); NodeAddress myAddress = injector.getInstance(P2PService.class).getNetworkNode().getNodeAddress(); int myIndex = -1; for (int i = 0; i < seedNodeAddresses.size(); i++) { if (seedNodeAddresses.get(i).equals(myAddress)) { myIndex = i; break; } } if (myIndex == -1) { log.warn("We did not find our node address in the seed nodes repository. " + "We use a 24 hour delay after startup as shut down strategy." + "myAddress={}, seedNodeAddresses={}", myAddress, seedNodeAddresses); UserThread.runPeriodically(() -> { if (System.currentTimeMillis() - startTime > SHUTDOWN_INTERVAL) { log.warn("\n\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n" + "Shut down as node was running longer as {} hours" + "\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n\n", SHUTDOWN_INTERVAL / 3600000); shutDown(gracefulShutDownHandler); } }, CHECK_SHUTDOWN_SEC); return; } // We interpret the value of myIndex as hour of day (0-23). That way we avoid the risk of a restart of // multiple nodes around the same time in case it would be not deterministic. // We wrap our periodic check in a delay of 2 hours to avoid that we get // triggered multiple times after a restart while being in the same hour. It can be that we miss our target // hour during that delay but that is not considered problematic, the seed would just restart a bit longer than // 24 hours. int target = myIndex; UserThread.runAfter(() -> { // We check every hour if we are in the target hour. UserThread.runPeriodically(() -> { int currentHour = ZonedDateTime.ofInstant(Instant.now(), ZoneId.of("UTC")).getHour(); if (currentHour == target) { log.warn("\n\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n" + "Shut down node at hour {} (UTC time is {})" + "\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n\n", target, ZonedDateTime.ofInstant(Instant.now(), ZoneId.of("UTC")).toString()); shutDown(gracefulShutDownHandler); } }, TimeUnit.MINUTES.toSeconds(10)); }, TimeUnit.HOURS.toSeconds(2)); } protected void checkMemory(Config config, GracefulShutDownHandler gracefulShutDownHandler) { int maxMemory = config.maxMemory; UserThread.runPeriodically(() -> { Profiler.printSystemLoad(); if (!stopped) { long usedMemoryInMB = Profiler.getUsedMemoryInMB(); double warningTrigger = maxMemory * 0.8; if (usedMemoryInMB > warningTrigger) { log.warn("\n\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n" + "We are over 80% of our memory limit ({}) and call the GC. usedMemory: {} MB. freeMemory: {} MB" + "\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n\n", (int) warningTrigger, usedMemoryInMB, Profiler.getFreeMemoryInMB()); System.gc(); Profiler.printSystemLoad(); } UserThread.runAfter(() -> { log.info("Memory 2 sec. after calling the GC. usedMemory: {} MB. freeMemory: {} MB", Profiler.getUsedMemoryInMB(), Profiler.getFreeMemoryInMB()); }, 2); UserThread.runAfter(() -> { long usedMemory = Profiler.getUsedMemoryInMB(); if (usedMemory > maxMemory) { log.warn("\n\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n" + "We are over our memory limit ({}) and trigger a shutdown. usedMemory: {} MB. freeMemory: {} MB" + "\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n\n", maxMemory, usedMemory, Profiler.getFreeMemoryInMB()); shutDown(gracefulShutDownHandler); } }, 5); } }, CHECK_MEMORY_PERIOD_SEC); } protected void shutDown(GracefulShutDownHandler gracefulShutDownHandler) { stopped = true; gracefulShutDownHandler.gracefulShutDown(() -> { log.info("Shutdown complete"); System.exit(1); }); } } ================================================ FILE: core/src/main/java/haveno/core/app/misc/ModuleForAppWithP2p.java ================================================ /* * 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 . */ package haveno.core.app.misc; import com.google.inject.Singleton; import haveno.common.ClockWatcher; import haveno.common.app.AppModule; import haveno.common.config.Config; import haveno.common.crypto.KeyRing; import haveno.common.crypto.KeyStorage; import haveno.common.proto.network.NetworkProtoResolver; import haveno.common.proto.persistable.PersistenceProtoResolver; import haveno.core.alert.AlertModule; import haveno.core.app.TorSetup; import haveno.core.filter.FilterModule; import haveno.core.network.CoreBanFilter; import haveno.core.network.p2p.seed.DefaultSeedNodeRepository; import haveno.core.offer.OfferModule; import haveno.core.proto.network.CoreNetworkProtoResolver; import haveno.core.proto.persistable.CorePersistenceProtoResolver; import haveno.core.trade.TradeModule; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.xmr.XmrConnectionModule; import haveno.core.xmr.XmrModule; import haveno.network.crypto.EncryptionServiceModule; import haveno.network.p2p.P2PModule; import haveno.network.p2p.network.BanFilter; import haveno.network.p2p.network.BridgeAddressProvider; import haveno.network.p2p.seed.SeedNodeRepository; import java.io.File; import static com.google.inject.name.Names.named; import static haveno.common.config.Config.KEY_STORAGE_DIR; import static haveno.common.config.Config.PREVENT_PERIODIC_SHUTDOWN_AT_SEED_NODE; import static haveno.common.config.Config.REFERRAL_ID; import static haveno.common.config.Config.STORAGE_DIR; import static haveno.common.config.Config.USE_DEV_MODE; import static haveno.common.config.Config.USE_DEV_MODE_HEADER; import static haveno.common.config.Config.USE_DEV_PRIVILEGE_KEYS; public class ModuleForAppWithP2p extends AppModule { public ModuleForAppWithP2p(Config config) { super(config); } @Override protected void configure() { bind(Config.class).toInstance(config); bind(KeyStorage.class).in(Singleton.class); bind(KeyRing.class).in(Singleton.class); bind(User.class).in(Singleton.class); bind(ClockWatcher.class).in(Singleton.class); bind(NetworkProtoResolver.class).to(CoreNetworkProtoResolver.class).in(Singleton.class); bind(PersistenceProtoResolver.class).to(CorePersistenceProtoResolver.class).in(Singleton.class); bind(Preferences.class).in(Singleton.class); bind(BridgeAddressProvider.class).to(Preferences.class).in(Singleton.class); bind(TorSetup.class).in(Singleton.class); bind(SeedNodeRepository.class).to(DefaultSeedNodeRepository.class).in(Singleton.class); bind(BanFilter.class).to(CoreBanFilter.class).in(Singleton.class); bind(File.class).annotatedWith(named(STORAGE_DIR)).toInstance(config.storageDir); bind(File.class).annotatedWith(named(KEY_STORAGE_DIR)).toInstance(config.keyStorageDir); bindConstant().annotatedWith(named(USE_DEV_PRIVILEGE_KEYS)).to(config.useDevPrivilegeKeys); bindConstant().annotatedWith(named(USE_DEV_MODE)).to(config.useDevMode); bindConstant().annotatedWith(named(USE_DEV_MODE_HEADER)).to(config.useDevModeHeader); bindConstant().annotatedWith(named(REFERRAL_ID)).to(config.referralId); bindConstant().annotatedWith(named(PREVENT_PERIODIC_SHUTDOWN_AT_SEED_NODE)).to(config.preventPeriodicShutdownAtSeedNode); // ordering is used for shut down sequence install(new TradeModule(config)); install(new EncryptionServiceModule(config)); install(new OfferModule(config)); install(new P2PModule(config)); install(new XmrModule(config)); install(new AlertModule(config)); install(new FilterModule(config)); install(new XmrConnectionModule(config)); } } ================================================ FILE: core/src/main/java/haveno/core/exceptions/TradePriceOutOfToleranceException.java ================================================ /* * 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 . */ package haveno.core.exceptions; public class TradePriceOutOfToleranceException extends Exception { public TradePriceOutOfToleranceException(String message) { super(message); } } ================================================ FILE: core/src/main/java/haveno/core/filter/Filter.java ================================================ /* * 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 . */ package haveno.core.filter; import com.google.common.annotations.VisibleForTesting; import com.google.protobuf.ByteString; import haveno.common.crypto.Sig; import haveno.common.proto.ProtoUtil; import haveno.common.util.CollectionUtils; import haveno.common.util.ExtraDataMapValidator; import haveno.common.util.Utilities; import haveno.network.p2p.storage.payload.ExpirablePayload; import haveno.network.p2p.storage.payload.ProtectedStoragePayload; import lombok.Value; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.security.PublicKey; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @Slf4j @Value public final class Filter implements ProtectedStoragePayload, ExpirablePayload { public static final long TTL = TimeUnit.DAYS.toMillis(180); private final List bannedOfferIds; private final List nodeAddressesBannedFromTrading; private final List bannedAutoConfExplorers; private final List bannedPaymentAccounts; private final List bannedCurrencies; private final List bannedPaymentMethods; private final List arbitrators; private final List seedNodes; private final List priceRelayNodes; private final boolean preventPublicXmrNetwork; private final List xmrNodes; // SignatureAsBase64 is not set initially as we use the serialized data for signing. We set it after signature is // created by cloning the object with a non-null sig. @Nullable private final String signatureAsBase64; // The pub EC key from the dev who has signed and published the filter (different to ownerPubKeyBytes) private final String signerPubKeyAsHex; // The pub key used for the data protection in the p2p storage private final byte[] ownerPubKeyBytes; private final String disableTradeBelowVersion; private final List mediators; private final List refundAgents; private final List bannedAccountWitnessSignerPubKeys; private final List xmrFeeReceiverAddresses; private final long creationDate; private final List bannedPrivilegedDevPubKeys; // Should be only used in emergency case if we need to add data but do not want to break backward compatibility // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new // field in a class would break that hash and therefore break the storage mechanism. @Nullable private Map extraDataMap; private transient PublicKey ownerPubKey; // added at v1.3.8 private final boolean disableAutoConf; // added at v1.5.5 private final Set nodeAddressesBannedFromNetwork; private final boolean disableApi; // added at v1.6.0 private final boolean disableMempoolValidation; // After we have created the signature from the filter data we clone it and apply the signature static Filter cloneWithSig(Filter filter, String signatureAsBase64) { return new Filter(filter.getBannedOfferIds(), filter.getNodeAddressesBannedFromTrading(), filter.getBannedPaymentAccounts(), filter.getBannedCurrencies(), filter.getBannedPaymentMethods(), filter.getArbitrators(), filter.getSeedNodes(), filter.getPriceRelayNodes(), filter.isPreventPublicXmrNetwork(), filter.getXmrNodes(), filter.getDisableTradeBelowVersion(), filter.getMediators(), filter.getRefundAgents(), filter.getBannedAccountWitnessSignerPubKeys(), filter.getXmrFeeReceiverAddresses(), filter.getOwnerPubKeyBytes(), filter.getCreationDate(), filter.getExtraDataMap(), signatureAsBase64, filter.getSignerPubKeyAsHex(), filter.getBannedPrivilegedDevPubKeys(), filter.isDisableAutoConf(), filter.getBannedAutoConfExplorers(), filter.getNodeAddressesBannedFromNetwork(), filter.isDisableMempoolValidation(), filter.isDisableApi()); } // Used for signature verification as we created the sig without the signatureAsBase64 field we set it to null again static Filter cloneWithoutSig(Filter filter) { return new Filter(filter.getBannedOfferIds(), filter.getNodeAddressesBannedFromTrading(), filter.getBannedPaymentAccounts(), filter.getBannedCurrencies(), filter.getBannedPaymentMethods(), filter.getArbitrators(), filter.getSeedNodes(), filter.getPriceRelayNodes(), filter.isPreventPublicXmrNetwork(), filter.getXmrNodes(), filter.getDisableTradeBelowVersion(), filter.getMediators(), filter.getRefundAgents(), filter.getBannedAccountWitnessSignerPubKeys(), filter.getXmrFeeReceiverAddresses(), filter.getOwnerPubKeyBytes(), filter.getCreationDate(), filter.getExtraDataMap(), null, filter.getSignerPubKeyAsHex(), filter.getBannedPrivilegedDevPubKeys(), filter.isDisableAutoConf(), filter.getBannedAutoConfExplorers(), filter.getNodeAddressesBannedFromNetwork(), filter.isDisableMempoolValidation(), filter.isDisableApi()); } public Filter(List bannedOfferIds, List nodeAddressesBannedFromTrading, List bannedPaymentAccounts, List bannedCurrencies, List bannedPaymentMethods, List arbitrators, List seedNodes, List priceRelayNodes, boolean preventPublicXmrNetwork, List xmrNodes, String disableTradeBelowVersion, List mediators, List refundAgents, List bannedAccountWitnessSignerPubKeys, List xmrFeeReceiverAddresses, PublicKey ownerPubKey, String signerPubKeyAsHex, List bannedPrivilegedDevPubKeys, boolean disableAutoConf, List bannedAutoConfExplorers, Set nodeAddressesBannedFromNetwork, boolean disableMempoolValidation, boolean disableApi) { this(bannedOfferIds, nodeAddressesBannedFromTrading, bannedPaymentAccounts, bannedCurrencies, bannedPaymentMethods, arbitrators, seedNodes, priceRelayNodes, preventPublicXmrNetwork, xmrNodes, disableTradeBelowVersion, mediators, refundAgents, bannedAccountWitnessSignerPubKeys, xmrFeeReceiverAddresses, Sig.getPublicKeyBytes(ownerPubKey), System.currentTimeMillis(), null, null, signerPubKeyAsHex, bannedPrivilegedDevPubKeys, disableAutoConf, bannedAutoConfExplorers, nodeAddressesBannedFromNetwork, disableMempoolValidation, disableApi); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @VisibleForTesting public Filter(List bannedOfferIds, List nodeAddressesBannedFromTrading, List bannedPaymentAccounts, List bannedCurrencies, List bannedPaymentMethods, List arbitrators, List seedNodes, List priceRelayNodes, boolean preventPublicXmrNetwork, List xmrNodes, String disableTradeBelowVersion, List mediators, List refundAgents, List bannedAccountWitnessSignerPubKeys, List xmrFeeReceiverAddresses, byte[] ownerPubKeyBytes, long creationDate, @Nullable Map extraDataMap, @Nullable String signatureAsBase64, String signerPubKeyAsHex, List bannedPrivilegedDevPubKeys, boolean disableAutoConf, List bannedAutoConfExplorers, Set nodeAddressesBannedFromNetwork, boolean disableMempoolValidation, boolean disableApi) { this.bannedOfferIds = bannedOfferIds; this.nodeAddressesBannedFromTrading = nodeAddressesBannedFromTrading; this.bannedPaymentAccounts = bannedPaymentAccounts; this.bannedCurrencies = bannedCurrencies; this.bannedPaymentMethods = bannedPaymentMethods; this.arbitrators = arbitrators; this.seedNodes = seedNodes; this.priceRelayNodes = priceRelayNodes; this.preventPublicXmrNetwork = preventPublicXmrNetwork; this.xmrNodes = xmrNodes; this.disableTradeBelowVersion = disableTradeBelowVersion; this.mediators = mediators; this.refundAgents = refundAgents; this.bannedAccountWitnessSignerPubKeys = bannedAccountWitnessSignerPubKeys; this.xmrFeeReceiverAddresses = xmrFeeReceiverAddresses; this.ownerPubKeyBytes = ownerPubKeyBytes; this.creationDate = creationDate; this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); this.signatureAsBase64 = signatureAsBase64; this.signerPubKeyAsHex = signerPubKeyAsHex; this.bannedPrivilegedDevPubKeys = bannedPrivilegedDevPubKeys; this.disableAutoConf = disableAutoConf; this.bannedAutoConfExplorers = bannedAutoConfExplorers; this.nodeAddressesBannedFromNetwork = nodeAddressesBannedFromNetwork; this.disableMempoolValidation = disableMempoolValidation; this.disableApi = disableApi; // ownerPubKeyBytes can be null when called from tests if (ownerPubKeyBytes != null) { ownerPubKey = Sig.getPublicKeyFromBytes(ownerPubKeyBytes); } else { ownerPubKey = null; } } @Override public protobuf.StoragePayload toProtoMessage() { List paymentAccountFilterList = bannedPaymentAccounts.stream() .map(PaymentAccountFilter::toProtoMessage) .collect(Collectors.toList()); protobuf.Filter.Builder builder = protobuf.Filter.newBuilder().addAllBannedOfferIds(bannedOfferIds) .addAllNodeAddressesBannedFromTrading(nodeAddressesBannedFromTrading) .addAllBannedPaymentAccounts(paymentAccountFilterList) .addAllBannedCurrencies(bannedCurrencies) .addAllBannedPaymentMethods(bannedPaymentMethods) .addAllArbitrators(arbitrators) .addAllSeedNodes(seedNodes) .addAllPriceRelayNodes(priceRelayNodes) .setPreventPublicXmrNetwork(preventPublicXmrNetwork) .addAllXmrNodes(xmrNodes) .setDisableTradeBelowVersion(disableTradeBelowVersion) .addAllMediators(mediators) .addAllRefundAgents(refundAgents) .addAllBannedSignerPubKeys(bannedAccountWitnessSignerPubKeys) .addAllXmrFeeReceiverAddresses(xmrFeeReceiverAddresses) .setOwnerPubKeyBytes(ByteString.copyFrom(ownerPubKeyBytes)) .setSignerPubKeyAsHex(signerPubKeyAsHex) .setCreationDate(creationDate) .addAllBannedPrivilegedDevPubKeys(bannedPrivilegedDevPubKeys) .setDisableAutoConf(disableAutoConf) .addAllBannedAutoConfExplorers(bannedAutoConfExplorers) .addAllNodeAddressesBannedFromNetwork(nodeAddressesBannedFromNetwork) .setDisableMempoolValidation(disableMempoolValidation) .setDisableApi(disableApi); Optional.ofNullable(signatureAsBase64).ifPresent(builder::setSignatureAsBase64); Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); return protobuf.StoragePayload.newBuilder().setFilter(builder).build(); } public static Filter fromProto(protobuf.Filter proto) { List bannedPaymentAccountsList = proto.getBannedPaymentAccountsList().stream() .map(PaymentAccountFilter::fromProto) .collect(Collectors.toList()); return new Filter(ProtoUtil.protocolStringListToList(proto.getBannedOfferIdsList()), ProtoUtil.protocolStringListToList(proto.getNodeAddressesBannedFromTradingList()), bannedPaymentAccountsList, ProtoUtil.protocolStringListToList(proto.getBannedCurrenciesList()), ProtoUtil.protocolStringListToList(proto.getBannedPaymentMethodsList()), ProtoUtil.protocolStringListToList(proto.getArbitratorsList()), ProtoUtil.protocolStringListToList(proto.getSeedNodesList()), ProtoUtil.protocolStringListToList(proto.getPriceRelayNodesList()), proto.getPreventPublicXmrNetwork(), ProtoUtil.protocolStringListToList(proto.getXmrNodesList()), proto.getDisableTradeBelowVersion(), ProtoUtil.protocolStringListToList(proto.getMediatorsList()), ProtoUtil.protocolStringListToList(proto.getRefundAgentsList()), ProtoUtil.protocolStringListToList(proto.getBannedSignerPubKeysList()), ProtoUtil.protocolStringListToList(proto.getXmrFeeReceiverAddressesList()), proto.getOwnerPubKeyBytes().toByteArray(), proto.getCreationDate(), CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap(), proto.getSignatureAsBase64(), proto.getSignerPubKeyAsHex(), ProtoUtil.protocolStringListToList(proto.getBannedPrivilegedDevPubKeysList()), proto.getDisableAutoConf(), ProtoUtil.protocolStringListToList(proto.getBannedAutoConfExplorersList()), ProtoUtil.protocolStringListToSet(proto.getNodeAddressesBannedFromNetworkList()), proto.getDisableMempoolValidation(), proto.getDisableApi() ); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public long getTTL() { return TTL; } @Override public String toString() { return "Filter{" + "\n bannedOfferIds=" + bannedOfferIds + ",\n nodeAddressesBannedFromTrading=" + nodeAddressesBannedFromTrading + ",\n bannedAutoConfExplorers=" + bannedAutoConfExplorers + ",\n bannedPaymentAccounts=" + bannedPaymentAccounts + ",\n bannedCurrencies=" + bannedCurrencies + ",\n bannedPaymentMethods=" + bannedPaymentMethods + ",\n arbitrators=" + arbitrators + ",\n seedNodes=" + seedNodes + ",\n priceRelayNodes=" + priceRelayNodes + ",\n preventPublicXmrNetwork=" + preventPublicXmrNetwork + ",\n xmrNodes=" + xmrNodes + ",\n signatureAsBase64='" + signatureAsBase64 + '\'' + ",\n signerPubKeyAsHex='" + signerPubKeyAsHex + '\'' + ",\n ownerPubKeyBytes=" + Utilities.bytesAsHexString(ownerPubKeyBytes) + ",\n disableTradeBelowVersion='" + disableTradeBelowVersion + '\'' + ",\n mediators=" + mediators + ",\n refundAgents=" + refundAgents + ",\n bannedAccountWitnessSignerPubKeys=" + bannedAccountWitnessSignerPubKeys + ",\n xmrFeeReceiverAddresses=" + xmrFeeReceiverAddresses + ",\n creationDate=" + creationDate + ",\n bannedPrivilegedDevPubKeys=" + bannedPrivilegedDevPubKeys + ",\n extraDataMap=" + extraDataMap + ",\n ownerPubKey=" + ownerPubKey + ",\n disableAutoConf=" + disableAutoConf + ",\n nodeAddressesBannedFromNetwork=" + nodeAddressesBannedFromNetwork + ",\n disableMempoolValidation=" + disableMempoolValidation + ",\n disableApi=" + disableApi + "\n}"; } } ================================================ FILE: core/src/main/java/haveno/core/filter/FilterManager.java ================================================ /* * 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 . */ package haveno.core.filter; import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.app.DevEnv; import haveno.common.app.Version; import haveno.common.config.Config; import haveno.common.config.ConfigFileEditor; import haveno.common.crypto.KeyRing; import haveno.core.locale.Res; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.provider.ProvidersRepository; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.xmr.nodes.XmrNodes; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import haveno.network.p2p.P2PServiceListener; import haveno.network.p2p.network.BanFilter; import haveno.network.p2p.storage.HashMapChangedListener; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import java.lang.reflect.Method; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.security.PublicKey; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Consumer; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javax.annotation.Nullable; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.Sha256Hash; import static org.bitcoinj.core.Utils.HEX; import org.bouncycastle.util.encoders.Base64; /** * We only support one active filter, if we receive multiple we use the one with the more recent creationDate. */ @Slf4j public class FilterManager { private static final String BANNED_PRICE_RELAY_NODES = "bannedPriceRelayNodes"; private static final String BANNED_SEED_NODES = "bannedSeedNodes"; private static final String BANNED_XMR_NODES = "bannedXmrNodes"; /////////////////////////////////////////////////////////////////////////////////////////// // Listener /////////////////////////////////////////////////////////////////////////////////////////// public interface Listener { void onFilterAdded(Filter filter); } private final P2PService p2PService; private final KeyRing keyRing; private final User user; private final Preferences preferences; private final ConfigFileEditor configFileEditor; private final ProvidersRepository providersRepository; private final boolean ignoreDevMsg; private final boolean useDevPrivilegeKeys; private final ObjectProperty filterProperty = new SimpleObjectProperty<>(); private final List listeners = new CopyOnWriteArrayList<>(); private ECKey filterSigningKey; private final Set invalidFilters = new HashSet<>(); private Consumer filterWarningHandler; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public FilterManager(P2PService p2PService, KeyRing keyRing, User user, Preferences preferences, Config config, ProvidersRepository providersRepository, BanFilter banFilter, @Named(Config.IGNORE_DEV_MSG) boolean ignoreDevMsg, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { this.p2PService = p2PService; this.keyRing = keyRing; this.user = user; this.preferences = preferences; this.configFileEditor = new ConfigFileEditor(config.configFile); this.providersRepository = providersRepository; this.ignoreDevMsg = ignoreDevMsg; this.useDevPrivilegeKeys = useDevPrivilegeKeys; banFilter.setBannedNodePredicate(this::isNodeAddressBannedFromNetwork); } protected List getPubKeyList() { switch (Config.baseCurrencyNetwork()) { case XMR_LOCAL: if (useDevPrivilegeKeys) return Collections.singletonList(DevEnv.DEV_PRIVILEGE_PUB_KEY); return List.of( "027a381b5333a56e1cc3d90d3a7d07f26509adf7029ed06fc997c656621f8da1ee", "024baabdba90e7cc0dc4626ef73ea9d722ea7085d1104491da8c76f28187513492", "026eeec3c119dd6d537249d74e5752a642dd2c3cc5b6a9b44588eb58344f29b519"); case XMR_STAGENET: return List.of( "03aa23e062afa0dda465f46986f8aa8d0374ad3e3f256141b05681dcb1e39c3859", "02d3beb1293ca2ca14e6d42ca8bd18089a62aac62fd6bb23923ee6ead46ac60fba", "0374dd70f3fa6e47ec5ab97932e1cec6233e98e6ae3129036b17118650c44fd3de"); case XMR_MAINNET: return List.of( "02d8ac0fbe4e25f4a1d68b95936f25fc2e1b218e161cb5ed6661c7ab4c85f1fd4f", "02e9dc14edddde19cc9f829a0739d0ab0c7310154ad94a15d477b51d85991b5a8a", "03c8efdf81287ce8b3212241e6aa7cdf094ecbed2d2f119730a3e4d596a764106a"); default: throw new RuntimeException("Unhandled base currency network: " + Config.baseCurrencyNetwork()); } } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void onAllServicesInitialized() { if (ignoreDevMsg) { return; } p2PService.getP2PDataStorage().getMap().values().stream() .map(ProtectedStorageEntry::getProtectedStoragePayload) .filter(protectedStoragePayload -> protectedStoragePayload instanceof Filter) .map(protectedStoragePayload -> (Filter) protectedStoragePayload) .forEach(this::onFilterAddedFromNetwork); // On mainNet we expect to have received a filter object, if not show a popup to the user to inform the // Haveno devs. if (Config.baseCurrencyNetwork().isMainnet() && getFilter() == null && filterWarningHandler != null) { filterWarningHandler.accept(Res.get("popup.warning.noFilter")); } p2PService.addHashSetChangedListener(new HashMapChangedListener() { @Override public void onAdded(Collection protectedStorageEntries) { protectedStorageEntries.stream() .filter(protectedStorageEntry -> protectedStorageEntry.getProtectedStoragePayload() instanceof Filter) .forEach(protectedStorageEntry -> { Filter filter = (Filter) protectedStorageEntry.getProtectedStoragePayload(); onFilterAddedFromNetwork(filter); }); } @Override public void onRemoved(Collection protectedStorageEntries) { protectedStorageEntries.stream() .filter(protectedStorageEntry -> protectedStorageEntry.getProtectedStoragePayload() instanceof Filter) .forEach(protectedStorageEntry -> { Filter filter = (Filter) protectedStorageEntry.getProtectedStoragePayload(); onFilterRemovedFromNetwork(filter); }); } }); p2PService.addP2PServiceListener(new P2PServiceListener() { @Override public void onDataReceived() { } @Override public void onNoSeedNodeAvailable() { } @Override public void onNoPeersAvailable() { } @Override public void onUpdatedDataReceived() { // We should have received all data at that point and if the filters were not set we // clean up the persisted banned nodes in the options file as it might be that we missed the filter // remove message if we have not been online. if (filterProperty.get() == null) { clearBannedNodes(); } } @Override public void onTorNodeReady() { } @Override public void onHiddenServicePublished() { } @Override public void onSetupFailed(Throwable throwable) { } @Override public void onRequestCustomBridges() { } }); } public void setFilterWarningHandler(Consumer filterWarningHandler) { this.filterWarningHandler = filterWarningHandler; addListener(filter -> { if (filter != null && filterWarningHandler != null) { try { if (filter.getSeedNodes() != null && !filter.getSeedNodes().isEmpty()) { log.info("One of the seed nodes got banned. {}", filter.getSeedNodes()); // Let's keep that more silent. Might be used in case a node is unstable and we don't want to confuse users. // filterWarningHandler.accept(Res.get("popup.warning.nodeBanned", Res.get("popup.warning.seed"))); } if (filter.getPriceRelayNodes() != null && !filter.getPriceRelayNodes().isEmpty()) { log.info("One of the price relay nodes got banned. {}", filter.getPriceRelayNodes()); // Let's keep that more silent. Might be used in case a node is unstable and we don't want to confuse users. // filterWarningHandler.accept(Res.get("popup.warning.nodeBanned", Res.get("popup.warning.priceRelay"))); } if (requireUpdateToNewVersionForTrading()) { filterWarningHandler.accept(Res.get("popup.warning.mandatoryUpdate.trading")); } } catch (Exception e) { log.warn("Error in filter warning handler", e); throw e; } } }); } public boolean isPrivilegedDevPubKeyBanned(String pubKeyAsHex) { Filter filter = getFilter(); if (filter == null) { return false; } return filter.getBannedPrivilegedDevPubKeys().contains(pubKeyAsHex); } public boolean canAddDevFilter(String privKeyString) { if (privKeyString == null || privKeyString.isEmpty()) { return false; } if (!isValidDevPrivilegeKey(privKeyString)) { log.warn("Key in invalid"); return false; } ECKey ecKeyFromPrivate = toECKey(privKeyString); String pubKeyAsHex = getPubKeyAsHex(ecKeyFromPrivate); if (isPrivilegedDevPubKeyBanned(pubKeyAsHex)) { log.warn("Pub key is banned."); return false; } return true; } public String getSignerPubKeyAsHex(String privKeyString) { ECKey ecKey = toECKey(privKeyString); return getPubKeyAsHex(ecKey); } public void addDevFilter(Filter filterWithoutSig, String privKeyString) { setFilterSigningKey(privKeyString); String signatureAsBase64 = getSignature(filterWithoutSig); Filter filterWithSig = Filter.cloneWithSig(filterWithoutSig, signatureAsBase64); user.setDevelopersFilter(filterWithSig); p2PService.addProtectedStorageEntry(filterWithSig); // Cleanup potential old filters created in the past with same priv key invalidFilters.forEach(filter -> { removeInvalidFilters(filter, privKeyString); }); } public void addToInvalidFilters(Filter filter) { invalidFilters.add(filter); } public void removeInvalidFilters(Filter filter, String privKeyString) { // We can only remove the filter if it's our own filter if (Arrays.equals(filter.getOwnerPubKey().getEncoded(), keyRing.getSignatureKeyPair().getPublic().getEncoded())) { log.info("Remove invalid filter {}", filter); setFilterSigningKey(privKeyString); String signatureAsBase64 = getSignature(Filter.cloneWithoutSig(filter)); Filter filterWithSig = Filter.cloneWithSig(filter, signatureAsBase64); boolean result = p2PService.removeData(filterWithSig); if (!result) { log.warn("Could not remove filter {}", filter); } } else { log.info("The invalid filter is not our own, so we cannot remove it from the network"); } } public boolean canRemoveDevFilter(String privKeyString) { if (privKeyString == null || privKeyString.isEmpty()) { return false; } Filter developersFilter = getDevFilter(); if (developersFilter == null) { log.warn("There is no persisted dev filter to be removed."); return false; } if (!isValidDevPrivilegeKey(privKeyString)) { log.warn("Key in invalid."); return false; } ECKey ecKeyFromPrivate = toECKey(privKeyString); String pubKeyAsHex = getPubKeyAsHex(ecKeyFromPrivate); if (!developersFilter.getSignerPubKeyAsHex().equals(pubKeyAsHex)) { log.warn("pubKeyAsHex derived from private key does not match filterSignerPubKey. " + "filterSignerPubKey={}, pubKeyAsHex derived from private key={}", developersFilter.getSignerPubKeyAsHex(), pubKeyAsHex); return false; } if (isPrivilegedDevPubKeyBanned(pubKeyAsHex)) { log.warn("Pub key is banned."); return false; } return true; } public void removeDevFilter(String privKeyString) { setFilterSigningKey(privKeyString); Filter filterWithSig = user.getDevelopersFilter(); if (filterWithSig == null) { // Should not happen as UI button is deactivated in that case return; } if (p2PService.removeData(filterWithSig)) { user.setDevelopersFilter(null); } else { log.warn("Removing dev filter from network failed"); } } public void addListener(Listener listener) { listeners.add(listener); } public ObjectProperty filterProperty() { return filterProperty; } @Nullable public Filter getFilter() { return filterProperty.get(); } @Nullable public Filter getDevFilter() { return user.getDevelopersFilter(); } public PublicKey getOwnerPubKey() { return keyRing.getSignatureKeyPair().getPublic(); } public boolean isCurrencyBanned(String currencyCode) { return getFilter() != null && getFilter().getBannedCurrencies() != null && getFilter().getBannedCurrencies().stream() .anyMatch(e -> e.equals(currencyCode)); } public boolean isPaymentMethodBanned(PaymentMethod paymentMethod) { return getFilter() != null && getFilter().getBannedPaymentMethods() != null && getFilter().getBannedPaymentMethods().stream() .anyMatch(e -> e.equals(paymentMethod.getId())); } public boolean isOfferIdBanned(String offerId) { return getFilter() != null && getFilter().getBannedOfferIds().stream() .anyMatch(e -> e.equals(offerId)); } public boolean isNodeAddressBanned(NodeAddress nodeAddress) { return getFilter() != null && getFilter().getNodeAddressesBannedFromTrading().stream() .anyMatch(e -> e.equals(nodeAddress.getFullAddress())); } public boolean isNodeAddressBannedFromNetwork(NodeAddress nodeAddress) { return getFilter() != null && getFilter().getNodeAddressesBannedFromNetwork().stream() .anyMatch(e -> e.equals(nodeAddress.getFullAddress())); } public boolean isAutoConfExplorerBanned(String address) { return getFilter() != null && getFilter().getBannedAutoConfExplorers().stream() .anyMatch(e -> e.equals(address)); } public String getDisableTradeBelowVersion() { return getFilter() == null || getFilter().getDisableTradeBelowVersion() == null || getFilter().getDisableTradeBelowVersion().isEmpty() ? null : getFilter().getDisableTradeBelowVersion(); } public boolean requireUpdateToNewVersionForTrading() { if (getFilter() == null) { return false; } boolean requireUpdateToNewVersion = false; String getDisableTradeBelowVersion = getFilter().getDisableTradeBelowVersion(); if (getDisableTradeBelowVersion != null && !getDisableTradeBelowVersion.isEmpty()) { requireUpdateToNewVersion = Version.isNewVersion(getDisableTradeBelowVersion); } return requireUpdateToNewVersion; } public boolean arePeersPaymentAccountDataBanned(PaymentAccountPayload paymentAccountPayload) { return getFilter() != null && getFilter().getBannedPaymentAccounts().stream() .filter(paymentAccountFilter -> paymentAccountFilter.getPaymentMethodId().equals( paymentAccountPayload.getPaymentMethodId())) .anyMatch(paymentAccountFilter -> { try { Method method = paymentAccountPayload.getClass().getMethod(paymentAccountFilter.getGetMethodName()); // We invoke getter methods (no args), e.g. getHolderName String valueFromInvoke = (String) method.invoke(paymentAccountPayload); return valueFromInvoke.equalsIgnoreCase(paymentAccountFilter.getValue()); } catch (Throwable e) { log.error(e.getMessage()); return false; } }); } public boolean isWitnessSignerPubKeyBanned(String witnessSignerPubKeyAsHex) { return getFilter() != null && getFilter().getBannedAccountWitnessSignerPubKeys() != null && getFilter().getBannedAccountWitnessSignerPubKeys().stream() .anyMatch(e -> e.equals(witnessSignerPubKeyAsHex)); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void onFilterAddedFromNetwork(Filter newFilter) { Filter currentFilter = getFilter(); if (!isFilterPublicKeyInList(newFilter)) { if (newFilter.getSignerPubKeyAsHex() != null && !newFilter.getSignerPubKeyAsHex().isEmpty()) { log.warn("isFilterPublicKeyInList failed. Filter.getSignerPubKeyAsHex={}", newFilter.getSignerPubKeyAsHex()); } else { log.info("isFilterPublicKeyInList failed. Filter.getSignerPubKeyAsHex not set (expected case for pre v1.3.9 filter)"); } return; } if (!isSignatureValid(newFilter)) { log.warn("verifySignature failed. Filter={}", newFilter); return; } if (currentFilter != null) { if (currentFilter.getCreationDate() > newFilter.getCreationDate()) { log.info("We received a new filter from the network but the creation date is older than the " + "filter we have already. We ignore the new filter."); addToInvalidFilters(newFilter); return; } else { log.info("We received a new filter from the network and the creation date is newer than the " + "filter we have already. We ignore the old filter."); addToInvalidFilters(currentFilter); } if (isPrivilegedDevPubKeyBanned(newFilter.getSignerPubKeyAsHex())) { log.warn("Pub key of filter is banned. currentFilter={}, newFilter={}", currentFilter, newFilter); return; } } // Our new filter is newer so we apply it. // We do not require strict guarantees here (e.g. clocks not synced) as only trusted developers have the key // for deploying filters and this is only in place to avoid unintended situations of multiple filters // from multiple devs or if same dev publishes new filter from different app without the persisted devFilter. filterProperty.set(newFilter); // Seed nodes are requested at startup before we get the filter so we only apply the banned // nodes at the next startup and don't update the list in the P2P network domain. // We persist it to the property file which is read before any other initialisation. saveBannedNodes(BANNED_SEED_NODES, newFilter.getSeedNodes()); saveBannedNodes(BANNED_XMR_NODES, newFilter.getXmrNodes()); // Banned price relay nodes we can apply at runtime List priceRelayNodes = newFilter.getPriceRelayNodes(); saveBannedNodes(BANNED_PRICE_RELAY_NODES, priceRelayNodes); //TODO should be moved to client with listening on onFilterAdded providersRepository.applyBannedNodes(priceRelayNodes); //TODO should be moved to client with listening on onFilterAdded if (newFilter.isPreventPublicXmrNetwork() && preferences.getMoneroNodesOptionOrdinal() == XmrNodes.MoneroNodesOption.PUBLIC.ordinal()) { preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal()); } listeners.forEach(e -> e.onFilterAdded(newFilter)); } private void onFilterRemovedFromNetwork(Filter filter) { if (!isFilterPublicKeyInList(filter)) { log.warn("isFilterPublicKeyInList failed. Filter={}", filter); return; } if (!isSignatureValid(filter)) { log.warn("verifySignature failed. Filter={}", filter); return; } // We don't check for banned filter as we want to remove a banned filter anyway. if (filterProperty.get() != null && !filterProperty.get().equals(filter)) { return; } clearBannedNodes(); if (filter.equals(user.getDevelopersFilter())) { user.setDevelopersFilter(null); } filterProperty.set(null); } // Clears options files from banned nodes private void clearBannedNodes() { saveBannedNodes(BANNED_XMR_NODES, null); saveBannedNodes(BANNED_SEED_NODES, null); saveBannedNodes(BANNED_PRICE_RELAY_NODES, null); if (providersRepository.getBannedNodes() != null) { providersRepository.applyBannedNodes(null); } } private void saveBannedNodes(String optionName, List bannedNodes) { if (bannedNodes != null) configFileEditor.setOption(optionName, String.join(",", bannedNodes)); else configFileEditor.clearOption(optionName); } private boolean isValidDevPrivilegeKey(String privKeyString) { try { ECKey filterSigningKey = toECKey(privKeyString); String pubKeyAsHex = getPubKeyAsHex(filterSigningKey); return isPublicKeyInList(pubKeyAsHex); } catch (Throwable t) { return false; } } private void setFilterSigningKey(String privKeyString) { this.filterSigningKey = toECKey(privKeyString); } private String getSignature(Filter filterWithoutSig) { Sha256Hash hash = getSha256Hash(filterWithoutSig); ECKey.ECDSASignature ecdsaSignature = filterSigningKey.sign(hash); byte[] encodeToDER = ecdsaSignature.encodeToDER(); return new String(Base64.encode(encodeToDER), StandardCharsets.UTF_8); } private boolean isFilterPublicKeyInList(Filter filter) { String signerPubKeyAsHex = filter.getSignerPubKeyAsHex(); if (!isPublicKeyInList(signerPubKeyAsHex)) { log.info("Invalid filter (expected case for pre v1.3.9 filter as we still keep that in the network " + "but the new version does not recognize it as valid filter): " + "signerPubKeyAsHex from filter is not part of our pub key list. " + "signerPubKeyAsHex={}, publicKeys={}, filterCreationDate={}", signerPubKeyAsHex, getPubKeyList(), new Date(filter.getCreationDate())); return false; } return true; } private boolean isPublicKeyInList(String pubKeyAsHex) { boolean isPublicKeyInList = getPubKeyList().contains(pubKeyAsHex); if (!isPublicKeyInList) { log.info("pubKeyAsHex is not part of our pub key list (expected case for pre v1.3.9 filter). pubKeyAsHex={}, publicKeys={}", pubKeyAsHex, getPubKeyList()); } return isPublicKeyInList; } private boolean isSignatureValid(Filter filter) { try { Filter filterForSigVerification = Filter.cloneWithoutSig(filter); Sha256Hash hash = getSha256Hash(filterForSigVerification); checkNotNull(filter.getSignatureAsBase64(), "filter.getSignatureAsBase64() must not be null"); byte[] sigData = Base64.decode(filter.getSignatureAsBase64()); ECKey.ECDSASignature ecdsaSignature = ECKey.ECDSASignature.decodeFromDER(sigData); String signerPubKeyAsHex = filter.getSignerPubKeyAsHex(); byte[] decode = HEX.decode(signerPubKeyAsHex); ECKey ecPubKey = ECKey.fromPublicOnly(decode); return ecPubKey.verify(hash, ecdsaSignature); } catch (Throwable e) { log.warn("verifySignature failed. filter={}", filter); return false; } } private ECKey toECKey(String privKeyString) { return ECKey.fromPrivate(new BigInteger(1, HEX.decode(privKeyString))); } private Sha256Hash getSha256Hash(Filter filter) { byte[] filterData = filter.toProtoMessage().toByteArray(); return Sha256Hash.of(filterData); } private String getPubKeyAsHex(ECKey ecKey) { return HEX.encode(ecKey.getPubKey()); } } ================================================ FILE: core/src/main/java/haveno/core/filter/FilterModule.java ================================================ /* * 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 . */ package haveno.core.filter; import com.google.inject.Singleton; import haveno.common.app.AppModule; import haveno.common.config.Config; import static com.google.inject.name.Names.named; import static haveno.common.config.Config.IGNORE_DEV_MSG; public class FilterModule extends AppModule { public FilterModule(Config config) { super(config); } @Override protected final void configure() { bind(FilterManager.class).in(Singleton.class); bindConstant().annotatedWith(named(IGNORE_DEV_MSG)).to(config.ignoreDevMsg); } } ================================================ FILE: core/src/main/java/haveno/core/filter/PaymentAccountFilter.java ================================================ /* * 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 . */ package haveno.core.filter; import haveno.common.proto.network.NetworkPayload; import lombok.Value; import lombok.extern.slf4j.Slf4j; @Value @Slf4j public class PaymentAccountFilter implements NetworkPayload { private final String paymentMethodId; private final String getMethodName; private final String value; public PaymentAccountFilter(String paymentMethodId, String getMethodName, String value) { this.paymentMethodId = paymentMethodId; this.getMethodName = getMethodName; this.value = value; } @Override public protobuf.PaymentAccountFilter toProtoMessage() { return protobuf.PaymentAccountFilter.newBuilder() .setPaymentMethodId(paymentMethodId) .setGetMethodName(getMethodName) .setValue(value) .build(); } public static PaymentAccountFilter fromProto(protobuf.PaymentAccountFilter proto) { return new PaymentAccountFilter(proto.getPaymentMethodId(), proto.getGetMethodName(), proto.getValue()); } } ================================================ FILE: core/src/main/java/haveno/core/locale/BankUtil.java ================================================ /* * 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 . */ package haveno.core.locale; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; public class BankUtil { private static final Logger log = LoggerFactory.getLogger(BankUtil.class); // BankName @SuppressWarnings("SameReturnValue") public static boolean isBankNameRequired(@SuppressWarnings("unused") String countryCode) { // Currently we always return true but let's keep that method to be more flexible in case we want to not show // it at some new payment method. return true; /* switch (countryCode) { // We show always the bank name as it is needed in specific banks. // Though that handling should be optimized in futures. case "GB": case "US": case "NZ": case "AU": case "CA": case "SE": case "HK": return false; case "MX": case "BR": case "AR": return true; default: return true; }*/ } public static String getBankNameLabel(String countryCode) { switch (countryCode) { case "BR": return Res.get("payment.bank.name"); default: return isBankNameRequired(countryCode) ? Res.get("payment.bank.name") : Res.get("payment.bank.nameOptional"); } } // BankId public static boolean isBankIdRequired(String countryCode) { switch (countryCode) { case "GB": case "US": case "BR": case "NZ": case "AU": case "SE": case "CL": case "NO": case "AR": return false; case "CA": case "MX": case "HK": return true; default: return true; } } public static String getBankIdLabel(String countryCode) { switch (countryCode) { case "CA": return "Institution Number";// do not translate as it is used in English only case "MX": case "HK": return Res.get("payment.bankCode"); default: return isBankIdRequired(countryCode) ? Res.get("payment.bankId") : Res.get("payment.bankIdOptional"); } } // BranchId public static boolean isBranchIdRequired(String countryCode) { switch (countryCode) { case "GB": case "US": case "BR": case "AU": case "CA": return true; case "NZ": case "MX": case "HK": case "SE": case "NO": return false; default: return true; } } public static String getBranchIdLabel(String countryCode) { switch (countryCode) { case "GB": return "UK sort code"; // do not translate as it is used in English only case "US": return "Routing Number"; // do not translate as it is used in English only case "BR": return "Código da Agência"; // do not translate as it is used in Portuguese only case "AU": return "BSB code"; // do not translate as it is used in English only case "CA": return "Transit Number"; // do not translate as it is used in English only default: return isBranchIdRequired(countryCode) ? Res.get("payment.branchNr") : Res.get("payment.branchNrOptional"); } } // AccountNr @SuppressWarnings("SameReturnValue") public static boolean isAccountNrRequired(String countryCode) { switch (countryCode) { default: return true; } } public static String getAccountNrLabel(String countryCode) { switch (countryCode) { case "GB": case "US": case "BR": case "NZ": case "AU": case "CA": case "HK": return Res.get("payment.accountNr"); case "NO": return "Kontonummer"; // do not translate as it is used in Norwegian and Swedish only case "SE": return "Kontonummer"; // do not translate as it is used in Norwegian and Swedish only case "MX": return "CLABE"; // do not translate as it is used in Spanish only case "CL": return "Cuenta"; // do not translate as it is used in Spanish only case "AR": return "Número de cuenta"; // do not translate as it is used in Spanish only default: return Res.get("payment.accountNrLabel"); } } // AccountType public static boolean isAccountTypeRequired(String countryCode) { switch (countryCode) { case "US": case "BR": case "CA": return true; default: return false; } } public static String getAccountTypeLabel(String countryCode) { switch (countryCode) { case "US": case "BR": case "CA": return Res.get("payment.accountType"); default: return ""; } } public static List getAccountTypeValues(String countryCode) { switch (countryCode) { case "US": case "BR": case "CA": return Arrays.asList(Res.get("payment.checking"), Res.get("payment.savings")); default: return new ArrayList<>(); } } // HolderId public static boolean isHolderIdRequired(String countryCode) { switch (countryCode) { case "BR": case "CL": case "AR": return true; default: return false; } } public static String getHolderIdLabel(String countryCode) { switch (countryCode) { case "BR": return "Cadastro de Pessoas Físicas (CPF)"; // do not translate as it is used in Portuguese only case "CL": return "Rol Único Tributario (RUT)"; // do not translate as it is used in Spanish only case "AR": return "CUIL/CUIT"; // do not translate as it is used in Spanish only default: return Res.get("payment.personalId"); } } public static String getHolderIdLabelShort(String countryCode) { switch (countryCode) { case "BR": return "CPF"; // do not translate as it is used in portuguese only case "CL": return "RUT"; // do not translate as it is used in spanish only case "AR": return "CUIT"; default: return "ID"; } } // Validation public static boolean useValidation(String countryCode) { switch (countryCode) { case "GB": case "US": case "BR": case "AU": case "CA": case "NZ": case "MX": case "HK": case "SE": case "NO": case "AR": return true; default: return false; } } public static List getAllStateRequiredCountries() { List codes = List.of("US", "CA", "AU", "MY", "MX", "CN"); List list = CountryUtil.getCountries(codes); list.sort((a, b) -> a.name.compareTo(b.name)); return list; } public static boolean isStateRequired(String countryCode) { return getAllStateRequiredCountries().stream().map(country -> country.code).collect(Collectors.toList()).contains(countryCode); } public static boolean isNationalAccountIdRequired(String countryCode) { switch (countryCode) { case "AR": return true; default: return false; } } public static String getNationalAccountIdLabel(String countryCode) { switch (countryCode) { case "AR": return Res.get("payment.national.account.id.AR"); default: return ""; } } } ================================================ FILE: core/src/main/java/haveno/core/locale/Country.java ================================================ /* * 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 . */ package haveno.core.locale; import com.google.protobuf.Message; import haveno.common.proto.persistable.PersistablePayload; import lombok.EqualsAndHashCode; import lombok.ToString; import javax.annotation.concurrent.Immutable; @Immutable @EqualsAndHashCode @ToString public final class Country implements PersistablePayload { public final String code; public final String name; public final Region region; public Country(String code, String name, Region region) { this.code = code; this.name = name; this.region = region; } @Override public Message toProtoMessage() { return protobuf.Country.newBuilder().setCode(code).setName(name) .setRegion(protobuf.Region.newBuilder().setCode(region.code).setName(region.name)).build(); } public static Country fromProto(protobuf.Country proto) { return new Country(proto.getCode(), proto.getName(), Region.fromProto(proto.getRegion())); } } ================================================ FILE: core/src/main/java/haveno/core/locale/CountryUtil.java ================================================ /* * 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 . */ package haveno.core.locale; import com.google.common.collect.Collections2; import com.google.common.collect.Lists; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @Slf4j public class CountryUtil { public static List getCountryCodes(List countries) { return countries.stream().map(country -> country.code).collect(Collectors.toList()); } public static Country getCountry(String code) { return getCountries(List.of(code)).get(0); } public static List getCountries(List codes) { List countries = new ArrayList(); for (String code : codes) { Locale locale = new Locale(LanguageUtil.getDefaultLanguage(), code, ""); final String countryCode = locale.getCountry(); String regionCode = getRegionCode(countryCode); final Region region = new Region(regionCode, getRegionName(regionCode)); Country country = new Country(countryCode, locale.getDisplayCountry(), region); if (countryCode.equals("XK")) country = new Country(countryCode, getNameByCode(countryCode), region); countries.add(country); } return countries; } public static List getAllSepaEuroCountries() { List list = new ArrayList<>(); String[] codes = { // Eurozone countries "AT", "BE", "CY", "DE", "EE", "FI", "FR", "GR", "HR", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PT", "SK", "SI", "ES", // Microstates using EUR officially "AD", "MC", "SM", "VA", // French euro territories "PM", "YT", "RE", "GP", "MQ", "GF", "BL", "MF", // Spanish euro territories "EA", "IC", // Finnish euro territory "AX" }; populateCountryListByCodes(list, codes); list.sort((a, b) -> a.name.compareTo(b.name)); return list; } public static List getAllRevolutCountries() { List list = new ArrayList<>(); String[] codes = {"AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IS", "IE", "IT", "LV", "LI", "LT", "LU", "MT", "NL", "NO", "PL", "PT", "RO", "SK", "SI", "ES", "SE", "GB", "AU", "CA", "SG", "CH", "US"}; populateCountryListByCodes(list, codes); list.sort((a, b) -> a.name.compareTo(b.name)); return list; } public static List getAllAmazonGiftCardCountries() { List list = new ArrayList<>(); String[] codes = {"AU", "CA", "FR", "DE", "IT", "NL", "ES", "GB", "IN", "JP", "SA", "SE", "SG", "TR", "US"}; populateCountryListByCodes(list, codes); list.sort(Comparator.comparing(a -> a.name)); return list; } public static List getAllSepaInstantEuroCountries() { return getAllSepaEuroCountries(); } private static void populateCountryListByCodes(List list, String[] codes) { list.addAll(getCountries(Arrays.asList(codes))); } public static boolean containsAllSepaEuroCountries(List countryCodesToCompare) { List countryCodesBase = getAllSepaEuroCountries().stream().map(c -> c.code).collect(Collectors.toList()); return countryCodesToCompare.containsAll(countryCodesBase) && countryCodesBase.containsAll(countryCodesToCompare); } public static boolean containsAllSepaInstantEuroCountries(List countryCodesToCompare) { return containsAllSepaEuroCountries(countryCodesToCompare); } public static List getAllSepaNonEuroCountries() { List list = new ArrayList<>(); String[] codes = { // EU non-euro SEPA countries "BG", "CZ", "DK", "HU", "PL", "RO", "SE", // EEA SEPA (non-euro) "IS", "NO", "LI", // Non-EEA SEPA "CH", "GB", "JE", "GG", "IM", // SEPA extension (non-EEA, non-euro) "GI", "AL", "MD", "ME", "RS", "MK" }; populateCountryListByCodes(list, codes); list.sort((a, b) -> a.name.compareTo(b.name)); return list; } public static List getAllSepaInstantNonEuroCountries() { return getAllSepaNonEuroCountries(); } public static List getAllSepaCountries() { List list = new ArrayList<>(); list.addAll(getAllSepaEuroCountries()); list.addAll(getAllSepaNonEuroCountries()); return list; } public static List getAllSepaInstantCountries() { // TODO find reliable source for list // //Austria, Estonia, Germany, Italy, Latvia, Lithuania, the Netherlands and Spain. /* List list = new ArrayList<>(); String[] codes = {"AT", "DE", "EE", "IT", "LV", "LT", "NL", "ES"}; populateCountryListByCodes(list, codes); list.sort((a, b) -> a.name.compareTo(b.name));*/ return getAllSepaCountries(); } public static Country getDefaultCountry() { String regionCode = getRegionCode(getLocale().getCountry()); final Region region = new Region(regionCode, getRegionName(regionCode)); return new Country(getLocale().getCountry(), getLocale().getDisplayCountry(), region); } public static Optional findCountryByCode(String countryCode) { return getAllCountries().stream().filter(e -> e.code.equals(countryCode)).findAny(); } public static String getNameByCode(String countryCode) { if (countryCode.equals("XK")) return "Republic of Kosovo"; else return new Locale(LanguageUtil.getDefaultLanguage(), countryCode).getDisplayCountry(); } public static String getNameAndCode(String countryCode) { if (countryCode.isEmpty()) return ""; return getNameByCode(countryCode) + " (" + countryCode + ")"; } public static String getCodesString(List countryCodes) { return countryCodes.stream().collect(Collectors.joining(", ")); } public static String getNamesByCodesString(List countryCodes) { return getNamesByCodes(countryCodes).stream().collect(Collectors.joining(",\n")); } public static String getCountriesString(List countryCodes) { if (CountryUtil.containsAllSepaEuroCountries(countryCodes)) { return Res.get("shared.allEuroCountries"); } else { if (countryCodes.size() == 1) { return CountryUtil.getNameAndCode(countryCodes.get(0)); } else { return CountryUtil.getCodesString(countryCodes); } } } public static List getAllRegions() { final List allRegions = new ArrayList<>(); String regionCode = "AM"; Region region = new Region(regionCode, getRegionName(regionCode)); allRegions.add(region); regionCode = "AF"; region = new Region(regionCode, getRegionName(regionCode)); allRegions.add(region); regionCode = "EU"; region = new Region(regionCode, getRegionName(regionCode)); allRegions.add(region); regionCode = "AS"; region = new Region(regionCode, getRegionName(regionCode)); allRegions.add(region); regionCode = "OC"; region = new Region(regionCode, getRegionName(regionCode)); allRegions.add(region); return allRegions; } public static List getAllCountriesForRegion(Region selectedRegion) { return Lists.newArrayList(Collections2.filter(getAllCountries(), country -> selectedRegion != null && country != null && selectedRegion.equals(country.region))); } public static List getAllCountries() { final Set allCountries = new HashSet<>(); for (final Locale locale : getAllCountryLocales()) { String regionCode = getRegionCode(locale.getCountry()); final Region region = new Region(regionCode, getRegionName(regionCode)); Country country = new Country(locale.getCountry(), locale.getDisplayCountry(), region); if (locale.getCountry().equals("XK")) country = new Country(locale.getCountry(), "Republic of Kosovo", region); allCountries.add(country); } allCountries.add(new Country("GE", "Georgia", new Region("AS", getRegionName("AS")))); allCountries.add(new Country("BW", "Botswana", new Region("AF", getRegionName("AF")))); allCountries.add(new Country("IR", "Iran", new Region("AS", getRegionName("AS")))); final List allCountriesList = new ArrayList<>(allCountries); allCountriesList.sort((locale1, locale2) -> locale1.name.compareTo(locale2.name)); return allCountriesList; } private static List getAllCountryLocales() { List allLocales = LocaleUtil.getAllLocales(); // Filter duplicate locale entries Set allLocalesAsSet = allLocales.stream().filter(locale -> !locale.getCountry().isEmpty()) .collect(Collectors.toSet()); List allCountryLocales = new ArrayList<>(); allCountryLocales.addAll(allLocalesAsSet); allCountryLocales.sort((locale1, locale2) -> locale1.getDisplayCountry().compareTo(locale2.getDisplayCountry())); return allCountryLocales; } private static List getNamesByCodes(List countryCodes) { return countryCodes.stream().map(CountryUtil::getNameByCode).collect(Collectors.toList()); } private static String getRegionName(final String regionCode) { return regionCodeToNameMap.get(regionCode); } private static final Map regionCodeToNameMap = new HashMap<>(); // Key is: ISO 3166 code, value is region code as defined in regionCodeToNameMap private static final Map regionByCountryCodeMap = new HashMap<>(); static { regionCodeToNameMap.put("AM", "Americas"); regionCodeToNameMap.put("AF", "Africa"); regionCodeToNameMap.put("EU", "Europe"); regionCodeToNameMap.put("AS", "Asia"); regionCodeToNameMap.put("OC", "Oceania"); // Data extracted from https://restcountries.eu/rest/v2/all?fields=name;region;subregion;alpha2Code;languages regionByCountryCodeMap.put("AF", "AS"); // name=Afghanistan / region=Asia / subregion=Southern Asia regionByCountryCodeMap.put("AX", "EU"); // name=Åland Islands / region=Europe / subregion=Northern Europe regionByCountryCodeMap.put("AL", "EU"); // name=Albania / region=Europe / subregion=Southern Europe regionByCountryCodeMap.put("DZ", "AF"); // name=Algeria / region=Africa / subregion=Northern Africa regionByCountryCodeMap.put("AS", "OC"); // name=American Samoa / region=Oceania / subregion=Polynesia regionByCountryCodeMap.put("AD", "EU"); // name=Andorra / region=Europe / subregion=Southern Europe regionByCountryCodeMap.put("AO", "AF"); // name=Angola / region=Africa / subregion=Middle Africa regionByCountryCodeMap.put("AI", "AM"); // name=Anguilla / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("AG", "AM"); // name=Antigua and Barbuda / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("AR", "AM"); // name=Argentina / region=Americas / subregion=South America regionByCountryCodeMap.put("AM", "AS"); // name=Armenia / region=Asia / subregion=Western Asia regionByCountryCodeMap.put("AW", "AM"); // name=Aruba / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("AU", "OC"); // name=Australia / region=Oceania / subregion=Australia and New Zealand regionByCountryCodeMap.put("AT", "EU"); // name=Austria / region=Europe / subregion=Western Europe regionByCountryCodeMap.put("AZ", "AS"); // name=Azerbaijan / region=Asia / subregion=Western Asia regionByCountryCodeMap.put("BS", "AM"); // name=Bahamas / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("BH", "AS"); // name=Bahrain / region=Asia / subregion=Western Asia regionByCountryCodeMap.put("BD", "AS"); // name=Bangladesh / region=Asia / subregion=Southern Asia regionByCountryCodeMap.put("BB", "AM"); // name=Barbados / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("BY", "EU"); // name=Belarus / region=Europe / subregion=Eastern Europe regionByCountryCodeMap.put("BE", "EU"); // name=Belgium / region=Europe / subregion=Western Europe regionByCountryCodeMap.put("BZ", "AM"); // name=Belize / region=Americas / subregion=Central America regionByCountryCodeMap.put("BJ", "AF"); // name=Benin / region=Africa / subregion=Western Africa regionByCountryCodeMap.put("BM", "AM"); // name=Bermuda / region=Americas / subregion=Northern America regionByCountryCodeMap.put("BT", "AS"); // name=Bhutan / region=Asia / subregion=Southern Asia regionByCountryCodeMap.put("BO", "AM"); // name=Bolivia (Plurinational State of) / region=Americas / subregion=South America regionByCountryCodeMap.put("BQ", "AM"); // name=Bonaire, Sint Eustatius and Saba / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("BA", "EU"); // name=Bosnia and Herzegovina / region=Europe / subregion=Southern Europe regionByCountryCodeMap.put("BW", "AF"); // name=Botswana / region=Africa / subregion=Southern Africa regionByCountryCodeMap.put("BR", "AM"); // name=Brazil / region=Americas / subregion=South America regionByCountryCodeMap.put("IO", "AF"); // name=British Indian Ocean Territory / region=Africa / subregion=Eastern Africa regionByCountryCodeMap.put("UM", "AM"); // name=United States Minor Outlying Islands / region=Americas / subregion=Northern America regionByCountryCodeMap.put("VG", "AM"); // name=Virgin Islands (British) / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("VI", "AM"); // name=Virgin Islands (U.S.) / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("BN", "AS"); // name=Brunei Darussalam / region=Asia / subregion=South-Eastern Asia regionByCountryCodeMap.put("BG", "EU"); // name=Bulgaria / region=Europe / subregion=Eastern Europe regionByCountryCodeMap.put("BF", "AF"); // name=Burkina Faso / region=Africa / subregion=Western Africa regionByCountryCodeMap.put("BI", "AF"); // name=Burundi / region=Africa / subregion=Eastern Africa regionByCountryCodeMap.put("KH", "AS"); // name=Cambodia / region=Asia / subregion=South-Eastern Asia regionByCountryCodeMap.put("CM", "AF"); // name=Cameroon / region=Africa / subregion=Middle Africa regionByCountryCodeMap.put("CA", "AM"); // name=Canada / region=Americas / subregion=Northern America regionByCountryCodeMap.put("CV", "AF"); // name=Cabo Verde / region=Africa / subregion=Western Africa regionByCountryCodeMap.put("KY", "AM"); // name=Cayman Islands / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("CF", "AF"); // name=Central African Republic / region=Africa / subregion=Middle Africa regionByCountryCodeMap.put("TD", "AF"); // name=Chad / region=Africa / subregion=Middle Africa regionByCountryCodeMap.put("CL", "AM"); // name=Chile / region=Americas / subregion=South America regionByCountryCodeMap.put("CN", "AS"); // name=China / region=Asia / subregion=Eastern Asia regionByCountryCodeMap.put("CX", "OC"); // name=Christmas Island / region=Oceania / subregion=Australia and New Zealand regionByCountryCodeMap.put("CC", "OC"); // name=Cocos (Keeling) Islands / region=Oceania / subregion=Australia and New Zealand regionByCountryCodeMap.put("CO", "AM"); // name=Colombia / region=Americas / subregion=South America regionByCountryCodeMap.put("KM", "AF"); // name=Comoros / region=Africa / subregion=Eastern Africa regionByCountryCodeMap.put("CG", "AF"); // name=Congo / region=Africa / subregion=Middle Africa regionByCountryCodeMap.put("CD", "AF"); // name=Congo (Democratic Republic of the) / region=Africa / subregion=Middle Africa regionByCountryCodeMap.put("CK", "OC"); // name=Cook Islands / region=Oceania / subregion=Polynesia regionByCountryCodeMap.put("CR", "AM"); // name=Costa Rica / region=Americas / subregion=Central America regionByCountryCodeMap.put("HR", "EU"); // name=Croatia / region=Europe / subregion=Southern Europe regionByCountryCodeMap.put("CU", "AM"); // name=Cuba / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("CW", "AM"); // name=Curaçao / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("CY", "EU"); // name=Cyprus / region=Europe / subregion=Southern Europe regionByCountryCodeMap.put("CZ", "EU"); // name=Czech Republic / region=Europe / subregion=Eastern Europe regionByCountryCodeMap.put("DK", "EU"); // name=Denmark / region=Europe / subregion=Northern Europe regionByCountryCodeMap.put("DJ", "AF"); // name=Djibouti / region=Africa / subregion=Eastern Africa regionByCountryCodeMap.put("DM", "AM"); // name=Dominica / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("DO", "AM"); // name=Dominican Republic / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("EA", "EU"); // name=Ceuta, Melilla / region=Europe / subregion=Northern Africa regionByCountryCodeMap.put("EC", "AM"); // name=Ecuador / region=Americas / subregion=South America regionByCountryCodeMap.put("EG", "AF"); // name=Egypt / region=Africa / subregion=Northern Africa regionByCountryCodeMap.put("SV", "AM"); // name=El Salvador / region=Americas / subregion=Central America regionByCountryCodeMap.put("GQ", "AF"); // name=Equatorial Guinea / region=Africa / subregion=Middle Africa regionByCountryCodeMap.put("ER", "AF"); // name=Eritrea / region=Africa / subregion=Eastern Africa regionByCountryCodeMap.put("EE", "EU"); // name=Estonia / region=Europe / subregion=Northern Europe regionByCountryCodeMap.put("ET", "AF"); // name=Ethiopia / region=Africa / subregion=Eastern Africa regionByCountryCodeMap.put("FK", "AM"); // name=Falkland Islands (Malvinas) / region=Americas / subregion=South America regionByCountryCodeMap.put("FO", "EU"); // name=Faroe Islands / region=Europe / subregion=Northern Europe regionByCountryCodeMap.put("FJ", "OC"); // name=Fiji / region=Oceania / subregion=Melanesia regionByCountryCodeMap.put("FI", "EU"); // name=Finland / region=Europe / subregion=Northern Europe regionByCountryCodeMap.put("FR", "EU"); // name=France / region=Europe / subregion=Western Europe regionByCountryCodeMap.put("GF", "AM"); // name=French Guiana / region=Americas / subregion=South America regionByCountryCodeMap.put("PF", "OC"); // name=French Polynesia / region=Oceania / subregion=Polynesia regionByCountryCodeMap.put("TF", "AF"); // name=French Southern Territories / region=Africa / subregion=Southern Africa regionByCountryCodeMap.put("GA", "AF"); // name=Gabon / region=Africa / subregion=Middle Africa regionByCountryCodeMap.put("GM", "AF"); // name=Gambia / region=Africa / subregion=Western Africa regionByCountryCodeMap.put("GE", "AS"); // name=Georgia / region=Asia / subregion=Western Asia regionByCountryCodeMap.put("DE", "EU"); // name=Germany / region=Europe / subregion=Western Europe regionByCountryCodeMap.put("GH", "AF"); // name=Ghana / region=Africa / subregion=Western Africa regionByCountryCodeMap.put("GI", "EU"); // name=Gibraltar / region=Europe / subregion=Southern Europe regionByCountryCodeMap.put("GR", "EU"); // name=Greece / region=Europe / subregion=Southern Europe regionByCountryCodeMap.put("GL", "AM"); // name=Greenland / region=Americas / subregion=Northern America regionByCountryCodeMap.put("GD", "AM"); // name=Grenada / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("GP", "AM"); // name=Guadeloupe / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("GU", "OC"); // name=Guam / region=Oceania / subregion=Micronesia regionByCountryCodeMap.put("GT", "AM"); // name=Guatemala / region=Americas / subregion=Central America regionByCountryCodeMap.put("GG", "EU"); // name=Guernsey / region=Europe / subregion=Northern Europe regionByCountryCodeMap.put("GN", "AF"); // name=Guinea / region=Africa / subregion=Western Africa regionByCountryCodeMap.put("GW", "AF"); // name=Guinea-Bissau / region=Africa / subregion=Western Africa regionByCountryCodeMap.put("GY", "AM"); // name=Guyana / region=Americas / subregion=South America regionByCountryCodeMap.put("HT", "AM"); // name=Haiti / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("VA", "EU"); // name=Holy See / region=Europe / subregion=Southern Europe regionByCountryCodeMap.put("HN", "AM"); // name=Honduras / region=Americas / subregion=Central America regionByCountryCodeMap.put("HK", "AS"); // name=Hong Kong / region=Asia / subregion=Eastern Asia regionByCountryCodeMap.put("HU", "EU"); // name=Hungary / region=Europe / subregion=Eastern Europe regionByCountryCodeMap.put("IC", "EU"); // name=Canary Islands / region=Europe / subregion=Northern Africa regionByCountryCodeMap.put("IS", "EU"); // name=Iceland / region=Europe / subregion=Northern Europe regionByCountryCodeMap.put("IN", "AS"); // name=India / region=Asia / subregion=Southern Asia regionByCountryCodeMap.put("ID", "AS"); // name=Indonesia / region=Asia / subregion=South-Eastern Asia regionByCountryCodeMap.put("CI", "AF"); // name=Côte d'Ivoire / region=Africa / subregion=Western Africa regionByCountryCodeMap.put("IR", "AS"); // name=Iran (Islamic Republic of) / region=Asia / subregion=Southern Asia regionByCountryCodeMap.put("IQ", "AS"); // name=Iraq / region=Asia / subregion=Western Asia regionByCountryCodeMap.put("IE", "EU"); // name=Ireland / region=Europe / subregion=Northern Europe regionByCountryCodeMap.put("IM", "EU"); // name=Isle of Man / region=Europe / subregion=Northern Europe regionByCountryCodeMap.put("IL", "AS"); // name=Israel / region=Asia / subregion=Western Asia regionByCountryCodeMap.put("IT", "EU"); // name=Italy / region=Europe / subregion=Southern Europe regionByCountryCodeMap.put("JM", "AM"); // name=Jamaica / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("JP", "AS"); // name=Japan / region=Asia / subregion=Eastern Asia regionByCountryCodeMap.put("JE", "EU"); // name=Jersey / region=Europe / subregion=Northern Europe regionByCountryCodeMap.put("JO", "AS"); // name=Jordan / region=Asia / subregion=Western Asia regionByCountryCodeMap.put("KZ", "AS"); // name=Kazakhstan / region=Asia / subregion=Central Asia regionByCountryCodeMap.put("KE", "AF"); // name=Kenya / region=Africa / subregion=Eastern Africa regionByCountryCodeMap.put("KI", "OC"); // name=Kiribati / region=Oceania / subregion=Micronesia regionByCountryCodeMap.put("KW", "AS"); // name=Kuwait / region=Asia / subregion=Western Asia regionByCountryCodeMap.put("KG", "AS"); // name=Kyrgyzstan / region=Asia / subregion=Central Asia regionByCountryCodeMap.put("LA", "AS"); // name=Lao People's Democratic Republic / region=Asia / subregion=South-Eastern Asia regionByCountryCodeMap.put("LV", "EU"); // name=Latvia / region=Europe / subregion=Northern Europe regionByCountryCodeMap.put("LB", "AS"); // name=Lebanon / region=Asia / subregion=Western Asia regionByCountryCodeMap.put("LS", "AF"); // name=Lesotho / region=Africa / subregion=Southern Africa regionByCountryCodeMap.put("LR", "AF"); // name=Liberia / region=Africa / subregion=Western Africa regionByCountryCodeMap.put("LY", "AF"); // name=Libya / region=Africa / subregion=Northern Africa regionByCountryCodeMap.put("LI", "EU"); // name=Liechtenstein / region=Europe / subregion=Western Europe regionByCountryCodeMap.put("LT", "EU"); // name=Lithuania / region=Europe / subregion=Northern Europe regionByCountryCodeMap.put("LU", "EU"); // name=Luxembourg / region=Europe / subregion=Western Europe regionByCountryCodeMap.put("MO", "AS"); // name=Macao / region=Asia / subregion=Eastern Asia regionByCountryCodeMap.put("MK", "EU"); // name=Macedonia (the former Yugoslav Republic of) / region=Europe / subregion=Southern Europe regionByCountryCodeMap.put("MG", "AF"); // name=Madagascar / region=Africa / subregion=Eastern Africa regionByCountryCodeMap.put("MW", "AF"); // name=Malawi / region=Africa / subregion=Eastern Africa regionByCountryCodeMap.put("MY", "AS"); // name=Malaysia / region=Asia / subregion=South-Eastern Asia regionByCountryCodeMap.put("MV", "AS"); // name=Maldives / region=Asia / subregion=Southern Asia regionByCountryCodeMap.put("ML", "AF"); // name=Mali / region=Africa / subregion=Western Africa regionByCountryCodeMap.put("MT", "EU"); // name=Malta / region=Europe / subregion=Southern Europe regionByCountryCodeMap.put("MH", "OC"); // name=Marshall Islands / region=Oceania / subregion=Micronesia regionByCountryCodeMap.put("MQ", "AM"); // name=Martinique / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("MR", "AF"); // name=Mauritania / region=Africa / subregion=Western Africa regionByCountryCodeMap.put("MU", "AF"); // name=Mauritius / region=Africa / subregion=Eastern Africa regionByCountryCodeMap.put("YT", "AF"); // name=Mayotte / region=Africa / subregion=Eastern Africa regionByCountryCodeMap.put("MX", "AM"); // name=Mexico / region=Americas / subregion=Central America regionByCountryCodeMap.put("FM", "OC"); // name=Micronesia (Federated States of) / region=Oceania / subregion=Micronesia regionByCountryCodeMap.put("MD", "EU"); // name=Moldova (Republic of) / region=Europe / subregion=Eastern Europe regionByCountryCodeMap.put("MC", "EU"); // name=Monaco / region=Europe / subregion=Western Europe regionByCountryCodeMap.put("MN", "AS"); // name=Mongolia / region=Asia / subregion=Eastern Asia regionByCountryCodeMap.put("ME", "EU"); // name=Montenegro / region=Europe / subregion=Southern Europe regionByCountryCodeMap.put("MS", "AM"); // name=Montserrat / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("MA", "AF"); // name=Morocco / region=Africa / subregion=Northern Africa regionByCountryCodeMap.put("MZ", "AF"); // name=Mozambique / region=Africa / subregion=Eastern Africa regionByCountryCodeMap.put("MM", "AS"); // name=Myanmar / region=Asia / subregion=South-Eastern Asia regionByCountryCodeMap.put("NA", "AF"); // name=Namibia / region=Africa / subregion=Southern Africa regionByCountryCodeMap.put("NR", "OC"); // name=Nauru / region=Oceania / subregion=Micronesia regionByCountryCodeMap.put("NP", "AS"); // name=Nepal / region=Asia / subregion=Southern Asia regionByCountryCodeMap.put("NL", "EU"); // name=Netherlands / region=Europe / subregion=Western Europe regionByCountryCodeMap.put("NC", "OC"); // name=New Caledonia / region=Oceania / subregion=Melanesia regionByCountryCodeMap.put("NZ", "OC"); // name=New Zealand / region=Oceania / subregion=Australia and New Zealand regionByCountryCodeMap.put("NI", "AM"); // name=Nicaragua / region=Americas / subregion=Central America regionByCountryCodeMap.put("NE", "AF"); // name=Niger / region=Africa / subregion=Western Africa regionByCountryCodeMap.put("NG", "AF"); // name=Nigeria / region=Africa / subregion=Western Africa regionByCountryCodeMap.put("NU", "OC"); // name=Niue / region=Oceania / subregion=Polynesia regionByCountryCodeMap.put("NF", "OC"); // name=Norfolk Island / region=Oceania / subregion=Australia and New Zealand regionByCountryCodeMap.put("KP", "AS"); // name=Korea (Democratic People's Republic of) / region=Asia / subregion=Eastern Asia regionByCountryCodeMap.put("MP", "OC"); // name=Northern Mariana Islands / region=Oceania / subregion=Micronesia regionByCountryCodeMap.put("NO", "EU"); // name=Norway / region=Europe / subregion=Northern Europe regionByCountryCodeMap.put("OM", "AS"); // name=Oman / region=Asia / subregion=Western Asia regionByCountryCodeMap.put("PK", "AS"); // name=Pakistan / region=Asia / subregion=Southern Asia regionByCountryCodeMap.put("PW", "OC"); // name=Palau / region=Oceania / subregion=Micronesia regionByCountryCodeMap.put("PS", "AS"); // name=Palestine, State of / region=Asia / subregion=Western Asia regionByCountryCodeMap.put("PA", "AM"); // name=Panama / region=Americas / subregion=Central America regionByCountryCodeMap.put("PG", "OC"); // name=Papua New Guinea / region=Oceania / subregion=Melanesia regionByCountryCodeMap.put("PY", "AM"); // name=Paraguay / region=Americas / subregion=South America regionByCountryCodeMap.put("PE", "AM"); // name=Peru / region=Americas / subregion=South America regionByCountryCodeMap.put("PH", "AS"); // name=Philippines / region=Asia / subregion=South-Eastern Asia regionByCountryCodeMap.put("PN", "OC"); // name=Pitcairn / region=Oceania / subregion=Polynesia regionByCountryCodeMap.put("PL", "EU"); // name=Poland / region=Europe / subregion=Eastern Europe regionByCountryCodeMap.put("PT", "EU"); // name=Portugal / region=Europe / subregion=Southern Europe regionByCountryCodeMap.put("PR", "AM"); // name=Puerto Rico / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("QA", "AS"); // name=Qatar / region=Asia / subregion=Western Asia regionByCountryCodeMap.put("XK", "EU"); // name=Republic of Kosovo / region=Europe / subregion=Eastern Europe regionByCountryCodeMap.put("RE", "AF"); // name=Réunion / region=Africa / subregion=Eastern Africa regionByCountryCodeMap.put("RO", "EU"); // name=Romania / region=Europe / subregion=Eastern Europe regionByCountryCodeMap.put("RU", "EU"); // name=Russian Federation / region=Europe / subregion=Eastern Europe regionByCountryCodeMap.put("RW", "AF"); // name=Rwanda / region=Africa / subregion=Eastern Africa regionByCountryCodeMap.put("BL", "AM"); // name=Saint Barthélemy / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("SH", "AF"); // name=Saint Helena, Ascension and Tristan da Cunha / region=Africa / subregion=Western Africa regionByCountryCodeMap.put("KN", "AM"); // name=Saint Kitts and Nevis / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("LC", "AM"); // name=Saint Lucia / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("MF", "AM"); // name=Saint Martin (French part) / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("PM", "AM"); // name=Saint Pierre and Miquelon / region=Americas / subregion=Northern America regionByCountryCodeMap.put("VC", "AM"); // name=Saint Vincent and the Grenadines / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("WS", "OC"); // name=Samoa / region=Oceania / subregion=Polynesia regionByCountryCodeMap.put("SM", "EU"); // name=San Marino / region=Europe / subregion=Southern Europe regionByCountryCodeMap.put("ST", "AF"); // name=Sao Tome and Principe / region=Africa / subregion=Middle Africa regionByCountryCodeMap.put("SA", "AS"); // name=Saudi Arabia / region=Asia / subregion=Western Asia regionByCountryCodeMap.put("SN", "AF"); // name=Senegal / region=Africa / subregion=Western Africa regionByCountryCodeMap.put("RS", "EU"); // name=Serbia / region=Europe / subregion=Southern Europe regionByCountryCodeMap.put("SC", "AF"); // name=Seychelles / region=Africa / subregion=Eastern Africa regionByCountryCodeMap.put("SL", "AF"); // name=Sierra Leone / region=Africa / subregion=Western Africa regionByCountryCodeMap.put("SG", "AS"); // name=Singapore / region=Asia / subregion=South-Eastern Asia regionByCountryCodeMap.put("SX", "AM"); // name=Sint Maarten (Dutch part) / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("SK", "EU"); // name=Slovakia / region=Europe / subregion=Eastern Europe regionByCountryCodeMap.put("SI", "EU"); // name=Slovenia / region=Europe / subregion=Southern Europe regionByCountryCodeMap.put("SB", "OC"); // name=Solomon Islands / region=Oceania / subregion=Melanesia regionByCountryCodeMap.put("SO", "AF"); // name=Somalia / region=Africa / subregion=Eastern Africa regionByCountryCodeMap.put("ZA", "AF"); // name=South Africa / region=Africa / subregion=Southern Africa regionByCountryCodeMap.put("GS", "AM"); // name=South Georgia and the South Sandwich Islands / region=Americas / subregion=South America regionByCountryCodeMap.put("KR", "AS"); // name=Korea (Republic of) / region=Asia / subregion=Eastern Asia regionByCountryCodeMap.put("SS", "AF"); // name=South Sudan / region=Africa / subregion=Middle Africa regionByCountryCodeMap.put("ES", "EU"); // name=Spain / region=Europe / subregion=Southern Europe regionByCountryCodeMap.put("LK", "AS"); // name=Sri Lanka / region=Asia / subregion=Southern Asia regionByCountryCodeMap.put("SD", "AF"); // name=Sudan / region=Africa / subregion=Northern Africa regionByCountryCodeMap.put("SR", "AM"); // name=Suriname / region=Americas / subregion=South America regionByCountryCodeMap.put("SJ", "EU"); // name=Svalbard and Jan Mayen / region=Europe / subregion=Northern Europe regionByCountryCodeMap.put("SZ", "AF"); // name=Swaziland / region=Africa / subregion=Southern Africa regionByCountryCodeMap.put("SE", "EU"); // name=Sweden / region=Europe / subregion=Northern Europe regionByCountryCodeMap.put("CH", "EU"); // name=Switzerland / region=Europe / subregion=Western Europe regionByCountryCodeMap.put("SY", "AS"); // name=Syrian Arab Republic / region=Asia / subregion=Western Asia regionByCountryCodeMap.put("TW", "AS"); // name=Taiwan / region=Asia / subregion=Eastern Asia regionByCountryCodeMap.put("TJ", "AS"); // name=Tajikistan / region=Asia / subregion=Central Asia regionByCountryCodeMap.put("TZ", "AF"); // name=Tanzania, United Republic of / region=Africa / subregion=Eastern Africa regionByCountryCodeMap.put("TH", "AS"); // name=Thailand / region=Asia / subregion=South-Eastern Asia regionByCountryCodeMap.put("TL", "AS"); // name=Timor-Leste / region=Asia / subregion=South-Eastern Asia regionByCountryCodeMap.put("TG", "AF"); // name=Togo / region=Africa / subregion=Western Africa regionByCountryCodeMap.put("TK", "OC"); // name=Tokelau / region=Oceania / subregion=Polynesia regionByCountryCodeMap.put("TO", "OC"); // name=Tonga / region=Oceania / subregion=Polynesia regionByCountryCodeMap.put("TT", "AM"); // name=Trinidad and Tobago / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("TN", "AF"); // name=Tunisia / region=Africa / subregion=Northern Africa regionByCountryCodeMap.put("TR", "AS"); // name=Turkey / region=Asia / subregion=Western Asia regionByCountryCodeMap.put("TM", "AS"); // name=Turkmenistan / region=Asia / subregion=Central Asia regionByCountryCodeMap.put("TC", "AM"); // name=Turks and Caicos Islands / region=Americas / subregion=Caribbean regionByCountryCodeMap.put("TV", "OC"); // name=Tuvalu / region=Oceania / subregion=Polynesia regionByCountryCodeMap.put("UG", "AF"); // name=Uganda / region=Africa / subregion=Eastern Africa regionByCountryCodeMap.put("UA", "EU"); // name=Ukraine / region=Europe / subregion=Eastern Europe regionByCountryCodeMap.put("AE", "AS"); // name=United Arab Emirates / region=Asia / subregion=Western Asia regionByCountryCodeMap.put("GB", "EU"); // name=United Kingdom of Great Britain and Northern Ireland / region=Europe / subregion=Northern Europe regionByCountryCodeMap.put("US", "AM"); // name=United States of America / region=Americas / subregion=Northern America regionByCountryCodeMap.put("UY", "AM"); // name=Uruguay / region=Americas / subregion=South America regionByCountryCodeMap.put("UZ", "AS"); // name=Uzbekistan / region=Asia / subregion=Central Asia regionByCountryCodeMap.put("VU", "OC"); // name=Vanuatu / region=Oceania / subregion=Melanesia regionByCountryCodeMap.put("VE", "AM"); // name=Venezuela (Bolivarian Republic of) / region=Americas / subregion=South America regionByCountryCodeMap.put("VN", "AS"); // name=Viet Nam / region=Asia / subregion=South-Eastern Asia regionByCountryCodeMap.put("WF", "OC"); // name=Wallis and Futuna / region=Oceania / subregion=Polynesia regionByCountryCodeMap.put("EH", "AF"); // name=Western Sahara / region=Africa / subregion=Northern Africa regionByCountryCodeMap.put("YE", "AS"); // name=Yemen / region=Asia / subregion=Western Asia regionByCountryCodeMap.put("ZM", "AF"); // name=Zambia / region=Africa / subregion=Eastern Africa regionByCountryCodeMap.put("ZW", "AF"); // name=Zimbabwe / region=Africa / subregion=Eastern Africa } public static String getRegionCode(String countryCode) { if (regionByCountryCodeMap.containsKey(countryCode)) return regionByCountryCodeMap.get(countryCode); else return "Undefined"; } public static String getDefaultCountryCode() { // might be set later in pref or config, so not use Preferences.getDefaultLocale() anywhere in the code return getLocale().getCountry(); } private static Locale getLocale() { return GlobalSettings.getLocale(); } } ================================================ FILE: core/src/main/java/haveno/core/locale/CryptoCurrency.java ================================================ /* * 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 . */ package haveno.core.locale; import java.util.Optional; import com.google.protobuf.Message; import lombok.Getter; public final class CryptoCurrency extends TradeCurrency { // http://boschista.deviantart.com/journal/Cool-ASCII-Symbols-214218618 private final static String PREFIX = "✦ "; @Getter private boolean isAsset = false; public CryptoCurrency(String currencyCode, String name) { this(currencyCode, name, false); } public CryptoCurrency(String currencyCode, String name, boolean isAsset) { super(currencyCode, name); this.isAsset = isAsset; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public Message toProtoMessage() { return getTradeCurrencyBuilder() .setCryptoCurrency(protobuf.CryptoCurrency.newBuilder() .setIsAsset(isAsset)) .build(); } public static CryptoCurrency fromProto(protobuf.TradeCurrency proto) { Optional currency = CurrencyUtil.getTradeCurrency(proto.getCode()); if (currency.isPresent() && currency.get() instanceof CryptoCurrency) { return new CryptoCurrency(proto.getCode(), CurrencyUtil.getNameByCode(proto.getCode()), proto.getCryptoCurrency().getIsAsset()); } else { return null; } } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getDisplayPrefix() { return PREFIX; } } ================================================ FILE: core/src/main/java/haveno/core/locale/CurrencyTuple.java ================================================ /* * 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 . */ package haveno.core.locale; import haveno.core.monetary.TraditionalMoney; import lombok.EqualsAndHashCode; @EqualsAndHashCode public class CurrencyTuple { public final String code; public final String name; public final int precision; // precision 8 is 1/100000000 -> 0.00000001 is smallest unit public CurrencyTuple(String code, String name) { // We use TraditionalCurrency class and the precision is 8 // In future we might add custom precision per currency this(code, name, TraditionalMoney.SMALLEST_UNIT_EXPONENT); } public CurrencyTuple(String code, String name, int precision) { this.code = code; this.name = name; this.precision = precision; } } ================================================ FILE: core/src/main/java/haveno/core/locale/CurrencyUtil.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.locale; import com.google.common.base.Suppliers; import haveno.asset.Asset; import haveno.asset.AssetRegistry; import haveno.asset.Coin; import haveno.asset.Token; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; import haveno.core.filter.FilterManager; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.Currency; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; import static java.lang.String.format; @Slf4j public class CurrencyUtil { public static void setup() { setBaseCurrencyCode(baseCurrencyCode); } private static final AssetRegistry assetRegistry = new AssetRegistry(); private static String baseCurrencyCode = "XMR"; // Calls to isTraditionalCurrency and isCryptoCurrency are very frequent so we use a cache of the results. // The main improvement was already achieved with using memoize for the source maps, but // the caching still reduces performance costs by about 20% for isCryptoCurrency (1752 ms vs 2121 ms) and about 50% // for isTraditionalCurrency calls (1777 ms vs 3467 ms). // See: https://github.com/bisq-network/bisq/pull/4955#issuecomment-745302802 private static final Map isTraditionalCurrencyMap = new ConcurrentHashMap<>(); private static final Map isCryptoCurrencyMap = new ConcurrentHashMap<>(); private static final Supplier> traditionalCurrencyMapSupplier = Suppliers.memoize( CurrencyUtil::createTraditionalCurrencyMap); private static final Supplier> cryptoCurrencyMapSupplier = Suppliers.memoize( CurrencyUtil::createCryptoCurrencyMap); public static void setBaseCurrencyCode(String baseCurrencyCode) { CurrencyUtil.baseCurrencyCode = baseCurrencyCode; } public static Collection getAllSortedFiatCurrencies(Comparator comparator) { return getAllSortedTraditionalCurrencies(comparator).stream() .filter(currency -> CurrencyUtil.isFiatCurrency(currency.getCode())) .collect(Collectors.toList()); // sorted by currency name } public static List getAllFiatCurrencies() { return getAllTraditionalCurrencies().stream() .filter(currency -> CurrencyUtil.isFiatCurrency(currency.getCode())) .collect(Collectors.toList()); } public static List getAllSortedFiatCurrencies() { return getAllSortedTraditionalCurrencies().stream() .filter(currency -> CurrencyUtil.isFiatCurrency(currency.getCode())) .collect(Collectors.toList()); // sorted by currency name } public static Collection getAllSortedTraditionalCurrencies() { return traditionalCurrencyMapSupplier.get().values(); // sorted by currency name } public static List getAllTraditionalCurrencies() { return new ArrayList<>(traditionalCurrencyMapSupplier.get().values()); } public static List getTraditionalNonFiatCurrencies() { return Arrays.asList( new TraditionalCurrency("XAG", "Silver"), new TraditionalCurrency("XAU", "Gold"), new TraditionalCurrency("XGB", "Goldback") ); } public static Collection getAllSortedTraditionalCurrencies(Comparator comparator) { return (List) getAllSortedTraditionalCurrencies().stream() .sorted(comparator) .collect(Collectors.toList()); } private static Map createTraditionalCurrencyMap() { List currencies = CountryUtil.getAllCountries().stream() .map(country -> getCurrencyByCountryCode(country.code)) .collect(Collectors.toList()); currencies.addAll(getTraditionalNonFiatCurrencies()); return currencies.stream().sorted(TradeCurrency::compareTo) .distinct() .collect(Collectors.toMap(TradeCurrency::getCode, Function.identity(), (x, y) -> x, LinkedHashMap::new)); } public static List getMainFiatCurrencies() { List list = new ArrayList<>(); list.add(new TraditionalCurrency("USD")); list.add(new TraditionalCurrency("EUR")); list.add(new TraditionalCurrency("GBP")); list.add(new TraditionalCurrency("CAD")); list.add(new TraditionalCurrency("AUD")); list.add(new TraditionalCurrency("RUB")); list.add(new TraditionalCurrency("INR")); list.add(new TraditionalCurrency("NGN")); postProcessTraditionalCurrenciesList(list); return list; } public static List getMainTraditionalCurrencies() { List list = getMainFiatCurrencies(); list.addAll(getTraditionalNonFiatCurrencies()); postProcessTraditionalCurrenciesList(list); return list; } public static boolean isTraditionalNonFiatCurrency(String currencyCode) { return getTraditionalNonFiatCurrencies().stream().anyMatch(c -> c.getCode().equals(currencyCode)); } private static void postProcessTraditionalCurrenciesList(List list) { list.sort(TradeCurrency::compareTo); TradeCurrency defaultTradeCurrency = getDefaultTradeCurrency(); TraditionalCurrency defaultTraditionalCurrency = defaultTradeCurrency instanceof TraditionalCurrency ? (TraditionalCurrency) defaultTradeCurrency : null; if (defaultTraditionalCurrency != null && list.contains(defaultTraditionalCurrency)) { list.remove(defaultTradeCurrency); list.add(0, defaultTraditionalCurrency); } } public static Collection getAllSortedCryptoCurrencies() { return cryptoCurrencyMapSupplier.get().values(); } private static Map createCryptoCurrencyMap() { return getSortedAssetStream() .map(CurrencyUtil::assetToCryptoCurrency) .collect(Collectors.toMap(TradeCurrency::getCode, Function.identity(), (x, y) -> x, LinkedHashMap::new)); } public static Stream getSortedAssetStream() { return assetRegistry.stream() .filter(CurrencyUtil::assetIsNotBaseCurrency) .filter(asset -> assetMatchesNetworkIfMainnet(asset, Config.baseCurrencyNetwork())) .sorted(Comparator.comparing(Asset::getName)); } public static List getMainCryptoCurrencies() { final List result = new ArrayList<>(); result.add(new CryptoCurrency("BTC", "Bitcoin")); result.add(new CryptoCurrency("BCH", "Bitcoin Cash")); result.add(new CryptoCurrency("DOGE", "Dogecoin")); result.add(new CryptoCurrency("ETH", "Ether")); result.add(new CryptoCurrency("LTC", "Litecoin")); result.add(new CryptoCurrency("XRP", "Ripple")); result.add(new CryptoCurrency("ADA", "Cardano")); result.add(new CryptoCurrency("SOL", "Solana")); result.add(new CryptoCurrency("TRX", "Tron")); result.add(new CryptoCurrency("DAI-ERC20", "Dai Stablecoin")); result.add(new CryptoCurrency("USDT-ERC20", "Tether USD")); result.add(new CryptoCurrency("USDC-ERC20", "USD Coin")); result.sort(TradeCurrency::compareTo); return result; } public static List getRemovedCryptoCurrencies() { final List currencies = new ArrayList<>(); currencies.add(new CryptoCurrency("BCHC", "Bitcoin Clashic")); currencies.add(new CryptoCurrency("ACH", "AchieveCoin")); currencies.add(new CryptoCurrency("SC", "Siacoin")); currencies.add(new CryptoCurrency("PPI", "PiedPiper Coin")); currencies.add(new CryptoCurrency("PEPECASH", "Pepe Cash")); currencies.add(new CryptoCurrency("GRC", "Gridcoin")); currencies.add(new CryptoCurrency("LTZ", "LitecoinZ")); currencies.add(new CryptoCurrency("ZOC", "01coin")); currencies.add(new CryptoCurrency("BURST", "Burstcoin")); currencies.add(new CryptoCurrency("STEEM", "Steem")); currencies.add(new CryptoCurrency("DAC", "DACash")); currencies.add(new CryptoCurrency("RDD", "ReddCoin")); return currencies; } public static List getMatureMarketCurrencies() { ArrayList currencies = new ArrayList<>(Arrays.asList( new TraditionalCurrency("EUR"), new TraditionalCurrency("USD"), new TraditionalCurrency("GBP"), new TraditionalCurrency("CAD"), new TraditionalCurrency("AUD"), new TraditionalCurrency("BRL") )); currencies.sort(Comparator.comparing(TradeCurrency::getCode)); return currencies; } public static boolean isFiatCurrency(String currencyCode) { if (!isTraditionalCurrency(currencyCode)) return false; if (isTraditionalNonFiatCurrency(currencyCode)) return false; return true; } public static boolean isTraditionalCurrency(String currencyCode) { if (currencyCode != null) currencyCode = currencyCode.toUpperCase(); if (currencyCode != null && isTraditionalCurrencyMap.containsKey(currencyCode)) { return isTraditionalCurrencyMap.get(currencyCode); } try { boolean isTraditionalCurrency = currencyCode != null && !currencyCode.isEmpty() && !isCryptoCurrency(currencyCode) && (isTraditionalNonFiatCurrency(currencyCode) || Currency.getInstance(currencyCode) != null); if (currencyCode != null) { isTraditionalCurrencyMap.put(currencyCode, isTraditionalCurrency); } return isTraditionalCurrency; } catch (Throwable t) { isTraditionalCurrencyMap.put(currencyCode, false); return false; } } public static boolean isVolumeRoundedToNearestUnit(String currencyCode) { return isFiatCurrency(currencyCode) || "XGB".equals(currencyCode.toUpperCase()); } public static boolean isPricePrecise(String currencyCode) { return isCryptoCurrency(currencyCode) || "XAU".equals(currencyCode.toUpperCase()) || "XAG".equals(currencyCode.toUpperCase()); } public static Optional getTraditionalCurrency(String currencyCode) { return Optional.ofNullable(traditionalCurrencyMapSupplier.get().get(currencyCode)); } /** * We return true if it is XMR or any of our currencies available in the assetRegistry. * For removed assets it would fail as they are not found but we don't want to conclude that they are traditional then. * As the caller might not deal with the case that a currency can be neither a cryptoCurrency nor Traditional if not found * we return true as well in case we have no traditional currency for the code. * * As we use a boolean result for isCryptoCurrency and isTraditionalCurrency we do not treat missing currencies correctly. * To throw an exception might be an option but that will require quite a lot of code change, so we don't do that * for the moment, but could be considered for the future. Another maybe better option is to introduce an enum which * contains 3 entries (CryptoCurrency, Traditional, Undefined). */ public static boolean isCryptoCurrency(String currencyCode) { if (currencyCode != null) currencyCode = currencyCode.toUpperCase(); if (currencyCode != null && isCryptoCurrencyMap.containsKey(currencyCode.toUpperCase())) { return isCryptoCurrencyMap.get(currencyCode.toUpperCase()); } if (isCryptoCurrencyCodeBase(currencyCode)) { return true; } boolean isCryptoCurrency; if (currencyCode == null) { // Some tests call that method with null values. Should be fixed in the tests but to not break them return false. isCryptoCurrency = false; } else if (getCryptoCurrency(currencyCode).isPresent()) { // If we find the code in our assetRegistry we return true. // It might be that an asset was removed from the assetsRegistry, we deal with such cases below by checking if // it is a traditional currency isCryptoCurrency = true; } else if (getTraditionalCurrency(currencyCode).isEmpty()) { // In case the code is from a removed asset we cross check if there exist a traditional currency with that code, // if we don't find a traditional currency we treat it as a crypto currency. isCryptoCurrency = true; } else { // If we would have found a traditional currency we return false isCryptoCurrency = false; } if (currencyCode != null) { isCryptoCurrencyMap.put(currencyCode, isCryptoCurrency); } return isCryptoCurrency; } private static boolean isCryptoCurrencyCodeBase(String currencyCode) { if (currencyCode == null) return false; currencyCode = currencyCode.toUpperCase(); return currencyCode.equals("USDT") || currencyCode.equals("USDC") || currencyCode.equals("DAI"); } public static String getCurrencyCodeBase(String currencyCode) { if (currencyCode == null) return null; currencyCode = currencyCode.toUpperCase(); if (currencyCode.contains("USDT")) return "USDT"; if (currencyCode.contains("USDC")) return "USDC"; if (currencyCode.contains("DAI")) return "DAI"; return currencyCode; } public static Optional getCryptoCurrency(String currencyCode) { return Optional.ofNullable(cryptoCurrencyMapSupplier.get().get(currencyCode)); } public static Optional getTradeCurrency(String currencyCode) { Optional traditionalCurrencyOptional = getTraditionalCurrency(currencyCode); if (traditionalCurrencyOptional.isPresent() && isTraditionalCurrency(currencyCode)) return Optional.of(traditionalCurrencyOptional.get()); Optional cryptoCurrencyOptional = getCryptoCurrency(currencyCode); if (cryptoCurrencyOptional.isPresent() && isCryptoCurrency(currencyCode)) return Optional.of(cryptoCurrencyOptional.get()); return Optional.empty(); } public static Optional> getTradeCurrencies(List currencyCodes) { List tradeCurrencies = new ArrayList<>(); currencyCodes.stream().forEachOrdered(c -> tradeCurrencies.add(getTradeCurrency(c).orElseThrow(() -> new IllegalArgumentException(format("%s is not a valid trade currency code", c))))); return tradeCurrencies.isEmpty() ? Optional.empty() : Optional.of(tradeCurrencies); } public static Optional> getTradeCurrenciesInList(List currencyCodes, List validCurrencies) { Optional> tradeCurrencies = getTradeCurrencies(currencyCodes); Consumer> validateCandidateCurrencies = (list) -> { for (TradeCurrency tradeCurrency : list) { if (!validCurrencies.contains(tradeCurrency)) { throw new IllegalArgumentException( format("%s is not a member of valid currencies list", tradeCurrency.getCode())); } } }; tradeCurrencies.ifPresent(validateCandidateCurrencies); return tradeCurrencies; } public static TraditionalCurrency getCurrencyByCountryCode(String countryCode) { if (countryCode.equals("XK")) return new TraditionalCurrency("EUR"); Currency currency = Currency.getInstance(new Locale(LanguageUtil.getDefaultLanguage(), countryCode)); return new TraditionalCurrency(currency.getCurrencyCode()); } public static String getNameByCode(String currencyCode) { if (isCryptoCurrency(currencyCode)) { // We might not find the name in case we have a call for a removed asset. // If BTC is the code (used in tests) we also want return Bitcoin as name. final Optional removedCryptoCurrency = getRemovedCryptoCurrencies().stream() .filter(cryptoCurrency -> cryptoCurrency.getCode().equals(currencyCode)) .findAny(); String xmrOrRemovedAsset = "XMR".equals(currencyCode) ? "Monero" : removedCryptoCurrency.isPresent() ? removedCryptoCurrency.get().getName() : Res.get("shared.na"); return getCryptoCurrency(currencyCode).map(TradeCurrency::getName).orElse(xmrOrRemovedAsset); } if (isTraditionalNonFiatCurrency(currencyCode)) { return getTraditionalNonFiatCurrencies().stream() .filter(currency -> currency.getCode().equals(currencyCode)) .findAny() .map(TradeCurrency::getName) .orElse(currencyCode); } try { return Currency.getInstance(currencyCode).getDisplayName(); } catch (Throwable t) { log.debug("No currency name available {}", t.getMessage()); return currencyCode; } } public static Optional findCryptoCurrencyByName(String currencyName) { return getAllSortedCryptoCurrencies().stream() .filter(e -> e.getName().equals(currencyName)) .findAny(); } public static String getNameAndCode(String currencyCode) { return getNameByCode(currencyCode) + " (" + currencyCode + ")"; } public static TradeCurrency getDefaultTradeCurrency() { return GlobalSettings.getDefaultTradeCurrency(); } private static boolean assetIsNotBaseCurrency(Asset asset) { return !assetMatchesCurrencyCode(asset, baseCurrencyCode); } // TODO We handle assets of other types (Token, ERC20) as matching the network which is not correct. // We should add support for network property in those tokens as well. public static boolean assetMatchesNetwork(Asset asset, BaseCurrencyNetwork baseCurrencyNetwork) { return !(asset instanceof Coin) || ((Coin) asset).getNetwork().name().equals(baseCurrencyNetwork.getNetwork()); } // We only check for coins not other types of assets (TODO network check should be supported for all assets) public static boolean assetMatchesNetworkIfMainnet(Asset asset, BaseCurrencyNetwork baseCurrencyNetwork) { return !(asset instanceof Coin) || coinMatchesNetworkIfMainnet((Coin) asset, baseCurrencyNetwork); } // We want all coins available also in testnet or regtest for testing purpose public static boolean coinMatchesNetworkIfMainnet(Coin coin, BaseCurrencyNetwork baseCurrencyNetwork) { boolean matchesNetwork = assetMatchesNetwork(coin, baseCurrencyNetwork); return !baseCurrencyNetwork.isMainnet() || matchesNetwork; } private static CryptoCurrency assetToCryptoCurrency(Asset asset) { return new CryptoCurrency(asset.getTickerSymbol(), asset.getName(), asset instanceof Token); } public static boolean assetMatchesCurrencyCode(Asset asset, String currencyCode) { return currencyCode.equals(asset.getTickerSymbol()); } public static Optional findAsset(AssetRegistry assetRegistry, String currencyCode, BaseCurrencyNetwork baseCurrencyNetwork) { List assets = assetRegistry.stream() .filter(asset -> assetMatchesCurrencyCode(asset, currencyCode)).collect(Collectors.toList()); // If we don't have the ticker symbol we throw an exception if (assets.stream().findFirst().isEmpty()) return Optional.empty(); // We check for exact match with network, e.g. BTC$TESTNET Optional optionalAssetMatchesNetwork = assets.stream() .filter(asset -> assetMatchesNetwork(asset, baseCurrencyNetwork)) .findFirst(); if (optionalAssetMatchesNetwork.isPresent()) return optionalAssetMatchesNetwork; // In testnet or regtest we want to show all coins as well. Most coins have only Mainnet defined so we deliver // that if no exact match was found in previous step if (!baseCurrencyNetwork.isMainnet()) { Optional optionalAsset = assets.stream().findFirst(); return optionalAsset; } // If we are in mainnet we need have a mainnet asset defined. throw new IllegalArgumentException("We are on mainnet and we could not find an asset with network type mainnet"); } public static Optional findAsset(String tickerSymbol) { return assetRegistry.stream() .filter(asset -> asset.getTickerSymbol().equals(tickerSymbol)) .findAny(); } public static Optional findAsset(String tickerSymbol, BaseCurrencyNetwork baseCurrencyNetwork) { return assetRegistry.stream() .filter(asset -> asset.getTickerSymbol().equals(tickerSymbol)) .filter(asset -> assetMatchesNetwork(asset, baseCurrencyNetwork)) .findAny(); } // Excludes all assets which got removed by voting public static List getActiveSortedCryptoCurrencies(FilterManager filterManager) { return getAllSortedCryptoCurrencies().stream() .filter(e -> !filterManager.isCurrencyBanned(e.getCode())) .collect(Collectors.toList()); } public static String getCurrencyPair(String currencyCode) { return Res.getBaseCurrencyCode() + "/" + currencyCode; } public static String getCounterCurrency(String currencyCode) { return currencyCode; } public static String getPriceWithCurrencyCode(String currencyCode) { return getPriceWithCurrencyCode(currencyCode, "shared.priceInCurForCur"); } public static String getPriceWithCurrencyCode(String currencyCode, String translationKey) { return Res.get(translationKey, currencyCode, Res.getBaseCurrencyCode()); } public static String getOfferVolumeCode(String currencyCode) { return Res.get("shared.offerVolumeCode", currencyCode); } public static List getAllTransferwiseUSDCurrencies() { return List.of(new TraditionalCurrency("USD")); } } ================================================ FILE: core/src/main/java/haveno/core/locale/GlobalSettings.java ================================================ /* * 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 . */ package haveno.core.locale; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleObjectProperty; import lombok.extern.slf4j.Slf4j; import java.util.Locale; @Slf4j public class GlobalSettings { private static boolean useAnimations = true; private static Locale locale; private static final ObjectProperty localeProperty = new SimpleObjectProperty<>(locale); private static TradeCurrency defaultTradeCurrency; private static String btcDenomination; static { locale = Locale.getDefault(); log.info("Locale info: {}", locale); // On some systems there is no country defined, in that case we use en_US if (locale.getCountry() == null || locale.getCountry().isEmpty()) locale = Locale.US; } public static void setLocale(Locale locale) { GlobalSettings.locale = locale; localeProperty.set(locale); } public static void setUseAnimations(boolean useAnimations) { GlobalSettings.useAnimations = useAnimations; } public static void setDefaultTradeCurrency(TradeCurrency tradeCurrency) { GlobalSettings.defaultTradeCurrency = tradeCurrency; } public static void setBtcDenomination(String btcDenomination) { GlobalSettings.btcDenomination = btcDenomination; } public static TradeCurrency getDefaultTradeCurrency() { return defaultTradeCurrency; } public static String getBtcDenomination() { return btcDenomination; } public static ReadOnlyObjectProperty localeProperty() { return localeProperty; } public static boolean getUseAnimations() { return useAnimations; } public static Locale getLocale() { return locale; } } ================================================ FILE: core/src/main/java/haveno/core/locale/LanguageUtil.java ================================================ /* * 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 . */ package haveno.core.locale; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.stream.Collectors; @Slf4j public class LanguageUtil { private static final List userLanguageCodes = Arrays.asList( "en", // English "de", // German "es", // Spanish "pt", // Portuguese "pt-BR", // Portuguese (Brazil) "zh-Hans", // Chinese [Han Simplified] "zh-Hant", // Chinese [Han Traditional] "ru", // Russian "fr", // French "vi", // Vietnamese "th", // Thai "ja", // Japanese "fa", // Persian "it", // Italian "cs", // Czech "pl", // Polish "tr" // Turkish /* // not translated yet "el", // Greek "sr-Latn-RS", // Serbian [Latin] (Serbia) "hu", // Hungarian "ro", // Romanian "iw", // Hebrew "hi", // Hindi "ko", // Korean "sv", // Swedish "no", // Norwegian "nl", // Dutch "be", // Belarusian "fi", // Finnish "bg", // Bulgarian "lt", // Lithuanian "lv", // Latvian "hr", // Croatian "uk", // Ukrainian "sk", // Slovak "sl", // Slovenian "ga", // Irish "sq", // Albanian "ca", // Catalan "mk", // Macedonian "kk", // Kazakh "km", // Khmer "sw", // Swahili "in", // Indonesian "ms", // Malay "is", // Icelandic "et", // Estonian "ar", // Arabic "vi", // Vietnamese "th", // Thai "da", // Danish "mt" // Maltese */ ); private static final List rtlLanguagesCodes = Arrays.asList( "fa", // Persian "ar", // Arabic "iw" // Hebrew ); public static List getAllLanguageCodes() { List allLocales = LocaleUtil.getAllLocales(); // Filter duplicate locale entries Set allLocalesAsSet = allLocales.stream().filter(locale -> !locale.getLanguage().isEmpty() && !locale.getDisplayLanguage().isEmpty()) .map(Locale::getLanguage) .collect(Collectors.toSet()); List allLanguageCodes = new ArrayList<>(); allLanguageCodes.addAll(allLocalesAsSet); allLanguageCodes.sort((o1, o2) -> getDisplayName(o1).compareTo(getDisplayName(o2))); return allLanguageCodes; } public static String getDefaultLanguage() { // might be set later in pref or config, so not use defaultLocale anywhere in the code return getLocale().getLanguage(); } public static String getDefaultLanguageLocaleAsCode() { return new Locale(LanguageUtil.getDefaultLanguage()).getLanguage(); } public static String getEnglishLanguageLocaleCode() { return new Locale(Locale.ENGLISH.getLanguage()).getLanguage(); } public static String getDisplayName(String code) { Locale locale = Locale.forLanguageTag(code); return locale.getDisplayName(locale); } public static boolean isDefaultLanguageRTL() { return rtlLanguagesCodes.contains(LanguageUtil.getDefaultLanguageLocaleAsCode()); } public static List getUserLanguageCodes() { return userLanguageCodes; } private static Locale getLocale() { return GlobalSettings.getLocale(); } } ================================================ FILE: core/src/main/java/haveno/core/locale/LocaleUtil.java ================================================ /* * 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 . */ package haveno.core.locale; import java.util.ArrayList; import java.util.List; import java.util.Locale; public class LocaleUtil { public static List getAllLocales() { // Data from https://restcountries.eu/rest/v2/all?fields=name;region;subregion;alpha2Code;languages List allLocales = new ArrayList<>(); allLocales.add(new Locale("ps", "AF")); // Afghanistan / lang=Pashto allLocales.add(new Locale("sv", "AX")); // Åland Islands / lang=Swedish allLocales.add(new Locale("sq", "AL")); // Albania / lang=Albanian allLocales.add(new Locale("ar", "DZ")); // Algeria / lang=Arabic allLocales.add(new Locale("en", "AS")); // American Samoa / lang=English allLocales.add(new Locale("ca", "AD")); // Andorra / lang=Catalan allLocales.add(new Locale("pt", "AO")); // Angola / lang=Portuguese allLocales.add(new Locale("en", "AI")); // Anguilla / lang=English allLocales.add(new Locale("en", "AG")); // Antigua and Barbuda / lang=English allLocales.add(new Locale("es", "AR")); // Argentina / lang=Spanish allLocales.add(new Locale("hy", "AM")); // Armenia / lang=Armenian allLocales.add(new Locale("nl", "AW")); // Aruba / lang=Dutch allLocales.add(new Locale("en", "AU")); // Australia / lang=English allLocales.add(new Locale("de", "AT")); // Austria / lang=German allLocales.add(new Locale("az", "AZ")); // Azerbaijan / lang=Azerbaijani allLocales.add(new Locale("en", "BS")); // Bahamas / lang=English allLocales.add(new Locale("ar", "BH")); // Bahrain / lang=Arabic allLocales.add(new Locale("bn", "BD")); // Bangladesh / lang=Bengali allLocales.add(new Locale("en", "BB")); // Barbados / lang=English allLocales.add(new Locale("be", "BY")); // Belarus / lang=Belarusian allLocales.add(new Locale("nl", "BE")); // Belgium / lang=Dutch allLocales.add(new Locale("en", "BZ")); // Belize / lang=English allLocales.add(new Locale("fr", "BJ")); // Benin / lang=French allLocales.add(new Locale("en", "BM")); // Bermuda / lang=English allLocales.add(new Locale("dz", "BT")); // Bhutan / lang=Dzongkha allLocales.add(new Locale("es", "BO")); // Bolivia (Plurinational State of) / lang=Spanish allLocales.add(new Locale("nl", "BQ")); // Bonaire, Sint Eustatius and Saba / lang=Dutch allLocales.add(new Locale("bs", "BA")); // Bosnia and Herzegovina / lang=Bosnian allLocales.add(new Locale("en", "BW")); // Botswana / lang=English allLocales.add(new Locale("pt", "BR")); // Brazil / lang=Portuguese allLocales.add(new Locale("en", "IO")); // British Indian Ocean Territory / lang=English allLocales.add(new Locale("en", "UM")); // United States Minor Outlying Islands / lang=English allLocales.add(new Locale("en", "VG")); // Virgin Islands (British) / lang=English allLocales.add(new Locale("en", "VI")); // Virgin Islands (U.S.) / lang=English allLocales.add(new Locale("ms", "BN")); // Brunei Darussalam / lang=Malay allLocales.add(new Locale("bg", "BG")); // Bulgaria / lang=Bulgarian allLocales.add(new Locale("fr", "BF")); // Burkina Faso / lang=French allLocales.add(new Locale("fr", "BI")); // Burundi / lang=French allLocales.add(new Locale("km", "KH")); // Cambodia / lang=Khmer allLocales.add(new Locale("en", "CM")); // Cameroon / lang=English allLocales.add(new Locale("en", "CA")); // Canada / lang=English allLocales.add(new Locale("pt", "CV")); // Cabo Verde / lang=Portuguese allLocales.add(new Locale("en", "KY")); // Cayman Islands / lang=English allLocales.add(new Locale("fr", "CF")); // Central African Republic / lang=French allLocales.add(new Locale("fr", "TD")); // Chad / lang=French allLocales.add(new Locale("es", "CL")); // Chile / lang=Spanish allLocales.add(new Locale("zh", "CN")); // China / lang=Chinese allLocales.add(new Locale("en", "CX")); // Christmas Island / lang=English allLocales.add(new Locale("en", "CC")); // Cocos (Keeling) Islands / lang=English allLocales.add(new Locale("es", "CO")); // Colombia / lang=Spanish allLocales.add(new Locale("ar", "KM")); // Comoros / lang=Arabic allLocales.add(new Locale("fr", "CG")); // Congo / lang=French allLocales.add(new Locale("fr", "CD")); // Congo (Democratic Republic of the) / lang=French allLocales.add(new Locale("en", "CK")); // Cook Islands / lang=English allLocales.add(new Locale("es", "CR")); // Costa Rica / lang=Spanish allLocales.add(new Locale("hr", "HR")); // Croatia / lang=Croatian allLocales.add(new Locale("es", "CU")); // Cuba / lang=Spanish allLocales.add(new Locale("nl", "CW")); // Curaçao / lang=Dutch allLocales.add(new Locale("el", "CY")); // Cyprus / lang=Greek (modern) allLocales.add(new Locale("cs", "CZ")); // Czech Republic / lang=Czech allLocales.add(new Locale("da", "DK")); // Denmark / lang=Danish allLocales.add(new Locale("fr", "DJ")); // Djibouti / lang=French allLocales.add(new Locale("en", "DM")); // Dominica / lang=English allLocales.add(new Locale("es", "DO")); // Dominican Republic / lang=Spanish allLocales.add(new Locale("es", "EC")); // Ecuador / lang=Spanish allLocales.add(new Locale("ar", "EG")); // Egypt / lang=Arabic allLocales.add(new Locale("es", "SV")); // El Salvador / lang=Spanish allLocales.add(new Locale("es", "GQ")); // Equatorial Guinea / lang=Spanish allLocales.add(new Locale("ti", "ER")); // Eritrea / lang=Tigrinya allLocales.add(new Locale("et", "EE")); // Estonia / lang=Estonian allLocales.add(new Locale("am", "ET")); // Ethiopia / lang=Amharic allLocales.add(new Locale("en", "FK")); // Falkland Islands (Malvinas) / lang=English allLocales.add(new Locale("fo", "FO")); // Faroe Islands / lang=Faroese allLocales.add(new Locale("en", "FJ")); // Fiji / lang=English allLocales.add(new Locale("fi", "FI")); // Finland / lang=Finnish allLocales.add(new Locale("fr", "FR")); // France / lang=French allLocales.add(new Locale("fr", "GF")); // French Guiana / lang=French allLocales.add(new Locale("fr", "PF")); // French Polynesia / lang=French allLocales.add(new Locale("fr", "TF")); // French Southern Territories / lang=French allLocales.add(new Locale("fr", "GA")); // Gabon / lang=French allLocales.add(new Locale("en", "GM")); // Gambia / lang=English allLocales.add(new Locale("ka", "GE")); // Georgia / lang=Georgian allLocales.add(new Locale("de", "DE")); // Germany / lang=German allLocales.add(new Locale("en", "GH")); // Ghana / lang=English allLocales.add(new Locale("en", "GI")); // Gibraltar / lang=English allLocales.add(new Locale("el", "GR")); // Greece / lang=Greek (modern) allLocales.add(new Locale("kl", "GL")); // Greenland / lang=Kalaallisut allLocales.add(new Locale("en", "GD")); // Grenada / lang=English allLocales.add(new Locale("fr", "GP")); // Guadeloupe / lang=French allLocales.add(new Locale("en", "GU")); // Guam / lang=English allLocales.add(new Locale("es", "GT")); // Guatemala / lang=Spanish allLocales.add(new Locale("en", "GG")); // Guernsey / lang=English allLocales.add(new Locale("fr", "GN")); // Guinea / lang=French allLocales.add(new Locale("pt", "GW")); // Guinea-Bissau / lang=Portuguese allLocales.add(new Locale("en", "GY")); // Guyana / lang=English allLocales.add(new Locale("fr", "HT")); // Haiti / lang=French allLocales.add(new Locale("la", "VA")); // Holy See / lang=Latin allLocales.add(new Locale("es", "HN")); // Honduras / lang=Spanish allLocales.add(new Locale("en", "HK")); // Hong Kong / lang=English allLocales.add(new Locale("hu", "HU")); // Hungary / lang=Hungarian allLocales.add(new Locale("is", "IS")); // Iceland / lang=Icelandic allLocales.add(new Locale("hi", "IN")); // India / lang=Hindi allLocales.add(new Locale("id", "ID")); // Indonesia / lang=Indonesian allLocales.add(new Locale("fr", "CI")); // Côte d'Ivoire / lang=French allLocales.add(new Locale("fa", "IR")); // Iran (Islamic Republic of) / lang=Persian (Farsi) allLocales.add(new Locale("ar", "IQ")); // Iraq / lang=Arabic allLocales.add(new Locale("ga", "IE")); // Ireland / lang=Irish allLocales.add(new Locale("en", "IM")); // Isle of Man / lang=English allLocales.add(new Locale("he", "IL")); // Israel / lang=Hebrew (modern) allLocales.add(new Locale("it", "IT")); // Italy / lang=Italian allLocales.add(new Locale("en", "JM")); // Jamaica / lang=English allLocales.add(new Locale("ja", "JP")); // Japan / lang=Japanese allLocales.add(new Locale("en", "JE")); // Jersey / lang=English allLocales.add(new Locale("ar", "JO")); // Jordan / lang=Arabic allLocales.add(new Locale("kk", "KZ")); // Kazakhstan / lang=Kazakh allLocales.add(new Locale("en", "KE")); // Kenya / lang=English allLocales.add(new Locale("en", "KI")); // Kiribati / lang=English allLocales.add(new Locale("ar", "KW")); // Kuwait / lang=Arabic allLocales.add(new Locale("ky", "KG")); // Kyrgyzstan / lang=Kyrgyz allLocales.add(new Locale("lo", "LA")); // Lao People's Democratic Republic / lang=Lao allLocales.add(new Locale("lv", "LV")); // Latvia / lang=Latvian allLocales.add(new Locale("ar", "LB")); // Lebanon / lang=Arabic allLocales.add(new Locale("en", "LS")); // Lesotho / lang=English allLocales.add(new Locale("en", "LR")); // Liberia / lang=English allLocales.add(new Locale("ar", "LY")); // Libya / lang=Arabic allLocales.add(new Locale("de", "LI")); // Liechtenstein / lang=German allLocales.add(new Locale("lt", "LT")); // Lithuania / lang=Lithuanian allLocales.add(new Locale("fr", "LU")); // Luxembourg / lang=French allLocales.add(new Locale("zh", "MO")); // Macao / lang=Chinese allLocales.add(new Locale("mk", "MK")); // Macedonia (the former Yugoslav Republic of) / lang=Macedonian allLocales.add(new Locale("fr", "MG")); // Madagascar / lang=French allLocales.add(new Locale("en", "MW")); // Malawi / lang=English allLocales.add(new Locale("en", "MY")); // Malaysia / lang=Malaysian allLocales.add(new Locale("dv", "MV")); // Maldives / lang=Divehi allLocales.add(new Locale("fr", "ML")); // Mali / lang=French allLocales.add(new Locale("mt", "MT")); // Malta / lang=Maltese allLocales.add(new Locale("en", "MH")); // Marshall Islands / lang=English allLocales.add(new Locale("fr", "MQ")); // Martinique / lang=French allLocales.add(new Locale("ar", "MR")); // Mauritania / lang=Arabic allLocales.add(new Locale("en", "MU")); // Mauritius / lang=English allLocales.add(new Locale("fr", "YT")); // Mayotte / lang=French allLocales.add(new Locale("es", "MX")); // Mexico / lang=Spanish allLocales.add(new Locale("en", "FM")); // Micronesia (Federated States of) / lang=English allLocales.add(new Locale("ro", "MD")); // Moldova (Republic of) / lang=Romanian allLocales.add(new Locale("fr", "MC")); // Monaco / lang=French allLocales.add(new Locale("mn", "MN")); // Mongolia / lang=Mongolian allLocales.add(new Locale("sr", "ME")); // Montenegro / lang=Serbian allLocales.add(new Locale("en", "MS")); // Montserrat / lang=English allLocales.add(new Locale("ar", "MA")); // Morocco / lang=Arabic allLocales.add(new Locale("pt", "MZ")); // Mozambique / lang=Portuguese allLocales.add(new Locale("my", "MM")); // Myanmar / lang=Burmese allLocales.add(new Locale("en", "NA")); // Namibia / lang=English allLocales.add(new Locale("en", "NR")); // Nauru / lang=English allLocales.add(new Locale("ne", "NP")); // Nepal / lang=Nepali allLocales.add(new Locale("nl", "NL")); // Netherlands / lang=Dutch allLocales.add(new Locale("fr", "NC")); // New Caledonia / lang=French allLocales.add(new Locale("en", "NZ")); // New Zealand / lang=English allLocales.add(new Locale("es", "NI")); // Nicaragua / lang=Spanish allLocales.add(new Locale("fr", "NE")); // Niger / lang=French allLocales.add(new Locale("en", "NG")); // Nigeria / lang=English allLocales.add(new Locale("en", "NU")); // Niue / lang=English allLocales.add(new Locale("en", "NF")); // Norfolk Island / lang=English allLocales.add(new Locale("ko", "KP")); // Korea (Democratic People's Republic of) / lang=Korean allLocales.add(new Locale("en", "MP")); // Northern Mariana Islands / lang=English allLocales.add(new Locale("no", "NO")); // Norway / lang=Norwegian allLocales.add(new Locale("ar", "OM")); // Oman / lang=Arabic allLocales.add(new Locale("en", "PK")); // Pakistan / lang=English allLocales.add(new Locale("en", "PW")); // Palau / lang=English allLocales.add(new Locale("ar", "PS")); // Palestine, State of / lang=Arabic allLocales.add(new Locale("es", "PA")); // Panama / lang=Spanish allLocales.add(new Locale("en", "PG")); // Papua New Guinea / lang=English allLocales.add(new Locale("es", "PY")); // Paraguay / lang=Spanish allLocales.add(new Locale("es", "PE")); // Peru / lang=Spanish allLocales.add(new Locale("en", "PH")); // Philippines / lang=English allLocales.add(new Locale("en", "PN")); // Pitcairn / lang=English allLocales.add(new Locale("pl", "PL")); // Poland / lang=Polish allLocales.add(new Locale("pt", "PT")); // Portugal / lang=Portuguese allLocales.add(new Locale("es", "PR")); // Puerto Rico / lang=Spanish allLocales.add(new Locale("ar", "QA")); // Qatar / lang=Arabic allLocales.add(new Locale("sq", "XK")); // Republic of Kosovo / lang=Albanian allLocales.add(new Locale("fr", "RE")); // Réunion / lang=French allLocales.add(new Locale("ro", "RO")); // Romania / lang=Romanian allLocales.add(new Locale("ru", "RU")); // Russian Federation / lang=Russian allLocales.add(new Locale("rw", "RW")); // Rwanda / lang=Kinyarwanda allLocales.add(new Locale("fr", "BL")); // Saint Barthélemy / lang=French allLocales.add(new Locale("en", "SH")); // Saint Helena, Ascension and Tristan da Cunha / lang=English allLocales.add(new Locale("en", "KN")); // Saint Kitts and Nevis / lang=English allLocales.add(new Locale("en", "LC")); // Saint Lucia / lang=English allLocales.add(new Locale("en", "MF")); // Saint Martin (French part) / lang=English allLocales.add(new Locale("fr", "PM")); // Saint Pierre and Miquelon / lang=French allLocales.add(new Locale("en", "VC")); // Saint Vincent and the Grenadines / lang=English allLocales.add(new Locale("sm", "WS")); // Samoa / lang=Samoan allLocales.add(new Locale("it", "SM")); // San Marino / lang=Italian allLocales.add(new Locale("pt", "ST")); // Sao Tome and Principe / lang=Portuguese allLocales.add(new Locale("ar", "SA")); // Saudi Arabia / lang=Arabic allLocales.add(new Locale("fr", "SN")); // Senegal / lang=French allLocales.add(new Locale("sr", "RS")); // Serbia / lang=Serbian allLocales.add(new Locale("fr", "SC")); // Seychelles / lang=French allLocales.add(new Locale("en", "SL")); // Sierra Leone / lang=English allLocales.add(new Locale("en", "SG")); // Singapore / lang=English allLocales.add(new Locale("nl", "SX")); // Sint Maarten (Dutch part) / lang=Dutch allLocales.add(new Locale("sk", "SK")); // Slovakia / lang=Slovak allLocales.add(new Locale("sl", "SI")); // Slovenia / lang=Slovene allLocales.add(new Locale("en", "SB")); // Solomon Islands / lang=English allLocales.add(new Locale("so", "SO")); // Somalia / lang=Somali allLocales.add(new Locale("af", "ZA")); // South Africa / lang=Afrikaans allLocales.add(new Locale("en", "GS")); // South Georgia and the South Sandwich Islands / lang=English allLocales.add(new Locale("ko", "KR")); // Korea (Republic of) / lang=Korean allLocales.add(new Locale("en", "SS")); // South Sudan / lang=English allLocales.add(new Locale("es", "ES")); // Spain / lang=Spanish allLocales.add(new Locale("si", "LK")); // Sri Lanka / lang=Sinhalese allLocales.add(new Locale("ar", "SD")); // Sudan / lang=Arabic allLocales.add(new Locale("nl", "SR")); // Suriname / lang=Dutch allLocales.add(new Locale("no", "SJ")); // Svalbard and Jan Mayen / lang=Norwegian allLocales.add(new Locale("en", "SZ")); // Swaziland / lang=English allLocales.add(new Locale("sv", "SE")); // Sweden / lang=Swedish allLocales.add(new Locale("de", "CH")); // Switzerland / lang=German allLocales.add(new Locale("ar", "SY")); // Syrian Arab Republic / lang=Arabic allLocales.add(new Locale("zh", "TW")); // Taiwan / lang=Chinese allLocales.add(new Locale("tg", "TJ")); // Tajikistan / lang=Tajik allLocales.add(new Locale("sw", "TZ")); // Tanzania, United Republic of / lang=Swahili allLocales.add(new Locale("th", "TH")); // Thailand / lang=Thai allLocales.add(new Locale("pt", "TL")); // Timor-Leste / lang=Portuguese allLocales.add(new Locale("fr", "TG")); // Togo / lang=French allLocales.add(new Locale("en", "TK")); // Tokelau / lang=English allLocales.add(new Locale("en", "TO")); // Tonga / lang=English allLocales.add(new Locale("en", "TT")); // Trinidad and Tobago / lang=English allLocales.add(new Locale("ar", "TN")); // Tunisia / lang=Arabic allLocales.add(new Locale("tr", "TR")); // Turkey / lang=Turkish allLocales.add(new Locale("tk", "TM")); // Turkmenistan / lang=Turkmen allLocales.add(new Locale("en", "TC")); // Turks and Caicos Islands / lang=English allLocales.add(new Locale("en", "TV")); // Tuvalu / lang=English allLocales.add(new Locale("en", "UG")); // Uganda / lang=English allLocales.add(new Locale("uk", "UA")); // Ukraine / lang=Ukrainian allLocales.add(new Locale("ar", "AE")); // United Arab Emirates / lang=Arabic allLocales.add(new Locale("en", "GB")); // United Kingdom of Great Britain and Northern Ireland / lang=English allLocales.add(new Locale("en", "US")); // United States of America / lang=English allLocales.add(new Locale("es", "UY")); // Uruguay / lang=Spanish allLocales.add(new Locale("uz", "UZ")); // Uzbekistan / lang=Uzbek allLocales.add(new Locale("bi", "VU")); // Vanuatu / lang=Bislama allLocales.add(new Locale("es", "VE")); // Venezuela (Bolivarian Republic of) / lang=Spanish allLocales.add(new Locale("vi", "VN")); // Viet Nam / lang=Vietnamese allLocales.add(new Locale("fr", "WF")); // Wallis and Futuna / lang=French allLocales.add(new Locale("es", "EH")); // Western Sahara / lang=Spanish allLocales.add(new Locale("ar", "YE")); // Yemen / lang=Arabic allLocales.add(new Locale("en", "ZM")); // Zambia / lang=English allLocales.add(new Locale("en", "ZW")); // Zimbabwe / lang=English return allLocales; } } ================================================ FILE: core/src/main/java/haveno/core/locale/Region.java ================================================ /* * 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 . */ package haveno.core.locale; import com.google.protobuf.Message; import haveno.common.proto.persistable.PersistablePayload; import lombok.EqualsAndHashCode; import lombok.ToString; import javax.annotation.concurrent.Immutable; @Immutable @EqualsAndHashCode @ToString public final class Region implements PersistablePayload { public final String code; public final String name; public Region(String code, String name) { this.code = code; this.name = name; } @Override public Message toProtoMessage() { return protobuf.Region.newBuilder().setCode(code).setName(name).build(); } public static Region fromProto(protobuf.Region proto) { return new Region(proto.getCode(), proto.getName()); } } ================================================ FILE: core/src/main/java/haveno/core/locale/Res.java ================================================ /* * 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 . */ package haveno.core.locale; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.net.URLConnection; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.MissingResourceException; import java.util.PropertyResourceBundle; import java.util.ResourceBundle; import static haveno.common.util.Utilities.toListOfWrappedStrings; import static java.nio.charset.StandardCharsets.UTF_8; @Slf4j public class Res { public static void setup() { BaseCurrencyNetwork baseCurrencyNetwork = Config.baseCurrencyNetwork(); setBaseCurrencyCode(baseCurrencyNetwork.getCurrencyCode()); setBaseCurrencyName(baseCurrencyNetwork.getCurrencyName()); } @SuppressWarnings("CanBeFinal") private static ResourceBundle resourceBundle = ResourceBundle.getBundle("i18n.displayStrings", GlobalSettings.getLocale(), new UTF8Control()); static { GlobalSettings.localeProperty().addListener((observable, oldValue, newValue) -> { if ("en".equalsIgnoreCase(newValue.getLanguage())) newValue = Locale.ROOT; resourceBundle = ResourceBundle.getBundle("i18n.displayStrings", newValue, new UTF8Control()); }); } public static String getWithCol(String key) { return get(key) + ":"; } public static String getWithColAndCap(String key) { return StringUtils.capitalize(get(key)) + ":"; } public static ResourceBundle getResourceBundle() { return resourceBundle; } private static String baseCurrencyCode; private static String baseCurrencyName; private static String baseCurrencyNameLowerCase; public static void setBaseCurrencyCode(String baseCurrencyCode) { Res.baseCurrencyCode = baseCurrencyCode; } public static void setBaseCurrencyName(String baseCurrencyName) { Res.baseCurrencyName = baseCurrencyName; baseCurrencyNameLowerCase = baseCurrencyName.toLowerCase(); } public static String getBaseCurrencyCode() { return baseCurrencyCode; } public static String getBaseCurrencyName() { return baseCurrencyName; } // Capitalize first character public static String getWithCap(String key) { return StringUtils.capitalize(get(key)); } public static String getWithCol(String key, Object... arguments) { return get(key, arguments) + ":"; } public static String get(String key, Object... arguments) { return MessageFormat.format(escapeQuotes(get(key)), arguments); } private static String escapeQuotes(String s) { return s.replace("'", "''"); } public static String get(String key) { try { return resourceBundle.getString(key) .replace("XMR", baseCurrencyCode) .replace("Monero", baseCurrencyName) .replace("monero", baseCurrencyNameLowerCase); } catch (MissingResourceException e) { log.warn("Missing resource for key: {}", key); if (DevEnv.isDevMode()) { e.printStackTrace(); UserThread.runAfter(() -> { // We delay a bit to not throw while UI is not ready throw new RuntimeException("Missing resource for key: " + key); }, 1); } return key; } } public static List getWrappedAsList(String key, int wrapLength) { String[] raw = get(key).split("\n"); List wrapped = new ArrayList<>(); for (String s : raw) { List list = toListOfWrappedStrings(s, wrapLength); for (String line : list) { if (!line.isEmpty()) wrapped.add(line); } } return wrapped; } } // Adds UTF8 support for property files class UTF8Control extends ResourceBundle.Control { public ResourceBundle newBundle(String baseName, @NotNull Locale locale, @NotNull String format, ClassLoader loader, boolean reload) throws IOException { // Below is a copy of the default implementation. final String bundleName = toBundleName(baseName, locale); final String resourceName = toResourceName(bundleName, "properties"); ResourceBundle bundle = null; InputStream stream = null; if (reload) { final URL url = loader.getResource(resourceName); if (url != null) { final URLConnection connection = url.openConnection(); if (connection != null) { connection.setUseCaches(false); stream = connection.getInputStream(); } } } else { stream = loader.getResourceAsStream(resourceName); } if (stream != null) { try { // Only this line is changed to make it read properties files as UTF-8. bundle = new PropertyResourceBundle(new InputStreamReader(stream, UTF_8)); } finally { stream.close(); } } return bundle; } } ================================================ FILE: core/src/main/java/haveno/core/locale/TradeCurrency.java ================================================ /* * 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 . */ package haveno.core.locale; import haveno.common.proto.ProtobufferRuntimeException; import haveno.common.proto.persistable.PersistablePayload; import lombok.Getter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; @ToString @Getter @Slf4j public abstract class TradeCurrency implements PersistablePayload, Comparable { protected final String code; protected final String name; public TradeCurrency(String code, String name) { this.code = code; this.name = name; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// public static TradeCurrency fromProto(protobuf.TradeCurrency proto) { switch (proto.getMessageCase()) { case TRADITIONAL_CURRENCY: return TraditionalCurrency.fromProto(proto); case CRYPTO_CURRENCY: return CryptoCurrency.fromProto(proto); default: throw new ProtobufferRuntimeException("Unknown message case: " + proto.getMessageCase()); } } public protobuf.TradeCurrency.Builder getTradeCurrencyBuilder() { return protobuf.TradeCurrency.newBuilder() .setCode(code) .setName(name); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public String getDisplayPrefix() { return ""; } public String getNameAndCode() { return name + " (" + code + ")"; } public String getCodeAndName() { return code + " (" + name + ")"; } @Override public int compareTo(@NotNull TradeCurrency other) { return this.name.compareTo(other.name); } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (obj == this) { return true; } if (obj instanceof TradeCurrency) { TradeCurrency other = (TradeCurrency) obj; return code.equals(other.code); } return false; } @Override public int hashCode() { return code.hashCode(); } } ================================================ FILE: core/src/main/java/haveno/core/locale/TraditionalCurrency.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.locale; import com.google.protobuf.Message; import lombok.Getter; import lombok.ToString; import java.util.Currency; import java.util.Locale; import java.util.Optional; @ToString @Getter public final class TraditionalCurrency extends TradeCurrency { // http://boschista.deviantart.com/journal/Cool-ASCII-Symbols-214218618 private final static String PREFIX = "★ "; public TraditionalCurrency(String currencyCode) { this(Currency.getInstance(currencyCode), getLocale()); } public TraditionalCurrency(String currencyCode, String name) { super(currencyCode, name); } public TraditionalCurrency(TraditionalCurrency currency) { this(currency.getCode(), currency.getName()); } @SuppressWarnings("WeakerAccess") public TraditionalCurrency(Currency currency) { this(currency, getLocale()); } @SuppressWarnings("WeakerAccess") public TraditionalCurrency(Currency currency, Locale locale) { super(currency.getCurrencyCode(), currency.getDisplayName(locale)); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public Message toProtoMessage() { return getTradeCurrencyBuilder() .setCode(code) .setName(name) .setTraditionalCurrency(protobuf.TraditionalCurrency.newBuilder()) .build(); } public static TraditionalCurrency fromProto(protobuf.TradeCurrency proto) { Optional currency = CurrencyUtil.getTradeCurrency(proto.getCode()); if (currency.isPresent() && currency.get() instanceof TraditionalCurrency) { return new TraditionalCurrency(proto.getCode(), CurrencyUtil.getNameByCode(proto.getCode())); } else { return null; } } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// private static Locale getLocale() { return GlobalSettings.getLocale(); } @Override public String getDisplayPrefix() { return PREFIX; } } ================================================ FILE: core/src/main/java/haveno/core/monetary/CryptoExchangeRate.java ================================================ /* * 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 . */ package haveno.core.monetary; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.Coin; import java.math.BigInteger; import static com.google.common.base.Preconditions.checkArgument; // Cloned from ExchangeRate. Use Crypto instead of Fiat. @Slf4j public class CryptoExchangeRate { /** * An exchange rate is expressed as a ratio of a {@link Coin} and a {@link CryptoMoney} amount. */ public final Coin coin; public final CryptoMoney cryptoMoney; /** * Construct exchange rate. This amount of coin is worth that amount of crypto. */ @SuppressWarnings("SameParameterValue") public CryptoExchangeRate(Coin coin, CryptoMoney crypto) { checkArgument(coin.isPositive()); checkArgument(crypto.isPositive()); checkArgument(crypto.currencyCode != null, "currency code required"); this.coin = coin; this.cryptoMoney = crypto; } /** * Construct exchange rate. One coin is worth this amount of crypto. */ public CryptoExchangeRate(CryptoMoney crypto) { this(Coin.COIN, crypto); } /** * Convert a coin amount to an crypto amount using this exchange rate. * * @throws ArithmeticException if the converted crypto amount is too high or too low. */ public CryptoMoney coinToCrypto(Coin convertCoin) { final BigInteger converted = BigInteger.valueOf(convertCoin.value) .multiply(BigInteger.valueOf(cryptoMoney.value)) .divide(BigInteger.valueOf(coin.value)); if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 || converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0) throw new ArithmeticException("Overflow"); return CryptoMoney.valueOf(cryptoMoney.currencyCode, converted.longValue()); } /** * Convert a crypto amount to a coin amount using this exchange rate. * * @throws ArithmeticException if the converted coin amount is too high or too low. */ public Coin cryptoToCoin(CryptoMoney convertCrypto) { checkArgument(convertCrypto.currencyCode.equals(cryptoMoney.currencyCode), "Currency mismatch: %s vs %s", convertCrypto.currencyCode, cryptoMoney.currencyCode); // Use BigInteger because it's much easier to maintain full precision without overflowing. final BigInteger converted = BigInteger.valueOf(convertCrypto.value).multiply(BigInteger.valueOf(coin.value)) .divide(BigInteger.valueOf(cryptoMoney.value)); if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 || converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0) throw new ArithmeticException("Overflow"); try { return Coin.valueOf(converted.longValue()); } catch (IllegalArgumentException x) { throw new ArithmeticException("Overflow: " + x.getMessage()); } } } ================================================ FILE: core/src/main/java/haveno/core/monetary/CryptoMoney.java ================================================ /* * 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 . */ package haveno.core.monetary; import com.google.common.math.LongMath; import haveno.core.util.ParsingUtils; import org.bitcoinj.core.Monetary; import org.bitcoinj.utils.MonetaryFormat; import org.jetbrains.annotations.NotNull; import java.math.BigDecimal; import static com.google.common.base.Preconditions.checkArgument; /** * Cloned from Fiat class and altered SMALLEST_UNIT_EXPONENT as Fiat is final. *

    * Represents a monetary crypto value. It was decided to not fold this into {@link org.bitcoinj.core.Coin} because of type * safety. Volume values always come with an attached currency code. *

    * This class is immutable. */ public final class CryptoMoney implements Monetary, Comparable { /** * The absolute value of exponent of the value of a "smallest unit" in scientific notation. We picked 4 rather than * 2, because in financial applications it's common to use sub-cent precision. */ public static final int SMALLEST_UNIT_EXPONENT = 8; private static final MonetaryFormat FRIENDLY_FORMAT = new MonetaryFormat().shift(0).minDecimals(2).repeatOptionalDecimals(2, 1).postfixCode(); private static final MonetaryFormat PLAIN_FORMAT = new MonetaryFormat().shift(0).minDecimals(0).repeatOptionalDecimals(1, 8).noCode(); /** * The number of smallest units of this monetary value. */ public final long value; public final String currencyCode; private CryptoMoney(final String currencyCode, final long value) { this.value = value; this.currencyCode = currencyCode; } public static CryptoMoney valueOf(final String currencyCode, final long value) { return new CryptoMoney(currencyCode, value); } @Override public int smallestUnitExponent() { return SMALLEST_UNIT_EXPONENT; } /** * Returns the number of "smallest units" of this monetary value. */ @Override public long getValue() { return value; } public String getCurrencyCode() { return currencyCode; } /** * Parses an amount expressed in the way humans are used to. *

    *

    * This takes string in a format understood by {@link BigDecimal#BigDecimal(String)}, for example "0", "1", "0.10", * "1.23E3", "1234.5E-5". * * @throws IllegalArgumentException if you try to specify fractional satoshis, or a value out of range. */ public static CryptoMoney parseCrypto(final String currencyCode, String input) { String cleaned = ParsingUtils.convertCharsForNumber(input); try { long val = new BigDecimal(cleaned).movePointRight(SMALLEST_UNIT_EXPONENT) .toBigIntegerExact().longValue(); return CryptoMoney.valueOf(currencyCode, val); } catch (ArithmeticException e) { throw new IllegalArgumentException(e); } } public CryptoMoney add(final CryptoMoney value) { checkArgument(value.currencyCode.equals(currencyCode)); return new CryptoMoney(currencyCode, LongMath.checkedAdd(this.value, value.value)); } public CryptoMoney subtract(final CryptoMoney value) { checkArgument(value.currencyCode.equals(currencyCode)); return new CryptoMoney(currencyCode, LongMath.checkedSubtract(this.value, value.value)); } public CryptoMoney multiply(final long factor) { return new CryptoMoney(currencyCode, LongMath.checkedMultiply(this.value, factor)); } public CryptoMoney divide(final long divisor) { return new CryptoMoney(currencyCode, this.value / divisor); } public CryptoMoney[] divideAndRemainder(final long divisor) { return new CryptoMoney[]{new CryptoMoney(currencyCode, this.value / divisor), new CryptoMoney(currencyCode, this.value % divisor)}; } public long divide(final CryptoMoney divisor) { checkArgument(divisor.currencyCode.equals(currencyCode)); return this.value / divisor.value; } /** * Returns true if and only if this instance represents a monetary value greater than zero, otherwise false. */ public boolean isPositive() { return signum() == 1; } /** * Returns true if and only if this instance represents a monetary value less than zero, otherwise false. */ public boolean isNegative() { return signum() == -1; } /** * Returns true if and only if this instance represents zero monetary value, otherwise false. */ public boolean isZero() { return signum() == 0; } /** * Returns true if the monetary value represented by this instance is greater than that of the given other Coin, * otherwise false. */ public boolean isGreaterThan(CryptoMoney other) { return compareTo(other) > 0; } /** * Returns true if the monetary value represented by this instance is less than that of the given other Coin, * otherwise false. */ public boolean isLessThan(CryptoMoney other) { return compareTo(other) < 0; } @Override public int signum() { if (this.value == 0) return 0; return this.value < 0 ? -1 : 1; } public CryptoMoney negate() { return new CryptoMoney(currencyCode, -this.value); } public String toFriendlyString() { return FRIENDLY_FORMAT.code(0, currencyCode).format(this).toString(); } /** *

    * Returns the value as a plain string denominated in BTC. The result is unformatted with no trailing zeroes. For * instance, a value of 150000 satoshis gives an output string of "0.0015" BTC *

    */ public String toPlainString() { return PLAIN_FORMAT.format(this).toString(); } @Override public String toString() { return toPlainString(); } @Override public boolean equals(final Object o) { if (o == this) return true; if (o == null || o.getClass() != getClass()) return false; final CryptoMoney other = (CryptoMoney) o; return this.value == other.value && this.currencyCode.equals(other.currencyCode); } @Override public int hashCode() { return (int) this.value + 37 * this.currencyCode.hashCode(); } @Override public int compareTo(@NotNull final CryptoMoney other) { if (!this.currencyCode.equals(other.currencyCode)) return this.currencyCode.compareTo(other.currencyCode); if (this.value != other.value) return this.value > other.value ? 1 : -1; return 0; } } ================================================ FILE: core/src/main/java/haveno/core/monetary/MonetaryWrapper.java ================================================ /* * 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 . */ package haveno.core.monetary; import org.bitcoinj.core.Monetary; import org.bitcoinj.utils.MonetaryFormat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public abstract class MonetaryWrapper { private static final Logger log = LoggerFactory.getLogger(MonetaryWrapper.class); /// Instance of TraditionalMoney or CryptoMoney protected final Monetary monetary; protected final MonetaryFormat traditionalFormat = MonetaryFormat.FIAT.repeatOptionalDecimals(0, 0); protected final MonetaryFormat cryptoFormat = MonetaryFormat.FIAT.repeatOptionalDecimals(0, 0); public MonetaryWrapper(Monetary monetary) { this.monetary = monetary; } public Monetary getMonetary() { return monetary; } public boolean isZero() { return monetary.getValue() == 0; } public int smallestUnitExponent() { return monetary.smallestUnitExponent(); } public long getValue() { return monetary.getValue(); } @Override public boolean equals(final Object o) { if (o == this) return true; if (o == null || o.getClass() != getClass()) return false; final Monetary otherMonetary = ((MonetaryWrapper) o).getMonetary(); return monetary.getValue() == otherMonetary.getValue(); } @Override public int hashCode() { return (int) monetary.getValue(); } } ================================================ FILE: core/src/main/java/haveno/core/monetary/Price.java ================================================ /* * 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 . */ package haveno.core.monetary; import haveno.core.locale.CurrencyUtil; import haveno.core.trade.HavenoUtils; import haveno.core.util.ParsingUtils; import org.bitcoinj.core.Monetary; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.math.BigDecimal; import java.math.BigInteger; /** * Monero price value with variable precision. *

    *
    * We wrap an object implementing the {@link Monetary} interface from bitcoinj. We respect the * number of decimal digits of precision specified in the {@code smallestUnitExponent()}, defined in * those classes, like {@link TraditionalMoney} or {@link CryptoMoney}. */ public class Price extends MonetaryWrapper implements Comparable { private static final Logger log = LoggerFactory.getLogger(Price.class); /** * Create a new {@code Price} from specified {@code Monetary}. * * @param monetary */ public Price(Monetary monetary) { super(monetary); } /** * Parse the Monero {@code Price} given a {@code currencyCode} and {@code inputValue}. * * @param currencyCode The currency code to parse, e.g "USD" or "LTC". * @param input The input value to parse as a String, e.g "2.54" or "-0.0001". * @return The parsed Price. */ public static Price parse(String currencyCode, String input) { String cleaned = ParsingUtils.convertCharsForNumber(input); if (CurrencyUtil.isTraditionalCurrency(currencyCode)) return new Price(TraditionalMoney.parseTraditionalMoney(currencyCode, cleaned)); else return new Price(CryptoMoney.parseCrypto(currencyCode, cleaned)); } /** * Parse the Monero {@code Price} given a {@code currencyCode} and {@code inputValue}. * * @param currencyCode The currency code to parse, e.g "USD" or "LTC". * @param value The value to parse. * @return The parsed Price. */ public static Price valueOf(String currencyCode, long value) { if (CurrencyUtil.isTraditionalCurrency(currencyCode)) { return new Price(TraditionalMoney.valueOf(currencyCode, value)); } else { return new Price(CryptoMoney.valueOf(currencyCode, value)); } } public Volume getVolumeByAmount(BigInteger amount) { if (monetary instanceof TraditionalMoney) return new Volume(new TraditionalExchangeRate((TraditionalMoney) monetary).coinToTraditionalMoney(HavenoUtils.atomicUnitsToCoin(amount))); else if (monetary instanceof CryptoMoney) return new Volume(new CryptoExchangeRate((CryptoMoney) monetary).coinToCrypto(HavenoUtils.atomicUnitsToCoin(amount))); else throw new IllegalStateException("Monetary must be either of type TraditionalMoney or CryptoMoney"); } public BigInteger getAmountByVolume(Volume volume) { Monetary monetary = volume.getMonetary(); if (monetary instanceof TraditionalMoney && this.monetary instanceof TraditionalMoney) return HavenoUtils.coinToAtomicUnits(new TraditionalExchangeRate((TraditionalMoney) this.monetary).traditionalMoneyToCoin((TraditionalMoney) monetary)); else if (monetary instanceof CryptoMoney && this.monetary instanceof CryptoMoney) return HavenoUtils.coinToAtomicUnits(new CryptoExchangeRate((CryptoMoney) this.monetary).cryptoToCoin((CryptoMoney) monetary)); else return BigInteger.ZERO; } public String getCurrencyCode() { return monetary instanceof CryptoMoney ? ((CryptoMoney) monetary).getCurrencyCode() : ((TraditionalMoney) monetary).getCurrencyCode(); } @Override public long getValue() { return monetary.getValue(); } /** * Get the amount of whole coins or units as double. */ public double getDoubleValue() { return BigDecimal.valueOf(monetary.getValue()).movePointLeft(monetary.smallestUnitExponent()).doubleValue(); } @Override public int compareTo(@NotNull Price other) { if (!this.getCurrencyCode().equals(other.getCurrencyCode())) return this.getCurrencyCode().compareTo(other.getCurrencyCode()); if (this.getValue() != other.getValue()) return this.getValue() > other.getValue() ? 1 : -1; return 0; } public boolean isPositive() { return monetary instanceof CryptoMoney ? ((CryptoMoney) monetary).isPositive() : ((TraditionalMoney) monetary).isPositive(); } public Price subtract(Price other) { if (monetary instanceof CryptoMoney) { return new Price(((CryptoMoney) monetary).subtract((CryptoMoney) other.monetary)); } else { return new Price(((TraditionalMoney) monetary).subtract((TraditionalMoney) other.monetary)); } } public String toFriendlyString() { return monetary instanceof CryptoMoney ? ((CryptoMoney) monetary).toFriendlyString().replace(((CryptoMoney) monetary).currencyCode, "") + "XMR/" + ((CryptoMoney) monetary).currencyCode : ((TraditionalMoney) monetary).toFriendlyString().replace(((TraditionalMoney) monetary).currencyCode, "") + "XMR/" + ((TraditionalMoney) monetary).currencyCode; } public String toPlainString() { return monetary instanceof CryptoMoney ? ((CryptoMoney) monetary).toPlainString() : ((TraditionalMoney) monetary).toPlainString(); } @Override public String toString() { return toPlainString(); } } ================================================ FILE: core/src/main/java/haveno/core/monetary/TraditionalExchangeRate.java ================================================ /* * 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 . */ package haveno.core.monetary; import static com.google.common.base.Preconditions.checkArgument; import java.io.Serializable; import java.math.BigInteger; import org.bitcoinj.core.Coin; import com.google.common.base.Objects; /** * An exchange rate is expressed as a ratio of a {@link Coin} and a traditional money amount. */ public class TraditionalExchangeRate implements Serializable { public final Coin coin; public final TraditionalMoney traditionalMoney; /** Construct exchange rate. This amount of coin is worth that amount of money. */ public TraditionalExchangeRate(Coin coin, TraditionalMoney traditionalMoney) { checkArgument(coin.isPositive()); checkArgument(traditionalMoney.isPositive()); checkArgument(traditionalMoney.currencyCode != null, "currency code required"); this.coin = coin; this.traditionalMoney = traditionalMoney; } /** Construct exchange rate. One coin is worth this amount of traditional money. */ public TraditionalExchangeRate(TraditionalMoney traditionalMoney) { this(Coin.COIN, traditionalMoney); } /** * Convert a coin amount to a traditional money amount using this exchange rate. * @throws ArithmeticException if the converted amount is too high or too low. */ public TraditionalMoney coinToTraditionalMoney(Coin convertCoin) { // Use BigInteger because it's much easier to maintain full precision without overflowing. final BigInteger converted = BigInteger.valueOf(convertCoin.value) .multiply(BigInteger.valueOf(traditionalMoney.value)) .divide(BigInteger.valueOf(coin.value)); if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 || converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0) throw new ArithmeticException("Overflow"); return TraditionalMoney.valueOf(traditionalMoney.currencyCode, converted.longValue()); } /** * Convert a traditional money amount to a coin amount using this exchange rate. * @throws ArithmeticException if the converted coin amount is too high or too low. */ public Coin traditionalMoneyToCoin(TraditionalMoney convertTraditionalMoney) { checkArgument(convertTraditionalMoney.currencyCode.equals(traditionalMoney.currencyCode), "Currency mismatch: %s vs %s", convertTraditionalMoney.currencyCode, traditionalMoney.currencyCode); // Use BigInteger because it's much easier to maintain full precision without overflowing. final BigInteger converted = BigInteger.valueOf(convertTraditionalMoney.value).multiply(BigInteger.valueOf(coin.value)) .divide(BigInteger.valueOf(traditionalMoney.value)); if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 || converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0) throw new ArithmeticException("Overflow"); try { return Coin.valueOf(converted.longValue()); } catch (IllegalArgumentException x) { throw new ArithmeticException("Overflow: " + x.getMessage()); } } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; TraditionalExchangeRate other = (TraditionalExchangeRate) o; return Objects.equal(this.coin, other.coin) && Objects.equal(this.traditionalMoney, other.traditionalMoney); } @Override public int hashCode() { return Objects.hashCode(coin, traditionalMoney); } } ================================================ FILE: core/src/main/java/haveno/core/monetary/TraditionalMoney.java ================================================ /* * Copyright 2014 Andreas Schildbach * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package haveno.core.monetary; import static com.google.common.base.Preconditions.checkArgument; import java.io.Serializable; import java.math.BigDecimal; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Monetary; import org.bitcoinj.utils.MonetaryFormat; import com.google.common.base.Objects; import com.google.common.math.LongMath; import com.google.common.primitives.Longs; /** * Represents a monetary value. It was decided to not fold this into * {@link Coin} because of type * safety. Traditional money values always come with an attached currency code. * * This class is immutable. */ public final class TraditionalMoney implements Monetary, Comparable, Serializable { /** * The absolute value of exponent of the value of a "smallest unit" in * scientific notation. We picked 8 rather than * 2, because in financial applications it's common to use sub-cent precision. */ public static final int SMALLEST_UNIT_EXPONENT = 8; /** * The number of smallest units of this monetary value. */ public final long value; public final String currencyCode; private TraditionalMoney(final String currencyCode, final long value) { this.value = value; this.currencyCode = currencyCode; } public static TraditionalMoney valueOf(final String currencyCode, final long value) { return new TraditionalMoney(currencyCode, value); } @Override public int smallestUnitExponent() { return SMALLEST_UNIT_EXPONENT; } /** * Returns the number of "smallest units" of this monetary value. */ @Override public long getValue() { return value; } public String getCurrencyCode() { return currencyCode; } /** *

    Parses an amount expressed in the way humans are used to.

    *

    This takes string in a format understood by {@link BigDecimal#BigDecimal(String)}, for example "0", "1", "0.10", * "1.23E3", "1234.5E-5".

    * * @throws IllegalArgumentException * if you try to specify more than 8 digits after the comma, or a value out of range. */ public static TraditionalMoney parseTraditionalMoney(final String currencyCode, final String str) { try { long val = new BigDecimal(str).movePointRight(SMALLEST_UNIT_EXPONENT).longValueExact(); return TraditionalMoney.valueOf(currencyCode, val); } catch (ArithmeticException e) { throw new IllegalArgumentException(e); } } /** *

    Parses an amount expressed in the way humans are used to. The amount is cut to 8 digits after the comma.

    *

    This takes string in a format understood by {@link BigDecimal#BigDecimal(String)}, for example "0", "1", "0.10", * "1.23E3", "1234.5E-5".

    * * @throws IllegalArgumentException * if you try to specify a value out of range. */ public static TraditionalMoney parseTraditionalMoneyInexact(final String currencyCode, final String str) { try { long val = new BigDecimal(str).movePointRight(SMALLEST_UNIT_EXPONENT).longValue(); return TraditionalMoney.valueOf(currencyCode, val); } catch (ArithmeticException e) { throw new IllegalArgumentException(e); } } public TraditionalMoney add(final TraditionalMoney value) { checkArgument(value.currencyCode.equals(currencyCode)); return new TraditionalMoney(currencyCode, LongMath.checkedAdd(this.value, value.value)); } public TraditionalMoney subtract(final TraditionalMoney value) { checkArgument(value.currencyCode.equals(currencyCode)); return new TraditionalMoney(currencyCode, LongMath.checkedSubtract(this.value, value.value)); } public TraditionalMoney multiply(final long factor) { return new TraditionalMoney(currencyCode, LongMath.checkedMultiply(this.value, factor)); } public TraditionalMoney divide(final long divisor) { return new TraditionalMoney(currencyCode, this.value / divisor); } public TraditionalMoney[] divideAndRemainder(final long divisor) { return new TraditionalMoney[] { new TraditionalMoney(currencyCode, this.value / divisor), new TraditionalMoney(currencyCode, this.value % divisor) }; } public long divide(final TraditionalMoney divisor) { checkArgument(divisor.currencyCode.equals(currencyCode)); return this.value / divisor.value; } /** * Returns true if and only if this instance represents a monetary value greater than zero, otherwise false. */ public boolean isPositive() { return signum() == 1; } /** * Returns true if and only if this instance represents a monetary value less than zero, otherwise false. */ public boolean isNegative() { return signum() == -1; } /** * Returns true if and only if this instance represents zero monetary value, otherwise false. */ public boolean isZero() { return signum() == 0; } /** * Returns true if the monetary value represented by this instance is greater than that of the given other TraditionalMoney, * otherwise false. */ public boolean isGreaterThan(TraditionalMoney other) { return compareTo(other) > 0; } /** * Returns true if the monetary value represented by this instance is less than that of the given other TraditionalMoney, * otherwise false. */ public boolean isLessThan(TraditionalMoney other) { return compareTo(other) < 0; } @Override public int signum() { if (this.value == 0) return 0; return this.value < 0 ? -1 : 1; } public TraditionalMoney negate() { return new TraditionalMoney(currencyCode, -this.value); } /** * Returns the number of "smallest units" of this monetary value. It's deprecated in favour of accessing {@link #value} * directly. */ public long longValue() { return this.value; } private static final MonetaryFormat FRIENDLY_FORMAT = MonetaryFormat.FIAT.postfixCode(); /** * Returns the value as a 0.12 type string. More digits after the decimal place will be used if necessary, but two * will always be present. */ public String toFriendlyString() { return FRIENDLY_FORMAT.code(0, currencyCode).format(this).toString(); } private static final MonetaryFormat PLAIN_FORMAT = MonetaryFormat.FIAT.minDecimals(0).repeatOptionalDecimals(1, TraditionalMoney.SMALLEST_UNIT_EXPONENT).noCode(); /** *

    * Returns the value as a plain string. The result is unformatted with no trailing zeroes. For * instance, a value of 150000 "smallest units" gives an output string of "0.0015". *

    */ public String toPlainString() { return PLAIN_FORMAT.format(this).toString(); } @Override public String toString() { return Long.toString(value); } @Override public boolean equals(final Object o) { if (o == this) return true; if (o == null || o.getClass() != getClass()) return false; final TraditionalMoney other = (TraditionalMoney) o; return this.value == other.value && this.currencyCode.equals(other.currencyCode); } @Override public int hashCode() { return Objects.hashCode(value, currencyCode); } @Override public int compareTo(final TraditionalMoney other) { if (!this.currencyCode.equals(other.currencyCode)) return this.currencyCode.compareTo(other.currencyCode); return Longs.compare(this.value, other.value); } } ================================================ FILE: core/src/main/java/haveno/core/monetary/Volume.java ================================================ /* * 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 . */ package haveno.core.monetary; import haveno.core.locale.CurrencyUtil; import haveno.core.util.ParsingUtils; import org.bitcoinj.core.Monetary; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Volume extends MonetaryWrapper implements Comparable { private static final Logger log = LoggerFactory.getLogger(Volume.class); public Volume(Monetary monetary) { super(monetary); } public static Volume parse(String input, String currencyCode) { String cleaned = ParsingUtils.convertCharsForNumber(input); if (CurrencyUtil.isTraditionalCurrency(currencyCode)) return new Volume(TraditionalMoney.parseTraditionalMoney(currencyCode, cleaned)); else return new Volume(CryptoMoney.parseCrypto(currencyCode, cleaned)); } @Override public int compareTo(@NotNull Volume other) { if (!this.getCurrencyCode().equals(other.getCurrencyCode())) return this.getCurrencyCode().compareTo(other.getCurrencyCode()); if (this.getValue() != other.getValue()) return this.getValue() > other.getValue() ? 1 : -1; return 0; } public String getCurrencyCode() { return monetary instanceof CryptoMoney ? ((CryptoMoney) monetary).getCurrencyCode() : ((TraditionalMoney) monetary).getCurrencyCode(); } public String toPlainString() { return monetary instanceof CryptoMoney ? ((CryptoMoney) monetary).toPlainString() : ((TraditionalMoney) monetary).toPlainString(); } @Override public String toString() { return toPlainString(); } } ================================================ FILE: core/src/main/java/haveno/core/network/CoreBanFilter.java ================================================ /* * 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 . */ package haveno.core.network; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.network.BanFilter; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.function.Predicate; import lombok.extern.slf4j.Slf4j; @Slf4j public class CoreBanFilter implements BanFilter { private final Set bannedPeersFromOptions = new HashSet<>(); private Predicate bannedNodePredicate; /** * @param banList List of banned peers from program argument */ @Inject public CoreBanFilter(@Named(Config.BAN_LIST) List banList) { banList.stream().map(NodeAddress::new).forEach(bannedPeersFromOptions::add); } @Override public void setBannedNodePredicate(Predicate bannedNodePredicate) { this.bannedNodePredicate = bannedNodePredicate; } @Override public boolean isPeerBanned(NodeAddress nodeAddress) { return bannedPeersFromOptions.contains(nodeAddress) || bannedNodePredicate != null && bannedNodePredicate.test(nodeAddress); } } ================================================ FILE: core/src/main/java/haveno/core/network/MessageState.java ================================================ /* * 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 . */ package haveno.core.network; public enum MessageState { UNDEFINED, SENT, ARRIVED, STORED_IN_MAILBOX, ACKNOWLEDGED, FAILED, NACKED } ================================================ FILE: core/src/main/java/haveno/core/network/p2p/inventory/GetInventoryRequestHandler.java ================================================ /* * 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 . */ package haveno.core.network.p2p.inventory; import com.google.common.base.Enums; import com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.app.Version; import haveno.common.config.Config; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.util.Profiler; import haveno.core.filter.Filter; import haveno.core.filter.FilterManager; import haveno.core.network.p2p.inventory.messages.GetInventoryRequest; import haveno.core.network.p2p.inventory.messages.GetInventoryResponse; import haveno.core.network.p2p.inventory.model.InventoryItem; import haveno.core.network.p2p.inventory.model.RequestInfo; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.MessageListener; import haveno.network.p2p.network.NetworkNode; import haveno.network.p2p.network.Statistic; import haveno.network.p2p.peers.PeerManager; import haveno.network.p2p.storage.P2PDataStorage; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import java.lang.management.ManagementFactory; import java.util.HashMap; import java.util.Map; import lombok.extern.slf4j.Slf4j; @Slf4j public class GetInventoryRequestHandler implements MessageListener { private final NetworkNode networkNode; private final PeerManager peerManager; private final P2PDataStorage p2PDataStorage; private final FilterManager filterManager; private final int maxConnections; @Inject public GetInventoryRequestHandler(NetworkNode networkNode, PeerManager peerManager, P2PDataStorage p2PDataStorage, FilterManager filterManager, @Named(Config.MAX_CONNECTIONS) int maxConnections) { this.networkNode = networkNode; this.peerManager = peerManager; this.p2PDataStorage = p2PDataStorage; this.filterManager = filterManager; this.maxConnections = maxConnections; this.networkNode.addMessageListener(this); } @Override public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { if (networkEnvelope instanceof GetInventoryRequest) { // Data GetInventoryRequest getInventoryRequest = (GetInventoryRequest) networkEnvelope; Map dataObjects = new HashMap<>(); p2PDataStorage.getMapForDataResponse(getInventoryRequest.getVersion()).values().stream() .map(e -> e.getClass().getSimpleName()) .forEach(className -> addClassNameToMap(dataObjects, className)); p2PDataStorage.getMap().values().stream() .map(ProtectedStorageEntry::getProtectedStoragePayload) .map(e -> e.getClass().getSimpleName()) .forEach(className -> addClassNameToMap(dataObjects, className)); Map inventory = new HashMap<>(); dataObjects.forEach((key, value) -> inventory.put(key, String.valueOf(value))); // network inventory.put(InventoryItem.maxConnections, String.valueOf(maxConnections)); inventory.put(InventoryItem.numConnections, String.valueOf(networkNode.getAllConnections().size())); inventory.put(InventoryItem.peakNumConnections, String.valueOf(peerManager.getPeakNumConnections())); inventory.put(InventoryItem.numAllConnectionsLostEvents, String.valueOf(peerManager.getNumAllConnectionsLostEvents())); peerManager.maybeResetNumAllConnectionsLostEvents(); inventory.put(InventoryItem.sentBytes, String.valueOf(Statistic.totalSentBytesProperty().get())); inventory.put(InventoryItem.sentBytesPerSec, String.valueOf(Statistic.totalSentBytesPerSecProperty().get())); inventory.put(InventoryItem.receivedBytes, String.valueOf(Statistic.totalReceivedBytesProperty().get())); inventory.put(InventoryItem.receivedBytesPerSec, String.valueOf(Statistic.totalReceivedBytesPerSecProperty().get())); inventory.put(InventoryItem.receivedMessagesPerSec, String.valueOf(Statistic.numTotalReceivedMessagesPerSecProperty().get())); inventory.put(InventoryItem.sentMessagesPerSec, String.valueOf(Statistic.numTotalSentMessagesPerSecProperty().get())); // node inventory.put(InventoryItem.version, Version.VERSION); inventory.put(InventoryItem.commitHash, RequestInfo.COMMIT_HASH); inventory.put(InventoryItem.usedMemory, String.valueOf(Profiler.getUsedMemoryInBytes())); inventory.put(InventoryItem.jvmStartTime, String.valueOf(ManagementFactory.getRuntimeMXBean().getStartTime())); Filter filter = filterManager.getFilter(); if (filter != null) { inventory.put(InventoryItem.filteredSeeds, Joiner.on("," + System.getProperty("line.separator")).join(filter.getSeedNodes())); } log.info("Send inventory {} to {}", inventory, connection.getPeersNodeAddressOptional()); GetInventoryResponse getInventoryResponse = new GetInventoryResponse(inventory); networkNode.sendMessage(connection, getInventoryResponse); } } public void shutDown() { networkNode.removeMessageListener(this); } private void addClassNameToMap(Map dataObjects, String className) { Optional optionalEnum = Enums.getIfPresent(InventoryItem.class, className); if (optionalEnum.isPresent()) { InventoryItem key = optionalEnum.get(); dataObjects.putIfAbsent(key, 0); int prev = dataObjects.get(key); dataObjects.put(key, prev + 1); } } } ================================================ FILE: core/src/main/java/haveno/core/network/p2p/inventory/GetInventoryRequestManager.java ================================================ /* * 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 . */ package haveno.core.network.p2p.inventory; import com.google.inject.Inject; import haveno.common.handlers.ErrorMessageHandler; import haveno.core.network.p2p.inventory.model.InventoryItem; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.network.NetworkNode; import java.util.HashMap; import java.util.Map; import java.util.function.Consumer; import lombok.extern.slf4j.Slf4j; @Slf4j public class GetInventoryRequestManager { private final NetworkNode networkNode; private final Map requesterMap = new HashMap<>(); @Inject public GetInventoryRequestManager(NetworkNode networkNode) { this.networkNode = networkNode; } public void request(NodeAddress nodeAddress, Consumer> resultHandler, ErrorMessageHandler errorMessageHandler) { if (requesterMap.containsKey(nodeAddress)) { log.warn("There was still a pending request for {}. We shut it down and make a new request", nodeAddress.getFullAddress()); requesterMap.get(nodeAddress).shutDown(); } GetInventoryRequester getInventoryRequester = new GetInventoryRequester(networkNode, nodeAddress, resultMap -> { requesterMap.remove(nodeAddress); resultHandler.accept(resultMap); }, errorMessage -> { requesterMap.remove(nodeAddress); errorMessageHandler.handleErrorMessage(errorMessage); }); requesterMap.put(nodeAddress, getInventoryRequester); getInventoryRequester.request(); } public void shutDown() { requesterMap.values().forEach(GetInventoryRequester::shutDown); requesterMap.clear(); } } ================================================ FILE: core/src/main/java/haveno/core/network/p2p/inventory/GetInventoryRequester.java ================================================ /* * 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 . */ package haveno.core.network.p2p.inventory; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.app.Version; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.proto.network.NetworkEnvelope; import haveno.core.network.p2p.inventory.messages.GetInventoryRequest; import haveno.core.network.p2p.inventory.messages.GetInventoryResponse; import haveno.core.network.p2p.inventory.model.InventoryItem; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.network.CloseConnectionReason; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.ConnectionListener; import haveno.network.p2p.network.MessageListener; import haveno.network.p2p.network.NetworkNode; import lombok.extern.slf4j.Slf4j; import java.util.Map; import java.util.function.Consumer; @Slf4j public class GetInventoryRequester implements MessageListener, ConnectionListener { private final static int TIMEOUT_SEC = 180; private final NetworkNode networkNode; private final NodeAddress nodeAddress; private final Consumer> resultHandler; private final ErrorMessageHandler errorMessageHandler; private Timer timer; public GetInventoryRequester(NetworkNode networkNode, NodeAddress nodeAddress, Consumer> resultHandler, ErrorMessageHandler errorMessageHandler) { this.networkNode = networkNode; this.nodeAddress = nodeAddress; this.resultHandler = resultHandler; this.errorMessageHandler = errorMessageHandler; } public void request() { networkNode.addMessageListener(this); networkNode.addConnectionListener(this); timer = UserThread.runAfter(this::onTimeOut, TIMEOUT_SEC); GetInventoryRequest getInventoryRequest = new GetInventoryRequest(Version.VERSION); networkNode.sendMessage(nodeAddress, getInventoryRequest); } private void onTimeOut() { errorMessageHandler.handleErrorMessage("Request timeout"); shutDown(); } @Override public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { if (networkEnvelope instanceof GetInventoryResponse) { connection.getPeersNodeAddressOptional().ifPresent(peer -> { if (peer.equals(nodeAddress)) { GetInventoryResponse getInventoryResponse = (GetInventoryResponse) networkEnvelope; resultHandler.accept(getInventoryResponse.getInventory()); shutDown(); // We shut down our connection after work as our node is not helpful for the network. UserThread.runAfter(() -> connection.shutDown(CloseConnectionReason.CLOSE_REQUESTED_BY_PEER), 1); } }); } } public void shutDown() { if (timer != null) { timer.stop(); timer = null; } networkNode.removeMessageListener(this); networkNode.removeConnectionListener(this); } @Override public void onConnection(Connection connection) { } @Override public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { connection.getPeersNodeAddressOptional().ifPresent(address -> { if (address.equals(nodeAddress)) { if (!closeConnectionReason.isIntended) { errorMessageHandler.handleErrorMessage("Connected closed because of " + closeConnectionReason.name()); } shutDown(); } }); } } ================================================ FILE: core/src/main/java/haveno/core/network/p2p/inventory/messages/GetInventoryRequest.java ================================================ /* * 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 . */ package haveno.core.network.p2p.inventory.messages; import haveno.common.app.Version; import haveno.common.proto.network.NetworkEnvelope; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; @Getter @EqualsAndHashCode(callSuper = false) @ToString public class GetInventoryRequest extends NetworkEnvelope { private final String version; public GetInventoryRequest(String version) { this(version, Version.getP2PMessageVersion()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private GetInventoryRequest(String version, String messageVersion) { super(messageVersion); this.version = version; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { return getNetworkEnvelopeBuilder() .setGetInventoryRequest(protobuf.GetInventoryRequest.newBuilder() .setVersion(version)) .build(); } public static GetInventoryRequest fromProto(protobuf.GetInventoryRequest proto, String messageVersion) { return new GetInventoryRequest(proto.getVersion(), messageVersion); } } ================================================ FILE: core/src/main/java/haveno/core/network/p2p/inventory/messages/GetInventoryResponse.java ================================================ /* * 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 . */ package haveno.core.network.p2p.inventory.messages; import com.google.common.base.Enums; import com.google.common.base.Optional; import haveno.common.app.Version; import haveno.common.proto.network.NetworkEnvelope; import haveno.core.network.p2p.inventory.model.InventoryItem; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; import java.util.HashMap; import java.util.Map; @Getter @EqualsAndHashCode(callSuper = false) @ToString public class GetInventoryResponse extends NetworkEnvelope { private final Map inventory; public GetInventoryResponse(Map inventory) { this(inventory, Version.getP2PMessageVersion()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private GetInventoryResponse(Map inventory, String messageVersion) { super(messageVersion); this.inventory = inventory; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { // For protobuf we use a map with a string key Map map = new HashMap<>(); inventory.forEach((key, value) -> map.put(key.getKey(), value)); return getNetworkEnvelopeBuilder() .setGetInventoryResponse(protobuf.GetInventoryResponse.newBuilder() .putAllInventory(map)) .build(); } public static GetInventoryResponse fromProto(protobuf.GetInventoryResponse proto, String messageVersion) { // For protobuf we use a map with a string key Map map = proto.getInventoryMap(); Map inventory = new HashMap<>(); map.forEach((key, value) -> { Optional optional = Enums.getIfPresent(InventoryItem.class, key); if (optional.isPresent()) { inventory.put(optional.get(), value); } }); return new GetInventoryResponse(inventory, messageVersion); } } ================================================ FILE: core/src/main/java/haveno/core/network/p2p/inventory/model/Average.java ================================================ /* * 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 . */ package haveno.core.network.p2p.inventory.model; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Set; public class Average { public static Map of(Set requestInfoSet) { Map averageValuesPerItem = new HashMap<>(); Arrays.asList(InventoryItem.values()).forEach(inventoryItem -> { if (inventoryItem.isNumberValue()) { averageValuesPerItem.put(inventoryItem, getAverage(requestInfoSet, inventoryItem)); } }); return averageValuesPerItem; } public static double getAverage(Set requestInfoSet, InventoryItem inventoryItem) { return requestInfoSet.stream() .map(RequestInfo::getDataMap) .filter(map -> map.containsKey(inventoryItem)) .map(map -> map.get(inventoryItem).getValue()) .filter(Objects::nonNull) .mapToDouble(Double::parseDouble) .average() .orElse(0d); } } ================================================ FILE: core/src/main/java/haveno/core/network/p2p/inventory/model/DeviationByIntegerDiff.java ================================================ /* * 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 . */ package haveno.core.network.p2p.inventory.model; import haveno.common.util.Tuple2; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; public class DeviationByIntegerDiff implements DeviationType { private final int warnTrigger; private final int alertTrigger; public DeviationByIntegerDiff(int warnTrigger, int alertTrigger) { this.warnTrigger = warnTrigger; this.alertTrigger = alertTrigger; } public DeviationSeverity getDeviationSeverity(Collection> collection, @Nullable String value, InventoryItem inventoryItem) { DeviationSeverity deviationSeverity = DeviationSeverity.OK; if (value == null) { return deviationSeverity; } Map sameItemsByValue = new HashMap<>(); collection.stream() .filter(list -> !list.isEmpty()) .map(list -> list.get(list.size() - 1)) // We use last item only .map(RequestInfo::getDataMap) .map(e -> e.get(inventoryItem).getValue()) .filter(Objects::nonNull) .forEach(e -> { sameItemsByValue.putIfAbsent(e, 0); int prev = sameItemsByValue.get(e); sameItemsByValue.put(e, prev + 1); }); if (sameItemsByValue.size() > 1) { List> sameItems = new ArrayList<>(); sameItemsByValue.forEach((k, v) -> sameItems.add(new Tuple2<>(k, v))); sameItems.sort(Comparator.comparing(o -> o.second)); Collections.reverse(sameItems); String majority = sameItems.get(0).first; if (!majority.equals(value)) { int majorityAsInt = Integer.parseInt(majority); int valueAsInt = Integer.parseInt(value); int diff = Math.abs(majorityAsInt - valueAsInt); if (diff >= alertTrigger) { deviationSeverity = DeviationSeverity.ALERT; } else if (diff >= warnTrigger) { deviationSeverity = DeviationSeverity.WARN; } else { deviationSeverity = DeviationSeverity.OK; } } } return deviationSeverity; } } ================================================ FILE: core/src/main/java/haveno/core/network/p2p/inventory/model/DeviationByPercentage.java ================================================ /* * 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 . */ package haveno.core.network.p2p.inventory.model; public class DeviationByPercentage implements DeviationType { private final double lowerAlertTrigger; private final double upperAlertTrigger; private final double lowerWarnTrigger; private final double upperWarnTrigger; // In case want to see the % deviation but not trigger any warnings or alerts public DeviationByPercentage() { this(0, Double.MAX_VALUE, 0, Double.MAX_VALUE); } public DeviationByPercentage(double lowerAlertTrigger, double upperAlertTrigger, double lowerWarnTrigger, double upperWarnTrigger) { this.lowerAlertTrigger = lowerAlertTrigger; this.upperAlertTrigger = upperAlertTrigger; this.lowerWarnTrigger = lowerWarnTrigger; this.upperWarnTrigger = upperWarnTrigger; } public DeviationSeverity getDeviationSeverity(double deviation) { if (deviation <= lowerAlertTrigger || deviation >= upperAlertTrigger) { return DeviationSeverity.ALERT; } if (deviation <= lowerWarnTrigger || deviation >= upperWarnTrigger) { return DeviationSeverity.WARN; } return DeviationSeverity.OK; } } ================================================ FILE: core/src/main/java/haveno/core/network/p2p/inventory/model/DeviationOfHashes.java ================================================ /* * 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 . */ package haveno.core.network.p2p.inventory.model; import haveno.common.util.Tuple2; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; public class DeviationOfHashes implements DeviationType { public DeviationSeverity getDeviationSeverity(Collection> collection, @Nullable String value, InventoryItem inventoryItem) { DeviationSeverity deviationSeverity = DeviationSeverity.OK; if (value == null) { return deviationSeverity; } Map sameHashesPerHashListByHash = new HashMap<>(); collection.stream() .filter(list -> !list.isEmpty()) .map(list -> list.get(list.size() - 1)) // We use last item only .map(RequestInfo::getDataMap) .map(map -> map.get(inventoryItem).getValue()) .filter(Objects::nonNull) .forEach(v -> { sameHashesPerHashListByHash.putIfAbsent(v, 0); int prev = sameHashesPerHashListByHash.get(v); sameHashesPerHashListByHash.put(v, prev + 1); }); if (sameHashesPerHashListByHash.size() > 1) { List> sameHashesPerHashList = new ArrayList<>(); sameHashesPerHashListByHash.forEach((k, v) -> sameHashesPerHashList.add(new Tuple2<>(k, v))); sameHashesPerHashList.sort(Comparator.comparing(o -> o.second)); Collections.reverse(sameHashesPerHashList); // It could be that first and any following list entry has same number of hashes, but we ignore that as // it is reason enough to alert the operators in case not all hashes are the same. if (sameHashesPerHashList.get(0).first.equals(value)) { // We are in the majority group. // We also set a warning to make sure the operators act quickly and to check if there are // more severe issues. deviationSeverity = DeviationSeverity.WARN; } else { deviationSeverity = DeviationSeverity.ALERT; } } return deviationSeverity; } } ================================================ FILE: core/src/main/java/haveno/core/network/p2p/inventory/model/DeviationSeverity.java ================================================ /* * 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 . */ package haveno.core.network.p2p.inventory.model; public enum DeviationSeverity { IGNORED, OK, WARN, ALERT } ================================================ FILE: core/src/main/java/haveno/core/network/p2p/inventory/model/DeviationType.java ================================================ /* * 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 . */ package haveno.core.network.p2p.inventory.model; public interface DeviationType { } ================================================ FILE: core/src/main/java/haveno/core/network/p2p/inventory/model/InventoryItem.java ================================================ /* * 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 . */ package haveno.core.network.p2p.inventory.model; import haveno.common.util.Tuple2; import lombok.Getter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Collection; import java.util.List; import java.util.Map; public enum InventoryItem { // Percentage deviation OfferPayload("OfferPayload", true, new DeviationByPercentage(0.5, 1.5, 0.75, 1.25), 10), MailboxStoragePayload("MailboxStoragePayload", true, new DeviationByPercentage(0.9, 1.1, 0.95, 1.05), 2), TradeStatistics3("TradeStatistics3", true, new DeviationByPercentage(0.9, 1.1, 0.95, 1.05), 2), AccountAgeWitness("AccountAgeWitness", true, new DeviationByPercentage(0.9, 1.1, 0.95, 1.05), 2), SignedWitness("SignedWitness", true, new DeviationByPercentage(0.9, 1.1, 0.95, 1.05), 2), // Should be same value Alert("Alert", true, new DeviationByIntegerDiff(1, 1), 2), Filter("Filter", true, new DeviationByIntegerDiff(1, 1), 2), Mediator("Mediator", true, new DeviationByIntegerDiff(1, 1), 2), RefundAgent("RefundAgent", true, new DeviationByIntegerDiff(1, 1), 2), // Percentage deviation maxConnections("maxConnections", true, new DeviationByPercentage(0.33, 3, 0.4, 2.5), 2), numConnections("numConnections", true, new DeviationByPercentage(0, 3, 0, 2.5), 2), peakNumConnections("peakNumConnections", true, new DeviationByPercentage(0, 3, 0, 2.5), 2), numAllConnectionsLostEvents("numAllConnectionsLostEvents", true, new DeviationByIntegerDiff(1, 2), 1), sentBytesPerSec("sentBytesPerSec", true, new DeviationByPercentage(), 5), receivedBytesPerSec("receivedBytesPerSec", true, new DeviationByPercentage(), 5), receivedMessagesPerSec("receivedMessagesPerSec", true, new DeviationByPercentage(), 5), sentMessagesPerSec("sentMessagesPerSec", true, new DeviationByPercentage(), 5), // No deviation check sentBytes("sentBytes", true), receivedBytes("receivedBytes", true), // No deviation check version("version", false), commitHash("commitHash", false), usedMemory("usedMemory", true), jvmStartTime("jvmStartTime", true), filteredSeeds("filteredSeeds", false); @Getter private final String key; @Getter private final boolean isNumberValue; @Getter @Nullable private DeviationType deviationType; // The number of past requests we check to see if there have been repeated alerts or warnings. The higher the // number the more repeated alert need to have happened to cause a notification alert. // Smallest number is 1, as that takes only the last request data and does not look further back. @Getter private int deviationTolerance = 1; InventoryItem(String key, boolean isNumberValue) { this.key = key; this.isNumberValue = isNumberValue; } InventoryItem(String key, boolean isNumberValue, @NotNull DeviationType deviationType, int deviationTolerance) { this(key, isNumberValue); this.deviationType = deviationType; this.deviationTolerance = deviationTolerance; } @Nullable public Tuple2 getDeviationAndAverage(Map averageValues, @Nullable String value) { if (averageValues.containsKey(this) && value != null) { double averageValue = averageValues.get(this); return new Tuple2<>(getDeviation(value, averageValue), averageValue); } return null; } @Nullable public Double getDeviation(@Nullable String value, double average) { if (deviationType != null && value != null && average != 0 && isNumberValue) { return Double.parseDouble(value) / average; } return null; } public DeviationSeverity getDeviationSeverity(Double deviation, Collection> collection, @Nullable String value) { if (deviationType == null || deviation == null || value == null) { return DeviationSeverity.OK; } if (deviationType instanceof DeviationByPercentage) { return ((DeviationByPercentage) deviationType).getDeviationSeverity(deviation); } else if (deviationType instanceof DeviationByIntegerDiff) { return ((DeviationByIntegerDiff) deviationType).getDeviationSeverity(collection, value, this); } else if (deviationType instanceof DeviationOfHashes) { return ((DeviationOfHashes) deviationType).getDeviationSeverity(collection, value, this); } else { return DeviationSeverity.OK; } } } ================================================ FILE: core/src/main/java/haveno/core/network/p2p/inventory/model/RequestInfo.java ================================================ /* * 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 . */ package haveno.core.network.p2p.inventory.model; import lombok.Getter; import lombok.Setter; import lombok.Value; import org.jetbrains.annotations.Nullable; import java.util.HashMap; import java.util.Map; @Getter public class RequestInfo { // Carries latest commit hash of feature changes (not latest commit as that is then the commit for editing that field) public static final String COMMIT_HASH = "c07d47a8"; private final long requestStartTime; @Setter private long responseTime; @Nullable @Setter private String errorMessage; private final Map dataMap = new HashMap<>(); public RequestInfo(long requestStartTime) { this.requestStartTime = requestStartTime; } public String getDisplayValue(InventoryItem inventoryItem) { String value = getValue(inventoryItem); return value != null ? value : "n/a"; } @Nullable public String getValue(InventoryItem inventoryItem) { return dataMap.containsKey(inventoryItem) ? dataMap.get(inventoryItem).getValue() : null; } public boolean hasError() { return errorMessage != null && !errorMessage.isEmpty(); } @Value public static class Data { private final String value; @Nullable private final Double average; private final Double deviation; private final DeviationSeverity deviationSeverity; private final boolean persistentWarning; private final boolean persistentAlert; public Data(String value, @Nullable Double average, Double deviation, DeviationSeverity deviationSeverity, boolean persistentWarning, boolean persistentAlert) { this.value = value; this.average = average; this.deviation = deviation; this.deviationSeverity = deviationSeverity; this.persistentWarning = persistentWarning; this.persistentAlert = persistentAlert; } @Override public String toString() { return "InventoryData{" + "\n value='" + value + '\'' + ",\n average=" + average + ",\n deviation=" + deviation + ",\n deviationSeverity=" + deviationSeverity + ",\n persistentWarning=" + persistentWarning + ",\n persistentAlert=" + persistentAlert + "\n}"; } } } ================================================ FILE: core/src/main/java/haveno/core/network/p2p/seed/DefaultSeedNodeRepository.java ================================================ /* * 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 . */ package haveno.core.network.p2p.seed; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.config.Config; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.seed.SeedNodeRepository; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; // If a new BaseCurrencyNetwork type gets added we need to add the resource file for it as well! @Slf4j @Singleton public class DefaultSeedNodeRepository implements SeedNodeRepository { //TODO add support for localhost addresses private static final Pattern pattern = Pattern.compile("^([a-z0-9]+\\.onion:\\d+)"); private static final String ENDING = ".seednodes"; private final Collection cache = new HashSet<>(); private final Config config; @Inject public DefaultSeedNodeRepository(Config config) { this.config = config; } private void reload() { try { // see if there are any seed nodes configured manually if (!config.seedNodes.isEmpty()) { cache.clear(); config.seedNodes.forEach(s -> cache.add(new NodeAddress(s))); return; } cache.clear(); List result = getSeedNodeAddressesFromPropertyFile(config.baseCurrencyNetwork.name().toLowerCase()); cache.addAll(result); // filter cache.removeAll( config.bannedSeedNodes.stream() .filter(n -> !n.isEmpty()) .map(NodeAddress::new) .collect(Collectors.toSet())); log.info("Seed nodes: {}", cache); } catch (Throwable t) { log.error("exception in DefaultSeedNodeRepository", t); t.printStackTrace(); throw t; } } public static Optional readSeedNodePropertyFile(String fileName) { InputStream fileInputStream = DefaultSeedNodeRepository.class.getClassLoader().getResourceAsStream( fileName + ENDING); if (fileInputStream == null) { return Optional.empty(); } return Optional.of(new BufferedReader(new InputStreamReader(fileInputStream))); } public static List getSeedNodeAddressesFromPropertyFile(String fileName) { List list = new ArrayList<>(); readSeedNodePropertyFile(fileName).ifPresent(seedNodeFile -> { seedNodeFile.lines().forEach(line -> { Matcher matcher = pattern.matcher(line); if (matcher.find()) list.add(new NodeAddress(matcher.group(1))); // Maybe better include in regex... if (line.startsWith("localhost")) { String[] strings = line.split(" \\(@"); String node = strings[0]; list.add(new NodeAddress(node)); } }); }); return list; } @Override public Collection getSeedNodeAddresses() { if (cache.isEmpty()) reload(); return cache; } @Override public boolean isSeedNode(NodeAddress nodeAddress) { if (cache.isEmpty()) reload(); return cache.contains(nodeAddress); } } ================================================ FILE: core/src/main/java/haveno/core/notifications/MobileMessage.java ================================================ /* * 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 . */ package haveno.core.notifications; import haveno.common.util.JsonExclude; import lombok.Value; import java.util.Date; @Value public class MobileMessage { private long sentDate; private String txId; private String title; private String message; @JsonExclude transient private MobileMessageType mobileMessageType; private String type; private String actionRequired; private int version; public MobileMessage(String title, String message, MobileMessageType mobileMessageType) { this(title, message, "", mobileMessageType); } public MobileMessage(String title, String message, String txId, MobileMessageType mobileMessageType) { this.title = title; this.message = message; this.txId = txId; this.mobileMessageType = mobileMessageType; this.type = mobileMessageType.name(); actionRequired = ""; sentDate = new Date().getTime(); version = 1; } } ================================================ FILE: core/src/main/java/haveno/core/notifications/MobileMessageEncryption.java ================================================ /* * 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 . */ package haveno.core.notifications; import com.google.common.base.Charsets; import com.google.inject.Inject; import com.google.inject.Singleton; import java.security.NoSuchAlgorithmException; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.binary.Base64; @Slf4j @Singleton public class MobileMessageEncryption { private SecretKeySpec keySpec; private Cipher cipher; @Inject public MobileMessageEncryption() { } public void setKey(String key) { keySpec = new SecretKeySpec(key.getBytes(Charsets.UTF_8), "AES"); try { cipher = Cipher.getInstance("AES/CBC/NOPadding"); } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { e.printStackTrace(); } } public String encrypt(String valueToEncrypt, String iv) throws Exception { while (valueToEncrypt.length() % 16 != 0) { valueToEncrypt = valueToEncrypt + " "; } if (iv.length() != 16) { throw new Exception("iv not 16 characters"); } IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes(Charsets.UTF_8)); byte[] encryptedBytes = doEncrypt(valueToEncrypt, ivSpec); return Base64.encodeBase64String(encryptedBytes); } private byte[] doEncrypt(String text, IvParameterSpec ivSpec) throws Exception { if (text == null || text.length() == 0) { throw new Exception("Empty string"); } byte[] encrypted; try { cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); encrypted = cipher.doFinal(text.getBytes(Charsets.UTF_8)); } catch (Exception e) { throw new Exception("[encrypt] " + e.getMessage()); } return encrypted; } } ================================================ FILE: core/src/main/java/haveno/core/notifications/MobileMessageType.java ================================================ /* * 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 . */ package haveno.core.notifications; public enum MobileMessageType { SETUP_CONFIRMATION, OFFER, TRADE, DISPUTE, PRICE, MARKET, ERASE } ================================================ FILE: core/src/main/java/haveno/core/notifications/MobileModel.java ================================================ /* * 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 . */ package haveno.core.notifications; import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; import com.google.inject.Singleton; import java.util.Arrays; import javax.annotation.Nullable; import lombok.Data; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @Data @Slf4j @Singleton public class MobileModel { public static final String PHONE_SEPARATOR_ESCAPED = "\\|"; // see https://stackoverflow.com/questions/5675704/java-string-split-not-returning-the-right-values public static final String PHONE_SEPARATOR_WRITING = "|"; public enum OS { UNDEFINED(""), IOS("iOS"), IOS_DEV("iOSDev"), ANDROID("android"); @Getter private String magicString; OS(String magicString) { this.magicString = magicString; } } @Nullable private OS os; @Nullable private String descriptor; @Nullable private String key; @Nullable private String token; private boolean isContentAvailable = true; @Inject public MobileModel() { } public void reset() { os = null; key = null; token = null; } public void applyKeyAndToken(String keyAndToken) { log.info("applyKeyAndToken: keyAndToken={}", keyAndToken.substring(0, 20) + "...(truncated in log for privacy reasons)"); String[] tokens = keyAndToken.split(PHONE_SEPARATOR_ESCAPED); String magic = tokens[0]; descriptor = tokens[1]; key = tokens[2]; token = tokens[3]; if (magic.equals(OS.IOS.getMagicString())) os = OS.IOS; else if (magic.equals(OS.IOS_DEV.getMagicString())) os = OS.IOS_DEV; else if (magic.equals(OS.ANDROID.getMagicString())) os = OS.ANDROID; isContentAvailable = parseDescriptor(descriptor); } @VisibleForTesting boolean parseDescriptor(String descriptor) { // phone descriptors /* iPod Touch 5 iPod Touch 6 iPhone 4 iPhone 4s iPhone 5 iPhone 5c iPhone 5s iPhone 6 iPhone 6 Plus iPhone 6s iPhone 6s Plus iPhone 7 iPhone 7 Plus iPhone SE iPhone 8 iPhone 8 Plus iPhone X iPhone XS iPhone XS Max iPhone XR iPhone 11 iPhone 11 Pro iPhone 11 Pro Max iPad 2 iPad 3 iPad 4 iPad Air iPad Air 2 iPad 5 iPad 6 iPad Mini iPad Mini 2 iPad Mini 3 iPad Mini 4 iPad Pro 9.7 Inch iPad Pro 12.9 Inch iPad Pro 12.9 Inch 2. Generation iPad Pro 10.5 Inch */ // iPhone 6 does not support isContentAvailable, iPhone 6s and 7 does. // We don't know about other versions, but let's assume all above iPhone 6 are ok. if (descriptor != null) { String[] descriptorTokens = descriptor.split(" "); if (descriptorTokens.length >= 1) { String model = descriptorTokens[0]; if (model.equals("iPhone")) { String versionString = descriptorTokens[1]; String[] validVersions = {"X", "XS", "XR"}; if (Arrays.asList(validVersions).contains(versionString)) { return true; } String versionSuffix = ""; if (versionString.matches("\\d[^\\d]")) { versionSuffix = versionString.substring(1); versionString = versionString.substring(0, 1); } else if (versionString.matches("\\d{2}[^\\d]")) { versionSuffix = versionString.substring(2); versionString = versionString.substring(0, 2); } try { int version = Integer.parseInt(versionString); return version > 6 || (version == 6 && versionSuffix.equalsIgnoreCase("s")); } catch (Throwable ignore) { } } else { return (model.equals("iPad")) && descriptorTokens[1].equals("Pro"); } } } return false; } } ================================================ FILE: core/src/main/java/haveno/core/notifications/MobileNotificationService.java ================================================ /* * 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 . */ package haveno.core.notifications; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.gson.Gson; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.inject.name.Named; import haveno.common.UserThread; import haveno.common.app.Version; import haveno.common.config.Config; import haveno.common.util.Utilities; import haveno.core.user.Preferences; import haveno.network.http.HttpClient; import java.util.UUID; import java.util.function.Consumer; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.binary.Hex; import org.jetbrains.annotations.NotNull; @Slf4j @Singleton public class MobileNotificationService { // Used in Relay app to response of a success state. We won't want a code dependency just for that string so we keep it // duplicated in relay and here. Must not be changed. private static final String SUCCESS = "success"; private static final String DEV_URL_LOCALHOST = "http://localhost:8080/"; private static final String DEV_URL = "http://165.227.40.124:8080/"; private static final String URL = "http://jtboonrvwmq7frkj.onion/"; private static final String HAVENO_MESSAGE_IOS_MAGIC = "HavenoMessageiOS"; private static final String HAVENO_MESSAGE_ANDROID_MAGIC = "HavenoMessageAndroid"; private final Preferences preferences; private final MobileMessageEncryption mobileMessageEncryption; private final MobileNotificationValidator mobileNotificationValidator; private final HttpClient httpClient; private final ListeningExecutorService executorService = Utilities.getListeningExecutorService( "MobileNotificationService", 10, 15, 10 * 60); @Getter private final MobileModel mobileModel; @Getter private boolean setupConfirmationSent; @Getter private BooleanProperty useSoundProperty = new SimpleBooleanProperty(); @Getter private BooleanProperty useTradeNotificationsProperty = new SimpleBooleanProperty(); @Getter private BooleanProperty useMarketNotificationsProperty = new SimpleBooleanProperty(); @Getter private BooleanProperty usePriceNotificationsProperty = new SimpleBooleanProperty(); @Inject public MobileNotificationService(Preferences preferences, MobileMessageEncryption mobileMessageEncryption, MobileNotificationValidator mobileNotificationValidator, MobileModel mobileModel, HttpClient httpClient, @Named(Config.USE_LOCALHOST_FOR_P2P) boolean useLocalHost) { this.preferences = preferences; this.mobileMessageEncryption = mobileMessageEncryption; this.mobileNotificationValidator = mobileNotificationValidator; this.httpClient = httpClient; this.mobileModel = mobileModel; // httpClient.setBaseUrl(useLocalHost ? DEV_URL_LOCALHOST : URL); httpClient.setBaseUrl(useLocalHost ? DEV_URL : URL); httpClient.setIgnoreSocks5Proxy(false); } public void onAllServicesInitialized() { String keyAndToken = preferences.getPhoneKeyAndToken(); if (mobileNotificationValidator.isValid(keyAndToken)) { setupConfirmationSent = true; mobileModel.applyKeyAndToken(keyAndToken); mobileMessageEncryption.setKey(mobileModel.getKey()); } useTradeNotificationsProperty.set(preferences.isUseTradeNotifications()); useMarketNotificationsProperty.set(preferences.isUseMarketNotifications()); usePriceNotificationsProperty.set(preferences.isUsePriceNotifications()); useSoundProperty.set(preferences.isUseSoundForMobileNotifications()); } public boolean sendMessage(MobileMessage message) throws Exception { return sendMessage(message, useSoundProperty.get()); } public boolean applyKeyAndToken(String keyAndToken) { if (mobileNotificationValidator.isValid(keyAndToken)) { mobileModel.applyKeyAndToken(keyAndToken); mobileMessageEncryption.setKey(mobileModel.getKey()); preferences.setPhoneKeyAndToken(keyAndToken); if (!setupConfirmationSent) { try { sendConfirmationMessage(); setupConfirmationSent = true; } catch (Exception e) { e.printStackTrace(); } } return true; } else { return false; } } /** * * @param message The message to send * @param useSound If a sound should be used on the mobile device. * @return Returns true if the message was sent. It does not reflect if the sending was successful. * The result and error handlers carry that information. * @throws Exception */ public boolean sendMessage(MobileMessage message, boolean useSound) throws Exception { return sendMessage(message, useSound, result -> log.debug("sendMessage result=" + result), throwable -> log.error("sendMessage failed. throwable=" + throwable.toString())); } /** * * @param message The message to send * @param useSound If a sound should be used on the mobile device. * @param resultHandler The result of the send operation (sent on a custom thread) * @param errorHandler Carries the throwable if an error occurred at sending (sent on a custom thread) * @return Returns true if the message was sent. It does not reflect if the sending was successful. * The result and error handlers carry that information. * @throws Exception */ private boolean sendMessage(MobileMessage message, boolean useSound, Consumer resultHandler, Consumer errorHandler) throws Exception { if (mobileModel.getKey() == null) return false; boolean doSend; switch (message.getMobileMessageType()) { case SETUP_CONFIRMATION: doSend = true; break; case OFFER: case TRADE: case DISPUTE: doSend = useTradeNotificationsProperty.get(); break; case PRICE: doSend = usePriceNotificationsProperty.get(); break; case MARKET: doSend = useMarketNotificationsProperty.get(); break; case ERASE: doSend = true; break; default: doSend = false; } if (!doSend) return false; log.info("Send message: '{}'", message.getMessage()); log.info("sendMessage message={}", message); Gson gson = new Gson(); String json = gson.toJson(message); log.info("json " + json); StringBuilder padded = new StringBuilder(json); while (padded.length() % 16 != 0) { padded.append(" "); } json = padded.toString(); // generate 16 random characters for iv String uuid = UUID.randomUUID().toString(); uuid = uuid.replace("-", ""); String iv = uuid.substring(0, 16); String cipher = mobileMessageEncryption.encrypt(json, iv); log.info("key = " + mobileModel.getKey()); log.info("iv = " + iv); log.info("encryptedJson = " + cipher); doSendMessage(iv, cipher, useSound, resultHandler, errorHandler); return true; } public void sendEraseMessage() throws Exception { MobileMessage message = new MobileMessage("", "", MobileMessageType.ERASE); sendMessage(message, false); } public void reset() { mobileModel.reset(); preferences.setPhoneKeyAndToken(null); setupConfirmationSent = false; } private void sendConfirmationMessage() throws Exception { log.info("sendConfirmationMessage"); MobileMessage message = new MobileMessage("", "", MobileMessageType.SETUP_CONFIRMATION); sendMessage(message, true); } private void doSendMessage(String iv, String cipher, boolean useSound, Consumer resultHandler, Consumer errorHandler) throws Exception { if (httpClient.hasPendingRequest()) { log.warn("We have a pending request open. We ignore that request. httpClient {}", httpClient); return; } String msg; if (mobileModel.getOs() == null) throw new RuntimeException("No mobileModel OS set"); switch (mobileModel.getOs()) { case IOS: msg = HAVENO_MESSAGE_IOS_MAGIC; break; case IOS_DEV: msg = HAVENO_MESSAGE_IOS_MAGIC; break; case ANDROID: msg = HAVENO_MESSAGE_ANDROID_MAGIC; break; case UNDEFINED: default: throw new RuntimeException("No mobileModel OS set"); } msg += MobileModel.PHONE_SEPARATOR_WRITING + iv + MobileModel.PHONE_SEPARATOR_WRITING + cipher; boolean isAndroid = mobileModel.getOs() == MobileModel.OS.ANDROID; boolean isProduction = mobileModel.getOs() == MobileModel.OS.IOS; checkNotNull(mobileModel.getToken(), "mobileModel.getToken() must not be null"); String tokenAsHex = Hex.encodeHexString(mobileModel.getToken().getBytes("UTF-8")); String msgAsHex = Hex.encodeHexString(msg.getBytes("UTF-8")); String param = "relay?" + "isAndroid=" + isAndroid + "&isProduction=" + isProduction + "&isContentAvailable=" + mobileModel.isContentAvailable() + "&snd=" + useSound + "&token=" + tokenAsHex + "&" + "msg=" + msgAsHex; log.info("Send: token={}", mobileModel.getToken()); log.info("Send: msg={}", msg); log.info("Send: isAndroid={}\nuseSound={}\ntokenAsHex={}\nmsgAsHex={}", isAndroid, useSound, tokenAsHex, msgAsHex); String threadName = "sendMobileNotification-" + msgAsHex.substring(0, 5) + "..."; ListenableFuture future = executorService.submit(() -> { Thread.currentThread().setName(threadName); String result = httpClient.get(param, "User-Agent", "haveno/" + Version.VERSION + ", uid:" + httpClient.getUid()); log.info("sendMobileNotification result: " + result); checkArgument(result.equals(SUCCESS), "Result was not 'success'. result=" + result); return result; }); Futures.addCallback(future, new FutureCallback<>() { @Override public void onSuccess(String result) { UserThread.execute(() -> resultHandler.accept(result)); } @Override public void onFailure(@NotNull Throwable throwable) { UserThread.execute(() -> errorHandler.accept(throwable)); } }, MoreExecutors.directExecutor()); } } ================================================ FILE: core/src/main/java/haveno/core/notifications/MobileNotificationValidator.java ================================================ /* * 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 . */ package haveno.core.notifications; import com.google.inject.Inject; import com.google.inject.Singleton; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton public class MobileNotificationValidator { @Inject public MobileNotificationValidator() { } public boolean isValid(String keyAndToken) { if (keyAndToken == null) return false; String[] tokens = keyAndToken.split(MobileModel.PHONE_SEPARATOR_ESCAPED); if (tokens.length != 4) { log.error("invalid pairing ID format: not 4 sections separated by " + MobileModel.PHONE_SEPARATOR_WRITING); return false; } String magic = tokens[0]; String key = tokens[2]; String phoneId = tokens[3]; if (key.length() != 32) { log.error("invalid pairing ID format: key not 32 bytes"); return false; } if (magic.equals(MobileModel.OS.IOS.getMagicString()) || magic.equals(MobileModel.OS.IOS_DEV.getMagicString())) { if (phoneId.length() != 64) { log.error("invalid Haveno MobileModel ID format: iOS token not 64 bytes"); return false; } } else if (magic.equals(MobileModel.OS.ANDROID.getMagicString())) { if (phoneId.length() < 32) { log.error("invalid Haveno MobileModel ID format: Android token too short (<32 bytes)"); return false; } } else { log.error("invalid Haveno MobileModel ID format"); return false; } return true; } } ================================================ FILE: core/src/main/java/haveno/core/notifications/alerts/DisputeMsgEvents.java ================================================ /* * 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 . */ package haveno.core.notifications.alerts; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.core.locale.Res; import haveno.core.notifications.MobileMessage; import haveno.core.notifications.MobileMessageType; import haveno.core.notifications.MobileNotificationService; import haveno.core.support.SupportType; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.mediation.MediationManager; import haveno.core.support.dispute.refund.RefundManager; import haveno.core.support.messages.ChatMessage; import haveno.network.p2p.P2PService; import java.util.UUID; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton public class DisputeMsgEvents { private final RefundManager refundManager; private final MediationManager mediationManager; private final P2PService p2PService; private final MobileNotificationService mobileNotificationService; @Inject public DisputeMsgEvents(RefundManager refundManager, MediationManager mediationManager, P2PService p2PService, MobileNotificationService mobileNotificationService) { this.refundManager = refundManager; this.mediationManager = mediationManager; this.p2PService = p2PService; this.mobileNotificationService = mobileNotificationService; } public void onAllServicesInitialized() { refundManager.getDisputesAsObservableList().addListener((ListChangeListener) c -> { c.next(); if (c.wasAdded()) { c.getAddedSubList().forEach(this::setDisputeListener); } }); refundManager.getDisputesAsObservableList().forEach(this::setDisputeListener); mediationManager.getDisputesAsObservableList().addListener((ListChangeListener) c -> { c.next(); if (c.wasAdded()) { c.getAddedSubList().forEach(this::setDisputeListener); } }); mediationManager.getDisputesAsObservableList().forEach(this::setDisputeListener); // We do not need a handling for unread messages as mailbox messages arrive later and will trigger the // event listeners. But the existing messages are not causing a notification. } public static MobileMessage getTestMsg() { String shortId = UUID.randomUUID().toString().substring(0, 8); return new MobileMessage(Res.get("account.notifications.dispute.message.title"), Res.get("account.notifications.dispute.message.msg", shortId), shortId, MobileMessageType.DISPUTE); } private void setDisputeListener(Dispute dispute) { log.debug("We got a dispute added. id={}, tradeId={}", dispute.getId(), dispute.getTradeId()); dispute.getChatMessages().addListener((ListChangeListener) c -> { log.debug("We got a ChatMessage added. id={}, tradeId={}", dispute.getId(), dispute.getTradeId()); c.next(); if (c.wasAdded()) { c.getAddedSubList().forEach(chatMessage -> onChatMessage(chatMessage, dispute)); } }); } private void onChatMessage(ChatMessage chatMessage, Dispute dispute) { if (chatMessage.getSenderNodeAddress().equals(p2PService.getAddress())) { return; } // We only send msg in case we are not the sender String shortId = chatMessage.getShortId(); MobileMessage message = new MobileMessage(Res.get("account.notifications.dispute.message.title"), Res.get("account.notifications.dispute.message.msg", shortId), shortId, MobileMessageType.DISPUTE); try { mobileNotificationService.sendMessage(message); } catch (Exception e) { log.error(e.toString()); e.printStackTrace(); } // We check at every new message if it might be a message sent after the dispute had been closed. If that is the // case we revert the isClosed flag so that the UI can reopen the dispute and indicate that a new dispute // message arrived. synchronized (dispute.getChatMessages()) { ObservableList chatMessages = dispute.getChatMessages(); // If last message is not a result message we re-open as we might have received a new message from the // trader/mediator/arbitrator who has reopened the case if (dispute.isClosed() && !chatMessages.isEmpty() && !chatMessages.get(chatMessages.size() - 1).isResultMessage(dispute)) { dispute.reOpen(); if (dispute.getSupportType() == SupportType.MEDIATION) { mediationManager.requestPersistence(); } else if (dispute.getSupportType() == SupportType.REFUND) { refundManager.requestPersistence(); } } } } } ================================================ FILE: core/src/main/java/haveno/core/notifications/alerts/MyOfferTakenEvents.java ================================================ /* * 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 . */ package haveno.core.notifications.alerts; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.core.locale.Res; import haveno.core.notifications.MobileMessage; import haveno.core.notifications.MobileMessageType; import haveno.core.notifications.MobileNotificationService; import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; import java.util.UUID; import javafx.collections.ListChangeListener; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton public class MyOfferTakenEvents { private final OpenOfferManager openOfferManager; private final MobileNotificationService mobileNotificationService; @Inject public MyOfferTakenEvents(OpenOfferManager openOfferManager, MobileNotificationService mobileNotificationService) { this.openOfferManager = openOfferManager; this.mobileNotificationService = mobileNotificationService; } public void onAllServicesInitialized() { openOfferManager.getObservableList().addListener((ListChangeListener) c -> { c.next(); if (c.wasRemoved()) c.getRemoved().forEach(this::onOpenOfferRemoved); }); openOfferManager.getObservableList().forEach(this::onOpenOfferRemoved); } private void onOpenOfferRemoved(OpenOffer openOffer) { OpenOffer.State state = openOffer.getState(); if (state == OpenOffer.State.RESERVED) { log.info("We got a offer removed. id={}, state={}", openOffer.getId(), state); String shortId = openOffer.getShortId(); MobileMessage message = new MobileMessage(Res.get("account.notifications.offer.message.title"), Res.get("account.notifications.offer.message.msg", shortId), shortId, MobileMessageType.OFFER); try { mobileNotificationService.sendMessage(message); } catch (Exception e) { log.error(e.toString()); e.printStackTrace(); } } } public static MobileMessage getTestMsg() { String shortId = UUID.randomUUID().toString().substring(0, 8); return new MobileMessage(Res.get("account.notifications.offer.message.title"), Res.get("account.notifications.offer.message.msg", shortId), shortId, MobileMessageType.OFFER); } } ================================================ FILE: core/src/main/java/haveno/core/notifications/alerts/TradeEvents.java ================================================ /* * 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 . */ package haveno.core.notifications.alerts; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.crypto.PubKeyRingProvider; import haveno.core.locale.Res; import haveno.core.notifications.MobileMessage; import haveno.core.notifications.MobileMessageType; import haveno.core.notifications.MobileNotificationService; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import java.util.ArrayList; import java.util.List; import java.util.UUID; import javafx.collections.ListChangeListener; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton public class TradeEvents { private final PubKeyRingProvider pubKeyRingProvider; private final TradeManager tradeManager; private final MobileNotificationService mobileNotificationService; private boolean isInitialized = false; @Inject public TradeEvents(TradeManager tradeManager, PubKeyRingProvider pubKeyRingProvider, MobileNotificationService mobileNotificationService) { this.tradeManager = tradeManager; this.mobileNotificationService = mobileNotificationService; this.pubKeyRingProvider = pubKeyRingProvider; } public void onAllServicesInitialized() { tradeManager.getObservableList().addListener((ListChangeListener) c -> { c.next(); if (c.wasAdded()) { c.getAddedSubList().forEach(this::setTradePhaseListener); } }); tradeManager.getObservableList().forEach(this::setTradePhaseListener); isInitialized = true; } private void setTradePhaseListener(Trade trade) { if (isInitialized) log.info("We got a new trade, tradeId={}", trade.getId(), "hasBuyerAsTakerWithoutDeposit=" + trade.getOffer().hasBuyerAsTakerWithoutDeposit()); if (!trade.isPayoutPublished()) { trade.statePhaseProperty().addListener((observable, oldValue, newValue) -> { String msg = null; String shortId = trade.getShortId(); switch (newValue) { case INIT: case DEPOSIT_REQUESTED: case DEPOSITS_PUBLISHED: break; case DEPOSITS_UNLOCKED: case DEPOSITS_FINALIZED: // TODO: use a separate message for deposits finalized? if (trade.getContract() != null && pubKeyRingProvider.get().equals(trade.getContract().getBuyerPubKeyRing())) msg = Res.get("account.notifications.trade.message.msg.conf", shortId); break; case PAYMENT_SENT: // We only notify the seller if (trade.getContract() != null && pubKeyRingProvider.get().equals(trade.getContract().getSellerPubKeyRing())) msg = Res.get("account.notifications.trade.message.msg.started", shortId); break; case PAYMENT_RECEIVED: // We only notify the buyer if (trade.getContract() != null && pubKeyRingProvider.get().equals(trade.getContract().getBuyerPubKeyRing())) msg = Res.get("account.notifications.trade.message.msg.completed", shortId); break; } if (msg != null) { MobileMessage message = new MobileMessage(Res.get("account.notifications.trade.message.title"), msg, shortId, MobileMessageType.TRADE); try { mobileNotificationService.sendMessage(message); } catch (Exception e) { log.error(e.toString()); e.printStackTrace(); } } }); } } public static List getTestMessages() { String shortId = UUID.randomUUID().toString().substring(0, 8); List list = new ArrayList<>(); list.add(new MobileMessage(Res.get("account.notifications.trade.message.title"), Res.get("account.notifications.trade.message.msg.conf", shortId), shortId, MobileMessageType.TRADE)); list.add(new MobileMessage(Res.get("account.notifications.trade.message.title"), Res.get("account.notifications.trade.message.msg.started", shortId), shortId, MobileMessageType.TRADE)); list.add(new MobileMessage(Res.get("account.notifications.trade.message.title"), Res.get("account.notifications.trade.message.msg.completed", shortId), shortId, MobileMessageType.TRADE)); return list; } } ================================================ FILE: core/src/main/java/haveno/core/notifications/alerts/market/MarketAlertFilter.java ================================================ /* * 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 . */ package haveno.core.notifications.alerts.market; import haveno.common.proto.persistable.PersistablePayload; import haveno.core.payment.PaymentAccount; import haveno.core.proto.CoreProtoResolver; import lombok.Value; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; @Slf4j @Value public class MarketAlertFilter implements PersistablePayload { private PaymentAccount paymentAccount; private int triggerValue; private boolean isBuyOffer; private List alertIds; public MarketAlertFilter(PaymentAccount paymentAccount, int triggerValue, boolean isBuyOffer) { this(paymentAccount, triggerValue, isBuyOffer, new ArrayList<>()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// /** * * @param paymentAccount // The payment account used for the filter * @param triggerValue // Percentage distance from market price (100 for 1.00%) * @param isBuyOffer // It the offer is a buy offer * @param alertIds // List of offerIds for which we have sent already an alert */ private MarketAlertFilter(PaymentAccount paymentAccount, int triggerValue, boolean isBuyOffer, List alertIds) { this.paymentAccount = paymentAccount; this.triggerValue = triggerValue; this.isBuyOffer = isBuyOffer; this.alertIds = alertIds; } @Override public protobuf.MarketAlertFilter toProtoMessage() { return protobuf.MarketAlertFilter.newBuilder() .setPaymentAccount(paymentAccount.toProtoMessage()) .setTriggerValue(triggerValue) .setIsBuyOffer(isBuyOffer) .addAllAlertIds(alertIds) .build(); } public static MarketAlertFilter fromProto(protobuf.MarketAlertFilter proto, CoreProtoResolver coreProtoResolver) { List list = proto.getAlertIdsList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getAlertIdsList()); return new MarketAlertFilter(PaymentAccount.fromProto(proto.getPaymentAccount(), coreProtoResolver), proto.getTriggerValue(), proto.getIsBuyOffer(), list); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void addAlertId(String alertId) { if (notContainsAlertId(alertId)) alertIds.add(alertId); } public boolean notContainsAlertId(String alertId) { return !alertIds.contains(alertId); } @Override public String toString() { return "MarketAlertFilter{" + "\n paymentAccount=" + paymentAccount + ",\n triggerValue=" + triggerValue + ",\n isBuyOffer=" + isBuyOffer + ",\n alertIds=" + alertIds + "\n}"; } } ================================================ FILE: core/src/main/java/haveno/core/notifications/alerts/market/MarketAlerts.java ================================================ /* * 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 . */ package haveno.core.notifications.alerts.market; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.crypto.KeyRing; import haveno.common.util.MathUtils; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.Price; import haveno.core.monetary.TraditionalMoney; import haveno.core.notifications.MobileMessage; import haveno.core.notifications.MobileMessageType; import haveno.core.notifications.MobileNotificationService; import haveno.core.offer.Offer; import haveno.core.offer.OfferBookService; import haveno.core.offer.OfferDirection; import haveno.core.provider.price.MarketPrice; import haveno.core.provider.price.PriceFeedService; import haveno.core.user.User; import haveno.core.util.FormattingUtils; import java.util.List; import java.util.UUID; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton public class MarketAlerts { private final OfferBookService offerBookService; private final MobileNotificationService mobileNotificationService; private final User user; private final PriceFeedService priceFeedService; private final KeyRing keyRing; @Inject private MarketAlerts(OfferBookService offerBookService, MobileNotificationService mobileNotificationService, User user, PriceFeedService priceFeedService, KeyRing keyRing) { this.offerBookService = offerBookService; this.mobileNotificationService = mobileNotificationService; this.user = user; this.priceFeedService = priceFeedService; this.keyRing = keyRing; } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void onAllServicesInitialized() { offerBookService.addOfferBookChangedListener(new OfferBookService.OfferBookChangedListener() { @Override public void onAdded(Offer offer) { onOfferAdded(offer); } @Override public void onRemoved(Offer offer) { } }); applyFilterOnAllOffers(); } public void addMarketAlertFilter(MarketAlertFilter filter) { user.addMarketAlertFilter(filter); applyFilterOnAllOffers(); } public void removeMarketAlertFilter(MarketAlertFilter filter) { user.removeMarketAlertFilter(filter); } public List getMarketAlertFilters() { return user.getMarketAlertFilters(); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void applyFilterOnAllOffers() { offerBookService.getOffers().forEach(this::onOfferAdded); } // We combine the offer ID and the price (either as % price or as fixed price) to get also updates for edited offers // % price get multiplied by 10000 to have 0.12% be converted to 12. For fixed price we have precision of 8 for // crypto and traditional. private String getAlertId(Offer offer) { double price = offer.isUseMarketBasedPrice() ? offer.getMarketPriceMarginPct() * 10000 : offer.getOfferPayload().getPrice(); String priceString = String.valueOf((long) price); return offer.getId() + "|" + priceString; } private void onOfferAdded(Offer offer) { String currencyCode = offer.getCounterCurrencyCode(); MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); Price offerPrice = offer.getPrice(); if (marketPrice != null && offerPrice != null) { boolean isSellOffer = offer.getDirection() == OfferDirection.SELL; String shortOfferId = offer.getShortId(); String alertId = getAlertId(offer); user.getMarketAlertFilters().stream() .filter(marketAlertFilter -> !offer.isMyOffer(keyRing)) .filter(marketAlertFilter -> offer.getPaymentMethod().equals(marketAlertFilter.getPaymentAccount().getPaymentMethod())) .filter(marketAlertFilter -> marketAlertFilter.notContainsAlertId(alertId)) .forEach(marketAlertFilter -> { int triggerValue = marketAlertFilter.getTriggerValue(); boolean isTriggerForBuyOffer = marketAlertFilter.isBuyOffer(); double marketPriceAsDouble1 = marketPrice.getPrice(); int precision = CurrencyUtil.isTraditionalCurrency(currencyCode) ? TraditionalMoney.SMALLEST_UNIT_EXPONENT : CryptoMoney.SMALLEST_UNIT_EXPONENT; double marketPriceAsDouble = MathUtils.scaleUpByPowerOf10(marketPriceAsDouble1, precision); double offerPriceValue = offerPrice.getValue(); double ratio = offerPriceValue / marketPriceAsDouble; ratio = 1 - ratio; if (isSellOffer) ratio *= -1; ratio = ratio * 10000; boolean triggered = ratio <= triggerValue; if (!triggered) return; boolean isTriggerForBuyOfferAndTriggered = !isSellOffer && isTriggerForBuyOffer; boolean isTriggerForSellOfferAndTriggered = isSellOffer && !isTriggerForBuyOffer; if (isTriggerForBuyOfferAndTriggered || isTriggerForSellOfferAndTriggered) { String direction = isSellOffer ? Res.get("shared.sell") : Res.get("shared.buy"); String marketDir; if (isSellOffer) { marketDir = ratio > 0 ? Res.get("account.notifications.marketAlert.message.msg.above") : Res.get("account.notifications.marketAlert.message.msg.below"); } else { marketDir = ratio < 0 ? Res.get("account.notifications.marketAlert.message.msg.above") : Res.get("account.notifications.marketAlert.message.msg.below"); } ratio = Math.abs(ratio); String msg = Res.get("account.notifications.marketAlert.message.msg", direction, CurrencyUtil.getCurrencyPair(currencyCode), FormattingUtils.formatPrice(offerPrice), FormattingUtils.formatToPercentWithSymbol(ratio / 10000d), marketDir, Res.get(offer.getPaymentMethod().getId()), shortOfferId); MobileMessage message = new MobileMessage(Res.get("account.notifications.marketAlert.message.title"), msg, shortOfferId, MobileMessageType.MARKET); try { boolean wasSent = mobileNotificationService.sendMessage(message); if (wasSent) { // In case we have disabled alerts wasSent is false and we do not // persist the offer marketAlertFilter.addAlertId(alertId); user.requestPersistence(); } } catch (Exception e) { e.printStackTrace(); } } }); } } public static MobileMessage getTestMsg() { String shortId = UUID.randomUUID().toString().substring(0, 8); return new MobileMessage(Res.get("account.notifications.marketAlert.message.title"), "A new 'sell BTC/USD' offer with price 6019.2744 (5.36% below market price) and payment method " + "'Perfect Money' was published to the Haveno offerbook.\n" + "Offer ID: wygiaw.", shortId, MobileMessageType.MARKET); } } ================================================ FILE: core/src/main/java/haveno/core/notifications/alerts/price/PriceAlert.java ================================================ /* * 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 . */ package haveno.core.notifications.alerts.price; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.util.MathUtils; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.TraditionalMoney; import haveno.core.notifications.MobileMessage; import haveno.core.notifications.MobileMessageType; import haveno.core.notifications.MobileNotificationService; import haveno.core.provider.price.MarketPrice; import haveno.core.provider.price.PriceFeedService; import haveno.core.user.User; import haveno.core.util.FormattingUtils; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton public class PriceAlert { private final PriceFeedService priceFeedService; private final MobileNotificationService mobileNotificationService; private final User user; @Inject public PriceAlert(PriceFeedService priceFeedService, MobileNotificationService mobileNotificationService, User user) { this.priceFeedService = priceFeedService; this.user = user; this.mobileNotificationService = mobileNotificationService; } public void onAllServicesInitialized() { priceFeedService.updateCounterProperty().addListener((observable, oldValue, newValue) -> update()); } private void update() { if (user.getPriceAlertFilter() != null) { PriceAlertFilter filter = user.getPriceAlertFilter(); String currencyCode = filter.getCurrencyCode(); MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); if (marketPrice != null) { int exp = CurrencyUtil.isTraditionalCurrency(currencyCode) ? TraditionalMoney.SMALLEST_UNIT_EXPONENT : CryptoMoney.SMALLEST_UNIT_EXPONENT; double priceAsDouble = marketPrice.getPrice(); long priceAsLong = MathUtils.roundDoubleToLong(MathUtils.scaleUpByPowerOf10(priceAsDouble, exp)); String currencyName = CurrencyUtil.getNameByCode(currencyCode); if (priceAsLong > filter.getHigh() || priceAsLong < filter.getLow()) { String msg = Res.get("account.notifications.priceAlert.message.msg", currencyName, FormattingUtils.formatMarketPrice(priceAsDouble, currencyCode), CurrencyUtil.getCurrencyPair(currencyCode)); MobileMessage message = new MobileMessage(Res.get("account.notifications.priceAlert.message.title", currencyName), msg, MobileMessageType.PRICE); log.error(msg); try { mobileNotificationService.sendMessage(message); // If an alert got triggered we remove the filter. user.removePriceAlertFilter(); } catch (Exception e) { log.error(e.toString()); e.printStackTrace(); } } } } } public static MobileMessage getTestMsg() { String currencyCode = "USD"; String currencyName = CurrencyUtil.getNameByCode(currencyCode); String msg = Res.get("account.notifications.priceAlert.message.msg", currencyName, "6023.34", "BTC/USD"); return new MobileMessage(Res.get("account.notifications.priceAlert.message.title", currencyName), msg, MobileMessageType.PRICE); } } ================================================ FILE: core/src/main/java/haveno/core/notifications/alerts/price/PriceAlertFilter.java ================================================ /* * 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 . */ package haveno.core.notifications.alerts.price; import haveno.common.proto.persistable.PersistablePayload; import lombok.Value; @Value public class PriceAlertFilter implements PersistablePayload { String currencyCode; long high; long low; public PriceAlertFilter(String currencyCode, long high, long low) { this.currencyCode = currencyCode; this.high = high; this.low = low; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.PriceAlertFilter toProtoMessage() { return protobuf.PriceAlertFilter.newBuilder() .setCurrencyCode(currencyCode) .setHigh(high) .setLow(low).build(); } public static PriceAlertFilter fromProto(protobuf.PriceAlertFilter proto) { return new PriceAlertFilter(proto.getCurrencyCode(), proto.getHigh(), proto.getLow()); } } ================================================ FILE: core/src/main/java/haveno/core/offer/AvailabilityResult.java ================================================ /* * 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 . */ package haveno.core.offer; public enum AvailabilityResult { UNKNOWN_FAILURE("cannot take offer for unknown reason"), AVAILABLE("offer available"), OFFER_TAKEN("offer taken"), PRICE_OUT_OF_TOLERANCE("cannot take offer because taker's price is outside tolerance"), MARKET_PRICE_NOT_AVAILABLE("cannot take offer because market price for calculating trade price is unavailable"), @SuppressWarnings("unused") NO_ARBITRATORS("cannot take offer because no arbitrators are available"), NO_MEDIATORS("cannot take offer because no mediators are available"), USER_IGNORED("cannot take offer because user is ignored"), @SuppressWarnings("unused") MISSING_MANDATORY_CAPABILITY("description not available"), @SuppressWarnings("unused") NO_REFUND_AGENTS("cannot take offer because no refund agents are available"), UNCONF_TX_LIMIT_HIT("cannot take offer because you have too many unconfirmed transactions at this moment"), MAKER_DENIED_API_USER("cannot take offer because maker is api user"), PRICE_CHECK_FAILED("cannot take offer because trade price check failed"), MAKER_DENIED_TAKER("cannot take offer because maker denied taker"); private final String description; AvailabilityResult(String description) { this.description = description; } public String description() { return description; } public static AvailabilityResult fromProto(protobuf.AvailabilityResult proto) { return AvailabilityResult.valueOf(proto.name()); } } ================================================ FILE: core/src/main/java/haveno/core/offer/CreateOfferService.java ================================================ /* * 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 . */ package haveno.core.offer; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.app.Version; import haveno.common.crypto.PubKeyRingProvider; import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.core.monetary.Price; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccountUtil; import haveno.core.payment.payload.PaymentMethod; import haveno.core.provider.price.MarketPrice; import haveno.core.provider.price.PriceFeedService; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.trade.HavenoUtils; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.User; import haveno.core.util.coin.CoinUtil; import haveno.core.xmr.wallet.Restrictions; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import java.math.BigInteger; import java.util.Date; import java.util.List; import java.util.Map; import java.util.UUID; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton public class CreateOfferService { private final OfferUtil offerUtil; private final PriceFeedService priceFeedService; private final P2PService p2PService; private final PubKeyRingProvider pubKeyRingProvider; private final User user; private final XmrWalletService xmrWalletService; private final TradeStatisticsManager tradeStatisticsManager; private final ArbitratorManager arbitratorManager; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialization /////////////////////////////////////////////////////////////////////////////////////////// @Inject public CreateOfferService(OfferUtil offerUtil, PriceFeedService priceFeedService, P2PService p2PService, PubKeyRingProvider pubKeyRingProvider, User user, XmrWalletService xmrWalletService, TradeStatisticsManager tradeStatisticsManager, ArbitratorManager arbitratorManager) { this.offerUtil = offerUtil; this.priceFeedService = priceFeedService; this.p2PService = p2PService; this.pubKeyRingProvider = pubKeyRingProvider; this.user = user; this.xmrWalletService = xmrWalletService; this.tradeStatisticsManager = tradeStatisticsManager; this.arbitratorManager = arbitratorManager; } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public String getRandomOfferId() { return Utilities.getRandomPrefix(5, 8) + "-" + UUID.randomUUID().toString() + "-" + Version.VERSION.replace(".", ""); } public Offer createAndGetOffer(String offerId, OfferDirection direction, String currencyCode, BigInteger amount, BigInteger minAmount, Price fixedPrice, boolean useMarketBasedPrice, double marketPriceMarginPct, double securityDepositPct, PaymentAccount paymentAccount, boolean isPrivateOffer, boolean buyerAsTakerWithoutDeposit, String extraInfo) { log.info("Create and get offer with offerId={}, " + "currencyCode={}, " + "direction={}, " + "fixedPrice={}, " + "useMarketBasedPrice={}, " + "marketPriceMarginPct={}, " + "amount={}, " + "minAmount={}, " + "securityDepositPct={}, " + "isPrivateOffer={}, " + "buyerAsTakerWithoutDeposit={}, " + "extraInfo={}", offerId, currencyCode, direction, fixedPrice == null ? null : fixedPrice.getValue(), useMarketBasedPrice, marketPriceMarginPct, amount, minAmount, securityDepositPct, isPrivateOffer, buyerAsTakerWithoutDeposit, extraInfo == null ? null : "\"" + extraInfo + "\""); // must nullify empty string so contracts match if ("".equals(extraInfo)) extraInfo = null; // verify config for private no deposit offers boolean isBuyerMaker = offerUtil.isBuyOffer(direction); if (buyerAsTakerWithoutDeposit || isPrivateOffer) { if (isBuyerMaker) throw new IllegalArgumentException("Buyer must be taker for private offers without deposit"); if (!buyerAsTakerWithoutDeposit) throw new IllegalArgumentException("Must set buyer as taker without deposit for private offers"); if (!isPrivateOffer) throw new IllegalArgumentException("Must set offer to private for buyer as taker without deposit"); } // verify payment account supports trade currency if (paymentAccount.getTradeCurrencies().stream().noneMatch(tradeCurrency -> tradeCurrency.getCode().equals(currencyCode))) { throw new IllegalArgumentException("Payment account does not support trade currency: " + currencyCode); } // verify fixed price xor market price with margin if (fixedPrice != null) { if (useMarketBasedPrice) throw new IllegalArgumentException("Can create offer with fixed price or floating market price but not both"); if (marketPriceMarginPct != 0) throw new IllegalArgumentException("Cannot set market price margin with fixed price"); } // verify price boolean useMarketBasedPriceValue = fixedPrice == null && useMarketBasedPrice && isExternalPriceAvailable(currencyCode) && !PaymentMethod.isFixedPriceOnly(paymentAccount.getPaymentMethod().getId()); if (fixedPrice == null && !useMarketBasedPriceValue) { throw new IllegalArgumentException("Must provide fixed price"); } // verify offer amounts BigInteger maxTradeLimit = offerUtil.getMaxTradeLimitForRelease(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit); BigInteger minTradeLimit = Restrictions.getMinTradeAmount(); if (amount.compareTo(maxTradeLimit) > 0) throw new IllegalArgumentException("Amount must be below maximum amount of " + HavenoUtils.atomicUnitsToXmr(maxTradeLimit) + " XMR"); if (minAmount.compareTo(minTradeLimit) < 0) throw new IllegalArgumentException("Amount must be above minimum amount of " + HavenoUtils.atomicUnitsToXmr(minTradeLimit) + " XMR"); if (amount.compareTo(minAmount) < 0) throw new IllegalArgumentException("Minimum amount is larger than amount"); // adjust amount and min amount amount = CoinUtil.getRoundedAmount(amount, fixedPrice, minTradeLimit, maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId()); minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, minTradeLimit, maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId()); // generate one-time challenge for private offer String challenge = null; String challengeHash = null; if (isPrivateOffer) { challenge = HavenoUtils.generateChallenge(); challengeHash = HavenoUtils.getChallengeHash(challenge); } long creationTime = new Date().getTime(); NodeAddress makerAddress = p2PService.getAddress(); long priceAsLong = fixedPrice != null ? fixedPrice.getValue() : 0L; double marketPriceMarginParam = useMarketBasedPriceValue ? marketPriceMarginPct : 0; long amountAsLong = amount != null ? amount.longValueExact() : 0L; long minAmountAsLong = minAmount != null ? minAmount.longValueExact() : 0L; String baseCurrencyCode = Res.getBaseCurrencyCode(); String counterCurrencyCode = currencyCode; String countryCode = PaymentAccountUtil.getCountryCode(paymentAccount); List acceptedCountryCodes = PaymentAccountUtil.getAcceptedCountryCodes(paymentAccount); String bankId = PaymentAccountUtil.getBankId(paymentAccount); List acceptedBanks = PaymentAccountUtil.getAcceptedBanks(paymentAccount); long maxTradePeriod = paymentAccount.getMaxTradePeriod(); boolean hasBuyerAsTakerWithoutDeposit = !isBuyerMaker && isPrivateOffer && buyerAsTakerWithoutDeposit; long maxTradeLimitAsLong = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, hasBuyerAsTakerWithoutDeposit).longValueExact(); boolean useAutoClose = false; boolean useReOpenAfterAutoClose = false; long lowerClosePrice = 0; long upperClosePrice = 0; Map extraDataMap = offerUtil.getExtraDataMap(paymentAccount, currencyCode, direction); offerUtil.validateOfferData( securityDepositPct, paymentAccount, currencyCode); OfferPayload offerPayload = new OfferPayload(offerId, creationTime, makerAddress, pubKeyRingProvider.get(), OfferDirection.valueOf(direction.name()), priceAsLong, marketPriceMarginParam, useMarketBasedPriceValue, amountAsLong, minAmountAsLong, HavenoUtils.getMakerFeePct(currencyCode, hasBuyerAsTakerWithoutDeposit), HavenoUtils.getTakerFeePct(currencyCode, hasBuyerAsTakerWithoutDeposit), HavenoUtils.PENALTY_FEE_PCT, hasBuyerAsTakerWithoutDeposit ? 0d : securityDepositPct, // buyer as taker security deposit is optional for private offers securityDepositPct, baseCurrencyCode, counterCurrencyCode, paymentAccount.getPaymentMethod().getId(), paymentAccount.getId(), countryCode, acceptedCountryCodes, bankId, acceptedBanks, Version.VERSION, xmrWalletService.getHeight(), maxTradeLimitAsLong, maxTradePeriod, useAutoClose, useReOpenAfterAutoClose, upperClosePrice, lowerClosePrice, isPrivateOffer, challengeHash, extraDataMap, Version.TRADE_PROTOCOL_VERSION, null, null, null, extraInfo); Offer offer = new Offer(offerPayload); offer.setPriceFeedService(priceFeedService); offer.setChallenge(challenge); return offer; } public Offer createClonedOffer(Offer sourceOffer, String currencyCode, Price fixedPrice, boolean useMarketBasedPrice, double marketPriceMargin, PaymentAccount paymentAccount, String extraInfo) { String newOfferId = OfferUtil.getRandomOfferId(); log.info("Creating cloned offer with sourceId={}, " + "newOfferId={}, " + "currencyCode={}, " + "fixedPrice={}, " + "useMarketBasedPrice={}, " + "marketPriceMargin={}, " + "paymentAccountId={}, " + "extraInfo={}", sourceOffer.getId(), newOfferId, currencyCode, fixedPrice == null ? null : fixedPrice.getValue(), useMarketBasedPrice, marketPriceMargin, paymentAccount.getId(), extraInfo); OfferPayload sourceOfferPayload = sourceOffer.getOfferPayload(); Offer editedOffer = createAndGetOffer(newOfferId, sourceOfferPayload.getDirection(), currencyCode, BigInteger.valueOf(sourceOfferPayload.getAmount()), BigInteger.valueOf(sourceOfferPayload.getMinAmount()), fixedPrice, useMarketBasedPrice, marketPriceMargin, sourceOfferPayload.getSellerSecurityDepositPct(), paymentAccount, sourceOfferPayload.isPrivateOffer(), sourceOfferPayload.isBuyerAsTakerWithoutDeposit(), extraInfo); // maker fee cannot change double newMakerFee = HavenoUtils.getMakerFeePct(currencyCode, sourceOfferPayload.isBuyerAsTakerWithoutDeposit()); if (sourceOfferPayload.getMakerFeePct() != newMakerFee) { throw new IllegalArgumentException("Cannot clone offer with different maker fee, source maker fee: " + sourceOfferPayload.getMakerFeePct() + ", new maker fee: " + newMakerFee); } // generate one-time challenge for private offer String challenge = null; String challengeHash = null; if (sourceOfferPayload.isPrivateOffer()) { challenge = HavenoUtils.generateChallenge(); challengeHash = HavenoUtils.getChallengeHash(challenge); } OfferPayload editedOfferPayload = editedOffer.getOfferPayload(); long date = new Date().getTime(); OfferPayload clonedOfferPayload = new OfferPayload(newOfferId, date, sourceOfferPayload.getOwnerNodeAddress(), sourceOfferPayload.getPubKeyRing(), sourceOfferPayload.getDirection(), editedOfferPayload.getPrice(), editedOfferPayload.getMarketPriceMarginPct(), editedOfferPayload.isUseMarketBasedPrice(), sourceOfferPayload.getAmount(), sourceOfferPayload.getMinAmount(), sourceOfferPayload.getMakerFeePct(), HavenoUtils.getTakerFeePct(currencyCode, sourceOfferPayload.isBuyerAsTakerWithoutDeposit()), sourceOfferPayload.getPenaltyFeePct(), sourceOfferPayload.getBuyerSecurityDepositPct(), sourceOfferPayload.getSellerSecurityDepositPct(), editedOfferPayload.getBaseCurrencyCode(), editedOfferPayload.getCounterCurrencyCode(), editedOfferPayload.getPaymentMethodId(), editedOfferPayload.getMakerPaymentAccountId(), editedOfferPayload.getCountryCode(), editedOfferPayload.getAcceptedCountryCodes(), editedOfferPayload.getBankId(), editedOfferPayload.getAcceptedBankIds(), editedOfferPayload.getVersionNr(), sourceOfferPayload.getBlockHeightAtOfferCreation(), editedOfferPayload.getMaxTradeLimit(), editedOfferPayload.getMaxTradePeriod(), sourceOfferPayload.isUseAutoClose(), sourceOfferPayload.isUseReOpenAfterAutoClose(), sourceOfferPayload.getLowerClosePrice(), sourceOfferPayload.getUpperClosePrice(), sourceOfferPayload.isPrivateOffer(), challengeHash, editedOfferPayload.getExtraDataMap(), sourceOfferPayload.getProtocolVersion(), null, null, sourceOfferPayload.getReserveTxKeyImages(), editedOfferPayload.getExtraInfo()); Offer clonedOffer = new Offer(clonedOfferPayload); clonedOffer.setPriceFeedService(priceFeedService); clonedOffer.setChallenge(challenge); clonedOffer.setState(Offer.State.AVAILABLE); return clonedOffer; } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private boolean isExternalPriceAvailable(String currencyCode) { MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); return marketPrice != null && marketPrice.isExternallyProvidedPrice(); } } ================================================ FILE: core/src/main/java/haveno/core/offer/MarketPriceNotAvailableException.java ================================================ /* * 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 . */ package haveno.core.offer; public class MarketPriceNotAvailableException extends Exception { public MarketPriceNotAvailableException(@SuppressWarnings("SameParameterValue") String message) { super(message); } } ================================================ FILE: core/src/main/java/haveno/core/offer/Offer.java ================================================ /* * 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 . */ package haveno.core.offer; import haveno.common.ThreadUtils; import haveno.common.crypto.KeyRing; import haveno.common.crypto.PubKeyRing; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.common.proto.network.NetworkPayload; import haveno.common.proto.persistable.PersistablePayload; import haveno.common.util.JsonExclude; import haveno.common.util.MathUtils; import haveno.common.util.Utilities; import haveno.core.exceptions.TradePriceOutOfToleranceException; import haveno.core.locale.CurrencyUtil; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.Price; import haveno.core.monetary.TraditionalMoney; import haveno.core.monetary.Volume; import haveno.core.offer.availability.OfferAvailabilityModel; import haveno.core.offer.availability.OfferAvailabilityProtocol; import haveno.core.payment.payload.PaymentMethod; import haveno.core.provider.price.MarketPrice; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.HavenoUtils; import haveno.core.util.PriceUtil; import haveno.core.util.VolumeUtil; import haveno.core.util.coin.CoinUtil; import haveno.network.p2p.NodeAddress; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyStringProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.math.BigInteger; import java.security.PublicKey; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Optional; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class Offer implements NetworkPayload, PersistablePayload { // We allow max. difference between own offerPayload price calculation and takers calculation. // Market price might be different at maker's and takers side so we need a bit of tolerance. // The tolerance will get smaller once we have multiple price feeds avoiding fast price fluctuations // from one provider. private final static double PRICE_TOLERANCE = 0.006; /////////////////////////////////////////////////////////////////////////////////////////// // Enums /////////////////////////////////////////////////////////////////////////////////////////// public enum State { UNKNOWN, OFFER_FEE_RESERVED, AVAILABLE, NOT_AVAILABLE, REMOVED, MAKER_OFFLINE, INVALID } /////////////////////////////////////////////////////////////////////////////////////////// // Instance fields /////////////////////////////////////////////////////////////////////////////////////////// @Getter private final OfferPayload offerPayload; @JsonExclude @Getter final transient private ObjectProperty stateProperty = new SimpleObjectProperty<>(Offer.State.UNKNOWN); @JsonExclude @Nullable transient private OfferAvailabilityProtocol availabilityProtocol; @JsonExclude @Getter final transient private StringProperty errorMessageProperty = new SimpleStringProperty(); @JsonExclude @Nullable @Setter transient private PriceFeedService priceFeedService; // Used only as cache @Nullable @JsonExclude transient private String currencyCode; @JsonExclude @Getter @Setter transient private boolean isReservedFundsSpent; @JsonExclude @Getter @Setter @Nullable transient private String challenge; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public Offer(OfferPayload offerPayload) { this.offerPayload = offerPayload; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.Offer toProtoMessage() { return protobuf.Offer.newBuilder().setOfferPayload(offerPayload.toProtoMessage().getOfferPayload()).build(); } public static Offer fromProto(protobuf.Offer proto) { return new Offer(OfferPayload.fromProto(proto.getOfferPayload())); } /////////////////////////////////////////////////////////////////////////////////////////// // Availability /////////////////////////////////////////////////////////////////////////////////////////// public synchronized void checkOfferAvailability(OfferAvailabilityModel model, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { availabilityProtocol = new OfferAvailabilityProtocol(model, () -> { cancelAvailabilityRequest(); new Thread(() -> resultHandler.handleResult()).start(); }, (errorMessage) -> { if (availabilityProtocol != null) availabilityProtocol.cancel(); log.error(errorMessage); errorMessageHandler.handleErrorMessage(errorMessage); }); ThreadUtils.submitToPool((() -> { availabilityProtocol.sendOfferAvailabilityRequest(); })); } public void cancelAvailabilityRequest() { if (availabilityProtocol != null) availabilityProtocol.cancel(); } @Nullable public Price getPrice() { String counterCurrencyCode = getCounterCurrencyCode(); if (!offerPayload.isUseMarketBasedPrice()) { return Price.valueOf(counterCurrencyCode, isInverted() ? PriceUtil.invertLongPrice(offerPayload.getPrice(), counterCurrencyCode) : offerPayload.getPrice()); } checkNotNull(priceFeedService, "priceFeed must not be null"); MarketPrice marketPrice = priceFeedService.getMarketPrice(counterCurrencyCode); if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) { double factor; double marketPriceMargin = offerPayload.getMarketPriceMarginPct(); factor = getDirection() == OfferDirection.BUY ? 1 - marketPriceMargin : 1 + marketPriceMargin; double marketPriceAsDouble = marketPrice.getPrice(); double targetPriceAsDouble = marketPriceAsDouble * factor; try { int precision = CurrencyUtil.isTraditionalCurrency(counterCurrencyCode) ? TraditionalMoney.SMALLEST_UNIT_EXPONENT : CryptoMoney.SMALLEST_UNIT_EXPONENT; double scaled = MathUtils.scaleUpByPowerOf10(targetPriceAsDouble, precision); final long roundedToLong = MathUtils.roundDoubleToLong(scaled); return Price.valueOf(counterCurrencyCode, roundedToLong); } catch (Exception e) { log.error("Exception at getPrice / parseToFiat: " + e + "\n" + "That case should never happen."); return null; } } else { log.trace("We don't have a market price. " + "That case could only happen if you don't have a price feed."); return null; } } public long getFixedPrice() { return offerPayload.getPrice(); } public void verifyTradePrice(long price) throws TradePriceOutOfToleranceException, MarketPriceNotAvailableException, IllegalArgumentException { if (!isUseMarketBasedPrice()) { checkArgument(price == getFixedPrice(), "Takers price does not match offer price. " + "Takers price=" + price + "; offer price=" + getFixedPrice()); return; } Price tradePrice = Price.valueOf(getCounterCurrencyCode(), price); Price offerPrice = getPrice(); if (offerPrice == null) throw new MarketPriceNotAvailableException("Market price required for calculating trade price is not available."); checkArgument(price > 0, "takersTradePrice must be positive"); double relation = (double) price / (double) offerPrice.getValue(); double deviation = Math.abs(1 - relation); log.info("Price at take-offer time: id={}, currency={}, takersPrice={}, makersPrice={}, deviation={}", getShortId(), getCounterCurrencyCode(), price, offerPrice.getValue(), deviation * 100 + "%"); if (deviation > PRICE_TOLERANCE) { String msg = "Taker's trade price is too far away from our calculated price based on the market price.\n" + "takersPrice=" + tradePrice.getValue() + "\n" + "makersPrice=" + offerPrice.getValue(); log.warn(msg); throw new TradePriceOutOfToleranceException(msg); } } @Nullable public Volume getVolumeByAmount(BigInteger amount, BigInteger minAmount, BigInteger maxAmount) { Price price = getPrice(); if (price == null || amount == null) { return null; } BigInteger adjustedAmount = CoinUtil.getRoundedAmount(amount, price, minAmount, maxAmount, getCounterCurrencyCode(), getPaymentMethodId()); Volume volumeByAmount = price.getVolumeByAmount(adjustedAmount); volumeByAmount = VolumeUtil.getAdjustedVolume(volumeByAmount, getPaymentMethod().getId()); return volumeByAmount; } public void resetState() { setState(Offer.State.UNKNOWN); } /////////////////////////////////////////////////////////////////////////////////////////// // Setter /////////////////////////////////////////////////////////////////////////////////////////// public void setState(Offer.State state) { stateProperty.set(state); } public ObjectProperty stateProperty() { return stateProperty; } public void setErrorMessage(String errorMessage) { errorMessageProperty.set(errorMessage); } /////////////////////////////////////////////////////////////////////////////////////////// // Getter /////////////////////////////////////////////////////////////////////////////////////////// // amount needed for the maker to reserve the offer public BigInteger getAmountNeeded() { BigInteger amountNeeded = getDirection() == OfferDirection.BUY ? getMaxBuyerSecurityDeposit() : getMaxSellerSecurityDeposit(); if (getDirection() == OfferDirection.SELL) amountNeeded = amountNeeded.add(getAmount()); amountNeeded = amountNeeded.add(getMaxMakerFee()); return amountNeeded; } // amount reserved for offer public BigInteger getReservedAmount() { if (offerPayload.getReserveTxKeyImages() == null) return null; return HavenoUtils.xmrWalletService.getOutputsAmount(offerPayload.getReserveTxKeyImages()); } public BigInteger getMaxMakerFee() { return offerPayload.getMaxMakerFee(); } public BigInteger getMaxBuyerSecurityDeposit() { return offerPayload.getMaxBuyerSecurityDeposit(); } public BigInteger getMaxSellerSecurityDeposit() { return offerPayload.getMaxSellerSecurityDeposit(); } public double getMakerFeePct() { return offerPayload.getMakerFeePct(); } public double getTakerFeePct() { return offerPayload.getTakerFeePct(); } public double getPenaltyFeePct() { return offerPayload.getPenaltyFeePct(); } public BigInteger getMakerFee(BigInteger tradeAmount) { return HavenoUtils.multiply(tradeAmount, getMakerFeePct()); } public BigInteger getTakerFee(BigInteger tradeAmount) { return HavenoUtils.multiply(tradeAmount, getTakerFeePct()); } public double getBuyerSecurityDepositPct() { return offerPayload.getBuyerSecurityDepositPct(); } public double getSellerSecurityDepositPct() { return offerPayload.getSellerSecurityDepositPct(); } public boolean isPrivateOffer() { return offerPayload.isPrivateOffer(); } public String getChallengeHash() { return offerPayload.getChallengeHash(); } public boolean hasBuyerAsTakerWithoutDeposit() { return getDirection() == OfferDirection.SELL && getBuyerSecurityDepositPct() == 0; } public BigInteger getMaxTradeLimit() { return BigInteger.valueOf(offerPayload.getMaxTradeLimit()); } public BigInteger getAmount() { return BigInteger.valueOf(offerPayload.getAmount()); } public BigInteger getMinAmount() { return BigInteger.valueOf(offerPayload.getMinAmount()); } public boolean isRange() { return offerPayload.getAmount() != offerPayload.getMinAmount(); } public Date getDate() { return new Date(offerPayload.getDate()); } public PaymentMethod getPaymentMethod() { return PaymentMethod.getPaymentMethodOrNA(offerPayload.getPaymentMethodId()); } // utils public String getShortId() { return Utilities.getShortId(offerPayload.getId()); } @Nullable public Volume getVolume() { return getVolumeByAmount(getAmount(), getMinAmount(), getAmount()); } @Nullable public Volume getMinVolume() { return getVolumeByAmount(getMinAmount(), getMinAmount(), getAmount()); } public boolean isBuyOffer() { return getDirection() == OfferDirection.BUY; } public OfferDirection getMirroredDirection() { return getDirection() == OfferDirection.BUY ? OfferDirection.SELL : OfferDirection.BUY; } public boolean isMyOffer(KeyRing keyRing) { return getPubKeyRing().equals(keyRing.getPubKeyRing()); } public Optional getAccountAgeWitnessHashAsHex() { Map extraDataMap = getExtraDataMap(); if (extraDataMap != null && extraDataMap.containsKey(OfferPayload.ACCOUNT_AGE_WITNESS_HASH)) return Optional.of(extraDataMap.get(OfferPayload.ACCOUNT_AGE_WITNESS_HASH)); else return Optional.empty(); } public String getF2FCity() { if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.F2F_CITY)) return getExtraDataMap().get(OfferPayload.F2F_CITY); else return ""; } public String getCombinedExtraInfo() { StringBuilder sb = new StringBuilder(); if (getOfferExtraInfo() != null && !getOfferExtraInfo().isEmpty()) { sb.append(getOfferExtraInfo()); } if (getPaymentAccountExtraInfo() != null && !getPaymentAccountExtraInfo().isEmpty()) { if (sb.length() > 0) sb.append("\n\n"); sb.append(getPaymentAccountExtraInfo()); } return sb.toString(); } public String getOfferExtraInfo() { return offerPayload.getExtraInfo(); } public String getPaymentAccountExtraInfo() { if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.F2F_EXTRA_INFO)) return getExtraDataMap().get(OfferPayload.F2F_EXTRA_INFO); else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.PAY_BY_MAIL_EXTRA_INFO)) return getExtraDataMap().get(OfferPayload.PAY_BY_MAIL_EXTRA_INFO); else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.AUSTRALIA_PAYID_EXTRA_INFO)) return getExtraDataMap().get(OfferPayload.AUSTRALIA_PAYID_EXTRA_INFO); else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.PAYPAL_EXTRA_INFO)) return getExtraDataMap().get(OfferPayload.PAYPAL_EXTRA_INFO); else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.CASHAPP_EXTRA_INFO)) return getExtraDataMap().get(OfferPayload.CASHAPP_EXTRA_INFO); else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.CASH_AT_ATM_EXTRA_INFO)) return getExtraDataMap().get(OfferPayload.CASH_AT_ATM_EXTRA_INFO); else return ""; } public String getPaymentMethodNameWithCountryCode() { String method = this.getPaymentMethod().getShortName(); String methodCountryCode = this.getCountryCode(); if (methodCountryCode != null) method = method + " (" + methodCountryCode + ")"; return method; } // domain properties public Offer.State getState() { return stateProperty.get(); } public ReadOnlyStringProperty errorMessageProperty() { return errorMessageProperty; } public String getErrorMessage() { return errorMessageProperty.get(); } /////////////////////////////////////////////////////////////////////////////////////////// // Delegate Getter (boilerplate code generated via IntelliJ generate delegate feature) /////////////////////////////////////////////////////////////////////////////////////////// public OfferDirection getDirection() { return offerPayload.getDirection(); } public String getId() { return offerPayload.getId(); } @Nullable public List getAcceptedBankIds() { return offerPayload.getAcceptedBankIds(); } @Nullable public String getBankId() { return offerPayload.getBankId(); } @Nullable public List getAcceptedCountryCodes() { return offerPayload.getAcceptedCountryCodes(); } @Nullable public String getCountryCode() { return offerPayload.getCountryCode(); } public String getBaseCurrencyCode() { return isInverted() ? offerPayload.getCounterCurrencyCode() : offerPayload.getBaseCurrencyCode(); // legacy offers inverted crypto } public String getCounterCurrencyCode() { if (currencyCode != null) return currencyCode; currencyCode = isInverted() ? offerPayload.getBaseCurrencyCode() : offerPayload.getCounterCurrencyCode(); // legacy offers inverted crypto return currencyCode; } public boolean isInverted() { return !offerPayload.getBaseCurrencyCode().equals("XMR"); } public String getPaymentMethodId() { return offerPayload.getPaymentMethodId(); } public long getProtocolVersion() { return offerPayload.getProtocolVersion(); } public boolean isUseMarketBasedPrice() { return offerPayload.isUseMarketBasedPrice(); } public double getMarketPriceMarginPct() { return offerPayload.getMarketPriceMarginPct(); } public NodeAddress getMakerNodeAddress() { return offerPayload.getOwnerNodeAddress(); } public PubKeyRing getPubKeyRing() { return offerPayload.getPubKeyRing(); } public String getMakerPaymentAccountId() { return offerPayload.getMakerPaymentAccountId(); } public String getVersionNr() { return offerPayload.getVersionNr(); } public long getMaxTradePeriod() { return offerPayload.getMaxTradePeriod(); } public NodeAddress getOwnerNodeAddress() { return offerPayload.getOwnerNodeAddress(); } // Yet unused public PublicKey getOwnerPubKey() { return offerPayload.getOwnerPubKey(); } @Nullable public Map getExtraDataMap() { return offerPayload.getExtraDataMap(); } public boolean isUseAutoClose() { return offerPayload.isUseAutoClose(); } public boolean isUseReOpenAfterAutoClose() { return offerPayload.isUseReOpenAfterAutoClose(); } public boolean isTraditionalOffer() { return CurrencyUtil.isTraditionalCurrency(currencyCode); } public boolean isFiatOffer() { return CurrencyUtil.isFiatCurrency(currencyCode); } public byte[] getOfferPayloadHash() { return offerPayload.getHash(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Offer offer = (Offer) o; if (offerPayload != null ? !offerPayload.equals(offer.offerPayload) : offer.offerPayload != null) return false; //noinspection SimplifiableIfStatement if (getState() != offer.getState()) return false; return !(getErrorMessage() != null ? !getErrorMessage().equals(offer.getErrorMessage()) : offer.getErrorMessage() != null); } @Override public int hashCode() { int result = offerPayload != null ? offerPayload.hashCode() : 0; result = 31 * result + (getState() != null ? getState().hashCode() : 0); result = 31 * result + (getErrorMessage() != null ? getErrorMessage().hashCode() : 0); return result; } @Override public String toString() { return "Offer{" + "getErrorMessage()='" + getErrorMessage() + '\'' + ", state=" + getState() + ", offerPayload=" + offerPayload + '}'; } } ================================================ FILE: core/src/main/java/haveno/core/offer/OfferBookService.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.offer; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.ThreadUtils; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.config.Config; import haveno.common.file.JsonFileManager; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.core.api.XmrConnectionService; import haveno.core.api.XmrKeyImageListener; import haveno.core.filter.FilterManager; import haveno.core.locale.Res; import haveno.core.provider.price.PriceFeedService; import haveno.core.util.JsonUtil; import haveno.core.xmr.wallet.Restrictions; import haveno.network.p2p.BootstrapListener; import haveno.network.p2p.P2PService; import haveno.network.p2p.storage.HashMapChangedListener; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import haveno.network.utils.Utils; import lombok.extern.slf4j.Slf4j; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; import monero.daemon.model.MoneroKeyImageSpentStatus; /** * Handles validation and announcement of offers added or removed. */ @Slf4j public class OfferBookService { private final static long INVALID_OFFERS_TIMEOUT = 5 * 60 * 1000; // 5 minutes private final P2PService p2PService; private final PriceFeedService priceFeedService; private final List offerBookChangedListeners = new LinkedList<>(); private final FilterManager filterManager; private final JsonFileManager jsonFileManager; private final XmrConnectionService xmrConnectionService; private final List validOffers = new ArrayList(); private final List invalidOffers = new ArrayList(); private final Map invalidOfferTimers = new HashMap<>(); public interface OfferBookChangedListener { void onAdded(Offer offer); void onRemoved(Offer offer); } /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public OfferBookService(P2PService p2PService, PriceFeedService priceFeedService, FilterManager filterManager, XmrConnectionService xmrConnectionService, @Named(Config.STORAGE_DIR) File storageDir, @Named(Config.DUMP_STATISTICS) boolean dumpStatistics) { this.p2PService = p2PService; this.priceFeedService = priceFeedService; this.filterManager = filterManager; this.xmrConnectionService = xmrConnectionService; jsonFileManager = new JsonFileManager(storageDir); // listen for offers p2PService.addHashSetChangedListener(new HashMapChangedListener() { @Override public void onAdded(Collection protectedStorageEntries) { ThreadUtils.execute(() -> { protectedStorageEntries.forEach(protectedStorageEntry -> { if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) { OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); Offer offer = new Offer(offerPayload); offer.setPriceFeedService(priceFeedService); synchronized (validOffers) { try { validateOfferPayload(offerPayload); replaceValidOffer(offer); announceOfferAdded(offer); } catch (IllegalArgumentException e) { log.warn("Ignoring invalid offer {}: {}", offerPayload.getId(), e.getMessage()); } catch (Exception e) { log.warn("Adding offer {} to invalid offers: {}", offerPayload.getId(), e.getMessage()); replaceInvalidOffer(offer); // offer can become valid later } } } }); }, OfferBookService.class.getSimpleName()); } @Override public void onRemoved(Collection protectedStorageEntries) { ThreadUtils.execute(() -> { protectedStorageEntries.forEach(protectedStorageEntry -> { if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) { OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); Offer offer = new Offer(offerPayload); offer.setPriceFeedService(priceFeedService); synchronized (validOffers) { removeValidOffer(offerPayload.getId()); removeInvalidOffer(offerPayload.getId()); announceOfferRemoved(offer); refreshInvalidOffers(); } } }); }, OfferBookService.class.getSimpleName()); } }); if (dumpStatistics) { p2PService.addP2PServiceListener(new BootstrapListener() { @Override public void onDataReceived() { addOfferBookChangedListener(new OfferBookChangedListener() { @Override public void onAdded(Offer offer) { doDumpStatistics(); } @Override public void onRemoved(Offer offer) { doDumpStatistics(); } }); UserThread.runAfter(OfferBookService.this::doDumpStatistics, 1); } }); } // listen for changes to key images xmrConnectionService.getKeyImagePoller().addListener(new XmrKeyImageListener() { @Override public void onSpentStatusChanged(Map spentStatuses) { for (String keyImage : spentStatuses.keySet()) { updateAffectedOffers(keyImage); } } }); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public boolean hasOffer(String offerId) { return hasValidOffer(offerId); } public void addOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { if (filterManager.requireUpdateToNewVersionForTrading()) { errorMessageHandler.handleErrorMessage(Res.get("popup.warning.mandatoryUpdate.trading")); return; } boolean result = p2PService.addProtectedStorageEntry(offer.getOfferPayload()); if (result) { resultHandler.handleResult(); } else { errorMessageHandler.handleErrorMessage("Add offer failed"); } } public void refreshTTL(OfferPayload offerPayload, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { if (filterManager.requireUpdateToNewVersionForTrading()) { errorMessageHandler.handleErrorMessage(Res.get("popup.warning.mandatoryUpdate.trading")); return; } boolean result = p2PService.refreshTTL(offerPayload); if (result) { resultHandler.handleResult(); } else { errorMessageHandler.handleErrorMessage("Refresh TTL failed."); } } public void activateOffer(Offer offer, @Nullable ResultHandler resultHandler, @Nullable ErrorMessageHandler errorMessageHandler) { addOffer(offer, resultHandler, errorMessageHandler); } public void deactivateOffer(OfferPayload offerPayload, @Nullable ResultHandler resultHandler, @Nullable ErrorMessageHandler errorMessageHandler) { removeOffer(offerPayload, resultHandler, errorMessageHandler); } public void removeOffer(OfferPayload offerPayload, @Nullable ResultHandler resultHandler, @Nullable ErrorMessageHandler errorMessageHandler) { if (p2PService.removeData(offerPayload)) { if (resultHandler != null) resultHandler.handleResult(); } else { if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage("Remove offer failed"); } } public List getOffers() { synchronized (validOffers) { return new ArrayList<>(validOffers); } } public List getOffersByCurrency(String direction, String currencyCode) { return getOffers().stream() .filter(o -> o.getOfferPayload().getCounterCurrencyCode().equalsIgnoreCase(currencyCode) && o.getDirection().name() == direction) .collect(Collectors.toList()); } public void removeOfferAtShutDown(OfferPayload offerPayload) { removeOffer(offerPayload, null, null); } public boolean isBootstrapped() { return p2PService.isBootstrapped(); } public void addOfferBookChangedListener(OfferBookChangedListener offerBookChangedListener) { synchronized (offerBookChangedListeners) { offerBookChangedListeners.add(offerBookChangedListener); } } public void shutDown() { xmrConnectionService.getKeyImagePoller().removeKeyImages(OfferBookService.class.getName()); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void announceOfferAdded(Offer offer) { xmrConnectionService.getKeyImagePoller().addKeyImages(offer.getOfferPayload().getReserveTxKeyImages(), OfferBookService.class.getSimpleName()); updateReservedFundsSpentStatus(offer); synchronized (offerBookChangedListeners) { offerBookChangedListeners.forEach(listener -> listener.onAdded(offer)); } } private void announceOfferRemoved(Offer offer) { updateReservedFundsSpentStatus(offer); removeKeyImages(offer); synchronized (offerBookChangedListeners) { offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer)); } } private boolean hasValidOffer(String offerId) { for (Offer offer : getOffers()) { if (offer.getId().equals(offerId)) { return true; } } return false; } private void replaceValidOffer(Offer offer) { synchronized (validOffers) { removeValidOffer(offer.getId()); validOffers.add(offer); } } private void refreshInvalidOffers() { synchronized (invalidOffers) { for (Offer invalidOffer : new ArrayList(invalidOffers)) { try { validateOfferPayload(invalidOffer.getOfferPayload()); removeInvalidOffer(invalidOffer.getId()); replaceValidOffer(invalidOffer); announceOfferAdded(invalidOffer); } catch (Exception e) { // ignore } } } } private void replaceInvalidOffer(Offer offer) { synchronized (invalidOffers) { removeInvalidOffer(offer.getId()); invalidOffers.add(offer); // remove invalid offer after timeout synchronized (invalidOfferTimers) { Timer timer = invalidOfferTimers.get(offer.getId()); if (timer != null) timer.stop(); timer = UserThread.runAfter(() -> { removeInvalidOffer(offer.getId()); }, INVALID_OFFERS_TIMEOUT); invalidOfferTimers.put(offer.getId(), timer); } } } private void removeValidOffer(String offerId) { synchronized (validOffers) { validOffers.removeIf(offer -> offer.getId().equals(offerId)); } } private void removeInvalidOffer(String offerId) { synchronized (invalidOffers) { invalidOffers.removeIf(offer -> offer.getId().equals(offerId)); // remove timeout synchronized (invalidOfferTimers) { Timer timer = invalidOfferTimers.get(offerId); if (timer != null) timer.stop(); invalidOfferTimers.remove(offerId); } } } private void validateOfferPayload(OfferPayload offerPayload) { // validate offer is not banned if (filterManager.isOfferIdBanned(offerPayload.getId())) { throw new IllegalArgumentException("Offer is banned with offerId=" + offerPayload.getId()); } // validate v3 node address compliance boolean isV3NodeAddressCompliant = !OfferRestrictions.requiresNodeAddressUpdate() || Utils.isV3Address(offerPayload.getOwnerNodeAddress().getHostName()); if (!isV3NodeAddressCompliant) { throw new IllegalArgumentException("Offer with non-V3 node address is not allowed with offerId=" + offerPayload.getId()); } // validate market price margin double marketPriceMarginPct = offerPayload.getMarketPriceMarginPct(); if (marketPriceMarginPct <= -1 || marketPriceMarginPct >= 1) { throw new IllegalArgumentException("Market price margin must be greater than -100% and less than 100% but was " + (marketPriceMarginPct * 100) + "% with offerId=" + offerPayload.getId()); } // validate against existing offers synchronized (validOffers) { int numOffersWithSharedKeyImages = 0; for (Offer validOffer : validOffers) { // validate that no offer has overlapping but different key images if (!new HashSet<>(validOffer.getOfferPayload().getReserveTxKeyImages()).equals(new HashSet<>(offerPayload.getReserveTxKeyImages())) && !Collections.disjoint(validOffer.getOfferPayload().getReserveTxKeyImages(), offerPayload.getReserveTxKeyImages())) { throw new RuntimeException("Offer with overlapping but different key images already exists with offerId=" + validOffer.getId()); } // validate that no offer has same key images, payment method, and currency if (!validOffer.getId().equals(offerPayload.getId()) && validOffer.getOfferPayload().getReserveTxKeyImages().equals(offerPayload.getReserveTxKeyImages()) && validOffer.getOfferPayload().getPaymentMethodId().equals(offerPayload.getPaymentMethodId()) && validOffer.getOfferPayload().getBaseCurrencyCode().equals(offerPayload.getBaseCurrencyCode()) && validOffer.getOfferPayload().getCounterCurrencyCode().equals(offerPayload.getCounterCurrencyCode())) { throw new RuntimeException("Offer with same key images, payment method, and currency already exists with offerId=" + validOffer.getId()); } // count offers with same key images if (!validOffer.getId().equals(offerPayload.getId()) && !Collections.disjoint(validOffer.getOfferPayload().getReserveTxKeyImages(), offerPayload.getReserveTxKeyImages())) numOffersWithSharedKeyImages = Math.max(2, numOffersWithSharedKeyImages + 1); } // validate max offers with same key images if (numOffersWithSharedKeyImages > Restrictions.getMaxOffersWithSharedFunds()) throw new RuntimeException("More than " + Restrictions.getMaxOffersWithSharedFunds() + " offers exist with same same key images as new offerId=" + offerPayload.getId()); } } private void removeKeyImages(Offer offer) { Set unsharedKeyImages = new HashSet<>(offer.getOfferPayload().getReserveTxKeyImages()); synchronized (validOffers) { for (Offer validOffer : validOffers) { if (validOffer.getId().equals(offer.getId())) continue; unsharedKeyImages.removeAll(validOffer.getOfferPayload().getReserveTxKeyImages()); } } xmrConnectionService.getKeyImagePoller().removeKeyImages(unsharedKeyImages, OfferBookService.class.getSimpleName()); } private void updateAffectedOffers(String keyImage) { for (Offer offer : getOffers()) { if (offer.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) { updateReservedFundsSpentStatus(offer); synchronized (offerBookChangedListeners) { offerBookChangedListeners.forEach(listener -> { listener.onRemoved(offer); listener.onAdded(offer); }); } } } } private void updateReservedFundsSpentStatus(Offer offer) { for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) { if (Boolean.TRUE.equals(xmrConnectionService.getKeyImagePoller().isSpent(keyImage))) { offer.setReservedFundsSpent(true); } } } private void doDumpStatistics() { // We filter the case that it is a MarketBasedPrice but the price is not available // That should only be possible if the price feed provider is not available final List offerForJsonList = getOffers().stream() .filter(offer -> !offer.isUseMarketBasedPrice() || priceFeedService.getMarketPrice(offer.getCounterCurrencyCode()) != null) .map(offer -> { try { return new OfferForJson(offer.getDirection(), offer.getCounterCurrencyCode(), offer.getMinAmount(), offer.getAmount(), offer.getPrice(), offer.getDate(), offer.getId(), offer.isUseMarketBasedPrice(), offer.getMarketPriceMarginPct(), offer.getPaymentMethod() ); } catch (Throwable t) { // In case an offer was corrupted with null values we ignore it return null; } }) .filter(Objects::nonNull) .collect(Collectors.toList()); jsonFileManager.writeToDiscThreaded(JsonUtil.objectToJson(offerForJsonList), "offers_statistics"); } } ================================================ FILE: core/src/main/java/haveno/core/offer/OfferDirection.java ================================================ /* * 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 . */ package haveno.core.offer; import haveno.common.proto.ProtoUtil; public enum OfferDirection { BUY, SELL; public static OfferDirection fromProto(protobuf.OfferDirection direction) { return ProtoUtil.enumFromProto(OfferDirection.class, direction.name()); } public static protobuf.OfferDirection toProtoMessage(OfferDirection direction) { return protobuf.OfferDirection.valueOf(direction.name()); } } ================================================ FILE: core/src/main/java/haveno/core/offer/OfferFilterService.java ================================================ /* * 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 . */ package haveno.core.offer; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.app.Version; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.filter.FilterManager; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccountUtil; import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; import haveno.core.trade.HavenoUtils; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.network.p2p.P2PService; import java.util.HashMap; import java.util.Map; import java.util.Optional; import javafx.collections.ListChangeListener; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.Coin; @Slf4j @Singleton public class OfferFilterService { private final User user; private final P2PService p2PService; private final Preferences preferences; private final FilterManager filterManager; private final AccountAgeWitnessService accountAgeWitnessService; private final Map insufficientCounterpartyTradeLimitCache = new HashMap<>(); private final Map myInsufficientTradeLimitCache = new HashMap<>(); @Inject public OfferFilterService(User user, P2PService p2PService, Preferences preferences, FilterManager filterManager, AccountAgeWitnessService accountAgeWitnessService) { this.user = user; this.p2PService = p2PService; this.preferences = preferences; this.filterManager = filterManager; this.accountAgeWitnessService = accountAgeWitnessService; if (user != null && user.getPaymentAccountsAsObservable() != null) { // If our accounts have changed we reset our myInsufficientTradeLimitCache as it depends on account data user.getPaymentAccountsAsObservable().addListener((ListChangeListener) c -> myInsufficientTradeLimitCache.clear()); } } public enum Result { VALID(true), API_DISABLED, HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER, HAS_NOT_SAME_PROTOCOL_VERSION, IS_IGNORED, IS_OFFER_BANNED, IS_CURRENCY_BANNED, IS_PAYMENT_METHOD_BANNED, IS_NODE_ADDRESS_BANNED, REQUIRE_UPDATE_TO_NEW_VERSION, IS_INSUFFICIENT_COUNTERPARTY_TRADE_LIMIT, IS_MY_INSUFFICIENT_TRADE_LIMIT, ARBITRATOR_NOT_VALIDATED, SIGNATURE_NOT_VALIDATED, RESERVE_FUNDS_SPENT; @Getter private final boolean isValid; Result(boolean isValid) { this.isValid = isValid; } Result() { this(false); } } public Result canTakeOffer(Offer offer, boolean isTakerApiUser) { if (isTakerApiUser && filterManager.getFilter() != null && filterManager.getFilter().isDisableApi()) { return Result.API_DISABLED; } if (!hasSameProtocolVersion(offer)) { return Result.HAS_NOT_SAME_PROTOCOL_VERSION; } if (isIgnored(offer)) { return Result.IS_IGNORED; } if (isOfferBanned(offer)) { return Result.IS_OFFER_BANNED; } if (isCurrencyBanned(offer)) { return Result.IS_CURRENCY_BANNED; } if (isPaymentMethodBanned(offer)) { return Result.IS_PAYMENT_METHOD_BANNED; } if (isNodeAddressBanned(offer)) { return Result.IS_NODE_ADDRESS_BANNED; } if (requireUpdateToNewVersion()) { return Result.REQUIRE_UPDATE_TO_NEW_VERSION; } if (isInsufficientCounterpartyTradeLimit(offer)) { return Result.IS_INSUFFICIENT_COUNTERPARTY_TRADE_LIMIT; } if (isMyInsufficientTradeLimit(offer)) { return Result.IS_MY_INSUFFICIENT_TRADE_LIMIT; } if (!hasValidArbitrator(offer)) { return Result.ARBITRATOR_NOT_VALIDATED; } if (!hasValidSignature(offer)) { return Result.SIGNATURE_NOT_VALIDATED; } if (isReservedFundsSpent(offer)) { return Result.RESERVE_FUNDS_SPENT; } if (!isAnyPaymentAccountValidForOffer(offer)) { return Result.HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER; } return Result.VALID; } public boolean isAnyPaymentAccountValidForOffer(Offer offer) { return user.getPaymentAccounts() != null && PaymentAccountUtil.isAnyPaymentAccountValidForOffer(offer, user.getPaymentAccounts()); } public boolean hasSameProtocolVersion(Offer offer) { return offer.getProtocolVersion() == Version.TRADE_PROTOCOL_VERSION; } public boolean isIgnored(Offer offer) { return preferences.getIgnoreTradersList().stream() .anyMatch(i -> i.equals(offer.getMakerNodeAddress().getFullAddress())); } public boolean isOfferBanned(Offer offer) { return filterManager.isOfferIdBanned(offer.getId()); } public boolean isCurrencyBanned(Offer offer) { return filterManager.isCurrencyBanned(offer.getCounterCurrencyCode()); } public boolean isPaymentMethodBanned(Offer offer) { return filterManager.isPaymentMethodBanned(offer.getPaymentMethod()); } public boolean isNodeAddressBanned(Offer offer) { return filterManager.isNodeAddressBanned(offer.getMakerNodeAddress()); } public boolean requireUpdateToNewVersion() { return filterManager.requireUpdateToNewVersionForTrading(); } // This call is a bit expensive so we cache results public boolean isInsufficientCounterpartyTradeLimit(Offer offer) { String offerId = offer.getId(); if (insufficientCounterpartyTradeLimitCache.containsKey(offerId)) { return insufficientCounterpartyTradeLimitCache.get(offerId); } boolean result = offer.isTraditionalOffer() && !accountAgeWitnessService.verifyPeersTradeAmount(offer, offer.getAmount(), errorMessage -> { }); insufficientCounterpartyTradeLimitCache.put(offerId, result); return result; } // This call is a bit expensive so we cache results public boolean isMyInsufficientTradeLimit(Offer offer) { String offerId = offer.getId(); if (myInsufficientTradeLimitCache.containsKey(offerId)) { return myInsufficientTradeLimitCache.get(offerId); } Optional accountOptional = PaymentAccountUtil.getMostMaturePaymentAccountForOffer(offer, user.getPaymentAccounts(), accountAgeWitnessService); long myTradeLimit = accountOptional .map(paymentAccount -> accountAgeWitnessService.getMyTradeLimit(paymentAccount, offer.getCounterCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit())) .orElse(0L); long offerMinAmount = offer.getMinAmount().longValueExact(); log.debug("isInsufficientTradeLimit accountOptional={}, myTradeLimit={}, offerMinAmount={}, ", accountOptional.isPresent() ? accountOptional.get().getAccountName() : "null", Coin.valueOf(myTradeLimit).toFriendlyString(), Coin.valueOf(offerMinAmount).toFriendlyString()); boolean result = offer.isTraditionalOffer() && accountOptional.isPresent() && myTradeLimit < offerMinAmount; myInsufficientTradeLimitCache.put(offerId, result); return result; } private boolean hasValidArbitrator(Offer offer) { Arbitrator arbitrator = getArbitrator(offer); return arbitrator != null; } private Arbitrator getArbitrator(Offer offer) { // get arbitrator by address Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(offer.getOfferPayload().getArbitratorSigner()); if (arbitrator != null) return arbitrator; // check if we are the signing arbitrator Arbitrator thisArbitrator = user.getRegisteredArbitrator(); if (thisArbitrator != null && thisArbitrator.getNodeAddress().equals(offer.getOfferPayload().getArbitratorSigner())) return thisArbitrator; // cannot get arbitrator return null; } private boolean hasValidSignature(Offer offer) { Arbitrator arbitrator = getArbitrator(offer); if (arbitrator == null) return false; return HavenoUtils.isArbitratorSignatureValid(offer.getOfferPayload(), arbitrator); } public boolean isReservedFundsSpent(Offer offer) { return offer.isReservedFundsSpent(); } } ================================================ FILE: core/src/main/java/haveno/core/offer/OfferForJson.java ================================================ /* * 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 . */ package haveno.core.offer; import com.fasterxml.jackson.annotation.JsonIgnore; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.Price; import haveno.core.monetary.TraditionalMoney; import haveno.core.monetary.Volume; import haveno.core.payment.payload.PaymentMethod; import haveno.core.trade.HavenoUtils; import org.bitcoinj.utils.MonetaryFormat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.math.BigInteger; import java.util.Date; public class OfferForJson { private static final Logger log = LoggerFactory.getLogger(OfferForJson.class); public final OfferDirection direction; public final String currencyCode; public final long minAmount; public final long amount; public final long price; public final long date; public final boolean useMarketBasedPrice; public final double marketPriceMargin; public final String paymentMethod; public final String id; // primaryMarket fields are based on industry standard where primaryMarket is always in the focus (in the app BTC is always in the focus - will be changed in a larger refactoring once) public String currencyPair; public OfferDirection primaryMarketDirection; public String priceDisplayString; public String primaryMarketAmountDisplayString; public String primaryMarketMinAmountDisplayString; public String primaryMarketVolumeDisplayString; public String primaryMarketMinVolumeDisplayString; public long primaryMarketPrice; public long primaryMarketAmount; public long primaryMarketMinAmount; public long primaryMarketVolume; public long primaryMarketMinVolume; @JsonIgnore transient private final MonetaryFormat traditionalFormat = new MonetaryFormat().shift(0).minDecimals(TraditionalMoney.SMALLEST_UNIT_EXPONENT).repeatOptionalDecimals(0, 0); @JsonIgnore transient private final MonetaryFormat cryptoFormat = new MonetaryFormat().shift(0).minDecimals(CryptoMoney.SMALLEST_UNIT_EXPONENT).repeatOptionalDecimals(0, 0); @JsonIgnore transient private final MonetaryFormat coinFormat = MonetaryFormat.BTC; public OfferForJson(OfferDirection direction, String currencyCode, BigInteger minAmount, BigInteger amount, @Nullable Price price, Date date, String id, boolean useMarketBasedPrice, double marketPriceMargin, PaymentMethod paymentMethod) { this.direction = direction; this.currencyCode = currencyCode; this.minAmount = minAmount.longValueExact(); this.amount = amount.longValueExact(); this.price = price.getValue(); this.date = date.getTime(); this.id = id; this.useMarketBasedPrice = useMarketBasedPrice; this.marketPriceMargin = marketPriceMargin; this.paymentMethod = paymentMethod.getId(); setDisplayStrings(); } private void setDisplayStrings() { try { final Price price = getPrice(); primaryMarketDirection = direction; currencyPair = Res.getBaseCurrencyCode() + "/" + currencyCode; if (CurrencyUtil.isTraditionalCurrency(currencyCode)) { priceDisplayString = traditionalFormat.noCode().format(price.getMonetary()).toString(); primaryMarketMinAmountDisplayString = HavenoUtils.formatXmr(getMinAmount()).toString(); primaryMarketAmountDisplayString = HavenoUtils.formatXmr(getAmount()).toString(); primaryMarketMinVolumeDisplayString = traditionalFormat.noCode().format(getMinVolume().getMonetary()).toString(); primaryMarketVolumeDisplayString = traditionalFormat.noCode().format(getVolume().getMonetary()).toString(); } else { priceDisplayString = cryptoFormat.noCode().format(price.getMonetary()).toString(); primaryMarketMinAmountDisplayString = cryptoFormat.noCode().format(getMinVolume().getMonetary()).toString(); primaryMarketAmountDisplayString = cryptoFormat.noCode().format(getVolume().getMonetary()).toString(); primaryMarketMinVolumeDisplayString = HavenoUtils.formatXmr(getMinAmount()).toString(); primaryMarketVolumeDisplayString = HavenoUtils.formatXmr(getAmount()).toString(); } primaryMarketPrice = price.getValue(); primaryMarketMinAmount = getMinVolume().getValue(); primaryMarketAmount = getVolume().getValue(); primaryMarketMinVolume = getMinAmount().longValueExact(); primaryMarketVolume = getAmount().longValueExact(); } catch (Throwable t) { log.error("Error at setDisplayStrings: " + t.getMessage()); } } private Price getPrice() { return Price.valueOf(currencyCode, price); } private BigInteger getAmount() { return BigInteger.valueOf(amount); } private BigInteger getMinAmount() { return BigInteger.valueOf(minAmount); } private Volume getVolume() { return getPrice().getVolumeByAmount(getAmount()); } private Volume getMinVolume() { return getPrice().getVolumeByAmount(getMinAmount()); } } ================================================ FILE: core/src/main/java/haveno/core/offer/OfferModule.java ================================================ /* * 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 . */ package haveno.core.offer; import com.google.inject.Singleton; import haveno.common.app.AppModule; import haveno.common.config.Config; import lombok.extern.slf4j.Slf4j; @Slf4j public class OfferModule extends AppModule { public OfferModule(Config config) { super(config); } @Override protected final void configure() { bind(OpenOfferManager.class).in(Singleton.class); bind(OfferBookService.class).in(Singleton.class); } } ================================================ FILE: core/src/main/java/haveno/core/offer/OfferPayload.java ================================================ /* * 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 . */ package haveno.core.offer; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonSerializationContext; import com.google.protobuf.ByteString; import haveno.common.crypto.Hash; import haveno.common.crypto.PubKeyRing; import haveno.common.proto.ProtoUtil; import haveno.common.util.CollectionUtils; import haveno.common.util.Hex; import haveno.common.util.JsonExclude; import haveno.common.util.Utilities; import haveno.core.trade.HavenoUtils; import haveno.core.xmr.wallet.Restrictions; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.storage.payload.ExpirablePayload; import haveno.network.p2p.storage.payload.ProtectedStoragePayload; import haveno.network.p2p.storage.payload.RequiresOwnerIsOnlinePayload; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.lang.reflect.Type; import java.math.BigInteger; import java.security.PublicKey; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; // OfferPayload has about 1.4 kb. We should look into options to make it smaller but will be hard to do it in a // backward compatible way. Maybe a candidate when segwit activation is done as hardfork? @EqualsAndHashCode(exclude = {"hash"}) @Getter @Slf4j public final class OfferPayload implements ProtectedStoragePayload, ExpirablePayload, RequiresOwnerIsOnlinePayload { public static final long TTL = TimeUnit.MINUTES.toMillis(11); protected final String id; protected final long date; // For traditional offer the baseCurrencyCode is XMR and the counterCurrencyCode is the traditional currency // For crypto offers it is the opposite. baseCurrencyCode is the crypto and the counterCurrencyCode is XMR. protected final String baseCurrencyCode; protected final String counterCurrencyCode; // price if fixed price is used (usePercentageBasedPrice = false), otherwise 0 protected final long price; protected final long amount; protected final long minAmount; protected final String paymentMethodId; protected final String makerPaymentAccountId; protected final NodeAddress ownerNodeAddress; protected final OfferDirection direction; protected final String versionNr; protected final int protocolVersion; @JsonExclude protected final PubKeyRing pubKeyRing; // cache protected transient byte[] hash; @Nullable protected final Map extraDataMap; // address and signature of signing arbitrator @Setter @Nullable protected NodeAddress arbitratorSigner; @Setter @Nullable protected byte[] arbitratorSignature; @Setter @Nullable protected List reserveTxKeyImages; // Keys for extra map // Only set for traditional offers public static final String ACCOUNT_AGE_WITNESS_HASH = "accountAgeWitnessHash"; public static final String CASHAPP_EXTRA_INFO = "cashAppExtraInfo"; public static final String REFERRAL_ID = "referralId"; // Only used in payment method F2F public static final String F2F_CITY = "f2fCity"; public static final String F2F_EXTRA_INFO = "f2fExtraInfo"; public static final String PAY_BY_MAIL_EXTRA_INFO = "payByMailExtraInfo"; public static final String AUSTRALIA_PAYID_EXTRA_INFO = "australiaPayidExtraInfo"; public static final String PAYPAL_EXTRA_INFO = "payPalExtraInfo"; public static final String CASH_AT_ATM_EXTRA_INFO = "cashAtAtmExtraInfo"; // Comma separated list of ordinal of a haveno.common.app.Capability. E.g. ordinal of // Capability.SIGNED_ACCOUNT_AGE_WITNESS is 11 and Capability.MEDIATION is 12 so if we want to signal that maker // of the offer supports both capabilities we add "11, 12" to capabilities. public static final String CAPABILITIES = "capabilities"; // If maker is seller and has xmrAutoConf enabled it is set to "1" otherwise it is not set public static final String XMR_AUTO_CONF = "xmrAutoConf"; public static final String XMR_AUTO_CONF_ENABLED_VALUE = "1"; /////////////////////////////////////////////////////////////////////////////////////////// // Instance fields /////////////////////////////////////////////////////////////////////////////////////////// // Distance form market price if percentage based price is used (usePercentageBasedPrice = true), otherwise 0. // E.g. 0.1 -> 10%. Can be negative as well. Depending on direction the marketPriceMargin is above or below the market price. // Positive values is always the usual case where you want a better price as the market. // E.g. Buy offer with market price 400.- leads to a 360.- price. // Sell offer with market price 400.- leads to a 440.- price. private final double marketPriceMarginPct; // We use 2 type of prices: fixed price or price based on distance from market price private final boolean useMarketBasedPrice; // Mutable property. Has to be set before offer is saved in P2P network as it changes the payload hash! @Nullable private final String countryCode; @Nullable private final List acceptedCountryCodes; @Nullable private final String bankId; @Nullable private final List acceptedBankIds; private final long blockHeightAtOfferCreation; private final double makerFeePct; private final double takerFeePct; private final double penaltyFeePct; private final double buyerSecurityDepositPct; private final double sellerSecurityDepositPct; private final long maxTradeLimit; private final long maxTradePeriod; // reserved for future use cases // Close offer when certain price is reached private final boolean useAutoClose; // If useReOpenAfterAutoClose=true we re-open a new offer with the remaining funds if the trade amount // was less than the offer's max. trade amount. private final boolean useReOpenAfterAutoClose; // Used when useAutoClose is set for canceling the offer when lowerClosePrice is triggered private final long lowerClosePrice; // Used when useAutoClose is set for canceling the offer when upperClosePrice is triggered private final long upperClosePrice; // Reserved for possible future use to support private trades where the taker needs to have an accessKey private final boolean isPrivateOffer; @Nullable private final String challengeHash; @Nullable private final String extraInfo; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public OfferPayload(String id, long date, @Nullable NodeAddress ownerNodeAddress, PubKeyRing pubKeyRing, OfferDirection direction, long price, double marketPriceMarginPct, boolean useMarketBasedPrice, long amount, long minAmount, double makerFeePct, double takerFeePct, double penaltyFeePct, double buyerSecurityDepositPct, double sellerSecurityDepositPct, String baseCurrencyCode, String counterCurrencyCode, String paymentMethodId, String makerPaymentAccountId, @Nullable String countryCode, @Nullable List acceptedCountryCodes, @Nullable String bankId, @Nullable List acceptedBankIds, String versionNr, long blockHeightAtOfferCreation, long maxTradeLimit, long maxTradePeriod, boolean useAutoClose, boolean useReOpenAfterAutoClose, long lowerClosePrice, long upperClosePrice, boolean isPrivateOffer, @Nullable String challengeHash, @Nullable Map extraDataMap, int protocolVersion, @Nullable NodeAddress arbitratorSigner, @Nullable byte[] arbitratorSignature, @Nullable List reserveTxKeyImages, @Nullable String extraInfo) { this.id = id; this.date = date; this.ownerNodeAddress = ownerNodeAddress; this.pubKeyRing = pubKeyRing; this.baseCurrencyCode = baseCurrencyCode; this.counterCurrencyCode = counterCurrencyCode; this.direction = direction; this.price = price; this.amount = amount; this.minAmount = minAmount; this.makerFeePct = makerFeePct; this.takerFeePct = takerFeePct; this.penaltyFeePct = penaltyFeePct; this.buyerSecurityDepositPct = buyerSecurityDepositPct; this.sellerSecurityDepositPct = sellerSecurityDepositPct; this.paymentMethodId = paymentMethodId; this.makerPaymentAccountId = makerPaymentAccountId; this.extraDataMap = extraDataMap; this.versionNr = versionNr; this.protocolVersion = protocolVersion; this.arbitratorSigner = arbitratorSigner; this.arbitratorSignature = arbitratorSignature; this.reserveTxKeyImages = reserveTxKeyImages; this.marketPriceMarginPct = marketPriceMarginPct; this.useMarketBasedPrice = useMarketBasedPrice; this.countryCode = countryCode; this.acceptedCountryCodes = acceptedCountryCodes; this.bankId = bankId; this.acceptedBankIds = acceptedBankIds; this.blockHeightAtOfferCreation = blockHeightAtOfferCreation; this.maxTradeLimit = maxTradeLimit; this.maxTradePeriod = maxTradePeriod; this.useAutoClose = useAutoClose; this.useReOpenAfterAutoClose = useReOpenAfterAutoClose; this.lowerClosePrice = lowerClosePrice; this.upperClosePrice = upperClosePrice; this.isPrivateOffer = isPrivateOffer; this.challengeHash = challengeHash; this.extraInfo = extraInfo; } public byte[] getHash() { if (this.hash == null) { this.hash = Hash.getSha256Hash(this.toProtoMessage().toByteArray()); } return this.hash; } public byte[] getSignatureHash() { // create copy with ignored fields standardized OfferPayload signee = new OfferPayload( id, date, null, pubKeyRing, direction, price, 0, false, amount, minAmount, makerFeePct, takerFeePct, penaltyFeePct, buyerSecurityDepositPct, sellerSecurityDepositPct, baseCurrencyCode, counterCurrencyCode, paymentMethodId, makerPaymentAccountId, countryCode, acceptedCountryCodes, bankId, acceptedBankIds, versionNr, blockHeightAtOfferCreation, maxTradeLimit, maxTradePeriod, useAutoClose, useReOpenAfterAutoClose, lowerClosePrice, upperClosePrice, isPrivateOffer, challengeHash, extraDataMap, protocolVersion, arbitratorSigner, null, reserveTxKeyImages, null ); return signee.getHash(); } @Override public long getTTL() { return TTL; } @Override public PublicKey getOwnerPubKey() { return pubKeyRing.getSignaturePubKey(); } // In the offer we support base and counter currency // Fiat offers have base currency XMR and counterCurrency Fiat // Cryptos have base currency Crypto and counterCurrency XMR // The rest of the app does not support yet that concept of base currency and counter currencies // so we map here for convenience public String getCurrencyCode() { return getBaseCurrencyCode().equals("XMR") ? getCounterCurrencyCode() : getBaseCurrencyCode(); } public BigInteger getMaxMakerFee() { return HavenoUtils.multiply(BigInteger.valueOf(getAmount()), getMakerFeePct()); } public BigInteger getMaxBuyerSecurityDeposit() { return getBuyerSecurityDepositForTradeAmount(BigInteger.valueOf(getAmount())); } public BigInteger getMaxSellerSecurityDeposit() { return getSellerSecurityDepositForTradeAmount(BigInteger.valueOf(getAmount())); } public BigInteger getBuyerSecurityDepositForTradeAmount(BigInteger tradeAmount) { BigInteger securityDepositUnadjusted = HavenoUtils.multiply(tradeAmount, getBuyerSecurityDepositPct()); boolean isBuyerTaker = getDirection() == OfferDirection.SELL; if (isPrivateOffer() && isBuyerTaker) { return securityDepositUnadjusted; } else { return Restrictions.getMinSecurityDeposit().max(securityDepositUnadjusted); } } public BigInteger getSellerSecurityDepositForTradeAmount(BigInteger tradeAmount) { BigInteger securityDepositUnadjusted = HavenoUtils.multiply(tradeAmount, getSellerSecurityDepositPct()); return Restrictions.getMinSecurityDeposit().max(securityDepositUnadjusted); } public boolean isBuyerAsTakerWithoutDeposit() { return getDirection() == OfferDirection.SELL && getBuyerSecurityDepositPct() == 0; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.StoragePayload toProtoMessage() { protobuf.OfferPayload.Builder builder = protobuf.OfferPayload.newBuilder() .setId(id) .setDate(date) .setPubKeyRing(pubKeyRing.toProtoMessage()) .setDirection(OfferDirection.toProtoMessage(direction)) .setPrice(price) .setMarketPriceMarginPct(marketPriceMarginPct) .setUseMarketBasedPrice(useMarketBasedPrice) .setAmount(amount) .setMinAmount(minAmount) .setMakerFeePct(makerFeePct) .setTakerFeePct(takerFeePct) .setPenaltyFeePct(penaltyFeePct) .setBuyerSecurityDepositPct(buyerSecurityDepositPct) .setSellerSecurityDepositPct(sellerSecurityDepositPct) .setBaseCurrencyCode(baseCurrencyCode) .setCounterCurrencyCode(counterCurrencyCode) .setPaymentMethodId(paymentMethodId) .setMakerPaymentAccountId(makerPaymentAccountId) .setVersionNr(versionNr) .setBlockHeightAtOfferCreation(blockHeightAtOfferCreation) .setMaxTradeLimit(maxTradeLimit) .setMaxTradePeriod(maxTradePeriod) .setUseAutoClose(useAutoClose) .setUseReOpenAfterAutoClose(useReOpenAfterAutoClose) .setLowerClosePrice(lowerClosePrice) .setUpperClosePrice(upperClosePrice) .setIsPrivateOffer(isPrivateOffer) .setProtocolVersion(protocolVersion); Optional.ofNullable(ownerNodeAddress).ifPresent(e -> builder.setOwnerNodeAddress(ownerNodeAddress.toProtoMessage())); Optional.ofNullable(countryCode).ifPresent(builder::setCountryCode); Optional.ofNullable(bankId).ifPresent(builder::setBankId); Optional.ofNullable(acceptedBankIds).ifPresent(builder::addAllAcceptedBankIds); Optional.ofNullable(acceptedCountryCodes).ifPresent(builder::addAllAcceptedCountryCodes); Optional.ofNullable(challengeHash).ifPresent(builder::setChallengeHash); Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); Optional.ofNullable(arbitratorSigner).ifPresent(e -> builder.setArbitratorSigner(arbitratorSigner.toProtoMessage())); Optional.ofNullable(arbitratorSignature).ifPresent(e -> builder.setArbitratorSignature(ByteString.copyFrom(e))); Optional.ofNullable(reserveTxKeyImages).ifPresent(builder::addAllReserveTxKeyImages); Optional.ofNullable(extraInfo).ifPresent(builder::setExtraInfo); return protobuf.StoragePayload.newBuilder().setOfferPayload(builder).build(); } public static OfferPayload fromProto(protobuf.OfferPayload proto) { List acceptedBankIds = proto.getAcceptedBankIdsList().isEmpty() ? null : new ArrayList<>(proto.getAcceptedBankIdsList()); List acceptedCountryCodes = proto.getAcceptedCountryCodesList().isEmpty() ? null : new ArrayList<>(proto.getAcceptedCountryCodesList()); List reserveTxKeyImages = proto.getReserveTxKeyImagesList().isEmpty() ? null : new ArrayList<>(proto.getReserveTxKeyImagesList()); Map extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap(); return new OfferPayload(proto.getId(), proto.getDate(), proto.hasOwnerNodeAddress() ? NodeAddress.fromProto(proto.getOwnerNodeAddress()) : null, PubKeyRing.fromProto(proto.getPubKeyRing()), OfferDirection.fromProto(proto.getDirection()), proto.getPrice(), proto.getMarketPriceMarginPct(), proto.getUseMarketBasedPrice(), proto.getAmount(), proto.getMinAmount(), proto.getMakerFeePct(), proto.getTakerFeePct(), proto.getPenaltyFeePct(), proto.getBuyerSecurityDepositPct(), proto.getSellerSecurityDepositPct(), proto.getBaseCurrencyCode(), proto.getCounterCurrencyCode(), proto.getPaymentMethodId(), proto.getMakerPaymentAccountId(), ProtoUtil.stringOrNullFromProto(proto.getCountryCode()), acceptedCountryCodes, ProtoUtil.stringOrNullFromProto(proto.getBankId()), acceptedBankIds, proto.getVersionNr(), proto.getBlockHeightAtOfferCreation(), proto.getMaxTradeLimit(), proto.getMaxTradePeriod(), proto.getUseAutoClose(), proto.getUseReOpenAfterAutoClose(), proto.getLowerClosePrice(), proto.getUpperClosePrice(), proto.getIsPrivateOffer(), ProtoUtil.stringOrNullFromProto(proto.getChallengeHash()), extraDataMapMap, proto.getProtocolVersion(), proto.hasArbitratorSigner() ? NodeAddress.fromProto(proto.getArbitratorSigner()) : null, ProtoUtil.byteArrayOrNullFromProto(proto.getArbitratorSignature()), reserveTxKeyImages, ProtoUtil.stringOrNullFromProto(proto.getExtraInfo())); } @Override public String toString() { return "OfferPayload{" + "\r\n id='" + id + '\'' + ",\r\n date=" + date + ",\r\n baseCurrencyCode='" + baseCurrencyCode + '\'' + ",\r\n counterCurrencyCode='" + counterCurrencyCode + '\'' + ",\r\n price=" + price + ",\r\n amount=" + amount + ",\r\n minAmount=" + minAmount + ",\r\n makerFeePct=" + makerFeePct + ",\r\n takerFeePct=" + takerFeePct + ",\r\n penaltyFeePct=" + penaltyFeePct + ",\r\n buyerSecurityDepositPct=" + buyerSecurityDepositPct + ",\r\n sellerSecurityDeposiPct=" + sellerSecurityDepositPct + ",\r\n paymentMethodId='" + paymentMethodId + '\'' + ",\r\n makerPaymentAccountId='" + makerPaymentAccountId + '\'' + ",\r\n ownerNodeAddress=" + ownerNodeAddress + ",\r\n direction=" + direction + ",\r\n versionNr='" + versionNr + '\'' + ",\r\n protocolVersion=" + protocolVersion + ",\r\n pubKeyRing=" + pubKeyRing + ",\r\n hash=" + (hash != null ? Hex.encode(hash) : "null") + ",\r\n extraDataMap=" + extraDataMap + ",\r\n reserveTxKeyImages=" + reserveTxKeyImages + ",\r\n marketPriceMargin=" + marketPriceMarginPct + ",\r\n useMarketBasedPrice=" + useMarketBasedPrice + ",\r\n countryCode='" + countryCode + '\'' + ",\r\n acceptedCountryCodes=" + acceptedCountryCodes + ",\r\n bankId='" + bankId + '\'' + ",\r\n acceptedBankIds=" + acceptedBankIds + ",\r\n blockHeightAtOfferCreation=" + blockHeightAtOfferCreation + ",\r\n maxTradeLimit=" + maxTradeLimit + ",\r\n maxTradePeriod=" + maxTradePeriod + ",\r\n useAutoClose=" + useAutoClose + ",\r\n useReOpenAfterAutoClose=" + useReOpenAfterAutoClose + ",\r\n lowerClosePrice=" + lowerClosePrice + ",\r\n upperClosePrice=" + upperClosePrice + ",\r\n isPrivateOffer=" + isPrivateOffer + ",\r\n challengeHash='" + challengeHash + '\'' + ",\r\n arbitratorSigner=" + arbitratorSigner + ",\r\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) + ",\r\n extraInfo='" + extraInfo + '\'' + "\r\n} "; } // For backward compatibility we need to ensure same order for json fields as with 1.7.5. and earlier versions. // The json is used for the hash in the contract and change of order would cause a different hash and // therefore a failure during trade. public static class JsonSerializer implements com.google.gson.JsonSerializer { @Override public JsonElement serialize(OfferPayload offerPayload, Type type, JsonSerializationContext context) { JsonObject object = new JsonObject(); object.add("id", context.serialize(offerPayload.getId())); object.add("date", context.serialize(offerPayload.getDate())); object.add("ownerNodeAddress", context.serialize(offerPayload.getOwnerNodeAddress())); object.add("direction", context.serialize(offerPayload.getDirection())); object.add("price", context.serialize(offerPayload.getPrice())); object.add("marketPriceMargin", context.serialize(offerPayload.getMarketPriceMarginPct())); object.add("useMarketBasedPrice", context.serialize(offerPayload.isUseMarketBasedPrice())); object.add("amount", context.serialize(offerPayload.getAmount())); object.add("minAmount", context.serialize(offerPayload.getMinAmount())); object.add("makerFeePct", context.serialize(offerPayload.getMakerFeePct())); object.add("takerFeePct", context.serialize(offerPayload.getTakerFeePct())); object.add("penaltyFeePct", context.serialize(offerPayload.getPenaltyFeePct())); object.add("buyerSecurityDepositPct", context.serialize(offerPayload.getBuyerSecurityDepositPct())); object.add("sellerSecurityDepositPct", context.serialize(offerPayload.getSellerSecurityDepositPct())); object.add("baseCurrencyCode", context.serialize(offerPayload.getBaseCurrencyCode())); object.add("counterCurrencyCode", context.serialize(offerPayload.getCounterCurrencyCode())); object.add("paymentMethodId", context.serialize(offerPayload.getPaymentMethodId())); object.add("makerPaymentAccountId", context.serialize(offerPayload.getMakerPaymentAccountId())); object.add("versionNr", context.serialize(offerPayload.getVersionNr())); object.add("blockHeightAtOfferCreation", context.serialize(offerPayload.getBlockHeightAtOfferCreation())); object.add("maxTradeLimit", context.serialize(offerPayload.getMaxTradeLimit())); object.add("maxTradePeriod", context.serialize(offerPayload.getMaxTradePeriod())); object.add("useAutoClose", context.serialize(offerPayload.isUseAutoClose())); object.add("useReOpenAfterAutoClose", context.serialize(offerPayload.isUseReOpenAfterAutoClose())); object.add("lowerClosePrice", context.serialize(offerPayload.getLowerClosePrice())); object.add("upperClosePrice", context.serialize(offerPayload.getUpperClosePrice())); object.add("isPrivateOffer", context.serialize(offerPayload.isPrivateOffer())); object.add("extraDataMap", context.serialize(offerPayload.getExtraDataMap())); object.add("protocolVersion", context.serialize(offerPayload.getProtocolVersion())); object.add("arbitratorSigner", context.serialize(offerPayload.getArbitratorSigner())); object.add("arbitratorSignature", context.serialize(offerPayload.getArbitratorSignature())); object.add("extraInfo", context.serialize(offerPayload.getExtraInfo())); // reserveTxKeyImages and challengeHash are purposely excluded because they are not relevant to existing trades and would break existing contracts return object; } } } ================================================ FILE: core/src/main/java/haveno/core/offer/OfferRestrictions.java ================================================ /* * 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 . */ package haveno.core.offer; import haveno.common.app.Capabilities; import haveno.common.app.Capability; import haveno.common.config.Config; import haveno.common.util.Utilities; import haveno.core.trade.HavenoUtils; import java.math.BigInteger; import java.util.Date; import java.util.GregorianCalendar; import java.util.Map; public class OfferRestrictions { // The date when traders who have not upgraded to a Tor v3 Node Address cannot take offers and their offers become // invisible. private static final Date REQUIRE_TOR_NODE_ADDRESS_V3_DATE = Utilities.getUTCDate(2021, GregorianCalendar.AUGUST, 15); public static boolean requiresNodeAddressUpdate() { return new Date().after(REQUIRE_TOR_NODE_ADDRESS_V3_DATE) && Config.baseCurrencyNetwork().isMainnet(); } public static BigInteger TOLERATED_SMALL_TRADE_AMOUNT = HavenoUtils.xmrToAtomicUnits(3); static boolean hasOfferMandatoryCapability(Offer offer, Capability mandatoryCapability) { Map extraDataMap = offer.getExtraDataMap(); if (extraDataMap != null && extraDataMap.containsKey(OfferPayload.CAPABILITIES)) { String commaSeparatedOrdinals = extraDataMap.get(OfferPayload.CAPABILITIES); Capabilities capabilities = Capabilities.fromStringList(commaSeparatedOrdinals); return Capabilities.hasMandatoryCapability(capabilities, mandatoryCapability); } return false; } } ================================================ FILE: core/src/main/java/haveno/core/offer/OfferUtil.java ================================================ /* * 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 . */ package haveno.core.offer; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.app.Capabilities; import haveno.common.app.Version; import haveno.common.util.MathUtils; import static haveno.common.util.MathUtils.roundDoubleToLong; import static haveno.common.util.MathUtils.scaleUpByPowerOf10; import haveno.common.util.Utilities; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.filter.FilterManager; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.monetary.Price; import haveno.core.monetary.TraditionalMoney; import haveno.core.monetary.Volume; import static haveno.core.offer.OfferPayload.ACCOUNT_AGE_WITNESS_HASH; import static haveno.core.offer.OfferPayload.AUSTRALIA_PAYID_EXTRA_INFO; import static haveno.core.offer.OfferPayload.CAPABILITIES; import static haveno.core.offer.OfferPayload.CASH_AT_ATM_EXTRA_INFO; import static haveno.core.offer.OfferPayload.CASHAPP_EXTRA_INFO; import static haveno.core.offer.OfferPayload.F2F_CITY; import static haveno.core.offer.OfferPayload.F2F_EXTRA_INFO; import static haveno.core.offer.OfferPayload.PAY_BY_MAIL_EXTRA_INFO; import static haveno.core.offer.OfferPayload.PAYPAL_EXTRA_INFO; import static haveno.core.offer.OfferPayload.REFERRAL_ID; import static haveno.core.offer.OfferPayload.XMR_AUTO_CONF; import static haveno.core.offer.OfferPayload.XMR_AUTO_CONF_ENABLED_VALUE; import haveno.core.payment.AustraliaPayidAccount; import haveno.core.payment.CashAppAccount; import haveno.core.payment.CashAtAtmAccount; import haveno.core.payment.F2FAccount; import haveno.core.payment.PayByMailAccount; import haveno.core.payment.PayPalAccount; import haveno.core.payment.PaymentAccount; import haveno.core.provider.price.MarketPrice; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.HavenoUtils; import haveno.core.trade.statistics.ReferralIdService; import haveno.core.user.AutoConfirmSettings; import haveno.core.user.Preferences; import haveno.core.util.coin.CoinFormatter; import static haveno.core.xmr.wallet.Restrictions.getMaxSecurityDepositPct; import static haveno.core.xmr.wallet.Restrictions.getMinSecurityDepositPct; import haveno.network.p2p.P2PService; import java.math.BigInteger; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.UUID; import lombok.extern.slf4j.Slf4j; /** * This class holds utility methods for creating, editing and taking an Offer. */ @Slf4j @Singleton public class OfferUtil { private final AccountAgeWitnessService accountAgeWitnessService; private final FilterManager filterManager; private final Preferences preferences; private final PriceFeedService priceFeedService; private final P2PService p2PService; private final ReferralIdService referralIdService; @Inject public OfferUtil(AccountAgeWitnessService accountAgeWitnessService, FilterManager filterManager, Preferences preferences, PriceFeedService priceFeedService, P2PService p2PService, ReferralIdService referralIdService) { this.accountAgeWitnessService = accountAgeWitnessService; this.filterManager = filterManager; this.preferences = preferences; this.priceFeedService = priceFeedService; this.p2PService = p2PService; this.referralIdService = referralIdService; } public static String getRandomOfferId() { return Utilities.getRandomPrefix(5, 8) + "-" + UUID.randomUUID() + "-" + getStrippedVersion(); } public static String getStrippedVersion() { return Version.VERSION.replace(".", ""); } /** * Given the direction, is this a BUY? * * @param direction the offer direction * @return {@code true} for an offer to buy BTC from the taker, {@code false} for an * offer to sell BTC to the taker */ public boolean isBuyOffer(OfferDirection direction) { return direction == OfferDirection.BUY; } public BigInteger getMaxTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferDirection direction, boolean buyerAsTakerWithoutDeposit) { return BigInteger.valueOf(paymentAccount != null ? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit) : 0); } /** * Return true if a balance can cover a cost. * * @param cost the cost of a trade * @param balance a wallet balance * @return true if balance >= cost */ public boolean isBalanceSufficient(BigInteger cost, BigInteger balance) { return cost != null && balance.compareTo(cost) >= 0; } /** * Return the wallet balance shortage for a given trade cost, or zero if there is * no shortage. * * @param cost the cost of a trade * @param balance a wallet balance * @return the wallet balance shortage for the given cost, else zero. */ public BigInteger getBalanceShortage(BigInteger cost, BigInteger balance) { if (cost != null) { BigInteger shortage = cost.subtract(balance); return shortage.compareTo(BigInteger.ZERO) < 0 ? BigInteger.ZERO : shortage; } else { return BigInteger.ZERO; } } public double calculateManualPrice(double volumeAsDouble, double amountAsDouble) { return volumeAsDouble / amountAsDouble; } public double calculateMarketPriceMarginPct(double manualPrice, double marketPrice) { return MathUtils.roundDouble(manualPrice / marketPrice, 4); } public boolean isBlockChainPaymentMethod(Offer offer) { return offer != null && offer.getPaymentMethod().isBlockchain(); } public Optional getFeeInUserFiatCurrency(BigInteger makerFee, CoinFormatter formatter) { String userCurrencyCode = preferences.getPreferredTradeCurrency().getCode(); if (CurrencyUtil.isCryptoCurrency(userCurrencyCode)) { // In case the user has selected a crypto as preferredTradeCurrency // we derive the fiat currency from the user country String countryCode = preferences.getUserCountry().code; userCurrencyCode = CurrencyUtil.getCurrencyByCountryCode(countryCode).getCode(); } return getFeeInUserFiatCurrency(makerFee, userCurrencyCode, formatter); } public Map getExtraDataMap(PaymentAccount paymentAccount, String currencyCode, OfferDirection direction) { Map extraDataMap = new HashMap<>(); if (CurrencyUtil.isTraditionalCurrency(currencyCode)) { String myWitnessHashAsHex = accountAgeWitnessService .getMyWitnessHashAsHex(paymentAccount.getPaymentAccountPayload()); extraDataMap.put(ACCOUNT_AGE_WITNESS_HASH, myWitnessHashAsHex); } if (referralIdService.getOptionalReferralId().isPresent()) { extraDataMap.put(REFERRAL_ID, referralIdService.getOptionalReferralId().get()); } if (paymentAccount instanceof F2FAccount) { extraDataMap.put(F2F_CITY, ((F2FAccount) paymentAccount).getCity()); extraDataMap.put(F2F_EXTRA_INFO, ((F2FAccount) paymentAccount).getExtraInfo()); } if (paymentAccount instanceof PayByMailAccount) { extraDataMap.put(PAY_BY_MAIL_EXTRA_INFO, ((PayByMailAccount) paymentAccount).getExtraInfo()); } if (paymentAccount instanceof PayPalAccount) { extraDataMap.put(PAYPAL_EXTRA_INFO, ((PayPalAccount) paymentAccount).getExtraInfo()); } if (paymentAccount instanceof CashAppAccount) { extraDataMap.put(CASHAPP_EXTRA_INFO, ((CashAppAccount) paymentAccount).getExtraInfo()); } if (paymentAccount instanceof AustraliaPayidAccount) { extraDataMap.put(AUSTRALIA_PAYID_EXTRA_INFO, ((AustraliaPayidAccount) paymentAccount).getExtraInfo()); } if (paymentAccount instanceof CashAtAtmAccount) { extraDataMap.put(CASH_AT_ATM_EXTRA_INFO, ((CashAtAtmAccount) paymentAccount).getExtraInfo()); } extraDataMap.put(CAPABILITIES, Capabilities.app.toStringList()); if (currencyCode.equals("XMR") && direction == OfferDirection.SELL) { preferences.getAutoConfirmSettingsList().stream() .filter(e -> e.getCurrencyCode().equals("XMR")) .filter(AutoConfirmSettings::isEnabled) .forEach(e -> extraDataMap.put(XMR_AUTO_CONF, XMR_AUTO_CONF_ENABLED_VALUE)); } return extraDataMap.isEmpty() ? null : extraDataMap; } public void validateOfferData(double securityDeposit, PaymentAccount paymentAccount, String currencyCode) { checkNotNull(p2PService.getAddress(), "Address must not be null"); checkArgument(securityDeposit <= getMaxSecurityDepositPct(), "securityDeposit must not exceed " + getMaxSecurityDepositPct()); checkArgument(securityDeposit >= getMinSecurityDepositPct(), "securityDeposit must not be less than " + getMinSecurityDepositPct() + " but was " + securityDeposit); checkArgument(!filterManager.isCurrencyBanned(currencyCode), Res.get("offerbook.warning.currencyBanned")); checkArgument(!filterManager.isPaymentMethodBanned(paymentAccount.getPaymentMethod()), Res.get("offerbook.warning.paymentMethodBanned")); } private Optional getFeeInUserFiatCurrency(BigInteger makerFee, String userCurrencyCode, CoinFormatter formatter) { MarketPrice marketPrice = priceFeedService.getMarketPrice(userCurrencyCode); if (marketPrice != null && makerFee != null) { long marketPriceAsLong = roundDoubleToLong(scaleUpByPowerOf10(marketPrice.getPrice(), TraditionalMoney.SMALLEST_UNIT_EXPONENT)); Price userCurrencyPrice = Price.valueOf(userCurrencyCode, marketPriceAsLong); return Optional.of(userCurrencyPrice.getVolumeByAmount(makerFee)); } else { return Optional.empty(); } } public static boolean isTraditionalOffer(Offer offer) { return CurrencyUtil.isTraditionalCurrency(offer.getCounterCurrencyCode()); } public static boolean isCryptoOffer(Offer offer) { return CurrencyUtil.isCryptoCurrency(offer.getCounterCurrencyCode()); } public BigInteger getMaxTradeLimitForRelease(PaymentAccount paymentAccount, String currencyCode, OfferDirection direction, boolean buyerAsTakerWithoutDeposit) { // disallow offers which no buyer can take due to trade limits on release if (HavenoUtils.isReleasedWithinDays(HavenoUtils.RELEASE_LIMIT_DAYS)) { return BigInteger.valueOf(accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, OfferDirection.BUY, buyerAsTakerWithoutDeposit)); } if (paymentAccount != null) { return BigInteger.valueOf(accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit)); } else { return BigInteger.ZERO; } } } ================================================ FILE: core/src/main/java/haveno/core/offer/OpenOffer.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.offer; import haveno.common.proto.ProtoUtil; import haveno.core.trade.Tradable; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleObjectProperty; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Optional; import java.util.UUID; @EqualsAndHashCode public final class OpenOffer implements Tradable { public enum State { PENDING, AVAILABLE, RESERVED, CLOSED, CANCELED, DEACTIVATED } @Getter private final Offer offer; @Getter private State state; @Setter @Getter private boolean reserveExactAmount; @Setter @Getter @Nullable private String scheduledAmount; @Setter @Getter @Nullable private List scheduledTxHashes; @Setter @Getter @Nullable String splitOutputTxHash; @Getter @Setter long splitOutputTxFee; @Nullable @Setter @Getter private String reserveTxHash; @Nullable @Setter @Getter private String reserveTxHex; @Nullable @Setter @Getter private String reserveTxKey; @Getter @Setter private String challenge; @Getter private final long triggerPrice; @Getter @Setter transient private long mempoolStatus = -1; transient final private ObjectProperty stateProperty = new SimpleObjectProperty<>(state); @Getter @Setter transient boolean isProcessing = false; @Getter @Setter transient int numProcessingAttempts = 0; @Getter @Setter private boolean deactivatedByTrigger; @Getter @Setter private String groupId; public OpenOffer(Offer offer) { this(offer, 0, false); } public OpenOffer(Offer offer, long triggerPrice, boolean reserveExactAmount) { this(offer, triggerPrice, reserveExactAmount, null); } public OpenOffer(Offer offer, long triggerPrice, boolean reserveExactAmount, String groupId) { this.offer = offer; this.triggerPrice = triggerPrice; this.reserveExactAmount = reserveExactAmount; this.challenge = offer.getChallenge(); this.groupId = groupId == null ? UUID.randomUUID().toString() : groupId; state = State.PENDING; } public OpenOffer(Offer offer, long triggerPrice, OpenOffer openOffer) { this.offer = offer; this.triggerPrice = triggerPrice; // copy open offer fields this.state = openOffer.state; this.reserveExactAmount = openOffer.reserveExactAmount; this.scheduledAmount = openOffer.scheduledAmount; this.scheduledTxHashes = openOffer.scheduledTxHashes == null ? null : new ArrayList(openOffer.scheduledTxHashes); this.splitOutputTxHash = openOffer.splitOutputTxHash; this.splitOutputTxFee = openOffer.splitOutputTxFee; this.reserveTxHash = openOffer.reserveTxHash; this.reserveTxHex = openOffer.reserveTxHex; this.reserveTxKey = openOffer.reserveTxKey; this.challenge = openOffer.challenge; this.deactivatedByTrigger = openOffer.deactivatedByTrigger; this.groupId = openOffer.groupId; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private OpenOffer(Offer offer, State state, long triggerPrice, boolean reserveExactAmount, @Nullable String scheduledAmount, @Nullable List scheduledTxHashes, String splitOutputTxHash, long splitOutputTxFee, @Nullable String reserveTxHash, @Nullable String reserveTxHex, @Nullable String reserveTxKey, @Nullable String challenge, boolean deactivatedByTrigger, @Nullable String groupId) { this.offer = offer; this.state = state; this.triggerPrice = triggerPrice; this.reserveExactAmount = reserveExactAmount; this.scheduledTxHashes = scheduledTxHashes; this.splitOutputTxHash = splitOutputTxHash; this.splitOutputTxFee = splitOutputTxFee; this.reserveTxHash = reserveTxHash; this.reserveTxHex = reserveTxHex; this.reserveTxKey = reserveTxKey; this.challenge = challenge; this.deactivatedByTrigger = deactivatedByTrigger; if (groupId == null) groupId = UUID.randomUUID().toString(); // initialize groupId if not set (added in v1.0.19) this.groupId = groupId; // reset reserved state to available if (this.state == State.RESERVED) setState(State.AVAILABLE); } @Override public protobuf.Tradable toProtoMessage() { protobuf.OpenOffer.Builder builder = protobuf.OpenOffer.newBuilder() .setOffer(offer.toProtoMessage()) .setTriggerPrice(triggerPrice) .setState(protobuf.OpenOffer.State.valueOf(state.name())) .setSplitOutputTxFee(splitOutputTxFee) .setReserveExactAmount(reserveExactAmount) .setDeactivatedByTrigger(deactivatedByTrigger); Optional.ofNullable(scheduledAmount).ifPresent(e -> builder.setScheduledAmount(scheduledAmount)); Optional.ofNullable(scheduledTxHashes).ifPresent(e -> builder.addAllScheduledTxHashes(scheduledTxHashes)); Optional.ofNullable(splitOutputTxHash).ifPresent(e -> builder.setSplitOutputTxHash(splitOutputTxHash)); Optional.ofNullable(reserveTxHash).ifPresent(e -> builder.setReserveTxHash(reserveTxHash)); Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex)); Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey)); Optional.ofNullable(challenge).ifPresent(e -> builder.setChallenge(challenge)); Optional.ofNullable(groupId).ifPresent(e -> builder.setGroupId(groupId)); return protobuf.Tradable.newBuilder().setOpenOffer(builder).build(); } public static Tradable fromProto(protobuf.OpenOffer proto) { OpenOffer openOffer = new OpenOffer(Offer.fromProto(proto.getOffer()), ProtoUtil.enumFromProto(OpenOffer.State.class, proto.getState().name()), proto.getTriggerPrice(), proto.getReserveExactAmount(), proto.getScheduledAmount(), proto.getScheduledTxHashesList(), ProtoUtil.stringOrNullFromProto(proto.getSplitOutputTxHash()), proto.getSplitOutputTxFee(), ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()), ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()), ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()), ProtoUtil.stringOrNullFromProto(proto.getChallenge()), proto.getDeactivatedByTrigger(), ProtoUtil.stringOrNullFromProto(proto.getGroupId())); return openOffer; } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// @Override public Date getDate() { return offer.getDate(); } @Override public String getId() { return offer.getId(); } @Override public String getShortId() { return offer.getShortId(); } public void setState(State state) { this.state = state; stateProperty.set(state); if (state == State.AVAILABLE) { deactivatedByTrigger = false; } } public void deactivate(boolean deactivatedByTrigger) { this.deactivatedByTrigger = deactivatedByTrigger; setState(State.DEACTIVATED); } public ReadOnlyObjectProperty stateProperty() { return stateProperty; } public boolean isPending() { return state == State.PENDING; } public boolean isAvailable() { return state == State.AVAILABLE; } public boolean isReserved() { return state == State.RESERVED; } public boolean isDeactivated() { return state == State.DEACTIVATED; } public boolean isCanceled() { return state == State.CANCELED; } @Override public String toString() { return "OpenOffer{" + ",\n offer=" + offer + ",\n state=" + state + ",\n triggerPrice=" + triggerPrice + ",\n reserveExactAmount=" + reserveExactAmount + ",\n scheduledAmount=" + scheduledAmount + ",\n splitOutputTxFee=" + splitOutputTxFee + ",\n groupId=" + groupId + "\n}"; } } ================================================ FILE: core/src/main/java/haveno/core/offer/OpenOfferManager.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.offer; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.collect.ImmutableList; import com.google.inject.Inject; import haveno.common.ThreadUtils; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.app.Capabilities; import haveno.common.app.Capability; import haveno.common.app.Version; import haveno.common.crypto.KeyRing; import haveno.common.crypto.PubKeyRing; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.proto.persistable.PersistedDataHost; import haveno.common.util.Tuple2; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.api.CoreContext; import haveno.core.api.XmrConnectionService; import haveno.core.api.XmrKeyImageListener; import haveno.core.api.XmrKeyImagePoller; import haveno.core.exceptions.TradePriceOutOfToleranceException; import haveno.core.filter.FilterManager; import haveno.core.locale.Res; import haveno.core.offer.messages.OfferAvailabilityRequest; import haveno.core.offer.messages.OfferAvailabilityResponse; import haveno.core.offer.messages.SignOfferRequest; import haveno.core.offer.messages.SignOfferResponse; import haveno.core.offer.placeoffer.PlaceOfferModel; import haveno.core.offer.placeoffer.PlaceOfferProtocol; import haveno.core.offer.placeoffer.tasks.ValidateOffer; import haveno.core.provider.price.PriceFeedService; import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.support.dispute.mediation.mediator.MediatorManager; import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.HavenoUtils; import haveno.core.trade.TradableList; import haveno.core.trade.handlers.TransactionResultHandler; import haveno.core.trade.protocol.TradeProtocol; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.util.JsonUtil; import haveno.core.util.PriceUtil; import haveno.core.util.Validator; import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.wallet.BtcWalletService; import haveno.core.xmr.wallet.Restrictions; import haveno.core.xmr.wallet.TradeWalletService; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.AckMessage; import haveno.network.p2p.AckMessageSourceType; import haveno.network.p2p.BootstrapListener; import haveno.network.p2p.DecryptedDirectMessageListener; import haveno.network.p2p.DecryptedMessageWithPubKey; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import haveno.network.p2p.SendDirectMessageListener; import haveno.network.p2p.peers.Broadcaster; import haveno.network.p2p.peers.PeerManager; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javax.annotation.Nullable; import lombok.Getter; import monero.common.MoneroRpcConnection; import monero.daemon.model.MoneroKeyImageSpentStatus; import monero.daemon.model.MoneroTx; import monero.wallet.model.MoneroOutputQuery; import monero.wallet.model.MoneroOutputWallet; import monero.wallet.model.MoneroTransferQuery; import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxQuery; import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroWalletListener; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMessageListener, PersistedDataHost { private static final Logger log = LoggerFactory.getLogger(OpenOfferManager.class); private static final String THREAD_ID = OpenOfferManager.class.getSimpleName(); private static final long RETRY_REPUBLISH_DELAY_SEC = 10; private static final long REPUBLISH_AGAIN_AT_STARTUP_DELAY_SEC = 30; private static final long REPUBLISH_INTERVAL_MS = TimeUnit.MINUTES.toMillis(30); private static final long REFRESH_INTERVAL_MS = OfferPayload.TTL / 2; private static final int NUM_ATTEMPTS_THRESHOLD = 5; // process offer only on republish cycle after this many attempts private static final long SHUTDOWN_TIMEOUT_MS = 60000; private static final String OPEN_OFFER_GROUP_KEY_IMAGE_ID = OpenOffer.class.getSimpleName(); private static final String SIGNED_OFFER_KEY_IMAGE_GROUP_ID = SignedOffer.class.getSimpleName(); private final CoreContext coreContext; private final KeyRing keyRing; private final User user; private final P2PService p2PService; @Getter private final XmrConnectionService xmrConnectionService; private final BtcWalletService btcWalletService; @Getter private final XmrWalletService xmrWalletService; private final TradeWalletService tradeWalletService; private final OfferBookService offerBookService; private final ClosedTradableManager closedTradableManager; private final PriceFeedService priceFeedService; private final Preferences preferences; private final TradeStatisticsManager tradeStatisticsManager; private final ArbitratorManager arbitratorManager; private final MediatorManager mediatorManager; private final FilterManager filterManager; private final Broadcaster broadcaster; private final PersistenceManager> persistenceManager; private final Map offersToBeEdited = new HashMap<>(); private final TradableList openOffers = new TradableList<>(); private final SignedOfferList signedOffers = new SignedOfferList(); private final PersistenceManager signedOfferPersistenceManager; private final Map placeOfferProtocols = new HashMap(); private boolean stopped; private Timer periodicRepublishOffersTimer, periodicRefreshOffersTimer, retryRepublishOffersTimer; @Getter private final ObservableList> invalidOffers = FXCollections.observableArrayList(); @Getter private final AccountAgeWitnessService accountAgeWitnessService; private Object processOffersLock = new Object(); // lock for processing offers /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialization /////////////////////////////////////////////////////////////////////////////////////////// @Inject public OpenOfferManager(CoreContext coreContext, KeyRing keyRing, User user, P2PService p2PService, XmrConnectionService xmrConnectionService, BtcWalletService btcWalletService, XmrWalletService xmrWalletService, TradeWalletService tradeWalletService, OfferBookService offerBookService, ClosedTradableManager closedTradableManager, PriceFeedService priceFeedService, Preferences preferences, TradeStatisticsManager tradeStatisticsManager, ArbitratorManager arbitratorManager, MediatorManager mediatorManager, FilterManager filterManager, Broadcaster broadcaster, PersistenceManager> persistenceManager, PersistenceManager signedOfferPersistenceManager, AccountAgeWitnessService accountAgeWitnessService) { this.coreContext = coreContext; this.keyRing = keyRing; this.user = user; this.p2PService = p2PService; this.xmrConnectionService = xmrConnectionService; this.btcWalletService = btcWalletService; this.xmrWalletService = xmrWalletService; this.tradeWalletService = tradeWalletService; this.offerBookService = offerBookService; this.closedTradableManager = closedTradableManager; this.priceFeedService = priceFeedService; this.preferences = preferences; this.tradeStatisticsManager = tradeStatisticsManager; this.arbitratorManager = arbitratorManager; this.mediatorManager = mediatorManager; this.filterManager = filterManager; this.broadcaster = broadcaster; this.persistenceManager = persistenceManager; this.signedOfferPersistenceManager = signedOfferPersistenceManager; this.accountAgeWitnessService = accountAgeWitnessService; HavenoUtils.openOfferManager = this; ThreadUtils.reset(THREAD_ID); this.persistenceManager.initialize(openOffers, "OpenOffers", PersistenceManager.Source.PRIVATE); this.signedOfferPersistenceManager.initialize(signedOffers, "SignedOffers", PersistenceManager.Source.PRIVATE); // arbitrator stores reserve tx for signed offers } @Override public void readPersisted(Runnable completeHandler) { // read open offers persistenceManager.readPersisted(persisted -> { openOffers.setAll(persisted.getList()); openOffers.forEach(openOffer -> openOffer.getOffer().setPriceFeedService(priceFeedService)); // read signed offers signedOfferPersistenceManager.readPersisted(signedOfferPersisted -> { signedOffers.setAll(signedOfferPersisted.getList()); completeHandler.run(); }, completeHandler); }, completeHandler); } public void onAllServicesInitialized() { p2PService.addDecryptedDirectMessageListener(this); if (p2PService.isBootstrapped()) { onBootstrapComplete(); } else { p2PService.addP2PServiceListener(new BootstrapListener() { @Override public void onDataReceived() { onBootstrapComplete(); } }); } cleanUpAddressEntries(); } private void cleanUpAddressEntries() { Set openOffersIdSet; synchronized (openOffers.getList()) { openOffersIdSet = openOffers.getList().stream().map(OpenOffer::getId).collect(Collectors.toSet()); } xmrWalletService.getAddressEntriesForOpenOffer().stream() .filter(e -> !openOffersIdSet.contains(e.getOfferId())) .forEach(e -> { log.warn("We found an outdated addressEntry with context {} for openOffer {} (openOffers does not contain that " + "offer), offers.size={}", e.getContext(), e.getOfferId(), openOffers.size()); xmrWalletService.resetAddressEntriesForOpenOffer(e.getOfferId()); }); } public void shutDown(@Nullable Runnable completeHandler) { stopped = true; p2PService.getPeerManager().removeListener(this); p2PService.removeDecryptedDirectMessageListener(this); xmrConnectionService.getKeyImagePoller().removeKeyImages(OPEN_OFFER_GROUP_KEY_IMAGE_ID); xmrConnectionService.getKeyImagePoller().removeKeyImages(SIGNED_OFFER_KEY_IMAGE_GROUP_ID); stopPeriodicRefreshOffersTimer(); stopPeriodicRepublishOffersTimer(); stopRetryRepublishOffersTimer(); // we remove own offers from offerbook when we go offline // Normally we use a delay for broadcasting to the peers, but at shut down we want to get it fast out int size = openOffers.size(); log.info("Remove open offers at shutDown. Number of open offers: {}", size); if (offerBookService.isBootstrapped() && size > 0) { ThreadUtils.execute(() -> { // remove offers from offer book synchronized (openOffers.getList()) { openOffers.forEach(openOffer -> { if (openOffer.getState() == OpenOffer.State.AVAILABLE) { offerBookService.removeOfferAtShutDown(openOffer.getOffer().getOfferPayload()); } }); } // Force broadcaster to send out immediately, otherwise we could have a 2 sec delay until the // bundled messages sent out. broadcaster.flush(); // For typical number of offers we are tolerant with delay to give enough time to broadcast. // If number of offers is very high we limit to 3 sec. to not delay other shutdown routines. long delayMs = Math.min(3000, size * 200 + 500); HavenoUtils.waitFor(delayMs); }, THREAD_ID); } else { broadcaster.flush(); } // shut down thread pool off main thread ThreadUtils.submitToPool(() -> { shutDownThreadPool(); // invoke completion handler if (completeHandler != null) completeHandler.run(); }); } private void shutDownThreadPool() { try { ThreadUtils.shutDown(THREAD_ID, SHUTDOWN_TIMEOUT_MS); } catch (Exception e) { log.error("Error shutting down OpenOfferManager thread pool", e); } } public void removeAllOpenOffers(@Nullable Runnable completeHandler) { removeOpenOffers(getObservableList(), completeHandler); } private void removeOpenOffers(List openOffers, @Nullable Runnable completeHandler) { synchronized (openOffers) { int size = openOffers.size(); // Copy list as we remove in the loop List openOffersList = new ArrayList<>(openOffers); openOffersList.forEach(openOffer -> cancelOpenOffer(openOffer, () -> { }, errorMessage -> { log.warn("Error removing open offer: " + errorMessage); })); if (completeHandler != null) UserThread.runAfter(completeHandler, size * 200 + 500, TimeUnit.MILLISECONDS); } } /////////////////////////////////////////////////////////////////////////////////////////// // DecryptedDirectMessageListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress peerNodeAddress) { // Handler for incoming offer availability requests // We get an encrypted message but don't do the signature check as we don't know the peer yet. // A basic sig check is in done also at decryption time NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); if (networkEnvelope instanceof SignOfferRequest) { handleSignOfferRequest((SignOfferRequest) networkEnvelope, peerNodeAddress); } if (networkEnvelope instanceof SignOfferResponse) { handleSignOfferResponse((SignOfferResponse) networkEnvelope, peerNodeAddress); } else if (networkEnvelope instanceof OfferAvailabilityRequest) { handleOfferAvailabilityRequest((OfferAvailabilityRequest) networkEnvelope, peerNodeAddress); } else if (networkEnvelope instanceof AckMessage) { AckMessage ackMessage = (AckMessage) networkEnvelope; if (ackMessage.getSourceType() == AckMessageSourceType.OFFER_MESSAGE) { if (ackMessage.isSuccess()) { log.info("Received AckMessage for {} with offerId {} and uid {}", ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid()); } else { log.warn("Received AckMessage with error state for {} with offerId {} and errorMessage={}", ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getErrorMessage()); } } } } /////////////////////////////////////////////////////////////////////////////////////////// // BootstrapListener delegate /////////////////////////////////////////////////////////////////////////////////////////// private void onBootstrapComplete() { stopped = false; maybeUpdatePersistedOffers(); // listen for spent key images to close open and signed offers xmrConnectionService.getKeyImagePoller().addListener(new XmrKeyImageListener() { @Override public void onSpentStatusChanged(Map spentStatuses) { for (Entry entry : spentStatuses.entrySet()) { if (XmrKeyImagePoller.isSpent(entry.getValue())) { cancelOpenOffersOnSpent(entry.getKey()); removeSignedOffers(entry.getKey()); } } } }); // run off user thread so app is not blocked from starting ThreadUtils.submitToPool(() -> { // wait for prices to be available priceFeedService.awaitExternalPrices(); // process open offers on dedicated thread ThreadUtils.execute(() -> { // Republish means we send the complete offer object republishOffers(); startPeriodicRepublishOffersTimer(); // Refresh is started once we get a success from republish // We republish after a bit as it might be that our connected node still has the offer in the data map // but other peers have it already removed because of expired TTL. // Those other not directly connected peers would not get the broadcast of the new offer, as the first // connected peer (seed node) does not broadcast if it has the data in the map. // To update quickly to the whole network we repeat the republishOffers call after a few seconds when we // are better connected to the network. There is no guarantee that all peers will receive it but we also // have our periodic timer, so after that longer interval the offer should be available to all peers. if (retryRepublishOffersTimer == null) retryRepublishOffersTimer = UserThread.runAfter(OpenOfferManager.this::republishOffers, REPUBLISH_AGAIN_AT_STARTUP_DELAY_SEC); p2PService.getPeerManager().addListener(this); // TODO: add to invalid offers on failure // openOffers.stream() // .forEach(openOffer -> OfferUtil.getInvalidMakerFeeTxErrorMessage(openOffer.getOffer(), btcWalletService) // .ifPresent(errorMsg -> invalidOffers.add(new Tuple2<>(openOffer, errorMsg)))); // processs offers processOffers(false, (transaction) -> {}, (errorMessage) -> { log.warn("Error processing offers on bootstrap: " + errorMessage); }); // register to process offers on new block xmrWalletService.addWalletListener(new MoneroWalletListener() { @Override public void onNewBlock(long height) { // process each offer on new block a few times, then rely on period republish processOffers(true, (transaction) -> {}, (errorMessage) -> { log.warn("Error processing offers on new block {}: {}", height, errorMessage); }); } }); // poll spent status of open offer key images synchronized (openOffers.getList()) { for (OpenOffer openOffer : openOffers.getList()) { xmrConnectionService.getKeyImagePoller().addKeyImages(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages(), OPEN_OFFER_GROUP_KEY_IMAGE_ID); } } // poll spent status of signed offer key images synchronized (signedOffers.getList()) { for (SignedOffer signedOffer : signedOffers.getList()) { xmrConnectionService.getKeyImagePoller().addKeyImages(signedOffer.getReserveTxKeyImages(), SIGNED_OFFER_KEY_IMAGE_GROUP_ID); } } }, THREAD_ID); }); } /////////////////////////////////////////////////////////////////////////////////////////// // PeerManager.Listener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onAllConnectionsLost() { log.info("onAllConnectionsLost"); stopped = true; stopPeriodicRefreshOffersTimer(); stopPeriodicRepublishOffersTimer(); stopRetryRepublishOffersTimer(); restart(); } @Override public void onNewConnectionAfterAllConnectionsLost() { log.info("onNewConnectionAfterAllConnectionsLost"); stopped = false; restart(); } @Override public void onAwakeFromStandby() { log.info("onAwakeFromStandby"); stopped = false; if (!p2PService.getNetworkNode().getAllConnections().isEmpty()) restart(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void placeOffer(Offer offer, boolean useSavingsWallet, long triggerPrice, boolean reserveExactAmount, boolean resetAddressEntriesOnError, String sourceOfferId, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { ThreadUtils.execute(() -> { // cannot set trigger price for fixed price offers if (triggerPrice != 0 && offer.getOfferPayload().getPrice() != 0) { errorMessageHandler.handleErrorMessage("Cannot set trigger price for fixed price offers."); return; } // check source offer and clone limit OpenOffer sourceOffer = null; if (sourceOfferId != null) { // get source offer Optional sourceOfferOptional = getOpenOffer(sourceOfferId); if (!sourceOfferOptional.isPresent()) { errorMessageHandler.handleErrorMessage("Source offer not found to clone, offerId=" + sourceOfferId + "."); return; } sourceOffer = sourceOfferOptional.get(); // check clone limit int numClones = getOpenOfferGroup(sourceOffer.getGroupId()).size(); if (numClones >= Restrictions.getMaxOffersWithSharedFunds()) { errorMessageHandler.handleErrorMessage("Cannot create offer because maximum number of " + Restrictions.getMaxOffersWithSharedFunds() + " cloned offers with shared funds reached."); return; } } // create open offer OpenOffer openOffer = new OpenOffer(offer, triggerPrice, sourceOffer == null ? reserveExactAmount : sourceOffer.isReserveExactAmount()); // set state from source offer if (sourceOffer != null) { openOffer.setReserveTxHash(sourceOffer.getReserveTxHash()); openOffer.setReserveTxHex(sourceOffer.getReserveTxHex()); openOffer.setReserveTxKey(sourceOffer.getReserveTxKey()); openOffer.setGroupId(sourceOffer.getGroupId()); openOffer.getOffer().getOfferPayload().setReserveTxKeyImages(sourceOffer.getOffer().getOfferPayload().getReserveTxKeyImages()); xmrWalletService.cloneAddressEntries(sourceOffer.getOffer().getId(), openOffer.getOffer().getId()); if (hasConflictingClone(openOffer)) openOffer.setState(OpenOffer.State.DEACTIVATED); } // add the open offer synchronized (processOffersLock) { addOpenOffer(openOffer); } // done if source offer is pending if (sourceOffer != null && sourceOffer.isPending()) { resultHandler.handleResult(null); return; } // schedule or post offer synchronized (processOffersLock) { CountDownLatch latch = new CountDownLatch(1); processOffer(getOpenOffers(), openOffer, (transaction) -> { requestPersistence(); latch.countDown(); resultHandler.handleResult(transaction); }, (errorMessage) -> { if (!openOffer.isCanceled()) { log.warn("Error processing offer {}: {}", openOffer.getId(), errorMessage); doCancelOffer(openOffer, resetAddressEntriesOnError); } latch.countDown(); errorMessageHandler.handleErrorMessage(errorMessage); }); HavenoUtils.awaitLatch(latch); } }, THREAD_ID); } // Remove from offerbook public void removeOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { Optional openOfferOptional = getOpenOffer(offer.getId()); if (openOfferOptional.isPresent()) { cancelOpenOffer(openOfferOptional.get(), resultHandler, errorMessageHandler); } else { String errorMsg = "Offer was not found in our list of open offers. We still try to remove it from the offerbook."; log.warn(errorMsg); errorMessageHandler.handleErrorMessage(errorMsg); offerBookService.removeOffer(offer.getOfferPayload(), () -> offer.setState(Offer.State.REMOVED), null); } } public void activateOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { if (openOffer.isPending()) { resultHandler.handleResult(); // ignore if pending } else if (offersToBeEdited.containsKey(openOffer.getId())) { errorMessageHandler.handleErrorMessage(Res.get("offerbook.cannotActivateEditedOffer.warning")); } else if (hasConflictingClone(openOffer)) { errorMessageHandler.handleErrorMessage(Res.get("offerbook.hasConflictingClone.warning")); } else { try { // validate arbitrator signature validateSignedState(openOffer); // activate offer on offer book Offer offer = openOffer.getOffer(); log.info("Activating open offer: {}", openOffer.getId()); offerBookService.activateOffer(offer, () -> { openOffer.setState(OpenOffer.State.AVAILABLE); applyTriggerState(openOffer); requestPersistence(); log.debug("activateOpenOffer, offerId={}", offer.getId()); resultHandler.handleResult(); }, errorMessageHandler); } catch (Exception e) { errorMessageHandler.handleErrorMessage(e.getMessage()); return; } } } private void applyTriggerState(OpenOffer openOffer) { if (openOffer.getState() != OpenOffer.State.AVAILABLE) return; if (TriggerPriceService.isTriggered(priceFeedService.getMarketPrice(openOffer.getOffer().getCounterCurrencyCode()), openOffer)) { openOffer.deactivate(true); } } public void deactivateOpenOffer(OpenOffer openOffer, boolean deactivatedByTrigger, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { Offer offer = openOffer.getOffer(); if (openOffer.isAvailable()) { log.info("Deactivating open offer: {}", openOffer.getId()); offerBookService.deactivateOffer(offer.getOfferPayload(), () -> { openOffer.deactivate(deactivatedByTrigger); requestPersistence(); log.debug("deactivateOpenOffer, offerId={}", offer.getId()); resultHandler.handleResult(); }, errorMessageHandler); } else { resultHandler.handleResult(); // ignore if unavailable } } public void cancelOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { log.info("Canceling open offer: {}", openOffer.getId()); try { if (!offersToBeEdited.containsKey(openOffer.getId())) { if (isOnOfferBook(openOffer)) { openOffer.setState(OpenOffer.State.CANCELED); offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(), () -> { ThreadUtils.submitToPool(() -> { // TODO: this runs off thread and then shows popup when done. should show overlay spinner until done doCancelOffer(openOffer); if (resultHandler != null) resultHandler.handleResult(); }); }, errorMessageHandler); } else { openOffer.setState(OpenOffer.State.CANCELED); ThreadUtils.submitToPool(() -> { doCancelOffer(openOffer); if (resultHandler != null) resultHandler.handleResult(); }); } } else { if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage("You can't cancel an offer that is currently edited."); } } catch (Throwable t) { log.warn("Error canceling open offer " + openOffer.getId() + ": " + t.getMessage(), t); if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage("Error canceling open offer " + openOffer.getId() + ": " + t.getMessage()); } } private boolean isOnOfferBook(OpenOffer openOffer) { if (!p2PService.isBootstrapped()) return false; return openOffer.isAvailable() || openOffer.isReserved(); } public void editOpenOfferStart(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { if (offersToBeEdited.containsKey(openOffer.getId())) { log.warn("editOpenOfferStart called for an offer which is already in edit mode."); resultHandler.handleResult(); return; } log.info("Editing open offer: {}", openOffer.getId()); offersToBeEdited.put(openOffer.getId(), openOffer); if (openOffer.isAvailable()) { deactivateOpenOffer(openOffer, false, resultHandler, errorMessage -> { offersToBeEdited.remove(openOffer.getId()); errorMessageHandler.handleErrorMessage(errorMessage); }); } else { resultHandler.handleResult(); } } public void editOpenOfferPublish(Offer editedOffer, long triggerPrice, OpenOffer.State originalState, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { ThreadUtils.execute(() -> { Optional openOfferOptional = getOpenOffer(editedOffer.getId()); // check that trigger price is not set for fixed price offers boolean isFixedPrice = editedOffer.getOfferPayload().getPrice() != 0; if (triggerPrice != 0 && isFixedPrice) { errorMessageHandler.handleErrorMessage("Cannot set trigger price for fixed price offers."); return; } if (openOfferOptional.isPresent()) { OpenOffer openOffer = openOfferOptional.get(); openOffer.getOffer().setState(Offer.State.REMOVED); openOffer.setState(OpenOffer.State.CANCELED); removeOpenOffer(openOffer); OpenOffer editedOpenOffer = new OpenOffer(editedOffer, triggerPrice, openOffer); if (originalState == OpenOffer.State.DEACTIVATED && openOffer.isDeactivatedByTrigger()) { if (hasConflictingClone(editedOpenOffer)) { editedOpenOffer.setState(OpenOffer.State.DEACTIVATED); } else { editedOpenOffer.setState(OpenOffer.State.AVAILABLE); } } else { if (originalState == OpenOffer.State.AVAILABLE && hasConflictingClone(editedOpenOffer)) { editedOpenOffer.setState(OpenOffer.State.DEACTIVATED); } else { editedOpenOffer.setState(originalState); } } applyTriggerState(editedOpenOffer); // apply trigger state before adding so it's not immediately removed addOpenOffer(editedOpenOffer); // check for valid arbitrator signature after editing Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(editedOpenOffer.getOffer().getOfferPayload().getArbitratorSigner()); if (arbitrator == null || !HavenoUtils.isArbitratorSignatureValid(editedOpenOffer.getOffer().getOfferPayload(), arbitrator)) { // reset arbitrator signature editedOpenOffer.getOffer().getOfferPayload().setArbitratorSignature(null); editedOpenOffer.getOffer().getOfferPayload().setArbitratorSigner(null); if (editedOpenOffer.isAvailable()) editedOpenOffer.setState(OpenOffer.State.PENDING); // process offer to sign and publish synchronized (processOffersLock) { CountDownLatch latch = new CountDownLatch(1); processOffer(getOpenOffers(), editedOpenOffer, (transaction) -> { offersToBeEdited.remove(openOffer.getId()); requestPersistence(); latch.countDown(); resultHandler.handleResult(); }, (errorMsg) -> { latch.countDown(); errorMessageHandler.handleErrorMessage(errorMsg); }); HavenoUtils.awaitLatch(latch); } } else { maybeRepublishOffer(editedOpenOffer, null); offersToBeEdited.remove(openOffer.getId()); requestPersistence(); resultHandler.handleResult(); } } else { errorMessageHandler.handleErrorMessage("There is no offer with this id existing to be published."); } }, THREAD_ID); } public void editOpenOfferCancel(OpenOffer openOffer, OpenOffer.State originalState, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { if (offersToBeEdited.containsKey(openOffer.getId())) { offersToBeEdited.remove(openOffer.getId()); if (originalState.equals(OpenOffer.State.AVAILABLE)) { activateOpenOffer(openOffer, resultHandler, errorMessageHandler); } else { resultHandler.handleResult(); } requestPersistence(); } else { errorMessageHandler.handleErrorMessage("Editing of offer can't be canceled as it is not edited."); } } private void doCancelOffer(OpenOffer openOffer) { doCancelOffer(openOffer, true); } // cancel open offer which thaws its key images private void doCancelOffer(@NotNull OpenOffer openOffer, boolean resetAddressEntries) { Offer offer = openOffer.getOffer(); offer.setState(Offer.State.REMOVED); openOffer.setState(OpenOffer.State.CANCELED); boolean hasClonedOffer = hasClonedOffer(offer.getId()); // record before removing open offer removeOpenOffer(openOffer); if (!hasClonedOffer) closedTradableManager.add(openOffer); // do not add clones to closed trades TODO: don't add canceled offers to closed tradables? if (resetAddressEntries) xmrWalletService.resetAddressEntriesForOpenOffer(offer.getId()); requestPersistence(); if (!hasClonedOffer) xmrWalletService.thawOutputs(offer.getOfferPayload().getReserveTxKeyImages()); } // close open offer group after key images spent public void closeSpentOffer(Offer offer) { getOpenOffer(offer.getId()).ifPresent(openOffer -> { for (OpenOffer groupOffer: getOpenOfferGroup(openOffer.getGroupId())) { doCloseOpenOffer(groupOffer); } }); } private void doCloseOpenOffer(OpenOffer openOffer) { removeOpenOffer(openOffer); openOffer.setState(OpenOffer.State.CLOSED); xmrWalletService.resetAddressEntriesForOpenOffer(openOffer.getId()); offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(), () -> log.info("Successfully removed offer {}", openOffer.getId()), log::error); requestPersistence(); } public void reserveOpenOffer(OpenOffer openOffer) { openOffer.setState(OpenOffer.State.RESERVED); requestPersistence(); } public void unreserveOpenOffer(OpenOffer openOffer) { openOffer.setState(OpenOffer.State.AVAILABLE); requestPersistence(); } public boolean hasConflictingClone(OpenOffer openOffer) { return hasConflictingClone(getOpenOffers(), openOffer); } private static boolean hasConflictingClone(List openOffers, OpenOffer openOffer) { for (OpenOffer clonedOffer : getOpenOfferGroup(openOffers, openOffer.getGroupId())) { if (clonedOffer.getId().equals(openOffer.getId())) continue; if (clonedOffer.isDeactivated()) continue; // deactivated offers do not conflict // pending offers later in the order do not conflict if (clonedOffer.isPending() && openOffers.indexOf(clonedOffer) > openOffers.indexOf(openOffer)) { continue; } // conflicts if same payment method and currency if (samePaymentMethodAndCurrency(clonedOffer.getOffer(), openOffer.getOffer())) { return true; } } return false; } public boolean hasConflictingClone(Offer offer, OpenOffer sourceOffer) { return hasConflictingClone(getOpenOffers(), offer, sourceOffer); } private static boolean hasConflictingClone(List openOffers, Offer offer, OpenOffer sourceOffer) { return getOpenOfferGroup(openOffers, sourceOffer.getGroupId()).stream() .filter(openOffer -> !openOffer.isDeactivated()) // we only check with activated offers .anyMatch(openOffer -> samePaymentMethodAndCurrency(openOffer.getOffer(), offer)); } private static boolean samePaymentMethodAndCurrency(Offer offer1, Offer offer2) { return offer1.getPaymentMethodId().equalsIgnoreCase(offer2.getPaymentMethodId()) && offer1.getCounterCurrencyCode().equalsIgnoreCase(offer2.getCounterCurrencyCode()) && offer1.getBaseCurrencyCode().equalsIgnoreCase(offer2.getBaseCurrencyCode()); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// public boolean isMyOffer(Offer offer) { return offer.isMyOffer(keyRing); } /** * Get the wallet balance which is not scheduled or reserved for offers. * * @return the unallocated balance as BigInteger */ public BigInteger getUnallocatedBalance() { BigInteger unallocatedBalance = xmrWalletService.getBalance(); for (OpenOffer openOffer : getOpenOffersWithoutClones()) { if (openOffer.getState() == OpenOffer.State.AVAILABLE) continue; if (openOffer.isReserveExactAmount()) { unallocatedBalance = unallocatedBalance.subtract(openOffer.getOffer().getAmountNeeded()); } else if (openOffer.getScheduledAmount() != null && !openOffer.getScheduledAmount().isEmpty()) { unallocatedBalance = unallocatedBalance.subtract(new BigInteger(openOffer.getScheduledAmount())); } } return unallocatedBalance; } public boolean hasAvailableOpenOffers() { for (OpenOffer openOffer : getOpenOffers()) { if (openOffer.getState() == OpenOffer.State.AVAILABLE) { return true; } } return false; } public List getOpenOffers() { synchronized (openOffers.getList()) { return ImmutableList.copyOf(getObservableList()); } } public List getOpenOffersWithoutClones() { List fundedOffers = new ArrayList<>(); synchronized (openOffers.getList()) { for (OpenOffer openOffer : getObservableList()) { if (openOffer.getGroupId() == null) fundedOffers.add(openOffer); else { List openOfferGroup = getOpenOfferGroup(getObservableList(), openOffer.getGroupId()); if (!fundedOffers.contains(openOfferGroup.get(0))) fundedOffers.add(openOfferGroup.get(0)); } } } return fundedOffers; } public List getOpenOfferGroup(String groupId) { return getOpenOfferGroup(getOpenOffers(), groupId); } private static List getOpenOfferGroup(List openOffers, String groupId) { if (groupId == null) throw new IllegalArgumentException("groupId cannot be null"); return openOffers.stream() .filter(openOffer -> groupId.equals(openOffer.getGroupId())) .collect(Collectors.toList()); } public boolean hasClonedOffer(String offerId) { return hasClonedOffer(getOpenOffers(), offerId); } private static boolean hasClonedOffer(List openOffers, String offerId) { OpenOffer openOffer = getOpenOffer(openOffers, offerId).orElse(null); if (openOffer == null) return false; return getOpenOfferGroup(openOffers, openOffer.getGroupId()).size() > 1; } public boolean hasClonedOffers() { List openOffers = getOpenOffers(); for (OpenOffer openOffer : openOffers) { if (getOpenOfferGroup(openOffers, openOffer.getGroupId()).size() > 1) { return true; } } return false; } public List getSignedOffers() { synchronized (signedOffers.getList()) { return ImmutableList.copyOf(signedOffers.getObservableList()); } } public ObservableList getObservableSignedOffersList() { synchronized (signedOffers.getList()) { return signedOffers.getObservableList(); } } public ObservableList getObservableList() { return openOffers.getObservableList(); } public Optional getOpenOffer(String offerId) { return getOpenOffer(getOpenOffers(), offerId); } private static Optional getOpenOffer(List openOffers, String offerId) { return openOffers.stream().filter(e -> e.getId().equals(offerId)).findFirst(); } private static boolean hasOpenOffer(List openOffers, String offerId) { return getOpenOffer(openOffers, offerId).isPresent(); } public Optional getSignedOfferById(String offerId) { return getSignedOffers().stream().filter(e -> e.getOfferId().equals(offerId)).findFirst(); } private void addOpenOffer(OpenOffer openOffer) { log.info("Adding open offer {}", openOffer.getId()); synchronized (openOffers.getList()) { openOffers.add(openOffer); if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null) { xmrConnectionService.getKeyImagePoller().addKeyImages(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages(), OPEN_OFFER_GROUP_KEY_IMAGE_ID); } } } private void removeOpenOffer(OpenOffer openOffer) { log.info("Removing open offer {}", openOffer.getId()); synchronized (openOffers.getList()) { openOffers.remove(openOffer); if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null) { xmrConnectionService.getKeyImagePoller().removeKeyImages(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages(), OPEN_OFFER_GROUP_KEY_IMAGE_ID); } } // cancel place offer protocol ThreadUtils.execute(() -> { synchronized (processOffersLock) { synchronized (placeOfferProtocols) { PlaceOfferProtocol protocol = placeOfferProtocols.remove(openOffer.getId()); if (protocol != null) protocol.cancelOffer(); } } }, THREAD_ID); } private void cancelOpenOffersOnSpent(String keyImage) { synchronized (openOffers.getList()) { for (OpenOffer openOffer : openOffers.getList()) { if (openOffer.getState() != OpenOffer.State.RESERVED && openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null && openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().contains(keyImage)) { log.warn("Canceling open offer because reserved funds have been spent unexpectedly, offerId={}, state={}", openOffer.getId(), openOffer.getState()); cancelOpenOffer(openOffer, null, null); } } } } private void addSignedOffer(SignedOffer signedOffer) { log.info("Adding SignedOffer for offer {}", signedOffer.getOfferId()); synchronized (signedOffers.getList()) { // remove signed offers with common key images for (String keyImage : signedOffer.getReserveTxKeyImages()) { removeSignedOffers(keyImage); } // add new signed offer signedOffers.add(signedOffer); xmrConnectionService.getKeyImagePoller().addKeyImages(signedOffer.getReserveTxKeyImages(), SIGNED_OFFER_KEY_IMAGE_GROUP_ID); } } private void removeSignedOffer(SignedOffer signedOffer) { log.info("Removing SignedOffer for offer {}", signedOffer.getOfferId()); synchronized (signedOffers.getList()) { signedOffers.remove(signedOffer); } xmrConnectionService.getKeyImagePoller().removeKeyImages(signedOffer.getReserveTxKeyImages(), SIGNED_OFFER_KEY_IMAGE_GROUP_ID); } private void removeSignedOffers(String keyImage) { synchronized (signedOffers.getList()) { for (SignedOffer signedOffer : getSignedOffers()) { if (signedOffer.getReserveTxKeyImages().contains(keyImage)) { removeSignedOffer(signedOffer); } } } } /////////////////////////////////////////////////////////////////////////////////////////// // Place offer helpers /////////////////////////////////////////////////////////////////////////////////////////// private void processOffers(boolean skipOffersWithTooManyAttempts, TransactionResultHandler resultHandler, // TODO (woodser): transaction not needed with result handler ErrorMessageHandler errorMessageHandler) { ThreadUtils.execute(() -> { List errorMessages = new ArrayList(); synchronized (processOffersLock) { List openOffers = getOpenOffers(); for (OpenOffer offer : openOffers) { if (skipOffersWithTooManyAttempts && offer.getNumProcessingAttempts() > NUM_ATTEMPTS_THRESHOLD) continue; // skip offers with too many attempts CountDownLatch latch = new CountDownLatch(1); processOffer(openOffers, offer, (transaction) -> { latch.countDown(); }, errorMessage -> { errorMessages.add(errorMessage); latch.countDown(); }); HavenoUtils.awaitLatch(latch); } } requestPersistence(); if (errorMessages.isEmpty()) { if (resultHandler != null) resultHandler.handleResult(null); } else { if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage(errorMessages.toString()); } }, THREAD_ID); } private void processOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { // skip if already processing if (openOffer.isProcessing()) { resultHandler.handleResult(null); return; } // process offer openOffer.setProcessing(true); doProcessOffer(openOffers, openOffer, (transaction) -> { openOffer.setProcessing(false); resultHandler.handleResult(transaction); }, (errorMsg) -> { openOffer.setProcessing(false); openOffer.setNumProcessingAttempts(openOffer.getNumProcessingAttempts() + 1); openOffer.getOffer().setErrorMessage(errorMsg); if (!openOffer.isCanceled()) { errorMsg = "Error processing offer, offerId=" + openOffer.getId() + ", attempt=" + openOffer.getNumProcessingAttempts() + ": " + errorMsg; openOffer.getOffer().setErrorMessage(errorMsg); // cancel offer if invalid if (openOffer.getOffer().getState() == Offer.State.INVALID) { log.warn("Canceling offer because it's invalid: {}", openOffer.getId()); doCancelOffer(openOffer); } } errorMessageHandler.handleErrorMessage(errorMsg); }); } private void doProcessOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { new Thread(() -> { try { // done processing if canceled or wallet not initialized if (openOffer.isCanceled() || xmrWalletService.getWallet() == null) { resultHandler.handleResult(null); return; } // validate offer try { ValidateOffer.validateOffer(openOffer.getOffer(), accountAgeWitnessService, user); } catch (Exception e) { openOffer.getOffer().setState(Offer.State.INVALID); errorMessageHandler.handleErrorMessage("Failed to validate offer: " + e.getMessage()); return; } // handle pending offer if (openOffer.isPending()) { // only process the first offer of a pending clone group if (openOffer.getGroupId() != null) { List openOfferClones = getOpenOfferGroup(openOffers, openOffer.getGroupId()); if (openOfferClones.size() > 1 && !openOfferClones.get(0).getId().equals(openOffer.getId()) && openOfferClones.get(0).isPending()) { resultHandler.handleResult(null); return; } } } else { // validate or reset non-pending state try { validateSignedState(openOffer); resultHandler.handleResult(null); // done processing if non-pending state is valid return; } catch (Exception e) { log.info("Open offer {} has invalid signature, which can happen after editing or cloning offer, validationMsg={}", openOffer.getId(), e.getMessage()); // reset arbitrator signature openOffer.getOffer().getOfferPayload().setArbitratorSignature(null); openOffer.getOffer().getOfferPayload().setArbitratorSigner(null); if (openOffer.isAvailable()) openOffer.setState(OpenOffer.State.PENDING); } } // sign and post offer if already funded if (openOffer.getReserveTxHash() != null) { signAndPostOffer(openOffer, false, resultHandler, errorMessageHandler); return; } // reset state if scheduled txs become unavailable if (openOffer.getScheduledTxHashes() != null) { boolean scheduledTxsAvailable = true; for (MoneroTxWallet tx : xmrWalletService.getTxs(openOffer.getScheduledTxHashes())) { if (!tx.isLocked() && !hasSpendableAmount(tx)) { scheduledTxsAvailable = false; break; } } if (!scheduledTxsAvailable) { log.warn("Scheduled txs are no longer available for offer {}, rescheduling", openOffer.getId()); openOffer.setScheduledAmount(null); openOffer.setScheduledTxHashes(null); } } // get amount needed to reserve offer BigInteger amountNeeded = openOffer.getOffer().getAmountNeeded(); // handle split output offer if (openOffer.isReserveExactAmount()) { // find tx with exact input amount MoneroTxWallet splitOutputTx = getSplitOutputFundingTx(openOffers, openOffer); if (splitOutputTx != null && openOffer.getSplitOutputTxHash() == null) { setSplitOutputTx(openOffer, splitOutputTx); } // if wallet has exact available balance, try to sign and post directly if (xmrWalletService.getAvailableBalance().equals(amountNeeded)) { signAndPostOffer(openOffer, true, resultHandler, (errorMessage) -> { splitOrSchedule(splitOutputTx, openOffers, openOffer, amountNeeded, resultHandler, errorMessageHandler); }); return; } else { splitOrSchedule(splitOutputTx, openOffers, openOffer, amountNeeded, resultHandler, errorMessageHandler); } } else { // sign and post offer if enough funds boolean hasSufficientBalance = xmrWalletService.getAvailableBalance().compareTo(amountNeeded) >= 0; if (hasSufficientBalance) { signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler); return; } else if (openOffer.getScheduledTxHashes() == null) { scheduleWithEarliestTxs(openOffers, openOffer); } resultHandler.handleResult(null); return; } } catch (Exception e) { if (!openOffer.isCanceled()) log.error("Error processing offer: {}\n", e.getMessage(), e); errorMessageHandler.handleErrorMessage(e.getMessage()); } }).start(); } private void validateSignedState(OpenOffer openOffer) { Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(openOffer.getOffer().getOfferPayload().getArbitratorSigner()); if (openOffer.getOffer().getOfferPayload().getArbitratorSigner() == null) { throw new IllegalArgumentException("Offer " + openOffer.getId() + " has no arbitrator signer"); } else if (openOffer.getOffer().getOfferPayload().getArbitratorSignature() == null) { throw new IllegalArgumentException("Offer " + openOffer.getId() + " has no arbitrator signature"); } else if (arbitrator == null) { throw new IllegalArgumentException("Offer " + openOffer.getId() + " signed by unregistered arbitrator"); } else if (!HavenoUtils.isArbitratorSignatureValid(openOffer.getOffer().getOfferPayload(), arbitrator)) { throw new IllegalArgumentException("Offer " + openOffer.getId() + " has invalid arbitrator signature"); } else if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() == null || openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().isEmpty() || openOffer.getReserveTxHash() == null || openOffer.getReserveTxHash().isEmpty()) { throw new IllegalArgumentException("Offer " + openOffer.getId() + " is missing reserve tx hash or key images"); } } private MoneroTxWallet getSplitOutputFundingTx(List openOffers, OpenOffer openOffer) { XmrAddressEntry addressEntry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); return getSplitOutputFundingTx(openOffers, openOffer, openOffer.getOffer().getAmountNeeded(), addressEntry.getSubaddressIndex()); } private MoneroTxWallet getSplitOutputFundingTx(List openOffers, OpenOffer openOffer, BigInteger reserveAmount, Integer preferredSubaddressIndex) { // return split output tx if already assigned if (openOffer != null && openOffer.getSplitOutputTxHash() != null) { // get recorded split output tx MoneroTxWallet splitOutputTx = xmrWalletService.getTx(openOffer.getSplitOutputTxHash()); // check if split output tx is available for offer if (splitOutputTx != null) { if (splitOutputTx.isLocked()) return splitOutputTx; else { boolean isAvailable = true; for (MoneroOutputWallet output : splitOutputTx.getOutputsWallet()) { if (output.isSpent() || output.isFrozen()) { isAvailable = false; break; } } if (isAvailable || isReservedByOffer(openOffer, splitOutputTx)) return splitOutputTx; else log.warn("Split output tx is no longer available for offerId={}, txId={}", openOffer.getId(), openOffer.getSplitOutputTxHash()); } } else { log.warn("Split output tx no longer exists for offerId={}, txId={}", openOffer.getId(), openOffer.getSplitOutputTxHash()); } } // get split output tx to offer's preferred subaddress if (preferredSubaddressIndex != null) { List fundingTxs = getSplitOutputFundingTxs(reserveAmount, preferredSubaddressIndex); MoneroTxWallet earliestUnscheduledTx = getEarliestUnscheduledTx(openOffers, openOffer, fundingTxs); if (earliestUnscheduledTx != null) return earliestUnscheduledTx; } // get split output tx to any subaddress List fundingTxs = getSplitOutputFundingTxs(reserveAmount, null); return getEarliestUnscheduledTx(openOffers, openOffer, fundingTxs); } private boolean isReservedByOffer(OpenOffer openOffer, MoneroTxWallet tx) { if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() == null) return false; Set offerKeyImages = new HashSet(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages()); for (MoneroOutputWallet output : tx.getOutputsWallet()) { if (offerKeyImages.contains(output.getKeyImage().getHex())) return true; } return false; } private List getSplitOutputFundingTxs(BigInteger reserveAmount, Integer preferredSubaddressIndex) { List splitOutputTxs = xmrWalletService.getTxs(new MoneroTxQuery().setIsFailed(false)); // TODO: not using setIsIncoming(true) because split output txs sent to self have false; fix in monero-java? Set removeTxs = new HashSet(); for (MoneroTxWallet tx : splitOutputTxs) { if (tx.getOutputs() != null) { // outputs not available until first confirmation for (MoneroOutputWallet output : tx.getOutputsWallet()) { if (output.isSpent() || output.isFrozen()) removeTxs.add(tx); } } if (!hasExactOutput(tx, reserveAmount, preferredSubaddressIndex)) removeTxs.add(tx); } splitOutputTxs.removeAll(removeTxs); return splitOutputTxs; } private boolean hasExactOutput(MoneroTxWallet tx, BigInteger amount, Integer preferredSubaddressIndex) { boolean hasExactOutput = (tx.getOutputsWallet(new MoneroOutputQuery() .setAccountIndex(0) .setSubaddressIndex(preferredSubaddressIndex) .setAmount(amount)).size() > 0); if (hasExactOutput) return true; boolean hasExactTransfer = (tx.getTransfers(new MoneroTransferQuery() .setAccountIndex(0) .setSubaddressIndex(preferredSubaddressIndex) .setIsIncoming(true) .setAmount(amount)).size() > 0); return hasExactTransfer; } private MoneroTxWallet getEarliestUnscheduledTx(List openOffers, OpenOffer excludeOpenOffer, List txs) { MoneroTxWallet earliestUnscheduledTx = null; for (MoneroTxWallet tx : txs) { if (isTxScheduledByOtherOffer(openOffers, excludeOpenOffer, tx.getHash())) continue; if (earliestUnscheduledTx == null || (earliestUnscheduledTx.getNumConfirmations() < tx.getNumConfirmations())) earliestUnscheduledTx = tx; } return earliestUnscheduledTx; } // if split tx not found and cannot reserve exact amount directly, create tx to split or reserve exact output private void splitOrSchedule(MoneroTxWallet splitOutputTx, List openOffers, OpenOffer openOffer, BigInteger amountNeeded, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { if (splitOutputTx == null) { if (openOffer.getSplitOutputTxHash() != null) { log.warn("Split output tx unexpectedly unavailable for offerId={}, txId={}", openOffer.getId(), openOffer.getSplitOutputTxHash()); setSplitOutputTx(openOffer, null); } try { splitOrScheduleAux(openOffers, openOffer, amountNeeded); resultHandler.handleResult(null); return; } catch (Exception e) { log.warn("Unable to split or schedule funds for offer {}: {}", openOffer.getId(), e.getMessage()); openOffer.getOffer().setState(Offer.State.INVALID); errorMessageHandler.handleErrorMessage(e.getMessage()); return; } } else if (!splitOutputTx.isLocked()) { // otherwise sign and post offer if split output available signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler); return; } else { resultHandler.handleResult(null); return; } } private void splitOrScheduleAux(List openOffers, OpenOffer openOffer, BigInteger offerReserveAmount) { // handle sufficient available balance to split output boolean sufficientAvailableBalance = xmrWalletService.getAvailableBalance().compareTo(offerReserveAmount) >= 0; if (sufficientAvailableBalance && openOffer.getSplitOutputTxHash() == null) { log.info("Splitting and scheduling outputs for offer {}", openOffer.getShortId()); splitAndSchedule(openOffer); } else if (openOffer.getScheduledTxHashes() == null) { scheduleWithEarliestTxs(openOffers, openOffer); } } private MoneroTxWallet splitAndSchedule(OpenOffer openOffer) { BigInteger reserveAmount = openOffer.getOffer().getAmountNeeded(); xmrWalletService.swapAddressEntryToAvailable(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); // change funding subaddress in case funded with unsuitable output(s) MoneroTxWallet splitOutputTx = null; synchronized (HavenoUtils.xmrWalletService.getWalletLock()) { XmrAddressEntry entry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); synchronized (HavenoUtils.getWalletFunctionLock()) { long startTime = System.currentTimeMillis(); for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection(); try { log.info("Creating split output tx to fund offer {} at subaddress {}", openOffer.getShortId(), entry.getSubaddressIndex()); splitOutputTx = xmrWalletService.createTx(new MoneroTxConfig() .setAccountIndex(0) .setAddress(entry.getAddressString()) .setAmount(reserveAmount) .setRelay(true) .setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY)); break; } catch (Exception e) { if (e.getMessage().contains("not enough")) throw e; // do not retry if not enough funds log.warn("Error creating split output tx to fund offer, offerId={}, subaddress={}, attempt={}/{}, error={}", openOffer.getShortId(), entry.getSubaddressIndex(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); xmrWalletService.handleMainWalletError(e, sourceConnection, i + 1); if (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } } log.info("Done creating split output tx to fund offer {} in {} ms", openOffer.getId(), System.currentTimeMillis() - startTime); } } // set split tx setSplitOutputTx(openOffer, splitOutputTx); return splitOutputTx; } private void setSplitOutputTx(OpenOffer openOffer, MoneroTxWallet splitOutputTx) { openOffer.setSplitOutputTxHash(splitOutputTx == null ? null : splitOutputTx.getHash()); openOffer.setSplitOutputTxFee(splitOutputTx == null ? 0l : splitOutputTx.getFee().longValueExact()); openOffer.setScheduledTxHashes(splitOutputTx == null ? null : Arrays.asList(splitOutputTx.getHash())); openOffer.setScheduledAmount(splitOutputTx == null ? null : openOffer.getOffer().getAmountNeeded().toString()); if (!openOffer.isCanceled()) openOffer.setState(OpenOffer.State.PENDING); } private void scheduleWithEarliestTxs(List openOffers, OpenOffer openOffer) { // get earliest available or pending txs with sufficient spendable amount BigInteger offerReserveAmount = openOffer.getOffer().getAmountNeeded(); BigInteger scheduledAmount = BigInteger.ZERO; Set scheduledTxs = new HashSet(); for (MoneroTxWallet tx : xmrWalletService.getTxs()) { // get unscheduled spendable amount BigInteger spendableAmount = getUnscheduledSpendableAmount(tx, openOffers); // skip if no spendable amount if (spendableAmount.equals(BigInteger.ZERO)) continue; // schedule tx scheduledAmount = scheduledAmount.add(spendableAmount); scheduledTxs.add(tx); // break if sufficient funds if (scheduledAmount.compareTo(offerReserveAmount) >= 0) break; } if (scheduledAmount.compareTo(offerReserveAmount) < 0) throw new RuntimeException("Not enough funds to create offer " + openOffer.getId()); // schedule txs openOffer.setScheduledTxHashes(scheduledTxs.stream().map(tx -> tx.getHash()).collect(Collectors.toList())); openOffer.setScheduledAmount(scheduledAmount.toString()); openOffer.setState(OpenOffer.State.PENDING); } private BigInteger getUnscheduledSpendableAmount(MoneroTxWallet tx, List openOffers) { if (isScheduledWithUnknownAmount(tx, openOffers)) return BigInteger.ZERO; return getSpendableAmount(tx).subtract(getSplitAmount(tx, openOffers)).max(BigInteger.ZERO); } private boolean isScheduledWithUnknownAmount(MoneroTxWallet tx, List openOffers) { for (OpenOffer openOffer : openOffers) { if (openOffer.getScheduledTxHashes() == null) continue; if (openOffer.getScheduledTxHashes().contains(tx.getHash()) && !tx.getHash().equals(openOffer.getSplitOutputTxHash())) { return true; } } return false; } private BigInteger getSplitAmount(MoneroTxWallet tx, List openOffers) { for (OpenOffer openOffer : openOffers) { if (openOffer.getSplitOutputTxHash() == null) continue; if (!openOffer.getSplitOutputTxHash().equals(tx.getHash())) continue; return openOffer.getOffer().getAmountNeeded(); } return BigInteger.ZERO; } private BigInteger getSpendableAmount(MoneroTxWallet tx) { // compute spendable amount from outputs if confirmed if (tx.isConfirmed()) { BigInteger spendableAmount = BigInteger.ZERO; if (tx.getOutputsWallet() != null) { for (MoneroOutputWallet output : tx.getOutputsWallet()) { if (!output.isSpent() && !output.isFrozen() && output.getAccountIndex() == 0) { spendableAmount = spendableAmount.add(output.getAmount()); } } } return spendableAmount; } // funds sent to self always show 0 incoming amount, so compute from destinations manually // TODO: this excludes change output, so change is missing from spendable amount until confirmed BigInteger sentToSelfAmount = xmrWalletService.getAmountSentToSelf(tx); if (sentToSelfAmount.compareTo(BigInteger.ZERO) > 0) return sentToSelfAmount; // if not confirmed and not sent to self, return incoming amount return tx.getIncomingAmount() == null ? BigInteger.ZERO : tx.getIncomingAmount(); } private boolean hasSpendableAmount(MoneroTxWallet tx) { return getSpendableAmount(tx).compareTo(BigInteger.ZERO) > 0; } private boolean isTxScheduledByOtherOffer(List openOffers, OpenOffer openOffer, String txHash) { for (OpenOffer otherOffer : openOffers) { if (otherOffer == openOffer) continue; if (otherOffer.getState() != OpenOffer.State.PENDING) continue; if (txHash.equals(otherOffer.getSplitOutputTxHash())) return true; if (otherOffer.getScheduledTxHashes() != null) { for (String scheduledTxHash : otherOffer.getScheduledTxHashes()) { if (txHash.equals(scheduledTxHash)) return true; } } } return false; } private void signAndPostOffer(OpenOffer openOffer, boolean useSavingsWallet, // TODO: remove this? TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { log.info("Signing and posting offer " + openOffer.getId()); // create model PlaceOfferModel model = new PlaceOfferModel(openOffer, openOffer.getOffer().getAmountNeeded(), useSavingsWallet, p2PService, btcWalletService, xmrWalletService, tradeWalletService, offerBookService, arbitratorManager, mediatorManager, tradeStatisticsManager, user, keyRing, filterManager, accountAgeWitnessService, this); // create protocol PlaceOfferProtocol placeOfferProtocol = new PlaceOfferProtocol(model, transaction -> { // set offer state openOffer.setScheduledTxHashes(null); openOffer.setScheduledAmount(null); requestPersistence(); if (!stopped) { startPeriodicRepublishOffersTimer(); startPeriodicRefreshOffersTimer(); } else { log.debug("We have stopped already. We ignore that placeOfferProtocol.placeOffer.onResult call."); } resultHandler.handleResult(transaction); }, errorMessageHandler); // run protocol synchronized (placeOfferProtocols) { placeOfferProtocols.put(openOffer.getOffer().getId(), placeOfferProtocol); } placeOfferProtocol.placeOffer(); } /////////////////////////////////////////////////////////////////////////////////////////// // Arbitrator Signs Offer /////////////////////////////////////////////////////////////////////////////////////////// private void handleSignOfferRequest(SignOfferRequest request, NodeAddress peer) { log.info("Received SignOfferRequest from {} with offerId {} and uid {}", peer, request.getOfferId(), request.getUid()); boolean result = false; String errorMessage = null; try { // verify this node is an arbitrator Arbitrator thisArbitrator = user.getRegisteredArbitrator(); NodeAddress thisAddress = p2PService.getNetworkNode().getNodeAddress(); if (thisArbitrator == null || !thisArbitrator.getNodeAddress().equals(thisAddress)) { errorMessage = "Cannot sign offer because we are not a registered arbitrator"; log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } // verify arbitrator is signer of offer payload if (!thisAddress.equals(request.getOfferPayload().getArbitratorSigner())) { errorMessage = "Cannot sign offer because offer payload is for a different arbitrator"; log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } // private offers must have challenge hash Offer offer = new Offer(request.getOfferPayload()); if (offer.isPrivateOffer() && (offer.getChallengeHash() == null || offer.getChallengeHash().length() == 0)) { errorMessage = "Private offer must have challenge hash for offer " + request.offerId; log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } // verify max length of extra info if (offer.getOfferPayload().getExtraInfo() != null && offer.getOfferPayload().getExtraInfo().length() > Restrictions.getMaxExtraInfoLength()) { errorMessage = "Extra info is too long for offer " + request.offerId + ". Max length is " + Restrictions.getMaxExtraInfoLength() + " but got " + offer.getOfferPayload().getExtraInfo().length(); log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } // verify the trade protocol version if (request.getOfferPayload().getProtocolVersion() != Version.TRADE_PROTOCOL_VERSION) { errorMessage = "Unsupported protocol version: " + request.getOfferPayload().getProtocolVersion(); log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } // verify the min version number if (filterManager.getDisableTradeBelowVersion() != null) { if (Version.compare(request.getOfferPayload().getVersionNr(), filterManager.getDisableTradeBelowVersion()) < 0) { errorMessage = "Offer version number is too low: " + request.getOfferPayload().getVersionNr() + " < " + filterManager.getDisableTradeBelowVersion(); log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } } // verify market price margin double marketPriceMarginPct = request.getOfferPayload().getMarketPriceMarginPct(); if (marketPriceMarginPct <= -1 || marketPriceMarginPct >= 1) { errorMessage = "Market price margin must be greater than -100% and less than 100% but was " + (marketPriceMarginPct * 100) + "%"; log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } // verify maker and taker fees boolean hasBuyerAsTakerWithoutDeposit = offer.getDirection() == OfferDirection.SELL && offer.isPrivateOffer() && offer.getChallengeHash() != null && offer.getChallengeHash().length() > 0 && offer.getTakerFeePct() == 0; if (hasBuyerAsTakerWithoutDeposit) { // verify maker's trade fee double makerFeePct = HavenoUtils.getMakerFeePct(request.getOfferPayload().getCounterCurrencyCode(), hasBuyerAsTakerWithoutDeposit); if (offer.getMakerFeePct() != makerFeePct) { errorMessage = "Wrong maker fee for offer " + request.offerId + ". Expected " + makerFeePct + " but got " + offer.getMakerFeePct(); log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } // verify taker's trade fee if (offer.getTakerFeePct() != 0) { errorMessage = "Wrong taker fee for offer " + request.offerId + ". Expected 0 but got " + offer.getTakerFeePct(); log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } // verify maker security deposit if (offer.getSellerSecurityDepositPct() != Restrictions.getMinSecurityDepositPct()) { errorMessage = "Wrong seller security deposit for offer " + request.offerId + ". Expected " + Restrictions.getMinSecurityDepositPct() + " but got " + offer.getSellerSecurityDepositPct(); log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } // verify taker's security deposit if (offer.getBuyerSecurityDepositPct() != 0) { errorMessage = "Wrong buyer security deposit for offer " + request.offerId + ". Expected 0 but got " + offer.getBuyerSecurityDepositPct(); log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } } else { // verify public offer (remove to generally allow private offers) if (offer.isPrivateOffer() || offer.getChallengeHash() != null) { errorMessage = "Private offer " + request.offerId + " is not valid. It must have direction SELL, taker fee of 0, and a challenge hash."; log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } // verify maker's trade fee double makerFeePct = HavenoUtils.getMakerFeePct(request.getOfferPayload().getCounterCurrencyCode(), hasBuyerAsTakerWithoutDeposit); if (offer.getMakerFeePct() != makerFeePct) { errorMessage = "Wrong maker fee for offer " + request.offerId + ". Expected " + makerFeePct + " but got " + offer.getMakerFeePct(); log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } // verify taker's trade fee double takerFeePct = HavenoUtils.getTakerFeePct(request.getOfferPayload().getCounterCurrencyCode(), hasBuyerAsTakerWithoutDeposit); if (offer.getTakerFeePct() != takerFeePct) { errorMessage = "Wrong taker fee for offer " + request.offerId + ". Expected " + takerFeePct + " but got " + offer.getTakerFeePct(); log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } // verify seller's security deposit if (offer.getSellerSecurityDepositPct() < Restrictions.getMinSecurityDepositPct()) { errorMessage = "Insufficient seller security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.getMinSecurityDepositPct() + " but got " + offer.getSellerSecurityDepositPct(); log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } // verify buyer's security deposit if (offer.getBuyerSecurityDepositPct() < Restrictions.getMinSecurityDepositPct()) { errorMessage = "Insufficient buyer security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.getMinSecurityDepositPct() + " but got " + offer.getBuyerSecurityDepositPct(); log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } // security deposits must be equal if (offer.getBuyerSecurityDepositPct() != offer.getSellerSecurityDepositPct()) { errorMessage = "Buyer and seller security deposits are not equal for offer " + request.offerId + ": " + offer.getSellerSecurityDepositPct() + " vs " + offer.getBuyerSecurityDepositPct(); log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } } // verify penalty fee if (offer.getPenaltyFeePct() != HavenoUtils.PENALTY_FEE_PCT) { errorMessage = "Wrong penalty fee percent for offer " + request.offerId + ". Expected " + HavenoUtils.PENALTY_FEE_PCT + " but got " + offer.getPenaltyFeePct(); log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } // verify maker's reserve tx (double spend, trade fee, trade amount, mining fee) double makerFeePct = HavenoUtils.getMakerFeePct(request.getOfferPayload().getCounterCurrencyCode(), hasBuyerAsTakerWithoutDeposit); BigInteger maxTradeFee = HavenoUtils.multiply(offer.getAmount(), makerFeePct); BigInteger sendTradeAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount(); BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit(); BigInteger penaltyFee = HavenoUtils.multiply(securityDeposit, HavenoUtils.PENALTY_FEE_PCT); MoneroTx verifiedTx = xmrWalletService.verifyReserveTx( offer.getId(), penaltyFee, maxTradeFee, sendTradeAmount, securityDeposit, request.getPayoutAddress(), request.getReserveTxHash(), request.getReserveTxHex(), request.getReserveTxKey(), request.getReserveTxKeyImages()); // arbitrator signs offer to certify they have valid reserve tx byte[] signature = HavenoUtils.signOffer(request.getOfferPayload(), keyRing); OfferPayload signedOfferPayload = request.getOfferPayload(); signedOfferPayload.setArbitratorSignature(signature); // create record of signed offer SignedOffer signedOffer = new SignedOffer( System.currentTimeMillis(), signedOfferPayload.getPubKeyRing().hashCode(), // trader id signedOfferPayload.getId(), offer.getAmount().longValueExact(), penaltyFee.longValueExact(), request.getReserveTxHash(), request.getReserveTxHex(), request.getReserveTxKeyImages(), verifiedTx.getFee().longValueExact(), signature); // TODO (woodser): no need for signature to be part of SignedOffer? UserThread.execute(() -> addSignedOffer(signedOffer)); requestPersistence(); // send response with signature SignOfferResponse response = new SignOfferResponse(request.getOfferId(), UUID.randomUUID().toString(), Version.getP2PMessageVersion(), signedOfferPayload); p2PService.sendEncryptedDirectMessage(peer, request.getPubKeyRing(), response, new SendDirectMessageListener() { @Override public void onArrived() { log.info("{} arrived at peer: offerId={}; uid={}", response.getClass().getSimpleName(), response.getOfferId(), response.getUid()); } @Override public void onFault(String errorMessage) { log.error("Sending {} failed: uid={}; peer={}; error={}", response.getClass().getSimpleName(), response.getUid(), peer, errorMessage); } }); result = true; } catch (Throwable t) { errorMessage = "Exception at handleSignOfferRequest " + t.getMessage(); log.error(errorMessage + "\n", t); } finally { if (result == false && errorMessage == null) { log.warn("Arbitrator is NACKing SignOfferRequest for unknown reason with offerId={}. That should never happen", request.getOfferId()); log.warn("Printing stacktrace:"); Thread.dumpStack(); } sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), result, errorMessage); } } private void handleSignOfferResponse(SignOfferResponse response, NodeAddress peer) { log.info("Received SignOfferResponse from {} with offerId {} and uid {}", peer, response.getOfferId(), response.getUid()); // get previously created protocol PlaceOfferProtocol protocol; synchronized (placeOfferProtocols) { protocol = placeOfferProtocols.get(response.getOfferId()); if (protocol == null) { log.warn("No place offer protocol created for offer " + response.getOfferId()); return; } } // handle response protocol.handleSignOfferResponse(response, peer); } /////////////////////////////////////////////////////////////////////////////////////////// // OfferPayload Availability /////////////////////////////////////////////////////////////////////////////////////////// private void handleOfferAvailabilityRequest(OfferAvailabilityRequest request, NodeAddress peer) { log.info("Received OfferAvailabilityRequest from {} with offerId {} and uid {}", peer, request.getOfferId(), request.getUid()); boolean result = false; String errorMessage = null; if (!p2PService.isBootstrapped()) { errorMessage = "We got a handleOfferAvailabilityRequest but we have not bootstrapped yet."; log.info(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } // Don't allow trade start if Monero node is not fully synced if (!xmrConnectionService.isSyncedWithinTolerance()) { errorMessage = "We got a handleOfferAvailabilityRequest but our chain is not synced."; log.info(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } // Don't allow trade start if not connected to Monero node if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) { errorMessage = "We got a handleOfferAvailabilityRequest but we are not connected to a Monero node."; log.info(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } if (stopped) { errorMessage = "We have stopped already. We ignore that handleOfferAvailabilityRequest call."; log.debug(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } try { Validator.nonEmptyStringOf(request.offerId); checkNotNull(request.getPubKeyRing()); } catch (Throwable t) { errorMessage = "Message validation failed. Error=" + t.toString() + ", Message=" + request.toString(); log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } try { Optional openOfferOptional = getOpenOffer(request.offerId); AvailabilityResult availabilityResult; byte[] makerSignature = null; if (openOfferOptional.isPresent()) { OpenOffer openOffer = openOfferOptional.get(); if (!apiUserDeniedByOffer(request)) { if (!takerDeniedByMaker(request)) { if (openOffer.getState() == OpenOffer.State.AVAILABLE) { Offer offer = openOffer.getOffer(); if (preferences.getIgnoreTradersList().stream().noneMatch(fullAddress -> fullAddress.equals(peer.getFullAddress()))) { // maker signs taker's request String tradeRequestAsJson = JsonUtil.objectToJson(request.getTradeRequest()); makerSignature = HavenoUtils.sign(keyRing, tradeRequestAsJson); try { // Check also tradePrice to avoid failures after taker fee is paid caused by a too big difference // in trade price between the peers. Also here poor connectivity might cause market price API connection // losses and therefore an outdated market price. offer.verifyTradePrice(request.getTakersTradePrice()); availabilityResult = AvailabilityResult.AVAILABLE; } catch (TradePriceOutOfToleranceException e) { log.warn("Trade price check failed because takers price is outside out tolerance."); availabilityResult = AvailabilityResult.PRICE_OUT_OF_TOLERANCE; } catch (MarketPriceNotAvailableException e) { log.warn(e.getMessage()); availabilityResult = AvailabilityResult.MARKET_PRICE_NOT_AVAILABLE; } catch (Throwable e) { log.warn("Trade price check failed. " + e.getMessage()); if (coreContext.isApiUser()) // Give api user something more than 'unknown_failure'. availabilityResult = AvailabilityResult.PRICE_CHECK_FAILED; else availabilityResult = AvailabilityResult.UNKNOWN_FAILURE; } } else { availabilityResult = AvailabilityResult.USER_IGNORED; } } else { availabilityResult = AvailabilityResult.OFFER_TAKEN; } } else { availabilityResult = AvailabilityResult.MAKER_DENIED_TAKER; } } else { availabilityResult = AvailabilityResult.MAKER_DENIED_API_USER; } } else { log.warn("handleOfferAvailabilityRequest: openOffer not found."); availabilityResult = AvailabilityResult.OFFER_TAKEN; } OfferAvailabilityResponse offerAvailabilityResponse = new OfferAvailabilityResponse(request.offerId, availabilityResult, makerSignature); log.info("Send {} with offerId {}, uid {}, and result {} to peer {}", offerAvailabilityResponse.getClass().getSimpleName(), offerAvailabilityResponse.getOfferId(), offerAvailabilityResponse.getUid(), availabilityResult, peer); p2PService.sendEncryptedDirectMessage(peer, request.getPubKeyRing(), offerAvailabilityResponse, new SendDirectMessageListener() { @Override public void onArrived() { log.info("{} arrived at peer: offerId={}; uid={}", offerAvailabilityResponse.getClass().getSimpleName(), offerAvailabilityResponse.getOfferId(), offerAvailabilityResponse.getUid()); } @Override public void onFault(String errorMessage) { log.error("Sending {} failed: uid={}; peer={}; error={}", offerAvailabilityResponse.getClass().getSimpleName(), offerAvailabilityResponse.getUid(), peer, errorMessage); } }); result = true; } catch (Throwable t) { errorMessage = "Exception at handleRequestIsOfferAvailableMessage " + t.getMessage(); log.error(errorMessage + "\n", t); } finally { sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), result, errorMessage); } } private boolean apiUserDeniedByOffer(OfferAvailabilityRequest request) { return preferences.isDenyApiTaker() && request.isTakerApiUser(); } private boolean takerDeniedByMaker(OfferAvailabilityRequest request) { if (request.getTradeRequest() == null) return true; return false; // TODO (woodser): implement taker verification here, doing work of ApplyFilter and VerifyPeersAccountAgeWitness } private void sendAckMessage(Class reqClass, NodeAddress sender, PubKeyRing senderPubKeyRing, String offerId, String uid, boolean result, String errorMessage) { String sourceUid = uid; AckMessage ackMessage = new AckMessage(p2PService.getNetworkNode().getNodeAddress(), AckMessageSourceType.OFFER_MESSAGE, reqClass.getSimpleName(), sourceUid, offerId, result, errorMessage); if (ackMessage.isSuccess()) { log.info("Send AckMessage for {} to peer {} with offerId {} and sourceUid {}", reqClass.getSimpleName(), sender, offerId, ackMessage.getSourceUid()); } else { log.warn("Sending NACK for {} to peer {} with offerId {} and sourceUid {}, errorMessage={}", reqClass.getSimpleName(), sender, offerId, ackMessage.getSourceUid(), errorMessage); } p2PService.sendEncryptedDirectMessage( sender, senderPubKeyRing, ackMessage, new SendDirectMessageListener() { @Override public void onArrived() { log.info("AckMessage for {} arrived at sender {}. offerId={}, sourceUid={}", reqClass.getSimpleName(), sender, offerId, ackMessage.getSourceUid()); } @Override public void onFault(String errorMessage) { log.error("AckMessage for {} failed. AckMessage={}, sender={}, errorMessage={}", reqClass.getSimpleName(), ackMessage, sender, errorMessage); } } ); } /////////////////////////////////////////////////////////////////////////////////////////// // Update persisted offer if a new capability is required after a software update /////////////////////////////////////////////////////////////////////////////////////////// private void maybeUpdatePersistedOffers() { // update open offers List updatedOpenOffers = new ArrayList<>(); getOpenOffers().forEach(originalOpenOffer -> { Offer originalOffer = originalOpenOffer.getOffer(); OfferPayload originalOfferPayload = originalOffer.getOfferPayload(); // We added CAPABILITIES with entry for Capability.MEDIATION in v1.1.6 and // Capability.REFUND_AGENT in v1.2.0 and want to rewrite a // persisted offer after the user has updated to 1.2.0 so their offer will be accepted by the network. if (originalOfferPayload.getProtocolVersion() < Version.TRADE_PROTOCOL_VERSION || !OfferRestrictions.hasOfferMandatoryCapability(originalOffer, Capability.MEDIATION) || !OfferRestrictions.hasOfferMandatoryCapability(originalOffer, Capability.REFUND_AGENT) || !originalOfferPayload.getOwnerNodeAddress().equals(p2PService.getAddress())) { // - Capabilities changed? // We rewrite our offer with the additional capabilities entry Map updatedExtraDataMap = new HashMap<>(); if (!OfferRestrictions.hasOfferMandatoryCapability(originalOffer, Capability.MEDIATION) || !OfferRestrictions.hasOfferMandatoryCapability(originalOffer, Capability.REFUND_AGENT)) { Map originalExtraDataMap = originalOfferPayload.getExtraDataMap(); if (originalExtraDataMap != null) { updatedExtraDataMap.putAll(originalExtraDataMap); } // We overwrite any entry with our current capabilities updatedExtraDataMap.put(OfferPayload.CAPABILITIES, Capabilities.app.toStringList()); log.info("Converted offer to support new Capability.MEDIATION and Capability.REFUND_AGENT capability. id={}", originalOffer.getId()); } else { updatedExtraDataMap = originalOfferPayload.getExtraDataMap(); } // - Protocol version changed? int protocolVersion = originalOfferPayload.getProtocolVersion(); if (protocolVersion < Version.TRADE_PROTOCOL_VERSION) { // We update the trade protocol version protocolVersion = Version.TRADE_PROTOCOL_VERSION; log.info("Updated the protocol version of offer id={}", originalOffer.getId()); } // - node address changed? (due to a faulty tor dir) NodeAddress ownerNodeAddress = originalOfferPayload.getOwnerNodeAddress(); if (!ownerNodeAddress.equals(p2PService.getAddress())) { ownerNodeAddress = p2PService.getAddress(); log.info("Updated the owner nodeaddress of offer id={}", originalOffer.getId()); } long normalizedPrice = originalOffer.isInverted() ? PriceUtil.invertLongPrice(originalOfferPayload.getPrice(), originalOffer.getCounterCurrencyCode()) : originalOfferPayload.getPrice(); OfferPayload updatedPayload = new OfferPayload(originalOfferPayload.getId(), originalOfferPayload.getDate(), ownerNodeAddress, originalOfferPayload.getPubKeyRing(), originalOfferPayload.getDirection(), normalizedPrice, originalOfferPayload.getMarketPriceMarginPct(), originalOfferPayload.isUseMarketBasedPrice(), originalOfferPayload.getAmount(), originalOfferPayload.getMinAmount(), originalOfferPayload.getMakerFeePct(), originalOfferPayload.getTakerFeePct(), HavenoUtils.PENALTY_FEE_PCT, originalOfferPayload.getBuyerSecurityDepositPct(), originalOfferPayload.getSellerSecurityDepositPct(), originalOffer.getBaseCurrencyCode(), originalOffer.getCounterCurrencyCode(), originalOfferPayload.getPaymentMethodId(), originalOfferPayload.getMakerPaymentAccountId(), originalOfferPayload.getCountryCode(), originalOfferPayload.getAcceptedCountryCodes(), originalOfferPayload.getBankId(), originalOfferPayload.getAcceptedBankIds(), Version.VERSION, originalOfferPayload.getBlockHeightAtOfferCreation(), originalOfferPayload.getMaxTradeLimit(), originalOfferPayload.getMaxTradePeriod(), originalOfferPayload.isUseAutoClose(), originalOfferPayload.isUseReOpenAfterAutoClose(), originalOfferPayload.getLowerClosePrice(), originalOfferPayload.getUpperClosePrice(), originalOfferPayload.isPrivateOffer(), originalOfferPayload.getChallengeHash(), updatedExtraDataMap, protocolVersion, null, null, null, originalOfferPayload.getExtraInfo()); // cancel old offer log.info("Canceling outdated offer id={}", originalOffer.getId()); doCancelOffer(originalOpenOffer, false); // create new offer Offer updatedOffer = new Offer(updatedPayload); updatedOffer.setPriceFeedService(priceFeedService); long normalizedTriggerPrice = originalOffer.isInverted() ? PriceUtil.invertLongPrice(originalOpenOffer.getTriggerPrice(), originalOffer.getCounterCurrencyCode()) : originalOpenOffer.getTriggerPrice(); OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer, normalizedTriggerPrice, originalOpenOffer.isReserveExactAmount(), originalOpenOffer.getGroupId()); updatedOpenOffer.setChallenge(originalOpenOffer.getChallenge()); updatedOpenOffers.add(updatedOpenOffer); } }); // add updated open offers updatedOpenOffers.forEach(updatedOpenOffer -> { addOpenOffer(updatedOpenOffer); requestPersistence(); log.info("Updating offer completed. id={}", updatedOpenOffer.getId()); }); } /////////////////////////////////////////////////////////////////////////////////////////// // RepublishOffers, refreshOffers /////////////////////////////////////////////////////////////////////////////////////////// private void republishOffers() { if (stopped) { return; } stopPeriodicRefreshOffersTimer(); ThreadUtils.execute(() -> { processListForRepublishOffers(new ArrayList<>(getOpenOffers())); // list will be modified }, THREAD_ID); } // modifies the given list private void processListForRepublishOffers(List list) { if (list.isEmpty()) { return; } OpenOffer openOffer = list.remove(0); boolean contained = false; synchronized (openOffers.getList()) { contained = openOffers.contains(openOffer); } if (contained) { // TODO It is not clear yet if it is better for the node and the network to send out all add offer // messages in one go or to spread it over a delay. With power users who have 100-200 offers that can have // some significant impact to user experience and the network maybeRepublishOffer(openOffer, () -> processListForRepublishOffers(list)); /* republishOffer(openOffer, () -> UserThread.runAfter(() -> processListForRepublishOffers(list), 30, TimeUnit.MILLISECONDS));*/ } else { // If the offer was removed in the meantime or if its deactivated we skip and call // processListForRepublishOffers again with the list where we removed the offer already. processListForRepublishOffers(list); } } private void maybeRepublishOffer(OpenOffer openOffer, @Nullable Runnable completeHandler) { ThreadUtils.execute(() -> { // skip if prevented from publishing if (preventedFromPublishing(openOffer, false)) { if (completeHandler != null) completeHandler.run(); return; } // reprocess offer then publish synchronized (processOffersLock) { CountDownLatch latch = new CountDownLatch(1); processOffer(getOpenOffers(), openOffer, (transaction) -> { requestPersistence(); latch.countDown(); // skip if prevented from publishing if (preventedFromPublishing(openOffer, true)) { if (completeHandler != null) completeHandler.run(); return; } // publish offer to books offerBookService.addOffer(openOffer.getOffer(), () -> { if (!stopped) { // refresh means we send only the data needed to refresh the TTL (hash, signature and sequence no.) if (periodicRefreshOffersTimer == null) { startPeriodicRefreshOffersTimer(); } if (completeHandler != null) { completeHandler.run(); } } }, errorMessage -> { if (!stopped) { log.error("Adding offer to P2P network failed. " + errorMessage); stopRetryRepublishOffersTimer(); retryRepublishOffersTimer = UserThread.runAfter(OpenOfferManager.this::republishOffers, RETRY_REPUBLISH_DELAY_SEC); if (completeHandler != null) completeHandler.run(); } }); }, (errorMessage) -> { log.warn("Error republishing offer {}: {}", openOffer.getId(), errorMessage); latch.countDown(); if (completeHandler != null) completeHandler.run(); }); HavenoUtils.awaitLatch(latch); } }, THREAD_ID); } private boolean preventedFromPublishing(OpenOffer openOffer, boolean checkSignature) { if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) return true; return openOffer.isDeactivated() || openOffer.isCanceled() || (checkSignature && openOffer.getOffer().getOfferPayload().getArbitratorSigner() == null) || hasConflictingClone(openOffer); } private void startPeriodicRepublishOffersTimer() { stopped = false; if (periodicRepublishOffersTimer == null) { periodicRepublishOffersTimer = UserThread.runPeriodically(() -> { if (!stopped) { republishOffers(); } }, REPUBLISH_INTERVAL_MS, TimeUnit.MILLISECONDS); } } private void startPeriodicRefreshOffersTimer() { stopped = false; // refresh sufficiently before offer would expire if (periodicRefreshOffersTimer == null) periodicRefreshOffersTimer = UserThread.runPeriodically(() -> { // TODO: this runs on user thread so can block if (!stopped) { log.info("Refreshing my open offers"); synchronized (openOffers.getList()) { int size = openOffers.size(); //we clone our list as openOffers might change during our delayed call final ArrayList openOffersList = new ArrayList<>(openOffers.getList()); for (int i = 0; i < size; i++) { // we delay to avoid reaching throttle limits // roughly 4 offers per second long delay = 300; final long minDelay = (i + 1) * delay; final long maxDelay = (i + 2) * delay; final OpenOffer openOffer = openOffersList.get(i); UserThread.runAfterRandomDelay(() -> { // we need to check if in the meantime the offer has been removed boolean contained = false; synchronized (openOffers.getList()) { contained = openOffers.contains(openOffer); } if (contained) maybeRefreshOffer(openOffer, 0, 1); }, minDelay, maxDelay, TimeUnit.MILLISECONDS); } } } else { log.debug("We have stopped already. We ignore that periodicRefreshOffersTimer.run call."); } }, REFRESH_INTERVAL_MS, TimeUnit.MILLISECONDS); else log.trace("periodicRefreshOffersTimer already stated"); } private void maybeRefreshOffer(OpenOffer openOffer, int numAttempts, int maxAttempts) { if (preventedFromPublishing(openOffer, true)) return; offerBookService.refreshTTL(openOffer.getOffer().getOfferPayload(), () -> log.debug("Successful refreshed TTL for offer"), (errorMessage) -> { log.warn(errorMessage); if (numAttempts + 1 < maxAttempts) { UserThread.runAfter(() -> maybeRefreshOffer(openOffer, numAttempts + 1, maxAttempts), 10); } }); } private void restart() { log.debug("Restart after connection loss"); if (retryRepublishOffersTimer == null) retryRepublishOffersTimer = UserThread.runAfter(() -> { stopped = false; stopRetryRepublishOffersTimer(); republishOffers(); }, RETRY_REPUBLISH_DELAY_SEC); startPeriodicRepublishOffersTimer(); } private void requestPersistence() { persistenceManager.requestPersistence(); signedOfferPersistenceManager.requestPersistence(); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void stopPeriodicRefreshOffersTimer() { if (periodicRefreshOffersTimer != null) { periodicRefreshOffersTimer.stop(); periodicRefreshOffersTimer = null; } } private void stopPeriodicRepublishOffersTimer() { if (periodicRepublishOffersTimer != null) { periodicRepublishOffersTimer.stop(); periodicRepublishOffersTimer = null; } } private void stopRetryRepublishOffersTimer() { if (retryRepublishOffersTimer != null) { retryRepublishOffersTimer.stop(); retryRepublishOffersTimer = null; } } } ================================================ FILE: core/src/main/java/haveno/core/offer/SignedOffer.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.offer; import com.google.protobuf.ByteString; import haveno.common.proto.ProtoUtil; import haveno.common.proto.persistable.PersistablePayload; import haveno.core.util.JsonUtil; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.util.List; @EqualsAndHashCode @Slf4j public final class SignedOffer implements PersistablePayload { @Getter private final long timeStamp; @Getter private int traderId; @Getter private final String offerId; @Getter private final long tradeAmount; @Getter private final long penaltyAmount; @Getter private final String reserveTxHash; @Getter private final String reserveTxHex; @Getter private final List reserveTxKeyImages; @Getter private final long reserveTxMinerFee; @Getter private final byte[] arbitratorSignature; public SignedOffer(long timeStamp, int traderId, String offerId, long tradeAmount, long penaltyAmount, String reserveTxHash, String reserveTxHex, List reserveTxKeyImages, long reserveTxMinerFee, byte[] arbitratorSignature) { this.timeStamp = timeStamp; this.traderId = traderId; this.offerId = offerId; this.tradeAmount = tradeAmount; this.penaltyAmount = penaltyAmount; this.reserveTxHash = reserveTxHash; this.reserveTxHex = reserveTxHex; this.reserveTxKeyImages = reserveTxKeyImages; this.reserveTxMinerFee = reserveTxMinerFee; this.arbitratorSignature = arbitratorSignature; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.SignedOffer toProtoMessage() { protobuf.SignedOffer.Builder builder = protobuf.SignedOffer.newBuilder() .setTimeStamp(timeStamp) .setTraderId(traderId) .setOfferId(offerId) .setTradeAmount(tradeAmount) .setPenaltyAmount(penaltyAmount) .setReserveTxHash(reserveTxHash) .setReserveTxHex(reserveTxHex) .addAllReserveTxKeyImages(reserveTxKeyImages) .setReserveTxMinerFee(reserveTxMinerFee) .setArbitratorSignature(ByteString.copyFrom(arbitratorSignature)); return builder.build(); } public static SignedOffer fromProto(protobuf.SignedOffer proto) { return new SignedOffer(proto.getTimeStamp(), proto.getTraderId(), proto.getOfferId(), proto.getTradeAmount(), proto.getPenaltyAmount(), proto.getReserveTxHash(), proto.getReserveTxHex(), proto.getReserveTxKeyImagesList(), proto.getReserveTxMinerFee(), ProtoUtil.byteArrayOrNullFromProto(proto.getArbitratorSignature())); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// public String toJson() { return JsonUtil.objectToJson(this); } @Override public String toString() { return "SignedOffer{" + ",\n timeStamp=" + timeStamp + ",\n traderId=" + traderId + ",\n offerId=" + offerId + ",\n tradeAmount=" + tradeAmount + ",\n penaltyAmount=" + penaltyAmount + ",\n reserveTxHash=" + reserveTxHash + ",\n reserveTxHex=" + reserveTxHex + ",\n reserveTxKeyImages=" + reserveTxKeyImages + ",\n reserveTxMinerFee=" + reserveTxMinerFee + ",\n arbitratorSignature=" + arbitratorSignature + "\n}"; } } ================================================ FILE: core/src/main/java/haveno/core/offer/SignedOfferList.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.offer; import com.google.protobuf.Message; import haveno.common.proto.ProtoUtil; import haveno.common.proto.persistable.PersistableListAsObservable; import lombok.extern.slf4j.Slf4j; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; @Slf4j public final class SignedOfferList extends PersistableListAsObservable { /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public SignedOfferList() { } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// protected SignedOfferList(Collection collection) { super(collection); } @Override public Message toProtoMessage() { synchronized (getList()) { return protobuf.PersistableEnvelope.newBuilder() .setSignedOfferList(protobuf.SignedOfferList.newBuilder() .addAllSignedOffer(ProtoUtil.collectionToProto(getList(), protobuf.SignedOffer.class))) .build(); } } public static SignedOfferList fromProto(protobuf.SignedOfferList proto) { List list = proto.getSignedOfferList().stream() .map(signedOffer -> { return SignedOffer.fromProto(signedOffer); }) .collect(Collectors.toList()); return new SignedOfferList(list); } @Override public String toString() { return "SignedOfferList{" + ",\n list=" + getList() + "\n}"; } } ================================================ FILE: core/src/main/java/haveno/core/offer/TriggerPriceService.java ================================================ /* * 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 . */ package haveno.core.offer; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.util.MathUtils; import static haveno.common.util.MathUtils.roundDoubleToLong; import static haveno.common.util.MathUtils.scaleUpByPowerOf10; import haveno.core.locale.CurrencyUtil; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.Price; import haveno.core.monetary.TraditionalMoney; import haveno.core.provider.price.MarketPrice; import haveno.core.provider.price.PriceFeedService; import haveno.network.p2p.BootstrapListener; import haveno.network.p2p.P2PService; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import javafx.collections.ListChangeListener; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton public class TriggerPriceService { private final P2PService p2PService; private final OpenOfferManager openOfferManager; private final PriceFeedService priceFeedService; private final Map> openOffersByCurrency = new HashMap<>(); @Inject public TriggerPriceService(P2PService p2PService, OpenOfferManager openOfferManager, PriceFeedService priceFeedService) { this.p2PService = p2PService; this.openOfferManager = openOfferManager; this.priceFeedService = priceFeedService; } public void onAllServicesInitialized() { if (p2PService.isBootstrapped()) { onBootstrapComplete(); } else { p2PService.addP2PServiceListener(new BootstrapListener() { @Override public void onDataReceived() { onBootstrapComplete(); } }); } } private void onBootstrapComplete() { openOfferManager.getObservableList().addListener((ListChangeListener) c -> { c.next(); if (c.wasAdded()) { onAddedOpenOffers(c.getAddedSubList()); } if (c.wasRemoved()) { onRemovedOpenOffers(c.getRemoved()); } }); onAddedOpenOffers(openOfferManager.getObservableList()); priceFeedService.updateCounterProperty().addListener((observable, oldValue, newValue) -> onPriceFeedChanged()); onPriceFeedChanged(); } private void onPriceFeedChanged() { openOffersByCurrency.keySet().stream() .map(priceFeedService::getMarketPrice) .filter(Objects::nonNull) .filter(marketPrice -> openOffersByCurrency.containsKey(marketPrice.getCurrencyCode())) .forEach(marketPrice -> { openOffersByCurrency.get(marketPrice.getCurrencyCode()).stream() .forEach(openOffer -> checkPriceThreshold(marketPrice, openOffer)); }); } public static boolean isTriggered(MarketPrice marketPrice, OpenOffer openOffer) { Price price = openOffer.getOffer().getPrice(); if (price == null || marketPrice == null) { return false; } String currencyCode = openOffer.getOffer().getCounterCurrencyCode(); boolean traditionalCurrency = CurrencyUtil.isTraditionalCurrency(currencyCode); int smallestUnitExponent = traditionalCurrency ? TraditionalMoney.SMALLEST_UNIT_EXPONENT : CryptoMoney.SMALLEST_UNIT_EXPONENT; long marketPriceAsLong = roundDoubleToLong( scaleUpByPowerOf10(marketPrice.getPrice(), smallestUnitExponent)); long triggerPrice = openOffer.getTriggerPrice(); if (triggerPrice <= 0) { return false; } OfferDirection direction = openOffer.getOffer().getDirection(); boolean isSellOffer = direction == OfferDirection.SELL; return isSellOffer ? marketPriceAsLong < triggerPrice : marketPriceAsLong > triggerPrice; } private void checkPriceThreshold(MarketPrice marketPrice, OpenOffer openOffer) { String currencyCode = openOffer.getOffer().getCounterCurrencyCode(); int smallestUnitExponent = CurrencyUtil.isTraditionalCurrency(currencyCode) ? TraditionalMoney.SMALLEST_UNIT_EXPONENT : CryptoMoney.SMALLEST_UNIT_EXPONENT; if (openOffer.getState() == OpenOffer.State.AVAILABLE && isTriggered(marketPrice, openOffer)) { log.info("Market price exceeded the trigger price of the open offer.\n" + "We deactivate the open offer with ID {}.\nCurrency: {};\nOffer direction: {};\n" + "Market price: {};\nTrigger price: {}", openOffer.getOffer().getShortId(), currencyCode, openOffer.getOffer().getDirection(), marketPrice.getPrice(), MathUtils.scaleDownByPowerOf10(openOffer.getTriggerPrice(), smallestUnitExponent) ); openOfferManager.deactivateOpenOffer(openOffer, true, () -> { }, errorMessage -> { }); } else if (openOffer.getState() == OpenOffer.State.DEACTIVATED && openOffer.isDeactivatedByTrigger() && !isTriggered(marketPrice, openOffer)) { log.info("Market price is back within the trigger price of the open offer.\n" + "We reactivate the open offer with ID {}.\nCurrency: {};\nOffer direction: {};\n" + "Market price: {};\nTrigger price: {}", openOffer.getOffer().getShortId(), currencyCode, openOffer.getOffer().getDirection(), marketPrice.getPrice(), MathUtils.scaleDownByPowerOf10(openOffer.getTriggerPrice(), smallestUnitExponent) ); openOfferManager.activateOpenOffer(openOffer, () -> { }, errorMessage -> { }); } } private void onAddedOpenOffers(List openOffers) { openOffers.forEach(openOffer -> { String currencyCode = openOffer.getOffer().getCounterCurrencyCode(); openOffersByCurrency.putIfAbsent(currencyCode, new HashSet<>()); openOffersByCurrency.get(currencyCode).add(openOffer); MarketPrice marketPrice = priceFeedService.getMarketPrice(openOffer.getOffer().getCounterCurrencyCode()); if (marketPrice != null) { checkPriceThreshold(marketPrice, openOffer); } }); } private void onRemovedOpenOffers(List openOffers) { openOffers.forEach(openOffer -> { String currencyCode = openOffer.getOffer().getCounterCurrencyCode(); if (openOffersByCurrency.containsKey(currencyCode)) { Set set = openOffersByCurrency.get(currencyCode); set.remove(openOffer); if (set.isEmpty()) { openOffersByCurrency.remove(currencyCode); } } }); } } ================================================ FILE: core/src/main/java/haveno/core/offer/availability/DisputeAgentSelection.java ================================================ /* * 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 . */ package haveno.core.offer.availability; import com.google.common.annotations.VisibleForTesting; import haveno.common.util.Tuple2; import haveno.core.support.dispute.agent.DisputeAgent; import haveno.core.support.dispute.agent.DisputeAgentManager; import haveno.core.trade.statistics.TradeStatistics3; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Random; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkArgument; @Slf4j public class DisputeAgentSelection { public static final int LOOK_BACK_RANGE = 100; public static T getLeastUsedArbitrator(TradeStatisticsManager tradeStatisticsManager, DisputeAgentManager disputeAgentManager) { return getLeastUsedDisputeAgent(tradeStatisticsManager, disputeAgentManager, null); } public static T getLeastUsedArbitrator(TradeStatisticsManager tradeStatisticsManager, DisputeAgentManager disputeAgentManager, Set excludedArbitrator) { return getLeastUsedDisputeAgent(tradeStatisticsManager, disputeAgentManager, excludedArbitrator); } private static T getLeastUsedDisputeAgent(TradeStatisticsManager tradeStatisticsManager, DisputeAgentManager disputeAgentManager, Set excludedDisputeAgents) { // We take last 100 entries from trade statistics List list = tradeStatisticsManager.getTradeStatisticsListCopy(); list.sort(Comparator.comparing(TradeStatistics3::getDateAsLong)); Collections.reverse(list); if (!list.isEmpty()) { int max = Math.min(list.size(), LOOK_BACK_RANGE); list = list.subList(0, max); } // We stored only first 4 chars of disputeAgents onion address List lastAddressesUsedInTrades = list.stream() .map(tradeStatistics3 -> tradeStatistics3.getArbitrator()) .filter(Objects::nonNull) .collect(Collectors.toList()); Set disputeAgents = disputeAgentManager.getObservableMap().values().stream() .map(disputeAgent -> disputeAgent.getNodeAddress().getFullAddress()) .collect(Collectors.toSet()); if (excludedDisputeAgents != null) disputeAgents.removeAll(excludedDisputeAgents.stream().map(NodeAddress::getFullAddress).collect(Collectors.toList())); if (disputeAgents.isEmpty()) return null; String result = getLeastUsedDisputeAgent(lastAddressesUsedInTrades, disputeAgents); Optional optionalDisputeAgent = disputeAgentManager.getObservableMap().values().stream() .filter(e -> e.getNodeAddress().getFullAddress().equals(result)) .findAny(); checkArgument(optionalDisputeAgent.isPresent(), "optionalDisputeAgent has to be present"); return optionalDisputeAgent.get(); } @VisibleForTesting static String getLeastUsedDisputeAgent(List lastAddressesUsedInTrades, Set disputeAgents) { checkArgument(!disputeAgents.isEmpty(), "disputeAgents must not be empty"); List> disputeAgentTuples = disputeAgents.stream() .map(e -> new Tuple2<>(e, new AtomicInteger(0))) .collect(Collectors.toList()); disputeAgentTuples.forEach(tuple -> { int count = (int) lastAddressesUsedInTrades.stream() .filter(tuple.first::startsWith) // we use only first 4 chars for comparing .mapToInt(e -> 1) .count(); tuple.second.set(count); }); disputeAgentTuples.sort(Comparator.comparing(e -> e.first)); disputeAgentTuples.sort(Comparator.comparingInt(e -> e.second.get())); return disputeAgentTuples.get(0).first; } public static T getRandomArbitrator(DisputeAgentManager disputeAgentManager) { return getRandomArbitrator(disputeAgentManager, null); } public static T getRandomArbitrator(DisputeAgentManager disputeAgentManager, Set excludedArbitrator) { return getRandomDisputeAgent(disputeAgentManager, excludedArbitrator); } private static T getRandomDisputeAgent(DisputeAgentManager disputeAgentManager, Set excludedDisputeAgents) { // get all dispute agents Set disputeAgents = disputeAgentManager.getObservableMap().values().stream() .map(disputeAgent -> disputeAgent.getNodeAddress().getFullAddress()) .collect(Collectors.toSet()); // remove excluded dispute agents if (excludedDisputeAgents != null) disputeAgents.removeAll(excludedDisputeAgents.stream().map(NodeAddress::getFullAddress).collect(Collectors.toList())); if (disputeAgents.isEmpty()) return null; // get random dispute agent String result = getRandomDisputeAgent(disputeAgents); Optional optionalDisputeAgent = disputeAgentManager.getObservableMap().values().stream() .filter(e -> e.getNodeAddress().getFullAddress().equals(result)) .findAny(); checkArgument(optionalDisputeAgent.isPresent(), "optionalDisputeAgent has to be present"); return optionalDisputeAgent.get(); } private static String getRandomDisputeAgent(Set disputeAgents) { int randomIndex = new Random().nextInt(disputeAgents.size()); List elements = new ArrayList(disputeAgents); return elements.get(randomIndex); } } ================================================ FILE: core/src/main/java/haveno/core/offer/availability/OfferAvailabilityModel.java ================================================ /* * 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 . */ package haveno.core.offer.availability; import haveno.common.crypto.PubKeyRing; import haveno.common.taskrunner.Model; import haveno.core.offer.Offer; import haveno.core.offer.OfferUtil; import haveno.core.offer.messages.OfferAvailabilityResponse; import haveno.core.support.dispute.mediation.mediator.MediatorManager; import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.User; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import lombok.Getter; import lombok.Setter; import java.math.BigInteger; import javax.annotation.Nullable; public class OfferAvailabilityModel implements Model { @Getter private final Offer offer; @Getter private final PubKeyRing pubKeyRing; // takers PubKey (my pubkey) @Getter private final XmrWalletService xmrWalletService; @Getter private final P2PService p2PService; @Getter final private User user; @Getter private final MediatorManager mediatorManager; @Getter private final TradeStatisticsManager tradeStatisticsManager; private NodeAddress peerNodeAddress; // maker private OfferAvailabilityResponse message; @Getter private String paymentAccountId; @Getter private BigInteger tradeAmount; @Getter private OfferUtil offerUtil; @Getter @Setter private InitTradeRequest tradeRequest; @Nullable @Setter @Getter private byte[] makerSignature; // Added in v1.5.5 @Getter private final boolean isTakerApiUser; public OfferAvailabilityModel(Offer offer, PubKeyRing pubKeyRing, XmrWalletService xmrWalletService, P2PService p2PService, User user, MediatorManager mediatorManager, TradeStatisticsManager tradeStatisticsManager, boolean isTakerApiUser, String paymentAccountId, BigInteger tradeAmount, OfferUtil offerUtil) { this.offer = offer; this.pubKeyRing = pubKeyRing; this.xmrWalletService = xmrWalletService; this.p2PService = p2PService; this.user = user; this.mediatorManager = mediatorManager; this.tradeStatisticsManager = tradeStatisticsManager; this.isTakerApiUser = isTakerApiUser; this.paymentAccountId = paymentAccountId; this.tradeAmount = tradeAmount; this.offerUtil = offerUtil; } public NodeAddress getPeerNodeAddress() { return peerNodeAddress; } void setPeerNodeAddress(NodeAddress peerNodeAddress) { this.peerNodeAddress = peerNodeAddress; } public void setMessage(OfferAvailabilityResponse message) { this.message = message; } public OfferAvailabilityResponse getMessage() { return message; } public long getTakersTradePrice() { return offer.getPrice() != null ? offer.getPrice().getValue() : 0; } @Override public void onComplete() { } } ================================================ FILE: core/src/main/java/haveno/core/offer/availability/OfferAvailabilityProtocol.java ================================================ /* * 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 . */ package haveno.core.offer.availability; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.crypto.PubKeyRing; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.taskrunner.TaskRunner; import haveno.core.offer.Offer; import haveno.core.offer.availability.tasks.ProcessOfferAvailabilityResponse; import haveno.core.offer.availability.tasks.SendOfferAvailabilityRequest; import haveno.core.offer.messages.OfferAvailabilityResponse; import haveno.core.offer.messages.OfferMessage; import haveno.core.util.Validator; import haveno.network.p2p.AckMessage; import haveno.network.p2p.AckMessageSourceType; import haveno.network.p2p.DecryptedDirectMessageListener; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.SendDirectMessageListener; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; @Slf4j public class OfferAvailabilityProtocol { private static final long TIMEOUT = 90; private final OfferAvailabilityModel model; private final ResultHandler resultHandler; private final ErrorMessageHandler errorMessageHandler; private final DecryptedDirectMessageListener decryptedDirectMessageListener; private TaskRunner taskRunner; private Timer timeoutTimer; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public OfferAvailabilityProtocol(OfferAvailabilityModel model, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { this.model = model; this.resultHandler = resultHandler; this.errorMessageHandler = errorMessageHandler; decryptedDirectMessageListener = (decryptedMessageWithPubKey, peersNodeAddress) -> { NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); if (networkEnvelope instanceof OfferMessage) { OfferMessage offerMessage = (OfferMessage) networkEnvelope; Validator.nonEmptyStringOf(offerMessage.offerId); if (networkEnvelope instanceof OfferAvailabilityResponse && model.getOffer().getId().equals(offerMessage.offerId)) { handleOfferAvailabilityResponse((OfferAvailabilityResponse) networkEnvelope, peersNodeAddress); } } }; } private void cleanup() { stopTimeout(); model.getP2PService().removeDecryptedDirectMessageListener(decryptedDirectMessageListener); } /////////////////////////////////////////////////////////////////////////////////////////// // Called from UI /////////////////////////////////////////////////////////////////////////////////////////// public void sendOfferAvailabilityRequest() { // reset model.getOffer().setState(Offer.State.UNKNOWN); model.getP2PService().addDecryptedDirectMessageListener(decryptedDirectMessageListener); model.setPeerNodeAddress(model.getOffer().getMakerNodeAddress()); taskRunner = new TaskRunner<>(model, () -> handleTaskRunnerSuccess("TaskRunner at sendOfferAvailabilityRequest completed", null), errorMessage -> handleTaskRunnerFault(errorMessage, null) ); taskRunner.addTasks(SendOfferAvailabilityRequest.class); startTimeout(); taskRunner.run(); } public void cancel() { taskRunner.cancel(); cleanup(); } /////////////////////////////////////////////////////////////////////////////////////////// // Incoming message handling /////////////////////////////////////////////////////////////////////////////////////////// private void handleOfferAvailabilityResponse(OfferAvailabilityResponse message, NodeAddress peersNodeAddress) { log.info("Received OfferAvailabilityResponse from {} with offerId {} and uid {}", peersNodeAddress, message.getOfferId(), message.getUid()); stopTimeout(); startTimeout(); model.setMessage(message); taskRunner = new TaskRunner<>(model, () -> { handleTaskRunnerSuccess("TaskRunner at handle OfferAvailabilityResponse completed", message); stopTimeout(); resultHandler.handleResult(); }, errorMessage -> handleTaskRunnerFault(errorMessage, message)); taskRunner.addTasks(ProcessOfferAvailabilityResponse.class); taskRunner.run(); } private void startTimeout() { if (timeoutTimer == null) { timeoutTimer = UserThread.runAfter(() -> { log.debug("Timeout reached at " + this); model.getOffer().setState(Offer.State.MAKER_OFFLINE); errorMessageHandler.handleErrorMessage("Timeout reached: Peer has not responded."); }, TIMEOUT); } else { log.warn("timeoutTimer already created. That must not happen."); } } private void stopTimeout() { if (timeoutTimer != null) { timeoutTimer.stop(); timeoutTimer = null; } } private void handleTaskRunnerSuccess(String info, @Nullable OfferAvailabilityResponse message) { log.debug("handleTaskRunnerSuccess " + info); if (message != null) sendAckMessage(message, true, null); } private void handleTaskRunnerFault(String errorMessage, @Nullable OfferAvailabilityResponse message) { log.error(errorMessage); stopTimeout(); errorMessageHandler.handleErrorMessage(errorMessage); if (message != null) sendAckMessage(message, false, errorMessage); } private void sendAckMessage(OfferAvailabilityResponse message, boolean result, @Nullable String errorMessage) { String offerId = message.getOfferId(); String sourceUid = message.getUid(); final NodeAddress makersNodeAddress = model.getPeerNodeAddress(); PubKeyRing makersPubKeyRing = model.getOffer().getPubKeyRing(); log.info("Send AckMessage for OfferAvailabilityResponse to peer {} with offerId {} and sourceUid {}", makersNodeAddress, offerId, sourceUid); AckMessage ackMessage = new AckMessage(model.getP2PService().getNetworkNode().getNodeAddress(), AckMessageSourceType.OFFER_MESSAGE, message.getClass().getSimpleName(), sourceUid, offerId, result, errorMessage); model.getP2PService().sendEncryptedDirectMessage( makersNodeAddress, makersPubKeyRing, ackMessage, new SendDirectMessageListener() { @Override public void onArrived() { log.info("AckMessage for OfferAvailabilityResponse arrived at makersNodeAddress {}. " + "offerId={}, sourceUid={}", makersNodeAddress, offerId, ackMessage.getSourceUid()); } @Override public void onFault(String errorMessage) { log.error("AckMessage for OfferAvailabilityResponse failed. AckMessage={}, " + "makersNodeAddress={}, errorMessage={}", ackMessage, makersNodeAddress, errorMessage); } } ); } } ================================================ FILE: core/src/main/java/haveno/core/offer/availability/tasks/ProcessOfferAvailabilityResponse.java ================================================ /* * 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 . */ package haveno.core.offer.availability.tasks; import haveno.common.taskrunner.Task; import haveno.common.taskrunner.TaskRunner; import haveno.core.offer.AvailabilityResult; import haveno.core.offer.Offer; import haveno.core.offer.availability.OfferAvailabilityModel; import haveno.core.offer.messages.OfferAvailabilityResponse; import lombok.extern.slf4j.Slf4j; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class ProcessOfferAvailabilityResponse extends Task { public ProcessOfferAvailabilityResponse(TaskRunner taskHandler, OfferAvailabilityModel model) { super(taskHandler, model); } @Override protected void run() { Offer offer = model.getOffer(); try { runInterceptHook(); checkArgument(offer.getState() != Offer.State.REMOVED, "Offer state must not be Offer.State.REMOVED"); // check availability result OfferAvailabilityResponse offerAvailabilityResponse = model.getMessage(); if (offerAvailabilityResponse.getAvailabilityResult() != AvailabilityResult.AVAILABLE) { offer.setState(Offer.State.NOT_AVAILABLE); failed("Take offer attempt rejected because of: " + offerAvailabilityResponse.getAvailabilityResult()); return; } offer.setState(Offer.State.AVAILABLE); model.setMakerSignature(offerAvailabilityResponse.getMakerSignature()); checkNotNull(model.getMakerSignature()); complete(); } catch (Throwable t) { offer.setErrorMessage("An error occurred.\n" + "Error message:\n" + t.getMessage()); failed(t); } } } ================================================ FILE: core/src/main/java/haveno/core/offer/availability/tasks/SendOfferAvailabilityRequest.java ================================================ /* * 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 . */ package haveno.core.offer.availability.tasks; import haveno.common.app.Version; import haveno.common.taskrunner.Task; import haveno.common.taskrunner.TaskRunner; import haveno.core.monetary.Price; import haveno.core.offer.Offer; import haveno.core.offer.availability.OfferAvailabilityModel; import haveno.core.offer.messages.OfferAvailabilityRequest; import haveno.core.trade.HavenoUtils; import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.messages.TradeProtocolVersion; import haveno.core.user.User; import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.P2PService; import haveno.network.p2p.SendDirectMessageListener; import lombok.extern.slf4j.Slf4j; import java.util.Date; import java.util.UUID; // TODO (woodser): rename to TakerSendOfferAvailabilityRequest and group with other taker tasks @Slf4j public class SendOfferAvailabilityRequest extends Task { public SendOfferAvailabilityRequest(TaskRunner taskHandler, OfferAvailabilityModel model) { super(taskHandler, model); } @Override protected void run() { try { runInterceptHook(); // collect fields Offer offer = model.getOffer(); User user = model.getUser(); P2PService p2PService = model.getP2PService(); XmrWalletService walletService = model.getXmrWalletService(); String makerPaymentAccountId = offer.getOfferPayload().getMakerPaymentAccountId(); String takerPaymentAccountId = model.getPaymentAccountId(); String paymentMethodId = user.getPaymentAccount(takerPaymentAccountId).getPaymentAccountPayload().getPaymentMethodId(); String payoutAddress = walletService.getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); // taker signs offer using offer id as nonce to avoid challenge protocol byte[] sig = HavenoUtils.sign(model.getP2PService().getKeyRing(), offer.getId()); // get price Price price = offer.getPrice(); if (price == null) throw new RuntimeException("Could not get price for offer"); // send InitTradeRequest to maker to sign InitTradeRequest tradeRequest = new InitTradeRequest( TradeProtocolVersion.MULTISIG_2_3, // TODO: replace with first of their accepted protocols offer.getId(), model.getTradeAmount().longValueExact(), price.getValue(), paymentMethodId, null, user.getAccountId(), makerPaymentAccountId, takerPaymentAccountId, p2PService.getKeyRing().getPubKeyRing(), UUID.randomUUID().toString(), Version.getP2PMessageVersion(), sig, new Date().getTime(), offer.getMakerNodeAddress(), P2PService.getMyNodeAddress(), null, // maker provides node address of backup arbitrator on response null, // reserve tx not sent from taker to maker null, null, payoutAddress, null); // challenge is required when offer taken // save trade request to later send to arbitrator model.setTradeRequest(tradeRequest); OfferAvailabilityRequest message = new OfferAvailabilityRequest(model.getOffer().getId(), model.getPubKeyRing(), model.getTakersTradePrice(), model.isTakerApiUser(), tradeRequest); log.info("Send {} with offerId {} and uid {} to peer {}", message.getClass().getSimpleName(), message.getOfferId(), message.getUid(), model.getPeerNodeAddress()); model.getP2PService().sendEncryptedDirectMessage(model.getPeerNodeAddress(), model.getOffer().getPubKeyRing(), message, new SendDirectMessageListener() { @Override public void onArrived() { log.info("{} arrived at peer: offerId={}; uid={}", message.getClass().getSimpleName(), message.getOfferId(), message.getUid()); complete(); } @Override public void onFault(String errorMessage) { log.error("Sending {} failed: uid={}; peer={}; error={}", message.getClass().getSimpleName(), message.getUid(), model.getPeerNodeAddress(), errorMessage); model.getOffer().setState(Offer.State.MAKER_OFFLINE); } } ); } catch (Throwable t) { model.getOffer().setErrorMessage("An error occurred.\n" + "Error message:\n" + t.getMessage()); failed(t); } } } ================================================ FILE: core/src/main/java/haveno/core/offer/messages/OfferAvailabilityRequest.java ================================================ /* * 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 . */ package haveno.core.offer.messages; import haveno.common.app.Capabilities; import haveno.common.app.Version; import haveno.common.crypto.PubKeyRing; import haveno.core.proto.CoreProtoResolver; import haveno.core.trade.messages.InitTradeRequest; import haveno.network.p2p.SupportedCapabilitiesMessage; import lombok.EqualsAndHashCode; import lombok.Value; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.util.Optional; import java.util.UUID; // Here we add the SupportedCapabilitiesMessage interface as that message always predates a direct connection // to the trading peer @EqualsAndHashCode(callSuper = true) @Value @Slf4j public final class OfferAvailabilityRequest extends OfferMessage implements SupportedCapabilitiesMessage { private final PubKeyRing pubKeyRing; private final long takersTradePrice; @Nullable private final Capabilities supportedCapabilities; private final boolean isTakerApiUser; private final InitTradeRequest tradeRequest; public OfferAvailabilityRequest(String offerId, PubKeyRing pubKeyRing, long takersTradePrice, boolean isTakerApiUser, InitTradeRequest tradeRequest) { this(offerId, pubKeyRing, takersTradePrice, isTakerApiUser, Capabilities.app, Version.getP2PMessageVersion(), UUID.randomUUID().toString(), tradeRequest); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private OfferAvailabilityRequest(String offerId, PubKeyRing pubKeyRing, long takersTradePrice, boolean isTakerApiUser, @Nullable Capabilities supportedCapabilities, String messageVersion, @Nullable String uid, InitTradeRequest tradeRequest) { super(messageVersion, offerId, uid); this.pubKeyRing = pubKeyRing; this.takersTradePrice = takersTradePrice; this.isTakerApiUser = isTakerApiUser; this.supportedCapabilities = supportedCapabilities; this.tradeRequest = tradeRequest; } // @Override // public protobuf.Offer toProtoMessage() { // return protobuf.Offer.newBuilder().setOfferPayload(offerPayload.toProtoMessage().getOfferPayload()).build(); // } // // public static Offer fromProto(protobuf.Offer proto) { // return new Offer(OfferPayload.fromProto(proto.getOfferPayload())); // } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { final protobuf.OfferAvailabilityRequest.Builder builder = protobuf.OfferAvailabilityRequest.newBuilder() .setOfferId(offerId) .setPubKeyRing(pubKeyRing.toProtoMessage()) .setTakersTradePrice(takersTradePrice) .setIsTakerApiUser(isTakerApiUser) .setTradeRequest(tradeRequest.toProtoNetworkEnvelope().getInitTradeRequest()); Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities))); Optional.ofNullable(uid).ifPresent(e -> builder.setUid(uid)); return getNetworkEnvelopeBuilder() .setOfferAvailabilityRequest(builder) .build(); } public static OfferAvailabilityRequest fromProto(protobuf.OfferAvailabilityRequest proto, CoreProtoResolver coreProtoResolver, String messageVersion) { return new OfferAvailabilityRequest(proto.getOfferId(), PubKeyRing.fromProto(proto.getPubKeyRing()), proto.getTakersTradePrice(), proto.getIsTakerApiUser(), Capabilities.fromIntList(proto.getSupportedCapabilitiesList()), messageVersion, proto.getUid().isEmpty() ? null : proto.getUid(), InitTradeRequest.fromProto(proto.getTradeRequest(), coreProtoResolver, messageVersion)); } } ================================================ FILE: core/src/main/java/haveno/core/offer/messages/OfferAvailabilityResponse.java ================================================ /* * 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 . */ package haveno.core.offer.messages; import com.google.protobuf.ByteString; import haveno.common.app.Capabilities; import haveno.common.app.Version; import haveno.common.proto.ProtoUtil; import haveno.core.offer.AvailabilityResult; import haveno.network.p2p.SupportedCapabilitiesMessage; import lombok.EqualsAndHashCode; import lombok.Value; import javax.annotation.Nullable; import java.util.Optional; import java.util.UUID; // We add here the SupportedCapabilitiesMessage interface as that message always predates a direct connection // to the trading peer @EqualsAndHashCode(callSuper = true) @Value public final class OfferAvailabilityResponse extends OfferMessage implements SupportedCapabilitiesMessage { private final AvailabilityResult availabilityResult; @Nullable private final Capabilities supportedCapabilities; @Nullable private byte[] makerSignature; public OfferAvailabilityResponse(String offerId, AvailabilityResult availabilityResult, @Nullable byte[] makerSignature) { this(offerId, availabilityResult, Capabilities.app, Version.getP2PMessageVersion(), UUID.randomUUID().toString(), makerSignature); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private OfferAvailabilityResponse(String offerId, AvailabilityResult availabilityResult, @Nullable Capabilities supportedCapabilities, String messageVersion, @Nullable String uid, byte[] makerSignature) { super(messageVersion, offerId, uid); this.availabilityResult = availabilityResult; this.supportedCapabilities = supportedCapabilities; this.makerSignature = makerSignature; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { final protobuf.OfferAvailabilityResponse.Builder builder = protobuf.OfferAvailabilityResponse.newBuilder() .setOfferId(offerId) .setAvailabilityResult(protobuf.AvailabilityResult.valueOf(availabilityResult.name())); Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities))); Optional.ofNullable(uid).ifPresent(e -> builder.setUid(uid)); Optional.ofNullable(makerSignature).ifPresent(e -> builder.setMakerSignature(ByteString.copyFrom(e))); return getNetworkEnvelopeBuilder() .setOfferAvailabilityResponse(builder) .build(); } public static OfferAvailabilityResponse fromProto(protobuf.OfferAvailabilityResponse proto, String messageVersion) { return new OfferAvailabilityResponse(proto.getOfferId(), ProtoUtil.enumFromProto(AvailabilityResult.class, proto.getAvailabilityResult().name()), Capabilities.fromIntList(proto.getSupportedCapabilitiesList()), messageVersion, proto.getUid().isEmpty() ? null : proto.getUid(), ProtoUtil.byteArrayOrNullFromProto(proto.getMakerSignature())); } } ================================================ FILE: core/src/main/java/haveno/core/offer/messages/OfferMessage.java ================================================ /* * 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 . */ package haveno.core.offer.messages; import haveno.common.proto.network.NetworkEnvelope; import haveno.network.p2p.DirectMessage; import haveno.network.p2p.UidMessage; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; import javax.annotation.Nullable; @EqualsAndHashCode(callSuper = true) @Getter @ToString public abstract class OfferMessage extends NetworkEnvelope implements DirectMessage, UidMessage { public final String offerId; // Added in version 0.7.1. Can be null if we receive the msg from a peer with an older version @Nullable protected final String uid; protected OfferMessage(String messageVersion, String offerId, @Nullable String uid) { super(messageVersion); this.offerId = offerId; this.uid = uid; } } ================================================ FILE: core/src/main/java/haveno/core/offer/messages/SignOfferRequest.java ================================================ /* * 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 . */ package haveno.core.offer.messages; import haveno.common.crypto.PubKeyRing; import haveno.core.offer.OfferPayload; import haveno.network.p2p.DirectMessage; import haveno.network.p2p.NodeAddress; import lombok.EqualsAndHashCode; import lombok.Value; import java.util.ArrayList; import java.util.List; @EqualsAndHashCode(callSuper = true) @Value public final class SignOfferRequest extends OfferMessage implements DirectMessage { private final NodeAddress senderNodeAddress; private final PubKeyRing pubKeyRing; private final String senderAccountId; private final OfferPayload offerPayload; private final long currentDate; private final String reserveTxHash; private final String reserveTxHex; private final String reserveTxKey; private final List reserveTxKeyImages; private final String payoutAddress; public SignOfferRequest(String offerId, NodeAddress senderNodeAddress, PubKeyRing pubKeyRing, String senderAccountId, OfferPayload offerPayload, String uid, String messageVersion, long currentDate, String reserveTxHash, String reserveTxHex, String reserveTxKey, List reserveTxKeyImages, String payoutAddress) { super(messageVersion, offerId, uid); this.senderNodeAddress = senderNodeAddress; this.pubKeyRing = pubKeyRing; this.senderAccountId = senderAccountId; this.offerPayload = offerPayload; this.currentDate = currentDate; this.reserveTxHash = reserveTxHash; this.reserveTxHex = reserveTxHex; this.reserveTxKey = reserveTxKey; this.reserveTxKeyImages = reserveTxKeyImages; this.payoutAddress = payoutAddress; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { protobuf.SignOfferRequest.Builder builder = protobuf.SignOfferRequest.newBuilder() .setOfferId(offerId) .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setPubKeyRing(pubKeyRing.toProtoMessage()) .setSenderAccountId(senderAccountId) .setOfferPayload(offerPayload.toProtoMessage().getOfferPayload()) .setUid(uid) .setCurrentDate(currentDate) .setReserveTxHash(reserveTxHash) .setReserveTxHex(reserveTxHex) .setReserveTxKey(reserveTxKey) .addAllReserveTxKeyImages(reserveTxKeyImages) .setPayoutAddress(payoutAddress); return getNetworkEnvelopeBuilder().setSignOfferRequest(builder).build(); } public static SignOfferRequest fromProto(protobuf.SignOfferRequest proto, String messageVersion) { return new SignOfferRequest(proto.getOfferId(), NodeAddress.fromProto(proto.getSenderNodeAddress()), PubKeyRing.fromProto(proto.getPubKeyRing()), proto.getSenderAccountId(), OfferPayload.fromProto(proto.getOfferPayload()), proto.getUid(), messageVersion, proto.getCurrentDate(), proto.getReserveTxHash(), proto.getReserveTxHex(), proto.getReserveTxKey(), new ArrayList(proto.getReserveTxKeyImagesList()), proto.getPayoutAddress()); } @Override public String toString() { return "SignOfferRequest {" + "\n senderNodeAddress=" + senderNodeAddress + ",\n pubKeyRing=" + pubKeyRing + ",\n currentDate=" + currentDate + ",\n reserveTxHash='" + reserveTxHash + ",\n reserveTxHex='" + reserveTxHex + ",\n reserveTxKey='" + reserveTxKey + ",\n reserveTxKeyImages='" + reserveTxKeyImages + ",\n payoutAddress='" + payoutAddress + "\n} " + super.toString(); } } ================================================ FILE: core/src/main/java/haveno/core/offer/messages/SignOfferResponse.java ================================================ /* * 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 . */ package haveno.core.offer.messages; import haveno.core.offer.OfferPayload; import haveno.network.p2p.DirectMessage; import lombok.EqualsAndHashCode; import lombok.Value; @EqualsAndHashCode(callSuper = true) @Value public final class SignOfferResponse extends OfferMessage implements DirectMessage { private final OfferPayload signedOfferPayload; public SignOfferResponse(String offerId, String uid, String messageVersion, OfferPayload signedOfferPayload) { super(messageVersion, offerId, uid); this.signedOfferPayload = signedOfferPayload; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { protobuf.SignOfferResponse.Builder builder = protobuf.SignOfferResponse.newBuilder() .setOfferId(offerId) .setUid(uid) .setSignedOfferPayload(signedOfferPayload.toProtoMessage().getOfferPayload()); return getNetworkEnvelopeBuilder().setSignOfferResponse(builder).build(); } public static SignOfferResponse fromProto(protobuf.SignOfferResponse proto, String messageVersion) { return new SignOfferResponse(proto.getOfferId(), proto.getUid(), messageVersion, OfferPayload.fromProto(proto.getSignedOfferPayload())); } @Override public String toString() { return "SignOfferResponse {" + ",\n arbitratorSignature='" + signedOfferPayload.getArbitratorSignature() + "\n} " + super.toString(); } } ================================================ FILE: core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferModel.java ================================================ /* * 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 . */ package haveno.core.offer.placeoffer; import haveno.common.crypto.KeyRing; import haveno.common.taskrunner.Model; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.filter.FilterManager; import haveno.core.offer.OfferBookService; import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; import haveno.core.offer.messages.SignOfferResponse; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.support.dispute.mediation.mediator.MediatorManager; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.User; import haveno.core.xmr.wallet.BtcWalletService; import haveno.core.xmr.wallet.TradeWalletService; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.P2PService; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.Transaction; import java.math.BigInteger; @Slf4j @Getter public class PlaceOfferModel implements Model { // Immutable private final OpenOffer openOffer; private final BigInteger reservedFundsForOffer; private final boolean useSavingsWallet; private final P2PService p2PService; private final BtcWalletService walletService; private final XmrWalletService xmrWalletService; private final TradeWalletService tradeWalletService; private final OfferBookService offerBookService; private final ArbitratorManager arbitratorManager; private final MediatorManager mediatorManager; private final TradeStatisticsManager tradeStatisticsManager; private final User user; private final KeyRing keyRing; @Getter private final FilterManager filterManager; @Getter private final AccountAgeWitnessService accountAgeWitnessService; @Getter private final OpenOfferManager openOfferManager; // Mutable @Setter private boolean offerAddedToOfferBook; @Setter private Transaction transaction; @Setter private SignOfferResponse signOfferResponse; @Setter @Getter protected PlaceOfferProtocol protocol; public PlaceOfferModel(OpenOffer openOffer, BigInteger reservedFundsForOffer, boolean useSavingsWallet, P2PService p2PService, BtcWalletService walletService, XmrWalletService xmrWalletService, TradeWalletService tradeWalletService, OfferBookService offerBookService, ArbitratorManager arbitratorManager, MediatorManager mediatorManager, TradeStatisticsManager tradeStatisticsManager, User user, KeyRing keyRing, FilterManager filterManager, AccountAgeWitnessService accountAgeWitnessService, OpenOfferManager openOfferManager) { this.openOffer = openOffer; this.reservedFundsForOffer = reservedFundsForOffer; this.useSavingsWallet = useSavingsWallet; this.p2PService = p2PService; this.walletService = walletService; this.xmrWalletService = xmrWalletService; this.tradeWalletService = tradeWalletService; this.offerBookService = offerBookService; this.arbitratorManager = arbitratorManager; this.mediatorManager = mediatorManager; this.tradeStatisticsManager = tradeStatisticsManager; this.user = user; this.keyRing = keyRing; this.filterManager = filterManager; this.accountAgeWitnessService = accountAgeWitnessService; this.openOfferManager = openOfferManager; } @Override public void onComplete() { } } ================================================ FILE: core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java ================================================ /* * 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 . */ package haveno.core.offer.placeoffer; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.taskrunner.TaskRunner; import haveno.core.locale.Res; import haveno.core.offer.messages.SignOfferResponse; import haveno.core.offer.placeoffer.tasks.MaybeAddToOfferBook; import haveno.core.offer.placeoffer.tasks.MakerProcessSignOfferResponse; import haveno.core.offer.placeoffer.tasks.MakerReserveOfferFunds; import haveno.core.offer.placeoffer.tasks.MakerSendSignOfferRequest; import haveno.core.offer.placeoffer.tasks.ValidateOffer; import haveno.core.trade.handlers.TransactionResultHandler; import haveno.core.trade.protocol.TradeProtocol; import haveno.network.p2p.NodeAddress; import org.bitcoinj.core.Transaction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class PlaceOfferProtocol { private static final Logger log = LoggerFactory.getLogger(PlaceOfferProtocol.class); private final PlaceOfferModel model; private Timer timeoutTimer; private TransactionResultHandler resultHandler; private ErrorMessageHandler errorMessageHandler; private TaskRunner taskRunner; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public PlaceOfferProtocol(PlaceOfferModel model, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { this.model = model; this.model.setProtocol(this); this.resultHandler = resultHandler; this.errorMessageHandler = errorMessageHandler; } /////////////////////////////////////////////////////////////////////////////////////////// // Called from UI /////////////////////////////////////////////////////////////////////////////////////////// public void placeOffer() { startTimeoutTimer(); taskRunner = new TaskRunner<>(model, () -> { // reset timer if response not yet received if (model.getSignOfferResponse() == null) startTimeoutTimer(); }, (errorMessage) -> { handleError(errorMessage); } ); taskRunner.addTasks( ValidateOffer.class, MakerReserveOfferFunds.class, MakerSendSignOfferRequest.class ); taskRunner.run(); } public void cancelOffer() { handleError("Offer was canceled: " + model.getOpenOffer().getOffer().getId()); // cancel is treated as error for callers to handle } public void handleSignOfferResponse(SignOfferResponse response, NodeAddress sender) { log.debug("handleSignOfferResponse() " + model.getOpenOffer().getOffer().getId()); model.setSignOfferResponse(response); // ignore if unexpected signer if (!model.getOpenOffer().getOffer().getOfferPayload().getArbitratorSigner().equals(sender)) { log.warn("Ignoring sign offer response from different sender"); return; } // ignore if payloads have different timestamps if (model.getOpenOffer().getOffer().getOfferPayload().getDate() != response.getSignedOfferPayload().getDate()) { log.warn("Ignoring sign offer response from arbitrator for offer payload with different timestamp"); return; } // ignore if timer already stopped if (timeoutTimer == null) { log.warn("Ignoring sign offer response from arbitrator because timeout has expired for offer " + model.getOpenOffer().getOffer().getId()); return; } // reset timer startTimeoutTimer(); TaskRunner taskRunner = new TaskRunner<>(model, () -> { log.debug("sequence at handleSignOfferResponse completed"); stopTimeoutTimer(); handleResult(model.getTransaction()); // TODO: use XMR transaction instead }, (errorMessage) -> { if (model.isOfferAddedToOfferBook()) { model.getOfferBookService().removeOffer(model.getOpenOffer().getOffer().getOfferPayload(), () -> { model.setOfferAddedToOfferBook(false); log.debug("OfferPayload removed from offer book."); }, log::error); } handleError(errorMessage); } ); taskRunner.addTasks( MakerProcessSignOfferResponse.class, MaybeAddToOfferBook.class ); taskRunner.run(); } public synchronized void startTimeoutTimer() { if (resultHandler == null) return; stopTimeoutTimer(); timeoutTimer = UserThread.runAfter(() -> { handleError(Res.get("createOffer.timeoutAtPublishing")); }, TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS); } private synchronized void stopTimeoutTimer() { if (timeoutTimer != null) { timeoutTimer.stop(); timeoutTimer = null; } } private synchronized void handleResult(Transaction transaction) { resultHandler.handleResult(transaction); resetHandlers(); } private synchronized void handleError(String errorMessage) { if (timeoutTimer != null) { taskRunner.cancel(); if (!model.getOpenOffer().isCanceled()) { model.getOpenOffer().getOffer().setErrorMessage(errorMessage); } stopTimeoutTimer(); errorMessageHandler.handleErrorMessage(errorMessage); } resetHandlers(); } private synchronized void resetHandlers() { resultHandler = null; errorMessageHandler = null; } } ================================================ FILE: core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerProcessSignOfferResponse.java ================================================ /* * 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 . */ package haveno.core.offer.placeoffer.tasks; import haveno.common.taskrunner.Task; import haveno.common.taskrunner.TaskRunner; import haveno.core.offer.Offer; import haveno.core.offer.placeoffer.PlaceOfferModel; import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; import haveno.core.trade.HavenoUtils; import static com.google.common.base.Preconditions.checkNotNull; public class MakerProcessSignOfferResponse extends Task { public MakerProcessSignOfferResponse(TaskRunner taskHandler, PlaceOfferModel model) { super(taskHandler, model); } @Override protected void run() { Offer offer = model.getOpenOffer().getOffer(); try { runInterceptHook(); // get arbitrator Arbitrator arbitrator = checkNotNull(model.getUser().getAcceptedArbitratorByAddress(offer.getOfferPayload().getArbitratorSigner()), "user.getAcceptedArbitratorByAddress(arbitratorSigner) must not be null"); // validate arbitrator signature if (!HavenoUtils.isArbitratorSignatureValid(model.getSignOfferResponse().getSignedOfferPayload(), arbitrator)) { throw new RuntimeException("Arbitrator's offer payload has invalid signature, offerId=" + offer.getId()); } // set arbitrator signature for maker's offer offer.getOfferPayload().setArbitratorSignature(model.getSignOfferResponse().getSignedOfferPayload().getArbitratorSignature()); if (!HavenoUtils.isArbitratorSignatureValid(offer.getOfferPayload(), arbitrator)) { throw new RuntimeException("Maker's offer payload has invalid signature, offerId=" + offer.getId()); } offer.setState(Offer.State.AVAILABLE); complete(); } catch (Exception e) { offer.setErrorMessage("An error occurred.\n" + "Error message:\n" + e.getMessage()); failed(e); } } } ================================================ FILE: core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java ================================================ /* * 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 . */ package haveno.core.offer.placeoffer.tasks; import java.math.BigInteger; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import haveno.common.taskrunner.Task; import haveno.common.taskrunner.TaskRunner; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OpenOffer; import haveno.core.offer.placeoffer.PlaceOfferModel; import haveno.core.trade.HavenoUtils; import haveno.core.trade.protocol.TradeProtocol; import haveno.core.xmr.model.XmrAddressEntry; import lombok.extern.slf4j.Slf4j; import monero.common.MoneroRpcConnection; import monero.daemon.model.MoneroOutput; import monero.wallet.model.MoneroOutputWallet; import monero.wallet.model.MoneroTxWallet; @Slf4j public class MakerReserveOfferFunds extends Task { public MakerReserveOfferFunds(TaskRunner taskHandler, PlaceOfferModel model) { super(taskHandler, model); } @Override protected void run() { OpenOffer openOffer = model.getOpenOffer(); Offer offer = openOffer.getOffer(); try { runInterceptHook(); // skip if reserve tx already created if (openOffer.getReserveTxHash() != null && !openOffer.getReserveTxHash().isEmpty()) { log.info("Reserve tx already created for offerId={}", openOffer.getShortId()); // verify reserve tx key images if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() == null || openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().isEmpty()) { log.warn("Reserve tx key images missing for offerId={}", openOffer.getShortId()); setReserveTx(null); } else { complete(); return; } } // verify monero connection model.getXmrWalletService().getXmrConnectionService().verifyConnection(); // create reserve tx synchronized (HavenoUtils.xmrWalletService.getWalletLock()) { // reset protocol timeout verifyPending(); model.getProtocol().startTimeoutTimer(); // collect relevant info BigInteger makerFee = offer.getMaxMakerFee(); BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount(); BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit(); BigInteger penaltyFee = HavenoUtils.multiply(securityDeposit, offer.getPenaltyFeePct()); String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); XmrAddressEntry fundingEntry = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null); Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex(); // copy address entries to clones for (OpenOffer offerClone : model.getOpenOfferManager().getOpenOfferGroup(model.getOpenOffer().getGroupId())) { if (offerClone.getId().equals(offer.getId())) continue; // skip self model.getXmrWalletService().cloneAddressEntries(openOffer.getId(), offerClone.getId()); } // attempt creating reserve tx MoneroTxWallet reserveTx = null; try { synchronized (HavenoUtils.getWalletFunctionLock()) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { MoneroRpcConnection sourceConnection = model.getXmrWalletService().getXmrConnectionService().getConnection(); try { //if (true) throw new RuntimeException("Pretend error"); reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex); } catch (IllegalStateException e) { log.warn("Illegal state creating reserve tx, offerId={}, error={}", openOffer.getShortId(), i + 1, e.getMessage()); throw e; } catch (Exception e) { log.warn("Error creating reserve tx, offerId={}, attempt={}/{}, error={}", openOffer.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); model.getXmrWalletService().handleMainWalletError(e, sourceConnection, i + 1); verifyPending(); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; model.getProtocol().startTimeoutTimer(); // reset protocol timeout HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } // verify still open verifyPending(); if (reserveTx != null) break; } } } catch (Exception e) { // reset state with wallet lock setReserveTx(null); model.getXmrWalletService().resetAddressEntriesForOpenOffer(offer.getId()); if (reserveTx != null) model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx)); throw e; } // reset protocol timeout model.getProtocol().startTimeoutTimer(); // update offer reserve tx setReserveTx(reserveTx); // reset offer funding address entries if unused if (fundingEntry != null) { // get reserve tx inputs List inputs = model.getXmrWalletService().getOutputs(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages()); // collect subaddress indices of inputs Set inputSubaddressIndices = new HashSet<>(); for (MoneroOutputWallet input : inputs) { if (input.getAccountIndex() == 0) inputSubaddressIndices.add(input.getSubaddressIndex()); } // swap funding address entries to available if unused for (OpenOffer clone : model.getOpenOfferManager().getOpenOfferGroup(model.getOpenOffer().getGroupId())) { XmrAddressEntry cloneFundingEntry = model.getXmrWalletService().getAddressEntry(clone.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null); if (cloneFundingEntry != null && !inputSubaddressIndices.contains(cloneFundingEntry.getSubaddressIndex())) { if (inputSubaddressIndices.contains(cloneFundingEntry.getSubaddressIndex())) { model.getXmrWalletService().swapAddressEntryToAvailable(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); } } } } } complete(); } catch (Throwable t) { offer.setErrorMessage("An error occurred.\n" + "Error message:\n" + t.getMessage()); failed(t); } } private void setReserveTx(MoneroTxWallet reserveTx) { OpenOffer openOffer = model.getOpenOffer(); // collect reserved key images List reservedKeyImages = null; if (reserveTx != null) { reservedKeyImages = new ArrayList(); for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex()); } // collect offers to update List offersToUpdate = new ArrayList(); if (openOffer.getGroupId() == null) { offersToUpdate.add(openOffer); } else { offersToUpdate.addAll(model.getOpenOfferManager().getOpenOfferGroup(model.getOpenOffer().getGroupId())); } // update offer state for (OpenOffer offerToUpdate : offersToUpdate) { offerToUpdate.setReserveTxHash(reserveTx == null ? null : reserveTx.getHash()); offerToUpdate.setReserveTxHex(reserveTx == null ? null : reserveTx.getFullHex()); offerToUpdate.setReserveTxKey(reserveTx == null ? null : reserveTx.getKey()); offerToUpdate.getOffer().getOfferPayload().setReserveTxKeyImages(reservedKeyImages); } } private boolean isPending() { return model.getOpenOffer().isPending(); } private void verifyPending() { if (!isPending()) throw new RuntimeException("Offer " + model.getOpenOffer().getOffer().getId() + " is canceled"); } } ================================================ FILE: core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java ================================================ /* * 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 . */ package haveno.core.offer.placeoffer.tasks; import haveno.common.app.Version; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.common.taskrunner.Task; import haveno.common.taskrunner.TaskRunner; import haveno.core.offer.Offer; import haveno.core.offer.OpenOffer; import haveno.core.offer.availability.DisputeAgentSelection; import haveno.core.offer.messages.SignOfferRequest; import haveno.core.offer.placeoffer.PlaceOfferModel; import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; import haveno.core.trade.HavenoUtils; import haveno.core.xmr.model.XmrAddressEntry; import haveno.network.p2p.AckMessage; import haveno.network.p2p.DecryptedDirectMessageListener; import haveno.network.p2p.DecryptedMessageWithPubKey; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import haveno.network.p2p.SendDirectMessageListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Date; import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.UUID; public class MakerSendSignOfferRequest extends Task { private static final Logger log = LoggerFactory.getLogger(MakerSendSignOfferRequest.class); @SuppressWarnings({"unused"}) public MakerSendSignOfferRequest(TaskRunner taskHandler, PlaceOfferModel model) { super(taskHandler, model); } @Override protected void run() { OpenOffer openOffer = model.getOpenOffer(); Offer offer = openOffer.getOffer(); try { runInterceptHook(); // get payout address entry String returnAddress; Optional addressEntryOpt = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT); if (addressEntryOpt.isPresent()) returnAddress = addressEntryOpt.get().getAddressString(); else { log.warn("Payout address entry found for unsigned offer {} is missing, creating anew", offer.getId()); returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); } // build sign offer request SignOfferRequest request = new SignOfferRequest( offer.getId(), P2PService.getMyNodeAddress(), model.getKeyRing().getPubKeyRing(), model.getUser().getAccountId(), offer.getOfferPayload(), UUID.randomUUID().toString(), Version.getP2PMessageVersion(), new Date().getTime(), openOffer.getReserveTxHash(), openOffer.getReserveTxHex(), openOffer.getReserveTxKey(), offer.getOfferPayload().getReserveTxKeyImages(), returnAddress); // send request to random arbitrators until success sendSignOfferRequests(request, () -> { complete(); }, (errorMessage) -> { failed("Error signing offer " + request.getOfferId() + ": " + errorMessage); }); } catch (Throwable t) { offer.setErrorMessage("An error occurred.\n" + "Error message:\n" + t.getMessage()); failed(t); } } private void sendSignOfferRequests(SignOfferRequest request, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { Arbitrator randomArbitrator = DisputeAgentSelection.getRandomArbitrator(model.getArbitratorManager()); if (randomArbitrator == null) { errorMessageHandler.handleErrorMessage("Could not get random arbitrator to send " + request.getClass().getSimpleName() + " for offer " + request.getOfferId()); return; } sendSignOfferRequests(request, randomArbitrator.getNodeAddress(), new HashSet(), resultHandler, errorMessageHandler); } private void sendSignOfferRequests(SignOfferRequest request, NodeAddress arbitratorNodeAddress, Set excludedArbitrators, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { // complete on successful ack message, fail on first nack DecryptedDirectMessageListener ackListener = new DecryptedDirectMessageListener() { @Override public void onDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress sender) { if (!(decryptedMessageWithPubKey.getNetworkEnvelope() instanceof AckMessage)) return; if (!sender.equals(arbitratorNodeAddress)) return; AckMessage ackMessage = (AckMessage) decryptedMessageWithPubKey.getNetworkEnvelope(); if (!ackMessage.getSourceMsgClassName().equals(SignOfferRequest.class.getSimpleName())) return; if (!ackMessage.getSourceUid().equals(request.getUid())) return; if (ackMessage.isSuccess()) { model.getP2PService().removeDecryptedDirectMessageListener(this); model.getOpenOffer().getOffer().getOfferPayload().setArbitratorSigner(arbitratorNodeAddress); model.getOpenOffer().getOffer().setState(Offer.State.OFFER_FEE_RESERVED); resultHandler.handleResult(); } else { model.getOpenOffer().getOffer().setState(Offer.State.INVALID); errorMessageHandler.handleErrorMessage("Arbitrator nacked SignOfferRequest for offer " + request.getOfferId() + ": " + ackMessage.getErrorMessage()); } } }; model.getP2PService().addDecryptedDirectMessageListener(ackListener); // send sign offer request sendSignOfferRequest(request, arbitratorNodeAddress, new SendDirectMessageListener() { @Override public void onArrived() { log.info("{} arrived at arbitrator: offerId={}", request.getClass().getSimpleName(), model.getOpenOffer().getId()); model.getProtocol().startTimeoutTimer(); // reset timeout } // if unavailable, try alternative arbitrator @Override public void onFault(String errorMessage) { log.warn("Arbitrator unavailable: address={}, error={}", arbitratorNodeAddress, errorMessage); excludedArbitrators.add(arbitratorNodeAddress); // check if offer still pending if (!model.getOpenOffer().isPending()) { errorMessageHandler.handleErrorMessage("Offer is no longer pending, offerId=" + model.getOpenOffer().getId()); return; } // get alternative arbitrator Arbitrator altArbitrator = DisputeAgentSelection.getRandomArbitrator(model.getArbitratorManager(), excludedArbitrators); if (altArbitrator == null) { errorMessageHandler.handleErrorMessage("Offer " + request.getOfferId() + " could not be signed by any arbitrator"); return; } // send request to alternative arbitrator log.info("Using alternative arbitrator {}", altArbitrator.getNodeAddress()); model.getProtocol().startTimeoutTimer(); // reset timeout sendSignOfferRequests(request, altArbitrator.getNodeAddress(), excludedArbitrators, resultHandler, errorMessageHandler); } }); } private void sendSignOfferRequest(SignOfferRequest request, NodeAddress arbitratorNodeAddress, SendDirectMessageListener listener) { // get registered arbitrator Arbitrator arbitrator = model.getUser().getAcceptedArbitratorByAddress(arbitratorNodeAddress); if (arbitrator == null) throw new RuntimeException("Node address " + arbitratorNodeAddress + " is not a registered arbitrator"); // TODO: use error handler request.getOfferPayload().setArbitratorSigner(arbitratorNodeAddress); // send request to arbitrator log.info("Sending {} with offerId {} and uid {} to arbitrator {}", request.getClass().getSimpleName(), request.getOfferId(), request.getUid(), arbitratorNodeAddress); model.getP2PService().sendEncryptedDirectMessage( arbitratorNodeAddress, arbitrator.getPubKeyRing(), request, listener, HavenoUtils.ARBITRATOR_ACK_TIMEOUT_SECONDS ); } } ================================================ FILE: core/src/main/java/haveno/core/offer/placeoffer/tasks/MaybeAddToOfferBook.java ================================================ /* * 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 . */ package haveno.core.offer.placeoffer.tasks; import haveno.common.taskrunner.Task; import haveno.common.taskrunner.TaskRunner; import haveno.core.offer.Offer; import haveno.core.offer.OpenOffer; import haveno.core.offer.placeoffer.PlaceOfferModel; import static com.google.common.base.Preconditions.checkNotNull; public class MaybeAddToOfferBook extends Task { public MaybeAddToOfferBook(TaskRunner taskHandler, PlaceOfferModel model) { super(taskHandler, model); } @Override protected void run() { try { runInterceptHook(); checkNotNull(model.getSignOfferResponse().getSignedOfferPayload().getArbitratorSignature(), "Offer's arbitrator signature is null: " + model.getOpenOffer().getOffer().getId()); // deactivate if conflicting offer exists if (model.getOpenOfferManager().hasConflictingClone(model.getOpenOffer())) { model.getOpenOffer().setState(OpenOffer.State.DEACTIVATED); model.setOfferAddedToOfferBook(false); complete(); return; } // add to offer book and activate if pending or available if (model.getOpenOffer().isPending() || model.getOpenOffer().isAvailable()) { model.getOfferBookService().addOffer(new Offer(model.getSignOfferResponse().getSignedOfferPayload()), () -> { model.getOpenOffer().setState(OpenOffer.State.AVAILABLE); model.setOfferAddedToOfferBook(true); complete(); }, errorMessage -> { model.getOpenOffer().getOffer().setErrorMessage("Could not add offer to offerbook.\n" + "Please check your network connection and try again."); failed(errorMessage); }); } else { complete(); return; } } catch (Throwable t) { model.getOpenOffer().getOffer().setErrorMessage("An error occurred.\n" + "Error message:\n" + t.getMessage()); failed(t); } } } ================================================ FILE: core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java ================================================ /* * 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 . */ package haveno.core.offer.placeoffer.tasks; import haveno.common.taskrunner.Task; import haveno.common.taskrunner.TaskRunner; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.placeoffer.PlaceOfferModel; import haveno.core.payment.PaymentAccount; import haveno.core.trade.HavenoUtils; import haveno.core.trade.messages.TradeMessage; import haveno.core.user.User; import haveno.core.xmr.wallet.Restrictions; import org.bitcoinj.core.Coin; import java.math.BigInteger; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; public class ValidateOffer extends Task { public ValidateOffer(TaskRunner taskHandler, PlaceOfferModel model) { super(taskHandler, model); } @Override protected void run() { Offer offer = model.getOpenOffer().getOffer(); try { runInterceptHook(); validateOffer(offer, model.getAccountAgeWitnessService(), model.getUser()); complete(); } catch (Exception e) { offer.setErrorMessage("An error occurred.\n" + "Error message:\n" + e.getMessage()); failed(e); } } public static void validateOffer(Offer offer, AccountAgeWitnessService accountAgeWitnessService, User user) { // Coins checkBINotNullOrZero(offer.getAmount(), "Amount"); checkBINotNullOrZero(offer.getMinAmount(), "MinAmount"); //checkCoinNotNullOrZero(offer.getTxFee(), "txFee"); // TODO: remove from data model checkBINotNullOrZero(offer.getMaxTradeLimit(), "MaxTradeLimit"); if (offer.getMakerFeePct() < 0) throw new IllegalArgumentException("Maker fee must be >= 0% but was " + offer.getMakerFeePct()); if (offer.getTakerFeePct() < 0) throw new IllegalArgumentException("Taker fee must be >= 0% but was " + offer.getTakerFeePct()); offer.isPrivateOffer(); if (offer.isPrivateOffer()) { boolean isBuyerMaker = offer.getDirection() == OfferDirection.BUY; if (isBuyerMaker) { if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct()); if (offer.getSellerSecurityDepositPct() < 0) throw new IllegalArgumentException("Seller security deposit percent must be >= 0% but was " + offer.getSellerSecurityDepositPct()); } else { if (offer.getBuyerSecurityDepositPct() < 0) throw new IllegalArgumentException("Buyer security deposit percent must be >= 0% but was " + offer.getBuyerSecurityDepositPct()); if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct()); } } else { if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct()); if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct()); } // We remove those checks to be more flexible with future changes. /*checkArgument(offer.getMakerFee().value >= FeeService.getMinMakerFee(offer.isCurrencyForMakerFeeBtc()).value, "createOfferFee must not be less than FeeService.MIN_CREATE_OFFER_FEE_IN_BTC. " + "MakerFee=" + offer.getMakerFee().toFriendlyString());*/ /*checkArgument(offer.getBuyerSecurityDeposit().value >= ProposalConsensus.getMinBuyerSecurityDeposit().value, "buyerSecurityDeposit must not be less than ProposalConsensus.MIN_BUYER_SECURITY_DEPOSIT. " + "buyerSecurityDeposit=" + offer.getBuyerSecurityDeposit().toFriendlyString()); checkArgument(offer.getBuyerSecurityDeposit().value <= ProposalConsensus.getMaxBuyerSecurityDeposit().value, "buyerSecurityDeposit must not be larger than ProposalConsensus.MAX_BUYER_SECURITY_DEPOSIT. " + "buyerSecurityDeposit=" + offer.getBuyerSecurityDeposit().toFriendlyString()); checkArgument(offer.getSellerSecurityDeposit().value == ProposalConsensus.getSellerSecurityDeposit().value, "sellerSecurityDeposit must be equal to ProposalConsensus.SELLER_SECURITY_DEPOSIT. " + "sellerSecurityDeposit=" + offer.getSellerSecurityDeposit().toFriendlyString());*/ /*checkArgument(offer.getMinAmount().compareTo(ProposalConsensus.getMinTradeAmount()) >= 0, "MinAmount is less than " + ProposalConsensus.getMinTradeAmount().toFriendlyString());*/ PaymentAccount paymentAccount = user.getPaymentAccount(offer.getMakerPaymentAccountId()); checkArgument(paymentAccount != null, "Payment account is null. makerPaymentAccountId=" + offer.getMakerPaymentAccountId()); long maxAmount = accountAgeWitnessService.getMyTradeLimit(user.getPaymentAccount(offer.getMakerPaymentAccountId()), offer.getCounterCurrencyCode(), offer.getDirection(), offer.hasBuyerAsTakerWithoutDeposit()); checkArgument(offer.getAmount().longValueExact() <= maxAmount, "Amount must be below maximum amount of " + HavenoUtils.atomicUnitsToXmr(maxAmount) + " XMR"); long minAmount = Restrictions.getMinTradeAmount().longValue(); checkArgument(offer.getMinAmount().longValueExact() >= minAmount, "Amount must be above minimum amount of " + HavenoUtils.atomicUnitsToXmr(minAmount) + " XMR"); checkArgument(offer.getAmount().compareTo(offer.getMinAmount()) >= 0, "Minimum amount is larger than amount"); checkNotNull(offer.getPrice(), "Price is null"); if (!offer.isUseMarketBasedPrice()) checkArgument(offer.getPrice().isPositive(), "Price must be positive unless using market based price. price=" + offer.getPrice().toFriendlyString()); checkArgument(offer.getOfferPayload().getMarketPriceMarginPct() > -1 && offer.getOfferPayload().getMarketPriceMarginPct() < 1, "Market price margin must be greater than -100% and less than 100% but was " + (offer.getOfferPayload().getMarketPriceMarginPct() * 100) + "%"); checkArgument(offer.getDate().getTime() > 0, "Date must not be 0. date=" + offer.getDate().toString()); checkNotNull(offer.getCounterCurrencyCode(), "Currency is null"); checkNotNull(offer.getDirection(), "Direction is null"); checkNotNull(offer.getId(), "Id is null"); checkNotNull(offer.getPubKeyRing(), "pubKeyRing is null"); checkNotNull(offer.getMinAmount(), "MinAmount is null"); checkNotNull(offer.getPrice(), "Price is null"); checkNotNull(offer.getVersionNr(), "VersionNr is null"); checkArgument(offer.getMaxTradePeriod() > 0, "maxTradePeriod must be positive. maxTradePeriod=" + offer.getMaxTradePeriod()); // TODO check upper and lower bounds for fiat // TODO check rest of new parameters } private static void checkBINotNullOrZero(BigInteger value, String name) { checkNotNull(value, name + " is null"); checkArgument(value.compareTo(BigInteger.ZERO) > 0, name + " must be positive. " + name + "=" + value); } private static void checkCoinNotNullOrZero(Coin value, String name) { checkNotNull(value, name + " is null"); checkArgument(value.isPositive(), name + " must be positive. " + name + "=" + value.toFriendlyString()); } private static String nonEmptyStringOf(String value) { checkNotNull(value); checkArgument(value.length() > 0); return value; } private static long nonNegativeLongOf(long value) { checkArgument(value >= 0); return value; } private static Coin nonZeroCoinOf(Coin value) { checkNotNull(value); checkArgument(!value.isZero()); return value; } private static Coin positiveCoinOf(Coin value) { checkNotNull(value); checkArgument(value.isPositive()); return value; } private static void checkTradeId(String tradeId, TradeMessage tradeMessage) { checkArgument(tradeId.equals(tradeMessage.getOfferId())); } } ================================================ FILE: core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java ================================================ /* * 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 . */ package haveno.core.offer.takeoffer; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.Inject; import haveno.common.taskrunner.Model; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.monetary.Price; import haveno.core.monetary.Volume; import haveno.core.offer.Offer; import static haveno.core.offer.OfferDirection.SELL; import haveno.core.offer.OfferUtil; import haveno.core.payment.PaymentAccount; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.HavenoUtils; import haveno.core.util.VolumeUtil; import haveno.core.xmr.model.XmrAddressEntry; import static haveno.core.xmr.model.XmrAddressEntry.Context.OFFER_FUNDING; import haveno.core.xmr.wallet.XmrWalletService; import java.math.BigInteger; import java.util.Objects; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; @Slf4j public class TakeOfferModel implements Model { // Immutable private final AccountAgeWitnessService accountAgeWitnessService; private final XmrWalletService xmrWalletService; private final OfferUtil offerUtil; private final PriceFeedService priceFeedService; // Mutable @Getter private XmrAddressEntry addressEntry; @Getter private BigInteger amount; private Offer offer; private PaymentAccount paymentAccount; @Getter private BigInteger securityDeposit; private boolean useSavingsWallet; @Getter private BigInteger takerFee; @Getter private BigInteger totalToPay; @Getter private BigInteger missingCoin = BigInteger.ZERO; @Getter private BigInteger totalAvailableBalance; @Getter private BigInteger availableBalance; @Getter private boolean isXmrWalletFunded; @Getter private Volume volume; @Inject public TakeOfferModel(AccountAgeWitnessService accountAgeWitnessService, XmrWalletService xmrWalletService, OfferUtil offerUtil, PriceFeedService priceFeedService) { this.accountAgeWitnessService = accountAgeWitnessService; this.xmrWalletService = xmrWalletService; this.offerUtil = offerUtil; this.priceFeedService = priceFeedService; } public void initModel(Offer offer, PaymentAccount paymentAccount, BigInteger tradeAmount, boolean useSavingsWallet) { this.clearModel(); this.offer = offer; this.paymentAccount = paymentAccount; this.addressEntry = xmrWalletService.getOrCreateAddressEntry(offer.getId(), OFFER_FUNDING); validateModelInputs(); this.useSavingsWallet = useSavingsWallet; this.amount = tradeAmount.min(BigInteger.valueOf(getMaxTradeLimit())); this.securityDeposit = offer.getDirection() == SELL ? offer.getOfferPayload().getBuyerSecurityDepositForTradeAmount(amount) : offer.getOfferPayload().getSellerSecurityDepositForTradeAmount(amount); this.takerFee = HavenoUtils.multiply(amount, offer.getTakerFeePct()); calculateVolume(); calculateTotalToPay(); offer.resetState(); priceFeedService.setCurrencyCode(offer.getCounterCurrencyCode()); } @Override public void onComplete() { // empty } private void calculateTotalToPay() { // Taker pays 2 times the tx fee because the mining fee might be different when // maker created the offer and reserved his funds, so that would not work well // with dynamic fees. The mining fee for the takeOfferFee tx is deducted from // the createOfferFee and not visible to the trader. BigInteger feeAndSecDeposit = securityDeposit.add(takerFee); totalToPay = offer.isBuyOffer() ? feeAndSecDeposit.add(amount) : feeAndSecDeposit; updateBalance(); } private void calculateVolume() { Price tradePrice = offer.getPrice(); Volume volumeByAmount = Objects.requireNonNull(tradePrice).getVolumeByAmount(amount); volumeByAmount = VolumeUtil.getAdjustedVolume(volumeByAmount, offer.getPaymentMethod().getId()); volume = volumeByAmount; updateBalance(); } private void updateBalance() { totalAvailableBalance = xmrWalletService.getAvailableBalance(); if (totalToPay != null) availableBalance = totalToPay.min(totalAvailableBalance); missingCoin = offerUtil.getBalanceShortage(totalToPay, availableBalance); isXmrWalletFunded = offerUtil.isBalanceSufficient(totalToPay, availableBalance); } private long getMaxTradeLimit() { return accountAgeWitnessService.getMyTradeLimit(paymentAccount, offer.getCounterCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()); } @NotNull public BigInteger getFundsNeededForTrade() { // If taking a buy offer, taker needs to reserve the offer.amt too. return securityDeposit.add(offer.isBuyOffer() ? amount : BigInteger.ZERO); } private void validateModelInputs() { checkNotNull(offer, "offer must not be null"); checkNotNull(offer.getAmount(), "offer amount must not be null"); checkArgument(offer.getAmount().longValueExact() > 0, "offer amount must not be zero"); checkNotNull(offer.getPrice(), "offer price must not be null"); checkNotNull(paymentAccount, "payment account must not be null"); checkNotNull(addressEntry, "address entry must not be null"); } private void clearModel() { this.addressEntry = null; this.amount = null; this.availableBalance = null; this.isXmrWalletFunded = false; this.missingCoin = BigInteger.ZERO; this.offer = null; this.paymentAccount = null; this.securityDeposit = null; this.takerFee = null; this.totalAvailableBalance = null; this.totalToPay = null; this.useSavingsWallet = true; this.volume = null; } @Override public String toString() { return "TakeOfferModel{" + " offer.id=" + offer.getId() + "\n" + " offer.state=" + offer.getState() + "\n" + ", paymentAccount.id=" + paymentAccount.getId() + "\n" + ", paymentAccount.method.id=" + paymentAccount.getPaymentMethod().getId() + "\n" + ", useSavingsWallet=" + useSavingsWallet + "\n" + ", addressEntry=" + addressEntry + "\n" + ", amount=" + amount + "\n" + ", securityDeposit=" + securityDeposit + "\n" + ", takerFee=" + takerFee + "\n" + ", totalToPay=" + totalToPay + "\n" + ", missingCoin=" + missingCoin + "\n" + ", totalAvailableBalance=" + totalAvailableBalance + "\n" + ", availableBalance=" + availableBalance + "\n" + ", volume=" + volume + "\n" + ", fundsNeededForTrade=" + getFundsNeededForTrade() + "\n" + ", isXmrWalletFunded=" + isXmrWalletFunded + "\n" + '}'; } } ================================================ FILE: core/src/main/java/haveno/core/payment/AchTransferAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.BankUtil; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.AchTransferAccountPayload; import haveno.core.payment.payload.BankAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.Arrays; import java.util.List; import javax.annotation.Nullable; @EqualsAndHashCode(callSuper = true) public final class AchTransferAccount extends CountryBasedPaymentAccount implements SameCountryRestrictedBankAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD")); private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.COUNTRY, PaymentAccountFormField.FieldId.HOLDER_NAME, PaymentAccountFormField.FieldId.HOLDER_ADDRESS, PaymentAccountFormField.FieldId.BANK_NAME, PaymentAccountFormField.FieldId.BRANCH_ID, PaymentAccountFormField.FieldId.ACCOUNT_NR, PaymentAccountFormField.FieldId.ACCOUNT_TYPE, PaymentAccountFormField.FieldId.TRADE_CURRENCIES, PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.SALT ); public AchTransferAccount() { super(PaymentMethod.ACH_TRANSFER); } @Override protected PaymentAccountPayload createPayload() { return new AchTransferAccountPayload(paymentMethod.getId(), id); } @Override public String getBankId() { return ((BankAccountPayload) paymentAccountPayload).getBankId(); } @Override public String getCountryCode() { return getCountry() != null ? getCountry().code : ""; } public AchTransferAccountPayload getPayload() { return (AchTransferAccountPayload) paymentAccountPayload; } @Override public String getMessageForBuyer() { return "payment.achTransfer.info.buyer"; } @Override public String getMessageForSeller() { return "payment.achTransfer.info.seller"; } @Override public String getMessageForAccountCreation() { return "payment.achTransfer.info.account"; } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { return INPUT_FIELD_IDS; } @Override @Nullable public List getSupportedCountries() { return Arrays.asList(CountryUtil.findCountryByCode("US").get()); } @Override protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) { var field = super.getEmptyFormField(fieldId); if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setComponent(PaymentAccountFormField.Component.SELECT_ONE); if (field.getId() == PaymentAccountFormField.FieldId.BRANCH_ID) field.setLabel(BankUtil.getBranchIdLabel("US")); if (field.getId() == PaymentAccountFormField.FieldId.ACCOUNT_TYPE) field.setLabel(BankUtil.getAccountTypeLabel("US")); return field; } } ================================================ FILE: core/src/main/java/haveno/core/payment/AdvancedCashAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.AdvancedCashAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.EqualsAndHashCode; import org.jetbrains.annotations.NotNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class AdvancedCashAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of( new TraditionalCurrency("BRL"), new TraditionalCurrency("EUR"), new TraditionalCurrency("GBP"), new TraditionalCurrency("KZT"), new TraditionalCurrency("RUB"), new TraditionalCurrency("UAH"), new TraditionalCurrency("USD")); public AdvancedCashAccount() { super(PaymentMethod.ADVANCED_CASH); } @Override protected PaymentAccountPayload createPayload() { return new AdvancedCashAccountPayload(paymentMethod.getId(), id); } @NotNull @Override public List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @NotNull @Override public List getInputFieldIds() { throw new RuntimeException("Not implemented"); } public void setAccountNr(String accountNr) { ((AdvancedCashAccountPayload) paymentAccountPayload).setAccountNr(accountNr); } public String getAccountNr() { return ((AdvancedCashAccountPayload) paymentAccountPayload).getAccountNr(); } } ================================================ FILE: core/src/main/java/haveno/core/payment/AliPayAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.AliPayAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class AliPayAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of( new TraditionalCurrency("AED"), new TraditionalCurrency("AUD"), new TraditionalCurrency("CAD"), new TraditionalCurrency("CHF"), new TraditionalCurrency("CNY"), new TraditionalCurrency("CZK"), new TraditionalCurrency("DKK"), new TraditionalCurrency("EUR"), new TraditionalCurrency("GBP"), new TraditionalCurrency("HKD"), new TraditionalCurrency("IDR"), new TraditionalCurrency("ILS"), new TraditionalCurrency("JPY"), new TraditionalCurrency("KRW"), new TraditionalCurrency("LKR"), new TraditionalCurrency("MUR"), new TraditionalCurrency("MYR"), new TraditionalCurrency("NOK"), new TraditionalCurrency("NZD"), new TraditionalCurrency("PHP"), new TraditionalCurrency("RUB"), new TraditionalCurrency("SEK"), new TraditionalCurrency("SGD"), new TraditionalCurrency("THB"), new TraditionalCurrency("USD"), new TraditionalCurrency("ZAR") ); private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.ACCOUNT_NR, PaymentAccountFormField.FieldId.TRADE_CURRENCIES, PaymentAccountFormField.FieldId.SALT ); public AliPayAccount() { super(PaymentMethod.ALI_PAY); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override protected PaymentAccountPayload createPayload() { return new AliPayAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { return INPUT_FIELD_IDS; } public void setAccountNr(String accountNr) { ((AliPayAccountPayload) paymentAccountPayload).setAccountNr(accountNr); } public String getAccountNr() { return ((AliPayAccountPayload) paymentAccountPayload).getAccountNr(); } // TODO: AliPayValidator is not used (see AliPayForm) // @Override // public void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) { // switch (fieldId) { // case ACCOUNT_NR: // processValidationResult(new AliPayValidator().validate(value)); // break; // default: // super.validateFormField(form, fieldId, value); // } // } } ================================================ FILE: core/src/main/java/haveno/core/payment/AmazonGiftCardAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.AmazonGiftCardAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; import java.util.List; public final class AmazonGiftCardAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of( new TraditionalCurrency("AUD"), new TraditionalCurrency("CAD"), new TraditionalCurrency("EUR"), new TraditionalCurrency("GBP"), new TraditionalCurrency("INR"), new TraditionalCurrency("JPY"), new TraditionalCurrency("SAR"), new TraditionalCurrency("SEK"), new TraditionalCurrency("SGD"), new TraditionalCurrency("TRY"), new TraditionalCurrency("USD") ); private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.EMAIL_OR_MOBILE_NR, PaymentAccountFormField.FieldId.COUNTRY, PaymentAccountFormField.FieldId.TRADE_CURRENCIES, PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.SALT ); @Nullable private Country country; public AmazonGiftCardAccount() { super(PaymentMethod.AMAZON_GIFT_CARD); } @Override protected PaymentAccountPayload createPayload() { return new AmazonGiftCardAccountPayload(paymentMethod.getId(), id); } @Override public @NotNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NotNull List getInputFieldIds() { return INPUT_FIELD_IDS; } public String getEmailOrMobileNr() { return getAmazonGiftCardAccountPayload().getEmailOrMobileNr(); } public void setEmailOrMobileNr(String emailOrMobileNr) { getAmazonGiftCardAccountPayload().setEmailOrMobileNr(emailOrMobileNr); } public boolean countryNotSet() { return (getAmazonGiftCardAccountPayload()).countryNotSet(); } @Nullable public Country getCountry() { if (country == null) { final String countryCode = getAmazonGiftCardAccountPayload().getCountryCode(); CountryUtil.findCountryByCode(countryCode).ifPresent(c -> this.country = c); } return country; } public void setCountry(@NotNull Country country) { this.country = country; getAmazonGiftCardAccountPayload().setCountryCode(country.code); } private AmazonGiftCardAccountPayload getAmazonGiftCardAccountPayload() { return (AmazonGiftCardAccountPayload) paymentAccountPayload; } @Override protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) { var field = super.getEmptyFormField(fieldId); if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setComponent(PaymentAccountFormField.Component.SELECT_ONE); return field; } } ================================================ FILE: core/src/main/java/haveno/core/payment/AssetAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.payment.payload.AssetAccountPayload; import haveno.core.payment.payload.PaymentMethod; public abstract class AssetAccount extends PaymentAccount { protected AssetAccount(PaymentMethod paymentMethod) { super(paymentMethod); } public void setAddress(String address) { ((AssetAccountPayload) paymentAccountPayload).setAddress(address); } public String getAddress() { return ((AssetAccountPayload) paymentAccountPayload).getAddress(); } } ================================================ FILE: core/src/main/java/haveno/core/payment/AustraliaPayidAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.AustraliaPayidAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.NonNull; import java.util.List; public final class AustraliaPayidAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("AUD")); public AustraliaPayidAccount() { super(PaymentMethod.AUSTRALIA_PAYID); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.BANK_ACCOUNT_NAME, PaymentAccountFormField.FieldId.PAYID, PaymentAccountFormField.FieldId.EXTRA_INFO, PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.SALT ); @Override protected PaymentAccountPayload createPayload() { return new AustraliaPayidAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { return INPUT_FIELD_IDS; } public String getPayid() { return ((AustraliaPayidAccountPayload) paymentAccountPayload).getPayid(); } public void setPayid(String payid) { if (payid == null) payid = ""; ((AustraliaPayidAccountPayload) paymentAccountPayload).setPayid(payid); } public String getBankAccountName() { return ((AustraliaPayidAccountPayload) paymentAccountPayload).getBankAccountName(); } public void setBankAccountName(String bankAccountName) { if (bankAccountName == null) bankAccountName = ""; ((AustraliaPayidAccountPayload) paymentAccountPayload).setBankAccountName(bankAccountName); } public void setExtraInfo(String extraInfo) { ((AustraliaPayidAccountPayload) paymentAccountPayload).setExtraInfo(extraInfo); } public String getExtraInfo() { return ((AustraliaPayidAccountPayload) paymentAccountPayload).getExtraInfo(); } } ================================================ FILE: core/src/main/java/haveno/core/payment/BankAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import javax.annotation.Nullable; public interface BankAccount { @Nullable String getBankId(); } ================================================ FILE: core/src/main/java/haveno/core/payment/BankNameRestrictedBankAccount.java ================================================ /* * 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 . */ package haveno.core.payment; public interface BankNameRestrictedBankAccount extends BankAccount { } ================================================ FILE: core/src/main/java/haveno/core/payment/BizumAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.BizumAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class BizumAccount extends CountryBasedPaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("EUR")); public BizumAccount() { super(PaymentMethod.BIZUM); } @Override protected PaymentAccountPayload createPayload() { return new BizumAccountPayload(paymentMethod.getId(), id); } public void setMobileNr(String mobileNr) { ((BizumAccountPayload) paymentAccountPayload).setMobileNr(mobileNr); } public String getMobileNr() { return ((BizumAccountPayload) paymentAccountPayload).getMobileNr(); } @Override public String getMessageForBuyer() { return "payment.bizum.info.buyer"; } @Override public String getMessageForSeller() { return "payment.bizum.info.seller"; } @Override public String getMessageForAccountCreation() { return "payment.bizum.info.account"; } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } } ================================================ FILE: core/src/main/java/haveno/core/payment/CapitualAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.CapitualAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.EqualsAndHashCode; import org.jetbrains.annotations.NotNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class CapitualAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of( new TraditionalCurrency("BRL"), new TraditionalCurrency("EUR"), new TraditionalCurrency("GBP"), new TraditionalCurrency("USD") ); public CapitualAccount() { super(PaymentMethod.CAPITUAL); } @Override protected PaymentAccountPayload createPayload() { return new CapitualAccountPayload(paymentMethod.getId(), id); } @NotNull @Override public List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @NotNull @Override public List getInputFieldIds() { throw new RuntimeException("Not implemented"); } public void setAccountNr(String accountNr) { ((CapitualAccountPayload) paymentAccountPayload).setAccountNr(accountNr); } public String getAccountNr() { return ((CapitualAccountPayload) paymentAccountPayload).getAccountNr(); } } ================================================ FILE: core/src/main/java/haveno/core/payment/CashAppAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.CashAppAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class CashAppAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of( new TraditionalCurrency("USD"), new TraditionalCurrency("GBP")); private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.EMAIL_OR_MOBILE_NR_OR_CASHTAG, PaymentAccountFormField.FieldId.TRADE_CURRENCIES, PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.EXTRA_INFO, PaymentAccountFormField.FieldId.SALT); public CashAppAccount() { super(PaymentMethod.CASH_APP); } @Override protected PaymentAccountPayload createPayload() { return new CashAppAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { return INPUT_FIELD_IDS; } public void setEmailOrMobileNrOrCashtag(String emailOrMobileNrOrCashtag) { ((CashAppAccountPayload) paymentAccountPayload).setEmailOrMobileNrOrCashtag(emailOrMobileNrOrCashtag); } public String getEmailOrMobileNrOrCashtag() { return ((CashAppAccountPayload) paymentAccountPayload).getEmailOrMobileNrOrCashtag(); } public void setExtraInfo(String extraInfo) { ((CashAppAccountPayload) paymentAccountPayload).setExtraInfo(extraInfo); } public String getExtraInfo() { return ((CashAppAccountPayload) paymentAccountPayload).getExtraInfo(); } } ================================================ FILE: core/src/main/java/haveno/core/payment/CashAtAtmAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.CashAtAtmAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.NonNull; import java.util.List; public final class CashAtAtmAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = CurrencyUtil.getAllFiatCurrencies(); private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.TRADE_CURRENCIES, PaymentAccountFormField.FieldId.EXTRA_INFO, PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.SALT ); public CashAtAtmAccount() { super(PaymentMethod.CASH_AT_ATM); } @Override protected PaymentAccountPayload createPayload() { return new CashAtAtmAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { return INPUT_FIELD_IDS; } public void setExtraInfo(String extraInfo) { ((CashAtAtmAccountPayload) paymentAccountPayload).setExtraInfo(extraInfo); } public String getExtraInfo() { return ((CashAtAtmAccountPayload) paymentAccountPayload).getExtraInfo(); } @Override protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) { var field = super.getEmptyFormField(fieldId); if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setComponent(PaymentAccountFormField.Component.SELECT_ONE); if (field.getId() == PaymentAccountFormField.FieldId.EXTRA_INFO) field.setLabel(Res.get("payment.cashAtAtm.extraInfo.prompt")); return field; } } ================================================ FILE: core/src/main/java/haveno/core/payment/CashDepositAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.CashDepositAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.NonNull; import javax.annotation.Nullable; import java.util.List; public final class CashDepositAccount extends CountryBasedPaymentAccount implements SameCountryRestrictedBankAccount { public static final List SUPPORTED_CURRENCIES = CurrencyUtil.getAllFiatCurrencies(); public CashDepositAccount() { super(PaymentMethod.CASH_DEPOSIT); } @Override protected PaymentAccountPayload createPayload() { return new CashDepositAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } @Override public String getBankId() { return ((CashDepositAccountPayload) paymentAccountPayload).getBankId(); } @Override public String getCountryCode() { return getCountry() != null ? getCountry().code : ""; } @Nullable public String getRequirements() { return ((CashDepositAccountPayload) paymentAccountPayload).getRequirements(); } public void setRequirements(String requirements) { ((CashDepositAccountPayload) paymentAccountPayload).setRequirements(requirements); } } ================================================ FILE: core/src/main/java/haveno/core/payment/CelPayAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.CelPayAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.EqualsAndHashCode; import org.jetbrains.annotations.NotNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class CelPayAccount extends PaymentAccount { // https://github.com/bisq-network/growth/issues/231 public static final List SUPPORTED_CURRENCIES = List.of( new TraditionalCurrency("AUD"), new TraditionalCurrency("CAD"), new TraditionalCurrency("GBP"), new TraditionalCurrency("HKD"), new TraditionalCurrency("USD") ); public CelPayAccount() { super(PaymentMethod.CELPAY); } @Override protected PaymentAccountPayload createPayload() { return new CelPayAccountPayload(paymentMethod.getId(), id); } public void setEmail(String accountId) { ((CelPayAccountPayload) paymentAccountPayload).setEmail(accountId); } public String getEmail() { return ((CelPayAccountPayload) paymentAccountPayload).getEmail(); } @Override public String getMessageForBuyer() { return "payment.celpay.info.buyer"; } @Override public String getMessageForSeller() { return "payment.celpay.info.seller"; } @Override public String getMessageForAccountCreation() { return "payment.celpay.info.account"; } @Override public @NotNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NotNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } } ================================================ FILE: core/src/main/java/haveno/core/payment/ChargeBackRisk.java ================================================ /* * 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 . */ package haveno.core.payment; import com.google.inject.Singleton; import haveno.core.payment.payload.PaymentMethod; @Singleton public class ChargeBackRisk { public boolean hasChargebackRisk(String id, String currencyCode) { return PaymentMethod.hasChargebackRisk(id, currencyCode); } } ================================================ FILE: core/src/main/java/haveno/core/payment/ChaseQuickPayAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.ChaseQuickPayAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; // Removed due to QuickPay becoming Zelle // Cannot be deleted as it would break old trade history entries @Deprecated @EqualsAndHashCode(callSuper = true) public final class ChaseQuickPayAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD")); public ChaseQuickPayAccount() { super(PaymentMethod.CHASE_QUICK_PAY); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override protected PaymentAccountPayload createPayload() { return new ChaseQuickPayAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } public void setEmail(String email) { ((ChaseQuickPayAccountPayload) paymentAccountPayload).setEmail(email); } public String getEmail() { return ((ChaseQuickPayAccountPayload) paymentAccountPayload).getEmail(); } public void setHolderName(String holderName) { ((ChaseQuickPayAccountPayload) paymentAccountPayload).setHolderName(holderName); } public String getHolderName() { return ((ChaseQuickPayAccountPayload) paymentAccountPayload).getHolderName(); } } ================================================ FILE: core/src/main/java/haveno/core/payment/CountryBasedPaymentAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountForm; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.payment.payload.CountryBasedPaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.validation.AccountNrValidator; import lombok.EqualsAndHashCode; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; import java.util.List; @EqualsAndHashCode(callSuper = true) public abstract class CountryBasedPaymentAccount extends PaymentAccount { @Nullable protected Country country; @Nullable protected List acceptedCountries; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// protected CountryBasedPaymentAccount(PaymentMethod paymentMethod) { super(paymentMethod); } /////////////////////////////////////////////////////////////////////////////////////////// // Getter, Setter /////////////////////////////////////////////////////////////////////////////////////////// @Nullable public Country getCountry() { if (country == null) { final String countryCode = ((CountryBasedPaymentAccountPayload) paymentAccountPayload).getCountryCode(); CountryUtil.findCountryByCode(countryCode).ifPresent(c -> this.country = c); } return country; } public void setCountry(@NotNull Country country) { this.country = country; ((CountryBasedPaymentAccountPayload) paymentAccountPayload).setCountryCode(country.code); } @Nullable public List getAcceptedCountries() { if (acceptedCountries == null) { final List acceptedCountryCodes = ((CountryBasedPaymentAccountPayload) paymentAccountPayload).getAcceptedCountryCodes(); acceptedCountries = CountryUtil.getCountries(acceptedCountryCodes); } return acceptedCountries; } public void setAcceptedCountries(List acceptedCountries) { this.acceptedCountries = acceptedCountries; ((CountryBasedPaymentAccountPayload) paymentAccountPayload).setAcceptedCountryCodes(CountryUtil.getCountryCodes(acceptedCountries)); } @Nullable public List getSupportedCountries() { return null; // support all countries by default } @Override public void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) { switch (fieldId) { case ACCOUNT_NR: if (country == null && paymentAccountPayload == null) { throw new IllegalStateException("Country must be set before validating account number"); } processValidationResult(new AccountNrValidator(getCountry().code).validate(value)); break; default: super.validateFormField(form, fieldId, value); } } } ================================================ FILE: core/src/main/java/haveno/core/payment/CryptoCurrencyAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.CryptoCurrencyAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.ArrayList; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class CryptoCurrencyAccount extends AssetAccount { private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.TRADE_CURRENCIES, PaymentAccountFormField.FieldId.ADDRESS, PaymentAccountFormField.FieldId.SALT ); public static final List SUPPORTED_CURRENCIES = new ArrayList<>(CurrencyUtil.getAllSortedCryptoCurrencies()); public CryptoCurrencyAccount() { super(PaymentMethod.BLOCK_CHAINS); } @Override protected PaymentAccountPayload createPayload() { return new CryptoCurrencyAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { return INPUT_FIELD_IDS; } @Override protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) { var field = super.getEmptyFormField(fieldId); if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setComponent(PaymentAccountFormField.Component.SELECT_ONE); return field; } } ================================================ FILE: core/src/main/java/haveno/core/payment/DomesticWireTransferAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.BankAccountPayload; import haveno.core.payment.payload.DomesticWireTransferAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class DomesticWireTransferAccount extends CountryBasedPaymentAccount implements SameCountryRestrictedBankAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD")); public DomesticWireTransferAccount() { super(PaymentMethod.DOMESTIC_WIRE_TRANSFER); } @Override protected PaymentAccountPayload createPayload() { return new DomesticWireTransferAccountPayload(paymentMethod.getId(), id); } @Override public String getBankId() { return ((BankAccountPayload) paymentAccountPayload).getBankId(); } @Override public String getCountryCode() { return getCountry() != null ? getCountry().code : ""; } public DomesticWireTransferAccountPayload getPayload() { return (DomesticWireTransferAccountPayload) paymentAccountPayload; } @Override public String getMessageForBuyer() { return "payment.domesticWire.info.buyer"; } @Override public String getMessageForSeller() { return "payment.domesticWire.info.seller"; } @Override public String getMessageForAccountCreation() { return "payment.domesticWire.info.account"; } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } } ================================================ FILE: core/src/main/java/haveno/core/payment/F2FAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.F2FAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class F2FAccount extends CountryBasedPaymentAccount { public static final List SUPPORTED_CURRENCIES = CurrencyUtil.getAllTraditionalCurrencies(); private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.COUNTRY, PaymentAccountFormField.FieldId.TRADE_CURRENCIES, PaymentAccountFormField.FieldId.CONTACT, // TODO: contact is not used anywhere? PaymentAccountFormField.FieldId.CITY, PaymentAccountFormField.FieldId.EXTRA_INFO, PaymentAccountFormField.FieldId.SALT ); public F2FAccount() { super(PaymentMethod.F2F); } @Override protected PaymentAccountPayload createPayload() { return new F2FAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { return INPUT_FIELD_IDS; } public void setContact(String contact) { ((F2FAccountPayload) paymentAccountPayload).setContact(contact); } public String getContact() { return ((F2FAccountPayload) paymentAccountPayload).getContact(); } public void setCity(String city) { ((F2FAccountPayload) paymentAccountPayload).setCity(city); } public String getCity() { return ((F2FAccountPayload) paymentAccountPayload).getCity(); } public void setExtraInfo(String extraInfo) { ((F2FAccountPayload) paymentAccountPayload).setExtraInfo(extraInfo); } public String getExtraInfo() { return ((F2FAccountPayload) paymentAccountPayload).getExtraInfo(); } @Override protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) { var field = super.getEmptyFormField(fieldId); if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setComponent(PaymentAccountFormField.Component.SELECT_ONE); if (field.getId() == PaymentAccountFormField.FieldId.CITY) field.setLabel(Res.get("payment.f2f.city")); if (field.getId() == PaymentAccountFormField.FieldId.CONTACT) field.setLabel(Res.get("payment.f2f.contact")); if (field.getId() == PaymentAccountFormField.FieldId.EXTRA_INFO) field.setLabel(Res.get("payment.shared.extraInfo.prompt.paymentAccount")); return field; } } ================================================ FILE: core/src/main/java/haveno/core/payment/FasterPaymentsAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountForm; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.FasterPaymentsAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.validation.AccountNrValidator; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class FasterPaymentsAccount extends PaymentAccount { private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.HOLDER_NAME, PaymentAccountFormField.FieldId.SORT_CODE, PaymentAccountFormField.FieldId.ACCOUNT_NR, PaymentAccountFormField.FieldId.SALT ); public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("GBP")); public FasterPaymentsAccount() { super(PaymentMethod.FASTER_PAYMENTS); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override protected PaymentAccountPayload createPayload() { return new FasterPaymentsAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { return INPUT_FIELD_IDS; } public void setHolderName(String value) { ((FasterPaymentsAccountPayload) paymentAccountPayload).setHolderName(value); } public String getHolderName() { return ((FasterPaymentsAccountPayload) paymentAccountPayload).getHolderName(); } public void setSortCode(String value) { ((FasterPaymentsAccountPayload) paymentAccountPayload).setSortCode(value); } public String getSortCode() { return ((FasterPaymentsAccountPayload) paymentAccountPayload).getSortCode(); } public void setAccountNr(String value) { ((FasterPaymentsAccountPayload) paymentAccountPayload).setAccountNr(value); } public String getAccountNr() { return ((FasterPaymentsAccountPayload) paymentAccountPayload).getAccountNr(); } @Override public void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) { switch (fieldId) { case ACCOUNT_NR: processValidationResult(new AccountNrValidator("GB").validate(value)); break; default: super.validateFormField(form, fieldId, value); } } } ================================================ FILE: core/src/main/java/haveno/core/payment/HalCashAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.HalCashAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class HalCashAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("EUR")); public HalCashAccount() { super(PaymentMethod.HAL_CASH); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override protected PaymentAccountPayload createPayload() { return new HalCashAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } public void setMobileNr(String mobileNr) { ((HalCashAccountPayload) paymentAccountPayload).setMobileNr(mobileNr); } public String getMobileNr() { return ((HalCashAccountPayload) paymentAccountPayload).getMobileNr(); } } ================================================ FILE: core/src/main/java/haveno/core/payment/IfscBasedAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentMethod; import lombok.NonNull; import java.util.List; abstract public class IfscBasedAccount extends CountryBasedPaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("INR")); protected IfscBasedAccount(PaymentMethod paymentMethod) { super(paymentMethod); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } } ================================================ FILE: core/src/main/java/haveno/core/payment/ImpsAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.ImpsAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class ImpsAccount extends CountryBasedPaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("INR")); public ImpsAccount() { super(PaymentMethod.IMPS); } @Override protected PaymentAccountPayload createPayload() { return new ImpsAccountPayload(paymentMethod.getId(), id); } @Override public String getMessageForBuyer() { return "payment.imps.info.buyer"; } @Override public String getMessageForSeller() { return "payment.imps.info.seller"; } @Override public String getMessageForAccountCreation() { return "payment.imps.info.account"; } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } } ================================================ FILE: core/src/main/java/haveno/core/payment/InstantCryptoCurrencyAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.InstantCryptoCurrencyPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.ArrayList; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class InstantCryptoCurrencyAccount extends AssetAccount { public static final List SUPPORTED_CURRENCIES = new ArrayList<>(CurrencyUtil.getAllSortedCryptoCurrencies()); public InstantCryptoCurrencyAccount() { super(PaymentMethod.BLOCK_CHAINS_INSTANT); } @Override protected PaymentAccountPayload createPayload() { return new InstantCryptoCurrencyPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } } ================================================ FILE: core/src/main/java/haveno/core/payment/InteracETransferAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountForm; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.InteracETransferAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.validation.InteracETransferValidator; import haveno.core.trade.HavenoUtils; import lombok.EqualsAndHashCode; import org.jetbrains.annotations.NotNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class InteracETransferAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("CAD")); private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.HOLDER_NAME, PaymentAccountFormField.FieldId.EMAIL_OR_MOBILE_NR, PaymentAccountFormField.FieldId.QUESTION, PaymentAccountFormField.FieldId.ANSWER, PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.SALT ); public InteracETransferAccount() { super(PaymentMethod.INTERAC_E_TRANSFER); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override protected PaymentAccountPayload createPayload() { return new InteracETransferAccountPayload(paymentMethod.getId(), id); } @Override public @NotNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NotNull List getInputFieldIds() { return INPUT_FIELD_IDS; } public void setEmail(String email) { ((InteracETransferAccountPayload) paymentAccountPayload).setEmailOrMobileNr(email); } public String getEmail() { return ((InteracETransferAccountPayload) paymentAccountPayload).getEmailOrMobileNr(); } public void setAnswer(String answer) { ((InteracETransferAccountPayload) paymentAccountPayload).setAnswer(answer); } public String getAnswer() { return ((InteracETransferAccountPayload) paymentAccountPayload).getAnswer(); } public void setQuestion(String question) { ((InteracETransferAccountPayload) paymentAccountPayload).setQuestion(question); } public String getQuestion() { return ((InteracETransferAccountPayload) paymentAccountPayload).getQuestion(); } public void setHolderName(String holderName) { ((InteracETransferAccountPayload) paymentAccountPayload).setHolderName(holderName); } public String getHolderName() { return ((InteracETransferAccountPayload) paymentAccountPayload).getHolderName(); } public void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) { InteracETransferValidator interacETransferValidator = HavenoUtils.corePaymentAccountService.interacETransferValidator; switch (fieldId) { case QUESTION: processValidationResult(interacETransferValidator.questionValidator.validate(value)); break; case ANSWER: processValidationResult(interacETransferValidator.answerValidator.validate(value)); break; default: super.validateFormField(form, fieldId, value); } } } ================================================ FILE: core/src/main/java/haveno/core/payment/JapanBankAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.JapanBankAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.NonNull; import java.util.List; public final class JapanBankAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("JPY")); public JapanBankAccount() { super(PaymentMethod.JAPAN_BANK); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override protected PaymentAccountPayload createPayload() { return new JapanBankAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } // bank code public String getBankCode() { return ((JapanBankAccountPayload) paymentAccountPayload).getBankCode(); } public void setBankCode(String bankCode) { if (bankCode == null) bankCode = ""; ((JapanBankAccountPayload) paymentAccountPayload).setBankCode(bankCode); } // bank name public String getBankName() { return ((JapanBankAccountPayload) paymentAccountPayload).getBankName(); } public void setBankName(String bankName) { if (bankName == null) bankName = ""; ((JapanBankAccountPayload) paymentAccountPayload).setBankName(bankName); } // branch code public String getBankBranchCode() { return ((JapanBankAccountPayload) paymentAccountPayload).getBankBranchCode(); } public void setBankBranchCode(String bankBranchCode) { if (bankBranchCode == null) bankBranchCode = ""; ((JapanBankAccountPayload) paymentAccountPayload).setBankBranchCode(bankBranchCode); } // branch name public String getBankBranchName() { return ((JapanBankAccountPayload) paymentAccountPayload).getBankBranchName(); } public void setBankBranchName(String bankBranchName) { if (bankBranchName == null) bankBranchName = ""; ((JapanBankAccountPayload) paymentAccountPayload).setBankBranchName(bankBranchName); } // account type public String getBankAccountType() { return ((JapanBankAccountPayload) paymentAccountPayload).getBankAccountType(); } public void setBankAccountType(String bankAccountType) { if (bankAccountType == null) bankAccountType = ""; ((JapanBankAccountPayload) paymentAccountPayload).setBankAccountType(bankAccountType); } // account number public String getBankAccountNumber() { return ((JapanBankAccountPayload) paymentAccountPayload).getBankAccountNumber(); } public void setBankAccountNumber(String bankAccountNumber) { if (bankAccountNumber == null) bankAccountNumber = ""; ((JapanBankAccountPayload) paymentAccountPayload).setBankAccountNumber(bankAccountNumber); } // account name public String getBankAccountName() { return ((JapanBankAccountPayload) paymentAccountPayload).getBankAccountName(); } public void setBankAccountName(String bankAccountName) { if (bankAccountName == null) bankAccountName = ""; ((JapanBankAccountPayload) paymentAccountPayload).setBankAccountName(bankAccountName); } } ================================================ FILE: core/src/main/java/haveno/core/payment/JapanBankData.java ================================================ /* * 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 . */ package haveno.core.payment; import com.google.common.collect.ImmutableMap; import haveno.core.trade.HavenoUtils; import java.util.ArrayList; import java.util.List; import java.util.Map; /* Japan's National Banking Association assigns 4 digit codes to all Financial Institutions, so we use that as the primary "Bank ID", add the English names for the top ~30 major international banks, and remove local farmers agricultural cooperative associations to keep the list to a reasonable size. Please update annually. Source: Zengin Net list of Financial Institutions Last Updated: July 16, 2019 URL: https://www.zengin-net.jp/company/member/ PDF: https://www.zengin-net.jp/company/pdf/member1.pdf PDF: https://www.zengin-net.jp/company/pdf/member2.pdf Source: Bank of Japan list of Financial Institutions Last Updated: July 16, 2019 URL: https://www5.boj.or.jp/bojnet/codenew/mokujinew.htm File: code1_20190716.xlsx Excel sheet: 金融機関等コード一覧 */ public class JapanBankData { /* Returns the main list of ~500 banks in Japan with bank codes, but since 90%+ of people will be using one of ~30 major banks, we hard-code those at the top for easier pull-down selection, and add their English names in parenthesis for foreigners. */ public static List prettyPrintBankList() // {{{ { List prettyList = new ArrayList<>(); // add mega banks at the top for (Map.Entry bank : megaBanksEnglish.entrySet()) { String bankId = bank.getKey(); String bankNameEn = bank.getValue(); String bankNameJa = majorBanksJapanese.get(bankId); if (bankNameJa == null) bankNameJa = minorBanksJapanese.get(bankId); prettyList.add(prettyPrintMajorBank(bankId, bankNameJa, bankNameEn)); } // append the major banks next for (Map.Entry bank : majorBanksJapanese.entrySet()) { String bankId = bank.getKey(); String bankNameJa = bank.getValue(); // avoid duplicates if (megaBanksEnglish.get(bankId) != null) continue; prettyList.add(prettyPrintBank(bankId, bankNameJa)); } // append the minor local banks last for (Map.Entry bank : minorBanksJapanese.entrySet()) { String bankId = bank.getKey(); String bankNameJa = bank.getValue(); prettyList.add(prettyPrintBank(bankId, bankNameJa)); } return prettyList; } // }}} // Pretty print major banks like this: (0001) みずほ (Mizuho Bank) private static String prettyPrintMajorBank(String bankId, String bankNameJa, String bankNameEn) // {{{ { return ID_OPEN + bankId + ID_CLOSE + SPACE + JA_OPEN + bankNameJa + JA_CLOSE + SPACE + EN_OPEN + bankNameEn + EN_CLOSE; } // }}} // Pretty print other banks like this: (9524) みずほ証券 private static String prettyPrintBank(String bankId, String bankName) // {{{ { return ID_OPEN + bankId + ID_CLOSE + SPACE + JA_OPEN + bankName + JA_CLOSE; } // }}} // top 30 mega banks with english private static final Map megaBanksEnglish = ImmutableMap.builder() // {{{ japan post office .put("9900", "Japan Post Bank Yucho") // }}} // {{{ japan mega-banks .put("0001", "Mizuho Bank") .put("0005", "Mitsubishi UFJ Bank (MUFG)") .put("0009", "Sumitomo Mitsui Banking Corporation (SMBC)") .put("0010", "Resona Bank") // }}} // {{{ major online banks .put("0033", "Japan Net Bank") .put("0034", "Seven Bank (7-11)") .put("0035", "Sony Bank") .put("0036", "Rakuten Bank") .put("0038", "SBI Sumishin Net Bank") .put("0039", "Jibun Bank") .put("0040", "Aeon Bank") .put("0042", "Lawson Bank") // }}} // {{{ major trust banks, etc. .put("0150", "Suruga Bank") .put("0288", "Mitsubishi UFJ Trust Bank") .put("0289", "Mizuho Trust Bank") .put("0294", "Sumitomo Trust Bank") .put("0300", "SMBC Trust Bank (PRESTIA)") .put("0304", "Nomura Trust Bank") .put("0307", "Orix Trust Bank") .put("0310", "GMO Aozora Net Bank") .put("0321", "Japan Securities Trust Bank") .put("0397", "Shinsei Bank") .put("0398", "Aozora Bank") .put("0402", "JP Morgan Chase Bank") .put("0442", "BNY Mellon") .put("0458", "DBS Bank") .put("0472", "SBJ Shinhan Bank Japan") // }}} .build(); // major ~200 banks private static final Map majorBanksJapanese = ImmutableMap.builder() // {{{ ゆうちょ銀行 (9900) .put("9900", "ゆうちょ銀行") // }}} // {{{ 都市銀行 (0001 ~ 0029) .put("0001", "みずほ銀行") .put("0005", "三菱UFJ銀行") .put("0009", "三井住友銀行") .put("0010", "りそな銀行") .put("0017", "埼玉りそな銀行") // }}} // {{{ ネット専業銀行等 (0030 ~ 0049) .put("0033", "ジャパンネット銀行") .put("0034", "セブン銀行") .put("0035", "ソニー銀行") .put("0036", "楽天銀行") .put("0038", "住信SBIネット銀行") .put("0039", "じぶん銀行") .put("0040", "イオン銀行") .put("0041", "大和ネクスト銀行") .put("0042", "ローソン銀行") // }}} // {{{ 協会 (0050 ~ 0099) .put("0051", "全銀協") .put("0052", "横浜銀行協会") .put("0053", "釧路銀行協会") .put("0054", "札幌銀行協会") .put("0056", "函館銀行協会") .put("0057", "青森銀行協会") .put("0058", "秋田銀行協会") .put("0059", "宮城銀行協会") .put("0060", "福島銀行協会") .put("0061", "群馬銀行協会") .put("0062", "新潟銀行協会") .put("0063", "石川銀行協会") .put("0064", "山梨銀行協会") .put("0065", "長野銀行協会") .put("0066", "静岡銀行協会") .put("0067", "名古屋銀行協会") .put("0068", "京都銀行協会") .put("0069", "大阪銀行協会") .put("0070", "神戸銀行協会") .put("0071", "岡山銀行協会") .put("0072", "広島銀行協会") .put("0073", "島根銀行協会") .put("0074", "山口銀行協会") .put("0075", "香川銀行協会") .put("0076", "愛媛銀行協会") .put("0077", "高知銀行協会") .put("0078", "北九州銀行協会") .put("0079", "福岡銀行協会") .put("0080", "大分銀行協会") .put("0081", "長崎銀行協会") .put("0082", "熊本銀行協会") .put("0083", "鹿児島銀行協会") .put("0084", "沖縄銀行協会") .put("0090", "全銀ネット") .put("0095", "CLSBANK") // }}} // {{{ 地方銀行 (0116 ~ 0190) .put("0116", "北海道銀行") .put("0117", "青森銀行") .put("0118", "みちのく銀行") .put("0119", "秋田銀行") .put("0120", "北都銀行") .put("0121", "荘内銀行") .put("0122", "山形銀行") .put("0123", "岩手銀行") .put("0124", "東北銀行") .put("0125", "七十七銀行") .put("0126", "東邦銀行") .put("0128", "群馬銀行") .put("0129", "足利銀行") .put("0130", "常陽銀行") .put("0131", "筑波銀行") .put("0133", "武蔵野銀行") .put("0134", "千葉銀行") .put("0135", "千葉興業銀行") .put("0137", "きらぼし銀行") .put("0138", "横浜銀行") .put("0140", "第四銀行") .put("0141", "北越銀行") .put("0142", "山梨中央銀行") .put("0143", "八十二銀行") .put("0144", "北陸銀行") .put("0145", "富山銀行") .put("0146", "北國銀行") .put("0147", "福井銀行") .put("0149", "静岡銀行") .put("0150", "スルガ銀行") .put("0151", "清水銀行") .put("0152", "大垣共立銀行") .put("0153", "十六銀行") .put("0154", "三重銀行") .put("0155", "百五銀行") .put("0157", "滋賀銀行") .put("0158", "京都銀行") .put("0159", "関西みらい銀行") .put("0161", "池田泉州銀行") .put("0162", "南都銀行") .put("0163", "紀陽銀行") .put("0164", "但馬銀行") .put("0166", "鳥取銀行") .put("0167", "山陰合同銀行") .put("0168", "中国銀行") .put("0169", "広島銀行") .put("0170", "山口銀行") .put("0172", "阿波銀行") .put("0173", "百十四銀行") .put("0174", "伊予銀行") .put("0175", "四国銀行") .put("0177", "福岡銀行") .put("0178", "筑邦銀行") .put("0179", "佐賀銀行") .put("0180", "十八銀行") .put("0181", "親和銀行") .put("0182", "肥後銀行") .put("0183", "大分銀行") .put("0184", "宮崎銀行") .put("0185", "鹿児島銀行") .put("0187", "琉球銀行") .put("0188", "沖縄銀行") .put("0190", "西日本シティ銀行") .put("0191", "北九州銀行") // }}} // {{{ 信託銀行 (0288 ~ 0326) .put("0288", "三菱UFJ信託銀行") .put("0289", "みずほ信託銀行") .put("0294", "三井住友信託銀行") .put("0295", "BNYM信託") .put("0297", "日本マスタートラスト信託銀行") .put("0299", "ステート信託") .put("0300", "SMBC信託銀行 プレスティア") .put("0304", "野村信託銀行") .put("0307", "オリックス銀行") .put("0310", "GMOあおぞらネット銀行") .put("0311", "農中信託") .put("0320", "新生信託") .put("0321", "日証金信託") .put("0324", "日本トラスティサービス信託銀行") .put("0325", "資産管理サービス信託銀行") // }}} // {{{ 旧長期信用銀行 (0397 ~ 0398) .put("0397", "新生銀行") .put("0398", "あおぞら銀行") // }}} // {{{ foreign banks (0400 ~ 0497) .put("0401", "シティバンク、エヌ・エイ 銀行") .put("0402", "JPモルガン・チェース銀行") .put("0403", "アメリカ銀行") .put("0411", "香港上海銀行") .put("0413", "スタンチヤート") .put("0414", "バークレイズ") .put("0421", "アグリコル") .put("0423", "ハナ") .put("0424", "印度") .put("0425", "兆豐國際商銀") .put("0426", "バンコツク") .put("0429", "バンクネガラ") .put("0430", "ドイツ銀行") .put("0432", "ブラジル") .put("0438", "ユーオバシーズ") .put("0439", "ユービーエス") .put("0442", "BNYメロン") .put("0443", "ビー・エヌ・ピー・パリバ銀行") .put("0444", "チヤイニーズ") .put("0445", "ソシエテ") .put("0456", "ユバフ") .put("0458", "DBS") .put("0459", "パキスタン") .put("0460", "クレデイスイス") .put("0461", "コメルツ銀行") .put("0463", "ウニクレデイト") .put("0468", "インドステイト") .put("0471", "カナダロイヤル") .put("0472", "SBJ銀行") .put("0477", "ウリイ") .put("0482", "アイエヌジー") .put("0484", "ナツトオース") .put("0485", "アンズバンク") .put("0487", "コモンウエルス") .put("0489", "バンクチヤイナ") .put("0495", "ステストリート") .put("0498", "中小企業") // }}} // {{{ 第二地方銀行 (0501 ~ 0597) .put("0501", "北洋銀行") .put("0508", "きらやか銀行") .put("0509", "北日本銀行") .put("0512", "仙台銀行") .put("0513", "福島銀行") .put("0514", "大東銀行") .put("0516", "東和銀行") .put("0517", "栃木銀行") .put("0522", "京葉銀行") .put("0525", "東日本銀行") .put("0526", "東京スター銀行") .put("0530", "神奈川銀行") .put("0532", "大光銀行") .put("0533", "長野銀行") .put("0534", "富山第一銀行") .put("0537", "福邦銀行") .put("0538", "静岡中央銀行") .put("0542", "愛知銀行") .put("0543", "名古屋銀行") .put("0544", "中京銀行") .put("0546", "第三銀行") .put("0555", "大正銀行") .put("0562", "みなと銀行") .put("0565", "島根銀行") .put("0566", "トマト銀行") .put("0569", "もみじ銀行") .put("0570", "西京銀行") .put("0572", "徳島銀行") .put("0573", "香川銀行") .put("0576", "愛媛銀行") .put("0578", "高知銀行") .put("0582", "福岡中央銀行") .put("0583", "佐賀共栄銀行") .put("0585", "長崎銀行") .put("0587", "熊本銀行") .put("0590", "豊和銀行") .put("0591", "宮崎太陽銀行") .put("0594", "南日本銀行") .put("0596", "沖縄海邦銀行") // }}} // {{{ more foreign banks (0600 ~ 0999) .put("0603", "韓国産業") .put("0607", "彰化商業") .put("0608", "ウエルズフアゴ") .put("0611", "第一商業") .put("0612", "台湾") .put("0615", "交通") .put("0616", "メトロポリタン") .put("0617", "フイリピン") .put("0619", "中国工商") .put("0621", "中國信託商業") .put("0623", "インテーザ") .put("0624", "國民") .put("0625", "中国建設") .put("0626", "イタウウニ") .put("0627", "BBVA") .put("0630", "中国農業") .put("0631", "台新") .put("0632", "玉山") .put("0633", "台湾企銀") .put("0808", "ドイツ証券") .put("0813", "ソシエテ証券") .put("0821", "ビーピー証券") .put("0822", "バークレイ証券") .put("0831", "アグリコル証券") .put("0832", "ジエイピー証券") .put("0842", "ゴルドマン証券") .put("0845", "ナツトウエ証券") .put("0900", "日本相互証券") .put("0905", "東京金融取引所") .put("0909", "日本クリア機構") .put("0910", "ほふりクリア") .put("0964", "しんきん証券") .put("0966", "HSBC証券") .put("0968", "セント東短証券") .put("0971", "UBS証券") .put("0972", "メリル日本証券") // }}} .build(); // minor ~280 lesser known banks private static final Map minorBanksJapanese = ImmutableMap.builder() // {{{ 信用金庫 (1001 ~ 1996) .put("1000", "信金中央金庫") .put("1001", "北海道信金") .put("1003", "室蘭信金") .put("1004", "空知信金") .put("1006", "苫小牧信金") .put("1008", "北門信金") .put("1009", "伊達信金") .put("1010", "北空知信金") .put("1011", "日高信金") .put("1013", "渡島信金") .put("1014", "道南うみ街信金") .put("1020", "旭川信金") .put("1021", "稚内信金") .put("1022", "留萌信金") .put("1024", "北星信金") .put("1026", "帯広信金") .put("1027", "釧路信金") .put("1028", "大地みらい信金") .put("1030", "北見信金") .put("1031", "網走信金") .put("1033", "遠軽信金") .put("1104", "東奥信金") .put("1105", "青い森信金") .put("1120", "秋田信金") .put("1123", "羽後信金") .put("1140", "山形信金") .put("1141", "米沢信金") .put("1142", "鶴岡信金") .put("1143", "新庄信金") .put("1150", "盛岡信金") .put("1152", "宮古信金") .put("1153", "一関信金") .put("1154", "北上信金") .put("1155", "花巻信金") .put("1156", "水沢信金") .put("1170", "杜の都信金") .put("1171", "宮城第一信金") .put("1172", "石巻信金") .put("1174", "仙南信金") .put("1181", "会津信金") .put("1182", "郡山信金") .put("1184", "白河信金") .put("1185", "須賀川信金") .put("1186", "ひまわり信金") .put("1188", "あぶくま信金") .put("1189", "二本松信金") .put("1190", "福島信金") .put("1203", "高崎信金") .put("1204", "桐生信金") .put("1206", "アイオー信金") .put("1208", "利根郡信金") .put("1209", "館林信金") .put("1210", "北群馬信金") .put("1211", "しののめ信金") .put("1221", "足利小山信金") .put("1222", "栃木信金") .put("1223", "鹿沼相互信金") .put("1224", "佐野信金") .put("1225", "大田原信金") .put("1227", "烏山信金") .put("1240", "水戸信金") .put("1242", "結城信金") .put("1250", "埼玉県信金") .put("1251", "川口信金") .put("1252", "青木信金") .put("1253", "飯能信金") .put("1260", "千葉信金") .put("1261", "銚子信金") .put("1262", "東京ベイ信金") .put("1264", "館山信金") .put("1267", "佐原信金") .put("1280", "横浜信金") .put("1281", "かながわ信金") .put("1282", "湘南信金") .put("1283", "川崎信金") .put("1286", "平塚信金") .put("1288", "さがみ信金") .put("1289", "中栄信金") .put("1290", "中南信金") .put("1303", "朝日信金") .put("1305", "興産信金") .put("1310", "さわやか信金") .put("1311", "東京シテイ信金") .put("1319", "芝信金") .put("1320", "東京東信金") .put("1321", "東栄信金") .put("1323", "亀有信金") .put("1326", "小松川信金") .put("1327", "足立成和信金") .put("1333", "東京三協信金") .put("1336", "西京信金") .put("1341", "西武信金") .put("1344", "城南信金") .put("1345", "東京)昭和信金") .put("1346", "目黒信金") .put("1348", "世田谷信金") .put("1349", "東京信金") .put("1351", "城北信金") .put("1352", "滝野川信金") .put("1356", "巣鴨信金") .put("1358", "青梅信金") .put("1360", "多摩信金") .put("1370", "新潟信金") .put("1371", "長岡信金") .put("1373", "三条信金") .put("1374", "新発田信金") .put("1375", "柏崎信金") .put("1376", "上越信金") .put("1377", "新井信金") .put("1379", "村上信金") .put("1380", "加茂信金") .put("1385", "甲府信金") .put("1386", "山梨信金") .put("1390", "長野信金") .put("1391", "松本信金") .put("1392", "上田信金") .put("1393", "諏訪信金") .put("1394", "飯田信金") .put("1396", "アルプス信金") .put("1401", "富山信金") .put("1402", "高岡信金") .put("1405", "にいかわ信金") .put("1406", "氷見伏木信金") .put("1412", "砺波信金") .put("1413", "石動信金") .put("1440", "金沢信金") .put("1442", "のと共栄信金") .put("1444", "北陸信金") .put("1445", "鶴来信金") .put("1448", "興能信金") .put("1470", "福井信金") .put("1471", "敦賀信金") .put("1473", "小浜信金") .put("1475", "越前信金") .put("1501", "しず焼津信金") .put("1502", "静清信金") .put("1503", "浜松磐田信金") .put("1505", "沼津信金") .put("1506", "三島信金") .put("1507", "富士宮信金") .put("1513", "島田掛川信金") .put("1515", "静岡)富士信金") .put("1517", "遠州信金") .put("1530", "岐阜信金") .put("1531", "大垣西濃信金") .put("1532", "高山信金") .put("1533", "東濃信金") .put("1534", "関信金") .put("1538", "八幡信金") .put("1550", "愛知信金") .put("1551", "豊橋信金") .put("1552", "岡崎信金") .put("1553", "いちい信金") .put("1554", "瀬戸信金") .put("1555", "半田信金") .put("1556", "知多信金") .put("1557", "豊川信金") .put("1559", "豊田信金") .put("1560", "碧海信金") .put("1561", "西尾信金") .put("1562", "蒲郡信金") .put("1563", "尾西信金") .put("1565", "中日信金") .put("1566", "東春信金") .put("1580", "津信金") .put("1581", "北伊勢上野信金") .put("1583", "桑名三重信金") .put("1585", "紀北信金") .put("1602", "滋賀中央信金") .put("1603", "長浜信金") .put("1604", "湖東信金") .put("1610", "京都信金") .put("1611", "京都中央信金") .put("1620", "京都北都信金") .put("1630", "大阪信金") .put("1633", "大阪厚生信金") .put("1635", "大阪シテイ信金") .put("1636", "大阪商工信金") .put("1643", "永和信金") .put("1645", "北おおさか信金") .put("1656", "枚方信金") .put("1666", "奈良信金") .put("1667", "大和信金") .put("1668", "奈良中央信金") .put("1671", "新宮信金") .put("1674", "きのくに信金") .put("1680", "神戸信金") .put("1685", "姫路信金") .put("1686", "播州信金") .put("1687", "兵庫信金") .put("1688", "尼崎信金") .put("1689", "日新信金") .put("1691", "淡路信金") .put("1692", "但馬信金") .put("1694", "西兵庫信金") .put("1695", "中兵庫信金") .put("1696", "但陽信金") .put("1701", "鳥取信金") .put("1702", "米子信金") .put("1703", "倉吉信金") .put("1710", "しまね信金") .put("1711", "日本海信金") .put("1712", "島根中央信金") .put("1732", "おかやま信金") .put("1734", "水島信金") .put("1735", "津山信金") .put("1738", "玉島信金") .put("1740", "備北信金") .put("1741", "吉備信金") .put("1742", "日生信金") .put("1743", "備前信金") .put("1750", "広島信金") .put("1752", "呉信金") .put("1756", "しまなみ信金") .put("1758", "広島みどり信金") .put("1780", "萩山口信金") .put("1781", "西中国信金") .put("1789", "東山口信金") .put("1801", "徳島信金") .put("1803", "阿南信金") .put("1830", "高松信金") .put("1833", "観音寺信金") .put("1860", "愛媛信金") .put("1862", "宇和島信金") .put("1864", "東予信金") .put("1866", "川之江信金") .put("1880", "幡多信金") .put("1881", "高知信金") .put("1901", "福岡信金") .put("1903", "福岡ひびき信金") .put("1908", "大牟田柳川信金") .put("1909", "筑後信金") .put("1910", "飯塚信金") .put("1917", "大川信金") .put("1920", "遠賀信金") .put("1930", "唐津信金") .put("1931", "佐賀信金") .put("1933", "九州ひぜん信金") .put("1942", "たちばな信金") .put("1951", "熊本信金") .put("1952", "熊本第一信金") .put("1954", "熊本中央信金") .put("1960", "大分信金") .put("1962", "大分みらい信金") .put("1980", "宮崎都城信金") .put("1985", "高鍋信金") .put("1990", "鹿児島信金") .put("1991", "鹿児島相互信金") .put("1993", "奄美大島信金") .put("1996", "コザ信金") // }}} // {{{ 信用組合 (2011 ~ 2895) .put("2004", "商工組合中央金庫") .put("2010", "全国信用協同組合連合会") .put("2213", "整理回収機構") // }}} // {{{ 労働金庫 (2951 ~ 2997) .put("2950", "労働金庫連合会") // }}} // {{{ 農林中央金庫 (3000) .put("3000", "農林中央金庫") // }}} // {{{ 信用農業協同組合連合会 (3001 ~ 3046) .put("3001", "北海道信用農業協同組合連合会") .put("3003", "岩手県信用農業協同組合連合会") .put("3008", "茨城県信用農業協同組合連合会") .put("3011", "埼玉県信用農業協同組合連合会") .put("3013", "東京都信用農業協同組合連合会") .put("3014", "神奈川県信用農業協同組合連合会") .put("3015", "山梨県信用農業協同組合連合会") .put("3016", "長野県信用農業協同組合連合会") .put("3017", "新潟県信用農業協同組合連合会") .put("3019", "石川県信用農業協同組合連合会") .put("3020", "岐阜県信用農業協同組合連合会") .put("3021", "静岡県信用農業協同組合連合会") .put("3022", "愛知県信用農業協同組合連合会") .put("3023", "三重県信用農業協同組合連合会") .put("3024", "福井県信用農業協同組合連合会") .put("3025", "滋賀県信用農業協同組合連合会") .put("3026", "京都府信用農業協同組合連合会") .put("3027", "大阪府信用農業協同組合連合会") .put("3028", "兵庫県信用農業協同組合連合会") .put("3030", "和歌山県信用農業協同組合連合会") .put("3031", "鳥取県信用農業協同組合連合会") .put("3034", "広島県信用農業協同組合連合会") .put("3035", "山口県信用農業協同組合連合会") .put("3036", "徳島県信用農業協同組合連合会") .put("3037", "香川県信用農業協同組合連合会") .put("3038", "愛媛県信用農業協同組合連合会") .put("3039", "高知県信用農業協同組合連合会") .put("3040", "福岡県信用農業協同組合連合会") .put("3041", "佐賀県信用農業協同組合連合会") .put("3044", "大分県信用農業協同組合連合会") .put("3045", "宮崎県信用農業協同組合連合会") .put("3046", "鹿児島県信用農業協同組合連合会") // }}} // {{{ "JA Bank" agricultural cooperative associations (3056 ~ 9375) // REMOVED: the farmers should use a real bank if they want to sell bitcoin // }}} // {{{ 信用漁業協同組合連合会 (9450 ~ 9496) .put("9450", "北海道信用漁業協同組合連合会") .put("9451", "青森県信用漁業協同組合連合会") .put("9452", "岩手県信用漁業協同組合連合会") .put("9453", "宮城県漁業協同組合") .put("9456", "福島県信用漁業協同組合連合会") .put("9457", "茨城県信用漁業協同組合連合会") .put("9461", "千葉県信用漁業協同組合連合会") .put("9462", "東京都信用漁業協同組合連合会") .put("9466", "新潟県信用漁業協同組合連合会") .put("9467", "富山県信用漁業協同組合連合会") .put("9468", "石川県信用漁業協同組合連合会") .put("9470", "静岡県信用漁業協同組合連合会") .put("9471", "愛知県信用漁業協同組合連合会") .put("9472", "三重県信用漁業協同組合連合会") .put("9473", "福井県信用漁業協同組合連合会") .put("9475", "京都府信用漁業協同組合連合会") .put("9477", "なぎさ信用漁業協同組合連合会") .put("9480", "鳥取県信用漁業協同組合連合会") .put("9481", "JFしまね漁業協同組合") .put("9483", "広島県信用漁業協同組合連合会") .put("9484", "山口県漁業協同組合") .put("9485", "徳島県信用漁業協同組合連合会") .put("9486", "香川県信用漁業協同組合連合会") .put("9487", "愛媛県信用漁業協同組合連合会") .put("9488", "高知県信用漁業協同組合連合会") .put("9489", "福岡県信用漁業協同組合連合会") .put("9490", "佐賀県信用漁業協同組合連合会") .put("9491", "長崎県信用漁業協同組合連合会") .put("9493", "大分県漁業協同組合") .put("9494", "宮崎県信用漁業協同組合連合会") .put("9495", "鹿児島県信用漁業協同組合連合会") .put("9496", "沖縄県信用漁業協同組合連合会") // }}} // {{{ securities firms .put("9500", "東京短資") .put("9501", "セントラル短資") .put("9507", "上田八木短資") .put("9510", "日本証券金融") .put("9520", "野村証券") .put("9521", "日興証券") .put("9523", "大和証券") .put("9524", "みずほ証券") .put("9528", "岡三証券") .put("9530", "岩井コスモ証券") .put("9532", "三菱UFJ証券") .put("9534", "丸三証券") .put("9535", "東洋証券") .put("9537", "水戸証券") .put("9539", "東海東京証券") .put("9542", "むさし証券") .put("9545", "いちよし証券") .put("9573", "極東証券") .put("9574", "立花証券") .put("9579", "光世証券") .put("9584", "ちばぎん証券") .put("9589", "シテイ証券") .put("9594", "CS証券") .put("9595", "スタンレー証券") .put("9930", "日本政策投資") .put("9932", "政策金融公庫") .put("9933", "国際協力") .put("9945", "預金保険機構") // }}} .build(); private final static String ID_OPEN = ""; private final static String ID_CLOSE = ""; private final static String JA_OPEN = ""; private final static String JA_CLOSE = ""; private final static String EN_OPEN = ""; private final static String EN_CLOSE = ""; public final static String SPACE = " "; // don't localize these strings into all languages, // all we want is either Japanese or English here. public static String getString(String id) { boolean ja = HavenoUtils.preferences.getUserLanguage().equals("ja"); switch (id) { case "bank": if (ja) return "銀行名 ・金融機関名"; return "Bank or Financial Institution"; case "bank.select": if (ja) return "金融機関 ・銀行検索 (名称入力検索)"; return "Search for Bank or Financial Institution"; case "bank.code": if (ja) return "銀行コード"; return "Zengin Bank Code"; case "bank.name": if (ja) return "金融機関名 ・銀行名"; return "Financial Institution / Bank Name"; case "branch": if (ja) return "支店名"; return "Bank Branch"; case "branch.code": if (ja) return "支店コード"; return "Zengin Branch Code"; case "branch.code.validation.error": if (ja) return "入力は3桁の支店コードでなければなりません"; return "Input must be a 3 digit branch code"; case "branch.name": if (ja) return "支店名"; return "Bank Branch Name"; case "account": if (ja) return "銀行口座"; return "Bank Account"; case "account.type": if (ja) return "口座科目"; return "Bank Account Type"; case "account.type.select": if (ja) return "口座科目"; return "Select Account Type"; // displayed while creating account case "account.type.futsu": if (ja) return "普通"; return "FUTSUU (ordinary) account"; case "account.type.touza": if (ja) return "当座"; return "TOUZA (checking) account"; case "account.type.chochiku": if (ja) return "貯金"; return "CHOCHIKU (special) account"; // used when saving account info case "account.type.futsu.ja": return "普通"; case "account.type.touza.ja": return "当座"; case "account.type.chochiku.ja": return "貯金"; case "account.number": if (ja) return "口座番号"; return "Bank Account Number"; case "account.number.validation.error": if (ja) return "入力は4〜8桁の口座番号でなければなりません"; return "Input must be 4 ~ 8 digit account number"; case "account.name": if (ja) return "口座名義"; return "Bank Account Name"; // for japanese-only input fields case "japanese.validation.error": if (ja) return "入力は漢字、ひらがな、またはカタカナでなければなりません"; return "Input must be Kanji, Hiragana, or Katakana"; case "japanese.validation.regex": // epic regex to only match Japanese input return "[" + // match any of these characters: // "A-z" + // full-width alphabet // "0-9" + // full-width numerals "一-龯" + // common Japanese kanji (0x4e00 ~ 0x9faf) "々" + // kanji iteration mark (0x3005) "〇" + // kanji number zero (0x3007) "ぁ-ゞ" + // hiragana (0x3041 ~ 0x309e) "ァ-・" + // full-width katakana (0x30a1 ~ 0x30fb) "ァ-ン゙゚" + // half-width katakana "ヽヾ゛゜ー" + // 0x30fd, 0x30fe, 0x309b, 0x309c, 0x30fc " " + // full-width space " " + // half-width space "]+"; // for any length } return "null"; } } ================================================ FILE: core/src/main/java/haveno/core/payment/MoneseAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.MoneseAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class MoneseAccount extends PaymentAccount { // https://github.com/bisq-network/growth/issues/227 public static final List SUPPORTED_CURRENCIES = List.of( new TraditionalCurrency("EUR"), new TraditionalCurrency("GBP"), new TraditionalCurrency("RON") ); public MoneseAccount() { super(PaymentMethod.MONESE); } @Override protected PaymentAccountPayload createPayload() { return new MoneseAccountPayload(paymentMethod.getId(), id); } public void setHolderName(String accountId) { ((MoneseAccountPayload) paymentAccountPayload).setHolderName(accountId); } public String getHolderName() { return ((MoneseAccountPayload) paymentAccountPayload).getHolderName(); } public void setMobileNr(String accountId) { ((MoneseAccountPayload) paymentAccountPayload).setMobileNr(accountId); } public String getMobileNr() { return ((MoneseAccountPayload) paymentAccountPayload).getMobileNr(); } @Override public String getMessageForBuyer() { return "payment.monese.info.buyer"; } @Override public String getMessageForSeller() { return "payment.monese.info.seller"; } @Override public String getMessageForAccountCreation() { return "payment.monese.info.account"; } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } } ================================================ FILE: core/src/main/java/haveno/core/payment/MoneyBeamAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.MoneyBeamAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; //TODO missing support for selected trade currency @EqualsAndHashCode(callSuper = true) public final class MoneyBeamAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("EUR")); public MoneyBeamAccount() { super(PaymentMethod.MONEY_BEAM); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override protected PaymentAccountPayload createPayload() { return new MoneyBeamAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } public void setAccountId(String accountId) { ((MoneyBeamAccountPayload) paymentAccountPayload).setAccountId(accountId); } public String getAccountId() { return ((MoneyBeamAccountPayload) paymentAccountPayload).getAccountId(); } public void setHolderName(String value) { ((MoneyBeamAccountPayload) paymentAccountPayload).setHolderName(value); } public String getHolderName() { return ((MoneyBeamAccountPayload) paymentAccountPayload).getHolderName(); } } ================================================ FILE: core/src/main/java/haveno/core/payment/MoneyGramAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.MoneyGramAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.EqualsAndHashCode; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class MoneyGramAccount extends PaymentAccount { @Nullable private Country country; private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.COUNTRY, PaymentAccountFormField.FieldId.STATE, PaymentAccountFormField.FieldId.HOLDER_NAME, PaymentAccountFormField.FieldId.EMAIL, PaymentAccountFormField.FieldId.TRADE_CURRENCIES, PaymentAccountFormField.FieldId.SALT ); public static final List SUPPORTED_CURRENCIES = List.of( new TraditionalCurrency("AED"), new TraditionalCurrency("ARS"), new TraditionalCurrency("AUD"), new TraditionalCurrency("BND"), new TraditionalCurrency("CAD"), new TraditionalCurrency("CHF"), new TraditionalCurrency("CZK"), new TraditionalCurrency("DKK"), new TraditionalCurrency("EUR"), new TraditionalCurrency("FJD"), new TraditionalCurrency("GBP"), new TraditionalCurrency("HKD"), new TraditionalCurrency("HUF"), new TraditionalCurrency("IDR"), new TraditionalCurrency("ILS"), new TraditionalCurrency("INR"), new TraditionalCurrency("JPY"), new TraditionalCurrency("KRW"), new TraditionalCurrency("KWD"), new TraditionalCurrency("LKR"), new TraditionalCurrency("MAD"), new TraditionalCurrency("MGA"), new TraditionalCurrency("MXN"), new TraditionalCurrency("MYR"), new TraditionalCurrency("NOK"), new TraditionalCurrency("NZD"), new TraditionalCurrency("OMR"), new TraditionalCurrency("PEN"), new TraditionalCurrency("PGK"), new TraditionalCurrency("PHP"), new TraditionalCurrency("PKR"), new TraditionalCurrency("PLN"), new TraditionalCurrency("SAR"), new TraditionalCurrency("SBD"), new TraditionalCurrency("SCR"), new TraditionalCurrency("SEK"), new TraditionalCurrency("SGD"), new TraditionalCurrency("THB"), new TraditionalCurrency("TOP"), new TraditionalCurrency("TRY"), new TraditionalCurrency("TWD"), new TraditionalCurrency("USD"), new TraditionalCurrency("VND"), new TraditionalCurrency("VUV"), new TraditionalCurrency("WST"), new TraditionalCurrency("XOF"), new TraditionalCurrency("XPF"), new TraditionalCurrency("ZAR") ); public MoneyGramAccount() { super(PaymentMethod.MONEY_GRAM); } @Override protected PaymentAccountPayload createPayload() { return new MoneyGramAccountPayload(paymentMethod.getId(), id); } @Override public @NotNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NotNull List getInputFieldIds() { return INPUT_FIELD_IDS; } @Nullable public Country getCountry() { if (country == null) { final String countryCode = ((MoneyGramAccountPayload) paymentAccountPayload).getCountryCode(); CountryUtil.findCountryByCode(countryCode).ifPresent(c -> this.country = c); } return country; } public void setCountry(@NotNull Country country) { this.country = country; ((MoneyGramAccountPayload) paymentAccountPayload).setCountryCode(country.code); } public String getEmail() { return ((MoneyGramAccountPayload) paymentAccountPayload).getEmail(); } public void setEmail(String email) { ((MoneyGramAccountPayload) paymentAccountPayload).setEmail(email); } public String getFullName() { return ((MoneyGramAccountPayload) paymentAccountPayload).getHolderName(); } public void setFullName(String email) { ((MoneyGramAccountPayload) paymentAccountPayload).setHolderName(email); } public String getState() { return ((MoneyGramAccountPayload) paymentAccountPayload).getState(); } public void setState(String state) { ((MoneyGramAccountPayload) paymentAccountPayload).setState(state); } @Override protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) { var field = super.getEmptyFormField(fieldId); if (field.getId() == PaymentAccountFormField.FieldId.HOLDER_NAME) field.setLabel("Full name (first, middle, last)"); return field; } } ================================================ FILE: core/src/main/java/haveno/core/payment/NationalBankAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.BankAccountPayload; import haveno.core.payment.payload.NationalBankAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class NationalBankAccount extends CountryBasedPaymentAccount implements SameCountryRestrictedBankAccount { public static final List SUPPORTED_CURRENCIES = CurrencyUtil.getAllFiatCurrencies(); public NationalBankAccount() { super(PaymentMethod.NATIONAL_BANK); } @Override protected PaymentAccountPayload createPayload() { return new NationalBankAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } @Override public String getBankId() { return ((BankAccountPayload) paymentAccountPayload).getBankId(); } @Override public String getCountryCode() { return getCountry() != null ? getCountry().code : ""; } } ================================================ FILE: core/src/main/java/haveno/core/payment/NeftAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.payment.payload.NeftAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.EqualsAndHashCode; @EqualsAndHashCode(callSuper = true) public final class NeftAccount extends IfscBasedAccount { public NeftAccount() { super(PaymentMethod.NEFT); } @Override protected PaymentAccountPayload createPayload() { return new NeftAccountPayload(paymentMethod.getId(), id); } public String getMessageForBuyer() { return "payment.neft.info.buyer"; } public String getMessageForSeller() { return "payment.neft.info.seller"; } public String getMessageForAccountCreation() { return "payment.neft.info.account"; } } ================================================ FILE: core/src/main/java/haveno/core/payment/NequiAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.NequiAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class NequiAccount extends CountryBasedPaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("COP")); public NequiAccount() { super(PaymentMethod.NEQUI); } @Override protected PaymentAccountPayload createPayload() { return new NequiAccountPayload(paymentMethod.getId(), id); } public void setMobileNr(String mobileNr) { ((NequiAccountPayload) paymentAccountPayload).setMobileNr(mobileNr); } public String getMobileNr() { return ((NequiAccountPayload) paymentAccountPayload).getMobileNr(); } @Override public String getMessageForBuyer() { return "payment.nequi.info.buyer"; } @Override public String getMessageForSeller() { return "payment.nequi.info.seller"; } @Override public String getMessageForAccountCreation() { return "payment.nequi.info.account"; } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } } ================================================ FILE: core/src/main/java/haveno/core/payment/OKPayAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.OKPayAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.EqualsAndHashCode; import org.jetbrains.annotations.NotNull; import java.util.List; // Cannot be deleted as it would break old trade history entries @Deprecated @EqualsAndHashCode(callSuper = true) public final class OKPayAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of( new TraditionalCurrency("AED"), new TraditionalCurrency("ARS"), new TraditionalCurrency("AUD"), new TraditionalCurrency("BRL"), new TraditionalCurrency("CAD"), new TraditionalCurrency("CHF"), new TraditionalCurrency("CNY"), new TraditionalCurrency("DKK"), new TraditionalCurrency("EUR"), new TraditionalCurrency("GBP"), new TraditionalCurrency("HKD"), new TraditionalCurrency("ILS"), new TraditionalCurrency("INR"), new TraditionalCurrency("JPY"), new TraditionalCurrency("KES"), new TraditionalCurrency("MXN"), new TraditionalCurrency("NOK"), new TraditionalCurrency("NZD"), new TraditionalCurrency("PHP"), new TraditionalCurrency("PLN"), new TraditionalCurrency("SEK"), new TraditionalCurrency("SGD"), new TraditionalCurrency("USD") ); public OKPayAccount() { super(PaymentMethod.OK_PAY); } @Override protected PaymentAccountPayload createPayload() { return new OKPayAccountPayload(paymentMethod.getId(), id); } @Override public @NotNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NotNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } public void setAccountNr(String accountNr) { ((OKPayAccountPayload) paymentAccountPayload).setAccountNr(accountNr); } public String getAccountNr() { return ((OKPayAccountPayload) paymentAccountPayload).getAccountNr(); } } ================================================ FILE: core/src/main/java/haveno/core/payment/PaxumAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaxumAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.EqualsAndHashCode; import org.jetbrains.annotations.NotNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class PaxumAccount extends PaymentAccount { private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.EMAIL, PaymentAccountFormField.FieldId.TRADE_CURRENCIES, PaymentAccountFormField.FieldId.SALT ); // https://github.com/bisq-network/growth/issues/235 public static final List SUPPORTED_CURRENCIES = List.of( new TraditionalCurrency("AUD"), new TraditionalCurrency("CAD"), new TraditionalCurrency("CHF"), new TraditionalCurrency("CZK"), new TraditionalCurrency("DKK"), new TraditionalCurrency("EUR"), new TraditionalCurrency("GBP"), new TraditionalCurrency("HUF"), new TraditionalCurrency("IDR"), new TraditionalCurrency("INR"), new TraditionalCurrency("NOK"), new TraditionalCurrency("NZD"), new TraditionalCurrency("PLN"), new TraditionalCurrency("RON"), new TraditionalCurrency("SEK"), new TraditionalCurrency("THB"), new TraditionalCurrency("USD"), new TraditionalCurrency("ZAR") ); public PaxumAccount() { super(PaymentMethod.PAXUM); } @Override protected PaymentAccountPayload createPayload() { return new PaxumAccountPayload(paymentMethod.getId(), id); } @Override public @NotNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NotNull List getInputFieldIds() { return INPUT_FIELD_IDS; } public void setEmail(String accountId) { ((PaxumAccountPayload) paymentAccountPayload).setEmail(accountId); } public String getEmail() { return ((PaxumAccountPayload) paymentAccountPayload).getEmail(); } @Override protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) { var field = super.getEmptyFormField(fieldId); if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setValue(""); return field; } } ================================================ FILE: core/src/main/java/haveno/core/payment/PayByMailAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PayByMailAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.NonNull; import java.util.List; public final class PayByMailAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = CurrencyUtil.getAllTraditionalCurrencies(); public PayByMailAccount() { super(PaymentMethod.PAY_BY_MAIL); } private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.TRADE_CURRENCIES, PaymentAccountFormField.FieldId.CONTACT, PaymentAccountFormField.FieldId.POSTAL_ADDRESS, PaymentAccountFormField.FieldId.EXTRA_INFO, PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.SALT ); @Override protected PaymentAccountPayload createPayload() { return new PayByMailAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { return INPUT_FIELD_IDS; } public void setPostalAddress(String postalAddress) { ((PayByMailAccountPayload) paymentAccountPayload).setPostalAddress(postalAddress); } public String getPostalAddress() { return ((PayByMailAccountPayload) paymentAccountPayload).getPostalAddress(); } public void setContact(String contact) { ((PayByMailAccountPayload) paymentAccountPayload).setContact(contact); } public String getContact() { return ((PayByMailAccountPayload) paymentAccountPayload).getContact(); } public void setExtraInfo(String extraInfo) { ((PayByMailAccountPayload) paymentAccountPayload).setExtraInfo(extraInfo); } public String getExtraInfo() { return ((PayByMailAccountPayload) paymentAccountPayload).getExtraInfo(); } @Override protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) { var field = super.getEmptyFormField(fieldId); if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setComponent(PaymentAccountFormField.Component.SELECT_ONE); if (field.getId() == PaymentAccountFormField.FieldId.CONTACT) field.setLabel(Res.get("payment.payByMail.contact.prompt")); if (field.getId() == PaymentAccountFormField.FieldId.POSTAL_ADDRESS) field.setLabel(Res.get("payment.postal.address")); if (field.getId() == PaymentAccountFormField.FieldId.EXTRA_INFO) field.setLabel(Res.get("payment.shared.optionalExtra")); return field; } } ================================================ FILE: core/src/main/java/haveno/core/payment/PayPalAccount.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.PayPalAccountPayload; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class PayPalAccount extends PaymentAccount { // https://developer.paypal.com/docs/reports/reference/paypal-supported-currencies/ public static final List SUPPORTED_CURRENCIES = List.of( new TraditionalCurrency("AUD"), new TraditionalCurrency("BRL"), new TraditionalCurrency("CAD"), new TraditionalCurrency("CNY"), new TraditionalCurrency("CZK"), new TraditionalCurrency("DKK"), new TraditionalCurrency("EUR"), new TraditionalCurrency("HKD"), new TraditionalCurrency("HUF"), new TraditionalCurrency("ILS"), new TraditionalCurrency("JPY"), new TraditionalCurrency("MYR"), new TraditionalCurrency("MXN"), new TraditionalCurrency("TWD"), new TraditionalCurrency("NZD"), new TraditionalCurrency("NOK"), new TraditionalCurrency("PHP"), new TraditionalCurrency("PLN"), new TraditionalCurrency("GBP"), new TraditionalCurrency("SGD"), new TraditionalCurrency("SEK"), new TraditionalCurrency("CHF"), new TraditionalCurrency("THB"), new TraditionalCurrency("USD")); private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.EMAIL_OR_MOBILE_NR_OR_USERNAME, PaymentAccountFormField.FieldId.TRADE_CURRENCIES, PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.EXTRA_INFO, PaymentAccountFormField.FieldId.SALT); public PayPalAccount() { super(PaymentMethod.PAYPAL); } @Override protected PaymentAccountPayload createPayload() { return new PayPalAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { return INPUT_FIELD_IDS; } public void setEmailOrMobileNrOrUsername(String emailOrMobileNrOrUsername) { ((PayPalAccountPayload) paymentAccountPayload) .setEmailOrMobileNrOrUsername(emailOrMobileNrOrUsername); } public String getEmailOrMobileNrOrUsername() { return ((PayPalAccountPayload) paymentAccountPayload).getEmailOrMobileNrOrUsername(); } public void setExtraInfo(String extraInfo) { ((PayPalAccountPayload) paymentAccountPayload).setExtraInfo(extraInfo); } public String getExtraInfo() { return ((PayPalAccountPayload) paymentAccountPayload).getExtraInfo(); } } ================================================ FILE: core/src/main/java/haveno/core/payment/PaymentAccount.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.payment; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import haveno.common.proto.ProtoUtil; import haveno.common.proto.persistable.PersistablePayload; import haveno.common.util.Utilities; import haveno.core.api.model.PaymentAccountForm; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.BankUtil; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.validation.BICValidator; import haveno.core.payment.validation.BranchIdValidator; import haveno.core.payment.validation.EmailOrMobileNrValidator; import haveno.core.payment.validation.EmailValidator; import haveno.core.payment.validation.IBANValidator; import haveno.core.payment.validation.LengthValidator; import haveno.core.proto.CoreProtoResolver; import haveno.core.trade.HavenoUtils; import haveno.core.util.validation.InputValidator; import haveno.core.util.validation.InputValidator.ValidationResult; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NonNull; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkNotNull; import static haveno.core.payment.payload.PaymentMethod.TRANSFERWISE_ID; import static java.util.Arrays.stream; import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toList; @EqualsAndHashCode @ToString @Getter @Slf4j public abstract class PaymentAccount implements PersistablePayload { protected final PaymentMethod paymentMethod; @Setter protected String id; @Setter protected long creationDate; @Setter public PaymentAccountPayload paymentAccountPayload; @Setter protected String accountName; @Setter @EqualsAndHashCode.Exclude protected String persistedAccountName; protected final List tradeCurrencies = new ArrayList<>(); @Setter @Nullable protected TradeCurrency selectedTradeCurrency; private static final GsonBuilder gsonBuilder = new GsonBuilder() .setPrettyPrinting() .serializeNulls(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// protected PaymentAccount(PaymentMethod paymentMethod) { this.paymentMethod = paymentMethod; } public void init() { id = UUID.randomUUID().toString(); creationDate = new Date().getTime(); paymentAccountPayload = createPayload(); } public void init(PaymentAccountPayload payload) { id = payload.getId(); creationDate = new Date().getTime(); paymentAccountPayload = payload; } public boolean isFiat() { return getSingleTradeCurrency() == null || CurrencyUtil.isFiatCurrency(getSingleTradeCurrency().getCode()); // TODO: check if trade currencies contain fiat } public boolean isCryptoCurrency() { return getSingleTradeCurrency() != null && CurrencyUtil.isCryptoCurrency(getSingleTradeCurrency().getCode()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.PaymentAccount toProtoMessage() { checkNotNull(accountName, "accountName must not be null"); protobuf.PaymentAccount.Builder builder = protobuf.PaymentAccount.newBuilder() .setPaymentMethod(paymentMethod.toProtoMessage()) .setId(id) .setCreationDate(creationDate) .setPaymentAccountPayload((protobuf.PaymentAccountPayload) paymentAccountPayload.toProtoMessage()) .setAccountName(accountName) .addAllTradeCurrencies(ProtoUtil.collectionToProto(tradeCurrencies, protobuf.TradeCurrency.class)); Optional.ofNullable(selectedTradeCurrency).ifPresent(selectedTradeCurrency -> builder.setSelectedTradeCurrency((protobuf.TradeCurrency) selectedTradeCurrency.toProtoMessage())); return builder.build(); } public protobuf.PaymentAccount toProtoMessage(protobuf.PaymentAccountPayload paymentAccountPayload) { checkNotNull(accountName, "accountName must not be null"); protobuf.PaymentAccount.Builder builder = protobuf.PaymentAccount.newBuilder() .setPaymentMethod(paymentMethod.toProtoMessage()) .setId(id) .setCreationDate(creationDate) .setPaymentAccountPayload(paymentAccountPayload) .setAccountName(accountName) .addAllTradeCurrencies(ProtoUtil.collectionToProto(tradeCurrencies, protobuf.TradeCurrency.class)); Optional.ofNullable(selectedTradeCurrency).ifPresent(selectedTradeCurrency -> builder.setSelectedTradeCurrency((protobuf.TradeCurrency) selectedTradeCurrency.toProtoMessage())); return builder.build(); } public static PaymentAccount fromProto(protobuf.PaymentAccount proto, CoreProtoResolver coreProtoResolver) { String paymentMethodId = proto.getPaymentMethod().getId(); List tradeCurrencies = proto.getTradeCurrenciesList().stream() .map(TradeCurrency::fromProto) .collect(Collectors.toList()); // Remove deprecated currencies which are nullified tradeCurrencies.removeIf(currency -> currency == null); // We need to remove NGN for Transferwise Optional ngnTwOptional = tradeCurrencies.stream() .filter(e -> paymentMethodId.equals(TRANSFERWISE_ID)) .filter(e -> e.getCode().equals("NGN")) .findAny(); // We cannot remove it in the stream as it would cause a concurrentModificationException ngnTwOptional.ifPresent(tradeCurrencies::remove); try { if (tradeCurrencies.isEmpty()) { throw new RuntimeException("No trade currencies found for account: " + proto.getAccountName() + ", payment method id: " + paymentMethodId + ", account id: " + proto.getId()); } PaymentAccount account = PaymentAccountFactory.getPaymentAccount(PaymentMethod.getPaymentMethodOrNA(paymentMethodId)); account.getTradeCurrencies().clear(); account.setId(proto.getId()); account.setCreationDate(proto.getCreationDate()); account.setAccountName(proto.getAccountName()); account.setPersistedAccountName(proto.getAccountName()); account.getTradeCurrencies().addAll(tradeCurrencies); account.setPaymentAccountPayload(coreProtoResolver.fromProto(proto.getPaymentAccountPayload())); if (proto.hasSelectedTradeCurrency()) account.setSelectedTradeCurrency(TradeCurrency.fromProto(proto.getSelectedTradeCurrency())); return account; } catch (RuntimeException e) { log.warn("Could not load account: {}, exception: {}", paymentMethodId, e.toString()); return null; } } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public Date getCreationDate() { return new Date(creationDate); } public void addCurrency(TradeCurrency tradeCurrency) { if (!tradeCurrencies.contains(tradeCurrency)) tradeCurrencies.add(tradeCurrency); } public void removeCurrency(TradeCurrency tradeCurrency) { tradeCurrencies.remove(tradeCurrency); } public boolean hasMultipleCurrencies() { return tradeCurrencies.size() > 1; } public void setSingleTradeCurrency(TradeCurrency tradeCurrency) { tradeCurrencies.clear(); tradeCurrencies.add(tradeCurrency); setSelectedTradeCurrency(tradeCurrency); } @Nullable public TradeCurrency getSingleTradeCurrency() { if (tradeCurrencies.size() == 1) return tradeCurrencies.get(0); else return null; } public long getMaxTradePeriod() { return paymentMethod.getMaxTradePeriod(); } protected abstract PaymentAccountPayload createPayload(); public void setSalt(byte[] salt) { paymentAccountPayload.setSalt(salt); } public byte[] getSalt() { return paymentAccountPayload.getSalt(); } public void setSaltAsHex(String saltAsHex) { setSalt(Utilities.decodeFromHex(saltAsHex)); } public String getSaltAsHex() { return Utilities.bytesAsHexString(getSalt()); } public String getOwnerId() { return paymentAccountPayload.getOwnerId(); } public boolean isCountryBasedPaymentAccount() { return this instanceof CountryBasedPaymentAccount; } public boolean hasPaymentMethodWithId(String paymentMethodId) { return this.getPaymentMethod().getId().equals(paymentMethodId); } /** * Return an Optional of the trade currency for this payment account, or * Optional.empty() if none is found. If this payment account has a selected * trade currency, that is returned, else its single trade currency is returned, * else the first trade currency in this payment account's tradeCurrencies * list is returned. * * @return Optional of the trade currency for the given payment account */ public Optional getTradeCurrency() { if (this.getSelectedTradeCurrency() != null) return Optional.of(this.getSelectedTradeCurrency()); else if (this.getSingleTradeCurrency() != null) return Optional.of(this.getSingleTradeCurrency()); else if (!this.getTradeCurrencies().isEmpty()) return Optional.of(this.getTradeCurrencies().get(0)); else return Optional.empty(); } public void onAddToUser() { // We are in the process to get added to the user. This is called just before saving the account and the // last moment we could apply some special handling if needed (e.g. as it happens for Revolut) } public String getPreTradeMessage(boolean isBuyer) { if (isBuyer) { return getMessageForBuyer(); } else { return getMessageForSeller(); } } // will be overridden by specific account when necessary public String getMessageForBuyer() { return null; } // will be overridden by specific account when necessary public String getMessageForSeller() { return null; } // will be overridden by specific account when necessary public String getMessageForAccountCreation() { return null; } public void onPersistChanges() { setPersistedAccountName(getAccountName()); } public void revertChanges() { setAccountName(getPersistedAccountName()); } @NonNull public abstract List getSupportedCurrencies(); // ---------------------------- SERIALIZATION ----------------------------- public String toJson() { Gson gson = gsonBuilder.create(); Map jsonMap = new HashMap<>(); if (paymentAccountPayload != null) { String payloadJson = paymentAccountPayload.toJson(); Map payloadMap = gson.fromJson(payloadJson, new TypeToken>() {}.getType()); for (Map.Entry entry : payloadMap.entrySet()) { Object value = entry.getValue(); if (value instanceof List) { List list = (List) value; String joinedString = list.stream().map(Object::toString).collect(Collectors.joining(",")); entry.setValue(joinedString); } } jsonMap.putAll(payloadMap); } jsonMap.put("accountName", getAccountName()); jsonMap.put("accountId", getId()); if (paymentAccountPayload != null) jsonMap.put("salt", getSaltAsHex()); return gson.toJson(jsonMap); } /** * Deserialize a PaymentAccount json string into a new PaymentAccount instance. * * @param paymentAccountJsonString The json data representing a new payment account form. * @return A populated PaymentAccount subclass instance. */ public static synchronized PaymentAccount fromJson(String paymentAccountJsonString) { Class clazz = getPaymentAccountClassFromJson(paymentAccountJsonString); Gson gson = gsonBuilder.registerTypeAdapter(clazz, new PaymentAccountTypeAdapter(clazz)).create(); return gson.fromJson(paymentAccountJsonString, clazz); } private static Class getPaymentAccountClassFromJson(String json) { Map jsonMap = gsonBuilder.create().fromJson(json, (Type) Object.class); String paymentMethodId = checkNotNull((String) jsonMap.get("paymentMethodId"), String.format("cannot not find a paymentMethodId in json string: %s", json)); return getPaymentAccountClass(paymentMethodId); } private static Class getPaymentAccountClass(String paymentMethodId) { PaymentMethod paymentMethod = PaymentMethod.getPaymentMethodOrNA(paymentMethodId); return PaymentAccountFactory.getPaymentAccount(paymentMethod).getClass(); } // ------------------------- PAYMENT ACCOUNT FORM ------------------------- @NonNull public abstract List getInputFieldIds(); public PaymentAccountForm toForm() { // convert to json map Map jsonMap = gsonBuilder.create().fromJson(toJson(), (Type) Object.class); // build form PaymentAccountForm form = new PaymentAccountForm(PaymentAccountForm.FormId.valueOf(paymentMethod.getId())); for (PaymentAccountFormField.FieldId fieldId : getInputFieldIds()) { PaymentAccountFormField field = getEmptyFormField(fieldId); field.setValue((String) jsonMap.get(HavenoUtils.toCamelCase(field.getId().toString()))); form.getFields().add(field); } return form; } public void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) { switch (fieldId) { case ACCEPTED_COUNTRY_CODES: { List countryCodes = PaymentAccount.commaDelimitedCodesToList.apply(value); List supportedCountryCodes = CountryUtil.getCountryCodes(((CountryBasedPaymentAccount) this).getSupportedCountries()); for (String countryCode : countryCodes) { if (!supportedCountryCodes.contains(countryCode)) throw new IllegalArgumentException("Country is not supported by " + getPaymentMethod().getId() + ": " + value); } break; } case ACCOUNT_ID: processValidationResult(new InputValidator().validate(value)); break; case ACCOUNT_NAME: processValidationResult(new LengthValidator(2, 100).validate(value)); break; case ACCOUNT_NR: processValidationResult(new LengthValidator(2, 100).validate(value)); break; case ACCOUNT_OWNER: processValidationResult(new LengthValidator(2, 100).validate(value)); break; case ACCOUNT_TYPE: processValidationResult(new LengthValidator(2, 100).validate(value)); break; case ANSWER: throw new IllegalArgumentException("Not implemented"); case BANK_ACCOUNT_NAME: processValidationResult(new LengthValidator(2, 100).validate(value)); break; case BANK_ACCOUNT_NUMBER: throw new IllegalArgumentException("Not implemented"); case BANK_ACCOUNT_TYPE: throw new IllegalArgumentException("Not implemented"); case BANK_ADDRESS: case INTERMEDIARY_ADDRESS: processValidationResult(new LengthValidator(1, 100).validate(value)); break; case BANK_BRANCH: case INTERMEDIARY_BRANCH: processValidationResult(new LengthValidator(2, 34).validate(value)); break; case BANK_BRANCH_CODE: throw new IllegalArgumentException("Not implemented"); case BANK_BRANCH_NAME: throw new IllegalArgumentException("Not implemented"); case BANK_CODE: throw new IllegalArgumentException("Not implemented"); case BANK_COUNTRY_CODE: if (!CountryUtil.findCountryByCode(value).isPresent()) throw new IllegalArgumentException("Invalid country code: " + value); break; case BANK_ID: throw new IllegalArgumentException("Not implemented"); case BANK_NAME: case INTERMEDIARY_NAME: processValidationResult(new LengthValidator(2, 34).validate(value)); break; case BANK_SWIFT_CODE: case INTERMEDIARY_SWIFT_CODE: processValidationResult(new LengthValidator(11, 11).validate(value)); break; case BENEFICIARY_ACCOUNT_NR: processValidationResult(new LengthValidator(2, 40).validate(value)); break; case BENEFICIARY_ADDRESS: processValidationResult(new LengthValidator(1, 100).validate(value)); break; case BENEFICIARY_CITY: processValidationResult(new LengthValidator(2, 34).validate(value)); break; case BENEFICIARY_NAME: processValidationResult(new LengthValidator(2, 34).validate(value)); break; case BENEFICIARY_PHONE: processValidationResult(new LengthValidator(2, 34).validate(value)); break; case BIC: processValidationResult(new BICValidator().validate(value)); break; case BRANCH_ID: processValidationResult(new LengthValidator(2, 34).validate(value)); break; case CITY: processValidationResult(new LengthValidator(2, 34).validate(value)); break; case CONTACT: processValidationResult(new InputValidator().validate(value)); break; case COUNTRY: if (this instanceof CountryBasedPaymentAccount) { List supportedCountries = ((CountryBasedPaymentAccount) this).getSupportedCountries(); if (supportedCountries != null && !supportedCountries.isEmpty()) { List supportedCountryCodes = CountryUtil.getCountryCodes(supportedCountries); if (!supportedCountryCodes.contains(value)) throw new IllegalArgumentException("Country is not supported by " + getPaymentMethod().getId() + ": " + value); return; } } if (!CountryUtil.findCountryByCode(value).isPresent()) throw new IllegalArgumentException("Invalid country code: " + value); break; case EMAIL: processValidationResult(new EmailValidator().validate(value)); break; case EMAIL_OR_MOBILE_NR: processValidationResult(new EmailOrMobileNrValidator().validate(value)); break; case EXTRA_INFO: break; case HOLDER_ADDRESS: processValidationResult(new LengthValidator(0, 100).validate(value)); break; case HOLDER_EMAIL: throw new IllegalArgumentException("Not implemented"); case HOLDER_NAME: processValidationResult(new LengthValidator(2, 100).validate(value)); break; case HOLDER_TAX_ID: throw new IllegalArgumentException("Not implemented"); case IBAN: processValidationResult(new IBANValidator().validate(value)); break; case IFSC: throw new IllegalArgumentException("Not implemented"); case INTERMEDIARY_COUNTRY_CODE: if (!CountryUtil.findCountryByCode(value).isPresent()) throw new IllegalArgumentException("Invalid country code: " + value); break; case MOBILE_NR: throw new IllegalArgumentException("Not implemented"); case NATIONAL_ACCOUNT_ID: throw new IllegalArgumentException("Not implemented"); case PAYID: processValidationResult(new LengthValidator(2, 100).validate(value)); break; case PIX_KEY: processValidationResult(new LengthValidator(2, 100).validate(value)); break; case POSTAL_ADDRESS: processValidationResult(new InputValidator().validate(value)); break; case PROMPT_PAY_ID: throw new IllegalArgumentException("Not implemented"); case QUESTION: throw new IllegalArgumentException("Not implemented"); case REQUIREMENTS: throw new IllegalArgumentException("Not implemented"); case SALT: if (!value.equals("")) throw new IllegalArgumentException("Salt must be empty"); break; case SORT_CODE: processValidationResult(new BranchIdValidator("GB").validate(value)); break; case SPECIAL_INSTRUCTIONS: break; case STATE: String countryCode = form.getValue(PaymentAccountFormField.FieldId.COUNTRY); boolean isStateRequired = BankUtil.isStateRequired(countryCode); if (value == null || value.isEmpty()) { if (isStateRequired) throw new IllegalArgumentException("Must provide state for country " + countryCode); } else { if (!isStateRequired) throw new IllegalArgumentException("Must not provide state for country " + countryCode); } break; case TRADE_CURRENCIES: processValidationResult(new InputValidator().validate(value)); List currencyCodes = commaDelimitedCodesToList.apply(value); Optional> tradeCurrencies = CurrencyUtil.getTradeCurrenciesInList(currencyCodes, getSupportedCurrencies()); if (!tradeCurrencies.isPresent()) throw new IllegalArgumentException("No trade currencies were found in the " + getPaymentMethod().getDisplayString() + " account form"); break; case USERNAME: processValidationResult(new LengthValidator(3, 100).validate(value)); break; case EMAIL_OR_MOBILE_NR_OR_USERNAME: processValidationResult(new LengthValidator(3, 100).validate(value)); break; case EMAIL_OR_MOBILE_NR_OR_CASHTAG: processValidationResult(new LengthValidator(3, 100).validate(value)); break; case ADDRESS: processValidationResult(new LengthValidator(10, 150).validate(value)); // TODO: validate crypto address break; default: throw new RuntimeException("Unhandled form field: " + fieldId); } } protected void processValidationResult(ValidationResult result) { if (!result.isValid) throw new IllegalArgumentException(result.errorMessage); } protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) { PaymentAccountFormField field = new PaymentAccountFormField(fieldId); switch (fieldId) { case ACCEPTED_COUNTRY_CODES: field.setLabel(Res.get("payment.accepted.countries")); List supportedCountries = ((CountryBasedPaymentAccount) this).getSupportedCountries(); field.setSupportedCountries(supportedCountries); field.setComponent(supportedCountries.size() == 1 ? PaymentAccountFormField.Component.SELECT_ONE : PaymentAccountFormField.Component.SELECT_MULTIPLE); break; case ACCOUNT_ID: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.uphold.accountId")); break; case ACCOUNT_NAME: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.account.name")); field.setMinLength(3); field.setMaxLength(100); break; case ACCOUNT_NR: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.accountNr")); break; case ACCOUNT_OWNER: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.account.owner.fullname")); break; case ACCOUNT_TYPE: field.setComponent(PaymentAccountFormField.Component.SELECT_ONE); field.setLabel(Res.get("payment.select.account")); break; case ANSWER: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.answer")); break; case BANK_ACCOUNT_NAME: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.account.owner.fullname")); field.setMinLength(2); field.setMaxLength(100); break; case BANK_ACCOUNT_NUMBER: throw new IllegalArgumentException("Not implemented"); case BANK_ACCOUNT_TYPE: throw new IllegalArgumentException("Not implemented"); case BANK_ADDRESS: field.setComponent(PaymentAccountFormField.Component.TEXTAREA); field.setLabel(Res.get("payment.swift.address.bank")); break; case BANK_BRANCH: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.swift.branch.bank")); break; case BANK_BRANCH_CODE: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.swift.swiftCode.bank")); break; case BANK_BRANCH_NAME: throw new IllegalArgumentException("Not implemented"); case BANK_CODE: throw new IllegalArgumentException("Not implemented"); case BANK_COUNTRY_CODE: field.setComponent(PaymentAccountFormField.Component.SELECT_ONE); field.setLabel(Res.get("payment.bank.country")); break; case BANK_ID: throw new IllegalArgumentException("Not implemented"); case BANK_NAME: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.swift.name.bank")); break; case BANK_SWIFT_CODE: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.swift.swiftCode.bank")); break; case BENEFICIARY_ACCOUNT_NR: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.swift.account")); // TODO: this is specific to swift break; case BENEFICIARY_ADDRESS: field.setComponent(PaymentAccountFormField.Component.TEXTAREA); field.setLabel(Res.get("payment.swift.address.beneficiary")); // TODO: this is specific to swift break; case BENEFICIARY_CITY: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.account.city")); break; case BENEFICIARY_NAME: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.account.owner.fullname")); break; case BENEFICIARY_PHONE: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.swift.phone.beneficiary")); break; case BIC: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel("BIC"); break; case BRANCH_ID: field.setComponent(PaymentAccountFormField.Component.TEXT); //field.setLabel("Not implemented"); // expected to be overridden by subclasses break; case CITY: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.account.city")); case CONTACT: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.payByMail.contact")); case COUNTRY: field.setComponent(PaymentAccountFormField.Component.SELECT_ONE); field.setLabel(Res.get("shared.country")); if (this instanceof CountryBasedPaymentAccount) field.setSupportedCountries(((CountryBasedPaymentAccount) this).getSupportedCountries()); break; case EMAIL: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setType("email"); field.setLabel(Res.get("payment.email")); break; case EMAIL_OR_MOBILE_NR: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.email.mobile")); break; case EXTRA_INFO: field.setComponent(PaymentAccountFormField.Component.TEXTAREA); field.setLabel(Res.get("payment.shared.optionalExtra")); break; case HOLDER_ADDRESS: field.setComponent(PaymentAccountFormField.Component.TEXTAREA); field.setLabel(Res.get("payment.account.owner.address")); break; case HOLDER_EMAIL: throw new IllegalArgumentException("Not implemented"); case HOLDER_NAME: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.account.owner.fullname")); field.setMinLength(2); field.setMaxLength(100); break; case HOLDER_TAX_ID: throw new IllegalArgumentException("Not implemented"); case IBAN: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel("IBAN"); break; case IFSC: throw new IllegalArgumentException("Not implemented"); case INTERMEDIARY_ADDRESS: field.setComponent(PaymentAccountFormField.Component.TEXTAREA); field.setLabel(Res.get("payment.swift.address.intermediary")); break; case INTERMEDIARY_BRANCH: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.swift.branch.intermediary")); break; case INTERMEDIARY_COUNTRY_CODE: field.setComponent(PaymentAccountFormField.Component.SELECT_ONE); field.setLabel(Res.get("payment.swift.country.intermediary")); break; case INTERMEDIARY_NAME: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.swift.name.intermediary")); break; case INTERMEDIARY_SWIFT_CODE: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.swift.swiftCode.intermediary")); break; case MOBILE_NR: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.mobile")); break; case NATIONAL_ACCOUNT_ID: throw new IllegalArgumentException("Not implemented"); case PAYID: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.email.mobile")); break; case PIX_KEY: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.pix.key")); break; case POSTAL_ADDRESS: field.setComponent(PaymentAccountFormField.Component.TEXTAREA); field.setLabel(Res.get("payment.postal.address")); break; case PROMPT_PAY_ID: throw new IllegalArgumentException("Not implemented"); case QUESTION: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.secret")); break; case REQUIREMENTS: throw new IllegalArgumentException("Not implemented"); case SALT: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel("Salt"); break; case SORT_CODE: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.fasterPayments.ukSortCode")); break; case SPECIAL_INSTRUCTIONS: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.shared.extraInfo")); break; case STATE: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.account.state")); field.setRequiredForCountries(CountryUtil.getCountryCodes(BankUtil.getAllStateRequiredCountries())); break; case TRADE_CURRENCIES: field.setComponent(PaymentAccountFormField.Component.SELECT_MULTIPLE); field.setLabel(Res.get("payment.supportedCurrencies")); field.setSupportedCurrencies(getSupportedCurrencies()); field.setValue(String.join(",", getSupportedCurrencies().stream().map(TradeCurrency::getCode).collect(Collectors.toList()))); break; case USERNAME: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.account.username")); field.setMinLength(3); field.setMaxLength(100); break; case EMAIL_OR_MOBILE_NR_OR_USERNAME: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.email.mobile.username")); field.setMinLength(3); field.setMaxLength(100); break; case EMAIL_OR_MOBILE_NR_OR_CASHTAG: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.email.mobile.cashtag")); field.setMinLength(3); field.setMaxLength(100); break; case ADDRESS: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.account.address")); field.setMinLength(10); field.setMaxLength(150); break; default: throw new RuntimeException("Unhandled form field: " + field); } if ("".equals(field.getValue())) field.setValue(""); return field; } private static final Predicate isCommaDelimitedCurrencyList = (s) -> s != null && s.contains(","); public static final Function> commaDelimitedCodesToList = (s) -> { if (isCommaDelimitedCurrencyList.test(s)) return stream(s.split(",")).map(a -> a.trim().toUpperCase()).collect(toList()); else if (s != null && !s.isEmpty()) return singletonList(s.trim().toUpperCase()); else return new ArrayList<>(); }; } ================================================ FILE: core/src/main/java/haveno/core/payment/PaymentAccountFactory.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.payment.payload.PaymentMethod; public class PaymentAccountFactory { public static PaymentAccount getPaymentAccount(PaymentMethod paymentMethod) { switch (paymentMethod.getId()) { case PaymentMethod.UPHOLD_ID: return new UpholdAccount(); case PaymentMethod.MONEY_BEAM_ID: return new MoneyBeamAccount(); case PaymentMethod.POPMONEY_ID: return new PopmoneyAccount(); case PaymentMethod.REVOLUT_ID: return new RevolutAccount(); case PaymentMethod.PERFECT_MONEY_ID: return new PerfectMoneyAccount(); case PaymentMethod.SEPA_ID: return new SepaAccount(); case PaymentMethod.SEPA_INSTANT_ID: return new SepaInstantAccount(); case PaymentMethod.FASTER_PAYMENTS_ID: return new FasterPaymentsAccount(); case PaymentMethod.NATIONAL_BANK_ID: return new NationalBankAccount(); case PaymentMethod.SAME_BANK_ID: return new SameBankAccount(); case PaymentMethod.SPECIFIC_BANKS_ID: return new SpecificBanksAccount(); case PaymentMethod.JAPAN_BANK_ID: return new JapanBankAccount(); case PaymentMethod.AUSTRALIA_PAYID_ID: return new AustraliaPayidAccount(); case PaymentMethod.ALI_PAY_ID: return new AliPayAccount(); case PaymentMethod.WECHAT_PAY_ID: return new WeChatPayAccount(); case PaymentMethod.SWISH_ID: return new SwishAccount(); case PaymentMethod.ZELLE_ID: return new ZelleAccount(); case PaymentMethod.CHASE_QUICK_PAY_ID: return new ChaseQuickPayAccount(); case PaymentMethod.INTERAC_E_TRANSFER_ID: return new InteracETransferAccount(); case PaymentMethod.US_POSTAL_MONEY_ORDER_ID: return new USPostalMoneyOrderAccount(); case PaymentMethod.CASH_DEPOSIT_ID: return new CashDepositAccount(); case PaymentMethod.BLOCK_CHAINS_ID: return new CryptoCurrencyAccount(); case PaymentMethod.MONEY_GRAM_ID: return new MoneyGramAccount(); case PaymentMethod.WESTERN_UNION_ID: return new WesternUnionAccount(); case PaymentMethod.HAL_CASH_ID: return new HalCashAccount(); case PaymentMethod.F2F_ID: return new F2FAccount(); case PaymentMethod.PAY_BY_MAIL_ID: return new PayByMailAccount(); case PaymentMethod.CASH_AT_ATM_ID: return new CashAtAtmAccount(); case PaymentMethod.PROMPT_PAY_ID: return new PromptPayAccount(); case PaymentMethod.ADVANCED_CASH_ID: return new AdvancedCashAccount(); case PaymentMethod.TRANSFERWISE_ID: return new TransferwiseAccount(); case PaymentMethod.TRANSFERWISE_USD_ID: return new TransferwiseUsdAccount(); case PaymentMethod.PAYPAL_ID: return new PayPalAccount(); case PaymentMethod.PAYSERA_ID: return new PayseraAccount(); case PaymentMethod.PAXUM_ID: return new PaxumAccount(); case PaymentMethod.NEFT_ID: return new NeftAccount(); case PaymentMethod.RTGS_ID: return new RtgsAccount(); case PaymentMethod.IMPS_ID: return new ImpsAccount(); case PaymentMethod.UPI_ID: return new UpiAccount(); case PaymentMethod.PAYTM_ID: return new PaytmAccount(); case PaymentMethod.NEQUI_ID: return new NequiAccount(); case PaymentMethod.BIZUM_ID: return new BizumAccount(); case PaymentMethod.PIX_ID: return new PixAccount(); case PaymentMethod.AMAZON_GIFT_CARD_ID: return new AmazonGiftCardAccount(); case PaymentMethod.BLOCK_CHAINS_INSTANT_ID: return new InstantCryptoCurrencyAccount(); case PaymentMethod.CAPITUAL_ID: return new CapitualAccount(); case PaymentMethod.CELPAY_ID: return new CelPayAccount(); case PaymentMethod.MONESE_ID: return new MoneseAccount(); case PaymentMethod.SATISPAY_ID: return new SatispayAccount(); case PaymentMethod.TIKKIE_ID: return new TikkieAccount(); case PaymentMethod.VERSE_ID: return new VerseAccount(); case PaymentMethod.STRIKE_ID: return new StrikeAccount(); case PaymentMethod.SWIFT_ID: return new SwiftAccount(); case PaymentMethod.ACH_TRANSFER_ID: return new AchTransferAccount(); case PaymentMethod.DOMESTIC_WIRE_TRANSFER_ID: return new DomesticWireTransferAccount(); case PaymentMethod.CASH_APP_ID: return new CashAppAccount(); case PaymentMethod.VENMO_ID: return new VenmoAccount(); case PaymentMethod.PAYSAFE_ID: return new PaysafeAccount(); // Cannot be deleted as it would break old trade history entries case PaymentMethod.OK_PAY_ID: return new OKPayAccount(); default: throw new RuntimeException("Not supported PaymentMethod: " + paymentMethod); } } } ================================================ FILE: core/src/main/java/haveno/core/payment/PaymentAccountList.java ================================================ /* * 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 . */ package haveno.core.payment; import com.google.protobuf.Message; import haveno.common.proto.persistable.PersistableList; import haveno.core.proto.CoreProtoResolver; import lombok.EqualsAndHashCode; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @EqualsAndHashCode(callSuper = true) public class PaymentAccountList extends PersistableList { public PaymentAccountList(List list) { super(list); } @Override public Message toProtoMessage() { synchronized (getList()) { return protobuf.PersistableEnvelope.newBuilder() .setPaymentAccountList(protobuf.PaymentAccountList.newBuilder() .addAllPaymentAccount(getList().stream().map(PaymentAccount::toProtoMessage).collect(Collectors.toList()))) .build(); } } public static PaymentAccountList fromProto(protobuf.PaymentAccountList proto, CoreProtoResolver coreProtoResolver) { return new PaymentAccountList(new ArrayList<>(proto.getPaymentAccountList().stream() .map(e -> PaymentAccount.fromProto(e, coreProtoResolver)) .filter(Objects::nonNull) .collect(Collectors.toList()))); } } ================================================ FILE: core/src/main/java/haveno/core/payment/PaymentAccountTypeAdapter.java ================================================ /* * 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 . */ package haveno.core.payment; import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Predicate; import static haveno.common.util.ReflectionUtils.getSetterMethodForFieldInClassHierarchy; import static haveno.common.util.ReflectionUtils.getVisibilityModifierAsString; import static haveno.common.util.ReflectionUtils.handleSetFieldValueError; import static haveno.common.util.ReflectionUtils.isSetterOnClass; import static haveno.common.util.ReflectionUtils.loadFieldListForClassHierarchy; import static haveno.common.util.Utilities.decodeFromHex; import static haveno.core.locale.CountryUtil.findCountryByCode; import static haveno.core.locale.CurrencyUtil.getTradeCurrenciesInList; import static haveno.core.locale.CurrencyUtil.getTradeCurrency; import static haveno.core.payment.payload.PaymentMethod.AMAZON_GIFT_CARD_ID; import static haveno.core.payment.payload.PaymentMethod.MONEY_GRAM_ID; import static java.lang.String.format; import static java.util.Arrays.stream; import static java.util.Collections.unmodifiableMap; import static java.util.Comparator.comparing; import static org.apache.commons.lang3.StringUtils.capitalize; @Slf4j class PaymentAccountTypeAdapter extends TypeAdapter { private static final String[] JSON_COMMENTS = new String[]{ "Do not manually edit the paymentMethodId field.", "Edit the salt field only if you are recreating a payment" + " account on a new installation and wish to preserve the account age." }; private final Class paymentAccountType; private final Class paymentAccountPayloadType; private final Map> fieldSettersMap; private final Predicate isExcludedField; /** * Constructor used when de-serializing a json payment account form into a * PaymentAccount instance. * * @param paymentAccountType the PaymentAccount subclass being instantiated */ public PaymentAccountTypeAdapter(Class paymentAccountType) { this(paymentAccountType, new String[]{}); } /** * Constructor used when serializing a PaymentAccount subclass instance into a json * payment account json form. * * @param paymentAccountType the PaymentAccount subclass being serialized * @param excludedFields a string array of field names to exclude from the serialized * payment account json form. */ public PaymentAccountTypeAdapter(Class paymentAccountType, String[] excludedFields) { this.paymentAccountType = paymentAccountType; this.paymentAccountPayloadType = getPaymentAccountPayloadType(); this.isExcludedField = (f) -> stream(excludedFields).anyMatch(e -> e.equals(f.getName())); this.fieldSettersMap = getFieldSetterMap(); } @Override public void write(JsonWriter out, PaymentAccount account) throws IOException { // We write a blank payment acct form for a payment method id. // We're not serializing a real payment account instance here. out.beginObject(); writeComments(out, account); out.name("paymentMethodId"); out.value(account.getPaymentMethod().getId()); // Write the editable, PaymentAccount subclass specific fields. writeInnerMutableFields(out, account); // The last field in all json forms is the empty, editable salt field. out.name("salt"); out.value(""); out.endObject(); } private void writeComments(JsonWriter out, PaymentAccount account) throws IOException { // All json forms start with immutable _COMMENTS_ and paymentMethodId fields. out.name("_COMMENTS_"); out.beginArray(); for (String s : JSON_COMMENTS) { out.value(s); } if (account.hasPaymentMethodWithId("SWIFT_ID")) { // Add extra comments for more complex swift account form. List wrappedSwiftComments = Res.getWrappedAsList("payment.swift.info.account", 110); for (String line : wrappedSwiftComments) { out.value(line); } } out.endArray(); } private void writeInnerMutableFields(JsonWriter out, PaymentAccount account) { if (account.hasMultipleCurrencies()) { writeTradeCurrenciesField(out, account); writeSelectedTradeCurrencyField(out, account); } fieldSettersMap.forEach((field, value) -> { try { // Write out a json element if there is a @Setter for this field. if (value.isPresent()) { log.debug("Append form with settable field: {} {} {} setter: {}", getVisibilityModifierAsString(field), field.getType().getSimpleName(), field.getName(), value); String fieldName = field.getName(); out.name(fieldName); if (fieldName.equals("country")) out.value("your two letter country code"); else out.value("your " + fieldName.toLowerCase()); } } catch (Exception ex) { String errMsg = format("cannot create a new %s json form", account.getClass().getSimpleName()); log.error(capitalize(errMsg) + ".", ex); throw new IllegalStateException("programmer error: " + errMsg); } }); } // In some cases (TransferwiseAccount), we need to include a 'tradeCurrencies' // field in the json form, though the 'tradeCurrencies' field has no setter method in // the PaymentAccount class hierarchy. At of time of this change, TransferwiseAccount // is the only known exception to the rule. private void writeTradeCurrenciesField(JsonWriter out, PaymentAccount account) { try { String fieldName = "tradeCurrencies"; log.debug("Append form with non-settable field: {}", fieldName); out.name(fieldName); out.value("comma delimited currency code list, e.g., gbp,eur,jpy,usd"); } catch (Exception ex) { String errMsg = format("cannot create a new %s json form", account.getClass().getSimpleName()); log.error(capitalize(errMsg) + ".", ex); throw new IllegalStateException("programmer error: " + errMsg); } } // PaymentAccounts that support multiple 'tradeCurrencies' need to define a // 'selectedTradeCurrency' field (not simply defaulting to first in list). // Write this field to the form. private void writeSelectedTradeCurrencyField(JsonWriter out, PaymentAccount account) { try { String fieldName = "selectedTradeCurrency"; log.debug("Append form with settable field: {}", fieldName); out.name(fieldName); out.value("primary trading currency code, e.g., eur"); } catch (Exception ex) { String errMsg = format("cannot create a new %s json form", account.getClass().getSimpleName()); log.error(capitalize(errMsg) + ".", ex); throw new IllegalStateException("programmer error: " + errMsg); } } @Override public PaymentAccount read(JsonReader in) throws IOException { PaymentAccount account = initNewPaymentAccount(); in.beginObject(); while (in.hasNext()) { String currentFieldName = in.nextName(); // The tradeCurrencies field is common to all payment account types, // but has no setter. if (didReadTradeCurrenciesField(in, account, currentFieldName)) continue; // The acceptedCountryCodes field has no setter. if (didReadAcceptedCountryCodes(in, account, currentFieldName)) continue; // The selectedTradeCurrency field is common to all payment account types, // but is @Nullable, and may not need to be explicitly defined by user. if (didReadSelectedTradeCurrencyField(in, account, currentFieldName)) continue; // Some fields are common to all payment account types. if (didReadCommonField(in, account, currentFieldName)) continue; // If the account is a subclass of CountryBasedPaymentAccount, set the // account's Country, and use the Country to derive and set the account's // FiatCurrency. if (didReadCountryField(in, account, currentFieldName)) continue; Optional field = fieldSettersMap.keySet().stream() .filter(k -> k.getName().equals(currentFieldName)).findFirst(); field.ifPresentOrElse((f) -> invokeSetterMethod(account, f, in), () -> { throw new IllegalStateException( format("programmer error: cannot de-serialize json to a '%s' " + " because there is no %s field.", account.getClass().getSimpleName(), currentFieldName)); }); } in.endObject(); return account; } private void invokeSetterMethod(PaymentAccount account, Field field, JsonReader jsonReader) { Optional setter = fieldSettersMap.get(field); if (setter.isPresent()) { try { // The setter might be on the PaymentAccount instance, or its // PaymentAccountPayload instance. if (isSetterOnPaymentAccountClass(setter.get(), account)) { setter.get().invoke(account, nextStringOrNull(jsonReader)); } else if (isSetterOnPaymentAccountPayloadClass(setter.get(), account)) { setter.get().invoke(account.getPaymentAccountPayload(), nextStringOrNull(jsonReader)); } else { String errMsg = format("programmer error: cannot de-serialize json to a '%s' using reflection" + " because the setter method's declaring class was not found.", account.getClass().getSimpleName()); throw new IllegalStateException(errMsg); } } catch (ReflectiveOperationException ex) { handleSetFieldValueError(account, field, ex); } } else { throw new IllegalStateException( format("programmer error: cannot de-serialize json to a '%s' " + " because field value cannot be set %s.", account.getClass().getSimpleName(), field.getName())); } } private boolean isSetterOnPaymentAccountClass(Method setter, PaymentAccount account) { return isSetterOnClass(setter, account.getClass()); } private boolean isSetterOnPaymentAccountPayloadClass(Method setter, PaymentAccount account) { return isSetterOnClass(setter, account.getPaymentAccountPayload().getClass()) || isSetterOnClass(setter, account.getPaymentAccountPayload().getClass().getSuperclass()); } private Map> getFieldSetterMap() { List orderedFields = getOrderedFields(); Map> map = new LinkedHashMap<>(); for (Field field : orderedFields) { Optional setter = getSetterMethodForFieldInClassHierarchy(field, paymentAccountType) .or(() -> getSetterMethodForFieldInClassHierarchy(field, paymentAccountPayloadType)); map.put(field, setter); } return unmodifiableMap(map); } private List getOrderedFields() { List fields = new ArrayList<>(); loadFieldListForClassHierarchy(fields, paymentAccountType, isExcludedField); loadFieldListForClassHierarchy(fields, paymentAccountPayloadType, isExcludedField); fields.sort(comparing(Field::getName)); return fields; } private String nextStringOrNull(JsonReader in) { try { if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; } else { return in.nextString(); } } catch (IOException ex) { String errMsg = "cannot see next string in json reader"; log.error(capitalize(errMsg) + ".", ex); throw new IllegalStateException("programmer error: " + errMsg); } } @SuppressWarnings("unused") private Long nextLongOrNull(JsonReader in) { try { if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; } else { return in.nextLong(); } } catch (IOException ex) { String errMsg = "cannot see next long in json reader"; log.error(capitalize(errMsg) + ".", ex); throw new IllegalStateException("programmer error: " + errMsg); } } private boolean didReadAcceptedCountryCodes(JsonReader in, PaymentAccount account, String fieldName) { if (!fieldName.equals("acceptedCountryCodes")) return false; String fieldValue = nextStringOrNull(in); List countryCodes = PaymentAccount.commaDelimitedCodesToList.apply(fieldValue); ((CountryBasedPaymentAccount) account).setAcceptedCountries(CountryUtil.getCountries(countryCodes)); return true; } private boolean didReadTradeCurrenciesField(JsonReader in, PaymentAccount account, String fieldName) { if (!fieldName.equals("tradeCurrencies")) return false; // The PaymentAccount.tradeCurrencies field is a special case because it has // no setter, so we add currencies to the List here if the payment account // supports multiple trade currencies. String fieldValue = nextStringOrNull(in); List currencyCodes = PaymentAccount.commaDelimitedCodesToList.apply(fieldValue); Optional> tradeCurrencies = getReconciledTradeCurrencies(currencyCodes, account); if (tradeCurrencies.isPresent()) { for (TradeCurrency tradeCurrency : tradeCurrencies.get()) { account.addCurrency(tradeCurrency); } } else { // Log a warning. We should not throw an exception here because the // gson library will not pass it up to the calling Haveno object exactly as // it would be defined here (causing confusion). Do a check in a calling // class to make sure the tradeCurrencies field is populated in the // PaymentAccount object, if it is required for the payment account method. log.warn("No trade currencies were found in the {} account form.", account.getPaymentMethod().getDisplayString()); } return true; } private Optional> getReconciledTradeCurrencies(List currencyCodes, PaymentAccount account) { return getTradeCurrenciesInList(currencyCodes, account.getSupportedCurrencies()); } private boolean didReadSelectedTradeCurrencyField(JsonReader in, PaymentAccount account, String fieldName) { if (!fieldName.equals("selectedTradeCurrency")) return false; String fieldValue = nextStringOrNull(in); if (fieldValue != null && !fieldValue.isEmpty()) { Optional tradeCurrency = getTradeCurrency(fieldValue.toUpperCase()); if (tradeCurrency.isPresent()) { account.setSelectedTradeCurrency(tradeCurrency.get()); } else { // Log an error. We should not throw an exception here because the // gson library will not pass it up to the calling Haveno object exactly as // it would be defined here (causing confusion). log.error("{} is not a valid trade currency code.", fieldValue); } } return true; } private boolean didReadCommonField(JsonReader in, PaymentAccount account, String fieldName) throws IOException { switch (fieldName) { case "_COMMENTS_": case "paymentMethodId": // Skip over comments and paymentMethodId field, which // are already set on the PaymentAccount instance. in.skipValue(); return true; case "accountName": // Set the acct name using the value read from json. account.setAccountName(nextStringOrNull(in)); return true; case "salt": // Set the acct salt using the value read from json. String saltAsHex = nextStringOrNull(in); if (saltAsHex != null && !saltAsHex.trim().isEmpty()) { account.setSalt(decodeFromHex(saltAsHex)); } return true; default: return false; } } private boolean didReadCountryField(JsonReader in, PaymentAccount account, String fieldName) { if (!fieldName.equals("country")) return false; // read country code String countryCode = nextStringOrNull(in); // skip if not defined if (countryCode == null || countryCode.isEmpty()) return true; // parse country Optional country = findCountryByCode(countryCode); if (country.isPresent()) { if (account.isCountryBasedPaymentAccount()) { ((CountryBasedPaymentAccount) account).setCountry(country.get()); // TODO: applying single trade currency default can overwrite provided currencies, apply elsewhere? // TraditionalCurrency fiatCurrency = getCurrencyByCountryCode(checkNotNull(countryCode)); // account.setSingleTradeCurrency(fiatCurrency); } else if (account.hasPaymentMethodWithId(MONEY_GRAM_ID)) { ((MoneyGramAccount) account).setCountry(country.get()); } else if (account.hasPaymentMethodWithId(AMAZON_GIFT_CARD_ID)) { ((AmazonGiftCardAccount) account).setCountry(country.get()); } else { String errMsg = format("cannot set the country on a %s", paymentAccountType.getSimpleName()); log.error(capitalize(errMsg) + "."); throw new IllegalStateException("programmer error: " + errMsg); } return true; } else { throw new IllegalArgumentException( format("'%s' is an invalid country code.", countryCode)); } } private Class getPaymentAccountPayloadType() { try { Package pkg = PaymentAccountPayload.class.getPackage(); //noinspection unchecked return (Class) Class.forName(pkg.getName() + "." + paymentAccountType.getSimpleName() + "Payload"); } catch (Exception ex) { String errMsg = format("cannot get the payload class for %s", paymentAccountType.getSimpleName()); log.error(capitalize(errMsg) + ".", ex); throw new IllegalStateException("programmer error: " + errMsg); } } private PaymentAccount initNewPaymentAccount() { try { Constructor constructor = paymentAccountType.getDeclaredConstructor(); PaymentAccount paymentAccount = (PaymentAccount) constructor.newInstance(); paymentAccount.init(); return paymentAccount; } catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException ex) { String errMsg = format("cannot instantiate a new %s", paymentAccountType.getSimpleName()); log.error(capitalize(errMsg) + ".", ex); throw new IllegalStateException("programmer error: " + errMsg); } } } ================================================ FILE: core/src/main/java/haveno/core/payment/PaymentAccountUtil.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Country; import haveno.core.locale.TradeCurrency; import haveno.core.offer.Offer; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.user.User; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import static haveno.core.payment.payload.PaymentMethod.ACH_TRANSFER_ID; import static haveno.core.payment.payload.PaymentMethod.ADVANCED_CASH_ID; import static haveno.core.payment.payload.PaymentMethod.ALI_PAY_ID; import static haveno.core.payment.payload.PaymentMethod.AMAZON_GIFT_CARD_ID; import static haveno.core.payment.payload.PaymentMethod.AUSTRALIA_PAYID_ID; import static haveno.core.payment.payload.PaymentMethod.BIZUM_ID; import static haveno.core.payment.payload.PaymentMethod.BLOCK_CHAINS; import static haveno.core.payment.payload.PaymentMethod.BLOCK_CHAINS_INSTANT; import static haveno.core.payment.payload.PaymentMethod.CAPITUAL_ID; import static haveno.core.payment.payload.PaymentMethod.CASH_APP_ID; import static haveno.core.payment.payload.PaymentMethod.PAY_BY_MAIL_ID; import static haveno.core.payment.payload.PaymentMethod.CASH_DEPOSIT_ID; import static haveno.core.payment.payload.PaymentMethod.CELPAY_ID; import static haveno.core.payment.payload.PaymentMethod.CHASE_QUICK_PAY_ID; import static haveno.core.payment.payload.PaymentMethod.ZELLE_ID; import static haveno.core.payment.payload.PaymentMethod.DOMESTIC_WIRE_TRANSFER_ID; import static haveno.core.payment.payload.PaymentMethod.F2F_ID; import static haveno.core.payment.payload.PaymentMethod.FASTER_PAYMENTS_ID; import static haveno.core.payment.payload.PaymentMethod.HAL_CASH_ID; import static haveno.core.payment.payload.PaymentMethod.IMPS_ID; import static haveno.core.payment.payload.PaymentMethod.INTERAC_E_TRANSFER_ID; import static haveno.core.payment.payload.PaymentMethod.JAPAN_BANK_ID; import static haveno.core.payment.payload.PaymentMethod.MONESE_ID; import static haveno.core.payment.payload.PaymentMethod.MONEY_BEAM_ID; import static haveno.core.payment.payload.PaymentMethod.MONEY_GRAM_ID; import static haveno.core.payment.payload.PaymentMethod.NATIONAL_BANK_ID; import static haveno.core.payment.payload.PaymentMethod.NEFT_ID; import static haveno.core.payment.payload.PaymentMethod.NEQUI_ID; import static haveno.core.payment.payload.PaymentMethod.PAXUM_ID; import static haveno.core.payment.payload.PaymentMethod.PAYPAL_ID; import static haveno.core.payment.payload.PaymentMethod.PAYSERA_ID; import static haveno.core.payment.payload.PaymentMethod.PAYTM_ID; import static haveno.core.payment.payload.PaymentMethod.PERFECT_MONEY_ID; import static haveno.core.payment.payload.PaymentMethod.PIX_ID; import static haveno.core.payment.payload.PaymentMethod.POPMONEY_ID; import static haveno.core.payment.payload.PaymentMethod.PROMPT_PAY_ID; import static haveno.core.payment.payload.PaymentMethod.REVOLUT_ID; import static haveno.core.payment.payload.PaymentMethod.RTGS_ID; import static haveno.core.payment.payload.PaymentMethod.SAME_BANK_ID; import static haveno.core.payment.payload.PaymentMethod.SATISPAY_ID; import static haveno.core.payment.payload.PaymentMethod.SEPA_ID; import static haveno.core.payment.payload.PaymentMethod.SEPA_INSTANT_ID; import static haveno.core.payment.payload.PaymentMethod.SPECIFIC_BANKS_ID; import static haveno.core.payment.payload.PaymentMethod.STRIKE_ID; import static haveno.core.payment.payload.PaymentMethod.SWIFT_ID; import static haveno.core.payment.payload.PaymentMethod.SWISH_ID; import static haveno.core.payment.payload.PaymentMethod.TIKKIE_ID; import static haveno.core.payment.payload.PaymentMethod.TRANSFERWISE_ID; import static haveno.core.payment.payload.PaymentMethod.TRANSFERWISE_USD_ID; import static haveno.core.payment.payload.PaymentMethod.UPHOLD_ID; import static haveno.core.payment.payload.PaymentMethod.UPI_ID; import static haveno.core.payment.payload.PaymentMethod.US_POSTAL_MONEY_ORDER_ID; import static haveno.core.payment.payload.PaymentMethod.VENMO_ID; import static haveno.core.payment.payload.PaymentMethod.VERSE_ID; import static haveno.core.payment.payload.PaymentMethod.WECHAT_PAY_ID; import static haveno.core.payment.payload.PaymentMethod.WESTERN_UNION_ID; import static haveno.core.payment.payload.PaymentMethod.hasChargebackRisk; @Slf4j public class PaymentAccountUtil { public static boolean isAnyPaymentAccountValidForOffer(Offer offer, Collection paymentAccounts) { for (PaymentAccount paymentAccount : new ArrayList(paymentAccounts)) { if (isPaymentAccountValidForOffer(offer, paymentAccount)) return true; } return false; } public static ObservableList getPossiblePaymentAccounts(Offer offer, Set paymentAccounts, AccountAgeWitnessService accountAgeWitnessService) { ObservableList result = FXCollections.observableArrayList(); result.addAll(paymentAccounts.stream() .filter(paymentAccount -> isPaymentAccountValidForOffer(offer, paymentAccount)) .filter(paymentAccount -> isAmountValidForOffer(offer, paymentAccount, accountAgeWitnessService)) .collect(Collectors.toList())); return result; } // Return true if paymentAccount can take this offer public static boolean isAmountValidForOffer(Offer offer, PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService) { boolean hasChargebackRisk = hasChargebackRisk(offer.getPaymentMethod(), offer.getCounterCurrencyCode()); boolean hasValidAccountAgeWitness = accountAgeWitnessService.getMyTradeLimit(paymentAccount, offer.getCounterCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()) >= offer.getMinAmount().longValueExact(); return !hasChargebackRisk || hasValidAccountAgeWitness; } // TODO might be used to show more details if we get payment methods updates with diff. limits public static String getInfoForMismatchingPaymentMethodLimits(Offer offer, PaymentAccount paymentAccount) { // don't translate atm as it is not used so far in the UI just for logs return "Payment methods have different trade limits or trade periods.\n" + "Our local Payment method: " + paymentAccount.getPaymentMethod().toString() + "\n" + "Payment method from offer: " + offer.getPaymentMethod().toString(); } public static boolean isPaymentAccountValidForOffer(Offer offer, PaymentAccount paymentAccount) { return new ReceiptValidator(offer, paymentAccount).isValid(); } public static Optional getMostMaturePaymentAccountForOffer(Offer offer, Set paymentAccounts, AccountAgeWitnessService service) { PaymentAccounts accounts = new PaymentAccounts(paymentAccounts, service); return Optional.ofNullable(accounts.getOldestPaymentAccountForOffer(offer)); } @Nullable public static ArrayList getAcceptedCountryCodes(PaymentAccount paymentAccount) { ArrayList acceptedCountryCodes = null; if (paymentAccount instanceof SepaAccount) { acceptedCountryCodes = new ArrayList<>(((SepaAccount) paymentAccount).getAcceptedCountryCodes()); } else if (paymentAccount instanceof SepaInstantAccount) { acceptedCountryCodes = new ArrayList<>(((SepaInstantAccount) paymentAccount).getAcceptedCountryCodes()); } else if (paymentAccount instanceof CountryBasedPaymentAccount) { acceptedCountryCodes = new ArrayList<>(); Country country = ((CountryBasedPaymentAccount) paymentAccount).getCountry(); if (country != null) acceptedCountryCodes.add(country.code); } return acceptedCountryCodes; } public static List getTradeCurrencies(PaymentMethod paymentMethod) { switch (paymentMethod.getId()) { case ADVANCED_CASH_ID: return AdvancedCashAccount.SUPPORTED_CURRENCIES; case AMAZON_GIFT_CARD_ID: return AmazonGiftCardAccount.SUPPORTED_CURRENCIES; case CAPITUAL_ID: return CapitualAccount.SUPPORTED_CURRENCIES; case MONEY_GRAM_ID: return MoneyGramAccount.SUPPORTED_CURRENCIES; case PAXUM_ID: return PaxumAccount.SUPPORTED_CURRENCIES; case PAYSERA_ID: return PayseraAccount.SUPPORTED_CURRENCIES; case REVOLUT_ID: return RevolutAccount.SUPPORTED_CURRENCIES; case SWIFT_ID: return SwiftAccount.SUPPORTED_CURRENCIES; case TRANSFERWISE_ID: return TransferwiseAccount.SUPPORTED_CURRENCIES; case UPHOLD_ID: return UpholdAccount.SUPPORTED_CURRENCIES; case INTERAC_E_TRANSFER_ID: return InteracETransferAccount.SUPPORTED_CURRENCIES; case STRIKE_ID: return StrikeAccount.SUPPORTED_CURRENCIES; case TIKKIE_ID: return TikkieAccount.SUPPORTED_CURRENCIES; case ALI_PAY_ID: return AliPayAccount.SUPPORTED_CURRENCIES; case NEQUI_ID: return NequiAccount.SUPPORTED_CURRENCIES; case IMPS_ID: case NEFT_ID: case PAYTM_ID: case RTGS_ID: case UPI_ID: return IfscBasedAccount.SUPPORTED_CURRENCIES; case BIZUM_ID: return BizumAccount.SUPPORTED_CURRENCIES; case MONEY_BEAM_ID: return MoneyBeamAccount.SUPPORTED_CURRENCIES; case PIX_ID: return PixAccount.SUPPORTED_CURRENCIES; case SATISPAY_ID: return SatispayAccount.SUPPORTED_CURRENCIES; case CHASE_QUICK_PAY_ID: return ChaseQuickPayAccount.SUPPORTED_CURRENCIES; case US_POSTAL_MONEY_ORDER_ID: return USPostalMoneyOrderAccount.SUPPORTED_CURRENCIES; case VENMO_ID: return VenmoAccount.SUPPORTED_CURRENCIES; case PAYPAL_ID: return PayPalAccount.SUPPORTED_CURRENCIES; case JAPAN_BANK_ID: return JapanBankAccount.SUPPORTED_CURRENCIES; case WECHAT_PAY_ID: return WeChatPayAccount.SUPPORTED_CURRENCIES; case ZELLE_ID: return ZelleAccount.SUPPORTED_CURRENCIES; case AUSTRALIA_PAYID_ID: return AustraliaPayidAccount.SUPPORTED_CURRENCIES; case PERFECT_MONEY_ID: return PerfectMoneyAccount.SUPPORTED_CURRENCIES; case HAL_CASH_ID: return HalCashAccount.SUPPORTED_CURRENCIES; case SWISH_ID: return SwishAccount.SUPPORTED_CURRENCIES; case CASH_APP_ID: return CashAppAccount.SUPPORTED_CURRENCIES; case POPMONEY_ID: return PopmoneyAccount.SUPPORTED_CURRENCIES; case PROMPT_PAY_ID: return PromptPayAccount.SUPPORTED_CURRENCIES; case SEPA_ID: return SepaAccount.SUPPORTED_CURRENCIES; case SEPA_INSTANT_ID: return SepaInstantAccount.SUPPORTED_CURRENCIES; case PAY_BY_MAIL_ID: return PayByMailAccount.SUPPORTED_CURRENCIES; case F2F_ID: return F2FAccount.SUPPORTED_CURRENCIES; case NATIONAL_BANK_ID: return NationalBankAccount.SUPPORTED_CURRENCIES; case SAME_BANK_ID: return SameBankAccount.SUPPORTED_CURRENCIES; case SPECIFIC_BANKS_ID: return SpecificBanksAccount.SUPPORTED_CURRENCIES; case CASH_DEPOSIT_ID: return CashDepositAccount.SUPPORTED_CURRENCIES; case WESTERN_UNION_ID: return WesternUnionAccount.SUPPORTED_CURRENCIES; case FASTER_PAYMENTS_ID: return FasterPaymentsAccount.SUPPORTED_CURRENCIES; case DOMESTIC_WIRE_TRANSFER_ID: return DomesticWireTransferAccount.SUPPORTED_CURRENCIES; case ACH_TRANSFER_ID: return AchTransferAccount.SUPPORTED_CURRENCIES; case CELPAY_ID: return CelPayAccount.SUPPORTED_CURRENCIES; case MONESE_ID: return MoneseAccount.SUPPORTED_CURRENCIES; case TRANSFERWISE_USD_ID: return TransferwiseUsdAccount.SUPPORTED_CURRENCIES; case VERSE_ID: return VerseAccount.SUPPORTED_CURRENCIES; default: return Collections.emptyList(); } } public static boolean supportsCurrency(PaymentMethod paymentMethod, TradeCurrency selectedTradeCurrency) { return getTradeCurrencies(paymentMethod).stream() .anyMatch(tradeCurrency -> tradeCurrency.equals(selectedTradeCurrency)); } @Nullable public static List getAcceptedBanks(PaymentAccount paymentAccount) { List acceptedBanks = null; if (paymentAccount instanceof SpecificBanksAccount) { acceptedBanks = new ArrayList<>(((SpecificBanksAccount) paymentAccount).getAcceptedBanks()); } else if (paymentAccount instanceof SameBankAccount) { acceptedBanks = new ArrayList<>(); acceptedBanks.add(((SameBankAccount) paymentAccount).getBankId()); } return acceptedBanks; } @Nullable public static String getBankId(PaymentAccount paymentAccount) { return paymentAccount instanceof BankAccount ? ((BankAccount) paymentAccount).getBankId() : null; } @Nullable public static String getCountryCode(PaymentAccount paymentAccount) { // That is optional and set to null if not supported (Cryptos,...) if (paymentAccount instanceof CountryBasedPaymentAccount) { Country country = (((CountryBasedPaymentAccount) paymentAccount)).getCountry(); return country != null ? country.code : null; } return null; } public static boolean isCryptoCurrencyAccount(PaymentAccount paymentAccount) { return (paymentAccount != null && paymentAccount.getPaymentMethod().equals(BLOCK_CHAINS) || paymentAccount != null && paymentAccount.getPaymentMethod().equals(BLOCK_CHAINS_INSTANT)); } public static Optional findPaymentAccount(PaymentAccountPayload paymentAccountPayload, User user) { return user.getPaymentAccountsAsObservable().stream(). filter(e -> e.getPaymentAccountPayload().equals(paymentAccountPayload)) .findAny(); } } ================================================ FILE: core/src/main/java/haveno/core/payment/PaymentAccounts.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.account.witness.AccountAgeWitness; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.offer.Offer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Set; import java.util.function.BiPredicate; import java.util.stream.Collectors; class PaymentAccounts { private static final Logger log = LoggerFactory.getLogger(PaymentAccounts.class); private final Set accounts; private final AccountAgeWitnessService accountAgeWitnessService; private final BiPredicate validator; PaymentAccounts(Set accounts, AccountAgeWitnessService accountAgeWitnessService) { this(accounts, accountAgeWitnessService, PaymentAccountUtil::isPaymentAccountValidForOffer); } PaymentAccounts(Set accounts, AccountAgeWitnessService accountAgeWitnessService, BiPredicate validator) { this.accounts = accounts; this.accountAgeWitnessService = accountAgeWitnessService; this.validator = validator; } @Nullable PaymentAccount getOldestPaymentAccountForOffer(Offer offer) { List sortedValidAccounts = sortValidAccounts(offer); logAccounts(sortedValidAccounts); return firstOrNull(sortedValidAccounts); } private List sortValidAccounts(Offer offer) { Comparator comparator = this::compareByTradeLimit; return accounts.stream() .filter(account -> validator.test(offer, account)) .sorted(comparator.reversed()) .collect(Collectors.toList()); } @Nullable private PaymentAccount firstOrNull(List accounts) { return accounts.isEmpty() ? null : accounts.get(0); } private void logAccounts(List accounts) { if (log.isDebugEnabled()) { StringBuilder message = new StringBuilder("Valid accounts: \n"); for (PaymentAccount account : accounts) { String accountName = account.getAccountName(); String witnessHex = accountAgeWitnessService.getMyWitnessHashAsHex(account.getPaymentAccountPayload()); message.append("name = ") .append(accountName) .append("; witness hex = ") .append(witnessHex) .append(";\n"); } log.debug(message.toString()); } } // Accounts ranked by trade limit private int compareByTradeLimit(PaymentAccount left, PaymentAccount right) { // Mature accounts count as infinite sign age if (accountAgeWitnessService.myHasTradeLimitException(left)) { return !accountAgeWitnessService.myHasTradeLimitException(right) ? 1 : 0; } if (accountAgeWitnessService.myHasTradeLimitException(right)) { return -1; } AccountAgeWitness leftWitness = accountAgeWitnessService.getMyWitness(left.getPaymentAccountPayload()); AccountAgeWitness rightWitness = accountAgeWitnessService.getMyWitness(right.getPaymentAccountPayload()); Date now = new Date(); long leftSignAge = accountAgeWitnessService.getWitnessSignAge(leftWitness, now); long rightSignAge = accountAgeWitnessService.getWitnessSignAge(rightWitness, now); return Long.compare(leftSignAge, rightSignAge); } } ================================================ FILE: core/src/main/java/haveno/core/payment/PaysafeAccount.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.PaysafeAccountPayload; import lombok.EqualsAndHashCode; import org.jetbrains.annotations.NotNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class PaysafeAccount extends PaymentAccount { private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.EMAIL, PaymentAccountFormField.FieldId.TRADE_CURRENCIES, PaymentAccountFormField.FieldId.SALT ); // https://developer.paysafe.com/en/support/reference-information/codes/ public static final List SUPPORTED_CURRENCIES = List.of( new TraditionalCurrency("AED"), new TraditionalCurrency("ARS"), new TraditionalCurrency("AUD"), new TraditionalCurrency("BRL"), new TraditionalCurrency("CAD"), new TraditionalCurrency("CHF"), new TraditionalCurrency("CZK"), new TraditionalCurrency("DKK"), new TraditionalCurrency("EGP"), new TraditionalCurrency("EUR"), new TraditionalCurrency("GBP"), new TraditionalCurrency("GEL"), new TraditionalCurrency("HUF"), new TraditionalCurrency("ILS"), new TraditionalCurrency("INR"), new TraditionalCurrency("JPY"), new TraditionalCurrency("ISK"), new TraditionalCurrency("KWD"), new TraditionalCurrency("KRW"), new TraditionalCurrency("MXN"), new TraditionalCurrency("NOK"), new TraditionalCurrency("NZD"), new TraditionalCurrency("PEN"), new TraditionalCurrency("PHP"), new TraditionalCurrency("PLN"), new TraditionalCurrency("RON"), new TraditionalCurrency("RSD"), new TraditionalCurrency("RUB"), new TraditionalCurrency("SAR"), new TraditionalCurrency("SEK"), new TraditionalCurrency("TRY"), new TraditionalCurrency("USD"), new TraditionalCurrency("UYU") ); public PaysafeAccount() { super(PaymentMethod.PAYSAFE); } @Override protected PaymentAccountPayload createPayload() { return new PaysafeAccountPayload(paymentMethod.getId(), id); } @Override public @NotNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NotNull List getInputFieldIds() { return INPUT_FIELD_IDS; } public void setEmail(String accountId) { ((PaysafeAccountPayload) paymentAccountPayload).setEmail(accountId); } public String getEmail() { return ((PaysafeAccountPayload) paymentAccountPayload).getEmail(); } @Override protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) { var field = super.getEmptyFormField(fieldId); if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setValue(""); return field; } } ================================================ FILE: core/src/main/java/haveno/core/payment/PayseraAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.PayseraAccountPayload; import lombok.EqualsAndHashCode; import org.jetbrains.annotations.NotNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class PayseraAccount extends PaymentAccount { // https://github.com/bisq-network/growth/issues/233 public static final List SUPPORTED_CURRENCIES = List.of( new TraditionalCurrency("AUD"), new TraditionalCurrency("BYN"), new TraditionalCurrency("CAD"), new TraditionalCurrency("CHF"), new TraditionalCurrency("CNY"), new TraditionalCurrency("CZK"), new TraditionalCurrency("DKK"), new TraditionalCurrency("EUR"), new TraditionalCurrency("GBP"), new TraditionalCurrency("GEL"), new TraditionalCurrency("HKD"), new TraditionalCurrency("HUF"), new TraditionalCurrency("ILS"), new TraditionalCurrency("INR"), new TraditionalCurrency("JPY"), new TraditionalCurrency("KZT"), new TraditionalCurrency("MXN"), new TraditionalCurrency("NOK"), new TraditionalCurrency("NZD"), new TraditionalCurrency("PHP"), new TraditionalCurrency("PLN"), new TraditionalCurrency("RON"), new TraditionalCurrency("RSD"), new TraditionalCurrency("RUB"), new TraditionalCurrency("SEK"), new TraditionalCurrency("SGD"), new TraditionalCurrency("THB"), new TraditionalCurrency("TRY"), new TraditionalCurrency("USD"), new TraditionalCurrency("ZAR") ); public PayseraAccount() { super(PaymentMethod.PAYSERA); } @Override protected PaymentAccountPayload createPayload() { return new PayseraAccountPayload(paymentMethod.getId(), id); } @Override public @NotNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NotNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } public void setEmail(String accountId) { ((PayseraAccountPayload) paymentAccountPayload).setEmail(accountId); } public String getEmail() { return ((PayseraAccountPayload) paymentAccountPayload).getEmail(); } } ================================================ FILE: core/src/main/java/haveno/core/payment/PaytmAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.PaytmAccountPayload; import lombok.EqualsAndHashCode; @EqualsAndHashCode(callSuper = true) public final class PaytmAccount extends IfscBasedAccount { public PaytmAccount() { super(PaymentMethod.PAYTM); } @Override protected PaymentAccountPayload createPayload() { return new PaytmAccountPayload(paymentMethod.getId(), id); } public void setEmailOrMobileNr(String emailOrMobileNr) { ((PaytmAccountPayload) paymentAccountPayload).setEmailOrMobileNr(emailOrMobileNr); } public String getEmailOrMobileNr() { return ((PaytmAccountPayload) paymentAccountPayload).getEmailOrMobileNr(); } public String getMessageForBuyer() { return "payment.paytm.info.buyer"; } public String getMessageForSeller() { return "payment.paytm.info.seller"; } public String getMessageForAccountCreation() { return "payment.paytm.info.account"; } } ================================================ FILE: core/src/main/java/haveno/core/payment/PerfectMoneyAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.PerfectMoneyAccountPayload; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class PerfectMoneyAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD")); public PerfectMoneyAccount() { super(PaymentMethod.PERFECT_MONEY); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override protected PaymentAccountPayload createPayload() { return new PerfectMoneyAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } public void setAccountNr(String accountNr) { ((PerfectMoneyAccountPayload) paymentAccountPayload).setAccountNr(accountNr); } public String getAccountNr() { return ((PerfectMoneyAccountPayload) paymentAccountPayload).getAccountNr(); } } ================================================ FILE: core/src/main/java/haveno/core/payment/PixAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.PixAccountPayload; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; import javax.annotation.Nullable; import org.jetbrains.annotations.NotNull; @EqualsAndHashCode(callSuper = true) public final class PixAccount extends CountryBasedPaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("BRL")); public static final List SUPPORTED_COUNTRIES = CountryUtil.getCountries(List.of("BR")); private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.PIX_KEY, PaymentAccountFormField.FieldId.HOLDER_NAME, PaymentAccountFormField.FieldId.COUNTRY, PaymentAccountFormField.FieldId.SALT ); public PixAccount() { super(PaymentMethod.PIX); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override protected PaymentAccountPayload createPayload() { return new PixAccountPayload(paymentMethod.getId(), id); } public void setPixKey(String pixKey) { ((PixAccountPayload) paymentAccountPayload).setPixKey(pixKey); } public String getPixKey() { return ((PixAccountPayload) paymentAccountPayload).getPixKey(); } public void setHolderName(String value) { ((PixAccountPayload) paymentAccountPayload).setHolderName(value); } public String getHolderName() { return ((PixAccountPayload) paymentAccountPayload).getHolderName(); } @Override public String getMessageForBuyer() { return "payment.pix.info.buyer"; } @Override public String getMessageForSeller() { return "payment.pix.info.seller"; } @Override public String getMessageForAccountCreation() { return "payment.pix.info.account"; } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { return INPUT_FIELD_IDS; } @Override @Nullable public @NotNull List getSupportedCountries() { return SUPPORTED_COUNTRIES; } } ================================================ FILE: core/src/main/java/haveno/core/payment/PopmoneyAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.PopmoneyAccountPayload; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; //TODO missing support for selected trade currency @EqualsAndHashCode(callSuper = true) public final class PopmoneyAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD")); public PopmoneyAccount() { super(PaymentMethod.POPMONEY); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override protected PaymentAccountPayload createPayload() { return new PopmoneyAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } public void setAccountId(String accountId) { ((PopmoneyAccountPayload) paymentAccountPayload).setAccountId(accountId); } public String getAccountId() { return ((PopmoneyAccountPayload) paymentAccountPayload).getAccountId(); } public void setHolderName(String holderName) { ((PopmoneyAccountPayload) paymentAccountPayload).setHolderName(holderName); } public String getHolderName() { return ((PopmoneyAccountPayload) paymentAccountPayload).getHolderName(); } } ================================================ FILE: core/src/main/java/haveno/core/payment/PromptPayAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.PromptPayAccountPayload; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class PromptPayAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("THB")); public PromptPayAccount() { super(PaymentMethod.PROMPT_PAY); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override protected PaymentAccountPayload createPayload() { return new PromptPayAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } public void setPromptPayId(String promptPayId) { ((PromptPayAccountPayload) paymentAccountPayload).setPromptPayId(promptPayId); } public String getPromptPayId() { return ((PromptPayAccountPayload) paymentAccountPayload).getPromptPayId(); } } ================================================ FILE: core/src/main/java/haveno/core/payment/ReceiptPredicates.java ================================================ /* * 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 . */ package haveno.core.payment; import com.google.common.base.Preconditions; import haveno.core.locale.TradeCurrency; import haveno.core.offer.Offer; import haveno.core.payment.payload.PaymentMethod; import lombok.extern.slf4j.Slf4j; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @Slf4j class ReceiptPredicates { boolean isEqualPaymentMethods(Offer offer, PaymentAccount account) { // check if we have a matching payment method or if its a bank account payment method which is treated special PaymentMethod accountPaymentMethod = account.getPaymentMethod(); PaymentMethod offerPaymentMethod = offer.getPaymentMethod(); boolean arePaymentMethodsEqual = accountPaymentMethod.equals(offerPaymentMethod); if (log.isWarnEnabled()) { String accountPaymentMethodId = accountPaymentMethod.getId(); String offerPaymentMethodId = offerPaymentMethod.getId(); if (!arePaymentMethodsEqual && accountPaymentMethodId.equals(offerPaymentMethodId)) { log.warn(PaymentAccountUtil.getInfoForMismatchingPaymentMethodLimits(offer, account)); } } return arePaymentMethodsEqual; } boolean isOfferRequireSameOrSpecificBank(Offer offer, PaymentAccount account) { PaymentMethod paymentMethod = offer.getPaymentMethod(); boolean isSameOrSpecificBank = paymentMethod.equals(PaymentMethod.SAME_BANK) || paymentMethod.equals(PaymentMethod.SPECIFIC_BANKS); return (account instanceof BankAccount) && isSameOrSpecificBank; } boolean isMatchingBankId(Offer offer, PaymentAccount account) { final List acceptedBanksForOffer = offer.getAcceptedBankIds(); Preconditions.checkNotNull(acceptedBanksForOffer, "offer.getAcceptedBankIds() must not be null"); final String accountBankId = ((BankAccount) account).getBankId(); if (account instanceof SpecificBanksAccount) { // check if we have a matching bank boolean offerSideMatchesBank = (accountBankId != null) && acceptedBanksForOffer.contains(accountBankId); List acceptedBanksForAccount = ((SpecificBanksAccount) account).getAcceptedBanks(); boolean paymentAccountSideMatchesBank = acceptedBanksForAccount.contains(offer.getBankId()); return offerSideMatchesBank && paymentAccountSideMatchesBank; } else { // national or same bank return (accountBankId != null) && acceptedBanksForOffer.contains(accountBankId); } } boolean isMatchingCountryCodes(Offer offer, PaymentAccount account) { List acceptedCodes = Optional.ofNullable(offer.getAcceptedCountryCodes()) .orElse(Collections.emptyList()); String code = Optional.of(account) .map(CountryBasedPaymentAccount.class::cast) .map(CountryBasedPaymentAccount::getCountry) .map(country -> country.code) .orElse("undefined"); return acceptedCodes.contains(code); } boolean isMatchingCurrency(Offer offer, PaymentAccount account) { List currencies = account.getTradeCurrencies(); Set codes = currencies.stream() .map(TradeCurrency::getCode) .collect(Collectors.toSet()); return codes.contains(offer.getCounterCurrencyCode()); } boolean isMatchingSepaOffer(Offer offer, PaymentAccount account) { boolean isSepa = account instanceof SepaAccount; boolean isSepaInstant = account instanceof SepaInstantAccount; return offer.getPaymentMethod().equals(PaymentMethod.SEPA) && (isSepa || isSepaInstant); } boolean isMatchingSepaInstant(Offer offer, PaymentAccount account) { return offer.getPaymentMethod().equals(PaymentMethod.SEPA_INSTANT) && account instanceof SepaInstantAccount; } } ================================================ FILE: core/src/main/java/haveno/core/payment/ReceiptValidator.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.offer.Offer; import lombok.extern.slf4j.Slf4j; @Slf4j class ReceiptValidator { private final ReceiptPredicates predicates; private final PaymentAccount account; private final Offer offer; ReceiptValidator(Offer offer, PaymentAccount account) { this(offer, account, new ReceiptPredicates()); } ReceiptValidator(Offer offer, PaymentAccount account, ReceiptPredicates predicates) { this.offer = offer; this.account = account; this.predicates = predicates; } boolean isValid() { // We only support trades with the same currencies if (!predicates.isMatchingCurrency(offer, account)) { return false; } boolean isEqualPaymentMethods = predicates.isEqualPaymentMethods(offer, account); // All non-CountryBasedPaymentAccount need to have same payment methods if (!(account instanceof CountryBasedPaymentAccount)) { return isEqualPaymentMethods; } // We have a CountryBasedPaymentAccount, countries need to match if (!predicates.isMatchingCountryCodes(offer, account)) { return false; } // We have same country if (predicates.isMatchingSepaOffer(offer, account)) { // Sepa offer and taker account is Sepa or Sepa Instant return true; } if (predicates.isMatchingSepaInstant(offer, account)) { // Sepa Instant offer and taker account return true; } // Aside from Sepa or Sepa Instant, payment methods need to match if (!isEqualPaymentMethods) { return false; } if (predicates.isOfferRequireSameOrSpecificBank(offer, account)) { return predicates.isMatchingBankId(offer, account); } return true; } } ================================================ FILE: core/src/main/java/haveno/core/payment/RevolutAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.RevolutAccountPayload; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class RevolutAccount extends PaymentAccount { private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.USERNAME, PaymentAccountFormField.FieldId.TRADE_CURRENCIES, PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.SALT ); // https://www.revolut.com/help/getting-started/exchanging-currencies/what-fiat-currencies-are-supported-for-holding-and-exchange public static final List SUPPORTED_CURRENCIES = List.of( new TraditionalCurrency("AED"), new TraditionalCurrency("AUD"), new TraditionalCurrency("CAD"), new TraditionalCurrency("CHF"), new TraditionalCurrency("CZK"), new TraditionalCurrency("DKK"), new TraditionalCurrency("EUR"), new TraditionalCurrency("GBP"), new TraditionalCurrency("HKD"), new TraditionalCurrency("HUF"), new TraditionalCurrency("ILS"), new TraditionalCurrency("ISK"), new TraditionalCurrency("JPY"), new TraditionalCurrency("MAD"), new TraditionalCurrency("MXN"), new TraditionalCurrency("NOK"), new TraditionalCurrency("NZD"), new TraditionalCurrency("PLN"), new TraditionalCurrency("QAR"), new TraditionalCurrency("RON"), new TraditionalCurrency("RSD"), new TraditionalCurrency("RUB"), new TraditionalCurrency("SAR"), new TraditionalCurrency("SEK"), new TraditionalCurrency("SGD"), new TraditionalCurrency("THB"), new TraditionalCurrency("TRY"), new TraditionalCurrency("USD"), new TraditionalCurrency("ZAR") ); public RevolutAccount() { super(PaymentMethod.REVOLUT); } @Override protected PaymentAccountPayload createPayload() { return new RevolutAccountPayload(paymentMethod.getId(), id); } public void setUsername(String userame) { revolutAccountPayload().setUserName(userame); } public String getUsername() { return (revolutAccountPayload()).getUsername(); } public boolean usernameNotSet() { return (revolutAccountPayload()).usernameNotSet(); } private RevolutAccountPayload revolutAccountPayload() { return (RevolutAccountPayload) paymentAccountPayload; } @Override public void onAddToUser() { super.onAddToUser(); } @Override public @NonNull List getInputFieldIds() { return INPUT_FIELD_IDS; } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } } ================================================ FILE: core/src/main/java/haveno/core/payment/RtgsAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.RtgsAccountPayload; import lombok.EqualsAndHashCode; @EqualsAndHashCode(callSuper = true) public final class RtgsAccount extends IfscBasedAccount { public RtgsAccount() { super(PaymentMethod.RTGS); } @Override protected PaymentAccountPayload createPayload() { return new RtgsAccountPayload(paymentMethod.getId(), id); } public String getMessageForBuyer() { return "payment.rtgs.info.buyer"; } public String getMessageForSeller() { return "payment.rtgs.info.seller"; } public String getMessageForAccountCreation() { return "payment.rtgs.info.account"; } } ================================================ FILE: core/src/main/java/haveno/core/payment/SameBankAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.BankAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.SameBankAccountPayload; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class SameBankAccount extends CountryBasedPaymentAccount implements BankNameRestrictedBankAccount, SameCountryRestrictedBankAccount { public static final List SUPPORTED_CURRENCIES = CurrencyUtil.getAllFiatCurrencies(); public SameBankAccount() { super(PaymentMethod.SAME_BANK); } @Override protected PaymentAccountPayload createPayload() { return new SameBankAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } @Override public String getBankId() { return ((BankAccountPayload) paymentAccountPayload).getBankId(); } @Override public String getCountryCode() { return getCountry() != null ? getCountry().code : ""; } } ================================================ FILE: core/src/main/java/haveno/core/payment/SameCountryRestrictedBankAccount.java ================================================ /* * 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 . */ package haveno.core.payment; public interface SameCountryRestrictedBankAccount extends BankAccount { String getCountryCode(); } ================================================ FILE: core/src/main/java/haveno/core/payment/SatispayAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.SatispayAccountPayload; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class SatispayAccount extends CountryBasedPaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("EUR")); public SatispayAccount() { super(PaymentMethod.SATISPAY); } @Override protected PaymentAccountPayload createPayload() { return new SatispayAccountPayload(paymentMethod.getId(), id); } public void setHolderName(String accountId) { ((SatispayAccountPayload) paymentAccountPayload).setHolderName(accountId); } public String getHolderName() { return ((SatispayAccountPayload) paymentAccountPayload).getHolderName(); } public void setMobileNr(String accountId) { ((SatispayAccountPayload) paymentAccountPayload).setMobileNr(accountId); } public String getMobileNr() { return ((SatispayAccountPayload) paymentAccountPayload).getMobileNr(); } @Override public String getMessageForBuyer() { return "payment.satispay.info.buyer"; } @Override public String getMessageForSeller() { return "payment.satispay.info.seller"; } @Override public String getMessageForAccountCreation() { return "payment.satispay.info.account"; } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } } ================================================ FILE: core/src/main/java/haveno/core/payment/SepaAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountForm; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.SepaAccountPayload; import haveno.core.payment.validation.SepaIBANValidator; import lombok.EqualsAndHashCode; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class SepaAccount extends CountryBasedPaymentAccount implements BankAccount { protected static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.HOLDER_NAME, PaymentAccountFormField.FieldId.IBAN, PaymentAccountFormField.FieldId.BIC, PaymentAccountFormField.FieldId.COUNTRY, PaymentAccountFormField.FieldId.ACCEPTED_COUNTRY_CODES, PaymentAccountFormField.FieldId.SALT ); public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("EUR")); public SepaAccount() { super(PaymentMethod.SEPA); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override protected PaymentAccountPayload createPayload() { return new SepaAccountPayload(paymentMethod.getId(), id, CountryUtil.getAllSepaCountries()); } @Override public String getBankId() { return ((SepaAccountPayload) paymentAccountPayload).getBic(); } public void setHolderName(String holderName) { ((SepaAccountPayload) paymentAccountPayload).setHolderName(holderName); } public String getHolderName() { return ((SepaAccountPayload) paymentAccountPayload).getHolderName(); } public void setIban(String iban) { ((SepaAccountPayload) paymentAccountPayload).setIban(iban); } public String getIban() { return ((SepaAccountPayload) paymentAccountPayload).getIban(); } public void setBic(String bic) { ((SepaAccountPayload) paymentAccountPayload).setBic(bic); } public String getBic() { return ((SepaAccountPayload) paymentAccountPayload).getBic(); } public List getAcceptedCountryCodes() { return ((SepaAccountPayload) paymentAccountPayload).getAcceptedCountryCodes(); } public void setAcceptedCountryCodes(List acceptedCountryCodes) { ((SepaAccountPayload) paymentAccountPayload).setAcceptedCountryCodes(acceptedCountryCodes); } public void addAcceptedCountry(String countryCode) { ((SepaAccountPayload) paymentAccountPayload).addAcceptedCountry(countryCode); } public void removeAcceptedCountry(String countryCode) { ((SepaAccountPayload) paymentAccountPayload).removeAcceptedCountry(countryCode); } @Override public void onPersistChanges() { super.onPersistChanges(); ((SepaAccountPayload) paymentAccountPayload).onPersistChanges(); } @Override public void revertChanges() { super.revertChanges(); ((SepaAccountPayload) paymentAccountPayload).revertChanges(); } @Override public @NotNull List getInputFieldIds() { return INPUT_FIELD_IDS; } @Override public @NotNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override @Nullable public List getSupportedCountries() { return CountryUtil.getAllSepaCountries(); } @Override public void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) { switch (fieldId) { case IBAN: processValidationResult(new SepaIBANValidator().validate(value)); break; default: super.validateFormField(form, fieldId, value); } } @Override protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) { var field = super.getEmptyFormField(fieldId); switch (fieldId) { case ACCEPTED_COUNTRY_CODES: field.setSupportedSepaEuroCountries(CountryUtil.getAllSepaEuroCountries()); field.setSupportedSepaNonEuroCountries(CountryUtil.getAllSepaNonEuroCountries()); break; default: // no action } return field; } } ================================================ FILE: core/src/main/java/haveno/core/payment/SepaInstantAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountForm; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.SepaInstantAccountPayload; import haveno.core.payment.validation.SepaIBANValidator; import lombok.EqualsAndHashCode; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class SepaInstantAccount extends CountryBasedPaymentAccount implements BankAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("EUR")); public SepaInstantAccount() { super(PaymentMethod.SEPA_INSTANT); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override protected PaymentAccountPayload createPayload() { return new SepaInstantAccountPayload(paymentMethod.getId(), id, CountryUtil.getAllSepaInstantCountries()); } @Override public String getBankId() { return ((SepaInstantAccountPayload) paymentAccountPayload).getBic(); } public void setHolderName(String holderName) { ((SepaInstantAccountPayload) paymentAccountPayload).setHolderName(holderName); } public String getHolderName() { return ((SepaInstantAccountPayload) paymentAccountPayload).getHolderName(); } public void setIban(String iban) { ((SepaInstantAccountPayload) paymentAccountPayload).setIban(iban); } public String getIban() { return ((SepaInstantAccountPayload) paymentAccountPayload).getIban(); } public void setBic(String bic) { ((SepaInstantAccountPayload) paymentAccountPayload).setBic(bic); } public String getBic() { return ((SepaInstantAccountPayload) paymentAccountPayload).getBic(); } public List getAcceptedCountryCodes() { return ((SepaInstantAccountPayload) paymentAccountPayload).getAcceptedCountryCodes(); } public void addAcceptedCountry(String countryCode) { ((SepaInstantAccountPayload) paymentAccountPayload).addAcceptedCountry(countryCode); } public void removeAcceptedCountry(String countryCode) { ((SepaInstantAccountPayload) paymentAccountPayload).removeAcceptedCountry(countryCode); } @Override public void onPersistChanges() { super.onPersistChanges(); ((SepaInstantAccountPayload) paymentAccountPayload).onPersistChanges(); } @Override public void revertChanges() { super.revertChanges(); ((SepaInstantAccountPayload) paymentAccountPayload).revertChanges(); } @Override public @NotNull List getInputFieldIds() { return SepaAccount.INPUT_FIELD_IDS; } @Override public @NotNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override @Nullable public List getSupportedCountries() { return CountryUtil.getAllSepaCountries(); } @Override public void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) { switch (fieldId) { case IBAN: processValidationResult(new SepaIBANValidator().validate(value)); break; default: super.validateFormField(form, fieldId, value); } } @Override protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) { var field = super.getEmptyFormField(fieldId); switch (fieldId) { case ACCEPTED_COUNTRY_CODES: field.setSupportedSepaEuroCountries(CountryUtil.getAllSepaEuroCountries()); field.setSupportedSepaNonEuroCountries(CountryUtil.getAllSepaNonEuroCountries()); break; default: // no action } return field; } } ================================================ FILE: core/src/main/java/haveno/core/payment/SpecificBanksAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.SpecificBanksAccountPayload; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.ArrayList; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class SpecificBanksAccount extends CountryBasedPaymentAccount implements BankNameRestrictedBankAccount, SameCountryRestrictedBankAccount { public static final List SUPPORTED_CURRENCIES = CurrencyUtil.getAllFiatCurrencies(); public SpecificBanksAccount() { super(PaymentMethod.SPECIFIC_BANKS); } @Override protected PaymentAccountPayload createPayload() { return new SpecificBanksAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } // TODO change to List public ArrayList getAcceptedBanks() { return ((SpecificBanksAccountPayload) paymentAccountPayload).getAcceptedBanks(); } @Override public String getBankId() { return ((SpecificBanksAccountPayload) paymentAccountPayload).getBankId(); } @Override public String getCountryCode() { return getCountry() != null ? getCountry().code : ""; } } ================================================ FILE: core/src/main/java/haveno/core/payment/StrikeAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.StrikeAccountPayload; import lombok.EqualsAndHashCode; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class StrikeAccount extends CountryBasedPaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD")); public static final List SUPPORTED_COUNTRIES = CountryUtil.getCountries(List.of("US")); private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.COUNTRY, PaymentAccountFormField.FieldId.HOLDER_NAME, PaymentAccountFormField.FieldId.SALT ); public StrikeAccount() { super(PaymentMethod.STRIKE); // this payment method is currently restricted to United States/USD setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override protected PaymentAccountPayload createPayload() { return new StrikeAccountPayload(paymentMethod.getId(), id); } public void setHolderName(String accountId) { ((StrikeAccountPayload) paymentAccountPayload).setHolderName(accountId); } public String getHolderName() { return ((StrikeAccountPayload) paymentAccountPayload).getHolderName(); } @Override public String getMessageForBuyer() { return "payment.strike.info.buyer"; } @Override public String getMessageForSeller() { return "payment.strike.info.seller"; } @Override public String getMessageForAccountCreation() { return "payment.strike.info.account"; } @Override public @NotNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override @Nullable public @NotNull List getSupportedCountries() { return SUPPORTED_COUNTRIES; } @Override public @NotNull List getInputFieldIds() { return INPUT_FIELD_IDS; } } ================================================ FILE: core/src/main/java/haveno/core/payment/SwiftAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.SwiftAccountPayload; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.ArrayList; import java.util.List; import static java.util.Comparator.comparing; @EqualsAndHashCode(callSuper = true) public final class SwiftAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = new ArrayList<>(CurrencyUtil.getAllSortedFiatCurrencies(comparing(TradeCurrency::getCode))); private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.BANK_SWIFT_CODE, PaymentAccountFormField.FieldId.BANK_COUNTRY_CODE, PaymentAccountFormField.FieldId.BANK_NAME, PaymentAccountFormField.FieldId.BANK_BRANCH, PaymentAccountFormField.FieldId.BANK_ADDRESS, PaymentAccountFormField.FieldId.INTERMEDIARY_SWIFT_CODE, PaymentAccountFormField.FieldId.INTERMEDIARY_COUNTRY_CODE, PaymentAccountFormField.FieldId.INTERMEDIARY_NAME, PaymentAccountFormField.FieldId.INTERMEDIARY_BRANCH, PaymentAccountFormField.FieldId.INTERMEDIARY_ADDRESS, PaymentAccountFormField.FieldId.BENEFICIARY_NAME, PaymentAccountFormField.FieldId.BENEFICIARY_ACCOUNT_NR, PaymentAccountFormField.FieldId.BENEFICIARY_ADDRESS, PaymentAccountFormField.FieldId.BENEFICIARY_CITY, PaymentAccountFormField.FieldId.BENEFICIARY_PHONE, PaymentAccountFormField.FieldId.SPECIAL_INSTRUCTIONS, PaymentAccountFormField.FieldId.SALT ); public SwiftAccount() { super(PaymentMethod.SWIFT); tradeCurrencies.addAll(SUPPORTED_CURRENCIES); } @Override protected PaymentAccountPayload createPayload() { return new SwiftAccountPayload(paymentMethod.getId(), id); } public SwiftAccountPayload getPayload() { return ((SwiftAccountPayload) this.paymentAccountPayload); } @Override public String getMessageForBuyer() { return "payment.swift.info.buyer"; } @Override public String getMessageForSeller() { return "payment.swift.info.seller"; } @Override public String getMessageForAccountCreation() { return "payment.swift.info.account"; } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { return INPUT_FIELD_IDS; } } ================================================ FILE: core/src/main/java/haveno/core/payment/SwishAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountForm; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.SwishAccountPayload; import haveno.core.payment.validation.SwishValidator; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class SwishAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("SEK")); private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.MOBILE_NR, PaymentAccountFormField.FieldId.HOLDER_NAME, PaymentAccountFormField.FieldId.SALT ); public SwishAccount() { super(PaymentMethod.SWISH); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override protected PaymentAccountPayload createPayload() { return new SwishAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { return INPUT_FIELD_IDS; } public void setMobileNr(String mobileNr) { ((SwishAccountPayload) paymentAccountPayload).setMobileNr(mobileNr); } public String getMobileNr() { return ((SwishAccountPayload) paymentAccountPayload).getMobileNr(); } public void setHolderName(String holderName) { ((SwishAccountPayload) paymentAccountPayload).setHolderName(holderName); } public String getHolderName() { return ((SwishAccountPayload) paymentAccountPayload).getHolderName(); } @Override public void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) { switch (fieldId) { case MOBILE_NR: processValidationResult(new SwishValidator().validate(value)); break; default: super.validateFormField(form, fieldId, value); break; } } } ================================================ FILE: core/src/main/java/haveno/core/payment/TikkieAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.TikkieAccountPayload; import lombok.EqualsAndHashCode; import org.jetbrains.annotations.NotNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class TikkieAccount extends CountryBasedPaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("EUR")); public TikkieAccount() { super(PaymentMethod.TIKKIE); // this payment method is only for Netherlands/EUR setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override protected PaymentAccountPayload createPayload() { return new TikkieAccountPayload(paymentMethod.getId(), id); } public void setIban(String iban) { ((TikkieAccountPayload) paymentAccountPayload).setIban(iban); } public String getIban() { return ((TikkieAccountPayload) paymentAccountPayload).getIban(); } @Override public String getMessageForBuyer() { return "payment.tikkie.info.buyer"; } @Override public String getMessageForSeller() { return "payment.tikkie.info.seller"; } @Override public String getMessageForAccountCreation() { return "payment.tikkie.info.account"; } @Override public @NotNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NotNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } } ================================================ FILE: core/src/main/java/haveno/core/payment/TradeLimits.java ================================================ /* * 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 . */ package haveno.core.payment; import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.util.MathUtils; import haveno.core.trade.HavenoUtils; import java.math.BigInteger; import javax.annotation.Nullable; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton public class TradeLimits { private static final BigInteger MAX_TRADE_LIMIT = HavenoUtils.xmrToAtomicUnits(528); // max trade limit for lowest risk payment method. Others will get derived from that. private static final BigInteger MAX_TRADE_LIMIT_WITHOUT_BUYER_AS_TAKER_DEPOSIT = HavenoUtils.xmrToAtomicUnits(1.5); // max trade limit without deposit from buyer @Nullable @Getter private static TradeLimits INSTANCE; @Inject public TradeLimits() { INSTANCE = this; } public void onAllServicesInitialized() { // Do nothing but required to enforce class creation by guice. // The TradeLimits is used by PaymentMethod via the static INSTANCE and this would not trigger class creation by // guice. } /** * The default trade limits. * * @see haveno.core.payment.payload.PaymentMethod * @return the maximum trade limit */ public BigInteger getMaxTradeLimit() { return MAX_TRADE_LIMIT; } /** * The maximum trade limit without a buyer deposit. * * @return the maximum trade limit for a buyer without a deposit */ public BigInteger getMaxTradeLimitBuyerAsTakerWithoutDeposit() { return MAX_TRADE_LIMIT_WITHOUT_BUYER_AS_TAKER_DEPOSIT; } // We possibly rounded value for the first month gets multiplied by 4 to get the trade limit after the account // age witness is not considered anymore (> 2 months). /** * * @param maxLimit Satoshi value of max trade limit * @param riskFactor Risk factor to decrease trade limit for higher risk payment methods * @return Possibly adjusted trade limit to avoid that in first month trade limit get precision < 4. */ public long getRoundedRiskBasedTradeLimit(long maxLimit, long riskFactor) { return getFirstMonthRiskBasedTradeLimit(maxLimit, riskFactor) * 4; } // The first month we allow only 0.25% of the trade limit. We want to ensure that precision is <=4 otherwise we round. /** * * @param maxLimit Satoshi value of max trade limit * @param riskFactor Risk factor to decrease trade limit for higher risk payment methods * @return Rounded trade limit for first month to avoid BTC value with precision < 4. */ @VisibleForTesting long getFirstMonthRiskBasedTradeLimit(long maxLimit, long riskFactor) { // The first month we use 1/4 of the max limit. We multiply with riskFactor, so 1/ (4 * 8) is smallest limit in // first month of a maxTradeLimitHighRisk method long smallestLimit = maxLimit / (4 * riskFactor); // e.g. 100000000 / 32 = 3125000 // We want to avoid more than 4 decimal places (100000000 / 32 = 3125000 or 1 BTC / 32 = 0.03125 BTC). // We want rounding to 0.0313 BTC double decimalForm = MathUtils.scaleDownByPowerOf10((double) smallestLimit, 8); double rounded = MathUtils.roundDouble(decimalForm, 4); return MathUtils.roundDoubleToLong(MathUtils.scaleUpByPowerOf10(rounded, 8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/TransferwiseAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.TransferwiseAccountPayload; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class TransferwiseAccount extends PaymentAccount { private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.EMAIL, PaymentAccountFormField.FieldId.HOLDER_NAME, PaymentAccountFormField.FieldId.TRADE_CURRENCIES, PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.SALT ); // https://github.com/bisq-network/proposals/issues/243 public static final List SUPPORTED_CURRENCIES = List.of( new TraditionalCurrency("AED"), new TraditionalCurrency("ARS"), new TraditionalCurrency("AUD"), new TraditionalCurrency("BDT"), new TraditionalCurrency("BRL"), new TraditionalCurrency("BWP"), new TraditionalCurrency("CAD"), new TraditionalCurrency("CHF"), new TraditionalCurrency("CLP"), new TraditionalCurrency("CNY"), new TraditionalCurrency("COP"), new TraditionalCurrency("CRC"), new TraditionalCurrency("CZK"), new TraditionalCurrency("DKK"), new TraditionalCurrency("EGP"), new TraditionalCurrency("EUR"), new TraditionalCurrency("FJD"), new TraditionalCurrency("GBP"), new TraditionalCurrency("GEL"), new TraditionalCurrency("GHS"), new TraditionalCurrency("HKD"), new TraditionalCurrency("HUF"), new TraditionalCurrency("IDR"), new TraditionalCurrency("ILS"), new TraditionalCurrency("INR"), new TraditionalCurrency("JPY"), new TraditionalCurrency("KES"), new TraditionalCurrency("KRW"), new TraditionalCurrency("LKR"), new TraditionalCurrency("MAD"), new TraditionalCurrency("MXN"), new TraditionalCurrency("MYR"), new TraditionalCurrency("NOK"), new TraditionalCurrency("NPR"), new TraditionalCurrency("NZD"), new TraditionalCurrency("PEN"), new TraditionalCurrency("PHP"), new TraditionalCurrency("PKR"), new TraditionalCurrency("PLN"), new TraditionalCurrency("RON"), new TraditionalCurrency("RUB"), new TraditionalCurrency("SEK"), new TraditionalCurrency("SGD"), new TraditionalCurrency("THB"), new TraditionalCurrency("TRY"), new TraditionalCurrency("UAH"), new TraditionalCurrency("UGX"), new TraditionalCurrency("UYU"), new TraditionalCurrency("VND"), new TraditionalCurrency("XOF"), new TraditionalCurrency("ZAR"), new TraditionalCurrency("ZMW") ); public TransferwiseAccount() { super(PaymentMethod.TRANSFERWISE); } @Override protected PaymentAccountPayload createPayload() { return new TransferwiseAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getInputFieldIds() { return INPUT_FIELD_IDS; } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } public void setEmail(String accountId) { ((TransferwiseAccountPayload) paymentAccountPayload).setEmail(accountId); } public String getEmail() { return ((TransferwiseAccountPayload) paymentAccountPayload).getEmail(); } public void setHolderName(String value) { ((TransferwiseAccountPayload) paymentAccountPayload).setHolderName(value); } public String getHolderName() { return ((TransferwiseAccountPayload) paymentAccountPayload).getHolderName(); } @Override protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) { var field = super.getEmptyFormField(fieldId); if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setLabel("Currencies for receiving funds"); return field; } } ================================================ FILE: core/src/main/java/haveno/core/payment/TransferwiseUsdAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.TransferwiseUsdAccountPayload; import lombok.EqualsAndHashCode; import org.jetbrains.annotations.NotNull; import java.util.List; import javax.annotation.Nullable; @EqualsAndHashCode(callSuper = true) public final class TransferwiseUsdAccount extends CountryBasedPaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD")); public static final List SUPPORTED_COUNTRIES = CountryUtil.getCountries(List.of("US")); private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.EMAIL, PaymentAccountFormField.FieldId.HOLDER_NAME, PaymentAccountFormField.FieldId.HOLDER_ADDRESS, PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.COUNTRY, PaymentAccountFormField.FieldId.SALT ); public TransferwiseUsdAccount() { super(PaymentMethod.TRANSFERWISE_USD); // this payment method is currently restricted to United States/USD setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override protected PaymentAccountPayload createPayload() { return new TransferwiseUsdAccountPayload(paymentMethod.getId(), id); } public void setEmail(String email) { ((TransferwiseUsdAccountPayload) paymentAccountPayload).setEmail(email); } public String getEmail() { return ((TransferwiseUsdAccountPayload) paymentAccountPayload).getEmail(); } public void setHolderName(String accountId) { ((TransferwiseUsdAccountPayload) paymentAccountPayload).setHolderName(accountId); } public String getHolderName() { return ((TransferwiseUsdAccountPayload) paymentAccountPayload).getHolderName(); } public void setBeneficiaryAddress(String address) { ((TransferwiseUsdAccountPayload) paymentAccountPayload).setHolderAddress(address); } public String getBeneficiaryAddress() { return ((TransferwiseUsdAccountPayload) paymentAccountPayload).getHolderAddress(); } @Override public String getMessageForBuyer() { return "payment.transferwiseUsd.info.buyer"; } @Override public String getMessageForSeller() { return "payment.transferwiseUsd.info.seller"; } @Override public String getMessageForAccountCreation() { return "payment.transferwiseUsd.info.account"; } @Override public @NotNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NotNull List getInputFieldIds() { return INPUT_FIELD_IDS; } @Override protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) { var field = super.getEmptyFormField(fieldId); if (field.getId() == PaymentAccountFormField.FieldId.HOLDER_ADDRESS) field.setLabel(field.getLabel() + " " + Res.get("payment.transferwiseUsd.address")); return field; } @Override @Nullable public @NotNull List getSupportedCountries() { return SUPPORTED_COUNTRIES; } } ================================================ FILE: core/src/main/java/haveno/core/payment/USPostalMoneyOrderAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.USPostalMoneyOrderAccountPayload; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class USPostalMoneyOrderAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD")); private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.HOLDER_NAME, PaymentAccountFormField.FieldId.POSTAL_ADDRESS, PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.SALT ); public USPostalMoneyOrderAccount() { super(PaymentMethod.US_POSTAL_MONEY_ORDER); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override protected PaymentAccountPayload createPayload() { return new USPostalMoneyOrderAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { return INPUT_FIELD_IDS; } public void setPostalAddress(String postalAddress) { ((USPostalMoneyOrderAccountPayload) paymentAccountPayload).setPostalAddress(postalAddress); } public String getPostalAddress() { return ((USPostalMoneyOrderAccountPayload) paymentAccountPayload).getPostalAddress(); } public void setHolderName(String holderName) { ((USPostalMoneyOrderAccountPayload) paymentAccountPayload).setHolderName(holderName); } public String getHolderName() { return ((USPostalMoneyOrderAccountPayload) paymentAccountPayload).getHolderName(); } } ================================================ FILE: core/src/main/java/haveno/core/payment/UpholdAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.UpholdAccountPayload; import lombok.EqualsAndHashCode; import org.jetbrains.annotations.NotNull; import java.util.List; //TODO missing support for selected trade currency @EqualsAndHashCode(callSuper = true) public final class UpholdAccount extends PaymentAccount { private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.ACCOUNT_OWNER, PaymentAccountFormField.FieldId.ACCOUNT_ID, PaymentAccountFormField.FieldId.TRADE_CURRENCIES, PaymentAccountFormField.FieldId.SALT ); // https://support.uphold.com/hc/en-us/articles/202473803-Supported-currencies public static final List SUPPORTED_CURRENCIES = List.of( new TraditionalCurrency("AED"), new TraditionalCurrency("ARS"), new TraditionalCurrency("AUD"), new TraditionalCurrency("BRL"), new TraditionalCurrency("CAD"), new TraditionalCurrency("CHF"), new TraditionalCurrency("CNY"), new TraditionalCurrency("DKK"), new TraditionalCurrency("EUR"), new TraditionalCurrency("GBP"), new TraditionalCurrency("HKD"), new TraditionalCurrency("ILS"), new TraditionalCurrency("INR"), new TraditionalCurrency("JPY"), new TraditionalCurrency("KES"), new TraditionalCurrency("MXN"), new TraditionalCurrency("NOK"), new TraditionalCurrency("NZD"), new TraditionalCurrency("PHP"), new TraditionalCurrency("PLN"), new TraditionalCurrency("SEK"), new TraditionalCurrency("SGD"), new TraditionalCurrency("USD") ); public UpholdAccount() { super(PaymentMethod.UPHOLD); } @Override protected PaymentAccountPayload createPayload() { return new UpholdAccountPayload(paymentMethod.getId(), id); } @Override public @NotNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NotNull List getInputFieldIds() { return INPUT_FIELD_IDS; } public void setAccountId(String accountId) { ((UpholdAccountPayload) paymentAccountPayload).setAccountId(accountId); } public String getAccountId() { return ((UpholdAccountPayload) paymentAccountPayload).getAccountId(); } public String getAccountOwner() { return ((UpholdAccountPayload) paymentAccountPayload).getAccountOwner(); } public void setAccountOwner(String accountOwner) { if (accountOwner == null) { accountOwner = ""; } ((UpholdAccountPayload) paymentAccountPayload).setAccountOwner(accountOwner); } } ================================================ FILE: core/src/main/java/haveno/core/payment/UpiAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.UpiAccountPayload; import lombok.EqualsAndHashCode; @EqualsAndHashCode(callSuper = true) public final class UpiAccount extends IfscBasedAccount { public UpiAccount() { super(PaymentMethod.UPI); } @Override protected PaymentAccountPayload createPayload() { return new UpiAccountPayload(paymentMethod.getId(), id); } public void setVirtualPaymentAddress(String virtualPaymentAddress) { ((UpiAccountPayload) paymentAccountPayload).setVirtualPaymentAddress(virtualPaymentAddress); } public String getVirtualPaymentAddress() { return ((UpiAccountPayload) paymentAccountPayload).getVirtualPaymentAddress(); } public String getMessageForBuyer() { return "payment.upi.info.buyer"; } public String getMessageForSeller() { return "payment.upi.info.seller"; } public String getMessageForAccountCreation() { return "payment.upi.info.account"; } } ================================================ FILE: core/src/main/java/haveno/core/payment/VenmoAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.VenmoAccountPayload; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class VenmoAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD")); private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.EMAIL_OR_MOBILE_NR_OR_USERNAME, PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.SALT); public VenmoAccount() { super(PaymentMethod.VENMO); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override protected PaymentAccountPayload createPayload() { return new VenmoAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { return INPUT_FIELD_IDS; } public void setNameOrUsernameOrEmailOrMobileNr(String usernameOrEmailOrMobileNr) { ((VenmoAccountPayload) paymentAccountPayload) .setEmailOrMobileNrOrUsername(usernameOrEmailOrMobileNr); } public String getNameOrUsernameOrEmailOrMobileNr() { return ((VenmoAccountPayload) paymentAccountPayload).getEmailOrMobileNrOrUsername(); } } ================================================ FILE: core/src/main/java/haveno/core/payment/VerseAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.VerseAccountPayload; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class VerseAccount extends PaymentAccount { // https://github.com/bisq-network/growth/issues/223 public static final List SUPPORTED_CURRENCIES = List.of( new TraditionalCurrency("DKK"), new TraditionalCurrency("EUR"), new TraditionalCurrency("HUF"), new TraditionalCurrency("PLN"), new TraditionalCurrency("SEK") ); public VerseAccount() { super(PaymentMethod.VERSE); } @Override protected PaymentAccountPayload createPayload() { return new VerseAccountPayload(paymentMethod.getId(), id); } public void setHolderName(String accountId) { ((VerseAccountPayload) paymentAccountPayload).setHolderName(accountId); } public String getHolderName() { return ((VerseAccountPayload) paymentAccountPayload).getHolderName(); } @Override public String getMessageForBuyer() { return "payment.verse.info.buyer"; } @Override public String getMessageForSeller() { return "payment.verse.info.seller"; } @Override public String getMessageForAccountCreation() { return "payment.verse.info.account"; } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } } ================================================ FILE: core/src/main/java/haveno/core/payment/WeChatPayAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.WeChatPayAccountPayload; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class WeChatPayAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of( new TraditionalCurrency("CNY"), new TraditionalCurrency("USD"), new TraditionalCurrency("EUR"), new TraditionalCurrency("GBP") ); private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.ACCOUNT_NR, PaymentAccountFormField.FieldId.TRADE_CURRENCIES, PaymentAccountFormField.FieldId.SALT ); public WeChatPayAccount() { super(PaymentMethod.WECHAT_PAY); } @Override protected PaymentAccountPayload createPayload() { return new WeChatPayAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { return INPUT_FIELD_IDS; } public void setAccountNr(String accountNr) { ((WeChatPayAccountPayload) paymentAccountPayload).setAccountNr(accountNr); } public String getAccountNr() { return ((WeChatPayAccountPayload) paymentAccountPayload).getAccountNr(); } // TODO: WeChatPayValidator is not used (see WeChatPayForm) // @Override // public void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) { // switch (fieldId) { // case ACCOUNT_NR: // processValidationResult(new WeChatPayValidator().validate(value)); // break; // default: // super.validateFormField(form, fieldId, value); // } // } } ================================================ FILE: core/src/main/java/haveno/core/payment/WesternUnionAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.WesternUnionAccountPayload; import lombok.NonNull; import java.util.List; public final class WesternUnionAccount extends CountryBasedPaymentAccount { public static final List SUPPORTED_CURRENCIES = CurrencyUtil.getAllFiatCurrencies(); public WesternUnionAccount() { super(PaymentMethod.WESTERN_UNION); } @Override protected PaymentAccountPayload createPayload() { return new WesternUnionAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { throw new RuntimeException("Not implemented"); } public String getEmail() { return ((WesternUnionAccountPayload) paymentAccountPayload).getEmail(); } public void setEmail(String email) { ((WesternUnionAccountPayload) paymentAccountPayload).setEmail(email); } public String getFullName() { return ((WesternUnionAccountPayload) paymentAccountPayload).getHolderName(); } public void setFullName(String email) { ((WesternUnionAccountPayload) paymentAccountPayload).setHolderName(email); } public String getCity() { return ((WesternUnionAccountPayload) paymentAccountPayload).getCity(); } public void setCity(String email) { ((WesternUnionAccountPayload) paymentAccountPayload).setCity(email); } public String getState() { return ((WesternUnionAccountPayload) paymentAccountPayload).getState(); } public void setState(String email) { ((WesternUnionAccountPayload) paymentAccountPayload).setState(email); } } ================================================ FILE: core/src/main/java/haveno/core/payment/ZelleAccount.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.ZelleAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import lombok.EqualsAndHashCode; import lombok.NonNull; import java.util.List; @EqualsAndHashCode(callSuper = true) public final class ZelleAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD")); public ZelleAccount() { super(PaymentMethod.ZELLE); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } private static final List INPUT_FIELD_IDS = List.of( PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.HOLDER_NAME, PaymentAccountFormField.FieldId.EMAIL_OR_MOBILE_NR, PaymentAccountFormField.FieldId.SALT ); @Override protected PaymentAccountPayload createPayload() { return new ZelleAccountPayload(paymentMethod.getId(), id); } @Override public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } @Override public @NonNull List getInputFieldIds() { return INPUT_FIELD_IDS; } public void setEmailOrMobileNr(String mobileNr) { ((ZelleAccountPayload) paymentAccountPayload).setEmailOrMobileNr(mobileNr); } public String getEmailOrMobileNr() { return ((ZelleAccountPayload) paymentAccountPayload).getEmailOrMobileNr(); } public void setHolderName(String holderName) { ((ZelleAccountPayload) paymentAccountPayload).setHolderName(holderName); } public String getHolderName() { return ((ZelleAccountPayload) paymentAccountPayload).getHolderName(); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/AchTransferAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Getter @Setter @Slf4j public final class AchTransferAccountPayload extends BankAccountPayload { private String holderAddress = ""; public AchTransferAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private AchTransferAccountPayload(String paymentMethodName, String id, String countryCode, List acceptedCountryCodes, String holderName, String bankName, String branchId, String accountNr, String accountType, String holderAddress, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethodName, id, countryCode, acceptedCountryCodes, holderName, bankName, branchId, accountNr, accountType, null, // holderTaxId not used null, // bankId not used null, // nationalAccountId not used maxTradePeriod, excludeFromJsonDataMap); this.holderAddress = holderAddress; } @Override public Message toProtoMessage() { protobuf.AchTransferAccountPayload.Builder builder = protobuf.AchTransferAccountPayload.newBuilder() .setHolderAddress(holderAddress); protobuf.BankAccountPayload.Builder bankAccountPayloadBuilder = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .getBankAccountPayloadBuilder() .setAchTransferAccountPayload(builder); protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayloadBuilder = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setBankAccountPayload(bankAccountPayloadBuilder); return getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayloadBuilder) .build(); } public static AchTransferAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); protobuf.BankAccountPayload bankAccountPayloadPB = countryBasedPaymentAccountPayload.getBankAccountPayload(); protobuf.AchTransferAccountPayload accountPayloadPB = bankAccountPayloadPB.getAchTransferAccountPayload(); return new AchTransferAccountPayload(proto.getPaymentMethodId(), proto.getId(), countryBasedPaymentAccountPayload.getCountryCode(), new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), bankAccountPayloadPB.getHolderName(), bankAccountPayloadPB.getBankName().isEmpty() ? null : bankAccountPayloadPB.getBankName(), bankAccountPayloadPB.getBranchId().isEmpty() ? null : bankAccountPayloadPB.getBranchId(), bankAccountPayloadPB.getAccountNr().isEmpty() ? null : bankAccountPayloadPB.getAccountNr(), bankAccountPayloadPB.getAccountType().isEmpty() ? null : bankAccountPayloadPB.getAccountType(), accountPayloadPB.getHolderAddress().isEmpty() ? null : accountPayloadPB.getHolderAddress(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + getPaymentDetailsForTradePopup().replace("\n", ", "); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/AdvancedCashAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class AdvancedCashAccountPayload extends PaymentAccountPayload { private String accountNr = ""; public AdvancedCashAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private AdvancedCashAccountPayload(String paymentMethod, String id, String accountNr, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.accountNr = accountNr; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setAdvancedCashAccountPayload(protobuf.AdvancedCashAccountPayload.newBuilder() .setAccountNr(accountNr)) .build(); } public static AdvancedCashAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new AdvancedCashAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getAdvancedCashAccountPayload().getAccountNr(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.wallet") + " " + accountNr; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(accountNr.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/AliPayAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @Getter @Setter @ToString public final class AliPayAccountPayload extends PaymentAccountPayload { private String accountNr = ""; public AliPayAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private AliPayAccountPayload(String paymentMethod, String id, String accountNr, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.accountNr = accountNr; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setAliPayAccountPayload(protobuf.AliPayAccountPayload.newBuilder() .setAccountNr(accountNr)) .build(); } public static AliPayAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new AliPayAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getAliPayAccountPayload().getAccountNr(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.no") + " " + accountNr; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(accountNr.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/AmazonGiftCardAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.common.util.JsonExclude; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public class AmazonGiftCardAccountPayload extends PaymentAccountPayload { private String emailOrMobileNr; // For backward compatibility we need to exclude the new field for the contract json. // We can remove that after a while when risk that users with pre 1.5.5 version is very low. @JsonExclude private String countryCode = ""; public AmazonGiftCardAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private AmazonGiftCardAccountPayload(String paymentMethodName, String id, String emailOrMobileNr, String countryCode, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethodName, id, maxTradePeriod, excludeFromJsonDataMap); this.emailOrMobileNr = emailOrMobileNr; this.countryCode = countryCode; } @Override public Message toProtoMessage() { protobuf.AmazonGiftCardAccountPayload.Builder builder = protobuf.AmazonGiftCardAccountPayload.newBuilder() .setCountryCode(countryCode) .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(), amazonGiftCardAccountPayload.getCountryCode(), 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)); } public boolean countryNotSet() { return countryCode.isEmpty(); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/AssetAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public abstract class AssetAccountPayload extends PaymentAccountPayload { protected String address = ""; protected AssetAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// protected AssetAccountPayload(String paymentMethod, String id, String address, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.address = address; } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.getWithCol("payment.crypto.receiver.address") + " " + address; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(address.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/AustraliaPayidAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.common.util.CollectionUtils; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class AustraliaPayidAccountPayload extends PaymentAccountPayload { private String payid = ""; private String bankAccountName = ""; private String extraInfo = ""; public AustraliaPayidAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private AustraliaPayidAccountPayload(String paymentMethod, String id, String payid, String bankAccountName, String extraInfo, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.payid = payid; this.bankAccountName = bankAccountName; this.extraInfo = extraInfo; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setAustraliaPayidPayload( protobuf.AustraliaPayidPayload.newBuilder() .setPayid(payid) .setBankAccountName(bankAccountName) .setExtraInfo(extraInfo) ).build(); } public static AustraliaPayidAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.AustraliaPayidPayload AustraliaPayidPayload = proto.getAustraliaPayidPayload(); return new AustraliaPayidAccountPayload(proto.getPaymentMethodId(), proto.getId(), AustraliaPayidPayload.getPayid(), AustraliaPayidPayload.getBankAccountName(), AustraliaPayidPayload.getExtraInfo(), proto.getMaxTradePeriod(), CollectionUtils.isEmpty(proto.getExcludeFromJsonDataMap()) ? null : new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + getPaymentDetailsForTradePopup().replace("\n", ", "); } @Override public String getPaymentDetailsForTradePopup() { return Res.get("payment.australia.payid") + ": " + payid + "\n" + Res.get("payment.account.owner.fullname") + ": " + bankAccountName + "\n" + Res.get("payment.shared.extraInfo") + ": " + extraInfo; } @Override public byte[] getAgeWitnessInputData() { String all = this.payid + this.bankAccountName; return super.getAgeWitnessInputData(all.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/BankAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import haveno.core.locale.BankUtil; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; import java.util.Optional; @EqualsAndHashCode(callSuper = true) @Setter @Getter @ToString @Slf4j public abstract class BankAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName { protected String holderName = ""; protected String bankName = ""; protected String branchId = ""; protected String accountNr = ""; @Nullable protected String accountType; @Nullable protected String holderTaxId; protected String bankId = ""; @Nullable protected String nationalAccountId; protected BankAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// protected BankAccountPayload(String paymentMethodName, String id, String countryCode, List acceptedCountryCodes, String holderName, @Nullable String bankName, @Nullable String branchId, @Nullable String accountNr, @Nullable String accountType, @Nullable String holderTaxId, @Nullable String bankId, @Nullable String nationalAccountId, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethodName, id, countryCode, acceptedCountryCodes, maxTradePeriod, excludeFromJsonDataMap); this.holderName = holderName; this.bankName = bankName; this.branchId = branchId; this.accountNr = accountNr; this.accountType = accountType; this.holderTaxId = holderTaxId; this.bankId = bankId; this.nationalAccountId = nationalAccountId; } @Override public protobuf.PaymentAccountPayload.Builder getPaymentAccountPayloadBuilder() { protobuf.BankAccountPayload.Builder builder = protobuf.BankAccountPayload.newBuilder() .setHolderName(holderName); Optional.ofNullable(holderTaxId).ifPresent(builder::setHolderTaxId); Optional.ofNullable(bankName).ifPresent(builder::setBankName); Optional.ofNullable(branchId).ifPresent(builder::setBranchId); Optional.ofNullable(accountNr).ifPresent(builder::setAccountNr); Optional.ofNullable(accountType).ifPresent(builder::setAccountType); Optional.ofNullable(bankId).ifPresent(builder::setBankId); Optional.ofNullable(nationalAccountId).ifPresent(builder::setNationalAccountId); final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayloadBuilder = super.getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setBankAccountPayload(builder); return super.getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayloadBuilder); } @Override public String getPaymentDetails() { return "Bank account transfer - " + getPaymentDetailsForTradePopup().replace("\n", ", "); } @Override public String getPaymentDetailsForTradePopup() { String bankName = BankUtil.isBankNameRequired(countryCode) ? BankUtil.getBankNameLabel(countryCode) + ": " + this.bankName + "\n" : ""; String bankId = BankUtil.isBankIdRequired(countryCode) ? BankUtil.getBankIdLabel(countryCode) + ": " + this.bankId + "\n" : ""; String branchId = BankUtil.isBranchIdRequired(countryCode) ? BankUtil.getBranchIdLabel(countryCode) + ": " + this.branchId + "\n" : ""; String nationalAccountId = BankUtil.isNationalAccountIdRequired(countryCode) ? BankUtil.getNationalAccountIdLabel(countryCode) + ": " + this.nationalAccountId + "\n" : ""; String accountNr = BankUtil.isAccountNrRequired(countryCode) ? BankUtil.getAccountNrLabel(countryCode) + ": " + this.accountNr + "\n" : ""; String accountType = BankUtil.isAccountTypeRequired(countryCode) ? BankUtil.getAccountTypeLabel(countryCode) + ": " + this.accountType + "\n" : ""; String holderTaxIdString = BankUtil.isHolderIdRequired(countryCode) ? (BankUtil.getHolderIdLabel(countryCode) + ": " + holderTaxId + "\n") : ""; return Res.getWithCol("payment.account.owner.fullname") + " " + holderName + "\n" + bankName + bankId + branchId + nationalAccountId + accountNr + accountType + holderTaxIdString + Res.getWithCol("payment.bank.country") + " " + CountryUtil.getNameByCode(countryCode); } protected String getHolderIdLabel() { return BankUtil.getHolderIdLabel(countryCode); } @Nullable public String getBankId() { return BankUtil.isBankIdRequired(countryCode) ? bankId : bankName; } @Override public byte[] getAgeWitnessInputData() { String bankName = BankUtil.isBankNameRequired(countryCode) ? this.bankName : ""; String bankId = BankUtil.isBankIdRequired(countryCode) ? this.bankId : ""; String branchId = BankUtil.isBranchIdRequired(countryCode) ? this.branchId : ""; String accountNr = BankUtil.isAccountNrRequired(countryCode) ? this.accountNr : ""; String accountType = BankUtil.isAccountTypeRequired(countryCode) ? this.accountType : ""; String holderTaxIdString = BankUtil.isHolderIdRequired(countryCode) ? (BankUtil.getHolderIdLabel(countryCode) + " " + holderTaxId + "\n") : ""; String nationalAccountId = BankUtil.isNationalAccountIdRequired(countryCode) ? this.nationalAccountId : ""; // We don't add holderName because we don't want to break age validation if the user recreates an account with // slight changes in holder name (e.g. add or remove middle name) String all = bankName + bankId + branchId + accountNr + accountType + holderTaxIdString + nationalAccountId; return super.getAgeWitnessInputData(all.getBytes(StandardCharsets.UTF_8)); } @Override public String getOwnerId() { return holderName; } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/BizumAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class BizumAccountPayload extends CountryBasedPaymentAccountPayload { private String mobileNr = ""; public BizumAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } private BizumAccountPayload(String paymentMethod, String id, String countryCode, List acceptedCountryCodes, String mobileNr, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, countryCode, acceptedCountryCodes, maxTradePeriod, excludeFromJsonDataMap); this.mobileNr = mobileNr; } @Override public Message toProtoMessage() { protobuf.BizumAccountPayload.Builder builder = protobuf.BizumAccountPayload.newBuilder() .setMobileNr(mobileNr); final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setBizumAccountPayload(builder); return getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) .build(); } public static BizumAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); protobuf.BizumAccountPayload paytmAccountPayloadPB = countryBasedPaymentAccountPayload.getBizumAccountPayload(); return new BizumAccountPayload(proto.getPaymentMethodId(), proto.getId(), countryBasedPaymentAccountPayload.getCountryCode(), new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), paytmAccountPayloadPB.getMobileNr(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.mobile") + " " + mobileNr; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(mobileNr.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/CapitualAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class CapitualAccountPayload extends PaymentAccountPayload { private String accountNr = ""; public CapitualAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private CapitualAccountPayload(String paymentMethod, String id, String accountNr, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.accountNr = accountNr; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setCapitualAccountPayload(protobuf.CapitualAccountPayload.newBuilder() .setAccountNr(accountNr)) .build(); } public static CapitualAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new CapitualAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getCapitualAccountPayload().getAccountNr(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.capitual.cap") + " " + accountNr; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(accountNr.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/CashAppAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class CashAppAccountPayload extends PaymentAccountPayload { private String emailOrMobileNrOrCashtag = ""; private String extraInfo = ""; public CashAppAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private CashAppAccountPayload(String paymentMethod, String id, String emailOrMobileNrOrCashtag, String extraInfo, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.emailOrMobileNrOrCashtag = emailOrMobileNrOrCashtag; this.extraInfo = extraInfo; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setCashAppAccountPayload(protobuf.CashAppAccountPayload.newBuilder() .setExtraInfo(extraInfo) .setEmailOrMobileNrOrCashtag(emailOrMobileNrOrCashtag)) .build(); } public static CashAppAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new CashAppAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getCashAppAccountPayload().getEmailOrMobileNrOrCashtag(), proto.getCashAppAccountPayload().getExtraInfo(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.email.mobile.cashtag") + " " + emailOrMobileNrOrCashtag + "\n" + Res.getWithCol("payment.shared.extraInfo") + " " + extraInfo+ "\n"; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(emailOrMobileNrOrCashtag.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/CashAtAtmAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class CashAtAtmAccountPayload extends PaymentAccountPayload { private String extraInfo = ""; public CashAtAtmAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private CashAtAtmAccountPayload(String paymentMethod, String id, String extraInfo, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.extraInfo = extraInfo; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setCashAtAtmAccountPayload(protobuf.CashAtAtmAccountPayload.newBuilder() .setExtraInfo(extraInfo)) .build(); } public static CashAtAtmAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new CashAtAtmAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getCashAtAtmAccountPayload().getExtraInfo(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.getWithCol("payment.shared.extraInfo") + " " + extraInfo; } @Override public String getPaymentDetailsForTradePopup() { return Res.getWithCol("payment.shared.extraInfo") + " " + extraInfo; } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(ArrayUtils.addAll(id.getBytes(StandardCharsets.UTF_8))); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/CashDepositAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.BankUtil; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public class CashDepositAccountPayload extends BankAccountPayload { @Nullable private String holderEmail; @Nullable private String requirements; public CashDepositAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private CashDepositAccountPayload(String paymentMethodName, String id, String countryCode, List acceptedCountryCodes, String holderName, @Nullable String holderEmail, @Nullable String bankName, @Nullable String branchId, @Nullable String accountNr, @Nullable String accountType, @Nullable String requirements, @Nullable String holderTaxId, @Nullable String bankId, @Nullable String nationalAccountId, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethodName, id, countryCode, acceptedCountryCodes, holderName, bankName, branchId, accountNr, accountType, holderTaxId, bankId, nationalAccountId, maxTradePeriod, excludeFromJsonDataMap); this.holderEmail = holderEmail; this.requirements = requirements; } @Override public Message toProtoMessage() { protobuf.CashDepositAccountPayload.Builder builder = protobuf.CashDepositAccountPayload.newBuilder() .setHolderName(holderName); Optional.ofNullable(holderEmail).ifPresent(builder::setHolderEmail); Optional.ofNullable(bankName).ifPresent(builder::setBankName); Optional.ofNullable(branchId).ifPresent(builder::setBranchId); Optional.ofNullable(accountNr).ifPresent(builder::setAccountNr); Optional.ofNullable(accountType).ifPresent(builder::setAccountType); Optional.ofNullable(requirements).ifPresent(builder::setRequirements); Optional.ofNullable(holderTaxId).ifPresent(builder::setHolderTaxId); Optional.ofNullable(bankId).ifPresent(builder::setBankId); Optional.ofNullable(nationalAccountId).ifPresent(builder::setNationalAccountId); final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setCashDepositAccountPayload(builder); return getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) .build(); } public static PaymentAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); protobuf.CashDepositAccountPayload cashDepositAccountPayload = countryBasedPaymentAccountPayload.getCashDepositAccountPayload(); return new CashDepositAccountPayload(proto.getPaymentMethodId(), proto.getId(), countryBasedPaymentAccountPayload.getCountryCode(), new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), cashDepositAccountPayload.getHolderName(), cashDepositAccountPayload.getHolderEmail().isEmpty() ? null : cashDepositAccountPayload.getHolderEmail(), cashDepositAccountPayload.getBankName().isEmpty() ? null : cashDepositAccountPayload.getBankName(), cashDepositAccountPayload.getBranchId().isEmpty() ? null : cashDepositAccountPayload.getBranchId(), cashDepositAccountPayload.getAccountNr().isEmpty() ? null : cashDepositAccountPayload.getAccountNr(), cashDepositAccountPayload.getAccountType().isEmpty() ? null : cashDepositAccountPayload.getAccountType(), cashDepositAccountPayload.getRequirements().isEmpty() ? null : cashDepositAccountPayload.getRequirements(), cashDepositAccountPayload.getHolderTaxId().isEmpty() ? null : cashDepositAccountPayload.getHolderTaxId(), cashDepositAccountPayload.getBankId().isEmpty() ? null : cashDepositAccountPayload.getBankId(), cashDepositAccountPayload.getNationalAccountId().isEmpty() ? null : cashDepositAccountPayload.getNationalAccountId(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return "Cash deposit - " + getPaymentDetailsForTradePopup().replace("\n", ", "); } @Override public String getPaymentDetailsForTradePopup() { String bankName = BankUtil.isBankNameRequired(countryCode) ? BankUtil.getBankNameLabel(countryCode) + ": " + this.bankName + "\n" : ""; String bankId = BankUtil.isBankIdRequired(countryCode) ? BankUtil.getBankIdLabel(countryCode) + ": " + this.bankId + "\n" : ""; String branchId = BankUtil.isBranchIdRequired(countryCode) ? BankUtil.getBranchIdLabel(countryCode) + ": " + this.branchId + "\n" : ""; String nationalAccountId = BankUtil.isNationalAccountIdRequired(countryCode) ? BankUtil.getNationalAccountIdLabel(countryCode) + ": " + this.nationalAccountId + "\n" : ""; String accountNr = BankUtil.isAccountNrRequired(countryCode) ? BankUtil.getAccountNrLabel(countryCode) + ": " + this.accountNr + "\n" : ""; String accountType = BankUtil.isAccountTypeRequired(countryCode) ? BankUtil.getAccountTypeLabel(countryCode) + ": " + this.accountType + "\n" : ""; String holderTaxIdString = BankUtil.isHolderIdRequired(countryCode) ? (BankUtil.getHolderIdLabel(countryCode) + ": " + holderTaxId + "\n") : ""; String requirementsString = requirements != null && !requirements.isEmpty() ? (Res.getWithCol("payment.extras") + " " + requirements + "\n") : ""; String emailString = holderEmail != null ? (Res.getWithCol("payment.email") + " " + holderEmail + "\n") : ""; return Res.getWithCol("payment.account.owner.fullname") + " " + holderName + "\n" + emailString + bankName + bankId + branchId + nationalAccountId + accountNr + accountType + holderTaxIdString + requirementsString + Res.getWithCol("payment.bank.country") + " " + CountryUtil.getNameByCode(countryCode); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/CelPayAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class CelPayAccountPayload extends PaymentAccountPayload { private String email = ""; public CelPayAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } private CelPayAccountPayload(String paymentMethod, String id, String email, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.email = email; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setCelPayAccountPayload(protobuf.CelPayAccountPayload.newBuilder().setEmail(email)) .build(); } public static CelPayAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new CelPayAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getCelPayAccountPayload().getEmail(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.email") + " " + email; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(email.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/ChaseQuickPayAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; // Removed due to QuickPay becoming Zelle // Cannot be deleted as it would break old trade history entries @Deprecated @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class ChaseQuickPayAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { private String email = ""; private String holderName = ""; public ChaseQuickPayAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private ChaseQuickPayAccountPayload(String paymentMethod, String id, String email, String holderName, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.email = email; this.holderName = holderName; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setChaseQuickPayAccountPayload(protobuf.ChaseQuickPayAccountPayload.newBuilder() .setEmail(email) .setHolderName(holderName)) .build(); } public static ChaseQuickPayAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new ChaseQuickPayAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getChaseQuickPayAccountPayload().getEmail(), proto.getChaseQuickPayAccountPayload().getHolderName(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner.fullname") + " " + holderName + ", " + Res.get("payment.email") + " " + email; } @Override public String getPaymentDetailsForTradePopup() { return Res.getWithCol("payment.account.owner.fullname") + " " + holderName + "\n" + Res.getWithCol("payment.email") + " " + email; } @Override public byte[] getAgeWitnessInputData() { // We don't add holderName because we don't want to break age validation if the user recreates an account with // slight changes in holder name (e.g. add or remove middle name) return super.getAgeWitnessInputData(email.getBytes(StandardCharsets.UTF_8)); } @Override public String getOwnerId() { return holderName; } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/CountryBasedPaymentAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public abstract class CountryBasedPaymentAccountPayload extends PaymentAccountPayload { protected String countryCode = ""; protected List acceptedCountryCodes = new ArrayList(); CountryBasedPaymentAccountPayload(String paymentMethodName, String id) { super(paymentMethodName, id); } protected CountryBasedPaymentAccountPayload(String paymentMethodName, String id, String countryCode, List acceptedCountryCodes, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethodName, id, maxTradePeriod, excludeFromJsonDataMap); this.countryCode = countryCode; this.acceptedCountryCodes = acceptedCountryCodes; } @Override protected protobuf.PaymentAccountPayload.Builder getPaymentAccountPayloadBuilder() { protobuf.CountryBasedPaymentAccountPayload.Builder builder = protobuf.CountryBasedPaymentAccountPayload.newBuilder() .setCountryCode(countryCode) .addAllAcceptedCountryCodes(acceptedCountryCodes); return super.getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(builder); } @Override public abstract String getPaymentDetails(); @Override public abstract String getPaymentDetailsForTradePopup(); @Override protected byte[] getAgeWitnessInputData(byte[] data) { return super.getAgeWitnessInputData(ArrayUtils.addAll(countryCode.getBytes(StandardCharsets.UTF_8), data)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/CryptoCurrencyAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class CryptoCurrencyAccountPayload extends AssetAccountPayload { public CryptoCurrencyAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private CryptoCurrencyAccountPayload(String paymentMethod, String id, String address, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, address, maxTradePeriod, excludeFromJsonDataMap); } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setCryptoCurrencyAccountPayload(protobuf.CryptoCurrencyAccountPayload.newBuilder() .setAddress(address)) .build(); } public static CryptoCurrencyAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new CryptoCurrencyAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getCryptoCurrencyAccountPayload().getAddress(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/DomesticWireTransferAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.BankUtil; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Getter @Setter @Slf4j public final class DomesticWireTransferAccountPayload extends BankAccountPayload { private String holderAddress = ""; public DomesticWireTransferAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private DomesticWireTransferAccountPayload(String paymentMethodName, String id, String countryCode, List acceptedCountryCodes, String holderName, String bankName, String branchId, String accountNr, String holderAddress, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethodName, id, countryCode, acceptedCountryCodes, holderName, bankName, branchId, accountNr, null, null, // holderTaxId not used null, // bankId not used null, // nationalAccountId not used maxTradePeriod, excludeFromJsonDataMap); this.holderAddress = holderAddress; } @Override public Message toProtoMessage() { protobuf.DomesticWireTransferAccountPayload.Builder builder = protobuf.DomesticWireTransferAccountPayload.newBuilder() .setHolderAddress(holderAddress); protobuf.BankAccountPayload.Builder bankAccountPayloadBuilder = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .getBankAccountPayloadBuilder() .setDomesticWireTransferAccountPayload(builder); protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayloadBuilder = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setBankAccountPayload(bankAccountPayloadBuilder); return getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayloadBuilder) .build(); } public static DomesticWireTransferAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); protobuf.BankAccountPayload bankAccountPayloadPB = countryBasedPaymentAccountPayload.getBankAccountPayload(); protobuf.DomesticWireTransferAccountPayload accountPayloadPB = bankAccountPayloadPB.getDomesticWireTransferAccountPayload(); return new DomesticWireTransferAccountPayload(proto.getPaymentMethodId(), proto.getId(), countryBasedPaymentAccountPayload.getCountryCode(), new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), bankAccountPayloadPB.getHolderName(), bankAccountPayloadPB.getBankName().isEmpty() ? null : bankAccountPayloadPB.getBankName(), bankAccountPayloadPB.getBranchId().isEmpty() ? null : bankAccountPayloadPB.getBranchId(), bankAccountPayloadPB.getAccountNr().isEmpty() ? null : bankAccountPayloadPB.getAccountNr(), accountPayloadPB.getHolderAddress().isEmpty() ? null : accountPayloadPB.getHolderAddress(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @Override public String getPaymentDetails() { String paymentDetails = (Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner.fullname") + " " + holderName + ", " + BankUtil.getBankNameLabel(countryCode) + ": " + this.bankName + ", " + BankUtil.getBranchIdLabel(countryCode) + ": " + this.branchId + ", " + BankUtil.getAccountNrLabel(countryCode) + ": " + this.accountNr); return paymentDetails; } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/F2FAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class F2FAccountPayload extends CountryBasedPaymentAccountPayload { private String contact = ""; private String city = ""; private String extraInfo = ""; public F2FAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private F2FAccountPayload(String paymentMethodName, String id, String countryCode, List acceptedCountryCodes, String contact, String city, String extraInfo, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethodName, id, countryCode, acceptedCountryCodes, maxTradePeriod, excludeFromJsonDataMap); this.contact = contact; this.city = city; this.extraInfo = extraInfo; } @Override public Message toProtoMessage() { protobuf.F2FAccountPayload.Builder builder = protobuf.F2FAccountPayload.newBuilder() .setContact(contact) .setCity(city) .setExtraInfo(extraInfo); final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setF2FAccountPayload(builder); return getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) .build(); } public static PaymentAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); protobuf.F2FAccountPayload f2fAccountPayloadPB = countryBasedPaymentAccountPayload.getF2FAccountPayload(); return new F2FAccountPayload(proto.getPaymentMethodId(), proto.getId(), countryBasedPaymentAccountPayload.getCountryCode(), new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), f2fAccountPayloadPB.getContact(), f2fAccountPayloadPB.getCity(), f2fAccountPayloadPB.getExtraInfo(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.f2f.contact") + " " + contact + ", " + Res.getWithCol("payment.f2f.city") + " " + city + ", " + Res.getWithCol("payment.shared.extraInfo") + " " + extraInfo; } @Override public String getPaymentDetailsForTradePopup() { // We don't show here more as the makers extra data are the relevant for the trade. City has to be anyway the // same for maker and taker. return Res.getWithCol("payment.f2f.contact") + " " + contact; } @Override public byte[] getAgeWitnessInputData() { // We use here the city because the address alone seems to be too weak return super.getAgeWitnessInputData(ArrayUtils.addAll(contact.getBytes(StandardCharsets.UTF_8), city.getBytes(StandardCharsets.UTF_8))); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/FasterPaymentsAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Getter @Slf4j public final class FasterPaymentsAccountPayload extends PaymentAccountPayload { @Setter private String holderName = ""; @Setter private String sortCode = ""; @Setter private String accountNr = ""; public FasterPaymentsAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private FasterPaymentsAccountPayload(String paymentMethod, String id, String holderName, String sortCode, String accountNr, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.holderName = holderName; this.sortCode = sortCode; this.accountNr = accountNr; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setFasterPaymentsAccountPayload(protobuf.FasterPaymentsAccountPayload.newBuilder() .setHolderName(holderName) .setSortCode(sortCode) .setAccountNr(accountNr)) .build(); } public static FasterPaymentsAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new FasterPaymentsAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getFasterPaymentsAccountPayload().getHolderName(), proto.getFasterPaymentsAccountPayload().getSortCode(), proto.getFasterPaymentsAccountPayload().getAccountNr(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + getPaymentDetailsForTradePopup().replace("\n", ", "); } @Override public String getPaymentDetailsForTradePopup() { return (getHolderName().isEmpty() ? "" : Res.getWithCol("payment.account.owner.fullname") + " " + getHolderName() + "\n") + "UK Sort code: " + sortCode + "\n" + Res.getWithCol("payment.accountNr") + " " + accountNr; } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(ArrayUtils.addAll(sortCode.getBytes(StandardCharsets.UTF_8), accountNr.getBytes(StandardCharsets.UTF_8))); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/HalCashAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class HalCashAccountPayload extends PaymentAccountPayload { private String mobileNr = ""; public HalCashAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private HalCashAccountPayload(String paymentMethod, String id, String mobileNr, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.mobileNr = mobileNr; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setHalCashAccountPayload(protobuf.HalCashAccountPayload.newBuilder() .setMobileNr(mobileNr)) .build(); } public static HalCashAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new HalCashAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getHalCashAccountPayload().getMobileNr(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.mobile") + " " + mobileNr; } @Override public String getPaymentDetailsForTradePopup() { return Res.getWithCol("payment.mobile") + " " + mobileNr; } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(mobileNr.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/IfscBasedAccountPayload.java ================================================ package haveno.core.payment.payload; import haveno.core.locale.BankUtil; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; import java.util.Optional; @EqualsAndHashCode(callSuper = true) @Setter @Getter @ToString @Slf4j public abstract class IfscBasedAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName { protected String holderName = ""; protected String ifsc = ""; protected String accountNr = ""; protected IfscBasedAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// protected IfscBasedAccountPayload(String paymentMethodName, String id, String countryCode, List acceptedCountryCodes, String holderName, String accountNr, String ifsc, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethodName, id, countryCode, acceptedCountryCodes, maxTradePeriod, excludeFromJsonDataMap); this.holderName = holderName; this.accountNr = accountNr; this.ifsc = ifsc; } @Override public protobuf.PaymentAccountPayload.Builder getPaymentAccountPayloadBuilder() { protobuf.IfscBasedAccountPayload.Builder builder = protobuf.IfscBasedAccountPayload.newBuilder() .setHolderName(holderName); Optional.ofNullable(ifsc).ifPresent(builder::setIfsc); Optional.ofNullable(accountNr).ifPresent(builder::setAccountNr); final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayloadBuilder = super.getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setIfscBasedAccountPayload(builder); return super.getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayloadBuilder); } @Override public String getPaymentDetails() { return "Ifsc account transfer - " + getPaymentDetailsForTradePopup().replace("\n", ", "); } @Override public String getPaymentDetailsForTradePopup() { return Res.getWithCol("payment.account.owner.fullname") + " " + holderName + "\n" + BankUtil.getAccountNrLabel(countryCode) + ": " + accountNr + "\n" + BankUtil.getBankIdLabel(countryCode) + ": " + ifsc + "\n" + Res.getWithCol("payment.bank.country") + " " + CountryUtil.getNameByCode(countryCode); } @Override public byte[] getAgeWitnessInputData() { // We don't add holderName because we don't want to break age validation if the user recreates an account with // slight changes in holder name (e.g. add or remove middle name) String all = accountNr + ifsc; return super.getAgeWitnessInputData(all.getBytes(StandardCharsets.UTF_8)); } @Override public String getOwnerId() { return holderName; } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/ImpsAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class ImpsAccountPayload extends IfscBasedAccountPayload { public ImpsAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } private ImpsAccountPayload(String paymentMethod, String id, String countryCode, List acceptedCountryCodes, String holderName, String accountNr, String ifsc, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, countryCode, acceptedCountryCodes, holderName, accountNr, ifsc, maxTradePeriod, excludeFromJsonDataMap); } @Override public Message toProtoMessage() { protobuf.IfscBasedAccountPayload.Builder ifscBasedAccountPayloadBuilder = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .getIfscBasedAccountPayloadBuilder() .setImpsAccountPayload(protobuf.ImpsAccountPayload.newBuilder()); protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayloadBuilder = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setIfscBasedAccountPayload(ifscBasedAccountPayloadBuilder); return getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayloadBuilder) .build(); } public static ImpsAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); protobuf.IfscBasedAccountPayload ifscBasedAccountPayloadPB = countryBasedPaymentAccountPayload.getIfscBasedAccountPayload(); return new ImpsAccountPayload(proto.getPaymentMethodId(), proto.getId(), countryBasedPaymentAccountPayload.getCountryCode(), new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), ifscBasedAccountPayloadPB.getHolderName(), ifscBasedAccountPayloadPB.getAccountNr(), ifscBasedAccountPayloadPB.getIfsc(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner.fullname") + " " + holderName + ", " + Res.getWithCol("payment.account.no") + " " + accountNr + Res.getWithCol("payment.ifsc") + " " + ifsc; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { String accountNr = this.accountNr == null ? "" : this.accountNr; return super.getAgeWitnessInputData(accountNr.getBytes(StandardCharsets.UTF_8)); } @Override public String getHolderName() { return getOwnerId(); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/InstantCryptoCurrencyPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class InstantCryptoCurrencyPayload extends AssetAccountPayload { public InstantCryptoCurrencyPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private InstantCryptoCurrencyPayload(String paymentMethod, String id, String address, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, address, maxTradePeriod, excludeFromJsonDataMap); } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setInstantCryptoCurrencyAccountPayload(protobuf.InstantCryptoCurrencyAccountPayload.newBuilder() .setAddress(address)) .build(); } public static InstantCryptoCurrencyPayload fromProto(protobuf.PaymentAccountPayload proto) { return new InstantCryptoCurrencyPayload(proto.getPaymentMethodId(), proto.getId(), proto.getInstantCryptoCurrencyAccountPayload().getAddress(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/InteracETransferAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class InteracETransferAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { private String emailOrMobileNr = ""; private String holderName = ""; private String question = ""; private String answer = ""; public InteracETransferAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private InteracETransferAccountPayload(String paymentMethod, String id, String emailOrMobileNr, String holderName, String question, String answer, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.emailOrMobileNr = emailOrMobileNr; this.holderName = holderName; this.question = question; this.answer = answer; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setInteracETransferAccountPayload(protobuf.InteracETransferAccountPayload.newBuilder() .setEmailOrMobileNr(emailOrMobileNr) .setHolderName(holderName) .setQuestion(question) .setAnswer(answer)) .build(); } public static InteracETransferAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new InteracETransferAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getInteracETransferAccountPayload().getEmailOrMobileNr(), proto.getInteracETransferAccountPayload().getHolderName(), proto.getInteracETransferAccountPayload().getQuestion(), proto.getInteracETransferAccountPayload().getAnswer(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner.fullname") + " " + holderName + ", " + Res.get("payment.email") + " " + emailOrMobileNr + ", " + Res.getWithCol("payment.secret") + " " + question + ", " + Res.getWithCol("payment.answer") + " " + answer; } @Override public String getPaymentDetailsForTradePopup() { return Res.getWithCol("payment.account.owner.fullname") + " " + holderName + "\n" + Res.getWithCol("payment.email") + " " + emailOrMobileNr + "\n" + Res.getWithCol("payment.secret") + " " + question + "\n" + Res.getWithCol("payment.answer") + " " + answer; } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(ArrayUtils.addAll(emailOrMobileNr.getBytes(StandardCharsets.UTF_8), ArrayUtils.addAll(question.getBytes(StandardCharsets.UTF_8), answer.getBytes(StandardCharsets.UTF_8)))); } @Override public String getOwnerId() { return holderName; } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/JapanBankAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class JapanBankAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { // bank private String bankName = ""; private String bankCode = ""; // branch private String bankBranchName = ""; private String bankBranchCode = ""; // account private String bankAccountType = ""; private String bankAccountName = ""; private String bankAccountNumber = ""; public JapanBankAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private JapanBankAccountPayload(String paymentMethod, String id, String bankName, String bankCode, String bankBranchName, String bankBranchCode, String bankAccountType, String bankAccountName, String bankAccountNumber, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.bankName = bankName; this.bankCode = bankCode; this.bankBranchName = bankBranchName; this.bankBranchCode = bankBranchCode; this.bankAccountType = bankAccountType; this.bankAccountName = bankAccountName; this.bankAccountNumber = bankAccountNumber; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setJapanBankAccountPayload( protobuf.JapanBankAccountPayload.newBuilder() .setBankName(bankName) .setBankCode(bankCode) .setBankBranchName(bankBranchName) .setBankBranchCode(bankBranchCode) .setBankAccountType(bankAccountType) .setBankAccountName(bankAccountName) .setBankAccountNumber(bankAccountNumber) ).build(); } public static JapanBankAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.JapanBankAccountPayload japanBankAccountPayload = proto.getJapanBankAccountPayload(); return new JapanBankAccountPayload(proto.getPaymentMethodId(), proto.getId(), japanBankAccountPayload.getBankName(), japanBankAccountPayload.getBankCode(), japanBankAccountPayload.getBankBranchName(), japanBankAccountPayload.getBankBranchCode(), japanBankAccountPayload.getBankAccountType(), japanBankAccountPayload.getBankAccountName(), japanBankAccountPayload.getBankAccountNumber(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + getPaymentDetailsForTradePopup().replace("\n", ", "); } @Override public String getPaymentDetailsForTradePopup() { return Res.get("payment.japan.bank") + ": " + bankName + "(" + bankCode + ")\n" + Res.get("payment.japan.branch") + ": " + bankBranchName + "(" + bankBranchCode + ")\n" + Res.get("payment.japan.account") + ": " + bankAccountType + " " + bankAccountNumber + "\n" + Res.get("payment.japan.recipient") + ": " + bankAccountName; } @Override public byte[] getAgeWitnessInputData() { String all = this.bankName + this.bankBranchName + this.bankAccountType + this.bankAccountNumber + this.bankAccountName; return super.getAgeWitnessInputData(all.getBytes(StandardCharsets.UTF_8)); } @Override public String getHolderName() { return bankAccountName; } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/MoneseAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class MoneseAccountPayload extends PaymentAccountPayload { private String holderName = ""; private String mobileNr = ""; public MoneseAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } private MoneseAccountPayload(String paymentMethod, String id, String holderName, String mobileNr, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.holderName = holderName; this.mobileNr = mobileNr; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setMoneseAccountPayload(protobuf.MoneseAccountPayload.newBuilder() .setHolderName(holderName) .setMobileNr(mobileNr)) .build(); } public static MoneseAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new MoneseAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getMoneseAccountPayload().getHolderName(), proto.getMoneseAccountPayload().getMobileNr(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.username") + " " + holderName; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(holderName.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/MoneyBeamAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.common.util.JsonExclude; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import org.apache.commons.lang3.ArrayUtils; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class MoneyBeamAccountPayload extends PaymentAccountPayload { private String accountId = ""; // This field is excluded for backward compatibility and to allow changes. @JsonExclude private String holderName = ""; public MoneyBeamAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private MoneyBeamAccountPayload(String paymentMethod, String id, String accountId, String holderName, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.accountId = accountId; this.holderName = holderName; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setMoneyBeamAccountPayload(protobuf.MoneyBeamAccountPayload.newBuilder() .setAccountId(accountId) .setHolderName(holderName)) .build(); } public static MoneyBeamAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new MoneyBeamAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getMoneyBeamAccountPayload().getAccountId(), proto.getMoneyBeamAccountPayload().getHolderName(), 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.account") + " " + accountId + "\n" + Res.getWithCol("payment.account.owner.fullname") + " " + PaymentAccountPayload.getHolderNameOrPromptIfEmpty(getHolderName()); } @Override public byte[] getAgeWitnessInputData() { // holderName will be included as part of the witness data. // older accounts that don't have holderName still retain their existing witness. return super.getAgeWitnessInputData(ArrayUtils.addAll( accountId.getBytes(StandardCharsets.UTF_8), getHolderName().getBytes(StandardCharsets.UTF_8))); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/MoneyGramAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.BankUtil; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public class MoneyGramAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { private String holderName; private String countryCode = ""; private String state = ""; // is optional. we don't use @Nullable because it would makes UI code more complex. private String email; public MoneyGramAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private MoneyGramAccountPayload(String paymentMethodName, String id, String countryCode, String holderName, String state, String email, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethodName, id, maxTradePeriod, excludeFromJsonDataMap); this.holderName = holderName; this.countryCode = countryCode; this.state = state; this.email = email; } @Override public Message toProtoMessage() { protobuf.MoneyGramAccountPayload.Builder builder = protobuf.MoneyGramAccountPayload.newBuilder() .setHolderName(holderName) .setCountryCode(countryCode) .setState(state) .setEmail(email); return getPaymentAccountPayloadBuilder() .setMoneyGramAccountPayload(builder) .build(); } public static PaymentAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.MoneyGramAccountPayload moneyGramAccountPayload = proto.getMoneyGramAccountPayload(); return new MoneyGramAccountPayload(proto.getPaymentMethodId(), proto.getId(), moneyGramAccountPayload.getCountryCode(), moneyGramAccountPayload.getHolderName(), moneyGramAccountPayload.getState(), moneyGramAccountPayload.getEmail(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + getPaymentDetailsForTradePopup().replace("\n", ", "); } @Override public String getPaymentDetailsForTradePopup() { String state = BankUtil.isStateRequired(countryCode) ? (Res.getWithCol("payment.account.state") + " " + this.state + "\n") : ""; return Res.getWithCol("payment.account.fullName") + " " + holderName + "\n" + state + Res.getWithCol("payment.bank.country") + " " + CountryUtil.getNameByCode(countryCode) + "\n" + Res.getWithCol("payment.email") + " " + email; } @Override public byte[] getAgeWitnessInputData() { String all = this.countryCode + this.state + this.holderName + this.email; return super.getAgeWitnessInputData(all.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/NationalBankAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Slf4j public final class NationalBankAccountPayload extends BankAccountPayload { public NationalBankAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private NationalBankAccountPayload(String paymentMethodName, String id, String countryCode, List acceptedCountryCodes, String holderName, String bankName, String branchId, String accountNr, String accountType, String holderTaxId, String bankId, String nationalAccountId, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethodName, id, countryCode, acceptedCountryCodes, holderName, bankName, branchId, accountNr, accountType, holderTaxId, bankId, nationalAccountId, maxTradePeriod, excludeFromJsonDataMap); } @Override public Message toProtoMessage() { protobuf.BankAccountPayload.Builder bankAccountPayloadBuilder = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .getBankAccountPayloadBuilder() .setNationalBankAccountPayload(protobuf.NationalBankAccountPayload.newBuilder()); protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayloadBuilder = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setBankAccountPayload(bankAccountPayloadBuilder); return getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayloadBuilder) .build(); } public static NationalBankAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); protobuf.BankAccountPayload bankAccountPayloadPB = countryBasedPaymentAccountPayload.getBankAccountPayload(); return new NationalBankAccountPayload(proto.getPaymentMethodId(), proto.getId(), countryBasedPaymentAccountPayload.getCountryCode(), new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), bankAccountPayloadPB.getHolderName(), bankAccountPayloadPB.getBankName().isEmpty() ? null : bankAccountPayloadPB.getBankName(), bankAccountPayloadPB.getBranchId().isEmpty() ? null : bankAccountPayloadPB.getBranchId(), bankAccountPayloadPB.getAccountNr().isEmpty() ? null : bankAccountPayloadPB.getAccountNr(), bankAccountPayloadPB.getAccountType().isEmpty() ? null : bankAccountPayloadPB.getAccountType(), bankAccountPayloadPB.getHolderTaxId().isEmpty() ? null : bankAccountPayloadPB.getHolderTaxId(), bankAccountPayloadPB.getBankId().isEmpty() ? null : bankAccountPayloadPB.getBankId(), bankAccountPayloadPB.getNationalAccountId().isEmpty() ? null : bankAccountPayloadPB.getNationalAccountId(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + getPaymentDetailsForTradePopup().replace("\n", ", "); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/NeftAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class NeftAccountPayload extends IfscBasedAccountPayload { public NeftAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } private NeftAccountPayload(String paymentMethod, String id, String countryCode, List acceptedCountryCodes, String holderName, String accountNr, String ifsc, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, countryCode, acceptedCountryCodes, holderName, accountNr, ifsc, maxTradePeriod, excludeFromJsonDataMap); } @Override public Message toProtoMessage() { protobuf.IfscBasedAccountPayload.Builder ifscBasedAccountPayloadBuilder = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .getIfscBasedAccountPayloadBuilder() .setNeftAccountPayload(protobuf.NeftAccountPayload.newBuilder()); protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayloadBuilder = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setIfscBasedAccountPayload(ifscBasedAccountPayloadBuilder); return getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayloadBuilder) .build(); } public static NeftAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); protobuf.IfscBasedAccountPayload ifscBasedAccountPayloadPB = countryBasedPaymentAccountPayload.getIfscBasedAccountPayload(); return new NeftAccountPayload(proto.getPaymentMethodId(), proto.getId(), countryBasedPaymentAccountPayload.getCountryCode(), new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), ifscBasedAccountPayloadPB.getHolderName(), ifscBasedAccountPayloadPB.getAccountNr(), ifscBasedAccountPayloadPB.getIfsc(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner.fullname") + " " + holderName + ", " + Res.getWithCol("payment.account.no") + " " + accountNr + Res.getWithCol("payment.ifsc") + " " + ifsc; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { String accountNr = this.accountNr == null ? "" : this.accountNr; return super.getAgeWitnessInputData(accountNr.getBytes(StandardCharsets.UTF_8)); } @Override public String getHolderName() { return getOwnerId(); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/NequiAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class NequiAccountPayload extends CountryBasedPaymentAccountPayload { private String mobileNr = ""; public NequiAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } private NequiAccountPayload(String paymentMethod, String id, String countryCode, List acceptedCountryCodes, String mobileNr, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, countryCode, acceptedCountryCodes, maxTradePeriod, excludeFromJsonDataMap); this.mobileNr = mobileNr; } @Override public Message toProtoMessage() { protobuf.NequiAccountPayload.Builder builder = protobuf.NequiAccountPayload.newBuilder() .setMobileNr(mobileNr); final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setNequiAccountPayload(builder); return getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) .build(); } public static NequiAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); protobuf.NequiAccountPayload paytmAccountPayloadPB = countryBasedPaymentAccountPayload.getNequiAccountPayload(); return new NequiAccountPayload(proto.getPaymentMethodId(), proto.getId(), countryBasedPaymentAccountPayload.getCountryCode(), new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), paytmAccountPayloadPB.getMobileNr(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.mobile") + " " + mobileNr; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(mobileNr.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/OKPayAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; // Cannot be deleted as it would break old trade history entries @Deprecated @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class OKPayAccountPayload extends PaymentAccountPayload { private String accountNr = ""; public OKPayAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private OKPayAccountPayload(String paymentMethod, String id, String accountNr, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.accountNr = accountNr; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setOKPayAccountPayload(protobuf.OKPayAccountPayload.newBuilder() .setAccountNr(accountNr)) .build(); } public static OKPayAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new OKPayAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getOKPayAccountPayload().getAccountNr(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.no") + " " + accountNr; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(accountNr.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/PaxumAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class PaxumAccountPayload extends PaymentAccountPayload { private String email = ""; public PaxumAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private PaxumAccountPayload(String paymentMethod, String id, String email, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.email = email; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setPaxumAccountPayload(protobuf.PaxumAccountPayload.newBuilder().setEmail(email)) .build(); } public static PaxumAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new PaxumAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getPaxumAccountPayload().getEmail(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.email") + " " + email; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(email.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/PayByMailAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class PayByMailAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { private String postalAddress = ""; private String contact = ""; private String extraInfo = ""; public PayByMailAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private PayByMailAccountPayload(String paymentMethod, String id, String postalAddress, String contact, String extraInfo, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.postalAddress = postalAddress; this.contact = contact; this.extraInfo = extraInfo; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setPayByMailAccountPayload(protobuf.PayByMailAccountPayload.newBuilder() .setPostalAddress(postalAddress) .setContact(contact) .setExtraInfo(extraInfo)) .build(); } public static PayByMailAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new PayByMailAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getPayByMailAccountPayload().getPostalAddress(), proto.getPayByMailAccountPayload().getContact(), proto.getPayByMailAccountPayload().getExtraInfo(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner.fullname") + " " + contact + ", " + Res.getWithCol("payment.postal.address") + " " + postalAddress + ", " + Res.getWithCol("payment.shared.extraInfo") + " " + extraInfo; } @Override public String getPaymentDetailsForTradePopup() { return Res.getWithCol("payment.account.owner.fullname") + " " + contact + "\n" + Res.getWithCol("payment.postal.address") + " " + postalAddress; } @Override public byte[] getAgeWitnessInputData() { // We use here the contact because the address alone seems to be too weak return super.getAgeWitnessInputData(ArrayUtils.addAll(contact.getBytes(StandardCharsets.UTF_8), postalAddress.getBytes(StandardCharsets.UTF_8))); } @Override public String getOwnerId() { return contact; } @Override public String getHolderName() { return contact; } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/PayPalAccountPayload.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class PayPalAccountPayload extends PaymentAccountPayload { private String emailOrMobileNrOrUsername = ""; private String extraInfo = ""; public PayPalAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private PayPalAccountPayload(String paymentMethod, String id, String emailOrMobileNrOrUsername, String extraInfo, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.emailOrMobileNrOrUsername = emailOrMobileNrOrUsername; this.extraInfo = extraInfo; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setPaypalAccountPayload(protobuf.PayPalAccountPayload.newBuilder() .setExtraInfo(extraInfo) .setEmailOrMobileNrOrUsername(emailOrMobileNrOrUsername)) .build(); } public static PayPalAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new PayPalAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getPaypalAccountPayload().getEmailOrMobileNrOrUsername(), proto.getPaypalAccountPayload().getExtraInfo(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.getWithCol("payment.email.mobile.username") + " "+ emailOrMobileNrOrUsername + "\n" + Res.getWithCol("payment.shared.extraInfo") + " " + extraInfo+ "\n"; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(emailOrMobileNrOrUsername.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/PayloadWithHolderName.java ================================================ /* * 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 . */ package haveno.core.payment.payload; public interface PayloadWithHolderName { String getHolderName(); } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/PaymentAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.gson.GsonBuilder; import haveno.common.consensus.UsedForTradeContractJson; import haveno.common.crypto.CryptoUtils; import haveno.common.crypto.Hash; import haveno.common.proto.network.NetworkPayload; import haveno.common.util.JsonExclude; import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.core.proto.CoreProtoResolver; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import static com.google.common.base.Preconditions.checkArgument; // That class is used in the contract for creating the contract json. Any change will break the contract. // If a field gets added it need to be be annotated with @JsonExclude (excluded from contract). // We should add an extraDataMap as in StoragePayload objects @Getter @EqualsAndHashCode @ToString @Slf4j public abstract class PaymentAccountPayload implements NetworkPayload, UsedForTradeContractJson { // Keys for excludeFromJsonDataMap public static final String SALT = "salt"; protected final String paymentMethodId; protected final String id; // Is just kept for not breaking backward compatibility. Set to -1 to indicate it is no used anymore. protected final long maxTradePeriod; // In v0.6 we removed maxTradePeriod but we need to keep it in the PB file for backward compatibility // protected final long maxTradePeriod; // Used for new data (e.g. salt introduced in v0.6) which would break backward compatibility as // PaymentAccountPayload is used for the json contract and a trade with a user who has an older version would // fail the contract verification. @JsonExclude protected final Map excludeFromJsonDataMap; private static final GsonBuilder gsonBuilder = new GsonBuilder() .setPrettyPrinting() .serializeNulls(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// PaymentAccountPayload(String paymentMethodId, String id) { this(paymentMethodId, id, -1, new HashMap<>()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// protected PaymentAccountPayload(String paymentMethodId, String id, long maxTradePeriod, Map excludeFromJsonDataMapParam) { this.paymentMethodId = paymentMethodId; this.id = id; this.maxTradePeriod = maxTradePeriod; this.excludeFromJsonDataMap = excludeFromJsonDataMapParam; // If not set (old versions) we set by default a random 256 bit salt. // User can set salt as well by hex string. // Persisted value will overwrite that if (!this.excludeFromJsonDataMap.containsKey(SALT)) this.excludeFromJsonDataMap.put(SALT, Utilities.encodeToHex(CryptoUtils.getRandomBytes(32))); } protected protobuf.PaymentAccountPayload.Builder getPaymentAccountPayloadBuilder() { final protobuf.PaymentAccountPayload.Builder builder = protobuf.PaymentAccountPayload.newBuilder() .setPaymentMethodId(paymentMethodId) .setMaxTradePeriod(maxTradePeriod) .setId(id); builder.putAllExcludeFromJsonData(excludeFromJsonDataMap); return builder; } public static PaymentAccountPayload fromProto(protobuf.PaymentAccountPayload proto, CoreProtoResolver coreProtoResolver) { return coreProtoResolver.fromProto(proto); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public String toJson() { return gsonBuilder.create().toJson(this); } public abstract String getPaymentDetails(); public abstract String getPaymentDetailsForTradePopup(); public byte[] getHash() { return Hash.getRipemd160hash(this.toProtoMessage().toByteArray()); // TODO: adopt serializeForHash() from Bisq? } public byte[] getSalt() { checkArgument(excludeFromJsonDataMap.containsKey(SALT), "Salt must have been set in excludeFromJsonDataMap."); return Utilities.decodeFromHex(excludeFromJsonDataMap.get(SALT)); } public void setSalt(byte[] salt) { excludeFromJsonDataMap.put(SALT, Utilities.encodeToHex(salt)); } // Identifying data of payment account (e.g. IBAN). // This is critical code for verifying age of payment account. // Any change would break validation of historical data! public abstract byte[] getAgeWitnessInputData(); protected byte[] getAgeWitnessInputData(byte[] data) { return ArrayUtils.addAll(paymentMethodId.getBytes(StandardCharsets.UTF_8), data); } public String getOwnerId() { return null; } public static String getHolderNameOrPromptIfEmpty(String holderName) { return holderName.isEmpty() ? Res.get("payment.account.owner.ask") : holderName; } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/PaymentMethod.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.payment.payload; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; import haveno.common.proto.persistable.PersistablePayload; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.AchTransferAccount; import haveno.core.payment.AdvancedCashAccount; import haveno.core.payment.AliPayAccount; import haveno.core.payment.AmazonGiftCardAccount; import haveno.core.payment.AustraliaPayidAccount; import haveno.core.payment.BizumAccount; import haveno.core.payment.CapitualAccount; import haveno.core.payment.CashAppAccount; import haveno.core.payment.CashAtAtmAccount; import haveno.core.payment.PayByMailAccount; import haveno.core.payment.PayPalAccount; import haveno.core.payment.PaysafeAccount; import haveno.core.payment.CashDepositAccount; import haveno.core.payment.CelPayAccount; import haveno.core.payment.ZelleAccount; import haveno.core.payment.DomesticWireTransferAccount; import haveno.core.payment.F2FAccount; import haveno.core.payment.FasterPaymentsAccount; import haveno.core.payment.HalCashAccount; import haveno.core.payment.ImpsAccount; import haveno.core.payment.InteracETransferAccount; import haveno.core.payment.JapanBankAccount; import haveno.core.payment.MoneseAccount; import haveno.core.payment.MoneyBeamAccount; import haveno.core.payment.MoneyGramAccount; import haveno.core.payment.NationalBankAccount; import haveno.core.payment.NeftAccount; import haveno.core.payment.NequiAccount; import haveno.core.payment.PaxumAccount; import haveno.core.payment.PayseraAccount; import haveno.core.payment.PaytmAccount; import haveno.core.payment.PerfectMoneyAccount; import haveno.core.payment.PixAccount; import haveno.core.payment.PopmoneyAccount; import haveno.core.payment.PromptPayAccount; import haveno.core.payment.RevolutAccount; import haveno.core.payment.RtgsAccount; import haveno.core.payment.SameBankAccount; import haveno.core.payment.SatispayAccount; import haveno.core.payment.SepaAccount; import haveno.core.payment.SepaInstantAccount; import haveno.core.payment.SpecificBanksAccount; import haveno.core.payment.StrikeAccount; import haveno.core.payment.SwiftAccount; import haveno.core.payment.SwishAccount; import haveno.core.payment.TikkieAccount; import haveno.core.payment.TradeLimits; import haveno.core.payment.TransferwiseAccount; import haveno.core.payment.TransferwiseUsdAccount; import haveno.core.payment.USPostalMoneyOrderAccount; import haveno.core.payment.UpholdAccount; import haveno.core.payment.UpiAccount; import haveno.core.payment.VenmoAccount; import haveno.core.payment.VerseAccount; import haveno.core.payment.WeChatPayAccount; import haveno.core.payment.WesternUnionAccount; import haveno.core.trade.HavenoUtils; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @EqualsAndHashCode(exclude = {"maxTradePeriod", "maxTradeLimit"}) @ToString @Slf4j public final class PaymentMethod implements PersistablePayload, Comparable { /////////////////////////////////////////////////////////////////////////////////////////// // Static /////////////////////////////////////////////////////////////////////////////////////////// // time in ms for 1 "day" (mainnet), 30m (stagenet) or 1 minute (local) private static final long DAY = Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_LOCAL ? TimeUnit.MINUTES.toMillis(1) : Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_STAGENET ? TimeUnit.MINUTES.toMillis(30) : TimeUnit.DAYS.toMillis(1); // These values are not used except to derive the associated risk factor. private static final BigInteger DEFAULT_TRADE_LIMIT_CRYPTO = HavenoUtils.xmrToAtomicUnits(200); private static final BigInteger DEFAULT_TRADE_LIMIT_VERY_LOW_RISK = HavenoUtils.xmrToAtomicUnits(100); private static final BigInteger DEFAULT_TRADE_LIMIT_LOW_RISK = HavenoUtils.xmrToAtomicUnits(50); private static final BigInteger DEFAULT_TRADE_LIMIT_MID_RISK = HavenoUtils.xmrToAtomicUnits(25); private static final BigInteger DEFAULT_TRADE_LIMIT_HIGH_RISK = HavenoUtils.xmrToAtomicUnits(12.5); public static final String UPHOLD_ID = "UPHOLD"; public static final String MONEY_BEAM_ID = "MONEY_BEAM"; public static final String POPMONEY_ID = "POPMONEY"; public static final String REVOLUT_ID = "REVOLUT"; public static final String PERFECT_MONEY_ID = "PERFECT_MONEY"; public static final String SEPA_ID = "SEPA"; public static final String SEPA_INSTANT_ID = "SEPA_INSTANT"; public static final String FASTER_PAYMENTS_ID = "FASTER_PAYMENTS"; public static final String NATIONAL_BANK_ID = "NATIONAL_BANK"; public static final String JAPAN_BANK_ID = "JAPAN_BANK"; public static final String AUSTRALIA_PAYID_ID = "AUSTRALIA_PAYID"; public static final String SAME_BANK_ID = "SAME_BANK"; public static final String SPECIFIC_BANKS_ID = "SPECIFIC_BANKS"; public static final String SWISH_ID = "SWISH"; public static final String ALI_PAY_ID = "ALI_PAY"; public static final String WECHAT_PAY_ID = "WECHAT_PAY"; public static final String ZELLE_ID = "ZELLE"; @Deprecated public static final String CHASE_QUICK_PAY_ID = "CHASE_QUICK_PAY"; // Removed due to QuickPay becoming Zelle public static final String INTERAC_E_TRANSFER_ID = "INTERAC_E_TRANSFER"; public static final String US_POSTAL_MONEY_ORDER_ID = "US_POSTAL_MONEY_ORDER"; public static final String CASH_DEPOSIT_ID = "CASH_DEPOSIT"; public static final String MONEY_GRAM_ID = "MONEY_GRAM"; public static final String WESTERN_UNION_ID = "WESTERN_UNION"; public static final String HAL_CASH_ID = "HAL_CASH"; public static final String F2F_ID = "F2F"; public static final String BLOCK_CHAINS_ID = "BLOCK_CHAINS"; 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 TRANSFERWISE_USD_ID = "TRANSFERWISE_USD"; public static final String PAYSERA_ID = "PAYSERA"; public static final String PAXUM_ID = "PAXUM"; public static final String NEFT_ID = "NEFT"; public static final String RTGS_ID = "RTGS"; public static final String IMPS_ID = "IMPS"; public static final String UPI_ID = "UPI"; public static final String PAYTM_ID = "PAYTM"; public static final String NEQUI_ID = "NEQUI"; public static final String BIZUM_ID = "BIZUM"; public static final String PIX_ID = "PIX"; public static final String AMAZON_GIFT_CARD_ID = "AMAZON_GIFT_CARD"; public static final String BLOCK_CHAINS_INSTANT_ID = "BLOCK_CHAINS_INSTANT"; public static final String PAY_BY_MAIL_ID = "PAY_BY_MAIL"; public static final String CASH_AT_ATM_ID = "CASH_AT_ATM"; public static final String CAPITUAL_ID = "CAPITUAL"; public static final String CELPAY_ID = "CELPAY"; public static final String MONESE_ID = "MONESE"; public static final String SATISPAY_ID = "SATISPAY"; public static final String TIKKIE_ID = "TIKKIE"; public static final String VERSE_ID = "VERSE"; public static final String STRIKE_ID = "STRIKE"; public static final String SWIFT_ID = "SWIFT"; public static final String ACH_TRANSFER_ID = "ACH_TRANSFER"; public static final String DOMESTIC_WIRE_TRANSFER_ID = "DOMESTIC_WIRE_TRANSFER"; @Deprecated public static final String OK_PAY_ID = "OK_PAY"; // Cannot be deleted as it would break old trade history entries public static final String CASH_APP_ID = "CASH_APP"; public static final String VENMO_ID = "VENMO"; public static final String PAYPAL_ID = "PAYPAL"; public static final String PAYSAFE_ID = "PAYSAFE"; public static PaymentMethod UPHOLD; public static PaymentMethod MONEY_BEAM; public static PaymentMethod POPMONEY; public static PaymentMethod REVOLUT; public static PaymentMethod PERFECT_MONEY; public static PaymentMethod SEPA; public static PaymentMethod SEPA_INSTANT; public static PaymentMethod FASTER_PAYMENTS; public static PaymentMethod NATIONAL_BANK; public static PaymentMethod JAPAN_BANK; public static PaymentMethod AUSTRALIA_PAYID; public static PaymentMethod SAME_BANK; public static PaymentMethod SPECIFIC_BANKS; public static PaymentMethod SWISH; public static PaymentMethod ALI_PAY; public static PaymentMethod WECHAT_PAY; public static PaymentMethod ZELLE; public static PaymentMethod CHASE_QUICK_PAY; public static PaymentMethod INTERAC_E_TRANSFER; public static PaymentMethod US_POSTAL_MONEY_ORDER; public static PaymentMethod CASH_DEPOSIT; public static PaymentMethod MONEY_GRAM; public static PaymentMethod WESTERN_UNION; public static PaymentMethod F2F; public static PaymentMethod HAL_CASH; public static PaymentMethod BLOCK_CHAINS; public static PaymentMethod PROMPT_PAY; public static PaymentMethod ADVANCED_CASH; public static PaymentMethod TRANSFERWISE; public static PaymentMethod TRANSFERWISE_USD; public static PaymentMethod PAYSERA; public static PaymentMethod PAXUM; public static PaymentMethod NEFT; public static PaymentMethod RTGS; public static PaymentMethod IMPS; public static PaymentMethod UPI; public static PaymentMethod PAYTM; public static PaymentMethod NEQUI; public static PaymentMethod BIZUM; public static PaymentMethod PIX; public static PaymentMethod AMAZON_GIFT_CARD; public static PaymentMethod BLOCK_CHAINS_INSTANT; public static PaymentMethod PAY_BY_MAIL; public static PaymentMethod CASH_AT_ATM; public static PaymentMethod CAPITUAL; public static PaymentMethod CELPAY; public static PaymentMethod MONESE; public static PaymentMethod SATISPAY; public static PaymentMethod TIKKIE; public static PaymentMethod VERSE; public static PaymentMethod STRIKE; public static PaymentMethod SWIFT; public static PaymentMethod ACH_TRANSFER; public static PaymentMethod DOMESTIC_WIRE_TRANSFER; public static PaymentMethod BSQ_SWAP; public static PaymentMethod PAYPAL; public static PaymentMethod CASH_APP; public static PaymentMethod VENMO; public static PaymentMethod PAYSAFE; // Cannot be deleted as it would break old trade history entries @Deprecated public static PaymentMethod OK_PAY = getDummyPaymentMethod(OK_PAY_ID); // The limit and duration assignment must not be changed as that could break old offers (if amount would be higher // than new trade limit) and violate the maker expectation when he created the offer (duration). public final static List paymentMethods = Arrays.asList( // EUR HAL_CASH = new PaymentMethod(HAL_CASH_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(HalCashAccount.SUPPORTED_CURRENCIES)), SEPA = new PaymentMethod(SEPA_ID, 6 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(SepaAccount.SUPPORTED_CURRENCIES)), SEPA_INSTANT = new PaymentMethod(SEPA_INSTANT_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(SepaInstantAccount.SUPPORTED_CURRENCIES)), MONEY_BEAM = new PaymentMethod(MONEY_BEAM_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(MoneyBeamAccount.SUPPORTED_CURRENCIES)), // UK FASTER_PAYMENTS = new PaymentMethod(FASTER_PAYMENTS_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(FasterPaymentsAccount.SUPPORTED_CURRENCIES)), // Sweden SWISH = new PaymentMethod(SWISH_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(SwishAccount.SUPPORTED_CURRENCIES)), // US ZELLE = new PaymentMethod(ZELLE_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(ZelleAccount.SUPPORTED_CURRENCIES)), POPMONEY = new PaymentMethod(POPMONEY_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(PopmoneyAccount.SUPPORTED_CURRENCIES)), US_POSTAL_MONEY_ORDER = new PaymentMethod(US_POSTAL_MONEY_ORDER_ID, 8 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(USPostalMoneyOrderAccount.SUPPORTED_CURRENCIES)), VENMO = new PaymentMethod(VENMO_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(VenmoAccount.SUPPORTED_CURRENCIES)), // Canada INTERAC_E_TRANSFER = new PaymentMethod(INTERAC_E_TRANSFER_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(InteracETransferAccount.SUPPORTED_CURRENCIES)), // Global CASH_DEPOSIT = new PaymentMethod(CASH_DEPOSIT_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(CashDepositAccount.SUPPORTED_CURRENCIES)), PAY_BY_MAIL = new PaymentMethod(PAY_BY_MAIL_ID, 8 * DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(PayByMailAccount.SUPPORTED_CURRENCIES)), CASH_AT_ATM = new PaymentMethod(CASH_AT_ATM_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(CashAtAtmAccount.SUPPORTED_CURRENCIES)), MONEY_GRAM = new PaymentMethod(MONEY_GRAM_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_MID_RISK, getAssetCodes(MoneyGramAccount.SUPPORTED_CURRENCIES)), WESTERN_UNION = new PaymentMethod(WESTERN_UNION_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_MID_RISK, getAssetCodes(WesternUnionAccount.SUPPORTED_CURRENCIES)), NATIONAL_BANK = new PaymentMethod(NATIONAL_BANK_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(NationalBankAccount.SUPPORTED_CURRENCIES)), SAME_BANK = new PaymentMethod(SAME_BANK_ID, 2 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(SameBankAccount.SUPPORTED_CURRENCIES)), SPECIFIC_BANKS = new PaymentMethod(SPECIFIC_BANKS_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(SpecificBanksAccount.SUPPORTED_CURRENCIES)), F2F = new PaymentMethod(F2F_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(F2FAccount.SUPPORTED_CURRENCIES)), AMAZON_GIFT_CARD = new PaymentMethod(AMAZON_GIFT_CARD_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(AmazonGiftCardAccount.SUPPORTED_CURRENCIES)), // Trans national UPHOLD = new PaymentMethod(UPHOLD_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(UpholdAccount.SUPPORTED_CURRENCIES)), REVOLUT = new PaymentMethod(REVOLUT_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(RevolutAccount.SUPPORTED_CURRENCIES)), PERFECT_MONEY = new PaymentMethod(PERFECT_MONEY_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(PerfectMoneyAccount.SUPPORTED_CURRENCIES)), ADVANCED_CASH = new PaymentMethod(ADVANCED_CASH_ID, DAY, DEFAULT_TRADE_LIMIT_VERY_LOW_RISK, getAssetCodes(AdvancedCashAccount.SUPPORTED_CURRENCIES)), TRANSFERWISE = new PaymentMethod(TRANSFERWISE_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(TransferwiseAccount.SUPPORTED_CURRENCIES)), TRANSFERWISE_USD = new PaymentMethod(TRANSFERWISE_USD_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(TransferwiseUsdAccount.SUPPORTED_CURRENCIES)), PAYSERA = new PaymentMethod(PAYSERA_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(PayseraAccount.SUPPORTED_CURRENCIES)), PAXUM = new PaymentMethod(PAXUM_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(PaxumAccount.SUPPORTED_CURRENCIES)), NEFT = new PaymentMethod(NEFT_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(NeftAccount.SUPPORTED_CURRENCIES)), RTGS = new PaymentMethod(RTGS_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(RtgsAccount.SUPPORTED_CURRENCIES)), IMPS = new PaymentMethod(IMPS_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(ImpsAccount.SUPPORTED_CURRENCIES)), UPI = new PaymentMethod(UPI_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(UpiAccount.SUPPORTED_CURRENCIES)), PAYTM = new PaymentMethod(PAYTM_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(PaytmAccount.SUPPORTED_CURRENCIES)), NEQUI = new PaymentMethod(NEQUI_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(NequiAccount.SUPPORTED_CURRENCIES)), BIZUM = new PaymentMethod(BIZUM_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(BizumAccount.SUPPORTED_CURRENCIES)), PIX = new PaymentMethod(PIX_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(PixAccount.SUPPORTED_CURRENCIES)), CAPITUAL = new PaymentMethod(CAPITUAL_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(CapitualAccount.SUPPORTED_CURRENCIES)), CELPAY = new PaymentMethod(CELPAY_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(CelPayAccount.SUPPORTED_CURRENCIES)), MONESE = new PaymentMethod(MONESE_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(MoneseAccount.SUPPORTED_CURRENCIES)), SATISPAY = new PaymentMethod(SATISPAY_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(SatispayAccount.SUPPORTED_CURRENCIES)), TIKKIE = new PaymentMethod(TIKKIE_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(TikkieAccount.SUPPORTED_CURRENCIES)), VERSE = new PaymentMethod(VERSE_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(VerseAccount.SUPPORTED_CURRENCIES)), STRIKE = new PaymentMethod(STRIKE_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(StrikeAccount.SUPPORTED_CURRENCIES)), SWIFT = new PaymentMethod(SWIFT_ID, 7 * DAY, DEFAULT_TRADE_LIMIT_MID_RISK, getAssetCodes(SwiftAccount.SUPPORTED_CURRENCIES)), ACH_TRANSFER = new PaymentMethod(ACH_TRANSFER_ID, 5 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(AchTransferAccount.SUPPORTED_CURRENCIES)), DOMESTIC_WIRE_TRANSFER = new PaymentMethod(DOMESTIC_WIRE_TRANSFER_ID, 3 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(DomesticWireTransferAccount.SUPPORTED_CURRENCIES)), PAYPAL = new PaymentMethod(PAYPAL_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(PayPalAccount.SUPPORTED_CURRENCIES)), CASH_APP = new PaymentMethod(CASH_APP_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(CashAppAccount.SUPPORTED_CURRENCIES)), PAYSAFE = new PaymentMethod(PaymentMethod.PAYSAFE_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(PaysafeAccount.SUPPORTED_CURRENCIES)), // Japan JAPAN_BANK = new PaymentMethod(JAPAN_BANK_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(JapanBankAccount.SUPPORTED_CURRENCIES)), // Australia AUSTRALIA_PAYID = new PaymentMethod(AUSTRALIA_PAYID_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(AustraliaPayidAccount.SUPPORTED_CURRENCIES)), // China ALI_PAY = new PaymentMethod(ALI_PAY_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(AliPayAccount.SUPPORTED_CURRENCIES)), WECHAT_PAY = new PaymentMethod(WECHAT_PAY_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(WeChatPayAccount.SUPPORTED_CURRENCIES)), // Thailand PROMPT_PAY = new PaymentMethod(PROMPT_PAY_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(PromptPayAccount.SUPPORTED_CURRENCIES)), // Cryptos BLOCK_CHAINS = new PaymentMethod(BLOCK_CHAINS_ID, DAY, DEFAULT_TRADE_LIMIT_CRYPTO, Arrays.asList()), // Cryptos with 1 hour trade period BLOCK_CHAINS_INSTANT = new PaymentMethod(BLOCK_CHAINS_INSTANT_ID, TimeUnit.HOURS.toMillis(1), DEFAULT_TRADE_LIMIT_CRYPTO, Arrays.asList()) ); // TODO: delete this override method, which overrides the paymentMethods variable, when all payment methods supported using structured form api, and make paymentMethods private public static List getPaymentMethods() { List paymentMethodIds = List.of( BLOCK_CHAINS_ID, CASH_AT_ATM_ID, FASTER_PAYMENTS_ID, F2F_ID, MONEY_GRAM_ID, PAXUM_ID, PAY_BY_MAIL_ID, REVOLUT_ID, SEPA_ID, SEPA_INSTANT_ID, STRIKE_ID, SWIFT_ID, TRANSFERWISE_ID, UPHOLD_ID, ZELLE_ID, AUSTRALIA_PAYID_ID, CASH_APP_ID, PAYPAL_ID, VENMO_ID, PAYSAFE_ID, WECHAT_PAY_ID, ALI_PAY_ID, SWISH_ID, TRANSFERWISE_USD_ID, AMAZON_GIFT_CARD_ID, ACH_TRANSFER_ID, INTERAC_E_TRANSFER_ID, US_POSTAL_MONEY_ORDER_ID, PIX_ID); return paymentMethods.stream().filter(paymentMethod -> paymentMethodIds.contains(paymentMethod.getId())).collect(Collectors.toList()); } private static List getAssetCodes(List tradeCurrencies) { return tradeCurrencies.stream().map(TradeCurrency::getCode).collect(Collectors.toList()); } static { paymentMethods.sort((o1, o2) -> { String id1 = o1.getId(); if (id1.equals(ZELLE_ID)) id1 = "ZELLE"; String id2 = o2.getId(); if (id2.equals(ZELLE_ID)) id2 = "ZELLE"; return id1.compareTo(id2); }); } public static PaymentMethod getDummyPaymentMethod(String id) { return new PaymentMethod(id, 0, BigInteger.ZERO, Arrays.asList()); } /////////////////////////////////////////////////////////////////////////////////////////// // Instance fields /////////////////////////////////////////////////////////////////////////////////////////// @Getter private final String id; // Must not change as old offers would get a new period then and that would violate the makers "contract" or // expectation when he created the offer. @Getter private final long maxTradePeriod; // With v0.9.4 we changed context of that field. Before it was the hard coded trade limit. Now it is the default // limit based on the risk factor. // The risk factor is derived from the maxTradeLimit. // As that field is used in protobuffer definitions we cannot change it to reflect better the new context. We prefer // to keep the convention that PB fields has the same name as the Java class field (as we could rename it in // Java without breaking PB). private final long maxTradeLimit; // list of asset codes the payment method supports @Getter private List supportedAssetCodes; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// /** * @param id against charge back risk. If Bank do the charge back quickly the Arbitrator and the seller can push another * double spend tx to invalidate the time locked payout tx. For the moment we set all to 0 but will have it in * place when needed. * @param maxTradePeriod The min. period a trader need to wait until he gets displayed the contact form for opening a dispute. * @param maxTradeLimit The max. allowed trade amount in Bitcoin for that payment method (depending on charge back risk) * @param supportedAssetCodes Supported asset codes. */ private PaymentMethod(String id, long maxTradePeriod, BigInteger maxTradeLimit, List supportedAssetCodes) { this.id = id; this.maxTradePeriod = maxTradePeriod; this.maxTradeLimit = maxTradeLimit.longValueExact(); this.supportedAssetCodes = supportedAssetCodes; } // Used for dummy entries in payment methods list (SHOW_ALL) private PaymentMethod(String id) { this(id, 0, BigInteger.ZERO, new ArrayList()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.PaymentMethod toProtoMessage() { return protobuf.PaymentMethod.newBuilder() .setId(id) .setMaxTradePeriod(maxTradePeriod) .setMaxTradeLimit(maxTradeLimit) .addAllSupportedAssetCodes(supportedAssetCodes) .build(); } public static PaymentMethod fromProto(protobuf.PaymentMethod proto) { return new PaymentMethod(proto.getId(), proto.getMaxTradePeriod(), BigInteger.valueOf(proto.getMaxTradeLimit()), proto.getSupportedAssetCodesList()); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public static PaymentMethod getPaymentMethod(String id) { return getActivePaymentMethod(id) .orElseThrow(() -> new IllegalArgumentException("Invalid payment method id: " + id)); } public static PaymentMethod getPaymentMethodOrNA(String id) { return getActivePaymentMethod(id) .orElseGet(() -> new PaymentMethod(Res.get("shared.na"))); } // We look up only our active payment methods not retired ones. public static Optional getActivePaymentMethod(String id) { return paymentMethods.stream() .filter(e -> e.getId().equals(id)) .findFirst(); } public BigInteger getMaxTradeLimit(String currencyCode) { // Hack for SF as the smallest unit is 1 SF ;-( and price is about 3 BTC! if (currencyCode.equals("SF")) return HavenoUtils.xmrToAtomicUnits(4); // payment methods which define their own trade limits if (id.equals(NEFT_ID) || id.equals(UPI_ID) || id.equals(PAYTM_ID) || id.equals(BIZUM_ID) || id.equals(TIKKIE_ID)) { return BigInteger.valueOf(maxTradeLimit); } // We use the class field maxTradeLimit only for mapping the risk factor. // The actual trade limit is calculated by dividing TradeLimits.MAX_TRADE_LIMIT by the // risk factor, and then further decreasing by chargeback risk, account signing, and age. long riskFactor; if (maxTradeLimit == DEFAULT_TRADE_LIMIT_CRYPTO.longValueExact()) riskFactor = 1; else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_VERY_LOW_RISK.longValueExact()) riskFactor = 4; else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_LOW_RISK.longValueExact()) riskFactor = 11; else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_MID_RISK.longValueExact()) riskFactor = 22; else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_HIGH_RISK.longValueExact()) riskFactor = 44; else { riskFactor = 44; log.warn("maxTradeLimit is not matching one of our default values. We use highest risk factor. " + "maxTradeLimit={}. PaymentMethod={}", maxTradeLimit, this); } // get risk based trade limit TradeLimits tradeLimits = new TradeLimits(); long maxTradeLimit = tradeLimits.getMaxTradeLimit().longValueExact(); long riskBasedTradeLimit = tradeLimits.getRoundedRiskBasedTradeLimit(maxTradeLimit, riskFactor); return BigInteger.valueOf(riskBasedTradeLimit); } public String getShortName() { // in cases where translation is not found, Res.get() simply returns the key string // so no need for special error-handling code. return Res.get(this.id + "_SHORT"); } @Override public int compareTo(@NotNull PaymentMethod other) { return Res.get(id).compareTo(Res.get(other.id)); } public String getDisplayString() { return Res.get(id); } public boolean isTraditional() { return !isCrypto(); } public boolean isBlockchain() { return this.equals(BLOCK_CHAINS_INSTANT) || this.equals(BLOCK_CHAINS); } // Includes any non btc asset, not limited to blockchain payment methods public boolean isCrypto() { return isBlockchain() || isBsqSwap(); } public boolean isBsqSwap() { return this.equals(BSQ_SWAP); } public static boolean hasChargebackRisk(PaymentMethod paymentMethod, List tradeCurrencies) { return tradeCurrencies.stream() .anyMatch(tradeCurrency -> hasChargebackRisk(paymentMethod, tradeCurrency.getCode())); } public static boolean hasChargebackRisk(PaymentMethod paymentMethod) { return hasChargebackRisk(paymentMethod, CurrencyUtil.getMatureMarketCurrencies()); } public static boolean hasChargebackRisk(PaymentMethod paymentMethod, String currencyCode) { if (paymentMethod == null) return false; String id = paymentMethod.getId(); return hasChargebackRisk(id, currencyCode); } public static boolean hasChargebackRisk(String id, String currencyCode) { // TODO: bisq indicates no chargeback risk for non-"mature" currencies, but they have chargeback risk too, so we disable // if (CurrencyUtil.getMatureMarketCurrencies().stream() // .noneMatch(c -> c.getCode().equals(currencyCode))) // return false; return id.equals(PaymentMethod.SEPA_ID) || id.equals(PaymentMethod.SEPA_INSTANT_ID) || id.equals(PaymentMethod.INTERAC_E_TRANSFER_ID) || id.equals(PaymentMethod.ZELLE_ID) || id.equals(PaymentMethod.REVOLUT_ID) || id.equals(PaymentMethod.NATIONAL_BANK_ID) || id.equals(PaymentMethod.SAME_BANK_ID) || id.equals(PaymentMethod.SPECIFIC_BANKS_ID) || id.equals(PaymentMethod.CHASE_QUICK_PAY_ID) || id.equals(PaymentMethod.POPMONEY_ID) || id.equals(PaymentMethod.MONEY_BEAM_ID) || id.equals(PaymentMethod.UPHOLD_ID) || id.equals(PaymentMethod.CASH_APP_ID) || id.equals(PaymentMethod.PAYPAL_ID) || id.equals(PaymentMethod.VENMO_ID) || id.equals(PaymentMethod.PAYSAFE_ID); } public static boolean isRoundedForAtmCash(String id) { return id.equals(PaymentMethod.CASH_AT_ATM_ID) || id.equals(PaymentMethod.HAL_CASH_ID); } public static boolean isFixedPriceOnly(String id) { return id.equals(PaymentMethod.HAL_CASH_ID); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/PaysafeAccountPayload.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class PaysafeAccountPayload extends PaymentAccountPayload { private String email = ""; public PaysafeAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private PaysafeAccountPayload(String paymentMethod, String id, String email, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.email = email; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setPaysafeAccountPayload(protobuf.PaysafeAccountPayload.newBuilder().setEmail(email)) .build(); } public static PaysafeAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new PaysafeAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getPaysafeAccountPayload().getEmail(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.email") + " " + email; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(email.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/PayseraAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class PayseraAccountPayload extends PaymentAccountPayload { private String email = ""; public PayseraAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private PayseraAccountPayload(String paymentMethod, String id, String email, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.email = email; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setPayseraAccountPayload(protobuf.PayseraAccountPayload.newBuilder().setEmail(email)) .build(); } public static PayseraAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new PayseraAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getPayseraAccountPayload().getEmail(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.email") + " " + email; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(email.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/PaytmAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class PaytmAccountPayload extends CountryBasedPaymentAccountPayload { private String emailOrMobileNr = ""; public PaytmAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } private PaytmAccountPayload(String paymentMethod, String id, String countryCode, List acceptedCountryCodes, String emailOrMobileNr, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, countryCode, acceptedCountryCodes, maxTradePeriod, excludeFromJsonDataMap); this.emailOrMobileNr = emailOrMobileNr; } @Override public Message toProtoMessage() { protobuf.PaytmAccountPayload.Builder builder = protobuf.PaytmAccountPayload.newBuilder() .setEmailOrMobileNr(emailOrMobileNr); final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setPaytmAccountPayload(builder); return getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) .build(); } public static PaytmAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); protobuf.PaytmAccountPayload paytmAccountPayloadPB = countryBasedPaymentAccountPayload.getPaytmAccountPayload(); return new PaytmAccountPayload(proto.getPaymentMethodId(), proto.getId(), countryBasedPaymentAccountPayload.getCountryCode(), new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), paytmAccountPayloadPB.getEmailOrMobileNr(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.email.mobile") + " " + emailOrMobileNr; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(emailOrMobileNr.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/PerfectMoneyAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class PerfectMoneyAccountPayload extends PaymentAccountPayload { private String accountNr = ""; public PerfectMoneyAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private PerfectMoneyAccountPayload(String paymentMethod, String id, String accountNr, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.accountNr = accountNr; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setPerfectMoneyAccountPayload(protobuf.PerfectMoneyAccountPayload.newBuilder() .setAccountNr(accountNr)) .build(); } public static PerfectMoneyAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new PerfectMoneyAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getPerfectMoneyAccountPayload().getAccountNr(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.no") + " " + accountNr; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(accountNr.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/PixAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.common.util.JsonExclude; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.lang3.ArrayUtils; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class PixAccountPayload extends CountryBasedPaymentAccountPayload { private String pixKey = ""; // This field is excluded for backward compatibility and to allow changes. @JsonExclude private String holderName = ""; public PixAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } private PixAccountPayload(String paymentMethod, String id, String countryCode, List acceptedCountryCodes, String pixKey, String holderName, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, countryCode, acceptedCountryCodes, maxTradePeriod, excludeFromJsonDataMap); this.pixKey = pixKey; this.holderName = holderName; } @Override public Message toProtoMessage() { protobuf.PixAccountPayload.Builder builder = protobuf.PixAccountPayload.newBuilder() .setPixKey(pixKey) .setHolderName(holderName); final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setPixAccountPayload(builder); return getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) .build(); } public static PixAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); protobuf.PixAccountPayload paytmAccountPayloadPB = countryBasedPaymentAccountPayload.getPixAccountPayload(); return new PixAccountPayload(proto.getPaymentMethodId(), proto.getId(), countryBasedPaymentAccountPayload.getCountryCode(), new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), paytmAccountPayloadPB.getPixKey(), paytmAccountPayloadPB.getHolderName(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + getPaymentDetailsForTradePopup().replace("\n", ", "); } @Override public String getPaymentDetailsForTradePopup() { return Res.getWithCol("payment.pix.key") + " " + pixKey + "\n" + Res.getWithCol("payment.account.owner.fullname") + " " + PaymentAccountPayload.getHolderNameOrPromptIfEmpty(getHolderName()); } @Override public byte[] getAgeWitnessInputData() { // holderName will be included as part of the witness data. // older accounts that don't have holderName still retain their existing witness. return super.getAgeWitnessInputData(ArrayUtils.addAll( pixKey.getBytes(StandardCharsets.UTF_8), getHolderName().getBytes(StandardCharsets.UTF_8))); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/PopmoneyAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class PopmoneyAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { private String accountId = ""; private String holderName = ""; public PopmoneyAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private PopmoneyAccountPayload(String paymentMethod, String id, String accountId, String holderName, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.accountId = accountId; this.holderName = holderName; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setPopmoneyAccountPayload(protobuf.PopmoneyAccountPayload.newBuilder() .setAccountId(accountId) .setHolderName(holderName)) .build(); } public static PopmoneyAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new PopmoneyAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getPopmoneyAccountPayload().getAccountId(), proto.getPopmoneyAccountPayload().getHolderName(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner.fullname") + " " + holderName + ", " + Res.getWithCol("payment.popmoney.accountId") + " " + accountId; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(accountId.getBytes(StandardCharsets.UTF_8)); } @Override public String getOwnerId() { return holderName; } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/PromptPayAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class PromptPayAccountPayload extends PaymentAccountPayload { private String promptPayId = ""; public PromptPayAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private PromptPayAccountPayload(String paymentMethod, String id, String promptPayId, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.promptPayId = promptPayId; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setPromptPayAccountPayload(protobuf.PromptPayAccountPayload.newBuilder() .setPromptPayId(promptPayId)) .build(); } public static PromptPayAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new PromptPayAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getPromptPayAccountPayload().getPromptPayId(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.getWithCol("payment.promptPay.promptPayId") + " " + promptPayId; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(promptPayId.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/RevolutAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.common.util.JsonExclude; import haveno.common.util.Tuple2; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import static com.google.common.base.Preconditions.checkArgument; @EqualsAndHashCode(callSuper = true) @ToString @Slf4j public final class RevolutAccountPayload extends PaymentAccountPayload { // Was added in 1.3.8 // To not break signed accounts we keep accountId as internal id used for signing. // Old accounts get a popup to add the new required field username but accountId is // left unchanged. Newly created accounts fill accountId with the value of username. // In the UI we only use username. // For backward compatibility we need to exclude the new field for the contract json. // We can remove that after a while when risk that users with pre 1.3.8 version trade with updated // users is very low. @JsonExclude @Getter private String username = ""; public RevolutAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private RevolutAccountPayload(String paymentMethod, String id, @Nullable String username, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.username = username; } @Override public Message toProtoMessage() { protobuf.RevolutAccountPayload.Builder revolutBuilder = protobuf.RevolutAccountPayload.newBuilder() .setUsername(username); return getPaymentAccountPayloadBuilder().setRevolutAccountPayload(revolutBuilder).build(); } public static RevolutAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.RevolutAccountPayload revolutAccountPayload = proto.getRevolutAccountPayload(); return new RevolutAccountPayload(proto.getPaymentMethodId(), proto.getId(), revolutAccountPayload.getUsername(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { Tuple2 tuple = getLabelValueTuple(); return Res.get(paymentMethodId) + " - " + tuple.first + ": " + tuple.second; } private Tuple2 getLabelValueTuple() { String label; String value; checkArgument(!username.isEmpty(), "Username must be set"); label = Res.get("payment.account.username"); value = username; return new Tuple2<>(label, value); } public Tuple2 getRecipientsAccountData() { Tuple2 tuple = getLabelValueTuple(); String label = Res.get("portfolio.pending.step2_buyer.recipientsAccountData", tuple.first); return new Tuple2<>(label, tuple.second); } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(username.getBytes(StandardCharsets.UTF_8)); } public boolean usernameNotSet() { return username.isEmpty(); } public void setUserName(String username) { this.username = username; } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/RtgsAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class RtgsAccountPayload extends IfscBasedAccountPayload { public RtgsAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } private RtgsAccountPayload(String paymentMethod, String id, String countryCode, List acceptedCountryCodes, String holderName, String accountNr, String ifsc, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, countryCode, acceptedCountryCodes, holderName, accountNr, ifsc, maxTradePeriod, excludeFromJsonDataMap); } @Override public Message toProtoMessage() { protobuf.IfscBasedAccountPayload.Builder ifscBasedAccountPayloadBuilder = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .getIfscBasedAccountPayloadBuilder() .setRtgsAccountPayload(protobuf.RtgsAccountPayload.newBuilder()); protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayloadBuilder = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setIfscBasedAccountPayload(ifscBasedAccountPayloadBuilder); return getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayloadBuilder) .build(); } public static RtgsAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); protobuf.IfscBasedAccountPayload ifscBasedAccountPayloadPB = countryBasedPaymentAccountPayload.getIfscBasedAccountPayload(); return new RtgsAccountPayload(proto.getPaymentMethodId(), proto.getId(), countryBasedPaymentAccountPayload.getCountryCode(), new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), ifscBasedAccountPayloadPB.getHolderName(), ifscBasedAccountPayloadPB.getAccountNr(), ifscBasedAccountPayloadPB.getIfsc(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner.fullname") + " " + holderName + ", " + Res.getWithCol("payment.account.no") + " " + accountNr + Res.getWithCol("payment.ifsc") + " " + ifsc; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { String accountNr = this.accountNr == null ? "" : this.accountNr; return super.getAgeWitnessInputData(accountNr.getBytes(StandardCharsets.UTF_8)); } @Override public String getHolderName() { return getOwnerId(); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/SameBankAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Slf4j public final class SameBankAccountPayload extends BankAccountPayload { public SameBankAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private SameBankAccountPayload(String paymentMethodName, String id, String countryCode, List acceptedCountryCodes, String holderName, String bankName, String branchId, String accountNr, String accountType, String holderTaxId, String bankId, String nationalAccountId, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethodName, id, countryCode, acceptedCountryCodes, holderName, bankName, branchId, accountNr, accountType, holderTaxId, bankId, nationalAccountId, maxTradePeriod, excludeFromJsonDataMap); } @Override public Message toProtoMessage() { protobuf.BankAccountPayload.Builder bankAccountPayloadBuilder = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .getBankAccountPayloadBuilder() .setSameBankAccontPayload(protobuf.SameBankAccountPayload.newBuilder()); protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayloadBuilder = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setBankAccountPayload(bankAccountPayloadBuilder); return getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayloadBuilder) .build(); } public static SameBankAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); protobuf.BankAccountPayload bankAccountPayload = countryBasedPaymentAccountPayload.getBankAccountPayload(); return new SameBankAccountPayload(proto.getPaymentMethodId(), proto.getId(), countryBasedPaymentAccountPayload.getCountryCode(), new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), bankAccountPayload.getHolderName(), bankAccountPayload.getBankName().isEmpty() ? null : bankAccountPayload.getBankName(), bankAccountPayload.getBranchId().isEmpty() ? null : bankAccountPayload.getBranchId(), bankAccountPayload.getAccountNr().isEmpty() ? null : bankAccountPayload.getAccountNr(), bankAccountPayload.getAccountType().isEmpty() ? null : bankAccountPayload.getAccountType(), bankAccountPayload.getHolderTaxId().isEmpty() ? null : bankAccountPayload.getHolderTaxId(), bankAccountPayload.getBankId().isEmpty() ? null : bankAccountPayload.getBankId(), bankAccountPayload.getNationalAccountId().isEmpty() ? null : bankAccountPayload.getNationalAccountId(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + getPaymentDetailsForTradePopup().replace("\n", ", "); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/SatispayAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class SatispayAccountPayload extends CountryBasedPaymentAccountPayload { private String holderName = ""; private String mobileNr = ""; public SatispayAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } private SatispayAccountPayload(String paymentMethod, String id, String countryCode, List acceptedCountryCodes, String holderName, String mobileNr, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, countryCode, acceptedCountryCodes, maxTradePeriod, excludeFromJsonDataMap); this.holderName = holderName; this.mobileNr = mobileNr; } @Override public Message toProtoMessage() { protobuf.SatispayAccountPayload.Builder builder = protobuf.SatispayAccountPayload.newBuilder() .setHolderName(holderName) .setMobileNr(mobileNr); final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setSatispayAccountPayload(builder); return getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) .build(); } public static SatispayAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); protobuf.SatispayAccountPayload accountPayloadPB = countryBasedPaymentAccountPayload.getSatispayAccountPayload(); return new SatispayAccountPayload(proto.getPaymentMethodId(), proto.getId(), countryBasedPaymentAccountPayload.getCountryCode(), new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), accountPayloadPB.getHolderName(), accountPayloadPB.getMobileNr(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.username") + " " + holderName; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(holderName.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/SepaAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @EqualsAndHashCode(callSuper = true) @ToString @Getter @Slf4j public final class SepaAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName { @Setter private String holderName = ""; @Setter private String iban = ""; @Setter private String bic = ""; private String email = ""; // not used anymore but need to keep it for backward compatibility, must not be null but empty string, otherwise hash check fails for contract // Don't use a set here as we need a deterministic ordering, otherwise the contract hash does not match private final List persistedAcceptedCountryCodes = new ArrayList<>(); public SepaAccountPayload(String paymentMethod, String id, List acceptedCountries) { super(paymentMethod, id); acceptedCountryCodes = acceptedCountries.stream() .map(e -> e.code) .sorted() .distinct() .collect(Collectors.toList()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private SepaAccountPayload(String paymentMethodName, String id, String countryCode, List acceptedCountryCodes, String holderName, String iban, String bic, String email, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethodName, id, countryCode, acceptedCountryCodes, maxTradePeriod, excludeFromJsonDataMap); this.holderName = holderName; this.iban = iban; this.bic = bic; this.email = email; this.acceptedCountryCodes = acceptedCountryCodes; persistedAcceptedCountryCodes.addAll(acceptedCountryCodes); } @Override public Message toProtoMessage() { protobuf.SepaAccountPayload.Builder builder = protobuf.SepaAccountPayload.newBuilder() .setHolderName(holderName) .setIban(iban) .setBic(bic) .setEmail(email); final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setSepaAccountPayload(builder); return getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) .build(); } public static PaymentAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); protobuf.SepaAccountPayload sepaAccountPayloadPB = countryBasedPaymentAccountPayload.getSepaAccountPayload(); return new SepaAccountPayload(proto.getPaymentMethodId(), proto.getId(), countryBasedPaymentAccountPayload.getCountryCode(), new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), sepaAccountPayloadPB.getHolderName(), sepaAccountPayloadPB.getIban(), sepaAccountPayloadPB.getBic(), sepaAccountPayloadPB.getEmail(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public void setAcceptedCountryCodes(List acceptedCountryCodes) { this.acceptedCountryCodes.clear(); for (String countryCode : acceptedCountryCodes) this.acceptedCountryCodes.add(countryCode); } public void addAcceptedCountry(String countryCode) { if (!acceptedCountryCodes.contains(countryCode)) acceptedCountryCodes.add(countryCode); } public void removeAcceptedCountry(String countryCode) { acceptedCountryCodes.remove(countryCode); } public void onPersistChanges() { persistedAcceptedCountryCodes.clear(); persistedAcceptedCountryCodes.addAll(acceptedCountryCodes); } public void revertChanges() { acceptedCountryCodes.clear(); acceptedCountryCodes.addAll(persistedAcceptedCountryCodes); } @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner.fullname") + " " + holderName + ", IBAN: " + iban + ", BIC: " + bic + ", " + Res.getWithCol("payment.bank.country") + " " + getCountryCode(); } @Override public String getPaymentDetailsForTradePopup() { return Res.getWithCol("payment.account.owner.fullname") + " " + holderName + "\n" + "IBAN: " + iban + "\n" + "BIC: " + bic + "\n" + Res.getWithCol("payment.bank.country") + " " + CountryUtil.getNameByCode(countryCode); } @Override public byte[] getAgeWitnessInputData() { // We don't add holderName because we don't want to break age validation if the user recreates an account with // slight changes in holder name (e.g. add or remove middle name) return super.getAgeWitnessInputData(ArrayUtils.addAll(iban.getBytes(StandardCharsets.UTF_8), bic.getBytes(StandardCharsets.UTF_8))); } @Override public String getOwnerId() { return holderName; } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/SepaInstantAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @EqualsAndHashCode(callSuper = true) @ToString @Getter @Slf4j public final class SepaInstantAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName { @Setter private String holderName = ""; @Setter private String iban = ""; @Setter private String bic = ""; // Don't use a set here as we need a deterministic ordering, otherwise the contract hash does not match private final List persistedAcceptedCountryCodes = new ArrayList<>(); public SepaInstantAccountPayload(String paymentMethod, String id, List acceptedCountries) { super(paymentMethod, id); acceptedCountryCodes = acceptedCountries.stream() .map(e -> e.code) .sorted() .distinct() .collect(Collectors.toList()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private SepaInstantAccountPayload(String paymentMethodName, String id, String countryCode, List acceptedCountryCodes, String holderName, String iban, String bic, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethodName, id, countryCode, acceptedCountryCodes, maxTradePeriod, excludeFromJsonDataMap); this.holderName = holderName; this.iban = iban; this.bic = bic; persistedAcceptedCountryCodes.addAll(acceptedCountryCodes); } @Override public Message toProtoMessage() { protobuf.SepaInstantAccountPayload.Builder builder = protobuf.SepaInstantAccountPayload.newBuilder() .setHolderName(holderName) .setIban(iban) .setBic(bic); final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setSepaInstantAccountPayload(builder); return getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) .build(); } public static PaymentAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); protobuf.SepaInstantAccountPayload sepaInstantAccountPayloadPB = countryBasedPaymentAccountPayload.getSepaInstantAccountPayload(); return new SepaInstantAccountPayload(proto.getPaymentMethodId(), proto.getId(), countryBasedPaymentAccountPayload.getCountryCode(), new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), sepaInstantAccountPayloadPB.getHolderName(), sepaInstantAccountPayloadPB.getIban(), sepaInstantAccountPayloadPB.getBic(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void addAcceptedCountry(String countryCode) { if (!acceptedCountryCodes.contains(countryCode)) acceptedCountryCodes.add(countryCode); } public void removeAcceptedCountry(String countryCode) { acceptedCountryCodes.remove(countryCode); } public void onPersistChanges() { persistedAcceptedCountryCodes.clear(); persistedAcceptedCountryCodes.addAll(acceptedCountryCodes); } public void revertChanges() { acceptedCountryCodes.clear(); acceptedCountryCodes.addAll(persistedAcceptedCountryCodes); } @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner.fullname") + " " + holderName + ", IBAN: " + iban + ", BIC: " + bic + ", " + Res.getWithCol("payment.bank.country") + " " + getCountryCode(); } @Override public String getPaymentDetailsForTradePopup() { return Res.getWithCol("payment.account.owner.fullname") + " " + holderName + "\n" + "IBAN: " + iban + "\n" + "BIC: " + bic + "\n" + Res.getWithCol("payment.bank.country") + " " + CountryUtil.getNameByCode(countryCode); } @Override public byte[] getAgeWitnessInputData() { // We don't add holderName because we don't want to break age validation if the user recreates an account with // slight changes in holder name (e.g. add or remove middle name) return super.getAgeWitnessInputData(ArrayUtils.addAll(iban.getBytes(StandardCharsets.UTF_8), bic.getBytes(StandardCharsets.UTF_8))); } @Override public String getOwnerId() { return holderName; } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/SpecificBanksAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.common.base.Joiner; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Getter @Slf4j public final class SpecificBanksAccountPayload extends BankAccountPayload { // Don't use a set here as we need a deterministic ordering, otherwise the contract hash does not match private ArrayList acceptedBanks = new ArrayList<>(); public SpecificBanksAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private SpecificBanksAccountPayload(String paymentMethodName, String id, String countryCode, List acceptedCountryCodes, String holderName, String bankName, String branchId, String accountNr, String accountType, String holderTaxId, String bankId, String nationalAccountId, ArrayList acceptedBanks, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethodName, id, countryCode, acceptedCountryCodes, holderName, bankName, branchId, accountNr, accountType, holderTaxId, bankId, nationalAccountId, maxTradePeriod, excludeFromJsonDataMap); this.acceptedBanks = acceptedBanks; } @Override public Message toProtoMessage() { final protobuf.SpecificBanksAccountPayload.Builder builder = protobuf.SpecificBanksAccountPayload.newBuilder() .addAllAcceptedBanks(acceptedBanks); protobuf.BankAccountPayload.Builder bankAccountPayloadBuilder = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .getBankAccountPayloadBuilder() .setSpecificBanksAccountPayload(builder); protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayloadBuilder = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setBankAccountPayload(bankAccountPayloadBuilder); return getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayloadBuilder) .build(); } public static SpecificBanksAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); protobuf.BankAccountPayload bankAccountPayload = countryBasedPaymentAccountPayload.getBankAccountPayload(); protobuf.SpecificBanksAccountPayload specificBanksAccountPayload = bankAccountPayload.getSpecificBanksAccountPayload(); return new SpecificBanksAccountPayload(proto.getPaymentMethodId(), proto.getId(), countryBasedPaymentAccountPayload.getCountryCode(), new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), bankAccountPayload.getHolderName(), bankAccountPayload.getBankName().isEmpty() ? null : bankAccountPayload.getBankName(), bankAccountPayload.getBranchId().isEmpty() ? null : bankAccountPayload.getBranchId(), bankAccountPayload.getAccountNr().isEmpty() ? null : bankAccountPayload.getAccountNr(), bankAccountPayload.getAccountType().isEmpty() ? null : bankAccountPayload.getAccountType(), bankAccountPayload.getHolderTaxId().isEmpty() ? null : bankAccountPayload.getHolderTaxId(), bankAccountPayload.getBankId().isEmpty() ? null : bankAccountPayload.getBankId(), bankAccountPayload.getNationalAccountId().isEmpty() ? null : bankAccountPayload.getNationalAccountId(), new ArrayList<>(specificBanksAccountPayload.getAcceptedBanksList()), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void clearAcceptedBanks() { acceptedBanks = new ArrayList<>(); } public void addAcceptedBank(String bankName) { if (!acceptedBanks.contains(bankName)) acceptedBanks.add(bankName); } @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + getPaymentDetailsForTradePopup().replace("\n", ", "); } @Override public String getPaymentDetailsForTradePopup() { return super.getPaymentDetailsForTradePopup() + "\n" + Res.getWithCol("payment.accepted.banks") + " " + Joiner.on(", ").join(acceptedBanks); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/StrikeAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class StrikeAccountPayload extends CountryBasedPaymentAccountPayload { private String holderName = ""; public StrikeAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } private StrikeAccountPayload(String paymentMethod, String id, String countryCode, List acceptedCountryCodes, String holderName, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, countryCode, acceptedCountryCodes, maxTradePeriod, excludeFromJsonDataMap); this.holderName = holderName; } @Override public Message toProtoMessage() { protobuf.StrikeAccountPayload.Builder builder = protobuf.StrikeAccountPayload.newBuilder() .setHolderName(holderName); final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setStrikeAccountPayload(builder); return getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) .build(); } public static StrikeAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); protobuf.StrikeAccountPayload accountPayloadPB = countryBasedPaymentAccountPayload.getStrikeAccountPayload(); return new StrikeAccountPayload(proto.getPaymentMethodId(), proto.getId(), countryBasedPaymentAccountPayload.getCountryCode(), new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), accountPayloadPB.getHolderName(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.username") + " " + holderName; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(holderName.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/SwiftAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class SwiftAccountPayload extends PaymentAccountPayload { // payload data elements private String bankSwiftCode = ""; private String bankCountryCode = ""; private String bankName = ""; private String bankBranch = ""; private String bankAddress = ""; private String beneficiaryName = ""; private String beneficiaryAccountNr = ""; private String beneficiaryAddress = ""; private String beneficiaryCity = ""; private String beneficiaryPhone = ""; private String specialInstructions = ""; private String intermediarySwiftCode = ""; private String intermediaryCountryCode = ""; private String intermediaryName = ""; private String intermediaryBranch = ""; private String intermediaryAddress = ""; // constants public static final String BANKPOSTFIX = ".bank"; public static final String INTERMEDIARYPOSTFIX = ".intermediary"; public static final String BENEFICIARYPOSTFIX = ".beneficiary"; public static final String SWIFT_CODE = "payment.swift.swiftCode"; public static final String COUNTRY = "payment.swift.country"; public static final String SWIFT_ACCOUNT = "payment.swift.account"; public static final String SNAME = "payment.swift.name"; public static final String BRANCH = "payment.swift.branch"; public static final String ADDRESS = "payment.swift.address"; public static final String PHONE = "payment.swift.phone"; public SwiftAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } private SwiftAccountPayload(String paymentMethod, String id, String bankSwiftCode, String bankCountryCode, String bankName, String bankBranch, String bankAddress, String beneficiaryName, String beneficiaryAccountNr, String beneficiaryAddress, String beneficiaryCity, String beneficiaryPhone, String specialInstructions, String intermediarySwiftCode, String intermediaryCountryCode, String intermediaryName, String intermediaryBranch, String intermediaryAddress, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.bankSwiftCode = bankSwiftCode; this.bankCountryCode = bankCountryCode; this.bankName = bankName; this.bankBranch = bankBranch; this.bankAddress = bankAddress; this.beneficiaryName = beneficiaryName; this.beneficiaryAccountNr = beneficiaryAccountNr; this.beneficiaryAddress = beneficiaryAddress; this.beneficiaryCity = beneficiaryCity; this.beneficiaryPhone = beneficiaryPhone; this.specialInstructions = specialInstructions; this.intermediarySwiftCode = intermediarySwiftCode; this.intermediaryCountryCode = intermediaryCountryCode; this.intermediaryName = intermediaryName; this.intermediaryBranch = intermediaryBranch; this.intermediaryAddress = intermediaryAddress; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setSwiftAccountPayload(protobuf.SwiftAccountPayload.newBuilder() .setBankSwiftCode(bankSwiftCode) .setBankCountryCode(bankCountryCode) .setBankName(bankName) .setBankBranch(bankBranch) .setBankAddress(bankAddress) .setBeneficiaryName(beneficiaryName) .setBeneficiaryAccountNr(beneficiaryAccountNr) .setBeneficiaryAddress(beneficiaryAddress) .setBeneficiaryCity(beneficiaryCity) .setBeneficiaryPhone(beneficiaryPhone) .setSpecialInstructions(specialInstructions) .setIntermediarySwiftCode(intermediarySwiftCode) .setIntermediaryCountryCode(intermediaryCountryCode) .setIntermediaryName(intermediaryName) .setIntermediaryBranch(intermediaryBranch) .setIntermediaryAddress(intermediaryAddress) ) .build(); } public static SwiftAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.SwiftAccountPayload x = proto.getSwiftAccountPayload(); return new SwiftAccountPayload(proto.getPaymentMethodId(), proto.getId(), x.getBankSwiftCode(), x.getBankCountryCode(), x.getBankName(), x.getBankBranch(), x.getBankAddress(), x.getBeneficiaryName(), x.getBeneficiaryAccountNr(), x.getBeneficiaryAddress(), x.getBeneficiaryCity(), x.getBeneficiaryPhone(), x.getSpecialInstructions(), x.getIntermediarySwiftCode(), x.getIntermediaryCountryCode(), x.getIntermediaryName(), x.getIntermediaryBranch(), x.getIntermediaryAddress(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + beneficiaryName; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(beneficiaryAccountNr.getBytes(StandardCharsets.UTF_8)); } public boolean usesIntermediaryBank() { return (intermediarySwiftCode != null && intermediarySwiftCode.length() > 0); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/SwishAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class SwishAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { private String mobileNr = ""; private String holderName = ""; public SwishAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private SwishAccountPayload(String paymentMethod, String id, String mobileNr, String holderName, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.mobileNr = mobileNr; this.holderName = holderName; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setSwishAccountPayload(protobuf.SwishAccountPayload.newBuilder() .setMobileNr(mobileNr) .setHolderName(holderName)) .build(); } public static SwishAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new SwishAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getSwishAccountPayload().getMobileNr(), proto.getSwishAccountPayload().getHolderName(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner.fullname") + " " + holderName + ", " + Res.getWithCol("payment.mobile") + " " + mobileNr; } @Override public String getPaymentDetailsForTradePopup() { return Res.getWithCol("payment.account.owner.fullname") + " " + holderName + "\n" + Res.getWithCol("payment.mobile") + " " + mobileNr; } @Override public byte[] getAgeWitnessInputData() { // We don't add holderName because we don't want to break age validation if the user recreates an account with // slight changes in holder name (e.g. add or remove middle name) return super.getAgeWitnessInputData(mobileNr.getBytes(StandardCharsets.UTF_8)); } @Override public String getOwnerId() { return holderName; } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/TikkieAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class TikkieAccountPayload extends CountryBasedPaymentAccountPayload { private String iban = ""; public TikkieAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } private TikkieAccountPayload(String paymentMethod, String id, String countryCode, List acceptedCountryCodes, String iban, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, countryCode, acceptedCountryCodes, maxTradePeriod, excludeFromJsonDataMap); this.iban = iban; } @Override public Message toProtoMessage() { protobuf.TikkieAccountPayload.Builder builder = protobuf.TikkieAccountPayload.newBuilder() .setIban(iban); final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setTikkieAccountPayload(builder); return getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) .build(); } public static TikkieAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); protobuf.TikkieAccountPayload accountPayloadPB = countryBasedPaymentAccountPayload.getTikkieAccountPayload(); return new TikkieAccountPayload(proto.getPaymentMethodId(), proto.getId(), countryBasedPaymentAccountPayload.getCountryCode(), new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), accountPayloadPB.getIban(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.iban") + " " + iban; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(iban.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/TransferwiseAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.common.util.JsonExclude; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import org.apache.commons.lang3.ArrayUtils; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class TransferwiseAccountPayload extends PaymentAccountPayload { private String email = ""; // This field is excluded for backward compatibility and to allow changes. @JsonExclude private String holderName = ""; public TransferwiseAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private TransferwiseAccountPayload(String paymentMethod, String id, String email, String holderName, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.email = email; this.holderName = holderName; } @Override public Message toProtoMessage() { protobuf.TransferwiseAccountPayload.Builder builder = protobuf.TransferwiseAccountPayload.newBuilder() .setEmail(email) .setHolderName(holderName); return getPaymentAccountPayloadBuilder() .setTransferwiseAccountPayload(builder) .build(); } public static TransferwiseAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new TransferwiseAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getTransferwiseAccountPayload().getEmail(), proto.getTransferwiseAccountPayload().getHolderName(), 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") + " " + email + "\n" + Res.getWithCol("payment.account.owner.fullname") + " " + PaymentAccountPayload.getHolderNameOrPromptIfEmpty(getHolderName()); } @Override public byte[] getAgeWitnessInputData() { // holderName will be included as part of the witness data. // older accounts that don't have holderName still retain their existing witness. return super.getAgeWitnessInputData(ArrayUtils.addAll( email.getBytes(StandardCharsets.UTF_8), getHolderName().getBytes(StandardCharsets.UTF_8))); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/TransferwiseUsdAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class TransferwiseUsdAccountPayload extends CountryBasedPaymentAccountPayload { private String email = ""; private String holderName = ""; private String holderAddress = ""; public TransferwiseUsdAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } private TransferwiseUsdAccountPayload(String paymentMethod, String id, String countryCode, List acceptedCountryCodes, String email, String holderName, String holderAddress, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, countryCode, acceptedCountryCodes, maxTradePeriod, excludeFromJsonDataMap); this.email = email; this.holderName = holderName; this.holderAddress = holderAddress; } @Override public Message toProtoMessage() { protobuf.TransferwiseUsdAccountPayload.Builder builder = protobuf.TransferwiseUsdAccountPayload.newBuilder() .setEmail(email) .setHolderName(holderName) .setHolderAddress(holderAddress); final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setTransferwiseUsdAccountPayload(builder); return getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) .build(); } public static TransferwiseUsdAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); protobuf.TransferwiseUsdAccountPayload accountPayloadPB = countryBasedPaymentAccountPayload.getTransferwiseUsdAccountPayload(); return new TransferwiseUsdAccountPayload(proto.getPaymentMethodId(), proto.getId(), countryBasedPaymentAccountPayload.getCountryCode(), new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), accountPayloadPB.getEmail(), accountPayloadPB.getHolderName(), accountPayloadPB.getHolderAddress(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.username") + " " + holderName; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(holderName.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/USPostalMoneyOrderAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class USPostalMoneyOrderAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { private String postalAddress = ""; private String holderName = ""; public USPostalMoneyOrderAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private USPostalMoneyOrderAccountPayload(String paymentMethod, String id, String postalAddress, String holderName, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.postalAddress = postalAddress; this.holderName = holderName; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setUSPostalMoneyOrderAccountPayload(protobuf.USPostalMoneyOrderAccountPayload.newBuilder() .setPostalAddress(postalAddress) .setHolderName(holderName)) .build(); } public static USPostalMoneyOrderAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new USPostalMoneyOrderAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getUSPostalMoneyOrderAccountPayload().getPostalAddress(), proto.getUSPostalMoneyOrderAccountPayload().getHolderName(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner.fullname") + " " + holderName + ", " + Res.getWithCol("payment.postal.address") + " " + postalAddress; } @Override public String getPaymentDetailsForTradePopup() { return Res.getWithCol("payment.account.owner.fullname") + " " + holderName + "\n" + Res.getWithCol("payment.postal.address") + " " + postalAddress; } @Override public byte[] getAgeWitnessInputData() { // We use here the holderName because the address alone seems to be too weak return super.getAgeWitnessInputData(ArrayUtils.addAll(holderName.getBytes(StandardCharsets.UTF_8), postalAddress.getBytes(StandardCharsets.UTF_8))); } @Override public String getOwnerId() { return holderName; } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/UpholdAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class UpholdAccountPayload extends PaymentAccountPayload { private String accountId = ""; private String accountOwner = ""; public UpholdAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private UpholdAccountPayload(String paymentMethod, String id, String accountId, String accountOwner, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.accountId = accountId; this.accountOwner = accountOwner; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setUpholdAccountPayload(protobuf.UpholdAccountPayload.newBuilder() .setAccountOwner(accountOwner) .setAccountId(accountId)) .build(); } public static UpholdAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new UpholdAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getUpholdAccountPayload().getAccountId(), proto.getUpholdAccountPayload().getAccountOwner(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + getPaymentDetailsForTradePopup().replace("\n", ", "); } @Override public String getPaymentDetailsForTradePopup() { if (accountOwner.isEmpty()) { return Res.get("payment.account") + ": " + accountId + "\n" + Res.get("payment.account.owner.fullname") + ": N/A"; } else { return Res.get("payment.account") + ": " + accountId + "\n" + Res.get("payment.account.owner.fullname") + ": " + accountOwner; } } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(accountId.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/UpiAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class UpiAccountPayload extends CountryBasedPaymentAccountPayload { private String virtualPaymentAddress = ""; public UpiAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } private UpiAccountPayload(String paymentMethod, String id, String countryCode, List acceptedCountryCodes, String virtualPaymentAddress, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, countryCode, acceptedCountryCodes, maxTradePeriod, excludeFromJsonDataMap); this.virtualPaymentAddress = virtualPaymentAddress; } @Override public Message toProtoMessage() { protobuf.UpiAccountPayload.Builder builder = protobuf.UpiAccountPayload.newBuilder() .setVirtualPaymentAddress(virtualPaymentAddress); final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setUpiAccountPayload(builder); return getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) .build(); } public static UpiAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); protobuf.UpiAccountPayload upiAccountPayloadPB = countryBasedPaymentAccountPayload.getUpiAccountPayload(); return new UpiAccountPayload(proto.getPaymentMethodId(), proto.getId(), countryBasedPaymentAccountPayload.getCountryCode(), new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), upiAccountPayloadPB.getVirtualPaymentAddress(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.upi.virtualPaymentAddress") + " " + virtualPaymentAddress; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(virtualPaymentAddress.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/VenmoAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class VenmoAccountPayload extends PaymentAccountPayload { private String emailOrMobileNrOrUsername = ""; public VenmoAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private VenmoAccountPayload(String paymentMethod, String id, String emailOrMobileNrOrUsername, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.emailOrMobileNrOrUsername = emailOrMobileNrOrUsername; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setVenmoAccountPayload(protobuf.VenmoAccountPayload.newBuilder() .setEmailOrMobileNrOrUsername(emailOrMobileNrOrUsername)) .build(); } public static VenmoAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new VenmoAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getVenmoAccountPayload().getEmailOrMobileNrOrUsername(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.getWithCol("payment.email.mobile.username") + " " + emailOrMobileNrOrUsername; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(emailOrMobileNrOrUsername.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/VerseAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class VerseAccountPayload extends PaymentAccountPayload { private String holderName = ""; public VerseAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } private VerseAccountPayload(String paymentMethod, String id, String holderName, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.holderName = holderName; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setVerseAccountPayload(protobuf.VerseAccountPayload.newBuilder().setHolderName(holderName)) .build(); } public static VerseAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new VerseAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getVerseAccountPayload().getHolderName(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.username") + " " + holderName; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(holderName.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/WeChatPayAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @Getter @Setter @ToString public final class WeChatPayAccountPayload extends PaymentAccountPayload { private String accountNr = ""; public WeChatPayAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private WeChatPayAccountPayload(String paymentMethod, String id, String accountNr, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.accountNr = accountNr; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setWeChatPayAccountPayload(protobuf.WeChatPayAccountPayload.newBuilder() .setAccountNr(accountNr)) .build(); } public static WeChatPayAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new WeChatPayAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getWeChatPayAccountPayload().getAccountNr(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.no") + " " + accountNr; } @Override public String getPaymentDetailsForTradePopup() { return getPaymentDetails(); } @Override public byte[] getAgeWitnessInputData() { return super.getAgeWitnessInputData(accountNr.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/WesternUnionAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.BankUtil; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public class WesternUnionAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName { private String holderName; private String city; private String state = ""; // is optional. we don't use @Nullable because it would makes UI code more complex. private String email; public WesternUnionAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private WesternUnionAccountPayload(String paymentMethodName, String id, String countryCode, List acceptedCountryCodes, String holderName, String city, String state, String email, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethodName, id, countryCode, acceptedCountryCodes, maxTradePeriod, excludeFromJsonDataMap); this.holderName = holderName; this.city = city; this.state = state; this.email = email; } @Override public Message toProtoMessage() { protobuf.WesternUnionAccountPayload.Builder builder = protobuf.WesternUnionAccountPayload.newBuilder() .setHolderName(holderName) .setCity(city) .setState(state) .setEmail(email); final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setWesternUnionAccountPayload(builder); return getPaymentAccountPayloadBuilder() .setCountryBasedPaymentAccountPayload(countryBasedPaymentAccountPayload) .build(); } public static PaymentAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { protobuf.CountryBasedPaymentAccountPayload countryBasedPaymentAccountPayload = proto.getCountryBasedPaymentAccountPayload(); protobuf.WesternUnionAccountPayload westernUnionAccountPayload = countryBasedPaymentAccountPayload.getWesternUnionAccountPayload(); return new WesternUnionAccountPayload(proto.getPaymentMethodId(), proto.getId(), countryBasedPaymentAccountPayload.getCountryCode(), new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), westernUnionAccountPayload.getHolderName(), westernUnionAccountPayload.getCity(), westernUnionAccountPayload.getState(), westernUnionAccountPayload.getEmail(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + getPaymentDetailsForTradePopup().replace("\n", ", "); } @Override public String getPaymentDetailsForTradePopup() { String cityState = BankUtil.isStateRequired(countryCode) ? (Res.get("payment.account.city") + " / " + Res.getWithCol("payment.account.state") + " " + city + " / " + state + "\n") : (Res.getWithCol("payment.account.city") + " " + city + "\n"); return Res.getWithCol("payment.account.fullName") + " " + holderName + "\n" + cityState + Res.getWithCol("payment.country") + " " + CountryUtil.getNameByCode(countryCode) + "\n" + Res.getWithCol("payment.email") + " " + email; } @Override public byte[] getAgeWitnessInputData() { String all = this.countryCode + this.holderName + this.email; return super.getAgeWitnessInputData(all.getBytes(StandardCharsets.UTF_8)); } } ================================================ FILE: core/src/main/java/haveno/core/payment/payload/ZelleAccountPayload.java ================================================ /* * 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 . */ package haveno.core.payment.payload; import com.google.protobuf.Message; import haveno.core.locale.Res; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @EqualsAndHashCode(callSuper = true) @ToString @Setter @Getter @Slf4j public final class ZelleAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { private String emailOrMobileNr = ""; private String holderName = ""; public ZelleAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private ZelleAccountPayload(String paymentMethod, String id, String emailOrMobileNr, String holderName, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, id, maxTradePeriod, excludeFromJsonDataMap); this.emailOrMobileNr = emailOrMobileNr; this.holderName = holderName; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setZelleAccountPayload(protobuf.ZelleAccountPayload.newBuilder() .setEmailOrMobileNr(emailOrMobileNr) .setHolderName(holderName)) .build(); } public static ZelleAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new ZelleAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getZelleAccountPayload().getEmailOrMobileNr(), proto.getZelleAccountPayload().getHolderName(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner.fullname") + " " + holderName + ", " + Res.getWithCol("payment.emailOrMobile") + " " + emailOrMobileNr; } @Override public String getPaymentDetailsForTradePopup() { return Res.getWithCol("payment.account.owner.fullname") + " " + holderName + "\n" + Res.getWithCol("payment.emailOrMobile") + " " + emailOrMobileNr; } @Override public byte[] getAgeWitnessInputData() { // We don't add holderName because we don't want to break age validation if the user recreates an account with // slight changes in holder name (e.g. add or remove middle name) return super.getAgeWitnessInputData(emailOrMobileNr.getBytes(StandardCharsets.UTF_8)); } @Override public String getOwnerId() { return holderName; } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/AccountNrValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.core.locale.BankUtil; import haveno.core.locale.Res; import org.apache.commons.lang3.StringUtils; public final class AccountNrValidator extends BankValidator { public AccountNrValidator(String countryCode) { super(countryCode); } @Override public ValidationResult validate(String input) { int length; String input2; switch (countryCode) { case "GB": length = 8; if (isNumberWithFixedLength(input, length)) return super.validate(input); else return new ValidationResult(false, Res.get("validation.accountNr", length)); case "US": if (isNumberInRange(input, 4, 17)) return super.validate(input); else return new ValidationResult(false, Res.get("validation.accountNr", "4 - 17")); case "BR": if (isStringInRange(input, 1, 20)) return super.validate(input); else return new ValidationResult(false, Res.get("validation.accountNrChars", "1 - 20")); case "NZ": input2 = input != null ? input.replaceAll("-", "") : null; if (isNumberInRange(input2, 15, 16)) return super.validate(input); else return new ValidationResult(false, Res.get("validation.accountNrFormat", "03-1587-0050000-00")); case "AU": if (isNumberInRange(input, 4, 10)) return super.validate(input); else return new ValidationResult(false, Res.get("validation.accountNr", "4 - 10")); case "CA": if (isNumberInRange(input, 7, 12)) return super.validate(input); else return new ValidationResult(false, Res.get("validation.accountNr", "7 - 12")); case "MX": length = 18; if (isNumberWithFixedLength(input, length)) return super.validate(input); else return new ValidationResult(false, Res.get("validation.sortCodeNumber", getLabel(), length)); case "HK": input2 = input != null ? input.replaceAll("-", "") : null; if (isNumberInRange(input2, 9, 12)) return super.validate(input); else return new ValidationResult(false, Res.get("validation.accountNrFormat", "005-231289-112")); case "NO": if (input != null) { length = 11; // Provided by sturles: // https://github.com/bisq-network/exchange/pull/707 // https://no.wikipedia.org/wiki/MOD11#Implementasjoner_i_forskjellige_programmeringspr.C3.A5k // https://en.wikipedia.org/wiki/International_Bank_Account_Number#Generating_IBAN_check_digits6 // 11 digits, last digit is checksum. Checksum algoritm is // MOD11 with weights 2,3,4,5,6,7,2,3,4,5 right to left. // First remove whitespace and periods. Normal formatting is: // 1234.56.78903 input2 = StringUtils.remove(input, " "); input2 = StringUtils.remove(input2, "."); // 11 digits, numbers only if (input2.length() != length || !StringUtils.isNumeric(input2)) return new ValidationResult(false, Res.get("validation.sortCodeNumber", getLabel(), length)); int lastDigit = Character.getNumericValue(input2.charAt(input2.length() - 1)); if (getMod11ControlDigit(input2) != lastDigit) return new ValidationResult(false, "Kontonummer har feil sjekksum"); // not translated else return super.validate(input); } else { return super.validate(null); } default: return super.validate(input); } } private int getMod11ControlDigit(String accountNrString) { int sumForMod = 0; int controlNumber = 2; char[] accountNr = accountNrString.toCharArray(); for (int i = accountNr.length - 2; i >= 0; i--) { sumForMod += (Character.getNumericValue(accountNr[i]) * controlNumber); controlNumber++; if (controlNumber > 7) { controlNumber = 2; } } int calculus = (11 - sumForMod % 11); if (calculus == 11) { return 0; } else { return calculus; } } private String getLabel() { String label = BankUtil.getAccountNrLabel(countryCode); return label.substring(0, label.length() - 1); } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/AdvancedCashValidator.java ================================================ package haveno.core.payment.validation; import com.google.inject.Inject; import haveno.core.locale.Res; import haveno.core.util.validation.InputValidator; import haveno.core.util.validation.RegexValidator; public class AdvancedCashValidator extends InputValidator { private EmailValidator emailValidator; private RegexValidator regexValidator; @Inject public AdvancedCashValidator(EmailValidator emailValidator, RegexValidator regexValidator) { this.emailValidator = emailValidator; regexValidator.setPattern("[A-Za-z]{1}\\d{12}"); regexValidator.setErrorMessage(Res.get("validation.advancedCash.invalidFormat")); this.regexValidator = regexValidator; } @Override public ValidationResult validate(String input) { ValidationResult result = super.validate(input); if (!result.isValid) return result; result = emailValidator.validate(input); if (!result.isValid) result = regexValidator.validate(input); return result; } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/AliPayValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.core.util.validation.InputValidator; public final class AliPayValidator extends InputValidator { /////////////////////////////////////////////////////////////////////////////////////////// // Public methods /////////////////////////////////////////////////////////////////////////////////////////// @Override public ValidationResult validate(String input) { // TODO return super.validate(input); } /////////////////////////////////////////////////////////////////////////////////////////// // Private methods /////////////////////////////////////////////////////////////////////////////////////////// } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/AustraliaPayidAccountNameValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import com.google.inject.Inject; import haveno.core.util.validation.InputValidator; import haveno.core.util.validation.RegexValidator; public final class AustraliaPayidAccountNameValidator extends InputValidator { @Override public ValidationResult validate(String input) { ValidationResult result = super.validate(input); if (result.isValid) result = lengthValidator.validate(input); if (result.isValid) result = regexValidator.validate(input); return result; } private final LengthValidator lengthValidator; private final RegexValidator regexValidator; @Inject public AustraliaPayidAccountNameValidator(LengthValidator lengthValidator, RegexValidator regexValidator) { lengthValidator.setMinLength(1); lengthValidator.setMaxLength(40); this.lengthValidator = lengthValidator; this.regexValidator = regexValidator; } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/AustraliaPayidValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.core.util.validation.InputValidator; public final class AustraliaPayidValidator extends InputValidator { private final EmailValidator emailValidator; /////////////////////////////////////////////////////////////////////////////////////////// // Public methods /////////////////////////////////////////////////////////////////////////////////////////// public AustraliaPayidValidator() { emailValidator = new EmailValidator(); } @Override public ValidationResult validate(String input) { ValidationResult result = validateIfNotEmpty(input); if (!result.isValid) { return result; } else { ValidationResult emailResult = emailValidator.validate(input); if (emailResult.isValid) return emailResult; else return validatePhoneNumber(input); } } /////////////////////////////////////////////////////////////////////////////////////////// // Private methods /////////////////////////////////////////////////////////////////////////////////////////// // TODO not impl yet -> see InteracETransferValidator private ValidationResult validatePhoneNumber(String input) { return super.validate(input); } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/BICValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.core.locale.Res; import haveno.core.util.validation.InputValidator; import java.util.Locale; /* * BIC information taken from German wikipedia (2017-01-30) * * length 8 or 11 characters * General format: BBBB CC LL (bbb) * with B - Bank code * C - Country code * L - Location code * b - branch code (if applicable) * * B and C must be letters * first L cannot be 0 or 1, second L cannot be O (upper case 'o') * bbb cannot begin with X, unless it is XXX */ // TODO Special letters like ä, å, ... are not detected as invalid public final class BICValidator extends InputValidator { /////////////////////////////////////////////////////////////////////////////////////////// // Public methods /////////////////////////////////////////////////////////////////////////////////////////// @Override public ValidationResult validate(String input) { // TODO Add validation for primary and secondary IDs according to the selected type // IBAN max 34 chars // bic: 8 or 11 chars // check ensure length 8 or 11 if (!isStringWithFixedLength(input, 8) && !isStringWithFixedLength(input, 11)) return new ValidationResult(false, Res.get("validation.bic.invalidLength")); input = input.toUpperCase(Locale.ROOT); // ensure Bank and Country code to be letters only for (int k = 0; k < 6; k++) { if (!Character.isLetter(input.charAt(k))) return new ValidationResult(false, Res.get("validation.bic.letters")); } // ensure location code starts not with 0 or 1 and ends not with O char ch = input.charAt(6); if (ch == '0' || ch == '1' || input.charAt(7) == 'O') return new ValidationResult(false, Res.get("validation.bic.invalidLocationCode")); if (input.startsWith("REVO")) return new ValidationResult(false, Res.get("validation.bic.sepaRevolutBic")); // check complete for 8 char BIC if (input.length() == 8) return new ValidationResult(true); // ensure branch code does not start with X unless it is XXX if (input.charAt(8) == 'X') if (input.charAt(9) != 'X' || input.charAt(10) != 'X') return new ValidationResult(false, Res.get("validation.bic.invalidBranchCode")); return new ValidationResult(true); } /////////////////////////////////////////////////////////////////////////////////////////// // Private methods /////////////////////////////////////////////////////////////////////////////////////////// } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/BankIdValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.core.locale.BankUtil; import haveno.core.locale.Res; public final class BankIdValidator extends BankValidator { public BankIdValidator(String countryCode) { super(countryCode); } @Override public ValidationResult validate(String input) { int length; switch (countryCode) { case "CA": length = 3; if (isNumberWithFixedLength(input, length)) return super.validate(input); else return new ValidationResult(false, Res.get("validation.bankIdNumber", getLabel(), length)); case "HK": length = 3; if (isNumberWithFixedLength(input, length)) return super.validate(input); else return new ValidationResult(false, Res.get("validation.bankIdNumber", getLabel(), length)); default: return super.validate(input); } } private String getLabel() { String label = BankUtil.getBankIdLabel(countryCode); return label.substring(0, label.length() - 1); } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/BankValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.core.util.validation.InputValidator; public abstract class BankValidator extends InputValidator { protected String countryCode = ""; public BankValidator() { super(); } public BankValidator(String countryCode) { this.countryCode = countryCode; } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/BranchIdValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.core.locale.BankUtil; import haveno.core.locale.Res; public final class BranchIdValidator extends BankValidator { public BranchIdValidator(String countryCode) { super(countryCode); } @Override public ValidationResult validate(String input) { int length; switch (countryCode) { case "GB": length = 6; if (isNumberWithFixedLength(input, length)) return super.validate(input); else return new ValidationResult(false, Res.get("validation.sortCodeNumber", getLabel(), length)); case "US": length = 9; if (isNumberWithFixedLength(input, length)) return super.validate(input); else return new ValidationResult(false, Res.get("validation.sortCodeNumber", getLabel(), length)); case "BR": if (isStringInRange(input, 2, 6)) return super.validate(input); else return new ValidationResult(false, Res.get("validation.sortCodeChars", getLabel(), "2 - 6")); case "AU": length = 6; if (isNumberWithFixedLength(input, length)) return super.validate(input); else return new ValidationResult(false, Res.get("validation.sortCodeChars", getLabel(), length)); case "CA": length = 5; if (isNumberWithFixedLength(input, length)) return super.validate(input); else return new ValidationResult(false, Res.get("validation.sortCodeNumber", getLabel(), length)); case "AR": length = 4; if(isNumberWithFixedLength(input, length)) return super.validate(input); else return new ValidationResult(false, Res.get("validation.sortCodeNumber", getLabel(), length)); default: return super.validate(input); } } private String getLabel() { return BankUtil.getBranchIdLabel(countryCode); } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/CapitualValidator.java ================================================ package haveno.core.payment.validation; import com.google.inject.Inject; import haveno.core.locale.Res; import haveno.core.util.validation.InputValidator; import haveno.core.util.validation.RegexValidator; public class CapitualValidator extends InputValidator { private final RegexValidator regexValidator; @Inject public CapitualValidator(RegexValidator regexValidator) { regexValidator.setPattern("CAP-[A-Za-z0-9]{6}"); regexValidator.setErrorMessage(Res.get("validation.capitual.invalidFormat")); this.regexValidator = regexValidator; } @Override public ValidationResult validate(String input) { return regexValidator.validate(input); } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/ChaseQuickPayValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.core.util.validation.InputValidator; public final class ChaseQuickPayValidator extends InputValidator { private final EmailValidator emailValidator; /////////////////////////////////////////////////////////////////////////////////////////// // Public methods /////////////////////////////////////////////////////////////////////////////////////////// public ChaseQuickPayValidator() { super(); emailValidator = new EmailValidator(); } @Override public ValidationResult validate(String input) { return emailValidator.validate(input); } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/CountryCallingCodes.java ================================================ package haveno.core.payment.validation; import java.util.Map; import static java.util.Map.entry; final class CountryCallingCodes { /** * Immutable mapping of ISO 3166 alpha-2 country code to national dialing number. *

    * In some regions, such as American Samoa ('AS'), there is only one area code, and * it is included in the mapping as part of the calling code. * * @see {@link https://en.wikipedia.org/wiki/E.164} * @see {@link https://en.wikipedia.org/wiki/List_of_country_calling_codes} */ private final static Map CALLING_CODE_MAP = Map.ofEntries( entry("AD", "376"), entry("AE", "971"), entry("AF", "93"), entry("AG", "1-268"), entry("AI", "1-264"), entry("AL", "355"), entry("AM", "374"), entry("AO", "244"), entry("AQ", "672"), entry("AR", "54"), entry("AS", "1-684"), entry("AT", "43"), entry("AU", "61"), entry("AW", "297"), entry("AX", "358"), entry("AZ", "994"), entry("BA", "387"), entry("BB", "1-246"), entry("BD", "880"), entry("BE", "32"), entry("BF", "226"), entry("BG", "359"), entry("BH", "973"), entry("BI", "257"), entry("BJ", "229"), entry("BL", "590"), entry("BM", "1-441"), entry("BN", "673"), entry("BO", "591"), entry("BQ", "599"), entry("BR", "55"), entry("BS", "1-242"), entry("BT", "975"), entry("BV", "47"), entry("BW", "267"), entry("BY", "375"), entry("BZ", "501"), entry("CA", "1"), entry("CC", "61"), entry("CD", "243"), entry("CF", "236"), entry("CG", "242"), entry("CH", "41"), entry("CI", "225"), entry("CK", "682"), entry("CL", "56"), entry("CM", "237"), entry("CN", "86"), entry("CO", "57"), entry("CR", "506"), entry("CU", "53"), entry("CV", "238"), entry("CW", "599"), entry("CX", "61"), entry("CY", "357"), entry("CZ", "420"), entry("DE", "49"), entry("DJ", "253"), entry("DK", "45"), entry("DM", "1-767"), entry("DO", "1"), // DO has three area codes 809,829,849; let user define hers. entry("DZ", "213"), entry("EC", "593"), entry("EE", "372"), entry("EG", "20"), entry("EH", "212"), entry("ER", "291"), entry("ES", "34"), entry("ET", "251"), entry("FI", "358"), entry("FJ", "679"), entry("FK", "500"), entry("FM", "691"), entry("FO", "298"), entry("FR", "33"), entry("GA", "241"), entry("GB", "44"), entry("GD", "1-473"), entry("GE", "995"), entry("GF", "594"), entry("GG", "44"), entry("GH", "233"), entry("GI", "350"), entry("GL", "299"), entry("GM", "220"), entry("GN", "224"), entry("GP", "590"), entry("GQ", "240"), entry("GR", "30"), entry("GS", "500"), entry("GT", "502"), entry("GU", "1-671"), entry("GW", "245"), entry("GY", "592"), entry("HK", "852"), entry("HM", "672"), entry("HN", "504"), entry("HR", "385"), entry("HT", "509"), entry("HU", "36"), entry("ID", "62"), entry("IE", "353"), entry("IL", "972"), entry("IM", "44"), entry("IN", "91"), entry("IO", "246"), entry("IQ", "964"), entry("IR", "98"), entry("IS", "354"), entry("IT", "39"), entry("JE", "44"), entry("JM", "1-876"), entry("JO", "962"), entry("JP", "81"), entry("KE", "254"), entry("KG", "996"), entry("KH", "855"), entry("KI", "686"), entry("KM", "269"), entry("KN", "1-869"), entry("KP", "850"), entry("KR", "82"), entry("KW", "965"), entry("KY", "1-345"), entry("KZ", "7"), entry("LA", "856"), entry("LB", "961"), entry("LC", "1-758"), entry("LI", "423"), entry("LK", "94"), entry("LR", "231"), entry("LS", "266"), entry("LT", "370"), entry("LU", "352"), entry("LV", "371"), entry("LY", "218"), entry("MA", "212"), entry("MC", "377"), entry("MD", "373"), entry("ME", "382"), entry("MF", "590"), entry("MG", "261"), entry("MH", "692"), entry("MK", "389"), entry("ML", "223"), entry("MM", "95"), entry("MN", "976"), entry("MO", "853"), entry("MP", "1-670"), entry("MQ", "596"), entry("MR", "222"), entry("MS", "1-664"), entry("MT", "356"), entry("MU", "230"), entry("MV", "960"), entry("MW", "265"), entry("MX", "52"), entry("MY", "60"), entry("MZ", "258"), entry("NA", "264"), entry("NC", "687"), entry("NE", "227"), entry("NF", "672"), entry("NG", "234"), entry("NI", "505"), entry("NL", "31"), entry("NO", "47"), entry("NP", "977"), entry("NR", "674"), entry("NU", "683"), entry("NZ", "64"), entry("OM", "968"), entry("PA", "507"), entry("PE", "51"), entry("PF", "689"), entry("PG", "675"), entry("PH", "63"), entry("PK", "92"), entry("PL", "48"), entry("PM", "508"), entry("PN", "870"), entry("PR", "1"), entry("PS", "970"), entry("PT", "351"), entry("PW", "680"), entry("PY", "595"), entry("QA", "974"), entry("RE", "262"), entry("RO", "40"), entry("RS", "381 p"), entry("RU", "7"), entry("RW", "250"), entry("SA", "966"), entry("SB", "677"), entry("SC", "248"), entry("SD", "249"), entry("SE", "46"), entry("SG", "65"), entry("SH", "290 n"), entry("SI", "386"), entry("SJ", "47"), entry("SK", "421"), entry("SL", "232"), entry("SM", "378"), entry("SN", "221"), entry("SO", "252"), entry("SR", "597"), entry("SS", "211"), entry("ST", "239"), entry("SV", "503"), entry("SX", "1-721"), entry("SY", "963"), entry("SZ", "268"), entry("TC", "1-649"), entry("TD", "235"), entry("TF", "262"), entry("TG", "228"), entry("TH", "66"), entry("TJ", "992"), entry("TK", "690"), entry("TL", "670"), entry("TM", "993"), entry("TN", "216"), entry("TO", "676"), entry("TR", "90"), entry("TT", "1-868"), entry("TV", "688"), entry("TW", "886"), entry("TZ", "255"), entry("UA", "380"), entry("UG", "256"), // entry("UM", null), entry("US", "1"), entry("UY", "598"), entry("UZ", "998"), entry("VA", "39-06"), entry("VC", "1-784"), entry("VE", "58"), entry("VG", "1-284"), entry("VI", "1-340"), entry("VN", "84"), entry("VU", "678"), entry("WF", "681"), entry("WS", "685"), entry("YE", "967"), entry("YT", "262"), entry("ZA", "27"), entry("ZM", "260"), entry("ZW", "263") ); private static final String normalizedCallingCodeRegex = "[\\-]"; static String getCallingCode(String isoCountryCode) { return CALLING_CODE_MAP.get(isoCountryCode); } static String getNormalizedCallingCode(String isoCountryCode) { return getCallingCode(isoCountryCode).replaceAll(normalizedCallingCodeRegex, ""); } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/CryptoAddressValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import com.google.inject.Inject; import haveno.asset.AddressValidationResult; import haveno.asset.Asset; import haveno.asset.AssetRegistry; import haveno.common.config.Config; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.util.validation.InputValidator; import lombok.extern.slf4j.Slf4j; import java.util.Optional; @Slf4j public final class CryptoAddressValidator extends InputValidator { private final AssetRegistry assetRegistry; private String currencyCode; @Inject public CryptoAddressValidator(AssetRegistry assetRegistry) { this.assetRegistry = assetRegistry; } public void setCurrencyCode(String currencyCode) { this.currencyCode = currencyCode; } @Override public ValidationResult validate(String input) { ValidationResult validationResult = super.validate(input); if (!validationResult.isValid || currencyCode == null) return validationResult; Optional optionalAsset = CurrencyUtil.findAsset(assetRegistry, currencyCode, Config.baseCurrencyNetwork()); if (optionalAsset.isPresent()) { Asset asset = optionalAsset.get(); AddressValidationResult result = asset.validateAddress(input); if (!result.isValid()) { return new ValidationResult(false, Res.get(result.getI18nKey(), asset.getTickerSymbol(), result.getMessage())); } return new ValidationResult(true); } else { return new ValidationResult(false); } } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/EmailOrMobileNrOrCashtagValidator.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.payment.validation; import haveno.core.util.validation.InputValidator; public final class EmailOrMobileNrOrCashtagValidator extends InputValidator { private final EmailOrMobileNrValidator emailOrMobileNrValidator; /////////////////////////////////////////////////////////////////////////////////////////// // Public methods /////////////////////////////////////////////////////////////////////////////////////////// public EmailOrMobileNrOrCashtagValidator() { emailOrMobileNrValidator = new EmailOrMobileNrValidator(); } @Override public ValidationResult validate(String input) { ValidationResult result = validateIfNotEmpty(input); if (!result.isValid) { return result; } else { ValidationResult emailOrMobileResult = emailOrMobileNrValidator.validate(input); if (emailOrMobileResult.isValid) return emailOrMobileResult; else return validateCashtag(input); } } /////////////////////////////////////////////////////////////////////////////////////////// // Private methods /////////////////////////////////////////////////////////////////////////////////////////// // TODO not impl yet -> see InteracETransferValidator private ValidationResult validateCashtag(String input) { return super.validate(input); } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/EmailOrMobileNrOrUsernameValidator.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.payment.validation; import haveno.core.util.validation.InputValidator; public final class EmailOrMobileNrOrUsernameValidator extends InputValidator { private final EmailOrMobileNrValidator emailOrMobileNrValidator; /////////////////////////////////////////////////////////////////////////////////////////// // Public methods /////////////////////////////////////////////////////////////////////////////////////////// public EmailOrMobileNrOrUsernameValidator() { emailOrMobileNrValidator = new EmailOrMobileNrValidator(); } @Override public ValidationResult validate(String input) { ValidationResult result = validateIfNotEmpty(input); if (!result.isValid) { return result; } else { ValidationResult emailOrMobileResult = emailOrMobileNrValidator.validate(input); if (emailOrMobileResult.isValid) return emailOrMobileResult; else return validateName(input); } } /////////////////////////////////////////////////////////////////////////////////////////// // Private methods /////////////////////////////////////////////////////////////////////////////////////////// // TODO: properly implement username validation private ValidationResult validateName(String input) { return super.validate(input); } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/EmailOrMobileNrValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.core.util.validation.InputValidator; public final class EmailOrMobileNrValidator extends InputValidator { private final EmailValidator emailValidator; /////////////////////////////////////////////////////////////////////////////////////////// // Public methods /////////////////////////////////////////////////////////////////////////////////////////// public EmailOrMobileNrValidator() { emailValidator = new EmailValidator(); } @Override public ValidationResult validate(String input) { ValidationResult result = validateIfNotEmpty(input); if (!result.isValid) { return result; } else { ValidationResult emailResult = emailValidator.validate(input); if (emailResult.isValid) return emailResult; else return validatePhoneNumber(input); } } /////////////////////////////////////////////////////////////////////////////////////////// // Private methods /////////////////////////////////////////////////////////////////////////////////////////// // TODO not impl yet -> see InteracETransferValidator private ValidationResult validatePhoneNumber(String input) { return super.validate(input); } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/EmailValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.core.locale.Res; import haveno.core.util.validation.InputValidator; /* * Mail addresses consist of localPart @ domainPart * * Local part: * May contain lots of symbols A-Za-z0-9.!#$%&'*+-/=?^_`{|}~ * but cannot begin or end with a dot (.) * between double quotes many more symbols are allowed: * "(),:;<>@[\] (ASCII: 32, 34, 40, 41, 44, 58, 59, 60, 62, 64, 91–93) * * Domain part: * Consists of name dot TLD * name can but usually doesn't (compatibility reasons) contain non-ASCII * symbols. * TLD is at least two letters long */ public final class EmailValidator extends InputValidator { /////////////////////////////////////////////////////////////////////////////////////////// // Public methods /////////////////////////////////////////////////////////////////////////////////////////// private final ValidationResult invalidAddress = new ValidationResult(false, Res.get("validation.email.invalidAddress")); @Override public ValidationResult validate(String input) { if (input == null || input.length() < 6 || input.length() > 100) // shortest address is l@d.cc, max length 100 return invalidAddress; String[] subStrings; String local, domain; subStrings = input.split("@", -1); if (subStrings.length == 1) // address does not contain '@' return invalidAddress; if (subStrings.length > 2) // multiple @'s included -> check for valid double quotes if (!checkForValidQuotes(subStrings)) // around @'s -> "..@..@.." and concatenate local part return invalidAddress; local = subStrings[0]; domain = subStrings[subStrings.length - 1]; if (local.isEmpty()) return invalidAddress; // local part cannot begin or end with '.' if (local.startsWith(".") || local.endsWith(".")) return invalidAddress; String[] splitDomain = domain.split("\\.", -1); // '.' is a regex in java and has to be escaped String tld = splitDomain[splitDomain.length - 1]; if (splitDomain.length < 2) return invalidAddress; if (splitDomain[0] == null || splitDomain[0].isEmpty()) return invalidAddress; // TLD length is at least two if (tld.length() < 2) return invalidAddress; // TLD is letters only for (int k = 0; k < tld.length(); k++) if (!Character.isLetter(tld.charAt(k))) return invalidAddress; return new ValidationResult(true); } /////////////////////////////////////////////////////////////////////////////////////////// // Private methods /////////////////////////////////////////////////////////////////////////////////////////// private boolean checkForValidQuotes(String[] subStrings) { int length = subStrings.length - 2; // is index on last substring of local part // check for odd number of double quotes before first and after last '@' if ((subStrings[0].split("\"", -1).length % 2 == 1) || (subStrings[length].split("\"", -1).length % 2 == 1)) return false; for (int k = 1; k < length; k++) { if (subStrings[k].split("\"", -1).length % 2 == 0) return false; } String patchLocal = ""; for (int k = 0; k <= length; k++) // remember: length is last index not array length patchLocal = patchLocal.concat(subStrings[k]); // @'s are not reinstalled, since not needed for further checks subStrings[0] = patchLocal; return true; } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/F2FValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.core.util.validation.InputValidator; public final class F2FValidator extends InputValidator { /////////////////////////////////////////////////////////////////////////////////////////// // Public methods /////////////////////////////////////////////////////////////////////////////////////////// @Override public ValidationResult validate(String input) { // TODO return super.validate(input); } /////////////////////////////////////////////////////////////////////////////////////////// // Private methods /////////////////////////////////////////////////////////////////////////////////////////// } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/FiatVolumeValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import com.google.inject.Inject; import haveno.core.util.validation.MonetaryValidator; public class FiatVolumeValidator extends MonetaryValidator { @Override public double getMinValue() { return 0.01; } @Override public double getMaxValue() { // Hard to say what the max value should be (zimbabwe dollar....)? // Lets set it to Double.MAX_VALUE until we find some reasonable number return Double.MAX_VALUE; } @Inject public FiatVolumeValidator() { } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/HalCashValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.core.util.validation.InputValidator; public final class HalCashValidator extends InputValidator { /////////////////////////////////////////////////////////////////////////////////////////// // Public methods /////////////////////////////////////////////////////////////////////////////////////////// @Override public ValidationResult validate(String input) { // TODO return super.validate(input); } /////////////////////////////////////////////////////////////////////////////////////////// // Private methods /////////////////////////////////////////////////////////////////////////////////////////// } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/IBANValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.core.locale.Res; import haveno.core.util.validation.InputValidator; import lombok.Setter; import java.math.BigInteger; import java.util.Locale; // TODO Does not yet recognize special letters like ä, ö, ü, å, ... as invalid characters public class IBANValidator extends InputValidator { @Setter private String restrictToCountry = ""; /////////////////////////////////////////////////////////////////////////////////////////// // Public methods /////////////////////////////////////////////////////////////////////////////////////////// public IBANValidator() { } public IBANValidator(String restrictToCountry) { this.restrictToCountry = restrictToCountry; } @Override public ValidationResult validate(String input) { // TODO Add validation for primary and secondary IDs according to the selected type // IBAN max 34 chars, shortest is Norwegian with 15 chars, BBAN may include letters // bic: max 11 char // check input length first if (isStringInRange(input, 15, 34)) { input = input.toUpperCase(Locale.ROOT); // ensure upper case // check if country code is letters and checksum numeric if (!(Character.isLetter(input.charAt(0)) && Character.isLetter(input.charAt(1)))) return new ValidationResult(false, Res.get("validation.iban.invalidCountryCode")); if (restrictToCountry.length() > 0 && !restrictToCountry.equals(input.substring(0, 2))) return new ValidationResult(false, Res.get("validation.iban.invalidCountryCode")); if (!(Character.isDigit(input.charAt(2)) && Character.isDigit(input.charAt(3)))) return new ValidationResult(false, Res.get("validation.iban.checkSumNotNumeric")); // reorder IBAN to format String input2 = input.substring(4, input.length()) + input.substring(0, 4); // check if input is alphanumeric and count included letters int charCount = 0; char ch; for (int k = 0; k < input2.length(); k++) { ch = input2.charAt(k); if (Character.isLetter(ch)) charCount++; else if (!Character.isDigit(ch)) return (new ValidationResult(false, Res.get("validation.iban.nonNumericChars"))); } // create final char array for checksum validation char[] charArray = new char[input2.length() + charCount]; int i = 0; int tmp; for (int k = 0; k < input2.length(); k++) { ch = input2.charAt(k); if (Character.isLetter(ch)) { tmp = ch - ('A' - 10); // letters are transformed to two digit numbers A->10, B->11, ... String s = Integer.toString(tmp); charArray[i++] = s.charAt(0); // insert transformed charArray[i++] = s.charAt(1); // letters into char array } else charArray[i++] = ch; // transfer digits directly to char array } // System.out.print(Arrays.toString(charArray) + '\t'); BigInteger bigInt = new BigInteger(new String(charArray)); int result = bigInt.mod(new BigInteger(Integer.toString(97))).intValue(); if (result == 1) return new ValidationResult(true); else return new ValidationResult(false, Res.get("validation.iban.checkSumInvalid")); } // return new ValidationResult(false, BSResources.get("validation.accountNrChars", "15 - 34")); return new ValidationResult(false, Res.get("validation.iban.invalidLength")); } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/InteracETransferAnswerValidator.java ================================================ package haveno.core.payment.validation; import com.google.inject.Inject; import haveno.core.locale.Res; import haveno.core.util.validation.InputValidator; import haveno.core.util.validation.RegexValidator; public class InteracETransferAnswerValidator extends InputValidator { private LengthValidator lengthValidator; private RegexValidator regexValidator; @Inject public InteracETransferAnswerValidator(LengthValidator lengthValidator, RegexValidator regexValidator) { lengthValidator.setMinLength(3); lengthValidator.setMaxLength(25); this.lengthValidator = lengthValidator; regexValidator.setPattern("[A-Za-z0-9\\-]+"); regexValidator.setErrorMessage(Res.get("validation.interacETransfer.invalidAnswer")); this.regexValidator = regexValidator; } @Override public ValidationResult validate(String input) { ValidationResult result = super.validate(input); if (result.isValid) result = lengthValidator.validate(input); if (result.isValid) result = regexValidator.validate(input); return result; } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/InteracETransferQuestionValidator.java ================================================ package haveno.core.payment.validation; import com.google.inject.Inject; import haveno.core.locale.Res; import haveno.core.util.validation.InputValidator; import haveno.core.util.validation.RegexValidator; public class InteracETransferQuestionValidator extends InputValidator { private LengthValidator lengthValidator; private RegexValidator regexValidator; @Inject public InteracETransferQuestionValidator(LengthValidator lengthValidator, RegexValidator regexValidator) { lengthValidator.setMinLength(1); lengthValidator.setMaxLength(160); this.lengthValidator = lengthValidator; regexValidator.setPattern("[A-Za-z0-9\\-\\_\\'\\,\\.\\? ]+"); regexValidator.setErrorMessage(Res.get("validation.interacETransfer.invalidQuestion")); this.regexValidator = regexValidator; } @Override public ValidationResult validate(String input) { ValidationResult result = super.validate(input); if (result.isValid) result = lengthValidator.validate(input); if (result.isValid) result = regexValidator.validate(input); return result; } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/InteracETransferValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import com.google.inject.Inject; import haveno.core.locale.Res; import haveno.core.util.validation.InputValidator; import org.apache.commons.lang3.StringUtils; /* * Interac e-Transfer requires a mail address or Canadian (mobile) phone number * * Mail addresses are covered with class EmailValidator * * Phone numbers have 11 digits, expected format is +1 NPA xxx-xxxx * Plus, spaces and dash might be omitted * Canadian area codes (NPA) taken from http://www.cnac.ca/canadian_dial_plan/Current_&_Future_Dialling_Plan.pdf * Valid (as of 2017-06-27) NPAs are hardcoded here * They are to change in some future (according to the linked document around 2019/2020) */ public final class InteracETransferValidator extends InputValidator { private static final String[] NPAS = {"204", "226", "236", "249", "250", "289", "306", "343", "365", "403", "416", "418", "431", "437", "438", "450", "506", "514", "519", "548", "579", "581", "587", "604", "613", "639", "647", "705", "709", "778", "780", "782", "807", "819", "825", "867", "873", "902", "905"}; private final EmailValidator emailValidator; /////////////////////////////////////////////////////////////////////////////////////////// // Public methods /////////////////////////////////////////////////////////////////////////////////////////// @Inject public InteracETransferValidator(EmailValidator emailValidator, InteracETransferQuestionValidator questionValidator, InteracETransferAnswerValidator answerValidator) { this.emailValidator = emailValidator; this.questionValidator = questionValidator; this.answerValidator = answerValidator; } public final InputValidator answerValidator; public final InputValidator questionValidator; @Override public ValidationResult validate(String input) { ValidationResult result = validateIfNotEmpty(input); if (!result.isValid) { return result; } else { ValidationResult emailResult = emailValidator.validate(input); if (emailResult.isValid) return emailResult; else return validatePhoneNumber(input); } } /////////////////////////////////////////////////////////////////////////////////////////// // Private methods /////////////////////////////////////////////////////////////////////////////////////////// private ValidationResult validatePhoneNumber(String input) { // check for correct format and strip +, space and - if (input.matches("\\+?1[ -]?\\d{3}[ -]?\\d{3}[ -]?\\d{4}")) { input = input.replace("+", ""); input = StringUtils.deleteWhitespace(input); input = input.replace("-", ""); String inputAreaCode = input.substring(1, 4); for (String s : NPAS) { // check area code agains list and return if valid if (inputAreaCode.compareTo(s) == 0) return new ValidationResult(true); } return new ValidationResult(false, Res.get("validation.interacETransfer.invalidAreaCode")); } else { return new ValidationResult(false, Res.get("validation.interacETransfer.invalidPhone")); } } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/JapanBankAccountNameValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import com.google.inject.Inject; import haveno.core.payment.JapanBankData; import haveno.core.util.validation.InputValidator; import haveno.core.util.validation.RegexValidator; public final class JapanBankAccountNameValidator extends InputValidator { @Override public ValidationResult validate(String input) { ValidationResult result = super.validate(input); if (result.isValid) result = lengthValidator.validate(input); if (result.isValid) result = regexValidator.validate(input); return result; } private LengthValidator lengthValidator; private RegexValidator regexValidator; @Inject public JapanBankAccountNameValidator(LengthValidator lengthValidator, RegexValidator regexValidator) { lengthValidator.setMinLength(1); lengthValidator.setMaxLength(40); this.lengthValidator = lengthValidator; regexValidator.setPattern(JapanBankData.getString("japanese.validation.regex")); regexValidator.setErrorMessage(JapanBankData.getString("japanese.validation.error")); this.regexValidator = regexValidator; } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/JapanBankAccountNumberValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.core.payment.JapanBankData; import haveno.core.util.validation.InputValidator; public final class JapanBankAccountNumberValidator extends InputValidator { @Override public ValidationResult validate(String input) { boolean lengthOK = ( isNumberWithFixedLength(input, 3) || isNumberWithFixedLength(input, 4) || isNumberWithFixedLength(input, 5) || isNumberWithFixedLength(input, 6) || isNumberWithFixedLength(input, 7) || isNumberWithFixedLength(input, 8)); if (lengthOK) return super.validate(input); return new ValidationResult(false, JapanBankData.getString("account.number.validation.error")); } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/JapanBankBranchCodeValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.core.payment.JapanBankData; import haveno.core.util.validation.InputValidator; public final class JapanBankBranchCodeValidator extends InputValidator { @Override public ValidationResult validate(String input) { if (isNumberWithFixedLength(input, 3)) return super.validate(input); return new ValidationResult(false, JapanBankData.getString("branch.code.validation.error")); } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/JapanBankBranchNameValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import com.google.inject.Inject; import haveno.core.payment.JapanBankData; import haveno.core.util.validation.InputValidator; import haveno.core.util.validation.RegexValidator; public final class JapanBankBranchNameValidator extends InputValidator { @Override public ValidationResult validate(String input) { ValidationResult result = super.validate(input); if (result.isValid) result = lengthValidator.validate(input); if (result.isValid) result = regexValidator.validate(input); return result; } private LengthValidator lengthValidator; private RegexValidator regexValidator; @Inject public JapanBankBranchNameValidator(LengthValidator lengthValidator, RegexValidator regexValidator) { lengthValidator.setMinLength(1); lengthValidator.setMaxLength(40); this.lengthValidator = lengthValidator; regexValidator.setPattern(JapanBankData.getString("japanese.validation.regex")); regexValidator.setErrorMessage(JapanBankData.getString("japanese.validation.error")); this.regexValidator = regexValidator; } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/JapanBankTransferValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.core.util.validation.InputValidator; public final class JapanBankTransferValidator extends InputValidator { /////////////////////////////////////////////////////////////////////////////////////////// // Public methods /////////////////////////////////////////////////////////////////////////////////////////// @Override public ValidationResult validate(String input) { // TODO return super.validate(input); } /////////////////////////////////////////////////////////////////////////////////////////// // Private methods /////////////////////////////////////////////////////////////////////////////////////////// } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/LengthValidator.java ================================================ package haveno.core.payment.validation; import haveno.core.locale.Res; import haveno.core.util.validation.InputValidator; public class LengthValidator extends InputValidator { private int minLength; private int maxLength; public LengthValidator() { this(0, Integer.MAX_VALUE); } public LengthValidator(int min, int max) { this.minLength = min; this.maxLength = max; } @Override public ValidationResult validate(String input) { ValidationResult result = new ValidationResult(true); int length = (input == null) ? 0 : input.length(); if (this.minLength == this.maxLength) { if (length != this.minLength) result = new ValidationResult(false, Res.get("validation.fixedLength", this.minLength)); } else if (length < this.minLength || length > this.maxLength) result = new ValidationResult(false, Res.get("validation.length", this.minLength, this.maxLength)); return result; } public void setMinLength(int minLength) { this.minLength = minLength; } public void setMaxLength(int maxLength) { this.maxLength = maxLength; } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/MoneyBeamValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.core.util.validation.InputValidator; public final class MoneyBeamValidator extends InputValidator { @Override public ValidationResult validate(String input) { // TODO return super.validate(input); } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/NationalAccountIdValidator.java ================================================ package haveno.core.payment.validation; import haveno.core.locale.BankUtil; import haveno.core.locale.Res; public class NationalAccountIdValidator extends BankValidator { public NationalAccountIdValidator(String countryCode) { super(countryCode); } @Override public ValidationResult validate(String input) { int length; switch (countryCode) { case "AR": length = 22; if (isNumberWithFixedLength(input, length)) return super.validate(input); else { String nationalAccountIdLabel = BankUtil.getNationalAccountIdLabel(countryCode); return new ValidationResult(false, Res.get("validation.nationalAccountId", nationalAccountIdLabel, length)); } default: return super.validate(input); } } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/PercentageNumberValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.core.locale.Res; import haveno.core.util.validation.NumberValidator; import lombok.Setter; import javax.annotation.Nullable; public class PercentageNumberValidator extends NumberValidator { @Nullable @Setter protected Double maxValue; // Keep it Double as we check for null @Override public ValidationResult validate(String input) { ValidationResult result = validateIfNotEmpty(input); if (result.isValid) { input = input.replace("%", ""); input = cleanInput(input); result = validateIfNumber(input); } return result.and(validateIfNotExceedsMaxValue(input)); } private ValidationResult validateIfNotExceedsMaxValue(String input) { try { double value = Double.parseDouble(input); if (maxValue != null && value > maxValue) return new ValidationResult(false, Res.get("validation.inputTooLarge", maxValue)); else return new ValidationResult(true); } catch (Throwable t) { return new ValidationResult(false, Res.get("validation.invalidInput", t.getMessage())); } } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/PerfectMoneyValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.core.util.validation.InputValidator; public final class PerfectMoneyValidator extends InputValidator { /////////////////////////////////////////////////////////////////////////////////////////// // Public methods /////////////////////////////////////////////////////////////////////////////////////////// @Override public ValidationResult validate(String input) { // TODO return super.validate(input); } /////////////////////////////////////////////////////////////////////////////////////////// // Private methods /////////////////////////////////////////////////////////////////////////////////////////// } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/PhoneNumberValidator.java ================================================ package haveno.core.payment.validation; import haveno.core.locale.Res; import haveno.core.util.validation.InputValidator; import lombok.Getter; import javax.annotation.Nullable; /** * Performs lenient validation of international phone numbers, and transforms given * input numbers into E.164 international form. The E.164 normalized phone number * can be accessed via {@link #getNormalizedPhoneNumber} after successful * validation -- {@link #getNormalizedPhoneNumber} will return null if validation * fails. *

    * Area codes and mobile provider codes are not validated, but all numbers following * calling codes are included in the normalized number. *

    * @see haveno.core.payment.validation.CountryCallingCodes */ public class PhoneNumberValidator extends InputValidator { /** * ISO 3166-1 alpha-2 country code */ private String isoCountryCode; /** * The international calling code mapped to the 'isoCountryCode' constructor argument. */ @Nullable @Getter private String callingCode; /** * The normalized (digits only) representation of an international calling code. */ private String normalizedCallingCode; /** * Phone number in E.164 format. */ @Nullable @Getter private String normalizedPhoneNumber; /////////////////////////////////////////////////////////////////////////////////////////// // Constructors /////////////////////////////////////////////////////////////////////////////////////////// // Public no-arg constructor required by Guice injector, // but isoCountryCode must be set before validation. public PhoneNumberValidator() { } public PhoneNumberValidator(String isoCountryCode) { this.isoCountryCode = isoCountryCode; this.callingCode = CountryCallingCodes.getCallingCode(isoCountryCode); this.normalizedCallingCode = CountryCallingCodes.getNormalizedCallingCode(isoCountryCode); } /////////////////////////////////////////////////////////////////////////////////////////// // Public methods /////////////////////////////////////////////////////////////////////////////////////////// @Override public ValidationResult validate(String input) { normalizedPhoneNumber = null; ValidationResult result = super.validate(isoCountryCode); if (!result.isValid) { return new ValidationResult(false, Res.get("validation.phone.missingCountryCode")); } result = super.validate(input); if (!result.isValid) { return result; } String trimmedInput = input.trim(); boolean isCountryDialingCodeExplicit = trimmedInput.startsWith("+"); // Remove non-alphanumeric chars. Letters may be left in the pureNumber string for // the isPositiveNumber(pureNumber) test. The US once had listed numbers with letters // -- I had one -- and I'm sure they are still used in some countries. In such cases, // users will be prompted to convert letters to numbers for normalization. String pureNumber = input.replaceAll("[^A-Za-z0-9]", ""); boolean hasValidCallingCodePrefix = pureNumber.startsWith(normalizedCallingCode); boolean isCountryCallingCodeImplicit = !isCountryDialingCodeExplicit && hasValidCallingCodePrefix; result = validateIsNumeric(input, pureNumber) .and(validateHasSufficientDigits(input, pureNumber)) .and(validateIsNotTooLong(input, pureNumber)) .and(validateIncludedCountryDialingCode(input, isCountryDialingCodeExplicit, hasValidCallingCodePrefix)); if (result.isValid) { // TODO corner cases: isCountryCallingCodeImplicit == true // && the country calling code is matched by following digits. // && Is this a problem? if (isCountryDialingCodeExplicit || isCountryCallingCodeImplicit) { normalizedPhoneNumber = "+" + pureNumber; } else { normalizedPhoneNumber = "+" + getCallingCode() + pureNumber; } } return result; } /** * Setter for property 'isoCountryCode'. * * @param isoCountryCode Value to set for property 'isoCountryCode'. */ public void setIsoCountryCode(String isoCountryCode) { this.isoCountryCode = isoCountryCode; this.callingCode = CountryCallingCodes.getCallingCode(isoCountryCode); this.normalizedCallingCode = CountryCallingCodes.getNormalizedCallingCode(isoCountryCode); } /////////////////////////////////////////////////////////////////////////////////////////// // Private methods /////////////////////////////////////////////////////////////////////////////////////////// private ValidationResult validateIsNumeric(String rawInput, String pureNumber) { try { if (isPositiveNumber(pureNumber)) { return new ValidationResult(true); } else { return new ValidationResult(false, Res.get("validation.phone.invalidCharacters", rawInput)); } } catch (Throwable t) { return new ValidationResult(false, Res.get("validation.invalidInput", t.getMessage())); } } private ValidationResult validateHasSufficientDigits(String rawInput, String pureNumber) { try { return ((pureNumber.length() - callingCode.length()) > 4) ? new ValidationResult(true) : new ValidationResult(false, Res.get("validation.phone.insufficientDigits", rawInput)); } catch (Throwable t) { return new ValidationResult(false, Res.get("validation.invalidInput", t.getMessage())); } } private ValidationResult validateIsNotTooLong(String rawInput, String pureNumber) { try { return ((pureNumber.length() - callingCode.length()) > 12) ? new ValidationResult(false, Res.get("validation.phone.tooManyDigits", rawInput)) : new ValidationResult(true); } catch (Throwable t) { return new ValidationResult(false, Res.get("validation.invalidInput", t.getMessage())); } } private ValidationResult validateIncludedCountryDialingCode(String rawInput, boolean isCountryDialingCodeExplicit, boolean hasValidDialingCodePrefix) { try { if (isCountryDialingCodeExplicit && !hasValidDialingCodePrefix) { return new ValidationResult(false, Res.get("validation.phone.invalidDialingCode", rawInput, isoCountryCode, callingCode)); } else { return new ValidationResult(true); } } catch (Throwable t) { return new ValidationResult(false, Res.get("validation.invalidInput", t.getMessage())); } } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/PopmoneyValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.core.util.validation.InputValidator; public final class PopmoneyValidator extends InputValidator { @Override public ValidationResult validate(String input) { // TODO return super.validate(input); } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/PromptPayValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.core.util.validation.InputValidator; public final class PromptPayValidator extends InputValidator { @Override public ValidationResult validate(String input) { // TODO return super.validate(input); } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/RevolutValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; public final class RevolutValidator extends LengthValidator { public RevolutValidator() { // Not sure what are requirements for Revolut usernames // Please keep in mind that even we force users to set username at startup we should handle also the case // that the old accountID as phone number or email is displayed at the username text field and we do not // want to break validation in those cases. So being too strict on the validators might cause more troubles // as its worth... // UPDATE 04/2021: Revolut usernames could be edited (3-16 characters, lowercase a-z and numbers only) super(3, 100); } public ValidationResult validate(String input) { return super.validate(input); } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/SecurityDepositValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import com.google.inject.Inject; import haveno.core.locale.Res; import haveno.core.payment.PaymentAccount; import haveno.core.util.FormattingUtils; import haveno.core.util.ParsingUtils; import haveno.core.util.validation.NumberValidator; import haveno.core.xmr.wallet.Restrictions; public class SecurityDepositValidator extends NumberValidator { private PaymentAccount paymentAccount; @Inject public SecurityDepositValidator() { } public void setPaymentAccount(PaymentAccount paymentAccount) { this.paymentAccount = paymentAccount; } @Override public ValidationResult validate(String input) { ValidationResult result = validateIfNotEmpty(input); if (result.isValid) { input = cleanInput(input); result = validateIfNumber(input); } if (result.isValid) { result = result.andValidation(input, this::validateIfNotZero, this::validateIfNotNegative, this::validateIfNotTooLowPercentageValue, this::validateIfNotTooHighPercentageValue); } return result; } private ValidationResult validateIfNotTooLowPercentageValue(String input) { try { double percentage = ParsingUtils.parsePercentStringToDouble(input); double minPercentage = Restrictions.getMinSecurityDepositPct(); if (percentage < minPercentage) return new ValidationResult(false, Res.get("validation.inputTooSmall", FormattingUtils.formatToPercentWithSymbol(minPercentage))); else return new ValidationResult(true); } catch (Throwable t) { return new ValidationResult(false, Res.get("validation.invalidInput", t.getMessage())); } } private ValidationResult validateIfNotTooHighPercentageValue(String input) { try { double percentage = ParsingUtils.parsePercentStringToDouble(input); double maxPercentage = Restrictions.getMaxSecurityDepositPct(); if (percentage > maxPercentage) return new ValidationResult(false, Res.get("validation.inputTooLarge", FormattingUtils.formatToPercentWithSymbol(maxPercentage))); else return new ValidationResult(true); } catch (Throwable t) { return new ValidationResult(false, Res.get("validation.invalidInput", t.getMessage())); } } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/SepaIBANValidator.java ================================================ package haveno.core.payment.validation; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import java.util.List; import java.util.Optional; public class SepaIBANValidator extends IBANValidator { @Override public ValidationResult validate(String input) { ValidationResult result = super.validate(input); if (result.isValid) { List sepaCountries = CountryUtil.getAllSepaCountries(); String ibanCountryCode = input.substring(0, 2).toUpperCase(); Optional ibanCountry = sepaCountries .stream() .filter(c -> c.code.equals(ibanCountryCode)) .findFirst(); if (!ibanCountry.isPresent()) { return new ValidationResult(false, Res.get("validation.iban.sepaNotSupported")); } } return result; } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/SwishValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; public final class SwishValidator extends PhoneNumberValidator { /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// // Public no-arg constructor required by Guice injector. // Superclass' isoCountryCode must be set before validation. public SwishValidator() { this.setIsoCountryCode("SE"); } /////////////////////////////////////////////////////////////////////////////////////////// // Public methods /////////////////////////////////////////////////////////////////////////////////////////// @Override public ValidationResult validate(String input) { return super.validate(input); } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/TransferwiseValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import com.google.inject.Inject; import haveno.core.util.validation.InputValidator; public final class TransferwiseValidator extends InputValidator { private final EmailValidator emailValidator; @Inject public TransferwiseValidator(EmailValidator emailValidator) { this.emailValidator = emailValidator; } @Override public ValidationResult validate(String input) { ValidationResult result = super.validate(input); if (!result.isValid) return result; return emailValidator.validate(input); } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/USPostalMoneyOrderValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.core.util.validation.InputValidator; public final class USPostalMoneyOrderValidator extends InputValidator { /////////////////////////////////////////////////////////////////////////////////////////// // Public methods /////////////////////////////////////////////////////////////////////////////////////////// @Override public ValidationResult validate(String input) { // TODO return super.validate(input); } /////////////////////////////////////////////////////////////////////////////////////////// // Private methods /////////////////////////////////////////////////////////////////////////////////////////// } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/UpholdValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.core.util.validation.InputValidator; public final class UpholdValidator extends InputValidator { @Override public ValidationResult validate(String input) { // TODO return super.validate(input); } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/WeChatPayValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.core.util.validation.InputValidator; public final class WeChatPayValidator extends InputValidator { @Override public ValidationResult validate(String input) { // TODO return super.validate(input); } } ================================================ FILE: core/src/main/java/haveno/core/payment/validation/XmrValidator.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import com.google.inject.Inject; import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; import haveno.core.util.validation.NumberValidator; import java.math.BigDecimal; import java.math.BigInteger; import javax.annotation.Nullable; import lombok.Getter; import lombok.Setter; public class XmrValidator extends NumberValidator { @Nullable @Setter protected BigInteger minValue; @Nullable @Setter protected BigInteger maxValue; @Nullable @Setter @Getter protected BigInteger maxTradeLimit; @Inject public XmrValidator() { } @Override public ValidationResult validate(String input) { ValidationResult result = validateIfNotEmpty(input); if (result.isValid) { input = cleanInput(input); result = validateIfNumber(input); } if (result.isValid) { result = result.andValidation(input, this::validateIfNotZero, this::validateIfNotNegative, this::validateIfNotFractionalXmrValue, this::validateIfNotExceedsMaxTradeLimit, this::validateIfNotExceedsMaxValue, this::validateIfNotUnderMinValue); } return result; } protected ValidationResult validateIfNotFractionalXmrValue(String input) { try { BigDecimal bd = new BigDecimal(input); final BigDecimal atomicUnits = bd.movePointRight(HavenoUtils.XMR_SMALLEST_UNIT_EXPONENT); if (atomicUnits.scale() > 0) return new ValidationResult(false, Res.get("validation.xmr.fraction")); else return new ValidationResult(true); } catch (Throwable t) { return new ValidationResult(false, Res.get("validation.invalidInput", t.getMessage())); } } protected ValidationResult validateIfNotExceedsMaxValue(String input) { try { final BigInteger amount = HavenoUtils.parseXmr(input); if (maxValue != null && amount.compareTo(maxValue) > 0) return new ValidationResult(false, Res.get("validation.xmr.tooLarge", HavenoUtils.formatXmr(maxValue, true))); else return new ValidationResult(true); } catch (Throwable t) { return new ValidationResult(false, Res.get("validation.invalidInput", t.getMessage())); } } protected ValidationResult validateIfNotExceedsMaxTradeLimit(String input) { try { final BigInteger amount = HavenoUtils.parseXmr(input); if (maxTradeLimit != null && amount.compareTo(maxTradeLimit) > 0) return new ValidationResult(false, Res.get("validation.xmr.exceedsMaxTradeLimit", HavenoUtils.formatXmr(maxTradeLimit, true))); else return new ValidationResult(true); } catch (Throwable t) { return new ValidationResult(false, Res.get("validation.invalidInput", t.getMessage())); } } protected ValidationResult validateIfNotUnderMinValue(String input) { try { final BigInteger amount = HavenoUtils.parseXmr(input); if (minValue != null && amount.compareTo(minValue) < 0) return new ValidationResult(false, Res.get("validation.xmr.tooSmall", HavenoUtils.formatXmr(minValue))); else return new ValidationResult(true); } catch (Throwable t) { return new ValidationResult(false, Res.get("validation.invalidInput", t.getMessage())); } } } ================================================ FILE: core/src/main/java/haveno/core/presentation/BalancePresentation.java ================================================ /* * 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 . */ package haveno.core.presentation; import com.google.inject.Inject; import haveno.common.UserThread; import haveno.core.api.model.XmrBalanceInfo; import haveno.core.trade.HavenoUtils; import haveno.core.xmr.Balances; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @Slf4j public class BalancePresentation { @Getter private final StringProperty availableBalance = new SimpleStringProperty(); @Getter private final StringProperty pendingBalance = new SimpleStringProperty(); @Getter private final StringProperty reservedBalance = new SimpleStringProperty(); @Inject public BalancePresentation(Balances balances) { balances.getUpdateCounter().addListener((observable, oldValue, newValue) -> { XmrBalanceInfo info = balances.getBalances(); UserThread.execute(() -> { availableBalance.set(HavenoUtils.formatXmr(info.getAvailableBalance(), true)); pendingBalance.set(HavenoUtils.formatXmr(info.getPendingBalance(), true)); reservedBalance.set(HavenoUtils.formatXmr(info.getReservedBalance(), true)); }); }); } } ================================================ FILE: core/src/main/java/haveno/core/presentation/CorePresentationModule.java ================================================ /* * 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 . */ package haveno.core.presentation; import com.google.inject.Singleton; import haveno.common.app.AppModule; import haveno.common.config.Config; public class CorePresentationModule extends AppModule { public CorePresentationModule(Config config) { super(config); } @Override protected void configure() { bind(BalancePresentation.class).in(Singleton.class); bind(TradePresentation.class).in(Singleton.class); bind(SupportTicketsPresentation.class).in(Singleton.class); } } ================================================ FILE: core/src/main/java/haveno/core/presentation/SupportTicketsPresentation.java ================================================ /* * 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 . */ package haveno.core.presentation; import com.google.inject.Inject; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.mediation.MediationManager; import haveno.core.support.dispute.refund.RefundManager; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import lombok.Getter; public class SupportTicketsPresentation { @Getter private final StringProperty numOpenSupportTickets = new SimpleStringProperty(); @Getter private final BooleanProperty showOpenSupportTicketsNotification = new SimpleBooleanProperty(); @org.jetbrains.annotations.NotNull private final ArbitrationManager arbitrationManager; @org.jetbrains.annotations.NotNull private final MediationManager mediationManager; @org.jetbrains.annotations.NotNull private final RefundManager refundManager; @Inject public SupportTicketsPresentation(ArbitrationManager arbitrationManager, MediationManager mediationManager, RefundManager refundManager) { this.arbitrationManager = arbitrationManager; this.mediationManager = mediationManager; this.refundManager = refundManager; arbitrationManager.getNumOpenDisputes().addListener((observable, oldValue, newValue) -> onChange()); mediationManager.getNumOpenDisputes().addListener((observable, oldValue, newValue) -> onChange()); refundManager.getNumOpenDisputes().addListener((observable, oldValue, newValue) -> onChange()); } private void onChange() { int supportTickets = arbitrationManager.getNumOpenDisputes().get() + mediationManager.getNumOpenDisputes().get() + refundManager.getNumOpenDisputes().get(); numOpenSupportTickets.set(String.valueOf(supportTickets)); showOpenSupportTicketsNotification.set(supportTickets > 0); } } ================================================ FILE: core/src/main/java/haveno/core/presentation/TradePresentation.java ================================================ /* * 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 . */ package haveno.core.presentation; import com.google.inject.Inject; import haveno.common.UserThread; import haveno.core.trade.TradeManager; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import lombok.Getter; public class TradePresentation { @Getter private final StringProperty numPendingTrades = new SimpleStringProperty(); @Getter private final BooleanProperty showPendingTradesNotification = new SimpleBooleanProperty(); @Inject public TradePresentation(TradeManager tradeManager) { tradeManager.getNumPendingTrades().addListener((observable, oldValue, newValue) -> { long numPendingTrades = (long) newValue; UserThread.execute(() -> { if (numPendingTrades > 0) this.numPendingTrades.set(String.valueOf(numPendingTrades)); showPendingTradesNotification.set(numPendingTrades > 0); }); }); } } ================================================ FILE: core/src/main/java/haveno/core/proto/CoreProtoResolver.java ================================================ /* * 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 . */ package haveno.core.proto; import haveno.common.proto.ProtoResolver; import haveno.common.proto.ProtobufferRuntimeException; import haveno.common.proto.persistable.PersistablePayload; import haveno.core.account.sign.SignedWitness; import haveno.core.account.witness.AccountAgeWitness; import haveno.core.payment.payload.AchTransferAccountPayload; import haveno.core.payment.payload.AdvancedCashAccountPayload; import haveno.core.payment.payload.AliPayAccountPayload; import haveno.core.payment.payload.AmazonGiftCardAccountPayload; import haveno.core.payment.payload.AustraliaPayidAccountPayload; import haveno.core.payment.payload.BizumAccountPayload; import haveno.core.payment.payload.CapitualAccountPayload; import haveno.core.payment.payload.CashAppAccountPayload; import haveno.core.payment.payload.CashAtAtmAccountPayload; import haveno.core.payment.payload.PayByMailAccountPayload; import haveno.core.payment.payload.CashDepositAccountPayload; import haveno.core.payment.payload.CelPayAccountPayload; import haveno.core.payment.payload.ChaseQuickPayAccountPayload; import haveno.core.payment.payload.ZelleAccountPayload; import haveno.core.payment.payload.CryptoCurrencyAccountPayload; import haveno.core.payment.payload.DomesticWireTransferAccountPayload; import haveno.core.payment.payload.F2FAccountPayload; import haveno.core.payment.payload.FasterPaymentsAccountPayload; import haveno.core.payment.payload.HalCashAccountPayload; import haveno.core.payment.payload.ImpsAccountPayload; import haveno.core.payment.payload.InstantCryptoCurrencyPayload; import haveno.core.payment.payload.InteracETransferAccountPayload; import haveno.core.payment.payload.JapanBankAccountPayload; import haveno.core.payment.payload.MoneseAccountPayload; import haveno.core.payment.payload.MoneyBeamAccountPayload; import haveno.core.payment.payload.MoneyGramAccountPayload; import haveno.core.payment.payload.NationalBankAccountPayload; import haveno.core.payment.payload.NeftAccountPayload; import haveno.core.payment.payload.NequiAccountPayload; import haveno.core.payment.payload.OKPayAccountPayload; import haveno.core.payment.payload.PaxumAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaysafeAccountPayload; import haveno.core.payment.payload.PayPalAccountPayload; import haveno.core.payment.payload.PayseraAccountPayload; import haveno.core.payment.payload.PaytmAccountPayload; import haveno.core.payment.payload.PerfectMoneyAccountPayload; import haveno.core.payment.payload.PixAccountPayload; import haveno.core.payment.payload.PopmoneyAccountPayload; import haveno.core.payment.payload.PromptPayAccountPayload; import haveno.core.payment.payload.RevolutAccountPayload; import haveno.core.payment.payload.RtgsAccountPayload; import haveno.core.payment.payload.SameBankAccountPayload; import haveno.core.payment.payload.SatispayAccountPayload; import haveno.core.payment.payload.SepaAccountPayload; import haveno.core.payment.payload.SepaInstantAccountPayload; import haveno.core.payment.payload.SpecificBanksAccountPayload; import haveno.core.payment.payload.StrikeAccountPayload; import haveno.core.payment.payload.SwiftAccountPayload; import haveno.core.payment.payload.SwishAccountPayload; import haveno.core.payment.payload.TikkieAccountPayload; import haveno.core.payment.payload.TransferwiseAccountPayload; import haveno.core.payment.payload.TransferwiseUsdAccountPayload; import haveno.core.payment.payload.USPostalMoneyOrderAccountPayload; import haveno.core.payment.payload.UpholdAccountPayload; import haveno.core.payment.payload.UpiAccountPayload; import haveno.core.payment.payload.VenmoAccountPayload; import haveno.core.payment.payload.VerseAccountPayload; import haveno.core.payment.payload.WeChatPayAccountPayload; import haveno.core.payment.payload.WesternUnionAccountPayload; import haveno.core.trade.statistics.TradeStatistics3; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.time.Clock; @Slf4j public class CoreProtoResolver implements ProtoResolver { @Getter protected Clock clock; @Override public PaymentAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { if (proto != null) { final protobuf.PaymentAccountPayload.MessageCase messageCase = proto.getMessageCase(); switch (messageCase) { case ALI_PAY_ACCOUNT_PAYLOAD: return AliPayAccountPayload.fromProto(proto); case WE_CHAT_PAY_ACCOUNT_PAYLOAD: return WeChatPayAccountPayload.fromProto(proto); case CHASE_QUICK_PAY_ACCOUNT_PAYLOAD: return ChaseQuickPayAccountPayload.fromProto(proto); case ZELLE_ACCOUNT_PAYLOAD: return ZelleAccountPayload.fromProto(proto); case COUNTRY_BASED_PAYMENT_ACCOUNT_PAYLOAD: final protobuf.CountryBasedPaymentAccountPayload.MessageCase messageCaseCountry = proto.getCountryBasedPaymentAccountPayload().getMessageCase(); switch (messageCaseCountry) { case BANK_ACCOUNT_PAYLOAD: final protobuf.BankAccountPayload.MessageCase messageCaseBank = proto.getCountryBasedPaymentAccountPayload().getBankAccountPayload().getMessageCase(); switch (messageCaseBank) { case NATIONAL_BANK_ACCOUNT_PAYLOAD: return NationalBankAccountPayload.fromProto(proto); case SAME_BANK_ACCONT_PAYLOAD: return SameBankAccountPayload.fromProto(proto); case SPECIFIC_BANKS_ACCOUNT_PAYLOAD: return SpecificBanksAccountPayload.fromProto(proto); case ACH_TRANSFER_ACCOUNT_PAYLOAD: return AchTransferAccountPayload.fromProto(proto); case DOMESTIC_WIRE_TRANSFER_ACCOUNT_PAYLOAD: return DomesticWireTransferAccountPayload.fromProto(proto); default: throw new ProtobufferRuntimeException("Unknown proto message case" + "(PB.PaymentAccountPayload.CountryBasedPaymentAccountPayload.BankAccountPayload). " + "messageCase=" + messageCaseBank); } case WESTERN_UNION_ACCOUNT_PAYLOAD: return WesternUnionAccountPayload.fromProto(proto); case CASH_DEPOSIT_ACCOUNT_PAYLOAD: return CashDepositAccountPayload.fromProto(proto); case SEPA_ACCOUNT_PAYLOAD: return SepaAccountPayload.fromProto(proto); case SEPA_INSTANT_ACCOUNT_PAYLOAD: return SepaInstantAccountPayload.fromProto(proto); case F2F_ACCOUNT_PAYLOAD: return F2FAccountPayload.fromProto(proto); case UPI_ACCOUNT_PAYLOAD: return UpiAccountPayload.fromProto(proto); case PAYTM_ACCOUNT_PAYLOAD: return PaytmAccountPayload.fromProto(proto); case NEQUI_ACCOUNT_PAYLOAD: return NequiAccountPayload.fromProto(proto); case BIZUM_ACCOUNT_PAYLOAD: return BizumAccountPayload.fromProto(proto); case PIX_ACCOUNT_PAYLOAD: return PixAccountPayload.fromProto(proto); case SATISPAY_ACCOUNT_PAYLOAD: return SatispayAccountPayload.fromProto(proto); case TIKKIE_ACCOUNT_PAYLOAD: return TikkieAccountPayload.fromProto(proto); case STRIKE_ACCOUNT_PAYLOAD: return StrikeAccountPayload.fromProto(proto); case TRANSFERWISE_USD_ACCOUNT_PAYLOAD: return TransferwiseUsdAccountPayload.fromProto(proto); case IFSC_BASED_ACCOUNT_PAYLOAD: final protobuf.IfscBasedAccountPayload.MessageCase messageCaseIfsc = proto.getCountryBasedPaymentAccountPayload().getIfscBasedAccountPayload().getMessageCase(); switch (messageCaseIfsc) { case NEFT_ACCOUNT_PAYLOAD: return NeftAccountPayload.fromProto(proto); case RTGS_ACCOUNT_PAYLOAD: return RtgsAccountPayload.fromProto(proto); case IMPS_ACCOUNT_PAYLOAD: return ImpsAccountPayload.fromProto(proto); default: throw new ProtobufferRuntimeException("Unknown proto message case" + "(PB.PaymentAccountPayload.CountryBasedPaymentAccountPayload.IfscBasedPaymentAccount). " + "messageCase=" + messageCaseIfsc); } default: throw new ProtobufferRuntimeException("Unknown proto message case" + "(PB.PaymentAccountPayload.CountryBasedPaymentAccountPayload)." + " messageCase=" + messageCaseCountry); } case CRYPTO_CURRENCY_ACCOUNT_PAYLOAD: return CryptoCurrencyAccountPayload.fromProto(proto); case FASTER_PAYMENTS_ACCOUNT_PAYLOAD: return FasterPaymentsAccountPayload.fromProto(proto); case INTERAC_E_TRANSFER_ACCOUNT_PAYLOAD: return InteracETransferAccountPayload.fromProto(proto); case JAPAN_BANK_ACCOUNT_PAYLOAD: return JapanBankAccountPayload.fromProto(proto); case AUSTRALIA_PAYID_PAYLOAD: return AustraliaPayidAccountPayload.fromProto(proto); case UPHOLD_ACCOUNT_PAYLOAD: return UpholdAccountPayload.fromProto(proto); case MONEY_BEAM_ACCOUNT_PAYLOAD: return MoneyBeamAccountPayload.fromProto(proto); case MONEY_GRAM_ACCOUNT_PAYLOAD: return MoneyGramAccountPayload.fromProto(proto); case POPMONEY_ACCOUNT_PAYLOAD: return PopmoneyAccountPayload.fromProto(proto); case REVOLUT_ACCOUNT_PAYLOAD: return RevolutAccountPayload.fromProto(proto); case PERFECT_MONEY_ACCOUNT_PAYLOAD: return PerfectMoneyAccountPayload.fromProto(proto); case SWISH_ACCOUNT_PAYLOAD: return SwishAccountPayload.fromProto(proto); case HAL_CASH_ACCOUNT_PAYLOAD: return HalCashAccountPayload.fromProto(proto); case U_S_POSTAL_MONEY_ORDER_ACCOUNT_PAYLOAD: return USPostalMoneyOrderAccountPayload.fromProto(proto); case PAY_BY_MAIL_ACCOUNT_PAYLOAD: return PayByMailAccountPayload.fromProto(proto); case CASH_AT_ATM_ACCOUNT_PAYLOAD: return CashAtAtmAccountPayload.fromProto(proto); case PROMPT_PAY_ACCOUNT_PAYLOAD: return PromptPayAccountPayload.fromProto(proto); case ADVANCED_CASH_ACCOUNT_PAYLOAD: return AdvancedCashAccountPayload.fromProto(proto); case TRANSFERWISE_ACCOUNT_PAYLOAD: return TransferwiseAccountPayload.fromProto(proto); case PAYSERA_ACCOUNT_PAYLOAD: return PayseraAccountPayload.fromProto(proto); case PAXUM_ACCOUNT_PAYLOAD: return PaxumAccountPayload.fromProto(proto); case AMAZON_GIFT_CARD_ACCOUNT_PAYLOAD: return AmazonGiftCardAccountPayload.fromProto(proto); case INSTANT_CRYPTO_CURRENCY_ACCOUNT_PAYLOAD: return InstantCryptoCurrencyPayload.fromProto(proto); case CAPITUAL_ACCOUNT_PAYLOAD: return CapitualAccountPayload.fromProto(proto); case CEL_PAY_ACCOUNT_PAYLOAD: return CelPayAccountPayload.fromProto(proto); case MONESE_ACCOUNT_PAYLOAD: return MoneseAccountPayload.fromProto(proto); case VERSE_ACCOUNT_PAYLOAD: return VerseAccountPayload.fromProto(proto); case SWIFT_ACCOUNT_PAYLOAD: return SwiftAccountPayload.fromProto(proto); // Cannot be deleted as it would break old trade history entries case O_K_PAY_ACCOUNT_PAYLOAD: return OKPayAccountPayload.fromProto(proto); case CASH_APP_ACCOUNT_PAYLOAD: return CashAppAccountPayload.fromProto(proto); case VENMO_ACCOUNT_PAYLOAD: return VenmoAccountPayload.fromProto(proto); case PAYPAL_ACCOUNT_PAYLOAD: return PayPalAccountPayload.fromProto(proto); case PAYSAFE_ACCOUNT_PAYLOAD: return PaysafeAccountPayload.fromProto(proto); default: throw new ProtobufferRuntimeException("Unknown proto message case(PB.PaymentAccountPayload). messageCase=" + messageCase); } } else { log.error("PersistableEnvelope.fromProto: PB.PaymentAccountPayload is null"); throw new ProtobufferRuntimeException("PB.PaymentAccountPayload is null"); } } @Override public PersistablePayload fromProto(protobuf.PersistableNetworkPayload proto) { if (proto != null) { switch (proto.getMessageCase()) { case ACCOUNT_AGE_WITNESS: return AccountAgeWitness.fromProto(proto.getAccountAgeWitness()); case SIGNED_WITNESS: return SignedWitness.fromProto(proto.getSignedWitness()); case TRADE_STATISTICS3: return TradeStatistics3.fromProto(proto.getTradeStatistics3()); default: throw new ProtobufferRuntimeException("Unknown proto message case (PB.PersistableNetworkPayload). messageCase=" + proto.getMessageCase()); } } else { log.error("PB.PersistableNetworkPayload is null"); throw new ProtobufferRuntimeException("PB.PersistableNetworkPayload is null"); } } } ================================================ FILE: core/src/main/java/haveno/core/proto/ProtoDevUtil.java ================================================ /* * 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 . */ package haveno.core.proto; import haveno.core.offer.AvailabilityResult; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OpenOffer; import haveno.core.support.dispute.DisputeResult; import haveno.core.trade.Trade; import haveno.core.xmr.model.AddressEntry; import lombok.extern.slf4j.Slf4j; @Slf4j public class ProtoDevUtil { // Util for auto generating enum values used in pb definition public static void printAllEnumsForPB() { StringBuilder sb = new StringBuilder("\n enum State {\n"); sb.append(" PB_ERROR = 0;\n"); for (int i = 0; i < Trade.State.values().length; i++) { Trade.State s = Trade.State.values()[i]; sb.append(" "); sb.append(s.toString()); sb.append(" = "); sb.append(s.ordinal() + 1); sb.append(";\n"); } sb.append(" }\n\n"); sb.append(" enum Phase {\n"); sb.append(" PB_ERROR = 0;\n"); for (int i = 0; i < Trade.Phase.values().length; i++) { Trade.Phase s = Trade.Phase.values()[i]; sb.append(" "); sb.append(s.toString()); sb.append(" = "); sb.append(s.ordinal() + 1); sb.append(";\n"); } sb.append(" }\n\n\n"); sb.append(" enum DisputeState {\n"); sb.append(" PB_ERROR = 0;\n"); for (int i = 0; i < Trade.DisputeState.values().length; i++) { Trade.DisputeState s = Trade.DisputeState.values()[i]; sb.append(" "); sb.append(s.toString()); sb.append(" = "); sb.append(s.ordinal() + 1); sb.append(";\n"); } sb.append(" }\n\n\n"); sb.append(" enum TradePeriodState {\n"); sb.append(" PB_ERROR = 0;\n"); for (int i = 0; i < Trade.TradePeriodState.values().length; i++) { Trade.TradePeriodState s = Trade.TradePeriodState.values()[i]; sb.append(" "); sb.append(s.toString()); sb.append(" = "); sb.append(s.ordinal() + 1); sb.append(";\n"); } sb.append(" }\n\n\n"); sb.append(" enum Direction {\n"); sb.append(" PB_ERROR = 0;\n"); for (int i = 0; i < OfferDirection.values().length; i++) { OfferDirection s = OfferDirection.values()[i]; sb.append(" "); sb.append(s.toString()); sb.append(" = "); sb.append(s.ordinal() + 1); sb.append(";\n"); } sb.append(" }\n\n\n"); sb.append(" enum Winner {\n"); sb.append(" PB_ERROR = 0;\n"); for (int i = 0; i < DisputeResult.Winner.values().length; i++) { DisputeResult.Winner s = DisputeResult.Winner.values()[i]; sb.append(" "); sb.append(s.toString()); sb.append(" = "); sb.append(s.ordinal() + 1); sb.append(";\n"); } sb.append(" }\n\n\n"); sb.append(" enum Reason {\n"); sb.append(" PB_ERROR = 0;\n"); for (int i = 0; i < DisputeResult.Reason.values().length; i++) { DisputeResult.Reason s = DisputeResult.Reason.values()[i]; sb.append(" "); sb.append(s.toString()); sb.append(" = "); sb.append(s.ordinal() + 1); sb.append(";\n"); } sb.append(" }\n\n\n"); sb.append(" enum AvailabilityResult {\n"); sb.append(" PB_ERROR = 0;\n"); for (int i = 0; i < AvailabilityResult.values().length; i++) { AvailabilityResult s = AvailabilityResult.values()[i]; sb.append(" "); sb.append(s.toString()); sb.append(" = "); sb.append(s.ordinal() + 1); sb.append(";\n"); } sb.append(" }\n\n\n"); sb.append(" enum Context {\n"); sb.append(" PB_ERROR = 0;\n"); for (int i = 0; i < AddressEntry.Context.values().length; i++) { AddressEntry.Context s = AddressEntry.Context.values()[i]; sb.append(" "); sb.append(s.toString()); sb.append(" = "); sb.append(s.ordinal() + 1); sb.append(";\n"); } sb.append(" }\n\n\n"); sb.append(" enum State {\n"); sb.append(" PB_ERROR = 0;\n"); for (int i = 0; i < Offer.State.values().length; i++) { Offer.State s = Offer.State.values()[i]; sb.append(" "); sb.append(s.toString()); sb.append(" = "); sb.append(s.ordinal() + 1); sb.append(";\n"); } sb.append(" }\n\n\n"); sb.append(" enum State {\n"); sb.append(" PB_ERROR = 0;\n"); for (int i = 0; i < OpenOffer.State.values().length; i++) { OpenOffer.State s = OpenOffer.State.values()[i]; sb.append(" "); sb.append(s.toString()); sb.append(" = "); sb.append(s.ordinal() + 1); sb.append(";\n"); } sb.append(" }\n\n\n"); log.info(sb.toString()); } } ================================================ FILE: core/src/main/java/haveno/core/proto/network/CoreNetworkProtoResolver.java ================================================ /* * 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 . */ package haveno.core.proto.network; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.proto.ProtobufferException; import haveno.common.proto.ProtobufferRuntimeException; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.proto.network.NetworkPayload; import haveno.common.proto.network.NetworkProtoResolver; import haveno.core.alert.Alert; import haveno.core.alert.PrivateNotificationMessage; import haveno.core.filter.Filter; import haveno.core.network.p2p.inventory.messages.GetInventoryRequest; import haveno.core.network.p2p.inventory.messages.GetInventoryResponse; import haveno.core.offer.OfferPayload; import haveno.core.offer.messages.OfferAvailabilityRequest; import haveno.core.offer.messages.OfferAvailabilityResponse; import haveno.core.offer.messages.SignOfferRequest; import haveno.core.offer.messages.SignOfferResponse; import haveno.core.proto.CoreProtoResolver; import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; import haveno.core.support.dispute.mediation.mediator.Mediator; import haveno.core.support.dispute.messages.DisputeClosedMessage; import haveno.core.support.dispute.messages.DisputeOpenedMessage; import haveno.core.support.dispute.refund.refundagent.RefundAgent; import haveno.core.support.messages.ChatMessage; import haveno.core.trade.messages.DepositRequest; import haveno.core.trade.messages.DepositResponse; import haveno.core.trade.messages.DepositsConfirmedMessage; import haveno.core.trade.messages.InitMultisigRequest; import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.messages.MediatedPayoutTxPublishedMessage; import haveno.core.trade.messages.MediatedPayoutTxSignatureMessage; import haveno.core.trade.messages.PaymentReceivedMessage; import haveno.core.trade.messages.PaymentSentMessage; import haveno.core.trade.messages.SignContractRequest; import haveno.core.trade.messages.SignContractResponse; import haveno.network.p2p.AckMessage; import haveno.network.p2p.BundleOfEnvelopes; import haveno.network.p2p.CloseConnectionMessage; import haveno.network.p2p.FileTransferPart; import haveno.network.p2p.PrefixedSealedAndSignedMessage; import haveno.network.p2p.peers.getdata.messages.GetDataResponse; import haveno.network.p2p.peers.getdata.messages.GetUpdatedDataRequest; import haveno.network.p2p.peers.getdata.messages.PreliminaryGetDataRequest; import haveno.network.p2p.peers.keepalive.messages.Ping; import haveno.network.p2p.peers.keepalive.messages.Pong; import haveno.network.p2p.peers.peerexchange.messages.GetPeersRequest; import haveno.network.p2p.peers.peerexchange.messages.GetPeersResponse; import haveno.network.p2p.storage.messages.AddDataMessage; import haveno.network.p2p.storage.messages.AddPersistableNetworkPayloadMessage; import haveno.network.p2p.storage.messages.RefreshOfferMessage; import haveno.network.p2p.storage.messages.RemoveDataMessage; import haveno.network.p2p.storage.messages.RemoveMailboxDataMessage; import haveno.network.p2p.storage.payload.MailboxStoragePayload; import haveno.network.p2p.storage.payload.ProtectedMailboxStorageEntry; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import java.time.Clock; import lombok.extern.slf4j.Slf4j; // TODO Use ProtobufferException instead of ProtobufferRuntimeException @Slf4j @Singleton public class CoreNetworkProtoResolver extends CoreProtoResolver implements NetworkProtoResolver { @Inject public CoreNetworkProtoResolver(Clock clock) { this.clock = clock; } @Override public NetworkEnvelope fromProto(protobuf.NetworkEnvelope proto) throws ProtobufferException { if (proto != null) { final String messageVersion = proto.getMessageVersion(); switch (proto.getMessageCase()) { case PRELIMINARY_GET_DATA_REQUEST: return PreliminaryGetDataRequest.fromProto(proto.getPreliminaryGetDataRequest(), messageVersion); case GET_DATA_RESPONSE: return GetDataResponse.fromProto(proto.getGetDataResponse(), this, messageVersion); case GET_UPDATED_DATA_REQUEST: return GetUpdatedDataRequest.fromProto(proto.getGetUpdatedDataRequest(), messageVersion); case GET_PEERS_REQUEST: return GetPeersRequest.fromProto(proto.getGetPeersRequest(), messageVersion); case GET_PEERS_RESPONSE: return GetPeersResponse.fromProto(proto.getGetPeersResponse(), messageVersion); case PING: return Ping.fromProto(proto.getPing(), messageVersion); case PONG: return Pong.fromProto(proto.getPong(), messageVersion); case SIGN_OFFER_REQUEST: return SignOfferRequest.fromProto(proto.getSignOfferRequest(), messageVersion); case SIGN_OFFER_RESPONSE: return SignOfferResponse.fromProto(proto.getSignOfferResponse(), messageVersion); case OFFER_AVAILABILITY_REQUEST: return OfferAvailabilityRequest.fromProto(proto.getOfferAvailabilityRequest(), this, messageVersion); case OFFER_AVAILABILITY_RESPONSE: return OfferAvailabilityResponse.fromProto(proto.getOfferAvailabilityResponse(), messageVersion); case REFRESH_OFFER_MESSAGE: return RefreshOfferMessage.fromProto(proto.getRefreshOfferMessage(), messageVersion); case ADD_DATA_MESSAGE: return AddDataMessage.fromProto(proto.getAddDataMessage(), this, messageVersion); case REMOVE_DATA_MESSAGE: return RemoveDataMessage.fromProto(proto.getRemoveDataMessage(), this, messageVersion); case REMOVE_MAILBOX_DATA_MESSAGE: return RemoveMailboxDataMessage.fromProto(proto.getRemoveMailboxDataMessage(), this, messageVersion); case CLOSE_CONNECTION_MESSAGE: return CloseConnectionMessage.fromProto(proto.getCloseConnectionMessage(), messageVersion); case PREFIXED_SEALED_AND_SIGNED_MESSAGE: return PrefixedSealedAndSignedMessage.fromProto(proto.getPrefixedSealedAndSignedMessage(), messageVersion); case INIT_TRADE_REQUEST: return InitTradeRequest.fromProto(proto.getInitTradeRequest(), this, messageVersion); case INIT_MULTISIG_REQUEST: return InitMultisigRequest.fromProto(proto.getInitMultisigRequest(), this, messageVersion); case SIGN_CONTRACT_REQUEST: return SignContractRequest.fromProto(proto.getSignContractRequest(), this, messageVersion); case SIGN_CONTRACT_RESPONSE: return SignContractResponse.fromProto(proto.getSignContractResponse(), this, messageVersion); case DEPOSIT_REQUEST: return DepositRequest.fromProto(proto.getDepositRequest(), this, messageVersion); case DEPOSIT_RESPONSE: return DepositResponse.fromProto(proto.getDepositResponse(), this, messageVersion); case DEPOSITS_CONFIRMED_MESSAGE: return DepositsConfirmedMessage.fromProto(proto.getDepositsConfirmedMessage(), this, messageVersion); case PAYMENT_SENT_MESSAGE: return PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion); case PAYMENT_RECEIVED_MESSAGE: return PaymentReceivedMessage.fromProto(proto.getPaymentReceivedMessage(), messageVersion); case MEDIATED_PAYOUT_TX_SIGNATURE_MESSAGE: return MediatedPayoutTxSignatureMessage.fromProto(proto.getMediatedPayoutTxSignatureMessage(), messageVersion); case MEDIATED_PAYOUT_TX_PUBLISHED_MESSAGE: return MediatedPayoutTxPublishedMessage.fromProto(proto.getMediatedPayoutTxPublishedMessage(), messageVersion); case DISPUTE_OPENED_MESSAGE: return DisputeOpenedMessage.fromProto(proto.getDisputeOpenedMessage(), this, messageVersion); case DISPUTE_CLOSED_MESSAGE: return DisputeClosedMessage.fromProto(proto.getDisputeClosedMessage(), messageVersion); case CHAT_MESSAGE: return ChatMessage.fromProto(proto.getChatMessage(), messageVersion); case PRIVATE_NOTIFICATION_MESSAGE: return PrivateNotificationMessage.fromProto(proto.getPrivateNotificationMessage(), messageVersion); case ADD_PERSISTABLE_NETWORK_PAYLOAD_MESSAGE: return AddPersistableNetworkPayloadMessage.fromProto(proto.getAddPersistableNetworkPayloadMessage(), this, messageVersion); case ACK_MESSAGE: return AckMessage.fromProto(proto.getAckMessage(), messageVersion); case BUNDLE_OF_ENVELOPES: return BundleOfEnvelopes.fromProto(proto.getBundleOfEnvelopes(), this, messageVersion); case GET_INVENTORY_REQUEST: return GetInventoryRequest.fromProto(proto.getGetInventoryRequest(), messageVersion); case GET_INVENTORY_RESPONSE: return GetInventoryResponse.fromProto(proto.getGetInventoryResponse(), messageVersion); case FILE_TRANSFER_PART: return FileTransferPart.fromProto(proto.getFileTransferPart(), messageVersion); default: throw new ProtobufferException("Unknown proto message case (PB.NetworkEnvelope). messageCase=" + proto.getMessageCase() + "; proto raw data=" + proto.toString()); } } else { log.error("PersistableEnvelope.fromProto: PB.NetworkEnvelope is null"); throw new ProtobufferException("PB.NetworkEnvelope is null"); } } @Override public NetworkPayload fromProto(protobuf.StorageEntryWrapper proto) { if (proto != null) { switch (proto.getMessageCase()) { case PROTECTED_MAILBOX_STORAGE_ENTRY: return ProtectedMailboxStorageEntry.fromProto(proto.getProtectedMailboxStorageEntry(), this); case PROTECTED_STORAGE_ENTRY: return ProtectedStorageEntry.fromProto(proto.getProtectedStorageEntry(), this); default: throw new ProtobufferRuntimeException("Unknown proto message case(PB.StorageEntryWrapper). " + "messageCase=" + proto.getMessageCase() + "; proto raw data=" + proto.toString()); } } else { log.error("PersistableEnvelope.fromProto: PB.StorageEntryWrapper is null"); throw new ProtobufferRuntimeException("PB.StorageEntryWrapper is null"); } } @Override public NetworkPayload fromProto(protobuf.StoragePayload proto) { if (proto != null) { switch (proto.getMessageCase()) { case ALERT: return Alert.fromProto(proto.getAlert()); case ARBITRATOR: return Arbitrator.fromProto(proto.getArbitrator()); case MEDIATOR: return Mediator.fromProto(proto.getMediator()); case REFUND_AGENT: return RefundAgent.fromProto(proto.getRefundAgent()); case FILTER: return Filter.fromProto(proto.getFilter()); case MAILBOX_STORAGE_PAYLOAD: return MailboxStoragePayload.fromProto(proto.getMailboxStoragePayload()); case OFFER_PAYLOAD: return OfferPayload.fromProto(proto.getOfferPayload()); default: throw new ProtobufferRuntimeException("Unknown proto message case (PB.StoragePayload). messageCase=" + proto.getMessageCase() + "; proto raw data=" + proto.toString()); } } else { log.error("PersistableEnvelope.fromProto: PB.StoragePayload is null"); throw new ProtobufferRuntimeException("PB.StoragePayload is null"); } } } ================================================ FILE: core/src/main/java/haveno/core/proto/persistable/CorePersistenceProtoResolver.java ================================================ /* * 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 . */ package haveno.core.proto.persistable; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import haveno.common.proto.ProtobufferRuntimeException; import haveno.common.proto.network.NetworkProtoResolver; import haveno.common.proto.persistable.NavigationPath; import haveno.common.proto.persistable.PersistableEnvelope; import haveno.common.proto.persistable.PersistenceProtoResolver; import haveno.core.account.sign.SignedWitnessStore; import haveno.core.account.witness.AccountAgeWitnessStore; import haveno.core.offer.SignedOfferList; import haveno.core.payment.PaymentAccountList; import haveno.core.proto.CoreProtoResolver; import haveno.core.support.dispute.arbitration.ArbitrationDisputeList; import haveno.core.support.dispute.mediation.MediationDisputeList; import haveno.core.support.dispute.refund.RefundDisputeList; import haveno.core.trade.TradableList; import haveno.core.trade.statistics.TradeStatistics3Store; import haveno.core.user.PreferencesPayload; import haveno.core.user.UserPayload; import haveno.core.xmr.model.AddressEntryList; import haveno.core.xmr.model.EncryptedConnectionList; import haveno.core.xmr.model.XmrAddressEntryList; import haveno.core.xmr.wallet.BtcWalletService; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.mailbox.IgnoredMailboxMap; import haveno.network.p2p.mailbox.MailboxMessageList; import haveno.network.p2p.peers.peerexchange.PeerList; import haveno.network.p2p.storage.persistence.RemovedPayloadsMap; import haveno.network.p2p.storage.persistence.SequenceNumberMap; import lombok.extern.slf4j.Slf4j; // TODO Use ProtobufferException instead of ProtobufferRuntimeException @Slf4j @Singleton public class CorePersistenceProtoResolver extends CoreProtoResolver implements PersistenceProtoResolver { private final Provider btcWalletService; private final Provider xmrWalletService; private final NetworkProtoResolver networkProtoResolver; @Inject public CorePersistenceProtoResolver(Provider btcWalletService, Provider xmrWalletService, NetworkProtoResolver networkProtoResolver) { this.btcWalletService = btcWalletService; this.xmrWalletService = xmrWalletService; this.networkProtoResolver = networkProtoResolver; } @Override public PersistableEnvelope fromProto(protobuf.PersistableEnvelope proto) { if (proto != null) { switch (proto.getMessageCase()) { case SIGNED_OFFER_LIST: return SignedOfferList.fromProto(proto.getSignedOfferList()); case SEQUENCE_NUMBER_MAP: return SequenceNumberMap.fromProto(proto.getSequenceNumberMap()); case PEER_LIST: return PeerList.fromProto(proto.getPeerList()); case ADDRESS_ENTRY_LIST: return AddressEntryList.fromProto(proto.getAddressEntryList()); case XMR_ADDRESS_ENTRY_LIST: return XmrAddressEntryList.fromProto(proto.getXmrAddressEntryList()); case ENCRYPTED_CONNECTION_LIST: return EncryptedConnectionList.fromProto(proto.getEncryptedConnectionList()); case TRADABLE_LIST: return TradableList.fromProto(proto.getTradableList(), this, xmrWalletService.get()); case ARBITRATION_DISPUTE_LIST: return ArbitrationDisputeList.fromProto(proto.getArbitrationDisputeList(), this); case MEDIATION_DISPUTE_LIST: return MediationDisputeList.fromProto(proto.getMediationDisputeList(), this); case REFUND_DISPUTE_LIST: return RefundDisputeList.fromProto(proto.getRefundDisputeList(), this); case PREFERENCES_PAYLOAD: return PreferencesPayload.fromProto(proto.getPreferencesPayload(), this); case USER_PAYLOAD: return UserPayload.fromProto(proto.getUserPayload(), this); case NAVIGATION_PATH: return NavigationPath.fromProto(proto.getNavigationPath()); case PAYMENT_ACCOUNT_LIST: return PaymentAccountList.fromProto(proto.getPaymentAccountList(), this); case ACCOUNT_AGE_WITNESS_STORE: return AccountAgeWitnessStore.fromProto(proto.getAccountAgeWitnessStore()); case SIGNED_WITNESS_STORE: return SignedWitnessStore.fromProto(proto.getSignedWitnessStore()); case TRADE_STATISTICS3_STORE: return TradeStatistics3Store.fromProto(proto.getTradeStatistics3Store()); case MAILBOX_MESSAGE_LIST: return MailboxMessageList.fromProto(proto.getMailboxMessageList(), networkProtoResolver); case IGNORED_MAILBOX_MAP: return IgnoredMailboxMap.fromProto(proto.getIgnoredMailboxMap()); case REMOVED_PAYLOADS_MAP: return RemovedPayloadsMap.fromProto(proto.getRemovedPayloadsMap()); default: throw new ProtobufferRuntimeException("Unknown proto message case(PB.PersistableEnvelope). " + "messageCase=" + proto.getMessageCase() + "; proto raw data=" + proto.toString()); } } else { log.error("PersistableEnvelope.fromProto: PB.PersistableEnvelope is null"); throw new ProtobufferRuntimeException("PB.PersistableEnvelope is null"); } } } ================================================ FILE: core/src/main/java/haveno/core/provider/FeeHttpClient.java ================================================ /* * 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 . */ package haveno.core.provider; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.network.Socks5ProxyProvider; import haveno.network.http.HttpClientImpl; import javax.annotation.Nullable; @Singleton public class FeeHttpClient extends HttpClientImpl { @Inject public FeeHttpClient(@Nullable Socks5ProxyProvider socks5ProxyProvider) { super(socks5ProxyProvider); } } ================================================ FILE: core/src/main/java/haveno/core/provider/HttpClientProvider.java ================================================ /* * 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 . */ package haveno.core.provider; import haveno.network.http.HttpClient; import lombok.extern.slf4j.Slf4j; @Slf4j public abstract class HttpClientProvider { protected final HttpClient httpClient; public HttpClientProvider(HttpClient httpClient, String baseUrl) { this(httpClient, baseUrl, false); } public HttpClientProvider(HttpClient httpClient, String baseUrl, @SuppressWarnings("SameParameterValue") boolean ignoreSocks5Proxy) { this.httpClient = httpClient; log.debug("{} with baseUrl {}", this.getClass().getSimpleName(), baseUrl); httpClient.setBaseUrl(baseUrl); httpClient.setIgnoreSocks5Proxy(ignoreSocks5Proxy); } @Override public String toString() { return "HttpClientProvider{" + "\n httpClient=" + httpClient + "\n}"; } } ================================================ FILE: core/src/main/java/haveno/core/provider/MempoolHttpClient.java ================================================ /* * 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 . */ package haveno.core.provider; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.app.Version; import haveno.network.Socks5ProxyProvider; import haveno.network.http.HttpClientImpl; import java.io.IOException; import javax.annotation.Nullable; @Singleton public class MempoolHttpClient extends HttpClientImpl { @Inject public MempoolHttpClient(@Nullable Socks5ProxyProvider socks5ProxyProvider) { super(socks5ProxyProvider); } // returns JSON of the transaction details public String getTxDetails(String txId) throws IOException { super.shutDown(); // close any prior incomplete request String api = "/" + txId; return get(api, "User-Agent", "haveno/" + Version.VERSION); } } ================================================ FILE: core/src/main/java/haveno/core/provider/PriceHttpClient.java ================================================ /* * 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 . */ package haveno.core.provider; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.network.Socks5ProxyProvider; import haveno.network.http.HttpClientImpl; import javax.annotation.Nullable; @Singleton public class PriceHttpClient extends HttpClientImpl { @Inject public PriceHttpClient(@Nullable Socks5ProxyProvider socks5ProxyProvider) { super(socks5ProxyProvider); } } ================================================ FILE: core/src/main/java/haveno/core/provider/ProvidersRepository.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.provider; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.config.Config; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import javax.annotation.Nullable; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @Slf4j public class ProvidersRepository { private static final String DEFAULT_LOCAL_NODE = "http://localhost:8078/"; private static final List DEFAULT_NODES = Arrays.asList( "http://elaxlgigphpicy5q7pi5wkz2ko2vgjbq4576vic7febmx4xcxvk6deqd.onion/", // Haveno "http://lrrgpezvdrbpoqvkavzobmj7dr2otxc5x6wgktrw337bk6mxsvfp5yid.onion/", // Cake "http://agorise7ae5g7lkqp7r7qddsyzskft7cqhgguwkadbqamtsrap5onead.onion/" // Agorise ); private final Config config; private final List providersFromProgramArgs; private final boolean useLocalhostForP2P; private List providerList; @Getter private String baseUrl = ""; @Getter @Nullable private List bannedNodes; private int index = -1; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public ProvidersRepository(Config config, @Named(Config.PROVIDERS) List providers, @Named(Config.USE_LOCALHOST_FOR_P2P) boolean useLocalhostForP2P) { this.config = config; this.providersFromProgramArgs = providers; this.useLocalhostForP2P = useLocalhostForP2P; Collections.shuffle(DEFAULT_NODES); // randomize order of default nodes applyBannedNodes(config.bannedPriceRelayNodes); } public void applyBannedNodes(@Nullable List bannedNodes) { this.bannedNodes = bannedNodes; // fill provider list fillProviderList(); // select next provider if current provider is null or banned if (baseUrl.isEmpty() || isBanned(baseUrl)) selectNextProviderBaseUrl(); if (bannedNodes != null && !bannedNodes.isEmpty()) { log.info("Excluded provider nodes from filter: nodes={}, selected provider baseUrl={}, providerList={}", bannedNodes, baseUrl, providerList); } } // returns true if provider selection loops to beginning public synchronized boolean selectNextProviderBaseUrl() { boolean looped = false; if (!providerList.isEmpty()) { // increment index index++; // loop to beginning if (index >= providerList.size()) { index = 0; looped = true; } // update base url baseUrl = providerList.get(index); log.info("Selected price provider: " + baseUrl); if (providerList.size() == 1 && config.baseCurrencyNetwork.isMainnet()) log.warn("We only have one provider"); } else { baseUrl = ""; log.warn("We do not have any providers. That can be if all providers are filtered or providersFromProgramArgs is set but empty. " + "bannedNodes={}. providersFromProgramArgs={}", bannedNodes, providersFromProgramArgs); } return looped; } private void fillProviderList() { List providers; if (providersFromProgramArgs.isEmpty()) { if (useLocalhostForP2P) { // If we run in localhost mode we don't have the tor node running, so we need a clearnet host // Use localhost for using a locally running provider providers = List.of( DEFAULT_LOCAL_NODE, "https://price.haveno.network/", "http://173.230.142.36:8078/"); } else { providers = new ArrayList(); //providers.add(DEFAULT_LOCAL_NODE); // try local provider first providers.addAll(DEFAULT_NODES); } } else { providers = providersFromProgramArgs; } providerList = providers.stream() .filter(e -> !isBanned(e)) .map(e -> e.endsWith("/") ? e : e + "/") .map(e -> e.startsWith("http") ? e : "http://" + e) .collect(Collectors.toList()); } private boolean isBanned(String provider) { if (bannedNodes == null) return false; return bannedNodes.stream() .anyMatch(e -> provider.replace("http://", "") .replace("/", "") .replace(".onion", "") .equals(e)); } } ================================================ FILE: core/src/main/java/haveno/core/provider/fee/FeeProvider.java ================================================ /* * 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 . */ package haveno.core.provider.fee; import com.google.gson.Gson; import com.google.gson.internal.LinkedTreeMap; import com.google.inject.Inject; import haveno.common.app.Version; import haveno.common.config.Config; import haveno.common.util.Tuple2; import haveno.core.provider.FeeHttpClient; import haveno.core.provider.HttpClientProvider; import haveno.core.provider.ProvidersRepository; import haveno.network.http.HttpClient; import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.util.HashMap; import java.util.Map; @Slf4j public class FeeProvider extends HttpClientProvider { @Inject public FeeProvider(FeeHttpClient httpClient, ProvidersRepository providersRepository) { super(httpClient, providersRepository.getBaseUrl(), false); } public Tuple2, Map> getFees() throws IOException { String json = httpClient.get("getFees", "User-Agent", "haveno/" + Version.VERSION); LinkedTreeMap linkedTreeMap = new Gson().fromJson(json, LinkedTreeMap.class); Map tsMap = new HashMap<>(); tsMap.put(Config.BTC_FEES_TS, ((Double) linkedTreeMap.get(Config.BTC_FEES_TS)).longValue()); Map map = new HashMap<>(); try { LinkedTreeMap dataMap = (LinkedTreeMap) linkedTreeMap.get("dataMap"); Long btcTxFee = ((Double) dataMap.get(Config.BTC_TX_FEE)).longValue(); Long btcMinTxFee = dataMap.get(Config.BTC_MIN_TX_FEE) != null ? ((Double) dataMap.get(Config.BTC_MIN_TX_FEE)).longValue() : Config.baseCurrencyNetwork().getDefaultMinFeePerVbyte(); map.put(Config.BTC_TX_FEE, btcTxFee); map.put(Config.BTC_MIN_TX_FEE, btcMinTxFee); } catch (Throwable t) { log.error("Error getting fees: {}\n", t.getMessage(), t); } return new Tuple2<>(tsMap, map); } public HttpClient getHttpClient() { return httpClient; } } ================================================ FILE: core/src/main/java/haveno/core/provider/fee/FeeRequest.java ================================================ /* * 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 . */ package haveno.core.provider.fee; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import haveno.common.util.Tuple2; import haveno.common.util.Utilities; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Map; public class FeeRequest { private static final Logger log = LoggerFactory.getLogger(FeeRequest.class); private static final ListeningExecutorService executorService = Utilities.getListeningExecutorService("FeeRequest", 3, 5, 10 * 60); public FeeRequest() { } public SettableFuture, Map>> getFees(FeeProvider provider) { final SettableFuture, Map>> resultFuture = SettableFuture.create(); ListenableFuture, Map>> future = executorService.submit(() -> { Thread.currentThread().setName("FeeRequest @ " + provider.getHttpClient().getBaseUrl()); return provider.getFees(); }); Futures.addCallback(future, new FutureCallback, Map>>() { public void onSuccess(Tuple2, Map> feeData) { log.debug("Received feeData of {}\nfrom provider {}", feeData, provider); resultFuture.set(feeData); } public void onFailure(@NotNull Throwable throwable) { resultFuture.setException(throwable); } }, MoreExecutors.directExecutor()); return resultFuture; } } ================================================ FILE: core/src/main/java/haveno/core/provider/price/MarketPrice.java ================================================ /* * 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 . */ package haveno.core.provider.price; import lombok.Getter; import lombok.Value; import java.time.Instant; @Value public class MarketPrice { public static final long MARKET_PRICE_MAX_AGE_SEC = 1800; // 30 min private final String currencyCode; private final double price; private final long timestampSec; @Getter private final boolean isExternallyProvidedPrice; public MarketPrice(String currencyCode, double price, long timestampSec, boolean isExternallyProvidedPrice) { this.currencyCode = currencyCode; this.price = price; this.timestampSec = timestampSec; this.isExternallyProvidedPrice = isExternallyProvidedPrice; } public boolean isPriceAvailable() { return price > 0; } public boolean isRecentPriceAvailable() { return isPriceAvailable() && timestampSec > (Instant.now().getEpochSecond() - MARKET_PRICE_MAX_AGE_SEC); } public boolean isRecentExternalPriceAvailable() { return isExternallyProvidedPrice && isRecentPriceAvailable(); } } ================================================ FILE: core/src/main/java/haveno/core/provider/price/PriceFeedService.java ================================================ /* * 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 . */ package haveno.core.provider.price; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import com.google.inject.Inject; import haveno.common.ThreadUtils; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.handlers.FaultHandler; import haveno.common.util.MathUtils; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TradeCurrency; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.Price; import haveno.core.monetary.TraditionalMoney; import haveno.core.provider.PriceHttpClient; import haveno.core.provider.ProvidersRepository; import haveno.core.trade.statistics.TradeStatistics3; import haveno.core.user.Preferences; import haveno.network.http.HttpClient; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ReadOnlyIntegerProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; import java.time.Instant; import java.util.ArrayList; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; import java.util.concurrent.CancellationException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class PriceFeedService { private final HttpClient httpClient; private final ProvidersRepository providersRepository; private final Preferences preferences; private static final long PERIOD_SEC = 60; private final Map cache = new HashMap<>(); private PriceProvider priceProvider; @Nullable private Consumer priceConsumer; @Nullable private FaultHandler faultHandler; private String currencyCode; private final StringProperty currencyCodeProperty = new SimpleStringProperty(); private final IntegerProperty updateCounter = new SimpleIntegerProperty(0); private long epochInMillisAtLastRequest; private long retryDelay = 0; private long requestTs; private long lastLoopTs = System.currentTimeMillis(); @Nullable private String baseUrlOfRespondingProvider; @Nullable private Timer requestTimer; @Nullable private PriceRequest priceRequest; private String requestAllPricesError = null; private static final String THREAD_ID = PriceFeedService.class.getSimpleName(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public PriceFeedService(PriceHttpClient httpClient, @SuppressWarnings("SameParameterValue") ProvidersRepository providersRepository, @SuppressWarnings("SameParameterValue") Preferences preferences) { this.httpClient = httpClient; this.providersRepository = providersRepository; this.preferences = preferences; // Do not use Guice for PriceProvider as we might create multiple instances this.priceProvider = new PriceProvider(httpClient, providersRepository.getBaseUrl()); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void shutDown() { log.info("Shutting down {}", getClass().getSimpleName()); if (requestTimer != null) { requestTimer.stop(); requestTimer = null; } if (priceRequest != null) { priceRequest.shutDown(); } } public void setCurrencyCodeOnInit() { if (getCurrencyCode() == null) { TradeCurrency preferredTradeCurrency = preferences.getPreferredTradeCurrency(); String code = preferredTradeCurrency != null ? preferredTradeCurrency.getCode() : "USD"; setCurrencyCode(code); } } public void requestPrices() { request(false); } /** * Awaits prices to be available, but does not request them. */ public void awaitExternalPrices() { CountDownLatch latch = new CountDownLatch(1); ChangeListener listener = (observable, oldValue, newValue) -> { if (hasExternalPrices()) UserThread.execute(() -> latch.countDown()); }; UserThread.execute(() -> updateCounter.addListener(listener)); if (hasExternalPrices()) UserThread.execute(() -> latch.countDown()); try { latch.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { UserThread.execute(() -> updateCounter.removeListener(listener)); } } public boolean hasExternalPrices() { synchronized (cache) { return cache.values().stream().anyMatch(MarketPrice::isExternallyProvidedPrice); } } public void startRequestingPrices() { if (requestTimer == null) request(true); // ignore if already repeat requesting } public void startRequestingPrices(Consumer resultHandler, FaultHandler faultHandler) { this.priceConsumer = resultHandler; this.faultHandler = faultHandler; startRequestingPrices(); } public String getProviderNodeAddress() { return httpClient.getBaseUrl(); } private void request(boolean repeatRequests) { if (requestTs == 0) log.debug("request from provider {}", providersRepository.getBaseUrl()); else log.debug("request from provider {} {} sec. after last request", providersRepository.getBaseUrl(), (System.currentTimeMillis() - requestTs) / 1000d); requestTs = System.currentTimeMillis(); baseUrlOfRespondingProvider = null; requestAllPrices(priceProvider, () -> { baseUrlOfRespondingProvider = priceProvider.getBaseUrl(); // At applyPriceToConsumer we also check if price is not exceeding max. age for price data. boolean success = applyPriceToConsumer(); if (success) { MarketPrice marketPrice = cache.get(currencyCode); if (marketPrice != null) log.debug("Received new {} from provider {} after {} sec.", marketPrice, baseUrlOfRespondingProvider, (System.currentTimeMillis() - requestTs) / 1000d); else log.debug("Received new data from provider {} after {} sec. " + "Requested market price for currency {} was not provided. " + "That is expected if currency is not listed at provider.", baseUrlOfRespondingProvider, (System.currentTimeMillis() - requestTs) / 1000d, currencyCode); } else { log.warn("applyPriceToConsumer was not successful. We retry with a new provider."); retryWithNewProvider(); } }, (errorMessage, throwable) -> { if (throwable instanceof PriceRequestException) { String baseUrlOfFaultyRequest = ((PriceRequestException) throwable).priceProviderBaseUrl; String baseUrlOfCurrentRequest = priceProvider.getBaseUrl(); if (baseUrlOfCurrentRequest.equals(baseUrlOfFaultyRequest)) { log.info("We received an error requesting prices: baseUrlOfFaultyRequest={}, error={}", baseUrlOfFaultyRequest, throwable.toString()); retryWithNewProvider(); } else { log.debug("We received an error from an earlier request. We have started a new request already so we ignore that error. " + "baseUrlOfCurrentRequest={}, baseUrlOfFaultyRequest={}", baseUrlOfCurrentRequest, baseUrlOfFaultyRequest); } } else { log.warn("We received an error with throwable={}", throwable.toString()); retryWithNewProvider(); } if (faultHandler != null) faultHandler.handleFault(errorMessage, throwable); }); if (repeatRequests) { if (requestTimer != null) requestTimer.stop(); long delay = PERIOD_SEC + new Random().nextInt(5); requestTimer = UserThread.runAfter(() -> { ThreadUtils.execute(() -> { // If we have not received a result from the last request. We try a new provider. if (baseUrlOfRespondingProvider == null) { final String oldBaseUrl = priceProvider.getBaseUrl(); setNewPriceProvider(); log.warn("We did not receive a response from provider {}. " + "We select the new provider {} and use that for a new request.", oldBaseUrl, priceProvider.getBaseUrl()); } request(true); }, THREAD_ID); }, delay); } } private void retryWithNewProvider() { long thisRetryDelay = 0; String oldBaseUrl = priceProvider.getBaseUrl(); boolean looped = setNewPriceProvider(); if (looped) { log.warn("Exhausted price provider list, looping to beginning"); if (System.currentTimeMillis() - lastLoopTs < PERIOD_SEC * 1000) { retryDelay = Math.min(retryDelay + 5, PERIOD_SEC); } else { retryDelay = 0; } lastLoopTs = System.currentTimeMillis(); thisRetryDelay = retryDelay; } log.info("We received an error at the request from provider {}. " + "We select the new provider {} and use that for a new request in {} sec.", oldBaseUrl, priceProvider.getBaseUrl(), thisRetryDelay); if (thisRetryDelay > 0) { UserThread.runAfter(() -> { request(true); }, thisRetryDelay); } else { request(true); } } // returns true if provider selection loops back to beginning private boolean setNewPriceProvider() { httpClient.cancelPendingRequest(); boolean looped = providersRepository.selectNextProviderBaseUrl(); if (!providersRepository.getBaseUrl().isEmpty()) { priceProvider = new PriceProvider(httpClient, providersRepository.getBaseUrl()); } else { log.warn("We cannot create a new priceProvider because new base url is empty."); } return looped; } @Nullable public MarketPrice getMarketPrice(String currencyCode) { synchronized (cache) { return cache.getOrDefault(CurrencyUtil.getCurrencyCodeBase(currencyCode), null); } } private void setHavenoMarketPrice(String counterCurrencyCode, Price price) { UserThread.execute(() -> { String counterCurrencyCodeBase = CurrencyUtil.getCurrencyCodeBase(counterCurrencyCode); synchronized (cache) { if (!cache.containsKey(counterCurrencyCodeBase) || !cache.get(counterCurrencyCodeBase).isExternallyProvidedPrice()) { cache.put(counterCurrencyCodeBase, new MarketPrice(counterCurrencyCodeBase, MathUtils.scaleDownByPowerOf10(price.getValue(), CurrencyUtil.isCryptoCurrency(counterCurrencyCode) ? CryptoMoney.SMALLEST_UNIT_EXPONENT : TraditionalMoney.SMALLEST_UNIT_EXPONENT), 0, false)); } updateCounter.set(updateCounter.get() + 1); } }); } /////////////////////////////////////////////////////////////////////////////////////////// // Setter /////////////////////////////////////////////////////////////////////////////////////////// public void setCurrencyCode(String currencyCode) { UserThread.await(() -> { if (this.currencyCode == null || !this.currencyCode.equals(currencyCode)) { this.currencyCode = currencyCode; currencyCodeProperty.set(currencyCode); if (priceConsumer != null) applyPriceToConsumer(); } }); } /////////////////////////////////////////////////////////////////////////////////////////// // Getter /////////////////////////////////////////////////////////////////////////////////////////// public String getCurrencyCode() { return currencyCode; } public StringProperty currencyCodeProperty() { return currencyCodeProperty; } public ReadOnlyIntegerProperty updateCounterProperty() { return updateCounter; } public Date getLastRequestTimeStamp() { return new Date(epochInMillisAtLastRequest); } public void applyLatestHavenoMarketPrice(List tradeStatisticsList) { // takes about 10 ms for 5000 items Map> mapByCurrencyCode = new HashMap<>(); tradeStatisticsList.forEach(e -> { List list; String currencyCode = e.getCurrency(); if (mapByCurrencyCode.containsKey(currencyCode)) { list = mapByCurrencyCode.get(currencyCode); } else { list = new ArrayList<>(); mapByCurrencyCode.put(currencyCode, list); } list.add(e); }); mapByCurrencyCode.values().stream() .filter(list -> !list.isEmpty()) .forEach(list -> { list.sort(Comparator.comparing(TradeStatistics3::getDate)); TradeStatistics3 tradeStatistics = list.get(list.size() - 1); setHavenoMarketPrice(tradeStatistics.getCurrency(), tradeStatistics.getTradePrice()); }); } /** * Returns prices for all available currencies. The base currency is always XMR. * * TODO: instrument requestPrices() result and fault handlers instead of using CountDownLatch and timeout */ public synchronized Map requestAllPrices() throws ExecutionException, InterruptedException, TimeoutException, CancellationException { CountDownLatch latch = new CountDownLatch(1); ChangeListener listener = (observable, oldValue, newValue) -> latch.countDown(); UserThread.execute(() -> updateCounter.addListener(listener)); requestAllPricesError = null; requestPrices(); UserThread.runAfter(() -> { if (latch.getCount() > 0) requestAllPricesError = "Timeout fetching market prices within 30 seconds"; UserThread.execute(() -> latch.countDown()); }, 30); try { latch.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { UserThread.execute(() -> updateCounter.removeListener(listener)); } if (requestAllPricesError != null) throw new RuntimeException(requestAllPricesError); return cache; } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private boolean applyPriceToConsumer() { boolean result = false; String errorMessage = null; if (currencyCode != null) { String baseUrl = priceProvider.getBaseUrl(); httpClient.setBaseUrl(baseUrl); if (cache.containsKey(currencyCode)) { try { MarketPrice marketPrice = cache.get(currencyCode); if (marketPrice.isExternallyProvidedPrice()) { if (marketPrice.isRecentPriceAvailable()) { if (priceConsumer != null) priceConsumer.accept(marketPrice.getPrice()); result = true; } else { errorMessage = "Price for currency " + currencyCode + " is outdated by " + (Instant.now().getEpochSecond() - marketPrice.getTimestampSec()) / 60 + " minutes. " + "Max. allowed age of price is " + MarketPrice.MARKET_PRICE_MAX_AGE_SEC / 60 + " minutes. " + "priceProvider=" + baseUrl + ". " + "marketPrice= " + marketPrice; } } else { if (baseUrlOfRespondingProvider == null) log.debug("Market price for currency " + currencyCode + " was not delivered by provider " + baseUrl + ". That is expected at startup."); else log.debug("Market price for currency " + currencyCode + " is not provided by the provider " + baseUrl + ". That is expected for currencies not listed at providers."); result = true; } } catch (Throwable t) { errorMessage = "Exception at applyPriceToConsumer for currency " + currencyCode + ". priceProvider=" + baseUrl + ". Exception=" + t; } } else { log.debug("We don't have a price for currency " + currencyCode + ". priceProvider=" + baseUrl + ". That is expected for currencies not listed at providers."); result = true; } } else { errorMessage = "We don't have a currency yet set. That should never happen"; } if (errorMessage != null) { log.warn(errorMessage); if (faultHandler != null) faultHandler.handleFault(errorMessage, new PriceRequestException(errorMessage)); } UserThread.execute(() -> updateCounter.set(updateCounter.get() + 1)); return result; } private void requestAllPrices(PriceProvider provider, Runnable resultHandler, FaultHandler faultHandler) { if (httpClient.hasPendingRequest()) { log.warn("We have a pending request open. We ignore that request. httpClient {}", httpClient); return; } priceRequest = new PriceRequest(); SettableFuture> future = priceRequest.requestAllPrices(provider); Futures.addCallback(future, new FutureCallback<>() { @Override public void onSuccess(@Nullable Map result) { UserThread.execute(() -> { checkNotNull(result, "Result must not be null at requestAllPrices"); // Each currency rate has a different timestamp, depending on when // the priceNode aggregate rate was calculated // However, the request timestamp is when the pricenode was queried epochInMillisAtLastRequest = System.currentTimeMillis(); Map priceMap = result; synchronized (cache) { cache.putAll(priceMap); } resultHandler.run(); }); } @Override public void onFailure(@NotNull Throwable throwable) { UserThread.execute(() -> faultHandler.handleFault("Could not load marketPrices", throwable)); } }, MoreExecutors.directExecutor()); } } ================================================ FILE: core/src/main/java/haveno/core/provider/price/PriceProvider.java ================================================ /* * 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 . */ package haveno.core.provider.price; import com.google.gson.Gson; import com.google.gson.internal.LinkedTreeMap; import haveno.common.app.Version; import haveno.common.util.MathUtils; import haveno.core.locale.CurrencyUtil; import haveno.core.provider.HttpClientProvider; import haveno.network.http.HttpClient; import haveno.network.p2p.P2PService; import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @Slf4j public class PriceProvider extends HttpClientProvider { private boolean shutDownRequested; // Do not use Guice here as we might create multiple instances public PriceProvider(HttpClient httpClient, String baseUrl) { super(httpClient, baseUrl, false); } public synchronized Map getAll() throws IOException { if (shutDownRequested) { return new HashMap<>(); } Map marketPriceMap = new HashMap<>(); String hsVersion = ""; if (P2PService.getMyNodeAddress() != null) hsVersion = P2PService.getMyNodeAddress().getHostName().length() > 22 ? ", HSv3" : ", HSv2"; String json = httpClient.get("getAllMarketPrices", "User-Agent", "haveno/" + Version.VERSION + hsVersion); LinkedTreeMap map = new Gson().fromJson(json, LinkedTreeMap.class); List list = (ArrayList) map.get("data"); list.forEach(obj -> { try { LinkedTreeMap treeMap = (LinkedTreeMap) obj; String baseCurrencyCode = (String) treeMap.get("baseCurrencyCode"); String counterCurrencyCode = (String) treeMap.get("counterCurrencyCode"); boolean isInverted = !"XMR".equalsIgnoreCase(baseCurrencyCode); if (isInverted) { String temp = baseCurrencyCode; baseCurrencyCode = counterCurrencyCode; counterCurrencyCode = temp; } counterCurrencyCode = CurrencyUtil.getCurrencyCodeBase(counterCurrencyCode); double price = (Double) treeMap.get("price"); if (isInverted) price = BigDecimal.ONE.divide(BigDecimal.valueOf(price), 10, RoundingMode.HALF_UP).doubleValue(); // XMR is always base currency, so invert price if applicable long timestampSec = MathUtils.doubleToLong((Double) treeMap.get("timestampSec")); marketPriceMap.put(counterCurrencyCode, new MarketPrice(counterCurrencyCode, price, timestampSec, true)); } catch (Throwable t) { log.error("Error getting all prices: {}\n", t.getMessage(), t); } }); return marketPriceMap; } public String getBaseUrl() { return httpClient.getBaseUrl(); } public void shutDown() { shutDownRequested = true; httpClient.shutDown(); } } ================================================ FILE: core/src/main/java/haveno/core/provider/price/PriceRequest.java ================================================ /* * 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 . */ package haveno.core.provider.price; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import haveno.common.util.Utilities; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Map; import java.util.concurrent.TimeUnit; @Slf4j public class PriceRequest { private final ListeningExecutorService executorService = Utilities.getListeningExecutorService("PriceRequest", 3, 5, 10 * 60); @Nullable private PriceProvider provider; private boolean shutDownRequested; public PriceRequest() { } public SettableFuture> requestAllPrices(PriceProvider provider) { this.provider = provider; String baseUrl = provider.getBaseUrl(); SettableFuture> resultFuture = SettableFuture.create(); ListenableFuture> future = executorService.submit(() -> { Thread.currentThread().setName("PriceRequest @ " + baseUrl); return provider.getAll(); }); Futures.addCallback(future, new FutureCallback<>() { public void onSuccess(Map marketPriceTuple) { log.trace("Received marketPriceTuple of {}\nfrom provider {}", marketPriceTuple, provider); if (!shutDownRequested) { resultFuture.set(marketPriceTuple); } } public void onFailure(@NotNull Throwable throwable) { if (!shutDownRequested) { resultFuture.setException(new PriceRequestException(throwable, baseUrl)); } } }, MoreExecutors.directExecutor()); return resultFuture; } public void shutDown() { shutDownRequested = true; if (provider != null) { provider.shutDown(); } Utilities.shutdownAndAwaitTermination(executorService, 1, TimeUnit.SECONDS); } } ================================================ FILE: core/src/main/java/haveno/core/provider/price/PriceRequestException.java ================================================ /* * 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 . */ package haveno.core.provider.price; import javax.annotation.Nullable; public class PriceRequestException extends Exception { @Nullable public String priceProviderBaseUrl; public PriceRequestException(String errorMessage) { super(errorMessage); } public PriceRequestException(Throwable throwable, String priceProviderBaseUrl) { super(throwable); this.priceProviderBaseUrl = priceProviderBaseUrl; } } ================================================ FILE: core/src/main/java/haveno/core/setup/CoreNetworkCapabilities.java ================================================ /* * 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 . */ package haveno.core.setup; import haveno.common.app.Capabilities; import haveno.common.app.Capability; import haveno.common.config.Config; import lombok.extern.slf4j.Slf4j; @Slf4j public class CoreNetworkCapabilities { static void setSupportedCapabilities(Config config) { Capabilities.app.addAll( Capability.TRADE_STATISTICS, Capability.TRADE_STATISTICS_2, Capability.ACCOUNT_AGE_WITNESS, Capability.ACK_MSG, Capability.PROPOSAL, Capability.BLIND_VOTE, Capability.BUNDLE_OF_ENVELOPES, Capability.MEDIATION, Capability.SIGNED_ACCOUNT_AGE_WITNESS, Capability.REFUND_AGENT, Capability.TRADE_STATISTICS_HASH_UPDATE, Capability.NO_ADDRESS_PRE_FIX, Capability.TRADE_STATISTICS_3 ); log.info(Capabilities.app.prettyPrint()); } } ================================================ FILE: core/src/main/java/haveno/core/setup/CorePersistedDataHost.java ================================================ /* * 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 . */ package haveno.core.setup; import com.google.inject.Injector; import haveno.common.proto.persistable.PersistedDataHost; import haveno.core.offer.OpenOfferManager; import haveno.core.support.dispute.arbitration.ArbitrationDisputeListService; import haveno.core.support.dispute.mediation.MediationDisputeListService; import haveno.core.support.dispute.refund.RefundDisputeListService; import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.TradeManager; import haveno.core.trade.failed.FailedTradesManager; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.xmr.model.AddressEntryList; import haveno.core.xmr.model.EncryptedConnectionList; import haveno.core.xmr.model.XmrAddressEntryList; import haveno.network.p2p.mailbox.IgnoredMailboxService; import haveno.network.p2p.mailbox.MailboxMessageService; import haveno.network.p2p.peers.PeerManager; import haveno.network.p2p.storage.P2PDataStorage; import haveno.network.p2p.storage.persistence.RemovedPayloadsService; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; @Slf4j public class CorePersistedDataHost { // All classes which are persisting objects need to be added here public static List getPersistedDataHosts(Injector injector) { List persistedDataHosts = new ArrayList<>(); persistedDataHosts.add(injector.getInstance(Preferences.class)); persistedDataHosts.add(injector.getInstance(User.class)); persistedDataHosts.add(injector.getInstance(AddressEntryList.class)); persistedDataHosts.add(injector.getInstance(XmrAddressEntryList.class)); persistedDataHosts.add(injector.getInstance(EncryptedConnectionList.class)); persistedDataHosts.add(injector.getInstance(OpenOfferManager.class)); persistedDataHosts.add(injector.getInstance(TradeManager.class)); persistedDataHosts.add(injector.getInstance(ClosedTradableManager.class)); persistedDataHosts.add(injector.getInstance(FailedTradesManager.class)); persistedDataHosts.add(injector.getInstance(ArbitrationDisputeListService.class)); persistedDataHosts.add(injector.getInstance(MediationDisputeListService.class)); persistedDataHosts.add(injector.getInstance(RefundDisputeListService.class)); persistedDataHosts.add(injector.getInstance(P2PDataStorage.class)); persistedDataHosts.add(injector.getInstance(PeerManager.class)); persistedDataHosts.add(injector.getInstance(MailboxMessageService.class)); persistedDataHosts.add(injector.getInstance(IgnoredMailboxService.class)); persistedDataHosts.add(injector.getInstance(RemovedPayloadsService.class)); return persistedDataHosts; } } ================================================ FILE: core/src/main/java/haveno/core/setup/CoreSetup.java ================================================ /* * 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 . */ package haveno.core.setup; import haveno.common.config.Config; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import lombok.extern.slf4j.Slf4j; @Slf4j public class CoreSetup { public static void setup(Config config) { CoreNetworkCapabilities.setSupportedCapabilities(config); Res.setup(); CurrencyUtil.setup(); } } ================================================ FILE: core/src/main/java/haveno/core/support/SupportManager.java ================================================ /* * 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 . */ package haveno.core.support; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.crypto.PubKeyRing; import haveno.common.proto.network.NetworkEnvelope; import haveno.core.api.XmrConnectionService; import haveno.core.api.CoreNotificationService; import haveno.core.locale.Res; import haveno.core.support.dispute.Dispute; import haveno.core.support.messages.ChatMessage; import haveno.core.support.messages.SupportMessage; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.trade.protocol.TradePeer; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.AckMessage; import haveno.network.p2p.AckMessageSourceType; import haveno.network.p2p.DecryptedMessageWithPubKey; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import haveno.network.p2p.SendMailboxMessageListener; import haveno.network.p2p.mailbox.MailboxMessage; import haveno.network.p2p.mailbox.MailboxMessageService; import haveno.network.p2p.mailbox.MailboxMessageService.DecryptedMessageWithPubKeyComparator; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArraySet; @Slf4j public abstract class SupportManager { protected final P2PService p2PService; protected final TradeManager tradeManager; protected final XmrConnectionService xmrConnectionService; protected final XmrWalletService xmrWalletService; protected final CoreNotificationService notificationService; protected final Map delayMsgMap = new HashMap<>(); private final Object lock = new Object(); private final CopyOnWriteArraySet decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>(); private final CopyOnWriteArraySet decryptedDirectMessageWithPubKeys = new CopyOnWriteArraySet<>(); protected final MailboxMessageService mailboxMessageService; private boolean allServicesInitialized; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public SupportManager(P2PService p2PService, XmrConnectionService xmrConnectionService, XmrWalletService xmrWalletService, CoreNotificationService notificationService, TradeManager tradeManager) { this.p2PService = p2PService; this.xmrConnectionService = xmrConnectionService; this.xmrWalletService = xmrWalletService; this.mailboxMessageService = p2PService.getMailboxMessageService(); this.notificationService = notificationService; this.tradeManager = tradeManager; // We get first the message handler called then the onBootstrapped p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> { synchronized (lock) { if (isReady()) applyDirectMessage(decryptedMessageWithPubKey); else { // As decryptedDirectMessageWithPubKeys is a CopyOnWriteArraySet we do not need to check if it was already stored decryptedDirectMessageWithPubKeys.add(decryptedMessageWithPubKey); tryApplyMessages(); } } }); mailboxMessageService.addDecryptedMailboxListener((decryptedMessageWithPubKey, senderAddress) -> { synchronized (lock) { if (isReady()) applyMailboxMessage(decryptedMessageWithPubKey); else { // As decryptedMailboxMessageWithPubKeys is a CopyOnWriteArraySet we do not need to check if it was already stored decryptedDirectMessageWithPubKeys.add(decryptedMessageWithPubKey); tryApplyMessages(); } } }); } /////////////////////////////////////////////////////////////////////////////////////////// // Abstract methods /////////////////////////////////////////////////////////////////////////////////////////// protected abstract void onSupportMessage(SupportMessage networkEnvelope); public abstract NodeAddress getPeerNodeAddress(ChatMessage message); public abstract PubKeyRing getPeerPubKeyRing(ChatMessage message); public abstract SupportType getSupportType(); public abstract boolean channelOpen(ChatMessage message); public abstract List getAllChatMessages(String tradeId); public abstract void addAndPersistChatMessage(ChatMessage message); public abstract void requestPersistence(); public abstract void persistNow(@Nullable Runnable completeHandler); /////////////////////////////////////////////////////////////////////////////////////////// // Delegates p2pService /////////////////////////////////////////////////////////////////////////////////////////// public boolean isBootstrapped() { return p2PService.isBootstrapped(); } public NodeAddress getMyAddress() { return p2PService.getAddress(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void onAllServicesInitialized() { allServicesInitialized = true; } public void tryApplyMessages() { if (isReady()) applyMessages(); } /////////////////////////////////////////////////////////////////////////////////////////// // Message handler /////////////////////////////////////////////////////////////////////////////////////////// protected void handle(ChatMessage chatMessage) { final String tradeId = chatMessage.getTradeId(); final String uid = chatMessage.getUid(); log.info("Received {} from peer {}. tradeId={}, uid={}", chatMessage.getClass().getSimpleName(), chatMessage.getSenderNodeAddress(), tradeId, uid); boolean channelOpen = channelOpen(chatMessage); if (!channelOpen) { log.warn("We got a chatMessage but we don't have a matching chat. TradeId = " + tradeId); if (!delayMsgMap.containsKey(uid)) { Timer timer = UserThread.runAfter(() -> handle(chatMessage), 1); delayMsgMap.put(uid, timer); } else { String msg = "We got a chatMessage after we already repeated to apply the message after a delay. That should never happen. TradeId = " + tradeId; log.warn(msg); } return; } cleanupRetryMap(uid); PubKeyRing receiverPubKeyRing = getPeerPubKeyRing(chatMessage); addAndPersistChatMessage(chatMessage); notificationService.sendChatNotification(chatMessage); // We never get a errorMessage in that method (only if we cannot resolve the receiverPubKeyRing but then we // cannot send it anyway) if (receiverPubKeyRing != null) sendAckMessage(chatMessage, receiverPubKeyRing, true, null); } private void onAckMessage(AckMessage ackMessage) { if (ackMessage.getSourceType() == getAckMessageSourceType()) { if (ackMessage.isSuccess()) { log.info("Received AckMessage for {} with tradeId {} and uid {}", ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid()); // ack message on chat message received when dispute is opened and closed if (ackMessage.getSourceMsgClassName().equals(ChatMessage.class.getSimpleName())) { Trade trade = tradeManager.getTrade(ackMessage.getSourceId()); for (Dispute dispute : trade.getDisputes()) { synchronized (dispute.getChatMessages()) { for (ChatMessage chatMessage : dispute.getChatMessages()) { if (chatMessage.getUid().equals(ackMessage.getSourceUid())) { // set ack state TradePeer sender = trade.getTradePeer(ackMessage.getSenderNodeAddress()); if (sender == null) { log.warn("Received AckMessage from unknown peer {} for {} with tradeId={}, uid={}", ackMessage.getSenderNodeAddress(), ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid()); } else { sender.setDisputeOpenedAckMessage(ackMessage); } // advance trade state if (trade.getDisputeState() == Trade.DisputeState.DISPUTE_PREPARING || trade.getDisputeState() == Trade.DisputeState.DISPUTE_REQUESTED) { // ack can arrive before saw arrived if (dispute.isClosed()) dispute.reOpen(); trade.advanceDisputeState(Trade.DisputeState.DISPUTE_OPENED); } else if (dispute.isClosed()) { trade.pollWalletNormallyForMs(Trade.POLL_WALLET_NORMALLY_DEFAULT_PERIOD_MS); // sync to check for payout } } } } } } } else { log.warn("Received AckMessage with error state for {} with tradeId={}, sender={}, errorMessage={}", ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSenderNodeAddress(), ackMessage.getErrorMessage()); // nack message on chat message received when dispute closed message is nacked if (ackMessage.getSourceMsgClassName().equals(ChatMessage.class.getSimpleName())) { Trade trade = tradeManager.getTrade(ackMessage.getSourceId()); for (Dispute dispute : trade.getDisputes()) { synchronized (dispute.getChatMessages()) { for (ChatMessage chatMessage : dispute.getChatMessages()) { if (chatMessage.getUid().equals(ackMessage.getSourceUid())) { if (!trade.isArbitrator() && (trade.getDisputeState().isRequested() || trade.getDisputeState().isCloseRequested())) { // set ack state TradePeer sender = trade.getTradePeer(ackMessage.getSenderNodeAddress()); if (sender == null) { log.warn("Received AckMessage from unknown peer {} for {} with tradeId={}, uid={}", ackMessage.getSenderNodeAddress(), ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid()); } else { sender.setDisputeOpenedAckMessage(ackMessage); } // advance trade state log.warn("DisputeOpenedMessage was nacked. We close the dispute now. tradeId={}, nack sender={}", trade.getId(), ackMessage.getSenderNodeAddress()); dispute.setIsClosed(); trade.advanceDisputeState(Trade.DisputeState.DISPUTE_CLOSED); } } } } } } } getAllChatMessages(ackMessage.getSourceId()).stream() .filter(msg -> msg.getUid().equals(ackMessage.getSourceUid())) .forEach(msg -> { UserThread.execute(() -> { if (ackMessage.isSuccess()) msg.setAcknowledged(true); else msg.setAckError(ackMessage.getErrorMessage()); }); }); tradeManager.persistNow(null); persistNow(null); requestPersistence(); } } protected abstract AckMessageSourceType getAckMessageSourceType(); /////////////////////////////////////////////////////////////////////////////////////////// // Send message /////////////////////////////////////////////////////////////////////////////////////////// public ChatMessage sendChatMessage(ChatMessage message) { NodeAddress peersNodeAddress = getPeerNodeAddress(message); PubKeyRing receiverPubKeyRing = getPeerPubKeyRing(message); if (peersNodeAddress == null || receiverPubKeyRing == null) { UserThread.runAfter(() -> message.setSendMessageError(Res.get("support.receiverNotKnown")), 1); } else { log.info("Send {} to peer {}. tradeId={}, uid={}", message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); mailboxMessageService.sendEncryptedMailboxMessage(peersNodeAddress, receiverPubKeyRing, message.copy(), new SendMailboxMessageListener() { @Override public void onArrived() { log.info("{} arrived at peer {}. tradeId={}, uid={}", message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); message.setArrived(true); requestPersistence(); } @Override public void onStoredInMailbox() { log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}", message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); message.setStoredInMailbox(true); requestPersistence(); } @Override public void onFault(String errorMessage) { log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage); message.setSendMessageError(errorMessage); requestPersistence(); } } ); } return message; } protected void sendAckMessage(SupportMessage supportMessage, PubKeyRing peersPubKeyRing, boolean result, @Nullable String errorMessage) { String tradeId = supportMessage.getTradeId(); String uid = supportMessage.getUid(); AckMessage ackMessage = new AckMessage(p2PService.getNetworkNode().getNodeAddress(), getAckMessageSourceType(), supportMessage.getClass().getSimpleName(), uid, tradeId, result, errorMessage); final NodeAddress peersNodeAddress = supportMessage.getSenderNodeAddress(); log.info("Send AckMessage for {} to peer {}. tradeId={}, uid={}", ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid); mailboxMessageService.sendEncryptedMailboxMessage( peersNodeAddress, peersPubKeyRing, ackMessage, new SendMailboxMessageListener() { @Override public void onArrived() { log.info("AckMessage for {} arrived at peer {}. tradeId={}, uid={}", ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid); } @Override public void onStoredInMailbox() { log.info("AckMessage for {} stored in mailbox for peer {}. tradeId={}, uid={}", ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid); } @Override public void onFault(String errorMessage) { log.error("AckMessage for {} failed. Peer {}. tradeId={}, uid={}, errorMessage={}", ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid, errorMessage); } } ); } /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// protected boolean canProcessMessage(SupportMessage message) { return message.getSupportType() == getSupportType(); } protected void cleanupRetryMap(String uid) { if (delayMsgMap.containsKey(uid)) { Timer timer = delayMsgMap.remove(uid); if (timer != null) timer.stop(); } } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private boolean isReady() { return allServicesInitialized && p2PService.isBootstrapped() && xmrConnectionService.isDownloadComplete() && xmrConnectionService.hasSufficientPeersForBroadcast() && xmrWalletService.wasWalletSynced(); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void applyMessages() { synchronized (lock) { // apply non-mailbox messages decryptedDirectMessageWithPubKeys.stream() .filter(e -> !(e.getNetworkEnvelope() instanceof MailboxMessage)) .forEach(decryptedMessageWithPubKey -> applyDirectMessage(decryptedMessageWithPubKey)); decryptedMailboxMessageWithPubKeys.stream() .filter(e -> !(e.getNetworkEnvelope() instanceof MailboxMessage)) .forEach(decryptedMessageWithPubKey -> applyMailboxMessage(decryptedMessageWithPubKey)); // apply mailbox messages in order decryptedDirectMessageWithPubKeys.stream() .filter(e -> (e.getNetworkEnvelope() instanceof MailboxMessage)) .sorted(new DecryptedMessageWithPubKeyComparator()) .forEach(decryptedMessageWithPubKey -> applyDirectMessage(decryptedMessageWithPubKey)); decryptedMailboxMessageWithPubKeys.stream() .filter(e -> (e.getNetworkEnvelope() instanceof MailboxMessage)) .sorted(new DecryptedMessageWithPubKeyComparator()) .forEach(decryptedMessageWithPubKey -> applyMailboxMessage(decryptedMessageWithPubKey)); // clear messages decryptedDirectMessageWithPubKeys.clear(); decryptedMailboxMessageWithPubKeys.clear(); } } private void applyDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey) { NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); if (networkEnvelope instanceof SupportMessage) { onSupportMessage((SupportMessage) networkEnvelope); } else if (networkEnvelope instanceof AckMessage) { onAckMessage((AckMessage) networkEnvelope); } } private void applyMailboxMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey) { NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); log.trace("## decryptedMessageWithPubKey message={}", networkEnvelope.getClass().getSimpleName()); if (networkEnvelope instanceof SupportMessage) { SupportMessage supportMessage = (SupportMessage) networkEnvelope; onSupportMessage(supportMessage); mailboxMessageService.removeMailboxMsg(supportMessage); } else if (networkEnvelope instanceof AckMessage) { AckMessage ackMessage = (AckMessage) networkEnvelope; onAckMessage(ackMessage); mailboxMessageService.removeMailboxMsg(ackMessage); } } } ================================================ FILE: core/src/main/java/haveno/core/support/SupportSession.java ================================================ /* * 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 . */ package haveno.core.support; import haveno.core.support.messages.ChatMessage; import javafx.collections.ObservableList; /** * A Support session is using a trade or a dispute to implement the methods. * It keeps the ChatView transparent if used in dispute or trade chat context. */ public abstract class SupportSession { // todo refactor ui so that can be converted to isTrader private boolean isClient; protected SupportSession(boolean isClient) { this.isClient = isClient; } protected SupportSession() { } // todo refactor ui so that can be converted to isTrader public boolean isClient() { return isClient; } public abstract String getTradeId(); public abstract int getClientId(); public abstract ObservableList getObservableChatMessageList(); public abstract boolean chatIsOpen(); public abstract boolean isDisputeAgent(); } ================================================ FILE: core/src/main/java/haveno/core/support/SupportType.java ================================================ /* * 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 . */ package haveno.core.support; import haveno.common.proto.ProtoUtil; public enum SupportType { ARBITRATION, // Need to be at index 0 to be the fallback for old clients MEDIATION, TRADE, REFUND; public static SupportType fromProto( protobuf.SupportType type) { return ProtoUtil.enumFromProto(SupportType.class, type.name()); } public static protobuf.SupportType toProtoMessage(SupportType supportType) { return protobuf.SupportType.valueOf(supportType.name()); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/Attachment.java ================================================ /* * 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 . */ package haveno.core.support.dispute; import com.google.protobuf.ByteString; import haveno.common.proto.network.NetworkPayload; import lombok.Value; @Value public final class Attachment implements NetworkPayload { private final String fileName; private final byte[] bytes; public Attachment(String fileName, byte[] bytes) { this.fileName = fileName; this.bytes = bytes; } @Override public protobuf.Attachment toProtoMessage() { return protobuf.Attachment.newBuilder() .setFileName(fileName) .setBytes(ByteString.copyFrom(bytes)) .build(); } public static Attachment fromProto(protobuf.Attachment proto) { return new Attachment(proto.getFileName(), proto.getBytes().toByteArray()); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/Dispute.java ================================================ /* * 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 . */ package haveno.core.support.dispute; import com.google.protobuf.ByteString; import haveno.common.UserThread; import haveno.common.crypto.PubKeyRing; import haveno.common.proto.ProtoUtil; import haveno.common.proto.network.NetworkPayload; import haveno.common.proto.persistable.PersistablePayload; import haveno.common.util.CollectionUtils; import haveno.common.util.ExtraDataMapValidator; import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.proto.CoreProtoResolver; import haveno.core.support.SupportType; import haveno.core.support.dispute.mediation.FileTransferReceiver; import haveno.core.support.dispute.mediation.FileTransferSender; import haveno.core.support.dispute.mediation.FileTransferSession; import haveno.core.support.messages.ChatMessage; import haveno.core.trade.Contract; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.network.NetworkNode; import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyIntegerProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; @Slf4j @EqualsAndHashCode @Getter public final class Dispute implements NetworkPayload, PersistablePayload { public enum State { NEEDS_UPGRADE, NEW, OPEN, REOPENED, CLOSED; public boolean isOpen() { return this == NEW || this == OPEN || this == REOPENED; } public static Dispute.State fromProto(protobuf.Dispute.State state) { return ProtoUtil.enumFromProto(Dispute.State.class, state.name()); } public static protobuf.Dispute.State toProtoMessage(Dispute.State state) { return protobuf.Dispute.State.valueOf(state.name()); } } private final String tradeId; private final String id; private final int traderId; private final boolean disputeOpenerIsBuyer; private final boolean disputeOpenerIsMaker; // PubKeyRing of trader who opened the dispute private final PubKeyRing traderPubKeyRing; private final long tradeDate; private final long tradePeriodEnd; private final Contract contract; @Nullable private final byte[] contractHash; @Nullable private final byte[] payoutTxSerialized; @Nullable private final String payoutTxId; private String contractAsJson; @Nullable private final byte[] makerContractSignature; @Nullable private final byte[] takerContractSignature; private final PubKeyRing agentPubKeyRing; // dispute agent private final boolean isSupportTicket; private final ObservableList chatMessages = FXCollections.observableArrayList(); // disputeResultProperty.get is Nullable! private final ObjectProperty disputeResultProperty = new SimpleObjectProperty<>(); private final long openingDate; @Nullable @Setter private String disputePayoutTxId; @Setter // Added v1.2.0 private SupportType supportType; // Only used at refundAgent so that he knows how the mediator resolved the case @Setter @Nullable private String mediatorsDisputeResult; @Setter @Nullable private String delayedPayoutTxId; // Added at v1.4.0 @Setter @Nullable private String donationAddressOfDelayedPayoutTx; // Added at v1.6.0 private Dispute.State disputeState = State.NEW; // Should be only used in emergency case if we need to add data but do not want to break backward compatibility // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new // field in a class would break that hash and therefore break the storage mechanism. @Nullable @Setter private Map extraDataMap; // Added for XMR integration private boolean isOpener; @Nullable private PaymentAccountPayload makerPaymentAccountPayload; @Nullable private PaymentAccountPayload takerPaymentAccountPayload; // We do not persist uid, it is only used by dispute agents to guarantee an uid. @Setter @Nullable private transient String uid; @Setter private transient long payoutTxConfirms = -1; private transient final BooleanProperty isClosedProperty = new SimpleBooleanProperty(); private transient final IntegerProperty badgeCountProperty = new SimpleIntegerProperty(); private transient FileTransferReceiver fileTransferSession = null; public FileTransferReceiver createOrGetFileTransferReceiver(NetworkNode networkNode, NodeAddress peerNodeAddress, FileTransferSession.FtpCallback callback) throws IOException { // the receiver stores its state temporarily here in the dispute // this method gets called to retrieve the session each time a part of the log files is received if (fileTransferSession == null) { fileTransferSession = new FileTransferReceiver(networkNode, peerNodeAddress, this.tradeId, this.traderId, this.getRoleStringForLogFile(), callback); } return fileTransferSession; } public FileTransferSender createFileTransferSender(NetworkNode networkNode, NodeAddress peerNodeAddress, FileTransferSession.FtpCallback callback) { return new FileTransferSender(networkNode, peerNodeAddress, this.tradeId, this.traderId, this.getRoleStringForLogFile(), false, callback); } /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public Dispute(long openingDate, String tradeId, int traderId, boolean isOpener, boolean disputeOpenerIsBuyer, boolean disputeOpenerIsMaker, PubKeyRing traderPubKeyRing, long tradeDate, long tradePeriodEnd, Contract contract, @Nullable byte[] contractHash, @Nullable byte[] payoutTxSerialized, @Nullable String payoutTxId, String contractAsJson, @Nullable byte[] makerContractSignature, @Nullable byte[] takerContractSignature, @Nullable PaymentAccountPayload makerPaymentAccountPayload, @Nullable PaymentAccountPayload takerPaymentAccountPayload, PubKeyRing agentPubKeyRing, boolean isSupportTicket, SupportType supportType) { this.openingDate = openingDate; this.tradeId = tradeId; this.traderId = traderId; this.isOpener = isOpener; this.disputeOpenerIsBuyer = disputeOpenerIsBuyer; this.disputeOpenerIsMaker = disputeOpenerIsMaker; this.traderPubKeyRing = traderPubKeyRing; this.tradeDate = tradeDate; this.tradePeriodEnd = tradePeriodEnd; this.contract = contract; this.contractHash = contractHash; this.payoutTxSerialized = payoutTxSerialized; this.payoutTxId = payoutTxId; this.contractAsJson = contractAsJson; this.makerContractSignature = makerContractSignature; this.takerContractSignature = takerContractSignature; this.makerPaymentAccountPayload = makerPaymentAccountPayload; this.takerPaymentAccountPayload = takerPaymentAccountPayload; this.agentPubKeyRing = agentPubKeyRing; this.isSupportTicket = isSupportTicket; this.supportType = supportType; id = tradeId + "_" + traderId; uid = UUID.randomUUID().toString(); refreshAlertLevel(true); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.Dispute toProtoMessage() { // Needed to avoid ConcurrentModificationException List clonedChatMessages = new ArrayList<>(chatMessages); protobuf.Dispute.Builder builder = protobuf.Dispute.newBuilder() .setTradeId(tradeId) .setTraderId(traderId) .setIsOpener(isOpener) .setDisputeOpenerIsBuyer(disputeOpenerIsBuyer) .setDisputeOpenerIsMaker(disputeOpenerIsMaker) .setTraderPubKeyRing(traderPubKeyRing.toProtoMessage()) .setTradeDate(tradeDate) .setTradePeriodEnd(tradePeriodEnd) .setContract(contract.toProtoMessage()) .setContractAsJson(contractAsJson) .setAgentPubKeyRing(agentPubKeyRing.toProtoMessage()) .setIsSupportTicket(isSupportTicket) .addAllChatMessage(clonedChatMessages.stream() .map(msg -> msg.toProtoNetworkEnvelope().getChatMessage()) .collect(Collectors.toList())) .setIsClosed(this.isClosed()) .setOpeningDate(openingDate) .setState(Dispute.State.toProtoMessage(disputeState)) .setId(id); Optional.ofNullable(contractHash).ifPresent(e -> builder.setContractHash(ByteString.copyFrom(e))); Optional.ofNullable(payoutTxSerialized).ifPresent(e -> builder.setPayoutTxSerialized(ByteString.copyFrom(e))); Optional.ofNullable(payoutTxId).ifPresent(builder::setPayoutTxId); Optional.ofNullable(disputePayoutTxId).ifPresent(builder::setDisputePayoutTxId); Optional.ofNullable(makerContractSignature).ifPresent(e -> builder.setMakerContractSignature(ByteString.copyFrom(e))); Optional.ofNullable(takerContractSignature).ifPresent(e -> builder.setTakerContractSignature(ByteString.copyFrom(e))); Optional.ofNullable(makerPaymentAccountPayload).ifPresent(e -> builder.setMakerPaymentAccountPayload((protobuf.PaymentAccountPayload) makerPaymentAccountPayload.toProtoMessage())); Optional.ofNullable(takerPaymentAccountPayload).ifPresent(e -> builder.setTakerPaymentAccountPayload((protobuf.PaymentAccountPayload) takerPaymentAccountPayload.toProtoMessage())); Optional.ofNullable(disputeResultProperty.get()).ifPresent(result -> builder.setDisputeResult(disputeResultProperty.get().toProtoMessage())); Optional.ofNullable(supportType).ifPresent(result -> builder.setSupportType(SupportType.toProtoMessage(supportType))); Optional.ofNullable(mediatorsDisputeResult).ifPresent(result -> builder.setMediatorsDisputeResult(mediatorsDisputeResult)); Optional.ofNullable(delayedPayoutTxId).ifPresent(result -> builder.setDelayedPayoutTxId(delayedPayoutTxId)); Optional.ofNullable(donationAddressOfDelayedPayoutTx).ifPresent(result -> builder.setDonationAddressOfDelayedPayoutTx(donationAddressOfDelayedPayoutTx)); Optional.ofNullable(getExtraDataMap()).ifPresent(builder::putAllExtraData); return builder.build(); } public static Dispute fromProto(protobuf.Dispute proto, CoreProtoResolver coreProtoResolver) { Dispute dispute = new Dispute(proto.getOpeningDate(), proto.getTradeId(), proto.getTraderId(), proto.getIsOpener(), proto.getDisputeOpenerIsBuyer(), proto.getDisputeOpenerIsMaker(), PubKeyRing.fromProto(proto.getTraderPubKeyRing()), proto.getTradeDate(), proto.getTradePeriodEnd(), Contract.fromProto(proto.getContract(), coreProtoResolver), ProtoUtil.byteArrayOrNullFromProto(proto.getContractHash()), ProtoUtil.byteArrayOrNullFromProto(proto.getPayoutTxSerialized()), ProtoUtil.stringOrNullFromProto(proto.getPayoutTxId()), proto.getContractAsJson(), ProtoUtil.byteArrayOrNullFromProto(proto.getMakerContractSignature()), ProtoUtil.byteArrayOrNullFromProto(proto.getTakerContractSignature()), proto.hasMakerPaymentAccountPayload() ? coreProtoResolver.fromProto(proto.getMakerPaymentAccountPayload()) : null, proto.hasTakerPaymentAccountPayload() ? coreProtoResolver.fromProto(proto.getTakerPaymentAccountPayload()) : null, PubKeyRing.fromProto(proto.getAgentPubKeyRing()), proto.getIsSupportTicket(), SupportType.fromProto(proto.getSupportType())); dispute.setExtraDataMap(CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : ExtraDataMapValidator.getValidatedExtraDataMap(proto.getExtraDataMap())); dispute.chatMessages.addAll(proto.getChatMessageList().stream() .map(ChatMessage::fromPayloadProto) .collect(Collectors.toList())); if (proto.hasDisputeResult()) dispute.disputeResultProperty.set(DisputeResult.fromProto(proto.getDisputeResult())); dispute.disputePayoutTxId = ProtoUtil.stringOrNullFromProto(proto.getDisputePayoutTxId()); String mediatorsDisputeResult = proto.getMediatorsDisputeResult(); if (!mediatorsDisputeResult.isEmpty()) { dispute.setMediatorsDisputeResult(mediatorsDisputeResult); } String delayedPayoutTxId = proto.getDelayedPayoutTxId(); if (!delayedPayoutTxId.isEmpty()) { dispute.setDelayedPayoutTxId(delayedPayoutTxId); } String donationAddressOfDelayedPayoutTx = proto.getDonationAddressOfDelayedPayoutTx(); if (!donationAddressOfDelayedPayoutTx.isEmpty()) { dispute.setDonationAddressOfDelayedPayoutTx(donationAddressOfDelayedPayoutTx); } if (Dispute.State.fromProto(proto.getState()) == State.NEEDS_UPGRADE) { // old disputes did not have a state field, so choose an appropriate state: dispute.setState(proto.getIsClosed() ? State.CLOSED : State.OPEN); if (dispute.getDisputeState() == State.CLOSED) { // mark chat messages as read for pre-existing CLOSED disputes // otherwise at upgrade, all old disputes would have 1 unread chat message // because currently when a dispute is closed, the last chat message is not marked read dispute.getChatMessages().forEach(m -> m.setWasDisplayed(true)); } } else { dispute.setState(Dispute.State.fromProto(proto.getState())); } dispute.refreshAlertLevel(true); return dispute; } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void addAndPersistChatMessage(ChatMessage chatMessage) { synchronized (chatMessages) { if (!chatMessages.contains(chatMessage)) { chatMessages.add(chatMessage); } else { log.error("disputeDirectMessage already exists"); } } } public boolean isMediationDispute() { return !chatMessages.isEmpty() && chatMessages.get(0).getSupportType() == SupportType.MEDIATION; } public boolean removeAllChatMessages() { synchronized (chatMessages) { if (chatMessages.size() > 1) { // removes all chat except the initial guidelines message. String firstMessageUid = chatMessages.get(0).getUid(); chatMessages.removeIf((msg) -> !msg.getUid().equals(firstMessageUid)); return true; } return false; } } public void maybeClearSensitiveData() { String change = ""; if (contract.maybeClearSensitiveData()) { change += "contract;"; } String edited = Contract.sanitizeContractAsJson(contractAsJson); if (!edited.equals(contractAsJson)) { contractAsJson = edited; change += "contractAsJson;"; } if (removeAllChatMessages()) { change += "chat messages;"; } if (change.length() > 0) { log.info("Cleared sensitive data from {} of dispute for trade {}", change, Utilities.getShortId(getTradeId())); } } // sanitizes a contract json string public static String sanitizeContractAsJson(String contractAsJson) { return contractAsJson; } /////////////////////////////////////////////////////////////////////////////////////////// // Setters /////////////////////////////////////////////////////////////////////////////////////////// public void setIsClosed() { setState(State.CLOSED); } public void reOpen() { setState(State.REOPENED); } public void setState(Dispute.State disputeState) { this.disputeState = disputeState; UserThread.execute(() -> this.isClosedProperty.set(disputeState == State.CLOSED)); } public void setDisputeResult(DisputeResult disputeResult) { disputeResultProperty.set(disputeResult); } public void setExtraData(String key, String value) { if (key == null || value == null) { return; } if (extraDataMap == null) { extraDataMap = new HashMap<>(); } extraDataMap.put(key, value); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// public String getShortTradeId() { return Utilities.getShortId(tradeId); } public ReadOnlyBooleanProperty isClosedProperty() { return isClosedProperty; } public ReadOnlyIntegerProperty getBadgeCountProperty() { return badgeCountProperty; } public ReadOnlyObjectProperty disputeResultProperty() { return disputeResultProperty; } public Date getTradeDate() { return new Date(tradeDate); } public Date getTradePeriodEnd() { return new Date(tradePeriodEnd); } public Date getOpeningDate() { return new Date(openingDate); } public boolean isNew() { return this.disputeState == State.NEW; } public boolean isOpen() { return isNew() || this.disputeState == State.OPEN || this.disputeState == State.REOPENED; } public boolean isClosed() { return this.disputeState == State.CLOSED; } public void refreshAlertLevel(boolean senderFlag) { // if the dispute is "new" that is 1 alert that has to be propagated upstream // or if there are unread messages that is 1 alert that has to be propagated upstream if (isNew() || unreadMessageCount(senderFlag) > 0) { badgeCountProperty.setValue(1); } else { badgeCountProperty.setValue(0); } } public long unreadMessageCount(boolean senderFlag) { return chatMessages.stream() .filter(m -> m.isSenderIsTrader() == senderFlag || m.isSystemMessage()) .filter(m -> !m.isWasDisplayed()) .count(); } public void setDisputeSeen(boolean senderFlag) { if (this.disputeState == State.NEW) setState(State.OPEN); refreshAlertLevel(senderFlag); } public void setChatMessagesSeen(boolean senderFlag) { getChatMessages().forEach(m -> m.setWasDisplayed(true)); refreshAlertLevel(senderFlag); } public String getRoleString() { if (disputeOpenerIsMaker) { if (disputeOpenerIsBuyer) return Res.get(isOpener() ? "support.buyerMaker" : "support.sellerTaker"); else return Res.get(isOpener() ? "support.sellerMaker" : "support.buyerTaker"); } else { if (disputeOpenerIsBuyer) return Res.get(isOpener() ? "support.buyerTaker" : "support.sellerMaker"); else return Res.get(isOpener() ? "support.sellerTaker" : "support.buyerMaker"); } } public String getRoleStringForLogFile() { return (disputeOpenerIsBuyer ? "BUYER" : "SELLER") + "_" + (disputeOpenerIsMaker ? "MAKER" : "TAKER"); } @Nullable public PaymentAccountPayload getBuyerPaymentAccountPayload() { return contract.isBuyerMakerAndSellerTaker() ? makerPaymentAccountPayload : takerPaymentAccountPayload; } @Nullable public PaymentAccountPayload getSellerPaymentAccountPayload() { return contract.isBuyerMakerAndSellerTaker() ? takerPaymentAccountPayload : makerPaymentAccountPayload; } @Override public String toString() { return "Dispute{" + "\n tradeId='" + tradeId + '\'' + ",\n id='" + id + '\'' + ",\n uid='" + uid + '\'' + ",\n state=" + disputeState + ",\n traderId=" + traderId + ",\n isOpener=" + isOpener + ",\n disputeOpenerIsBuyer=" + disputeOpenerIsBuyer + ",\n disputeOpenerIsMaker=" + disputeOpenerIsMaker + ",\n traderPubKeyRing=" + traderPubKeyRing + ",\n tradeDate=" + tradeDate + ",\n tradePeriodEnd=" + tradePeriodEnd + ",\n contract=" + contract + ",\n contractHash=" + Utilities.bytesAsHexString(contractHash) + ",\n payoutTxSerialized=" + Utilities.bytesAsHexString(payoutTxSerialized) + ",\n payoutTxId='" + payoutTxId + '\'' + ",\n contractAsJson='" + contractAsJson + '\'' + ",\n makerContractSignature='" + Utilities.bytesAsHexString(makerContractSignature) + '\'' + ",\n takerContractSignature='" + Utilities.bytesAsHexString(takerContractSignature) + '\'' + ",\n agentPubKeyRing=" + agentPubKeyRing + ",\n isSupportTicket=" + isSupportTicket + ",\n chatMessages=" + chatMessages + ",\n isClosedProperty=" + isClosedProperty + ",\n disputeResultProperty=" + disputeResultProperty + ",\n disputePayoutTxId='" + disputePayoutTxId + '\'' + ",\n openingDate=" + openingDate + ",\n supportType=" + supportType + ",\n mediatorsDisputeResult='" + mediatorsDisputeResult + '\'' + ",\n delayedPayoutTxId='" + delayedPayoutTxId + '\'' + ",\n donationAddressOfDelayedPayoutTx='" + donationAddressOfDelayedPayoutTx + '\'' + ",\n makerPaymentAccountPayload='" + makerPaymentAccountPayload + '\'' + ",\n takerPaymentAccountPayload='" + takerPaymentAccountPayload + '\'' + "\n}"; } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/DisputeAlreadyOpenException.java ================================================ /* * 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 . */ package haveno.core.support.dispute; public class DisputeAlreadyOpenException extends Exception { public DisputeAlreadyOpenException() { super(); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/DisputeList.java ================================================ /* * 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 . */ package haveno.core.support.dispute; import haveno.common.proto.persistable.PersistableListAsObservable; import haveno.common.proto.persistable.PersistablePayload; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.util.Collection; @Slf4j @ToString /* * Holds a List of Dispute objects. * * Calls to the List are delegated because this class intercepts the add/remove calls so changes * can be saved to disc. */ public abstract class DisputeList extends PersistableListAsObservable { public DisputeList() { } protected DisputeList(Collection collection) { super(collection); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/DisputeListService.java ================================================ /* * 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 . */ package haveno.core.support.dispute; import haveno.common.UserThread; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.persistable.PersistedDataHost; import haveno.core.trade.Contract; import haveno.network.p2p.NodeAddress; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.collections.ObservableList; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.fxmisc.easybind.EasyBind; import javax.annotation.Nullable; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; @Slf4j public abstract class DisputeListService> implements PersistedDataHost { @Getter protected final PersistenceManager persistenceManager; @Getter private final T disputeList; @Getter private final IntegerProperty numOpenDisputes = new SimpleIntegerProperty(); @Getter private final Set disputedTradeIds = new HashSet<>(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public DisputeListService(PersistenceManager persistenceManager) { this.persistenceManager = persistenceManager; disputeList = getConcreteDisputeList(); this.persistenceManager.initialize(disputeList, getFileName(), PersistenceManager.Source.PRIVATE); } /////////////////////////////////////////////////////////////////////////////////////////// // Abstract methods /////////////////////////////////////////////////////////////////////////////////////////// protected abstract T getConcreteDisputeList(); /////////////////////////////////////////////////////////////////////////////////////////// // PersistedDataHost /////////////////////////////////////////////////////////////////////////////////////////// @Override public void readPersisted(Runnable completeHandler) { persistenceManager.readPersisted(getFileName(), persisted -> { synchronized (persisted.getList()) { disputeList.setAll(persisted.getList()); } completeHandler.run(); }, completeHandler); } protected String getFileName() { return disputeList.getDefaultStorageFileName(); } /////////////////////////////////////////////////////////////////////////////////////////// // Public /////////////////////////////////////////////////////////////////////////////////////////// public void cleanupDisputes(@Nullable Consumer closedDisputeHandler) { synchronized (disputeList.getObservableList()) { disputeList.stream().forEach(dispute -> { String tradeId = dispute.getTradeId(); if (dispute.isClosed() && closedDisputeHandler != null) { closedDisputeHandler.accept(tradeId); } }); } } /////////////////////////////////////////////////////////////////////////////////////////// // Package scope /////////////////////////////////////////////////////////////////////////////////////////// void onAllServicesInitialized() { disputeList.addListener(change -> { change.next(); onDisputesChangeListener(change.getAddedSubList(), change.getRemoved()); }); onDisputesChangeListener(disputeList.getList(), null); } String getNrOfDisputes(boolean isBuyer, Contract contract) { return String.valueOf(getObservableList().stream() .filter(e -> { Contract contract1 = e.getContract(); if (contract1 == null) return false; if (isBuyer) { NodeAddress buyerNodeAddress = contract1.getBuyerNodeAddress(); return buyerNodeAddress != null && buyerNodeAddress.equals(contract.getBuyerNodeAddress()); } else { NodeAddress sellerNodeAddress = contract1.getSellerNodeAddress(); return sellerNodeAddress != null && sellerNodeAddress.equals(contract.getSellerNodeAddress()); } }) .collect(Collectors.toSet()).size()); } ObservableList getObservableList() { synchronized (disputeList.getObservableList()) { return disputeList.getObservableList(); } } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void onDisputesChangeListener(List addedList, @Nullable List removedList) { if (removedList != null) { synchronized (removedList) { removedList.forEach(dispute -> { disputedTradeIds.remove(dispute.getTradeId()); }); } } synchronized (addedList) { addedList.forEach(dispute -> { // for each dispute added, keep track of its "BadgeCountProperty" EasyBind.subscribe(dispute.getBadgeCountProperty(), isAlerting -> { // We get the event before the list gets updated, so we execute on next frame synchronized (disputeList.getObservableList()) { int numAlerts = (int) disputeList.getList().stream() .mapToLong(x -> x.getBadgeCountProperty().getValue()) .sum(); UserThread.execute(() -> numOpenDisputes.set(numAlerts)); } }); disputedTradeIds.add(dispute.getTradeId()); }); } } public void requestPersistence() { persistenceManager.requestPersistence(); } public void persistNow(@Nullable Runnable completeHandler) { persistenceManager.persistNow(completeHandler); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/DisputeManager.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.support.dispute; import haveno.common.ThreadUtils; import haveno.common.UserThread; import haveno.common.app.Version; import haveno.common.config.Config; import haveno.common.crypto.KeyRing; import haveno.common.crypto.PubKeyRing; import haveno.common.handlers.FaultHandler; import haveno.common.handlers.ResultHandler; import haveno.common.util.MathUtils; import haveno.common.util.Tuple2; import haveno.core.api.XmrConnectionService; import haveno.core.api.CoreNotificationService; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.Price; import haveno.core.monetary.TraditionalMoney; import haveno.core.offer.OfferPayload; import haveno.core.offer.OpenOfferManager; import haveno.core.provider.price.MarketPrice; import haveno.core.provider.price.PriceFeedService; import haveno.core.support.SupportManager; import haveno.core.support.dispute.messages.DisputeClosedMessage; import haveno.core.support.dispute.messages.DisputeOpenedMessage; import haveno.core.support.messages.ChatMessage; import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.Contract; import haveno.core.trade.HavenoUtils; import haveno.core.trade.SellerTrade; import haveno.core.trade.Trade; import haveno.core.trade.Trade.DisputeState; import haveno.core.trade.TradeManager; import haveno.core.trade.protocol.ArbitratorProtocol; import haveno.core.trade.protocol.TradePeer; import haveno.core.xmr.wallet.Restrictions; import haveno.core.xmr.wallet.TradeWalletService; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.BootstrapListener; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import haveno.network.p2p.SendMailboxMessageListener; import javafx.beans.property.IntegerProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import org.apache.commons.lang3.exception.ExceptionUtils; import java.math.BigInteger; import java.security.KeyPair; import java.time.Instant; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public abstract class DisputeManager> extends SupportManager { protected final TradeWalletService tradeWalletService; protected final XmrWalletService xmrWalletService; protected final ClosedTradableManager closedTradableManager; protected final OpenOfferManager openOfferManager; protected final KeyRing keyRing; protected final DisputeListService disputeListService; private final Config config; private final PriceFeedService priceFeedService; protected String pendingOutgoingMessage; @Getter protected final ObservableList validationExceptions = FXCollections.observableArrayList(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public DisputeManager(P2PService p2PService, TradeWalletService tradeWalletService, XmrWalletService xmrWalletService, XmrConnectionService xmrConnectionService, CoreNotificationService notificationService, TradeManager tradeManager, ClosedTradableManager closedTradableManager, OpenOfferManager openOfferManager, KeyRing keyRing, DisputeListService disputeListService, Config config, PriceFeedService priceFeedService) { super(p2PService, xmrConnectionService, xmrWalletService, notificationService, tradeManager); this.tradeWalletService = tradeWalletService; this.xmrWalletService = xmrWalletService; this.closedTradableManager = closedTradableManager; this.openOfferManager = openOfferManager; this.keyRing = keyRing; this.disputeListService = disputeListService; this.config = config; this.priceFeedService = priceFeedService; clearPendingMessage(); } /////////////////////////////////////////////////////////////////////////////////////////// // Implement template methods /////////////////////////////////////////////////////////////////////////////////////////// public KeyPair getSignatureKeyPair() { return keyRing.getSignatureKeyPair(); } @Override public void requestPersistence() { tradeManager.requestPersistence(); disputeListService.requestPersistence(); } public void requestPersistence(Trade trade) { trade.requestPersistence(); disputeListService.requestPersistence(); } @Override public void persistNow(@Nullable Runnable completeHandler) { tradeManager.persistNow(null); disputeListService.persistNow(completeHandler); } @Override public NodeAddress getPeerNodeAddress(ChatMessage message) { Optional disputeOptional = findDispute(message); if (disputeOptional.isEmpty()) { log.warn("Could not find dispute for tradeId = {} traderId = {}", message.getTradeId(), message.getTraderId()); return null; } return getNodeAddressPubKeyRingTuple(disputeOptional.get()).first; } @Override public PubKeyRing getPeerPubKeyRing(ChatMessage message) { Optional disputeOptional = findDispute(message); if (disputeOptional.isEmpty()) { log.warn("Could not find dispute for tradeId = {} traderId = {}", message.getTradeId(), message.getTraderId()); return null; } return getNodeAddressPubKeyRingTuple(disputeOptional.get()).second; } @Override public List getAllChatMessages(String tradeId) { synchronized (getDisputeList().getObservableList()) { return getDisputeList().stream() .filter(dispute -> dispute.getTradeId().equals(tradeId)) .flatMap(dispute -> dispute.getChatMessages().stream()) .collect(Collectors.toList()); } } @Override public boolean channelOpen(ChatMessage message) { return findDispute(message).isPresent(); } @Override public void addAndPersistChatMessage(ChatMessage message) { findDispute(message).ifPresent(dispute -> { if (dispute.getChatMessages().stream().noneMatch(m -> m.getUid().equals(message.getUid()))) { dispute.addAndPersistChatMessage(message); requestPersistence(); } else { log.warn("We got a chatMessage that we have already stored. UId = {} TradeId = {}", message.getUid(), message.getTradeId()); } }); } /////////////////////////////////////////////////////////////////////////////////////////// // Abstract methods /////////////////////////////////////////////////////////////////////////////////////////// // We get this message at both peers. The dispute object is in context of the trader public abstract void handle(DisputeClosedMessage disputeClosedMessage); public abstract NodeAddress getAgentNodeAddress(Dispute dispute); public abstract void cleanupDisputes(); protected abstract String getDisputeInfo(Dispute dispute); protected abstract String getDisputeIntroForPeer(String disputeInfo); protected abstract String getDisputeIntroForDisputeCreator(String disputeInfo); /////////////////////////////////////////////////////////////////////////////////////////// // Delegates for disputeListService /////////////////////////////////////////////////////////////////////////////////////////// public IntegerProperty getNumOpenDisputes() { return disputeListService.getNumOpenDisputes(); } public ObservableList getDisputesAsObservableList() { synchronized(disputeListService.getDisputeList().getObservableList()) { return disputeListService.getObservableList(); } } public String getNrOfDisputes(boolean isBuyer, Contract contract) { return disputeListService.getNrOfDisputes(isBuyer, contract); } protected T getDisputeList() { synchronized(disputeListService.getDisputeList().getObservableList()) { return disputeListService.getDisputeList(); } } public Set getDisputedTradeIds() { return disputeListService.getDisputedTradeIds(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onAllServicesInitialized() { super.onAllServicesInitialized(); disputeListService.onAllServicesInitialized(); p2PService.addP2PServiceListener(new BootstrapListener() { @Override public void onDataReceived() { tryApplyMessages(); } }); xmrWalletService.downloadPercentageProperty().addListener((observable, oldValue, newValue) -> { if (xmrWalletService.isSyncedWithinTolerance()) tryApplyMessages(); }); tryApplyMessages(); cleanupDisputes(); List disputes = getDisputeList().getList(); synchronized (disputes) { disputes.forEach(dispute -> { try { DisputeValidation.validateNodeAddresses(dispute, config); } catch (DisputeValidation.ValidationException e) { log.error(e.toString()); validationExceptions.add(e); } }); } maybeClearSensitiveData(); } public boolean isTrader(Dispute dispute) { return keyRing.getPubKeyRing().equals(dispute.getTraderPubKeyRing()); } public Optional findOwnDispute(String tradeId) { synchronized (getDisputeList()) { T disputeList = getDisputeList(); if (disputeList == null) { log.warn("disputes is null"); return Optional.empty(); } return disputeList.stream().filter(e -> e.getTradeId().equals(tradeId)).findAny(); } } public void maybeClearSensitiveData() { log.info("{} checking closed disputes eligibility for having sensitive data cleared", super.getClass().getSimpleName()); Instant safeDate = closedTradableManager.getSafeDateForSensitiveDataClearing(); synchronized (getDisputeList().getList()) { getDisputeList().getList().stream() .filter(e -> e.isClosed()) .filter(e -> e.getOpeningDate().toInstant().isBefore(safeDate)) .forEach(Dispute::maybeClearSensitiveData); requestPersistence(); } } /////////////////////////////////////////////////////////////////////////////////////////// // Dispute handling /////////////////////////////////////////////////////////////////////////////////////////// // trader sends message to arbitrator to open dispute public void sendDisputeOpenedMessage(Dispute dispute, ResultHandler resultHandler, FaultHandler faultHandler) { // get trade Trade trade = tradeManager.getTrade(dispute.getTradeId()); if (trade == null) { String errorMsg = "Dispute trade does not exist, tradeId=" + dispute.getTradeId(); faultHandler.handleFault(errorMsg, new IllegalStateException(errorMsg)); return; } // update dispute state trade.setDisputeState(Trade.DisputeState.DISPUTE_PREPARING); // open dispute on trade thread ThreadUtils.execute(() -> { // try to send dispute try { sendDisputeOpenedMessageAux(trade, dispute, resultHandler, (errorMessage, throwable) -> { log.warn("Failed to open dispute for trade: " + dispute.getTradeId() + ": " + errorMessage, throwable); removeDisputes(trade); faultHandler.handleFault(errorMessage, throwable); }); } catch (Exception e) { String errorMsg = "Failed to open dispute for trade " + dispute.getTradeId() + ": " + e.getMessage(); log.error(errorMsg, e); removeDisputes(trade); faultHandler.handleFault(errorMsg, e); } }, trade.getId()); } private void sendDisputeOpenedMessageAux(Trade trade, Dispute dispute, ResultHandler resultHandler, FaultHandler faultHandler) { log.info("Opening dispute for {} {}, dispute {}", trade.getClass().getSimpleName(), dispute.getTradeId(), dispute.getId()); // arbitrator cannot open disputes if (trade.isArbitrator()) { String errorMsg = "Arbitrators cannot open disputes."; faultHandler.handleFault(errorMsg, new IllegalStateException(errorMsg)); return; } // verify deposits unlocked or one is missing if (trade.getPhase().ordinal() < Trade.Phase.DEPOSITS_UNLOCKED.ordinal() && !trade.isDepositTxMissing()) { String errorMsg = Res.get("portfolio.pending.error.depositTxNotConfirmed"); faultHandler.handleFault(errorMsg, new IllegalStateException(errorMsg)); return; } // skip if payout is confirmed if (trade.isPayoutConfirmed()) { String errorMsg = "Cannot open dispute because payout is already confirmed for " + trade.getClass().getSimpleName() + " " + trade.getId(); faultHandler.handleFault(errorMsg, new IllegalStateException(errorMsg)); return; } // set dispute T disputeList = getDisputeList(); synchronized (disputeList.getObservableList()) { if (disputeList.contains(dispute)) { String msg = "We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId() + ", DisputeId = " + dispute.getId(); log.warn(msg); faultHandler.handleFault(msg, new DisputeAlreadyOpenException()); return; } Optional storedDisputeOptional = findDispute(dispute); boolean reOpen = storedDisputeOptional.isPresent(); // add or re-open dispute if (reOpen) { dispute = storedDisputeOptional.get(); } else { final Dispute finalDispute = dispute; UserThread.execute(() -> { synchronized (disputeList.getObservableList()) { disputeList.add(finalDispute); } }); } } // save state persistNow(null); // add dispute system message once boolean hasSystemMessage = dispute.getChatMessages().stream().anyMatch(ChatMessage::isSystemMessage); if (!hasSystemMessage) { String disputeInfo = getDisputeInfo(dispute); String sysMsg = dispute.isSupportTicket() ? Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) : Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION); ChatMessage chatMessage = new ChatMessage( getSupportType(), dispute.getTradeId(), keyRing.getPubKeyRing().hashCode(), false, Res.get("support.systemMsg", sysMsg), p2PService.getAddress()); chatMessage.setSystemMessage(true); dispute.addAndPersistChatMessage(chatMessage); } ChatMessage chatMessage = dispute.getChatMessages().get(dispute.getChatMessages().size() - 1); // last message // TODO: why can't this be assigned to local variable above? // try to import latest multisig info try { trade.importMultisigHex(); } catch (Exception e) { if (!trade.isShutDownStarted()) log.error("Failed to import multisig hex", e); } // abort if shutting down if (trade.isShutDownStarted()) { String errorMsg = "Aborting opening dispute for " + trade.getClass().getSimpleName() + " " + trade.getId() + " because shut down is started"; faultHandler.handleFault(errorMsg, new IllegalStateException(errorMsg)); return; } // try to export latest multisig info try { trade.exportMultisigHex(); if (trade instanceof SellerTrade) { trade.getProcessModel().setPaymentSentPayoutTxStale(true); // exporting multisig hex will invalidate previously unsigned payout txs trade.getSelf().setUnsignedPayoutTxHex(null); } } catch (Exception e) { if (!trade.isShutDownStarted()) log.error("Failed to export multisig hex", e); } // abort if shutting down if (trade.isShutDownStarted()) { String errorMsg = "Aborting opening dispute for " + trade.getClass().getSimpleName() + " " + trade.getId() + " because shut down is started"; faultHandler.handleFault(errorMsg, new IllegalStateException(errorMsg)); return; } // create dispute opened message NodeAddress agentNodeAddress = getAgentNodeAddress(dispute); DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute, p2PService.getAddress(), UUID.randomUUID().toString(), getSupportType(), trade.getSelf().getUpdatedMultisigHex(), trade.getArbitrator().getPaymentSentMessage()); log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + "chatMessage.uid={}", disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(), chatMessage.getUid()); recordPendingMessage(disputeOpenedMessage.getClass().getSimpleName()); // send dispute opened message mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress, dispute.getAgentPubKeyRing(), disputeOpenedMessage, new SendMailboxMessageListener() { @Override public void onArrived() { log.info("{} arrived at peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + "chatMessage.uid={}", disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(), chatMessage.getUid()); clearPendingMessage(); // We use the chatMessage wrapped inside the openNewDisputeMessage for // the state, as that is displayed to the user and we only persist that msg chatMessage.setArrived(true); trade.advanceDisputeState(Trade.DisputeState.DISPUTE_REQUESTED); persistNow(null); if (resultHandler != null) resultHandler.handleResult(); } @Override public void onStoredInMailbox() { log.info("{} stored in mailbox for peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + "chatMessage.uid={}", disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(), chatMessage.getUid()); clearPendingMessage(); // We use the chatMessage wrapped inside the openNewDisputeMessage for // the state, as that is displayed to the user and we only persist that msg chatMessage.setStoredInMailbox(true); trade.advanceDisputeState(Trade.DisputeState.DISPUTE_REQUESTED); trade.persistNow(null); persistNow(null); if (resultHandler != null) resultHandler.handleResult(); } @Override public void onFault(String errorMessage) { log.error("{} failed: Peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + "chatMessage.uid={}, errorMessage={}", disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(), chatMessage.getUid(), errorMessage); clearPendingMessage(); // We use the chatMessage wrapped inside the openNewDisputeMessage for // the state, as that is displayed to the user and we only persist that msg chatMessage.setSendMessageError(errorMessage); trade.setDisputeState(Trade.DisputeState.NO_DISPUTE); persistNow(null); faultHandler.handleFault("Sending dispute message failed: " + errorMessage, new DisputeMessageDeliveryFailedException()); } }); persistNow(null); } public void removeDisputes(Trade trade) { UserThread.execute(() -> { T disputeList = getDisputeList(); synchronized (disputeList.getObservableList()) { for (Dispute dispute : trade.getDisputes()) { disputeList.remove(dispute); } } trade.setDisputeState(Trade.DisputeState.NO_DISPUTE); clearPendingMessage(); requestPersistence(); }); } // arbitrator receives dispute opened message from opener, opener's peer receives from arbitrator protected void handle(DisputeOpenedMessage message) { Dispute msgDispute = message.getDispute(); log.info("Processing DisputeOpenedMessage with trade {}, dispute {}", message.getClass().getSimpleName(), msgDispute.getTradeId(), msgDispute.getId()); // get trade Trade trade = tradeManager.getTrade(msgDispute.getTradeId()); if (trade == null) { log.warn("Ignoring DisputeOpenedMessage for trade {} because it does not exist", msgDispute.getTradeId()); return; } // find existing dispute Optional storedDisputeOptional = findDispute(msgDispute); // determine if re-opening dispute boolean reOpen = storedDisputeOptional.isPresent(); // use existing dispute or create new Dispute dispute = reOpen ? storedDisputeOptional.get() : msgDispute; // get contract Contract contract = dispute.getContract(); // get sender TradePeer sender; PubKeyRing senderPubKeyRing; if (reOpen) { // re-open can come from either peer sender = trade.isArbitrator() ? trade.getTradePeer(message.getSenderNodeAddress()) : trade.getArbitrator(); senderPubKeyRing = sender.getPubKeyRing(); } else { senderPubKeyRing = trade.isArbitrator() ? (dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing()) : trade.getArbitrator().getPubKeyRing(); sender = trade.getTradePeer(senderPubKeyRing); } if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller"); // TODO: save message for reprocessing (arbitrator must remove this when processed or it'll attempt to be sent to peer) // sender.setDisputeOpenedMessage(message); // process on trade thread ThreadUtils.execute(() -> { synchronized (trade.getLock()) { String errorMessage = null; try { // initialize T disputeList = getDisputeList(); if (disputeList == null) { log.warn("disputes is null"); return; } dispute.setSupportType(message.getSupportType()); dispute.setState(Dispute.State.NEW); // validate dispute try { DisputeValidation.validateDisputeData(dispute); DisputeValidation.validateNodeAddresses(dispute, config); DisputeValidation.validateSenderNodeAddress(dispute, message.getSenderNodeAddress(), config); //DisputeValidation.testIfDisputeTriesReplay(dispute, disputeList.getList()); } catch (DisputeValidation.ValidationException e) { log.error(ExceptionUtils.getStackTrace(e)); validationExceptions.add(e); throw e; } // try to validate payment accounts try { DisputeValidation.validatePaymentAccountPayloads(dispute); // TODO: add field to dispute details: valid, invalid, missing } catch (Exception e) { log.error(ExceptionUtils.getStackTrace(e)); trade.prependErrorMessage(e.getMessage()); throw e; } // set arbitrator's payment account payloads if (trade.isArbitrator()) { if (trade.getBuyer().getPaymentAccountPayload() == null) trade.getBuyer().setPaymentAccountPayload(dispute.getBuyerPaymentAccountPayload()); if (trade.getSeller().getPaymentAccountPayload() == null) trade.getSeller().setPaymentAccountPayload(dispute.getSellerPaymentAccountPayload()); } // update sender node address sender.setNodeAddress(message.getSenderNodeAddress()); // verify message to trader is expected from arbitrator if (!trade.isArbitrator() && sender != trade.getArbitrator()) { throw new RuntimeException(message.getClass().getSimpleName() + " to trader is expected only from arbitrator"); } // arbitrator verifies signature of payment sent message if given if (trade.isArbitrator() && message.getPaymentSentMessage() != null) { HavenoUtils.verifyPaymentSentMessage(trade, message.getPaymentSentMessage()); trade.getBuyer().setUpdatedMultisigHex(message.getPaymentSentMessage().getUpdatedMultisigHex()); trade.setStateIfValidTransitionTo(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); } // update opener's multisig hex TradePeer opener = sender == trade.getArbitrator() ? trade.getTradePeer() : sender; if (message.getOpenerUpdatedMultisigHex() != null) opener.setUpdatedMultisigHex(message.getOpenerUpdatedMultisigHex()); // TODO: peer needs to import multisig hex at some point // TODO: DisputeOpenedMessage should include arbitrator's updated multisig hex too // TODO: arbitrator needs to import multisig info then scan for updated state? // sync and poll wallet unless finalized if (!trade.isPayoutFinalized()) { trade.syncAndPollWallet(); trade.recoverIfMissingWalletData(); } // nack if payout published if (trade.isPayoutPublished()) { throw new RuntimeException("Ignoring DisputeOpenedMessage because payout is already published for " + trade.getClass().getSimpleName() + " " + trade.getId() + ", payoutTxId=" + trade.getPayoutTxId()); } // add chat message with price info if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0); // add or re-open dispute synchronized (disputeList) { if (!disputeList.contains(msgDispute)) { if (!storedDisputeOptional.isPresent() || reOpen) { // update trade state if (reOpen) { trade.setDisputeState(Trade.DisputeState.DISPUTE_OPENED); } else { UserThread.execute(() -> { synchronized (disputeList) { disputeList.add(dispute); } }); trade.advanceDisputeState(Trade.DisputeState.DISPUTE_OPENED); } // reset buyer and seller unsigned payout tx hex trade.getBuyer().setUnsignedPayoutTxHex(null); trade.getSeller().setUnsignedPayoutTxHex(null); // send dispute opened message to other peer if arbitrator if (trade.isArbitrator()) { TradePeer senderPeer = sender == trade.getMaker() ? trade.getTaker() : trade.getMaker(); if (senderPeer != trade.getMaker() && senderPeer != trade.getTaker()) throw new RuntimeException("Sender peer is not maker or taker, address=" + senderPeer.getNodeAddress()); sendDisputeOpenedMessageToPeer(dispute, contract, senderPeer.getPubKeyRing(), opener.getUpdatedMultisigHex()); } tradeManager.requestPersistence(); errorMessage = null; } else { // valid case if both have opened a dispute and agent was not online log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}", dispute.getTradeId()); } // add chat message with mediation info if applicable addMediationResultMessage(dispute); } else { throw new RuntimeException("We got a dispute msg that we have already stored. TradeId = " + msgDispute.getTradeId()); } } } catch (Exception e) { log.error(ExceptionUtils.getStackTrace(e)); errorMessage = e.getMessage(); if (trade != null) trade.setErrorMessage(errorMessage); } // use chat message instead of open dispute message for the ack ObservableList messages = message.getDispute().getChatMessages(); if (!messages.isEmpty()) { ChatMessage msg = messages.get(messages.size() - 1); // send ack to sender of last chat message sendAckMessage(msg, senderPubKeyRing, errorMessage == null, errorMessage); } requestPersistence(); } }, trade.getId()); } // arbitrator sends dispute opened message to opener's peer private void sendDisputeOpenedMessageToPeer(Dispute disputeFromOpener, Contract contractFromOpener, PubKeyRing pubKeyRing, String updatedMultisigHex) { log.info("{} sendPeerOpenedDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), disputeFromOpener.getTradeId(), disputeFromOpener.getId()); // We delay a bit for sending the message to the peer to allow that a openDispute message from the peer is // being used as the valid msg. If dispute agent was offline and both peer requested we want to see the correct // message and not skip the system message of the peer as it would be the case if we have created the system msg // from the code below. UserThread.runAfter(() -> doSendPeerOpenedDisputeMessage(disputeFromOpener, contractFromOpener, pubKeyRing, updatedMultisigHex), 100, TimeUnit.MILLISECONDS); } private void doSendPeerOpenedDisputeMessage(Dispute disputeFromOpener, Contract contractFromOpener, PubKeyRing pubKeyRing, String updatedMultisigHex) { T disputeList = getDisputeList(); if (disputeList == null) { log.warn("disputes is null"); return; } // create mirrored dispute Dispute dispute = new Dispute(new Date().getTime(), disputeFromOpener.getTradeId(), pubKeyRing.hashCode(), false, disputeFromOpener.isDisputeOpenerIsBuyer(), disputeFromOpener.isDisputeOpenerIsMaker(), pubKeyRing, disputeFromOpener.getTradeDate().getTime(), disputeFromOpener.getTradePeriodEnd().getTime(), contractFromOpener, disputeFromOpener.getContractHash(), disputeFromOpener.getPayoutTxSerialized(), disputeFromOpener.getPayoutTxId(), disputeFromOpener.getContractAsJson(), disputeFromOpener.getMakerContractSignature(), disputeFromOpener.getTakerContractSignature(), disputeFromOpener.getMakerPaymentAccountPayload(), disputeFromOpener.getTakerPaymentAccountPayload(), disputeFromOpener.getAgentPubKeyRing(), disputeFromOpener.isSupportTicket(), disputeFromOpener.getSupportType()); dispute.setExtraDataMap(disputeFromOpener.getExtraDataMap()); dispute.setDelayedPayoutTxId(disputeFromOpener.getDelayedPayoutTxId()); dispute.setDonationAddressOfDelayedPayoutTx(disputeFromOpener.getDonationAddressOfDelayedPayoutTx()); // skip if dispute already open Optional storedDisputeOptional = findDispute(dispute); if (storedDisputeOptional.isPresent() && !storedDisputeOptional.get().isClosed()) { log.info("We got a dispute already open for that trade and trading peer. TradeId = {}", dispute.getTradeId()); return; } String disputeInfo = getDisputeInfo(dispute); String disputeMessage = getDisputeIntroForPeer(disputeInfo); String sysMsg = dispute.isSupportTicket() ? Res.get("support.peerOpenedTicket", disputeInfo, Version.VERSION) : disputeMessage; ChatMessage chatMessage = new ChatMessage( getSupportType(), dispute.getTradeId(), pubKeyRing.hashCode(), false, Res.get("support.systemMsg", sysMsg), p2PService.getAddress()); chatMessage.setSystemMessage(true); dispute.addAndPersistChatMessage(chatMessage); addPriceInfoMessage(dispute, 0); // add or re-open dispute boolean reOpen = storedDisputeOptional.isPresent() && storedDisputeOptional.get().isClosed(); if (reOpen) { dispute = storedDisputeOptional.get(); dispute.reOpen(); } else { final Dispute finalDispute = dispute; UserThread.execute(() -> { synchronized (disputeList) { disputeList.add(finalDispute); } }); } // get trade Trade trade = tradeManager.getTrade(dispute.getTradeId()); if (trade == null) { log.warn("Dispute trade {} does not exist", dispute.getTradeId()); return; } // create dispute opened message with peer dispute TradePeer peer = trade.getTradePeer(pubKeyRing); DisputeOpenedMessage peerOpenedDisputeMessage = new DisputeOpenedMessage(dispute, p2PService.getAddress(), UUID.randomUUID().toString(), getSupportType(), updatedMultisigHex, trade.getArbitrator().getPaymentSentMessage()); // save message for resending if needed peer.setDisputeOpenedMessage(peerOpenedDisputeMessage); // send dispute opened message ((ArbitratorProtocol) trade.getProtocol()).sendDisputeOpenedMessageIfApplicable(); persistNow(null); } // arbitrator sends result to trader when their dispute is closed public void closeDisputeTicket(DisputeResult disputeResult, Dispute dispute, String summaryText, ResultHandler resultHandler, FaultHandler faultHandler) { try { // get trade Trade trade = tradeManager.getTrade(dispute.getTradeId()); if (trade == null) throw new RuntimeException("Dispute trade " + dispute.getTradeId() + " does not exist"); // persist result in dispute's chat message once boolean exists = disputeResult.getChatMessage() != null && disputeResult.getChatMessage().getMessage() != null && !disputeResult.getChatMessage().getMessage().isEmpty(); if (!exists) { ChatMessage chatMessage = new ChatMessage( getSupportType(), dispute.getTradeId(), dispute.getTraderPubKeyRing().hashCode(), false, summaryText, p2PService.getAddress()); disputeResult.setChatMessage(chatMessage); dispute.addAndPersistChatMessage(chatMessage); } // create dispute payout tx TradePeer receiver = trade.getTradePeer(dispute.getTraderPubKeyRing()); if (!trade.isPayoutPublished() && receiver.getUpdatedMultisigHex() != null && receiver.getUnsignedPayoutTxHex() == null) { trade.createDisputePayoutTx(dispute.getContract(), disputeResult, true); } // create dispute closed message TradePeer receiverPeer = receiver == trade.getBuyer() ? trade.getSeller() : trade.getBuyer(); boolean deferPublishPayout = !exists && receiver.getUnsignedPayoutTxHex() != null && receiverPeer.getUpdatedMultisigHex() != null && (trade.getDisputeState() == Trade.DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG || trade.getDisputeState().ordinal() >= Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG.ordinal()); DisputeClosedMessage disputeClosedMessage = new DisputeClosedMessage(disputeResult, p2PService.getAddress(), UUID.randomUUID().toString(), getSupportType(), trade.getSelf().getUpdatedMultisigHex(), receiver.getUnsignedPayoutTxHex(), // include dispute payout tx if arbitrator has their updated multisig info deferPublishPayout); // instruct trader to defer publishing payout tx because peer is expected to publish imminently receiverPeer.setDisputeClosedMessage(disputeClosedMessage); // send dispute closed message log.info("Send {} to trader {}. tradeId={}, {}.uid={}, chatMessage.uid={}", disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(), disputeClosedMessage.getClass().getSimpleName(), disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(), disputeResult.getChatMessage().getUid()); recordPendingMessage(disputeClosedMessage.getClass().getSimpleName()); mailboxMessageService.sendEncryptedMailboxMessage(receiver.getNodeAddress(), dispute.getTraderPubKeyRing(), disputeClosedMessage, new SendMailboxMessageListener() { @Override public void onArrived() { log.info("{} arrived at trader {}. tradeId={}, disputeClosedMessage.uid={}, " + "chatMessage.uid={}", disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(), disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(), disputeResult.getChatMessage().getUid()); clearPendingMessage(); dispute.setIsClosed(); // We use the chatMessage wrapped inside the DisputeClosedMessage for // the state, as that is displayed to the user and we only persist that msg disputeResult.getChatMessage().setArrived(true); trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG); trade.pollWalletNormallyForMs(Trade.POLL_WALLET_NORMALLY_DEFAULT_PERIOD_MS); requestPersistence(trade); resultHandler.handleResult(); } @Override public void onStoredInMailbox() { log.info("{} stored in mailbox for trader {}. tradeId={}, DisputeClosedMessage.uid={}, " + "chatMessage.uid={}", disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(), disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(), disputeResult.getChatMessage().getUid()); clearPendingMessage(); dispute.setIsClosed(); // We use the chatMessage wrapped inside the DisputeClosedMessage for // the state, as that is displayed to the user and we only persist that msg disputeResult.getChatMessage().setStoredInMailbox(true); Trade trade = tradeManager.getTrade(dispute.getTradeId()); trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG); requestPersistence(trade); resultHandler.handleResult(); } @Override public void onFault(String errorMessage) { log.error("{} failed: Trader {}. tradeId={}, DisputeClosedMessage.uid={}, " + "chatMessage.uid={}, errorMessage={}", disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(), disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(), disputeResult.getChatMessage().getUid(), errorMessage); clearPendingMessage(); // We use the chatMessage wrapped inside the DisputeClosedMessage for // the state, as that is displayed to the user and we only persist that msg disputeResult.getChatMessage().setSendMessageError(errorMessage); trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG); requestPersistence(trade); faultHandler.handleFault(errorMessage, new RuntimeException(errorMessage)); } } ); trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG); requestPersistence(trade); } catch (Exception e) { faultHandler.handleFault(e.getMessage(), e); } } /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// private Tuple2 getNodeAddressPubKeyRingTuple(Dispute dispute) { PubKeyRing receiverPubKeyRing = null; NodeAddress peerNodeAddress = null; if (isTrader(dispute)) { receiverPubKeyRing = dispute.getAgentPubKeyRing(); peerNodeAddress = getAgentNodeAddress(dispute); } else if (isAgent(dispute)) { receiverPubKeyRing = dispute.getTraderPubKeyRing(); Contract contract = dispute.getContract(); if (contract.getBuyerPubKeyRing().equals(receiverPubKeyRing)) peerNodeAddress = contract.getBuyerNodeAddress(); else peerNodeAddress = contract.getSellerNodeAddress(); } else { log.error("That must not happen. Trader cannot communicate to other trader."); } return new Tuple2<>(peerNodeAddress, receiverPubKeyRing); } public boolean isAgent(Dispute dispute) { return keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing()); } public Optional findDispute(Dispute dispute) { return findDispute(dispute.getTradeId(), dispute.getTraderId()); } public Optional findDispute(DisputeResult disputeResult) { ChatMessage chatMessage = disputeResult.getChatMessage(); checkNotNull(chatMessage, "chatMessage must not be null"); return findDispute(disputeResult.getTradeId(), disputeResult.getTraderId()); } public Optional findDispute(ChatMessage message) { return findDispute(message.getTradeId(), message.getTraderId()); } public Optional findDispute(String tradeId, int traderId) { T disputeList = getDisputeList(); if (disputeList == null) { log.warn("disputes is null"); return Optional.empty(); } return disputeList.stream() .filter(e -> e.getTradeId().equals(tradeId) && e.getTraderId() == traderId) .findAny(); } // TODO: throw if more than one dispute found? should not be called then public Optional findDispute(String tradeId) { T disputeList = getDisputeList(); if (disputeList == null) { log.warn("disputes is null"); return Optional.empty(); } return disputeList.stream() .filter(e -> e.getTradeId().equals(tradeId)) .findAny(); } public List findDisputes(String tradeId) { T disputeList = getDisputeList(); if (disputeList == null) return new ArrayList(); return disputeList.stream() .filter(e -> e.getTradeId().equals(tradeId)) .collect(Collectors.toList()); } public Optional findDisputeById(String disputeId) { T disputeList = getDisputeList(); if (disputeList == null) { log.warn("disputes is null"); return Optional.empty(); } return disputeList.stream() .filter(e -> e.getId().equals(disputeId)) .findAny(); } public Optional findTrade(Dispute dispute) { Optional retVal = tradeManager.getOpenTrade(dispute.getTradeId()); if (!retVal.isPresent()) { retVal = tradeManager.getClosedTrade(dispute.getTradeId()); } return retVal; } public boolean canSendChatMessages(Dispute dispute) { if (dispute.isClosed()) return false; Optional tradeOptional = findTrade(dispute); if (!tradeOptional.isPresent()) { //log.warn("Dispute trade {} does not exist", dispute.getTradeId()); return false; } Trade trade = tradeOptional.get(); if (trade.isPayoutPublished()) return false; if (trade.getDisputeState() == DisputeState.DISPUTE_REQUESTED) { for (ChatMessage msg : dispute.getChatMessages()) { if (Boolean.TRUE.equals(msg.getStoredInMailboxProperty().get())) { return true; } } } return trade.getDisputeState().isOpen(); } private void addMediationResultMessage(Dispute dispute) { // In case of refundAgent we add a message with the mediatorsDisputeSummary. Only visible for refundAgent. if (dispute.getMediatorsDisputeResult() != null) { String mediatorsDisputeResult = Res.get("support.mediatorsDisputeSummary", dispute.getMediatorsDisputeResult()); ChatMessage mediatorsDisputeClosedMessage = new ChatMessage( getSupportType(), dispute.getTradeId(), keyRing.getPubKeyRing().hashCode(), false, mediatorsDisputeResult, p2PService.getAddress()); mediatorsDisputeClosedMessage.setSystemMessage(true); dispute.addAndPersistChatMessage(mediatorsDisputeClosedMessage); requestPersistence(); } } public void addMediationReOpenedMessage(Dispute dispute, boolean senderIsTrader) { ChatMessage chatMessage = new ChatMessage( getSupportType(), dispute.getTradeId(), dispute.getTraderId(), senderIsTrader, Res.get("support.info.disputeReOpened"), p2PService.getAddress()); chatMessage.setSystemMessage(false); dispute.addAndPersistChatMessage(chatMessage); this.sendChatMessage(chatMessage); requestPersistence(); } protected void addMediationLogsReceivedMessage(Dispute dispute, String logsIdentifier) { String logsReceivedMessage = Res.get("support.mediatorReceivedLogs", logsIdentifier); ChatMessage chatMessage = new ChatMessage( getSupportType(), dispute.getTradeId(), keyRing.hashCode(), false, logsReceivedMessage, p2PService.getAddress()); chatMessage.setSystemMessage(true); dispute.addAndPersistChatMessage(chatMessage); requestPersistence(); } // If price was going down between take offer time and open dispute time the buyer has an incentive to // not send the payment but to try to make a new trade with the better price. We risks to lose part of the // security deposit (in mediation we will always get back 0.003 BTC to keep some incentive to accept mediated // proposal). But if gain is larger than this loss he has economically an incentive to default in the trade. // We do all those calculations to give a hint to mediators to detect option trades. protected void addPriceInfoMessage(Dispute dispute, int counter) { if (!priceFeedService.hasExternalPrices()) { if (counter < 3) { log.info("Price provider has still no data. This is expected at startup. We try again in 10 sec."); UserThread.runAfter(() -> addPriceInfoMessage(dispute, counter + 1), 10); } else { log.warn("Price provider still has no data after 3 repeated requests and 30 seconds delay. We give up."); } return; } Contract contract = dispute.getContract(); OfferPayload offerPayload = contract.getOfferPayload(); Price priceAtDisputeOpening = getPrice(offerPayload.getCurrencyCode()); if (priceAtDisputeOpening == null) { log.info("Price provider did not provide a price for {}. " + "This is expected if this currency is not supported by the price providers.", offerPayload.getCurrencyCode()); return; } // The amount we would get if we do a new trade with current price BigInteger potentialAmountAtDisputeOpening = priceAtDisputeOpening.getAmountByVolume(contract.getTradeVolume()); BigInteger buyerSecurityDeposit = offerPayload.getMaxBuyerSecurityDeposit(); BigInteger minRefundAtMediatedDispute = Restrictions.getMinRefundAtMediatedDispute(); // minRefundAtMediatedDispute is always larger as buyerSecurityDeposit at mediated payout, we ignore refund agent case here as there it can be 0. BigInteger maxLossSecDeposit = buyerSecurityDeposit.subtract(minRefundAtMediatedDispute); BigInteger tradeAmount = contract.getTradeAmount(); BigInteger potentialGain = potentialAmountAtDisputeOpening.subtract(tradeAmount).subtract(maxLossSecDeposit); String optionTradeDetails; // We don't translate those strings (yet) as it is only displayed to mediators/arbitrators. String headline; if (potentialGain.compareTo(BigInteger.ZERO) > 0) { headline = "This might be a potential option trade!"; optionTradeDetails = "\nBTC amount calculated with price at dispute opening: " + HavenoUtils.formatXmr(potentialAmountAtDisputeOpening, true) + "\nMax loss of security deposit is: " + HavenoUtils.formatXmr(maxLossSecDeposit, true) + "\nPossible gain from an option trade is: " + HavenoUtils.formatXmr(potentialGain, true); } else { headline = "It does not appear to be an option trade."; optionTradeDetails = "\nBTC amount calculated with price at dispute opening: " + HavenoUtils.formatXmr(potentialAmountAtDisputeOpening, true) + "\nMax loss of security deposit is: " + HavenoUtils.formatXmr(maxLossSecDeposit, true) + "\nPossible loss from an option trade is: " + HavenoUtils.formatXmr(potentialGain.multiply(BigInteger.valueOf(-1)), true); } String percentagePriceDetails = offerPayload.isUseMarketBasedPrice() ? " (market based price was used: " + offerPayload.getMarketPriceMarginPct() * 100 + "%)" : " (fix price was used)"; String priceInfoText = "System message: " + headline + "\n\nTrade price: " + contract.getPrice().toFriendlyString() + percentagePriceDetails + "\nTrade amount: " + HavenoUtils.formatXmr(tradeAmount, true) + "\nPrice at dispute opening: " + priceAtDisputeOpening.toFriendlyString() + optionTradeDetails; // We use the existing msg to copy over the users data ChatMessage priceInfoMessage = new ChatMessage( getSupportType(), dispute.getTradeId(), keyRing.getPubKeyRing().hashCode(), false, priceInfoText, p2PService.getAddress()); priceInfoMessage.setSystemMessage(true); dispute.addAndPersistChatMessage(priceInfoMessage); requestPersistence(); } @Nullable private Price getPrice(String currencyCode) { MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) { double marketPriceAsDouble = marketPrice.getPrice(); try { int precision = CurrencyUtil.isTraditionalCurrency(currencyCode) ? TraditionalMoney.SMALLEST_UNIT_EXPONENT : CryptoMoney.SMALLEST_UNIT_EXPONENT; double scaled = MathUtils.scaleUpByPowerOf10(marketPriceAsDouble, precision); long roundedToLong = MathUtils.roundDoubleToLong(scaled); return Price.valueOf(currencyCode, roundedToLong); } catch (Exception e) { log.error("Exception at getPrice / parseToFiat: " + e.toString()); return null; } } else { return null; } } public boolean hasPendingMessageAtShutdown() { if (pendingOutgoingMessage.length() > 0) { log.warn("{} has an outgoing message pending: {}", this.getClass().getSimpleName(), pendingOutgoingMessage); return true; } return false; } private void recordPendingMessage(String className) { pendingOutgoingMessage = className; } private void clearPendingMessage() { pendingOutgoingMessage = ""; } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/DisputeMessageDeliveryFailedException.java ================================================ /* * 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 . */ package haveno.core.support.dispute; public class DisputeMessageDeliveryFailedException extends Exception { public DisputeMessageDeliveryFailedException() { super(); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/DisputeResult.java ================================================ /* * 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 . */ package haveno.core.support.dispute; import com.google.protobuf.ByteString; import haveno.common.proto.ProtoUtil; import haveno.common.proto.network.NetworkPayload; import haveno.common.util.Utilities; import haveno.core.support.messages.ChatMessage; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.math.BigInteger; import java.util.Date; import java.util.Optional; @EqualsAndHashCode @Getter @Slf4j public final class DisputeResult implements NetworkPayload { public enum Winner { BUYER, SELLER } public enum Reason { OTHER, BUG, USABILITY, SCAM, // Not used anymore PROTOCOL_VIOLATION, // Not used anymore NO_REPLY, // Not used anymore BANK_PROBLEMS, OPTION_TRADE, SELLER_NOT_RESPONDING, WRONG_SENDER_ACCOUNT, TRADE_ALREADY_SETTLED, PEER_WAS_LATE } public enum SubtractFeeFrom { BUYER_ONLY, SELLER_ONLY, BUYER_AND_SELLER } private final String tradeId; private final int traderId; @Setter @Nullable private Winner winner; private int reasonOrdinal = Reason.OTHER.ordinal(); @Setter @Nullable private SubtractFeeFrom subtractFeeFrom; private final BooleanProperty tamperProofEvidenceProperty = new SimpleBooleanProperty(); private final BooleanProperty idVerificationProperty = new SimpleBooleanProperty(); private final BooleanProperty screenCastProperty = new SimpleBooleanProperty(); private final StringProperty summaryNotesProperty = new SimpleStringProperty(""); @Setter @Nullable private ChatMessage chatMessage; @Setter @Nullable private byte[] arbitratorSignature; private long buyerPayoutAmountBeforeCost; private long sellerPayoutAmountBeforeCost; @Setter @Nullable private byte[] arbitratorPubKey; private long closeDate; public DisputeResult(String tradeId, int traderId) { this.tradeId = tradeId; this.traderId = traderId; } public DisputeResult(String tradeId, int traderId, @Nullable Winner winner, int reasonOrdinal, @Nullable SubtractFeeFrom subtractFeeFrom, boolean tamperProofEvidence, boolean idVerification, boolean screenCast, String summaryNotes, @Nullable ChatMessage chatMessage, @Nullable byte[] arbitratorSignature, long buyerPayoutAmountBeforeCost, long sellerPayoutAmountBeforeCost, @Nullable byte[] arbitratorPubKey, long closeDate) { this.tradeId = tradeId; this.traderId = traderId; this.winner = winner; this.reasonOrdinal = reasonOrdinal; this.subtractFeeFrom = subtractFeeFrom; this.tamperProofEvidenceProperty.set(tamperProofEvidence); this.idVerificationProperty.set(idVerification); this.screenCastProperty.set(screenCast); this.summaryNotesProperty.set(summaryNotes); this.chatMessage = chatMessage; this.arbitratorSignature = arbitratorSignature; this.buyerPayoutAmountBeforeCost = buyerPayoutAmountBeforeCost; this.sellerPayoutAmountBeforeCost = sellerPayoutAmountBeforeCost; this.arbitratorPubKey = arbitratorPubKey; this.closeDate = closeDate; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// public static DisputeResult fromProto(protobuf.DisputeResult proto) { return new DisputeResult(proto.getTradeId(), proto.getTraderId(), ProtoUtil.enumFromProto(DisputeResult.Winner.class, proto.getWinner().name()), proto.getReasonOrdinal(), ProtoUtil.enumFromProto(DisputeResult.SubtractFeeFrom.class, proto.getSubtractFeeFrom().name()), proto.getTamperProofEvidence(), proto.getIdVerification(), proto.getScreenCast(), proto.getSummaryNotes(), proto.getChatMessage() == null ? null : ChatMessage.fromPayloadProto(proto.getChatMessage()), proto.getArbitratorSignature().toByteArray(), proto.getBuyerPayoutAmountBeforeCost(), proto.getSellerPayoutAmountBeforeCost(), proto.getArbitratorPubKey().toByteArray(), proto.getCloseDate()); } @Override public protobuf.DisputeResult toProtoMessage() { final protobuf.DisputeResult.Builder builder = protobuf.DisputeResult.newBuilder() .setTradeId(tradeId) .setTraderId(traderId) .setReasonOrdinal(reasonOrdinal) .setTamperProofEvidence(tamperProofEvidenceProperty.get()) .setIdVerification(idVerificationProperty.get()) .setScreenCast(screenCastProperty.get()) .setSummaryNotes(summaryNotesProperty.get()) .setBuyerPayoutAmountBeforeCost(buyerPayoutAmountBeforeCost) .setSellerPayoutAmountBeforeCost(sellerPayoutAmountBeforeCost) .setCloseDate(closeDate); Optional.ofNullable(arbitratorSignature).ifPresent(arbitratorSignature -> builder.setArbitratorSignature(ByteString.copyFrom(arbitratorSignature))); Optional.ofNullable(arbitratorPubKey).ifPresent(arbitratorPubKey -> builder.setArbitratorPubKey(ByteString.copyFrom(arbitratorPubKey))); Optional.ofNullable(winner).ifPresent(result -> builder.setWinner(protobuf.DisputeResult.Winner.valueOf(winner.name()))); Optional.ofNullable(subtractFeeFrom).ifPresent(result -> builder.setSubtractFeeFrom(protobuf.DisputeResult.SubtractFeeFrom.valueOf(subtractFeeFrom.name()))); Optional.ofNullable(chatMessage).ifPresent(chatMessage -> builder.setChatMessage(chatMessage.toProtoNetworkEnvelope().getChatMessage())); return builder.build(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public BooleanProperty tamperProofEvidenceProperty() { return tamperProofEvidenceProperty; } public BooleanProperty idVerificationProperty() { return idVerificationProperty; } public BooleanProperty screenCastProperty() { return screenCastProperty; } public void setReason(Reason reason) { this.reasonOrdinal = reason.ordinal(); } public Reason getReason() { if (reasonOrdinal < Reason.values().length) return Reason.values()[reasonOrdinal]; else return Reason.OTHER; } public void setSummaryNotes(String summaryNotes) { this.summaryNotesProperty.set(summaryNotes); } public StringProperty summaryNotesProperty() { return summaryNotesProperty; } public void setBuyerPayoutAmountBeforeCost(BigInteger buyerPayoutAmountBeforeCost) { if (buyerPayoutAmountBeforeCost.compareTo(BigInteger.ZERO) < 0) throw new IllegalArgumentException("buyerPayoutAmountBeforeCost cannot be negative"); this.buyerPayoutAmountBeforeCost = buyerPayoutAmountBeforeCost.longValueExact(); } public BigInteger getBuyerPayoutAmountBeforeCost() { return BigInteger.valueOf(buyerPayoutAmountBeforeCost); } public void setSellerPayoutAmountBeforeCost(BigInteger sellerPayoutAmountBeforeCost) { if (sellerPayoutAmountBeforeCost.compareTo(BigInteger.ZERO) < 0) throw new IllegalArgumentException("sellerPayoutAmountBeforeCost cannot be negative"); this.sellerPayoutAmountBeforeCost = sellerPayoutAmountBeforeCost.longValueExact(); } public BigInteger getSellerPayoutAmountBeforeCost() { return BigInteger.valueOf(sellerPayoutAmountBeforeCost); } public void setCloseDate(Date closeDate) { this.closeDate = closeDate.getTime(); } public Date getCloseDate() { return new Date(closeDate); } @Override public String toString() { return "DisputeResult{" + "\n tradeId='" + tradeId + '\'' + ",\n traderId=" + traderId + ",\n winner=" + winner + ",\n reasonOrdinal=" + reasonOrdinal + ",\n subtractFeeFrom=" + subtractFeeFrom + ",\n tamperProofEvidenceProperty=" + tamperProofEvidenceProperty + ",\n idVerificationProperty=" + idVerificationProperty + ",\n screenCastProperty=" + screenCastProperty + ",\n summaryNotesProperty=" + summaryNotesProperty + ",\n chatMessage=" + chatMessage + ",\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) + ",\n buyerPayoutAmountBeforeCost=" + buyerPayoutAmountBeforeCost + ",\n sellerPayoutAmountBeforeCost=" + sellerPayoutAmountBeforeCost + ",\n arbitratorPubKey=" + Utilities.bytesAsHexString(arbitratorPubKey) + ",\n closeDate=" + closeDate + "\n}"; } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/DisputeSession.java ================================================ /* * 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 . */ package haveno.core.support.dispute; import haveno.core.support.SupportSession; import haveno.core.support.messages.ChatMessage; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; @Slf4j public abstract class DisputeSession extends SupportSession { @Nullable private Dispute dispute; private final boolean isTrader; public DisputeSession(@Nullable Dispute dispute, boolean isTrader) { super(); this.dispute = dispute; this.isTrader = isTrader; } /////////////////////////////////////////////////////////////////////////////////////////// // Dependent on selected dispute /////////////////////////////////////////////////////////////////////////////////////////// @Override public boolean isClient() { return isTrader; } @Override public String getTradeId() { return dispute != null ? dispute.getTradeId() : ""; } @Override public int getClientId() { // Get pubKeyRing of trader. Arbitrator is considered server for the chat session try { return dispute.getTraderPubKeyRing().hashCode(); } catch (NullPointerException e) { log.warn("Unable to get traderPubKeyRing from Dispute - {}", e.toString()); } return 0; } @Override public ObservableList getObservableChatMessageList() { return dispute != null ? dispute.getChatMessages() : FXCollections.observableArrayList(); } @Override public boolean chatIsOpen() { return dispute != null && dispute.isOpen(); } @Override public boolean isDisputeAgent() { return !isClient(); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/DisputeSummaryVerification.java ================================================ /* * 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 . */ package haveno.core.support.dispute; import haveno.common.crypto.Hash; import haveno.common.crypto.PubKeyRing; import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.core.support.dispute.agent.DisputeAgent; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.trade.HavenoUtils; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; import java.security.KeyPair; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class DisputeSummaryVerification { // Must not change as it is used for splitting the text for verifying the signature of the summary message private static final String SEPARATOR1 = "\n-----BEGIN SIGNATURE-----\n"; private static final String SEPARATOR2 = "\n-----END SIGNATURE-----\n"; public static String signAndApply(DisputeManager> disputeManager, DisputeResult disputeResult, String textToSign) { byte[] hash = Hash.getSha256Hash(textToSign); KeyPair signatureKeyPair = disputeManager.getSignatureKeyPair(); String sigAsHex; try { byte[] signature = HavenoUtils.sign(signatureKeyPair.getPrivate(), hash); sigAsHex = Utilities.encodeToHex(signature); disputeResult.setArbitratorSignature(signature); } catch (Exception e) { sigAsHex = "Signing failed"; } return Res.get("disputeSummaryWindow.close.msgWithSig", textToSign, SEPARATOR1, sigAsHex, SEPARATOR2); } public static void verifySignature(String input, ArbitratorManager arbitratorManager) { // get dispute agent DisputeAgent disputeAgent = null; try { String[] parts = input.split(SEPARATOR1); String textToSign = parts[0]; String fullAddress = textToSign.split("\n")[1].split(": ")[1]; NodeAddress nodeAddress = new NodeAddress(fullAddress); disputeAgent = arbitratorManager.getDisputeAgentByNodeAddress(nodeAddress).orElse(null); checkNotNull(disputeAgent, "Dispute agent is null"); } catch (Throwable e) { log.error("Error verifying signature: {}\n", e.getMessage(), e); throw new IllegalArgumentException(Res.get("support.sigCheck.popup.invalidFormat")); } // verify signature with pub key ring verifySignature(input, disputeAgent.getPubKeyRing()); } public static void verifySignature(String input, PubKeyRing agentPubKeyRing) { try { String[] parts = input.split(SEPARATOR1); String textToSign = parts[0]; String sigString = parts[1].split(SEPARATOR2)[0]; byte[] sig = Utilities.decodeFromHex(sigString); byte[] hash = Hash.getSha256Hash(textToSign); try { HavenoUtils.verifySignature(agentPubKeyRing, hash, sig); } catch (Exception e) { throw new IllegalArgumentException(Res.get("support.sigCheck.popup.failed")); } } catch (Throwable e) { log.error("Error verifying signature with agent pub key ring: {}\n", e.getMessage(), e); throw new IllegalArgumentException(Res.get("support.sigCheck.popup.invalidFormat")); } } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/DisputeValidation.java ================================================ /* * 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 . */ package haveno.core.support.dispute; import haveno.common.config.Config; import haveno.common.crypto.Hash; import haveno.core.trade.Contract; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.util.JsonUtil; import haveno.core.util.validation.RegexValidatorFactory; import haveno.network.p2p.NodeAddress; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.Address; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionOutput; import java.util.Arrays; import java.util.Objects; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class DisputeValidation { public static void validatePaymentAccountPayloads(Dispute dispute) throws ValidationException { if (dispute.getSellerPaymentAccountPayload() != null) { if (!Arrays.equals(dispute.getSellerPaymentAccountPayload().getHash(), dispute.getContract().getSellerPaymentAccountPayloadHash())) { throw new ValidationException(dispute, "Hash of seller's payment account payload does not match contract"); } } if (dispute.getBuyerPaymentAccountPayload() != null) { if (!Arrays.equals(dispute.getBuyerPaymentAccountPayload().getHash(), dispute.getContract().getBuyerPaymentAccountPayloadHash())) { throw new ValidationException(dispute, "Hash of buyer's payment account payload does not match contract"); } } } public static void validateDisputeData(Dispute dispute) throws ValidationException { try { Contract contract = dispute.getContract(); checkArgument(contract.getOfferPayload().getId().equals(dispute.getTradeId()), "Invalid tradeId"); checkArgument(dispute.getContractAsJson().equals(JsonUtil.objectToJson(contract)), "Invalid contractAsJson"); checkArgument(Arrays.equals(Objects.requireNonNull(dispute.getContractHash()), Hash.getSha256Hash(checkNotNull(dispute.getContractAsJson()))), "Invalid contractHash"); // Only the dispute opener has set the signature byte[] makerContractSignature = dispute.getMakerContractSignature(); if (makerContractSignature != null) HavenoUtils.verifySignature(contract.getMakerPubKeyRing(), dispute.getContractAsJson(), makerContractSignature); byte[] takerContractSignature = dispute.getTakerContractSignature(); if (takerContractSignature != null) HavenoUtils.verifySignature(contract.getTakerPubKeyRing(), dispute.getContractAsJson(), takerContractSignature); } catch (Throwable t) { throw new ValidationException(dispute, t.getMessage()); } } public static void validateTradeAndDispute(Dispute dispute, Trade trade) throws ValidationException { try { checkArgument(dispute.getContract().equals(trade.getContract()), "contract must match contract from trade"); } catch (Throwable t) { throw new ValidationException(dispute, t.getMessage()); } } public static void validateSenderNodeAddress(Dispute dispute, NodeAddress senderNodeAddress, Config config) throws NodeAddressException { if (config.useLocalhostForP2P) return; if (!senderNodeAddress.getHostName().equals(dispute.getContract().getBuyerNodeAddress().getHostName()) && !senderNodeAddress.getHostName().equals(dispute.getContract().getSellerNodeAddress().getHostName()) && !senderNodeAddress.getHostName().equals(dispute.getContract().getArbitratorNodeAddress().getHostName())) { throw new NodeAddressException(dispute, "senderNodeAddress not matching any of the trade node addresses"); } } public static void validateNodeAddresses(Dispute dispute, Config config) throws NodeAddressException { if (config.useLocalhostForP2P) return; validateNodeAddress(dispute, dispute.getContract().getBuyerNodeAddress()); validateNodeAddress(dispute, dispute.getContract().getSellerNodeAddress()); } private static void validateNodeAddress(Dispute dispute, NodeAddress nodeAddress) throws NodeAddressException { if (!RegexValidatorFactory.onionAddressRegexValidator().validate(nodeAddress.getFullAddress()).isValid) { String msg = "Node address " + nodeAddress.getFullAddress() + " at dispute with trade ID " + dispute.getShortTradeId() + " is not a valid address"; log.error(msg); throw new NodeAddressException(dispute, msg); } } public static void validateDonationAddress(Dispute dispute, Transaction delayedPayoutTx, NetworkParameters params) throws AddressException { TransactionOutput output = delayedPayoutTx.getOutput(0); Address address = output.getScriptPubKey().getToAddress(params); if (address == null) { String errorMsg = "Donation address cannot be resolved (not of type P2PK nor P2SH nor P2WH). Output: " + output; log.error(errorMsg); log.error(delayedPayoutTx.toString()); throw new DisputeValidation.AddressException(dispute, errorMsg); } // Verify that address in the dispute matches the one in the trade. String delayedPayoutTxOutputAddress = address.toString(); checkArgument(delayedPayoutTxOutputAddress.equals(dispute.getDonationAddressOfDelayedPayoutTx()), "donationAddressOfDelayedPayoutTx from dispute does not match address from delayed payout tx. " + "delayedPayoutTxOutputAddress=" + delayedPayoutTxOutputAddress + "; dispute.getDonationAddressOfDelayedPayoutTx()=" + dispute.getDonationAddressOfDelayedPayoutTx()); } /////////////////////////////////////////////////////////////////////////////////////////// // Exceptions /////////////////////////////////////////////////////////////////////////////////////////// public static class ValidationException extends Exception { @Getter private final Dispute dispute; ValidationException(Dispute dispute, String msg) { super(msg); this.dispute = dispute; } } public static class NodeAddressException extends ValidationException { NodeAddressException(Dispute dispute, String msg) { super(dispute, msg); } } public static class AddressException extends ValidationException { AddressException(Dispute dispute, String msg) { super(dispute, msg); } } public static class DisputeReplayException extends ValidationException { DisputeReplayException(Dispute dispute, String msg) { super(dispute, msg); } } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/agent/DisputeAgent.java ================================================ /* * 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 . */ package haveno.core.support.dispute.agent; import haveno.common.crypto.PubKeyRing; import haveno.common.util.ExtraDataMapValidator; import haveno.common.util.Utilities; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.storage.payload.ExpirablePayload; import haveno.network.p2p.storage.payload.ProtectedStoragePayload; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.security.PublicKey; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @EqualsAndHashCode @Slf4j @Getter public abstract class DisputeAgent implements ProtectedStoragePayload, ExpirablePayload { public static final long TTL = TimeUnit.DAYS.toMillis(7); protected final NodeAddress nodeAddress; protected final PubKeyRing pubKeyRing; protected final List languageCodes; protected final long registrationDate; protected final byte[] registrationPubKey; protected final String registrationSignature; @Nullable protected final String emailAddress; @Nullable protected final String info; // Should be only used in emergency case if we need to add data but do not want to break backward compatibility // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new // field in a class would break that hash and therefore break the storage mechanism. @Nullable protected Map extraDataMap; public DisputeAgent(NodeAddress nodeAddress, PubKeyRing pubKeyRing, List languageCodes, long registrationDate, byte[] registrationPubKey, String registrationSignature, @Nullable String emailAddress, @Nullable String info, @Nullable Map extraDataMap) { this.nodeAddress = nodeAddress; this.pubKeyRing = pubKeyRing; this.languageCodes = languageCodes; this.registrationDate = registrationDate; this.registrationPubKey = registrationPubKey; this.registrationSignature = registrationSignature; this.emailAddress = emailAddress; this.info = info; this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public long getTTL() { return TTL; } @Override public PublicKey getOwnerPubKey() { return pubKeyRing.getSignaturePubKey(); } @Override public String toString() { return "DisputeAgent{" + "\n nodeAddress=" + nodeAddress + ",\n pubKeyRing=" + pubKeyRing + ",\n languageCodes=" + languageCodes + ",\n registrationDate=" + registrationDate + ",\n registrationPubKey=" + Utilities.bytesAsHexString(registrationPubKey) + ",\n registrationSignature='" + registrationSignature + '\'' + ",\n emailAddress='" + emailAddress + '\'' + ",\n info='" + info + '\'' + ",\n extraDataMap=" + extraDataMap + "\n}"; } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/agent/DisputeAgentLookupMap.java ================================================ /* * 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 . */ package haveno.core.support.dispute.agent; import haveno.core.locale.Res; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; @Slf4j public class DisputeAgentLookupMap { // See also: https://haveno.exchange/wiki/Finding_your_mediator @Nullable public static String getMatrixUserName(String fullAddress) { if (fullAddress.matches("localhost(.*)")) { return fullAddress; // on regtest, agent displays as localhost } switch (fullAddress) { case "7hkpotiyaukuzcfy6faihjaols5r2mkysz7bm3wrhhbpbphzz3zbwyqd.onion:9999": return "leo816"; case "wizhavenozd7ku25di7p2ztsajioabihlnyp5lq5av66tmu7do2dke2tid.onion:9999": return "wiz"; case "apbp7ubuyezav4hy.onion:9999": return "haveno_knight"; case "a56olqlmmpxrn5q34itq5g5tb5d3fg7vxekpbceq7xqvfl3cieocgsyd.onion:9999": return "huey735"; case "3z5jnirlccgxzoxc6zwkcgwj66bugvqplzf6z2iyd5oxifiaorhnanqd.onion:9999": return "refundagent2"; case "6c4cim7h7t3bm4bnchbf727qrhdfrfr6lhod25wjtizm2sifpkktvwad.onion:9999": return "pazza83"; default: log.warn("No username for dispute agent with address {} found.", fullAddress); return Res.get("shared.na"); } } public static String getMatrixLinkForAgent(String onion) { // when a new mediator starts or an onion address changes, mediator name won't be known until // the table above is updated in the software. // as a stopgap measure, replace unknown ones with a link to the Haveno team String agentName = getMatrixUserName(onion).replaceAll(Res.get("shared.na"), "haveno"); return "https://matrix.to/#/@" + agentName + ":matrix.org"; } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/agent/DisputeAgentManager.java ================================================ /* * 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 . */ package haveno.core.support.dispute.agent; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.crypto.KeyRing; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.common.util.Utilities; import haveno.core.filter.FilterManager; import haveno.core.user.User; import haveno.network.p2p.BootstrapListener; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import haveno.network.p2p.storage.HashMapChangedListener; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import javafx.collections.FXCollections; import javafx.collections.ObservableMap; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.Utils; import javax.annotation.Nullable; import java.math.BigInteger; import java.security.PublicKey; import java.security.SignatureException; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.Collectors; import static org.bitcoinj.core.Utils.HEX; @Slf4j public abstract class DisputeAgentManager { /////////////////////////////////////////////////////////////////////////////////////////// // Static /////////////////////////////////////////////////////////////////////////////////////////// protected static final long REPUBLISH_MILLIS = DisputeAgent.TTL / 2; protected static final long RETRY_REPUBLISH_SEC = 5; protected static final long REPEATED_REPUBLISH_AT_STARTUP_SEC = 60; protected final List publicKeys; /////////////////////////////////////////////////////////////////////////////////////////// // Instance fields /////////////////////////////////////////////////////////////////////////////////////////// protected final KeyRing keyRing; protected final DisputeAgentService disputeAgentService; protected final User user; protected final FilterManager filterManager; protected final ObservableMap observableMap = FXCollections.observableHashMap(); protected List persistedAcceptedDisputeAgents; protected Timer republishTimer, retryRepublishTimer; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public DisputeAgentManager(KeyRing keyRing, DisputeAgentService disputeAgentService, User user, FilterManager filterManager) { this.keyRing = keyRing; this.disputeAgentService = disputeAgentService; this.user = user; this.filterManager = filterManager; publicKeys = getPubKeyList(); } /////////////////////////////////////////////////////////////////////////////////////////// // Abstract methods /////////////////////////////////////////////////////////////////////////////////////////// protected abstract List getPubKeyList(); protected abstract boolean isExpectedInstance(ProtectedStorageEntry data); protected abstract void addAcceptedDisputeAgentToUser(T disputeAgent); protected abstract T getRegisteredDisputeAgentFromUser(); protected abstract void clearAcceptedDisputeAgentsAtUser(); protected abstract List getAcceptedDisputeAgentsFromUser(); protected abstract void removeAcceptedDisputeAgentFromUser(ProtectedStorageEntry data); protected abstract void setRegisteredDisputeAgentAtUser(T disputeAgent); /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void onAllServicesInitialized() { disputeAgentService.addHashSetChangedListener(new HashMapChangedListener() { @Override public void onAdded(Collection protectedStorageEntries) { protectedStorageEntries.forEach(protectedStorageEntry -> { if (isExpectedInstance(protectedStorageEntry)) { updateMap(); } }); } @Override public void onRemoved(Collection protectedStorageEntries) { protectedStorageEntries.forEach(protectedStorageEntry -> { if (isExpectedInstance(protectedStorageEntry)) { updateMap(); removeAcceptedDisputeAgentFromUser(protectedStorageEntry); } }); } }); persistedAcceptedDisputeAgents = new ArrayList<>(getAcceptedDisputeAgentsFromUser()); clearAcceptedDisputeAgentsAtUser(); if (getRegisteredDisputeAgentFromUser() != null) { P2PService p2PService = disputeAgentService.getP2PService(); if (p2PService.isBootstrapped()) startRepublishDisputeAgent(); else p2PService.addP2PServiceListener(new BootstrapListener() { @Override public void onDataReceived() { startRepublishDisputeAgent(); } }); } filterManager.filterProperty().addListener((observable, oldValue, newValue) -> updateMap()); updateMap(); } public void shutDown() { stopRepublishTimer(); stopRetryRepublishTimer(); } protected void startRepublishDisputeAgent() { if (republishTimer == null) { republishTimer = UserThread.runPeriodically(this::republish, REPUBLISH_MILLIS, TimeUnit.MILLISECONDS); UserThread.runAfter(this::republish, REPEATED_REPUBLISH_AT_STARTUP_SEC); republish(); } } public void updateMap() { Map map = disputeAgentService.getDisputeAgents(); observableMap.clear(); Map filtered = map.values().stream() .filter(e -> { String pubKeyAsHex = Utils.HEX.encode(e.getRegistrationPubKey()); boolean isInPublicKeyInList = isPublicKeyInList(pubKeyAsHex); if (!isInPublicKeyInList) { if (DevEnv.DEV_PRIVILEGE_PUB_KEY.equals(pubKeyAsHex)) log.info("We got the DEV_PRIVILEGE_PUB_KEY in our list of publicKeys. RegistrationPubKey={}, nodeAddress={}", Utilities.bytesAsHexString(e.getRegistrationPubKey()), e.getNodeAddress().getFullAddress()); else log.warn("We got an disputeAgent which is not in our list of publicKeys. RegistrationPubKey={}, nodeAddress={}", Utilities.bytesAsHexString(e.getRegistrationPubKey()), e.getNodeAddress().getFullAddress()); } final boolean isSigValid = verifySignature(e.getPubKeyRing().getSignaturePubKey(), e.getRegistrationPubKey(), e.getRegistrationSignature()); if (!isSigValid) log.warn("Sig check for disputeAgent failed. DisputeAgent={}", e.toString()); return isInPublicKeyInList && isSigValid; }) .collect(Collectors.toMap(DisputeAgent::getNodeAddress, Function.identity())); observableMap.putAll(filtered); observableMap.values().forEach(this::addAcceptedDisputeAgentToUser); } public void addDisputeAgent(T disputeAgent, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { setRegisteredDisputeAgentAtUser(disputeAgent); observableMap.put(disputeAgent.getNodeAddress(), disputeAgent); disputeAgentService.addDisputeAgent(disputeAgent, () -> { log.info("DisputeAgent successfully saved in P2P network"); resultHandler.handleResult(); if (observableMap.size() > 0) UserThread.runAfter(this::updateMap, 100, TimeUnit.MILLISECONDS); }, errorMessageHandler); } public void removeDisputeAgent(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { T registeredDisputeAgent = getRegisteredDisputeAgentFromUser(); if (registeredDisputeAgent != null) { setRegisteredDisputeAgentAtUser(null); observableMap.remove(registeredDisputeAgent.getNodeAddress()); disputeAgentService.removeDisputeAgent(registeredDisputeAgent, () -> { log.debug("DisputeAgent successfully removed from P2P network"); resultHandler.handleResult(); }, errorMessageHandler); } else { errorMessageHandler.handleErrorMessage("User is not registered dispute agent"); } } public ObservableMap getObservableMap() { return observableMap; } // A protected key is handed over to selected disputeAgents for registration. // An invited disputeAgent will sign at registration his storageSignaturePubKey with that protected key and attach the signature and pubKey to his data. // Other users will check the signature with the list of public keys hardcoded in the app. public String signStorageSignaturePubKey(ECKey key) { String keyToSignAsHex = Utils.HEX.encode(keyRing.getPubKeyRing().getSignaturePubKey().getEncoded()); return key.signMessage(keyToSignAsHex); } @Nullable public ECKey getRegistrationKey(String privKeyBigIntString) { try { return ECKey.fromPrivate(new BigInteger(1, HEX.decode(privKeyBigIntString))); } catch (Throwable t) { return null; } } public boolean isPublicKeyInList(String pubKeyAsHex) { return publicKeys.contains(pubKeyAsHex); } public boolean isAgentAvailableForLanguage(String languageCode) { return observableMap.values().stream().anyMatch(agent -> agent.getLanguageCodes().stream().anyMatch(lc -> lc.equals(languageCode))); } public List getDisputeAgentLanguages(List nodeAddresses) { return observableMap.values().stream() .filter(disputeAgent -> nodeAddresses.stream().anyMatch(nodeAddress -> nodeAddress.equals(disputeAgent.getNodeAddress()))) .flatMap(disputeAgent -> disputeAgent.getLanguageCodes().stream()) .distinct() .collect(Collectors.toList()); } public Optional getDisputeAgentByNodeAddress(NodeAddress nodeAddress) { return observableMap.containsKey(nodeAddress) ? Optional.of(observableMap.get(nodeAddress)) : Optional.empty(); } /////////////////////////////////////////////////////////////////////////////////////////// // protected /////////////////////////////////////////////////////////////////////////////////////////// protected void republish() { T registeredDisputeAgent = getRegisteredDisputeAgentFromUser(); if (registeredDisputeAgent != null) { addDisputeAgent(registeredDisputeAgent, this::updateMap, errorMessage -> { if (retryRepublishTimer == null) retryRepublishTimer = UserThread.runPeriodically(() -> { stopRetryRepublishTimer(); republish(); }, RETRY_REPUBLISH_SEC); } ); } } protected boolean verifySignature(PublicKey storageSignaturePubKey, byte[] registrationPubKey, String signature) { String keyToSignAsHex = Utils.HEX.encode(storageSignaturePubKey.getEncoded()); try { ECKey key = ECKey.fromPublicOnly(registrationPubKey); key.verifyMessage(keyToSignAsHex, signature); return true; } catch (SignatureException e) { log.warn("verifySignature failed"); return false; } } protected void stopRetryRepublishTimer() { if (retryRepublishTimer != null) { retryRepublishTimer.stop(); retryRepublishTimer = null; } } protected void stopRepublishTimer() { if (republishTimer != null) { republishTimer.stop(); republishTimer = null; } } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/agent/DisputeAgentService.java ================================================ /* * 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 . */ package haveno.core.support.dispute.agent; import haveno.common.app.DevEnv; import haveno.common.config.Config; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.common.util.Utilities; import haveno.core.filter.FilterManager; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import haveno.network.p2p.storage.HashMapChangedListener; import lombok.extern.slf4j.Slf4j; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * Used to store disputeAgents profile and load map of disputeAgents */ @Slf4j public abstract class DisputeAgentService { protected final P2PService p2PService; protected final FilterManager filterManager; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public DisputeAgentService(P2PService p2PService, FilterManager filterManager) { this.p2PService = p2PService; this.filterManager = filterManager; } public void addHashSetChangedListener(HashMapChangedListener hashMapChangedListener) { p2PService.addHashSetChangedListener(hashMapChangedListener); } public void addDisputeAgent(T disputeAgent, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { log.debug("addDisputeAgent disputeAgent.hashCode() " + disputeAgent.hashCode()); if (!Config.baseCurrencyNetwork().isMainnet() || !Utilities.encodeToHex(disputeAgent.getRegistrationPubKey()).equals(DevEnv.DEV_PRIVILEGE_PUB_KEY)) { boolean result = p2PService.addProtectedStorageEntry(disputeAgent); if (result) { log.trace("Add disputeAgent to network was successful. DisputeAgent.hashCode() = {}", disputeAgent.hashCode()); resultHandler.handleResult(); } else { errorMessageHandler.handleErrorMessage("Add disputeAgent failed"); } } else { log.error("Attempt to publish dev disputeAgent on mainnet."); errorMessageHandler.handleErrorMessage("Add disputeAgent failed. Attempt to publish dev disputeAgent on mainnet."); } } public void removeDisputeAgent(T disputeAgent, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { log.debug("removeDisputeAgent disputeAgent.hashCode() " + disputeAgent.hashCode()); if (p2PService.removeData(disputeAgent)) { log.trace("Remove disputeAgent from network was successful. DisputeAgent.hashCode() = {}", disputeAgent.hashCode()); resultHandler.handleResult(); } else { errorMessageHandler.handleErrorMessage("Remove disputeAgent failed"); } } public P2PService getP2PService() { return p2PService; } public Map getDisputeAgents() { final List bannedDisputeAgents; if (filterManager.getFilter() != null) { bannedDisputeAgents = getDisputeAgentsFromFilter(); } else { bannedDisputeAgents = null; } if (bannedDisputeAgents != null && !bannedDisputeAgents.isEmpty()) { log.warn("bannedDisputeAgents=" + bannedDisputeAgents); } Set disputeAgentSet = getDisputeAgentSet(bannedDisputeAgents); Map map = new HashMap<>(); for (T disputeAgent : disputeAgentSet) { NodeAddress disputeAgentNodeAddress = disputeAgent.getNodeAddress(); if (map.containsKey(disputeAgentNodeAddress)) log.warn("disputeAgentAddress already exists in disputeAgent map. Seems a disputeAgent object is already registered with the same address."); map.put(disputeAgentNodeAddress, disputeAgent); } return map; } protected abstract Set getDisputeAgentSet(List bannedDisputeAgents); protected abstract List getDisputeAgentsFromFilter(); } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/agent/MultipleHolderNameDetection.java ================================================ /* * 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 . */ package haveno.core.support.dispute.agent; import haveno.common.crypto.Hash; import haveno.common.crypto.PubKeyRing; import haveno.common.util.Tuple2; import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.core.payment.payload.PayloadWithHolderName; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeList; import haveno.core.support.dispute.DisputeManager; import haveno.core.support.dispute.DisputeResult; import haveno.core.user.DontShowAgainLookup; import javafx.collections.ListChangeListener; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.stream.Collectors; /** * Detects traders who had disputes where they used different account holder names. Only payment methods where a * real name is required are used for the check. * Strings are not translated here as it is only visible to dispute agents */ @Slf4j public class MultipleHolderNameDetection { /////////////////////////////////////////////////////////////////////////////////////////// // Listener /////////////////////////////////////////////////////////////////////////////////////////// public interface Listener { void onSuspiciousDisputeDetected(); } /////////////////////////////////////////////////////////////////////////////////////////// // Static /////////////////////////////////////////////////////////////////////////////////////////// private static final String ACK_KEY = "Ack-"; private static String getSigPuKeyHashAsHex(PubKeyRing pubKeyRing) { return Utilities.encodeToHex(Hash.getRipemd160hash(pubKeyRing.getSignaturePubKeyBytes())); } private static String getSigPubKeyHashAsHex(Dispute dispute) { return getSigPuKeyHashAsHex(dispute.getTraderPubKeyRing()); } private static boolean isBuyer(Dispute dispute) { String traderSigPubKeyHashAsHex = getSigPubKeyHashAsHex(dispute); String buyerSigPubKeyHashAsHex = getSigPuKeyHashAsHex(dispute.getContract().getBuyerPubKeyRing()); return buyerSigPubKeyHashAsHex.equals(traderSigPubKeyHashAsHex); } private static PayloadWithHolderName getPayloadWithHolderName(Dispute dispute) { return (PayloadWithHolderName) getPaymentAccountPayload(dispute); } public static PaymentAccountPayload getPaymentAccountPayload(Dispute dispute) { return isBuyer(dispute) ? dispute.getBuyerPaymentAccountPayload() : dispute.getSellerPaymentAccountPayload(); } public static String getAddress(Dispute dispute) { return isBuyer(dispute) ? dispute.getContract().getBuyerNodeAddress().getHostName() : dispute.getContract().getSellerNodeAddress().getHostName(); } public static String getAckKey(Dispute dispute) { return ACK_KEY + getSigPubKeyHashAsHex(dispute).substring(0, 4) + "/" + dispute.getShortTradeId(); } private static String getIsBuyerSubString(boolean isBuyer) { return "'\n Role: " + (isBuyer ? "'Buyer'" : "'Seller'"); } /////////////////////////////////////////////////////////////////////////////////////////// // Class fields /////////////////////////////////////////////////////////////////////////////////////////// private final DisputeManager> disputeManager; // Key is hex of hash of sig pubKey which we consider a trader identity. We could use onion address as well but // once we support multiple onion addresses that would not work anymore. @Getter private final Map> suspiciousDisputesByTraderMap = new HashMap<>(); private final List listeners = new CopyOnWriteArrayList<>(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public MultipleHolderNameDetection(DisputeManager> disputeManager) { this.disputeManager = disputeManager; disputeManager.getDisputesAsObservableList().addListener((ListChangeListener) c -> { c.next(); if (c.wasAdded()) { detectMultipleHolderNames(); } }); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void detectMultipleHolderNames() { String previous = suspiciousDisputesByTraderMap.toString(); getAllDisputesByTraderMap().forEach((key, value) -> { Set userNames = value.stream() .map(dispute -> getPayloadWithHolderName(dispute).getHolderName()) .collect(Collectors.toSet()); if (userNames.size() > 1) { // As we compare previous results we need to make sorting deterministic value.sort(Comparator.comparing(Dispute::getId)); suspiciousDisputesByTraderMap.put(key, value); } }); String updated = suspiciousDisputesByTraderMap.toString(); if (!previous.equals(updated)) { listeners.forEach(Listener::onSuspiciousDisputeDetected); } } public boolean hasSuspiciousDisputesDetected() { return !suspiciousDisputesByTraderMap.isEmpty(); } // Returns all disputes of a trader who used multiple names public List getDisputesForTrader(Dispute dispute) { String traderPubKeyHash = getSigPubKeyHashAsHex(dispute); if (suspiciousDisputesByTraderMap.containsKey(traderPubKeyHash)) { return suspiciousDisputesByTraderMap.get(traderPubKeyHash); } return new ArrayList<>(); } // Get a report of traders who used multiple names with all their disputes listed public String getReportForAllDisputes() { return getReport(suspiciousDisputesByTraderMap.values()); } // Get a report for a trader who used multiple names with all their disputes listed public String getReportForDisputeOfTrader(List disputes) { Collection> values = new ArrayList<>(); values.add(disputes); return getReport(values); } public void addListener(Listener listener) { listeners.add(listener); } public void removeListener(Listener listener) { listeners.remove(listener); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private Map> getAllDisputesByTraderMap() { Map> allDisputesByTraderMap = new HashMap<>(); disputeManager.getDisputesAsObservableList().stream() .filter(dispute -> { PaymentAccountPayload paymentAccountPayload = isBuyer(dispute) ? dispute.getBuyerPaymentAccountPayload() : dispute.getSellerPaymentAccountPayload(); return paymentAccountPayload instanceof PayloadWithHolderName; }) .forEach(dispute -> { String traderPubKeyHash = getSigPubKeyHashAsHex(dispute); allDisputesByTraderMap.putIfAbsent(traderPubKeyHash, new ArrayList<>()); List disputes = allDisputesByTraderMap.get(traderPubKeyHash); disputes.add(dispute); }); return allDisputesByTraderMap; } // Get a text report for a trader who used multiple names and list all the his disputes private String getReport(Collection> collectionOfDisputesOfTrader) { return collectionOfDisputesOfTrader.stream() .map(disputes -> { Set addresses = new HashSet<>(); Set isBuyerHashSet = new HashSet<>(); Set names = new HashSet<>(); String disputesReport = disputes.stream() .map(dispute -> { addresses.add(getAddress(dispute)); String ackKey = getAckKey(dispute); String ackSubString = " "; if (!DontShowAgainLookup.showAgain(ackKey)) { ackSubString = "[ACK] "; } String holderName = getPayloadWithHolderName(dispute).getHolderName(); names.add(holderName); boolean isBuyer = isBuyer(dispute); isBuyerHashSet.add(isBuyer); String isBuyerSubString = getIsBuyerSubString(isBuyer); DisputeResult disputeResult = dispute.disputeResultProperty().get(); String summaryNotes = disputeResult != null ? disputeResult.getSummaryNotesProperty().get().trim() : "Not closed yet"; return ackSubString + "Trade ID: '" + dispute.getShortTradeId() + "'\n Account holder name: '" + holderName + "'\n Payment method: '" + Res.get(getPaymentAccountPayload(dispute).getPaymentMethodId()) + isBuyerSubString + "'\n Summary: '" + summaryNotes; }) .collect(Collectors.joining("\n")); String addressSubString = addresses.size() > 1 ? "used multiple addresses " + addresses + " with" : "with address " + new ArrayList<>(addresses).get(0) + " used"; String roleSubString = "Trader "; if (isBuyerHashSet.size() == 1) { boolean isBuyer = new ArrayList<>(isBuyerHashSet).get(0); String isBuyerSubString = getIsBuyerSubString(isBuyer); disputesReport = disputesReport.replace(isBuyerSubString, ""); roleSubString = isBuyer ? "Buyer " : "Seller "; } String traderReport = roleSubString + addressSubString + " multiple names: " + names.toString() + "\n" + disputesReport; return new Tuple2<>(roleSubString, traderReport); }) .sorted(Comparator.comparing(o -> o.first)) // Buyers first, then seller, then mixed (trader was in seller and buyer role) .map(e -> e.second) .collect(Collectors.joining("\n\n")); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationDisputeList.java ================================================ /* * 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 . */ package haveno.core.support.dispute.arbitration; import com.google.protobuf.Message; import haveno.common.proto.ProtoUtil; import haveno.core.proto.CoreProtoResolver; import haveno.core.support.SupportType; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeList; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkArgument; @Slf4j @ToString /* * Holds a List of arbitration dispute objects. * * Calls to the List are delegated because this class intercepts the add/remove calls so changes * can be saved to disc. */ public final class ArbitrationDisputeList extends DisputeList { ArbitrationDisputeList() { super(); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// protected ArbitrationDisputeList(Collection collection) { super(collection); } @Override public Message toProtoMessage() { synchronized (this.list) { forEach(dispute -> checkArgument(dispute.getSupportType().equals(SupportType.ARBITRATION), "Support type has to be ARBITRATION")); return protobuf.PersistableEnvelope.newBuilder().setArbitrationDisputeList(protobuf.ArbitrationDisputeList.newBuilder() .addAllDispute(ProtoUtil.collectionToProto(getList(), protobuf.Dispute.class))).build(); } } public static ArbitrationDisputeList fromProto(protobuf.ArbitrationDisputeList proto, CoreProtoResolver coreProtoResolver) { List list = proto.getDisputeList().stream() .map(disputeProto -> Dispute.fromProto(disputeProto, coreProtoResolver)) .filter(e -> e.getSupportType().equals(SupportType.ARBITRATION)) .collect(Collectors.toList()); return new ArbitrationDisputeList(list); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationDisputeListService.java ================================================ /* * 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 . */ package haveno.core.support.dispute.arbitration; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.persistence.PersistenceManager; import haveno.core.support.dispute.DisputeListService; @Singleton public final class ArbitrationDisputeListService extends DisputeListService { /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public ArbitrationDisputeListService(PersistenceManager persistenceManager) { super(persistenceManager); } /////////////////////////////////////////////////////////////////////////////////////////// // Implement template methods /////////////////////////////////////////////////////////////////////////////////////////// @Override protected ArbitrationDisputeList getConcreteDisputeList() { return new ArbitrationDisputeList(); } @Override protected String getFileName() { return "DisputeList"; } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.support.dispute.arbitration; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.ThreadUtils; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.app.Version; import haveno.common.config.Config; import haveno.common.crypto.KeyRing; import haveno.common.proto.network.NetworkEnvelope; import haveno.core.api.XmrConnectionService; import haveno.core.api.CoreNotificationService; import haveno.core.locale.Res; import haveno.core.offer.OpenOfferManager; import haveno.core.provider.price.PriceFeedService; import haveno.core.support.SupportType; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeManager; import haveno.core.support.dispute.DisputeResult; import haveno.core.support.dispute.DisputeSummaryVerification; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.support.dispute.mediation.FileTransferReceiver; import haveno.core.support.dispute.mediation.FileTransferSender; import haveno.core.support.dispute.mediation.FileTransferSession; import haveno.core.support.dispute.messages.DisputeClosedMessage; import haveno.core.support.dispute.messages.DisputeOpenedMessage; import haveno.core.support.messages.ChatMessage; import haveno.core.support.messages.SupportMessage; import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.trade.Trade.DisputeState; import haveno.core.xmr.wallet.TradeWalletService; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.AckMessageSourceType; import haveno.network.p2p.FileTransferPart; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.MessageListener; import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.CountDownLatch; import org.apache.commons.lang3.exception.ExceptionUtils; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j @Singleton public final class ArbitrationManager extends DisputeManager implements MessageListener, FileTransferSession.FtpCallback { private final ArbitratorManager arbitratorManager; private Map reprocessDisputeClosedMessageCounts = new HashMap<>(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public ArbitrationManager(P2PService p2PService, TradeWalletService tradeWalletService, XmrWalletService walletService, XmrConnectionService xmrConnectionService, CoreNotificationService notificationService, ArbitratorManager arbitratorManager, TradeManager tradeManager, ClosedTradableManager closedTradableManager, OpenOfferManager openOfferManager, KeyRing keyRing, ArbitrationDisputeListService arbitrationDisputeListService, Config config, PriceFeedService priceFeedService) { super(p2PService, tradeWalletService, walletService, xmrConnectionService, notificationService, tradeManager, closedTradableManager, openOfferManager, keyRing, arbitrationDisputeListService, config, priceFeedService); this.arbitratorManager = arbitratorManager; HavenoUtils.arbitrationManager = this; // TODO: storing static reference, better way? p2PService.getNetworkNode().addMessageListener(this); // listening for FileTransferPart message } /////////////////////////////////////////////////////////////////////////////////////////// // Implement template methods /////////////////////////////////////////////////////////////////////////////////////////// @Override public SupportType getSupportType() { return SupportType.ARBITRATION; } @Override public void onSupportMessage(SupportMessage message) { if (canProcessMessage(message)) { log.info("Received {} from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), message.getSenderNodeAddress(), message.getTradeId(), message.getUid()); if (message instanceof DisputeOpenedMessage) { handle((DisputeOpenedMessage) message); } else if (message instanceof ChatMessage) { handle((ChatMessage) message); } else if (message instanceof DisputeClosedMessage) { handle((DisputeClosedMessage) message); } else { log.warn("Unsupported message at dispatchMessage. message={}", message); } } } @Override public NodeAddress getAgentNodeAddress(Dispute dispute) { return dispute.getContract().getArbitratorNodeAddress(); } @Override protected AckMessageSourceType getAckMessageSourceType() { return AckMessageSourceType.ARBITRATION_MESSAGE; } @Override public void cleanupDisputes() { List disputes = getDisputeList().getList(); synchronized (disputes) { // collect disputes to remove Set toRemoves = new HashSet<>(); for (Dispute dispute : disputes) { // get dispute's trade final Trade trade = tradeManager.getTrade(dispute.getTradeId()); if (trade == null) { log.warn("Dispute trade {} does not exist", dispute.getTradeId()); return; } // remove dispute if owned by arbitrator if (dispute.getTraderPubKeyRing().equals(trade.getArbitrator().getPubKeyRing())) { log.warn("Removing invalid dispute opened by arbitrator, disputeId={}", trade.getId(), dispute.getId()); toRemoves.add(dispute); } // remove dispute if preparing if (trade.getDisputeState() == DisputeState.DISPUTE_PREPARING) { log.warn("Removing dispute for {} {} with disputeState={}, disputeId={}", trade.getClass().getSimpleName(), trade.getId(), trade.getDisputeState(), dispute.getId()); toRemoves.add(dispute); } // remove dispute if requested and not stored in mailbox if (trade.getDisputeState() == DisputeState.DISPUTE_REQUESTED) { boolean storedInMailbox = false; for (ChatMessage msg : dispute.getChatMessages()) { if (Boolean.TRUE.equals(msg.getStoredInMailboxProperty().get())) { storedInMailbox = true; log.info("Keeping dispute for {} {} with disputeState={}, disputeId={}. Stored in mailbox", trade.getClass().getSimpleName(), trade.getId(), trade.getDisputeState(), dispute.getId()); break; } } if (!storedInMailbox) { log.warn("Removing dispute for {} {} with disputeState={}, disputeId={}. Not stored in mailbox", trade.getClass().getSimpleName(), trade.getId(), trade.getDisputeState(), dispute.getId()); toRemoves.add(dispute); } } } // remove disputes and reset state for (Dispute dispute : toRemoves) { getDisputeList().remove(dispute); // get dispute's trade final Trade trade = tradeManager.getTrade(dispute.getTradeId()); if (trade == null) { log.warn("Dispute trade {} does not exist", dispute.getTradeId()); continue; } trade.setDisputeState(DisputeState.NO_DISPUTE); } // close open disputes with published payout for (Dispute dispute : disputes) { // skip if dispute is closed if (dispute.isClosed()) continue; // get dispute's trade final Trade trade = tradeManager.getTrade(dispute.getTradeId()); if (trade == null) { log.warn("Dispute trade {} does not exist", dispute.getTradeId()); continue; } // skip if trade's payout is not published if (!trade.isPayoutPublished()) continue; // skip if arbitrator's peer dispute is closed Optional peersDisputeOptional = null; if (trade.isArbitrator()) { peersDisputeOptional = getDisputesAsObservableList().stream() .filter(d -> dispute.getTradeId().equals(d.getTradeId()) && dispute.getTraderId() != d.getTraderId()) .findFirst(); if (peersDisputeOptional.isPresent()) { if (peersDisputeOptional.get().isClosed()) continue; } else { log.warn("No peer dispute found for disputeId={}, tradeId={}", dispute.getId(), dispute.getTradeId()); continue; } } // close trade disputes if payout published log.warn("Auto-closing dispute for {} {} with published payout, disputeId={}", trade.getClass().getSimpleName(), trade.getId(), dispute.getId()); dispute.setIsClosed(); if (peersDisputeOptional != null && peersDisputeOptional.isPresent()) peersDisputeOptional.get().setIsClosed(); trade.setDisputeState(Trade.DisputeState.DISPUTE_CLOSED); } } } @Override protected String getDisputeInfo(Dispute dispute) { String role = Res.get("shared.arbitrator").toLowerCase(); String link = "https://docs.haveno.exchange/trading-rules.html#legacy-arbitration"; return Res.get("support.initialInfo", role, role, link); } @Override protected String getDisputeIntroForPeer(String disputeInfo) { return Res.get("support.peerOpenedDispute", disputeInfo, Version.VERSION); } @Override protected String getDisputeIntroForDisputeCreator(String disputeInfo) { return Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION); } @Override protected void addPriceInfoMessage(Dispute dispute, int counter) { // Arbitrator is not used anymore. } /////////////////////////////////////////////////////////////////////////////////////////// // Dispute handling /////////////////////////////////////////////////////////////////////////////////////////// // received by both peers when arbitrator closes disputes @Override public void handle(DisputeClosedMessage disputeClosedMessage) { handle(disputeClosedMessage, true); } public void handle(DisputeClosedMessage disputeClosedMessage, boolean reprocessOnError) { // get dispute's trade final Trade trade = tradeManager.getTrade(disputeClosedMessage.getTradeId()); if (trade == null) { log.warn("Dispute trade {} does not exist", disputeClosedMessage.getTradeId()); return; } // set dispute closed message for reprocessing trade.getArbitrator().setDisputeClosedMessage(disputeClosedMessage); // process on initialization thread after delay to get latest message ThreadUtils.execute(() -> { // get latest message HavenoUtils.waitFor(100); if (disputeClosedMessage != trade.getArbitrator().getDisputeClosedMessage()) { log.info("Ignoring DisputeClosedMessage because a newer message was received for {} {}", trade.getClass().getSimpleName(), trade.getId()); return; } // persist trade and return when processing on trade thread CountDownLatch initLatch = new CountDownLatch(1); trade.persistNow(() -> { // try to process dispute closed message ThreadUtils.execute(() -> { initLatch.countDown(); ChatMessage chatMessage = null; Dispute dispute = null; synchronized (trade.getLock()) { try { DisputeResult disputeResult = disputeClosedMessage.getDisputeResult(); chatMessage = disputeResult.getChatMessage(); checkNotNull(chatMessage, "chatMessage must not be null"); String tradeId = disputeResult.getTradeId(); log.info("Processing {} for {} {}", disputeClosedMessage.getClass().getSimpleName(), trade.getClass().getSimpleName(), disputeResult.getTradeId()); // get dispute Optional disputeOptional = findDispute(disputeResult); String uid = disputeClosedMessage.getUid(); if (!disputeOptional.isPresent()) { log.warn("We got a dispute closed msg but we don't have a matching dispute. " + "That might happen when we get the DisputeClosedMessage before the dispute was created. " + "We try again after 2 sec. to apply the DisputeClosedMessage. TradeId = " + tradeId); if (!delayMsgMap.containsKey(uid)) { // We delay 2 sec. to be sure the comm. msg gets added first Timer timer = UserThread.runAfter(() -> handle(disputeClosedMessage), 2); delayMsgMap.put(uid, timer); } else { log.warn("We got a dispute closed msg after we already repeated to apply the message after a delay. " + "That should never happen. TradeId = " + tradeId); } return; } dispute = disputeOptional.get(); // verify arbitrator signature String summaryText = chatMessage.getMessage(); if (summaryText == null || summaryText.isEmpty()) throw new IllegalArgumentException("Summary text for dispute is missing, tradeId=" + tradeId + (dispute == null ? "" : ", disputeId=" + dispute.getId())); if (dispute != null) DisputeSummaryVerification.verifySignature(summaryText, dispute.getAgentPubKeyRing()); // use dispute's arbitrator pub key ring else DisputeSummaryVerification.verifySignature(summaryText, arbitratorManager); // verify using registered arbitrator (will fail if arbitrator is unregistered) // verify arbitrator does not receive DisputeClosedMessage if (keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing())) { log.error("Arbitrator received disputeResultMessage. That should never happen."); trade.getArbitrator().setDisputeClosedMessage(null); // don't reprocess return; } // set dispute state cleanupRetryMap(uid); synchronized (dispute.getChatMessages()) { if (!dispute.getChatMessages().contains(chatMessage)) { dispute.addAndPersistChatMessage(chatMessage); } else { log.warn("We got a dispute mail msg that we have already stored. TradeId = " + chatMessage.getTradeId()); } } dispute.setIsClosed(); if (dispute.disputeResultProperty().get() != null) { log.info("We already got a dispute result, indicating the message was resent after updating multisig info. TradeId = " + tradeId); } dispute.setDisputeResult(disputeResult); // update multisig hex if (disputeClosedMessage.getUpdatedMultisigHex() != null) trade.getArbitrator().setUpdatedMultisigHex(disputeClosedMessage.getUpdatedMultisigHex()); if (trade.walletExists()) trade.importMultisigHex(); // sync and save wallet if (!trade.isPayoutPublished()) trade.syncAndPollWallet(); // attempt to sign and publish dispute payout tx if given and not already published if (!trade.isPayoutPublished() && disputeClosedMessage.getUnsignedPayoutTxHex() != null) { // wait to sign and publish payout tx if defer flag set if (disputeClosedMessage.isDeferPublishPayout()) { log.info("Deferring signing and publishing dispute payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId()); trade.pollWalletNormallyForMs(Trade.POLL_WALLET_NORMALLY_DEFAULT_PERIOD_MS); // override idling for (int i = 0; i < 5; i++) { if (trade.isPayoutPublished()) break; HavenoUtils.waitFor(Trade.DEFER_PUBLISH_MS / 5); } if (!trade.isPayoutPublished()) trade.syncAndPollWallet(); } // sign and publish dispute payout tx if peer still has not published if (trade.isPayoutPublished()) { log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId()); } else { try { log.info("Signing and publishing dispute payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId()); trade.processDisputePayoutTx(); } catch (Exception e) { // check if payout published again trade.syncAndPollWallet(); if (trade.isPayoutPublished()) { log.warn("Payout tx already published for {} {}, skipping dispute processing", trade.getClass().getSimpleName(), trade.getId()); } else { if (e instanceof IllegalArgumentException || e instanceof IllegalStateException) throw e; else throw new RuntimeException("Failed to sign and publish dispute payout tx from arbitrator for " + trade.getClass().getSimpleName() + " " + tradeId + ": " + e.getMessage(), e); } } } } else { if (trade.isPayoutPublished()) log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId()); else if (disputeClosedMessage.getUnsignedPayoutTxHex() == null) log.info("{} did not receive unsigned dispute payout tx for trade {} because the arbitrator did not have their updated multisig info (can happen if trader went offline after trade started)", trade.getClass().getSimpleName(), trade.getId()); } // complete disputed trade if (trade.isPayoutPublished()) { tradeManager.closeDisputedTrade(trade.getId(), Trade.DisputeState.DISPUTE_CLOSED); } // We use the chatMessage as we only persist those not the DisputeClosedMessage. // If we would use the DisputeClosedMessage we could not lookup for the msg when we receive the AckMessage. sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null); requestPersistence(trade); } catch (Exception e) { log.warn("Error processing dispute closed message: {}", e.getMessage()); log.warn(ExceptionUtils.getStackTrace(e)); requestPersistence(trade); // nack bad message and do not reprocess if (HavenoUtils.isIllegal(e)) { trade.setPayoutTxHex(null); // clear signed payout tx hex trade.getArbitrator().setDisputeClosedMessage(null); // message is processed trade.setDisputeState(Trade.DisputeState.DISPUTE_CLOSED); String warningMsg = "Error processing dispute closed message: " + e.getMessage() + "\n\nOpen another dispute to try again (ctrl+o)."; trade.prependErrorMessage(warningMsg); sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), false, e.getMessage()); HavenoUtils.havenoSetup.getTopErrorMsg().set(warningMsg); requestPersistence(trade); throw e; } // schedule to reprocess message unless deleted if (trade.getArbitrator().getDisputeClosedMessage() != null && reprocessOnError) { if (!reprocessDisputeClosedMessageCounts.containsKey(trade.getId())) reprocessDisputeClosedMessageCounts.put(trade.getId(), 0); UserThread.runAfter(() -> { reprocessDisputeClosedMessageCounts.put(trade.getId(), reprocessDisputeClosedMessageCounts.get(trade.getId()) + 1); // increment reprocess count maybeReprocessDisputeClosedMessage(trade, reprocessOnError); }, trade.getReprocessDelayInSeconds(reprocessDisputeClosedMessageCounts.get(trade.getId()))); } } } }, trade.getId()); }); HavenoUtils.awaitLatch(initLatch); }, trade.getProtocol().getInitId()); // TODO: getInitId() should be private. ideally this function is moved to TradeProtocol, but logic above depends on SupportManager internals } public void maybeReprocessDisputeClosedMessage(Trade trade, boolean reprocessOnError) { if (trade.isShutDownStarted()) return; ThreadUtils.execute(() -> { synchronized (trade.getLock()) { // skip if no need to reprocess if (trade.isArbitrator() || trade.getArbitrator().getDisputeClosedMessage() == null || trade.getArbitrator().getDisputeClosedMessage().getUnsignedPayoutTxHex() == null || trade.getDisputeState().ordinal() >= Trade.DisputeState.DISPUTE_CLOSED.ordinal()) { return; } log.warn("Reprocessing dispute closed message for {} {}", trade.getClass().getSimpleName(), trade.getId()); handle(trade.getArbitrator().getDisputeClosedMessage(), reprocessOnError); } }, trade.getId()); } public FileTransferSender initLogUpload(FileTransferSession.FtpCallback callback, String tradeId, int traderId) throws IOException { Dispute dispute = findDispute(tradeId, traderId) .orElseThrow(() -> new IOException("could not locate Dispute for tradeId/traderId")); return dispute.createFileTransferSender(p2PService.getNetworkNode(), dispute.getContract().getArbitratorNodeAddress(), callback); } private void processFilePartReceived(FileTransferPart ftp) { if (!ftp.isInitialRequest()) { return; // existing sessions are processed by FileTransferSession object directly } // we create a new session which is related to an open dispute from our list Optional dispute = findDispute(ftp.getTradeId(), ftp.getTraderId()); if (dispute.isEmpty()) { log.error("Received log upload request for unknown TradeId/TraderId {}/{}", ftp.getTradeId(), ftp.getTraderId()); return; } if (dispute.get().isClosed()) { log.error("Received a file transfer request for closed dispute {}", ftp.getTradeId()); return; } try { FileTransferReceiver session = dispute.get().createOrGetFileTransferReceiver( p2PService.getNetworkNode(), ftp.getSenderNodeAddress(), this); session.processFilePartReceived(ftp); } catch (IOException e) { log.error("Unable to process a received file message" + e); } } @Override public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { if (networkEnvelope instanceof FileTransferPart) { // mediator receiving log file data FileTransferPart ftp = (FileTransferPart) networkEnvelope; processFilePartReceived(ftp); } } @Override public void onFtpProgress(double progressPct) { log.trace("ftp progress: {}", progressPct); } @Override public void onFtpComplete(FileTransferSession session) { Optional dispute = findDispute(session.getFullTradeId(), session.getTraderId()); dispute.ifPresent(d -> addMediationLogsReceivedMessage(d, session.getZipId())); } @Override public void onFtpTimeout(String statusMsg, FileTransferSession session) { session.resetSession(); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationSession.java ================================================ /* * 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 . */ package haveno.core.support.dispute.arbitration; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeSession; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; @Slf4j public class ArbitrationSession extends DisputeSession { public ArbitrationSession(@Nullable Dispute dispute, boolean isTrader) { super(dispute, isTrader); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/arbitration/TraderDataItem.java ================================================ /* * 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 . */ package haveno.core.support.dispute.arbitration; import haveno.core.account.witness.AccountAgeWitness; import haveno.core.payment.payload.PaymentAccountPayload; import lombok.EqualsAndHashCode; import lombok.Getter; import java.math.BigInteger; import java.security.PublicKey; // TODO consider to move to signed witness domain @Getter @EqualsAndHashCode(onlyExplicitlyIncluded = true) public class TraderDataItem { private final PaymentAccountPayload paymentAccountPayload; @EqualsAndHashCode.Include private final AccountAgeWitness accountAgeWitness; private final BigInteger tradeAmount; private final PublicKey peersPubKey; public TraderDataItem(PaymentAccountPayload paymentAccountPayload, AccountAgeWitness accountAgeWitness, BigInteger tradeAmount, PublicKey peersPubKey) { this.paymentAccountPayload = paymentAccountPayload; this.accountAgeWitness = accountAgeWitness; this.tradeAmount = tradeAmount; this.peersPubKey = peersPubKey; } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/arbitration/arbitrator/Arbitrator.java ================================================ /* * 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 . */ package haveno.core.support.dispute.arbitration.arbitrator; import com.google.protobuf.ByteString; import haveno.common.crypto.PubKeyRing; import haveno.common.proto.ProtoUtil; import haveno.common.util.CollectionUtils; import haveno.core.support.dispute.agent.DisputeAgent; import haveno.network.p2p.NodeAddress; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; @EqualsAndHashCode(callSuper = true) @Slf4j @Getter public final class Arbitrator extends DisputeAgent { public Arbitrator(NodeAddress nodeAddress, PubKeyRing pubKeyRing, List languageCodes, long registrationDate, byte[] registrationPubKey, String registrationSignature, @Nullable String emailAddress, @Nullable String info, @Nullable Map extraDataMap) { super(nodeAddress, pubKeyRing, languageCodes, registrationDate, registrationPubKey, registrationSignature, emailAddress, info, extraDataMap); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.StoragePayload toProtoMessage() { protobuf.Arbitrator.Builder builder = protobuf.Arbitrator.newBuilder() .setNodeAddress(nodeAddress.toProtoMessage()) .setPubKeyRing(pubKeyRing.toProtoMessage()) .addAllLanguageCodes(languageCodes) .setRegistrationDate(registrationDate) .setRegistrationPubKey(ByteString.copyFrom(registrationPubKey)) .setRegistrationSignature(registrationSignature); Optional.ofNullable(emailAddress).ifPresent(builder::setEmailAddress); Optional.ofNullable(info).ifPresent(builder::setInfo); Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); return protobuf.StoragePayload.newBuilder().setArbitrator(builder).build(); } public static Arbitrator fromProto(protobuf.Arbitrator proto) { return new Arbitrator(NodeAddress.fromProto(proto.getNodeAddress()), PubKeyRing.fromProto(proto.getPubKeyRing()), new ArrayList<>(proto.getLanguageCodesList()), proto.getRegistrationDate(), proto.getRegistrationPubKey().toByteArray(), proto.getRegistrationSignature(), ProtoUtil.stringOrNullFromProto(proto.getEmailAddress()), ProtoUtil.stringOrNullFromProto(proto.getInfo()), CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap()); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String toString() { return "Arbitrator{} " + super.toString(); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/arbitration/arbitrator/ArbitratorManager.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.support.dispute.arbitration.arbitrator; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.config.Config; import haveno.common.crypto.KeyRing; import haveno.core.filter.FilterManager; import haveno.core.support.dispute.agent.DisputeAgentManager; import haveno.core.user.User; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; //import java.util.ArrayList;TODO import java.util.List; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton public class ArbitratorManager extends DisputeAgentManager { @Inject public ArbitratorManager(KeyRing keyRing, ArbitratorService arbitratorService, User user, FilterManager filterManager) { super(keyRing, arbitratorService, user, filterManager); } @Override protected List getPubKeyList() { switch (Config.baseCurrencyNetwork()) { case XMR_LOCAL: return List.of( "027a381b5333a56e1cc3d90d3a7d07f26509adf7029ed06fc997c656621f8da1ee", "024baabdba90e7cc0dc4626ef73ea9d722ea7085d1104491da8c76f28187513492", "026eeec3c119dd6d537249d74e5752a642dd2c3cc5b6a9b44588eb58344f29b519"); case XMR_STAGENET: return List.of( "03bb559ce207a4deb51d4c705076c95b85ad8581d35936b2a422dcb504eaf7cdb0", "026c581ad773d987e6bd10785ac7f7e0e64864aedeb8bce5af37046de812a37854", "025b058c9f2c60d839669dbfa5578cf5a8117d60e6b70e2f0946f8a691273c6a36", "036c7d3f4bf05ef39b9d1b0a5d453a18210de36220c3d83cd16e59bd6132b037ad", "030f7122a10ff73cd73808bddace95be77a94189c8a0eb24586265e125ce5ce6b9", "03aa23e062afa0dda465f46986f8aa8d0374ad3e3f256141b05681dcb1e39c3859", "02d3beb1293ca2ca14e6d42ca8bd18089a62aac62fd6bb23923ee6ead46ac60fba", "03fa0f38f27bdd324db6f933f7e57851dadf3b911e4db6b19dd0950492c4525a31", "02a1a458df5acf4ab08fdca748e28f33a955a30854c8c1a831ee733dca7f0d2fcd", "0374dd70f3fa6e47ec5ab97932e1cec6233e98e6ae3129036b17118650c44fd3de"); case XMR_MAINNET: return List.of( "02d8ac0fbe4e25f4a1d68b95936f25fc2e1b218e161cb5ed6661c7ab4c85f1fd4f", "03c8efdf81287ce8b3212241e6aa7cdf094ecbed2d2f119730a3e4d596a764106a", "021c798eb224ba23bd91ed7710a85d9b9a6439c29f4f29c1a14b96750a0da36aa7", "029da09bc04dea33cd11a31bc1c05aa830b9180acb84e5370ee7fde60cae9f3d03", "02e9dc14edddde19cc9f829a0739d0ab0c7310154ad94a15d477b51d85991b5a8a"); default: throw new RuntimeException("Unhandled base currency network: " + Config.baseCurrencyNetwork()); } } @Override protected boolean isExpectedInstance(ProtectedStorageEntry data) { return data.getProtectedStoragePayload() instanceof Arbitrator; } @Override protected void addAcceptedDisputeAgentToUser(Arbitrator disputeAgent) { user.addAcceptedArbitrator(disputeAgent); } @Override protected void removeAcceptedDisputeAgentFromUser(ProtectedStorageEntry data) { user.removeAcceptedArbitrator((Arbitrator) data.getProtectedStoragePayload()); } @Override protected List getAcceptedDisputeAgentsFromUser() { return user.getAcceptedArbitrators(); } @Override protected void clearAcceptedDisputeAgentsAtUser() { user.clearAcceptedArbitrators(); } @Override protected Arbitrator getRegisteredDisputeAgentFromUser() { return user.getRegisteredArbitrator(); } @Override protected void setRegisteredDisputeAgentAtUser(Arbitrator disputeAgent) { user.setRegisteredArbitrator(disputeAgent); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/arbitration/arbitrator/ArbitratorService.java ================================================ /* * 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 . */ package haveno.core.support.dispute.arbitration.arbitrator; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.core.filter.FilterManager; import haveno.core.support.dispute.agent.DisputeAgentService; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @Singleton public class ArbitratorService extends DisputeAgentService { @Inject public ArbitratorService(P2PService p2PService, FilterManager filterManager) { super(p2PService, filterManager); } @Override protected Set getDisputeAgentSet(List bannedDisputeAgents) { return p2PService.getDataMap().values().stream() .filter(data -> data.getProtectedStoragePayload() instanceof Arbitrator) .map(data -> (Arbitrator) data.getProtectedStoragePayload()) .filter(a -> bannedDisputeAgents == null || !bannedDisputeAgents.contains(a.getNodeAddress().getFullAddress())) .collect(Collectors.toSet()); } @Override protected List getDisputeAgentsFromFilter() { return filterManager.getFilter() != null ? filterManager.getFilter().getArbitrators() : new ArrayList<>(); } public Map getArbitrators() { return super.getDisputeAgents(); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/arbitration/messages/ArbitrationMessage.java ================================================ /* * 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 . */ package haveno.core.support.dispute.arbitration.messages; import haveno.core.support.SupportType; import haveno.core.support.dispute.messages.DisputeMessage; abstract class ArbitrationMessage extends DisputeMessage { ArbitrationMessage(String messageVersion, String uid, SupportType supportType) { super(messageVersion, uid, supportType); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/mediation/FileTransferReceiver.java ================================================ /* * 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 . */ package haveno.core.support.dispute.mediation; import haveno.network.p2p.AckMessage; import haveno.network.p2p.AckMessageSourceType; import haveno.network.p2p.FileTransferPart; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.network.NetworkNode; import haveno.common.UserThread; import haveno.common.config.Config; import haveno.common.util.Utilities; import java.nio.file.FileSystems; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; @Slf4j public class FileTransferReceiver extends FileTransferSession { protected final String zipFilePath; public FileTransferReceiver(NetworkNode networkNode, NodeAddress peerNodeAddress, String tradeId, int traderId, String traderRole, @Nullable FileTransferSession.FtpCallback callback) throws IOException { super(networkNode, peerNodeAddress, tradeId, traderId, traderRole, callback); zipFilePath = ensureReceivingDirectoryExists().getAbsolutePath() + FileSystems.getDefault().getSeparator() + zipId + ".zip"; } public void processFilePartReceived(FileTransferPart ftp) { checkpointLastActivity(); // check that the supplied sequence number is in line with what we are expecting if (currentBlockSeqNum < 0) { // we have not yet started receiving a file, validate this ftp packet as the initiation request initReceiveSession(ftp.uid, ftp.seqNumOrFileLength); } else if (currentBlockSeqNum == ftp.seqNumOrFileLength) { // we are in the middle of receiving a file; add the block of data to the file processReceivedBlock(ftp, networkNode, peerNodeAddress); } else { log.error("ftp sequence num mismatch, expected {} received {}", currentBlockSeqNum, ftp.seqNumOrFileLength); resetSession(); // aborts the file transfer } } public void initReceiveSession(String uid, long expectedFileBytes) { networkNode.addMessageListener(this); this.expectedFileLength = expectedFileBytes; fileOffsetBytes = 0; currentBlockSeqNum = 0; initSessionTimer(); log.info("Received a start file transfer request, tradeId={}, traderId={}, size={}", fullTradeId, traderId, expectedFileBytes); log.info("New file will be written to {}", zipFilePath); UserThread.execute(() -> ackReceivedPart(uid, networkNode, peerNodeAddress)); } private void processReceivedBlock(FileTransferPart ftp, NetworkNode networkNode, NodeAddress peerNodeAddress) { try { RandomAccessFile file = new RandomAccessFile(zipFilePath, "rwd"); file.seek(fileOffsetBytes); file.write(ftp.messageData.toByteArray(), 0, ftp.messageData.size()); fileOffsetBytes = fileOffsetBytes + ftp.messageData.size(); log.info("Sequence number {} for {}, received data {} / {}", ftp.seqNumOrFileLength, Utilities.getShortId(ftp.tradeId), fileOffsetBytes, expectedFileLength); currentBlockSeqNum++; UserThread.runAfter(() -> { ackReceivedPart(ftp.uid, networkNode, peerNodeAddress); if (fileOffsetBytes >= expectedFileLength) { log.info("Success! We have reached the EOF, received {} expected {}", fileOffsetBytes, expectedFileLength); ftpCallback.ifPresent(c -> c.onFtpComplete(this)); resetSession(); } }, 100, TimeUnit.MILLISECONDS); } catch (IOException e) { log.error(e.toString()); e.printStackTrace(); } } private void ackReceivedPart(String uid, NetworkNode networkNode, NodeAddress peerNodeAddress) { AckMessage ackMessage = new AckMessage(peerNodeAddress, AckMessageSourceType.LOG_TRANSFER, FileTransferPart.class.getSimpleName(), uid, Utilities.getShortId(fullTradeId), true, // result null); // errorMessage log.info("Send AckMessage for {} to peer {}. id={}, uid={}", ackMessage.getSourceMsgClassName(), peerNodeAddress, ackMessage.getSourceId(), ackMessage.getSourceUid()); sendMessage(ackMessage, networkNode, peerNodeAddress); } private static File ensureReceivingDirectoryExists() throws IOException { File directory = new File(Config.appDataDir() + "/clientLogs"); if (!directory.exists() && !directory.mkdirs()) { log.error("Could not create directory {}", directory.getAbsolutePath()); throw new IOException("Could not create directory: " + directory.getAbsolutePath()); } return directory; } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/mediation/FileTransferSender.java ================================================ /* * 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 . */ package haveno.core.support.dispute.mediation; import haveno.network.p2p.FileTransferPart; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.network.NetworkNode; import haveno.common.UserThread; import haveno.common.config.Config; import haveno.common.util.Utilities; import com.google.protobuf.ByteString; import java.net.URI; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.io.IOException; import java.io.RandomAccessFile; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import static haveno.common.file.FileUtil.doesFileContainKeyword; @Slf4j public class FileTransferSender extends FileTransferSession { protected final String zipFilePath; private final boolean isTest; public FileTransferSender(NetworkNode networkNode, NodeAddress peerNodeAddress, String tradeId, int traderId, String traderRole, boolean isTest, @Nullable FileTransferSession.FtpCallback callback) { super(networkNode, peerNodeAddress, tradeId, traderId, traderRole, callback); zipFilePath = Utilities.getUserDataDir() + FileSystems.getDefault().getSeparator() + zipId + ".zip"; this.isTest = isTest; updateProgress(); } public void createZipFileToSend() { createZipFileOfLogs(zipFilePath, zipId, fullTradeId); } public static void createZipFileOfLogs(String zipFilePath, String zipId, String fullTradeId) { try { Map env = new HashMap<>(); env.put("create", "true"); URI uri = URI.create("jar:file:///" + zipFilePath .replace('\\', '/') .replaceAll(" ", "%20")); FileSystem zipfs = FileSystems.newFileSystem(uri, env); Files.createDirectory(zipfs.getPath(zipId)); // store logfiles in a usefully-named subdir Stream paths = Files.walk(Paths.get(Config.appDataDir().toString()), 1); paths.filter(Files::isRegularFile).forEach(externalTxtFile -> { try { // always include haveno.log; and other .log files if they contain the TradeId if (externalTxtFile.getFileName().toString().equals("haveno.log") || (fullTradeId == null && externalTxtFile.getFileName().toString().matches(".*.log")) || (externalTxtFile.getFileName().toString().matches(".*.log") && doesFileContainKeyword(externalTxtFile.toFile(), fullTradeId))) { Path pathInZipfile = zipfs.getPath(zipId + "/" + externalTxtFile.getFileName().toString()); log.info("adding {} to zip file {}", pathInZipfile, zipfs); Files.copy(externalTxtFile, pathInZipfile, StandardCopyOption.REPLACE_EXISTING); } } catch (IOException e) { log.error(e.toString()); e.printStackTrace(); } }); zipfs.close(); } catch (IOException | IllegalArgumentException ex) { log.error(ex.toString()); ex.printStackTrace(); } } public void initSend() throws IOException { initSessionTimer(); networkNode.addMessageListener(this); RandomAccessFile file = new RandomAccessFile(zipFilePath, "r"); expectedFileLength = file.length(); file.close(); // an empty block is sent as request to initiate file transfer, peer must ACK for transfer to continue dataAwaitingAck = Optional.of(new FileTransferPart(networkNode.getNodeAddress(), fullTradeId, traderId, UUID.randomUUID().toString(), expectedFileLength, ByteString.EMPTY)); uploadData(); } public void sendNextBlock() throws IOException, IllegalStateException { if (dataAwaitingAck.isPresent()) { log.warn("prepNextBlockToSend invoked, but we are still waiting for a previous ACK"); throw new IllegalStateException("prepNextBlockToSend invoked, but we are still waiting for a previous ACK"); } RandomAccessFile file = new RandomAccessFile(zipFilePath, "r"); file.seek(fileOffsetBytes); byte[] buff = new byte[FILE_BLOCK_SIZE]; int nBytesRead = file.read(buff, 0, FILE_BLOCK_SIZE); file.close(); if (nBytesRead < 0) { log.info("Success! We have reached the EOF, {} bytes sent. Removing zip file {}", fileOffsetBytes, zipFilePath); Files.delete(Paths.get(zipFilePath)); ftpCallback.ifPresent(c -> c.onFtpComplete(this)); UserThread.runAfter(this::resetSession, 1); return; } dataAwaitingAck = Optional.of(new FileTransferPart(networkNode.getNodeAddress(), fullTradeId, traderId, UUID.randomUUID().toString(), currentBlockSeqNum, ByteString.copyFrom(buff, 0, nBytesRead))); uploadData(); } public void retrySend() { if (transferIsInProgress()) { log.info("Retry send of current block"); initSessionTimer(); uploadData(); } else { UserThread.runAfter(() -> ftpCallback.ifPresent((f) -> f.onFtpTimeout("Could not re-send", this)), 1); } } protected void uploadData() { if (dataAwaitingAck.isEmpty()) { return; } FileTransferPart ftp = dataAwaitingAck.get(); log.info("Send FileTransferPart seq {} length {} to peer {}, UID={}", ftp.seqNumOrFileLength, ftp.messageData.size(), peerNodeAddress, ftp.uid); sendMessage(ftp, networkNode, peerNodeAddress); } public boolean processAckForFilePart(String ackUid) { if (dataAwaitingAck.isEmpty()) { log.warn("We received an ACK we were not expecting. {}", ackUid); return false; } if (!dataAwaitingAck.get().uid.equals(ackUid)) { log.warn("We received an ACK that has a different UID to what we were expecting. We ignore and wait for the correct ACK"); log.info("Received {} expecting {}", ackUid, dataAwaitingAck.get().uid); return false; } // fileOffsetBytes gets incremented by the size of the block that was ack'd fileOffsetBytes += dataAwaitingAck.get().messageData.size(); currentBlockSeqNum++; dataAwaitingAck = Optional.empty(); checkpointLastActivity(); updateProgress(); if (isTest) { return true; } UserThread.runAfter(() -> { // to trigger continuing the file transfer try { sendNextBlock(); } catch (IOException e) { log.error(e.toString()); e.printStackTrace(); } }, 100, TimeUnit.MILLISECONDS); return true; } public void updateProgress() { double progressPct = expectedFileLength > 0 ? ((double) fileOffsetBytes / expectedFileLength) : 0.0; ftpCallback.ifPresent(c -> c.onFtpProgress(progressPct)); log.info("ftp progress: {}", String.format("%.0f%%", progressPct * 100)); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/mediation/FileTransferSession.java ================================================ /* * 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 . */ package haveno.core.support.dispute.mediation; import haveno.network.p2p.AckMessage; import haveno.network.p2p.AckMessageSourceType; import haveno.network.p2p.FileTransferPart; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.MessageListener; import haveno.network.p2p.network.NetworkNode; import haveno.common.UserThread; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.util.Utilities; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Optional; import java.util.concurrent.TimeUnit; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; import static haveno.network.p2p.network.Connection.getPermittedMessageSize; @Slf4j public abstract class FileTransferSession implements MessageListener { protected static final int FTP_SESSION_TIMEOUT_MILLIS = (int) TimeUnit.SECONDS.toMillis(60); protected static final int FILE_BLOCK_SIZE = getPermittedMessageSize() - 1024; // allowing space for protobuf public interface FtpCallback { void onFtpProgress(double progressPct); void onFtpComplete(FileTransferSession session); void onFtpTimeout(String statusMsg, FileTransferSession session); } @Getter protected final String fullTradeId; @Getter protected final int traderId; @Getter protected final String zipId; protected final Optional ftpCallback; protected final NetworkNode networkNode; // for sending network messages protected final NodeAddress peerNodeAddress; protected Optional dataAwaitingAck; protected long fileOffsetBytes; protected long currentBlockSeqNum; protected long expectedFileLength; protected long lastActivityTime; public FileTransferSession(NetworkNode networkNode, NodeAddress peerNodeAddress, String tradeId, int traderId, String traderRole, @Nullable FileTransferSession.FtpCallback callback) { this.networkNode = networkNode; this.peerNodeAddress = peerNodeAddress; this.fullTradeId = tradeId; this.traderId = traderId; this.ftpCallback = Optional.ofNullable(callback); this.zipId = Utilities.getShortId(fullTradeId) + "_" + traderRole.toUpperCase() + "_" + new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); resetSession(); } public void resetSession() { lastActivityTime = 0; currentBlockSeqNum = -1; fileOffsetBytes = 0; expectedFileLength = 0; dataAwaitingAck = Optional.empty(); networkNode.removeMessageListener(this); log.info("Ftp session parameters have been reset."); } @Override public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { if (networkEnvelope instanceof FileTransferPart) { // mediator receiving log file data FileTransferPart ftp = (FileTransferPart) networkEnvelope; if (this instanceof FileTransferReceiver) { ((FileTransferReceiver) this).processFilePartReceived(ftp); } } else if (networkEnvelope instanceof AckMessage) { AckMessage ackMessage = (AckMessage) networkEnvelope; if (ackMessage.getSourceType() == AckMessageSourceType.LOG_TRANSFER) { if (ackMessage.isSuccess()) { log.info("Received AckMessage for {} with id {} and uid {}", ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid()); if (this instanceof FileTransferSender) { ((FileTransferSender) this).processAckForFilePart(ackMessage.getSourceUid()); } } else { log.warn("Received AckMessage with error state for {} with id {} and errorMessage={}", ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getErrorMessage()); } } } } protected void checkpointLastActivity() { lastActivityTime = System.currentTimeMillis(); } protected void initSessionTimer() { UserThread.runAfter(() -> { if (!transferIsInProgress()) // transfer may have finished before this timer executes return; if (System.currentTimeMillis() - lastActivityTime < FTP_SESSION_TIMEOUT_MILLIS) { log.info("Last activity was {}, we have not yet timed out.", new Date(lastActivityTime)); initSessionTimer(); } else { log.warn("File transfer session timed out. expected: {} received: {}", expectedFileLength, fileOffsetBytes); ftpCallback.ifPresent((e) -> e.onFtpTimeout("Timed out during send", this)); } }, FTP_SESSION_TIMEOUT_MILLIS / 4, TimeUnit.MILLISECONDS); // check more frequently than the timeout } protected boolean transferIsInProgress() { return fileOffsetBytes != expectedFileLength; } protected void sendMessage(NetworkEnvelope message, NetworkNode networkNode, NodeAddress nodeAddress) { SettableFuture future = networkNode.sendMessage(nodeAddress, message); if (future != null) { // is null when testing with Mockito Futures.addCallback(future, new FutureCallback<>() { @Override public void onSuccess(Connection connection) { } @Override public void onFailure(@NotNull Throwable throwable) { String errorSend = "Sending " + message.getClass().getSimpleName() + " to " + nodeAddress.getFullAddress() + " failed. That is expected if the peer is offline.\n\t" + ".\n\tException=" + throwable.getMessage(); log.warn(errorSend); ftpCallback.ifPresent((f) -> f.onFtpTimeout("Peer offline", FileTransferSession.this)); resetSession(); } }, MoreExecutors.directExecutor()); } } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/mediation/MediationDisputeList.java ================================================ /* * 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 . */ package haveno.core.support.dispute.mediation; import com.google.protobuf.Message; import haveno.common.proto.ProtoUtil; import haveno.core.proto.CoreProtoResolver; import haveno.core.support.SupportType; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeList; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; @Slf4j @ToString /* * Holds a List of mediation dispute objects. * * Calls to the List are delegated because this class intercepts the add/remove calls so changes * can be saved to disc. */ public final class MediationDisputeList extends DisputeList { MediationDisputeList() { super(); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// protected MediationDisputeList(Collection collection) { super(collection); } @Override public Message toProtoMessage() { synchronized (getList()) { return protobuf.PersistableEnvelope.newBuilder().setMediationDisputeList(protobuf.MediationDisputeList.newBuilder() .addAllDispute(ProtoUtil.collectionToProto(getList(), protobuf.Dispute.class))).build(); } } public static MediationDisputeList fromProto(protobuf.MediationDisputeList proto, CoreProtoResolver coreProtoResolver) { List list = proto.getDisputeList().stream() .map(disputeProto -> Dispute.fromProto(disputeProto, coreProtoResolver)) .filter(e -> e.getSupportType().equals(SupportType.MEDIATION)) .collect(Collectors.toList()); return new MediationDisputeList(list); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/mediation/MediationDisputeListService.java ================================================ /* * 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 . */ package haveno.core.support.dispute.mediation; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.persistence.PersistenceManager; import haveno.core.support.dispute.DisputeListService; @Singleton public final class MediationDisputeListService extends DisputeListService { /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public MediationDisputeListService(PersistenceManager persistenceManager) { super(persistenceManager); } /////////////////////////////////////////////////////////////////////////////////////////// // Implement template methods /////////////////////////////////////////////////////////////////////////////////////////// @Override protected MediationDisputeList getConcreteDisputeList() { return new MediationDisputeList(); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/mediation/MediationManager.java ================================================ /* * 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 . */ package haveno.core.support.dispute.mediation; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.app.Version; import haveno.common.config.Config; import haveno.common.crypto.KeyRing; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.core.api.XmrConnectionService; import haveno.core.api.CoreNotificationService; import haveno.core.locale.Res; import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; import haveno.core.provider.price.PriceFeedService; import haveno.core.support.SupportType; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeManager; import haveno.core.support.dispute.DisputeResult; import haveno.core.support.dispute.messages.DisputeClosedMessage; import haveno.core.support.dispute.messages.DisputeOpenedMessage; import haveno.core.support.messages.ChatMessage; import haveno.core.support.messages.SupportMessage; import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.trade.protocol.DisputeProtocol; import haveno.core.trade.protocol.ProcessModel; import haveno.core.xmr.wallet.TradeWalletService; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.AckMessageSourceType; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.math.BigInteger; import java.util.Optional; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j @Singleton public final class MediationManager extends DisputeManager { /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public MediationManager(P2PService p2PService, TradeWalletService tradeWalletService, XmrWalletService walletService, XmrConnectionService xmrConnectionService, CoreNotificationService notificationService, TradeManager tradeManager, ClosedTradableManager closedTradableManager, OpenOfferManager openOfferManager, KeyRing keyRing, MediationDisputeListService mediationDisputeListService, Config config, PriceFeedService priceFeedService) { super(p2PService, tradeWalletService, walletService, xmrConnectionService, notificationService, tradeManager, closedTradableManager, openOfferManager, keyRing, mediationDisputeListService, config, priceFeedService); } /////////////////////////////////////////////////////////////////////////////////////////// // Implement template methods /////////////////////////////////////////////////////////////////////////////////////////// @Override public SupportType getSupportType() { return SupportType.MEDIATION; } @Override public void onSupportMessage(SupportMessage message) { if (canProcessMessage(message)) { log.info("Received {} with tradeId {} and uid {}", message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); if (message instanceof DisputeOpenedMessage) { handle((DisputeOpenedMessage) message); } else if (message instanceof ChatMessage) { handle((ChatMessage) message); } else if (message instanceof DisputeClosedMessage) { handle((DisputeClosedMessage) message); } else { log.warn("Unsupported message at dispatchMessage. message={}", message); } } } @Override protected AckMessageSourceType getAckMessageSourceType() { return AckMessageSourceType.MEDIATION_MESSAGE; } @Override public void cleanupDisputes() { disputeListService.cleanupDisputes(tradeId -> { tradeManager.getOpenTrade(tradeId).filter(trade -> trade.getPayoutTx() != null) .ifPresent(trade -> { tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.MEDIATION_CLOSED); }); }); } @Override protected String getDisputeInfo(Dispute dispute) { String role = Res.get("shared.mediator").toLowerCase(); String link = "https://docs.haveno.exchange/trading-rules.html#mediation"; return Res.get("support.initialInfo", role, role, link); } @Override protected String getDisputeIntroForPeer(String disputeInfo) { return Res.get("support.peerOpenedDisputeForMediation", disputeInfo, Version.VERSION); } @Override protected String getDisputeIntroForDisputeCreator(String disputeInfo) { return Res.get("support.youOpenedDisputeForMediation", disputeInfo, Version.VERSION); } /////////////////////////////////////////////////////////////////////////////////////////// // Message handler /////////////////////////////////////////////////////////////////////////////////////////// @Override // We get that message at both peers. The dispute object is in context of the trader public void handle(DisputeClosedMessage disputeResultMessage) { DisputeResult disputeResult = disputeResultMessage.getDisputeResult(); String tradeId = disputeResult.getTradeId(); ChatMessage chatMessage = disputeResult.getChatMessage(); checkNotNull(chatMessage, "chatMessage must not be null"); Optional disputeOptional = findDispute(disputeResult); String uid = disputeResultMessage.getUid(); if (!disputeOptional.isPresent()) { log.warn("We got a dispute result msg but we don't have a matching dispute. " + "That might happen when we get the disputeResultMessage before the dispute was created. " + "We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId); if (!delayMsgMap.containsKey(uid)) { // We delay 2 sec. to be sure the comm. msg gets added first Timer timer = UserThread.runAfter(() -> handle(disputeResultMessage), 2); delayMsgMap.put(uid, timer); } else { log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " + "That should never happen. TradeId = " + tradeId); } return; } Dispute dispute = disputeOptional.get(); cleanupRetryMap(uid); if (!dispute.getChatMessages().contains(chatMessage)) { dispute.addAndPersistChatMessage(chatMessage); } else { log.warn("We got a dispute mail msg that we have already stored. TradeId = " + chatMessage.getTradeId()); } dispute.setIsClosed(); dispute.setDisputeResult(disputeResult); Optional tradeOptional = tradeManager.getOpenTrade(tradeId); if (tradeOptional.isPresent()) { Trade trade = tradeOptional.get(); if (trade.getDisputeState() == Trade.DisputeState.MEDIATION_REQUESTED || trade.getDisputeState() == Trade.DisputeState.MEDIATION_STARTED_BY_PEER) { trade.getProcessModel().setBuyerPayoutAmountFromMediation(disputeResult.getBuyerPayoutAmountBeforeCost().longValueExact()); trade.getProcessModel().setSellerPayoutAmountFromMediation(disputeResult.getSellerPayoutAmountBeforeCost().longValueExact()); trade.setDisputeState(Trade.DisputeState.MEDIATION_CLOSED); tradeManager.requestPersistence(); } } else { Optional openOfferOptional = openOfferManager.getOpenOffer(tradeId); openOfferOptional.ifPresent(openOffer -> openOfferManager.closeSpentOffer(openOffer.getOffer())); } sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null); requestPersistence(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Nullable @Override public NodeAddress getAgentNodeAddress(Dispute dispute) { return dispute.getContract().getArbitratorNodeAddress(); // TODO (woodser): mediator becomes and replaces current arbitrator? } public void onAcceptMediationResult(Trade trade, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { String tradeId = trade.getId(); Optional optionalDispute = findDispute(tradeId); checkArgument(optionalDispute.isPresent(), "dispute must be present"); DisputeResult disputeResult = optionalDispute.get().getDisputeResultProperty().get(); BigInteger buyerPayoutAmount = disputeResult.getBuyerPayoutAmountBeforeCost(); BigInteger sellerPayoutAmount = disputeResult.getSellerPayoutAmountBeforeCost(); ProcessModel processModel = trade.getProcessModel(); processModel.setBuyerPayoutAmountFromMediation(buyerPayoutAmount.longValueExact()); processModel.setSellerPayoutAmountFromMediation(sellerPayoutAmount.longValueExact()); 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. if (trade.getTradePeer().getMediatedPayoutTxSignature() == null) { tradeProtocol.onAcceptMediationResult(() -> { if (trade.getPayoutTx() != null) { tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.MEDIATION_CLOSED); } resultHandler.handleResult(); }, errorMessageHandler); } else { tradeProtocol.onFinalizeMediationResultPayout(() -> { if (trade.getPayoutTx() != null) { tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.MEDIATION_CLOSED); } resultHandler.handleResult(); }, errorMessageHandler); } } public void rejectMediationResult(Trade trade) { trade.setMediationResultState(MediationResultState.MEDIATION_RESULT_REJECTED); tradeManager.requestPersistence(); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/mediation/MediationResultState.java ================================================ /* * 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 . */ package haveno.core.support.dispute.mediation; import haveno.common.proto.ProtoUtil; public enum MediationResultState { UNDEFINED_MEDIATION_RESULT, MEDIATION_RESULT_ACCEPTED(), MEDIATION_RESULT_REJECTED, SIG_MSG_SENT, SIG_MSG_ARRIVED, SIG_MSG_IN_MAILBOX, SIG_MSG_SEND_FAILED, RECEIVED_SIG_MSG, PAYOUT_TX_PUBLISHED, PAYOUT_TX_PUBLISHED_MSG_SENT, PAYOUT_TX_PUBLISHED_MSG_ARRIVED, PAYOUT_TX_PUBLISHED_MSG_IN_MAILBOX, PAYOUT_TX_PUBLISHED_MSG_SEND_FAILED, RECEIVED_PAYOUT_TX_PUBLISHED_MSG, PAYOUT_TX_SEEN_IN_NETWORK; public static MediationResultState fromProto(protobuf.MediationResultState mediationResultState) { return ProtoUtil.enumFromProto(MediationResultState.class, mediationResultState.name()); } public static protobuf.MediationResultState toProtoMessage(MediationResultState mediationResultState) { return protobuf.MediationResultState.valueOf(mediationResultState.name()); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/mediation/MediationSession.java ================================================ /* * 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 . */ package haveno.core.support.dispute.mediation; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeSession; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; @Slf4j public class MediationSession extends DisputeSession { public MediationSession(@Nullable Dispute dispute, boolean isTrader) { super(dispute, isTrader); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/mediation/mediator/Mediator.java ================================================ /* * 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 . */ package haveno.core.support.dispute.mediation.mediator; import com.google.protobuf.ByteString; import haveno.common.crypto.PubKeyRing; import haveno.common.proto.ProtoUtil; import haveno.common.util.CollectionUtils; import haveno.core.support.dispute.agent.DisputeAgent; import haveno.network.p2p.NodeAddress; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; @EqualsAndHashCode(callSuper = true) @Slf4j public final class Mediator extends DisputeAgent { public Mediator(NodeAddress nodeAddress, PubKeyRing pubKeyRing, List languageCodes, long registrationDate, byte[] registrationPubKey, String registrationSignature, @Nullable String emailAddress, @Nullable String info, @Nullable Map extraDataMap) { super(nodeAddress, pubKeyRing, languageCodes, registrationDate, registrationPubKey, registrationSignature, emailAddress, info, extraDataMap); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.StoragePayload toProtoMessage() { final protobuf.Mediator.Builder builder = protobuf.Mediator.newBuilder() .setNodeAddress(nodeAddress.toProtoMessage()) .setPubKeyRing(pubKeyRing.toProtoMessage()) .addAllLanguageCodes(languageCodes) .setRegistrationDate(registrationDate) .setRegistrationPubKey(ByteString.copyFrom(registrationPubKey)) .setRegistrationSignature(registrationSignature); Optional.ofNullable(emailAddress).ifPresent(builder::setEmailAddress); Optional.ofNullable(info).ifPresent(builder::setInfo); Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); return protobuf.StoragePayload.newBuilder().setMediator(builder).build(); } public static Mediator fromProto(protobuf.Mediator proto) { return new Mediator(NodeAddress.fromProto(proto.getNodeAddress()), PubKeyRing.fromProto(proto.getPubKeyRing()), new ArrayList<>(proto.getLanguageCodesList()), proto.getRegistrationDate(), proto.getRegistrationPubKey().toByteArray(), proto.getRegistrationSignature(), ProtoUtil.stringOrNullFromProto(proto.getEmailAddress()), ProtoUtil.stringOrNullFromProto(proto.getInfo()), CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap()); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String toString() { return "Mediator{} " + super.toString(); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/mediation/mediator/MediatorManager.java ================================================ /* * 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 . */ package haveno.core.support.dispute.mediation.mediator; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.crypto.KeyRing; import haveno.core.filter.FilterManager; import haveno.core.support.dispute.agent.DisputeAgentManager; import haveno.core.user.User; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import java.util.List; @Singleton public class MediatorManager extends DisputeAgentManager { @Inject public MediatorManager(KeyRing keyRing, MediatorService mediatorService, User user, FilterManager filterManager) { super(keyRing, mediatorService, user, filterManager); } @Override protected List getPubKeyList() { return List.of("03be5471ff9090d322110d87912eefe89871784b1754d0707fdb917be5d88d3809", "023736953a5a6638db71d7f78edc38cea0e42143c3b184ee67f331dafdc2c59efa", "03d82260038253f7367012a4fc0c52dac74cfc67ac9cfbc3c3ad8fca746d8e5fc6", "02dac85f726121ef333d425bc8e13173b5b365a6444176306e6a0a9e76ae1073bd", "0342a5b37c8f843c3302e930d0197cdd8948a6f76747c05e138a6671a6a4caf739", "027afa67c920867a70dfad77db6c6f74051f5af8bf56a1ad479f0bc4005df92325", "03505f44f1893b64a457f8883afdd60774d7f4def6f82bb6f60be83a4b5b85cf82", "0277d2d505d28ad67a03b001ef66f0eaaf1184fa87ebeaa937703cec7073cb2e8f", "027cb3e9a56a438714e2144e2f75db7293ad967f12d5c29b17623efbd35ddbceb0", "03be5471ff9090d322110d87912eefe89871784b1754d0707fdb917be5d88d3809", "03756937d33d028eea274a3154775b2bffd076ffcc4a23fe0f9080f8b7fa0dab5b", "03d8359823a91736cb7aecfaf756872daf258084133c9dd25b96ab3643707c38ca", "03589ed6ded1a1aa92d6ad38bead13e4ad8ba24c60ca6ed8a8efc6e154e3f60add", "0356965753f77a9c0e33ca7cc47fd43ce7f99b60334308ad3c11eed3665de79a78", "031112eb033ebacb635754a2b7163c68270c9171c40f271e70e37b22a2590d3c18"); } @Override protected boolean isExpectedInstance(ProtectedStorageEntry data) { return data.getProtectedStoragePayload() instanceof Mediator; } @Override protected void addAcceptedDisputeAgentToUser(Mediator disputeAgent) { user.addAcceptedMediator(disputeAgent); } @Override protected void removeAcceptedDisputeAgentFromUser(ProtectedStorageEntry data) { user.removeAcceptedMediator((Mediator) data.getProtectedStoragePayload()); } @Override protected List getAcceptedDisputeAgentsFromUser() { return user.getAcceptedMediators(); } @Override protected void clearAcceptedDisputeAgentsAtUser() { user.clearAcceptedMediators(); } @Override protected Mediator getRegisteredDisputeAgentFromUser() { return user.getRegisteredMediator(); } @Override protected void setRegisteredDisputeAgentAtUser(Mediator disputeAgent) { user.setRegisteredMediator(disputeAgent); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/mediation/mediator/MediatorService.java ================================================ /* * 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 . */ package haveno.core.support.dispute.mediation.mediator; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.core.filter.FilterManager; import haveno.core.support.dispute.agent.DisputeAgentService; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton public class MediatorService extends DisputeAgentService { @Inject public MediatorService(P2PService p2PService, FilterManager filterManager) { super(p2PService, filterManager); } @Override protected Set getDisputeAgentSet(List bannedDisputeAgents) { return p2PService.getDataMap().values().stream() .filter(data -> data.getProtectedStoragePayload() instanceof Mediator) .map(data -> (Mediator) data.getProtectedStoragePayload()) .filter(a -> bannedDisputeAgents == null || !bannedDisputeAgents.contains(a.getNodeAddress().getFullAddress())) .collect(Collectors.toSet()); } @Override protected List getDisputeAgentsFromFilter() { return filterManager.getFilter() != null ? filterManager.getFilter().getMediators() : new ArrayList<>(); } public Map getMediators() { return super.getDisputeAgents(); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/messages/DisputeClosedMessage.java ================================================ /* * 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 . */ package haveno.core.support.dispute.messages; import haveno.common.app.Version; import haveno.common.proto.ProtoUtil; import haveno.core.support.SupportType; import haveno.core.support.dispute.DisputeResult; import haveno.network.p2p.NodeAddress; import lombok.EqualsAndHashCode; import lombok.Value; import javax.annotation.Nullable; import java.util.Optional; import static com.google.common.base.Preconditions.checkArgument; @Value @EqualsAndHashCode(callSuper = true) public final class DisputeClosedMessage extends DisputeMessage { private final DisputeResult disputeResult; private final NodeAddress senderNodeAddress; @Nullable private final String updatedMultisigHex; @Nullable private final String unsignedPayoutTxHex; private final boolean deferPublishPayout; public DisputeClosedMessage(DisputeResult disputeResult, NodeAddress senderNodeAddress, String uid, SupportType supportType, @Nullable String updatedMultisigHex, @Nullable String unsignedPayoutTxHex, boolean deferPublishPayout) { this(disputeResult, senderNodeAddress, uid, Version.getP2PMessageVersion(), supportType, updatedMultisigHex, unsignedPayoutTxHex, deferPublishPayout); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private DisputeClosedMessage(DisputeResult disputeResult, NodeAddress senderNodeAddress, String uid, String messageVersion, SupportType supportType, String updatedMultisigHex, String unsignedPayoutTxHex, boolean deferPublishPayout) { super(messageVersion, uid, supportType); this.disputeResult = disputeResult; this.senderNodeAddress = senderNodeAddress; this.updatedMultisigHex = updatedMultisigHex; this.unsignedPayoutTxHex = unsignedPayoutTxHex; this.deferPublishPayout = deferPublishPayout; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { protobuf.DisputeClosedMessage.Builder builder = protobuf.DisputeClosedMessage.newBuilder() .setDisputeResult(disputeResult.toProtoMessage()) .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setUid(uid) .setType(SupportType.toProtoMessage(supportType)) .setDeferPublishPayout(deferPublishPayout); Optional.ofNullable(unsignedPayoutTxHex).ifPresent(e -> builder.setUnsignedPayoutTxHex(unsignedPayoutTxHex)); Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex)); return getNetworkEnvelopeBuilder().setDisputeClosedMessage(builder).build(); } public static DisputeClosedMessage fromProto(protobuf.DisputeClosedMessage proto, String messageVersion) { checkArgument(proto.hasDisputeResult(), "DisputeResult must be set"); return new DisputeClosedMessage(DisputeResult.fromProto(proto.getDisputeResult()), NodeAddress.fromProto(proto.getSenderNodeAddress()), proto.getUid(), messageVersion, SupportType.fromProto(proto.getType()), ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()), ProtoUtil.stringOrNullFromProto(proto.getUnsignedPayoutTxHex()), proto.getDeferPublishPayout()); } @Override public String getTradeId() { return disputeResult.getTradeId(); } @Override public String toString() { return "DisputeClosedMessage{" + "\n disputeResult=" + disputeResult + ",\n senderNodeAddress=" + senderNodeAddress + ",\n DisputeClosedMessage.uid='" + uid + '\'' + ",\n messageVersion=" + messageVersion + ",\n supportType=" + supportType + ",\n deferPublishPayout=" + deferPublishPayout + "\n} " + super.toString(); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/messages/DisputeMessage.java ================================================ /* * 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 . */ package haveno.core.support.dispute.messages; import haveno.core.support.SupportType; import haveno.core.support.messages.SupportMessage; import java.util.concurrent.TimeUnit; public abstract class DisputeMessage extends SupportMessage { public static final long TTL = TimeUnit.DAYS.toMillis(15); public DisputeMessage(String messageVersion, String uid, SupportType supportType) { super(messageVersion, uid, supportType); } @Override public long getTTL() { return TTL; } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/messages/DisputeOpenedMessage.java ================================================ /* * 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 . */ package haveno.core.support.dispute.messages; import haveno.common.app.Version; import haveno.common.proto.ProtoUtil; import haveno.core.proto.CoreProtoResolver; import haveno.core.support.SupportType; import haveno.core.support.dispute.Dispute; import haveno.core.trade.messages.PaymentSentMessage; import haveno.network.p2p.NodeAddress; import lombok.EqualsAndHashCode; import lombok.Value; import java.util.Optional; @EqualsAndHashCode(callSuper = true) @Value public final class DisputeOpenedMessage extends DisputeMessage { private final Dispute dispute; private final NodeAddress senderNodeAddress; private final String openerUpdatedMultisigHex; private final PaymentSentMessage paymentSentMessage; public DisputeOpenedMessage(Dispute dispute, NodeAddress senderNodeAddress, String uid, SupportType supportType, String updatedMultisigHex, PaymentSentMessage paymentSentMessage) { this(dispute, senderNodeAddress, uid, Version.getP2PMessageVersion(), supportType, updatedMultisigHex, paymentSentMessage); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private DisputeOpenedMessage(Dispute dispute, NodeAddress senderNodeAddress, String uid, String messageVersion, SupportType supportType, String updatedMultisigHex, PaymentSentMessage paymentSentMessage) { super(messageVersion, uid, supportType); this.dispute = dispute; this.senderNodeAddress = senderNodeAddress; this.openerUpdatedMultisigHex = updatedMultisigHex; this.paymentSentMessage = paymentSentMessage; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { protobuf.DisputeOpenedMessage.Builder builder = protobuf.DisputeOpenedMessage.newBuilder() .setUid(uid) .setDispute(dispute.toProtoMessage()) .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setType(SupportType.toProtoMessage(supportType)) .setOpenerUpdatedMultisigHex(openerUpdatedMultisigHex); Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage())); return getNetworkEnvelopeBuilder().setDisputeOpenedMessage(builder).build(); } public static DisputeOpenedMessage fromProto(protobuf.DisputeOpenedMessage proto, CoreProtoResolver coreProtoResolver, String messageVersion) { return new DisputeOpenedMessage(Dispute.fromProto(proto.getDispute(), coreProtoResolver), NodeAddress.fromProto(proto.getSenderNodeAddress()), proto.getUid(), messageVersion, SupportType.fromProto(proto.getType()), ProtoUtil.stringOrNullFromProto(proto.getOpenerUpdatedMultisigHex()), proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion) : null); } @Override public String getTradeId() { return dispute.getTradeId(); } @Override public String toString() { return "DisputeOpenedMessage{" + "\n dispute=" + dispute + ",\n senderNodeAddress=" + senderNodeAddress + ",\n DisputeOpenedMessage.uid='" + uid + '\'' + ",\n messageVersion=" + messageVersion + ",\n supportType=" + supportType + ",\n openerUpdatedMultisigHex=" + openerUpdatedMultisigHex + ",\n paymentSentMessage=" + paymentSentMessage + "\n} " + super.toString(); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/refund/RefundDisputeList.java ================================================ /* * 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 . */ package haveno.core.support.dispute.refund; import com.google.protobuf.Message; import haveno.common.proto.ProtoUtil; import haveno.core.proto.CoreProtoResolver; import haveno.core.support.SupportType; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeList; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkArgument; @Slf4j @ToString /* * Holds a List of refund dispute objects. * * Calls to the List are delegated because this class intercepts the add/remove calls so changes * can be saved to disc. */ public final class RefundDisputeList extends DisputeList { RefundDisputeList() { super(); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// protected RefundDisputeList(Collection collection) { super(collection); } @Override public Message toProtoMessage() { forEach(dispute -> checkArgument(dispute.getSupportType().equals(SupportType.REFUND), "Support type has to be REFUND")); synchronized (getList()) { return protobuf.PersistableEnvelope.newBuilder().setRefundDisputeList(protobuf.RefundDisputeList.newBuilder() .addAllDispute(ProtoUtil.collectionToProto(getList(), protobuf.Dispute.class))).build(); } } public static RefundDisputeList fromProto(protobuf.RefundDisputeList proto, CoreProtoResolver coreProtoResolver) { List list = proto.getDisputeList().stream() .map(disputeProto -> Dispute.fromProto(disputeProto, coreProtoResolver)) .filter(e -> e.getSupportType().equals(SupportType.REFUND)) .collect(Collectors.toList()); return new RefundDisputeList(list); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/refund/RefundDisputeListService.java ================================================ /* * 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 . */ package haveno.core.support.dispute.refund; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.persistence.PersistenceManager; import haveno.core.support.dispute.DisputeListService; @Singleton public final class RefundDisputeListService extends DisputeListService { /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public RefundDisputeListService(PersistenceManager persistenceManager) { super(persistenceManager); } /////////////////////////////////////////////////////////////////////////////////////////// // Implement template methods /////////////////////////////////////////////////////////////////////////////////////////// @Override protected RefundDisputeList getConcreteDisputeList() { return new RefundDisputeList(); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/refund/RefundManager.java ================================================ /* * 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 . */ package haveno.core.support.dispute.refund; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.app.Version; import haveno.common.config.Config; import haveno.common.crypto.KeyRing; import haveno.core.api.XmrConnectionService; import haveno.core.api.CoreNotificationService; import haveno.core.locale.Res; import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; import haveno.core.provider.price.PriceFeedService; import haveno.core.support.SupportType; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeManager; import haveno.core.support.dispute.DisputeResult; import haveno.core.support.dispute.messages.DisputeClosedMessage; import haveno.core.support.dispute.messages.DisputeOpenedMessage; import haveno.core.support.messages.ChatMessage; import haveno.core.support.messages.SupportMessage; import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.xmr.wallet.TradeWalletService; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.AckMessageSourceType; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.util.Optional; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j @Singleton public final class RefundManager extends DisputeManager { /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public RefundManager(P2PService p2PService, TradeWalletService tradeWalletService, XmrWalletService walletService, XmrConnectionService xmrConnectionService, CoreNotificationService notificationService, TradeManager tradeManager, ClosedTradableManager closedTradableManager, OpenOfferManager openOfferManager, // TODO (woodser): remove priceFeedService? KeyRing keyRing, RefundDisputeListService refundDisputeListService, Config config, PriceFeedService priceFeedService) { super(p2PService, tradeWalletService, walletService, xmrConnectionService, notificationService, tradeManager, closedTradableManager, openOfferManager, keyRing, refundDisputeListService, config, priceFeedService); } /////////////////////////////////////////////////////////////////////////////////////////// // Implement template methods /////////////////////////////////////////////////////////////////////////////////////////// @Override public SupportType getSupportType() { return SupportType.REFUND; } @Override public void onSupportMessage(SupportMessage message) { if (canProcessMessage(message)) { log.info("Received {} with tradeId {} and uid {}", message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); if (message instanceof DisputeOpenedMessage) { handle((DisputeOpenedMessage) message); } else if (message instanceof ChatMessage) { handle((ChatMessage) message); } else if (message instanceof DisputeClosedMessage) { handle((DisputeClosedMessage) message); } else { log.warn("Unsupported message at dispatchMessage. message={}", message); } } } @Override protected AckMessageSourceType getAckMessageSourceType() { return AckMessageSourceType.REFUND_MESSAGE; } @Override public void cleanupDisputes() { disputeListService.cleanupDisputes(tradeId -> tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.REFUND_REQUEST_CLOSED)); } @Override protected String getDisputeInfo(Dispute dispute) { String role = Res.get("shared.refundAgent").toLowerCase(); String link = "https://docs.haveno.exchange/trading-rules.html#arbitration"; return Res.get("support.initialInfo", role, role, link); } @Override protected String getDisputeIntroForPeer(String disputeInfo) { return Res.get("support.peerOpenedDispute", disputeInfo, Version.VERSION); } @Override protected String getDisputeIntroForDisputeCreator(String disputeInfo) { return Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION); } @Override protected void addPriceInfoMessage(Dispute dispute, int counter) { // At refund agent we do not add the option trade price check as the time for dispute opening is not correct. // In case of an option trade the mediator adds to the result summary message automatically the system message // with the option trade detection info so the refund agent can see that as well. } /////////////////////////////////////////////////////////////////////////////////////////// // Message handler /////////////////////////////////////////////////////////////////////////////////////////// @Override // We get that message at both peers. The dispute object is in context of the trader public void handle(DisputeClosedMessage disputeResultMessage) { DisputeResult disputeResult = disputeResultMessage.getDisputeResult(); String tradeId = disputeResult.getTradeId(); ChatMessage chatMessage = disputeResult.getChatMessage(); checkNotNull(chatMessage, "chatMessage must not be null"); Optional disputeOptional = findDispute(disputeResult); String uid = disputeResultMessage.getUid(); if (!disputeOptional.isPresent()) { log.warn("We got a dispute result msg but we don't have a matching dispute. " + "That might happen when we get the disputeResultMessage before the dispute was created. " + "We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId); if (!delayMsgMap.containsKey(uid)) { // We delay 2 sec. to be sure the comm. msg gets added first Timer timer = UserThread.runAfter(() -> handle(disputeResultMessage), 2); delayMsgMap.put(uid, timer); } else { log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " + "That should never happen. TradeId = " + tradeId); } return; } Dispute dispute = disputeOptional.get(); cleanupRetryMap(uid); if (!dispute.getChatMessages().contains(chatMessage)) { dispute.addAndPersistChatMessage(chatMessage); } else { log.warn("We got a dispute mail msg that we have already stored. TradeId = " + chatMessage.getTradeId()); } dispute.setIsClosed(); if (dispute.disputeResultProperty().get() != null) { log.warn("We got already a dispute result. That should only happen if a dispute needs to be closed " + "again because the first close did not succeed. TradeId = " + tradeId); } dispute.setDisputeResult(disputeResult); Optional tradeOptional = tradeManager.getOpenTrade(tradeId); if (tradeOptional.isPresent()) { Trade trade = tradeOptional.get(); 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 openOfferOptional = openOfferManager.getOpenOffer(tradeId); openOfferOptional.ifPresent(openOffer -> openOfferManager.closeSpentOffer(openOffer.getOffer())); } sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null); // set state after payout as we call swapAddressEntryToAvailable if (tradeManager.getOpenTrade(tradeId).isPresent()) { tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.REFUND_REQUEST_CLOSED); } else { Optional openOfferOptional = openOfferManager.getOpenOffer(tradeId); openOfferOptional.ifPresent(openOffer -> openOfferManager.closeSpentOffer(openOffer.getOffer())); } requestPersistence(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Nullable @Override public NodeAddress getAgentNodeAddress(Dispute dispute) { throw new RuntimeException("Refund manager not used in XMR adapation"); //return dispute.getContract().getRefundAgentNodeAddress(); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/refund/RefundResultState.java ================================================ /* * 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 . */ package haveno.core.support.dispute.refund; import haveno.common.proto.ProtoUtil; // todo public enum RefundResultState { UNDEFINED_REFUND_RESULT; public static RefundResultState fromProto(protobuf.RefundResultState refundResultState) { return ProtoUtil.enumFromProto(RefundResultState.class, refundResultState.name()); } public static protobuf.RefundResultState toProtoMessage(RefundResultState refundResultState) { return protobuf.RefundResultState.valueOf(refundResultState.name()); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/refund/RefundSession.java ================================================ /* * 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 . */ package haveno.core.support.dispute.refund; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeSession; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; @Slf4j public class RefundSession extends DisputeSession { public RefundSession(@Nullable Dispute dispute, boolean isTrader) { super(dispute, isTrader); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/refund/refundagent/RefundAgent.java ================================================ /* * 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 . */ package haveno.core.support.dispute.refund.refundagent; import com.google.protobuf.ByteString; import haveno.common.app.Capabilities; import haveno.common.app.Capability; import haveno.common.crypto.PubKeyRing; import haveno.common.proto.ProtoUtil; import haveno.common.util.CollectionUtils; import haveno.core.support.dispute.agent.DisputeAgent; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.storage.payload.CapabilityRequiringPayload; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; @EqualsAndHashCode(callSuper = true) @Slf4j @Getter public final class RefundAgent extends DisputeAgent implements CapabilityRequiringPayload { public RefundAgent(NodeAddress nodeAddress, PubKeyRing pubKeyRing, List languageCodes, long registrationDate, byte[] registrationPubKey, String registrationSignature, @Nullable String emailAddress, @Nullable String info, @Nullable Map extraDataMap) { super(nodeAddress, pubKeyRing, languageCodes, registrationDate, registrationPubKey, registrationSignature, emailAddress, info, extraDataMap); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.StoragePayload toProtoMessage() { protobuf.RefundAgent.Builder builder = protobuf.RefundAgent.newBuilder() .setNodeAddress(nodeAddress.toProtoMessage()) .setPubKeyRing(pubKeyRing.toProtoMessage()) .addAllLanguageCodes(languageCodes) .setRegistrationDate(registrationDate) .setRegistrationPubKey(ByteString.copyFrom(registrationPubKey)) .setRegistrationSignature(registrationSignature); Optional.ofNullable(emailAddress).ifPresent(builder::setEmailAddress); Optional.ofNullable(info).ifPresent(builder::setInfo); Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); return protobuf.StoragePayload.newBuilder().setRefundAgent(builder).build(); } public static RefundAgent fromProto(protobuf.RefundAgent proto) { return new RefundAgent(NodeAddress.fromProto(proto.getNodeAddress()), PubKeyRing.fromProto(proto.getPubKeyRing()), new ArrayList<>(proto.getLanguageCodesList()), proto.getRegistrationDate(), proto.getRegistrationPubKey().toByteArray(), proto.getRegistrationSignature(), ProtoUtil.stringOrNullFromProto(proto.getEmailAddress()), ProtoUtil.stringOrNullFromProto(proto.getInfo()), CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap()); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String toString() { return "RefundAgent{} " + super.toString(); } @Override public Capabilities getRequiredCapabilities() { return new Capabilities(Capability.REFUND_AGENT); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/refund/refundagent/RefundAgentManager.java ================================================ /* * 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 . */ package haveno.core.support.dispute.refund.refundagent; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.crypto.KeyRing; import haveno.core.filter.FilterManager; import haveno.core.support.dispute.agent.DisputeAgentManager; import haveno.core.user.User; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import java.util.List; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton public class RefundAgentManager extends DisputeAgentManager { @Inject public RefundAgentManager(KeyRing keyRing, RefundAgentService refundAgentService, User user, FilterManager filterManager) { super(keyRing, refundAgentService, user, filterManager); } @Override protected List getPubKeyList() { return List.of("02a25798e256b800d7ea71c31098ac9a47cb20892176afdfeb051f5ded382d44af", "0360455d3cffe00ef73cc1284c84eedacc8c5c3374c43f4aac8ffb95f5130b9ef5", "03b0513afbb531bc4551b379eba027feddd33c92b5990fd477b0fa6eff90a5b7db", "03533fd75fda29c351298e50b8ea696656dcb8ce4e263d10618c6901a50450bf0e", "028124436482aa4c61a4bc4097d60c80b09f4285413be3b023a37a0164cbd5d818", "0384fcf883116d8e9469720ed7808cc4141f6dc6a5ed23d76dd48f2f5f255590d7", "029bd318ecee4e212ff06a4396770d600d72e9e0c6532142a428bdb401491e9721", "02e375b4b24d0a858953f7f94666667554d41f78000b9c8a301294223688b29011", "0232c088ae7c070de89d2b6c8d485b34bf0e3b2a964a2c6622f39ca501260c23f7", "033e047f74f2aa1ce41e8c85731f97ab83d448d65dc8518ab3df4474a5d53a3d19", "02f52a8cf373c8cbddb318e523b7f111168bf753fdfb6f8aa81f88c950ede3a5ce", "039784029922c54bcd0f0e7f14530f586053a5f4e596e86b3474cd7404657088ae", "037969f9d5ab2cc609104c6e61323df55428f8f108c11aab7c7b5f953081d39304", "031bd37475b8c5615ac46d6816e791c59d806d72a0bc6739ae94e5fe4545c7f8a6", "021bb92c636feacf5b082313eb071a63dfcd26501a48b3cd248e35438e5afb7daf"); } @Override protected boolean isExpectedInstance(ProtectedStorageEntry data) { return data.getProtectedStoragePayload() instanceof RefundAgent; } @Override protected void addAcceptedDisputeAgentToUser(RefundAgent disputeAgent) { user.addAcceptedRefundAgent(disputeAgent); } @Override protected void removeAcceptedDisputeAgentFromUser(ProtectedStorageEntry data) { user.removeAcceptedRefundAgent((RefundAgent) data.getProtectedStoragePayload()); } @Override protected List getAcceptedDisputeAgentsFromUser() { return user.getAcceptedRefundAgents(); } @Override protected void clearAcceptedDisputeAgentsAtUser() { user.clearAcceptedRefundAgents(); } @Override protected RefundAgent getRegisteredDisputeAgentFromUser() { return user.getRegisteredRefundAgent(); } @Override protected void setRegisteredDisputeAgentAtUser(RefundAgent disputeAgent) { user.setRegisteredRefundAgent(disputeAgent); } } ================================================ FILE: core/src/main/java/haveno/core/support/dispute/refund/refundagent/RefundAgentService.java ================================================ /* * 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 . */ package haveno.core.support.dispute.refund.refundagent; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.core.filter.FilterManager; import haveno.core.support.dispute.agent.DisputeAgentService; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @Singleton public class RefundAgentService extends DisputeAgentService { @Inject public RefundAgentService(P2PService p2PService, FilterManager filterManager) { super(p2PService, filterManager); } @Override protected Set getDisputeAgentSet(List bannedDisputeAgents) { return p2PService.getDataMap().values().stream() .filter(data -> data.getProtectedStoragePayload() instanceof RefundAgent) .map(data -> (RefundAgent) data.getProtectedStoragePayload()) .filter(a -> bannedDisputeAgents == null || !bannedDisputeAgents.contains(a.getNodeAddress().getFullAddress())) .collect(Collectors.toSet()); } @Override protected List getDisputeAgentsFromFilter() { return filterManager.getFilter() != null ? filterManager.getFilter().getRefundAgents() : new ArrayList<>(); } public Map getRefundAgents() { return super.getDisputeAgents(); } } ================================================ FILE: core/src/main/java/haveno/core/support/messages/ChatMessage.java ================================================ /* * 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 . */ package haveno.core.support.messages; import haveno.core.locale.Res; import haveno.core.support.SupportType; import haveno.core.support.dispute.Attachment; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeResult; import haveno.network.p2p.NodeAddress; import haveno.common.UserThread; import haveno.common.app.Version; import haveno.common.util.Utilities; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyStringProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.lang.ref.WeakReference; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; /* Message for direct communication between two nodes. Originally built for trader to * arbitrator communication as no other direct communication was allowed. Arbitrator is * considered as the server and trader as the client in arbitration chats * * For trader to trader communication the maker is considered to be the server * and the taker is considered as the client. */ @EqualsAndHashCode(callSuper = true) // listener is transient and therefore excluded anyway @Getter @Slf4j public final class ChatMessage extends SupportMessage { public static final long TTL = TimeUnit.DAYS.toMillis(7); public interface Listener { void onMessageStateChanged(); } private final String tradeId; private final int traderId; // This is only used for the server client relationship // If senderIsTrader == true then the sender is the client private final boolean senderIsTrader; private final String message; private final ArrayList attachments = new ArrayList<>(); private final NodeAddress senderNodeAddress; private final long date; @Setter private boolean isSystemMessage; // Added in v1.1.6. for trader chat to store if message was shown in popup @Setter private boolean wasDisplayed; //todo move to base class private final BooleanProperty arrivedProperty; private final BooleanProperty storedInMailboxProperty; private final BooleanProperty acknowledgedProperty; private final StringProperty sendMessageErrorProperty; private final StringProperty ackErrorProperty; transient private WeakReference listener; public ChatMessage(SupportType supportType, String tradeId, int traderId, boolean senderIsTrader, String message, NodeAddress senderNodeAddress) { this(supportType, tradeId, traderId, senderIsTrader, message, null, senderNodeAddress, new Date().getTime(), false, false, UUID.randomUUID().toString(), Version.getP2PMessageVersion(), false, null, null, false); } public ChatMessage(SupportType supportType, String tradeId, int traderId, boolean senderIsTrader, String message, NodeAddress senderNodeAddress, ArrayList attachments) { this(supportType, tradeId, traderId, senderIsTrader, message, attachments, senderNodeAddress, new Date().getTime(), false, false, UUID.randomUUID().toString(), Version.getP2PMessageVersion(), false, null, null, false); } public ChatMessage(SupportType supportType, String tradeId, int traderId, boolean senderIsTrader, String message, NodeAddress senderNodeAddress, long date) { this(supportType, tradeId, traderId, senderIsTrader, message, null, senderNodeAddress, date, false, false, UUID.randomUUID().toString(), Version.getP2PMessageVersion(), false, null, null, false); } public ChatMessage copy() { return new ChatMessage(supportType, tradeId, traderId, senderIsTrader, message, new ArrayList<>(attachments), senderNodeAddress, date, arrivedProperty.get(), storedInMailboxProperty.get(), uid, messageVersion, acknowledgedProperty.get(), sendMessageErrorProperty.get(), ackErrorProperty.get(), wasDisplayed); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private ChatMessage(SupportType supportType, String tradeId, int traderId, boolean senderIsTrader, String message, @Nullable List attachments, NodeAddress senderNodeAddress, long date, boolean arrived, boolean storedInMailbox, String uid, String messageVersion, boolean acknowledged, @Nullable String sendMessageError, @Nullable String ackError, boolean wasDisplayed) { super(messageVersion, uid, supportType); this.tradeId = tradeId; this.traderId = traderId; this.senderIsTrader = senderIsTrader; this.message = message; this.wasDisplayed = wasDisplayed; Optional.ofNullable(attachments).ifPresent(e -> addAllAttachments(attachments)); this.senderNodeAddress = senderNodeAddress; this.date = date; arrivedProperty = new SimpleBooleanProperty(arrived); storedInMailboxProperty = new SimpleBooleanProperty(storedInMailbox); acknowledgedProperty = new SimpleBooleanProperty(acknowledged); sendMessageErrorProperty = new SimpleStringProperty(sendMessageError); ackErrorProperty = new SimpleStringProperty(ackError); notifyChangeListener(); } // We cannot rename protobuf definition because it would break backward compatibility @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { protobuf.ChatMessage.Builder builder = protobuf.ChatMessage.newBuilder() .setType(SupportType.toProtoMessage(supportType)) .setTradeId(tradeId) .setTraderId(traderId) .setSenderIsTrader(senderIsTrader) .setMessage(message) .addAllAttachments(attachments.stream().map(Attachment::toProtoMessage).collect(Collectors.toList())) .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setDate(date) .setArrived(arrivedProperty.get()) .setStoredInMailbox(storedInMailboxProperty.get()) .setIsSystemMessage(isSystemMessage) .setUid(uid) .setAcknowledged(acknowledgedProperty.get()) .setWasDisplayed(wasDisplayed); Optional.ofNullable(sendMessageErrorProperty.get()).ifPresent(builder::setSendMessageError); Optional.ofNullable(ackErrorProperty.get()).ifPresent(builder::setAckError); return getNetworkEnvelopeBuilder() .setChatMessage(builder) .build(); } // The protobuf definition ChatMessage cannot be changed as it would break backward compatibility. public static ChatMessage fromProto(protobuf.ChatMessage proto, String messageVersion) { // If we get a msg from an old client type will be ordinal 0 which is the dispute entry and as we only added // the trade case it is the desired behaviour. final ChatMessage chatMessage = new ChatMessage( SupportType.fromProto(proto.getType()), proto.getTradeId(), proto.getTraderId(), proto.getSenderIsTrader(), proto.getMessage(), new ArrayList<>(proto.getAttachmentsList().stream().map(Attachment::fromProto).collect(Collectors.toList())), NodeAddress.fromProto(proto.getSenderNodeAddress()), proto.getDate(), proto.getArrived(), proto.getStoredInMailbox(), proto.getUid(), messageVersion, proto.getAcknowledged(), proto.getSendMessageError().isEmpty() ? null : proto.getSendMessageError(), proto.getAckError().isEmpty() ? null : proto.getAckError(), proto.getWasDisplayed()); chatMessage.setSystemMessage(proto.getIsSystemMessage()); return chatMessage; } public static ChatMessage fromPayloadProto(protobuf.ChatMessage proto) { // We have the case that an envelope got wrapped into a payload. // We don't check the message version here as it was checked in the carrier envelope already (in connection class) // Payloads don't have a message version and are also used for persistence // We set the value to -1 to indicate it is set but irrelevant return fromProto(proto, "-1"); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// private void addAllAttachments(List attachments) { this.attachments.addAll(attachments); } public void setArrived(@SuppressWarnings("SameParameterValue") boolean arrived) { this.arrivedProperty.set(arrived); notifyChangeListener(); } public ReadOnlyBooleanProperty arrivedProperty() { return arrivedProperty; } public void setStoredInMailbox(@SuppressWarnings("SameParameterValue") boolean storedInMailbox) { this.storedInMailboxProperty.set(storedInMailbox); notifyChangeListener(); } public ReadOnlyBooleanProperty storedInMailboxProperty() { return storedInMailboxProperty; } public void setAcknowledged(boolean acknowledged) { this.acknowledgedProperty.set(acknowledged); notifyChangeListener(); } // each chat message notifies the user if an ACK is not received in time public void startAckTimer() { UserThread.runAfter(() -> { if (!this.getAcknowledgedProperty().get() && !this.getStoredInMailboxProperty().get()) { this.setArrived(false); this.setAckError(Res.get("support.errorTimeout")); } }, 60, TimeUnit.SECONDS); } public ReadOnlyBooleanProperty acknowledgedProperty() { return acknowledgedProperty; } public void setSendMessageError(String sendMessageError) { this.sendMessageErrorProperty.set(sendMessageError); notifyChangeListener(); } public ReadOnlyStringProperty sendMessageErrorProperty() { return sendMessageErrorProperty; } public void setAckError(String ackError) { this.ackErrorProperty.set(ackError); notifyChangeListener(); } public ReadOnlyStringProperty ackErrorProperty() { return ackErrorProperty; } @Override public String getTradeId() { return tradeId; } public String getShortId() { return Utilities.getShortId(tradeId); } public void addWeakMessageStateListener(Listener listener) { this.listener = new WeakReference<>(listener); } public boolean isResultMessage(Dispute dispute) { DisputeResult disputeResult = dispute.getDisputeResultProperty().get(); if (disputeResult == null) { return false; } ChatMessage resultChatMessage = disputeResult.getChatMessage(); return resultChatMessage != null && resultChatMessage.getUid().equals(uid); } @Override public long getTTL() { return TTL; } private void notifyChangeListener() { if (listener != null) { Listener listener = this.listener.get(); if (listener != null) { listener.onMessageStateChanged(); } } } @Override public String toString() { return "ChatMessage{" + "\n tradeId='" + tradeId + '\'' + ",\n traderId=" + traderId + ",\n senderIsTrader=" + senderIsTrader + ",\n message='" + message + '\'' + ",\n attachments=" + attachments + ",\n senderNodeAddress=" + senderNodeAddress + ",\n date=" + date + ",\n isSystemMessage=" + isSystemMessage + ",\n wasDisplayed=" + wasDisplayed + ",\n arrivedProperty=" + arrivedProperty + ",\n storedInMailboxProperty=" + storedInMailboxProperty + ",\n acknowledgedProperty=" + acknowledgedProperty + ",\n sendMessageErrorProperty=" + sendMessageErrorProperty + ",\n ackErrorProperty=" + ackErrorProperty + "\n} " + super.toString(); } } ================================================ FILE: core/src/main/java/haveno/core/support/messages/SupportMessage.java ================================================ /* * 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 . */ package haveno.core.support.messages; import haveno.common.proto.network.NetworkEnvelope; import haveno.core.support.SupportType; import haveno.network.p2p.UidMessage; import haveno.network.p2p.mailbox.MailboxMessage; import lombok.EqualsAndHashCode; import lombok.Getter; @EqualsAndHashCode(callSuper = true) @Getter public abstract class SupportMessage extends NetworkEnvelope implements MailboxMessage, UidMessage { protected final String uid; // Added with v1.1.6. Old clients will not have set that field and we fall back to entry 0 which is ARBITRATION. protected final SupportType supportType; public SupportMessage(String messageVersion, String uid, SupportType supportType) { super(messageVersion); this.uid = uid; this.supportType = supportType; } public abstract String getTradeId(); @Override public String toString() { return "DisputeMessage{" + "\n uid='" + uid + '\'' + ",\n messageVersion=" + messageVersion + ",\n supportType=" + supportType + "\n} " + super.toString(); } } ================================================ FILE: core/src/main/java/haveno/core/support/traderchat/TradeChatSession.java ================================================ /* * 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 . */ package haveno.core.support.traderchat; import haveno.core.support.SupportSession; import haveno.core.support.messages.ChatMessage; import haveno.core.trade.Trade; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; @Slf4j public class TradeChatSession extends SupportSession { @Nullable private Trade trade; public TradeChatSession(@Nullable Trade trade, boolean isClient) { super(isClient); this.trade = trade; } @Override public String getTradeId() { return trade != null ? trade.getId() : ""; } @Override public int getClientId() { // TODO remove that client-server concept for trade chat // Get pubKeyRing of taker. Maker is considered server for chat sessions try { return trade.getContract().getTakerPubKeyRing().hashCode(); } catch (NullPointerException e) { log.warn("Unable to get takerPubKeyRing from Trade Contract - {}", e.toString()); } return 0; } @Override public ObservableList getObservableChatMessageList() { return trade != null ? trade.getChatMessages() : FXCollections.observableArrayList(); } @Override public boolean chatIsOpen() { return trade != null && !trade.isCompleted(); } @Override public boolean isDisputeAgent() { return false; } } ================================================ FILE: core/src/main/java/haveno/core/support/traderchat/TraderChatManager.java ================================================ /* * 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 . */ package haveno.core.support.traderchat; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.crypto.PubKeyRing; import haveno.common.crypto.PubKeyRingProvider; import haveno.core.api.CoreNotificationService; import haveno.core.api.XmrConnectionService; import haveno.core.locale.Res; import haveno.core.support.SupportManager; import haveno.core.support.SupportType; import haveno.core.support.messages.ChatMessage; import haveno.core.support.messages.SupportMessage; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.AckMessageSourceType; import haveno.network.p2p.BootstrapListener; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import java.util.List; import java.util.Optional; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton public class TraderChatManager extends SupportManager { private final PubKeyRingProvider pubKeyRingProvider; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public TraderChatManager(P2PService p2PService, XmrConnectionService xmrConnectionService, XmrWalletService xmrWalletService, CoreNotificationService notificationService, TradeManager tradeManager, PubKeyRingProvider pubKeyRingProvider) { super(p2PService, xmrConnectionService, xmrWalletService, notificationService, tradeManager); this.pubKeyRingProvider = pubKeyRingProvider; } /////////////////////////////////////////////////////////////////////////////////////////// // Implement template methods /////////////////////////////////////////////////////////////////////////////////////////// @Override public SupportType getSupportType() { return SupportType.TRADE; } @Override public void requestPersistence() { tradeManager.requestPersistence(); } @Override public void persistNow(Runnable completeHandler) { tradeManager.persistNow(completeHandler); } @Override public NodeAddress getPeerNodeAddress(ChatMessage message) { return tradeManager.getOpenTrade(message.getTradeId()).map(trade -> { if (trade.getContract() != null) { return trade.getContract().getPeersNodeAddress(pubKeyRingProvider.get()); } else { return null; } }).orElse(null); } @Override public PubKeyRing getPeerPubKeyRing(ChatMessage message) { return tradeManager.getOpenTrade(message.getTradeId()).map(trade -> { if (trade.getContract() != null) { return trade.getContract().getPeersPubKeyRing(pubKeyRingProvider.get()); } else { return null; } }).orElse(null); } @Override public List getAllChatMessages(String tradeId) { return Optional.of(tradeManager.getTrade(tradeId)).map(Trade::getChatMessages) .orElse(FXCollections.emptyObservableList()); } @Override public boolean channelOpen(ChatMessage message) { return tradeManager.getOpenTrade(message.getTradeId()).isPresent(); } @Override public void addAndPersistChatMessage(ChatMessage message) { tradeManager.getOpenTrade(message.getTradeId()).ifPresent(trade -> { ObservableList chatMessages = trade.getChatMessages(); if (chatMessages.stream().noneMatch(m -> m.getUid().equals(message.getUid()))) { if (chatMessages.isEmpty()) { addSystemMsg(trade); } trade.addAndPersistChatMessage(message); tradeManager.requestPersistence(); } else { log.warn("Trade got a chatMessage that we have already stored. UId = {} TradeId = {}", message.getUid(), message.getTradeId()); } }); } @Override protected AckMessageSourceType getAckMessageSourceType() { return AckMessageSourceType.TRADE_CHAT_MESSAGE; } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onAllServicesInitialized() { super.onAllServicesInitialized(); p2PService.addP2PServiceListener(new BootstrapListener() { @Override public void onDataReceived() { tryApplyMessages(); } }); xmrWalletService.downloadPercentageProperty().addListener((observable, oldValue, newValue) -> { if (xmrWalletService.isSyncedWithinTolerance()) tryApplyMessages(); }); tryApplyMessages(); } @Override public void onSupportMessage(SupportMessage message) { if (canProcessMessage(message)) { log.info("Received {} with tradeId {} and uid {}", message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); if (message instanceof ChatMessage) { handle((ChatMessage) message); } else { log.warn("Unsupported message at dispatchMessage. message={}", message); } } } public void addSystemMsg(Trade trade) { // We need to use the trade date as otherwise our system msg would not be displayed first as the list is sorted // by date. ChatMessage chatMessage = new ChatMessage( getSupportType(), trade.getId(), 0, false, Res.get("tradeChat.rules"), new NodeAddress("null:0000"), trade.getDate().getTime()); chatMessage.setSystemMessage(true); trade.getChatMessages().add(chatMessage); requestPersistence(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/ArbitratorTrade.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade; import haveno.common.proto.ProtoUtil; import haveno.core.offer.Offer; import haveno.core.proto.CoreProtoResolver; import haveno.core.trade.protocol.ProcessModel; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; import java.math.BigInteger; import java.util.Date; import java.util.UUID; import javax.annotation.Nullable; /** * Trade in the context of an arbitrator. */ @Slf4j public class ArbitratorTrade extends Trade { private static final long resendDisputeOpenedMessageDurationMs = 1L * 30 * 24 * 60 * 60 * 1000; // 30 days public ArbitratorTrade(Offer offer, BigInteger tradeAmount, long tradePrice, XmrWalletService xmrWalletService, ProcessModel processModel, String uid, NodeAddress makerNodeAddress, NodeAddress takerNodeAddress, NodeAddress arbitratorNodeAddress, @Nullable String challenge) { super(offer, tradeAmount, tradePrice, xmrWalletService, processModel, uid, makerNodeAddress, takerNodeAddress, arbitratorNodeAddress, challenge); } @Override public BigInteger getPayoutAmountBeforeCost() { throw new RuntimeException("Arbitrator does not have a payout amount"); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.Tradable toProtoMessage() { return protobuf.Tradable.newBuilder() .setArbitratorTrade(protobuf.ArbitratorTrade.newBuilder() .setTrade((protobuf.Trade) super.toProtoMessage())) .build(); } public static Tradable fromProto(protobuf.ArbitratorTrade arbitratorTradeProto, XmrWalletService xmrWalletService, CoreProtoResolver coreProtoResolver) { protobuf.Trade proto = arbitratorTradeProto.getTrade(); ProcessModel processModel = ProcessModel.fromProto(proto.getProcessModel(), coreProtoResolver); String uid = ProtoUtil.stringOrNullFromProto(proto.getUid()); if (uid == null) { uid = UUID.randomUUID().toString(); } return fromProto(new ArbitratorTrade( Offer.fromProto(proto.getOffer()), BigInteger.valueOf(proto.getAmount()), proto.getPrice(), xmrWalletService, processModel, uid, proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null, proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null, proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null, ProtoUtil.stringOrNullFromProto(proto.getChallenge())), proto, coreProtoResolver); } @Override public boolean confirmPermitted() { throw new RuntimeException("ArbitratorTrade.confirmPermitted() not implemented"); // TODO (woodser): implement } public boolean resendDisputeOpenedMessageWithinDuration() { Date startDate = getMaxTradePeriodDate(); return new Date().getTime() <= (startDate.getTime() + resendDisputeOpenedMessageDurationMs); } } ================================================ FILE: core/src/main/java/haveno/core/trade/BuyerAsMakerTrade.java ================================================ /* * 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 . */ package haveno.core.trade; import haveno.common.proto.ProtoUtil; import haveno.core.offer.Offer; import haveno.core.proto.CoreProtoResolver; import haveno.core.trade.protocol.ProcessModel; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; import java.math.BigInteger; import java.util.UUID; import javax.annotation.Nullable; @Slf4j public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade { /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, initialization /////////////////////////////////////////////////////////////////////////////////////////// public BuyerAsMakerTrade(Offer offer, BigInteger tradeAmount, long tradePrice, XmrWalletService xmrWalletService, ProcessModel processModel, String uid, NodeAddress makerNodeAddress, NodeAddress takerNodeAddress, NodeAddress arbitratorNodeAddress, @Nullable String challenge) { super(offer, tradeAmount, tradePrice, xmrWalletService, processModel, uid, makerNodeAddress, takerNodeAddress, arbitratorNodeAddress, challenge); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.Tradable toProtoMessage() { return protobuf.Tradable.newBuilder() .setBuyerAsMakerTrade(protobuf.BuyerAsMakerTrade.newBuilder() .setTrade((protobuf.Trade) super.toProtoMessage())) .build(); } public static Tradable fromProto(protobuf.BuyerAsMakerTrade buyerAsMakerTradeProto, XmrWalletService xmrWalletService, CoreProtoResolver coreProtoResolver) { protobuf.Trade proto = buyerAsMakerTradeProto.getTrade(); ProcessModel processModel = ProcessModel.fromProto(proto.getProcessModel(), coreProtoResolver); String uid = ProtoUtil.stringOrNullFromProto(proto.getUid()); if (uid == null) { uid = UUID.randomUUID().toString(); } BuyerAsMakerTrade trade = new BuyerAsMakerTrade( Offer.fromProto(proto.getOffer()), BigInteger.valueOf(proto.getAmount()), proto.getPrice(), xmrWalletService, processModel, uid, proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null, proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null, proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null, ProtoUtil.stringOrNullFromProto(proto.getChallenge())); trade.setPrice(proto.getPrice()); return fromProto(trade, proto, coreProtoResolver); } } ================================================ FILE: core/src/main/java/haveno/core/trade/BuyerAsTakerTrade.java ================================================ /* * 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 . */ package haveno.core.trade; import haveno.common.proto.ProtoUtil; import haveno.core.offer.Offer; import haveno.core.proto.CoreProtoResolver; import haveno.core.trade.protocol.ProcessModel; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.math.BigInteger; import java.util.UUID; @Slf4j public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade { /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, initialization /////////////////////////////////////////////////////////////////////////////////////////// public BuyerAsTakerTrade(Offer offer, BigInteger tradeAmount, long tradePrice, XmrWalletService xmrWalletService, ProcessModel processModel, String uid, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress takerNodeAddress, @Nullable NodeAddress arbitratorNodeAddress, @Nullable String challenge) { super(offer, tradeAmount, tradePrice, xmrWalletService, processModel, uid, makerNodeAddress, takerNodeAddress, arbitratorNodeAddress, challenge); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.Tradable toProtoMessage() { return protobuf.Tradable.newBuilder() .setBuyerAsTakerTrade(protobuf.BuyerAsTakerTrade.newBuilder() .setTrade((protobuf.Trade) super.toProtoMessage())) .build(); } public static Tradable fromProto(protobuf.BuyerAsTakerTrade buyerAsTakerTradeProto, XmrWalletService xmrWalletService, CoreProtoResolver coreProtoResolver) { protobuf.Trade proto = buyerAsTakerTradeProto.getTrade(); ProcessModel processModel = ProcessModel.fromProto(proto.getProcessModel(), coreProtoResolver); String uid = ProtoUtil.stringOrNullFromProto(proto.getUid()); if (uid == null) { uid = UUID.randomUUID().toString(); } return fromProto(new BuyerAsTakerTrade( Offer.fromProto(proto.getOffer()), BigInteger.valueOf(proto.getAmount()), proto.getPrice(), xmrWalletService, processModel, uid, proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null, proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null, proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null, ProtoUtil.stringOrNullFromProto(proto.getChallenge())), proto, coreProtoResolver); } } ================================================ FILE: core/src/main/java/haveno/core/trade/BuyerTrade.java ================================================ /* * 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 . */ package haveno.core.trade; import haveno.core.offer.Offer; import haveno.core.trade.protocol.ProcessModel; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.math.BigInteger; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public abstract class BuyerTrade extends Trade { BuyerTrade(Offer offer, BigInteger tradeAmount, long tradePrice, XmrWalletService xmrWalletService, ProcessModel processModel, String uid, @Nullable NodeAddress takerNodeAddress, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress arbitratorNodeAddress, @Nullable String challenge) { super(offer, tradeAmount, tradePrice, xmrWalletService, processModel, uid, takerNodeAddress, makerNodeAddress, arbitratorNodeAddress, challenge); } @Override public BigInteger getPayoutAmountBeforeCost() { checkNotNull(getAmount(), "Invalid state: getTradeAmount() = null"); return getAmount().add(getBuyerSecurityDepositBeforeMiningFee()); } @Override public boolean confirmPermitted() { return true; } } ================================================ FILE: core/src/main/java/haveno/core/trade/CleanupMailboxMessages.java ================================================ /* * 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 . */ package haveno.core.trade; import com.google.inject.Inject; import haveno.common.crypto.PubKeyRing; import haveno.common.proto.network.NetworkEnvelope; import haveno.core.trade.messages.TradeMessage; import haveno.network.p2p.AckMessage; import haveno.network.p2p.AckMessageSourceType; import haveno.network.p2p.BootstrapListener; import haveno.network.p2p.DecryptedMessageWithPubKey; import haveno.network.p2p.P2PService; import haveno.network.p2p.mailbox.MailboxMessage; import haveno.network.p2p.mailbox.MailboxMessageService; 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 * is closed already and the TradeProtocol is not executing the message processing, thus the mailbox message would not * be removed. To ensure that in such cases (as well other potential cases in failure scenarios) the mailbox message * gets removed from the network we use that util. * * This class must not be injected as a singleton! */ @Slf4j public class CleanupMailboxMessages { private final P2PService p2PService; private final MailboxMessageService mailboxMessageService; @Inject public CleanupMailboxMessages(P2PService p2PService, MailboxMessageService mailboxMessageService) { this.p2PService = p2PService; this.mailboxMessageService = mailboxMessageService; } public void handleTrades(List trades) { synchronized (trades) { // We wrap in a try catch as in failed trades we cannot be sure if expected data is set, so we could get // a NullPointer and do not want that this escalate to the user. try { if (p2PService.isBootstrapped()) { cleanupMailboxMessages(trades); } else { p2PService.addP2PServiceListener(new BootstrapListener() { @Override public void onDataReceived() { cleanupMailboxMessages(trades); } }); } } catch (Throwable t) { log.error("Cleanup mailbox messages failed. {}", t.toString()); } } } private void cleanupMailboxMessages(List trades) { mailboxMessageService.getMyDecryptedMailboxMessages() .forEach(message -> handleDecryptedMessageWithPubKey(message, trades)); } private void handleDecryptedMessageWithPubKey(DecryptedMessageWithPubKey decryptedMessageWithPubKey, List trades) { trades.stream() .filter(trade -> isMessageForTrade(decryptedMessageWithPubKey, trade)) .filter(trade -> isPubKeyValid(decryptedMessageWithPubKey, trade)) .filter(trade -> decryptedMessageWithPubKey.getNetworkEnvelope() instanceof MailboxMessage) .forEach(trade -> removeEntryFromMailbox((MailboxMessage) decryptedMessageWithPubKey.getNetworkEnvelope(), trade)); } private boolean isMessageForTrade(DecryptedMessageWithPubKey decryptedMessageWithPubKey, Trade trade) { NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); if (networkEnvelope instanceof TradeMessage) { return isMyMessage((TradeMessage) networkEnvelope, trade); } else if (networkEnvelope instanceof AckMessage) { return isMyMessage((AckMessage) networkEnvelope, trade); } // Instance must be TradeMessage or AckMessage. return false; } private void removeEntryFromMailbox(MailboxMessage mailboxMessage, Trade trade) { log.info("We found a pending mailbox message ({}) for trade {}. " + "As the trade is closed we remove the mailbox message.", mailboxMessage.getClass().getSimpleName(), trade.getId()); mailboxMessageService.removeMailboxMsg(mailboxMessage); } private boolean isMyMessage(TradeMessage message, Trade trade) { return message.getOfferId().equals(trade.getId()); } private boolean isMyMessage(AckMessage ackMessage, Trade trade) { return ackMessage.getSourceType() == AckMessageSourceType.TRADE_MESSAGE && ackMessage.getSourceId().equals(trade.getId()); } private boolean isPubKeyValid(DecryptedMessageWithPubKey decryptedMessageWithPubKey, Trade trade) { // We can only validate the peers pubKey if we have it already. If we are the taker we get it from the offer // Otherwise it depends on the state of the trade protocol if we have received the peers pubKeyRing already. PubKeyRing peersPubKeyRing = trade.getTradePeer().getPubKeyRing(); boolean isValid = true; if (peersPubKeyRing != null && !decryptedMessageWithPubKey.getSignaturePubKey().equals(peersPubKeyRing.getSignaturePubKey())) { isValid = false; log.warn("SignaturePubKey in decryptedMessageWithPubKey does not match the SignaturePubKey we have set for our trading peer."); } return isValid; } } ================================================ FILE: core/src/main/java/haveno/core/trade/CleanupMailboxMessagesService.java ================================================ /* * 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 . */ package haveno.core.trade; import com.google.inject.Inject; import haveno.common.proto.network.NetworkEnvelope; import haveno.core.trade.messages.TradeMessage; import haveno.network.p2p.AckMessage; import haveno.network.p2p.AckMessageSourceType; import haveno.network.p2p.BootstrapListener; import haveno.network.p2p.DecryptedMessageWithPubKey; import haveno.network.p2p.P2PService; import haveno.network.p2p.mailbox.MailboxMessage; import haveno.network.p2p.mailbox.MailboxMessageService; 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 * is closed already and the TradeProtocol is not executing the message processing, thus the mailbox message would not * be removed. To ensure that in such cases (as well other potential cases in failure scenarios) the mailbox message * gets removed from the network we use that util. * * This class must not be injected as a singleton! */ @Slf4j public class CleanupMailboxMessagesService { private final P2PService p2PService; private final MailboxMessageService mailboxMessageService; @Inject public CleanupMailboxMessagesService(P2PService p2PService, MailboxMessageService mailboxMessageService) { this.p2PService = p2PService; this.mailboxMessageService = mailboxMessageService; } public void handleTrades(List trades) { // We wrap in a try catch as in failed trades we cannot be sure if expected data is set, so we could get // a NullPointer and do not want that this escalate to the user. try { if (p2PService.isBootstrapped()) { cleanupMailboxMessages(trades); } else { p2PService.addP2PServiceListener(new BootstrapListener() { @Override public void onDataReceived() { cleanupMailboxMessages(trades); } }); } } catch (Throwable t) { log.error("Cleanup mailbox messages failed. {}", t.toString()); } } private void cleanupMailboxMessages(List trades) { mailboxMessageService.getMyDecryptedMailboxMessages() .forEach(message -> handleDecryptedMessageWithPubKey(message, trades)); } private void handleDecryptedMessageWithPubKey(DecryptedMessageWithPubKey decryptedMessageWithPubKey, List trades) { trades.stream() .filter(trade -> isMessageForTrade(decryptedMessageWithPubKey, trade)) .filter(trade -> isPubKeyValid(decryptedMessageWithPubKey, trade)) .filter(trade -> decryptedMessageWithPubKey.getNetworkEnvelope() instanceof MailboxMessage) .forEach(trade -> removeEntryFromMailbox((MailboxMessage) decryptedMessageWithPubKey.getNetworkEnvelope(), trade)); } private boolean isMessageForTrade(DecryptedMessageWithPubKey decryptedMessageWithPubKey, Trade trade) { NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); if (networkEnvelope instanceof TradeMessage) { return isMyMessage((TradeMessage) networkEnvelope, trade); } else if (networkEnvelope instanceof AckMessage) { return isMyMessage((AckMessage) networkEnvelope, trade); } // Instance must be TradeMessage or AckMessage. return false; } private void removeEntryFromMailbox(MailboxMessage mailboxMessage, Trade trade) { log.info("We found a pending mailbox message ({}) for trade {}. " + "As the trade is closed we remove the mailbox message.", mailboxMessage.getClass().getSimpleName(), trade.getId()); mailboxMessageService.removeMailboxMsg(mailboxMessage); } private boolean isMyMessage(TradeMessage message, Trade trade) { return message.getOfferId().equals(trade.getId()); } private boolean isMyMessage(AckMessage ackMessage, Trade trade) { return ackMessage.getSourceType() == AckMessageSourceType.TRADE_MESSAGE && ackMessage.getSourceId().equals(trade.getId()); } private boolean isPubKeyValid(DecryptedMessageWithPubKey decryptedMessageWithPubKey, Trade trade) { return trade.getProtocol().isPubKeyValid(decryptedMessageWithPubKey); } } ================================================ FILE: core/src/main/java/haveno/core/trade/ClosedTradableFormatter.java ================================================ /* * 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 . */ package haveno.core.trade; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.TraditionalMoney; import haveno.core.monetary.Volume; import haveno.core.offer.OpenOffer; import static haveno.core.trade.ClosedTradableUtil.castToTrade; import static haveno.core.trade.ClosedTradableUtil.getTotalTxFee; import static haveno.core.trade.ClosedTradableUtil.getTotalVolumeByCurrency; import static haveno.core.trade.ClosedTradableUtil.isHavenoV1Trade; import static haveno.core.trade.ClosedTradableUtil.isOpenOffer; import static haveno.core.trade.Trade.DisputeState.DISPUTE_CLOSED; import static haveno.core.trade.Trade.DisputeState.MEDIATION_CLOSED; import static haveno.core.trade.Trade.DisputeState.REFUND_REQUEST_CLOSED; import haveno.core.util.FormattingUtils; import static haveno.core.util.FormattingUtils.formatPercentagePrice; import static haveno.core.util.FormattingUtils.formatToPercentWithSymbol; import static haveno.core.util.VolumeUtil.formatVolume; import static haveno.core.util.VolumeUtil.formatVolumeWithCode; import java.math.BigInteger; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.Monetary; @Slf4j @Singleton public class ClosedTradableFormatter { // Resource bundle i18n keys with Desktop UI specific property names, // having "generic-enough" property values to be referenced in the core layer. private static final String I18N_KEY_TOTAL_AMOUNT = "closedTradesSummaryWindow.totalAmount.value"; private static final String I18N_KEY_TOTAL_TX_FEE = "closedTradesSummaryWindow.totalMinerFee.value"; private static final String I18N_KEY_TOTAL_TRADE_FEE_BTC = "closedTradesSummaryWindow.totalTradeFeeInXmr.value"; private final ClosedTradableManager closedTradableManager; @Inject public ClosedTradableFormatter(ClosedTradableManager closedTradableManager) { this.closedTradableManager = closedTradableManager; } public String getAmountAsString(Tradable tradable) { return tradable.getOptionalAmount().map(HavenoUtils::formatXmr).orElse(""); } public String getTotalAmountWithVolumeAsString(BigInteger totalTradeAmount, Volume volume) { return Res.get(I18N_KEY_TOTAL_AMOUNT, HavenoUtils.formatXmr(totalTradeAmount, true), formatVolumeWithCode(volume)); } public String getTotalTxFeeAsString(Tradable tradable) { return HavenoUtils.formatXmr(getTotalTxFee(tradable)); } public String getTotalTxFeeAsString(BigInteger totalTradeAmount, BigInteger totalTxFee) { double percentage = totalTradeAmount.equals(BigInteger.ZERO) ? 0 : HavenoUtils.divide(totalTxFee, totalTradeAmount); return Res.get(I18N_KEY_TOTAL_TX_FEE, HavenoUtils.formatXmr(totalTxFee, true), formatToPercentWithSymbol(percentage)); } public String getBuyerSecurityDepositAsString(Tradable tradable) { if (tradable instanceof Trade) { Trade trade = castToTrade(tradable); return HavenoUtils.formatXmr(trade.getBuyerSecurityDepositBeforeMiningFee()); } return HavenoUtils.formatXmr(tradable.getOffer().getMaxBuyerSecurityDeposit()); } public String getSellerSecurityDepositAsString(Tradable tradable) { if (tradable instanceof Trade) { Trade trade = castToTrade(tradable); return HavenoUtils.formatXmr(trade.getSellerSecurityDepositBeforeMiningFee()); } return HavenoUtils.formatXmr(tradable.getOffer().getMaxSellerSecurityDeposit()); } public String getTradeFeeAsString(Tradable tradable, boolean appendCode) { BigInteger tradeFee = closedTradableManager.getXmrTradeFee(tradable); return HavenoUtils.formatXmr(tradeFee, appendCode); } public String getTotalTradeFeeAsString(BigInteger totalTradeAmount, BigInteger totalTradeFee) { double percentage = totalTradeAmount.equals(BigInteger.ZERO) ? 0 : HavenoUtils.divide(totalTradeFee, totalTradeAmount); return Res.get(I18N_KEY_TOTAL_TRADE_FEE_BTC, HavenoUtils.formatXmr(totalTradeFee, true), formatToPercentWithSymbol(percentage)); } public String getPriceDeviationAsString(Tradable tradable) { if (tradable.getOffer().isUseMarketBasedPrice()) { return formatPercentagePrice(tradable.getOffer().getMarketPriceMarginPct()); } else { return Res.get("shared.na"); } } public String getVolumeAsString(Tradable tradable, boolean appendCode) { return tradable.getOptionalVolume().map(volume -> formatVolume(volume, appendCode)).orElse(""); } public String getVolumeCurrencyAsString(Tradable tradable) { return tradable.getOptionalVolume().map(Volume::getCurrencyCode).orElse(""); } public String getPriceAsString(Tradable tradable) { return tradable.getOptionalPrice().map(FormattingUtils::formatPrice).orElse(""); } public Map getTotalVolumeByCurrencyAsString(List tradableList) { return getTotalVolumeByCurrency(tradableList).entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, entry -> { String currencyCode = entry.getKey(); Monetary monetary; if (CurrencyUtil.isCryptoCurrency(currencyCode)) { monetary = CryptoMoney.valueOf(currencyCode, entry.getValue()); } else { monetary = TraditionalMoney.valueOf(currencyCode, entry.getValue()); } return formatVolumeWithCode(new Volume(monetary)); } )); } public String getStateAsString(Tradable tradable) { if (tradable == null) { return ""; } if (isHavenoV1Trade(tradable)) { Trade trade = castToTrade(tradable); if (trade.isCompleted() || trade.isPayoutPublished()) { return Res.get("portfolio.closed.completed"); } else if (trade.getDisputeState() == DISPUTE_CLOSED) { return Res.get("portfolio.closed.ticketClosed"); } else if (trade.getDisputeState() == MEDIATION_CLOSED) { return Res.get("portfolio.closed.mediationTicketClosed"); } else if (trade.getDisputeState() == REFUND_REQUEST_CLOSED) { return Res.get("portfolio.closed.ticketClosed"); } else { log.error("That must not happen. We got a pending state but we are in" + " the closed trades list. state={}", trade.getState().name()); return Res.get("shared.na"); } } else if (isOpenOffer(tradable)) { OpenOffer.State state = ((OpenOffer) tradable).getState(); log.trace("OpenOffer state={}", state); switch (state) { case AVAILABLE: case RESERVED: case CLOSED: case DEACTIVATED: log.error("Invalid state {}", state); return state.name(); case CANCELED: return Res.get("portfolio.closed.canceled"); default: log.error("Unhandled state {}", state); return state.name(); } } return Res.get("shared.na"); } } ================================================ FILE: core/src/main/java/haveno/core/trade/ClosedTradableManager.java ================================================ /* * 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 . */ package haveno.core.trade; import com.google.common.collect.ImmutableList; import com.google.inject.Inject; import haveno.common.crypto.KeyRing; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.persistable.PersistedDataHost; import haveno.core.offer.Offer; import haveno.core.offer.OpenOffer; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.Preferences; import haveno.network.p2p.NodeAddress; import javafx.collections.ObservableList; import lombok.extern.slf4j.Slf4j; import java.math.BigInteger; import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; import static haveno.core.offer.OpenOffer.State.CANCELED; import static haveno.core.trade.ClosedTradableUtil.castToTradeModel; import static haveno.core.trade.ClosedTradableUtil.isOpenOffer; /** * Manages closed trades or offers. * BsqSwap trades are once confirmed moved in the closed trades domain as well. * We do not manage the persistence of BsqSwap trades here but in BsqSwapTradeManager. */ @Slf4j public class ClosedTradableManager implements PersistedDataHost { private final KeyRing keyRing; private final PriceFeedService priceFeedService; private final Preferences preferences; private final TradeStatisticsManager tradeStatisticsManager; private final PersistenceManager> persistenceManager; private final CleanupMailboxMessagesService cleanupMailboxMessagesService; private final TradableList closedTradables = new TradableList<>(); @Inject public ClosedTradableManager(KeyRing keyRing, PriceFeedService priceFeedService, Preferences preferences, TradeStatisticsManager tradeStatisticsManager, PersistenceManager> persistenceManager, CleanupMailboxMessagesService cleanupMailboxMessagesService) { this.keyRing = keyRing; this.priceFeedService = priceFeedService; this.preferences = preferences; this.tradeStatisticsManager = tradeStatisticsManager; this.cleanupMailboxMessagesService = cleanupMailboxMessagesService; this.persistenceManager = persistenceManager; this.persistenceManager.initialize(closedTradables, "ClosedTrades", PersistenceManager.Source.PRIVATE); } @Override public void readPersisted(Runnable completeHandler) { persistenceManager.readPersisted(persisted -> { synchronized (persisted.getList()) { closedTradables.setAll(persisted.getList()); closedTradables.stream() .filter(tradable -> tradable.getOffer() != null) .forEach(tradable -> tradable.getOffer().setPriceFeedService(priceFeedService)); } completeHandler.run(); }, completeHandler); } public void onAllServicesInitialized() { cleanupMailboxMessagesService.handleTrades(getClosedTrades()); maybeClearSensitiveData(); } public void add(Tradable tradable) { synchronized (closedTradables.getList()) { if (closedTradables.add(tradable)) { maybeClearSensitiveData(); requestPersistence(); } } } public void remove(Tradable tradable) { synchronized (closedTradables.getList()) { if (closedTradables.remove(tradable)) { requestPersistence(); } } } public boolean wasMyOffer(Offer offer) { return offer.isMyOffer(keyRing); } public ObservableList getObservableList() { return closedTradables.getObservableList(); } public List getTradableList() { synchronized (closedTradables.getList()) { return ImmutableList.copyOf(new ArrayList<>(getObservableList())); } } public List getClosedTrades() { synchronized (closedTradables.getList()) { return ImmutableList.copyOf(getObservableList().stream() .filter(e -> e instanceof Trade) .map(e -> (Trade) e) .collect(Collectors.toList())); } } public List getCanceledOpenOffers() { synchronized (closedTradables.getList()) { return ImmutableList.copyOf(getObservableList().stream() .filter(e -> (e instanceof OpenOffer) && ((OpenOffer) e).getState().equals(CANCELED)) .map(e -> (OpenOffer) e) .collect(Collectors.toList())); } } public Optional getTradableById(String id) { synchronized (closedTradables.getList()) { return closedTradables.stream().filter(e -> e.getId().equals(id)).findFirst(); } } public Optional getTradeById(String id) { synchronized (closedTradables.getList()) { return getClosedTrades().stream().filter(e -> e.getId().equals(id)).findFirst(); } } public void maybeClearSensitiveData() { synchronized (closedTradables.getList()) { log.info("checking closed trades eligibility for having sensitive data cleared"); closedTradables.stream() .filter(e -> e instanceof Trade) .map(e -> (Trade) e) .filter(e -> canTradeHaveSensitiveDataCleared(e.getId())) .forEach(Trade::maybeClearSensitiveData); requestPersistence(); } } public boolean canTradeHaveSensitiveDataCleared(String tradeId) { Instant safeDate = getSafeDateForSensitiveDataClearing(); synchronized (closedTradables.getList()) { return closedTradables.stream() .filter(e -> e.getId().equals(tradeId)) .filter(e -> e.getDate().toInstant().isBefore(safeDate)) .count() > 0; } } public Instant getSafeDateForSensitiveDataClearing() { return Instant.ofEpochSecond(Instant.now().getEpochSecond() - TimeUnit.DAYS.toSeconds(preferences.getClearDataAfterDays())); } public Stream getTradesStreamWithFundsLockedIn() { return getClosedTrades().stream() .filter(Trade::isFundsLockedIn); } public Stream getTradeModelStream() { return getClosedTrades().stream(); } public int getNumPastTrades(Tradable tradable) { if (isOpenOffer(tradable)) { return 0; } NodeAddress addressInTrade = castToTradeModel(tradable).getTradePeerNodeAddress(); return (int) getTradeModelStream() .map(Trade::getTradePeerNodeAddress) .filter(Objects::nonNull) .filter(address -> address.equals(addressInTrade)) .count(); } public BigInteger getTotalTradeFee(List tradableList) { synchronized (tradableList) { return BigInteger.valueOf(tradableList.stream() .mapToLong(tradable -> getTradeFee(tradable).longValueExact()) .sum()); } } private BigInteger getTradeFee(Tradable tradable) { return getXmrTradeFee(tradable); } public BigInteger getXmrTradeFee(Tradable tradable) { return isMaker(tradable) ? tradable.getOptionalMakerFee().orElse(BigInteger.ZERO) : tradable.getOptionalTakerFee().orElse(BigInteger.ZERO); } public boolean isMaker(Tradable tradable) { return tradable instanceof MakerTrade || tradable.getOffer().isMyOffer(keyRing); } private void requestPersistence() { persistenceManager.requestPersistence(); } public void removeTrade(Trade trade) { synchronized (closedTradables.getList()) { if (closedTradables.remove(trade)) { requestPersistence(); } } } } ================================================ FILE: core/src/main/java/haveno/core/trade/ClosedTradableUtil.java ================================================ /* * 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 . */ package haveno.core.trade; import haveno.core.offer.OpenOffer; import java.math.BigInteger; import java.util.HashMap; import java.util.List; import java.util.Map; public class ClosedTradableUtil { public static BigInteger getTotalAmount(List tradableList) { return BigInteger.valueOf(tradableList.stream() .flatMap(tradable -> tradable.getOptionalAmount().stream()) .mapToLong(value -> value.longValueExact()) .sum()); } public static BigInteger getTotalTxFee(List tradableList) { return BigInteger.valueOf(tradableList.stream() .mapToLong(tradable -> getTotalTxFee(tradable).longValueExact()) .sum()); } public static Map getTotalVolumeByCurrency(List tradableList) { Map map = new HashMap<>(); tradableList.stream() .filter(tradable -> { if (tradable instanceof Trade) { Trade trade = castToTrade(tradable); return trade.isCompleted(); // TODO: does not consider if trade was reverted by arbitrator } else { return false; } }) .flatMap(tradable -> tradable.getOptionalVolume().stream()) .forEach(volume -> { String currencyCode = volume.getCurrencyCode(); map.putIfAbsent(currencyCode, 0L); map.put(currencyCode, volume.getValue() + map.get(currencyCode)); }); return map; } public static BigInteger getTotalTxFee(Tradable tradable) { return tradable.getTotalTxFee(); } public static boolean isOpenOffer(Tradable tradable) { return tradable instanceof OpenOffer; } public static boolean isHavenoV1Trade(Tradable tradable) { return tradable instanceof Trade; } public static Trade castToTrade(Tradable tradable) { return (Trade) tradable; } public static Trade castToTradeModel(Tradable tradable) { return (Trade) tradable; } } ================================================ FILE: core/src/main/java/haveno/core/trade/Contract.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade; import com.google.protobuf.ByteString; import haveno.common.crypto.PubKeyRing; import haveno.common.proto.ProtoUtil; import haveno.common.proto.network.NetworkPayload; import haveno.common.util.JsonExclude; import haveno.common.util.Utilities; import haveno.core.monetary.Price; import haveno.core.monetary.Volume; import haveno.core.offer.OfferPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.proto.CoreProtoResolver; import haveno.core.util.JsonUtil; import haveno.core.util.VolumeUtil; import haveno.network.p2p.NodeAddress; import lombok.Value; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import javax.annotation.Nullable; import java.math.BigInteger; import java.util.Optional; import static com.google.common.base.Preconditions.checkArgument; @Slf4j @Value public final class Contract implements NetworkPayload { private final OfferPayload offerPayload; private final long tradeAmount; private final long tradePrice; private final NodeAddress buyerNodeAddress; private final NodeAddress sellerNodeAddress; private final NodeAddress arbitratorNodeAddress; private final boolean isBuyerMakerAndSellerTaker; private final String makerAccountId; private final String takerAccountId; private final String makerPaymentMethodId; private final String takerPaymentMethodId; private final byte[] makerPaymentAccountPayloadHash; private final byte[] takerPaymentAccountPayloadHash; @JsonExclude private final PubKeyRing makerPubKeyRing; @JsonExclude private final PubKeyRing takerPubKeyRing; private final String makerPayoutAddressString; private final String takerPayoutAddressString; private final String makerDepositTxHash; @Nullable private final String takerDepositTxHash; public Contract(OfferPayload offerPayload, long tradeAmount, long tradePrice, NodeAddress buyerNodeAddress, NodeAddress sellerNodeAddress, NodeAddress arbitratorNodeAddress, boolean isBuyerMakerAndSellerTaker, String makerAccountId, String takerAccountId, String makerPaymentMethodId, String takerPaymentMethodId, byte[] makerPaymentAccountPayloadHash, byte[] takerPaymentAccountPayloadHash, PubKeyRing makerPubKeyRing, PubKeyRing takerPubKeyRing, String makerPayoutAddressString, String takerPayoutAddressString, String makerDepositTxHash, @Nullable String takerDepositTxHash) { this.offerPayload = offerPayload; this.tradeAmount = tradeAmount; this.tradePrice = tradePrice; this.buyerNodeAddress = buyerNodeAddress; this.sellerNodeAddress = sellerNodeAddress; this.arbitratorNodeAddress = arbitratorNodeAddress; this.isBuyerMakerAndSellerTaker = isBuyerMakerAndSellerTaker; this.makerAccountId = makerAccountId; this.takerAccountId = takerAccountId; this.makerPaymentMethodId = makerPaymentMethodId; this.takerPaymentMethodId = takerPaymentMethodId; this.makerPaymentAccountPayloadHash = makerPaymentAccountPayloadHash; this.takerPaymentAccountPayloadHash = takerPaymentAccountPayloadHash; this.makerPubKeyRing = makerPubKeyRing; this.takerPubKeyRing = takerPubKeyRing; this.makerPayoutAddressString = makerPayoutAddressString; this.takerPayoutAddressString = takerPayoutAddressString; this.makerDepositTxHash = makerDepositTxHash; this.takerDepositTxHash = takerDepositTxHash; // For SEPA offers we accept also SEPA_INSTANT takers // Otherwise both ids need to be the same boolean result = (makerPaymentMethodId.equals(PaymentMethod.SEPA_ID) && takerPaymentMethodId.equals(PaymentMethod.SEPA_INSTANT_ID)) || makerPaymentMethodId.equals(takerPaymentMethodId); checkArgument(result, "payment methods of maker and taker must be the same.\n" + "makerPaymentMethodId=" + makerPaymentMethodId + "\n" + "takerPaymentMethodId=" + takerPaymentMethodId); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.Contract toProtoMessage() { protobuf.Contract.Builder builder = protobuf.Contract.newBuilder() .setOfferPayload(offerPayload.toProtoMessage().getOfferPayload()) .setTradeAmount(tradeAmount) .setTradePrice(tradePrice) .setBuyerNodeAddress(buyerNodeAddress.toProtoMessage()) .setSellerNodeAddress(sellerNodeAddress.toProtoMessage()) .setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage()) .setIsBuyerMakerAndSellerTaker(isBuyerMakerAndSellerTaker) .setMakerAccountId(makerAccountId) .setTakerAccountId(takerAccountId) .setMakerPaymentMethodId(makerPaymentMethodId) .setTakerPaymentMethodId(takerPaymentMethodId) .setMakerPaymentAccountPayloadHash(ByteString.copyFrom(makerPaymentAccountPayloadHash)) .setTakerPaymentAccountPayloadHash(ByteString.copyFrom(takerPaymentAccountPayloadHash)) .setMakerPubKeyRing(makerPubKeyRing.toProtoMessage()) .setTakerPubKeyRing(takerPubKeyRing.toProtoMessage()) .setMakerPayoutAddressString(makerPayoutAddressString) .setTakerPayoutAddressString(takerPayoutAddressString) .setMakerDepositTxHash(makerDepositTxHash); Optional.ofNullable(takerDepositTxHash).ifPresent(builder::setTakerDepositTxHash); return builder.build(); } public static Contract fromProto(protobuf.Contract proto, CoreProtoResolver coreProtoResolver) { return new Contract(OfferPayload.fromProto(proto.getOfferPayload()), proto.getTradeAmount(), proto.getTradePrice(), NodeAddress.fromProto(proto.getBuyerNodeAddress()), NodeAddress.fromProto(proto.getSellerNodeAddress()), NodeAddress.fromProto(proto.getArbitratorNodeAddress()), proto.getIsBuyerMakerAndSellerTaker(), proto.getMakerAccountId(), proto.getTakerAccountId(), proto.getMakerPaymentMethodId(), proto.getTakerPaymentMethodId(), proto.getMakerPaymentAccountPayloadHash().toByteArray(), proto.getTakerPaymentAccountPayloadHash().toByteArray(), PubKeyRing.fromProto(proto.getMakerPubKeyRing()), PubKeyRing.fromProto(proto.getTakerPubKeyRing()), proto.getMakerPayoutAddressString(), proto.getTakerPayoutAddressString(), proto.getMakerDepositTxHash(), ProtoUtil.stringOrNullFromProto(proto.getTakerDepositTxHash())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public String getBuyerPayoutAddressString() { return isBuyerMakerAndSellerTaker ? makerPayoutAddressString : takerPayoutAddressString; } public String getSellerPayoutAddressString() { return isBuyerMakerAndSellerTaker ? takerPayoutAddressString : makerPayoutAddressString; } public PubKeyRing getBuyerPubKeyRing() { return isBuyerMakerAndSellerTaker ? makerPubKeyRing : takerPubKeyRing; } public PubKeyRing getSellerPubKeyRing() { return isBuyerMakerAndSellerTaker ? takerPubKeyRing : makerPubKeyRing; } public byte[] getBuyerPaymentAccountPayloadHash() { return isBuyerMakerAndSellerTaker ? makerPaymentAccountPayloadHash : takerPaymentAccountPayloadHash; } public byte[] getSellerPaymentAccountPayloadHash() { return isBuyerMakerAndSellerTaker ? takerPaymentAccountPayloadHash : makerPaymentAccountPayloadHash; } public String getPaymentMethodId() { return makerPaymentMethodId; } public BigInteger getTradeAmount() { return BigInteger.valueOf(tradeAmount); } public Volume getTradeVolume() { Volume volumeByAmount = getPrice().getVolumeByAmount(getTradeAmount()); volumeByAmount = VolumeUtil.getAdjustedVolume(volumeByAmount, getPaymentMethodId()); return volumeByAmount; } public Price getPrice() { return Price.valueOf(offerPayload.getCurrencyCode(), tradePrice); } public NodeAddress getMyNodeAddress(PubKeyRing myPubKeyRing) { if (myPubKeyRing.equals(getBuyerPubKeyRing())) return buyerNodeAddress; else return sellerNodeAddress; } public NodeAddress getPeersNodeAddress(PubKeyRing myPubKeyRing) { if (myPubKeyRing.equals(getSellerPubKeyRing())) return buyerNodeAddress; else return sellerNodeAddress; } public PubKeyRing getPeersPubKeyRing(PubKeyRing myPubKeyRing) { if (myPubKeyRing.equals(getSellerPubKeyRing())) return getBuyerPubKeyRing(); else return getSellerPubKeyRing(); } public boolean isMyRoleBuyer(PubKeyRing myPubKeyRing) { return getBuyerPubKeyRing().equals(myPubKeyRing); } public boolean isMyRoleMaker(PubKeyRing myPubKeyRing) { return isBuyerMakerAndSellerTaker() == isMyRoleBuyer(myPubKeyRing); } public boolean maybeClearSensitiveData() { return false; // nothing to clear } // edits a contract json string public static String sanitizeContractAsJson(String contractAsJson) { return contractAsJson; // nothing to sanitize because the contract does not contain the payment account payloads } public void printDiff(@Nullable String peersContractAsJson) { String json = JsonUtil.objectToJson(this); String diff = StringUtils.difference(json, peersContractAsJson); if (!diff.isEmpty()) { log.warn("Diff of both contracts: \n" + diff); log.warn("\n\n------------------------------------------------------------\n" + "Contract as json\n" + json + "\n------------------------------------------------------------\n"); log.warn("\n\n------------------------------------------------------------\n" + "Peers contract as json\n" + peersContractAsJson + "\n------------------------------------------------------------\n"); } else { log.debug("Both contracts are the same"); } } @Override public String toString() { return "Contract{" + "\n offerPayload=" + offerPayload + ",\n tradeAmount=" + tradeAmount + ",\n tradePrice=" + tradePrice + ",\n buyerNodeAddress=" + buyerNodeAddress + ",\n sellerNodeAddress=" + sellerNodeAddress + ",\n arbitratorNodeAddress=" + arbitratorNodeAddress + ",\n isBuyerMakerAndSellerTaker=" + isBuyerMakerAndSellerTaker + ",\n makerAccountId='" + makerAccountId + '\'' + ",\n takerAccountId='" + takerAccountId + '\'' + ",\n makerPaymentMethodId='" + makerPaymentMethodId + '\'' + ",\n takerPaymentMethodId='" + takerPaymentMethodId + '\'' + ",\n makerPaymentAccountPayloadHash=" + Utilities.bytesAsHexString(makerPaymentAccountPayloadHash) + ",\n takerPaymentAccountPayloadHash=" + Utilities.bytesAsHexString(takerPaymentAccountPayloadHash) + ",\n makerPubKeyRing=" + makerPubKeyRing + ",\n takerPubKeyRing=" + takerPubKeyRing + ",\n makerPayoutAddressString='" + makerPayoutAddressString + '\'' + ",\n takerPayoutAddressString='" + takerPayoutAddressString + '\'' + ",\n makerDepositTxHash='" + makerDepositTxHash + '\'' + ",\n takerDepositTxHash='" + takerDepositTxHash + '\'' + "\n}"; } } ================================================ FILE: core/src/main/java/haveno/core/trade/HavenoUtils.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade; import com.google.common.base.CaseFormat; import com.google.common.base.Charsets; import common.utils.GenUtils; import haveno.common.config.Config; import haveno.common.crypto.CryptoException; import haveno.common.crypto.Hash; import haveno.common.crypto.KeyRing; import haveno.common.crypto.PubKeyRing; import haveno.common.crypto.Sig; import haveno.common.file.FileUtil; import haveno.common.util.Base64; import haveno.common.util.Utilities; import haveno.core.api.CoreNotificationService; import haveno.core.api.CorePaymentAccountsService; import haveno.core.api.XmrConnectionService; import haveno.core.app.HavenoSetup; import haveno.core.locale.CurrencyUtil; import haveno.core.offer.OfferPayload; import haveno.core.offer.OpenOfferManager; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; import haveno.core.trade.messages.PaymentReceivedMessage; import haveno.core.trade.messages.PaymentSentMessage; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.Preferences; import haveno.core.util.JsonUtil; import haveno.core.xmr.wallet.XmrWalletBase; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.NodeAddress; import java.io.File; import java.math.BigDecimal; import java.math.BigInteger; import java.net.InetAddress; import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.security.PrivateKey; import java.security.SecureRandom; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.concurrent.CountDownLatch; import java.util.regex.Pattern; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.DataLine; import javax.sound.sampled.SourceDataLine; import lombok.extern.slf4j.Slf4j; import monero.common.MoneroRpcConnection; import monero.common.MoneroUtils; import monero.daemon.model.MoneroOutput; import monero.wallet.model.MoneroDestination; import monero.wallet.model.MoneroTxWallet; import org.bitcoinj.core.Coin; /** * Collection of utilities. */ @Slf4j public class HavenoUtils { // configure release date private static final String RELEASE_DATE = "25-05-2024 00:00:00"; // optionally set to release date of the network in format dd-mm-yyyy to impose temporary limits, etc. e.g. "25-05-2024 00:00:00" public static final int RELEASE_LIMIT_DAYS = 60; // number of days to limit sell offers to max buy limit for new accounts public static final int WARN_ON_OFFER_EXCEEDS_UNSIGNED_BUY_LIMIT_DAYS = 182; // number of days to warn if sell offer exceeds unsigned buy limit public static final int ARBITRATOR_ACK_TIMEOUT_SECONDS = 60; // configure fees public static final boolean ARBITRATOR_ASSIGNS_TRADE_FEE_ADDRESS = true; public static final double PENALTY_FEE_PCT = 0.25; // charge 25% of security deposit for penalty private static final double MAKER_FEE_PCT_CRYPTO = 0.001; private static final double TAKER_FEE_PCT_CRYPTO = 0.005; private static final double MAKER_FEE_PCT_TRADITIONAL = 0.001; private static final double TAKER_FEE_PCT_TRADITIONAL = 0.01; private static final double MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT_CRYPTO = MAKER_FEE_PCT_CRYPTO + TAKER_FEE_PCT_CRYPTO; // can customize maker's fee when no deposit from taker private static final double MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT_TRADITIONAL = MAKER_FEE_PCT_TRADITIONAL + TAKER_FEE_PCT_TRADITIONAL; public static final double MINER_FEE_TOLERANCE_FACTOR = 5.0; // miner fees must be within 5x of each other // other configuration public static final long LOG_POLL_ERROR_PERIOD_MS = 1000 * 60 * 4; // log poll errors up to once every 4 minutes public static final long LOG_MONEROD_NOT_SYNCED_WARN_PERIOD_MS = 1000 * 30; // log warnings when daemon not synced once every 30s public static final int PRIVATE_OFFER_PASSPHRASE_NUM_WORDS = 8; // number of words in a private offer passphrase public static final boolean RECOMMEND_CONFIRMATIONS_BEFORE_SENDING_PAYMENT = true; // recommend waiting additional confirmations before sending payment // synchronize requests to the daemon private static boolean SYNC_DAEMON_REQUESTS = false; // sync long requests to daemon (e.g. refresh, update pool) // TODO: performance suffers by syncing daemon requests, but otherwise we sometimes get sporadic errors? private static boolean SYNC_WALLET_REQUESTS = false; // additionally sync wallet functions to daemon (e.g. create txs) private static boolean SYNC_IMPORT_MULTISIG_REQUESTS = false; // sync import multisig requests to avoid concurrent imports private static Object DAEMON_LOCK = new Object(); private static Object IMPORT_MULTISIG_LOCK = new Object(); public static Object getDaemonLock() { return SYNC_DAEMON_REQUESTS ? DAEMON_LOCK : new Object(); } public static Object getWalletFunctionLock() { return SYNC_WALLET_REQUESTS ? getDaemonLock() : new Object(); } public static Object getImportMultisigLock() { return SYNC_IMPORT_MULTISIG_REQUESTS ? IMPORT_MULTISIG_LOCK : new Object(); } // non-configurable public static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = DecimalFormatSymbols.getInstance(Locale.US); // use the US locale as a base for all DecimalFormats (commas should be omitted from number strings) public static int XMR_SMALLEST_UNIT_EXPONENT = 12; public static final String LOOPBACK_HOST = "127.0.0.1"; // local loopback address to host Monero node public static final String LOCALHOST = "localhost"; private static final Pattern IPV4 = Pattern.compile("^((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)$"); private static final long CENTINEROS_AU_MULTIPLIER = 10000; private static final BigInteger XMR_AU_MULTIPLIER = new BigInteger("1000000000000"); public static final DecimalFormat XMR_FORMATTER = new DecimalFormat("##############0.000000000000", DECIMAL_FORMAT_SYMBOLS); public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss"); private static List bip39Words = new ArrayList(); // shared references TODO: better way to share references? public static HavenoSetup havenoSetup; public static ArbitrationManager arbitrationManager; public static XmrWalletService xmrWalletService; public static XmrConnectionService xmrConnectionService; public static OpenOfferManager openOfferManager; public static CoreNotificationService notificationService; public static CorePaymentAccountsService corePaymentAccountService; public static TradeStatisticsManager tradeStatisticsManager; public static Preferences preferences; public static boolean isSeedNode() { return havenoSetup == null; } public static boolean isDaemon() { if (isSeedNode()) return true; return havenoSetup.getCoreContext().isApiUser(); } public static boolean isAllDomainServicesInitialized() { return havenoSetup != null && havenoSetup.getAppStartupState() != null && havenoSetup.getAppStartupState().isAllDomainServicesInitialized(); } @SuppressWarnings("unused") public static Date getReleaseDate() { if (RELEASE_DATE == null) return null; return parseDate(RELEASE_DATE); } private static Date parseDate(String date) { synchronized (DATE_FORMAT) { try { return DATE_FORMAT.parse(date); } catch (Exception e) { log.error("Failed to parse date: " + date, e); throw new IllegalArgumentException(e); } } } public static boolean isReleasedWithinDays(int days) { Date releaseDate = getReleaseDate(); if (releaseDate == null) return false; Calendar calendar = Calendar.getInstance(); calendar.setTime(releaseDate); calendar.add(Calendar.DATE, days); Date releaseDatePlusDays = calendar.getTime(); return new Date().before(releaseDatePlusDays); } public static void waitFor(long waitMs) { GenUtils.waitFor(waitMs); } public static double getMakerFeePct(String currencyCode, boolean hasBuyerAsTakerWithoutDeposit) { if (CurrencyUtil.isCryptoCurrency(currencyCode)) { return hasBuyerAsTakerWithoutDeposit ? MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT_CRYPTO : MAKER_FEE_PCT_CRYPTO; } else if (CurrencyUtil.isTraditionalCurrency(currencyCode)) { return hasBuyerAsTakerWithoutDeposit ? MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT_TRADITIONAL : MAKER_FEE_PCT_TRADITIONAL; } else { throw new IllegalArgumentException("Unsupported currency code: " + currencyCode); } } public static double getTakerFeePct(String currencyCode, boolean hasBuyerAsTakerWithoutDeposit) { if (CurrencyUtil.isCryptoCurrency(currencyCode)) { return hasBuyerAsTakerWithoutDeposit ? 0d : TAKER_FEE_PCT_CRYPTO; } else if (CurrencyUtil.isTraditionalCurrency(currencyCode)) { return hasBuyerAsTakerWithoutDeposit ? 0d : TAKER_FEE_PCT_TRADITIONAL; } else { throw new IllegalArgumentException("Unsupported currency code: " + currencyCode); } } // ----------------------- CONVERSION UTILS ------------------------------- public static BigInteger coinToAtomicUnits(Coin coin) { return centinerosToAtomicUnits(coin.value); } public static BigInteger centinerosToAtomicUnits(long centineros) { return BigInteger.valueOf(centineros).multiply(BigInteger.valueOf(CENTINEROS_AU_MULTIPLIER)); } public static double centinerosToXmr(long centineros) { return atomicUnitsToXmr(centinerosToAtomicUnits(centineros)); } public static Coin centinerosToCoin(long centineros) { return atomicUnitsToCoin(centinerosToAtomicUnits(centineros)); } public static long atomicUnitsToCentineros(long atomicUnits) { return atomicUnitsToCentineros(BigInteger.valueOf(atomicUnits)); } public static long atomicUnitsToCentineros(BigInteger atomicUnits) { return atomicUnits.divide(BigInteger.valueOf(CENTINEROS_AU_MULTIPLIER)).longValueExact(); } public static Coin atomicUnitsToCoin(long atomicUnits) { return Coin.valueOf(atomicUnitsToCentineros(atomicUnits)); } public static Coin atomicUnitsToCoin(BigInteger atomicUnits) { return atomicUnitsToCoin(atomicUnits.longValueExact()); } public static double atomicUnitsToXmr(long atomicUnits) { return atomicUnitsToXmr(BigInteger.valueOf(atomicUnits)); } public static double atomicUnitsToXmr(BigInteger atomicUnits) { return MoneroUtils.atomicUnitsToXmr(atomicUnits); } public static BigInteger xmrToAtomicUnits(double xmr) { return MoneroUtils.xmrToAtomicUnits(xmr); } public static long xmrToCentineros(double xmr) { return atomicUnitsToCentineros(xmrToAtomicUnits(xmr)); } public static double coinToXmr(Coin coin) { return atomicUnitsToXmr(coinToAtomicUnits(coin)); } public static double divide(BigInteger auDividend, BigInteger auDivisor) { return MoneroUtils.divide(auDividend, auDivisor); } public static BigInteger multiply(BigInteger amount1, double amount2) { return MoneroUtils.multiply(amount1, amount2); } // ------------------------- FORMAT UTILS --------------------------------- public static String formatXmr(BigInteger atomicUnits) { return formatXmr(atomicUnits, false); } public static String formatXmr(BigInteger atomicUnits, int decimalPlaces) { return formatXmr(atomicUnits, false, decimalPlaces); } public static String formatXmr(BigInteger atomicUnits, boolean appendCode) { return formatXmr(atomicUnits, appendCode, 0); } public static String formatXmr(BigInteger atomicUnits, boolean appendCode, int decimalPlaces) { if (atomicUnits == null) return ""; return formatXmr(atomicUnits.longValueExact(), appendCode, decimalPlaces); } public static String formatXmr(long atomicUnits) { return formatXmr(atomicUnits, false, 0); } public static String formatXmr(long atomicUnits, boolean appendCode) { return formatXmr(atomicUnits, appendCode, 0); } public static String formatXmr(long atomicUnits, boolean appendCode, int decimalPlaces) { String formatted = XMR_FORMATTER.format(atomicUnitsToXmr(atomicUnits)); // strip trailing 0s if (formatted.contains(".")) { while (formatted.length() > 3 && formatted.charAt(formatted.length() - 1) == '0') { formatted = formatted.substring(0, formatted.length() - 1); } } return applyDecimals(formatted, Math.max(2, decimalPlaces)) + (appendCode ? " XMR" : ""); } public static String formatPercent(double percent) { return (percent * 100) + "%"; } private static String applyDecimals(String decimalStr, int decimalPlaces) { if (decimalStr.contains(".")) return decimalStr + getNumZeros(decimalPlaces - (decimalStr.length() - decimalStr.indexOf(".") - 1)); else return decimalStr + "." + getNumZeros(decimalPlaces); } private static String getNumZeros(int numZeros) { String zeros = ""; for (int i = 0; i < numZeros; i++) zeros += "0"; return zeros; } public static BigInteger parseXmr(String input) { if (input == null || input.length() == 0) return BigInteger.ZERO; // TODO: throw instead? try { return new BigDecimal(input).multiply(new BigDecimal(XMR_AU_MULTIPLIER)).toBigInteger(); } catch (Exception e) { return BigInteger.ZERO; } } // ------------------------ SIGNING AND VERIFYING ------------------------- public static String generateChallenge() { try { // load bip39 words loadBip39Words(); // select words randomly List passphraseWords = new ArrayList(); SecureRandom secureRandom = new SecureRandom(); for (int i = 0; i < PRIVATE_OFFER_PASSPHRASE_NUM_WORDS; i++) { passphraseWords.add(bip39Words.get(secureRandom.nextInt(bip39Words.size()))); } return String.join(" ", passphraseWords); } catch (Exception e) { throw new IllegalStateException("Failed to generate challenge", e); } } private static synchronized void loadBip39Words() { if (bip39Words.isEmpty()) { try { String fileName = "bip39_english.txt"; File bip39File = new File(havenoSetup.getConfig().appDataDir, fileName); if (!bip39File.exists()) FileUtil.resourceToFile(fileName, bip39File); bip39Words = Files.readAllLines(bip39File.toPath(), StandardCharsets.UTF_8); } catch (Exception e) { throw new IllegalStateException("Failed to load BIP39 words", e); } } } public static String getChallengeHash(String challenge) { if (challenge == null) return null; // tokenize passphrase String[] words = challenge.toLowerCase().split(" "); // collect up to first 4 letters of each word, which are unique in bip39 List prefixes = new ArrayList(); for (String word : words) prefixes.add(word.substring(0, Math.min(word.length(), 4))); // hash the result return Base64.encode(Hash.getSha256Hash(String.join(" ", prefixes).getBytes())); } public static byte[] sign(KeyRing keyRing, String message) { return sign(keyRing.getSignatureKeyPair().getPrivate(), message); } public static byte[] sign(KeyRing keyRing, byte[] message) { return sign(keyRing.getSignatureKeyPair().getPrivate(), message); } public static byte[] sign(PrivateKey privateKey, String message) { return sign(privateKey, message.getBytes(Charsets.UTF_8)); } public static byte[] sign(PrivateKey privateKey, byte[] message) { try { return Sig.sign(privateKey, message); } catch (Exception e) { throw new IllegalArgumentException(e); } } public static void verifySignature(PubKeyRing pubKeyRing, String message, byte[] signature) { verifySignature(pubKeyRing, message.getBytes(Charsets.UTF_8), signature); } public static void verifySignature(PubKeyRing pubKeyRing, byte[] message, byte[] signature) { try { boolean isValid = Sig.verify(pubKeyRing.getSignaturePubKey(), message, signature); if (!isValid) throw new IllegalArgumentException("Signature verification failed."); } catch (CryptoException e) { throw new IllegalArgumentException(e); } } public static boolean isSignatureValid(PubKeyRing pubKeyRing, String message, byte[] signature) { return isSignatureValid(pubKeyRing, message.getBytes(Charsets.UTF_8), signature); } public static boolean isSignatureValid(PubKeyRing pubKeyRing, byte[] message, byte[] signature) { try { verifySignature(pubKeyRing, message, signature); return true; } catch (Exception e) { return false; } } /** * Sign an offer. * * @param offer is an unsigned offer to sign * @param keyRing is the arbitrator's key ring to sign with * @return the arbitrator's signature */ public static byte[] signOffer(OfferPayload offer, KeyRing keyRing) { return HavenoUtils.sign(keyRing, offer.getSignatureHash()); } /** * Check if the arbitrator signature is valid for an offer. * * @param offer is a signed offer with payload * @param arbitrator is the original signing arbitrator * @return true if the arbitrator's signature is valid for the offer */ public static boolean isArbitratorSignatureValid(OfferPayload offer, Arbitrator arbitrator) { return isSignatureValid(arbitrator.getPubKeyRing(), offer.getSignatureHash(), offer.getArbitratorSignature()); } /** * Verify the buyer signature for a PaymentSentMessage. * * @param trade - the trade to verify * @param message - signed payment sent message to verify * @return true if the buyer's signature is valid for the message */ public static void verifyPaymentSentMessage(Trade trade, PaymentSentMessage message) { // remove signature from message byte[] signature = message.getBuyerSignature(); message.setBuyerSignature(null); // get unsigned message as json string String unsignedMessageAsJson = JsonUtil.objectToJson(message); // replace signature message.setBuyerSignature(signature); // verify signature if (!isSignatureValid(trade.getBuyer().getPubKeyRing(), unsignedMessageAsJson, signature)) { throw new IllegalArgumentException("The buyer signature is invalid for the " + message.getClass().getSimpleName() + " for " + trade.getClass().getSimpleName() + " " + trade.getId()); } // verify trade id if (!trade.getId().equals(message.getOfferId())) throw new IllegalArgumentException("The " + message.getClass().getSimpleName() + " has the wrong trade id, expected " + trade.getId() + " but was " + message.getOfferId()); } /** * Verify the seller signature for a PaymentReceivedMessage. * * @param trade - the trade to verify * @param message - signed payment received message to verify * @return true if the seller's signature is valid for the message */ public static void verifyPaymentReceivedMessage(Trade trade, PaymentReceivedMessage message) { // remove signature from message byte[] signature = message.getSellerSignature(); message.setSellerSignature(null); // get unsigned message as json string String unsignedMessageAsJson = JsonUtil.objectToJson(message); // replace signature message.setSellerSignature(signature); // verify signature if (!isSignatureValid(trade.getSeller().getPubKeyRing(), unsignedMessageAsJson, signature)) { throw new IllegalArgumentException("The seller signature is invalid for the " + message.getClass().getSimpleName() + " for " + trade.getClass().getSimpleName() + " " + trade.getId()); } // verify trade id if (!trade.getId().equals(message.getOfferId())) throw new IllegalArgumentException("The " + message.getClass().getSimpleName() + " has the wrong trade id, expected " + trade.getId() + " but was " + message.getOfferId()); // verify buyer signature of payment sent message if (message.getPaymentSentMessage() != null) verifyPaymentSentMessage(trade, message.getPaymentSentMessage()); } // ----------------------------- OTHER UTILS ------------------------------ public static String getGlobalTradeFeeAddress() { switch (Config.baseCurrencyNetwork()) { case XMR_LOCAL: return "Bd37nTGHjL3RvPxc9dypzpWiXQrPzxxG4RsWAasD9CV2iZ1xfFZ7mzTKNDxWBfsqQSUimctAsGtTZ8c8bZJy35BYL9jYj88"; case XMR_STAGENET: return "5B11hTJdG2XDNwjdKGLRxwSLwDhkbGg7C7UEAZBxjE6FbCeRMjudrpNACmDNtWPiSnNfjDQf39QRjdtdgoL69txv81qc2Mc"; case XMR_MAINNET: throw new RuntimeException("Mainnet fee address not implemented"); default: throw new RuntimeException("Unhandled base currency network: " + Config.baseCurrencyNetwork()); } } public static String getBurnAddress() { switch (Config.baseCurrencyNetwork()) { case XMR_LOCAL: return "Bd37nTGHjL3RvPxc9dypzpWiXQrPzxxG4RsWAasD9CV2iZ1xfFZ7mzTKNDxWBfsqQSUimctAsGtTZ8c8bZJy35BYL9jYj88"; case XMR_STAGENET: return "577XbZ8yGfrWJM3aAoCpHVgDCm5higshGVJBb4ZNpTYARp8rLcCdcA1J8QgRfFWTzmJ8QgRfFWTzmJ8QgRfFWTzmCbXF9hd"; case XMR_MAINNET: return "46uVWiE1d4kWJM3aAoCpHVgDCm5higshGVJBb4ZNpTYARp8rLcCdcA1J8QgRfFWTzmJ8QgRfFWTzmJ8QgRfFWTzmCag5CXT"; default: throw new RuntimeException("Unhandled base currency network: " + Config.baseCurrencyNetwork()); } } /** * Check if the given URI is on local host. */ public static boolean isLocalHost(String uriString) { try { String host = new URI(uriString).getHost(); return LOOPBACK_HOST.equals(host) || LOCALHOST.equals(host); } catch (Exception e) { return false; } } /** * Check if the given URI is local or a private IP address. */ public static boolean isPrivateIp(String uriString) { if (uriString == null || uriString.isEmpty()) return false; if (isLocalHost(uriString)) return true; try { URI uri = new URI(uriString); String host = uri.getHost(); if (host == null) return false; // strip IPv6 brackets if (host.startsWith("[") && host.endsWith("]")) { host = host.substring(1, host.length() - 1); } // check if private IP address if (!isLiteralIp(host)) return false; InetAddress addr = InetAddress.getByName(host); // does not perform DNS check if using literal IP return addr.isAnyLocalAddress() || addr.isLoopbackAddress() || addr.isLinkLocalAddress() || addr.isSiteLocalAddress(); } catch (Exception e) { return false; } } private static boolean isLiteralIp(String host) { return IPV4.matcher(host).matches() || host.indexOf(':') >= 0; } /** * Returns a unique deterministic id for sending a trade mailbox message. * * @param trade the trade * @param tradeMessageClass the trade message class * @param receiver the receiver address * @return a unique deterministic id for sending a trade mailbox message */ public static String getDeterministicId(Trade trade, Class tradeMessageClass, NodeAddress receiver) { String uniqueId = trade.getId() + "_" + tradeMessageClass.getSimpleName() + "_" + trade.getRole() + "_to_" + trade.getPeerRole(trade.getTradePeer(receiver)); return Utilities.bytesAsHexString(Hash.getSha256Ripemd160hash(uniqueId.getBytes(Charsets.UTF_8))); } public static void awaitLatch(CountDownLatch latch) { try { latch.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } } public static String toCamelCase(String underscore) { return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, underscore); } public static boolean connectionConfigsEqual(MoneroRpcConnection c1, MoneroRpcConnection c2) { if (c1 == c2) return true; if (c1 == null) return false; return c1.equals(c2); // equality considers uri, username, and password } // TODO: move to monero-java MoneroTxWallet public static MoneroDestination getDestination(String address, MoneroTxWallet tx) { for (MoneroDestination destination : tx.getOutgoingTransfer().getDestinations()) { if (address.equals(destination.getAddress())) return destination; } return null; } public static List getInputKeyImages(MoneroTxWallet tx) { List inputKeyImages = new ArrayList(); for (MoneroOutput input : tx.getInputs()) inputKeyImages.add(input.getKeyImage().getHex()); return inputKeyImages; } public static int getDefaultMoneroPort() { if (Config.baseCurrencyNetwork().isMainnet()) return 18081; else if (Config.baseCurrencyNetwork().isTestnet()) return 28081; else if (Config.baseCurrencyNetwork().isStagenet()) return 38081; else throw new RuntimeException("Base network is not local testnet, stagenet, or mainnet"); } public static void setTopError(String msg) { havenoSetup.getTopErrorMsg().set(msg); } public static boolean isConnectionRefused(Throwable e) { return e != null && e.getMessage() != null && e.getMessage().contains("Connection refused"); } public static boolean isReadTimeout(Throwable e) { return e != null && e.getMessage() != null && e.getMessage().contains("Read timed out"); } public static boolean isUnresponsive(Throwable e) { return isConnectionRefused(e) || isReadTimeout(e) || XmrWalletBase.isSyncWithProgressTimeout(e); } private static boolean isNotEnoughSigners(Throwable e) { return e != null && e.getMessage() != null && e.getMessage().contains("Not enough signers"); } private static boolean isFailedToParse(Throwable e) { return e != null && e.getMessage() != null && e.getMessage().contains("Failed to parse"); } private static boolean isStaleData(Throwable e) { return e != null && e.getMessage() != null && e.getMessage().contains("stale data"); } private static boolean isNoTransactionCreated(Throwable e) { return e != null && e.getMessage() != null && e.getMessage().contains("No transaction created"); } private static boolean isLRNotFound(Throwable e) { return e != null && e.getMessage() != null && e.getMessage().contains("LR not found for enough participants"); } // TODO: handling specific error messages is brittle, inverse so all errors are illegal except known local issues? public static boolean isMultisigError(Throwable e) { return isLRNotFound(e) || isNotEnoughSigners(e) || isNoTransactionCreated(e) || isFailedToParse(e) || isStaleData(e); } public static boolean isTransactionRejected(Throwable e) { return e != null && e.getMessage() != null && e.getMessage().contains("was rejected"); } public static boolean isIllegal(Throwable e) { return e instanceof IllegalArgumentException || e instanceof IllegalStateException; } public static void playChimeSound() { playAudioFile("chime.wav"); } public static void playCashRegisterSound() { playAudioFile("cash_register.wav"); } private static void playAudioFile(String fileName) { if (isDaemon()) return; // ignore if running as daemon if (!preferences.getUseSoundForNotificationsProperty().get()) return; // ignore if sounds disabled new Thread(() -> { try { // get audio file File wavFile = new File(havenoSetup.getConfig().appDataDir, fileName); if (!wavFile.exists()) FileUtil.resourceToFile(fileName, wavFile); AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(wavFile); // get original format AudioFormat baseFormat = audioInputStream.getFormat(); // set target format: PCM_SIGNED, 16-bit, 44100 Hz AudioFormat targetFormat = new AudioFormat( AudioFormat.Encoding.PCM_SIGNED, 44100.0f, 16, // 16-bit instead of 32-bit float baseFormat.getChannels(), baseFormat.getChannels() * 2, // Frame size: 2 bytes per channel (16-bit) 44100.0f, false // Little-endian ); // convert audio to target format AudioInputStream convertedStream = AudioSystem.getAudioInputStream(targetFormat, audioInputStream); // play audio DataLine.Info info = new DataLine.Info(SourceDataLine.class, targetFormat); SourceDataLine sourceLine = (SourceDataLine) AudioSystem.getLine(info); sourceLine.open(targetFormat); sourceLine.start(); byte[] buffer = new byte[1024]; int bytesRead = 0; while ((bytesRead = convertedStream.read(buffer, 0, buffer.length)) != -1) { sourceLine.write(buffer, 0, bytesRead); } sourceLine.drain(); sourceLine.close(); convertedStream.close(); audioInputStream.close(); } catch (Exception e) { e.printStackTrace(); } }).start(); } public static void verifyMinerFee(BigInteger expected, BigInteger actual) { BigInteger max = expected.max(actual); BigInteger min = expected.min(actual); if (min.compareTo(BigInteger.ZERO) <= 0) { throw new IllegalArgumentException("Miner fees must be greater than zero"); } double factor = divide(max, min); if (factor > MINER_FEE_TOLERANCE_FACTOR) { throw new IllegalArgumentException("Miner fees are not within " + MINER_FEE_TOLERANCE_FACTOR + "x of each other. Expected=" + expected + ", actual=" + actual + ", factor=" + factor); } } } ================================================ FILE: core/src/main/java/haveno/core/trade/MakerTrade.java ================================================ /* * 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 . */ package haveno.core.trade; public interface MakerTrade { } ================================================ FILE: core/src/main/java/haveno/core/trade/SellerAsMakerTrade.java ================================================ /* * 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 . */ package haveno.core.trade; import haveno.common.proto.ProtoUtil; import haveno.core.offer.Offer; import haveno.core.proto.CoreProtoResolver; import haveno.core.trade.protocol.ProcessModel; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.math.BigInteger; import java.util.UUID; @Slf4j public final class SellerAsMakerTrade extends SellerTrade implements MakerTrade { /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, initialization /////////////////////////////////////////////////////////////////////////////////////////// public SellerAsMakerTrade(Offer offer, BigInteger tradeAmount, long tradePrice, XmrWalletService xmrWalletService, ProcessModel processModel, String uid, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress takerNodeAddress, @Nullable NodeAddress arbitratorNodeAddress, @Nullable String challenge) { super(offer, tradeAmount, tradePrice, xmrWalletService, processModel, uid, makerNodeAddress, takerNodeAddress, arbitratorNodeAddress, challenge); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.Tradable toProtoMessage() { return protobuf.Tradable.newBuilder() .setSellerAsMakerTrade(protobuf.SellerAsMakerTrade.newBuilder() .setTrade((protobuf.Trade) super.toProtoMessage())) .build(); } public static Tradable fromProto(protobuf.SellerAsMakerTrade sellerAsMakerTradeProto, XmrWalletService xmrWalletService, CoreProtoResolver coreProtoResolver) { protobuf.Trade proto = sellerAsMakerTradeProto.getTrade(); ProcessModel processModel = ProcessModel.fromProto(proto.getProcessModel(), coreProtoResolver); String uid = ProtoUtil.stringOrNullFromProto(proto.getUid()); if (uid == null) { uid = UUID.randomUUID().toString(); } SellerAsMakerTrade trade = new SellerAsMakerTrade( Offer.fromProto(proto.getOffer()), BigInteger.valueOf(proto.getAmount()), proto.getPrice(), xmrWalletService, processModel, uid, proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null, proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null, proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null, ProtoUtil.stringOrNullFromProto(proto.getChallenge())); trade.setPrice(proto.getPrice()); return fromProto(trade, proto, coreProtoResolver); } } ================================================ FILE: core/src/main/java/haveno/core/trade/SellerAsTakerTrade.java ================================================ /* * 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 . */ package haveno.core.trade; import haveno.common.proto.ProtoUtil; import haveno.core.offer.Offer; import haveno.core.proto.CoreProtoResolver; import haveno.core.trade.protocol.ProcessModel; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.math.BigInteger; import java.util.UUID; @Slf4j public final class SellerAsTakerTrade extends SellerTrade implements TakerTrade { /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, initialization /////////////////////////////////////////////////////////////////////////////////////////// public SellerAsTakerTrade(Offer offer, BigInteger tradeAmount, long tradePrice, XmrWalletService xmrWalletService, ProcessModel processModel, String uid, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress takerNodeAddress, @Nullable NodeAddress arbitratorNodeAddress, @Nullable String challenge) { super(offer, tradeAmount, tradePrice, xmrWalletService, processModel, uid, makerNodeAddress, takerNodeAddress, arbitratorNodeAddress, challenge); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.Tradable toProtoMessage() { return protobuf.Tradable.newBuilder() .setSellerAsTakerTrade(protobuf.SellerAsTakerTrade.newBuilder() .setTrade((protobuf.Trade) super.toProtoMessage())) .build(); } public static Tradable fromProto(protobuf.SellerAsTakerTrade sellerAsTakerTradeProto, XmrWalletService xmrWalletService, CoreProtoResolver coreProtoResolver) { protobuf.Trade proto = sellerAsTakerTradeProto.getTrade(); ProcessModel processModel = ProcessModel.fromProto(proto.getProcessModel(), coreProtoResolver); String uid = ProtoUtil.stringOrNullFromProto(proto.getUid()); if (uid == null) { uid = UUID.randomUUID().toString(); } return fromProto(new SellerAsTakerTrade( Offer.fromProto(proto.getOffer()), BigInteger.valueOf(proto.getAmount()), proto.getPrice(), xmrWalletService, processModel, uid, proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null, proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null, proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null, ProtoUtil.stringOrNullFromProto(proto.getChallenge())), proto, coreProtoResolver); } } ================================================ FILE: core/src/main/java/haveno/core/trade/SellerTrade.java ================================================ /* * 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 . */ package haveno.core.trade; import haveno.core.offer.Offer; import haveno.core.trade.protocol.ProcessModel; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.math.BigInteger; import java.util.Date; @Slf4j public abstract class SellerTrade extends Trade { private static final long resendPaymentReceivedMessagesDurationMs = 1L * 30 * 24 * 60 * 60 * 1000; // 30 days SellerTrade(Offer offer, BigInteger tradeAmount, long tradePrice, XmrWalletService xmrWalletService, ProcessModel processModel, String uid, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress takerNodeAddress, @Nullable NodeAddress arbitratorNodeAddress, @Nullable String challenge) { super(offer, tradeAmount, tradePrice, xmrWalletService, processModel, uid, makerNodeAddress, takerNodeAddress, arbitratorNodeAddress, challenge); } @Override public BigInteger getPayoutAmountBeforeCost() { return getSellerSecurityDepositBeforeMiningFee(); } @Override public boolean confirmPermitted() { return true; } public boolean isFinished() { return super.isFinished() && !needsToResendPaymentReceivedMessages(); } public boolean needsToResendPaymentReceivedMessages() { boolean hasNoPaymentReceivedMessages = getBuyer().getPaymentReceivedMessage() == null && getArbitrator().getPaymentReceivedMessage() == null; if (!walletExistsNoSync() && !hasNoPaymentReceivedMessages) return false; // cannot provide any updated state return !isShutDownStarted() && getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && !getProcessModel().isPaymentReceivedMessagesAcked() && resendPaymentReceivedMessagesEnabled() && resendPaymentReceivedMessagesWithinDuration(); } private boolean resendPaymentReceivedMessagesEnabled() { return getOffer().getOfferPayload().getProtocolVersion() >= 2; } public boolean resendPaymentReceivedMessagesWithinDuration() { Date startDate = getMaxTradePeriodDate(); // TODO: preferably use the date when the payment receipt was confirmed return new Date().getTime() <= (startDate.getTime() + resendPaymentReceivedMessagesDurationMs); } } ================================================ FILE: core/src/main/java/haveno/core/trade/TakerTrade.java ================================================ /* * 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 . */ package haveno.core.trade; public interface TakerTrade { } ================================================ FILE: core/src/main/java/haveno/core/trade/Tradable.java ================================================ /* * 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 . */ package haveno.core.trade; import haveno.common.proto.persistable.PersistablePayload; import haveno.core.monetary.Price; import haveno.core.monetary.Volume; import haveno.core.offer.Offer; import haveno.network.p2p.NodeAddress; import java.math.BigInteger; import java.util.Date; import java.util.Optional; public interface Tradable extends PersistablePayload { Offer getOffer(); Date getDate(); String getId(); String getShortId(); default Optional asTradeModel() { if (this instanceof Trade) { return Optional.of(((Trade) this)); } else { return Optional.empty(); } } default Optional getOptionalVolume() { return asTradeModel().map(Trade::getVolume).or(() -> Optional.ofNullable(getOffer().getVolume())); } default Optional getOptionalPrice() { return asTradeModel().map(Trade::getPrice).or(() -> Optional.ofNullable(getOffer().getPrice())); } default Optional getOptionalAmount() { return asTradeModel().map(Trade::getAmount); } default BigInteger getTotalTxFee() { return asTradeModel().map(Trade::getTotalTxFee).orElse(BigInteger.ZERO); } default Optional getOptionalTakerFee() { return asTradeModel().map(Trade::getTakerFee); } default Optional getOptionalMakerFee() { return asTradeModel().map(Trade::getMakerFee); } default Optional getOptionalTradePeerNodeAddress() { return asTradeModel().map(Trade::getTradePeerNodeAddress); } } ================================================ FILE: core/src/main/java/haveno/core/trade/TradableList.java ================================================ /* * 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 . */ package haveno.core.trade; import com.google.protobuf.Message; import haveno.common.proto.ProtoUtil; import haveno.common.proto.ProtobufferRuntimeException; import haveno.common.proto.persistable.PersistableListAsObservable; import haveno.core.offer.OpenOffer; import haveno.core.proto.CoreProtoResolver; import haveno.core.xmr.wallet.XmrWalletService; import lombok.extern.slf4j.Slf4j; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; @Slf4j public final class TradableList extends PersistableListAsObservable { /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public TradableList() { } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// protected TradableList(Collection collection) { super(collection); } @Override public Message toProtoMessage() { synchronized (getList()) { return protobuf.PersistableEnvelope.newBuilder() .setTradableList(protobuf.TradableList.newBuilder() .addAllTradable(ProtoUtil.collectionToProto(getList(), protobuf.Tradable.class))) .build(); } } public static TradableList fromProto(protobuf.TradableList proto, CoreProtoResolver coreProtoResolver, XmrWalletService xmrWalletService) { List list = proto.getTradableList().stream() .map(tradable -> { switch (tradable.getMessageCase()) { case OPEN_OFFER: return OpenOffer.fromProto(tradable.getOpenOffer()); case BUYER_AS_MAKER_TRADE: return BuyerAsMakerTrade.fromProto(tradable.getBuyerAsMakerTrade(), xmrWalletService, coreProtoResolver); case BUYER_AS_TAKER_TRADE: return BuyerAsTakerTrade.fromProto(tradable.getBuyerAsTakerTrade(), xmrWalletService, coreProtoResolver); case SELLER_AS_MAKER_TRADE: return SellerAsMakerTrade.fromProto(tradable.getSellerAsMakerTrade(), xmrWalletService, coreProtoResolver); case SELLER_AS_TAKER_TRADE: return SellerAsTakerTrade.fromProto(tradable.getSellerAsTakerTrade(), xmrWalletService, coreProtoResolver); case ARBITRATOR_TRADE: return ArbitratorTrade.fromProto(tradable.getArbitratorTrade(), xmrWalletService, coreProtoResolver); default: log.error("Unknown messageCase. tradable.getMessageCase() = " + tradable.getMessageCase()); throw new ProtobufferRuntimeException("Unknown messageCase. tradable.getMessageCase() = " + tradable.getMessageCase()); } }) .collect(Collectors.toList()); return new TradableList<>(list); } @Override public String toString() { return "TradableList{" + ",\n list=" + getList() + "\n}"; } } ================================================ FILE: core/src/main/java/haveno/core/trade/Trade.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade; import com.google.common.base.Preconditions; import com.google.protobuf.ByteString; import com.google.protobuf.Message; import haveno.common.ThreadUtils; import haveno.common.UserThread; import haveno.common.config.Config; import haveno.common.crypto.Encryption; import haveno.common.crypto.PubKeyRing; import haveno.common.proto.ProtoUtil; import haveno.common.taskrunner.Model; import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.core.monetary.Price; import haveno.core.monetary.Volume; import haveno.core.network.MessageState; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OpenOffer; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.proto.CoreProtoResolver; import haveno.core.proto.network.CoreNetworkProtoResolver; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeResult; import haveno.core.support.dispute.DisputeResult.Winner; import haveno.core.support.dispute.mediation.MediationResultState; import haveno.core.support.dispute.refund.RefundResultState; import haveno.core.support.messages.ChatMessage; import haveno.core.trade.messages.TradeMessage; import haveno.core.trade.protocol.ProcessModel; import haveno.core.trade.protocol.ProcessModelServiceProvider; import haveno.core.trade.protocol.SellerProtocol; import haveno.core.trade.protocol.TradeListener; import haveno.core.trade.protocol.TradePeer; import haveno.core.trade.protocol.TradeProtocol; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.util.PriceUtil; import haveno.core.util.VolumeUtil; import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.wallet.XmrWalletBase; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.AckMessage; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import haveno.network.p2p.network.TorNetworkNode; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyStringProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import monero.common.MoneroError; import monero.common.MoneroRpcConnection; import monero.common.TaskLooper; import monero.daemon.MoneroDaemon; import monero.daemon.model.MoneroKeyImage; import monero.daemon.model.MoneroTx; import monero.wallet.MoneroWallet; import monero.wallet.MoneroWalletRpc; import monero.wallet.model.MoneroDestination; import monero.wallet.model.MoneroMultisigSignResult; import monero.wallet.model.MoneroOutputQuery; import monero.wallet.model.MoneroOutputWallet; import monero.wallet.model.MoneroSyncResult; import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxQuery; import monero.wallet.model.MoneroTxSet; import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroWalletListener; import org.apache.commons.lang3.StringUtils; import org.bitcoinj.core.Coin; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; import javax.crypto.SecretKey; import java.math.BigInteger; import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Optional; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkNotNull; /** * Holds all data which are relevant to the trade, but not those which are only needed in the trade process as shared data between tasks. Those data are * stored in the task model. */ @Slf4j public abstract class Trade extends XmrWalletBase implements Tradable, Model { @Getter public final Object lock = new Object(); private static final String MONERO_TRADE_WALLET_PREFIX = "xmr_trade_"; private static final long SHUTDOWN_TIMEOUT_MS = Config.baseCurrencyNetwork().isTestnet() ? 20000 : 60000; private static final long SYNC_EVERY_NUM_BLOCKS = Config.baseCurrencyNetwork().isTestnet() ? 40 : 360; // ~1/2 day private static final long DELETE_AFTER_NUM_BLOCKS = 2; // if deposit requested but not published private static final long EXTENDED_RPC_TIMEOUT = 600000; // 10 minutes private static final long DELETE_AFTER_MS = TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS; private static final int NUM_CONFIRMATIONS_FOR_SCHEDULED_IMPORT = 5; public static final int NUM_BLOCKS_DEPOSITS_FINALIZED = 30; // ~1 hour before deposits are considered finalized public static final int NUM_BLOCKS_PAYOUT_FINALIZED = Config.baseCurrencyNetwork().isTestnet() ? 60 : 720; // ~1 day before payout is considered finalized and multisig wallet deleted public static final long DEFER_PUBLISH_MS = 25000; // 25 seconds public static final long POLL_WALLET_NORMALLY_DEFAULT_PERIOD_MS = 120000; // 2 minutes private static final long IDLE_SYNC_PERIOD_MS = Config.baseCurrencyNetwork().isTestnet() ? 75000 : 28 * 60 * 1000; // 28 minutes (monero's default connection timeout is 30 minutes on a local connection, so beyond this the wallets will disconnect) private static final long MAX_REPROCESS_DELAY_SECONDS = 7200; // max delay to reprocess messages (once per 2 hours) private static final long REVERT_AFTER_NUM_CONFIRMATIONS = 3; private static final Object IDLE_BLOCK_POLLER_LOCK = new Object(); // global lock to serialize idle trade polling private static final Object SYNC_DELAY_LOCK = new Object(); private static final long SYNC_DELAY_MS = 3000; private static final long MAX_SYNC_DELAY_MS = 30000; private static Long firstSyncDelay = null; protected final Object pollLock = new Object(); private final Object removeTradeOnErrorLock = new Object(); private boolean pollInProgress; private boolean restartInProgress; private Subscription protocolErrorStateSubscription; private Subscription protocolErrorHeightSubscription; public static final String PROTOCOL_VERSION = "protocolVersion"; // key for extraDataMap in trade statistics public BooleanProperty wasWalletPolledProperty = new SimpleBooleanProperty(false); public BooleanProperty wasWalletSyncedAndPolledProperty = new SimpleBooleanProperty(false); private static final long MISSING_TXS_DELAY_MS = Config.baseCurrencyNetwork().isTestnet() ? 5000 : 30000; private Long firstDepositTxMissingHeight; // height when we first saw missing deposit txs (to wait for a confirmation before reverting state) private Long firstPayoutTxMissingHeight; // height when we first saw missing payout tx (to wait for a confirmation before reverting state) /////////////////////////////////////////////////////////////////////////////////////////// // Enums /////////////////////////////////////////////////////////////////////////////////////////// public enum State { // trade initialization PREPARATION(Phase.INIT), MULTISIG_PREPARED(Phase.INIT), MULTISIG_MADE(Phase.INIT), MULTISIG_EXCHANGED(Phase.INIT), MULTISIG_COMPLETED(Phase.INIT), CONTRACT_SIGNATURE_REQUESTED(Phase.INIT), CONTRACT_SIGNED(Phase.INIT), // deposit requested SENT_PUBLISH_DEPOSIT_TX_REQUEST(Phase.DEPOSIT_REQUESTED), SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST(Phase.DEPOSIT_REQUESTED), SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST(Phase.DEPOSIT_REQUESTED), PUBLISH_DEPOSIT_TX_REQUEST_FAILED(Phase.DEPOSIT_REQUESTED), // deposits published ARBITRATOR_PUBLISHED_DEPOSIT_TXS(Phase.DEPOSITS_PUBLISHED), DEPOSIT_TXS_SEEN_IN_NETWORK(Phase.DEPOSITS_PUBLISHED), // deposits confirmed DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN(Phase.DEPOSITS_CONFIRMED), // deposits unlocked DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN(Phase.DEPOSITS_UNLOCKED), // deposits finalized DEPOSIT_TXS_FINALIZED_IN_BLOCKCHAIN(Phase.DEPOSITS_FINALIZED), // payment sent BUYER_CONFIRMED_PAYMENT_SENT(Phase.PAYMENT_SENT), BUYER_SENT_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT), BUYER_SEND_FAILED_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT), BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT), BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT), SELLER_RECEIVED_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT), // payment received SELLER_CONFIRMED_PAYMENT_RECEIPT(Phase.PAYMENT_RECEIVED), SELLER_SENT_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED), SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED), SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED), SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED), BUYER_RECEIVED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED); @NotNull public Phase getPhase() { return phase; } @NotNull private final Phase phase; State(@NotNull Phase phase) { this.phase = phase; } public static Trade.State fromProto(protobuf.Trade.State state) { return ProtoUtil.enumFromProto(Trade.State.class, state.name()); } public static protobuf.Trade.State toProtoMessage(Trade.State state) { return protobuf.Trade.State.valueOf(state.name()); } // We allow a state change only if the phase is the next phase or if we do not change the phase by the // state change (e.g. detail change inside the same phase) public boolean isValidTransitionTo(State newState) { Phase newPhase = newState.getPhase(); Phase currentPhase = this.getPhase(); return currentPhase.isValidTransitionTo(newPhase) || newPhase.equals(currentPhase); } } public enum Phase { INIT, DEPOSIT_REQUESTED, DEPOSITS_PUBLISHED, DEPOSITS_CONFIRMED, DEPOSITS_UNLOCKED, DEPOSITS_FINALIZED, PAYMENT_SENT, PAYMENT_RECEIVED; public static Trade.Phase fromProto(protobuf.Trade.Phase phase) { return ProtoUtil.enumFromProto(Trade.Phase.class, phase.name()); } public static protobuf.Trade.Phase toProtoMessage(Trade.Phase phase) { return protobuf.Trade.Phase.valueOf(phase.name()); } // We allow a phase change only if the phase a future phase (we cannot limit it to next phase as we have cases where // we skip a phase as it is only relevant to one role -> states and phases need a redesign ;-( ) public boolean isValidTransitionTo(Phase newPhase) { // this is current phase return newPhase.ordinal() > this.ordinal(); } } public enum PayoutState { PAYOUT_UNPUBLISHED, PAYOUT_PUBLISHED, PAYOUT_CONFIRMED, PAYOUT_UNLOCKED, PAYOUT_FINALIZED; public static Trade.PayoutState fromProto(protobuf.Trade.PayoutState state) { return ProtoUtil.enumFromProto(Trade.PayoutState.class, state.name()); } public static protobuf.Trade.PayoutState toProtoMessage(Trade.PayoutState state) { return protobuf.Trade.PayoutState.valueOf(state.name()); } public boolean isValidTransitionTo(PayoutState newState) { return newState.ordinal() > this.ordinal(); } } public enum DisputeState { NO_DISPUTE, DISPUTE_PREPARING, DISPUTE_REQUESTED, DISPUTE_OPENED, ARBITRATOR_SENT_DISPUTE_CLOSED_MSG, ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG, ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG, ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG, DISPUTE_CLOSED, // mediation MEDIATION_REQUESTED, MEDIATION_STARTED_BY_PEER, MEDIATION_CLOSED, // refund REFUND_REQUESTED, REFUND_REQUEST_STARTED_BY_PEER, REFUND_REQUEST_CLOSED; public static Trade.DisputeState fromProto(protobuf.Trade.DisputeState disputeState) { return ProtoUtil.enumFromProto(Trade.DisputeState.class, disputeState.name()); } public static protobuf.Trade.DisputeState toProtoMessage(Trade.DisputeState disputeState) { return protobuf.Trade.DisputeState.valueOf(disputeState.name()); } public boolean isMediated() { return this == Trade.DisputeState.MEDIATION_REQUESTED || this == Trade.DisputeState.MEDIATION_STARTED_BY_PEER || this == Trade.DisputeState.MEDIATION_CLOSED; } public boolean isDisputed() { return this.ordinal() >= DisputeState.DISPUTE_PREPARING.ordinal(); } public boolean isPreparing() { return this == DisputeState.DISPUTE_PREPARING; } public boolean isRequested() { return ordinal() >= DisputeState.DISPUTE_REQUESTED.ordinal(); } public boolean isOpen() { return ordinal() >= DisputeState.DISPUTE_OPENED.ordinal() && !isClosed(); } public boolean isCloseRequested() { return this.ordinal() >= DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG.ordinal(); } public boolean isClosed() { return this == DisputeState.DISPUTE_CLOSED; } } public enum TradePeriodState { FIRST_HALF, SECOND_HALF, TRADE_PERIOD_OVER; public static Trade.TradePeriodState fromProto(protobuf.Trade.TradePeriodState tradePeriodState) { return ProtoUtil.enumFromProto(Trade.TradePeriodState.class, tradePeriodState.name()); } public static protobuf.Trade.TradePeriodState toProtoMessage(Trade.TradePeriodState tradePeriodState) { return protobuf.Trade.TradePeriodState.valueOf(tradePeriodState.name()); } } /////////////////////////////////////////////////////////////////////////////////////////// // Fields /////////////////////////////////////////////////////////////////////////////////////////// // Persistable // Immutable @Getter private final ProcessModel processModel; @Getter private final Offer offer; // Added in 1.5.1 @Getter private final String uid; @Setter private long takeOfferDate; // Initialization private static final int TOTAL_INIT_STEPS = 24; // total estimated steps private int initStep = 0; @Getter private double initProgress = 0; @Getter @Setter private Exception initError; // Mutable private long amount; @Setter private long price; @Nullable @Getter private State state = State.PREPARATION; @Getter private PayoutState payoutState = PayoutState.PAYOUT_UNPUBLISHED; @Getter private DisputeState disputeState = DisputeState.NO_DISPUTE; @Getter private TradePeriodState periodState = TradePeriodState.FIRST_HALF; @Nullable @Getter @Setter private Contract contract; @Nullable @Getter @Setter private String contractAsJson; @Nullable @Getter @Setter private byte[] contractHash; @Nullable private String errorMessage; @Getter @Setter @Nullable private String counterCurrencyTxId; @Getter private final ObservableList chatMessages = FXCollections.observableArrayList(); // Transient // Immutable @Getter transient final private XmrWalletService xmrWalletService; transient final private DoubleProperty initProgressProperty = new SimpleDoubleProperty(0.0); transient final private ObjectProperty stateProperty = new SimpleObjectProperty<>(state); transient final private ObjectProperty phaseProperty = new SimpleObjectProperty<>(state.phase); transient final private ObjectProperty payoutStateProperty = new SimpleObjectProperty<>(payoutState); transient final private ObjectProperty disputeStateProperty = new SimpleObjectProperty<>(disputeState); transient final private ObjectProperty tradePeriodStateProperty = new SimpleObjectProperty<>(periodState); @Getter transient public final IntegerProperty depositTxsUpdateCounter = new SimpleIntegerProperty(0); transient final private StringProperty errorMessageProperty = new SimpleStringProperty(); transient private Subscription tradeStateSubscription; transient private Subscription tradePhaseSubscription; transient private Subscription payoutStateSubscription; transient private Subscription disputeStateSubscription; transient private TaskLooper pollLooper; transient private Long pollPeriodMs; transient private Long pollNormalStartTimeMs; // Mutable @Getter transient private boolean isInitialized; transient private boolean isFullyInitialized; // Added in v1.2.0 transient private ObjectProperty tradeAmountProperty; transient private ObjectProperty tradeVolumeProperty; // Added in v1.1.6 @Getter @Nullable private MediationResultState mediationResultState = MediationResultState.UNDEFINED_MEDIATION_RESULT; transient final private ObjectProperty mediationResultStateProperty = new SimpleObjectProperty<>(mediationResultState); // Added in v1.2.0 @Getter @Setter private long lockTime; @Setter private long startTime; // added for haveno private final Object startTimeLock = new Object(); @Getter @Nullable private RefundResultState refundResultState = RefundResultState.UNDEFINED_REFUND_RESULT; transient final private ObjectProperty refundResultStateProperty = new SimpleObjectProperty<>(refundResultState); // Added at v1.3.8 // We use that for the XMR txKey but want to keep it generic to be flexible for other payment methods or assets. @Getter @Setter private String counterCurrencyExtraData; // Added in XMR integration private transient List tradeListeners; // notified on fully validated trade messages transient MoneroWalletListener depositTxListener; transient MoneroWalletListener payoutTxListener; transient Boolean makerDepositLocked; // null when unknown, true while locked, false when unlocked transient Boolean takerDepositLocked; @Nullable transient private MoneroTx payoutTx; @Getter @Setter private String payoutTxId; @Nullable @Getter @Setter private String payoutTxHex; // signed payout tx hex @Getter @Setter private String payoutTxKey; private long payoutTxFee; private Long payoutHeight; private IdleBlockPoller idleBlockPoller; @Getter private boolean isCompleted; @Getter private final String challenge; /////////////////////////////////////////////////////////////////////////////////////////// // Constructors /////////////////////////////////////////////////////////////////////////////////////////// // maker protected Trade(Offer offer, BigInteger tradeAmount, long tradePrice, XmrWalletService xmrWalletService, ProcessModel processModel, String uid, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress takerNodeAddress, @Nullable NodeAddress arbitratorNodeAddress, @Nullable String challenge) { super(); this.offer = offer; this.amount = tradeAmount.longValueExact(); this.price = tradePrice; this.xmrWalletService = xmrWalletService; this.xmrConnectionService = xmrWalletService.getXmrConnectionService(); this.processModel = processModel; this.uid = uid; this.takeOfferDate = new Date().getTime(); this.tradeListeners = new ArrayList(); this.challenge = challenge; getMaker().setNodeAddress(makerNodeAddress); getTaker().setNodeAddress(takerNodeAddress); getArbitrator().setNodeAddress(arbitratorNodeAddress); setAmount(tradeAmount); } // TODO (woodser): this constructor has mediator and refund agent (to be removed), otherwise use common // taker @SuppressWarnings("NullableProblems") protected Trade(Offer offer, BigInteger tradeAmount, BigInteger txFee, long tradePrice, @Nullable NodeAddress mediatorNodeAddress, // TODO (woodser): remove mediator, refund agent from trade @Nullable NodeAddress refundAgentNodeAddress, XmrWalletService xmrWalletService, ProcessModel processModel, String uid, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress takerNodeAddress, @Nullable NodeAddress arbitratorNodeAddress, @Nullable String challenge) { this(offer, tradeAmount, tradePrice, xmrWalletService, processModel, uid, makerNodeAddress, takerNodeAddress, arbitratorNodeAddress, challenge); } // TODO: remove these constructors // arbitrator @SuppressWarnings("NullableProblems") protected Trade(Offer offer, BigInteger tradeAmount, Coin txFee, long tradePrice, NodeAddress makerNodeAddress, NodeAddress takerNodeAddress, NodeAddress arbitratorNodeAddress, XmrWalletService xmrWalletService, ProcessModel processModel, String uid, @Nullable String challenge) { this(offer, tradeAmount, tradePrice, xmrWalletService, processModel, uid, makerNodeAddress, takerNodeAddress, arbitratorNodeAddress, challenge); setAmount(tradeAmount); } /////////////////////////////////////////////////////////////////////////////////////////// // Listeners /////////////////////////////////////////////////////////////////////////////////////////// public void addListener(TradeListener listener) { tradeListeners.add(listener); } public void removeListener(TradeListener listener) { if (!tradeListeners.remove(listener)) throw new RuntimeException("TradeMessageListener is not registered"); } // notified from TradeProtocol of verified trade messages public void onVerifiedTradeMessage(TradeMessage message, NodeAddress sender) { for (TradeListener listener : new ArrayList(tradeListeners)) { // copy array to allow listener invocation to unregister listener without concurrent modification exception listener.onVerifiedTradeMessage(message, sender); } } // notified from TradeProtocol of ack messages public void onAckMessage(AckMessage ackMessage, NodeAddress sender) { for (TradeListener listener : new ArrayList(tradeListeners)) { // copy array to allow listener invocation to unregister listener without concurrent modification exception listener.onAckMessage(ackMessage, sender); } } /////////////////////////////////////////////////////////////////////////////////////////// // INITIALIZATION /////////////////////////////////////////////////////////////////////////////////////////// public void initialize(ProcessModelServiceProvider serviceProvider) { if (isInitialized) throw new IllegalStateException(getClass().getSimpleName() + " " + getId() + " is already initialized"); // reset shut down state isShutDownStarted = false; isShutDown = false; // skip initialization if trade is complete // starting in v1.0.19, seller resends payment received message until acked or stored in mailbox if (isFinished()) { clearAndShutDown(); return; } // set arbitrator pub key ring once known serviceProvider.getArbitratorManager().getDisputeAgentByNodeAddress(getArbitratorNodeAddress()).ifPresent(arbitrator -> { getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing()); }); // handle connection change on dedicated thread xmrConnectionService.addConnectionListener(connection -> { ThreadUtils.execute(() -> onConnectionChanged(connection), getId()); }); // reset states if not awaiting processing if (!isPayoutPublished()) { // reset buyer's payment sent state if (this instanceof BuyerTrade && (getState().ordinal() == Trade.State.BUYER_CONFIRMED_PAYMENT_SENT.ordinal() || getState() == State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG)) { log.warn("Resetting state of {} {} from {} to {} because sending PaymentSentMessage failed", getClass().getSimpleName(), getId(), getState(), Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN); setState(Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN); } // reset seller's payment received state if (this instanceof SellerTrade && (getState().ordinal() == Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT.ordinal() || getState() == State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG)) { log.warn("Resetting state of {} {} from {} to {} because sending PaymentReceivedMessage failed", getClass().getSimpleName(), getId(), getState(), Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); resetToPaymentSentState(); } } // handle trade state events tradeStateSubscription = EasyBind.subscribe(stateProperty, newValue -> { if (!isInitialized || isShutDownStarted) return; // no processing }); // handle trade phase events tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> { if (!isInitialized || isShutDownStarted) return; ThreadUtils.execute(() -> { awaitInitialized(); if (newValue == Trade.Phase.DEPOSIT_REQUESTED) onDepositRequested(); if (newValue == Trade.Phase.DEPOSITS_PUBLISHED) onDepositsPublished(); if (newValue == Trade.Phase.DEPOSITS_CONFIRMED) onDepositsConfirmed(); if (newValue == Trade.Phase.DEPOSITS_UNLOCKED) onDepositsUnlocked(); if (newValue == Trade.Phase.DEPOSITS_FINALIZED) onDepositsFinalized(); if (newValue == Trade.Phase.PAYMENT_SENT) onPaymentSent(); if (isDepositsPublished() && !isPayoutFinalized()) updatePollPeriod(); if (isPaymentReceived()) { UserThread.execute(() -> { if (tradePhaseSubscription != null) { tradePhaseSubscription.unsubscribe(); tradePhaseSubscription = null; } }); } }, getTradeEventThreadId()); }); // handle payout events payoutStateSubscription = EasyBind.subscribe(payoutStateProperty, newValue -> { if (!isInitialized || isShutDownStarted) return; ThreadUtils.execute(() -> { awaitInitialized(); updatePollPeriod(); // handle when payout published if (newValue == Trade.PayoutState.PAYOUT_PUBLISHED) { log.info("Payout published for {} {}", getClass().getSimpleName(), getId()); // sync main wallet to update pending balance ThreadUtils.submitToPool(() -> { HavenoUtils.waitFor(1000); if (isPayoutConfirmed()) return; if (isShutDownStarted) return; if (xmrConnectionService.isConnected()) { try { xmrWalletService.doPollWallet(); } catch (Exception e) { // use default error handling } } }); // complete disputed trade if (getDisputeState().isDisputed() && !getDisputeState().isClosed()) { processModel.getTradeManager().closeDisputedTrade(getId(), Trade.DisputeState.DISPUTE_CLOSED); if (!isArbitrator()) for (Dispute dispute : getDisputes()) dispute.setIsClosed(); // auto close trader tickets } // auto complete arbitrator trade if (isArbitrator() && !isCompleted()) processModel.getTradeManager().onTradeCompleted(this); // maybe publish trade statistic maybePublishTradeStatistics(); // reset address entries ThreadUtils.submitToPool(() -> { processModel.getXmrWalletService().swapPayoutAddressEntryToAvailable(getId()); }); } // handle when payout finalized if (newValue == Trade.PayoutState.PAYOUT_FINALIZED) { if (!isInitialized) return; log.info("Payout finalized for {} {}, deleting multisig wallet", getClass().getSimpleName(), getId()); if (isInitialized && isFinished()) clearAndShutDown(); else ThreadUtils.execute(() -> deleteWallet(), getId()); } }, getTradeEventThreadId()); }); // handle dispute events disputeStateSubscription = EasyBind.subscribe(disputeStateProperty, newValue -> { if (!isInitialized || isShutDownStarted) return; ThreadUtils.submitToPool(() -> { awaitInitialized(); if (isDisputeClosed()) maybePublishTradeStatistics(); }); }); // listen to wallet events to sync while idling idleBlockPoller = new IdleBlockPoller(); xmrWalletService.addWalletListener(idleBlockPoller); // TODO: buyer's payment sent message state property became unsynced if shut down while awaiting ack from seller. fixed mismatch in v1.0.19, but can this check be removed? if (isBuyer()) { MessageState expectedState = getPaymentSentMessageState(); if (expectedState != null && expectedState != getSeller().getPaymentSentMessageStateProperty().get()) { log.warn("Updating unexpected payment sent message state for {} {}, expected={}, actual={}", getClass().getSimpleName(), getId(), expectedState, processModel.getPaymentSentMessageStatePropertySeller().get()); getSeller().getPaymentSentMessageStateProperty().set(expectedState); } } // handle confirmations walletHeight.addListener((observable, oldValue, newValue) -> { importMultisigHexIfScheduled(); }); // done if deposit not requested or payout finalized if (!isDepositRequested() || isPayoutFinalized()) { isInitialized = true; isFullyInitialized = true; return; } // open wallet or done if wallet does not exist if (!walletExists()) { MoneroTx payoutTx = getPayoutTx(); if (payoutTx != null) { // update payout state if necessary if (payoutTx.getNumConfirmations() != null) { if (!isPayoutUnlocked() && payoutTx.getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) { log.warn("Payout state for {} {} is {} but payout is unlocked, updating state", getClass().getSimpleName(), getId(), getPayoutState()); setPayoutStateUnlocked(); } if (!isPayoutFinalized() && payoutTx.getNumConfirmations() >= NUM_BLOCKS_PAYOUT_FINALIZED) { log.warn("Payout state for {} {} is {} but payout is finalized, updating state", getClass().getSimpleName(), getId(), getPayoutState()); setPayoutStateFinalized(); } } isInitialized = true; isFullyInitialized = true; return; } else { throw new RuntimeException("Missing trade wallet for " + getClass().getSimpleName() + " " + getShortId() + ", state=" + getState() + ", marked completed=" + isCompleted()); } } // trade is initialized isInitialized = true; // init syncing if deposit requested maybeInitSyncing(); isFullyInitialized = true; } // Note that this function is overriden by subclasses. public boolean isFinished() { if (!isCompleted()) return false; if (isPayoutUnlocked() && !walletExistsNoSync()) return true; return isPayoutFinalized(); } public void resetToPaymentSentState() { setState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); for (TradePeer peer : getAllPeers()) { peer.setPaymentReceivedMessage(null); } setPayoutTxHex(null); } public void initializeAfterMailboxMessages() { // handle when wallet first synced if (wasWalletSyncedAndPolledProperty.get()) onWalletFirstSynced(); else { wasWalletSyncedAndPolledProperty.addListener((observable, oldValue, newValue) -> { if (newValue) onWalletFirstSynced(); }); } } private void onWalletFirstSynced() { requestSaveWallet(); checkForUnconfirmedTimeout(); } private void checkForUnconfirmedTimeout() { if (isDepositsConfirmed()) return; long unconfirmedHours = Duration.between(getDate().toInstant(), Instant.now()).toHours(); if (unconfirmedHours >= 3 && !hasFailed()) { String errorMessage = Res.get("portfolio.pending.unconfirmedTooLong", getShortId(), unconfirmedHours); prependErrorMessage(errorMessage); } } public void awaitInitialized() { while (!isFullyInitialized) HavenoUtils.waitFor(100); // TODO: use proper notification and refactor isInitialized, fullyInitialized, and arbitrator idling } // TODO: throw if trade manager is null public void requestPersistence() { if (processModel.getTradeManager() != null) processModel.getTradeManager().requestPersistence(); } // TODO: throw if trade manager is null public void persistNow(@Nullable Runnable completeHandler) { if (processModel.getTradeManager() != null) processModel.getTradeManager().persistNow(completeHandler); } public TradeProtocol getProtocol() { return processModel.getTradeManager().getTradeProtocol(this); } public void setMyNodeAddress() { getSelf().setNodeAddress(P2PService.getMyNodeAddress()); } public NodeAddress getTradePeerNodeAddress() { return getTradePeer() == null ? null : getTradePeer().getNodeAddress(); } public NodeAddress getArbitratorNodeAddress() { return getArbitrator() == null ? null : getArbitrator().getNodeAddress(); } public void setCompleted(boolean completed) { this.isCompleted = completed; if (isInitialized && isFinished()) clearAndShutDown(); } /////////////////////////////////////////////////////////////////////////////////////////// // WALLET MANAGEMENT /////////////////////////////////////////////////////////////////////////////////////////// public boolean walletExists() { synchronized (walletLock) { return xmrWalletService.walletExists(getWalletName()); } } protected boolean walletExistsNoSync() { return xmrWalletService.walletExists(getWalletName()); } public MoneroWallet createWallet() { synchronized (walletLock) { if (walletExists()) throw new RuntimeException("Cannot create trade wallet because it already exists"); long time = System.currentTimeMillis(); wallet = xmrWalletService.createWallet(getWalletName(), xmrWalletService.isProxyApplied(wasWalletSynced)); log.info("{} {} created multisig wallet in {} ms", getClass().getSimpleName(), getId(), System.currentTimeMillis() - time); return wallet; } } /** * Get the trade wallet, opening the wallet if necessary. * * @return the trade wallet, or null if the wallet does not exist */ public MoneroWallet getWallet() { synchronized (walletLock) { if (wallet != null) return wallet; if (!walletExists()) return null; if (isShutDownStarted) throw new RuntimeException("Cannot open wallet for " + getClass().getSimpleName() + " " + getId() + " because shut down is started"); // log opening wallet String startOpenLogMsg = "Opening wallet for " + getClass().getSimpleName() + " " + getId(); boolean logInfoLevel = logWalletFunctionsAtInfoLevel(); if (logInfoLevel) log.info(startOpenLogMsg); else log.debug(startOpenLogMsg); // open wallet wallet = xmrWalletService.openWallet(getWalletName(), xmrWalletService.isProxyApplied(wasWalletSynced)); // backup wallet on successful open maybeBackupWallet(); // log done opening wallet String doneOpenLogMsg = "Done opening wallet for " + getClass().getSimpleName() + " " + getId(); if (logInfoLevel) log.info(doneOpenLogMsg); else log.debug(doneOpenLogMsg); // poll wallet walletHeight.set(wallet.getHeight()); doPollWallet(true); // poll wallet without network calls return wallet; } } private void maybeBackupWallet() { if (isArbitrator()) return; // arbitrator does not create backup of trade wallets synchronized (walletLock) { if (Utilities.isWindows() && isWalletOpen()) { boolean logInfoLevel = logWalletFunctionsAtInfoLevel(); if (logInfoLevel) log.info("Closing wallet for {} {} to create a backup on Windows", getClass().getSimpleName(), getShortId()); closeWallet(); doBackupWallet(); if (isShutDownStarted) throw new IllegalStateException("Cannot reopen wallet for " + getClass().getSimpleName() + " " + getId() + " after backup because shut down is started"); if (logInfoLevel) log.info("Reopening wallet for {} {} after backup on Windows", getClass().getSimpleName(), getShortId()); wallet = xmrWalletService.openWallet(getWalletName(), xmrWalletService.isProxyApplied(wasWalletSynced)); } else { doBackupWallet(); } } } private void doBackupWallet() { synchronized (walletLock) { xmrWalletService.backupWallet(getWalletName()); } } public long getHeight() { return walletHeight.get(); } private String getWalletName() { return MONERO_TRADE_WALLET_PREFIX + getShortId() + "_" + getShortUid(); } public BigInteger getWalletBalance() { synchronized (walletLock) { return getWallet().getBalance(); } } public void verifyDaemonConnection() { if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) throw new RuntimeException("Connection service is not connected to a Monero node"); } public boolean isWalletConnectedToDaemon() { synchronized (walletLock) { try { if (wallet == null) return false; return wallet.isConnectedToDaemon(); } catch (Exception e) { return false; } } } public boolean isIdling() { if (pollNormalStartTimeMs != null) return false; if (!walletExistsNoSync()) return false; if (isPayoutUnlocked()) return true; if (isPayoutPublished()) return false; if (isArbitrator() && isDepositsConfirmed()) return true; if (!isDepositsUnlocked()) return false; if (isPaymentReceived() || isDisputeClosed()) return false; if (isBuyer() && !isDepositsFinalized()) return false; if (isSeller()) { if (!isPaymentSent()) return true; if (!isDepositsFinalized()) return false; } if (!isArbitrator() && (isPaymentReceived() || isDisputeClosed())) return false; return true; } public boolean isSyncedWithinTolerance() { synchronized (walletLock) { if (!xmrConnectionService.isSyncedWithinTolerance()) return false; Long targetHeight = xmrConnectionService.getTargetHeight(); if (targetHeight == null) return false; if (targetHeight - walletHeight.get() <= 3) return true; // synced if within 3 blocks of target height return false; } } public void syncAndPollWallet() { syncWallet(true); } public void pollWalletNormallyForMs(long pollNormalDuration) { pollNormalStartTimeMs = System.currentTimeMillis(); // override wallet poll period setPollPeriodMs(xmrConnectionService.getRefreshPeriodMs(), false); // reset wallet poll period after duration new Thread(() -> { HavenoUtils.waitFor(pollNormalDuration); Long pollNormalStartTimeMsCopy = pollNormalStartTimeMs; // copy to avoid race condition if (pollNormalStartTimeMsCopy == null) return; if (!isShutDown && System.currentTimeMillis() >= pollNormalStartTimeMsCopy + pollNormalDuration) { pollNormalStartTimeMs = null; updatePollPeriod(); } }).start(); } // TODO: checking error strings isn't robust, but the library doesn't provide a way to check if multisig hex is invalid. throw IllegalArgumentException from library on invalid multisig hex? private boolean isInvalidImportError(String errMsg) { return errMsg.contains("Failed to parse hex") || errMsg.contains("Multisig info is for a different account"); } public void changeWalletPassword(String oldPassword, String newPassword) { synchronized (walletLock) { getWallet().changePassword(oldPassword, newPassword); saveWallet(); } } @Override public void requestSaveWalletIfElapsedTime() { ThreadUtils.submitToPool(() -> { synchronized (walletLock) { if (walletExists()) saveWalletIfElapsedTime(); } }); } private void requestSaveWallet() { ThreadUtils.submitToPool(() -> { synchronized (walletLock) { if (walletExists()) saveWallet(); } }); } @Override public void saveWallet() { synchronized (walletLock) { if (!walletExists()) { log.warn("Cannot save wallet for {} {} because it does not exist", getClass().getSimpleName(), getShortId()); return; } if (wallet == null) throw new IllegalStateException("Cannot save trade wallet because it's not open for " + getClass().getSimpleName() + " " + getShortId()); wallet.save(); lastSaveTimeMs = System.currentTimeMillis(); } } private boolean isWalletOpen() { synchronized (walletLock) { return wallet != null; } } private void closeWallet() { closeWallet(true); } private void closeWallet(boolean stopPolling) { synchronized (walletLock) { if (stopPolling) { stopPolling(); pollPeriodMs = null; } if (wallet == null) return; // already closed String closeLogMsg = "Closing wallet for " + getClass().getSimpleName() + " " + getId(); boolean logInfoLevel = logWalletFunctionsAtInfoLevel(); if (logInfoLevel) log.info(closeLogMsg); else log.debug(closeLogMsg); xmrWalletService.closeWallet(wallet, true); wallet = null; } } private void forceCloseWallet() { if (wallet != null) { MoneroWallet walletRef = wallet; wallet = null; // nullify wallet before force closing so state is updated for error handling try { xmrWalletService.forceCloseWallet(walletRef, walletRef.getPath()); } catch (Exception e) { log.warn("Error force closing wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); } stopPolling(); } } public void deleteWallet() { synchronized (walletLock) { if (walletExists()) { try { // check wallet state if deposit requested and payout not finalized if (isDepositRequested() && !isPayoutFinalized()) { // ensure wallet is initialized boolean syncedWallet = false; if (wallet == null) { log.warn("Wallet is not initialized for {} {}, opening", getClass().getSimpleName(), getId()); getWallet(); syncWallet(true); syncedWallet = true; } // sync wallet if deposit requested if (!syncedWallet) { log.warn("Syncing wallet on deletion for trade {} {}, syncing", getClass().getSimpleName(), getId()); syncWallet(true); } // check if deposits published if (isDepositsPublished() && !isPayoutFinalized()) { throw new IllegalStateException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because the deposit txs have been published but payout tx has not finalized"); } // check for balance if (wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { log.warn("Rescanning spent outputs for {} {}", getClass().getSimpleName(), getId()); rescanSpent(false); if (wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { if (isBuyer()) { processBuyerPayout(payoutTxId); // process payout to main wallet log.warn("Trade wallet for " + getClass().getSimpleName() + " " + getId() + " has a balance of " + wallet.getBalance() + ", but payout tx " + payoutTxId + " is verified, so proceeding to delete wallet"); } else { throw new IllegalStateException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because it has a balance of " + wallet.getBalance()); } } } } // force close wallet without warning forceCloseWallet(); // delete wallet log.info("Deleting wallet and backups for {} {}", getClass().getSimpleName(), getId()); xmrWalletService.deleteWallet(getWalletName()); xmrWalletService.deleteWalletBackups(getWalletName()); } catch (Exception e) { log.warn("Error deleting wallet for {} {}: {}\n", getClass().getSimpleName(), getId(), e.getMessage(), e); prependErrorMessage(e.getMessage()); processModel.getTradeManager().getNotificationService().sendErrorNotification("Error", e.getMessage()); } } else { log.warn("Multisig wallet to delete for trade {} does not exist", getId()); } } } /////////////////////////////////////////////////////////////////////////////////////////// // PROTOCOL API /////////////////////////////////////////////////////////////////////////////////////////// /** * Create a contract based on the current state. * * @param trade is the trade to create the contract from * @return the contract */ public Contract createContract() { boolean isBuyerMakerAndSellerTaker = getOffer().getDirection() == OfferDirection.BUY; Contract contract = new Contract( getOffer().getOfferPayload(), checkNotNull(getAmount()).longValueExact(), getPrice().getValue(), (isBuyerMakerAndSellerTaker ? getMaker() : getTaker()).getNodeAddress(), // buyer node address // TODO (woodser): use maker and taker node address instead of buyer and seller node address for consistency (isBuyerMakerAndSellerTaker ? getTaker() : getMaker()).getNodeAddress(), // seller node address getArbitrator().getNodeAddress(), isBuyerMakerAndSellerTaker, this instanceof MakerTrade ? processModel.getAccountId() : getMaker().getAccountId(), // maker account id this instanceof TakerTrade ? processModel.getAccountId() : getTaker().getAccountId(), // taker account id checkNotNull(this instanceof MakerTrade ? getMaker().getPaymentAccountPayload().getPaymentMethodId() : getOffer().getOfferPayload().getPaymentMethodId()), checkNotNull(this instanceof TakerTrade ? getTaker().getPaymentAccountPayload().getPaymentMethodId() : getTaker().getPaymentMethodId()), this instanceof MakerTrade ? getMaker().getPaymentAccountPayload().getHash() : getMaker().getPaymentAccountPayloadHash(), this instanceof TakerTrade ? getTaker().getPaymentAccountPayload().getHash() : getTaker().getPaymentAccountPayloadHash(), getMaker().getPubKeyRing(), getTaker().getPubKeyRing(), this instanceof MakerTrade ? xmrWalletService.getAddressEntry(getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString() : getMaker().getPayoutAddressString(), // maker payout address this instanceof TakerTrade ? xmrWalletService.getAddressEntry(getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString() : getTaker().getPayoutAddressString(), // taker payout address getMaker().getDepositTxHash(), getTaker().getDepositTxHash() ); return contract; } public MoneroTxWallet createTx(MoneroTxConfig txConfig) { synchronized (walletLock) { if (getWallet() == null) throw new IllegalStateException("Cannot create transaction for trade wallet because it doesn't exist for " + getClass().getSimpleName() + " " + getId()); synchronized (HavenoUtils.getWalletFunctionLock()) { MoneroTxWallet tx = wallet.createTx(txConfig); exportMultisigHex(); return tx; } } } public void exportMultisigHex() { synchronized (walletLock) { log.info("Exporting multisig info for {} {}", getClass().getSimpleName(), getShortId()); if (getWallet() == null) throw new IllegalStateException("Cannot export multisig hex for trade wallet because it doesn't exist for " + getClass().getSimpleName() + " " + getId()); getSelf().setUpdatedMultisigHex(wallet.exportMultisigHex()); saveWallet(); } } public void importMultisigHexIfNeeded() { synchronized (walletLock) { if (getWallet() == null) throw new IllegalStateException("Cannot import multisig hex if needed for trade wallet because it doesn't exist for " + getClass().getSimpleName() + " " + getId()); if (wallet.isMultisigImportNeeded()) { importMultisigHex(); } } } public void scheduleImportMultisigHex() { processModel.setImportMultisigHexScheduled(true); requestPersistence(); } private void importMultisigHexIfScheduled() { if (!isInitialized || isShutDownStarted) return; MoneroTxWallet makerDepositTx = getMaker().getDepositTx(); if (!isDepositsConfirmed() || makerDepositTx == null) return; if (walletHeight.get() - makerDepositTx.getHeight() < NUM_CONFIRMATIONS_FOR_SCHEDULED_IMPORT) return; ThreadUtils.execute(() -> { if (!isInitialized || isShutDownStarted) return; synchronized (getLock()) { if (processModel.isImportMultisigHexScheduled()) { importMultisigHex(); processModel.setImportMultisigHexScheduled(false); } } }, getId()); } public void importMultisigHex() { synchronized (walletLock) { if (getWallet() == null) throw new IllegalStateException("Cannot import multisig hex for trade wallet because it doesn't exist for " + getClass().getSimpleName() + " " + getId()); synchronized (HavenoUtils.getDaemonLock()) { // lock on daemon because import calls full refresh synchronized (HavenoUtils.getImportMultisigLock()) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection(); try { doImportMultisigHex(); break; } catch (IllegalArgumentException | IllegalStateException e) { log.warn("Illegal error importing multisig hex for {} {} on attempt {}/{}: {}", getClass().getSimpleName(), getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); throw e; } catch (Exception e) { log.warn("Error importing multisig hex for {} {} on attempt {}/{}: {}", getClass().getSimpleName(), getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); if (isShutDownStarted && wallet == null) { log.warn("Aborting import of multisig hex for {} {} because shut down is started and wallet is closed", getClass().getSimpleName(), getShortId()); break; } handleWalletError(e, sourceConnection, i + 1); doPollWallet(); if (isPayoutPublished()) break; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } } } } } } private void doImportMultisigHex() { // sync and poll wallet if deposits not confirmed (unless only one deposit unlocked) if (!isDepositsConfirmed() && !hasUnlockedTx()) syncAndPollWallet(); // collect multisig hex from peers List multisigHexes = new ArrayList(); for (TradePeer peer : getOtherPeers()) if (peer.getUpdatedMultisigHex() != null) multisigHexes.add(peer.getUpdatedMultisigHex()); // import multisig hex log.info("Importing multisig hexes for {} {}, count={}", getClass().getSimpleName(), getShortId(), multisigHexes.size()); long startTime = System.currentTimeMillis(); if (!multisigHexes.isEmpty()) { try { wallet.importMultisigHex(multisigHexes.toArray(new String[0])); // check if import is still needed // TODO: we once received a multisig hex which was too short, causing import to still be needed if (wallet.isMultisigImportNeeded()) { String errorMessage = "Multisig import still needed for " + getClass().getSimpleName() + " " + getShortId() + " after already importing, multisigHexes=" + multisigHexes; log.warn(errorMessage); // remove shortest multisig hex if applicable int maxLength = 0; String shortestMultisigHex = null; for (String hex : multisigHexes) { if (shortestMultisigHex == null || hex.length() < shortestMultisigHex.length()) shortestMultisigHex = hex; if (hex.length() > maxLength) maxLength = hex.length(); } if (shortestMultisigHex.length() < maxLength) { log.warn("Removing multisig hex from " + getMultisigHexRole(shortestMultisigHex) + " for " + getClass().getSimpleName() + " " + getShortId() + " because it's the shortest, multisigHex=" + shortestMultisigHex); multisigHexes.remove(shortestMultisigHex); wallet.importMultisigHex(multisigHexes.toArray(new String[0])); } // throw if multisig import still needed if (wallet.isMultisigImportNeeded()) throw new IllegalStateException(errorMessage); } // remove scheduled import processModel.setImportMultisigHexScheduled(false); } catch (MoneroError e) { // import multisig hex individually if one is invalid if (isInvalidImportError(e.getMessage())) { log.warn("Peer has invalid multisig hex for {} {}, importing individually", getClass().getSimpleName(), getShortId()); boolean imported = false; Exception lastError = null; for (TradePeer peer : getOtherPeers()) { if (peer.getUpdatedMultisigHex() == null) continue; try { wallet.importMultisigHex(peer.getUpdatedMultisigHex()); imported = true; } catch (MoneroError e2) { lastError = e2; if (isInvalidImportError(e2.getMessage())) { log.warn("{} has invalid multisig hex for {} {}, error={}, multisigHex={}", getPeerRole(peer), getClass().getSimpleName(), getShortId(), e2.getMessage(), peer.getUpdatedMultisigHex()); } else { throw e2; } } } if (!imported) throw new IllegalArgumentException("Could not import any multisig hexes for " + getClass().getSimpleName() + " " + getShortId(), lastError); } else { throw e; } } saveWallet(); } log.info("Done importing multisig hexes for {} {} in {} ms, count={}", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime, multisigHexes.size()); } private void handleWalletError(Exception e, MoneroRpcConnection sourceConnection, int numAttempts) { if (HavenoUtils.isUnresponsive(e)) forceCloseWallet(); // wallet can be stuck a while if (numAttempts % TradeProtocol.REQUEST_CONNECTION_SWITCH_EVERY_NUM_ATTEMPTS == 0 && !HavenoUtils.isIllegal(e) && xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(sourceConnection); // request connection switch every n attempts if (!isShutDownStarted) getWallet(); // re-open wallet } private String getMultisigHexRole(String multisigHex) { if (multisigHex.equals(getArbitrator().getUpdatedMultisigHex())) return "arbitrator"; if (multisigHex.equals(getBuyer().getUpdatedMultisigHex())) return "buyer"; if (multisigHex.equals(getSeller().getUpdatedMultisigHex())) return "seller"; throw new IllegalArgumentException("Multisig hex does not belong to any peer"); } /** * Create the payout tx. * * @return the payout tx when the trade is successfully completed */ public MoneroTxWallet createPayoutTx() { // check connection to monero daemon verifyDaemonConnection(); // create payout tx synchronized (walletLock) { synchronized (HavenoUtils.getWalletFunctionLock()) { // import multisig hex if needed importMultisigHexIfNeeded(); // create payout tx for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection(); try { MoneroTxWallet unsignedPayoutTx = doCreatePayoutTx(); log.info("Done creating unsigned payout tx for {} {}", getClass().getSimpleName(), getShortId()); return unsignedPayoutTx; } catch (IllegalArgumentException | IllegalStateException e) { throw e; } catch (Exception e) { handleWalletError(e, sourceConnection, i + 1); doPollWallet(); if (isPayoutPublished()) break; log.warn("Failed to create payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } } throw new RuntimeException("Failed to create payout tx for " + getClass().getSimpleName() + " " + getId()); } } } private MoneroTxWallet doCreatePayoutTx() { // check if multisig import needed if (wallet.isMultisigImportNeeded()) throw new IllegalStateException("Cannot create payout tx because multisig import is needed for " + getClass().getSimpleName() + " " + getShortId()); // recover if missing wallet data recoverIfMissingWalletData(); // gather info String sellerPayoutAddress = getSeller().getPayoutAddressString(); String buyerPayoutAddress = getBuyer().getPayoutAddressString(); Preconditions.checkNotNull(sellerPayoutAddress, "Seller payout address must not be null"); Preconditions.checkNotNull(buyerPayoutAddress, "Buyer payout address must not be null"); BigInteger sellerDepositAmount = getSeller().getDepositTx().getIncomingAmount(); BigInteger buyerDepositAmount = hasBuyerAsTakerWithoutDeposit() ? BigInteger.ZERO : getBuyer().getDepositTx().getIncomingAmount(); BigInteger tradeAmount = getAmount(); BigInteger buyerPayoutAmount = buyerDepositAmount.add(tradeAmount); BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount); // create payout tx MoneroTxWallet payoutTx; try { payoutTx = createTx(new MoneroTxConfig() .setAccountIndex(0) .addDestination(buyerPayoutAddress, buyerPayoutAmount) .addDestination(sellerPayoutAddress, sellerPayoutAmount) .setSubtractFeeFrom(0, 1) // split tx fee .setRelay(false) .setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY)); } catch (Exception e) { if (HavenoUtils.isMultisigError(e)) throw new IllegalStateException(e); else throw e; } // update state BigInteger payoutTxFeeSplit = payoutTx.getFee().divide(BigInteger.valueOf(2)); getBuyer().setPayoutTxFee(payoutTxFeeSplit); getBuyer().setPayoutAmount(HavenoUtils.getDestination(buyerPayoutAddress, payoutTx).getAmount()); getSeller().setPayoutTxFee(payoutTxFeeSplit); getSeller().setPayoutAmount(HavenoUtils.getDestination(sellerPayoutAddress, payoutTx).getAmount()); return payoutTx; } public MoneroTxWallet createDisputePayoutTx(Contract contract, DisputeResult disputeResult, boolean updateState) { synchronized (walletLock) { // import multisig hex importMultisigHex(); // sync and poll syncAndPollWallet(); // recover if missing wallet data recoverIfMissingWalletData(); // check if payout tx already published String alreadyPublishedMsg = "Cannot create dispute payout tx because payout tx is already published for trade " + getId(); if (isPayoutPublished()) throw new RuntimeException(alreadyPublishedMsg); // create unsigned dispute payout tx if (updateState) log.info("Creating unsigned dispute payout tx for trade {}", getId()); try { // trade wallet must be synced if (getWallet().isMultisigImportNeeded()) throw new RuntimeException("Arbitrator's wallet needs updated multisig hex to create payout tx which means a trader must have already broadcast the payout tx for trade " + getId()); // check amounts if (disputeResult.getBuyerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Buyer payout cannot be negative"); if (disputeResult.getSellerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Seller payout cannot be negative"); if (disputeResult.getBuyerPayoutAmountBeforeCost().add(disputeResult.getSellerPayoutAmountBeforeCost()).compareTo(getWallet().getUnlockedBalance()) > 0) { throw new RuntimeException("The payout amounts are more than the wallet's unlocked balance, unlocked balance=" + getWallet().getUnlockedBalance() + " vs " + disputeResult.getBuyerPayoutAmountBeforeCost() + " + " + disputeResult.getSellerPayoutAmountBeforeCost() + " = " + (disputeResult.getBuyerPayoutAmountBeforeCost().add(disputeResult.getSellerPayoutAmountBeforeCost()))); } // create dispute payout tx config MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0); String buyerPayoutAddress = contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString(); String sellerPayoutAddress = contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString(); txConfig.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY); if (disputeResult.getBuyerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(buyerPayoutAddress, disputeResult.getBuyerPayoutAmountBeforeCost()); if (disputeResult.getSellerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(sellerPayoutAddress, disputeResult.getSellerPayoutAmountBeforeCost()); // configure who pays mining fee BigInteger loserPayoutAmount = disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmountBeforeCost() : disputeResult.getBuyerPayoutAmountBeforeCost(); if (loserPayoutAmount.equals(BigInteger.ZERO)) txConfig.setSubtractFeeFrom(0); // winner pays fee if loser gets 0 else { switch (disputeResult.getSubtractFeeFrom()) { case BUYER_AND_SELLER: txConfig.setSubtractFeeFrom(0, 1); break; case BUYER_ONLY: txConfig.setSubtractFeeFrom(0); break; case SELLER_ONLY: txConfig.setSubtractFeeFrom(1); break; } } // create dispute payout tx MoneroTxWallet payoutTx = createDisputePayoutTx(txConfig); // update trade state if (updateState) { getProcessModel().setUnsignedPayoutTx(payoutTx); setPayoutTx(payoutTx); if (getBuyer().getUpdatedMultisigHex() != null) getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); if (getSeller().getUpdatedMultisigHex() != null) getSeller().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); } requestPersistence(); return payoutTx; } catch (Exception e) { syncAndPollWallet(); if (isPayoutPublished()) throw new IllegalStateException(alreadyPublishedMsg); throw e; } catch (AssertionError e) { // tx creation throws assertion error with invalid config syncAndPollWallet(); if (isPayoutPublished()) throw new IllegalStateException(alreadyPublishedMsg); throw new RuntimeException(e); } } } private MoneroTxWallet createDisputePayoutTx(MoneroTxConfig txConfig) { synchronized (walletLock) { if (getWallet() == null) throw new IllegalStateException("Cannot create dispute payout tx for trade wallet because it doesn't exist for " + getClass().getSimpleName() + " " + getId()); synchronized (HavenoUtils.getWalletFunctionLock()) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection(); try { if (wallet.isMultisigImportNeeded()) throw new IllegalStateException("Cannot create dispute payout tx because multisig import is needed for " + getClass().getSimpleName() + " " + getShortId()); return createTx(txConfig); } catch (IllegalArgumentException | IllegalStateException e) { throw e; } catch (Exception e) { if (HavenoUtils.isMultisigError(e)) throw new IllegalStateException(e); if (e.getMessage().contains("not possible")) throw new IllegalArgumentException("Loser payout is too small to cover the mining fee"); handleWalletError(e, sourceConnection, i + 1); doPollWallet(); if (isPayoutPublished()) break; log.warn("Failed to create dispute payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } } throw new RuntimeException("Failed to create payout tx for " + getClass().getSimpleName() + " " + getId()); } } } /** * Process a payout tx. * * @param payoutTxHex is the payout tx hex to verify * @param sign signs the payout tx if true * @param publish publishes the signed payout tx if true */ public void processPayoutTx(String payoutTxHex, boolean sign, boolean publish) { synchronized (walletLock) { synchronized (HavenoUtils.getWalletFunctionLock()) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection(); try { doProcessPayoutTx(payoutTxHex, sign, publish); break; } catch (IllegalArgumentException | IllegalStateException e) { throw e; } catch (Exception e) { handleWalletError(e, sourceConnection, i + 1); doPollWallet(); if (isPayoutPublished()) break; log.warn("Failed to process payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage(), e); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } finally { saveWallet(); persistNow(null); } } } } } private void doProcessPayoutTx(String payoutTxHex, boolean sign, boolean publish) { log.info("Processing payout tx for {} {}", getClass().getSimpleName(), getId()); // recover if missing wallet data recoverIfMissingWalletData(); // gather relevant info getWallet(); Contract contract = getContract(); BigInteger sellerDepositAmount = getSeller().getDepositTx().getIncomingAmount(); BigInteger buyerDepositAmount = hasBuyerAsTakerWithoutDeposit() ? BigInteger.ZERO : getBuyer().getDepositTx().getIncomingAmount(); BigInteger tradeAmount = getAmount(); // describe payout tx MoneroTxSet describedTxSet = wallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex)); if (describedTxSet.getTxs() == null || describedTxSet.getTxs().size() != 1) throw new IllegalArgumentException("Bad payout tx"); // TODO (woodser): test nack MoneroTxWallet payoutTx = describedTxSet.getTxs().get(0); if (payoutTxId == null) setPayoutTx(payoutTx); // update payout tx if id currently unknown // verify payout tx has exactly 2 destinations if (payoutTx.getOutgoingTransfer() == null || payoutTx.getOutgoingTransfer().getDestinations() == null || payoutTx.getOutgoingTransfer().getDestinations().size() != 2) throw new IllegalArgumentException("Payout tx does not have exactly two destinations"); // get buyer and seller destinations (order not preserved) boolean buyerFirst = payoutTx.getOutgoingTransfer().getDestinations().get(0).getAddress().equals(contract.getBuyerPayoutAddressString()); MoneroDestination buyerPayoutDestination = payoutTx.getOutgoingTransfer().getDestinations().get(buyerFirst ? 0 : 1); MoneroDestination sellerPayoutDestination = payoutTx.getOutgoingTransfer().getDestinations().get(buyerFirst ? 1 : 0); // verify payout addresses if (!buyerPayoutDestination.getAddress().equals(contract.getBuyerPayoutAddressString())) throw new IllegalArgumentException("Buyer payout address does not match contract"); if (!sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new IllegalArgumentException("Seller payout address does not match contract"); // verify change address is multisig's primary address if (!payoutTx.getChangeAmount().equals(BigInteger.ZERO)) log.warn("Dust left in multisig wallet for {} {}: {}", getClass().getSimpleName(), getId(), payoutTx.getChangeAmount()); if (!payoutTx.getChangeAmount().equals(BigInteger.ZERO) && !payoutTx.getChangeAddress().equals(wallet.getPrimaryAddress())) throw new IllegalArgumentException("Change address is not multisig wallet's primary address"); // verify sum of outputs = destination amounts + change amount if (!payoutTx.getOutputSum().equals(buyerPayoutDestination.getAmount().add(sellerPayoutDestination.getAmount()).add(payoutTx.getChangeAmount()))) throw new IllegalArgumentException("Sum of outputs != destination amounts + change amount"); // verify buyer destination amount is deposit amount + this amount - 1/2 tx costs BigInteger txCost = payoutTx.getFee().add(payoutTx.getChangeAmount()); BigInteger txCostSplit = txCost.divide(BigInteger.valueOf(2)); BigInteger expectedBuyerPayout = buyerDepositAmount.add(tradeAmount).subtract(txCostSplit); if (!buyerPayoutDestination.getAmount().equals(expectedBuyerPayout)) throw new IllegalArgumentException("Buyer destination amount is not deposit amount + trade amount - 1/2 tx costs, " + buyerPayoutDestination.getAmount() + " vs " + expectedBuyerPayout); // verify seller destination amount is deposit amount - this amount - 1/2 tx costs BigInteger expectedSellerPayout = sellerDepositAmount.subtract(tradeAmount).subtract(txCostSplit); if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new IllegalArgumentException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout); // update payout tx setPayoutTx(payoutTx); // check connection boolean doSign = sign && getPayoutTxHex() == null; if (doSign || publish) verifyDaemonConnection(); // handle tx signing if (doSign) { // sign tx String signedPayoutTxHex; try { MoneroMultisigSignResult result = wallet.signMultisigTxHex(payoutTxHex); if (result.getSignedMultisigTxHex() == null) throw new IllegalArgumentException("Error signing payout tx, signed multisig hex is null"); signedPayoutTxHex = result.getSignedMultisigTxHex(); } catch (Exception e) { throw new IllegalStateException(e); } // verify miner fee is within tolerance unless outdated offer version if (getOffer().getOfferPayload().getProtocolVersion() >= 2) { // verify fee is within tolerance by recreating payout tx // TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated? log.info("Creating fee estimate tx for {} {}", getClass().getSimpleName(), getShortId()); saveWallet(); // save wallet before creating fee estimate tx MoneroTxWallet feeEstimateTx = createPayoutTx(); HavenoUtils.verifyMinerFee(feeEstimateTx.getFee(), payoutTx.getFee()); log.info("Payout tx fee is within tolerance for {} {}", getClass().getSimpleName(), getShortId()); } // set signed payout tx hex setPayoutTxHex(signedPayoutTxHex); // describe result describedTxSet = wallet.describeMultisigTxSet(getPayoutTxHex()); payoutTx = describedTxSet.getTxs().get(0); setPayoutTx(payoutTx); } // save trade state saveWallet(); requestPersistence(); // submit payout tx boolean doPublish = publish && !isPayoutPublished(); if (doPublish) { try { List payoutTxIds = wallet.submitMultisigTxHex(getPayoutTxHex()); payoutTxId = payoutTxIds.get(0); setPayoutStatePublished(); } catch (Exception e) { if (!isPayoutPublished()) { if (HavenoUtils.isTransactionRejected(e) || HavenoUtils.isMultisigError(e)) throw new IllegalArgumentException(e); throw new RuntimeException("Failed to submit payout tx for " + getClass().getSimpleName() + " " + getId() + ", error=" + e.getMessage(), e); } } } } public void processDisputePayoutTx() { synchronized (walletLock) { synchronized (HavenoUtils.getWalletFunctionLock()) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection(); try { doProcessDisputePayoutTx(); break; } catch (IllegalArgumentException | IllegalStateException e) { throw e; } catch (Exception e) { handleWalletError(e, sourceConnection, i + 1); doPollWallet(); if (isPayoutPublished()) break; log.warn("Failed to process dispute payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage(), e); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } finally { saveWallet(); persistNow(null); } } } } } private MoneroTxSet doProcessDisputePayoutTx() { log.info("Processing dispute payout tx for {} {}", getClass().getSimpleName(), getId()); // recover if missing wallet data recoverIfMissingWalletData(); // gather trade info MoneroWallet multisigWallet = getWallet(); Optional disputeOptional = HavenoUtils.arbitrationManager.findDispute(getId()); if (!disputeOptional.isPresent()) throw new IllegalArgumentException("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + getId()); Dispute dispute = disputeOptional.get(); Contract contract = dispute.getContract(); DisputeResult disputeResult = dispute.getDisputeResultProperty().get(); String unsignedPayoutTxHex = getArbitrator().getDisputeClosedMessage().getUnsignedPayoutTxHex(); // Offer offer = checkNotNull(trade.getOffer(), "offer must not be null"); // BigInteger sellerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? trade.getMaker().getDepositTxHash() : trade.getTaker().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): use contract instead of trade to get deposit tx ids when contract has deposit tx ids // BigInteger buyerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? trade.getTaker().getDepositTxHash() : trade.getMaker().getDepositTxHash()).getIncomingAmount(); // BigInteger tradeAmount = BigInteger.valueOf(contract.getTradeAmount().value).multiply(ParsingUtils.XMR_SATOSHI_MULTIPLIER); // parse arbitrator-signed payout tx MoneroTxSet disputeTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(unsignedPayoutTxHex)); if (disputeTxSet.getTxs() == null || disputeTxSet.getTxs().size() != 1) throw new IllegalArgumentException("Bad arbitrator-signed payout tx"); // TODO (woodser): nack MoneroTxWallet arbitratorSignedPayoutTx = disputeTxSet.getTxs().get(0); // verify payout tx has 1 or 2 destinations int numDestinations = arbitratorSignedPayoutTx.getOutgoingTransfer() == null || arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations() == null ? 0 : arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations().size(); if (numDestinations != 1 && numDestinations != 2) throw new IllegalArgumentException("Buyer-signed payout tx does not have 1 or 2 destinations"); // get buyer and seller destinations (order not preserved) List destinations = arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations(); boolean buyerFirst = destinations.get(0).getAddress().equals(contract.getBuyerPayoutAddressString()); MoneroDestination buyerPayoutDestination = buyerFirst ? destinations.get(0) : numDestinations == 2 ? destinations.get(1) : null; MoneroDestination sellerPayoutDestination = buyerFirst ? (numDestinations == 2 ? destinations.get(1) : null) : destinations.get(0); // verify payout addresses if (buyerPayoutDestination != null && !buyerPayoutDestination.getAddress().equals(contract.getBuyerPayoutAddressString())) throw new IllegalArgumentException("Buyer payout address does not match contract"); if (sellerPayoutDestination != null && !sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new IllegalArgumentException("Seller payout address does not match contract"); // verify change address is multisig's primary address if (!arbitratorSignedPayoutTx.getChangeAmount().equals(BigInteger.ZERO) && !arbitratorSignedPayoutTx.getChangeAddress().equals(multisigWallet.getPrimaryAddress())) throw new IllegalArgumentException("Change address is not multisig wallet's primary address"); // verify sum of outputs = destination amounts + change amount BigInteger destinationSum = (buyerPayoutDestination == null ? BigInteger.ZERO : buyerPayoutDestination.getAmount()).add(sellerPayoutDestination == null ? BigInteger.ZERO : sellerPayoutDestination.getAmount()); if (!arbitratorSignedPayoutTx.getOutputSum().equals(destinationSum.add(arbitratorSignedPayoutTx.getChangeAmount()))) throw new IllegalArgumentException("Sum of outputs != destination amounts + change amount"); // get actual payout amounts BigInteger actualBuyerAmount = buyerPayoutDestination == null ? BigInteger.ZERO : buyerPayoutDestination.getAmount(); BigInteger actualSellerAmount = sellerPayoutDestination == null ? BigInteger.ZERO : sellerPayoutDestination.getAmount(); // verify payouts sum to unlocked balance within loss of precision due to conversion to centineros BigInteger txCost = arbitratorSignedPayoutTx.getFee().add(arbitratorSignedPayoutTx.getChangeAmount()); // cost = fee + lost dust change if (!arbitratorSignedPayoutTx.getChangeAmount().equals(BigInteger.ZERO)) log.warn("Dust left in multisig wallet for {} {}: {}", getClass().getSimpleName(), getId(), arbitratorSignedPayoutTx.getChangeAmount()); if (getWallet().getUnlockedBalance().subtract(actualBuyerAmount.add(actualSellerAmount).add(txCost)).compareTo(BigInteger.ZERO) > 0) { throw new IllegalArgumentException("The dispute payout amounts do not sum to the wallet's unlocked balance while verifying the dispute payout tx, unlocked balance=" + getWallet().getUnlockedBalance() + " vs sum payout amount=" + actualBuyerAmount.add(actualSellerAmount) + ", buyer payout=" + actualBuyerAmount + ", seller payout=" + actualSellerAmount); } // verify payout amounts BigInteger[] buyerSellerPayoutTxCost = getBuyerSellerPayoutTxCost(disputeResult, txCost); BigInteger expectedBuyerAmount = disputeResult.getBuyerPayoutAmountBeforeCost().subtract(buyerSellerPayoutTxCost[0]); BigInteger expectedSellerAmount = disputeResult.getSellerPayoutAmountBeforeCost().subtract(buyerSellerPayoutTxCost[1]); if (!expectedBuyerAmount.equals(actualBuyerAmount)) throw new IllegalArgumentException("Unexpected buyer payout: " + expectedBuyerAmount + " vs " + actualBuyerAmount); if (!expectedSellerAmount.equals(actualSellerAmount)) throw new IllegalArgumentException("Unexpected seller payout: " + expectedSellerAmount + " vs " + actualSellerAmount); // check daemon connection verifyDaemonConnection(); // sign arbitrator-signed payout tx if (getPayoutTxHex() == null) { try { log.info("Signing dispute payout tx for {} {}", getClass().getSimpleName(), getShortId()); MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(unsignedPayoutTxHex); if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx"); String signedMultisigTxHex = result.getSignedMultisigTxHex(); disputeTxSet.setMultisigTxHex(signedMultisigTxHex); setPayoutTxHex(signedMultisigTxHex); HavenoUtils.arbitrationManager.requestPersistence(this); // TODO: no need to update disputes so far? } catch (Exception e) { throw new IllegalStateException(e.getMessage()); } // verify mining fee is within tolerance by recreating payout tx // TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated? MoneroTxWallet feeEstimateTx = null; try { log.info("Creating dispute fee estimate tx for {} {}", getClass().getSimpleName(), getShortId()); feeEstimateTx = createDisputePayoutTx(dispute.getContract(), disputeResult, false); } catch (Exception e) { if (isPayoutPublished()) log.warn("Payout tx already published for {} {}, skipping fee verification", getClass().getSimpleName(), getShortId()); else throw new RuntimeException("Could not recreate dispute payout tx to verify fee: " + e.getMessage(), e); } if (feeEstimateTx != null) { HavenoUtils.verifyMinerFee(feeEstimateTx.getFee(), arbitratorSignedPayoutTx.getFee()); log.info("Dispute payout tx fee is within tolerance for {} {}", getClass().getSimpleName(), getShortId()); } } else { log.warn("Payout tx already signed for {} {}, skipping signing", getClass().getSimpleName(), getShortId()); disputeTxSet.setMultisigTxHex(getPayoutTxHex()); } // submit fully signed payout tx to the network for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection(); try { List txHashes = multisigWallet.submitMultisigTxHex(disputeTxSet.getMultisigTxHex()); disputeTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed break; } catch (Exception e) { if (isPayoutPublished()) return null; if (HavenoUtils.isMultisigError(e)) throw new IllegalArgumentException(e); log.warn("Failed to submit dispute payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (getXmrConnectionService().isConnected()) requestSwitchToNextBestConnection(sourceConnection); HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } } // update state setPayoutTx(disputeTxSet.getTxs().get(0)); setPayoutState(Trade.PayoutState.PAYOUT_PUBLISHED); dispute.setDisputePayoutTxId(disputeTxSet.getTxs().get(0).getHash()); HavenoUtils.arbitrationManager.requestPersistence(this); return disputeTxSet; } private static BigInteger[] getBuyerSellerPayoutTxCost(DisputeResult disputeResult, BigInteger payoutTxCost) { boolean isBuyerWinner = disputeResult.getWinner() == Winner.BUYER; BigInteger loserAmount = isBuyerWinner ? disputeResult.getSellerPayoutAmountBeforeCost() : disputeResult.getBuyerPayoutAmountBeforeCost(); if (loserAmount.equals(BigInteger.ZERO)) { BigInteger buyerPayoutTxFee = isBuyerWinner ? payoutTxCost : BigInteger.ZERO; BigInteger sellerPayoutTxFee = isBuyerWinner ? BigInteger.ZERO : payoutTxCost; return new BigInteger[] { buyerPayoutTxFee, sellerPayoutTxFee }; } else { switch (disputeResult.getSubtractFeeFrom()) { case BUYER_AND_SELLER: BigInteger payoutTxFeeSplit = payoutTxCost.divide(BigInteger.valueOf(2)); return new BigInteger[] { payoutTxFeeSplit, payoutTxFeeSplit }; case BUYER_ONLY: return new BigInteger[] { payoutTxCost, BigInteger.ZERO }; case SELLER_ONLY: return new BigInteger[] { BigInteger.ZERO, payoutTxCost }; default: throw new RuntimeException("Unsupported subtract fee from: " + disputeResult.getSubtractFeeFrom()); } } } /** * In case there's a problem observing the payout tx (e.g. due to stale multisig state), * peers can communicate the payout tx id. * * @param payoutTxId is the payout tx id to process */ public void processBuyerPayout(String payoutTxId) { if (payoutTxId == null) throw new IllegalArgumentException("Payout tx id cannot be null"); if (!isBuyer()) throw new IllegalStateException("Only buyer can process buyer payout tx for " + getClass().getSimpleName() + " " + getShortId()); // poll the main wallet log.warn("Processing payout tx for {} {} by polling main wallet", getClass().getSimpleName(), getShortId()); try { xmrWalletService.doPollWallet(); } catch (Exception e) { // use default error handling } // fetch payout tx from main wallet MoneroTxWallet payoutTx = xmrWalletService.getWallet().getTx(payoutTxId); if (payoutTx == null) throw new IllegalStateException("Payout tx id " + payoutTxId + " not found for " + getClass().getSimpleName() + " " + getId()); if (payoutTx.isFailed()) throw new IllegalStateException("Payout tx " + payoutTxId + " is failed for " + getClass().getSimpleName() + " " + getId()); // verify incoming amount BigInteger txCost = payoutTx.getFee(); BigInteger txCostSplit = txCost.divide(BigInteger.valueOf(2)); BigInteger expectedAmount = getBuyer().getSecurityDeposit().add(getAmount()).subtract(txCostSplit); if (!payoutTx.getIncomingAmount().equals(expectedAmount)) throw new IllegalStateException("Payout tx incoming amount is not deposit amount + trade amount - 1/2 tx costs, " + payoutTx.getIncomingAmount() + " vs " + getBuyer().getSecurityDeposit().add(getAmount()).subtract(txCostSplit)); // update payout tx setPayoutTx(payoutTx); } /** * Decrypt the peer's payment account payload using the given key. * * @param paymentAccountKey is the key to decrypt the payment account payload */ public void decryptPeerPaymentAccountPayload(byte[] paymentAccountKey) { try { // decrypt payment account payload getTradePeer().setPaymentAccountKey(paymentAccountKey); SecretKey sk = Encryption.getSecretKeyFromBytes(getTradePeer().getPaymentAccountKey()); byte[] decryptedPaymentAccountPayload = Encryption.decrypt(getTradePeer().getEncryptedPaymentAccountPayload(), sk); CoreNetworkProtoResolver resolver = new CoreNetworkProtoResolver(Clock.systemDefaultZone()); // TODO: reuse resolver from elsewhere? PaymentAccountPayload paymentAccountPayload = resolver.fromProto(protobuf.PaymentAccountPayload.parseFrom(decryptedPaymentAccountPayload)); // verify hash of payment account payload byte[] peerPaymentAccountPayloadHash = this instanceof MakerTrade ? getContract().getTakerPaymentAccountPayloadHash() : getContract().getMakerPaymentAccountPayloadHash(); if (!Arrays.equals(paymentAccountPayload.getHash(), peerPaymentAccountPayloadHash)) throw new RuntimeException("Hash of peer's payment account payload does not match contract"); // set payment account payload getTradePeer().setPaymentAccountPayload(paymentAccountPayload); processModel.getPaymentAccountDecryptedProperty().set(true); } catch (Exception e) { throw new RuntimeException(e); } } @Nullable public MoneroTxWallet getTakerDepositTx() { return getTaker().getDepositTx(); } @Nullable public MoneroTxWallet getMakerDepositTx() { return getMaker().getDepositTx(); } public Long getNumDepositConfirmations() { MoneroTxWallet makerDepositTx = getMakerDepositTx(); MoneroTxWallet takerDepositTx = getTakerDepositTx(); if (makerDepositTx == null || (takerDepositTx == null && !hasBuyerAsTakerWithoutDeposit())) return null; if (Boolean.TRUE.equals(makerDepositTx.isFailed()) || (takerDepositTx != null && Boolean.TRUE.equals(takerDepositTx.isFailed()))) return null; if (makerDepositTx.getHeight() == null || (takerDepositTx != null && takerDepositTx.getHeight() == null)) return null; return hasBuyerAsTakerWithoutDeposit() ? makerDepositTx.getNumConfirmations() : Math.min(makerDepositTx.getNumConfirmations(), takerDepositTx.getNumConfirmations()); } private Long getDepositsConfirmedHeight() { MoneroTxWallet makerDepositTx = getMakerDepositTx(); MoneroTxWallet takerDepositTx = getTakerDepositTx(); if (makerDepositTx == null || (takerDepositTx == null && !hasBuyerAsTakerWithoutDeposit())) return null; if (Boolean.TRUE.equals(makerDepositTx.isFailed()) || (takerDepositTx != null && Boolean.TRUE.equals(takerDepositTx.isFailed()))) return null; if (makerDepositTx.getHeight() == null || (takerDepositTx != null && takerDepositTx.getHeight() == null)) return null; return Math.max(makerDepositTx.getHeight(), hasBuyerAsTakerWithoutDeposit() ? 0l : takerDepositTx.getHeight()); } private Long getDepositsFinalizedHeight() { Long depositsConfirmedHeight = getDepositsConfirmedHeight(); if (depositsConfirmedHeight == null) return null; return depositsConfirmedHeight + NUM_BLOCKS_DEPOSITS_FINALIZED - 1; } public void addAndPersistChatMessage(ChatMessage chatMessage) { synchronized (chatMessages) { if (!chatMessages.contains(chatMessage)) { chatMessages.add(chatMessage); } else { log.error("Trade ChatMessage already exists"); } } } public boolean removeAllChatMessages() { synchronized (chatMessages) { if (chatMessages.size() > 0) { chatMessages.clear(); return true; } return false; } } 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 // without the penalty. long payoutAmountFromMediation = processModel.getSellerPayoutAmountFromMediation(); long normalPayoutAmount = getSeller().getSecurityDeposit().longValueExact(); return payoutAmountFromMediation < normalPayoutAmount; } public void clearAndShutDown() { // unregister p2p message listener immediately removeDecryptedDirectMessageListener(); // clear process data and shut down trade ThreadUtils.execute(() -> { clearProcessData(); onShutDownStarted(); ThreadUtils.submitToPool(() -> shutDown()); // run off trade thread }, getId()); } private void clearProcessData() { // delete trade wallet synchronized (walletLock) { if (!walletExists()) return; // done if already cleared deleteWallet(); } // TODO: clear other process data if (isPayoutFinalized() || processModel.isPaymentReceivedMessagesAcked()) setPayoutTxHex(null); for (TradePeer peer : getAllPeers()) { peer.setUpdatedMultisigHex(null); peer.setDisputeClosedMessage(null); peer.setPaymentSentMessage(null); peer.setDepositTxHex(null); peer.setDepositTxKey(null); if (peer.isPaymentReceivedMessageAckedOrNacked() || isPayoutFinalized()) peer.setUnsignedPayoutTxHex(null); if (peer.isPaymentReceivedMessageAckedOrNacked()) peer.setPaymentReceivedMessage(null); } } private void removeDecryptedDirectMessageListener() { if (getProcessModel() == null || getProcessModel().getProvider() == null || getProcessModel().getP2PService() == null) return; getProcessModel().getP2PService().removeDecryptedDirectMessageListener(getProtocol()); } public void maybeClearSensitiveData() { String change = ""; if (contract != null && contract.maybeClearSensitiveData()) { change += "contract;"; } if (processModel != null && processModel.maybeClearSensitiveData()) { change += "processModel;"; } if (contractAsJson != null) { String edited = Contract.sanitizeContractAsJson(contractAsJson); if (!edited.equals(contractAsJson)) { contractAsJson = edited; change += "contractAsJson;"; } } if (removeAllChatMessages()) { change += "chat messages;"; } if (change.length() > 0) { log.info("Cleared sensitive data from {} of {} {}", change, getClass().getSimpleName(), getShortId()); } } public void onShutDownStarted() { if (!isShutDownStarted) { if (wallet != null) log.info("Preparing to shut down {} {}", getClass().getSimpleName(), getId()); isShutDownStarted = true; stopPolling(); } } public void shutDown() { if (isShutDown) return; // ignore if already shut down onShutDownStarted(); boolean isUnlockedAndDeleted = isPayoutUnlocked() && !walletExistsNoSync(); // previous versions deleted wallet after payout unlocked if (!isPayoutFinalized() && !isUnlockedAndDeleted) log.info("Shutting down {} {}", getClass().getSimpleName(), getId()); // unregister p2p message listener removeDecryptedDirectMessageListener(); // create task to shut down trade Runnable shutDownTask = () -> { // repeatedly acquire lock to clear tasks for (int i = 0; i < 20; i++) { synchronized (getLock()) { HavenoUtils.waitFor(10); } } // shut down trade threads isShutDown = true; List shutDownThreads = new ArrayList<>(); shutDownThreads.add(() -> ThreadUtils.shutDown(getId())); shutDownThreads.add(() -> ThreadUtils.shutDown(getTradeEventThreadId())); ThreadUtils.awaitTasks(shutDownThreads); stopProtocolTimeout(); isInitialized = false; // close trade wallet, force close if syncing if (isSyncing()) forceCloseWallet(); else { try { closeWallet(); } catch (Exception e) { // warning will be logged for main wallet, so skip logging here //log.warn("Error closing monero-wallet-rpc subprocess for {} {}: {}. Was Haveno stopped manually with ctrl+c?", getClass().getSimpleName(), getId(), e.getMessage()); } } }; // shut down trade with timeout try { ThreadUtils.awaitTask(shutDownTask, SHUTDOWN_TIMEOUT_MS); } catch (Exception e) { log.warn("Error shutting down {} {}: {}\n", getClass().getSimpleName(), getId(), e.getMessage(), e); // force close wallet forceCloseWallet(); } // de-initialize if (idleBlockPoller != null) { xmrWalletService.removeWalletListener(idleBlockPoller); idleBlockPoller = null; } UserThread.execute(() -> { if (tradeStateSubscription != null) tradeStateSubscription.unsubscribe(); if (tradePhaseSubscription != null) tradePhaseSubscription.unsubscribe(); if (payoutStateSubscription != null) payoutStateSubscription.unsubscribe(); if (disputeStateSubscription != null) disputeStateSubscription.unsubscribe(); }); } /////////////////////////////////////////////////////////////////////////////////////////// // Trade error cleanup /////////////////////////////////////////////////////////////////////////////////////////// public void onProtocolInitializationError() { // check if deposits published if (isDepositsPublished()) { restoreDepositsPublishedTrade(); return; } // remove if deposit not requested or is failed if (!isDepositRequested() || isDepositRequestFailed()) { removeTradeOnError(); return; } // done if wallet already deleted if (!walletExists()) { removeTradeOnError(); return; } // set error height if (processModel.getTradeProtocolErrorHeight() == 0) { log.warn("Scheduling to remove trade if unfunded for {} {} from height {}", getClass().getSimpleName(), getId(), xmrConnectionService.getTargetHeight()); processModel.setTradeProtocolErrorHeight(xmrConnectionService.getTargetHeight()); // height denotes scheduled error handling } // move to failed trades processModel.getTradeManager().onMoveInvalidTradeToFailedTrades(this); requestPersistence(); // listen for deposits published to restore trade protocolErrorStateSubscription = EasyBind.subscribe(stateProperty(), state -> { if (isDepositsPublished()) { restoreDepositsPublishedTrade(); if (protocolErrorStateSubscription != null) { // unsubscribe protocolErrorStateSubscription.unsubscribe(); protocolErrorStateSubscription = null; } } }); // listen for block confirmations to remove trade long startTime = System.currentTimeMillis(); protocolErrorHeightSubscription = EasyBind.subscribe(walletHeight, lastWalletHeight -> { if (isShutDown || isDepositsPublished()) return; if (lastWalletHeight.longValue() < processModel.getTradeProtocolErrorHeight() + DELETE_AFTER_NUM_BLOCKS) return; if (System.currentTimeMillis() - startTime < DELETE_AFTER_MS) return; // remove on trade thread ThreadUtils.execute(() -> { try { // get trade's deposit txs from daemon MoneroTx makerDepositTx = getMaker().getDepositTxHash() == null ? null : xmrWalletService.getMonerod().getTx(getMaker().getDepositTxHash()); MoneroTx takerDepositTx = getTaker().getDepositTxHash() == null ? null : xmrWalletService.getMonerod().getTx(getTaker().getDepositTxHash()); // remove trade and wallet if neither deposit tx published if (makerDepositTx == null && takerDepositTx == null) { log.warn("Deleting {} {} after protocol error", getClass().getSimpleName(), getId()); if (this instanceof ArbitratorTrade && (getMaker().getReserveTxHash() != null || getTaker().getReserveTxHash() != null)) { processModel.getTradeManager().onMoveInvalidTradeToFailedTrades(this); // arbitrator retains trades with reserved funds for analysis and penalty deleteWallet(); onShutDownStarted(); ThreadUtils.submitToPool(() -> shutDown()); // run off thread } else { removeTradeOnError(); } } else if (!isPayoutPublished()) { // set error if wallet may be partially funded String errorMessage = "Refusing to delete " + getClass().getSimpleName() + " " + getId() + " after protocol error because its wallet might be funded"; prependErrorMessage(errorMessage); log.warn(errorMessage); } // unsubscribe if (protocolErrorHeightSubscription != null) { protocolErrorHeightSubscription.unsubscribe(); protocolErrorHeightSubscription = null; } } catch (Exception e) { log.warn("Error during protocol error handling for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage(), e); } }, getId()); }); } public boolean isProtocolErrorHandlingScheduled() { return processModel.getTradeProtocolErrorHeight() > 0; } private void restoreDepositsPublishedTrade() { // close open offer if (this instanceof MakerTrade && processModel.getOpenOfferManager().getOpenOffer(getId()).isPresent()) { log.info("Closing open offer because {} {} was restored after protocol error", getClass().getSimpleName(), getShortId()); processModel.getOpenOfferManager().closeSpentOffer(checkNotNull(getOffer())); } // re-freeze outputs xmrWalletService.freezeOutputs(getSelf().getReserveTxKeyImages()); // restore trade from failed trades processModel.getTradeManager().onMoveFailedTradeToPendingTrades(this); } private void removeTradeOnError() { synchronized (removeTradeOnErrorLock) { // skip if already shut down or removed if (isShutDown || !processModel.getTradeManager().hasTrade(getId())) return; log.warn("removeTradeOnError() for {} {}, state={}", getClass().getSimpleName(), getShortId(), getState()); // force close and re-open wallet in case stuck forceCloseWallet(); if (isDepositRequested()) getWallet(); // unreserve taker's key images if (this instanceof TakerTrade) { ThreadUtils.submitToPool(() -> { xmrWalletService.thawOutputs(getSelf().getReserveTxKeyImages()); }); } // unreserve maker's open offer Optional openOffer = processModel.getOpenOfferManager().getOpenOffer(this.getId()); if (this instanceof MakerTrade && openOffer.isPresent()) { processModel.getOpenOfferManager().unreserveOpenOffer(openOffer.get()); } // clear and shut down trade clearAndShutDown(); // unregister trade processModel.getTradeManager().unregisterTrade(this); } } /////////////////////////////////////////////////////////////////////////////////////////// // Model implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onComplete() { } /////////////////////////////////////////////////////////////////////////////////////////// // Abstract /////////////////////////////////////////////////////////////////////////////////////////// public abstract BigInteger getPayoutAmountBeforeCost(); public abstract boolean confirmPermitted(); /////////////////////////////////////////////////////////////////////////////////////////// // Setters /////////////////////////////////////////////////////////////////////////////////////////// public void addInitProgressStep() { startProtocolTimeout(); initProgress = Math.min(1.0, (double) ++initStep / TOTAL_INIT_STEPS); //if (this instanceof TakerTrade) log.warn("Init step count: " + initStep); // log init step count for taker trades in order to update total steps UserThread.execute(() -> initProgressProperty.set(initProgress)); } public void startProtocolTimeout() { getProtocol().startTimeout(TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS); } public void stopProtocolTimeout() { if (!isInitialized) return; TradeProtocol protocol = getProtocol(); if (protocol == null) return; protocol.stopTimeout(); } public void setStateIfValidTransitionTo(State newState) { if (state.isValidTransitionTo(newState)) { setState(newState); } } public void setState(State state) { // skip if no change if (state.ordinal() == this.state.ordinal()) return; // skip logging until initialized if (isInitialized) { log.info("Set new state for trade {} {}: {}", getShortId(), this.getClass().getSimpleName(), state); } // warn if reverting state if (state.getPhase().ordinal() < this.state.getPhase().ordinal()) { String message = "We got a state change to a previous phase for " + getClass().getSimpleName() + " " + getShortId() + "\n" + "Old state is: " + this.state + ". New state is: " + state; log.warn(message); } this.state = state; persistNow(null); UserThread.execute(() -> { stateProperty.set(state); phaseProperty.set(state.getPhase()); }); // automatically advance unlocked state to finalized if sufficient confirmations if (state == State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN) { Long numDepositsConfirmations = getNumDepositConfirmations(); if (numDepositsConfirmations != null && numDepositsConfirmations >= NUM_BLOCKS_DEPOSITS_FINALIZED) { log.info("Auto-advancing state to {} for {} {} because deposits are unlocked and have at least {} confirmations", State.DEPOSIT_TXS_FINALIZED_IN_BLOCKCHAIN, this.getClass().getSimpleName(), getShortId(), NUM_BLOCKS_DEPOSITS_FINALIZED); setStateDepositsFinalized(); } } } public void advanceState(State state) { if (state.ordinal() > getState().ordinal()) setState(state); } public void setPayoutStateIfValidTransitionTo(PayoutState newPayoutState) { if (payoutState.isValidTransitionTo(newPayoutState)) { setPayoutState(newPayoutState); } else { log.warn("Payout state change is not getting applied because it would cause an invalid transition. " + "Trade payout state={}, intended payout state={}", payoutState, newPayoutState); } } public void setPayoutState(PayoutState payoutState) { // skip if no change if (payoutState.ordinal() == this.payoutState.ordinal()) return; // warn if reverting state if (payoutState.ordinal() < this.payoutState.ordinal()) { log.warn("Reverting payout state from {} to {} for trade {} {}. Possible reorg?", this.payoutState, payoutState, this.getClass().getSimpleName(), getShortId()); } // skip logging until initialized if (isInitialized) { log.info("Set new payout state for trade {} {}: {}", getShortId(), this.getClass().getSimpleName(), payoutState); } this.payoutState = payoutState; persistNow(null); UserThread.execute(() -> payoutStateProperty.set(payoutState)); } public void setDisputeState(DisputeState disputeState) { // skip if no change if (disputeState.ordinal() == this.disputeState.ordinal()) return; // skip logging until initialized if (isInitialized) { log.info("Set new dispute state for trade {} {}: {}", getShortId(), this.getClass().getSimpleName(), disputeState); } // warn if reverting state if (disputeState.ordinal() < this.disputeState.ordinal()) { String message = "We got a dispute state change to a previous state (id=" + getShortId() + ").\n" + "Old dispute state is: " + this.disputeState + ". New dispute state is: " + disputeState; log.warn(message); } this.disputeState = disputeState; persistNow(null); UserThread.execute(() -> { disputeStateProperty.set(disputeState); }); } public void advanceDisputeState(DisputeState disputeState) { if (disputeState.ordinal() > getDisputeState().ordinal()) setDisputeState(disputeState); } public List getDisputes() { return HavenoUtils.arbitrationManager.findDisputes(getId()); } public void setMediationResultState(MediationResultState mediationResultState) { this.mediationResultState = mediationResultState; mediationResultStateProperty.set(mediationResultState); } public void setRefundResultState(RefundResultState refundResultState) { this.refundResultState = refundResultState; refundResultStateProperty.set(refundResultState); } public void setPeriodState(TradePeriodState tradePeriodState) { this.periodState = tradePeriodState; tradePeriodStateProperty.set(tradePeriodState); } public void setAmount(BigInteger tradeAmount) { this.amount = tradeAmount.longValueExact(); getAmountProperty().set(getAmount()); getVolumeProperty().set(getVolume()); } public DisputeResult getDisputeResult() { if (getDisputes().isEmpty()) return null; return getDisputes().get(getDisputes().size() - 1).getDisputeResultProperty().get(); } @Nullable public MoneroTx getPayoutTx() { if (payoutTx == null && payoutTxId != null) { if (this instanceof ArbitratorTrade) { payoutTx = xmrConnectionService.getTxWithCache(payoutTxId); } else { payoutTx = xmrWalletService.getTx(payoutTxId); if (payoutTx == null) { log.warn("Main wallet is missing payout tx for {} {}, fetching from daemon", getClass().getSimpleName(), getShortId()); payoutTx = xmrConnectionService.getTxWithCache(payoutTxId); } } } return payoutTx; } public void setPayoutTxFee(BigInteger payoutTxFee) { this.payoutTxFee = payoutTxFee.longValueExact(); } public BigInteger getPayoutTxFee() { return BigInteger.valueOf(payoutTxFee); } public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; errorMessageProperty.set(errorMessage); } public void prependErrorMessage(String errorMessage) { StringBuilder sb = new StringBuilder(); sb.append(errorMessage); if (this.errorMessage != null && !this.errorMessage.isEmpty()) { sb.append("\n\n---- Previous Error -----\n\n"); sb.append(this.errorMessage); } String appendedErrorMessage = sb.toString(); this.errorMessage = appendedErrorMessage; errorMessageProperty.set(appendedErrorMessage); } /////////////////////////////////////////////////////////////////////////////////////////// // Getter /////////////////////////////////////////////////////////////////////////////////////////// public boolean isArbitrator() { return this instanceof ArbitratorTrade; } public boolean isBuyer() { return getBuyer() == getSelf(); } public boolean isSeller() { return getSeller() == getSelf(); } public boolean isMaker() { return this instanceof MakerTrade; } public boolean isTaker() { return this instanceof TakerTrade; } public TradePeer getSelf() { if (this instanceof MakerTrade) return processModel.getMaker(); if (this instanceof TakerTrade) return processModel.getTaker(); if (this instanceof ArbitratorTrade) return processModel.getArbitrator(); throw new RuntimeException("Trade is not maker, taker, or arbitrator"); } public List getOtherPeers() { List peers = getAllPeers(); if (!peers.remove(getSelf())) throw new IllegalStateException("Failed to remove self from list of peers"); return peers; } public List getAllPeers() { List peers = new ArrayList(); peers.add(getMaker()); peers.add(getTaker()); peers.add(getArbitrator()); return peers; } public TradePeer getArbitrator() { return processModel.getArbitrator(); } public TradePeer getMaker() { return processModel.getMaker(); } public TradePeer getTaker() { return processModel.getTaker(); } public TradePeer getBuyer() { return offer.getDirection() == OfferDirection.BUY ? processModel.getMaker() : processModel.getTaker(); } public TradePeer getSeller() { return offer.getDirection() == OfferDirection.BUY ? processModel.getTaker() : processModel.getMaker(); } public TradePeer getOtherPeer(TradePeer peer) { List peers = getAllPeers(); if (!peers.remove(peer)) throw new IllegalArgumentException("Peer is not maker, taker, or arbitrator"); if (!peers.remove(getSelf())) throw new IllegalStateException("Self is not maker, taker, or arbitrator"); if (peers.size() != 1) throw new IllegalStateException("There should be exactly one other peer"); return peers.get(0); } // get the taker if maker, maker if taker, null if arbitrator public TradePeer getTradePeer() { if (this instanceof MakerTrade) return processModel.getTaker(); else if (this instanceof TakerTrade) return processModel.getMaker(); else if (this instanceof ArbitratorTrade) return null; else throw new RuntimeException("Unknown trade type: " + getClass().getName()); } // TODO (woodser): this naming convention is confusing public TradePeer getTradePeer(NodeAddress address) { if (address.equals(getMaker().getNodeAddress())) return processModel.getMaker(); if (address.equals(getTaker().getNodeAddress())) return processModel.getTaker(); if (address.equals(getArbitrator().getNodeAddress())) return processModel.getArbitrator(); return null; } public TradePeer getTradePeer(PubKeyRing pubKeyRing) { if (getMaker() != null && getMaker().getPubKeyRing().equals(pubKeyRing)) return getMaker(); if (getTaker() != null && getTaker().getPubKeyRing().equals(pubKeyRing)) return getTaker(); if (getArbitrator() != null && getArbitrator().getPubKeyRing().equals(pubKeyRing)) return getArbitrator(); return null; } public String getRole() { if (isBuyer()) return "Buyer"; if (isSeller()) return "Seller"; if (isArbitrator()) return "Arbitrator"; throw new IllegalArgumentException("Trade is not buyer, seller, or arbitrator"); } private MessageState getPaymentSentMessageState() { if (isPaymentReceived()) return MessageState.ACKNOWLEDGED; if (getSeller().getPaymentSentMessageStateProperty().get() == MessageState.ACKNOWLEDGED) return MessageState.ACKNOWLEDGED; if (getSeller().getPaymentSentMessageStateProperty().get() == MessageState.NACKED) return MessageState.NACKED; switch (state) { case BUYER_SENT_PAYMENT_SENT_MSG: return MessageState.SENT; case BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG: return MessageState.ARRIVED; case BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG: return MessageState.STORED_IN_MAILBOX; case SELLER_RECEIVED_PAYMENT_SENT_MSG: return MessageState.ACKNOWLEDGED; case BUYER_SEND_FAILED_PAYMENT_SENT_MSG: return MessageState.FAILED; default: return null; } } public String getPeerRole(TradePeer peer) { if (peer == getBuyer()) return "Buyer"; if (peer == getSeller()) return "Seller"; if (peer == getArbitrator()) return "Arbitrator"; throw new IllegalArgumentException("Peer is not buyer, seller, or arbitrator"); } public Date getTakeOfferDate() { return new Date(takeOfferDate); } public Phase getPhase() { return state.getPhase(); } @Nullable public Volume getVolume() { try { if (getAmount() != null && getPrice() != null) { Volume volumeByAmount = getPrice().getVolumeByAmount(getAmount()); if (offer != null) volumeByAmount = VolumeUtil.getAdjustedVolume(volumeByAmount, offer.getPaymentMethod().getId()); return volumeByAmount; } else { return null; } } catch (Throwable ignore) { return null; } } public void maybeUpdateTradePeriod() { // skip if possible synchronized (startTimeLock) { if (startTime > 0) return; // already set if (getTakeOfferDate() == null) return; // trade not started yet if (!isDepositsFinalized()) return; // deposits not finalized yet } // get last finalized height of deposit txs (do not keep lock to prevent deadlock) Long finalizedHeight = null; synchronized (walletLock) { if (getWallet() == null) throw new RuntimeException("Cannot set start time for trade " + getId() + " because cannot get its wallet"); finalizedHeight = getDepositsFinalizedHeight(); if (finalizedHeight == null || finalizedHeight > xmrConnectionService.getTargetHeight() - 1) return; // TODO: isDepositsFinalized() can assume true, so skip if finalized height not reached } // set start time synchronized (startTimeLock) { if (startTime > 0) return; // already set // get finalized time from block timestamp long now = System.currentTimeMillis(); long tradeTime = getTakeOfferDate().getTime(); MoneroDaemon monerod = xmrWalletService.getMonerod(); if (monerod == null) throw new RuntimeException("Cannot set start time for trade " + getId() + " because it has no connection to monerod"); long finalizedTime = monerod.getBlockByHeight(finalizedHeight).getTimestamp() * 1000; // use current date if block timestamp is in future (date can be off by +/- 2 hours), otherwise use trade date startTime = finalizedTime > now ? now : Math.max(finalizedTime, tradeTime); log.debug("We set the start for the trade period to {}. Trade started at: {}. Block got mined at: {}", new Date(startTime), new Date(tradeTime), new Date(finalizedTime)); } } public long getMaxTradePeriod() { return getOffer().getPaymentMethod().getMaxTradePeriod(); } public Date getHalfTradePeriodDate() { return new Date(getEffectiveStartTime() + getMaxTradePeriod() / 2); } public Date getMaxTradePeriodDate() { return new Date(getEffectiveStartTime() + getMaxTradePeriod()); } public Date getStartDate() { return new Date(getEffectiveStartTime()); } /** * Returns the effective start time for the trade period. * Returns the current time until the deposits are finalized. */ private long getEffectiveStartTime() { synchronized (startTimeLock) { return startTime > 0 ? startTime : System.currentTimeMillis(); } } public boolean hasFailed() { return errorMessageProperty().get() != null; } public boolean isInPreparation() { return getState().getPhase().ordinal() == Phase.INIT.ordinal(); } public boolean isReservingMainWallet() { if (isArbitrator()) return false; return getState().ordinal() >= State.PREPARATION.ordinal() && getState().ordinal() < State.CONTRACT_SIGNATURE_REQUESTED.ordinal(); // reserve main wallet during initialization protocol until deposit tx created } public boolean isFundsLockedIn() { return isDepositsPublished() && !isPayoutPublished(); } public boolean isDepositRequested() { return getState().getPhase().ordinal() >= Phase.DEPOSIT_REQUESTED.ordinal(); } public boolean isDepositRequestFailed() { return getState() == Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED; } public boolean isDepositTxMissing() { if (!wasWalletPolledProperty.get()) throw new IllegalStateException("Cannot determine if deposit tx is missing because wallet has not been polled"); MoneroTxWallet makerDepositTx = getMakerDepositTx(); MoneroTxWallet takerDepositTx = getTakerDepositTx(); boolean hasUnlockedDepositTx = (makerDepositTx != null && Boolean.FALSE.equals(makerDepositTx.isLocked())) || (takerDepositTx != null && Boolean.FALSE.equals(takerDepositTx.isLocked())); if (!hasUnlockedDepositTx) return false; boolean hasMissingDepositTx = makerDepositTx == null || (!hasBuyerAsTakerWithoutDeposit() && takerDepositTx == null); return hasMissingDepositTx; } public boolean isDepositsPublished() { if (isDepositRequestFailed()) return false; return getState().getPhase().ordinal() >= Phase.DEPOSITS_PUBLISHED.ordinal() && getMaker().getDepositTxHash() != null && (getTaker().getDepositTxHash() != null || hasBuyerAsTakerWithoutDeposit()); } public boolean isDepositsSeen() { return isDepositsPublished() && getState().ordinal() >= State.DEPOSIT_TXS_SEEN_IN_NETWORK.ordinal(); } public boolean isDepositsConfirmed() { return isDepositsPublished() && getState().getPhase().ordinal() >= Phase.DEPOSITS_CONFIRMED.ordinal(); } // TODO: hacky way to check for deposits confirmed acks, redundant with getDepositsConfirmedTasks() public boolean isDepositsConfirmedAcked() { if (this instanceof BuyerTrade) { return getArbitrator().isDepositsConfirmedMessageAcked(); } else { for (TradePeer peer : getOtherPeers()) if (!peer.isDepositsConfirmedMessageAcked()) return false; return true; } } public boolean isDepositsUnlocked() { return isDepositsPublished() && getState().getPhase().ordinal() >= Phase.DEPOSITS_UNLOCKED.ordinal(); } public boolean isDepositsFinalized() { if (getState().getPhase().ordinal() < Phase.DEPOSITS_FINALIZED.ordinal()) return false; else if (getState().getPhase() == Phase.DEPOSITS_FINALIZED) return true; else if (isPayoutFinalized()) return true; else { // TODO: state can be past finalized (e.g. payment_sent) before the deposits are finalized, ideally use separate enum for deposits, or a single published state + num confirmations Long numDepositsConfirmations = getNumDepositConfirmations(); if (numDepositsConfirmations == null) { if (isBuyer()) { // log a warning for the buyer, since only they are at risk of reorg after payment sent log.warn("Assuming that deposit txs are finalized for trade {} {} because trade is in state {} but has unknown confirmations", getClass().getSimpleName(), getShortId(), getState()); Thread.dumpStack(); } return true; } else { return numDepositsConfirmations >= NUM_BLOCKS_DEPOSITS_FINALIZED; } } } public boolean hasPaymentSentMessage() { return (isBuyer() ? getSeller() : getBuyer()).getPaymentSentMessage() != null; // buyer stores message to seller and arbitrator, peers store message from buyer } public boolean hasPaymentReceivedMessage() { return (isSeller() ? getBuyer() : getSeller()).getPaymentReceivedMessage() != null; // seller stores message to buyer and arbitrator, peers store message from seller } public boolean hasDisputeClosedMessage() { // arbitrator stores message to buyer and seller, peers store message from arbitrator return isArbitrator() ? getBuyer().getDisputeClosedMessage() != null || getSeller().getDisputeClosedMessage() != null : getArbitrator().getDisputeClosedMessage() != null; } public boolean isDisputeClosed() { return getDisputeState().isClosed(); } public boolean isPaymentMarkedSent() { return getState().getPhase().ordinal() >= Phase.PAYMENT_SENT.ordinal(); } public boolean isPaymentSent() { return getState().getPhase().ordinal() >= Phase.PAYMENT_SENT.ordinal() && getState() != State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG; } public boolean isPaymentSentMessageProcessed() { if (isPaymentReceived()) return true; if (isBuyer()) return getState() == State.SELLER_RECEIVED_PAYMENT_SENT_MSG; return getState() == Trade.State.BUYER_SENT_PAYMENT_SENT_MSG; } public boolean isPaymentMarkedReceived() { return getState().getPhase().ordinal() >= Phase.PAYMENT_RECEIVED.ordinal(); } public boolean isPaymentReceived() { return getState().getPhase().ordinal() >= Phase.PAYMENT_RECEIVED.ordinal() && getState() != State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG; } public boolean isPaymentReceivedMessageProcessed() { if (isSeller()) return getState() == State.BUYER_RECEIVED_PAYMENT_RECEIVED_MSG; return getState() == Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG; } public boolean isPayoutPublished() { return getPayoutState().ordinal() >= PayoutState.PAYOUT_PUBLISHED.ordinal(); } public boolean isPayoutConfirmed() { return getPayoutState().ordinal() >= PayoutState.PAYOUT_CONFIRMED.ordinal(); } public boolean isPayoutUnlocked() { return getPayoutState().ordinal() >= PayoutState.PAYOUT_UNLOCKED.ordinal(); } public boolean isPayoutFinalized() { return getPayoutState().ordinal() >= PayoutState.PAYOUT_FINALIZED.ordinal(); } public ReadOnlyDoubleProperty initProgressProperty() { return initProgressProperty; } public ReadOnlyObjectProperty stateProperty() { return stateProperty; } public ReadOnlyObjectProperty statePhaseProperty() { return phaseProperty; } public ReadOnlyObjectProperty payoutStateProperty() { return payoutStateProperty; } public ReadOnlyObjectProperty disputeStateProperty() { return disputeStateProperty; } public ReadOnlyObjectProperty mediationResultStateProperty() { return mediationResultStateProperty; } public ReadOnlyObjectProperty refundResultStateProperty() { return refundResultStateProperty; } public ReadOnlyObjectProperty tradePeriodStateProperty() { return tradePeriodStateProperty; } public ReadOnlyObjectProperty tradeAmountProperty() { return tradeAmountProperty; } public ReadOnlyObjectProperty tradeVolumeProperty() { return tradeVolumeProperty; } public ReadOnlyStringProperty errorMessageProperty() { return errorMessageProperty; } @Override public Date getDate() { return getTakeOfferDate(); } @Override public String getId() { return offer.getId(); } private String getTradeEventThreadId() { return getId() + "_events"; } @Override public String getShortId() { return offer.getShortId(); } public String getShortUid() { return Utilities.getShortId(getUid()); } public BigInteger getFrozenAmount() { BigInteger sum = BigInteger.ZERO; if (getSelf().getReserveTxKeyImages() != null) { for (String keyImage : getSelf().getReserveTxKeyImages()) { List outputs = xmrWalletService.getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false).setKeyImage(new MoneroKeyImage(keyImage))); if (!outputs.isEmpty()) sum = sum.add(outputs.get(0).getAmount()); } } return sum; } public BigInteger getReservedAmount() { if (isArbitrator() || !isDepositsPublished() || isPayoutPublished()) return BigInteger.ZERO; return isBuyer() ? getBuyer().getSecurityDeposit() : getAmount().add(getSeller().getSecurityDeposit()); } /** * Returns the price as XMR/QUOTE. */ public Price getPrice() { boolean isInverted = getOffer().isInverted(); // return uninverted price return Price.valueOf(offer.getCounterCurrencyCode(), isInverted ? PriceUtil.invertLongPrice(price, offer.getCounterCurrencyCode()) : price); } public Price getRawPrice() { return Price.valueOf(offer.getCounterCurrencyCode(), price); } @Nullable public BigInteger getAmount() { return BigInteger.valueOf(amount); } public BigInteger getMakerFee() { return offer.getMakerFee(getAmount()); } public BigInteger getTakerFee() { return hasBuyerAsTakerWithoutDeposit() ? BigInteger.ZERO : offer.getTakerFee(getAmount()); } public BigInteger getSecurityDepositBeforeMiningFee() { return isBuyer() ? getBuyerSecurityDepositBeforeMiningFee() : getSellerSecurityDepositBeforeMiningFee(); } public BigInteger getBuyerSecurityDepositBeforeMiningFee() { return offer.getOfferPayload().getBuyerSecurityDepositForTradeAmount(getAmount()); } public BigInteger getSellerSecurityDepositBeforeMiningFee() { return offer.getOfferPayload().getSellerSecurityDepositForTradeAmount(getAmount()); } public boolean isBuyerAsTakerWithoutDeposit() { return isBuyer() && isTaker() && BigInteger.ZERO.equals(getBuyerSecurityDepositBeforeMiningFee()); } public boolean hasBuyerAsTakerWithoutDeposit() { return getOffer().getOfferPayload().isBuyerAsTakerWithoutDeposit(); } @Override public BigInteger getTotalTxFee() { return getSelf().getDepositTxFee().add(getSelf().getPayoutTxFee()); // sum my tx fees } public boolean hasErrorMessage() { return getErrorMessage() != null && !getErrorMessage().isEmpty(); } @Nullable public String getErrorMessage() { return errorMessageProperty.get(); } public boolean isTxChainInvalid() { return processModel.getMaker().getDepositTxHash() == null || (processModel.getTaker().getDepositTxHash() == null && !hasBuyerAsTakerWithoutDeposit()); } /** * Get the duration to delay reprocessing a message based on its reprocess count. * * @return the duration to delay in seconds */ public long getReprocessDelayInSeconds(int reprocessCount) { int retryCycles = 3; // reprocess on next refresh periods for first few attempts (app might auto switch to a good connection) if (reprocessCount < retryCycles) return xmrConnectionService.getRefreshPeriodMs() / 1000; long delay = 60; for (int i = retryCycles; i < reprocessCount; i++) delay *= 2; return Math.min(MAX_REPROCESS_DELAY_SECONDS, delay); } public void maybePublishTradeStatistics() { if (shouldPublishTradeStatistics()) { // publish after random delay within 24 hours UserThread.runAfterRandomDelay(() -> { if (!isShutDownStarted) doPublishTradeStatistics(); }, 0, TradeStatisticsManager.PUBLISH_STATS_RANDOM_DELAY_HOURS * 60 * 60 * 1000, TimeUnit.MILLISECONDS); } } public boolean shouldPublishTradeStatistics() { // do not publish if funds not transferred if (!tradeAmountTransferred()) return false; // only seller or arbitrator publish trade stats if (!isSeller() && !isArbitrator()) return false; // prior to v3 protocol, only seller publishes trade stats if (getOffer().getOfferPayload().getProtocolVersion() < 3 && !isSeller()) return false; return true; } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private boolean logWalletFunctionsAtInfoLevel() { return !isArbitrator() || !isIdling() || !isInitialized; } private boolean tradeAmountTransferred() { return isPayoutPublished() && (isPaymentReceived() || (getDisputeResult() != null && getDisputeResult().getWinner() == DisputeResult.Winner.SELLER)); } private void doPublishTradeStatistics() { String referralId = processModel.getReferralIdService().getOptionalReferralId().orElse(null); boolean isTorNetworkNode = getProcessModel().getP2PService().getNetworkNode() instanceof TorNetworkNode; HavenoUtils.tradeStatisticsManager.maybePublishTradeStatistics(this, referralId, isTorNetworkNode); } // lazy initialization private ObjectProperty getAmountProperty() { if (tradeAmountProperty == null) tradeAmountProperty = getAmount() != null ? new SimpleObjectProperty<>(getAmount()) : new SimpleObjectProperty<>(); return tradeAmountProperty; } // lazy initialization private ObjectProperty getVolumeProperty() { if (tradeVolumeProperty == null) tradeVolumeProperty = getVolume() != null ? new SimpleObjectProperty<>(getVolume()) : new SimpleObjectProperty<>(); return tradeVolumeProperty; } @Override protected void onConnectionChanged(MoneroRpcConnection connection) { synchronized (walletLock) { // configure wallet connection connection = new MoneroRpcConnection(xmrConnectionService.getConnection()); if (!xmrWalletService.isProxyApplied(wasWalletSynced)) connection.setProxyUri(null); // ignore if no change if (getWallet() == null || isShutDownStarted) return; if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) { updatePollPeriod(); return; } // set daemon connection (must restart monero-wallet-rpc if proxy uri changed) String oldProxyUri = wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri(); String newProxyUri = connection == null ? null : connection.getProxyUri(); log.info("Setting daemon connection for {} {}: uri={}, proxyUri={}", getClass().getSimpleName(), getId() , connection == null ? null : connection.getUri(), newProxyUri); if (wallet instanceof MoneroWalletRpc && !StringUtils.equals(oldProxyUri, newProxyUri)) { log.info("Restarting trade wallet {} because proxy URI has changed, old={}, new={}", getId(), oldProxyUri, newProxyUri); // TODO: remove this when wallet server is not started with proxy uri closeWallet(); wallet = getWallet(); } else { wallet.setDaemonConnection(connection); } // sync and reprocess messages on new thread if (isInitialized && connection != null && !Boolean.FALSE.equals(xmrConnectionService.isConnected())) { ThreadUtils.execute(() -> maybeInitSyncing(), getId()); } log.info("Done setting daemon connection for {} {}", getClass().getSimpleName(), getId()); } } private void maybeInitSyncing() { if (isShutDownStarted || !isDepositRequested()) return; // start polling if (isArbitrator() && isIdling()) { long syncDelayInMs = Math.max(1, ThreadLocalRandom.current().nextLong(0, getPollPeriodMs())); // random delay to start polling UserThread.runAfter(() -> { if (isShutDownStarted) return; ThreadUtils.execute(() -> { if (!isShutDownStarted) doTryInitSyncing(); }, getId()); }, syncDelayInMs, TimeUnit.MILLISECONDS); } else { // stagger wallet syncs on startup up to 30s if (!isArbitrator() && !xmrConnectionService.isConnectionLocalHost()) { synchronized (SYNC_DELAY_LOCK) { if (firstSyncDelay == null) firstSyncDelay = System.currentTimeMillis(); if (System.currentTimeMillis() - firstSyncDelay < MAX_SYNC_DELAY_MS) { HavenoUtils.waitFor(SYNC_DELAY_MS); } } } doTryInitSyncing(); } } private void doTryInitSyncing() { getWallet(); // ensure wallet is initialized updatePollPeriod(); startPolling(); } private void trySyncWallet(boolean pollWallet) { try { syncWallet(pollWallet); } catch (Exception e) { if (!isShutDownStarted && walletExists()) { log.warn("Error syncing trade wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); } } } private void syncWallet(boolean pollWallet) { MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection(); MoneroWallet sourceWallet = wallet; synchronized (walletLock) { try { if (getWallet() == null) throw new IllegalStateException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + " " + getId()); if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot sync trade wallet because it's not connected to a Monero daemon for " + getClass().getSimpleName() + " " + getId()); if (!isDepositRequested()) throw new IllegalStateException("Cannot sync trade wallet because deposit txs are not requested for " + getClass().getSimpleName() + " " + getId()); // sync wallet if behind boolean isFirstSync = !wasWalletSynced; if (isWalletBehind()) { String startSyncLogMsg = "Syncing wallet for " + getShortId() + " " + getClass().getSimpleName() + " from height " + walletHeight.get(); boolean logInfoLevel = logWalletFunctionsAtInfoLevel(); if (logInfoLevel) log.info(startSyncLogMsg); else log.debug(startSyncLogMsg); long startTime = System.currentTimeMillis(); syncWalletIfBehind(); String doneSyncLogMsg = "Done syncing wallet for " + getShortId() + " " + getClass().getSimpleName() + " in " + (System.currentTimeMillis() - startTime) + " ms"; if (logInfoLevel) log.info(doneSyncLogMsg); else log.debug(doneSyncLogMsg); } // reapply connection after wallet synced for config changes if (isFirstSync) { onConnectionChanged(xmrConnectionService.getConnection()); } if (pollWallet) doPollWallet(); } catch (Exception e) { if (wallet == null || wallet != sourceWallet) throw e; if (!(e instanceof IllegalStateException) && !isShutDownStarted) { ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId()); } if (HavenoUtils.isUnresponsive(e)) { // wallet can be stuck a while log.warn("Cannot sync wallet for {} {} because wallet is unresponsive: {}", getClass().getSimpleName(), getId(), e.getMessage()); if (isShutDownStarted) forceCloseWallet(); else forceRestartTradeWallet(); } throw e; } } } private void updatePollPeriod() { updatePollPeriod(false); } private void updatePollPeriod(boolean skipFirstPoll) { if (isShutDownStarted) return; setPollPeriodMs(getPollPeriodMs(), skipFirstPoll); } private void setPollPeriodMs(long pollPeriodMs, boolean skipFirstPoll) { synchronized (pollLock) { if (this.isShutDownStarted) return; if (this.pollPeriodMs != null && this.pollPeriodMs == pollPeriodMs) return; this.pollPeriodMs = pollPeriodMs; resetPolling(false); } } private long getPollPeriodMs() { if (isIdling()) return IDLE_SYNC_PERIOD_MS; return xmrConnectionService.getRefreshPeriodMs(); } private void startPolling() { startPolling(false); } private void startPolling(boolean skipFirstPoll) { synchronized (pollLock) { if (isShutDownStarted || isPolling()) return; updatePollPeriod(); AtomicReference skipNextPoll = new AtomicReference<>(skipFirstPoll); if (!skipFirstPoll) log.info("Starting to poll wallet for {} {}", getClass().getSimpleName(), getId()); // TODO: why only logging this if not skipping? pollLooper = new TaskLooper(() -> { if (skipNextPoll.get()) { skipNextPoll.set(false); return; } pollWallet(); }); pollLooper.start(pollPeriodMs); } } private void stopPolling() { synchronized (pollLock) { if (isPolling()) { pollLooper.stop(); pollLooper = null; } } } private void resetPolling(boolean skipFirstPoll) { synchronized (pollLock) { if (isShutDownStarted || !isPolling()) return; stopPolling(); startPolling(skipFirstPoll); } } private boolean isPolling() { synchronized (pollLock) { return pollLooper != null; } } private void pollWallet() { synchronized (pollLock) { if (pollInProgress) { maybeCloseIdlingWallet(); return; } } doPollWallet(); maybeCloseIdlingWallet(); } private void maybeCloseIdlingWallet() { if (isShutDownStarted) return; // close arbitrator trade wallet while idling if (isArbitrator()) { ThreadUtils.execute(() -> { if (isIdling() && !isPayoutFinalized()) { closeWallet(false); } }, getId()); } } private void doPollWallet() { doPollWallet(false); } private void doPollWallet(boolean offlinePoll) { MoneroWallet sourceWallet = wallet; // skip if shut down started or wallet is null if (isShutDownStarted || sourceWallet == null) return; // set poll in progress boolean pollInProgressSet = false; synchronized (pollLock) { if (!pollInProgress) pollInProgressSet = true; pollInProgress = true; } // poll wallet MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection(); try { // skip if shut down started if (isShutDownStarted) return; // skip if payout finalized if (isPayoutFinalized()) return; // skip if deposit txs unknown or not expected if (!isDepositRequested() || isDepositRequestFailed() || processModel.getMaker().getDepositTxHash() == null || (processModel.getTaker().getDepositTxHash() == null && !hasBuyerAsTakerWithoutDeposit())) return; // skip if daemon not synced if (!offlinePoll && (xmrConnectionService.getTargetHeight() == null || !xmrConnectionService.isSyncedWithinTolerance())) return; // sync if wallet too far behind daemon boolean longSync = false; if (!offlinePoll) { if (!wasWalletSynced || walletHeight.get() < xmrConnectionService.getTargetHeight() - SYNC_EVERY_NUM_BLOCKS) { longSync = true; syncWallet(false); } else { syncWalletIfBehind(); } } // update deposit txs boolean depositTxsUninitialized = isDepositRequested() && (getMaker().getDepositTx() == null || (getTaker().getDepositTx() == null && !hasBuyerAsTakerWithoutDeposit())); if (depositTxsUninitialized || !isDepositsFinalized()) { // set deposit txs from trade wallet List txs = getTxs(false); if (hasDepositTxs(txs)) { setDepositTxs(txs, false); } else if (!offlinePoll) { txs = getTxs(true); // txs may not be fetched if confirmed after last sync if (isDepositsSeen() && !hasDepositTxs(txs)) { log.warn("Deposits are missing for {} {} after being published, resyncing", getClass().getSimpleName(), getId()); HavenoUtils.waitFor(MISSING_TXS_DELAY_MS); sync(); txs = getTxs(true); } setDepositTxs(txs, true); } } // update payout tx boolean hasUnlockedDeposit = hasUnlockedTx(); if (isDepositsUnlocked() || hasUnlockedDeposit) { // arbitrator idles so these may not be the same // determine if payout tx expected boolean isPayoutExpected = isPaymentReceived() || hasPaymentReceivedMessage() || hasDisputeClosedMessage() || disputeState.ordinal() >= DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG.ordinal(); // rescan spent outputs to detect unconfirmed payout tx // TODO: can this be removed after https://github.com/monero-project/monero/pull/10255 in monero-project v0.18.4.5? if (getPayoutState() == PayoutState.PAYOUT_PUBLISHED || (isPayoutExpected && wallet != null && wallet.getBalance().compareTo(BigInteger.ZERO) > 0) || (isDepositsPublished() && longSync)) { try { rescanSpent(true); } catch (Exception e) { // TODO: rescan error is common, e.g. "no connection to daemon" //ThreadUtils.submitToPool(() -> requestSwitchToNextBestConnection(sourceConnection)); // do not block polling thread } } // get txs from trade wallet boolean checkPool = !offlinePoll && isPayoutExpected && !isPayoutConfirmed(); List txs = null; if (depositTxsUninitialized || wallet != null) { // get txs if deposits uninitialized or wallet is open txs = getTxs(checkPool); } // txs may not be fetched if confirmed after last sync if (!offlinePoll && isPayoutPublished() && getPayoutTxId() != null && !hasPayoutTx(txs)) { log.warn("Payout is missing for {} {} after being published, resyncing", getClass().getSimpleName(), getId()); HavenoUtils.waitFor(MISSING_TXS_DELAY_MS); sync(); txs = getTxs(true); checkPool = true; } // set deposit and payout txs if (txs != null) { setDepositTxs(txs, checkPool); setPayoutTx(txs, checkPool); } } // update trade period and poll properties if (!offlinePoll) maybeUpdateTradePeriod(); // no update possible if offline poll wasWalletPolledProperty.set(true); if (!offlinePoll) { wasWalletSyncedAndPolledProperty.set(true); resetPolling(true); // do not poll again until next period } } catch (Exception e) { if (wallet == null || wallet != sourceWallet || isShutDownStarted) return; // skip error handling if shut down or another thread force restarts while polling if (!(e instanceof IllegalStateException) && !offlinePoll && !wasWalletSyncedAndPolledProperty.get()) { // request connection switch on failure until synced and polled ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId()); } if (Boolean.TRUE.equals(xmrConnectionService.isConnected())) { if (isExpectedWalletError(e)) { log.warn("Error polling trade wallet for {} {}, errorMessage={}. Monerod={}", getClass().getSimpleName(), getShortId(), e.getMessage(), getXmrConnectionService().getConnection()); } else { log.warn("Error polling trade wallet for {} {}, errorMessage={}. Monerod={}", getClass().getSimpleName(), getShortId(), e.getMessage(), getXmrConnectionService().getConnection(), e); // include stack trace for unexpected errors } } if (HavenoUtils.isUnresponsive(e)) { // wallet can be stuck a while forceRestartTradeWallet(); } } finally { if (pollInProgressSet) { synchronized (pollLock) { pollInProgress = false; } } requestSaveWalletIfElapsedTime(); } } private boolean isWalletBehind() { return walletHeight.get() < xmrConnectionService.getTargetHeight(); } private boolean syncWalletIfBehind() { synchronized (walletLock) { if (!isDepositRequested()) throw new IllegalStateException("Cannot sync trade wallet because deposit txs are not requested for " + getClass().getSimpleName() + ", " + getId()); if (isWalletBehind()) { syncWithProgress(); return true; } else { return false; } } } public MoneroSyncResult sync() { synchronized (walletLock) { if (!isDepositRequested()) throw new IllegalStateException("Cannot sync trade wallet because deposit txs are not requested for " + getClass().getSimpleName() + ", " + getId()); log.info("Syncing wallet directly for {} {}", getClass().getSimpleName(), getShortId()); MoneroSyncResult result = super.sync(); log.info("Done syncing wallet directly for {} {}", getClass().getSimpleName(), getShortId()); return result; } } private List getTxs(boolean checkPool) { MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true); if (!checkPool) query.setInTxPool(false); synchronized (walletLock) { if (getWallet() == null) throw new IllegalStateException("Cannot get transactions from trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId()); if (checkPool) { synchronized (HavenoUtils.getDaemonLock()) { return wallet.getTxs(query); } } else { return wallet.getTxs(query); } } } private void setDepositTxs(List txs, boolean poolChecked) { // get deposit txs MoneroTxWallet makerDepositTx = getMakerDepositTx(txs); MoneroTxWallet takerDepositTx = getTakerDepositTx(txs); MoneroTxWallet buyerDepositTx = getBuyerDepositTx(txs); MoneroTxWallet sellerDepositTx = getSellerDepositTx(txs); // set txs if known if (makerDepositTx != null) getMaker().setDepositTx(makerDepositTx); if (takerDepositTx != null) getTaker().setDepositTx(takerDepositTx); // set actual buyer security deposit if (isSeen(buyerDepositTx)) { BigInteger buyerSecurityDeposit = buyerDepositTx.getIncomingAmount(); if (!getBuyer().getSecurityDeposit().equals(BigInteger.ZERO) && !buyerSecurityDeposit.equals(getBuyer().getSecurityDeposit())) { log.warn("Overwriting buyer security deposit for {} {}, old={}, new={}", getClass().getSimpleName(), getShortId(), getBuyer().getSecurityDeposit(), buyerSecurityDeposit); } getBuyer().setSecurityDeposit(buyerSecurityDeposit); } // set actual seller security deposit if (isSeen(sellerDepositTx)) { BigInteger sellerSecurityDeposit = sellerDepositTx.getIncomingAmount().subtract(getAmount()); if (!getSeller().getSecurityDeposit().equals(BigInteger.ZERO) && !sellerSecurityDeposit.equals(getSeller().getSecurityDeposit())) { log.warn("Overwriting seller security deposit for {} {}, old={}, new={}", getClass().getSimpleName(), getShortId(), getSeller().getSecurityDeposit(), sellerSecurityDeposit); } getSeller().setSecurityDeposit(sellerSecurityDeposit); } // advance deposit state if (isSeen(makerDepositTx) && (hasBuyerAsTakerWithoutDeposit() || isSeen(takerDepositTx))) { setStateDepositsSeen(); // check for deposit txs confirmed if (makerDepositTx.isConfirmed() && (hasBuyerAsTakerWithoutDeposit() || takerDepositTx.isConfirmed())) { setStateDepositsConfirmed(); } if (makerDepositTx.getNumConfirmations() != null) { // check for deposit txs unlocked if (makerDepositTx.getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && (hasBuyerAsTakerWithoutDeposit() || (takerDepositTx.getNumConfirmations() != null && takerDepositTx.getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK))) { setStateDepositsUnlocked(); } // check for deposit txs finalized if (makerDepositTx.getNumConfirmations() >= NUM_BLOCKS_DEPOSITS_FINALIZED && (hasBuyerAsTakerWithoutDeposit() || (takerDepositTx.getNumConfirmations() != null && takerDepositTx.getNumConfirmations() >= NUM_BLOCKS_DEPOSITS_FINALIZED))) { setStateDepositsFinalized(); } } } // revert deposit state if necessary State depositsState = getDepositsState(makerDepositTx, takerDepositTx); State minDepositsState = isPaymentSent() ? State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN : getState(); if (depositsState.ordinal() >= minDepositsState.ordinal()) { firstDepositTxMissingHeight = null; } else if (poolChecked) { // skip reverting state until next confirmation // TODO: sometimes txs are missing from the wallet and reappear without reorg if (firstDepositTxMissingHeight != null && walletHeight.get() > firstDepositTxMissingHeight + REVERT_AFTER_NUM_CONFIRMATIONS - 1) { log.warn("Reverting deposits state from {} to {} for {} {}. Possible reorg?", minDepositsState, depositsState, getClass().getSimpleName(), getShortId()); getMaker().setDepositTx(makerDepositTx); getTaker().setDepositTx(takerDepositTx); if (depositsState == State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS) setErrorMessage("Deposit transactions are missing for trade " + getShortId() + ". This can happen after a blockchain reorganization.\n\nIf the issue continues, you can contact support or mark the trade as failed."); if (!isPaymentSent()) setState(depositsState); // only revert state if payment not sent } else if (firstDepositTxMissingHeight == null) { log.warn("Missing deposit txs for {} {} at height {}. Waiting {} confirmation before reverting state", getClass().getSimpleName(), getShortId(), walletHeight.get(), REVERT_AFTER_NUM_CONFIRMATIONS); firstDepositTxMissingHeight = walletHeight.get(); } } // announce deposits update depositTxsUpdateCounter.set(depositTxsUpdateCounter.get() + 1); } private void setPayoutTx(List txs, boolean poolChecked) { // collect payout info boolean hasPayoutTx = false; MoneroTxWallet payoutTx = null; for (MoneroTxWallet tx : txs) { if (!Boolean.TRUE.equals(tx.isIncoming()) && !tx.isFailed()) { payoutTx = tx; hasPayoutTx = true; break; } else { for (MoneroOutputWallet output : tx.getOutputsWallet()) { if (Boolean.TRUE.equals(output.isSpent())) hasPayoutTx = true; // spent outputs observed on payout published (after rescanning) } } } // set payout state if (payoutTx != null) { firstPayoutTxMissingHeight = null; setPayoutTx(payoutTx); } else if (hasPayoutTx) { firstPayoutTxMissingHeight = null; setPayoutState(PayoutState.PAYOUT_PUBLISHED); } else if (poolChecked && isPayoutPublished()) { // payout tx seen then lost (e.g. reorg) // skip reverting state until confirmations if (firstPayoutTxMissingHeight != null && walletHeight.get() > firstPayoutTxMissingHeight + REVERT_AFTER_NUM_CONFIRMATIONS - 1) { // reset payment received and dispute closed messages for (TradePeer peer : getAllPeers()) { peer.setPaymentReceivedMessage(null); peer.setPaymentReceivedMessageState(MessageState.UNDEFINED); peer.setDisputeClosedMessage(null); } // revert state setPayoutState(PayoutState.PAYOUT_UNPUBLISHED); String errorMsg = "The payout transaction is not seen for trade " + getShortId() + ". This can happen after a blockchain reorganization..\n\nIf the payout does not confirm automatically, you can contact support or mark the trade as failed."; if (getState().ordinal() >= State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal()) { log.warn("Reverting state of {} {} from {} to {} because payout is unseen. Possible reorg?", getClass().getSimpleName(), getId(), getState(), Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT); setState(State.SELLER_CONFIRMED_PAYMENT_RECEIPT); if (isSeller()) onPayoutError(false, true, null); setErrorMessage(errorMsg); } // move trade back to pending if marked completed if (isCompleted()) processModel.getTradeManager().onMoveClosedTradeToPendingTrades(this); } else if (firstPayoutTxMissingHeight == null) { log.warn("Missing payout tx for {} {} at height {}. Waiting {} confirmations before reverting state", getClass().getSimpleName(), getShortId(), walletHeight.get(), REVERT_AFTER_NUM_CONFIRMATIONS); firstPayoutTxMissingHeight = walletHeight.get(); } } } public void setPayoutTx(MoneroTx payoutTx) { // set payout tx fields this.payoutTx = payoutTx; this.payoutTxId = payoutTx.getHash(); this.payoutTxFee = payoutTx.getFee() == null ? 0 : payoutTx.getFee().longValueExact(); this.payoutTxKey = payoutTx.getKey(); if ("".equals(payoutTxId)) this.payoutTxId = null; // tx id is empty until signed // set payout tx id in dispute(s) for (Dispute dispute : getDisputes()) dispute.setDisputePayoutTxId(payoutTxId); // set final payout amounts if (isPaymentReceived()) { BigInteger splitTxFee = payoutTx.getFee().divide(BigInteger.valueOf(2)); getBuyer().setPayoutTxFee(splitTxFee); getSeller().setPayoutTxFee(splitTxFee); getBuyer().setPayoutAmount(getBuyer().getSecurityDeposit().subtract(getBuyer().getPayoutTxFee()).add(getAmount())); getSeller().setPayoutAmount(getSeller().getSecurityDeposit().subtract(getSeller().getPayoutTxFee())); } else { DisputeResult disputeResult = getDisputeResult(); if (disputeResult != null) { BigInteger[] buyerSellerPayoutTxFees = getBuyerSellerPayoutTxCost(disputeResult, payoutTx.getFee()); getBuyer().setPayoutTxFee(buyerSellerPayoutTxFees[0]); getSeller().setPayoutTxFee(buyerSellerPayoutTxFees[1]); getBuyer().setPayoutAmount(disputeResult.getBuyerPayoutAmountBeforeCost().subtract(getBuyer().getPayoutTxFee())); getSeller().setPayoutAmount(disputeResult.getSellerPayoutAmountBeforeCost().subtract(getSeller().getPayoutTxFee())); } } // advance payout state if (Boolean.TRUE.equals(payoutTx.isRelayed()) || Boolean.TRUE.equals(payoutTx.inTxPool())) setPayoutStatePublished(); if (payoutTx.isConfirmed()) setPayoutStateConfirmed(); if (payoutTx.getNumConfirmations() != null) { if (payoutTx.getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) setPayoutStateUnlocked(); if (payoutTx.getNumConfirmations() >= NUM_BLOCKS_PAYOUT_FINALIZED) setPayoutStateFinalized(); } // revert payout state if necessary if (getPayoutState() != getPayoutState(payoutTx)) setPayoutState(getPayoutState(payoutTx)); } /** * Handle a payout error due to NACK or the transaction failing (e.g. due to reorg). * * @param syncAndPoll whether to sync and poll * @param resendPaymentReceivedMessages whether to resend payment received messages if previously confirmed * @param paymentReceivedNackSender the peer that sent the payment received NACK, or null if not applicable * @return true if the payment received were resent, false otherwise */ public boolean onPayoutError(boolean syncAndPoll, boolean resendPaymentReceivedMessages, TradePeer paymentReceivedNackSender) { log.warn("Handling payout error for {} {}", getClass().getSimpleName(), getId()); if (syncAndPoll) { try { syncAndPollWallet(); } catch (Exception e) { log.warn("Error syncing and polling wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); } } // reset trade state log.warn("Resetting trade state after payout error for {} {}, nackSender={}", getClass().getSimpleName(), getId(), paymentReceivedNackSender == null ? null : getPeerRole(paymentReceivedNackSender)); processModel.setPaymentSentPayoutTxStale(true); if (paymentReceivedNackSender != null) { paymentReceivedNackSender.setPaymentReceivedMessage(null); paymentReceivedNackSender.setPaymentReceivedMessageState(MessageState.NACKED); } if (!isPayoutFinalized()) { // payout tx may be stale and show as published after reorg, so clear local info for reprocessing until finalized log.warn("Resetting missing or failed payout tx after payout error for {} {}", getClass().getSimpleName(), getId()); getSelf().setUnsignedPayoutTxHex(null); setPayoutTxHex(null); setPayoutTxId(null); } persistNow(null); // send updated payment received message if applicable if (resendPaymentReceivedMessages && walletExists()) { if (!isSeller()) throw new IllegalArgumentException("Only the seller can resend PaymentReceivedMessages after a payout error for " + getClass().getSimpleName() + " " + getId()); if (!isPaymentReceived()) throw new IllegalStateException("Cannot resend PaymentReceivedMessages after a payout error for " + getClass().getSimpleName() + " " + getId() + " because payment not marked received"); log.warn("Sending updated PaymentReceivedMessages for {} {} after payout error", getClass().getSimpleName(), getId()); ((SellerProtocol) getProtocol()).onPaymentReceived(() -> { log.info("Done sending updated PaymentReceivedMessages on payout error for {} {}", getClass().getSimpleName(), getId()); }, (errorMessage) -> { log.warn("Error sending updated PaymentReceivedMessages on payout error for {} {}: {}", getClass().getSimpleName(), getId(), errorMessage); }); return true; } return false; } private boolean hasDepositTxs(List txs) { return getMakerDepositTx(txs) != null && (getTakerDepositTx(txs) != null || hasBuyerAsTakerWithoutDeposit()); } private boolean hasPayoutTx(List txs) { for (MoneroTxWallet tx : txs) { if (tx.getHash().equals(getPayoutTxId())) return true; } return false; } private static boolean isUnlocked(MoneroTx tx) { if (tx == null) return false; if (tx.getNumConfirmations() == null || tx.getNumConfirmations() < XmrWalletService.NUM_BLOCKS_UNLOCK) return false; return true; } private boolean hasUnlockedTx() { return isUnlocked(getMaker().getDepositTx()) || isUnlocked(getTaker().getDepositTx()); } private MoneroTxWallet getMakerDepositTx(List txs) { return getValidDepositTx(txs, getMaker()); } private MoneroTxWallet getTakerDepositTx(List txs) { return getValidDepositTx(txs, getTaker()); } private MoneroTxWallet getBuyerDepositTx(List txs) { return getValidDepositTx(txs, getBuyer()); } private MoneroTxWallet getSellerDepositTx(List txs) { return getValidDepositTx(txs, getSeller()); } private MoneroTxWallet getValidDepositTx(List txs, TradePeer peer) { for (MoneroTxWallet tx : txs) { if (tx.getHash().equals(peer.getDepositTxHash()) && !Boolean.TRUE.equals(tx.isFailed())) { return tx; } } return null; } private static boolean isSeen(MoneroTx tx) { if (tx == null) return false; if (Boolean.TRUE.equals(tx.isFailed())) return false; if (!Boolean.TRUE.equals(tx.inTxPool()) && !Boolean.TRUE.equals(tx.isConfirmed())) return false; return true; } private State getDepositsState(MoneroTxWallet makerDepositTx, MoneroTxWallet takerDepositTx) { if (makerDepositTx == null || (!hasBuyerAsTakerWithoutDeposit() && takerDepositTx == null)) return State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS; if (makerDepositTx.isFailed() || (!hasBuyerAsTakerWithoutDeposit() && takerDepositTx.isFailed())) return State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS; if (makerDepositTx.getNumConfirmations() == null || (!hasBuyerAsTakerWithoutDeposit() && takerDepositTx.getNumConfirmations() == null)) return State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS; if (makerDepositTx.getNumConfirmations() >= NUM_BLOCKS_DEPOSITS_FINALIZED && (hasBuyerAsTakerWithoutDeposit() || takerDepositTx.getNumConfirmations() >= NUM_BLOCKS_DEPOSITS_FINALIZED)) return State.DEPOSIT_TXS_FINALIZED_IN_BLOCKCHAIN; if (makerDepositTx.getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && (hasBuyerAsTakerWithoutDeposit() || takerDepositTx.getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK)) return State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN; if (makerDepositTx.isConfirmed() && (hasBuyerAsTakerWithoutDeposit() || takerDepositTx.isConfirmed())) return State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN; if (isSeen(makerDepositTx) && (hasBuyerAsTakerWithoutDeposit() || isSeen(takerDepositTx))) return State.DEPOSIT_TXS_SEEN_IN_NETWORK; return State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS; } private static PayoutState getPayoutState(MoneroTx payoutTx) { if (payoutTx.getHash() == null) return PayoutState.PAYOUT_UNPUBLISHED; if (Boolean.TRUE.equals(payoutTx.isFailed())) return PayoutState.PAYOUT_UNPUBLISHED; if (payoutTx.getNumConfirmations() != null) { if (payoutTx.getNumConfirmations() >= NUM_BLOCKS_PAYOUT_FINALIZED) return PayoutState.PAYOUT_FINALIZED; if (payoutTx.getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) return PayoutState.PAYOUT_UNLOCKED; } if (payoutTx.isConfirmed()) return PayoutState.PAYOUT_CONFIRMED; if (Boolean.TRUE.equals(payoutTx.isRelayed()) || Boolean.TRUE.equals(payoutTx.inTxPool())) return PayoutState.PAYOUT_PUBLISHED; return PayoutState.PAYOUT_UNPUBLISHED; } // TODO: wallet is sometimes missing balance or deposits, due to reorgs, specific daemon connections, not saving? public void recoverIfMissingWalletData() { synchronized (walletLock) { if (isWalletMissingData()) { log.warn("Wallet is missing data for {} {}, attempting to recover", getClass().getSimpleName(), getShortId()); // force restart wallet forceRestartTradeWallet(); // skip if payout published in the meantime if (isPayoutPublished()) return; // rescan blockchain rescanBlockchain(); // must import multisig hex after rescan log.warn("Importing multisig hex after rescanning blockchain for {} {}", getClass().getSimpleName(), getShortId()); importMultisigHex(); // poll wallet doPollWallet(); // check again if missing data if (isWalletMissingData()) throw new IllegalStateException("Wallet is still missing data after attempting recovery for " + getClass().getSimpleName() + " " + getShortId()); } } } public void rescanBlockchain() { synchronized (walletLock) { synchronized (HavenoUtils.getDaemonLock()) { if (getWallet() == null) throw new IllegalStateException("Cannot rescan blockchain because trade wallet doesn't exist for " + getClass().getSimpleName() + ", " + getId()); if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot rescan blockchain because trade wallet is not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId()); Long timeout = null; try { // extend rpc timeout for rescan if (wallet instanceof MoneroWalletRpc) { timeout = ((MoneroWalletRpc) wallet).getRpcConnection().getTimeout(); ((MoneroWalletRpc) wallet).getRpcConnection().setTimeout(EXTENDED_RPC_TIMEOUT); } // rescan blockchain log.warn("Rescanning blockchain for {} {}", getClass().getSimpleName(), getShortId()); wallet.rescanBlockchain(); } catch (Exception e) { log.warn("Error rescanning blockchain for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage()); if (HavenoUtils.isUnresponsive(e)) forceRestartTradeWallet(); // wallet can be stuck a while throw e; } finally { // restore rpc timeout if (wallet instanceof MoneroWalletRpc) { ((MoneroWalletRpc) wallet).getRpcConnection().setTimeout(timeout); } } } } } public void rescanSpent(boolean skipLog) { synchronized (walletLock) { if (getWallet() == null) throw new IllegalStateException("Cannot rescan spent outputs because trade wallet doesn't exist for " + getClass().getSimpleName() + ", " + getId()); if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot rescan spent outputs because trade wallet is not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId()); Long timeout = null; try { // extend rpc timeout for rescan if (wallet instanceof MoneroWalletRpc) { timeout = ((MoneroWalletRpc) wallet).getRpcConnection().getTimeout(); ((MoneroWalletRpc) wallet).getRpcConnection().setTimeout(EXTENDED_RPC_TIMEOUT); } // rescan spent outputs if (!skipLog) log.info("Rescanning spent outputs for {} {}", getClass().getSimpleName(), getShortId()); wallet.rescanSpent(); if (!skipLog) log.info("Done rescanning spent outputs for {} {}", getClass().getSimpleName(), getShortId()); saveWalletIfElapsedTime(); } catch (Exception e) { log.warn("Error rescanning spent outputs for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage()); if (HavenoUtils.isUnresponsive(e)) forceRestartTradeWallet(); // wallet can be stuck a while throw e; } finally { // restore rpc timeout if (wallet instanceof MoneroWalletRpc) { ((MoneroWalletRpc) wallet).getRpcConnection().setTimeout(timeout); } } } } private boolean isWalletMissingData() { synchronized (walletLock) { if (getWallet() == null) throw new IllegalStateException("Cannot check for missing wallet data for trade wallet because it doesn't exist for " + getClass().getSimpleName() + " " + getId()); if (!isDepositsUnlocked() || isPayoutPublished()) return false; if (getMakerDepositTx() == null) { log.warn("Missing maker deposit tx for {} {}", getClass().getSimpleName(), getId()); return true; } if (getTakerDepositTx() == null && !hasBuyerAsTakerWithoutDeposit()) { log.warn("Missing taker deposit tx for {} {}", getClass().getSimpleName(), getId()); return true; } if (wallet.getBalance().equals(BigInteger.ZERO)) { doPollWallet(); // poll once more to be sure if (isPayoutPublished()) return false; // payout can become published while checking balance log.warn("Wallet balance is zero for {} {}", getClass().getSimpleName(), getId()); return true; } return false; } } private void forceRestartTradeWallet() { if (isShutDownStarted || restartInProgress) return; log.warn("Force restarting trade wallet for {} {}", getClass().getSimpleName(), getId()); restartInProgress = true; stopPolling(); forceCloseWallet(); if (!isShutDownStarted) wallet = getWallet(); restartInProgress = false; ThreadUtils.execute(() -> maybeInitSyncing(), getId()); } private void setStateDepositsSeen() { if (getState().ordinal() < State.DEPOSIT_TXS_SEEN_IN_NETWORK.ordinal()) setState(State.DEPOSIT_TXS_SEEN_IN_NETWORK); } private void setStateDepositsConfirmed() { if (!isDepositsConfirmed()) setState(State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN); } private void setStateDepositsUnlocked() { if (!isDepositsUnlocked()) setState(State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN); } private void setStateDepositsFinalized() { if (!isPaymentSent() && !isDepositsFinalized()) setStateIfValidTransitionTo(State.DEPOSIT_TXS_FINALIZED_IN_BLOCKCHAIN); // do not revert state if payment already sent try { maybeUpdateTradePeriod(); } catch (Exception e) { log.warn("Error updating trade period after deposits finalized for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); } } private void setPayoutStatePublished() { if (!isPayoutPublished()) setPayoutState(PayoutState.PAYOUT_PUBLISHED); } private void setPayoutStateConfirmed() { if (!isPayoutConfirmed()) setPayoutState(PayoutState.PAYOUT_CONFIRMED); } private void setPayoutStateUnlocked() { if (!isPayoutUnlocked()) setPayoutState(PayoutState.PAYOUT_UNLOCKED); } private void setPayoutStateFinalized() { if (!isPayoutFinalized()) setPayoutState(PayoutState.PAYOUT_FINALIZED); } private Trade getTrade() { return this; } /** * Listen to block notifications from the main wallet in order to sync * idling trade wallets if necessary. */ private class IdleBlockPoller extends MoneroWalletListener { boolean processing = false; @Override public void onNewBlock(long height) { if (isShutDownStarted) return; ThreadUtils.submitToPool(() -> { // skip rapid succession blocks synchronized (this) { if (processing) return; processing = true; } try { // skip unless idling or waiting for finalization if (!isIdling() || isShutDownStarted || !isInitialized || !wasWalletPolledProperty.get() || (isDepositsFinalized() && (!isPayoutPublished() || isPayoutFinalized()))) { return; } // get payout height if unknown if (payoutHeight == null && getPayoutTxId() != null && isPayoutPublished()) { synchronized (IDLE_BLOCK_POLLER_LOCK) { MoneroTx tx = xmrConnectionService.getTx(getPayoutTxId()); if (tx == null) log.warn("Payout tx not found for {} {}, txId={}", getTrade().getClass().getSimpleName(), getId(), getPayoutTxId()); else if (tx.isConfirmed()) payoutHeight = tx.getHeight(); } } // sync if idling wallet is expected to change state long currentHeight = xmrConnectionService.getTargetHeight(); boolean depositsFinalizeExpected = !isDepositsFinalized() && (currentHeight - getDepositsConfirmedHeight() >= NUM_BLOCKS_DEPOSITS_FINALIZED); boolean payoutConfirmExpected = payoutHeight != null && !isPayoutConfirmed() && currentHeight >= payoutHeight; boolean payoutUnlockExpected = payoutHeight != null && !isPayoutUnlocked() && currentHeight >= payoutHeight + XmrWalletService.NUM_BLOCKS_UNLOCK; boolean payoutFinalizeExpected = payoutHeight != null && !isPayoutFinalized() && currentHeight >= payoutHeight + NUM_BLOCKS_PAYOUT_FINALIZED; if (depositsFinalizeExpected || payoutConfirmExpected || payoutUnlockExpected || payoutFinalizeExpected) { log.info("Syncing idle trade wallet for {} {}", getTrade().getClass().getSimpleName(), getId()); syncAndPollWallet(); } } catch (Exception e) { if (!isInitialized || isShutDownStarted) return; if (Boolean.TRUE.equals(xmrConnectionService.isConnected())) { log.warn("Error polling idle trade for {} {}: {}. Monerod={}\n", getClass().getSimpleName(), getId(), e.getMessage(), getXmrWalletService().getXmrConnectionService().getConnection()); }; } finally { processing = false; } }); } } private void onDepositRequested() { if (!isArbitrator()) startPolling(); // peers start polling after deposits requested } private void onDepositsPublished() { // backup trade wallet after creation maybeBackupWallet(); // arbitrator starts polling after deposits published if (isArbitrator()) { startPolling(); return; } // close open offer or reset address entries if (this instanceof MakerTrade) { processModel.getOpenOfferManager().closeSpentOffer(getOffer()); // TODO: use language translation // TODO: this will send notification if deposits are reverted to published HavenoUtils.notificationService.sendTradeNotification(this, Phase.DEPOSITS_PUBLISHED, "Offer Taken", "Your offer " + offer.getId() + " has been accepted"); } else { getXmrWalletService().resetAddressEntriesForOpenOffer(getId()); } // freeze outputs until spent if (!isArbitrator()) { ThreadUtils.submitToPool(() -> xmrWalletService.freezeOutputs(getSelf().getReserveTxKeyImages())); } } private void onDepositsConfirmed() { HavenoUtils.notificationService.sendTradeNotification(this, Phase.DEPOSITS_CONFIRMED, "Trade Deposits Confirmed", "The deposit transactions have confirmed"); } private void onDepositsUnlocked() { HavenoUtils.notificationService.sendTradeNotification(this, Phase.DEPOSITS_UNLOCKED, "Trade Deposits Unlocked", "The deposit transactions have unlocked"); } private void onDepositsFinalized() { HavenoUtils.notificationService.sendTradeNotification(this, Phase.DEPOSITS_FINALIZED, "Trade Deposits Finalized", "The deposit transactions have finalized"); } private void onPaymentSent() { HavenoUtils.notificationService.sendTradeNotification(this, Phase.PAYMENT_SENT, "Payment Sent", "The buyer has sent the payment"); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public Message toProtoMessage() { protobuf.Trade.Builder builder = protobuf.Trade.newBuilder() .setOffer(offer.toProtoMessage()) .setTakeOfferDate(takeOfferDate) .setProcessModel(processModel.toProtoMessage()) .setAmount(amount) .setPrice(price) .setState(Trade.State.toProtoMessage(state)) .setPayoutState(Trade.PayoutState.toProtoMessage(payoutState)) .setDisputeState(Trade.DisputeState.toProtoMessage(disputeState)) .setPeriodState(Trade.TradePeriodState.toProtoMessage(periodState)) .setLockTime(lockTime) .setStartTime(startTime) .setUid(uid) .setIsCompleted(isCompleted); synchronized (getChatMessages()) { builder.addAllChatMessage(getChatMessages().stream() .map(msg -> msg.toProtoNetworkEnvelope().getChatMessage()) .collect(Collectors.toList())); } Optional.ofNullable(payoutTxId).ifPresent(builder::setPayoutTxId); Optional.ofNullable(contract).ifPresent(e -> builder.setContract(contract.toProtoMessage())); Optional.ofNullable(contractAsJson).ifPresent(builder::setContractAsJson); Optional.ofNullable(contractHash).ifPresent(e -> builder.setContractHash(ByteString.copyFrom(contractHash))); Optional.ofNullable(errorMessage).ifPresent(builder::setErrorMessage); Optional.ofNullable(counterCurrencyTxId).ifPresent(e -> builder.setCounterCurrencyTxId(counterCurrencyTxId)); Optional.ofNullable(mediationResultState).ifPresent(e -> builder.setMediationResultState(MediationResultState.toProtoMessage(mediationResultState))); Optional.ofNullable(refundResultState).ifPresent(e -> builder.setRefundResultState(RefundResultState.toProtoMessage(refundResultState))); Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex)); Optional.ofNullable(payoutTxKey).ifPresent(e -> builder.setPayoutTxKey(payoutTxKey)); Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData)); Optional.ofNullable(challenge).ifPresent(e -> builder.setChallenge(challenge)); return builder.build(); } public static Trade fromProto(Trade trade, protobuf.Trade proto, CoreProtoResolver coreProtoResolver) { trade.setTakeOfferDate(proto.getTakeOfferDate()); trade.setState(State.fromProto(proto.getState())); trade.setPayoutState(PayoutState.fromProto(proto.getPayoutState())); trade.setDisputeState(DisputeState.fromProto(proto.getDisputeState())); trade.setPeriodState(TradePeriodState.fromProto(proto.getPeriodState())); trade.setPayoutTxId(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxId())); trade.setPayoutTxHex(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxHex())); trade.setPayoutTxKey(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxKey())); trade.setContract(proto.hasContract() ? Contract.fromProto(proto.getContract(), coreProtoResolver) : null); trade.setContractAsJson(ProtoUtil.stringOrNullFromProto(proto.getContractAsJson())); trade.setContractHash(ProtoUtil.byteArrayOrNullFromProto(proto.getContractHash())); trade.setErrorMessage(ProtoUtil.stringOrNullFromProto(proto.getErrorMessage())); trade.setCounterCurrencyTxId(proto.getCounterCurrencyTxId().isEmpty() ? null : proto.getCounterCurrencyTxId()); trade.setMediationResultState(MediationResultState.fromProto(proto.getMediationResultState())); trade.setRefundResultState(RefundResultState.fromProto(proto.getRefundResultState())); trade.setLockTime(proto.getLockTime()); trade.setStartTime(proto.getStartTime()); trade.setCounterCurrencyExtraData(ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData())); trade.setCompleted(proto.getIsCompleted()); trade.chatMessages.addAll(proto.getChatMessageList().stream() .map(ChatMessage::fromPayloadProto) .collect(Collectors.toList())); return trade; } @Override public String toString() { return "Trade{" + "\n offer=" + offer + ",\n totalTxFee=" + getTotalTxFee() + ",\n takeOfferDate=" + takeOfferDate + ",\n processModel=" + processModel + ",\n payoutTxId='" + payoutTxId + '\'' + ",\n amount=" + amount + ",\n tradePrice=" + price + ",\n state=" + state + ",\n payoutState=" + payoutState + ",\n disputeState=" + disputeState + ",\n tradePeriodState=" + periodState + ",\n contract=" + contract + ",\n contractAsJson='" + contractAsJson + '\'' + ",\n contractHash=" + Utilities.bytesAsHexString(contractHash) + ",\n errorMessage='" + errorMessage + '\'' + ",\n counterCurrencyTxId='" + counterCurrencyTxId + '\'' + ",\n counterCurrencyExtraData='" + counterCurrencyExtraData + '\'' + ",\n chatMessages=" + chatMessages + ",\n xmrWalletService=" + xmrWalletService + ",\n stateProperty=" + stateProperty + ",\n statePhaseProperty=" + phaseProperty + ",\n disputeStateProperty=" + disputeStateProperty + ",\n tradePeriodStateProperty=" + tradePeriodStateProperty + ",\n errorMessageProperty=" + errorMessageProperty + ",\n payoutTx=" + payoutTx + ",\n amount=" + amount + ",\n tradeAmountProperty=" + tradeAmountProperty + ",\n tradeVolumeProperty=" + tradeVolumeProperty + ",\n mediationResultState=" + mediationResultState + ",\n mediationResultStateProperty=" + mediationResultStateProperty + ",\n lockTime=" + lockTime + ",\n startTime=" + startTime + ",\n refundResultState=" + refundResultState + ",\n refundResultStateProperty=" + refundResultStateProperty + ",\n isCompleted=" + isCompleted + ",\n challenge='" + challenge + '\'' + "\n}"; } } ================================================ FILE: core/src/main/java/haveno/core/trade/TradeDataValidation.java ================================================ /* * 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 . */ package haveno.core.trade; import haveno.core.support.dispute.Dispute; import haveno.core.xmr.wallet.BtcWalletService; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.Address; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionInput; import org.bitcoinj.core.TransactionOutPoint; import org.bitcoinj.core.TransactionOutput; import javax.annotation.Nullable; import java.math.BigInteger; import java.util.function.Consumer; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; // TODO: remove for XMR? @Slf4j public class TradeDataValidation { public static void validateDelayedPayoutTx(Trade trade, Transaction delayedPayoutTx, BtcWalletService btcWalletService) throws AddressException, MissingTxException, InvalidTxException, InvalidLockTimeException, InvalidAmountException { validateDelayedPayoutTx(trade, delayedPayoutTx, null, btcWalletService, null); } public static void validateDelayedPayoutTx(Trade trade, Transaction delayedPayoutTx, @Nullable Dispute dispute, BtcWalletService btcWalletService) throws AddressException, MissingTxException, InvalidTxException, InvalidLockTimeException, InvalidAmountException { validateDelayedPayoutTx(trade, delayedPayoutTx, dispute, btcWalletService, null); } public static void validateDelayedPayoutTx(Trade trade, Transaction delayedPayoutTx, BtcWalletService btcWalletService, @Nullable Consumer addressConsumer) throws AddressException, MissingTxException, InvalidTxException, InvalidLockTimeException, InvalidAmountException { validateDelayedPayoutTx(trade, delayedPayoutTx, null, btcWalletService, addressConsumer); } public static void validateDelayedPayoutTx(Trade trade, Transaction delayedPayoutTx, @Nullable Dispute dispute, BtcWalletService btcWalletService, @Nullable Consumer addressConsumer) throws AddressException, MissingTxException, InvalidTxException, InvalidLockTimeException, InvalidAmountException { String errorMsg; if (delayedPayoutTx == null) { errorMsg = "DelayedPayoutTx must not be null"; log.error(errorMsg); throw new MissingTxException("DelayedPayoutTx must not be null"); } // Validate tx structure if (delayedPayoutTx.getInputs().size() != 1) { errorMsg = "Number of delayedPayoutTx inputs must be 1"; log.error(errorMsg); log.error(delayedPayoutTx.toString()); throw new InvalidTxException(errorMsg); } if (delayedPayoutTx.getOutputs().size() != 1) { errorMsg = "Number of delayedPayoutTx outputs must be 1"; log.error(errorMsg); log.error(delayedPayoutTx.toString()); throw new InvalidTxException(errorMsg); } // connectedOutput is null and input.getValue() is null at that point as the tx is not committed to the wallet // yet. So we cannot check that the input matches but we did the amount check earlier in the trade protocol. // Validate lock time if (delayedPayoutTx.getLockTime() != trade.getLockTime()) { errorMsg = "delayedPayoutTx.getLockTime() must match trade.getLockTime()"; log.error(errorMsg); log.error(delayedPayoutTx.toString()); throw new InvalidLockTimeException(errorMsg); } // Validate seq num if (delayedPayoutTx.getInput(0).getSequenceNumber() != TransactionInput.NO_SEQUENCE - 1) { errorMsg = "Sequence number must be 0xFFFFFFFE"; log.error(errorMsg); log.error(delayedPayoutTx.toString()); throw new InvalidLockTimeException(errorMsg); } // Check amount TransactionOutput output = delayedPayoutTx.getOutput(0); BigInteger msOutputAmount = trade.getBuyerSecurityDepositBeforeMiningFee() .add(trade.getSellerSecurityDepositBeforeMiningFee()) .add(checkNotNull(trade.getAmount())); if (!output.getValue().equals(msOutputAmount)) { errorMsg = "Output value of deposit tx and delayed payout tx is not matching. Output: " + output + " / msOutputAmount: " + msOutputAmount; log.error(errorMsg); log.error(delayedPayoutTx.toString()); throw new InvalidAmountException(errorMsg); } NetworkParameters params = btcWalletService.getParams(); Address address = output.getScriptPubKey().getToAddress(params); if (address == null) { errorMsg = "Donation address cannot be resolved (not of type P2PK nor P2SH nor P2WH). Output: " + output; log.error(errorMsg); log.error(delayedPayoutTx.toString()); throw new AddressException(dispute, errorMsg); } String addressAsString = address.toString(); if (addressConsumer != null) { addressConsumer.accept(addressAsString); } if (dispute != null) { // Verify that address in the dispute matches the one in the trade. String donationAddressOfDelayedPayoutTx = dispute.getDonationAddressOfDelayedPayoutTx(); // Old clients don't have it set yet. Can be removed after a forced update if (donationAddressOfDelayedPayoutTx != null) { checkArgument(addressAsString.equals(donationAddressOfDelayedPayoutTx), "donationAddressOfDelayedPayoutTx from dispute does not match address from delayed payout tx"); } } } public static void validatePayoutTxInput(Transaction depositTx, Transaction delayedPayoutTx) throws InvalidInputException { TransactionInput input = delayedPayoutTx.getInput(0); checkNotNull(input, "delayedPayoutTx.getInput(0) must not be null"); // input.getConnectedOutput() is null as the tx is not committed at that point TransactionOutPoint outpoint = input.getOutpoint(); if (!outpoint.getHash().toString().equals(depositTx.getTxId().toString()) || outpoint.getIndex() != 0) { throw new InvalidInputException("Input of delayed payout transaction does not point to output of deposit tx.\n" + "Delayed payout tx=" + delayedPayoutTx + "\n" + "Deposit tx=" + depositTx); } } /////////////////////////////////////////////////////////////////////////////////////////// // Exceptions /////////////////////////////////////////////////////////////////////////////////////////// public static class ValidationException extends Exception { @Nullable @Getter private final Dispute dispute; ValidationException(String msg) { this(null, msg); } ValidationException(@Nullable Dispute dispute, String msg) { super(msg); this.dispute = dispute; } } public static class InvalidPaymentAccountPayloadException extends ValidationException { InvalidPaymentAccountPayloadException(@Nullable Dispute dispute, String msg) { super(dispute, msg); } } public static class AddressException extends ValidationException { AddressException(@Nullable Dispute dispute, String msg) { super(dispute, msg); } } public static class MissingTxException extends ValidationException { MissingTxException(String msg) { super(msg); } } public static class InvalidTxException extends ValidationException { InvalidTxException(String msg) { super(msg); } } public static class InvalidAmountException extends ValidationException { InvalidAmountException(String msg) { super(msg); } } public static class InvalidLockTimeException extends ValidationException { InvalidLockTimeException(String msg) { super(msg); } } public static class InvalidInputException extends ValidationException { InvalidInputException(String msg) { super(msg); } } public static class DisputeReplayException extends ValidationException { DisputeReplayException(Dispute dispute, String msg) { super(dispute, msg); } } public static class NodeAddressException extends ValidationException { NodeAddressException(Dispute dispute, String msg) { super(dispute, msg); } } } ================================================ FILE: core/src/main/java/haveno/core/trade/TradeManager.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.collect.ImmutableList; import com.google.inject.Inject; import haveno.common.ClockWatcher; import haveno.common.ThreadUtils; import haveno.common.UserThread; import haveno.common.crypto.KeyRing; import haveno.common.crypto.PubKeyRing; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.FaultHandler; import haveno.common.handlers.ResultHandler; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.proto.persistable.PersistedDataHost; import haveno.core.api.AccountServiceListener; import haveno.core.api.CoreAccountService; import haveno.core.api.CoreNotificationService; import haveno.core.locale.Res; import haveno.core.offer.Offer; import haveno.core.offer.OfferBookService; import haveno.core.offer.OfferDirection; import haveno.core.offer.OfferUtil; import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; import haveno.core.offer.SignedOffer; import haveno.core.offer.availability.OfferAvailabilityModel; import haveno.core.provider.price.PriceFeedService; import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.support.dispute.mediation.mediator.MediatorManager; import haveno.core.support.dispute.messages.DisputeClosedMessage; import haveno.core.support.dispute.messages.DisputeOpenedMessage; import haveno.core.support.messages.ChatMessage; import haveno.core.trade.Trade.DisputeState; import haveno.core.trade.failed.FailedTradesManager; import haveno.core.trade.handlers.TradeResultHandler; import haveno.core.trade.messages.DepositRequest; import haveno.core.trade.messages.DepositResponse; import haveno.core.trade.messages.DepositsConfirmedMessage; import haveno.core.trade.messages.InitMultisigRequest; import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.messages.PaymentReceivedMessage; import haveno.core.trade.messages.PaymentSentMessage; import haveno.core.trade.messages.SignContractRequest; import haveno.core.trade.messages.SignContractResponse; import haveno.core.trade.messages.TradeMessage; import haveno.core.trade.protocol.ArbitratorProtocol; import haveno.core.trade.protocol.MakerProtocol; import haveno.core.trade.protocol.ProcessModel; import haveno.core.trade.protocol.ProcessModelServiceProvider; import haveno.core.trade.protocol.TakerProtocol; import haveno.core.trade.protocol.TradeProtocol; import haveno.core.trade.protocol.TradeProtocolFactory; import haveno.core.trade.protocol.TraderProtocol; import haveno.core.trade.statistics.ReferralIdService; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.User; import haveno.core.util.Validator; import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.AckMessage; import haveno.network.p2p.AckMessageSourceType; import haveno.network.p2p.BootstrapListener; import haveno.network.p2p.DecryptedDirectMessageListener; import haveno.network.p2p.DecryptedMessageWithPubKey; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import haveno.network.p2p.SendMailboxMessageListener; import haveno.network.p2p.mailbox.MailboxMessage; import haveno.network.p2p.mailbox.MailboxMessageService; import haveno.network.p2p.network.TorNetworkNode; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; import javafx.beans.property.BooleanProperty; import javafx.beans.property.LongProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleLongProperty; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javax.annotation.Nullable; import lombok.Getter; import lombok.Setter; import monero.daemon.model.MoneroTx; import org.bitcoinj.core.Coin; import org.bouncycastle.crypto.params.KeyParameter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class TradeManager implements PersistedDataHost, DecryptedDirectMessageListener { private static final Logger log = LoggerFactory.getLogger(TradeManager.class); private static final int INIT_TRADE_RANDOM_DELAY_MS = 10000; // random delay to initialize trades private boolean isShutDownStarted; private boolean isShutDown; private final User user; @Getter private final KeyRing keyRing; private final CoreAccountService accountService; private final XmrWalletService xmrWalletService; @Getter private final CoreNotificationService notificationService; private final OfferBookService offerBookService; @Getter private final OpenOfferManager openOfferManager; private final ClosedTradableManager closedTradableManager; private final FailedTradesManager failedTradesManager; private final P2PService p2PService; private final PriceFeedService priceFeedService; private final TradeStatisticsManager tradeStatisticsManager; private final OfferUtil offerUtil; private final TradeUtil tradeUtil; @Getter private final ArbitratorManager arbitratorManager; private final MediatorManager mediatorManager; private final ProcessModelServiceProvider processModelServiceProvider; private final ClockWatcher clockWatcher; private final Map tradeProtocolByTradeId = new HashMap<>(); private final PersistenceManager> persistenceManager; private final TradableList tradableList = new TradableList<>(); @Getter private final BooleanProperty tradesInitialized = new SimpleBooleanProperty(); @Getter private final LongProperty numPendingTrades = new SimpleLongProperty(); private final ReferralIdService referralIdService; @Setter @Nullable private Consumer lockedUpFundsHandler; // TODO: this is unused // set comparator for processing mailbox messages static { MailboxMessageService.setMailboxMessageComparator(new MailboxMessageComparator()); } /** * Sort mailbox messages for processing. */ public static class MailboxMessageComparator implements Comparator { private static List> messageOrder = Arrays.asList( AckMessage.class, DepositsConfirmedMessage.class, PaymentSentMessage.class, PaymentReceivedMessage.class, DisputeOpenedMessage.class, ChatMessage.class, DisputeClosedMessage.class); @Override public int compare(MailboxMessage m1, MailboxMessage m2) { int idx1 = messageOrder.indexOf(m1.getClass()); int idx2 = messageOrder.indexOf(m2.getClass()); return idx1 - idx2; } } /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public TradeManager(User user, KeyRing keyRing, CoreAccountService accountService, XmrWalletService xmrWalletService, CoreNotificationService notificationService, OfferBookService offerBookService, OpenOfferManager openOfferManager, ClosedTradableManager closedTradableManager, FailedTradesManager failedTradesManager, P2PService p2PService, PriceFeedService priceFeedService, TradeStatisticsManager tradeStatisticsManager, OfferUtil offerUtil, TradeUtil tradeUtil, ArbitratorManager arbitratorManager, MediatorManager mediatorManager, ProcessModelServiceProvider processModelServiceProvider, ClockWatcher clockWatcher, PersistenceManager> persistenceManager, ReferralIdService referralIdService) { this.user = user; this.keyRing = keyRing; this.accountService = accountService; this.xmrWalletService = xmrWalletService; this.notificationService = notificationService; this.offerBookService = offerBookService; this.openOfferManager = openOfferManager; this.closedTradableManager = closedTradableManager; this.failedTradesManager = failedTradesManager; this.p2PService = p2PService; this.priceFeedService = priceFeedService; this.tradeStatisticsManager = tradeStatisticsManager; this.offerUtil = offerUtil; this.tradeUtil = tradeUtil; this.arbitratorManager = arbitratorManager; this.mediatorManager = mediatorManager; this.processModelServiceProvider = processModelServiceProvider; this.clockWatcher = clockWatcher; this.referralIdService = referralIdService; this.persistenceManager = persistenceManager; this.persistenceManager.initialize(tradableList, "PendingTrades", PersistenceManager.Source.PRIVATE); p2PService.addDecryptedDirectMessageListener(this); failedTradesManager.setUnFailTradeCallback(this::unFailTrade); // TODO: better way to set references xmrWalletService.setTradeManager(this); // TODO: set reference in HavenoUtils for consistency HavenoUtils.notificationService = notificationService; } /////////////////////////////////////////////////////////////////////////////////////////// // PersistedDataHost /////////////////////////////////////////////////////////////////////////////////////////// @Override public void readPersisted(Runnable completeHandler) { persistenceManager.readPersisted(persisted -> { synchronized (persisted.getList()) { tradableList.setAll(persisted.getList()); tradableList.stream() .filter(trade -> trade.getOffer() != null) .forEach(trade -> trade.getOffer().setPriceFeedService(priceFeedService)); } completeHandler.run(); }, completeHandler); } /////////////////////////////////////////////////////////////////////////////////////////// // DecryptedDirectMessageListener /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onDirectMessage(DecryptedMessageWithPubKey message, NodeAddress sender) { NetworkEnvelope networkEnvelope = message.getNetworkEnvelope(); if (!(networkEnvelope instanceof TradeMessage)) return; TradeMessage tradeMessage = (TradeMessage) networkEnvelope; String tradeId = tradeMessage.getOfferId(); log.info("TradeManager received {} for tradeId={}, sender={}, uid={}", networkEnvelope.getClass().getSimpleName(), tradeId, sender, tradeMessage.getUid()); ThreadUtils.execute(() -> { if (networkEnvelope instanceof InitTradeRequest) { handleInitTradeRequest((InitTradeRequest) networkEnvelope, sender); } else if (networkEnvelope instanceof InitMultisigRequest) { handleInitMultisigRequest((InitMultisigRequest) networkEnvelope, sender); } else if (networkEnvelope instanceof SignContractRequest) { handleSignContractRequest((SignContractRequest) networkEnvelope, sender); } else if (networkEnvelope instanceof SignContractResponse) { handleSignContractResponse((SignContractResponse) networkEnvelope, sender); } else if (networkEnvelope instanceof DepositRequest) { handleDepositRequest((DepositRequest) networkEnvelope, sender); } else if (networkEnvelope instanceof DepositResponse) { handleDepositResponse((DepositResponse) networkEnvelope, sender); } }, tradeId); } /////////////////////////////////////////////////////////////////////////////////////////// // Lifecycle /////////////////////////////////////////////////////////////////////////////////////////// public void onAllServicesInitialized() { if (p2PService.isBootstrapped()) { initTrades(); } else { p2PService.addP2PServiceListener(new BootstrapListener() { @Override public void onDataReceived() { initTrades(); } }); } // listen for account updates accountService.addListener(new AccountServiceListener() { @Override public void onAccountCreated() { log.info(TradeManager.class + ".accountService.onAccountCreated()"); initTrades(); } @Override public void onAccountOpened() { log.info(TradeManager.class + ".accountService.onAccountOpened()"); initTrades(); } @Override public void onAccountClosed() { log.info(TradeManager.class + ".accountService.onAccountClosed()"); closeAllTrades(); } @Override public void onPasswordChanged(String oldPassword, String newPassword) { // handled in XmrWalletService } }); } public void onShutDownStarted() { log.info("{}.onShutDownStarted()", getClass().getSimpleName()); isShutDownStarted = true; // skip trade shut down if not initialized if (!HavenoUtils.isSeedNode() && !tradesInitialized.get()) { log.warn("Skipping trade shut down because trades were not initialized"); return; } // collect trades to prepare for shut down List trades = getAllTrades(); // prepare to shut down trades in parallel Set tasks = new HashSet(); for (Trade trade : trades) tasks.add(() -> { try { trade.onShutDownStarted(); } catch (Exception e) { if (e.getMessage() != null && e.getMessage().contains("Connection reset")) return; // expected if shut down with ctrl+c log.warn("Error notifying {} {} that shut down started: {}\n", trade.getClass().getSimpleName(), trade.getId(), e.getMessage(), e); } }); try { ThreadUtils.awaitTasks(tasks); } catch (Exception e) { log.warn("Error notifying trades that shut down started: {}", e.getMessage()); throw e; } } public void shutDown() { log.info("Shutting down {}", getClass().getSimpleName()); isShutDown = true; closeAllTrades(); } private void closeAllTrades() { // collect trades to shutdown List trades = getAllTrades(); // shut down trades in parallel Set tasks = new HashSet(); for (Trade trade : trades) tasks.add(() -> { try { trade.shutDown(); } catch (Exception e) { if (e.getMessage() != null && (e.getMessage().contains("Connection reset") || e.getMessage().contains("Connection refused"))) return; // expected if shut down with ctrl+c log.warn("Error closing {} {}: {}", trade.getClass().getSimpleName(), trade.getId(), e.getMessage(), e); } }); try { ThreadUtils.awaitTasks(tasks); } catch (Exception e) { log.warn("Error shutting down trades: {}\n", e.getMessage(), e); } } public TradeProtocol getTradeProtocol(Trade trade) { synchronized (tradeProtocolByTradeId) { return tradeProtocolByTradeId.get(trade.getUid()); } } private void unregisterTradeProtocol(Trade trade) { synchronized (tradeProtocolByTradeId) { tradeProtocolByTradeId.remove(trade.getUid()); } } public TradeProtocol createTradeProtocol(Trade trade) { synchronized (tradeProtocolByTradeId) { TradeProtocol tradeProtocol = TradeProtocolFactory.getNewTradeProtocol(trade); TradeProtocol prev = tradeProtocolByTradeId.put(trade.getUid(), tradeProtocol); if (prev != null) log.error("We had already an entry with uid {}", trade.getUid()); return tradeProtocol; } } /////////////////////////////////////////////////////////////////////////////////////////// // Init trades /////////////////////////////////////////////////////////////////////////////////////////// private void initTrades() { try { log.info("Initializing trades"); // get all trades List trades = getAllTrades(); // initialize trades in parallel int threadPoolSize = 10; Set initTradeTasks = new HashSet(); Set uids = new HashSet(); Set tradesToSkip = new HashSet(); Set uninitializedTrades = new HashSet(); for (Trade trade : trades) { initTradeTasks.add(getInitTradeTask(trade, trades, tradesToSkip, uninitializedTrades, uids)); }; ThreadUtils.awaitTasks(initTradeTasks, threadPoolSize); log.info("Done initializing trades"); if (isShutDownStarted) return; // remove skipped trades trades.removeAll(tradesToSkip); // process after all trades initialized if (!HavenoUtils.isSeedNode()) { // handle uninitialized trades for (Trade trade : uninitializedTrades) { trade.onProtocolInitializationError(); } // freeze or thaw outputs if (isShutDownStarted) return; xmrWalletService.fixReservedOutputs(); // TODO: this can cause application to hang on startup // reset any available funded address entries if (isShutDownStarted) return; xmrWalletService.getAddressEntriesForAvailableBalanceStream() .filter(addressEntry -> addressEntry.getOfferId() != null) .forEach(addressEntry -> { log.warn("Swapping pending {} entries at startup. offerId={}", addressEntry.getContext(), addressEntry.getOfferId()); xmrWalletService.swapAddressEntryToAvailable(addressEntry.getOfferId(), addressEntry.getContext()); }); checkForLockedUpFunds(); } // notify that persisted trades initialized log.info("Done postprocessing after initializing trades"); if (isShutDownStarted) return; tradesInitialized.set(true); getObservableList().addListener((ListChangeListener) change -> onTradesChanged()); onTradesChanged(); // We do not include failed trades as they should not be counted anyway in the trade statistics // TODO: remove stats? Set nonFailedTrades = new HashSet<>(closedTradableManager.getClosedTrades()); nonFailedTrades.addAll(tradableList.getList()); String referralId = referralIdService.getOptionalReferralId().orElse(null); boolean isTorNetworkNode = p2PService.getNetworkNode() instanceof TorNetworkNode; tradeStatisticsManager.maybePublishTradeStatistics(nonFailedTrades, referralId, isTorNetworkNode); } catch (Exception e) { log.warn("Error initializing trades: {}\n", e.getMessage(), e); } } private Runnable getInitTradeTask(Trade trade, Collection trades, Set tradesToSkip, Set uninitializedTrades, Set uids) { return () -> { try { // check for duplicate uid synchronized (uids) { if (!uids.add(trade.getUid())) { log.warn("Found trade with duplicate uid, skipping. That should never happen. {} {}, uid={}", trade.getClass().getSimpleName(), trade.getId(), trade.getUid()); tradesToSkip.add(trade); return; } } // skip if failed and error handling not scheduled if (failedTradesManager.getObservableList().contains(trade) && !trade.isProtocolErrorHandlingScheduled()) { log.warn("Skipping initialization of failed trade {} {}", trade.getClass().getSimpleName(), trade.getId()); synchronized (tradesToSkip) { tradesToSkip.add(trade); return; } } // initialize trade initTrade(trade); // record if protocol didn't initialize if (!trade.isDepositsPublished()) { synchronized (uninitializedTrades) { uninitializedTrades.add(trade); } } } catch (Exception e) { if (!isShutDownStarted) { log.warn("Error initializing {} {}: {}\n", trade.getClass().getSimpleName(), trade.getId(), e.getMessage(), e); trade.setInitError(e); trade.prependErrorMessage(e.getMessage()); } } }; } private void initTrade(Trade trade) { if (isShutDown) return; if (getTradeProtocol(trade) != null) return; initTradeAndProtocol(trade, createTradeProtocol(trade)); requestPersistence(); } private void initTradeAndProtocol(Trade trade, TradeProtocol tradeProtocol) { tradeProtocol.initialize(processModelServiceProvider, this); requestPersistence(); // TODO requesting persistence twice with initTrade() } public void requestPersistence() { persistenceManager.requestPersistence(); } public void persistNow(@Nullable Runnable completeHandler) { persistenceManager.persistNow(completeHandler); } private void handleInitTradeRequest(InitTradeRequest request, NodeAddress sender) { log.info("TradeManager handling InitTradeRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); try { Validator.nonEmptyStringOf(request.getOfferId()); } catch (Throwable t) { log.warn("Invalid InitTradeRequest message " + request.toString()); return; } // handle request as maker if (request.getMakerNodeAddress().equals(p2PService.getNetworkNode().getNodeAddress())) { // get open offer Optional openOfferOptional = openOfferManager.getOpenOffer(request.getOfferId()); if (!openOfferOptional.isPresent()) return; OpenOffer openOffer = openOfferOptional.get(); Offer offer = openOffer.getOffer(); // check availability if (openOffer.getState() != OpenOffer.State.AVAILABLE) { log.warn("Ignoring InitTradeRequest to maker because offer is not available, offerId={}, sender={}", request.getOfferId(), sender); return; } // validate challenge if (openOffer.getChallenge() != null && !HavenoUtils.getChallengeHash(openOffer.getChallenge()).equals(HavenoUtils.getChallengeHash(request.getChallenge()))) { log.warn("Ignoring InitTradeRequest to maker because challenge is incorrect, tradeId={}, sender={}", request.getOfferId(), sender); return; } // ensure trade does not already exist Optional tradeOptional = getOpenTrade(request.getOfferId()); if (tradeOptional.isPresent()) { log.warn("Ignoring InitTradeRequest to maker because trade already exists with id " + request.getOfferId() + ". This should never happen."); return; } // reserve open offer openOfferManager.reserveOpenOffer(openOffer); // initialize trade Trade trade; if (offer.isBuyOffer()) trade = new BuyerAsMakerTrade(offer, BigInteger.valueOf(request.getTradeAmount()), offer.getOfferPayload().getPrice(), xmrWalletService, getNewProcessModel(offer), UUID.randomUUID().toString(), request.getMakerNodeAddress(), request.getTakerNodeAddress(), request.getArbitratorNodeAddress(), openOffer.getChallenge()); else trade = new SellerAsMakerTrade(offer, BigInteger.valueOf(request.getTradeAmount()), offer.getOfferPayload().getPrice(), xmrWalletService, getNewProcessModel(offer), UUID.randomUUID().toString(), request.getMakerNodeAddress(), request.getTakerNodeAddress(), request.getArbitratorNodeAddress(), openOffer.getChallenge()); trade.getMaker().setPaymentAccountId(trade.getOffer().getOfferPayload().getMakerPaymentAccountId()); trade.getTaker().setPaymentAccountId(request.getTakerPaymentAccountId()); trade.getMaker().setPubKeyRing(trade.getOffer().getPubKeyRing()); trade.getTaker().setPubKeyRing(request.getTakerPubKeyRing()); trade.getSelf().setPaymentAccountId(offer.getOfferPayload().getMakerPaymentAccountId()); trade.getSelf().setReserveTxHash(openOffer.getReserveTxHash()); // TODO (woodser): initialize in initTradeAndProtocol? trade.getSelf().setReserveTxHex(openOffer.getReserveTxHex()); trade.getSelf().setReserveTxKey(openOffer.getReserveTxKey()); trade.getSelf().setReserveTxKeyImages(offer.getOfferPayload().getReserveTxKeyImages()); initTradeAndProtocol(trade, createTradeProtocol(trade)); addTrade(trade); // process with protocol ((MakerProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> { log.warn("Maker error during trade initialization: " + errorMessage); trade.onProtocolInitializationError(); }); } // handle request as arbitrator else if (request.getArbitratorNodeAddress().equals(p2PService.getNetworkNode().getNodeAddress())) { // verify this node is registered arbitrator Arbitrator thisArbitrator = user.getRegisteredArbitrator(); NodeAddress thisAddress = p2PService.getNetworkNode().getNodeAddress(); if (thisArbitrator == null || !thisArbitrator.getNodeAddress().equals(thisAddress)) { log.warn("Ignoring InitTradeRequest because we are not an arbitrator, tradeId={}, sender={}", request.getOfferId(), sender); return; } // get offer associated with trade Offer offer = null; for (Offer anOffer : offerBookService.getOffers()) { if (anOffer.getId().equals(request.getOfferId())) { offer = anOffer; } } if (offer == null) { log.warn("Ignoring InitTradeRequest to arbitrator because offer is not on the books, tradeId={}, sender={}", request.getOfferId(), sender); return; } // verify arbitrator is payload signer unless they are offline // TODO (woodser): handle if payload signer differs from current arbitrator (verify signer is offline) // verify maker is offer owner // TODO (woodser): maker address might change if they disconnect and reconnect, should allow maker address to differ if pubKeyRing is same? if (!offer.getOwnerNodeAddress().equals(request.getMakerNodeAddress())) { log.warn("Ignoring InitTradeRequest to arbitrator because maker is not offer owner, tradeId={}, sender={}", request.getOfferId(), sender); return; } // validate challenge hash if (offer.getChallengeHash() != null && !offer.getChallengeHash().equals(HavenoUtils.getChallengeHash(request.getChallenge()))) { log.warn("Ignoring InitTradeRequest to arbitrator because challenge hash is incorrect, tradeId={}, sender={}", request.getOfferId(), sender); return; } // handle trade Trade trade; Optional tradeOptional = getOpenTrade(offer.getId()); if (tradeOptional.isPresent()) { trade = tradeOptional.get(); // verify request is from taker if (!sender.equals(request.getTakerNodeAddress())) { if (sender.equals(request.getMakerNodeAddress())) { log.warn("Received InitTradeRequest from maker to arbitrator for trade that is already initializing, tradeId={}, sender={}", request.getOfferId(), sender); sendAckMessage(sender, trade.getMaker().getPubKeyRing(), request, false, "Trade is already initializing for " + getClass().getSimpleName() + " " + trade.getId(), null); } else { log.warn("Ignoring InitTradeRequest from non-taker, tradeId={}, sender={}", request.getOfferId(), sender); } return; } } else { // verify request is from maker if (!sender.equals(request.getMakerNodeAddress())) { log.warn("Ignoring InitTradeRequest to arbitrator because request must be from maker when trade is not initialized, tradeId={}, sender={}", request.getOfferId(), sender); return; } // create arbitrator trade trade = new ArbitratorTrade(offer, BigInteger.valueOf(request.getTradeAmount()), offer.getOfferPayload().getPrice(), xmrWalletService, getNewProcessModel(offer), UUID.randomUUID().toString(), request.getMakerNodeAddress(), request.getTakerNodeAddress(), request.getArbitratorNodeAddress(), request.getChallenge()); // set reserve tx hash if available Optional signedOfferOptional = openOfferManager.getSignedOfferById(request.getOfferId()); if (signedOfferOptional.isPresent()) { SignedOffer signedOffer = signedOfferOptional.get(); trade.getMaker().setReserveTxHash(signedOffer.getReserveTxHash()); } // initialize trade protocol initTradeAndProtocol(trade, createTradeProtocol(trade)); addTrade(trade); } // process with protocol ((ArbitratorProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> { log.warn("Arbitrator error during trade initialization for trade {}: {}", trade.getId(), errorMessage); trade.onProtocolInitializationError(); }); requestPersistence(); } // handle request as taker else if (request.getTakerNodeAddress().equals(p2PService.getNetworkNode().getNodeAddress())) { // verify request is from arbitrator Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(sender); if (arbitrator == null) { log.warn("Ignoring InitTradeRequest to taker because request is not from accepted arbitrator, tradeId={}, sender={}", request.getOfferId(), sender); return; } // get trade Optional tradeOptional = getOpenTrade(request.getOfferId()); if (!tradeOptional.isPresent()) { log.warn("Ignoring InitTradeRequest to taker because trade is not initialized, tradeId={}, sender={}", request.getOfferId(), sender); return; } Trade trade = tradeOptional.get(); // process with protocol ((TakerProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender); } // invalid sender else { log.warn("Ignoring InitTradeRequest because sender is not maker, arbitrator, or taker, tradeId={}, sender={}", request.getOfferId(), sender); return; } } private void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { log.info("TradeManager handling InitMultisigRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); try { Validator.nonEmptyStringOf(request.getOfferId()); } catch (Throwable t) { log.warn("Invalid InitMultisigRequest " + request.toString()); return; } Optional tradeOptional = getOpenTrade(request.getOfferId()); if (!tradeOptional.isPresent()) { log.warn("No trade with id " + request.getOfferId() + " at node " + P2PService.getMyNodeAddress()); return; } Trade trade = tradeOptional.get(); getTradeProtocol(trade).handleInitMultisigRequest(request, sender); } private void handleSignContractRequest(SignContractRequest request, NodeAddress sender) { log.info("TradeManager handling SignContractRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); try { Validator.nonEmptyStringOf(request.getOfferId()); } catch (Throwable t) { log.warn("Invalid SignContractRequest message " + request.toString()); return; } Optional tradeOptional = getOpenTrade(request.getOfferId()); if (!tradeOptional.isPresent()) { log.warn("No trade with id " + request.getOfferId()); return; } Trade trade = tradeOptional.get(); getTradeProtocol(trade).handleSignContractRequest(request, sender); } private void handleSignContractResponse(SignContractResponse request, NodeAddress sender) { log.info("TradeManager handling SignContractResponse for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); try { Validator.nonEmptyStringOf(request.getOfferId()); } catch (Throwable t) { log.warn("Invalid SignContractResponse message " + request.toString()); return; } Optional tradeOptional = getOpenTrade(request.getOfferId()); if (!tradeOptional.isPresent()) { log.warn("No trade with id " + request.getOfferId()); return; } Trade trade = tradeOptional.get(); ((TraderProtocol) getTradeProtocol(trade)).handleSignContractResponse(request, sender); } private void handleDepositRequest(DepositRequest request, NodeAddress sender) { log.info("TradeManager handling DepositRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); try { Validator.nonEmptyStringOf(request.getOfferId()); } catch (Throwable t) { log.warn("Invalid DepositRequest message " + request.toString()); return; } Optional tradeOptional = getOpenTrade(request.getOfferId()); if (!tradeOptional.isPresent()) { log.warn("No trade with id " + request.getOfferId()); return; } Trade trade = tradeOptional.get(); ((ArbitratorProtocol) getTradeProtocol(trade)).handleDepositRequest(request, sender); } private void handleDepositResponse(DepositResponse response, NodeAddress sender) { log.info("TradeManager handling DepositResponse for tradeId={}, sender={}, uid={}", response.getOfferId(), sender, response.getUid()); try { Validator.nonEmptyStringOf(response.getOfferId()); } catch (Throwable t) { log.warn("Invalid DepositResponse message " + response.toString()); return; } Optional tradeOptional = getOpenTrade(response.getOfferId()); if (!tradeOptional.isPresent()) { tradeOptional = getFailedTrade(response.getOfferId()); if (!tradeOptional.isPresent()) { log.warn("No trade with id " + response.getOfferId()); return; } } Trade trade = tradeOptional.get(); ((TraderProtocol) getTradeProtocol(trade)).handleDepositResponse(response, sender); } /////////////////////////////////////////////////////////////////////////////////////////// // Take offer /////////////////////////////////////////////////////////////////////////////////////////// public void checkOfferAvailability(Offer offer, boolean isTakerApiUser, String paymentAccountId, BigInteger tradeAmount, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { offer.checkOfferAvailability(getOfferAvailabilityModel(offer, isTakerApiUser, paymentAccountId, tradeAmount), resultHandler, errorMessageHandler); } // First we check if offer is still available then we create the trade with the protocol public void onTakeOffer(BigInteger tradeAmount, BigInteger fundsNeededForTrade, Offer offer, String paymentAccountId, boolean useSavingsWallet, boolean isTakerApiUser, TradeResultHandler tradeResultHandler, ErrorMessageHandler errorMessageHandler) { // check offer availability and create trade if available checkOfferAvailability(offer, isTakerApiUser, paymentAccountId, tradeAmount, () -> { if (offer.getState() == Offer.State.AVAILABLE) { if (ThreadUtils.isShutDown(offer.getId())) ThreadUtils.reset(offer.getId()); ThreadUtils.execute(() -> { try { // check that offer is not already used in a trade checkArgument(!wasOfferAlreadyUsedInTrade(offer.getId())); // check that trade is not already open Optional tradeOptional = getOpenTrade(offer.getId()); if (tradeOptional.isPresent()) throw new RuntimeException("Cannot create trade protocol because trade with ID " + offer.getId() + " is already open"); // check that failed trade is not processing tradeOptional = getFailedTrade(offer.getId()); if (tradeOptional.isPresent() && tradeOptional.get().walletExists()) throw new RuntimeException("Cannot create trade protocol because trade with ID " + offer.getId() + " has failed but is not processed"); // create trade Trade trade; if (offer.isBuyOffer()) { trade = new SellerAsTakerTrade(offer, tradeAmount, offer.getPrice().getValue(), xmrWalletService, getNewProcessModel(offer), UUID.randomUUID().toString(), offer.getMakerNodeAddress(), P2PService.getMyNodeAddress(), null, offer.getChallenge()); } else { trade = new BuyerAsTakerTrade(offer, tradeAmount, offer.getPrice().getValue(), xmrWalletService, getNewProcessModel(offer), UUID.randomUUID().toString(), offer.getMakerNodeAddress(), P2PService.getMyNodeAddress(), null, offer.getChallenge()); } trade.getProcessModel().setUseSavingsWallet(useSavingsWallet); trade.getProcessModel().setFundsNeededForTrade(fundsNeededForTrade.longValueExact()); trade.getMaker().setPaymentAccountId(offer.getOfferPayload().getMakerPaymentAccountId()); trade.getMaker().setPubKeyRing(offer.getPubKeyRing()); trade.getSelf().setPubKeyRing(keyRing.getPubKeyRing()); trade.getSelf().setPaymentAccountId(paymentAccountId); trade.getSelf().setPaymentMethodId(user.getPaymentAccount(paymentAccountId).getPaymentAccountPayload().getPaymentMethodId()); // initialize trade protocol TradeProtocol tradeProtocol = createTradeProtocol(trade); addTrade(trade); initTradeAndProtocol(trade, tradeProtocol); trade.addInitProgressStep(); // process with protocol ((TakerProtocol) tradeProtocol).onTakeOffer(result -> { tradeResultHandler.handleResult(trade); requestPersistence(); }, errorMessage -> { log.warn("Taker error during trade initialization: " + errorMessage); trade.onProtocolInitializationError(); xmrWalletService.resetAddressEntriesForOpenOffer(trade.getId()); // TODO: move this into protocol error handling errorMessageHandler.handleErrorMessage(errorMessage); }); requestPersistence(); } catch (Throwable t) { log.warn("Error taking offer: " + t.getMessage(), t); errorMessageHandler.handleErrorMessage(t.getMessage()); } }, offer.getId()); } else { String errorMessage = "Cannot take offer " + offer.getId() + " because it's not available, state=" + offer.getState(); log.warn(errorMessage); errorMessageHandler.handleErrorMessage(errorMessage); } }, errorMessageHandler); } private ProcessModel getNewProcessModel(Offer offer) { return new ProcessModel(checkNotNull(offer).getId(), processModelServiceProvider.getUser().getAccountId(), processModelServiceProvider.getKeyRing().getPubKeyRing()); } private OfferAvailabilityModel getOfferAvailabilityModel(Offer offer, boolean isTakerApiUser, String paymentAccountId, BigInteger tradeAmount) { return new OfferAvailabilityModel( offer, keyRing.getPubKeyRing(), xmrWalletService, p2PService, user, mediatorManager, tradeStatisticsManager, isTakerApiUser, paymentAccountId, tradeAmount, offerUtil); } /////////////////////////////////////////////////////////////////////////////////////////// // Complete trade /////////////////////////////////////////////////////////////////////////////////////////// // TODO (woodser): remove this function public void onWithdrawRequest(String toAddress, Coin amount, Coin fee, KeyParameter aesKey, Trade trade, @Nullable String memo, ResultHandler resultHandler, FaultHandler faultHandler) { throw new RuntimeException("Withdraw trade funds after payout to Haveno wallet not supported"); } // If trade was completed (closed without fault but might be closed by a dispute) we move it to the closed trades public void onTradeCompleted(Trade trade) { if (trade.isCompleted()) throw new RuntimeException("Trade " + trade.getId() + " was already completed"); closedTradableManager.add(trade); trade.setCompleted(true); removeTrade(trade); xmrWalletService.swapPayoutAddressEntryToAvailable(trade.getId()); // TODO The address entry should have been removed already. Check and if its the case remove that. requestPersistence(); } public void unregisterTrade(Trade trade) { log.warn("Unregistering {} {}", trade.getClass().getSimpleName(), trade.getId()); removeTrade(trade); removeFailedTrade(trade); if (!trade.isMaker()) xmrWalletService.swapPayoutAddressEntryToAvailable(trade.getId()); // TODO The address entry should have been removed already. Check and if its the case remove that. requestPersistence(); } public void removeTrade(Trade trade) { log.info("TradeManager.removeTrade() " + trade.getId()); // remove trade synchronized (tradableList.getList()) { if (!tradableList.remove(trade)) return; } requestPersistence(); } /////////////////////////////////////////////////////////////////////////////////////////// // Dispute /////////////////////////////////////////////////////////////////////////////////////////// public void closeDisputedTrade(String tradeId, DisputeState disputeState) { Optional tradeOptional = getOpenTrade(tradeId); if (tradeOptional.isPresent()) { Trade trade = tradeOptional.get(); trade.setDisputeState(disputeState); xmrWalletService.swapPayoutAddressEntryToAvailable(trade.getId()); requestPersistence(); } } /////////////////////////////////////////////////////////////////////////////////////////// // Trade period state /////////////////////////////////////////////////////////////////////////////////////////// public void applyTradePeriodState() { updateTradePeriodState(); clockWatcher.addListener(new ClockWatcher.Listener() { @Override public void onSecondTick() { } @Override public void onMinuteTick() { ThreadUtils.submitToPool(() -> updateTradePeriodState()); // update trade period off main thread } }); } // TODO: could use monerod.getBlocksByHeight() to more efficiently update trade period state private void updateTradePeriodState() { if (isShutDownStarted) return; for (Trade trade : getOpenTrades()) { if (!trade.isInitialized() || trade.isPayoutPublished()) continue; try { trade.maybeUpdateTradePeriod(); Date maxTradePeriodDate = trade.getMaxTradePeriodDate(); Date halfTradePeriodDate = trade.getHalfTradePeriodDate(); if (maxTradePeriodDate != null && halfTradePeriodDate != null) { Date now = new Date(); if (now.after(maxTradePeriodDate)) { trade.setPeriodState(Trade.TradePeriodState.TRADE_PERIOD_OVER); requestPersistence(); } else if (now.after(halfTradePeriodDate)) { trade.setPeriodState(Trade.TradePeriodState.SECOND_HALF); requestPersistence(); } } } catch (Exception e) { log.warn("Error updating trade period state for {} {}: {}", trade.getClass().getSimpleName(), trade.getShortId(), e.getMessage(), e); continue; } } } /////////////////////////////////////////////////////////////////////////////////////////// // Failed trade handling /////////////////////////////////////////////////////////////////////////////////////////// // If trade is in already in critical state (if taker role: taker fee; both roles: after deposit published) // we move the trade to FailedTradesManager public void onMoveInvalidTradeToFailedTrades(Trade trade) { log.warn("Moving {} {} to failed trades", trade.getClass().getSimpleName(), trade.getShortId()); if (trade.isInitialized()) { ThreadUtils.submitToPool(() -> { try { trade.shutDown(); unregisterTradeProtocol(trade); } catch (Exception e) { log.warn("Error shutting down {} {} on move to failed trades", trade.getClass().getSimpleName(), trade.getShortId(), e); } }); } failedTradesManager.add(trade); removeTrade(trade); xmrWalletService.fixReservedOutputs(); } public void onMoveFailedTradeToPendingTrades(Trade trade) { log.warn("Moving {} {} from failed trades to pending trades", trade.getClass().getSimpleName(), trade.getShortId()); addTradeToPendingTrades(trade); failedTradesManager.removeTrade(trade); xmrWalletService.fixReservedOutputs(); } public void onMoveClosedTradeToPendingTrades(Trade trade) { log.warn("Moving {} {} from closed trades to pending trades", trade.getClass().getSimpleName(), trade.getShortId()); trade.setCompleted(false); addTradeToPendingTrades(trade); closedTradableManager.removeTrade(trade); } private void removeFailedTrade(Trade trade) { failedTradesManager.removeTrade(trade); } private void addTradeToPendingTrades(Trade trade) { if (!trade.isInitialized()) { ThreadUtils.submitToPool(() -> { try { initTrade(trade); } catch (Exception e) { log.warn("Error initializing {} {} on move to pending trades", trade.getClass().getSimpleName(), trade.getShortId(), e); } }); } addTrade(trade); } public Stream getTradesStreamWithFundsLockedIn() { synchronized (tradableList.getList()) { return getObservableList().stream().filter(Trade::isFundsLockedIn); } } private void checkForLockedUpFunds() { try { getSetOfFailedOrClosedTradeIdsFromLockedInFunds(); } catch (Exception e) { throw new IllegalStateException(e); } } public Set getSetOfFailedOrClosedTradeIdsFromLockedInFunds() throws TradeTxException { AtomicReference tradeTxException = new AtomicReference<>(); synchronized (tradableList.getList()) { Set tradesIdSet = getTradesStreamWithFundsLockedIn() .filter(Trade::hasFailed) .map(Trade::getId) .collect(Collectors.toSet()); tradesIdSet.addAll(failedTradesManager.getTradesStreamWithFundsLockedIn() .filter(trade -> trade.getMakerDepositTx() != null || trade.getTakerDepositTx() != null) .map(trade -> { log.warn("We found a failed trade with locked up funds. " + "That should never happen. trade ID=" + trade.getId()); return trade.getId(); }) .collect(Collectors.toSet())); tradesIdSet.addAll(closedTradableManager.getTradesStreamWithFundsLockedIn() .map(trade -> { MoneroTx makerDepositTx = trade.getMakerDepositTx(); if (makerDepositTx != null) { if (!makerDepositTx.isConfirmed()) { tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithUnconfirmedDepositTx", trade.getShortId()))); // TODO (woodser): rename to closedTradeWithLockedDepositTx } else { log.warn("We found a closed trade with locked up funds. " + "That should never happen. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState()); } } else { log.warn("Closed trade with locked up funds missing maker deposit tx. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState()); tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId()))); } MoneroTx takerDepositTx = trade.getTakerDepositTx(); if (takerDepositTx != null) { if (!takerDepositTx.isConfirmed()) { tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithUnconfirmedDepositTx", trade.getShortId()))); } else { log.warn("We found a closed trade with locked up funds. " + "That should never happen. trade ID={} ID={}, state={}, payoutState={}, disputeState={}", trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState()); } } else if (!trade.hasBuyerAsTakerWithoutDeposit()) { log.warn("Closed trade with locked up funds missing taker deposit tx. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState()); tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId()))); } return trade.getId(); }) .collect(Collectors.toSet())); if (tradeTxException.get() != null) throw tradeTxException.get(); return tradesIdSet; } } // If trade still has funds locked up it might come back from failed trades // Aborts unfailing if the address entries needed are not available private boolean unFailTrade(Trade trade) { if (!recoverAddresses(trade)) { log.warn("Failed to recover address during unFail trade"); return false; } initTrade(trade); UserThread.execute(() -> { synchronized (tradableList.getList()) { if (!tradableList.contains(trade)) { tradableList.add(trade); } } }); return true; } // The trade is added to pending trades if the associated address entries are AVAILABLE and // the relevant entries are changed, otherwise it's not added and no address entries are changed private boolean recoverAddresses(Trade trade) { // Find addresses associated with this trade. var entries = tradeUtil.getAvailableAddresses(trade); if (entries == null) return false; xmrWalletService.recoverAddressEntry(trade.getId(), entries.second, XmrAddressEntry.Context.TRADE_PAYOUT); return true; } /////////////////////////////////////////////////////////////////////////////////////////// // Getters, Utils /////////////////////////////////////////////////////////////////////////////////////////// public void sendAckMessage(NodeAddress peer, PubKeyRing peersPubKeyRing, TradeMessage message, boolean result, @Nullable String errorMessage, String updatedMultisigHex) { // create ack message String tradeId = message.getOfferId(); String sourceUid = message.getUid(); AckMessage ackMessage = new AckMessage(P2PService.getMyNodeAddress(), AckMessageSourceType.TRADE_MESSAGE, message.getClass().getSimpleName(), sourceUid, tradeId, result, errorMessage, updatedMultisigHex); // send ack message if (!result) { if (errorMessage == null) { log.warn("Sending NACK for {} to peer {} without error message. That should never happen. tradeId={}, sourceUid={}", ackMessage.getSourceMsgClassName(), peer, tradeId, sourceUid); } log.warn("Sending NACK for {} to peer {}. tradeId={}, sourceUid={}, errorMessage={}, updatedMultisigHex={}", ackMessage.getSourceMsgClassName(), peer, tradeId, sourceUid, errorMessage, updatedMultisigHex == null ? "null" : updatedMultisigHex.length() + " characters"); } else { log.info("Sending AckMessage for {} to peer {}. tradeId={}, sourceUid={}", ackMessage.getSourceMsgClassName(), peer, tradeId, sourceUid); } p2PService.getMailboxMessageService().sendEncryptedMailboxMessage( peer, peersPubKeyRing, ackMessage, new SendMailboxMessageListener() { @Override public void onArrived() { log.info("AckMessage for {} arrived at peer {}. tradeId={}, sourceUid={}", ackMessage.getSourceMsgClassName(), peer, tradeId, sourceUid); } @Override public void onStoredInMailbox() { log.info("AckMessage for {} stored in mailbox for peer {}. tradeId={}, sourceUid={}", ackMessage.getSourceMsgClassName(), peer, tradeId, sourceUid); } @Override public void onFault(String errorMessage) { log.error("AckMessage for {} failed. Peer {}. tradeId={}, sourceUid={}, errorMessage={}", ackMessage.getSourceMsgClassName(), peer, tradeId, sourceUid, errorMessage); } } ); } public ObservableList getObservableList() { synchronized (tradableList.getList()) { return tradableList.getObservableList(); } } public BooleanProperty tradesInitializedProperty() { return tradesInitialized; } public boolean isMyOffer(Offer offer) { return offer.isMyOffer(keyRing); } public boolean wasOfferAlreadyUsedInTrade(String offerId) { return getOpenTrade(offerId).isPresent() || failedTradesManager.getTradeById(offerId).isPresent() || closedTradableManager.getTradableById(offerId).isPresent(); } public boolean isBuyer(Offer offer) { // If I am the maker, we use the OfferDirection, otherwise the mirrored direction if (isMyOffer(offer)) return offer.isBuyOffer(); else return offer.getDirection() == OfferDirection.SELL; } // TODO: make Optional versus Trade return types consistent public Trade getTrade(String tradeId) { return getOpenTrade(tradeId).orElseGet(() -> getClosedTrade(tradeId).orElseGet(() -> getFailedTrade(tradeId).orElseGet(() -> null))); } public boolean hasTrade(String tradeId) { return getTrade(tradeId) != null; } public Optional getOpenTrade(String tradeId) { synchronized (tradableList.getList()) { return tradableList.stream().filter(e -> e.getId().equals(tradeId)).findFirst(); } } public boolean hasOpenTrade(Trade trade) { synchronized (tradableList.getList()) { return tradableList.contains(trade); } } public boolean hasFailedScheduledTrade(String offerId) { return failedTradesManager.getTradeById(offerId).isPresent() && failedTradesManager.getTradeById(offerId).get().isProtocolErrorHandlingScheduled(); } public Optional getOpenTradeByUid(String tradeUid) { synchronized (tradableList.getList()) { return tradableList.stream().filter(e -> e.getUid().equals(tradeUid)).findFirst(); } } public List getAllTrades() { synchronized (tradableList.getList()) { List trades = new ArrayList(); synchronized (tradableList.getList()) { trades.addAll(tradableList.getList()); } trades.addAll(closedTradableManager.getClosedTrades()); trades.addAll(failedTradesManager.getObservableList()); return trades; } } public List getTradesReservingMainWallet() { synchronized (tradableList.getList()) { return tradableList.getList().stream() .filter(trade -> trade.isReservingMainWallet()) .collect(Collectors.toList()); } } public List getOpenTrades() { synchronized (tradableList.getList()) { return ImmutableList.copyOf(getObservableList().stream() .filter(e -> e instanceof Trade) .map(e -> e) .collect(Collectors.toList())); } } public List getClosedTrades() { return closedTradableManager.getClosedTrades(); } public Optional getClosedTrade(String tradeId) { return closedTradableManager.getTradeById(tradeId); } public Optional getFailedTrade(String tradeId) { return failedTradesManager.getTradeById(tradeId); } private void addTrade(Trade trade) { synchronized (tradableList.getList()) { if (tradableList.add(trade)) { requestPersistence(); } } } // TODO Remove once tradableList is refactored to a final field // (part of the persistence refactor PR) private void onTradesChanged() { synchronized (numPendingTrades) { numPendingTrades.set(getObservableList().size()); } } } ================================================ FILE: core/src/main/java/haveno/core/trade/TradeModule.java ================================================ /* * 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 . */ package haveno.core.trade; import com.google.inject.Singleton; import haveno.common.app.AppModule; import haveno.common.config.Config; import haveno.core.account.sign.SignedWitnessService; import haveno.core.account.sign.SignedWitnessStorageService; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.account.witness.AccountAgeWitnessStorageService; import haveno.core.trade.failed.FailedTradesManager; import haveno.core.trade.statistics.ReferralIdService; import static com.google.inject.name.Names.named; import static haveno.common.config.Config.DUMP_STATISTICS; public class TradeModule extends AppModule { public TradeModule(Config config) { super(config); } @Override protected void configure() { bind(TradeManager.class).in(Singleton.class); bind(ClosedTradableManager.class).in(Singleton.class); bind(FailedTradesManager.class).in(Singleton.class); bind(AccountAgeWitnessService.class).in(Singleton.class); bind(AccountAgeWitnessStorageService.class).in(Singleton.class); bind(SignedWitnessService.class).in(Singleton.class); bind(SignedWitnessStorageService.class).in(Singleton.class); bind(ReferralIdService.class).in(Singleton.class); bindConstant().annotatedWith(named(DUMP_STATISTICS)).to(config.dumpStatistics); } } ================================================ FILE: core/src/main/java/haveno/core/trade/TradeTxException.java ================================================ /* * 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 . */ package haveno.core.trade; public class TradeTxException extends Exception { public TradeTxException(String message) { super(message); } } ================================================ FILE: core/src/main/java/haveno/core/trade/TradeUtil.java ================================================ /* * 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 . */ package haveno.core.trade; import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.crypto.KeyRing; import haveno.common.util.Tuple2; import static haveno.core.locale.CurrencyUtil.getCurrencyPair; import haveno.core.locale.Res; import haveno.core.offer.Offer; import static haveno.core.util.FormattingUtils.formatDurationAsWords; import haveno.core.xmr.wallet.BtcWalletService; import static java.lang.String.format; import java.util.Date; import java.util.Objects; import javax.annotation.Nullable; import lombok.extern.slf4j.Slf4j; /** * This class contains trade utility methods. */ @Slf4j @Singleton public class TradeUtil { private final BtcWalletService btcWalletService; private final KeyRing keyRing; @Inject public TradeUtil(BtcWalletService btcWalletService, KeyRing keyRing) { this.btcWalletService = btcWalletService; this.keyRing = keyRing; } /** * Returns if and only if both are AVAILABLE, * otherwise null. * @param trade the trade being queried for MULTI_SIG, TRADE_PAYOUT addresses * @return Tuple2 tuple containing MULTI_SIG, TRADE_PAYOUT addresses for trade */ public Tuple2 getAvailableAddresses(Trade trade) { var addresses = getTradeAddresses(trade); if (addresses == null) return null; if (btcWalletService.getAvailableAddressEntries().stream() .noneMatch(e -> Objects.equals(e.getAddressString(), addresses.first))) return null; if (btcWalletService.getAvailableAddressEntries().stream() .noneMatch(e -> Objects.equals(e.getAddressString(), addresses.second))) return null; return new Tuple2<>(addresses.first, addresses.second); } /** * Returns addresses as strings if they're known by the * wallet. * @param trade the trade being queried for MULTI_SIG, TRADE_PAYOUT addresses * @return Tuple2 tuple containing MULTI_SIG, TRADE_PAYOUT addresses for trade */ public Tuple2 getTradeAddresses(Trade trade) { throw new RuntimeException("TradeUtil.getTradeAddresses() not implemented for XMR"); // var contract = trade.getContract(); // if (contract == null) // return null; // // // Get multisig address // var isMyRoleBuyer = contract.isMyRoleBuyer(keyRing.getPubKeyRing()); // var multiSigPubKey = isMyRoleBuyer // ? contract.getBuyerMultiSigPubKey() // : contract.getSellerMultiSigPubKey(); // if (multiSigPubKey == null) // return null; // // var multiSigPubKeyString = Utilities.bytesAsHexString(multiSigPubKey); // var multiSigAddress = btcWalletService.getAddressEntryListAsImmutableList().stream() // .filter(e -> e.getKeyPair().getPublicKeyAsHex().equals(multiSigPubKeyString)) // .findAny() // .orElse(null); // if (multiSigAddress == null) // return null; // // // Get payout address // var payoutAddress = isMyRoleBuyer // ? contract.getBuyerPayoutAddressString() // : contract.getSellerPayoutAddressString(); // var payoutAddressEntry = btcWalletService.getAddressEntryListAsImmutableList().stream() // .filter(e -> Objects.equals(e.getAddressString(), payoutAddress)) // .findAny() // .orElse(null); // if (payoutAddressEntry == null) // return null; // // return new Tuple2<>(multiSigAddress.getAddressString(), payoutAddress); } public long getRemainingTradeDuration(Trade trade) { return trade.getMaxTradePeriodDate() != null ? trade.getMaxTradePeriodDate().getTime() - new Date().getTime() : getMaxTradePeriod(trade); } public long getMaxTradePeriod(Trade trade) { return trade.getOffer() != null ? trade.getOffer().getPaymentMethod().getMaxTradePeriod() : 0; } public double getRemainingTradeDurationAsPercentage(Trade trade) { long maxPeriod = getMaxTradePeriod(trade); long remaining = getRemainingTradeDuration(trade); if (maxPeriod != 0) { return 1 - (double) remaining / (double) maxPeriod; } else return 0; } public String getRemainingTradeDurationAsWords(Trade trade) { return formatDurationAsWords(Math.max(0, getRemainingTradeDuration(trade))); } @Nullable public Date getHalfTradePeriodDate(Trade trade) { return trade != null ? trade.getHalfTradePeriodDate() : null; } public Date getDateForOpenDispute(Trade trade) { return new Date(new Date().getTime() + getRemainingTradeDuration(trade)); } public String getMarketDescription(Trade trade) { if (trade == null) return ""; checkNotNull(trade.getOffer()); checkNotNull(trade.getOffer().getCounterCurrencyCode()); return getCurrencyPair(trade.getOffer().getCounterCurrencyCode()); } public String getPaymentMethodNameWithCountryCode(Trade trade) { if (trade == null) return ""; Offer offer = trade.getOffer(); checkNotNull(offer); checkNotNull(offer.getPaymentMethod()); return offer.getPaymentMethodNameWithCountryCode(); } /** * Returns a string describing a trader's role for a given trade. * @param trade Trade * @return String describing a trader's role for a given trade */ public static String getRole(Trade trade) { Offer offer = trade.getOffer(); if (offer == null) throw new IllegalStateException(format("could not get role because no offer was found for trade '%s'", trade.getShortId())); return (trade.isArbitrator() ? "Arbitrator for " : "") + // TODO: use Res.get() getRole(trade.getBuyer() == trade.getMaker(), trade.isArbitrator() ? true : trade.isMaker(), // arbitrator role in context of maker offer.getCounterCurrencyCode()); } /** * Returns a string describing a trader's role. * * @param isBuyerMakerAndSellerTaker boolean * @param isMaker boolean * @param currencyCode String * @return String describing a trader's role */ private static String getRole(boolean isBuyerMakerAndSellerTaker, boolean isMaker, String currencyCode) { String baseCurrencyCode = Res.getBaseCurrencyCode(); if (isBuyerMakerAndSellerTaker) return isMaker ? Res.get("formatter.asMaker", baseCurrencyCode, Res.get("shared.buyer")) : Res.get("formatter.asTaker", baseCurrencyCode, Res.get("shared.seller")); else return isMaker ? Res.get("formatter.asMaker", baseCurrencyCode, Res.get("shared.seller")) : Res.get("formatter.asTaker", baseCurrencyCode, Res.get("shared.buyer")); } } ================================================ FILE: core/src/main/java/haveno/core/trade/failed/FailedTradesManager.java ================================================ /* * 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 . */ package haveno.core.trade.failed; import com.google.inject.Inject; import haveno.common.crypto.KeyRing; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.persistable.PersistedDataHost; import haveno.core.offer.Offer; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.CleanupMailboxMessages; import haveno.core.trade.TradableList; import haveno.core.trade.Trade; import haveno.core.trade.TradeUtil; import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.wallet.XmrWalletService; import javafx.collections.ObservableList; import lombok.Setter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Optional; import java.util.function.Function; import java.util.stream.Stream; public class FailedTradesManager implements PersistedDataHost { private static final Logger log = LoggerFactory.getLogger(FailedTradesManager.class); private final TradableList failedTrades = new TradableList<>(); private final KeyRing keyRing; private final PriceFeedService priceFeedService; private final XmrWalletService xmrWalletService; private final CleanupMailboxMessages cleanupMailboxMessages; private final PersistenceManager> persistenceManager; private final TradeUtil tradeUtil; @Setter private Function unFailTradeCallback; @Inject public FailedTradesManager(KeyRing keyRing, PriceFeedService priceFeedService, XmrWalletService xmrWalletService, PersistenceManager> persistenceManager, TradeUtil tradeUtil, CleanupMailboxMessages cleanupMailboxMessages) { this.keyRing = keyRing; this.priceFeedService = priceFeedService; this.xmrWalletService = xmrWalletService; this.cleanupMailboxMessages = cleanupMailboxMessages; this.persistenceManager = persistenceManager; this.tradeUtil = tradeUtil; this.persistenceManager.initialize(failedTrades, "FailedTrades", PersistenceManager.Source.PRIVATE); } @Override public void readPersisted(Runnable completeHandler) { persistenceManager.readPersisted(persisted -> { synchronized (persisted.getList()) { failedTrades.setAll(persisted.getList()); failedTrades.stream() .filter(trade -> trade.getOffer() != null) .forEach(trade -> trade.getOffer().setPriceFeedService(priceFeedService)); } completeHandler.run(); }, completeHandler); } public void onAllServicesInitialized() { cleanupMailboxMessages.handleTrades(failedTrades.getList()); } public void add(Trade trade) { synchronized (failedTrades.getList()) { if (failedTrades.add(trade)) { requestPersistence(); } } } public void removeTrade(Trade trade) { synchronized (failedTrades.getList()) { if (failedTrades.remove(trade)) { requestPersistence(); } } } public boolean wasMyOffer(Offer offer) { return offer.isMyOffer(keyRing); } public ObservableList getObservableList() { synchronized (failedTrades.getList()) { return failedTrades.getObservableList(); } } public Optional getTradeById(String id) { synchronized (failedTrades.getList()) { return failedTrades.stream().filter(e -> e.getId().equals(id)).findFirst(); } } public Stream getTradesStreamWithFundsLockedIn() { synchronized (failedTrades.getList()) { return failedTrades.stream() .filter(Trade::isFundsLockedIn); } } public void unFailTrade(Trade trade) { synchronized (failedTrades.getList()) { if (unFailTradeCallback == null) return; if (unFailTradeCallback.apply(trade)) { log.info("Unfailing trade {}", trade.getId()); if (failedTrades.remove(trade)) { requestPersistence(); } } } } public String checkUnFail(Trade trade) { var addresses = tradeUtil.getTradeAddresses(trade); if (addresses == null) { return "Addresses not found"; } StringBuilder blockingTrades = new StringBuilder(); for (var entry : xmrWalletService.getAddressEntryListAsImmutableList()) { if (entry.getContext() == XmrAddressEntry.Context.AVAILABLE) continue; if (entry.getAddressString() != null && (entry.getAddressString().equals(addresses.first) || entry.getAddressString().equals(addresses.second))) { blockingTrades.append(entry.getOfferId()).append(", "); } } return blockingTrades.toString(); } private void requestPersistence() { persistenceManager.requestPersistence(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/handlers/TradeResultHandler.java ================================================ /* * 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 . */ package haveno.core.trade.handlers; import haveno.core.trade.Trade; public interface TradeResultHandler { void handleResult(Trade trade); } ================================================ FILE: core/src/main/java/haveno/core/trade/handlers/TransactionResultHandler.java ================================================ /* * 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 . */ package haveno.core.trade.handlers; import org.bitcoinj.core.Transaction; public interface TransactionResultHandler { void handleResult(Transaction transaction); } ================================================ FILE: core/src/main/java/haveno/core/trade/messages/DepositRequest.java ================================================ /* * 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 . */ package haveno.core.trade.messages; import com.google.protobuf.ByteString; import haveno.common.proto.ProtoUtil; import haveno.common.util.Utilities; import haveno.core.proto.CoreProtoResolver; import haveno.network.p2p.DirectMessage; import lombok.EqualsAndHashCode; import lombok.Value; import javax.annotation.Nullable; import java.util.Optional; @EqualsAndHashCode(callSuper = true) @Value public final class DepositRequest extends TradeMessage implements DirectMessage { private final long currentDate; private final byte[] contractSignature; @Nullable private final String depositTxHex; @Nullable private final String depositTxKey; @Nullable private final byte[] paymentAccountKey; public DepositRequest(String tradeId, String uid, String messageVersion, long currentDate, byte[] contractSignature, @Nullable String depositTxHex, @Nullable String depositTxKey, @Nullable byte[] paymentAccountKey) { super(messageVersion, tradeId, uid); this.currentDate = currentDate; this.contractSignature = contractSignature; this.depositTxHex = depositTxHex; this.depositTxKey = depositTxKey; this.paymentAccountKey = paymentAccountKey; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { protobuf.DepositRequest.Builder builder = protobuf.DepositRequest.newBuilder() .setTradeId(offerId) .setUid(uid); builder.setCurrentDate(currentDate); Optional.ofNullable(paymentAccountKey).ifPresent(e -> builder.setPaymentAccountKey(ByteString.copyFrom(e))); Optional.ofNullable(depositTxHex).ifPresent(builder::setDepositTxHex); Optional.ofNullable(depositTxKey).ifPresent(builder::setDepositTxKey); Optional.ofNullable(contractSignature).ifPresent(e -> builder.setContractSignature(ByteString.copyFrom(e))); return getNetworkEnvelopeBuilder().setDepositRequest(builder).build(); } public static DepositRequest fromProto(protobuf.DepositRequest proto, CoreProtoResolver coreProtoResolver, String messageVersion) { return new DepositRequest(proto.getTradeId(), proto.getUid(), messageVersion, proto.getCurrentDate(), ProtoUtil.byteArrayOrNullFromProto(proto.getContractSignature()), ProtoUtil.stringOrNullFromProto(proto.getDepositTxHex()), ProtoUtil.stringOrNullFromProto(proto.getDepositTxKey()), ProtoUtil.byteArrayOrNullFromProto(proto.getPaymentAccountKey())); } @Override public String toString() { return "DepositRequest {" + ",\n currentDate=" + currentDate + ",\n contractSignature=" + Utilities.bytesAsHexString(contractSignature) + ",\n depositTxHex='" + depositTxHex + ",\n depositTxKey='" + depositTxKey + ",\n paymentAccountKey='" + paymentAccountKey + "\n} " + super.toString(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/messages/DepositResponse.java ================================================ /* * 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 . */ package haveno.core.trade.messages; import haveno.common.proto.ProtoUtil; import haveno.core.proto.CoreProtoResolver; import haveno.network.p2p.DirectMessage; import lombok.EqualsAndHashCode; import lombok.Value; import java.util.Optional; @EqualsAndHashCode(callSuper = true) @Value public final class DepositResponse extends TradeMessage implements DirectMessage { private final long currentDate; private final String errorMessage; private final long buyerSecurityDeposit; private final long sellerSecurityDeposit; public DepositResponse(String tradeId, String uid, String messageVersion, long currentDate, String errorMessage, long buyerSecurityDeposit, long sellerSecurityDeposit) { super(messageVersion, tradeId, uid); this.currentDate = currentDate; this.errorMessage = errorMessage; this.buyerSecurityDeposit = buyerSecurityDeposit; this.sellerSecurityDeposit = sellerSecurityDeposit; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { protobuf.DepositResponse.Builder builder = protobuf.DepositResponse.newBuilder() .setTradeId(offerId) .setUid(uid); builder.setCurrentDate(currentDate); builder.setBuyerSecurityDeposit(buyerSecurityDeposit); builder.setSellerSecurityDeposit(sellerSecurityDeposit); Optional.ofNullable(errorMessage).ifPresent(e -> builder.setErrorMessage(errorMessage)); return getNetworkEnvelopeBuilder().setDepositResponse(builder).build(); } public static DepositResponse fromProto(protobuf.DepositResponse proto, CoreProtoResolver coreProtoResolver, String messageVersion) { return new DepositResponse(proto.getTradeId(), proto.getUid(), messageVersion, proto.getCurrentDate(), ProtoUtil.stringOrNullFromProto(proto.getErrorMessage()), proto.getBuyerSecurityDeposit(), proto.getSellerSecurityDeposit()); } @Override public String toString() { return "DepositResponse {" + ",\n currentDate=" + currentDate + ",\n errorMessage=" + errorMessage + ",\n buyerSecurityDeposit=" + buyerSecurityDeposit + ",\n sellerSecurityDeposit=" + sellerSecurityDeposit + "\n} " + super.toString(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/messages/DepositsConfirmedMessage.java ================================================ /* * 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 . */ package haveno.core.trade.messages; import com.google.protobuf.ByteString; import haveno.common.app.Version; import haveno.common.crypto.PubKeyRing; import haveno.common.proto.ProtoUtil; import haveno.common.util.Utilities; import haveno.core.proto.CoreProtoResolver; import haveno.network.p2p.NodeAddress; import lombok.EqualsAndHashCode; import lombok.Value; import javax.annotation.Nullable; import java.util.Optional; @EqualsAndHashCode(callSuper = true) @Value public final class DepositsConfirmedMessage extends TradeMailboxMessage { private final NodeAddress senderNodeAddress; private final PubKeyRing pubKeyRing; @Nullable private final byte[] sellerPaymentAccountKey; @Nullable private final String updatedMultisigHex; public DepositsConfirmedMessage(String tradeId, NodeAddress senderNodeAddress, PubKeyRing pubKeyRing, String uid, @Nullable byte[] sellerPaymentAccountKey, @Nullable String updatedMultisigHex) { super(Version.getP2PMessageVersion(), tradeId, uid); this.senderNodeAddress = senderNodeAddress; this.pubKeyRing = pubKeyRing; this.sellerPaymentAccountKey = sellerPaymentAccountKey; this.updatedMultisigHex = updatedMultisigHex; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { protobuf.DepositsConfirmedMessage.Builder builder = protobuf.DepositsConfirmedMessage.newBuilder() .setTradeId(offerId) .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setPubKeyRing(pubKeyRing.toProtoMessage()) .setUid(uid); Optional.ofNullable(sellerPaymentAccountKey).ifPresent(e -> builder.setSellerPaymentAccountKey(ByteString.copyFrom(e))); Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex)); return getNetworkEnvelopeBuilder().setDepositsConfirmedMessage(builder).build(); } public static DepositsConfirmedMessage fromProto(protobuf.DepositsConfirmedMessage proto, CoreProtoResolver coreProtoResolver, String messageVersion) { return new DepositsConfirmedMessage(proto.getTradeId(), NodeAddress.fromProto(proto.getSenderNodeAddress()), PubKeyRing.fromProto(proto.getPubKeyRing()), proto.getUid(), ProtoUtil.byteArrayOrNullFromProto(proto.getSellerPaymentAccountKey()), ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex())); } @Override public String toString() { return "DepositsConfirmedMessage {" + "\n senderNodeAddress=" + senderNodeAddress + ",\n sellerPaymentAccountKey=" + Utilities.bytesAsHexString(sellerPaymentAccountKey) + ",\n updatedMultisigHex=" + (updatedMultisigHex == null ? null : updatedMultisigHex.substring(0, Math.max(updatedMultisigHex.length(), 1000))) + "\n} " + super.toString(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/messages/InitMultisigRequest.java ================================================ /* * 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 . */ package haveno.core.trade.messages; import haveno.common.proto.ProtoUtil; import haveno.core.proto.CoreProtoResolver; import haveno.network.p2p.DirectMessage; import lombok.EqualsAndHashCode; import lombok.Value; import javax.annotation.Nullable; import java.util.Optional; @EqualsAndHashCode(callSuper = true) @Value public final class InitMultisigRequest extends TradeMessage implements DirectMessage { private final long currentDate; @Nullable private final String preparedMultisigHex; @Nullable private final String madeMultisigHex; @Nullable private final String exchangedMultisigHex; @Nullable private final String tradeFeeAddress; public InitMultisigRequest(String tradeId, String uid, String messageVersion, long currentDate, String preparedMultisigHex, String madeMultisigHex, String exchangedMultisigHex, String tradeFeeAddress) { super(messageVersion, tradeId, uid); this.currentDate = currentDate; this.preparedMultisigHex = preparedMultisigHex; this.madeMultisigHex = madeMultisigHex; this.exchangedMultisigHex = exchangedMultisigHex; this.tradeFeeAddress = tradeFeeAddress; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { protobuf.InitMultisigRequest.Builder builder = protobuf.InitMultisigRequest.newBuilder() .setTradeId(offerId) .setUid(uid); Optional.ofNullable(preparedMultisigHex).ifPresent(e -> builder.setPreparedMultisigHex(preparedMultisigHex)); Optional.ofNullable(madeMultisigHex).ifPresent(e -> builder.setMadeMultisigHex(madeMultisigHex)); Optional.ofNullable(exchangedMultisigHex).ifPresent(e -> builder.setExchangedMultisigHex(exchangedMultisigHex)); Optional.ofNullable(tradeFeeAddress).ifPresent(e -> builder.setTradeFeeAddress(tradeFeeAddress)); builder.setCurrentDate(currentDate); return getNetworkEnvelopeBuilder().setInitMultisigRequest(builder).build(); } public static InitMultisigRequest fromProto(protobuf.InitMultisigRequest proto, CoreProtoResolver coreProtoResolver, String messageVersion) { return new InitMultisigRequest(proto.getTradeId(), proto.getUid(), messageVersion, proto.getCurrentDate(), ProtoUtil.stringOrNullFromProto(proto.getPreparedMultisigHex()), ProtoUtil.stringOrNullFromProto(proto.getMadeMultisigHex()), ProtoUtil.stringOrNullFromProto(proto.getExchangedMultisigHex()), ProtoUtil.stringOrNullFromProto(proto.getTradeFeeAddress())); } @Override public String toString() { return "InitMultisigRequest {" + ",\n currentDate=" + currentDate + ",\n preparedMultisigHex=" + preparedMultisigHex + ",\n madeMultisigHex=" + madeMultisigHex + ",\n exchangedMultisigHex=" + exchangedMultisigHex + ",\n tradeFeeAddress=" + tradeFeeAddress + "\n} " + super.toString(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/messages/InitTradeRequest.java ================================================ /* * 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 . */ package haveno.core.trade.messages; import com.google.protobuf.ByteString; import haveno.common.crypto.PubKeyRing; import haveno.common.proto.ProtoUtil; import haveno.common.util.Utilities; import haveno.core.proto.CoreProtoResolver; import haveno.network.p2p.DirectMessage; import haveno.network.p2p.NodeAddress; import lombok.EqualsAndHashCode; import lombok.Value; import javax.annotation.Nullable; import java.util.Optional; @EqualsAndHashCode(callSuper = true) @Value public final class InitTradeRequest extends TradeMessage implements DirectMessage { TradeProtocolVersion tradeProtocolVersion; private final long tradeAmount; private final long tradePrice; private final String paymentMethodId; @Nullable private final String makerAccountId; private final String takerAccountId; private final String makerPaymentAccountId; private final String takerPaymentAccountId; private final PubKeyRing takerPubKeyRing; @Nullable private final byte[] accountAgeWitnessSignatureOfOfferId; private final long currentDate; private final NodeAddress makerNodeAddress; private final NodeAddress takerNodeAddress; @Nullable private final NodeAddress arbitratorNodeAddress; @Nullable private final String reserveTxHash; @Nullable private final String reserveTxHex; @Nullable private final String reserveTxKey; @Nullable private final String payoutAddress; @Nullable private final String challenge; public InitTradeRequest(TradeProtocolVersion tradeProtocolVersion, String offerId, long tradeAmount, long tradePrice, String paymentMethodId, @Nullable String makerAccountId, String takerAccountId, String makerPaymentAccountId, String takerPaymentAccountId, PubKeyRing takerPubKeyRing, String uid, String messageVersion, @Nullable byte[] accountAgeWitnessSignatureOfOfferId, long currentDate, NodeAddress makerNodeAddress, NodeAddress takerNodeAddress, NodeAddress arbitratorNodeAddress, @Nullable String reserveTxHash, @Nullable String reserveTxHex, @Nullable String reserveTxKey, @Nullable String payoutAddress, @Nullable String challenge) { super(messageVersion, offerId, uid); this.tradeProtocolVersion = tradeProtocolVersion; this.tradeAmount = tradeAmount; this.tradePrice = tradePrice; this.makerAccountId = makerAccountId; this.takerAccountId = takerAccountId; this.makerPaymentAccountId = makerPaymentAccountId; this.takerPaymentAccountId = takerPaymentAccountId; this.takerPubKeyRing = takerPubKeyRing; this.paymentMethodId = paymentMethodId; this.accountAgeWitnessSignatureOfOfferId = accountAgeWitnessSignatureOfOfferId; this.currentDate = currentDate; this.makerNodeAddress = makerNodeAddress; this.takerNodeAddress = takerNodeAddress; this.arbitratorNodeAddress = arbitratorNodeAddress; this.reserveTxHash = reserveTxHash; this.reserveTxHex = reserveTxHex; this.reserveTxKey = reserveTxKey; this.payoutAddress = payoutAddress; this.challenge = challenge; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { protobuf.InitTradeRequest.Builder builder = protobuf.InitTradeRequest.newBuilder() .setTradeProtocolVersion(TradeProtocolVersion.toProtoMessage(tradeProtocolVersion)) .setOfferId(offerId) .setTakerNodeAddress(takerNodeAddress.toProtoMessage()) .setMakerNodeAddress(makerNodeAddress.toProtoMessage()) .setTradeAmount(tradeAmount) .setTradePrice(tradePrice) .setTakerPubKeyRing(takerPubKeyRing.toProtoMessage()) .setMakerPaymentAccountId(makerPaymentAccountId) .setTakerPaymentAccountId(takerPaymentAccountId) .setPaymentMethodId(paymentMethodId) .setTakerAccountId(takerAccountId) .setUid(uid); Optional.ofNullable(makerAccountId).ifPresent(e -> builder.setMakerAccountId(makerAccountId)); Optional.ofNullable(arbitratorNodeAddress).ifPresent(e -> builder.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage())); Optional.ofNullable(reserveTxHash).ifPresent(e -> builder.setReserveTxHash(reserveTxHash)); Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex)); Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey)); Optional.ofNullable(payoutAddress).ifPresent(e -> builder.setPayoutAddress(payoutAddress)); Optional.ofNullable(challenge).ifPresent(e -> builder.setChallenge(challenge)); Optional.ofNullable(accountAgeWitnessSignatureOfOfferId).ifPresent(e -> builder.setAccountAgeWitnessSignatureOfOfferId(ByteString.copyFrom(e))); builder.setCurrentDate(currentDate); return getNetworkEnvelopeBuilder().setInitTradeRequest(builder).build(); } public static InitTradeRequest fromProto(protobuf.InitTradeRequest proto, CoreProtoResolver coreProtoResolver, String messageVersion) { return new InitTradeRequest(TradeProtocolVersion.fromProto(proto.getTradeProtocolVersion()), proto.getOfferId(), proto.getTradeAmount(), proto.getTradePrice(), proto.getPaymentMethodId(), ProtoUtil.stringOrNullFromProto(proto.getMakerAccountId()), proto.getTakerAccountId(), proto.getMakerPaymentAccountId(), proto.getTakerPaymentAccountId(), PubKeyRing.fromProto(proto.getTakerPubKeyRing()), proto.getUid(), messageVersion, ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessSignatureOfOfferId()), proto.getCurrentDate(), NodeAddress.fromProto(proto.getMakerNodeAddress()), NodeAddress.fromProto(proto.getTakerNodeAddress()), proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null, ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()), ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()), ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()), ProtoUtil.stringOrNullFromProto(proto.getPayoutAddress()), ProtoUtil.stringOrNullFromProto(proto.getChallenge())); } @Override public String toString() { return "InitTradeRequest{" + "\n tradeProtocolVersion=" + tradeProtocolVersion + ",\n offerId=" + offerId + ",\n tradeAmount=" + tradeAmount + ",\n tradePrice=" + tradePrice + ",\n paymentMethodId=" + paymentMethodId + ",\n makerAccountId=" + makerAccountId + ",\n takerAccountId=" + takerAccountId + ",\n makerPaymentAccountId=" + makerPaymentAccountId + ",\n takerPaymentAccountId=" + takerPaymentAccountId + ",\n takerPubKeyRing=" + takerPubKeyRing + ",\n accountAgeWitnessSignatureOfOfferId=" + Utilities.bytesAsHexString(accountAgeWitnessSignatureOfOfferId) + ",\n currentDate=" + currentDate + ",\n makerNodeAddress=" + makerNodeAddress + ",\n takerNodeAddress=" + takerNodeAddress + ",\n arbitratorNodeAddress=" + arbitratorNodeAddress + ",\n reserveTxHash=" + reserveTxHash + ",\n reserveTxHex=" + reserveTxHex + ",\n reserveTxKey=" + reserveTxKey + ",\n payoutAddress=" + payoutAddress + ",\n challenge=" + challenge + "\n} " + super.toString(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/messages/MediatedPayoutTxPublishedMessage.java ================================================ /* * 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 . */ package haveno.core.trade.messages; import com.google.protobuf.ByteString; import haveno.common.app.Version; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.util.Utilities; import haveno.network.p2p.NodeAddress; import lombok.EqualsAndHashCode; import lombok.Value; @EqualsAndHashCode(callSuper = true) @Value public final class MediatedPayoutTxPublishedMessage extends TradeMailboxMessage { private final byte[] payoutTx; private final NodeAddress senderNodeAddress; public MediatedPayoutTxPublishedMessage(String tradeId, byte[] payoutTx, NodeAddress senderNodeAddress, String uid) { this(tradeId, payoutTx, senderNodeAddress, uid, Version.getP2PMessageVersion()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private MediatedPayoutTxPublishedMessage(String tradeId, byte[] payoutTx, NodeAddress senderNodeAddress, String uid, String messageVersion) { super(messageVersion, tradeId, uid); this.payoutTx = payoutTx; this.senderNodeAddress = senderNodeAddress; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { return getNetworkEnvelopeBuilder() .setMediatedPayoutTxPublishedMessage(protobuf.MediatedPayoutTxPublishedMessage.newBuilder() .setTradeId(offerId) .setPayoutTx(ByteString.copyFrom(payoutTx)) .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setUid(uid)) .build(); } public static NetworkEnvelope fromProto(protobuf.MediatedPayoutTxPublishedMessage proto, String messageVersion) { return new MediatedPayoutTxPublishedMessage(proto.getTradeId(), proto.getPayoutTx().toByteArray(), NodeAddress.fromProto(proto.getSenderNodeAddress()), proto.getUid(), messageVersion); } @Override public String toString() { return "MediatedPayoutTxPublishedMessage{" + "\n payoutTx=" + Utilities.bytesAsHexString(payoutTx) + ",\n senderNodeAddress=" + senderNodeAddress + ",\n uid='" + uid + '\'' + "\n} " + super.toString(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/messages/MediatedPayoutTxSignatureMessage.java ================================================ /* * 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 . */ package haveno.core.trade.messages; import com.google.protobuf.ByteString; import haveno.common.app.Version; import haveno.common.util.Utilities; import haveno.network.p2p.NodeAddress; import lombok.EqualsAndHashCode; import lombok.Value; import lombok.extern.slf4j.Slf4j; @Slf4j @Value @EqualsAndHashCode(callSuper = true) public class MediatedPayoutTxSignatureMessage extends TradeMailboxMessage { private final byte[] txSignature; private final NodeAddress senderNodeAddress; public MediatedPayoutTxSignatureMessage(byte[] txSignature, String tradeId, NodeAddress senderNodeAddress, String uid) { this(txSignature, tradeId, senderNodeAddress, uid, Version.getP2PMessageVersion()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private MediatedPayoutTxSignatureMessage(byte[] txSignature, String tradeId, NodeAddress senderNodeAddress, String uid, String messageVersion) { super(messageVersion, tradeId, uid); this.txSignature = txSignature; this.senderNodeAddress = senderNodeAddress; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { return getNetworkEnvelopeBuilder() .setMediatedPayoutTxSignatureMessage(protobuf.MediatedPayoutTxSignatureMessage.newBuilder() .setTxSignature(ByteString.copyFrom(txSignature)) .setTradeId(offerId) .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setUid(uid)) .build(); } public static MediatedPayoutTxSignatureMessage fromProto(protobuf.MediatedPayoutTxSignatureMessage proto, String messageVersion) { return new MediatedPayoutTxSignatureMessage(proto.getTxSignature().toByteArray(), proto.getTradeId(), NodeAddress.fromProto(proto.getSenderNodeAddress()), proto.getUid(), messageVersion); } @Override public String getOfferId() { return offerId; } @Override public String toString() { return "MediatedPayoutSignatureMessage{" + "\n txSignature=" + Utilities.bytesAsHexString(txSignature) + ",\n tradeId='" + offerId + '\'' + ",\n senderNodeAddress=" + senderNodeAddress + "\n} " + super.toString(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/messages/PaymentReceivedMessage.java ================================================ /* * 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 . */ package haveno.core.trade.messages; import com.google.protobuf.ByteString; import haveno.common.app.Version; import haveno.common.proto.ProtoUtil; import haveno.core.account.sign.SignedWitness; import haveno.core.account.witness.AccountAgeWitness; import haveno.network.p2p.NodeAddress; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.util.Optional; @Slf4j @EqualsAndHashCode(callSuper = true) @Getter public final class PaymentReceivedMessage extends TradeMailboxMessage { private final NodeAddress senderNodeAddress; @Nullable private final String unsignedPayoutTxHex; @Nullable private final String signedPayoutTxHex; private final String updatedMultisigHex; private final boolean deferPublishPayout; @Nullable private final AccountAgeWitness buyerAccountAgeWitness; @Nullable private final SignedWitness buyerSignedWitness; @Nullable private final PaymentSentMessage paymentSentMessage; @Setter @Nullable private byte[] sellerSignature; @Setter @Nullable private String payoutTxId; public PaymentReceivedMessage(String tradeId, NodeAddress senderNodeAddress, String uid, String unsignedPayoutTxHex, String signedPayoutTxHex, String updatedMultisigHex, boolean deferPublishPayout, AccountAgeWitness buyerAccountAgeWitness, @Nullable SignedWitness buyerSignedWitness, @Nullable PaymentSentMessage paymentSentMessage, @Nullable String payoutTxId) { this(tradeId, senderNodeAddress, uid, Version.getP2PMessageVersion(), unsignedPayoutTxHex, signedPayoutTxHex, updatedMultisigHex, deferPublishPayout, buyerAccountAgeWitness, buyerSignedWitness, paymentSentMessage, payoutTxId); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private PaymentReceivedMessage(String tradeId, NodeAddress senderNodeAddress, String uid, String messageVersion, String unsignedPayoutTxHex, String signedPayoutTxHex, String updatedMultisigHex, boolean deferPublishPayout, AccountAgeWitness buyerAccountAgeWitness, @Nullable SignedWitness buyerSignedWitness, PaymentSentMessage paymentSentMessage, @Nullable String payoutTxId) { super(messageVersion, tradeId, uid); this.senderNodeAddress = senderNodeAddress; this.unsignedPayoutTxHex = unsignedPayoutTxHex; this.signedPayoutTxHex = signedPayoutTxHex; this.updatedMultisigHex = updatedMultisigHex; this.deferPublishPayout = deferPublishPayout; this.paymentSentMessage = paymentSentMessage; this.buyerAccountAgeWitness = buyerAccountAgeWitness; this.buyerSignedWitness = buyerSignedWitness; this.payoutTxId = payoutTxId; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { protobuf.PaymentReceivedMessage.Builder builder = protobuf.PaymentReceivedMessage.newBuilder() .setTradeId(offerId) .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setUid(uid) .setDeferPublishPayout(deferPublishPayout); Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex)); Optional.ofNullable(unsignedPayoutTxHex).ifPresent(e -> builder.setUnsignedPayoutTxHex(unsignedPayoutTxHex)); Optional.ofNullable(signedPayoutTxHex).ifPresent(e -> builder.setSignedPayoutTxHex(signedPayoutTxHex)); Optional.ofNullable(buyerAccountAgeWitness).ifPresent(buyerAccountAgeWitness -> builder.setBuyerAccountAgeWitness(buyerAccountAgeWitness.toProtoAccountAgeWitness())); Optional.ofNullable(buyerSignedWitness).ifPresent(buyerSignedWitness -> builder.setBuyerSignedWitness(buyerSignedWitness.toProtoSignedWitness())); Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage())); Optional.ofNullable(sellerSignature).ifPresent(e -> builder.setSellerSignature(ByteString.copyFrom(e))); Optional.ofNullable(payoutTxId).ifPresent(builder::setPayoutTxId); return getNetworkEnvelopeBuilder().setPaymentReceivedMessage(builder).build(); } public static PaymentReceivedMessage fromProto(protobuf.PaymentReceivedMessage proto, String messageVersion) { // There is no method to check for a nullable non-primitive data type object but we know that all fields // are empty/null, so we check for the signature to see if we got a valid buyerSignedWitness. protobuf.AccountAgeWitness protoAccountAgeWitness = proto.getBuyerAccountAgeWitness(); AccountAgeWitness buyerAccountAgeWitness = protoAccountAgeWitness.getHash().isEmpty() ? null : AccountAgeWitness.fromProto(protoAccountAgeWitness); protobuf.SignedWitness protoSignedWitness = proto.getBuyerSignedWitness(); SignedWitness buyerSignedWitness = !protoSignedWitness.getSignature().isEmpty() ? SignedWitness.fromProto(protoSignedWitness) : null; PaymentReceivedMessage message = new PaymentReceivedMessage(proto.getTradeId(), NodeAddress.fromProto(proto.getSenderNodeAddress()), proto.getUid(), messageVersion, ProtoUtil.stringOrNullFromProto(proto.getUnsignedPayoutTxHex()), ProtoUtil.stringOrNullFromProto(proto.getSignedPayoutTxHex()), ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()), proto.getDeferPublishPayout(), buyerAccountAgeWitness, buyerSignedWitness, proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion) : null, ProtoUtil.stringOrNullFromProto(proto.getPayoutTxId())); message.setSellerSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getSellerSignature())); return message; } @Override public String toString() { return "PaymentReceivedMessage{" + "\n senderNodeAddress=" + senderNodeAddress + ",\n buyerSignedWitness=" + buyerSignedWitness + ",\n unsignedPayoutTxHex=" + unsignedPayoutTxHex + ",\n signedPayoutTxHex=" + signedPayoutTxHex + ",\n updatedMultisigHex=" + (updatedMultisigHex == null ? null : updatedMultisigHex.substring(0, Math.max(updatedMultisigHex.length(), 1000))) + ",\n deferPublishPayout=" + deferPublishPayout + ",\n paymentSentMessage=" + paymentSentMessage + ",\n sellerSignature=" + sellerSignature + ",\n payoutTxId=" + payoutTxId + "\n} " + super.toString(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/messages/PaymentSentMessage.java ================================================ /* * 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 . */ package haveno.core.trade.messages; import com.google.protobuf.ByteString; import haveno.common.app.Version; import haveno.common.proto.ProtoUtil; import haveno.core.account.witness.AccountAgeWitness; import haveno.network.p2p.NodeAddress; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import javax.annotation.Nullable; import java.util.Optional; @EqualsAndHashCode(callSuper = true) @Getter public final class PaymentSentMessage extends TradeMailboxMessage { private final NodeAddress senderNodeAddress; @Nullable private final String counterCurrencyTxId; @Nullable private final String payoutTxHex; @Nullable private final String updatedMultisigHex; @Nullable private final byte[] paymentAccountKey; @Nullable private AccountAgeWitness sellerAccountAgeWitness; @Setter @Nullable private byte[] buyerSignature; // Added after v1.3.7 // We use that for the XMR txKey but want to keep it generic to be flexible for data of other payment methods or assets. @Nullable private String counterCurrencyExtraData; public PaymentSentMessage(String tradeId, NodeAddress senderNodeAddress, @Nullable String counterCurrencyTxId, @Nullable String counterCurrencyExtraData, String uid, @Nullable String signedPayoutTxHex, @Nullable String updatedMultisigHex, @Nullable byte[] paymentAccountKey, AccountAgeWitness sellerAccountAgeWitness) { this(tradeId, senderNodeAddress, counterCurrencyTxId, counterCurrencyExtraData, uid, Version.getP2PMessageVersion(), signedPayoutTxHex, updatedMultisigHex, paymentAccountKey, sellerAccountAgeWitness); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private PaymentSentMessage(String tradeId, NodeAddress senderNodeAddress, @Nullable String counterCurrencyTxId, @Nullable String counterCurrencyExtraData, String uid, String messageVersion, @Nullable String signedPayoutTxHex, @Nullable String updatedMultisigHex, @Nullable byte[] paymentAccountKey, AccountAgeWitness sellerAccountAgeWitness) { super(messageVersion, tradeId, uid); this.senderNodeAddress = senderNodeAddress; this.counterCurrencyTxId = counterCurrencyTxId; this.counterCurrencyExtraData = counterCurrencyExtraData; this.payoutTxHex = signedPayoutTxHex; this.updatedMultisigHex = updatedMultisigHex; this.paymentAccountKey = paymentAccountKey; this.sellerAccountAgeWitness = sellerAccountAgeWitness; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { final protobuf.PaymentSentMessage.Builder builder = protobuf.PaymentSentMessage.newBuilder(); builder.setTradeId(offerId) .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setUid(uid); Optional.ofNullable(counterCurrencyTxId).ifPresent(e -> builder.setCounterCurrencyTxId(counterCurrencyTxId)); Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData)); Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex)); Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex)); Optional.ofNullable(paymentAccountKey).ifPresent(e -> builder.setPaymentAccountKey(ByteString.copyFrom(e))); Optional.ofNullable(buyerSignature).ifPresent(e -> builder.setBuyerSignature(ByteString.copyFrom(e))); Optional.ofNullable(sellerAccountAgeWitness).ifPresent(e -> builder.setSellerAccountAgeWitness(sellerAccountAgeWitness.toProtoAccountAgeWitness())); return getNetworkEnvelopeBuilder().setPaymentSentMessage(builder).build(); } public static PaymentSentMessage fromProto(protobuf.PaymentSentMessage proto, String messageVersion) { protobuf.AccountAgeWitness protoAccountAgeWitness = proto.getSellerAccountAgeWitness(); AccountAgeWitness accountAgeWitness = protoAccountAgeWitness.getHash().isEmpty() ? null : AccountAgeWitness.fromProto(protoAccountAgeWitness); PaymentSentMessage message = new PaymentSentMessage(proto.getTradeId(), NodeAddress.fromProto(proto.getSenderNodeAddress()), ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyTxId()), ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData()), proto.getUid(), messageVersion, ProtoUtil.stringOrNullFromProto(proto.getPayoutTxHex()), ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()), ProtoUtil.byteArrayOrNullFromProto(proto.getPaymentAccountKey()), accountAgeWitness ); message.setBuyerSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getBuyerSignature())); return message; } @Override public String toString() { return "PaymentSentMessage{" + ",\n tradeId=" + offerId + ",\n uid='" + uid + '\'' + ",\n senderNodeAddress=" + senderNodeAddress + ",\n counterCurrencyTxId=" + counterCurrencyTxId + ",\n counterCurrencyExtraData=" + counterCurrencyExtraData + ",\n payoutTxHex=" + payoutTxHex + ",\n updatedMultisigHex=" + updatedMultisigHex + ",\n paymentAccountKey=" + paymentAccountKey + ",\n buyerSignature=" + buyerSignature + "\n} " + super.toString(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/messages/SignContractRequest.java ================================================ /* * 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 . */ package haveno.core.trade.messages; import com.google.protobuf.ByteString; import haveno.common.proto.ProtoUtil; import haveno.common.util.Utilities; import haveno.core.proto.CoreProtoResolver; import haveno.network.p2p.DirectMessage; import lombok.EqualsAndHashCode; import lombok.Value; import javax.annotation.Nullable; import java.util.Optional; @EqualsAndHashCode(callSuper = true) @Value public final class SignContractRequest extends TradeMessage implements DirectMessage { private final long currentDate; private final String accountId; private final byte[] paymentAccountPayloadHash; private final String payoutAddress; @Nullable private final String depositTxHash; @Nullable private final byte[] accountAgeWitnessSignatureOfDepositHash; public SignContractRequest(String tradeId, String uid, String messageVersion, long currentDate, String accountId, byte[] paymentAccountPayloadHash, String payoutAddress, @Nullable String depositTxHash, @Nullable byte[] accountAgeWitnessSignatureOfDepositHash) { super(messageVersion, tradeId, uid); this.currentDate = currentDate; this.accountId = accountId; this.paymentAccountPayloadHash = paymentAccountPayloadHash; this.payoutAddress = payoutAddress; this.depositTxHash = depositTxHash; this.accountAgeWitnessSignatureOfDepositHash = accountAgeWitnessSignatureOfDepositHash; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { protobuf.SignContractRequest.Builder builder = protobuf.SignContractRequest.newBuilder() .setTradeId(offerId) .setUid(uid) .setAccountId(accountId) .setPaymentAccountPayloadHash(ByteString.copyFrom(paymentAccountPayloadHash)) .setPayoutAddress(payoutAddress); Optional.ofNullable(accountAgeWitnessSignatureOfDepositHash).ifPresent(e -> builder.setAccountAgeWitnessSignatureOfDepositHash(ByteString.copyFrom(e))); Optional.ofNullable(depositTxHash).ifPresent(builder::setDepositTxHash); builder.setCurrentDate(currentDate); return getNetworkEnvelopeBuilder().setSignContractRequest(builder).build(); } public static SignContractRequest fromProto(protobuf.SignContractRequest proto, CoreProtoResolver coreProtoResolver, String messageVersion) { return new SignContractRequest(proto.getTradeId(), proto.getUid(), messageVersion, proto.getCurrentDate(), proto.getAccountId(), proto.getPaymentAccountPayloadHash().toByteArray(), proto.getPayoutAddress(), ProtoUtil.stringOrNullFromProto(proto.getDepositTxHash()), ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessSignatureOfDepositHash())); } @Override public String toString() { return "SignContractRequest {" + ",\n currentDate=" + currentDate + ",\n accountId=" + accountId + ",\n paymentAccountPayloadHash='" + Utilities.bytesAsHexString(paymentAccountPayloadHash) + ",\n payoutAddress='" + payoutAddress + ",\n depositTxHash='" + depositTxHash + ",\n accountAgeWitnessSignatureOfDepositHash='" + Utilities.bytesAsHexString(accountAgeWitnessSignatureOfDepositHash) + "\n} " + super.toString(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/messages/SignContractResponse.java ================================================ /* * 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 . */ package haveno.core.trade.messages; import com.google.protobuf.ByteString; import haveno.common.proto.ProtoUtil; import haveno.common.util.Utilities; import haveno.core.proto.CoreProtoResolver; import haveno.network.p2p.DirectMessage; import lombok.EqualsAndHashCode; import lombok.Value; import javax.annotation.Nullable; import java.util.Optional; @EqualsAndHashCode(callSuper = true) @Value public final class SignContractResponse extends TradeMessage implements DirectMessage { private final long currentDate; private final String contractAsJson; private final byte[] contractSignature; private final byte[] encryptedPaymentAccountPayload; public SignContractResponse(String tradeId, String uid, String messageVersion, long currentDate, String contractAsJson, byte[] contractSignature, @Nullable byte[] encryptedPaymentAccountPayload) { super(messageVersion, tradeId, uid); this.currentDate = currentDate; this.contractAsJson = contractAsJson; this.contractSignature = contractSignature; this.encryptedPaymentAccountPayload = encryptedPaymentAccountPayload; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { protobuf.SignContractResponse.Builder builder = protobuf.SignContractResponse.newBuilder() .setTradeId(offerId) .setUid(uid); Optional.ofNullable(contractAsJson).ifPresent(e -> builder.setContractAsJson(contractAsJson)); Optional.ofNullable(contractSignature).ifPresent(e -> builder.setContractSignature(ByteString.copyFrom(e))); Optional.ofNullable(encryptedPaymentAccountPayload).ifPresent(e -> builder.setEncryptedPaymentAccountPayload(ByteString.copyFrom(e))); builder.setCurrentDate(currentDate); return getNetworkEnvelopeBuilder().setSignContractResponse(builder).build(); } public static SignContractResponse fromProto(protobuf.SignContractResponse proto, CoreProtoResolver coreProtoResolver, String messageVersion) { return new SignContractResponse(proto.getTradeId(), proto.getUid(), messageVersion, proto.getCurrentDate(), ProtoUtil.stringOrNullFromProto(proto.getContractAsJson()), ProtoUtil.byteArrayOrNullFromProto(proto.getContractSignature()), proto.getEncryptedPaymentAccountPayload().toByteArray()); } @Override public String toString() { return "SignContractResponse {" + ",\n currentDate=" + currentDate + ",\n contractAsJson='" + contractAsJson + ",\n contractSignature='" + Utilities.bytesAsHexString(contractSignature) + "\n} " + super.toString(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/messages/TradeMailboxMessage.java ================================================ /* * 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 . */ package haveno.core.trade.messages; import haveno.network.p2p.mailbox.MailboxMessage; import lombok.EqualsAndHashCode; import lombok.ToString; import java.util.concurrent.TimeUnit; @EqualsAndHashCode(callSuper = true) @ToString public abstract class TradeMailboxMessage extends TradeMessage implements MailboxMessage { public static final long TTL = TimeUnit.DAYS.toMillis(15); protected TradeMailboxMessage(String messageVersion, String tradeId, String uid) { super(messageVersion, tradeId, uid); } @Override public long getTTL() { return TTL; } } ================================================ FILE: core/src/main/java/haveno/core/trade/messages/TradeMessage.java ================================================ /* * 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 . */ package haveno.core.trade.messages; import haveno.common.proto.network.NetworkEnvelope; import haveno.network.p2p.UidMessage; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; @EqualsAndHashCode(callSuper = true) @Getter @ToString public abstract class TradeMessage extends NetworkEnvelope implements UidMessage { protected final String offerId; protected final String uid; protected TradeMessage(String messageVersion, String offerId, String uid) { super(messageVersion); this.offerId = offerId; this.uid = uid; } } ================================================ FILE: core/src/main/java/haveno/core/trade/messages/TradeProtocolVersion.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.messages; import haveno.common.proto.ProtoUtil; public enum TradeProtocolVersion { MULTISIG_2_3; public static TradeProtocolVersion fromProto( protobuf.TradeProtocolVersion tradeProtocolVersion) { return ProtoUtil.enumFromProto(TradeProtocolVersion.class, tradeProtocolVersion.name()); } public static protobuf.TradeProtocolVersion toProtoMessage(TradeProtocolVersion tradeProtocolVersion) { return protobuf.TradeProtocolVersion.valueOf(tradeProtocolVersion.name()); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/ArbitratorProtocol.java ================================================ package haveno.core.trade.protocol; import haveno.common.ThreadUtils; import haveno.common.handlers.ErrorMessageHandler; import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.Trade; import haveno.core.trade.messages.DepositRequest; import haveno.core.trade.messages.DepositResponse; import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.messages.SignContractResponse; import haveno.core.trade.messages.TradeMessage; import haveno.core.trade.protocol.FluentProtocol.Condition; import haveno.core.trade.protocol.tasks.ApplyFilter; import haveno.core.trade.protocol.tasks.ArbitratorProcessDepositRequest; import haveno.core.trade.protocol.tasks.ArbitratorProcessReserveTx; import haveno.core.trade.protocol.tasks.ArbitratorSendDisputeOpenedMessageToBuyer; import haveno.core.trade.protocol.tasks.ArbitratorSendDisputeOpenedMessageToSeller; import haveno.core.trade.protocol.tasks.ArbitratorSendInitTradeOrMultisigRequests; import haveno.core.trade.protocol.tasks.ProcessInitTradeRequest; import haveno.core.trade.protocol.tasks.SendDepositsConfirmedMessageToBuyer; import haveno.core.trade.protocol.tasks.SendDepositsConfirmedMessageToSeller; import haveno.core.trade.protocol.tasks.TradeTask; import haveno.core.util.Validator; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; @Slf4j public class ArbitratorProtocol extends DisputeProtocol { public ArbitratorProtocol(ArbitratorTrade trade) { super(trade); } @Override protected void onTradeMessage(TradeMessage message, NodeAddress peer) { super.onTradeMessage(message, peer); } @Override public void onMailboxMessage(TradeMessage message, NodeAddress peer) { super.onMailboxMessage(message, peer); } @Override protected void onInitialized() { super.onInitialized(); // re-send dispute opened message if applicable sendDisputeOpenedMessageIfApplicable(); // TODO: resend dispute closed message if not acked } public void sendDisputeOpenedMessageIfApplicable() { ThreadUtils.execute(() -> { if (!needsToResendDisputeOpenedMessage()) return; if (trade.isShutDownStarted() || trade.isPayoutPublished()) return; synchronized (trade.getLock()) { if (!needsToResendDisputeOpenedMessage()) return; latchTrade(); given(new Condition(trade)) .setup(tasks( ArbitratorSendDisputeOpenedMessageToBuyer.class, ArbitratorSendDisputeOpenedMessageToSeller.class) .using(new TradeTaskRunner(trade, () -> { unlatchTrade(); }, (errorMessage) -> { log.warn("Error sending DisputeOpenedMessage: " + errorMessage); unlatchTrade(); }))) .executeTasks(); awaitTradeLatch(); } }, trade.getId()); } private boolean needsToResendDisputeOpenedMessage() { if (trade.isShutDownStarted()) return false; if (trade.isPayoutPublished()) return false; if (trade.getBuyer().getDisputeOpenedMessage() == null && trade.getSeller().getDisputeOpenedMessage() == null) return false; if (trade.getDisputeState() != Trade.DisputeState.DISPUTE_OPENED) return false; if (!((ArbitratorTrade) trade).resendDisputeOpenedMessageWithinDuration()) return false; return !trade.getProcessModel().isDisputeOpenedMessageAckedOrNacked(); } /////////////////////////////////////////////////////////////////////////////////////////// // Incoming messages /////////////////////////////////////////////////////////////////////////////////////////// public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) { log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); this.errorMessageHandler = errorMessageHandler; processModel.setTradeMessage(message); // TODO (woodser): confirm these are null without being set expect(phase(Trade.Phase.INIT) .with(message) .from(peer)) .setup(tasks( ApplyFilter.class, ProcessInitTradeRequest.class, ArbitratorProcessReserveTx.class, ArbitratorSendInitTradeOrMultisigRequests.class) .using(new TradeTaskRunner(trade, () -> { startTimeout(); handleTaskRunnerSuccess(peer, message); }, errorMessage -> { handleTaskRunnerFault(peer, message, errorMessage); })) .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) .executeTasks(true); awaitTradeLatch(); } }, trade.getId()); } @Override public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) { log.warn("Arbitrator ignoring SignContractResponse"); } public void handleDepositRequest(DepositRequest request, NodeAddress sender) { log.info(TradeProtocol.LOG_HIGHLIGHT + "handleDepositRequest() for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); Validator.checkTradeId(processModel.getOfferId(), request); processModel.setTradeMessage(request); expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_REQUESTED) .with(request) .from(sender)) .setup(tasks( ArbitratorProcessDepositRequest.class) .using(new TradeTaskRunner(trade, () -> { if (trade.getState().ordinal() >= Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS.ordinal()) { stopTimeout(); this.errorMessageHandler = null; } handleTaskRunnerSuccess(sender, request); }, errorMessage -> { handleTaskRunnerFault(sender, request, errorMessage); }))) .executeTasks(true); awaitTradeLatch(); } }, trade.getId()); } @Override public void handleDepositResponse(DepositResponse response, NodeAddress sender) { log.warn("Arbitrator ignoring DepositResponse for trade " + response.getOfferId()); } @SuppressWarnings("unchecked") @Override public Class[] getDepositsConfirmedTasks() { return new Class[] { SendDepositsConfirmedMessageToBuyer.class, SendDepositsConfirmedMessageToSeller.class }; } @Override public void handleError(String errorMessage) { // set trade state to send deposit responses with nack if (trade instanceof ArbitratorTrade && trade.getState() == Trade.State.SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST) { trade.setStateIfValidTransitionTo(Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED); } super.handleError(errorMessage); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java ================================================ /* e * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol; import haveno.common.ThreadUtils; import haveno.common.handlers.ErrorMessageHandler; import haveno.core.trade.BuyerAsMakerTrade; import haveno.core.trade.Trade; import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.protocol.tasks.ApplyFilter; import haveno.core.trade.protocol.tasks.MakerSendInitTradeRequestToArbitrator; import haveno.core.trade.protocol.tasks.ProcessInitTradeRequest; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; @Slf4j public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol { /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public BuyerAsMakerProtocol(BuyerAsMakerTrade trade) { super(trade); } @Override public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) { log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {} from {}", trade.getClass().getSimpleName(), trade.getShortId(), peer); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); this.errorMessageHandler = errorMessageHandler; expect(phase(Trade.Phase.INIT) .with(message) .from(peer)) .setup(tasks( ApplyFilter.class, ProcessInitTradeRequest.class, MakerSendInitTradeRequestToArbitrator.class) .using(new TradeTaskRunner(trade, () -> { startTimeout(); handleTaskRunnerSuccess(peer, message); }, errorMessage -> { handleTaskRunnerFault(peer, message, errorMessage); })) .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) .executeTasks(true); awaitTradeLatch(); } }, trade.getId()); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/BuyerAsTakerProtocol.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol; import haveno.common.ThreadUtils; import haveno.common.handlers.ErrorMessageHandler; import haveno.core.trade.BuyerAsTakerTrade; import haveno.core.trade.Trade; import haveno.core.trade.handlers.TradeResultHandler; import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.protocol.tasks.ApplyFilter; import haveno.core.trade.protocol.tasks.ProcessInitTradeRequest; import haveno.core.trade.protocol.tasks.TakerReserveTradeFunds; import haveno.core.trade.protocol.tasks.TakerSendInitTradeRequestToArbitrator; import haveno.core.trade.protocol.tasks.TakerSendInitTradeRequestToMaker; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; @Slf4j public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol { /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public BuyerAsTakerProtocol(BuyerAsTakerTrade trade) { super(trade); } /////////////////////////////////////////////////////////////////////////////////////////// // Take offer /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onTakeOffer(TradeResultHandler tradeResultHandler, ErrorMessageHandler errorMessageHandler) { log.info(TradeProtocol.LOG_HIGHLIGHT + "onTakerOffer for {} {}", getClass().getSimpleName(), trade.getShortId()); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); this.tradeResultHandler = tradeResultHandler; this.errorMessageHandler = errorMessageHandler; expect(phase(Trade.Phase.INIT) .with(TakerEvent.TAKE_OFFER) .from(trade.getTradePeer().getNodeAddress())) .setup(tasks( ApplyFilter.class, TakerReserveTradeFunds.class, TakerSendInitTradeRequestToMaker.class) .using(new TradeTaskRunner(trade, () -> { startTimeout(); unlatchTrade(); }, errorMessage -> { handleError(errorMessage); })) .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) .executeTasks(true); awaitTradeLatch(); } }, trade.getId()); } @Override public void handleInitTradeRequest(InitTradeRequest message, NodeAddress sender) { log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {} from {}", trade.getClass().getSimpleName(), trade.getShortId(), sender); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { // ignore if assigned a different arbitrator NodeAddress nodeAddress = trade.getArbitrator().getNodeAddress(); if (nodeAddress != null && !nodeAddress.equals(sender)) { log.warn("Ignoring InitTradeRequest because sender is not the arbitrator for the trade, tradeId={}, sender={}", message.getOfferId(), sender); return; } latchTrade(); expect(phase(Trade.Phase.INIT) .with(message) .from(sender)) .setup(tasks( ApplyFilter.class, ProcessInitTradeRequest.class, TakerSendInitTradeRequestToArbitrator.class) .using(new TradeTaskRunner(trade, () -> { startTimeout(); handleTaskRunnerSuccess(sender, message); }, errorMessage -> { handleTaskRunnerFault(sender, message, errorMessage); })) .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) .executeTasks(true); awaitTradeLatch(); } }, trade.getId()); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol; import haveno.common.ThreadUtils; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.core.trade.BuyerTrade; import haveno.core.trade.Trade; import haveno.core.trade.messages.SignContractResponse; import haveno.core.trade.messages.TradeMessage; import haveno.core.trade.protocol.tasks.ApplyFilter; import haveno.core.trade.protocol.tasks.BuyerPreparePaymentSentMessage; import haveno.core.trade.protocol.tasks.BuyerSendPaymentSentMessageToArbitrator; import haveno.core.trade.protocol.tasks.BuyerSendPaymentSentMessageToSeller; import haveno.core.trade.protocol.tasks.SendDepositsConfirmedMessageToArbitrator; import haveno.core.trade.protocol.tasks.SendDepositsConfirmedMessageToSeller; import haveno.core.trade.protocol.tasks.TradeTask; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; @Slf4j public class BuyerProtocol extends DisputeProtocol { enum BuyerEvent implements FluentProtocol.Event { STARTUP, DEPOSIT_TXS_CONFIRMED, PAYMENT_SENT } /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public BuyerProtocol(BuyerTrade trade) { super(trade); } @Override protected void onInitializeAfterMailboxMessages() { super.onInitializeAfterMailboxMessages(); maybeResendPaymentSentMessage(); } private void maybeResendPaymentSentMessage() { // re-send payment sent message if not acked if (trade.isShutDownStarted() || trade.isPayoutPublished()) return; ThreadUtils.execute(() -> { synchronized (trade.getLock()) { if (trade.isShutDownStarted() || trade.isPayoutPublished()) return; if (trade.getState().ordinal() >= Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal() && trade.getState().ordinal() < Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG.ordinal()) { latchTrade(); given(anyPhase(Trade.Phase.PAYMENT_SENT) .with(BuyerEvent.STARTUP)) .setup(tasks( BuyerSendPaymentSentMessageToSeller.class, BuyerSendPaymentSentMessageToArbitrator.class) .using(new TradeTaskRunner(trade, () -> { unlatchTrade(); }, (errorMessage) -> { log.warn("Error sending PaymentSentMessage on startup: " + errorMessage); unlatchTrade(); }))) .executeTasks(); awaitTradeLatch(); } } }, trade.getId()); } @Override protected void onTradeMessage(TradeMessage message, NodeAddress peer) { super.onTradeMessage(message, peer); } @Override public void onMailboxMessage(TradeMessage message, NodeAddress peer) { super.onMailboxMessage(message, peer); } @Override public void handleSignContractResponse(SignContractResponse response, NodeAddress sender) { super.handleSignContractResponse(response, sender); } /////////////////////////////////////////////////////////////////////////////////////////// // User interaction /////////////////////////////////////////////////////////////////////////////////////////// public void onPaymentSent(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { log.info(TradeProtocol.LOG_HIGHLIGHT + "BuyerProtocol.onPaymentSent() for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); // advance trade state if (trade.isDepositsUnlocked() || trade.isDepositsFinalized() || trade.isPaymentSent()) { trade.setStateIfValidTransitionTo(Trade.State.BUYER_CONFIRMED_PAYMENT_SENT); } else { errorMessageHandler.handleErrorMessage("Cannot confirm payment sent for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " in state " + trade.getState()); return; } // process on trade thread ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); this.errorMessageHandler = errorMessageHandler; BuyerEvent event = BuyerEvent.PAYMENT_SENT; try { expect(anyPhase(Trade.Phase.DEPOSITS_UNLOCKED, Trade.Phase.DEPOSITS_FINALIZED, Trade.Phase.PAYMENT_SENT) .with(event) .preCondition(trade.confirmPermitted())) .setup(tasks(ApplyFilter.class, BuyerPreparePaymentSentMessage.class, BuyerSendPaymentSentMessageToSeller.class, BuyerSendPaymentSentMessageToArbitrator.class) .using(new TradeTaskRunner(trade, () -> { stopTimeout(); this.errorMessageHandler = null; resultHandler.handleResult(); handleTaskRunnerSuccess(event); }, (errorMessage) -> { log.warn("Error confirming payment sent, reverting state to {}, error={}", Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN, errorMessage); trade.setState(Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN); handleTaskRunnerFault(event, errorMessage); }))) .executeTasks(true); } catch (Exception e) { errorMessageHandler.handleErrorMessage("Error confirming payment sent: " + e.getMessage()); unlatchTrade(); } awaitTradeLatch(); } }, trade.getId()); } @SuppressWarnings("unchecked") @Override public Class[] getDepositsConfirmedTasks() { return new Class[] { SendDepositsConfirmedMessageToSeller.class, SendDepositsConfirmedMessageToArbitrator.class }; } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/DisputeProtocol.java ================================================ /* * 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 . */ package haveno.core.trade.protocol; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.core.trade.Trade; import haveno.core.trade.messages.MediatedPayoutTxPublishedMessage; import haveno.core.trade.messages.MediatedPayoutTxSignatureMessage; import haveno.core.trade.messages.TradeMessage; import haveno.core.trade.protocol.tasks.ApplyFilter; import haveno.core.trade.protocol.tasks.mediation.FinalizeMediatedPayoutTx; import haveno.core.trade.protocol.tasks.mediation.ProcessMediatedPayoutSignatureMessage; import haveno.core.trade.protocol.tasks.mediation.ProcessMediatedPayoutTxPublishedMessage; import haveno.core.trade.protocol.tasks.mediation.SendMediatedPayoutSignatureMessage; import haveno.core.trade.protocol.tasks.mediation.SendMediatedPayoutTxPublishedMessage; import haveno.core.trade.protocol.tasks.mediation.SetupMediatedPayoutTxListener; import haveno.core.trade.protocol.tasks.mediation.SignMediatedPayoutTx; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; @Slf4j public abstract class DisputeProtocol extends TradeProtocol { enum DisputeEvent implements FluentProtocol.Event { MEDIATION_RESULT_ACCEPTED, MEDIATION_RESULT_REJECTED, ARBITRATION_REQUESTED } public DisputeProtocol(Trade trade) { super(trade); } /////////////////////////////////////////////////////////////////////////////////////////// // Dispatcher /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void onTradeMessage(TradeMessage message, NodeAddress peer) { super.onTradeMessage(message, peer); if (message instanceof MediatedPayoutTxSignatureMessage) { handle((MediatedPayoutTxSignatureMessage) message, peer); } else if (message instanceof MediatedPayoutTxPublishedMessage) { handle((MediatedPayoutTxPublishedMessage) message, peer); } } @Override protected void onMailboxMessage(TradeMessage message, NodeAddress peer) { super.onMailboxMessage(message, peer); if (message instanceof MediatedPayoutTxSignatureMessage) { handle((MediatedPayoutTxSignatureMessage) message, peer); } else if (message instanceof MediatedPayoutTxPublishedMessage) { handle((MediatedPayoutTxPublishedMessage) message, peer); } } /////////////////////////////////////////////////////////////////////////////////////////// // User interaction: Trader accepts mediation result /////////////////////////////////////////////////////////////////////////////////////////// // Trader has not yet received the peer's signature but has clicked the accept button. public void onAcceptMediationResult(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { DisputeEvent event = DisputeEvent.MEDIATION_RESULT_ACCEPTED; expect(anyPhase( Trade.Phase.DEPOSITS_UNLOCKED, Trade.Phase.DEPOSITS_FINALIZED, Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED) .with(event) .preCondition(trade.getTradePeer().getMediatedPayoutTxSignature() == null, () -> errorMessageHandler.handleErrorMessage("We have received already the signature from the peer.")) .preCondition(trade.getPayoutTx() == null, () -> errorMessageHandler.handleErrorMessage("Payout tx is already published."))) .setup(tasks(ApplyFilter.class, SignMediatedPayoutTx.class, SendMediatedPayoutSignatureMessage.class, SetupMediatedPayoutTxListener.class) .using(new TradeTaskRunner(trade, () -> { resultHandler.handleResult(); handleTaskRunnerSuccess(event); }, errorMessage -> { errorMessageHandler.handleErrorMessage(errorMessage); handleTaskRunnerFault(event, errorMessage); }))) .executeTasks(); } // Trader has already received the peer's signature and has clicked the accept button as well. public void onFinalizeMediationResultPayout(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { DisputeEvent event = DisputeEvent.MEDIATION_RESULT_ACCEPTED; expect(anyPhase( Trade.Phase.DEPOSITS_UNLOCKED, Trade.Phase.DEPOSITS_FINALIZED, Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED) .with(event) .preCondition(trade.getPayoutTx() == null, () -> errorMessageHandler.handleErrorMessage("Payout tx is already published."))) .setup(tasks(ApplyFilter.class, SignMediatedPayoutTx.class, FinalizeMediatedPayoutTx.class, SendMediatedPayoutTxPublishedMessage.class) .using(new TradeTaskRunner(trade, () -> { resultHandler.handleResult(); handleTaskRunnerSuccess(event); }, errorMessage -> { errorMessageHandler.handleErrorMessage(errorMessage); handleTaskRunnerFault(event, errorMessage); }))) .executeTasks(); } /////////////////////////////////////////////////////////////////////////////////////////// // Mediation: incoming message /////////////////////////////////////////////////////////////////////////////////////////// protected void handle(MediatedPayoutTxSignatureMessage message, NodeAddress peer) { expect(anyPhase( Trade.Phase.DEPOSITS_UNLOCKED, Trade.Phase.DEPOSITS_FINALIZED, Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED) .with(message) .from(peer)) .setup(tasks(ProcessMediatedPayoutSignatureMessage.class)) .executeTasks(); } protected void handle(MediatedPayoutTxPublishedMessage message, NodeAddress peer) { expect(anyPhase( Trade.Phase.DEPOSITS_UNLOCKED, Trade.Phase.DEPOSITS_FINALIZED, Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED) .with(message) .from(peer)) .setup(tasks(ProcessMediatedPayoutTxPublishedMessage.class)) .executeTasks(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/FluentProtocol.java ================================================ /* * 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 . */ package haveno.core.trade.protocol; import haveno.common.taskrunner.Task; import haveno.core.trade.Trade; import haveno.core.trade.messages.TradeMessage; import haveno.network.p2p.NodeAddress; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.text.MessageFormat; import java.util.HashSet; import java.util.Set; import java.util.function.Consumer; import static com.google.common.base.Preconditions.checkArgument; import static haveno.core.util.Validator.isTradeIdValid; // Main class. Contains the condition and setup, if condition is valid it will execute the // taskRunner and the optional runnable. public class FluentProtocol { interface Event { String name(); } private final TradeProtocol tradeProtocol; private Condition condition; private Setup setup; private Consumer resultHandler; public FluentProtocol(TradeProtocol tradeProtocol) { this.tradeProtocol = tradeProtocol; } protected FluentProtocol condition(Condition condition) { this.condition = condition; return this; } protected FluentProtocol setup(Setup setup) { this.setup = setup; return this; } public FluentProtocol resultHandler(Consumer resultHandler) { this.resultHandler = resultHandler; return this; } // Can be used before or after executeTasks public FluentProtocol run(Runnable runnable) { Condition.Result result = condition.getResult(); if (result.isValid) { runnable.run(); } else if (resultHandler != null) { resultHandler.accept(result); } return this; } public FluentProtocol executeTasks(boolean newThread) { if (newThread) { new Thread(() -> { executeTasks(); }).start(); } else { executeTasks(); } return this; } public FluentProtocol executeTasks() { Condition.Result result = condition.getResult(); if (!result.isValid) { if (resultHandler != null) { resultHandler.accept(result); } return this; } if (setup.getTimeoutSec() > 0) { tradeProtocol.startTimeout(setup.getTimeoutSec()); } NodeAddress peer = condition.getPeer(); if (peer != null) { tradeProtocol.processModel.setTempTradePeerNodeAddress(peer); // TODO (woodser): node has multiple peers (arbitrator and maker or taker), but fluent protocol assumes only one tradeProtocol.processModel.getTradeManager().requestPersistence(); } TradeMessage message = condition.getMessage(); if (message != null) { tradeProtocol.processModel.setTradeMessage(message); tradeProtocol.processModel.getTradeManager().requestPersistence(); } TradeTaskRunner taskRunner = setup.getTaskRunner(peer, message, condition.getEvent()); taskRunner.addTasks(setup.getTasks()); taskRunner.run(); return this; } /////////////////////////////////////////////////////////////////////////////////////////// // Condition class /////////////////////////////////////////////////////////////////////////////////////////// @Slf4j public static class Condition { enum Result { VALID(true), INVALID_PHASE, INVALID_STATE, INVALID_PRE_CONDITION, INVALID_TRADE_ID; @Getter private boolean isValid; @Getter private String info; Result() { } Result(boolean isValid) { this.isValid = isValid; } public Result info(String info) { this.info = info; return this; } } private final Set expectedPhases = new HashSet<>(); private final Set expectedStates = new HashSet<>(); private final Set preConditions = new HashSet<>(); private final Trade trade; @Nullable private Result result; @Nullable @Getter private TradeMessage message; @Nullable @Getter private Event event; @Nullable @Getter private NodeAddress peer; @Nullable private Runnable preConditionFailedHandler; public Condition(Trade trade) { this.trade = trade; } public Condition phase(Trade.Phase expectedPhase) { checkArgument(result == null); this.expectedPhases.add(expectedPhase); return this; } public Condition anyPhase(Trade.Phase... expectedPhases) { checkArgument(result == null); this.expectedPhases.addAll(Set.of(expectedPhases)); return this; } public Condition state(Trade.State state) { checkArgument(result == null); this.expectedStates.add(state); return this; } public Condition anyState(Trade.State... states) { checkArgument(result == null); this.expectedStates.addAll(Set.of(states)); return this; } public Condition with(TradeMessage message) { checkArgument(result == null); this.message = message; return this; } public Condition with(Event event) { checkArgument(result == null); this.event = event; return this; } public Condition from(NodeAddress peer) { checkArgument(result == null); this.peer = peer; return this; } public Condition preCondition(boolean preCondition) { checkArgument(result == null); preConditions.add(preCondition); return this; } public Condition preCondition(boolean preCondition, Runnable conditionFailedHandler) { checkArgument(result == null); preCondition(preCondition); this.preConditionFailedHandler = conditionFailedHandler; return this; } public Result getResult() { if (result == null) { boolean isTradeIdValid = message == null || isTradeIdValid(trade.getId(), message); if (!isTradeIdValid) { String info = MessageFormat.format("TradeId does not match tradeId in message, TradeId={0}, tradeId in message={1}", trade.getId(), message.getOfferId()); result = Result.INVALID_TRADE_ID.info(info); return result; } Result phaseValidationResult = getPhaseResult(); if (!phaseValidationResult.isValid) { result = phaseValidationResult; return result; } Result stateResult = getStateResult(); if (!stateResult.isValid) { result = stateResult; return result; } boolean allPreConditionsMet = preConditions.stream().allMatch(e -> e); if (!allPreConditionsMet) { String info = MessageFormat.format("PreConditions not met. preConditions={0}, this={1}, tradeId={2}", preConditions, this, trade.getId()); result = Result.INVALID_PRE_CONDITION.info(info); if (preConditionFailedHandler != null) { preConditionFailedHandler.run(); } return result; } result = Result.VALID; } return result; } private Result getPhaseResult() { if (expectedPhases.isEmpty()) { return Result.VALID; } boolean isPhaseValid = expectedPhases.stream().anyMatch(e -> e == trade.getPhase()); String trigger = message != null ? message.getClass().getSimpleName() : event != null ? event.name() + " event" : ""; if (isPhaseValid) { String info = MessageFormat.format("We received a {0} at phase {1} and state {2}, tradeId={3}, peer={4}", trigger, trade.getPhase(), trade.getState(), trade.getId(), this.peer); log.info(info); return Result.VALID.info(info); } else { String info = MessageFormat.format("We received a {0} but we are are not in the expected phase.\n" + "Expected phases={1},\nTrade phase={2},\nTrade state= {3},\ntradeId={4}", trigger, expectedPhases, trade.getPhase(), trade.getState(), trade.getId()); return Result.INVALID_PHASE.info(info); } } private Result getStateResult() { if (expectedStates.isEmpty()) { return Result.VALID; } boolean isStateValid = expectedStates.stream().anyMatch(e -> e == trade.getState()); String trigger = message != null ? message.getClass().getSimpleName() : event != null ? event.name() + " event" : ""; if (isStateValid) { String info = MessageFormat.format("We received a {0} at state {1}, tradeId={2}", trigger, trade.getState(), trade.getId()); log.info(info); return Result.VALID.info(info); } else { String info = MessageFormat.format("We received a {0} but we are not in the expected state. " + "Expected states={1}, Trade state= {2}, tradeId={3}", trigger, expectedStates, trade.getState(), trade.getId()); return Result.INVALID_STATE.info(info); } } } /////////////////////////////////////////////////////////////////////////////////////////// // Setup class /////////////////////////////////////////////////////////////////////////////////////////// @Slf4j public static class Setup { private final TradeProtocol tradeProtocol; private final Trade trade; @Getter private Class>[] tasks; @Getter private int timeoutSec; @Nullable private TradeTaskRunner taskRunner; public Setup(TradeProtocol tradeProtocol, Trade trade) { this.tradeProtocol = tradeProtocol; this.trade = trade; } @SafeVarargs public final Setup tasks(Class>... tasks) { this.tasks = tasks; return this; } public Setup withTimeout(int timeoutSec) { this.timeoutSec = timeoutSec; return this; } public Setup using(TradeTaskRunner taskRunner) { this.taskRunner = taskRunner; return this; } public TradeTaskRunner getTaskRunner(NodeAddress sender, @Nullable TradeMessage message, @Nullable Event event) { if (taskRunner == null) { if (message != null) { taskRunner = new TradeTaskRunner(trade, () -> tradeProtocol.handleTaskRunnerSuccess(sender, message), errorMessage -> tradeProtocol.handleTaskRunnerFault(sender, message, errorMessage)); } else if (event != null) { taskRunner = new TradeTaskRunner(trade, () -> tradeProtocol.handleTaskRunnerSuccess(event), errorMessage -> tradeProtocol.handleTaskRunnerFault(event, errorMessage)); } else { throw new IllegalStateException("addTasks must not be called without message or event " + "set in case no taskRunner has been created yet"); } } return taskRunner; } } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/MakerProtocol.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol; import haveno.common.handlers.ErrorMessageHandler; import haveno.core.trade.messages.InitTradeRequest; import haveno.network.p2p.NodeAddress; public interface MakerProtocol extends TraderProtocol { void handleInitTradeRequest(InitTradeRequest message, NodeAddress taker, ErrorMessageHandler errorMessageHandler); } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/ProcessModel.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol; import com.google.protobuf.ByteString; import haveno.common.crypto.KeyRing; import haveno.common.crypto.PubKeyRing; import haveno.common.proto.ProtoUtil; import haveno.common.proto.persistable.PersistablePayload; import haveno.common.taskrunner.Model; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.filter.FilterManager; import haveno.core.network.MessageState; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.proto.CoreProtoResolver; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.support.dispute.mediation.mediator.MediatorManager; import haveno.core.support.dispute.refund.refundagent.RefundAgentManager; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.trade.messages.TradeMessage; import haveno.core.trade.statistics.ReferralIdService; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.User; import haveno.core.xmr.wallet.BtcWalletService; import haveno.core.xmr.wallet.TradeWalletService; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.AckMessage; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import monero.wallet.model.MoneroTxWallet; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; import javax.annotation.Nullable; import java.util.Arrays; import java.util.List; import java.util.Optional; // Fields marked as transient are only used during protocol execution which are based on directMessages so we do not // persist them. /** * This is the base model for the trade protocol. It is persisted with the trade (non transient fields). * It uses the {@link ProcessModelServiceProvider} for access to domain services. */ @Getter @Slf4j public class ProcessModel implements Model, PersistablePayload { // Transient/Immutable (net set in constructor so they are not final, but at init) transient private ProcessModelServiceProvider provider; transient private TradeManager tradeManager; transient private Offer offer; transient public Throwable error; // Added in v1.4.0 // MessageState of the last message sent from the seller to the buyer in the take offer process. // It is used only in a task which would not be executed after restart, so no need to persist it. @Setter transient private ObjectProperty depositTxMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); @Setter @Getter transient private Transaction depositTx; // TODO (woodser): remove and rename depositTxBtc with depositTx // Persistable Immutable (private setter only used by PB method) private TradePeer maker = new TradePeer(); private TradePeer taker = new TradePeer(); private TradePeer arbitrator = new TradePeer(); private String offerId; private String accountId; private PubKeyRing pubKeyRing; // Persistable Mutable @Nullable @Setter private byte[] payoutTxSignature; @Nullable @Setter private byte[] preparedDepositTx; // TODO: remove this unused field @Setter private boolean useSavingsWallet; @Setter private long fundsNeededForTrade; // that is used to store temp. the peers address when we get an incoming message before the message is verified. // After successful verified we copy that over to the trade.tradePeerAddress @Nullable @Setter private NodeAddress tempTradePeerNodeAddress; // Added in v.1.1.6 @Nullable @Setter private byte[] mediatedPayoutTxSignature; @Setter private long buyerPayoutAmountFromMediation; @Setter private long sellerPayoutAmountFromMediation; // Added for XMR integration @Setter transient private TradeMessage tradeMessage; @Getter @Setter private byte[] makerSignature; @Nullable @Getter @Setter transient private MoneroTxWallet reserveTx; @Getter @Setter transient private MoneroTxWallet unsignedPayoutTx; @Nullable @Getter @Setter private String tradeFeeAddress; @Getter @Setter private String multisigAddress; @Getter @Setter private long tradeProtocolErrorHeight; @Getter @Setter private boolean importMultisigHexScheduled; @Getter @Setter private boolean paymentSentPayoutTxStale; private ObjectProperty paymentAccountDecryptedProperty = new SimpleObjectProperty<>(false); @Deprecated private ObjectProperty paymentSentMessageStatePropertySeller = new SimpleObjectProperty<>(MessageState.UNDEFINED); @Deprecated private ObjectProperty paymentSentMessageStatePropertyArbitrator = new SimpleObjectProperty<>(MessageState.UNDEFINED); public ProcessModel(String offerId, String accountId, PubKeyRing pubKeyRing) { this(offerId, accountId, pubKeyRing, new TradePeer(), new TradePeer(), new TradePeer()); } public ProcessModel(String offerId, String accountId, PubKeyRing pubKeyRing, TradePeer arbitrator, TradePeer maker, TradePeer taker) { this.offerId = offerId; this.accountId = accountId; this.pubKeyRing = pubKeyRing; // If tradePeer was null in persisted data from some error cases we set a new one to not cause nullPointers this.arbitrator = arbitrator != null ? arbitrator : new TradePeer(); this.maker = maker != null ? maker : new TradePeer(); this.taker = taker != null ? taker : new TradePeer(); } public void applyTransient(ProcessModelServiceProvider provider, TradeManager tradeManager, Offer offer) { this.offer = offer; this.provider = provider; this.tradeManager = tradeManager; for (TradePeer peer : getTradePeers()) { peer.applyTransient(tradeManager); } // migrate deprecated fields to new model for v1.0.19 if (paymentSentMessageStatePropertySeller.get() != MessageState.UNDEFINED && getSeller().getPaymentSentMessageStateProperty().get() == MessageState.UNDEFINED) { getSeller().getPaymentSentMessageStateProperty().set(paymentSentMessageStatePropertySeller.get()); tradeManager.requestPersistence(); } if (paymentSentMessageStatePropertyArbitrator.get() != MessageState.UNDEFINED && getArbitrator().getPaymentSentMessageStateProperty().get() == MessageState.UNDEFINED) { getArbitrator().getPaymentSentMessageStateProperty().set(paymentSentMessageStatePropertyArbitrator.get()); tradeManager.requestPersistence(); } } private List getTradePeers() { return Arrays.asList(maker, taker, arbitrator); } private TradePeer getBuyer() { return offer.getDirection() == OfferDirection.BUY ? maker : taker; } private TradePeer getSeller() { return offer.getDirection() == OfferDirection.BUY ? taker : maker; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.ProcessModel toProtoMessage() { protobuf.ProcessModel.Builder builder = protobuf.ProcessModel.newBuilder() .setOfferId(offerId) .setAccountId(accountId) .setPubKeyRing(pubKeyRing.toProtoMessage()) .setUseSavingsWallet(useSavingsWallet) .setFundsNeededForTrade(fundsNeededForTrade) .setPaymentSentMessageStateSeller(paymentSentMessageStatePropertySeller.get().name()) .setPaymentSentMessageStateArbitrator(paymentSentMessageStatePropertyArbitrator.get().name()) .setBuyerPayoutAmountFromMediation(buyerPayoutAmountFromMediation) .setSellerPayoutAmountFromMediation(sellerPayoutAmountFromMediation) .setTradeProtocolErrorHeight(tradeProtocolErrorHeight) .setImportMultisigHexScheduled(importMultisigHexScheduled) .setPaymentSentPayoutTxStale(paymentSentPayoutTxStale); Optional.ofNullable(maker).ifPresent(e -> builder.setMaker((protobuf.TradePeer) maker.toProtoMessage())); Optional.ofNullable(taker).ifPresent(e -> builder.setTaker((protobuf.TradePeer) taker.toProtoMessage())); Optional.ofNullable(arbitrator).ifPresent(e -> builder.setArbitrator((protobuf.TradePeer) arbitrator.toProtoMessage())); Optional.ofNullable(payoutTxSignature).ifPresent(e -> builder.setPayoutTxSignature(ByteString.copyFrom(payoutTxSignature))); Optional.ofNullable(tempTradePeerNodeAddress).ifPresent(e -> builder.setTempTradePeerNodeAddress(tempTradePeerNodeAddress.toProtoMessage())); Optional.ofNullable(mediatedPayoutTxSignature).ifPresent(e -> builder.setMediatedPayoutTxSignature(ByteString.copyFrom(e))); Optional.ofNullable(makerSignature).ifPresent(e -> builder.setMakerSignature(ByteString.copyFrom(e))); Optional.ofNullable(tradeFeeAddress).ifPresent(e -> builder.setTradeFeeAddress(tradeFeeAddress)); Optional.ofNullable(multisigAddress).ifPresent(e -> builder.setMultisigAddress(multisigAddress)); return builder.build(); } public static ProcessModel fromProto(protobuf.ProcessModel proto, CoreProtoResolver coreProtoResolver) { TradePeer arbitrator = TradePeer.fromProto(proto.getArbitrator(), coreProtoResolver); TradePeer maker = TradePeer.fromProto(proto.getMaker(), coreProtoResolver); TradePeer taker = TradePeer.fromProto(proto.getTaker(), coreProtoResolver); PubKeyRing pubKeyRing = PubKeyRing.fromProto(proto.getPubKeyRing()); ProcessModel processModel = new ProcessModel(proto.getOfferId(), proto.getAccountId(), pubKeyRing, arbitrator, maker, taker); processModel.setUseSavingsWallet(proto.getUseSavingsWallet()); processModel.setFundsNeededForTrade(proto.getFundsNeededForTrade()); processModel.setBuyerPayoutAmountFromMediation(proto.getBuyerPayoutAmountFromMediation()); processModel.setSellerPayoutAmountFromMediation(proto.getSellerPayoutAmountFromMediation()); processModel.setTradeProtocolErrorHeight(proto.getTradeProtocolErrorHeight()); processModel.setImportMultisigHexScheduled(proto.getImportMultisigHexScheduled()); processModel.setPaymentSentPayoutTxStale(proto.getPaymentSentPayoutTxStale()); // nullable processModel.setPayoutTxSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getPayoutTxSignature())); processModel.setTempTradePeerNodeAddress(proto.hasTempTradePeerNodeAddress() ? NodeAddress.fromProto(proto.getTempTradePeerNodeAddress()) : null); processModel.setMediatedPayoutTxSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getMediatedPayoutTxSignature())); processModel.setMakerSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getMakerSignature())); processModel.setTradeFeeAddress(ProtoUtil.stringOrNullFromProto(proto.getTradeFeeAddress())); processModel.setMultisigAddress(ProtoUtil.stringOrNullFromProto(proto.getMultisigAddress())); // deprecated fields need to be read in order to migrate to new fields String paymentSentMessageStateSellerString = ProtoUtil.stringOrNullFromProto(proto.getPaymentSentMessageStateSeller()); MessageState paymentSentMessageStateSeller = ProtoUtil.enumFromProto(MessageState.class, paymentSentMessageStateSellerString); processModel.paymentSentMessageStatePropertySeller.set(paymentSentMessageStateSeller); String paymentSentMessageStateArbitratorString = ProtoUtil.stringOrNullFromProto(proto.getPaymentSentMessageStateArbitrator()); MessageState paymentSentMessageStateArbitrator = ProtoUtil.enumFromProto(MessageState.class, paymentSentMessageStateArbitratorString); processModel.paymentSentMessageStatePropertyArbitrator.set(paymentSentMessageStateArbitrator); return processModel; } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onComplete() { } @Nullable public PaymentAccountPayload getPaymentAccountPayload(String paymentAccountId) { PaymentAccount paymentAccount = getUser().getPaymentAccount(paymentAccountId); return paymentAccount != null ? paymentAccount.getPaymentAccountPayload() : null; } public Coin getFundsNeededForTrade() { return Coin.valueOf(fundsNeededForTrade); } public NodeAddress getMyNodeAddress() { return getP2PService().getAddress(); } public boolean isPaymentReceivedMessagesAcked() { return getArbitrator().isPaymentReceivedMessageAcked() && getBuyer().isPaymentReceivedMessageAcked(); } public boolean isDisputeOpenedMessageAckedOrNacked() { return getBuyer().isDisputeOpenedMessageAckedOrNacked() || getSeller().isDisputeOpenedMessageAckedOrNacked(); } void setDepositTxSentAckMessage(AckMessage ackMessage) { MessageState messageState = ackMessage.isSuccess() ? MessageState.ACKNOWLEDGED : MessageState.NACKED; setDepositTxMessageState(messageState); } public void setDepositTxMessageState(MessageState messageState) { this.depositTxMessageStateProperty.set(messageState); if (tradeManager != null) { tradeManager.requestPersistence(); } } void witnessDebugLog(Trade trade) { getAccountAgeWitnessService().getAccountAgeWitnessUtils().witnessDebugLog(trade, null); } public boolean maybeClearSensitiveData() { boolean changed = false; for (TradePeer tradingPeer : getTradePeers()) { if (tradingPeer.getPaymentAccountPayload() != null || tradingPeer.getContractAsJson() != null) { tradingPeer.setPaymentAccountPayload(null); tradingPeer.setContractAsJson(null); changed = true; } } return changed; } /////////////////////////////////////////////////////////////////////////////////////////// // Delegates /////////////////////////////////////////////////////////////////////////////////////////// public XmrWalletService getXmrWalletService() { return provider.getXmrWalletService(); } public BtcWalletService getBtcWalletService() { return provider.getBtcWalletService(); } public AccountAgeWitnessService getAccountAgeWitnessService() { return provider.getAccountAgeWitnessService(); } public P2PService getP2PService() { return provider.getP2PService(); } public TradeWalletService getTradeWalletService() { return provider.getTradeWalletService(); } public User getUser() { return provider.getUser(); } public OpenOfferManager getOpenOfferManager() { return provider.getOpenOfferManager(); } public ReferralIdService getReferralIdService() { return provider.getReferralIdService(); } public FilterManager getFilterManager() { return provider.getFilterManager(); } public TradeStatisticsManager getTradeStatisticsManager() { return provider.getTradeStatisticsManager(); } public ArbitratorManager getArbitratorManager() { return provider.getArbitratorManager(); } public MediatorManager getMediatorManager() { return provider.getMediatorManager(); } public RefundAgentManager getRefundAgentManager() { return provider.getRefundAgentManager(); } public KeyRing getKeyRing() { return provider.getKeyRing(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/ProcessModelServiceProvider.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol; import com.google.inject.Inject; import haveno.common.crypto.KeyRing; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.filter.FilterManager; import haveno.core.offer.OpenOfferManager; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.support.dispute.mediation.mediator.MediatorManager; import haveno.core.support.dispute.refund.refundagent.RefundAgentManager; import haveno.core.trade.statistics.ReferralIdService; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.User; import haveno.core.xmr.wallet.BtcWalletService; import haveno.core.xmr.wallet.TradeWalletService; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.P2PService; import lombok.Getter; @Getter public class ProcessModelServiceProvider { private final OpenOfferManager openOfferManager; private final P2PService p2PService; private final BtcWalletService btcWalletService; private final XmrWalletService xmrWalletService; private final TradeWalletService tradeWalletService; private final ReferralIdService referralIdService; private final User user; private final FilterManager filterManager; private final AccountAgeWitnessService accountAgeWitnessService; private final TradeStatisticsManager tradeStatisticsManager; private final ArbitratorManager arbitratorManager; private final MediatorManager mediatorManager; private final RefundAgentManager refundAgentManager; private final KeyRing keyRing; @Inject public ProcessModelServiceProvider(OpenOfferManager openOfferManager, P2PService p2PService, BtcWalletService btcWalletService, XmrWalletService xmrWalletService, TradeWalletService tradeWalletService, ReferralIdService referralIdService, User user, FilterManager filterManager, AccountAgeWitnessService accountAgeWitnessService, TradeStatisticsManager tradeStatisticsManager, ArbitratorManager arbitratorManager, MediatorManager mediatorManager, RefundAgentManager refundAgentManager, KeyRing keyRing) { this.openOfferManager = openOfferManager; this.p2PService = p2PService; this.btcWalletService = btcWalletService; this.xmrWalletService = xmrWalletService; this.tradeWalletService = tradeWalletService; this.referralIdService = referralIdService; this.user = user; this.filterManager = filterManager; this.accountAgeWitnessService = accountAgeWitnessService; this.tradeStatisticsManager = tradeStatisticsManager; this.arbitratorManager = arbitratorManager; this.mediatorManager = mediatorManager; this.refundAgentManager = refundAgentManager; this.keyRing = keyRing; } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/SellerAsMakerProtocol.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol; import haveno.common.ThreadUtils; import haveno.common.handlers.ErrorMessageHandler; import haveno.core.trade.SellerAsMakerTrade; import haveno.core.trade.Trade; import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.protocol.tasks.ApplyFilter; import haveno.core.trade.protocol.tasks.MakerSendInitTradeRequestToArbitrator; import haveno.core.trade.protocol.tasks.ProcessInitTradeRequest; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; @Slf4j public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtocol { /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public SellerAsMakerProtocol(SellerAsMakerTrade trade) { super(trade); } /////////////////////////////////////////////////////////////////////////////////////////// // MakerProtocol /////////////////////////////////////////////////////////////////////////////////////////// @Override public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) { log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {} from {}", trade.getClass().getSimpleName(), trade.getShortId(), peer); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); this.errorMessageHandler = errorMessageHandler; expect(phase(Trade.Phase.INIT) .with(message) .from(peer)) .setup(tasks( ApplyFilter.class, ProcessInitTradeRequest.class, MakerSendInitTradeRequestToArbitrator.class) .using(new TradeTaskRunner(trade, () -> { startTimeout(); handleTaskRunnerSuccess(peer, message); }, errorMessage -> { handleTaskRunnerFault(peer, message, errorMessage); })) .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) .executeTasks(true); awaitTradeLatch(); } }, trade.getId()); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/SellerAsTakerProtocol.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol; import haveno.common.ThreadUtils; import haveno.common.handlers.ErrorMessageHandler; import haveno.core.trade.SellerAsTakerTrade; import haveno.core.trade.Trade; import haveno.core.trade.handlers.TradeResultHandler; import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.protocol.tasks.ApplyFilter; import haveno.core.trade.protocol.tasks.ProcessInitTradeRequest; import haveno.core.trade.protocol.tasks.TakerReserveTradeFunds; import haveno.core.trade.protocol.tasks.TakerSendInitTradeRequestToArbitrator; import haveno.core.trade.protocol.tasks.TakerSendInitTradeRequestToMaker; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; @Slf4j public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtocol { /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public SellerAsTakerProtocol(SellerAsTakerTrade trade) { super(trade); } /////////////////////////////////////////////////////////////////////////////////////////// // Take offer /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onTakeOffer(TradeResultHandler tradeResultHandler, ErrorMessageHandler errorMessageHandler) { log.info(TradeProtocol.LOG_HIGHLIGHT + "onTakerOffer for {} {}", getClass().getSimpleName(), trade.getShortId()); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); this.tradeResultHandler = tradeResultHandler; this.errorMessageHandler = errorMessageHandler; expect(phase(Trade.Phase.INIT) .with(TakerEvent.TAKE_OFFER) .from(trade.getTradePeer().getNodeAddress())) .setup(tasks( ApplyFilter.class, TakerReserveTradeFunds.class, TakerSendInitTradeRequestToMaker.class) .using(new TradeTaskRunner(trade, () -> { startTimeout(); unlatchTrade(); }, errorMessage -> { handleError(errorMessage); })) .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) .executeTasks(true); awaitTradeLatch(); } }, trade.getId()); } @Override public void handleInitTradeRequest(InitTradeRequest message, NodeAddress sender) { log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {} from {}", trade.getClass().getSimpleName(), trade.getShortId(), sender); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { // ignore if assigned a different arbitrator NodeAddress nodeAddress = trade.getArbitrator().getNodeAddress(); if (nodeAddress != null && !nodeAddress.equals(sender)) { log.warn("Ignoring InitTradeRequest because sender is not the arbitrator for the trade, tradeId={}, sender={}", message.getOfferId(), sender); return; } latchTrade(); expect(phase(Trade.Phase.INIT) .with(message) .from(sender)) .setup(tasks( ApplyFilter.class, ProcessInitTradeRequest.class, TakerSendInitTradeRequestToArbitrator.class) .using(new TradeTaskRunner(trade, () -> { startTimeout(); handleTaskRunnerSuccess(sender, message); }, errorMessage -> { handleTaskRunnerFault(sender, message, errorMessage); })) .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) .executeTasks(true); awaitTradeLatch(); } }, trade.getId()); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol; import haveno.common.ThreadUtils; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.core.trade.SellerTrade; import haveno.core.trade.Trade; import haveno.core.trade.messages.SignContractResponse; import haveno.core.trade.messages.TradeMessage; import haveno.core.trade.protocol.tasks.ApplyFilter; import haveno.core.trade.protocol.tasks.SellerPreparePaymentReceivedMessage; import haveno.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToArbitrator; import haveno.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToBuyer; import haveno.core.trade.protocol.tasks.SendDepositsConfirmedMessageToArbitrator; import haveno.core.trade.protocol.tasks.SendDepositsConfirmedMessageToBuyer; import haveno.core.trade.protocol.tasks.TradeTask; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; @Slf4j public class SellerProtocol extends DisputeProtocol { enum SellerEvent implements FluentProtocol.Event { STARTUP, DEPOSIT_TXS_CONFIRMED, PAYMENT_RECEIVED } public SellerProtocol(SellerTrade trade) { super(trade); } @Override protected void onInitializeAfterMailboxMessages() { super.onInitializeAfterMailboxMessages(); maybeResendPaymentReceivedMessage(); } private void maybeResendPaymentReceivedMessage() { // re-send payment received message if not acked if (!((SellerTrade) trade).needsToResendPaymentReceivedMessages()) return; ThreadUtils.execute(() -> { synchronized (trade.getLock()) { if (!((SellerTrade) trade).needsToResendPaymentReceivedMessages()) return; latchTrade(); given(anyPhase(Trade.Phase.PAYMENT_RECEIVED) .with(SellerEvent.STARTUP)) .setup(tasks( SellerPreparePaymentReceivedMessage.class, SellerSendPaymentReceivedMessageToBuyer.class, SellerSendPaymentReceivedMessageToArbitrator.class) .using(new TradeTaskRunner(trade, () -> { unlatchTrade(); }, (errorMessage) -> { log.warn("Error sending PaymentReceivedMessage on startup: " + errorMessage); unlatchTrade(); }))) .executeTasks(); awaitTradeLatch(); } }, trade.getId()); } @Override protected void onTradeMessage(TradeMessage message, NodeAddress peer) { super.onTradeMessage(message, peer); } @Override public void onMailboxMessage(TradeMessage message, NodeAddress peerNodeAddress) { super.onMailboxMessage(message, peerNodeAddress); } @Override public void handleSignContractResponse(SignContractResponse response, NodeAddress sender) { super.handleSignContractResponse(response, sender); } /////////////////////////////////////////////////////////////////////////////////////////// // User interaction /////////////////////////////////////////////////////////////////////////////////////////// public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { log.info(TradeProtocol.LOG_HIGHLIGHT + "SellerProtocol.onPaymentReceived() for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); // advance trade state if (trade.isPaymentSentMessageProcessed()) { trade.setStateIfValidTransitionTo(Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT); } else { errorMessageHandler.handleErrorMessage("Cannot confirm payment received for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " in state " + trade.getState()); return; } // process on trade thread ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); this.errorMessageHandler = errorMessageHandler; SellerEvent event = SellerEvent.PAYMENT_RECEIVED; try { expect(anyPhase(Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED) .with(event) .preCondition(trade.confirmPermitted())) .setup(tasks( ApplyFilter.class, SellerPreparePaymentReceivedMessage.class, SellerSendPaymentReceivedMessageToBuyer.class, SellerSendPaymentReceivedMessageToArbitrator.class) .using(new TradeTaskRunner(trade, () -> { stopTimeout(); this.errorMessageHandler = null; handleTaskRunnerSuccess(event); resultHandler.handleResult(); }, (errorMessage) -> { log.warn("Error confirming payment received, reverting state to {}, error={}", Trade.State.BUYER_SENT_PAYMENT_SENT_MSG, errorMessage); if (!trade.isPayoutPublished()) trade.resetToPaymentSentState(); handleTaskRunnerFault(event, errorMessage); }))) .executeTasks(true); } catch (Exception e) { errorMessageHandler.handleErrorMessage("Error confirming payment received: " + e.getMessage()); unlatchTrade(); } awaitTradeLatch(); } }, trade.getId()); } @SuppressWarnings("unchecked") @Override public Class[] getDepositsConfirmedTasks() { return new Class[] { SendDepositsConfirmedMessageToArbitrator.class, SendDepositsConfirmedMessageToBuyer.class }; } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/TakerProtocol.java ================================================ /* * 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 . */ package haveno.core.trade.protocol; import haveno.common.handlers.ErrorMessageHandler; import haveno.core.trade.handlers.TradeResultHandler; import haveno.core.trade.messages.InitTradeRequest; import haveno.network.p2p.NodeAddress; public interface TakerProtocol extends TraderProtocol { void onTakeOffer(TradeResultHandler tradeResultHandler, ErrorMessageHandler errorMessageHandler); void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer); enum TakerEvent implements FluentProtocol.Event { TAKE_OFFER } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/TradeListener.java ================================================ package haveno.core.trade.protocol; import haveno.core.trade.messages.TradeMessage; import haveno.network.p2p.AckMessage; import haveno.network.p2p.NodeAddress; /** * Receives notifications of decrypted, verified trade and ack messages. */ public class TradeListener { public void onVerifiedTradeMessage(TradeMessage message, NodeAddress sender) { } public void onAckMessage(AckMessage ackMessage, NodeAddress sender) { } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/TradePeer.java ================================================ /* * 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 . */ package haveno.core.trade.protocol; import com.google.protobuf.ByteString; import com.google.protobuf.Message; import haveno.common.app.Version; import haveno.common.crypto.PubKeyRing; import haveno.common.proto.ProtoUtil; import haveno.common.proto.persistable.PersistablePayload; import haveno.core.account.witness.AccountAgeWitness; import haveno.core.network.MessageState; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.proto.CoreProtoResolver; import haveno.core.support.dispute.messages.DisputeClosedMessage; import haveno.core.support.dispute.messages.DisputeOpenedMessage; import haveno.core.trade.TradeManager; import haveno.core.trade.messages.PaymentReceivedMessage; import haveno.core.trade.messages.PaymentSentMessage; import haveno.network.p2p.AckMessage; import haveno.network.p2p.NodeAddress; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import monero.wallet.model.MoneroTxWallet; import javax.annotation.Nullable; import java.math.BigInteger; import java.util.ArrayList; import java.util.List; import java.util.Optional; // Fields marked as transient are only used during protocol execution which are based on directMessages so we do not // persist them. //todo clean up older fields as well to make most transient @Slf4j @Getter @Setter public final class TradePeer implements PersistablePayload { // Transient/Mutable // Added in v1.2.0 @Setter @Nullable transient private byte[] delayedPayoutTxSignature; @Setter @Nullable transient private byte[] preparedDepositTx; transient private MoneroTxWallet depositTx; transient private TradeManager tradeManager; // Persistable mutable @Nullable private NodeAddress nodeAddress; @Nullable private PubKeyRing pubKeyRing; @Nullable private String accountId; @Nullable private String paymentAccountId; @Nullable private String paymentMethodId; @Nullable private byte[] paymentAccountPayloadHash; @Nullable private byte[] encryptedPaymentAccountPayload; @Nullable private byte[] paymentAccountKey; @Nullable private PaymentAccountPayload paymentAccountPayload; @Nullable private String payoutAddressString; @Nullable private String contractAsJson; @Nullable private byte[] contractSignature; @Nullable @Setter @Getter private PaymentSentMessage paymentSentMessage; @Nullable @Setter @Getter private PaymentReceivedMessage paymentReceivedMessage; @Nullable @Setter @Getter private DisputeOpenedMessage disputeOpenedMessage; @Setter @Getter private DisputeClosedMessage disputeClosedMessage; // added in v 0.6 @Nullable private byte[] accountAgeWitnessNonce; @Nullable private byte[] accountAgeWitnessSignature; @Getter @Setter @Nullable private AccountAgeWitness accountAgeWitness; private long currentDate; // Added in v.1.1.6 @Nullable private byte[] mediatedPayoutTxSignature; // Added for XMR integration @Nullable private String reserveTxHash; @Nullable private String reserveTxHex; @Nullable private String reserveTxKey; @Nullable private List reserveTxKeyImages = new ArrayList<>(); @Nullable private String preparedMultisigHex; @Nullable private String madeMultisigHex; @Nullable private String exchangedMultisigHex; @Nullable private String depositTxHash; @Nullable private String depositTxHex; @Nullable private String depositTxKey; private long depositTxFee; private long securityDeposit; @Nullable @Setter private String unsignedPayoutTxHex; private long payoutTxFee; private long payoutAmount; @Nullable private String updatedMultisigHex; @Deprecated private boolean depositsConfirmedMessageAcked; // We want to indicate the user the state of the message delivery of the payment // confirmation messages. We do an automatic re-send in case it was not ACKed yet. // To enable that even after restart we persist the state. @Setter private ObjectProperty depositsConfirmedMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); @Setter private ObjectProperty paymentSentMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); @Setter private ObjectProperty paymentReceivedMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); @Setter private ObjectProperty disputeOpenedMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); @Setter private ObjectProperty disputeClosedMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); public TradePeer() { } public void applyTransient(TradeManager tradeManager) { this.tradeManager = tradeManager; // migrate deprecated fields to new model for v1.0.19 if (depositsConfirmedMessageAcked && depositsConfirmedMessageStateProperty.get() == MessageState.UNDEFINED) { depositsConfirmedMessageStateProperty.set(MessageState.ACKNOWLEDGED); tradeManager.requestPersistence(); } } public BigInteger getDepositTxFee() { return BigInteger.valueOf(depositTxFee); } public void setDepositTxFee(BigInteger depositTxFee) { this.depositTxFee = depositTxFee.longValueExact(); } public BigInteger getSecurityDeposit() { return BigInteger.valueOf(securityDeposit); } public void setSecurityDeposit(BigInteger securityDeposit) { this.securityDeposit = securityDeposit.longValueExact(); } public BigInteger getPayoutTxFee() { return BigInteger.valueOf(payoutTxFee); } public void setPayoutTxFee(BigInteger payoutTxFee) { this.payoutTxFee = payoutTxFee.longValueExact(); } public BigInteger getPayoutAmount() { return BigInteger.valueOf(payoutAmount); } public void setPayoutAmount(BigInteger payoutAmount) { this.payoutAmount = payoutAmount.longValueExact(); } void setDepositsConfirmedAckMessage(AckMessage ackMessage) { MessageState messageState = ackMessage.isSuccess() ? MessageState.ACKNOWLEDGED : MessageState.NACKED; setDepositsConfirmedMessageState(messageState); } void setPaymentSentAckMessage(AckMessage ackMessage) { MessageState messageState = ackMessage.isSuccess() ? MessageState.ACKNOWLEDGED : MessageState.NACKED; setPaymentSentMessageState(messageState); } void setPaymentReceivedAckMessage(AckMessage ackMessage) { MessageState messageState = ackMessage.isSuccess() ? MessageState.ACKNOWLEDGED : MessageState.NACKED; setPaymentReceivedMessageState(messageState); } public void setDepositsConfirmedMessageState(MessageState depositsConfirmedMessageStateProperty) { this.depositsConfirmedMessageStateProperty.set(depositsConfirmedMessageStateProperty); if (tradeManager != null) { tradeManager.requestPersistence(); } } public void setPaymentSentMessageState(MessageState paymentSentMessageStateProperty) { this.paymentSentMessageStateProperty.set(paymentSentMessageStateProperty); if (tradeManager != null) { tradeManager.requestPersistence(); } } public void setPaymentReceivedMessageState(MessageState paymentReceivedMessageStateProperty) { this.paymentReceivedMessageStateProperty.set(paymentReceivedMessageStateProperty); if (tradeManager != null) { tradeManager.requestPersistence(); } } public void setDisputeOpenedAckMessage(AckMessage ackMessage) { MessageState messageState = ackMessage.isSuccess() ? MessageState.ACKNOWLEDGED : MessageState.NACKED; setDisputeOpenedMessageState(messageState); } public void setDisputeOpenedMessageState(MessageState disputeOpenedMessageStateProperty) { this.disputeOpenedMessageStateProperty.set(disputeOpenedMessageStateProperty); if (tradeManager != null) { tradeManager.requestPersistence(); } } public void setDisputeClosedMessageState(MessageState disputeClosedMessageStateProperty) { this.disputeClosedMessageStateProperty.set(disputeClosedMessageStateProperty); if (tradeManager != null) { tradeManager.requestPersistence(); } } public boolean isDepositsConfirmedMessageAcked() { return depositsConfirmedMessageStateProperty.get() == MessageState.ACKNOWLEDGED; } public boolean isPaymentSentMessageAcked() { return paymentSentMessageStateProperty.get() == MessageState.ACKNOWLEDGED; } public boolean isPaymentSentMessageStored() { return paymentSentMessageStateProperty.get() == MessageState.STORED_IN_MAILBOX; } public boolean isPaymentReceivedMessageAcked() { return paymentReceivedMessageStateProperty.get() == MessageState.ACKNOWLEDGED; } public boolean isPaymentReceivedMessageNacked() { return paymentReceivedMessageStateProperty.get() == MessageState.NACKED; } public boolean isPaymentReceivedMessageStored() { return paymentReceivedMessageStateProperty.get() == MessageState.STORED_IN_MAILBOX; } public boolean isPaymentReceivedMessageAckedOrNacked() { return isPaymentReceivedMessageAcked() || isPaymentReceivedMessageNacked(); } public boolean isPaymentReceivedMessageArrived() { return paymentReceivedMessageStateProperty.get() == MessageState.ARRIVED; } public boolean isDisputeOpenedMessageAckedOrNacked() { return disputeOpenedMessageStateProperty.get() == MessageState.ACKNOWLEDGED || disputeOpenedMessageStateProperty.get() == MessageState.NACKED; } public boolean isDisputeOpenedMessageStored() { return disputeOpenedMessageStateProperty.get() == MessageState.STORED_IN_MAILBOX; } @Override public Message toProtoMessage() { final protobuf.TradePeer.Builder builder = protobuf.TradePeer.newBuilder(); Optional.ofNullable(nodeAddress).ifPresent(e -> builder.setNodeAddress(nodeAddress.toProtoMessage())); Optional.ofNullable(pubKeyRing).ifPresent(e -> builder.setPubKeyRing(pubKeyRing.toProtoMessage())); Optional.ofNullable(accountId).ifPresent(builder::setAccountId); Optional.ofNullable(paymentAccountId).ifPresent(builder::setPaymentAccountId); Optional.ofNullable(paymentMethodId).ifPresent(builder::setPaymentMethodId); Optional.ofNullable(paymentAccountPayloadHash).ifPresent(e -> builder.setPaymentAccountPayloadHash(ByteString.copyFrom(paymentAccountPayloadHash))); Optional.ofNullable(encryptedPaymentAccountPayload).ifPresent(e -> builder.setEncryptedPaymentAccountPayload(ByteString.copyFrom(e))); Optional.ofNullable(paymentAccountKey).ifPresent(e -> builder.setPaymentAccountKey(ByteString.copyFrom(e))); Optional.ofNullable(paymentAccountPayload).ifPresent(e -> builder.setPaymentAccountPayload((protobuf.PaymentAccountPayload) e.toProtoMessage())); Optional.ofNullable(payoutAddressString).ifPresent(builder::setPayoutAddressString); Optional.ofNullable(contractAsJson).ifPresent(builder::setContractAsJson); Optional.ofNullable(contractSignature).ifPresent(e -> builder.setContractSignature(ByteString.copyFrom(e))); Optional.ofNullable(pubKeyRing).ifPresent(e -> builder.setPubKeyRing(e.toProtoMessage())); Optional.ofNullable(accountAgeWitnessNonce).ifPresent(e -> builder.setAccountAgeWitnessNonce(ByteString.copyFrom(e))); Optional.ofNullable(accountAgeWitnessSignature).ifPresent(e -> builder.setAccountAgeWitnessSignature(ByteString.copyFrom(e))); Optional.ofNullable(accountAgeWitness).ifPresent(e -> builder.setAccountAgeWitness(accountAgeWitness.toProtoAccountAgeWitness())); Optional.ofNullable(mediatedPayoutTxSignature).ifPresent(e -> builder.setMediatedPayoutTxSignature(ByteString.copyFrom(e))); Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage())); Optional.ofNullable(paymentReceivedMessage).ifPresent(e -> builder.setPaymentReceivedMessage(paymentReceivedMessage.toProtoNetworkEnvelope().getPaymentReceivedMessage())); Optional.ofNullable(disputeOpenedMessage).ifPresent(e -> builder.setDisputeOpenedMessage(disputeOpenedMessage.toProtoNetworkEnvelope().getDisputeOpenedMessage())); Optional.ofNullable(disputeClosedMessage).ifPresent(e -> builder.setDisputeClosedMessage(disputeClosedMessage.toProtoNetworkEnvelope().getDisputeClosedMessage())); Optional.ofNullable(reserveTxHash).ifPresent(e -> builder.setReserveTxHash(reserveTxHash)); Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex)); Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey)); Optional.ofNullable(reserveTxKeyImages).ifPresent(e -> builder.addAllReserveTxKeyImages(reserveTxKeyImages)); Optional.ofNullable(preparedMultisigHex).ifPresent(e -> builder.setPreparedMultisigHex(preparedMultisigHex)); Optional.ofNullable(madeMultisigHex).ifPresent(e -> builder.setMadeMultisigHex(madeMultisigHex)); Optional.ofNullable(exchangedMultisigHex).ifPresent(e -> builder.setExchangedMultisigHex(exchangedMultisigHex)); Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex)); Optional.ofNullable(depositTxHash).ifPresent(e -> builder.setDepositTxHash(depositTxHash)); Optional.ofNullable(depositTxHex).ifPresent(e -> builder.setDepositTxHex(depositTxHex)); Optional.ofNullable(depositTxKey).ifPresent(e -> builder.setDepositTxKey(depositTxKey)); Optional.ofNullable(depositTxFee).ifPresent(e -> builder.setDepositTxFee(depositTxFee)); Optional.ofNullable(securityDeposit).ifPresent(e -> builder.setSecurityDeposit(securityDeposit)); Optional.ofNullable(unsignedPayoutTxHex).ifPresent(e -> builder.setUnsignedPayoutTxHex(unsignedPayoutTxHex)); Optional.ofNullable(payoutTxFee).ifPresent(e -> builder.setPayoutTxFee(payoutTxFee)); Optional.ofNullable(payoutAmount).ifPresent(e -> builder.setPayoutAmount(payoutAmount)); builder.setDepositsConfirmedMessageAcked(depositsConfirmedMessageAcked); builder.setDepositsConfirmedMessageState(depositsConfirmedMessageStateProperty.get().name()); builder.setPaymentSentMessageState(paymentSentMessageStateProperty.get().name()); builder.setPaymentReceivedMessageState(paymentReceivedMessageStateProperty.get().name()); builder.setDisputeOpenedMessageState(disputeOpenedMessageStateProperty.get().name()); builder.setDisputeClosedMessageState(disputeClosedMessageStateProperty.get().name()); builder.setCurrentDate(currentDate); return builder.build(); } public static TradePeer fromProto(protobuf.TradePeer proto, CoreProtoResolver coreProtoResolver) { if (proto.getDefaultInstanceForType().equals(proto)) { return null; } else { TradePeer tradePeer = new TradePeer(); tradePeer.setNodeAddress(proto.hasNodeAddress() ? NodeAddress.fromProto(proto.getNodeAddress()) : null); tradePeer.setPubKeyRing(proto.hasPubKeyRing() ? PubKeyRing.fromProto(proto.getPubKeyRing()) : null); tradePeer.setAccountId(ProtoUtil.stringOrNullFromProto(proto.getAccountId())); tradePeer.setPaymentAccountId(ProtoUtil.stringOrNullFromProto(proto.getPaymentAccountId())); tradePeer.setPaymentMethodId(ProtoUtil.stringOrNullFromProto(proto.getPaymentMethodId())); tradePeer.setPaymentAccountPayloadHash(proto.getPaymentAccountPayloadHash().toByteArray()); tradePeer.setEncryptedPaymentAccountPayload(proto.getEncryptedPaymentAccountPayload().toByteArray()); tradePeer.setPaymentAccountKey(ProtoUtil.byteArrayOrNullFromProto(proto.getPaymentAccountKey())); tradePeer.setPaymentAccountPayload(proto.hasPaymentAccountPayload() ? coreProtoResolver.fromProto(proto.getPaymentAccountPayload()) : null); tradePeer.setPayoutAddressString(ProtoUtil.stringOrNullFromProto(proto.getPayoutAddressString())); tradePeer.setContractAsJson(ProtoUtil.stringOrNullFromProto(proto.getContractAsJson())); tradePeer.setContractSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getContractSignature())); tradePeer.setPubKeyRing(proto.hasPubKeyRing() ? PubKeyRing.fromProto(proto.getPubKeyRing()) : null); tradePeer.setAccountAgeWitnessNonce(ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessNonce())); tradePeer.setAccountAgeWitnessSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessSignature())); protobuf.AccountAgeWitness protoAccountAgeWitness = proto.getAccountAgeWitness(); tradePeer.setAccountAgeWitness(protoAccountAgeWitness.getHash().isEmpty() ? null : AccountAgeWitness.fromProto(protoAccountAgeWitness)); tradePeer.setCurrentDate(proto.getCurrentDate()); tradePeer.setMediatedPayoutTxSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getMediatedPayoutTxSignature())); tradePeer.setPaymentSentMessage(proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), Version.getP2PMessageVersion()) : null); tradePeer.setPaymentReceivedMessage(proto.hasPaymentReceivedMessage() ? PaymentReceivedMessage.fromProto(proto.getPaymentReceivedMessage(), Version.getP2PMessageVersion()) : null); tradePeer.setDisputeOpenedMessage(proto.hasDisputeOpenedMessage() ? DisputeOpenedMessage.fromProto(proto.getDisputeOpenedMessage(), coreProtoResolver, Version.getP2PMessageVersion()) : null); tradePeer.setDisputeClosedMessage(proto.hasDisputeClosedMessage() ? DisputeClosedMessage.fromProto(proto.getDisputeClosedMessage(), Version.getP2PMessageVersion()) : null); tradePeer.setReserveTxHash(ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash())); tradePeer.setReserveTxHex(ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex())); tradePeer.setReserveTxKey(ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey())); tradePeer.setReserveTxKeyImages(proto.getReserveTxKeyImagesList()); tradePeer.setPreparedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getPreparedMultisigHex())); tradePeer.setMadeMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getMadeMultisigHex())); tradePeer.setExchangedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getExchangedMultisigHex())); tradePeer.setUpdatedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex())); tradePeer.setDepositsConfirmedMessageAcked(proto.getDepositsConfirmedMessageAcked()); tradePeer.setDepositTxHash(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHash())); tradePeer.setDepositTxHex(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHex())); tradePeer.setDepositTxKey(ProtoUtil.stringOrNullFromProto(proto.getDepositTxKey())); tradePeer.setDepositTxFee(BigInteger.valueOf(proto.getDepositTxFee())); tradePeer.setSecurityDeposit(BigInteger.valueOf(proto.getSecurityDeposit())); tradePeer.setUnsignedPayoutTxHex(ProtoUtil.stringOrNullFromProto(proto.getUnsignedPayoutTxHex())); tradePeer.setPayoutTxFee(BigInteger.valueOf(proto.getPayoutTxFee())); tradePeer.setPayoutAmount(BigInteger.valueOf(proto.getPayoutAmount())); String depositsConfirmedMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getDepositsConfirmedMessageState()); MessageState depositsConfirmedMessageState = ProtoUtil.enumFromProto(MessageState.class, depositsConfirmedMessageStateString); tradePeer.setDepositsConfirmedMessageState(depositsConfirmedMessageState); String paymentSentMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getPaymentSentMessageState()); MessageState paymentSentMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentSentMessageStateString); tradePeer.setPaymentSentMessageState(paymentSentMessageState); String paymentReceivedMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getPaymentReceivedMessageState()); MessageState paymentReceivedMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentReceivedMessageStateString); tradePeer.setPaymentReceivedMessageState(paymentReceivedMessageState); String disputeOpenedMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getDisputeOpenedMessageState()); MessageState disputeOpenedMessageState = ProtoUtil.enumFromProto(MessageState.class, disputeOpenedMessageStateString); tradePeer.setDisputeOpenedMessageState(disputeOpenedMessageState); String disputeClosedMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getDisputeClosedMessageState()); MessageState disputeClosedMessageState = ProtoUtil.enumFromProto(MessageState.class, disputeClosedMessageStateString); tradePeer.setDisputeClosedMessageState(disputeClosedMessageState); return tradePeer; } } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol; import haveno.common.ThreadUtils; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.config.Config; import haveno.common.crypto.PubKeyRing; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.taskrunner.Task; import haveno.core.offer.OpenOffer; import haveno.core.support.messages.ChatMessage; import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.BuyerTrade; import haveno.core.trade.HavenoUtils; import haveno.core.trade.SellerTrade; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.trade.TradeManager.MailboxMessageComparator; import haveno.core.trade.handlers.TradeResultHandler; import haveno.core.trade.messages.DepositRequest; import haveno.core.trade.messages.DepositResponse; import haveno.core.trade.messages.DepositsConfirmedMessage; import haveno.core.trade.messages.InitMultisigRequest; import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.messages.PaymentReceivedMessage; import haveno.core.trade.messages.PaymentSentMessage; import haveno.core.trade.messages.SignContractRequest; import haveno.core.trade.messages.SignContractResponse; import haveno.core.trade.messages.TradeMessage; import haveno.core.trade.protocol.FluentProtocol.Condition; import haveno.core.trade.protocol.FluentProtocol.Event; import haveno.core.trade.protocol.tasks.ApplyFilter; import haveno.core.trade.protocol.tasks.MakerRecreateReserveTx; import haveno.core.trade.protocol.tasks.MakerSendInitTradeRequestToArbitrator; import haveno.core.trade.protocol.tasks.MaybeSendSignContractRequest; import haveno.core.trade.protocol.tasks.ProcessDepositResponse; import haveno.core.trade.protocol.tasks.ProcessDepositsConfirmedMessage; import haveno.core.trade.protocol.tasks.ProcessInitMultisigRequest; import haveno.core.trade.protocol.tasks.ProcessPaymentReceivedMessage; import haveno.core.trade.protocol.tasks.ProcessPaymentSentMessage; import haveno.core.trade.protocol.tasks.ProcessSignContractRequest; import haveno.core.trade.protocol.tasks.SendDepositRequest; import haveno.core.trade.protocol.tasks.MaybeResendDisputeClosedMessageWithPayout; import haveno.core.trade.protocol.tasks.TradeTask; import haveno.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness; import haveno.core.util.Validator; import haveno.network.p2p.AckMessage; import haveno.network.p2p.AckMessageSourceType; import haveno.network.p2p.DecryptedDirectMessageListener; import haveno.network.p2p.DecryptedMessageWithPubKey; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.mailbox.MailboxMessage; import haveno.network.p2p.mailbox.MailboxMessageService; import haveno.network.p2p.messaging.DecryptedMailboxListener; import lombok.extern.slf4j.Slf4j; import org.fxmisc.easybind.EasyBind; import javax.annotation.Nullable; import java.util.Collection; import java.util.Collections; import java.util.concurrent.CountDownLatch; @Slf4j public abstract class TradeProtocol implements DecryptedDirectMessageListener, DecryptedMailboxListener { public static final int TRADE_STEP_TIMEOUT_SECONDS = Config.baseCurrencyNetwork().isTestnet() ? 60 : 180; private static final String TIMEOUT_REACHED = "Timeout reached."; public static final int MAX_ATTEMPTS = 5; // max attempts to create txs and other protocol functions public static final int REQUEST_CONNECTION_SWITCH_EVERY_NUM_ATTEMPTS = 2; // request connection switch on even attempts public static final long REPROCESS_DELAY_MS = 5000; public static final String LOG_HIGHLIGHT = ""; // TODO: how to highlight some logs with cyan? ("\u001B[36m")? coloring works in the terminal but prints character literals to .log files public static final String SEND_INIT_TRADE_REQUEST_FAILED = "Sending InitTradeRequest failed"; protected final ProcessModel processModel; protected final Trade trade; protected CountDownLatch tradeLatch; // to synchronize on trade private Timer timeoutTimer; private Object timeoutTimerLock = new Object(); protected TradeResultHandler tradeResultHandler; protected ErrorMessageHandler errorMessageHandler; private boolean depositsConfirmedTasksCalled; private int reprocessPaymentSentMessageCount; private int reprocessPaymentReceivedMessageCount; private boolean makerInitTradeRequestHasBeenNacked = false; private PaymentReceivedMessage lastAckedPaymentReceivedMessage = null; private int numPaymentReceivedNacks = 0; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public TradeProtocol(Trade trade) { this.trade = trade; this.processModel = trade.getProcessModel(); } /////////////////////////////////////////////////////////////////////////////////////////// // Message dispatching /////////////////////////////////////////////////////////////////////////////////////////// protected void onTradeMessage(TradeMessage message, NodeAddress peerNodeAddress) { log.info("Received {} as TradeMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getOfferId(), message.getUid()); handle(message, peerNodeAddress); } protected void onMailboxMessage(TradeMessage message, NodeAddress peerNodeAddress) { log.info("Received {} as MailboxMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getOfferId(), message.getUid()); handle(message, peerNodeAddress); } private void handle(TradeMessage message, NodeAddress peerNodeAddress) { if (message instanceof DepositsConfirmedMessage) { handle((DepositsConfirmedMessage) message, peerNodeAddress); } else if (message instanceof PaymentSentMessage) { handle((PaymentSentMessage) message, peerNodeAddress); } else if (message instanceof PaymentReceivedMessage) { handle((PaymentReceivedMessage) message, peerNodeAddress); } } @Override public void onDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress peer) { NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); if (!isMyMessage(networkEnvelope)) { return; } if (!isPubKeyValid(decryptedMessageWithPubKey)) { return; } if (networkEnvelope instanceof TradeMessage) { onTradeMessage((TradeMessage) networkEnvelope, peer); // notify trade listeners // TODO (woodser): better way to register message notifications for trade? if (((TradeMessage) networkEnvelope).getOfferId().equals(processModel.getOfferId())) { trade.onVerifiedTradeMessage((TradeMessage) networkEnvelope, peer); } } else if (networkEnvelope instanceof AckMessage) { onAckMessage((AckMessage) networkEnvelope, peer); } } @Override public void onMailboxMessageAdded(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress peer) { if (!isPubKeyValid(decryptedMessageWithPubKey)) return; handleMailboxCollectionSkipValidation(Collections.singletonList(decryptedMessageWithPubKey)); } // TODO (woodser): this method only necessary because isPubKeyValid not called with sender argument, so it's validated before private void handleMailboxCollectionSkipValidation(Collection collection) { collection.stream() .map(DecryptedMessageWithPubKey::getNetworkEnvelope) .filter(this::isMyMessage) .filter(e -> e instanceof MailboxMessage) .map(e -> (MailboxMessage) e) .forEach(this::handleMailboxMessage); } private void handleMailboxCollection(Collection collection) { collection.stream() .filter(this::isPubKeyValid) .map(DecryptedMessageWithPubKey::getNetworkEnvelope) .filter(this::isMyMessage) .filter(e -> e instanceof MailboxMessage) .map(e -> (MailboxMessage) e) .sorted(new MailboxMessageComparator()) .forEach(this::handleMailboxMessage); } private void handleMailboxMessage(MailboxMessage mailboxMessage) { if (mailboxMessage instanceof TradeMessage) { TradeMessage tradeMessage = (TradeMessage) mailboxMessage; // We only remove here if we have already completed the trade. // Otherwise removal is done after successfully applied the task runner. if (trade.isCompleted()) { processModel.getP2PService().getMailboxMessageService().removeMailboxMsg(mailboxMessage); log.info("Remove {} from the P2P network as trade is already completed.", tradeMessage.getClass().getSimpleName()); return; } onMailboxMessage(tradeMessage, mailboxMessage.getSenderNodeAddress()); } else if (mailboxMessage instanceof AckMessage) { AckMessage ackMessage = (AckMessage) mailboxMessage; onAckMessage(ackMessage, mailboxMessage.getSenderNodeAddress()); processModel.getP2PService().getMailboxMessageService().removeMailboxMsg(ackMessage); log.info("Remove {} from the P2P network.", ackMessage.getClass().getSimpleName()); } } public void removeMailboxMessageAfterProcessing(TradeMessage tradeMessage) { if (tradeMessage instanceof MailboxMessage) { processModel.getP2PService().getMailboxMessageService().removeMailboxMsg((MailboxMessage) tradeMessage); log.info("Remove {} from the P2P network.", tradeMessage.getClass().getSimpleName()); } } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public abstract Class[] getDepositsConfirmedTasks(); public void initialize(ProcessModelServiceProvider serviceProvider, TradeManager tradeManager) { processModel.applyTransient(serviceProvider, tradeManager, trade.getOffer()); onInitialized(); } protected void onInitialized() { // reset pools for trade ids ThreadUtils.reset(getInitId()); ThreadUtils.reset(trade.getId()); // listen for direct messages unless finished (assumes this is called before mailbox message service is intialized) MailboxMessageService mailboxMessageService = processModel.getP2PService().getMailboxMessageService(); if (!trade.isFinished()) { processModel.getP2PService().addDecryptedDirectMessageListener(this); mailboxMessageService.addDecryptedMailboxListener(this); } // initialize trade synchronized (trade.getLock()) { trade.initialize(processModel.getProvider()); // initialize trade after mailbox messages processed UserThread.execute(() -> { // subscribe on user thread to avoid concurrent modification EasyBind.subscribe(mailboxMessageService.getIsInitializedProperty(), changed -> { if (Boolean.TRUE.equals(changed)) { onInitializeAfterMailboxMessages(); } }); }); } // send deposits confirmed message if applicable EasyBind.subscribe(trade.stateProperty(), state -> maybeSendDepositsConfirmedMessages()); } protected void onInitializeAfterMailboxMessages() { // run on initialization thread to preserve ordering processing messages if (ignoreMailboxMessages()) return; ThreadUtils.execute(() -> { if (ignoreMailboxMessages()) return; // reprocess messages if applicable maybeReprocessPaymentSentMessage(false); maybeReprocessPaymentReceivedMessage(false); maybeReprocessDisputeClosedMessage(false); // initialize trade after startup mailbox messages processed trade.initializeAfterMailboxMessages(); }, getInitId()); } private boolean ignoreMailboxMessages() { return !trade.isDepositRequested() || trade.isPayoutFinalized() || trade.isCompleted() || trade.isShutDownStarted(); } public String getInitId() { return trade.getId() + "_INIT"; } public void maybeSendDepositsConfirmedMessages() { if (!trade.isInitialized() || trade.isShutDownStarted()) return; // skip if shutting down ThreadUtils.execute(() -> { if (!trade.isInitialized() || trade.isShutDownStarted()) return; if (!trade.isDepositsConfirmed() || trade.isDepositsConfirmedAcked() || trade.isPayoutPublished() || depositsConfirmedTasksCalled) return; depositsConfirmedTasksCalled = true; synchronized (trade.getLock()) { if (!trade.isInitialized() || trade.isShutDownStarted()) return; latchTrade(); expect(new Condition(trade)) .setup(tasks(getDepositsConfirmedTasks()) .using(new TradeTaskRunner(trade, () -> { handleTaskRunnerSuccess(null, null, "maybeSendDepositsConfirmedMessages"); }, (errorMessage) -> { handleTaskRunnerFault(null, null, "maybeSendDepositsConfirmedMessages", errorMessage, null); }))) .executeTasks(true); awaitTradeLatch(); } }, trade.getId()); } public void maybeReprocessPaymentSentMessage(boolean reprocessOnError) { if (trade.isShutDownStarted()) return; ThreadUtils.execute(() -> { if (trade.isShutDownStarted()) return; synchronized (trade.getLock()) { // skip if no need to reprocess if (trade.isShutDownStarted() || trade.isBuyer() || trade.getBuyer().getPaymentSentMessage() == null || trade.getState().ordinal() >= Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal()) { return; } log.warn("Reprocessing PaymentSentMessage for {} {}", trade.getClass().getSimpleName(), trade.getId()); handle(trade.getBuyer().getPaymentSentMessage(), trade.getBuyer().getPaymentSentMessage().getSenderNodeAddress(), reprocessOnError); } }, trade.getId()); } public void maybeReprocessPaymentReceivedMessage(boolean reprocessOnError) { if (trade.isShutDownStarted()) return; ThreadUtils.execute(() -> { if (trade.isShutDownStarted()) return; synchronized (trade.getLock()) { // skip if no need to reprocess if (trade.isShutDownStarted() || trade.isSeller() || trade.getSeller().getPaymentReceivedMessage() == null || (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && trade.isPayoutPublished())) { return; } log.warn("Reprocessing PaymentReceivedMessage for {} {}", trade.getClass().getSimpleName(), trade.getId()); handle(trade.getSeller().getPaymentReceivedMessage(), trade.getSeller().getPaymentReceivedMessage().getSenderNodeAddress(), reprocessOnError); } }, trade.getId()); } public void maybeReprocessDisputeClosedMessage(boolean reprocessOnError) { if (trade.isShutDownStarted()) return; ThreadUtils.execute(() -> { synchronized (trade.getLock()) { // skip if no need to reprocess if (trade.isShutDownStarted() || trade.isArbitrator() || trade.getArbitrator().getDisputeClosedMessage() == null || trade.getArbitrator().getDisputeClosedMessage().getUnsignedPayoutTxHex() == null || trade.getDisputeState().ordinal() >= Trade.DisputeState.DISPUTE_CLOSED.ordinal()) { return; } log.warn("Reprocessing dispute closed message for {} {}", trade.getClass().getSimpleName(), trade.getId()); HavenoUtils.arbitrationManager.handle(trade.getArbitrator().getDisputeClosedMessage(), reprocessOnError); } }, trade.getId()); } public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { log.info(LOG_HIGHLIGHT + "handleInitMultisigRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender); trade.addInitProgressStep(); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { // check trade if (trade.hasFailed()) { log.warn("{} {} ignoring {} from {} because trade failed with previous error: {}", trade.getClass().getSimpleName(), trade.getId(), request.getClass().getSimpleName(), sender, trade.getErrorMessage()); return; } Validator.checkTradeId(processModel.getOfferId(), request); // process message latchTrade(); processModel.setTradeMessage(request); expect(anyPhase(Trade.Phase.INIT) .with(request) .from(sender)) .setup(tasks( ProcessInitMultisigRequest.class, MaybeSendSignContractRequest.class) .using(new TradeTaskRunner(trade, () -> { startTimeout(); handleTaskRunnerSuccess(sender, request); }, errorMessage -> { handleTaskRunnerFault(sender, request, errorMessage); })) .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) .executeTasks(true); awaitTradeLatch(); } }, trade.getId()); } public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) { log.info(LOG_HIGHLIGHT + "handleSignContractRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { // check trade if (trade.hasFailed()) { log.warn("{} {} ignoring {} from {} because trade failed with previous error: {}", trade.getClass().getSimpleName(), trade.getId(), message.getClass().getSimpleName(), sender, trade.getErrorMessage()); return; } Validator.checkTradeId(processModel.getOfferId(), message); // process message if (trade.getState() == Trade.State.MULTISIG_COMPLETED || trade.getState() == Trade.State.CONTRACT_SIGNATURE_REQUESTED) { latchTrade(); Validator.checkTradeId(processModel.getOfferId(), message); processModel.setTradeMessage(message); expect(anyState(Trade.State.MULTISIG_COMPLETED, Trade.State.CONTRACT_SIGNATURE_REQUESTED) .with(message) .from(sender)) .setup(tasks( // TODO (woodser): validate request ProcessSignContractRequest.class) .using(new TradeTaskRunner(trade, () -> { handleTaskRunnerSuccess(sender, message); }, errorMessage -> { handleTaskRunnerFault(sender, message, errorMessage); }))) .executeTasks(true); awaitTradeLatch(); } else { // process sign contract request after multisig created EasyBind.subscribe(trade.stateProperty(), state -> { if (state == Trade.State.MULTISIG_COMPLETED) ThreadUtils.execute(() -> handleSignContractRequest(message, sender), trade.getId()); // process notification without trade lock }); } } }, trade.getId()); } public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) { log.info(LOG_HIGHLIGHT + "handleSignContractResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender); trade.addInitProgressStep(); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { // check trade if (trade.hasFailed()) { log.warn("{} {} ignoring {} from {} because trade failed with previous error: {}", trade.getClass().getSimpleName(), trade.getId(), message.getClass().getSimpleName(), sender, trade.getErrorMessage()); return; } Validator.checkTradeId(processModel.getOfferId(), message); // process message if (trade.getState() == Trade.State.CONTRACT_SIGNED) { latchTrade(); Validator.checkTradeId(processModel.getOfferId(), message); processModel.setTradeMessage(message); expect(state(Trade.State.CONTRACT_SIGNED) .with(message) .from(sender)) .setup(tasks( // TODO (woodser): validate request SendDepositRequest.class) .using(new TradeTaskRunner(trade, () -> { startTimeout(); handleTaskRunnerSuccess(sender, message); }, errorMessage -> { handleTaskRunnerFault(sender, message, errorMessage); })) .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) // extend timeout .executeTasks(true); awaitTradeLatch(); } else { // process sign contract response after contract signed EasyBind.subscribe(trade.stateProperty(), state -> { if (state == Trade.State.CONTRACT_SIGNED) ThreadUtils.execute(() -> handleSignContractResponse(message, sender), trade.getId()); // process notification without trade lock }); } } }, trade.getId()); } public void handleDepositResponse(DepositResponse response, NodeAddress sender) { log.info(LOG_HIGHLIGHT + "handleDepositResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender); trade.addInitProgressStep(); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { Validator.checkTradeId(processModel.getOfferId(), response); latchTrade(); processModel.setTradeMessage(response); expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_REQUESTED, Trade.Phase.DEPOSITS_PUBLISHED, Trade.Phase.DEPOSITS_CONFIRMED) .with(response) .from(sender)) .setup(tasks( ProcessDepositResponse.class) .using(new TradeTaskRunner(trade, () -> { stopTimeout(); // tasks may complete successfully but process an error if (trade.getInitError() == null) { this.errorMessageHandler = null; // TODO: set this when trade state is >= DEPOSIT_PUBLISHED handleTaskRunnerSuccess(sender, response); if (tradeResultHandler != null) tradeResultHandler.handleResult(trade); // trade is initialized } else { handleTaskRunnerSuccess(sender, response); if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage(trade.getInitError().getMessage()); } this.tradeResultHandler = null; this.errorMessageHandler = null; }, errorMessage -> { handleTaskRunnerFault(sender, response, errorMessage); })) .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) .executeTasks(true); awaitTradeLatch(); } }, trade.getId()); } public void handle(DepositsConfirmedMessage message, NodeAddress sender) { log.info(LOG_HIGHLIGHT + "handle(DepositsConfirmedMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender); if (!trade.isInitialized() || trade.isShutDown()) return; ThreadUtils.execute(() -> { synchronized (trade.getLock()) { if (!trade.isInitialized() || trade.isShutDown()) return; latchTrade(); this.errorMessageHandler = null; expect(new Condition(trade) .with(message) .from(sender)) .setup(tasks( ProcessDepositsConfirmedMessage.class, VerifyPeersAccountAgeWitness.class, MaybeResendDisputeClosedMessageWithPayout.class) .using(new TradeTaskRunner(trade, () -> { handleTaskRunnerSuccess(sender, message); }, errorMessage -> { handleTaskRunnerFault(sender, message, errorMessage); }))) .executeTasks(); awaitTradeLatch(); } }, trade.getId()); } // received by seller and arbitrator protected void handle(PaymentSentMessage message, NodeAddress peer) { handle(message, peer, true); } // received by seller and arbitrator protected void handle(PaymentSentMessage message, NodeAddress peer, boolean reprocessOnError) { log.info(LOG_HIGHLIGHT + "handle(PaymentSentMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + peer); // ignore if not seller or arbitrator if (!(trade instanceof SellerTrade || trade instanceof ArbitratorTrade)) { log.warn("Ignoring PaymentSentMessage since not seller or arbitrator"); return; } // log warning if trade not open if (!processModel.getTradeManager().hasOpenTrade(trade)) { log.warn("We received a PaymentSentMessage for {} {} but it is not an open trade. This can happen if the trade is pending processing as a failed trade.", trade.getClass().getSimpleName(), trade.getId()); } // validate signature try { HavenoUtils.verifyPaymentSentMessage(trade, message); } catch (Throwable t) { log.warn("Ignoring PaymentSentMessage with invalid signature for {} {}, error={}", trade.getClass().getSimpleName(), trade.getId(), t.getMessage()); return; } // set message for reprocessing trade.getBuyer().setPaymentSentMessage(message); // persist trade and return when processing on trade thread ThreadUtils.execute(() -> { // get latest message HavenoUtils.waitFor(100); if (message != trade.getBuyer().getPaymentSentMessage()) { log.info("Ignoring PaymentSentMessage because a newer message was received for {} {}", trade.getClass().getSimpleName(), trade.getId()); return; } // persist before processing on trade thread CountDownLatch initLatch = new CountDownLatch(1); trade.persistNow(() -> { // process message on trade thread if (!trade.isInitialized() || trade.isShutDownStarted()) { initLatch.countDown(); return; } ThreadUtils.execute(() -> { initLatch.countDown(); // We are more tolerant with expected phase and allow also DEPOSITS_PUBLISHED as it can be the case // that the wallet is still syncing and so the DEPOSITS_CONFIRMED state to yet triggered when we received // a mailbox message with PaymentSentMessage. // 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. synchronized (trade.getLock()) { if (!trade.isInitialized() || trade.isShutDownStarted()) return; if (trade.isPaymentSentMessageProcessed()) { log.warn("Received another PaymentSentMessage which was already processed for {} {}, ACKing", trade.getClass().getSimpleName(), trade.getId()); handleTaskRunnerSuccess(trade.getBuyer().getNodeAddress(), message); return; } if (trade.getPayoutTx() != null) { log.warn("We received a PaymentSentMessage but we have already created the payout tx " + "so we ignore the message. This can happen if the ACK message to the peer did not " + "arrive and the peer repeats sending us the message. We send another ACK msg."); sendAckMessage(trade.getBuyer().getNodeAddress(), message, true, null); removeMailboxMessageAfterProcessing(message); return; } latchTrade(); expect(anyPhase() .with(message) .from(peer)) .setup(tasks( ApplyFilter.class, ProcessPaymentSentMessage.class, VerifyPeersAccountAgeWitness.class) .using(new TradeTaskRunner(trade, () -> { handleTaskRunnerSuccess(peer, message); }, (errorMessage) -> { log.warn("Error processing payment sent message: " + errorMessage); processModel.getTradeManager().requestPersistence(); // schedule to reprocess message unless deleted if (trade.getBuyer().getPaymentSentMessage() != null) { UserThread.runAfter(() -> { reprocessPaymentSentMessageCount++; maybeReprocessPaymentSentMessage(reprocessOnError); }, trade.getReprocessDelayInSeconds(reprocessPaymentSentMessageCount)); } else { handleTaskRunnerFault(peer, message, errorMessage); // otherwise send nack } unlatchTrade(); }))) .executeTasks(true); awaitTradeLatch(); } }, trade.getId()); }); HavenoUtils.awaitLatch(initLatch); }, getInitId()); } // received by buyer and arbitrator protected void handle(PaymentReceivedMessage message, NodeAddress peer) { handle(message, peer, true); } private void handle(PaymentReceivedMessage message, NodeAddress peer, boolean reprocessOnError) { log.info(LOG_HIGHLIGHT + "handle(PaymentReceivedMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + peer); // ignore if not buyer or arbitrator if (!(trade instanceof BuyerTrade || trade instanceof ArbitratorTrade)) { log.warn("Ignoring PaymentReceivedMessage since not buyer or arbitrator"); return; } // validate signature try { HavenoUtils.verifyPaymentReceivedMessage(trade, message); } catch (Throwable t) { log.warn("Ignoring PaymentReceivedMessage with invalid signature for {} {}, error={}", trade.getClass().getSimpleName(), trade.getId(), t.getMessage()); return; } // save message for reprocessing trade.getSeller().setPaymentReceivedMessage(message); // process on initialization thread after delay to get latest message ThreadUtils.execute(() -> { // get latest message HavenoUtils.waitFor(100); if (message != trade.getSeller().getPaymentReceivedMessage()) { log.info("Ignoring PaymentReceivedMessage because a newer message was received for {} {}", trade.getClass().getSimpleName(), trade.getId()); return; } // persist trade and return when processing on trade thread CountDownLatch initLatch = new CountDownLatch(1); trade.persistNow(() -> { // process message on trade thread if (!trade.isInitialized() || trade.isShutDownStarted()) { initLatch.countDown(); return; } ThreadUtils.execute(() -> { initLatch.countDown(); synchronized (trade.getLock()) { if (!trade.isInitialized() || trade.isShutDownStarted()) { log.warn("Skipping processing PaymentReceivedMessage because the trade is not initialized or it's shutting down for {} {}", trade.getClass().getSimpleName(), trade.getId()); return; } if (trade.isPaymentReceivedMessageProcessed() && trade.isPayoutPublished()) { log.warn("Received another PaymentReceivedMessage after processed and payout is published for {} {}, ACKing", trade.getClass().getSimpleName(), trade.getId()); handleTaskRunnerSuccess(trade.getSeller().getNodeAddress(), message); return; } if (lastAckedPaymentReceivedMessage != null && lastAckedPaymentReceivedMessage.equals(trade.getSeller().getPaymentReceivedMessage())) { log.warn("Ignoring PaymentReceivedMessage which was already processed and responded to for {} {}", trade.getClass().getSimpleName(), trade.getId()); return; } latchTrade(); Validator.checkTradeId(processModel.getOfferId(), message); processModel.setTradeMessage(message); // check minimum trade phase if (trade.isBuyer() && trade.getPhase().ordinal() < Trade.Phase.PAYMENT_SENT.ordinal()) { log.warn("Received PaymentReceivedMessage before payment sent for {} {}, ignoring", trade.getClass().getSimpleName(), trade.getId()); return; } if (trade.isArbitrator() && trade.getPhase().ordinal() < Trade.Phase.DEPOSITS_CONFIRMED.ordinal()) { log.warn("Received PaymentReceivedMessage before deposits confirmed for {} {}, ignoring", trade.getClass().getSimpleName(), trade.getId()); return; } if (trade.isSeller() && trade.getPhase().ordinal() < Trade.Phase.DEPOSITS_UNLOCKED.ordinal()) { log.warn("Received PaymentReceivedMessage before deposits unlocked for {} {}, ignoring", trade.getClass().getSimpleName(), trade.getId()); return; } expect(anyPhase() .with(message) .from(peer)) .setup(tasks( ProcessPaymentReceivedMessage.class) .using(new TradeTaskRunner(trade, () -> { lastAckedPaymentReceivedMessage = message; handleTaskRunnerSuccess(peer, message); }, errorMessage -> { log.warn("Error processing payment received message: " + errorMessage); processModel.getTradeManager().requestPersistence(); // schedule to reprocess message or nack if (trade.getSeller().getPaymentReceivedMessage() != null) { if (reprocessOnError) { UserThread.runAfter(() -> { reprocessPaymentReceivedMessageCount++; maybeReprocessPaymentReceivedMessage(reprocessOnError); }, trade.getReprocessDelayInSeconds(reprocessPaymentReceivedMessageCount)); } unlatchTrade(); } else { // export fresh multisig info for nack trade.exportMultisigHex(); // handle payout error boolean isFirstNack = lastAckedPaymentReceivedMessage == null; // TODO: this is "poor man's" first nack because it's only in memory lastAckedPaymentReceivedMessage = message; trade.onPayoutError(false, false, null); handleTaskRunnerFault(peer, message, null, errorMessage, trade.getSelf().getUpdatedMultisigHex(), !isFirstNack); // send nack } }))) .executeTasks(true); awaitTradeLatch(); } }, trade.getId()); }); HavenoUtils.awaitLatch(initLatch); }, getInitId()); } public void onWithdrawCompleted() { log.info("Withdraw completed"); } /////////////////////////////////////////////////////////////////////////////////////////// // FluentProtocol /////////////////////////////////////////////////////////////////////////////////////////// // We log an error if condition is not met and call the protocol error handler protected FluentProtocol expect(FluentProtocol.Condition condition) { return new FluentProtocol(this) .condition(condition) .resultHandler(result -> { if (!result.isValid()) { log.warn(result.getInfo()); handleTaskRunnerFault(null, null, result.name(), result.getInfo(), null); } }); } // We execute only if condition is met but do not log an error. protected FluentProtocol given(FluentProtocol.Condition condition) { return new FluentProtocol(this) .condition(condition); } protected FluentProtocol.Condition phase(Trade.Phase expectedPhase) { return new FluentProtocol.Condition(trade).phase(expectedPhase); } protected FluentProtocol.Condition anyPhase(Trade.Phase... expectedPhases) { return new FluentProtocol.Condition(trade).anyPhase(expectedPhases); } protected FluentProtocol.Condition state(Trade.State expectedState) { return new FluentProtocol.Condition(trade).state(expectedState); } protected FluentProtocol.Condition anyState(Trade.State... expectedStates) { return new FluentProtocol.Condition(trade).anyState(expectedStates); } @SafeVarargs public final FluentProtocol.Setup tasks(Class>... tasks) { return new FluentProtocol.Setup(this, trade).tasks(tasks); } /////////////////////////////////////////////////////////////////////////////////////////// // ACK msg /////////////////////////////////////////////////////////////////////////////////////////// private void onAckMessage(AckMessage ackMessage, NodeAddress sender) { boolean processOnTradeThread = !ackMessage.getSourceMsgClassName().equals(ChatMessage.class.getSimpleName()); // handle chat message acks off trade thread for responsiveness if the thread is busy if (processOnTradeThread) { ThreadUtils.execute(() -> onAckMessageAux(ackMessage, sender), trade.getId()); } else { onAckMessageAux(ackMessage, sender); } } // TODO: this has grown in complexity over time and could use refactoring private void onAckMessageAux(AckMessage ackMessage, NodeAddress sender) { // ignore if trade is completely finished if (trade.isFinished()) return; // get trade peer TradePeer peer = trade.getTradePeer(sender); if (peer == null) peer = getTradePeer(ackMessage); if (peer == null) { if (ackMessage.isSuccess()) log.warn("Received AckMessage from unknown peer for {}, sender={}, trade={} {}, messageUid={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid()); else log.warn("Received AckMessage with error state from unknown peer for {}, sender={}, trade={} {}, messageUid={}, errorMessage={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.getErrorMessage()); return; } // update sender's node address if (!peer.getNodeAddress().equals(sender)) { log.info("Updating peer's node address from {} to {} using ACK message to {}", peer.getNodeAddress(), sender, ackMessage.getSourceMsgClassName()); peer.setNodeAddress(sender); } // handle nack of InitTradeRequest from arbitrator to maker if (!ackMessage.isSuccess() && trade.isMaker() && peer == trade.getArbitrator() && ackMessage.getSourceMsgClassName().equals(InitTradeRequest.class.getSimpleName())) { if (ignoreInitTradeRequestNackFromArbitrator(ackMessage)) { log.warn("Ignoring InitTradeRequest NACK from arbitrator, offerId={}, errorMessage={}", processModel.getOfferId(), ackMessage.getErrorMessage()); // use default postprocessing } else { if (makerInitTradeRequestHasBeenNacked) { handleSecondMakerInitTradeRequestNack(ackMessage); // use default postprocessing } else { makerInitTradeRequestHasBeenNacked = true; handleFirstMakerInitTradeRequestNack(ackMessage); return; } } } // handle nack of deposit request if (ackMessage.getSourceMsgClassName().equals(DepositRequest.class.getSimpleName())) { if (!ackMessage.isSuccess()) { trade.setStateIfValidTransitionTo(Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED); processModel.getTradeManager().requestPersistence(); } } // handle ack message for DepositsConfirmedMessage, which automatically re-sends if not ACKed in a certain time if (ackMessage.getSourceMsgClassName().equals(DepositsConfirmedMessage.class.getSimpleName())) { peer.setDepositsConfirmedAckMessage(ackMessage); processModel.getTradeManager().requestPersistence(); } // handle ack message for PaymentSentMessage, which automatically re-sends if not ACKed in a certain time if (ackMessage.getSourceMsgClassName().equals(PaymentSentMessage.class.getSimpleName())) { if (!trade.isPaymentMarkedSent()) { log.warn("Received AckMessage for PaymentSentMessage but trade is in unexpected state, ignoring. Sender={}, trade={} {}, state={}, success={}, error={}, messageUid={}", sender, trade.getClass().getSimpleName(), trade.getId(), trade.getState(), ackMessage.isSuccess(), ackMessage.getErrorMessage(), ackMessage.getSourceUid()); return; } if (peer == trade.getSeller()) { trade.getSeller().setPaymentSentAckMessage(ackMessage); if (ackMessage.isSuccess()) trade.setStateIfValidTransitionTo(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG); else trade.setState(Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG); processModel.getTradeManager().requestPersistence(); } else if (peer == trade.getArbitrator()) { trade.getArbitrator().setPaymentSentAckMessage(ackMessage); processModel.getTradeManager().requestPersistence(); } else { log.warn("Received AckMessage from unexpected peer. Sender={}, trade={} {}, state={}, success={}, error={}, messageUid={}", sender, trade.getClass().getSimpleName(), trade.getId(), trade.getState(), ackMessage.isSuccess(), ackMessage.getErrorMessage(), ackMessage.getSourceUid()); return; } } // handle ack message for PaymentReceivedMessage, which automatically re-sends if not ACKed in a certain time // TODO: trade state can be reset twice if both peers nack before published payout is detected // TODO: do not reset state if payment received message is acknowledged because payout is likely broadcast? if (ackMessage.getSourceMsgClassName().equals(PaymentReceivedMessage.class.getSimpleName())) { // ack message from buyer if (peer == trade.getBuyer()) { trade.getBuyer().setPaymentReceivedAckMessage(ackMessage); processModel.getTradeManager().persistNow(null); // handle successful ack if (ackMessage.isSuccess()) { // validate state if (!trade.isPaymentMarkedReceived()) { log.warn("Received AckMessage for PaymentReceivedMessage but trade is in unexpected state, ignoring. Sender={}, trade={} {}, state={}, success={}, error={}, messageUid={}", sender, trade.getClass().getSimpleName(), trade.getId(), trade.getState(), ackMessage.isSuccess(), ackMessage.getErrorMessage(), ackMessage.getSourceUid()); return; } trade.setStateIfValidTransitionTo(Trade.State.BUYER_RECEIVED_PAYMENT_RECEIVED_MSG); processModel.getTradeManager().persistNow(null); } // handle nack else { log.warn("We received a NACK for our PaymentReceivedMessage to the buyer for {} {}: {}", trade.getClass().getSimpleName(), trade.getId(), ackMessage.getErrorMessage()); // nack includes updated multisig hex since v1.1.1 if (ackMessage.getUpdatedMultisigHex() != null) { trade.getBuyer().setUpdatedMultisigHex(ackMessage.getUpdatedMultisigHex()); processModel.getTradeManager().persistNow(null); onPaymentReceivedNack(true, peer, ackMessage); return; // skip remaining processing } } } // ack message from arbitrator else if (peer == trade.getArbitrator()) { trade.getArbitrator().setPaymentReceivedAckMessage(ackMessage); processModel.getTradeManager().persistNow(null); // handle nack if (!ackMessage.isSuccess()) { log.warn("We received a NACK for our PaymentReceivedMessage to the arbitrator for {} {}: {}", trade.getClass().getSimpleName(), trade.getId(), ackMessage.getErrorMessage()); // nack includes updated multisig hex since v1.1.1 if (ackMessage.getUpdatedMultisigHex() != null) { trade.getArbitrator().setUpdatedMultisigHex(ackMessage.getUpdatedMultisigHex()); processModel.getTradeManager().persistNow(null); onPaymentReceivedNack(true, peer, ackMessage); return; // skip remaining processing } } } else { log.warn("Received AckMessage from unexpected peer. Sender={}, trade={} {}, state={}, success={}, error={}, messageUid={}", sender, trade.getClass().getSimpleName(), trade.getId(), trade.getState(), ackMessage.isSuccess(), ackMessage.getErrorMessage(), ackMessage.getSourceUid()); return; } // clear and shut down trade if completely finished after ack if (trade.isFinished()) { log.info("Trade {} {} is finished after PaymentReceivedMessage ACK, shutting it down", trade.getClass().getSimpleName(), trade.getId()); trade.clearAndShutDown(); } } // generic handling if (ackMessage.isSuccess()) { log.info("Received AckMessage for {}, sender={}, trade={} {}, messageUid={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid()); } else { log.warn("Received AckMessage with error state for {}, sender={}, trade={} {}, messageUid={}, errorMessage={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.getErrorMessage()); handleError("Your peer had a problem processing your message. Please ensure you and your peer are running the latest version and try again.\n\nError details:\n" + ackMessage.getErrorMessage()); } // notify trade listeners trade.onAckMessage(ackMessage, sender); } private TradePeer getTradePeer(AckMessage ackMessage) { Class[] messageClasses = {DepositsConfirmedMessage.class, PaymentSentMessage.class, PaymentReceivedMessage.class}; // TODO: same for DisputeOpenedMessage, DisputeClosedMessage? TradePeer[] peers = {trade.getArbitrator(), trade.getMaker(), trade.getTaker()}; for (Class messageClass : messageClasses) { for (TradePeer peer : peers) { String expectedUid = HavenoUtils.getDeterministicId(trade, messageClass, peer.getNodeAddress()); if (ackMessage.getSourceUid().equals(expectedUid)) { return peer; } } } return null; } private static boolean ignoreInitTradeRequestNackFromArbitrator(AckMessage ackMessage) { return ackMessage.getErrorMessage() != null && ackMessage.getErrorMessage().contains(SEND_INIT_TRADE_REQUEST_FAILED); // ignore if arbitrator's request failed to taker } private boolean onPaymentReceivedNack(boolean syncAndPoll, TradePeer peer, AckMessage ackMessage) { // prevent infinite nack loop with max attempts numPaymentReceivedNacks++; if (numPaymentReceivedNacks > MAX_ATTEMPTS) { String errorMsg = "Failed to process the payment confirmation after " + MAX_ATTEMPTS + " attempts for " + trade.getClass().getSimpleName() + " " + trade.getId() + ". Restart the application to try again.\n\nMessage from peer:\n" + ackMessage.getErrorMessage(); log.warn(errorMsg); trade.setErrorMessage(errorMsg); return false; } // handle payout error return trade.onPayoutError(syncAndPoll, true, peer); } private void handleFirstMakerInitTradeRequestNack(AckMessage ackMessage) { log.warn("Maker received NACK to InitTradeRequest from arbitrator for {} {}, messageUid={}, errorMessage={}", trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.getErrorMessage()); ThreadUtils.execute(() -> { Event event = new Event() { @Override public String name() { return "MakerRecreateReserveTx"; } }; synchronized (trade.getLock()) { latchTrade(); expect(phase(Trade.Phase.INIT) .with(event)) .setup(tasks( MakerRecreateReserveTx.class, MakerSendInitTradeRequestToArbitrator.class) .using(new TradeTaskRunner(trade, () -> { startTimeout(); unlatchTrade(); }, errorMessage -> { handleError("Failed to re-send InitTradeRequest to arbitrator for " + trade.getClass().getSimpleName() + " " + trade.getId() + ": " + errorMessage); })) .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) .executeTasks(true); awaitTradeLatch(); } }, trade.getId()); } private void handleSecondMakerInitTradeRequestNack(AckMessage ackMessage) { log.warn("Maker received 2nd NACK to InitTradeRequest from arbitrator for {} {}, messageUid={}, errorMessage={}", trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.getErrorMessage()); String warningMessage = "Your offer (" + trade.getOffer().getShortId() + ") has been removed because there was a problem taking the trade.\n\nError message: " + ackMessage.getErrorMessage(); OpenOffer openOffer = HavenoUtils.openOfferManager.getOpenOffer(trade.getId()).orElse(null); if (openOffer != null) { HavenoUtils.openOfferManager.cancelOpenOffer(openOffer, null, null); HavenoUtils.setTopError(warningMessage); } log.warn(warningMessage); } protected void sendAckMessage(NodeAddress peer, TradeMessage message, boolean result, @Nullable String errorMessage) { sendAckMessage(peer, message, result, errorMessage, null); } protected void sendAckMessage(NodeAddress peer, TradeMessage message, boolean result, @Nullable String errorMessage, String updatedMultisigHex) { // get peer's pub key ring PubKeyRing peersPubKeyRing = getPeersPubKeyRing(peer); if (peersPubKeyRing == null) { log.error("We cannot send the ACK message as peersPubKeyRing is null"); return; } // send ack message processModel.getTradeManager().sendAckMessage(peer, peersPubKeyRing, message, result, errorMessage, updatedMultisigHex); } /////////////////////////////////////////////////////////////////////////////////////////// // Timeout /////////////////////////////////////////////////////////////////////////////////////////// public synchronized void startTimeout() { startTimeout(TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS); } public synchronized void startTimeout(long timeoutSec) { synchronized (timeoutTimerLock) { stopTimeout(); timeoutTimer = UserThread.runAfter(() -> { handleError(TIMEOUT_REACHED + " Protocol did not complete in " + timeoutSec + " sec. TradeID=" + trade.getId() + ", state=" + trade.stateProperty().get()); }, timeoutSec); } } public synchronized void stopTimeout() { synchronized (timeoutTimerLock) { if (timeoutTimer != null) { timeoutTimer.stop(); timeoutTimer = null; } } } public static boolean isTimeoutError(String errorMessage) { return errorMessage != null && errorMessage.contains(TIMEOUT_REACHED); } /////////////////////////////////////////////////////////////////////////////////////////// // Task runner /////////////////////////////////////////////////////////////////////////////////////////// protected void handleTaskRunnerSuccess(NodeAddress sender, TradeMessage message) { handleTaskRunnerSuccess(sender, message, message.getClass().getSimpleName()); } protected void handleTaskRunnerSuccess(FluentProtocol.Event event) { handleTaskRunnerSuccess(null, null, event.name()); } protected void handleTaskRunnerFault(NodeAddress sender, TradeMessage message, String errorMessage) { handleTaskRunnerFault(sender, message, message.getClass().getSimpleName(), errorMessage, null); } protected void handleTaskRunnerFault(FluentProtocol.Event event, String errorMessage) { handleTaskRunnerFault(null, null, event.name(), errorMessage, null); } /////////////////////////////////////////////////////////////////////////////////////////// // Validation /////////////////////////////////////////////////////////////////////////////////////////// private PubKeyRing getPeersPubKeyRing(NodeAddress address) { trade.setMyNodeAddress(); // TODO: this is a hack to update my node address before verifying the message TradePeer peer = trade.getTradePeer(address); if (peer == null) { log.warn("Cannot get peer's pub key ring because peer is not maker, taker, or arbitrator. Their address might have changed: " + address); return null; } return peer.getPubKeyRing(); } public boolean isPubKeyValid(DecryptedMessageWithPubKey message) { if (this instanceof ArbitratorProtocol) { // valid if traders unknown if (trade.getMaker().getPubKeyRing() == null || trade.getTaker().getPubKeyRing() == null) return true; // valid if maker pub key if (message.getSignaturePubKey().equals(trade.getMaker().getPubKeyRing().getSignaturePubKey())) return true; // valid if taker pub key if (message.getSignaturePubKey().equals(trade.getTaker().getPubKeyRing().getSignaturePubKey())) return true; } else { // valid if arbitrator or peer unknown if (trade.getArbitrator().getPubKeyRing() == null || (trade.getTradePeer() == null || trade.getTradePeer().getPubKeyRing() == null)) return true; // valid if arbitrator's pub key ring if (message.getSignaturePubKey().equals(trade.getArbitrator().getPubKeyRing().getSignaturePubKey())) return true; // valid if peer's pub key ring if (message.getSignaturePubKey().equals(trade.getTradePeer().getPubKeyRing().getSignaturePubKey())) return true; } // invalid return false; } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// protected void handleTaskRunnerSuccess(NodeAddress sender, @Nullable TradeMessage message, String source) { log.info("TaskRunner successfully completed. Triggered from {}, tradeId={}", source, trade.getId()); if (message != null) { sendAckMessage(sender, message, true, null); // Once a taskRunner is completed we remove the mailbox message. To not remove it directly at the task // adds some resilience in case of minor errors, so after a restart the mailbox message can be applied // again. removeMailboxMessageAfterProcessing(message); } unlatchTrade(); } void handleTaskRunnerFault(NodeAddress ackReceiver, @Nullable TradeMessage message, String source, String errorMessage, String updatedMultisigHex) { handleTaskRunnerFault(ackReceiver, message, source, errorMessage, updatedMultisigHex, true); } void handleTaskRunnerFault(NodeAddress ackReceiver, @Nullable TradeMessage message, String source, String errorMessage, String updatedMultisigHex, boolean setTradeError) { log.error("Task runner failed with error {}. Triggered from {}. Monerod={}" , errorMessage, source, trade.getXmrWalletService().getXmrConnectionService().getConnection()); handleError(errorMessage, setTradeError); if (message != null) { sendAckMessage(ackReceiver, message, false, errorMessage, updatedMultisigHex); } } // NOTE: these are not thread safe, so they must be used within a lock on the trade protected void handleError(String errorMessage) { handleError(errorMessage, true); } protected void handleError(String errorMessage, boolean setTradeError) { stopTimeout(); log.error(errorMessage); if (setTradeError) trade.setErrorMessage(errorMessage); processModel.getTradeManager().requestPersistence(); unlatchTrade(); if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage(errorMessage); errorMessageHandler = null; } protected void latchTrade() { trade.awaitInitialized(); if (tradeLatch != null) throw new RuntimeException("Trade latch is not null. That should never happen."); if (trade.isShutDown()) throw new RuntimeException("Cannot latch trade " + trade.getId() + " for protocol because it's shut down"); tradeLatch = new CountDownLatch(1); } protected void unlatchTrade() { CountDownLatch lastLatch = tradeLatch; tradeLatch = null; if (lastLatch != null) lastLatch.countDown(); } protected void awaitTradeLatch() { if (tradeLatch == null) return; HavenoUtils.awaitLatch(tradeLatch); } private boolean isMyMessage(NetworkEnvelope message) { if (message instanceof TradeMessage) { TradeMessage tradeMessage = (TradeMessage) message; return tradeMessage.getOfferId().equals(trade.getId()); } else if (message instanceof AckMessage) { AckMessage ackMessage = (AckMessage) message; return ackMessage.getSourceType() == AckMessageSourceType.TRADE_MESSAGE && ackMessage.getSourceId().equals(trade.getId()); } else { return false; } } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/TradeProtocolFactory.java ================================================ /* * 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 . */ package haveno.core.trade.protocol; import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.BuyerAsMakerTrade; import haveno.core.trade.BuyerAsTakerTrade; import haveno.core.trade.SellerAsMakerTrade; import haveno.core.trade.SellerAsTakerTrade; import haveno.core.trade.Trade; public class TradeProtocolFactory { public static TradeProtocol getNewTradeProtocol(Trade trade) { if (trade instanceof BuyerAsMakerTrade) { return new BuyerAsMakerProtocol((BuyerAsMakerTrade) trade); } else if (trade instanceof BuyerAsTakerTrade) { return new BuyerAsTakerProtocol((BuyerAsTakerTrade) trade); } else if (trade instanceof SellerAsMakerTrade) { return new SellerAsMakerProtocol((SellerAsMakerTrade) trade); } else if (trade instanceof SellerAsTakerTrade) { return new SellerAsTakerProtocol((SellerAsTakerTrade) trade); } else if (trade instanceof ArbitratorTrade) { return new ArbitratorProtocol((ArbitratorTrade) trade); } else { throw new IllegalStateException("Trade not of expected type. Trade=" + trade); } } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/TradeTaskRunner.java ================================================ /* * 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 . */ package haveno.core.trade.protocol; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; public class TradeTaskRunner extends TaskRunner { public TradeTaskRunner(Trade sharedModel, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { //noinspection unchecked super(sharedModel, (Class) Trade.class, resultHandler, errorMessageHandler); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/TraderProtocol.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol; import haveno.core.trade.messages.DepositResponse; import haveno.core.trade.messages.SignContractResponse; import haveno.network.p2p.NodeAddress; public interface TraderProtocol { public void handleSignContractResponse(SignContractResponse message, NodeAddress peer); public void handleDepositResponse(DepositResponse response, NodeAddress peer); } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/ApplyFilter.java ================================================ /* * 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 . */ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; import haveno.core.filter.FilterManager; import haveno.core.trade.Trade; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class ApplyFilter extends TradeTask { public ApplyFilter(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); NodeAddress nodeAddress = checkNotNull(processModel.getTempTradePeerNodeAddress()); FilterManager filterManager = processModel.getFilterManager(); if (filterManager.isNodeAddressBanned(nodeAddress)) { failed("Other trader is banned by their node address.\n" + "tradePeerNodeAddress=" + nodeAddress); } else if (filterManager.isOfferIdBanned(trade.getId())) { failed("Offer ID is banned.\n" + "Offer ID=" + trade.getId()); } else if (trade.getOffer() != null && filterManager.isCurrencyBanned(trade.getOffer().getCounterCurrencyCode())) { failed("Currency is banned.\n" + "Currency code=" + trade.getOffer().getCounterCurrencyCode()); } else if (filterManager.isPaymentMethodBanned(checkNotNull(trade.getOffer()).getPaymentMethod())) { failed("Payment method is banned.\n" + "Payment method=" + trade.getOffer().getPaymentMethod().getId()); } else if (filterManager.requireUpdateToNewVersionForTrading()) { failed("Your version of Haveno is not compatible for trading anymore. " + "Please update to the latest Haveno version."); } else { complete(); } } catch (Throwable t) { failed(t); } } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import common.utils.JsonUtils; import haveno.common.app.Version; import haveno.common.crypto.PubKeyRing; import haveno.common.taskrunner.TaskRunner; import haveno.core.offer.Offer; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.messages.DepositRequest; import haveno.core.trade.messages.DepositResponse; import haveno.core.trade.protocol.TradePeer; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.SendDirectMessageListener; import lombok.extern.slf4j.Slf4j; import monero.daemon.MoneroDaemon; import monero.daemon.model.MoneroSubmitTxResult; import monero.daemon.model.MoneroTx; import java.math.BigInteger; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.UUID; @Slf4j public class ArbitratorProcessDepositRequest extends TradeTask { private boolean depositResponsesSent; @SuppressWarnings({"unused"}) public ArbitratorProcessDepositRequest(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); // check if trade is failed if (trade.getState() == Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED) throw new RuntimeException("Cannot process deposit request because trade is already failed, tradeId=" + trade.getId()); // update trade state trade.setStateIfValidTransitionTo(Trade.State.SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST); processModel.getTradeManager().requestPersistence(); // process request processDepositRequest(); complete(); } catch (Throwable t) { trade.getProcessModel().error = t; log.error("Error processing deposit request for {} {}: {}\n", trade.getClass().getSimpleName(), trade.getId(), t.getMessage(), t); trade.setStateIfValidTransitionTo(Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED); failed(t); } processModel.getTradeManager().requestPersistence(); } private void processDepositRequest() { // get contract and signature String contractAsJson = trade.getContractAsJson(); DepositRequest request = (DepositRequest) processModel.getTradeMessage(); // TODO (woodser): verify response byte[] signature = request.getContractSignature(); // get trader info TradePeer sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); if (sender == null) throw new RuntimeException(request.getClass().getSimpleName() + " is not from maker, taker, or arbitrator"); PubKeyRing senderPubKeyRing = sender.getPubKeyRing(); // verify signature if (!HavenoUtils.isSignatureValid(senderPubKeyRing, contractAsJson, signature)) { throw new RuntimeException("Peer's contract signature is invalid"); } // set peer's signature sender.setContractSignature(signature); // subscribe to trade state once to send responses with ack or nack if (!hasBothContractSignatures()) { trade.stateProperty().addListener((obs, oldState, newState) -> { if (oldState == newState) return; if (newState == Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED) { sendDepositResponsesOnce(trade.getProcessModel().error == null ? "Arbitrator failed to publish deposit txs within timeout for trade " + trade.getId() : trade.getProcessModel().error.getMessage()); } else if (newState.ordinal() >= Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS.ordinal()) { sendDepositResponsesOnce(null); } }); } // collect expected values Offer offer = trade.getOffer(); boolean isFromTaker = sender == trade.getTaker(); boolean isFromBuyer = sender == trade.getBuyer(); BigInteger tradeFee = isFromTaker ? trade.getTakerFee() : trade.getMakerFee(); BigInteger sendTradeAmount = isFromBuyer ? BigInteger.ZERO : trade.getAmount(); BigInteger securityDepositBeforeMiningFee = isFromBuyer ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); String depositAddress = processModel.getMultisigAddress(); sender.setSecurityDeposit(securityDepositBeforeMiningFee); // verify deposit tx boolean isFromBuyerAsTakerWithoutDeposit = isFromBuyer && isFromTaker && trade.hasBuyerAsTakerWithoutDeposit(); if (!isFromBuyerAsTakerWithoutDeposit) { try { MoneroTx verifiedTx = trade.getXmrWalletService().verifyDepositTx( offer.getId(), tradeFee, trade.getProcessModel().getTradeFeeAddress(), sendTradeAmount, securityDepositBeforeMiningFee, depositAddress, sender.getDepositTxHash(), request.getDepositTxHex(), request.getDepositTxKey(), null); // TODO: it seems a deposit tx had 0 fee once? if (BigInteger.ZERO.equals(verifiedTx.getFee())) { String errorMessage = "Deposit transaction from " + (isFromTaker ? "taker" : "maker") + " has 0 fee for trade " + trade.getId() + ". This should never happen."; log.warn(errorMessage + "\n" + verifiedTx); throw new RuntimeException(errorMessage); } // update trade state sender.setSecurityDeposit(securityDepositBeforeMiningFee.subtract(verifiedTx.getFee())); // subtract mining fee from security deposit sender.setDepositTxFee(verifiedTx.getFee()); sender.setDepositTxHex(request.getDepositTxHex()); sender.setDepositTxKey(request.getDepositTxKey()); } catch (Exception e) { throw new RuntimeException("Error processing deposit tx from " + (isFromTaker ? "taker " : "maker ") + sender.getNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage(), e); } } // update trade state if (request.getPaymentAccountKey() != null) sender.setPaymentAccountKey(request.getPaymentAccountKey()); processModel.getTradeManager().requestPersistence(); // relay deposit txs when both requests received MoneroDaemon monerod = trade.getXmrWalletService().getMonerod(); if (hasBothContractSignatures()) { // check timeout and extend just before relaying if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out before relaying deposit txs for {} {}" + trade.getClass().getSimpleName() + " " + trade.getShortId()); trade.addInitProgressStep(); // relay deposit txs boolean depositTxsRelayed = false; List txHashes = new ArrayList<>(); try { // submit maker tx to pool but do not relay MoneroSubmitTxResult makerResult = monerod.submitTxHex(processModel.getMaker().getDepositTxHex(), true); if (!makerResult.isGood()) throw new RuntimeException("Error submitting maker deposit tx: " + JsonUtils.serialize(makerResult)); txHashes.add(processModel.getMaker().getDepositTxHash()); // submit taker tx to pool but do not relay if (!trade.hasBuyerAsTakerWithoutDeposit()) { MoneroSubmitTxResult takerResult = monerod.submitTxHex(processModel.getTaker().getDepositTxHex(), true); if (!takerResult.isGood()) throw new RuntimeException("Error submitting taker deposit tx: " + JsonUtils.serialize(takerResult)); txHashes.add(processModel.getTaker().getDepositTxHash()); } // relay txs try { monerod.relayTxsByHash(txHashes); // call will error if txs are already confirmed, but they're still relayed } catch (Exception e) { log.warn("Error relaying deposit txs for trade {}. They could already be confirmed. Error={}", trade.getId(), e.getMessage()); } depositTxsRelayed = true; // update trade state log.info("Arbitrator published deposit txs for trade " + trade.getId()); trade.setStateIfValidTransitionTo(Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS); } catch (Exception e) { log.warn("Arbitrator error publishing deposit txs for trade {} {}: {}\n", trade.getClass().getSimpleName(), trade.getShortId(), e.getMessage(), e); if (!depositTxsRelayed) { // flush txs from pool try { monerod.flushTxPool(txHashes); } catch (Exception e2) { log.warn("Error flushing deposit txs from pool for trade {}: {}\n", trade.getId(), e2.getMessage(), e2); } } throw e; } } else { if (processModel.getMaker().getDepositTxHex() == null) log.info("Arbitrator waiting for deposit request from maker for trade " + trade.getId()); if (processModel.getTaker().getDepositTxHex() == null && !trade.hasBuyerAsTakerWithoutDeposit()) log.info("Arbitrator waiting for deposit request from taker for trade " + trade.getId()); } } private boolean hasBothContractSignatures() { return processModel.getMaker().getContractSignature() != null && processModel.getTaker().getContractSignature() != null; } private boolean isTimedOut() { return !processModel.getTradeManager().hasOpenTrade(trade); } private synchronized void sendDepositResponsesOnce(String errorMessage) { // skip if sent if (depositResponsesSent) return; depositResponsesSent = true; // log error if (errorMessage != null) { log.warn("Sending deposit responses for tradeId={}, error={}", trade.getId(), errorMessage); } // create deposit response DepositResponse response = new DepositResponse( trade.getOffer().getId(), UUID.randomUUID().toString(), Version.getP2PMessageVersion(), new Date().getTime(), errorMessage, trade.getBuyer().getSecurityDeposit().longValue(), trade.getSeller().getSecurityDeposit().longValue()); // send deposit response to maker and taker sendDepositResponse(trade.getMaker().getNodeAddress(), trade.getMaker().getPubKeyRing(), response); sendDepositResponse(trade.getTaker().getNodeAddress(), trade.getTaker().getPubKeyRing(), response); } private void sendDepositResponse(NodeAddress nodeAddress, PubKeyRing pubKeyRing, DepositResponse response) { log.info("Sending deposit response to trader={}; offerId={}, error={}", nodeAddress, trade.getId(), response.getErrorMessage()); processModel.getP2PService().sendEncryptedDirectMessage(nodeAddress, pubKeyRing, response, new SendDirectMessageListener() { @Override public void onArrived() { log.info("{} arrived: trading peer={}; offerId={}; uid={}", response.getClass().getSimpleName(), nodeAddress, trade.getId(), trade.getUid()); } @Override public void onFault(String errorMessage) { log.error("Sending {} failed: uid={}; peer={}; error={}", response.getClass().getSimpleName(), nodeAddress, trade.getId(), errorMessage); appendToErrorMessage("Sending message failed: message=" + response + "\nerrorMessage=" + errorMessage); } }); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.protocol.TradePeer; import lombok.extern.slf4j.Slf4j; import monero.daemon.model.MoneroTx; import java.math.BigInteger; import org.apache.commons.lang3.exception.ExceptionUtils; /** * Arbitrator verifies reserve tx from maker or taker. * * The maker reserve tx is only verified here if this arbitrator is not * the original offer signer and thus does not have the original reserve tx. */ @Slf4j public class ArbitratorProcessReserveTx extends TradeTask { @SuppressWarnings({"unused"}) public ArbitratorProcessReserveTx(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); Offer offer = trade.getOffer(); InitTradeRequest request = (InitTradeRequest) processModel.getTradeMessage(); TradePeer sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); boolean isFromMaker = sender == trade.getMaker(); boolean isFromBuyer = isFromMaker ? offer.getDirection() == OfferDirection.BUY : offer.getDirection() == OfferDirection.SELL; sender = isFromMaker ? processModel.getMaker() : processModel.getTaker(); BigInteger securityDeposit = isFromMaker ? isFromBuyer ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit() : isFromBuyer ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); sender.setSecurityDeposit(securityDeposit); // TODO (woodser): if signer online, should never be called by maker? // process reserve tx unless from buyer as taker without deposit boolean isFromBuyerAsTakerWithoutDeposit = isFromBuyer && !isFromMaker && trade.hasBuyerAsTakerWithoutDeposit(); if (!isFromBuyerAsTakerWithoutDeposit) { // process reserve tx with expected values BigInteger penaltyFee = HavenoUtils.multiply(securityDeposit, offer.getPenaltyFeePct()); BigInteger tradeFee = isFromMaker ? offer.getMaxMakerFee() : trade.getTakerFee(); BigInteger sendAmount = isFromBuyer ? BigInteger.ZERO : isFromMaker ? offer.getAmount() : trade.getAmount(); // maker reserve tx is for offer amount try { MoneroTx verifiedTx = trade.getXmrWalletService().verifyReserveTx( offer.getId(), penaltyFee, tradeFee, sendAmount, securityDeposit, request.getPayoutAddress(), request.getReserveTxHash(), request.getReserveTxHex(), request.getReserveTxKey(), null); // TODO: it seems a deposit tx had 0 fee once? if (BigInteger.ZERO.equals(verifiedTx.getFee())) { String errorMessage = "Reserve transaction from " + (isFromMaker ? "maker" : "taker") + " has 0 fee for trade " + trade.getId() + ". This should never happen."; log.warn(errorMessage + "\n" + verifiedTx); throw new RuntimeException(errorMessage); } // save reserve tx to model sender.setSecurityDeposit(sender.getSecurityDeposit().subtract(verifiedTx.getFee())); // subtract mining fee from security deposit sender.setReserveTxHash(request.getReserveTxHash()); sender.setReserveTxHex(request.getReserveTxHex()); sender.setReserveTxKey(request.getReserveTxKey()); } catch (Exception e) { log.error(ExceptionUtils.getStackTrace(e)); throw new RuntimeException("Error processing reserve tx from " + (isFromMaker ? "maker " : "taker ") + processModel.getTempTradePeerNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage()); } } // persist trade processModel.getTradeManager().requestPersistence(); complete(); } catch (Throwable t) { failed(t); } } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorSendDisputeOpenedMessage.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import java.util.Optional; import java.util.concurrent.TimeUnit; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.taskrunner.TaskRunner; import haveno.core.network.MessageState; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.messages.DisputeOpenedMessage; import haveno.core.support.messages.ChatMessage; import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.messages.TradeMailboxMessage; import haveno.network.p2p.mailbox.MailboxMessage; import javafx.beans.value.ChangeListener; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; /** * Arbitrator sends the DisputeOpenedMessage. * We wait to receive a ACK message back and resend the message * in case that does not happen in 10 minutes or if the message was stored in mailbox or failed. We keep repeating that * with doubling the interval each time and until the MAX_RESEND_ATTEMPTS is reached. * If never successful we give up and complete. It might be a valid case that the peer was not online for an extended * time but we can be very sure that our message was stored as mailbox message in the network and one the peer goes * online he will process it. */ @Slf4j @EqualsAndHashCode(callSuper = true) public abstract class ArbitratorSendDisputeOpenedMessage extends SendMailboxMessageTask { private ChangeListener listener; private Timer timer; private static final int MAX_RESEND_ATTEMPTS = 20; private long delayInMin = -1; private int resendCounter = 0; private DisputeOpenedMessage message = null; public ArbitratorSendDisputeOpenedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); // reset nack state if (getReceiver().isDisputeOpenedMessageAckedOrNacked()) { getReceiver().setDisputeOpenedMessageState(MessageState.UNDEFINED); } // skip if not applicable or already acked if (stopSending()) { if (!isCompleted()) complete(); return; } // reset ack state getReceiver().setPaymentReceivedMessageState(MessageState.UNDEFINED); super.run(); } catch (Throwable t) { failed(t); } } protected Optional getDispute() { return HavenoUtils.arbitrationManager.findDispute(getReceiver().getDisputeOpenedMessage().getDispute()); } protected ChatMessage getSystemChatMessage() { return getDispute().get().getChatMessages().get(0); } @Override protected MailboxMessage getMailboxMessage(String tradeId) { if (message == null) message = getReceiver().getDisputeOpenedMessage(); return message; } @Override protected void setStateSent() { getReceiver().setDisputeOpenedMessageState(MessageState.SENT); tryToSendAgainLater(); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateArrived() { getReceiver().setDisputeOpenedMessageState(MessageState.ARRIVED); getSystemChatMessage().setArrived(true); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateStoredInMailbox() { getReceiver().setDisputeOpenedMessageState(MessageState.STORED_IN_MAILBOX); getSystemChatMessage().setStoredInMailbox(true); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateFault() { getReceiver().setDisputeOpenedMessageState(MessageState.FAILED); getSystemChatMessage().setSendMessageError(errorMessage); processModel.getTradeManager().requestPersistence(); } private void cleanup() { if (timer != null) { timer.stop(); } if (listener != null) { getReceiver().getDisputeOpenedMessageStateProperty().removeListener(listener); } } private void tryToSendAgainLater() { // skip if stopped if (stopSending()) return; // stop after max attempts if (resendCounter >= MAX_RESEND_ATTEMPTS) { cleanup(); log.warn("We never received an ACK message when sending the DisputeOpenedMessage to the peer. We stop trying to send the message."); return; } // register listeners once if (resendCounter == 0) { listener = (observable, oldValue, newValue) -> onMessageStateChange(newValue); getReceiver().getDisputeOpenedMessageStateProperty().addListener(listener); onMessageStateChange(getReceiver().getDisputeOpenedMessageStateProperty().get()); } // set resend delay if (resendCounter == 0) { delayInMin = SendMailboxMessageTask.INITIAL_RESEND_DELAY_MINS_FIRST; } else if (resendCounter == 1) { delayInMin = SendMailboxMessageTask.INITIAL_RESEND_DELAY_MINS_SECOND; } else { delayInMin = Math.min(TimeUnit.MILLISECONDS.toMinutes(TradeMailboxMessage.TTL), delayInMin * SendMailboxMessageTask.RESEND_DELAY_MULTIPLIER); } // use minimum delay if stored to mailbox if (getReceiver().isDisputeOpenedMessageStored()) { delayInMin = Math.max(delayInMin, SendMailboxMessageTask.INITIAL_RESEND_DELAY_MINS_MAILBOX); } // send again after delay log.info("We will send the message again to the peer after a delay of {} min.", delayInMin); if (timer != null) timer.stop(); timer = UserThread.runAfter(this::run, delayInMin, TimeUnit.MINUTES); resendCounter++; } private void onMessageStateChange(MessageState newValue) { if (stopSending()) { cleanup(); } } protected boolean stopSending() { if (getReceiver().getDisputeOpenedMessage() == null) return true; // stop if no message to send if (isMessageReceived()) return true; // stop if message received if (trade.isPayoutPublished()) return true; // stop if payout is published if (!((ArbitratorTrade) trade).resendDisputeOpenedMessageWithinDuration()) return true; // stop if payout is published and we are not in the resend period if (message != null && !message.equals(getReceiver().getDisputeOpenedMessage())) return true; // stop if message state is outdated return false; } protected boolean isMessageReceived() { return getReceiver().isDisputeOpenedMessageAckedOrNacked(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorSendDisputeOpenedMessageToBuyer.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; import haveno.core.trade.protocol.TradePeer; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; @EqualsAndHashCode(callSuper = true) @Slf4j public class ArbitratorSendDisputeOpenedMessageToBuyer extends ArbitratorSendDisputeOpenedMessage { public ArbitratorSendDisputeOpenedMessageToBuyer(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected TradePeer getReceiver() { return trade.getBuyer(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorSendDisputeOpenedMessageToSeller.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; import haveno.core.trade.protocol.TradePeer; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; @EqualsAndHashCode(callSuper = true) @Slf4j public class ArbitratorSendDisputeOpenedMessageToSeller extends ArbitratorSendDisputeOpenedMessage { public ArbitratorSendDisputeOpenedMessageToSeller(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected TradePeer getReceiver() { return trade.getSeller(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.app.Version; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.messages.InitMultisigRequest; import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.protocol.TradePeer; import haveno.core.trade.protocol.TradeProtocol; import haveno.network.p2p.SendDirectMessageListener; import lombok.extern.slf4j.Slf4j; import monero.wallet.MoneroWallet; import java.util.Date; import java.util.UUID; /** * Arbitrator sends InitTradeRequest to maker after receiving InitTradeRequest * from taker and verifying taker reserve tx. * * Arbitrator sends InitMultisigRequests after the maker acks. */ @Slf4j public class ArbitratorSendInitTradeOrMultisigRequests extends TradeTask { @SuppressWarnings({"unused"}) public ArbitratorSendInitTradeOrMultisigRequests(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); InitTradeRequest request = (InitTradeRequest) processModel.getTradeMessage(); TradePeer sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); // handle request from maker if (sender == trade.getMaker()) { // create request to taker InitTradeRequest takerRequest = new InitTradeRequest( request.getTradeProtocolVersion(), processModel.getOfferId(), trade.getAmount().longValueExact(), trade.getPrice().getValue(), request.getPaymentMethodId(), request.getMakerAccountId(), request.getTakerAccountId(), request.getMakerPaymentAccountId(), request.getTakerPaymentAccountId(), request.getTakerPubKeyRing(), UUID.randomUUID().toString(), Version.getP2PMessageVersion(), request.getAccountAgeWitnessSignatureOfOfferId(), request.getCurrentDate(), trade.getMaker().getNodeAddress(), trade.getTaker().getNodeAddress(), trade.getArbitrator().getNodeAddress(), null, null, null, null, null); // send request to taker log.info("Send {} with offerId {} and uid {} to taker {}", takerRequest.getClass().getSimpleName(), takerRequest.getOfferId(), takerRequest.getUid(), trade.getTaker().getNodeAddress()); processModel.getP2PService().sendEncryptedDirectMessage( trade.getTaker().getNodeAddress(), // TODO (woodser): maker's address might be different from original owner address if they disconnect and reconnect, need to validate and update address when requests received trade.getTaker().getPubKeyRing(), takerRequest, new SendDirectMessageListener() { @Override public void onArrived() { log.info("{} arrived at taker: offerId={}; uid={}", takerRequest.getClass().getSimpleName(), takerRequest.getOfferId(), takerRequest.getUid()); complete(); } @Override public void onFault(String errorMessage) { appendToErrorMessage(TradeProtocol.SEND_INIT_TRADE_REQUEST_FAILED + ": errorMessage=" + errorMessage); failed(); } } ); } // handle request from taker else if (sender == trade.getTaker()) { sendInitMultisigRequests(); complete(); // TODO: wait for InitMultisigRequest arrivals? } else { throw new RuntimeException("Request is not from maker or taker"); } } catch (Throwable t) { failed(t); } } private void sendInitMultisigRequests() { // ensure arbitrator has reserve txs if (processModel.getMaker().getReserveTxHash() == null) throw new RuntimeException("Arbitrator does not have maker's reserve tx after initializing trade"); if (processModel.getTaker().getReserveTxHash() == null && !trade.hasBuyerAsTakerWithoutDeposit()) throw new RuntimeException("Arbitrator does not have taker's reserve tx after initializing trade"); // create wallet for multisig MoneroWallet multisigWallet = trade.createWallet(); // prepare multisig String preparedHex = multisigWallet.prepareMultisig(); trade.getSelf().setPreparedMultisigHex(preparedHex); // set trade fee address String address = HavenoUtils.ARBITRATOR_ASSIGNS_TRADE_FEE_ADDRESS ? trade.getXmrWalletService().getBaseAddressEntry().getAddressString() : HavenoUtils.getGlobalTradeFeeAddress(); if (trade.getProcessModel().getTradeFeeAddress() == null) { trade.getProcessModel().setTradeFeeAddress(address); } // create message to initialize multisig InitMultisigRequest initMultisigRequest = new InitMultisigRequest( processModel.getOffer().getId(), UUID.randomUUID().toString(), Version.getP2PMessageVersion(), new Date().getTime(), preparedHex, null, null, trade.getProcessModel().getTradeFeeAddress()); // send request to maker log.info("Send {} with offerId {} and uid {} to maker {}", initMultisigRequest.getClass().getSimpleName(), initMultisigRequest.getOfferId(), initMultisigRequest.getUid(), trade.getMaker().getNodeAddress()); processModel.getP2PService().sendEncryptedDirectMessage( trade.getMaker().getNodeAddress(), trade.getMaker().getPubKeyRing(), initMultisigRequest, new SendDirectMessageListener() { @Override public void onArrived() { log.info("{} arrived at maker: offerId={}; uid={}", initMultisigRequest.getClass().getSimpleName(), initMultisigRequest.getOfferId(), initMultisigRequest.getUid()); } @Override public void onFault(String errorMessage) { log.error("Sending {} failed: uid={}; peer={}; error={}", initMultisigRequest.getClass().getSimpleName(), initMultisigRequest.getUid(), trade.getMaker().getNodeAddress(), errorMessage); } } ); // send request to taker log.info("Send {} with offerId {} and uid {} to taker {}", initMultisigRequest.getClass().getSimpleName(), initMultisigRequest.getOfferId(), initMultisigRequest.getUid(), trade.getTaker().getNodeAddress()); processModel.getP2PService().sendEncryptedDirectMessage( trade.getTaker().getNodeAddress(), trade.getTaker().getPubKeyRing(), initMultisigRequest, new SendDirectMessageListener() { @Override public void onArrived() { log.info("{} arrived at taker: offerId={}; uid={}", initMultisigRequest.getClass().getSimpleName(), initMultisigRequest.getOfferId(), initMultisigRequest.getUid()); } @Override public void onFault(String errorMessage) { log.error("Sending {} failed: uid={}; peer={}; error={}", initMultisigRequest.getClass().getSimpleName(), initMultisigRequest.getUid(), trade.getTaker().getNodeAddress(), errorMessage); } } ); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import com.google.common.base.Preconditions; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import lombok.extern.slf4j.Slf4j; import monero.wallet.MoneroWallet; import monero.wallet.model.MoneroAccount; import monero.wallet.model.MoneroSubaddress; import monero.wallet.model.MoneroTxWallet; import java.math.BigInteger; import java.util.ArrayList; import java.util.List; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class BuyerPreparePaymentSentMessage extends TradeTask { @SuppressWarnings({"unused"}) public BuyerPreparePaymentSentMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); // done if payout already published if (trade.isPayoutPublished()) { throw new RuntimeException("Cannot mark payment sent because payout already published for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); } // skip if payout tx already created if (trade.getSelf().getUnsignedPayoutTxHex() != null) { log.warn("Skipping preparation of payment sent message because payout tx is already created for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); complete(); return; } // validate state Preconditions.checkNotNull(trade.getSeller().getPaymentAccountPayload(), "Seller's payment account payload is null"); Preconditions.checkNotNull(trade.getAmount(), "trade.getTradeAmount() must not be null"); Preconditions.checkNotNull(trade.getMakerDepositTx(), "trade.getMakerDepositTx() must not be null"); if (!trade.hasBuyerAsTakerWithoutDeposit()) Preconditions.checkNotNull(trade.getTakerDepositTx(), "trade.getTakerDepositTx() must not be null"); checkNotNull(trade.getOffer(), "offer must not be null"); // create payout tx if we have seller's updated multisig hex if (trade.getSeller().getUpdatedMultisigHex() != null) { // synchronize on lock for wallet operations synchronized (trade.getWalletLock()) { synchronized (HavenoUtils.getWalletFunctionLock()) { try { // import multisig hex trade.importMultisigHex(); // create payout tx log.info("Buyer creating unsigned payout tx for {} {} ", trade.getClass().getSimpleName(), trade.getShortId()); MoneroTxWallet payoutTx = trade.createPayoutTx(); trade.setPayoutTx(payoutTx); trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); trade.requestPersistence(); } catch (Exception e) { if (HavenoUtils.isIllegal(e)) log.warn("Failed to create unsigned payout tx for " + trade.getClass().getSimpleName() + " " + trade.getShortId(), e); // continue to send message if illegal state else throw e; } } } } complete(); } catch (Throwable t) { failed(t); } } // TODO (woodser): move these to gen utils /** * Generic parameterized pair. * * @author woodser * * @param the type of the first element * @param the type of the second element */ public static class Pair { private F first; private S second; public Pair(F first, S second) { super(); this.first = first; this.second = second; } public F getFirst() { return first; } public void setFirst(F first) { this.first = first; } public S getSecond() { return second; } public void setSecond(S second) { this.second = second; } } public static void printBalances(MoneroWallet wallet) { // collect info about subaddresses List>> pairs = new ArrayList>>(); //if (wallet == null) wallet = TestUtils.getWalletJni(); BigInteger balance = wallet.getBalance(); BigInteger unlockedBalance = wallet.getUnlockedBalance(); List accounts = wallet.getAccounts(true); System.out.println("Wallet balance: " + balance); System.out.println("Wallet unlocked balance: " + unlockedBalance); for (MoneroAccount account : accounts) { add(pairs, "ACCOUNT", account.getIndex()); add(pairs, "SUBADDRESS", ""); add(pairs, "LABEL", ""); add(pairs, "ADDRESS", ""); add(pairs, "BALANCE", account.getBalance()); add(pairs, "UNLOCKED", account.getUnlockedBalance()); for (MoneroSubaddress subaddress : account.getSubaddresses()) { add(pairs, "ACCOUNT", account.getIndex()); add(pairs, "SUBADDRESS", subaddress.getIndex()); add(pairs, "LABEL", subaddress.getLabel()); add(pairs, "ADDRESS", subaddress.getAddress()); add(pairs, "BALANCE", subaddress.getBalance()); add(pairs, "UNLOCKED", subaddress.getUnlockedBalance()); } } // convert info to csv Integer length = null; for (Pair> pair : pairs) { if (length == null) length = pair.getSecond().size(); } System.out.println(pairsToCsv(pairs)); } private static void add(List>> pairs, String header, Object value) { if (value == null) value = ""; Pair> pair = null; for (Pair> aPair : pairs) { if (aPair.getFirst().equals(header)) { pair = aPair; break; } } if (pair == null) { List vals = new ArrayList(); pair = new Pair>(header, vals); pairs.add(pair); } pair.getSecond().add(value); } private static String pairsToCsv(List>> pairs) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < pairs.size(); i++) { sb.append(pairs.get(i).getFirst()); if (i < pairs.size() - 1) sb.append(','); else sb.append('\n'); } for (int i = 0; i < pairs.get(0).getSecond().size(); i++) { for (int j = 0; j < pairs.size(); j++) { sb.append(pairs.get(j).getSecond().get(i)); if (j < pairs.size() - 1) sb.append(','); else sb.append('\n'); } } return sb.toString(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import java.util.concurrent.TimeUnit; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.taskrunner.TaskRunner; import haveno.core.network.MessageState; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.messages.PaymentSentMessage; import haveno.core.trade.messages.TradeMailboxMessage; import haveno.core.util.JsonUtil; import javafx.beans.value.ChangeListener; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; /** * We send the seller the BuyerSendPaymentSentMessage. * We wait to receive a ACK message back and resend the message * in case that does not happen in 10 minutes or if the message was stored in mailbox or failed. We keep repeating that * with doubling the interval each time and until the MAX_RESEND_ATTEMPTS is reached. * If never successful we give up and complete. It might be a valid case that the peer was not online for an extended * time but we can be very sure that our message was stored as mailbox message in the network and one the peer goes * online he will process it. */ @Slf4j @EqualsAndHashCode(callSuper = true) public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask { private ChangeListener listener; private Timer timer; private static final int MAX_RESEND_ATTEMPTS = 20; private long delayInMin = -1; private int resendCounter = 0; public BuyerSendPaymentSentMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); // skip if already acked by receiver if (stopSending()) { if (!isCompleted()) complete(); return; } super.run(); } catch (Throwable t) { failed(t); } } @Override protected TradeMailboxMessage getMailboxMessage(String tradeId) { if (getReceiver().getPaymentSentMessage() == null) { // We do not use a real unique ID here as we want to be able to re-send the exact same message in case the // peer does not respond with an ACK msg in a certain time interval. To avoid that we get dangling mailbox // messages where only the one which gets processed by the peer would be removed we use the same uid. All // other data stays the same when we re-send the message at any time later. String deterministicId = HavenoUtils.getDeterministicId(trade, PaymentSentMessage.class, getReceiverNodeAddress()); // create payment sent message PaymentSentMessage message = new PaymentSentMessage( tradeId, processModel.getMyNodeAddress(), trade.getCounterCurrencyTxId(), trade.getCounterCurrencyExtraData(), deterministicId, trade.getSelf().getUnsignedPayoutTxHex(), trade.getSelf().getUpdatedMultisigHex(), trade.getSelf().getPaymentAccountKey(), trade.getTradePeer().getAccountAgeWitness() ); // sign message try { String messageAsJson = JsonUtil.objectToJson(message); byte[] sig = HavenoUtils.sign(processModel.getP2PService().getKeyRing(), messageAsJson); message.setBuyerSignature(sig); getReceiver().setPaymentSentMessage(message); trade.requestPersistence(); } catch (Exception e) { throw new RuntimeException (e); } } return getReceiver().getPaymentSentMessage(); } @Override protected void setStateSent() { getReceiver().setPaymentSentMessageState(MessageState.SENT); tryToSendAgainLater(); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateArrived() { getReceiver().setPaymentSentMessageState(MessageState.ARRIVED); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateStoredInMailbox() { getReceiver().setPaymentSentMessageState(MessageState.STORED_IN_MAILBOX); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateFault() { getReceiver().setPaymentSentMessageState(MessageState.FAILED); processModel.getTradeManager().requestPersistence(); } private void cleanup() { if (timer != null) { timer.stop(); } if (listener != null) { getReceiver().getPaymentReceivedMessageStateProperty().removeListener(listener); } } private void tryToSendAgainLater() { // skip if stopped if (stopSending()) return; // stop after max attempts if (resendCounter >= MAX_RESEND_ATTEMPTS) { cleanup(); log.warn("We never received an ACK message when sending the PaymentSentMessage to the peer. We stop trying to send the message."); return; } // register listeners once if (resendCounter == 0) { listener = (observable, oldValue, newValue) -> onMessageStateChange(newValue); getReceiver().getPaymentSentMessageStateProperty().addListener(listener); onMessageStateChange(getReceiver().getPaymentSentMessageStateProperty().get()); } // set resend delay if (resendCounter == 0) { delayInMin = SendMailboxMessageTask.INITIAL_RESEND_DELAY_MINS_FIRST; } else if (resendCounter == 1) { delayInMin = SendMailboxMessageTask.INITIAL_RESEND_DELAY_MINS_SECOND; } else { delayInMin = Math.min(TimeUnit.MILLISECONDS.toMinutes(TradeMailboxMessage.TTL), delayInMin * SendMailboxMessageTask.RESEND_DELAY_MULTIPLIER); } // use minimum delay if stored to mailbox if (getReceiver().isPaymentSentMessageStored()) { delayInMin = Math.max(delayInMin, SendMailboxMessageTask.INITIAL_RESEND_DELAY_MINS_MAILBOX); } // send again after delay log.info("We will send the message again to the peer after a delay of {} min.", delayInMin); if (timer != null) timer.stop(); timer = UserThread.runAfter(this::run, delayInMin, TimeUnit.MINUTES); resendCounter++; } private void onMessageStateChange(MessageState newValue) { if (stopSending()) { cleanup(); } } protected boolean stopSending() { return getReceiver().isPaymentSentMessageAcked(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToArbitrator.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; import haveno.core.trade.protocol.TradePeer; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; @EqualsAndHashCode(callSuper = true) @Slf4j public class BuyerSendPaymentSentMessageToArbitrator extends BuyerSendPaymentSentMessage { public BuyerSendPaymentSentMessageToArbitrator(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected TradePeer getReceiver() { return trade.getArbitrator(); } @Override protected void setStateSent() { super.setStateSent(); if (!isCompleted()) complete(); // don't wait for message to arbitrator } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToSeller.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; import haveno.core.trade.protocol.TradePeer; import haveno.network.p2p.mailbox.MailboxMessage; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; @EqualsAndHashCode(callSuper = true) @Slf4j public class BuyerSendPaymentSentMessageToSeller extends BuyerSendPaymentSentMessage { public BuyerSendPaymentSentMessageToSeller(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected TradePeer getReceiver() { return trade.getSeller(); } @Override protected void setStateSent() { if (trade.getState().equals(Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG)) { trade.setState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); } else { trade.advanceState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); // do not revert previous send progress } super.setStateSent(); } @Override protected void setStateArrived() { trade.setStateIfValidTransitionTo(Trade.State.BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG); super.setStateArrived(); } @Override protected void setStateStoredInMailbox() { trade.setStateIfValidTransitionTo(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG); super.setStateStoredInMailbox(); } @Override protected void setStateFault() { trade.setStateIfValidTransitionTo(Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG); super.setStateFault(); } // continue execution on fault so payment sent message is sent to arbitrator @Override protected void onFault(String errorMessage, MailboxMessage message) { setStateFault(); appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage); complete(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/MakerRecreateReserveTx.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OpenOffer; import haveno.core.trade.HavenoUtils; import haveno.core.trade.MakerTrade; import haveno.core.trade.Trade; import haveno.core.trade.protocol.TradeProtocol; import haveno.core.xmr.model.XmrAddressEntry; import lombok.extern.slf4j.Slf4j; import monero.common.MoneroRpcConnection; import monero.wallet.model.MoneroTxWallet; import java.math.BigInteger; @Slf4j public class MakerRecreateReserveTx extends TradeTask { public MakerRecreateReserveTx(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); // maker trade expected if (!(trade instanceof MakerTrade)) { throw new RuntimeException("Expected maker trade but was " + trade.getClass().getSimpleName() + " " + trade.getShortId() + ". That should never happen."); } // get open offer OpenOffer openOffer = HavenoUtils.openOfferManager.getOpenOffer(trade.getOffer().getId()).orElse(null); if (openOffer == null) throw new RuntimeException("Open offer not found for " + trade.getClass().getSimpleName() + " " + trade.getId()); Offer offer = openOffer.getOffer(); // reset reserve tx state trade.getSelf().setReserveTxHex(null); trade.getSelf().setReserveTxHash(null); trade.getSelf().setReserveTxKey(null); trade.getSelf().setReserveTxKeyImages(null); // recreate reserve tx log.warn("Maker is recreating reserve tx for tradeId={}", trade.getShortId()); MoneroTxWallet reserveTx = null; synchronized (HavenoUtils.xmrWalletService.getWalletLock()) { // check for timeout if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create reserve tx, tradeId=" + trade.getShortId()); trade.startProtocolTimeout(); // thaw reserved key images log.info("Thawing offer payload tx key images for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); HavenoUtils.xmrWalletService.thawOutputs(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages()); // check for timeout if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while thawing key images, tradeId=" + trade.getShortId()); trade.startProtocolTimeout(); // collect relevant info BigInteger makerFee = offer.getMaxMakerFee(); BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount(); BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit(); BigInteger penaltyFee = HavenoUtils.multiply(securityDeposit, offer.getPenaltyFeePct()); String returnAddress = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(); XmrAddressEntry fundingEntry = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null); Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex(); // attempt re-creating reserve tx try { synchronized (HavenoUtils.getWalletFunctionLock()) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection(); try { reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex); } catch (IllegalStateException e) { log.warn("Illegal state creating reserve tx, tradeId={}, error={}", trade.getShortId(), i + 1, e.getMessage()); throw e; } catch (Exception e) { log.warn("Error creating reserve tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); trade.getXmrWalletService().handleMainWalletError(e, sourceConnection, i + 1); if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } // check for timeout if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId()); if (reserveTx != null) break; } } } catch (Exception e) { // reset state if (reserveTx != null) model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx)); model.getXmrWalletService().freezeOutputs(offer.getOfferPayload().getReserveTxKeyImages()); trade.getSelf().setReserveTxKeyImages(null); throw e; } // reset protocol timeout trade.startProtocolTimeout(); // update state trade.getSelf().setReserveTxHash(reserveTx.getHash()); trade.getSelf().setReserveTxHex(reserveTx.getFullHex()); trade.getSelf().setReserveTxKey(reserveTx.getKey()); trade.getSelf().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(reserveTx)); } // save process state processModel.setReserveTx(reserveTx); // TODO: remove this? how is it used? processModel.getTradeManager().requestPersistence(); complete(); } catch (Throwable t) { trade.setErrorMessage("An error occurred.\n" + "Error message:\n" + t.getMessage()); failed(t); } } private boolean isTimedOut() { return !processModel.getTradeManager().hasOpenTrade(trade); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequestToArbitrator.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.app.Version; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.common.taskrunner.TaskRunner; import haveno.core.offer.availability.DisputeAgentSelection; import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.messages.InitTradeRequest; import haveno.core.xmr.model.XmrAddressEntry; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.SendDirectMessageListener; import lombok.extern.slf4j.Slf4j; import java.util.HashSet; import java.util.Set; @Slf4j public class MakerSendInitTradeRequestToArbitrator extends TradeTask { @SuppressWarnings({"unused"}) public MakerSendInitTradeRequestToArbitrator(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); // get least used arbitrator Arbitrator leastUsedArbitrator = DisputeAgentSelection.getLeastUsedArbitrator(processModel.getTradeStatisticsManager(), processModel.getArbitratorManager()); if (leastUsedArbitrator == null) { failed("Could not get least used arbitrator to send " + InitTradeRequest.class.getSimpleName() + " for offer " + trade.getId()); return; } // send request to least used arbitrators until success sendInitTradeRequests(leastUsedArbitrator.getNodeAddress(), new HashSet(), () -> { trade.addInitProgressStep(); complete(); }, (errorMessage) -> { log.warn("Cannot initialize trade with arbitrators: " + errorMessage); failed(errorMessage); }); } catch (Throwable t) { failed(t); } } private void sendInitTradeRequests(NodeAddress arbitratorNodeAddress, Set excludedArbitrators, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { sendInitTradeRequest(arbitratorNodeAddress, new SendDirectMessageListener() { @Override public void onArrived() { log.info("{} arrived at arbitrator: offerId={}", InitTradeRequest.class.getSimpleName(), trade.getId()); // check if trade still exists if (!processModel.getTradeManager().hasOpenTrade(trade)) { errorMessageHandler.handleErrorMessage("Trade protocol no longer exists, tradeId=" + trade.getId()); return; } resultHandler.handleResult(); } // if unavailable, try alternative arbitrator @Override public void onFault(String errorMessage) { log.warn("Arbitrator unavailable: address={}, error={}", arbitratorNodeAddress, errorMessage); excludedArbitrators.add(arbitratorNodeAddress); // check if trade still exists if (!processModel.getTradeManager().hasOpenTrade(trade)) { errorMessageHandler.handleErrorMessage("Trade protocol no longer exists, tradeId=" + trade.getId()); return; } Arbitrator altArbitrator = DisputeAgentSelection.getLeastUsedArbitrator(processModel.getTradeStatisticsManager(), processModel.getArbitratorManager(), excludedArbitrators); if (altArbitrator == null) { errorMessageHandler.handleErrorMessage("Cannot take offer because no arbitrators are available"); return; } log.info("Using alternative arbitrator {}", altArbitrator.getNodeAddress()); sendInitTradeRequests(altArbitrator.getNodeAddress(), excludedArbitrators, resultHandler, errorMessageHandler); } }); } private void sendInitTradeRequest(NodeAddress arbitratorNodeAddress, SendDirectMessageListener listener) { // get registered arbitrator Arbitrator arbitrator = processModel.getUser().getAcceptedArbitratorByAddress(arbitratorNodeAddress); if (arbitrator == null) throw new RuntimeException("Node address " + arbitratorNodeAddress + " is not a registered arbitrator"); // set pub keys processModel.getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing()); trade.getArbitrator().setNodeAddress(arbitratorNodeAddress); trade.getArbitrator().setPubKeyRing(processModel.getArbitrator().getPubKeyRing()); // create request to arbitrator InitTradeRequest takerRequest = (InitTradeRequest) processModel.getTradeMessage(); // taker's InitTradeRequest to maker InitTradeRequest arbitratorRequest = new InitTradeRequest( takerRequest.getTradeProtocolVersion(), trade.getId(), trade.getAmount().longValueExact(), trade.getPrice().getValue(), trade.getOffer().getOfferPayload().getPaymentMethodId(), trade.getProcessModel().getAccountId(), takerRequest.getTakerAccountId(), trade.getOffer().getOfferPayload().getMakerPaymentAccountId(), takerRequest.getTakerPaymentAccountId(), trade.getTaker().getPubKeyRing(), takerRequest.getUid(), Version.getP2PMessageVersion(), null, trade.getTakeOfferDate().getTime(), // maker's date is used as shared timestamp trade.getMaker().getNodeAddress(), trade.getTaker().getNodeAddress(), trade.getArbitrator().getNodeAddress(), trade.getSelf().getReserveTxHash(), trade.getSelf().getReserveTxHex(), trade.getSelf().getReserveTxKey(), model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(), trade.getChallenge()); // send request to arbitrator log.info("Sending {} with offerId {} and uid {} to arbitrator {}", arbitratorRequest.getClass().getSimpleName(), arbitratorRequest.getOfferId(), arbitratorRequest.getUid(), trade.getArbitrator().getNodeAddress()); processModel.getP2PService().sendEncryptedDirectMessage( arbitratorNodeAddress, arbitrator.getPubKeyRing(), arbitratorRequest, listener, HavenoUtils.ARBITRATOR_ACK_TIMEOUT_SECONDS ); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/MakerSetLockTime.java ================================================ /* * 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 . */ package haveno.core.trade.protocol.tasks; import haveno.common.config.Config; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; import haveno.core.xmr.wallet.Restrictions; import lombok.extern.slf4j.Slf4j; @Slf4j public class MakerSetLockTime extends TradeTask { public MakerSetLockTime(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); // 10 days for cryptos, 20 days for other payment methods // For regtest dev environment we use 5 blocks int delay = Config.baseCurrencyNetwork().isTestnet() ? 5 : Restrictions.getLockTime(processModel.getOffer().getPaymentMethod().isBlockchain()); long lockTime = processModel.getBtcWalletService().getBestChainHeight() + delay; log.info("lockTime={}, delay={}", lockTime, delay); trade.setLockTime(lockTime); processModel.getTradeManager().requestPersistence(); complete(); } catch (Throwable t) { failed(t); } } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/MaybeResendDisputeClosedMessageWithPayout.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; import haveno.core.support.dispute.Dispute; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.messages.DepositsConfirmedMessage; import haveno.core.trade.protocol.TradePeer; import haveno.core.util.Validator; import lombok.extern.slf4j.Slf4j; import java.util.List; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class MaybeResendDisputeClosedMessageWithPayout extends TradeTask { @SuppressWarnings({"unused"}) public MaybeResendDisputeClosedMessageWithPayout(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); // get peer DepositsConfirmedMessage request = (DepositsConfirmedMessage) processModel.getTradeMessage(); checkNotNull(request); Validator.checkTradeId(processModel.getOfferId(), request); TradePeer sender = trade.getTradePeer(request.getPubKeyRing()); if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller"); // arbitrator resends DisputeClosedMessage with payout tx when updated multisig info received boolean ticketClosed = false; if (!trade.isPayoutPublished() && trade.isArbitrator()) { List disputes = trade.getDisputes(); for (Dispute dispute : disputes) { if (!dispute.isClosed()) continue; // dispute must be closed if (sender.getPubKeyRing().equals(dispute.getTraderPubKeyRing())) { log.info("Arbitrator resending DisputeClosedMessage for trade {} after receiving updated multisig hex", trade.getId()); HavenoUtils.arbitrationManager.closeDisputeTicket(dispute.getDisputeResultProperty().get(), dispute, dispute.getDisputeResultProperty().get().summaryNotesProperty().get(), () -> { completeAux(); }, (errMessage, err) -> { log.error("Failed to close dispute ticket for trade {}: {}\n", trade.getId(), errMessage, err); failed(err); }); ticketClosed = true; break; } } } // complete if not waiting for result if (!ticketClosed) completeAux(); } catch (Throwable t) { failed(t); } } private void completeAux() { processModel.getTradeManager().requestPersistence(); complete(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.app.Version; import haveno.common.taskrunner.TaskRunner; import haveno.core.offer.OpenOffer; import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.BuyerTrade; import haveno.core.trade.HavenoUtils; import haveno.core.trade.MakerTrade; import haveno.core.trade.Trade; import haveno.core.trade.messages.SignContractRequest; import haveno.core.trade.protocol.TradeProtocol; import haveno.core.xmr.model.XmrAddressEntry; import haveno.network.p2p.SendDirectMessageListener; import lombok.extern.slf4j.Slf4j; import monero.common.MoneroRpcConnection; import monero.wallet.model.MoneroTxWallet; import java.math.BigInteger; import java.util.Date; import java.util.Optional; import java.util.UUID; // TODO (woodser): separate classes for deposit tx creation and contract request, or combine into ProcessInitMultisigRequest @Slf4j public class MaybeSendSignContractRequest extends TradeTask { private boolean ack1 = false; // TODO (woodser) these represent onArrived(), not the ack private boolean ack2 = false; @SuppressWarnings({"unused"}) public MaybeSendSignContractRequest(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); // skip if arbitrator if (trade instanceof ArbitratorTrade) { complete(); return; } // skip if multisig wallet not complete if (processModel.getMultisigAddress() == null) { complete(); return; } // skip if deposit tx already created if (trade.getSelf().getDepositTx() != null) { complete(); return; } // initialize progress steps trade.addInitProgressStep(); // create deposit tx and freeze inputs MoneroTxWallet depositTx = null; synchronized (HavenoUtils.xmrWalletService.getWalletLock()) { // check for timeout if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create deposit tx, tradeId=" + trade.getShortId()); trade.startProtocolTimeout(); // collect info Integer subaddressIndex = null; boolean reserveExactAmount = false; if (trade instanceof MakerTrade) { Optional openOffer = processModel.getOpenOfferManager().getOpenOffer(trade.getId()); if (openOffer.isPresent()) { reserveExactAmount = openOffer.get().isReserveExactAmount(); if (reserveExactAmount) subaddressIndex = model.getXmrWalletService().getAddressEntry(trade.getId(), XmrAddressEntry.Context.OFFER_FUNDING).get().getSubaddressIndex(); } else { throw new RuntimeException("Cannot request contract signature because open offer has been removed for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); } } // thaw reserved outputs trade.getXmrWalletService().thawOutputs(trade.getSelf().getReserveTxKeyImages()); // attempt creating deposit tx if (!trade.isBuyerAsTakerWithoutDeposit()) { try { synchronized (HavenoUtils.getWalletFunctionLock()) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection(); try { depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex); } catch (Exception e) { log.warn("Error creating deposit tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); trade.getXmrWalletService().handleMainWalletError(e, sourceConnection, i + 1); if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } // check for timeout if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId()); if (depositTx != null) break; } } } catch (Exception e) { // thaw deposit inputs if (depositTx != null) { trade.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(depositTx)); trade.getSelf().setReserveTxKeyImages(null); } // re-freeze maker offer inputs if (trade instanceof MakerTrade) { trade.getXmrWalletService().freezeOutputs(trade.getOffer().getOfferPayload().getReserveTxKeyImages()); } throw e; } } // reset protocol timeout trade.addInitProgressStep(); // update trade state trade.getSelf().setPayoutAddressString(trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString()); // TODO (woodser): allow custom payout address? trade.getSelf().setPaymentAccountPayload(trade.getProcessModel().getPaymentAccountPayload(trade.getSelf().getPaymentAccountId())); trade.getSelf().setPaymentAccountPayloadHash(trade.getSelf().getPaymentAccountPayload().getHash()); BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); if (depositTx == null) { trade.getSelf().setSecurityDeposit(securityDeposit); } else { trade.getSelf().setSecurityDeposit(securityDeposit.subtract(depositTx.getFee())); trade.getSelf().setDepositTx(depositTx); trade.getSelf().setDepositTxHash(depositTx.getHash()); trade.getSelf().setDepositTxFee(depositTx.getFee()); trade.getSelf().setDepositTxHex(depositTx.getFullHex()); trade.getSelf().setDepositTxKey(depositTx.getKey()); trade.getSelf().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(depositTx)); } } // maker signs deposit hash nonce to avoid challenge protocol byte[] sig = null; if (trade instanceof MakerTrade) { sig = HavenoUtils.sign(processModel.getP2PService().getKeyRing(), depositTx.getHash()); } // create request for peer and arbitrator to sign contract SignContractRequest request = new SignContractRequest( trade.getOffer().getId(), UUID.randomUUID().toString(), Version.getP2PMessageVersion(), new Date().getTime(), trade.getProcessModel().getAccountId(), trade.getSelf().getPaymentAccountPayload().getHash(), trade.getSelf().getPayoutAddressString(), depositTx == null ? null : depositTx.getHash(), sig); // send request to trading peer processModel.getP2PService().sendEncryptedDirectMessage(trade.getTradePeer().getNodeAddress(), trade.getTradePeer().getPubKeyRing(), request, new SendDirectMessageListener() { @Override public void onArrived() { log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getTradePeer().getNodeAddress(), trade.getId(), request.getUid()); ack1 = true; if (ack1 && ack2) completeAux(); } @Override public void onFault(String errorMessage) { log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), trade.getTradePeer().getNodeAddress(), trade.getId(), errorMessage); appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage); failed(); } }); // send request to arbitrator processModel.getP2PService().sendEncryptedDirectMessage(trade.getArbitrator().getNodeAddress(), trade.getArbitrator().getPubKeyRing(), request, new SendDirectMessageListener() { @Override public void onArrived() { log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getArbitrator().getNodeAddress(), trade.getId()); ack2 = true; if (ack1 && ack2) completeAux(); } @Override public void onFault(String errorMessage) { log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), trade.getArbitrator().getNodeAddress(), trade.getId(), errorMessage); appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage); failed(); } }); } catch (Throwable t) { failed(t); } } private void completeAux() { trade.setState(Trade.State.CONTRACT_SIGNATURE_REQUESTED); trade.addInitProgressStep(); processModel.getTradeManager().requestPersistence(); complete(); } private boolean isTimedOut() { return !processModel.getTradeManager().hasOpenTrade(trade); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositResponse.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import java.math.BigInteger; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; import haveno.core.trade.messages.DepositResponse; import lombok.extern.slf4j.Slf4j; @Slf4j public class ProcessDepositResponse extends TradeTask { @SuppressWarnings({"unused"}) public ProcessDepositResponse(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); // ignore if deposits confirmed if (trade.isDepositsConfirmed()) { complete(); return; } // handle error DepositResponse message = (DepositResponse) processModel.getTradeMessage(); if (message.getErrorMessage() != null) { log.warn("Deposit response has error message for {} {}: {}", trade.getClass().getSimpleName(), trade.getShortId(), message.getErrorMessage()); trade.setStateIfValidTransitionTo(Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED); trade.setInitError(new RuntimeException(message.getErrorMessage())); complete(); return; } // publish deposit transaction for redundancy try { model.getXmrWalletService().getMonerod().submitTxHex(trade.getSelf().getDepositTxHex()); } catch (Exception e) { log.warn("Failed to redundantly publish deposit transaction for {} {}: {}", trade.getClass().getSimpleName(), trade.getShortId(), e.getMessage()); } // record security deposits trade.getBuyer().setSecurityDeposit(BigInteger.valueOf(message.getBuyerSecurityDeposit())); trade.getSeller().setSecurityDeposit(BigInteger.valueOf(message.getSellerSecurityDeposit())); // set success state trade.setStateIfValidTransitionTo(Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS); processModel.getTradeManager().requestPersistence(); // update balances trade.getXmrWalletService().updateBalanceListeners(); complete(); } catch (Throwable t) { failed(t); } } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; import haveno.core.trade.messages.DepositsConfirmedMessage; import haveno.core.trade.protocol.TradePeer; import haveno.core.util.Validator; import lombok.extern.slf4j.Slf4j; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class ProcessDepositsConfirmedMessage extends TradeTask { @SuppressWarnings({"unused"}) public ProcessDepositsConfirmedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); // get peer DepositsConfirmedMessage request = (DepositsConfirmedMessage) processModel.getTradeMessage(); checkNotNull(request); Validator.checkTradeId(processModel.getOfferId(), request); TradePeer sender = trade.getTradePeer(request.getPubKeyRing()); if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller"); // update peer node address sender.setNodeAddress(processModel.getTempTradePeerNodeAddress()); if (sender.getNodeAddress().equals(trade.getBuyer().getNodeAddress()) && sender != trade.getBuyer()) trade.getBuyer().setNodeAddress(null); // tests can reuse addresses if (sender.getNodeAddress().equals(trade.getSeller().getNodeAddress()) && sender != trade.getSeller()) trade.getSeller().setNodeAddress(null); if (sender.getNodeAddress().equals(trade.getArbitrator().getNodeAddress()) && sender != trade.getArbitrator()) trade.getArbitrator().setNodeAddress(null); // decrypt seller payment account payload if key given if (request.getSellerPaymentAccountKey() != null && trade.getTradePeer().getPaymentAccountPayload() == null) { log.info("Decrypting seller payment account payload for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey()); } // update multisig hex if (sender.getUpdatedMultisigHex() == null) { sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex()); trade.scheduleImportMultisigHex(); } // persist processModel.getTradeManager().requestPersistence(); complete(); } catch (Throwable t) { failed(t); } } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitMultisigRequest.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.app.Version; import haveno.common.crypto.PubKeyRing; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.HavenoUtils; import haveno.core.trade.MakerTrade; import haveno.core.trade.TakerTrade; import haveno.core.trade.Trade; import haveno.core.trade.messages.InitMultisigRequest; import haveno.core.trade.protocol.TradePeer; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.SendDirectMessageListener; import lombok.extern.slf4j.Slf4j; import monero.wallet.MoneroWallet; import monero.wallet.model.MoneroMultisigInfo; import monero.wallet.model.MoneroMultisigInitResult; import java.util.Arrays; import java.util.Date; import java.util.UUID; import static com.google.common.base.Preconditions.checkNotNull; import static haveno.core.util.Validator.checkTradeId; @Slf4j public class ProcessInitMultisigRequest extends TradeTask { private boolean ack1 = false; private boolean ack2 = false; MoneroWallet multisigWallet; @SuppressWarnings({"unused"}) public ProcessInitMultisigRequest(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); log.debug("current trade state " + trade.getState()); InitMultisigRequest request = (InitMultisigRequest) processModel.getTradeMessage(); checkNotNull(request); checkTradeId(processModel.getOfferId(), request); XmrWalletService xmrWalletService = processModel.getProvider().getXmrWalletService(); // get sender TradePeer sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); // set trade fee address if (HavenoUtils.ARBITRATOR_ASSIGNS_TRADE_FEE_ADDRESS) { if (request.getTradeFeeAddress() != null && sender == trade.getArbitrator()) { trade.getProcessModel().setTradeFeeAddress(request.getTradeFeeAddress()); } } else { trade.getProcessModel().setTradeFeeAddress(HavenoUtils.getGlobalTradeFeeAddress()); } // reconcile peer's established multisig hex with message if (sender.getPreparedMultisigHex() == null) sender.setPreparedMultisigHex(request.getPreparedMultisigHex()); else if (request.getPreparedMultisigHex() != null && !sender.getPreparedMultisigHex().equals(request.getPreparedMultisigHex())) throw new RuntimeException("Message's prepared multisig differs from previous messages, previous: " + sender.getPreparedMultisigHex() + ", message: " + request.getPreparedMultisigHex()); if (sender.getMadeMultisigHex() == null) sender.setMadeMultisigHex(request.getMadeMultisigHex()); else if (request.getMadeMultisigHex() != null && !sender.getMadeMultisigHex().equals(request.getMadeMultisigHex())) throw new RuntimeException("Message's made multisig differs from previous messages: " + request.getMadeMultisigHex() + " versus " + sender.getMadeMultisigHex()); if (sender.getExchangedMultisigHex() == null) sender.setExchangedMultisigHex(request.getExchangedMultisigHex()); else if (request.getExchangedMultisigHex() != null && !sender.getExchangedMultisigHex().equals(request.getExchangedMultisigHex())) throw new RuntimeException("Message's exchanged multisig differs from previous messages: " + request.getExchangedMultisigHex() + " versus " + sender.getExchangedMultisigHex()); // prepare multisig if applicable boolean updateParticipants = false; if (trade.getSelf().getPreparedMultisigHex() == null) { log.info("Preparing multisig wallet for {} {}", trade.getClass().getSimpleName(), trade.getId()); multisigWallet = trade.createWallet(); trade.getSelf().setPreparedMultisigHex(multisigWallet.prepareMultisig()); trade.setStateIfValidTransitionTo(Trade.State.MULTISIG_PREPARED); updateParticipants = true; } else if (processModel.getMultisigAddress() == null) { multisigWallet = trade.getWallet(); } // make multisig if applicable TradePeer[] peers = getMultisigPeers(); if (trade.getSelf().getMadeMultisigHex() == null && peers[0].getPreparedMultisigHex() != null && peers[1].getPreparedMultisigHex() != null) { log.info("Making multisig wallet for {} {}", trade.getClass().getSimpleName(), trade.getId()); String multisigHex = multisigWallet.makeMultisig(Arrays.asList(peers[0].getPreparedMultisigHex(), peers[1].getPreparedMultisigHex()), 2, xmrWalletService.getWalletPassword()); // TODO (woodser): xmrWalletService.makeMultisig(tradeId, multisigHexes, threshold)? trade.getSelf().setMadeMultisigHex(multisigHex); trade.setStateIfValidTransitionTo(Trade.State.MULTISIG_MADE); updateParticipants = true; } // import made multisig keys if applicable if (trade.getSelf().getExchangedMultisigHex() == null && peers[0].getMadeMultisigHex() != null && peers[1].getMadeMultisigHex() != null) { log.info("Importing made multisig hex for {} {}", trade.getClass().getSimpleName(), trade.getId()); MoneroMultisigInitResult result = multisigWallet.exchangeMultisigKeys(Arrays.asList(peers[0].getMadeMultisigHex(), peers[1].getMadeMultisigHex()), xmrWalletService.getWalletPassword()); trade.getSelf().setExchangedMultisigHex(result.getMultisigHex()); trade.setStateIfValidTransitionTo(Trade.State.MULTISIG_EXCHANGED); updateParticipants = true; } // import exchanged multisig keys if applicable if (processModel.getMultisigAddress() == null && peers[0].getExchangedMultisigHex() != null && peers[1].getExchangedMultisigHex() != null) { log.info("Importing exchanged multisig hex for trade {}", trade.getId()); MoneroMultisigInitResult result = multisigWallet.exchangeMultisigKeys(Arrays.asList(peers[0].getExchangedMultisigHex(), peers[1].getExchangedMultisigHex()), xmrWalletService.getWalletPassword()); // check multisig state MoneroMultisigInfo multisigInfo = multisigWallet.getMultisigInfo(); if (!multisigInfo.isMultisig()) throw new RuntimeException("Multisig wallet is not multisig on completion"); if (!multisigInfo.isReady()) throw new RuntimeException("Multisig wallet is not ready on completion"); if (multisigInfo.getThreshold() != 2) throw new RuntimeException("Multisig wallet has unexpected threshold: " + multisigInfo.getThreshold()); if (multisigInfo.getNumParticipants() != 3) throw new RuntimeException("Multisig wallet has unexpected number of participants: " + multisigInfo.getNumParticipants()); // set final address and save processModel.setMultisigAddress(result.getAddress()); trade.saveWallet(); trade.setStateIfValidTransitionTo(Trade.State.MULTISIG_COMPLETED); } // update multisig participants if new state to communicate if (updateParticipants) { // get destination addresses and pub key rings // TODO: better way, use getMultisigPeers() NodeAddress peer1Address; PubKeyRing peer1PubKeyRing; NodeAddress peer2Address; PubKeyRing peer2PubKeyRing; if (trade instanceof ArbitratorTrade) { peer1Address = trade.getTaker().getNodeAddress(); peer1PubKeyRing = trade.getTaker().getPubKeyRing(); peer2Address = trade.getMaker().getNodeAddress(); peer2PubKeyRing = trade.getMaker().getPubKeyRing(); } else if (trade instanceof MakerTrade) { peer1Address = trade.getTaker().getNodeAddress(); peer1PubKeyRing = trade.getTaker().getPubKeyRing(); peer2Address = trade.getArbitrator().getNodeAddress(); peer2PubKeyRing = trade.getArbitrator().getPubKeyRing(); } else { peer1Address = trade.getMaker().getNodeAddress(); peer1PubKeyRing = trade.getMaker().getPubKeyRing(); peer2Address = trade.getArbitrator().getNodeAddress(); peer2PubKeyRing = trade.getArbitrator().getPubKeyRing(); } if (peer1Address == null) throw new RuntimeException("Peer1 address is null"); if (peer1PubKeyRing == null) throw new RuntimeException("Peer1 pub key ring is null"); if (peer2Address == null) throw new RuntimeException("Peer2 address is null"); if (peer2PubKeyRing == null) throw new RuntimeException("Peer2 pub key ring null"); log.info("{} {} sending InitMultisigRequests", trade.getClass().getSimpleName(), trade.getId()); // send to peer 1 sendInitMultisigRequest(peer1Address, peer1PubKeyRing, new SendDirectMessageListener() { @Override public void onArrived() { log.info("{} arrived: peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), peer1Address, request.getOfferId(), request.getUid()); ack1 = true; if (ack1 && ack2) completeAux(); } @Override public void onFault(String errorMessage) { log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), request.getUid(), peer1Address, errorMessage); appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage); failed(); } }); // send to peer 2 sendInitMultisigRequest(peer2Address, peer2PubKeyRing, new SendDirectMessageListener() { @Override public void onArrived() { log.info("{} arrived: peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), peer2Address, request.getOfferId(), request.getUid()); ack2 = true; if (ack1 && ack2) completeAux(); } @Override public void onFault(String errorMessage) { log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), request.getUid(), peer2Address, errorMessage); appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage); failed(); } }); } else { completeAux(); } } catch (Throwable t) { failed(t); } } private TradePeer[] getMultisigPeers() { TradePeer[] peers = new TradePeer[2]; if (trade instanceof TakerTrade) { peers[0] = processModel.getArbitrator(); peers[1] = processModel.getMaker(); } else if (trade instanceof MakerTrade) { peers[1] = processModel.getTaker(); peers[0] = processModel.getArbitrator(); } else { peers[0] = processModel.getTaker(); peers[1] = processModel.getMaker(); } return peers; } private void sendInitMultisigRequest(NodeAddress recipient, PubKeyRing pubKeyRing, SendDirectMessageListener listener) { // create multisig message with current multisig hex InitMultisigRequest request = new InitMultisigRequest( processModel.getOffer().getId(), UUID.randomUUID().toString(), Version.getP2PMessageVersion(), new Date().getTime(), trade.getSelf().getPreparedMultisigHex(), trade.getSelf().getMadeMultisigHex(), trade.getSelf().getExchangedMultisigHex(), null); log.info("Send {} with offerId {} and uid {} to peer {}", request.getClass().getSimpleName(), request.getOfferId(), request.getUid(), recipient); processModel.getP2PService().sendEncryptedDirectMessage(recipient, pubKeyRing, request, listener); } private void completeAux() { trade.addInitProgressStep(); complete(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitTradeRequest.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import com.google.common.base.Charsets; import haveno.common.taskrunner.TaskRunner; import haveno.core.exceptions.TradePriceOutOfToleranceException; import haveno.core.offer.Offer; import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.MakerTrade; import haveno.core.trade.TakerTrade; import haveno.core.trade.Trade; import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.messages.TradeProtocolVersion; import haveno.core.trade.protocol.TradePeer; import lombok.extern.slf4j.Slf4j; import java.util.Date; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static haveno.core.util.Validator.checkTradeId; import static haveno.core.util.Validator.nonEmptyStringOf; @Slf4j public class ProcessInitTradeRequest extends TradeTask { @SuppressWarnings({"unused"}) public ProcessInitTradeRequest(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } // TODO (woodser): synchronize access to setting trade state in case of concurrent requests @Override protected void run() { try { runInterceptHook(); Offer offer = checkNotNull(trade.getOffer(), "Offer must not be null"); InitTradeRequest request = (InitTradeRequest) processModel.getTradeMessage(); // validate checkNotNull(request); checkTradeId(processModel.getOfferId(), request); checkArgument(request.getTradeAmount() > 0); if (trade.getAmount().compareTo(trade.getOffer().getAmount()) > 0) throw new RuntimeException("Trade amount exceeds offer amount"); if (trade.getAmount().compareTo(trade.getOffer().getMinAmount()) < 0) throw new RuntimeException("Trade amount is less than minimum offer amount"); if (!request.getTakerNodeAddress().equals(trade.getTaker().getNodeAddress())) throw new RuntimeException("Trade's taker node address does not match request"); if (!request.getMakerNodeAddress().equals(trade.getMaker().getNodeAddress())) throw new RuntimeException("Trade's maker node address does not match request"); if (!request.getOfferId().equals(offer.getId())) throw new RuntimeException("Offer id does not match request's offer id"); // handle request as maker TradePeer sender; if (trade instanceof MakerTrade) { sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); if (sender != trade.getTaker()) throw new RuntimeException("InitTradeRequest to maker is expected from taker"); trade.getTaker().setPubKeyRing(request.getTakerPubKeyRing()); // check protocol version if (request.getTradeProtocolVersion() != TradeProtocolVersion.MULTISIG_2_3) throw new RuntimeException("Trade protocol version is not supported"); // TODO: check if contained in supported versions // check trade price try { long tradePrice = request.getTradePrice(); offer.verifyTradePrice(tradePrice); trade.setPrice(tradePrice); } catch (TradePriceOutOfToleranceException e) { failed(e.getMessage()); } catch (Throwable e2) { failed(e2); } } // handle request as arbitrator else if (trade instanceof ArbitratorTrade) { trade.getMaker().setPubKeyRing((trade.getOffer().getPubKeyRing())); // TODO: why initializing this here? trade.getArbitrator().setPubKeyRing(processModel.getPubKeyRing()); // TODO: why duplicating field in process model? if (!trade.getArbitrator().getNodeAddress().equals(request.getArbitratorNodeAddress())) throw new RuntimeException("Trade's arbitrator node address does not match request"); // check protocol version if (request.getTradeProtocolVersion() != TradeProtocolVersion.MULTISIG_2_3) throw new RuntimeException("Trade protocol version is not supported"); // TODO: check consistent from maker and taker when multiple protocols supported // handle request from maker sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); if (sender == trade.getMaker()) { trade.getTaker().setPubKeyRing(request.getTakerPubKeyRing()); trade.setTakeOfferDate(request.getCurrentDate()); // check trade price try { long tradePrice = request.getTradePrice(); offer.verifyTradePrice(tradePrice); trade.setPrice(tradePrice); } catch (TradePriceOutOfToleranceException e) { failed(e.getMessage()); } catch (Throwable e2) { failed(e2); } } // handle request from taker else if (sender == trade.getTaker()) { if (!trade.getTaker().getPubKeyRing().equals(request.getTakerPubKeyRing())) throw new RuntimeException("Taker's pub key ring does not match request's pub key ring"); if (request.getTradeAmount() != trade.getAmount().longValueExact()) throw new RuntimeException("Trade amount does not match request's trade amount"); if (request.getTradePrice() != trade.getPrice().getValue()) throw new RuntimeException("Trade price does not match request's trade price"); if (request.getCurrentDate() != trade.getTakeOfferDate().getTime()) throw new RuntimeException("Trade's take offer date does not match request's current date"); } // handle invalid sender else { throw new RuntimeException("Sender is not trade's maker or taker"); } } // handle request as taker else if (trade instanceof TakerTrade) { if (request.getTradeAmount() != trade.getAmount().longValueExact()) throw new RuntimeException("Trade amount does not match request's trade amount"); if (request.getTradePrice() != trade.getPrice().getValue()) throw new RuntimeException("Trade price does not match request's trade price"); Arbitrator arbitrator = processModel.getUser().getAcceptedArbitratorByAddress(request.getArbitratorNodeAddress()); if (arbitrator == null) throw new RuntimeException("Arbitrator is not accepted by taker"); if (trade.getArbitrator().getNodeAddress() != null && !trade.getArbitrator().getNodeAddress().equals(request.getArbitratorNodeAddress())) throw new RuntimeException("Trade's arbitrator node address does not match request"); trade.getArbitrator().setNodeAddress(request.getArbitratorNodeAddress()); trade.getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing()); sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); if (sender != trade.getArbitrator()) throw new RuntimeException("InitTradeRequest to taker is expected from arbitrator"); trade.setTakeOfferDate(request.getCurrentDate()); } // handle invalid trade type else { throw new RuntimeException("Invalid trade type to process init trade request: " + trade.getClass().getName()); } // set trading peer info if (trade.getMaker().getAccountId() == null) trade.getMaker().setAccountId(request.getMakerAccountId()); else if (!trade.getMaker().getAccountId().equals(request.getMakerAccountId())) throw new RuntimeException("Maker account id is different from previous"); if (trade.getTaker().getAccountId() == null) trade.getTaker().setAccountId(request.getTakerAccountId()); else if (!trade.getTaker().getAccountId().equals(request.getTakerAccountId())) throw new RuntimeException("Taker account id is different from previous"); if (trade.getMaker().getPaymentAccountId() == null) trade.getMaker().setPaymentAccountId(request.getMakerPaymentAccountId()); else if (!trade.getMaker().getPaymentAccountId().equals(request.getMakerPaymentAccountId())) throw new RuntimeException("Maker payment account id is different from previous"); if (trade.getTaker().getPaymentAccountId() == null) trade.getTaker().setPaymentAccountId(request.getTakerPaymentAccountId()); else if (!trade.getTaker().getPaymentAccountId().equals(request.getTakerPaymentAccountId())) throw new RuntimeException("Taker payment account id is different from previous"); sender.setPaymentMethodId(nonEmptyStringOf(request.getPaymentMethodId())); // TODO: move to process model? sender.setAccountAgeWitnessNonce(trade.getId().getBytes(Charsets.UTF_8)); sender.setAccountAgeWitnessSignature(request.getAccountAgeWitnessSignatureOfOfferId()); sender.setCurrentDate(request.getCurrentDate()); // check peer's current date processModel.getAccountAgeWitnessService().verifyPeersCurrentDate(new Date(sender.getCurrentDate())); // persist trade trade.addInitProgressStep(); processModel.getTradeManager().requestPersistence(); complete(); } catch (Throwable t) { failed(t); } } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; import haveno.core.account.sign.SignedWitness; import haveno.core.support.dispute.Dispute; import haveno.core.trade.BuyerTrade; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.messages.PaymentReceivedMessage; import haveno.core.trade.messages.PaymentSentMessage; import haveno.core.util.Validator; import lombok.extern.slf4j.Slf4j; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class ProcessPaymentReceivedMessage extends TradeTask { public ProcessPaymentReceivedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); log.debug("current trade state " + trade.getState()); PaymentReceivedMessage message = (PaymentReceivedMessage) processModel.getTradeMessage(); checkNotNull(message); Validator.checkTradeId(processModel.getOfferId(), message); // verify signature of payment received message HavenoUtils.verifyPaymentReceivedMessage(trade, message); // update to the latest peer address of our peer if message is correct trade.getSeller().setNodeAddress(processModel.getTempTradePeerNodeAddress()); if (trade.getSeller().getNodeAddress().equals(trade.getBuyer().getNodeAddress())) trade.getBuyer().setNodeAddress(null); // tests can reuse addresses trade.requestPersistence(); // ack and complete if already processed if (trade.isPaymentReceivedMessageProcessed() && trade.isPayoutPublished()) { log.warn("Received another PaymentReceivedMessage which was already processed, ACKing"); complete(); return; } // set state to confirmed payment receipt before processing trade.setStateIfValidTransitionTo(Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT); // cannot process until wallet sees deposits unlocked if (!trade.isDepositsUnlocked()) { trade.syncAndPollWallet(); if (!trade.isDepositsUnlocked()) { throw new RuntimeException("Cannot process PaymentReceivedMessage until the trade wallet sees that the deposits are unlocked for " + trade.getClass().getSimpleName() + " " + trade.getId()); } } // set state if (!trade.isPayoutPublished()) trade.getSeller().setUpdatedMultisigHex(message.getUpdatedMultisigHex()); trade.getBuyer().setAccountAgeWitness(message.getBuyerAccountAgeWitness()); if (trade.isArbitrator() && trade.getBuyer().getPaymentSentMessage() == null) { checkNotNull(message.getPaymentSentMessage(), "PaymentSentMessage is null for arbitrator"); trade.getBuyer().setPaymentSentMessage(message.getPaymentSentMessage()); trade.getBuyer().setUpdatedMultisigHex(message.getPaymentSentMessage().getUpdatedMultisigHex()); } trade.requestPersistence(); // process payout tx if not confirmed if (!trade.isPayoutConfirmed()) processPayoutTx(message); // close open disputes if (trade.isPayoutPublished() && trade.getDisputeState().isDisputed()) { trade.advanceDisputeState(Trade.DisputeState.DISPUTE_CLOSED); for (Dispute dispute : trade.getDisputes()) dispute.setIsClosed(); } // advance state, arbitrator auto completes when payout published trade.setStateIfValidTransitionTo(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); // buyer republishes signed witness for resilience SignedWitness signedWitness = message.getBuyerSignedWitness(); if (signedWitness != null && trade instanceof BuyerTrade) { processModel.getAccountAgeWitnessService().publishOwnSignedWitness(signedWitness); } // complete trade.requestPersistence(); complete(); } catch (Throwable t) { // handle illegal exception if (HavenoUtils.isIllegal(t)) { trade.getSeller().setPaymentReceivedMessage(null); // stops reprocessing trade.requestPersistence(); } failed(t); } } private void processPayoutTx(PaymentReceivedMessage message) { // update wallet trade.importMultisigHex(); trade.syncAndPollWallet(); // handle if payout tx not published if (!trade.isPayoutPublished()) { // nack with updated multisig info if no payout tx provided if (message.getUnsignedPayoutTxHex() == null && message.getSignedPayoutTxHex() == null && message.getPayoutTxId() == null) { throw new IllegalStateException("No payout tx provided in PaymentReceivedMessage for " + trade.getClass().getSimpleName() + " " + trade.getId()); } // wait to publish payout tx if defer flag set from seller (payout is expected) if (message.isDeferPublishPayout()) { log.info("Deferring publishing payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId()); trade.pollWalletNormallyForMs(Trade.POLL_WALLET_NORMALLY_DEFAULT_PERIOD_MS); // override idling for (int i = 0; i < 5; i++) { if (trade.isPayoutPublished()) break; HavenoUtils.waitFor(Trade.DEFER_PUBLISH_MS / 5); } if (!trade.isPayoutPublished()) trade.syncAndPollWallet(); } // verify and publish payout tx if (!trade.isPayoutPublished()) { try { if (message.getPayoutTxId() != null && trade.isBuyer()) { trade.processBuyerPayout(message.getPayoutTxId()); // buyer can validate payout tx by id with main wallet (in case of multisig issues) } else if (message.getSignedPayoutTxHex() != null) { log.info("{} {} publishing signed payout tx from seller", trade.getClass().getSimpleName(), trade.getId()); trade.processPayoutTx(message.getSignedPayoutTxHex(), false, true); } else { PaymentSentMessage paymentSentMessage = (trade.isArbitrator() ? trade.getBuyer() : trade.getArbitrator()).getPaymentSentMessage(); if (paymentSentMessage == null) throw new RuntimeException("Process model does not have payment sent message for " + trade.getClass().getSimpleName() + " " + trade.getId()); if (trade.getPayoutTxHex() == null) { // unsigned log.info("{} {} verifying, signing, and publishing payout tx", trade.getClass().getSimpleName(), trade.getId()); trade.processPayoutTx(message.getUnsignedPayoutTxHex(), true, true); } else { log.info("{} {} re-verifying and publishing signed payout tx", trade.getClass().getSimpleName(), trade.getId()); trade.processPayoutTx(trade.getPayoutTxHex(), false, true); } } } catch (Exception e) { HavenoUtils.waitFor(trade.getXmrConnectionService().getRefreshPeriodMs()); // wait to see published tx trade.syncAndPollWallet(); if (trade.isPayoutPublished()) log.info("Payout tx already published for {} {}", trade.getClass().getName(), trade.getId()); else throw e; } } } } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentSentMessage.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.messages.PaymentSentMessage; import haveno.core.util.Validator; import lombok.extern.slf4j.Slf4j; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class ProcessPaymentSentMessage extends TradeTask { public ProcessPaymentSentMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); log.debug("current trade state " + trade.getState()); PaymentSentMessage message = (PaymentSentMessage) processModel.getTradeMessage(); checkNotNull(message); Validator.checkTradeId(processModel.getOfferId(), message); // verify signature of payment sent message HavenoUtils.verifyPaymentSentMessage(trade, message); // update latest peer address trade.getBuyer().setNodeAddress(processModel.getTempTradePeerNodeAddress()); trade.requestPersistence(); // cannot process until wallet sees deposits confirmed if (!trade.isDepositsConfirmed()) { trade.syncAndPollWallet(); if (!trade.isDepositsConfirmed()) { throw new RuntimeException("Cannot process PaymentSentMessage until the trade wallet sees that the deposits are confirmed for " + trade.getClass().getSimpleName() + " " + trade.getId()); } } // update state from message trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex()); trade.getSeller().setAccountAgeWitness(message.getSellerAccountAgeWitness()); String counterCurrencyTxId = message.getCounterCurrencyTxId(); if (counterCurrencyTxId != null && counterCurrencyTxId.length() < 100) trade.setCounterCurrencyTxId(counterCurrencyTxId); String counterCurrencyExtraData = message.getCounterCurrencyExtraData(); if (counterCurrencyExtraData != null && counterCurrencyExtraData.length() < 100) trade.setCounterCurrencyExtraData(counterCurrencyExtraData); // if seller, decrypt buyer's payment account payload if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey()); // update state trade.setStateIfValidTransitionTo(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); trade.requestPersistence(); complete(); } catch (Throwable t) { failed(t); } } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/ProcessSignContractRequest.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import com.google.common.base.Charsets; import haveno.common.app.Version; import haveno.common.crypto.Encryption; import haveno.common.crypto.Hash; import haveno.common.crypto.PubKeyRing; import haveno.common.crypto.ScryptUtil; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.Contract; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.messages.SignContractRequest; import haveno.core.trade.messages.SignContractResponse; import haveno.core.trade.protocol.TradePeer; import haveno.core.util.JsonUtil; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.SendDirectMessageListener; import lombok.extern.slf4j.Slf4j; import javax.crypto.SecretKey; import java.util.Date; import java.util.UUID; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class ProcessSignContractRequest extends TradeTask { private boolean ack1 = false; private boolean ack2 = false; @SuppressWarnings({"unused"}) public ProcessSignContractRequest(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); // extract fields from request // TODO (woodser): verify request and from maker or taker SignContractRequest request = (SignContractRequest) processModel.getTradeMessage(); TradePeer sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); sender.setDepositTxHash(request.getDepositTxHash()); sender.setAccountId(request.getAccountId()); sender.setPaymentAccountPayloadHash(request.getPaymentAccountPayloadHash()); sender.setPayoutAddressString(request.getPayoutAddress()); // maker sends witness signature of deposit tx hash if (sender == trade.getMaker()) { sender.setAccountAgeWitnessNonce(request.getDepositTxHash().getBytes(Charsets.UTF_8)); sender.setAccountAgeWitnessSignature(request.getAccountAgeWitnessSignatureOfDepositHash()); } // sign contract only when received from both peers if (processModel.getMaker().getPaymentAccountPayloadHash() == null || processModel.getTaker().getPaymentAccountPayloadHash() == null) { complete(); return; } // create and sign contract Contract contract = trade.createContract(); String contractAsJson = JsonUtil.objectToJson(contract); byte[] signature = HavenoUtils.sign(processModel.getKeyRing(), contractAsJson); // save contract and signature trade.setContract(contract); trade.setContractAsJson(contractAsJson); trade.setContractHash(Hash.getSha256Hash(checkNotNull(contractAsJson))); trade.getSelf().setContractSignature(signature); // traders send encrypted payment account payload byte[] encryptedPaymentAccountPayload = null; if (!trade.isArbitrator()) { // generate random key to encrypt payment account payload byte[] decryptionKey = ScryptUtil.getKeyCrypterScrypt().deriveKey(UUID.randomUUID().toString()).getKey(); trade.getSelf().setPaymentAccountKey(decryptionKey); // encrypt payment account payload byte[] unencrypted = trade.getSelf().getPaymentAccountPayload().toProtoMessage().toByteArray(); SecretKey sk = Encryption.getSecretKeyFromBytes(trade.getSelf().getPaymentAccountKey()); encryptedPaymentAccountPayload = Encryption.encrypt(unencrypted, sk); } // create response with contract signature SignContractResponse response = new SignContractResponse( trade.getOffer().getId(), UUID.randomUUID().toString(), Version.getP2PMessageVersion(), new Date().getTime(), contractAsJson, signature, encryptedPaymentAccountPayload); // get response recipients. only arbitrator sends response to both peers NodeAddress recipient1 = trade instanceof ArbitratorTrade ? trade.getMaker().getNodeAddress() : trade.getTradePeer().getNodeAddress(); PubKeyRing recipient1PubKey = trade instanceof ArbitratorTrade ? trade.getMaker().getPubKeyRing() : trade.getTradePeer().getPubKeyRing(); NodeAddress recipient2 = trade instanceof ArbitratorTrade ? trade.getTaker().getNodeAddress() : null; PubKeyRing recipient2PubKey = trade instanceof ArbitratorTrade ? trade.getTaker().getPubKeyRing() : null; // send response to recipient 1 processModel.getP2PService().sendEncryptedDirectMessage(recipient1, recipient1PubKey, response, new SendDirectMessageListener() { @Override public void onArrived() { log.info("{} arrived: trading peer={}; offerId={}; uid={}", response.getClass().getSimpleName(), recipient1, trade.getId()); ack1 = true; if (ack1 && (recipient2 == null || ack2)) completeAux(); } @Override public void onFault(String errorMessage) { log.error("Sending {} failed: uid={}; peer={}; error={}", response.getClass().getSimpleName(), recipient1, trade.getId(), errorMessage); appendToErrorMessage("Sending message failed: message=" + response + "\nerrorMessage=" + errorMessage); failed(); } }); // send response to recipient 2 if applicable if (recipient2 != null) { processModel.getP2PService().sendEncryptedDirectMessage(recipient2, recipient2PubKey, response, new SendDirectMessageListener() { @Override public void onArrived() { log.info("{} arrived: trading peer={}; offerId={}; uid={}", response.getClass().getSimpleName(), recipient2, trade.getId()); ack2 = true; if (ack1 && ack2) completeAux(); } @Override public void onFault(String errorMessage) { log.error("Sending {} failed: uid={}; peer={}; error={}", response.getClass().getSimpleName(), recipient2, trade.getId(), errorMessage); appendToErrorMessage("Sending message failed: message=" + response + "\nerrorMessage=" + errorMessage); failed(); } }); } } catch (Throwable t) { failed(t); } } private void completeAux() { trade.addInitProgressStep(); trade.setState(Trade.State.CONTRACT_SIGNED); processModel.getTradeManager().requestPersistence(); complete(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; import haveno.core.support.dispute.Dispute; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import lombok.extern.slf4j.Slf4j; import monero.wallet.model.MoneroTxWallet; @Slf4j public class SellerPreparePaymentReceivedMessage extends TradeTask { @SuppressWarnings({"unused"}) public SellerPreparePaymentReceivedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); // check connection trade.verifyDaemonConnection(); // prepare payout info for payment received message if (!trade.isPayoutPublished() || (!trade.isPayoutFinalized() && trade.getPayoutTxHex() == null)) { // synchronize on lock for wallet operations synchronized (trade.getWalletLock()) { synchronized (HavenoUtils.getWalletFunctionLock()) { // import multisig hex unless already signed if (trade.getPayoutTxHex() == null) { trade.importMultisigHex(); } // verify, sign, and publish payout tx if given if (trade.getBuyer().getPaymentSentMessage().getPayoutTxHex() != null && !trade.getProcessModel().isPaymentSentPayoutTxStale()) { try { if (trade.getPayoutTxHex() == null) { log.info("Seller verifying, signing, and publishing payout tx for trade {}", trade.getId()); trade.processPayoutTx(trade.getBuyer().getPaymentSentMessage().getPayoutTxHex(), true, true); } else { log.warn("Seller publishing previously signed payout tx for trade {}", trade.getId()); trade.processPayoutTx(trade.getPayoutTxHex(), false, true); } } catch (IllegalArgumentException | IllegalStateException e) { log.warn("Illegal state or argument verifying, signing, and publishing payout tx for {} {}. Creating new unsigned payout tx. error={}. ", trade.getClass().getSimpleName(), trade.getId(), e.getMessage(), e); createUnsignedPayoutTx(); } catch (Exception e) { log.warn("Error verifying, signing, and publishing payout tx for trade {}: {}", trade.getId(), e.getMessage(), e); throw e; } } // otherwise create unsigned payout tx else if (trade.getSelf().getUnsignedPayoutTxHex() == null) { createUnsignedPayoutTx(); } } } } else if (!trade.isPayoutPublished() && trade.getPayoutTxHex() != null) { // republish payout tx from previous message log.info("Seller re-verifying and publishing signed payout tx for trade {}", trade.getId()); trade.processPayoutTx(trade.getPayoutTxHex(), false, true); } else if (!trade.isPayoutFinalized() && (trade.getArbitrator().getPaymentReceivedMessage() == null || trade.getBuyer().getPaymentReceivedMessage() == null)) { // update multisig info if payout not finalized and recreating messages synchronized (trade.getWalletLock()) { if (trade.walletExists()) { synchronized (HavenoUtils.getWalletFunctionLock()) { trade.importMultisigHex(); trade.exportMultisigHex(); } } } } // close open disputes if (trade.isPayoutPublished() && trade.getDisputeState().ordinal() >= Trade.DisputeState.DISPUTE_PREPARING.ordinal()) { trade.advanceDisputeState(Trade.DisputeState.DISPUTE_CLOSED); for (Dispute dispute : trade.getDisputes()) dispute.setIsClosed(); } trade.requestPersistence(); complete(); } catch (Throwable t) { if (HavenoUtils.isIllegal(t)) { log.error("Illegal exception preparing payment received message in {} {}: {}", trade.getClass().getSimpleName(), trade.getId(), t.getMessage(), t); trade.exportMultisigHex(); complete(); // proceed to send the message to perform nack flow with updated multsig state } else { failed(t); } } } private void createUnsignedPayoutTx() { log.info("Seller creating unsigned payout tx for trade {}", trade.getId()); try { trade.getProcessModel().setPaymentSentPayoutTxStale(true); MoneroTxWallet payoutTx = trade.createPayoutTx(); trade.setPayoutTx(payoutTx); trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); } catch (Exception e) { if (trade.isPayoutPublished()) log.info("Payout tx already published for {} {}", trade.getClass().getName(), trade.getId()); else throw e; } } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import com.google.common.base.Charsets; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.crypto.Sig; import haveno.common.taskrunner.TaskRunner; import haveno.core.account.sign.SignedWitness; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.network.MessageState; import haveno.core.trade.HavenoUtils; import haveno.core.trade.SellerTrade; import haveno.core.trade.Trade; import haveno.core.trade.messages.PaymentReceivedMessage; import haveno.core.trade.messages.TradeMailboxMessage; import haveno.core.util.JsonUtil; import javafx.beans.value.ChangeListener; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import static com.google.common.base.Preconditions.checkArgument; import java.util.concurrent.TimeUnit; import org.apache.commons.lang3.StringUtils; @Slf4j @EqualsAndHashCode(callSuper = true) public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask { private SignedWitness signedWitness = null; private ChangeListener listener; private Timer timer; private static final int MAX_RESEND_ATTEMPTS = 20; private long delayInMin = -1; private int resendCounter = 0; private String unsignedPayoutTxHex = null; private String signedPayoutTxHex = null; private String updatedMultisigHex = null; private PaymentReceivedMessage message = null; public SellerSendPaymentReceivedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); // reset nack state if (getReceiver().isPaymentReceivedMessageNacked()) { getReceiver().setPaymentReceivedMessageState(MessageState.UNDEFINED); } // skip if stopped if (stopSending()) { if (!isCompleted()) complete(); return; } // reset ack state getReceiver().setPaymentReceivedMessageState(MessageState.UNDEFINED); super.run(); } catch (Throwable t) { failed(t); } } @Override protected TradeMailboxMessage getMailboxMessage(String tradeId) { if (getReceiver().getPaymentReceivedMessage() == null) { // sign account witness if (trade.getSelf().getPaymentAccountPayload() == null) { log.warn("Cannot sign account age witness for {} {} as no payment account is set", trade.getClass().getSimpleName(), trade.getId()); } else { AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService(); if (accountAgeWitnessService.isSignWitnessTrade(trade)) { try { accountAgeWitnessService.traderSignAndPublishPeersAccountAgeWitness(trade).ifPresent(witness -> signedWitness = witness); log.info("{} {} signed and published peers account age witness", trade.getClass().getSimpleName(), trade.getId()); } catch (Exception e) { log.warn("Failed to sign and publish peer's account age witness for {} {}, error={}\n", getClass().getSimpleName(), trade.getId(), e.getMessage(), e); } } } // We do not use a real unique ID here as we want to be able to re-send the exact same message in case the // peer does not respond with an ACK msg in a certain time interval. To avoid that we get dangling mailbox // messages where only the one which gets processed by the peer would be removed we use the same uid. All // other data stays the same when we re-send the message at any time later. String deterministicId = HavenoUtils.getDeterministicId(trade, PaymentReceivedMessage.class, getReceiverNodeAddress()); boolean deferPublishPayout = getReceiver() == trade.getArbitrator() && (trade.isPayoutPublished() || trade.getOtherPeer(getReceiver()).isPaymentReceivedMessageArrived()); // informs receiver to expect payout so delay processing unsignedPayoutTxHex = trade.getPayoutTxHex() == null ? trade.getSelf().getUnsignedPayoutTxHex() : null; // signed signedPayoutTxHex = trade.getPayoutTxHex(); updatedMultisigHex = trade.getSelf().getUpdatedMultisigHex(); message = new PaymentReceivedMessage( tradeId, processModel.getMyNodeAddress(), deterministicId, unsignedPayoutTxHex, signedPayoutTxHex, updatedMultisigHex, deferPublishPayout, trade.getTradePeer().getAccountAgeWitness(), signedWitness, getReceiver() == trade.getArbitrator() ? trade.getBuyer().getPaymentSentMessage() : null, // buyer already has payment sent message, trade.getPayoutTxId() ); // verify message if (trade.isPayoutPublished()) { checkArgument(message.getUpdatedMultisigHex() != null || message.getPayoutTxId() != null, "PaymentReceivedMessage does not include updated multisig hex or payout tx id after payout published"); } // sign message try { String messageAsJson = JsonUtil.objectToJson(message); byte[] sig = Sig.sign(processModel.getP2PService().getKeyRing().getSignatureKeyPair().getPrivate(), messageAsJson.getBytes(Charsets.UTF_8)); message.setSellerSignature(sig); getReceiver().setPaymentReceivedMessage(message); trade.requestPersistence(); } catch (Exception e) { throw new RuntimeException(e); } } return getReceiver().getPaymentReceivedMessage(); } @Override protected void setStateSent() { log.info("{} sent: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness); getReceiver().setPaymentReceivedMessageState(MessageState.SENT); tryToSendAgainLater(); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateFault() { log.error("{} failed: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness); getReceiver().setPaymentReceivedMessageState(MessageState.FAILED); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateStoredInMailbox() { log.info("{} stored in mailbox: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness); getReceiver().setPaymentReceivedMessageState(MessageState.STORED_IN_MAILBOX); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateArrived() { log.info("{} arrived: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness); getReceiver().setPaymentReceivedMessageState(MessageState.ARRIVED); processModel.getTradeManager().requestPersistence(); } private void cleanup() { if (timer != null) { timer.stop(); } if (listener != null) { trade.getBuyer().getPaymentReceivedMessageStateProperty().removeListener(listener); } } private void tryToSendAgainLater() { // skip if stopped if (stopSending()) return; // stop after max attempts if (resendCounter >= MAX_RESEND_ATTEMPTS) { cleanup(); log.warn("We never received an ACK message when sending the PaymentReceivedMessage to the peer. We stop trying to send the message."); return; } // register listeners once if (resendCounter == 0) { listener = (observable, oldValue, newValue) -> onMessageStateChange(newValue); getReceiver().getPaymentReceivedMessageStateProperty().addListener(listener); onMessageStateChange(getReceiver().getPaymentReceivedMessageStateProperty().get()); } // set resend delay if (resendCounter == 0) { delayInMin = SendMailboxMessageTask.INITIAL_RESEND_DELAY_MINS_FIRST; } else if (resendCounter == 1) { delayInMin = SendMailboxMessageTask.INITIAL_RESEND_DELAY_MINS_SECOND; } else { delayInMin = Math.min(TimeUnit.MILLISECONDS.toMinutes(TradeMailboxMessage.TTL), delayInMin * SendMailboxMessageTask.RESEND_DELAY_MULTIPLIER); } // use minimum delay if stored to mailbox if (getReceiver().isPaymentReceivedMessageStored()) { delayInMin = Math.max(delayInMin, SendMailboxMessageTask.INITIAL_RESEND_DELAY_MINS_MAILBOX); } // send again after delay log.info("We will send the message again to the peer after a delay of {} min.", delayInMin); if (timer != null) timer.stop(); timer = UserThread.runAfter(this::run, delayInMin, TimeUnit.MINUTES); resendCounter++; } private void onMessageStateChange(MessageState newValue) { if (stopSending()) { cleanup(); } } protected boolean stopSending() { if (isMessageReceived()) return true; // stop if message received if (!trade.isPaymentReceived()) return true; // stop if trade state reset if (trade.isPayoutPublished() && !((SellerTrade) trade).resendPaymentReceivedMessagesWithinDuration()) return true; // stop if payout is published and we are not in the resend period // check if message state is outdated if (message != null && !message.equals(getReceiver().getPaymentReceivedMessage())) return true; if (unsignedPayoutTxHex != null && !StringUtils.equals(unsignedPayoutTxHex, trade.getSelf().getUnsignedPayoutTxHex())) return true; if (signedPayoutTxHex != null && !StringUtils.equals(signedPayoutTxHex, trade.getPayoutTxHex())) return true; if (updatedMultisigHex != null && !StringUtils.equals(updatedMultisigHex, trade.getSelf().getUpdatedMultisigHex())) return true; return false; } protected boolean isMessageReceived() { return getReceiver().isPaymentReceivedMessageAckedOrNacked(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessageToArbitrator.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; import haveno.core.trade.protocol.TradePeer; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; @EqualsAndHashCode(callSuper = true) @Slf4j public class SellerSendPaymentReceivedMessageToArbitrator extends SellerSendPaymentReceivedMessage { public SellerSendPaymentReceivedMessageToArbitrator(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected TradePeer getReceiver() { return trade.getArbitrator(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessageToBuyer.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; import haveno.core.trade.protocol.TradePeer; import haveno.network.p2p.mailbox.MailboxMessage; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; @EqualsAndHashCode(callSuper = true) @Slf4j public class SellerSendPaymentReceivedMessageToBuyer extends SellerSendPaymentReceivedMessage { public SellerSendPaymentReceivedMessageToBuyer(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected TradePeer getReceiver() { return trade.getBuyer(); } @Override protected void setStateSent() { if (trade.getState().equals(Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG)) { trade.setState(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); } else { trade.advanceState(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); // do not revert previous send progress } super.setStateSent(); } @Override protected void setStateFault() { trade.setStateIfValidTransitionTo(Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG); super.setStateFault(); } @Override protected void setStateStoredInMailbox() { trade.setStateIfValidTransitionTo(Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG); super.setStateStoredInMailbox(); } @Override protected void setStateArrived() { trade.setStateIfValidTransitionTo(Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG); super.setStateArrived(); } // continue execution on fault so payment received message is sent to arbitrator @Override protected void onFault(String errorMessage, MailboxMessage message) { setStateFault(); appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage); complete(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositRequest.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.app.Version; import haveno.common.crypto.PubKeyRing; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.messages.DepositRequest; import haveno.core.trade.messages.SignContractResponse; import haveno.core.trade.protocol.TradePeer; import haveno.network.p2p.SendDirectMessageListener; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.UUID; @Slf4j public class SendDepositRequest extends TradeTask { @SuppressWarnings({"unused"}) public SendDepositRequest(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); // compare contracts String contractAsJson = trade.getContractAsJson(); SignContractResponse response = (SignContractResponse) processModel.getTradeMessage(); if (!contractAsJson.equals(response.getContractAsJson())) { trade.getContract().printDiff(response.getContractAsJson()); failed("Contracts are not matching"); return; } // get peer info TradePeer peer = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); PubKeyRing peerPubKeyRing = peer.getPubKeyRing(); // save peer's encrypted payment account payload peer.setEncryptedPaymentAccountPayload(response.getEncryptedPaymentAccountPayload()); if (peer.getEncryptedPaymentAccountPayload() == null) throw new RuntimeException("Peer did not send encrypted payment account payload"); // verify signature // TODO (woodser): transfer contract for convenient comparison? byte[] signature = response.getContractSignature(); if (!HavenoUtils.isSignatureValid(peerPubKeyRing, contractAsJson, signature)) throw new RuntimeException("Peer's contract signature is invalid"); // set peer's signature peer.setContractSignature(signature); // send deposit request when all contract signatures received if (processModel.getArbitrator().getContractSignature() != null && processModel.getMaker().getContractSignature() != null && processModel.getTaker().getContractSignature() != null) { // create request for arbitrator to deposit funds to multisig DepositRequest request = new DepositRequest( trade.getOffer().getId(), UUID.randomUUID().toString(), Version.getP2PMessageVersion(), new Date().getTime(), trade.getSelf().getContractSignature(), trade.getSelf().getDepositTxHex(), trade.getSelf().getDepositTxKey(), trade.getSelf().getPaymentAccountKey()); // update trade state trade.setState(Trade.State.SENT_PUBLISH_DEPOSIT_TX_REQUEST); processModel.getTradeManager().requestPersistence(); // send request to arbitrator log.info("Sending {} to arbitrator {}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getArbitrator().getNodeAddress(), trade.getId(), request.getUid()); processModel.getP2PService().sendEncryptedDirectMessage(trade.getArbitrator().getNodeAddress(), trade.getArbitrator().getPubKeyRing(), request, new SendDirectMessageListener() { @Override public void onArrived() { log.info("{} arrived: arbitrator={}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getArbitrator().getNodeAddress(), trade.getId(), request.getUid()); trade.setStateIfValidTransitionTo(Trade.State.SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST); processModel.getTradeManager().requestPersistence(); trade.addInitProgressStep(); complete(); } @Override public void onFault(String errorMessage) { log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), trade.getArbitrator().getNodeAddress(), trade.getId(), errorMessage); appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage); failed(); } }); } else { List awaitingSignaturesFrom = new ArrayList<>(); if (processModel.getArbitrator().getContractSignature() == null) awaitingSignaturesFrom.add("arbitrator"); if (processModel.getMaker().getContractSignature() == null) awaitingSignaturesFrom.add("maker"); if (processModel.getTaker().getContractSignature() == null) awaitingSignaturesFrom.add("taker"); log.info("Waiting for contract signature from {} to send deposit request", awaitingSignaturesFrom); complete(); // does not yet have needed signatures } } catch (Throwable t) { failed(t); } } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import java.util.concurrent.TimeUnit; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.taskrunner.TaskRunner; import haveno.core.network.MessageState; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.messages.DepositsConfirmedMessage; import haveno.core.trade.messages.TradeMailboxMessage; import lombok.extern.slf4j.Slf4j; /** * Send message on first confirmation to decrypt peer payment account and update multisig hex. */ @Slf4j public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTask { private Timer timer; private static final int MAX_RESEND_ATTEMPTS = 20; private long delayInMin = -1; private int resendCounter = 0; private DepositsConfirmedMessage message; public SendDepositsConfirmedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); // skip if already acked or payout published if (isAckedByReceiver() || trade.isPayoutPublished()) { if (!isCompleted()) complete(); return; } super.run(); } catch (Throwable t) { failed(t); } } @Override protected TradeMailboxMessage getMailboxMessage(String tradeId) { if (message == null) { // export multisig hex once if (trade.getSelf().getUpdatedMultisigHex() == null) { trade.exportMultisigHex(); } // We do not use a real unique ID here as we want to be able to re-send the exact same message in case the // peer does not respond with an ACK msg in a certain time interval. To avoid that we get dangling mailbox // messages where only the one which gets processed by the peer would be removed we use the same uid. All // other data stays the same when we re-send the message at any time later. String deterministicId = HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, getReceiverNodeAddress()); message = new DepositsConfirmedMessage( trade.getOffer().getId(), processModel.getMyNodeAddress(), processModel.getPubKeyRing(), deterministicId, trade.getBuyer() == trade.getTradePeer(getReceiverNodeAddress()) ? trade.getSeller().getPaymentAccountKey() : null, // buyer receives seller's payment account decryption key trade.getSelf().getUpdatedMultisigHex()); } return message; } @Override protected void setStateSent() { getReceiver().setDepositsConfirmedMessageState(MessageState.SENT); tryToSendAgainLater(); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateArrived() { getReceiver().setDepositsConfirmedMessageState(MessageState.ARRIVED); } @Override protected void setStateStoredInMailbox() { getReceiver().setDepositsConfirmedMessageState(MessageState.STORED_IN_MAILBOX); } @Override protected void setStateFault() { getReceiver().setDepositsConfirmedMessageState(MessageState.FAILED); } private void cleanup() { if (timer != null) { timer.stop(); } } private void tryToSendAgainLater() { // skip if already acked or payout published if (isAckedByReceiver() || trade.isPayoutPublished()) return; // stop after max attempts if (resendCounter >= MAX_RESEND_ATTEMPTS) { cleanup(); log.warn("We never received an ACK message when sending the DepositsConfirmedMessage to the peer. We stop trying to send the message."); return; } // set resend delay if (resendCounter == 0) { delayInMin = SendMailboxMessageTask.INITIAL_RESEND_DELAY_MINS_FIRST; } else if (resendCounter == 1) { delayInMin = SendMailboxMessageTask.INITIAL_RESEND_DELAY_MINS_SECOND; } else { delayInMin = Math.min(TimeUnit.MILLISECONDS.toMinutes(TradeMailboxMessage.TTL), delayInMin * SendMailboxMessageTask.RESEND_DELAY_MULTIPLIER); } // send again after delay log.info("We will send the message again to the peer after a delay of {} min.", delayInMin); if (timer != null) timer.stop(); timer = UserThread.runAfter(this::run, delayInMin, TimeUnit.MINUTES); resendCounter++; } private boolean isAckedByReceiver() { return getReceiver().isDepositsConfirmedMessageAcked(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToArbitrator.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; import haveno.core.trade.protocol.TradePeer; import lombok.extern.slf4j.Slf4j; /** * Send message on first confirmation to decrypt peer payment account and update multisig hex. */ @Slf4j public class SendDepositsConfirmedMessageToArbitrator extends SendDepositsConfirmedMessage { public SendDepositsConfirmedMessageToArbitrator(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected TradePeer getReceiver() { return trade.getArbitrator(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToBuyer.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; import haveno.core.trade.protocol.TradePeer; import lombok.extern.slf4j.Slf4j; /** * Send message on first confirmation to decrypt peer payment account and update multisig hex. */ @Slf4j public class SendDepositsConfirmedMessageToBuyer extends SendDepositsConfirmedMessage { public SendDepositsConfirmedMessageToBuyer(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected TradePeer getReceiver() { return trade.getBuyer(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToSeller.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; import haveno.core.trade.protocol.TradePeer; import lombok.extern.slf4j.Slf4j; /** * Send message on first confirmation to decrypt peer payment account and update multisig hex. */ @Slf4j public class SendDepositsConfirmedMessageToSeller extends SendDepositsConfirmedMessage { public SendDepositsConfirmedMessageToSeller(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected TradePeer getReceiver() { return trade.getSeller(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/SendMailboxMessageTask.java ================================================ /* * 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 . */ package haveno.core.trade.protocol.tasks; import java.util.concurrent.TimeUnit; import haveno.common.crypto.PubKeyRing; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; import haveno.core.trade.protocol.TradePeer; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.SendMailboxMessageListener; import haveno.network.p2p.mailbox.MailboxMessage; import lombok.extern.slf4j.Slf4j; @Slf4j public abstract class SendMailboxMessageTask extends TradeTask { public static final long INITIAL_RESEND_DELAY_MINS_FIRST = 2; public static final long INITIAL_RESEND_DELAY_MINS_SECOND = 15; public static final long INITIAL_RESEND_DELAY_MINS_MAILBOX = TimeUnit.HOURS.toMinutes(6); public static final int RESEND_DELAY_MULTIPLIER = 2; public SendMailboxMessageTask(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } protected abstract TradePeer getReceiver(); protected NodeAddress getReceiverNodeAddress() { return getReceiver().getNodeAddress(); } protected PubKeyRing getReceiverPubKeyRing() { return getReceiver().getPubKeyRing(); } protected abstract MailboxMessage getMailboxMessage(String tradeId); protected abstract void setStateSent(); protected abstract void setStateArrived(); protected abstract void setStateStoredInMailbox(); protected abstract void setStateFault(); @Override protected void run() { try { runInterceptHook(); String id = processModel.getOfferId(); MailboxMessage message = getMailboxMessage(id); setStateSent(); NodeAddress peersNodeAddress = getReceiverNodeAddress(); log.info("Send {} to peer {} for {} {}, uid={}", message.getClass().getSimpleName(), peersNodeAddress, trade.getClass().getSimpleName(), trade.getId(), message.getUid()); TradeTask task = this; processModel.getP2PService().getMailboxMessageService().sendEncryptedMailboxMessage( peersNodeAddress, getReceiverPubKeyRing(), message, new SendMailboxMessageListener() { @Override public void onArrived() { log.info("{} arrived at peer {}. tradeId={}, uid={}", message.getClass().getSimpleName(), peersNodeAddress, trade.getId(), message.getUid()); setStateArrived(); if (!task.isCompleted()) complete(); } @Override public void onStoredInMailbox() { log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}", message.getClass().getSimpleName(), peersNodeAddress, trade.getId(), message.getUid()); SendMailboxMessageTask.this.onStoredInMailbox(); } @Override public void onFault(String errorMessage) { if (processModel.getP2PService().isShutDownStarted()) return; log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", message.getClass().getSimpleName(), peersNodeAddress, trade.getId(), message.getUid(), errorMessage); SendMailboxMessageTask.this.onFault(errorMessage, message); } } ); } catch (Throwable t) { failed(t); } } protected void onStoredInMailbox() { setStateStoredInMailbox(); if (!isCompleted()) complete(); } protected void onFault(String errorMessage, MailboxMessage message) { setStateFault(); appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage); failed(errorMessage); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; import haveno.core.offer.OfferDirection; import haveno.core.trade.HavenoUtils; import haveno.core.trade.TakerTrade; import haveno.core.trade.Trade; import haveno.core.trade.protocol.TradeProtocol; import haveno.core.xmr.model.XmrAddressEntry; import lombok.extern.slf4j.Slf4j; import monero.common.MoneroRpcConnection; import monero.wallet.model.MoneroTxWallet; import java.math.BigInteger; @Slf4j public class TakerReserveTradeFunds extends TradeTask { public TakerReserveTradeFunds(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); // taker trade expected if (!(trade instanceof TakerTrade)) { throw new RuntimeException("Expected taker trade but was " + trade.getClass().getSimpleName() + " " + trade.getShortId() + ". That should never happen."); } // create reserve tx unless deposit not required from buyer as taker MoneroTxWallet reserveTx = null; if (!trade.isBuyerAsTakerWithoutDeposit()) { synchronized (HavenoUtils.xmrWalletService.getWalletLock()) { // check for timeout if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create reserve tx, tradeId=" + trade.getShortId()); trade.startProtocolTimeout(); // collect relevant info BigInteger takerFee = trade.getTakerFee(); BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getAmount() : BigInteger.ZERO; BigInteger securityDeposit = trade.getSecurityDepositBeforeMiningFee(); BigInteger penaltyFee = HavenoUtils.multiply(securityDeposit, trade.getOffer().getPenaltyFeePct()); String returnAddress = trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); // attempt creating reserve tx try { synchronized (HavenoUtils.getWalletFunctionLock()) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection(); try { reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null); } catch (IllegalStateException e) { log.warn("Illegal state creating reserve tx, offerId={}, error={}", trade.getShortId(), i + 1, e.getMessage()); throw e; } catch (Exception e) { log.warn("Error creating reserve tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); trade.getXmrWalletService().handleMainWalletError(e, sourceConnection, i + 1); if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } // check for timeout if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId()); if (reserveTx != null) break; } } } catch (Exception e) { // reset state model.getXmrWalletService().swapPayoutAddressEntryToAvailable(trade.getId()); if (reserveTx != null) { model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx)); trade.getSelf().setReserveTxKeyImages(null); } throw e; } // reset protocol timeout trade.startProtocolTimeout(); // update state trade.getSelf().setReserveTxHash(reserveTx.getHash()); trade.getSelf().setReserveTxHex(reserveTx.getFullHex()); trade.getSelf().setReserveTxKey(reserveTx.getKey()); trade.getSelf().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(reserveTx)); } } // save process state processModel.setReserveTx(reserveTx); // TODO: remove this? how is it used? processModel.getTradeManager().requestPersistence(); trade.addInitProgressStep(); complete(); } catch (Throwable t) { trade.setErrorMessage("An error occurred.\n" + "Error message:\n" + t.getMessage()); failed(t); } } private boolean isTimedOut() { return !processModel.getTradeManager().hasOpenTrade(trade); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToArbitrator.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.app.Version; import haveno.common.taskrunner.TaskRunner; import haveno.core.offer.Offer; import haveno.core.trade.Trade; import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.messages.TradeProtocolVersion; import haveno.core.xmr.model.XmrAddressEntry; import haveno.network.p2p.SendDirectMessageListener; import lombok.extern.slf4j.Slf4j; import java.util.UUID; import static com.google.common.base.Preconditions.checkNotNull; import static haveno.core.util.Validator.checkTradeId; @Slf4j public class TakerSendInitTradeRequestToArbitrator extends TradeTask { @SuppressWarnings({"unused"}) public TakerSendInitTradeRequestToArbitrator(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); // verify trade state InitTradeRequest sourceRequest = (InitTradeRequest) processModel.getTradeMessage(); // arbitrator's InitTradeRequest to taker checkNotNull(sourceRequest); checkTradeId(processModel.getOfferId(), sourceRequest); if (!trade.isBuyerAsTakerWithoutDeposit() && trade.getSelf().getReserveTxHash() == null) { throw new IllegalStateException("Taker reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash()); } // create request to arbitrator Offer offer = processModel.getOffer(); InitTradeRequest arbitratorRequest = new InitTradeRequest( TradeProtocolVersion.MULTISIG_2_3, // TODO: use processModel.getTradeProtocolVersion(), select one of maker's supported versions offer.getId(), trade.getAmount().longValueExact(), trade.getPrice().getValue(), trade.getSelf().getPaymentMethodId(), trade.getMaker().getAccountId(), trade.getTaker().getAccountId(), trade.getMaker().getPaymentAccountId(), trade.getTaker().getPaymentAccountId(), trade.getTaker().getPubKeyRing(), UUID.randomUUID().toString(), Version.getP2PMessageVersion(), null, sourceRequest.getCurrentDate(), trade.getMaker().getNodeAddress(), trade.getTaker().getNodeAddress(), trade.getArbitrator().getNodeAddress(), trade.getSelf().getReserveTxHash(), trade.getSelf().getReserveTxHex(), trade.getSelf().getReserveTxKey(), model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(), trade.getChallenge()); // send request to arbitrator log.info("Sending {} with offerId {} and uid {} to arbitrator {}", arbitratorRequest.getClass().getSimpleName(), arbitratorRequest.getOfferId(), arbitratorRequest.getUid(), trade.getArbitrator().getNodeAddress()); processModel.getP2PService().sendEncryptedDirectMessage( trade.getArbitrator().getNodeAddress(), trade.getArbitrator().getPubKeyRing(), arbitratorRequest, new SendDirectMessageListener() { @Override public void onArrived() { log.info("{} arrived at arbitrator: offerId={}", InitTradeRequest.class.getSimpleName(), trade.getId()); complete(); } @Override public void onFault(String errorMessage) { log.warn("Failed to send {} to arbitrator, error={}.", InitTradeRequest.class.getSimpleName(), errorMessage); failed(); } }); } catch (Throwable t) { failed(t); } } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToMaker.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.app.Version; import haveno.common.taskrunner.TaskRunner; import haveno.core.offer.Offer; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.messages.TradeProtocolVersion; import haveno.core.user.User; import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.P2PService; import haveno.network.p2p.SendDirectMessageListener; import lombok.extern.slf4j.Slf4j; import java.util.Date; import java.util.UUID; @Slf4j public class TakerSendInitTradeRequestToMaker extends TradeTask { @SuppressWarnings({"unused"}) public TakerSendInitTradeRequestToMaker(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); // verify trade state if (!trade.isBuyerAsTakerWithoutDeposit() && trade.getSelf().getReserveTxHash() == null) { throw new IllegalStateException("Taker reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash()); } // collect fields Offer offer = model.getOffer(); User user = processModel.getUser(); P2PService p2PService = processModel.getP2PService(); XmrWalletService walletService = model.getXmrWalletService(); String payoutAddress = walletService.getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); String challenge = model.getChallenge(); // taker signs offer using offer id as nonce to avoid challenge protocol byte[] sig = HavenoUtils.sign(p2PService.getKeyRing(), offer.getId()); // create request to maker InitTradeRequest makerRequest = new InitTradeRequest( TradeProtocolVersion.MULTISIG_2_3, // TODO: use processModel.getTradeProtocolVersion(), select one of maker's supported versions offer.getId(), trade.getAmount().longValueExact(), trade.getPrice().getValue(), trade.getSelf().getPaymentMethodId(), null, user.getAccountId(), trade.getMaker().getPaymentAccountId(), trade.getTaker().getPaymentAccountId(), p2PService.getKeyRing().getPubKeyRing(), UUID.randomUUID().toString(), Version.getP2PMessageVersion(), sig, new Date().getTime(), offer.getMakerNodeAddress(), P2PService.getMyNodeAddress(), null, // maker selects arbitrator null, // reserve tx not sent from taker to maker null, null, payoutAddress, challenge); // send request to maker log.info("Sending {} with offerId {} and uid {} to maker {}", makerRequest.getClass().getSimpleName(), makerRequest.getOfferId(), makerRequest.getUid(), trade.getMaker().getNodeAddress()); processModel.getP2PService().sendEncryptedDirectMessage( trade.getMaker().getNodeAddress(), trade.getMaker().getPubKeyRing(), makerRequest, new SendDirectMessageListener() { @Override public void onArrived() { log.info("{} arrived at maker: offerId={}", InitTradeRequest.class.getSimpleName(), trade.getId()); complete(); } @Override public void onFault(String errorMessage) { log.warn("Failed to send {} to maker, error={}.", InitTradeRequest.class.getSimpleName(), errorMessage); failed(); } }); } catch (Throwable t) { failed(t); } } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/TradeTask.java ================================================ /* * 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 . */ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.Task; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; import haveno.core.trade.protocol.ProcessModel; import lombok.extern.slf4j.Slf4j; @Slf4j public abstract class TradeTask extends Task { protected final ProcessModel processModel; protected final Trade trade; protected TradeTask(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); this.trade = trade; processModel = trade.getProcessModel(); } @Override protected void complete() { processModel.getTradeManager().requestPersistence(); super.complete(); } @Override protected void failed() { processModel.getTradeManager().requestPersistence(); super.failed(); } @Override protected void failed(String message) { appendToErrorMessage(message); processModel.getTradeManager().requestPersistence(); super.failed(); } @Override protected void failed(Throwable t) { log.error("Trade task failed, error={}\n", t.getMessage(), t); appendExceptionToErrorMessage(t); processModel.getTradeManager().requestPersistence(); super.failed(); } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/VerifyPeersAccountAgeWitness.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.trade.protocol.tasks; import haveno.common.crypto.PubKeyRing; import haveno.common.taskrunner.TaskRunner; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.CurrencyUtil; import haveno.core.offer.Offer; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.Trade; import haveno.core.trade.protocol.TradePeer; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.atomic.AtomicReference; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class VerifyPeersAccountAgeWitness extends TradeTask { public VerifyPeersAccountAgeWitness(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); // only verify traditional offer Offer offer = checkNotNull(trade.getOffer()); if (CurrencyUtil.isCryptoCurrency(offer.getCounterCurrencyCode())) { complete(); return; } // skip if arbitrator if (trade instanceof ArbitratorTrade) { complete(); return; } // skip if payment account payload is null TradePeer tradePeer = trade.getTradePeer(); if (tradePeer.getPaymentAccountPayload() == null) { complete(); return; } AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService(); PaymentAccountPayload peersPaymentAccountPayload = checkNotNull(tradePeer.getPaymentAccountPayload(), "Peers peersPaymentAccountPayload must not be null"); PubKeyRing peersPubKeyRing = checkNotNull(tradePeer.getPubKeyRing(), "peersPubKeyRing must not be null"); byte[] nonce = checkNotNull(tradePeer.getAccountAgeWitnessNonce()); byte[] signature = checkNotNull(tradePeer.getAccountAgeWitnessSignature()); AtomicReference errorMsg = new AtomicReference<>(); boolean isValid = accountAgeWitnessService.verifyAccountAgeWitness(trade, peersPaymentAccountPayload, peersPubKeyRing, nonce, signature, errorMsg::set); if (isValid) { trade.getTradePeer().setAccountAgeWitness(processModel.getAccountAgeWitnessService().findWitness(trade.getTradePeer().getPaymentAccountPayload(), trade.getTradePeer().getPubKeyRing()).orElse(null)); log.info("{} {} verified witness data of peer {}", trade.getClass().getSimpleName(), trade.getId(), tradePeer.getNodeAddress()); complete(); } else { failed(errorMsg.get()); } } catch (Throwable t) { failed(t); } } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/mediation/FinalizeMediatedPayoutTx.java ================================================ /* * 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 . */ package haveno.core.trade.protocol.tasks.mediation; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; import haveno.core.trade.protocol.tasks.TradeTask; import lombok.extern.slf4j.Slf4j; @Slf4j public class FinalizeMediatedPayoutTx extends TradeTask { public FinalizeMediatedPayoutTx(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); throw new RuntimeException("FinalizeMediatedPayoutTx not implemented for xmr"); // Transaction depositTx = checkNotNull(trade.getDepositTx()); // String tradeId = trade.getId(); // TradePeer tradePeer = trade.getTradePeer(); // BtcWalletService walletService = processModel.getBtcWalletService(); // Offer offer = checkNotNull(trade.getOffer(), "offer must not be null"); // Coin tradeAmount = checkNotNull(trade.getTradeAmount(), "tradeAmount must not be null"); // Contract contract = checkNotNull(trade.getContract(), "contract must not be null"); // // checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null"); // // // byte[] mySignature = checkNotNull(processModel.getMediatedPayoutTxSignature(), // "processModel.getTxSignatureFromMediation must not be null"); // byte[] peersSignature = checkNotNull(tradePeer.getMediatedPayoutTxSignature(), // "tradePeer.getTxSignatureFromMediation must not be null"); // // boolean isMyRoleBuyer = contract.isMyRoleBuyer(processModel.getPubKeyRing()); // byte[] buyerSignature = isMyRoleBuyer ? mySignature : peersSignature; // byte[] sellerSignature = isMyRoleBuyer ? peersSignature : mySignature; // // Coin totalPayoutAmount = offer.getBuyerSecurityDeposit().add(tradeAmount).add(offer.getSellerSecurityDeposit()); // Coin buyerPayoutAmount = Coin.valueOf(processModel.getBuyerPayoutAmountFromMediation()); // Coin sellerPayoutAmount = Coin.valueOf(processModel.getSellerPayoutAmountFromMediation()); // checkArgument(totalPayoutAmount.equals(buyerPayoutAmount.add(sellerPayoutAmount)), // "Payout amount does not match buyerPayoutAmount=" + buyerPayoutAmount.toFriendlyString() + // "; sellerPayoutAmount=" + sellerPayoutAmount); // // String myPayoutAddressString = walletService.getOrCreateAddressEntry(tradeId, AddressEntry.Context.TRADE_PAYOUT).getAddressString(); // String peersPayoutAddressString = tradePeer.getPayoutAddressString(); // String buyerPayoutAddressString = isMyRoleBuyer ? myPayoutAddressString : peersPayoutAddressString; // String sellerPayoutAddressString = isMyRoleBuyer ? peersPayoutAddressString : myPayoutAddressString; // // byte[] myMultiSigPubKey = processModel.getMyMultiSigPubKey(); // byte[] peersMultiSigPubKey = tradePeer.getMultiSigPubKey(); // byte[] buyerMultiSigPubKey = isMyRoleBuyer ? myMultiSigPubKey : peersMultiSigPubKey; // byte[] sellerMultiSigPubKey = isMyRoleBuyer ? peersMultiSigPubKey : myMultiSigPubKey; // // DeterministicKey multiSigKeyPair = walletService.getMultiSigKeyPair(tradeId, myMultiSigPubKey); // // checkArgument(Arrays.equals(myMultiSigPubKey, // walletService.getOrCreateAddressEntry(tradeId, AddressEntry.Context.MULTI_SIG).getPubKey()), // "myMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + tradeId); // // Transaction transaction = processModel.getTradeWalletService().finalizeMediatedPayoutTx( // depositTx, // buyerSignature, // sellerSignature, // buyerPayoutAmount, // sellerPayoutAmount, // buyerPayoutAddressString, // sellerPayoutAddressString, // multiSigKeyPair, // buyerMultiSigPubKey, // sellerMultiSigPubKey // ); // // trade.setPayoutTx(transaction); // // processModel.getTradeManager().requestPersistence(); // // walletService.resetCoinLockedInMultiSigAddressEntry(tradeId); // // complete(); } catch (Throwable t) { failed(t); } } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/mediation/ProcessMediatedPayoutSignatureMessage.java ================================================ /* * 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 . */ package haveno.core.trade.protocol.tasks.mediation; import haveno.common.taskrunner.TaskRunner; import haveno.core.support.dispute.mediation.MediationResultState; import haveno.core.trade.Trade; import haveno.core.trade.messages.MediatedPayoutTxSignatureMessage; import haveno.core.trade.protocol.tasks.TradeTask; import haveno.core.util.Validator; import lombok.extern.slf4j.Slf4j; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class ProcessMediatedPayoutSignatureMessage extends TradeTask { public ProcessMediatedPayoutSignatureMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); log.debug("current trade state " + trade.getState()); MediatedPayoutTxSignatureMessage message = (MediatedPayoutTxSignatureMessage) processModel.getTradeMessage(); Validator.checkTradeId(processModel.getOfferId(), message); checkNotNull(message); trade.getTradePeer().setMediatedPayoutTxSignature(checkNotNull(message.getTxSignature())); // update to the latest peer address of our peer if the message is correct trade.getTradePeer().setNodeAddress(processModel.getTempTradePeerNodeAddress()); trade.setMediationResultState(MediationResultState.RECEIVED_SIG_MSG); processModel.getTradeManager().requestPersistence(); complete(); } catch (Throwable t) { failed(t); } } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/mediation/ProcessMediatedPayoutTxPublishedMessage.java ================================================ /* * 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 . */ package haveno.core.trade.protocol.tasks.mediation; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; import haveno.core.trade.protocol.tasks.TradeTask; import lombok.extern.slf4j.Slf4j; @Slf4j public class ProcessMediatedPayoutTxPublishedMessage extends TradeTask { public ProcessMediatedPayoutTxPublishedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); throw new RuntimeException("ProcessMediatedPayoutTxPublishedMessage not implemented for xmr"); // MediatedPayoutTxPublishedMessage message = (MediatedPayoutTxPublishedMessage) processModel.getTradeMessage(); // Validator.checkTradeId(processModel.getOfferId(), message); // checkNotNull(message); // checkArgument(message.getPayoutTx() != null); // // // update to the latest peer address of our peer if the message is correct // trade.getTradePeer().setNodeAddress(processModel.getTempTradePeerNodeAddress()); // // if (trade.getPayoutTx() == null) { // Transaction committedMediatedPayoutTx = WalletService.maybeAddNetworkTxToWallet(message.getPayoutTx(), processModel.getBtcWalletService().getWallet()); // trade.setPayoutTx(committedMediatedPayoutTx); // log.info("MediatedPayoutTx received from peer. Txid: {}\nhex: {}", // committedMediatedPayoutTx.getTxId().toString(), Utils.HEX.encode(committedMediatedPayoutTx.bitcoinSerialize())); // // trade.setMediationResultState(MediationResultState.RECEIVED_PAYOUT_TX_PUBLISHED_MSG); // // if (trade.getPayoutTx() != null) { // // We need to delay that call as we might get executed at startup after mailbox messages are // // applied where we iterate over out pending trades. The closeDisputedTrade method would remove // // that trade from the list causing a ConcurrentModificationException. // // To avoid that we delay for one render frame. // UserThread.execute(() -> processModel.getTradeManager() // .closeDisputedTrade(trade.getId(), Trade.DisputeState.MEDIATION_CLOSED)); // } // // processModel.getBtcWalletService().resetCoinLockedInMultiSigAddressEntry(trade.getId()); // } 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); } } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/mediation/SendMediatedPayoutSignatureMessage.java ================================================ /* * 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 . */ package haveno.core.trade.protocol.tasks.mediation; import haveno.common.crypto.PubKeyRing; import haveno.common.taskrunner.TaskRunner; import haveno.core.support.dispute.mediation.MediationResultState; import haveno.core.trade.Contract; import haveno.core.trade.Trade; import haveno.core.trade.messages.MediatedPayoutTxSignatureMessage; import haveno.core.trade.protocol.tasks.TradeTask; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import haveno.network.p2p.SendMailboxMessageListener; import lombok.extern.slf4j.Slf4j; import java.util.UUID; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class SendMediatedPayoutSignatureMessage extends TradeTask { public SendMediatedPayoutSignatureMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); PubKeyRing pubKeyRing = processModel.getPubKeyRing(); Contract contract = checkNotNull(trade.getContract(), "contract must not be null"); PubKeyRing peersPubKeyRing = contract.getPeersPubKeyRing(pubKeyRing); NodeAddress peersNodeAddress = contract.getPeersNodeAddress(pubKeyRing); P2PService p2PService = processModel.getP2PService(); MediatedPayoutTxSignatureMessage message = new MediatedPayoutTxSignatureMessage(processModel.getMediatedPayoutTxSignature(), trade.getId(), p2PService.getAddress(), UUID.randomUUID().toString()); log.info("Send {} to peer {}. offerId={}, uid={}", message.getClass().getSimpleName(), peersNodeAddress, message.getOfferId(), message.getUid()); trade.setMediationResultState(MediationResultState.SIG_MSG_SENT); processModel.getTradeManager().requestPersistence(); p2PService.getMailboxMessageService().sendEncryptedMailboxMessage(peersNodeAddress, peersPubKeyRing, message, new SendMailboxMessageListener() { @Override public void onArrived() { log.info("{} arrived at peer {}. offerId={}, uid={}", message.getClass().getSimpleName(), peersNodeAddress, message.getOfferId(), message.getUid()); trade.setMediationResultState(MediationResultState.SIG_MSG_ARRIVED); processModel.getTradeManager().requestPersistence(); complete(); } @Override public void onStoredInMailbox() { log.info("{} stored in mailbox for peer {}. offerId={}, uid={}", message.getClass().getSimpleName(), peersNodeAddress, message.getOfferId(), message.getUid()); trade.setMediationResultState(MediationResultState.SIG_MSG_IN_MAILBOX); processModel.getTradeManager().requestPersistence(); complete(); } @Override public void onFault(String errorMessage) { log.error("{} failed: Peer {}. offerId={}, uid={}, errorMessage={}", message.getClass().getSimpleName(), peersNodeAddress, message.getOfferId(), message.getUid(), errorMessage); trade.setMediationResultState(MediationResultState.SIG_MSG_SEND_FAILED); appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage); processModel.getTradeManager().requestPersistence(); failed(errorMessage); } } ); } catch (Throwable t) { failed(t); } } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/mediation/SendMediatedPayoutTxPublishedMessage.java ================================================ /* * 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 . */ package haveno.core.trade.protocol.tasks.mediation; import haveno.common.taskrunner.TaskRunner; import haveno.core.support.dispute.mediation.MediationResultState; import haveno.core.trade.Trade; import haveno.core.trade.messages.TradeMailboxMessage; import haveno.core.trade.protocol.TradePeer; import haveno.core.trade.protocol.tasks.SendMailboxMessageTask; import lombok.extern.slf4j.Slf4j; @Slf4j public class SendMediatedPayoutTxPublishedMessage extends SendMailboxMessageTask { public SendMediatedPayoutTxPublishedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected TradePeer getReceiver() { return trade.getTradePeer(); } @Override protected TradeMailboxMessage getMailboxMessage(String id) { throw new RuntimeException("SendMediatedPayoutTxPublishedMessage.getMessage(id) not implemented for xmr"); // Transaction payoutTx = checkNotNull(trade.getPayoutTx(), "trade.getPayoutTx() must not be null"); // return new MediatedPayoutTxPublishedMessage( // id, // payoutTx.bitcoinSerialize(), // processModel.getMyNodeAddress(), // UUID.randomUUID().toString() // ); } @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 protected void run() { try { runInterceptHook(); if (trade.getPayoutTx() == null) { log.error("PayoutTx is null"); failed("PayoutTx is null"); return; } super.run(); } catch (Throwable t) { failed(t); } } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/mediation/SetupMediatedPayoutTxListener.java ================================================ /* * 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 . */ package haveno.core.trade.protocol.tasks.mediation; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; import haveno.core.trade.protocol.tasks.TradeTask; import lombok.extern.slf4j.Slf4j; @Slf4j public class SetupMediatedPayoutTxListener extends TradeTask { @SuppressWarnings({ "unused" }) public SetupMediatedPayoutTxListener(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); if (true) throw new RuntimeException("Not implemented"); complete(); } catch (Throwable t) { failed(t); } } } ================================================ FILE: core/src/main/java/haveno/core/trade/protocol/tasks/mediation/SignMediatedPayoutTx.java ================================================ /* * 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 . */ package haveno.core.trade.protocol.tasks.mediation; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; import haveno.core.trade.protocol.tasks.TradeTask; import lombok.extern.slf4j.Slf4j; @Slf4j public class SignMediatedPayoutTx extends TradeTask { public SignMediatedPayoutTx(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @Override protected void run() { try { runInterceptHook(); throw new RuntimeException("SignMediatedPayoutTx not implemented for xmr"); // TradePeer tradePeer = trade.getTradePeer(); // if (processModel.getMediatedPayoutTxSignature() != null) { // log.warn("processModel.getTxSignatureFromMediation is already set"); // } // // String tradeId = trade.getId(); // BtcWalletService walletService = processModel.getBtcWalletService(); // Transaction depositTx = checkNotNull(trade.getDepositTx(), "trade.getDepositTx() must not be null"); // Offer offer = checkNotNull(trade.getOffer(), "offer must not be null"); // Coin tradeAmount = checkNotNull(trade.getTradeAmount(), "tradeAmount must not be null"); // Contract contract = checkNotNull(trade.getContract(), "contract must not be null"); // // Coin totalPayoutAmount = offer.getBuyerSecurityDeposit().add(tradeAmount).add(offer.getSellerSecurityDeposit()); // Coin buyerPayoutAmount = Coin.valueOf(processModel.getBuyerPayoutAmountFromMediation()); // Coin sellerPayoutAmount = Coin.valueOf(processModel.getSellerPayoutAmountFromMediation()); // // checkArgument(totalPayoutAmount.equals(buyerPayoutAmount.add(sellerPayoutAmount)), // "Payout amount does not match buyerPayoutAmount=" + buyerPayoutAmount.toFriendlyString() + // "; sellerPayoutAmount=" + sellerPayoutAmount); // // boolean isMyRoleBuyer = contract.isMyRoleBuyer(processModel.getPubKeyRing()); // // String myPayoutAddressString = walletService.getOrCreateAddressEntry(tradeId, AddressEntry.Context.TRADE_PAYOUT).getAddressString(); // String peersPayoutAddressString = tradePeer.getPayoutAddressString(); // String buyerPayoutAddressString = isMyRoleBuyer ? myPayoutAddressString : peersPayoutAddressString; // String sellerPayoutAddressString = isMyRoleBuyer ? peersPayoutAddressString : myPayoutAddressString; // // byte[] myMultiSigPubKey = processModel.getMyMultiSigPubKey(); // byte[] peersMultiSigPubKey = tradePeer.getMultiSigPubKey(); // byte[] buyerMultiSigPubKey = isMyRoleBuyer ? myMultiSigPubKey : peersMultiSigPubKey; // byte[] sellerMultiSigPubKey = isMyRoleBuyer ? peersMultiSigPubKey : myMultiSigPubKey; // // DeterministicKey myMultiSigKeyPair = walletService.getMultiSigKeyPair(tradeId, myMultiSigPubKey); // // checkArgument(Arrays.equals(myMultiSigPubKey, // walletService.getOrCreateAddressEntry(tradeId, AddressEntry.Context.MULTI_SIG).getPubKey()), // "myMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + tradeId); // // byte[] mediatedPayoutTxSignature = processModel.getTradeWalletService().signMediatedPayoutTx( // depositTx, // buyerPayoutAmount, // sellerPayoutAmount, // buyerPayoutAddressString, // sellerPayoutAddressString, // myMultiSigKeyPair, // buyerMultiSigPubKey, // sellerMultiSigPubKey); // processModel.setMediatedPayoutTxSignature(mediatedPayoutTxSignature); // // processModel.getTradeManager().requestPersistence(); // // complete(); } catch (Throwable t) { failed(t); } } } ================================================ FILE: core/src/main/java/haveno/core/trade/statistics/ReferralId.java ================================================ /* * 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 . */ package haveno.core.trade.statistics; /** * Those are random ids which can be assigned to a market maker or API provider who generates trade volume for Haveno. * * The assignment process is that a partner requests a referralId from the core developers and if accepted they get * assigned an ID. With the ID we can quantify the generated trades from that partner from analysing the trade * statistics. Compensation requests will be based on that data. */ public enum ReferralId { REF_ID_342, REF_ID_768, REF_ID_196, REF_ID_908, REF_ID_023, REF_ID_605, REF_ID_896, REF_ID_183 } ================================================ FILE: core/src/main/java/haveno/core/trade/statistics/ReferralIdService.java ================================================ /* * 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 . */ package haveno.core.trade.statistics; import com.google.inject.Inject; import haveno.core.user.Preferences; import java.util.Arrays; import java.util.Optional; import javax.annotation.Nullable; public class ReferralIdService { private final Preferences preferences; private Optional optionalReferralId = Optional.empty(); @Inject public ReferralIdService(Preferences preferences) { this.preferences = preferences; } public boolean verify(String referralId) { return Arrays.stream(ReferralId.values()).anyMatch(e -> e.name().equals(referralId)); } public Optional getOptionalReferralId() { String referralId = preferences.getReferralId(); if (referralId != null && !referralId.isEmpty() && verify(referralId)) optionalReferralId = Optional.of(referralId); else optionalReferralId = Optional.empty(); return optionalReferralId; } public void setReferralId(@Nullable String referralId) { if (referralId == null || referralId.isEmpty() || verify(referralId)) { optionalReferralId = Optional.ofNullable(referralId); preferences.setReferralId(referralId); } } } ================================================ FILE: core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java ================================================ /* * 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 . */ package haveno.core.trade.statistics; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Charsets; import com.google.protobuf.ByteString; import haveno.common.app.Capabilities; import haveno.common.app.Capability; import haveno.common.crypto.Hash; import haveno.common.proto.ProtoUtil; import haveno.common.util.CollectionUtils; import haveno.common.util.ExtraDataMapValidator; import haveno.common.util.JsonExclude; import haveno.common.util.Utilities; import haveno.core.locale.CurrencyUtil; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.Price; import haveno.core.monetary.Volume; import haveno.core.offer.Offer; import haveno.core.offer.OfferPayload; import haveno.core.trade.Trade; import haveno.core.util.JsonUtil; import haveno.core.util.PriceUtil; import haveno.core.util.VolumeUtil; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.storage.payload.CapabilityRequiringPayload; import haveno.network.p2p.storage.payload.DateSortedTruncatablePayload; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import haveno.network.p2p.storage.payload.ProcessOncePersistableNetworkPayload; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.math.BigInteger; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.Random; import static com.google.common.base.Preconditions.checkNotNull; /** * This new trade statistics class uses only the bare minimum of data. * Data size is about 50 bytes in average */ @Slf4j public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayload, PersistableNetworkPayload, CapabilityRequiringPayload, DateSortedTruncatablePayload { private static final String VERSION_KEY = "v"; // single character key for versioning @JsonExclude private transient static final ZoneId ZONE_ID = ZoneId.systemDefault(); public static TradeStatistics3 fromV0(Trade trade, @Nullable String referralId, boolean isTorNetworkNode) { return from(trade, referralId, isTorNetworkNode, 0.0, 0, 0); } public static TradeStatistics3 fromV1(Trade trade, @Nullable String referralId, boolean isTorNetworkNode) { return from(trade, referralId, isTorNetworkNode, 0.05, 24, 0); } public static TradeStatistics3 fromV2(Trade trade, @Nullable String referralId, boolean isTorNetworkNode) { return from(trade, referralId, isTorNetworkNode, 0.10, 48, .01); } // randomize completed trade info #1099 private static TradeStatistics3 from(Trade trade, @Nullable String referralId, boolean isTorNetworkNode, double fuzzAmountPct, int fuzzDateHours, double fuzzPricePct) { Map extraDataMap = new HashMap<>(); if (referralId != null) { extraDataMap.put(OfferPayload.REFERRAL_ID, referralId); } // Store the trade protocol version to denote that the crypto price is not inverted starting with v3. // This can be removed in the future after all stats are expected to not be inverted, // then only stats which are missing this field prior to then need to be uninverted. if (!trade.getOffer().isInverted() && CurrencyUtil.isCryptoCurrency(trade.getOffer().getCounterCurrencyCode())) { extraDataMap.put(VERSION_KEY, trade.getOffer().getOfferPayload().getProtocolVersion() + ""); } NodeAddress arbitratorNodeAddress = checkNotNull(trade.getArbitrator().getNodeAddress(), "Arbitrator address is null", trade.getClass().getSimpleName(), trade.getId()); // The first 4 chars are sufficient to identify an arbitrator. // For testing with regtest/localhost we use the full address as its localhost and would result in // same values for multiple arbitrators. String truncatedArbitratorNodeAddress = isTorNetworkNode ? arbitratorNodeAddress.getFullAddress().substring(0, 4) : arbitratorNodeAddress.getFullAddress(); Offer offer = checkNotNull(trade.getOffer()); return new TradeStatistics3(offer.getCounterCurrencyCode(), fuzzTradePriceReproducibly(trade, fuzzPricePct), fuzzTradeAmountReproducibly(trade, fuzzAmountPct), offer.getPaymentMethod().getId(), fuzzTradeDateReproducibly(trade, fuzzDateHours), truncatedArbitratorNodeAddress, extraDataMap); } private static long fuzzTradePriceReproducibly(Trade trade, double fuzzPricePct) { if (fuzzPricePct == 0.0) return trade.getRawPrice().getValue(); long originalTimestamp = trade.getTakeOfferDate().getTime(); Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp long exactPrice = trade.getRawPrice().getValue(); long adjustedPrice = (long) random.nextDouble(exactPrice * (1.0 - fuzzPricePct), exactPrice * (1.0 + fuzzPricePct)); log.debug("trade {} fuzzed trade price for tradeStatistics is {}", trade.getShortId(), adjustedPrice); return adjustedPrice; } private static long fuzzTradeAmountReproducibly(Trade trade, double fuzzAmountPct) { if (fuzzAmountPct == 0.0) return trade.getAmount().longValueExact(); long originalTimestamp = trade.getTakeOfferDate().getTime(); Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp long exactAmount = trade.getAmount().longValueExact(); long adjustedAmount = (long) random.nextDouble(exactAmount * (1.0 - fuzzAmountPct), exactAmount * (1.0 + fuzzAmountPct)); log.debug("trade {} fuzzed trade amount for tradeStatistics is {}", trade.getShortId(), adjustedAmount); return adjustedAmount; } private static long fuzzTradeDateReproducibly(Trade trade, int fuzzDateHours) { if (fuzzDateHours == 0) return trade.getTakeOfferDate().getTime(); long originalTimestamp = trade.getTakeOfferDate().getTime(); Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp long adjustedTimestamp = random.nextLong(originalTimestamp - TimeUnit.HOURS.toMillis(fuzzDateHours), originalTimestamp); log.debug("trade {} fuzzed trade datestamp for tradeStatistics is {}", trade.getShortId(), new Date(adjustedTimestamp)); return adjustedTimestamp; } // This enum must not change the order as we use the ordinal for storage to reduce data size. // The payment method string can be quite long and would consume 15% more space. // When we get a new payment method we can add it to the enum at the end. Old users would add it as string if not // recognized. // NOTE: Only ***UNUSED*** payment methods can be added here, otherwise historical stats will have a different hash than new stats. private enum PaymentMethodMapper { OK_PAY, CASH_APP, VENMO, AUSTRALIA_PAYID, // seems there is a dev trade UPHOLD, MONEY_BEAM, POPMONEY, REVOLUT, PERFECT_MONEY, SEPA, SEPA_INSTANT, FASTER_PAYMENTS, NATIONAL_BANK, JAPAN_BANK, SAME_BANK, SPECIFIC_BANKS, SWISH, ALI_PAY, WECHAT_PAY, ZELLE, CHASE_QUICK_PAY, INTERAC_E_TRANSFER, US_POSTAL_MONEY_ORDER, CASH_DEPOSIT, MONEY_GRAM, WESTERN_UNION, HAL_CASH, F2F, BLOCK_CHAINS, PROMPT_PAY, ADVANCED_CASH, BLOCK_CHAINS_INSTANT, TRANSFERWISE, AMAZON_GIFT_CARD, PAY_BY_MAIL, CAPITUAL, PAYSERA, PAXUM, SWIFT, NEFT, RTGS, IMPS, UPI, PAYTM, CELPAY, NEQUI, BIZUM, PIX, MONESE, SATISPAY, VERSE, STRIKE, TIKKIE, TRANSFERWISE_USD, ACH_TRANSFER, DOMESTIC_WIRE_TRANSFER, PAYPAL } @Getter private final String currency; private final long price; @Getter private final long amount; // XMR amount private final String paymentMethod; private final long date; // Old converted trade stat objects might not have it set @Nullable @JsonExclude @Getter private String arbitrator; // todo should we add referrerId as well? get added to extra map atm but not used so far // Hash get set in constructor from json of all the other data fields (with hash = null). @JsonExclude private final byte[] hash; // Should be only used in exceptional case if we need to add data but do not want to break backward compatibility // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new // field in a class would break that hash and therefore break the storage mechanism. @Nullable @JsonExclude @Getter private final Map extraDataMap; // We cache the date object to avoid reconstructing a new Date at each getDate call. @JsonExclude private transient final Date dateObj; @JsonExclude private transient Volume volume = null; // Traditional or crypto volume @JsonExclude private transient LocalDateTime localDateTime; public TradeStatistics3(String currency, long price, long amount, String paymentMethod, long date, String arbitrator, @Nullable Map extraDataMap) { this(currency, price, amount, paymentMethod, date, arbitrator, extraDataMap, null); } // Used from conversion method where we use the hash of the TradeStatistics2 objects to avoid duplicate entries public TradeStatistics3(String currency, long price, long amount, String paymentMethod, long date, String arbitrator, @Nullable byte[] hash) { this(currency, price, amount, paymentMethod, date, arbitrator, null, hash); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @VisibleForTesting public TradeStatistics3(String currency, long price, long amount, String paymentMethod, long date, @Nullable String arbitrator, @Nullable Map extraDataMap, @Nullable byte[] hash) { this.currency = currency; this.price = price; this.amount = amount; String tempPaymentMethod; try { tempPaymentMethod = String.valueOf(PaymentMethodMapper.valueOf(paymentMethod).ordinal()); } catch (Throwable t) { tempPaymentMethod = paymentMethod; } this.paymentMethod = tempPaymentMethod; this.date = date; this.arbitrator = arbitrator; this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); this.hash = hash == null ? createHash() : hash; dateObj = new Date(date); } public byte[] createHash() { // We create hash from all fields excluding hash itself. We use json as simple data serialisation. return Hash.getSha256Ripemd160hash(JsonUtil.objectToJson(this).getBytes(Charsets.UTF_8)); } private protobuf.TradeStatistics3.Builder getBuilder() { protobuf.TradeStatistics3.Builder builder = protobuf.TradeStatistics3.newBuilder() .setCurrency(currency) .setPrice(price) .setAmount(amount) .setPaymentMethod(paymentMethod) .setDate(date) .setHash(ByteString.copyFrom(hash)); Optional.ofNullable(arbitrator).ifPresent(builder::setArbitrator); Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); return builder; } public protobuf.TradeStatistics3 toProtoTradeStatistics3() { return getBuilder().build(); } @Override public protobuf.PersistableNetworkPayload toProtoMessage() { return protobuf.PersistableNetworkPayload.newBuilder().setTradeStatistics3(getBuilder()).build(); } public static TradeStatistics3 fromProto(protobuf.TradeStatistics3 proto) { return new TradeStatistics3( proto.getCurrency(), proto.getPrice(), proto.getAmount(), proto.getPaymentMethod(), proto.getDate(), ProtoUtil.stringOrNullFromProto(proto.getArbitrator()), CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap(), proto.getHash().toByteArray()); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public byte[] getHash() { return hash; } @Override public boolean verifyHashSize() { checkNotNull(hash, "hash must not be null"); return hash.length == 20; } @Override public Capabilities getRequiredCapabilities() { return new Capabilities(Capability.TRADE_STATISTICS_3); } @Override public Date getDate() { return dateObj; } public LocalDateTime getLocalDateTime() { if (localDateTime == null) { localDateTime = dateObj.toInstant().atZone(ZONE_ID).toLocalDateTime(); } return localDateTime; } public long getDateAsLong() { return date; } @Override public int maxItems() { return 3000; } public void pruneOptionalData() { arbitrator = null; } public String getPaymentMethodId() { try { return PaymentMethodMapper.values()[Integer.parseInt(paymentMethod)].name(); } catch (Throwable ignore) { return paymentMethod; } } private transient Price priceObj; public Price getTradePrice() { if (priceObj == null) { priceObj = Price.valueOf(currency, getNormalizedPrice()); } return priceObj; } /** * Returns the price as XMR/QUOTE. * * Note: Cannot override getPrice() because it's used for gson serialization, nor do we want expose it publicly. */ public long getNormalizedPrice() { return isInverted() ? PriceUtil.invertLongPrice(price, currency) : price; } private boolean isInverted() { return CurrencyUtil.isCryptoCurrency(currency) && (extraDataMap == null || !extraDataMap.containsKey(VERSION_KEY)); // crypto price is inverted if missing key } public BigInteger getTradeAmount() { return BigInteger.valueOf(amount); } public Volume getTradeVolume() { Volume volume = this.volume; if (volume == null) { Price price = getTradePrice(); this.volume = volume = price.getMonetary() instanceof CryptoMoney ? price.getVolumeByAmount(getTradeAmount()) : VolumeUtil.getAdjustedVolume(price.getVolumeByAmount(getTradeAmount()), paymentMethod); } return volume; } public boolean isValid() { return amount > 0 && price > 0 && date > 0 && paymentMethod != null && !paymentMethod.isEmpty() && currency != null && !currency.isEmpty(); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof TradeStatistics3)) return false; TradeStatistics3 that = (TradeStatistics3) o; if (price != that.price) return false; if (amount != that.amount) return false; if (date != that.date) return false; if (currency != null ? !currency.equals(that.currency) : that.currency != null) return false; if (paymentMethod != null ? !paymentMethod.equals(that.paymentMethod) : that.paymentMethod != null) return false; return Arrays.equals(hash, that.hash); } @Override public int hashCode() { int result = currency != null ? currency.hashCode() : 0; result = 31 * result + (int) (price ^ (price >>> 32)); result = 31 * result + (int) (amount ^ (amount >>> 32)); result = 31 * result + (paymentMethod != null ? paymentMethod.hashCode() : 0); result = 31 * result + (int) (date ^ (date >>> 32)); result = 31 * result + Arrays.hashCode(hash); return result; } @Override public String toString() { return "TradeStatistics3{" + "\n currency='" + currency + '\'' + ",\n rawPrice=" + price + ",\n normalizedPrice=" + getNormalizedPrice() + ",\n amount=" + amount + ",\n paymentMethod='" + paymentMethod + '\'' + ",\n date=" + date + ",\n arbitrator='" + arbitrator + '\'' + ",\n hash=" + Utilities.bytesAsHexString(hash) + ",\n extraDataMap=" + extraDataMap + "\n}"; } } ================================================ FILE: core/src/main/java/haveno/core/trade/statistics/TradeStatistics3StorageService.java ================================================ /* * 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 . */ package haveno.core.trade.statistics; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.common.persistence.PersistenceManager; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import haveno.network.p2p.storage.persistence.HistoricalDataStoreService; import java.io.File; import lombok.extern.slf4j.Slf4j; @Singleton @Slf4j public class TradeStatistics3StorageService extends HistoricalDataStoreService { private static final String FILE_NAME = "TradeStatistics3Store"; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public TradeStatistics3StorageService(@Named(Config.STORAGE_DIR) File storageDir, PersistenceManager persistenceManager) { super(storageDir, persistenceManager); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public String getFileName() { return FILE_NAME; } @Override protected void initializePersistenceManager() { persistenceManager.initialize(store, PersistenceManager.Source.NETWORK); } @Override public boolean canHandle(PersistableNetworkPayload payload) { return payload instanceof TradeStatistics3; } /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// @Override protected TradeStatistics3Store createStore() { return new TradeStatistics3Store(); } public void persistNow() { persistenceManager.persistNow(() -> { }); } } ================================================ FILE: core/src/main/java/haveno/core/trade/statistics/TradeStatistics3Store.java ================================================ /* * 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 . */ package haveno.core.trade.statistics; import com.google.protobuf.Message; import haveno.network.p2p.storage.P2PDataStorage; import haveno.network.p2p.storage.persistence.PersistableNetworkPayloadStore; import lombok.extern.slf4j.Slf4j; import java.util.List; import java.util.stream.Collectors; /** * We store only the payload in the PB file to save disc space. The hash of the payload can be created anyway and * is only used as key in the map. So we have a hybrid data structure which is represented as list in the protobuffer * definition and provide a hashMap for the domain access. */ @Slf4j public class TradeStatistics3Store extends PersistableNetworkPayloadStore { public TradeStatistics3Store() { } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private TradeStatistics3Store(List list) { list.forEach(item -> map.put(new P2PDataStorage.ByteArray(item.getHash()), item)); } public Message toProtoMessage() { return protobuf.PersistableEnvelope.newBuilder() .setTradeStatistics3Store(getBuilder()) .build(); } private protobuf.TradeStatistics3Store.Builder getBuilder() { List protoList = map.values().stream() .map(payload -> (TradeStatistics3) payload) .map(TradeStatistics3::toProtoTradeStatistics3) .collect(Collectors.toList()); return protobuf.TradeStatistics3Store.newBuilder().addAllItems(protoList); } public static TradeStatistics3Store fromProto(protobuf.TradeStatistics3Store proto) { List list = proto.getItemsList().stream() .map(TradeStatistics3::fromProto).collect(Collectors.toList()); return new TradeStatistics3Store(list); } public boolean containsKey(P2PDataStorage.ByteArray hash) { return map.containsKey(hash); } } ================================================ FILE: core/src/main/java/haveno/core/trade/statistics/TradeStatisticsForJson.java ================================================ /* * 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 . */ package haveno.core.trade.statistics; import haveno.core.locale.Res; import haveno.core.monetary.Price; import haveno.core.monetary.Volume; import lombok.EqualsAndHashCode; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import javax.annotation.concurrent.Immutable; import java.math.BigInteger; @Immutable @EqualsAndHashCode @ToString @Slf4j public final class TradeStatisticsForJson { public final String currency; public final long tradePrice; public final long tradeAmount; public final long tradeDate; public final String paymentMethod; // primaryMarket fields are based on industry standard where primaryMarket is always in the focus (in the app XMR is always in the focus) public String currencyPair; public long primaryMarketTradePrice; public long primaryMarketTradeAmount; public long primaryMarketTradeVolume; public TradeStatisticsForJson(TradeStatistics3 tradeStatistics) { this.currency = tradeStatistics.getCurrency(); this.paymentMethod = tradeStatistics.getPaymentMethodId(); this.tradePrice = tradeStatistics.getNormalizedPrice(); this.tradeAmount = tradeStatistics.getAmount(); this.tradeDate = tradeStatistics.getDateAsLong(); try { Price tradePrice = getPrice(); currencyPair = Res.getBaseCurrencyCode() + "/" + currency; primaryMarketTradePrice = tradePrice.getValue(); primaryMarketTradeAmount = getTradeAmount().longValueExact(); primaryMarketTradeVolume = getTradeVolume() != null ? getTradeVolume().getValue() : 0; } catch (Throwable t) { log.error(t.getMessage()); t.printStackTrace(); } } public Price getPrice() { return Price.valueOf(currency, tradePrice); } public BigInteger getTradeAmount() { return BigInteger.valueOf(tradeAmount); } public Volume getTradeVolume() { try { return getPrice().getVolumeByAmount(getTradeAmount()); } catch (Throwable t) { return Volume.parse("0", currency); } } } ================================================ FILE: core/src/main/java/haveno/core/trade/statistics/TradeStatisticsManager.java ================================================ /* * 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 . */ package haveno.core.trade.statistics; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.inject.name.Named; import haveno.common.UserThread; import haveno.common.config.Config; import haveno.common.file.JsonFileManager; import haveno.core.locale.CurrencyTuple; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.util.JsonUtil; import haveno.network.p2p.P2PService; import haveno.network.p2p.storage.P2PDataStorage; import haveno.network.p2p.storage.persistence.AppendOnlyDataStoreService; import java.io.File; import java.time.Instant; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javax.annotation.Nullable; import lombok.extern.slf4j.Slf4j; @Singleton @Slf4j public class TradeStatisticsManager { private final P2PService p2PService; private final PriceFeedService priceFeedService; private final TradeStatistics3StorageService tradeStatistics3StorageService; private final File storageDir; private final boolean dumpStatistics; private final ObservableList observableTradeStatisticsList = FXCollections.observableArrayList(); private JsonFileManager jsonFileManager; public static final int PUBLISH_STATS_RANDOM_DELAY_HOURS = 24; @Inject public TradeStatisticsManager(P2PService p2PService, PriceFeedService priceFeedService, TradeStatistics3StorageService tradeStatistics3StorageService, AppendOnlyDataStoreService appendOnlyDataStoreService, @Named(Config.STORAGE_DIR) File storageDir, @Named(Config.DUMP_STATISTICS) boolean dumpStatistics) { this.p2PService = p2PService; this.priceFeedService = priceFeedService; this.tradeStatistics3StorageService = tradeStatistics3StorageService; this.storageDir = storageDir; this.dumpStatistics = dumpStatistics; appendOnlyDataStoreService.addService(tradeStatistics3StorageService); HavenoUtils.tradeStatisticsManager = this; } public void shutDown() { if (jsonFileManager != null) { jsonFileManager.shutDown(); } } public void onAllServicesInitialized() { p2PService.getP2PDataStorage().addAppendOnlyDataStoreListener(payload -> { if (payload instanceof TradeStatistics3) { TradeStatistics3 tradeStatistics = (TradeStatistics3) payload; if (!tradeStatistics.isValid()) { return; } synchronized (observableTradeStatisticsList) { observableTradeStatisticsList.add(tradeStatistics); priceFeedService.applyLatestHavenoMarketPrice(observableTradeStatisticsList); } maybeDumpStatistics(); } }); Set set = tradeStatistics3StorageService.getMapOfAllData().values().stream() .filter(e -> e instanceof TradeStatistics3) .map(e -> (TradeStatistics3) e) .filter(TradeStatistics3::isValid) .collect(Collectors.toSet()); // remove duplicates in early trade stats due to bugs removeDuplicateStats(set); synchronized (observableTradeStatisticsList) { observableTradeStatisticsList.addAll(set); priceFeedService.applyLatestHavenoMarketPrice(observableTradeStatisticsList); } maybeDumpStatistics(); } private void removeDuplicateStats(Set tradeStats) { removeEarlyDuplicateStats(tradeStats); removeEarlyDuplicateStatsFuzzy(tradeStats); } private void removeEarlyDuplicateStats(Set tradeStats) { // collect trades before September 30, 2024 Set earlyTrades = tradeStats.stream() .filter(e -> e.getDate().toInstant().isBefore(Instant.parse("2024-09-30T00:00:00Z"))) .collect(Collectors.toSet()); // collect stats with duplicated timestamp, currency, and payment method Set duplicates = new HashSet<>(); Set deduplicates = new HashSet<>(); for (TradeStatistics3 tradeStatistic : earlyTrades) { TradeStatistics3 duplicate = findDuplicate(tradeStatistic, deduplicates); if (duplicate == null) deduplicates.add(tradeStatistic); else duplicates.add(tradeStatistic); } // remove duplicated stats tradeStats.removeAll(duplicates); } private TradeStatistics3 findDuplicate(TradeStatistics3 tradeStatistics, Set set) { return set.stream().filter(e -> isDuplicate(tradeStatistics, e)).findFirst().orElse(null); } private boolean isDuplicate(TradeStatistics3 tradeStatistics1, TradeStatistics3 tradeStatistics2) { if (!tradeStatistics1.getPaymentMethodId().equals(tradeStatistics2.getPaymentMethodId())) return false; if (!tradeStatistics1.getCurrency().equals(tradeStatistics2.getCurrency())) return false; return tradeStatistics1.getDateAsLong() == tradeStatistics2.getDateAsLong(); } private void removeEarlyDuplicateStatsFuzzy(Set tradeStats) { // collect trades before August 7, 2024 Set earlyTrades = tradeStats.stream() .filter(e -> e.getDate().toInstant().isBefore(Instant.parse("2024-08-07T00:00:00Z"))) .collect(Collectors.toSet()); // collect duplicated trades Set duplicates = new HashSet(); Set deduplicates = new HashSet(); for (TradeStatistics3 tradeStatistic : earlyTrades) { TradeStatistics3 fuzzyDuplicate = findFuzzyDuplicate(tradeStatistic, deduplicates); if (fuzzyDuplicate == null) deduplicates.add(tradeStatistic); else duplicates.add(tradeStatistic); } // remove duplicated stats tradeStats.removeAll(duplicates); } private TradeStatistics3 findFuzzyDuplicate(TradeStatistics3 tradeStatistics, Set set) { return set.stream().filter(e -> isFuzzyDuplicate(tradeStatistics, e)).findFirst().orElse(null); } private boolean isFuzzyDuplicate(TradeStatistics3 tradeStatistics1, TradeStatistics3 tradeStatistics2) { if (!tradeStatistics1.getPaymentMethodId().equals(tradeStatistics2.getPaymentMethodId())) return false; if (!tradeStatistics1.getCurrency().equals(tradeStatistics2.getCurrency())) return false; if (tradeStatistics1.getNormalizedPrice() != tradeStatistics2.getNormalizedPrice()) return false; return isFuzzyDuplicateV1(tradeStatistics1, tradeStatistics2) || isFuzzyDuplicateV2(tradeStatistics1, tradeStatistics2); } // bug caused all peers to publish same trade with similar timestamps private boolean isFuzzyDuplicateV1(TradeStatistics3 tradeStatistics1, TradeStatistics3 tradeStatistics2) { boolean isWithin2Minutes = Math.abs(tradeStatistics1.getDate().getTime() - tradeStatistics2.getDate().getTime()) <= TimeUnit.MINUTES.toMillis(2); return isWithin2Minutes; } // bug caused sellers to re-publish their trades with randomized amounts private static final double FUZZ_AMOUNT_PCT = 0.05; private static final int FUZZ_DATE_HOURS = 24; private boolean isFuzzyDuplicateV2(TradeStatistics3 tradeStatistics1, TradeStatistics3 tradeStatistics2) { boolean isWithinFuzzedHours = Math.abs(tradeStatistics1.getDate().getTime() - tradeStatistics2.getDate().getTime()) <= TimeUnit.HOURS.toMillis(FUZZ_DATE_HOURS); boolean isWithinFuzzedAmount = Math.abs(tradeStatistics1.getAmount() - tradeStatistics2.getAmount()) <= FUZZ_AMOUNT_PCT * tradeStatistics1.getAmount(); return isWithinFuzzedHours && isWithinFuzzedAmount; } public ObservableList getObservableTradeStatisticsList() { return observableTradeStatisticsList; } public List getTradeStatisticsListCopy() { synchronized (observableTradeStatisticsList) { return new ArrayList<>(observableTradeStatisticsList); } } private void maybeDumpStatistics() { if (!dumpStatistics) { return; } if (jsonFileManager == null) { jsonFileManager = new JsonFileManager(storageDir); // We only dump once the currencies as they do not change during runtime ArrayList traditionalCurrencyList = CurrencyUtil.getAllSortedTraditionalCurrencies().stream() .map(e -> new CurrencyTuple(e.getCode(), e.getName(), 8)) .collect(Collectors.toCollection(ArrayList::new)); jsonFileManager.writeToDiscThreaded(JsonUtil.objectToJson(traditionalCurrencyList), "traditional_currency_list"); ArrayList cryptoCurrencyList = CurrencyUtil.getAllSortedCryptoCurrencies().stream() .map(e -> new CurrencyTuple(e.getCode(), e.getName(), 8)) .collect(Collectors.toCollection(ArrayList::new)); cryptoCurrencyList.add(0, new CurrencyTuple(Res.getBaseCurrencyCode(), Res.getBaseCurrencyName(), 8)); jsonFileManager.writeToDiscThreaded(JsonUtil.objectToJson(cryptoCurrencyList), "crypto_currency_list"); Instant yearAgo = Instant.ofEpochSecond(Instant.now().getEpochSecond() - TimeUnit.DAYS.toSeconds(365)); Set activeCurrencies; synchronized (observableTradeStatisticsList) { activeCurrencies = observableTradeStatisticsList.stream() .filter(e -> e.getDate().toInstant().isAfter(yearAgo)) .map(p -> p.getCurrency()) .collect(Collectors.toSet()); } ArrayList activeTraditionalCurrencyList = traditionalCurrencyList.stream() .filter(e -> activeCurrencies.contains(e.code)) .map(e -> new CurrencyTuple(e.code, e.name, 8)) .collect(Collectors.toCollection(ArrayList::new)); jsonFileManager.writeToDiscThreaded(JsonUtil.objectToJson(activeTraditionalCurrencyList), "active_traditional_currency_list"); ArrayList activeCryptoCurrencyList = cryptoCurrencyList.stream() .filter(e -> activeCurrencies.contains(e.code)) .map(e -> new CurrencyTuple(e.code, e.name, 8)) .collect(Collectors.toCollection(ArrayList::new)); jsonFileManager.writeToDiscThreaded(JsonUtil.objectToJson(activeCryptoCurrencyList), "active_crypto_currency_list"); } List list; synchronized (observableTradeStatisticsList) { list = observableTradeStatisticsList.stream() .map(TradeStatisticsForJson::new) .sorted((o1, o2) -> (Long.compare(o2.tradeDate, o1.tradeDate))) .collect(Collectors.toList()); } TradeStatisticsForJson[] array = new TradeStatisticsForJson[list.size()]; list.toArray(array); jsonFileManager.writeToDiscThreaded(JsonUtil.objectToJson(array), "trade_statistics"); } public void maybePublishTradeStatistics(Trade trade, @Nullable String referralId, boolean isTorNetworkNode) { Set trades = new HashSet<>(); trades.add(trade); maybePublishTradeStatistics(trades, referralId, isTorNetworkNode); } public void maybePublishTradeStatistics(Set trades, @Nullable String referralId, boolean isTorNetworkNode) { long ts = System.currentTimeMillis(); Set hashes = tradeStatistics3StorageService.getMapOfAllData().keySet(); trades.forEach(trade -> { if (!trade.shouldPublishTradeStatistics()) { log.debug("Trade: {} should not publish trade statistics", trade.getShortId()); return; } TradeStatistics3 tradeStatistics3V0 = null; try { tradeStatistics3V0 = TradeStatistics3.fromV0(trade, referralId, isTorNetworkNode); } catch (Exception e) { log.warn("Error getting trade statistic for {} {}: {}", trade.getClass().getName(), trade.getId(), e.getMessage()); return; } TradeStatistics3 tradeStatistics3V1 = null; try { tradeStatistics3V1 = TradeStatistics3.fromV1(trade, referralId, isTorNetworkNode); } catch (Exception e) { log.warn("Error getting trade statistic for {} {}: {}", trade.getClass().getName(), trade.getId(), e.getMessage()); return; } TradeStatistics3 tradeStatistics3V2 = null; try { tradeStatistics3V2 = TradeStatistics3.fromV2(trade, referralId, isTorNetworkNode); } catch (Exception e) { log.warn("Error getting trade statistic for {} {}: {}", trade.getClass().getName(), trade.getId(), e.getMessage()); return; } boolean hasTradeStatistics3V0 = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3V0.getHash())); boolean hasTradeStatistics3V1 = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3V1.getHash())); boolean hasTradeStatistics3V2 = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3V2.getHash())); if (hasTradeStatistics3V0 || hasTradeStatistics3V1 || hasTradeStatistics3V2) { log.debug("Trade: {}. We have already a tradeStatistics matching the hash of tradeStatistics3.", trade.getShortId()); return; } if (!tradeStatistics3V2.isValid()) { log.warn("Trade statistics are invalid for {} {}. We do not publish: {}", trade.getClass().getSimpleName(), trade.getShortId(), tradeStatistics3V1); return; } // publish after random delay within 12 hours log.info("Scheduling to publish trade statistics at random time for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); TradeStatistics3 tradeStatistics3V2Final = tradeStatistics3V2; UserThread.runAfterRandomDelay(() -> { p2PService.addPersistableNetworkPayload(tradeStatistics3V2Final, true); }, 0, PUBLISH_STATS_RANDOM_DELAY_HOURS / 2 * 60 * 60 * 1000, TimeUnit.MILLISECONDS); }); log.info("maybeRepublishTradeStatistics took {} ms. Number of tradeStatistics: {}. Number of own trades: {}", System.currentTimeMillis() - ts, hashes.size(), trades.size()); } } ================================================ FILE: core/src/main/java/haveno/core/user/AutoConfirmSettings.java ================================================ /* * 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 . */ package haveno.core.user; import com.google.protobuf.Message; import haveno.common.proto.persistable.PersistablePayload; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.Coin; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.CopyOnWriteArrayList; @Slf4j @Getter public final class AutoConfirmSettings implements PersistablePayload { public interface Listener { void onChange(); } private boolean enabled; private int requiredConfirmations; private long tradeLimit; private List serviceAddresses; private String currencyCode; private List listeners = new CopyOnWriteArrayList<>(); @SuppressWarnings("SameParameterValue") static Optional getDefault(List serviceAddresses, String currencyCode) { //noinspection SwitchStatementWithTooFewBranches switch (currencyCode) { case "XMR": return Optional.of(new AutoConfirmSettings( false, 5, Coin.COIN.value, serviceAddresses, "XMR")); default: log.error("No AutoConfirmSettings supported yet for currency {}", currencyCode); return Optional.empty(); } } public AutoConfirmSettings(boolean enabled, int requiredConfirmations, long tradeLimit, List serviceAddresses, String currencyCode) { this.enabled = enabled; this.requiredConfirmations = requiredConfirmations; this.tradeLimit = tradeLimit; this.serviceAddresses = serviceAddresses; this.currencyCode = currencyCode; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public Message toProtoMessage() { return protobuf.AutoConfirmSettings.newBuilder() .setEnabled(enabled) .setRequiredConfirmations(requiredConfirmations) .setTradeLimit(tradeLimit) .addAllServiceAddresses(serviceAddresses) .setCurrencyCode(currencyCode) .build(); } public static AutoConfirmSettings fromProto(protobuf.AutoConfirmSettings proto) { List serviceAddresses = proto.getServiceAddressesList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getServiceAddressesList()); return new AutoConfirmSettings( proto.getEnabled(), proto.getRequiredConfirmations(), proto.getTradeLimit(), serviceAddresses, proto.getCurrencyCode()); } public void addListener(Listener listener) { listeners.add(listener); } public void removeListener(Listener listener) { listeners.remove(listener); } private void notifyListeners() { listeners.forEach(Listener::onChange); } public void setEnabled(boolean enabled) { this.enabled = enabled; notifyListeners(); } public void setRequiredConfirmations(int requiredConfirmations) { this.requiredConfirmations = requiredConfirmations; notifyListeners(); } public void setTradeLimit(long tradeLimit) { this.tradeLimit = tradeLimit; notifyListeners(); } public void setServiceAddresses(List serviceAddresses) { this.serviceAddresses = serviceAddresses; notifyListeners(); } public void setCurrencyCode(String currencyCode) { this.currencyCode = currencyCode; notifyListeners(); } } ================================================ FILE: core/src/main/java/haveno/core/user/BlockChainExplorer.java ================================================ /* * 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 . */ package haveno.core.user; import com.google.protobuf.Message; import haveno.common.proto.persistable.PersistablePayload; public final class BlockChainExplorer implements PersistablePayload { public final String name; public final String txUrl; public BlockChainExplorer(String name, String txUrl) { this.name = name; this.txUrl = txUrl; } @Override public Message toProtoMessage() { return protobuf.BlockChainExplorer.newBuilder().setName(name).setTxUrl(txUrl).build(); } public static BlockChainExplorer fromProto(protobuf.BlockChainExplorer proto) { return new BlockChainExplorer(proto.getName(), proto.getTxUrl()); } } ================================================ FILE: core/src/main/java/haveno/core/user/Cookie.java ================================================ /* * 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 . */ package haveno.core.user; import haveno.common.proto.ProtoUtil; import javax.annotation.Nullable; import java.util.HashMap; import java.util.Map; import java.util.Optional; /** * Serves as flexible container for persisting UI states, layout,... * Should not be over-used for domain specific data where type safety and data integrity is important. */ public class Cookie extends HashMap { public void putAsDouble(CookieKey key, double value) { put(key, String.valueOf(value)); } public Optional getAsOptionalDouble(CookieKey key) { try { return containsKey(key) ? Optional.of(Double.parseDouble(get(key))) : Optional.empty(); } catch (Throwable t) { return Optional.empty(); } } public void putAsBoolean(CookieKey key, boolean value) { put(key, value ? "1" : "0"); } public Optional getAsOptionalBoolean(CookieKey key) { return containsKey(key) ? Optional.of(get(key).equals("1")) : Optional.empty(); } public Map toProtoMessage() { Map protoMap = new HashMap<>(); this.forEach((key, value) -> { if (key != null) { String name = key.name(); protoMap.put(name, value); } }); return protoMap; } public static Cookie fromProto(@Nullable Map protoMap) { Cookie cookie = new Cookie(); if (protoMap != null) { protoMap.forEach((key, value) -> cookie.put(ProtoUtil.enumFromProto(CookieKey.class, key), value)); } return cookie; } } ================================================ FILE: core/src/main/java/haveno/core/user/CookieKey.java ================================================ /* * 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 . */ package haveno.core.user; // Used for persistence of Cookie. Entries must not be changes or removed. Only adding entries is permitted. public enum CookieKey { STAGE_X, STAGE_Y, STAGE_W, STAGE_H, TRADE_STAT_CHART_USE_USD, CLEAN_TOR_DIR_AT_RESTART } ================================================ FILE: core/src/main/java/haveno/core/user/DontShowAgainLookup.java ================================================ /* * 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 . */ package haveno.core.user; public class DontShowAgainLookup { private static Preferences preferences; public static void setPreferences(Preferences preferences) { DontShowAgainLookup.preferences = preferences; } public static boolean showAgain(String key) { return preferences.showAgain(key); } public static void dontShowAgain(String key, boolean dontShowAgain) { preferences.dontShowAgain(key, dontShowAgain); } } ================================================ FILE: core/src/main/java/haveno/core/user/Preferences.java ================================================ /* * 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 . */ package haveno.core.user; import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.inject.name.Named; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.persistable.PersistedDataHost; import haveno.common.util.Utilities; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.GlobalSettings; import haveno.core.locale.TradeCurrency; import haveno.core.locale.TraditionalCurrency; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccountUtil; import haveno.core.trade.HavenoUtils; import haveno.core.xmr.XmrNodeSettings; import haveno.core.xmr.nodes.XmrNodes; import haveno.core.xmr.nodes.XmrNodes.MoneroNodesOption; import haveno.core.xmr.nodes.XmrNodesSetupPreferences; import haveno.core.xmr.wallet.Restrictions; import haveno.network.p2p.network.BridgeAddressProvider; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.ObservableMap; import javax.annotation.Nullable; import lombok.Getter; import lombok.Setter; import lombok.experimental.Delegate; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; @Slf4j @Singleton public final class Preferences implements PersistedDataHost, BridgeAddressProvider { public enum UseTorForXmr { AFTER_SYNC, OFF, ON; public boolean isUseTorForXmr() { return this != UseTorForXmr.OFF; } } private static final ArrayList XMR_MAIN_NET_EXPLORERS = new ArrayList<>(Arrays.asList( new BlockChainExplorer("xmrchain.net", "https://xmrchain.net/tx/") )); private static final ArrayList XMR_STAGE_NET_EXPLORERS = new ArrayList<>(Arrays.asList( new BlockChainExplorer("stagenet.xmrchain.net", "https://stagenet.xmrchain.net/tx/") )); private static final ArrayList XMR_TX_PROOF_SERVICES_CLEAR_NET = new ArrayList<>(Arrays.asList( "xmrblocks.monero.emzy.de", // @emzy //"explorer.monero.wiz.biz", // @wiz "xmrblocks.bisq.services" // @devinbileck )); private static final ArrayList XMR_TX_PROOF_SERVICES = new ArrayList<>(Arrays.asList( "monero3bec7m26vx6si6qo7q7imlaoz45ot5m2b5z2ppgoooo6jx2rqd.onion", // @emzy "devinxmrwu4jrfq2zmq5kqjpxb44hx7i7didebkwrtvmvygj4uuop2ad.onion" // @devinbileck )); private static final ArrayList TX_BROADCAST_SERVICES_CLEAR_NET = new ArrayList<>(Arrays.asList( "https://mempool.space/api/tx", // @wiz "https://mempool.emzy.de/api/tx", // @emzy "https://mempool.haveno.services/api/tx" // @devinbileck )); private static final ArrayList TX_BROADCAST_SERVICES = new ArrayList<>(Arrays.asList( "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/tx", // @wiz "http://mempool4t6mypeemozyterviq3i5de4kpoua65r3qkn5i3kknu5l2cad.onion/api/tx", // @emzy "http://mempoolusb2f67qi7mz2it7n5e77a6komdzx6wftobcduxszkdfun2yd.onion/api/tx" // @devinbileck )); public static final boolean USE_SYMMETRIC_SECURITY_DEPOSIT = true; public static final int CLEAR_DATA_AFTER_DAYS_DEFAULT = 60; // used with new instance or when existing user has agreed to settings notification public static final int CLEAR_DATA_AFTER_DAYS_DISABLED = 99999; // feature effectively disabled until existing user agrees to settings notification // payload is initialized so the default values are available for Property initialization. @Setter @Delegate(excludes = ExcludesDelegateMethods.class) private PreferencesPayload prefPayload = new PreferencesPayload(); private boolean initialReadDone = false; @Getter private final BooleanProperty useAnimationsProperty = new SimpleBooleanProperty(prefPayload.isUseAnimations()); @Getter private final IntegerProperty cssThemeProperty = new SimpleIntegerProperty(prefPayload.getCssTheme()); private final ObservableList traditionalCurrenciesAsObservable = FXCollections.observableArrayList(); private final ObservableList cryptoCurrenciesAsObservable = FXCollections.observableArrayList(); private final ObservableList tradeCurrenciesAsObservable = FXCollections.observableArrayList(); private final ObservableMap dontShowAgainMapAsObservable = FXCollections.observableHashMap(); private final PersistenceManager persistenceManager; private final Config config; private final String xmrNodesFromOptions; private final XmrNodes xmrNodes; @Getter private final BooleanProperty useStandbyModeProperty = new SimpleBooleanProperty(prefPayload.isUseStandbyMode()); @Getter private final BooleanProperty useSoundForNotificationsProperty = new SimpleBooleanProperty(prefPayload.isUseSoundForNotifications()); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public Preferences(PersistenceManager persistenceManager, Config config, @Named(Config.XMR_NODES) String xmrNodesFromOptions, XmrNodes xmrNodes) { this.persistenceManager = persistenceManager; this.config = config; this.xmrNodesFromOptions = xmrNodesFromOptions; this.xmrNodes = xmrNodes; useAnimationsProperty.addListener((ov) -> { prefPayload.setUseAnimations(useAnimationsProperty.get()); GlobalSettings.setUseAnimations(prefPayload.isUseAnimations()); requestPersistence(); }); cssThemeProperty.addListener((ov) -> { prefPayload.setCssTheme(cssThemeProperty.get()); requestPersistence(); }); useStandbyModeProperty.addListener((ov) -> { prefPayload.setUseStandbyMode(useStandbyModeProperty.get()); requestPersistence(); }); useSoundForNotificationsProperty.addListener((ov) -> { prefPayload.setUseSoundForNotifications(useSoundForNotificationsProperty.get()); requestPersistence(); }); traditionalCurrenciesAsObservable.addListener((javafx.beans.Observable ov) -> { prefPayload.getTraditionalCurrencies().clear(); prefPayload.getTraditionalCurrencies().addAll(traditionalCurrenciesAsObservable); prefPayload.getTraditionalCurrencies().sort(TradeCurrency::compareTo); requestPersistence(); }); cryptoCurrenciesAsObservable.addListener((javafx.beans.Observable ov) -> { prefPayload.getCryptoCurrencies().clear(); prefPayload.getCryptoCurrencies().addAll(cryptoCurrenciesAsObservable); prefPayload.getCryptoCurrencies().sort(TradeCurrency::compareTo); requestPersistence(); }); traditionalCurrenciesAsObservable.addListener(this::updateTradeCurrencies); cryptoCurrenciesAsObservable.addListener(this::updateTradeCurrencies); } @Override public void readPersisted(Runnable completeHandler) { persistenceManager.readPersisted("PreferencesPayload", persisted -> { initFromPersistedPreferences(persisted); completeHandler.run(); }, () -> { initNewPreferences(); completeHandler.run(); }); } private void initFromPersistedPreferences(PreferencesPayload persisted) { prefPayload = persisted; GlobalSettings.setLocale(new Locale(prefPayload.getUserLanguage(), prefPayload.getUserCountry().code)); GlobalSettings.setUseAnimations(prefPayload.isUseAnimations()); TradeCurrency preferredTradeCurrency = checkNotNull(prefPayload.getPreferredTradeCurrency(), "preferredTradeCurrency must not be null"); setPreferredTradeCurrency(preferredTradeCurrency); setTraditionalCurrencies(prefPayload.getTraditionalCurrencies()); setCryptoCurrencies(prefPayload.getCryptoCurrencies()); GlobalSettings.setDefaultTradeCurrency(preferredTradeCurrency); // If a user has updated and the field was not set and get set to 0 by protobuf // As there is no way to detect that a primitive value field was set we cannot apply // a "marker" value like -1 to it. We also do not want to wrap the value in a new // proto message as thats too much for that feature... So we accept that if the user // sets the value to 0 it will be overwritten by the default at next startup. if (prefPayload.getBsqAverageTrimThreshold() == 0) { prefPayload.setBsqAverageTrimThreshold(0.05); } setupPreferences(); } private void initNewPreferences() { prefPayload = new PreferencesPayload(); prefPayload.setUserLanguage(GlobalSettings.getLocale().getLanguage()); prefPayload.setUserCountry(CountryUtil.getDefaultCountry()); GlobalSettings.setLocale(new Locale(prefPayload.getUserLanguage(), prefPayload.getUserCountry().code)); TradeCurrency preferredTradeCurrency = CurrencyUtil.getCurrencyByCountryCode("US"); // default fallback option try { preferredTradeCurrency = CurrencyUtil.getCurrencyByCountryCode(prefPayload.getUserCountry().code); } catch (IllegalArgumentException ia) { log.warn("Could not determine currency for country {} [{}]", prefPayload.getUserCountry().code, ia.toString()); } prefPayload.setPreferredTradeCurrency(preferredTradeCurrency); setTraditionalCurrencies(CurrencyUtil.getMainFiatCurrencies()); setCryptoCurrencies(CurrencyUtil.getMainCryptoCurrencies()); BaseCurrencyNetwork baseCurrencyNetwork = Config.baseCurrencyNetwork(); if ("XMR".equals(baseCurrencyNetwork.getCurrencyCode())) { setBlockChainExplorerMainNet(XMR_MAIN_NET_EXPLORERS.get(0)); setBlockChainExplorerStageNet(XMR_STAGE_NET_EXPLORERS.get(0)); } else { throw new RuntimeException("BaseCurrencyNetwork not defined. BaseCurrencyNetwork=" + baseCurrencyNetwork); } prefPayload.setDirectoryChooserPath(Utilities.getSystemHomeDirectory()); prefPayload.setOfferBookChartScreenCurrencyCode(preferredTradeCurrency.getCode()); prefPayload.setTradeChartsScreenCurrencyCode(preferredTradeCurrency.getCode()); prefPayload.setBuyScreenCurrencyCode(preferredTradeCurrency.getCode()); prefPayload.setSellScreenCurrencyCode(preferredTradeCurrency.getCode()); GlobalSettings.setDefaultTradeCurrency(preferredTradeCurrency); setupPreferences(); } private void setupPreferences() { persistenceManager.initialize(prefPayload, PersistenceManager.Source.PRIVATE); // We don't want to pass Preferences to all popups where the don't show again checkbox is used, so we use // that static lookup class to avoid static access to the Preferences directly. DontShowAgainLookup.setPreferences(this); // set all properties useAnimationsProperty.set(prefPayload.isUseAnimations()); useStandbyModeProperty.set(prefPayload.isUseStandbyMode()); useSoundForNotificationsProperty.set(prefPayload.isUseSoundForNotifications()); cssThemeProperty.set(prefPayload.getCssTheme()); // if no valid Monero block explorer is set, select the 1st valid Monero block explorer ArrayList xmrExplorers = getBlockChainExplorers(); if (getBlockChainExplorer() == null || getBlockChainExplorer().name.length() == 0) { setBlockChainExplorer(xmrExplorers.get(0)); } tradeCurrenciesAsObservable.addAll(prefPayload.getTraditionalCurrencies()); tradeCurrenciesAsObservable.addAll(prefPayload.getCryptoCurrencies()); dontShowAgainMapAsObservable.putAll(getDontShowAgainMap()); // Override settings with options if set if (config.useTorForXmrOptionSetExplicitly) setUseTorForXmr(config.useTorForXmr); // switch to public nodes if no provided nodes available boolean isFixedConnection = !"".equals(config.xmrNode) && (!HavenoUtils.isLocalHost(config.xmrNode) || !config.ignoreLocalXmrNode); if (!isFixedConnection && getMoneroNodesOptionOrdinal() == XmrNodes.MoneroNodesOption.PROVIDED.ordinal() && xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(this)).isEmpty()) { log.warn("No provided nodes available, switching to public nodes"); setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal()); } if (xmrNodesFromOptions != null && !xmrNodesFromOptions.isEmpty()) { if (getMoneroNodes() != null && !getMoneroNodes().equals(xmrNodesFromOptions)) { log.warn("The Monero node(s) from the program argument and the one(s) persisted in the UI are different. " + "The Monero node(s) {} from the program argument will be used.", xmrNodesFromOptions); } setMoneroNodes(xmrNodesFromOptions); setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.CUSTOM.ordinal()); } if (prefPayload.getIgnoreDustThreshold() < Restrictions.getMinNonDustOutput().value) { setIgnoreDustThreshold(600); } if (prefPayload.getClearDataAfterDays() < 1) { setClearDataAfterDays(Preferences.CLEAR_DATA_AFTER_DAYS_DISABLED); } // For users from old versions the 4 flags a false but we want to have it true by default // PhoneKeyAndToken is also null so we can use that to enable the flags if (prefPayload.getPhoneKeyAndToken() == null) { setUseSoundForMobileNotifications(true); setUseTradeNotifications(true); setUseMarketNotifications(true); setUsePriceNotifications(true); } if (prefPayload.getAutoConfirmSettingsList().isEmpty()) { List defaultXmrTxProofServices = getDefaultXmrTxProofServices(); AutoConfirmSettings.getDefault(defaultXmrTxProofServices, "XMR") .ifPresent(xmrAutoConfirmSettings -> { getAutoConfirmSettingsList().add(xmrAutoConfirmSettings); }); } // enable sounds by default for existing clients (protobuf does not express that new field is unset) if (!prefPayload.isUseSoundForNotificationsInitialized()) { prefPayload.setUseSoundForNotificationsInitialized(true); setUseSoundForNotifications(true); } initialReadDone = true; requestPersistence(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public MoneroNodesOption getMoneroNodesOption() { return XmrNodes.MoneroNodesOption.values()[getMoneroNodesOptionOrdinal()]; } public void dontShowAgain(String key, boolean dontShowAgain) { prefPayload.getDontShowAgainMap().put(key, dontShowAgain); requestPersistence(); dontShowAgainMapAsObservable.put(key, dontShowAgain); } public void resetDontShowAgain() { prefPayload.getDontShowAgainMap().clear(); dontShowAgainMapAsObservable.clear(); requestPersistence(); } /////////////////////////////////////////////////////////////////////////////////////////// // Setter /////////////////////////////////////////////////////////////////////////////////////////// public void setUseAnimations(boolean useAnimations) { this.useAnimationsProperty.set(useAnimations); } public void setCssTheme(boolean useDarkMode) { this.cssThemeProperty.set(useDarkMode ? 1 : 0); } public void addTraditionalCurrency(TraditionalCurrency tradeCurrency) { if (!traditionalCurrenciesAsObservable.contains(tradeCurrency)) traditionalCurrenciesAsObservable.add(tradeCurrency); } public void removeTraditionalCurrency(TraditionalCurrency tradeCurrency) { if (tradeCurrenciesAsObservable.size() > 1) { traditionalCurrenciesAsObservable.remove(tradeCurrency); if (prefPayload.getPreferredTradeCurrency() != null && prefPayload.getPreferredTradeCurrency().equals(tradeCurrency)) setPreferredTradeCurrency(tradeCurrenciesAsObservable.get(0)); } else { log.error("you cannot remove the last currency"); } } public void addCryptoCurrency(CryptoCurrency tradeCurrency) { if (!cryptoCurrenciesAsObservable.contains(tradeCurrency)) cryptoCurrenciesAsObservable.add(tradeCurrency); } public void removeCryptoCurrency(CryptoCurrency tradeCurrency) { if (tradeCurrenciesAsObservable.size() > 1) { cryptoCurrenciesAsObservable.remove(tradeCurrency); if (prefPayload.getPreferredTradeCurrency() != null && prefPayload.getPreferredTradeCurrency().equals(tradeCurrency)) setPreferredTradeCurrency(tradeCurrenciesAsObservable.get(0)); } else { log.error("you cannot remove the last currency"); } } public void setBlockChainExplorer(BlockChainExplorer blockChainExplorer) { if (Config.baseCurrencyNetwork().isMainnet()) setBlockChainExplorerMainNet(blockChainExplorer); else setBlockChainExplorerStageNet(blockChainExplorer); } public void setTacAccepted(boolean tacAccepted) { prefPayload.setTacAccepted(tacAccepted); requestPersistence(); } public void setTacAcceptedV120(boolean tacAccepted) { prefPayload.setTacAcceptedV120(tacAccepted); requestPersistence(); } public void setBsqAverageTrimThreshold(double bsqAverageTrimThreshold) { prefPayload.setBsqAverageTrimThreshold(bsqAverageTrimThreshold); requestPersistence(); } public Optional findAutoConfirmSettings(String currencyCode) { return prefPayload.getAutoConfirmSettingsList().stream() .filter(e -> e.getCurrencyCode().equals(currencyCode)) .findAny(); } public void setAutoConfServiceAddresses(String currencyCode, List serviceAddresses) { findAutoConfirmSettings(currencyCode).ifPresent(e -> { e.setServiceAddresses(serviceAddresses); requestPersistence(); }); } public void setAutoConfEnabled(String currencyCode, boolean enabled) { findAutoConfirmSettings(currencyCode).ifPresent(e -> { e.setEnabled(enabled); requestPersistence(); }); } public void setAutoConfRequiredConfirmations(String currencyCode, int requiredConfirmations) { findAutoConfirmSettings(currencyCode).ifPresent(e -> { e.setRequiredConfirmations(requiredConfirmations); requestPersistence(); }); } public void setAutoConfTradeLimit(String currencyCode, long tradeLimit) { findAutoConfirmSettings(currencyCode).ifPresent(e -> { e.setTradeLimit(tradeLimit); requestPersistence(); }); } public void setHideNonAccountPaymentMethods(boolean hideNonAccountPaymentMethods) { prefPayload.setHideNonAccountPaymentMethods(hideNonAccountPaymentMethods); requestPersistence(); } private void requestPersistence() { if (initialReadDone) persistenceManager.requestPersistence(); } public void setUserLanguage(@NotNull String userLanguageCode) { prefPayload.setUserLanguage(userLanguageCode); if (prefPayload.getUserCountry() != null && prefPayload.getUserLanguage() != null) GlobalSettings.setLocale(new Locale(prefPayload.getUserLanguage(), prefPayload.getUserCountry().code)); requestPersistence(); } public void setUserCountry(@NotNull Country userCountry) { prefPayload.setUserCountry(userCountry); if (prefPayload.getUserLanguage() != null) GlobalSettings.setLocale(new Locale(prefPayload.getUserLanguage(), userCountry.code)); requestPersistence(); } public void setPreferredTradeCurrency(TradeCurrency preferredTradeCurrency) { if (preferredTradeCurrency != null) { prefPayload.setPreferredTradeCurrency(preferredTradeCurrency); GlobalSettings.setDefaultTradeCurrency(preferredTradeCurrency); requestPersistence(); } } public void setUseTorForXmr(Config.UseTorForXmr useTorForXmr) { switch (useTorForXmr) { case AFTER_SYNC: setUseTorForXmrOrdinal(Preferences.UseTorForXmr.AFTER_SYNC.ordinal()); break; case OFF: setUseTorForXmrOrdinal(Preferences.UseTorForXmr.OFF.ordinal()); break; case ON: setUseTorForXmrOrdinal(Preferences.UseTorForXmr.ON.ordinal()); break; default: throw new IllegalArgumentException("Unexpected case: " + useTorForXmr); } } public void setSplitOfferOutput(boolean splitOfferOutput) { prefPayload.setSplitOfferOutput(splitOfferOutput); requestPersistence(); } public void setShowOwnOffersInOfferBook(boolean showOwnOffersInOfferBook) { prefPayload.setShowOwnOffersInOfferBook(showOwnOffersInOfferBook); requestPersistence(); } public void setMaxPriceDistanceInPercent(double maxPriceDistanceInPercent) { prefPayload.setMaxPriceDistanceInPercent(maxPriceDistanceInPercent); requestPersistence(); } public void setBackupDirectory(String backupDirectory) { prefPayload.setBackupDirectory(backupDirectory); requestPersistence(); } public void setAutoSelectArbitrators(boolean autoSelectArbitrators) { prefPayload.setAutoSelectArbitrators(autoSelectArbitrators); requestPersistence(); } public void setUsePercentageBasedPrice(boolean usePercentageBasedPrice) { prefPayload.setUsePercentageBasedPrice(usePercentageBasedPrice); requestPersistence(); } public void setTagForPeer(String fullAddress, String tag) { prefPayload.getPeerTagMap().put(fullAddress, tag); requestPersistence(); } public void setOfferBookChartScreenCurrencyCode(String offerBookChartScreenCurrencyCode) { prefPayload.setOfferBookChartScreenCurrencyCode(offerBookChartScreenCurrencyCode); requestPersistence(); } public void setBuyScreenCurrencyCode(String buyScreenCurrencyCode) { prefPayload.setBuyScreenCurrencyCode(buyScreenCurrencyCode); requestPersistence(); } public void setSellScreenCurrencyCode(String sellScreenCurrencyCode) { prefPayload.setSellScreenCurrencyCode(sellScreenCurrencyCode); requestPersistence(); } public void setBuyScreenCryptoCurrencyCode(String buyScreenCurrencyCode) { prefPayload.setBuyScreenCryptoCurrencyCode(buyScreenCurrencyCode); requestPersistence(); } public void setSellScreenCryptoCurrencyCode(String sellScreenCurrencyCode) { prefPayload.setSellScreenCryptoCurrencyCode(sellScreenCurrencyCode); requestPersistence(); } public void setBuyScreenOtherCurrencyCode(String buyScreenCurrencyCode) { prefPayload.setBuyScreenOtherCurrencyCode(buyScreenCurrencyCode); requestPersistence(); } public void setSellScreenOtherCurrencyCode(String sellScreenCurrencyCode) { prefPayload.setSellScreenOtherCurrencyCode(sellScreenCurrencyCode); requestPersistence(); } public void setIgnoreTradersList(List ignoreTradersList) { prefPayload.setIgnoreTradersList(ignoreTradersList); requestPersistence(); } public void setDirectoryChooserPath(String directoryChooserPath) { prefPayload.setDirectoryChooserPath(directoryChooserPath); requestPersistence(); } public void setTradeChartsScreenCurrencyCode(String tradeChartsScreenCurrencyCode) { prefPayload.setTradeChartsScreenCurrencyCode(tradeChartsScreenCurrencyCode); requestPersistence(); } public void setTradeStatisticsTickUnitIndex(int tradeStatisticsTickUnitIndex) { prefPayload.setTradeStatisticsTickUnitIndex(tradeStatisticsTickUnitIndex); requestPersistence(); } public void setSortMarketCurrenciesNumerically(boolean sortMarketCurrenciesNumerically) { prefPayload.setSortMarketCurrenciesNumerically(sortMarketCurrenciesNumerically); requestPersistence(); } public void setMoneroNodes(String moneroNodes) { prefPayload.setMoneroNodes(moneroNodes); requestPersistence(); } public void setUseCustomWithdrawalTxFee(boolean useCustomWithdrawalTxFee) { prefPayload.setUseCustomWithdrawalTxFee(useCustomWithdrawalTxFee); requestPersistence(); } public void setWithdrawalTxFeeInVbytes(long withdrawalTxFeeInVbytes) { prefPayload.setWithdrawalTxFeeInVbytes(withdrawalTxFeeInVbytes); requestPersistence(); } public void setSecurityDepositAsPercent(double securityDepositAsPercent, PaymentAccount paymentAccount) { double max = Restrictions.getMaxSecurityDepositPct(); double min = Restrictions.getMinSecurityDepositPct(); if (PaymentAccountUtil.isCryptoCurrencyAccount(paymentAccount)) prefPayload.setSecurityDepositAsPercentForCrypto(Math.min(max, Math.max(min, securityDepositAsPercent))); else prefPayload.setSecurityDepositAsPercent(Math.min(max, Math.max(min, securityDepositAsPercent))); requestPersistence(); } public void setSelectedPaymentAccountForCreateOffer(@Nullable PaymentAccount paymentAccount) { prefPayload.setSelectedPaymentAccountForCreateOffer(paymentAccount); requestPersistence(); } public void setTraditionalCurrencies(List currencies) { traditionalCurrenciesAsObservable.setAll(currencies.stream() .map(traditionalCurrency -> new TraditionalCurrency(traditionalCurrency)) .distinct().collect(Collectors.toList())); requestPersistence(); } private void setCryptoCurrencies(List currencies) { cryptoCurrenciesAsObservable.setAll(currencies.stream().distinct().collect(Collectors.toList())); requestPersistence(); } private void setBlockChainExplorerStageNet(BlockChainExplorer blockChainExplorerTestNet) { prefPayload.setBlockChainExplorerTestNet(blockChainExplorerTestNet); requestPersistence(); } private void setBlockChainExplorerMainNet(BlockChainExplorer blockChainExplorerMainNet) { prefPayload.setBlockChainExplorerMainNet(blockChainExplorerMainNet); requestPersistence(); } public void setResyncSpvRequested(boolean resyncSpvRequested) { prefPayload.setResyncSpvRequested(resyncSpvRequested); // We call that before shutdown so we dont want a delay here requestPersistence(); } public void setBridgeAddresses(List bridgeAddresses) { prefPayload.setBridgeAddresses(bridgeAddresses); // We call that before shutdown so we dont want a delay here persistenceManager.forcePersistNow(); } // Only used from PB but keep it explicit as it may be used from the client and then we want to persist public void setPeerTagMap(Map peerTagMap) { prefPayload.setPeerTagMap(peerTagMap); requestPersistence(); } public void setBridgeOptionOrdinal(int bridgeOptionOrdinal) { prefPayload.setBridgeOptionOrdinal(bridgeOptionOrdinal); persistenceManager.forcePersistNow(); } public void setTorTransportOrdinal(int torTransportOrdinal) { prefPayload.setTorTransportOrdinal(torTransportOrdinal); persistenceManager.forcePersistNow(); } public void setCustomBridges(String customBridges) { prefPayload.setCustomBridges(customBridges); persistenceManager.forcePersistNow(); } public void setUseTorForXmrOrdinal(int useTorForXmrOrdinal) { prefPayload.setUseTorForXmrOrdinal(useTorForXmrOrdinal); requestPersistence(); } public void setMoneroNodesOptionOrdinal(int moneroNodesOptionOrdinal) { prefPayload.setMoneroNodesOptionOrdinal(moneroNodesOptionOrdinal); requestPersistence(); } public void setReferralId(String referralId) { prefPayload.setReferralId(referralId); requestPersistence(); } public void setPhoneKeyAndToken(String phoneKeyAndToken) { prefPayload.setPhoneKeyAndToken(phoneKeyAndToken); requestPersistence(); } public void setUseSoundForMobileNotifications(boolean value) { prefPayload.setUseSoundForMobileNotifications(value); requestPersistence(); } public void setUseTradeNotifications(boolean value) { prefPayload.setUseTradeNotifications(value); requestPersistence(); } public void setUseMarketNotifications(boolean value) { prefPayload.setUseMarketNotifications(value); requestPersistence(); } public void setUsePriceNotifications(boolean value) { prefPayload.setUsePriceNotifications(value); requestPersistence(); } public void setUseStandbyMode(boolean useStandbyMode) { this.useStandbyModeProperty.set(useStandbyMode); } public void setUseSoundForNotifications(boolean useSoundForNotifications) { this.useSoundForNotificationsProperty.set(useSoundForNotifications); } public void setTakeOfferSelectedPaymentAccountId(String value) { prefPayload.setTakeOfferSelectedPaymentAccountId(value); requestPersistence(); } public void setIgnoreDustThreshold(int value) { prefPayload.setIgnoreDustThreshold(value); requestPersistence(); } public void setClearDataAfterDays(int value) { prefPayload.setClearDataAfterDays(value); requestPersistence(); } public void setShowOffersMatchingMyAccounts(boolean value) { prefPayload.setShowOffersMatchingMyAccounts(value); requestPersistence(); } public void setShowPrivateOffers(boolean value) { prefPayload.setShowPrivateOffers(value); requestPersistence(); } public void setDenyApiTaker(boolean value) { prefPayload.setDenyApiTaker(value); requestPersistence(); } public void setNotifyOnPreRelease(boolean value) { prefPayload.setNotifyOnPreRelease(value); requestPersistence(); } public void setXmrNodeSettings(XmrNodeSettings settings) { prefPayload.setXmrNodeSettings(settings); requestPersistence(); } /////////////////////////////////////////////////////////////////////////////////////////// // Getter /////////////////////////////////////////////////////////////////////////////////////////// public BooleanProperty useAnimationsProperty() { return useAnimationsProperty; } public ObservableList getTraditionalCurrenciesAsObservable() { return traditionalCurrenciesAsObservable; } public ObservableList getCryptoCurrenciesAsObservable() { return cryptoCurrenciesAsObservable; } public ObservableList getTradeCurrenciesAsObservable() { return tradeCurrenciesAsObservable; } public ObservableMap getDontShowAgainMapAsObservable() { return dontShowAgainMapAsObservable; } public BlockChainExplorer getBlockChainExplorer() { BaseCurrencyNetwork baseCurrencyNetwork = Config.baseCurrencyNetwork(); switch (baseCurrencyNetwork) { case XMR_MAINNET: return prefPayload.getBlockChainExplorerMainNet(); case XMR_STAGENET: return prefPayload.getBlockChainExplorerTestNet(); case XMR_LOCAL: return prefPayload.getBlockChainExplorerTestNet(); // TODO: no testnet explorer for private testnet default: throw new RuntimeException("BaseCurrencyNetwork not defined. BaseCurrencyNetwork=" + baseCurrencyNetwork); } } public ArrayList getBlockChainExplorers() { BaseCurrencyNetwork baseCurrencyNetwork = Config.baseCurrencyNetwork(); switch (baseCurrencyNetwork) { case XMR_MAINNET: return XMR_MAIN_NET_EXPLORERS; case XMR_STAGENET: return XMR_STAGE_NET_EXPLORERS; case XMR_LOCAL: return XMR_STAGE_NET_EXPLORERS; // TODO: no testnet explorer for private testnet default: throw new RuntimeException("BaseCurrencyNetwork not defined. BaseCurrencyNetwork=" + baseCurrencyNetwork); } } public boolean showAgain(String key) { return !prefPayload.getDontShowAgainMap().containsKey(key) || !prefPayload.getDontShowAgainMap().get(key); } public UseTorForXmr getUseTorForXmr() { return UseTorForXmr.class.getEnumConstants()[prefPayload.getUseTorForXmrOrdinal()]; } public boolean isProxyApplied(boolean wasWalletSynced) { return getUseTorForXmr() == UseTorForXmr.ON || (getUseTorForXmr() == UseTorForXmr.AFTER_SYNC && wasWalletSynced); } public boolean getSplitOfferOutput() { return prefPayload.isSplitOfferOutput(); } public double getSecurityDepositAsPercent(PaymentAccount paymentAccount) { double value = PaymentAccountUtil.isCryptoCurrencyAccount(paymentAccount) ? prefPayload.getSecurityDepositAsPercentForCrypto() : prefPayload.getSecurityDepositAsPercent(); if (value < Restrictions.getMinSecurityDepositPct()) { value = Restrictions.getMinSecurityDepositPct(); setSecurityDepositAsPercent(value, paymentAccount); } return value == 0 ? Restrictions.getDefaultSecurityDepositPct() : value; } @Override @Nullable public List getBridgeAddresses() { return prefPayload.getBridgeAddresses(); } public List getDefaultXmrTxProofServices() { if (config.useLocalhostForP2P) { return XMR_TX_PROOF_SERVICES_CLEAR_NET; } else { return XMR_TX_PROOF_SERVICES; } } public List getDefaultTxBroadcastServices() { if (config.useLocalhostForP2P) { return TX_BROADCAST_SERVICES_CLEAR_NET; } else { return TX_BROADCAST_SERVICES; } } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void updateTradeCurrencies(ListChangeListener.Change change) { change.next(); if (change.wasAdded() && change.getAddedSize() == 1 && initialReadDone) tradeCurrenciesAsObservable.add(change.getAddedSubList().get(0)); else if (change.wasRemoved() && change.getRemovedSize() == 1 && initialReadDone) tradeCurrenciesAsObservable.remove(change.getRemoved().get(0)); requestPersistence(); } private interface ExcludesDelegateMethods { void setTacAccepted(boolean tacAccepted); void setUseAnimations(boolean useAnimations); void setCssTheme(int cssTheme); void setUserLanguage(@NotNull String userLanguageCode); void setUserCountry(@NotNull Country userCountry); void setPreferredTradeCurrency(TradeCurrency preferredTradeCurrency); void setSplitOfferOutput(boolean splitOfferOutput); void setShowOwnOffersInOfferBook(boolean showOwnOffersInOfferBook); void setMaxPriceDistanceInPercent(double maxPriceDistanceInPercent); void setBackupDirectory(String backupDirectory); void setAutoSelectArbitrators(boolean autoSelectArbitrators); void setUsePercentageBasedPrice(boolean usePercentageBasedPrice); void setTagForPeer(String hostName, String tag); void setOfferBookChartScreenCurrencyCode(String offerBookChartScreenCurrencyCode); void setBuyScreenCurrencyCode(String buyScreenCurrencyCode); void setSellScreenCurrencyCode(String sellScreenCurrencyCode); void setIgnoreTradersList(List ignoreTradersList); void setDirectoryChooserPath(String directoryChooserPath); void setTradeChartsScreenCurrencyCode(String tradeChartsScreenCurrencyCode); void setTradeStatisticsTickUnitIndex(int tradeStatisticsTickUnitIndex); void setSortMarketCurrenciesNumerically(boolean sortMarketCurrenciesNumerically); void setMoneroNodes(String moneroNodes); void setUseCustomWithdrawalTxFee(boolean useCustomWithdrawalTxFee); void setWithdrawalTxFeeInVbytes(long withdrawalTxFeeInVbytes); void setSelectedPaymentAccountForCreateOffer(@Nullable PaymentAccount paymentAccount); void setPayFeeInXmr(boolean payFeeInXmr); void setTraditionalCurrencies(List currencies); void setCryptoCurrencies(List currencies); void setBlockChainExplorerTestNet(BlockChainExplorer blockChainExplorerTestNet); void setBlockChainExplorerMainNet(BlockChainExplorer blockChainExplorerMainNet); void setResyncSpvRequested(boolean resyncSpvRequested); void setDontShowAgainMap(Map dontShowAgainMap); void setPeerTagMap(Map peerTagMap); void setBridgeAddresses(List bridgeAddresses); void setBridgeOptionOrdinal(int bridgeOptionOrdinal); void setTorTransportOrdinal(int torTransportOrdinal); void setCustomBridges(String customBridges); void setUseTorForXmrOrdinal(int useTorForXmrOrdinal); void setMoneroNodesOptionOrdinal(int moneroNodesOption); void setReferralId(String referralId); void setPhoneKeyAndToken(String phoneKeyAndToken); void setUseSoundForMobileNotifications(boolean value); void setUseTradeNotifications(boolean value); void setUseMarketNotifications(boolean value); void setUsePriceNotifications(boolean value); List getBridgeAddresses(); long getWithdrawalTxFeeInVbytes(); void setUseStandbyMode(boolean useStandbyMode); void setUseSoundForNotifications(boolean useSoundForNotifications); void setTakeOfferSelectedPaymentAccountId(String value); void setIgnoreDustThreshold(int value); void setBuyerSecurityDepositAsPercent(double buyerSecurityDepositAsPercent); double getBuyerSecurityDepositAsPercent(); void setRpcUser(String value); void setRpcPw(String value); void setBlockNotifyPort(int value); String getRpcUser(); String getRpcPw(); int getBlockNotifyPort(); void setTacAcceptedV120(boolean tacAccepted); void setBsqAverageTrimThreshold(double bsqAverageTrimThreshold); void setAutoConfirmSettings(AutoConfirmSettings autoConfirmSettings); void setHideNonAccountPaymentMethods(boolean hideNonAccountPaymentMethods); void setShowOffersMatchingMyAccounts(boolean value); void setDenyApiTaker(boolean value); void setNotifyOnPreRelease(boolean value); void setXmrNodeSettings(XmrNodeSettings settings); } } ================================================ FILE: core/src/main/java/haveno/core/user/PreferencesPayload.java ================================================ /* * 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 . */ package haveno.core.user; import com.google.common.collect.Maps; import com.google.protobuf.Message; import haveno.common.proto.ProtoUtil; import haveno.common.proto.persistable.PersistableEnvelope; import haveno.core.locale.Country; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.PaymentAccount; import haveno.core.proto.CoreProtoResolver; import haveno.core.xmr.XmrNodeSettings; import lombok.AllArgsConstructor; import lombok.Data; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositPct; @Slf4j @Data @AllArgsConstructor public final class PreferencesPayload implements PersistableEnvelope { private String userLanguage; private Country userCountry; private List traditionalCurrencies = new ArrayList<>(); private List cryptoCurrencies = new ArrayList<>(); private BlockChainExplorer blockChainExplorerMainNet; private BlockChainExplorer blockChainExplorerTestNet; @Nullable private String backupDirectory; private boolean autoSelectArbitrators = true; private Map dontShowAgainMap = new HashMap<>(); private boolean tacAccepted; private boolean splitOfferOutput = true; private boolean showOwnOffersInOfferBook = true; @Nullable private TradeCurrency preferredTradeCurrency; private long withdrawalTxFeeInVbytes = 100; private boolean useCustomWithdrawalTxFee = false; private double maxPriceDistanceInPercent = 0.3; @Nullable private String offerBookChartScreenCurrencyCode; @Nullable private String tradeChartsScreenCurrencyCode; @Nullable private String buyScreenCurrencyCode; @Nullable private String sellScreenCurrencyCode; @Nullable private String buyScreenCryptoCurrencyCode; @Nullable private String sellScreenCryptoCurrencyCode; @Nullable private String buyScreenOtherCurrencyCode; @Nullable private String sellScreenOtherCurrencyCode; private int tradeStatisticsTickUnitIndex = 3; private boolean resyncSpvRequested; private boolean sortMarketCurrenciesNumerically = true; private boolean usePercentageBasedPrice = true; private Map peerTagMap = new HashMap<>(); // custom xmr nodes private String moneroNodes = ""; private List ignoreTradersList = new ArrayList<>(); private String directoryChooserPath; private boolean useAnimations; private int cssTheme; @Nullable private PaymentAccount selectedPaymentAccountForCreateOffer; @Nullable private List bridgeAddresses; private int bridgeOptionOrdinal; private int torTransportOrdinal; @Nullable private String customBridges; private int useTorForXmrOrdinal; private int moneroNodesOptionOrdinal; @Nullable private String referralId; @Nullable private String phoneKeyAndToken; private boolean useSoundForMobileNotifications = true; private boolean useTradeNotifications = true; private boolean useMarketNotifications = true; private boolean usePriceNotifications = true; private boolean useStandbyMode = false; private boolean useSoundForNotifications = true; private boolean useSoundForNotificationsInitialized = false; @Nullable private String rpcUser; @Nullable private String rpcPw; @Nullable private String takeOfferSelectedPaymentAccountId; private double securityDepositAsPercent = getDefaultSecurityDepositPct(); private int ignoreDustThreshold = 600; private int clearDataAfterDays = Preferences.CLEAR_DATA_AFTER_DAYS_DEFAULT; private double securityDepositAsPercentForCrypto = getDefaultSecurityDepositPct(); private int blockNotifyPort; private boolean tacAcceptedV120; private double bsqAverageTrimThreshold = 0.05; // Added at 1.3.8 private List autoConfirmSettingsList = new ArrayList<>(); // Added in 1.5.5 private boolean hideNonAccountPaymentMethods; private boolean showOffersMatchingMyAccounts; private boolean showPrivateOffers; private boolean denyApiTaker; private boolean notifyOnPreRelease; private XmrNodeSettings xmrNodeSettings = new XmrNodeSettings(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// PreferencesPayload() { } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// @Override public Message toProtoMessage() { protobuf.PreferencesPayload.Builder builder = protobuf.PreferencesPayload.newBuilder() .setUserLanguage(userLanguage) .setUserCountry((protobuf.Country) userCountry.toProtoMessage()) .addAllTraditionalCurrencies(traditionalCurrencies.stream() .map(traditionalCurrency -> ((protobuf.TradeCurrency) traditionalCurrency.toProtoMessage())) .collect(Collectors.toList())) .addAllCryptoCurrencies(cryptoCurrencies.stream() .map(cryptoCurrency -> ((protobuf.TradeCurrency) cryptoCurrency.toProtoMessage())) .collect(Collectors.toList())) .setBlockChainExplorerMainNet((protobuf.BlockChainExplorer) blockChainExplorerMainNet.toProtoMessage()) .setBlockChainExplorerTestNet((protobuf.BlockChainExplorer) blockChainExplorerTestNet.toProtoMessage()) .setAutoSelectArbitrators(autoSelectArbitrators) .putAllDontShowAgainMap(dontShowAgainMap) .setTacAccepted(tacAccepted) .setSplitOfferOutput(splitOfferOutput) .setShowOwnOffersInOfferBook(showOwnOffersInOfferBook) .setWithdrawalTxFeeInVbytes(withdrawalTxFeeInVbytes) .setUseCustomWithdrawalTxFee(useCustomWithdrawalTxFee) .setMaxPriceDistanceInPercent(maxPriceDistanceInPercent) .setTradeStatisticsTickUnitIndex(tradeStatisticsTickUnitIndex) .setResyncSpvRequested(resyncSpvRequested) .setSortMarketCurrenciesNumerically(sortMarketCurrenciesNumerically) .setUsePercentageBasedPrice(usePercentageBasedPrice) .putAllPeerTagMap(peerTagMap) .setMoneroNodes(moneroNodes) .addAllIgnoreTradersList(ignoreTradersList) .setDirectoryChooserPath(directoryChooserPath) .setUseAnimations(useAnimations) .setCssTheme(cssTheme) .setBridgeOptionOrdinal(bridgeOptionOrdinal) .setTorTransportOrdinal(torTransportOrdinal) .setUseTorForXmrOrdinal(useTorForXmrOrdinal) .setMoneroNodesOptionOrdinal(moneroNodesOptionOrdinal) .setUseSoundForMobileNotifications(useSoundForMobileNotifications) .setUseTradeNotifications(useTradeNotifications) .setUseMarketNotifications(useMarketNotifications) .setUsePriceNotifications(usePriceNotifications) .setUseStandbyMode(useStandbyMode) .setUseSoundForNotifications(useSoundForNotifications) .setUseSoundForNotificationsInitialized(useSoundForNotificationsInitialized) .setSecurityDepositAsPercent(securityDepositAsPercent) .setIgnoreDustThreshold(ignoreDustThreshold) .setClearDataAfterDays(clearDataAfterDays) .setSecurityDepositAsPercentForCrypto(securityDepositAsPercentForCrypto) .setBlockNotifyPort(blockNotifyPort) .setTacAcceptedV120(tacAcceptedV120) .setBsqAverageTrimThreshold(bsqAverageTrimThreshold) .addAllAutoConfirmSettings(autoConfirmSettingsList.stream() .map(autoConfirmSettings -> ((protobuf.AutoConfirmSettings) autoConfirmSettings.toProtoMessage())) .collect(Collectors.toList())) .setHideNonAccountPaymentMethods(hideNonAccountPaymentMethods) .setShowOffersMatchingMyAccounts(showOffersMatchingMyAccounts) .setShowPrivateOffers(showPrivateOffers) .setDenyApiTaker(denyApiTaker) .setNotifyOnPreRelease(notifyOnPreRelease); Optional.ofNullable(backupDirectory).ifPresent(builder::setBackupDirectory); Optional.ofNullable(preferredTradeCurrency).ifPresent(e -> builder.setPreferredTradeCurrency((protobuf.TradeCurrency) e.toProtoMessage())); Optional.ofNullable(offerBookChartScreenCurrencyCode).ifPresent(builder::setOfferBookChartScreenCurrencyCode); Optional.ofNullable(tradeChartsScreenCurrencyCode).ifPresent(builder::setTradeChartsScreenCurrencyCode); Optional.ofNullable(buyScreenCurrencyCode).ifPresent(builder::setBuyScreenCurrencyCode); Optional.ofNullable(sellScreenCurrencyCode).ifPresent(builder::setSellScreenCurrencyCode); Optional.ofNullable(buyScreenCryptoCurrencyCode).ifPresent(builder::setBuyScreenCryptoCurrencyCode); Optional.ofNullable(sellScreenCryptoCurrencyCode).ifPresent(builder::setSellScreenCryptoCurrencyCode); Optional.ofNullable(buyScreenOtherCurrencyCode).ifPresent(builder::setBuyScreenOtherCurrencyCode); Optional.ofNullable(sellScreenOtherCurrencyCode).ifPresent(builder::setSellScreenOtherCurrencyCode); Optional.ofNullable(selectedPaymentAccountForCreateOffer).ifPresent( account -> builder.setSelectedPaymentAccountForCreateOffer(selectedPaymentAccountForCreateOffer.toProtoMessage())); Optional.ofNullable(bridgeAddresses).ifPresent(builder::addAllBridgeAddresses); Optional.ofNullable(customBridges).ifPresent(builder::setCustomBridges); Optional.ofNullable(referralId).ifPresent(builder::setReferralId); Optional.ofNullable(phoneKeyAndToken).ifPresent(builder::setPhoneKeyAndToken); Optional.ofNullable(rpcUser).ifPresent(builder::setRpcUser); Optional.ofNullable(rpcPw).ifPresent(builder::setRpcPw); Optional.ofNullable(takeOfferSelectedPaymentAccountId).ifPresent(builder::setTakeOfferSelectedPaymentAccountId); Optional.ofNullable(xmrNodeSettings).ifPresent(settings -> builder.setXmrNodeSettings(settings.toProtoMessage())); return protobuf.PersistableEnvelope.newBuilder().setPreferencesPayload(builder).build(); } public static PreferencesPayload fromProto(protobuf.PreferencesPayload proto, CoreProtoResolver coreProtoResolver) { final protobuf.Country userCountry = proto.getUserCountry(); PaymentAccount paymentAccount = null; if (proto.hasSelectedPaymentAccountForCreateOffer() && proto.getSelectedPaymentAccountForCreateOffer().hasPaymentMethod()) paymentAccount = PaymentAccount.fromProto(proto.getSelectedPaymentAccountForCreateOffer(), coreProtoResolver); return new PreferencesPayload( proto.getUserLanguage(), Country.fromProto(userCountry), proto.getTraditionalCurrenciesList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getTraditionalCurrenciesList().stream() .map(TraditionalCurrency::fromProto) .filter(Objects::nonNull) .collect(Collectors.toList())), proto.getCryptoCurrenciesList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getCryptoCurrenciesList().stream() .map(CryptoCurrency::fromProto) .filter(Objects::nonNull) .collect(Collectors.toList())), BlockChainExplorer.fromProto(proto.getBlockChainExplorerMainNet()), BlockChainExplorer.fromProto(proto.getBlockChainExplorerTestNet()), ProtoUtil.stringOrNullFromProto(proto.getBackupDirectory()), proto.getAutoSelectArbitrators(), Maps.newHashMap(proto.getDontShowAgainMapMap()), proto.getTacAccepted(), proto.getSplitOfferOutput(), proto.getShowOwnOffersInOfferBook(), proto.hasPreferredTradeCurrency() ? TradeCurrency.fromProto(proto.getPreferredTradeCurrency()) : null, proto.getWithdrawalTxFeeInVbytes(), proto.getUseCustomWithdrawalTxFee(), proto.getMaxPriceDistanceInPercent(), ProtoUtil.stringOrNullFromProto(proto.getOfferBookChartScreenCurrencyCode()), ProtoUtil.stringOrNullFromProto(proto.getTradeChartsScreenCurrencyCode()), ProtoUtil.stringOrNullFromProto(proto.getBuyScreenCurrencyCode()), ProtoUtil.stringOrNullFromProto(proto.getSellScreenCurrencyCode()), ProtoUtil.stringOrNullFromProto(proto.getBuyScreenCryptoCurrencyCode()), ProtoUtil.stringOrNullFromProto(proto.getSellScreenCryptoCurrencyCode()), ProtoUtil.stringOrNullFromProto(proto.getBuyScreenOtherCurrencyCode()), ProtoUtil.stringOrNullFromProto(proto.getSellScreenOtherCurrencyCode()), proto.getTradeStatisticsTickUnitIndex(), proto.getResyncSpvRequested(), proto.getSortMarketCurrenciesNumerically(), proto.getUsePercentageBasedPrice(), Maps.newHashMap(proto.getPeerTagMapMap()), proto.getMoneroNodes(), proto.getIgnoreTradersListList(), proto.getDirectoryChooserPath(), proto.getUseAnimations(), proto.getCssTheme(), paymentAccount, proto.getBridgeAddressesList().isEmpty() ? null : new ArrayList<>(proto.getBridgeAddressesList()), proto.getBridgeOptionOrdinal(), proto.getTorTransportOrdinal(), ProtoUtil.stringOrNullFromProto(proto.getCustomBridges()), proto.getUseTorForXmrOrdinal(), proto.getMoneroNodesOptionOrdinal(), proto.getReferralId().isEmpty() ? null : proto.getReferralId(), proto.getPhoneKeyAndToken().isEmpty() ? null : proto.getPhoneKeyAndToken(), proto.getUseSoundForMobileNotifications(), proto.getUseTradeNotifications(), proto.getUseMarketNotifications(), proto.getUsePriceNotifications(), proto.getUseStandbyMode(), proto.getUseSoundForNotifications(), proto.getUseSoundForNotificationsInitialized(), proto.getRpcUser().isEmpty() ? null : proto.getRpcUser(), proto.getRpcPw().isEmpty() ? null : proto.getRpcPw(), proto.getTakeOfferSelectedPaymentAccountId().isEmpty() ? null : proto.getTakeOfferSelectedPaymentAccountId(), proto.getSecurityDepositAsPercent(), proto.getIgnoreDustThreshold(), proto.getClearDataAfterDays(), proto.getSecurityDepositAsPercentForCrypto(), proto.getBlockNotifyPort(), proto.getTacAcceptedV120(), proto.getBsqAverageTrimThreshold(), proto.getAutoConfirmSettingsList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getAutoConfirmSettingsList().stream() .map(AutoConfirmSettings::fromProto) .collect(Collectors.toList())), proto.getHideNonAccountPaymentMethods(), proto.getShowOffersMatchingMyAccounts(), proto.getShowPrivateOffers(), proto.getDenyApiTaker(), proto.getNotifyOnPreRelease(), XmrNodeSettings.fromProto(proto.getXmrNodeSettings()) ); } } ================================================ FILE: core/src/main/java/haveno/core/user/User.java ================================================ /* * 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 . */ package haveno.core.user; import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.crypto.KeyRing; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.persistable.PersistedDataHost; import haveno.core.alert.Alert; import haveno.core.filter.Filter; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.LanguageUtil; import haveno.core.locale.TradeCurrency; import haveno.core.notifications.alerts.market.MarketAlertFilter; import haveno.core.notifications.alerts.price.PriceAlertFilter; import haveno.core.payment.PaymentAccount; import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; import haveno.core.support.dispute.mediation.mediator.Mediator; import haveno.core.support.dispute.refund.refundagent.RefundAgent; import haveno.network.p2p.NodeAddress; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javax.annotation.Nullable; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; /** * The User is persisted locally. * It must never be transmitted over the wire (messageKeyPair contains private key!). */ @Slf4j @AllArgsConstructor @Singleton public class User implements PersistedDataHost { private final PersistenceManager persistenceManager; private final KeyRing keyRing; private ObservableList paymentAccountsAsObservable; private ObjectProperty currentPaymentAccountProperty; private UserPayload userPayload = new UserPayload(); private boolean isPaymentAccountImport = false; @Inject public User(PersistenceManager persistenceManager, KeyRing keyRing) { this.persistenceManager = persistenceManager; this.keyRing = keyRing; } // for unit tests public User() { persistenceManager = null; keyRing = null; } @Override public void readPersisted(Runnable completeHandler) { checkNotNull(persistenceManager).readPersisted("UserPayload", persisted -> { userPayload = persisted; init(); completeHandler.run(); }, () -> { init(); completeHandler.run(); }); } private void init() { checkNotNull(persistenceManager).initialize(userPayload, PersistenceManager.Source.PRIVATE); checkNotNull(userPayload.getPaymentAccounts(), "userPayload.getPaymentAccounts() must not be null"); checkNotNull(userPayload.getAcceptedLanguageLocaleCodes(), "userPayload.getAcceptedLanguageLocaleCodes() must not be null"); paymentAccountsAsObservable = FXCollections.observableArrayList(userPayload.getPaymentAccounts()); currentPaymentAccountProperty = new SimpleObjectProperty<>(userPayload.getCurrentPaymentAccount()); userPayload.setAccountId(String.valueOf(Math.abs(checkNotNull(keyRing).getPubKeyRing().hashCode()))); // language setup if (!userPayload.getAcceptedLanguageLocaleCodes().contains(LanguageUtil.getDefaultLanguageLocaleAsCode())) userPayload.getAcceptedLanguageLocaleCodes().add(LanguageUtil.getDefaultLanguageLocaleAsCode()); String english = LanguageUtil.getEnglishLanguageLocaleCode(); if (!userPayload.getAcceptedLanguageLocaleCodes().contains(english)) userPayload.getAcceptedLanguageLocaleCodes().add(english); paymentAccountsAsObservable.addListener((ListChangeListener) change -> { synchronized (paymentAccountsAsObservable) { userPayload.setPaymentAccounts(new HashSet<>(paymentAccountsAsObservable)); requestPersistence(); } }); currentPaymentAccountProperty.addListener((ov) -> { userPayload.setCurrentPaymentAccount(currentPaymentAccountProperty.get()); requestPersistence(); }); requestPersistence(); } public void requestPersistence() { if (persistenceManager != null) persistenceManager.requestPersistence(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Nullable public Arbitrator getAcceptedArbitratorByAddress(NodeAddress nodeAddress) { final List acceptedArbitrators = userPayload.getAcceptedArbitrators(); if (acceptedArbitrators != null) { Optional arbitratorOptional = acceptedArbitrators.stream() .filter(e -> e.getNodeAddress().equals(nodeAddress)) .findFirst(); return arbitratorOptional.orElse(null); } else { return null; } } @Nullable public Mediator getAcceptedMediatorByAddress(NodeAddress nodeAddress) { final List acceptedMediators = userPayload.getAcceptedMediators(); if (acceptedMediators != null) { Optional mediatorOptionalOptional = acceptedMediators.stream() .filter(e -> e.getNodeAddress().equals(nodeAddress)) .findFirst(); return mediatorOptionalOptional.orElse(null); } else { return null; } } @Nullable public RefundAgent getAcceptedRefundAgentByAddress(NodeAddress nodeAddress) { final List acceptedRefundAgents = userPayload.getAcceptedRefundAgents(); if (acceptedRefundAgents != null) { Optional refundAgentOptional = acceptedRefundAgents.stream() .filter(e -> e.getNodeAddress().equals(nodeAddress)) .findFirst(); return refundAgentOptional.orElse(null); } else { return null; } } @Nullable public PaymentAccount findFirstPaymentAccountWithCurrency(TradeCurrency tradeCurrency) { if (userPayload.getPaymentAccounts() != null) { for (PaymentAccount paymentAccount : userPayload.getPaymentAccounts()) { for (TradeCurrency currency : paymentAccount.getTradeCurrencies()) { if (currency.equals(tradeCurrency)) return paymentAccount; } } return null; } else { return null; } } public boolean hasPaymentAccountForCurrency(TradeCurrency tradeCurrency) { return findFirstPaymentAccountWithCurrency(tradeCurrency) != null; } public boolean hasFiatPaymentAccount() { if (userPayload.getPaymentAccounts() != null) { for (PaymentAccount paymentAccount : userPayload.getPaymentAccounts()) { List tradeCurrencies = paymentAccount.getTradeCurrencies(); if (tradeCurrencies.isEmpty()) continue; if (CurrencyUtil.isFiatCurrency(tradeCurrencies.get(0).getCode())) return true; else continue; } } return false; } public boolean hasCryptoPaymentAccount() { if (userPayload.getPaymentAccounts() != null) { for (PaymentAccount paymentAccount : userPayload.getPaymentAccounts()) { List tradeCurrencies = paymentAccount.getTradeCurrencies(); if (tradeCurrencies.isEmpty()) continue; if (CurrencyUtil.isCryptoCurrency(tradeCurrencies.get(0).getCode())) return true; else continue; } } return false; } public boolean hasTraditionalNonFiatAccount() { if (userPayload.getPaymentAccounts() != null) { for (PaymentAccount paymentAccount : userPayload.getPaymentAccounts()) { List tradeCurrencies = paymentAccount.getTradeCurrencies(); if (tradeCurrencies.isEmpty()) continue; if (CurrencyUtil.isTraditionalNonFiatCurrency(tradeCurrencies.get(0).getCode())) return true; else continue; } } return false; } /////////////////////////////////////////////////////////////////////////////////////////// // Collection operations /////////////////////////////////////////////////////////////////////////////////////////// public void addPaymentAccountIfNotExists(PaymentAccount paymentAccount) { if (!paymentAccountExists(paymentAccount)) { addPaymentAccount(paymentAccount); requestPersistence(); } } public void addPaymentAccount(PaymentAccount paymentAccount) { paymentAccount.onAddToUser(); synchronized (paymentAccountsAsObservable) { boolean changed = paymentAccountsAsObservable.add(paymentAccount); setCurrentPaymentAccount(paymentAccount); if (changed) requestPersistence(); } } public void addImportedPaymentAccounts(Collection paymentAccounts) { synchronized (paymentAccountsAsObservable) { isPaymentAccountImport = true; boolean changed = paymentAccountsAsObservable.addAll(paymentAccounts); paymentAccounts.stream().findFirst().ifPresent(this::setCurrentPaymentAccount); if (changed) requestPersistence(); isPaymentAccountImport = false; } } public void removePaymentAccount(PaymentAccount paymentAccount) { synchronized (paymentAccountsAsObservable) { boolean changed = paymentAccountsAsObservable.remove(paymentAccount); if (changed) requestPersistence(); } } public boolean addAcceptedArbitrator(Arbitrator arbitrator) { List arbitrators = userPayload.getAcceptedArbitrators(); if (arbitrators != null && !arbitrators.contains(arbitrator) && !isMyOwnRegisteredArbitrator(arbitrator)) { arbitrators.add(arbitrator); requestPersistence(); return true; } else { return false; } } public void removeAcceptedArbitrator(Arbitrator arbitrator) { if (userPayload.getAcceptedArbitrators() != null) { boolean changed = userPayload.getAcceptedArbitrators().remove(arbitrator); if (changed) requestPersistence(); } } public void clearAcceptedArbitrators() { if (userPayload.getAcceptedArbitrators() != null) { userPayload.getAcceptedArbitrators().clear(); requestPersistence(); } } public boolean addAcceptedMediator(Mediator mediator) { List mediators = userPayload.getAcceptedMediators(); if (mediators != null && !mediators.contains(mediator) && !isMyOwnRegisteredMediator(mediator)) { mediators.add(mediator); requestPersistence(); return true; } else { return false; } } public void removeAcceptedMediator(Mediator mediator) { if (userPayload.getAcceptedMediators() != null) { boolean changed = userPayload.getAcceptedMediators().remove(mediator); if (changed) requestPersistence(); } } public void clearAcceptedMediators() { if (userPayload.getAcceptedMediators() != null) { userPayload.getAcceptedMediators().clear(); requestPersistence(); } } public boolean addAcceptedRefundAgent(RefundAgent refundAgent) { List refundAgents = userPayload.getAcceptedRefundAgents(); if (refundAgents != null && !refundAgents.contains(refundAgent) && !isMyOwnRegisteredRefundAgent(refundAgent)) { refundAgents.add(refundAgent); requestPersistence(); return true; } else { return false; } } public void removeAcceptedRefundAgent(RefundAgent refundAgent) { if (userPayload.getAcceptedRefundAgents() != null) { boolean changed = userPayload.getAcceptedRefundAgents().remove(refundAgent); if (changed) requestPersistence(); } } public void clearAcceptedRefundAgents() { if (userPayload.getAcceptedRefundAgents() != null) { userPayload.getAcceptedRefundAgents().clear(); requestPersistence(); } } /////////////////////////////////////////////////////////////////////////////////////////// // Setters /////////////////////////////////////////////////////////////////////////////////////////// public void setCurrentPaymentAccount(PaymentAccount paymentAccount) { currentPaymentAccountProperty.set(paymentAccount); requestPersistence(); } public void setRegisteredArbitrator(@Nullable Arbitrator arbitrator) { userPayload.setRegisteredArbitrator(arbitrator); requestPersistence(); } public void setRegisteredMediator(@Nullable Mediator mediator) { userPayload.setRegisteredMediator(mediator); requestPersistence(); } public void setRegisteredRefundAgent(@Nullable RefundAgent refundAgent) { userPayload.setRegisteredRefundAgent(refundAgent); requestPersistence(); } public void setDevelopersFilter(@Nullable Filter developersFilter) { userPayload.setDevelopersFilter(developersFilter); requestPersistence(); } public void setDevelopersAlert(@Nullable Alert developersAlert) { userPayload.setDevelopersAlert(developersAlert); requestPersistence(); } public void setDisplayedAlert(@Nullable Alert displayedAlert) { userPayload.setDisplayedAlert(displayedAlert); requestPersistence(); } public void addMarketAlertFilter(MarketAlertFilter filter) { getMarketAlertFilters().add(filter); requestPersistence(); } public void removeMarketAlertFilter(MarketAlertFilter filter) { getMarketAlertFilters().remove(filter); requestPersistence(); } public void setPriceAlertFilter(PriceAlertFilter filter) { userPayload.setPriceAlertFilter(filter); requestPersistence(); } public void removePriceAlertFilter() { userPayload.setPriceAlertFilter(null); requestPersistence(); } public void setWalletCreationDate(long walletCreationDate) { userPayload.setWalletCreationDate(walletCreationDate); requestPersistence(); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// @Nullable public PaymentAccount getPaymentAccount(String paymentAccountId) { Optional optional = userPayload.getPaymentAccounts() != null ? userPayload.getPaymentAccounts().stream().filter(e -> e.getId().equals(paymentAccountId)).findAny() : Optional.empty(); return optional.orElse(null); } public String getAccountId() { return userPayload.getAccountId(); } public ReadOnlyObjectProperty currentPaymentAccountProperty() { return currentPaymentAccountProperty; } @Nullable public Set getPaymentAccounts() { return userPayload.getPaymentAccounts(); } @Nullable public ObservableList getPaymentAccountsAsObservable() { return paymentAccountsAsObservable; } /** * If this user is an arbitrator it returns the registered arbitrator. * * @return The arbitrator registered for this user */ @Nullable public Arbitrator getRegisteredArbitrator() { return userPayload.getRegisteredArbitrator(); } @Nullable public Mediator getRegisteredMediator() { return userPayload.getRegisteredMediator(); } @Nullable public RefundAgent getRegisteredRefundAgent() { return userPayload.getRegisteredRefundAgent(); } @Nullable public List getAcceptedArbitrators() { return userPayload.getAcceptedArbitrators(); } @Nullable public List getAcceptedMediators() { return userPayload.getAcceptedMediators(); } @Nullable public List getAcceptedRefundAgents() { return userPayload.getAcceptedRefundAgents(); } @Nullable public List getAcceptedArbitratorAddresses() { return userPayload.getAcceptedArbitrators() != null ? userPayload.getAcceptedArbitrators().stream().map(Arbitrator::getNodeAddress).collect(Collectors.toList()) : null; } @Nullable public List getAcceptedMediatorAddresses() { return userPayload.getAcceptedMediators() != null ? userPayload.getAcceptedMediators().stream().map(Mediator::getNodeAddress).collect(Collectors.toList()) : null; } @Nullable public List getAcceptedRefundAgentAddresses() { return userPayload.getAcceptedRefundAgents() != null ? userPayload.getAcceptedRefundAgents().stream().map(RefundAgent::getNodeAddress).collect(Collectors.toList()) : null; } public boolean hasAcceptedArbitrators() { return getAcceptedArbitrators() != null && !getAcceptedArbitrators().isEmpty(); } public boolean hasAcceptedMediators() { return getAcceptedMediators() != null && !getAcceptedMediators().isEmpty(); } public boolean hasAcceptedRefundAgents() { return getAcceptedRefundAgents() != null && !getAcceptedRefundAgents().isEmpty(); } @Nullable public Filter getDevelopersFilter() { return userPayload.getDevelopersFilter(); } @Nullable public Alert getDevelopersAlert() { return userPayload.getDevelopersAlert(); } @Nullable public Alert getDisplayedAlert() { return userPayload.getDisplayedAlert(); } public boolean isMyOwnRegisteredArbitrator(Arbitrator arbitrator) { return arbitrator.equals(userPayload.getRegisteredArbitrator()); } public boolean isMyOwnRegisteredMediator(Mediator mediator) { return mediator.equals(userPayload.getRegisteredMediator()); } public boolean isMyOwnRegisteredRefundAgent(RefundAgent refundAgent) { return refundAgent.equals(userPayload.getRegisteredRefundAgent()); } public List getMarketAlertFilters() { return userPayload.getMarketAlertFilters(); } @Nullable public PriceAlertFilter getPriceAlertFilter() { return userPayload.getPriceAlertFilter(); } public boolean isPaymentAccountImport() { return isPaymentAccountImport; } private boolean paymentAccountExists(PaymentAccount paymentAccount) { synchronized (paymentAccountsAsObservable) { return getPaymentAccountsAsObservable().stream().anyMatch(e -> e.equals(paymentAccount)); } } public Cookie getCookie() { return userPayload.getCookie(); } public long getWalletCreationDate() { return userPayload.getWalletCreationDate(); } } ================================================ FILE: core/src/main/java/haveno/core/user/UserPayload.java ================================================ /* * 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 . */ package haveno.core.user; import haveno.common.proto.ProtoUtil; import haveno.common.proto.persistable.PersistableEnvelope; import haveno.core.alert.Alert; import haveno.core.filter.Filter; import haveno.core.notifications.alerts.market.MarketAlertFilter; import haveno.core.notifications.alerts.price.PriceAlertFilter; import haveno.core.payment.PaymentAccount; import haveno.core.proto.CoreProtoResolver; import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; import haveno.core.support.dispute.mediation.mediator.Mediator; import haveno.core.support.dispute.refund.refundagent.RefundAgent; import lombok.AllArgsConstructor; import lombok.Data; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @Slf4j @Data @AllArgsConstructor public class UserPayload implements PersistableEnvelope { @Nullable private String accountId; @Nullable private Set paymentAccounts = new HashSet<>(); @Nullable private PaymentAccount currentPaymentAccount; @Nullable private List acceptedLanguageLocaleCodes = new ArrayList<>(); @Nullable private Alert developersAlert; @Nullable private Alert displayedAlert; @Nullable private Filter developersFilter; @Nullable private Arbitrator registeredArbitrator; @Nullable private Mediator registeredMediator; @Nullable private List acceptedArbitrators = new ArrayList<>(); @Nullable private List acceptedMediators = new ArrayList<>(); @Nullable private PriceAlertFilter priceAlertFilter; @Nullable private List marketAlertFilters = new ArrayList<>(); // Added v1.2.0 @Nullable private RefundAgent registeredRefundAgent; @Nullable private List acceptedRefundAgents = new ArrayList<>(); // Added at 1.5.3 // Generic map for persisting various UI states. We keep values un-typed as string to // provide sufficient flexibility. private Cookie cookie = new Cookie(); private long walletCreationDate; public UserPayload() { } @Override public protobuf.PersistableEnvelope toProtoMessage() { protobuf.UserPayload.Builder builder = protobuf.UserPayload.newBuilder(); Optional.ofNullable(accountId).ifPresent(e -> builder.setAccountId(accountId)); Optional.ofNullable(paymentAccounts) .ifPresent(e -> builder.addAllPaymentAccounts(ProtoUtil.collectionToProto(paymentAccounts, protobuf.PaymentAccount.class))); Optional.ofNullable(currentPaymentAccount) .ifPresent(e -> builder.setCurrentPaymentAccount(currentPaymentAccount.toProtoMessage())); Optional.ofNullable(acceptedLanguageLocaleCodes) .ifPresent(e -> builder.addAllAcceptedLanguageLocaleCodes(acceptedLanguageLocaleCodes)); Optional.ofNullable(developersAlert) .ifPresent(developersAlert -> builder.setDevelopersAlert(developersAlert.toProtoMessage().getAlert())); Optional.ofNullable(displayedAlert) .ifPresent(displayedAlert -> builder.setDisplayedAlert(displayedAlert.toProtoMessage().getAlert())); Optional.ofNullable(developersFilter) .ifPresent(developersFilter -> builder.setDevelopersFilter(developersFilter.toProtoMessage().getFilter())); Optional.ofNullable(registeredArbitrator) .ifPresent(registeredArbitrator -> builder.setRegisteredArbitrator(registeredArbitrator.toProtoMessage().getArbitrator())); Optional.ofNullable(registeredMediator) .ifPresent(registeredMediator -> builder.setRegisteredMediator(registeredMediator.toProtoMessage().getMediator())); Optional.ofNullable(acceptedArbitrators) .ifPresent(e -> builder.addAllAcceptedArbitrators(ProtoUtil.collectionToProto(acceptedArbitrators, message -> ((protobuf.StoragePayload) message).getArbitrator()))); Optional.ofNullable(acceptedMediators) .ifPresent(e -> builder.addAllAcceptedMediators(ProtoUtil.collectionToProto(acceptedMediators, message -> ((protobuf.StoragePayload) message).getMediator()))); Optional.ofNullable(priceAlertFilter).ifPresent(priceAlertFilter -> builder.setPriceAlertFilter(priceAlertFilter.toProtoMessage())); Optional.ofNullable(marketAlertFilters) .ifPresent(e -> builder.addAllMarketAlertFilters(ProtoUtil.collectionToProto(marketAlertFilters, protobuf.MarketAlertFilter.class))); Optional.ofNullable(registeredRefundAgent) .ifPresent(registeredRefundAgent -> builder.setRegisteredRefundAgent(registeredRefundAgent.toProtoMessage().getRefundAgent())); Optional.ofNullable(acceptedRefundAgents) .ifPresent(e -> builder.addAllAcceptedRefundAgents(ProtoUtil.collectionToProto(acceptedRefundAgents, message -> ((protobuf.StoragePayload) message).getRefundAgent()))); Optional.ofNullable(cookie).ifPresent(e -> builder.putAllCookie(cookie.toProtoMessage())); builder.setWalletCreationDate(walletCreationDate); return protobuf.PersistableEnvelope.newBuilder().setUserPayload(builder).build(); } public static UserPayload fromProto(protobuf.UserPayload proto, CoreProtoResolver coreProtoResolver) { return new UserPayload( ProtoUtil.stringOrNullFromProto(proto.getAccountId()), proto.getPaymentAccountsList().isEmpty() ? new HashSet<>() : new HashSet<>(proto.getPaymentAccountsList().stream() .map(e -> PaymentAccount.fromProto(e, coreProtoResolver)) .filter(Objects::nonNull) .collect(Collectors.toSet())), proto.hasCurrentPaymentAccount() ? PaymentAccount.fromProto(proto.getCurrentPaymentAccount(), coreProtoResolver) : null, proto.getAcceptedLanguageLocaleCodesList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getAcceptedLanguageLocaleCodesList()), proto.hasDevelopersAlert() ? Alert.fromProto(proto.getDevelopersAlert()) : null, proto.hasDisplayedAlert() ? Alert.fromProto(proto.getDisplayedAlert()) : null, proto.hasDevelopersFilter() ? Filter.fromProto(proto.getDevelopersFilter()) : null, proto.hasRegisteredArbitrator() ? Arbitrator.fromProto(proto.getRegisteredArbitrator()) : null, proto.hasRegisteredMediator() ? Mediator.fromProto(proto.getRegisteredMediator()) : null, proto.getAcceptedArbitratorsList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getAcceptedArbitratorsList().stream() .map(Arbitrator::fromProto) .collect(Collectors.toList())), proto.getAcceptedMediatorsList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getAcceptedMediatorsList().stream() .map(Mediator::fromProto) .collect(Collectors.toList())), PriceAlertFilter.fromProto(proto.getPriceAlertFilter()), proto.getMarketAlertFiltersList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getMarketAlertFiltersList().stream() .map(e -> MarketAlertFilter.fromProto(e, coreProtoResolver)) .collect(Collectors.toSet())), proto.hasRegisteredRefundAgent() ? RefundAgent.fromProto(proto.getRegisteredRefundAgent()) : null, proto.getAcceptedRefundAgentsList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getAcceptedRefundAgentsList().stream() .map(RefundAgent::fromProto) .collect(Collectors.toList())), Cookie.fromProto(proto.getCookieMap()), proto.getWalletCreationDate() ); } } ================================================ FILE: core/src/main/java/haveno/core/util/AveragePriceUtil.java ================================================ /* * 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 . */ package haveno.core.util; import haveno.common.util.MathUtils; import haveno.common.util.Tuple2; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.Price; import haveno.core.monetary.TraditionalMoney; import haveno.core.trade.statistics.TradeStatistics3; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.Preferences; import java.util.ArrayList; import java.util.Calendar; import java.util.Comparator; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; import java.util.stream.Collectors; public class AveragePriceUtil { private static final double HOW_MANY_STD_DEVS_CONSTITUTE_OUTLIER = 10; public static Tuple2 getAveragePriceTuple(Preferences preferences, TradeStatisticsManager tradeStatisticsManager, int days) { double percentToTrim = Math.max(0, Math.min(49, preferences.getBsqAverageTrimThreshold() * 100)); Date pastXDays = getPastDate(days); List bsqAllTradePastXDays; // TODO: remove BSQ List usdAllTradePastXDays; synchronized (tradeStatisticsManager.getObservableTradeStatisticsList()) { bsqAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsList().stream() .filter(e -> e.getCurrency().equals("BSQ")) .filter(e -> e.getDate().after(pastXDays)) .collect(Collectors.toList()); usdAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsList().stream() .filter(e -> e.getCurrency().equals("USD")) .filter(e -> e.getDate().after(pastXDays)) .collect(Collectors.toList()); } List bsqTradePastXDays = percentToTrim > 0 ? removeOutliers(bsqAllTradePastXDays, percentToTrim) : bsqAllTradePastXDays; List usdTradePastXDays = percentToTrim > 0 ? removeOutliers(usdAllTradePastXDays, percentToTrim) : usdAllTradePastXDays; Price usdPrice = Price.valueOf("USD", getUSDAverage(bsqTradePastXDays, usdTradePastXDays)); Price bsqPrice = Price.valueOf("BSQ", getBTCAverage(bsqTradePastXDays)); return new Tuple2<>(usdPrice, bsqPrice); } private static List removeOutliers(List list, double percentToTrim) { List yValues = list.stream() .filter(TradeStatistics3::isValid) .map(e -> (double) e.getNormalizedPrice()) .collect(Collectors.toList()); Tuple2 tuple = InlierUtil.findInlierRange(yValues, percentToTrim, HOW_MANY_STD_DEVS_CONSTITUTE_OUTLIER); double lowerBound = tuple.first; double upperBound = tuple.second; return list.stream() .filter(e -> e.getNormalizedPrice() > lowerBound) .filter(e -> e.getNormalizedPrice() < upperBound) .collect(Collectors.toList()); } private static long getBTCAverage(List list) { long accumulatedVolume = 0; long accumulatedAmount = 0; for (TradeStatistics3 item : list) { accumulatedVolume += item.getTradeVolume().getValue(); accumulatedAmount += item.getTradeAmount().longValueExact(); // Amount of XMR traded } long averagePrice; double accumulatedAmountAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedAmount, CryptoMoney.SMALLEST_UNIT_EXPONENT); averagePrice = accumulatedVolume > 0 ? MathUtils.roundDoubleToLong(accumulatedAmountAsDouble / accumulatedVolume) : 0; return averagePrice; } private static long getUSDAverage(List bsqList, List usdList) { // Use next USD/BTC print as price to calculate BSQ/USD rate // Store each trade as amount of USD and amount of BSQ traded List> usdBsqList = new ArrayList<>(bsqList.size()); usdList.sort(Comparator.comparing(TradeStatistics3::getDateAsLong)); var usdBTCPrice = 10000d; // Default to 10000 USD per BTC if there is no USD feed at all for (TradeStatistics3 item : bsqList) { // Find usdprice for trade item usdBTCPrice = usdList.stream() .filter(usd -> usd.getDateAsLong() > item.getDateAsLong()) .map(usd -> MathUtils.scaleDownByPowerOf10((double) usd.getTradePrice().getValue(), TraditionalMoney.SMALLEST_UNIT_EXPONENT)) .findFirst() .orElse(usdBTCPrice); var bsqAmount = MathUtils.scaleDownByPowerOf10((double) item.getTradeVolume().getValue(), CryptoMoney.SMALLEST_UNIT_EXPONENT); var btcAmount = MathUtils.scaleDownByPowerOf10((double) item.getTradeAmount().longValueExact(), CryptoMoney.SMALLEST_UNIT_EXPONENT); usdBsqList.add(new Tuple2<>(usdBTCPrice * btcAmount, bsqAmount)); } long averagePrice; var usdTraded = usdBsqList.stream() .mapToDouble(item -> item.first) .sum(); var bsqTraded = usdBsqList.stream() .mapToDouble(item -> item.second) .sum(); var averageAsDouble = bsqTraded > 0 ? usdTraded / bsqTraded : 0d; var averageScaledUp = MathUtils.scaleUpByPowerOf10(averageAsDouble, TraditionalMoney.SMALLEST_UNIT_EXPONENT); averagePrice = bsqTraded > 0 ? MathUtils.roundDoubleToLong(averageScaledUp) : 0; return averagePrice; } private static Date getPastDate(int days) { Calendar cal = new GregorianCalendar(); cal.setTime(new Date()); cal.add(Calendar.DAY_OF_MONTH, -1 * days); return cal.getTime(); } } ================================================ FILE: core/src/main/java/haveno/core/util/FormattingUtils.java ================================================ package haveno.core.util; import haveno.common.app.Version; import haveno.common.util.MathUtils; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.GlobalSettings; import haveno.core.locale.Res; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.Price; import haveno.core.monetary.TraditionalMoney; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DurationFormatUtils; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Monetary; import org.bitcoinj.utils.MonetaryFormat; import org.jetbrains.annotations.NotNull; import java.text.DateFormat; import java.text.DecimalFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.TimeZone; import java.util.concurrent.TimeUnit; @Slf4j public class FormattingUtils { public static final String BTC_FORMATTER_KEY = "BTC"; public final static String RANGE_SEPARATOR = " - "; private static final MonetaryFormat priceFormat4Decimals = new MonetaryFormat().shift(0).minDecimals(4).repeatOptionalDecimals(0, 0); private static final MonetaryFormat priceFormat8Decimals = new MonetaryFormat().shift(0).minDecimals(8).repeatOptionalDecimals(0, 0); private static final MonetaryFormat cryptoFormat = new MonetaryFormat().shift(0).minDecimals(CryptoMoney.SMALLEST_UNIT_EXPONENT).repeatOptionalDecimals(0, 0); private static final DecimalFormat decimalFormat = new DecimalFormat("#.#"); public static String formatVersion() { return Res.get("mainView.footer.version", Version.VERSION); } public static String formatCoinWithCode(long value, MonetaryFormat coinFormat) { return formatCoinWithCode(Coin.valueOf(value), coinFormat); } public static String formatCoinWithCode(Coin coin, MonetaryFormat coinFormat) { if (coin != null) { try { // we don't use the code feature from coinFormat as it does automatic switching between mBTC and BTC and // pre and post fixing return coinFormat.postfixCode().format(coin).toString(); } catch (Throwable t) { log.warn("Exception at formatCoinWithCode: " + t.toString()); return ""; } } else { return ""; } } public static String formatCoin(long value, MonetaryFormat coinFormat) { return formatCoin(Coin.valueOf(value), -1, false, 0, coinFormat); } public static String formatCoin(Coin coin, int decimalPlaces, boolean decimalAligned, int maxNumberOfDigits, MonetaryFormat coinFormat) { String formattedCoin = ""; if (coin != null) { try { if (decimalPlaces < 0 || decimalPlaces > 4) { formattedCoin = coinFormat.noCode().format(coin).toString(); } else { formattedCoin = coinFormat.noCode().minDecimals(decimalPlaces).repeatOptionalDecimals(1, decimalPlaces).format(coin).toString(); } } catch (Throwable t) { log.warn("Exception at formatBtc: " + t.toString()); } } if (decimalAligned) { formattedCoin = fillUpPlacesWithEmptyStrings(formattedCoin, maxNumberOfDigits); } return formattedCoin; } public static String formatTraditionalMoney(TraditionalMoney traditionalMoney, MonetaryFormat format, boolean appendCurrencyCode) { if (traditionalMoney != null) { try { final String res = format.noCode().format(traditionalMoney).toString(); if (appendCurrencyCode) return res + " " + traditionalMoney.getCurrencyCode(); else return res; } catch (Throwable t) { log.warn("Exception at formatTraditionalMoneyWithCode: " + t.toString()); return Res.get("shared.na") + " " + traditionalMoney.getCurrencyCode(); } } else { return Res.get("shared.na"); } } private static String formatCrypto(CryptoMoney crypto, boolean appendCurrencyCode) { if (crypto != null) { try { String res = cryptoFormat.noCode().format(crypto).toString(); if (appendCurrencyCode) return res + " " + crypto.getCurrencyCode(); else return res; } catch (Throwable t) { log.warn("Exception at formatCrypto: " + t.toString()); return Res.get("shared.na") + " " + crypto.getCurrencyCode(); } } else { return Res.get("shared.na"); } } public static String formatCryptoVolume(CryptoMoney crypto, boolean appendCurrencyCode) { if (crypto != null) { try { // TODO quick hack... String res; if (crypto.getCurrencyCode().equals("BSQ")) res = cryptoFormat.noCode().minDecimals(2).repeatOptionalDecimals(0, 0).format(crypto).toString(); else res = cryptoFormat.noCode().format(crypto).toString(); if (appendCurrencyCode) return res + " " + crypto.getCurrencyCode(); else return res; } catch (Throwable t) { log.warn("Exception at formatCryptoVolume: " + t.toString()); return Res.get("shared.na") + " " + crypto.getCurrencyCode(); } } else { return Res.get("shared.na"); } } public static String formatPrice(Price price, MonetaryFormat priceFormat, boolean appendCurrencyCode) { if (price != null) { Monetary monetary = price.getMonetary(); if (monetary instanceof TraditionalMoney) return formatTraditionalMoney((TraditionalMoney) monetary, priceFormat, appendCurrencyCode); else return formatCrypto((CryptoMoney) monetary, appendCurrencyCode); } else { return Res.get("shared.na"); } } public static String formatPrice(Price price, boolean appendCurrencyCode) { return formatPrice(price, getPriceMonetaryFormat(price.getCurrencyCode()), appendCurrencyCode); } public static String formatPrice(Price price) { return formatPrice(price, price == null ? null : getPriceMonetaryFormat(price.getCurrencyCode()), false); } public static String formatMarketPrice(double price, String currencyCode) { if (CurrencyUtil.isTraditionalCurrency(currencyCode)) return formatMarketPrice(price, 2); else return formatMarketPrice(price, 8); } public static String formatMarketPrice(double price, int precision) { return formatRoundedDoubleWithPrecision(price, precision); } public static String formatRoundedDoubleWithPrecision(double value, int precision) { decimalFormat.setMinimumFractionDigits(precision); decimalFormat.setMaximumFractionDigits(precision); return decimalFormat.format(MathUtils.roundDouble(value, precision)).replace(",", "."); } public static String formatDateTime(Date date, boolean useLocaleAndLocalTimezone) { Locale locale = useLocaleAndLocalTimezone ? GlobalSettings.getLocale() : Locale.US; DateFormat dateInstance = DateFormat.getDateInstance(DateFormat.DEFAULT, locale); DateFormat timeInstance = DateFormat.getTimeInstance(DateFormat.DEFAULT, locale); if (!useLocaleAndLocalTimezone) { dateInstance.setTimeZone(TimeZone.getTimeZone("UTC")); timeInstance.setTimeZone(TimeZone.getTimeZone("UTC")); } return formatDateTime(date, dateInstance, timeInstance); } public static String formatDateTime(Date date, DateFormat dateFormatter, DateFormat timeFormatter) { if (date != null) { return dateFormatter.format(date) + " " + timeFormatter.format(date); } else { return ""; } } public static String getDateFromBlockHeight(long blockHeight) { long now = new Date().getTime(); SimpleDateFormat dateFormatter = new SimpleDateFormat("dd MMM", Locale.getDefault()); SimpleDateFormat timeFormatter = new SimpleDateFormat("HH:mm", Locale.getDefault()); return formatDateTime(new Date(now + blockHeight * 10 * 60 * 1000L), dateFormatter, timeFormatter); } public static String formatToPercentWithSymbol(double value) { return formatToPercent(value) + "%"; } public static String formatToRoundedPercentWithSymbol(double value) { return formatToPercent(value, new DecimalFormat("#")) + "%"; } public static String formatToClampedRoundedPercentWithSymbol(double value) { return formatToPercent(clampPercentTo99(value), new DecimalFormat("#")) + "%"; } private static double clampPercentTo99(double value) { return value >= 1.0 ? 1.0 : Math.min(value, 0.99); } public static String formatPercentagePrice(double value) { return formatToPercentWithSymbol(value); } public static String formatToPercent(double value) { DecimalFormat decimalFormat = new DecimalFormat("#.##"); decimalFormat.setMinimumFractionDigits(2); decimalFormat.setMaximumFractionDigits(2); return formatToPercent(value, decimalFormat); } public static String formatToPercent(double value, DecimalFormat decimalFormat) { return decimalFormat.format(MathUtils.roundDouble(value * 100.0, 2)).replace(",", "."); } public static String formatDurationAsWords(long durationMillis) { return formatDurationAsWords(durationMillis, false, true); } public static String formatDurationAsWords(long durationMillis, boolean showSeconds, boolean showZeroValues) { String format = ""; String second = Res.get("time.second"); String minute = Res.get("time.minute"); String hour = Res.get("time.hour").toLowerCase(); String day = Res.get("time.day").toLowerCase(); String days = Res.get("time.days"); String hours = Res.get("time.hours"); String minutes = Res.get("time.minutes"); String seconds = Res.get("time.seconds"); if (durationMillis >= TimeUnit.DAYS.toMillis(1)) { format = "d\' " + days + ", \'"; } if (showSeconds) { format += "H\' " + hours + ", \'m\' " + minutes + ", \'s\' " + seconds + "\'"; } else { format += "H\' " + hours + ", \'m\' " + minutes + "\'"; } String duration = durationMillis > 0 ? DurationFormatUtils.formatDuration(durationMillis, format) : ""; duration = StringUtils.replacePattern(duration, "^1 " + seconds + "|\\b1 " + seconds, "1 " + second); duration = StringUtils.replacePattern(duration, "^1 " + minutes + "|\\b1 " + minutes, "1 " + minute); duration = StringUtils.replacePattern(duration, "^1 " + hours + "|\\b1 " + hours, "1 " + hour); duration = StringUtils.replacePattern(duration, "^1 " + days + "|\\b1 " + days, "1 " + day); if (!showZeroValues) { duration = duration.replace(", 0 seconds", ""); duration = duration.replace(", 0 minutes", ""); duration = duration.replace(", 0 hours", ""); duration = StringUtils.replacePattern(duration, "^0 days, ", ""); duration = StringUtils.replacePattern(duration, "^0 hours, ", ""); duration = StringUtils.replacePattern(duration, "^0 minutes, ", ""); duration = StringUtils.replacePattern(duration, "^0 seconds, ", ""); } return duration.trim(); } public static String formatBytes(long bytes) { double kb = 1024; double mb = kb * kb; DecimalFormat decimalFormat = new DecimalFormat("#.##"); if (bytes < kb) return bytes + " bytes"; else if (bytes < mb) return decimalFormat.format(bytes / kb) + " KB"; else return decimalFormat.format(bytes / mb) + " MB"; } @NotNull public static String fillUpPlacesWithEmptyStrings(String formattedNumber, @SuppressWarnings("unused") int maxNumberOfDigits) { //FIXME: temporary deactivate adding spaces in front of numbers as we don't use a monospace font right now. /*int numberOfPlacesToFill = maxNumberOfDigits - formattedNumber.length(); for (int i = 0; i < numberOfPlacesToFill; i++) { formattedNumber = " " + formattedNumber; }*/ return formattedNumber; } public static MonetaryFormat getPriceMonetaryFormat(String currencyCode) { return CurrencyUtil.isPricePrecise(currencyCode) ? priceFormat8Decimals : priceFormat4Decimals; } } ================================================ FILE: core/src/main/java/haveno/core/util/GenerateKeyPairs.java ================================================ package haveno.core.util; import java.math.BigInteger; import java.util.ArrayList; import java.util.List; import javax.crypto.SecretKey; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.Utils; import haveno.common.crypto.Encryption; /** * This utility generates and prints public/private key-pairs * which can be used to register arbitrators on the network. */ public class GenerateKeyPairs { public static void main(String[] args) { // generate public/private key-pairs List secretKeys = new ArrayList<>(); for (int i = 0; i < 20; i++) { secretKeys.add(Encryption.generateSecretKey(256)); } // print key-pairs System.out.println("Private keys:"); for (SecretKey sk : secretKeys) { String privateKey = Utils.HEX.encode(sk.getEncoded()); System.out.println(privateKey); } System.out.println("Corresponding public keys:"); for (SecretKey sk : secretKeys) { String privateKey = Utils.HEX.encode(sk.getEncoded()); ECKey ecKey = ECKey.fromPrivate(new BigInteger(1, Utils.HEX.decode(privateKey))); String pubKey = Utils.HEX.encode(ecKey.getPubKey()); System.out.println(pubKey); } } } ================================================ FILE: core/src/main/java/haveno/core/util/InlierUtil.java ================================================ /* * 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 . */ package haveno.core.util; import haveno.common.util.DoubleSummaryStatisticsWithStdDev; import haveno.common.util.Tuple2; import javafx.collections.FXCollections; import java.util.DoubleSummaryStatistics; import java.util.List; import java.util.stream.Collectors; public class InlierUtil { /* Finds the minimum and maximum inlier values. The returned values may be NaN. * See `computeInlierThreshold` for the definition of inlier. */ public static Tuple2 findInlierRange( List yValues, double percentToTrim, double howManyStdDevsConstituteOutlier ) { Tuple2 inlierThreshold = computeInlierThreshold(yValues, percentToTrim, howManyStdDevsConstituteOutlier); DoubleSummaryStatistics inlierStatistics = yValues .stream() .filter(y -> withinBounds(inlierThreshold, y)) .mapToDouble(Double::doubleValue) .summaryStatistics(); var inlierMin = inlierStatistics.getMin(); var inlierMax = inlierStatistics.getMax(); return new Tuple2<>(inlierMin, inlierMax); } private static boolean withinBounds(Tuple2 bounds, double number) { var lowerBound = bounds.first; var upperBound = bounds.second; return (lowerBound <= number) && (number <= upperBound); } /* Computes the lower and upper inlier thresholds. A point lying outside * these thresholds is considered an outlier, and a point lying within * is considered an inlier. * The thresholds are found by trimming the dataset (see method `trim`), * then adding or subtracting a multiple of its (trimmed) standard * deviation from its (trimmed) mean. */ private static Tuple2 computeInlierThreshold( List numbers, double percentToTrim, double howManyStdDevsConstituteOutlier ) { if (howManyStdDevsConstituteOutlier <= 0) { throw new IllegalArgumentException( "howManyStdDevsConstituteOutlier should be a positive number"); } List trimmed = trim(percentToTrim, numbers); DoubleSummaryStatisticsWithStdDev summaryStatistics = trimmed.stream() .collect( DoubleSummaryStatisticsWithStdDev::new, DoubleSummaryStatisticsWithStdDev::accept, DoubleSummaryStatisticsWithStdDev::combine); double mean = summaryStatistics.getAverage(); double stdDev = summaryStatistics.getStandardDeviation(); var inlierLowerThreshold = mean - (stdDev * howManyStdDevsConstituteOutlier); var inlierUpperThreshold = mean + (stdDev * howManyStdDevsConstituteOutlier); return new Tuple2<>(inlierLowerThreshold, inlierUpperThreshold); } /* Sorts the data and discards given percentage from the left and right sides each. * E.g. 5% trim implies a total of 10% (2x 5%) of elements discarded. * Used in calculating trimmed mean (and in turn trimmed standard deviation), * which is more robust to outliers than a simple mean. */ private static List trim(double percentToTrim, List numbers) { var minPercentToTrim = 0; var maxPercentToTrim = 50; if (minPercentToTrim > percentToTrim || percentToTrim > maxPercentToTrim) { throw new IllegalArgumentException( String.format( "The percentage of data points to trim must be in the range [%d,%d].", minPercentToTrim, maxPercentToTrim)); } var totalPercentTrim = percentToTrim * 2; if (totalPercentTrim == 0) { return numbers; } if (totalPercentTrim == 100) { return FXCollections.emptyObservableList(); } if (numbers.isEmpty()) { return numbers; } var count = numbers.size(); int countToDropFromEachSide = (int) Math.round((count / 100d) * percentToTrim); // visada >= 0? if (countToDropFromEachSide == 0) { return numbers; } var sorted = numbers.stream().sorted(); var oneSideTrimmed = sorted.skip(countToDropFromEachSide); // Here, having already trimmed the left-side, we are implicitly trimming // the right-side by specifying a limit to the stream's length. // An explicit right-side drop/trim/skip is not supported by the Stream API. var countAfterTrim = count - (countToDropFromEachSide * 2); // visada > 0? ir <= count? var bothSidesTrimmed = oneSideTrimmed.limit(countAfterTrim); return bothSidesTrimmed.collect(Collectors.toList()); } } ================================================ FILE: core/src/main/java/haveno/core/util/JsonUtil.java ================================================ /* * 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 . */ package haveno.core.util; import com.google.gson.ExclusionStrategy; import com.google.gson.FieldAttributes; import com.google.gson.GsonBuilder; import haveno.common.util.JsonExclude; import haveno.core.offer.OfferPayload; import haveno.core.trade.Contract; public class JsonUtil { public static String objectToJson(Object object) { GsonBuilder gsonBuilder = new GsonBuilder() .setExclusionStrategies(new AnnotationExclusionStrategy()) .setPrettyPrinting(); if (object instanceof Contract || object instanceof OfferPayload) { gsonBuilder.registerTypeAdapter(OfferPayload.class, new OfferPayload.JsonSerializer()); } return gsonBuilder.create().toJson(object); } private static class AnnotationExclusionStrategy implements ExclusionStrategy { @Override public boolean shouldSkipField(FieldAttributes f) { return f.getAnnotation(JsonExclude.class) != null; } @Override public boolean shouldSkipClass(Class clazz) { return false; } } } ================================================ FILE: core/src/main/java/haveno/core/util/ParsingUtils.java ================================================ package haveno.core.util; import haveno.common.util.MathUtils; import haveno.core.monetary.Price; import haveno.core.util.coin.CoinFormatter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.bitcoinj.core.Coin; import org.bitcoinj.utils.MonetaryFormat; @Slf4j public class ParsingUtils { public static Coin parseToCoin(String input, CoinFormatter coinFormatter) { return parseToCoin(input, coinFormatter.getMonetaryFormat()); } public static Coin parseToCoin(String input, MonetaryFormat coinFormat) { if (input != null && input.length() > 0) { try { return coinFormat.parse(cleanDoubleInput(input)); } catch (Throwable t) { log.warn("Exception at parseToBtc: " + t.toString()); return Coin.ZERO; } } else { return Coin.ZERO; } } public static double parseNumberStringToDouble(String input) throws NumberFormatException { return Double.parseDouble(cleanDoubleInput(input)); } public static double parsePercentStringToDouble(String percentString) throws NumberFormatException { String input = percentString.replace("%", ""); input = cleanDoubleInput(input); double value = Double.parseDouble(input); return MathUtils.roundDouble(value / 100d, 4); } public static long parsePriceStringToLong(String currencyCode, String amount, int precision) { if (amount == null || amount.isEmpty()) return 0; long value = 0; try { double amountValue = Double.parseDouble(amount); amount = FormattingUtils.formatRoundedDoubleWithPrecision(amountValue, precision); value = Price.parse(currencyCode, amount).getValue(); } catch (NumberFormatException ignore) { // expected NumberFormatException if input is not a number } catch (Throwable t) { log.error("parsePriceStringToLong: " + t.toString()); } return value; } public static String convertCharsForNumber(String input) { // Some languages like Finnish use the long dash for the minus input = input.replace("−", "-"); input = StringUtils.deleteWhitespace(input); return input.replace(",", "."); } public static String cleanDoubleInput(String input) { input = convertCharsForNumber(input); if (input.equals(".")) input = input.replace(".", "0."); if (input.equals("-.")) input = input.replace("-.", "-0."); // don't use String.valueOf(Double.parseDouble(input)) as return value as it gives scientific // notation (1.0E-6) which screw up coinFormat.parse //noinspection ResultOfMethodCallIgnored // Just called to check if we have a valid double, throws exception otherwise //noinspection ResultOfMethodCallIgnored Double.parseDouble(input); return input; } } ================================================ FILE: core/src/main/java/haveno/core/util/PriceUtil.java ================================================ /* * 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 . */ package haveno.core.util; import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.util.MathUtils; import haveno.core.locale.CurrencyUtil; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.Price; import haveno.core.monetary.TraditionalMoney; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.provider.price.MarketPrice; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.Preferences; import haveno.core.util.validation.AmountValidator4Decimals; import haveno.core.util.validation.AmountValidator8Decimals; import haveno.core.util.validation.InputValidator; import haveno.core.util.validation.MonetaryValidator; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Optional; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton public class PriceUtil { private final PriceFeedService priceFeedService; @Inject public PriceUtil(PriceFeedService priceFeedService, TradeStatisticsManager tradeStatisticsManager, Preferences preferences) { this.priceFeedService = priceFeedService; } public static MonetaryValidator getPriceValidator(String currencyCode) { return CurrencyUtil.isPricePrecise(currencyCode) ? new AmountValidator4Decimals() : new AmountValidator8Decimals(); } public static InputValidator.ValidationResult isTriggerPriceValid(String triggerPriceAsString, MarketPrice marketPrice, boolean isSellOffer, String currencyCode) { if (triggerPriceAsString == null || triggerPriceAsString.isEmpty()) { return new InputValidator.ValidationResult(true); } InputValidator.ValidationResult result = getPriceValidator(currencyCode).validate(triggerPriceAsString); if (!result.isValid) { return result; } return new InputValidator.ValidationResult(true); } public static Price marketPriceToPrice(MarketPrice marketPrice) { String currencyCode = marketPrice.getCurrencyCode(); double priceAsDouble = marketPrice.getPrice(); int precision = CurrencyUtil.isTraditionalCurrency(currencyCode) ? TraditionalMoney.SMALLEST_UNIT_EXPONENT : CryptoMoney.SMALLEST_UNIT_EXPONENT; double scaled = MathUtils.scaleUpByPowerOf10(priceAsDouble, precision); long roundedToLong = MathUtils.roundDoubleToLong(scaled); return Price.valueOf(currencyCode, roundedToLong); } public boolean hasMarketPrice(Offer offer) { String currencyCode = offer.getCounterCurrencyCode(); checkNotNull(priceFeedService, "priceFeed must not be null"); MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); Price price = offer.getPrice(); return price != null && marketPrice != null && marketPrice.isRecentExternalPriceAvailable(); } public Optional getMarketBasedPrice(Offer offer, OfferDirection direction) { if (offer.isUseMarketBasedPrice()) { return Optional.of(offer.getMarketPriceMarginPct()); } if (!hasMarketPrice(offer)) { log.trace("We don't have a market price. " + "That case could only happen if you don't have a price feed."); return Optional.empty(); } String currencyCode = offer.getCounterCurrencyCode(); checkNotNull(priceFeedService, "priceFeed must not be null"); MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); double marketPriceAsDouble = checkNotNull(marketPrice).getPrice(); return calculatePercentage(offer, marketPriceAsDouble, direction); } public static Optional calculatePercentage(Offer offer, double marketPrice, OfferDirection direction) { // If the offer did not use % price we calculate % from current market price String currencyCode = offer.getCounterCurrencyCode(); Price price = offer.getPrice(); int precision = CurrencyUtil.isTraditionalCurrency(currencyCode) ? TraditionalMoney.SMALLEST_UNIT_EXPONENT : CryptoMoney.SMALLEST_UNIT_EXPONENT; long priceAsLong = checkNotNull(price).getValue(); double scaled = MathUtils.scaleDownByPowerOf10(priceAsLong, precision); double value; if (direction == OfferDirection.SELL) { if (marketPrice == 0) { return Optional.empty(); } value = 1 - scaled / marketPrice; } else { if (marketPrice == 1) { return Optional.empty(); } value = scaled / marketPrice - 1; } return Optional.of(value); } public static long getMarketPriceAsLong(String inputValue, String currencyCode) { if (inputValue == null || inputValue.isEmpty() || currencyCode == null) { return 0; } try { int precision = getMarketPricePrecision(currencyCode); String stringValue = reformatMarketPrice(inputValue, currencyCode); return ParsingUtils.parsePriceStringToLong(currencyCode, stringValue, precision); } catch (Throwable t) { return 0; } } public static String reformatMarketPrice(String inputValue, String currencyCode) { if (inputValue == null || inputValue.isEmpty() || currencyCode == null) { return ""; } double priceAsDouble = ParsingUtils.parseNumberStringToDouble(inputValue); int precision = getMarketPricePrecision(currencyCode); return FormattingUtils.formatRoundedDoubleWithPrecision(priceAsDouble, precision); } public static String formatMarketPrice(long priceAsLong, String currencyCode) { Price price = Price.valueOf(currencyCode, priceAsLong); return FormattingUtils.formatPrice(price); } public static int getMarketPricePrecision(String currencyCode) { return CurrencyUtil.isTraditionalCurrency(currencyCode) ? TraditionalMoney.SMALLEST_UNIT_EXPONENT : CryptoMoney.SMALLEST_UNIT_EXPONENT; } public static long invertLongPrice(long price, String currencyCode) { if (price == 0) return 0; int precision = CurrencyUtil.isTraditionalCurrency(currencyCode) ? TraditionalMoney.SMALLEST_UNIT_EXPONENT : CryptoMoney.SMALLEST_UNIT_EXPONENT; double priceDouble = MathUtils.scaleDownByPowerOf10(price, precision); double priceDoubleInverted = BigDecimal.ONE.divide(BigDecimal.valueOf(priceDouble), precision, RoundingMode.HALF_UP).doubleValue(); double scaled = MathUtils.scaleUpByPowerOf10(priceDoubleInverted, precision); return MathUtils.roundDoubleToLong(scaled); } } ================================================ FILE: core/src/main/java/haveno/core/util/SimpleMarkdownParser.java ================================================ /* * 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 . */ package haveno.core.util; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.ArrayList; import java.util.List; public class SimpleMarkdownParser { private enum MarkdownParsingState { TEXT, LINK_TEXT, LINK_HREF } // Simple parser without correctness validation, currently supports only links public static List parse(String markdown) { List items = new ArrayList<>(); StringBuilder sb = new StringBuilder(); StringBuilder sb2 = new StringBuilder(); MarkdownParsingState state = MarkdownParsingState.TEXT; for (int i = 0; i < markdown.length(); i++) { char c = markdown.charAt(i); if (c == '[') { if (sb.length() > 0) { items.add(new TextNode(sb.toString())); sb = new StringBuilder(); } state = MarkdownParsingState.LINK_TEXT; } else if (c == '(' && state == MarkdownParsingState.LINK_TEXT) { state = MarkdownParsingState.LINK_HREF; } else if (c == ')' && state == MarkdownParsingState.LINK_HREF) { state = MarkdownParsingState.TEXT; items.add(new HyperlinkNode(sb.toString(), sb2.toString())); sb = new StringBuilder(); sb2 = new StringBuilder(); } else if (c != ']') { if (state == MarkdownParsingState.LINK_HREF) { sb2.append(c); } else { sb.append(c); } } } if (sb.length() > 0) { items.add(new TextNode(sb.toString())); } return items; } public static class MarkdownNode {} @AllArgsConstructor public static class HyperlinkNode extends MarkdownNode { @Getter private final String text; @Getter private final String href; } @AllArgsConstructor public static class TextNode extends MarkdownNode { @Getter private final String text; } } ================================================ FILE: core/src/main/java/haveno/core/util/Validator.java ================================================ /* * 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 . */ package haveno.core.util; import haveno.core.trade.messages.TradeMessage; import org.bitcoinj.core.Coin; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; /** * Utility class for validating domain data. */ public class Validator { public static String nonEmptyStringOf(String value) { checkNotNull(value); checkArgument(value.length() > 0); return value; } public static long nonNegativeLongOf(long value) { checkArgument(value >= 0); return value; } public static Coin nonZeroCoinOf(Coin value) { checkNotNull(value); checkArgument(!value.isZero()); return value; } public static Coin positiveCoinOf(Coin value) { checkNotNull(value); checkArgument(value.isPositive()); return value; } public static void checkTradeId(String tradeId, TradeMessage tradeMessage) { checkArgument(isTradeIdValid(tradeId, tradeMessage)); } public static boolean isTradeIdValid(String tradeId, TradeMessage tradeMessage) { return tradeId.equals(tradeMessage.getOfferId()); } } ================================================ FILE: core/src/main/java/haveno/core/util/VolumeUtil.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.util; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.CryptoExchangeRate; import haveno.core.monetary.Price; import haveno.core.monetary.TraditionalMoney; import haveno.core.monetary.TraditionalExchangeRate; import haveno.core.monetary.Volume; import haveno.core.offer.Offer; import haveno.core.payment.payload.PaymentMethod; import haveno.core.trade.HavenoUtils; import org.bitcoinj.core.Monetary; import org.bitcoinj.utils.MonetaryFormat; import java.math.BigInteger; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.Collection; import java.util.Locale; public class VolumeUtil { private static final MonetaryFormat VOLUME_FORMAT_UNIT = new MonetaryFormat().shift(0).minDecimals(0).repeatOptionalDecimals(0, 0); private static final MonetaryFormat VOLUME_FORMAT_PRECISE = new MonetaryFormat().shift(0).minDecimals(4).repeatOptionalDecimals(0, 0); private static double EXPONENT = Math.pow(10, TraditionalMoney.SMALLEST_UNIT_EXPONENT); // 1000000000000 with precision 8 public static Volume getAdjustedVolume(Volume volumeByAmount, String paymentMethodId) { if (PaymentMethod.isRoundedForAtmCash(paymentMethodId)) return VolumeUtil.getRoundedAtmCashVolume(volumeByAmount); else if (CurrencyUtil.isVolumeRoundedToNearestUnit(volumeByAmount.getCurrencyCode())) return VolumeUtil.getRoundedVolumeUnit(volumeByAmount); else if (CurrencyUtil.isTraditionalCurrency(volumeByAmount.getCurrencyCode())) return VolumeUtil.getRoundedVolumePrecise(volumeByAmount); return volumeByAmount; } public static Volume getRoundedVolumeUnit(Volume volumeByAmount) { // We want to get rounded to 1 unit of the currency, e.g. 1 EUR. return getAdjustedVolumeUnit(volumeByAmount, 1); } private static Volume getRoundedAtmCashVolume(Volume volumeByAmount) { // EUR has precision TraditionalMoney.SMALLEST_UNIT_EXPONENT and we want multiple of 10 so we divide by EXPONENT then // round and multiply with 10 return getAdjustedVolumeUnit(volumeByAmount, 10); } public static Volume getRoundedVolumePrecise(Volume volumeByAmount) { DecimalFormat decimalFormat = new DecimalFormat("#.####", HavenoUtils.DECIMAL_FORMAT_SYMBOLS); double roundedVolume = Double.parseDouble(decimalFormat.format(Double.parseDouble(volumeByAmount.toString()))); return Volume.parse(String.valueOf(roundedVolume), volumeByAmount.getCurrencyCode()); } /** * * @param volumeByAmount The volume generated from an amount * @param factor The factor used for rounding. E.g. 1 means rounded to * units of 1 EUR, 10 means rounded to 10 EUR. * @return The adjusted Fiat volume */ public static Volume getAdjustedVolumeUnit(Volume volumeByAmount, int factor) { // Fiat currencies use precision TraditionalMoney.SMALLEST_UNIT_EXPONENT and we want multiple of factor so we divide // by EXPONENT * factor then round and multiply with factor long roundedVolume = Math.round((double) volumeByAmount.getValue() / (EXPONENT * factor)) * factor; // Smallest allowed volume is factor (e.g. 10 EUR or 1 EUR,...) roundedVolume = Math.max(factor, roundedVolume); return Volume.parse(String.valueOf(roundedVolume), volumeByAmount.getCurrencyCode()); } public static Volume getVolume(BigInteger amount, Price price) { // TODO: conversion to Coin loses precision if (price.getMonetary() instanceof CryptoMoney) { return new Volume(new CryptoExchangeRate((CryptoMoney) price.getMonetary()).coinToCrypto(HavenoUtils.atomicUnitsToCoin(amount))); } else { return new Volume(new TraditionalExchangeRate((TraditionalMoney) price.getMonetary()).coinToTraditionalMoney(HavenoUtils.atomicUnitsToCoin(amount))); } } public static String formatVolume(Offer offer, Boolean decimalAligned, int maxNumberOfDigits) { return formatVolume(offer, decimalAligned, maxNumberOfDigits, true); } public static String formatVolume(Offer offer, Boolean decimalAligned, int maxNumberOfDigits, boolean showRange) { String formattedVolume = offer.isRange() && showRange ? formatVolume(offer.getMinVolume()) + FormattingUtils.RANGE_SEPARATOR + formatVolume(offer.getVolume()) : formatVolume(offer.getVolume()); if (decimalAligned) { formattedVolume = FormattingUtils.fillUpPlacesWithEmptyStrings(formattedVolume, maxNumberOfDigits); } return formattedVolume; } public static String formatLargeFiat(double value, String currency) { if (value <= 0) { return "0"; } NumberFormat numberFormat = DecimalFormat.getInstance(Locale.US); numberFormat.setGroupingUsed(true); return numberFormat.format(value) + " " + currency; } public static String formatLargeFiatWithUnitPostFix(double value, String currency) { if (value <= 0) { return "0"; } String[] units = new String[]{"", "K", "M", "B"}; int digitGroups = (int) (Math.log10(value) / Math.log10(1000)); return new DecimalFormat("#,##0.###") .format(value / Math.pow(1000, digitGroups)) + units[digitGroups] + " " + currency; } public static String formatVolume(Volume volume) { return volume == null ? "" : formatVolume(volume, getMonetaryFormat(volume.getCurrencyCode()), false); } private static String formatVolume(Volume volume, MonetaryFormat volumeFormat, boolean appendCurrencyCode) { if (volume != null) { Monetary monetary = volume.getMonetary(); if (monetary instanceof TraditionalMoney) return FormattingUtils.formatTraditionalMoney((TraditionalMoney) monetary, volumeFormat, appendCurrencyCode); else return FormattingUtils.formatCryptoVolume((CryptoMoney) monetary, appendCurrencyCode); } else { return ""; } } public static String formatVolumeWithCode(Volume volume) { return formatVolume(volume, true); } public static String formatVolume(Volume volume, boolean appendCode) { return formatVolume(volume, volume == null ? null : getMonetaryFormat(volume.getCurrencyCode()), appendCode); } public static String formatAverageVolumeWithCode(Volume volume) { return formatVolume(volume, volume == null ? null : getMonetaryFormat(volume.getCurrencyCode()).minDecimals(2), true); } public static String formatVolumeLabel(String currencyCode) { return formatVolumeLabel(currencyCode, ""); } public static String formatVolumeLabel(String currencyCode, String postFix) { return Res.get("formatter.formatVolumeLabel", currencyCode, postFix); } private static MonetaryFormat getMonetaryFormat(String currencyCode) { return CurrencyUtil.isVolumeRoundedToNearestUnit(currencyCode) ? VOLUME_FORMAT_UNIT : VOLUME_FORMAT_PRECISE; } public static Volume sum(Collection volumes) { if (volumes == null || volumes.isEmpty()) { return null; } Volume sum = null; for (Volume volume : volumes) { if (sum == null) { sum = volume; } else { if (!sum.getCurrencyCode().equals(volume.getCurrencyCode())) { throw new IllegalArgumentException("Cannot sum volumes with different currencies"); } sum = add(sum, volume); } } return sum; } public static Volume add(Volume volume1, Volume volume2) { if (volume1 == null) return volume2; if (volume2 == null) return volume1; if (!volume1.getCurrencyCode().equals(volume2.getCurrencyCode())) { throw new IllegalArgumentException("Cannot add volumes with different currencies"); } if (volume1.getMonetary() instanceof CryptoMoney) { return new Volume(((CryptoMoney) volume1.getMonetary()).add((CryptoMoney) volume2.getMonetary())); } else { return new Volume(((TraditionalMoney) volume1.getMonetary()).add((TraditionalMoney) volume2.getMonetary())); } } } ================================================ FILE: core/src/main/java/haveno/core/util/coin/CoinFormatter.java ================================================ package haveno.core.util.coin; import org.bitcoinj.core.Coin; public interface CoinFormatter { String formatCoin(Coin coin); String formatCoin(Coin coin, boolean appendCode); String formatCoin(Coin coin, int decimalPlaces); String formatCoin(Coin coin, int decimalPlaces, boolean decimalAligned, int maxNumberOfDigits); String formatCoinWithCode(Coin coin); String formatCoinWithCode(long value); org.bitcoinj.utils.MonetaryFormat getMonetaryFormat(); } ================================================ FILE: core/src/main/java/haveno/core/util/coin/CoinUtil.java ================================================ /* * 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 . */ package haveno.core.util.coin; import com.google.common.annotations.VisibleForTesting; import haveno.common.util.MathUtils; import haveno.core.locale.CurrencyUtil; import haveno.core.monetary.Price; import haveno.core.monetary.Volume; import haveno.core.payment.payload.PaymentMethod; import haveno.core.trade.HavenoUtils; import haveno.core.xmr.wallet.Restrictions; import org.bitcoinj.core.Coin; import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; import static com.google.common.base.Preconditions.checkArgument; import static haveno.core.util.VolumeUtil.getAdjustedVolumeUnit; public class CoinUtil { public static Coin minCoin(Coin a, Coin b) { return a.compareTo(b) <= 0 ? a : b; } public static Coin maxCoin(Coin a, Coin b) { return a.compareTo(b) >= 0 ? a : b; } /** * @param value Xmr amount to be converted to percent value. E.g. 0.01 XMR is 1% (of 1 XMR) * @return The percentage value as double (e.g. 1% is 0.01) */ public static double getAsPercentPerXmr(BigInteger value) { return getAsPercentPerXmr(value, HavenoUtils.xmrToAtomicUnits(1.0)); } /** * @param part Xmr amount to be converted to percent value, based on total value passed. * E.g. 0.1 XMR is 25% (of 0.4 XMR) * @param total Total Xmr amount the percentage part is calculated from * * @return The percentage value as double (e.g. 1% is 0.01) */ public static double getAsPercentPerXmr(BigInteger part, BigInteger total) { return MathUtils.roundDouble(HavenoUtils.divide(part == null ? BigInteger.ZERO : part, total == null ? BigInteger.valueOf(1) : total), 4); } /** * @param percent The percentage value as double (e.g. 1% is 0.01) * @param amount The amount as atomic units for the percentage calculation * @return The percentage as atomic units (e.g. 1% of 1 XMR is 0.01 XMR) */ public static BigInteger getPercentOfAmount(double percent, BigInteger amount) { if (amount == null) amount = BigInteger.ZERO; return BigDecimal.valueOf(percent).multiply(new BigDecimal(amount)).setScale(8, RoundingMode.DOWN).toBigInteger(); } public static BigInteger getRoundedAmount(BigInteger amount, Price price, BigInteger minAmount, BigInteger maxAmount, String currencyCode, String paymentMethodId) { if (price != null) { if (PaymentMethod.isRoundedForAtmCash(paymentMethodId)) { return getRoundedAtmCashAmount(amount, price, minAmount, maxAmount); } else if (CurrencyUtil.isVolumeRoundedToNearestUnit(currencyCode)) { return getRoundedAmountUnit(amount, price, minAmount, maxAmount); } } return getRoundedAmount4Decimals(amount, minAmount, maxAmount); } public static BigInteger getRoundedAtmCashAmount(BigInteger amount, Price price, BigInteger minAmount, BigInteger maxAmount) { return getAdjustedAmount(amount, price, minAmount, maxAmount, 10); } /** * Calculate the possibly adjusted amount for {@code amount}, taking into account the * {@code price} and {@code maxTradeLimit} and {@code factor}. * * @param amount Monero amount which is a candidate for getting rounded. * @param price Price used in relation to that amount. * @param minAmount The minimum amount. * @param maxAmount The maximum amount. * @return The adjusted amount */ public static BigInteger getRoundedAmountUnit(BigInteger amount, Price price, BigInteger minAmount, BigInteger maxAmount) { return getAdjustedAmount(amount, price, minAmount, maxAmount, 1); } /** * Calculate the possibly adjusted amount for {@code amount}, taking into account the * {@code price} and {@code maxTradeLimit} and {@code factor}. * * @param amount amount which is a candidate for getting rounded. * @param price Price used in relation to that amount. * @param minAmount The minimum amount. * @param maxAmount The maximum amount. * @param factor The factor used for rounding. E.g. 1 means rounded to units of * 1 EUR, 10 means rounded to 10 EUR, etc. * @return The adjusted amount */ @VisibleForTesting static BigInteger getAdjustedAmount(BigInteger amount, Price price, BigInteger minAmount, BigInteger maxAmount, int factor) { if (minAmount == null) minAmount = Restrictions.getMinTradeAmount(); checkOfferAmountRange(amount, minAmount, maxAmount); checkArgument( factor > 0, "factor must be positive" ); // Amount must result in a volume of min factor units of the fiat currency, e.g. 1 EUR or 10 EUR in case of HalCash. Volume smallestUnitForVolume = Volume.parse(String.valueOf(factor), price.getCurrencyCode()); if (smallestUnitForVolume.getValue() <= 0) return BigInteger.ZERO; BigInteger smallestUnitForAmount = price.getAmountByVolume(smallestUnitForVolume); smallestUnitForAmount = BigInteger.valueOf(Math.max(minAmount.longValueExact(), smallestUnitForAmount.longValueExact())); // We get the adjusted volume from our amount boolean useSmallestUnitForAmount = amount.compareTo(smallestUnitForAmount) < 0; Volume volume = useSmallestUnitForAmount ? getAdjustedVolumeUnit(price.getVolumeByAmount(smallestUnitForAmount), factor) : getAdjustedVolumeUnit(price.getVolumeByAmount(amount), factor); if (volume.getValue() <= 0) return BigInteger.ZERO; // From that adjusted volume we calculate back the amount. It might be a bit different as // the amount used as input before due to rounding. BigInteger amountByVolume = price.getAmountByVolume(volume); // For the amount we allow only 4 decimal places long adjustedAmount = HavenoUtils.centinerosToAtomicUnits(Math.round(HavenoUtils.atomicUnitsToCentineros(amountByVolume) / 10000d) * 10000).longValueExact(); // If we are below the minAmount we increase the amount by the smallestUnitForAmount BigInteger smallestUnitForAmountUnadjusted = price.getAmountByVolume(smallestUnitForVolume); if (minAmount != null) { while (adjustedAmount < minAmount.longValueExact()) { adjustedAmount += smallestUnitForAmountUnadjusted.longValueExact(); } } // If we are above our trade limit we reduce the amount by the smallestUnitForAmount if (maxAmount != null) { while (adjustedAmount > maxAmount.longValueExact()) { adjustedAmount -= smallestUnitForAmountUnadjusted.longValueExact(); } } adjustedAmount = Math.max(minAmount.longValueExact(), adjustedAmount); if (maxAmount != null) adjustedAmount = Math.min(maxAmount.longValueExact(), adjustedAmount); return BigInteger.valueOf(adjustedAmount); } public static BigInteger getRoundedAmount4Decimals(BigInteger amount, BigInteger minAmount, BigInteger maxAmount) { if (minAmount == null) minAmount = Restrictions.getMinTradeAmount(); checkOfferAmountRange(amount, minAmount, maxAmount); // round to nearest 4 decimals BigInteger factor = BigInteger.TEN.pow(8); // 4 decimals in XMR (12 total) = 10^(12-4) = 10^8 BigInteger roundedAmount = amount.add(factor.divide(BigInteger.valueOf(2))) .divide(factor) .multiply(factor); // round up to nearest 4 decimal increment if below min amount if (roundedAmount.compareTo(minAmount) < 0) { roundedAmount = amount.add(factor.subtract(BigInteger.ONE)) .divide(factor) .multiply(factor); // step up one more factor if still below min amount (e.g. min isn't a multiple of factor) if (roundedAmount.compareTo(minAmount) < 0) { roundedAmount = roundedAmount.add(factor); } } // round down to nearest 4 decimal increment if above max amount if (maxAmount != null) { BigInteger roundedMaxAmount = maxAmount.divide(factor).multiply(factor); roundedAmount = roundedAmount.min(roundedMaxAmount); } // verify rounding if (minAmount != null && roundedAmount.compareTo(minAmount) < 0) throw new IllegalStateException("Rounded amount " + HavenoUtils.atomicUnitsToXmr(roundedAmount) + " XMR is below minimum of " + HavenoUtils.atomicUnitsToXmr(minAmount) + " XMR. That should never happen."); if (maxAmount != null && roundedAmount.compareTo(maxAmount) > 0) throw new IllegalStateException("Rounded amount " + HavenoUtils.atomicUnitsToXmr(roundedAmount) + " XMR is above maximum of " + HavenoUtils.atomicUnitsToXmr(maxAmount) + " XMR. That should never happen."); return roundedAmount; } private static void checkOfferAmountRange(BigInteger amount, BigInteger minAmount, BigInteger maxAmount) { checkArgument( amount.longValueExact() >= Restrictions.getMinTradeAmount().longValueExact(), "amount must be above minimum of " + HavenoUtils.atomicUnitsToXmr(Restrictions.getMinTradeAmount()) + " xmr but was " + HavenoUtils.atomicUnitsToXmr(amount) + " xmr" ); checkArgument( minAmount.longValueExact() >= Restrictions.getMinTradeAmount().longValueExact(), "minAmount must be above minimum of " + HavenoUtils.atomicUnitsToXmr(Restrictions.getMinTradeAmount()) + " xmr but was " + HavenoUtils.atomicUnitsToXmr(minAmount) + " xmr" ); if (maxAmount != null) { checkArgument( amount.longValueExact() <= maxAmount.longValueExact(), "amount must be below maximum of " + HavenoUtils.atomicUnitsToXmr(maxAmount) + " xmr but was " + HavenoUtils.atomicUnitsToXmr(amount) + " xmr" ); checkArgument( maxAmount.longValueExact() >= minAmount.longValueExact(), "maxAmount must be above minimum of " + HavenoUtils.atomicUnitsToXmr(Restrictions.getMinTradeAmount()) + " xmr but was " + HavenoUtils.atomicUnitsToXmr(maxAmount) + " xmr" ); } } } ================================================ FILE: core/src/main/java/haveno/core/util/coin/ImmutableCoinFormatter.java ================================================ /* * 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 . */ package haveno.core.util.coin; import com.google.inject.Inject; import haveno.core.util.FormattingUtils; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.Coin; import org.bitcoinj.utils.MonetaryFormat; @Slf4j public class ImmutableCoinFormatter implements CoinFormatter { // We don't support localized formatting. Format is always using "." as decimal mark and no grouping separator. // Input of "," as decimal mark (like in german locale) will be replaced with ".". // Input of a group separator (1,123,45) lead to an validation error. // Note: BtcFormat was intended to be used, but it lead to many problems (automatic format to mBit, // no way to remove grouping separator). It seems to be not optimal for user input formatting. @Getter private MonetaryFormat monetaryFormat; @Inject public ImmutableCoinFormatter(MonetaryFormat monetaryFormat) { this.monetaryFormat = monetaryFormat; } /////////////////////////////////////////////////////////////////////////////////////////// // BTC /////////////////////////////////////////////////////////////////////////////////////////// @Override public String formatCoin(Coin coin) { return formatCoin(coin, -1); } @Override public String formatCoin(Coin coin, boolean appendCode) { return appendCode ? formatCoinWithCode(coin) : formatCoin(coin); } @Override public String formatCoin(Coin coin, int decimalPlaces) { return formatCoin(coin, decimalPlaces, false, 0); } @Override public String formatCoin(Coin coin, int decimalPlaces, boolean decimalAligned, int maxNumberOfDigits) { return FormattingUtils.formatCoin(coin, decimalPlaces, decimalAligned, maxNumberOfDigits, monetaryFormat); } @Override public String formatCoinWithCode(Coin coin) { return FormattingUtils.formatCoinWithCode(coin, monetaryFormat); } @Override public String formatCoinWithCode(long value) { return FormattingUtils.formatCoinWithCode(Coin.valueOf(value), monetaryFormat); } } ================================================ FILE: core/src/main/java/haveno/core/util/validation/AmountValidator4Decimals.java ================================================ /* * 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 . */ package haveno.core.util.validation; import com.google.inject.Inject; public class AmountValidator4Decimals extends MonetaryValidator { @Override public double getMinValue() { return 0.0001; } @Override public double getMaxValue() { // Hard to say what the max value should be (zimbabwe dollar....)? // Lets set it to Double.MAX_VALUE until we find some reasonable number return Double.MAX_VALUE; } @Inject public AmountValidator4Decimals() { } } ================================================ FILE: core/src/main/java/haveno/core/util/validation/AmountValidator8Decimals.java ================================================ /* * 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 . */ package haveno.core.util.validation; import com.google.inject.Inject; public class AmountValidator8Decimals extends MonetaryValidator { @Override public double getMinValue() { return 0.00000001; } @Override public double getMaxValue() { // hard to say what the max value should be with cryptos return 100_000_000; } @Inject public AmountValidator8Decimals() { } } ================================================ FILE: core/src/main/java/haveno/core/util/validation/BtcAddressValidator.java ================================================ /* * 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 . */ package haveno.core.util.validation; import com.google.inject.Inject; import haveno.common.config.Config; import haveno.core.locale.Res; import org.bitcoinj.core.Address; import org.bitcoinj.core.AddressFormatException; public final class BtcAddressValidator extends InputValidator { @Inject public BtcAddressValidator() { } @Override public ValidationResult validate(String input) { ValidationResult result = validateIfNotEmpty(input); if (result.isValid) return validateBtcAddress(input); else return result; } private ValidationResult validateBtcAddress(String input) { try { Address.fromString(Config.baseCurrencyNetworkParameters(), input); return new ValidationResult(true); } catch (AddressFormatException e) { return new ValidationResult(false, Res.get("validation.btc.invalidFormat")); } } } ================================================ FILE: core/src/main/java/haveno/core/util/validation/HexStringValidator.java ================================================ /* * 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 . */ package haveno.core.util.validation; import haveno.common.util.Utilities; import haveno.core.locale.Res; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.Setter; @EqualsAndHashCode(callSuper = true) @Data public class HexStringValidator extends InputValidator { @Setter private int minLength = Integer.MIN_VALUE; @Setter private int maxLength = Integer.MAX_VALUE; public HexStringValidator() { } public ValidationResult validate(String input) { ValidationResult validationResult = super.validate(input); if (!validationResult.isValid) return validationResult; if (input.length() > maxLength || input.length() < minLength) new ValidationResult(false, Res.get("validation.length", minLength, maxLength)); try { Utilities.decodeFromHex(input); return validationResult; } catch (Throwable t) { return new ValidationResult(false, Res.get("validation.noHexString", input)); } } } ================================================ FILE: core/src/main/java/haveno/core/util/validation/InputValidator.java ================================================ /* * 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 . */ package haveno.core.util.validation; import haveno.core.locale.Res; import java.math.BigInteger; import java.util.Objects; import java.util.function.Function; public class InputValidator { public ValidationResult validate(String input) { return validateIfNotEmpty(input); } protected ValidationResult validateIfNotEmpty(String input) { //trim added to avoid empty input if (input == null || input.trim().length() == 0) return new ValidationResult(false, Res.get("validation.empty")); else return new ValidationResult(true); } public static class ValidationResult { public final boolean isValid; public final String errorMessage; public ValidationResult(boolean isValid, String errorMessage) { this.isValid = isValid; this.errorMessage = errorMessage; } public ValidationResult(boolean isValid) { this(isValid, null); } public ValidationResult and(ValidationResult next) { if (this.isValid) return next; else return this; } @Override public String toString() { return "ValidationResult{" + "isValid=" + isValid + ", errorMessage='" + errorMessage + '\'' + '}'; } public boolean errorMessageEquals(ValidationResult other) { if (this == other) return true; if (other == null) return false; return Objects.equals(errorMessage, other.errorMessage); } public interface Validator extends Function { } /* This function validates the input with array of validator functions. If any function validation result is false, it short circuits as in && (and) operation. */ public ValidationResult andValidation(String input, Validator... validators) { ValidationResult result = null; for (Validator validator : validators) { result = validator.apply(input); if (!result.isValid) return result; } return result; } } protected boolean isPositiveNumber(String input) { try { return input != null && new BigInteger(input).compareTo(BigInteger.ZERO) >= 0; } catch (Throwable t) { return false; } } protected boolean isNumberWithFixedLength(String input, int length) { return isPositiveNumber(input) && input.length() == length; } protected boolean isNumberInRange(String input, int minLength, int maxLength) { return isPositiveNumber(input) && input.length() >= minLength && input.length() <= maxLength; } @SuppressWarnings("BooleanMethodIsAlwaysInverted") protected boolean isStringWithFixedLength(String input, int length) { return input != null && input.length() == length; } protected boolean isStringInRange(String input, int minLength, int maxLength) { return input != null && input.length() >= minLength && input.length() <= maxLength; } } ================================================ FILE: core/src/main/java/haveno/core/util/validation/IntegerValidator.java ================================================ /* * 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 . */ package haveno.core.util.validation; import haveno.core.locale.Res; import lombok.Data; import lombok.EqualsAndHashCode; @EqualsAndHashCode(callSuper = true) @Data public class IntegerValidator extends InputValidator { private int minValue = Integer.MIN_VALUE; private int maxValue = Integer.MAX_VALUE; private int intValue; public IntegerValidator() { } public IntegerValidator(int minValue, int maxValue) { this.minValue = minValue; this.maxValue = maxValue; } public ValidationResult validate(String input) { ValidationResult validationResult = super.validate(input); if (!validationResult.isValid) return validationResult; if (!isInteger(input)) return new ValidationResult(false, Res.get("validation.notAnInteger")); if (isBelowMinValue(intValue)) return new ValidationResult(false, Res.get("validation.xmr.tooSmall", minValue)); if (isAboveMaxValue(intValue)) return new ValidationResult(false, Res.get("validation.xmr.tooLarge", maxValue)); return validationResult; } private boolean isBelowMinValue(int intValue) { return intValue < minValue; } private boolean isAboveMaxValue(int intValue) { return intValue > maxValue; } private boolean isInteger(String input) { try { intValue = Integer.parseInt(input); return true; } catch (Throwable t) { return false; } } } ================================================ FILE: core/src/main/java/haveno/core/util/validation/MonetaryValidator.java ================================================ /* * 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 . */ package haveno.core.util.validation; import com.google.inject.Inject; import haveno.core.locale.Res; public abstract class MonetaryValidator extends NumberValidator { public abstract double getMinValue(); @SuppressWarnings("SameReturnValue") public abstract double getMaxValue(); @Inject public MonetaryValidator() { } @Override public ValidationResult validate(String input) { ValidationResult result = validateIfNotEmpty(input); if (result.isValid) { input = cleanInput(input); result = validateIfNumber(input); } if (result.isValid) { result = result.andValidation(input, this::validateIfNotZero, this::validateIfNotNegative, this::validateIfNotExceedsMinValue, this::validateIfNotExceedsMaxValue); } return result; } protected ValidationResult validateIfNotExceedsMinValue(String input) { double d = Double.parseDouble(input); if (d < getMinValue()) return new ValidationResult(false, Res.get("validation.traditional.tooSmall")); else return new ValidationResult(true); } protected ValidationResult validateIfNotExceedsMaxValue(String input) { double d = Double.parseDouble(input); if (d > getMaxValue()) return new ValidationResult(false, Res.get("validation.traditional.tooLarge")); else return new ValidationResult(true); } } ================================================ FILE: core/src/main/java/haveno/core/util/validation/NumberValidator.java ================================================ /* * 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 . */ package haveno.core.util.validation; import haveno.core.locale.Res; import haveno.core.util.ParsingUtils; /** * NumberValidator for validating basic number values. * Localisation not supported at the moment * The decimal mark can be either "." or ",". Thousand separators are not supported yet, * but might be added alter with Local support. */ public abstract class NumberValidator extends InputValidator { protected String cleanInput(String input) { return ParsingUtils.convertCharsForNumber(input); } protected ValidationResult validateIfNumber(String input) { try { //noinspection ResultOfMethodCallIgnored Double.parseDouble(input); return new ValidationResult(true); } catch (Exception e) { return new ValidationResult(false, Res.get("validation.NaN")); } } protected ValidationResult validateIfNotZero(String input) { if (Double.parseDouble(input) == 0) return new ValidationResult(false, Res.get("validation.zero")); else return new ValidationResult(true); } protected ValidationResult validateIfNotNegative(String input) { if (Double.parseDouble(input) < 0) return new ValidationResult(false, Res.get("validation.negative")); else return new ValidationResult(true); } } ================================================ FILE: core/src/main/java/haveno/core/util/validation/RegexValidator.java ================================================ package haveno.core.util.validation; import haveno.core.locale.Res; public class RegexValidator extends InputValidator { private String pattern; private String errorMessage; @Override public ValidationResult validate(String input) { ValidationResult result = new ValidationResult(true); String message = (this.errorMessage == null) ? Res.get("validation.pattern", this.pattern) : this.errorMessage; String testStr = input == null ? "" : input; if (this.pattern == null) return result; if (!testStr.matches(this.pattern)) result = new ValidationResult(false, message); return result; } public void setPattern(String pattern) { this.pattern = pattern; } public String getErrorMessage() { return errorMessage; } public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } } ================================================ FILE: core/src/main/java/haveno/core/util/validation/RegexValidatorFactory.java ================================================ /* * 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 . */ package haveno.core.util.validation; public class RegexValidatorFactory { public static RegexValidator addressRegexValidator() { RegexValidator regexValidator = new RegexValidator(); String portRegexPattern = "(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])"; String onionV2RegexPattern = String.format("[a-zA-Z2-7]{16}\\.onion(?:\\:%1$s)?", portRegexPattern); String onionV3RegexPattern = String.format("[a-zA-Z2-7]{56}\\.onion(?:\\:%1$s)?", portRegexPattern); String ipv4RegexPattern = String.format("(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}" + "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + "(?:\\:%1$s)?", portRegexPattern); String ipv6RegexPattern = "(" + "([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|" + // 1:2:3:4:5:6:7:8 "([0-9a-fA-F]{1,4}:){1,7}:|" + // 1:: 1:2:3:4:5:6:7:: "([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|" + // 1::8 1:2:3:4:5:6::8 1:2:3:4:5:6::8 "([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|" + // 1::7:8 1:2:3:4:5::7:8 1:2:3:4:5::8 "([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|" + // 1::6:7:8 1:2:3:4::6:7:8 1:2:3:4::8 "([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|" + // 1::5:6:7:8 1:2:3::5:6:7:8 1:2:3::8 "([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|" + // 1::4:5:6:7:8 1:2::4:5:6:7:8 1:2::8 "[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|" + // 1::3:4:5:6:7:8 1::3:4:5:6:7:8 1::8 ":((:[0-9a-fA-F]{1,4}){1,7}|:)|" + // ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 :: "fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|" + // fe80::7:8%eth0 fe80::7:8%1 "::(ffff(:0{1,4}){0,1}:){0,1}" + // (link-local IPv6 addresses with zone index) "((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}" + "(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|" + // ::255.255.255.255 ::ffff:255.255.255.255 ::ffff:0:255.255.255.255 "([0-9a-fA-F]{1,4}:){1,4}:" + // (IPv4-mapped IPv6 addresses and IPv4-translated addresses) "((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}" + "(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])" + // 2001:db8:3:4::192.0.2.33 64:ff9b::192.0.2.33 ")"; // (IPv4-Embedded IPv6 Address) ipv6RegexPattern = String.format("(?:%1$s)|(?:\\[%1$s\\]\\:%2$s)", ipv6RegexPattern, portRegexPattern); String fqdnRegexPattern = String.format("(((?!-)[a-zA-Z0-9-]{1,63}(?. */ package haveno.core.util.validation; import haveno.core.locale.Res; import lombok.Data; import lombok.EqualsAndHashCode; @EqualsAndHashCode(callSuper = true) @Data public class StringValidator extends InputValidator { private int length = 0; public StringValidator() { } public ValidationResult validate(String input) { ValidationResult validationResult = super.validate(input); if (!validationResult.isValid) return validationResult; if (!isStringWithFixedLength(input, length)) return new ValidationResult(false, Res.get("validation.invalidInput", input)); return validationResult; } } ================================================ FILE: core/src/main/java/haveno/core/util/validation/UrlInputValidator.java ================================================ /* * 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 . */ package haveno.core.util.validation; import haveno.core.locale.Res; import java.net.URL; import static com.google.common.base.Preconditions.checkArgument; public class UrlInputValidator extends InputValidator { public UrlInputValidator() { } public ValidationResult validate(String input) { ValidationResult validationResult = super.validate(input); if (!validationResult.isValid) return validationResult; try { new URL(input); // does not cover all invalid urls, so we use a regex as well String regex = "^(https?)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]"; checkArgument(input.matches(regex), "URL does not match regex"); return validationResult; } catch (Throwable t) { return new ValidationResult(false, Res.get("validation.invalidUrl")); } } } ================================================ FILE: core/src/main/java/haveno/core/xmr/Balances.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.xmr; import com.google.inject.Inject; import haveno.common.ThreadUtils; import haveno.core.api.model.XmrBalanceInfo; import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.refund.RefundManager; import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.HavenoUtils; import haveno.core.trade.MakerTrade; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.trade.failed.FailedTradesManager; import haveno.core.xmr.listeners.XmrBalanceListener; import haveno.core.xmr.wallet.XmrWalletService; import java.math.BigInteger; import java.util.List; import java.util.stream.Collectors; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.collections.ListChangeListener; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import monero.wallet.model.MoneroOutputQuery; import monero.wallet.model.MoneroOutputWallet; @Slf4j public class Balances { private final TradeManager tradeManager; private final XmrWalletService xmrWalletService; private final OpenOfferManager openOfferManager; private final RefundManager refundManager; @Getter private BigInteger availableBalance; @Getter private BigInteger pendingBalance; @Getter private BigInteger reservedOfferBalance; @Getter private BigInteger reservedTradeBalance; @Getter private BigInteger reservedBalance; // TODO (woodser): this balance is sum of reserved funds for offers and trade multisigs; remove? @Getter private final IntegerProperty updateCounter = new SimpleIntegerProperty(0); @Inject public Balances(TradeManager tradeManager, XmrWalletService xmrWalletService, OpenOfferManager openOfferManager, ClosedTradableManager closedTradableManager, FailedTradesManager failedTradesManager, RefundManager refundManager) { this.tradeManager = tradeManager; this.xmrWalletService = xmrWalletService; this.openOfferManager = openOfferManager; this.refundManager = refundManager; } public void onAllServicesInitialized() { openOfferManager.getObservableList().addListener((ListChangeListener) c -> updateBalances()); tradeManager.getObservableList().addListener((ListChangeListener) change -> updateBalances()); refundManager.getDisputesAsObservableList().addListener((ListChangeListener) c -> updateBalances()); xmrWalletService.addBalanceListener(new XmrBalanceListener() { @Override public void onBalanceChanged(BigInteger balance) { doUpdateBalances(); } }); doUpdateBalances(); } public XmrBalanceInfo getBalances() { synchronized (this) { if (availableBalance == null) return null; return new XmrBalanceInfo(availableBalance.longValue() + pendingBalance.longValue(), availableBalance.longValue(), pendingBalance.longValue(), reservedOfferBalance.longValue(), reservedTradeBalance.longValue()); } } private void updateBalances() { ThreadUtils.submitToPool(() -> doUpdateBalances()); } private void doUpdateBalances() { synchronized (HavenoUtils.xmrWalletService.getWalletLock()) { synchronized (this) { // get non-trade balance before BigInteger balanceSumBefore = getNonTradeBalanceSum(); // get wallet balances BigInteger balance = xmrWalletService.getWallet() == null ? BigInteger.ZERO : xmrWalletService.getBalance(); availableBalance = xmrWalletService.getWallet() == null ? BigInteger.ZERO : xmrWalletService.getAvailableBalance(); // calculate pending balance by adding frozen trade balances - reserved amounts pendingBalance = balance.subtract(availableBalance); List trades = tradeManager.getTradesStreamWithFundsLockedIn().collect(Collectors.toList()); for (Trade trade : trades) { if (trade.getFrozenAmount().equals(new BigInteger("0"))) continue; BigInteger tradeFee = trade instanceof MakerTrade ? trade.getMakerFee() : trade.getTakerFee(); pendingBalance = pendingBalance.add(trade.getFrozenAmount()).subtract(trade.getReservedAmount()).subtract(tradeFee).subtract(trade.getSelf().getDepositTxFee()); } // calculate reserved offer balance reservedOfferBalance = BigInteger.ZERO; if (xmrWalletService.getWallet() != null) { List frozenOutputs = xmrWalletService.getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false)); for (MoneroOutputWallet frozenOutput : frozenOutputs) reservedOfferBalance = reservedOfferBalance.add(frozenOutput.getAmount()); } for (Trade trade : trades) { reservedOfferBalance = reservedOfferBalance.subtract(trade.getFrozenAmount()); // subtract frozen trade balances } // calculate reserved trade balance reservedTradeBalance = BigInteger.ZERO; for (Trade trade : trades) { reservedTradeBalance = reservedTradeBalance.add(trade.getReservedAmount()); } // calculate reserved balance reservedBalance = reservedOfferBalance.add(reservedTradeBalance); // play sound if funds received boolean fundsReceived = balanceSumBefore != null && getNonTradeBalanceSum().compareTo(balanceSumBefore) > 0; if (fundsReceived) HavenoUtils.playCashRegisterSound(); } // notify balance update updateCounter.set(updateCounter.get() + 1); } } private BigInteger getNonTradeBalanceSum() { synchronized (this) { if (availableBalance == null) return null; return availableBalance.add(pendingBalance).add(reservedOfferBalance); } } } ================================================ FILE: core/src/main/java/haveno/core/xmr/XmrConnectionModule.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.xmr; import com.google.inject.Singleton; import haveno.common.app.AppModule; import haveno.common.config.Config; import haveno.core.api.XmrConnectionService; import haveno.core.xmr.model.EncryptedConnectionList; import lombok.extern.slf4j.Slf4j; @Slf4j public class XmrConnectionModule extends AppModule { public XmrConnectionModule(Config config) { super(config); } @Override protected final void configure() { bind(EncryptedConnectionList.class).in(Singleton.class); bind(XmrConnectionService.class).in(Singleton.class); } } ================================================ FILE: core/src/main/java/haveno/core/xmr/XmrModule.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.xmr; import com.google.inject.Singleton; import com.google.inject.TypeLiteral; import haveno.common.app.AppModule; import haveno.common.config.Config; import haveno.core.provider.ProvidersRepository; import haveno.core.provider.fee.FeeProvider; import haveno.core.provider.price.PriceFeedService; import haveno.core.xmr.model.AddressEntryList; import haveno.core.xmr.model.EncryptedConnectionList; import haveno.core.xmr.model.XmrAddressEntryList; import haveno.core.xmr.nodes.XmrNodes; import haveno.core.xmr.setup.RegTestHost; import haveno.core.xmr.setup.WalletsSetup; import haveno.core.xmr.wallet.BtcWalletService; import haveno.core.xmr.wallet.NonBsqCoinSelector; import haveno.core.xmr.wallet.TradeWalletService; import haveno.core.xmr.wallet.XmrWalletService; import java.io.File; import java.util.Arrays; import java.util.List; import static com.google.inject.name.Names.named; import static haveno.common.config.Config.PROVIDERS; import static haveno.common.config.Config.WALLET_DIR; import static haveno.common.config.Config.WALLET_RPC_BIND_PORT; public class XmrModule extends AppModule { public XmrModule(Config config) { super(config); } @Override protected void configure() { // If we have selected BTC_DAO_REGTEST or BTC_DAO_TESTNET we use our master regtest node, // otherwise the specified host or default (localhost) String regTestHost = config.bitcoinRegtestHost; if (regTestHost.isEmpty()) { regTestHost = Config.DEFAULT_REGTEST_HOST; } RegTestHost.HOST = regTestHost; if (Arrays.asList("localhost", "127.0.0.1").contains(regTestHost)) { bind(RegTestHost.class).toInstance(RegTestHost.LOCALHOST); } else if ("none".equals(regTestHost)) { bind(RegTestHost.class).toInstance(RegTestHost.NONE); } else { bind(RegTestHost.class).toInstance(RegTestHost.REMOTE_HOST); } bind(File.class).annotatedWith(named(WALLET_DIR)).toInstance(config.walletDir); bind(int.class).annotatedWith(named(WALLET_RPC_BIND_PORT)).toInstance(config.walletRpcBindPort); bindConstant().annotatedWith(named(Config.XMR_NODE)).to(config.xmrNode); bindConstant().annotatedWith(named(Config.XMR_NODE_USERNAME)).to(config.xmrNodeUsername); bindConstant().annotatedWith(named(Config.XMR_NODE_PASSWORD)).to(config.xmrNodePassword); bindConstant().annotatedWith(named(Config.XMR_NODES)).to(config.xmrNodes); bindConstant().annotatedWith(named(Config.USE_NATIVE_XMR_WALLET)).to(config.useNativeXmrWallet); bindConstant().annotatedWith(named(Config.USER_AGENT)).to(config.userAgent); bindConstant().annotatedWith(named(Config.NUM_CONNECTIONS_FOR_BTC)).to(config.numConnectionsForBtc); bindConstant().annotatedWith(named(Config.USE_ALL_PROVIDED_NODES)).to(config.useAllProvidedNodes); bindConstant().annotatedWith(named(Config.SOCKS5_DISCOVER_MODE)).to(config.socks5DiscoverMode); bind(new TypeLiteral>(){}).annotatedWith(named(PROVIDERS)).toInstance(config.providers); bind(AddressEntryList.class).in(Singleton.class); bind(XmrAddressEntryList.class).in(Singleton.class); bind(EncryptedConnectionList.class).in(Singleton.class); bind(WalletsSetup.class).in(Singleton.class); bind(XmrWalletService.class).in(Singleton.class); bind(BtcWalletService.class).in(Singleton.class); bind(TradeWalletService.class).in(Singleton.class); bind(NonBsqCoinSelector.class).in(Singleton.class); bind(XmrNodes.class).in(Singleton.class); bind(Balances.class).in(Singleton.class); bind(ProvidersRepository.class).in(Singleton.class); bind(FeeProvider.class).in(Singleton.class); bind(PriceFeedService.class).in(Singleton.class); } } ================================================ FILE: core/src/main/java/haveno/core/xmr/XmrNodeSettings.java ================================================ /* * 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 . */ package haveno.core.xmr; import haveno.common.proto.persistable.PersistableEnvelope; import lombok.AllArgsConstructor; import lombok.Data; import lombok.extern.slf4j.Slf4j; import java.util.List; import java.util.Optional; import javax.annotation.Nullable; @Slf4j @Data @AllArgsConstructor public class XmrNodeSettings implements PersistableEnvelope { @Nullable String blockchainPath; @Nullable String bootstrapUrl; @Nullable List startupFlags; @Nullable Boolean syncBlockchain; public XmrNodeSettings() { } public static XmrNodeSettings fromProto(protobuf.XmrNodeSettings proto) { return new XmrNodeSettings( proto.getBlockchainPath(), proto.getBootstrapUrl(), proto.getStartupFlagsList(), proto.getSyncBlockchain()); } @Override public protobuf.XmrNodeSettings toProtoMessage() { protobuf.XmrNodeSettings.Builder builder = protobuf.XmrNodeSettings.newBuilder(); Optional.ofNullable(blockchainPath).ifPresent(e -> builder.setBlockchainPath(blockchainPath)); Optional.ofNullable(bootstrapUrl).ifPresent(e -> builder.setBootstrapUrl(bootstrapUrl)); Optional.ofNullable(startupFlags).ifPresent(e -> builder.addAllStartupFlags(startupFlags)); Optional.ofNullable(syncBlockchain).ifPresent(e -> builder.setSyncBlockchain(syncBlockchain)); return builder.build(); } } ================================================ FILE: core/src/main/java/haveno/core/xmr/exceptions/AddressEntryException.java ================================================ /* * 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 . */ package haveno.core.xmr.exceptions; public class AddressEntryException extends Exception { public AddressEntryException(String message) { super(message); } } ================================================ FILE: core/src/main/java/haveno/core/xmr/exceptions/InsufficientFundsException.java ================================================ /* * 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 . */ package haveno.core.xmr.exceptions; public class InsufficientFundsException extends Exception { public InsufficientFundsException(String message) { super(message); } } ================================================ FILE: core/src/main/java/haveno/core/xmr/exceptions/InvalidHostException.java ================================================ /* * 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 . */ package haveno.core.xmr.exceptions; public class InvalidHostException extends IllegalArgumentException { public InvalidHostException(String message) { super(message); } } ================================================ FILE: core/src/main/java/haveno/core/xmr/exceptions/RejectedTxException.java ================================================ /* * 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 . */ package haveno.core.xmr.exceptions; import lombok.Getter; import org.bitcoinj.core.RejectMessage; import javax.annotation.Nullable; public class RejectedTxException extends RuntimeException { @Getter private final RejectMessage rejectMessage; @Getter @Nullable private final String txId; public RejectedTxException(String message, RejectMessage rejectMessage) { super(message); this.rejectMessage = rejectMessage; txId = rejectMessage.getRejectedObjectHash() != null ? rejectMessage.getRejectedObjectHash().toString() : null; } @Override public String toString() { return "RejectedTxException{" + "\n rejectMessage=" + rejectMessage + ",\n txId='" + txId + '\'' + "\n} " + super.toString(); } } ================================================ FILE: core/src/main/java/haveno/core/xmr/exceptions/SigningException.java ================================================ /* * 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 . */ package haveno.core.xmr.exceptions; public class SigningException extends Exception { public SigningException(String message) { super(message); } } ================================================ FILE: core/src/main/java/haveno/core/xmr/exceptions/TransactionVerificationException.java ================================================ /* * 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 . */ package haveno.core.xmr.exceptions; public class TransactionVerificationException extends Exception { public TransactionVerificationException(Throwable t) { super(t); } public TransactionVerificationException(String errorMessage) { super(errorMessage); } } ================================================ FILE: core/src/main/java/haveno/core/xmr/exceptions/TxBroadcastException.java ================================================ /* * 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 . */ package haveno.core.xmr.exceptions; import lombok.Getter; import javax.annotation.Nullable; /** * Used in case the broadcasting of a tx did not succeed in the expected time. * The broadcast can still succeed at a later moment though. */ public class TxBroadcastException extends Exception { @Getter @Nullable private String txId; public TxBroadcastException(String message) { super(message); } public TxBroadcastException(String message, Throwable cause) { super(message, cause); } public TxBroadcastException(String message, String txId) { super(message); this.txId = txId; } @Override public String toString() { return "TxBroadcastException{" + "\n txId='" + txId + '\'' + "\n} " + super.toString(); } } ================================================ FILE: core/src/main/java/haveno/core/xmr/exceptions/TxBroadcastTimeoutException.java ================================================ /* * 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 . */ package haveno.core.xmr.exceptions; import lombok.Getter; import org.bitcoinj.core.Transaction; import org.bitcoinj.wallet.Wallet; import javax.annotation.Nullable; public class TxBroadcastTimeoutException extends TxBroadcastException { @Getter @Nullable private final Transaction localTx; @Getter private final int delay; @Getter private final Wallet wallet; /** * @param localTx The tx we sent out * @param delay The timeout delay * @param wallet Wallet is needed if a client is calling wallet.commitTx(tx) */ public TxBroadcastTimeoutException(Transaction localTx, int delay, Wallet wallet) { super("The transaction was not broadcasted in " + delay + " seconds. txId=" + localTx.getTxId().toString()); this.localTx = localTx; this.delay = delay; this.wallet = wallet; } @Override public String toString() { return "TxBroadcastTimeoutException{" + "\n localTx=" + localTx + ",\n delay=" + delay + "\n} " + super.toString(); } } ================================================ FILE: core/src/main/java/haveno/core/xmr/exceptions/WalletException.java ================================================ /* * 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 . */ package haveno.core.xmr.exceptions; public class WalletException extends Exception { public WalletException(Throwable t) { super(t); } } ================================================ FILE: core/src/main/java/haveno/core/xmr/listeners/AddressConfidenceListener.java ================================================ /* * 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 . */ package haveno.core.xmr.listeners; import org.bitcoinj.core.Address; import org.bitcoinj.core.TransactionConfidence; public class AddressConfidenceListener { private final Address address; public AddressConfidenceListener(Address address) { this.address = address; } public Address getAddress() { return address; } @SuppressWarnings("UnusedParameters") public void onTransactionConfidenceChanged(TransactionConfidence confidence) { } } ================================================ FILE: core/src/main/java/haveno/core/xmr/listeners/BalanceListener.java ================================================ /* * 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 . */ package haveno.core.xmr.listeners; import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; public class BalanceListener { private Address address; public BalanceListener() { } public BalanceListener(Address address) { this.address = address; } public Address getAddress() { return address; } @SuppressWarnings("UnusedParameters") public void onBalanceChanged(Coin balance, Transaction tx) { } } ================================================ FILE: core/src/main/java/haveno/core/xmr/listeners/TxConfidenceListener.java ================================================ /* * 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 . */ package haveno.core.xmr.listeners; import org.bitcoinj.core.TransactionConfidence; public class TxConfidenceListener { private final String txID; public TxConfidenceListener(String txID) { this.txID = txID; } public String getTxID() { return txID; } @SuppressWarnings("UnusedParameters") public void onTransactionConfidenceChanged(TransactionConfidence confidence) { } } ================================================ FILE: core/src/main/java/haveno/core/xmr/listeners/XmrBalanceListener.java ================================================ /* * 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 . */ package haveno.core.xmr.listeners; import java.math.BigInteger; public class XmrBalanceListener { private Integer subaddressIndex; public XmrBalanceListener() { } public XmrBalanceListener(Integer subaddressIndex) { this.subaddressIndex = subaddressIndex; } public Integer getSubaddressIndex() { return subaddressIndex; } public void onBalanceChanged(BigInteger balance) { } } ================================================ FILE: core/src/main/java/haveno/core/xmr/model/AddressEntry.java ================================================ /* * 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 . */ package haveno.core.xmr.model; import com.google.protobuf.ByteString; import haveno.common.config.Config; import haveno.common.proto.ProtoUtil; import haveno.common.proto.persistable.PersistablePayload; import haveno.common.util.Utilities; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.script.Script; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; import java.util.Optional; import static com.google.common.base.Preconditions.checkNotNull; /** * Every trade uses a addressEntry with a dedicated address for all transactions related to the trade. * That way we have a kind of separated trade wallet, isolated from other transactions and avoiding coin merge. * If we would not avoid coin merge the user would lose privacy between trades. */ @EqualsAndHashCode @Slf4j public final class AddressEntry implements PersistablePayload { public enum Context { ARBITRATOR, AVAILABLE, OFFER_FUNDING, RESERVED_FOR_TRADE, MULTI_SIG, TRADE_PAYOUT } // keyPair can be null in case the object is created from deserialization as it is transient. // It will be restored when the wallet is ready at setDeterministicKey // So after startup it must never be null @Nullable @Getter private final String offerId; @Getter private final Context context; @Getter private final byte[] pubKey; @Getter private final byte[] pubKeyHash; @Getter private final long coinLockedInMultiSig; @Getter private final boolean segwit; // Not an immutable field. Set at startup once wallet is ready and at encrypting/decrypting wallet. @Nullable transient private DeterministicKey keyPair; // Only used as cache @Nullable transient private Address address; // Only used as cache @Nullable transient private String addressString; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, initialization /////////////////////////////////////////////////////////////////////////////////////////// public AddressEntry(DeterministicKey keyPair, Context context, boolean segwit) { this(keyPair, context, null, segwit); } public AddressEntry(DeterministicKey keyPair, Context context, @Nullable String offerId, boolean segwit) { this(keyPair, context, offerId, 0, segwit); } public AddressEntry(DeterministicKey keyPair, Context context, @Nullable String offerId, long coinLockedInMultiSig, boolean segwit) { this(keyPair.getPubKey(), keyPair.getPubKeyHash(), context, offerId, coinLockedInMultiSig, segwit); this.keyPair = keyPair; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private AddressEntry(byte[] pubKey, byte[] pubKeyHash, Context context, @Nullable String offerId, long coinLockedInMultiSig, boolean segwit) { this.pubKey = pubKey; this.pubKeyHash = pubKeyHash; this.context = context; this.offerId = offerId; this.coinLockedInMultiSig = coinLockedInMultiSig; this.segwit = segwit; } public static AddressEntry fromProto(protobuf.AddressEntry proto) { return new AddressEntry(proto.getPubKey().toByteArray(), proto.getPubKeyHash().toByteArray(), ProtoUtil.enumFromProto(AddressEntry.Context.class, proto.getContext().name()), ProtoUtil.stringOrNullFromProto(proto.getOfferId()), proto.getCoinLockedInMultiSig(), proto.getSegwit()); } @Override public protobuf.AddressEntry toProtoMessage() { protobuf.AddressEntry.Builder builder = protobuf.AddressEntry.newBuilder() .setPubKey(ByteString.copyFrom(pubKey)) .setPubKeyHash(ByteString.copyFrom(pubKeyHash)) .setContext(protobuf.AddressEntry.Context.valueOf(context.name())) .setCoinLockedInMultiSig(coinLockedInMultiSig) .setSegwit(segwit); Optional.ofNullable(offerId).ifPresent(builder::setOfferId); return builder.build(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// // Set after wallet is ready public void setDeterministicKey(DeterministicKey deterministicKey) { this.keyPair = deterministicKey; } // getKeyPair must not be called before wallet is ready (in case we get the object recreated from disk deserialization) // If the object is created at runtime it must be always constructed after wallet is ready. @NotNull public DeterministicKey getKeyPair() { checkNotNull(keyPair, "keyPair must not be null. If we got the addressEntry created from PB we need to have " + "setDeterministicKey got called before any access with getKeyPair()."); return keyPair; } // For display we usually only display the first 8 characters. @Nullable public String getShortOfferId() { return offerId != null ? Utilities.getShortId(offerId) : null; } @Nullable public String getAddressString() { if (addressString == null && getAddress() != null) addressString = getAddress().toString(); return addressString; } @Nullable public Address getAddress() { if (address == null && keyPair != null) { address = Address.fromKey(Config.baseCurrencyNetworkParameters(), keyPair, segwit ? Script.ScriptType.P2WPKH : Script.ScriptType.P2PKH); } if (address == null) { log.warn("Address is null at getAddress(). keyPair={}", keyPair); } return address; } public boolean isAddressNull() { return address == null; } public boolean isOpenOffer() { return context == Context.OFFER_FUNDING || context == Context.RESERVED_FOR_TRADE; } public boolean isTrade() { return context == Context.MULTI_SIG || context == Context.TRADE_PAYOUT; } public Coin getCoinLockedInMultiSigAsCoin() { return Coin.valueOf(coinLockedInMultiSig); } @Override public String toString() { return "AddressEntry{" + "address=" + getAddress() + ", context=" + context + ", offerId='" + offerId + '\'' + ", coinLockedInMultiSig=" + coinLockedInMultiSig + ", segwit=" + segwit + "}"; } } ================================================ FILE: core/src/main/java/haveno/core/xmr/model/AddressEntryList.java ================================================ /* * 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 . */ package haveno.core.xmr.model; import com.google.common.collect.ImmutableList; import com.google.inject.Inject; import com.google.protobuf.Message; import haveno.common.config.Config; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.persistable.PersistableEnvelope; import haveno.common.proto.persistable.PersistedDataHost; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.tuple.Pair; import org.bitcoinj.core.Address; import org.bitcoinj.core.SegwitAddress; import org.bitcoinj.core.Transaction; import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.script.Script; import org.bitcoinj.wallet.Wallet; import java.util.HashSet; import java.util.Objects; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.stream.Collectors; /** * The AddressEntries was previously stored as list, now as hashSet. We still keep the old name to reflect the * associated protobuf message. */ @Slf4j public final class AddressEntryList implements PersistableEnvelope, PersistedDataHost { transient private PersistenceManager persistenceManager; transient private Wallet wallet; private final Set entrySet = new CopyOnWriteArraySet<>(); @Inject public AddressEntryList(PersistenceManager persistenceManager) { this.persistenceManager = persistenceManager; this.persistenceManager.initialize(this, PersistenceManager.Source.PRIVATE); } @Override public void readPersisted(Runnable completeHandler) { persistenceManager.readPersisted(persisted -> { entrySet.clear(); entrySet.addAll(persisted.entrySet); completeHandler.run(); }, completeHandler); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private AddressEntryList(Set entrySet) { this.entrySet.addAll(entrySet); } public static AddressEntryList fromProto(protobuf.AddressEntryList proto) { Set entrySet = proto.getAddressEntryList().stream() .map(AddressEntry::fromProto) .collect(Collectors.toSet()); return new AddressEntryList(entrySet); } @Override public Message toProtoMessage() { Set addressEntries = entrySet.stream() .map(AddressEntry::toProtoMessage) .collect(Collectors.toSet()); return protobuf.PersistableEnvelope.newBuilder() .setAddressEntryList(protobuf.AddressEntryList.newBuilder() .addAllAddressEntry(addressEntries)) .build(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void onWalletReady(Wallet wallet) { this.wallet = wallet; if (!entrySet.isEmpty()) { Set toBeRemoved = new HashSet<>(); entrySet.forEach(addressEntry -> { Script.ScriptType scriptType = addressEntry.isSegwit() ? Script.ScriptType.P2WPKH : Script.ScriptType.P2PKH; DeterministicKey keyFromPubHash = (DeterministicKey) wallet.findKeyFromPubKeyHash( addressEntry.getPubKeyHash(), scriptType); if (keyFromPubHash != null) { Address addressFromKey = Address.fromKey(Config.baseCurrencyNetworkParameters(), keyFromPubHash, scriptType); // We want to ensure key and address matches in case we have address in entry available already if (addressEntry.isAddressNull() || addressFromKey.equals(addressEntry.getAddress())) { addressEntry.setDeterministicKey(keyFromPubHash); } else { log.error("We found an address entry without key but cannot apply the key as the address " + "is not matching. " + "We remove that entry as it seems it is not compatible with our wallet. " + "addressFromKey={}, addressEntry.getAddress()={}", addressFromKey, addressEntry.getAddress()); toBeRemoved.add(addressEntry); } } else { log.error("Key from addressEntry {} not found in that wallet. We remove that entry. " + "This is expected at restore from seeds.", addressEntry.toString()); toBeRemoved.add(addressEntry); } }); toBeRemoved.forEach(entrySet::remove); } else { // As long the old arbitration domain is not removed from the code base we still support it here. DeterministicKey key = (DeterministicKey) wallet.findKeyFromAddress(wallet.freshReceiveAddress(Script.ScriptType.P2PKH)); entrySet.add(new AddressEntry(key, AddressEntry.Context.ARBITRATOR, false)); } // In case we restore from seed words and have balance we need to add the relevant addresses to our list. // IssuedReceiveAddresses does not contain all addresses where we expect balance so we need to listen to // incoming txs at blockchain sync to add the rest. if (wallet.getBalance().isPositive()) { wallet.getIssuedReceiveAddresses().stream() .filter(this::isAddressNotInEntries) .forEach(address -> { DeterministicKey key = (DeterministicKey) wallet.findKeyFromAddress(address); if (key != null) { // Address will be derived from key in getAddress method log.info("Create AddressEntry for IssuedReceiveAddress. address={}", address.toString()); entrySet.add(new AddressEntry(key, AddressEntry.Context.AVAILABLE, address instanceof SegwitAddress)); } else { log.warn("DeterministicKey for address {} is null", address); } }); } // We add those listeners to get notified about potential new transactions and // add an address entry list in case it does not exist yet. This is mainly needed for restore from seed words // but can help as well in case the addressEntry list would miss an address where the wallet was received // funds (e.g. if the user sends funds to an address which has not been provided in the main UI - like from the // wallet details window). wallet.addCoinsReceivedEventListener((wallet1, tx, prevBalance, newBalance) -> { maybeAddNewAddressEntry(tx); }); wallet.addCoinsSentEventListener((wallet1, tx, prevBalance, newBalance) -> { maybeAddNewAddressEntry(tx); }); requestPersistence(); } public ImmutableList getAddressEntriesAsListImmutable() { return ImmutableList.copyOf(entrySet); } public void addAddressEntry(AddressEntry addressEntry) { boolean entryWithSameOfferIdAndContextAlreadyExist = entrySet.stream().anyMatch(e -> { if (addressEntry.getOfferId() != null) { return addressEntry.getOfferId().equals(e.getOfferId()) && addressEntry.getContext() == e.getContext(); } return false; }); if (entryWithSameOfferIdAndContextAlreadyExist) { log.error("We have an address entry with the same offer ID and context. We do not add the new one. " + "addressEntry={}, entrySet={}", addressEntry, entrySet); return; } log.info("addAddressEntry: add new AddressEntry {}", addressEntry); boolean setChangedByAdd = entrySet.add(addressEntry); if (setChangedByAdd) requestPersistence(); } public void swapToAvailable(AddressEntry addressEntry) { if (addressEntry.getContext() == AddressEntry.Context.MULTI_SIG) { log.error("swapToAvailable called with an addressEntry with MULTI_SIG context. " + "This in not permitted as we must not reuse those address entries and there are " + "no redeemable funds on those addresses. " + "Only the keys are used for creating the Multisig address. " + "addressEntry={}", addressEntry); return; } log.info("swapToAvailable addressEntry to swap={}", addressEntry); boolean setChangedByRemove = entrySet.remove(addressEntry); boolean setChangedByAdd = entrySet.add(new AddressEntry(addressEntry.getKeyPair(), AddressEntry.Context.AVAILABLE, addressEntry.isSegwit())); if (setChangedByRemove || setChangedByAdd) { requestPersistence(); } } public AddressEntry swapAvailableToAddressEntryWithOfferId(AddressEntry addressEntry, AddressEntry.Context context, String offerId) { boolean setChangedByRemove = entrySet.remove(addressEntry); AddressEntry newAddressEntry = new AddressEntry(addressEntry.getKeyPair(), context, offerId, addressEntry.isSegwit()); log.info("swapAvailableToAddressEntryWithOfferId newAddressEntry={}", newAddressEntry); boolean setChangedByAdd = entrySet.add(newAddressEntry); if (setChangedByRemove || setChangedByAdd) requestPersistence(); return newAddressEntry; } public void setCoinLockedInMultiSigAddressEntry(AddressEntry addressEntry, long value) { if (addressEntry.getContext() != AddressEntry.Context.MULTI_SIG) { log.error("setCoinLockedInMultiSigAddressEntry must be called only on MULTI_SIG entries"); return; } log.info("setCoinLockedInMultiSigAddressEntry addressEntry={}, value={}", addressEntry, value); boolean setChangedByRemove = entrySet.remove(addressEntry); AddressEntry entry = new AddressEntry(addressEntry.getKeyPair(), addressEntry.getContext(), addressEntry.getOfferId(), value, addressEntry.isSegwit()); boolean setChangedByAdd = entrySet.add(entry); if (setChangedByRemove || setChangedByAdd) { requestPersistence(); } } public void requestPersistence() { persistenceManager.requestPersistence(); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void maybeAddNewAddressEntry(Transaction tx) { tx.getOutputs().stream() .filter(output -> output.isMine(wallet)) .map(output -> output.getScriptPubKey().getToAddress(wallet.getNetworkParameters())) .filter(Objects::nonNull) .filter(this::isAddressNotInEntries) .map(address -> Pair.of(address, (DeterministicKey) wallet.findKeyFromAddress(address))) .filter(pair -> pair.getRight() != null) .map(pair -> new AddressEntry(pair.getRight(), AddressEntry.Context.AVAILABLE, pair.getLeft() instanceof SegwitAddress)) .forEach(this::addAddressEntry); } private boolean isAddressNotInEntries(Address address) { return entrySet.stream().noneMatch(e -> address.equals(e.getAddress())); } @Override public String toString() { return "AddressEntryList{" + ",\n entrySet=" + entrySet + "\n}"; } } ================================================ FILE: core/src/main/java/haveno/core/xmr/model/EncryptedConnectionList.java ================================================ package haveno.core.xmr.model; import com.google.inject.Inject; import com.google.protobuf.ByteString; import com.google.protobuf.Message; import haveno.common.crypto.CryptoException; import haveno.common.crypto.Encryption; import haveno.common.crypto.ScryptUtil; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.persistable.PersistableEnvelope; import haveno.common.proto.persistable.PersistedDataHost; import haveno.core.api.CoreAccountService; import haveno.core.api.model.EncryptedConnection; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Function; import java.util.stream.Collectors; import javax.crypto.SecretKey; import lombok.NonNull; import monero.common.MoneroRpcConnection; import org.bitcoinj.crypto.KeyCrypterScrypt; /** * Store for {@link EncryptedConnection}s. *

    * Passwords are encrypted when stored onto disk, using the account password. * If a connection has no password, this is "hidden" by using some random value as fake password. * * @implNote The password encryption mechanism is handled as follows. * A random salt is generated and stored for each connection. If the connection has no password, * the salt is used as prefix and some random data is attached as fake password. If the connection has a password, * the salt is used as suffix to the actual password. When the password gets decrypted, it is checked whether the * salt is a prefix of the decrypted value. If it is a prefix, the connection has no password. * Otherwise, it is removed (from the end) and the remaining value is the actual password. */ public class EncryptedConnectionList implements PersistableEnvelope, PersistedDataHost { private static final int MIN_FAKE_PASSWORD_LENGTH = 5; private static final int MAX_FAKE_PASSWORD_LENGTH = 32; private static final int SALT_LENGTH = 16; transient private final ReadWriteLock lock = new ReentrantReadWriteLock(); transient private final Lock readLock = lock.readLock(); transient private final Lock writeLock = lock.writeLock(); transient private final SecureRandom random = new SecureRandom(); transient private KeyCrypterScrypt keyCrypterScrypt; transient private SecretKey encryptionKey; transient private CoreAccountService accountService; transient private PersistenceManager persistenceManager; private final Map items = new HashMap<>(); private @NonNull String currentConnectionUrl = ""; private long refreshPeriod; // -1 means no refresh, 0 means default, >0 means custom private boolean autoSwitch = true; @Inject public EncryptedConnectionList(PersistenceManager persistenceManager, CoreAccountService accountService) { this.accountService = accountService; this.persistenceManager = persistenceManager; this.persistenceManager.initialize(this, "EncryptedConnectionList", PersistenceManager.Source.PRIVATE); } private EncryptedConnectionList(byte[] salt, List items, @NonNull String currentConnectionUrl, long refreshPeriod, boolean autoSwitch) { this.keyCrypterScrypt = ScryptUtil.getKeyCrypterScrypt(salt); this.items.putAll(items.stream().collect(Collectors.toMap(EncryptedConnection::getUrl, Function.identity()))); this.currentConnectionUrl = currentConnectionUrl; this.refreshPeriod = refreshPeriod; this.autoSwitch = autoSwitch; } @Override public void readPersisted(Runnable completeHandler) { persistenceManager.readPersisted(persistedEncryptedConnectionList -> { writeLock.lock(); try { initializeEncryption(persistedEncryptedConnectionList.keyCrypterScrypt); items.clear(); items.putAll(persistedEncryptedConnectionList.items); currentConnectionUrl = persistedEncryptedConnectionList.currentConnectionUrl; refreshPeriod = persistedEncryptedConnectionList.refreshPeriod; autoSwitch = persistedEncryptedConnectionList.autoSwitch; } catch (Exception e) { e.printStackTrace(); } finally { writeLock.unlock(); } completeHandler.run(); }, () -> { writeLock.lock(); try { initializeEncryption(ScryptUtil.getKeyCrypterScrypt()); } catch (Exception e) { e.printStackTrace(); } finally { writeLock.unlock(); } completeHandler.run(); }); } private void initializeEncryption(KeyCrypterScrypt keyCrypterScrypt) { this.keyCrypterScrypt = keyCrypterScrypt; encryptionKey = toSecretKey(accountService.getPassword()); } public List getConnections() { readLock.lock(); try { return items.values().stream().map(this::toMoneroRpcConnection).collect(Collectors.toList()); } finally { readLock.unlock(); } } public boolean hasConnection(String connection) { readLock.lock(); try { return items.containsKey(connection); } finally { readLock.unlock(); } } public void addConnection(MoneroRpcConnection connection) { EncryptedConnection currentValue; writeLock.lock(); try { EncryptedConnection encryptedConnection = toEncryptedConnection(connection); currentValue = items.putIfAbsent(connection.getUri(), encryptedConnection); } finally { writeLock.unlock(); } if (currentValue != null) { throw new IllegalStateException(String.format("There exists already a connection for \"%s\"", connection.getUri())); } requestPersistence(); } public void removeConnection(String connection) { writeLock.lock(); try { items.remove(connection); } finally { writeLock.unlock(); } requestPersistence(); } public void setAutoSwitch(boolean autoSwitch) { boolean changed; writeLock.lock(); try { changed = this.autoSwitch != (this.autoSwitch = autoSwitch); } finally { writeLock.unlock(); } if (changed) { requestPersistence(); } } public boolean getAutoSwitch() { readLock.lock(); try { return autoSwitch; } finally { readLock.unlock(); } } public void setRefreshPeriod(Long refreshPeriod) { boolean changed; writeLock.lock(); try { changed = this.refreshPeriod != (this.refreshPeriod = refreshPeriod == null ? 0L : refreshPeriod); } finally { writeLock.unlock(); } if (changed) { requestPersistence(); } } public long getRefreshPeriod() { readLock.lock(); try { return refreshPeriod; } finally { readLock.unlock(); } } public void setCurrentConnectionUri(String currentConnectionUrl) { boolean changed; writeLock.lock(); try { changed = !this.currentConnectionUrl.equals(this.currentConnectionUrl = currentConnectionUrl == null ? "" : currentConnectionUrl); } finally { writeLock.unlock(); } if (changed) { if (!PersistenceManager.allServicesInitialized.get()) { persistenceManager.forcePersistNow(); // connection can be changed before all services initialized } else { requestPersistence(); } } } public Optional getCurrentConnectionUri() { readLock.lock(); try { return Optional.of(currentConnectionUrl).filter(s -> !s.isEmpty()); } finally { readLock.unlock(); } } public void requestPersistence() { persistenceManager.requestPersistence(); } @Override public Message toProtoMessage() { List connections; ByteString saltString; String currentConnectionUrl; boolean autoSwitchEnabled; long refreshPeriod; readLock.lock(); try { connections = items.values().stream() .map(EncryptedConnection::toProtoMessage).collect(Collectors.toList()); saltString = keyCrypterScrypt.getScryptParameters().getSalt(); currentConnectionUrl = this.currentConnectionUrl; autoSwitchEnabled = this.autoSwitch; refreshPeriod = this.refreshPeriod; } finally { readLock.unlock(); } return protobuf.PersistableEnvelope.newBuilder() .setEncryptedConnectionList(protobuf.EncryptedConnectionList.newBuilder() .setSalt(saltString) .addAllItems(connections) .setCurrentConnectionUrl(currentConnectionUrl) .setRefreshPeriod(refreshPeriod) .setAutoSwitch(autoSwitchEnabled)) .build(); } public static EncryptedConnectionList fromProto(protobuf.EncryptedConnectionList proto) { List items = proto.getItemsList().stream() .map(EncryptedConnection::fromProto) .collect(Collectors.toList()); return new EncryptedConnectionList(proto.getSalt().toByteArray(), items, proto.getCurrentConnectionUrl(), proto.getRefreshPeriod(), proto.getAutoSwitch()); } // ----------------------------- HELPERS ---------------------------------- public void changePassword(String oldPassword, String newPassword) { writeLock.lock(); try { SecretKey oldSecret = encryptionKey; assert Objects.equals(oldSecret, toSecretKey(oldPassword)) : "Old secret does not match old password"; encryptionKey = toSecretKey(newPassword); items.replaceAll((key, connection) -> reEncrypt(connection, oldSecret, encryptionKey)); } finally { writeLock.unlock(); } requestPersistence(); } private SecretKey toSecretKey(String password) { if (password == null) return null; return Encryption.getSecretKeyFromBytes(keyCrypterScrypt.deriveKey(password).getKey()); } private static EncryptedConnection reEncrypt(EncryptedConnection connection, SecretKey oldSecret, SecretKey newSecret) { return connection.toBuilder() .encryptedPassword(reEncrypt(connection.getEncryptedPassword(), oldSecret, newSecret)) .build(); } private static byte[] reEncrypt(byte[] value, SecretKey oldSecret, SecretKey newSecret) { // was previously not encrypted if null byte[] decrypted = oldSecret == null ? value : decrypt(value, oldSecret); // should not be encrypted if null return newSecret == null ? decrypted : encrypt(decrypted, newSecret); } private static byte[] decrypt(byte[] encrypted, SecretKey secret) { if (secret == null) return encrypted; // no encryption try { return Encryption.decrypt(encrypted, secret); } catch (CryptoException e) { throw new IllegalArgumentException("Incorrect password", e); } } private static byte[] encrypt(byte[] unencrypted, SecretKey secretKey) { if (secretKey == null) return unencrypted; // no encryption try { return Encryption.encrypt(unencrypted, secretKey); } catch (CryptoException e) { throw new RuntimeException("Could not encrypt data with the provided secret", e); } } private EncryptedConnection toEncryptedConnection(MoneroRpcConnection connection) { String password = connection.getPassword(); byte[] passwordBytes = password == null ? null : password.getBytes(StandardCharsets.UTF_8); byte[] passwordSalt = generateSalt(passwordBytes); byte[] encryptedPassword = encryptPassword(passwordBytes, passwordSalt); return EncryptedConnection.builder() .url(connection.getUri()) .username(connection.getUsername() == null ? "" : connection.getUsername()) .encryptedPassword(encryptedPassword) .encryptionSalt(passwordSalt) .priority(connection.getPriority()) .build(); } private MoneroRpcConnection toMoneroRpcConnection(EncryptedConnection connection) { byte[] decryptedPasswordBytes = decryptPassword(connection.getEncryptedPassword(), connection.getEncryptionSalt()); String password = decryptedPasswordBytes == null ? null : new String(decryptedPasswordBytes, StandardCharsets.UTF_8); String username = connection.getUsername().isEmpty() ? null : connection.getUsername(); MoneroRpcConnection moneroRpcConnection = new MoneroRpcConnection(connection.getUrl(), username, password); moneroRpcConnection.setPriority(connection.getPriority()); return moneroRpcConnection; } private byte[] encryptPassword(byte[] password, byte[] salt) { byte[] saltedPassword; if (password == null) { // no password given, so use salt as prefix and add some random data, which disguises itself as password int fakePasswordLength = random.nextInt(MAX_FAKE_PASSWORD_LENGTH - MIN_FAKE_PASSWORD_LENGTH + 1) + MIN_FAKE_PASSWORD_LENGTH; byte[] fakePassword = new byte[fakePasswordLength]; random.nextBytes(fakePassword); saltedPassword = new byte[salt.length + fakePasswordLength]; System.arraycopy(salt, 0, saltedPassword, 0, salt.length); System.arraycopy(fakePassword, 0, saltedPassword, salt.length, fakePassword.length); } else { // password given, so append salt to end saltedPassword = new byte[password.length + salt.length]; System.arraycopy(password, 0, saltedPassword, 0, password.length); System.arraycopy(salt, 0, saltedPassword, password.length, salt.length); } return encrypt(saltedPassword, encryptionKey); } private byte[] decryptPassword(byte[] encryptedSaltedPassword, byte[] salt) { byte[] decryptedSaltedPassword = decrypt(encryptedSaltedPassword, encryptionKey); if (arrayStartsWith(decryptedSaltedPassword, salt)) { // salt is prefix, so no actual password set return null; } else { // remove salt suffix, the rest is the actual password byte[] decryptedPassword = new byte[decryptedSaltedPassword.length - salt.length]; System.arraycopy(decryptedSaltedPassword, 0, decryptedPassword, 0, decryptedPassword.length); return decryptedPassword; } } private byte[] generateSalt(byte[] password) { byte[] salt = new byte[SALT_LENGTH]; // Generate salt, that is guaranteed to be no prefix of the password do { random.nextBytes(salt); } while (password != null && arrayStartsWith(password, salt)); return salt; } private static boolean arrayStartsWith(byte[] container, byte[] prefix) { if (container.length < prefix.length) { return false; } for (int i = 0; i < prefix.length; i++) { if (container[i] != prefix[i]) { return false; } } return true; } } ================================================ FILE: core/src/main/java/haveno/core/xmr/model/InputsAndChangeOutput.java ================================================ /* * 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 . */ package haveno.core.xmr.model; import javax.annotation.Nullable; import java.util.List; import static com.google.common.base.Preconditions.checkArgument; public class InputsAndChangeOutput { public final List rawTransactionInputs; // Is set to 0L in case we don't have an output public final long changeOutputValue; @Nullable public final String changeOutputAddress; public InputsAndChangeOutput(List rawTransactionInputs, long changeOutputValue, @Nullable String changeOutputAddress) { checkArgument(!rawTransactionInputs.isEmpty(), "rawInputs.isEmpty()"); this.rawTransactionInputs = rawTransactionInputs; this.changeOutputValue = changeOutputValue; this.changeOutputAddress = changeOutputAddress; } } ================================================ FILE: core/src/main/java/haveno/core/xmr/model/PreparedDepositTxAndMakerInputs.java ================================================ /* * 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 . */ package haveno.core.xmr.model; import java.util.ArrayList; public class PreparedDepositTxAndMakerInputs { public final ArrayList rawMakerInputs; public final byte[] depositTransaction; public PreparedDepositTxAndMakerInputs(ArrayList rawMakerInputs, byte[] depositTransaction) { this.rawMakerInputs = rawMakerInputs; this.depositTransaction = depositTransaction; } } ================================================ FILE: core/src/main/java/haveno/core/xmr/model/RawTransactionInput.java ================================================ /* * 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 . */ package haveno.core.xmr.model; import com.google.protobuf.ByteString; import haveno.common.proto.network.NetworkPayload; import haveno.common.proto.persistable.PersistablePayload; import haveno.common.util.Utilities; import lombok.EqualsAndHashCode; import javax.annotation.concurrent.Immutable; @EqualsAndHashCode @Immutable public final class RawTransactionInput implements NetworkPayload, PersistablePayload { public final long index; // Index of spending txo public final byte[] parentTransaction; // Spending tx (fromTx) public final long value; /** * Holds the relevant data for the connected output for a tx input. * @param index the index of the parentTransaction * @param parentTransaction the spending output tx, not the parent tx of the input * @param value the number of satoshis being spent */ public RawTransactionInput(long index, byte[] parentTransaction, long value) { this.index = index; this.parentTransaction = parentTransaction; this.value = value; } @Override public protobuf.RawTransactionInput toProtoMessage() { return protobuf.RawTransactionInput.newBuilder() .setIndex(index) .setParentTransaction(ByteString.copyFrom(parentTransaction)) .setValue(value) .build(); } public static RawTransactionInput fromProto(protobuf.RawTransactionInput proto) { return new RawTransactionInput(proto.getIndex(), proto.getParentTransaction().toByteArray(), proto.getValue()); } @Override public String toString() { return "RawTransactionInput{" + "index=" + index + ", parentTransaction as HEX " + Utilities.bytesAsHexString(parentTransaction) + ", value=" + value + '}'; } } ================================================ FILE: core/src/main/java/haveno/core/xmr/model/XmrAddressEntry.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.xmr.model; import haveno.common.proto.ProtoUtil; import haveno.common.proto.persistable.PersistablePayload; import haveno.common.util.Utilities; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.Coin; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; import java.util.Optional; /** * Every trade uses a XmrAddressEntry with a dedicated address for all transactions related to the trade. * That way we have a kind of separated trade wallet, isolated from other transactions and avoiding coin merge. * If we would not avoid coin merge the user would lose privacy between trades. */ @EqualsAndHashCode @Slf4j public final class XmrAddressEntry implements PersistablePayload { public enum Context { ARBITRATOR, BASE_ADDRESS, AVAILABLE, OFFER_FUNDING, TRADE_PAYOUT; } // keyPair can be null in case the object is created from deserialization as it is transient. // It will be restored when the wallet is ready at setDeterministicKey // So after startup it must never be null @Nullable @Getter private final String offerId; @Getter private final Context context; @Getter private final int subaddressIndex; @Getter private final String addressString; private long coinLockedInMultiSig; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, initialization /////////////////////////////////////////////////////////////////////////////////////////// public XmrAddressEntry(int subaddressIndex, String address, Context context) { this(subaddressIndex, address, context, null, null); } public XmrAddressEntry(int subaddressIndex, String address, Context context, @Nullable String offerId, Coin coinLockedInMultiSig) { this.subaddressIndex = subaddressIndex; this.addressString = address; this.offerId = offerId; this.context = context; if (coinLockedInMultiSig != null) this.coinLockedInMultiSig = coinLockedInMultiSig.value; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// public static XmrAddressEntry fromProto(protobuf.XmrAddressEntry proto) { return new XmrAddressEntry(proto.getSubaddressIndex(), ProtoUtil.stringOrNullFromProto(proto.getAddressString()), ProtoUtil.enumFromProto(XmrAddressEntry.Context.class, proto.getContext().name()), ProtoUtil.stringOrNullFromProto(proto.getOfferId()), Coin.valueOf(proto.getCoinLockedInMultiSig())); } @Override public protobuf.XmrAddressEntry toProtoMessage() { protobuf.XmrAddressEntry.Builder builder = protobuf.XmrAddressEntry.newBuilder() .setSubaddressIndex(subaddressIndex) .setAddressString(addressString) .setContext(protobuf.XmrAddressEntry.Context.valueOf(context.name())) .setCoinLockedInMultiSig(coinLockedInMultiSig); Optional.ofNullable(offerId).ifPresent(builder::setOfferId); return builder.build(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void setCoinLockedInMultiSig(@NotNull Coin coinLockedInMultiSig) { this.coinLockedInMultiSig = coinLockedInMultiSig.value; } // For display we usually only display the first 8 characters. @Nullable public String getShortOfferId() { return offerId != null ? Utilities.getShortId(offerId) : null; } public boolean isOpenOffer() { return context == Context.OFFER_FUNDING; } public boolean isTradePayout() { return context == Context.TRADE_PAYOUT; } public boolean isTradable() { return isOpenOffer() || isTradePayout(); } public Coin getCoinLockedInMultiSig() { return Coin.valueOf(coinLockedInMultiSig); } @Override public String toString() { return "XmrAddressEntry{" + "offerId='" + getOfferId() + '\'' + ", context=" + context + ", subaddressIndex=" + getSubaddressIndex() + ", address=" + getAddressString() + '}'; } } ================================================ FILE: core/src/main/java/haveno/core/xmr/model/XmrAddressEntryList.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.xmr.model; import com.google.common.collect.ImmutableList; import com.google.inject.Inject; import com.google.protobuf.Message; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.persistable.PersistableEnvelope; import haveno.common.proto.persistable.PersistedDataHost; import lombok.extern.slf4j.Slf4j; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.stream.Collectors; /** * The AddressEntries was previously stored as list, now as hashSet. We still keep the old name to reflect the * associated protobuf message. */ @Slf4j public final class XmrAddressEntryList implements PersistableEnvelope, PersistedDataHost { transient private PersistenceManager persistenceManager; private final Set entrySet = new CopyOnWriteArraySet<>(); @Inject public XmrAddressEntryList(PersistenceManager persistenceManager) { this.persistenceManager = persistenceManager; this.persistenceManager.initialize(this, PersistenceManager.Source.PRIVATE); } @Override public void readPersisted(Runnable completeHandler) { persistenceManager.readPersisted(persisted -> { entrySet.clear(); entrySet.addAll(persisted.entrySet); completeHandler.run(); }, completeHandler); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private XmrAddressEntryList(Set entrySet) { this.entrySet.addAll(entrySet); } public static XmrAddressEntryList fromProto(protobuf.XmrAddressEntryList proto) { Set entrySet = proto.getXmrAddressEntryList().stream() .map(XmrAddressEntry::fromProto) .collect(Collectors.toSet()); return new XmrAddressEntryList(entrySet); } @Override public Message toProtoMessage() { Set addressEntries = entrySet.stream() .map(XmrAddressEntry::toProtoMessage) .collect(Collectors.toSet()); return protobuf.PersistableEnvelope.newBuilder() .setXmrAddressEntryList(protobuf.XmrAddressEntryList.newBuilder() .addAllXmrAddressEntry(addressEntries)) .build(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public ImmutableList getAddressEntriesAsListImmutable() { return ImmutableList.copyOf(entrySet); } public boolean addAddressEntry(XmrAddressEntry addressEntry) { boolean entryWithSameOfferIdAndContextAlreadyExist = entrySet.stream().anyMatch(e -> { if (addressEntry.getOfferId() != null) { return addressEntry.getOfferId().equals(e.getOfferId()) && addressEntry.getContext() == e.getContext(); } return false; }); if (entryWithSameOfferIdAndContextAlreadyExist) { throw new IllegalArgumentException("We have an address entry with the same offer ID and context. We do not add the new one. addressEntry=" + addressEntry); } boolean setChangedByAdd = entrySet.add(addressEntry); if (setChangedByAdd) requestPersistence(); return setChangedByAdd; } public void swapToAvailable(XmrAddressEntry addressEntry) { log.info("swapToAvailable addressEntry to swap={}", addressEntry); if (entrySet.remove(addressEntry)) { requestPersistence(); } // If we have an address entry which shared the address with another one (shared funding use case) // then we do not swap to available as we need to protect the address of the remaining entry. boolean entryWithSameContextStillExists = entrySet.stream().anyMatch(entry -> { if (addressEntry.getAddressString() != null) { return addressEntry.getAddressString().equals(entry.getAddressString()) && addressEntry.getContext() == entry.getContext(); } return false; }); if (entryWithSameContextStillExists) { return; } // no other uses of the address context remain, so make it available if (entrySet.add(new XmrAddressEntry(addressEntry.getSubaddressIndex(), addressEntry.getAddressString(), XmrAddressEntry.Context.AVAILABLE))) { requestPersistence(); } } public XmrAddressEntry swapAvailableToAddressEntryWithOfferId(XmrAddressEntry addressEntry, XmrAddressEntry.Context context, String offerId) { // remove old entry boolean setChangedByRemove = entrySet.remove(addressEntry); // add new entry final XmrAddressEntry newAddressEntry = new XmrAddressEntry(addressEntry.getSubaddressIndex(), addressEntry.getAddressString(), context, offerId, null); boolean setChangedByAdd = false; try { setChangedByAdd = addAddressEntry(newAddressEntry); } catch (Exception e) { entrySet.add(addressEntry); // undo change if error throw e; } if (setChangedByRemove || setChangedByAdd) requestPersistence(); return newAddressEntry; } public void clear() { entrySet.clear(); requestPersistence(); } public void requestPersistence() { persistenceManager.requestPersistence(); } @Override public String toString() { return "XmrAddressEntryList{" + ",\n entrySet=" + entrySet + "\n}"; } } ================================================ FILE: core/src/main/java/haveno/core/xmr/nodes/ProxySocketFactory.java ================================================ /* * 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 . */ /** * Copyright (C) 2010-2014 Leon Blakey *

    * This file is part of PircBotX. *

    * PircBotX is free software: you can redistribute it and/or modify it under the * terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later * version. *

    * PircBotX 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 General Public License for more details. *

    * You should have received a copy of the GNU General Public License along with * PircBotX. If not, see . */ package haveno.core.xmr.nodes; import javax.net.SocketFactory; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.Socket; /** * A basic SocketFactory for creating sockets that connect through the specified * proxy. * * @author Leon Blakey */ public class ProxySocketFactory extends SocketFactory { protected final Proxy proxy; /** * Create all sockets with the specified proxy. * * @param proxy An existing proxy */ public ProxySocketFactory(Proxy proxy) { this.proxy = proxy; } /** * A convenience constructor for creating a proxy with the specified host * and port. * * @param proxyType The type of proxy were connecting to * @param hostname The hostname of the proxy server * @param port The port of the proxy server */ public ProxySocketFactory(Proxy.Type proxyType, String hostname, int port) { this.proxy = new Proxy(proxyType, new InetSocketAddress(hostname, port)); } @Override public Socket createSocket() throws IOException { Socket socket = new Socket(proxy); return socket; } @Override public Socket createSocket(String string, int i) throws IOException { Socket socket = new Socket(proxy); socket.connect(new InetSocketAddress(string, i)); return socket; } @Override public Socket createSocket(String string, int i, InetAddress localAddress, int localPort) throws IOException { Socket socket = new Socket(proxy); socket.bind(new InetSocketAddress(localAddress, localPort)); socket.connect(new InetSocketAddress(string, i)); return socket; } @Override public Socket createSocket(InetAddress ia, int i) throws IOException { Socket socket = new Socket(proxy); socket.connect(new InetSocketAddress(ia, i)); return socket; } @Override public Socket createSocket(InetAddress ia, int i, InetAddress localAddress, int localPort) throws IOException { Socket socket = new Socket(proxy); socket.bind(new InetSocketAddress(localAddress, localPort)); socket.connect(new InetSocketAddress(ia, i)); return socket; } } ================================================ FILE: core/src/main/java/haveno/core/xmr/nodes/SeedPeersSocks5Dns.java ================================================ /* * 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 . */ /** * Copyright 2011 Micheal Swiggs *

    * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

    * http://www.apache.org/licenses/LICENSE-2.0 *

    * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package haveno.core.xmr.nodes; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import com.runjva.sourceforge.jsocks.protocol.SocksSocket; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.net.discovery.PeerDiscovery; import org.bitcoinj.net.discovery.PeerDiscoveryException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.concurrent.TimeUnit; // TODO not used anymore. Not sure if it was replaced by something else or removed by accident. /** * SeedPeersSocks5Dns resolves peers via Proxy (Socks5) remote DNS. */ public class SeedPeersSocks5Dns implements PeerDiscovery { private final Socks5Proxy proxy; private final NetworkParameters params; private final InetSocketAddress[] seedAddrs; @SuppressWarnings("MismatchedReadAndWriteOfArray") private InetSocketAddress[] seedAddrsIP; private int pnseedIndex; private final InetSocketAddress[] seedAddrsResolved; private static final Logger log = LoggerFactory.getLogger(SeedPeersSocks5Dns.class); /** * Supports finding peers by hostname over a socks5 proxy. */ public SeedPeersSocks5Dns(Socks5Proxy proxy, NetworkParameters params) { this.proxy = proxy; this.params = params; this.seedAddrs = convertAddrsString(params.getDnsSeeds(), params.getPort()); /* // This is an example of how .onion servers could be used. Unfortunately there is presently no way // to hand the onion address (or a connected socket) back to bitcoinj without it crashing in PeerAddress. // note: the onion addresses should be added into bitcoinj NetworkParameters classes, eg for mainnet, testnet // not here! this.seedAddrs = new InetSocketAddress[]{InetSocketAddress.createUnresolved("cajrifqkvalh2ooa.onion", 8333), InetSocketAddress.createUnresolved("bk7yp6epnmcllq72.onion", 8333) }; */ //TODO seedAddrsIP is never written; not used method... seedAddrsResolved = new InetSocketAddress[seedAddrs.length]; System.arraycopy(seedAddrsIP, seedAddrs.length, seedAddrsResolved, seedAddrs.length, seedAddrsResolved.length - seedAddrs.length); } /** * Acts as an iterator, returning the address of each node in the list sequentially. * Once all the list has been iterated, null will be returned for each subsequent query. * * @return InetSocketAddress - The address/port of the next node. * @throws PeerDiscoveryException */ @Nullable public InetSocketAddress getPeer() throws PeerDiscoveryException { try { return nextPeer(); } catch (PeerDiscoveryException e) { throw new PeerDiscoveryException(e); } } /** * worker for getPeer() */ @Nullable private InetSocketAddress nextPeer() throws PeerDiscoveryException { if (seedAddrs == null || seedAddrs.length == 0) { throw new PeerDiscoveryException("No IP address seeds configured; unable to find any peers"); } if (pnseedIndex >= seedAddrsResolved.length) { return null; } if (seedAddrsResolved[pnseedIndex] == null) { seedAddrsResolved[pnseedIndex] = lookup(proxy, seedAddrs[pnseedIndex]); } log.error("SeedPeersSocks5Dns::nextPeer: " + seedAddrsResolved[pnseedIndex]); return seedAddrsResolved[pnseedIndex++]; } /** * Returns an array containing all the Bitcoin nodes within the list. */ @Override public InetSocketAddress[] getPeers(long services, long timeoutValue, TimeUnit timeoutUnit) throws PeerDiscoveryException { if (services != 0) throw new PeerDiscoveryException("DNS seeds cannot filter by services: " + services); return allPeers(); } /** * returns all seed peers, performs hostname lookups if necessary. */ private InetSocketAddress[] allPeers() { for (int i = 0; i < seedAddrsResolved.length; ++i) { if (seedAddrsResolved[i] == null) { seedAddrsResolved[i] = lookup(proxy, seedAddrs[i]); } } return seedAddrsResolved; } /** * Resolves a hostname via remote DNS over socks5 proxy. */ @Nullable public static InetSocketAddress lookup(Socks5Proxy proxy, InetSocketAddress addr) { if (!addr.isUnresolved()) { return addr; } try { SocksSocket proxySocket = new SocksSocket(proxy, addr.getHostString(), addr.getPort()); InetAddress addrResolved = proxySocket.getInetAddress(); proxySocket.close(); if (addrResolved != null) { //log.debug("Resolved " + addr.getHostString() + " to " + addrResolved.getHostAddress()); return new InetSocketAddress(addrResolved, addr.getPort()); } else { // note: .onion nodes fall in here when proxy is Tor. But they have no IP address. // Unfortunately bitcoinj crashes in PeerAddress if it finds an unresolved address. log.error("Connected to " + addr.getHostString() + ". But did not resolve to address."); } } catch (Exception e) { log.warn("Error resolving " + addr.getHostString() + ". Exception:\n" + e.toString()); } return null; } /** * Converts an array of hostnames to array of unresolved InetSocketAddress */ private InetSocketAddress[] convertAddrsString(String[] addrs, int port) { InetSocketAddress[] list = new InetSocketAddress[addrs.length]; for (int i = 0; i < addrs.length; i++) { list[i] = InetSocketAddress.createUnresolved(addrs[i], port); } return list; } @Override public void shutdown() { } } ================================================ FILE: core/src/main/java/haveno/core/xmr/nodes/XmrNetworkConfig.java ================================================ /* * 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 . */ package haveno.core.xmr.nodes; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import haveno.common.config.Config; import haveno.core.xmr.setup.WalletConfig; import haveno.network.Socks5MultiDiscovery; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.PeerAddress; import org.bitcoinj.params.MainNetParams; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.util.List; public class XmrNetworkConfig { private static final Logger log = LoggerFactory.getLogger(XmrNetworkConfig.class); @Nullable private final Socks5Proxy proxy; private final WalletConfig delegate; private final NetworkParameters parameters; private final int socks5DiscoverMode; public XmrNetworkConfig(WalletConfig delegate, NetworkParameters parameters, int socks5DiscoverMode, @Nullable Socks5Proxy proxy) { this.delegate = delegate; this.parameters = parameters; this.socks5DiscoverMode = socks5DiscoverMode; this.proxy = proxy; } public void proposePeers(List peers) { if (!peers.isEmpty()) { log.info("You connect with peerAddresses: {}", peers); PeerAddress[] peerAddresses = peers.toArray(new PeerAddress[peers.size()]); delegate.setPeerNodes(peerAddresses); } else if (proxy != null) { if (log.isWarnEnabled()) { MainNetParams mainNetParams = MainNetParams.get(); if (parameters.equals(mainNetParams)) { log.warn("You use the public Bitcoin network and are exposed to privacy issues " + "caused by the broken bloom filters. See https://haveno.exchange/blog/privacy-in-bitsquare/ " + "for more info. It is recommended to use the provided nodes."); } } // SeedPeers uses hard coded stable addresses (from MainNetParams). It should be updated from time to time. delegate.setDiscovery(new Socks5MultiDiscovery(proxy, parameters, socks5DiscoverMode)); } else if (Config.baseCurrencyNetwork().isMainnet()) { log.warn("You don't use tor and use the public Bitcoin network and are exposed to privacy issues " + "caused by the broken bloom filters. See https://haveno.exchange/blog/privacy-in-bitsquare/ " + "for more info. It is recommended to use Tor and the provided nodes."); } } } ================================================ FILE: core/src/main/java/haveno/core/xmr/nodes/XmrNodeConverter.java ================================================ /* * 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 . */ package haveno.core.xmr.nodes; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import haveno.core.xmr.nodes.XmrNodes.XmrNode; import haveno.network.DnsLookupException; import haveno.network.DnsLookupTor; import org.bitcoinj.core.PeerAddress; import org.bitcoinj.net.OnionCatConverter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.util.Objects; class XmrNodeConverter { private static final Logger log = LoggerFactory.getLogger(XmrNodeConverter.class); private final Facade facade; XmrNodeConverter() { this.facade = new Facade(); } XmrNodeConverter(Facade facade) { this.facade = facade; } @Nullable PeerAddress convertOnionHost(XmrNode node) { // no DNS lookup for onion addresses String onionAddress = Objects.requireNonNull(node.getOnionAddress()); return new PeerAddress(onionAddress, node.getPort()); } @Nullable PeerAddress convertClearNode(XmrNode node) { int port = node.getPort(); PeerAddress result = create(node.getHostNameOrAddress(), port); if (result == null) { String address = node.getAddress(); if (address != null) { result = create(address, port); } else { log.warn("Lookup failed, no address for node {}", node); } } return result; } @Nullable PeerAddress convertWithTor(XmrNode node, Socks5Proxy proxy) { int port = node.getPort(); PeerAddress result = create(proxy, node.getHostNameOrAddress(), port); if (result == null) { String address = node.getAddress(); if (address != null) { result = create(proxy, address, port); } else { log.warn("Lookup failed, no address for node {}", node); } } return result; } @Nullable private PeerAddress create(Socks5Proxy proxy, String host, int port) { try { // We use DnsLookupTor to not leak with DNS lookup // Blocking call. takes about 600 ms ;-( InetAddress lookupAddress = facade.torLookup(proxy, host); InetSocketAddress address = new InetSocketAddress(lookupAddress, port); return new PeerAddress(address); } catch (Exception e) { log.error("Failed to create peer address", e); return null; } } @Nullable private static PeerAddress create(String hostName, int port) { try { InetSocketAddress address = new InetSocketAddress(hostName, port); return new PeerAddress(address); } catch (Exception e) { log.error("Failed to create peer address", e); return null; } } static class Facade { InetAddress onionHostToInetAddress(String onionAddress) throws UnknownHostException { return OnionCatConverter.onionHostToInetAddress(onionAddress); } InetAddress torLookup(Socks5Proxy proxy, String host) throws DnsLookupException { return DnsLookupTor.lookup(proxy, host); } } } ================================================ FILE: core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.xmr.nodes; import haveno.common.config.Config; import haveno.core.trade.HavenoUtils; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkArgument; @Slf4j public class XmrNodes { // TODO: rename to XmrNodeType ? public enum MoneroNodesOption { PROVIDED, CUSTOM, PUBLIC } public List selectPreferredNodes(XmrNodesSetupPreferences xmrNodesSetupPreferences) { return xmrNodesSetupPreferences.selectPreferredNodes(this); } // TODO: always using null hostname public List getAllXmrNodes() { switch (Config.baseCurrencyNetwork()) { case XMR_LOCAL: return Arrays.asList( new XmrNode(MoneroNodesOption.PROVIDED, null, null, "127.0.0.1", 28081, 1, "@local") ); case XMR_STAGENET: return Arrays.asList( new XmrNode(MoneroNodesOption.PROVIDED, null, null, "127.0.0.1", 38081, 1, "@local"), new XmrNode(MoneroNodesOption.PROVIDED, null, null, "127.0.0.1", 39081, 1, "@local"), new XmrNode(MoneroNodesOption.PROVIDED, null, null, "45.63.8.26", 38081, 2, "@haveno"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node.sethforprivacy.com", 38089, 3, null), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node2.sethforprivacy.com", 38089, 3, null), new XmrNode(MoneroNodesOption.PUBLIC, null, "plowsof3t5hogddwabaeiyrno25efmzfxyro2vligremt7sxpsclfaid.onion", null, 38089, 3, null) ); case XMR_MAINNET: return Arrays.asList( new XmrNode(MoneroNodesOption.PUBLIC, null, null, "127.0.0.1", 18081, 1, "@local"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "xmr-node.cakewallet.com", 18081, 2, "@cakewallet"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "p2pmd.xmrvsbeast.com", 18081, 2, "@xmrvsbeast"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node.monerodevs.org", 18089, 2, "@monerodevs.org"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node2.monerodevs.org", 18089, 2, "@monerodevs.org"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node3.monerodevs.org", 18089, 2, "@monerodevs.org"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "nodex.monerujo.io", 18081, 2, "@monerujo.io"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "rucknium.me", 18081, 2, "@Rucknium"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node.sethforprivacy.com", 18089, 2, "@sethforprivacy"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "selsta1.featherwallet.net", 18081, 2, "@selsta"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "selsta2.featherwallet.net", 18081, 2, "@selsta"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node.xmr.ru", 18081, 2, "@xmr.ru"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "xmr.stormycloud.org", 18089, 2, "@stormycloud"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "ravfx.its-a-node.org", 18081, 2, "@ravfx"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "ravfx2.its-a-node.org", 18089, 2, "@ravfx") // new XmrNode(MoneroNodesOption.PUBLIC, null, "plowsof3t5hogddwabaeiyrno25efmzfxyro2vligremt7sxpsclfaid.onion", null, 18089, 3, "@plowsof"), // onions tend to have poorer performance // new XmrNode(MoneroNodesOption.PUBLIC, null, "cakexmrl7bonq7ovjka5kuwuyd3f7qnkz6z6s6dmsy3uckwra7bvggyd.onion", null, 18081, 3, "@cakewallet") ); default: throw new IllegalStateException("Unexpected base currency network: " + Config.baseCurrencyNetwork()); } } public List getProvidedXmrNodes() { return getXmrNodes(MoneroNodesOption.PROVIDED); } public List getPublicXmrNodes() { return getXmrNodes(MoneroNodesOption.PUBLIC); } public List getCustomXmrNodes() { return getXmrNodes(MoneroNodesOption.CUSTOM); } private List getXmrNodes(MoneroNodesOption type) { List nodes = new ArrayList<>(); for (XmrNode node : getAllXmrNodes()) if (node.getType() == type) nodes.add(node); return nodes; } public static List toCustomXmrNodesList(Collection nodes) { return nodes.stream() .filter(e -> !e.isEmpty()) .map(XmrNodes.XmrNode::fromFullAddress) .collect(Collectors.toList()); } @EqualsAndHashCode @Getter public static class XmrNode { private final MoneroNodesOption type; @Nullable private final String onionAddress; @Nullable private final String hostName; @Nullable private final String operator; // null in case the user provides a list of custom btc nodes @Nullable private final String address; // IPv4 address private int port = HavenoUtils.getDefaultMoneroPort(); private int priority = 0; /** * @param fullAddress [IPv4 address:port or onion:port] * @return XmrNode instance */ public static XmrNode fromFullAddress(String fullAddress) { String[] parts = fullAddress.split("]"); checkArgument(parts.length > 0); String host = ""; int port = HavenoUtils.getDefaultMoneroPort(); if (parts[0].contains("[") && parts[0].contains(":")) { // IPv6 address and optional port number // address part delimited by square brackets e.g. [2a01:123:456:789::2]:8333 host = parts[0] + "]"; // keep the square brackets per RFC-2732 if (parts.length == 2) port = Integer.parseInt(parts[1].replace(":", "")); } else if (parts[0].contains(":") && !parts[0].contains(".")) { // IPv6 address only; not delimited by square brackets host = parts[0]; } else if (parts[0].contains(".")) { // address and an optional port number // e.g. 127.0.0.1:8333 or abcdef123xyz.onion:9999 parts = fullAddress.split(":"); checkArgument(parts.length > 0); host = parts[0]; if (parts.length == 2) port = Integer.parseInt(parts[1]); } checkArgument(host.length() > 0, "XmrNode address format not recognised"); return host.contains(".onion") ? new XmrNode(MoneroNodesOption.CUSTOM, null, host, null, port, null, null) : new XmrNode(MoneroNodesOption.CUSTOM, null, null, host, port, null, null); } public XmrNode(MoneroNodesOption type, @Nullable String hostName, @Nullable String onionAddress, @Nullable String address, int port, Integer priority, @Nullable String operator) { this.type = type; this.hostName = hostName; this.onionAddress = onionAddress; this.address = address; this.port = port; this.priority = priority == null ? 0 : priority; this.operator = operator; } public String getHostNameOrAddress() { if (hostName != null) return hostName; else return address; } public boolean hasOnionAddress() { return onionAddress != null; } public boolean hasClearNetAddress() { return hostName != null || address != null; } public String getClearNetUri() { if (!hasClearNetAddress()) throw new IllegalStateException("XmrNode does not have clearnet address"); return "http://" + getHostNameOrAddress() + ":" + port; } @Override public String toString() { return "onionAddress='" + onionAddress + '\'' + ", hostName='" + hostName + '\'' + ", address='" + address + '\'' + ", port='" + port + '\'' + ", priority='" + priority + '\'' + ", operator='" + operator; } } } ================================================ FILE: core/src/main/java/haveno/core/xmr/nodes/XmrNodesRepository.java ================================================ /* * 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 . */ package haveno.core.xmr.nodes; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import org.bitcoinj.core.PeerAddress; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; public class XmrNodesRepository { private final XmrNodeConverter converter; private final List nodes; public XmrNodesRepository(List nodes) { this(new XmrNodeConverter(), nodes); } public XmrNodesRepository(XmrNodeConverter converter, List nodes) { this.converter = converter; this.nodes = nodes; } public List getPeerAddresses(@Nullable Socks5Proxy proxy, boolean isUseClearNodesWithProxies) { List result; // We connect to onion nodes only in case we use Tor for BitcoinJ (default) to avoid privacy leaks at // exit nodes with bloom filters. if (proxy != null) { List onionHosts = getOnionHosts(); result = new ArrayList<>(onionHosts); if (isUseClearNodesWithProxies) { // We also use the clear net nodes (used for monitor) List torAddresses = getClearNodesBehindProxy(proxy); result.addAll(torAddresses); } } else { result = getClearNodes(); } return result; } private List getClearNodes() { return nodes.stream() .filter(XmrNodes.XmrNode::hasClearNetAddress) .flatMap(node -> nullableAsStream(converter.convertClearNode(node))) .collect(Collectors.toList()); } private List getOnionHosts() { return nodes.stream() .filter(XmrNodes.XmrNode::hasOnionAddress) .flatMap(node -> nullableAsStream(converter.convertOnionHost(node))) .collect(Collectors.toList()); } private List getClearNodesBehindProxy(Socks5Proxy proxy) { return nodes.stream() .filter(XmrNodes.XmrNode::hasClearNetAddress) .flatMap(node -> nullableAsStream(converter.convertWithTor(node, proxy))) .collect(Collectors.toList()); } private static Stream nullableAsStream(@Nullable T item) { return Optional.ofNullable(item).stream(); } } ================================================ FILE: core/src/main/java/haveno/core/xmr/nodes/XmrNodesSetupPreferences.java ================================================ /* * 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 . */ package haveno.core.xmr.nodes; import haveno.common.config.Config; import haveno.common.util.Utilities; import haveno.core.user.Preferences; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; import java.util.Set; public class XmrNodesSetupPreferences { private static final Logger log = LoggerFactory.getLogger(XmrNodesSetupPreferences.class); private final Preferences preferences; public XmrNodesSetupPreferences(Preferences preferences) { this.preferences = preferences; } public List selectPreferredNodes(XmrNodes nodes) { List result; XmrNodes.MoneroNodesOption nodesOption = XmrNodes.MoneroNodesOption.values()[preferences.getMoneroNodesOptionOrdinal()]; switch (nodesOption) { case CUSTOM: String moneroNodes = preferences.getMoneroNodes(); Set distinctNodes = Utilities.commaSeparatedListToSet(moneroNodes, false); result = XmrNodes.toCustomXmrNodesList(distinctNodes); if (result.isEmpty()) { log.warn("Custom nodes is set but no valid nodes are provided. " + "We fall back to provided nodes option."); preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal()); result = nodes.getProvidedXmrNodes(); } break; case PUBLIC: result = nodes.getAllXmrNodes(); // public entails provided nodes break; case PROVIDED: default: result = nodes.getProvidedXmrNodes(); break; } return result; } public boolean isUseCustomNodes() { return XmrNodes.MoneroNodesOption.CUSTOM.ordinal() == preferences.getMoneroNodesOptionOrdinal(); } public int calculateMinBroadcastConnections(List nodes) { XmrNodes.MoneroNodesOption nodesOption = XmrNodes.MoneroNodesOption.values()[preferences.getMoneroNodesOptionOrdinal()]; int result; switch (nodesOption) { case CUSTOM: // We have set the nodes already above result = (int) Math.ceil(nodes.size() * 0.5); // If Tor is set we usually only use onion nodes, // but if user provides mixed clear net and onion nodes we want to use both break; case PUBLIC: // We keep the empty nodes result = (int) Math.floor(Config.DEFAULT_NUM_CONNECTIONS_FOR_BTC * 0.8); break; case PROVIDED: default: // We require only 4 nodes instead of 7 (for 9 max connections) because our provided nodes // are more reliable than random public nodes. result = 4; break; } return result; } } ================================================ FILE: core/src/main/java/haveno/core/xmr/setup/DownloadListener.java ================================================ package haveno.core.xmr.setup; import haveno.common.UserThread; import javafx.beans.property.DoubleProperty; import javafx.beans.property.LongProperty; import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleLongProperty; public class DownloadListener { private final DoubleProperty percentage = new SimpleDoubleProperty(-1); private final LongProperty blocksRemaining = new SimpleLongProperty(-1); private final LongProperty numUpdates = new SimpleLongProperty(0); public void progress(double percentage, long blocksRemaining) { if (!UserThread.isUserThread(Thread.currentThread())) { throw new IllegalStateException("DownloadListener.progress() must be called on the JavaFX Application Thread"); } this.percentage.set(percentage); this.blocksRemaining.set(blocksRemaining); this.numUpdates.set(this.numUpdates.get() + 1); } public void doneDownload() { if (!UserThread.isUserThread(Thread.currentThread())) { throw new IllegalStateException("DownloadListener.doneDownload() must be called on the JavaFX Application Thread"); } this.percentage.set(1d); } public LongProperty numUpdates() { return numUpdates; } public ReadOnlyDoubleProperty percentageProperty() { return percentage; } public LongProperty blocksRemainingProperty() { return blocksRemaining; } } ================================================ FILE: core/src/main/java/haveno/core/xmr/setup/HavenoKeyChainFactory.java ================================================ /* * 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 . */ package haveno.core.xmr.setup; import com.google.common.collect.ImmutableList; import org.bitcoinj.crypto.ChildNumber; import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.crypto.KeyCrypter; import org.bitcoinj.script.Script; import org.bitcoinj.wallet.DefaultKeyChainFactory; import org.bitcoinj.wallet.DeterministicKeyChain; import org.bitcoinj.wallet.DeterministicSeed; import org.bitcoinj.wallet.KeyChainGroupStructure; import org.bitcoinj.wallet.Protos; /** * Hack to convert bitcoinj 0.14 wallets to bitcoinj 0.15 format. * * This code is required to be executed only once per user (actually twice, for btc wallets). * Once all users using bitcoinj 0.14 wallets have executed this code, this class will be no longer needed. * * Since that is almost impossible to guarantee, this hack will stay until we decide to don't be * backwards compatible with pre bitcoinj 0.15 wallets. * In that scenario, users will have to migrate using this procedure: * 1) Run pre bitcoinj 0.15 btc and copy their seed words on a piece of paper. * 2) Run post bitcoinj 0.15 btc and use recover from seed. * */ public class HavenoKeyChainFactory extends DefaultKeyChainFactory { @Override public DeterministicKeyChain makeKeyChain(Protos.Key key, Protos.Key firstSubKey, DeterministicSeed seed, KeyCrypter crypter, boolean isMarried, Script.ScriptType outputScriptType, ImmutableList accountPath) { ImmutableList maybeUpdatedAccountPath = accountPath; if (DeterministicKeyChain.ACCOUNT_ZERO_PATH.equals(accountPath)) { // This is a bitcoinj 0.14 wallet that has no account path in the serialized mnemonic KeyChainGroupStructure structure = new HavenoKeyChainGroupStructure(); maybeUpdatedAccountPath = structure.accountPathFor(outputScriptType); } return super.makeKeyChain(key, firstSubKey, seed, crypter, isMarried, outputScriptType, maybeUpdatedAccountPath); } @Override public DeterministicKeyChain makeWatchingKeyChain(Protos.Key key, Protos.Key firstSubKey, DeterministicKey accountKey, boolean isFollowingKey, boolean isMarried, Script.ScriptType outputScriptType) { throw new UnsupportedOperationException("Haveno is not supposed to use this"); } @Override public DeterministicKeyChain makeSpendingKeyChain(Protos.Key key, Protos.Key firstSubKey, DeterministicKey accountKey, boolean isMarried, Script.ScriptType outputScriptType) { throw new UnsupportedOperationException("Haveno is not supposed to use this"); } } ================================================ FILE: core/src/main/java/haveno/core/xmr/setup/HavenoKeyChainGroupStructure.java ================================================ /* * 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 . */ package haveno.core.xmr.setup; import com.google.common.collect.ImmutableList; import org.bitcoinj.crypto.ChildNumber; import org.bitcoinj.script.Script; import org.bitcoinj.wallet.KeyChainGroupStructure; public class HavenoKeyChainGroupStructure implements KeyChainGroupStructure { // See https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki // https://github.com/satoshilabs/slips/blob/master/slip-0044.md // We use 0 (0x80000000) as coin_type for BTC // m / purpose' / coin_type' / account' / change / address_index public static final ImmutableList BIP44_BTC_NON_SEGWIT_ACCOUNT_PATH = ImmutableList.of( new ChildNumber(44, true), new ChildNumber(0, true), ChildNumber.ZERO_HARDENED); public static final ImmutableList BIP44_BTC_SEGWIT_ACCOUNT_PATH = ImmutableList.of( new ChildNumber(44, true), new ChildNumber(0, true), ChildNumber.ONE_HARDENED); @Override public ImmutableList accountPathFor(Script.ScriptType outputScriptType) { if (outputScriptType == null || outputScriptType == Script.ScriptType.P2PKH) return BIP44_BTC_NON_SEGWIT_ACCOUNT_PATH; else if (outputScriptType == Script.ScriptType.P2WPKH) return BIP44_BTC_SEGWIT_ACCOUNT_PATH; else throw new IllegalArgumentException(outputScriptType.toString()); } } ================================================ FILE: core/src/main/java/haveno/core/xmr/setup/MoneroWalletRpcManager.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.xmr.setup; import lombok.extern.slf4j.Slf4j; import monero.common.MoneroError; import monero.wallet.MoneroWalletRpc; import java.io.IOException; import java.net.ServerSocket; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Manages monero-wallet-rpc processes bound to ports. */ @Slf4j public class MoneroWalletRpcManager { private static final String RPC_BIND_PORT_ARGUMENT = "--rpc-bind-port"; private static int NUM_ALLOWED_ATTEMPTS = 3; // allow this many attempts to bind to an assigned port private Integer startPort; private final Map registeredPorts = new HashMap<>(); /** * Manage monero-wallet-rpc instances by auto-assigning ports. */ public MoneroWalletRpcManager() { } /** * Manage monero-wallet-rpc instances by assigning consecutive ports from a * starting port. * * @param startPort is the starting port to bind to */ public MoneroWalletRpcManager(int startPort) { this.startPort = startPort; } /** * Start a new instance of monero-wallet-rpc. * * @param cmd command line parameters to start monero-wallet-rpc * @return a client connected to the monero-wallet-rpc instance */ public MoneroWalletRpc startInstance(List cmd) { try { // register given port if (cmd.contains(RPC_BIND_PORT_ARGUMENT)) { int portArgumentPosition = cmd.indexOf(RPC_BIND_PORT_ARGUMENT) + 1; int port = Integer.parseInt(cmd.get(portArgumentPosition)); synchronized (registeredPorts) { if (registeredPorts.containsKey(port)) throw new RuntimeException("Port " + port + " is already registered"); registeredPorts.put(port, null); } MoneroWalletRpc walletRpc = new MoneroWalletRpc(cmd); // starts monero-wallet-rpc process synchronized (registeredPorts) { registeredPorts.put(port, walletRpc); } return walletRpc; } // register assigned ports up to maximum attempts else { int numAttempts = 0; while (numAttempts < NUM_ALLOWED_ATTEMPTS) { int port = -1; try { numAttempts++; // get port ServerSocket socket = null; if (startPort != null) port = registerNextPort(); else { socket = new ServerSocket(0); port = socket.getLocalPort(); synchronized (registeredPorts) { registeredPorts.put(port, null); } } // start monero-wallet-rpc List cmdCopy = new ArrayList<>(cmd); // preserve original cmd cmdCopy.add(RPC_BIND_PORT_ARGUMENT); cmdCopy.add("" + port); if (socket != null) socket.close(); // TODO: port can be taken by another process before monero-wallet-rpc starts. we could synchronize on a lock until monero-wallet-rpc is started, but that would interfere with stress testing MoneroWalletRpc walletRpc = new MoneroWalletRpc(cmdCopy); // start monero-wallet-rpc process synchronized (registeredPorts) { registeredPorts.put(port, walletRpc); } return walletRpc; } catch (Exception e) { log.warn("Unable to start monero-wallet-rpc instance on attempt {}/{} with port {}: {}", numAttempts, NUM_ALLOWED_ATTEMPTS, port, e.getMessage()); if (numAttempts >= NUM_ALLOWED_ATTEMPTS) throw e; } } throw new MoneroError("Failed to start monero-wallet-rpc instance after " + NUM_ALLOWED_ATTEMPTS + " attempts. This should never happen"); // should never reach here } } catch (IOException e) { throw new MoneroError(e); } } /** * Stop an instance of monero-wallet-rpc. * * @param walletRpc the client connected to the monero-wallet-rpc instance to stop * @param path the wallet path, since the wallet might be closed * @param force specifies if the process should be forcibly destroyed */ public void stopInstance(MoneroWalletRpc walletRpc, String path, boolean force) { // unregister port int port = unregisterPort(walletRpc); // stop process String pid = walletRpc.getProcess() == null ? null : String.valueOf(walletRpc.getProcess().pid()); if (force) log.info("Stopping MoneroWalletRpc path={}, port={}, pid={}, force={}", path, port, pid, force); else log.debug("Stopping MoneroWalletRpc path={}, port={}, pid={}, force={}", path, port, pid, force); walletRpc.stopProcess(force); } private int registerNextPort() throws IOException { synchronized (registeredPorts) { int port = startPort; while (registeredPorts.containsKey(port)) port++; registeredPorts.put(port, null); return port; } } private int unregisterPort(MoneroWalletRpc walletRpc) { synchronized (registeredPorts) { int port = -1; boolean found = false; for (Map.Entry entry : registeredPorts.entrySet()) { if (walletRpc == entry.getValue()) { found = true; try { port = entry.getKey(); removePort(port); } catch (Exception e) { throw new MoneroError(e); } break; } } if (!found) throw new RuntimeException("MoneroWalletRpc instance not registered with a port"); return port; } } private void removePort(int port) { synchronized (registeredPorts) { registeredPorts.remove(port); } } } ================================================ FILE: core/src/main/java/haveno/core/xmr/setup/RegTestHost.java ================================================ /* * 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 . */ package haveno.core.xmr.setup; import haveno.common.config.Config; public enum RegTestHost { NONE, LOCALHOST, REMOTE_HOST; public static String HOST = Config.DEFAULT_REGTEST_HOST; } ================================================ FILE: core/src/main/java/haveno/core/xmr/setup/WalletConfig.java ================================================ /* * 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 . */ package haveno.core.xmr.setup; import com.google.common.io.Closeables; import com.google.common.util.concurrent.AbstractIdleService; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import haveno.common.config.Config; import haveno.common.file.FileUtil; import haveno.core.api.XmrLocalNode; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import lombok.Getter; import lombok.Setter; import org.bitcoinj.core.BlockChain; import org.bitcoinj.core.Context; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.PeerAddress; import org.bitcoinj.core.PeerGroup; import org.bitcoinj.crypto.KeyCrypter; import org.bitcoinj.net.discovery.PeerDiscovery; import org.bitcoinj.script.Script; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.SPVBlockStore; import org.bitcoinj.wallet.DeterministicKeyChain; import org.bitcoinj.wallet.DeterministicSeed; import org.bitcoinj.wallet.Wallet; import org.bouncycastle.crypto.params.KeyParameter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; import java.util.concurrent.TimeUnit; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static haveno.common.util.Preconditions.checkDir; /** *

    Utility class that wraps the boilerplate needed to set up a new SPV bitcoinj app. Instantiate it with a directory * and file prefix, optionally configure a few things, then use startAsync and optionally awaitRunning. The object will * construct and configure a {@link BlockChain}, {@link SPVBlockStore}, {@link Wallet} and {@link PeerGroup}.

    * *

    To add listeners and modify the objects that are constructed, you can either do that by overriding the * {@link #onSetupCompleted()} method (which will run on a background thread) and make your changes there, * or by waiting for the service to start and then accessing the objects from wherever you want. However, you cannot * access the objects this class creates until startup is complete.

    * *

    The asynchronous design of this class may seem puzzling (just use {@link #awaitRunning()} if you don't want that). * It is to make it easier to fit bitcoinj into GUI apps, which require a high degree of responsiveness on their main * thread which handles all the animation and user interaction. Even when blockingStart is false, initializing bitcoinj * means doing potentially blocking file IO, generating keys and other potentially intensive operations. By running it * on a background thread, there's no risk of accidentally causing UI lag.

    * *

    Note that {@link #awaitRunning()} can throw an unchecked {@link IllegalStateException} * if anything goes wrong during startup - you should probably handle it and use {@link Exception#getCause()} to figure * out what went wrong more precisely. Same thing if you just use the {@link #startAsync()} method.

    */ public class WalletConfig extends AbstractIdleService { protected static final Logger log = LoggerFactory.getLogger(WalletConfig.class); protected final NetworkParameters params; protected final String filePrefix; protected volatile BlockChain vChain; protected volatile SPVBlockStore vStore; protected volatile Wallet vBtcWallet; protected volatile PeerGroup vPeerGroup; protected final File directory; protected volatile File vXmrWalletFile; protected volatile File vBtcWalletFile; protected PeerAddress[] peerAddresses; protected InputStream checkpoints; protected String userAgent, version; @Nullable protected DeterministicSeed restoreFromSeed; @Nullable protected PeerDiscovery discovery; protected volatile Context context; protected Config config; protected XmrLocalNode xmrLocalNode; protected Socks5Proxy socks5Proxy; protected int numConnectionsForBtc; @Getter @Setter private int minBroadcastConnections; @Getter private BooleanProperty migratedWalletToSegwit = new SimpleBooleanProperty(false); /** * Creates a new WalletConfig, with a newly created {@link Context}. Files will be stored in the given directory. */ public WalletConfig(NetworkParameters params, File directory, String filePrefix) { this(new Context(params), directory, filePrefix); } /** * Creates a new WalletConfig, with the given {@link Context}. Files will be stored in the given directory. */ private WalletConfig(Context context, File directory, String filePrefix) { this.context = context; this.params = checkNotNull(context.getParams()); this.directory = checkDir(directory); this.filePrefix = checkNotNull(filePrefix); } public WalletConfig setSocks5Proxy(Socks5Proxy socks5Proxy) { checkState(state() == State.NEW, "Cannot call after startup"); this.socks5Proxy = socks5Proxy; return this; } public WalletConfig setConfig(Config config) { checkState(state() == State.NEW, "Cannot call after startup"); this.config = config; return this; } public WalletConfig setXmrLocalNode(XmrLocalNode xmrLocalNode) { checkState(state() == State.NEW, "Cannot call after startup"); this.xmrLocalNode = xmrLocalNode; return this; } public WalletConfig setNumConnectionsForBtc(int numConnectionsForBtc) { checkState(state() == State.NEW, "Cannot call after startup"); this.numConnectionsForBtc = numConnectionsForBtc; return this; } /** Will only connect to the given addresses. Cannot be called after startup. */ public WalletConfig setPeerNodes(PeerAddress... addresses) { checkState(state() == State.NEW, "Cannot call after startup"); this.peerAddresses = addresses; return this; } /** Will only connect to localhost. Cannot be called after startup. */ public WalletConfig connectToLocalHost() { final InetAddress localHost = InetAddress.getLoopbackAddress(); return setPeerNodes(new PeerAddress(params, localHost, params.getPort())); } /** * If set, the file is expected to contain a checkpoints file calculated with BuildCheckpoints. It makes initial * block sync faster for new users - please refer to the documentation on the bitcoinj website * (https://bitcoinj.github.io/speeding-up-chain-sync) for further details. */ public WalletConfig setCheckpoints(InputStream checkpoints) { if (this.checkpoints != null) Closeables.closeQuietly(checkpoints); this.checkpoints = checkNotNull(checkpoints); return this; } /** * Sets the string that will appear in the subver field of the version message. * @param userAgent A short string that should be the name of your app, e.g. "My Wallet" * @param version A short string that contains the version number, e.g. "1.0-BETA" */ public WalletConfig setUserAgent(String userAgent, String version) { this.userAgent = checkNotNull(userAgent); this.version = checkNotNull(version); return this; } /** * If a seed is set here then any existing wallet that matches the file name will be renamed to a backup name, * the chain file will be deleted, and the wallet object will be instantiated with the given seed instead of * a fresh one being created. This is intended for restoring a wallet from the original seed. To implement restore * you would shut down the existing appkit, if any, then recreate it with the seed given by the user, then start * up the new kit. The next time your app starts it should work as normal (that is, don't keep calling this each * time). */ public WalletConfig restoreWalletFromSeed(DeterministicSeed seed) { this.restoreFromSeed = seed; return this; } /** * Sets the peer discovery class to use. If none is provided then DNS is used, which is a reasonable default. */ public WalletConfig setDiscovery(@Nullable PeerDiscovery discovery) { this.discovery = discovery; return this; } /** * This method is invoked on a background thread after all objects are initialised, but before the peer group * or block chain download is started. You can tweak the objects configuration here. */ protected void onSetupCompleted() { // Meant to be overridden by subclasses } @Override protected void startUp() throws Exception { onSetupCompleted(); } protected void setupAutoSave(Wallet wallet, File walletFile) { wallet.autosaveToFile(walletFile, 5, TimeUnit.SECONDS, null); } @Override protected void shutDown() throws Exception { } public NetworkParameters params() { return params; } public BlockChain chain() { checkState(state() == State.STARTING || state() == State.RUNNING, "Cannot call until startup is complete"); return vChain; } public BlockStore store() { checkState(state() == State.STARTING || state() == State.RUNNING, "Cannot call until startup is complete"); return vStore; } public Wallet btcWallet() { checkState(state() == State.STARTING || state() == State.RUNNING, "Cannot call until startup is complete"); return vBtcWallet; } public PeerGroup peerGroup() { checkState(state() == State.STARTING || state() == State.RUNNING, "Cannot call until startup is complete"); return vPeerGroup; } public File directory() { return directory; } public void maybeAddSegwitKeychain(Wallet wallet, KeyParameter aesKey) { if (HavenoKeyChainGroupStructure.BIP44_BTC_NON_SEGWIT_ACCOUNT_PATH.equals(wallet.getActiveKeyChain().getAccountPath())) { if (wallet.isEncrypted() && aesKey == null) { // wait for the aesKey to be set and this method to be invoked again. return; } // Do a backup of the wallet File backup = new File(directory, WalletsSetup.PRE_SEGWIT_WALLET_BACKUP); try { FileUtil.copyFile(new File(directory, "haveno_BTC.wallet"), backup); } catch (IOException e) { log.error(e.toString(), e); } // Btc wallet does not have a native segwit keychain, we should add one. DeterministicSeed seed = wallet.getKeyChainSeed(); if (aesKey != null) { // If wallet is encrypted, decrypt the seed. KeyCrypter keyCrypter = wallet.getKeyCrypter(); seed = seed.decrypt(keyCrypter, DeterministicKeyChain.DEFAULT_PASSPHRASE_FOR_MNEMONIC, aesKey); } DeterministicKeyChain nativeSegwitKeyChain = DeterministicKeyChain.builder().seed(seed) .outputScriptType(Script.ScriptType.P2WPKH) .accountPath(new HavenoKeyChainGroupStructure().accountPathFor(Script.ScriptType.P2WPKH)).build(); if (aesKey != null) { // If wallet is encrypted, encrypt the new keychain. KeyCrypter keyCrypter = wallet.getKeyCrypter(); nativeSegwitKeyChain = nativeSegwitKeyChain.toEncrypted(keyCrypter, aesKey); } wallet.addAndActivateHDChain(nativeSegwitKeyChain); } migratedWalletToSegwit.set(true); } public boolean stateStartingOrRunning() { return state() == State.STARTING || state() == State.RUNNING; } } ================================================ FILE: core/src/main/java/haveno/core/xmr/setup/WalletsSetup.java ================================================ /* * 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 . */ package haveno.core.xmr.setup; import com.google.common.annotations.VisibleForTesting; import com.google.common.util.concurrent.Service; import com.google.inject.Inject; import com.google.inject.name.Named; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.app.Version; import haveno.common.config.Config; import haveno.common.file.FileUtil; import haveno.common.handlers.ExceptionHandler; import haveno.common.handlers.ResultHandler; import haveno.core.api.XmrLocalNode; import haveno.core.user.Preferences; import haveno.core.xmr.exceptions.InvalidHostException; import haveno.core.xmr.model.AddressEntry; import haveno.core.xmr.model.AddressEntryList; import haveno.core.xmr.nodes.XmrNetworkConfig; import haveno.core.xmr.nodes.XmrNodes; import haveno.core.xmr.nodes.XmrNodes.XmrNode; import haveno.core.xmr.nodes.XmrNodesRepository; import haveno.core.xmr.nodes.XmrNodesSetupPreferences; import haveno.network.Socks5MultiDiscovery; import haveno.network.Socks5ProxyProvider; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.bitcoinj.core.Address; import org.bitcoinj.core.BlockChain; import org.bitcoinj.core.Context; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.PeerAddress; import org.bitcoinj.core.PeerGroup; import org.bitcoinj.utils.Threading; import org.bitcoinj.wallet.DeterministicSeed; import org.bitcoinj.wallet.Wallet; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; import java.io.File; import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkNotNull; // Setup wallets and use WalletConfig for BitcoinJ wiring. // Other like WalletConfig we are here always on the user thread. That is one reason why we do not // merge WalletsSetup with WalletConfig to one class. @Slf4j public class WalletsSetup { public static final String PRE_SEGWIT_WALLET_BACKUP = "pre_segwit_haveno_BTC.wallet.backup"; private static final int MIN_BROADCAST_CONNECTIONS = 0; @Getter public final BooleanProperty walletsSetupFailed = new SimpleBooleanProperty(); private static final long STARTUP_TIMEOUT_SECONDS = 3600; // 1 hour private static final String SPV_CHAIN_FILE_NAME = "haveno.spvchain"; private final RegTestHost regTestHost; private final AddressEntryList addressEntryList; private final Preferences preferences; private final Socks5ProxyProvider socks5ProxyProvider; private final Config config; private final XmrLocalNode xmrLocalNode; private final XmrNodes xmrNodes; private final int numConnectionsForBtc; private final String userAgent; private final NetworkParameters params; private final File walletDir; private final int socks5DiscoverMode; private final List setupTaskHandlers = new ArrayList<>(); private final List setupCompletedHandlers = new ArrayList<>(); public final BooleanProperty shutDownComplete = new SimpleBooleanProperty(); private final boolean useAllProvidedNodes; private WalletConfig walletConfig; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public WalletsSetup(RegTestHost regTestHost, AddressEntryList addressEntryList, Preferences preferences, Socks5ProxyProvider socks5ProxyProvider, Config config, XmrLocalNode xmrLocalNode, XmrNodes xmrNodes, @Named(Config.USER_AGENT) String userAgent, @Named(Config.WALLET_DIR) File walletDir, @Named(Config.USE_ALL_PROVIDED_NODES) boolean useAllProvidedNodes, @Named(Config.NUM_CONNECTIONS_FOR_BTC) int numConnectionsForBtc, @Named(Config.SOCKS5_DISCOVER_MODE) String socks5DiscoverModeString) { this.regTestHost = regTestHost; this.addressEntryList = addressEntryList; this.preferences = preferences; this.socks5ProxyProvider = socks5ProxyProvider; this.config = config; this.xmrLocalNode = xmrLocalNode; this.xmrNodes = xmrNodes; this.numConnectionsForBtc = numConnectionsForBtc; this.useAllProvidedNodes = useAllProvidedNodes; this.userAgent = userAgent; this.socks5DiscoverMode = evaluateMode(socks5DiscoverModeString); this.walletDir = walletDir; params = Config.baseCurrencyNetworkParameters(); PeerGroup.setIgnoreHttpSeeds(true); } /////////////////////////////////////////////////////////////////////////////////////////// // Lifecycle /////////////////////////////////////////////////////////////////////////////////////////// public void initialize(@Nullable DeterministicSeed seed, ResultHandler resultHandler, ExceptionHandler exceptionHandler) { // Tell bitcoinj to execute event handlers on the JavaFX UI thread. This keeps things simple and means // we cannot forget to switch threads when adding event handlers. Unfortunately, the DownloadListener // we give to the app kit is currently an exception and runs on a library thread. It'll get fixed in // a future version. Threading.USER_THREAD = UserThread.getExecutor(); Timer timeoutTimer = UserThread.runAfter(() -> exceptionHandler.handleException(new TimeoutException("Wallet did not initialize in " + STARTUP_TIMEOUT_SECONDS + " seconds.")), STARTUP_TIMEOUT_SECONDS); backupWallets(); final Socks5Proxy socks5Proxy = socks5ProxyProvider.getSocks5Proxy(); log.info("Using Socks5Proxy: " + socks5Proxy); walletConfig = new WalletConfig(params, walletDir, "haveno") { @Override protected void onSetupCompleted() { super.onSetupCompleted(); // run external startup handlers setupTaskHandlers.forEach(Runnable::run); // Map to user thread UserThread.execute(() -> { timeoutTimer.stop(); setupCompletedHandlers.forEach(Runnable::run); }); // onSetupCompleted in walletAppKit is not the called on the last invocations, so we add a bit of delay UserThread.runAfter(resultHandler::handleResult, 100, TimeUnit.MILLISECONDS); } }; walletConfig.setSocks5Proxy(socks5Proxy); walletConfig.setConfig(config); walletConfig.setXmrLocalNode(xmrLocalNode); // TODO: adapt to xmr or remove walletConfig.setUserAgent(userAgent, Version.VERSION); walletConfig.setNumConnectionsForBtc(numConnectionsForBtc); String checkpointsPath = null; if (params.getId().equals(NetworkParameters.ID_MAINNET)) { // Checkpoints are block headers that ship inside our app: for a new user, we pick the last header // in the checkpoints file and then download the rest from the network. It makes things much faster. // Checkpoint files are made using the BuildCheckpoints tool and usually we have to download the // last months worth or more (takes a few seconds). checkpointsPath = "/wallet/checkpoints.txt"; } else if (params.getId().equals(NetworkParameters.ID_TESTNET)) { checkpointsPath = "/wallet/checkpoints.testnet.txt"; } if (checkpointsPath != null) { walletConfig.setCheckpoints(getClass().getResourceAsStream(checkpointsPath)); } // TODO: update this for xmr if (params.getId().equals(NetworkParameters.ID_REGTEST)) { walletConfig.setMinBroadcastConnections(1); if (regTestHost == RegTestHost.LOCALHOST) { walletConfig.connectToLocalHost(); } else if (regTestHost == RegTestHost.REMOTE_HOST) { configPeerNodesForRegTestServer(); } else { try { configPeerNodes(socks5Proxy); } catch (IllegalArgumentException e) { timeoutTimer.stop(); walletsSetupFailed.set(true); exceptionHandler.handleException(new InvalidHostException(e.getMessage())); return; } } } else if (xmrLocalNode.shouldBeUsed()) { walletConfig.setMinBroadcastConnections(1); walletConfig.connectToLocalHost(); } else { try { configPeerNodes(socks5Proxy); } catch (IllegalArgumentException e) { timeoutTimer.stop(); walletsSetupFailed.set(true); exceptionHandler.handleException(new InvalidHostException(e.getMessage())); return; } } // If seed is non-null it means we are restoring from backup. if (seed != null) { walletConfig.restoreWalletFromSeed(seed); } walletConfig.addListener(new Service.Listener() { @Override public void failed(@NotNull Service.State from, @NotNull Throwable failure) { walletConfig = null; log.error("Service failure from state: {}; failure={}", from, failure); timeoutTimer.stop(); UserThread.execute(() -> exceptionHandler.handleException(failure)); } }, Threading.USER_THREAD); walletConfig.startAsync(); } public void shutDown() { if (walletConfig != null) { try { log.info("walletConfig shutDown started"); walletConfig.stopAsync(); walletConfig.awaitTerminated(1, TimeUnit.SECONDS); log.info("walletConfig shutDown completed"); } catch (Throwable ignore) { log.info("walletConfig shutDown interrupted by timeout"); } } shutDownComplete.set(true); } public void reSyncSPVChain() throws IOException { FileUtil.deleteFileIfExists(new File(walletDir, SPV_CHAIN_FILE_NAME)); } /////////////////////////////////////////////////////////////////////////////////////////// // Initialize methods /////////////////////////////////////////////////////////////////////////////////////////// @VisibleForTesting private int evaluateMode(String socks5DiscoverModeString) { String[] socks5DiscoverModes = StringUtils.deleteWhitespace(socks5DiscoverModeString).split(","); int mode = 0; for (String socks5DiscoverMode : socks5DiscoverModes) { switch (socks5DiscoverMode) { case "ADDR": mode |= Socks5MultiDiscovery.SOCKS5_DISCOVER_ADDR; break; case "DNS": mode |= Socks5MultiDiscovery.SOCKS5_DISCOVER_DNS; break; case "ONION": mode |= Socks5MultiDiscovery.SOCKS5_DISCOVER_ONION; break; case "ALL": default: mode |= Socks5MultiDiscovery.SOCKS5_DISCOVER_ALL; break; } } return mode; } private void configPeerNodesForRegTestServer() { try { if (RegTestHost.HOST.endsWith(".onion")) { walletConfig.setPeerNodes(new PeerAddress(RegTestHost.HOST, params.getPort())); } else { walletConfig.setPeerNodes(new PeerAddress(params, InetAddress.getByName(RegTestHost.HOST), params.getPort())); } } catch (UnknownHostException e) { log.error(e.toString()); e.printStackTrace(); throw new RuntimeException(e); } } private void configPeerNodes(@Nullable Socks5Proxy proxy) { walletConfig.setMinBroadcastConnections(MIN_BROADCAST_CONNECTIONS); XmrNodesSetupPreferences xmrNodesSetupPreferences = new XmrNodesSetupPreferences(preferences); List nodes = xmrNodesSetupPreferences.selectPreferredNodes(xmrNodes); XmrNodesRepository repository = new XmrNodesRepository(nodes); boolean isUseClearNodesWithProxies = (useAllProvidedNodes || xmrNodesSetupPreferences.isUseCustomNodes()); List peers = repository.getPeerAddresses(proxy, isUseClearNodesWithProxies); XmrNetworkConfig networkConfig = new XmrNetworkConfig(walletConfig, params, socks5DiscoverMode, proxy); networkConfig.proposePeers(peers); } /////////////////////////////////////////////////////////////////////////////////////////// // Backup /////////////////////////////////////////////////////////////////////////////////////////// public void backupWallets() { // TODO: remove? } public void clearBackups() { try { FileUtil.deleteDirectory(new File(Paths.get(walletDir.getAbsolutePath(), "backup").toString())); } catch (IOException e) { log.error("Could not delete directory " + e.getMessage()); e.printStackTrace(); } File segwitBackup = new File(walletDir, PRE_SEGWIT_WALLET_BACKUP); try { FileUtil.deleteFileIfExists(segwitBackup); } catch (IOException e) { log.error(e.toString(), e); } } /////////////////////////////////////////////////////////////////////////////////////////// // Restore /////////////////////////////////////////////////////////////////////////////////////////// public void restoreSeedWords(@Nullable DeterministicSeed seed, ResultHandler resultHandler, ExceptionHandler exceptionHandler) { checkNotNull(seed, "Seed must be not be null."); backupWallets(); Context ctx = Context.get(); new Thread(() -> { try { Context.propagate(ctx); walletConfig.stopAsync(); walletConfig.awaitTerminated(); initialize(seed, resultHandler, exceptionHandler); } catch (Throwable t) { t.printStackTrace(); log.error("Executing task failed. " + t.getMessage()); } }, "RestoreBTCWallet-%d").start(); } /////////////////////////////////////////////////////////////////////////////////////////// // Handlers /////////////////////////////////////////////////////////////////////////////////////////// public void addSetupTaskHandler(Runnable handler) { setupTaskHandlers.add(handler); } public void addSetupCompletedHandler(Runnable handler) { setupCompletedHandlers.add(handler); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// public Wallet getBtcWallet() { return walletConfig.btcWallet(); } public NetworkParameters getParams() { return params; } @Nullable public BlockChain getChain() { return walletConfig != null && walletConfig.stateStartingOrRunning() ? walletConfig.chain() : null; } public PeerGroup getPeerGroup() { return walletConfig.peerGroup(); } public WalletConfig getWalletConfig() { return walletConfig; } public Set
    getAddressesByContext(@SuppressWarnings("SameParameterValue") AddressEntry.Context context) { return addressEntryList.getAddressEntriesAsListImmutable().stream() .filter(addressEntry -> addressEntry.getContext() == context) .map(AddressEntry::getAddress) .collect(Collectors.toSet()); } public Set
    getAddressesFromAddressEntries(Set addressEntries) { return addressEntries.stream() .map(AddressEntry::getAddress) .collect(Collectors.toSet()); } public int getMinBroadcastConnections() { return walletConfig.getMinBroadcastConnections(); } } ================================================ FILE: core/src/main/java/haveno/core/xmr/wallet/BtcCoinSelector.java ================================================ /* * 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 . */ package haveno.core.xmr.wallet; import com.google.common.collect.Sets; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.Address; import org.bitcoinj.core.TransactionOutput; import java.util.Set; /** * We use a specialized version of the CoinSelector based on the DefaultCoinSelector implementation. * We lookup for spendable outputs which matches any of our addresses. */ @Slf4j class BtcCoinSelector extends HavenoDefaultCoinSelector { private final Set
    addresses; private final long ignoreDustThreshold; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// private BtcCoinSelector(Set
    addresses, long ignoreDustThreshold, boolean permitForeignPendingTx) { super(permitForeignPendingTx); this.addresses = addresses; this.ignoreDustThreshold = ignoreDustThreshold; } BtcCoinSelector(Set
    addresses, long ignoreDustThreshold) { this(addresses, ignoreDustThreshold, true); } BtcCoinSelector(Address address, long ignoreDustThreshold, @SuppressWarnings("SameParameterValue") boolean permitForeignPendingTx) { this(Sets.newHashSet(address), ignoreDustThreshold, permitForeignPendingTx); } BtcCoinSelector(Address address, long ignoreDustThreshold) { this(Sets.newHashSet(address), ignoreDustThreshold, true); } @Override protected boolean isTxOutputSpendable(TransactionOutput output) { if (WalletService.isOutputScriptConvertibleToAddress(output)) { Address address = WalletService.getAddressFromOutput(output); return addresses.contains(address); } else { log.warn("transactionOutput.getScriptPubKey() is not P2PKH nor P2SH nor P2WH"); return false; } } // We ignore utxos which are considered dust attacks for spying on users' wallets. // The ignoreDustThreshold value is set in the preferences. If not set we use default non dust // value of 546 sat. @Override protected boolean isDustAttackUtxo(TransactionOutput output) { return output.getValue().value < ignoreDustThreshold; } } ================================================ FILE: core/src/main/java/haveno/core/xmr/wallet/BtcWalletService.java ================================================ /* * 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 . */ package haveno.core.xmr.wallet; import com.google.common.base.Preconditions; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.Inject; import haveno.common.util.Tuple2; import haveno.core.user.Preferences; import haveno.core.xmr.exceptions.AddressEntryException; import haveno.core.xmr.exceptions.InsufficientFundsException; import haveno.core.xmr.exceptions.TransactionVerificationException; import haveno.core.xmr.exceptions.WalletException; import haveno.core.xmr.model.AddressEntry; import haveno.core.xmr.model.AddressEntryList; import haveno.core.xmr.setup.WalletsSetup; import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; 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.SegwitAddress; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionInput; import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.crypto.KeyCrypterScrypt; import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptPattern; import org.bitcoinj.wallet.SendRequest; import org.bouncycastle.crypto.params.KeyParameter; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class BtcWalletService extends WalletService { private static final Logger log = LoggerFactory.getLogger(BtcWalletService.class); private final AddressEntryList addressEntryList; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public BtcWalletService(WalletsSetup walletsSetup, AddressEntryList addressEntryList, Preferences preferences) { super(walletsSetup, preferences); this.addressEntryList = addressEntryList; // TODO: set and use chainHeightProperty in XmrWalletService walletsSetup.addSetupCompletedHandler(() -> { // wallet = walletsSetup.getBtcWallet(); // addListenersToWallet(); // // walletsSetup.getChain().addNewBestBlockListener(block -> chainHeightProperty.set(block.getHeight())); // chainHeightProperty.set(walletsSetup.getChain().getBestChainHeight()); }); } /////////////////////////////////////////////////////////////////////////////////////////// // Overridden Methods /////////////////////////////////////////////////////////////////////////////////////////// public boolean isWalletSyncedWithinTolerance() { throw new RuntimeException("Not implemented"); } @Override void decryptWallet(@NotNull KeyParameter key) { super.decryptWallet(key); addressEntryList.getAddressEntriesAsListImmutable().forEach(e -> { DeterministicKey keyPair = e.getKeyPair(); if (keyPair.isEncrypted()) e.setDeterministicKey(keyPair.decrypt(key)); }); addressEntryList.requestPersistence(); } @Override void encryptWallet(KeyCrypterScrypt keyCrypterScrypt, KeyParameter key) { super.encryptWallet(keyCrypterScrypt, key); addressEntryList.getAddressEntriesAsListImmutable().forEach(e -> { DeterministicKey keyPair = e.getKeyPair(); if (keyPair.isEncrypted()) e.setDeterministicKey(keyPair.encrypt(keyCrypterScrypt, key)); }); addressEntryList.requestPersistence(); } @Override String getWalletAsString(boolean includePrivKeys) { StringBuilder sb = new StringBuilder(); getAddressEntryListAsImmutableList().forEach(e -> sb.append(e.toString()).append("\n")); //boolean reallyIncludePrivKeys = includePrivKeys && !wallet.isEncrypted(); return "Address entry list:\n" + sb.toString() + "\n\n" + wallet.toString(true, includePrivKeys, this.aesKey, true, true, walletsSetup.getChain()) + "\n\n" + "All pubKeys as hex:\n" + wallet.printAllPubKeysAsHex(); } private Tuple2 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 /////////////////////////////////////////////////////////////////////////////////////////// public void commitTx(Transaction tx) { wallet.commitTx(tx); // printTx("BTC commit Tx", tx); } /////////////////////////////////////////////////////////////////////////////////////////// // AddressEntry /////////////////////////////////////////////////////////////////////////////////////////// public Optional getAddressEntry(String offerId, @SuppressWarnings("SameParameterValue") AddressEntry.Context context) { return getAddressEntryListAsImmutableList().stream() .filter(e -> offerId.equals(e.getOfferId())) .filter(e -> context == e.getContext()) .findAny(); } public AddressEntry getOrCreateAddressEntry(String offerId, AddressEntry.Context context) { Optional addressEntry = getAddressEntryListAsImmutableList().stream() .filter(e -> offerId.equals(e.getOfferId())) .filter(e -> context == e.getContext()) .findAny(); if (addressEntry.isPresent()) { return addressEntry.get(); } else { // We try to use available and not yet used entries Optional emptyAvailableAddressEntry = getAddressEntryListAsImmutableList().stream() .filter(e -> AddressEntry.Context.AVAILABLE == e.getContext()) .filter(e -> isAddressUnused(e.getAddress())) .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.P2WPKH)); AddressEntry entry = new AddressEntry(key, context, offerId, true); log.info("getOrCreateAddressEntry: new AddressEntry={}", entry); addressEntryList.addAddressEntry(entry); return entry; } } } public AddressEntry getArbitratorAddressEntry() { AddressEntry.Context context = AddressEntry.Context.ARBITRATOR; Optional addressEntry = getAddressEntryListAsImmutableList().stream() .filter(e -> context == e.getContext()) .findAny(); return getOrCreateAddressEntry(context, addressEntry, false); } public AddressEntry getFreshAddressEntry() { return getFreshAddressEntry(true); } public AddressEntry getFreshAddressEntry(boolean segwit) { AddressEntry.Context context = AddressEntry.Context.AVAILABLE; Optional addressEntry = getAddressEntryListAsImmutableList().stream() .filter(e -> context == e.getContext()) .filter(e -> isAddressUnused(e.getAddress())) .filter(e -> { boolean isSegwitOutputScriptType = Script.ScriptType.P2WPKH.equals(e.getAddress().getOutputScriptType()); // We need to ensure that we take only addressEntries which matches our segWit flag return isSegwitOutputScriptType == segwit; }) .findAny(); return getOrCreateAddressEntry(context, addressEntry, segwit); } public void recoverAddressEntry(String offerId, String address, AddressEntry.Context context) { findAddressEntry(address, AddressEntry.Context.AVAILABLE).ifPresent(addressEntry -> addressEntryList.swapAvailableToAddressEntryWithOfferId(addressEntry, context, offerId)); } private AddressEntry getOrCreateAddressEntry(AddressEntry.Context context, Optional addressEntry, boolean segwit) { if (addressEntry.isPresent()) { return addressEntry.get(); } else { DeterministicKey key; if (segwit) { key = (DeterministicKey) wallet.findKeyFromAddress(wallet.freshReceiveAddress(Script.ScriptType.P2WPKH)); } else { key = (DeterministicKey) wallet.findKeyFromAddress(wallet.freshReceiveAddress(Script.ScriptType.P2PKH)); } AddressEntry entry = new AddressEntry(key, context, segwit); log.info("getOrCreateAddressEntry: add new AddressEntry {}", entry); addressEntryList.addAddressEntry(entry); return entry; } } private Optional findAddressEntry(String address, AddressEntry.Context context) { return getAddressEntryListAsImmutableList().stream() .filter(e -> address.equals(e.getAddressString())) .filter(e -> context == e.getContext()) .findAny(); } public List getAvailableAddressEntries() { return getAddressEntryListAsImmutableList().stream() .filter(addressEntry -> AddressEntry.Context.AVAILABLE == addressEntry.getContext()) .collect(Collectors.toList()); } public List getAddressEntriesForOpenOffer() { return getAddressEntryListAsImmutableList().stream() .filter(addressEntry -> AddressEntry.Context.OFFER_FUNDING == addressEntry.getContext() || AddressEntry.Context.RESERVED_FOR_TRADE == addressEntry.getContext()) .collect(Collectors.toList()); } public List getAddressEntriesForTrade() { return getAddressEntryListAsImmutableList().stream() .filter(addressEntry -> AddressEntry.Context.MULTI_SIG == addressEntry.getContext() || AddressEntry.Context.TRADE_PAYOUT == addressEntry.getContext()) .collect(Collectors.toList()); } public List getAddressEntries(AddressEntry.Context context) { return getAddressEntryListAsImmutableList().stream() .filter(addressEntry -> context == addressEntry.getContext()) .collect(Collectors.toList()); } public List getFundedAvailableAddressEntries() { return getAvailableAddressEntries().stream() .filter(addressEntry -> getBalanceForAddress(addressEntry.getAddress()).isPositive()) .collect(Collectors.toList()); } public List getAddressEntryListAsImmutableList() { return addressEntryList.getAddressEntriesAsListImmutable(); } public void swapAddressEntryToAvailable(String offerId, AddressEntry.Context context) { if (context == AddressEntry.Context.MULTI_SIG) { log.error("swapAddressEntryToAvailable called with MULTI_SIG context. " + "This in not permitted as we must not reuse those address entries and there " + "are no redeemable funds on that addresses. Only the keys are used for creating " + "the Multisig address. offerId={}, context={}", offerId, context); return; } getAddressEntryListAsImmutableList().stream() .filter(e -> offerId.equals(e.getOfferId())) .filter(e -> context == e.getContext()) .forEach(e -> { log.info("swap addressEntry with address {} and offerId {} from context {} to available", e.getAddressString(), e.getOfferId(), context); addressEntryList.swapToAvailable(e); }); } // When funds from MultiSig address is spent we reset the coinLockedInMultiSig value to 0. public void resetCoinLockedInMultiSigAddressEntry(String offerId) { setCoinLockedInMultiSigAddressEntry(offerId, 0); } public void setCoinLockedInMultiSigAddressEntry(String offerId, long value) { getAddressEntryListAsImmutableList().stream() .filter(e -> AddressEntry.Context.MULTI_SIG == e.getContext()) .filter(e -> offerId.equals(e.getOfferId())) .forEach(addressEntry -> setCoinLockedInMultiSigAddressEntry(addressEntry, value)); } public void setCoinLockedInMultiSigAddressEntry(AddressEntry addressEntry, long value) { log.info("Set coinLockedInMultiSig for addressEntry {} to value {}", addressEntry, value); addressEntryList.setCoinLockedInMultiSigAddressEntry(addressEntry, value); } public void resetAddressEntriesForOpenOffer(String offerId) { log.info("resetAddressEntriesForOpenOffer offerId={}", offerId); swapAddressEntryToAvailable(offerId, AddressEntry.Context.OFFER_FUNDING); swapAddressEntryToAvailable(offerId, AddressEntry.Context.RESERVED_FOR_TRADE); } public void resetAddressEntriesForPendingTrade(String offerId) { // We must not swap MULTI_SIG entries as those addresses are not detected in the isAddressUnused // check at getOrCreateAddressEntry and could lead to a reuse of those keys and result in the same 2of2 MS // address if same peers trade again. // We swap TRADE_PAYOUT to be sure all is cleaned up. There might be cases where a user cannot send the funds // to an external wallet directly in the last step of the trade, but the funds are in the Haveno wallet anyway and // the dealing with the external wallet is pure UI thing. The user can move the funds to the wallet and then // send out the funds to the external wallet. As this cleanup is a rare situation and most users do not use // the feature to send out the funds we prefer that strategy (if we keep the address entry it might cause // complications in some edge cases after a SPV resync). swapAddressEntryToAvailable(offerId, AddressEntry.Context.TRADE_PAYOUT); } public void swapAnyTradeEntryContextToAvailableEntry(String offerId) { resetAddressEntriesForOpenOffer(offerId); resetAddressEntriesForPendingTrade(offerId); } public void saveAddressEntryList() { addressEntryList.requestPersistence(); } public DeterministicKey getMultiSigKeyPair(String tradeId, byte[] pubKey) { Optional multiSigAddressEntryOptional = getAddressEntry(tradeId, AddressEntry.Context.MULTI_SIG); DeterministicKey multiSigKeyPair; if (multiSigAddressEntryOptional.isPresent()) { AddressEntry multiSigAddressEntry = multiSigAddressEntryOptional.get(); multiSigKeyPair = multiSigAddressEntry.getKeyPair(); if (!Arrays.equals(pubKey, multiSigAddressEntry.getPubKey())) { log.error("Pub Key from AddressEntry does not match key pair from trade data. Trade ID={}\n" + "We try to find the keypair in the wallet with the pubKey we found in the trade data.", tradeId); multiSigKeyPair = findKeyFromPubKey(pubKey); } } else { log.error("multiSigAddressEntry not found for trade ID={}.\n" + "We try to find the keypair in the wallet with the pubKey we found in the trade data.", tradeId); multiSigKeyPair = findKeyFromPubKey(pubKey); } return multiSigKeyPair; } /////////////////////////////////////////////////////////////////////////////////////////// // Balance /////////////////////////////////////////////////////////////////////////////////////////// public Coin getSavingWalletBalance() { return Coin.valueOf(getFundedAvailableAddressEntries().stream() .mapToLong(addressEntry -> getBalanceForAddress(addressEntry.getAddress()).value) .sum()); } public Stream getAddressEntriesForAvailableBalanceStream() { Stream availableAndPayout = Stream.concat(getAddressEntries(AddressEntry.Context.TRADE_PAYOUT) .stream(), getFundedAvailableAddressEntries().stream()); Stream available = Stream.concat(availableAndPayout, getAddressEntries(AddressEntry.Context.ARBITRATOR).stream()); available = Stream.concat(available, getAddressEntries(AddressEntry.Context.OFFER_FUNDING).stream()); return available.filter(addressEntry -> getBalanceForAddress(addressEntry.getAddress()).isPositive()); } /////////////////////////////////////////////////////////////////////////////////////////// // Withdrawal Fee calculation /////////////////////////////////////////////////////////////////////////////////////////// public Coin getTxFeeForWithdrawalPerVbyte() { throw new RuntimeException("BTC fee estimation removed"); } public Transaction getFeeEstimationTransaction(String fromAddress, String toAddress, Coin amount, AddressEntry.Context context) throws AddressFormatException, AddressEntryException, InsufficientFundsException { Optional addressEntry = findAddressEntry(fromAddress, context); if (!addressEntry.isPresent()) throw new AddressEntryException("WithdrawFromAddress is not found in our wallet."); checkNotNull(addressEntry.get().getAddress(), "addressEntry.get().getAddress() must nto be null"); try { Coin fee; int counter = 0; int txVsize = 0; Transaction tx; Coin txFeeForWithdrawalPerVbyte = getTxFeeForWithdrawalPerVbyte(); do { counter++; fee = txFeeForWithdrawalPerVbyte.multiply(txVsize); SendRequest sendRequest = getSendRequest(fromAddress, toAddress, amount, fee, aesKey, context); wallet.completeTx(sendRequest); tx = sendRequest.tx; txVsize = tx.getVsize(); printTx("FeeEstimationTransaction", tx); } while (feeEstimationNotSatisfied(counter, tx)); if (counter == 10) log.error("Could not calculate the fee. Tx=" + tx); return tx; } catch (InsufficientMoneyException e) { throw new InsufficientFundsException("The fees for that transaction exceed the available funds " + "or the resulting output value is below the min. dust value:\n" + "Missing " + (e.missing != null ? e.missing.toFriendlyString() : "null")); } } public Transaction getFeeEstimationTransactionForMultipleAddresses(Set fromAddresses, Coin amount) throws AddressFormatException, AddressEntryException, InsufficientFundsException { Coin txFeeForWithdrawalPerVbyte = getTxFeeForWithdrawalPerVbyte(); return getFeeEstimationTransactionForMultipleAddresses(fromAddresses, amount, txFeeForWithdrawalPerVbyte); } public Transaction getFeeEstimationTransactionForMultipleAddresses(Set fromAddresses, Coin amount, Coin txFeeForWithdrawalPerVbyte) throws AddressFormatException, AddressEntryException, InsufficientFundsException { Set addressEntries = fromAddresses.stream() .map(address -> { Optional addressEntryOptional = findAddressEntry(address, AddressEntry.Context.AVAILABLE); if (!addressEntryOptional.isPresent()) addressEntryOptional = findAddressEntry(address, AddressEntry.Context.OFFER_FUNDING); if (!addressEntryOptional.isPresent()) addressEntryOptional = findAddressEntry(address, AddressEntry.Context.TRADE_PAYOUT); if (!addressEntryOptional.isPresent()) addressEntryOptional = findAddressEntry(address, AddressEntry.Context.ARBITRATOR); return addressEntryOptional; }) .filter(Optional::isPresent) .map(Optional::get) .collect(Collectors.toSet()); if (addressEntries.isEmpty()) throw new AddressEntryException("No Addresses for withdraw found in our wallet"); try { Coin fee; int counter = 0; int txVsize = 0; Transaction tx; do { counter++; fee = txFeeForWithdrawalPerVbyte.multiply(txVsize); // We use a dummy address for the output // 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; txVsize = tx.getVsize(); printTx("FeeEstimationTransactionForMultipleAddresses", tx); } while (feeEstimationNotSatisfied(counter, tx)); if (counter == 10) log.error("Could not calculate the fee. Tx=" + tx); return tx; } catch (InsufficientMoneyException e) { throw new InsufficientFundsException("The fees for that transaction exceed the available funds " + "or the resulting output value is below the min. dust value:\n" + "Missing " + (e.missing != null ? e.missing.toFriendlyString() : "null")); } } private boolean feeEstimationNotSatisfied(int counter, Transaction tx) { return feeEstimationNotSatisfied(counter, tx, getTxFeeForWithdrawalPerVbyte()); } private boolean feeEstimationNotSatisfied(int counter, Transaction tx, Coin txFeeForWithdrawalPerVbyte) { long targetFee = txFeeForWithdrawalPerVbyte.multiply(tx.getVsize()).value; return counter < 10 && (tx.getFee().value < targetFee || tx.getFee().value - targetFee > 1000); } public int getEstimatedFeeTxVsize(List outputValues, Coin txFee) throws InsufficientMoneyException, AddressFormatException { Transaction transaction = new Transaction(params); // 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); sendRequest.shuffleOutputs = false; sendRequest.aesKey = aesKey; sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE), preferences.getIgnoreDustThreshold()); sendRequest.fee = txFee; sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; sendRequest.changeAddress = dummyAddress; wallet.completeTx(sendRequest); return transaction.getVsize(); } /////////////////////////////////////////////////////////////////////////////////////////// // Withdrawal Send /////////////////////////////////////////////////////////////////////////////////////////// private SendRequest getSendRequest(String fromAddress, String toAddress, Coin amount, Coin fee, @Nullable KeyParameter aesKey, AddressEntry.Context context) throws AddressFormatException, AddressEntryException { Transaction tx = new Transaction(params); final Coin receiverAmount = amount.subtract(fee); Preconditions.checkArgument(Restrictions.isAboveDust(receiverAmount), "The amount is too low (dust limit)."); tx.addOutput(receiverAmount, Address.fromString(params, toAddress)); SendRequest sendRequest = SendRequest.forTx(tx); sendRequest.fee = fee; sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; sendRequest.aesKey = aesKey; sendRequest.shuffleOutputs = false; Optional addressEntry = findAddressEntry(fromAddress, context); if (!addressEntry.isPresent()) throw new AddressEntryException("WithdrawFromAddress is not found in our wallet."); checkNotNull(addressEntry.get(), "addressEntry.get() must not be null"); checkNotNull(addressEntry.get().getAddress(), "addressEntry.get().getAddress() must not be null"); sendRequest.coinSelector = new BtcCoinSelector(addressEntry.get().getAddress(), preferences.getIgnoreDustThreshold()); sendRequest.changeAddress = addressEntry.get().getAddress(); return sendRequest; } private SendRequest getSendRequestForMultipleAddresses(Set fromAddresses, String toAddress, Coin amount, Coin fee, @Nullable String changeAddress, @Nullable KeyParameter aesKey) throws AddressFormatException, AddressEntryException { Transaction tx = new Transaction(params); final Coin netValue = amount.subtract(fee); checkArgument(Restrictions.isAboveDust(netValue), "The amount is too low (dust limit)."); tx.addOutput(netValue, Address.fromString(params, toAddress)); SendRequest sendRequest = SendRequest.forTx(tx); sendRequest.fee = fee; sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; sendRequest.aesKey = aesKey; sendRequest.shuffleOutputs = false; Set addressEntries = fromAddresses.stream() .map(address -> { Optional addressEntryOptional = findAddressEntry(address, AddressEntry.Context.AVAILABLE); if (!addressEntryOptional.isPresent()) addressEntryOptional = findAddressEntry(address, AddressEntry.Context.OFFER_FUNDING); if (!addressEntryOptional.isPresent()) addressEntryOptional = findAddressEntry(address, AddressEntry.Context.TRADE_PAYOUT); if (!addressEntryOptional.isPresent()) addressEntryOptional = findAddressEntry(address, AddressEntry.Context.ARBITRATOR); return addressEntryOptional; }) .filter(Optional::isPresent) .map(Optional::get) .collect(Collectors.toSet()); if (addressEntries.isEmpty()) throw new AddressEntryException("No Addresses for withdraw found in our wallet"); sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesFromAddressEntries(addressEntries), preferences.getIgnoreDustThreshold()); Optional addressEntryOptional = Optional.empty(); if (changeAddress != null) addressEntryOptional = findAddressEntry(changeAddress, AddressEntry.Context.AVAILABLE); AddressEntry changeAddressAddressEntry = addressEntryOptional.orElseGet(this::getFreshAddressEntry); checkNotNull(changeAddressAddressEntry, "change address must not be null"); sendRequest.changeAddress = changeAddressAddressEntry.getAddress(); return sendRequest; } // We ignore utxos which are considered dust attacks for spying on users' wallets. // The ignoreDustThreshold value is set in the preferences. If not set we use default non dust // value of 546 sat. @Override protected boolean isDustAttackUtxo(TransactionOutput output) { return output.getValue().value < preferences.getIgnoreDustThreshold(); } /////////////////////////////////////////////////////////////////////////////////////////// // Refund payoutTx /////////////////////////////////////////////////////////////////////////////////////////// public Transaction createRefundPayoutTx(Coin buyerAmount, Coin sellerAmount, Coin fee, String buyerAddressString, String sellerAddressString) throws AddressFormatException, InsufficientMoneyException, WalletException, TransactionVerificationException { Transaction tx = new Transaction(params); Preconditions.checkArgument(buyerAmount.add(sellerAmount).isPositive(), "The sellerAmount + buyerAmount must be positive."); // buyerAmount can be 0 if (buyerAmount.isPositive()) { Preconditions.checkArgument(Restrictions.isAboveDust(buyerAmount), "The buyerAmount is too low (dust limit)."); tx.addOutput(buyerAmount, Address.fromString(params, buyerAddressString)); } // sellerAmount can be 0 if (sellerAmount.isPositive()) { Preconditions.checkArgument(Restrictions.isAboveDust(sellerAmount), "The sellerAmount is too low (dust limit)."); tx.addOutput(sellerAmount, Address.fromString(params, sellerAddressString)); } SendRequest sendRequest = SendRequest.forTx(tx); sendRequest.fee = fee; sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; sendRequest.aesKey = aesKey; sendRequest.shuffleOutputs = false; sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE), preferences.getIgnoreDustThreshold()); sendRequest.changeAddress = getFreshAddressEntry().getAddress(); checkNotNull(wallet); wallet.completeTx(sendRequest); Transaction resultTx = sendRequest.tx; checkWalletConsistency(wallet); verifyTransaction(resultTx); WalletService.printTx("createRefundPayoutTx", resultTx); return resultTx; } } ================================================ FILE: core/src/main/java/haveno/core/xmr/wallet/HavenoDefaultCoinSelector.java ================================================ /* * 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 . */ package haveno.core.xmr.wallet; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.Coin; import org.bitcoinj.core.InsufficientMoneyException; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionConfidence; import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.wallet.CoinSelection; import org.bitcoinj.wallet.CoinSelector; import javax.annotation.Nullable; import java.math.BigInteger; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; /** * Used from org.bitcoinj.wallet.DefaultCoinSelector but added selectOutput method and changed static methods to * instance methods. *

    *

    * This class implements a {@link CoinSelector} which attempts to get the highest priority * possible. This means that the transaction is the most likely to get confirmed. Note that this means we may end up * "spending" more priority than would be required to get the transaction we are creating confirmed. */ @Slf4j public abstract class HavenoDefaultCoinSelector implements CoinSelector { protected final boolean permitForeignPendingTx; // TransactionOutputs to be used as candidates in the select method. // We reset the value to null just after we have applied it inside the select method. @Nullable @Setter protected Set utxoCandidates; public CoinSelection select(Coin target, Set candidates) { return select(target, new ArrayList<>(candidates)); } public HavenoDefaultCoinSelector(boolean permitForeignPendingTx) { this.permitForeignPendingTx = permitForeignPendingTx; } public HavenoDefaultCoinSelector() { permitForeignPendingTx = false; } @Override public CoinSelection select(Coin target, List candidates) { ArrayList selected = new ArrayList<>(); // Sort the inputs by age*value so we get the highest "coin days" spent. ArrayList sortedOutputs; if (utxoCandidates != null) { sortedOutputs = new ArrayList<>(utxoCandidates); // We reuse the selectors. Reset the transactionOutputCandidates field utxoCandidates = null; } else { sortedOutputs = new ArrayList<>(candidates); } // If we spend all we don't need to sort if (!target.equals(NetworkParameters.MAX_MONEY)) sortOutputs(sortedOutputs); // Now iterate over the sorted outputs until we have got as close to the target as possible or a little // bit over (excessive value will be change). long total = 0; long targetValue = target.value; for (TransactionOutput output : sortedOutputs) { if (!isDustAttackUtxo(output)) { if (total >= targetValue) { long change = total - targetValue; if (change == 0 || change >= Restrictions.getMinNonDustOutput().value) break; } if (output.getParentTransaction() != null && isTxSpendable(output.getParentTransaction()) && isTxOutputSpendable(output)) { selected.add(output); total += output.getValue().value; } } } // Total may be lower than target here, if the given candidates were insufficient to create to requested // transaction. return new CoinSelection(Coin.valueOf(total), selected); } protected abstract boolean isDustAttackUtxo(TransactionOutput output); public Coin getChange(Coin target, CoinSelection coinSelection) throws InsufficientMoneyException { long value = target.value; long available = coinSelection.valueGathered.value; long change = available - value; if (change < 0) throw new InsufficientMoneyException(Coin.valueOf(change * -1)); return Coin.valueOf(change); } // We allow spending from own unconfirmed txs and if permitForeignPendingTx is set as well from foreign // unconfirmed txs. protected boolean isTxSpendable(Transaction tx) { TransactionConfidence confidence = tx.getConfidence(); TransactionConfidence.ConfidenceType type = confidence.getConfidenceType(); boolean isConfirmed = type.equals(TransactionConfidence.ConfidenceType.BUILDING); boolean isPending = type.equals(TransactionConfidence.ConfidenceType.PENDING); boolean isOwnTx = confidence.getSource().equals(TransactionConfidence.Source.SELF); return isConfirmed || (isPending && (permitForeignPendingTx || isOwnTx)); } abstract boolean isTxOutputSpendable(TransactionOutput output); // TODO Why it uses coin age and not try to minimize number of inputs as the highest priority? // Asked Oscar and he also don't knows why coin age is used. Should be changed so that min. number of inputs is // target. protected void sortOutputs(ArrayList outputs) { Collections.sort(outputs, (a, b) -> { int depth1 = a.getParentTransactionDepthInBlocks(); int depth2 = b.getParentTransactionDepthInBlocks(); Coin aValue = a.getValue(); Coin bValue = b.getValue(); BigInteger aCoinDepth = BigInteger.valueOf(aValue.value).multiply(BigInteger.valueOf(depth1)); BigInteger bCoinDepth = BigInteger.valueOf(bValue.value).multiply(BigInteger.valueOf(depth2)); int c1 = bCoinDepth.compareTo(aCoinDepth); if (c1 != 0) return c1; // The "coin*days" destroyed are equal, sort by value alone to get the lowest transaction size. int c2 = bValue.compareTo(aValue); if (c2 != 0) return c2; // They are entirely equivalent (possibly pending) so sort by hash to ensure a total ordering. BigInteger aHash = a.getParentTransactionHash() != null ? a.getParentTransactionHash().toBigInteger() : BigInteger.ZERO; BigInteger bHash = b.getParentTransactionHash() != null ? b.getParentTransactionHash().toBigInteger() : BigInteger.ZERO; return aHash.compareTo(bHash); }); } } ================================================ FILE: core/src/main/java/haveno/core/xmr/wallet/HavenoRiskAnalysis.java ================================================ /* * Copyright 2013 Google Inc. * Copyright 2014 Andreas Schildbach * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * 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 . */ package haveno.core.xmr.wallet; import org.bitcoinj.core.Coin; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.ECKey.ECDSASignature; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.SignatureDecodeException; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionConfidence; import org.bitcoinj.core.TransactionInput; import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.crypto.TransactionSignature; import org.bitcoinj.script.ScriptChunk; import org.bitcoinj.script.ScriptPattern; import org.bitcoinj.wallet.RiskAnalysis; import org.bitcoinj.wallet.Wallet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.util.List; import static com.google.common.base.Preconditions.checkState; // Copied from DefaultRiskAnalysis as DefaultRiskAnalysis has mostly private methods and constructor so we cannot // override it. // The changes to DefaultRiskAnalysis are: removal of the RBF check and accept as standard an OP_RETURN outputs // with 0 value. // For Haveno's use cases RBF is not considered risky. Requiring a confirmation for RBF payments from a user's // external wallet to Haveno would hurt usability. The trade transaction requires anyway a confirmation and we don't see // a use case where a Haveno user accepts unconfirmed payment from untrusted peers and would not wait anyway for at least // one confirmation. /** *

    The default risk analysis. Currently, it only is concerned with whether a tx/dependency is non-final or not, and * whether a tx/dependency violates the dust rules. Outside of specialised protocols you should not encounter non-final * transactions.

    */ public class HavenoRiskAnalysis implements RiskAnalysis { private static final Logger log = LoggerFactory.getLogger(HavenoRiskAnalysis.class); /** * Any standard output smaller than this value (in satoshis) will be considered risky, as it's most likely be * rejected by the network. This is usually the same as {@link Transaction#MIN_NONDUST_OUTPUT} but can be * different when the fee is about to change in Bitcoin Core. */ public static final Coin MIN_ANALYSIS_NONDUST_OUTPUT = Transaction.MIN_NONDUST_OUTPUT; protected final Transaction tx; protected final List dependencies; @Nullable protected final Wallet wallet; private Transaction nonStandard; protected Transaction nonFinal; protected boolean analyzed; private HavenoRiskAnalysis(Wallet wallet, Transaction tx, List dependencies) { this.tx = tx; this.dependencies = dependencies; this.wallet = wallet; } @Override public Result analyze() { checkState(!analyzed); analyzed = true; Result result = analyzeIsFinal(); if (result != null && result != Result.OK) return result; return analyzeIsStandard(); } @Nullable private Result analyzeIsFinal() { // Transactions we create ourselves are, by definition, not at risk of double spending against us. if (tx.getConfidence().getSource() == TransactionConfidence.Source.SELF) return Result.OK; // Commented out to accept replace-by-fee txs. // // We consider transactions that opt into replace-by-fee at risk of double spending. // if (tx.isOptInFullRBF()) { // nonFinal = tx; // return Result.NON_FINAL; // } // Relative time-locked transactions are risky too. We can't check the locks because usually we don't know the // spent outputs (to know when they were created). if (tx.hasRelativeLockTime()) { nonFinal = tx; return Result.NON_FINAL; } if (wallet == null) return null; final int height = wallet.getLastBlockSeenHeight(); final long time = wallet.getLastBlockSeenTimeSecs(); // If the transaction has a lock time specified in blocks, we consider that if the tx would become final in the // next block it is not risky (as it would confirm normally). final int adjustedHeight = height + 1; if (!tx.isFinal(adjustedHeight, time)) { nonFinal = tx; return Result.NON_FINAL; } for (Transaction dep : dependencies) { if (!dep.isFinal(adjustedHeight, time)) { nonFinal = dep; return Result.NON_FINAL; } } return Result.OK; } /** * The reason a transaction is considered non-standard, returned by * {@link #isStandard(org.bitcoinj.core.Transaction)}. */ public enum RuleViolation { NONE, VERSION, DUST, SHORTEST_POSSIBLE_PUSHDATA, NONEMPTY_STACK, // Not yet implemented (for post 0.12) SIGNATURE_CANONICAL_ENCODING } /** *

    Checks if a transaction is considered "standard" by Bitcoin Core's IsStandardTx and AreInputsStandard * functions.

    * *

    Note that this method currently only implements a minimum of checks. More to be added later.

    */ public static RuleViolation isStandard(Transaction tx) { // TODO: Finish this function off. if (tx.getVersion() > 2 || tx.getVersion() < 1) { log.warn("TX considered non-standard due to unknown version number {}", tx.getVersion()); return RuleViolation.VERSION; } final List outputs = tx.getOutputs(); for (int i = 0; i < outputs.size(); i++) { TransactionOutput output = outputs.get(i); RuleViolation violation = isOutputStandard(output); if (violation != RuleViolation.NONE) { log.warn("TX considered non-standard due to output {} violating rule {}", i, violation); return violation; } } final List inputs = tx.getInputs(); for (int i = 0; i < inputs.size(); i++) { TransactionInput input = inputs.get(i); RuleViolation violation = isInputStandard(input); if (violation != RuleViolation.NONE) { log.warn("TX considered non-standard due to input {} violating rule {}", i, violation); return violation; } } return RuleViolation.NONE; } /** * Checks the output to see if the script violates a standardness rule. Not complete. */ public static RuleViolation isOutputStandard(TransactionOutput output) { // OP_RETURN has usually output value zero, so we exclude that from the MIN_ANALYSIS_NONDUST_OUTPUT check if (!ScriptPattern.isOpReturn(output.getScriptPubKey()) && output.getValue().compareTo(MIN_ANALYSIS_NONDUST_OUTPUT) < 0) return RuleViolation.DUST; for (ScriptChunk chunk : output.getScriptPubKey().getChunks()) { if (chunk.isPushData() && !chunk.isShortestPossiblePushData()) return RuleViolation.SHORTEST_POSSIBLE_PUSHDATA; } return RuleViolation.NONE; } /** Checks if the given input passes some of the AreInputsStandard checks. Not complete. */ public static RuleViolation isInputStandard(TransactionInput input) { for (ScriptChunk chunk : input.getScriptSig().getChunks()) { if (chunk.data != null && !chunk.isShortestPossiblePushData()) return RuleViolation.SHORTEST_POSSIBLE_PUSHDATA; if (chunk.isPushData()) { ECDSASignature signature; try { signature = ECKey.ECDSASignature.decodeFromDER(chunk.data); } catch (SignatureDecodeException x) { // Doesn't look like a signature. signature = null; } if (signature != null) { if (!TransactionSignature.isEncodingCanonical(chunk.data)) return RuleViolation.SIGNATURE_CANONICAL_ENCODING; if (!signature.isCanonical()) return RuleViolation.SIGNATURE_CANONICAL_ENCODING; } } } return RuleViolation.NONE; } private Result analyzeIsStandard() { // The IsStandard rules don't apply on testnet, because they're just a safety mechanism and we don't want to // crush innovation with valueless test coins. if (wallet != null && !wallet.getNetworkParameters().getId().equals(NetworkParameters.ID_MAINNET)) return Result.OK; RuleViolation ruleViolation = isStandard(tx); if (ruleViolation != RuleViolation.NONE) { nonStandard = tx; return Result.NON_STANDARD; } for (Transaction dep : dependencies) { ruleViolation = isStandard(dep); if (ruleViolation != RuleViolation.NONE) { nonStandard = dep; return Result.NON_STANDARD; } } return Result.OK; } /** Returns the transaction that was found to be non-standard, or null. */ @Nullable public Transaction getNonStandard() { return nonStandard; } /** Returns the transaction that was found to be non-final, or null. */ @Nullable public Transaction getNonFinal() { return nonFinal; } @Override public String toString() { if (!analyzed) return "Pending risk analysis for " + tx.getTxId().toString(); else if (nonFinal != null) return "Risky due to non-finality of " + nonFinal.getTxId().toString(); else if (nonStandard != null) return "Risky due to non-standard tx " + nonStandard.getTxId().toString(); else return "Non-risky"; } public static class Analyzer implements RiskAnalysis.Analyzer { @Override public HavenoRiskAnalysis create(Wallet wallet, Transaction tx, List dependencies) { return new HavenoRiskAnalysis(wallet, tx, dependencies); } } public static Analyzer FACTORY = new Analyzer(); } ================================================ FILE: core/src/main/java/haveno/core/xmr/wallet/NonBsqCoinSelector.java ================================================ /* * 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 . */ package haveno.core.xmr.wallet; import com.google.inject.Inject; import haveno.core.user.Preferences; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionConfidence; import org.bitcoinj.core.TransactionOutput; /** * We use a specialized version of the CoinSelector based on the DefaultCoinSelector implementation. * We lookup for spendable outputs which matches our address of our address. */ @Slf4j public class NonBsqCoinSelector extends HavenoDefaultCoinSelector { @Setter private Preferences preferences; @Inject public NonBsqCoinSelector() { super(false); } @Override protected boolean isTxOutputSpendable(TransactionOutput output) { // output.getParentTransaction() cannot be null as it is checked in calling method Transaction parentTransaction = output.getParentTransaction(); if (parentTransaction == null) return false; // It is important to not allow pending txs as otherwise unconfirmed BSQ txs would be considered nonBSQ as // below outputIsNotInBsqState would be true. if (parentTransaction.getConfidence().getConfidenceType() != TransactionConfidence.ConfidenceType.BUILDING) return false; return true; } // Prevent usage of dust attack utxos @Override protected boolean isDustAttackUtxo(TransactionOutput output) { return output.getValue().value < preferences.getIgnoreDustThreshold(); } } ================================================ FILE: core/src/main/java/haveno/core/xmr/wallet/Restrictions.java ================================================ /* * 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 . */ package haveno.core.xmr.wallet; import haveno.common.config.Config; import haveno.core.trade.HavenoUtils; import org.bitcoinj.core.Coin; import java.math.BigInteger; public class Restrictions { // configure restrictions private static final double MIN_SECURITY_DEPOSIT_PCT = 0.15; private static final double MAX_SECURITY_DEPOSIT_PCT = 0.5; private static final BigInteger MIN_TRADE_AMOUNT = HavenoUtils.xmrToAtomicUnits(0.05); private static final BigInteger MIN_SECURITY_DEPOSIT = HavenoUtils.xmrToAtomicUnits(0.1); private static final int MAX_EXTRA_INFO_LENGTH = 1500; private static final int MAX_OFFERS_WITH_SHARED_FUNDS = 10; // At mediation we require a min. payout to the losing party to keep incentive for the trader to accept the // mediated payout. For Refund agent cases we do not have that restriction. private static BigInteger MIN_REFUND_AT_MEDIATED_DISPUTE; public static Coin getMinNonDustOutput() { if (minNonDustOutput == null) minNonDustOutput = Config.baseCurrencyNetwork().getParameters().getMinNonDustOutput(); return minNonDustOutput; } private static Coin minNonDustOutput; public static boolean isAboveDust(Coin amount) { return amount.compareTo(getMinNonDustOutput()) >= 0; } public static boolean isDust(Coin amount) { return !isAboveDust(amount); } public static BigInteger getMinTradeAmount() { return MIN_TRADE_AMOUNT; } public static double getDefaultSecurityDepositPct() { return MIN_SECURITY_DEPOSIT_PCT; } public static double getMinSecurityDepositPct() { return MIN_SECURITY_DEPOSIT_PCT; } public static double getMaxSecurityDepositPct() { return MAX_SECURITY_DEPOSIT_PCT; } public static BigInteger getMinSecurityDeposit() { return MIN_SECURITY_DEPOSIT; } public static int getMaxExtraInfoLength() { return MAX_EXTRA_INFO_LENGTH; } public static int getMaxOffersWithSharedFunds() { return MAX_OFFERS_WITH_SHARED_FUNDS; } // This value must be lower than MIN_BUYER_SECURITY_DEPOSIT and SELLER_SECURITY_DEPOSIT public static BigInteger getMinRefundAtMediatedDispute() { if (MIN_REFUND_AT_MEDIATED_DISPUTE == null) MIN_REFUND_AT_MEDIATED_DISPUTE = HavenoUtils.xmrToAtomicUnits(0.0005); return MIN_REFUND_AT_MEDIATED_DISPUTE; } public static int getLockTime(boolean isAsset) { // 10 days for cryptos, 20 days for other payment methods return isAsset ? 144 * 10 : 144 * 20; } } ================================================ FILE: core/src/main/java/haveno/core/xmr/wallet/TradeWalletService.java ================================================ /* * 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 . */ package haveno.core.xmr.wallet; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.collect.ImmutableList; import com.google.inject.Inject; import haveno.common.config.Config; import haveno.common.util.Tuple2; import haveno.core.user.Preferences; import haveno.core.xmr.exceptions.SigningException; import haveno.core.xmr.exceptions.TransactionVerificationException; import haveno.core.xmr.exceptions.WalletException; import haveno.core.xmr.model.InputsAndChangeOutput; import haveno.core.xmr.model.PreparedDepositTxAndMakerInputs; import haveno.core.xmr.model.RawTransactionInput; import haveno.core.xmr.setup.WalletConfig; import haveno.core.xmr.setup.WalletsSetup; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.bitcoinj.core.Address; import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.Coin; import org.bitcoinj.core.ECKey; 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; import org.bitcoinj.core.TransactionInput; import org.bitcoinj.core.TransactionOutPoint; import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.core.TransactionWitness; import org.bitcoinj.core.Utils; import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.crypto.TransactionSignature; 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; import org.bouncycastle.crypto.params.KeyParameter; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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; private final NetworkParameters params; @Nullable private Wallet wallet; @Nullable private WalletConfig walletConfig; @Nullable private KeyParameter aesKey; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, initialization /////////////////////////////////////////////////////////////////////////////////////////// @Inject public TradeWalletService(WalletsSetup walletsSetup, Preferences preferences) { this.walletsSetup = walletsSetup; this.preferences = preferences; this.params = Config.baseCurrencyNetworkParameters(); walletsSetup.addSetupCompletedHandler(() -> { walletConfig = walletsSetup.getWalletConfig(); wallet = walletsSetup.getBtcWallet(); }); } /////////////////////////////////////////////////////////////////////////////////////////// // AesKey /////////////////////////////////////////////////////////////////////////////////////////// void setAesKey(@Nullable KeyParameter newAesKey) { this.aesKey = newAesKey; } @Nullable public KeyParameter getAesKey() { return aesKey; } /////////////////////////////////////////////////////////////////////////////////////////// // Deposit tx /////////////////////////////////////////////////////////////////////////////////////////// // We construct the deposit transaction in the way that the buyer is always the first entry (inputs, outputs, MS keys) and then the seller. // In the creation of the deposit tx the taker/maker roles are the determining roles instead of buyer/seller. // In the payout tx it is the buyer/seller role. We keep the buyer/seller ordering over all transactions to not get confusion with ordering, // which is important to follow correctly specially for the order of the MS keys. /** * The taker creates a dummy transaction to get the input(s) and optional change output for the amount and the * taker's address for that trade. That will be used to send to the maker for creating the deposit transaction. * * @param takeOfferFeeTx the take offer fee tx * @param inputAmount amount of takers input * @param txFee mining fee * @return a data container holding the inputs, the output value and address * @throws TransactionVerificationException if there was an unexpected problem with the created dummy tx */ public InputsAndChangeOutput takerCreatesDepositTxInputs(Transaction takeOfferFeeTx, Coin inputAmount, Coin txFee) throws TransactionVerificationException { // We add the mining fee 2 times to the deposit tx: // 1. Will be spent when publishing the deposit tx (paid by buyer) // 2. Will be added to the MS amount, so when publishing the payout tx the fee is already there and the outputs are not changed by fee reduction // The fee for the payout will be paid by the seller. /* The tx we create has that structure: IN[0] input from taker fee tx > inputAmount (including tx fee) (unsigned) OUT[0] dummyOutputAmount (inputAmount - tx fee) We are only interested in the inputs. We get the exact input value from the taker fee tx so we don't create a change output. */ // inputAmount includes the tx fee. So we subtract the fee to get the dummyOutputAmount. Coin dummyOutputAmount = inputAmount.subtract(txFee); 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, SegwitAddress.fromKey(params, new ECKey())); dummyTX.addOutput(dummyOutput); // Find the needed inputs to pay the output, optionally add 1 change output. // Normally only 1 input and no change output is used, but we support multiple inputs and 1 change output. // Our spending transaction output is from the create offer fee payment. // We created the take offer fee tx in the structure that the second output is for the funds for the deposit tx. TransactionOutput reservedForTradeOutput = takeOfferFeeTx.getOutputs().get(1); checkArgument(reservedForTradeOutput.getValue().equals(inputAmount), "Reserve amount does not equal input amount"); dummyTX.addInput(reservedForTradeOutput); WalletService.verifyTransaction(dummyTX); //WalletService.printTx("dummyTX", dummyTX); List rawTransactionInputList = dummyTX.getInputs().stream().map(e -> { checkNotNull(e.getConnectedOutput(), "e.getConnectedOutput() must not be null"); checkNotNull(e.getConnectedOutput().getParentTransaction(), "e.getConnectedOutput().getParentTransaction() must not be null"); checkNotNull(e.getValue(), "e.getValue() must not be null"); return getRawInputFromTransactionInput(e); }).collect(Collectors.toList()); // TODO changeOutputValue and changeOutputAddress is not used as taker spends exact amount from fee tx. // Change is handled already at the fee tx creation so the handling of a change output for the deposit tx // can be removed here. We still keep it atm as we prefer to not introduce a larger // refactoring. When new trade protocol gets implemented this can be cleaned. // The maker though can have a change output if the taker takes less as the max. offer amount! return new InputsAndChangeOutput(new ArrayList<>(rawTransactionInputList), 0, null); } public PreparedDepositTxAndMakerInputs sellerAsMakerCreatesDepositTx(byte[] contractHash, Coin makerInputAmount, Coin msOutputAmount, List takerRawTransactionInputs, long takerChangeOutputValue, @Nullable String takerChangeAddressString, Address makerAddress, Address makerChangeAddress, byte[] buyerPubKey, byte[] sellerPubKey) throws SigningException, TransactionVerificationException, WalletException, AddressFormatException { return makerCreatesDepositTx(false, contractHash, makerInputAmount, msOutputAmount, takerRawTransactionInputs, takerChangeOutputValue, takerChangeAddressString, makerAddress, makerChangeAddress, buyerPubKey, sellerPubKey); } public PreparedDepositTxAndMakerInputs buyerAsMakerCreatesAndSignsDepositTx(byte[] contractHash, Coin makerInputAmount, Coin msOutputAmount, List takerRawTransactionInputs, long takerChangeOutputValue, @Nullable String takerChangeAddressString, Address makerAddress, Address makerChangeAddress, byte[] buyerPubKey, byte[] sellerPubKey) throws SigningException, TransactionVerificationException, WalletException, AddressFormatException { return makerCreatesDepositTx(true, contractHash, makerInputAmount, msOutputAmount, takerRawTransactionInputs, takerChangeOutputValue, takerChangeAddressString, makerAddress, makerChangeAddress, buyerPubKey, sellerPubKey); } /** * The maker creates the deposit transaction using the takers input(s) and optional output and signs his input(s). * * @param makerIsBuyer the flag indicating if we are in the maker as buyer role or the opposite * @param contractHash the hash of the contract to be added to the OP_RETURN output * @param makerInputAmount the input amount of the maker * @param msOutputAmount the output amount to our MS output * @param takerRawTransactionInputs raw data for the connected outputs for all inputs of the taker (normally 1 input) * @param takerChangeOutputValue optional taker change output value * @param takerChangeAddressString optional taker change address * @param makerAddress the maker's address * @param makerChangeAddress the maker's change address * @param buyerPubKey the public key of the buyer * @param sellerPubKey the public key of the seller * @return a data container holding the serialized transaction and the maker raw inputs * @throws SigningException if there was an unexpected problem signing (one of) the input(s) from the maker's wallet * @throws AddressFormatException if the taker base58 change address doesn't parse or its checksum is invalid * @throws TransactionVerificationException if there was an unexpected problem with the deposit tx or its signature(s) * @throws WalletException if the maker's wallet is null or there was an error choosing deposit tx input(s) from it */ private PreparedDepositTxAndMakerInputs makerCreatesDepositTx(boolean makerIsBuyer, byte[] contractHash, Coin makerInputAmount, Coin msOutputAmount, List takerRawTransactionInputs, long takerChangeOutputValue, @Nullable String takerChangeAddressString, Address makerAddress, Address makerChangeAddress, byte[] buyerPubKey, byte[] sellerPubKey) throws SigningException, TransactionVerificationException, WalletException, AddressFormatException { checkArgument(!takerRawTransactionInputs.isEmpty()); // 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, 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. List makerInputs = dummyTx.getInputs(); TransactionOutput makerOutput = null; // We don't support more than 1 optional change output checkArgument(dummyTx.getOutputs().size() < 3, "dummyTx.getOutputs().size() >= 3"); // Only save change outputs, the dummy output is ignored (that's why we start with index 1) if (dummyTx.getOutputs().size() > 1) { makerOutput = dummyTx.getOutput(1); } // Now we construct the real deposit tx Transaction preparedDepositTx = new Transaction(params); ArrayList makerRawTransactionInputs = new ArrayList<>(); if (makerIsBuyer) { // Add buyer inputs for (TransactionInput input : makerInputs) { preparedDepositTx.addInput(input); makerRawTransactionInputs.add(getRawInputFromTransactionInput(input)); } // Add seller inputs // the seller's input is not signed so we attach empty script bytes for (RawTransactionInput rawTransactionInput : takerRawTransactionInputs) preparedDepositTx.addInput(getTransactionInput(preparedDepositTx, new byte[]{}, rawTransactionInput)); } else { // taker is buyer role // Add buyer inputs // the seller's input is not signed so we attach empty script bytes for (RawTransactionInput rawTransactionInput : takerRawTransactionInputs) preparedDepositTx.addInput(getTransactionInput(preparedDepositTx, new byte[]{}, rawTransactionInput)); // Add seller inputs for (TransactionInput input : makerInputs) { preparedDepositTx.addInput(input); makerRawTransactionInputs.add(getRawInputFromTransactionInput(input)); } } // Add MultiSig output Script hashedMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey, false); // Tx fee for deposit tx will be paid by buyer. 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, ScriptBuilder.createOpReturnScript(contractHash).getProgram()); preparedDepositTx.addOutput(contractHashOutput); TransactionOutput takerTransactionOutput = null; if (takerChangeOutputValue > 0 && takerChangeAddressString != null) { takerTransactionOutput = new TransactionOutput(params, preparedDepositTx, Coin.valueOf(takerChangeOutputValue), Address.fromString(params, takerChangeAddressString)); } if (makerIsBuyer) { // Add optional buyer outputs if (makerOutput != null) { preparedDepositTx.addOutput(makerOutput); } // Add optional seller outputs if (takerTransactionOutput != null) { preparedDepositTx.addOutput(takerTransactionOutput); } } else { // taker is buyer role // Add optional seller outputs if (takerTransactionOutput != null) { preparedDepositTx.addOutput(takerTransactionOutput); } // Add optional buyer outputs if (makerOutput != null) { preparedDepositTx.addOutput(makerOutput); } } int start = makerIsBuyer ? 0 : takerRawTransactionInputs.size(); int end = makerIsBuyer ? makerInputs.size() : preparedDepositTx.getInputs().size(); for (int i = start; i < end; i++) { TransactionInput input = preparedDepositTx.getInput(i); signInput(preparedDepositTx, input, i); WalletService.checkScriptSig(preparedDepositTx, input, i); } WalletService.printTx("makerCreatesDepositTx", preparedDepositTx); WalletService.verifyTransaction(preparedDepositTx); return new PreparedDepositTxAndMakerInputs(makerRawTransactionInputs, preparedDepositTx.bitcoinSerialize()); } /** * The taker signs the deposit transaction he received from the maker and publishes it. * * @param takerIsSeller the flag indicating if we are in the taker as seller role or the opposite * @param contractHash the hash of the contract to be added to the OP_RETURN output * @param makersDepositTxSerialized the prepared deposit transaction signed by the maker * @param msOutputAmount the MultiSig output amount, as determined by the taker * @param buyerInputs the connected outputs for all inputs of the buyer * @param sellerInputs the connected outputs for all inputs of the seller * @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 non-P2WH maker-as-buyer input wasn't signed, the maker's MultiSig * script, contract hash or output amount 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, byte[] contractHash, byte[] makersDepositTxSerialized, Coin msOutputAmount, List buyerInputs, List sellerInputs, byte[] buyerPubKey, byte[] sellerPubKey) throws SigningException, TransactionVerificationException, WalletException { Transaction makersDepositTx = new Transaction(params, makersDepositTxSerialized); checkArgument(!buyerInputs.isEmpty()); checkArgument(!sellerInputs.isEmpty()); // Check if maker's MultiSig script is identical to the taker's Script hashedMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey, false); if (!makersDepositTx.getOutput(0).getScriptPubKey().equals(hashedMultiSigOutputScript)) { throw new TransactionVerificationException("Maker's hashedMultiSigOutputScript does not match taker's hashedMultiSigOutputScript"); } // Check if maker's MultiSig output value is identical to the taker's if (!makersDepositTx.getOutput(0).getValue().equals(msOutputAmount)) { throw new TransactionVerificationException("Maker's MultiSig output amount does not match taker's MultiSig output amount"); } // The outpoints are not available from the serialized makersDepositTx, so we cannot use that tx directly, but we use it to construct a new // depositTx Transaction depositTx = new Transaction(params); if (takerIsSeller) { // Add buyer inputs and apply signature // 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 = 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()); } depositTx.addInput(input); } // Add seller inputs for (RawTransactionInput rawTransactionInput : sellerInputs) { depositTx.addInput(getTransactionInput(depositTx, new byte[]{}, rawTransactionInput)); } } else { // taker is buyer // Add buyer inputs and apply signature for (RawTransactionInput rawTransactionInput : buyerInputs) { depositTx.addInput(getTransactionInput(depositTx, new byte[]{}, rawTransactionInput)); } // Add seller inputs // We grab the signature from the makersDepositTx and apply it to the new tx input for (int i = buyerInputs.size(), k = 0; i < makersDepositTx.getInputs().size(); i++, k++) { TransactionInput transactionInput = makersDepositTx.getInputs().get(i); // We get the deposit tx unsigned if maker is seller depositTx.addInput(getTransactionInput(depositTx, new byte[]{}, sellerInputs.get(k))); } } // Check if OP_RETURN output with contract hash matches the one from the maker TransactionOutput contractHashOutput = new TransactionOutput(params, makersDepositTx, Coin.ZERO, ScriptBuilder.createOpReturnScript(contractHash).getProgram()); log.debug("contractHashOutput {}", contractHashOutput); TransactionOutput makersContractHashOutput = makersDepositTx.getOutputs().get(1); log.debug("makersContractHashOutput {}", makersContractHashOutput); if (!makersContractHashOutput.getScriptPubKey().equals(contractHashOutput.getScriptPubKey())) { throw new TransactionVerificationException("Maker's transaction output for the contract hash is not matching taker's version."); } // Add all outputs from makersDepositTx to depositTx makersDepositTx.getOutputs().forEach(depositTx::addOutput); WalletService.printTx("makersDepositTx", makersDepositTx); // Sign inputs int start = takerIsSeller ? buyerInputs.size() : 0; int end = takerIsSeller ? depositTx.getInputs().size() : buyerInputs.size(); for (int i = start; i < end; i++) { TransactionInput input = depositTx.getInput(i); signInput(depositTx, input, i); WalletService.checkScriptSig(depositTx, input, i); } WalletService.printTx("takerSignsDepositTx", depositTx); WalletService.verifyTransaction(depositTx); WalletService.checkWalletConsistency(wallet); return depositTx; } public void sellerAsMakerFinalizesDepositTx(Transaction myDepositTx, Transaction takersDepositTx, int numTakersInputs) throws TransactionVerificationException, AddressFormatException { // We add takers signature from his inputs and add it to out tx which was already signed earlier. for (int i = 0; i < numTakersInputs; i++) { TransactionInput takersInput = takersDepositTx.getInput(i); Script takersScriptSig = takersInput.getScriptSig(); TransactionInput txInput = myDepositTx.getInput(i); txInput.setScriptSig(takersScriptSig); TransactionWitness witness = takersInput.getWitness(); if (!TransactionWitness.EMPTY.equals(witness)) { txInput.setWitness(witness); } } WalletService.printTx("sellerAsMakerFinalizesDepositTx", myDepositTx); WalletService.verifyTransaction(myDepositTx); } 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 /////////////////////////////////////////////////////////////////////////////////////////// public Transaction createDelayedUnsignedPayoutTx(Transaction depositTx, String donationAddressString, Coin minerFee, long lockTime) throws AddressFormatException, TransactionVerificationException { TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0); Transaction delayedPayoutTx = new Transaction(params); delayedPayoutTx.addInput(hashedMultiSigOutput); applyLockTime(lockTime, delayedPayoutTx); Coin outputAmount = hashedMultiSigOutput.getValue().subtract(minerFee); delayedPayoutTx.addOutput(outputAmount, Address.fromString(params, donationAddressString)); WalletService.printTx("Unsigned delayedPayoutTx ToDonationAddress", delayedPayoutTx); WalletService.verifyTransaction(delayedPayoutTx); return delayedPayoutTx; } public byte[] signDelayedPayoutTx(Transaction delayedPayoutTx, Transaction preparedDepositTx, DeterministicKey myMultiSigKeyPair, byte[] buyerPubKey, byte[] sellerPubKey) throws AddressFormatException, TransactionVerificationException { Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); 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); } ECKey.ECDSASignature mySignature = myMultiSigKeyPair.sign(sigHash, aesKey).toCanonicalised(); WalletService.printTx("delayedPayoutTx for sig creation", delayedPayoutTx); WalletService.verifyTransaction(delayedPayoutTx); 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 { TransactionInput input = delayedPayoutTx.getInput(0); finalizeUnconnectedDelayedPayoutTx(delayedPayoutTx, buyerPubKey, sellerPubKey, buyerSignature, sellerSignature, input.getValue()); WalletService.checkWalletConsistency(wallet); checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null"); input.verify(input.getConnectedOutput()); return delayedPayoutTx; } /////////////////////////////////////////////////////////////////////////////////////////// // Standard payout tx /////////////////////////////////////////////////////////////////////////////////////////// /** * Seller signs payout transaction, buyer has not signed yet. * * @param depositTx deposit transaction * @param buyerPayoutAmount payout amount for buyer * @param sellerPayoutAmount payout amount for seller * @param buyerPayoutAddressString address for buyer * @param sellerPayoutAddressString address for seller * @param multiSigKeyPair DeterministicKey for MultiSig from seller * @param buyerPubKey the public key of the buyer * @param sellerPubKey the public key of the seller * @return DER encoded canonical signature * @throws AddressFormatException if the buyer or seller base58 address doesn't parse or its checksum is invalid * @throws TransactionVerificationException if there was an unexpected problem with the payout tx or its signature */ public byte[] buyerSignsPayoutTx(Transaction depositTx, Coin buyerPayoutAmount, Coin sellerPayoutAmount, String buyerPayoutAddressString, String sellerPayoutAddressString, DeterministicKey multiSigKeyPair, byte[] buyerPubKey, byte[] sellerPubKey) throws AddressFormatException, TransactionVerificationException { Transaction preparedPayoutTx = createPayoutTx(depositTx, buyerPayoutAmount, sellerPayoutAmount, buyerPayoutAddressString, sellerPayoutAddressString); // MS redeemScript Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); // MS output from prev. tx is index 0 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); } ECKey.ECDSASignature buyerSignature = multiSigKeyPair.sign(sigHash, aesKey).toCanonicalised(); WalletService.printTx("prepared payoutTx", preparedPayoutTx); WalletService.verifyTransaction(preparedPayoutTx); return buyerSignature.encodeToDER(); } /** * Seller creates and signs payout transaction and adds signature of buyer to complete the transaction. * * @param depositTx deposit transaction * @param buyerSignature DER encoded canonical signature of buyer * @param buyerPayoutAmount payout amount for buyer * @param sellerPayoutAmount payout amount for seller * @param buyerPayoutAddressString address for buyer * @param sellerPayoutAddressString address for seller * @param multiSigKeyPair seller's key pair for MultiSig * @param buyerPubKey the public key of the buyer * @param sellerPubKey the public key of the seller * @return the payout transaction * @throws AddressFormatException if the buyer or seller base58 address doesn't parse or its checksum is invalid * @throws TransactionVerificationException if there was an unexpected problem with the payout tx or its signatures * @throws WalletException if the seller's wallet is null or structurally inconsistent */ @Deprecated public Transaction sellerSignsAndFinalizesPayoutTx(Transaction depositTx, byte[] buyerSignature, Coin buyerPayoutAmount, Coin sellerPayoutAmount, String buyerPayoutAddressString, String sellerPayoutAddressString, DeterministicKey multiSigKeyPair, byte[] buyerPubKey, byte[] sellerPubKey) throws AddressFormatException, TransactionVerificationException, WalletException, SignatureDecodeException { Transaction payoutTx = createPayoutTx(depositTx, buyerPayoutAmount, sellerPayoutAmount, buyerPayoutAddressString, sellerPayoutAddressString); // MS redeemScript Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); // MS output from prev. tx is index 0 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); } ECKey.ECDSASignature sellerSignature = multiSigKeyPair.sign(sigHash, aesKey).toCanonicalised(); TransactionSignature buyerTxSig = new TransactionSignature(ECKey.ECDSASignature.decodeFromDER(buyerSignature), 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) TransactionInput input = payoutTx.getInput(0); 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); WalletService.checkScriptSig(payoutTx, input, 0); checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null"); input.verify(input.getConnectedOutput()); return payoutTx; } /////////////////////////////////////////////////////////////////////////////////////////// // Mediated payoutTx /////////////////////////////////////////////////////////////////////////////////////////// public byte[] signMediatedPayoutTx(Transaction depositTx, Coin buyerPayoutAmount, Coin sellerPayoutAmount, String buyerPayoutAddressString, String sellerPayoutAddressString, DeterministicKey myMultiSigKeyPair, byte[] buyerPubKey, byte[] sellerPubKey) throws AddressFormatException, TransactionVerificationException { Transaction preparedPayoutTx = createPayoutTx(depositTx, buyerPayoutAmount, sellerPayoutAmount, buyerPayoutAddressString, sellerPayoutAddressString); // MS redeemScript Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); // MS output from prev. tx is index 0 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); } ECKey.ECDSASignature mySignature = myMultiSigKeyPair.sign(sigHash, aesKey).toCanonicalised(); WalletService.printTx("prepared mediated payoutTx for sig creation", preparedPayoutTx); WalletService.verifyTransaction(preparedPayoutTx); return mySignature.encodeToDER(); } public Transaction finalizeMediatedPayoutTx(Transaction depositTx, byte[] buyerSignature, byte[] sellerSignature, Coin buyerPayoutAmount, Coin sellerPayoutAmount, String buyerPayoutAddressString, String sellerPayoutAddressString, DeterministicKey multiSigKeyPair, byte[] buyerPubKey, byte[] sellerPubKey) throws AddressFormatException, TransactionVerificationException, WalletException, SignatureDecodeException { Transaction payoutTx = createPayoutTx(depositTx, buyerPayoutAmount, sellerPayoutAmount, buyerPayoutAddressString, sellerPayoutAddressString); // MS redeemScript Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); // MS output from prev. tx is index 0 checkNotNull(multiSigKeyPair, "multiSigKeyPair must not be null"); TransactionSignature buyerTxSig = new TransactionSignature(ECKey.ECDSASignature.decodeFromDER(buyerSignature), Transaction.SigHash.ALL, false); 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) TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0); boolean hashedMultiSigOutputIsLegacy = ScriptPattern.isP2SH(hashedMultiSigOutput.getScriptPubKey()); TransactionInput input = payoutTx.getInput(0); 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); WalletService.checkScriptSig(payoutTx, input, 0); checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null"); input.verify(input.getConnectedOutput()); return payoutTx; } /////////////////////////////////////////////////////////////////////////////////////////// // Arbitrated payoutTx /////////////////////////////////////////////////////////////////////////////////////////// // TODO: Once we have removed legacy arbitrator from dispute domain we can remove that method as well. // Atm it is still used by ArbitrationManager. /** * A trader who got the signed tx from the arbitrator finalizes the payout tx. * * @param depositTxSerialized serialized deposit tx * @param arbitratorSignature DER encoded canonical signature of arbitrator * @param buyerPayoutAmount payout amount of the buyer * @param sellerPayoutAmount payout amount of the seller * @param buyerAddressString the address of the buyer * @param sellerAddressString the address of the seller * @param tradersMultiSigKeyPair the key pair for the MultiSig of the trader who calls that method * @param buyerPubKey the public key of the buyer * @param sellerPubKey the public key of the seller * @param arbitratorPubKey the public key of the arbitrator * @return the completed payout tx * @throws AddressFormatException if the buyer or seller base58 address doesn't parse or its checksum is invalid * @throws TransactionVerificationException if there was an unexpected problem with the payout tx or its signature * @throws WalletException if the trade wallet is null or structurally inconsistent */ public Transaction traderSignAndFinalizeDisputedPayoutTx(byte[] depositTxSerialized, byte[] arbitratorSignature, Coin buyerPayoutAmount, Coin sellerPayoutAmount, String buyerAddressString, String sellerAddressString, DeterministicKey tradersMultiSigKeyPair, byte[] buyerPubKey, byte[] sellerPubKey, byte[] arbitratorPubKey) throws AddressFormatException, TransactionVerificationException, WalletException, SignatureDecodeException { Transaction depositTx = new Transaction(params, depositTxSerialized); TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0); Transaction payoutTx = new Transaction(params); payoutTx.addInput(hashedMultiSigOutput); if (buyerPayoutAmount.isPositive()) { payoutTx.addOutput(buyerPayoutAmount, Address.fromString(params, buyerAddressString)); } if (sellerPayoutAmount.isPositive()) { payoutTx.addOutput(sellerPayoutAmount, Address.fromString(params, sellerAddressString)); } // take care of sorting! Script redeemScript = get2of3MultiSigRedeemScript(buyerPubKey, sellerPubKey, arbitratorPubKey); 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); } ECKey.ECDSASignature tradersSignature = tradersMultiSigKeyPair.sign(sigHash, aesKey).toCanonicalised(); TransactionSignature tradersTxSig = new TransactionSignature(tradersSignature, Transaction.SigHash.ALL, false); TransactionSignature arbitratorTxSig = new TransactionSignature(ECKey.ECDSASignature.decodeFromDER(arbitratorSignature), Transaction.SigHash.ALL, false); TransactionInput input = payoutTx.getInput(0); // 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); WalletService.checkScriptSig(payoutTx, input, 0); checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null"); input.verify(input.getConnectedOutput()); return payoutTx; } /////////////////////////////////////////////////////////////////////////////////////////// // Emergency payoutTx /////////////////////////////////////////////////////////////////////////////////////////// public Tuple2 emergencyBuildPayoutTxFrom2of2MultiSig(String depositTxHex, Coin buyerPayoutAmount, Coin sellerPayoutAmount, Coin txFee, String buyerAddressString, String sellerAddressString, String buyerPubKeyAsHex, String sellerPubKeyAsHex, boolean hashedMultiSigOutputIsLegacy) { byte[] buyerPubKey = ECKey.fromPublicOnly(Utils.HEX.decode(buyerPubKeyAsHex)).getPubKey(); byte[] sellerPubKey = ECKey.fromPublicOnly(Utils.HEX.decode(sellerPubKeyAsHex)).getPubKey(); Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); Coin msOutputValue = buyerPayoutAmount.add(sellerPayoutAmount).add(txFee); Transaction payoutTx = new Transaction(params); Sha256Hash spendTxHash = Sha256Hash.wrap(depositTxHex); payoutTx.addInput(new TransactionInput(params, payoutTx, new byte[]{}, new TransactionOutPoint(params, 0, spendTxHash), msOutputValue)); if (buyerPayoutAmount.isPositive()) { payoutTx.addOutput(buyerPayoutAmount, Address.fromString(params, buyerAddressString)); } if (sellerPayoutAmount.isPositive()) { payoutTx.addOutput(sellerPayoutAmount, Address.fromString(params, sellerAddressString)); } String redeemScriptHex = Utils.HEX.encode(redeemScript.getProgram()); String unsignedTxHex = Utils.HEX.encode(payoutTx.bitcoinSerialize(!hashedMultiSigOutputIsLegacy)); return new Tuple2<>(redeemScriptHex, unsignedTxHex); } public String emergencyGenerateSignature(String rawTxHex, String redeemScriptHex, Coin inputValue, String myPrivKeyAsHex) throws IllegalArgumentException { boolean hashedMultiSigOutputIsLegacy = true; if (rawTxHex.startsWith("010000000001")) hashedMultiSigOutputIsLegacy = false; byte[] payload = Utils.HEX.decode(rawTxHex); Transaction payoutTx = new Transaction(params, payload, null, params.getDefaultSerializer(), payload.length); Script redeemScript = new Script(Utils.HEX.decode(redeemScriptHex)); Sha256Hash sigHash; if (hashedMultiSigOutputIsLegacy) { sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); } else { sigHash = payoutTx.hashForWitnessSignature(0, redeemScript, inputValue, Transaction.SigHash.ALL, false); } ECKey myPrivateKey = ECKey.fromPrivate(Utils.HEX.decode(myPrivKeyAsHex)); checkNotNull(myPrivateKey, "key must not be null"); ECKey.ECDSASignature myECDSASignature = myPrivateKey.sign(sigHash, aesKey).toCanonicalised(); TransactionSignature myTxSig = new TransactionSignature(myECDSASignature, Transaction.SigHash.ALL, false); return Utils.HEX.encode(myTxSig.encodeToBitcoin()); } public Tuple2 emergencyApplySignatureToPayoutTxFrom2of2MultiSig(String unsignedTxHex, String redeemScriptHex, String buyerSignatureAsHex, String sellerSignatureAsHex, boolean hashedMultiSigOutputIsLegacy) throws AddressFormatException, SignatureDecodeException { Transaction payoutTx = new Transaction(params, Utils.HEX.decode(unsignedTxHex)); TransactionSignature buyerTxSig = TransactionSignature.decodeFromBitcoin(Utils.HEX.decode(buyerSignatureAsHex), true, true); TransactionSignature sellerTxSig = TransactionSignature.decodeFromBitcoin(Utils.HEX.decode(sellerSignatureAsHex), true, true); Script redeemScript = new Script(Utils.HEX.decode(redeemScriptHex)); TransactionInput input = payoutTx.getInput(0); 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); } String txId = payoutTx.getTxId().toString(); String signedTxHex = Utils.HEX.encode(payoutTx.bitcoinSerialize(!hashedMultiSigOutputIsLegacy)); return new Tuple2<>(txId, signedTxHex); } /////////////////////////////////////////////////////////////////////////////////////////// // Misc /////////////////////////////////////////////////////////////////////////////////////////// /** * Returns the local existing wallet transaction with the given ID, or {@code null} if missing. * * @param txId the transaction ID of the transaction we want to lookup */ public Transaction getWalletTx(Sha256Hash txId) { checkNotNull(wallet); return wallet.getTransaction(txId); } public void commitTx(Transaction tx) { checkNotNull(wallet); wallet.commitTx(tx); } public Transaction getClonedTransaction(Transaction tx) { return new Transaction(params, tx.bitcoinSerialize()); } /////////////////////////////////////////////////////////////////////////////////////////// // Private methods /////////////////////////////////////////////////////////////////////////////////////////// private RawTransactionInput getRawInputFromTransactionInput(@NotNull TransactionInput input) { checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null"); checkNotNull(input.getConnectedOutput().getParentTransaction(), "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 haveno 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 TransactionInput getTransactionInput(Transaction depositTx, byte[] scriptProgram, RawTransactionInput rawTransactionInput) { 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. // Don't use ScriptBuilder.createRedeemScript and ScriptBuilder.createP2SHOutputScript as they use a sorting // (Collections.sort(pubKeys, ECKey.PUBKEY_COMPARATOR);) which can lead to a non-matching list of signatures with pubKeys and the executeMultiSig does // not iterate all possible combinations of sig/pubKeys leading to a verification fault. That nasty bug happens just randomly as the list after sorting // might differ from the provided one or not. // Changing the while loop in executeMultiSig to fix that does not help as the reference implementation seems to behave the same (not iterating all // possibilities) . // Furthermore the executed list is reversed to the provided. // Best practice is to provide the list sorted by the least probable successful candidates first (arbitrator is first -> will be last in execution loop, so // avoiding unneeded expensive ECKey.verify calls) private Script get2of3MultiSigRedeemScript(byte[] buyerPubKey, byte[] sellerPubKey, byte[] arbitratorPubKey) { ECKey buyerKey = ECKey.fromPublicOnly(buyerPubKey); ECKey sellerKey = ECKey.fromPublicOnly(sellerPubKey); ECKey arbitratorKey = ECKey.fromPublicOnly(arbitratorPubKey); // Take care of sorting! Need to reverse to the order we use normally (buyer, seller, arbitrator) List keys = ImmutableList.of(arbitratorKey, sellerKey, buyerKey); return ScriptBuilder.createMultiSigOutputScript(2, keys); } private Script get2of2MultiSigRedeemScript(byte[] buyerPubKey, byte[] sellerPubKey) { ECKey buyerKey = ECKey.fromPublicOnly(buyerPubKey); ECKey sellerKey = ECKey.fromPublicOnly(sellerPubKey); // Take care of sorting! Need to reverse to the order we use normally (buyer, seller) List keys = ImmutableList.of(sellerKey, buyerKey); return ScriptBuilder.createMultiSigOutputScript(2, keys); } 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, Coin buyerPayoutAmount, Coin sellerPayoutAmount, String buyerAddressString, String sellerAddressString) throws AddressFormatException { TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0); Transaction transaction = new Transaction(params); transaction.addInput(hashedMultiSigOutput); if (buyerPayoutAmount.isPositive()) { transaction.addOutput(buyerPayoutAmount, Address.fromString(params, buyerAddressString)); } if (sellerPayoutAmount.isPositive()) { transaction.addOutput(sellerPayoutAmount, Address.fromString(params, sellerAddressString)); } checkArgument(transaction.getOutputs().size() >= 1, "We need at least one output."); return transaction; } private void signInput(Transaction transaction, TransactionInput input, int inputIndex) throws SigningException { checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null"); Script scriptPubKey = input.getConnectedOutput().getScriptPubKey(); checkNotNull(wallet); ECKey sigKey = input.getOutpoint().getConnectedKey(wallet); checkNotNull(sigKey, "signInput: sigKey must not be null. input.getOutpoint()=" + input.getOutpoint().toString()); if (sigKey.isEncrypted()) { checkNotNull(aesKey); } if (ScriptPattern.isP2PK(scriptPubKey) || ScriptPattern.isP2PKH(scriptPubKey)) { Sha256Hash hash = transaction.hashForSignature(inputIndex, scriptPubKey, Transaction.SigHash.ALL, false); ECKey.ECDSASignature signature = sigKey.sign(hash, aesKey); TransactionSignature txSig = new TransactionSignature(signature, Transaction.SigHash.ALL, false); if (ScriptPattern.isP2PK(scriptPubKey)) { input.setScriptSig(ScriptBuilder.createInputScript(txSig)); } else if (ScriptPattern.isP2PKH(scriptPubKey)) { input.setScriptSig(ScriptBuilder.createInputScript(txSig, sigKey)); } } else if (ScriptPattern.isP2WPKH(scriptPubKey)) { // 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, aesKey, scriptCode, value, Transaction.SigHash.ALL, false); input.setScriptSig(ScriptBuilder.createEmpty()); input.setWitness(TransactionWitness.redeemP2WPKH(txSig, sigKey)); } else { throw new SigningException("Don't know how to sign for this kind of scriptPubKey: " + scriptPubKey); } } private void addAvailableInputsAndChangeOutputs(Transaction transaction, Address address, Address changeAddress) throws WalletException { SendRequest sendRequest = null; try { // Let the framework do the work to find the right inputs sendRequest = SendRequest.forTx(transaction); sendRequest.shuffleOutputs = false; sendRequest.aesKey = aesKey; // We use a fixed fee sendRequest.fee = Coin.ZERO; sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; // we allow spending of unconfirmed tx (double spend risk is low and usability would suffer if we need to wait for 1 confirmation) sendRequest.coinSelector = new BtcCoinSelector(address, preferences.getIgnoreDustThreshold()); // We use always the same address in a trade for all transactions sendRequest.changeAddress = changeAddress; // With the usage of completeTx() we get all the work done with fee calculation, validation and coin selection. // We don't commit that tx to the wallet as it will be changed later and it's not signed yet. // So it will not change the wallet balance. checkNotNull(wallet, "wallet must not be null"); wallet.completeTx(sendRequest); } catch (Throwable t) { if (sendRequest != null && sendRequest.tx != null) { log.warn("addAvailableInputsAndChangeOutputs: sendRequest.tx={}, sendRequest.tx.getOutputs()={}", sendRequest.tx, sendRequest.tx.getOutputs()); } throw new WalletException(t); } } private void applyLockTime(long lockTime, Transaction tx) { checkArgument(!tx.getInputs().isEmpty(), "The tx must have inputs. tx={}", tx); tx.getInputs().forEach(input -> input.setSequenceNumber(TransactionInput.NO_SEQUENCE - 1)); tx.setLockTime(lockTime); } // HAVENO issue #4039: prevent dust outputs from being created. // check all the outputs in a proposed transaction, if any are below the dust threshold // remove them, noting the details in the log. returns 'true' to indicate if any dust was // removed. private boolean removeDust(Transaction transaction) { List originalTransactionOutputs = transaction.getOutputs(); List keepTransactionOutputs = new ArrayList<>(); for (TransactionOutput transactionOutput : originalTransactionOutputs) { if (transactionOutput.getValue().isLessThan(Restrictions.getMinNonDustOutput())) { log.info("your transaction would have contained a dust output of {}", transactionOutput.toString()); } else { keepTransactionOutputs.add(transactionOutput); } } // if dust was detected, keepTransactionOutputs will have fewer elements than originalTransactionOutputs // set the transaction outputs to what we saved in keepTransactionOutputs, thus discarding dust. if (keepTransactionOutputs.size() != originalTransactionOutputs.size()) { log.info("dust output was detected and removed, the new output is as follows:"); transaction.clearOutputs(); for (TransactionOutput transactionOutput : keepTransactionOutputs) { transaction.addOutput(transactionOutput); log.info("{}", transactionOutput.toString()); } return true; // dust was removed } return false; // no action necessary } } ================================================ FILE: core/src/main/java/haveno/core/xmr/wallet/WalletService.java ================================================ /* * 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 . */ package haveno.core.xmr.wallet; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import com.google.common.collect.ImmutableMultiset; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.Multiset; import com.google.common.collect.SetMultimap; import com.google.inject.Inject; import haveno.common.config.Config; import haveno.core.user.Preferences; import haveno.core.xmr.exceptions.TransactionVerificationException; import haveno.core.xmr.exceptions.WalletException; import haveno.core.xmr.listeners.AddressConfidenceListener; import haveno.core.xmr.listeners.BalanceListener; import haveno.core.xmr.listeners.TxConfidenceListener; import haveno.core.xmr.setup.WalletsSetup; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; import javax.annotation.Nullable; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import monero.wallet.MoneroWallet; import monero.wallet.model.MoneroTxWallet; import org.bitcoinj.core.Address; import org.bitcoinj.core.BlockChain; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Context; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionConfidence; import org.bitcoinj.core.TransactionInput; import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.core.TransactionWitness; import org.bitcoinj.core.VerificationException; import org.bitcoinj.core.listeners.NewBestBlockListener; import org.bitcoinj.core.listeners.TransactionConfidenceEventListener; import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.crypto.KeyCrypter; import org.bitcoinj.crypto.KeyCrypterScrypt; import org.bitcoinj.crypto.TransactionSignature; import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptBuilder; import org.bitcoinj.script.ScriptChunk; import org.bitcoinj.script.ScriptException; import org.bitcoinj.script.ScriptPattern; import org.bitcoinj.signers.TransactionSigner; import org.bitcoinj.utils.Threading; import org.bitcoinj.wallet.DecryptingKeyBag; import org.bitcoinj.wallet.DeterministicSeed; import org.bitcoinj.wallet.KeyBag; import org.bitcoinj.wallet.RedeemData; import org.bitcoinj.wallet.Wallet; import org.bitcoinj.wallet.listeners.WalletChangeEventListener; import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener; import org.bitcoinj.wallet.listeners.WalletCoinsSentEventListener; import org.bitcoinj.wallet.listeners.WalletReorganizeEventListener; import org.bouncycastle.crypto.params.KeyParameter; import org.jetbrains.annotations.NotNull; /** * Abstract base class for BTC wallet. Provides all non-trade specific functionality. */ @Slf4j public abstract class WalletService { protected final WalletsSetup walletsSetup; protected final Preferences preferences; protected final NetworkParameters params; private final HavenoWalletListener walletEventListener = new HavenoWalletListener(); private final CopyOnWriteArraySet addressConfidenceListeners = new CopyOnWriteArraySet<>(); private final CopyOnWriteArraySet txConfidenceListeners = new CopyOnWriteArraySet<>(); private final CopyOnWriteArraySet balanceListeners = new CopyOnWriteArraySet<>(); private final WalletChangeEventListener cacheInvalidationListener; private final AtomicReference> txOutputAddressCache = new AtomicReference<>(); private final AtomicReference> addressToMatchingTxSetCache = new AtomicReference<>(); @Getter protected Wallet wallet; @Getter protected KeyParameter aesKey; @Getter protected IntegerProperty chainHeightProperty = new SimpleIntegerProperty(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject WalletService(WalletsSetup walletsSetup, Preferences preferences) { this.walletsSetup = walletsSetup; this.preferences = preferences; params = walletsSetup.getParams(); cacheInvalidationListener = wallet -> { txOutputAddressCache.set(null); addressToMatchingTxSetCache.set(null); }; } /////////////////////////////////////////////////////////////////////////////////////////// // Lifecycle /////////////////////////////////////////////////////////////////////////////////////////// protected void addListenersToWallet() { if (wallet != null) { wallet.addCoinsReceivedEventListener(walletEventListener); wallet.addCoinsSentEventListener(walletEventListener); wallet.addReorganizeEventListener(walletEventListener); wallet.addTransactionConfidenceEventListener(walletEventListener); wallet.addChangeEventListener(Threading.SAME_THREAD, cacheInvalidationListener); } } public void shutDown() { if (wallet != null) { wallet.removeCoinsReceivedEventListener(walletEventListener); wallet.removeCoinsSentEventListener(walletEventListener); wallet.removeReorganizeEventListener(walletEventListener); wallet.removeTransactionConfidenceEventListener(walletEventListener); wallet.removeChangeEventListener(cacheInvalidationListener); } } /////////////////////////////////////////////////////////////////////////////////////////// // Package scope Methods /////////////////////////////////////////////////////////////////////////////////////////// void decryptWallet(@NotNull KeyParameter key) { wallet.decrypt(key); aesKey = null; } void encryptWallet(KeyCrypterScrypt keyCrypterScrypt, KeyParameter key) { if (this.aesKey != null) { log.warn("encryptWallet called but we have a aesKey already set. " + "We decryptWallet with the old key before we apply the new key."); decryptWallet(this.aesKey); } wallet.encrypt(keyCrypterScrypt, key); aesKey = key; } void setAesKey(KeyParameter aesKey) { this.aesKey = aesKey; } abstract String getWalletAsString(boolean includePrivKeys); /////////////////////////////////////////////////////////////////////////////////////////// // Listener /////////////////////////////////////////////////////////////////////////////////////////// public void addAddressConfidenceListener(AddressConfidenceListener listener) { addressConfidenceListeners.add(listener); } public void removeAddressConfidenceListener(AddressConfidenceListener listener) { addressConfidenceListeners.remove(listener); } public void addTxConfidenceListener(TxConfidenceListener listener) { txConfidenceListeners.add(listener); } public void removeTxConfidenceListener(TxConfidenceListener listener) { txConfidenceListeners.remove(listener); } public void addBalanceListener(BalanceListener listener) { balanceListeners.add(listener); } public void removeBalanceListener(BalanceListener listener) { balanceListeners.remove(listener); } /////////////////////////////////////////////////////////////////////////////////////////// // Checks /////////////////////////////////////////////////////////////////////////////////////////// public static void checkWalletConsistency(Wallet wallet) throws WalletException { try { checkNotNull(wallet); checkState(wallet.isConsistent()); } catch (Throwable t) { t.printStackTrace(); log.error(t.getMessage()); throw new WalletException(t); } } public static void verifyTransaction(Transaction transaction) throws TransactionVerificationException { try { transaction.verify(); } catch (Throwable t) { t.printStackTrace(); log.error(t.getMessage()); throw new TransactionVerificationException(t); } } public static void checkAllScriptSignaturesForTx(Transaction transaction) throws TransactionVerificationException { for (int i = 0; i < transaction.getInputs().size(); i++) { WalletService.checkScriptSig(transaction, transaction.getInputs().get(i), i); } } public static void checkScriptSig(Transaction transaction, TransactionInput input, int inputIndex) throws TransactionVerificationException { try { checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null"); input.getScriptSig().correctlySpends(transaction, inputIndex, input.getWitness(), input.getValue(), input.getConnectedOutput().getScriptPubKey(), Script.ALL_VERIFY_FLAGS); } catch (Throwable t) { t.printStackTrace(); log.error(t.getMessage()); throw new TransactionVerificationException(t); } } /////////////////////////////////////////////////////////////////////////////////////////// // Sign tx /////////////////////////////////////////////////////////////////////////////////////////// public static void signTransactionInput(Wallet wallet, KeyParameter aesKey, Transaction tx, TransactionInput txIn, int index) { KeyBag maybeDecryptingKeyBag = new DecryptingKeyBag(wallet, aesKey); if (txIn.getConnectedOutput() != null) { try { // We assume if it's already signed, it's hopefully got a SIGHASH type that will not invalidate when // we sign missing pieces (to check this would require either assuming any signatures are signing // standard output types or a way to get processed signatures out of script execution) txIn.getScriptSig().correctlySpends(tx, index, txIn.getWitness(), txIn.getValue(), txIn.getConnectedOutput().getScriptPubKey(), Script.ALL_VERIFY_FLAGS); log.warn("Input {} already correctly spends output, assuming SIGHASH type used will be safe and skipping signing.", index); return; } catch (ScriptException e) { // Expected. } Script scriptPubKey = txIn.getConnectedOutput().getScriptPubKey(); RedeemData redeemData = txIn.getConnectedRedeemData(maybeDecryptingKeyBag); checkNotNull(redeemData, "Transaction exists in wallet that we cannot redeem: %s", txIn.getOutpoint().getHash()); txIn.setScriptSig(scriptPubKey.createEmptyInputScript(redeemData.keys.get(0), redeemData.redeemScript)); TransactionSigner.ProposedTransaction propTx = new TransactionSigner.ProposedTransaction(tx); Transaction partialTx = propTx.partialTx; txIn = partialTx.getInput(index); if (txIn.getConnectedOutput() != null) { // If we don't have a sig we don't do the check to avoid error reports of failed sig checks final List chunks = txIn.getConnectedOutput().getScriptPubKey().getChunks(); if (!chunks.isEmpty() && chunks.get(0).data != null && chunks.get(0).data.length > 0) { try { // We assume if it's already signed, it's hopefully got a SIGHASH type that will not invalidate when // we sign missing pieces (to check this would require either assuming any signatures are signing // standard output types or a way to get processed signatures out of script execution) txIn.getScriptSig().correctlySpends(tx, index, txIn.getWitness(), txIn.getValue(), txIn.getConnectedOutput().getScriptPubKey(), Script.ALL_VERIFY_FLAGS); log.warn("Input {} already correctly spends output, assuming SIGHASH type used will be safe and skipping signing.", index); return; } catch (ScriptException e) { // Expected. } } redeemData = txIn.getConnectedRedeemData(maybeDecryptingKeyBag); scriptPubKey = txIn.getConnectedOutput().getScriptPubKey(); checkNotNull(redeemData, "redeemData must not be null"); ECKey pubKey = redeemData.keys.get(0); if (pubKey instanceof DeterministicKey) propTx.keyPaths.put(scriptPubKey, (((DeterministicKey) pubKey).getPath())); ECKey key; if ((key = redeemData.getFullKey()) == null) { log.warn("No local key found for input {}", index); return; } Script inputScript = txIn.getScriptSig(); byte[] script = redeemData.redeemScript.getProgram(); if (ScriptPattern.isP2PK(scriptPubKey) || ScriptPattern.isP2PKH(scriptPubKey)) { try { TransactionSignature signature = partialTx.calculateSignature(index, key, script, Transaction.SigHash.ALL, false); inputScript = scriptPubKey.getScriptSigWithSignature(inputScript, signature.encodeToBitcoin(), 0); txIn.setScriptSig(inputScript); } catch (ECKey.KeyIsEncryptedException e1) { throw e1; } catch (ECKey.MissingPrivateKeyException e1) { log.warn("No private key in keypair for input {}", index); } } else if (ScriptPattern.isP2WPKH(scriptPubKey)) { try { // 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, aesKey, scriptCode, value, Transaction.SigHash.ALL, false); txIn.setScriptSig(ScriptBuilder.createEmpty()); txIn.setWitness(TransactionWitness.redeemP2WPKH(txSig, key)); } catch (ECKey.KeyIsEncryptedException e1) { throw e1; } catch (ECKey.MissingPrivateKeyException e1) { log.warn("No private key in keypair for input {}", index); } } else { // log.error("Unexpected script type."); throw new RuntimeException("Unexpected script type."); } } else { log.warn("Missing connected output, assuming input {} is already signed.", index); } } else { log.error("Missing connected output, assuming already signed."); } } /////////////////////////////////////////////////////////////////////////////////////////// // TransactionConfidence /////////////////////////////////////////////////////////////////////////////////////////// @Nullable public TransactionConfidence getConfidenceForAddress(Address address) { List transactionConfidenceList = new ArrayList<>(); if (wallet != null) { Set transactions = getAddressToMatchingTxSetMultiset().get(address); transactionConfidenceList.addAll(transactions.stream().map(tx -> getTransactionConfidence(tx, address)).collect(Collectors.toList())); } return getMostRecentConfidence(transactionConfidenceList); } private SetMultimap getAddressToMatchingTxSetMultiset() { return addressToMatchingTxSetCache.updateAndGet(set -> set != null ? set : computeAddressToMatchingTxSetMultimap()); } private SetMultimap computeAddressToMatchingTxSetMultimap() { return wallet.getTransactions(false).stream() .collect(ImmutableSetMultimap.flatteningToImmutableSetMultimap( Function.identity(), (Function>) ( t -> getOutputsWithConnectedOutputs(t).stream() .map(WalletService::getAddressFromOutput) .filter(Objects::nonNull)))) .inverse(); } @Nullable public TransactionConfidence getConfidenceForTxId(String txId) { if (wallet != null) { Set transactions = wallet.getTransactions(false); for (Transaction tx : transactions) { if (tx.getTxId().toString().equals(txId)) return tx.getConfidence(); } } return null; } @Nullable private TransactionConfidence getTransactionConfidence(Transaction tx, Address address) { List transactionConfidenceList = getOutputsWithConnectedOutputs(tx).stream() .filter(output -> address != null && address.equals(getAddressFromOutput(output))) .flatMap(o -> Stream.ofNullable(o.getParentTransaction())) .map(Transaction::getConfidence) .collect(Collectors.toList()); return getMostRecentConfidence(transactionConfidenceList); } private List getOutputsWithConnectedOutputs(Transaction tx) { List transactionOutputs = tx.getOutputs(); List connectedOutputs = new ArrayList<>(); // add all connected outputs from any inputs as well List transactionInputs = tx.getInputs(); for (TransactionInput transactionInput : transactionInputs) { TransactionOutput transactionOutput = transactionInput.getConnectedOutput(); if (transactionOutput != null) { connectedOutputs.add(transactionOutput); } } List mergedOutputs = new ArrayList<>(); mergedOutputs.addAll(transactionOutputs); mergedOutputs.addAll(connectedOutputs); return mergedOutputs; } @Nullable private TransactionConfidence getMostRecentConfidence(List transactionConfidenceList) { TransactionConfidence transactionConfidence = null; for (TransactionConfidence confidence : transactionConfidenceList) { if (confidence != null) { if (transactionConfidence == null || confidence.getConfidenceType().equals(TransactionConfidence.ConfidenceType.PENDING) || (confidence.getConfidenceType().equals(TransactionConfidence.ConfidenceType.BUILDING) && transactionConfidence.getConfidenceType().equals( TransactionConfidence.ConfidenceType.BUILDING) && confidence.getDepthInBlocks() < transactionConfidence.getDepthInBlocks())) { transactionConfidence = confidence; } } } return transactionConfidence; } /////////////////////////////////////////////////////////////////////////////////////////// // Balance /////////////////////////////////////////////////////////////////////////////////////////// public Coin getAvailableConfirmedBalance() { return wallet != null ? wallet.getBalance(Wallet.BalanceType.AVAILABLE) : Coin.ZERO; } public Coin getEstimatedBalance() { return wallet != null ? wallet.getBalance(Wallet.BalanceType.ESTIMATED) : Coin.ZERO; } public Coin getBalanceForAddress(Address address) { return wallet != null ? getBalance(wallet.calculateAllSpendCandidates(), address) : Coin.ZERO; } protected Coin getBalance(List transactionOutputs, Address address) { Coin balance = Coin.ZERO; for (TransactionOutput output : transactionOutputs) { if (!isDustAttackUtxo(output)) { if (isOutputScriptConvertibleToAddress(output) && address != null && address.equals(getAddressFromOutput(output))) balance = balance.add(output.getValue()); } } return balance; } protected abstract boolean isDustAttackUtxo(TransactionOutput output); public Coin getBalance(TransactionOutput output) { return getBalanceForAddress(getAddressFromOutput(output)); } /////////////////////////////////////////////////////////////////////////////////////////// // Tx outputs /////////////////////////////////////////////////////////////////////////////////////////// public int getNumTxOutputsForAddress(Address address) { return getTxOutputAddressMultiset().count(address); } private Multiset
    getTxOutputAddressMultiset() { return txOutputAddressCache.updateAndGet(set -> set != null ? set : computeTxOutputAddressMultiset()); } private Multiset
    computeTxOutputAddressMultiset() { return wallet.getTransactions(false).stream() .flatMap(t -> t.getOutputs().stream()) .map(WalletService::getAddressFromOutput) .filter(Objects::nonNull) .collect(ImmutableMultiset.toImmutableMultiset()); } public boolean isAddressUnused(Address address) { return getNumTxOutputsForAddress(address) == 0; } // HAVENO issue #4039: Prevent dust outputs from being created. // Check the outputs of a proposed transaction. If any are below the dust threshold, // add up the dust, log the details, and return the cumulative dust amount. public Coin getDust(Transaction proposedTransaction) { Coin dust = Coin.ZERO; for (TransactionOutput transactionOutput : proposedTransaction.getOutputs()) { if (transactionOutput.getValue().isLessThan(Restrictions.getMinNonDustOutput())) { dust = dust.add(transactionOutput.getValue()); log.info("Dust TXO = {}", transactionOutput.toString()); } } return dust; } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// public Transaction getTxFromSerializedTx(byte[] tx) { return new Transaction(params, tx); } public NetworkParameters getParams() { return params; } public int getBestChainHeight() { final BlockChain chain = walletsSetup.getChain(); return isWalletReady() && chain != null ? chain.getBestChainHeight() : 0; } public abstract boolean isWalletSyncedWithinTolerance(); public Transaction getClonedTransaction(Transaction tx) { return new Transaction(params, tx.bitcoinSerialize()); } /////////////////////////////////////////////////////////////////////////////////////////// // Wallet delegates to avoid direct access to wallet outside the service class /////////////////////////////////////////////////////////////////////////////////////////// public void addChangeEventListener(WalletChangeEventListener listener) { wallet.addChangeEventListener(Threading.USER_THREAD, listener); } public void removeChangeEventListener(WalletChangeEventListener listener) { wallet.removeChangeEventListener(listener); } public void addNewBestBlockListener(NewBestBlockListener listener) { final BlockChain chain = walletsSetup.getChain(); if (isWalletReady() && chain != null) chain.addNewBestBlockListener(listener); } public void removeNewBestBlockListener(NewBestBlockListener listener) { final BlockChain chain = walletsSetup.getChain(); if (isWalletReady() && chain != null) chain.removeNewBestBlockListener(listener); } public boolean isWalletReady() { return wallet != null; } public DeterministicSeed getKeyChainSeed() { return wallet.getKeyChainSeed(); } @Nullable public KeyCrypter getKeyCrypter() { return wallet.getKeyCrypter(); } public boolean checkAESKey(KeyParameter aesKey) { return wallet.checkAESKey(aesKey); } @Nullable public DeterministicKey findKeyFromPubKey(byte[] pubKey) { return (DeterministicKey) wallet.findKeyFromPubKey(pubKey); } public boolean isEncrypted() { return wallet.isEncrypted(); } public List getRecentTransactions(int numTransactions, boolean includeDead) { // Returns a list ordered by tx.getUpdateTime() desc return wallet.getRecentTransactions(numTransactions, includeDead); } public int getLastBlockSeenHeight() { return wallet.getLastBlockSeenHeight(); } /** * Check if there are more than 20 unconfirmed transactions in the chain right now. * * @return true when queue is full */ public boolean isUnconfirmedTransactionsLimitHit() { // For published delayed payout transactions we do not receive the tx confidence // so we cannot check if it is confirmed so we ignore it for that check. The check is any arbitrarily // using a limit of 20, so we don't need to be exact here. Should just reduce the likelihood of issues with // the too long chains of unconfirmed transactions. return getTransactions(false).stream() .filter(tx -> tx.getLockTime() == 0) .filter(Transaction::isPending) .count() > 20; } public Set getTransactions(boolean includeDead) { return wallet.getTransactions(includeDead); } public Coin getBalance(@SuppressWarnings("SameParameterValue") Wallet.BalanceType balanceType) { return wallet.getBalance(balanceType); } @Nullable public Transaction getTransaction(Sha256Hash hash) { return wallet.getTransaction(hash); } @Nullable public Transaction getTransaction(String txId) { if (txId == null) { return null; } return getTransaction(Sha256Hash.wrap(txId)); } public boolean isTransactionOutputMine(TransactionOutput transactionOutput) { return transactionOutput.isMine(wallet); } /* public boolean isTxOutputMine(TxOutput txOutput) { try { Script script = txOutput.getScript(); if (script.isSentToRawPubKey()) { byte[] pubkey = script.getPubKey(); return wallet.isPubKeyMine(pubkey); } if (script.isPayToScriptHash()) { return wallet.isPayToScriptHashMine(script.getPubKeyHash()); } else { byte[] pubkeyHash = script.getPubKeyHash(); return wallet.isPubKeyHashMine(pubkeyHash); } } catch (ScriptException e) { // Just means we didn't understand the output of this transaction: ignore it. log.debug("Could not parse tx output script: {}", e.toString()); return false; } }*/ public Coin getValueSentFromMeForTransaction(Transaction transaction) throws ScriptException { return transaction.getValueSentFromMe(wallet); } public Coin getValueSentToMeForTransaction(Transaction transaction) throws ScriptException { return transaction.getValueSentToMe(wallet); } /////////////////////////////////////////////////////////////////////////////////////////// // Util /////////////////////////////////////////////////////////////////////////////////////////// public static void printTx(String tracePrefix, Transaction tx) { log.info("\n" + tracePrefix + ":\n" + tx.toString()); } public static boolean isOutputScriptConvertibleToAddress(TransactionOutput output) { return ScriptPattern.isP2PKH(output.getScriptPubKey()) || ScriptPattern.isP2SH(output.getScriptPubKey()) || ScriptPattern.isP2WH(output.getScriptPubKey()); } @Nullable public static Address getAddressFromOutput(TransactionOutput output) { return isOutputScriptConvertibleToAddress(output) ? output.getScriptPubKey().getToAddress(Config.baseCurrencyNetworkParameters()) : null; } @Nullable public static String getAddressStringFromOutput(TransactionOutput output) { return isOutputScriptConvertibleToAddress(output) ? output.getScriptPubKey().getToAddress(Config.baseCurrencyNetworkParameters()).toString() : null; } /** * @param serializedTransaction The serialized transaction to be added to the wallet * @return The transaction we added to the wallet, which is different as the one we passed as argument! * @throws VerificationException */ public static Transaction maybeAddTxToWallet(byte[] serializedTransaction, Wallet wallet, TransactionConfidence.Source source) throws VerificationException { Transaction tx = new Transaction(wallet.getParams(), serializedTransaction); Transaction walletTransaction = wallet.getTransaction(tx.getTxId()); if (walletTransaction == null) { // We need to recreate the transaction otherwise we get a null pointer... tx.getConfidence(Context.get()).setSource(source); //wallet.maybeCommitTx(tx); wallet.receivePending(tx, null, true); return tx; } else { return walletTransaction; } } public static MoneroTxWallet maybeAddNetworkTxToWallet(byte[] serializedTransaction, MoneroWallet wallet) throws VerificationException { throw new RuntimeException("Not implemented"); // TODO (woodser): need to serialize/deserialize tx for xmr integration? // Transaction tx = new Transaction(wallet.getParams(), serializedTransaction); // Transaction walletTransaction = wallet.getTransaction(tx.getHash()); // // if (walletTransaction == null) { // // We need to recreate the transaction otherwise we get a null pointer... // tx.getConfidence(Context.get()).setSource(source); // //wallet.maybeCommitTx(tx); // wallet.receivePending(tx, null, true); // return tx; // } else { // return walletTransaction; // } } public static Transaction maybeAddNetworkTxToWallet(byte[] serializedTransaction, Wallet wallet) throws VerificationException { return maybeAddTxToWallet(serializedTransaction, wallet, TransactionConfidence.Source.NETWORK); } public static Transaction maybeAddSelfTxToWallet(Transaction transaction, Wallet wallet) throws VerificationException { return maybeAddTxToWallet(transaction, wallet, TransactionConfidence.Source.SELF); } public static Transaction maybeAddTxToWallet(Transaction transaction, Wallet wallet, TransactionConfidence.Source source) throws VerificationException { return maybeAddTxToWallet(transaction.bitcoinSerialize(), wallet, source); } /////////////////////////////////////////////////////////////////////////////////////////// // havenoWalletEventListener /////////////////////////////////////////////////////////////////////////////////////////// public class HavenoWalletListener implements WalletCoinsReceivedEventListener, WalletCoinsSentEventListener, WalletReorganizeEventListener, TransactionConfidenceEventListener { @Override public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) { notifyBalanceListeners(tx); } @Override public void onCoinsSent(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) { notifyBalanceListeners(tx); } @Override public void onReorganize(Wallet wallet) { log.warn("onReorganize "); } @Override public void onTransactionConfidenceChanged(Wallet wallet, Transaction tx) { for (AddressConfidenceListener addressConfidenceListener : addressConfidenceListeners) { TransactionConfidence confidence = getTransactionConfidence(tx, addressConfidenceListener.getAddress()); addressConfidenceListener.onTransactionConfidenceChanged(confidence); } txConfidenceListeners.stream() .filter(txConfidenceListener -> tx != null && tx.getTxId().toString() != null && txConfidenceListener != null && tx.getTxId().toString().equals(txConfidenceListener.getTxID())) .forEach(txConfidenceListener -> txConfidenceListener.onTransactionConfidenceChanged(tx.getConfidence())); } void notifyBalanceListeners(Transaction tx) { for (BalanceListener balanceListener : balanceListeners) { Coin balance; if (balanceListener.getAddress() != null) balance = getBalanceForAddress(balanceListener.getAddress()); else balance = getAvailableConfirmedBalance(); balanceListener.onBalanceChanged(balance, tx); } } } } ================================================ FILE: core/src/main/java/haveno/core/xmr/wallet/WalletsManager.java ================================================ /* * 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 . */ package haveno.core.xmr.wallet; import com.google.inject.Inject; import haveno.common.crypto.ScryptUtil; import haveno.common.handlers.ExceptionHandler; import haveno.common.handlers.ResultHandler; import haveno.core.locale.Res; import haveno.core.xmr.setup.WalletsSetup; import org.bitcoinj.crypto.KeyCrypter; import org.bitcoinj.crypto.KeyCrypterScrypt; import org.bitcoinj.wallet.DeterministicSeed; import org.bitcoinj.wallet.Wallet; import org.bouncycastle.crypto.params.KeyParameter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; // Convenience class to handle methods applied to several wallets public class WalletsManager { private static final Logger log = LoggerFactory.getLogger(WalletsManager.class); private final BtcWalletService btcWalletService; private final XmrWalletService xmrWalletService; private final TradeWalletService tradeWalletService; private final WalletsSetup walletsSetup; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public WalletsManager(BtcWalletService btcWalletService, XmrWalletService xmrWalletService, TradeWalletService tradeWalletService, WalletsSetup walletsSetup) { this.btcWalletService = btcWalletService; this.xmrWalletService = xmrWalletService; this.tradeWalletService = tradeWalletService; this.walletsSetup = walletsSetup; } public void decryptWallets(KeyParameter aesKey) { btcWalletService.decryptWallet(aesKey); tradeWalletService.setAesKey(null); } public void encryptWallets(KeyCrypterScrypt keyCrypterScrypt, KeyParameter aesKey) { try { btcWalletService.encryptWallet(keyCrypterScrypt, aesKey); // we save the key for the trade wallet as we don't require passwords here tradeWalletService.setAesKey(aesKey); } catch (Throwable t) { log.error(t.toString()); throw t; } } public String getWalletsAsString(boolean includePrivKeys) { final String baseCurrencyWalletDetails = Res.getBaseCurrencyCode() + " Wallet:\n" + btcWalletService.getWalletAsString(includePrivKeys); return baseCurrencyWalletDetails; } public void restoreSeedWords(@Nullable DeterministicSeed seed, ResultHandler resultHandler, ExceptionHandler exceptionHandler) { walletsSetup.restoreSeedWords(seed, resultHandler, exceptionHandler); } public void backupWallets() { walletsSetup.backupWallets(); } public void clearBackup() { walletsSetup.clearBackups(); } public boolean areWalletsEncrypted() { return xmrWalletService.isWalletEncrypted(); } public boolean areWalletsAvailable() { return xmrWalletService.isWalletAvailable(); } public KeyCrypterScrypt getKeyCrypterScrypt() { if (areWalletsEncrypted() && btcWalletService.getKeyCrypter() != null) return (KeyCrypterScrypt) btcWalletService.getKeyCrypter(); else return ScryptUtil.getKeyCrypterScrypt(); } public boolean checkAESKey(KeyParameter aesKey) { return btcWalletService.checkAESKey(aesKey); } public long getChainSeedCreationTimeSeconds() { return btcWalletService.getKeyChainSeed().getCreationTimeSeconds(); } public boolean hasPositiveBalance() { return btcWalletService.getBalance(Wallet.BalanceType.AVAILABLE) .isPositive(); } public void setAesKey(KeyParameter aesKey) { btcWalletService.setAesKey(aesKey); tradeWalletService.setAesKey(aesKey); } public DeterministicSeed getDecryptedSeed(KeyParameter aesKey, DeterministicSeed keyChainSeed, KeyCrypter keyCrypter) { if (keyCrypter != null) { return keyChainSeed.decrypt(keyCrypter, "", aesKey); } else { log.warn("keyCrypter is null"); return null; } } } ================================================ FILE: core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java ================================================ package haveno.core.xmr.wallet; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import haveno.common.ThreadUtils; import haveno.common.Timer; import haveno.common.UserThread; import haveno.core.api.XmrConnectionService; import haveno.core.trade.HavenoUtils; import haveno.core.xmr.setup.DownloadListener; import javafx.beans.property.LongProperty; import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.ReadOnlyLongProperty; import javafx.beans.property.SimpleLongProperty; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import monero.common.MoneroRpcConnection; import monero.common.TaskLooper; import monero.wallet.MoneroWallet; import monero.wallet.MoneroWalletFull; import monero.wallet.model.MoneroSyncResult; import monero.wallet.model.MoneroWalletListener; @Slf4j public abstract class XmrWalletBase { // constants private static final int SYNC_TIMEOUT_SECONDS = 180; private static final String SYNC_TIMEOUT_MSG = "Sync timeout called"; private static final String RECEIVED_ERROR_RESPONSE_MSG = "Received error response from RPC request"; private static final long SAVE_AFTER_ELAPSED_SECONDS = 300; protected long lastSaveTimeMs = 0; // inherited protected MoneroWallet wallet; @Getter protected final Object walletLock = new Object(); private final Object resetSyncProgressTimeoutLock = new Object(); protected Timer saveWalletDelayTimer; @Getter protected XmrConnectionService xmrConnectionService; protected boolean wasWalletSynced; protected boolean isSyncingWithoutProgress; protected boolean isSyncingWithProgress; protected Long syncStartHeight; protected TaskLooper syncProgressLooper; protected CountDownLatch syncProgressLatch; protected Exception syncProgressError; protected Timer syncProgressTimeout; protected long syncProgressTargetHeight; @Getter protected final DownloadListener syncProgressListener = new DownloadListener(); protected final LongProperty walletHeight = new SimpleLongProperty(0); @Getter protected boolean isShutDownStarted; @Getter protected boolean isShutDown; // private private boolean testReconnectOnStartup = false; // test reconnecting on startup while syncing so the wallet is blocked private String testReconnectMonerod1 = "http://xmr-node.cakewallet.com:18081"; private String testReconnectMonerod2 = "http://nodex.monerujo.io:18081"; public XmrWalletBase() { this.xmrConnectionService = HavenoUtils.xmrConnectionService; } public MoneroSyncResult sync() { return syncWithTimeout(SYNC_TIMEOUT_SECONDS); } public MoneroSyncResult syncWithTimeout(long timeoutSec) { synchronized (walletLock) { synchronized (HavenoUtils.getDaemonLock()) { ExecutorService executor = Executors.newSingleThreadExecutor(); Callable task = () -> { if (isSyncing()) log.warn("Syncing without progress while already syncing. That should never happen."); isSyncingWithoutProgress = true; walletHeight.set(wallet.getHeight()); MoneroSyncResult result = wallet.sync(); walletHeight.set(wallet.getHeight()); wasWalletSynced = true; return result; }; Future future = executor.submit(task); try { return future.get(timeoutSec, TimeUnit.SECONDS); } catch (TimeoutException e) { future.cancel(true); throw new RuntimeException(SYNC_TIMEOUT_MSG, e); } catch (ExecutionException e) { throw new RuntimeException("Sync failed", e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // restore interrupt status throw new RuntimeException("Sync was interrupted", e); } finally { isSyncingWithoutProgress = false; saveWalletIfElapsedTime(); executor.shutdownNow(); } } } } public void syncWithProgress() { MoneroWallet sourceWallet = wallet; synchronized (walletLock) { try { // set initial state if (isSyncing()) log.warn("Syncing with progress while already syncing. That should never happen."); resetSyncProgressTimeout(); isSyncingWithProgress = true; syncStartHeight = null; syncProgressError = null; syncProgressTargetHeight = xmrConnectionService.getTargetHeight(); updateSyncProgress(wallet.getHeight(), syncProgressTargetHeight); // done if already synced if (wallet.getHeight() >= syncProgressTargetHeight) { onDoneSyncWithProgress(); return; } // test connection changing on startup before wallet synced if (testReconnectOnStartup) { UserThread.runAfter(() -> { log.warn("Testing connection change on startup before wallet synced"); if (xmrConnectionService.getConnection().getUri().equals(testReconnectMonerod1)) xmrConnectionService.setConnection(testReconnectMonerod2); else xmrConnectionService.setConnection(testReconnectMonerod1); }, 1); testReconnectOnStartup = false; // only run once } // native wallet provides sync notifications if (wallet instanceof MoneroWalletFull) { if (testReconnectOnStartup) HavenoUtils.waitFor(1000); // delay sync to test wallet.sync(new MoneroWalletListener() { @Override public void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) { updateSyncProgress(height, endHeight); } }); onDoneSyncWithProgress(); return; } // start polling wallet for progress syncProgressLatch = new CountDownLatch(1); syncProgressLooper = new TaskLooper(() -> { // stop if wallet has changed if (wallet == null || wallet != sourceWallet) { syncProgressError = new RuntimeException("Wallet is null or has changed while syncing with progress"); syncProgressLatch.countDown(); return; } // get height long height; try { height = wallet.getHeight(); // can get read timeout while syncing } catch (Exception e) { if (wallet != null && !isShutDownStarted) { log.warn("Error getting wallet height while syncing with progress: " + e.getMessage()); } if (wallet == null || wallet != sourceWallet) { syncProgressError = new RuntimeException("Wallet is null or has changed while syncing with progress"); syncProgressLatch.countDown(); } return; } // update sync progress updateSyncProgress(height, syncProgressTargetHeight); if (height >= syncProgressTargetHeight) { syncProgressLatch.countDown(); } // update target height after each update to prevent stalling on new blocks syncProgressTargetHeight = xmrConnectionService.getTargetHeight(); }); wallet.startSyncing(xmrConnectionService.getRefreshPeriodMs()); syncProgressLooper.start(1000); // wait for sync to complete HavenoUtils.awaitLatch(syncProgressLatch); syncProgressLooper.stop(); // set synced or throw error if (syncProgressError == null) onDoneSyncWithProgress(); else throw new RuntimeException(syncProgressError); } catch (Exception e) { throw e; } finally { isSyncingWithProgress = false; if (syncProgressTimeout != null) syncProgressTimeout.stop(); } } } public boolean wasWalletSynced() { return wasWalletSynced; } public boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) { if (xmrConnectionService.requestSwitchToNextBestConnection(sourceConnection)) { onConnectionChanged(xmrConnectionService.getConnection()); // change connection on same thread return true; } return false; } public void saveWalletIfElapsedTime() { synchronized (walletLock) { if (System.currentTimeMillis() - lastSaveTimeMs >= SAVE_AFTER_ELAPSED_SECONDS * 1000) { saveWallet(); lastSaveTimeMs = System.currentTimeMillis(); } } } public void requestSaveWalletIfElapsedTime() { ThreadUtils.submitToPool(() -> saveWalletIfElapsedTime()); } public boolean isSyncing() { return isSyncingWithProgress || isSyncingWithoutProgress; } public ReadOnlyDoubleProperty downloadPercentageProperty() { return syncProgressListener.percentageProperty(); } public ReadOnlyLongProperty blocksRemainingProperty() { return syncProgressListener.blocksRemainingProperty(); } public static boolean isSyncWithProgressTimeout(Throwable e) { return e.getMessage() != null && e.getMessage().contains(SYNC_TIMEOUT_MSG); } // --------------------------------- ABSTRACT ----------------------------- public abstract void saveWallet(); protected abstract void onConnectionChanged(MoneroRpcConnection connection); // ------------------------------ PRIVATE HELPERS ------------------------- private void updateSyncProgress(Long height, long targetHeight) { // use last height if no update long appliedHeight = height == null ? walletHeight.get() : height; // reset progress timeout if height advanced if (appliedHeight != walletHeight.get()) { resetSyncProgressTimeout(); } // set wallet height walletHeight.set(appliedHeight); // calculate progress long blocksRemaining = appliedHeight <= 1 ? -1 : targetHeight - appliedHeight; // unknown blocks left if height <= 1 if (syncStartHeight == null && appliedHeight > 1) syncStartHeight = appliedHeight; double percent = syncStartHeight == null ? 0.0 : Math.min(1.0, targetHeight <= syncStartHeight ? 1.0 : ((double) appliedHeight - syncStartHeight) / (double) (targetHeight - syncStartHeight)); if (percent >= 1.0) wasWalletSynced = true; // set synced state before announcing progress // notify progress listener on user thread UserThread.execute(() -> { syncProgressListener.progress(percent, blocksRemaining); }); } private void resetSyncProgressTimeout() { synchronized (resetSyncProgressTimeoutLock) { if (syncProgressTimeout != null) syncProgressTimeout.stop(); syncProgressTimeout = UserThread.runAfter(() -> { if (isShutDownStarted) return; syncProgressError = new RuntimeException(SYNC_TIMEOUT_MSG); syncProgressLatch.countDown(); }, SYNC_TIMEOUT_SECONDS, TimeUnit.SECONDS); } } private void onDoneSyncWithProgress() { wasWalletSynced = true; // this is redundant but conservative to set again // stop syncing and save wallet if elapsed time if (wallet != null) { // can become null if interrupted by force close if (syncProgressError == null || !HavenoUtils.isUnresponsive(syncProgressError)) { // TODO: skipping stop sync if unresponsive because wallet will hang. if unresponsive, wallet is assumed to be force restarted by caller, but that should be done internally here instead of externally? wallet.stopSyncing(); saveWalletIfElapsedTime(); } } } protected boolean isExpectedWalletError(Exception e) { return e.getMessage() != null && (HavenoUtils.isUnresponsive(e) || e.getMessage().contains(RECEIVED_ERROR_RESPONSE_MSG)); // TODO: why does this error happen "normally"? } } ================================================ FILE: core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.core.xmr.wallet; import static com.google.common.base.Preconditions.checkState; import com.google.common.util.concurrent.Service.State; import com.google.inject.Inject; import com.google.inject.name.Named; import common.utils.JsonUtils; import haveno.common.ThreadUtils; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.config.Config; import haveno.common.file.FileUtil; import haveno.common.util.Utilities; import haveno.core.api.AccountServiceListener; import haveno.core.api.CoreAccountService; import haveno.core.api.XmrConnectionService; import haveno.core.offer.OpenOffer; import haveno.core.trade.BuyerTrade; import haveno.core.trade.HavenoUtils; import haveno.core.trade.MakerTrade; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.trade.protocol.TradeProtocol; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.xmr.listeners.XmrBalanceListener; import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.model.XmrAddressEntryList; import haveno.core.xmr.setup.MoneroWalletRpcManager; import haveno.core.xmr.setup.WalletsSetup; import java.io.File; import java.math.BigInteger; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.Stream; import javafx.beans.property.LongProperty; import javafx.beans.value.ChangeListener; import monero.common.MoneroError; import monero.common.MoneroRpcConnection; import monero.common.MoneroRpcError; import monero.common.MoneroUtils; import monero.common.TaskLooper; import monero.daemon.MoneroDaemonRpc; import monero.daemon.model.MoneroDaemonInfo; import monero.daemon.model.MoneroFeeEstimate; import monero.daemon.model.MoneroKeyImage; import monero.daemon.model.MoneroNetworkType; import monero.daemon.model.MoneroOutput; import monero.daemon.model.MoneroSubmitTxResult; import monero.daemon.model.MoneroTx; import monero.wallet.MoneroWallet; import monero.wallet.MoneroWalletFull; import monero.wallet.MoneroWalletRpc; import monero.wallet.model.MoneroCheckTx; import monero.wallet.model.MoneroDestination; import monero.wallet.model.MoneroIncomingTransfer; import monero.wallet.model.MoneroOutputQuery; import monero.wallet.model.MoneroOutputWallet; import monero.wallet.model.MoneroSubaddress; import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxPriority; import monero.wallet.model.MoneroTxQuery; import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroWalletConfig; import monero.wallet.model.MoneroWalletListenerI; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class XmrWalletService extends XmrWalletBase { private static final Logger log = LoggerFactory.getLogger(XmrWalletService.class); // monero configuration public static final int NUM_BLOCKS_UNLOCK = 10; public static final String MONERO_BINS_DIR = Config.appDataDir().getAbsolutePath(); public static final String MONERO_WALLET_RPC_NAME = Utilities.isWindows() ? "monero-wallet-rpc.exe" : "monero-wallet-rpc"; public static final String MONERO_WALLET_RPC_PATH = MONERO_BINS_DIR + File.separator + MONERO_WALLET_RPC_NAME; public static final MoneroTxPriority PROTOCOL_FEE_PRIORITY = MoneroTxPriority.DEFAULT; public static final int MONERO_LOG_LEVEL = -1; // monero library log level, -1 to disable private static final MoneroNetworkType MONERO_NETWORK_TYPE = getMoneroNetworkType(); private static final MoneroWalletRpcManager MONERO_WALLET_RPC_MANAGER = new MoneroWalletRpcManager(); private static final String MONERO_WALLET_RPC_USERNAME = "haveno_user"; private static final String MONERO_WALLET_RPC_DEFAULT_PASSWORD = "password"; // only used if account password is null private static final String MONERO_WALLET_NAME = "haveno_XMR"; private static final String KEYS_FILE_POSTFIX = ".keys"; private static final String ADDRESS_FILE_POSTFIX = ".address.txt"; private static final int NUM_WALLET_BACKUPS = 3; private static final int MAX_SYNC_ATTEMPTS = 3; private static final boolean PRINT_RPC_STACK_TRACE = false; private static final long SHUTDOWN_TIMEOUT_MS = 60000; private static final long NUM_BLOCKS_BEHIND_TOLERANCE = 5; private static final long POLL_TXS_TOLERANCE_MS = 1000 * 60 * 3; // request connection switch if txs not updated within 3 minutes private static final boolean TEST_STARTUP_SYNC_ERROR = false; private static final long INIT_WALLET_DELAY_MS = 5000; private final User user; private final Preferences preferences; private final CoreAccountService accountService; private final XmrAddressEntryList xmrAddressEntryList; private final WalletsSetup walletsSetup; private final File walletDir; private final int rpcBindPort; private final boolean useNativeXmrWallet; protected final CopyOnWriteArraySet balanceListeners = new CopyOnWriteArraySet<>(); protected final CopyOnWriteArraySet walletListeners = new CopyOnWriteArraySet<>(); private ChangeListener walletInitListener; private TradeManager tradeManager; private final Object lock = new Object(); private TaskLooper pollLooper; private boolean pollInProgress; private Long pollPeriodMs; private long lastLogDaemonNotSyncedTimestamp; private long lastLogPollErrorTimestamp; private long lastPollTxsTimestamp; private final Object pollLock = new Object(); private Long cachedHeight; private BigInteger cachedBalance; private BigInteger cachedAvailableBalance = null; private List cachedSubaddresses; private List cachedOutputs; private List cachedTxs; private boolean isInitializingWallet; private static final Object WALLET_HEIGHT_MONITOR_LOCK = new Object(); private static final long WALLET_HEIGHT_MONITOR_PERIOD_SEC = 1200; // request connection change if wallet height is not updated within 20 minutes private long lastWalletHeightUpdate; private Timer walletHeightMonitorTimer; @SuppressWarnings("unused") @Inject XmrWalletService(User user, Preferences preferences, CoreAccountService accountService, XmrConnectionService xmrConnectionService, WalletsSetup walletsSetup, XmrAddressEntryList xmrAddressEntryList, @Named(Config.WALLET_DIR) File walletDir, @Named(Config.WALLET_RPC_BIND_PORT) int rpcBindPort, @Named(Config.USE_NATIVE_XMR_WALLET) boolean useNativeXmrWallet) { this.user = user; this.preferences = preferences; this.accountService = accountService; this.walletsSetup = walletsSetup; this.xmrAddressEntryList = xmrAddressEntryList; this.walletDir = walletDir; this.rpcBindPort = rpcBindPort; this.useNativeXmrWallet = useNativeXmrWallet; this.xmrConnectionService = xmrConnectionService; // TODO: super's is null unless set here from injection HavenoUtils.xmrWalletService = this; // set monero logging if (MONERO_LOG_LEVEL >= 0) MoneroUtils.setLogLevel(MONERO_LOG_LEVEL); // initialize after account open and basic setup walletsSetup.addSetupTaskHandler(() -> { // TODO: use something better than legacy WalletSetup for notification to initialize // initialize initialize(); // listen for account updates accountService.addListener(new AccountServiceListener() { @Override public void onAccountCreated() { log.info("onAccountCreated()"); initialize(); } @Override public void onAccountOpened() { log.info("onAccountOpened()"); initialize(); } @Override public void onAccountClosed() { log.info("onAccountClosed()"); wasWalletSynced = false; closeMainWallet(); UserThread.execute(() -> syncProgressListener.progress(-1, -1)); // TODO: reset more properties? } @Override public void onPasswordChanged(String oldPassword, String newPassword) { log.info(getClass() + "accountservice.onPasswordChanged()"); if (oldPassword == null || oldPassword.isEmpty()) oldPassword = MONERO_WALLET_RPC_DEFAULT_PASSWORD; if (newPassword == null || newPassword.isEmpty()) newPassword = MONERO_WALLET_RPC_DEFAULT_PASSWORD; changeWalletPasswords(oldPassword, newPassword); } }); }); } // TODO (woodser): need trade manager to get trade ids to change all wallet passwords? public void setTradeManager(TradeManager tradeManager) { this.tradeManager = tradeManager; } public MoneroWallet getWallet() { State state = walletsSetup.getWalletConfig().state(); checkState(state == State.STARTING || state == State.RUNNING, "Cannot call until startup is complete and running, but state is: " + state); return wallet; } /** * Get the wallet creation date in seconds since epoch. * * @return the wallet creation date in seconds since epoch */ public long getWalletCreationDate() { return user.getWalletCreationDate(); } @Override public void saveWallet() { synchronized (walletLock) { if (wallet == null) throw new IllegalStateException("Cannot save main wallet because it's not open"); wallet.save(); lastSaveTimeMs = System.currentTimeMillis(); } } public boolean isWalletAvailable() { try { return getWallet() != null; } catch (Exception e) { return false; } } public boolean isWalletEncrypted() { return accountService.getPassword() != null; } public LongProperty walletHeightProperty() { return walletHeight; } public boolean isSyncedWithinTolerance() { if (!xmrConnectionService.isSyncedWithinTolerance()) return false; Long targetHeight = xmrConnectionService.getTargetHeight(); if (targetHeight == null) return false; if (targetHeight - walletHeight.get() <= NUM_BLOCKS_BEHIND_TOLERANCE) return true; // synced if within a few blocks of target height return false; } public MoneroDaemonRpc getMonerod() { return xmrConnectionService.getMonerod(); } public boolean isProxyApplied() { return isProxyApplied(wasWalletSynced); } public boolean isProxyApplied(boolean wasWalletSynced) { MoneroRpcConnection connection = xmrConnectionService.getConnection(); if (connection != null && connection.isOnion()) return true; // must use proxy if connected to onion return xmrConnectionService.isProxyApplied() && preferences.isProxyApplied(wasWalletSynced); } public String getWalletPassword() { return accountService.getPassword() == null ? MONERO_WALLET_RPC_DEFAULT_PASSWORD : accountService.getPassword(); } public boolean walletExists(String walletName) { String path = walletDir.toString() + File.separator + walletName; return new File(path + KEYS_FILE_POSTFIX).exists(); } public MoneroWallet createWallet(String walletName, boolean applyProxyUri) { return createWallet(walletName, null, applyProxyUri); } private MoneroWallet createWallet(String walletName, Integer walletRpcPort, boolean applyProxyUri) { log.info("{}.createWallet({})", getClass().getSimpleName(), walletName); if (isShutDownStarted) throw new IllegalStateException("Cannot create wallet because shutting down"); MoneroWalletConfig config = getWalletConfig(walletName); return isNativeLibraryApplied() ? createWalletFull(config, applyProxyUri) : createWalletRpc(config, walletRpcPort, applyProxyUri); } public MoneroWallet openWallet(String walletName, boolean applyProxyUri) { return openWallet(walletName, null, applyProxyUri); } public MoneroWallet openWallet(String walletName, Integer walletRpcPort, boolean applyProxyUri) { log.debug("{}.openWallet({})", getClass().getSimpleName(), walletName); if (isShutDownStarted) throw new IllegalStateException("Cannot open wallet '" + walletName + "' because shutting down"); MoneroWalletConfig config = getWalletConfig(walletName); return isNativeLibraryApplied() ? openWalletFull(config, applyProxyUri) : openWalletRpc(config, walletRpcPort, applyProxyUri); } private MoneroWalletConfig getWalletConfig(String walletName) { MoneroWalletConfig config = new MoneroWalletConfig().setPath(getWalletPath(walletName)).setPassword(getWalletPassword()); if (isNativeLibraryApplied()) config.setNetworkType(getMoneroNetworkType()); return config; } private String getWalletPath(String walletName) { return (isNativeLibraryApplied() ? walletDir.getPath() + File.separator : "") + walletName; } private static String getWalletName(String walletPath) { return walletPath.substring(walletPath.lastIndexOf(File.separator) + 1); } private boolean isNativeLibraryApplied() { return useNativeXmrWallet && MoneroUtils.isNativeLibraryLoaded(); } public void closeWallet(MoneroWallet wallet, boolean save) { log.debug("Closing wallet with path={}, save={}", wallet.getPath(), save); MoneroError err = null; String path = wallet.getPath(); try { if (save && wallet instanceof MoneroWalletRpc) { ((MoneroWalletRpc) wallet).stop(); // saves wallet and stops rpc server } else { wallet.close(save); } } catch (MoneroError e) { err = e; } // stop wallet rpc instance if applicable if (wallet instanceof MoneroWalletRpc) MONERO_WALLET_RPC_MANAGER.stopInstance((MoneroWalletRpc) wallet, path, false); if (err != null) throw err; } public void forceCloseWallet(MoneroWallet wallet, String path) { if (wallet == null) { log.warn("Ignoring force close wallet because wallet is null, path={}", path); return; } if (wallet instanceof MoneroWalletRpc) { MONERO_WALLET_RPC_MANAGER.stopInstance((MoneroWalletRpc) wallet, path, true); } else { wallet.close(false); } } public void deleteWallet(String walletName) { assertNotPath(walletName); log.info("{}.deleteWallet({})", getClass().getSimpleName(), walletName); if (!walletExists(walletName)) throw new RuntimeException("Wallet does not exist at path: " + walletName); String path = walletDir.toString() + File.separator + walletName; if (!new File(path).delete()) throw new RuntimeException("Failed to delete wallet cache file: " + path); if (!new File(path + KEYS_FILE_POSTFIX).delete()) throw new RuntimeException("Failed to delete wallet keys file: " + path + KEYS_FILE_POSTFIX); if (!new File(path + ADDRESS_FILE_POSTFIX).delete() && !Config.baseCurrencyNetwork().isMainnet()) throw new RuntimeException("Failed to delete wallet address file: " + path + ADDRESS_FILE_POSTFIX); // mainnet does not have address file by default } public void backupWallet(String walletName) { assertNotPath(walletName); FileUtil.rollingBackup(walletDir, walletName, NUM_WALLET_BACKUPS); FileUtil.rollingBackup(walletDir, walletName + KEYS_FILE_POSTFIX, NUM_WALLET_BACKUPS); FileUtil.rollingBackup(walletDir, walletName + ADDRESS_FILE_POSTFIX, NUM_WALLET_BACKUPS); } public void deleteWalletBackups(String walletName) { assertNotPath(walletName); FileUtil.deleteRollingBackup(walletDir, walletName); FileUtil.deleteRollingBackup(walletDir, walletName + KEYS_FILE_POSTFIX); FileUtil.deleteRollingBackup(walletDir, walletName + ADDRESS_FILE_POSTFIX); } private static void assertNotPath(String name) { if (name.contains(File.separator)) throw new IllegalArgumentException("Path not expected: " + name); } public MoneroTxWallet createTx(List destinations) { MoneroTxWallet tx = createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false)); //printTxs("XmrWalletService.createTx", tx); return tx; } public MoneroTxWallet createTx(MoneroTxConfig txConfig) { synchronized (walletLock) { synchronized (HavenoUtils.getWalletFunctionLock()) { MoneroTxWallet tx = wallet.createTx(txConfig); if (Boolean.TRUE.equals(txConfig.getRelay())) { cachedTxs.addFirst(tx); cacheWalletInfo(); saveWallet(); } return tx; } } } public List createSweepTxs(String address) { return createSweepTxs(new MoneroTxConfig().setAccountIndex(0).setAddress(address).setRelay(false)); } public List createSweepTxs(MoneroTxConfig txConfig) { synchronized (walletLock) { synchronized (HavenoUtils.getWalletFunctionLock()) { List txs = wallet.sweepUnlocked(txConfig); if (Boolean.TRUE.equals(txConfig.getRelay())) { for (MoneroTxWallet tx : txs) cachedTxs.addFirst(tx); cacheWalletInfo(); saveWallet(); } return txs; } } } public List relayTxs(List metadatas) { synchronized (walletLock) { List txIds = wallet.relayTxs(metadatas); saveWallet(); return txIds; } } /** * Freeze reserved outputs and thaw unreserved outputs. */ public void fixReservedOutputs() { synchronized (walletLock) { // collect reserved outputs Set reservedKeyImages = new HashSet(); for (Trade trade : tradeManager.getOpenTrades()) { if (trade.getSelf().getReserveTxKeyImages() == null) continue; reservedKeyImages.addAll(trade.getSelf().getReserveTxKeyImages()); } for (OpenOffer openOffer : tradeManager.getOpenOfferManager().getOpenOffers()) { if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() == null) continue; reservedKeyImages.addAll(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages()); } freezeReservedOutputs(reservedKeyImages); thawUnreservedOutputs(reservedKeyImages); } } private void freezeReservedOutputs(Set reservedKeyImages) { synchronized (walletLock) { // ensure wallet is open if (wallet == null) { log.warn("Cannot freeze reserved outputs because wallet not open"); return; } // freeze reserved outputs Set reservedUnfrozenKeyImages = getOutputs(new MoneroOutputQuery() .setIsFrozen(false) .setIsSpent(false)) .stream() .map(output -> output.getKeyImage().getHex()) .collect(Collectors.toSet()); reservedUnfrozenKeyImages.retainAll(reservedKeyImages); if (!reservedUnfrozenKeyImages.isEmpty()) { log.warn("Freezing unfrozen outputs which are reserved for offer or trade: " + reservedUnfrozenKeyImages); freezeOutputs(reservedUnfrozenKeyImages); } } } private void thawUnreservedOutputs(Set reservedKeyImages) { synchronized (walletLock) { // ensure wallet is open if (wallet == null) { log.warn("Cannot thaw unreserved outputs because wallet not open"); return; } // thaw unreserved outputs Set unreservedFrozenKeyImages = getOutputs(new MoneroOutputQuery() .setIsFrozen(true) .setIsSpent(false)) .stream() .map(output -> output.getKeyImage().getHex()) .collect(Collectors.toSet()); unreservedFrozenKeyImages.removeAll(reservedKeyImages); if (!unreservedFrozenKeyImages.isEmpty()) { log.warn("Thawing frozen outputs which are not reserved for offer or trade: " + unreservedFrozenKeyImages); thawOutputs(unreservedFrozenKeyImages); } } } /** * Freeze the given outputs with a lock on the wallet. * * @param keyImages the key images to freeze (ignored if null or empty) */ public void freezeOutputs(Collection keyImages) { if (keyImages == null || keyImages.isEmpty()) return; synchronized (walletLock) { // collect outputs to freeze List unfrozenKeyImages = getOutputs(new MoneroOutputQuery().setIsFrozen(false).setIsSpent(false)).stream() .map(output -> output.getKeyImage().getHex()) .collect(Collectors.toList()); unfrozenKeyImages.retainAll(keyImages); // freeze outputs for (String keyImage : unfrozenKeyImages) wallet.freezeOutput(keyImage); cacheNonPoolTxs(); cacheWalletInfo(); saveWallet(); } } /** * Thaw the given outputs with a lock on the wallet. * * @param keyImages the key images to thaw (ignored if null or empty) */ public void thawOutputs(Collection keyImages) { if (keyImages == null || keyImages.isEmpty()) return; synchronized (walletLock) { // collect outputs to thaw List frozenKeyImages = getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false)).stream() .map(output -> output.getKeyImage().getHex()) .collect(Collectors.toList()); frozenKeyImages.retainAll(keyImages); // thaw outputs for (String keyImage : frozenKeyImages) wallet.thawOutput(keyImage); cacheNonPoolTxs(); cacheWalletInfo(); saveWallet(); } } private void cacheNonPoolTxs() { // get non-pool txs List nonPoolTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true).setInTxPool(false)); // replace non-pool txs in cache for (MoneroTxWallet nonPoolTx : nonPoolTxs) { boolean replaced = false; for (int i = 0; i < cachedTxs.size(); i++) { if (cachedTxs.get(i).getHash().equals(nonPoolTx.getHash())) { cachedTxs.set(i, nonPoolTx); replaced = true; break; } } if (!replaced) cachedTxs.add(nonPoolTx); } } private List getSubaddressesWithExactInput(BigInteger amount) { // fetch unspent, unfrozen, unlocked outputs List exactOutputs = getOutputs(new MoneroOutputQuery() .setAmount(amount) .setIsSpent(false) .setIsFrozen(false) .setTxQuery(new MoneroTxQuery().setIsLocked(false))); // collect subaddresses indices as sorted set TreeSet subaddressIndices = new TreeSet(); for (MoneroOutputWallet output : exactOutputs) subaddressIndices.add(output.getSubaddressIndex()); return new ArrayList(subaddressIndices); } /** * Create the reserve tx and freeze its inputs. The full amount is returned * to the sender's payout address less the penalty and mining fees. * * @param penaltyFee penalty fee for breaking protocol * @param tradeFee trade fee * @param sendTradeAmount trade amount to send peer * @param securityDeposit security deposit amount * @param returnAddress return address for reserved funds * @param reserveExactAmount specifies to reserve the exact input amount * @param preferredSubaddressIndex preferred source subaddress to spend from (optional) * @return the reserve tx */ public MoneroTxWallet createReserveTx(BigInteger penaltyFee, BigInteger tradeFee, BigInteger sendTradeAmount, BigInteger securityDeposit, String returnAddress, boolean reserveExactAmount, Integer preferredSubaddressIndex) { synchronized (walletLock) { synchronized (HavenoUtils.getWalletFunctionLock()) { log.info("Creating reserve tx with preferred subaddress index={}, return address={}", preferredSubaddressIndex, returnAddress); long time = System.currentTimeMillis(); BigInteger sendAmount = sendTradeAmount.add(securityDeposit).add(tradeFee).subtract(penaltyFee); MoneroTxWallet reserveTx = createTradeTx(penaltyFee, HavenoUtils.getBurnAddress(), sendAmount, returnAddress, reserveExactAmount, preferredSubaddressIndex); log.info("Done creating reserve tx in {} ms", System.currentTimeMillis() - time); return reserveTx; } } } /** * Create the multisig deposit tx and freeze its inputs. * * @param trade the trade to create a deposit tx from * @param reserveExactAmount specifies to reserve the exact input amount * @param preferredSubaddressIndex preferred source subaddress to spend from (optional) * @return MoneroTxWallet the multisig deposit tx */ public MoneroTxWallet createDepositTx(Trade trade, boolean reserveExactAmount, Integer preferredSubaddressIndex) { synchronized (walletLock) { synchronized (HavenoUtils.getWalletFunctionLock()) { BigInteger feeAmount = trade instanceof MakerTrade ? trade.getMakerFee() : trade.getTakerFee(); String feeAddress = trade.getProcessModel().getTradeFeeAddress(); BigInteger sendTradeAmount = trade instanceof BuyerTrade ? BigInteger.ZERO : trade.getAmount(); BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); BigInteger sendAmount = sendTradeAmount.add(securityDeposit); String multisigAddress = trade.getProcessModel().getMultisigAddress(); long time = System.currentTimeMillis(); log.info("Creating deposit tx for trade {} {} with multisig address={}", trade.getClass().getSimpleName(), trade.getShortId(), multisigAddress); MoneroTxWallet depositTx = createTradeTx(feeAmount, feeAddress, sendAmount, multisigAddress, reserveExactAmount, preferredSubaddressIndex); log.info("Done creating deposit tx for trade {} {} in {} ms", trade.getClass().getSimpleName(), trade.getShortId(), System.currentTimeMillis() - time); return depositTx; } } } private MoneroTxWallet createTradeTx(BigInteger feeAmount, String feeAddress, BigInteger sendAmount, String sendAddress, boolean reserveExactAmount, Integer preferredSubaddressIndex) { synchronized (walletLock) { MoneroWallet wallet = getWallet(); // create a list of subaddresses to attempt spending from in preferred order List subaddressIndices = new ArrayList(); if (reserveExactAmount) { BigInteger exactInputAmount = feeAmount.add(sendAmount); List subaddressIndicesWithExactInput = getSubaddressesWithExactInput(exactInputAmount); if (preferredSubaddressIndex != null) subaddressIndicesWithExactInput.remove(preferredSubaddressIndex); Collections.sort(subaddressIndicesWithExactInput); Collections.reverse(subaddressIndicesWithExactInput); subaddressIndices.addAll(subaddressIndicesWithExactInput); } if (preferredSubaddressIndex != null) { if (wallet.getBalance(0, preferredSubaddressIndex).compareTo(BigInteger.ZERO) > 0) { subaddressIndices.add(0, preferredSubaddressIndex); // try preferred subaddress first if funded } else if (reserveExactAmount) { subaddressIndices.add(preferredSubaddressIndex); // otherwise only try preferred subaddress if using exact output } } // first try preferred subaddressess for (int i = 0; i < subaddressIndices.size(); i++) { try { return createTradeTxFromSubaddress(feeAmount, feeAddress, sendAmount, sendAddress, subaddressIndices.get(i)); } catch (Exception e) { log.info("Cannot create trade tx from preferred subaddress index " + subaddressIndices.get(i) + ": " + e.getMessage()); } } // try any subaddress if (!subaddressIndices.isEmpty()) log.info("Could not create trade tx from preferred subaddresses, trying any subaddress"); return createTradeTxFromSubaddress(feeAmount, feeAddress, sendAmount, sendAddress, null); } } private MoneroTxWallet createTradeTxFromSubaddress(BigInteger feeAmount, String feeAddress, BigInteger sendAmount, String sendAddress, Integer subaddressIndex) { synchronized (walletLock) { // create tx MoneroTxConfig txConfig = new MoneroTxConfig() .setAccountIndex(0) .setSubaddressIndices(subaddressIndex) .addDestination(sendAddress, sendAmount) .setSubtractFeeFrom(0) // pay mining fee from send amount .setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY); if (!BigInteger.valueOf(0).equals(feeAmount)) txConfig.addDestination(feeAddress, feeAmount); MoneroTxWallet tradeTx = createTx(txConfig); // freeze inputs List keyImages = new ArrayList(); for (MoneroOutput input : tradeTx.getInputs()) keyImages.add(input.getKeyImage().getHex()); freezeOutputs(keyImages); return tradeTx; } } public MoneroTx verifyReserveTx(String offerId, BigInteger penaltyFee, BigInteger tradeFee, BigInteger sendTradeAmount, BigInteger securityDeposit, String returnAddress, String txHash, String txHex, String txKey, List keyImages) { BigInteger sendAmount = sendTradeAmount.add(securityDeposit).add(tradeFee).subtract(penaltyFee); return verifyTradeTx(offerId, penaltyFee, HavenoUtils.getBurnAddress(), sendAmount, returnAddress, txHash, txHex, txKey, keyImages); } public MoneroTx verifyDepositTx(String offerId, BigInteger feeAmount, String feeAddress, BigInteger sendTradeAmount, BigInteger securityDeposit, String multisigAddress, String txHash, String txHex, String txKey, List keyImages) { BigInteger sendAmount = sendTradeAmount.add(securityDeposit); return verifyTradeTx(offerId, feeAmount, feeAddress, sendAmount, multisigAddress, txHash, txHex, txKey, keyImages); } /** * Verify a reserve or deposit transaction. * Checks double spends, trade fee, deposit amount and destination, and miner fee. * The transaction is submitted to the pool then flushed without relaying. * * @param offerId id of offer to verify trade tx * @param tradeFeeAmount amount sent to fee address * @param feeAddress fee address * @param sendAmount amount sent to transfer address * @param sendAddress transfer address * @param txHash transaction hash * @param txHex transaction hex * @param txKey transaction key * @param keyImages expected key images of inputs, ignored if null * @return the verified tx */ public MoneroTx verifyTradeTx(String offerId, BigInteger tradeFeeAmount, String feeAddress, BigInteger sendAmount, String sendAddress, String txHash, String txHex, String txKey, List keyImages) { if (txHash == null) throw new IllegalArgumentException("Cannot verify trade tx with null id"); MoneroDaemonRpc monerod = getMonerod(); MoneroWallet wallet = getWallet(); MoneroTx tx = null; synchronized (lock) { try { // verify tx not submitted to pool tx = monerod.getTx(txHash); if (tx != null) throw new RuntimeException("Tx is already submitted"); // submit tx to pool MoneroSubmitTxResult result = monerod.submitTxHex(txHex, true); // TODO (woodser): invert doNotRelay flag to relay for library consistency? if (!result.isGood()) throw new RuntimeException("Failed to submit tx to daemon: " + JsonUtils.serialize(result)); // get pool tx which has weight and size for (MoneroTx poolTx : monerod.getTxPool()) if (poolTx.getHash().equals(txHash)) tx = poolTx; if (tx == null) throw new RuntimeException("Tx is not in pool after being submitted"); // verify key images if (keyImages != null) { Set txKeyImages = new HashSet(); for (MoneroOutput input : tx.getInputs()) txKeyImages.add(input.getKeyImage().getHex()); if (!txKeyImages.equals(new HashSet(keyImages))) throw new RuntimeException("Tx inputs do not match claimed key images"); } // verify unlock height if (!BigInteger.ZERO.equals(tx.getUnlockTime())) throw new RuntimeException("Unlock height must be 0"); // verify miner fee BigInteger minerFeeEstimate = getFeeEstimate(tx.getWeight()); HavenoUtils.verifyMinerFee(minerFeeEstimate, tx.getFee()); log.info("Trade miner fee {} is within tolerance", tx.getFee()); // verify proof to fee address BigInteger actualTradeFee = BigInteger.ZERO; if (tradeFeeAmount.compareTo(BigInteger.ZERO) > 0) { MoneroCheckTx tradeFeeCheck = wallet.checkTxKey(txHash, txKey, feeAddress); if (!tradeFeeCheck.isGood()) throw new RuntimeException("Invalid proof to trade fee address"); actualTradeFee = tradeFeeCheck.getReceivedAmount(); } // verify proof to transfer address MoneroCheckTx transferCheck = wallet.checkTxKey(txHash, txKey, sendAddress); if (!transferCheck.isGood()) throw new RuntimeException("Invalid proof to transfer address"); BigInteger actualSendAmount = transferCheck.getReceivedAmount(); // verify trade fee amount if (!actualTradeFee.equals(tradeFeeAmount)) { if (equalsWithinFractionError(actualTradeFee, tradeFeeAmount)) { log.warn("Trade fee amount is within fraction error, expected " + tradeFeeAmount + " but was " + actualTradeFee); } else { throw new RuntimeException("Invalid trade fee amount, expected " + tradeFeeAmount + " but was " + actualTradeFee); } } // verify send amount BigInteger expectedSendAmount = sendAmount.subtract(tx.getFee()); if (!actualSendAmount.equals(expectedSendAmount)) { if (equalsWithinFractionError(actualSendAmount, expectedSendAmount)) { log.warn("Trade tx send amount is within fraction error, expected " + expectedSendAmount + " but was " + actualSendAmount + " with tx fee " + tx.getFee()); } else { throw new RuntimeException("Invalid send amount, expected " + expectedSendAmount + " but was " + actualSendAmount + " with tx fee " + tx.getFee()); } } return tx; } catch (Exception e) { log.warn("Error verifying trade tx with offer id=" + offerId + (tx == null ? "" : ", tx=\n" + tx) + ": " + e.getMessage()); throw e; } finally { try { monerod.flushTxPool(txHash); // flush tx from pool } catch (MoneroRpcError err) { System.out.println(monerod.getRpcConnection()); throw err.getCode().equals(-32601) ? new RuntimeException("Failed to flush tx from pool. Arbitrator must use trusted, unrestricted daemon") : err; } } } } // TODO: old bug in atomic unit conversion could cause fractional difference error, remove this in future release, maybe re-sign all offers then private static boolean equalsWithinFractionError(BigInteger a, BigInteger b) { return a.subtract(b).abs().compareTo(new BigInteger("1")) <= 0; } /** * Get the tx fee estimate based on its weight. * * @param txWeight - the tx weight * @return the tx fee estimate */ private BigInteger getFeeEstimate(long txWeight) { // get fee priority MoneroTxPriority priority; if (PROTOCOL_FEE_PRIORITY == MoneroTxPriority.DEFAULT) { priority = wallet.getDefaultFeePriority(); } else { priority = PROTOCOL_FEE_PRIORITY; } // get fee estimates per kB from daemon MoneroFeeEstimate feeEstimates = getMonerod().getFeeEstimate(); BigInteger baseFeeEstimate = feeEstimates.getFees().get(priority.ordinal() - 1); BigInteger qmask = feeEstimates.getQuantizationMask(); log.info("Monero base fee estimate={}, qmask={}", baseFeeEstimate, qmask); // get tx base fee BigInteger baseFee = baseFeeEstimate.multiply(BigInteger.valueOf(txWeight)); // round up to multiple of quantization mask BigInteger[] quotientAndRemainder = baseFee.divideAndRemainder(qmask); BigInteger feeEstimate = qmask.multiply(quotientAndRemainder[0]); if (quotientAndRemainder[1].compareTo(BigInteger.ZERO) > 0) feeEstimate = feeEstimate.add(qmask); return feeEstimate; } public void onShutDownStarted() { log.info("XmrWalletService.onShutDownStarted()"); this.isShutDownStarted = true; } public void shutDown() { log.info("Shutting down {}", getClass().getSimpleName()); // create task to shut down Runnable shutDownTask = () -> { // close main wallet, force close if syncing if (isSyncing()) forceCloseMainWallet(); else { try { closeMainWallet(); } catch (Exception e) { log.warn("Error closing main wallet: {}. Was Haveno stopped manually with ctrl+c?", e.getMessage()); } } }; // shut down with timeout try { ThreadUtils.awaitTask(shutDownTask, SHUTDOWN_TIMEOUT_MS); } catch (Exception e) { log.warn("Error shutting down {}: {}\n", getClass().getSimpleName(), e.getMessage(), e); // force close wallet forceCloseMainWallet(); } log.info("Done shutting down {}", getClass().getSimpleName()); } // -------------------------- ADDRESS ENTRIES ----------------------------- public synchronized XmrAddressEntry getNewAddressEntry() { return getNewAddressEntryAux(null, XmrAddressEntry.Context.AVAILABLE); } public synchronized XmrAddressEntry getNewAddressEntry(String offerId, XmrAddressEntry.Context context) { // try to use available and not yet used entries try { List unusedAddressEntries = getUnusedAddressEntries(); if (!unusedAddressEntries.isEmpty()) return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(unusedAddressEntries.get(0), context, offerId); } catch (Exception e) { log.warn("Error getting new address entry based on incoming transactions: {}\n", e.getMessage(), e); } // create new entry return getNewAddressEntryAux(offerId, context); } private XmrAddressEntry getNewAddressEntryAux(String offerId, XmrAddressEntry.Context context) { MoneroSubaddress subaddress = wallet.createSubaddress(0); XmrAddressEntry entry = new XmrAddressEntry(subaddress.getIndex(), subaddress.getAddress(), context, offerId, null); log.info("Add new XmrAddressEntry {}", entry); xmrAddressEntryList.addAddressEntry(entry); return entry; } public synchronized XmrAddressEntry getFreshAddressEntry() { List unusedAddressEntries = getUnusedAddressEntries(); if (unusedAddressEntries.isEmpty()) return getNewAddressEntry(); else return unusedAddressEntries.get(0); } public synchronized XmrAddressEntry recoverAddressEntry(String offerId, String address, XmrAddressEntry.Context context) { var available = findAddressEntry(address, XmrAddressEntry.Context.AVAILABLE); if (!available.isPresent()) return null; return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(available.get(), context, offerId); } public synchronized XmrAddressEntry getOrCreateAddressEntry(String offerId, XmrAddressEntry.Context context) { Optional addressEntry = getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).findAny(); if (addressEntry.isPresent()) return addressEntry.get(); else return getNewAddressEntry(offerId, context); } public synchronized Optional getAddressEntry(String offerId, XmrAddressEntry.Context context) { List entries = getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).collect(Collectors.toList()); if (entries.size() > 1) throw new RuntimeException("Multiple address entries exist with offer ID " + offerId + " and context " + context + ". That should never happen."); return entries.isEmpty() ? Optional.empty() : Optional.of(entries.get(0)); } public synchronized void swapAddressEntryToAvailable(String offerId, XmrAddressEntry.Context context) { Optional addressEntryOptional = getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).findAny(); addressEntryOptional.ifPresent(e -> { xmrAddressEntryList.swapToAvailable(e); saveAddressEntryList(); }); } public synchronized void cloneAddressEntries(String offerId, String cloneOfferId) { List entries = getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).collect(Collectors.toList()); for (XmrAddressEntry entry : entries) { XmrAddressEntry clonedEntry = new XmrAddressEntry(entry.getSubaddressIndex(), entry.getAddressString(), entry.getContext(), cloneOfferId, null); Optional existingEntry = getAddressEntry(clonedEntry.getOfferId(), clonedEntry.getContext()); if (existingEntry.isPresent()) continue; xmrAddressEntryList.addAddressEntry(clonedEntry); } } public synchronized void resetAddressEntriesForOpenOffer(String offerId) { log.info("resetAddressEntriesForOpenOffer offerId={}", offerId); // skip if failed trade is scheduled for processing // TODO: do not call this function in this case? if (tradeManager.hasFailedScheduledTrade(offerId)) { log.warn("Refusing to reset address entries because trade is scheduled for deletion with offerId={}", offerId); return; } swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.OFFER_FUNDING); // swap trade payout to available if applicable if (tradeManager == null) return; Trade trade = tradeManager.getTrade(offerId); if (trade == null || trade.isPayoutFinalized()) swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.TRADE_PAYOUT); } public synchronized void swapPayoutAddressEntryToAvailable(String offerId) { swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.TRADE_PAYOUT); } private Optional findAddressEntry(String address, XmrAddressEntry.Context context) { return getAddressEntryListAsImmutableList().stream().filter(e -> address.equals(e.getAddressString())).filter(e -> context == e.getContext()).findAny(); } public List getAddressEntries() { return getAddressEntryListAsImmutableList().stream().collect(Collectors.toList()); } public List getAvailableAddressEntries() { return getAddressEntryListAsImmutableList().stream().filter(addressEntry -> XmrAddressEntry.Context.AVAILABLE == addressEntry.getContext()).collect(Collectors.toList()); } public List getAddressEntriesForOpenOffer() { return getAddressEntryListAsImmutableList().stream() .filter(addressEntry -> XmrAddressEntry.Context.OFFER_FUNDING == addressEntry.getContext()) .collect(Collectors.toList()); } public List getAddressEntriesForTrade() { return getAddressEntryListAsImmutableList().stream() .filter(addressEntry -> XmrAddressEntry.Context.TRADE_PAYOUT == addressEntry.getContext()) .collect(Collectors.toList()); } public List getAddressEntries(XmrAddressEntry.Context context) { return getAddressEntryListAsImmutableList().stream().filter(addressEntry -> context == addressEntry.getContext()).collect(Collectors.toList()); } public XmrAddressEntry getBaseAddressEntry() { return getAddressEntryListAsImmutableList().stream().filter(e -> e.getContext() == XmrAddressEntry.Context.BASE_ADDRESS).findAny().orElse(null); } public List getFundedAvailableAddressEntries() { return getAvailableAddressEntries().stream().filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex()).compareTo(BigInteger.ZERO) > 0).collect(Collectors.toList()); } public List getAddressEntryListAsImmutableList() { for (MoneroSubaddress subaddress : cachedSubaddresses) { boolean exists = xmrAddressEntryList.getAddressEntriesAsListImmutable().stream().filter(addressEntry -> addressEntry.getAddressString().equals(subaddress.getAddress())).findAny().isPresent(); if (!exists) { XmrAddressEntry entry = new XmrAddressEntry(subaddress.getIndex(), subaddress.getAddress(), subaddress.getIndex() == 0 ? XmrAddressEntry.Context.BASE_ADDRESS : XmrAddressEntry.Context.AVAILABLE, null, null); xmrAddressEntryList.addAddressEntry(entry); } } return xmrAddressEntryList.getAddressEntriesAsListImmutable(); } public List getUnusedAddressEntries() { return getAvailableAddressEntries().stream() .filter(e -> e.getContext() == XmrAddressEntry.Context.AVAILABLE && !subaddressHasIncomingTransfers(e.getSubaddressIndex())) .collect(Collectors.toList()); } public boolean subaddressHasIncomingTransfers(int subaddressIndex) { return getNumOutputsForSubaddress(subaddressIndex) > 0; } public int getNumOutputsForSubaddress(int subaddressIndex) { int numUnspentOutputs = 0; for (MoneroTxWallet tx : cachedTxs) { //if (tx.getTransfers(new MoneroTransferQuery().setSubaddressIndex(subaddressIndex)).isEmpty()) continue; // TODO monero-project: transfers are occluded by transfers from/to same account, so this will return unused when used numUnspentOutputs += tx.getOutputsWallet(new MoneroOutputQuery().setAccountIndex(0).setSubaddressIndex(subaddressIndex)).size(); // TODO: monero-project does not provide outputs for unconfirmed txs } boolean positiveBalance = getBalanceForSubaddress(subaddressIndex).compareTo(BigInteger.ZERO) > 0; if (positiveBalance && numUnspentOutputs == 0) return 1; // outputs do not appear until confirmed and internal transfers are occluded, so report 1 if positive balance return numUnspentOutputs; } private MoneroSubaddress getSubaddress(int subaddressIndex) { for (MoneroSubaddress subaddress : cachedSubaddresses) { if (subaddress.getIndex() == subaddressIndex) return subaddress; } return null; } public int getNumTxsWithIncomingOutputs(int subaddressIndex) { List txsWithIncomingOutputs = getTxsWithIncomingOutputs(subaddressIndex); if (txsWithIncomingOutputs.isEmpty() && subaddressHasIncomingTransfers(subaddressIndex)) return 1; // outputs do not appear until confirmed and internal transfers are occluded, so report 1 if positive balance return txsWithIncomingOutputs.size(); } public List getTxsWithIncomingOutputs() { return getTxsWithIncomingOutputs(null); } public List getTxsWithIncomingOutputs(Integer subaddressIndex) { List incomingTxs = new ArrayList<>(); for (MoneroTxWallet tx : cachedTxs) { boolean isIncoming = false; if (tx.getIncomingTransfers() != null) { for (MoneroIncomingTransfer transfer : tx.getIncomingTransfers()) { if (transfer.getAccountIndex().equals(0) && (subaddressIndex == null || transfer.getSubaddressIndex().equals(subaddressIndex))) { isIncoming = true; break; } } } if (tx.getOutputs() != null && !isIncoming) { for (MoneroOutputWallet output : tx.getOutputsWallet()) { if (output.getAccountIndex().equals(0) && (subaddressIndex == null || output.getSubaddressIndex().equals(subaddressIndex))) { isIncoming = true; break; } } } if (isIncoming) incomingTxs.add(tx); } return incomingTxs; } public BigInteger getBalanceForAddress(String address) { return getBalanceForSubaddress(wallet.getAddressIndex(address).getIndex()); } public BigInteger getBalanceForSubaddress(int subaddressIndex) { MoneroSubaddress subaddress = getSubaddress(subaddressIndex); return subaddress == null ? BigInteger.ZERO : subaddress.getBalance(); } public BigInteger getBalanceForSubaddress(int subaddressIndex, boolean includeFrozen) { return getBalanceForSubaddress(subaddressIndex).add(includeFrozen ? getFrozenBalanceForSubaddress(subaddressIndex) : BigInteger.ZERO); } public BigInteger getFrozenBalanceForSubaddress(int subaddressIndex) { List outputs = getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false).setAccountIndex(0).setSubaddressIndex(subaddressIndex)); return outputs.stream().map(output -> output.getAmount()).reduce(BigInteger.ZERO, BigInteger::add); } public BigInteger getAvailableBalanceForSubaddress(int subaddressIndex) { MoneroSubaddress subaddress = getSubaddress(subaddressIndex); return subaddress == null ? BigInteger.ZERO : subaddress.getUnlockedBalance(); } public Stream getAddressEntriesForAvailableBalanceStream() { Stream available = getFundedAvailableAddressEntries().stream(); available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.ARBITRATOR).stream()); available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.OFFER_FUNDING).stream().filter(entry -> !tradeManager.getOpenOfferManager().getOpenOffer(entry.getOfferId()).isPresent())); available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.TRADE_PAYOUT).stream().filter(entry -> tradeManager.getTrade(entry.getOfferId()) == null || tradeManager.getTrade(entry.getOfferId()).isPayoutFinalized())); return available.filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex()).compareTo(BigInteger.ZERO) > 0); } public void addWalletListener(MoneroWalletListenerI listener) { synchronized (walletListeners) { walletListeners.add(listener); } } public void removeWalletListener(MoneroWalletListenerI listener) { synchronized (walletListeners) { if (!walletListeners.contains(listener)) throw new RuntimeException("Listener is not registered with wallet"); walletListeners.remove(listener); } } // TODO (woodser): update balance and other listening public void addBalanceListener(XmrBalanceListener listener) { if (listener == null) throw new IllegalArgumentException("Cannot add null balance listener"); synchronized (balanceListeners) { if (!balanceListeners.contains(listener)) balanceListeners.add(listener); } } public void removeBalanceListener(XmrBalanceListener listener) { if (listener == null) throw new IllegalArgumentException("Cannot add null balance listener"); synchronized (balanceListeners) { balanceListeners.remove(listener); } } public void updateBalanceListeners() { synchronized (walletLock) { BigInteger availableBalance = getAvailableBalance(); synchronized (balanceListeners) { for (XmrBalanceListener balanceListener : balanceListeners) { BigInteger balance; if (balanceListener.getSubaddressIndex() != null && balanceListener.getSubaddressIndex() != 0) balance = getBalanceForSubaddress(balanceListener.getSubaddressIndex()); else balance = availableBalance; try { balanceListener.onBalanceChanged(balance); } catch (Exception e) { log.warn("Failed to notify balance listener of change: {}\n", e.getMessage(), e); } } } } } public void saveAddressEntryList() { xmrAddressEntryList.requestPersistence(); } public long getHeight() { return walletHeight.get(); } public List getTxs(boolean includeFailed) { List txs = getTxs(); if (includeFailed) return txs; return txs.stream().filter(tx -> !tx.isFailed()).collect(Collectors.toList()); } public List getTxs() { return getTxs(new MoneroTxQuery().setIncludeOutputs(true)); } public List getTxs(MoneroTxQuery query) { if (cachedTxs == null) { log.warn("Transactions not cached, fetching from wallet"); cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true)); // fetches from pool } return cachedTxs.stream().filter(tx -> query.meetsCriteria(tx)).collect(Collectors.toList()); } public List getTxs(List txIds) { return getTxs(new MoneroTxQuery().setIncludeOutputs(true).setHashes(txIds)); } public MoneroTxWallet getTx(String txId) { List txs = getTxs(new MoneroTxQuery().setIncludeOutputs(true).setHash(txId)); return txs.isEmpty() ? null : txs.get(0); } public BigInteger getBalance() { return cachedBalance; } public BigInteger getAvailableBalance() { return cachedAvailableBalance; } public boolean hasAddress(String address) { for (MoneroSubaddress subaddress : getSubaddresses()) { if (subaddress.getAddress().equals(address)) return true; } return false; } public List getSubaddresses() { return cachedSubaddresses; } public BigInteger getAmountSentToSelf(MoneroTxWallet tx) { BigInteger sentToSelfAmount = BigInteger.ZERO; if (tx.getOutgoingTransfer() != null && tx.getOutgoingTransfer().getDestinations() != null) { for (MoneroDestination destination : tx.getOutgoingTransfer().getDestinations()) { if (hasAddress(destination.getAddress())) { sentToSelfAmount = sentToSelfAmount.add(destination.getAmount()); } } } return sentToSelfAmount; } public List getOutputs(MoneroOutputQuery query) { List filteredOutputs = new ArrayList(); for (MoneroOutputWallet output : cachedOutputs) { if (query == null || query.meetsCriteria(output)) filteredOutputs.add(output); } return filteredOutputs; } public List getOutputs(Collection keyImages) { List outputs = new ArrayList(); for (String keyImage : keyImages) { List outputList = getOutputs(new MoneroOutputQuery().setIsSpent(false).setKeyImage(new MoneroKeyImage(keyImage))); if (!outputList.isEmpty()) outputs.add(outputList.get(0)); } return outputs; } public BigInteger getOutputsAmount(Collection keyImages) { return getOutputs(keyImages).stream().map(output -> output.getAmount()).reduce(BigInteger.ZERO, BigInteger::add); } /////////////////////////////////////////////////////////////////////////////////////////// // Util /////////////////////////////////////////////////////////////////////////////////////////// public static MoneroNetworkType getMoneroNetworkType() { switch (Config.baseCurrencyNetwork()) { case XMR_LOCAL: return MoneroNetworkType.TESTNET; case XMR_STAGENET: return MoneroNetworkType.STAGENET; case XMR_MAINNET: return MoneroNetworkType.MAINNET; default: throw new RuntimeException("Unhandled base currency network: " + Config.baseCurrencyNetwork()); } } public static void printTxs(String tracePrefix, MoneroTxWallet... txs) { StringBuilder sb = new StringBuilder(); for (MoneroTxWallet tx : txs) sb.append('\n' + tx.toString()); log.info("\n" + tracePrefix + ":" + sb.toString()); } // ------------------------------ PRIVATE HELPERS ------------------------- private void initialize() { // try to load native monero library if (useNativeXmrWallet && !MoneroUtils.isNativeLibraryLoaded()) { try { MoneroUtils.loadNativeLibrary(); } catch (Exception | UnsatisfiedLinkError e) { log.warn("Failed to load Monero native libraries: " + e.getMessage()); } } String appliedMsg = "Monero native libraries applied: " + isNativeLibraryApplied(); if (useNativeXmrWallet && !isNativeLibraryApplied()) log.warn(appliedMsg); else log.info(appliedMsg); // listen for connection changes xmrConnectionService.addConnectionListener(connection -> { if (wasWalletSynced && !isSyncing()) { ThreadUtils.submitToPool(() -> { onConnectionChanged(connection); }); } else { // check if ignored if (wallet == null || isShutDownStarted) return; if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) { updatePollPeriod(); return; } // force restart main wallet if connection changed while syncing if (isSyncing()) { log.warn("Force restarting main wallet because connection changed while syncing"); forceRestartMainWallet(); } } }); // initialize main wallet when daemon synced walletInitListener = (obs, oldVal, newVal) -> initMainWalletIfConnected(); xmrConnectionService.downloadPercentageProperty().addListener(walletInitListener); initMainWalletIfConnected(); // monitor wallet height updates to request connection change walletHeight.addListener((obs, oldVal, newVal) -> { lastWalletHeightUpdate = System.currentTimeMillis(); startWalletHeightMonitor(); }); startWalletHeightMonitor(); } private void startWalletHeightMonitor() { synchronized (WALLET_HEIGHT_MONITOR_LOCK) { if (walletHeightMonitorTimer != null) walletHeightMonitorTimer.stop(); walletHeightMonitorTimer = UserThread.runPeriodically(() -> { ThreadUtils.submitToPool(() -> { if (System.currentTimeMillis() - lastWalletHeightUpdate > WALLET_HEIGHT_MONITOR_PERIOD_SEC * 1000) { log.warn("Requesting connection change because main wallet height has not updated in over {} minutes", WALLET_HEIGHT_MONITOR_PERIOD_SEC / 60); requestSwitchToNextBestConnection(); } }); }, WALLET_HEIGHT_MONITOR_PERIOD_SEC); } } private void initMainWalletIfConnected() { if (wallet == null && xmrConnectionService.downloadPercentageProperty().get() == 1 && !isShutDownStarted) { requestInitMainWallet(); } } private void requestInitMainWallet() { ThreadUtils.submitToPool(() -> { try { initMainWallet(); } catch (Exception e) { if (isShutDownStarted) return; log.warn("Error initializing main wallet: {}\n", e.getMessage(), e); HavenoUtils.setTopError(e.getMessage()); throw e; } }); } private void initMainWallet() { synchronized (walletLock) { if (isShutDownStarted) return; // open or create main wallet openOrCreateMainWallet(); // stop recursion if already initializing if (isInitializingWallet) return; isInitializingWallet = true; // try to sync wallet on startup, otherwise start polling try { if (isWalletServiceInitialized()) { startPolling(); } else { // repeatedly attempt to sync wallet on startup, otherwise open application for (int i = 0; i < MAX_SYNC_ATTEMPTS; i++) { try { doPollWallet(); break; } catch (Exception e) { if (isShutDownStarted) return; log.warn("Error polling main wallet on startup, attempt={}/{}: {}", i + 1, MAX_SYNC_ATTEMPTS, e.getMessage()); if (i + 1 >= MAX_SYNC_ATTEMPTS) { log.warn("Opening application without syncing main wallet"); HavenoUtils.setTopError("Could not sync main wallet on startup.\n\nError: " + e.getMessage()); UserThread.execute(() -> onWalletServiceInitialized()); } } } // start polling wallet startPolling(true); // skip first poll because we already polled } } finally { isInitializingWallet = false; } } } private void resetIfWalletChanged() { getAddressEntryListAsImmutableList(); // TODO: using getter to create base address if necessary List baseAddresses = getAddressEntries(XmrAddressEntry.Context.BASE_ADDRESS); if (baseAddresses.size() > 1 || (baseAddresses.size() == 1 && !baseAddresses.get(0).getAddressString().equals(wallet.getPrimaryAddress()))) { String warningMsg = "New Monero wallet detected. Resetting internal state."; if (!tradeManager.getOpenTrades().isEmpty()) warningMsg += "\n\nWARNING: Your open trades will settle to the payout address in the OLD wallet!"; // TODO: allow payout address to be updated in PaymentSentMessage, PaymentReceivedMessage, and DisputeOpenedMessage? HavenoUtils.setTopError(warningMsg); // reset address entries xmrAddressEntryList.clear(); getAddressEntryListAsImmutableList(); // recreate base address // cancel offers tradeManager.getOpenOfferManager().removeAllOpenOffers(null); } } private MoneroWalletFull createWalletFull(MoneroWalletConfig config, boolean applyProxyUri) { // must be connected to daemon if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) throw new RuntimeException("Must be connected to daemon before creating wallet"); // create wallet MoneroWalletFull walletFull = null; try { // configure wallet connection MoneroRpcConnection connection = new MoneroRpcConnection(xmrConnectionService.getConnection()); if (!applyProxyUri) connection.setProxyUri(null); // create wallet log.debug("Creating full wallet '{}' with monerod={}, proxyUri={}", config.getPath(), connection.getUri(), connection.getProxyUri()); long time = System.currentTimeMillis(); config.setServer(connection); walletFull = MoneroWalletFull.createWallet(config); walletFull.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE); log.info("Done creating full wallet " + config.getPath() + " in " + (System.currentTimeMillis() - time) + " ms"); return walletFull; } catch (Exception e) { String errorMsg = "Could not create wallet '" + config.getPath() + "': " + e.getMessage(); log.warn(errorMsg + "\n", e); if (walletFull != null) forceCloseWallet(walletFull, config.getPath()); throw new IllegalStateException(errorMsg); } } private MoneroWalletFull openWalletFull(MoneroWalletConfig config, boolean applyProxyUri) { MoneroWalletFull walletFull = null; try { // configure wallet connection MoneroRpcConnection connection = new MoneroRpcConnection(xmrConnectionService.getConnection()); if (!applyProxyUri) connection.setProxyUri(null); // try opening wallet config.setNetworkType(getMoneroNetworkType()); config.setServer(connection); log.debug("Opening full wallet '{}' with monerod={}, proxyUri={}", config.getPath(), connection.getUri(), connection.getProxyUri()); try { walletFull = MoneroWalletFull.openWallet(config); } catch (Exception e) { if (isShutDownStarted) throw e; log.warn("Failed to open full wallet '{}', attempting to use backup cache files, error={}", config.getPath(), e.getMessage()); boolean retrySuccessful = false; try { // rename wallet cache to backup String cachePath = walletDir.toString() + File.separator + getWalletName(config.getPath()); File originalCacheFile = new File(cachePath); if (originalCacheFile.exists()) originalCacheFile.renameTo(new File(cachePath + ".backup")); // try opening wallet with backup cache files in descending order List backupCacheFiles = FileUtil.getBackupFiles(walletDir, getWalletName(config.getPath())); Collections.reverse(backupCacheFiles); for (File backupCacheFile : backupCacheFiles) { try { FileUtil.copyFile(backupCacheFile, new File(cachePath)); walletFull = MoneroWalletFull.openWallet(config); log.warn("Successfully opened full wallet using backup cache"); retrySuccessful = true; break; } catch (Exception e2) { // delete cache file if failed to open File cacheFile = new File(cachePath); if (cacheFile.exists()) cacheFile.delete(); File unportableCacheFile = new File(cachePath + ".unportable"); if (unportableCacheFile.exists()) unportableCacheFile.delete(); } } // handle success or failure File originalCacheBackup = new File(cachePath + ".backup"); if (retrySuccessful) { if (originalCacheBackup.exists()) originalCacheBackup.delete(); // delete original wallet cache backup } else { // retry opening wallet after cache deleted try { log.warn("Failed to open full wallet '{}' using backup cache files, retrying with cache deleted", config.getPath()); walletFull = MoneroWalletFull.openWallet(config); log.warn("Successfully opened full wallet after cache deleted"); retrySuccessful = true; } catch (Exception e2) { // ignore } // handle success or failure if (retrySuccessful) { if (originalCacheBackup.exists()) originalCacheBackup.delete(); // delete original wallet cache backup } else { // restore original wallet cache log.warn("Failed to open full wallet '{}' after deleting cache, restoring original cache", config.getPath()); File cacheFile = new File(cachePath); if (cacheFile.exists()) cacheFile.delete(); if (originalCacheBackup.exists()) originalCacheBackup.renameTo(new File(cachePath)); // throw original exception throw e; } } } catch (Exception e2) { throw e; // throw original exception } } if (walletFull.getDaemonConnection() != null) walletFull.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE); log.info("Done opening full wallet " + config.getPath()); return walletFull; } catch (Exception e) { String errorMsg = "Could not open full wallet '" + config.getPath() + "': " + e.getMessage(); log.warn(errorMsg + "\n", e); if (walletFull != null) forceCloseWallet(walletFull, config.getPath()); throw new IllegalStateException(errorMsg); } } private MoneroWalletRpc createWalletRpc(MoneroWalletConfig config, Integer port, boolean applyProxyUri) { // must be connected to daemon if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) throw new RuntimeException("Must be connected to daemon before creating wallet"); // create wallet MoneroWalletRpc walletRpc = null; try { // get daemon connection MoneroRpcConnection serviceConnection = xmrConnectionService.getConnection(); if (serviceConnection == null) throw new IllegalStateException("Cannot create wallet '" + config.getPath() + "' via RPC because daemon connection is null"); // configure wallet connection MoneroRpcConnection connection = new MoneroRpcConnection(serviceConnection); if (!applyProxyUri) connection.setProxyUri(null); // start monero-wallet-rpc instance walletRpc = startWalletRpcInstance(port, connection); walletRpc.getRpcConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE); // prevent wallet rpc from syncing walletRpc.stopSyncing(); // create wallet if (isShutDownStarted) throw new IllegalStateException("Cannot create wallet '" + config.getPath() + "' because shutdown is started"); log.info("Creating RPC wallet '{}' with monerod={}, proxyUri={}", config.getPath(), connection.getUri(), connection.getProxyUri()); long time = System.currentTimeMillis(); config.setServer(connection); walletRpc.createWallet(config); walletRpc.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE); log.info("Done creating RPC wallet " + config.getPath() + " in " + (System.currentTimeMillis() - time) + " ms"); return walletRpc; } catch (Exception e) { if (walletRpc != null) forceCloseWallet(walletRpc, config.getPath()); if (!isShutDownStarted) log.warn("Could not create RPC wallet '" + config.getPath() + "': " + e.getMessage() + "\n", e); throw new IllegalStateException("Could not create wallet '" + config.getPath() + "'. Please close Haveno, stop all monero-wallet-rpc processes in your task manager, and restart Haveno.\n\nError message: " + e.getMessage(), e); } } private MoneroWalletRpc openWalletRpc(MoneroWalletConfig config, Integer port, boolean applyProxyUri) { MoneroWalletRpc walletRpc = null; try { // get daemon connection from service MoneroRpcConnection serviceConnection = xmrConnectionService.getConnection(); if (serviceConnection == null) throw new IllegalStateException("Cannot open wallet '" + config.getPath() + "' via RPC because daemon connection is null"); // configure wallet connection MoneroRpcConnection connection = new MoneroRpcConnection(serviceConnection); if (!applyProxyUri) connection.setProxyUri(null); // start monero-wallet-rpc instance walletRpc = startWalletRpcInstance(port, connection); walletRpc.getRpcConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE); // prevent wallet rpc from syncing walletRpc.stopSyncing(); // try opening wallet if (isShutDownStarted) throw new IllegalStateException("Cannot open wallet '" + config.getPath() + "' because shutdown is started"); log.debug("Opening RPC wallet '{}' with monerod={}, proxyUri={}", config.getPath(), connection.getUri(), connection.getProxyUri()); config.setServer(connection); try { walletRpc.openWallet(config); } catch (Exception e) { if (isShutDownStarted) throw e; log.warn("Failed to open RPC wallet '{}', attempting to use backup cache files, error={}", config.getPath(), e.getMessage()); boolean retrySuccessful = false; try { // rename wallet cache to backup String cachePath = walletDir.toString() + File.separator + config.getPath(); File originalCacheFile = new File(cachePath); if (originalCacheFile.exists()) originalCacheFile.renameTo(new File(cachePath + ".backup")); // try opening wallet with backup cache files in descending order List backupCacheFiles = FileUtil.getBackupFiles(walletDir, config.getPath()); Collections.reverse(backupCacheFiles); for (File backupCacheFile : backupCacheFiles) { try { FileUtil.copyFile(backupCacheFile, new File(cachePath)); walletRpc.openWallet(config); log.warn("Successfully opened RPC wallet using backup cache"); retrySuccessful = true; break; } catch (Exception e2) { // delete cache file if failed to open File cacheFile = new File(cachePath); if (cacheFile.exists()) cacheFile.delete(); File unportableCacheFile = new File(cachePath + ".unportable"); if (unportableCacheFile.exists()) unportableCacheFile.delete(); } } // handle success or failure File originalCacheBackup = new File(cachePath + ".backup"); if (retrySuccessful) { if (originalCacheBackup.exists()) originalCacheBackup.delete(); // delete original wallet cache backup } else { // retry opening wallet after cache deleted try { log.warn("Failed to open RPC wallet '{}' using backup cache files, retrying with cache deleted", config.getPath()); walletRpc.openWallet(config); log.warn("Successfully opened RPC wallet after cache deleted"); retrySuccessful = true; } catch (Exception e2) { // ignore } // handle success or failure if (retrySuccessful) { if (originalCacheBackup.exists()) originalCacheBackup.delete(); // delete original wallet cache backup } else { // restore original wallet cache log.warn("Failed to open RPC wallet '{}' after deleting cache, restoring original cache", config.getPath()); File cacheFile = new File(cachePath); if (cacheFile.exists()) cacheFile.delete(); if (originalCacheBackup.exists()) originalCacheBackup.renameTo(new File(cachePath)); // throw original exception throw e; } } } catch (Exception e2) { throw e; // throw original exception } } if (walletRpc.getDaemonConnection() != null) walletRpc.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE); log.debug("Done opening RPC wallet " + config.getPath()); return walletRpc; } catch (Exception e) { if (walletRpc != null) forceCloseWallet(walletRpc, config.getPath()); if (!isShutDownStarted) log.warn("Could not open RPC wallet '{}': {}\n", config.getPath(), e.getMessage(), e); throw new IllegalStateException("Could not open wallet '" + config.getPath() + "'. Please close Haveno, stop all monero-wallet-rpc processes in your task manager, and restart Haveno.\n\nError message: " + e.getMessage(), e); } } private MoneroWalletRpc startWalletRpcInstance(Integer port, MoneroRpcConnection connection) { // check if monero-wallet-rpc exists if (!new File(MONERO_WALLET_RPC_PATH).exists()) throw new RuntimeException("monero-wallet-rpc executable doesn't exist at path " + MONERO_WALLET_RPC_PATH + "; copy monero-wallet-rpc to the project root or set WalletConfig.java MONERO_WALLET_RPC_PATH for your system"); // build command to start monero-wallet-rpc List cmd = new ArrayList<>(Arrays.asList( // modifiable list MONERO_WALLET_RPC_PATH, "--rpc-login", MONERO_WALLET_RPC_USERNAME + ":" + MONERO_WALLET_RPC_DEFAULT_PASSWORD, "--wallet-dir", walletDir.toString())); // omit --mainnet flag since it does not exist if (MONERO_NETWORK_TYPE != MoneroNetworkType.MAINNET) { cmd.add("--" + MONERO_NETWORK_TYPE.toString().toLowerCase()); } // set connection flags if (connection != null) { cmd.add("--daemon-address"); cmd.add(connection.getUri()); if (connection.getProxyUri() != null) { // TODO: remove this when wallet server is not started with proxy uri cmd.add("--proxy"); cmd.add(connection.getProxyUri()); if (!connection.isOnion()) cmd.add("--daemon-ssl-allow-any-cert"); // necessary to use proxy with clearnet monerod } if (connection.getUsername() != null) { cmd.add("--daemon-login"); cmd.add(connection.getUsername() + ":" + connection.getPassword()); } } if (port != null && port > 0) { cmd.add("--rpc-bind-port"); cmd.add(Integer.toString(port)); } // start monero-wallet-rpc instance and return connected client return MONERO_WALLET_RPC_MANAGER.startInstance(cmd); } @Override protected void onConnectionChanged(MoneroRpcConnection connection) { synchronized (walletLock) { // configure wallet connection connection = new MoneroRpcConnection(xmrConnectionService.getConnection()); if (!isProxyApplied(wasWalletSynced)) connection.setProxyUri(null); // ignore if no change if (wallet == null || isShutDownStarted) return; if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) { updatePollPeriod(); return; } // update connection String oldProxyUri = wallet == null || wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri(); String newProxyUri = connection == null ? null : connection.getProxyUri(); log.info("Setting daemon connection for main wallet, monerod={}, proxyUri={}", connection == null ? null : connection.getUri(), newProxyUri); if (wallet instanceof MoneroWalletRpc && !StringUtils.equals(oldProxyUri, newProxyUri)) { log.info("Restarting main wallet because proxy URI has changed, old={}, new={}", oldProxyUri, newProxyUri); // TODO: remove this when wallet server is not started with proxy uri closeMainWallet(); initMainWallet(); return; // wallet re-initializes off thread } else { wallet.setDaemonConnection(connection); } // update poll period if (connection != null && !isShutDownStarted) { wallet.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE); updatePollPeriod(); } log.info("Done setting daemon connection for main wallet, monerod=" + (wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getUri())); } } private void changeWalletPasswords(String oldPassword, String newPassword) { // create task to change main wallet password List tasks = new ArrayList(); tasks.add(() -> { try { wallet.changePassword(oldPassword, newPassword); saveWallet(); } catch (Exception e) { log.warn("Error changing main wallet password: " + e.getMessage() + "\n", e); throw e; } }); // create tasks to change trade wallet passwords List trades = tradeManager.getAllTrades(); for (Trade trade : trades) { tasks.add(() -> { synchronized (trade.getWalletLock()) { if (trade.walletExists()) { trade.changeWalletPassword(oldPassword, newPassword); // TODO (woodser): this unnecessarily connects and syncs unopen wallets and leaves open } } }); } // execute tasks in parallel ThreadUtils.awaitTasks(tasks, Math.min(10, 1 + trades.size())); log.info("Done changing all wallet passwords"); } private MoneroWallet openOrCreateMainWallet() { synchronized (walletLock) { if (wallet != null) return wallet; if (isShutDownStarted) throw new IllegalStateException("Cannot open or create main wallet because shut down has started"); try { // open or create wallet MoneroDaemonRpc monerod = xmrConnectionService.getMonerod(); boolean isProxyApplied = isProxyApplied(wasWalletSynced); log.info("Initializing main wallet with monerod=" + (monerod == null ? "null" : monerod.getRpcConnection().getUri()) + ", proxyUri=" + (monerod == null || !isProxyApplied ? "null" : monerod.getRpcConnection().getProxyUri())); if (walletExists(MONERO_WALLET_NAME)) { wallet = openWallet(MONERO_WALLET_NAME, rpcBindPort, isProxyApplied); } else { if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) throw new RuntimeException("Cannot create main wallet because there is no connection to Monero daemon"); wallet = createWallet(MONERO_WALLET_NAME, rpcBindPort, isProxyApplied); // set wallet creation date to yesterday to guarantee complete restore LocalDateTime localDateTime = LocalDate.now().atStartOfDay().minusDays(1); long date = localDateTime.toEpochSecond(ZoneOffset.UTC); user.setWalletCreationDate(date); } // set state from wallet walletHeight.set(wallet.getHeight()); cacheWalletInfo(); resetIfWalletChanged(); // backup wallet on successful open or create if (Utilities.isWindows()) { log.info("Closing main wallet to create a backup on Windows"); closeMainWallet(); doBackupWallet(); log.info("Reopening main wallet with monerod=" + (monerod == null ? "null" : monerod.getRpcConnection().getUri()) + ", proxyUri=" + (monerod == null || !isProxyApplied ? "null" : monerod.getRpcConnection().getProxyUri())); wallet = openWallet(MONERO_WALLET_NAME, rpcBindPort, isProxyApplied); } else { doBackupWallet(); } } catch (Exception e) { log.warn("Error initializing main wallet: {}\n", e.getMessage(), e); throw e; } // wait before returning wallet to avoid rate limiting if (!xmrConnectionService.isConnectionLocalHost()) { HavenoUtils.waitFor(INIT_WALLET_DELAY_MS); } return wallet; } } private void doBackupWallet() { synchronized (walletLock) { backupWallet(MONERO_WALLET_NAME); } } private void closeMainWallet() { closeMainWallet(true); } private void closeMainWallet(boolean stopPolling) { synchronized (walletLock) { if (stopPolling) stopPolling(); try { if (wallet != null) { log.info("Closing main wallet"); closeWallet(wallet, true); wallet = null; } } catch (Exception e) { log.warn("Error closing main wallet: {}. Was Haveno stopped manually with ctrl+c?", e.getMessage()); } } } private void forceCloseMainWallet() { stopPolling(); if (wallet != null) { MoneroWallet walletRef = wallet; wallet = null; // nullify wallet before force closing so state is updated for error handling forceCloseWallet(walletRef, getWalletPath(MONERO_WALLET_NAME)); } } public void forceRestartMainWallet() { log.warn("Force restarting main wallet"); forceCloseMainWallet(); initMainWallet(); } public void handleMainWalletError(Exception e, MoneroRpcConnection sourceConnection, int numAttempts) { if (HavenoUtils.isUnresponsive(e)) forceCloseMainWallet(); // wallet can be stuck a while if (numAttempts % TradeProtocol.REQUEST_CONNECTION_SWITCH_EVERY_NUM_ATTEMPTS == 0) requestSwitchToNextBestConnection(sourceConnection); // request connection switch every n attempts initMainWallet(); } private void startPolling() { startPolling(false); } private void startPolling(boolean skipFirstPoll) { synchronized (walletLock) { if (isShutDownStarted || isPolling()) return; updatePollPeriod(); AtomicReference skipNextPoll = new AtomicReference<>(skipFirstPoll); pollLooper = new TaskLooper(() -> { if (skipNextPoll.get()) { skipNextPoll.set(false); return; } try { pollWallet(); } catch (Exception e) { // use default error handling } }); pollLooper.start(pollPeriodMs); } } private void stopPolling() { if (isPolling()) { pollLooper.stop(); pollLooper = null; } } private boolean isPolling() { return pollLooper != null; } public void updatePollPeriod() { if (isShutDownStarted) return; setPollPeriodMs(getPollPeriodMs()); } private long getPollPeriodMs() { return xmrConnectionService.getRefreshPeriodMs(); } private void setPollPeriodMs(long pollPeriodMs) { synchronized (walletLock) { if (this.isShutDownStarted) return; if (this.pollPeriodMs != null && this.pollPeriodMs == pollPeriodMs) return; this.pollPeriodMs = pollPeriodMs; if (isPolling()) { stopPolling(); startPolling(); } } } private void pollWallet() { synchronized (pollLock) { if (pollInProgress) return; } doPollWallet(); } @SuppressWarnings("unused") public void doPollWallet() { // skip polling after wallet service initialized until all domain services are initialized if (isWalletServiceInitialized() && !HavenoUtils.isAllDomainServicesInitialized()) { return; } // skip if shut down started MoneroWallet sourceWallet = wallet; if (isShutDownStarted || sourceWallet == null) return; MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection(); // set poll in progress boolean pollInProgressSet = false; synchronized (pollLock) { if (!pollInProgress) pollInProgressSet = true; pollInProgress = true; } // poll wallet try { // test sync error on startup if (TEST_STARTUP_SYNC_ERROR && !isWalletServiceInitialized()) { throw new RuntimeException("Testing wallet sync error on startup"); } // skip if shut down started if (isShutDownStarted) return; // skip if daemon not synced MoneroDaemonInfo lastInfo = xmrConnectionService.getLastInfo(); if (lastInfo == null) { log.warn("Last daemon info is null"); return; } if (!xmrConnectionService.isSyncedWithinTolerance()) { // throttle warnings if (System.currentTimeMillis() - lastLogDaemonNotSyncedTimestamp > HavenoUtils.LOG_MONEROD_NOT_SYNCED_WARN_PERIOD_MS) { log.warn("Monero daemon is not synced within tolerance, height={}, targetHeight={}, monerod={}", xmrConnectionService.chainHeightProperty().get(), xmrConnectionService.getTargetHeight(), xmrConnectionService.getConnection() == null ? null : xmrConnectionService.getConnection().getUri()); lastLogDaemonNotSyncedTimestamp = System.currentTimeMillis(); } return; } // skip polling if trades are reserving main wallet (disable if testnet or too long since last poll) List tradesReservingMainWallet = tradeManager.getTradesReservingMainWallet(); boolean lastPollWithinTolerance = System.currentTimeMillis() - lastPollTxsTimestamp <= POLL_TXS_TOLERANCE_MS; if (!tradesReservingMainWallet.isEmpty() && lastPollWithinTolerance && !Config.baseCurrencyNetwork().isTestnet()) { List tradeIds = tradesReservingMainWallet.stream().map(Trade::getShortId).collect(Collectors.toList()); log.info("Skipping main wallet poll because trades are reserving main wallet: " + tradeIds); return; } // sync wallet if first sync or behind daemon boolean isFirstSync = !wasWalletSynced; if (isFirstSync || walletHeight.get() < xmrConnectionService.getTargetHeight()) { if (isFirstSync) log.info("Syncing main wallet from height " + walletHeight.get()); long startTime = System.currentTimeMillis(); syncWithProgress(); if (isFirstSync) log.info("Done syncing main wallet in " + (System.currentTimeMillis() - startTime) + " ms"); } // fetch transactions and store to cache // TODO: ideally wallet should sync every poll and then avoid updating from pool on fetching txs? synchronized (HavenoUtils.getDaemonLock()) { if (lastPollTxsTimestamp == 0) lastPollTxsTimestamp = System.currentTimeMillis(); // set initial timestamp try { cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true)); lastPollTxsTimestamp = System.currentTimeMillis(); } catch (Exception e) { // fetch from pool can fail if (!isShutDownStarted && wallet == sourceWallet) { // throttle error handling if (System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS) { log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage()); lastLogPollErrorTimestamp = System.currentTimeMillis(); if (System.currentTimeMillis() - lastPollTxsTimestamp > POLL_TXS_TOLERANCE_MS) ThreadUtils.submitToPool(() -> requestSwitchToNextBestConnection(sourceConnection)); } } } } // handle first wallet sync if (isFirstSync) onFirstSync(); } catch (Exception e) { // skip error handling if shut down or another thread force restarts while polling if (isShutDownStarted || wallet == null || wallet != sourceWallet) return; // log "expected" vs unexpected errors if (Boolean.TRUE.equals(xmrConnectionService.isConnected())) { if (isExpectedWalletError(e)) { log.warn("Error polling main wallet, errorMessage={}. Monerod={}", e.getMessage(), getXmrConnectionService().getConnection()); } else { log.warn("Error polling main wallet, errorMessage={}. Monerod={}", e.getMessage(), getXmrConnectionService().getConnection(), e); // include stack trace for unexpected errors } } // handle unresponsive wallet if (HavenoUtils.isUnresponsive(e)) { forceCloseMainWallet(); } // request connection switch if (!isWalletServiceInitialized()) { requestSwitchToNextBestConnection(sourceConnection); } // reinitialize main wallet if applicable initMainWallet(); throw e; } finally { if (pollInProgressSet) { synchronized (pollLock) { pollInProgress = false; } } requestSaveWalletIfElapsedTime(); // cache wallet info last synchronized (walletLock) { if (wallet != null && !isShutDownStarted) { try { cacheWalletInfo(); } catch (Exception e) { log.warn("Error caching wallet info: " + e.getMessage() + "\n", e); } } } } } private void onFirstSync() { wasWalletSynced = true; if (walletInitListener != null) xmrConnectionService.downloadPercentageProperty().removeListener(walletInitListener); // log wallet balances if (getMoneroNetworkType() != MoneroNetworkType.MAINNET) { BigInteger balance = getBalance(); BigInteger unlockedBalance = getAvailableBalance(); log.info("Monero wallet unlocked balance={}, pending balance={}, total balance={}", unlockedBalance, balance.subtract(unlockedBalance), balance); } // reapply connection after wallet synced for config changes onConnectionChanged(xmrConnectionService.getConnection()); // announce progress on main thread UserThread.execute(() -> { // signal that main wallet is synced syncProgressListener.doneDownload(); // notify setup that main wallet is initialized onWalletServiceInitialized(); }); } private void onWalletServiceInitialized() { HavenoUtils.havenoSetup.getWalletInitialized().set(true); } private boolean isWalletServiceInitialized() { return HavenoUtils.havenoSetup.getWalletInitialized().get(); } private boolean requestSwitchToNextBestConnection() { return requestSwitchToNextBestConnection(null); } private void onNewBlock(long height) { UserThread.execute(() -> { walletHeight.set(height); for (MoneroWalletListenerI listener : walletListeners) ThreadUtils.submitToPool(() -> listener.onNewBlock(height)); }); } private void cacheWalletInfo() { synchronized (walletLock) { if (wallet == null) { log.warn("Cannot cache wallet info because wallet is null"); return; } // get basic wallet info long height = wallet.getHeight(); BigInteger balance = wallet.getBalance(); BigInteger unlockedBalance = wallet.getUnlockedBalance(); cachedSubaddresses = wallet.getSubaddresses(0); cachedOutputs = wallet.getOutputs(); if (cachedTxs == null) cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true).setInTxPool(false)); // cache and notify changes if (cachedHeight == null) { cachedHeight = height; cachedBalance = balance; cachedAvailableBalance = unlockedBalance; onNewBlock(height); onBalancesChanged(balance, unlockedBalance); } else { boolean heightChanged = height != cachedHeight; boolean balancesChanged = !balance.equals(cachedBalance) || !unlockedBalance.equals(cachedAvailableBalance); cachedHeight = height; cachedBalance = balance; cachedAvailableBalance = unlockedBalance; if (heightChanged) onNewBlock(height); if (balancesChanged) onBalancesChanged(balance, unlockedBalance); } } } private void onBalancesChanged(BigInteger newBalance, BigInteger newUnlockedBalance) { updateBalanceListeners(); for (MoneroWalletListenerI listener : walletListeners) listener.onBalancesChanged(newBalance, newUnlockedBalance); } } ================================================ FILE: core/src/main/resources/bip39_english.txt ================================================ abandon ability able about above absent absorb abstract absurd abuse access accident account accuse achieve acid acoustic acquire across act action actor actress actual adapt add addict address adjust admit adult advance advice aerobic affair afford afraid again age agent agree ahead aim air airport aisle alarm album alcohol alert alien all alley allow almost alone alpha already also alter always amateur amazing among amount amused analyst anchor ancient anger angle angry animal ankle announce annual another answer antenna antique anxiety any apart apology appear apple approve april arch arctic area arena argue arm armed armor army around arrange arrest arrive arrow art artefact artist artwork ask aspect assault asset assist assume asthma athlete atom attack attend attitude attract auction audit august aunt author auto autumn average avocado avoid awake aware away awesome awful awkward axis baby bachelor bacon badge bag balance balcony ball bamboo banana banner bar barely bargain barrel base basic basket battle beach bean beauty because become beef before begin behave behind believe below belt bench benefit best betray better between beyond bicycle bid bike bind biology bird birth bitter black blade blame blanket blast bleak bless blind blood blossom blouse blue blur blush board boat body boil bomb bone bonus book boost border boring borrow boss bottom bounce box boy bracket brain brand brass brave bread breeze brick bridge brief bright bring brisk broccoli broken bronze broom brother brown brush bubble buddy budget buffalo build bulb bulk bullet bundle bunker burden burger burst bus business busy butter buyer buzz cabbage cabin cable cactus cage cake call calm camera camp can canal cancel candy cannon canoe canvas canyon capable capital captain car carbon card cargo carpet carry cart case cash casino castle casual cat catalog catch category cattle caught cause caution cave ceiling celery cement census century cereal certain chair chalk champion change chaos chapter charge chase chat cheap check cheese chef cherry chest chicken chief child chimney choice choose chronic chuckle chunk churn cigar cinnamon circle citizen city civil claim clap clarify claw clay clean clerk clever click client cliff climb clinic clip clock clog close cloth cloud clown club clump cluster clutch coach coast coconut code coffee coil coin collect color column combine come comfort comic common company concert conduct confirm congress connect consider control convince cook cool copper copy coral core corn correct cost cotton couch country couple course cousin cover coyote crack cradle craft cram crane crash crater crawl crazy cream credit creek crew cricket crime crisp critic crop cross crouch crowd crucial cruel cruise crumble crunch crush cry crystal cube culture cup cupboard curious current curtain curve cushion custom cute cycle dad damage damp dance danger daring dash daughter dawn day deal debate debris decade december decide decline decorate decrease deer defense define defy degree delay deliver demand demise denial dentist deny depart depend deposit depth deputy derive describe desert design desk despair destroy detail detect develop device devote diagram dial diamond diary dice diesel diet differ digital dignity dilemma dinner dinosaur direct dirt disagree discover disease dish dismiss disorder display distance divert divide divorce dizzy doctor document dog doll dolphin domain donate donkey donor door dose double dove draft dragon drama drastic draw dream dress drift drill drink drip drive drop drum dry duck dumb dune during dust dutch duty dwarf dynamic eager eagle early earn earth easily east easy echo ecology economy edge edit educate effort egg eight either elbow elder electric elegant element elephant elevator elite else embark embody embrace emerge emotion employ empower empty enable enact end endless endorse enemy energy enforce engage engine enhance enjoy enlist enough enrich enroll ensure enter entire entry envelope episode equal equip era erase erode erosion error erupt escape essay essence estate eternal ethics evidence evil evoke evolve exact example excess exchange excite exclude excuse execute exercise exhaust exhibit exile exist exit exotic expand expect expire explain expose express extend extra eye eyebrow fabric face faculty fade faint faith fall false fame family famous fan fancy fantasy farm fashion fat fatal father fatigue fault favorite feature february federal fee feed feel female fence festival fetch fever few fiber fiction field figure file film filter final find fine finger finish fire firm first fiscal fish fit fitness fix flag flame flash flat flavor flee flight flip float flock floor flower fluid flush fly foam focus fog foil fold follow food foot force forest forget fork fortune forum forward fossil foster found fox fragile frame frequent fresh friend fringe frog front frost frown frozen fruit fuel fun funny furnace fury future gadget gain galaxy gallery game gap garage garbage garden garlic garment gas gasp gate gather gauge gaze general genius genre gentle genuine gesture ghost giant gift giggle ginger giraffe girl give glad glance glare glass glide glimpse globe gloom glory glove glow glue goat goddess gold good goose gorilla gospel gossip govern gown grab grace grain grant grape grass gravity great green grid grief grit grocery group grow grunt guard guess guide guilt guitar gun gym habit hair half hammer hamster hand happy harbor hard harsh harvest hat have hawk hazard head health heart heavy hedgehog height hello helmet help hen hero hidden high hill hint hip hire history hobby hockey hold hole holiday hollow home honey hood hope horn horror horse hospital host hotel hour hover hub huge human humble humor hundred hungry hunt hurdle hurry hurt husband hybrid ice icon idea identify idle ignore ill illegal illness image imitate immense immune impact impose improve impulse inch include income increase index indicate indoor industry infant inflict inform inhale inherit initial inject injury inmate inner innocent input inquiry insane insect inside inspire install intact interest into invest invite involve iron island isolate issue item ivory jacket jaguar jar jazz jealous jeans jelly jewel job join joke journey joy judge juice jump jungle junior junk just kangaroo keen keep ketchup key kick kid kidney kind kingdom kiss kit kitchen kite kitten kiwi knee knife knock know lab label labor ladder lady lake lamp language laptop large later latin laugh laundry lava law lawn lawsuit layer lazy leader leaf learn leave lecture left leg legal legend leisure lemon lend length lens leopard lesson letter level liar liberty library license life lift light like limb limit link lion liquid list little live lizard load loan lobster local lock logic lonely long loop lottery loud lounge love loyal lucky luggage lumber lunar lunch luxury lyrics machine mad magic magnet maid mail main major make mammal man manage mandate mango mansion manual maple marble march margin marine market marriage mask mass master match material math matrix matter maximum maze meadow mean measure meat mechanic medal media melody melt member memory mention menu mercy merge merit merry mesh message metal method middle midnight milk million mimic mind minimum minor minute miracle mirror misery miss mistake mix mixed mixture mobile model modify mom moment monitor monkey monster month moon moral more morning mosquito mother motion motor mountain mouse move movie much muffin mule multiply muscle museum mushroom music must mutual myself mystery myth naive name napkin narrow nasty nation nature near neck need negative neglect neither nephew nerve nest net network neutral never news next nice night noble noise nominee noodle normal north nose notable note nothing notice novel now nuclear number nurse nut oak obey object oblige obscure observe obtain obvious occur ocean october odor off offer office often oil okay old olive olympic omit once one onion online only open opera opinion oppose option orange orbit orchard order ordinary organ orient original orphan ostrich other outdoor outer output outside oval oven over own owner oxygen oyster ozone pact paddle page pair palace palm panda panel panic panther paper parade parent park parrot party pass patch path patient patrol pattern pause pave payment peace peanut pear peasant pelican pen penalty pencil people pepper perfect permit person pet phone photo phrase physical piano picnic picture piece pig pigeon pill pilot pink pioneer pipe pistol pitch pizza place planet plastic plate play please pledge pluck plug plunge poem poet point polar pole police pond pony pool popular portion position possible post potato pottery poverty powder power practice praise predict prefer prepare present pretty prevent price pride primary print priority prison private prize problem process produce profit program project promote proof property prosper protect proud provide public pudding pull pulp pulse pumpkin punch pupil puppy purchase purity purpose purse push put puzzle pyramid quality quantum quarter question quick quit quiz quote rabbit raccoon race rack radar radio rail rain raise rally ramp ranch random range rapid rare rate rather raven raw razor ready real reason rebel rebuild recall receive recipe record recycle reduce reflect reform refuse region regret regular reject relax release relief rely remain remember remind remove render renew rent reopen repair repeat replace report require rescue resemble resist resource response result retire retreat return reunion reveal review reward rhythm rib ribbon rice rich ride ridge rifle right rigid ring riot ripple risk ritual rival river road roast robot robust rocket romance roof rookie room rose rotate rough round route royal rubber rude rug rule run runway rural sad saddle sadness safe sail salad salmon salon salt salute same sample sand satisfy satoshi sauce sausage save say scale scan scare scatter scene scheme school science scissors scorpion scout scrap screen script scrub sea search season seat second secret section security seed seek segment select sell seminar senior sense sentence series service session settle setup seven shadow shaft shallow share shed shell sheriff shield shift shine ship shiver shock shoe shoot shop short shoulder shove shrimp shrug shuffle shy sibling sick side siege sight sign silent silk silly silver similar simple since sing siren sister situate six size skate sketch ski skill skin skirt skull slab slam sleep slender slice slide slight slim slogan slot slow slush small smart smile smoke smooth snack snake snap sniff snow soap soccer social sock soda soft solar soldier solid solution solve someone song soon sorry sort soul sound soup source south space spare spatial spawn speak special speed spell spend sphere spice spider spike spin spirit split spoil sponsor spoon sport spot spray spread spring spy square squeeze squirrel stable stadium staff stage stairs stamp stand start state stay steak steel stem step stereo stick still sting stock stomach stone stool story stove strategy street strike strong struggle student stuff stumble style subject submit subway success such sudden suffer sugar suggest suit summer sun sunny sunset super supply supreme sure surface surge surprise surround survey suspect sustain swallow swamp swap swarm swear sweet swift swim swing switch sword symbol symptom syrup system table tackle tag tail talent talk tank tape target task taste tattoo taxi teach team tell ten tenant tennis tent term test text thank that theme then theory there they thing this thought three thrive throw thumb thunder ticket tide tiger tilt timber time tiny tip tired tissue title toast tobacco today toddler toe together toilet token tomato tomorrow tone tongue tonight tool tooth top topic topple torch tornado tortoise toss total tourist toward tower town toy track trade traffic tragic train transfer trap trash travel tray treat tree trend trial tribe trick trigger trim trip trophy trouble truck true truly trumpet trust truth try tube tuition tumble tuna tunnel turkey turn turtle twelve twenty twice twin twist two type typical ugly umbrella unable unaware uncle uncover under undo unfair unfold unhappy uniform unique unit universe unknown unlock until unusual unveil update upgrade uphold upon upper upset urban urge usage use used useful useless usual utility vacant vacuum vague valid valley valve van vanish vapor various vast vault vehicle velvet vendor venture venue verb verify version very vessel veteran viable vibrant vicious victory video view village vintage violin virtual virus visa visit visual vital vivid vocal voice void volcano volume vote voyage wage wagon wait walk wall walnut want warfare warm warrior wash wasp waste water wave way wealth weapon wear weasel weather web wedding weekend weird welcome west wet whale what wheat wheel when where whip whisper wide width wife wild will win window wine wing wink winner winter wire wisdom wise wish witness wolf woman wonder wood wool word work world worry worth wrap wreck wrestle wrist write wrong yard year yellow you young youth zebra zero zone zoo ================================================ FILE: core/src/main/resources/haveno.policy ================================================ grant { permission "java.util.PropertyPermission" "idea.launcher.*", "read"; permission "java.util.PropertyPermission" "slf4j.detectLoggerNameMismatch", "read"; permission "java.util.PropertyPermission" "user.home", "read"; permission "java.util.PropertyPermission" "java.runtime.name", "read"; permission "java.util.PropertyPermission" "java.runtime.version", "read"; permission "java.util.PropertyPermission" "sun.arch.data.model", "read"; permission "java.util.PropertyPermission" "ignoreDevMsg", "read"; permission "java.util.PropertyPermission" "baseCryptoNetwork", "read"; permission "java.util.PropertyPermission" "appDataDir", "read"; permission "java.util.PropertyPermission" "logLevel", "read"; permission "java.util.PropertyPermission" "storageDir", "read"; permission "java.util.PropertyPermission" "keyStorageDir", "read"; permission "java.util.PropertyPermission" "dumpStatistics", "read"; permission "java.util.PropertyPermission" "torDir", "read"; permission "java.util.PropertyPermission" "maxConnections", "read"; permission "java.util.PropertyPermission" "networkId", "read"; permission "java.util.PropertyPermission" "banList", "read"; permission "java.util.PropertyPermission" "socks5ProxyXmrAddress", "read"; permission "java.util.PropertyPermission" "socks5ProxyHttpAddress", "read"; permission "java.util.PropertyPermission" "useragent.name", "read"; permission "java.util.PropertyPermission" "useragent.version", "read"; permission "java.util.PropertyPermission" "walletDir", "read"; permission "java.util.PropertyPermission" "useTorForXmr", "read"; permission "java.util.PropertyPermission" "providers", "read"; permission "java.util.PropertyPermission" "rpcUser", "read"; permission "java.util.PropertyPermission" "rpcPassword", "read"; permission "java.util.PropertyPermission" "rpcPort", "read"; permission "java.util.PropertyPermission" "rpcBlockPort", "read"; permission "java.util.PropertyPermission" "rpcWalletPort", "read"; permission "java.util.PropertyPermission" "logback.*", "read"; permission "java.util.PropertyPermission" "org.apache.commons.logging.*", "read"; permission "java.util.PropertyPermission" "spring.getenv.ignore", "read"; permission "java.util.PropertyPermission" "javafx.toolkit", "read"; permission "java.util.PropertyPermission" "guice.custom.loader", "read"; permission "java.util.PropertyPermission" "cglib.debugLocation", "read"; permission "java.util.PropertyPermission" "useLocalhost", "read"; permission "java.util.PropertyPermission" "nodePort", "read"; permission "java.util.PropertyPermission" "seedNodes", "read"; permission "java.util.PropertyPermission" "bitcoinRegtestHost", "read"; permission "java.util.PropertyPermission" "xmrNodes", "read"; permission "java.util.PropertyPermission" "appName", "read"; permission "java.util.PropertyPermission" "socks5DiscoverMode", "read"; permission "java.util.PropertyPermission" "priceFeedProviders", "read"; permission "java.util.PropertyPermission" "maxMemory", "read"; /* Why ??? no permission exceptions, no dependency in haveno nor in bitcoinj. Local problem? */ permission "java.util.PropertyPermission" "com.google.appengine.runtime.environment", "read"; permission "java.lang.RuntimePermission" "accessDeclaredMembers"; permission "java.lang.RuntimePermission" "setDefaultUncaughtExceptionHandler"; permission "java.lang.RuntimePermission" "accessClassInPackage.sun.misc"; permission "java.lang.RuntimePermission" "accessClassInPackage.sun.util.logging.resources"; permission "java.lang.RuntimePermission" "accessClassInPackage.com.sun.javafx.tk.quantum"; permission "java.lang.RuntimePermission" "accessClassInPackage.sun.reflect"; permission "java.lang.RuntimePermission" "getProtectionDomain"; permission "java.lang.RuntimePermission" "setContextClassLoader"; permission "java.lang.RuntimePermission" "shutdownHooks"; permission "java.lang.RuntimePermission" "getenv.logLevel"; permission "java.lang.RuntimePermission" "getenv.storageDir"; permission "java.lang.RuntimePermission" "getenv.keyStorageDir"; permission "java.lang.RuntimePermission" "getenv.dumpStatistics"; permission "java.lang.RuntimePermission" "getenv.torDir"; permission "java.lang.RuntimePermission" "getenv.maxConnections"; permission "java.lang.RuntimePermission" "getenv.networkId"; permission "java.lang.RuntimePermission" "getenv.banList"; permission "java.lang.RuntimePermission" "getenv.socks5ProxyXmrAddress"; permission "java.lang.RuntimePermission" "getenv.socks5ProxyHttpAddress"; permission "java.lang.RuntimePermission" "getenv.useragent.name"; permission "java.lang.RuntimePermission" "getenv.useragent.version"; permission "java.lang.RuntimePermission" "getenv.walletDir"; permission "java.lang.RuntimePermission" "getenv.useTorForXmr"; permission "java.lang.RuntimePermission" "getenv.providers"; permission "java.lang.RuntimePermission" "getenv.rpcPassword"; permission "java.lang.RuntimePermission" "getenv.rpcUser"; permission "java.lang.RuntimePermission" "getenv.rpcPort"; permission "java.lang.RuntimePermission" "getenv.rpcBlockPort"; permission "java.lang.RuntimePermission" "getenv.rpcWalletPort"; permission "java.lang.RuntimePermission" "getenv.ignoreDevMsg"; permission "java.lang.RuntimePermission" "getenv.ignoreDevMsg"; permission "java.lang.RuntimePermission" "getenv.baseCryptoNetwork"; permission "java.lang.RuntimePermission" "getenv.appDataDir"; permission "java.lang.RuntimePermission" "getenv.socks5DiscoverMode"; permission "java.lang.RuntimePermission" "getenv.priceFeedProviders"; permission "java.lang.RuntimePermission" "getenv.seedNodes"; permission "java.lang.RuntimePermission" "getenv.bitcoinRegtestHost"; permission "java.lang.RuntimePermission" "getenv.xmrNodes"; permission "java.lang.RuntimePermission" "getenv.maxMemory"; permission "java.lang.RuntimePermission" "getClassLoader"; permission "java.lang.RuntimePermission" "accessUserInformation"; permission "java.lang.RuntimePermission" "loadLibrary.javasecp256k1"; permission "java.lang.RuntimePermission" "modifyThread"; /* user data dir for Mac, Linux, Windows */ permission "java.io.FilePermission" "${user.home}${/}Library${/}Application Support${/}-", "read,write,delete"; permission "java.io.FilePermission" "${user.home}${/}.local${/}share${/}haveno-", "read,write,delete"; permission "java.io.FilePermission" "${appdata}${/}haveno-", "read,write,delete"; /* temp dir Mac, Linux, Windows TODO */ permission "java.io.FilePermission" "/var/folders/-", "read,write,delete"; /* only for dev permission "java.io.FilePermission" "${user.home}${/}.m2${/}-", "read"; permission "java.io.FilePermission" "/Users/me/dev/bitcoin_projects/haveno/-", "read"; permission "java.lang.reflect.ReflectPermission" "suppressAccessChecks"; */ permission "java.net.SocketPermission" "127.0.0.1:*", "listen,connect,resolve, accept"; permission "java.net.URLPermission" "http://95.85.11.205:8080/all", "GET:User-Agent"; permission "java.net.URLPermission" "http://95.85.11.205:8080/getFees", "GET:User-Agent"; permission "java.net.URLPermission" "http://95.85.11.205:8080/getAllMarketPrices", "GET:User-Agent"; permission "java.net.SocketPermission" "*:8333", "connect,resolve"; permission "java.net.SocketPermission" "*.onion:80", "connect,resolve"; permission "java.awt.AWTPermission" "accessSystemTray"; permission "java.awt.AWTPermission" "showWindowWithoutWarningBanner"; permission "java.security.SecurityPermission" "insertProvider"; permission "java.util.logging.LoggingPermission" "control"; }; ================================================ FILE: core/src/main/resources/haveno.properties ================================================ ================================================ FILE: core/src/main/resources/help/canceloffer-help.txt ================================================ canceloffer NAME ---- canceloffer - cancel an existing offer to buy or sell XMR SYNOPSIS -------- canceloffer --offer-id= DESCRIPTION ----------- Cancel an existing offer. The offer will be removed from other Haveno users' offer views, and paid transaction fees will be forfeited. OPTIONS ------- --offer-id The ID of the buy or sell offer to cancel. EXAMPLES -------- To cancel an offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea: $ ./haveno-cli --password=xyz --port=9998 canceloffer --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea ================================================ FILE: core/src/main/resources/help/confirmpaymentreceived-help.txt ================================================ confirmpaymentreceived NAME ---- confirmpaymentreceived - confirm payment has been received SYNOPSIS -------- confirmpaymentreceived --trade-id= DESCRIPTION ----------- After the seller receives payment from the XMR buyer, confirmpaymentreceived notifies the buyer the payment has arrived. The seller can release locked XMR only after the this confirmation message has been sent. OPTIONS ------- --trade-id The ID of the trade (the full offer-id). EXAMPLES -------- A XMR seller has taken an offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea, and has recently received the required fiat payment from the buyer's fiat account: $ ./haveno-cli --password=xyz --port=9998 confirmpaymentreceived --trade-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea ================================================ FILE: core/src/main/resources/help/confirmpaymentstarted-help.txt ================================================ confirmpaymentsent NAME ---- confirmpaymentsent - confirm payment has been sent SYNOPSIS -------- confirmpaymentsent --trade-id= DESCRIPTION ----------- After the buyer initiates payment to the XMR seller, confirmpaymentsent notifies the seller to begin watching for a funds deposit in her payment account. OPTIONS ------- --trade-id The ID of the trade (the full offer-id). EXAMPLES -------- A XMR buyer has taken an offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea, and has recently initiated the required fiat payment to the seller's fiat account: $ ./haveno-cli --password=xyz --port=9998 confirmpaymentsent --trade-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea ================================================ FILE: core/src/main/resources/help/createoffer-help.txt ================================================ createoffer NAME ---- createoffer - create offer to buy or sell XMR SYNOPSIS -------- createoffer --payment-account= --direction= --currency-code= --market-price-margin= | --fixed-price= --amount= --min-amount= --security-deposit= [--fee-currency=] DESCRIPTION ----------- Create and place an offer to buy or sell XMR using a fiat account. OPTIONS ------- --payment-account The ID of the fiat payment account used to send or receive funds during the trade. --direction The direction of the trade (BUY or SELL). --currency-code The three letter code for the fiat used to buy or sell XMR, e.g., EUR, USD, BRL, ... --market-price-margin The % above or below market XMR price, e.g., 1.00 (1%). If --market-price-margin is not present, --fixed-price must be. --fixed-price The fixed XMR price in fiat used to buy or sell XMR, e.g., 34000 (USD). If --fixed-price is not present, --market-price-margin must be. --amount The amount of XMR to buy or sell, e.g., 0.125. --min-amount The minimum amount of XMR to buy or sell, e.g., 0.006. If --min-amount is not present, it defaults to the --amount value. --security-deposit The percentage of the XMR amount being traded for the security deposit, e.g., 60.0 (60%). --fee-currency The wallet currency used to pay the Haveno trade maker fee (XMR). Default is XMR EXAMPLES -------- To create a BUY 0.125 XMR with EUR offer at the current market price, using a payment account with ID 7413d263-225a-4f1b-837a-1e3094dc0d77, putting up a 30 percent security deposit, and paying the Haveno maker trading fee in XMR: $ ./haveno-cli --password=xyz --port=9998 createoffer --payment-account=7413d263-225a-4f1b-837a-1e3094dc0d77 \ --direction=buy \ --currency-code=eur \ --amount=0.125 \ --market-price-margin=0.00 \ --security-deposit=30.0 \ --fee-currency=xmr To create a SELL 0.006 XMR for USD offer at a fixed price of 40,000 USD, using a payment account with ID 7413d263-225a-4f1b-837a-1e3094dc0d77, putting up a 25 percent security deposit, and paying the Haveno maker trading fee in XMR: $ ./haveno-cli --password=xyz --port=9998 createoffer --payment-account=7413d263-225a-4f1b-837a-1e3094dc0d77 \ --direction=sell \ --currency-code=usd \ --amount=0.006 \ --fixed-price=40000 \ --security-deposit=25.0 \ --fee-currency=xmr ================================================ FILE: core/src/main/resources/help/createpaymentacct-help.txt ================================================ createpaymentacct NAME ---- createpaymentacct - create a payment account SYNOPSIS -------- createpaymentacct --payment-account-form= DESCRIPTION ----------- Create a Haveno trading account with a payment account form. The details of the payment account are defined in a manually edited json file generated by a getpaymentacctform command, e.g., { "_COMMENTS_": [ "Do not manually edit the paymentMethodId field.", "Edit the salt field only if you are recreating a payment account on a new installation and wish to preserve the account age." ], "paymentMethodId": "SEPA", "accountName": "your accountname", "bic": "your bic", "country": "your country", "holderName": "your holdername", "iban": "your iban", "salt": "" } EXAMPLES -------- To create a new SEPA payment account, find the payment-method-id for the getpaymentacctform command: $ ./haveno-cli --password=xyz --port=9998 getpaymentmethods Get a new, blank SEPA payment account form: $ ./haveno-cli --password=xyz --port=9998 getpaymentacctform --payment-method-id=SEPA The previous command created a json file named sepa_1610817857085.json. The timestamp in the file name is to ensure each generated file is uniquely named (you can rename the file). Manually edit the json file, and pass the file's path to the createpaymentacct command: $ ./haveno-cli --password=xyz --port=9998 createpaymentacct --payment-account-form=sepa_1610817857085.json ================================================ FILE: core/src/main/resources/help/getaddressbalance-help.txt ================================================ getaddressbalance NAME ---- getaddressbalance - get btc address balance SYNOPSIS -------- getaddressbalance --address= DESCRIPTION ----------- Returns the balance of a XMR address in the Haveno server's wallet. OPTIONS ------- --address= The XMR address. EXAMPLES -------- $ ./haveno-cli --password=xyz --port=9998 getaddressbalance --address=bcrt1qygvsqmyt8jyhtp7l3zwqm7s7v3nar6vkc2luz3 ================================================ FILE: core/src/main/resources/help/getbalance-help.txt ================================================ getbalance NAME ---- getbalance - get wallet balance(s) SYNOPSIS -------- getbalance [--currency-code=] DESCRIPTION ----------- Returns full balance information for Haveno XMR wallets. OPTIONS ------- --currency-code= The three letter Haveno wallet crypto currency code. EXAMPLES -------- Show full XMR wallet balance information: $ ./haveno-cli --password=xyz --port=9998 getbalance Show full wallet balance information: $ ./haveno-cli --password=xyz --port=9998 getbalance --currency-code=bsq Show full XMR wallet balance information: $ ./haveno-cli --password=xyz --port=9998 getbalance --currency-code=btc ================================================ FILE: core/src/main/resources/help/getfundingaddresses-help.txt ================================================ getfundingaddresses NAME ---- getfundingaddresses - list XMR receiving address SYNOPSIS -------- getfundingaddresses DESCRIPTION ----------- Returns a list of receiving XMR addresses. EXAMPLES -------- $ ./haveno-cli --password=xyz --port=9998 getfundingaddresses ================================================ FILE: core/src/main/resources/help/getmyoffer-help.txt ================================================ getmyoffer NAME ---- getmyoffer - get your offer to buy or sell XMR SYNOPSIS -------- getmyoffer --offer-id= DESCRIPTION ----------- List one of your existing offers' details. OPTIONS ------- --offer-id The ID of your buy or sell offer. EXAMPLES -------- To view your offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea: $ ./haveno-cli --password=xyz --port=9998 getmyoffer --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea ================================================ FILE: core/src/main/resources/help/getmyoffers-help.txt ================================================ getmyoffers NAME ---- getmyoffers - get your own buy or sell XMR offers for a fiat currency SYNOPSIS -------- getmyoffers --direction= --currency-code= DESCRIPTION ----------- List your existing offers for a direction (SELL|BUY) and currency (EUR|GBP|USD|BRL|...). OPTIONS ------- --direction The direction of the offer (BUY or SELL). --currency-code The three letter code for the fiat used to buy or sell XMR, e.g., EUR, USD, BRL, ... EXAMPLES -------- List all of your existing BUY offers for BRL: $ ./haveno-cli --password=xyz --port=9998 getmyoffers --direction=buy --currency-code=brl List all of your existing SELL offers for EUR: $ ./haveno-cli --password=xyz --port=9998 getmyoffers --direction=sell --currency-code=eur ================================================ FILE: core/src/main/resources/help/getoffer-help.txt ================================================ getoffer NAME ---- getoffer - get an offer to buy or sell XMR SYNOPSIS -------- getoffer --offer-id= DESCRIPTION ----------- List an existing offer's details. The offer must not be one of your own. The offer must be available to take with one of your matching payment accounts. OPTIONS ------- --offer-id The ID of the buy or sell offer to view. EXAMPLES -------- To view an offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea: $ ./haveno-cli --password=xyz --port=9998 getoffer --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea ================================================ FILE: core/src/main/resources/help/getoffers-help.txt ================================================ getoffers NAME ---- getoffers - get available buy or sell XMR offers for a fiat currency SYNOPSIS -------- getoffers --direction= --currency-code= DESCRIPTION ----------- List existing offers for a direction (SELL|BUY) and currency (EUR|GBP|USD|BRL|...). All of the listed offers will be available for the taking because you have a matching payment account, and none of the offers listed will be one of yours. OPTIONS ------- --direction The direction of the offer (BUY or SELL). --currency-code The three letter code for the fiat used to buy or sell XMR, e.g., EUR, USD, BRL, ... EXAMPLES -------- You have one Brazilian Real payment account with a face-to-face payment method type. To view available offers to BUY XMR with BRL, created by other users with the same face-to-fact account type: $ ./haveno-cli --password=xyz --port=9998 getoffers --direction=buy --currency-code=brl You have several EUR payment accounts, each with a different payment method type. To view available offers to SELL XMR with EUR, created by other users having at least one payment account that matches any of your own: $ ./haveno-cli --password=xyz --port=9998 getoffers --direction=sell --currency-code=eur ================================================ FILE: core/src/main/resources/help/getpaymentacctform-help.txt ================================================ getpaymentacctform NAME ---- getpaymentacctform - get a blank payment account form for a payment method SYNOPSIS -------- getpaymentacctform --payment-method-id= DESCRIPTION ----------- Returns a new, blank payment account form as a json file, e.g., { "_COMMENTS_": [ "Do not manually edit the paymentMethodId field.", "Edit the salt field only if you are recreating a payment account on a new installation and wish to preserve the account age." ], "paymentMethodId": "ZELLE", "accountName": "your accountname", "emailOrMobileNr": "your emailormobilenr", "holderName": "your holdername", "salt": "" } This form is manually edited, and used as a parameter to the createpaymentacct command, which creates the new payment account. EXAMPLES -------- To create a new ZELLE payment account, find the payment-method-id for the getpaymentacctform command: $ ./haveno-cli --password=xyz --port=9998 getpaymentmethods Get a new, blank ZELLE payment account form: $ ./haveno-cli --password=xyz --port=9998 getpaymentacctform --payment-method-id=ZELLE_ID The previous command created a json file named zelle_1610818248040.json. The timestamp in the file name is to ensure each generated file is uniquely named (you can rename the file). Manually edit the json file, and pass the file's path to the createpaymentacct command: $ ./haveno-cli --password=xyz --port=9998 createpaymentacct --payment-account-form=zelle_1610818248040.json ================================================ FILE: core/src/main/resources/help/getpaymentaccts-help.txt ================================================ getpaymentaccts NAME ---- getpaymentaccts - list user payment accounts SYNOPSIS -------- getpaymentaccts DESCRIPTION ----------- Returns the list of user payment accounts. EXAMPLES -------- $ ./haveno-cli --password=xyz --port=9998 getpaymentaccts ================================================ FILE: core/src/main/resources/help/getpaymentmethods-help.txt ================================================ getpaymentmethods NAME ---- getpaymentmethods - list fiat payment methods SYNOPSIS -------- getpaymentmethods DESCRIPTION ----------- Returns a list of currently supported fiat payment method IDs. EXAMPLES -------- $ ./haveno-cli --password=xyz --port=9998 getpaymentmethods ================================================ FILE: core/src/main/resources/help/gettrade-help.txt ================================================ gettrade NAME ---- gettrade - get a buy or sell XMR trade SYNOPSIS -------- gettrade --trade-id= [--show-contract=] DESCRIPTION ----------- List details of a trade with the given trade-id. If the trade has not yet been completed, the details can inform each side of the trade of the current phase of the trade protocol. OPTIONS ------- --trade-id The ID of the trade (the full offer-id). --show-contract Optionally display the trade's full contract details in json format. The default = false. EXAMPLES -------- To see the summary of a trade with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea: $ ./haveno-cli --password=xyz --port=9998 gettrade --trade-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea To see the full contract for a trade with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea: $ ./haveno-cli --password=xyz --port=9998 gettrade --trade-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea --show-contract=true ================================================ FILE: core/src/main/resources/help/gettransaction-help.txt ================================================ gettransaction NAME ---- gettransaction - get transaction summary SYNOPSIS -------- gettransaction --transaction-id= DESCRIPTION ----------- Returns a very brief summary of a XMR transaction created by the Haveno server. To see full transaction details, use a monero-core client or an online block explorer. OPTIONS ------- --transaction-id The ID of the XMR transaction. EXAMPLES -------- To see the summary of a transaction with ID 282dc2a5755219a49ee9f6d46a31a2cbaec6624beba96548180eccb1f004cdd8: $ ./haveno-cli --password=xyz --port=9998 gettransaction \ --transaction-id=282dc2a5755219a49ee9f6d46a31a2cbaec6624beba96548180eccb1f004cdd8 ================================================ FILE: core/src/main/resources/help/gettxfeerate-help.txt ================================================ gettxfeerate NAME ---- gettxfeerate - get transaction fee rate SYNOPSIS -------- gettxfeerate DESCRIPTION ----------- Returns the most recent monero network transaction fee the Haveno server could find. EXAMPLES -------- $ ./haveno-cli --password=xyz --port=9998 gettxfeerate ================================================ FILE: core/src/main/resources/help/getversion-help.txt ================================================ getversion NAME ---- getversion - get server version SYNOPSIS -------- getversion DESCRIPTION ----------- Returns the Haveno server version. EXAMPLES -------- $ ./haveno-cli --password=xyz --port=9998 getversion ================================================ FILE: core/src/main/resources/help/getxmrprice-help.txt ================================================ getbtcprice NAME ---- getbtcprice - get current btc market price SYNOPSIS -------- getbtcprice --currency-code= DESCRIPTION ----------- Returns the current market XMR price for the given currency-code. OPTIONS ------- --currency-code The three letter code for the fiat currency code, e.g., EUR, USD, BRL, ... EXAMPLES -------- Get the current XMR market price in Euros: $ ./haveno-cli --password=xyz --port=9998 getbtcprice --currency-code=eur Get the current XMR market price in Brazilian Reais: $ ./haveno-cli --password=xyz --port=9998 getbtcprice --currency-code=brl ================================================ FILE: core/src/main/resources/help/keepfunds-help.txt ================================================ keepfunds NAME ---- keepfunds - keep XMR received during a trade in Haveno wallet SYNOPSIS -------- keepfunds --trade-id= DESCRIPTION ----------- A XMR buyer completes the final step in the trade protocol by keeping received XMR in his Haveno wallet. This step may not seem necessary from the buyer's perspective, but it is necessary for correct transition of a trade's state to CLOSED, within the Haveno server. The alternative way to close out the trade is to send the received XMR to an external XMR wallet, using the withdrawfunds command. OPTIONS ------- --trade-id The ID of the trade (the full offer-id). EXAMPLES -------- A XMR seller has informed the buyer that fiat payment has been received for trade with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea, and locked XMR has been released to the buyer. The XMR buyer closes out the trade by keeping the received XMR in her Haveno wallet: $ ./haveno-cli --password=xyz --port=9998 keepfunds --trade-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea ================================================ FILE: core/src/main/resources/help/lockwallet-help.txt ================================================ lockwallet NAME ---- lockwallet - lock Haveno wallet SYNOPSIS -------- lockwallet DESCRIPTION ----------- Locks an unlocked wallet before an unlockwallet timeout expires. EXAMPLES -------- Immediately lock an encrypted wallet set to automatically lock in the future: $ ./haveno-cli --password=xyz --port=9998 lockwallet ================================================ FILE: core/src/main/resources/help/removewalletpassword-help.txt ================================================ removewalletpassword NAME ---- removewalletpassword - remove a Haveno wallet's encryption password SYNOPSIS -------- removewalletpassword --wallet-password= --timeout= DESCRIPTION ----------- Remove an encryption password from an encrypted Haveno wallet. EXAMPLES -------- $ ./haveno-cli --password=xyz --port=9998 removewalletpassword --wallet-password=mypassword ================================================ FILE: core/src/main/resources/help/sendxmr-help.txt ================================================ sendbtc NAME ---- sendbtc - send XMR to an external wallet SYNOPSIS -------- sendbtc --address= --amount= [--tx-fee-rate=] [--memo=<"memo">] DESCRIPTION ----------- Send XMR from your Haveno wallet to an external XMR address. OPTIONS ------- --address The destination XMR address for the send transaction. --amount The amount of XMR to send. --tx-fee-rate An optional transaction fee rate (sats/byte) for the transaction. The user is responsible for choosing a fee rate that will be accepted by the network in a reasonable amount of time, and the fee rate must be greater than 1 (sats/byte). --memo An optional memo to be saved with the send btc transaction. A multi word memo must be enclosed in double quotes. EXAMPLES -------- Send 0.10 XMR to address bcrt1qygvsqmyt8jyhtp7l3zwqm7s7v3nar6vkc2luz3 with a default transaction fee rate: $ ./haveno-cli --password=xyz --port=9998 sendbtc --address=bcrt1qygvsqmyt8jyhtp7l3zwqm7s7v3nar6vkc2luz3 --amount=0.10 Send 0.05 XMR to address bcrt1qygvsqmyt8jyhtp7l3zwqm7s7v3nar6vkc2luz3 with a transaction fee rate of 10 sats/byte: $ ./haveno-cli --password=xyz --port=9998 sendbtc --address=bcrt1qygvsqmyt8jyhtp7l3zwqm7s7v3nar6vkc2luz3 --amount=0.05 \ --tx-fee-rate=10 Send 0.005 XMR to address bcrt1qygvsqmyt8jyhtp7l3zwqm7s7v3nar6vkc2luz3 with a transaction fee rate of 40 sats/byte, and save a memo with the send transaction: $ ./haveno-cli --password=xyz --port=9998 sendbtc --address=bcrt1qygvsqmyt8jyhtp7l3zwqm7s7v3nar6vkc2luz3 --amount=0.005 \ --tx-fee-rate=40 \ --memo="note to self" ================================================ FILE: core/src/main/resources/help/settxfeerate-help.txt ================================================ settxfeerate NAME ---- settxfeerate - set custom transaction fee rate preference SYNOPSIS -------- settxfeerate --tx-fee-rate= DESCRIPTION ----------- Sets the user's custom transaction fee rate preference. EXAMPLES -------- Set custom transaction fee rate to 25 sats/byte: $ ./haveno-cli --password=xyz --port=9998 settxfeerate --tx-fee-rate=25 ================================================ FILE: core/src/main/resources/help/setwalletpassword-help.txt ================================================ setwalletpassword NAME ---- setwalletpassword - set Haveno wallet password SYNOPSIS -------- setwalletpassword --wallet-password= --new-wallet-password= DESCRIPTION ----------- Encrypts a Haveno wallet with a password. If the optional new wallet password option is present, a new wallet password replaces the existing password EXAMPLES -------- Encrypt an unencrypted Haveno wallet with a password: $ ./haveno-cli --password=xyz --port=9998 setwalletpassword --wallet-password=mypassword Set a new password on a Haveno wallet that is already encrypted: $ ./haveno-cli --password=xyz --port=9998 setwalletpassword --wallet-password=myoldpassword \ --new-wallet-password=mynewpassword ================================================ FILE: core/src/main/resources/help/stop-help.txt ================================================ stop NAME ---- stop - stop the server SYNOPSIS -------- stop DESCRIPTION ----------- Shutdown the RPC server. OPTIONS ------- EXAMPLES -------- To shutdown the server: $ ./haveno-cli --password=xyz --port=9998 stop ================================================ FILE: core/src/main/resources/help/takeoffer-help.txt ================================================ takeoffer NAME ---- takeoffer - take an offer to buy or sell XMR SYNOPSIS -------- takeoffer --offer-id= --payment-account= --fee-currency= DESCRIPTION ----------- Take an existing offer using a matching payment method. The Haveno trade fee can be paid in XMR. OPTIONS ------- --offer-id The ID of the buy or sell offer to take. --payment-account The ID of the fiat payment account used to send or receive funds during the trade. The payment account's payment method must match that of the offer. --fee-currency The wallet currency used to pay the Haveno trade taker fee (XMR). Default is XMR EXAMPLES -------- To take an offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea using a payment account with ID fe20cdbd-22be-4b8a-a4b6-d2608ff09d6e, and paying the Haveno trading fee in XMR: $ ./haveno-cli --password=xyz --port=9998 takeoffer --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ --payment-account=fe20cdbd-22be-4b8a-a4b6-d2608ff09d6e \ -fee-currency=btc ================================================ FILE: core/src/main/resources/help/unlockwallet-help.txt ================================================ unlockwallet NAME ---- unlockwallet - unlock an encrypted Haveno wallet SYNOPSIS -------- unlockwallet --wallet-password= --timeout= DESCRIPTION ----------- Unlocks an encrypted Haveno wallet for a specified number of seconds. The timeout can be manually overridden with the lockwallet command. EXAMPLES -------- Unlock a wallet encrypted with the wallet-password "mypassword" for 30 seconds: $ ./haveno-cli --password=xyz --port=9998 unlockwallet --wallet-password=mypassword --timeout=30 ================================================ FILE: core/src/main/resources/help/unsettxfeerate-help.txt ================================================ unsettxfeerate NAME ---- unsettxfeerate - unset transaction fee rate preference SYNOPSIS -------- unsettxfeerate DESCRIPTION ----------- Unsets (removes) the transaction fee rate user preference. EXAMPLES -------- Remove the user's custom transaction fee rate preference: $ ./haveno-cli --password=xyz --port=9998 unsettxfeerate ================================================ FILE: core/src/main/resources/help/withdrawfunds-help.txt ================================================ withdrawfunds NAME ---- withdrawfunds - send XMR received during a trade to an external XMR wallet SYNOPSIS -------- withdrawfunds --trade-id= --address= [--memo=<"memo">] DESCRIPTION ----------- A XMR buyer completes the final step in the trade protocol by sending received XMR to an external XMR wallet. The alternative way to close out the trade is to keep the received XMR in the Haveno wallet, using the keepfunds command. The buyer needs to complete the trade protocol using the keepfunds or withdrawfunds or command. This step may not seem necessary from the buyer's perspective, but it is necessary for correct transition of a trade's state to CLOSED, within the Haveno server. OPTIONS ------- --trade-id The ID of the trade (the full offer-id). --address The destination btc address for the send btc transaction. --memo An optional memo to be saved with the send btc transaction. A multi word memo must be enclosed in double quotes. EXAMPLES -------- A XMR seller has informed the buyer that fiat payment has been received for trade with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea, and locked XMR has been released to the buyer. The XMR buyer closes out the trade by sending the received XMR to an external XMR wallet: $ ./haveno-cli --password=xyz --port=9998 withdrawfunds --trade-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ --address=2N5J6MyjAsWnashimGiNwoRzUXThsQzRmbv (monero stagetnet address) A seller sends a trade's XMR proceeds to an external wallet, and includes an optional memo: $ ./haveno-cli --password=xyz --port=9998 withdrawfunds --trade-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ --address=2N5J6MyjAsWnashimGiNwoRzUXThsQzRmbv \ --memo="note to self" ================================================ FILE: core/src/main/resources/i18n/displayStrings.properties ================================================ # Keep display strings organized by domain # Naming convention: We use camelCase and dot separated name spaces. # Use as many sub spaces as required to make the structure clear, but as little as possible. # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break # in the display but only in the editor. # Please use in all language files the exact same order of the entries, that way a comparison is easier. # Please try to keep the length of the translated string similar to English. If it is longer it might break layout or # get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. #################################################################### # Shared #################################################################### shared.readMore=Read more shared.openHelp=Open Help shared.warning=Warning shared.close=Close shared.closeAnywayDanger=Shut down anyway (DANGER!) shared.okWait=Ok I'll wait shared.cancel=Cancel shared.ok=OK shared.yes=Yes shared.no=No shared.iUnderstand=I understand shared.continueAnyway=Continue anyway shared.na=N/A shared.shutDown=Shut down shared.reportBug=Report bug on GitHub shared.buyMonero=Buy Monero shared.sellMonero=Sell Monero shared.buyCurrency=Buy {0} shared.sellCurrency=Sell {0} shared.buyCurrency.locked=Buy {0} 🔒 shared.sellCurrency.locked=Sell {0} 🔒 shared.buyingXMRWith=buying XMR with {0} shared.sellingXMRFor=selling XMR for {0} shared.buyingCurrency=buying {0} (selling XMR) shared.sellingCurrency=selling {0} (buying XMR) shared.buy=buy shared.sell=sell shared.buying=buying shared.selling=selling shared.P2P=P2P shared.oneOffer=offer shared.multipleOffers=offers shared.Offer=Offer shared.offerVolumeCode={0} Offer Volume shared.openOffers=open offers shared.trade=trade shared.trades=trades shared.openTrades=open trades shared.dateTime=Date/Time shared.price=Price shared.priceWithCur=Price in {0} shared.priceInCurForCur=Price in {0} for 1 {1} shared.fixedPriceInCurForCur=Fixed price in {0} for 1 {1} shared.amount=Amount shared.txFee=Transaction Fee shared.tradeFee=Trade Fee shared.buyerSecurityDeposit=Buyer Deposit shared.sellerSecurityDeposit=Seller Deposit shared.amountWithCur=Amount in {0} shared.volumeWithCur=Volume in {0} shared.currency=Currency shared.market=Market shared.deviation=Deviation shared.paymentMethod=Payment method shared.tradeCurrency=Trade currency shared.offerType=Offer type shared.details=Details shared.address=Address shared.balanceWithCur=Balance in {0} shared.utxo=Unspent transaction output shared.txId=Transaction ID shared.confirmations=Confirmations shared.revert=Revert Tx shared.select=Select shared.usage=Usage shared.state=Status shared.tradeId=Trade ID shared.offerId=Offer ID shared.traderId=Trader ID shared.bankName=Bank name shared.acceptedBanks=Accepted banks shared.amountMinMax=Amount (min - max) shared.amountHelp=If an offer has a minimum and a maximum amount set, then you can trade any amount within this range. shared.remove=Remove shared.goTo=Go to {0} shared.XMRMinMax=XMR (min - max) shared.removeOffer=Remove offer shared.dontRemoveOffer=Don't remove offer shared.editOffer=Edit offer shared.openLargeQRWindow=Open large QR code window shared.chooseTradingAccount=Choose trading account shared.faq=Visit FAQ page shared.yesCancel=Yes, cancel shared.nextStep=Next step shared.fundFromSavingsWalletButton=Apply funds from Haveno wallet shared.fundFromExternalWalletButton=Open your external wallet for funding shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? shared.belowInPercent=Below % from market price shared.aboveInPercent=Above % from market price shared.enterPercentageValue=Enter % value shared.OR=OR shared.notEnoughFunds=You don't have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. shared.waitingForFunds=Waiting for funds... shared.yourDepositTransactionId=Your deposit transaction ID shared.peerDepositTransactionId=Peer's deposit transaction ID shared.makerDepositTransactionId=Maker's deposit transaction ID shared.takerDepositTransactionId=Taker's deposit transaction ID shared.TheXMRBuyer=The XMR buyer shared.You=You shared.preparingConfirmation=Preparing confirmation... shared.sendingConfirmation=Sending confirmation... shared.sendingConfirmationAgain=Please send confirmation again shared.exportCSV=Export to CSV shared.exportJSON=Export to JSON shared.summary=Show summary shared.noDateAvailable=No date available shared.noDetailsAvailable=No details available shared.notUsedYet=Not used yet shared.date=Date shared.sendFundsDetailsWithFee=Sending: {0}\n\nTo receiving address: {1}\n\nAdditional miner fee: {2}\n\nAre you sure you want to send this amount? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Copy to clipboard shared.copiedToClipboard=Copied to clipboard! shared.language=Language shared.country=Country shared.applyAndShutDown=Apply and shut down shared.selectPaymentMethod=Select payment method shared.accountNameAlreadyUsed=That account name is already used for another saved account.\nPlease choose another name. shared.askConfirmDeleteAccount=Do you really want to delete the selected account? shared.cannotDeleteAccount=You cannot delete that account because it is being used in an open offer (or in an open trade). shared.noAccountsSetupYet=There are no accounts set up yet shared.manageAccounts=Manage accounts shared.addNewAccount=Add new account shared.ExportAccounts=Export Accounts shared.importAccounts=Import Accounts shared.createNewAccount=Create new account shared.createNewAccountDescription=Your account details are stored locally on your device and shared only with your trading peer and the arbitrator if a dispute is opened. shared.saveNewAccount=Save new account shared.selectedAccount=Selected account shared.deleteAccount=Delete account shared.errorMessageInline=\nError message: {0} shared.errorMessage=Error message shared.information=Information shared.name=Name shared.id=ID shared.dashboard=Dashboard shared.accept=Accept shared.balance=Balance shared.save=Save shared.onionAddress=Onion address shared.supportTicket=support ticket shared.dispute=dispute shared.mediationCase=mediation case shared.seller=seller shared.buyer=buyer shared.allEuroCountries=All Euro countries shared.acceptedTakerCountries=Accepted taker countries shared.tradePrice=Trade price shared.tradeAmount=Trade amount shared.tradeVolume=Trade volume shared.reservedAmount=Reserved amount shared.invalidKey=The key you entered was not correct. shared.enterPrivKey=Enter private key to unlock shared.payoutTxId=Payout transaction ID shared.contractAsJson=Contract in JSON format shared.viewContractAsJson=View contract in JSON format shared.contract.title=Contract for trade with ID: {0} shared.paymentDetails=XMR {0} payment details shared.securityDeposit=Security deposit shared.yourSecurityDeposit=Your security deposit shared.contract=Contract shared.messageArrived=Message arrived. shared.messageStoredInMailbox=Message stored in mailbox. shared.messageSendingFailed=Message sending failed. Error: {0} shared.unlock=Unlock shared.toReceive=to receive shared.toSpend=to spend shared.xmrAmount=XMR amount shared.yourLanguage=Your languages shared.addLanguage=Add language shared.total=Total shared.totalsNeeded=Funds needed shared.tradeWalletAddress=Trade wallet address shared.tradeWalletBalance=Trade wallet balance shared.reserveExactAmount=Reserve only the necessary funds. Requires a mining fee and ~20 minutes before your offer goes live. shared.makerTxFee=Maker: {0} shared.takerTxFee=Taker: {0} shared.iConfirm=I confirm shared.openURL=Open {0} shared.fiat=Fiat shared.crypto=Crypto shared.traditional=Traditional shared.otherAssets=other assets shared.other=Other shared.preciousMetals=Precious Metals shared.all=All shared.edit=Edit shared.advancedOptions=Advanced options shared.interval=Interval shared.actions=Actions shared.buyerUpperCase=Buyer shared.sellerUpperCase=Seller shared.new=NEW shared.learnMore=Learn more shared.dismiss=Dismiss shared.selectedArbitrator=Selected arbitrator shared.selectedMediator=Selected mediator shared.selectedRefundAgent=Selected arbitrator shared.mediator=Mediator shared.arbitrator=Arbitrator shared.refundAgent=Arbitrator shared.refundAgentForSupportStaff=Refund agent shared.delayedPayoutTxId=Delayed payout transaction ID shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later. shared.numItemsLabel=Number of entries: {0} shared.filter=Filter shared.enabled=Enabled shared.pending=Pending shared.me=Me shared.maker=Maker shared.taker=Taker shared.none=None #################################################################### # UI views #################################################################### #################################################################### # MainView #################################################################### mainView.menu.market=Market mainView.menu.buyXmr=Buy XMR mainView.menu.sellXmr=Sell XMR mainView.menu.portfolio=Portfolio mainView.menu.funds=Funds mainView.menu.support=Support mainView.menu.settings=Settings mainView.menu.account=Account mainView.marketPriceWithProvider.label=Market price by {0} mainView.marketPrice.havenoInternalPrice=Price of latest Haveno trade mainView.marketPrice.tooltip.havenoInternalPrice=There is no market price from external price feed providers available.\n\ The displayed price is the latest Haveno trade price for that currency. mainView.marketPrice.tooltip=Market price is provided by {0}{1}\nLast update: {2}\nProvider node URL: {3} mainView.balance.available=Available balance mainView.balance.reserved=Reserved in offers mainView.balance.pending=Pending balance mainView.balance.reserved.short=Reserved mainView.balance.pending.short=Pending mainView.footer.usingTor=(via Tor) mainView.footer.localhostMoneroNode=(localhost) mainView.footer.clearnet=(via clearnet) mainView.footer.xmrInfo={0} {1} mainView.footer.xmrFeeRate=/ Fee rate: {0} sat/vB mainView.footer.xmrInfo.initializing=Connecting to Haveno network mainView.footer.xmrInfo.synchronizingWith=Synchronizing with {0} at block: {1} / {2} mainView.footer.xmrInfo.connectedTo=Connected to {0} at block {1} mainView.footer.xmrInfo.synchronizingWalletWith=Synchronizing wallet with {0} at block: {1} / {2} mainView.footer.xmrInfo.syncedWith=Synced with {0} at block {1} mainView.footer.xmrInfo.connectingTo=Connecting to mainView.footer.xmrInfo.connectionFailed=Connection failed to mainView.footer.xmrPeers=Monero network peers: {0} mainView.footer.p2pPeers=Haveno network peers: {0} mainView.footer.version=v{0} mainView.bootstrapState.connectionToTorNetwork=(1/4) Connecting to Tor network... mainView.bootstrapState.torNodeCreated=(2/4) Tor node created mainView.bootstrapState.hiddenServicePublished=(3/4) Hidden Service published mainView.bootstrapState.initialDataReceived=(4/4) Initial data received mainView.bootstrapWarning.noSeedNodesAvailable=No seed nodes available mainView.bootstrapWarning.noNodesAvailable=No seed nodes and peers available mainView.bootstrapWarning.bootstrappingToP2PFailed=Bootstrapping to Haveno network failed mainView.p2pNetworkWarnMsg.noNodesAvailable=There are no seed nodes or persisted peers available for requesting data.\nPlease check your internet connection or try to restart the application. mainView.p2pNetworkWarnMsg.connectionToP2PFailed=Connecting to the Haveno network failed (reported error: {0}).\nPlease check your internet connection or try to restart the application. mainView.walletServiceErrorMsg.timeout=Connecting to the Monero network failed because of a timeout. mainView.walletServiceErrorMsg.connectionError=Connection to the Monero network failed because of an error: {0} mainView.walletServiceErrorMsg.rejectedTxException=A transaction was rejected from the network.\n\n{0} mainView.networkWarning.allConnectionsLost=You lost the connection to all {0} network peers.\nMaybe you lost your internet connection or your computer was in standby mode. mainView.networkWarning.localhostMoneroLost=You lost the connection to the localhost Monero node.\nPlease restart the Haveno application to connect to other Monero nodes or restart the localhost Monero node. mainView.version.update=(Update available) mainView.status.connections=Inbound connections: {0}\nOutbound connections: {1} #################################################################### # MarketView #################################################################### market.tabs.offerBook=Offer book market.tabs.spreadCurrency=Offers by Currency market.tabs.spreadPayment=Offers by Payment Method market.tabs.trades=Trades # OfferBookChartView market.offerBook.sellOffersHeaderLabel=Sell {0} to market.offerBook.buyOffersHeaderLabel=Buy {0} from market.offerBook.buy=I want to buy monero market.offerBook.sell=I want to sell monero # SpreadView market.spread.numberOfOffersColumn=All offers ({0}) market.spread.numberOfBuyOffersColumn=Buy XMR ({0}) market.spread.numberOfSellOffersColumn=Sell XMR ({0}) market.spread.totalAmountColumn=Total XMR ({0}) market.spread.spreadColumn=Spread market.spread.expanded=Expanded view # TradesChartsView market.trades.nrOfTrades=Trades: {0} market.trades.tooltip.volumeBar=Volume: {0} / {1}\nNo. of trades: {2}\nDate: {3} market.trades.tooltip.candle.open=Open: market.trades.tooltip.candle.close=Close: market.trades.tooltip.candle.high=High: market.trades.tooltip.candle.low=Low: market.trades.tooltip.candle.average=Average: market.trades.tooltip.candle.median=Median: market.trades.tooltip.candle.date=Date: market.trades.showVolumeInUSD=Show volume in USD #################################################################### # OfferView #################################################################### offerbook.createOffer=Create offer offerbook.takeOffer=Take offer offerbook.takeOffer.createAccount=Create account and take offer offerbook.takeOffer.enterChallenge=Enter the offer passphrase offerbook.trader=Trader offerbook.offerersBankId=Maker's bank ID (BIC/SWIFT): {0} offerbook.offerersBankName=Maker's bank name: {0} offerbook.offerersBankSeat=Maker's seat of bank country: {0} offerbook.offerersAcceptedBankSeatsEuro=Accepted seat of bank countries (taker): All Euro countries offerbook.offerersAcceptedBankSeats=Accepted seat of bank countries (taker):\n {0} offerbook.availableOffersToBuy=Buy {0} with {1} offerbook.availableOffersToSell=Sell {0} for {1} offerbook.filterByCurrency=Choose currency offerbook.filterByPaymentMethod=Choose payment method offerbook.matchingOffers=Offers matching my accounts offerbook.filterNoDeposit=No deposit offerbook.noDepositOffers=Offers with no deposit (passphrase required) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info.arbitrator=signed by an arbitrator and can sign peer accounts offerbook.timeSinceSigning.info.peer=signed by a peer, waiting %d days for limits to be lifted offerbook.timeSinceSigning.info.peerLimitLifted=signed by a peer and limits were lifted offerbook.timeSinceSigning.info.signer=signed by peer and can sign peer accounts (limits lifted) offerbook.timeSinceSigning.info.banned=account was banned offerbook.timeSinceSigning.daysSinceSigning={0} days offerbook.timeSinceSigning.daysSinceSigning.long={0} since signing offerbook.timeSinceSigning.tooltip.accountLimit=Account limit: {0} offerbook.timeSinceSigning.tooltip.accountLimitLifted=Account limit lifted offerbook.timeSinceSigning.tooltip.info.unsigned=This account hasn't been signed yet offerbook.timeSinceSigning.tooltip.info.signed=This account has been signed offerbook.timeSinceSigning.tooltip.info.signedAndLifted=This account has been signed and can sign peer accounts offerbook.timeSinceSigning.tooltip.checkmark.buyXmr=buy XMR from a signed account offerbook.timeSinceSigning.tooltip.checkmark.wait=wait a minimum of {0} days offerbook.timeSinceSigning.tooltip.learnMore=Learn more offerbook.xmrAutoConf=Is auto-confirm enabled offerbook.buyXmrWith=Buy XMR with: offerbook.sellXmrFor=Sell XMR for: offerbook.cloneOffer=Clone offer with shared funds offerbook.clonedOffer.tooltip=This is a cloned offer with shared funds.\n\Group ID: {0} offerbook.nonClonedOffer.tooltip=Regular offer without shared funds.\n\Maker reserve transaction ID: {0} offerbook.hasConflictingClone.warning=This cloned offer with shared funds cannot be activated because it uses \ the same payment method and currency as another active offer.\n\n\ You need to edit the offer and change the \ payment method or currency or deactivate the offer which has the same payment method and currency. offerbook.cannotActivateEditedOffer.warning=You can't activate an offer that is currently edited. offerbook.clonedOffer.headline=Cloning an offer offerbook.clonedOffer.info=Cloning an offer creates a copy without reserving additional funds.\n\n\ This helps reduce locked capital, making it easier to list the same offer across multiple markets or payment methods.\n\n\ If one of the cloned offers is taken, the others will close automatically, since they all share the same reserved funds.\n\n\ Cloned offers must share the same trade amount and security deposit, but use a different payment method or currency.\n\n\ For more information about cloning offers see: [HYPERLINK:https://docs.haveno.exchange/users/haveno-ui/cloning_an_offer/] offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n\ {0} days later, the initial limit of {1} is lifted and your account can sign other peers' payment accounts. offerbook.timeSinceSigning.notSigned=Not signed yet offerbook.timeSinceSigning.notSigned.ageDays={0} days offerbook.timeSinceSigning.notSigned.noNeed=N/A shared.notSigned.noNeedDays=This account type does not require signing and was created {0} days ago shared.notSigned.noNeedAlts=Cryptocurrency accounts do not feature signing or aging offerbook.nrOffers=No. of offers: {0} offerbook.volume={0} (min - max) offerbook.volumeTotal={0} {1} offerbook.deposit=Deposit XMR (%) offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed. offerbook.XMRTotal=XMR ({0}) offerbook.createNewOffer=Create offer to {0} {1} offerbook.createOfferDisabled.tooltip=You can only create one offer at a time offerbook.takeOfferButton.tooltip=Take offer for {0} offerbook.setupNewAccount=Set up a new trading account offerbook.removeOffer.success=Remove offer was successful. offerbook.removeOffer.failed=Remove offer failed:\n{0} offerbook.deactivateOffer.failed=Deactivating of offer failed:\n{0} offerbook.activateOffer.failed=Publishing of offer failed:\n{0} offerbook.withdrawFundsHint=Offer has been removed. Funds are not reserved for this offer anymore. \ You can send Available funds to an external wallet at the {0} screen. offerbook.warning.noTradingAccountForCurrency.headline=No payment account for selected currency offerbook.warning.noTradingAccountForCurrency.msg=You don't have a payment account set up for the selected currency. offerbook.warning.noMatchingAccount.headline=No matching payment account. offerbook.warning.noMatchingAccount.msg=This offer uses a payment method you haven't set up yet. \n\nWould you like to set up a new payment account now? offerbook.warning.counterpartyTradeRestrictions=This offer cannot be taken due to counterparty trade restrictions offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\n\ After successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\n\ For more information on account signing, please see the documentation at [HYPERLINK:https://docs.haveno.exchange/overview/account_limits/#account-signing]. popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n\ - The buyer's account has not been signed by an arbitrator or a peer\n\ - The time since signing of the buyer's account is not at least 30 days\n\ - The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n\ - Your account has not been signed by an arbitrator or a peer\n\ - The time since signing of your account is not at least 30 days\n\ - The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=This payment method is temporarily limited to {0} until {1} because all buyers have new accounts.\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=Your offer will be limited to buyers with signed and aged accounts because it exceeds {0}.\n\n{1} offerbook.warning.wrongTradeProtocol=That offer requires a different protocol version as the one used in your version of the software.\n\nPlease check if you have the latest version installed, otherwise the user who created the offer has used an older version.\n\nUsers cannot trade with an incompatible trade protocol version. offerbook.warning.userIgnored=You have added that user's onion address to your ignore list. offerbook.warning.offerBlocked=That offer was blocked by the Haveno developers.\nProbably there is an unhandled bug causing issues when taking that offer. offerbook.warning.currencyBanned=The currency used in that offer was blocked by the Haveno developers.\nPlease visit the Haveno Forum for more information. offerbook.warning.paymentMethodBanned=The payment method used in that offer was blocked by the Haveno developers.\nPlease visit the Haveno Forum for more information. offerbook.warning.nodeBlocked=The onion address of that trader was blocked by the Haveno developers.\nProbably there is an unhandled bug causing issues when taking offers from that trader. offerbook.warning.requireUpdateToNewVersion=Your version of Haveno is not compatible for trading anymore.\n\ Please update to the latest Haveno version. offerbook.warning.offerWasAlreadyUsedInTrade=You cannot take this offer because you already took it earlier. \ It could be that your previous take-offer attempt resulted in a failed trade. offerbook.warning.arbitratorNotValidated=This offer cannot be taken because the arbitrator is not registered. offerbook.warning.signatureNotValidated=This offer cannot be taken because the arbitrator's signature is invalid. offerbook.warning.reserveFundsSpent=This offer cannot be taken because the reserved funds were already spent. offerbook.info.sellAtMarketPrice=You will sell at market price (updated every minute). offerbook.info.buyAtMarketPrice=You will buy at market price (updated every minute). offerbook.info.sellBelowMarketPrice=You will get {0} less than the current market price (updated every minute). offerbook.info.buyAboveMarketPrice=You will pay {0} more than the current market price (updated every minute). offerbook.info.sellAboveMarketPrice=You will get {0} more than the current market price (updated every minute). offerbook.info.buyBelowMarketPrice=You will pay {0} less than the current market price (updated every minute). offerbook.info.buyAtFixedPrice=You will buy at this fixed price. offerbook.info.sellAtFixedPrice=You will sell at this fixed price. offerbook.info.roundedFiatVolume=The amount was rounded to increase the privacy of your trade. #################################################################### # Offerbook / Create offer #################################################################### createOffer.amount.prompt=Enter amount in XMR createOffer.price.prompt=Enter price createOffer.volume.prompt=Enter amount in {0} createOffer.amountPriceBox.amountDescription=Amount of XMR to {0} createOffer.amountPriceBox.buy.volumeDescription=Amount in {0} to spend createOffer.amountPriceBox.sell.volumeDescription=Amount in {0} to receive createOffer.amountPriceBox.minAmountDescription=Minimum amount of XMR createOffer.securityDeposit.prompt=Security deposit createOffer.fundsBox.title=Fund your offer createOffer.fundsBox.offerFee=Trade fee createOffer.fundsBox.networkFee=Mining fee createOffer.fundsBox.placeOfferSpinnerInfo=Offer publishing is in progress ... createOffer.fundsBox.paymentLabel=Haveno trade with ID {0} createOffer.fundsBox.fundsStructure=({0} security deposit, {1} trade fee) createOffer.success.headline=Your offer has been created createOffer.success.info=You can manage your open offers at \"Portfolio/My open offers\". createOffer.info.sellAtMarketPrice=You will always sell at market price as the price of your offer will be continuously updated. createOffer.info.buyAtMarketPrice=You will always buy at market price as the price of your offer will be continuously updated. createOffer.info.sellAboveMarketPrice=You will always get {0}% more than the current market price as the price of your offer will be continuously updated. createOffer.info.buyBelowMarketPrice=You will always pay {0}% less than the current market price as the price of your offer will be continuously updated. createOffer.warning.sellBelowMarketPrice=You will always get {0}% less than the current market price as the price of your offer will be continuously updated. createOffer.warning.buyAboveMarketPrice=You will always pay {0}% more than the current market price as the price of your offer will be continuously updated. createOffer.tradeFee.descriptionXMROnly=Trade fee createOffer.tradeFee.description=Trade fee createOffer.triggerPrice.prompt=Set optional trigger price createOffer.triggerPrice.label=Deactivate offer if market price is {0} createOffer.triggerPrice.tooltip=As protection against drastic price movements you can set a trigger price which \ deactivates the offer if the market price reaches that value. createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0} createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0} createOffer.extraInfo.invalid.tooLong=Must not exceed {0} characters. # new entries createOffer.placeOfferButton.buy=Review: Place offer to buy XMR with {0} createOffer.placeOfferButton.sell=Review: Place offer to sell XMR for {0} createOffer.createOfferFundWalletInfo.headline=Fund your offer # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Trade amount: {0} \n createOffer.createOfferFundWalletInfo.msg=You need to deposit {0} to this offer.\n\n\ These funds are reserved in your local wallet and will get locked into a multisig wallet once someone takes your offer.\n\n\ The amount is the sum of:\n\ {1}\ - Your security deposit: {2}\n\ - Trading fee: {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=An error occurred when placing the offer:\n\n{0}\n\n\ No funds have left your wallet yet.\n\ Please restart your application and check your network connection. createOffer.setAmountPrice=Set amount and price createOffer.warnCancelOffer=You have already funded that offer.\nIf you cancel now, your funds will remain in your local Haveno wallet and are available for withdrawal in the \"Funds/Send funds\" screen.\nAre you sure you want to cancel? createOffer.timeoutAtPublishing=A timeout occurred at publishing the offer. createOffer.errorInfo=\n\nThe maker fee is already paid. In the worst case you have lost that fee.\nPlease try to restart your application and check your network connection to see if you can resolve the issue. createOffer.tooLowSecDeposit.warning=You have set the security deposit to a lower value than the recommended default value of {0}.\n\ Are you sure you want to use a lower security deposit? createOffer.tooLowSecDeposit.makerIsSeller=It gives you less protection in case the trading peer does not follow the trade protocol. createOffer.tooLowSecDeposit.makerIsBuyer=It gives less protection for the trading peer that you follow the trade protocol as you have less deposit at risk. \ Other users might prefer to take other offers instead of yours. createOffer.resetToDefault=No, reset to the default value createOffer.useLowerValue=Yes, use my lower value createOffer.priceOutSideOfDeviation=The price you have entered is outside the max. allowed deviation from the market price.\nThe max. allowed deviation is {0} and can be adjusted in the preferences. createOffer.changePrice=Change price createOffer.tac=With publishing this offer I agree to trade with any trader who fulfills the conditions as defined in this screen. createOffer.setDeposit=Set buyer's security deposit (%) createOffer.setDepositAsBuyer=Set my security deposit as buyer (%) createOffer.setDepositForBothTraders=Set both traders' security deposit (%) createOffer.securityDepositInfo=Your buyer's security deposit will be {0} createOffer.securityDepositInfoAsBuyer=Your security deposit as buyer will be {0} createOffer.minSecurityDepositUsed=Minimum security deposit is used createOffer.buyerAsTakerWithoutDeposit=No deposit required from buyer (passphrase protected) createOffer.myDeposit=My security deposit (%) createOffer.myDepositInfo=Your security deposit will be {0} #################################################################### # Offerbook / Take offer #################################################################### takeOffer.amount.prompt=Enter amount in XMR takeOffer.amountPriceBox.buy.amountDescription=Amount of XMR to sell takeOffer.amountPriceBox.sell.amountDescription=Amount of XMR to buy takeOffer.amountPriceBox.buy.amountDescriptionCrypto=Amount of XMR to sell takeOffer.amountPriceBox.sell.amountDescriptionCrypto=Amount of XMR to buy takeOffer.amountPriceBox.priceDescription=Price per monero in {0} takeOffer.amountPriceBox.amountRangeDescription=Possible amount range takeOffer.amountPriceBox.warning.invalidXmrDecimalPlaces=The amount you have entered exceeds the number of allowed decimal places.\nThe amount has been adjusted to 4 decimal places. takeOffer.validation.amountSmallerThanMinAmount=Amount cannot be smaller than minimum amount defined in the offer. takeOffer.validation.amountLargerThanOfferAmount=Input amount cannot be higher than the amount defined in the offer. takeOffer.validation.amountLargerThanOfferAmountMinusFee=That input amount would create dust change for the XMR seller. takeOffer.fundsBox.title=Fund your trade takeOffer.fundsBox.isOfferAvailable=Checking if the offer is still available ... takeOffer.fundsBox.tradeAmount=Amount to sell takeOffer.fundsBox.offerFee=Trade fee takeOffer.fundsBox.networkFee=Total mining fees takeOffer.fundsBox.takeOfferSpinnerInfo=Taking offer: {0} takeOffer.fundsBox.paymentLabel=Haveno trade with ID {0} takeOffer.fundsBox.fundsStructure=({0} security deposit, {1} trade fee) takeOffer.fundsBox.noFundingRequiredTitle=No funding required takeOffer.fundsBox.noFundingRequiredDescription=Get the offer passphrase from the seller outside Haveno to take this offer. takeOffer.success.headline=You have successfully taken an offer. takeOffer.success.info=You can see the status of your trade at \"Portfolio/Open trades\". takeOffer.error.message=An error occurred when taking the offer.\n\n{0} # new entries takeOffer.takeOfferButton.buy=Review: Take offer to buy XMR with {0} takeOffer.takeOfferButton.sell=Review: Take offer to sell XMR for {0} takeOffer.noPriceFeedAvailable=You cannot take that offer as it uses a percentage price based on the market price but there is no price feed available. takeOffer.takeOfferFundWalletInfo.headline=Fund your trade # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Trade amount: {0} \n takeOffer.takeOfferFundWalletInfo.msg=You need to deposit {0} for taking this offer.\n\nThe amount is the sum of:\n{1}- Your security deposit: {2}\n- Trading fee: {3} takeOffer.alreadyPaidInFunds=If you have already paid in funds you can withdraw it in the \"Funds/Send funds\" screen. takeOffer.setAmountPrice=Set amount takeOffer.alreadyFunded.askCancel=You have already funded that offer.\nIf you cancel now, your funds will remain in your local Haveno wallet and are available for withdrawal in the \"Funds/Send funds\" screen.\nAre you sure you want to cancel? takeOffer.failed.offerNotAvailable=Take offer request failed because the offer is not available anymore. Maybe another trader has taken the offer in the meantime. takeOffer.failed.offerTaken=You cannot take that offer because the offer was already taken by another trader. takeOffer.failed.offerInvalid=You cannot take that offer because the maker's signature is invalid. takeOffer.failed.offerRemoved=You cannot take that offer because the offer has been removed in the meantime. takeOffer.failed.offererNotOnline=Take offer request failed because maker is not online anymore. takeOffer.failed.offererOffline=You cannot take that offer because the maker is offline. takeOffer.warning.connectionToPeerLost=You lost connection to the maker.\nThey might have gone offline or has closed the connection to you because of too many open connections.\n\nIf you can still see their offer in the offerbook you can try to take the offer again. takeOffer.error.noFundsLost=\n\nNo funds have left your wallet yet.\nPlease try to restart your application and check your network connection to see if you can resolve the issue. # suppress inspection "TrailingSpacesInProperty" takeOffer.error.feePaid=\n\n takeOffer.error.depositPublished=\n\nThe deposit transaction is already published.\nPlease try to restart your application and check your network connection to see if you can resolve the issue.\nIf the problem still remains please contact the developers for support. takeOffer.error.payoutPublished=\n\nThe payout transaction is already published.\nPlease try to restart your application and check your network connection to see if you can resolve the issue.\nIf the problem still remains please contact the developers for support. takeOffer.tac=With taking this offer I agree to the trade conditions as defined in this screen. #################################################################### # Offerbook / Edit offer #################################################################### openOffer.header.triggerPrice=Trigger price openOffer.header.groupId=Group ID openOffer.triggerPrice=Trigger price {0} openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\n\ Please edit the offer to define a new trigger price editOffer.setPrice=Set price editOffer.confirmEdit=Confirm: Edit offer editOffer.publishOffer=Publishing your offer. editOffer.failed=Editing of offer failed:\n{0} editOffer.success=Your offer has been successfully edited. editOffer.invalidDeposit=The buyer's security deposit is not within the constraints defined by Haveno and can no longer be edited. editOffer.openTabWarning=You have already the \"Edit Offer\" tab open. editOffer.hasConflictingClone=You have edited an offer which uses shared funding with another offer and your edit \ made the payment method and currency now the same as another active cloned offer. Your edited offer will be \ deactivated because it is not permitted to publish 2 offers sharing the funds with the same payment method \ and currency.\n\n\ You can edit the offer again at \"Portfolio/My open offers\" to fulfill the requirements to activate it. cloneOffer.clone=Clone offer cloneOffer.publishOffer=Publishing cloned offer. cloneOffer.success=Your offer has been successfully cloned. cloneOffer.hasConflictingClone=You have not changed the payment method or the currency. You still can clone the offer, but it will \ be deactivated and not published.\n\n\ You can edit the offer later again at \"Portfolio/My open offers\" to fulfill the requirements to activate it.\n\n\ Do you still want to clone the offer? cloneOffer.openTabWarning=You have already the \"Clone Offer\" tab open. #################################################################### # Portfolio #################################################################### portfolio.tab.openOffers=My open offers portfolio.tab.pendingTrades=Open trades portfolio.tab.history=History portfolio.tab.failed=Failed portfolio.tab.editOpenOffer=Edit offer portfolio.tab.duplicateOffer=Create offer portfolio.tab.cloneOpenOffer=Clone offer portfolio.context.offerLikeThis=Create new offer like this... portfolio.context.notYourOffer=You can only duplicate offers where you were the maker. portfolio.closedTrades.deviation.help=Percentage price deviation from market portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\n\ Please do NOT send the traditional or cryptocurrency payment.\n\n\ Open a support ticket to get assistance from a Mediator.\n\n\ Error message: {0} portfolio.pending.unconfirmedTooLong=Deposit transactions on trade {0} are still unconfirmed after {1} hours. \ Check the deposit transactions using a blockchain explorer; if they are confirmed but not displayed as \ confirmed in Haveno, try restarting Haveno.\n\n\ If the problem persists, contact Haveno support [HYPERLINK:https://matrix.to/#/#haveno:monero.social]. portfolio.pending.syncing=Syncing trade wallet portfolio.pending.syncing.blockRemaining=Syncing trade wallet — 1 block remaining portfolio.pending.syncing.blocksRemaining=Syncing trade wallet — {0} blocks remaining portfolio.pending.step1.waitForConf=Wait for blockchain confirmations portfolio.pending.step2_buyer.additionalConf=Deposits have reached 10 confirmations.\nFor extra security, we recommend waiting {0} confirmations before sending payment.\nProceed early at your own risk. portfolio.pending.step2_buyer.startPayment=Start payment portfolio.pending.step2_seller.waitPaymentSent=Wait until payment has been sent portfolio.pending.step3_buyer.waitPaymentArrived=Wait until payment arrived portfolio.pending.step3_seller.confirmPaymentReceived=Confirm payment received portfolio.pending.step5.completed=Completed portfolio.pending.step3_seller.autoConf.status.label=Auto-confirm status portfolio.pending.autoConf=Auto-confirmed portfolio.pending.autoConf.blocks=XMR confirmations: {0} / Required: {1} portfolio.pending.autoConf.state.xmr.txKeyReused=Transaction key re-used. Please open a dispute. portfolio.pending.autoConf.state.confirmations=XMR confirmations: {0}/{1} portfolio.pending.autoConf.state.txNotFound=Transaction not seen in mem-pool yet portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=No valid transaction ID / transaction key portfolio.pending.autoConf.state.filterDisabledFeature=Disabled by developers. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FEATURE_DISABLED=Auto-confirm feature is disabled. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=Trade amount exceeds auto-confirm amount limit # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.INVALID_DATA=Peer provided invalid data. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=Payout transaction was already published. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.DISPUTE_OPENED=Dispute was opened. Auto-confirm is deactivated for that trade. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.REQUESTS_STARTED=Transaction proof requests started # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PENDING=Success results: {0}/{1}; {2} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.COMPLETED=Proof at all services succeeded # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. No auto-confirm possible. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. portfolio.pending.step1.info.you=Deposit transaction has been published.\nYou can start the payment after 10 confirmations (~{0} minutes remaining). portfolio.pending.step1.info.buyer=Deposit transaction has been published.\nThe XMR buyer can start the payment after 10 confirmations (~{0} minutes remaining). portfolio.pending.step1.warn=The deposit transaction is not confirmed yet. This usually takes about 20 minutes, but could be more if the network is congested. portfolio.pending.step1.openForDispute=The deposit transaction is still not confirmed. \ If you have been waiting for much longer than 20 minutes, contact Haveno support. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2.confReached=Your trade has reached 10 confirmations.\n\n portfolio.pending.step2_buyer.refTextWarn=Important: when making the payment, leave the \"reason for payment\" field \ empty. DO NOT put the trade ID or any other text like 'monero', 'XMR', or 'Haveno'. \ You are free to discuss via trader chat if an alternate \"reason for payment\" would be suitable to you both. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees=If your bank charges you any fees to make the transfer, you are responsible for paying those fees. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees.swift=Make sure to use the SHA (shared fee model) to send the SWIFT payment. \ See more details at [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/SWIFT#Use_the_correct_fee_option]. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.crypto=Please transfer from your external {0} wallet\n{1} to the XMR seller.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.cash=Please go to a bank and pay {0} to the XMR seller.\n\n portfolio.pending.step2_buyer.cash.extra=IMPORTANT REQUIREMENT:\nAfter you have done the payment write on the paper receipt: NO REFUNDS.\nThen tear it in 2 parts, make a photo and send it to the XMR seller's email address. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.moneyGram=Please pay {0} to the XMR seller by using MoneyGram.\n\n portfolio.pending.step2_buyer.moneyGram.extra=IMPORTANT REQUIREMENT:\nAfter you have done the payment send the Authorisation number and a photo of the receipt by email to the XMR seller.\n\ The receipt must clearly show the seller's full name, country, state and the amount. The seller's email is: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.westernUnion=Please pay {0} to the XMR seller by using Western Union.\n\n portfolio.pending.step2_buyer.westernUnion.extra=IMPORTANT REQUIREMENT:\nAfter you have done the payment send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller.\n\ The receipt must clearly show the seller's full name, city, country and the amount. The seller's email is: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Please send {0} by \"US Postal Money Order\" to the XMR seller.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. \ Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. \ See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Please contact the XMR seller by the provided contact and arrange a meeting to pay {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Start payment using {0} portfolio.pending.step2_buyer.recipientsAccountData=Recipients {0} portfolio.pending.step2_buyer.amountToTransfer=Amount to transfer portfolio.pending.step2_buyer.sellersAddress=Seller's {0} address portfolio.pending.step2_buyer.buyerAccount=Your payment account to be used portfolio.pending.step2_buyer.paymentSent=Payment sent portfolio.pending.step2_buyer.showEarly=Show payment details early portfolio.pending.step2_buyer.warn=You still have not done your {0} payment!\nPlease note that the trade has to be completed by {1}. portfolio.pending.step2_buyer.openForDispute=You have not completed your payment yet\nThe max. period for the trade has elapsed, but you can still complete the payment.\n\ Contact an arbitrator if you need assistance. portfolio.pending.step2_buyer.paperReceipt.headline=Did you send the paper receipt to the XMR seller? portfolio.pending.step2_buyer.paperReceipt.msg=Remember:\n\ You need to write on the paper receipt: NO REFUNDS.\n\ Then tear it in 2 parts, make a photo and send it to the XMR seller's email address. portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Send Authorisation number and receipt portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=You need to send the Authorisation number and a photo of the receipt by email to the XMR seller.\n\ The receipt must clearly show the seller's full name, country, state and the amount. The seller's email is: {0}.\n\n\ Did you send the Authorisation number and contract to the seller? portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=Send MTCN and receipt portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=You need to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller.\n\ The receipt must clearly show the seller's full name, city, country and the amount. The seller's email is: {0}.\n\n\ Did you send the MTCN and contract to the seller? portfolio.pending.step2_buyer.halCashInfo.headline=Send HalCash code portfolio.pending.step2_buyer.halCashInfo.msg=You need to send a text message with the HalCash code as well as the \ trade ID ({0}) to the XMR seller.\nThe seller's mobile nr. is {1}.\n\n\ Did you send the code to the seller? portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Some banks might require the receiver's name. \ The UK sort code and account number used to be sufficient for a Faster Payment transfer, but since then some banks \ have started verifying the receiver's name. For this reason, accounts created in old Haveno clients do not provide the \ trade partner with an owner full name. Please use trade chat to avoid any issues in such cases. portfolio.pending.step2_buyer.confirmStart.headline=Confirm that you have started the payment portfolio.pending.step2_buyer.confirmStart.msg=Did you initiate the {0} payment to your trading partner? portfolio.pending.step2_buyer.confirmStart.yes=Yes, I have started the payment portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=You have not provided proof of payment portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=You have not entered the transaction ID and the transaction key.\n\n\ By not providing this data the peer cannot use the auto-confirm feature to release the XMR as soon the XMR has been received.\n\ Beside that, Haveno requires that the sender of the XMR transaction is able to provide this information to the arbitrator in case of a dispute.\n\ See more details on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=Input is not a 32 byte hexadecimal value portfolio.pending.step2_buyer.confirmStart.warningButton=Ignore and continue anyway portfolio.pending.step2_seller.waitPayment.headline=Wait for payment portfolio.pending.step2_seller.f2fInfo.headline=Buyer's contact information portfolio.pending.step2_seller.waitPayment.msg=The deposit transaction is unlocked.\nYou need to wait until the XMR buyer starts the {0} payment. portfolio.pending.step2_seller.warn=The XMR buyer still has not done the {0} payment.\nYou need to wait until they have started the payment.\nIf the trade has not been completed on {1} the arbitrator will investigate. portfolio.pending.step2_seller.openForDispute=The XMR buyer has not started their payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the arbitrator for assistance. disputeChat.chatWindowTitle=Dispute chat window for trade with ID '{0}' tradeChat.chatWindowTitle=Trader Chat window for trade with ID '{0}' tradeChat.openChat=Open chat window tradeChat.rules=You can communicate with your trade peer to resolve potential problems with this trade.\n\ It is not mandatory to reply in the chat.\n\ If a trader violates any of the rules below, open a dispute and report it to the arbitrator.\n\n\ Chat rules:\n\ \t● Do not send any links (risk of malware). You can send the transaction ID and the name of a block explorer.\n\ \t● Do not send your seed words, private keys, passwords or other sensitive information!\n\ \t● Do not encourage trading outside of Haveno (no security).\n\ \t● Do not engage in any form of social engineering scam attempts.\n\ \t● If a peer is not responding and prefers to not communicate via chat, respect their decision.\n\ \t● Keep conversation scope limited to the trade. This chat is not a messenger replacement or troll-box.\n\ \t● Keep conversation friendly and respectful. # suppress inspection "UnusedProperty" message.state.UNDEFINED=Undefined # suppress inspection "UnusedProperty" message.state.SENT=Message sent # suppress inspection "UnusedProperty" message.state.ARRIVED=Message arrived at peer # suppress inspection "UnusedProperty" message.state.STORED_IN_MAILBOX=Message of payment sent but not yet received by peer # suppress inspection "UnusedProperty" message.state.ACKNOWLEDGED=Peer confirmed message receipt # suppress inspection "UnusedProperty" message.state.FAILED=Sending message failed portfolio.pending.step3_buyer.wait.headline=Wait for XMR seller's payment confirmation portfolio.pending.step3_buyer.wait.info=Waiting for the XMR seller's confirmation for the receipt of the {0} payment. portfolio.pending.step3_buyer.wait.msgStateInfo.label=Payment started message status portfolio.pending.step3_buyer.warn.part1a=on the {0} blockchain portfolio.pending.step3_buyer.warn.part1b=at your payment provider (e.g. bank) portfolio.pending.step3_buyer.warn.part2=The XMR seller still has not confirmed your payment. Please check {0} if the \ payment sending was successful. portfolio.pending.step3_buyer.openForDispute=The XMR seller has not confirmed your payment! The max. period for the \ trade has elapsed. You can wait longer and give the trading peer more time or request assistance from the arbitrator. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.part=Your trading partner has confirmed that they have initiated the {0} payment.\n\n portfolio.pending.step3_seller.crypto.explorer=on your favorite {0} blockchain explorer portfolio.pending.step3_seller.crypto.wallet=at your {0} wallet portfolio.pending.step3_seller.crypto={0}Please check {1} if the transaction to your receiving address\n\ {2}\n\ has already sufficient blockchain confirmations.\nThe payment amount has to be {3}\n\n\ You can copy & paste your {4} address from the main screen after closing that popup. portfolio.pending.step3_seller.postal={0}Please check if you have received {1} with \"US Postal Money Order\" from the XMR buyer. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.payByMail={0}Please check if you have received {1} with \"Pay by Mail\" from the XMR buyer. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.bank=Your trading partner has confirmed that they have initiated the {0} payment.\n\n\ Please go to your online banking web page and check if you have received {1} from the XMR buyer. portfolio.pending.step3_seller.cash=Because the payment is done via Cash Deposit the XMR buyer has to write \"NO REFUND\" on the paper receipt, tear it in 2 parts and send you a photo by email.\n\n\ To avoid chargeback risk, only confirm if you received the email and if you are sure the paper receipt is valid.\n\ If you are not sure, {0} portfolio.pending.step3_seller.moneyGram=The buyer has to send you the Authorisation number and a photo of the receipt by email.\n\ The receipt must clearly show your full name, country, state and the amount. Please check your email if you received the Authorisation number.\n\n\ After closing that popup you will see the XMR buyer's name and address for picking up the money from MoneyGram.\n\n\ Only confirm receipt after you have successfully picked up the money! portfolio.pending.step3_seller.westernUnion=The buyer has to send you the MTCN (tracking number) and a photo of the receipt by email.\n\ The receipt must clearly show your full name, city, country and the amount. Please check your email if you received the MTCN.\n\n\ After closing that popup you will see the XMR buyer's name and address for picking up the money from Western Union.\n\n\ Only confirm receipt after you have successfully picked up the money! portfolio.pending.step3_seller.halCash=The buyer has to send you the HalCash code as text message. Beside that you will receive a message from HalCash with the required information to withdraw the EUR from a HalCash supporting ATM.\n\n\ After you have picked up the money from the ATM please confirm here the receipt of the payment! portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon eGift Card by email or by text \ message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted \ confirm the payment receipt. portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\n\ If the names are not exactly the same, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=Confirm payment receipt portfolio.pending.step3_seller.amountToReceive=Amount to receive portfolio.pending.step3_seller.yourAddress=Your {0} address portfolio.pending.step3_seller.buyersAddress=Buyers {0} address portfolio.pending.step3_seller.yourAccount=Your trading account portfolio.pending.step3_seller.xmrTxHash=Transaction ID portfolio.pending.step3_seller.xmrTxKey=Transaction key portfolio.pending.step3_seller.buyersAccount=Buyers account data portfolio.pending.step3_seller.confirmReceipt=Confirm payment receipt portfolio.pending.step3_seller.buyerStartedPayment=The XMR buyer has started the {0} payment.\n{1} portfolio.pending.step3_seller.buyerStartedPayment.crypto=Check for blockchain confirmations at your cryptocurrency wallet or block explorer and confirm the payment when you have sufficient blockchain confirmations. portfolio.pending.step3_seller.buyerStartedPayment.traditional=Check at your trading account (e.g. bank account) and confirm when you have received the payment. portfolio.pending.step3_seller.warn.part1a=on the {0} blockchain portfolio.pending.step3_seller.warn.part1b=at your payment provider (e.g. bank) portfolio.pending.step3_seller.warn.part2=You still have not confirmed the receipt of the payment. \ Please check {0} if you have received the payment. portfolio.pending.step3_seller.openForDispute=You have not confirmed the receipt of the payment yet.\n\ The max. period for the trade has elapsed.\nPlease confirm or request assistance from the arbitrator. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=Have you received the {0} payment from your trading partner?\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Please note, that as soon you have confirmed the receipt, the locked trade amount will be released to the XMR buyer and the security deposit will be refunded.\n\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Confirm that you have received the payment portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=Yes, I have received the payment portfolio.pending.step3_seller.onPaymentReceived.signer=IMPORTANT: By confirming receipt of payment, you are also \ verifying the account of the counterparty and signing it accordingly. Since the account of the counterparty hasn't been signed yet, \ you should delay confirmation of the payment as long as possible to reduce the risk of a chargeback. portfolio.pending.step5_buyer.groupTitle=Summary of completed trade portfolio.pending.step5_buyer.groupTitle.mediated=This trade was resolved by mediation portfolio.pending.step5_buyer.groupTitle.arbitrated=This trade was resolved by arbitration portfolio.pending.step5_buyer.tradeFee=Trade fee portfolio.pending.step5_buyer.makersMiningFee=Mining fee portfolio.pending.step5_buyer.takersMiningFee=Total mining fees portfolio.pending.step5_buyer.refunded=Refunded security deposit portfolio.pending.step5_buyer.amountTooLow=The amount to transfer is lower than the transaction fee and the min. possible tx value (dust). portfolio.pending.step5_buyer.tradeCompleted.headline=Trade completed portfolio.pending.step5_buyer.tradeCompleted.msg=Your completed trades are stored under \"Portfolio/History\".\nYou can review all your monero transactions under \"Funds/Transactions\" portfolio.pending.step5_buyer.bought=You have bought portfolio.pending.step5_buyer.paid=You have paid portfolio.pending.step5_seller.sold=You have sold portfolio.pending.step5_seller.received=You have received tradeFeedbackWindow.title=Your trade was completed successfully! tradeFeedbackWindow.msg.part1=We'd love to hear back from you about your experience. It'll help us improve the software and smooth out any rough edges. tradeFeedbackWindow.msg.part2=If you have any questions, or experienced any problems, please get in touch with other users and contributors in our Matrix chatroom: tradeFeedbackWindow.msg.part3=Thanks for using Haveno! portfolio.pending.role=My role portfolio.pending.tradeInformation=Trade information portfolio.pending.remainingTime=Remaining time portfolio.pending.remainingTimeDetail={0} (until {1}) portfolio.pending.remainingTimeDetail.startsAfter=Starts after {0} confirmations portfolio.pending.tradePeriodInfo=After {0} confirmations, the trade period starts. Based on the payment method used, a different maximum allowed trade period is applied. portfolio.pending.tradePeriodWarning=If the period is exceeded both traders can open a dispute. portfolio.pending.tradeNotCompleted=Trade not completed in time (until {0}) portfolio.pending.tradeProcess=Trade process portfolio.pending.stillNotResolved=If your issue remains unsolved, you can request support in our [Matrix chatroom](https://matrix.to/#/#haveno:monero.social). portfolio.pending.openAgainDispute.msg=If you are not sure that the message to the arbitrator arrived (e.g. if you did not get a response after 1 day) feel free to contact the team. portfolio.pending.openAgainDispute.button=Open dispute again portfolio.pending.openSupportTicket.headline=Open support ticket portfolio.pending.openSupportTicket.msg=Please use this function only in emergency cases if you don't see a \ \"Open support\" or \"Open dispute\" button.\n\nWhen you open a support ticket the trade will be interrupted and \ handled by an arbitrator. portfolio.pending.timeLockNotOver=You have to wait until ≈{0} ({1} more blocks) before you can open an arbitration dispute. portfolio.pending.error.depositTxNull=A deposit transaction is null. You cannot open a dispute \ with an invalid deposit transaction.\n\n\ For further help, please contact Haveno support in our Matrix chatroom. portfolio.pending.mediationResult.error.depositTxNull=The deposit transaction is null. You can move the \ trade to failed trades. portfolio.pending.mediationResult.error.delayedPayoutTxNull=The delayed payout transaction is null. You can move the \ trade to failed trades. portfolio.pending.error.depositTxNotConfirmed=The deposit transactions are not confirmed and available. You cannot open an arbitration dispute \ with a pending deposit transaction. Please wait until both deposit transasctions are confirmed and available.\n\n\ For further help, please contact Haveno support in our Matrix chatroom. portfolio.pending.support.headline.getHelp=Need help? portfolio.pending.support.button.getHelp=Open Trader Chat portfolio.pending.support.headline.halfPeriodOver=Check payment portfolio.pending.support.headline.periodOver=Trade period is over portfolio.pending.support.headline.depositTxMissing=Missing deposit transaction portfolio.pending.support.depositTxMissing=A deposit transaction is missing for this trade. Open a support ticket to contact an arbitrator for assistance. portfolio.pending.arbitrationRequested=Arbitration requested portfolio.pending.mediationRequested=Mediation requested portfolio.pending.refundRequested=Refund requested portfolio.pending.openSupport=Open support ticket portfolio.pending.supportTicketOpened=Support ticket opened portfolio.pending.communicateWithArbitrator=Please communicate with the arbitrator on the \"Support\" screen. portfolio.pending.communicateWithMediator=Please communicate with the mediator on the \"Support\" screen. portfolio.pending.disputeOpenedByUser=You have already opened a dispute.\n{0} portfolio.pending.disputeOpenedByPeer=Your trading peer opened a dispute\n{0} portfolio.pending.noReceiverAddressDefined=No receiver address defined portfolio.pending.mediationResult.headline=Suggested payout from mediation portfolio.pending.mediationResult.info.noneAccepted=Complete the trade by accepting the mediator's suggestion for the trade payout. portfolio.pending.mediationResult.info.selfAccepted=You have accepted the mediator's suggestion. Waiting for peer to accept as well. portfolio.pending.mediationResult.info.peerAccepted=Your trade peer has accepted the mediator's suggestion. Do you accept as well? portfolio.pending.mediationResult.button=View proposed resolution portfolio.pending.mediationResult.popup.headline=Mediation result for trade with ID: {0} portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator's suggestion for trade {0} portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\n\ You receive: {0}\n\ Your trading peer receives: {1}\n\n\ You can accept or reject this suggested payout.\n\n\ By accepting, you sign the proposed payout transaction. \ If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\n\ If one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a \ second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\n\ The arbitrator may charge a small fee (fee maximum: the trader's security deposit) as compensation for their work. \ Both traders agreeing to the mediator's suggestion is the happy path—requesting arbitration is meant for \ exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion \ (or if the other peer is unresponsive).\n\n\ More details about the new arbitration model: [HYPERLINK:https://haveno.exchange/wiki/Dispute_resolution#Level_3:_Arbitration] portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator's suggested payout \ but it seems that your trading peer has not accepted it.\n\n\ Once the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will \ investigate the case again and do a payout based on their findings.\n\n\ You can find more details about the arbitration model at:\ [HYPERLINK:https://haveno.exchange/wiki/Dispute_resolution#Level_3:_Arbitration] portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\n\ Without this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. \ You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\n\ Without this tx, the trade cannot be completed. No funds have been locked. \ Your offer is still available to other traders, so you have not lost the maker fee. \ You can move this trade to failed trades. portfolio.pending.failedTrade.missingDepositTx=A deposit transaction is missing.\n\nThis transaction is required to complete the trade. Please ensure your wallet is fully synchronized with the Monero blockchain.\n\nYou can move this trade to the "Failed Trades" section to deactivate it. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, \ but funds have been locked in the deposit transaction.\n\n\ Please do NOT send the traditional or cryptocurrency payment to the XMR seller, because without the delayed payout tx, arbitration \ cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. \ The mediator should suggest that both peers each get back the full amount of their security deposits \ (with seller receiving full trade amount back as well). \ This way, there is no security risk, and only trade fees are lost. \n\n\ You can request a reimbursement for lost trade fees here: \ [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing \ but funds have been locked in the deposit transaction.\n\n\ If the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open \ a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\n\ If the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of \ their security deposits (with seller receiving full trade amount back as well). \ Otherwise the trade amount should go to the buyer. \n\n\ You can request a reimbursement for lost trade fees here: \ [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\n\ Error: {0}\n\n\ It might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation \ ticket to get advice from Haveno mediators. \n\n\ If the error was critical and the trade cannot be completed, you might have lost your trade fee. \ Request a reimbursement for lost trade fees here: \ [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\n\ The trade cannot be completed and you might \ have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: \ [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\n\ Do you want to move the trade to failed trades?\n\n\ You cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open \ trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\n\ The trade transactions have been published and funds are locked. Only move the trade to failed trades if you are \ really sure. It might prevent options to resolve the problem.\n\n\ Do you want to move the trade to failed trades?\n\n\ You cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open \ trades screen any time. portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Move trade to failed trades portfolio.pending.failedTrade.warningIcon.tooltip=Click to open details about the issues of this trade portfolio.failed.revertToPending.popup=Do you want to move this trade to open trades? portfolio.failed.revertToPending.failed=Failed to move this trade to open trades. portfolio.failed.revertToPending=Move trade to open trades portfolio.closed.completed=Completed portfolio.closed.ticketClosed=Arbitrated portfolio.closed.mediationTicketClosed=Mediated portfolio.closed.canceled=Canceled portfolio.failed.Failed=Failed portfolio.failed.unfail=Before proceeding, make sure you have a backup of your data directory!\n\ Do you want to move this trade back to open trades?\n\ This is a way to unlock funds stuck in a failed trade. portfolio.failed.cantUnfail=This trade cannot be moved back to open trades at the moment. \n\ Try again after completion of trade(s) {0} portfolio.failed.depositTxNull=The trade cannot be reverted to a open trade. Deposit transaction is null. portfolio.failed.delayedPayoutTxNull=The trade cannot be reverted to a open trade. Delayed payout transaction is null. portfolio.failed.penalty.msg=This will charge the {0}/{1} a penalty fee of {2} and return the remaining trade funds to their wallet. Are you sure you want to send?\n\n\ Other Info:\n\ Transaction Fee: {3}\n\ Reserve Tx Hash: {4} portfolio.failed.error.msg=Trade record does not exist. #################################################################### # Funds #################################################################### funds.tab.deposit=Receive funds funds.tab.withdrawal=Send funds funds.tab.reserved=Reserved funds funds.tab.locked=Locked funds funds.tab.transactions=Transactions funds.deposit.unused=Unused funds.deposit.usedInTx=Used in {0} transaction(s) funds.deposit.baseAddress=Base address funds.deposit.offerFunding=Reserved for offer funding ({0}) funds.deposit.tradePayout=Reserved for trade payout ({0}) funds.deposit.fundHavenoWallet=Fund Haveno wallet funds.deposit.noAddresses=No deposit addresses have been generated yet funds.deposit.fundWallet=Fund your wallet funds.deposit.withdrawFromWallet=Send funds from wallet funds.deposit.amount=Amount in XMR (optional) funds.deposit.generateAddress=Generate new address funds.deposit.generateAddressSegwit=Native segwit format (Bech32) funds.deposit.selectUnused=Please select an unused address from the table above rather than generating a new one. funds.withdrawal.arbitrationFee=Arbitration fee funds.withdrawal.inputs=Inputs selection funds.withdrawal.useAllInputs=Use all available inputs funds.withdrawal.useCustomInputs=Use custom inputs funds.withdrawal.receiverAmount=Receiver's amount funds.withdrawal.sendMax=Send max available funds.withdrawal.senderAmount=Sender's amount funds.withdrawal.feeExcluded=Amount excludes mining fee funds.withdrawal.feeIncluded=Amount includes mining fee funds.withdrawal.fromLabel=Withdraw from address funds.withdrawal.toLabel=Withdraw to address funds.withdrawal.maximum=MAX funds.withdrawal.memoLabel=Withdrawal memo funds.withdrawal.memo=Optionally fill memo funds.withdrawal.withdrawButton=Withdraw selected funds.withdrawal.noFundsAvailable=No funds are available for withdrawal funds.withdrawal.confirmWithdrawalRequest=Confirm withdrawal request funds.withdrawal.withdrawMultipleAddresses=Withdraw from multiple addresses ({0}) funds.withdrawal.withdrawMultipleAddresses.tooltip=Withdraw from multiple addresses:\n{0} funds.withdrawal.notEnoughFunds=You don't have enough funds in your wallet. funds.withdrawal.selectAddress=Select a source address from the table funds.withdrawal.setAmount=Set the amount to withdraw funds.withdrawal.fillDestAddress=Fill in your destination address funds.withdrawal.warn.noSourceAddressSelected=You need to select a source address in the table above. funds.withdrawal.warn.amountExceeds=You don't have sufficient funds available from the selected address.\n\ Consider to select multiple addresses in the table above or change the fee toggle to include the miner fee. funds.withdrawal.warn.amountMissing=Enter an amount to withdraw. funds.withdrawal.txFee=Withdrawal transaction fee (satoshis/vbyte) funds.withdrawal.useCustomFeeValueInfo=Insert a custom transaction fee value funds.withdrawal.useCustomFeeValue=Use custom value funds.withdrawal.txFeeMin=Transaction fee must be at least {0} satoshis/vbyte funds.withdrawal.txFeeTooLarge=Your input is above any reasonable value (>5000 satoshis/vbyte). Transaction fee is usually in the range of 50-400 satoshis/vbyte. funds.reserved.noFunds=No funds are reserved in open offers funds.reserved.reserved=Reserved in local wallet for offer with ID: {0} funds.locked.noFunds=No funds are locked in trades funds.locked.locked=Locked in multisig for trade with ID: {0} funds.tx.direction.sentTo=Sent to: funds.tx.direction.receivedWith=Received with: funds.tx.direction.genesisTx=From Genesis tx: funds.tx.createOfferFee=Maker and tx fee: {0} funds.tx.takeOfferFee=Taker and tx fee: {0} funds.tx.multiSigDeposit=Multisig deposit: {0} funds.tx.multiSigPayout=Multisig payout: {0} funds.tx.disputePayout=Dispute payout: {0} funds.tx.disputeLost=Lost dispute case: {0} funds.tx.collateralForRefund=Refund collateral: {0} funds.tx.timeLockedPayoutTx=Time locked payout tx: {0} funds.tx.refund=Refund from arbitration: {0} funds.tx.makerTradeFee=Maker fee: {0} funds.tx.takerTradeFee=Taker fee: {0} funds.tx.unknown=Unknown reason: {0} funds.tx.noFundsFromDispute=No refund from dispute funds.tx.receivedFunds=Received funds funds.tx.withdrawnFromWallet=Withdrawn from wallet funds.tx.memo=Memo funds.tx.noTxAvailable=No transactions available funds.tx.revert=Revert funds.tx.txSent=Transaction successfully sent to a new address in the local Haveno wallet. funds.tx.direction.self=Sent to yourself funds.tx.dustAttackTx=Received dust funds.tx.dustAttackTx.popup=This transaction is sending a very small XMR amount to your wallet and might be an attempt \ from chain analysis companies to spy on your wallet.\n\n\ If you use that transaction output in a spending transaction they will learn that you are likely the owner of the \ other address as well (coin merge).\n\n\ To protect your privacy the Haveno wallet ignores such dust outputs for spending purposes and in the balance display. \ You can set the threshold amount when an output is considered dust in the settings. #################################################################### # Support #################################################################### support.tab.mediation.support=Mediation support.tab.refund.support=Refund support.tab.arbitration.support=Arbitration support.tab.legacyArbitration.support=Legacy Arbitration support.tab.ArbitratorsSupportTickets={0}'s tickets support.tab.SignedOffers=Signed Offers support.prompt.signedOffer.penalty.msg=This will charge the maker a penalty fee and return the remaining trade funds to their wallet. Are you sure you want to send?\n\n\ Offer ID: {0}\n\ Maker Penalty Fee: {1}\n\ Reserve Tx Miner Fee: {2}\n\ Reserve Tx Hash: {3}\n\ Reserve Tx Key Images: {4}\n\ support.contextmenu.penalize.msg=Penalize {0} by publishing reserve tx support.prompt.signedOffer.error.msg=Signed Offer record does not exist; contact administrator. support.info.submitTxHex=Reserve transaction has been published with the following result:\n support.result.success=Transaction hex has been successfully submitted. support.sigCheck.button=Check signature support.sigCheck.popup.info=Paste the summary message of the arbitration process. With this tool, any user can check if the signature of the arbitrator matches the summary message. support.sigCheck.popup.header=Verify dispute result signature support.sigCheck.popup.msg.label=Summary message support.sigCheck.popup.msg.prompt=Copy & paste summary message from dispute support.sigCheck.popup.result=Validation result support.sigCheck.popup.success=Signature is valid support.sigCheck.popup.failed=Signature verification failed support.sigCheck.popup.invalidFormat=Message is not of expected format. Copy & paste summary message from dispute. support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? support.reOpenByTrader.failed=Failed to re-open the dispute. support.reOpenButton.label=Re-open support.sendNotificationButton.label=Private notification support.reportButton.label=Report support.fullReportButton.label=All disputes support.noTickets=There are no open tickets support.sendingMessage=Sending Message... support.receiverNotOnline=Receiver is not online. Message is saved to their mailbox. support.sendMessageError=Sending message failed. Error: {0} support.receiverNotKnown=Receiver not known support.wrongVersion=The offer in that dispute has been created with an older version of Haveno.\n\ You cannot close that dispute with your version of the application.\n\n\ Please use an older version with protocol version {0} support.openFile=Open file to attach (max. file size: {0} kb) support.attachmentTooLarge=The total size of your attachments is {0} kb and is exceeding the max. allowed message size of {1} kB. support.maxSize=The max. allowed file size is {0} kB. support.attachment=Attachment support.tooManyAttachments=You cannot send more than 3 attachments in one message. support.save=Save file to disk support.messages=Messages support.input.prompt=Enter message... support.send=Send support.addAttachments=Add attachments support.closeTicket=Close ticket support.attachments=Attachments: support.savedInMailbox=Message saved in receiver's mailbox support.arrived=Message arrived at receiver support.transient=Message is on its way to receiver support.acknowledged=Message arrival confirmed by receiver support.error=Receiver could not process message. Error: {0} support.errorTimeout=timed out. Try sending message again. support.buyerAddress=XMR buyer address support.sellerAddress=XMR seller address support.role=Role support.agent=Support agent support.state=State support.chat=Chat support.preparing=Preparing support.requested=Requested support.closed=Closed support.open=Open support.moreButton=MORE... support.sendLogFiles=Send Log Files support.uploadTraderChat=Upload Trader Chat support.process=Process support.buyerMaker=XMR Buyer/Maker support.sellerMaker=XMR Seller/Maker support.buyerTaker=XMR Buyer/Taker support.sellerTaker=XMR Seller/Taker support.sendLogs.title=Send Log Files support.sendLogs.backgroundInfo=When you experience a bug, arbitrators and support staff will often request copies of the your log files to diagnose the issue.\n\n\ Upon pressing 'Send', your log files will be compressed and transmitted directly to the arbitrator. support.sendLogs.step1=Create Zip Archive of Log Files support.sendLogs.step2=Connection Request to Arbitrator support.sendLogs.step3=Upload Archived Log Data support.sendLogs.send=Send support.sendLogs.cancel=Cancel support.sendLogs.init=Initializing support.sendLogs.retry=Retrying send support.sendLogs.stopped=Transfer stopped support.sendLogs.progress=Transfer progress: %.0f%% support.sendLogs.finished=Transfer complete! support.sendLogs.command=Press 'Send' to retry, or 'Stop' to abort support.txKeyImages=Key Images support.txHash=Transaction Hash support.txHex=Transaction Hex support.signature=Signature support.maker.penalty.fee=Maker Penalty Fee support.tx.miner.fee=Miner Fee support.backgroundInfo=Haveno is not a company, so it handles disputes differently.\n\n\ Traders can communicate within the application via secure chat on the open trades screen to try solving disputes on their own. \ If that is not sufficient, an arbitrator will evaluate the situation and decide a \ payout of trade funds. support.initialInfo=Please enter a description of your problem in the text field below. \ Add as much information as possible to speed up dispute resolution time.\n\n\ Here is a check list for information you should provide:\n\ \t● If you are the XMR buyer: Did you make the Traditional or Cryptocurrency transfer? If so, did you click the 'payment started' \ button in the application?\n\ \t● If you are the XMR seller: Did you receive the Traditional or Cryptocurrency payment? If so, did you click the 'payment received' \ button in the application?\n\ \t● Which version of Haveno are you using?\n\ \t● Which operating system are you using?\n\ \t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\ \t Sometimes the data directory gets corrupted and leads to strange bugs. \n\ \t See: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\n\ Please make yourself familiar with the basic rules for the dispute process:\n\ \t● You need to respond to the {0}'s requests within 2 days.\n\ \t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\ \t● The maximum period for a dispute is 14 days.\n\ \t● You need to cooperate with the {1} and provide the information they request to make your case.\n\ \t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\n\ You can read more about the dispute process at: {2} support.systemMsg=System message: {0} support.youOpenedTicket=You opened a request for support.\n\n{0}\n\nHaveno version: {1} support.youOpenedDispute=You opened a request for a dispute.\n\n{0}\n\nHaveno version: {1} support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nHaveno version: {1} support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nHaveno version: {1} support.mediatorsDisputeSummary=System message: Mediator's dispute summary:\n{0} support.mediatorReceivedLogs=System message: Mediator has received logs: {0} support.mediatorsAddress=Mediator's node address: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. \ It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. \ Please inform the developers about that incident and do not close that case before the situation is resolved!\n\n\ Address used in the dispute: {0}\n\n\ All DAO param donation addresses: {1}\n\n\ Trade ID: {2}\ {3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. support.info.disputeReOpened=Dispute ticket has been re-opened. #################################################################### # Settings #################################################################### settings.tab.preferences=Preferences settings.tab.network=Network info settings.tab.about=About setting.preferences.general=General preferences setting.preferences.explorer=Monero Explorer setting.preferences.deviation=Max. deviation from market price setting.preferences.avoidStandbyMode=Avoid standby mode setting.preferences.useSoundForNotifications=Play sounds for notifications setting.preferences.autoConfirmXMR=XMR auto-confirm setting.preferences.autoConfirmEnabled=Enabled setting.preferences.autoConfirmRequiredConfirmations=Required confirmations setting.preferences.autoConfirmMaxTradeSize=Max. trade amount (XMR) setting.preferences.autoConfirmServiceAddresses=Monero Explorer URLs (uses Tor, except for localhost, LAN IP addresses, and *.local hostnames) setting.preferences.deviationToLarge=Values higher than {0}% are not allowed. setting.preferences.txFee=BSQ Withdrawal transaction fee (satoshis/vbyte) setting.preferences.useCustomValue=Use custom value setting.preferences.ignorePeers=Ignored peers [onion address:port] setting.preferences.ignoreDustThreshold=Min. non-dust output value setting.preferences.currenciesInList=Currencies in market price feed list setting.preferences.prefCurrency=Preferred currency setting.preferences.displayTraditional=Display traditional currencies setting.preferences.noTraditional=There are no traditional currencies selected setting.preferences.cannotRemovePrefCurrency=You cannot remove your selected preferred display currency setting.preferences.displayCryptos=Display cryptos setting.preferences.noCryptos=There are no cryptos selected setting.preferences.addTraditional=Add traditional currency setting.preferences.addCrypto=Add cryptocurrency setting.preferences.displayOptions=Display options setting.preferences.showOwnOffers=Show my own offers in offer book setting.preferences.useAnimations=Use animations setting.preferences.useDarkMode=Use dark mode setting.preferences.useLightMode=Use light mode setting.preferences.sortWithNumOffers=Sort market lists with no. of offers/trades setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API setting.preferences.notifyOnPreRelease=Receive pre-release notifications setting.preferences.resetAllFlags=Reset all \"Don't show again\" flags settings.preferences.languageChange=To apply the language change to all screens requires a restart. settings.preferences.supportLanguageWarning=In case of a dispute, please note that arbitration is handled in {0}. setting.preferences.clearDataAfterDays=Clear sensitive data after (days) settings.preferences.editCustomExplorer.headline=Explorer Settings settings.preferences.editCustomExplorer.description=Choose a system defined explorer from the list on the left, and/or \ customize to suit your own preferences. settings.preferences.editCustomExplorer.available=Available explorers settings.preferences.editCustomExplorer.chosen=Chosen explorer settings settings.preferences.editCustomExplorer.name=Name settings.preferences.editCustomExplorer.txUrl=Transaction URL settings.preferences.editCustomExplorer.addressUrl=Address URL setting.info.headline=New data-privacy feature settings.preferences.sensitiveDataRemoval.msg=To protect the privacy of yourself and other traders, Haveno intends to \ remove sensitive data from old trades. This is particularly important for fiat trades which may include bank \ account details.\n\n\ The threshold for data removal can be configured on this screen via the field "Clear sensitive data after (days)". \ It is recommended to set it as low as possible, for example 60 days. That means trades from more than 60 \ days ago will have sensitive data cleared, as long as they are completed. Completed trades are found in the \ Portfolio / History tab. settings.net.xmrHeader=Monero network settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=My onion address settings.net.xmrNodesLabel=Use custom Monero nodes settings.net.moneroPeersLabel=Connected peers settings.net.connection=Connection settings.net.connected=Connected settings.net.useTorForXmrJLabel=Use Tor for Monero network settings.net.useTorForXmrAfterSyncRadio=After wallet is synchronized settings.net.useTorForXmrOffRadio=Never settings.net.useTorForXmrOnRadio=Always settings.net.moneroNodesLabel=Monero nodes to connect to settings.net.useProvidedNodesRadio=Use provided Monero nodes settings.net.usePublicNodesRadio=Use public Monero network settings.net.useCustomNodesRadio=Use custom Monero nodes settings.net.warn.usePublicNodes=If you use public Monero nodes, you are subject to any risk of using untrusted remote nodes.\n\nPlease read more details at [HYPERLINK:https://www.getmonero.org/resources/moneropedia/remote-node.html].\n\nAre you sure you want to use public nodes? settings.net.warn.usePublicNodes.useProvided=No, use provided nodes settings.net.warn.usePublicNodes.usePublic=Yes, use public network settings.net.warn.useCustomNodes.B2XWarning=Please be sure that your Monero node is a trusted Monero node!\n\n\ Connecting to nodes which do not follow the Monero consensus rules could corrupt your wallet and cause problems in the trade process.\n\n\ Users who connect to nodes that violate consensus rules are responsible for any resulting damage. \ Any resulting disputes will be decided in favor of the other peer. No technical support will be given \ to users who ignore this warning and protection mechanisms! settings.net.warn.invalidXmrConfig=Connection to the Monero network failed because your configuration is invalid.\n\nYour configuration has been reset to use the provided Monero nodes instead. You will need to restart the application. settings.net.localhostXmrNodeInfo=Background information: Haveno looks for a local Monero node when starting. If it is found, Haveno will communicate with the Monero network exclusively through it. settings.net.p2PPeersLabel=Connected peers settings.net.onionAddressColumn=Onion address settings.net.creationDateColumn=Established settings.net.connectionTypeColumn=In/Out settings.net.sentDataLabel=Sent data statistics settings.net.receivedDataLabel=Received data statistics settings.net.chainHeightLabel=Latest XMR block height settings.net.roundTripTimeColumn=Roundtrip settings.net.sentBytesColumn=Sent settings.net.receivedBytesColumn=Received settings.net.peerTypeColumn=Peer type settings.net.openTorSettingsButton=Open Tor settings settings.net.versionColumn=Version settings.net.subVersionColumn=Subversion settings.net.heightColumn=Height settings.net.needRestart=You need to restart the application to apply that change.\nDo you want to do that now? settings.net.notKnownYet=Not known yet... settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec settings.net.chainHeight=Monero Peers chain height: {0} settings.net.ips=[IP address:port | host name:port | onion address:port] (comma separated). Port can be omitted if default is used ({0}). settings.net.seedNode=Seed node settings.net.directPeer=Peer (direct) settings.net.initialDataExchange={0} [Bootstrapping] settings.net.peer=Peer settings.net.inbound=inbound settings.net.outbound=outbound settings.net.rescanOutputsLabel=Rescan Outputs settings.net.rescanOutputsButton=Rescan Wallet Outputs settings.net.rescanOutputsSuccess=Are you sure you want to rescan your wallet outputs? settings.net.rescanOutputsFailed=Could not rescan wallet outputs.\nError: {0} setting.about.aboutHaveno=About Haveno setting.about.about=Haveno is open-source software which facilitates the exchange of monero with national currencies (and other cryptocurrencies) through a decentralized peer-to-peer network in a way that strongly protects user privacy. Learn more about Haveno on our project web page. setting.about.web=Haveno web page setting.about.code=Source code setting.about.agpl=AGPL License setting.about.support=Support Haveno setting.about.def=Haveno is not a company—it is a project open to the community. If you want to support Haveno please follow the links below. setting.about.contribute=Contribute setting.about.providers=Data providers setting.about.apisWithFee=Haveno uses price indices for Traditional and Cryptocurrency market prices setting.about.apis=Haveno uses Price Indices for Traditional and Cryptocurrency market prices. setting.about.pricesProvided=Market prices provided by setting.about.feeEstimation.label=Mining fee estimation provided by setting.about.versionDetails=Version details setting.about.version=Application version setting.about.subsystems.label=Versions of subsystems setting.about.subsystems.val=Network version: {0}; P2P message version: {1}; Local DB version: {2}; Trade protocol version: {3} setting.about.shortcuts=Short cuts setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' or 'alt + {0}' or 'cmd + {0}' setting.about.shortcuts.menuNav=Navigate main menu setting.about.shortcuts.menuNav.value=To navigate the main menu press: 'Ctrl' or 'alt' or 'cmd' with a numeric key between '1-9' setting.about.shortcuts.close=Close Haveno setting.about.shortcuts.close.value='Ctrl + {0}' or 'cmd + {0}' or 'Ctrl + {1}' or 'cmd + {1}' setting.about.shortcuts.closePopup=Close popup or dialog window setting.about.shortcuts.closePopup.value='ESCAPE' key setting.about.shortcuts.chatSendMsg=Send trader chat message setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' or 'alt + ENTER' or 'cmd + ENTER' setting.about.shortcuts.openDispute=Open dispute setting.about.shortcuts.openDispute.value=Select pending trade and click: {0} setting.about.shortcuts.walletDetails=Open wallet details window setting.about.shortcuts.openEmergencyXmrWalletTool=Open emergency wallet tool for XMR wallet setting.about.shortcuts.showTorLogs=Toggle log level for Tor messages between DEBUG and WARN setting.about.shortcuts.manualPayoutTxWindow=Open window for manual payout from 2of2 Multisig deposit tx setting.about.shortcuts.removeStuckTrade=Open popup to move failed trade to open trades tab again setting.about.shortcuts.removeStuckTrade.value=Select failed trade and press: {0} setting.about.shortcuts.registerArbitrator=Register arbitrator (mediator/arbitrator only) setting.about.shortcuts.registerArbitrator.value=Navigate to account and press: {0} setting.about.shortcuts.registerMediator=Register mediator (mediator/arbitrator only) setting.about.shortcuts.registerMediator.value=Navigate to account and press: {0} setting.about.shortcuts.openSignPaymentAccountsWindow=Open window for account age signing (legacy arbitrators only) setting.about.shortcuts.openSignPaymentAccountsWindow.value=Navigate to legacy arbitrator view and press: {0} setting.about.shortcuts.sendAlertMsg=Send alert or update message (privileged activity) setting.about.shortcuts.sendFilter=Set Filter (privileged activity) setting.about.shortcuts.sendPrivateNotification=Send private notification to peer (privileged activity) setting.about.shortcuts.sendPrivateNotification.value=Open peer info at avatar and press: {0} #################################################################### # Account #################################################################### account.tab.arbitratorRegistration=Arbitrator registration account.tab.mediatorRegistration=Mediator registration account.tab.refundAgentRegistration=Refund agent registration account.tab.signing=Signing account.menu.paymentAccount=Traditional currency accounts account.menu.altCoinsAccountView=Cryptocurrency accounts account.menu.password=Wallet password account.menu.seedWords=Wallet seed account.menu.walletInfo=Wallet info account.menu.backup=Backup account.menu.notifications=Notifications account.menu.walletInfo.balance.headLine=Wallet balances account.menu.walletInfo.balance.info=This shows the internal wallet balance including unconfirmed transactions.\n\ For XMR, the internal wallet balance shown below should match the sum of the 'Available' and 'Reserved' balances shown in the top right of this window. account.menu.walletInfo.xpub.headLine=Watch keys (xpub keys) account.menu.walletInfo.walletSelector={0} {1} wallet account.menu.walletInfo.path.headLine=HD keychain paths account.menu.walletInfo.path.info=If you import seed words into another wallet (like Electrum), you'll need to define the \ path. This should only be done in emergency cases when you lose access to the Haveno wallet and data directory.\n\ Keep in mind that spending funds from a non-Haveno wallet can bungle the internal Haveno data structures associated with the wallet \ data, which can lead to failed trades.\n\n\ NEVER send BSQ from a non-Haveno wallet, as it will probably lead to an invalid BSQ transaction and losing your BSQ. account.menu.walletInfo.openDetails=Show raw wallet details and private keys ## TODO should we rename the following to a gereric name? account.arbitratorRegistration.pubKey=Public key account.arbitratorRegistration.register=Register account.arbitratorRegistration.registration={0} registration account.arbitratorRegistration.revoke=Revoke account.arbitratorRegistration.info.msg=Please note that you need to stay available for 15 days after revoking as there might be trades which are using you as {0}. The max. allowed trade period is 8 days and the dispute process might take up to 7 days. account.arbitratorRegistration.warn.min1Language=You need to set at least 1 language.\nWe added the default language for you. account.arbitratorRegistration.removedSuccess=You have successfully removed your registration from the Haveno network. account.arbitratorRegistration.removedFailed=Could not remove registration.{0} account.arbitratorRegistration.registerSuccess=You have successfully registered to the Haveno network. account.arbitratorRegistration.registerFailed=Could not complete registration.{0} account.crypto.yourCryptoAccounts=Your cryptocurrency accounts account.crypto.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as \ described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don't control your keys or \ (b) which don't use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is \ not a {2} specialist and cannot help in such cases. account.crypto.popup.wallet.confirm=I understand and confirm that I know which wallet I need to use. # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=Trading UPX on Haveno requires that you understand and fulfill \ the following requirements:\n\n\ For sending UPX, you need to use either the official uPlexa GUI wallet or uPlexa CLI wallet with the \ store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as \ that would be required in case of a dispute.\n\ uplexa-wallet-cli (use the command get_tx_key)\n\ uplexa-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\n\ At normal block explorers the transfer is not verifiable.\n\n\ You need to provide the arbitrator the following data in case of a dispute:\n\ - The tx private key\n\ - The transaction hash\n\ - The recipient's public address\n\n\ Failure to provide the above data, or if you used an incompatible wallet, will result in losing the \ dispute case. The UPX sender is responsible for providing verification of the UPX transfer to the \ arbitrator in case of a dispute.\n\n\ There is no payment ID required, just the normal public address.\n\ If you are not sure about that process visit uPlexa discord channel (https://discord.gg/vhdNSrV) \ or the uPlexa Telegram Chat (https://t.me/uplexaOfficial) to find more information. # suppress inspection "UnusedProperty" account.crypto.popup.arq.msg=Trading ARQ on Haveno requires that you understand and fulfill \ the following requirements:\n\n\ For sending ARQ, you need to use either the official ArQmA GUI wallet or ArQmA CLI wallet with the \ store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as \ that would be required in case of a dispute.\n\ arqma-wallet-cli (use the command get_tx_key)\n\ arqma-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\n\ At normal block explorers the transfer is not verifiable.\n\n\ You need to provide the mediator or arbitrator the following data in case of a dispute:\n\ - The tx private key\n\ - The transaction hash\n\ - The recipient's public address\n\n\ Failure to provide the above data, or if you used an incompatible wallet, will result in losing the \ dispute case. The ARQ sender is responsible for providing verification of the ARQ transfer to the \ mediator or arbitrator in case of a dispute.\n\n\ There is no payment ID required, just the normal public address.\n\ If you are not sure about that process visit ArQmA discord channel (https://discord.gg/s9BQpJT) \ or the ArQmA forum (https://labs.arqma.com) to find more information. # suppress inspection "UnusedProperty" account.crypto.popup.xmr.msg=Trading XMR on Haveno requires that you understand the following requirement.\n\n\ If selling XMR, you must be able to provide the following information to a mediator or arbitrator in case of a dispute:\n\ - the transaction key (Tx Key, Tx Secret Key or Tx Private Key)\n\ - the transaction ID (Tx ID or Tx Hash)\n\ - the destination address (recipient's address)\n\n\ See the wiki for details on where to find this information on popular Monero wallets [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Proving_payments].\n\ Failure to provide the required transaction data will result in losing disputes.\n\n\ Also note that Haveno now offers automatic confirming for XMR transactions to make trades quicker, \ but you need to enable it in Settings.\n\n\ See the wiki for more information about the auto-confirm feature: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. # suppress inspection "UnusedProperty" account.crypto.popup.msr.msg=Trading MSR on Haveno requires that you understand and fulfill \ the following requirements:\n\n\ For sending MSR, you need to use either the official Masari GUI wallet, Masari CLI wallet with the \ store-tx-info flag enabled (enabled by default) or the Masari web wallet (https://wallet.getmasari.org). Please be sure you can access the tx key as \ that would be required in case of a dispute.\n\ masari-wallet-cli (use the command get_tx_key)\n\ masari-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\n\ Masari Web Wallet (goto Account -> transaction history and view details on your sent transaction)\n\n\ Verification can be accomplished in-wallet.\n\ masari-wallet-cli : using command (check_tx_key).\n\ masari-wallet-gui : on the Advanced > Prove/Check page.\n\ Verification can be accomplished in the block explorer \n\ Open block explorer (https://explorer.getmasari.org), use the search bar to find your transaction hash.\n\ Once transaction is found, scroll to bottom to the 'Prove Sending' area and fill in details as needed.\n\ You need to provide the mediator or arbitrator the following data in case of a dispute:\n\ - The tx private key\n\ - The transaction hash\n\ - The recipient's public address\n\n\ Failure to provide the above data, or if you used an incompatible wallet, will result in losing the \ dispute case. The MSR sender is responsible for providing verification of the MSR transfer to the \ mediator or arbitrator in case of a dispute.\n\n\ There is no payment ID required, just the normal public address.\n\ If you are not sure about that process, ask for help on the Official Masari Discord (https://discord.gg/sMCwMqs). # suppress inspection "UnusedProperty" account.crypto.popup.blur.msg=Trading BLUR on Haveno requires that you understand and fulfill \ the following requirements:\n\n\ To send BLUR you must use the Blur Network CLI or GUI Wallet. \n\n\ If you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save \ this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the \ transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\n\ If you are using the Blur Network GUI Wallet, the transaction private key and transaction ID can be found conveniently \ in the "History" tab. Immediately after sending, locate the transaction of interest. Click the "?" symbol in the \ lower-right corner of the box containing the transaction. You must save this information. \n\n\ In the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, \ 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the BLUR \ transfer using the Blur Transaction Viewer (https://blur.cash/#tx-viewer).\n\n\ Failure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the \ BLUR sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\n\ If you do not understand these requirements, do not trade on Haveno. First, seek help at the Blur Network Discord (https://discord.gg/dMWaqVW). # suppress inspection "UnusedProperty" account.crypto.popup.solo.msg=Trading Solo on Haveno requires that you understand and fulfill \ the following requirements:\n\n\ To send Solo you must use the Solo Network CLI Wallet. \n\n\ If you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save \ this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the \ transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\n\ In the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, \ 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the Solo \ transfer using the Solo Block Explorer by searching for the transaction and then using the "Prove sending" function (https://explorer.minesolo.com/).\n\n\ failure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the \ Solo sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\n\ If you do not understand these requirements, do not trade on Haveno. First, seek help at the Solo Network Discord (https://discord.minesolo.com/). # suppress inspection "UnusedProperty" account.crypto.popup.cash2.msg=Trading CASH2 on Haveno requires that you understand and fulfill \ the following requirements:\n\n\ To send CASH2 you must use the Cash2 Wallet version 3 or higher. \n\n\ After a transaction is sent, the transaction ID will be displayed. You must save this information. \ Immediately after sending the transaction, you must use the command 'getTxKey' in simplewallet to retrieve the \ transaction secret key. \n\n\ In the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, \ 2) the transaction secret key, and 3) the recipient's Cash2 address. The mediator or arbitrator will then verify the CASH2 \ transfer using the Cash2 Block Explorer (https://blocks.cash2.org).\n\n\ Failure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the \ CASH2 sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\n\ If you do not understand these requirements, do not trade on Haveno. First, seek help at the Cash2 Discord (https://discord.gg/FGfXAYN). # suppress inspection "UnusedProperty" account.crypto.popup.qwertycoin.msg=Trading Qwertycoin on Haveno requires that you understand and fulfill \ the following requirements:\n\n\ To send QWC you must use the official QWC Wallet version 5.1.3 or higher. \n\n\ After a transaction is sent, the transaction ID will be displayed. You must save this information. \ Immediately after sending the transaction, you must use the command 'get_Tx_Key' in simplewallet to retrieve the \ transaction secret key. \n\n\ In the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, \ 2) the transaction secret key, and 3) the recipient's QWC address. The mediator or arbitrator will then verify the QWC \ transfer using the QWC Block Explorer (https://explorer.qwertycoin.org).\n\n\ Failure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the \ QWC sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\n\ If you do not understand these requirements, do not trade on Haveno. First, seek help at the QWC Discord (https://discord.gg/rUkfnpC). # suppress inspection "UnusedProperty" account.crypto.popup.drgl.msg=Trading Dragonglass on Haveno requires that you understand and fulfill \ the following requirements:\n\n\ Because of the privacy Dragonglass provides, a transaction is not verifiable on the public blockchain. If required, you \ can prove your payment through the use of your TXN-Private-Key.\n\ The TXN-Private Key is a one-time key automatically generated for every transaction that can \ only be accessed from within your DRGL wallet.\n\ Either by DRGL-wallet GUI (inside transaction details dialog) or by the Dragonglass CLI simplewallet (using command "get_tx_key").\n\n\ DRGL version 'Oathkeeper' and higher are REQUIRED for both.\n\n\ In case of a dispute, you must provide the mediator or arbitrator the following data:\n\ - The TXN-Private key\n\ - The transaction hash\n\ - The recipient's public address\n\n\ Verification of payment can be made using the above data as inputs at (http://drgl.info/#check_txn).\n\n\ Failure to provide the above data, or if you used an incompatible wallet, will result in losing the \ dispute case. The Dragonglass sender is responsible for providing verification of the DRGL transfer to the \ mediator or arbitrator in case of a dispute. Use of PaymentID is not required.\n\n\ If you are unsure about any part of this process, visit Dragonglass on Discord (http://discord.drgl.info) for help. # suppress inspection "UnusedProperty" account.crypto.popup.ZEC.msg=When using Zcash you can only use the transparent addresses (starting with t), not \ the z-addresses (private), because the mediator or arbitrator would not be able to verify the transaction with z-addresses. # suppress inspection "UnusedProperty" account.crypto.popup.XZC.msg=When using Zcoin you can only use the transparent (traceable) addresses, not \ the untraceable addresses, because the mediator or arbitrator would not be able to verify the transaction with untraceable addresses at a block explorer. # suppress inspection "UnusedProperty" account.crypto.popup.grin.msg=GRIN requires an interactive process between the sender and receiver to create the \ transaction. Be sure to follow the instructions from the GRIN project web page to reliably send and receive GRIN \ (the receiver needs to be online or at least be online during a certain time frame). \n\n\ Haveno supports only the Grinbox (Wallet713) wallet URL format. \n\n\ The GRIN sender is required to provide proof that they have sent GRIN successfully. If the wallet cannot provide that proof, a \ potential dispute will be resolved in favor of the GRIN receiver. Please be sure that you use the \ latest Grinbox software which supports the transaction proof and that you understand the process of transferring and \ receiving GRIN as well as how to create the proof. \n\n\ See https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only for more \ information about the Grinbox proof tool. # suppress inspection "UnusedProperty" account.crypto.popup.beam.msg=BEAM requires an interactive process between the sender and receiver to create the \ transaction. \n\n\ Be sure to follow the instructions from the BEAM project web page to reliably send and receive BEAM \ (the receiver needs to be online or at least be online during a certain time frame). \n\n\ The BEAM sender is required to provide proof that they sent BEAM successfully. \ Be sure to use wallet software which can produce such a proof. If the wallet cannot provide the proof a potential \ dispute will be resolved in favor of the BEAM receiver. # suppress inspection "UnusedProperty" account.crypto.popup.pars.msg=Trading ParsiCoin on Haveno requires that you understand and fulfill \ the following requirements:\n\n\ To send PARS you must use the official ParsiCoin Wallet version 3.0.0 or higher. \n\n\ You can Check your Transaction Hash and Transaction Key on Transactions Section on your GUI Wallet (ParsiPay) \ You need to right Click on the Transaction and then click on show details. \n\n\ In the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the Transaction Hash, \ 2) the Transaction Key, and 3) the recipient's PARS address. The mediator or arbitrator will then verify the PARS \ transfer using the ParsiCoin Block Explorer (http://explorer.parsicoin.net/#check_payment).\n\n\ Failure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the \ ParsiCoin sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\n\ If you do not understand these requirements, do not trade on Haveno. First, seek help at the ParsiCoin Discord (https://discord.gg/c7qmFNh). # suppress inspection "UnusedProperty" account.crypto.popup.blk-burnt.msg=To trade burnt blackcoins, you need to know the following:\n\n\ Burnt blackcoins are unspendable. To trade them on Haveno, output scripts need to be in the form: \ OP_RETURN OP_PUSHDATA, followed by associated data bytes which, after being hex-encoded, constitute addresses. \ For example, burnt blackcoins with an address 666f6f (“foo” in UTF-8) will have the following script:\n\n\ OP_RETURN OP_PUSHDATA 666f6f\n\n\ To create burnt blackcoins, one may use the “burn” RPC command available in some wallets.\n\n\ For possible use cases, one may look at https://ibo.laboratorium.ee .\n\n\ As burnt blackcoins are unspendable, they can not be reselled. “Selling” burnt blackcoins means \ burning ordinary blackcoins (with associated data equal to the destination address).\n\n\ In case of a dispute, the BLK seller needs to provide the transaction hash. # suppress inspection "UnusedProperty" account.crypto.popup.liquidmonero.msg=Trading L-XMR on Haveno requires that you understand the following:\n\n\ When receiving L-XMR for a trade on Haveno, you cannot use the mobile Blockstream Green Wallet app or a \ custodial/exchange wallet. You must only receive L-XMR into the Liquid Elements Core wallet, or another \ L-XMR wallet which allows you to obtain the blinding key for your blinded L-XMR address.\n\n\ In the event mediation is necessary, or if a trade dispute arises, you must disclose the blinding key for \ your receiving L-XMR address to the Haveno mediator or refund agent so they can verify the details of \ your Confidential Transaction on their own Elements Core full node.\n\n\ Failure to provide the required information to the mediator or refund agent will result in losing the \ dispute case. In all cases of dispute, the L-XMR receiver bears 100% of the burden of responsibility in \ providing cryptographic proof to the mediator or refund agent.\n\n\ If you do not understand these requirements, do not trade L-XMR on Haveno. account.traditional.yourTraditionalAccounts=Your national currency accounts account.backup.title=Backup wallet account.backup.location=Backup location account.backup.selectLocation=Select backup location account.backup.backupNow=Backup now (backup is not encrypted!) account.backup.appDir=Application data directory account.backup.openDirectory=Open directory account.backup.openLogFile=Open Log file account.backup.success=Backup successfully saved at:\n{0} account.backup.directoryNotAccessible=The directory you have chosen is not accessible. {0} account.password.removePw.button=Remove password account.password.removePw.headline=Remove password protection for wallet account.password.setPw.button=Set password account.password.setPw.headline=Set password protection for wallet account.password.info=With password protection enabled, you'll need to enter your password at application startup, when withdrawing monero out of your wallet, and when displaying your seed words. account.seed.backup.title=Backup your wallet seed words account.seed.info=Please write down both the wallet seed words and the date. You can recover your wallet any time with the seed words and date.\n\nYou should write down the seed words on a sheet of paper. Do not save them on the computer.\n\nPlease note that the seed words are NOT a replacement for a backup.\nYou need to create a backup of the whole application directory from the \"Account/Backup\" screen to recover application state and data. account.seed.backup.warning=Please note that the seed words are NOT a replacement for a backup.\nYou need to create a backup of the whole application directory from the \"Account/Backup\" screen to recover application state and data. account.seed.warn.noPw.msg=You have not setup a wallet password which would protect the display of the seed words.\n\n\ Do you want to display the seed words? account.seed.warn.noPw.yes=Yes, and don't ask me again account.seed.enterPw=Enter password to view seed words account.seed.restore.info=Please make a backup before applying restore from seed words. Be aware that wallet restore is \ only for emergency cases and might cause problems with the internal wallet database.\n\ It is not a way for applying a backup! Please use a backup from the application data directory for restoring a \ previous application state.\n\n\ After restoring the application will shut down automatically. After you have restarted the application it will resync \ with the Monero network. This can take a while and can consume a lot of CPU, especially if the wallet was older and \ had many transactions. Please avoid interrupting that process, otherwise you might need to delete the SPV chain file \ again or repeat the restore process. account.seed.restore.ok=Ok, do the restore and shut down Haveno account.keys.clipboard.warning=Please note that wallet private keys are highly sensitive financial data.\n\n\ ● You should NOT divulge any of your keys to any individual who asks for them, unless you are absolutely certain that they are to be trusted handling your money! \n\n\ ● You should NOT copy private key data to the clipboard unless you are absolutely certain that you are running a secure computing environment that has no malware risks. \n\n\ Many people have lost their Monero this way. If you have ANY doubts, close this dialog immediately and seek assistance from someone knowledgeable. #################################################################### # Mobile notifications #################################################################### account.notifications.setup.title=Setup account.notifications.download.label=Download mobile app account.notifications.waitingForWebCam=Waiting for webcam... account.notifications.webCamWindow.headline=Scan QR-code from phone account.notifications.webcam.label=Use webcam account.notifications.webcam.button=Scan QR code account.notifications.noWebcam.button=I don't have a webcam account.notifications.erase.label=Clear notifications on phone account.notifications.erase.title=Clear notifications account.notifications.email.label=Pairing token account.notifications.email.prompt=Enter pairing token you received by email account.notifications.settings.title=Settings account.notifications.useSound.label=Play notification sound on phone account.notifications.trade.label=Receive trade messages account.notifications.market.label=Receive offer alerts account.notifications.price.label=Receive price alerts account.notifications.priceAlert.title=Price alerts account.notifications.priceAlert.high.label=Notify if XMR price is above account.notifications.priceAlert.low.label=Notify if XMR price is below account.notifications.priceAlert.setButton=Set price alert account.notifications.priceAlert.removeButton=Remove price alert account.notifications.trade.message.title=Trade state changed account.notifications.trade.message.msg.conf=The deposit transaction for the trade with ID {0} is confirmed. \ Please open your Haveno application and start the payment. account.notifications.trade.message.msg.started=The XMR buyer has started the payment for the trade with ID {0}. account.notifications.trade.message.msg.completed=The trade with ID {0} is completed. account.notifications.offer.message.title=Your offer was taken account.notifications.offer.message.msg=Your offer with ID {0} was taken account.notifications.dispute.message.title=New dispute message account.notifications.dispute.message.msg=You received a dispute message for trade with ID {0} account.notifications.marketAlert.title=Offer alerts account.notifications.marketAlert.selectPaymentAccount=Offers matching payment account account.notifications.marketAlert.offerType.label=Offer type I am interested in account.notifications.marketAlert.offerType.buy=Buy offers (I want to sell XMR) account.notifications.marketAlert.offerType.sell=Sell offers (I want to buy XMR) account.notifications.marketAlert.trigger=Offer price distance (%) account.notifications.marketAlert.trigger.info=With a price distance set, you will only receive an alert when an offer \ that meets (or exceeds) your requirements is published. Example: you want to sell XMR, but you will only sell at \ a 2% premium to the current market price. Setting this field to 2% will ensure you only receive alerts for offers \ with prices that are 2% (or more) above the current market price. account.notifications.marketAlert.trigger.prompt=Percentage distance from market price (e.g. 2.50%, -0.50%, etc) account.notifications.marketAlert.addButton=Add offer alert account.notifications.marketAlert.manageAlertsButton=Manage offer alerts account.notifications.marketAlert.manageAlerts.title=Manage offer alerts account.notifications.marketAlert.manageAlerts.header.paymentAccount=Payment account account.notifications.marketAlert.manageAlerts.header.trigger=Trigger price account.notifications.marketAlert.manageAlerts.header.offerType=Offer type account.notifications.marketAlert.message.title=Offer alert account.notifications.marketAlert.message.msg.below=below account.notifications.marketAlert.message.msg.above=above account.notifications.marketAlert.message.msg=A new '{0} {1}' offer with price {2} ({3} {4} market price) and \ payment method '{5}' was published to the Haveno offerbook.\n\ Offer ID: {6}. account.notifications.priceAlert.message.title=Price alert for {0} account.notifications.priceAlert.message.msg=Your price alert got triggered. The current {0} price is {1} {2} account.notifications.noWebCamFound.warning=No webcam found.\n\n\ Please use the email option to send the token and encryption key from your mobile phone to the Haveno application. account.notifications.priceAlert.warning.highPriceTooLow=The higher price must be larger than the lower price. account.notifications.priceAlert.warning.lowerPriceTooHigh=The lower price must be lower than the higher price. #################################################################### # Windows #################################################################### inputControlWindow.headline=Select inputs for transaction inputControlWindow.balanceLabel=Available balance contractWindow.title=Dispute details contractWindow.dates=Offer date / Trade date contractWindow.xmrAddresses=Monero address XMR buyer / XMR seller contractWindow.onions=Network address XMR buyer / XMR seller contractWindow.accountAge=Account age XMR buyer / XMR seller contractWindow.numDisputes=No. of disputes XMR buyer / XMR seller contractWindow.contractHash=Contract hash displayAlertMessageWindow.headline=Important information! displayAlertMessageWindow.update.headline=Important update information! displayAlertMessageWindow.update.download=Download: displayUpdateDownloadWindow.downloadedFiles=Files: displayUpdateDownloadWindow.downloadingFile=Downloading: {0} displayUpdateDownloadWindow.verifiedSigs=Signature verified with keys: displayUpdateDownloadWindow.status.downloading=Downloading files... displayUpdateDownloadWindow.status.verifying=Verifying signature... displayUpdateDownloadWindow.button.label=Download installer and verify signature displayUpdateDownloadWindow.button.downloadLater=Download later displayUpdateDownloadWindow.button.ignoreDownload=Ignore this version displayUpdateDownloadWindow.headline=A new Haveno update is available! displayUpdateDownloadWindow.download.failed.headline=Download failed displayUpdateDownloadWindow.download.failed=Download failed.\n\ Please download and verify manually at [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.installer.failed=Unable to determine the correct installer. Please download and verify manually at \ [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.verify.failed=Verification failed.\n\ Please download and verify manually at [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.success=The new version has been successfully downloaded and the signature verified.\n\n\ Please open the download directory, shut down the application and install the new version. displayUpdateDownloadWindow.download.openDir=Open download directory disputeSummaryWindow.title=Summary disputeSummaryWindow.openDate=Ticket opening date disputeSummaryWindow.role=Opener's role disputeSummaryWindow.payout=Trade amount payout disputeSummaryWindow.payout.getsTradeAmount=XMR {0} gets trade amount payout disputeSummaryWindow.payout.getsAll=Max. payout to XMR {0} disputeSummaryWindow.payout.custom=Custom payout disputeSummaryWindow.payoutAmount.buyer=Buyer's payout amount disputeSummaryWindow.payoutAmount.seller=Seller's payout amount disputeSummaryWindow.payoutAmount.invert=Use loser as publisher disputeSummaryWindow.reason=Reason of dispute disputeSummaryWindow.tradePeriodEnd=Trade period end disputeSummaryWindow.extraInfo=Extra information disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BUG=Bug # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.USABILITY=Usability # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Protocol violation # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.NO_REPLY=No reply # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SCAM=Scam # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OTHER=Other # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BANK_PROBLEMS=Bank # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OPTION_TRADE=Option trade # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Trader not responding # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled disputeSummaryWindow.summaryNotes=Summary notes disputeSummaryWindow.addSummaryNotes=Add summary notes disputeSummaryWindow.close.button=Close ticket # Do no change any line break or order of tokens as the structure is used for signature verification # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.msg=Ticket closed on {0}\n\ {1} node address: {2}\n\n\ Summary:\n\ Trade ID: {3}\n\ Currency: {4}\n\ Reason for dispute: {5}\n\ Trade amount: {6}\n\ Payout amount for XMR buyer: {7}\n\ Payout amount for XMR seller: {8}\n\n\ Summary notes:\n{9}\n # Do no change any line break or order of tokens as the structure is used for signature verification disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3} disputeSummaryWindow.close.nextStepsForMediation=\nNext steps:\n\ Open trade and accept or reject suggestion from mediator disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\n\ A dispute has been opened with the arbitrator. You can chat with the arbitrator in the "Support" tab to resolve the dispute. disputeSummaryWindow.close.closePeer=You need to close also the trading peers ticket! disputeSummaryWindow.close.txDetails.headline=Publish refund transaction # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.buyer=Buyer receives {0} on address: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=Seller receives {0} on address: {1}\n disputeSummaryWindow.close.txDetails=Spending: {0}\n\ {1}{2}\ Transaction fee: {3}\n\n\ Are you sure you want to publish this transaction? disputeSummaryWindow.close.noPayout.headline=Close without any payout disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? disputeSummaryWindow.close.alreadyPaid.headline=Payout already done disputeSummaryWindow.close.alreadyPaid.text=Restart the client to do another payout for this dispute emptyWalletWindow.headline={0} emergency wallet tool emptyWalletWindow.info=Please use that only in emergency case if you cannot access your fund from the UI.\n\n\ Please note that all open offers will be closed automatically when using this tool.\n\n\ Before you use this tool, please backup your data directory. \ You can do this at \"Account/Backup\".\n\n\ Please report us your problem and file a bug report on GitHub or at the Haveno forum so that we can investigate what was causing the problem. emptyWalletWindow.balance=Your available wallet balance emptyWalletWindow.address=Your destination address emptyWalletWindow.button=Send all funds emptyWalletWindow.openOffers.warn=You have open offers which will be removed if you empty the wallet.\nAre you sure that you want to empty your wallet? emptyWalletWindow.openOffers.yes=Yes, I am sure emptyWalletWindow.sent.success=The balance of your wallet was successfully transferred. enterPrivKeyWindow.headline=Enter private key for registration filterWindow.headline=Edit filter list filterWindow.offers=Filtered offers (comma sep.) filterWindow.onions=Banned from trading addresses (comma sep.) filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.) filterWindow.accounts=Filtered trading account data:\nFormat: comma sep. list of [payment method id | data field | value] filterWindow.bannedCurrencies=Filtered currency codes (comma sep.) filterWindow.bannedPaymentMethods=Filtered payment method IDs (comma sep.) filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) filterWindow.arbitrators=Filtered arbitrators (comma sep. onion addresses) filterWindow.mediators=Filtered mediators (comma sep. onion addresses) filterWindow.refundAgents=Filtered refund agents (comma sep. onion addresses) filterWindow.seedNode=Filtered seed nodes (comma sep. onion addresses) filterWindow.priceRelayNode=Filtered price relay nodes (comma sep. onion addresses) filterWindow.xmrNode=Filtered Monero nodes (comma sep. addresses + port) filterWindow.preventPublicXmrNetwork=Prevent usage of public Monero network filterWindow.disableAutoConf=Disable auto-confirm filterWindow.autoConfExplorers=Filtered auto-confirm explorers (comma sep. addresses) filterWindow.disableTradeBelowVersion=Min. version required for trading filterWindow.add=Add filter filterWindow.remove=Remove filter filterWindow.xmrFeeReceiverAddresses=XMR fee receiver addresses filterWindow.disableApi=Disable API filterWindow.disableMempoolValidation=Disable Mempool Validation offerDetailsWindow.minXmrAmount=Min. XMR amount offerDetailsWindow.min=(min. {0}) offerDetailsWindow.distance=(distance from market price: {0}) offerDetailsWindow.myTradingAccount=My trading account offerDetailsWindow.bankId=Bank ID (e.g. BIC or SWIFT) offerDetailsWindow.countryBank=Maker's country of bank offerDetailsWindow.commitment=Commitment offerDetailsWindow.agree=I agree offerDetailsWindow.tac=Terms and conditions offerDetailsWindow.confirm.maker.buy=Confirm: Place offer to buy XMR with {0} offerDetailsWindow.confirm.maker.sell=Confirm: Place offer to sell XMR for {0} offerDetailsWindow.confirm.taker.buy=Confirm: Take offer to buy XMR with {0} offerDetailsWindow.confirm.taker.sell=Confirm: Take offer to sell XMR for {0} offerDetailsWindow.creationDate=Creation date offerDetailsWindow.makersOnion=Maker's onion address offerDetailsWindow.challenge=Offer passphrase offerDetailsWindow.challenge.copy=Copy passphrase to share with your peer qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. qRCodeWindow.request=Payment request:\n{0} selectDepositTxWindow.headline=Select deposit transaction for dispute selectDepositTxWindow.msg=The deposit transaction was not stored in the trade.\n\ Please select one of the existing multisig transactions from your wallet which was the \ deposit transaction used in the failed trade.\n\n\ You can find the correct transaction by opening the trade details window (click on the trade ID in the list)\ and following the trading fee payment transaction output to the next transaction where you see \ the multisig deposit transaction (the address starts with 3). That transaction ID should be \ visible in the list presented here. Once you found the correct transaction select that transaction here and continue.\n\n\ Sorry for the inconvenience but that error case should happen very rarely and in future we will try \ to find better ways to resolve it. selectDepositTxWindow.select=Select deposit transaction sendAlertMessageWindow.headline=Send global notification sendAlertMessageWindow.alertMsg=Alert message sendAlertMessageWindow.enterMsg=Enter message sendAlertMessageWindow.isSoftwareUpdate=Software download notification sendAlertMessageWindow.isUpdate=Is full release sendAlertMessageWindow.isPreRelease=Is pre-release sendAlertMessageWindow.version=New version no. sendAlertMessageWindow.send=Send notification sendAlertMessageWindow.remove=Remove notification sendPrivateNotificationWindow.headline=Send private message sendPrivateNotificationWindow.privateNotification=Private notification sendPrivateNotificationWindow.enterNotification=Enter notification sendPrivateNotificationWindow.send=Send private notification showWalletDataWindow.walletData=Wallet data showWalletDataWindow.includePrivKeys=Include private keys setXMRTxKeyWindow.headline=Prove sending of XMR setXMRTxKeyWindow.note=Adding tx info below enables auto-confirm for quicker trades. See more: https://haveno.exchange/wiki/Trading_Monero setXMRTxKeyWindow.txHash=Transaction ID (optional) setXMRTxKeyWindow.txKey=Transaction key (optional) # We do not translate the tac because of the legal nature. We would need translations checked by lawyers # in each language which is too expensive atm. tacWindow.headline=User agreement tacWindow.agree=I agree tacWindow.disagree=I disagree and quit tacWindow.arbitrationSystem=Dispute resolution tradeDetailsWindow.headline=Trade tradeDetailsWindow.disputedPayoutTxId=Disputed payout transaction ID tradeDetailsWindow.tradeDate=Trade date tradeDetailsWindow.txFee=Mining fee tradeDetailsWindow.tradePeersOnion=Trading peers onion address tradeDetailsWindow.tradePeersPubKeyHash=Trading peers pubkey hash tradeDetailsWindow.tradeState=Trade state tradeDetailsWindow.tradePhase=Trade phase tradeDetailsWindow.agentAddresses=Arbitrator/Mediator tradeDetailsWindow.detailData=Detail data txDetailsWindow.headline=Transaction Details txDetailsWindow.xmr.noteSent=You have sent XMR. txDetailsWindow.xmr.noteReceived=You have received XMR. txDetailsWindow.sentTo=Sent to txDetailsWindow.receivedWith=Received with txDetailsWindow.txId=TxId txDetailsWindow.txKey=Transaction Key closedTradesSummaryWindow.headline=Trade history summary closedTradesSummaryWindow.totalAmount.title=Total trade amount closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) closedTradesSummaryWindow.totalTradeFeeInXmr.title=Sum of all trade fees paid in XMR closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} of total trade amount) walletPasswordWindow.headline=Enter password to unlock xmrConnectionError.headline=Monero connection error xmrConnectionError.providedNodes=Error connecting to provided Monero node(s).\n\nDo you want to use the next best available Monero node? xmrConnectionError.customNodes=Error connecting to your custom Monero node(s).\n\nDo you want to use the next best available Monero node? xmrConnectionError.localNode=Haveno was previously connected to a local Monero node, but it’s no longer reachable.\n\nMake sure your local node is running and fully synced, or choose another option to continue. xmrConnectionError.localNode.start=Start local node xmrConnectionError.localNode.start.error=Error starting local node xmrConnectionError.localNode.fallback=Connect to next best node torNetworkSettingWindow.header=Tor networks settings torNetworkSettingWindow.noBridges=Don't use bridges torNetworkSettingWindow.providedBridges=Connect with provided bridges torNetworkSettingWindow.customBridges=Enter custom bridges torNetworkSettingWindow.transportType=Transport type torNetworkSettingWindow.obfs3=obfs3 torNetworkSettingWindow.obfs4=obfs4 (recommended) torNetworkSettingWindow.meekAmazon=meek-amazon torNetworkSettingWindow.meekAzure=meek-azure torNetworkSettingWindow.enterBridge=Enter one or more bridge relays (one per line) torNetworkSettingWindow.enterBridgePrompt=type address:port torNetworkSettingWindow.restartInfo=You need to restart to apply the changes torNetworkSettingWindow.openTorWebPage=Open Tor project web page torNetworkSettingWindow.deleteFiles.header=Connection problems? torNetworkSettingWindow.deleteFiles.info=If you have repeated connection problems at start up, deleting outdated Tor files might help. To do that click the button below and restart afterwards. torNetworkSettingWindow.deleteFiles.button=Delete outdated Tor files and shut down torNetworkSettingWindow.deleteFiles.progress=Shut down Tor in progress torNetworkSettingWindow.deleteFiles.success=Outdated Tor files deleted successfully. Please restart. torNetworkSettingWindow.bridges.header=Is Tor blocked? torNetworkSettingWindow.bridges.info=If Tor is blocked by your internet provider or by your country you can try to use Tor bridges.\n\ Visit the Tor web page at: https://bridges.torproject.org to learn more about \ bridges and pluggable transports. feeOptionWindow.useXMR=Use XMR feeOptionWindow.fee={0} (≈ {1}) feeOptionWindow.xmrFeeWithFiatAndPercentage={0} (≈ {1} / {2}) feeOptionWindow.xmrFeeWithPercentage={0} ({1}) #################################################################### # Popups #################################################################### popup.headline.notification=Notification popup.headline.instruction=Please note: popup.headline.attention=Attention popup.headline.backgroundInfo=Background information popup.headline.feedback=Completed popup.headline.confirmation=Confirmation popup.headline.information=Information popup.headline.warning=Warning popup.headline.error=Error popup.doNotShowAgain=Don't show again popup.reportError.log=Open log file popup.reportError.gitHub=Report to GitHub issue tracker popup.reportError={0}\n\nTo help us to improve the software please report this bug by opening a new issue at https://github.com/haveno-dex/haveno/issues.\n\ The above error message will be copied to the clipboard when you click either of the buttons below.\n\ It will make debugging easier if you include the haveno.log file by pressing "Open log file", saving a copy, and attaching it to your bug report. popup.error.tryRestart=Please try to restart your application and check your network connection to see if you can resolve the issue. popup.error.takeOfferRequestFailed=An error occurred while taking an offer:\n{0} error.spvFileCorrupted=An error occurred when reading the SPV chain file.\nIt might be that the SPV chain file is corrupted.\n\nError message: {0}\n\nDo you want to delete it and start a resync? error.deleteAddressEntryListFailed=Could not delete AddressEntryList file.\nError: {0} error.closedTradeWithUnconfirmedDepositTx=The deposit transaction of the closed trade with the trade ID {0} is still \ unconfirmed.\n\n\ Please do a SPV resync at \"Setting/Network info\" to see if the transaction is valid. error.closedTradeWithNoDepositTx=The deposit transaction of the closed trade with the trade ID {0} is null.\n\n\ Please restart the application to clean up the closed trades list. popup.warning.walletNotInitialized=The wallet is not initialized yet popup.warning.wrongVersion=You probably have the wrong Haveno version for this computer.\n\ Your computer's architecture is: {0}.\n\ The Haveno binary you installed is: {1}.\n\ Please shut down and re-install the correct version ({2}). popup.warning.incompatibleDB=We detected incompatible data base files!\n\n\ Those database file(s) are not compatible with our current code base:\n{0}\n\n\ We made a backup of the corrupted file(s) and applied the default values to a new database version.\n\n\ The backup is located at:\n\ {1}/db/backup_of_corrupted_data.\n\n\ Please check if you have the latest version of Haveno installed.\n\ You can download it at: [HYPERLINK:https://haveno.exchange/downloads].\n\n\ Please restart the application. popup.warning.startupFailed.twoInstances=Haveno is already running. You cannot run two instances of Haveno. popup.warning.tradePeriod.halfReached=Your trade with ID {0} has reached the half of the max. allowed trading period and is still not completed.\n\nThe trade period ends on {1}\n\nPlease check your trade state at \"Portfolio/Open trades\" for further information. popup.warning.tradePeriod.ended=Your trade with ID {0} has reached the max. allowed trading period and is not completed.\n\n\ The trade period ended on {1}\n\n\ Please check your trade at \"Portfolio/Open trades\" for contacting the arbitrator. popup.warning.noTradingAccountSetup.headline=You have not setup a trading account popup.warning.noTradingAccountSetup.msg=You need to setup a national currency or cryptocurrency account before you can create an offer.\nDo you want to setup an account? popup.warning.noArbitratorsAvailable=There are no arbitrators available. popup.warning.noMediatorsAvailable=There are no mediators available. popup.warning.notFullyConnected=You need to wait until you are fully connected to the network.\nThat might take up to about 2 minutes at startup. popup.warning.notSufficientConnectionsToXmrNetwork=You need to wait until you have at least {0} connections to the Monero network. popup.warning.downloadNotComplete=You need to wait until the download of missing Monero blocks is complete. popup.warning.walletNotSynced=The Haveno wallet is not synced with the latest blockchain height. Please wait until the wallet syncs or check your connection. popup.warning.removeOffer=Are you sure you want to remove that offer? popup.warning.tooLargePercentageValue=You cannot set a percentage of 100% or larger. popup.warning.examplePercentageValue=Please enter a percentage number like \"5.4\" for 5.4% popup.warning.noPriceFeedAvailable=There is no price feed available for that currency. You cannot use a percent based price.\nPlease select the fixed price. popup.warning.sendMsgFailed=Sending message to your trading partner failed.\nPlease try again and if it continue to fail report a bug. popup.warning.messageTooLong=Your message exceeds the max. allowed size. Please send it in several parts or upload it to a service like https://pastebin.com. popup.warning.lockedUpFunds=You have locked up funds from a failed trade.\n\ Locked up balance: {0} \n\ Deposit tx address: {1}\n\ Trade ID: {2}.\n\n\ Please open a support ticket by selecting the trade in the open trades screen and pressing \"alt + o\" or \"option + o\"." popup.warning.moneroConnection=There was a problem connecting to the Monero network.\n\n{0} popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n takeOffer.cancelButton=Cancel take-offer takeOffer.warningButton=Ignore and continue anyway # suppress inspection "UnusedProperty" popup.warning.nodeBanned=One of the {0} nodes got banned. # suppress inspection "UnusedProperty" popup.warning.priceRelay=price relay popup.warning.seed=seed popup.warning.mandatoryUpdate.trading=Please update to the latest Haveno version. \ A mandatory update was released which disables trading for old versions. \ Please check out the Haveno Forum for more information. popup.warning.noFilter=We did not receive a filter object from the seed nodes. Please inform the network administrators to register a filter object. popup.warning.burnXMR=This transaction is not possible, as the mining fees of {0} would exceed the amount to transfer of {1}. \ Please wait until the mining fees are low again or until you've accumulated more XMR to transfer. popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Monero network.\n\ Transaction ID={1}.\n\ The offer has been removed to avoid further problems.\n\ Please go to \"Settings/Network info\" and do a SPV resync.\n\ For further help please contact the Haveno support channel at the Haveno Keybase team. popup.warning.trade.txRejected.tradeFee=trade fee popup.warning.trade.txRejected.deposit=deposit popup.warning.trade.txRejected=The {0} transaction for trade with ID {1} was rejected by the Monero network.\n\ Transaction ID={2}\n\ The trade has been moved to failed trades.\n\ Please go to \"Settings/Network info\" and do a SPV resync.\n\ For further help please contact the Haveno support channel at the Haveno Keybase team. popup.warning.openOfferWithInvalidMakerFeeTx=The maker fee transaction for offer with ID {0} is invalid.\n\ Transaction ID={1}.\n\ Please go to \"Settings/Network info\" and do a SPV resync.\n\ For further help please contact the Haveno support channel at the Haveno Keybase team. popup.info.cashDepositInfo=Please be sure that you have a bank branch in your area to be able to make the cash deposit.\n\ The bank ID (BIC/SWIFT) of the seller's bank is: {0}. popup.info.cashDepositInfo.confirm=I confirm that I can make the deposit popup.info.shutDownWithOpenOffers=Haveno is being shut down, but there are open offers. \n\n\ These offers won't be available on the P2P network while Haveno is shut down, but \ they will be re-published to the P2P network the next time you start Haveno.\n\n\ To keep your offers online, keep Haveno running and make sure this computer remains online too \ (i.e., make sure it doesn't go into standby mode...monitor standby is not a problem). popup.info.shutDownWithTradeInit={0}\n\ This trade has not finished initializing; shutting down now will probably make it corrupted. Please wait a minute and try again. popup.info.shutDownWithDisputeInit=Haveno is being shut down, but there is a Dispute system message still pending.\n\ Please wait a minute before shutting down. popup.info.shutDownQuery=Are you sure you want to exit Haveno? popup.info.qubesOSSetupInfo=It appears you are running Haveno on Qubes OS. \n\n\ Please make sure your Haveno qube is setup according to our Setup Guide at [HYPERLINK:https://haveno.exchange/wiki/Running_Haveno_on_Qubes]. popup.info.p2pStatusIndicator.red={0}\n\n\ Your node has no connection to the P2P network. Haveno cannot operate in this state. popup.info.p2pStatusIndicator.yellow={0}\n\n\ Your node has no inbound Tor connections. Haveno will function ok, but if this state persists for several hours it may be an indication of connectivity problems. popup.info.p2pStatusIndicator.green={0}\n\n\ Good news, your P2P connection state looks healthy! popup.info.firewallSetupInfo=It appears this machine blocks incoming Tor connections. \ This can happen in VM environments such as Qubes/VirtualBox/Whonix. \n\n\ Please set up your environment to accept incoming Tor connections, otherwise no-one will be able to take your offers. popup.warn.downGradePrevention=Downgrade from version {0} to version {1} is not supported. Please use the latest Haveno version. popup.warn.daoRequiresRestart=There was a problem with synchronizing the DAO state. You have to restart the application to fix the issue. popup.privateNotification.headline=Important private notification! popup.xmrLocalNode.msg=Haveno detected a Monero node running on this machine (at localhost).\n\n\Please ensure the node is fully synced before starting Haveno. popup.shutDownInProgress.headline=Shut down in progress popup.shutDownInProgress.msg=Shutting down application can take a few seconds.\nPlease don't interrupt this process. popup.attention.forTradeWithId=Attention required for trade with ID {0} popup.attention.welcome.stagenet=Welcome to the Haveno test instance!\n\n\ This platform allows you to test Haveno's protocol. Make sure to follow the instructions[HYPERLINK:https://github.com/haveno-dex/haveno/blob/master/docs/installing.md].\n\n\ If you encounter any problem, please let us know by opening an issue [HYPERLINK:https://github.com/haveno-dex/haveno/issues/new].\n\n\ This is a test instance. Do not use real money! popup.attention.welcome.mainnet=Welcome to Haveno!\n\n\ This platform allows you to trade Monero for fiat currencies or other cryptocurrencies in a decentralized way.\n\n\ Get started by creating a new payment account then making or taking an offer.\n\n\ If you encounter any problem, please let us know by opening an issue [HYPERLINK:https://github.com/haveno-dex/haveno/issues/new]. popup.attention.welcome.mainnet.test=Welcome to Haveno!\n\n\ This platform allows you to trade Monero for fiat currencies or other cryptocurrencies in a decentralized way.\n\n\ Get started by creating a new payment account then making or taking an offer.\n\n\ If you encounter any problem, please let us know by opening an issue [HYPERLINK:https://github.com/haveno-dex/haveno/issues/new].\n\n\ Haveno was recently released for public testing. Please use small amounts! popup.info.multiplePaymentAccounts.headline=Multiple payment accounts available popup.info.multiplePaymentAccounts.msg=You have multiple payment accounts available for this offer. Please make sure you've picked the right one. popup.accountSigning.selectAccounts.headline=Select payment accounts popup.accountSigning.selectAccounts.description=Based on the payment method and point of time all payment accounts that are connected to a dispute where a payout to the buyer occurred will be selected for you to sign. popup.accountSigning.selectAccounts.signAll=Sign all payment methods popup.accountSigning.selectAccounts.datePicker=Select point of time until which accounts will be signed popup.accountSigning.confirmSelectedAccounts.headline=Confirm selected payment accounts popup.accountSigning.confirmSelectedAccounts.description=Based on your input, {0} payment accounts will be selected. popup.accountSigning.confirmSelectedAccounts.button=Confirm payment accounts popup.accountSigning.signAccounts.headline=Confirm signing of payment accounts popup.accountSigning.signAccounts.description=Based on your selection, {0} payment accounts will be signed. popup.accountSigning.signAccounts.button=Sign payment accounts popup.accountSigning.signAccounts.ECKey=Enter private arbitrator key popup.accountSigning.signAccounts.ECKey.error=Bad arbitrator ECKey popup.accountSigning.success.headline=Congratulations popup.accountSigning.success.description=All {0} payment accounts were successfully signed! popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\n\ For further information, please visit [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer's account after a successful trade.\n\n{0} popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you'll be able to sign other accounts in {0} days from now.\n\n{1} popup.accountSigning.peerLimitLifted=The initial limit for one of your accounts has been lifted.\n\n{0} popup.accountSigning.peerSigner=One of your accounts is mature enough to sign other payment accounts \ and the initial limit for one of your accounts has been lifted.\n\n{0} popup.accountSigning.singleAccountSelect.headline=Import unsigned account age witness popup.accountSigning.confirmSingleAccount.headline=Confirm selected account age witness popup.accountSigning.confirmSingleAccount.selectedHash=Selected witness hash popup.accountSigning.confirmSingleAccount.button=Sign account age witness popup.accountSigning.successSingleAccount.description=Witness {0} was signed popup.accountSigning.successSingleAccount.success.headline=Success popup.accountSigning.unsignedPubKeys.headline=Unsigned Pubkeys popup.accountSigning.unsignedPubKeys.sign=Sign Pubkeys popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign popup.info.buyerAsTakerWithoutDeposit.headline=No deposit required from buyer popup.info.buyerAsTakerWithoutDeposit=\ Your offer will not require a security deposit or fee from the XMR buyer.\n\n\ To accept your offer, you must share a passphrase with your trade partner outside Haveno.\n\n\ The passphrase is automatically generated and shown in the offer details after creation.\ popup.info.torMigration.msg=Your Haveno node is probably using a deprecated Tor v2 address. \ Please switch your Haveno node to a Tor v3 address. \ Make sure to back up your data directory beforehand. #################################################################### # Notifications #################################################################### notification.trade.headline=Notification for trade with ID {0} notification.ticket.headline=Support ticket for trade with ID {0} notification.trade.completed=The trade is now completed, and you can withdraw your funds. notification.trade.accepted=Your offer has been accepted by a XMR {0}. notification.trade.unlocked=Your trade has been confirmed.\nYou can start the payment now. notification.trade.finalized=The trade has {0} confirmations.\nYou can start the payment now. notification.trade.paymentSent=The XMR buyer has sent the payment. notification.trade.selectTrade=Select trade notification.trade.peerOpenedDispute=Your trading peer has opened a {0}. notification.trade.disputeClosed=The {0} has been closed. notification.walletUpdate.headline=Trading wallet update notification.walletUpdate.msg=Your trading wallet is sufficiently funded.\nAmount: {0} notification.takeOffer.walletUpdate.msg=Your trading wallet was already sufficiently funded from an earlier take offer attempt.\nAmount: {0} notification.tradeCompleted.headline=Trade completed notification.tradeCompleted.msg=You can withdraw your funds to an external Monero wallet or keep them in your Haveno wallet. #################################################################### # System Tray #################################################################### systemTray.show=Show application window systemTray.hide=Hide application window systemTray.info=Info about Haveno systemTray.exit=Exit systemTray.tooltip=Haveno: A decentralized monero exchange network #################################################################### # GUI Util #################################################################### guiUtil.accountExport.savedToPath=Trading accounts saved to path:\n{0} guiUtil.accountExport.noAccountSetup=You don't have trading accounts set up for exporting. guiUtil.accountExport.selectPath=Select path to {0} # suppress inspection "TrailingSpacesInProperty" guiUtil.accountExport.tradingAccount=Trading account with id {0}\n # suppress inspection "TrailingSpacesInProperty" guiUtil.accountImport.noImport=We did not import trading account with id {0} because it exists already.\n guiUtil.accountExport.exportFailed=Exporting to CSV failed because of an error.\nError = {0} guiUtil.accountExport.selectExportPath=Select export path guiUtil.accountImport.imported=Trading account imported from path:\n{0}\n\nImported accounts:\n{1} guiUtil.accountImport.noAccountsFound=No exported trading accounts has been found at path: {0}.\nFile name is {1}." guiUtil.openWebBrowser.warning=You are going to open a web page \ in your system web browser.\n\ Do you want to open the web page now?\n\n\ If you are not using the \"Tor Browser\" as your default system web browser you \ will connect to the web page in clear net.\n\n\ URL: \"{0}\" guiUtil.openWebBrowser.doOpen=Open the web page and don't ask again guiUtil.openWebBrowser.copyUrl=Copy URL and cancel guiUtil.ofTradeAmount=of trade amount guiUtil.requiredMinimum=(required minimum) #################################################################### # Component specific #################################################################### list.currency.select=Select currency list.currency.showAll=Show all list.currency.editList=Edit currency list table.placeholder.noItems=Currently there are no {0} available table.placeholder.noData=Currently there is no data available table.placeholder.processingData=Processing data... peerInfoIcon.tooltip.tradePeer=Trading peer's peerInfoIcon.tooltip.maker=Maker's peerInfoIcon.tooltip.trade.traded={0} onion address: {1}\nYou have already traded {2} time(s) with that peer\n{3} peerInfoIcon.tooltip.trade.notTraded={0} onion address: {1}\nYou have not traded with that peer so far.\n{2} peerInfoIcon.tooltip.age=Payment account created {0} ago. peerInfoIcon.tooltip.unknownAge=Payment account age not known. peerInfoIcon.tooltip.dispute={0}\nNumber of disputes: {1}.\n{2} tooltip.openPopupForDetails=Open popup for details tooltip.invalidTradeState.warning=This trade is in an invalid state. Open the details window for more information tooltip.openBlockchainForAddress=Open external blockchain explorer for address: {0} tooltip.openBlockchainForTx=Open external blockchain explorer for transaction: {0} confidence.unknown=Unknown transaction status confidence.seen=Seen by {0} peer(s) / 0 confirmations confidence.confirmed={0} confirmation(s) confidence.invalid=Transaction is invalid peerInfo.title=Peer info peerInfo.nrOfTrades=Number of completed trades peerInfo.notTradedYet=You have not traded with that user so far. peerInfo.setTag=Set tag for that peer peerInfo.age.noRisk=Payment account age peerInfo.age.chargeBackRisk=Time since signing peerInfo.unknownAge=Age not known addressTextField.openWallet=Open your default Monero wallet addressTextField.copyToClipboard=Copy address to clipboard addressTextField.addressCopiedToClipboard=Address has been copied to clipboard addressTextField.openWallet.failed=Opening a default Monero wallet application has failed. Perhaps you don't have one installed? explorerAddressTextField.copyToClipboard=Copy address to clipboard explorerAddressTextField.blockExplorerIcon.tooltip=Open a blockchain explorer with this address explorerAddressTextField.missingTx.warning.tooltip=Missing required address peerInfoIcon.tooltip={0}\nTag: {1} txIdTextField.copyIcon.tooltip=Copy transaction ID to clipboard txIdTextField.blockExplorerIcon.tooltip=Open a blockchain explorer with this transaction ID txIdTextField.missingTx.warning.tooltip=Missing required transaction #################################################################### # Navigation #################################################################### navigation.account=\"Account\" navigation.account.walletSeed=\"Account/Wallet seed\" navigation.funds.availableForWithdrawal=\"Funds/Send funds\" navigation.portfolio.myOpenOffers=\"Portfolio/My open offers\" navigation.portfolio.pending=\"Portfolio/Open trades\" navigation.portfolio.closedTrades=\"Portfolio/History\" navigation.funds.depositFunds=\"Funds/Receive funds\" navigation.settings.preferences=\"Settings/Preferences\" # suppress inspection "UnusedProperty" navigation.funds.transactions=\"Funds/Transactions\" navigation.support=\"Support\" #################################################################### # Formatter #################################################################### formatter.formatVolumeLabel={0} amount{1} formatter.makerTaker=Maker as {0} {1} / Taker as {2} {3} formatter.makerTaker.locked=Maker as {0} {1} / Taker as {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=You are {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=You are creating an offer to {0} {1} formatter.youAreCreatingAnOffer.traditional.locked=You are creating an offer to {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=You are creating an offer to {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.crypto.locked=You are creating an offer to {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} as maker formatter.asTaker={0} {1} as taker #################################################################### # Domain specific #################################################################### # we use enum values here # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" XMR_MAINNET=Monero Mainnet # suppress inspection "UnusedProperty" XMR_LOCAL=Monero Local Testnet # suppress inspection "UnusedProperty" XMR_STAGENET=Monero Stagenet time.year=Year time.month=Month time.halfYear=Half-year time.quarter=Quarter time.week=Week time.day=Day time.hour=Hour time.minute10=10 Minutes time.hours=hours time.days=days time.1hour=1 hour time.1day=1 day time.minute=minute time.second=second time.minutes=minutes time.seconds=seconds password.enterPassword=Enter password password.confirmPassword=Confirm password password.tooLong=Password must be less than 500 characters. password.deriveKey=Derive key from password password.walletDecrypted=Wallet successfully decrypted and password protection removed. password.wrongPw=You entered the wrong password.\n\nPlease try entering your password again, carefully checking for typos or spelling errors. password.walletEncrypted=Wallet successfully encrypted and password protection enabled. password.walletEncryptionFailed=Password could not be set. password.passwordsDoNotMatch=The 2 passwords you entered don't match. password.forgotPassword=Forgot password? password.backupReminder=Please note that when setting a wallet password all automatically created backups from the unencrypted wallet will be deleted.\n\n\ It is highly recommended that you make a backup of the application directory and write down your seed words before setting a password! password.setPassword=Set Password (I already made a backup) password.makeBackup=Make Backup seed.seedWords=Wallet seed words seed.enterSeedWords=Enter wallet seed words seed.date=Wallet date seed.restore.title=Restore wallets from seed words seed.restore=Restore wallets seed.creationDate=Creation date seed.warn.walletNotEmpty.msg=Your Monero wallet is not empty.\n\n\ You must empty this wallet before attempting to restore an older one, as mixing wallets \ together can lead to invalidated backups.\n\n\ Please finalize your trades, close all your open offers and go to the Funds section to withdraw your monero.\n\ In case you cannot access your monero you can use the emergency tool to empty the wallet.\n\ To open the emergency tool press \"Alt+e\" or \"Cmd/Ctrl+e\". seed.warn.walletNotEmpty.restore=I want to restore anyway seed.warn.walletNotEmpty.emptyWallet=I will empty my wallets first seed.warn.notEncryptedAnymore=Your wallets are encrypted.\n\n\ After restore, the wallets will no longer be encrypted and you must set a new password.\n\n\ Do you want to proceed? seed.warn.walletDateEmpty=As you have not specified a wallet date, haveno will have to scan the blockchain from 2013.10.09 (the BIP39 epoch date).\n\n\ BIP39 wallets were first introduced in haveno on 2017.06.28 (release v0.5). So you could save time by using that date.\n\n\ Ideally you should specify the date your wallet seed was created.\n\n\n\ Are you sure you want to go ahead without specifying a wallet date? seed.restore.success=Wallets restored successfully with the new seed words.\n\nYou need to shut down and restart the application. seed.restore.error=An error occurred when restoring the wallets with seed words.{0} seed.restore.openOffers.warn=You have open offers which will be removed if you restore from seed words.\n\ Are you sure that you want to continue? #################################################################### # Payment methods #################################################################### payment.account=Account payment.account.no=Account no. payment.account.name=Account name payment.account.username=Username payment.account.phoneNr=Phone number payment.account.owner.fullname=Account owner full name payment.account.owner.ask=[Ask trader to provide account name if needed] payment.account.fullName=Full name (first, middle, last) payment.account.state=State/Province/Region payment.account.city=City payment.account.address=Address payment.bank.country=Country of bank payment.account.name.email=Account owner full name / email payment.account.name.emailAndHolderId=Account owner full name / email / {0} payment.bank.name=Bank name payment.select.account=Select account type payment.select.region=Select region payment.select.country=Select country payment.select.bank.country=Select country of bank payment.foreign.currency=Are you sure you want to choose a currency other than the country's default currency? payment.restore.default=No, restore default currency payment.email=Email payment.country=Country payment.extras=Extra requirements payment.email.mobile=Email or mobile no. payment.email.mobile.cashtag=Cashtag, email, or mobile no. payment.email.mobile.username=Username, email, or mobile no. payment.crypto.address=Cryptocurrency address payment.crypto.tradeInstantCheckbox=Trade instant (within 1 hour) with this Cryptocurrency payment.crypto.tradeInstant.popup=For instant trading it is required that both trading peers are online to be able \ to complete the trade in less than 1 hour.\n\n\ If you have offers open, and you are not available please disable \ those offers under the 'Portfolio' screen. payment.crypto=Cryptocurrency payment.select.crypto=Select or search Cryptocurrency payment.secret=Secret question payment.answer=Answer payment.wallet=Wallet ID payment.capitual.cap=CAP Code payment.upi.virtualPaymentAddress=Virtual Payment Address # suppress inspection "UnusedProperty" payment.swift.headline=International SWIFT Wire Transfer # suppress inspection "UnusedProperty" payment.swift.title.bank=Receiving Bank # suppress inspection "UnusedProperty" payment.swift.title.intermediary=Intermediary Bank (click to expand) # suppress inspection "UnusedProperty" payment.swift.country.bank=Receiving Bank Country # suppress inspection "UnusedProperty" payment.swift.country.intermediary=Intermediary Bank Country # suppress inspection "UnusedProperty" payment.swift.swiftCode.bank=Receiving Bank SWIFT Code # suppress inspection "UnusedProperty" payment.swift.swiftCode.intermediary=Intermediary Bank SWIFT Code # suppress inspection "UnusedProperty" payment.swift.name.bank=Receiving Bank name # suppress inspection "UnusedProperty" payment.swift.name.intermediary=Intermediary Bank name # suppress inspection "UnusedProperty" payment.swift.branch.bank=Receiving Bank branch # suppress inspection "UnusedProperty" payment.swift.branch.intermediary=Intermediary Bank branch # suppress inspection "UnusedProperty" payment.swift.address.bank=Receiving Bank address # suppress inspection "UnusedProperty" payment.swift.address.intermediary=Intermediary Bank address # suppress inspection "UnusedProperty" payment.swift.address.beneficiary=Beneficiary address # suppress inspection "UnusedProperty" payment.swift.phone.beneficiary=Beneficiary phone number payment.swift.account=Account No. (or IBAN) payment.swift.use.intermediary=Use Intermediary Bank payment.swift.showPaymentInfo=Show Payment Information... payment.account.owner.address=Account owner address payment.transferwiseUsd.address=(must be US-based, consider using bank address) payment.amazon.site=Buy giftcard at payment.ask=Ask in Trader Chat payment.uphold.accountId=Username or email or phone no. payment.moneyBeam.accountId=Email or phone no. payment.popmoney.accountId=Email or phone no. payment.promptPay.promptPayId=Citizen ID/Tax ID or phone no. payment.supportedCurrencies=Supported currencies payment.supportedCurrenciesForReceiver=Currencies for receiving funds payment.limitations=Limitations payment.salt=Salt for account age verification payment.error.noHexSalt=The salt needs to be in HEX format.\n\ It is only recommended to edit the salt field if you want to transfer the salt from an old account to keep your account age. \ The account age is verified by using the account salt and the identifying account data (e.g. IBAN). payment.accept.euro=Accept trades from these Euro countries payment.accept.nonEuro=Accept trades from these non-Euro countries payment.accepted.countries=Accepted countries payment.accepted.banks=Accepted banks (ID) payment.mobile=Mobile no. payment.postal.address=Postal address payment.national.account.id.AR=CBU number shared.accountSigningState=Account signing status #new payment.crypto.address.dyn={0} address payment.crypto.receiver.address=Receiver's cryptocurrency address payment.accountNr=Account number payment.emailOrMobile=Email or mobile no. payment.useCustomAccountName=Use custom account name payment.maxPeriod=Max. allowed trade period payment.maxPeriodAndLimit=Max. trade duration: {0} / Max. buy: {1} / Max. sell: {2} / Account age: {3} payment.maxPeriodAndLimitCrypto=Max. trade duration: {0} / Max. trade limit: {1} payment.currencyWithSymbol=Currency: {0} payment.nameOfAcceptedBank=Name of accepted bank payment.addAcceptedBank=Add accepted bank payment.clearAcceptedBanks=Clear accepted banks payment.bank.nameOptional=Bank name (optional) payment.bankCode=Bank code payment.bankId=Bank ID (BIC/SWIFT) payment.bankIdOptional=Bank ID (BIC/SWIFT) (optional) payment.branchNr=Branch no. payment.branchNrOptional=Branch no. (optional) payment.accountNrLabel=Account no. (IBAN) payment.iban=IBAN payment.tikkie.iban=IBAN used for Haveno trading on Tikkie payment.accountType=Account type payment.checking=Checking payment.savings=Savings payment.personalId=Personal ID payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n\ 1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n\ 2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n\ 3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n\ 4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\n\ If you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\n\ Because of Zelle's somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer \ really owns the Zelle account specified in Haveno. payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver's full name for Faster \ Payments transfers. Your current Faster Payments account does not specify a full name.\n\n\ Please consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\n\ When you recreate the account, make sure to copy the precise sort code, account number and account age verification \ salt values from your old account to your new account. This will ensure your existing account's age and signing \ status are preserved. payment.fasterPayments.ukSortCode=UK sort code payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. \ The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. \ The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=When using HalCash the XMR buyer needs to send the XMR seller the HalCash code via a text message from their mobile phone.\n\n\ Please make sure to not exceed the maximum amount your bank allows you to send with HalCash. \ The min. amount per withdrawal is 10 EUR and the max. amount is 600 EUR. For repeated withdrawals it is \ 3000 EUR per receiver per day and 6000 EUR per receiver per month. Please cross-check those limits with your \ bank to be sure they use the same limits as stated here.\n\n\ The withdrawal amount must be a multiple of 10 EUR as you cannot withdraw other amounts from an ATM. The \ UI in the create-offer and take-offer screen will adjust the XMR amount so that the EUR amount is correct. You cannot use market \ based price as the EUR amount would be changing with changing prices.\n\n\ In case of a dispute the XMR buyer needs to provide the proof that they sent the EUR. # suppress inspection "UnusedMessageFormatParameter" payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, \ Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\ \n\ For this payment method, your per-trade limit for buying and selling is {2}.\n\ \n\ This limit only applies to the size of a single trade—you can place as many trades as you like.\n\ \n\ See more details on the wiki [HYPERLINK:https://docs.haveno.exchange/overview/account_limits]. # suppress inspection "UnusedProperty" payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based \ on the following 2 factors:\n\n\ 1. General chargeback risk for the payment method\n\ 2. Account signing status\n\ \n\ This payment account is not yet signed, so it is limited to buying {0} per trade. \ After signing, buy limits will increase as follows:\n\ \n\ ● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n\ ● 30 days after signing, your per-trade buy limit will be {1}\n\ ● 60 days after signing, your per-trade buy limit will be {2}\n\ \n\ Sell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\ \n\ These limits only apply to the size of a single trade—you can place as many trades as you like. \n\ \n\ See more details on the wiki [HYPERLINK:https://docs.haveno.exchange/overview/account_limits]. payment.cashDeposit.info=Please confirm your bank allows you to send cash deposits into other peoples' accounts. \ For example, Bank of America and Wells Fargo no longer allow such deposits. payment.revolut.info=Revolut requires the 'Username' as account ID not the phone number or email as it was the case in the past. payment.account.revolut.addUserNameInfo={0}\n\ Your existing Revolut account ({1}) does not have a 'Username'.\n\ Please enter your Revolut 'Username' to update your account data.\n\ This will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account payment.cashapp.info=Please be aware that Cash App has higher chargeback risk than most bank transfers. payment.venmo.info=Please be aware that Venmo has higher chargeback risk than most bank transfers. payment.paypal.info=Please be aware that PayPal has higher chargeback risk than most bank transfers. payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\n\ Your existing Amazon Gift Card account ({1}) does not have a Country specified.\n\ Please enter your Amazon Gift Card Country to update your account data.\n\ This will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account payment.swift.info.account=Carefully review the core guidelines for using SWIFT on Haveno:\n\ \n\ - fill all fields completely and accurately \n\ - buyer must send payment in currency specified by the offer maker \n\ - buyer must use the shared fee model (SHA) \n\ - buyer and seller may incur fees, so they should check their bank's fee schedules beforehand \n\ \n\ SWIFT is more sophisticated than other payment methods, so please take a moment to review full guidance on the wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/SWIFT]. payment.swift.info.buyer=To buy monero with SWIFT, you must:\n\ \n\ - send payment in the currency specified by the offer maker \n\ - use the shared fee model (SHA) to send payment\n\ \n\ Please review further guidance on the wiki to avoid penalties and ensure smooth trades [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/SWIFT]. payment.swift.info.seller=SWIFT senders are required to use the shared fee model (SHA) to send payments.\n\ \n\ If you receive a SWIFT payment that does not use SHA, open a mediation ticket.\n\ \n\ Please review further guidance on the wiki to avoid penalties and ensure smooth trades [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/SWIFT]. payment.imps.info.account=Please make sure to include your:\n\n\ ● Account owner full name\n\ ● Account number\n\ ● IFSC number\n\n\ These details should match your bank account that you will use for sending / receiving payments.\n\n\ Please be aware there is a maximum of Rs. 200,000 that can be sent per transaction. If you are trading over this amount multiple transactions will be needed. However be aware their is a maximum limit of Rs. 1,000,000 that can be sent per day.\n\n\ Some banks have different limits for their customers. payment.imps.info.buyer=Please send payment only to the account details provided in Haveno.\n\n\ The maximum trade size is Rs. 200,000 per transaction.\n\n\ If your trade is over Rs. 200,000 you will have to make multiple transfers. However, be aware there is a maximum limit of Rs. 1,000,000 that can be sent per day.\n\n\ Please note some banks have different limits for their customers. payment.imps.info.seller=If you intend to receive over Rs. 200,000 per trade you should expect the buyer to have to make multiple transfers. However, be aware there is a maximum limit of Rs. 1,000,000 that can be sent per day.\n\n\ Please note some banks have different limits for their customers. payment.neft.info.account=Please make sure to include your:\n\n\ ● Account owner full name\n\ ● Account number\n\ ● IFSC number\n\n\ These details should match your bank account that you will use for sending / receiving payments.\n\n\ Please be aware there is a maximum of Rs. 50,000 that can be sent per transaction. If you are trading over this amount multiple transactions will be needed.\n\n\ Please note some banks have different limits for their customers. payment.neft.info.buyer=Please send payment only to the account details provided in Haveno.\n\n\ The maximum trade size is Rs. 50,000 per transaction.\n\n\ If your trade is over Rs. 50,000 you will have to make multiple transfers.\n\n\ Please note some banks have different limits for their customers. payment.neft.info.seller=If you intend to receive over Rs. 50,000 per trade you should expect the buyer to have to make multiple transfers.\n\n\ Please note some banks have different limits for their customers. payment.paytm.info.account=Please make sure to include your email or phone number that matches your email or phone number in your PayTM account. \n\n\ When users set up a PayTM account with No KYC users are limited to: \n\n\ ● Maximum of Rs. 5,000 can be sent per transaction.\n\ ● Maximum of Rs. 10,000 can be held in someone's PayTM wallet.\n\n\ If you intend to trade amount of over 5,000 per trade you will need to complete KYC with PayTM. With KYC users are limited to:\n\n\ ● Maximum of Rs. 100,000 can be sent per transaction.\n\ ● Maximum of Rs. 100,000 can be held in someone's PayTM wallet.\n\n\ Users should also be aware of account limits. Trades above PayTM account limits will likely have to take place over more than one day, or, be cancelled. payment.paytm.info.buyer=Please send payment only to the email address or phone number provided.\n\n\ If you intend to trade amount of over Rs. 5,000 per trade you will need to complete KYC with PayTM.\n\n\ With No KYC Rs. 5,000 can be sent per transaction.\n\n\ With KYC users Rs. 100,000 can be sent per transaction. payment.paytm.info.seller=If you intend to trade amount of over Rs. 5,000 per trade you will need to complete KYC with PayTM. With KYC users are limited to:\n\n\ ● Maximum of Rs. 100,000 can be sent per transaction.\n\ ● Maximum of Rs. 100,000 can be held in your PayTM wallet .\n\n\ Users should also be aware of account limits. As a maximum of Rs. 100,000 can be held in your PayTM wallet please make sure you transfer out your rupees regularly. payment.rtgs.info.account=RTGS is for payments of large trades of Rs. 200,000 or over.\n\n\ When setting up your RTGS payment account please make sure to include your:\n\n\ ● Account owner full name\n\ ● Account number\n\ ● IFSC number\n\n\ These details should match your bank account that you will use for sending / receiving payments.\n\n\ Please be aware there is a minimum trade amount of Rs. 200,000 that can be sent per transaction. If you are trading under this amount either the trade would get cancelled or both traders would have to agree on another payment method (eg IMPS or UPI). payment.rtgs.info.buyer=Please send payment only to the account details provided in Haveno.\n\n\ Please be aware there is a minimum trade amount of Rs. 200,000 that can be sent per transaction. If you are trading under this amount either the trade would get cancelled or both traders would have to agree on another payment method (eg IMPS or UPI). payment.rtgs.info.seller=Please be aware there is a minimum trade amount of Rs. 200,000 that can be sent per transaction. If you are trading under this amount either the trade would get cancelled or both traders would have to agree on another payment method (eg IMPS or UPI). payment.upi.info.account=Please make sure to include your Virtual Payment Address (VPA) also called your UPI ID. The format for this is like an email ID: with the sign “@” in the middle. For example, your UPI ID could be “receiver’s_name@bank_name” or “phone_number@bank_name.” \n\n\ For UPI there is a maximum limit of Rs. 100,000 that can be sent per transaction. \n\n\ If you intend to trade amount of over Rs. 100,000 per trade it is likely trades will have to take place over multiple transfers. \n\n\ Please note some banks have different limits for their customers. payment.upi.info.buyer=Please send payment only to the VPA / UPI ID provided in Haveno. \n\n\ The maximum trade size is Rs. 100,000 per transaction. \n\n\ If your trade is over Rs. 100,000 you will have to make multiple transfers. \n\n\ Please note some banks have different limits for their customers. payment.upi.info.seller=If you intend to receive over Rs. 100,000 per trade you should expect the buyer to have to make multiple transfers. \n\n\ Please note some banks have different limits for their customers. payment.celpay.info.account=Please make sure to include the email your Celsius account is registered to. \ This will ensure that when you send funds they show from the correct account and when you receive funds they will be credited to your account.\n\n\ CelPay users are limited to sending $2,500 (or other currency/crypto equivalent) in 24 hours.\n\n\ Trades above CelPay account limits will likely have to take place over more than one day, or, be cancelled.\n\n\ CelPay supports multiple stablecoins:\n\n\ ● USD Stablecoins; DAI, TrueUSD, USDC, ZUSD, BUSD, GUSD, PAX, USDT (ERC20)\n\ ● CAD Stablecoins; TrueCAD\n\ ● GBP Stablecoins; TrueGBP\n\ ● HKD Stablecoins; TrueHKD\n\ ● AUD Stablecoins; TrueAUD\n\n\ XMR Buyers can send any matching currency stablecoin to the XMR Seller. payment.celpay.info.buyer=Please send payment only to the email address provided by the XMR Seller by sending a payment link.\n\n\ CelPay is limited to sending $2,500 (or other currency/crypto equivalent) in 24 hours.\n\n\ Trades above CelPay account limits will likely have to take place over more than one day, or, be cancelled.\n\n\ CelPay supports multiple stablecoins:\n\n\ ● USD Stablecoins; DAI, TrueUSD, USDC, ZUSD, BUSD, GUSD, PAX, USDT (ERC20)\n\ ● CAD Stablecoins; TrueCAD\n\ ● GBP Stablecoins; TrueGBP\n\ ● HKD Stablecoins; TrueHKD\n\ ● AUD Stablecoins; TrueAUD\n\n\ XMR Buyers can send any matching currency stablecoin to the XMR Seller. payment.celpay.info.seller=XMR Sellers should expect to receive payment via a secure payment link. \ Please make sure the email payment link contains the email address provided by the XMR Buyer.\n\n\ CelPay users are limited to sending $2,500 (or other currency/crypto equivalent) in 24 hours.\n\n\ Trades above CelPay account limits will likely have to take place over more than one day, or, be cancelled.\n\n\ CelPay supports multiple stablecoins:\n\n\ ● USD Stablecoins; DAI, TrueUSD, USDC, ZUSD, BUSD, GUSD, PAX, USDT (ERC20)\n\ ● CAD Stablecoins; TrueCAD\n\ ● GBP Stablecoins; TrueGBP\n\ ● HKD Stablecoins; TrueHKD\n\ ● AUD Stablecoins; TrueAUD\n\n\ XMR Sellers should expect to receive any matching currency stablecoin from the XMR Buyer. It is possible for the XMR Buyer to send any matching currency stablecoin. payment.celpay.supportedCurrenciesForReceiver=Supported currencies (please note: all the currencies below are supported stable coins within the Celcius app. Trades are for stable coins, not fiat.) payment.nequi.info.account=Please make sure to include your phone number that is associated with your Nequi account.\n\n\ When users set up a Nequi account payment limits are set to a maximum of ~ 7,000,000 COP that can be sent per month.\n\n\ If you intend to trade amount of over 7,000,000 COP per trade you will need to complete KYC with Bancolombia and pay a fee \ of around 15,000 COP. After this all transactions will incur a 0.4% of tax. Please ensure you are aware of the latest taxes.\n\n\ Users should also be aware of account limits. If you trade over the above limits your trade might be cancelled and there could be a penalty. payment.nequi.info.buyer=Please send payment only to the phone number provided in the XMR Seller's Haveno account.\n\n\ When users set up a Nequi account, payment limits are set to a maximum of ~ 7,000,000 COP that can be sent per month.\n\n\ If you intend to trade amount of over 7,000,000 COP per trade you will need to complete KYC with Bancolombia and pay a fee \ of around 15,000 COP. After this all transactions will incur a 0.4% of tax. Please ensure you are aware of the latest taxes.\n\n\ Users should also be aware of account limits. If you trade over the above limits your trade might be cancelled and there could be a penalty. payment.nequi.info.seller=Please check that the payment received matches the phone number provided in the XMR Buyer's Haveno account.\n\n\ When users set up a Nequi account, payment limits are set to a maximum of ~ 7,000,000 COP that can be sent per month.\n\n\ If you intend to trade amount of over 7,000,000 COP per trade you will need to complete KYC with Bancolombia and pay a fee \ of around 15,000 COP. After this all transactions will incur a 0.4% of tax. Please ensure you are aware of the latest taxes.\n\n\ Users should also be aware of account limits. If you trade over the above limits your trade might be cancelled and there could be a penalty. payment.bizum.info.account=To use Bizum you need a bank account (IBAN) in Spain and to be registered for the service.\n\n\ Bizum can be used for trades between €0.50 and €1,000.\n\n\ The maximum amount of transactions you can send/receive using Bizum is €2,000 Euros per day.\n\n\ Bizum users can have a maximum of 150 operations per month.\n\n\ Each bank however may establish its own limits, within the above limits, for its clients.\n\n\ Users should also be aware of account limits. If you trade over the above limits your trade might be cancelled and there could be a penalty. payment.bizum.info.buyer=Please send payment only to the XMR Seller's mobile phone number as provided in Haveno.\n\n\ The maximum trade size is €1,000 per payment. The maximum amount of transactions you can send using Bizum is €2,000 Euros per day.\n\n\ If you trade over the above limits your trade might be cancelled and there could be a penalty. payment.bizum.info.seller=Please make sure your payment is received from the XMR Buyer's mobile phone number as provided in Haveno.\n\n\ The maximum trade size is €1,000 per payment. The maximum amount of transactions you can receive using Bizum is €2,000 Euros per day.\n\n\ If you trade over the above limits your trade might be cancelled and there could be a penalty. payment.pix.info.account=Please make sure to include your chosen Pix Key. There are four types of keys: \ CPF (Natural Persons Register) or CNPJ (National Registry of Legal Entities), e-mail address, telephone number or a \ random key generated by the system called a universally unique identifier (UUID). A different key must be used for \ each Pix account you have. Individuals can create up to five keys for each account they own.\n\n\ When trading on Haveno, XMR Buyers should use their Pix Keys as the payment description so that it is easy for the XMR Sellers to identify the payment as coming from themselves. payment.pix.info.buyer=Please send payment only the Pix Key provided in the XMR Seller's Haveno account.\n\n\ Please use your Pix Key as the payment reference so that it is easy for the XMR Seller to identify the payment as coming from yourself. payment.pix.info.seller=Please check that the payment received description matches the Pix Key provided in the XMR Buyer's Haveno account. payment.pix.key=Pix Key (CPF, CNPJ, Email, Phone number or UUID) payment.monese.info.account=Monese is a bank app for users of GBP, EUR and RON*. Monese allows users to send money to \ other Monese accounts instantly and for free in any supported currency.\n\n\ *To open a RON account in Monese, you need to either live in Romania or have Romanian citizenship.\n\n\ When setting up your Monese account in Haveno please make sure to include your name and phone number that matches your \ Monese account. This will ensure that when you send funds they show from the correct account and when you receive \ funds they will be credited to your account. payment.monese.info.buyer=Please send payment only to the phone number provided by the XMR Seller in their Haveno account. Please leave the payment description blank. payment.monese.info.seller=XMR Sellers should expect to receive payment from the phone number / name shown in the XMR Buyer's Haveno account. payment.satispay.info.account=To use Satispay you need a bank account (IBAN) in Italy and to be registered for the service.\n\n\ Satispay account limits are individually set. If you want to trade increased amounts you will need to contact Satispay \ support to increase your limits. Users should also be aware of account limits. If you trade over the above limits \ your trade might be cancelled and there could be a penalty. payment.satispay.info.buyer=Please send payment only to the XMR Seller's mobile phone number as provided in Haveno.\n\n\ Satispay account limits are individually set. If you want to trade increased amounts you will need to contact Satispay \ support to increase your limits. Users should also be aware of account limits. If you trade over the above limits \ your trade might be cancelled and there could be a penalty. payment.satispay.info.seller=Please make sure your payment is received from the XMR Buyer's mobile phone number / name as provided in Haveno.\n\n\ Satispay account limits are individually set. If you want to trade increased amounts you will need to contact Satispay \ support to increase your limits. Users should also be aware of account limits. If you trade over the above limits \ your trade might be cancelled and there could be a penalty. payment.tikkie.info.account=To use Tikkie you need a bank account (IBAN) in The Netherlands and to be registered for the service.\n\n\ When you send a Tikkie payment request to an individual person you can ask to receive a maximum of €750 per Tikkie \ request. The maximum amount you can request within 24 hours is €2,500 per Tikkie account.\n\n\ Each bank however may establish its own limits, within these limits, for its clients.\n\n\ Users should also be aware of account limits. If you trade over the above limits your trade might be cancelled and there could be a penalty. payment.tikkie.info.buyer=Please request a payment link from the XMR Seller in trader chat. Once the XMR Seller has \ sent you a payment link that matches the correct amount for the trade please proceed to payment.\n\n\ When the XMR Seller requests a Tikkie payment the maximum they can ask to receive is €750 per Tikkie request. If the \ trade is over that amount the XMR Seller will have to sent multiple requests to total the trade amount. The maximum \ you can request in a day is €2,500.\n\n\ Each bank however may establish its own limits, within these limits, for its clients.\n\n\ Users should also be aware of account limits. If you trade over the above limits your trade might be cancelled and there could be a penalty. payment.tikkie.info.seller=Please send a payment link to the XMR Seller in trader chat. Once the XMR \ Buyer has sent you payment please check their IBAN detail match the details they have in Haveno.\n\n\ When the XMR Seller requests a Tikkie payment the maximum they can ask to receive is €750 per Tikkie request. If the \ trade is over that amount the XMR Seller will have to sent multiple requests to total the trade amount. The maximum \ you can request in a day is €2,500.\n\n\ Each bank however may establish its own limits, within these limits, for its clients.\n\n\ Users should also be aware of account limits. If you trade over the above limits your trade might be cancelled and there could be a penalty. payment.verse.info.account=Verse is a multiple currency payment method that can send and receive payment in EUR, SEK, HUF, DKK, PLN.\n\n\ When setting up your Verse account in Haveno please make sure to include the username that matches your username in your \ Verse account. This will ensure that when you send funds they show from the correct account and when you receive \ funds they will be credited to your account.\n\n\ Verse users are limited to sending or receiving €10,000 per year (or equivalent foreign currency amount) for \ accumulated payments made from or received into their payment account. This can be increased by Verse on request. payment.verse.info.buyer=Please send payment only to the username provided by the XMR Seller in their Haveno account. \ Please leave the payment description blank.\n\n\ Verse users are limited to sending or receiving €10,000 per year (or equivalent foreign currency amount) for \ accumulated payments made from or received into their payment account. This can be increased by Verse on request. payment.verse.info.seller=XMR Sellers should expect to receive payment from the username shown in the XMR Buyer's Haveno account.\n\n\ Verse users are limited to sending or receiving €10,000 per year (or equivalent foreign currency amount) for \ accumulated payments made from or received into their payment account. This can be increased by Verse on request. payment.achTransfer.info.account=When adding ACH as a payment method in Haveno users should make sure they are aware what \ it will cost to send and receive an ACH transfer. payment.achTransfer.info.buyer=Please ensure you are aware of what it will cost you to send an ACH transfer.\n\n\ When paying, send only to the payment details provided in the XMR Seller's account using ACH transfer. payment.achTransfer.info.seller=Please ensure you are aware of what it will cost you to receive an ACH transfer.\n\n\ When receiving payment, please check that it is received from the XMR Buyer's account as an ACH transfer. payment.domesticWire.info.account=When adding Domestic Wire Transfer as a payment method in Haveno users should make sure \ they are aware what it will cost to send and receive a wire transfer. payment.domesticWire.info.buyer=Please ensure you are aware of what it will cost you to send a wire transfer.\n\n\ When paying, send only to the payment details provided in the XMR Seller's account. payment.domesticWire.info.seller=Please ensure you are aware of what it will cost you to receive a wire transfer.\n\n\ When receiving payment, please check that it is received from the XMR Buyer's account. payment.strike.info.account=Please make sure to include your Strike username.\n\n\ In Haveno, Strike is used for fiat to fiat payments only.\n\n\ Please make sure you are aware of the Strike limits:\n\n\ Users who have registered with only their email, name, and phone number have the following limits:\n\n\ ● $100 maximum per deposit\n\ ● $1,000 maximum total deposits per week\n\ ● $100 maximum per payment\n\n\ Users can increase their limits by providing Strike with more information. These users have the following limits:\n\n\ ● $1,000 maximum per deposit\n\ ● $1,000 maximum total deposits per week\n\ ● $1,000 maximum per payment\n\n\ If you trade over the above limits your trade might be cancelled and there could be a penalty. payment.strike.info.buyer=Please send payment only to the XMR Seller's Strike username as provided in Haveno.\n\n\ The maximum trade size is $1,000 per payment.\n\n\ If you trade over the above limits your trade might be cancelled and there could be a penalty. payment.strike.info.seller=Please make sure your payment is received from the XMR Buyer's Strike username as provided in Haveno.\n\n\ The maximum trade size is $1,000 per payment.\n\n\ If you trade over the above limits your trade might be cancelled and there could be a penalty. payment.transferwiseUsd.info.account=Due to US banking regulation, sending and receiving USD payments has more restrictions \ than most other currencies. For this reason USD was not added to Haveno Wise payment method.\n\n\ The Wise-USD payment method allows Haveno users to trade in USD.\n\n\ Anyone with a Wise, formally Wise account, can add Wise-USD as a payment method in Haveno. This will \ allow them to buy and sell XMR with USD.\n\n\ When trading on Haveno XMR Buyers should not use any reference for reason for payment. If reason for payment is required \ they should only use the full name of the Wise-USD account owner. payment.transferwiseUsd.info.buyer=Please send payment only to the email address in the XMR Seller's Haveno Wise-USD account. payment.transferwiseUsd.info.seller=Please check that the payment received matches the XMR Buyer's name of the Wise-USD account in Haveno. payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Haveno requires that you understand the following:\n\ \n\ - XMR buyers must write the XMR Seller’s name in both the Payer and the Payee’s fields & take a high-resolution photo of the USPMO and envelope with proof of tracking before sending.\n\ - XMR buyers must send the USPMO to the XMR seller with Delivery Confirmation.\n\ \n\ In the event mediation is necessary, or if there is a trade dispute, you will be required to send the photos to the Haveno mediator or refund agent, together with the USPMO Serial Number, Post Office Number, and dollar amount, so they can verify the details on the US Post Office website.\n\n\ Failure to provide the required information to the arbitrator will result in losing the dispute case.\n\n\ In all dispute cases, the USPMO sender bears 100% of the burden of responsibility in providing evidence/proof to the arbitrator.\n\n\ If you do not understand these requirements, do not trade using USPMO on Haveno. payment.payByMail.info=Trading using Pay by Mail on Haveno requires that you understand the following:\n\ \n\ ● XMR buyer should package cash in a tamper-evident cash bag.\n\ ● XMR buyer should film or take high-resolution photos of the cash packaging process with the address & tracking number already affixed to packaging.\n\ ● XMR buyer should send the cash package to the XMR seller with Delivery Confirmation and appropriate Insurance.\n\ ● XMR seller should film the opening of the package, making sure that the tracking number provided by the sender is visible in the video.\n\ ● Offer maker must state any special terms or conditions in the 'Additional Information' field of the payment account.\n\ ● Offer taker agrees to the offer maker's terms and conditions by taking the offer.\n\ \n\ Pay by Mail trades put the onus to act honestly squarely on both peers.\n\ \n\ ● Pay by Mail trades have less verifiable actions than other traditional trades. This makes handling dispute much harder.\n\ ● Try to resolve disputes directly with your peer using trader chat. This is your most promising route to solving any Pay by Mail dispute.\n\ ● Arbitrators can consider your case and make a suggestion, but they are NOT guaranteed to help.\n\ ● Arbitrators will make a decision based on the evidence provided to them. Therefore, please follow and document the above processes to have evidence in case of dispute.\n\ ● Reimbursement requests for any lost funds resulting from Pay By Mail trades to Haveno will NOT be considered.\n\ \n\ If you do not understand these requirements, do not trade using Pay by Mail on Haveno. payment.payByMail.contact=Contact info payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.payByMail.extraInfo.prompt=Please state on your offers: \n\n\ Country you are located (eg France); \n\ Countries / regions you would accept trades from (eg France, EU, or any European country); \n\ Any special terms/conditions; \n\ Any other details. payment.tradingRestrictions=Please review the maker's terms and conditions.\n\ If you do not meet the requirements do not take this trade. payment.cashAtAtm.info=Cardless Cash: Cardless withdraw at ATM using code\n\n\ To use this payment method:\n\n\ 1. Create a Cardless Cash payment account, listing your accepted banks, regions, or other terms to be shown with the offer.\n\n\ 2. Create or take an offer with the payment account.\n\n\ 3. When the offer is taken, chat with your peer to coordinate a time to complete the payment and share the payment details.\n\n\ If the trade amount is above the cash withdrawal limit, traders should split it into multiple transactions.\n\n\ ATM cash trades must be in multiples of 10. Using range offers is recommended so the XMR amount can adjust to match the exact price.\n\n\ If you cannot complete the transaction as specified in your trade contract, you may lose some (or all) of your security deposit. payment.cashAtAtm.extraInfo.prompt=Please state on your offers: \n\n\ Your accepted banks / locations; \n\ Any special terms/conditions; \n\ Any other details. payment.f2f.contact=Contact info payment.f2f.contact.prompt=How would you like to be contacted by the trading peer? (email address, phone number,...) payment.f2f.city=City for 'Face to face' meeting payment.f2f.city.prompt=The city will be displayed with the offer payment.shared.optionalExtra=Optional additional information payment.shared.extraInfo=Additional information payment.shared.extraInfo.offer=Additional offer information payment.shared.extraInfo.prompt.paymentAccount=Define any special terms, conditions, or details you would like to be displayed with your offers for this payment account (users will see this info before accepting offers). payment.shared.extraInfo.prompt.offer=Define any special terms, conditions, or details you would like to be displayed with your offer. payment.shared.extraInfo.noDeposit=Contact details and offer terms payment.f2f.info='Face to Face' trades have different rules and come with different risks than online transactions.\n\n\ The main differences are:\n\ ● The trading peers need to exchange information about the meeting location and time by using their provided contact details.\n\ ● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n\ ● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n\ ● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n\ ● In case of a dispute the arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence \ of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to \ an agreement.\n\n\ To be sure you fully understand the differences with 'Face to Face' trades please read the instructions and \ recommendations at: [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/F2F] payment.f2f.info.openURL=Open web page payment.f2f.offerbook.tooltip.countryAndCity=Country and city: {0} / {1} payment.shared.extraInfo.tooltip=Additional information: {0} payment.ifsc=IFS Code payment.ifsc.validation=IFSC format: XXXX0999999 payment.japan.bank=Bank payment.japan.branch=Branch payment.japan.account=Account payment.japan.recipient=Name payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your \ bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. \ Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\n\ Please see the wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Amazon_eGift_card] for further details and best practices. \n\n\ Three important notes:\n\ - try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n\ - try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat \ to tell your trading peer the reference text you picked so they can verify your payment)\n\ - Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=For your protection, we strongly discourage using Paysafecard PINs for payment.\n\n\ Transactions made via PINs cannot be independently verified for dispute resolution. If an issue arises, recovering funds may not be possible.\n\n\ To ensure transaction security with dispute resolution, always use payment methods that provide verifiable records. # We use constants from the code so we do not use our normal naming convention # dynamic values are not recognized by IntelliJ # Only translate general terms NATIONAL_BANK=National bank transfer SAME_BANK=Transfer with same bank SPECIFIC_BANKS=Transfers with specific banks US_POSTAL_MONEY_ORDER=US Postal Money Order CASH_DEPOSIT=Cash Deposit PAY_BY_MAIL=Pay By Mail CASH_AT_ATM=Cardless Cash MONEY_GRAM=MoneyGram WESTERN_UNION=Western Union F2F=Face to face (in person) JAPAN_BANK=Japan Bank Furikomi AUSTRALIA_PAYID=Australian PayID # suppress inspection "UnusedProperty" NATIONAL_BANK_SHORT=National banks # suppress inspection "UnusedProperty" SAME_BANK_SHORT=Same bank # suppress inspection "UnusedProperty" SPECIFIC_BANKS_SHORT=Specific banks # suppress inspection "UnusedProperty" US_POSTAL_MONEY_ORDER_SHORT=US Money Order # suppress inspection "UnusedProperty" CASH_DEPOSIT_SHORT=Cash Deposit # suppress inspection "UnusedProperty" PAY_BY_MAIL_SHORT=Pay By Mail # suppress inspection "UnusedProperty" CASH_AT_ATM_SHORT=Cardless Cash # suppress inspection "UnusedProperty" MONEY_GRAM_SHORT=MoneyGram # suppress inspection "UnusedProperty" WESTERN_UNION_SHORT=Western Union # suppress inspection "UnusedProperty" F2F_SHORT=F2F # suppress inspection "UnusedProperty" JAPAN_BANK_SHORT=Japan Furikomi # suppress inspection "UnusedProperty" AUSTRALIA_PAYID_SHORT=PayID # Do not translate brand names # suppress inspection "UnusedProperty" UPHOLD=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY=Popmoney # suppress inspection "UnusedProperty" REVOLUT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY=WeChat Pay # suppress inspection "UnusedProperty" SEPA=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT=SEPA Instant Payments # suppress inspection "UnusedProperty" FASTER_PAYMENTS=Faster Payments # suppress inspection "UnusedProperty" SWISH=Swish # suppress inspection "UnusedProperty" ZELLE=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS=Cryptocurrencies # suppress inspection "UnusedProperty" PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE=Wise # suppress inspection "UnusedProperty" TRANSFERWISE_USD=Wise-USD # suppress inspection "UnusedProperty" PAYSERA=Paysera # suppress inspection "UnusedProperty" PAXUM=Paxum # suppress inspection "UnusedProperty" NEFT=India/NEFT # suppress inspection "UnusedProperty" RTGS=India/RTGS # suppress inspection "UnusedProperty" IMPS=India/IMPS # suppress inspection "UnusedProperty" UPI=India/UPI # suppress inspection "UnusedProperty" PAYTM=India/PayTM # suppress inspection "UnusedProperty" NEQUI=Nequi # suppress inspection "UnusedProperty" BIZUM=Bizum # suppress inspection "UnusedProperty" PIX=Pix # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT=Cryptocurrencies Instant # suppress inspection "UnusedProperty" CAPITUAL=Capitual # suppress inspection "UnusedProperty" CELPAY=CelPay # suppress inspection "UnusedProperty" MONESE=Monese # suppress inspection "UnusedProperty" SATISPAY=Satispay # suppress inspection "UnusedProperty" TIKKIE=Tikkie # suppress inspection "UnusedProperty" VERSE=Verse # suppress inspection "UnusedProperty" STRIKE=Strike # suppress inspection "UnusedProperty" SWIFT=SWIFT International Wire Transfer # suppress inspection "UnusedProperty" ACH_TRANSFER=ACH Transfer # suppress inspection "UnusedProperty" DOMESTIC_WIRE_TRANSFER=Domestic Wire Transfer # suppress inspection "UnusedProperty" BSQ_SWAP=BSQ Swap # suppress inspection "UnusedProperty" OK_PAY=OKPay # suppress inspection "UnusedProperty" CASH_APP=Cash App # suppress inspection "UnusedProperty" VENMO=Venmo # suppress inspection "UnusedProperty" PAYPAL=PayPal # suppress inspection "UnusedProperty" PAYSAFE=Paysafe # suppress inspection "UnusedProperty" UPHOLD_SHORT=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM_SHORT=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY_SHORT=Popmoney # suppress inspection "UnusedProperty" REVOLUT_SHORT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY_SHORT=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY_SHORT=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY_SHORT=WeChat Pay # suppress inspection "UnusedProperty" SEPA_SHORT=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT_SHORT=SEPA Instant # suppress inspection "UnusedProperty" FASTER_PAYMENTS_SHORT=Faster Payments # suppress inspection "UnusedProperty" SWISH_SHORT=Swish # suppress inspection "UnusedProperty" ZELLE_SHORT=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY_SHORT=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER_SHORT=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH_SHORT=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS_SHORT=Cryptocurrencies # suppress inspection "UnusedProperty" PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" TRANSFERWISE_USD_SHORT=Wise-USD # suppress inspection "UnusedProperty" PAYSERA_SHORT=Paysera # suppress inspection "UnusedProperty" PAXUM_SHORT=Paxum # suppress inspection "UnusedProperty" NEFT_SHORT=NEFT # suppress inspection "UnusedProperty" RTGS_SHORT=RTGS # suppress inspection "UnusedProperty" IMPS_SHORT=IMPS # suppress inspection "UnusedProperty" UPI_SHORT=UPI # suppress inspection "UnusedProperty" PAYTM_SHORT=PayTM # suppress inspection "UnusedProperty" NEQUI_SHORT=Nequi # suppress inspection "UnusedProperty" BIZUM_SHORT=Bizum # suppress inspection "UnusedProperty" PIX_SHORT=Pix # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT_SHORT=Cryptocurrencies Instant # suppress inspection "UnusedProperty" CAPITUAL_SHORT=Capitual # suppress inspection "UnusedProperty" CELPAY_SHORT=CelPay # suppress inspection "UnusedProperty" MONESE_SHORT=Monese # suppress inspection "UnusedProperty" SATISPAY_SHORT=Satispay # suppress inspection "UnusedProperty" TIKKIE_SHORT=Tikkie # suppress inspection "UnusedProperty" VERSE_SHORT=Verse # suppress inspection "UnusedProperty" STRIKE_SHORT=Strike # suppress inspection "UnusedProperty" SWIFT_SHORT=SWIFT # suppress inspection "UnusedProperty" ACH_TRANSFER_SHORT=ACH # suppress inspection "UnusedProperty" DOMESTIC_WIRE_TRANSFER_SHORT=Domestic Wire # suppress inspection "UnusedProperty" BSQ_SWAP_SHORT=BSQ Swap # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY_SHORT=OKPay # suppress inspection "UnusedProperty" CASH_APP_SHORT=Cash App # suppress inspection "UnusedProperty" VENMO_SHORT=Venmo # suppress inspection "UnusedProperty" PAYPAL_SHORT=PayPal # suppress inspection "UnusedProperty" PAYSAFE_SHORT=Paysafe #################################################################### # Validation #################################################################### validation.empty=Empty input is not allowed. validation.NaN=Input is not a valid number. validation.notAnInteger=Input is not an integer value. validation.zero=Input of 0 is not allowed. validation.negative=A negative value is not allowed. validation.traditional.tooSmall=Input smaller than minimum possible amount is not allowed. validation.traditional.tooLarge=Input larger than maximum possible amount is not allowed. validation.xmr.fraction=Input will result in a monero value of less than 1 satoshi validation.xmr.tooLarge=Input larger than {0} is not allowed. validation.xmr.tooSmall=Input smaller than {0} is not allowed. validation.passwordTooShort=The password you entered is too short. It needs to have a min. of 8 characters. validation.passwordTooLong=The password you entered is too long. It cannot be longer than 50 characters. validation.sortCodeNumber={0} must consist of {1} numbers. validation.sortCodeChars={0} must consist of {1} characters. validation.bankIdNumber={0} must consist of {1} numbers. validation.accountNr=Account number must consist of {0} numbers. validation.accountNrChars=Account number must consist of {0} characters. validation.xmr.invalidAddress=The address is not correct. Please check the address format. validation.integerOnly=Please enter integer numbers only. validation.inputError=Your input caused an error:\n{0} validation.xmr.exceedsMaxTradeLimit=Your trade limit is {0}. validation.nationalAccountId={0} must consist of {1} numbers. #new validation.invalidInput=Invalid input: {0} validation.accountNrFormat=Account number must be of format: {0} # suppress inspection "UnusedProperty" validation.crypto.wrongStructure=Address validation failed because it does not match the structure of a {0} address. # suppress inspection "UnusedProperty" validation.crypto.ltz.zAddressesNotSupported=LTZ address must start with L. Addresses starting with z are not supported. # suppress inspection "UnusedProperty" validation.crypto.zAddressesNotSupported=ZEC addresses must start with t. Addresses starting with z are not supported. # suppress inspection "UnusedProperty" validation.crypto.invalidAddress=Address is not a valid {0} address! {1} # suppress inspection "UnusedProperty" validation.crypto.liquidBitcoin.invalidAddress=Native segwit addresses (those starting with 'lq') are not supported. validation.bic.invalidLength=Input length must be 8 or 11 validation.bic.letters=Bank and Country code must be letters validation.bic.invalidLocationCode=BIC contains invalid location code validation.bic.invalidBranchCode=BIC contains invalid branch code validation.bic.sepaRevolutBic=Revolut Sepa accounts are not supported. validation.btc.invalidFormat=Invalid format for a Bitcoin address. validation.email.invalidAddress=Invalid address validation.iban.invalidCountryCode=Country code invalid validation.iban.checkSumNotNumeric=Checksum must be numeric validation.iban.nonNumericChars=Non-alphanumeric character detected validation.iban.checkSumInvalid=IBAN checksum is invalid validation.iban.invalidLength=Number must have a length of 15 to 34 chars. validation.iban.sepaNotSupported=SEPA is not supported in this country validation.interacETransfer.invalidAreaCode=Non-Canadian area code validation.interacETransfer.invalidPhone=Please enter a valid 11-digit phone number (ex: 1-123-456-7890) or an email address validation.interacETransfer.invalidQuestion=Must contain only letters, numbers, spaces and/or the symbols ' _ , . ? - validation.interacETransfer.invalidAnswer=Must be one word and contain only letters, numbers, and/or the symbol - validation.inputTooLarge=Input must not be larger than {0} validation.inputTooSmall=Input has to be larger than {0} validation.inputToBeAtLeast=Input has to be at least {0} validation.amountBelowDust=An amount below the dust limit of {0} satoshi is not allowed. validation.length=Length must be between {0} and {1} validation.fixedLength=Length must be {0} validation.pattern=Input must be of format: {0} validation.noHexString=The input is not in HEX format. validation.advancedCash.invalidFormat=Must be a valid email or wallet id of format: X000000000000 validation.invalidUrl=This is not a valid URL validation.mustBeDifferent=Your input must be different from the current value validation.cannotBeChanged=Parameter cannot be changed validation.numberFormatException=Number format exception {0} validation.mustNotBeNegative=Input must not be negative validation.phone.missingCountryCode=Need two-letter country code to validate phone number validation.phone.invalidCharacters=Phone number {0} contains invalid characters validation.phone.insufficientDigits=There are not enough digits in {0} to be a valid phone number validation.phone.tooManyDigits=There are too many digits in {0} to be a valid phone number validation.phone.invalidDialingCode=Country dialing code for number {0} is invalid for country {1}. \ The correct dialing code is {2}. validation.invalidAddressList=Must be comma separated list of valid addresses validation.capitual.invalidFormat=Must be a valid CAP code of format: CAP-XXXXXX (6 alphanumeric characters) ================================================ FILE: core/src/main/resources/i18n/displayStrings_cs.properties ================================================ # Keep display strings organized by domain # Naming convention: We use camelCase and dot separated name spaces. # Use as many sub spaces as required to make the structure clear, but as little as possible. # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break # in the display but only in the editor. # Please use in all language files the exact same order of the entries, that way a comparison is easier. # Please try to keep the length of the translated string similar to English. If it is longer it might break layout or # get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. #################################################################### # Shared #################################################################### shared.readMore=Přečíst více shared.openHelp=Otevřít nápovědu shared.warning=Varování shared.close=Zavřít shared.closeAnywayDanger=Přesto vypnout (NEBEZPEČNÉ!) shared.okWait=Dobře, počkám shared.cancel=Zrušit shared.ok=OK shared.yes=Ano shared.no=Ne shared.iUnderstand=Rozumím shared.continueAnyway=Přesto pokračovat shared.na=N/A shared.shutDown=Vypnout shared.reportBug=Nahlásit chybu na GitHubu shared.buyMonero=Koupit monero shared.sellMonero=Prodat monero shared.buyCurrency=Koupit {0} shared.sellCurrency=Prodat {0} shared.buyCurrency.locked=Koupit {0} 🔒 shared.sellCurrency.locked=Prodat {0} 🔒 shared.buyingXMRWith=nakoupit XMR za {0} shared.sellingXMRFor=prodat XMR za {0} shared.buyingCurrency=nakoupit {0} (prodat XMR) shared.sellingCurrency=prodat {0} (nakoupit XMR) shared.buy=koupit shared.sell=prodat shared.buying=kupuje shared.selling=prodává shared.P2P=P2P shared.oneOffer=nabídka shared.multipleOffers=nabídky shared.Offer=Nabídka shared.offerVolumeCode={0} Objem nabídky shared.openOffers=otevřené nabídky shared.trade=obchod shared.trades=obchody shared.openTrades=otevřené obchody shared.dateTime=Datum/Čas shared.price=Cena shared.priceWithCur=Cena v {0} shared.priceInCurForCur=Cena v {0} za 1 {1} shared.fixedPriceInCurForCur=Pevná cena v {0} za 1 {1} shared.amount=Množství shared.txFee=Transakční poplatek shared.tradeFee=Obchodní poplatek shared.buyerSecurityDeposit=Vklad kauce kupujícího shared.sellerSecurityDeposit=Vklad kauce prodejce shared.amountWithCur=Množství v {0} shared.volumeWithCur=Objem v {0} shared.currency=Měna shared.market=Trh shared.deviation=Odchylka shared.paymentMethod=Platební metoda shared.tradeCurrency=Obchodní měna shared.offerType=Typ nabídky shared.details=Detaily shared.address=Adresa shared.balanceWithCur=Zůstatek v {0} shared.utxo=Nevyčerpaný transakční výstup shared.txId=ID transakce shared.confirmations=Potvrzení shared.revert=Vzít zpět Tx shared.select=Vybrat shared.usage=Použití shared.state=Stav shared.tradeId=ID obchodu shared.offerId=ID nabídky shared.traderId=ID obchodníka shared.bankName=Jméno banky shared.acceptedBanks=Přijímané banky shared.amountMinMax=Množství (min - max) shared.amountHelp=Pokud je v nabídce nastavena minimální a maximální částka, můžete obchodovat s jakoukoli částkou v tomto rozsahu. shared.remove=Odstranit shared.goTo=Přejít na {0} shared.XMRMinMax=XMR (min - max) shared.removeOffer=Odstranit nabídku shared.dontRemoveOffer=Neodstraňovat nabídku shared.editOffer=Upravit nabídku shared.openLargeQRWindow=Otevřít velké okno s QR kódem shared.chooseTradingAccount=Vyberte obchodní účet shared.faq=Navštívit stránku FAQ shared.yesCancel=Ano, zrušit shared.nextStep=Další krok shared.fundFromSavingsWalletButton=Použít prostředky z peněženky Haveno shared.fundFromExternalWalletButton=Otevřít vaši externí peněženku pro financování shared.openDefaultWalletFailed=Nepodařilo se otevřít aplikaci peněženky Monero. Jste si jisti, že máte nějakou nainstalovanou? shared.belowInPercent=% pod tržní cenou shared.aboveInPercent=% nad tržní cenou shared.enterPercentageValue=Zadejte % hodnotu shared.OR=NEBO shared.notEnoughFunds=Ve své peněžence Haveno nemáte pro tuto transakci dostatek prostředků — je potřeba {0}, ale k dispozici je pouze {1}.\n\nPřidejte prostředky z externí peněženky nebo financujte svou peněženku Haveno v části Prostředky > Přijmout prostředky. shared.waitingForFunds=Čekání na finanční prostředky... shared.yourDepositTransactionId=ID vaší vkladové transakce shared.peerDepositTransactionId=ID vkladové transakce peera shared.makerDepositTransactionId=ID vkladové transakce tvůrce shared.takerDepositTransactionId=ID vkladové transakce příjemce shared.TheXMRBuyer=XMR kupující shared.You=Vy shared.preparingConfirmation=Příprava potvrzení... shared.sendingConfirmation=Odesílání potvrzení... shared.sendingConfirmationAgain=Prosím pošlete potvrzení znovu shared.exportCSV=Exportovat do CSV shared.exportJSON=Exportovat do JSON shared.summary=Ukázat souhrn shared.noDateAvailable=Žádné datum není k dispozici shared.noDetailsAvailable=Detaily nejsou k dispozici shared.notUsedYet=Ještě nepoužito shared.date=Datum shared.sendFundsDetailsWithFee=Odesílání: {0}\n\nNa přijímací adresu: {1}\n\nDalší poplatek pro těžaře: {2}\n\nJste si jisti, že chcete vyplatit tuto částku? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno zjistil, že tato transakce by vytvořila drobné mince, které jsou pod limitem drobných mincí (a není to povoleno pravidly pro konsenzus Monero). Místo toho budou tyto drobné mince ({0} satoshi {1}) přidány k poplatku za těžbu.\n\n\n shared.copyToClipboard=Kopírovat do schránky shared.copiedToClipboard=Zkopírováno do schránky! shared.language=Jazyk shared.country=Země shared.applyAndShutDown=Potvrdit a ukončit shared.selectPaymentMethod=Vybrat platební metodu shared.accountNameAlreadyUsed=Toto jméno účtu je již použito pro jiný účet.\nPoužijte prosím jiné jméno. shared.askConfirmDeleteAccount=Skutečně chcete smazat vybraný účet? shared.cannotDeleteAccount=Tento účet nemůžete smazat, protože je použit v otevřené nabídce (nebo v otevřeném obchodě). shared.noAccountsSetupYet=Ještě nejsou nastaveny žádné účty shared.manageAccounts=Spravovat účty shared.addNewAccount=Přidat nový účet shared.ExportAccounts=Exportovat účty shared.importAccounts=Importovat účty shared.createNewAccount=Vytvořit nový účet shared.createNewAccountDescription=Vaše údaje o účtu jsou uloženy místně na vašem zařízení a sdíleny pouze s vaším obchodním partnerem a rozhodcem, pokud dojde k otevření sporu. shared.saveNewAccount=Uložit nový účet shared.selectedAccount=Vybraný účet shared.deleteAccount=Smazat účet shared.errorMessageInline=\nChybová zpráva {0} shared.errorMessage=Chybová zpráva shared.information=Informace shared.name=Jméno shared.id=ID shared.dashboard=Tabule shared.accept=Přijmout shared.balance=Zůstatek shared.save=Uložit shared.onionAddress=Onion adresa shared.supportTicket=úkol pro podporu shared.dispute=spor shared.mediationCase=mediační případ shared.seller=prodejce shared.buyer=kupující shared.allEuroCountries=Všechny Euro země shared.acceptedTakerCountries=Země příjemce akceptovány shared.tradePrice=Tržní cena shared.tradeAmount=Výše obchodu shared.tradeVolume=Objem obchodu shared.reservedAmount=Rezervovaná částka shared.invalidKey=Vložený klíč není správný shared.enterPrivKey=Pro odemknutí vložte privátní klíč shared.payoutTxId=ID platební transakce shared.contractAsJson=Kontakt v JSON formátu shared.viewContractAsJson=Zobrazit kontrakt v JSON formátu shared.contract.title=Kontrakt pro obchod s ID: {0} shared.paymentDetails=XMR {0} detaily platby shared.securityDeposit=Kauce shared.yourSecurityDeposit=Vaše kauce shared.contract=Kontrakt shared.messageArrived=Zpráva dorazila. shared.messageStoredInMailbox=Zpráva uložena ve schránce shared.messageSendingFailed=Odeslání zprávy selhalo. Chyba: {0} shared.unlock=Odemknout shared.toReceive=bude přijata shared.toSpend=bude utracena shared.xmrAmount=Částka XMR shared.yourLanguage=Vaše jazyky shared.addLanguage=Přidat jazyk shared.total=Celkem shared.totalsNeeded=Potřebné prostředky shared.tradeWalletAddress=Adresa obchodní peněženky shared.tradeWalletBalance=Zůstatek obchodní peněženky shared.reserveExactAmount=Rezervujte pouze nezbytné prostředky. Vyžaduje poplatek za těžbu a přibližně 20 minut, než vaše nabídka půjde živě. shared.makerTxFee=Tvůrce: {0} shared.takerTxFee=Příjemce: {0} shared.iConfirm=Potvrzuji shared.openURL=Otevřít {0} shared.fiat=Fiat shared.crypto=Krypto shared.traditional=Tradiční shared.otherAssets=jiná aktiva shared.other=Jiné shared.preciousMetals=Drahé kovy shared.all=Vše shared.edit=Upravit shared.advancedOptions=Pokročilé možnosti shared.interval=Interval shared.actions=Akce shared.buyerUpperCase=Kupující shared.sellerUpperCase=Prodejce shared.new=NOVÝ shared.learnMore=Zjistit více shared.dismiss=Zavřít shared.selectedArbitrator=Zvolený rozhodce shared.selectedMediator=Zvolený mediátor shared.selectedRefundAgent=Zvolený rozhodce shared.mediator=Mediátor shared.arbitrator=Rozhodce shared.refundAgent=Rozhodce shared.refundAgentForSupportStaff=Rozhodce pro vrácení peněz shared.delayedPayoutTxId=ID zpožděné platební transakce shared.delayedPayoutTxReceiverAddress=Zpožděná výplatní transakce odeslána na shared.unconfirmedTransactionsLimitReached=Momentálně máte příliš mnoho nepotvrzených transakcí. Prosím zkuste to znovu později. shared.numItemsLabel=Počet položek: {0} shared.filter=Filtr shared.enabled=Aktivní shared.pending=Otevřené shared.me=Já shared.maker=Tvůrce shared.taker=Příjemce shared.none=Nic #################################################################### # UI views #################################################################### #################################################################### # MainView #################################################################### mainView.menu.market=Trh mainView.menu.buyXmr=Koupit XMR mainView.menu.sellXmr=Prodat XMR mainView.menu.portfolio=Portfolio mainView.menu.funds=Prostředky mainView.menu.support=Podpora mainView.menu.settings=Nastavení mainView.menu.account=Účet mainView.marketPriceWithProvider.label=Tržní cena {0} mainView.marketPrice.havenoInternalPrice=Cena posledního Haveno obchodu mainView.marketPrice.tooltip.havenoInternalPrice=Neexistují tržní ceny od externích poskytovatelů cenových feedů.\n\ Zobrazená cena je nejnovější obchodní cena Haveno pro tuto měnu. mainView.marketPrice.tooltip=Tržní cena je poskytována {0}{1}\nPoslední aktualizace: {2}\nURL uzlu poskytovatele: {3} mainView.balance.available=Dostupný zůstatek mainView.balance.reserved=Rezervováno v nabídkách mainView.balance.pending=Zamčené v obchodech mainView.balance.reserved.short=Rezervováno mainView.balance.pending.short=Zamčeno mainView.footer.usingTor=(přes Tor) mainView.footer.localhostMoneroNode=(localhost) mainView.footer.clearnet=(přes clearnet) mainView.footer.xmrInfo={0} {1} mainView.footer.xmrFeeRate=/ Míra poplatku: {0} sat/vB mainView.footer.xmrInfo.initializing=Připojování k síti Haveno mainView.footer.xmrInfo.synchronizingWith=Synchronizace s {0} na bloku: {1} / {2} mainView.footer.xmrInfo.connectedTo=Připojeno k {0} v bloku {1} mainView.footer.xmrInfo.synchronizingWalletWith=Synchronizace peněženky s {0} na bloku: {1} / {2} mainView.footer.xmrInfo.syncedWith=Synchronizováno s {0} na bloku {1} mainView.footer.xmrInfo.connectingTo=Připojování mainView.footer.xmrInfo.connectionFailed=Připojení se nezdařilo mainView.footer.xmrPeers=Monero síťové uzly: {0} mainView.footer.p2pPeers=Haveno síťové uzly: {0} mainView.bootstrapState.connectionToTorNetwork=(1/4) Připojování do sítě Tor... mainView.bootstrapState.torNodeCreated=(2/4) Tor uzel vytvořen mainView.bootstrapState.hiddenServicePublished=(3/4) Skrytá služba zveřejněna mainView.bootstrapState.initialDataReceived=(4/4) Iniciační data přijata mainView.bootstrapWarning.noSeedNodesAvailable=Žádné seed uzly nejsou k dispozici mainView.bootstrapWarning.noNodesAvailable=Žádné seed ani peer uzly k dispozici mainView.bootstrapWarning.bootstrappingToP2PFailed=Zavádění do sítě Haveno se nezdařilo mainView.p2pNetworkWarnMsg.noNodesAvailable=Pro vyžádání dat nejsou k dispozici žádné seed ani peer uzly.\nZkontrolujte připojení k internetu nebo zkuste aplikaci restartovat. mainView.p2pNetworkWarnMsg.connectionToP2PFailed=Připojení k síti Haveno selhalo (nahlášená chyba: {0}).\nZkontrolujte připojení k internetu nebo zkuste aplikaci restartovat. mainView.walletServiceErrorMsg.timeout=Připojení k síti Monero selhalo kvůli vypršení časového limitu. mainView.walletServiceErrorMsg.connectionError=Připojení k síti Monero selhalo kvůli chybě {0} mainView.walletServiceErrorMsg.rejectedTxException=Transakce byla ze sítě zamítnuta.\n\n{0} mainView.networkWarning.allConnectionsLost=Ztratili jste připojení ke všem {0} síťovým peer uzlům.\nMožná jste ztratili připojení k internetu nebo byl váš počítač v pohotovostním režimu. mainView.networkWarning.localhostMoneroLost=Ztratili jste připojení k localhost uzlu Monero.\nRestartujte aplikaci Haveno a připojte se k jiným uzlům Monero nebo restartujte localhost Monero uzel. mainView.version.update=(Dostupná aktualizace) mainView.status.connections=Příchozí připojení: {0}nOdchozí připojení: {1} #################################################################### # MarketView #################################################################### market.tabs.offerBook=Seznam nabídek market.tabs.spreadCurrency=Nabídky podle měn market.tabs.spreadPayment=Nabídky podle metod platby market.tabs.trades=Obchody # OfferBookChartView market.offerBook.sellOffersHeaderLabel=Prodat {0} kupujícímu market.offerBook.buyOffersHeaderLabel=Koupit {0} od prodejce market.offerBook.buy=Chci koupit monero market.offerBook.sell=Chci prodat monero # SpreadView market.spread.numberOfOffersColumn=Všechny nabídky ({0}) market.spread.numberOfBuyOffersColumn=Koupit XMR ({0}) market.spread.numberOfSellOffersColumn=Prodat XMR ({0}) market.spread.totalAmountColumn=Celkem XMR ({0}) market.spread.spreadColumn=Rozptyl market.spread.expanded=Rozbalit # TradesChartsView market.trades.nrOfTrades=Obchodů: {0} market.trades.tooltip.volumeBar=Objem: {0} / {1}\nPočet obchodů: {2}\nDatum: {3} market.trades.tooltip.candle.open=Otevřené: market.trades.tooltip.candle.close=Zavřít: market.trades.tooltip.candle.high=Nejvyšší: market.trades.tooltip.candle.low=Nejnižší: market.trades.tooltip.candle.average=Průměr: market.trades.tooltip.candle.median=Medián: market.trades.tooltip.candle.date=Datum: market.trades.showVolumeInUSD=Zobrazit objem v USD #################################################################### # OfferView #################################################################### offerbook.createOffer=Vytvořit nabídku offerbook.takeOffer=Přijmout nabídku offerbook.takeOffer.createAccount=Vytvořit účet a přijmout nabídku offerbook.takeOffer.enterChallenge=Zadejte heslo nabídky offerbook.trader=Obchodník offerbook.offerersBankId=ID banky tvůrce (BIC/SWIFT): {0} offerbook.offerersBankName=Jméno banky tvůrce: {0} offerbook.offerersBankSeat=Sídlo banky tvůrce: {0} offerbook.offerersAcceptedBankSeatsEuro=Přijatá sídla bank (příjemce): Všechny země Eura offerbook.offerersAcceptedBankSeats=Přijatá sídla bank (příjemce):\n {0} offerbook.availableOffersToBuy=Koupit {0} pomocí {1} offerbook.availableOffersToSell=Prodat {0} za {1} offerbook.filterByCurrency=Filtrovat podle měny offerbook.filterByPaymentMethod=Filtrovat podle platební metody offerbook.matchingOffers=Nabídky odpovídající mým účtům offerbook.filterNoDeposit=Žádný vklad offerbook.noDepositOffers=Nabídky bez zálohy (vyžaduje se heslo) offerbook.timeSinceSigning=Informace o účtu offerbook.timeSinceSigning.info.arbitrator=podepsán rozhodcem a může podepisovat účty partnerů offerbook.timeSinceSigning.info.peer=podepsáno partnerem, nyní čeká ještě %d dnů na zrušení limitů offerbook.timeSinceSigning.info.peerLimitLifted=podepsán partnerem a limity byly zrušeny offerbook.timeSinceSigning.info.signer=podepsán partnerem a může podepsat účty partnera (pro zrušení limitů) offerbook.timeSinceSigning.info.banned=účet byl zablokován offerbook.timeSinceSigning.daysSinceSigning={0} dní offerbook.timeSinceSigning.daysSinceSigning.long={0} od podpisu offerbook.timeSinceSigning.tooltip.accountLimit=Limit účtu: {0} offerbook.timeSinceSigning.tooltip.accountLimitLifted=Limit účtu odebrán offerbook.timeSinceSigning.tooltip.info.unsigned=Tento účet ještě nebyl podepsán offerbook.timeSinceSigning.tooltip.info.signed=Tento účet byl podepsán offerbook.timeSinceSigning.tooltip.info.signedAndLifted=Tento účet byl podepsán a může podepisovat účty peerů offerbook.timeSinceSigning.tooltip.checkmark.buyXmr=nakoupeno XMR od podepsaného účtu offerbook.timeSinceSigning.tooltip.checkmark.wait=čekáno alespoň {0} dnů offerbook.timeSinceSigning.tooltip.learnMore=Více informací offerbook.xmrAutoConf=Je automatické potvrzení povoleno offerbook.buyXmrWith=Koupit XMR za: offerbook.sellXmrFor=Prodat XMR za: offerbook.cloneOffer=Klonovat nabídku se sdílenými prostředky offerbook.clonedOffer.tooltip=Toto je klonovaná nabídka se sdílenými prostředky.\n\ID skupiny: {0} offerbook.nonClonedOffer.tooltip=Běžná nabídka bez sdílených prostředků.\n\ID rezervní transakce tvůrce: {0} offerbook.hasConflictingClone.warning=Tato klonovaná nabídka se sdílenými prostředky nemůže být aktivována, protože používá \ stejnou platební metodu a měnu, jako jiná aktivní nabídka.\n\n\ Je potřeba, abyste upravili svou nabídku a změnili metodu platby nebo měnu, nebo abyste deaktivovali nabídku, která má stejnou metodu platby a měnu. offerbook.cannotActivateEditedOffer.warning=Nemůžete aktivovat nabídku, která je právě upravována. offerbook.clonedOffer.headline=Klonování nabídky offerbook.clonedOffer.info=Klonování nabídky vytvoří kopii bez rezervování dalších prostředků.\n\n\ Toto pomůže snížit množství prostředků, které jsou uzamčené, což usnadní přidání jedné nabídky na více trhů nebo platebních metod.\n\n\ Pokud jedna z klonovaných nabídek je někým přijata, ostatní shodné nabídky budou automaticky uzavřeny, protože všechny sdílí stejné rezervované prostředky.\n\n\ Klonované nabídky musí sdílet stejnou obchodovanou částku a vklad a lišit se od sebe platební metodou a měnou.\n\n\ Pro další informace o klonování nabídek se podívejte na: [HYPERLINK:https://docs.haveno.exchange/users/haveno-ui/cloning_an_offer/] offerbook.timeSinceSigning.help=Když úspěšně dokončíte obchod s uživatelem, který má podepsaný platební účet, je váš platební účet podepsán.\n\ {0} dní později se počáteční limit {1} zruší a váš účet může podepisovat platební účty ostatních uživatelů. offerbook.timeSinceSigning.notSigned=Dosud nepodepsáno offerbook.timeSinceSigning.notSigned.ageDays={0} dní offerbook.timeSinceSigning.notSigned.noNeed=N/A shared.notSigned.noNeedDays=Tento typ účtu nevyžaduje podepisování a byl vytvořen před {0} dny shared.notSigned.noNeedAlts=Kryptoměnové účty neprocházejí kontrolou podpisu a stáří offerbook.nrOffers=Počet nabídek: {0} offerbook.volume={0} (min - max) offerbook.volumeTotal={0} {1} offerbook.deposit=Kauce XMR (%) offerbook.deposit.help=Kauce zaplacená každým obchodníkem k zajištění obchodu. Bude vrácena po dokončení obchodu. offerbook.XMRTotal=XMR ({0}) offerbook.createNewOffer=Vytvořit nabídku pro {0} {1} offerbook.createOfferDisabled.tooltip=Můžete vytvořit zároveň jen jednu nabídku offerbook.takeOfferButton.tooltip=Využít nabídku {0} offerbook.setupNewAccount=Založit nový obchodní účet offerbook.removeOffer.success=Odebrání nabídky bylo úspěšné. offerbook.removeOffer.failed=Odebrání nabídky selhalo:\n{0} offerbook.deactivateOffer.failed=Deaktivace nabídky se nezdařila:\n{0} offerbook.activateOffer.failed=Zveřejnění nabídky se nezdařilo:\n{0} offerbook.withdrawFundsHint=Nabídka byla odebrána. Prostředky již nejsou pro tuto nabídku rezervovány. \n Můžete odeslat Dostupné prostředky do externí peněženky na obrazovce {0}. offerbook.warning.noTradingAccountForCurrency.headline=Žádný platební účet pro vybranou měnu offerbook.warning.noTradingAccountForCurrency.msg=Pro vybranou měnu nemáte nastavený platební účet.\n\nChcete místo toho vytvořit nabídku pro jinou měnu? offerbook.warning.noMatchingAccount.headline=Žádný odpovídající platební účet. offerbook.warning.noMatchingAccount.msg=Tato nabídka používá platební metodu, kterou jste dosud nenastavili.\n\nChcete nyní založit nový platební účet? offerbook.warning.counterpartyTradeRestrictions=Tuto nabídku nelze přijmout z důvodu obchodních omezení protistrany offerbook.warning.newVersionAnnouncement=S touto verzí softwaru mohou obchodní partneři navzájem ověřovat a podepisovat platební účty ostatních a vytvářet tak síť důvěryhodných platebních účtů.\n\n\ Po úspěšném obchodování s partnerským účtem s ověřeným platebním účtem bude váš platební účet podepsán a obchodní limity budou zrušeny po určitém časovém intervalu (délka tohoto intervalu závisí na způsobu ověření).\n\n\ Další informace o podepsání účtu naleznete v dokumentaci na adrese [HYPERLINK:https://docs.haveno.exchange/overview/account_limits/#account-signing]. popup.warning.tradeLimitDueAccountAgeRestriction.seller=Povolená částka obchodu je omezena na {0} z důvodu bezpečnostních omezení na základě následujících kritérií:\n\ - Účet kupujícího nebyl podepsán rozhodcem ani obchodním partnerem\n\ - Doba od podpisu účtu kupujícího není alespoň 30 dní\n\ - Způsob platby této nabídky je považován za riskantní pro bankovní zpětné zúčtování\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=Povolená částka obchodu je omezena na {0} z důvodu bezpečnostních omezení na základě následujících kritérií:\n\ - Váš účet nebyl podepsán rozhodcem ani obchodním partnerem\n\ - Čas od podpisu vašeho účtu není alespoň 30 dní\n\ - Způsob platby této nabídky je považován za riskantní pro bankovní zpětné zúčtování\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=Tento platební metoda je dočasně omezena na {0} do {1}, protože všichni kupující mají nové účty.\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=Vaše nabídka bude omezena na kupující s podepsanými a starými účty, protože překračuje {0}.\n\n{1} offerbook.warning.wrongTradeProtocol=Tato nabídka vyžaduje jinou verzi protokolu než ta, která byla použita ve vaší verzi softwaru.\n\nZkontrolujte, zda máte nainstalovanou nejnovější verzi, jinak uživatel, který nabídku vytvořil, použil starší verzi.\n\nUživatelé nemohou obchodovat s nekompatibilní verzí obchodního protokolu. offerbook.warning.userIgnored=Do seznamu ignorovaných uživatelů jste přidali onion adresu tohoto uživatele. offerbook.warning.offerBlocked=Tato nabídka byla blokována vývojáři Haveno.\nPravděpodobně existuje neošetřená chyba způsobující problémy při přijetí této nabídky. offerbook.warning.currencyBanned=Měna použitá v této nabídce byla blokována vývojáři Haveno.\nDalší informace naleznete na fóru Haveno. offerbook.warning.paymentMethodBanned=Vývojáři Haveno zablokovali platební metodu použitou v této nabídce.\nDalší informace naleznete na fóru Haveno. offerbook.warning.nodeBlocked=Onion adresa tohoto obchodníka byla zablokována vývojáři Haveno.\nPravděpodobně existuje neošetřená chyba způsobující problémy při přijímání nabídek od tohoto obchodníka. offerbook.warning.requireUpdateToNewVersion=Vaše verze Haveno již není kompatibilní pro obchodování. Aktualizujte prosím na nejnovější verzi Haveno. offerbook.warning.offerWasAlreadyUsedInTrade=Tuto nabídku nemůžete přijmout, protože jste ji již dříve využili. \ Je možné, že váš předchozí pokus o přijetí nabídky vyústil v neúspěšný obchod. offerbook.warning.arbitratorNotValidated=Tuto nabídku nelze přijmout, protože rozhodce není registrován. offerbook.warning.signatureNotValidated=Tuto nabídku nelze přijmout, protože rozhodce má neplatný podpis offerbook.warning.reserveFundsSpent=Tuto nabídku nelze přijmout, protože rezervované prostředky byly již utraceny. offerbook.info.sellAtMarketPrice=Budete prodávat za tržní cenu (aktualizováno každou minutu). offerbook.info.buyAtMarketPrice=Budete nakupovat za tržní cenu (aktualizováno každou minutu). offerbook.info.sellBelowMarketPrice=Získáte o {0} méně než je aktuální tržní cena (aktualizováno každou minutu). offerbook.info.buyAboveMarketPrice=Platíte o {0} více, než je aktuální tržní cena (aktualizováno každou minutu). offerbook.info.sellAboveMarketPrice=Získáte o {0} více, než je aktuální tržní cena (aktualizováno každou minutu). offerbook.info.buyBelowMarketPrice=Platíte o {0} méně, než je aktuální tržní cena (aktualizováno každou minutu). offerbook.info.buyAtFixedPrice=Budete nakupovat za tuto pevnou cenu. offerbook.info.sellAtFixedPrice=Budete prodávat za tuto pevnou cenu. offerbook.info.roundedFiatVolume=Částka byla zaokrouhlena, aby se zvýšilo soukromí vašeho obchodu. #################################################################### # Offerbook / Create offer #################################################################### createOffer.amount.prompt=Zadejte množství v XMR createOffer.price.prompt=Zadejte cenu createOffer.volume.prompt=Zadejte množství v {0} createOffer.amountPriceBox.amountDescription=Množství XMR, které chcete {0} createOffer.amountPriceBox.buy.volumeDescription=Množství {0}, které odešlete createOffer.amountPriceBox.sell.volumeDescription=Množství {0}, které přijmete createOffer.amountPriceBox.minAmountDescription=Minimální množství XMR createOffer.securityDeposit.prompt=Kauce createOffer.fundsBox.title=Financovat nabídku createOffer.fundsBox.offerFee=Obchodní poplatek createOffer.fundsBox.networkFee=Poplatek za těžbu createOffer.fundsBox.placeOfferSpinnerInfo=Probíhá zveřejnění nabídky ... createOffer.fundsBox.paymentLabel=Haveno obchod s ID {0} createOffer.fundsBox.fundsStructure=(kauce {0}, obchodní poplatek {1}, poplatek za těžbu {2}) createOffer.success.headline=Vaše nabídka byla vytvořena createOffer.success.info=Otevřené nabídky můžete spravovat na stránce \"Portfolio/Moje otevřené nabídky\". createOffer.info.sellAtMarketPrice=Vždy budete prodávat za tržní cenu, protože cena vaší nabídky bude průběžně aktualizována. createOffer.info.buyAtMarketPrice=Vždy budete nakupovat za tržní cenu, protože cena vaší nabídky bude průběžně aktualizována. createOffer.info.sellAboveMarketPrice=Vždy získáte o {0} % více, než je aktuální tržní cena, protože cena vaší nabídky bude průběžně aktualizována. createOffer.info.buyBelowMarketPrice=Vždy zaplatíte o {0} % méně, než je aktuální tržní cena, protože cena vaší nabídky bude průběžně aktualizována. createOffer.warning.sellBelowMarketPrice=Vždy získáte o {0} % méně, než je aktuální tržní cena, protože cena vaší nabídky bude průběžně aktualizována. createOffer.warning.buyAboveMarketPrice=Vždy zaplatíte o {0} % více, než je aktuální tržní cena, protože cena vaší nabídky bude průběžně aktualizována. createOffer.tradeFee.descriptionXMROnly=Obchodní poplatek createOffer.tradeFee.description=Obchodní poplatek createOffer.triggerPrice.prompt=Nepovinná limitní cena createOffer.triggerPrice.label=Deaktivovat nabídku, pokud tržní cena dosáhne {0} createOffer.triggerPrice.tooltip=Abyste se ochránili před prudkými výkyvy tržních cen, můžete nastavit limitní cenu, \ po jejímž dosažení bude vaše nabídka stažena. createOffer.triggerPrice.invalid.tooLow=Hodnota musí být vyšší než {0} createOffer.triggerPrice.invalid.tooHigh=Hodnota musí být nižší než {0} createOffer.extraInfo.invalid.tooLong=Nesmí překročit {0} znaků. # new entries createOffer.placeOfferButton.buy=Zkontrolovat: Vytvořit nabídku na nákup XMR za {0} createOffer.placeOfferButton.sell=Zkontrolovat: Vytvořit nabídku na prodej XMR za {0} createOffer.createOfferFundWalletInfo.headline=Financovat nabídku # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Výše obchodu: {0}\n createOffer.createOfferFundWalletInfo.msg=Potřebujete vložit {0} do této nabídky.\n\n\ Tyto prostředky jsou rezervovány ve vaší místní peněžence a budou zablokovány v multisig peněžence, jakmile někdo přijme vaši nabídku.\n\n\ Částka je součtem:\n\ {1}\ - Vaše záloha: {2}\n\ - Poplatek za obchodování: {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=Při zadávání nabídky došlo k chybě:\n\n{0}\n\n Peněženku ještě neopustily žádné finanční prostředky.\n\ Restartujte aplikaci a zkontrolujte síťové připojení. createOffer.setAmountPrice=Nastavit množství a cenu createOffer.warnCancelOffer=Tuto nabídku jste již financovali. Pokud ji nyní zrušíte, zůstanou vaše prostředky v místní peněžence Haveno a budou k dispozici pro výběr na obrazovce \"Prostředky/Odeslat prostředky\". Opravdu si přejete zrušit? createOffer.timeoutAtPublishing=Při zveřejnění nabídky došlo k vypršení časového limitu. createOffer.errorInfo=\n\nTvůrčí poplatek je již zaplacen. V nejhorším případě jste tento poplatek ztratili.\nZkuste prosím restartovat aplikaci a zkontrolovat síťové připojení, abyste zjistili, zda můžete problém vyřešit. createOffer.tooLowSecDeposit.warning=Nastavili jste kauci na nižší hodnotu, než je doporučená výchozí hodnota {0}.\n Opravdu chcete použít nižší kauci? createOffer.tooLowSecDeposit.makerIsSeller=Poskytuje vám to menší ochranu v případě, že obchodní partner nedodrží obchodní protokol. createOffer.tooLowSecDeposit.makerIsBuyer=Obchodní partner bude mít menší jistotu, že dodržíte obchodní protokol, protože uložená kauce bude příliš nízká. \ Ostatní uživatelé mohou raději využít jiné nabídky než té vaší. createOffer.resetToDefault=Ne, restartovat na výchozí hodnotu createOffer.useLowerValue=Ano, použijte moji nižší hodnotu createOffer.priceOutSideOfDeviation=Cena, kterou jste zadali, je mimo max. povolenou odchylku od tržní ceny.\nMax. povolená odchylka je {0} a lze ji upravit v preferencích. createOffer.changePrice=Změnit cenu createOffer.tac=Zveřejněním této nabídky souhlasím s obchodováním s jakýmkoli obchodníkem, který splňuje podmínky definované na této obrazovce. createOffer.setDeposit=Nastavit kauci kupujícího (%) createOffer.setDepositAsBuyer=Nastavit mou kauci jako kupujícího (%) createOffer.setDepositForBothTraders=Nastavit kauci obou obchodníků (%) createOffer.securityDepositInfo=Vaše kauce kupujícího bude {0} createOffer.securityDepositInfoAsBuyer=Vaše kauce jako kupující bude {0} createOffer.minSecurityDepositUsed=Minimální bezpečnostní záloha je použita createOffer.buyerAsTakerWithoutDeposit=Žádný vklad od kupujícího (chráněno heslem) createOffer.myDeposit=Můj bezpečnostní vklad (%) createOffer.myDepositInfo=Vaše záloha na bezpečnost bude {0} #################################################################### # Offerbook / Take offer #################################################################### takeOffer.amount.prompt=Vložte množství v XMR takeOffer.amountPriceBox.buy.amountDescription=Množství XMR na prodej takeOffer.amountPriceBox.sell.amountDescription=Množství XMR k nakoupení takeOffer.amountPriceBox.buy.amountDescriptionCrypto=Množství XMR na prodej takeOffer.amountPriceBox.sell.amountDescriptionCrypto=Množství XMR k nakoupení takeOffer.amountPriceBox.priceDescription=Cena za monero v {0} takeOffer.amountPriceBox.amountRangeDescription=Možný rozsah množství takeOffer.amountPriceBox.warning.invalidXmrDecimalPlaces=Částka, kterou jste zadali, přesahuje počet povolených desetinných míst.\nČástka byla upravena na 4 desetinná místa. takeOffer.validation.amountSmallerThanMinAmount=Částka nesmí být menší než minimální částka stanovená v nabídce. takeOffer.validation.amountLargerThanOfferAmount=Vstupní částka nesmí být vyšší než částka stanovená v nabídce. takeOffer.validation.amountLargerThanOfferAmountMinusFee=Toto vstupní množství by vytvořilo zanedbatelné drobné pro prodejce XMR. takeOffer.fundsBox.title=Financovat obchod takeOffer.fundsBox.isOfferAvailable=Kontroluje se, zda je nabídka k dispozici ... takeOffer.fundsBox.tradeAmount=Částka k prodeji takeOffer.fundsBox.offerFee=Obchodní poplatek takeOffer.fundsBox.networkFee=Celkové poplatky za těžbu takeOffer.fundsBox.takeOfferSpinnerInfo=Přijímám nabídku: {0} takeOffer.fundsBox.paymentLabel=Haveno obchod s ID {0} takeOffer.fundsBox.fundsStructure=(kauce {0}, obchodní poplatek {1}, poplatek za těžbu {2}) takeOffer.fundsBox.noFundingRequiredTitle=Žádné financování požadováno takeOffer.fundsBox.noFundingRequiredDescription=Získejte passphrase nabídky od prodávajícího mimo Haveno, abyste tuto nabídku přijali. takeOffer.success.headline=Úspěšně jste přijali nabídku. takeOffer.success.info=Stav vašeho obchodu můžete vidět v \"Portfolio/Otevřené obchody\". takeOffer.error.message=Při převzetí nabídky došlo k chybě.\n\n{0} # new entries takeOffer.takeOfferButton.buy=Zkontrolovat: Přijmout nabídku na nákup XMR za {0} takeOffer.takeOfferButton.sell=Zkontrolovat: Přijmout nabídku na prodej XMR za {0} takeOffer.noPriceFeedAvailable=Tuto nabídku nemůžete vzít, protože používá procentuální cenu založenou na tržní ceně, ale není k dispozici žádný zdroj cen. takeOffer.takeOfferFundWalletInfo.headline=Financovat obchod # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Výše obchodu: {0} \n takeOffer.takeOfferFundWalletInfo.msg=Abyste mohli tuto nabídku využít, musíte vložit {0}.\n\nČástka je součtem:\n{1} - Vaší kauce: {2}\n- Obchodního poplatku: {3}\n- Celkového poplatku za těžbu: {4}\n\nPři financování obchodu si můžete vybrat ze dvou možností:\n- Použijte svou peněženku Haveno (pohodlné, ale transakce mohou být propojitelné) NEBO\n- Platba z externí peněženky (potenciálně více soukromé)\n\nPo uzavření tohoto vyskakovacího okna se zobrazí všechny možnosti a podrobnosti financování. takeOffer.alreadyPaidInFunds=Pokud jste již prostředky zaplatili, můžete je vybrat na obrazovce \"Prostředky/Odeslat prostředky\". takeOffer.setAmountPrice=Nastavit částku takeOffer.alreadyFunded.askCancel=Tuto nabídku jste již financovali. Pokud ji nyní zrušíte, zůstanou vaše prostředky v místní peněžence Haveno a budou k dispozici pro výběr na obrazovce \"Prostředky/Odeslat prostředky\". Opravdu si přejete zrušit? takeOffer.failed.offerNotAvailable=Žádost o nabídku se nezdařila, protože nabídka již není k dispozici. Možná, že mezitím nabídku přijal jiný obchodník. takeOffer.failed.offerTaken=Tuto nabídku nemůžete přijmout, protože ji již přijal jiný obchodník. takeOffer.failed.offerInvalid=Tuto nabídku nemůžete přijmout, protože podpis tvůrce je neplatný. takeOffer.failed.offerRemoved=Tuto nabídku nemůžete přijmout, protože mezitím byla nabídka odstraněna. takeOffer.failed.offererNotOnline=Přijetí nabídky se nezdařilo, protože tvůrce již není online. takeOffer.failed.offererOffline=Tuto nabídku nemůžete přijmout, protože je tvůrce offline. takeOffer.warning.connectionToPeerLost=Ztratili jste spojení s tvůrcem.\nMohli odejít do režimu offline nebo s vámi ukončili připojení kvůli příliš velkému počtu otevřených připojení.\n\nPokud stále jeho nabídku vidíte v seznamu nabídek, můžete zkusit nabídku znovu využít. takeOffer.error.noFundsLost=\n\nPeněženku ještě neopustily žádné finanční prostředky.\nZkuste prosím restartovat aplikaci a zkontrolovat síťové připojení, abyste zjistili, zda můžete problém vyřešit. # suppress inspection "TrailingSpacesInProperty" takeOffer.error.feePaid=\n\n takeOffer.error.depositPublished=\n\nVkladová transakce je již zveřejněna.\nZkuste prosím restartovat aplikaci a zkontrolovat síťové připojení, abyste zjistili, zda můžete problém vyřešit.\nPokud problém přetrvává, kontaktujte vývojáře a požádejte je o podporu. takeOffer.error.payoutPublished=\n\nVyplacená transakce je již zveřejněna.\nZkuste prosím restartovat aplikaci a zkontrolovat síťové připojení, abyste zjistili, zda můžete problém vyřešit.\nPokud problém přetrvává, kontaktujte vývojáře a požádejte je o podporu. takeOffer.tac=Přijetím této nabídky souhlasím s obchodními podmínkami definovanými na této obrazovce. #################################################################### # Offerbook / Edit offer #################################################################### openOffer.header.triggerPrice=Limitní cena openOffer.header.groupId=ID skupiny openOffer.triggerPrice=Limitní cena {0} openOffer.triggered=Nabídka byla deaktivována, protože tržní cena dosáhla vámi stanovené limitní ceny.\n\ Prosím nastavte novou limitní cenu ve vaší nabídce editOffer.setPrice=Nastavit cenu editOffer.confirmEdit=Potvrdit: Upravit nabídku editOffer.publishOffer=Zveřejnění vaší nabídky. editOffer.failed=Úprava nabídky se nezdařila:\n{0} editOffer.success=Vaše nabídka byla úspěšně upravena. editOffer.invalidDeposit=Kauce kupujícího není v rámci omezení definovaných Haveno DAO a nemůže být dále upravována. editOffer.openTabWarning=Již máte otevřenou kartu \"Upravit nabídku\". editOffer.hasConflictingClone=Upravili jste nabídku, která využívá sdílené financování s jinou nabídkou a vaše úprava \ způsobila, že platební metoda a měna jsou nyní shodné s jinou aktivní klonovanou nabídkou. Vaše upravovaná nabídka bude \ deaktivována protože není povoleno zveřejnit 2 nabídky, které sdílí prostředky a stejnou platební metodu \ a měnu.\n\n\ Můžete nabídku znovu upravit na \"Portfolio/Moje otevřené nabídky\" a splnit podmínky pro její aktivaci. cloneOffer.clone=Klonovat nabídku cloneOffer.publishOffer=Zveřejňování klonované nabídky. cloneOffer.success=Vaše nabídka byla úspěšně naklonována. cloneOffer.hasConflictingClone=Nezměnili jste metodu platby nebo měnu. Můžete stále naklonovat nabídku, ale bude \ deaktivována a nebude zveřejněna.\n\n\ Nabídku můžete znovu později změni na \"Portfolio/Moje otevřené nabídky\" a splnit podmínky pro její aktivaci.\n\n\ Přejete si stále klonovat nabídku? cloneOffer.openTabWarning=Již máte otevřenou kartu \"Klonovat nabídku\". #################################################################### # Portfolio #################################################################### portfolio.tab.openOffers=Moje otevřené nabídky portfolio.tab.pendingTrades=Otevřené obchody portfolio.tab.history=Historie portfolio.tab.failed=Selhalo portfolio.tab.editOpenOffer=Upravit nabídku portfolio.tab.duplicateOffer=Duplicitní nabídka portfolio.tab.cloneOpenOffer=Klonovat nabídku portfolio.context.offerLikeThis=Vytvořit novou nabídku jako je tato... portfolio.context.notYourOffer=Duplikovat můžete pouze nabídky, u kterých jste byli tvůrcem. portfolio.closedTrades.deviation.help=Procentuální odchylka od tržní ceny portfolio.pending.invalidTx=Došlo k problému s chybějící nebo neplatnou transakcí.\n\n\ Prosím neposílejte fiat nebo crypto platby.\n\n\ Otevřete úkol pro podporu, některý z mediátorů vám pomůže.\n\n\ Chybová zpráva: {0} portfolio.pending.unconfirmedTooLong=Vkladové transakce obchodu {0} jsou stále nepotvrzené po {1} hodinách. \ Zkontrolujte transakce vkladu pomocí průzkumníka blockchainu; pokud jsou potvrzené, ale nezobrazují se jako \ potvrzené v Haveno, zkuste Haveno restartovat.\n\n\ Pokud problém přetrvává, kontaktujte podporu Haveno [HYPERLINK:https://matrix.to/#/#haveno:monero.social]. portfolio.pending.syncing=Synchronizuji obchodní peněženku portfolio.pending.syncing.blockRemaining=Synchronizuji obchodní peněženku — 1 blok zbývá portfolio.pending.syncing.blocksRemaining=Synchronizuji obchodní peněženku — {0} bloků zbývá portfolio.pending.step1.waitForConf=Počkejte na potvrzení na blockchainu portfolio.pending.step2_buyer.additionalConf=Vklady dosáhly 10 potvrzení.\nPro vyšší bezpečnost doporučujeme počkat na {0} potvrzení před odesláním platby.\nPokračujte dříve na vlastní riziko. portfolio.pending.step2_buyer.startPayment=Zahajte platbu portfolio.pending.step2_seller.waitPaymentSent=Počkejte, než začne platba portfolio.pending.step3_buyer.waitPaymentArrived=Počkejte, než dorazí platba portfolio.pending.step3_seller.confirmPaymentReceived=Potvrďte přijetí platby portfolio.pending.step5.completed=Hotovo portfolio.pending.step3_seller.autoConf.status.label=Stav automat. potvrzení portfolio.pending.autoConf=Automaticky potvrzeno portfolio.pending.autoConf.blocks=Potvrzení XMR: {0} / požadováno: {1} portfolio.pending.autoConf.state.xmr.txKeyReused=Transakční klíč byl použit znovu. Otevřete prosím spor. portfolio.pending.autoConf.state.confirmations=Potvrzení XMR: {0}/{1} portfolio.pending.autoConf.state.txNotFound=Transakce zatím není v mem-poolu vidět portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=Žádné platné ID transakce / transakční klíč portfolio.pending.autoConf.state.filterDisabledFeature=Zakázáno vývojáři. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FEATURE_DISABLED=Funkce automatického potvrzení je zakázána. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=Částka obchodu překračuje limit částky pro automatické potvrzení # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.INVALID_DATA=Peer poskytl neplatná data. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=Výplatní transakce již byla zveřejněna. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.DISPUTE_OPENED=Spor byl otevřen. Automatické potvrzení je u tohoto obchodu deaktivováno. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.REQUESTS_STARTED=Byly zahájeny žádosti o ověření transakce # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PENDING=Úspěšné výsledky: {0}/{1}; {2} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.COMPLETED=Důkaz u všech služeb byl úspěšný # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.ERROR=Došlo k chybě při požadavku na službu. Není možné automatické potvrzení. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=Služba se vrátila se selháním. Není možné automatické potvrzení. portfolio.pending.step1.info.you=Transakce vkladu byla publikována.\nPlatbu můžete zahájit po 10 potvrzeních (zbývá přibližně {0} minut). portfolio.pending.step1.info.buyer=Transakce vkladu byla publikována.\nKupující XMR může zahájit platbu po 10 potvrzeních (zbývá přibližně {0} minut). portfolio.pending.step1.warn=Vkladová transakce není stále potvrzena. K tomu někdy dochází ve vzácných případech, kdy byl poplatek za financování jednoho obchodníka z externí peněženky příliš nízký. portfolio.pending.step1.openForDispute=Vkladová transakce není stále potvrzena. \ Pokud jste čekali mnohem déle než 20 minut, můžete poádat o pomoc podporu Haveno. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2.confReached=Váš obchod má alespoň jedno potvrzení blockchainu.\n\n portfolio.pending.step2_buyer.refTextWarn=Důležité: když vyplňujete platební informace, nechte pole \"důvod platby\" \ prázdné. NEPOUŽÍVEJTE ID obchodu ani jiné poznámky jako např. 'monero', 'XMR' nebo 'Haveno'. \ Můžete se se svým obchodním partnerem domluvit pomocí chatu na \"důvod platby\", který bude vyhovovat oběma. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees=Pokud vaše banka účtuje poplatky za převod, musíte tyto poplatky uhradit vy. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees.swift=Ujistěte se, že k odeslání platby SWIFT používáte model SHA (model sdílených poplatků). \ Více detailů [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/SWIFT#Use_the_correct_fee_option]. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.crypto=Převeďte prosím z vaší externí {0} peněženky\n{1} prodejci XMR.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.cash=Přejděte do banky a zaplaťte {0} prodejci XMR.\n\n portfolio.pending.step2_buyer.cash.extra=DŮLEŽITÉ POŽADAVKY:\nPo provedení platby zapište na papírový doklad: NO REFUNDS - bez náhrady.\nPoté ji roztrhněte na 2 části, vytvořte fotografii a odešlete ji na e-mailovou adresu prodejce XMR. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.moneyGram=Zaplaťte prosím {0} prodejci XMR pomocí MoneyGram.\n\n portfolio.pending.step2_buyer.moneyGram.extra=DŮLEŽITÉ POŽADAVKY:\nPo provedení platby zašlete autorizační číslo a fotografii s potvrzením e-mailem prodejci XMR.\n\ Potvrzení musí jasně uvádět celé jméno, zemi, stát a částku prodávajícího. E-mail prodejce je: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.westernUnion=Zaplaťte prosím {0} prodejci XMR pomocí Western Union.\n\n portfolio.pending.step2_buyer.westernUnion.extra=DŮLEŽITÉ POŽADAVKY:\nPo provedení platby zašlete prodejci XMR e-mail s MTCN (sledovací číslo) a fotografii s potvrzením o přijetí.\n\ Potvrzení musí jasně uvádět celé jméno prodávajícího, město, zemi a částku. E-mail prodejce je: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Zašlete prosím {0} prodejci XMR pomocí \"US Postal Money Order\".\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Zašlete prosím {0} prodejci XMR v poštovní zásilce (\"Hotovost poštou\"). \ Konkrétní instrukce naleznete v obchodní smlouvě. V případě pochybností se můžete zeptat protistrany pomocí obchodního chatu. \ Více informací naleznete na Haveno wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Prosím uhraďte {0} pomocí zvolené platební metody prodejci XMR. V dalším kroku naleznete detaily o účtu prodejce.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Kontaktujte prodejce XMR prostřednictvím poskytnutého kontaktu a domluvte si schůzku kde zaplatíte {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Zahajte platbu pomocí {0} portfolio.pending.step2_buyer.recipientsAccountData=Příjemci {0} portfolio.pending.step2_buyer.amountToTransfer=Částka k převodu portfolio.pending.step2_buyer.sellersAddress={0} adresa prodejce portfolio.pending.step2_buyer.buyerAccount=Použijte svůj platební účet portfolio.pending.step2_buyer.paymentSent=Platba zahájena portfolio.pending.step2_buyer.showEarly=Zobrazit podrobnosti platby včas portfolio.pending.step2_buyer.warn=Platbu {0} jste ještě neprovedli!\nVezměte prosím na vědomí, že obchod musí být dokončen do {1}. portfolio.pending.step2_buyer.openForDispute=Neukončili jste platbu!\nMax. doba obchodu uplynula, ale platbu stále ještě můžete dokončit.\n\ Obraťte se na mediátora a požádejte o pomoc. portfolio.pending.step2_buyer.paperReceipt.headline=Odeslali jste papírový doklad prodejci XMR? portfolio.pending.step2_buyer.paperReceipt.msg=Zapamatujte si:\n\ Musíte napsat na papírový doklad: NO REFUNDS - bez náhrady.\n\ Poté ho roztrhněte na 2 části, vytvořte fotografii a odešlete ji na e-mailovou adresu prodejce XMR. portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Odeslat autorizační číslo a účtenku portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=Musíte zaslat autorizační číslo a fotografii dokladu e-mailem prodejci XMR.\n\ Doklad musí jasně uvádět celé jméno prodávajícího, zemi, stát a částku. E-mail prodejce je: {0}.\n\n Odeslali jste autorizační číslo a smlouvu prodejci? portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=Pošlete MTCN a účtenku portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Musíte odeslat MTCN (sledovací číslo) a fotografii dokladu e-mailem prodejci XMR.\n\ Doklad musí jasně uvádět celé jméno prodávajícího, město, zemi a částku. E-mail prodejce je: {0}.\n\n Odeslali jste MTCN a smlouvu prodejci? portfolio.pending.step2_buyer.halCashInfo.headline=Pošlete HalCash kód portfolio.pending.step2_buyer.halCashInfo.msg=Musíte odeslat jak textovou zprávu s kódem HalCash tak i \ obchodní ID ({0}) prodejci XMR.\nMobilní číslo prodejce je {1}.\n\n Poslali jste kód prodejci? portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Některé banky mohou ověřovat jméno příjemce. \ Účty Faster Payments vytvořené u starých klientů Haveno neposkytují jméno příjemce, \ proto si jej (v případě potřeby) vyžádejte pomocí obchodního chatu. portfolio.pending.step2_buyer.confirmStart.headline=Potvrďte, že jste zahájili platbu portfolio.pending.step2_buyer.confirmStart.msg=Zahájili jste platbu {0} vašemu obchodnímu partnerovi? portfolio.pending.step2_buyer.confirmStart.yes=Ano, zahájil jsem platbu portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=Neposkytli jste doklad o platbě portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=Nezadali jste ID transakce a klíč transakce.\n\n\ Neposkytnutím těchto údajů nemůže peer použít funkci automatického potvrzení k uvolnění XMR, jakmile bude přijat XMR.\n\ Kromě toho Haveno vyžaduje, aby odesílatel transakce XMR mohl tyto informace poskytnout mediátorovi nebo rozhodci v případě sporu.\n\ Další podrobnosti na wiki Haveno: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=Vstup není 32 bajtová hexadecimální hodnota portfolio.pending.step2_buyer.confirmStart.warningButton=Ignorovat a přesto pokračovat portfolio.pending.step2_seller.waitPayment.headline=Počkejte na platbu portfolio.pending.step2_seller.f2fInfo.headline=Kontaktní informace kupujícího portfolio.pending.step2_seller.waitPayment.msg=Vkladová transakce má alespoň jedno potvrzení na blockchainu.\nMusíte počkat, než kupující XMR zahájí platbu {0}. portfolio.pending.step2_seller.warn=Kupující XMR dosud neprovedl platbu {0}.\nMusíte počkat, než zahájí platbu.\nPokud obchod nebyl dokončen dne {1}, bude rozhodce vyšetřovat. portfolio.pending.step2_seller.openForDispute=Kupující XMR ještě nezačal s platbou!\nMax. povolené období pro obchod vypršelo.\nMůžete počkat déle a dát obchodnímu partnerovi více času nebo požádat o pomoc mediátora. disputeChat.chatWindowTitle=Okno chatu sporu pro obchod s ID '{0}' tradeChat.chatWindowTitle=Okno chatu pro obchod s ID '{0}' tradeChat.openChat=Otevřít chatovací okno tradeChat.rules=Můžete komunikovat se svým obchodním partnerem a vyřešit případné problémy s tímto obchodem.\n\ Odpovídat v chatu není povinné.\n\ Pokud obchodník poruší některé z níže uvedených pravidel, zahajte spor a nahlaste jej mediátorovi nebo rozhodci.\n\n\ Pravidla chatu:\n\ \t● Neposílejte žádné odkazy (riziko malwaru). Můžete odeslat ID transakce a jméno block exploreru.\n\ \t● Neposílejte seed slova, soukromé klíče, hesla nebo jiné citlivé informace!\n\ \t● Nepodporujte obchodování mimo Haveno (bez zabezpečení).\n\ \t● Nezapojujte se do žádných forem podvodů v oblasti sociálního inženýrství.\n\ \t● Pokud partner nereaguje a dává přednost nekomunikovat prostřednictvím chatu, respektujte jeho rozhodnutí.\n\ \t● Soustřeďte konverzaci pouze na obchod. Tento chat není náhradou messengeru.\n\ \t● Udržujte konverzaci přátelskou a uctivou. # suppress inspection "UnusedProperty" message.state.UNDEFINED=Nedefinováno # suppress inspection "UnusedProperty" message.state.SENT=Zpráva odeslána # suppress inspection "UnusedProperty" message.state.ARRIVED=Zpráva dorazila partnerovi # suppress inspection "UnusedProperty" message.state.STORED_IN_MAILBOX=Zpráva o platbě je odeslaná, ale není dosud přijatá partnerem # suppress inspection "UnusedProperty" message.state.ACKNOWLEDGED=Partner potvrdil přijetí zprávy # suppress inspection "UnusedProperty" message.state.FAILED=Odeslání zprávy se nezdařilo portfolio.pending.step3_buyer.wait.headline=Počkejte na potvrzení platby prodejce XMR portfolio.pending.step3_buyer.wait.info=Čekání na potvrzení prodejce XMR na přijetí platby {0}. portfolio.pending.step3_buyer.wait.msgStateInfo.label=Stav zprávy o zahájení platby portfolio.pending.step3_buyer.warn.part1a=na {0} blockchainu portfolio.pending.step3_buyer.warn.part1b=u vašeho poskytovatele plateb (např. banky) portfolio.pending.step3_buyer.warn.part2=Prodejce XMR vaši platbu stále nepotvrdil. Zkontrolujte {0}, zda \ bylo odeslání platby úspěšné. portfolio.pending.step3_buyer.openForDispute=Prodejce XMR nepotvrdil vaši platbu! Max. období pro uskutečnění obchodu uplynulo. \ Můžete počkat déle a dát obchodnímu partnerovi více času nebo požádat o pomoc mediátora. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.part=Váš obchodní partner potvrdil, že zahájil platbu {0}.\n\n portfolio.pending.step3_seller.crypto.explorer=ve vašem oblíbeném {0} průzkumníku blockchainu portfolio.pending.step3_seller.crypto.wallet=na vaší {0} peněžence portfolio.pending.step3_seller.crypto={0}Zkontrolujte prosím {1}, zda transakce na vaši přijímací adresu\n\ {2}\n\ má již dostatečné potvrzení na blockchainu.\nČástka platby musí být {3}\n\n\ Po zavření vyskakovacího okna můžete zkopírovat a vložit svou {4} adresu z hlavní obrazovky. portfolio.pending.step3_seller.postal={0}Zkontrolujte, zda jste od kupujícího XMR obdrželi {1} přes \"US Postal Money Order\". # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.payByMail={0}Zkontrolujte, zda jste od kupujícího XMR obdrželi {1} přes \"Hotovost poštou\". # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.bank=Váš obchodní partner potvrdil, že zahájil platbu {0}.\n\n\ Přejděte na webovou stránku online bankovnictví a zkontrolujte, zda jste od kupujícího XMR obdrželi {1}. portfolio.pending.step3_seller.cash=Vzhledem k tomu, že se platba provádí prostřednictvím hotovostního vkladu, musí kupující XMR napsat na papírový doklad \"NO REFUND\", roztrhat ho na 2 části a odeslat vám e-mailem fotografii.\n\n\ Abyste se vyhnuli riziku zpětného zúčtování, potvrďte pouze, zda jste obdrželi e-mail a zda si jste jisti, že papírový doklad je platný.\n\ Pokud si nejste jisti, {0} portfolio.pending.step3_seller.moneyGram=Kupující vám musí zaslat e-mailem autorizační číslo a fotografii s potvrzením.\n\ Potvrzení musí jasně uvádět vaše celé jméno, zemi, stát a částku. Zkontrolujte si prosím váš e-mail, pokud jste obdrželi autorizační číslo.\n\n\ Po uzavření tohoto vyskakovacího okna se zobrazí jméno a adresa kupujícího XMR pro vyzvednutí peněz z MoneyGram.\n\n\ Potvrďte příjem až po úspěšném vyzvednutí peněz! portfolio.pending.step3_seller.westernUnion=Kupující vám musí zaslat MTCN (sledovací číslo) a fotografii s potvrzením e-mailem.\n\ Potvrzení musí jasně uvádět vaše celé jméno, město, zemi a částku. Zkontrolujte svůj e-mail, pokud jste obdrželi MTCN.\n\n\ Po zavření tohoto vyskakovacího okna uvidíte jméno a adresu kupujícího XMR pro vyzvednutí peněz z Western Union.\n\n\ Potvrďte příjem až po úspěšném vyzvednutí peněz! portfolio.pending.step3_seller.halCash=Kupující vám musí poslat kód HalCash jako textovou zprávu. Kromě toho obdržíte zprávu od HalCash s požadovanými informacemi pro výběr EUR z bankomatu podporujícího HalCash.\n\n\ Poté, co jste vyzvedli peníze z bankomatu, potvrďte zde přijetí platby! portfolio.pending.step3_seller.amazonGiftCard=Kupující vám poslal e-mailovou kartu Amazon eGift e-mailem nebo textovou zprávou \ na váš mobilní telefon. Uplatněte nyní kartu Amazon eGift ve svém účtu Amazon \ a po přijetí potvrďte potvrzení o platbě. portfolio.pending.step3_seller.bankCheck=\n\nOvěřte také, zda se jméno odesílatele uvedené v obchodní smlouvě shoduje s jménem uvedeným na výpisu z účtu:\nJméno odesílatele podle obchodní smlouvy: {0}\n\n\ Pokud jména nejsou úplně stejná, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=nepotvrzujte příjem platby. Místo toho otevřete spor stisknutím \"alt + o\" nebo \"option + o\".\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=Potvrďte příjem platby portfolio.pending.step3_seller.amountToReceive=Částka k přijetí portfolio.pending.step3_seller.yourAddress=Vaše {0} adresa portfolio.pending.step3_seller.buyersAddress={0} adresa kupujících portfolio.pending.step3_seller.yourAccount=Váš obchodní účet portfolio.pending.step3_seller.xmrTxHash=ID transakce portfolio.pending.step3_seller.xmrTxKey=Transakční klíč portfolio.pending.step3_seller.buyersAccount=Údaje o účtu kupujícího portfolio.pending.step3_seller.confirmReceipt=Potvrďte příjem platby portfolio.pending.step3_seller.buyerStartedPayment=Kupující XMR zahájil platbu {0}.\n{1} portfolio.pending.step3_seller.buyerStartedPayment.crypto=Podívejte se na potvrzení na blockchainu ve své crypto peněžence nebo v blok exploreru a potvrďte platbu, pokud máte dostatečné potvrzení na blockchainu. portfolio.pending.step3_seller.buyerStartedPayment.traditional=Zkontrolujte na svém obchodním účtu (např. Bankovní účet) a potvrďte, kdy jste platbu obdrželi. portfolio.pending.step3_seller.warn.part1a=na {0} blockchainu portfolio.pending.step3_seller.warn.part1b=u vašeho poskytovatele plateb (např. banky) portfolio.pending.step3_seller.warn.part2=Stále jste nepotvrdili přijetí platby. \ Zkontrolujte {0}, zda jste obdrželi platbu. portfolio.pending.step3_seller.openForDispute=Nepotvrdili jste příjem platby!\n\ Uplynulo max. období obchodu.\nPotvrďte nebo požádejte o pomoc mediátora. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=Obdrželi jste od svého obchodního partnera platbu v měně {0}?\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.name=Ověřte také, zda se jméno odesílatele uvedené v obchodní smlouvě shoduje se jménem uvedeným na výpisu z účtu:\nJméno odesílatele podle obchodní smlouvy: {0}\n\nPokud jména nejsou úplně stejná, nepotvrzujte příjem platby. Místo toho otevřete spor stisknutím \"alt + o\" nebo \"option + o\".\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Vezměte prosím na vědomí, že jakmile potvrdíte příjem, dosud uzamčený obchodovaný XMR bude uvolněn kupujícímu a kauce bude vrácena.\n\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Potvrďte, že jste obdržel(a) platbu portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=Ano, obdržel(a) jsem platbu portfolio.pending.step3_seller.onPaymentReceived.signer=DŮLEŽITÉ: Potvrzením přijetí platby ověřujete také \ účet protistrany a odpovídajícím způsobem jej podepisujete. Protože účet protistrany dosud nebyl podepsán, \ měli byste odložit potvrzení platby co nejdéle, abyste snížili riziko zpětného zúčtování. portfolio.pending.step5_buyer.groupTitle=Shrnutí dokončeného obchodu portfolio.pending.step5_buyer.groupTitle.mediated=Tento obchod byl vyřešen pomocí mediátora portfolio.pending.step5_buyer.groupTitle.arbitrated=Tento obchod byl vyřešen pomocí rozhodce portfolio.pending.step5_buyer.tradeFee=Obchodní poplatek portfolio.pending.step5_buyer.makersMiningFee=Poplatek za těžbu portfolio.pending.step5_buyer.takersMiningFee=Celkové poplatky za těžbu portfolio.pending.step5_buyer.refunded=Vrácená kauce portfolio.pending.step5_buyer.amountTooLow=Částka k převodu je nižší než transakční poplatek a min. možná hodnota tx (drobné). portfolio.pending.step5_buyer.tradeCompleted.headline=Obchod dokončen portfolio.pending.step5_buyer.tradeCompleted.msg=Vaše dokončené obchody jsou uchovávány pod \"Portfolio/Historie\".\nVšechny své monero transakce najdete pod \"Prostředky/Transakce\" portfolio.pending.step5_buyer.bought=Koupili jste portfolio.pending.step5_buyer.paid=Zaplatili jste portfolio.pending.step5_seller.sold=Prodali jste portfolio.pending.step5_seller.received=Přijali jste tradeFeedbackWindow.title=Gratulujeme k dokončení obchodu tradeFeedbackWindow.msg.part1=Rádi bychom od vás slyšeli o vašich zkušenostech. Pomůže nám to vylepšit software a vyhladit všechny nejasnosti. Pokud chcete poskytnout zpětnou vazbu, vyplňte prosím tento krátký dotazník (není nutná registrace) na: tradeFeedbackWindow.msg.part2=Pokud máte nějaké dotazy nebo máte nějaké problémy, obraťte se prosím na ostatní uživatele a přispěvatele prostřednictvím fóra Haveno na: tradeFeedbackWindow.msg.part3=Děkujeme, že používáte Haveno! portfolio.pending.role=Moje role portfolio.pending.tradeInformation=Obchodní informace portfolio.pending.remainingTime=Zbývající čas portfolio.pending.remainingTimeDetail={0} (do {1}) portfolio.pending.remainingTimeDetail.startsAfter=Začne po {0} potvrzeních portfolio.pending.tradePeriodInfo=Po {0} potvrzeních začíná obchodní období. Na základě použité platební metody se uplatňuje jiná maximální povolená doba obchodování. portfolio.pending.tradePeriodWarning=Pokud je tato lhůta překročena, mohou oba obchodníci zahájit spor. portfolio.pending.tradeNotCompleted=Obchod nebyl dokončen včas (do {0}) portfolio.pending.tradeProcess=Obchodní proces portfolio.pending.stillNotResolved=Pokud váš problém zůstává nevyřešen, můžete požádat o podporu v naší [Matrix místnosti](https://matrix.to/#/#haveno:monero.social). portfolio.pending.openAgainDispute.msg=Pokud si nejste jisti, že zpráva pro mediátora nebo rozhodce dorazila (např. Pokud jste nedostali odpověď po 1 dni), neváhejte znovu zahájit spor s Cmd/Ctrl+o. Můžete také požádat o další pomoc na fóru Haveno na adrese [HYPERLINK:https://haveno.community]. portfolio.pending.openAgainDispute.button=Otevřete spor znovu portfolio.pending.openSupportTicket.headline=Otevřít úkol pro podporu portfolio.pending.openSupportTicket.msg=Použijte tuto funkci pouze v naléhavých případech, \ pokud nevidíte tlačítko \"Otevřít podporu\" nebo \"Otevřít spor\".\n\nKdyž otevřete úkol pro podporu, obchod bude přerušen \ a zpracován mediátorem nebo rozhodcem. portfolio.pending.timeLockNotOver=Než budete moci zahájit rozhodčí spor, musíte počkat do ≈{0} ({1} dalších bloků). portfolio.pending.error.depositTxNull=Vkladová operace je nulová. Nemůžete otevřít spor v případě \ neplatné vkladové transakce.\n\n\ Pro další pomoc kontaktujte podporu Haveno pomocí naší místnosti na Matrixu. portfolio.pending.mediationResult.error.depositTxNull=Vkladová transakce je nulová. Obchod můžete přesunout \ do neúspěšných obchodů. portfolio.pending.mediationResult.error.delayedPayoutTxNull=Zpožděný výplatní transakce je nulová. Obchod můžete přesunout \ do neúspěšných obchodů. portfolio.pending.error.depositTxNotConfirmed=Vkladová transakce není potvrzena. Nemůžete zahájit rozhodčí spor s nepotvrzenou vkladovou transakcí. \ Počkejte prosím, až bude potvrzena a dostupná.\n\n\ Pro další pomoc kontaktujte podporu Haveno pomocí naší místnosti na Matrixu. portfolio.pending.support.headline.getHelp=Potřebujete pomoc? portfolio.pending.support.button.getHelp=Otevřít obchodní chat portfolio.pending.support.headline.halfPeriodOver=Zkontrolujte platbu portfolio.pending.support.headline.periodOver=Obchodní období skončilo portfolio.pending.support.headline.depositTxMissing=Chybějící vkladová transakce portfolio.pending.support.depositTxMissing=U tohoto obchodu chybí transakce vkladu. Otevřete podporu, abyste kontaktovali rozhodce a získali pomoc. portfolio.pending.arbitrationRequested=Požádáno o arbitráž portfolio.pending.mediationRequested=Požádáno o mediaci portfolio.pending.refundRequested=Požádáno o vrácení peněz portfolio.pending.openSupport=Otevřít úkol pro podporu portfolio.pending.supportTicketOpened=Úkol pro podporu otevřen portfolio.pending.communicateWithArbitrator=Komunikujte prosím na obrazovce \"Podpora\" s rozhodcem. portfolio.pending.communicateWithMediator=Komunikujte prosím na obrazovce \"Podpora\" s mediátorem. portfolio.pending.disputeOpenedByUser=Už jste otevřeli spor.\n{0} portfolio.pending.disputeOpenedByPeer=Váš obchodní partner otevřel spor\n{0} portfolio.pending.noReceiverAddressDefined=Není definována žádná adresa příjemce portfolio.pending.mediationResult.headline=Navrhovaná výplata z mediace portfolio.pending.mediationResult.info.noneAccepted=Dokončete obchod přijetím návrhu mediátora na výplatu obchodu. portfolio.pending.mediationResult.info.selfAccepted=Přijali jste návrh mediátora. Čekáte na to, aby ho také přijal partner. portfolio.pending.mediationResult.info.peerAccepted=Váš obchodní partner přijal návrh mediátora. Přijímáte také? portfolio.pending.mediationResult.button=Zobrazit navrhované řešení portfolio.pending.mediationResult.popup.headline=Výsledek mediace obchodu s ID: {0} portfolio.pending.mediationResult.popup.headline.peerAccepted=Váš obchodní partner přijal návrh mediátora na obchod {0} portfolio.pending.mediationResult.popup.info=Mediátor navrhl následující výplatu:\n\ Obdržíte: {0}\n\ Váš obchodní partner obdrží: {1}\n\n\ Tuto navrhovanou výplatu můžete přijmout nebo odmítnout.\n\n\ Přijetím podepíšete navrhovanou výplatní transakci. \ Pokud váš obchodní partner také přijme a podepíše, výplata bude dokončena a obchod bude uzavřen.\n\n\ Pokud jeden nebo oba odmítnete návrh, budete muset počkat do {2} (blok {3}), abyste zahájili spor \ druhého kola s rozhodcem, který případ znovu prošetří a na základě svých zjištění provede výplatu.\n\n\ Rozhodce může jako náhradu za svou práci účtovat malý poplatek (maximální poplatek: bezpečnostní záloha obchodníka). \ Oba obchodníci, kteří souhlasí s návrhem zprostředkovatele, jsou na dobré cestě - žádost o arbitráž je určena pro \ výjimečné okolnosti, například pokud je obchodník přesvědčen, že zprostředkovatel neučinil návrh na spravedlivou výplatu \ (nebo pokud druhý partner nereaguje).\n\n\ Další podrobnosti o novém rozhodčím modelu: [HYPERLINK:https://haveno.exchange/wiki/Dispute_resolution#Level_3:_Arbitration] portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=Přijali jste výplatu navrženou mediátorem, \ ale zdá se, že váš obchodní partner ji nepřijal.\n\n\ Po uplynutí doby uzamčení na {0} (blok {1}) můžete zahájit spor druhého kola s rozhodcem, který případ \ znovu prošetří a na základě jeho zjištění provede platbu.\n\n\ Další podrobnosti o rozhodčím modelu najdete na adrese:\ [https://haveno.exchange/wiki/Dispute_resolution#Level_3:_Arbitration] portfolio.pending.mediationResult.popup.openArbitration=Odmítnout a požádat o arbitráž portfolio.pending.mediationResult.popup.alreadyAccepted=Už jste přijali portfolio.pending.failedTrade.taker.missingTakerFeeTx=Chybí poplatek příjemce transakce.\n\n\ Bez tohoto tx nelze obchod dokončit. Nebyly uzamčeny žádné prostředky a nebyl zaplacen žádný obchodní poplatek. \ Tento obchod můžete přesunout do neúspěšných obchodů. portfolio.pending.failedTrade.maker.missingTakerFeeTx=Chybí poplatek příjemce transakce.\n\n\ Bez tohoto tx nelze obchod dokončit. Nebyly uzamčeny žádné prostředky. Vaše nabídka je \ stále k dispozici dalším obchodníkům, takže jste neztratili poplatek za vytvoření. \ Tento obchod můžete přesunout do neúspěšných obchodů. portfolio.pending.failedTrade.missingDepositTx=Chybí vkladová transakce.\n\nTato transakce je nutná k dokončení obchodu. Ujistěte se, že je vaše peněženka plně synchronizována s blockchainem Monero.\n\nTento obchod můžete přesunout do sekce „Neúspěšné obchody“ pro jeho deaktivaci. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Zpožděná výplatní transakce chybí, ale prostředky byly uzamčeny v vkladové transakci.\n\n\ Nezasílejte prosím fiat nebo crypto platbu prodejci XMR, protože bez odložené platby tx nelze zahájit arbitráž. \ Místo toho otevřete mediační úkol pomocí Cmd/Ctrl+o. \ Mediátor by měl navrhnout, aby oba partneři dostali zpět celou částku svých bezpečnostních vkladů \ (přičemž prodejce také obdrží plnou částku obchodu). \ Tímto způsobem nehrozí žádné bezpečnostní riziko a jsou ztraceny pouze obchodní poplatky.\n\n\ O vrácení ztracených obchodních poplatků můžete požádat zde: \ [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Zpožděná výplatní transakce chybí, \ ale prostředky byly v depozitní transakci uzamčeny.\n\n\ Pokud kupujícímu chybí také odložená výplatní transakce, bude poučen, aby platbu NEPOSLAL a místo toho otevřel \ mediační úkol. Měli byste také otevřít mediační úkol pomocí Cmd/Ctrl+o.\n\n\ Pokud kupující ještě neposlal platbu, měl by mediátor navrhnout, aby oba partneři dostali zpět celou částku \ svých bezpečnostních vkladů (přičemž prodejce také obdrží plnou částku obchodu). \ Jinak by částka obchodu měla jít kupujícímu.\n\n\ O vrácení ztracených obchodních poplatků můžete požádat zde: \ [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=Během provádění obchodního protokolu došlo k chybě.\n\n Chyba: {0}\n\n\ Je možné, že tato chyba není kritická a obchod lze dokončit normálně. Pokud si nejste jisti, otevřete si mediační úkol \ a získejte radu od mediátorů Haveno.\n\n\ Pokud byla chyba kritická a obchod nelze dokončit, možná jste ztratili obchodní poplatek. \ O vrácení ztracených obchodních poplatků požádejte zde: \ [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.missingContract=Obchodní kontrakt není stanoven.\n\n\ Obchod nelze dokončit a možná jste ztratili poplatek \ za obchodování. Pokud ano, můžete požádat o vrácení ztracených obchodních poplatků zde: \ [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=Obchodní protokol narazil na některé problémy.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=Obchodní protokol narazil na vážný problém.\n\n{0}\n\n\ Chcete obchod přesunout do neúspěšných obchodů?\n\n\ Z obrazovky neúspěšných obchodů nemůžete otevřít mediaci nebo arbitráž, ale můžete kdykoli přesunout neúspěšný obchod zpět \ na obrazovku otevřených obchodů. portfolio.pending.failedTrade.txChainValid.moveToFailed=Obchodní protokol narazil na některé problémy.\n\n{0}\n\n\ Obchodní transakce byly zveřejněny a finanční prostředky jsou uzamčeny. Přesuňte obchod do neúspěšných obchodů, \ pouze pokud jste si opravdu jisti. Může to bránit možnostem řešení problému.\n\n\ Chcete obchod přesunout do neúspěšných obchodů?\n\n\ Z obrazovky neúspěšných obchodů nemůžete otevřít mediaci nebo arbitráž, ale můžete kdykoli přesunout neúspěšný obchod \ zpět na obrazovku otevřených obchodů. portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Přesuňte obchod do neúspěšných obchodů portfolio.pending.failedTrade.warningIcon.tooltip=Kliknutím otevřete podrobnosti o problémech tohoto obchodu portfolio.failed.revertToPending.popup=Chcete přesunout tento obchod do otevřených obchodů? portfolio.failed.revertToPending.failed=Selhal přesun tohoto obchodu do otevřených obchodů. portfolio.failed.revertToPending=Přesunout obchod do otevřených obchodů portfolio.closed.completed=Dokončeno portfolio.closed.ticketClosed=Rozhodnuto portfolio.closed.mediationTicketClosed=Mediováno portfolio.closed.canceled=Zrušeno portfolio.failed.Failed=Selhalo portfolio.failed.unfail=Před pokračováním se ujistěte, že máte zálohu vašeho datového adresáře!\n\ Chcete tento obchod přesunout zpět do otevřených obchodů?\n\ Je to způsob, jak odemknout finanční prostředky uvízlé v neúspěšném obchodu. portfolio.failed.cantUnfail=Tento obchod nelze v tuto chvíli přesunout zpět do otevřených obchodů.\n\ Zkuste to znovu po dokončení obchodu (obchodů) {0} portfolio.failed.depositTxNull=Obchod nelze změnit zpět na otevřený obchod. Transakce s vkladem je neplatná. portfolio.failed.delayedPayoutTxNull=Obchod nelze změnit zpět na otevřený obchod. Zpožděná výplatní transakce je nulová. portfolio.failed.penalty.msg=Toto strhne {0}/{1} poplatek penále {2} a vrátí zbytek obchodovaných financí do jejich peněženky. Jste si jisti, že chcete odeslat?\n\n\ Jiné info:\n\ Transakční poplatek: {3}\n\ Rezervní hash Tx: {4} portfolio.failed.error.msg=Záznam obchodu neexistuje. #################################################################### # Funds #################################################################### funds.tab.deposit=Přijmout finanční prostředky funds.tab.withdrawal=Poslat finanční prostředky funds.tab.reserved=Vyhrazené prostředky funds.tab.locked=Zamčené prostředky funds.tab.transactions=Transakce funds.deposit.unused=Nepoužito funds.deposit.usedInTx=Používá se v {0} transakcích funds.deposit.baseAddress=Základní adresa funds.deposit.offerFunding=Rezervováno pro financování nabídky ({0}) funds.deposit.tradePayout=Rezervováno pro výplatu obchodu ({0}) funds.deposit.fundHavenoWallet=Financovat Haveno peněženku funds.deposit.noAddresses=Dosud nebyly vygenerovány žádné adresy pro vklad funds.deposit.fundWallet=Financovat peněženku funds.deposit.withdrawFromWallet=Pošlete peníze z peněženky funds.deposit.amount=Částka v XMR (volitelná) funds.deposit.generateAddress=Vygenerujte novou adresu funds.deposit.generateAddressSegwit=Nativní formát segwit (Bech32) funds.deposit.selectUnused=Vyberte prosím nepoužívanou adresu z výše uvedené tabulky místo generování nové. funds.withdrawal.arbitrationFee=Poplatek za arbitráž funds.withdrawal.inputs=Volba vstupů funds.withdrawal.useAllInputs=Použijte všechny dostupné vstupy funds.withdrawal.useCustomInputs=Použijte vlastní vstupy funds.withdrawal.receiverAmount=Částka pro příjemce funds.withdrawal.sendMax=Poslat max. dostupné funds.withdrawal.senderAmount=Náklad pro odesílatele funds.withdrawal.feeExcluded=Částka nezahrnuje poplatek za těžbu funds.withdrawal.feeIncluded=Částka zahrnuje poplatek za těžbu funds.withdrawal.fromLabel=Výběr z adresy funds.withdrawal.toLabel=Adresa příjemce funds.withdrawal.maximum=MAX funds.withdrawal.memoLabel=Poznámka k výběru funds.withdrawal.memo=Volitelně vyplňte poznámku funds.withdrawal.withdrawButton=Odeslat výběr funds.withdrawal.noFundsAvailable=Pro výběr nejsou k dispozici žádné finanční prostředky funds.withdrawal.confirmWithdrawalRequest=Potvrďte žádost o výběr funds.withdrawal.withdrawMultipleAddresses=Výběr z více adres ({0}) funds.withdrawal.withdrawMultipleAddresses.tooltip=Výběr z více adres:\n{0} funds.withdrawal.notEnoughFunds=V peněžence nemáte dostatek finančních prostředků. funds.withdrawal.selectAddress=Vyberte zdrojovou adresu z tabulky funds.withdrawal.setAmount=Nastavte částku k výběru funds.withdrawal.fillDestAddress=Vyplňte svou cílovou adresu funds.withdrawal.warn.noSourceAddressSelected=Ve výše uvedené tabulce musíte vybrat zdrojovou adresu. funds.withdrawal.warn.amountExceeds=Na vybrané adrese nemáte dostatek prostředků.\n\ Zvažte výběr více adres ve výše uvedené tabulce nebo změňte přepínač poplatků tak, aby zahrnoval poplatek za těžbu. funds.withdrawal.warn.amountMissing=Zadejte částku k vybrání funds.withdrawal.txFee=Poplatek transakce výběru (satoshi/vbyte) funds.withdrawal.useCustomFeeValueInfo=Zadejte vlastní hodnotu poplatku transakce funds.withdrawal.useCustomFeeValue=Použít vlastní hodnotu funds.withdrawal.txFeeMin=Poplatek transakce musí být alespoň {0} satoshi/vbyte funds.withdrawal.txFeeTooLarge=Vámi zadaná hodnota je nad rozumnou hodnotou (>5000 satoshi/vbyte). Poplatek transakce je obvykle v rozmezí 50-400 satoshi/vbyte. funds.reserved.noFunds=V otevřených nabídkách nejsou rezervovány žádné finanční prostředky funds.reserved.reserved=Rezervováno v místní peněžence pro nabídku s ID: {0} funds.locked.noFunds=V obchodech nejsou zamčeny žádné prostředky funds.locked.locked=Uzamčeno v multisig adrese pro obchodování s ID: {0} funds.tx.direction.sentTo=Odesláno na: funds.tx.direction.receivedWith=Přijato z: funds.tx.direction.genesisTx=Z Genesis tx: funds.tx.createOfferFee=Poplatky tvůrce a tx: {0} funds.tx.takeOfferFee=Poplatky příjemce a tx: {0} funds.tx.multiSigDeposit=Vklad na multisig adresu: {0} funds.tx.multiSigPayout=Výběr z multisig adresy: {0} funds.tx.disputePayout=Výběr ze sporu: {0} funds.tx.disputeLost=Prohraných sporů: {0} funds.tx.collateralForRefund=Zástava na vrácení peněz: {0} funds.tx.timeLockedPayoutTx=Časově uzamčená výplata tx: {0} funds.tx.refund=Vrácení peněz z rozhodčího řízení: {0} funds.tx.makerTradeFee=Poplatek tvůrce: {0} funds.tx.takerTradeFee=Poplatek příjemce: {0} funds.tx.unknown=Neznámý důvod: {0} funds.tx.noFundsFromDispute=Žádná náhrada ze sporu funds.tx.receivedFunds=Přijaté prostředky funds.tx.withdrawnFromWallet=Výběr z peněženky funds.tx.memo=Poznámka funds.tx.noTxAvailable=Není k dispozici žádná transakce funds.tx.revert=Vrátit funds.tx.txSent=Transakce byla úspěšně odeslána na novou adresu v lokální peněžence Haveno. funds.tx.direction.self=Posláno sobě funds.tx.dustAttackTx=Přijaté drobné funds.tx.dustAttackTx.popup=Tato transakce odesílá do vaší peněženky velmi malou částku XMR a může se jednat o pokus \ společností provádějících analýzu blockchainu o špehování vaší peněženky.\n\n\ Použijete-li tento transakční výstup ve výdajové transakci, zjistí, že jste pravděpodobně také vlastníkem \ jiné adresy (sloučení mincí).\n\n\ Kvůli ochraně vašeho soukromí ignoruje peněženka Haveno takové drobné výstupy pro účely utrácení a na obrazovce zůstatku. \ V nastavení můžete nastavit prahovou hodnotu, při které je výstup považován za drobné(dust). #################################################################### # Support #################################################################### support.tab.mediation.support=Mediace support.tab.refund.support=Vrácení peněz support.tab.arbitration.support=Arbitráž support.tab.legacyArbitration.support=Starší arbitráž support.tab.ArbitratorsSupportTickets=Úkoly pro {0} support.tab.SignedOffers=Podepsané nabídky support.prompt.signedOffer.penalty.msg=Tím se tvůrci účtuje sankční poplatek a zbývající prostředky z obchodu se vrátí do jeho peněženky. Jste si jisti, že chcete odeslat?\n\n\ ID nabídky: {0}\n\ Poplatek penále tvůrce: {1}\n\ Rezervní poplatek těžby Tx: {2}\n\ Rezervní hash Tx: {3}\n\ Rezervní klíčové obrázky Tx: {4}\n\ support.contextmenu.penalize.msg=Penalizovat {0} zveřejněním tx rezervy support.prompt.signedOffer.error.msg=Podepsaný záznam nabídky neexistuje; kontaktujte správce. support.info.submitTxHex=Rezervní transakce byla zveřejněna s tímto výsledkem:\n support.result.success=Transakce hex byla úspěšně zadána. support.sigCheck.button=Ověřit podpis support.sigCheck.popup.info=Vložte souhrnnou zprávu procesu zprostředkování. S tímto nástrojem může každý uživatel zkontrolovat, zda se podpis zprostředkovatele shoduje se souhrnnou zprávou. support.sigCheck.popup.header=Ověřit podpis výsledku sporu support.sigCheck.popup.msg.label=Souhrnná zpráva support.sigCheck.popup.msg.prompt=Zkopírovat a vložit souhrnnou zprávu ze sporu support.sigCheck.popup.result=Výsledek ověření support.sigCheck.popup.success=Podpis je platný support.sigCheck.popup.failed=Ověření podpisu selhalo support.sigCheck.popup.invalidFormat=Zpráva nemá očekávaný formát. Zkopírujte a vložte souhrnnou zprávu ze sporu. support.reOpenByTrader.prompt=Opravdu chcete spor znovu otevřít? support.reOpenByTrader.failed=Opětovné otevření sporu selhalo. support.reOpenButton.label=Znovu otevřít support.sendNotificationButton.label=Soukromé oznámení support.reportButton.label=Zpráva support.fullReportButton.label=Všechny spory support.noTickets=Žádné otevřené úkoly support.sendingMessage=Odesílání zprávy... support.receiverNotOnline=Příjemce není online. Zpráva je uložena v jejich schránce. support.sendMessageError=Odeslání zprávy se nezdařilo. Chyba: {0} support.receiverNotKnown=Příjemce není znám support.wrongVersion=Nabídka v tomto sporu byla vytvořena se starší verzí Haveno.\n\ Tento spor nemůžete ukončit s touto verzí aplikace.\n\n\ Použijte prosím starší verzi s verzí protokolu {0} support.openFile=Otevřete soubor, který chcete připojit (maximální velikost souboru: {0} kb) support.attachmentTooLarge=Celková velikost vašich příloh je {0} kb a překračuje maximální povolenou velikost zprávy {1} kB. support.maxSize=Max. povolená velikost souboru je {0} kB. support.attachment=Příloha support.tooManyAttachments=V jedné zprávě nelze odeslat více než 3 přílohy. support.save=Uložit soubor na disk support.messages=Zprávy support.input.prompt=Vložte zprávu... support.send=Odeslat support.addAttachments=Připojit soubory support.closeTicket=Zavřít úkol support.attachments=Přílohy: support.savedInMailbox=Zpráva uložena ve schránce příjemce support.arrived=Zpráva dorazila k příjemci support.transient=Zpráva je na cestě k příjemci support.acknowledged=Přijetí zprávy potvrzeno příjemcem support.error=Příjemce nemohl zpracovat zprávu. Chyba: {0} support.errorTimeout=vypršení platnosti. Zkuste zprávu odeslat znovu. support.buyerAddress=Adresa kupujícího XMR support.sellerAddress=Adresa prodejce XMR support.role=Role support.agent=Agent podpory support.state=Stav support.chat=Chat support.preparing=Připravuje se support.requested=Požadováno support.closed=Uzavřeno support.open=Otevřeno support.moreButton=VÍCE... support.sendLogFiles=Odeslat soubory logů support.uploadTraderChat=Nahrát obchodní chat support.process=Rozhodnout support.buyerMaker=Kupující XMR/Tvůrce support.sellerMaker=Prodejce XMR/Tvůrce support.buyerTaker=Kupující XMR/Příjemce support.sellerTaker=Prodávající XMR/Příjemce support.sendLogs.title=Odeslat logy support.sendLogs.backgroundInfo=Pokud se vyskytne chyba, rozhodčí a pracovníci podpory si často vyžádají kopie souborů log, aby mohli problém prozkoumat.\n\n\ Po stisku 'Odeslat' dojde ke kompresi a odeslání logů přímo rozhodci. support.sendLogs.step1=Vytvořit archiv Zip s log soubory support.sendLogs.step2=Požadavek připojení k rozhodci support.sendLogs.step3=Nahrát archivovaná data logů support.sendLogs.send=Odeslat support.sendLogs.cancel=Zrušit support.sendLogs.init=Zavádění support.sendLogs.retry=Opakování odeslání support.sendLogs.stopped=Přenos zastaven support.sendLogs.progress=Průběh přenosu: %.0f%% support.sendLogs.finished=Přenos dokončen! support.sendLogs.command=Stiskněte 'Odeslat' pro opakování, nebo 'Zastavit' pro zrušení support.txKeyImages=Klíčové obrázky support.txHash=Hash transakce support.txHex=Hex transakce support.signature=Podpis support.maker.penalty.fee=Poplatek penále tvůrce support.tx.miner.fee=Poplatek těžby support.backgroundInfo=Haveno není společnost, takže spory řeší jinak.\n\n\ Obchodníci mohou v rámci aplikace komunikovat prostřednictvím zabezpečeného chatu na obrazovce otevřených obchodů \ a sami se pokusit o řešení sporů. Pokud to nestačí, rozhodce rozhodne o situaci \ a určí výplatu obchodních prostředků. support.initialInfo=Do níže uvedeného textového pole zadejte popis problému. \ Přidejte co nejvíce informací k urychlení doby řešení sporu.\n\n\ Zde je kontrolní seznam informací, které byste měli poskytnout:\n\ \● Pokud kupujete XMR: Provedli jste převod Fiat nebo Cryptou? Pokud ano, klikli jste v aplikaci na tlačítko 'Platba zahájena'?\n\ \t● Pokud jste prodejcem XMR: Obdrželi jste platbu fiat nebo kryptoměny? Pokud ano, \ klikli jste v aplikaci \ na tlačítko 'Platba přijata'?\n\ \t● Kterou verzi Haveno používáte?\n\ \t● Jaký operační systém používáte?\n\ \t● Pokud se vyskytl problém s neúspěšnými transakcemi, zvažte přechod na nový datový adresář.\n\ \t Někdy dojde k poškození datového adresáře a vede to k podivným chybám.\n\ \t  Viz: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\n\ Seznamte se prosím se základními pravidly procesu sporu:\n\ \t● Musíte odpovědět na požadavky {0} do 2 dnů.\n\ \t● Mediátoři reagují do 2 dnů. Rozhodci odpoví do 5 pracovních dnů.\n\ \t● Maximální doba sporu je 14 dní.\n\ \t● Musíte spolupracovat s {1} a poskytnout informace, které požaduje, aby jste vyřešili váš případ.\n\ \t● Při prvním spuštění aplikace jste přijali pravidla uvedena v dokumentu sporu v uživatelské smlouvě.\n\n\ Další informace o procesu sporu naleznete na: {2} support.systemMsg=Systémová zpráva: {0} support.youOpenedTicket=Otevřeli jste žádost o podporu.\n\n{0}\n\nVerze Haveno: {1} support.youOpenedDispute=Otevřeli jste žádost o spor.\n\n{0}\n\nVerze Haveno: {1} support.youOpenedDisputeForMediation=Vyžádali jste si mediaci.\n\n{0}\n\nHaveno verze {1} support.peerOpenedTicket=Váš obchodní partner požádal o podporu kvůli technickým problémům.\n\n{0}\n\nHaveno verze: {1} support.peerOpenedDispute=Váš obchodní partner požádal o spor.\n\n{0}\n\nHaveno verze: {1} support.peerOpenedDisputeForMediation=Váš obchodní partner požádal o mediaci.\n\n{0}\n\nHaveno verze: {1} support.mediatorsDisputeSummary=Systémová zpráva: Shrnutí sporu mediátora:\n{0} support.mediatorReceivedLogs=Systémová zpráva: Mediátor obdržel logy: {0} support.mediatorsAddress=Adresa uzlu mediátora: {0} support.warning.disputesWithInvalidDonationAddress=Odložená výplatní transakce použila neplatnou adresu příjemce. \ Neshoduje se s žádnou z hodnot parametrů DAO pro platné dárcovské adresy.\n\nMůže to být pokus o podvod. \ Informujte prosím vývojáře o tomto incidentu a neuzavírejte tento případ, dokud nebude situace vyřešena!\n\n\ Adresa použitá ve sporu: {0}\n\n\ Všechny parametry pro darovací adresy DAO: {1}\n\n\ Obchodní ID: {2}\ {3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nStále chcete spor uzavřít? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nVýplatu nesmíte provést. support.warning.traderCloseOwnDisputeWarning=Obchodníci mohou sami zrušit úkol pro podporu pouze pokud došlo k výplatě prostředků. support.info.disputeReOpened=Spor byl znovuotevřen. #################################################################### # Settings #################################################################### settings.tab.preferences=Preference settings.tab.network=Informace o síti settings.tab.about=O Haveno setting.preferences.general=Základní nastavení setting.preferences.explorer=Průzkumník Monero setting.preferences.deviation=Max. odchylka od tržní ceny setting.preferences.avoidStandbyMode=Vyhněte se pohotovostnímu režimu setting.preferences.useSoundForNotifications=Přehrávat zvuky pro upozornění setting.preferences.autoConfirmXMR=Automatické potvrzení XMR setting.preferences.autoConfirmEnabled=Povoleno setting.preferences.autoConfirmRequiredConfirmations=Požadovaná potvrzení setting.preferences.autoConfirmMaxTradeSize=Max. částka obchodu (XMR) setting.preferences.autoConfirmServiceAddresses=Adresa průzkumníka Monero (používá Tor, kromě localhost, LAN IP adres a názvů hostitele *.local) setting.preferences.deviationToLarge=Hodnoty vyšší než {0} % nejsou povoleny. setting.preferences.txFee=Poplatek za výběr transakce (satoshi/vbyte) setting.preferences.useCustomValue=Použijte vlastní hodnotu setting.preferences.ignorePeers=Ignorované peer uzly [onion addresa:port] setting.preferences.ignoreDustThreshold=Min. hodnota výstupu bez drobných setting.preferences.currenciesInList=Měny v seznamu zdrojů tržních cen setting.preferences.prefCurrency=Preferovaná měna setting.preferences.displayTraditional=Zobrazit národní měny setting.preferences.noTraditional=Nejsou vybrány žádné národní měny setting.preferences.cannotRemovePrefCurrency=Vybranou zobrazovanou měnu nelze odebrat. setting.preferences.displayCryptos=Zobrazit kryptoměny setting.preferences.noCryptos=Nejsou vybrány žádné kryptoměny setting.preferences.addTraditional=Přidejte národní měnu setting.preferences.addCrypto=Přidejte crypto setting.preferences.displayOptions=Zobrazit možnosti setting.preferences.showOwnOffers=Zobrazit mé vlastní nabídky v seznamu nabídek setting.preferences.useAnimations=Použít animace setting.preferences.useDarkMode=Použít tmavý režim setting.preferences.useLightMode=Použijte světlý režim setting.preferences.sortWithNumOffers=Seřadit seznamy trhů s počtem nabídek/obchodů setting.preferences.onlyShowPaymentMethodsFromAccount=Skrýt nepodporované metody platby setting.preferences.denyApiTaker=Odmítat příjemce, kteří používají API setting.preferences.notifyOnPreRelease=Získávat oznámení o beta verzích setting.preferences.resetAllFlags=Zrušit všechny \"Nezobrazovat znovu\" settings.preferences.languageChange=Chcete-li použít změnu jazyka na všech obrazovkách, musíte restartovat aplikaci. settings.preferences.supportLanguageWarning=V případě sporu mějte na paměti, že arbitráž je řešena v {0}. setting.preferences.clearDataAfterDays=Smazat citlivá data po (dnech) settings.preferences.editCustomExplorer.headline=Nastavení Průzkumníku settings.preferences.editCustomExplorer.description=Ze seznamu vlevo vyberte průzkumníka definovaného systémem a nebo \ si jej přizpůsobte podle svých vlastních preferencí. settings.preferences.editCustomExplorer.available=Dostupní průzkumníci settings.preferences.editCustomExplorer.chosen=Nastavení zvoleného průzkumníka settings.preferences.editCustomExplorer.name=Jméno settings.preferences.editCustomExplorer.txUrl=Transakční URL settings.preferences.editCustomExplorer.addressUrl=Adresa URL setting.info.headline=Nová funkce ochrany osobních údajů settings.preferences.sensitiveDataRemoval.msg=Aby bylo chráněno vaše soukromí i soukromí ostatních obchodníků, Haveno zamýšlí \ odstranit citlivá data ze starých obchodů. Toto je důležité zvláště u obchodů fiat, které mohou zahrnovat detaily \ bankovního účtu.\n\n\ Prahová hodnota pro odebrání dat je nastavitelná na této obrazovce pomocí pole "Smazat citlivá data po (dnech)". \ Doporučuje se nastavit tuto hodnotu co nejníže, například na 60 dní. To znamená, že u dokončených obchodů \ starších než 60 dní budou odstraněna citlivá data. Dokončené obchody najdete na kartě \ Portfolio / Historie. settings.net.xmrHeader=Síť Monero settings.net.p2pHeader=Síť Haveno settings.net.onionAddressLabel=Moje onion adresa settings.net.xmrNodesLabel=Použijte vlastní Monero uzel settings.net.moneroPeersLabel=Připojené peer uzly settings.net.connection=Připojení settings.net.connected=Připojeno settings.net.useTorForXmrJLabel=Použít Tor pro Monero síť settings.net.useTorForXmrAfterSyncRadio=Po synchronizaci peněženky settings.net.useTorForXmrOffRadio=Nikdy settings.net.useTorForXmrOnRadio=Vždy settings.net.moneroNodesLabel=Monero uzly, pro připojení settings.net.useProvidedNodesRadio=Použít nabízené Monero uzly settings.net.usePublicNodesRadio=Použít veřejnou síť Monero settings.net.useCustomNodesRadio=Použít vlastní Monero uzel settings.net.warn.usePublicNodes=Pokud používáte veřejné Monero uzly, jste vystaveni riziku spojenému s používáním nedůvěryhodných vzdálených uzlů.\n\nProsím, přečtěte si více podrobností na [HYPERLINK:https://www.getmonero.org/resources/moneropedia/remote-node.html].\n\nJste si jistí, že chcete použít veřejné uzly? settings.net.warn.usePublicNodes.useProvided=Ne, použijte nabízené uzly settings.net.warn.usePublicNodes.usePublic=Ano, použít veřejnou síť settings.net.warn.useCustomNodes.B2XWarning=Ujistěte se, že váš Monero uzel je důvěryhodný Monero uzel!\n\n\ Připojení k uzlům, které nedodržují pravidla konsensu Monero, může poškodit vaši peněženku a způsobit problémy v obchodním procesu.\n\n\ Uživatelé, kteří se připojují k uzlům, které porušují pravidla konsensu, odpovídají za případné škody, které z toho vyplývají. \ Jakékoli výsledné spory budou rozhodnuty ve prospěch druhého obchodníka. Uživatelům, kteří ignorují \ tyto varovné a ochranné mechanismy, nebude poskytována technická podpora! settings.net.warn.invalidXmrConfig=Připojení k síti Monero selhalo, protože je vaše konfigurace neplatná.\n\nVaše konfigurace byla resetována, aby byly místo toho použity poskytnuté uzly Monero. Budete muset restartovat aplikaci. settings.net.localhostXmrNodeInfo=Základní informace: Haveno při spuštění hledá místní Monero uzel. Pokud je nalezen, Haveno bude komunikovat se sítí Monero výhradně skrze něj. settings.net.p2PPeersLabel=Připojené uzly settings.net.onionAddressColumn=Onion adresa settings.net.creationDateColumn=Založeno settings.net.connectionTypeColumn=Příchozí/Odchozí settings.net.sentDataLabel=Statistiky odeslaných dat settings.net.receivedDataLabel=Statistiky přijatých dat settings.net.chainHeightLabel=Poslední výška bloku XMR settings.net.roundTripTimeColumn=Roundtrip settings.net.sentBytesColumn=Odesláno settings.net.receivedBytesColumn=Přijato settings.net.peerTypeColumn=Typ peer uzlu settings.net.openTorSettingsButton=Otevřít nastavení Toru settings.net.versionColumn=Verze settings.net.subVersionColumn=Subverze settings.net.heightColumn=Výška settings.net.needRestart=Chcete-li použít tuto změnu, musíte restartovat aplikaci.\nChcete to udělat hned teď? settings.net.notKnownYet=Není dosud známo... settings.net.sentData=Odeslaná data: {0}, {1} zprávy, {2} zprávy/sekundu settings.net.receivedData=Přijatá data: {0}, {1} zprávy, {2} zprávy/sekundu settings.net.chainHeight=Monero Peers: {0} settings.net.ips=[IP adresa:port | název hostitele:port | onion adresa:port] (oddělené čárkou). Pokud je použit výchozí port (8333), lze port vynechat. settings.net.seedNode=Seed uzel settings.net.directPeer=Peer uzel (přímý) settings.net.initialDataExchange={0} [Bootstrapping] settings.net.peer=Peer settings.net.inbound=příchozí settings.net.outbound=odchozí settings.net.rescanOutputsLabel=Znovu oskenovat výstupy settings.net.rescanOutputsButton=Znovu oskenovat výstupy peněženky settings.net.rescanOutputsSuccess=Jste si jistí, že chcete znovu oskenovat výstupy vaší peněženky? settings.net.rescanOutputsFailed=Nepodařilo se oskenovat výstupy peněženky.\nChyba: {0} setting.about.aboutHaveno=O projektu Haveno setting.about.about=Haveno je software s otevřeným zdrojovým kódem, který usnadňuje směnu moneroů s národními měnami (a jinými kryptoměnami) prostřednictvím decentralizované sítě typu peer-to-peer způsobem, který silně chrání soukromí uživatelů. Zjistěte více o Haveno na naší webové stránce projektu. setting.about.web=Webová stránka Haveno setting.about.code=Zdrojový kód setting.about.agpl=AGPL licence setting.about.support=Podpořte Haveno setting.about.def=Haveno není společnost - je to projekt otevřený komunitě. Pokud se chcete zapojit nebo podpořit Haveno, postupujte podle níže uvedených odkazů. setting.about.contribute=Přispět setting.about.providers=Poskytovatelé dat setting.about.apisWithFee=Haveno používá Haveno cenový index pro tržní ceny Fiat měn a Cryptou a Haveno Mempool Nodes pro odhad poplatků za těžbu. setting.about.apis=Haveno používá Haveno cenové indexy pro tržní ceny Fiat měn a Cryptoů. setting.about.pricesProvided=Tržní ceny poskytované setting.about.feeEstimation.label=Odhad poplatků za těžbu poskytl setting.about.versionDetails=Podrobnosti o verzi setting.about.version=Verze aplikace setting.about.subsystems.label=Verze subsystémů setting.about.subsystems.val=Verze sítě: {0}; Verze zpráv P2P: {1}; Verze lokální DB: {2}; Verze obchodního protokolu: {3} setting.about.shortcuts=Zkratky setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' nebo 'alt + {0}' nebo 'cmd + {0}' setting.about.shortcuts.menuNav=Procházet hlavní nabídku setting.about.shortcuts.menuNav.value=Pro pohyb v hlavním menu stiskněte: 'Ctrl' nebo 'alt' nebo 'cmd' s numerickou klávesou mezi '1-9' setting.about.shortcuts.close=Zavřít Haveno setting.about.shortcuts.close.value='Ctrl + {0}' nebo 'cmd + {0}' nebo 'Ctrl + {1}' nebo 'cmd + {1}' setting.about.shortcuts.closePopup=Zavřete vyskakovací nebo dialogové okno setting.about.shortcuts.closePopup.value=Klávesa 'ESCAPE' setting.about.shortcuts.chatSendMsg=Odeslat obchodní soukromou zprávu setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' nebo 'alt + ENTER' nebo 'cmd + ENTER' setting.about.shortcuts.openDispute=Otevřít spor setting.about.shortcuts.openDispute.value=Vyberte nevyřízený obchod a klikněte na: {0} setting.about.shortcuts.walletDetails=Otevřít okno s podrobností peněženky setting.about.shortcuts.openEmergencyXmrWalletTool=Otevřít nástroj nouzové peněženky pro XMR peněženku setting.about.shortcuts.showTorLogs=Přepnout úroveň protokolu pro zprávy Tor mezi DEBUG a WARN setting.about.shortcuts.manualPayoutTxWindow=Otevřít okno pro manuální výběr z vkladu 2z2 Multisig tx setting.about.shortcuts.removeStuckTrade=Otevřít vyskakovací okno pro přesun neúspěšného obchodu zpět na kartu otevřených obchodů setting.about.shortcuts.removeStuckTrade.value=Vyberte neúspěšný obchod a stiskněte: {0} setting.about.shortcuts.registerArbitrator=Registrovat rozhodce (pouze mediátor/rozhodce) setting.about.shortcuts.registerArbitrator.value=Přejděte na účet a stiskněte: {0} setting.about.shortcuts.registerMediator=Registrovat mediátora (pouze mediátor/rozhodce) setting.about.shortcuts.registerMediator.value=Přejděte na účet a stiskněte: {0} setting.about.shortcuts.openSignPaymentAccountsWindow=Otevřené okno pro podpis věku účtu (pouze starší rozhodci) setting.about.shortcuts.openSignPaymentAccountsWindow.value=Přejděte ke starému zobrazení rozhodce a stiskněte: {0} setting.about.shortcuts.sendAlertMsg=Odeslat výstražnou nebo aktualizační zprávu (privilegovaná aktivita) setting.about.shortcuts.sendFilter=Nastavit filtr (privilegovaná aktivita) setting.about.shortcuts.sendPrivateNotification=Odeslat soukromé oznámení partnerovi (privilegovaná aktivita) setting.about.shortcuts.sendPrivateNotification.value=Otevřete informace o uživateli kliknutím na avatar a stiskněte: {0} #################################################################### # Account #################################################################### account.tab.arbitratorRegistration=Registrace rozhodce account.tab.mediatorRegistration=Registrace mediátora account.tab.refundAgentRegistration=Registrace rozhodce pro vrácení peněz account.tab.signing=Podepisování account.menu.paymentAccount=Účty v národní měně account.menu.altCoinsAccountView=Kryptoměnové účty account.menu.password=Heslo peněženky account.menu.seedWords=Seed peněženky account.menu.walletInfo=Info o peněžence account.menu.backup=Záloha account.menu.notifications=Oznámení account.menu.walletInfo.balance.headLine=Zůstatky v peněžence account.menu.walletInfo.balance.info=Zde jsou zobrazeny celkové zůstatky v interní peněžence včetně nepotvrzených transakcí.\n\ Interní zůstatek XMR uvedený níže by měl odpovídat součtu hodnot 'Dostupný zůstatek' a 'Rezervováno v nabídkách' v pravém horním rohu aplikace. account.menu.walletInfo.xpub.headLine=Veřejné klíče (xpub) account.menu.walletInfo.walletSelector={0} {1} peněženka account.menu.walletInfo.path.headLine=HD identifikátory klíčů account.menu.walletInfo.path.info=Pokud importujete vaše seed slova do jiné peněženky (např. Electrum), budete muset nastavit také \ cestu. Toto provádějte pouze ve výjimečných případech, např. pokud úplně ztratíte kontrolu nad Haveno peněženkou a složkou dat.\n\ Mějte na paměti, že provádění transakcí pomocí jiných softwarových peněženek může snadno poškodit interní datové struktury \ systému Haveno, a znemožnit tak provádění obchodů.\n\n\ NIKDY neposílejte BSQ pomocí jiných softwarových peněženek než Haveno, protože byste tím velmi pravděpodobně vytvořili neplatnou BSQ transakci, a ztratili tak své BSQ. account.menu.walletInfo.openDetails=Zobrazit detailní data peněženky a soukromé klíče ## TODO should we rename the following to a gereric name? account.arbitratorRegistration.pubKey=Veřejný klíč account.arbitratorRegistration.register=Registrovat account.arbitratorRegistration.registration=Registrace {0} account.arbitratorRegistration.revoke=Odvolat account.arbitratorRegistration.info.msg=Upozorňujeme, že po odvolání musíte zůstat k dispozici 15 dní, protože mohou existovat obchody, pro které jste {0}. Max. povolené obchodní období je 8 dní a proces řešení sporu může trvat až 7 dní. account.arbitratorRegistration.warn.min1Language=Musíte nastavit alespoň 1 jazyk.\nPřidali jsme vám výchozí jazyk. account.arbitratorRegistration.removedSuccess=Úspěšně jste odstranili svou registraci ze sítě Haveno. account.arbitratorRegistration.removedFailed=Registraci se nepodařilo odebrat. {0} account.arbitratorRegistration.registerSuccess=Úspěšně jste se zaregistrovali do sítě Haveno. account.arbitratorRegistration.registerFailed=Registraci se nepodařilo dokončit. {0} account.crypto.yourCryptoAccounts=Vaše kryptoměnové účty account.crypto.popup.wallet.msg=Ujistěte se, že dodržujete požadavky na používání peněženek {0}, jak je \ popsáno na webové stránce {1}.\nPoužití peněženek z centralizovaných směnáren, kde (a) nevlastníte své soukromé klíče nebo \ (b) které nepoužívají kompatibilní software peněženk, je riskantní: může to vést ke ztrátě obchodovaných prostředků!\nMediátor nebo rozhodce \ není specialista {2} a v takových případech nemůže pomoci. account.crypto.popup.wallet.confirm=Rozumím a potvrzuji, že vím, jakou peněženku musím použít. # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=Obchodování s UPX na Haveno vyžaduje, abyste pochopili a splnili následující požadavky:\n\n\ K odeslání UPX musíte použít buď oficiální peněženku GUI uPlexa nebo CLI peněženku uPlexa \ s povoleným příznakem store-tx-info (výchozí hodnota v nových verzích). \ Ujistěte se, že máte přístup \ ke klíči tx, který může být vyžadován v případě sporu.\n\ uplexa-wallet-cli (použijte příkaz get_tx_key)\n\ uplexa-wallet-gui (přejděte na kartu historie a pro potvrzení platby klikněte na tlačítko (P))\n\n\ V normálním block exploreru není přenos ověřitelný.\n\n\ V případě sporu musíte rozhodci poskytnout následující údaje:\n\ - Soukromý klíč tx\n\ - Hash transakce\n\ - Veřejnou adresa příjemce\n\n\ Pokud neposkytnete výše uvedená data nebo použijete nekompatibilní peněženku, dojde ke prohrání sporu. \ Odesílatel UPX odpovídá za zajištění ověření přenosu UPX rozhodci \ v případě sporu.\n\n\ Není požadováno žádné platební ID, pouze normální veřejná adresa.\n\ Pokud si nejste jisti tímto procesem, vyhledejte další informace na discord kanálu uPlexa (https://discord.gg/vhdNSrV) \ nebo uPlexa Telegram Chatu (https://t.me/uplexaOfficial). # suppress inspection "UnusedProperty" account.crypto.popup.arq.msg=Obchodování ARQ na Haveno vyžaduje, abyste pochopili \ a splnili následující požadavky:\n\n\ K odeslání ARQ musíte použít buď oficiální peněženku ArQmA GUI nebo peněženku ArQmA CLI \ s povoleným příznakem store-tx-info (výchozí hodnota v nových verzích). \ Ujistěte se, že máte přístup ke klíči tx, který může být vyžadován v případě sporu.\n\ arqma-wallet-cli (použijte příkaz get_tx_key)\n\ arqma-wallet-gui (přejděte na kartu historie a pro potvrzení platby klikněte na tlačítko (P))\n\n\ V normálním prohlížeči bloků není přenos ověřitelný.\n\n\ V případě sporu musíte mediátorovi nebo rozhodci poskytnout následující údaje:\n\ - Soukromý klíč tx\n\ - Hash transakce\n\ - Veřejnou adresu příjemce\n\n\ Pokud neposkytnete výše uvedená data nebo použijete nekompatibilní peněženku, dojde k prohrání sporu. \ Odesílatel ARQ odpovídá za zajištění ověření převodu ARQ mediátorovi \ nebo rozhodci v případě sporu.\n\n\ Není požadováno žádné platební ID, pouze normální veřejná adresa.\n\ Pokud si nejste jisti tímto procesem, navštivte discord kanál ArQmA (https://discord.gg/s9BQpJT) \ nebo fórum ArQmA (https://labs.arqma.com). # suppress inspection "UnusedProperty" account.crypto.popup.xmr.msg=Obchodování s XMR na Haveno vyžaduje, abyste pochopili následující požadavek.\n\n\ Pokud prodáváte XMR, musíte být schopni v případě sporu poskytnout mediátorovi nebo rozhodci následující informace:\n\ - transakční klíč (Tx klíč, Tx tajný klíč nebo Tx soukromý klíč)\n\ - ID transakce (Tx ID nebo Tx Hash)\n\ - cílová adresa (adresa příjemce)\n\n\ Na wiki najdete podrobnosti, kde najdete tyto informace v populárních peněženkách Monero:\n[HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Proving_payments].\n\ Neposkytnutí požadovaných údajů o transakci bude mít za následek ztrátu sporů.\n\n\ Všimněte si také, že Haveno nyní nabízí automatické potvrzení transakcí XMR, aby byly obchody rychlejší, \ ale musíte to povolit v Nastavení.\n\n\ Další informace o funkci automatického potvrzení najdete na wiki: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. # suppress inspection "UnusedProperty" account.crypto.popup.msr.msg=Obchodování MSR na Haveno vyžaduje, abyste pochopili a splnili \ následující požadavky:\n\n\ K odeslání MSR musíte použít buď oficiální peněženku Masari GUI, peněženku Masari CLI s povoleným příznakem \ store-tx-info (ve výchozím nastavení povoleno) nebo webovou peněženku Masari (https://wallet.getmasari.org). Ujistěte se, že máte přístup ke klíči tx, \ který může být vyžadován v případě sporu.\n\ masari-wallet-cli (použijte příkaz get_tx_key)\n\ masari-wallet-gui (přejděte na kartu historie a klikněte na tlačítko (P) pro potvrzení platby)\n\n\ Webová peněženka Masari (jděte do Účet -> Historie transakcí a zobrazte podrobností o odeslané transakci)\n\n\ Ověření lze provést v peněžence.\n\ masari-wallet-cli: pomocí příkazu (check_tx_key).\n\ masari-wallet-gui: na stránce Pokročilé > Dokázat/Ověřit.\n\ Ověření lze provést v block exploreru\n\ Otevřete Block explorer (https://explorer.getmasari.org), použijte vyhledávací lištu k nalezení hash transakce.\n\ Jakmile je transakce nalezena, přejděte dolů do oblasti 'Prokázat odesílání' a podle potřeby vyplňte podrobnosti.\n\ V případě sporu musíte zprostředkovateli nebo rozhodci poskytnout následující údaje:\n\ - Soukromý klíč tx\n\ - Hash transakce\n\ - Veřejnou adresu příjemce\n\n\ Pokud neposkytnete výše uvedená data nebo použijete nekompatibilní peněženku, dojde ke ztrátě sporu. \ Odesílatel MSR odpovídá za zajištění ověření přenosu MSR mediátorovi \ nebo rozhodci v případě sporu.\n\n\ Není požadováno žádné platební ID, pouze normální veřejná adresa.\n\ Pokud si nejste jisti tímto procesem, požádejte o pomoc oficiální Masari Discord (https://discord.gg/sMCwMqs). # suppress inspection "UnusedProperty" account.crypto.popup.blur.msg=Obchodování BLUR na Haveno vyžaduje, abyste pochopili \ a splnili následující požadavky:\n\n\ K odeslání BLUR musíte použít Blur Network CLI nebo GUI peněženku.\n\n\ Používáte-li peněženku CLI, po odeslání transakce se zobrazí hash transakce (tx ID). Tyto informace si musíte uložit. \ Ihned po odeslání transakce musíte použít příkaz 'get_tx_key' pro načtení soukromého klíče transakce. \ Pokud tento krok neprovedete, pravděpodobně nebudete moci klíč získat později.\n\n\ Pokud používáte peněženku GUI Blur Network, lze soukromý klíč transakce a ID transakce pohodlně nalézt \ na kartě Historie. Ihned po odeslání vyhledejte příslušnou transakci. Klikněte na "?" symbol \ v pravém dolním rohu pole obsahující transakci. Tyto informace si musíte uložit.\n\n\ V případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit následující: 1.) ID transakce, \ 2.) soukromý klíč transakce a 3.) Adresu příjemce. Mediátor nebo rozhodce poté ověří přenos BLUR \ pomocí prohlížeče BLUR transakcí (https://blur.cash/#tx-viewer).\n\n\ Neposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. \ Ve všech sporných případech nese odesílatel BLUR 100% odpovědnosti za ověřování transakcí mediátorovi nebo rozhodci.\n\n\ Pokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na Blur Network Discord (https://discord.gg/dMWaqVW). # suppress inspection "UnusedProperty" account.crypto.popup.solo.msg=Obchodování Solo na Haveno vyžaduje, abyste pochopili a splnili \ následující požadavky:\n\n\ K odeslání Solo musíte použít peněženku CLI Solo Network.\n\n\ Používáte-li peněženku CLI, po odeslání přenosu se zobrazí hash transakce (tx ID). Tyto informace si musíte uložit. \ Ihned po odeslání převodu musíte použít příkaz 'get_tx_key' pro načtení soukromého klíče transakce. \ Pokud tento krok neprovedete, pravděpodobně nebudete moci klíč získat později.\n\n\ V případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit následující: 1.) ID transakce, \ 2.) Soukromý klíč transakce a 3.) Adresu příjemce. Mediátor nebo rozhodce poté ověří převod Solo \ pomocí Solo Block Exploreru vyhledáním transakce a poté pomocí funkce 'Prokažte odesílání' (https://explorer.minesolo.com/).\n\n\ neposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. \ Ve všech sporných případech nese Solo odesílatel 100% odpovědnost za ověřování transakcí mediátorovi nebo rozhodci.\n\n\ Pokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na stránce Solo Network Discord (https://discord.minesolo.com/). # suppress inspection "UnusedProperty" account.crypto.popup.cash2.msg=Obchodování CASH2 na Haveno vyžaduje, abyste pochopili \ a splnili následující požadavky:\n\n\ K odeslání CASH2 musíte použít peněženku Cash2 Wallet verze 3 nebo vyšší.\n\n\ Po odeslání transakce se zobrazí ID transakce. Tyto informace si musíte uložit. \ Ihned po odeslání transakce musíte použít příkaz 'getTxKey' v simplewallet \ a získat tajný klíč transakce.\n\n\ V případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit následující: 1) ID transakce, \ 2) Tajný klíč transakce a 3) Adresu Cash2 příjemce. Mediátor nebo rozhodce poté ověří převod CASH2 \ pomocí průzkumníku Cash2 Block Explorer (https://blocks.cash2.org).\n\n\ Neposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. \ Ve všech sporných případech nese odesílatel CASH2 100% odpovědnost za ověření transakcí mediátorovi nebo rozhodci. \n\n Pokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na Cash2 Discord (https://discord.gg/FGfXAYN). # suppress inspection "UnusedProperty" account.crypto.popup.qwertycoin.msg=Obchodování s Qwertycoinem na Haveno vyžaduje, abyste pochopili \ a splnili následující požadavky:\n\n\ K odeslání QWC musíte použít oficiální QWC peněženku verze 5.1.3 nebo vyšší.\n\n\ Po odeslání transakce se zobrazí ID transakce. Tyto informace si musíte uložit. \ Ihned po odeslání transakce musíte použít příkaz 'get_Tx_Key' v simplewallet \ a získat tajný klíč transakce.\n\n\ V případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit následující: 1) ID transakce, \ 2) Tajný klíč transakce a 3) Adresu QWC příjemce. Mediátor nebo rozhodce poté ověří přenos QWC \ pomocí Průzkumníka bloků QWC (https://explorer.qwertycoin.org).\n\n\ Neposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. \ Ve všech sporných případech nese odesílatel QWC 100% odpovědnost za ověřování transakcí mediátorovi nebo rozhodci.\n\n\ Pokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na stránce QWC Discord (https://discord.gg/rUkfnpC). # suppress inspection "UnusedProperty" account.crypto.popup.drgl.msg=Obchodování Dragonglass na Haveno vyžaduje, abyste pochopili a splnili \ následující požadavky:\n\n\ Vzhledem k tomu, že Dragonglass poskytuje soukromí, není transakce na veřejném blockchainu ověřitelná. V případě potřeby \ můžete svou platbu prokázat pomocí vašeho soukromého klíče TXN.\n\ Soukromý klíč TXN je jednorázový klíč automaticky generovaný pro každou transakci, \ ke které lze přistupovat pouze z vaší DRGL peněženky.\n\ Buď pomocí GUI peněženky DRGL (uvnitř dialogu s podrobnostmi o transakci) nebo pomocí simplewallet CLI Dragonglass (pomocí příkazu "get_tx_key").\n\n\ Verze DRGL 'Oathkeeper' a vyšší jsou požadovány pro obě možnosti.\n\n\ V případě sporu musíte mediátorovi nebo rozhodci poskytnout následující údaje:\n\ - TXN-soukromý klíč\n\ - Hash transakce\n\ - Veřejnou adresu příjemce\n\n Ověření platby lze provést pomocí výše uvedených údajů jako vstupů na adrese (http://drgl.info/#check_txn).\n\n\ Pokud neposkytnete výše uvedená data nebo použijete nekompatibilní peněženku, dojde ke ztrátě sporu. \ Odesílatel Dragonglass odpovídá za ověření přenosu DRGL mediátorovi nebo rozhodci v případě sporu. \ Použití PaymentID není nutné.\n\n Pokud si nejste jisti některou částí tohoto procesu, navštivte Dragonglass na Discordu (http://discord.drgl.info) pro pomoc. # suppress inspection "UnusedProperty" account.crypto.popup.ZEC.msg=Při použití Zcash můžete použít pouze transparentní adresy (začínající na t), nikoli \ z-adresy (soukromé), protože mediátor nebo rozhodce by nemohl ověřit transakci pomocí z-adres. # suppress inspection "UnusedProperty" account.crypto.popup.XZC.msg=Při použití Zcoinu můžete použít pouze transparentní (sledovatelné) adresy, \ nikoli nevysledovatelné adresy, protože mediátor nebo rozhodce by nemohl ověřit transakci s nevysledovatelnými adresami v blok exploreru. # suppress inspection "UnusedProperty" account.crypto.popup.grin.msg=GRIN vyžaduje k vytvoření transakce interaktivní proces mezi odesílatelem a příjemcem. \ Nezapomeňte postupovat podle pokynů z webové stránky projektu GRIN, abyste spolehlivě odeslali a přijali GRIN \ (příjemce musí být online nebo alespoň online v určitém časovém rozmezí).\n\n\ Haveno podporuje pouze formát URL peněženky Grinbox (Wallet713).\n\n\ Odesílatel GRIN je povinen prokázat, že GRIN úspěšně odeslal. Pokud peněženka nemůže tento důkaz poskytnout, \ bude potenciální spor vyřešen ve prospěch příjemce GRIN. Ujistěte se, že používáte \ nejnovější software Grinbox, který podporuje důkaz transakcí a že chápete proces přenosu \ a přijímání GRIN a také způsob, jak vytvořit důkaz.\n\n\ Viz https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only \ pro více informací o nástroji Grinbox proof. # suppress inspection "UnusedProperty" account.crypto.popup.beam.msg=BEAM vyžaduje k vytvoření transakce interaktivní proces \ mezi odesílatelem \ a příjemcem.\n\n\ Nezapomeňte postupovat podle pokynů na webové stránce projektu BEAM, abyste spolehlivě odeslali a přijali BEAM \ (příjemce musí být online nebo alespoň online během určitého časového období).\n\n\ Odesílatel BEAM je povinen prokázat, že úspěšně odeslali BEAM. \ Nezapomeňte použít software peněženku, která může takový důkaz předložit. Pokud peněženka nemůže poskytnout důkaz, \ bude potenciální spor vyřešen ve prospěch příjemce BEAM. # suppress inspection "UnusedProperty" account.crypto.popup.pars.msg=Trading ParsiCoin na Haveno vyžaduje, abyste pochopili a splnili následující požadavky:\n\n\ K odeslání PARS musíte použít oficiální ParsiCoin peněženku verze 3.0.0 nebo vyšší.\n\n\ V Peněženka GUI (ParsiPay) si můžete zkontrolovat svůj Hash Transakce a Klíč Transakce v sekci Transakce. \ Je zapotřebí kliknout na transakci a potom na zobrazení detailů. \n\n\ V případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit: 1) Hash Transakce, \ 2) Transakční Klíč a 3) Adresu PARS příjemce. Mediátor nebo rozhodce poté ověří přenos PARS \ pomocí Block exploreru ParsiCoin (http://explorer.parsicoin.net/#check_payment).\n\n\ Neposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. Ve všech sporných případech nese \ odesílatel ParsiCoin 100% odpovědnost za ověřování transakcí mediátorovi nebo rozhodci.\n\n\ Pokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na ParsiCoin Discord (https://discord.gg/c7qmFNh). # suppress inspection "UnusedProperty" account.crypto.popup.blk-burnt.msg=Chcete-li obchodovat s burnt blackcoiny, musíte znát následující:\n\n\ Burnt blackcoiny jsou nevyčerpatelné. Aby je bylo možné obchodovat na Haveno, musí mít výstupní skripty podobu: \ OP_RETURN OP_PUSHDATA, následované přidruženými datovými bajty, které po hexadecimálním zakódování tvoří adresy. \ Například Burnt blackcoiny s adresou 666f6f (“foo” v UTF-8) budou mít následující skript:\n\n\ OP_RETURN OP_PUSHDATA 666f6f\n\n\ Pro vytvoření Burnt blackcoinů lze použít příkaz ”burn” RPC, který je k dispozici v některých peněženkách.\n\n\ Pro možné případy použití se můžete podívat na https://ibo.laboratorium.ee.\n\n\ Vzhledem k tomu, že Burnt blackcoiny jsou nevyčerpatelné, nelze je znovu prodat. ”Prodej” \ Burnt blackcoinů znamená vypalování běžných blackcoinů (s přidruženými údaji rovnými cílové adrese).\n\n\ V případě sporu musí prodejce BLK poskytnout hash transakce. # suppress inspection "UnusedProperty" account.crypto.popup.liquidmonero.msg=Obchodování s L-XMR na Haveno vyžaduje, abyste rozuměli následujícím skutečnostem.:\n\n\ Při přijímání L-XMR za obchod na platformě Haveno nemůžete použít mobilní aplikaci Blockstream Green Wallet nebo \ peněženku custodial/směnárny. L-XMR musíte přijímat pouze do peněženky Liquid Elements Core nebo do jiné \ L-XMR peněženky, která vám umožní získat zaslepovací(blinding) klíč pro vaši zaslepenou adresu L-XMR.\n\n\ V případě nutnosti mediace nebo v případě vzniku obchodního sporu je nutné zveřejnit zaslepovací klíč pro \ vaši přijímající adresu L-XMR mediátorovi Haveno nebo agentovi vrácení financí, aby mohl ověřit detaily \ vaší důvěrné transakce na vlastním Elements Core full node.\n\n\ Neposkytnutí požadovaných informací zprostředkovateli nebo agentovi vrácení financí, bude mít za následek ztrátu sporu. \ Ve všech případech sporu nese příjemce L-XMR 100% břemeno odpovědnosti \ poskytnutí kryptografického důkazu mediátorovi nebo agentovi pro vrácení financí.\n\n\ Pokud těmto požadavkům nerozumíte, neobchodujte s L-XMR na Havenu. account.traditional.yourTraditionalAccounts=Vaše účty v národní měně account.backup.title=Zálohujte peněženku account.backup.location=Umístění zálohy account.backup.selectLocation=Zvolte umístění zálohy account.backup.backupNow=Zálohujte nyní (záloha není šifrována!) account.backup.appDir=Adresář dat aplikace account.backup.openDirectory=Otevřít adresář account.backup.openLogFile=Otevřít soubor log account.backup.success=Záloha byla úspěšně uložena na:\n{0} account.backup.directoryNotAccessible=Vybraný adresář není přístupný. {0} account.password.removePw.button=Odstraňte heslo account.password.removePw.headline=Odstraňte ochranu peněženky pomocí hesla account.password.setPw.button=Nastavit heslo account.password.setPw.headline=Nastavte ochranu peněženky pomocí hesla account.password.info=S ochranou pomocí hesla budete muset zadat heslo při spuštění aplikace, při výběru monera z vaší peněženky a při zobrazení slov seedu peněženky. account.seed.backup.title=Zálohujte svá klíčová slova peněženky. account.seed.info=Prosím, zapište si jak klíčová slova peněženky, tak datum. Kdykoliv můžete obnovit svou peněženku pomocí klíčových slov a data.\n\nKlíčová slova byste měli zapsat na kus papíru. Neukládejte je na počítač.\n\nVezměte prosím na vědomí, že klíčová slova NEJSOU náhradou za zálohu.\nMusíte vytvořit zálohu celého adresáře aplikace z obrazovky \"Účet/Záloha\", abyste mohli obnovit stav a data aplikace. account.seed.backup.warning=Prosím, poznamenejte si, že klíčová slova nejsou náhradou za zálohu.\nMusíte vytvořit zálohu celého adresáře aplikace z obrazovky \"Účet/Záloha\", abyste mohli obnovit stav a data aplikace. account.seed.warn.noPw.msg=Nenastavili jste si heslo k peněžence, které by chránilo zobrazení seed slov.\n\n\ Chcete zobrazit seed slova? account.seed.warn.noPw.yes=Ano, a už se mě znovu nezeptat account.seed.enterPw=Chcete-li zobrazit seed slova, zadejte heslo account.seed.restore.info=Před použitím obnovení ze seed slov si vytvořte zálohu. Uvědomte si, že obnova peněženky je \ pouze pro naléhavé případy a může způsobit problémy s interní databází peněženky.\n\ Není to způsob, jak použít zálohu! K obnovení předchozího stavu aplikace \ použijte zálohu z adresáře dat aplikace.\n\n\ Po obnovení se aplikace automaticky vypne. Po restartování aplikace se bude znovu synchronizovat se sítí Monero. \ To může chvíli trvat a může spotřebovat hodně CPU, zejména pokud byla peněženka starší a měla mnoho transakcí. \ Vyhněte se přerušování tohoto procesu, jinak budete možná muset \ znovu odstranit soubor řetězu SPV nebo opakovat proces obnovy. account.seed.restore.ok=Dobře, proveďte obnovu a vypněte Haveno account.keys.clipboard.warning=Upozorňujeme, že soukromé klíče peněženky jsou velmi citlivé finanční údaje.\n\n\ ● Nikomu, kdo vás o ně požádá, byste neměli sdělovat své klíče, pokud si nejste naprosto jisti, že mu můžete důvěřovat při nakládání s vašimi penězi! \n\n\ ● Data soukromých klíčů byste NEMĚLI kopírovat do schránky, pokud si nejste naprosto jisti, že používáte zabezpečené počítačové prostředí bez rizik malwaru. \n\n\ Mnoho lidí tímto způsobem přišlo o své Monero. Pokud máte JAKÉKOLI pochybnosti, okamžitě zavřete tento dialog a vyhledejte pomoc někoho znalého. #################################################################### # Mobile notifications #################################################################### account.notifications.setup.title=Nastavení account.notifications.download.label=Stáhnout mobilní aplikaci account.notifications.waitingForWebCam=Čekání na webkameru... account.notifications.webCamWindow.headline=Naskenujte QR kód z telefonu account.notifications.webcam.label=Použijte webkameru account.notifications.webcam.button=Naskenujte QR kód account.notifications.noWebcam.button=Nemám webkameru account.notifications.erase.label=Vymazat oznámení na telefonu account.notifications.erase.title=Vymazat oznámení account.notifications.email.label=Párovací token account.notifications.email.prompt=Zadejte párovací token, který jste obdrželi e-mailem account.notifications.settings.title=Nastavení account.notifications.useSound.label=Přehrajte zvuk oznámení v telefonu account.notifications.trade.label=Dostávat zprávy o obchodu account.notifications.market.label=Dostávat upozornění na nabídky account.notifications.price.label=Dostávat upozornění o cenách account.notifications.priceAlert.title=Cenová upozornění account.notifications.priceAlert.high.label=Upozorněte, pokud bude cena XMR nad account.notifications.priceAlert.low.label=Upozorněte, pokud bude cena XMR pod account.notifications.priceAlert.setButton=Nastavit upozornění na cenu account.notifications.priceAlert.removeButton=Odstraňte upozornění na cenu account.notifications.trade.message.title=Obchodní stav se změnil account.notifications.trade.message.msg.conf=Vkladová transakce pro obchod s ID {0} je potvrzena. \ Otevřete prosím svou aplikaci Haveno a začněte s platbou. account.notifications.trade.message.msg.started=Kupující XMR zahájil platbu za obchod s ID {0}. account.notifications.trade.message.msg.completed=Obchod s ID {0} je dokončen. account.notifications.offer.message.title=Vaše nabídka byla přijata account.notifications.offer.message.msg=Vaše nabídka s ID {0} byla přijata account.notifications.dispute.message.title=Nová zpráva o sporu account.notifications.dispute.message.msg=Obdrželi jste zprávu o sporu pro obchod s ID {0} account.notifications.marketAlert.title=Upozornění na nabídku account.notifications.marketAlert.selectPaymentAccount=Nabídky odpovídající platebnímu účtu account.notifications.marketAlert.offerType.label=Typ nabídky, o kterou mám zájem account.notifications.marketAlert.offerType.buy=Nákupní nabídky (Chci prodat XMR) account.notifications.marketAlert.offerType.sell=Prodejní nabídky (Chci si koupit XMR) account.notifications.marketAlert.trigger=Nabídková cenová vzdálenost (%) account.notifications.marketAlert.trigger.info=Když je nastavena cenová vzdálenost, obdržíte upozornění pouze v případě, \ že je zveřejněna nabídka, která splňuje (nebo překračuje) vaše požadavky. Příklad: chcete prodat XMR, \ ale budete prodávat pouze s 2% přirážkou k aktuální tržní ceně. Nastavení tohoto pole na 2% zajistí, \ že budete dostávat upozornění pouze na nabídky s cenami, které jsou o 2% (nebo více) nad aktuální tržní cenou. account.notifications.marketAlert.trigger.prompt=Procentní vzdálenost od tržní ceny (např. 2,50%, -0,50% atd.) account.notifications.marketAlert.addButton=Přidat upozornění na nabídku account.notifications.marketAlert.manageAlertsButton=Spravovat upozornění na nabídku account.notifications.marketAlert.manageAlerts.title=Spravovat upozornění na nabídku account.notifications.marketAlert.manageAlerts.header.paymentAccount=Platební účet account.notifications.marketAlert.manageAlerts.header.trigger=Limitní cena account.notifications.marketAlert.manageAlerts.header.offerType=Typ nabídky account.notifications.marketAlert.message.title=Upozornění na nabídku account.notifications.marketAlert.message.msg.below=pod account.notifications.marketAlert.message.msg.above=nad account.notifications.marketAlert.message.msg=Do Haveno byla zveřejněna nová nabídka '{0} {1}' s cenou {2} ({3} {4} tržní cena) a \ metoda platby '{5}'.\n\ ID nabídky: {6}. account.notifications.priceAlert.message.title=Upozornění na cenu pro {0} account.notifications.priceAlert.message.msg=Vaše upozornění na cenu bylo aktivováno. Aktuální {0} cena je {1} {2} account.notifications.noWebCamFound.warning=Nebyla nalezena žádná webkamera.\n\n\ Použijte e-mailu k odeslání tokenu a šifrovacího klíče z vašeho mobilního telefonu do aplikace Haveno. account.notifications.priceAlert.warning.highPriceTooLow=Vyšší cena musí být větší než nižší cena. account.notifications.priceAlert.warning.lowerPriceTooHigh=Nižší cena musí být nižší než vyšší cena. #################################################################### # Windows #################################################################### inputControlWindow.headline=Vyberte vstupy pro transakci inputControlWindow.balanceLabel=Dostupný zůstatek contractWindow.title=Podrobnosti o sporu contractWindow.dates=Datum nabídky / Datum obchodu contractWindow.xmrAddresses=Monero adresa kupujícího XMR / prodávajícího XMR contractWindow.onions=Síťová adresa kupující XMR / prodávající XMR contractWindow.accountAge=Stáří účtu XMR kupující / XMR prodejce contractWindow.numDisputes=Počet sporů XMR kupující / XMR prodejce contractWindow.contractHash=Hash kontraktu displayAlertMessageWindow.headline=Důležitá informace! displayAlertMessageWindow.update.headline=Důležité informace o aktualizaci! displayAlertMessageWindow.update.download=Stáhnout: displayUpdateDownloadWindow.downloadedFiles=Soubory: displayUpdateDownloadWindow.downloadingFile=Stahuji: {0} displayUpdateDownloadWindow.verifiedSigs=Podpis ověřen pomocí klíčů: displayUpdateDownloadWindow.status.downloading=Stahuji soubory... displayUpdateDownloadWindow.status.verifying=Ověřování podpisu ... displayUpdateDownloadWindow.button.label=Stáhněte si instalační program a ověřte podpis displayUpdateDownloadWindow.button.downloadLater=Stáhnout později displayUpdateDownloadWindow.button.ignoreDownload=Ignorovat tuto verzi displayUpdateDownloadWindow.headline=K dispozici je nová aktualizace Haveno! displayUpdateDownloadWindow.download.failed.headline=Stahování selhalo displayUpdateDownloadWindow.download.failed=Stažení se nezdařilo.\n\ Stáhněte a ručně ověřte na adrese [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.installer.failed=Nelze určit správný instalační program. Stáhněte a ručně ověřte na adrese \ [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.verify.failed=Ověření se nezdařilo.\n\ Stáhněte a ručně ověřte na adrese [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.success=Nová verze byla úspěšně stažena a podpis ověřen.\n\n\ Otevřete adresář ke stažení, vypněte aplikaci a nainstalujte novou verzi. displayUpdateDownloadWindow.download.openDir=Otevřít adresář ke stažení disputeSummaryWindow.title=Souhrn disputeSummaryWindow.openDate=Datum otevření úkolu disputeSummaryWindow.role=Role obchodníka disputeSummaryWindow.payout=Výplata částky obchodu disputeSummaryWindow.payout.getsTradeAmount=XMR {0} dostane výplatu částky obchodu disputeSummaryWindow.payout.getsAll=XMR {0} dostane maximální výplatu disputeSummaryWindow.payout.custom=Vlastní výplata disputeSummaryWindow.payoutAmount.buyer=Výše výplaty kupujícího disputeSummaryWindow.payoutAmount.seller=Výše výplaty prodejce disputeSummaryWindow.payoutAmount.invert=Poražený ve sporu odesílá transakci disputeSummaryWindow.reason=Důvod sporu disputeSummaryWindow.tradePeriodEnd=Konec obchodního období disputeSummaryWindow.extraInfo=Detailní informace disputeSummaryWindow.delayedPayoutStatus=Stav zpožděné transakce # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BUG=Chyba # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.USABILITY=Použitelnost # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Porušení protokolu # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.NO_REPLY=Bez odpovědi # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SCAM=Podvod # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OTHER=Jiný # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BANK_PROBLEMS=Banka # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OPTION_TRADE=Obchodování opcí # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Obchodník neodpovídá # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Špatný účet odesílatele # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PEER_WAS_LATE=Obchodní partner se opozdil # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Obchod je již vypořádán disputeSummaryWindow.summaryNotes=Souhrnné poznámky disputeSummaryWindow.addSummaryNotes=Přidejte souhrnné poznámky disputeSummaryWindow.close.button=Zavřít úkol # Do no change any line break or order of tokens as the structure is used for signature verification # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.msg=Úkol uzavřen {0}\n\ {1} adresa uzlu: {2}\n\n\ Souhrn:\n\ Obchodní ID: {3}\n\ Měna: {4}\n\ Důvod sporu: {5}\n\n\ Výše obchodu: {6}\n\ Výplatní částka pro kupujícího XMR: {7}\n\ Výplatní částka pro prodejce XMR: {8}\n\n\ Souhrnné poznámky:\n{9}\n # Do no change any line break or order of tokens as the structure is used for signature verification disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3} disputeSummaryWindow.close.nextStepsForMediation=\nDalší kroky:\n\ Otevřete obchod a přijměte nebo odmítněte návrhy od mediátora disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nDalší kroky:\n\ Nevyžadují se od vás žádné další kroky. Pokud rozhodce rozhodl ve váš prospěch, v sekci Prostředky/Transakce se zobrazí transakce 'Vrácení peněz z rozhodčího řízení' disputeSummaryWindow.close.closePeer=Potřebujete také zavřít žádost obchodního partnera! disputeSummaryWindow.close.txDetails.headline=Zveřejněte transakci vrácení peněz # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.buyer=Kupující obdrží {0} na adresu: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=Prodejce obdrží {0} na adresu: {1}\n disputeSummaryWindow.close.txDetails=Výdaje: {0}\n\ {1} {2}\ Transakční poplatek: {3}\n\n\ Opravdu chcete tuto transakci zveřejnit? disputeSummaryWindow.close.noPayout.headline=Uzavřít bez jakékoli výplaty disputeSummaryWindow.close.noPayout.text=Chcete zavřít bez výplaty? disputeSummaryWindow.close.alreadyPaid.headline=Výplata již proběhla disputeSummaryWindow.close.alreadyPaid.text=Restartujte klienta pro provedení další výplaty u tohoto sporu emptyWalletWindow.headline={0} nouzový nástroj peněženky emptyWalletWindow.info=Použijte jej pouze v naléhavých případech, pokud nemůžete získat přístup k vašim prostředkům z uživatelského rozhraní.\n\n\ Upozorňujeme, že při použití tohoto nástroje budou všechny otevřené nabídky automaticky uzavřeny.\n\n\ Před použitím tohoto nástroje si prosím zálohujte datový adresář. \ Můžete to udělat na obrazovce \"Účet/Záloha\".\n\n\ Nahlaste nám svůj problém a nahlaste nám chybu na GitHubu nebo na fóru Haveno, abychom mohli prozkoumat, co způsobilo problém. emptyWalletWindow.balance=Váš zůstatek v peněžence emptyWalletWindow.address=Vaše cílová adresa emptyWalletWindow.button=Pošlete všechny prostředky emptyWalletWindow.openOffers.warn=Máte otevřené nabídky, které budou odstraněny, pokud vyprázdníte peněženku.\nOpravdu chcete vyprázdnit peněženku? emptyWalletWindow.openOffers.yes=Ano, jsem si jistý emptyWalletWindow.sent.success=Zůstatek vaší peněženky byl úspěšně přenesen. enterPrivKeyWindow.headline=Zadejte soukromý klíč pro registraci filterWindow.headline=Upravit seznam filtrů filterWindow.offers=Filtrované nabídky (oddělené čárkami) filterWindow.onions=Onion adresy vyloučené z obchodování (oddělené čárkami) filterWindow.bannedFromNetwork=Onion adresy blokované ze síťové komunikace (oddělené čárkami) filterWindow.accounts=Filtrovaná data obchodního účtu:\nFormát: seznam [ID platební metody | datové pole | hodnota] oddělený čárkami filterWindow.bannedCurrencies=Blokované kódy měn (oddělené čárkami) filterWindow.bannedPaymentMethods=ID blokované platební metody (oddělené čárkami) filterWindow.bannedAccountWitnessSignerPubKeys=Blokované veřejné klíče účtů podepisujícího svědka (hex nebo pub klíče oddělené čárkou) filterWindow.bannedPrivilegedDevPubKeys=Blokované privilegované klíče pub dev (hex nebo pub klíče oddělené čárkou) filterWindow.arbitrators=Filtrovaní rozhodci (onion adresy oddělené čárkami) filterWindow.mediators=Filtrovaní mediátoři (onion adresy oddělené čárkami) filterWindow.refundAgents=Filtrovaní rozhodci pro vrácení peněz (onion adresy oddělené čárkami) filterWindow.seedNode=Filtrované seed uzly (onion adresy oddělené čárkami) filterWindow.priceRelayNode=Filtrované cenové relay uzly (onion adresy oddělené čárkami) filterWindow.xmrNode=Filtrované uzly Monero (adresy+porty oddělené čárkami) filterWindow.preventPublicXmrNetwork=Zabraňte použití veřejné sítě Monero filterWindow.disableAutoConf=Zakázat automatické potvrzení filterWindow.autoConfExplorers=Filtrované průzkumníky s automatickým potvrzením (adresy oddělené čárkami) filterWindow.disableTradeBelowVersion=Min. verze nutná pro obchodování filterWindow.add=Přidat filtr filterWindow.remove=Zrušit filtr filterWindow.xmrFeeReceiverAddresses=Adresy příjemců poplatků XMR filterWindow.disableApi=Deaktivovat API filterWindow.disableMempoolValidation=Deaktivovat validaci mempoolu offerDetailsWindow.minXmrAmount=Min. částka XMR offerDetailsWindow.min=(min. {0}) offerDetailsWindow.distance=(vzdálenost od tržní ceny: {0}) offerDetailsWindow.myTradingAccount=Můj obchodní účet offerDetailsWindow.bankId=ID banky (např. BIC nebo SWIFT) offerDetailsWindow.countryBank=Země původu banky tvůrce offerDetailsWindow.commitment=Závazek offerDetailsWindow.agree=Souhlasím offerDetailsWindow.tac=Pravidla a podmínky offerDetailsWindow.confirm.maker.buy=Potvrdit: Vytvořit nabídku na nákup XMR za {0} offerDetailsWindow.confirm.maker.sell=Potvrdit: Vytvořit nabídku na prodej XMR za {0} offerDetailsWindow.confirm.taker.buy=Potvrdit: Přijmout nabídku na nákup XMR za {0} offerDetailsWindow.confirm.taker.sell=Potvrdit: Přijmout nabídku na prodej XMR za {0} offerDetailsWindow.creationDate=Datum vzniku offerDetailsWindow.makersOnion=Onion adresa tvůrce offerDetailsWindow.challenge=Passphrase nabídky offerDetailsWindow.challenge.copy=Zkopírujte přístupovou frázi pro sdílení s protějškem qRCodeWindow.headline=QR Kód qRCodeWindow.msg=Použijte tento QR kód k financování vaší peněženky Haveno z vaší externí peněženky. qRCodeWindow.request=Žádost o platbu:\n{0} selectDepositTxWindow.headline=Vyberte vkladovou transakci ke sporu selectDepositTxWindow.msg=Vkladová transakce nebyla v obchodě uložena.\n\ Vyberte prosím jednu z existujících multisig transakcí z vaší peněženky, která byla \ vkladovou transakcí použitou při selhání obchodu.\n\n\ Správnou transakci najdete tak, že otevřete okno s podrobnostmi o obchodu (klikněte na ID obchodu v seznamu)\ a sledujete výstup transakce s platebním poplatkem za obchodní transakci k následující transakci, \ kde uvidíte multisig vkladovou transakci (adresa začíná na 3). Toto ID transakce by mělo být \ viditelné v seznamu zde prezentovaném. Jakmile najdete správnou transakci, vyberte ji a pokračujte.\n\n\ Omlouváme se za nepříjemnosti, ale tento případ chyby by se měl stát velmi zřídka \ a v budoucnu se pokusíme najít lepší způsoby, jak jej vyřešit. selectDepositTxWindow.select=Vyberte vkladovou transakci sendAlertMessageWindow.headline=Odeslat globální oznámení sendAlertMessageWindow.alertMsg=Výstražná zpráva sendAlertMessageWindow.enterMsg=Zadejte zprávu sendAlertMessageWindow.isSoftwareUpdate=Oznámení o nové verzi software sendAlertMessageWindow.isUpdate=Plná verze sendAlertMessageWindow.isPreRelease=Beta verze sendAlertMessageWindow.version=Číslo nové verze sendAlertMessageWindow.send=Odeslat oznámení sendAlertMessageWindow.remove=Odebrat oznámení sendPrivateNotificationWindow.headline=Odeslat soukromou zprávu sendPrivateNotificationWindow.privateNotification=Soukromé oznámení sendPrivateNotificationWindow.enterNotification=Zadejte oznámení sendPrivateNotificationWindow.send=Odeslat soukromé oznámení showWalletDataWindow.walletData=Data peněženky showWalletDataWindow.includePrivKeys=Zahrnout soukromé klíče setXMRTxKeyWindow.headline=Prokázat odeslání XMR setXMRTxKeyWindow.note=Přidání tx informací níže umožní automatické potvrzení pro rychlejší obchody. Zobrazit více: https://haveno.exchange/wiki/Trading_Monero setXMRTxKeyWindow.txHash=ID transakce (volitelné) setXMRTxKeyWindow.txKey=Transakční klíč (volitelný) # We do not translate the tac because of the legal nature. We would need translations checked by lawyers # in each language which is too expensive atm. tacWindow.headline=Uživatelská dohoda tacWindow.agree=Souhlasím tacWindow.disagree=Nesouhlasím a odcházím tacWindow.arbitrationSystem=Rozhodnutí sporu tradeDetailsWindow.headline=Obchod tradeDetailsWindow.disputedPayoutTxId=ID sporné platební transakce tradeDetailsWindow.tradeDate=Datum obchodu tradeDetailsWindow.txFee=Poplatek za těžbu tradeDetailsWindow.tradePeersOnion=Onion adresa obchodního partnera tradeDetailsWindow.tradePeersPubKeyHash=Pubkey hash obchodních partnerů tradeDetailsWindow.tradeState=Stav obchodu tradeDetailsWindow.tradePhase=Fáze obchodu tradeDetailsWindow.agentAddresses=Rozhodce/Mediátor tradeDetailsWindow.detailData=Detailní data txDetailsWindow.headline=Detaily transakce txDetailsWindow.xmr.noteSent=Poslali jste XMR. txDetailsWindow.xmr.noteReceived=Obdrželi jste XMR. txDetailsWindow.sentTo=Odesláno na txDetailsWindow.receivedWith=Přijato s txDetailsWindow.txId=TxId txDetailsWindow.txKey=Klíč transakce closedTradesSummaryWindow.headline=Souhrn uzavřených obchodů closedTradesSummaryWindow.totalAmount.title=Celkový objem obchodů closedTradesSummaryWindow.totalAmount.value={0} ({1} podle aktuální tržní ceny) closedTradesSummaryWindow.totalVolume.title=Celkový objem obchodovaný v {0} closedTradesSummaryWindow.totalMinerFee.title=Suma poplatků za těžbu closedTradesSummaryWindow.totalMinerFee.value={0} ({1} z celkového objemu obchodů) closedTradesSummaryWindow.totalTradeFeeInXmr.title=Suma obchodních poplatků v XMR closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} z celkového objemu obchodů) walletPasswordWindow.headline=Pro odemknutí zadejte heslo xmrConnectionError.headline=Chyba připojení k Monero xmrConnectionError.providedNodes=Chyba při připojení k poskytnutým serverům Monero.\n\nChcete použít další nejlepší server Monero? xmrConnectionError.customNodes=Chyba při připojení k vašim vlastním serverům Monero.\n\nChcete použít další nejlepší server Monero? xmrConnectionError.localNode=Haveno bylo dříve připojeno k místnímu uzlu Monero, ale nyní jej nelze dosáhnout.\n\nUjistěte se, že váš místní uzel je spuštěn a plně synchronizován, nebo zvolte jinou možnost pro pokračování. xmrConnectionError.localNode.start=Spustit místní server xmrConnectionError.localNode.start.error=Chyba spuštění místního serveru xmrConnectionError.localNode.fallback=Připojit se k dalšímu nejvhodnějšímu uzlu torNetworkSettingWindow.header=Nastavení sítě Tor torNetworkSettingWindow.noBridges=Nepoužívat most (bridge) torNetworkSettingWindow.providedBridges=Spojte se s poskytnutými mosty (bridges) torNetworkSettingWindow.customBridges=Zadejte vlastní mosty (bridge) torNetworkSettingWindow.transportType=Typ přepravy torNetworkSettingWindow.obfs3=obfs3 torNetworkSettingWindow.obfs4=obfs4 (doporučeno) torNetworkSettingWindow.meekAmazon=meek-amazon torNetworkSettingWindow.meekAzure=meek-azure torNetworkSettingWindow.enterBridge=Zadejte jeden nebo více bridge relays (jeden na řádek) torNetworkSettingWindow.enterBridgePrompt=typ addresa:port torNetworkSettingWindow.restartInfo=Chcete-li použít změny, musíte restartovat aplikaci torNetworkSettingWindow.openTorWebPage=Otevřít webovou stránku projektu Tor torNetworkSettingWindow.deleteFiles.header=Problémy s připojením? torNetworkSettingWindow.deleteFiles.info=Pokud máte při spuštění opakované problémy s připojením, může pomoci odstranění zastaralých souborů Tor. Chcete-li to provést, klikněte na tlačítko níže a poté restartujte aplikaci. torNetworkSettingWindow.deleteFiles.button=Odstranit zastaralé soubory Tor a vypnout aplikaci torNetworkSettingWindow.deleteFiles.progress=Probíhá vypínání sítě Tor torNetworkSettingWindow.deleteFiles.success=Zastaralé soubory Tor byly úspěšně odstraněny. Prosím restartujte aplikaci. torNetworkSettingWindow.bridges.header=Je Tor blokovaný? torNetworkSettingWindow.bridges.info=Pokud je Tor zablokován vaším internetovým poskytovatelem nebo vaší zemí, můžete zkusit použít Tor mosty (bridges).\n\ Navštivte webovou stránku Tor na adrese: https://bridges.torproject.org, \ kde se dozvíte více o mostech a pluggable transports. feeOptionWindow.useXMR=Použít XMR feeOptionWindow.fee={0} (≈ {1}) feeOptionWindow.xmrFeeWithFiatAndPercentage={0} (≈ {1} / {2}) feeOptionWindow.xmrFeeWithPercentage={0} ({1}) #################################################################### # Popups #################################################################### popup.headline.notification=Oznámení popup.headline.instruction=Upozornění: popup.headline.attention=Pozor popup.headline.backgroundInfo=Základní informace popup.headline.feedback=Hotovo popup.headline.confirmation=Potvrzení popup.headline.information=Informace popup.headline.warning=Varování popup.headline.error=Chyba popup.doNotShowAgain=Znovu nezobrazovat popup.reportError.log=Otevřít log popup.reportError.gitHub=Nahlaste problém na GitHub popup.reportError={0}\n\nChcete-li nám pomoci vylepšit software, nahlaste tuto chybu otevřením nového problému na adrese https://github.com/haveno-dex/haveno/issues.\n\ Výše uvedená chybová zpráva bude zkopírována do schránky po kliknutí na některé z níže uvedených tlačítek.\n\ Usnadníte ladění, pokud zahrnete soubor haveno.log stisknutím tlačítka 'Otevřít log soubor', uložením kopie a připojením ke hlášení chyby. popup.error.tryRestart=Zkuste prosím restartovat aplikaci a zkontrolovat síťové připojení, abyste zjistili, zda můžete problém vyřešit. popup.error.takeOfferRequestFailed=Když se někdo pokusil využít jednu z vašich nabídek, došlo k chybě:\n{0} error.spvFileCorrupted=Při čtení řetězce SPV došlo k chybě.\nJe možné, že je poškozen řetězový soubor SPV.\n\nChybová zpráva: {0}\n\nChcete ji smazat a začít znovu synchronizovat? error.deleteAddressEntryListFailed=Soubor AddressEntryList nelze smazat.\nChyba: {0} error.closedTradeWithUnconfirmedDepositTx=Vkladová transakce uzavřeného obchodu s obchodním ID {0} je stále \ nepotvrzená.\n\n\ Proveďte prosím SPV resynchronizaci v \"Nastavení/Informace o síti\" a zkontrolujte, zda je transakce platná. error.closedTradeWithNoDepositTx=Vkladová transakce uzavřeného obchodu s obchodním ID {0} je nulová.\n\n\ Chcete-li vyčistit seznam uzavřených obchodů, restartujte aplikaci. popup.warning.walletNotInitialized=Peněženka ještě není inicializována popup.warning.wrongVersion=Pravděpodobně máte nesprávnou verzi Haveno pro tento počítač.\n\ Architektura vašeho počítače je: {0}.\n\ Binární kód Haveno, který jste nainstalovali, je: {1}.\n\ Vypněte prosím a znovu nainstalujte správnou verzi ({2}). popup.warning.incompatibleDB=Zjistili jsme nekompatibilní soubory databáze!\n\n\ Tyto databázové soubory nejsou kompatibilní s naší aktuální kódovou základnou:\n{0}\n\n\ Vytvořili jsme zálohu poškozených souborů a aplikovali jsme výchozí hodnoty na novou verzi databáze.\n\n\ Záloha se nachází na adrese:\n\ {1}/db/backup_of_corrupted_data.\n\n\ Zkontrolujte, zda máte nainstalovanou nejnovější verzi Haveno.\n\ Můžete si jej stáhnout na adrese: [HYPERLINK:https://haveno.exchange/downloads].\n\n\ Restartujte prosím aplikaci. popup.warning.startupFailed.twoInstances=Haveno již běží. Nemůžete mít spuštěné dvě instance Haveno. popup.warning.tradePeriod.halfReached=Váš obchod s ID {0} dosáhl poloviny max. povoleného obchodního období a stále není dokončen.\n\nObdobí obchodování končí {1}\n\nDalší informace o stavu obchodu naleznete na adrese \"Portfolio/Otevřené obchody\". popup.warning.tradePeriod.ended=Váš obchod s ID {0} dosáhl max. povoleného obchodního období a není dokončen.\n\n\ Období obchodování skončilo {1}\n\n\ Zkontrolujte prosím svůj obchod v sekci "Portfolio/Otevřené obchody\", abyste kontaktovali mediátora. popup.warning.noTradingAccountSetup.headline=Nemáte nastaven obchodní účet popup.warning.noTradingAccountSetup.msg=Než budete moci vytvořit nabídku, musíte si nastavit národní měnu nebo kryptoměnový účet.\nChcete si založit účet? popup.warning.noArbitratorsAvailable=Nejsou k dispozici žádní rozhodci. popup.warning.noMediatorsAvailable=Nejsou k dispozici žádní mediátoři. popup.warning.notFullyConnected=Musíte počkat, až budete plně připojeni k síti.\nTo může při spuštění trvat až 2 minuty. popup.warning.notSufficientConnectionsToXmrNetwork=Musíte počkat, až budete mít alespoň {0} připojení k síti Monero. popup.warning.downloadNotComplete=Musíte počkat, až bude dokončeno stahování chybějících bloků Monero. popup.warning.walletNotSynced=Haveno peněženka není synchronizována s nejnovější výškou blockchainu. Počkejte, dokud se peněženka nesynchronizuje, nebo zkontrolujte své připojení. popup.warning.removeOffer=Opravdu chcete tuto nabídku odebrat? popup.warning.tooLargePercentageValue=Nelze nastavit procento 100% nebo větší. popup.warning.examplePercentageValue=Zadejte procento jako číslo \"5.4\" pro 5.4% popup.warning.noPriceFeedAvailable=Pro tuto měnu není k dispozici žádný zdroj cen. Nelze použít procentuální cenu.\nVyberte pevnou cenu. popup.warning.sendMsgFailed=Odeslání zprávy vašemu obchodnímu partnerovi se nezdařilo.\nZkuste to prosím znovu a pokud to i nadále selže, nahlaste chybu. popup.warning.messageTooLong=Vaše zpráva překračuje max. povolená velikost. Zašlete jej prosím v několika částech nebo ji nahrajte do služby, jako je https://pastebin.com. popup.warning.lockedUpFunds=Zamkli jste finanční prostředky z neúspěšného obchodu.\n\ Uzamčený zůstatek: {0}\n\ Adresa vkladové tx: {1}\n\ ID obchodu: {2}.\n\n\ Otevřete prosím úkol pro podporu výběrem obchodu na obrazovce otevřených obchodů a stisknutím \"alt + o\" nebo \"option + o\"." popup.warning.moneroConnection=Došlo k problému s připojením k síti Monero.\n\n{0} popup.warning.makerTxInvalid=Tato nabídka není platná. Prosím vyberte jinou nabídku.\n\n takeOffer.cancelButton=Zrušit akceptaci nabídky takeOffer.warningButton=Ignorovat a přesto pokračovat # suppress inspection "UnusedProperty" popup.warning.nodeBanned=Jeden z {0} uzlů byl zablokován. # suppress inspection "UnusedProperty" popup.warning.priceRelay=cenové relé popup.warning.seed=seed popup.warning.mandatoryUpdate.trading=Aktualizujte prosím na nejnovější verzi Haveno. \ Byla vydána povinná aktualizace, která zakazuje obchodování se starými verzemi. \ Další informace naleznete na fóru Haveno. popup.warning.noFilter=Nepřijali jsme objekt filtru od seedových uzlů. Prosím informujte správce sítě, aby zaregistrovali objekt filtru. popup.warning.burnXMR=Tato transakce není možná, protože poplatky za těžbu {0} by přesáhly částku převodu {1}. \ Počkejte prosím, dokud nebudou poplatky za těžbu opět nízké nebo dokud nenahromadíte více XMR k převodu. popup.warning.openOffer.makerFeeTxRejected=Transakční poplatek tvůrce za nabídku s ID {0} byl odmítnut sítí Monero.\n\ ID transakce = {1}.\n\ Nabídka byla odebrána, aby se zabránilo dalším problémům.\n\ Běžte prosím do \"Nastavení/Informace o síti\" a proveďte SPV resynchronizaci.\n\ Pro další pomoc prosím použijte kanál podpory Haveno Keybase teamu. popup.warning.trade.txRejected.tradeFee=obchodní poplatek popup.warning.trade.txRejected.deposit=vklad popup.warning.trade.txRejected=Síť Monero odmítla {0} transakci pro obchod s ID {1}.\n\ ID transakce = {2}\n\ Nabídka byla přesunuta mezi ty, které selhaly.\n\ Běžte prosím do \"Nastavení/Informace o síti\" a proveďte SPV resynchronizaci.\n\ Pro další pomoc prosím použijte kanál podpory Haveno Keybase teamu. popup.warning.openOfferWithInvalidMakerFeeTx=Transakční poplatek tvůrce za nabídku s ID {0} je neplatný.\n\ ID transakce = {1}.\n\ Běžte prosím do \"Nastavení/Informace o síti\" a proveďte SPV resynchronizaci.\n\ Pro další pomoc prosím použijte kanál podpory Haveno Keybase teamu. popup.info.cashDepositInfo=Ujistěte se, že ve své oblasti máte pobočku banky, abyste mohli provést hotovostní vklad.\n\ ID banky prodávajícího (BIC/SWIFT) je: {0}. popup.info.cashDepositInfo.confirm=Potvrzuji, že mohu provést vklad popup.info.shutDownWithOpenOffers=Haveno se vypíná, ale existují otevřené nabídky.\n\n\ Tyto nabídky nebudou dostupné v síti P2P, pokud bude Haveno vypnutý, ale budou znovu \ zveřejněny do sítě P2P při příštím spuštění Haveno. Chcete-li zachovat své nabídky \ online, udržujte Haveno spuštěné a připojení k internetu (ujistěte se, že počítač \ nepřejde do pohotovostního režimu... pohotovostní režim monitoru není problém). popup.info.shutDownWithTradeInit={0}\n\ Tento obchod se ještě neinicializoval; jeho ukončení by pravděpodobně vedlo k jeho poškození. Počkejte prosím minutu a zkuste to znovu. popup.info.shutDownWithDisputeInit=Služba Haveno se vypíná, ale stále je zde čekající zpráva systému sporu.\n\ Před vypnutím prosím počkejte minutu. popup.info.shutDownQuery=Jste si jisti, že chcete opustit Haveno? popup.info.qubesOSSetupInfo=Zdá se, že používáte Haveno na Qubes OS.\n\n\ Ujistěte se, že je vaše Haveno qube nastaveno podle našeho průvodce nastavením na [HYPERLINK:https://haveno.exchange/wiki/Running_Haveno_on_Qubes]. popup.info.p2pStatusIndicator.red={0}\n\n\ Váš uzel není připojen k síti P2P. V tomto stavu nemůže Haveno fungovat. popup.info.p2pStatusIndicator.yellow={0}\n\n\ Váš uzel nemá žádná příchozí připojení Tor. Haveno bude fungovat v pořádku, ale pokud tento stav přetrvává několik hodin, může to znamenat problémy s připojením. popup.info.p2pStatusIndicator.green={0}\n\n\ Dobrá zpráva, stav vašeho připojení P2P vypadá zdravě! popup.info.firewallSetupInfo=Zdá se, že tento počítač blokuje příchozí připojení Tor. \ K tomu může dojít v prostředích virtuálních počítačů, jako je Qubes/VirtualBox/Whonix. \n\n\ Nastavte prosím své prostředí tak, aby přijímalo příchozí připojení Tor, jinak vaše nabídky nebude moci nikdo přijmout. popup.warn.downGradePrevention=Downgrade z verze {0} na verzi {1} není podporován. Použijte prosím nejnovější verzi Haveno. popup.warn.daoRequiresRestart=Vyskytl se problém se synchronizací stavu DAO. Pro odstranění problému je nutné restartovat aplikaci. popup.privateNotification.headline=Důležité soukromé oznámení! popup.xmrLocalNode.msg=Haveno zjistil, že na tomto stroji (na localhostu) běží Monero uzel.\n\n\Prosím ujistěte se, že tento uzel je plně synchronizován před spuštěním Havena. popup.shutDownInProgress.headline=Probíhá vypínání popup.shutDownInProgress.msg=Vypnutí aplikace může trvat několik sekund.\nProsím, nepřerušujte tento proces. popup.attention.forTradeWithId=Je třeba věnovat pozornost obchodu ID {0} popup.attention.welcome.stagenet=Vítejte v testovací instanci Haveno!\n\n\ Tato platforma umožňuje testovat protokol Haveno. Ujistěte se, že postupujete podle pokynů [HYPERLINK:https://github.com/haveno-dex/haveno/blob/master/docs/installing.md].\n\n\ Pokud narazíte na nějaký problém, dejte nám prosím vědět [HYPERLINK:https://github.com/haveno-dex/haveno/issues/new].\n\n\ Jedná se o testovací instanci. Nepoužívejte skutečné peníze! popup.attention.welcome.mainnet=Vítejte v Haveno!\n\n\ Tato platforma umožňuje decentralizovaně obchodovat s měnou Monero za fiat měny nebo jiné kryptoměny.\n\n\ Začněte tím, že si vytvoříte nový platební účet a poté vytvoříte nebo přijmete nabídku.\n\n\ Pokud narazíte na nějaký problém, dejte nám prosím vědět [HYPERLINK:https://github.com/haveno-dex/haveno/issues/new]. popup.attention.welcome.mainnet.test=Vítejte v Haveno!\n\n\ Tato platforma umožňuje decentralizovaně obchodovat s měnou Monero za fiat měny nebo jiné kryptoměny.\n\n\ Začněte tím, že si vytvoříte nový platební účet a poté vytvoříte nebo přijmete nabídku.\n\n\ Pokud narazíte na nějaký problém, dejte nám prosím vědět [HYPERLINK:https://github.com/haveno-dex/haveno/issues/new].\n\n\ Systém Haveno byl nedávno uvolněn k veřejnému testování. Používejte prosím nízké částky! popup.info.multiplePaymentAccounts.headline=K dispozici jsou účty a více platebními metodami popup.info.multiplePaymentAccounts.msg=Pro tuto nabídku máte k dispozici více platebních účtů. Ujistěte se, že jste vybrali ten správný. popup.accountSigning.selectAccounts.headline=Vyberte platební účty popup.accountSigning.selectAccounts.description=Na základě metody platby a časového limitu budou pro podpis vybrány všechny platební účty, které jsou spojeny se sporem, ve kterém došlo k výplatě kupujícímu. popup.accountSigning.selectAccounts.signAll=Podepište všechny platební metody popup.accountSigning.selectAccounts.datePicker=Vyberte čas, do kterého budou účty podepsány popup.accountSigning.confirmSelectedAccounts.headline=Potvrďte vybrané platební účty popup.accountSigning.confirmSelectedAccounts.description=Na základě vašeho zadání bude vybráno {0} platebních účtů. popup.accountSigning.confirmSelectedAccounts.button=Potvrďte platební účty popup.accountSigning.signAccounts.headline=Potvrďte podpis platebních účtů popup.accountSigning.signAccounts.description=Na základě vašeho výběru budou podepsány platební účty {0}. popup.accountSigning.signAccounts.button=Podepsat platební účty popup.accountSigning.signAccounts.ECKey=Zadejte soukromý klíč rozhodce popup.accountSigning.signAccounts.ECKey.error=Špatný ECKey rozhodce popup.accountSigning.success.headline=Gratulujeme popup.accountSigning.success.description=Všechny {0} platební účty byly úspěšně podepsány! popup.accountSigning.generalInformation=Podpisový stav všech vašich účtů najdete v sekci účtu.\n\n Další informace naleznete na adrese [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. popup.accountSigning.signedByArbitrator=Jeden z vašich platebních účtů byl ověřen a podepsán rozhodcem. Obchodování s tímto účtem po úspěšném obchodování automaticky podepíše účet vašeho obchodního partnera.\n\n{0} popup.accountSigning.signedByPeer=Jeden z vašich platebních účtů byl ověřen a podepsán obchodním partnerem. Váš počáteční obchodní limit bude zrušen a do {0} dnů budete moci podepsat další účty.\n\n{1} popup.accountSigning.peerLimitLifted=Počáteční limit pro jeden z vašich účtů byl zrušen.\n\n{0} popup.accountSigning.peerSigner=Jeden z vašich účtů je dostatečně zralý, aby podepsal další platební účty, \ a počáteční limit pro jeden z vašich účtů byl zrušen.\n\n{0} popup.accountSigning.singleAccountSelect.headline=Importujte svědka stáří účtu k podepsání popup.accountSigning.confirmSingleAccount.headline=Potvrďte vybrané svědky o stáří účtu popup.accountSigning.confirmSingleAccount.selectedHash=Hash vybraného svědka popup.accountSigning.confirmSingleAccount.button=Podepsat svědka stáří účtu popup.accountSigning.successSingleAccount.description=Svědek {0} byl podepsán popup.accountSigning.successSingleAccount.success.headline=Úspěch popup.accountSigning.unsignedPubKeys.headline=Nepodepsané Pubkeys popup.accountSigning.unsignedPubKeys.sign=Podepsat Pubkeys popup.accountSigning.unsignedPubKeys.signed=Pubkeys byly podepsány popup.accountSigning.unsignedPubKeys.result.signed=Podepsané pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Nepodařilo se podepsat popup.info.buyerAsTakerWithoutDeposit.headline=Žádný vklad není od kupujícího požadován popup.info.buyerAsTakerWithoutDeposit=\ Vaše nabídka nebude vyžadovat bezpečnostní zálohu ani poplatek od kupujícího XMR.\n\n Pro přijetí vaší nabídky musíte sdílet heslo se svým obchodním partnerem mimo Haveno.\n\n\ Heslo je automaticky vygenerováno a zobrazeno v detailech nabídky po jejím vytvoření.\ popup.info.torMigration.msg=Váš uzel Haveno pravděpodobně používá zastaralou adresu Tor v2. \ Přepněte svůj uzel Haveno na adresu Tor v3. \ Nezapomeňte si předem zálohovat datový adresář. #################################################################### # Notifications #################################################################### notification.trade.headline=Oznámení o obchodu s ID {0} notification.ticket.headline=Úkol pro podporu pro obchod s ID {0} notification.trade.completed=Obchod je nyní dokončen a můžete si vybrat své prostředky. notification.trade.accepted=Vaše nabídka byla přijata XMR {0}. notification.trade.unlocked=Váš obchod má alespoň jedno potvrzení blockchainu.\nPlatbu můžete začít hned teď. notification.trade.finalized=Obchod má {0} potvrzení.\nNyní můžete zahájit platbu. notification.trade.paymentSent=Kupující XMR zahájil platbu. notification.trade.selectTrade=Přejít na obchod notification.trade.peerOpenedDispute=Váš obchodní partner otevřel {0}. notification.trade.disputeClosed={0} byl uzavřen. notification.walletUpdate.headline=Aktualizace obchodní peněženky notification.walletUpdate.msg=Vaše obchodní peněženka má dostatečné finanční prostředky.\nČástka: {0} notification.takeOffer.walletUpdate.msg=Vaše obchodní peněženka již byla dostatečně financována z předchozího pokusu o nabídku.\nČástka: {0} notification.tradeCompleted.headline=Obchod dokončen notification.tradeCompleted.msg=Prostředky můžete nyní vybrat do své externí monerové peněženky nebo je ponechat ve své peněžence Haveno. #################################################################### # System Tray #################################################################### systemTray.show=Otevřít okno aplikace systemTray.hide=Skrýt okno aplikace systemTray.info=Informace o Haveno systemTray.exit=Odejít systemTray.tooltip=Haveno: Decentralizovaná směnárna monero #################################################################### # GUI Util #################################################################### guiUtil.accountExport.savedToPath=Obchodní účty uložené na:\n{0} guiUtil.accountExport.noAccountSetup=Nemáte nastaveny obchodní účty pro export. guiUtil.accountExport.selectPath=Vyberte cestu k {0} # suppress inspection "TrailingSpacesInProperty" guiUtil.accountExport.tradingAccount=Obchodní účet s ID {0}\n # suppress inspection "TrailingSpacesInProperty" guiUtil.accountImport.noImport=Neobchodovali jsme obchodní účet s ID {0}, protože již existuje.\n guiUtil.accountExport.exportFailed=Export do CSV selhal kvůli chybě.\nChyba = {0} guiUtil.accountExport.selectExportPath=Vyberte složku pro export guiUtil.accountImport.imported=Obchodní účet importovaný z:\n{0}\n\nImportované účty:\n{1} guiUtil.accountImport.noAccountsFound=Nebyly nalezeny žádné exportované obchodní účty na: {0}.\nNázev souboru je {1}." guiUtil.openWebBrowser.warning=Chystáte se otevřít webovou stránku \ ve webovém prohlížeči.\n\ Chcete nyní otevřít webovou stránku?\n\n\ Pokud nepoužíváte \"Tor Browser\" jako výchozí systémový webový prohlížeč, \ připojíte se k webové stránce přes nechráněné spojení.\n\n\ URL: \"{0}\" guiUtil.openWebBrowser.doOpen=Otevřít webovou stránku a znovu se neptat guiUtil.openWebBrowser.copyUrl=Zkopírovat URL a zrušit guiUtil.ofTradeAmount=obchodní částky guiUtil.requiredMinimum=(požadované minimum) #################################################################### # Component specific #################################################################### list.currency.select=Vyberte měnu list.currency.showAll=Zobrazit vše list.currency.editList=Upravit seznam měn table.placeholder.noItems=Momentálně nejsou k dispozici žádné {0} table.placeholder.noData=V současné době nejsou k dispozici žádné údaje table.placeholder.processingData=Zpracovávají se data... peerInfoIcon.tooltip.tradePeer=Obchodního partnera peerInfoIcon.tooltip.maker=Tvůrčí peerInfoIcon.tooltip.trade.traded={0} onion adresa: {1}\nUž jste s tímto partnerem obchodovali {2} x\n{3} peerInfoIcon.tooltip.trade.notTraded={0} onion adresa: {1}\nDosud jste s tímto partnerem neobchodovali.\n{2} peerInfoIcon.tooltip.age=Platební účet byl vytvořen před {0}. peerInfoIcon.tooltip.unknownAge=Stáří platebního účtu není znám. peerInfoIcon.tooltip.dispute={0}nPočet sporů: {1}.\n{2} tooltip.openPopupForDetails=Otevřít vyskakovací okno s podrobnostmi tooltip.invalidTradeState.warning=Tento obchod je v neplatném stavu. Chcete-li získat další informace, otevřete okno s podrobnostmi tooltip.openBlockchainForAddress=Otevřít externí průzkumník blockchainu pro adresu: {0} tooltip.openBlockchainForTx=Otevřít externí průzkumník blockchainu pro transakci: {0} confidence.unknown=Neznámý stav transakce confidence.seen=Viděno {0} partnery / 0 potvrzení confidence.confirmed={0} potvrzení confidence.invalid=Transakce je neplatná peerInfo.title=Info o obchodním partnerovi peerInfo.nrOfTrades=Počet dokončených obchodů peerInfo.notTradedYet=Dosud jste s tímto uživatelem neobchodovali. peerInfo.setTag=Nastavit poznámku pro tohoto uživatele peerInfo.age.noRisk=Stáří platebního účtu peerInfo.age.chargeBackRisk=Čas od podpisu peerInfo.unknownAge=Stáří není známo addressTextField.openWallet=Otevřete výchozí peněženku Monero addressTextField.copyToClipboard=Zkopírujte adresu do schránky addressTextField.addressCopiedToClipboard=Adresa byla zkopírována do schránky addressTextField.openWallet.failed=Otevření výchozí peněženky Monero selhalo. Možná nemáte žádnou nainstalovanou? explorerAddressTextField.copyToClipboard=Kopírovat adresu do schránky explorerAddressTextField.blockExplorerIcon.tooltip=Otevřete průzkumník blockchainu s touto adresou explorerAddressTextField.missingTx.warning.tooltip=Chybí požadovaná adresa peerInfoIcon.tooltip={0}\nŠtítek: {1} txIdTextField.copyIcon.tooltip=Zkopírujte ID transakce do schránky txIdTextField.blockExplorerIcon.tooltip=Otevřete průzkumník blockchainu s tímto ID transakce txIdTextField.missingTx.warning.tooltip=Chybí požadovaná transakce #################################################################### # Navigation #################################################################### navigation.account=\"Účet\" navigation.account.walletSeed=\"Účet/Seed peněženky\" navigation.funds.availableForWithdrawal=\"Prostředky/Poslat finanční prostředky\" navigation.portfolio.myOpenOffers=\"Portfolio/Moje otevřené nabídky\" navigation.portfolio.pending=\"Portfolio/Otevřené obchody\" navigation.portfolio.closedTrades=\"Portfolio/Historie\" navigation.funds.depositFunds=\"Prostředky/Přijmout finanční prostředky\" navigation.settings.preferences=\"Nastavení/Preference\" # suppress inspection "UnusedProperty" navigation.funds.transactions=\"Prostředky/Transakce\" navigation.support=\"Podpora\" #################################################################### # Formatter #################################################################### formatter.formatVolumeLabel={0} částka{1} formatter.makerTaker=Tvůrce jako {0} {1} / Příjemce jako {2} {3} formatter.makerTaker.locked=Tvůrce jako {0} {1} / Příjemce jako {2} {3} 🔒 formatter.youAreAsMaker=Jste {1} {0} (jako tvůrce) / Příjemce je {3} {2} formatter.youAreAsTaker=Jste {1} {0} (jako příjemce) / Tvůrce je {3} {2} formatter.youAre={0}te {1} ({2}te {3}) formatter.youAreCreatingAnOffer.traditional=Vytváříte nabídku: {0} {1} formatter.youAreCreatingAnOffer.traditional.locked=Vytváříte nabídku: {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Vytváříte nabídku: {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.crypto.locked=Vytváříte nabídku: {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} jako tvůrce formatter.asTaker={0} {1} jako příjemce #################################################################### # Domain specific #################################################################### # we use enum values here # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" XMR_MAINNET=Monero Mainnet # suppress inspection "UnusedProperty" XMR_LOCAL=Monero Local Testnet # suppress inspection "UnusedProperty" XMR_STAGENET=Monero Stagenet time.year=Rok time.month=Měsíc time.halfYear=Půlrok time.quarter=Čtvrtrok time.week=Týden time.day=Den time.hour=Hodina time.minute10=10 minut time.hours=hodin time.days=dnů time.1hour=1 hodina time.1day=1 den time.minute=minuta time.second=sekunda time.minutes=minut time.seconds=sekund password.enterPassword=Vložte heslo password.confirmPassword=Potvrďte heslo password.tooLong=Heslo musí mít méně než 500 znaků. password.deriveKey=Odvozuji klíč z hesla password.walletDecrypted=Peněženka úspěšně dešifrována a ochrana heslem byla odstraněna. password.wrongPw=Zadali jste nesprávné heslo.\n\nZkuste prosím zadat heslo znovu a pečlivě zkontrolujte překlepy nebo pravopisné chyby. password.walletEncrypted=Peněženka úspěšně šifrována a ochrana heslem povolena. password.walletEncryptionFailed=Heslo se nepodařilo nastavit password.passwordsDoNotMatch=Zadaná 2 hesla se neshodují. password.forgotPassword=Zapomněli jste heslo? password.backupReminder=Pamatujte, že při nastavování hesla do peněženky budou smazány všechny automaticky vytvořené zálohy z nezašifrované peněženky.\n\n\ Před nastavením hesla se důrazně doporučuje provést zálohu adresáře aplikace a zapsat si seed slova! password.setPassword=Nastavit heslo (Už jsem provedl/a zálohu) password.makeBackup=Provést zálohu seed.seedWords=Seed slova peněženky seed.enterSeedWords=Vložte seed slova peněženky seed.date=Datum peněženky seed.restore.title=Obnovit peněženky z seed slov seed.restore=Obnovit peněženky seed.creationDate=Datum vzniku seed.warn.walletNotEmpty.msg=Vaše peněženka Monero není prázdná.\n\n\ Tuto peněženku musíte vyprázdnit, než se pokusíte obnovit starší, protože smíchání peněženek \ může vést ke zneplatnění záloh.\n\n\ Dokončete své obchody, uzavřete všechny otevřené nabídky a přejděte do sekce Prostředky, kde si můžete vybrat své monero.\n\ V případě, že nemáte přístup ke svým moneroům, můžete použít nouzový nástroj k vyprázdnění peněženky.\n\ Nouzový nástroj otevřete stisknutím kombinace kláves \"Alt+e\" nebo \"Cmd/Ctrl+e\". seed.warn.walletNotEmpty.restore=Chci přesto obnovit seed.warn.walletNotEmpty.emptyWallet=Nejprve vyprázdním své peněženky seed.warn.notEncryptedAnymore=Vaše peněženky jsou šifrovány.\n\n\ Po obnovení již nebudou peněženky šifrovány a musíte nastavit nové heslo.\n\n\ Chcete pokračovat? seed.warn.walletDateEmpty=Protože jste nezadali datum peněženky, bude muset haveno skenovat blockchain od roku 2013.10.09 (datum spuštění BIP39).\n\n\ Peněženky BIP39 byly poprvé představeny v Haveno dne 2017.06.28 (verze v0.5). Použitím tohoto data můžete ušetřit čas.\n\n\ V ideálním případě byste měli určit datum, kdy byl vytvořen váš seed peněženky.\n\n\n\ Opravdu chcete pokračovat bez zadání data peněženky? seed.restore.success=Peněženky byly úspěšně obnoveny pomocí nových seed slov.\n\nMusíte vypnout a restartovat aplikaci. seed.restore.error=Při obnově peněženek pomocí seed slov došlo k chybě. {0} seed.restore.openOffers.warn=Máte otevřené nabídky, které budou odstraněny, pokud obnovíte ze seedu.\n\ Jste si jisti, že chcete pokračovat? #################################################################### # Payment methods #################################################################### payment.account=Účet payment.account.no=Číslo účtu payment.account.name=Název účtu payment.account.username=Uživatelské jméno payment.account.phoneNr=Telefonní číslo payment.account.owner.fullname=Celé jméno vlastníka účtu payment.account.fullName=Celé jméno (křestní, střední, příjmení) payment.account.state=Stát/Provincie/Region payment.account.city=Město payment.account.address=Adresa payment.bank.country=Země původu banky payment.account.name.email=Celé jméno majitele účtu / e-mail payment.account.name.emailAndHolderId=Celé jméno majitele účtu / e-mail / {0} payment.bank.name=Jméno banky payment.select.account=Vyberte typ účtu payment.select.region=Vyberte region payment.select.country=Vyberte zemi payment.select.bank.country=Vyberte zemi původu banky payment.foreign.currency=Opravdu chcete vybrat jinou měnu, než je výchozí měna země? payment.restore.default=Ne, obnovit výchozí měnu payment.email=E-mail payment.country=Země payment.extras=Zvláštní požadavky payment.email.mobile=E-mail nebo mobilní číslo payment.email.mobile.cashtag=Cashtag, e-mail, nebo mobilní číslo. payment.email.mobile.username=Uživatelské jméno, e-mail, nebo mobilní číslo. payment.crypto.address=Adresa kryptoměny payment.crypto.tradeInstantCheckbox=Obchodujte okamžitě (do 1 hodiny) s touto kryptoměnou payment.crypto.tradeInstant.popup=Pro okamžité obchodování je nutné, aby oba obchodní partneři byli online, aby mohli \ obchod dokončit za méně než 1 hodinu.\n\n\ Pokud máte otevřené nabídky a nejste k dispozici, \ deaktivujte je na obrazovce 'Portfolio'. payment.crypto=Kryptoměna payment.select.crypto=Vyberte nebo vyhledejte kryptoměnu payment.secret=Tajná otázka payment.answer=Odpověď payment.wallet=ID peněženky payment.capitual.cap=CAP kód payment.upi.virtualPaymentAddress=Virtuální platební adresa # suppress inspection "UnusedProperty" payment.swift.headline=Mezinárodní SWIFT Wire Transfer # suppress inspection "UnusedProperty" payment.swift.title.bank=Přijímající banka # suppress inspection "UnusedProperty" payment.swift.title.intermediary=Zprostředkující banka (klikněte pro rozbalení) # suppress inspection "UnusedProperty" payment.swift.country.bank=Země přijímající banky # suppress inspection "UnusedProperty" payment.swift.country.intermediary=Země zprostředkující banky # suppress inspection "UnusedProperty" payment.swift.swiftCode.bank=SWIFT kód přijímající banky # suppress inspection "UnusedProperty" payment.swift.swiftCode.intermediary=SWIFT kód zprostředkující banky # suppress inspection "UnusedProperty" payment.swift.name.bank=Název přijímající banky # suppress inspection "UnusedProperty" payment.swift.name.intermediary=Název zprostředkující banky # suppress inspection "UnusedProperty" payment.swift.branch.bank=Pobočka přijímající banky # suppress inspection "UnusedProperty" payment.swift.branch.intermediary=Pobočka zprostředkující banky # suppress inspection "UnusedProperty" payment.swift.address.bank=Adresa přijímající banky # suppress inspection "UnusedProperty" payment.swift.address.intermediary=Adresa zprostředkující banky # suppress inspection "UnusedProperty" payment.swift.address.beneficiary=Adresa příjemce # suppress inspection "UnusedProperty" payment.swift.phone.beneficiary=Telefon příjemce prostředků payment.swift.account=Číslo účtu (nebo IBAN) payment.swift.use.intermediary=Použít zprostředkující banku payment.swift.showPaymentInfo=Ukázat platební informace... payment.account.owner.address=Adresa majitele účtu payment.transferwiseUsd.address=(musí být v US, zvažte použití adresy banky) payment.amazon.site=Kupte Amazon eGift zde: payment.ask=Zjistěte pomocí obchodního chatu payment.uphold.accountId=Uživatelské jméno, e-mail nebo číslo telefonu payment.moneyBeam.accountId=E-mail nebo číslo telefonu payment.popmoney.accountId=E-mail nebo číslo telefonu payment.promptPay.promptPayId=Občanské/daňové identifikační číslo nebo telefonní číslo payment.supportedCurrencies=Podporované měny payment.supportedCurrenciesForReceiver=Měny pro příjem prostředků payment.limitations=Omezení payment.salt=Salt pro ověření stáří účtu payment.error.noHexSalt=Salt musí být ve formátu HEX.\n\ Doporučujeme upravit pole salt, pokud chcete salt převést ze starého účtu, aby bylo stáří vašeho účtu zachováno. \ Stáří účtu se ověřuje pomocí salt účtu a identifikačních údajů účtu (např. IBAN). payment.accept.euro=Přijímejte obchody z těchto zemí eurozóny payment.accept.nonEuro=Přijímejte obchody z těchto zemí mimo eurozónu payment.accepted.countries=Akceptované země payment.accepted.banks=Akceptované banky (ID) payment.mobile=Číslo mobilu payment.postal.address=Poštovní adresa payment.national.account.id.AR=Číslo CBU shared.accountSigningState=Stav podpisu účtu #new payment.crypto.address.dyn={0} adresa payment.crypto.receiver.address=Kryptoměnová adresa příjemce payment.accountNr=Číslo účtu payment.emailOrMobile=E-mail nebo mobilní číslo payment.useCustomAccountName=Použijte vlastní název účtu payment.maxPeriod=Max. povolené obchodní období payment.maxPeriodAndLimit=Max. doba trvání obchodu: {0} / Max. nákup: {1} / Max. prodej: {2} / Stáří účtu: {3} payment.maxPeriodAndLimitCrypto=Max. doba trvání obchodu: {0} / Max. obchodní limit: {1} payment.currencyWithSymbol=Měna: {0} payment.nameOfAcceptedBank=Název akceptované banky payment.addAcceptedBank=Přidat akceptovanou banku payment.clearAcceptedBanks=Vymazat akceptované banky payment.bank.nameOptional=Název banky (volitelné) payment.bankCode=Kód banky payment.bankId=ID Banky (BIC/SWIFT) payment.bankIdOptional=ID Banky (BIC/SWIFT) (volitelné) payment.branchNr=Číslo pobočky payment.branchNrOptional=Číslo pobočky (volitelné) payment.accountNrLabel=Číslo účtu (IBAN) payment.iban=IBAN payment.tikkie.iban=IBAN použitý pro obchodování Haveno na Tikkie payment.accountType=Typ účtu payment.checking=Kontrola payment.savings=Úspory payment.personalId=Číslo občanského průkazu payment.zelle.info=Zelle je služba převodu peněz, která funguje nejlépe *prostřednictvím* jiné banky.\n\n\ 1. Na této stránce zjistěte, zda (a jak) vaše banka spolupracuje se Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n\ 2. Zaznamenejte si zvláštní limity převodů - limity odesílání se liší podle banky a banky často určují samostatné denní, týdenní a měsíční limity.\n\n\ 3. Pokud vaše banka s Zelle nepracuje, můžete ji stále používat prostřednictvím mobilní aplikace Zelle, ale vaše limity převodu budou mnohem nižší.\n\n\ 4. Název uvedený na vašem účtu Haveno MUSÍ odpovídat názvu vašeho účtu Zelle/bankovního účtu.\n\n\ Pokud nemůžete dokončit transakci Zelle, jak je uvedeno ve vaší obchodní smlouvě, můžete ztratit část (nebo vše) ze svého bezpečnostního vkladu.\n\n\ Vzhledem k poněkud vyššímu riziku zpětného zúčtování společnosti Zelle se prodejcům doporučuje kontaktovat nepodepsané kupující \ prostřednictvím e-mailu nebo SMS, aby ověřili, že kupující skutečně vlastní účet Zelle uvedený v Haveno. payment.fasterPayments.newRequirements.info=Některé banky začaly ověřovat celé jméno příjemce pro převody \ Faster Payments. Váš současný účet Faster Payments nepožadoval celé jméno.\n\n\ Zvažte prosím znovu vytvoření svého Faster Payments účtu v Havenou, abyste mohli budoucím kupujícím {0} poskytnout celé jméno.\n\n\ Při opětovném vytvoření účtu nezapomeňte zkopírovat přesný kód řazení, číslo účtu a hodnoty soli (salt) pro ověření \ věku ze starého účtu do nového účtu. Tím zajistíte zachování stáří \ a stavu vašeho stávajícího účtu. payment.fasterPayments.ukSortCode=UK sort kód payment.moneyGram.info=Při používání MoneyGram musí XMR kupující zaslat autorizační číslo a fotografii potvrzení e-mailem prodejci XMR. \ Potvrzení musí jasně uvádět celé jméno prodejce, zemi, stát a částku. E-mail prodávajícího se kupujícímu zobrazí během procesu obchodování. payment.westernUnion.info=Při používání služby Western Union musí kupující XMR zaslat prodejci XMR e-mailem MTCN (sledovací číslo) a fotografii potvrzení. \ Potvrzení musí jasně uvádět celé jméno prodejce, město, zemi a částku. E-mail prodávajícího se kupujícímu zobrazí během procesu obchodování. payment.halCash.info=Při používání HalCash musí kupující XMR poslat prodejci XMR kód HalCash prostřednictvím textové zprávy z mobilního telefonu.\n\n\ Ujistěte se, že nepřekračujete maximální částku, kterou vám banka umožňuje odesílat pomocí HalCash. \ Min. částka za výběr je 10 EUR a max. částka je 600 EUR. Pro opakované výběry je to 3000 EUR za příjemce za den \ a 6000 EUR za příjemce za měsíc. Zkontrolujte prosím tyto limity u své banky, abyste se ujistili, \ že používají stejné limity, jaké jsou zde uvedeny.\n\n\ Částka pro výběr musí být násobkem 10 EUR, protože z bankomatu nemůžete vybrat jiné částky. \ Uživatelské rozhraní na obrazovce vytvořit-nabídku and přijmout-nabídku upraví částku XMR tak, aby částka EUR byla správná. \ Nemůžete použít tržní cenu, protože částka v EURECH se mění s měnícími se cenami.\n\n\ V případě sporu musí kupující XMR poskytnout důkaz, že zaslal EURA. # suppress inspection "UnusedMessageFormatParameter" payment.limits.info=Uvědomte si, že u všech bankovních převodů existuje určité riziko zpětného zúčtování. Aby se toto riziko zmírnilo, \ stanoví Haveno limity pro jednotlivé obchody na základě odhadované úrovně rizika zpětného zúčtování pro použitou platební metodu.\n\ \n\ U této platební metody je váš limit pro jednotlivé obchody pro nákup a prodej {2}.\n\ \n\ Toto omezení se vztahuje pouze na velikost jednoho obchodu - můžete zadat tolik obchodů, kolik chcete.\n\ \n\ Další podrobnosti najdete na wiki [HYPERLINK:https://docs.haveno.exchange/overview/account_limits]. # suppress inspection "UnusedProperty" payment.limits.info.withSigning=Aby se omezilo riziko zpětného zúčtování, Haveno stanoví limity pro jednotlivé obchody \ pro tento typ platebního účtu na základě následujících 2 faktorů:\n\n\ 1. Obecné riziko zpětného zúčtování pro platební metodu\n\ 2. Stav podepisování účtu\n\ \n\ Tento platební účet ještě není podepsán, takže je omezen na nákup {0} za obchod. \ Po podpisu se limity nákupu zvýší následovně:\n\ \n\ ● Před podpisem a 30 dní po podpisu bude váš limit nákupu podle obchodu {0}\n\ ● 30 dní po podpisu bude váš limit nákupu podle obchodu {1}\n\ ● 60 dní po podpisu bude váš limit nákupu podle obchodu {2}\n\ \n\ Podpisy účtu neovlivňují prodejní limity. Můžete okamžitě prodat {2} v jednom obchodu.\n\ \n\ Tato omezení platí pouze pro objem jednoho obchodu - můžete zadat tolik obchodů, kolik chcete.\n\ \n\ Další podrobnosti najdete na wiki [HYPERLINK:https://docs.haveno.exchange/overview/account_limits]. payment.cashDeposit.info=Potvrďte, že vám vaše banka umožňuje odesílat hotovostní vklady na účty jiných lidí. \ Například Bank of America a Wells Fargo již takové vklady nepovolují. payment.revolut.info=Revolut vyžaduje 'uživatelské jméno' jako ID účtu, nikoli telefonní číslo nebo e-mail, jako tomu bylo v minulosti. payment.account.revolut.addUserNameInfo={0}\n\ Váš stávající účet Revolut ({1}) nemá "Uživatelské jméno".\n\ Chcete-li aktualizovat údaje o svém účtu, zadejte své "Uživatelské jméno" Revolut.\n\ To neovlivní stav podepisování stáří vašeho účtu. payment.revolut.addUserNameInfo.headLine=Aktualizujte účet Revolut payment.cashapp.info=Uvědomte si, že u aplikace Cash App je riziko zpětné úhrady (chargeback) vyšší než u většiny bankovních převodů. payment.venmo.info=Mějte na paměti, že u služby Venmo je riziko zpětné úhrady (chargeback) vyšší než u většiny bankovních převodů. payment.paypal.info=Uvědomte si prosím, že u služby PayPal je riziko zpětné úhrady (chargeback) vyšší než u většiny bankovních převodů. payment.amazonGiftCard.upgrade=Platba kartou Amazon eGift nyní vyžaduje také nastavení země. payment.account.amazonGiftCard.addCountryInfo={0}\n\ Váš stávající účet pro platbu kartou Amazon eGift ({1}) nemá nastavenou zemi.\n\ Vyberte prosím zemi, ve které je možné vaše karty Amazon eGift uplatnit.\n\ Tato aktualizace vašeho účtu nebude mít vliv na stáří tohoto účtu. payment.amazonGiftCard.upgrade.headLine=Aktualizace účtu pro platbu kartou Amazon eGift payment.swift.info.account=Pečlivě si prostudujte základní pokyny pro používání SWIFT v Haveno:\n\ \n\ - vyplňte všechna pole kompletně a přesně \n\ - kupující musí odeslat platbu v měně stanovené tvůrcem nabídky \n\ - kupující použije u platby model sdílených poplatků (SHA) \n\ - kupující a prodejce mohou platit poplatky, proto by se měli nejprve seznámit s poplatky své banky \n\ \n\ SWIFT je složitější než jiné platební metody, proto si prosím přečtěte kompletní pokyny na wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/SWIFT]. payment.swift.info.buyer=Pro nákup monero pomocí SWIFT, musíte:\n\ \n\ - odeslat platbu v měně, stanovené tvůrcem nabídky \n\ - použít u platby model sdílených poplatků (SHA) \n\ \n\ Přečtěte si další pokyny na wiki, abyste se vyhnuli sankcím a zajistili hladký průběh obchodů [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/SWIFT]. payment.swift.info.seller=Odesílatelé SWIFT musí při odesílání plateb používat model sdílených poplatků (SHA).\n\ \n\ Pokud obdržíte platbu SWIFT, která nepoužívá SHA, otevřete mediační požadavek.\n\ \n\ Přečtěte si další pokyny na wiki, abyste se vyhnuli sankcím a zajistili hladký průběh obchodů [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/SWIFT]. payment.imps.info.account=Prosím, nezapomeňte uvést své:\n\n\ ● Celé jméno majitele účtu\n\ ● Číslo účtu\n\ ● IFSC číslo\n\n\ Tyto údaje by měly odpovídat vašemu bankovnímu účtu, který budete používat pro odesílání / přijímání plateb.\n\n\ Upozorňujeme, že na jednu transakci lze poslat maximálně 200 000 rupií. Pokud obchodujete s částkou vyšší než tato, bude třeba provést více transakcí. Uvědomte si však, že jejich maximální limit je 1 000 000 rupií, které lze odeslat za den.\n\n\ Některé banky mají pro své zákazníky jiné limity. payment.imps.info.buyer=Platbu prosím zasílejte pouze na účet uvedený v systému Haveno.\n\n\ Maximální velikost obchodu je 200 000 rupií na transakci.\n\n\ Pokud váš obchod přesahuje 200 000 rupií, budete muset provést více převodů. Mějte však na paměti, že je stanoven maximální limit 1 000 000 rupií, který lze odeslat za den.\n\n\ Vezměte prosím na vědomí, že některé banky mají pro své klienty jiné limity. payment.imps.info.seller=Pokud máte v úmyslu přijmout více než 200 000 rupií za obchod, měli byste počítat s tím, že kupující bude muset provést více převodů. Mějte však na paměti, že existuje maximální limit 1 000 000 rupií, které lze odeslat za den.\n\n\ Vezměte prosím na vědomí, že některé banky mají pro své klienty jiné limity. payment.neft.info.account=Prosím, nezapomeňte uvést své:\n\n\ ● Celé jméno majitele účtu\n\ ● Číslo účtu\n\ ● IFSC číslo\n\n\ Tyto údaje by měly odpovídat vašemu bankovnímu účtu, který budete používat pro odesílání/přijímání plateb.\n\n\ Upozorňujeme, že na jednu transakci lze poslat maximálně 50 000 rupií. Pokud obchodujete s částkou vyšší než tato, bude třeba provést více transakcí.\n\n\ Vezměte prosím na vědomí, že některé banky mají pro své klienty jiné limity. payment.neft.info.buyer=Platbu prosím zasílejte pouze na účet uvedený v systému Haveno.\n\n\ Maximální velikost obchodu je 50 000 rupií na transakci.\n\n\ Pokud váš obchod přesáhne 50 000 rupií, budete muset provést více převodů.\n\n\ Vezměte prosím na vědomí, že některé banky mají pro své klienty různé limity. payment.neft.info.seller=Pokud máte v úmyslu získat více než 50 000 rupií za obchod, měli byste počítat s tím, že kupující bude muset provést více převodů.\n\n\ Vezměte prosím na vědomí, že některé banky mají pro své klienty jiné limity. payment.paytm.info.account=Nezapomeňte uvést e-mail nebo telefonní číslo, které se shoduje s vaším e-mailem nebo telefonním číslem ve vašem účtu PayTM. \n\n\ Když si uživatelé založí účet PayTM bez KYC, jsou omezeni: \n\n\ ● Na jednu transakci lze poslat maximálně 5 000 rupií.\n\ ● V peněžence PayTM můžete mít maximálně 10 000 rupií.\n\n\ Pokud máte v úmyslu obchodovat s částkou vyšší než 5 000 za obchod, budete muset u společnosti PayTM vyplnit KYC. S KYC jsou uživatelé omezeni na:\n\n\ ● Na jednu transakci lze poslat maximálně 100 000 rupií.\n\ ● V peněžence PayTM můžete mít maximálně 100 000 rupií.\n\n\ Uživatelé by také měli znát limity účtu. Obchody překračující limity účtu PayTM se pravděpodobně budou muset uskutečnit v průběhu více než jednoho dne, nebo budou zrušeny. payment.paytm.info.buyer=Platbu zasílejte pouze na uvedenou e-mailovou adresu nebo telefonní číslo.\n\n\ Pokud máte v úmyslu obchodovat s částkou vyšší než 5 000 rupií za obchod, budete muset u společnosti PayTM vyplnit KYC.\n\n\ Bez KYC lze na jednu transakci poslat 5 000 rupií.\n\n\ S uživateli KYC lze na jednu transakci poslat 100 000 rupií. payment.paytm.info.seller=Pokud máte v úmyslu obchodovat s částkou vyšší než 5 000 rupií za obchod, budete muset u společnosti PayTM vyplnit KYC. S KYC jsou uživatelé omezeni na:\n\n\ ● Na jednu transakci lze poslat maximálně 100 000 rupií.\n\ ● V peněžence PayTM můžete mít maximálně 100 000 rupií.\n\n\ Uživatelé by také měli znát limity účtu. Protože v peněžence PayTM můžete mít maximálně 100 000 rupií, dbejte na to, abyste své rupie pravidelně převáděli. payment.rtgs.info.account=RTGS je určen pro platby velkých obchodů ve výši 200 000 rupií a více.\n\n\ Při nastavování platebního účtu RTGS nezapomeňte uvést své:\n\n\ ● Celé jméno majitele účtu\n\ ● Číslo účtu\n\ ● IFSC číslo\n\n\\ Tyto údaje by měly odpovídat vašemu bankovnímu účtu, který budete používat pro odesílání/přijímání plateb.\n\n\ Vezměte prosím na vědomí, že minimální částka obchodu, kterou lze poslat na jednu transakci, je 200 000 rupií. Pokud obchodujete pod touto částkou, buď bude obchod zrušen, nebo se oba obchodníci budou muset dohodnout na jiné metodě platby (např. IMPS nebo UPI). payment.rtgs.info.buyer=Platbu prosím zasílejte pouze na účet uvedený v systému Haveno.\n\n\ Vezměte prosím na vědomí, že minimální částka obchodu, kterou lze poslat na jednu transakci, je 200 000 rupií. Pokud obchodujete pod touto částkou, buď bude obchod zrušen, nebo se oba obchodníci budou muset dohodnout na jiné metodě platby (např. IMPS nebo UPI). payment.rtgs.info.seller=Vezměte prosím na vědomí, že minimální částka obchodu, kterou lze poslat na jednu transakci, je 200 000 rupií. Pokud obchodujete pod touto částkou, buď bude obchod zrušen, nebo se oba obchodníci budou muset dohodnout na jiné metodě platby (např. IMPS nebo UPI). payment.upi.info.account=Nezapomeňte uvést svou virtuální platební adresu (VPA), která se také nazývá UPI ID. Formát tohoto údaje je podobný e-mailovému ID: se znakem ”@” uprostřed. Vaše UPI ID může být například ”jméno_příjemce@název_banky” nebo ”telefonní_číslo@název_banky”. \n\n\ Pro UPI je stanoven maximální limit 100 000 rupií, které lze poslat na jednu transakci. \n\n\ Pokud máte v úmyslu obchodovat s částkou vyšší než 100 000 rupií na jeden obchod, je pravděpodobné, že obchody budou muset proběhnout více převody. \n\n\ Vezměte prosím na vědomí, že některé banky mají pro své klienty různé limity. payment.upi.info.buyer=Platbu prosím zasílejte pouze na VPA / UPI ID uvedené v systému Haveno. \n\n\ Maximální velikost obchodu je 100 000 rupií na transakci. \n\n\ Pokud váš obchod přesahuje 100 000 rupií, budete muset provést více převodů. \n\n\ Vezměte prosím na vědomí, že některé banky mají pro své klienty jiné limity. payment.upi.info.seller=Pokud máte v úmyslu přijmout více než 100 000 rupií za obchod, měli byste počítat s tím, že kupující bude muset provést více převodů. \n\n\ Vezměte prosím na vědomí, že některé banky mají pro své klienty jiné limity. payment.celpay.info.account=Nezapomeňte uvést e-mail, na který je váš účet Celsius registrován. \ Tím zajistíte, že se při odesílání peněz zobrazí ze správného účtu a při jejich přijímání budou připsány na váš účet.\n\n\ Uživatelé CelPay mohou poslat maximálně 2500 dolarů (nebo ekvivalent v jiné měně/krypto) za 24 hodin.\n\n\ Obchody překračující limity účtu CelPay se pravděpodobně budou muset uskutečnit během více než jednoho dne, nebo budou zrušeny.\n\n\ CelPay podporuje více stablecoinů:\n\n\ ● Stablecoiny USD: DAI, TrueUSD, USDC, ZUSD, BUSD, GUSD, PAX, USDT (ERC20)\n\ ● Stablecoiny CAD; TrueCAD\n\ ● Stablecoiny GBP; TrueGBP\n\ ● Stablecoiny HKD; TrueHKD\n\ ● Stablecoiny AUD; TrueAUD\n\n\ Kupující XMR mohou prodávajícímu XMR poslat libovolnou odpovídající měnu stablecoin. payment.celpay.info.buyer=Platbu prosím zasílejte pouze na e-mailovou adresu, kterou prodejce XMR uvedl zasláním platebního odkazu.\n\n\ CelPay je omezena na odeslání 2 500 dolarů (nebo jiné měny/krypto ekvivalentu) za 24 hodin.\n\n\ Obchody překračující limit účtu CelPay se pravděpodobně budou muset uskutečnit v průběhu více než jednoho dne, nebo budou zrušeny.\n\n\ CelPay podporuje více stablecoinů:\n\n\ ● USD Stablecoiny; DAI, TrueUSD, USDC, ZUSD, BUSD, GUSD, PAX, USDT (ERC20)\n\ ● CAD Stablecoiny; TrueCAD\n\ ● GBP Stablecoiny; TrueGBP\n\ ● HKD Stablecoiny; TrueHKD\n\ ● AUD Stablecoiny; TrueAUD\n\n\ Kupující XMR mohou prodávajícímu XMR poslat libovolnou odpovídající měnu stablecoin. payment.celpay.info.seller=Prodejci XMR by měli očekávat, že obdrží platbu prostřednictvím zabezpečeného platebního odkazu. \ Ujistěte se, že odkaz na e-mailovou platbu obsahuje e-mailovou adresu zadanou kupujícím XMR.\n\n\ Uživatelé CelPay mohou poslat maximálně 2500 dolarů (nebo ekvivalent v jiné měně/krypto) za 24 hodin.\n\n\ Obchody překračující limity účtu CelPay se pravděpodobně budou muset uskutečnit během více než jednoho dne, nebo budou zrušeny.\n\n\ CelPay podporuje více stablecoinů:\n\n\ ● USD Stablecoiny; DAI, TrueUSD, USDC, ZUSD, BUSD, GUSD, PAX, USDT (ERC20)\n\ ● CAD Stablecoiny; TrueCAD\n\ ● GBP Stablecoiny; TrueGBP\n\ ● HKD Stablecoiny; TrueHKD\n\ ● AUD Stablecoiny; TrueAUD\n\n\ Prodávající XMR by měli očekávat, že od kupujícího XMR obdrží odpovídající měnu stablecoin. Je možné, aby kupující XMR zaslal libovolnou odpovídající měnu stablecoin. payment.celpay.supportedCurrenciesForReceiver=Podporované měny (Upozornění: všechny níže uvedené měny jsou v aplikaci Celcius podporovány jako stablecoiny. Obchody se týkají stablecoinů, nikoli fiat.) payment.nequi.info.account=Nezapomeňte uvést své telefonní číslo, které je spojeno s vaším účtem Nequi.\n\n\ Při zakládání účtu Nequi jsou platební limity nastaveny na maximálně ~ 7 000 000 COP, které lze měsíčně odeslat.\n\n\ Pokud máte v úmyslu obchodovat s částkou vyšší než 7 000 000 COP na jeden obchod, budete muset u společnosti Bancolombia provést KYC a zaplatit poplatek \ ve výši přibližně 15 000 COP. Poté budou všechny transakce zatíženy 0,4% daní. Ujistěte se prosím, že znáte aktuální výši daní.\n\n\ Uživatelé by také měli znát limity účtu. Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. payment.nequi.info.buyer=Platbu prosím zasílejte pouze na telefonní číslo uvedené v účtu XMR prodávajícího na Havenu.\n\n\ Když si uživatelé založí účet Nequi, jsou limity plateb nastaveny na maximálně ~ 7 000 000 COP, které lze měsíčně odeslat.\n\n\ Pokud máte v úmyslu obchodovat s částkou vyšší než 7 000 000 COP na jeden obchod, budete muset u společnosti Bancolombia provést KYC a zaplatit poplatek \ ve výši přibližně 15 000 COP. Poté budou všechny transakce zatíženy 0,4% daní. Ujistěte se prosím, že znáte aktuální výši daní.\n\n\ Uživatelé by také měli znát limity účtu. Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. payment.nequi.info.seller=Zkontrolujte, zda se přijatá platba shoduje s telefonním číslem uvedeným v účtu XMR kupujícího Haveno.\n\n\ Když si uživatelé založí účet Nequi, jsou limity plateb nastaveny na maximálně ~ 7 000 000 COP, které lze měsíčně odeslat.\n\n\ Pokud máte v úmyslu obchodovat s částkou vyšší než 7 000 000 COP na jeden obchod, budete muset u společnosti Bancolombia provést KYC a zaplatit poplatek \ ve výši přibližně 15 000 COP. Poté budou všechny transakce zatíženy 0,4% daní. Ujistěte se prosím, že znáte aktuální výši daní.\n\n\ Uživatelé by také měli znát limity účtu. Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. payment.bizum.info.account=K využívání služby Bizum potřebujete bankovní účet (IBAN) ve Španělsku a být zaregistrováni pro tuto službu.\n\n\ Bizum lze použít pro obchody v rozmezí od 0,50 € do 1 000 €.\n\n\ Maximální částka transakcí, které můžete odeslat/přijmout prostřednictvím služby Bizum, je 2 000 eur denně.\n\n\ Uživatelé služby Bizum mohou měsíčně provést maximálně 150 operací.\n\n\ Každá banka však může pro své klienty stanovit vlastní limity v rámci výše uvedených limitů.\n\n\ Uživatelé by také měli znát limity účtu. Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. payment.bizum.info.buyer=Platbu prosím zasílejte pouze na mobilní telefonní číslo prodávajícího XMR uvedené v systému Haveno.\n\n\ Maximální velikost obchodu je 1 000 € na jednu platbu. Maximální objem transakcí, které můžete prostřednictvím Bizumu odeslat, je 2 000 eur za den.\n\n\ Pokud překročíte výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. payment.bizum.info.seller=Ujistěte se prosím, že platba byla přijata z mobilního telefonního čísla kupujícího XMR, které je uvedeno v Haveno.\n\n\ Maximální velikost obchodu je 1 000 EUR na jednu platbu. Maximální objem transakcí, které můžete přijmout pomocí služby Bizum, je 2 000 eur za den.\n\n\ Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. payment.pix.info.account=Nezapomeňte uvést vámi vybraný klíč Pix Key. Existují čtyři typy klíčů: \ CPF (Registr fyzických osob) nebo CNPJ (Národní registr právnických osob), e-mailová adresa, telefonní číslo nebo \ náhodný klíč vygenerovaný systémem, tzv. univerzální jedinečný identifikátor (UUID). Jiný klíč musí být použit pro \ každý účet Pix, který máte. Jednotlivci si mohou vytvořit až pět klíčů pro každý účet, který vlastní.\n\n\ Při obchodování v Haveno by kupující XMR měli používat své klíče Pix v popise platby, aby prodávající XMR mohli snadno identifikovat, že platba pochází od nich. payment.pix.info.buyer=Platbu prosím zasílejte pouze na Pix Key uvedený v účtu XMR prodávajícího na Haveno.\n\n\ Jako referenční číslo platby použijte svůj Pix Key, aby prodejce XMR mohl snadno identifikovat, že platba pochází od vás. payment.pix.info.seller=Zkontrolujte, zda se popis přijaté platby shoduje s klíčem Pix uvedeným v účtu XMR kupujícího na Havenu. payment.pix.key=Pix Key (CPF, CNPJ, e-mail, telefonní číslo nebo UUID) payment.monese.info.account=Monese je bankovní aplikace pro uživatele GBP, EUR a RON*. Monese umožňuje uživatelům posílat peníze do \ jiných účtů Monese a to okamžitě a zdarma v jakékoli podporované měně.\n\n\ *Chcete-li si v Monese otevřít účet v RON, musíte buď žít v Rumunsku, nebo mít rumunské občanství.\n\n\ Při zakládání účtu Monese v Havenu nezapomeňte uvést své jméno a telefonní číslo, které se shoduje s vaším \ Monese. Tím zajistíte, že se při odesílání peněz zobrazí ze správného účtu a při přijímání \ budou připsány na váš účet. payment.monese.info.buyer=Platbu prosím zasílejte pouze na telefonní číslo, které prodejce XMR uvedl ve svém účtu Haveno. Popis platby ponechte nevyplněný. payment.monese.info.seller=Prodávající XMR by měli očekávat, že obdrží platbu z telefonního čísla / jména uvedeného v účtu XMR kupujícího Haveno. payment.satispay.info.account=Pro používání služby Satispay potřebujete bankovní účet (IBAN) v Itálii a být zaregistrován pro tuto službu.\n\n\ Limity účtu Satispay se nastavují individuálně. Pokud chcete obchodovat se zvýšenými částkami, musíte se obrátit na Satispay. podporu, aby vám zvýšila limity. Uživatelé by si také měli být vědomi limitů na účtu. Pokud obchodujete nad výše uvedené limity, \ může být váš obchod zrušen a může být uložena pokuta. payment.satispay.info.buyer=Platbu prosím zasílejte pouze na mobilní telefonní číslo prodávajícího XMR uvedené v Haveno.\n\n\ Limity účtu Satispay jsou nastaveny individuálně. Pokud chcete obchodovat se zvýšenými částkami, musíte se obrátit na Satispay. podporu, aby vám zvýšila limity. Uživatelé by si měli být vědomi také limitů na účtu. Pokud obchodujete nad výše uvedené limity \ může být váš obchod zrušen a může být uložena pokuta. payment.satispay.info.seller=Ujistěte se prosím, že platba byla přijata z mobilního telefonního čísla / jména kupujícího XMR, jak je uvedeno v Haveno.\n\n\ Limity účtu Satispay jsou nastaveny individuálně. Pokud chcete obchodovat se zvýšenými částkami, budete muset kontaktovat Satispay \ podporu, aby vám zvýšila limity. Uživatelé by si také měli být vědomi limitů na účtu. Pokud obchodujete nad výše uvedené limity \ může být váš obchod zrušen a může být uložena pokuta. payment.tikkie.info.account=K používání Tikkie potřebujete bankovní účet (IBAN) v Nizozemsku a být zaregistrováni pro tuto službu.\n\n\ Když pošlete žádost o platbu Tikkie konkrétní osobě, můžete požádat o příjem maximálně 750 EUR na Tikkie \ žádost. Maximální částka, o kterou můžete požádat během 24 hodin, je 2 500 EUR na jeden účet Tikkie.\n\n\ Každá banka však může v rámci těchto limitů stanovit pro své klienty vlastní limity.\n\n\ Uživatelé by si také měli být vědomi limitů na účtech. Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. payment.tikkie.info.buyer=Vyžádejte si prosím platební odkaz od prodejce XMR v chatu obchodníků. Jakmile prodejce XMR \ zašle platební odkaz, který odpovídá správné částce za obchod, přejděte prosím k platbě.\n\n\ Když Prodejce XMR požádá o platbu Tikkie, může požádat o platbu maximálně 750 EUR za žádost Tikkie. Pokud \ je obchod vyšší než tato částka, bude muset prodejce XMR odeslat více žádostí, aby dosáhl celkové částky obchodu. Maximální částka \ o kterou můžete požádat za den, je 2 500 €.\n\n\ Každá banka však může v rámci těchto limitů stanovit pro své klienty vlastní limity.\n\n\ Uživatelé by si také měli být vědomi limitů na účtu. Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. payment.tikkie.info.seller=Pošlete prosím odkaz na platbu prodejci XMR pomocí chatu obchodníků. Jakmile vám XMR \ kupující pošle platbu, zkontrolujte prosím, zda se jeho IBAN shoduje s údaji, které má v Haveno.\n\n\ Když Prodejce XMR požádá o platbu Tikkie, může požádat maximálně o 750 EUR na jednu žádost Tikkie. Pokud \ je obchod vyšší než tato částka, bude muset prodejce XMR odeslat více žádostí, aby dosáhl celkové částky obchodu. Maximální částka \ o které můžete požádat za den, je 2 500 €.\n\n\ Každá banka však může v rámci těchto limitů stanovit pro své klienty další vlastní limity.\n\n\ Uživatelé by si také měli být vědomi limitů na účtu. Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. payment.verse.info.account=Verse je metoda platby ve více měnách, který umožňuje odesílat a přijímat platby v EUR, SEK, HUF, DKK, PLN.\n\n\ Při nastavování účtu Verse v Haveno nezapomeňte uvést uživatelské jméno, které odpovídá vašemu uživatelskému jménu ve \ Verse. Tím zajistíte, že se při odesílání peněz zobrazí jako odeslané ze správného účtu a při přijímání \ budou připsány na váš účet.\n\n\ Uživatelé Verse jsou omezeni na odeslání nebo přijetí 10 000 EUR ročně (nebo ekvivalentní částky v cizí měně) pro \ kumulované platby odeslané z jejich platebního účtu nebo přijaté na jejich platební účet. Tuto částku může Verse na požádání zvýšit. payment.verse.info.buyer=Platbu prosím zasílejte pouze na uživatelské jméno, které prodejce XMR uvedl ve svém účtu Haveno. \ Popis platby prosím ponechte prázdný.\n\n\ Uživatelé Verse jsou omezeni na odeslání nebo přijetí 10 000 EUR ročně (nebo ekvivalentní částky v cizí měně) pro \ kumulované platby odeslané z jejich platebního účtu nebo přijaté na jejich platební účet. Tuto částku může Verse na požádání zvýšit. payment.verse.info.seller=Prodávající XMR by měli očekávat, že obdrží platbu od uživatelského jména uvedeného v účtu XMR kupujícího na Havenu.\n\n\ Uživatelé Verse jsou omezeni na odeslání nebo přijetí 10 000 EUR ročně (nebo ekvivalentní částky v cizí měně) pro \ kumulované platby odeslané z jejich platebního účtu nebo přijaté na jejich platební účet. Tuto částku může Verse na požádání zvýšit. payment.achTransfer.info.account=Při přidávání ACH jako platební metody v systému Haveno by se uživatelé měli ujistit, že vědí, \ kolik peněz bude odeslání a přijetí ACH převodu stát. payment.achTransfer.info.buyer=Ujistěte se, že jste si vědomi, kolik vás bude odeslání ACH převodu stát.\n\n\ Při platbě zasílejte pouze na platební údaje uvedené v účtu prodávajícího XMR pomocí převodu ACH. payment.achTransfer.info.seller=Ujistěte se prosím, že jste si vědomi toho, kolik vás bude přijetí ACH převodu stát peněz.\n\n\ Při přijímání platby zkontrolujte, zda je přijata z účtu kupujícího XMR jako převod ACH. payment.domesticWire.info.account=Při přidávání tuzemského bankovního převodu jako platební metody v systému Haveno by se uživatelé měli ujistit, že \ vědí, kolik je bude stát peněz odeslání a přijetí bankovního převodu. payment.domesticWire.info.buyer=Ujistěte se, že jste si vědomi, kolik vás bude odeslání bankovního převodu stát peněz.\n\n\ Při platbě zasílejte pouze na platební údaje uvedené v účtu prodávajícího XMR. payment.domesticWire.info.seller=Ujistěte se prosím, že jste si vědomi toho, kolik vás bude přijetí bankovního převodu stát peněz.\n\n\ Při přijímání platby zkontrolujte, zda byla přijata z účtu kupujícího XMR. payment.strike.info.account=Nezapomeňte uvést své uživatelské jméno Strike.\n\n\ V systému Haveno se Strike používá pouze pro platby fiat na fiat.\n\n\ Ujistěte se prosím, že znáte limity Strike:\n\n\ Uživatelé, kteří se zaregistrovali pouze se svým e-mailem, jménem a telefonním číslem, mají následující limity:\n\n\ ● maximálně 100 USD na vklad\n\n ● Maximální celkový vklad 1000 USD za týden\n\n ● maximálně 100 USD na platbu\n\n\ Uživatelé mohou své limity zvýšit tím, že společnosti Strike poskytnou více informací. Tito uživatelé mají následující limity:\n\n\n ● 1 000 USD maximálně na jeden vklad\n\ ● Maximální celkový objem vkladů za týden ve výši 1 000 USD\n\ ● 1 000 USD maximálně za platbu\n\n\ Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. payment.strike.info.buyer=Platbu prosím zasílejte pouze na uživatelské jméno Strike prodávajícího XMR, které je uvedené v Haveno.\n\n\ Maximální velikost obchodu je 1 000 USD na jednu platbu.\n\n\ Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. payment.strike.info.seller=Ujistěte se, že platba byla přijata z uživatelského jména Strike, které patří XMR kupujícímu a které je uvedeno v Haveno.\n\n\ Maximální velikost obchodu je 1 000 USD na jednu platbu.\n\n\ Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. payment.transferwiseUsd.info.account=Vzhledem k bankovní regulaci USA má odesílání a přijímání plateb v USD více omezení \ než u většiny ostatních měn. Z tohoto důvodu nebyl USD přidán do platební metody Haveno Wise.\n\n\ Platební metoda Wise-USD umožňuje uživatelům Haveno obchodovat v USD.\n\n\ Každý, kdo má účet Wise, formálně Wise, může přidat Wise-USD jako platební metodu v systému Haveno. To umožní \ uživateli nakupovat a prodávat XMR za USD.\n\n\ Při obchodování na Haveno by kupující XMR neměli uvádět v poznámce žádný důvod platby. Pokud je důvod platby vyžadován, \ měli by používat pouze celé jméno majitele účtu Wise-USD. payment.transferwiseUsd.info.buyer=Platbu prosím zasílejte pouze na e-mailovou adresu uvedenou v účtu Haveno Wise-USD prodávajícího XMR. payment.transferwiseUsd.info.seller=Zkontrolujte, zda se přijatá platba shoduje se jménem kupujícího XMR na účtu Wise-USD v systému Haveno. payment.usPostalMoneyOrder.info=Obchodování pomocí amerických poštovních poukázek (USPMO) na Haveno vyžaduje, abyste rozuměli následujícímu:\n\ \n\ - Kupující XMR musí před odesláním napsat jméno prodejce XMR do polí plátce i příjemce a pořídit fotografii USPMO a obálku s dokladem o sledování ve vysokém rozlišení.\n\ - Kupující XMR musí odeslat USPMO prodejci XMR s potvrzením dodávky.\n\ \n\ V případě, že je nutná mediace, nebo pokud dojde k obchodnímu sporu, budete povinni poslat fotografie mediátorovi Haveno nebo zástupci pro vrácení peněz spolu s pořadovým číslem USPMO, číslem pošty a částkou dolaru, aby mohli ověřit podrobnosti na webu US Post Office.\n\n\ Neposkytnutí požadovaných informací mediátorovi nebo rozhodci bude mít za následek ztrátu případu sporu.\n\n\ Ve všech sporných případech nese odesílatel USPMO 100% břemeno odpovědnosti za poskytnutí důkazů mediátorovi nebo rozhodci.\n\n\ Pokud těmto požadavkům nerozumíte, neobchodujte pomocí USPMO na Haveno. payment.payByMail.info=Obchodování pomocí služby Hotovost poštou na Havenu vyžaduje, abyste rozuměli následujícím podmínkám:\n\ \n\ ● Kupující XMR by měl zabalit hotovost do sáčku na peníze, který je odolný proti manipulaci.\n\ ● Kupující XMR by měl natočit nebo vyfotografovat proces balení hotovosti ve vysokém rozlišení s adresou a sledovacím číslem již připevněným na obalu.\n\ ● Kupující XMR by měl odeslat balíček s hotovostí prodávajícímu XMR s potvrzením o doručení a příslušným pojištěním.\n\ ● Prodávající XMR by měl natočit otevření balíku a ujistit se, že je na videu vidět sledovací číslo poskytnuté odesílatelem.\n\ ● Tvůrce nabídky musí uvést veškeré dodatečné podmínky v poli 'Další informace' na platebním účtu.\n\ ● Příjemce nabídky přijetím nabídky souhlasí s podmínkami tvůrce nabídky.\n\ \n\ Obchody Hotovost poštou kladou břemeno jednat čestně rovnoměrně na obě strany.\n\ \n\ ● Hotovostní poštovní obchody jsou méně ověřitelné než jiné tradiční obchody. To značně ztěžuje řešení sporů.\n\ ● Spory se snažte řešit přímo s partnerem pomocí chatu obchodníků. To je nejslibnější cesta k vyřešení jakéhokoli sporu o Hotovost poštou.\n\ ● Rozhodci mohou váš případ posoudit a přednést své doporučení, ale NEMOHOU vám zaručeně pomoci.\n\ ● Rozhodci rozhodnou na základě důkazů, které jim budou poskytnuty. Proto dodržujte a dokumentujte výše uvedené postupy, abyste měli důkazy pro případ sporu.\n\ ● Žádosti o náhradu jakýchkoli ztracených prostředků v důsledku obchodů Hotovost poštou na Haveno NEBUDOU brány v úvahu.\n\ \n\ Pokud těmto požadavkům nerozumíte, neobchodujte pomocí Hotovost poštou na Haveno. payment.payByMail.contact=Kontaktní informace payment.payByMail.contact.prompt=Jméno příjemce poštovní obálky payment.payByMail.extraInfo.prompt=Uveďte prosím ve svých nabídkách: \n\n\ Zemi, ve které se nacházíte (např. Francie); \n\ Země / regiony, ze kterých byste přijímali obchody (např. Francie, EU nebo jakákoli evropská země); \n\ Jakékoli zvláštní podmínky; \n\ Jakékoli další údaje. payment.tradingRestrictions=Přečtěte si prosím podmínky tvůrce.\n\ Pokud nesplňujete požadavky, nepřijímejte tento obchod. payment.cashAtAtm.info=Výběr bez karty: Výběr z bankomatu bez karty a pomocí kódu\n\n\ Použití této metody platby:\n\n\ 1. Vytvořte si platební účet Výběr bez karty a uveďte přijímané banky, regiony nebo jiné podmínky, které se zobrazí u nabídky.\n\n\n\ 2. Vytvořte nebo přijměte nabídku s tímto platebním účtem.\n\n\ 3. Po přijetí nabídky se domluvte s obchodním partnerem na čase dokončení platby a sdílejte podrobnosti o platbě.\n\n\ Pokud je částka vyšší než je limit pro výběr hotovosti, obchodníci by ji měli rozdělit na několik transakcí.\n\n\ Hotovostní transakce v bankomatech musí být násobkem 10. Doporučuje se používat nabídky s rozsahem, aby se částka XMR mohla přizpůsobit a odpovídat přesné ceně.\n\n\ Pokud se vám nepodaří dokončit transakci, jak je uvedeno v obchodní smlouvě, můžete přijít o část (nebo celou) vaší kauci. payment.cashAtAtm.extraInfo.prompt=Uveďte prosím ve svých nabídkách: \n\n\ Vámi přijímané banky / místa; \n\ Jakékoli zvláštní podmínky; \n\ Jakékoli další podrobnosti. payment.f2f.contact=Kontaktní informace payment.f2f.contact.prompt=Jak byste chtěli být kontaktováni obchodním partnerem? (e-mailová adresa, telefonní číslo, ...) payment.f2f.city=Město pro setkání 'tváří v tvář' payment.f2f.city.prompt=Město se zobrazí s nabídkou payment.shared.optionalExtra=Volitelné další informace payment.shared.extraInfo=Další informace payment.shared.extraInfo.offer=Další informace o nabídce payment.shared.extraInfo.prompt.paymentAccount=Uveďte jakékoli speciální požadavky, podmínky a detaily, které chcete zobrazit u vašich nabídek s tímto platebním účtem. (Uživatelé uvidí tyto informace předtím, než akceptují vaši nabídku.) payment.shared.extraInfo.prompt.offer=Definujte jakékoli speciální podmínky, podmínky nebo detaily, které chcete zobrazit u své nabídky. payment.shared.extraInfo.noDeposit=Kontaktní údaje a podmínky nabídky payment.f2f.info=Obchody 'tváří v tvář' mají různá pravidla a přicházejí s jinými riziky než online transakce.\n\n\ Hlavní rozdíly jsou:\n\ ● Obchodní partneři si musí vyměňovat informace o místě a čase schůzky pomocí poskytnutých kontaktních údajů.\n\ ● Obchodní partneři musí přinést své notebooky a na místě setkání potvrdit 'platba odeslána' a 'platba přijata'.\n\ ● Pokud má tvůrce speciální 'podmínky', musí uvést podmínky v textovém poli 'Další informace' na účtu.\n\ ● Přijetím nabídky zadavatel souhlasí s uvedenými 'podmínkami a podmínkami' tvůrce.\n\ ● V případě sporu nemůže být mediátor nebo rozhodce příliš nápomocný, protože je obvykle obtížné získat důkazy o tom, co se na schůzce stalo. \ V takových případech mohou být prostředky XMR uzamčeny na dobu neurčitou \ nebo dokud se obchodní partneři nedohodnou.\n\n\ Abyste si byli jisti, že plně rozumíte rozdílům v obchodech 'tváří v tvář', přečtěte si pokyny a doporučení \ na adrese: [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/F2F] payment.f2f.info.openURL=Otevřít webovou stránku payment.f2f.offerbook.tooltip.countryAndCity=Země a město: {0} / {1} payment.shared.extraInfo.tooltip=Další informace: {0} payment.ifsc=IFS kód payment.ifsc.validation=IFSC formát: XXXX0999999 payment.japan.bank=Banka payment.japan.branch=Pobočka payment.japan.account=Účet payment.japan.recipient=Jméno payment.australia.payid=PayID payment.payid=PayID spojené s finanční institucí. Jako e-mailová adresa nebo mobilní telefon. payment.payid.info=PayID jako telefonní číslo, e-mailová adresa nebo australské obchodní číslo (ABN), které můžete bezpečně propojit se svou \ bankou, družstevní záložnou nebo účtem stavební spořitelny. Musíte mít již vytvořený PayID u své australské finanční instituce. \ Odesílající i přijímající finanční instituce musí podporovat PayID. Další informace najdete na [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=Chcete-li platit dárkovou kartou Amazon eGift, budete muset prodejci XMR poslat kartu Amazon eGift přes svůj účet Amazon.\n\n\ Podívejte se do wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Amazon_eGift_card] pro podrobnosti a rady.\n\n\ Zde jsou tři důležité poznámky:\n\ - Preferujte dárkové karty v hodnotě do 100 USD, protože Amazon může považovat nákupy karet s vyššími částkami jako podezřelé a zablokovat je.\n\ - Na kartě do zprávy pro příjemce můžete přidat i vlastní originální text (např. "Happy birthday Susan!") spolu s ID obchodu (v takovém případě \ o tom informujte protistranu pomocí obchodovacího chatu, aby mohli s jistotou ověřit, že obdržená dárková karta pochází od vás.)\n\ - Karty Amazon eGift lze uplatnit pouze na té stránce Amazon, na které byly také koupeny (např. karta koupená na amazon.it může být uplatněna zase jen na amazon.it). payment.paysafe.info=Pro vaši ochranu důrazně nedoporučujeme používat Paysafecard PINy pro platby.\n\n\ Transakce provedené pomocí PINů nelze nezávisle ověřit pro řešení sporů. Pokud nastane problém, obnova prostředků nemusí být možná.\n\n\ Pro zajištění bezpečnosti transakcí a podpory řešení sporů vždy používejte platební metody, které poskytují ověřitelné záznamy. # We use constants from the code so we do not use our normal naming convention # dynamic values are not recognized by IntelliJ # Only translate general terms NATIONAL_BANK=Národní bankovní převod SAME_BANK=Převod ve stejné bance SPECIFIC_BANKS=Převody u konkrétních bank US_POSTAL_MONEY_ORDER=Poukázka US Postal CASH_DEPOSIT=Vklad hotovosti PAY_BY_MAIL=Hotovost poštou CASH_AT_ATM=Hotovost u bankomatu MONEY_GRAM=MoneyGram WESTERN_UNION=Western Union F2F=Tváří v tvář (osobně) JAPAN_BANK=Japan Bank Furikomi AUSTRALIA_PAYID=Australské PayID # suppress inspection "UnusedProperty" NATIONAL_BANK_SHORT=Národní banky # suppress inspection "UnusedProperty" SAME_BANK_SHORT=Stejná banka # suppress inspection "UnusedProperty" SPECIFIC_BANKS_SHORT=Konkrétní banky # suppress inspection "UnusedProperty" US_POSTAL_MONEY_ORDER_SHORT=US Money Order # suppress inspection "UnusedProperty" CASH_DEPOSIT_SHORT=Vklad hotovosti # suppress inspection "UnusedProperty" PAY_BY_MAIL_SHORT=Hotovost poštou # suppress inspection "UnusedProperty" CASH_AT_ATM_SHORT=Hotovost u bankomatu # suppress inspection "UnusedProperty" MONEY_GRAM_SHORT=MoneyGram # suppress inspection "UnusedProperty" WESTERN_UNION_SHORT=Western Union # suppress inspection "UnusedProperty" F2F_SHORT=F2F # suppress inspection "UnusedProperty" JAPAN_BANK_SHORT=Japan Furikomi # suppress inspection "UnusedProperty" AUSTRALIA_PAYID_SHORT=PayID # Do not translate brand names # suppress inspection "UnusedProperty" UPHOLD=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY=Popmoney # suppress inspection "UnusedProperty" REVOLUT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY=WeChat Pay # suppress inspection "UnusedProperty" SEPA=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT=SEPA okamžité platby # suppress inspection "UnusedProperty" FASTER_PAYMENTS=Faster Payments # suppress inspection "UnusedProperty" SWISH=Swish # suppress inspection "UnusedProperty" ZELLE=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS=Kryptoměny # suppress inspection "UnusedProperty" PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE=Wise # suppress inspection "UnusedProperty" TRANSFERWISE_USD=Wise-USD # suppress inspection "UnusedProperty" PAYSERA=Paysera # suppress inspection "UnusedProperty" PAXUM=Paxum # suppress inspection "UnusedProperty" NEFT=India/NEFT # suppress inspection "UnusedProperty" RTGS=India/RTGS # suppress inspection "UnusedProperty" IMPS=India/IMPS # suppress inspection "UnusedProperty" UPI=India/UPI # suppress inspection "UnusedProperty" PAYTM=India/PayTM # suppress inspection "UnusedProperty" NEQUI=Nequi # suppress inspection "UnusedProperty" BIZUM=Bizum # suppress inspection "UnusedProperty" PIX=Pix # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT=Kryptoměny okamžité # suppress inspection "UnusedProperty" CAPITUAL=Capitual # suppress inspection "UnusedProperty" CELPAY=CelPay # suppress inspection "UnusedProperty" MONESE=Monese # suppress inspection "UnusedProperty" SATISPAY=Satispay # suppress inspection "UnusedProperty" TIKKIE=Tikkie # suppress inspection "UnusedProperty" VERSE=Verse # suppress inspection "UnusedProperty" STRIKE=Strike # suppress inspection "UnusedProperty" SWIFT=SWIFT mezinárodní bankovní převod # suppress inspection "UnusedProperty" ACH_TRANSFER=ACH Transfer # suppress inspection "UnusedProperty" DOMESTIC_WIRE_TRANSFER=Domestic Wire Transfer # suppress inspection "UnusedProperty" BSQ_SWAP=BSQ Swap # suppress inspection "UnusedProperty" OK_PAY=OKPay # suppress inspection "UnusedProperty" CASH_APP=Cash App # suppress inspection "UnusedProperty" VENMO=Venmo # suppress inspection "UnusedProperty" PAYPAL=PayPal # suppress inspection "UnusedProperty" PAYSAFE=Paysafe # suppress inspection "UnusedProperty" UPHOLD_SHORT=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM_SHORT=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY_SHORT=Popmoney # suppress inspection "UnusedProperty" REVOLUT_SHORT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY_SHORT=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY_SHORT=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY_SHORT=WeChat Pay # suppress inspection "UnusedProperty" SEPA_SHORT=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT_SHORT=SEPA okamžité # suppress inspection "UnusedProperty" FASTER_PAYMENTS_SHORT=Faster Payments # suppress inspection "UnusedProperty" SWISH_SHORT=Swish # suppress inspection "UnusedProperty" ZELLE_SHORT=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY_SHORT=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER_SHORT=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH_SHORT=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS_SHORT=Kryptoměny # suppress inspection "UnusedProperty" PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" TRANSFERWISE_USD_SHORT=Wise-USD # suppress inspection "UnusedProperty" PAYSERA_SHORT=Paysera # suppress inspection "UnusedProperty" PAXUM_SHORT=Paxum # suppress inspection "UnusedProperty" NEFT_SHORT=NEFT # suppress inspection "UnusedProperty" RTGS_SHORT=RTGS # suppress inspection "UnusedProperty" IMPS_SHORT=IMPS # suppress inspection "UnusedProperty" UPI_SHORT=UPI # suppress inspection "UnusedProperty" PAYTM_SHORT=PayTM # suppress inspection "UnusedProperty" NEQUI_SHORT=Nequi # suppress inspection "UnusedProperty" BIZUM_SHORT=Bizum # suppress inspection "UnusedProperty" PIX_SHORT=Pix # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT_SHORT=Kryptoměny okamžité # suppress inspection "UnusedProperty" CAPITUAL_SHORT=Capitual # suppress inspection "UnusedProperty" CELPAY_SHORT=CelPay # suppress inspection "UnusedProperty" MONESE_SHORT=Monese # suppress inspection "UnusedProperty" SATISPAY_SHORT=Satispay # suppress inspection "UnusedProperty" TIKKIE_SHORT=Tikkie # suppress inspection "UnusedProperty" VERSE_SHORT=Verse # suppress inspection "UnusedProperty" STRIKE_SHORT=Strike # suppress inspection "UnusedProperty" SWIFT_SHORT=SWIFT # suppress inspection "UnusedProperty" ACH_TRANSFER_SHORT=ACH # suppress inspection "UnusedProperty" DOMESTIC_WIRE_TRANSFER_SHORT=Domestic Wire # suppress inspection "UnusedProperty" BSQ_SWAP_SHORT=BSQ Swap # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY_SHORT=OKPay # suppress inspection "UnusedProperty" CASH_APP_SHORT=Cash App # suppress inspection "UnusedProperty" VENMO_SHORT=Venmo # suppress inspection "UnusedProperty" PAYPAL_SHORT=PayPal # suppress inspection "UnusedProperty" PAYSAFE_SHORT=Paysafe #################################################################### # Validation #################################################################### validation.empty=Prázdný vstup není povolen. validation.NaN=Vstup není platné číslo. validation.notAnInteger=Vstup není celočíselná hodnota. validation.zero=Vstup 0 není povolen. validation.negative=Záporná hodnota není povolena. validation.traditional.tooSmall=Vstup menší než minimální možné množství není povolen. validation.traditional.tooLarge=Vstup větší než maximální možné množství není povolen. validation.xmr.fraction=Zadání povede k hodnotě monerou menší než 1 satoshi validation.xmr.tooLarge=Vstup větší než {0} není povolen. validation.xmr.tooSmall=Vstup menší než {0} není povolen. validation.passwordTooShort=Zadané heslo je příliš krátké. Musí mít min. 8 znaků. validation.passwordTooLong=Zadané heslo je příliš dlouhé. Nemůže být delší než 50 znaků. validation.sortCodeNumber={0} se musí skládat z {1} čísel. validation.sortCodeChars={0} musí obsahovat {1} znaků. validation.bankIdNumber={0} se musí skládat z {1} čísel. validation.accountNr=Číslo účtu se musí skládat z {0} čísel. validation.accountNrChars=Číslo účtu musí obsahovat {0} znaků. validation.xmr.invalidAddress=Adresa není správná. Zkontrolujte formát adresy. validation.integerOnly=Zadejte pouze celá čísla. validation.inputError=Váš vstup způsobil chybu:\n{0} validation.xmr.exceedsMaxTradeLimit=Váš obchodní limit je {0}. validation.nationalAccountId={0} se musí skládat z {1} čísel. #new validation.invalidInput=Neplatný vstup: {0} validation.accountNrFormat=Číslo účtu musí být ve formátu: {0} # suppress inspection "UnusedProperty" validation.crypto.wrongStructure=Ověření adresy se nezdařilo, protože neodpovídá struktuře adresy {0}. # suppress inspection "UnusedProperty" validation.crypto.ltz.zAddressesNotSupported=Adresa LTZ musí začínat na "L". Adresy začínající na "z" nejsou podporovány. # suppress inspection "UnusedProperty" validation.crypto.zAddressesNotSupported=Adresy ZEC musí začínat na "t". Adresy začínající na "z" nejsou podporovány. # suppress inspection "UnusedProperty" validation.crypto.invalidAddress=Adresa není platná {0} adresa! {1} # suppress inspection "UnusedProperty" validation.crypto.liquidBitcoin.invalidAddress=Nativní adresy segwit (ty začínající na 'lq') nejsou podporovány. validation.bic.invalidLength=Délka vstupu musí být 8 nebo 11 validation.bic.letters=Banka a kód země musí být písmena validation.bic.invalidLocationCode=BIC obsahuje neplatný location kód validation.bic.invalidBranchCode=BIC obsahuje neplatný kód pobočky validation.bic.sepaRevolutBic=Účty Revolut Sepa nejsou podporovány. validation.btc.invalidFormat=Neplatný formát adresy Bitcoin. validation.email.invalidAddress=Neplatná adresa validation.iban.invalidCountryCode=Kód země je neplatný validation.iban.checkSumNotNumeric=Kontrolní součet musí být číselný validation.iban.nonNumericChars=Byl zjištěn nealfanumerický znak validation.iban.checkSumInvalid=Kontrolní součet IBAN je neplatný validation.iban.invalidLength=Číslo musí mít délku 15 až 34 znaků. validation.iban.sepaNotSupported=SEPA není v této zemi podporována validation.interacETransfer.invalidAreaCode=Non-kanadské směrové číslo oblasti validation.interacETransfer.invalidPhone=Zadejte platné 11místné telefonní číslo (např. 1-123-456-7890) nebo e-mailovou adresu validation.interacETransfer.invalidQuestion=Musí obsahovat pouze písmena, čísla, mezery a/nebo symboly ' _ , . ? - validation.interacETransfer.invalidAnswer=Musí to být jedno slovo a obsahovat pouze písmena, čísla a/nebo symbol - validation.inputTooLarge=Vstup nesmí být větší než {0} validation.inputTooSmall=Vstup musí být větší než {0} validation.inputToBeAtLeast=Vstup musí být alespoň {0} validation.amountBelowDust=Množství pod mezní hodnotou drobných (dust limit) {0} není povoleno. validation.length=Délka musí být mezi {0} a {1} validation.fixedLength=Délka musí být {0} validation.pattern=Vstup musí být ve formátu: {0} validation.noHexString=Vstup není ve formátu HEX. validation.advancedCash.invalidFormat=Musí to být platný e-mail nebo ID peněženky ve formátu: X000000000000 validation.invalidUrl=Toto není platná adresa URL validation.mustBeDifferent=Váš vstup se musí lišit od aktuální hodnoty validation.cannotBeChanged=Parametr nelze změnit validation.numberFormatException=Výjimka formátu čísla {0} validation.mustNotBeNegative=Vstup nesmí být záporný validation.phone.missingCountryCode=K ověření telefonního čísla je potřeba dvoumístný kód země validation.phone.invalidCharacters=Telefonní číslo {0} obsahuje neplatné znaky validation.phone.insufficientDigits=V čísle {0} není dostatek číslic, aby mohlo být platné telefonní číslo validation.phone.tooManyDigits=V čísle {0} je příliš mnoho číslic, než aby mohlo být platné telefonní číslo validation.phone.invalidDialingCode=Telefonní předvolba země pro číslo {0} je pro zemi {1} neplatná. \ Správné předčíslí je {2}. validation.invalidAddressList=Seznam platných adres musí být oddělený čárkami validation.capitual.invalidFormat=Musí jít o platný kód formátu CAP: CAP-XXXXXX (6 alfanumerických znaků). ================================================ FILE: core/src/main/resources/i18n/displayStrings_de.properties ================================================ # Keep display strings organized by domain # Naming convention: We use camelCase and dot separated name spaces. # Use as many sub spaces as required to make the structure clear, but as little as possible. # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break # in the display but only in the editor. # Please use in all language files the exact same order of the entries, that way a comparison is easier. # Please try to keep the length of the translated string similar to English. If it is longer it might break layout or # get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. #################################################################### # Shared #################################################################### shared.readMore=Weiterlesen shared.openHelp=Hilfe öffnen shared.warning=Warnung shared.close=Schließen shared.cancel=Abbrechen shared.ok=OK shared.yes=Ja shared.no=Nein shared.iUnderstand=Ich verstehe shared.na=N/A shared.shutDown=Herunterfahren shared.reportBug=Fehler auf GitHub melden shared.buyMonero=Monero kaufen shared.sellMonero=Monero verkaufen shared.buyCurrency={0} kaufen shared.sellCurrency={0} verkaufen shared.buyCurrency.locked={0} kaufen 🔒 shared.sellCurrency.locked={0} verkaufen 🔒 shared.buyingXMRWith=kaufe XMR mit {0} shared.sellingXMRFor=verkaufe XMR für {0} shared.buyingCurrency=kaufe {0} (verkaufe XMR) shared.sellingCurrency=verkaufe {0} (kaufe XMR) shared.buy=kaufen shared.sell=verkaufen shared.buying=kaufe shared.selling=verkaufe shared.P2P=P2P shared.oneOffer=Angebot shared.multipleOffers=Angebote shared.Offer=Angebot shared.offerVolumeCode={0} Angebotsvolumen shared.openOffers=offene Angebote shared.trade=Handel shared.trades=Trades shared.openTrades=offene Trades shared.dateTime=Datum/Zeit shared.price=Preis shared.priceWithCur=Preis in {0} shared.priceInCurForCur=Preis in {0} für 1 {1} shared.fixedPriceInCurForCur=Festpreis in {0} für 1 {1} shared.amount=Betrag shared.txFee=Transaktionsgebühr shared.tradeFee=Handelsgebühr shared.buyerSecurityDeposit=Käufer-Kaution shared.sellerSecurityDeposit=Verkäufer-Kaution shared.amountWithCur=Betrag in {0} shared.volumeWithCur=Volumen in {0} shared.currency=Währung shared.market=Markt shared.deviation=Abweichung shared.paymentMethod=Zahlungsmethode shared.tradeCurrency=Handelswährung shared.offerType=Angebotstyp shared.details=Details shared.address=Adresse shared.balanceWithCur=Guthaben in {0} shared.utxo=Unverbrauchte Transaktionsausgabe shared.txId=Transaktions-ID shared.confirmations=Bestätigungen shared.revert=Tx umkehren shared.select=Auswählen shared.usage=Nutzung shared.state=Status shared.tradeId=Handels-ID shared.offerId=Angebots-ID shared.bankName=Bankname shared.acceptedBanks=Akzeptierte Banken shared.amountMinMax=Betrag (min - max) shared.amountHelp=Wurde für ein Angebot ein minimaler und maximaler Betrag gesetzt, können Sie jeden Betrag innerhalb dieses Bereiches handeln. shared.remove=Entfernen shared.goTo=Zu {0} gehen shared.XMRMinMax=XMR (min - max) shared.removeOffer=Angebot entfernen shared.dontRemoveOffer=Angebot nicht entfernen shared.editOffer=Angebot bearbeiten shared.openLargeQRWindow=Großes QR-Code Fenster öffnen shared.tradingAccount=Handelskonto shared.faq=Zur FAQ Seite shared.yesCancel=Ja, abbrechen shared.nextStep=Nächster Schritt shared.selectTradingAccount=Handelskonto auswählen shared.fundFromSavingsWalletButton=Wenden Sie Gelder aus der Haveno-Wallet an shared.fundFromExternalWalletButton=Ihre externe Wallet zum Finanzieren öffnen shared.openDefaultWalletFailed=Das Öffnen des Standardprogramms für Monero-Wallets ist fehlgeschlagen. Sind Sie sicher, dass Sie eines installiert haben? shared.belowInPercent=% unter dem Marktpreis shared.aboveInPercent=% über dem Marktpreis shared.enterPercentageValue=%-Wert eingeben shared.OR=ODER shared.notEnoughFunds=Für diese Transaktion haben Sie nicht genug Gelder in Ihrem Haveno-Wallet—{0} werden benötigt, aber nur {1} sind verfügbar.\n\nBitte fügen Sie Gelder aus einer externen Wallet hinzu, oder senden Sie Gelder an Ihr Haveno-Wallet unter Gelder > Gelder erhalten. shared.waitingForFunds=Warte auf Gelder... shared.TheXMRBuyer=Der XMR-Käufer shared.You=Sie shared.sendingConfirmation=Sende Bestätigung... shared.sendingConfirmationAgain=Bitte senden Sie die Bestätigung erneut shared.exportCSV=Als CSV exportieren shared.exportJSON=Exportiere als JSON shared.summary=Show summary shared.noDateAvailable=Kein Datum verfügbar shared.noDetailsAvailable=Keine Details vorhanden shared.notUsedYet=Noch ungenutzt shared.date=Datum shared.sendFundsDetailsWithFee=Senden: {0}\n\nAn die Empfangsadresse: {1}\n\nZusätzliche Miner-Gebühr: {2}\n\nSind Sie sicher, dass Sie diesen Betrag senden möchten? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Diese Transaktion würde ein Wechselgeld erzeugen das unterhalb des Dust-Grenzwerts liegt (und daher von den Monero-Konsensregeln nicht erlaubt wäre). Stattdessen wird dieser Dust ({0} Satoshi{1}) der Mining-Gebühr hinzugefügt.\n\n\n shared.copyToClipboard=In Zwischenablage kopieren shared.language=Sprache shared.country=Land shared.applyAndShutDown=Anwenden und herunterfahren shared.selectPaymentMethod=Zahlungsmethode wählen shared.accountNameAlreadyUsed=Der Name des Kontos wird bereits für ein existierendes Konto verwendet.\nBitte wählen Sie einen anderen Namen. shared.askConfirmDeleteAccount=Möchten Sie das ausgewählte Konto wirklich löschen? shared.cannotDeleteAccount=Sie können dieses Konto nicht löschen, da es in einem offenen Angebot oder Handel gebraucht wird. shared.noAccountsSetupYet=Es wurden noch keine Konten eingerichtet shared.manageAccounts=Konten verwalten shared.addNewAccount=Neues Konto hinzufügen shared.ExportAccounts=Konten exportieren shared.importAccounts=Konten importieren shared.createNewAccount=Neues Konto erstellen shared.createNewAccountDescription=Ihre Kontodaten werden lokal auf Ihrem Gerät gespeichert und nur mit Ihrem Handelspartner und dem Schiedsrichter geteilt, wenn ein Streitfall eröffnet wird. shared.saveNewAccount=Neues Konto speichern shared.selectedAccount=Konto auswählen shared.deleteAccount=Konto löschen shared.errorMessageInline=\nFehlermeldung: {0} shared.errorMessage=Fehlermeldung shared.information=Information shared.name=Name shared.id=ID shared.dashboard=Übersicht shared.accept=Annehmen shared.balance=Guthaben shared.save=Speichern shared.onionAddress=Onion-Adresse shared.supportTicket=Support-Ticket shared.dispute=Konflikt shared.mediationCase=Mediationsfall shared.seller=Verkäufer shared.buyer=Käufer shared.allEuroCountries=Alle Euroländer shared.acceptedTakerCountries=Akzeptierte Länder für Abnehmer shared.tradePrice=Handelspreis shared.tradeAmount=Handelsbetrag shared.tradeVolume=Handelsvolumen shared.invalidKey=Der eingegebene Schlüssel war nicht korrekt. shared.enterPrivKey=Privaten Schlüssel zum Entsperren eingeben shared.payoutTxId=Transaktions-ID der Auszahlung shared.contractAsJson=Vertrag im JSON-Format shared.viewContractAsJson=Vertrag im JSON-Format ansehen shared.contract.title=Vertrag für den Handel mit der ID: {0} shared.paymentDetails=Zahlungsdetails des XMR-{0} shared.securityDeposit=Kaution shared.yourSecurityDeposit=Ihre Kaution shared.contract=Vertrag shared.messageArrived=Nachricht angekommen. shared.messageStoredInMailbox=Nachricht in Postfach gespeichert. shared.messageSendingFailed=Versenden der Nachricht fehlgeschlagen. Fehler: {0} shared.unlock=Entsperren shared.toReceive=erhalten shared.toSpend=ausgeben shared.xmrAmount=XMR-Betrag shared.yourLanguage=Ihre Sprachen shared.addLanguage=Sprache hinzufügen shared.total=Insgesamt shared.totalsNeeded=Benötigte Gelder shared.tradeWalletAddress=Adresse der Handels-Wallet shared.tradeWalletBalance=Guthaben der Handels-Wallet shared.reserveExactAmount=Reserviere nur die notwendigen Mittel. Erfordert eine Mining-Gebühr und ca. 20 Minuten, bevor dein Angebot live geht. shared.makerTxFee=Ersteller: {0} shared.takerTxFee=Abnehmer: {0} shared.iConfirm=Ich bestätige shared.openURL=Öffne {0} shared.fiat=Fiat shared.crypto=Crypto shared.preciousMetals=Edelmetalle shared.all=Alle shared.edit=Bearbeiten shared.advancedOptions=Erweiterte Optionen shared.interval=Intervall shared.actions=Aktionen shared.buyerUpperCase=Käufer shared.sellerUpperCase=Verkäufer shared.new=NEU shared.learnMore=Mehr erfahren shared.dismiss=Verwerfen shared.selectedArbitrator=Gewählte Vermittler shared.selectedMediator=Gewählter Vermittler shared.selectedRefundAgent=Gewählter Vermittler shared.mediator=Mediator shared.arbitrator=Vermittler shared.refundAgent=Vermittler shared.refundAgentForSupportStaff=Rückerstattungsbeauftragten shared.delayedPayoutTxId=Transaktions-ID der verzögerten Auszahlung shared.delayedPayoutTxReceiverAddress=Verzögerte Auszahlungs-Transaktion gesendet an shared.unconfirmedTransactionsLimitReached=Sie haben im Moment zu viele unbestätigte Transaktionen. Bitte versuchen Sie es später noch einmal. shared.numItemsLabel=Anzahl der Einträge: {0} shared.filter=Filter shared.enabled=Aktiviert #################################################################### # UI views #################################################################### #################################################################### # MainView #################################################################### mainView.menu.market=Markt mainView.menu.buyXmr=XMR kaufen mainView.menu.sellXmr=XMR verkaufen mainView.menu.portfolio=Portfolio mainView.menu.funds=Gelder mainView.menu.support=Support mainView.menu.settings=Einstellungen mainView.menu.account=Konto mainView.marketPriceWithProvider.label=Marktpreis von {0} mainView.marketPrice.havenoInternalPrice=Preis des letzten Haveno-Handels mainView.marketPrice.tooltip.havenoInternalPrice=Es ist kein Marktpreis von externen Marktpreis-Anbietern verfügbar.\nDer angezeigte Preis, ist der letzte Haveno-Handelspreis für diese Währung. mainView.marketPrice.tooltip=Marktpreis bereitgestellt von {0}{1}\nLetzte Aktualisierung: {2}\nURL des Knoten-Anbieters: {3} mainView.balance.available=Verfügbarer Betrag mainView.balance.reserved=In Angeboten reserviert mainView.balance.pending=In Trades gesperrt mainView.balance.reserved.short=Reserviert mainView.balance.pending.short=Gesperrt mainView.footer.usingTor=(über Tor) mainView.footer.localhostMoneroNode=(localhost) mainView.footer.clearnet=(über clearnet) mainView.footer.xmrInfo={0} {1} mainView.footer.xmrFeeRate=/ Aktuelle Gebühr: {0} sat/vB mainView.footer.xmrInfo.initializing=Verbindung mit Haveno-Netzwerk wird hergestellt mainView.footer.xmrInfo.synchronizingWith=Synchronisierung mit {0} bei Block: {1} / {2} mainView.footer.xmrInfo.connectedTo=Verbunden mit {0} am Block {1} mainView.footer.xmrInfo.synchronizingWalletWith=Synchronisierung der Brieftasche mit {0} im Block: {1} / {2} mainView.footer.xmrInfo.syncedWith=Synchronisiert mit {0} bei Block {1} mainView.footer.xmrInfo.connectingTo=Verbinde mit mainView.footer.xmrInfo.connectionFailed=Verbindung fehlgeschlagen zu mainView.footer.xmrPeers=Monero Netzwerk Peers: {0} mainView.footer.p2pPeers=Haveno Netzwerk Peers: {0} mainView.bootstrapState.connectionToTorNetwork=(1/4) Verbinde mit Tor-Netzwerk... mainView.bootstrapState.torNodeCreated=(2/4) Tor-Knoten erstellt mainView.bootstrapState.hiddenServicePublished=(3/4) Hidden Service veröffentlicht mainView.bootstrapState.initialDataReceived=(4/4) Anfangsdaten erhalten mainView.bootstrapWarning.noSeedNodesAvailable=Keine Seed-Knoten verfügbar mainView.bootstrapWarning.noNodesAvailable=Keine Seed-Knoten und Peers verfügbar mainView.bootstrapWarning.bootstrappingToP2PFailed=Bootstrapping zum Haveno-Netzwerk fehlgeschlagen mainView.p2pNetworkWarnMsg.noNodesAvailable=Es sind keine Seed-Knoten oder bestehenden Peers verfügbar, um Daten anzufordern.\nÜberprüfen Sie bitte Ihre Internetverbindung oder versuchen Sie die Anwendung neu zu starten. mainView.p2pNetworkWarnMsg.connectionToP2PFailed=Verbinden mit Haveno-Netzwerk fehlgeschlagen (gemeldeter Fehler: {0}).\nBitte überprüfen Sie Ihre Internetverbindungen oder versuchen Sie die Anwendung neu zu starten. mainView.walletServiceErrorMsg.timeout=Verbindung mit Monero-Netzwerk aufgrund einer Zeitüberschreitung fehlgeschlagen. mainView.walletServiceErrorMsg.connectionError=Verbindung mit Monero-Netzwerk aufgrund eines Fehlers fehlgeschlagen: {0} mainView.walletServiceErrorMsg.rejectedTxException=Eine Transaktion wurde aus dem Netzwerk abgelehnt.\n\n{0} mainView.networkWarning.allConnectionsLost=Sie haben die Verbindung zu allen {0} Netzwerk-Peers verloren.\nMöglicherweise haben Sie Ihre Internetverbindung verloren oder Ihr Computer war im Standbymodus. mainView.networkWarning.localhostMoneroLost=Sie haben die Verbindung zum localhost Moneroknoten verloren.\nBitte starten Sie die Haveno Anwendung neu, um mit anderen Moneroknoten zu verbinden oder starten Sie den localhost Moneroknoten neu. mainView.version.update=(Update verfügbar) #################################################################### # MarketView #################################################################### market.tabs.offerBook=Angebotsbuch market.tabs.spreadCurrency=Angebote nach Währung market.tabs.spreadPayment=Angebote nach Zahlungsmethode market.tabs.trades=Trades # OfferBookChartView market.offerBook.buyCrypto={0} kaufen ({1} verkaufen) market.offerBook.sellCrypto={0} verkaufen ({1} kaufen) market.offerBook.buyWithTraditional={0} kaufen market.offerBook.sellWithTraditional={0} verkaufen market.offerBook.sellOffersHeaderLabel=Verkaufe {0} an market.offerBook.buyOffersHeaderLabel=Kaufe {0} von market.offerBook.buy=Ich möchte Moneros kaufen market.offerBook.sell=Ich möchte Moneros verkaufen # SpreadView market.spread.numberOfOffersColumn=Alle Angebote ({0}) market.spread.numberOfBuyOffersColumn=XMR kaufen ({0}) market.spread.numberOfSellOffersColumn=XMR verkaufen ({0}) market.spread.totalAmountColumn=XMR insgesamt ({0}) market.spread.spreadColumn=Verteilung market.spread.expanded=Erweiterte Ansicht # TradesChartsView market.trades.nrOfTrades=Trades: {0} market.trades.tooltip.volumeBar=Volumen: {0} / {1}\nAnzahl der Trades: {2}\nDatum: {3} market.trades.tooltip.candle.open=Eröffnung: market.trades.tooltip.candle.close=Abschluss: market.trades.tooltip.candle.high=Hoch: market.trades.tooltip.candle.low=Niedrig: market.trades.tooltip.candle.average=Durchschnitt: market.trades.tooltip.candle.median=Median: market.trades.tooltip.candle.date=Datum: market.trades.showVolumeInUSD=Volumen in USD anzeigen #################################################################### # OfferView #################################################################### offerbook.createOffer=Angebot erstellen offerbook.takeOffer=Angebot annehmen offerbook.takeOfferToBuy=Angebot annehmen {0} zu kaufen offerbook.takeOfferToSell=Angebot annehmen {0} zu verkaufen offerbook.takeOffer.enterChallenge=Geben Sie das Angebots-Passphrase ein offerbook.trader=Händler offerbook.offerersBankId=Bankkennung des Erstellers (BIC/SWIFT): {0} offerbook.offerersBankName=Bankname des Erstellers: {0} offerbook.offerersBankSeat=Banksitz-Land des Erstellers: {0} offerbook.offerersAcceptedBankSeatsEuro=Als Banksitz akzeptierte Länder (Abnehmer): Alle Euroländer offerbook.offerersAcceptedBankSeats=Als Banksitz akzeptierte Länder (Abnehmer):\n{0} offerbook.availableOffers=Verfügbare Angebote offerbook.filterByCurrency=Nach Währung filtern offerbook.filterByPaymentMethod=Nach Zahlungsmethode filtern offerbook.matchingOffers=Angebote die meinen Zahlungskonten entsprechen offerbook.filterNoDeposit=Kein Deposit offerbook.noDepositOffers=Angebote ohne Einzahlung (Passphrase erforderlich) offerbook.timeSinceSigning=Informationen zum Zahlungskonto offerbook.timeSinceSigning.info=Dieses Konto wurde verifiziert und {0} offerbook.timeSinceSigning.info.arbitrator=von einem Vermittler unterzeichnet und kann Partner-Konten unterzeichnen offerbook.timeSinceSigning.info.peer=von einem Handelspartner unterzeichnet, es muss noch %d Tage warten bis alle Beschränkungen aufgehoben werden offerbook.timeSinceSigning.info.peerLimitLifted=von einem Partner unterzeichnet und Limits wurden aufgehoben offerbook.timeSinceSigning.info.signer=vom Partner unterzeichnet und kann Partner-Konten unterzeichnen (Limits aufgehoben) offerbook.timeSinceSigning.info.banned=Konto wurde geblockt offerbook.timeSinceSigning.daysSinceSigning={0} Tage offerbook.timeSinceSigning.daysSinceSigning.long={0} seit der Unterzeichnung offerbook.xmrAutoConf=Automatische Bestätigung aktiviert offerbook.buyXmrWith=XMR kaufen mit: offerbook.sellXmrFor=XMR verkaufen für: offerbook.timeSinceSigning.help=Wenn Sie einen Trade mit einem Partner erfolgreich abschließen, der ein unterzeichnetes Zahlungskonto hat, wird Ihr Zahlungskonto unterzeichnet.\n{0} Tage später wird das anfängliche Limit von {1} aufgehoben und Ihr Konto kann die Zahlungskonten anderer Partner unterzeichnen. offerbook.timeSinceSigning.notSigned=Noch nicht unterzeichnet offerbook.timeSinceSigning.notSigned.ageDays={0} Tage offerbook.timeSinceSigning.notSigned.noNeed=N/A shared.notSigned=Dieses Konto wurde noch nicht unterzeichnet. Es wurde vor {0} Tag(en) erstellt shared.notSigned.noNeed=Konten dieses Typs benötigen keine Unterzeichnung shared.notSigned.noNeedDays=Konten dieses Typs benötigen keine Unterzeichnung. Es wurde vor {0} Tag(en) erstellt shared.notSigned.noNeedAlts=Crypto Konten haben keine Merkmale wie Unterzeichnung oder Alter offerbook.nrOffers=Anzahl der Angebote: {0} offerbook.volume={0} (min - max) offerbook.deposit=Kaution XMR (%) offerbook.deposit.help=Kaution die von beiden Handelspartnern bezahlt werden muss, um den Handel abzusichern. Wird zurückgezahlt, wenn der Handel erfolgreich abgeschlossen wurde. offerbook.createNewOffer=Erstelle Angebot an {0} {1} offerbook.createOfferToBuy=Neues Angebot erstellen, um {0} zu kaufen offerbook.createOfferToSell=Neues Angebot erstellen, um {0} zu verkaufen offerbook.createOfferToBuy.withTraditional=Neues Angebot erstellen, um {0} mit {1} zu kaufen offerbook.createOfferToSell.forTraditional=Neues Angebot erstellen, um {0} für {1} zu verkaufen offerbook.createOfferToBuy.withCrypto=Angebot erstellen, um {0} zu verkaufen ({1} kaufen) offerbook.createOfferToSell.forCrypto=Angebot erstellen, um {0} zu kaufen ({1} verkaufen) offerbook.takeOfferButton.tooltip=Angebot annehmen für {0} offerbook.yesCreateOffer=Ja, Angebot erstellen offerbook.setupNewAccount=Neues Handelskonto einrichten offerbook.removeOffer.success=Das Entfernen des Angebots war erfolgreich. offerbook.removeOffer.failed=Entfernen des Angebots ist fehlgeschlagen:\n{0} offerbook.deactivateOffer.failed=Deaktivieren des Angebots fehlgeschlagen:\n{0} offerbook.activateOffer.failed=Veröffentlichung des Angebots fehlgeschlagen:\n{0} offerbook.withdrawFundsHint=Sie können die eingezahlten Gelder im {0}-Bildschirm abheben. offerbook.warning.noTradingAccountForCurrency.headline=Kein Zahlungskonto für die gewählte Währung offerbook.warning.noTradingAccountForCurrency.msg=Sie haben kein Zahlungskonto für die gewählte Währung eingerichtet.\n\nWollen Sie stattdessen ein Handelsangebot für eine andere Währung erstellen? offerbook.warning.noMatchingAccount.headline=Kein passendes Zahlungskonto. offerbook.warning.noMatchingAccount.msg=Dieses Angebot verwendet eine Zahlungsmethode die Sie noch nicht eingerichtet haben.\n\nWollen Sie jetzt ein neues Zahlungskonto einrichten? offerbook.warning.counterpartyTradeRestrictions=Dieses Angebot kann aufgrund von Handelsbeschränkungen der Gegenpartei nicht angenommen werden offerbook.warning.newVersionAnnouncement=Mit dieser Version der Software können Handelspartner gegenseitig Zahlungskonten verifizieren und unterzeichnen, um ein Netzwerk vertrauenswürdiger Zahlungskonten aufzubauen.\n\nNach dem erfolgreichen Handel mit einem verifizierten Handelspartner, wird auch Ihr Zahlungskonto unterzeichnet und Ihre Handels-Beschränkungen werden nach einer gewissen Zeit aufgehoben (die Länge kann je nach Zahlungsmethode unterschiedlich sein).\n\nWeitere Informationen zur Unterzeichnung von Konten finden Sie hier in der Dokumentation: [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. popup.warning.tradeLimitDueAccountAgeRestriction.seller=Der zulässige Trade-Betrag ist aufgrund von Sicherheitseinschränkungen, die auf den folgenden Kriterien basieren, auf {0} begrenzt:\n- Das Konto des Käufers wurde nicht von einem Vermittler oder einem Partner unterzeichnet\n- Die Zeit seit der Unterzeichnung des Kontos des Käufers beträgt nicht mindestens 30 Tage\n- Die Zahlungsmethode für dieses Angebot gilt als riskant für Bankrückbuchungen\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=Der zulässige Trade-Betrag ist aufgrund von Sicherheitseinschränkungen, die auf den folgenden Kriterien basieren, auf {0} begrenzt:\n- Ihr Konto wurde nicht von einem Vermittler oder einem Partner unterzeichnet\n- Die Zeit seit der Unterzeichnung Ihres Kontos beträgt nicht mindestens 30 Tage\n- Die Zahlungsmethode für dieses Angebot gilt als riskant für Bankrückbuchungen\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=Diese Zahlungsmethode ist vorübergehend auf {0} bis {1} begrenzt, da alle Käufer neue Konten haben.\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=Ihr Angebot wird auf Käufer mit unterzeichneten und alten Konten beschränkt sein, da es {0} übersteigt.\n\n{1} offerbook.warning.wrongTradeProtocol=Dieses Angebot benötigt eine andere Protokollversion, als die Version Ihrer Software.\n\nBitte überprüfen Sie, ob Sie die aktuellste Version installiert haben. Andernfalls hat der Nutzer, der das Angebot erstellt hat, eine ältere Version benutzt.\n\nNutzer können nicht mit inkompatiblen Protokollversionen handeln. offerbook.warning.userIgnored=Sie haben die Onion-Adresse dieses Nutzers zu Ihrer Liste ignorierter Adressen hinzugefügt. offerbook.warning.offerBlocked=Das Angebot wurde von den Haveno-Entwicklern blockiert.\nWahrscheinlich gibt es einen unbehobenen Bug, der Probleme beim Annehmen dieses Angebots verursacht. offerbook.warning.currencyBanned=Die in diesem Handel verwendete Währung wurde von den Haveno-Entwicklern blockiert.\nBitte besuchen sie das Haveno-Forum für weitere Informationen. offerbook.warning.paymentMethodBanned=Die in diesem Handel verwendete Zahlungsmethode wurde von den Haveno-Entwicklern blockiert.\nBitte besuchen sie das Haveno-Forum für weitere Informationen. offerbook.warning.nodeBlocked=Die Onion-Adresse dieses Händlers wurde von den Haveno-Entwicklern blockiert.\nWahrscheinlich gibt es einen unbehobenen Bug, der Probleme beim Annehmen von Angeboten dieses Händlers verursacht. offerbook.warning.requireUpdateToNewVersion=Ihre Haveno-Version ist nicht mehr zum Handeln geeignet.\nBitte updaten Sie Haveno auf die aktuellste Version unter [HYPERLINK:https://haveno.exchange/downloads]. offerbook.warning.offerWasAlreadyUsedInTrade=Sie können dieses Angebot nicht annehmen, weil Sie das früher schon getan haben. Es kann sein, dass Ihr vorheriger Annahme-Versuch zu einem fehlgeschlagenen Handel geführt hat. offerbook.info.sellAtMarketPrice=Sie verkaufen zum aktuellen Marktpreis (jede Minute aktualisiert). offerbook.info.buyAtMarketPrice=Sie kaufen zum aktuellen Marktpreis (jede Minute aktualisiert). offerbook.info.sellBelowMarketPrice=Sie bekommen {0} weniger verglichen zum aktuellen Marktpreis (jede Minute aktualisiert). offerbook.info.buyAboveMarketPrice=Sie zahlen {0} mehr verglichen zum aktuellen Marktpreis (jede Minute aktualisiert). offerbook.info.sellAboveMarketPrice=Sie bekommen {0} mehr verglichen zum aktuellen Marktpreis (jede Minute aktualisiert). offerbook.info.buyBelowMarketPrice=Sie zahlen {0} weniger verglichen zum aktuellen Marktpreis (jede Minute aktualisiert). offerbook.info.buyAtFixedPrice=Sie kaufen zu diesem Festpreis. offerbook.info.sellAtFixedPrice=Sie verkaufen zu diesem Festpreis. offerbook.info.noArbitrationInUserLanguage=Im Konflikt ist zu beachten, dass die Vermittlung für dieses Angebot in {0} abgewickelt wird. Die Sprache ist derzeit auf {1} eingestellt. offerbook.info.roundedFiatVolume=Der Betrag wurde gerundet, um die Privatsphäre Ihres Handels zu erhöhen. #################################################################### # Offerbook / Create offer #################################################################### createOffer.amount.prompt=Betrag in XMR eingeben createOffer.price.prompt=Preis eingeben createOffer.volume.prompt=Betrag in {0} eingeben createOffer.amountPriceBox.amountDescription=Betrag in XMR zu {0} createOffer.amountPriceBox.buy.volumeDescription=Auszugebender Betrag in {0} createOffer.amountPriceBox.sell.volumeDescription=Zu erhaltender Betrag in {0} createOffer.amountPriceBox.minAmountDescription=Minimaler Betrag in XMR createOffer.securityDeposit.prompt=Kaution createOffer.fundsBox.title=Ihr Angebot finanzieren createOffer.fundsBox.offerFee=Handelsgebühr createOffer.fundsBox.networkFee=Mining-Gebühr createOffer.fundsBox.placeOfferSpinnerInfo=Das Angebot wird veröffentlicht ... createOffer.fundsBox.paymentLabel=Haveno-Handel mit der ID {0} createOffer.fundsBox.fundsStructure=({0} Kaution, {1} Handelsgebühr, {2} Mining-Gebühr) createOffer.success.headline=Ihr Angebot wurde erstellt createOffer.success.info=Sie können Ihre offenen Angebote unter \"Portfolio/Meine offenen Angebote\" verwalten. createOffer.info.sellAtMarketPrice=Sie verkaufen immer zum aktuellen Marktpreis, da ihr Angebot ständig aktualisiert wird. createOffer.info.buyAtMarketPrice=Sie kaufen immer zum aktuellen Marktpreis, da ihr Angebot ständig aktualisiert wird. createOffer.info.sellAboveMarketPrice=Sie erhalten immer {0}% mehr als der aktuelle Marktpreis, da ihr Angebot ständig aktualisiert wird. createOffer.info.buyBelowMarketPrice=Sie zahlen immer {0}% weniger als der aktuelle Marktpreis, da ihr Angebot ständig aktualisiert wird. createOffer.warning.sellBelowMarketPrice=Sie erhalten immer {0}% weniger als der aktuelle Marktpreis, da ihr Angebot ständig aktualisiert wird. createOffer.warning.buyAboveMarketPrice=Sie zahlen immer {0}% mehr als der aktuelle Marktpreis, da ihr Angebot ständig aktualisiert wird. createOffer.tradeFee.descriptionXMROnly=Handelsgebühr createOffer.tradeFee.descriptionBSQEnabled=Gebührenwährung festlegen createOffer.triggerPrice.prompt=Auslösepreis (optional) createOffer.triggerPrice.label=Angebot bei einem Marktpreis von {0} deaktivieren createOffer.triggerPrice.tooltip=Als Schutz vor drastischen Preisbewegungen können Sie einen Auslösepreis festlegen, der das Angebot deaktiviert, wenn der Marktpreis diesen Wert erreicht. createOffer.triggerPrice.invalid.tooLow=Wert muss höher sein als {0} createOffer.triggerPrice.invalid.tooHigh=Wert muss niedriger sein als {0} # new entries createOffer.placeOfferButton.buy=Überprüfen: Angebot zum Kauf von XMR mit {0} erstellen createOffer.placeOfferButton.sell=Überprüfen: Angebot zum Verkauf von XMR für {0} erstellen createOffer.createOfferFundWalletInfo.headline=Ihr Angebot finanzieren # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Handelsbetrag: {0} \n createOffer.createOfferFundWalletInfo.msg=Sie müssen {0} in dieses Angebot einzahlen.\n\n\ Diese Gelder werden in Ihrer lokalen Wallet reserviert und in eine Multisig-Wallet gesperrt, sobald jemand Ihr Angebot annimmt.\n\n\ Der Betrag ist die Summe aus:\n\ {1}\ - Ihre Sicherheitskaution: {2}\n\ - Handelsgebühr: {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=Es gab einen Fehler beim Erstellen des Angebots:\n\n{0}\n\nEs haben noch keine Gelder Ihre Wallet verlassen.\nBitte starten Sie Ihre Anwendung neu und überprüfen Sie Ihre Netzwerkverbindung. createOffer.setAmountPrice=Betrag und Preis festlegen createOffer.warnCancelOffer=Sie haben bereits dieses Angebot finanziert. Wenn Sie jetzt abbrechen, verbleiben Ihre Gelder in Ihrer örtlichen Haveno-Wallet und können auf dem Bildschirm "Gelder/Gelder senden" abgehoben werden. Sind Sie sicher, dass Sie den Vorgang abbrechen möchten? createOffer.timeoutAtPublishing=Beim Veröffentlichen des Angebots ist eine Zeitüberschreitung aufgetreten. createOffer.errorInfo=\n\nDie Erstellungsgebühr wurde schon gezahlt. Im schlimmsten Fall haben Sie diese Gebühr verloren.\nVersuchen Sie bitte die Anwendung neu zu starten und überprüfen Sie Ihre Netzwerkverbindung um zu sehen, ob Sie das Problem beheben können. createOffer.tooLowSecDeposit.warning=Sie haben die Kaution auf einen niedrigeren Wert als den empfohlenen Standardwert von {0} gesetzt.\nSind Sie sicher, dass Sie eine niedrigere Kaution nutzen wollen? createOffer.tooLowSecDeposit.makerIsSeller=Dies gibt Ihnen weniger Schutz, sollte der Handelspartner nicht dem Handelsprotokoll folgen. createOffer.tooLowSecDeposit.makerIsBuyer=Es gibt Ihrem Handelspartner weniger Schutz, dass Sie dem Handelsprotokoll folgen, da Sie so weniger Kaution riskieren. Andere Nutzer könnten andere Angebote Ihrem vorziehen. createOffer.resetToDefault=Nein, den Standardwert wiederherstellen createOffer.useLowerValue=Ja, meinen niedrigeren Wert nutzen createOffer.priceOutSideOfDeviation=Der eingegebene Preis liegt außerhalb der maximal zulässigen Abweichung vom Marktpreis.\nDie maximale Abweichung ist {0} und kann in den Voreinstellungen angepasst werden. createOffer.changePrice=Preis ändern createOffer.tac=Mit der Erstellung dieses Angebots stimme ich zu, mit jedem Händler zu handeln, der die oben festgelegten Bedingungen erfüllt. createOffer.currencyForFee=Handelsgebühr createOffer.setDeposit=Kaution des Käufers festlegen (%) createOffer.setDepositAsBuyer=Meine Kaution als Käufer festlegen (%) createOffer.setDepositForBothTraders=Legen Sie die Kaution für beide Handelspartner fest (%) createOffer.securityDepositInfo=Die Kaution ihres Käufers wird {0} createOffer.securityDepositInfoAsBuyer=Ihre Kaution als Käufer wird {0} createOffer.minSecurityDepositUsed=Der Mindest-Sicherheitsbetrag wird verwendet. createOffer.buyerAsTakerWithoutDeposit=Kein Deposit erforderlich vom Käufer (Passphrase geschützt) createOffer.myDeposit=Meine Sicherheitsleistung (%) createOffer.myDepositInfo=Ihre Sicherheitsleistung beträgt {0} #################################################################### # Offerbook / Take offer #################################################################### takeOffer.amount.prompt=Betrag in XMR eingeben takeOffer.amountPriceBox.buy.amountDescription=Betrag in XMR zu verkaufen takeOffer.amountPriceBox.sell.amountDescription=Betrag in XMR zu kaufen takeOffer.amountPriceBox.priceDescription=Preis pro Monero in {0} takeOffer.amountPriceBox.amountRangeDescription=Mögliche Betragsspanne takeOffer.amountPriceBox.warning.invalidXmrDecimalPlaces=Der eingegebene Betrag besitzt zu viele Nachkommastellen.\nDer Betrag wurde auf 4 Nachkommastellen angepasst. takeOffer.validation.amountSmallerThanMinAmount=Der Betrag kann nicht kleiner als der im Angebot festgelegte minimale Betrag sein. takeOffer.validation.amountLargerThanOfferAmount=Der eingegebene Betrag kann nicht größer als der im Angebot festgelegte Betrag sein. takeOffer.validation.amountLargerThanOfferAmountMinusFee=Der eingegebene Betrag würde Staub als Wechselgeld für den XMR-Verkäufer erzeugen. takeOffer.fundsBox.title=Ihren Handel finanzieren takeOffer.fundsBox.isOfferAvailable=Verfügbarkeit des Angebots wird überprüft ... takeOffer.fundsBox.tradeAmount=Zu verkaufender Betrag takeOffer.fundsBox.offerFee=Handelsgebühr takeOffer.fundsBox.networkFee=Gesamte Mining-Gebühr takeOffer.fundsBox.takeOfferSpinnerInfo=Angebot annehmen: {0} takeOffer.fundsBox.paymentLabel=Haveno-Handel mit der ID {0} takeOffer.fundsBox.fundsStructure=({0} Kaution, {1} Handelsgebühr, {2} Mining-Gebühr) takeOffer.fundsBox.noFundingRequiredTitle=Keine Finanzierung erforderlich takeOffer.fundsBox.noFundingRequiredDescription=Holen Sie sich das Angebots-Passwort vom Verkäufer außerhalb von Haveno, um dieses Angebot anzunehmen. takeOffer.success.headline=Sie haben erfolgreich ein Angebot angenommen. takeOffer.success.info=Sie können den Status Ihres Trades unter \"Portfolio/Offene Trades\" einsehen. takeOffer.error.message=Bei der Angebotsannahme trat ein Fehler auf.\n\n{0} # new entries takeOffer.takeOfferButton.buy=Überprüfen: Angebot zum Kauf von XMR mit {0} annehmen takeOffer.takeOfferButton.sell=Überprüfen: Angebot zum Verkauf von XMR für {0} annehmen takeOffer.noPriceFeedAvailable=Sie können dieses Angebot nicht annehmen, da es auf einem Prozentsatz vom Marktpreis basiert, jedoch keiner verfügbar ist. takeOffer.takeOfferFundWalletInfo.headline=Ihren Handel finanzieren # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Handelsbetrag: {0}\n takeOffer.takeOfferFundWalletInfo.msg=Sie müssen zum Annehmen dieses Angebots {0} einzahlen.\n\nDer Betrag ist die Summe aus:\n{1}- Ihre Kaution: {2}\n- Handelsgebühr: {3}\n\nSie haben zwei Möglichkeiten Ihren Handel zu finanzieren:\n- Nutzen Sie Ihre Haveno-Wallet (bequem, aber Transaktionen können nach verfolgbar sein) ODER\n- Von einer externen Wallet überweisen (möglicherweise vertraulicher)\n\nSie werden nach dem Schließen dieses Dialogs alle Finanzierungsmöglichkeiten und Details sehen. takeOffer.alreadyPaidInFunds=Wenn Sie bereits Gelder gezahlt haben, können Sie diese unter \"Gelder/Gelder senden\" abheben. takeOffer.paymentInfo=Zahlungsinformationen takeOffer.setAmountPrice=Betrag festlegen takeOffer.alreadyFunded.askCancel=Sie haben bereits dieses Angebot finanziert. Wenn Sie jetzt abbrechen, verbleiben Ihre Gelder in Ihrer örtlichen Haveno-Wallet und können auf dem Bildschirm "Gelder/Gelder senden" abgehoben werden. Sind Sie sicher, dass Sie den Vorgang abbrechen möchten? takeOffer.failed.offerNotAvailable=Die Annahme des Angebots ist fehlgeschlagen, da das Angebot nicht mehr verfügbar ist. Möglicherweise hat zwischenzeitlich ein anderer Händler das Angebot angenommen. takeOffer.failed.offerTaken=Sie können dieses Angebot nicht annehmen, da es bereits von einem anderen Händler angenommen wurde. takeOffer.failed.offerRemoved=Sie können dieses Angebot nicht annehmen, da es inzwischen entfernt wurde. takeOffer.failed.offererNotOnline=Die Angebotsannahme ist fehlgeschlagen, da der Ersteller nicht mehr online ist. takeOffer.failed.offererOffline=Sie können das Angebot nicht annehmen, da der Ersteller offline ist. takeOffer.warning.connectionToPeerLost=Sie haben die Verbindung zum Ersteller verloren.\nEr ist möglicherweise offline gegangen oder hat die Verbindung zu Ihnen wegen zu vieler offener Verbindungen geschlossen.\n\nFalls Sie das Angebot noch im Angebotsbuch sehen, können Sie versuchen das Angebot erneut anzunehmen. takeOffer.error.noFundsLost=\n\nEs haben noch keine Gelder Ihre Wallet verlassen.\nVersuchen Sie bitte Ihre Anwendung neu zu starten und überprüfen Sie Ihre Netzwerkverbindung, um zu sehen ob Sie das Problem beheben können. # suppress inspection "TrailingSpacesInProperty" takeOffer.error.feePaid=.\n\n takeOffer.error.depositPublished=\n\nDie Kautionstransaktion wurde schon veröffentlicht.\nVersuchen Sie bitte Ihre Anwendung neu zu starten und überprüfen Sie Ihre Netzwerkverbindung, um zu sehen ob Sie das Problem beheben können.\nWenn das Problem weiter besteht, kontaktieren Sie bitte die Entwickler für Support. takeOffer.error.payoutPublished=\n\nDie Auszahlungstransaktion wurde schon veröffentlicht.\nVersuchen Sie bitte Ihre Anwendung neu zu starten und überprüfen Sie Ihre Netzwerkverbindung, um zu sehen ob Sie das Problem beheben können.\nWenn das Problem weiter besteht, kontaktieren Sie bitte die Entwickler für Support. takeOffer.tac=Mit der Annahme dieses Angebots stimme ich den oben festgelegten Handelsbedingungen zu. #################################################################### # Offerbook / Edit offer #################################################################### openOffer.header.triggerPrice=Triggerpreis openOffer.triggerPrice=Auslösepreis {0} openOffer.triggered=Das Angebot wurde deaktiviert, weil der Marktpreis Ihren Auslösepreis erreicht hat.\nBitte bearbeiten Sie das Angebot, um einen neuen Auslösepreis festzulegen. editOffer.setPrice=Preis festlegen editOffer.confirmEdit=Bestätigen: Angebot bearbeiten editOffer.publishOffer=Ihr Angebot wird veröffentlicht. editOffer.failed=Bearbeiten des Angebots fehlgeschlagen:\n{0} editOffer.success=Ihr Angebot wurde erfolgreich bearbeitet. editOffer.invalidDeposit=Die Kaution des Käufers ist nicht in den, vom Haveno DAO definierten, Beschränkungen und können nicht mehr geändert werden. #################################################################### # Portfolio #################################################################### portfolio.tab.openOffers=Meine offenen Angebote portfolio.tab.pendingTrades=Offene Trades portfolio.tab.history=Verlauf portfolio.tab.failed=Fehlgeschlagen portfolio.tab.editOpenOffer=Angebot bearbeiten portfolio.closedTrades.deviation.help=Prozentuale Preisabweichung vom Markt portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} portfolio.pending.syncing=Handelwallet wird synchronisiert portfolio.pending.syncing.blockRemaining=Handelwallet wird synchronisiert — 1 Block verbleibend portfolio.pending.syncing.blocksRemaining=Handelwallet wird synchronisiert — {0} Blöcke verbleibend portfolio.pending.step1.waitForConf=Auf Blockchain-Bestätigung warten portfolio.pending.step2_buyer.additionalConf=Einzahlungen haben 10 Bestätigungen erreicht.\nFür zusätzliche Sicherheit empfehlen wir, {0} Bestätigungen abzuwarten, bevor Sie die Zahlung senden.\nEin früheres Vorgehen erfolgt auf eigenes Risiko. portfolio.pending.step2_buyer.startPayment=Zahlung beginnen portfolio.pending.step2_seller.waitPaymentSent=Auf Zahlungsbeginn warten portfolio.pending.step3_buyer.waitPaymentArrived=Auf Zahlungseingang warten portfolio.pending.step3_seller.confirmPaymentReceived=Zahlungseingang bestätigen portfolio.pending.step5.completed=Abgeschlossen portfolio.pending.step3_seller.autoConf.status.label=Status der automatischen Bestätigung portfolio.pending.autoConf=Automatisch bestätigt portfolio.pending.autoConf.blocks=XMR Bestätigungen: {0} / Benötigt: {1} portfolio.pending.autoConf.state.xmr.txKeyReused=Transaktionsschlüssel wiederverwendet. Bitte eröffnen Sie einen Konflikt / eine Auseinandersetzung. portfolio.pending.autoConf.state.confirmations=XMR Bestätigungen: {0}/{1} portfolio.pending.autoConf.state.txNotFound=Transaktion ist noch nicht im Mem-Pool sichtbar portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=ID / Schlüssel der Transaktion (noch) nicht validiert portfolio.pending.autoConf.state.filterDisabledFeature=Von Entwicklern deaktiviert. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FEATURE_DISABLED=Automatische Bestätigung ist deaktiviert. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=Handelsbetrag überschreitet das Limit für die automatische Bestätigung # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.INVALID_DATA=Handelspartner hat ungültige Daten angegeben. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=Die Auszahlungstransaktion wurde bereits durchgeführt. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.DISPUTE_OPENED=Ein Konflikt wurde eröffnet. Die automatische Bestätigung ist für diesen Handel deaktiviert. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.REQUESTS_STARTED=Anfrage zum Nachweis der Transaktion gestartet # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PENDING=Fortschrittsergebnisse: {0}/{1}; {2} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.COMPLETED=Überprüfung aller Stationen war erfolgreich # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.ERROR=An einer Service-Abfrage ist ein Fehler aufgetreten. Eine Automatische Bestätigung ist nicht mehr möglich. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=Eine Service-Abfrage ist ausgefallen. Eine Automatische Bestätigung ist nicht mehr möglich. portfolio.pending.step1.info.you=Die Einzahlungstransaktion wurde veröffentlicht.\nSie können die Zahlung nach 10 Bestätigungen beginnen (ca. {0} Minuten verbleibend). portfolio.pending.step1.info.buyer=Die Einzahlungstransaktion wurde veröffentlicht.\nDer XMR-Käufer kann die Zahlung nach 10 Bestätigungen beginnen (ca. {0} Minuten verbleibend). portfolio.pending.step1.warn=Die Kautionstransaktion ist noch nicht bestätigt. Dies geschieht manchmal in seltenen Fällen, wenn die Finanzierungsgebühr aus der externen Wallet eines Traders zu niedrig war. portfolio.pending.step1.openForDispute=Die Kautionstransaktion ist noch nicht bestätigt. Sie können länger warten oder den Vermittler um Hilfe bitten. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2.confReached=Ihr Handel hat mindestens eine Blockchain-Bestätigung erreicht.\n\n portfolio.pending.step2_buyer.refTextWarn=Wichtig: Wenn Sie die Zahlung durchführen, lassen Sie das Feld \"Verwendungszweck\" leer. Geben Sie NICHT die Handels-ID oder einen anderen Text wie 'Monero', 'XMR' oder 'Haveno' an. Sie können im Handels-Chat gerne besprechen ob ein alternativer \"Verwendungszweck\" für Sie beide zweckmäßig wäre. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees=Sollte Ihre Bank irgendwelche Gebühren für die Überweisung erheben, müssen Sie diese Gebühren bezahlen. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.crypto=Bitte überweisen Sie von Ihrer externen {0}-Wallet\n{1} an den XMR-Verkäufer.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.cash=Bitte gehen Sie zu einer Bank und zahlen Sie {0} an den XMR-Verkäufer.\n\n portfolio.pending.step2_buyer.cash.extra=WICHTIGE VORAUSSETZUNG:\nNachdem Sie die Zahlung getätigt haben, schreiben Sie auf die Quittung: NO REFUNDS.\nReißen Sie diese in zwei Teile und machen Sie ein Foto, das Sie an die E-Mail-Adresse des XMR-Verkäufers senden. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.moneyGram=Bitte zahlen Sie {0} an den XMR-Verkäufer mit MoneyGram.\n\n portfolio.pending.step2_buyer.moneyGram.extra=WICHTIGE VORAUSSETZUNG: \nNachdem Sie die Zahlung getätigt haben, senden Sie die Authorisierungs-Nummer und ein Foto der Quittung per E-Mail an den XMR-Verkäufer.\nDie Quittung muss den vollständigen Namen, das Land, Bundesland des Verkäufers und den Betrag deutlich zeigen. Die E-Mail-Adresse des Verkäufers lautet: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.westernUnion=Bitte zahlen Sie {0} an den XMR-Verkäufer mit Western Union.\n\n portfolio.pending.step2_buyer.westernUnion.extra=WICHTIGE VORAUSSETZUNG: \nNachdem Sie die Zahlung getätigt haben, senden Sie die MTCN (Tracking-Nummer) und ein Foto der Quittung per E-Mail an den XMR-Verkäufer.\nDie Quittung muss den vollständigen Namen, die Stadt, das Land des Verkäufers und den Betrag deutlich zeigen. Die E-Mail-Adresse des Verkäufers lautet: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Bitte senden Sie {0} per \"US Postal Money Order\" an den XMR-Verkäufer.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Bitte schicken Sie {0} Bargeld per Post an den XMR Verkäufer. Genaue Anweisungen finden Sie im Handelsvertrag, oder Sie stellen über den Handels-Chat Fragen, wenn etwas unklar ist. Weitere Informationen über \"Bargeld per Post\" finden Sie im Haveno-Wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Bitte zahlen Sie {0} mit der gewählten Zahlungsmethode an den XMR Verkäufer. Sie finden die Konto Details des Verkäufers im nächsten Fenster.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Bitte kontaktieren Sie den XMR-Verkäufer, mit den bereitgestellten Daten und organisieren Sie ein Treffen um {0} zu zahlen.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Zahlung per {0} beginnen portfolio.pending.step2_buyer.recipientsAccountData=Empfänger {0} portfolio.pending.step2_buyer.amountToTransfer=Zu überweisender Betrag portfolio.pending.step2_buyer.sellersAddress={0}-Adresse des Verkäufers portfolio.pending.step2_buyer.buyerAccount=Ihr zu verwendendes Zahlungskonto portfolio.pending.step2_buyer.paymentSent=Zahlung begonnen portfolio.pending.step2_buyer.showEarly=Zahlungsdetails frühzeitig anzeigen portfolio.pending.step2_buyer.warn=Sie haben Ihre {0} Zahlung noch nicht getätigt!\nBeachten Sie bitte, dass der Handel bis {1} abgeschlossen werden muss. portfolio.pending.step2_buyer.openForDispute=Sie haben Ihre Zahlung noch nicht abgeschlossen!\nDie maximale Frist für den Handel ist abgelaufen, bitte wenden Sie sich an den Vermittler, um Hilfe zu erhalten. portfolio.pending.step2_buyer.paperReceipt.headline=Haben Sie die Quittung an den XMR-Verkäufer gesendet? portfolio.pending.step2_buyer.paperReceipt.msg=Erinnerung:\nSie müssen folgendes auf die Quittung schreiben: NO REFUNDS.\nZerreißen Sie diese dann in zwei Teile und machen Sie ein Foto, das Sie an die E-Mail-Adresse des XMR-Verkäufers senden. portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Authorisierungs-Nummer und Quittung senden portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=Sie müssen die Authorisierungs-Nummer und ein Foto der Quittung per E-Mail an den XMR-Verkäufer senden.\nDie Quittung muss den vollständigen Namen, das Land, das Bundesland des Verkäufers und den Betrag deutlich zeigen. Die E-Mail-Adresse des Verkäufers lautet: {0}.\n\nHaben Sie die Authorisierungs-Nummer und Vertragt an den Verkäufer gesendet? portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=MTCN und Quittung senden portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Sie müssen die MTCN (Tracking-Nummer) und ein Foto der Quittung per E-Mail an den XMR-Verkäufer senden.\nDie Quittung muss den vollständigen Namen, die Stadt, das Land des Verkäufers und den Betrag deutlich zeigen. Die E-Mail-Adresse des Verkäufers lautet: {0}.\n\nHaben Sie die MTCN und Vertragt an den Verkäufer gesendet? portfolio.pending.step2_buyer.halCashInfo.headline=HalCash Code senden portfolio.pending.step2_buyer.halCashInfo.msg=Sie müssen eine SMS mit dem HalCash-Code sowie der Trade-ID ({0}) an den XMR-Verkäufer senden.\nDie Handynummer des Verkäufers lautet {1}.\n\nHaben Sie den Code an den Verkäufer gesendet? portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Einige Banken könnten den Namen des Empfängers überprüfen. Faster Payments Konten, die in alten Haveno-Clients angelegt wurden, geben den Namen des Empfängers nicht an, also benutzen Sie bitte den Trade-Chat, um ihn zu erhalten (falls erforderlich). portfolio.pending.step2_buyer.confirmStart.headline=Bestätigen Sie, dass Sie die Zahlung begonnen haben portfolio.pending.step2_buyer.confirmStart.msg=Haben Sie die {0}-Zahlung an Ihren Handelspartner begonnen? portfolio.pending.step2_buyer.confirmStart.yes=Ja, ich habe die Zahlung begonnen portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=Sie haben keinen Zahlungsnachweis eingereicht. portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=Sie haben die Transaktions-ID und den Transaktionsschlüssel nicht eingegeben.\n\nWenn Sie diese Informationen nicht zur Verfügung stellen, kann Ihr Handelspartner die automatische Bestätigung nicht nutzen, um die XMR freizugeben sobald die XMR erhalten wurden.\nAußerdem setzt Haveno voraus, dass der Sender der XMR Transaktion diese Informationen im Falle eines Konflikts dem Vermittler oder der Schiedsperson mitteilen kann.\nWeitere Informationen finden Sie im Haveno Wiki [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=Die Eingabe ist kein 32 byte Hexadezimalwert portfolio.pending.step2_buyer.confirmStart.warningButton=Ignorieren und fortfahren portfolio.pending.step2_seller.waitPayment.headline=Auf Zahlung warten portfolio.pending.step2_seller.f2fInfo.headline=Kontaktinformation des Käufers portfolio.pending.step2_seller.waitPayment.msg=Die Kautionstransaktion hat mindestens eine Blockchain-Bestätigung.\nSie müssen warten bis der XMR-Käufer die {0}-Zahlung beginnt. portfolio.pending.step2_seller.warn=Der XMR-Käufer hat die {0}-Zahlung noch nicht getätigt.\nSie müssen warten bis die Zahlung begonnen wurde.\nWenn der Handel nicht bis {1} abgeschlossen wurde, wird der Vermittler diesen untersuchen. portfolio.pending.step2_seller.openForDispute=Der XMR-Käufer hat seine Zahlung nicht begonnen!\nDie maximal zulässige Frist für den Handel ist abgelaufen.\nSie können länger warten und dem Handelspartner mehr Zeit geben oder den Vermittler um Hilfe bitten. tradeChat.chatWindowTitle=Chat-Fenster für Trade mit ID '{0}' tradeChat.openChat=Chat-Fenster öffnen tradeChat.rules=Sie können mit Ihrem Trade-Partner kommunizieren, um mögliche Probleme mit diesem Trade zu lösen.\nEs ist nicht zwingend erforderlich, im Chat zu antworten.\nWenn ein Trader gegen eine der folgenden Regeln verstößt, eröffnen Sie einen Streitfall und melden Sie ihn dem Mediator oder Vermittler.\n\nChat-Regeln:\n\t● Senden Sie keine Links (Risiko von Malware). Sie können die Transaktions-ID und den Namen eines Block-Explorers senden.\n\t● Senden Sie keine Seed-Wörter, Private Keys, Passwörter oder andere sensible Informationen!\n\t● Traden Sie nicht außerhalb von Haveno (keine Sicherheit).\n\t● Beteiligen Sie sich nicht an Betrugsversuchen in Form von Social Engineering.\n\t● Wenn ein Partner nicht antwortet und es vorzieht, nicht über den Chat zu kommunizieren, respektieren Sie seine Entscheidung.\n\t● Beschränken Sie Ihre Kommunikation auf das Traden. Dieser Chat ist kein Messenger-Ersatz oder eine Trollbox.\n\t● Bleiben Sie im Gespräch freundlich und respektvoll. # suppress inspection "UnusedProperty" message.state.UNDEFINED=Undefiniert # suppress inspection "UnusedProperty" message.state.SENT=Nachricht gesendet # suppress inspection "UnusedProperty" message.state.ARRIVED=Nachricht beim Peer angekommen # suppress inspection "UnusedProperty" message.state.STORED_IN_MAILBOX=Nachricht über die gesendete Zahlung wurde verschickt, aber vom Peer noch nicht erhalten # suppress inspection "UnusedProperty" message.state.ACKNOWLEDGED=Peer hat Nachrichtenerhalt bestätigt # suppress inspection "UnusedProperty" message.state.FAILED=Senden der Nachricht fehlgeschlagen portfolio.pending.step3_buyer.wait.headline=Auf Zahlungsbestätigung des XMR-Verkäufers warten portfolio.pending.step3_buyer.wait.info=Auf Bestätigung des XMR-Verkäufers zum Erhalt der {0}-Zahlung warten. portfolio.pending.step3_buyer.wait.msgStateInfo.label=Zahlungsbeginn-Nachricht-Status portfolio.pending.step3_buyer.warn.part1a=in der {0}-Blockchain portfolio.pending.step3_buyer.warn.part1b=bei Ihrem Zahlungsanbieter (z.B. Bank) portfolio.pending.step3_buyer.warn.part2=Der XMR-Verkäufer hat Ihre Zahlung noch nicht bestätigt. Bitte überprüfen Sie {0}, ob der Zahlungsvorgang erfolgreich war. portfolio.pending.step3_buyer.openForDispute=Der XMR-Verkäufer hat Ihre Zahlung nicht bestätigt! Die maximale Frist für den Handel ist abgelaufen. Sie können länger warten und dem Trading-Partner mehr Zeit geben oder den Vermittler um Hilfe bitten. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.part=Ihr Handelspartner hat bestätigt, die {0}-Zahlung begonnen zu haben.\n\n portfolio.pending.step3_seller.crypto.explorer=in ihrem bevorzugten {0} Blockchain Explorer portfolio.pending.step3_seller.crypto.wallet=in ihrer {0} Wallet portfolio.pending.step3_seller.crypto={0}Bitte überprüfen Sie mit Ihrem bevorzugten {1}-Blockchain-Explorer, ob die Transaktion zu Ihrer Empfangsadresse\n{2}\nschon genug Blockchain-Bestätigungen hat.\nDer Zahlungsbetrag muss {3} sein\n\nSie können Ihre {4}-Adresse vom Hauptbildschirm kopieren und woanders einfügen, nachdem dieser Dialog geschlossen wurde. portfolio.pending.step3_seller.postal={0}Bitte überprüfen Sie, ob Sie {1} per \"US Postal Money Order\" vom XMR-Käufer erhalten haben. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.payByMail={0}Bitte überprüfen Sie, ob Sie {1} als \"Bargeld per Post\" vom XMR-Käufer erhalten haben. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.bank=Ihr Handelspartner hat den Beginn der {0}-Zahlung bestätigt.\n\nBitte gehen Sie auf Ihre Online-Banking-Website und überprüfen Sie, ob Sie {1} vom XMR-Käufer erhalten haben. portfolio.pending.step3_seller.cash=Da die Zahlung per Cash Deposit ausgeführt wurde, muss der XMR-Käufer \"NO REFUND\" auf die Quittung schreiben, diese in 2 Teile reißen und Ihnen ein Foto per E-Mail schicken.\n\nUm die Gefahr einer Rückbuchung zu vermeiden bestätigen Sie nur, wenn Sie die E-Mail erhalten haben und Sie sicher sind, dass die Quittung gültig ist.\nWenn Sie nicht sicher sind, {0} portfolio.pending.step3_seller.moneyGram=Der Käufer muss Ihnen die Authorisierungs-Nummer und ein Foto der Quittung per E-Mail zusenden.\nDie Quittung muss deutlich Ihren vollständigen Namen, Ihr Land, Ihr Bundesland und den Betrag enthalten. Bitte überprüfen Sie Ihre E-Mail, wenn Sie die Authorisierungs-Nummer erhalten haben.\n\nNach dem Schließen dieses Pop-ups sehen Sie den Namen und die Adresse des XMR-Käufers, um das Geld von MoneyGram abzuholen.\n\nBestätigen Sie den Erhalt erst, nachdem Sie das Geld erfolgreich abgeholt haben! portfolio.pending.step3_seller.westernUnion=Der Käufer muss Ihnen die MTCN (Sendungsnummer) und ein Foto der Quittung per E-Mail zusenden.\nDie Quittung muss deutlich Ihren vollständigen Namen, Ihre Stadt, Ihr Land und den Betrag enthalten. Bitte überprüfen Sie Ihre E-Mail, wenn Sie die MTCN erhalten haben.\n\nNach dem Schließen dieses Pop-ups sehen Sie den Namen und die Adresse des XMR-Käufers, um das Geld von Western Union abzuholen.\n\nBestätigen Sie den Erhalt erst, nachdem Sie das Geld erfolgreich abgeholt haben! portfolio.pending.step3_seller.halCash=Der Käufer muss Ihnen den HalCash-Code als SMS zusenden. Außerdem erhalten Sie eine Nachricht von HalCash mit den erforderlichen Informationen, um EUR an einem HalCash-fähigen Geldautomaten abzuheben.\n\nNachdem Sie das Geld am Geldautomaten abgeholt haben, bestätigen Sie bitte hier den Zahlungseingang! portfolio.pending.step3_seller.amazonGiftCard=Der Käufer hat Ihnen eine Amazon eGift Geschenkkarte per E-Mail oder per Textnachricht auf Ihr Handy geschickt. Bitte lösen Sie die Amazon eGift Geschenkkarte jetzt in Ihrem Amazon-Konto ein und bestätigen Sie nach der erfolgreichen Annahme den Zahlungseingang. portfolio.pending.step3_seller.bankCheck=\n\nBitte überprüfen Sie auch, ob der Name des im Trading-Vertrag angegebenen Absenders mit dem Namen auf Ihrem Kontoauszug übereinstimmt:\nName des Absenders, pro Trade-Vertrag: {0}\n\nWenn die Namen nicht genau gleich sind, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=bestätigen Sie den Zahlungseingang nicht. Eröffnen Sie stattdessen einen Konflikt, indem Sie \"alt + o\" oder \"option + o\" drücken.\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=Zahlungserhalt bestätigen portfolio.pending.step3_seller.amountToReceive=Zu erhaltender Betrag portfolio.pending.step3_seller.yourAddress=Ihre {0}-Adresse portfolio.pending.step3_seller.buyersAddress={0}-Adresse des Käufers portfolio.pending.step3_seller.yourAccount=Ihr Handelskonto portfolio.pending.step3_seller.xmrTxHash=Transaktions-ID portfolio.pending.step3_seller.xmrTxKey=Transaktions-Schlüssel portfolio.pending.step3_seller.buyersAccount=Käufer Konto-Informationen portfolio.pending.step3_seller.confirmReceipt=Zahlungserhalt bestätigen portfolio.pending.step3_seller.buyerStartedPayment=Der XMR-Käufer hat die {0}-Zahlung begonnen.\n{1} portfolio.pending.step3_seller.buyerStartedPayment.crypto=Überprüfen Sie Ihre Crypto-Wallet oder Ihren Block-Explorer auf Blockchain-Bestätigungen und bestätigen Sie die Zahlung, wenn ausreichend viele Blockchain-Bestätigungen angezeigt werden. portfolio.pending.step3_seller.buyerStartedPayment.traditional=Prüfen Sie Ihr Handelskonto (z.B. Bankkonto) und bestätigen Sie, wenn Sie die Zahlung erhalten haben. portfolio.pending.step3_seller.warn.part1a=in der {0}-Blockchain portfolio.pending.step3_seller.warn.part1b=bei Ihrem Zahlungsanbieter (z.B. Bank) portfolio.pending.step3_seller.warn.part2=Sie haben den Eingang der Zahlung noch nicht bestätigt. Bitte überprüfen Sie {0} ob Sie die Zahlung erhalten haben. portfolio.pending.step3_seller.openForDispute=Sie haben den Eingang der Zahlung nicht bestätigt!\nDie maximale Frist für den Handel ist abgelaufen.\nBitte bestätigen Sie oder bitten Sie den Vermittler um Unterstützung. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=Ist die {0}-Zahlung Ihres Handelspartners eingegangen?\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.name=Bitte überprüfen Sie auch, ob der Name des im Trade-Vertrag angegebenen Absenders mit dem Namen auf Ihrem Kontoauszug übereinstimmt:\nName des Absenders, pro Trade-Vertrag: {0}\n\nWenn die Namen nicht genau gleich sind, bestätigen Sie den Zahlungseingang nicht. Eröffnen Sie stattdessen einen Konflikt, indem Sie \"alt + o\" oder \"option + o\" drücken.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Bitte beachten Sie, dass, sobald Sie den Erhalt bestätigt haben, der gesperrte Trade-Betrag an den XMR-Käufer freigegeben wird und die Kaution zurückerstattet wird.\n\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Bestätigen Sie, die Zahlung erhalten zu haben portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=Ja, ich habe die Zahlung erhalten portfolio.pending.step3_seller.onPaymentReceived.signer=WICHTIG: Mit der Bestätigung des Zahlungseingangs verifizieren Sie auch das Konto der Gegenpartei und unterzeichnen es entsprechend. Da das Konto der Gegenpartei noch nicht unterzeichnet ist, sollten Sie die Bestätigung der Zahlung so lange wie möglich hinauszögern, um das Risiko einer Rückbelastung zu reduzieren. portfolio.pending.step5_buyer.groupTitle=Zusammenfassung des abgeschlossenen Handels portfolio.pending.step5_buyer.tradeFee=Handelsgebühr portfolio.pending.step5_buyer.makersMiningFee=Mining-Gebühr portfolio.pending.step5_buyer.takersMiningFee=Gesamte Mining-Gebühr portfolio.pending.step5_buyer.refunded=Rückerstattete Kaution portfolio.pending.step5_buyer.withdrawXMR=Ihre Moneros abheben portfolio.pending.step5_buyer.amount=Abzuhebender Betrag portfolio.pending.step5_buyer.withdrawToAddress=An diese Adresse abheben portfolio.pending.step5_buyer.moveToHavenoWallet=Gelder in der Haveno Wallet aufbewahren portfolio.pending.step5_buyer.withdrawExternal=An externe Wallet abheben portfolio.pending.step5_buyer.alreadyWithdrawn=Ihre Gelder wurden bereits abgehoben.\nBitte überprüfen Sie den Transaktionsverlauf. portfolio.pending.step5_buyer.confirmWithdrawal=Anfrage zum Abheben bestätigen portfolio.pending.step5_buyer.amountTooLow=Der zu überweisende Betrag ist kleiner als die Transaktionsgebühr und der minimale Tx-Wert (Staub). portfolio.pending.step5_buyer.withdrawalCompleted.headline=Abheben abgeschlossen portfolio.pending.step5_buyer.withdrawalCompleted.msg=Ihre abgeschlossenen Trades sind unter \"Portfolio/Verlauf\" gespeichert.\nSie können all Ihre Monero-Transaktionen unter \"Gelder/Transaktionen\" einsehen portfolio.pending.step5_buyer.bought=Sie haben gekauft portfolio.pending.step5_buyer.paid=Sie haben gezahlt portfolio.pending.step5_seller.sold=Sie haben verkauft portfolio.pending.step5_seller.received=Sie haben erhalten tradeFeedbackWindow.title=Glückwunsch zum Abschluss ihres Handels. tradeFeedbackWindow.msg.part1=Wir würden gerne von Ihren Erfahrungen mit Haveno hören. Dies hilft uns die Software zu verbessern und etwaige Stolpersteine zu beseitigen. Um uns ihr Feedback mitzuteilen, füllen Sie bitte diese kurze Umfrage aus (keine Registrierung benötigt): tradeFeedbackWindow.msg.part2=Sollten Sie Fragen oder Probleme haben, kontaktieren Sie andere Nutzer und Mitwirkende im Haveno-Forum auf: tradeFeedbackWindow.msg.part3=Vielen Dank, dass Sie Haveno benutzen! portfolio.pending.role=Meine Rolle portfolio.pending.tradeInformation=Handelsinformationen portfolio.pending.remainingTime=Verbleibende Zeit portfolio.pending.remainingTimeDetail={0} (bis {1}) portfolio.pending.remainingTimeDetail.startsAfter=Beginnt nach {0} Bestätigungen portfolio.pending.tradePeriodInfo=Nach {0} Bestätigungen beginnt die Handelsperiode. Je nach verwendeter Zahlungsmethode gilt eine unterschiedliche maximal zulässige Handelsdauer. portfolio.pending.tradePeriodWarning=Wird die Dauer überschritten, können beide Händler einen Konflikt öffnen. portfolio.pending.tradeNotCompleted=Maximale Handelsdauer wurde überschritten (bis {0}) portfolio.pending.tradeProcess=Handelsprozess portfolio.pending.openAgainDispute.msg=Wenn Sie sich nicht sicher sind, ob die Nachricht an den Vermittler oder die Schiedsperson angekommen ist (z. B., wenn Sie nach einem Tag noch keine Antwort erhalten haben), können Sie mit Cmd/Strg+o einen weiteren Konfliktfall eröffnen. Sie können auch im Haveno Forum nach Hilfe fragen [HYPERLINK:https://haveno.community]. portfolio.pending.openAgainDispute.button=Konflikt erneut öffnen portfolio.pending.openSupportTicket.headline=Support-Ticket öffnen portfolio.pending.openSupportTicket.msg=Bitte verwenden Sie diese Funktion nur in Notfällen, wenn Sie keinen \"Open support\" oder \"Open dispute\" Button sehen.\n\nWenn Sie ein Support-Ticket öffnen, wird der Trade unterbrochen und von einem Mediator oder Vermittler bearbeitet. portfolio.pending.timeLockNotOver=Sie müssen ≈{0} ({1} weitere Blöcke) warten, bevor Sie einen Vermittlungskonflikt eröffnen können. portfolio.pending.error.depositTxNull=Die Einzahlungstransaktion ist null. Sie können einen Streitfall nicht ohne eine gültige Einzahlungstransaktion eröffnen. Bitte gehen Sie zu \"Einstellungen/Netzwerkinformationen\" und führen Sie eine SPV-Resynchronisierung durch.\n\nFür weitere Hilfe wenden Sie sich bitte an den Haveno-Support-Kanal des Haveno Keybase Teams. portfolio.pending.mediationResult.error.depositTxNull=Die Einzahlungstransaktion ist ungültig. Sie können den Handel zu den fehlgeschlagenen Händeln verschieben. portfolio.pending.mediationResult.error.delayedPayoutTxNull=Die verzögerte Auszahlungstransaktion ist ungültig. Sie können den Handel zu den fehlgeschlagenen Händeln verschieben. portfolio.pending.error.depositTxNotConfirmed=Die Einzahlungstransaktion ist nicht bestätigt. Sie können einen Streitfall nicht ohne eine bestätigte Einzahlungstransaktion eröffnen. Bitte warten Sie, bis diese bestätigt ist, oder gehen Sie zu \"Einstellungen/Netzwerkinformationen\" und führen Sie eine SPV-Resynchronisierung durch.\n\nFür weitere Hilfe wenden Sie sich bitte an den Haveno-Support-Kanal des Haveno Keybase Teams. portfolio.pending.support.headline.getHelp=Brauchen Sie Hilfe? portfolio.pending.support.text.getHelp=Wenn Sie irgendwelche Probleme haben, können Sie versuchen, den Trade-Partner im Trade-Chat zu kontaktieren oder die Haveno-Community unter https://haveno.community zu fragen. Wenn Ihr Problem immer noch nicht gelöst ist, können Sie weitere Hilfe von einem Mediator anfordern. portfolio.pending.support.button.getHelp=Trader Chat öffnen portfolio.pending.support.headline.halfPeriodOver=Zahlung überprüfen portfolio.pending.support.headline.periodOver=Die Handelsdauer ist abgelaufen portfolio.pending.support.headline.depositTxMissing=Fehlende Einzahlungstransaktion portfolio.pending.support.depositTxMissing=Für diesen Handel fehlt eine Einzahlungstransaktion. Öffnen Sie ein Support-Ticket, um einen Schlichter um Hilfe zu bitten. portfolio.pending.mediationRequested=Mediation beantragt portfolio.pending.refundRequested=Rückerstattung beantragt portfolio.pending.openSupport=Support-Ticket öffnen portfolio.pending.supportTicketOpened=Support-Ticket geöffnet portfolio.pending.communicateWithArbitrator=Bitte setzen Sie sich im \"Support\"-Bildschirm mit dem Vermittler in Verbindung. portfolio.pending.communicateWithMediator=Bitte kommunizieren Sie im \"Support\" Bildschirm mit dem Mediator. portfolio.pending.disputeOpenedByUser=Sie haben bereits einen Konflikt geöffnet.\n{0} portfolio.pending.disputeOpenedByPeer=Ihr Handelspartner hat einen Konflikt geöffnet\n{0} portfolio.pending.noReceiverAddressDefined=Keine Empfangsadresse festgelegt portfolio.pending.mediationResult.headline=Vorgeschlagene Auszahlung aus der Mediation portfolio.pending.mediationResult.info.noneAccepted=Schließen Sie den Trade ab, indem Sie den Vorschlag des Mediators für die Trade-Auszahlung annehmen. portfolio.pending.mediationResult.info.selfAccepted=Sie haben den Vorschlag des Mediators angenommen. Warten Sie darauf, dass auch der Partner akzeptiert. portfolio.pending.mediationResult.info.peerAccepted=Ihr Trade-Partner hat den Vorschlag des Mediators angenommen. Akzeptieren Sie ihn auch? portfolio.pending.mediationResult.button=Lösungsvorschlag ansehen portfolio.pending.mediationResult.popup.headline=Mediationsergebnis für Trade mit ID: {0} portfolio.pending.mediationResult.popup.headline.peerAccepted=Ihr Trade-Partner hat den Vorschlag des Mediators akzeptiert für Trade {0} portfolio.pending.mediationResult.popup.info=Der Vermittler hat folgende Auszahlung vorgeschlagen: \nSie erhalten: {0}\nIhr Handelspartner erhält: {1}\n\nSie können die vorgeschlagene Auszahlung akzeptieren oder ablehnen.\n\nAkzeptieren Sie, unterzeichnen Sie die vorgeschlagene Transaktion. Wenn Ihr Handelspartner auch akzeptiert und unterzeichnet, wird die Auszahlung getätigt und der Handel abgeschlossen.\n\nWenn einer oder beide den Vorschlag ablehnen, müssen Sie bis {2} (block {3}) warten, um eine zweite Konfliktrunde mit einer Schiedsperson zu starten, die den Handel erneut untersuchen wird und je nach eigenem Ergebnis eine Auszahlung veranlassen wird.\n\nDie Schiedsperson kann eine kleine Gebühr für ihre Arbeit berechnen (maximale Gebühr: Sicherheitskaution des Händlers). Im Idealfall akzeptieren beide Händler den Vorschlag des Vermittlers — eine Schiedsperson hinzuzuziehen ist nur für außergewöhnliche Fälle vorgesehen. Ein solcher Fall wäre, wenn ein Händler sich sicher ist, dass der Auszahlungsvorschlag nicht fair ist, oder der Handelspartner nicht antwortet.\n\nWeitere Informationen über das Schlichtungssystem finden Sie unter [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=Sie haben die vom Vermittler vorgeschlagene Auszahlung akzeptiert, aber es scheint so, als hätte Ihr Handelspartner sie noch nicht akzeptiert.\n\nSobald die Sperre bei {0} (block {1})) aufgehoben ist, können Sie eine zweite Runde des Konflikts eröffnen. Eine Schiedsperson wird dann den Konflikt erneut untersuchen und je nach eigenem Ergebnis eine Auszahlung veranlassen.\n\nHier können Sie mehr Informationen über das Schiedsverfahren finden:\n[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=Ablehnen und Vermittler hinzuziehen portfolio.pending.mediationResult.popup.alreadyAccepted=Sie haben bereits akzeptiert portfolio.pending.failedTrade.taker.missingTakerFeeTx=Die Transaktion der Abnehmer-Gebühr fehlt.\n\nOhne diese tx kann der Handel nicht abgeschlossen werden. Keine Gelder wurden gesperrt und keine Handelsgebühr wurde bezahlt. Sie können diesen Handel zu den fehlgeschlagenen Händeln verschieben. portfolio.pending.failedTrade.maker.missingTakerFeeTx=Die Transaktion der Abnehmer-Gebühr fehlt.\n\nOhne diese tx kann der Handel nicht abgeschlossen werden. Keine Gelder wurden gesperrt. Ihr Angebot ist für andere Händler weiterhin verfügbar. Sie haben die Ersteller-Gebühr also nicht verloren. Sie können diesen Handel zu den fehlgeschlagenen Händeln verschieben. portfolio.pending.failedTrade.missingDepositTx=Eine Einzahlungstransaktion fehlt.\n\nDiese Transaktion ist erforderlich, um den Handel abzuschließen. Bitte stellen Sie sicher, dass Ihre Wallet vollständig mit der Monero-Blockchain synchronisiert ist.\n\nSie können diesen Handel in den Bereich „Fehlgeschlagene Trades“ verschieben, um ihn zu deaktivieren. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Die verzögerte Auszahlungstransaktion fehlt, aber die Gelder wurden in der Einzahlungstransaktion gesperrt.\n\nBitte schicken Sie KEINE Geld-(Traditional-) oder Crypto-Zahlungen an den XMR Verkäufer, weil ohne die verzögerte Auszahlungstransaktion später kein Schlichtungsverfahren eröffnet werden kann. Stattdessen öffnen Sie ein Vermittlungs-Ticket mit Cmd/Strg+o. Der Vermittler sollte vorschlagen, dass beide Handelspartner ihre vollständige Sicherheitskaution zurückerstattet bekommen (und der Verkäufer auch seinen Handels-Betrag). Durch diese Vorgehensweise entsteht kein Sicherheitsrisiko und es geht ausschließlich die Handelsgebühr verloren.\n\nSie können eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier erbitten: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Die verzögerte Auszahlungstransaktion fehlt, aber die Gelder wurden in der Einzahlungstransaktion gesperrt.\n\nWenn dem Käufer die verzögerte Auszahlungstransaktion auch fehlt, wird er dazu aufgefordert die Bezahlung NICHT zu schicken und stattdessen ein Vermittlungs-Ticket zu eröffnen. Sie sollten auch ein Vermittlungs-Ticket mit Cmd/Strg+o öffnen.\n\nWenn der Käufer die Zahlung noch nicht geschickt hat, sollte der Vermittler vorschlagen, dass beide Handelspartner ihre Sicherheitskaution vollständig zurückerhalten (und der Verkäufer auch den Handels-Betrag). Anderenfalls sollte der Handels-Betrag an den Käufer gehen.\n\nSie können eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier erbitten: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=Während der Ausführung des Handel-Protokolls ist ein Fehler aufgetreten.\n\nFehler: {0}\n\nEs kann sein, dass dieser Fehler nicht gravierend ist und der Handel ganz normal abgeschlossen werden kann. Wenn Sie sich unsicher sind, öffnen Sie ein Vermittlungs-Ticket um den Rat eines Haveno Vermittlers zu erhalten.\n\nWenn der Fehler gravierend war, kann der Handel nicht abgeschlossen werden und Sie haben vielleicht die Handelsgebühr verloren. Sie können eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier erbitten: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.missingContract=Der Handelsvertrag ist nicht festgelegt.\n\nDer Handel kann nicht abgeschlossen werden und Sie haben möglicherweise die Handelsgebühr verloren. Sollte das der Fall sein, können Sie eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier beantragen: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=Das Handels-Protokoll hat ein paar Probleme gefunden.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=Das Handels-Protokoll hat ein schwerwiegendes Problem gefunden.\n\n{0}\n\nWollen Sie den Handel zu den fehlgeschlagenen Händeln verschieben?\n\nSie können keine Vermittlungs- oder Schlichtungsverfahren auf der Seite für fehlgeschlagene Händel eröffnen, aber Sie können einen fehlgeschlagene Handel wieder auf die Seite der offenen Händeln zurück verschieben. portfolio.pending.failedTrade.txChainValid.moveToFailed=Das Handels-Protokoll hat ein paar Probleme gefunden.\n\n{0}\n\nDie Transaktionen des Handels wurden veröffentlicht und die Gelder sind gesperrt. Verschieben Sie den Handel nur dann zu den fehlgeschlagenen Händeln, wenn Sie sich wirklich sicher sind. Dies könnte Optionen zur Behebung des Problems verhindern.\n\nWollen Sie den Handel zu den fehlgeschlagenen Händeln verschieben?\n\nSie können keine Vermittlungs- oder Schlichtungsverfahren auf der Seite für fehlgeschlagene Händel eröffnen, aber Sie können einen fehlgeschlagene Handel wieder auf die Seite der offenen Händeln zurück verschieben. portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Handel zu den fehlgeschlagenen Händeln verschieben. portfolio.pending.failedTrade.warningIcon.tooltip=Klicken Sie hier um herauszufinden welche Probleme beim Handel aufgetreten sind. portfolio.failed.revertToPending.popup=Wollen Sie diesen Handel zu den offenen Händeln verschieben? portfolio.failed.revertToPending=Handel zu den offenen Händeln verschieben portfolio.closed.completed=Abgeschlossen portfolio.closed.ticketClosed=Vermittelt portfolio.closed.mediationTicketClosed=Mediiert portfolio.closed.canceled=Abgebrochen portfolio.failed.Failed=Fehlgeschlagen portfolio.failed.unfail=Bevor Sie fortfahren, stellen Sie sicher, dass Sie ein Backup Ihres Datenverzeichnisses haben!\nWollen Sie diesen Trade wieder in offene Trades verschieben?\nDies ist eine Möglichkeit, Gelder freizugeben, die in einem gescheiterten Trade stecken geblieben sind. portfolio.failed.cantUnfail=Dieser Trade kann im Moment nicht wieder in offene Trades verschoben werden. \nVersuchen Sie es nach Abschluss des/der Trades erneut {0} portfolio.failed.depositTxNull=Der Trade kann nicht als offener Trade zurückgeändert werden. Einzahlungstransaktion ist ungültig. portfolio.failed.delayedPayoutTxNull=Der Trade kann nicht als offener Trade zurückgeändert werden. Verzögerte Auszahlungstransaktion ist ungültig. #################################################################### # Funds #################################################################### funds.tab.deposit=Gelder erhalten funds.tab.withdrawal=Gelder senden funds.tab.reserved=Reservierte Gelder funds.tab.locked=Gesperrte Gelder funds.tab.transactions=Transaktionen funds.deposit.unused=Ungenutzt funds.deposit.usedInTx=In {0} Transaktion(en) genutzt funds.deposit.fundHavenoWallet=Haveno-Wallet finanzieren funds.deposit.noAddresses=Es wurden noch keine Kautionsadressen generiert funds.deposit.fundWallet=Ihre Wallet finanzieren funds.deposit.withdrawFromWallet=Gelder aus Wallet übertragen funds.deposit.amount=Betrag in XMR (optional) funds.deposit.generateAddress=Neue Adresse generieren funds.deposit.generateAddressSegwit=Native segwit Format (Bech32) funds.deposit.selectUnused=Bitte wählen Sie eine ungenutzte Adresse aus der Tabelle oben, anstatt eine neue zu generieren. funds.withdrawal.arbitrationFee=Vermittlergebühr funds.withdrawal.inputs=Eingaben Auswahl funds.withdrawal.useAllInputs=Alle verfügbaren Eingaben benutzen funds.withdrawal.useCustomInputs=Spezifische Eingaben benutzen funds.withdrawal.receiverAmount=Empfängers Betrag funds.withdrawal.senderAmount=Senders Betrag funds.withdrawal.feeExcluded=Betrag ohne Mining-Gebühr funds.withdrawal.feeIncluded=Betrag beinhaltet Mining-Gebühr funds.withdrawal.fromLabel=Von Adresse abheben funds.withdrawal.toLabel=An diese Adresse abheben funds.withdrawal.memoLabel=Notiz für Abhebung funds.withdrawal.memo=Optional ausgefüllte Notiz funds.withdrawal.withdrawButton=Auswahl abheben funds.withdrawal.noFundsAvailable=Keine Gelder zum Abheben verfügbar funds.withdrawal.confirmWithdrawalRequest=Anfrage zum Abheben bestätigen funds.withdrawal.withdrawMultipleAddresses=Von mehreren Adressen abheben ({0}) funds.withdrawal.withdrawMultipleAddresses.tooltip=Von mehreren Adressen abheben:\n{0} funds.withdrawal.notEnoughFunds=Sie haben nicht genug Geld in Ihrer Wallet. funds.withdrawal.selectAddress=Wählen Sie eine Quelladresse aus der Tabelle funds.withdrawal.setAmount=Legen Sie den abzuhebenen Betrag fest funds.withdrawal.fillDestAddress=Geben Sie Ihre Zieladresse an funds.withdrawal.warn.noSourceAddressSelected=Sie müssen eine Quelladresse aus der Tabelle oben wählen. funds.withdrawal.warn.amountExceeds=Ihre Gelder in den ausgewählten Adressen reichen nicht aus.\nWählen Sie mehrere Adressen aus der Tabelle oben, oder ändern Sie die Gebühren-Schalter, um die Mining-Gebühr zu beinhalten. funds.reserved.noFunds=Es sind keine Gelder in offenen Angeboten reserviert funds.reserved.reserved=In lokaler Wallet für das Angebot mit dieser ID reserviert: {0} funds.locked.noFunds=Es sind keine Gelder in Trades gesperrt funds.locked.locked=Für den Handel mit dieser ID in MultiSig eingesperrt: {0} funds.tx.direction.sentTo=Gesendet nach: funds.tx.direction.receivedWith=Erhalten mit: funds.tx.direction.genesisTx=Aus Ursprungs-Tx: funds.tx.createOfferFee=Ersteller- und Tx-Gebühr: {0} funds.tx.takeOfferFee=Abnehmer- und Tx-Gebühr: {0} funds.tx.multiSigDeposit=MultiSig-Kaution: {0} funds.tx.multiSigPayout=MultiSig-Auszahlung: {0} funds.tx.disputePayout=Konfliktauszahlung: {0} funds.tx.disputeLost=Verlorener Konflikt: {0} funds.tx.collateralForRefund=Sicherheiten für die Rückerstattung: {0} funds.tx.timeLockedPayoutTx=Zeitgesperrte Auszahlung tx: {0} funds.tx.refund=Erstattung aus dem Vermittlungsverfahren: {0} funds.tx.unknown=Unbekannter Grund: {0} funds.tx.noFundsFromDispute=Keine Rückzahlung vom Konflikt funds.tx.receivedFunds=Gelder erhalten funds.tx.withdrawnFromWallet=Von Wallet abgehoben funds.tx.memo=Notiz funds.tx.noTxAvailable=Keine Transaktionen verfügbar funds.tx.revert=Umkehren funds.tx.txSent=Transaktion erfolgreich zu einer neuen Adresse in der lokalen Haveno-Wallet gesendet. funds.tx.direction.self=An Sie selbst senden funds.tx.dustAttackTx=Staub erhalten funds.tx.dustAttackTx.popup=Diese Transaktion sendet einen sehr kleinen XMR Betrag an Ihre Wallet und kann von Chainanalyse Unternehmen genutzt werden um ihre Wallet zu spionieren.\n\nWenn Sie den Transaktionsausgabe in einer Ausgabe nutzen, wird es lernen, dass Sie wahrscheinlich auch Besitzer der anderen Adressen sind (coin merge),\n\nUm Ihre Privatsphäre zu schützen, wir die Havenowallet Staubausgaben für Ausgaben und bei der Anzeige der Guthabens ignorieren. Sie können den Grenzwert, ab wann ein Wert als Staub angesehen wird in den Einstellungen ändern. #################################################################### # Support #################################################################### support.tab.mediation.support=Mediation support.tab.arbitration.support=Vermittlung support.tab.legacyArbitration.support=Legacy-Vermittlung support.tab.ArbitratorsSupportTickets={0} Tickets support.sigCheck.button=Signatur überprüfen support.sigCheck.popup.info=Fügen Sie die Zusammenfassungsnachricht des Schiedsverfahrens ein. Mit diesem Tool kann jeder Benutzer überprüfen, ob die Unterschrift des Schiedsrichters mit der Zusammenfassungsnachricht übereinstimmt. support.sigCheck.popup.header=Signatur des Konfliktergebnisses überprüfen support.sigCheck.popup.msg.label=Zusammenfassende Nachricht support.sigCheck.popup.msg.prompt=Zusammenfassende Angaben aus dem Konflikt kopieren und einfügen support.sigCheck.popup.result=Ergebnis der Überprüfung support.sigCheck.popup.success=Unterzeichnung ist gültig support.sigCheck.popup.failed=Unterzeichnung der Signatur ist fehlgeschlagen support.sigCheck.popup.invalidFormat=Nachricht ist nicht im erwarteten Format. Copy & paste Konflikt-Zusammenfassung. support.reOpenByTrader.prompt=Sind Sie sicher, dass Sie den Konflikt wiedereröffnen möchten? support.reOpenButton.label=Wiedereröffnen support.sendNotificationButton.label=Private Benachrichtigung support.reportButton.label=Melden support.fullReportButton.label=Alle Konflikte support.noTickets=Keine offenen Tickets vorhanden support.sendingMessage=Nachricht wird gesendet... support.receiverNotOnline=Empfänger ist nicht online. Nachricht wird in der Mailbox gespeichert. support.sendMessageError=Senden der Nachricht fehlgeschlagen. Fehler: {0} support.receiverNotKnown=Empfänger unbekannt support.wrongVersion=Das Angebot im Konflikt wurde mit einer älteren Haveno-Version erstellt.\nSie können den Konflikt nicht mir Ihrer Version der Anwendung schließen.\n\nNutzen Sie bitte eine ältere Version mit der Protokollversion {0} support.openFile=Anzufügende Datei öffnen (max. Dateigröße: {0} kb) support.attachmentTooLarge=Die Gesamtgröße Ihres Anhangs ist {0} kb und überschreitet die maximal erlaubte Nachrichtengröße von {1} kB. support.maxSize=Die maximal erlaubte Dateigröße ist {0} kB. support.attachment=Anhang support.tooManyAttachments=Sie können nicht mehr als 3 Anhänge mit einer Nachricht senden. support.save=Datei auf Festplatte speichern support.messages=Nachrichten support.input.prompt=Nachricht eingeben... support.send=Senden support.addAttachments=Anhang anfügen support.closeTicket=Ticket schließen support.attachments=Anhänge: support.savedInMailbox=Die Nachricht wurde im Postfach des Empfängers gespeichert support.arrived=Die Nachricht ist beim Empfänger angekommen support.acknowledged=Nachrichtenankunft vom Empfänger bestätigt support.error=Empfänger konnte die Nachricht nicht verarbeiten. Fehler: {0} support.buyerAddress=XMR-Adresse des Käufers support.sellerAddress=XMR-Adresse des Verkäufers support.role=Rolle support.agent=Support-Mitarbeiter support.state=Status support.chat=Chat support.preparing=In Vorbereitung support.requested=Angefragt support.closed=Geschlossen support.open=Offen support.process=Process support.buyerMaker=XMR-Käufer/Ersteller support.sellerMaker=XMR-Verkäufer/Ersteller support.buyerTaker=XMR-Käufer/Abnehmer support.sellerTaker=XMR-Verkäufer/Abnehmer support.backgroundInfo=Haveno ist kein Unternehmen, daher behandelt es Konflikte unterschiedlich.\n\nTrader können innerhalb der Anwendung über einen sicheren Chat auf dem Bildschirm für offene Trades kommunizieren, um zu versuchen, Konflikte selbst zu lösen. Wenn das nicht ausreicht, kann ein Mediator einschreiten und helfen. Der Mediator wird die Situation bewerten und eine Auszahlung von Trade Funds vorschlagen. support.initialInfo=Bitte geben Sie eine Beschreibung Ihres Problems in das untenstehende Textfeld ein. Fügen Sie so viele Informationen wie möglich hinzu, um die Zeit für die Konfliktlösung zu verkürzen.\n\nHier ist eine Checkliste für Informationen, die Sie angeben sollten:\n\t● Wenn Sie der XMR-Käufer sind: Haben Sie die Traditional- oder Crypto-Überweisung gemacht? Wenn ja, haben Sie in der Anwendung auf die Schaltfläche "Zahlung gestartet" geklickt?\n\t● Wenn Sie der XMR-Verkäufer sind: Haben Sie die Traditional- oder Crypto-Zahlung erhalten? Wenn ja, haben Sie in der Anwendung auf die Schaltfläche "Zahlung erhalten" geklickt?\n\t● Welche Version von Haveno verwenden Sie?\n\t● Welches Betriebssystem verwenden Sie?\n\t● Wenn Sie ein Problem mit fehlgeschlagenen Transaktionen hatten, überlegen Sie bitte, in ein neues Datenverzeichnis zu wechseln.\n\t Manchmal wird das Datenverzeichnis beschädigt und führt zu seltsamen Fehlern. \n\t Siehe: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nBitte machen Sie sich mit den Grundregeln für den Konfliktprozess vertraut:\n\t● Sie müssen auf die Anfragen der {0}' innerhalb von 2 Tagen antworten.\n\t● Mediatoren antworten innerhalb von 2 Tagen. Die Vermittler antworten innerhalb von 5 Werktagen.\n\t● Die maximale Frist für einen Konflikt beträgt 14 Tage.\n\t● Sie müssen mit den {1} zusammenarbeiten und die Informationen zur Verfügung stellen, die sie anfordern, um Ihren Fall zu bearbeiten.\n\t● Mit dem ersten Start der Anwendung haben Sie die Regeln des Konfliktdokuments in der Nutzervereinbarung akzeptiert.\n\nSie können mehr über den Konfliktprozess erfahren unter: {2} support.systemMsg=Systemnachricht: {0} support.youOpenedTicket=Sie haben eine Anfrage auf Support geöffnet.\n\n{0}\n\nHaveno-Version: {1} support.youOpenedDispute=Sie haben eine Anfrage für einen Konflikt geöffnet.\n\n{0}\n\nHaveno-version: {1} support.youOpenedDisputeForMediation=Sie haben um Mediation gebeten.\n\n{0}\n\nHaveno-Version: {1} support.peerOpenedTicket=Ihr Trading-Partner hat aufgrund technischer Probleme Unterstützung angefordert.\n\n{0}\n\nHaveno-Version: {1} support.peerOpenedDispute=Ihr Trading-Partner hat einen Konflikt eröffnet.\n\n{0}\n\nHaveno-Version: {1} support.peerOpenedDisputeForMediation=Ihr Trading-Partner hat eine Mediation beantragt.\n\n{0}\n\nHaveno-Version: {1} support.mediatorsDisputeSummary=Systemnachricht: Konflikt-Zusammenfassung des Mediators:\n{0} support.mediatorsAddress=Node-Adresse des Mediators: {0} support.warning.disputesWithInvalidDonationAddress=Die verzögerte Auszahlungstransaktion hat eine ungültige Empfängeradresse verwendet. Sie stimmt mit keinem der DAO-Parameter für die gültigen Spendenadressen überein.\n\nDies könnte ein Betrugsversuch sein. Bitte informieren Sie die Entwickler über diesen Vorfall und schließen Sie den Fall nicht ab, bevor die Situation geklärt ist!\n\nIn dem Konflikt verwendete Adresse: {0}\n\nAlle DAO-Param-Spendenadressen: {1}\n\nHandels-ID: {2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nWollen Sie den Konflikt trotzdem schließen? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nSie müssen nicht auszahlen. support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. support.info.disputeReOpened=Dispute ticket has been re-opened. #################################################################### # Settings #################################################################### settings.tab.preferences=Voreinstellungen settings.tab.network=Netzwerk-Info settings.tab.about=Über setting.preferences.general=Allgemeine Voreinstellungen setting.preferences.explorer=Monero Explorer setting.preferences.deviation=Max. Abweichung vom Marktpreis setting.preferences.avoidStandbyMode=Standby Modus verhindern setting.preferences.useSoundForNotifications=Spiele Geräusche für Benachrichtigungen setting.preferences.autoConfirmXMR=XMR automatische Bestätigung setting.preferences.autoConfirmEnabled=Aktiviert setting.preferences.autoConfirmRequiredConfirmations=Benötigte Bestätigungen setting.preferences.autoConfirmMaxTradeSize=Max. Trade-Höhe (XMR) setting.preferences.autoConfirmServiceAddresses=Monero Explorer URLs (verwendet Tor, außer localhost, LAN IP Adresse und *.local hostnames) setting.preferences.deviationToLarge=Werte größer als {0}% sind nicht erlaubt. setting.preferences.txFee=Auszahlungstransaktionsgebühr (satoshis/vbyte) setting.preferences.useCustomValue=Spezifischen Wert nutzen setting.preferences.txFeeMin=Die Transaktionsgebühr muss mindestens {0} satoshis/vbyte betragen setting.preferences.txFeeTooLarge=Ihre Eingabe ist höher als jeder sinnvolle Wert (>5000 satoshis/vbyte). Transaktionsgebühren sind normalerweise zwischen 50-400 satoshis/vbyte. setting.preferences.ignorePeers=Ignorierte Peers [Onion Adresse:Port] setting.preferences.ignoreDustThreshold=Min. nicht-dust Ausgabewert setting.preferences.currenciesInList=Währungen in Liste der Marktpreise setting.preferences.prefCurrency=Bevorzugte Währung setting.preferences.displayTraditional=Nationale Währungen anzeigen setting.preferences.noTraditional=Es wurden keine nationalen Währungen ausgewählt setting.preferences.cannotRemovePrefCurrency=Sie können Ihre ausgewählte bevorzugte Anzeigewährung nicht entfernen setting.preferences.displayCryptos=Cryptos anzeigen setting.preferences.noCryptos=Es sind keine Cryptos ausgewählt setting.preferences.addTraditional=Nationale Währung hinzufügen setting.preferences.addCrypto=Crypto hinzufügen setting.preferences.displayOptions=Darstellungsoptionen setting.preferences.showOwnOffers=Eigenen Angebote im Angebotsbuch zeigen setting.preferences.useAnimations=Animationen abspielen setting.preferences.useDarkMode=Nacht-Modus benutzen setting.preferences.useLightMode=Leichtmodus verwenden setting.preferences.sortWithNumOffers=Marktlisten nach Anzahl der Angebote/Trades sortieren setting.preferences.onlyShowPaymentMethodsFromAccount=Nicht unterstützte Zahlungsmethoden ausblenden setting.preferences.denyApiTaker=Taker die das API nutzen vermeiden setting.preferences.notifyOnPreRelease=Pre-Release Benachrichtungen erhalten setting.preferences.resetAllFlags=Alle \"Nicht erneut anzeigen\"-Häkchen zurücksetzen settings.preferences.languageChange=Um den Sprachwechsel auf alle Bildschirme anzuwenden ist ein Neustart nötig. settings.preferences.supportLanguageWarning=Wenn es zu einem Streitfall kommen sollte, beachten Sie bitte, dass die Schiedsgerichtsbarkeit in {0} geregelt wird. settings.preferences.editCustomExplorer.headline=Explorer-Einstellungen settings.preferences.editCustomExplorer.description=Wählen Sie auf der linken Liste einen Explorer des Systems aus, und/oder passen Sie ihn an Ihre Vorlieben an. settings.preferences.editCustomExplorer.available=Verfügbare Explorer settings.preferences.editCustomExplorer.chosen=Wählen Sie die Explorer-Einstellungen aus settings.preferences.editCustomExplorer.name=Name settings.preferences.editCustomExplorer.txUrl=Transaktions-URL settings.preferences.editCustomExplorer.addressUrl=Adress-URL setting.info.headline=Neue Datenschutzfunktion settings.preferences.sensitiveDataRemoval.msg=Zum Schutz der Privatsphäre von Ihnen und anderen Händlern beabsichtigt Haveno, sensible Daten aus alten Trades zu entfernen. Dies ist besonders wichtig bei Fiat-Trades, die Bankkontodaten enthalten können.\n\nEs wird empfohlen, den Wert so niedrig wie möglich zu setzen, zum Beispiel 60 Tage. Das bedeutet, dass Trades, die älter als 60 Tage sind, sensible Daten gelöscht bekommen, sofern sie abgeschlossen sind. Abgeschlossene Trades finden Sie im Portfolio- / Verlauf-Reiter. settings.net.xmrHeader=Monero-Netzwerk settings.net.p2pHeader=Haveno-Netzwerk settings.net.onionAddressLabel=Meine Onion-Adresse settings.net.xmrNodesLabel=Spezifische Monero-Knoten verwenden settings.net.moneroPeersLabel=Verbundene Peers settings.net.connection=Verbindung settings.net.connected=Verbunden settings.net.useTorForXmrJLabel=Tor für das Monero-Netzwerk verwenden settings.net.moneroNodesLabel=Mit Monero-Knoten verbinden settings.net.useProvidedNodesRadio=Bereitgestellte Monero-Core-Knoten verwenden settings.net.usePublicNodesRadio=Öffentliches Monero-Netzwerk benutzen settings.net.useCustomNodesRadio=Spezifische Monero-Core-Knoten verwenden settings.net.warn.usePublicNodes=Wenn Sie öffentliche Monero-Nodes verwenden, sind Sie den Risiken ausgesetzt, die mit der Verwendung unvertrauenswürdiger Remote-Nodes verbunden sind.\n\nBitte lesen Sie weitere Details unter [HYPERLINK:https://www.getmonero.org/resources/moneropedia/remote-node.html].\n\nSind Sie sich sicher, dass Sie öffentliche Nodes verwenden möchten? settings.net.warn.usePublicNodes.useProvided=Nein, bereitgestellte Knoten verwenden settings.net.warn.usePublicNodes.usePublic=Ja, öffentliches Netzwerk verwenden settings.net.warn.useCustomNodes.B2XWarning=Bitte stellen Sie sicher, dass Sie sich mit einem vertrauenswürdigen Monero-Core-Knoten verbinden!\n\nWenn Sie sich mit Knoten verbinden, die gegen die Monero Core Konsensus-Regeln verstoßen, kann es zu Problemen in Ihrer Wallet und im Verlauf des Handelsprozesses kommen.\n\nBenutzer die sich zu oben genannten Knoten verbinden, sind für den verursachten Schaden verantwortlich. Dadurch entstandene Konflikte werden zugunsten des anderen Teilnehmers entschieden. Benutzer die unsere Warnungen und Sicherheitsmechanismen ignorieren wird keine technische Unterstützung geleistet! settings.net.warn.invalidXmrConfig=Die Verbindung zum Monero-Netzwerk ist fehlgeschlagen, weil Ihre Konfiguration ungültig ist.\n\nIhre Konfiguration wurde zurückgesetzt, um stattdessen die bereitgestellten Monero-Nodes zu verwenden. Sie müssen die Anwendung neu starten. settings.net.localhostXmrNodeInfo=Hintergrundinformationen: Haveno sucht beim Start nach einem lokalen Monero-Node. Wird dieser gefunden, kommuniziert Haveno ausschließlich über diesen mit dem Monero-Netzwerk. settings.net.p2PPeersLabel=Verbundene Peers settings.net.onionAddressColumn=Onion-Adresse settings.net.creationDateColumn=Eingerichtet settings.net.connectionTypeColumn=Ein/Aus settings.net.sentDataLabel=Daten-Statistiken senden settings.net.receivedDataLabel=Daten-Statistiken empfangen settings.net.chainHeightLabel=Letzte XMR Blockzeit settings.net.roundTripTimeColumn=Umlaufzeit settings.net.sentBytesColumn=Gesendet settings.net.receivedBytesColumn=Erhalten settings.net.peerTypeColumn=Peer-Typ settings.net.openTorSettingsButton=Tor-Netzwerkeinstellungen öffnen settings.net.versionColumn=Version settings.net.subVersionColumn=Subversion settings.net.heightColumn=Höhe settings.net.needRestart=Sie müssen die Anwendung neustarten, um die Änderungen anzuwenden.\nMöchten Sie jetzt neustarten? settings.net.notKnownYet=Noch nicht bekannt... settings.net.sentData=Gesendete Daten: {0}, {1} Nachrichten, {2} Nachrichten/Sekunde settings.net.receivedData=Empfangene Daten: {0}, {1} Nachrichten, {2} Nachrichten/Sekunde settings.net.chainHeight=Monero Peers chain height: {0} settings.net.ips=[IP Adresse:Port | Hostname:Port | Onion-Adresse:Port] (Komma getrennt). Port kann weggelassen werden, wenn Standard genutzt wird (8333). settings.net.seedNode=Seed-Knoten settings.net.directPeer=Peer (direkt) settings.net.initialDataExchange={0} [Bootstrapping] settings.net.peer=Peer settings.net.inbound=eingehend settings.net.outbound=ausgehend setting.about.aboutHaveno=Über Haveno setting.about.about=Haveno ist Open-Source Software, die den Tausch von Monero mit nationaler Währung (und anderen Kryptowährungen), durch ein dezentralisiertes Peer-zu-Peer Netzwerk auf eine Weise ermöglicht, die Ihre Privatsphäre stark beschützt. Lernen Sie auf unserer Projektwebseite mehr über Haveno. setting.about.web=Haveno-Website setting.about.code=Quellcode setting.about.agpl=AGPL-Lizenz setting.about.support=Haveno unterstützen setting.about.def=Haveno ist keine Firma, sondern ein Gemeinschaftsprojekt, das offen für Mitwirken ist. Wenn Sie an Haveno mitwirken oder das Projekt unterstützen wollen, folgen Sie bitte den unten stehenden Links. setting.about.contribute=Mitwirken setting.about.providers=Datenanbieter setting.about.apisWithFee=Haveno verwendet die Haveno Preis Indizes für Traditional und Crypto Preise und die Haveno Mempool Nodes für die Abschätzung der Mining-Gebühr. setting.about.apis=Haveno verwendet den Haveno Price Index für Traditional und Crypto Preise. setting.about.pricesProvided=Marktpreise zur Verfügung gestellt von setting.about.feeEstimation.label=Geschätzte Mining-Gebühr bereitgestellt von setting.about.versionDetails=Versionsdetails setting.about.version=Anwendungsversion setting.about.subsystems.label=Version des Teilsystems setting.about.subsystems.val=Netzwerkversion: {0}; P2P-Nachrichtenversion: {1}; Lokale DB-Version: {2}; Version des Handelsprotokolls: {3} setting.about.shortcuts=Shortcuts setting.about.shortcuts.ctrlOrAltOrCmd='Strg + {0}' oder 'Alt + {0}' oder 'cmd + {0}' setting.about.shortcuts.menuNav=Hauptmenü navigieren setting.about.shortcuts.menuNav.value=Um durch das Hauptmenü zu navigieren, drücken Sie: 'Strg' oder 'Alt' oder 'cmd' mit einer numerischen Taste zwischen '1-9' setting.about.shortcuts.close=Haveno beenden setting.about.shortcuts.close.value='Strg + {0}' oder 'cmd + {0}' bzw. 'Strg + {1}' oder 'cmd + {1}' setting.about.shortcuts.closePopup=Popup- oder Dialogfenster schließen setting.about.shortcuts.closePopup.value='ESCAPE' Taste setting.about.shortcuts.chatSendMsg=Trader eine Chat-Nachricht senden setting.about.shortcuts.chatSendMsg.value='Strg + ENTER' oder 'Alt + ENTER' oder 'cmd + ENTER' setting.about.shortcuts.openDispute=Streitfall eröffnen setting.about.shortcuts.openDispute.value=Wählen Sie den ausstehenden Trade und klicken Sie auf: {0} setting.about.shortcuts.walletDetails=Öffnen Sie das Fenster für Wallet-Details setting.about.shortcuts.openEmergencyXmrWalletTool=Öffnen Sie das Notfallwerkzeug für die XMR-Wallet setting.about.shortcuts.showTorLogs=Umschalten des Log-Levels für Tor-Meldungen zwischen DEBUG und WARN setting.about.shortcuts.manualPayoutTxWindow=Fenster öffnen für manuelle Auszahlung einer 2von2 Multisig Einzahlung tx setting.about.shortcuts.removeStuckTrade=Popup öffnen um fehlgeschlagenen Trade wieder zu den offenen Trades zu verschieben setting.about.shortcuts.removeStuckTrade.value=Fehlgeschlagenen Trade auswählen und drücken: {0} setting.about.shortcuts.registerArbitrator=Vermittler registrieren (nur Mediator/Vermittler) setting.about.shortcuts.registerArbitrator.value=Navigieren Sie zu Konto und drücken Sie: {0} setting.about.shortcuts.registerMediator=Mediator registrieren (nur Mediator/Vermittler) setting.about.shortcuts.registerMediator.value=Navigieren Sie zu Konto und drücken Sie: {0} setting.about.shortcuts.openSignPaymentAccountsWindow=Öffnen Sie das Fenster für Kontoalter-Unterzeichnung (nur Legacy-Vermittler) setting.about.shortcuts.openSignPaymentAccountsWindow.value=Navigieren Sie zur Legacy-Vermittler-Ansicht und drücken Sie: {0} setting.about.shortcuts.sendAlertMsg=Warnung oder Update per Nachricht senden (privilegierte Aktivität) setting.about.shortcuts.sendFilter=Filter einstellen (privilegierte Aktivität) setting.about.shortcuts.sendPrivateNotification=Private Benachrichtigung an Peer senden (privilegierte Aktivität) setting.about.shortcuts.sendPrivateNotification.value=Öffnen Sie die Partner-Infos am Avatar und klicken Sie: {0} setting.info.headline=Neues automatisches Bestätigungs-Feature für XMR setting.info.msg=Wenn Sie XMR für XMR verkaufen, können Sie die automatische Bestätigung aktivieren um nachzuprüfen ob die korrekte Menge an Ihr Wallet gesendet wurde. So kann Haveno den Trade automatisch abschließen und Trades dadurch für alle schneller machen.\n\nDie automatische Bestätigung überprüft die XMR Transaktion über mindestens 2 XMR Explorer Nodes mit dem privaten Transaktionsschlüssel den der Sender zur Verfügung gestellt hat. Haveno verwendet standardmäßig Explorer Nodes die von Haveno Contributors betrieben werden aber wir empfehlen, dass Sie für ein Maximum an Sicherheit und Privatsphäre Ihre eigene XMR Explorer Node betreiben.\n\nFür automatische Bestätigungen, können Sie die max. Höhe an XMR pro Trade und die Anzahl der benötigten Bestätigungen in den Einstellungen festlegen.\n\nFinden Sie weitere Informationen (und eine Anleitung wie Sie Ihre eigene Explorer Node aufsetzen) im Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades] #################################################################### # Account #################################################################### account.tab.mediatorRegistration=Mediator-Registrierung account.tab.refundAgentRegistration=Registrierung des Rückerstattungsbeauftragten account.tab.signing=Unterzeichnung account.info.headline=Willkommen in Ihrem Haveno-Konto account.info.msg=Hier können Sie Trading-Konten für nationale Währungen und Cryptos hinzufügen und Backups für Ihre Wallets & Kontodaten erstellen.\n\nEine leere Monero-Wallet wurde erstellt, als Sie das erste Mal Haveno gestartet haben.\n\nWir empfehlen, dass Sie Ihre Monero-Wallet-Seed-Wörter aufschreiben (siehe Tab oben) und sich überlegen ein Passwort hinzuzufügen, bevor Sie einzahlen. Monero-Einzahlungen und Auszahlungen werden unter \"Gelder\" verwaltet.\n\nHinweis zu Privatsphäre & Sicherheit: da Haveno eine dezentralisierte Börse ist, bedeutet dies, dass all Ihre Daten auf ihrem Computer bleiben. Es gibt keine Server und wir haben keinen Zugriff auf Ihre persönlichen Informationen, Ihre Gelder oder selbst Ihre IP-Adresse. Daten wie Bankkontonummern, Crypto- & Moneroadressen, etc werden nur mit Ihrem Trading-Partner geteilt, um Trades abzuschließen, die Sie initiiert haben (im Falle eines Konflikts wird der Vermittler die selben Daten sehen wie Ihr Trading-Partner). account.menu.paymentAccount=Nationale Währungskonten account.menu.altCoinsAccountView=Crypto-Konten account.menu.password=Wallet-Passwort account.menu.seedWords=Wallet-Seed account.menu.walletInfo=Wallet Info account.menu.backup=Backup account.menu.notifications=Benachrichtigungen account.menu.walletInfo.balance.headLine=Wallet-Guthaben account.menu.walletInfo.balance.info=Hier wird das Wallet-Guthaben einschließlich unbestätigter Transaktionen angezeigt.\nFür XMR sollte das unten angezeigte Wallet-Guthaben mit der Summe der oben rechts in diesem Fenster angezeigten "Verfügbaren" und "Reservierten" Guthaben übereinstimmen. account.menu.walletInfo.xpub.headLine=Watch Keys (xpub keys) account.menu.walletInfo.walletSelector={0} {1} Wallet account.menu.walletInfo.path.headLine=HD Keychain Pfade account.menu.walletInfo.path.info=Wenn Sie Seed Wörter in eine andere Wallet (wie Electrum) importieren, müssen Sie den Pfad angeben. Dies sollte nur in Notfällen gemacht werden, wenn Sie den Zugriff auf die Haveno-Wallet und das Data-Verzeichnis verloren haben.\nDenken Sie daran, dass die Ausgabe von Geldern aus einer Nicht-Haveno-Wallet die internen Haveno-Datenstrukturen, die mit den Wallet-Daten verbunden sind, durcheinander bringen kann, was zu fehlgeschlagenen Trades führen kann.\n\nSenden Sie NIEMALS BSQ von einer Nicht-Haveno-Wallet, da dies wahrscheinlich zu einer ungültigen BSQ-Transaktion und dem Verlust Ihrer BSQ führen wird. account.menu.walletInfo.openDetails=Wallet-Rohdaten und Private Schlüssel anzeigen ## TODO should we rename the following to a gereric name? account.arbitratorRegistration.pubKey=Öffentlicher Schlüssel account.arbitratorRegistration.register=Registrieren account.arbitratorRegistration.registration={0} Registrierung account.arbitratorRegistration.revoke=Widerrufen account.arbitratorRegistration.info.msg=Beachten Sie bitte, dass Sie nach dem Widerrufen für 15 Tage verfügbar bleiben müssen, da es Trades geben kann, die Sie als {0} nutzen. Die maximal erlaubte Trade-Dauer ist 8 Tage und der Konfliktprozess kann bis zu 7 Tage dauern. account.arbitratorRegistration.warn.min1Language=Sie müssen wenigstens 1 Sprache festlegen.\nWir haben Ihre Standardsprache für Sie hinzugefügt. account.arbitratorRegistration.removedSuccess=Sie haben Ihre Registrierung erfolgreich aus dem Haveno-Netzwerk entfernt. account.arbitratorRegistration.removedFailed=Die Registrierung konnte nicht entfernt werden.{0} account.arbitratorRegistration.registerSuccess=Sie haben sich erfolgreich im Haveno-Netzwerk registriert. account.arbitratorRegistration.registerFailed=Die Registrierung konnte nicht abgeschlossen werden.{0} account.crypto.yourCryptoAccounts=Ihre Crypto-Konten account.crypto.popup.wallet.msg=Bitte stellen Sie sicher, dass Sie die Anforderungen für die Verwendung von {0} Wallets wie auf der {1} Webseite beschrieben erfüllen.\nDie Verwendung von Wallets von zentralisierten Börsen, bei denen (a) Sie Ihre Keys nicht kontrollieren oder (b) die keine kompatible Wallet-Software verwenden, ist riskant: Es kann zum Verlust der gehandelten Gelder führen!\nDer Mediator oder Vermittler ist kein {2} Spezialist und kann in solchen Fällen nicht helfen. account.crypto.popup.wallet.confirm=Ich verstehe und bestätige, dass ich weiß, welche Wallet ich benutzen muss. # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=Der Handel mit UPX auf Haveno setzt voraus, dass Sie die folgenden Anforderungen verstehen und erfüllen:\n\nFür das Senden von UPX müssen Sie entweder das offizielle uPlexa GUI-Wallet oder das uPlexa CLI-Wallet mit aktiviertem store-tx-info Flag verwenden (Standard in neuen Versionen). Bitte stellen Sie sicher, dass Sie auf den tx key zugreifen können, da dies bei einem Konfliktfall erforderlich wäre.\nuplexa-wallet-cli (verwenden Sie den Befehl get_tx_key)\nuplexa-wallet-gui (gehen Sie zum History Tab und klicken Sie auf (P) für den Zahlungsnachweis)\n\nBei normalen Blockexplorern ist der Transfer nicht verifizierbar.\n\nSie müssen dem Vermittler im Konfliktfall die folgenden Daten zur Verfügung stellen:\n- Der tx Private Key\n- Der Transaktionshash\n- Die öffentliche Adresse des Empfängers\n\nWenn Sie die oben genannten Daten nicht angeben oder wenn Sie eine inkompatible Wallet verwendet haben, verlieren Sie den Konfliktfall. Der UPX-Sender ist dafür verantwortlich, im Konfliktfall dem Vermittler die Verifizierung des UPX-Transfers nachzuweisen.\n\nEs ist keine Zahlungs-ID erforderlich, sondern nur die normale öffentliche Adresse.\nWenn Sie sich über diesen Prozess nicht sicher sind, besuchen Sie den uPlexa discord channel (https://discord.gg/vhdNSrV) oder den uPlexa Telegram Chat (https://t.me/uplexaOfficial), um weitere Informationen zu erhalten. # suppress inspection "UnusedProperty" account.crypto.popup.arq.msg=Der Handel mit ARQ auf Haveno setzt voraus, dass Sie die folgenden Anforderungen verstehen und erfüllen:\n\nFür den Versand von ARQ müssen Sie entweder das offizielle ArQmA GUI-Wallet oder das ArQmA CLI-Wallet mit aktiviertem store-tx-info Flag verwenden (Standard in neuen Versionen). Bitte stellen Sie sicher, dass Sie auf den tx Key zugreifen können, da dies im Falle eines Konfliktes erforderlich wäre.\narqma-wallet-cli (verwenden Sie den Befehl get_tx_key)\narqma-wallet-gui (gehen Sie zur History Tab und klicken Sie auf (P) für den Zahlungsnachweis)\n\nBei normalen Blockexplorern ist der Transfer nicht verifizierbar.\n\nSie müssen dem Mediator oder Vermittler im Konfliktfall die folgenden Daten zur Verfügung stellen:\n- Der tx Private Key\n- Der Transaktionshash\n- Die öffentliche Adresse des Empfängers\n\nWenn Sie die oben genannten Daten nicht angeben oder wenn Sie eine inkompatible Wallet verwendet haben, verlieren Sie den Konfliktfall. Der ARQ-Sender ist im Fall eines Konflikts dafür verantwortlich, die Verifizierung des ARQ-Transfers dem Mediator oder Vermittler nachzuweisen.\n\nEs ist keine Zahlungs-ID erforderlich, sondern nur die normale öffentliche Adresse.\nWenn Sie sich über diesen Prozess nicht sicher sind, besuchen Sie den ArQmA Discord Channel (https://discord.gg/s9BQpJT) oder das ArQmA Forum (https://labs.arqma.com), um weitere Informationen zu erhalten. # suppress inspection "UnusedProperty" account.crypto.popup.xmr.msg=Der Handel mit XMR auf Haveno setzt voraus, dass Sie die folgende Anforderung verstehen.\n\nWenn Sie XMR verkaufen, müssen Sie in der Lage sein, einem Vermittler oder einer Schiedsperson im Falle eines Konflikts die folgenden Informationen zur Verfügung zu stellen:\n- den Transaktionsschlüssel (Tx Key, Tx Secret Key oder Tx Private Key)\n- die Transaktions-ID (Tx ID oder Tx Hash)\n- die Zieladresse (Empfängeradresse)\n\nIm Wiki finden Sie Einzelheiten darüber wo Sie diese Informationen in den populärsten Monero-Wallets finden [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Proving_payments].\nWerden die erforderlichen Transaktionsdaten nicht zur Verfügung gestellt, führt dies dazu, dass der Konflikt zu Ihrem Nachteil entschieden wird.\n\nBeachten Sie auch, dass Haveno jetzt eine automatische Bestätigung für XMR-Transaktionen anbietet, um Transaktionen schneller zu machen, aber Sie müssen dies in den Einstellungen aktivieren.\n\nWeitere Informationen über die automatische Bestätigungsfunktion finden Sie im Wiki: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. # suppress inspection "UnusedProperty" account.crypto.popup.msr.msg=Der Handel mit MSR auf Haveno setzt voraus, dass Sie die folgenden Anforderungen verstehen und erfüllen:\n\nFür den Versand von MSR müssen Sie entweder das offizielle Masari GUI Wallet, das Masari CLI Wallet mit dem aktivierten store-tx-info Flag (standardmäßig aktiviert) oder das Masari Web Wallet (https://wallet.getmasari.org) verwenden. Bitte stellen Sie sicher, dass Sie auf den tx Key zugreifen können, da dies im Falle eines Konfliktes erforderlich wäre.\nmasari-wallet-cli (verwenden Sie den Befehl get_tx_key)\nmasari-wallet-gui (gehen Sie zur History Tab und klicken Sie auf (P) für den Zahlungsnachweis).\n\nMasari Web Wallet (gehen Sie zum Konto -> Transaktionshistorie und lassen Sie Details zu Ihrer gesendeten Transaktion anzeigen)\n\nDie Verifizierung kann im Wallet durchgeführt werden.\nmasari-wallet-cli : mit dem Befehl (check_tx_key).\nmasari-wallet-gui : auf der Seite Advanced > Prove/Check.\nDie Verifizierung kann im Block-Explorer durchgeführt werden. \nÖffnen Sie den Block-Explorer (https://explorer.getmasari.org), verwenden Sie die Suchleiste, um Ihren Transaktionshash zu finden.\nSobald die Transaktion gefunden wurde, scrollen Sie nach unten zum Bereich 'Prove Sending' und geben Sie bei Bedarf Details ein.\nSie müssen dem Mediator oder Vermittler im Konfliktfall die folgenden Daten zur Verfügung stellen:\n- Den tx Private Key\n- Den Transaktionshash\n- Die öffentliche Adresse des Empfängers\n\nWenn Sie die oben genannten Daten nicht angeben oder wenn Sie eine inkompatible Wallet verwendet haben, verlieren Sie den Konfliktfall. Der MSR-Sender ist im Fall eines Konflikts dafür verantwortlich, die Verifizierung des MSR-Transfers dem Mediator oder Vermittler nachzuweisen.\n\nEs ist keine Zahlungs-ID erforderlich, sondern nur die normale öffentliche Adresse.\nWenn Sie sich über diesen Prozess nicht sicher sind, fragen Sie um Hilfe auf der offiziellen Masari Discord (https://discord.gg/sMCwMqs). # suppress inspection "UnusedProperty" account.crypto.popup.blur.msg=Der Handel mit BLUR auf Haveno setzt voraus, dass Sie die folgenden Anforderungen verstehen und erfüllen:\n\nUm BLUR zu senden, müssen Sie die Blur Network CLI oder GUI Wallet verwenden. \n\nWenn Sie die CLI-Wallet verwenden, wird nach dem Senden eines Transfers ein Transaktionshash (tx ID) angezeigt. Sie müssen diese Informationen speichern. Unmittelbar nach dem Senden des Transfers müssen Sie den Private Key der Transaktion mit dem Befehl 'get_tx_key' ermitteln. Wenn Sie diesen Schritt nicht ausführen, können Sie den Key möglicherweise später nicht mehr abrufen. \n\nWenn Sie das Blur Network GUI Wallet verwenden, können Sie problemlos den Private Key der Transaktion und die Transaktion-ID im "History" Tab finden. Suchen Sie sofort nach dem Absenden die Transaktion, die von Interesse ist. Klicken Sie auf das Symbol "?" in der unteren rechten Ecke des Feldes, das die Transaktion enthält. Sie müssen diese Informationen speichern. \n\nFalls ein Vermittlungsverfahren erforderlich ist, müssen Sie einem Mediator oder Vermittler Folgendes vorlegen: 1.) die Transaktions-ID, 2.) den Private Key der Transaktion und 3.) die Adresse des Empfängers. Der Mediator oder Vermittler überprüft dann den BLUR-Transfer mit dem Blur Transaction Viewer (https://blur.cash/#tx-viewer).\n\nWenn Sie dem Mediator oder Vermittler nicht die erforderlichen Informationen zur Verfügung stellen, verlieren Sie den Konfliktfall. In allen Konfliktfällen trägt der BLUR-Sender 100% der Verantwortung für die Verifizierung von Transaktionen gegenüber einem Mediator oder Vermittler. \n\nWenn Sie diese Anforderungen nicht verstehen, handeln Sie nicht auf Haveno. Als Erstes suchen Sie Hilfe im Blur Network Discord (https://discord.gg/dMWaqVW). # suppress inspection "UnusedProperty" account.crypto.popup.solo.msg=Der Handel mit Solo auf Haveno setzt voraus, dass Sie die folgenden Anforderungen verstehen und erfüllen:\n\nUm Solo zu senden, müssen Sie das Solo Network CLI Wallet verwenden. \n\nWenn Sie das CLI-Wallet verwenden, wird nach dem Senden eines Transfers ein Transaktionshash (tx ID) angezeigt. Sie müssen diese Informationen speichern. Unmittelbar nach dem Senden des Transfers müssen Sie den Private Key der Transaktion mit dem Befehl 'get_tx_key' ermitteln. Wenn Sie diesen Schritt nicht ausführen, können Sie den Key möglicherweise später nicht mehr abrufen.\n\nFalls ein Vermittlungsverfahren erforderlich ist, müssen Sie einem Mediator oder Vermittler Folgendes vorlegen: 1.) die Transaktion-ID, 2.) den Private Key der Transaktion und 3.) die Adresse des Empfängers. Der Mediator oder Vermittler überprüft dann den Solo-Transfer mit dem Solo Block Explorer, indem er nach der Transaktion sucht und dann die Funktion "Senden nachweisen" (https://explorer.minesolo.com/) verwendet.\n\nWenn Sie dem Mediator oder Vermittler nicht die erforderlichen Informationen zur Verfügung stellen, verlieren Sie den Konfliktfall. In allen Konfliktfällen trägt der Solo-Sender 100% der Verantwortung für die Verifizierung von Transaktionen gegenüber einem Mediator oder Vermittler. \n\nWenn Sie diese Anforderungen nicht verstehen, handeln Sie nicht auf Haveno. Suchen Sie zuerst Hilfe im Solo Network Discord (https://discord.minesolo.com/). # suppress inspection "UnusedProperty" account.crypto.popup.cash2.msg=Der Handel mit CASH2 auf Haveno setzt voraus, dass Sie die folgenden Anforderungen verstehen und erfüllen:\n\nUm CASH2 zu versenden, müssen Sie die Cash2 Wallet Version 3 oder höher verwenden. \n\nNachdem eine Transaktion gesendet wurde, wird die Transaktions-ID angezeigt. Sie müssen diese Informationen speichern. Unmittelbar nach dem Senden der Transaktion müssen Sie den Befehl 'getTxKey' in simplewallet verwenden, um den Secret Key der Transaktion abzurufen. \n\nFalls ein Vermittlungsverfahren erforderlich ist, müssen Sie einem Mediator oder Vermittler Folgendes vorlegen: 1) die Transaktions-ID, 2) den Secret Key der Transaktion und 3) die Cash2-Adresse des Empfängers. Der Mediator oder Vermittler überprüft dann den CASH2-Transfer mit dem Cash2 Block Explorer (https://blocks.cash2.org).\n\nWenn Sie dem Mediator oder Vermittler nicht die erforderlichen Informationen zur Verfügung stellen, werden Sie den Konfliktfall verlieren. In allen Konfliktfällen trägt der CASH2-Sender 100% der Verantwortung für die Verifizierung von Transaktionen gegenüber einem Mediator oder Vermittler.\n\nWenn Sie diese Anforderungen nicht verstehen, handeln Sie nicht auf Haveno, sondern suchen Sie zunächst Hilfe im Cash2 Discord (https://discord.gg/FGfXAYN). # suppress inspection "UnusedProperty" account.crypto.popup.qwertycoin.msg=Der Handel mit Qwertycoin auf Haveno setzt voraus, dass Sie die folgenden Anforderungen verstehen und erfüllen:\n\nUm QWC zu versenden, müssen Sie die offizielle QWC Wallet Version 5.1.3 oder höher verwenden. \n\nNachdem eine Transaktion gesendet wurde, wird die Transaktions-ID angezeigt. Sie müssen diese Informationen speichern. Unmittelbar nach dem Senden der Transaktion müssen Sie den Befehl 'get_Tx_Key' in simplewallet verwenden, um den Secret Key der Transaktion abzurufen. \n\nFalls ein Vermittlungsverfahren erforderlich ist, müssen Sie einem Mediator oder Vermittler Folgendes vorlegen: 1) die Transaktions-ID, 2) den Secret Key der Transaktion und 3) die QWC-Adresse des Empfängers. Der Mediator oder Vermittler überprüft dann den QWC-Transfer mit dem QWC Block Explorer (https://explorer.qwertycoin.org).\n\nWenn Sie dem Mediator oder Vermittler nicht die erforderlichen Informationen zur Verfügung stellen, werden Sie den Konfliktfall verlieren. In allen Konfliktfällen trägt der QWC-Sender 100% der Verantwortung für die Verifizierung von Transaktionen gegenüber einem Mediator oder Vermittler.\n\nWenn Sie diese Anforderungen nicht verstehen, handeln Sie nicht auf Haveno, sondern suchen Sie zunächst Hilfe im QWC Discord (https://discord.gg/rUkfnpC). # suppress inspection "UnusedProperty" account.crypto.popup.drgl.msg=Der Handel mit Dragonglass auf Haveno setzt voraus, dass Sie die folgenden Anforderungen verstehen und erfüllen:\n\nAufgrund der Privatsphäre, die Dragonglass bietet, ist eine Transaktion auf der Public Blockchain nicht verifizierbar. Bei Bedarf können Sie Ihre Zahlung durch die Verwendung Ihres TXN-Private-Key nachweisen.\nDer TXN-Private Key ist ein einmaliger Schlüssel, der automatisch für jede Transaktion generiert wird, auf die Sie nur über Ihre DRGL-Wallet zugreifen können - entweder über die DRGL-Wallet GUI (im Transaktionsdetaildialog) oder über die Dragonglass CLI simplewallet (mit dem Befehl "get_tx_key").\n\nDRGL-Version "Oathkeeper" und höher sind für beide ERFORDERLICH.\n\nIm Konfliktfall müssen Sie dem Mediator oder Vermittler die folgenden Daten zur Verfügung stellen:\n- Den TXN-Private-Key\n- Den Transaktionshash\n- Die öffentliche Adresse des Empfängers\n\nDie Verifizierung der Zahlung kann mit den oben genannten Daten als Eingabe unter (http://drgl.info/#check_txn) erfolgen.\n\nWenn Sie die oben genannten Daten nicht angeben oder wenn Sie eine inkompatible Wallet verwendet haben, verlieren Sie den Klärungsfall. Der Dragonglass-Sender ist für die Verifizierung des DRGL-Transfers gegenüber dem Mediator oder Vermittler im Konfliktfall verantwortlich. Die Verwendung einer PaymentID ist nicht erforderlich.\n\nWenn Sie sich über irgendeinen Teil dieses Prozesses unsicher sind, besuchen Sie Dragonglass auf Discord (http://discord.drgl.info) um Hilfe zu erhalten. # suppress inspection "UnusedProperty" account.crypto.popup.ZEC.msg=Bei der Verwendung von Zcash können Sie nur die transparenten Adressen (beginnend mit t) verwenden, nicht die z-Adressen (privat), da der Mediator oder Vermittler nicht in der Lage wäre, die Transaktion mit z-Adressen zu verifizieren. # suppress inspection "UnusedProperty" account.crypto.popup.XZC.msg=Bei der Verwendung von Zcoin können Sie nur die transparenten (rückverfolgbaren) Adressen verwenden, nicht die nicht rückverfolgbaren Adressen, da der Mediator oder Vermittler nicht in der Lage wäre, die Transaktion mit nicht rückverfolgbaren Adressen in einem Block-Explorer zu verifizieren. # suppress inspection "UnusedProperty" account.crypto.popup.grin.msg=GRIN benötigt einen interaktiven Prozess zwischen Sender und Empfänger, um die Transaktion zu erstellen. Stellen Sie sicher, den Anweisungen der GRIN Projekt Webseite zu folgen, um zuverlässig GRIN zu senden und empfangen (der Empfänger muss oinline oder wenigstens während eines gewissen Zeitfensters). \n\nHaveno unterstützt nur das Grinbox (Wallet713) Wallet URL Format. \n\nDer GRIN Sender muss beweisen können, die GRIN erfolgreich gesendet zu haben. Wenn die Wallet dies nicht kann, wird ein potentieller Konflikt zugunsten des GRIN Empfängers entschieden. Bitte stellen Sie sicher, dass Sie die letzte Grinbox Software nutzen, die den Transaktionsbeweis unterstützt, und Sie den Prozess verstehen, wie GRIN gesendet und empfangen wird, sowie wie man den Beweis erstellt. \n\nBeachten Sie https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only für weitere Informationen über das Grinbox proof tool. # suppress inspection "UnusedProperty" account.crypto.popup.beam.msg=BEAM benötigt einen interaktiven Prozess zwischen Sender und Empfänger, um die Transaktion zu erstellen.\n\nStellen Sie sicher, den Anweisungen der BEAM Projekt Webseite zu folgen, um zuverlässig BEAM zu senden und empfangen (der Empfänger muss oinline oder wenigstens während eines gewissen Zeitfensters). \n\nDer BEAM Sender muss beweisen können, die BEAM erfolgreich gesendet zu haben. Bitte stellen Sie sicher, dass Sie Wallet Software verwenden, die solche Beweise erstellen kann. Falls Die Wallet den Beweis nicht erstellen kann, wird ein potentieller Konflikt zugunsten des BEAM Empfängers entschieden. # suppress inspection "UnusedProperty" account.crypto.popup.pars.msg=Der Handel mit ParsiCoin auf Haveno setzt voraus, dass Sie die folgenden Anforderungen verstehen und erfüllen:\n\nUm PARS zu versenden, müssen Sie die offizielle ParsiCoin Wallet Version 3.0.0 oder höher verwenden. \n\nSie können Ihren Transaktionshash und Transaktionsschlüssel im Bereich Transaktionen in Ihrer GUI-Wallet (ParsiPay) überprüfen. Sie müssen mit der rechten Maustaste auf die Transaktion und dann auf Details anzeigen klicken.\n\nFalls ein Vermittlungsverfahren erforderlich ist, müssen Sie einem Mediator oder Vermittler Folgendes vorlegen: 1) den Transaktionshash, 2) den Transaktionsschlüssel und 3) die PARS-Adresse des Empfängers. Der Mediator oder Vermittler überprüft dann den PARS-Transfer mit dem ParsiCoin Block Explorer (http://explorer.parsicoin.net/#check_payment).\n\nWenn Sie dem Mediator oder Vermittler nicht die erforderlichen Informationen zur Verfügung stellen, werden Sie den Konfliktfall verlieren. In allen Konfliktfällen trägt der ParsiCoin-Sender 100% der Verantwortung für die Verifizierung von Transaktionen gegenüber einem Mediator oder Vermittler. \n\nWenn Sie diese Anforderungen nicht verstehen, handeln Sie nicht auf Haveno, sondern suchen Sie zunächst Hilfe im ParsiCoin Discord (https://discord.gg/c7qmFNh). # suppress inspection "UnusedProperty" account.crypto.popup.blk-burnt.msg=Um "Burnt Blackcoins" zu handeln, müssen Sie folgendes wissen:\n\nBurnt Blackcoins können nicht ausgegeben werden. Um sie auf Haveno zu handeln, müssen die Ausgabeskripte in der Form vorliegen: OP_RETURN OP_PUSHDATA, gefolgt von zugehörigen Datenbytes, die nach der Hex-Codierung Adressen darstellen. Beispielsweise haben Burnt Blackcoins mit der Adresse 666f6f ("foo" in UTF-8) das folgende Skript:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\nUm Burnt Blackcoins zu erstellen, kann man den in einigen Wallets verfügbaren RPC-Befehl "burn" verwenden.\n\nFür mögliche Anwendungsfälle kann man einen Blick auf https://ibo.laboratorium.ee werfen.\n\nDa Burnt Blackcoins nicht ausgegeben werden können, können sie nicht wieder verkauft werden. "Verkaufen" von Burnt Blackcoins bedeutet, gewöhnliche Blackcoins zu verbrennen (mit zugehörigen Daten entsprechend der Zieladresse).\n\nIm Konfliktfall hat der BLK-Verkäufer den Transaktionshash zur Verfügung zu stellen. # suppress inspection "UnusedProperty" account.crypto.popup.liquidmonero.msg=Das Trading mit L-XMR auf Haveno setzt voraus, dass Sie Folgendes verstehen:\n\nWenn Sie L-XMR für einen Trade auf Haveno erhalten, können Sie nicht die mobile Blockstream Green Wallet App oder eine Custodial/Exchange Wallet verwenden. Sie dürfen L-XMR nur in der Liquid Elements Core Wallet oder eine andere L-XMR-Wallet erhalten, die es Ihnen ermöglicht, den Blinding Key für Ihre verdeckte L-XMR-Adresse zu erhalten.\n\nFalls eine Mediation erforderlich ist oder ein Trade-Konflikt entsteht, müssen Sie den Blinding Key für Ihre L-XMR-Empfangsadresse dem Haveno-Mediator oder dem Refund Agent mitteilen, damit dieser die Details Ihrer vertraulichen Transaktion auf seinem eigenen Elements Core Full Node überprüfen kann.\n\nWenn Sie dem Mediator oder Refund Agent die erforderlichen Informationen nicht zur Verfügung stellen, führt dies dazu, dass Sie den Streitfall verlieren. In allen Streitfällen trägt der L-XMR-Empfänger 100 % der Verantwortung für die Bereitstellung kryptographischer Beweise an den Mediator oder den Refund Agent.\n\nWenn Sie diese Anforderungen nicht verstehen, sollten Sie nicht mit L-XMR auf Haveno traden. account.traditional.yourTraditionalAccounts=Ihre Nationalen Währungskonten account.backup.title=Backup der Wallet erstellen account.backup.location=Speicherort des Backups account.backup.selectLocation=Speicherort des Backups wählen account.backup.backupNow=Backup jetzt erstellen (Backup ist nicht verschlüsselt!) account.backup.appDir=Verzeichnis der Anwendungsdaten account.backup.openDirectory=Verzeichnis öffnen account.backup.openLogFile=Protokolldatei öffnen account.backup.success=Backup erfolgreich gespeichert in:\n{0} account.backup.directoryNotAccessible=Der ausgewählt Ordner ist nicht erreichbar. {0} account.password.removePw.button=Passwort entfernen account.password.removePw.headline=Passwortschutz Ihrer Wallet entfernen account.password.setPw.button=Passwort festlegen account.password.setPw.headline=Passwortschutz Ihrer Wallet einrichten account.password.info=Mit aktiviertem Passwortschutz müssen Sie Ihr Passwort eingeben, wenn Sie Monero aus Ihrer Brieftasche abheben, wenn Sie Ihre Seed-Wörter anzeigen oder Ihre Brieftasche wiederherstellen möchten sowie beim Start der Anwendung. account.seed.backup.title=Erstelle eine Sicherungskopie deiner Wallet-Schlüsselwörter. account.seed.info=Bitte notiere sowohl die Schlüsselwörter deiner Geldbörse als auch das Datum. Du kannst deine Geldbörse jederzeit mit den Schlüsselwörtern und dem Datum wiederherstellen.\n\nDu solltest die Schlüsselwörter auf ein Blatt Papier schreiben. Speichere sie nicht auf dem Computer.\n\nBitte beachte, dass die Schlüsselwörter KEIN Ersatz für ein Backup sind.\nDu musst eine Sicherungskopie des gesamten Anwendungsverzeichnisses von der "Konto/Sicherung" Seite erstellen, um den Anwendungsstatus und die Daten wiederherzustellen. account.seed.backup.warning=Bitte beachte, dass die Seed-Wörter KEIN Ersatz für ein Backup sind.\nDu musst eine Sicherungskopie des gesamten Anwendungsverzeichnisses von der "Konto/Sicherung" Seite erstellen, um den Anwendungsstatus und die Daten wiederherzustellen. account.seed.warn.noPw.msg=Sie haben kein Wallet-Passwort festgelegt, was das Anzeigen der Seed-Wörter schützen würde.\n\nMöchten Sie die Seed-Wörter jetzt anzeigen? account.seed.warn.noPw.yes=Ja, und nicht erneut fragen account.seed.enterPw=Geben Sie Ihr Passwort ein um die Seed-Wörter zu sehen account.seed.restore.info=Bitte erstellen Sie vor dem Wiederherstellen durch Keimwörter ein Backup. Beachten Sie auch, dass Wallet-Wiederherstellung nur für Notfälle ist und Probleme mit der internen Wallet-Datenbank verursachen kann.\nEs ist kein Weg ein Backup anzuwenden! Bitte nutzen Sie ein Backup des Anwendungsdatenordner um eine vorherigen Zustand wiederherzustellen. \n\nNach der Wiederherstellung wird die Anwendung herunterfahren. Nachdem Sie die Anwendung wieder gestartet haben, wird sie wieder mit dem Monero-Netzwerk synchronisieren. Dies kann lange dauern und die CPU stark beanspruchen, vor allem, wenn die Wallet alt und viele Transaktionen hatte. Bitte unterbreche Sie diesen Prozess nicht, sonst müssen Sie vielleicht die SPV Kettendatei löschen und den Wiederherstellungsprozess wiederholen. account.seed.restore.ok=Ok, mache die Wiederherstellung und fahre Haveno herunter #################################################################### # Mobile notifications #################################################################### account.notifications.setup.title=Konfiguration account.notifications.download.label=Mobile App herunterladen account.notifications.waitingForWebCam=Warte auf Webcam... account.notifications.webCamWindow.headline=Scanne QR-Code vom Smartphone account.notifications.webcam.label=Nutze Webcam account.notifications.webcam.button=QR-Code scannen account.notifications.noWebcam.button=Ich habe keine Webcam account.notifications.erase.label=Benachrichtigungen von Smartphone löschen account.notifications.erase.title=Benachrichtigungen leeren account.notifications.email.label=Kopplungs-Token account.notifications.email.prompt=Geben Sie den Kopplungs-Token ein, den Sie per E-Mail erhalten haben account.notifications.settings.title=Einstellungen account.notifications.useSound.label=Benachrichtigungs-Ton am Smartphone abspielen account.notifications.trade.label=Erhalte Nachrichten zu Händel account.notifications.market.label=Erhalte Benachrichtigungen zu Angeboten account.notifications.price.label=Erhalte Preisbenachrichtigungen account.notifications.priceAlert.title=Preisalarme: account.notifications.priceAlert.high.label=Benachrichtigen, wenn XMR-Preis über account.notifications.priceAlert.low.label=Benachrichtigen, wenn XMR-Preis unter account.notifications.priceAlert.setButton=Preisalarm setzen account.notifications.priceAlert.removeButton=Preisalarm entfernen account.notifications.trade.message.title=Handelsstatus verändert account.notifications.trade.message.msg.conf=Die Kaution-Transaktion für den Handel mit ID {0} wurde bestätigt. Bitte öffnen Sie Ihre Haveno Anwendung und starten die Zahlung. account.notifications.trade.message.msg.started=Der XMR-Käufer hat die Zahlung für den Handel mit ID {0} begonnen. account.notifications.trade.message.msg.completed=Der Handel mit ID {0} ist abgeschlossen. account.notifications.offer.message.title=Ihr Angebot wurde angenommen account.notifications.offer.message.msg=Ihr Angebot mit ID {0} wurde angenommen account.notifications.dispute.message.title=Neue Konflikt-Nachricht account.notifications.dispute.message.msg=Sie haben eine Konflikt-Nachricht für den Handel mit ID {0} erhalten account.notifications.marketAlert.title=Angebotsalarme account.notifications.marketAlert.selectPaymentAccount=Angebote, die dem Zahlungskonto entsprechen account.notifications.marketAlert.offerType.label=Angebotstyp, an dem ich interessiert bin account.notifications.marketAlert.offerType.buy=Kauf-Angebote (Ich möchte XMR verkaufen) account.notifications.marketAlert.offerType.sell=Verkauf-Angebote (Ich möchte XMR kaufen) account.notifications.marketAlert.trigger=Angebot Preisdistanz (%) account.notifications.marketAlert.trigger.info=Mit gesetzter Preisdistanz, werden Sie nur einen Alarm erhalten, wenn ein Angebot veröffentlicht wird, das die Bedingungen erfüllt (oder übertrifft). Beispiel: Sie möchten XMR verkaufen, aber Sie werden nur 2% über dem momentanen Marktpreis verkaufen. Dieses Feld auf 2% setzen stellt sicher, dass Sie nur nur Alarme für Angebote erhalten, die 2% (oder mehr) über dem momentanen Marktpreis liegen. account.notifications.marketAlert.trigger.prompt=Prozentualer Abstand zum Marktpreis (z.B. 2.50%, -0.50%, etc) account.notifications.marketAlert.addButton=Angebotsalarme hinzufügen account.notifications.marketAlert.manageAlertsButton=Angebotsalarme verwalten account.notifications.marketAlert.manageAlerts.title=Angebotsalarme verwalten account.notifications.marketAlert.manageAlerts.header.paymentAccount=Zahlungskonto account.notifications.marketAlert.manageAlerts.header.trigger=Triggerpreis account.notifications.marketAlert.manageAlerts.header.offerType=Angebotstyp account.notifications.marketAlert.message.title=Angebotsalarm account.notifications.marketAlert.message.msg.below=unter account.notifications.marketAlert.message.msg.above=über account.notifications.marketAlert.message.msg=Eine neue "{0} {1}" Angebote mit {2} ({3} {4} Marktpreis) und Zahlungsmethode "{5}" wurde zum Haveno Angebotsbuch.\nAngebot ID: {6}. account.notifications.priceAlert.message.title=Preisalarm für {0} account.notifications.priceAlert.message.msg=Ihr Preisalarm wurde ausgelöst. Der momentane {0} Preis ist {1} {2} account.notifications.noWebCamFound.warning=Keine Webcam gefunden.\n\nBitte nutzen Sie die E-Mail Option um den Token und Schlüssel von Ihrem Smartphone zur Haveno-Anwendung zu senden. account.notifications.priceAlert.warning.highPriceTooLow=Der hohe Preis muss größer als der tiefe Preis sein. account.notifications.priceAlert.warning.lowerPriceTooHigh=Der tiefe Preis muss kleiner als der hohe Preis sein. #################################################################### # Windows #################################################################### inputControlWindow.headline=Inputs für die Transaktion auswählen inputControlWindow.balanceLabel=Verfügbarer Betrag contractWindow.title=Konfliktdetails contractWindow.dates=Angebotsdatum / Handelsdatum contractWindow.xmrAddresses=Moneroadresse XMR-Käufer / XMR-Verkäufer contractWindow.onions=Netzwerkadresse XMR-Käufer / XMR-Verkäufer contractWindow.accountAge=Kontoalter XMR Käufer / XMR Verkäufer contractWindow.numDisputes=Anzahl Konflikte XMR-Käufer / XMR-Verkäufer contractWindow.contractHash=Vertrags-Hash displayAlertMessageWindow.headline=Wichtige Informationen! displayAlertMessageWindow.update.headline=Wichtige Update Informationen! displayAlertMessageWindow.update.download=Download: displayUpdateDownloadWindow.downloadedFiles=Dateien: displayUpdateDownloadWindow.downloadingFile=Lade herunter: {0} displayUpdateDownloadWindow.verifiedSigs=Signatur mit Schlüsseln überprüft: displayUpdateDownloadWindow.status.downloading=Lade Dateien herunter... displayUpdateDownloadWindow.status.verifying=Überprüfe Signatur... displayUpdateDownloadWindow.button.label=Installer herunterladen und Signatur überprüfen displayUpdateDownloadWindow.button.downloadLater=Später herunterladen displayUpdateDownloadWindow.button.ignoreDownload=Diese Version ignorieren displayUpdateDownloadWindow.headline=Ein neues Haveno-Update ist verfügbar! displayUpdateDownloadWindow.download.failed.headline=Download fehlgeschlagen displayUpdateDownloadWindow.download.failed=Download fehlgeschlagen.\nBitte downloaden Sie und verifizieren Sie manuell unter [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.installer.failed=Unfähig den richtigen Installer zu bestimmen. Bitte downloaden und verifizieren Sie manuell unter [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.verify.failed=Verifikation fehlgeschlagen.\nBitte downloaden Sie und verifizieren Sie manuell unter [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.success=Die neue Version wurde erfolgreich heruntergeladen und die Signatur überprüft.\n\nBitte öffnen Sie das Downloadverzeichnis, schließen die Anwendung und installieren die neue Version. displayUpdateDownloadWindow.download.openDir=Downloadverzeichnis öffnen disputeSummaryWindow.title=Zusammenfassung disputeSummaryWindow.openDate=Erstellungsdatum des Tickets disputeSummaryWindow.role=Rolle des Händlers disputeSummaryWindow.payout=Auszahlung des Handelsbetrags disputeSummaryWindow.payout.getsTradeAmount=Der XMR-{0} erhält die Auszahlung des Handelsbetrags disputeSummaryWindow.payout.getsAll=Menge in XMR zu {0} disputeSummaryWindow.payout.custom=Spezifische Auszahlung disputeSummaryWindow.payoutAmount.buyer=Auszahlungsbetrag des Käufers disputeSummaryWindow.payoutAmount.seller=Auszahlungsbetrag des Verkäufers disputeSummaryWindow.payoutAmount.invert=Verlierer als Veröffentlicher nutzen disputeSummaryWindow.reason=Grund des Konflikts disputeSummaryWindow.tradePeriodEnd=Trade period end disputeSummaryWindow.extraInfo=Extra information disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BUG=Fehler # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.USABILITY=Benutzerfreundlichkeit # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Protokollverletzung # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.NO_REPLY=Keine Antwort # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SCAM=Betrug # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OTHER=Andere # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BANK_PROBLEMS=Bank # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OPTION_TRADE=Options-Handel # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Trader antwortet nicht # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Falsches Sender-Konto # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PEER_WAS_LATE=Peer war zu spät # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade wurde bereits festgelegt. disputeSummaryWindow.summaryNotes=Zusammenfassende Anmerkungen disputeSummaryWindow.addSummaryNotes=Zusammenfassende Anmerkungen hinzufügen disputeSummaryWindow.close.button=Ticket schließen # Do no change any line break or order of tokens as the structure is used for signature verification # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.msg=Ticket geschlossen am {0}\n{1}Node Adresse: {2}\n\nZusammenfassung:\nTrade ID: {3}\nWährung: {4}\nTrade-Betrag: {5}\nAuszahlungsbetrag für den XMR Käufer: {6}\nAuszahlungsbetrag für den XMR Verkäufer: {7}\n\nGrund für den Konflikt: {8}\n\nWeitere Hinweise:\n{9}\n # Do no change any line break or order of tokens as the structure is used for signature verification disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3} disputeSummaryWindow.close.nextStepsForMediation=\nNächster Schritt:\nTrade öffnen und Vorschlag des Mediators akzeptieren oder ablehnen. disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNächster Schritt:\nSie müssen nichts weiteres machen. Wenn der Arbitrator in Ihrem Vorteil entscheidet, sehen Sie eine "Rückerstattung des Arbitratos"-Transaktion in Ihren Funds/Transaktionen disputeSummaryWindow.close.closePeer=Sie müssen auch das Ticket des Handelspartners schließen! disputeSummaryWindow.close.txDetails.headline=Rückerstattungstransaktion veröffentlichen # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.buyer=Käufer erhält {0} an Adresse: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=Verkäufer erhält {0} an Adresse: {1}\n disputeSummaryWindow.close.txDetails=Ausgaben: {0}\n{1}{2}Transaktionsgebühr: {3}\n\nSind Sie sicher, dass Sie diese Transaktion veröffentlichen möchten? disputeSummaryWindow.close.noPayout.headline=Ohne Auszahlung schließen disputeSummaryWindow.close.noPayout.text=Wollen Sie schließen ohne eine Auszahlung zu tätigen? emptyWalletWindow.headline={0} Notfall-Wallets-Werkzeug emptyWalletWindow.info=Bitte nur in Notfällen nutzen, wenn Sie vom UI aus nicht auf Ihre Gelder zugreifen können.\n\nBeachten Sie bitte, dass alle offenen Angebote geschlossen werden, wenn Sie dieses Werkzeug verwenden.\n\nErstellen Sie ein Backup Ihres Dateiverzeichnisses, bevor Sie dieses Werkzeug verwenden. Dies können Sie unter \"Konto/Backup\" tun.\n\nBitte melden Sie uns das Problem und erstellen Sie einen Fehlerbericht auf GitHub oder im Haveno-Forum, damit wir feststellen können, was das Problem verursacht hat. emptyWalletWindow.balance=Ihr verfügbares Wallets-Guthaben emptyWalletWindow.address=Ihre Zieladresse emptyWalletWindow.button=Alle Gelder senden emptyWalletWindow.openOffers.warn=Sie haben offene Angebote, die entfernt werden, wenn Sie die Wallet leeren.\nSind Sie sicher, dass Sie Ihre Wallet leeren wollen? emptyWalletWindow.openOffers.yes=Ja, ich bin sicher. emptyWalletWindow.sent.success=Das Guthaben Ihrer Wallet wurde erfolgreich überwiesen. enterPrivKeyWindow.headline=Private Key für die Registrierung eingeben filterWindow.headline=Filterliste bearbeiten filterWindow.offers=Herausgefilterte Angebote (durch Kommas getrennt) filterWindow.onions=Vom Handel ausgeschlossene Adressen (Komma getr.) filterWindow.bannedFromNetwork=Vom Netzwerk ausgeschlossene Adressen (Komma getr.) filterWindow.accounts=Herausgefilterte Handelskonten Daten:\nFormat: Komma getrennte Liste von [Zahlungsmethoden ID | Datenfeld | Wert] filterWindow.bannedCurrencies=Herausgefilterte Währungscodes (durch Kommas getrennt) filterWindow.bannedPaymentMethods=Herausgefilterte Zahlungsmethoden-IDs (durch Kommas getrennt) filterWindow.bannedAccountWitnessSignerPubKeys=Gefilterte Pub Keys von unterzeichnenden Kontozeugen (durch Komma getrennte Hex der Pub Keys) filterWindow.bannedPrivilegedDevPubKeys=Gefilterte privilegierte Dev Pub Keys (durch Komma getrennte Hex der Pub Keys) filterWindow.arbitrators=Gefilterte Vermittler (mit Komma getr. Onion-Adressen) filterWindow.mediators=Gefilterte Mediatoren (mit Komma getr. Onion-Adressen) filterWindow.refundAgents=Gefilterte Rückerstattungsagenten (mit Komma getr. Onion-Adressen) filterWindow.seedNode=Gefilterte Seed-Knoten (Komma getr. Onion-Adressen) filterWindow.priceRelayNode=Gefilterte Preisrelais Knoten (Komma getr. Onion-Adressen) filterWindow.xmrNode=Gefilterte Moneroknoten (Komma getr. Adresse + Port) filterWindow.preventPublicXmrNetwork=Nutzung des öffentlichen Monero-Netzwerks verhindern filterWindow.disableAutoConf=Automatische Bestätigung deaktivieren filterWindow.autoConfExplorers=Gefilterter Explorer mit Auto-Bestätigung (Adressen mit Komma separiert) filterWindow.disableTradeBelowVersion=Min. zum Handeln erforderliche Version filterWindow.add=Filter hinzufügen filterWindow.remove=Filter entfernen filterWindow.xmrFeeReceiverAddresses=XMR Gebühr Empfänger-Adressen filterWindow.disableApi=API deaktivieren filterWindow.disableMempoolValidation=Disable Mempool Validation offerDetailsWindow.minXmrAmount=Min. XMR-Betrag offerDetailsWindow.min=(min. {0}) offerDetailsWindow.distance=(Abstand zum Marktpreis: {0}) offerDetailsWindow.myTradingAccount=Mein Handelskonto offerDetailsWindow.offererBankId=(Erstellers Bank ID/BIC/SWIFT) offerDetailsWindow.offerersBankName=(Bankname des Erstellers) offerDetailsWindow.bankId=Bankkennung (z.B. BIC oder SWIFT) offerDetailsWindow.countryBank=Land der Bank des Erstellers offerDetailsWindow.commitment=Verpflichtung offerDetailsWindow.agree=Ich stimme zu offerDetailsWindow.tac=Geschäftsbedingungen offerDetailsWindow.confirm.maker.buy=Bestätigen: Angebot zum Kauf von XMR mit {0} erstellen offerDetailsWindow.confirm.maker.sell=Bestätigen: Angebot zum Verkauf von XMR für {0} erstellen offerDetailsWindow.confirm.taker.buy=Bestätigen: Angebot zum Kauf von XMR mit {0} annehmen offerDetailsWindow.confirm.taker.sell=Bestätigen: Angebot zum Verkauf von XMR für {0} annehmen offerDetailsWindow.creationDate=Erstellungsdatum offerDetailsWindow.makersOnion=Onion-Adresse des Erstellers offerDetailsWindow.challenge=Angebots-Passphrase offerDetailsWindow.challenge.copy=Passphrase kopieren, um sie mit Ihrem Handelspartner zu teilen qRCodeWindow.headline=QR Code qRCodeWindow.msg=Bitte nutzen Sie diesen QR Code um Ihr Haveno Wallet von Ihrem externen Wallet aufzuladen. qRCodeWindow.request=Zahlungsanfrage:\n{0} selectDepositTxWindow.headline=Kautionstransaktion für Konflikt auswählen selectDepositTxWindow.msg=Die Kautionstransaktion wurde nicht im Handel gespeichert.\nBitte wählen Sie die existierenden MultiSig-Transaktionen aus Ihrer Wallet, die die Kautionstransaktion für den fehlgeschlagenen Handel war.\n\nSie können die korrekte Transaktion finden, indem Sie das Handelsdetail-Fenster öffnen (klicken Sie auf die Handels-ID in der Liste) und dem Transaktions-Output der Handelsgebührenzahlung zur nächsten Transaktion folgen, wo Sie die MultiSig-Kautionstransaktion sehen (die Adresse beginnt mit einer 3). Diese Transaktions-ID sollte in der dargestellten Liste auftauchen. Sobald Sie die korrekte Transaktion gefunden haben, wählen Sie diese Transaktion hier aus und fahren fort.\n\nEntschuldigen Sie die Unannehmlichkeiten, aber dieser Fehler sollte sehr selten auftreten und wir werden in Zukunft versuchen bessere Wege zu finden, ihn zu lösen. selectDepositTxWindow.select=Kautionstransaktion auswählen sendAlertMessageWindow.headline=Globale Benachrichtigung senden sendAlertMessageWindow.alertMsg=Warnmeldung sendAlertMessageWindow.enterMsg=Nachricht eingeben sendAlertMessageWindow.isSoftwareUpdate=Software Download Benachrichtigung sendAlertMessageWindow.isUpdate=Ist voller Release sendAlertMessageWindow.isPreRelease=Ist Pre-Release sendAlertMessageWindow.version=Neue Versionsnummer sendAlertMessageWindow.send=Benachrichtigung senden sendAlertMessageWindow.remove=Benachrichtigung entfernen sendPrivateNotificationWindow.headline=Private Nachricht senden sendPrivateNotificationWindow.privateNotification=Private Benachrichtigung sendPrivateNotificationWindow.enterNotification=Benachrichtigung eingeben sendPrivateNotificationWindow.send=Private Benachrichtigung senden showWalletDataWindow.walletData=Wallet-Daten showWalletDataWindow.includePrivKeys=Private Schlüssel einbeziehen setXMRTxKeyWindow.headline=Senden der XMR beweisen setXMRTxKeyWindow.note=Hinzufügen der Transaktionsinfo unten aktiviert die automatische Bestätigung für schneller Trades. Mehr lesen: https://haveno.exchange/wiki/Trading_Monero setXMRTxKeyWindow.txHash=ID der Transaktion (optional) setXMRTxKeyWindow.txKey=Transaktionsschlüssel (optional) # We do not translate the tac because of the legal nature. We would need translations checked by lawyers # in each language which is too expensive atm. tacWindow.headline=Nutzervereinbarung tacWindow.agree=Ich stimme zu tacWindow.disagree=Ich stimme nicht zu und beende tacWindow.arbitrationSystem=Streitbeilegung tradeDetailsWindow.headline=Handel tradeDetailsWindow.disputedPayoutTxId=Transaktions-ID der strittigen Auszahlung tradeDetailsWindow.tradeDate=Handelsdatum tradeDetailsWindow.txFee=Mining-Gebühr tradeDetailsWindow.tradePeersOnion=Onion-Adresse des Handelspartners tradeDetailsWindow.tradePeersPubKeyHash=Trading Peers Pubkey Hash tradeDetailsWindow.tradeState=Handelsstatus tradeDetailsWindow.agentAddresses=Vermittler/Mediator tradeDetailsWindow.detailData=Detaillierte Daten txDetailsWindow.headline=Transaktionsdetails txDetailsWindow.xmr.noteSent=Sie haben XMR gesendet. txDetailsWindow.xmr.noteReceived=Sie haben XMR erhalten. txDetailsWindow.sentTo=Gesendet an txDetailsWindow.receivedWith=Erhalten mit txDetailsWindow.txId=TxId closedTradesSummaryWindow.headline=Trade history summary closedTradesSummaryWindow.totalAmount.title=Total trade amount closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) closedTradesSummaryWindow.totalTradeFeeInXmr.title=Sum of all trade fees paid in XMR closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} of total trade amount) walletPasswordWindow.headline=Passwort zum Entsperren eingeben xmrConnectionError.headline=Monero-Verbindungsfehler xmrConnectionError.providedNodes=Fehler beim Verbinden mit den angegebenen Monero-Knoten.\n\nMöchten Sie den nächstbesten verfügbaren Monero-Knoten verwenden? xmrConnectionError.customNodes=Fehler beim Verbinden mit Ihren benutzerdefinierten Monero-Knoten.\n\nMöchten Sie den nächstbesten verfügbaren Monero-Knoten verwenden? xmrConnectionError.localNode=Haveno war zuvor mit einem lokalen Monero-Knoten verbunden, dieser ist jedoch nicht mehr erreichbar.\n\nStellen Sie sicher, dass Ihr lokaler Knoten ausgeführt wird und vollständig synchronisiert ist, oder wählen Sie eine andere Option, um fortzufahren. xmrConnectionError.localNode.start=Lokalen Knoten starten xmrConnectionError.localNode.start.error=Fehler beim Starten des lokalen Knotens xmrConnectionError.localNode.fallback=Mit nächstbestem Knoten verbinden torNetworkSettingWindow.header=Tor-Netzwerkeinstellungen torNetworkSettingWindow.noBridges=Keine Bridges verwenden torNetworkSettingWindow.providedBridges=Mit bereitgestellten Bridges verbinden torNetworkSettingWindow.customBridges=Benutzerdefinierte Bridges eingeben torNetworkSettingWindow.transportType=Transport-Typ torNetworkSettingWindow.obfs3=obfs3 torNetworkSettingWindow.obfs4=obfs4 (empfohlen) torNetworkSettingWindow.meekAmazon=meek-amazon torNetworkSettingWindow.meekAzure=meek-azure torNetworkSettingWindow.enterBridge=Geben Sie ein oder mehrere Bridge-Relays ein (eine pro Zeile) torNetworkSettingWindow.enterBridgePrompt=Adresse:Port eingeben torNetworkSettingWindow.restartInfo=Sie müssen neu starten, um die Änderungen anzuwenden torNetworkSettingWindow.openTorWebPage=Tor-Projekt-Webseite öffnen torNetworkSettingWindow.deleteFiles.header=Verbindungsprobleme? torNetworkSettingWindow.deleteFiles.info=Wenn beim Start häufig Probleme mit der Verbindung auftreten, kann das Löschen veralteter Tor-Dateien hilfreich sein. Dazu klicken Sie auf die Schaltfläche unten und starten danach neu. torNetworkSettingWindow.deleteFiles.button=Lösche veraltete Tor-Dateien und fahre herunter torNetworkSettingWindow.deleteFiles.progress=Tor wird herunterfahren torNetworkSettingWindow.deleteFiles.success=Veraltete Tor-Dateien erfolgreich gelöscht. Bitte neu starten. torNetworkSettingWindow.bridges.header=Ist Tor blockiert? torNetworkSettingWindow.bridges.info=Falls Tor von Ihrem Provider oder in Ihrem Land blockiert wird, können Sie versuchen Tor-Bridges zu nutzen.\nBesuchen Sie die Tor-Webseite unter: https://bridges.torproject.org um mehr über Bridges und pluggable transposrts zu lernen. feeOptionWindow.headline=Währung für Handelsgebührzahlung auswählen feeOptionWindow.info=Sie können wählen, die Gebühr in BSQ oder XMR zu zahlen. Wählen Sie BSQ, erhalten Sie eine vergünstigte Handelsgebühr. feeOptionWindow.optionsLabel=Währung für Handelsgebührzahlung auswählen feeOptionWindow.useXMR=XMR nutzen feeOptionWindow.fee={0} (≈ {1}) feeOptionWindow.xmrFeeWithFiatAndPercentage={0} (≈ {1} / {2}) feeOptionWindow.xmrFeeWithPercentage={0} ({1}) #################################################################### # Popups #################################################################### popup.headline.notification=Benachrichtigung popup.headline.instruction=Bitte beachten Sie: popup.headline.attention=Achtung popup.headline.backgroundInfo=Hintergrundinformation popup.headline.feedback=Abgeschlossen popup.headline.confirmation=Bestätigung popup.headline.information=Information popup.headline.warning=Warnung popup.headline.error=Fehler popup.doNotShowAgain=Nicht erneut anzeigen popup.reportError.log=Protokolldatei öffnen popup.reportError.gitHub=Auf GitHub-Issue-Tracker melden popup.reportError={0}\n\nUm uns bei der Verbesserung der Software zu helfen, erstellen Sie bitte einen Fehler-Bericht auf https://github.com/haveno-dex/haveno/issues.\nDie Fehlermeldung wird in die Zwischenablage kopiert, wenn Sie auf einen der Knöpfe unten klicken.\nEs wird das Debuggen einfacher machen, wenn Sie die haveno.log Datei anfügen indem Sie "Logdatei öffnen" klicken, eine Kopie speichern und diese dem Fehler-Bericht anfügen. popup.error.tryRestart=Versuchen Sie bitte Ihre Anwendung neu zu starten und überprüfen Sie Ihre Netzwerkverbindung um zu sehen, ob Sie das Problem beheben können. popup.error.takeOfferRequestFailed=Es ist ein Fehler aufgetreten, als jemand versuchte eins Ihrer Angebote anzunehmen:\n{0} error.spvFileCorrupted=Beim Einlesen der SPV-Kettendatei ist ein Fehler aufgetreten.\nDie SPV-Kettendatei ist möglicherweise beschädigt.\n\nFehlermeldung: {0}\n\nMöchten Sie diese löschen und neu synchronisieren? error.deleteAddressEntryListFailed=Konnte AddressEntryList-Datei nicht löschen.\nFehler: {0} error.closedTradeWithUnconfirmedDepositTx=Die Einzahlungstransaktion des geschlossenen Trades mit der Trade ID {0} ist noch immer unbestätigt.\n\nBitte führen Sie eine SPV-Resynchronisation unter \" Einstellungen/Netzwerkinformationen\" durch, um zu sehen, ob die Transaktion gültig ist. error.closedTradeWithNoDepositTx=Die Einzahlungstransaktion des geschlossenen Trades mit der Trade-ID {0} ist null.\n\nBitte starten Sie die Anwendung neu, um die Liste der geschlossenen Trades zu bereinigen. popup.warning.walletNotInitialized=Die Wallet ist noch nicht initialisiert popup.warning.osxKeyLoggerWarning=Aufgrund strengerer Sicherheitsmaßnahmen ab MacOS 10.14 führt der Start einer Java-Anwendung (Haveno verwendet Java) zu einer Popup-Warnung in MacOS ("Haveno möchte Tastenanschläge von einer Anwendung empfangen").\n\nUm dieses Problem zu vermeiden, öffnen Sie bitte Ihre 'macOS-Einstellungen' und gehen Sie zu 'Sicherheit & Datenschutz' -> 'Datenschutz' -> 'Eingabe-Überwachung' und entfernen Sie 'Haveno' aus der Liste auf der rechten Seite.\n\nHaveno wird auf eine neuere Java-Version upgraden, um dieses Problem zu vermeiden, sobald die technischen Einschränkungen (Java-Packager für die benötigte Java-Version wird noch nicht ausgeliefert) behoben sind. popup.warning.wrongVersion=Sie verwenden vermutlich die falsche Haveno-Version für diesen Computer.\nDie Architektur Ihres Computers ist: {0}.\nDie installierten Haveno-Binärdateien sind: {1}.\nBitte fahren Sie Haveno herunter und installieren die korrekte Version ({2}). popup.warning.incompatibleDB=Wir haben inkompatible Datenbankdateien entdeckt!\n\nDiese Datenbankdatei(en) ist (sind) nicht kompatibel mit unserer aktuellen Code-Basis:\n{0}\n\nWir haben ein Backup der beschädigten Datei(en) erstellt und die Standardwerte auf eine neue Datenbankversion angewendet.\n\nDas Backup befindet sich unter:\n{1}/db/backup_of_corrupted_data.\n\nBitte prüfen Sie, ob Sie die neueste Version von Haveno installiert haben.\nSie können sie herunterladen unter: [HYPERLINK:https://haveno.exchange/downloads].\n\nBitte starten Sie die Anwendung neu. popup.warning.startupFailed.twoInstances=Haveno läuft bereits. Sie können nicht zwei Instanzen von Haveno laufen lassen. popup.warning.tradePeriod.halfReached=Ihr Trade mit der ID {0} hat die Hälfte der maximal erlaubten Trade-Periode erreicht und ist immer noch nicht abgeschlossen.\n\nDie Trade-Periode endet am {1}\n\nBitte überprüfen Sie den Status Ihres Trades unter \"Portfolio/Offene Trades\" für weitere Informationen. popup.warning.tradePeriod.ended=Ihr Trade mit der ID {0} hat die maximal zulässige Trade-Periode erreicht und ist nicht abgeschlossen.\n\nDie Trade-Periode endete am {1}.\n\nBitte überprüfen Sie Ihren Trade unter \"Portfolio/Offene Trades\", um den Mediator zu kontaktieren. popup.warning.noTradingAccountSetup.headline=Sie haben kein Handelskonto eingerichtet popup.warning.noTradingAccountSetup.msg=Sie müssen ein nationales Währung- oder Crypto-Konto einrichten, bevor Sie ein Angebot erstellen können.\nMöchten Sie ein Konto einrichten? popup.warning.noArbitratorsAvailable=Momentan sind keine Vermittler verfügbar. popup.warning.noMediatorsAvailable=Es sind keine Mediatoren verfügbar. popup.warning.notFullyConnected=Sie müssen warten, bis Sie vollständig mit dem Netzwerk verbunden sind.\nDas kann bis ungefähr 2 Minuten nach dem Start dauern. popup.warning.notSufficientConnectionsToXmrNetwork=Sie müssen warten, bis Sie wenigstens {0} Verbindungen zum Moneronetzwerk haben. popup.warning.downloadNotComplete=Sie müssen warten bis der Download der fehlenden Moneroblöcke abgeschlossen ist. popup.warning.walletNotSynced=Die Haveno-Brieftasche ist nicht mit der neuesten Höhe der Blockchain synchronisiert. Bitte warten Sie, bis die Brieftasche synchronisiert ist, oder überprüfen Sie Ihre Verbindung. popup.warning.removeOffer=Sind Sie sicher, dass Sie das Angebot entfernen wollen? popup.warning.tooLargePercentageValue=Es kann kein Prozentsatz von 100% oder mehr verwendet werden. popup.warning.examplePercentageValue=Bitte geben sei einen Prozentsatz wie folgt ein \"5.4\" für 5.4% popup.warning.noPriceFeedAvailable=Es ist kein Marktpreis für diese Währung verfügbar. Sie können keinen auf Prozent basierenden Preis verwenden.\nBitte wählen Sie den Festpreis. popup.warning.sendMsgFailed=Das Senden der Nachricht an Ihren Handelspartner ist fehlgeschlagen.\nVersuchen Sie es bitte erneut und falls es weiter fehlschlägt, erstellen Sie bitte einen Fehlerbericht. popup.warning.messageTooLong=Ihre Nachricht überschreitet die maximal erlaubte Größe. Sende Sie diese in mehreren Teilen oder laden Sie sie in einen Dienst wie https://pastebin.com hoch. popup.warning.lockedUpFunds=Sie haben gesperrtes Guthaben aus einem gescheiterten Trade.\nGesperrtes Guthaben: {0} \nEinzahlungs-Tx-Adresse: {1}\nTrade ID: {2}.\n\nBitte öffnen Sie ein Support-Ticket, indem Sie den Trade im Bildschirm "Offene Trades" auswählen und auf \"alt + o\" oder \"option + o\" drücken. popup.warning.moneroConnection=Es gab ein Problem bei der Verbindung zum Monero-Netzwerk.\n\n{0} popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n takeOffer.cancelButton=Cancel take-offer takeOffer.warningButton=Ignorieren und fortfahren # suppress inspection "UnusedProperty" popup.warning.nodeBanned=Einer der {0} Nodes wurde gebannt. # suppress inspection "UnusedProperty" popup.warning.priceRelay=Preisrelais popup.warning.seed=Seed popup.warning.mandatoryUpdate.trading=Bitte aktualisieren Sie auf die neueste Haveno-Version. Es wurde ein obligatorisches Update veröffentlicht, das den Handel mit alten Versionen deaktiviert. Bitte besuchen Sie das Haveno-Forum für weitere Informationen. popup.warning.noFilter=Wir haben kein Filterobjekt von den Seed-Knoten erhalten. Bitte informieren Sie die Netzwerkadministratoren, ein Filterobjekt zu registrieren. popup.warning.burnXMR=Die Transaktion ist nicht möglich, da die Mininggebühren von {0} den übertragenen Betrag von {1} überschreiten würden. Bitte warten Sie, bis die Gebühren wieder niedrig sind, oder Sie mehr XMR zum übertragen angesammelt haben. popup.warning.openOffer.makerFeeTxRejected=Die Verkäufergebühren-Transaktion für das Angebot mit der ID {0} wurde vom Monero-Netzwerk abgelehnt.\nTransaktions-ID={1}.\nDas Angebot wurde entfernt, um weitere Probleme zu vermeiden.\nBitte gehen Sie zu \"Einstellungen/Netzwerkinformationen\" und führen Sie eine SPV-Resynchronisierung durch.\nFür weitere Hilfe wenden Sie sich bitte an den Haveno-Support-Kanal des Haveno Keybase Teams. popup.warning.trade.txRejected.tradeFee=Trade-Gebühr popup.warning.trade.txRejected.deposit=Kaution popup.warning.trade.txRejected=Die {0} Transaktion für den Trade mit der ID {1} wurde vom Monero-Netzwerk abgelehnt.\nTransaktions-ID={2}}\nDer Trade wurde in gescheiterte Trades verschoben.\nBitte gehen Sie zu \"Einstellungen/Netzwerkinformationen\" und führen Sie einen SPV Resync durch.\nFür weitere Hilfe wenden Sie sich bitte an den Haveno-Support-Kanal des Haveno Keybase Teams. popup.warning.openOfferWithInvalidMakerFeeTx=Die Verkäufergebühren-Transaktion für das Angebot mit der ID {0} ist ungültig.\nTransaktions-ID={1}.\nBitte gehen Sie zu \"Einstellungen/Netzwerkinformationen\" und führen Sie eine SPV-Resynchronisierung durch.\nFür weitere Hilfe wenden Sie sich bitte an den Haveno-Support-Kanal des Haveno Keybase Teams. popup.info.securityDepositInfo=Um sicherzustellen, dass beide Händler dem Handelsprotokoll folgen, müssen diese eine Kaution zahlen.\n\nDie Kaution bleibt in Ihrer lokalen Wallet, bis das Angebot von einem anderen Händler angenommen wurde.\nSie wird Ihnen zurückerstattet, nachdem der Handel erfolgreich abgeschlossen wurde.\n\nBitte beachten Sie, dass Sie die Anwendung laufen lassen müssen, wenn Sie ein offenes Angebot haben.\nWenn ein anderer Händler Ihr Angebot annehmen möchte ist es notwendig, dass Ihre Anwendung online ist und reagieren kann.\nStellen Sie sicher, dass Sie den Ruhezustand deaktiviert haben, da dieser Ihren Client vom Netzwerk trennen würde (Der Ruhezustand des Monitors ist kein Problem). popup.info.cashDepositInfo=Stellen Sie sicher, dass eine Bank-Filiale in Ihrer Nähe befindet, um die Bargeld Kaution zu zahlen.\nDie Bankkennung (BIC/SWIFT) der Bank des Verkäufers ist: {0}. popup.info.cashDepositInfo.confirm=Ich bestätige, dass ich die Kaution zahlen kann popup.info.shutDownWithOpenOffers=Haveno wird heruntergefahren, aber Sie haben offene Angebote verfügbar.\n\nDiese Angebote sind nach dem Herunterfahren nicht mehr verfügbar und werden erneut im P2P-Netzwerk veröffentlicht wenn Sie das nächste Mal Haveno starten.\n\nLassen Sie Haveno weiter laufen und stellen Sie sicher, dass Ihr Computer online bleibt, um Ihre Angebote verfügbar zu halten (z.B.: verhindern Sie den Standby-Modus... der Standby-Modus des Monitors stellt kein Problem dar). popup.info.qubesOSSetupInfo=Es scheint so als ob Sie Haveno auf Qubes OS laufen haben.\n\nBitte stellen Sie sicher, dass Haveno qube nach unserem Setup Guide eingerichtet wurde: [HYPERLINK:https://haveno.exchange/wiki/Running_Haveno_on_Qubes]. popup.warn.downGradePrevention=Downgrade von Version {0} auf Version {1} wird nicht unterstützt. Bitte nutzen Sie die aktuelle Haveno Version. popup.privateNotification.headline=Wichtige private Benachrichtigung! popup.securityRecommendation.headline=Wichtige Sicherheitsempfehlung popup.securityRecommendation.msg=Wir würden Sie gerne daran erinnern, sich zu überlegen, den Passwortschutz Ihrer Wallet zu verwenden, falls Sie diesen noch nicht aktiviert haben.\n\nEs wird außerdem dringend empfohlen, dass Sie die Wallet-Seed-Wörter aufschreiben. Diese Seed-Wörter sind wie ein Master-Passwort zum Wiederherstellen ihrer Monero-Wallet.\nIm \"Wallet-Seed\"-Abschnitt finden Sie weitere Informationen.\n\nZusätzlich sollten Sie ein Backup des ganzen Anwendungsdatenordners im \"Backup\"-Abschnitt erstellen. popup.xmrLocalNode.msg=Haveno hat einen Monero-Knoten entdeckt, der auf diesem Rechner (auf localhost) läuft.\n\nBitte stellen Sie sicher, dass der Knoten vollständig synchronisiert ist, bevor Sie Haveno starten. popup.shutDownInProgress.headline=Anwendung wird heruntergefahren popup.shutDownInProgress.msg=Das Herunterfahren der Anwendung kann einige Sekunden dauern.\nBitte unterbrechen Sie diesen Vorgang nicht. popup.attention.forTradeWithId=Der Handel mit der ID {0} benötigt Ihre Aufmerksamkeit popup.attention.reasonForPaymentRuleChange=Version 1.5.5 bringt eine gravierende Änderung der Trading Regeln zum Thema \"Zahlungsgrund\" bei Banküberweisungen mit sich. Bitte lassen Sie dieses Feld immer leer -- fügen Sie NICHT MEHR die Trade-ID als \"Zahlungsgrund\" an. popup.info.multiplePaymentAccounts.headline=Mehrere Zahlungskonten verfügbar popup.info.multiplePaymentAccounts.msg=Für dieses Angebot stehen Ihnen mehrere Zahlungskonten zur Verfügung. Bitte stellen Sie sicher, dass Sie das richtige ausgewählt haben. popup.accountSigning.selectAccounts.headline=Zahlungskonten auswählen popup.accountSigning.selectAccounts.description=Basierend auf der Zahlungsmethode und dem Zeitpunkt werden alle Zahlungskonten, die mit einem Konfliktfall verbunden sind, bei dem eine Auszahlung an den Käufer erfolgt ist, zur Unterzeichnung ausgewählt. popup.accountSigning.selectAccounts.signAll=Alle Zahlungsmethoden unterzeichnen popup.accountSigning.selectAccounts.datePicker=Zeitpunkt wählen, bis zu dem die Konten unterzeichnet werden sollen popup.accountSigning.confirmSelectedAccounts.headline=Ausgewählte Zahlungskonten bestätigen popup.accountSigning.confirmSelectedAccounts.description=Basierend auf Ihren Eingaben werden {0} Zahlungskonten ausgewählt. popup.accountSigning.confirmSelectedAccounts.button=Zahlungskonten bestätigen popup.accountSigning.signAccounts.headline=Unterzeichnung der Zahlungskonten bestätigen popup.accountSigning.signAccounts.description=Basierend auf Ihrer Auswahl werden {0} Zahlungskonten unterzeichnet. popup.accountSigning.signAccounts.button=Zahlungskonten unterzeichnen popup.accountSigning.signAccounts.ECKey=Privaten Vermittler-Schlüssel eingeben popup.accountSigning.signAccounts.ECKey.error=Ungültiger Vermittler ECKey popup.accountSigning.success.headline=Glückwunsch popup.accountSigning.success.description=Alle {0} Zahlungskonten wurden erfolgreich unterzeichnet! popup.accountSigning.generalInformation=Den Unterzeichnungsstand all Ihrer Konten finden Sie im Abschnitt Konto.\n\nFür weitere Informationen besuchen Sie bitte [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. popup.accountSigning.signedByArbitrator=Eines Ihrer Zahlungskonten wurde von einem Vermittler verifiziert und unterzeichnet. Wenn Sie mit diesem Konto traden, wird das Konto Ihres Trade-Partners nach einem erfolgreichen Trade automatisch unterzeichnet.\n\n{0} popup.accountSigning.signedByPeer=Eines Ihrer Zahlungskonten wurde von einem Trade-Partner verifiziert und unterzeichnet. Ihr anfängliches Trade-Limit wird aufgehoben und Sie können in {0} Tagen andere Konten unterzeichnen.\n\n{1} popup.accountSigning.peerLimitLifted=Das anfängliche Limit für eines Ihrer Konten wurde aufgehoben.\n\n{0} popup.accountSigning.peerSigner=Eines Ihrer Konten ist reif genug, um andere Zahlungskonten zu unterzeichnen, und das anfängliche Limit für eines Ihrer Konten wurde aufgehoben.\n\n{0} popup.accountSigning.singleAccountSelect.headline=Importiere unsigniertes Zeugnis für Kontoalter popup.accountSigning.confirmSingleAccount.headline=Ausgewähltes Zeugnis für Kontoalter bestätigen popup.accountSigning.confirmSingleAccount.selectedHash=Ausgewählter Zeugnis-Hash popup.accountSigning.confirmSingleAccount.button=Zeugnis für Kontoalter signieren popup.accountSigning.successSingleAccount.description=Zeugnis {0} wurde signiert popup.accountSigning.successSingleAccount.success.headline=Erfolg popup.accountSigning.unsignedPubKeys.headline=Nicht unterzeichnete Pubkeys popup.accountSigning.unsignedPubKeys.sign=Pubkeys unterzeichnen popup.accountSigning.unsignedPubKeys.signed=Pubkeys wurden unterzeichnet popup.accountSigning.unsignedPubKeys.result.signed=Unterzeichnete Pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Unterzeichnung fehlgeschlagen popup.info.buyerAsTakerWithoutDeposit.headline=Kein Depositum vom Käufer erforderlich popup.info.buyerAsTakerWithoutDeposit=Ihr Angebot erfordert keine Sicherheitsleistung oder Gebühr vom XMR-Käufer.\n\nUm Ihr Angebot anzunehmen, müssen Sie ein Passwort mit Ihrem Handelspartner außerhalb von Haveno teilen.\n\nDas Passwort wird automatisch generiert und nach der Erstellung in den Angebotsdetails angezeigt. #################################################################### # Notifications #################################################################### notification.trade.headline=Benachrichtigung zum Handel mit der ID {0} notification.ticket.headline=Support-Ticket für den Handel mit der ID {0} notification.trade.completed=Ihr Handel ist jetzt abgeschlossen und Sie können Ihre Gelder abheben. notification.trade.accepted=Ihr Angebot wurde von einem XMR-{0} angenommen. notification.trade.unlocked=Ihr Handel hat wenigstens eine Blockchain-Bestätigung.\nSie können die Zahlung nun beginnen. notification.trade.paymentSent=Der XMR-Käufer hat die Zahlung begonnen. notification.trade.selectTrade=Handel wählen notification.trade.peerOpenedDispute=Ihr Handelspartner hat ein/einen {0} geöffnet. notification.trade.disputeClosed=Der/Das {0} wurde geschlossen. notification.walletUpdate.headline=Update der Handels-Wallets notification.walletUpdate.msg=Ihre Handels-Wallet ist ausreichend finanziert.\nBetrag: {0} notification.takeOffer.walletUpdate.msg=Ihre Handels-Wallet wurde bereits durch eine früher versuchte Angebotsannahme ausreichend finanziert.\nBetrag: {0} notification.tradeCompleted.headline=Handel abgeschlossen notification.tradeCompleted.msg=Sie können Ihre Gelder auf eine externe Monero-Wallet abheben oder in Ihrer Haveno-Wallet behalten. #################################################################### # System Tray #################################################################### systemTray.show=Anwendungsfenster anzeigen systemTray.hide=Anwendungsfenster verstecken systemTray.info=Informationen zu Haveno systemTray.exit=Beenden systemTray.tooltip=Haveno: Ein dezentrales Monero-Tauschbörsen-Netzwerk #################################################################### # GUI Util #################################################################### guiUtil.accountExport.savedToPath=Handelskonten in Verzeichnis gespeichert:\n{0} guiUtil.accountExport.noAccountSetup=Sie haben kein Handelskonto zum Exportieren eingerichtet. guiUtil.accountExport.selectPath=Verzeichnis auswählen zum {0} # suppress inspection "TrailingSpacesInProperty" guiUtil.accountExport.tradingAccount=Handelskonto mit der ID {0}\n # suppress inspection "TrailingSpacesInProperty" guiUtil.accountImport.noImport=Wir haben das Handelskonto mit der Kennung {0} nicht importiert, da es schon existiert.\n guiUtil.accountExport.exportFailed=Export in CSV ist aufgrund eines Fehlers fehlgeschlagen.\nFehler = {0} guiUtil.accountExport.selectExportPath=Exportverzeichnis auswählen guiUtil.accountImport.imported=Handelskonto aus Verzeichnis importiert:\n{0}\n\nImportierte Konten:\n{1} guiUtil.accountImport.noAccountsFound=Keine exportierten Handelskonten im Verzeichnis gefunden: {0}.\nDateiname ist {1}." guiUtil.openWebBrowser.warning=Es wird eine Website im Webbrowser Ihres Betriebssystems geöffnet.\nMöchten Sie die Website jetzt öffnen?\n\nWenn Sie nicht den \"Torbrowser\" als Standardbrowser verwenden, werden Sie sich über das Clearnet mit der Website verbinden.\n\nURL:\"{0}\" guiUtil.openWebBrowser.doOpen=Website öffnen und nicht erneut fragen guiUtil.openWebBrowser.copyUrl=URL kopieren und abbrechen guiUtil.ofTradeAmount=vom Handelsbetrag guiUtil.requiredMinimum=(erforderliches Minimum) #################################################################### # Component specific #################################################################### list.currency.select=Währung wählen list.currency.showAll=Alle anzeigen list.currency.editList=Währungsliste bearbeiten table.placeholder.noItems=Momentan sind keine {0} verfügbar table.placeholder.noData=Momentan sind keine Daten verfügbar table.placeholder.processingData=Datenverarbeitung... peerInfoIcon.tooltip.tradePeer=Handelspartners peerInfoIcon.tooltip.maker=Erstellers peerInfoIcon.tooltip.trade.traded={0} Onion-Adresse: {1}\nSie haben schon {2} Mal(e) mit diesem Partner gehandelt.\n{3} peerInfoIcon.tooltip.trade.notTraded={0} Onion-Adresse: {1}\nSie haben noch nicht mit diesem Partner gehandelt.\n{2} peerInfoIcon.tooltip.age=Zahlungskonto vor {0} erstellt. peerInfoIcon.tooltip.unknownAge=Alter des Zahlungskontos unbekannt. tooltip.openPopupForDetails=Dialogfenster für Details öffnen tooltip.invalidTradeState.warning=Dieser Trade hat einen ungültigen Status. Öffnen Sie das Detail-Fenster für weitere Informationen. tooltip.openBlockchainForAddress=Externen Blockchain-Explorer für Adresse öffnen: {0} tooltip.openBlockchainForTx=Externen Blockchain-Explorer für Transaktion öffnen: {0} confidence.unknown=Unbekannter Transaktionsstatus confidence.seen=Von {0} Peer(s) gesehen / 0 Bestätigungen confidence.confirmed={0} Bestätigung(en) confidence.invalid=Die Transaktion ist ungültig peerInfo.title=Peer-Infos peerInfo.nrOfTrades=Anzahl abgeschlossener Trades peerInfo.notTradedYet=Sie haben noch nicht mit diesem Nutzer gehandelt. peerInfo.setTag=Markierung für diesen Peer setzen peerInfo.age.noRisk=Alter des Zahlungskontos peerInfo.age.chargeBackRisk=Zeit seit der Unterzeichnung peerInfo.unknownAge=Alter unbekannt addressTextField.openWallet=Ihre Standard-Monero-Wallet öffnen addressTextField.copyToClipboard=Adresse in Zwischenablage kopieren addressTextField.addressCopiedToClipboard=Die Adresse wurde in die Zwischenablage kopiert addressTextField.openWallet.failed=Öffnen einer Monero-Wallet-Standardanwendung ist fehlgeschlagen. Haben Sie möglicherweise keine installiert? peerInfoIcon.tooltip={0}\nMarkierung: {1} txIdTextField.copyIcon.tooltip=Transaktions-ID in Zwischenablage kopieren txIdTextField.blockExplorerIcon.tooltip=Blockchain Explorer mit dieser Transaktions-ID öffnen txIdTextField.missingTx.warning.tooltip=Erforderliche Transaktion fehlt #################################################################### # Navigation #################################################################### navigation.account=\"Konto\" navigation.account.walletSeed=\"Konto/Wallet-Seed\" navigation.funds.availableForWithdrawal=\"Funds/Funds senden\" navigation.portfolio.myOpenOffers=\"Portfolio/Meine offenen Angebote\" navigation.portfolio.pending=\"Portfolio/Offene Trades\" navigation.portfolio.closedTrades=\"Portfolio/Verlauf\" navigation.funds.depositFunds=\"Gelder/Gelder erhalten\" navigation.settings.preferences=\"Einstellungen/Voreinstellungen\" # suppress inspection "UnusedProperty" navigation.funds.transactions=\"Gelder/Transaktionen\" navigation.support=\"Support\" #################################################################### # Formatter #################################################################### formatter.formatVolumeLabel={0} Betrag{1} formatter.makerTaker=Ersteller als {0} {1} / Abnehmer als {2} {3} formatter.makerTaker.locked=Ersteller als {0} {1} / Abnehmer als {2} {3} 🔒 formatter.youAreAsMaker=Sie sind: {1} {0} (Ersteller) / Abnehmer ist: {3} {2} formatter.youAreAsTaker=Sie sind: {1} {0} (Abnehmer) / Ersteller ist: {3} {2} formatter.youAre=Sie {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Sie erstellen ein Angebot um {1} zu {0} formatter.youAreCreatingAnOffer.traditional.locked=Sie erstellen ein Angebot um {1} zu {0} 🔒 formatter.youAreCreatingAnOffer.crypto=Sie erstellen ein Angebot {1} zu {0} ({3} zu {2}) formatter.youAreCreatingAnOffer.crypto.locked=Sie erstellen ein Angebot {1} zu {0} ({3} zu {2}) 🔒 formatter.asMaker={0} {1} als Ersteller formatter.asTaker={0} {1} als Abnehmer #################################################################### # Domain specific #################################################################### # we use enum values here # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" XMR_MAINNET=Monero-Hauptnetzwerk # suppress inspection "UnusedProperty" XMR_LOCAL=Monero-Testnetzwerk # suppress inspection "UnusedProperty" XMR_STAGENET=Monero-Regtest time.year=Jahr time.month=Monat time.week=Woche time.day=Tag time.hour=Stunde time.minute10=10 Minuten time.hours=Stunden time.days=Tage time.1hour=1 Stunde time.1day=1 Tag time.minute=Minute time.second=Sekunde time.minutes=Minuten time.seconds=Sekunden password.enterPassword=Passwort eingeben password.confirmPassword=Passwort bestätigen password.tooLong=Das Passwort muss aus weniger als 500 Zeichen bestehen. password.deriveKey=Schlüssel aus Passwort ableiten password.walletDecrypted=Die Wallet wurde erfolgreich entschlüsselt und der Passwortschutz entfernt. password.wrongPw=Sie haben das falsche Passwort eingegeben.\n\nVersuchen Sie bitte Ihr Passwort erneut einzugeben, wobei Sie dies vorsichtig auf Tipp- und Rechtschreibfehler überprüfen sollten. password.walletEncrypted=Die Wallet wurde erfolgreich verschlüsselt und der Passwortschutz aktiviert. password.passwordsDoNotMatch=Die 2 eingegebenen Passwörter stimmen nicht überein. password.forgotPassword=Passwort vergessen? password.backupReminder=Beachten Sie, dass wenn Sie ein Passwort setzen, alle automatisch erstellten Backups der unverschlüsselten Wallet gelöscht werden.\n\nEs wird dringend empfohlen ein Backup des Anwendungsverzeichnisses zu erstellen und die Seed-Wörter aufzuschreiben, bevor Sie ein Passwort erstellen! password.backupWasDone=Ich habe bereits ein Backup erstellt password.setPassword=Passwort hinzufügen (Ich habe schon ein Backup erstellt) password.makeBackup=Backup erstellen seed.seedWords=Seed-Wörter der Wallet seed.enterSeedWords=Seed-Wörter der Wallet eingeben seed.date=Wallets-Datum seed.restore.title=Wallets aus Seed-Wörtern wiederherstellen seed.restore=Wallets wiederherstellen seed.creationDate=Erstellungsdatum seed.warn.walletNotEmpty.msg=Ihre Monero-Wallet ist nicht leer.\n\nSie müssen diese Wallet leeren, bevor Sie versuchen, eine ältere Wallet wiederherzustellen, da das Verwechseln von Wallets zu ungültigen Backups führen kann.\n\nBitte schließen Sie Ihre laufenden Trades ab, schließen Sie Ihre offenen Angebote und gehen Sie auf \"Gelder\", um Ihre Moneros zu versenden.\nSollten Sie nicht auf Ihre Moneros zugreifen können, können Sie das Notfallwerkzeug nutzen, um Ihre Wallet zu leeren.\nUm das Notfallwerkzeug zu öffnen, drücken Sie \"alt + e\" oder \"cmd/Strg + e\". seed.warn.walletNotEmpty.restore=Ich möchte trotzdem wiederherstellen seed.warn.walletNotEmpty.emptyWallet=Ich werde meine Wallets erst leeren seed.warn.notEncryptedAnymore=Ihre Wallets sind verschlüsselt.\n\nNach einer Wiederherstellung werden die Wallets nicht mehr verschlüsselt sein und Sie werden ein neues Passwort festlegen müssen.\n\nMöchten Sie fortfahren? seed.warn.walletDateEmpty=Da Sie kein Wallet-Datum angegeben haben, muss haveno die Blockchain ab 09. 10. 2013 (seit diesem Datum existiert BIP39) scannen.\n\nBIP39-Wallets wurden in haveno erstmals am 28.06.2017 (Release v0.5) eingeführt. Wenn Sie dieses Datum nehmen, können Sie etwas Zeit sparen.\n\nIdealerweise sollten Sie das Datum angeben, an dem Ihr Wallet Seed erstellt wurde.\n\n\nSind Sie sicher, dass Sie ohne Angabe eines Wallet-Datums fortfahren wollen? seed.restore.success=Wallets mit den neuen Seed-Wörtern erfolgreich wiederhergestellt.\n\nSie müssen die Anwendung herunterfahren und neu starten. seed.restore.error=Beim Wiederherstellen der Wallets mit den Seed-Wörtern ist ein Fehler aufgetreten.{0} seed.restore.openOffers.warn=Sie haben noch offene Angebote die entfernt werden wenn Sie Ihre Seed Wörter wiederherstellen.\nSind Sie sicher, dass Sie fortfahren möchten? #################################################################### # Payment methods #################################################################### payment.account=Konto payment.account.no=Kontonummer payment.account.name=Kontoname payment.account.username=Benutzername payment.account.phoneNr=Telefonnummer payment.account.owner.fullname=Vollständiger Name des Kontoinhabers payment.account.fullName=Vollständiger Name (vor, zweit, nach) payment.account.state=Bundesland/Landkreis/Region payment.account.city=Stadt payment.bank.country=Land der Bank payment.account.name.email=Vollständiger Name / E-Mail des Kontoinhabers payment.account.name.emailAndHolderId=Vollständiger Name / E-Mail / {0} des Kontoinhabers payment.bank.name=Bankname payment.select.account=Kontotyp wählen payment.select.region=Region wählen payment.select.country=Land wählen payment.select.bank.country=Land der Bank wählen payment.foreign.currency=Sind Sie sicher, dass Sie eine Währung wählen wollen, die nicht der Standardwährung des Landes entspricht? payment.restore.default=Nein, Standardwährung wiederherstellen payment.email=E-Mail payment.country=Land payment.extras=Besondere Erfordernisse payment.email.mobile=E-Mail oder Telefonnummer payment.crypto.address=Crypto-Adresse payment.crypto.tradeInstantCheckbox=Handeln sie schnell (innerhalb 1 Stunde) mit diesem Crypto payment.crypto.tradeInstant.popup=Für "Schnelles Handeln" müssen beide Handelspartner online sein, um den Handel innerhalb 1 Stunde abschließen zu können.\n\nFalls sie offene Angebote haben jedoch nicht verfügbar sind, deaktivieren sie bitte diese Angebote unter 'Portfolio'. payment.crypto=Crypto payment.select.crypto=Crypto wählen oder suchen payment.secret=Geheimfrage payment.answer=Antwort payment.wallet=Wallets-ID payment.amazon.site=Kaufe Geschenkkarte auf payment.ask=Im Trader Chat fragen payment.uphold.accountId=Nutzername oder Email oder Telefonnr. payment.moneyBeam.accountId=E-Mail oder Telefonnummer payment.popmoney.accountId=E-Mail oder Telefonnummer payment.promptPay.promptPayId=Personalausweis/Steuernummer oder Telefonnr. payment.supportedCurrencies=Unterstützte Währungen payment.supportedCurrenciesForReceiver=Währungen für den Geldeingang payment.limitations=Einschränkungen payment.salt=Salt für Überprüfung des Kontoalters payment.error.noHexSalt=Der Salt muss im HEX-Format sein.\nEs wird empfohlen das Salt-Feld zu bearbeiten, wenn Sie den Salt von einem alten Konto übertragen, um das Alter Ihres Kontos zu erhalten. Das Alter des Kontos wird durch den Konto-Salt und die Kontodaten (z.B. IBAN) verifiziert. payment.accept.euro=Trades aus diesen Euroländern akzeptieren payment.accept.nonEuro=Trades aus diesen Nicht-Euroländern akzeptieren payment.accepted.countries=Akzeptierte Länder payment.accepted.banks=Akzeptierte Banken (ID) payment.mobile=Mobil-Tel.-Nr. payment.postal.address=Postanschrift payment.national.account.id.AR=CBU Nummer shared.accountSigningState=Konto-Unterzeichnungsstatus #new payment.crypto.address.dyn={0} Adresse payment.crypto.receiver.address=Crypto Adresse des Empfängers payment.accountNr=Kontonummer payment.emailOrMobile=E-Mail oder Telefonnummer payment.useCustomAccountName=Spezifischen Kontonamen nutzen payment.maxPeriod=Max. erlaubte Handelsdauer payment.maxPeriodAndLimit=Max. Trade-Dauer : {0} / Max. Kaufen: {1} / Max. Verkauf: {2} / Kontoalter: {3} payment.maxPeriodAndLimitCrypto=Max. Handelsdauer: {0} / Max. Handelsgrenze: {1} payment.currencyWithSymbol=Währung: {0} payment.nameOfAcceptedBank=Name der akzeptierten Bank payment.addAcceptedBank=Akzeptierte Bank hinzufügen payment.clearAcceptedBanks=Akzeptierte Banken entfernen payment.bank.nameOptional=Bankname (optional) payment.bankCode=Bankleitzahl payment.bankId=Bankkennung (BIC/SWIFT) payment.bankIdOptional=Bankkennung (BIC/SWIFT) (optional) payment.branchNr=Filialnummer payment.branchNrOptional=Filialnummer (optional) payment.accountNrLabel=Kontonummer (IBAN) payment.accountType=Kontotyp payment.checking=Überprüfe payment.savings=Ersparnisse payment.personalId=Personalausweis payment.zelle.info=Zelle ist ein Geldtransferdienst, der am besten *durch* eine andere Bank funktioniert.\n\n1. Sehen Sie auf dieser Seite nach, ob (und wie) Ihre Bank mit Zelle zusammenarbeitet:\nhttps://www.zellepay.com/get-started\n\n2. Achten Sie besonders auf Ihre Überweisungslimits - die Sendelimits variieren je nach Bank, und die Banken geben oft separate Tages-, Wochen- und Monatslimits an.\n\n3. Wenn Ihre Bank nicht mit Zelle zusammenarbeitet, können Sie die Zahlungsmethode trotzdem über die Zelle Mobile App benutzen, aber Ihre Überweisungslimits werden viel niedriger sein.\n\n4. Der auf Ihrem Haveno-Konto angegebene Name MUSS mit dem Namen auf Ihrem Zelle/Bankkonto übereinstimmen. \n\nWenn Sie eine Zelle Transaktion nicht wie in Ihrem Handelsvertrag angegeben durchführen können, verlieren Sie möglicherweise einen Teil (oder die gesamte) Sicherheitskaution.\n\nWegen des etwas höheren Chargeback-Risikos von Zelle wird Verkäufern empfohlen, nicht unterzeichnete Käufer per E-Mail oder SMS zu kontaktieren, um zu überprüfen, ob der Käufer wirklich das in Haveno angegebene Zelle-Konto besitzt. payment.fasterPayments.newRequirements.info=Einige Banken haben damit begonnen, den vollständigen Namen des Empfängers für Faster Payments Überweisungen zu überprüfen. Ihr aktuelles Faster Payments-Konto gibt keinen vollständigen Namen an.\n\nBitte erwägen Sie, Ihr Faster Payments-Konto in Haveno neu einzurichten, um zukünftigen {0} Käufern einen vollständigen Namen zu geben.\n\nWenn Sie das Konto neu erstellen, stellen Sie sicher, dass Sie die genaue Bankleitzahl, Kontonummer und die "Salt"-Werte für die Altersverifikation von Ihrem alten Konto auf Ihr neues Konto kopieren. Dadurch wird sichergestellt, dass das Alter und der Unterschriftsstatus Ihres bestehenden Kontos erhalten bleiben. payment.moneyGram.info=Bei der Nutzung von MoneyGram, muss der XMR Käufer die MoneyGram Zulassungsnummer und ein Foto der Quittung per E-Mail an den XMR-Verkäufer senden. Die Quittung muss den vollständigen Namen, das Land, das Bundesland des Verkäufers und den Betrag deutlich zeigen. Der Käufer bekommt die E-Mail-Adresse des Verkäufers im Handelsprozess angezeigt. payment.westernUnion.info=Bei der Nutzung von Western Union, muss der XMR Käufer die MTCN (Tracking-Nummer) Foto der Quittung per E-Mail an den XMR-Verkäufer senden. Die Quittung muss den vollständigen Namen, das Land, die Stadt des Verkäufers und den Betrag deutlich zeigen. Der Käufer bekommt die E-Mail-Adresse des Verkäufers im Handelsprozess angezeigt. payment.halCash.info=Bei Verwendung von HalCash muss der XMR-Käufer dem XMR-Verkäufer den HalCash-Code per SMS vom Mobiltelefon senden.\n\nBitte achten Sie darauf, dass Sie den maximalen Betrag, den Sie bei Ihrer Bank mit HalCash versenden dürfen, nicht überschreiten. Der Mindestbetrag pro Auszahlung beträgt 10 EUR und der Höchstbetrag 600 EUR. Bei wiederholten Abhebungen sind es 3000 EUR pro Empfänger pro Tag und 6000 EUR pro Empfänger pro Monat. Bitte überprüfen Sie diese Limits bei Ihrer Bank, um sicherzustellen, dass sie die gleichen Limits wie hier angegeben verwenden.\n\nDer Auszahlungsbetrag muss ein Vielfaches von 10 EUR betragen, da Sie keine anderen Beträge an einem Geldautomaten abheben können. Die Benutzeroberfläche beim Erstellen und Annehmen eines Angebots passt den XMR-Betrag so an, dass der EUR-Betrag korrekt ist. Sie können keinen marktbasierten Preis verwenden, da sich der EUR-Betrag bei sich ändernden Preisen ändern würde.\n\nIm Streitfall muss der XMR-Käufer den Nachweis erbringen, dass er die EUR geschickt hat. # suppress inspection "UnusedMessageFormatParameter" payment.limits.info=Bitte beachten Sie, dass alle Banküberweisungen mit einem gewissen Rückbuchungsrisiko verbunden sind. Um dieses Risiko zu mindern, setzt Haveno Limits pro Trade fest, je nachdem wie hoch das Rückbuchungsrisiko der Zahlungsmethode ist. \n\nFür diese Zahlungsmethode beträgt Ihr Pro-Trade-Limit zum Kaufen oder Verkaufen {2}.\nDieses Limit gilt nur für die Größe eines einzelnen Trades - Sie können soviele Trades platzieren wie Sie möchten.\n\nFinden Sie mehr Informationen im Wiki [HYPERLINK:https://docs.haveno.exchange/overview/account_limits]. # suppress inspection "UnusedProperty" payment.limits.info.withSigning=Um das Risiko einer Rückbuchung zu minimieren, setzt Haveno für diese Zahlungsmethode Limits pro Trade auf der Grundlage der folgenden 2 Faktoren fest:\n\n1. Allgemeines Rückbuchungsrisiko für die Zahlungsmethode\n2. Status der Kontounterzeichnung\n\nDieses Zahlungskonto ist noch nicht unterzeichnet. Es ist daher auf den Kauf von {0} pro Trade beschränkt ist. Nach der Unterzeichnung werden die Kauflimits wie folgt erhöht:\n\n● Vor der Unterzeichnung und für 30 Tage nach der Unterzeichnung beträgt Ihr Kauflimit pro Trade {0}\n● 30 Tage nach der Unterzeichnung beträgt Ihr Kauflimit pro Trade {1}\n● 60 Tage nach der Unterzeichnung beträgt Ihr Kauflimit pro Trade {2}\n\nVerkaufslimits sind von der Kontounterzeichnung nicht betroffen. Sie können {2} in einem einzigen Trade sofort verkaufen.\n\nDieses Limit gilt nur für die Größe eines einzelnen Trades - Sie können soviele Trades platzieren wie sie möchten.\n\nWeitere Informationen gibt es im Wiki [HYPERLINK:https://docs.haveno.exchange/overview/account_limits]. payment.cashDeposit.info=Bitte bestätigen Sie, dass Ihre Bank Bareinzahlungen in Konten von anderen Personen erlaubt. Zum Beispiel werden diese Einzahlungen bei der Bank of America und Wells Fargo nicht mehr erlaubt. payment.revolut.info=Revolut benötigt den "Benutzernamen" als Account ID und nicht die Telefonnummer oder E-Mail, wie es in der Vergangenheit war. payment.account.revolut.addUserNameInfo={0}\nDein existierendes Revolut Konto ({1}) hat keinen "Benutzernamen".\nBitte geben Sie Ihren Revolut "Benutzernamen" ein um Ihre Kontodaten zu aktualisieren.\nDas wird Ihr Kontoalter und die Verifizierung nicht beeinflussen. payment.revolut.addUserNameInfo.headLine=Revolut Account updaten payment.cashapp.info=Bitte beachten Sie, dass Cash App ein höheres Rückbuchungsrisiko hat als die meisten Banküberweisungen. payment.venmo.info=Bitte beachten Sie, dass Venmo ein höheres Rückbuchungsrisiko hat als die meisten Banküberweisungen. payment.paypal.info=Bitte beachten Sie, dass PayPal ein höheres Rückbuchungsrisiko hat als die meisten Banküberweisungen. payment.amazonGiftCard.upgrade=Bei der Zahlungsmethode Amazon Geschenkkarten muss das Land angegeben werden. payment.account.amazonGiftCard.addCountryInfo={0}\nDein bestehendes Amazon Geschenkkarten Konto ({1}) wurde keinem Land zugeteilt.\nBitte geben Sie das Amazon Geschenkkarten Land ein um Ihre Kontodaten zu aktualisieren.\nDas wird ihr Kontoalter nicht beeinflussen. payment.amazonGiftCard.upgrade.headLine=Amazon Geschenkkarte Konto updaten payment.usPostalMoneyOrder.info=Der Handel auf Haveno unter Verwendung von US Postal Money Orders (USPMO) setzt voraus, dass Sie Folgendes verstehen:\n\n- Der XMR-Käufer muss den Namen des XMR-Verkäufers sowohl in das Feld des Zahlers als auch in das Feld des Zahlungsempfängers eintragen und vor dem Versand ein hochauflösendes Foto des USPMO und des Umschlags mit dem Tracking-Nachweis machen.\n- XMR-Käufer müssen den USPMO mit Zustellbenachrichtigung an den XMR-Verkäufer schicken.\n\nFür den Fall, dass eine Mediation erforderlich ist oder es zu einem Handelskonflikt kommt, müssen Sie die Fotos zusammen mit der USPMO-Seriennummer, der Nummer des Postamtes und dem Dollarbetrag an den Haveno-Vermittler oder Rückerstattungsbeauftragten schicken, damit dieser die Angaben auf der Website der US-Post überprüfen kann.\n\nWenn Sie dem Vermittler oder der Schiedsperson die erforderlichen Informationen nicht zur Verfügung stellen, führt dies dazu, dass der Konflikt zu Ihrem Nachteil entschieden wird.\n\nIn allen Konfliktfällen trägt der USPMO-Absender 100% der Verantwortung für die Bereitstellung von Beweisen/Nachweisen für den Vermittler oder die Schiedsperson.\n\nWenn Sie diese Anforderungen nicht verstehen, handeln Sie bitte nicht auf Haveno unter Verwendung von USPMO. payment.payByMail.info=Der Handel über Pay by Mail auf Haveno erfordert, dass Sie Folgendes verstehen:\n\ \n\ - Der XMR-Käufer sollte das Bargeld in einem manipulationssicheren Geldbeutel verpacken.\n\ - Der XMR-Käufer sollte den Verpackungsprozess des Bargeldes filmen oder hochauflösende Fotos davon machen, auf denen die Adresse und die Tracking-Nummer bereits auf der Verpackung angebracht sind.\n\ - Der XMR-Käufer sollte das Bargeldpaket mit Zustellbestätigung und angemessener Versicherung an den XMR-Verkäufer senden.\n\ - Der XMR-Verkäufer sollte die Öffnung des Pakets filmen und sicherstellen, dass die vom Absender bereitgestellte Tracking-Nummer im Video sichtbar ist.\n\ - Der Angebotsersteller muss spezielle Bedingungen oder Vereinbarungen im Feld 'Zusätzliche Informationen' des Zahlungskontos angeben.\n\ - Der Angebotsempfänger erklärt sich damit einverstanden, die Bedingungen und Vereinbarungen des Angebotserstellers zu akzeptieren, indem er das Angebot annimmt.\n\ \n\ Pay by Mail Trades legen die Verantwortung, ehrlich zu handeln, klar auf beide Parteien.\n\ \n\ - Pay by Mail Trades haben weniger überprüfbare Handlungen als andere traditionelle Trades. Dies erschwert die Bearbeitung von Streitfällen erheblich.\n\ - Versuchen Sie, Streitigkeiten direkt mit Ihrem Handelspartner über den Trader-Chat zu lösen. Dies ist Ihr vielversprechendster Weg, um jeden Pay by Mail Streitfall zu lösen.\n\ - Schiedsrichter können Ihren Fall prüfen und einen Vorschlag machen, aber es ist NICHT garantiert, dass sie helfen werden.\n\ - Schiedsrichter treffen eine Entscheidung auf Grundlage der ihnen vorgelegten Beweise. Bitte befolgen und dokumentieren Sie daher die oben genannten Prozesse, um im Falle eines Streitfalls Beweise zu haben.\n\ - Rückerstattungsanfragen für verlorene Gelder, die aus Pay By Mail Trades zu Haveno resultieren, werden NICHT berücksichtigt.\n\ \n\ Wenn Sie diese Anforderungen nicht verstehen, handeln Sie nicht über Pay by Mail auf Haveno. payment.payByMail.contact=Kontaktinformationen payment.payByMail.contact.prompt=Name oder Pseudonym Umschlag sollten adressiert werden an payment.f2f.contact=Kontaktinformationen payment.f2f.contact.prompt=Wie möchten Sie vom Trading-Peer kontaktiert werden? (E-Mail Adresse, Telefonnummer,...) payment.f2f.city=Stadt für ein "Angesicht zu Angesicht" Treffen payment.f2f.city.prompt=Die Stadt wird mit dem Angebot angezeigt payment.shared.optionalExtra=Freiwillige zusätzliche Informationen payment.shared.extraInfo=Zusätzliche Informationen payment.shared.extraInfo.offer=Zusätzliche Angebotsinformationen payment.shared.extraInfo.prompt.paymentAccount=Gib spezielle Bedingungen, Abmachungen oder Details die bei ihren Angeboten unter diesem Zahlungskonto angezeigt werden sollen an. Nutzer werden diese Informationen vor der Annahme des Angebots sehen. payment.shared.extraInfo.prompt.offer=Definieren Sie alle speziellen Begriffe, Bedingungen oder Details, die Sie mit Ihrem Angebot anzeigen möchten. payment.shared.extraInfo.noDeposit=Kontaktdaten und Angebotsbedingungen payment.f2f.info=Persönliche 'Face to Face' Trades haben unterschiedliche Regeln und sind mit anderen Risiken verbunden als gewöhnliche Online-Trades.\n\nDie Hauptunterschiede sind:\n● Die Trading Partner müssen die Kontaktdaten und Informationen über den Ort und die Uhrzeit des Treffens austauschen.\n● Die Trading Partner müssen ihre Laptops mitbringen und die Bestätigung der "gesendeten Zahlung" und der "erhaltenen Zahlung" am Treffpunkt vornehmen.\n● Wenn ein Ersteller eines Angebots spezielle "Allgemeine Geschäftsbedingungen" hat, muss er diese im Textfeld "Zusatzinformationen" des Kontos angeben.\n● Mit der Annahme eines Angebots erklärt sich der Käufer mit den vom Anbieter angegebenen "Allgemeinen Geschäftsbedingungen" einverstanden.\n● Im Konfliktfall kann der Mediator oder Arbitrator nicht viel tun, da es in der Regel schwierig ist zu bestimmen, was beim Treffen passiert ist. In solchen Fällen können die Monero auf unbestimmte Zeit oder bis zu einer Einigung der Trading Peers gesperrt werden.\n\nUm sicherzustellen, dass Sie die Besonderheiten der persönlichen 'Face to Face' Trades vollständig verstehen, lesen Sie bitte die Anweisungen und Empfehlungen unter: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=Webseite öffnen payment.f2f.offerbook.tooltip.countryAndCity=Land und Stadt: {0} / {1} payment.shared.extraInfo.tooltip=Zusätzliche Informationen: {0} payment.japan.bank=Bank payment.japan.branch=Filiale payment.japan.account=Konto payment.japan.recipient=Name payment.australia.payid=PayID payment.payid=PayIDs wie E-Mail Adressen oder Telefonnummern die mit Finanzinstitutionen verbunden sind. payment.payid.info=Eine PayID wie eine Telefonnummer, E-Mail Adresse oder Australische Business Number (ABN) mit der Sie sicher Ihre Bank, Kreditgenossenschaft oder Bausparkassenkonto verlinken können. Sie müssen bereits eine PayID mit Ihrer Australischen Finanzinstitution erstellt haben. Beide Institutionen, die die sendet und die die empfängt, müssen PayID unterstützen. Weitere informationen finden Sie unter [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=Um mit einer Amazon eGift Geschenkkarte zu bezahlen, müssen Sie eine Amazon eGift Geschenkkarte über Ihr Amazon-Konto an den XMR-Verkäufer senden. \n\nHaveno zeigt die E-Mail-Adresse oder Telefonnummer des XMR-Verkäufers an, an die die Geschenkkarte gesendet werden soll, und Sie müssen die Handels-ID in das Nachrichtenfeld der Geschenkkarte eintragen. Bitte lesen Sie das Wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Amazon_eGift_card] für weitere Details und empfohlene Vorgehensweisen. \n\nDrei wichtige Hinweise:\n- Versuchen Sie Geschenkkarten mit Beträgen von 100 USD oder weniger zu versenden, weil Amazon größere Geschenkkarten gerne als betrügerisch kennzeichnet\n- Versuchen Sie einen kreativen, glaubwürdigen Text für die Nachricht der Geschenkkarten zu verwenden (z.B. "Alles Gute zum Geburtstag Susi!"), zusammen mit der Handels-ID (und verwenden Sie den Handels-Chat, um Ihrem Handelspartner den von Ihnen gewählten Referenztext mitzuteilen, damit er Ihre Zahlung überprüfen kann)\n- Amazon Geschenkkarten können nur auf der Amazon-Website eingelöst werden, auf der sie gekauft wurden (z. B. kann eine auf amazon.it gekaufte Geschenkkarte nur auf amazon.it eingelöst werden) payment.paysafe.info=Zum Schutz Ihrer Sicherheit raten wir dringend davon ab, Paysafecard-PINs für Zahlungen zu verwenden.\n\n\ Transaktionen, die über PINs durchgeführt werden, können nicht unabhängig zur Streitbeilegung überprüft werden. Wenn ein Problem auftritt, kann die Rückerstattung von Geldern möglicherweise nicht möglich sein.\n\n\ Um die Transaktionssicherheit mit Streitbeilegung zu gewährleisten, verwenden Sie immer Zahlungsmethoden, die überprüfbare Aufzeichnungen bieten. # We use constants from the code so we do not use our normal naming convention # dynamic values are not recognized by IntelliJ # Only translate general terms NATIONAL_BANK=Inlandsüberweisung SAME_BANK=Überweisung mit derselben Bank SPECIFIC_BANKS=Überweisungen mit bestimmten Banken US_POSTAL_MONEY_ORDER=US Postal Money Order CASH_DEPOSIT=Cash Deposit PAY_BY_MAIL=Bargeld per Post MONEY_GRAM=MoneyGram WESTERN_UNION=Western Union F2F=Angesicht zu Angesicht (persönlich) JAPAN_BANK=Japan Bank Furikomi AUSTRALIA_PAYID=Australische PayID # suppress inspection "UnusedProperty" NATIONAL_BANK_SHORT=Inlandsbanken # suppress inspection "UnusedProperty" SAME_BANK_SHORT=Gleiche Bank # suppress inspection "UnusedProperty" SPECIFIC_BANKS_SHORT=Spezifische Banken # suppress inspection "UnusedProperty" US_POSTAL_MONEY_ORDER_SHORT=US Money Order # suppress inspection "UnusedProperty" CASH_DEPOSIT_SHORT=Cash Deposit # suppress inspection "UnusedProperty" PAY_BY_MAIL_SHORT=BargeldPerPost # suppress inspection "UnusedProperty" MONEY_GRAM_SHORT=MoneyGram # suppress inspection "UnusedProperty" WESTERN_UNION_SHORT=Western Union # suppress inspection "UnusedProperty" F2F_SHORT=A2A # suppress inspection "UnusedProperty" JAPAN_BANK_SHORT=Japan Furikomi # suppress inspection "UnusedProperty" AUSTRALIA_PAYID_SHORT=PayID # Do not translate brand names # suppress inspection "UnusedProperty" UPHOLD=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY=Popmoney # suppress inspection "UnusedProperty" REVOLUT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY=WeChat Pay # suppress inspection "UnusedProperty" SEPA=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT=SEPA Echtzeitzahlungen # suppress inspection "UnusedProperty" FASTER_PAYMENTS=Faster Payments # suppress inspection "UnusedProperty" SWISH=Swish # suppress inspection "UnusedProperty" ZELLE=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS=Cryptos # suppress inspection "UnusedProperty" PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon Gift-Karte # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT=Cryptos schnell # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY=OKPay # suppress inspection "UnusedProperty" CASH_APP=Cash App # suppress inspection "UnusedProperty" VENMO=Venmo # suppress inspection "UnusedProperty" UPHOLD_SHORT=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM_SHORT=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY_SHORT=Popmoney # suppress inspection "UnusedProperty" REVOLUT_SHORT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY_SHORT=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY_SHORT=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY_SHORT=WeChat Pay # suppress inspection "UnusedProperty" SEPA_SHORT=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT_SHORT=SEPA Echtzeit # suppress inspection "UnusedProperty" FASTER_PAYMENTS_SHORT=Faster Payments # suppress inspection "UnusedProperty" SWISH_SHORT=Swish # suppress inspection "UnusedProperty" ZELLE_SHORT=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY_SHORT=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER_SHORT=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH_SHORT=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS_SHORT=Cryptos # suppress inspection "UnusedProperty" PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon Gift-Karte # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT_SHORT=Cryptos schnell # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY_SHORT=OKPay # suppress inspection "UnusedProperty" CASH_APP_SHORT=Cash App # suppress inspection "UnusedProperty" VENMO_SHORT=Venmo #################################################################### # Validation #################################################################### validation.empty=Eine leere Eingabe ist nicht erlaubt. validation.NaN=Die Eingabe ist keine gültige Zahl. validation.notAnInteger=Eingabe ist keine ganze Zahl. validation.zero=Die Eingabe von 0 ist nicht erlaubt. validation.negative=Ein negativer Wert ist nicht erlaubt. validation.traditional.tooSmall=Eingaben kleiner als der minimal mögliche Betrag sind nicht erlaubt. validation.traditional.tooLarge=Eingaben größer als der maximal mögliche Betrag sind nicht erlaubt. validation.xmr.fraction=Input wird einem Monero Wert von weniger als 1 satoshi entsprechen validation.xmr.tooLarge=Eingaben größer als {0} sind nicht erlaubt. validation.xmr.tooSmall=Eingabe kleiner als {0} ist nicht erlaubt. validation.passwordTooShort=Das Passwort das Sie eingegeben haben ist zu kurz. Es muss mindestens 8 Zeichen enthalten. validation.passwordTooLong=Das eingegebene Passwort ist zu lang. Es darf nicht aus mehr als 50 Zeichen bestehen. validation.sortCodeNumber={0} muss aus {1} Zahlen bestehen. validation.sortCodeChars={0} muss aus {1} Zeichen bestehen. validation.bankIdNumber={0} muss aus {1} Zahlen bestehen. validation.accountNr=Die Kontonummer muss aus {0} Zahlen bestehen. validation.accountNrChars=Die Kontonummer muss aus {0} Zeichen bestehen. validation.xmr.invalidAddress=Die Adresse ist nicht korrekt. Bitte überprüfen Sie das Adressformat. validation.integerOnly=Bitte nur ganze Zahlen eingeben. validation.inputError=Ihre Eingabe hat einen Fehler verursacht:\n{0} validation.xmr.exceedsMaxTradeLimit=Ihr Handelslimit ist {0}. validation.nationalAccountId={0} muss aus {1} Zahlen bestehen. #new validation.invalidInput=Ungültige Eingabe: {0} validation.accountNrFormat=Die Kontonummer muss folgendes Format haben: {0} # suppress inspection "UnusedProperty" validation.crypto.wrongStructure=Die Adressvalidierung ist fehlgeschlagen, da diese nicht mit der Struktur einer {0}-Adresse übereinstimmt. # suppress inspection "UnusedProperty" validation.crypto.ltz.zAddressesNotSupported=Die LTZ Adresse muss mit L beginnen. Adressen die mit z beginnen werden nicht unterstützt. # suppress inspection "UnusedProperty" validation.crypto.zAddressesNotSupported=ZEC Adressen müssen mit t beginnen. Adressen die mit z beginnen werden nicht unterstützt. # suppress inspection "UnusedProperty" validation.crypto.invalidAddress=Die Adresse ist keine gültige {0}-Adresse! {1} # suppress inspection "UnusedProperty" validation.crypto.liquidBitcoin.invalidAddress=Native Segwit-Adressen (die mit 'lq' beginnen) werden nicht unterstützt. validation.bic.invalidLength=Eingabelänge muss 8 oder 11 betragen validation.bic.letters=Bank- und Ländercode müssen aus Buchstaben bestehen validation.bic.invalidLocationCode=Der BIC enthält einen ungültigen Standort-Code validation.bic.invalidBranchCode=Der BIC enthält eine ungültige Filialennummer validation.bic.sepaRevolutBic=Revolut SEPA Konten werden nicht unterstüzt. validation.btc.invalidFormat=Ungültiges Bitcoin Adressformat. validation.email.invalidAddress=Ungültige Adresse validation.iban.invalidCountryCode=Der Ländercode ist ungültig validation.iban.checkSumNotNumeric=Die Prüfsumme muss numerisch sein validation.iban.nonNumericChars=Nicht-alphanumerisches Zeichen entdeckt validation.iban.checkSumInvalid=Die IBAN-Prüfsumme ist ungültig validation.iban.invalidLength=Die Zahl muss zwischen 15 und 34 Zeichen lang sein. validation.interacETransfer.invalidAreaCode=Nicht-kanadische Postleitzahl validation.interacETransfer.invalidPhone=Bitte geben Sie eine gültige 11-stellige Telefonnummer (ex:1-123-456-7890) oder eine E-Mail Adresse an validation.interacETransfer.invalidQuestion=Nur Buchstaben, Zahlen, Leerzeichen und/oder die Symbole _ , . ? - sind erlaubt validation.interacETransfer.invalidAnswer=Muss ein Wort mit Buchstaben, Zahlen und/oder dem Symbol - sein validation.inputTooLarge=Eingabe darf nicht größer als {0} sein validation.inputTooSmall=Eingabe muss größer als {0} sein validation.inputToBeAtLeast=Eingabe muss mindestens {0} sein validation.amountBelowDust=Menge unter dem Dust Limit von {0} Satoshi ist nicht erlaubt. validation.length=Die Länge muss zwischen {0} und {1} sein validation.fixedLength=Länge muss {0} betragen validation.pattern=Die Eingabe muss im Format {0} sein validation.noHexString=Die Eingabe ist nicht im HEX-Format. validation.advancedCash.invalidFormat=Gültige E-Mail-Adresse oder Wallets ID vom Format "X000000000000" benötigt validation.invalidUrl=Dies ist keine gültige URL validation.mustBeDifferent=Ihre Eingabe muss vom aktuellen Wert abweichen validation.cannotBeChanged=Parameter kann nicht geändert werden validation.numberFormatException=Zahlenformat Ausnahme {0} validation.mustNotBeNegative=Eingabe darf nicht negativ sein validation.phone.missingCountryCode=Es wird eine zweistellige Ländervorwahl benötigt, um die Telefonnummer zu bestätigen validation.phone.invalidCharacters=Telefonnummer {0} enthält ungültige Zeichen validation.phone.insufficientDigits=Das ist keine gültige Telefonnummer. Sie habe nicht genügend Stellen angegeben. validation.phone.tooManyDigits=Es sind zu viele Ziffern in {0} um eine gültige Telefonnummer zu sein. validation.phone.invalidDialingCode=Die Ländervorwahl in der Nummer {0} ist für das Land {1} ungültig. Die richtige Vorwahl ist {2}. validation.invalidAddressList=Muss eine kommagetrennte Liste der gültigen Adressen sein ================================================ FILE: core/src/main/resources/i18n/displayStrings_es.properties ================================================ # Keep display strings organized by domain # Naming convention: We use camelCase and dot separated name spaces. # Use as many sub spaces as required to make the structure clear, but as little as possible. # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break # in the display but only in the editor. # Please use in all language files the exact same order of the entries, that way a comparison is easier. # Please try to keep the length of the translated string similar to English. If it is longer it might break layout or # get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. #################################################################### # Shared #################################################################### shared.readMore=Leer más shared.openHelp=Abrir ayuda shared.warning=Advertencia shared.close=Cerrar shared.cancel=Cancelar shared.ok=Ok shared.yes=Sí shared.no=No shared.iUnderstand=Entendido shared.na=No disponible shared.shutDown=Apagar shared.reportBug=Reportar error de software en Github shared.buyMonero=Comprar monero shared.sellMonero=Vender monero shared.buyCurrency=Comprar {0} shared.sellCurrency=Vender {0} shared.buyCurrency.locked=Comprar {0} 🔒 shared.sellCurrency.locked=Vender {0} 🔒 shared.buyingXMRWith=Comprando XMR con {0} shared.sellingXMRFor=Vendiendo XMR por {0} shared.buyingCurrency=comprando {0} (Vendiendo XMR) shared.sellingCurrency=Vendiendo {0} (comprando XMR) shared.buy=comprar shared.sell=vender shared.buying=comprando shared.selling=vendiendo shared.P2P=P2P shared.oneOffer=oferta shared.multipleOffers=ofertas shared.Offer=Oferta shared.offerVolumeCode={0} Volumen de oferta shared.openOffers=ofertas abiertas shared.trade=intercambio shared.trades=intercambios shared.openTrades=intercambios abiertos shared.dateTime=Fecha/Hora shared.price=Precio shared.priceWithCur=Precio en {0} shared.priceInCurForCur=precio en {0} por 1 {1} shared.fixedPriceInCurForCur=Precio fijo en {0} por 1 {1} shared.amount=Cantidad shared.txFee=Tasa de transacción shared.tradeFee=Tasa de intercambio shared.buyerSecurityDeposit=Depósito de comprador shared.sellerSecurityDeposit=Depósito de vendedor shared.amountWithCur=Cantidad en {0} shared.volumeWithCur=Volumen en {0} shared.currency=Moneda shared.market=Mercado shared.deviation=Desviación shared.paymentMethod=Método de pago shared.tradeCurrency=Moneda de intercambio shared.offerType=Tipo de oferta shared.details=Detalles shared.address=Dirección shared.balanceWithCur=Saldo en {0} shared.utxo=Output de transacción no gastado shared.txId=ID de la transacción shared.confirmations=Confirmaciones shared.revert=Revertir Tx shared.select=Seleccionar shared.usage=Uso shared.state=Estado shared.tradeId=ID de intercambio shared.offerId=ID de oferta shared.bankName=Nombre del banco shared.acceptedBanks=Bancos aceptados shared.amountMinMax=Cantidad (min-max) shared.amountHelp=Si una oferta tiene una cantidad mínima y máxima establecida, entonces puede intercambiar cualquier cantidad dentro de este rango. shared.remove=Eliminar shared.goTo=Ir a {0} shared.XMRMinMax=XMR (min - max) shared.removeOffer=Eliminar oferta shared.dontRemoveOffer=No eliminar oferta shared.editOffer=Editar oferta shared.openLargeQRWindow=Abrir código QR en ventana grande shared.tradingAccount=Cuenta de intercambio shared.faq=Visitar web preguntas frecuentes shared.yesCancel=Sí, cancelar shared.nextStep=Siguiente paso shared.selectTradingAccount=Selecionar cuenta de intercambio shared.fundFromSavingsWalletButton=Aplicar fondos desde la billetera de Haveno shared.fundFromExternalWalletButton=Abrir su monedero externo para agregar fondos shared.openDefaultWalletFailed=Fallo al abrir la aplicación de cartera predeterminada. ¿Tal vez no tenga una instalada? shared.belowInPercent=% por debajo del precio de mercado shared.aboveInPercent=% por encima del precio de mercado shared.enterPercentageValue=Introduzca valor % shared.OR=ó shared.notEnoughFunds=No tiene suficientes fondos en su monedero haveno para esta transacción. Necesita {0} pero solo tiene {1} disponibles.\n\nPor favor deposite desde un monedero externo o agregue fondos a su monedero Haveno en Fondos > Recibir Fondos. shared.waitingForFunds=Esperando fondos... shared.TheXMRBuyer=El comprador de XMR shared.You=Usted shared.sendingConfirmation=Enviando confirmación... shared.sendingConfirmationAgain=Por favor envíe confirmación de nuevo shared.exportCSV=Exportar a csv shared.exportJSON=Exportar a JSON shared.summary=Mostrar resumen shared.noDateAvailable=Sin fecha disponible shared.noDetailsAvailable=Sin detalles disponibles shared.notUsedYet=Sin usar aún shared.date=Fecha shared.sendFundsDetailsWithFee=Enviando: {0}\n\nA la dirección receptora: {1}\n\nTarifa adicional para el minero: {2}\n\n¿Estás seguro de que deseas enviar esta cantidad? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detectó que esta transacción crearía una salida que está por debajo del umbral mínimo considerada polvo (y no está permitida por las reglas de consenso en Monero). En cambio, esta transacción polvo ({0} satoshi {1}) se agregará a la tarifa de minería.\n\n\n shared.copyToClipboard=Copiar al portapapeles shared.language=Idioma shared.country=País shared.applyAndShutDown=Aplicar y cerrar shared.selectPaymentMethod=Seleccionar método de pago shared.accountNameAlreadyUsed=Ese nombre de cuenta ya está en uso para otra cuenta guardada.\nPor favor use otro nombre. shared.askConfirmDeleteAccount=¿Realmente quiere borrar la cuenta seleccionada? shared.cannotDeleteAccount=No puede borrar esta cuenta porque está siendo usada en una oferta abierta (o en un intercambio abierto). shared.noAccountsSetupYet=Aún no hay cuentas configuradas. shared.manageAccounts=Gestionar cuentas shared.addNewAccount=Añadir una nueva cuenta shared.ExportAccounts=Exportar cuentas shared.importAccounts=Importar cuentas shared.createNewAccount=Crear nueva cuenta shared.createNewAccountDescription=Los detalles de su cuenta se almacenan localmente en su dispositivo y se comparten solo con su contraparte comercial y el árbitro si se abre una disputa. shared.saveNewAccount=Guardar nueva cuenta shared.selectedAccount=Cuenta seleccionada shared.deleteAccount=Borrar cuenta shared.errorMessageInline=\nMensaje de error: {0} shared.errorMessage=Mensaje de error shared.information=Información shared.name=Nombre shared.id=ID shared.dashboard=Panel de control shared.accept=Aceptar shared.balance=Saldo shared.save=Guardar shared.onionAddress=Dirección onion shared.supportTicket=Ticket de soporte shared.dispute=Disputa shared.mediationCase=caso de mediación shared.seller=vendedor shared.buyer=comprador shared.allEuroCountries=Todos los países Euro shared.acceptedTakerCountries=Países aceptados como tomador shared.tradePrice=Precio de intercambio shared.tradeAmount=Cantidad de intercambio shared.tradeVolume=Volumen de intercambio shared.invalidKey=La clave que ha introducido no es correcta. shared.enterPrivKey=Introducir clave privada para desbloquear shared.payoutTxId=ID de transacción de pago shared.contractAsJson=Contrato en formato JSON shared.viewContractAsJson=Ver contrato en formato JSON shared.contract.title=Contrato de intercambio con ID: {0} shared.paymentDetails=Detalles de pago XMR {0} shared.securityDeposit=Depósito de seguridad shared.yourSecurityDeposit=Su depósito de seguridad shared.contract=Contrato shared.messageArrived=Mensaje recibido. shared.messageStoredInMailbox=Mensaje almacenado en buzón. shared.messageSendingFailed=Envío de mensaje fallido. Error: {0} shared.unlock=Desbloquear shared.toReceive=a recibir shared.toSpend=a gastar shared.xmrAmount=Cantidad XMR shared.yourLanguage=Sus idiomas shared.addLanguage=Añadir idioma shared.total=Total shared.totalsNeeded=Fondos necesarios shared.tradeWalletAddress=Dirección de la cartera para intercambio shared.tradeWalletBalance=Saldo de la cartera de intercambio shared.reserveExactAmount=Reserve solo los fondos necesarios. Requiere una tarifa de minería y aproximadamente 20 minutos antes de que tu oferta se haga pública. shared.makerTxFee=Creador: {0} shared.takerTxFee=Tomador: {0} shared.iConfirm=Confirmo shared.openURL=Abrir {0} shared.fiat=Fiat shared.crypto=Cripto shared.preciousMetals=Metales Preciosos shared.all=Todos shared.edit=Editar shared.advancedOptions=Opciones avanzadas shared.interval=Intervalo shared.actions=Acciones shared.buyerUpperCase=Comprador shared.sellerUpperCase=Vendedor shared.new=NUEVO shared.learnMore=Aprender más shared.dismiss=Descartar shared.selectedArbitrator=Árbitro seleccionado shared.selectedMediator=Mediador seleccionado shared.selectedRefundAgent=Árbitro seleccionado shared.mediator=Mediador shared.arbitrator=Árbitro shared.refundAgent=Árbitro shared.refundAgentForSupportStaff=Agente de devolución de fondos shared.delayedPayoutTxId=ID de transacción del pago demorado shared.delayedPayoutTxReceiverAddress=Transacción de pago demorado enviada a shared.unconfirmedTransactionsLimitReached=Tiene demasiadas transacciones no confirmadas en este momento. Por favor, inténtelo de nuevo más tarde. shared.numItemsLabel=Número de entradas: {0} shared.filter=Filtro shared.enabled=Habilitado #################################################################### # UI views #################################################################### #################################################################### # MainView #################################################################### mainView.menu.market=Mercado mainView.menu.buyXmr=Comprar XMR mainView.menu.sellXmr=Vender XMR mainView.menu.portfolio=Portafolio mainView.menu.funds=Fondos mainView.menu.support=Soporte mainView.menu.settings=Configuración mainView.menu.account=Cuenta mainView.marketPriceWithProvider.label=Precio de mercado por {0} mainView.marketPrice.havenoInternalPrice=Precio del último intercambio en Haveno mainView.marketPrice.tooltip.havenoInternalPrice=No existe un precio de mercado disponible proveniente de fuentes externas.\nEl precio mostrado es el último precio de intercambio en Haveno para esa moneda. mainView.marketPrice.tooltip=Precio de mercado ofrecido por {0}{1}\nÚltima actualización: {2}\nURL del nodo proveedor: {3} mainView.balance.available=Saldo disponible mainView.balance.reserved=Reservado en ofertas mainView.balance.pending=Bloqueado en intercambios mainView.balance.reserved.short=Reservado mainView.balance.pending.short=Bloqueado mainView.footer.usingTor=(via Tor) mainView.footer.localhostMoneroNode=(localhost) mainView.footer.clearnet=(via clearnet) mainView.footer.xmrInfo={0} {1} mainView.footer.xmrFeeRate=/Tasas actuales: {0} sat/vB mainView.footer.xmrInfo.initializing=Conectando a la red Haveno mainView.footer.xmrInfo.synchronizingWith=Sincronizando con {0} en el bloque: {1} / {2} mainView.footer.xmrInfo.connectedTo=Conectado a {0} en el bloque {1} mainView.footer.xmrInfo.synchronizingWalletWith=Sincronizando la billetera con {0} en el bloque: {1} / {2} mainView.footer.xmrInfo.syncedWith=Sincronizado con {0} en el bloque {1} mainView.footer.xmrInfo.connectingTo=Conectando a mainView.footer.xmrInfo.connectionFailed=Conexión fallida a mainView.footer.xmrPeers=Pares de Monero: {0} mainView.footer.p2pPeers=Pares de la red de Haveno: {0} mainView.bootstrapState.connectionToTorNetwork=(1/4) Conectando a la red Tor... mainView.bootstrapState.torNodeCreated=(2/4) Nodo Tor creado mainView.bootstrapState.hiddenServicePublished=(3/4) Servicio oculto publicado mainView.bootstrapState.initialDataReceived=(4/4) Datos iniciales recibidos mainView.bootstrapWarning.noSeedNodesAvailable=No hay nodos de siembra disponibles mainView.bootstrapWarning.noNodesAvailable=No hay nodos de sembrado y pares disponibles mainView.bootstrapWarning.bootstrappingToP2PFailed=Fallo al conectarse a la red Haveno en el arranque mainView.p2pNetworkWarnMsg.noNodesAvailable=No hay nodos de sembrado o puntos de red persistentes para los datos requeridos.\nPor favor, compruebe su conexión a Internet o intente reiniciar la aplicación. mainView.p2pNetworkWarnMsg.connectionToP2PFailed=Fallo conectándose a la red Haveno (error reportado: {0}).\nPor favor, compruebe su conexión a internet o pruebe reiniciando la aplicación. mainView.walletServiceErrorMsg.timeout=Error al conectar a la red Monero en el límite de tiempo establecido. mainView.walletServiceErrorMsg.connectionError=La conexión a la red Monero falló por un error: {0} mainView.walletServiceErrorMsg.rejectedTxException=Se rechazó una transacción desde la red.\n\n{0} mainView.networkWarning.allConnectionsLost=Perdió la conexión a todos los {0} usuarios de red.\nTal vez se ha interrumpido su conexión a Internet o su computadora estaba en modo suspendido. mainView.networkWarning.localhostMoneroLost=Perdió la conexión al nodo Monero localhost.\nPor favor reinicie la aplicación Haveno para conectarse a otros nodos Monero o reinice el nodo Monero localhost. mainView.version.update=(Actualización disponible) #################################################################### # MarketView #################################################################### market.tabs.offerBook=Libro de ofertas market.tabs.spreadCurrency=Ofertas según moneda market.tabs.spreadPayment=Ofertas según método de pago market.tabs.trades=Intercambios # OfferBookChartView market.offerBook.buyCrypto=Comprar {0} (vender {1}) market.offerBook.sellCrypto=Vender {0} (comprar {1}) market.offerBook.buyWithTraditional=Comprar {0} market.offerBook.sellWithTraditional=Vender {0} market.offerBook.sellOffersHeaderLabel=Vender {0} a market.offerBook.buyOffersHeaderLabel=Comprar {0} de market.offerBook.buy=Quiero comprar monero market.offerBook.sell=Quiero vender monero # SpreadView market.spread.numberOfOffersColumn=Todas las ofertas ({0}) market.spread.numberOfBuyOffersColumn=Comprar XMR ({0}) market.spread.numberOfSellOffersColumn=Vender XMR ({0}) market.spread.totalAmountColumn=Total XMR ({0}) market.spread.spreadColumn=Diferencial market.spread.expanded=Vista expandida # TradesChartsView market.trades.nrOfTrades=Intercambios: {0} market.trades.tooltip.volumeBar=Volumen: {0} / {1}\nNúmero de intercambios: {2}\nFecha: {3} market.trades.tooltip.candle.open=Apertura: market.trades.tooltip.candle.close=Cierre: market.trades.tooltip.candle.high=Máximo: market.trades.tooltip.candle.low=Mínimo: market.trades.tooltip.candle.average=Media: market.trades.tooltip.candle.median=Mediana: market.trades.tooltip.candle.date=Fecha: market.trades.showVolumeInUSD=Mostrar volumen en USD #################################################################### # OfferView #################################################################### offerbook.createOffer=Crear oferta offerbook.takeOffer=Tomar oferta offerbook.takeOfferToBuy=Tomar oferta de compra de {0} offerbook.takeOfferToSell=Tomar oferta de venta de {0} offerbook.takeOffer.enterChallenge=Introduzca la frase secreta de la oferta offerbook.trader=Trader offerbook.offerersBankId=ID del banco del creador (BIC/SWIFT): {0} offerbook.offerersBankName=Nombre del banco del creador: {0} offerbook.offerersBankSeat=País de establecimiento del banco del creador: {0} offerbook.offerersAcceptedBankSeatsEuro=País de establecimiento del banco aceptado (tomador): Todos los países Euro offerbook.offerersAcceptedBankSeats=Países de sede de banco aceptados (tomador):\n {0} offerbook.availableOffers=Ofertas disponibles offerbook.filterByCurrency=Filtrar por moneda offerbook.filterByPaymentMethod=Filtrar por método de pago offerbook.matchingOffers=Ofertas que concuerden con mis cuentas offerbook.filterNoDeposit=Sin depósito offerbook.noDepositOffers=Ofertas sin depósito (se requiere frase de paso) offerbook.timeSinceSigning=Información de la cuenta offerbook.timeSinceSigning.info=Esta cuenta fue verificada y {0} offerbook.timeSinceSigning.info.arbitrator=firmada por un árbitro y puede firmar cuentas de pares offerbook.timeSinceSigning.info.peer=firmado por un par, esperando %d días para aumentar límites offerbook.timeSinceSigning.info.peerLimitLifted=firmador por un par y los límites se elevaron offerbook.timeSinceSigning.info.signer=firmado por un par y puede firmar cuentas de pares (límites elevados) offerbook.timeSinceSigning.info.banned=La cuenta fue bloqueada offerbook.timeSinceSigning.daysSinceSigning={0} días offerbook.timeSinceSigning.daysSinceSigning.long={0} desde el firmado offerbook.xmrAutoConf=¿Está habilitada la confirmación automática? offerbook.buyXmrWith=Compra XMR con: offerbook.sellXmrFor=Vender XMR por: offerbook.timeSinceSigning.help=Cuando complete con éxito un intercambio con un par que tenga una cuenta de pago firmada, su cuenta de pago es firmada.\n{0} días después, el límite inicial de {1} se eleva y su cuenta puede firmar tras cuentas de pago. offerbook.timeSinceSigning.notSigned=No firmada aún offerbook.timeSinceSigning.notSigned.ageDays={0} días offerbook.timeSinceSigning.notSigned.noNeed=No disponible shared.notSigned=Esta cuenta no ha sido firmada aún y fue creada hace {0} días shared.notSigned.noNeed=Este tipo de cuenta no necesita firmado shared.notSigned.noNeedDays=Este tipo de cuenta no necesita firmado y fue creada hace {0} días shared.notSigned.noNeedAlts=Las cuentas de crypto no necesitan firmado o edad offerbook.nrOffers=Número de ofertas: {0} offerbook.volume={0} (min - max) offerbook.deposit=Depósito en XMR (%) offerbook.deposit.help=Depósito pagado por cada comerciante para garantizar el intercambio. Será devuelto al acabar el intercambio. offerbook.createNewOffer=Crear oferta a {0} {1} offerbook.createOfferToBuy=Crear nueva oferta para comprar {0} offerbook.createOfferToSell=Crear nueva oferta para vender {0} offerbook.createOfferToBuy.withTraditional=Crear nueva oferta para comprar {0} con {1} offerbook.createOfferToSell.forTraditional=Crear nueva oferta para vender {0} por {1} offerbook.createOfferToBuy.withCrypto=Crear nueva oferta para vender {0} (comprar {1}) offerbook.createOfferToSell.forCrypto=Crear nueva oferta para comprar {0} (vender {1}) offerbook.takeOfferButton.tooltip=Tomar oferta {0} offerbook.yesCreateOffer=Sí, crear oferta offerbook.setupNewAccount=Configurar una nueva cuenta de intercambio offerbook.removeOffer.success=Oferta eliminada con éxito. offerbook.removeOffer.failed=Fallo en la eliminación de oferta:\n{0} offerbook.deactivateOffer.failed=Error desactivando la oferta:\n{0} offerbook.activateOffer.failed=Fallo en la publicación de la oferta:\n{0} offerbook.withdrawFundsHint=Puede retirar los fondos pagados desde la pantalla {0}. offerbook.warning.noTradingAccountForCurrency.headline=No hay cuenta de intercambio para la moneda seleccionada offerbook.warning.noTradingAccountForCurrency.msg=No tiene una cuenta de pago para la moneda seleccionada.\n¿Desea crear una oferta con otra moneda en su lugar? offerbook.warning.noMatchingAccount.headline=No La cuenta de pago no concuerda. offerbook.warning.noMatchingAccount.msg=Esta oferta usa un método de pago que no tiene configurado.\n\n¿Quiere configurar un nuevo método de pago ahora? offerbook.warning.counterpartyTradeRestrictions=Esta oferta no puede tomarse debido a restricciones de intercambio de la contraparte offerbook.warning.newVersionAnnouncement=Con esta versión de software, los pares de intercambio pueden verificar y firmar entre sí sus cuentas de pago para crear una red de cuentas de pago de confianza.\n\nDespués de intercambiar con éxito con un par con una cuenta de pago verificada, su cuenta de pago será firmada y los límites de intercambio se elevarán después de un cierto intervalo de tiempo (la duración de este intervalo depende del método de verificación).\n\nPara más información acerca del firmado de cuentas, por favor vea la documentación en [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. popup.warning.tradeLimitDueAccountAgeRestriction.seller=El monto de intercambio permitido está limitado a {0} debido a restricciones de seguridad basadas en los siguientes criterios:\n- La cuenta del comprador no ha sido firmada por un árbitro o par\n- El tiempo desde el firmado de la cuenta del comprador no es de al menos 30 días.\n- el método de pago para esta oferta se considera riesgoso para devoluciones de cargo\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=El monto de intercambio permitido está limitado a {0} debido a restricciones de seguridad basadas en los siguientes criterios:\n- Su cuenta de pago no ha sido firmada por un árbitro o par\n- El tiempo desde el firmado de su cuenta no es de al menos 30 días\n- El método de pago para esta oferta se considera riesgoso para devoluciones de cargo\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=Este método de pago está temporalmente limitado a {0} hasta {1} porque todos los compradores tienen cuentas nuevas.\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=Tu oferta estará limitada a compradores con cuentas firmadas y antiguas porque excede {0}.\n\n{1} offerbook.warning.wrongTradeProtocol=Esta oferta requiere un protocolo de intercambio diferente al utilizado en su versión del software.\n\nPor favor, compruebe que tiene instalada la última versión del software, o de otra forma el usuario que creó la oferta ha utilizado una versión más antigua que la suya.\n\nLos usuarios no pueden realizar transacciones con una versión de protocolo de intercambio incompatible. offerbook.warning.userIgnored=Ha añadido esta dirección onion a la lista de ignorados. offerbook.warning.offerBlocked=Esta oferta ha sido bloqueada por los desarrolladores de Haveno.\nProbablemente existe un error de software desatendido que causa problemas al tomar esta oferta. offerbook.warning.currencyBanned=La moneda utilizada en esta oferta fue bloqueada por los desarrolladores de Haveno.\nPor favor visite el Forum de Haveno para más información. offerbook.warning.paymentMethodBanned=El método de pago utilizado en esta oferta fue bloqueado por los desarrolladores de Haveno.\nPor favor visite el Forum Haveno para más información. offerbook.warning.nodeBlocked=La dirección onion de este comerciante ha sido bloqueada por los desarrolladores de Haveno.\nProbablemente existe un error de software desatendido que causa problemas al tomar ofertas de este comerciante. offerbook.warning.requireUpdateToNewVersion=Su versión de Haveno ya no es compatible para realizar intercambios.\nPor favor actualice a la última versión de Haveno en [HYPERLINK:https://haveno.exchange/downloads]. offerbook.warning.offerWasAlreadyUsedInTrade=No puede aceptar esta oferta porque ya lo hizo antes. Podría ser que su intento anterior de aceptar esta oferta haya terminado como un intercambio fallido. offerbook.info.sellAtMarketPrice=Venderá a precio de mercado (actualizado cada minuto). offerbook.info.buyAtMarketPrice=Comprará a precio de mercado (actualizado cada minuto). offerbook.info.sellBelowMarketPrice=Recibirá {0} menos que el precio de mercado actual (actualizado cada minuto). offerbook.info.buyAboveMarketPrice=Pagará {0} más que el precio de mercado actual (actualizado cada minuto). offerbook.info.sellAboveMarketPrice=Recibirá {0} más que el precio de mercado actual (actualizado cada minuto). offerbook.info.buyBelowMarketPrice=Pagará {0} menos que el precio de mercado actual (actualizado cada minuto) offerbook.info.buyAtFixedPrice=Comprará a este precio fijo. offerbook.info.sellAtFixedPrice=Venderá a este precio fijo. offerbook.info.noArbitrationInUserLanguage=En caso de disputa, tenga en cuenta que el arbitraje para esta oferta se manejará en {0}. El idioma actualmente está configurado en {1}. offerbook.info.roundedFiatVolume=La cantidad se redondeó para incrementar la privacidad de su intercambio. #################################################################### # Offerbook / Create offer #################################################################### createOffer.amount.prompt=Introducir cantidad en XMR createOffer.price.prompt=Introducir precio createOffer.volume.prompt=Introducir cantidad en {0} createOffer.amountPriceBox.amountDescription=Cantidad de XMR a {0} createOffer.amountPriceBox.buy.volumeDescription=Cantidad a gastar en {0} createOffer.amountPriceBox.sell.volumeDescription=Cantidad a recibir en {0}. createOffer.amountPriceBox.minAmountDescription=Cantidad mínima de XMR createOffer.securityDeposit.prompt=Depósito de seguridad createOffer.fundsBox.title=Dote de fondos su oferta. createOffer.fundsBox.offerFee=Comisión de transacción createOffer.fundsBox.networkFee=Comisión de minado createOffer.fundsBox.placeOfferSpinnerInfo=Publicación de oferta en curso... createOffer.fundsBox.paymentLabel=Intercambio Haveno con ID {0} createOffer.fundsBox.fundsStructure=({0} depósito de seguridad, {1} comisión de transacción, {2} comisión de minado) createOffer.success.headline=Su oferta ha sido creada createOffer.success.info=Puede gestionar sus ofertas abiertas en \"Portafolio/Mis ofertas abiertas\". createOffer.info.sellAtMarketPrice=Siempre venderá a precio de mercado ya que el precio de su oferta será actualizado continuamente. createOffer.info.buyAtMarketPrice=Siempre comprará a precio de mercado ya que el precio de su oferta será actualizado continuamente. createOffer.info.sellAboveMarketPrice=Siempre tendrá {0}% más que el precio de mercado ya que el precio de su oferta será actualizado continuamente. createOffer.info.buyBelowMarketPrice=Siempre pagará {0}% menos que el precio de mercado ya que el precio de su oferta será actualizado continuamente. createOffer.warning.sellBelowMarketPrice=Siempre tendrá {0}% menos que el precio de mercado ya que el precio de su oferta será actualizado continuamente. createOffer.warning.buyAboveMarketPrice=Siempre pagará {0}% más que el precio de mercado ya que el precio de su oferta será actualizado continuamente. createOffer.tradeFee.descriptionXMROnly=Comisión de transacción createOffer.tradeFee.descriptionBSQEnabled=Seleccionar moneda de comisión de intercambio createOffer.triggerPrice.prompt=Establecer precio de ejecución opcional createOffer.triggerPrice.label=Desactivar oferta si el precio de mercado es {0} createOffer.triggerPrice.tooltip=Como protección contra movimientos drásticos de precio puede establecer un precio de ejecución que desactive la oferta si el precio de mercado alcanza ese valor. createOffer.triggerPrice.invalid.tooLow=El valor debe ser superior a {0} createOffer.triggerPrice.invalid.tooHigh=El valor debe ser inferior a {0} # new entries createOffer.placeOfferButton.buy=Revisar: Crear oferta para comprar XMR con {0} createOffer.placeOfferButton.sell=Revisar: Crear oferta para vender XMR por {0} createOffer.createOfferFundWalletInfo.headline=Dote de fondos su trato. # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Cantidad a intercambiar: {0}\n createOffer.createOfferFundWalletInfo.msg=Necesitas depositar {0} para esta oferta.\n\n\ Estos fondos están reservados en tu billetera local y se bloquearán en una billetera multisig una vez que alguien acepte tu oferta.\n\n\ El monto es la suma de:\n\ {1}\ - Tu depósito de seguridad: {2}\n\ - Comisión de comercio: {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=Ocurrió un error al colocar la oferta:\n\n{0}\n\nNingún importe de su cartera ha sido deducido aún.\nPor favor, reinicie su aplicación y compruebe su conexión a la red. createOffer.setAmountPrice=Establezca cantidad y precio createOffer.warnCancelOffer=Ya ha financiado esa oferta. Si la cancela ahora, sus fondos permanecerán en su billetera local de Haveno y estarán disponibles para su retiro en la pantalla "Fondos/Enviar fondos". ¿Está seguro de que desea cancelar? createOffer.timeoutAtPublishing=Error. Fuera de tiempo en la publicación de la oferta. createOffer.errorInfo=\n\nLa tasa del creador ya se ha pagado. En el peor caso ha perdido esa tasa. Lo sentimos, pero tenga en cuenta que es una cantidad pequeña.\nPor favor pruebe a reiniciar su aplicación y compruebe la conexión a la red para ver si puede resolver el asunto. createOffer.tooLowSecDeposit.warning=Ha configurado el depósito de seguridad en un valor más bajo que el recomendado de forma predeterminada, que es {0}.\n¿Está seguro que quiere usar un valor de depósito de seguridad más bajo? createOffer.tooLowSecDeposit.makerIsSeller=Le da menos protección en caso de que el participante en la transacción no siga el protocolo de intercambio. createOffer.tooLowSecDeposit.makerIsBuyer=Esto resulta en menos protección para el otro participante en la transacción si usted continua el protocolo de intercambio, pues tiene menos depósitos en riesgo. Otros usuarios podrían preferir tomar otras ofertas en vez de la suya. createOffer.resetToDefault=No, restablecer al valor por defecto createOffer.useLowerValue=Sí, usar mi valor más bajo createOffer.priceOutSideOfDeviation=El precio que ha introducido está fuera de la máxima desviación permitida en relación al precio de mercado.\nLa desviación máxima permitida es {0} y puede ajustarse en las preferencias. createOffer.changePrice=Cambiar precio createOffer.tac=Al colocar esta oferta estoy de acuerdo en comerciar con cualquier comerciante que cumpla con las condiciones definidas anteriormente. createOffer.currencyForFee=Tasa de transacción createOffer.setDeposit=Establecer depósito de seguridad para el comprador (%) createOffer.setDepositAsBuyer=Establecer mi depósito de seguridad como comprador (%) createOffer.setDepositForBothTraders=Establecer el depósito de seguridad para los comerciantes (%) createOffer.securityDepositInfo=Su depósito de seguridad como comprador será {0} createOffer.securityDepositInfoAsBuyer=Su depósito de seguridad como comprador será {0} createOffer.minSecurityDepositUsed=Se utiliza un depósito de seguridad mínimo createOffer.buyerAsTakerWithoutDeposit=No se requiere depósito del comprador (protegido por passphrase) createOffer.myDeposit=Mi depósito de seguridad (%) createOffer.myDepositInfo=Tu depósito de seguridad será {0} #################################################################### # Offerbook / Take offer #################################################################### takeOffer.amount.prompt=Introducir la cantidad en XMR takeOffer.amountPriceBox.buy.amountDescription=Cantidad de XMR a vender takeOffer.amountPriceBox.sell.amountDescription=Cantidad de XMR a comprar takeOffer.amountPriceBox.priceDescription=Precio por monero en {0} takeOffer.amountPriceBox.amountRangeDescription=Rango de cantidad posible. takeOffer.amountPriceBox.warning.invalidXmrDecimalPlaces=La cantidad introducida excede el número de decimales permitidos.\nLa cantidad ha sido ajustada a 4 decimales. takeOffer.validation.amountSmallerThanMinAmount=La cantidad no puede ser menor que el mínimo definido en la oferta. takeOffer.validation.amountLargerThanOfferAmount=La cantidad introducida no puede ser mayor que el máximo definido en la oferta. takeOffer.validation.amountLargerThanOfferAmountMinusFee=La cantidad introducida crearía polvo (dust change) para el vendedor de monero. takeOffer.fundsBox.title=Dote de fondos su intercambio. takeOffer.fundsBox.isOfferAvailable=Comprobar si la oferta está disponible... takeOffer.fundsBox.tradeAmount=Cantidad a vender takeOffer.fundsBox.offerFee=Comisión de transacción takeOffer.fundsBox.networkFee=Comisiones de minado totales takeOffer.fundsBox.takeOfferSpinnerInfo=Aceptando oferta: {0} takeOffer.fundsBox.paymentLabel=Intercambio Haveno con ID {0} takeOffer.fundsBox.fundsStructure=({0} depósito de seguridad {1} tasa de intercambio, {2} tarifa de minado) takeOffer.fundsBox.noFundingRequiredTitle=No se requiere financiamiento takeOffer.fundsBox.noFundingRequiredDescription=Obtén la frase de acceso de la oferta del vendedor fuera de Haveno para aceptar esta oferta. takeOffer.success.headline=Ha aceptado la oferta con éxito. takeOffer.success.info=Puede ver el estado de su intercambio en \"Portafolio/Intercambios abiertos\". takeOffer.error.message=Un error ocurrió al tomar la oferta.\n\n{0} # new entries takeOffer.takeOfferButton.buy=Revisar: Aceptar oferta para comprar XMR con {0} takeOffer.takeOfferButton.sell=Revisar: Aceptar oferta para vender XMR por {0} takeOffer.noPriceFeedAvailable=No puede tomar esta oferta porque utiliza un precio porcentual basado en el precio de mercado y no hay fuentes de precio disponibles. takeOffer.takeOfferFundWalletInfo.headline=Dotar de fondos su intercambio # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Cantidad a intercambiar: {0}\n takeOffer.takeOfferFundWalletInfo.msg=Necesitas depositar {0} para aceptar esta oferta.\n\nLa cantidad es la suma de:\n{1}- Tu depósito de seguridad: {2}\n- Tarifa de transacción: {3} takeOffer.alreadyPaidInFunds=Si ya ha depositado puede retirarlo en la pantalla \"Fondos/Disponible para retirar\". takeOffer.paymentInfo=Información de pago takeOffer.setAmountPrice=Establecer cantidad takeOffer.alreadyFunded.askCancel=Ya ha financiado esa oferta. Si la cancela ahora, sus fondos permanecerán en su billetera local de Haveno y estarán disponibles para su retiro en la pantalla "Fondos/Enviar fondos". ¿Está seguro de que desea cancelar? takeOffer.failed.offerNotAvailable=Falló la solicitud de toma de oferta porque la oferta ya no está disponible. Tal vez otro comerciante la haya tomado en su lugar. takeOffer.failed.offerTaken=No puede tomar la oferta porque la oferta fue tomada por otro comerciante. takeOffer.failed.offerRemoved=No puede tomar esta oferta porque la oferta ha sido eliminada. takeOffer.failed.offererNotOnline=La solicitud de toma de oferta falló porque el creador no se encuentra online. takeOffer.failed.offererOffline=No puede tomar la oferta porque el tomador está offline. takeOffer.warning.connectionToPeerLost=Ha perdido conexión con el creador.\nPuede haberse desconectado o haber cortado la conexión hacia usted debido a que existan demasiadas conexiones abiertas.\n\nSi aún puede ver la oferta en el libro de ofertas puede intentar tomarla de nuevo. takeOffer.error.noFundsLost=\n\nNingún importe de su cartera ha sido deducido aún.\nPor favor intente reiniciar su aplicación y compruebe la conexión a la red para ver si puede resolver el problema. # suppress inspection "TrailingSpacesInProperty" takeOffer.error.feePaid=.\n\n takeOffer.error.depositPublished=\n\nLa transacción de depósito ya se ha publicado.\nPor favor intente reiniciar su aplicación y compruebe su conexión a la red para ver si puede resolver el problema.\nSi el problema persiste, por favor contacte a los desarrolladores para solicitar asistencia. takeOffer.error.payoutPublished=\n\nLa transacción de pago ya se ha publicado.\nPor favor intente reiniciar su aplicación y compruebe su conexión a la red para ver si puede resolver el problema.\nSi el problema persiste, por favor contacte a los desarrolladores para solicitar asistencia. takeOffer.tac=Al tomar esta oferta, afirmo estar de acuerdo con las condiciones de intercambio definidas anteriormente en esta pantalla. #################################################################### # Offerbook / Edit offer #################################################################### openOffer.header.triggerPrice=Precio de ejecución openOffer.triggerPrice=Precio de ejecución {0} openOffer.triggered=La oferta ha sido desactivada porque el precio de mercado alcanzó su precio de disparo.\nPor favor edite la oferta para definir un nuevo precio de disparo. editOffer.setPrice=Establecer precio editOffer.confirmEdit=Confirmar: Editar oferta editOffer.publishOffer=Publicando su oferta. editOffer.failed=Fallo en la edición de oferta:\n{0} editOffer.success=Su oferta ha sido editada con éxito. editOffer.invalidDeposit=El depósito de seguridad del comprador no está dentro de los límites definidos por la DAO Haveno y ya no puede ser ser editado. #################################################################### # Portfolio #################################################################### portfolio.tab.openOffers=Mis ofertas abiertas portfolio.tab.pendingTrades=Intercambios abiertos portfolio.tab.history=Historial portfolio.tab.failed=Fallidas portfolio.tab.editOpenOffer=Editar oferta portfolio.closedTrades.deviation.help=Desviación porcentual de precio de mercado portfolio.pending.invalidTx=Hay un problema con una transacción inválida o no encontrada.\n\nPor faovr NO envíe el pago de traditional o cryptos.\n\nAbra un ticket de soporte para obtener asistencia de un mediador.\n\nMensaje de error: {0} portfolio.pending.syncing=Sincronizando la cartera de operaciones portfolio.pending.syncing.blockRemaining=Sincronizando la cartera de operaciones — queda 1 bloque portfolio.pending.syncing.blocksRemaining=Sincronizando la cartera de operaciones — quedan {0} bloques portfolio.pending.step1.waitForConf=Esperar a la confirmación en la cadena de bloques portfolio.pending.step2_buyer.additionalConf=Los depósitos han alcanzado 10 confirmaciones.\nPara mayor seguridad, recomendamos esperar {0} confirmaciones antes de enviar el pago.\nProceda antes bajo su propio riesgo. portfolio.pending.step2_buyer.startPayment=Comenzar pago portfolio.pending.step2_seller.waitPaymentSent=Esperar hasta que el pago se haya iniciado portfolio.pending.step3_buyer.waitPaymentArrived=Esperar hasta que el pago haya llegado portfolio.pending.step3_seller.confirmPaymentReceived=Confirmar recepción de pago portfolio.pending.step5.completed=Completado portfolio.pending.step3_seller.autoConf.status.label=Estado de autoconfirmación portfolio.pending.autoConf=Auto-confirmado portfolio.pending.autoConf.blocks=Confirmaciones XMR: {0} / Requerido: {1} portfolio.pending.autoConf.state.xmr.txKeyReused=Clave de transacción re-utilizada. Por favor, abra una disputa. portfolio.pending.autoConf.state.confirmations=Confirmaciones XMR: {0}/{1} portfolio.pending.autoConf.state.txNotFound=Transacción no vista aún en la mempool portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=ID de transacción no válida / clave de transacción portfolio.pending.autoConf.state.filterDisabledFeature=Deshabilitado por los desarrolladores # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FEATURE_DISABLED=La función de autoconfirmación está deshabilitada. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=La cantidad de intercambio excede el límite. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.INVALID_DATA=El par entregó datos inválidos. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=La transacción de pago ya fue publicada. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.DISPUTE_OPENED=La disputa se abrió. La autoconfirmación se ha desactivado para este intercambio. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.REQUESTS_STARTED=La solicitud de prueba de transacción comenzó # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PENDING=Resultados de éxito: {0}/{1}; {2} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.COMPLETED=Prueba con éxito en todos los servicios # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.ERROR=Ocurrió un error en el servicio solicitado. No es posible la autoconfirmación. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=Un servicio volvió con algún fallo. No es posible la autoconfirmación. portfolio.pending.step1.info.you=La transacción de depósito ha sido publicada.\nPuede iniciar el pago después de 10 confirmaciones (aprox. {0} minutos restantes). portfolio.pending.step1.info.buyer=La transacción de depósito ha sido publicada.\nEl comprador de XMR puede iniciar el pago después de 10 confirmaciones (aprox. {0} minutos restantes). portfolio.pending.step1.warn=La transacción del depósito aún no se ha confirmado.\nEsto puede suceder en raras ocasiones cuando la tasa de depósito de un comerciante desde una cartera externa es demasiado baja. portfolio.pending.step1.openForDispute=La transacción de depósito aún no ha sido confirmada. Puede esperar más o contactar con el mediador para obtener asistencia. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2.confReached=Su intercambio tiene al menos una confirmación en la cadena de bloques.\n\n portfolio.pending.step2_buyer.refTextWarn=Importante: al hacer un pago, deje el campo \"motivo de pago\" vacío. NO PONGA la ID de intercambio o algún otro texto como 'monero', 'XMR', o 'Haveno'. Los comerciantes pueden convenir en el chat de intercambio un \"motivo de pago\" alternativo. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees=Si su banco carga alguna tasa por hacer la transferencia, es responsable de pagar esas tasas. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.crypto=Por favor transfiera fondos desde su cartera externa {0}\n{1} al vendedor de XMR.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.cash=Por favor vaya al banco y pague {0} al vendedor de XMR.\n\n portfolio.pending.step2_buyer.cash.extra=REQUERIMIENTO IMPORTANTE:\nDespués de haber hecho el pago escribe en el recibo de papel: NO REFUNDS\nLuego divídalo en 2 partes, haga una foto y envíela a la dirección de correo electrónico del vendedor de XMR. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.moneyGram=Por favor pague {0} al vendedor de XMR utilizando MoneyGram.\n\n portfolio.pending.step2_buyer.moneyGram.extra=REQUERIMIENTO IMPORTANTE:\nDespués que usted haya realizado el pago, envíe el número de autorización y una foto del recibo al vendedor de XMR por correo electrónico.\nEl recibo debe mostrar claramente el monto, asi como el nombre completo, país y demarcación (departamento,estado, etc.) del vendedor. El correo electrónico del vendedor es: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.westernUnion=Por favor pague {0} al vendedor de XMR usando Western Union.\n\n portfolio.pending.step2_buyer.westernUnion.extra=REQUERIMIENTO IMPORTANTE:\nDespués de haber realizado el pago envíe el MTCN (número de seguimiento) y una foto de el recibo por email a el vendedor de XMR.\nEl recibo debe mostrar claramente el nombre completo del emisor, la ciudad, país y la cantidad. El email del vendedor es: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Por favor envíe {0} mediante \"US Postal Money Order\" a el vendedor de XMR.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Por favor envíe {0} usando \"Efectivo por Correo\" al vendedor. Las instrucciones específicas están en el contrato de intercambio, y si no queda claro, pregunte a través del chat de intercambio.\nVea más detalles acerca de Efectivo por Correo en la wiki de Haveno [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Por favor pague {0} a través del método de pago especificado al vendedor XMR. Encontrará los detalles de la cuenta del vendedor en la siguiente pantalla.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Por favor contacte al vendedor de XMR con el contacto proporcionado y acuerden un encuentro para pagar {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Comenzar pago utilizando {0} portfolio.pending.step2_buyer.recipientsAccountData=Receptores {0} portfolio.pending.step2_buyer.amountToTransfer=Cantidad a transferir portfolio.pending.step2_buyer.sellersAddress=Dirección {0} del vendedor portfolio.pending.step2_buyer.buyerAccount=Su cuenta de pago para ser usada portfolio.pending.step2_buyer.paymentSent=Pago iniciado portfolio.pending.step2_buyer.showEarly=Mostrar los detalles del pago con anticipación portfolio.pending.step2_buyer.warn=¡Todavía no ha realizado su pago {0}!\nPor favor, tenga en cuenta que el pago tiene que completarse antes de {1}. portfolio.pending.step2_buyer.openForDispute=¡No ha completado su pago!\nEl periodo máximo para el intercambio ha concluido. Por favor, contacte con el mediador para abrir una disputa. portfolio.pending.step2_buyer.paperReceipt.headline=¿Ha enviado el recibo a el vendedor de XMR? portfolio.pending.step2_buyer.paperReceipt.msg=Recuerde:\nTiene que escribir en el recibo de papel: NO REFUNDS.\nLuego divídalo en 2 partes, haga una foto y envíela a la dirección de e-mail del vendedor de XMR. portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Enviar número de autorización y recibo portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=Debe enviar el número de autorización y una foto del recibo por correo electrónico al vendedor de XMR.\nEl recibo debe mostrar claramente el monto, así como el nombre completo, país y demarcación (departamento,estado, etc.) del vendedor. El correo electrónico del vendedor es: {0}.\n\n¿Envió usted el número de autorización y el contrato al vendedor? portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=Enviar MTCN y recibo portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Necesita enviar el MTCN (número de seguimiento) y una foto de el recibo por email a el vendedor de XMR\nEl recibo debe mostrar claramente el nombre completo del emisor, la ciudad, el país y la cantidad. El email del vendedor es: {0}\n\n¿Envió el MTCN y el contrato al vendedor? portfolio.pending.step2_buyer.halCashInfo.headline=Enviar código HalCash portfolio.pending.step2_buyer.halCashInfo.msg=Necesita enviar un mensaje de texto con el código HalCash\nEl móvil del vendedor es {1}.\n\n¿Envió el código al vendedor? portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Algunos bancos pueden verificar el nombre del receptor. Las cuentas Faster Payments creadas en antiguos clientes de Haveno no proporcionan el nombre completo del receptor, así que por favor, utilice el chat del intercambio para obtenerlo (si es necesario). portfolio.pending.step2_buyer.confirmStart.headline=Confirme que ha comenzado el pago. portfolio.pending.step2_buyer.confirmStart.msg=¿Ha iniciado el pago de {0} a su par de intercambio? portfolio.pending.step2_buyer.confirmStart.yes=Sí, lo he iniciado. portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=No ha entregado una prueba de pago válida. portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=No ha introducido la transacción de ID y la clave de transacción.\n\nAl no proveer esta información el par no puede usar la autoconfirmación para liberar los XMR en cuanto los XMR se han recibido.\nAdemás de esto, Haveno requiere que el emisor de la transacción XMR sea capaz de entregar esta información al mediador o árbitro en caso de disputa.\nVea más detalles en la wiki de Haveno: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades] portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=El valor introducido no es un valor hexadecimal de 32 bytes portfolio.pending.step2_buyer.confirmStart.warningButton=Ignorar y continuar de todos modos portfolio.pending.step2_seller.waitPayment.headline=Esperar al pago. portfolio.pending.step2_seller.f2fInfo.headline=Información de contacto del comprador portfolio.pending.step2_seller.waitPayment.msg=La transacción del depósito tiene al menos una confirmación en la cadena de bloques.\nTiene que esperar hasta que el comprador de XMR comience el pago de {0}. portfolio.pending.step2_seller.warn=El comprador de XMR aún no ha realizado el pago de {0}.\nNecesita esperar hasta que el pago comience.\nSi el intercambio aún no se ha completado el {1} el árbitro procederá a investigar. portfolio.pending.step2_seller.openForDispute=El comprador de XMR no ha comenzado su pago!\nEl periodo máximo permitido ha finalizado.\nPuede esperar más y dar más tiempo a la otra parte o contactar con el mediador para abrir una disputa. tradeChat.chatWindowTitle=Ventana de chat para intercambio con ID "{0}" tradeChat.openChat=Abrir ventana de chat tradeChat.rules=Puede comunicarse con su par de intercambio para resolver posibles problemas con este intercambio.\nNo es obligatorio responder en el chat.\nSi un comerciante viola alguna de las reglas de abajo, abra una disputa y repórtelo al mediador o árbitro.\n\nReglas del chat:\n\t● No enviar ningún enlace (riesgo de malware). Puedes enviar el ID de la transacción y el nombre de un explorador de bloques.\n\t● ¡No enviar las palabras semilla, llaves privadas, contraseñas u otra información sensible!\n\t● No alentar a intercambiar fuera de Haveno (sin seguridad).\n\t● No se enfrente a ningún intento de estafa de ingeniería social.\n\t● Si un par no responde y prefiere no comunicarse, respete su decisión.\n\t● Limite el tema de conversación al intercambio. Este chat no es un sustituto del messenger o troll-box.\n\t● Mantenga la conversación amigable y respetuosa. # suppress inspection "UnusedProperty" message.state.UNDEFINED=Indefinido # suppress inspection "UnusedProperty" message.state.SENT=Mensaje enviado # suppress inspection "UnusedProperty" message.state.ARRIVED=El mensaje llegó al usuario de red # suppress inspection "UnusedProperty" message.state.STORED_IN_MAILBOX=Mensaje de pago enviado, pero aún no recibido por el par. # suppress inspection "UnusedProperty" message.state.ACKNOWLEDGED=El usuario de red confirmó la recepción del mensaje # suppress inspection "UnusedProperty" message.state.FAILED=El envío del mensaje falló portfolio.pending.step3_buyer.wait.headline=Espere a la confirmación de pago del vendedor de XMR. portfolio.pending.step3_buyer.wait.info=Esperando a la confirmación del recibo de pago de {0} por parte del vendedor de XMR. portfolio.pending.step3_buyer.wait.msgStateInfo.label=Estado del mensaje de pago iniciado portfolio.pending.step3_buyer.warn.part1a=en la cadena de bloques {0} portfolio.pending.step3_buyer.warn.part1b=en su proveedor de pago (v.g. banco) portfolio.pending.step3_buyer.warn.part2=El vendedor de XMR aún no ha confirmado su pago. Por favor, compruebe {0} si el envío del pago fue correcto. portfolio.pending.step3_buyer.openForDispute=¡El vendedor de XMR aún no ha confirmado su pago! El periodo máximo para el intercambio ha concluido. Puede esperar y dar más tiempo a la otra parte o solicitar asistencia del mediador. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.part=La otra parte del intercambio confirma haber iniciado el pago de {0}.\n\n portfolio.pending.step3_seller.crypto.explorer=en su explorador de cadena de bloques {0} favorito portfolio.pending.step3_seller.crypto.wallet=en su cartera {0} portfolio.pending.step3_seller.crypto={0}Por favor compruebe {1} si la transacción a su dirección de recepción\n{2}\ntiene suficientes confirmaciones en la cadena de bloques.\nLa cantidad a pagar tiene que ser {3}\n\nPuede copiar y pegar su dirección {4} desde la pantalla principal después de cerrar este popup. portfolio.pending.step3_seller.postal={0}Por favor compruebe si ha recibido {1} con \"US Postal Money Order\" enviados por el comprador XMR. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.payByMail={0}Por favor compruebe si ha recibido {1} con \"Efectivo por Correo\" enviados por el comprador XMR. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.bank=Su par de intercambio confirma que ha iniciado el pago de {0}.\n\nPor favor vaya a la página web de su banco y compruebe si ha recibido {1} del comprador de XMR. portfolio.pending.step3_seller.cash=Debido a que el pago se hecho vía depósito en efectivo el comprador de XMR tiene que escribir \"SIN REEMBOLSO\" en el recibo de papel, dividirlo en 2 partes y enviarte una foto por e-mail.\n\nPara impedir el riesgo de reembolso, solo confirme si ha recibido el e-mail y si está seguro de que el recibo es válido.\nSi no está seguro, {0} portfolio.pending.step3_seller.moneyGram=El comprador tiene que enviarle el número de autorización y una foto del recibo por correo electrónico.\n\nEl recibo debe mostrar claramente el monto, asi como su nombre completo, país y demarcación (departamento,estado, etc.). Por favor revise su correo electrónico si recibió el número de autorización.\n\nDespués de cerrar esa ventana emergente (popup), verá el nombre y la dirección del comprador de XMR para retirar el dinero de MoneyGram.\n\n¡Solo confirme el recibo de transacción después de haber obtenido el dinero con éxito! portfolio.pending.step3_seller.westernUnion=El comprador tiene que enviarle el MTCN (número de seguimiento) y una foto de el recibo por email.\nEl recibo debe mostrar claramente su nombre completo, ciudad, país y la cantidad. Por favor compruebe su email si ha recibido el MTCN.\n\nDespués de cerrar ese popup verá el nombre del comprador de XMR y la dirección para recoger el dinero de Western Union.\n\nSolo confirme el recibo después de haber recogido satisfactoriamente el dinero! portfolio.pending.step3_seller.halCash=El comprador tiene que enviarle el código HalCash como un mensaje de texto. Junto a esto recibirá un mensaje desde HalCash con la información requerida para retirar los EUR de un cajero que soporte HalCash.\n\nDespués de retirar el dinero del cajero confirme aquí la recepción del pago! portfolio.pending.step3_seller.amazonGiftCard=El comprador le ha enviado una Tarjeta Amazon eGift por email o mensaje de texto al teléfono móvil. Por favor canjee ahora la Tarjeta Amazon eGift en su cuenta Amazon y una vez aceptado confirme el recibo del pago. portfolio.pending.step3_seller.bankCheck=\n\nPor favor verifique también que el nombre y el emisor especificado en el contrato de intercambio se corresponde con el nombre que aparece en su declaración bancaria:\nNombre del emisor, para el contrato de intercambio: {0}\n\nSi los nombres no son exactamente los mismos, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=no confirme el recibo de pago. En su lugar, abra una disputa pulsando \"alt + o\" o \"option + o\".\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=Confirmar recibo de pago portfolio.pending.step3_seller.amountToReceive=Cantidad a recibir portfolio.pending.step3_seller.yourAddress=Su dirección {0} portfolio.pending.step3_seller.buyersAddress=Dirección {0} del comprador portfolio.pending.step3_seller.yourAccount=Su cuenta de intercambio portfolio.pending.step3_seller.xmrTxHash=ID de la transacción portfolio.pending.step3_seller.xmrTxKey=Clave de transacción portfolio.pending.step3_seller.buyersAccount=Datos de cuenta del comprador portfolio.pending.step3_seller.confirmReceipt=Confirmar recibo de pago portfolio.pending.step3_seller.buyerStartedPayment=El comprador de XMR ha iniciado el pago de {0}.\n{1} portfolio.pending.step3_seller.buyerStartedPayment.crypto=Compruebe las confirmaciones en la cadena de bloques en su monedero de crypto o explorador de bloques y confirme el pago cuando tenga suficientes confirmaciones. portfolio.pending.step3_seller.buyerStartedPayment.traditional=Compruebe su cuenta de intercambio (v.g. cuenta bancaria) y confirme cuando haya recibido el pago. portfolio.pending.step3_seller.warn.part1a=en la cadena de bloques {0} portfolio.pending.step3_seller.warn.part1b=en su proveedor de pago (v.g. banco) portfolio.pending.step3_seller.warn.part2=Todavía no ha confirmado el recibo del pago. Por favor, compruebe {0} si ha recibido el pago. portfolio.pending.step3_seller.openForDispute=No ha confirmado la recepción del pago.\nEl periodo máximo para el intercambio ha concluido.\nPor favor confirme o solicite asistencia del mediador. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=¿Ha recibido el pago de {0} de su par de intercambio?\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.name=Por favor verifique también que el nombre del emisor especificado en el contrato de intercambio concuerda con el nombre que aparece en su declaración bancaria:\nNombre del emisor, para el contrato de intercambio: {0}\n\nSi los nombres no son exactamente los mismos, no confirme el recibo de pago. En su lugar, abra una disputa pulsando \"alt + o\" o \"option + o\".\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Por favor tenga en cuenta, que tan pronto como haya confirmado el recibo, la cantidad de intercambio bloqueada será librerada al comprador de XMR y el depósito de seguridad será devuelto. portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Confirme que ha recibido el pago portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=Sí, he recibido el pago portfolio.pending.step3_seller.onPaymentReceived.signer=IMPORTANTE: Confirmando el recibo de pago, está también verificando la cuenta de la contraparte y firmándola en consecuencia. Como la cuenta de la contraparte no ha sido firmada aún, debería retrasar la confirmación de pago tanto como sea posible para reducir el riesgo de devolución de cargo. portfolio.pending.step5_buyer.groupTitle=Resumen de el intercambio completado portfolio.pending.step5_buyer.tradeFee=Comisión de transacción portfolio.pending.step5_buyer.makersMiningFee=Comisión de minado portfolio.pending.step5_buyer.takersMiningFee=Comisiones de minado totales portfolio.pending.step5_buyer.refunded=Depósito de seguridad devuelto portfolio.pending.step5_buyer.withdrawXMR=Retirar moneros portfolio.pending.step5_buyer.amount=Cantidad a retirar portfolio.pending.step5_buyer.withdrawToAddress=Retirar a la dirección portfolio.pending.step5_buyer.moveToHavenoWallet=Mantener fondos en el monedero de Haveno portfolio.pending.step5_buyer.withdrawExternal=Retirar al monedero externo portfolio.pending.step5_buyer.alreadyWithdrawn=Sus fondos ya han sido retirados.\nPor favor, compruebe el historial de transacciones. portfolio.pending.step5_buyer.confirmWithdrawal=Confirme la petición de retiro portfolio.pending.step5_buyer.amountTooLow=La cantidad a transferir es inferior a la tasa de transacción y el mínimo valor de transacción posible (polvo - dust). portfolio.pending.step5_buyer.withdrawalCompleted.headline=Retiro completado portfolio.pending.step5_buyer.withdrawalCompleted.msg=Sus intercambios completados están almacenados en \"Portfolio/Historial\".\nPuede revisar todas las transacciones de monero en \"Fondos/Transacciones\" portfolio.pending.step5_buyer.bought=Ha comprado portfolio.pending.step5_buyer.paid=Ha pagado portfolio.pending.step5_seller.sold=Ha vendido portfolio.pending.step5_seller.received=Ha recibido tradeFeedbackWindow.title=Felicitaciones por completar su intercambio tradeFeedbackWindow.msg.part1=Nos encantaría saber su opinión acerca de su experiencia. Nos ayudará a mejorar el software y refinar sus características. Si desea enviar sus comentarios, por favor complete esta breve encuesta (sin registro requerido) en: tradeFeedbackWindow.msg.part2=Si tiene alguna pregunta o experimenta algún problema, por favor póngase en contacto con otros usuarios y colaboradores a través del foro Haveno en: tradeFeedbackWindow.msg.part3=¡Gracias por usar Haveno! portfolio.pending.role=Mi rol portfolio.pending.tradeInformation=Información de intercambio portfolio.pending.remainingTime=Tiempo requerido portfolio.pending.remainingTimeDetail={0} (hasta {1}) portfolio.pending.remainingTimeDetail.startsAfter=Comienza después de {0} confirmaciones portfolio.pending.tradePeriodInfo=Después de {0} confirmaciones, comienza el período de la transacción. Según el método de pago utilizado, se aplica un período máximo de transacción diferente. portfolio.pending.tradePeriodWarning=Si el periodo se excede ambos comerciantes pueden abrir una disputa. portfolio.pending.tradeNotCompleted=Intercambio no completado a tiempo(hasta {0}) portfolio.pending.tradeProcess=Proceso de intercambio portfolio.pending.openAgainDispute.msg=Si no está seguro de que el mensaje al mediador o árbitro llegó (Ej. si no ha tenido respuesta después de 1 día), siéntase libre de abrir una disputa de nuevo con Cmd/Ctrl+o. También puede pedir ayuda adicional en el forum de Haveno en [HYPERLINK:https://haveno.community]. portfolio.pending.openAgainDispute.button=Abrir disputa de nuevo portfolio.pending.openSupportTicket.headline=Abrir ticket de soporte portfolio.pending.openSupportTicket.msg=Por favor use esta función solo en caso de emergencia si no se muestra el botón \"Abrir soporte\" o \"Abrir disputa\".\n\nCuando abra un ticket de soporte el intercambio se interrumpirá y será manejado por un mediador o un árbitro. portfolio.pending.timeLockNotOver=Tiene hasta ≈{0} ({1} bloques más) antes de que pueda abrir una disputa de arbitraje. portfolio.pending.error.depositTxNull=La transacción de depósito es inválida. No puede abrir una disputa sin una transacción de depósito válida. Por favor vaya a \"Configuración/Información de red\" y haga una resincronización SPV.\n\nPara obtener ayuda contacte con el equipo de soporte en el canal Haveno de Keybase. portfolio.pending.mediationResult.error.depositTxNull=La transacción de depósito es nula. Puede mover la transacción a operaciones fallidas. portfolio.pending.mediationResult.error.delayedPayoutTxNull=La transacción de pago demorado es nula. Puede mover la transacción a operaciones fallidas. portfolio.pending.error.depositTxNotConfirmed=El depósito de transacción no se ha confirmado. No puede abrir una disputa de arbitraje con una transacción de depósito no confirmada. Por favor espere a que se confirme o vaya a \"Configuración/Información de red\" y haga una resincronización SPV.\n\nPara más ayuda por favor contacte con el equipo de soporte en el canal Haveno de Keybase. portfolio.pending.support.headline.getHelp=¿Necesita ayuda? portfolio.pending.support.text.getHelp=Si tiene algún problema puede intentar contactar al par de intercambio en el chat o preguntar en la la comunidad Haveno en https://haveno.comunnity. Si su problema no se resuelve, puede abrir una disputa con un mediador. portfolio.pending.support.button.getHelp=Abrir chat de intercambio portfolio.pending.support.headline.halfPeriodOver=Comprobar pago portfolio.pending.support.headline.periodOver=El periodo de intercambio se acabó portfolio.pending.support.headline.depositTxMissing=Transacción de depósito faltante portfolio.pending.support.depositTxMissing=Falta una transacción de depósito para este comercio. Abra un ticket de soporte para contactar a un árbitro y recibir asistencia. portfolio.pending.mediationRequested=Mediación solicitada portfolio.pending.refundRequested=Devolución de fondos solicitada portfolio.pending.openSupport=Abrir ticket de soporte portfolio.pending.supportTicketOpened=Ticket de soporte abierto portfolio.pending.communicateWithArbitrator=Por favor, comuníquese en la pantalla de \"Soporte\" con el árbitro. portfolio.pending.communicateWithMediator=Por favor, comuníquese en la pantalla \"Soporte\" con el mediador. portfolio.pending.disputeOpenedByUser=Ya ha abierto una disputa.\n{0} portfolio.pending.disputeOpenedByPeer=Su pareja de intercambio ha abierto una disputa\n{0} portfolio.pending.noReceiverAddressDefined=No se ha definido la dirección del receptor. portfolio.pending.mediationResult.headline=Pago sugerido por la mediación portfolio.pending.mediationResult.info.noneAccepted=Complete el intercambio aceptando la sugerencia del mediador para el pago de la transacción. portfolio.pending.mediationResult.info.selfAccepted=Ha aceptado la sugerencia del mediador. Esperando a que el par también la acepte. portfolio.pending.mediationResult.info.peerAccepted=El par de intercambio ha aceptador la sugerencia del mediador. ¿Usted también la acepta? portfolio.pending.mediationResult.button=Ver resolución propuesta portfolio.pending.mediationResult.popup.headline=Resultado de mediación para el intercambio con ID: {0} portfolio.pending.mediationResult.popup.headline.peerAccepted=El par de intercambio ha aceptado la sugerencia del mediador para el intercmabio {0} portfolio.pending.mediationResult.popup.info=El mediador ha sugerido el siguiente pago:\nUsted recibe: {0}\nEl par de intercambio recibe: {1}\n\nUsted puede aceptar o rechazar esta sugerencia de pago.\n\nAceptándola, usted firma el pago propuesto. Si su par de intercambio también acepta y firma, el pago se completará y el intercambio se cerrará.\n\nSi una o ambas partes rechaza la sugerencia, tendrá que esperar hasta {2} (bloque {3}) para abrir una segunda ronda de disputa con un árbitro que investigará el caso de nuevo y realizará el pago de acuerdo a sus hallazgos.\n\nEl árbitro puede cobrar una tasa pequeña (tasa máxima: el depósito de seguridad del comerciante) como compensación por su trabajo. Que las dos partes estén de acuerdo es el buen camino, ya que requerir arbitraje se reserva para circunstancias excepcionales, como que un comerciante esté seguro de que el mediador hizo una sugerencia de pago injusta (o que la otra parte no responda).\n\nMás detalles acerca del nuevo modelo de arbitraje:\n[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=Ha aceptado el pago sugerido por el mediador, pero parece que su par de intercambio no lo ha aceptado.\n\nUna vez que finaliza el tiempo de bloqueo en el {0} (bloque {1}), puede abrir una segunda ronda de disputa con un árbitro que investigará el caso nuevamente y realizará un pago en función de sus hallazgos.\n\nPuede encontrar más detalles sobre el modelo de arbitraje en:\n[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=Rechazar y solicitar arbitraje portfolio.pending.mediationResult.popup.alreadyAccepted=Ya ha aceptado portfolio.pending.failedTrade.taker.missingTakerFeeTx=Falta la transacción de tasa de tomador\n\nSin esta tx, el intercambio no se puede completar. No se han bloqueado fondos y no se ha pagado ninguna tasa de intercambio. Puede mover esta operación a intercambios fallidos. portfolio.pending.failedTrade.maker.missingTakerFeeTx=Falta la transacción de tasa de tomador de su par.\n\nSin esta tx, el intercambio no se puede completar. No se han bloqueado fondos. Su oferta aún está disponible para otros comerciantes, por lo que no ha perdido la tasa de tomador. Puede mover este intercambio a intercambios fallidos. portfolio.pending.failedTrade.missingDepositTx=Falta una transacción de depósito.\n\nEsta transacción es necesaria para completar la operación. Por favor, asegúrate de que tu monedero esté completamente sincronizado con la cadena de bloques de Monero.\n\nPuedes mover esta operación a la sección de "Operaciones Fallidas" para desactivarla. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Falta la transacción de pago demorado, pero los fondos se han bloqueado en la transacción de depósito.\n\nNO envíe el pago traditional o crypto al vendedor de XMR, porque sin el tx de pago demorado, no se puede abrir el arbitraje. En su lugar, abra un ticket de mediación con Cmd / Ctrl + o. El mediador debe sugerir que ambos pares recuperen el monto total de sus depósitos de seguridad (y el vendedor también recibirá el monto total de la operación). De esta manera, no hay riesgo en la seguridad y solo se pierden las tarifas comerciales.\n\nPuede solicitar un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]. portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Falta la transacción del pago demorado, pero los fondos se han bloqueado en la transacción de depósito.\n\nSi al comprador también le falta la transacción de pago demorado, se le indicará que NO envíe el pago y abra un ticket de mediación. También debe abrir un ticket de mediación con Cmd / Ctrl + o.\n\nSi el comprador aún no ha enviado el pago, el mediador debe sugerir que ambos pares recuperen el monto total de sus depósitos de seguridad (y el vendedor también recibirá el monto total de la operación). De lo contrario, el monto comercial debe ir al comprador.\n\nPuede solicitar un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]. portfolio.pending.failedTrade.errorMsgSet=Hubo un error durante la ejecución del protocolo de intercambio.\n\nError: {0}\n\nPuede ser que este error no sea crítico y que el intercambio se pueda completar normalmente. Si no está seguro, abra un ticket de mediación para obtener consejos de los mediadores de Haveno.\n\nSi el error fue crítico y la operación no se puede completar, es posible que haya perdido su tarifa de operación. Solicite un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:ttps://github.com/bisq-network/support/issues]. portfolio.pending.failedTrade.missingContract=El contrato del intercambio no está establecido.\n\nLa operación no se puede completar y es posible que haya perdido su tarifa de operación. Si es así, puede solicitar un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]. portfolio.pending.failedTrade.info.popup=El protocolo de intercambio encontró algunos problemas.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=El protocolo de intercambio encontró un problema grave.\n\n{0}\n\n¿Quiere mover la operación a intercambios fallidos?\n\nNo puede abrir mediación o arbitraje desde la vista de operaciones fallidas, pero puede mover un intercambio fallido a la pantalla de intercambios abiertos en cualquier momento. portfolio.pending.failedTrade.txChainValid.moveToFailed=El protocolo de intercambio encontró algunos problemas.\n\n{0}\n\nLas transacciones del intercambio se han publicado y los fondos están bloqueados. Mueva la operación a operaciones fallidas solo si está realmente seguro. Podría impedir opciones para resolver el problema.\n\n¿Quiere mover la operación a operaciones fallidas?\n\nNo puede abrir mediación o el arbitraje desde la vista de intercambios fallidos, pero puede mover un intercambio fallido a la pantalla de intercambios abiertos en cualquier momento. portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Mover intercambio a intercambios fallidos portfolio.pending.failedTrade.warningIcon.tooltip=Clique para mostrar los detalles sobre los problemas en este intercambio portfolio.failed.revertToPending.popup=¿Quiere mover este intercambio a intercambios abiertos? portfolio.failed.revertToPending=Mueva intercambio a intercambios abiertos portfolio.closed.completed=Completado portfolio.closed.ticketClosed=Arbitrada portfolio.closed.mediationTicketClosed=Mediada portfolio.closed.canceled=Cancelado portfolio.failed.Failed=Fallado portfolio.failed.unfail=Antes de continuar, ¡asegúrese de tener un respaldo de su directorio de datos!\n¿Desea mover este intercambio de nuevo a intercambios abiertos?\nEsta es una forma de desbloquear los fondos retenidos en los intercambios fallidos. portfolio.failed.cantUnfail=Este intercambio no puede ser movido de nuevo a intercambios abiertos en este momento.\nIntente de nuevo después de completar el/los intercambios/s {0} portfolio.failed.depositTxNull=El intercambio no se puede revertir a intercambios abiertos. La transacción de depósito es nula. portfolio.failed.delayedPayoutTxNull=El intercambio no se puede revertir a uno abierto. La transacción del pago demorado es nula. #################################################################### # Funds #################################################################### funds.tab.deposit=Recibir fondos funds.tab.withdrawal=Enviar fondos funds.tab.reserved=Fondos reservados funds.tab.locked=Fondos bloqueados funds.tab.transactions=Transacciones funds.deposit.unused=Sin usar funds.deposit.usedInTx=Usadas en {0} transacciones funds.deposit.fundHavenoWallet=Fondear monedero Haveno funds.deposit.noAddresses=Aún no se ha generado la dirección de depósito funds.deposit.fundWallet=Dotar de fondos su monedero funds.deposit.withdrawFromWallet=Enviar fondos desde monedero funds.deposit.amount=Cantidad en XMR (opcional) funds.deposit.generateAddress=Generar una nueva dirección funds.deposit.generateAddressSegwit=Formato de segwit nativo (Bech32) funds.deposit.selectUnused=Por favor seleccione una dirección no utilizada de la tabla de arriba en vez de generar una nueva. funds.withdrawal.arbitrationFee=Tasa de arbitraje funds.withdrawal.inputs=Selección de entradas funds.withdrawal.useAllInputs=Usar todos los entradas disponibles funds.withdrawal.useCustomInputs=Usar entradas personalizados funds.withdrawal.receiverAmount=Cantidad del receptor funds.withdrawal.sendMax=Enviar máximo disponible funds.withdrawal.senderAmount=Cantidad del emisor funds.withdrawal.feeExcluded=La cantidad no incluye comisión de minado funds.withdrawal.feeIncluded=La cantidad incluye comisión de minado funds.withdrawal.fromLabel=Retirar desde la dirección funds.withdrawal.toLabel=Retirar a la dirección funds.withdrawal.memoLabel=Nota de retiro funds.withdrawal.memo=Rellenar opcionalmente la nota funds.withdrawal.withdrawButton=Retiro seleccionado funds.withdrawal.noFundsAvailable=No hay fondos disponibles para el retiro funds.withdrawal.confirmWithdrawalRequest=Confirme la petición de retiro funds.withdrawal.withdrawMultipleAddresses=Retirar desde múltiples direcciones ({0}) funds.withdrawal.withdrawMultipleAddresses.tooltip=Retirar desde múltiples direcciones:\n{0} funds.withdrawal.notEnoughFunds=No tiene suficientes fondos en su cartera. funds.withdrawal.selectAddress=Seleccione una dirección de fuente de la tabla funds.withdrawal.setAmount=Introduzca la cantidad a retirar funds.withdrawal.fillDestAddress=Introduzca su dirección de destino funds.withdrawal.warn.noSourceAddressSelected=Necesita seleccionar una fuente de direcciones en la tabla superior. funds.withdrawal.warn.amountExceeds=No tiene suficientes fondos disponibles en la dirección seleccionada.\nConsidere seleccionar múltiples direcciones en la tabla superior o cambien el selector de comisión para incluir una comisión de minería. funds.reserved.noFunds=No hay fondos reservados en las ofertas abiertas funds.reserved.reserved=Reservados en el monedero local para la oferta con ID: {0} funds.locked.noFunds=No hay fondos bloqueados en intercambios funds.locked.locked=Bloqueado en Multifirma para el intercambio con ID: {0} funds.tx.direction.sentTo=Enviado a: funds.tx.direction.receivedWith=Recibido con: funds.tx.direction.genesisTx=Desde la transacción Génesis: funds.tx.createOfferFee=Creador y comisión de transacción: {0} funds.tx.takeOfferFee=Tomador y comisión de transacción: {0} funds.tx.multiSigDeposit=Depósito Multifirma: {0} funds.tx.multiSigPayout=Pago Multifirma: {0} funds.tx.disputePayout=Pago disputa:{0} funds.tx.disputeLost=Caso de pérdida de disputa: {0} funds.tx.collateralForRefund=Colateral para devolución de fondos: {0} funds.tx.timeLockedPayoutTx=Pago de transacción bloqueada en tiempo: {0} funds.tx.refund=Devolución de fondos de arbitraje: {0} funds.tx.unknown=Razón desconocida: {0} funds.tx.noFundsFromDispute=Sin devolución de disputa funds.tx.receivedFunds=Fondos recibidos funds.tx.withdrawnFromWallet=Retirar desde el monedero funds.tx.memo=Nota funds.tx.noTxAvailable=Sin transacciones disponibles funds.tx.revert=Revertir funds.tx.txSent=Transacción enviada exitosamente a una nueva dirección en la billetera Haveno local. funds.tx.direction.self=Enviado a usted mismo funds.tx.dustAttackTx=Dust recibido funds.tx.dustAttackTx.popup=Esta transacción está enviando una cantidad de XMR muy pequeña a su monedero y puede ser un intento de compañías de análisis de cadenas para espiar su monedero.\n\nSi usa este output para gastar en una transacción, conocerán que probablemente usted sea el propietario de sus otras direcciones (fusión de monedas).\n\nPara proteger su privacidad el monedero Haveno ignora estos outputs para propósitos de gasto y en el balance mostrado. Puede establecer el umbral en el que un output es considerado dust en ajustes. #################################################################### # Support #################################################################### support.tab.mediation.support=Mediación support.tab.arbitration.support=Arbitraje support.tab.legacyArbitration.support=Legado de arbitraje support.tab.ArbitratorsSupportTickets=Tickets de {0} support.sigCheck.button=Comprobar firma support.sigCheck.popup.info=Pegue el mensaje resumido del proceso de arbitraje. Con esta herramienta, cualquier usuario puede verificar si la firma del árbitro coincide con el mensaje resumido. support.sigCheck.popup.header=Verificar firma del resultado de la disputa support.sigCheck.popup.msg.label=Mensaje de resumen support.sigCheck.popup.msg.prompt=Copie y pegue el mensaje de resumen de la disputa support.sigCheck.popup.result=Resultado de la validación support.sigCheck.popup.success=La firma es válida support.sigCheck.popup.failed=La verificación de la firma ha fallado support.sigCheck.popup.invalidFormat=El mensaje no tiene el formato esperado. Copie y pegue el resumen del mensaje desde la disputa. support.reOpenByTrader.prompt=¿Está seguro de que quiere reabrir la disputa? support.reOpenButton.label=Volver a abrir support.sendNotificationButton.label=Notificación privada support.reportButton.label=Reportar support.fullReportButton.label=Todas las disputas support.noTickets=No hay tickets abiertos support.sendingMessage=Enviando mensaje... support.receiverNotOnline=El receptor no está conectado. El mensaje se ha guardado en su bandeja de entrada. support.sendMessageError=El envío del mensaje no tuvo éxito. Error: {0} support.receiverNotKnown=Receptor desconocido support.wrongVersion=La oferta en esta disputa ha sido creada con una versión antigua de Haveno.\nNo puede cerrar esta disputa con su versión de la aplicación.\n\nPor favor, utilice una versión anterior con la versión de protocolo {0} support.openFile=Abrir archivo a adjuntar (tamaño máximo del archivo: {0} kb) support.attachmentTooLarge=El tamaño total de sus adjuntos es {0} kb y excede el máximo permitido por mensaje de {1} kB. support.maxSize=El tamaño máximo permitido del archivo es {0} kB. support.attachment=Adjuntado support.tooManyAttachments=No puede enviar más de 3 adjuntos en un mensaje. support.save=Guardar archivo al disco support.messages=Mensajes support.input.prompt=Introduzca mensaje... support.send=Enviar support.addAttachments=Añadir adjuntos support.closeTicket=Cerrar ticket support.attachments=Adjuntos: support.savedInMailbox=Mensaje guardado en la bandeja de entrada del receptor support.arrived=El mensaje ha llegado al receptor support.acknowledged=El arribo del mensaje fue confirmado por el receptor support.error=El receptor no pudo procesar el mensaje. Error: {0} support.buyerAddress=Dirección del comprador de XMR support.sellerAddress=Dirección del vendedor de XMR support.role=Rol support.agent=Agente de soporte support.state=Estado support.chat=Chat support.preparing=Preparando support.requested=Solicitado support.closed=Cerrado support.open=Abierto support.process=Proceso support.buyerMaker=comprador/creador XMR support.sellerMaker=vendedor/creador XMR support.buyerTaker=comprador/Tomador XMR support.sellerTaker=vendedor/Tomador XMR support.backgroundInfo=Haveno no es una compañía, por lo que maneja las disputas de manera diferente.\n\nLos comerciantes pueden comunicarse dentro de la aplicación a través de un chat seguro en la pantalla de operaciones abiertas para intentar resolver las disputas por sí mismos. Si eso no es suficiente, un mediador evaluará la situación y decidirá un pago de los fondos de la operación. support.initialInfo=Por favor, introduzca una descripción de su problema en el campo de texto de abajo. Añada tanta información como sea posible para agilizar la resolución de la disputa.\n\nEsta es una lista de la información que usted debe proveer:\n\t● Si es el comprador de XMR: ¿Hizo la transferencia Traditional o Cryptocurrency? Si es así, ¿Pulsó el botón 'pago iniciado' en la aplicación?\n\t● Si es el vendedor de XMR: ¿Recibió el pago Traditional o Cryptocurrency? Si es así, ¿Pulsó el botón 'pago recibido' en la aplicación?\n\t● ¿Qué versión de Haveno está usando?\n\t● ¿Qué sistema operativo está usando?\n\t● Si tiene problemas con transacciones fallidas, por favor considere cambiar a un nuevo directorio de datos.\n\tA veces el directorio de datos se corrompe y causa errores extraños.\n\tVer: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nPor favor, familiarícese con las reglas básicas del proceso de disputa:\n\t● Tiene que responder a los requerimientos de {0} en 2 días.\n\t● Los mediadores responden en 2 días. Los árbitros responden en 5 días laborables.\n\t● El periodo máximo para una disputa es de 14 días.\n\t● Tiene que cooperar con {1} y proveer la información necesaria que soliciten.\n\t● Aceptó la reglas esbozadas en el documento de disputa en el acuerdo de usuario cuando inició por primera ver la aplicación.\n\nPuede leer más sobre el proceso de disputa en: {2} support.systemMsg=Mensaje de sistema: {0} support.youOpenedTicket=Ha abierto una solicitud de soporte.\n\n{0}\n\nVersión Haveno: {1} support.youOpenedDispute=Ha abierto una solicitud de disputa.\n\n{0}\n\nVersión Haveno: {1} support.youOpenedDisputeForMediation=Ha solicitado mediación\n\n{0}\n\nVersión Haveno: {1} support.peerOpenedTicket=Su par de intercambio ha solicitado soporte debido a problemas técnicos\n\n{0}\n\nVersión Haveno: {1} support.peerOpenedDispute=Su pareja de intercambio ha solicitado una disputa.\n\n{0}\n\nVersión Haveno: {1} support.peerOpenedDisputeForMediation=Su par de intercambio ha solicitado mediación.\n\n{0}\n\nVersión Haveno: {1} support.mediatorsDisputeSummary=Mensaje de sistema: Resumen de la disputa del mediador: {0} support.mediatorsAddress=Dirección del nodo del mediador: {0} support.warning.disputesWithInvalidDonationAddress=La transacción de pago demorado utilizó una dirección de receptor inválida. No coincide con ninguno de los valores de parámetro de la DAO para las direcciones de donación válidas.\n\nEsto podría ser un intento de estafa. Informe a los desarrolladores sobre ese incidente y no cierre ese caso antes de que se resuelva la situación.\n\nDirección utilizada en la disputa: {0}\n\nTodas las direcciones de donación en los parámetros de la DAO: {1}\n\nIdentificación comercial: {2} {3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\n¿Aún quiere cerrar la disputa? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nUsted no debería realizar el pago. support.warning.traderCloseOwnDisputeWarning=Los comerciantes puden cerrar por sí mismos sus tickets de soporte cuando el intercambio se haya pagado. support.info.disputeReOpened=El ticket de disputa se ha reabierto. #################################################################### # Settings #################################################################### settings.tab.preferences=Preferencias settings.tab.network=Información de red settings.tab.about=Acerca de setting.preferences.general=Preferencias generales setting.preferences.explorer=Explorador Monero setting.preferences.deviation=Desviación máxima del precio de mercado setting.preferences.setting.preferences.avoidStandbyMode=Evitar modo en espera setting.preferences.useSoundForNotifications=Reproducir sonidos para notificaciones setting.preferences.autoConfirmXMR=Autoconfirmación XMR setting.preferences.autoConfirmEnabled=Habilitado setting.preferences.autoConfirmRequiredConfirmations=Confirmaciones requeridas setting.preferences.autoConfirmMaxTradeSize=Cantidad máxima de intecambio (XMR) setting.preferences.autoConfirmServiceAddresses=Explorador de URLs Monero (usa Tor, excepto para localhost, direcciones LAN IP, y hostnames *.local) setting.preferences.deviationToLarge=No se permiten valores superiores a {0}% setting.preferences.txFee=Tasa de transacción de retiro (satoshis/vbyte) setting.preferences.useCustomValue=Usar valor personalizado setting.preferences.txFeeMin=La tasa de transacción debe ser al menos de {0} sat/vbyte setting.preferences.txFeeTooLarge=El valor introducido está muy por encima de lo razonable (>5000 satoshis/vbyte). La tasa de transacción normalmente está en el rango de 50-400 satoshis/vbyte. setting.preferences.ignorePeers=Pares ignorados [dirección onion:puerto] setting.preferences.ignoreDustThreshold=Valor mínimo de output que no sea dust setting.preferences.currenciesInList=Monedas en lista para precio de mercado setting.preferences.prefCurrency=Moneda preferida setting.preferences.displayTraditional=Mostrar monedas nacionales setting.preferences.noTraditional=No hay monedas nacionales seleccionadas setting.preferences.cannotRemovePrefCurrency=No puede eliminar su moneda preferida para mostrar seleccionada setting.preferences.displayCryptos=Mostrar cryptos setting.preferences.noCryptos=No hay cryptos seleccionadas setting.preferences.addTraditional=Añadir moneda nacional setting.preferences.addCrypto=Añadir crypto setting.preferences.displayOptions=Mostrar opciones setting.preferences.showOwnOffers=Mostrar mis propias ofertas en el libro de ofertas setting.preferences.useAnimations=Usar animaciones setting.preferences.useDarkMode=Usar modo oscuro setting.preferences.useLightMode=Usar modo claro setting.preferences.sortWithNumOffers=Ordenar listas de mercado por número de ofertas/intercambios setting.preferences.onlyShowPaymentMethodsFromAccount=Ocultar métodos de pago no soportados setting.preferences.denyApiTaker=Denegar tomadores usando la misma API setting.preferences.notifyOnPreRelease=Recibir notificaciones de pre-lanzamiento setting.preferences.resetAllFlags=Restablecer todas las casillas \"No mostrar de nuevo\" settings.preferences.languageChange=Para aplicar un cambio de idioma en todas las pantallas, se precisa reiniciar. settings.preferences.supportLanguageWarning=En caso de disputa, tenga en cuenta que el arbitraje se maneja en {0}. settings.preferences.editCustomExplorer.headline=Configuraciones de explorador settings.preferences.editCustomExplorer.description=Elija un explorador definido por el sistema de la lista de la izquierda, y/o personalícelo para ajustarse a sus preferencias. settings.preferences.editCustomExplorer.available=Exploradores disponibles settings.preferences.editCustomExplorer.chosen=Configuración de exploradores elegidos settings.preferences.editCustomExplorer.name=Nombre settings.preferences.editCustomExplorer.txUrl=URL de transacción settings.preferences.editCustomExplorer.addressUrl=URL de la dirección setting.info.headline=Nueva función de privacidad de datos settings.preferences.sensitiveDataRemoval.msg=Para proteger la privacidad de usted y otros traders, Haveno pretende eliminar datos sensibles de las operaciones antiguas. Esto es especialmente importante para las operaciones con fiat que pueden incluir detalles de cuentas bancarias.\n\nSe recomienda configurarlo lo más bajo posible, por ejemplo, 60 días. Esto significa que las operaciones de hace más de 60 días tendrán los datos sensibles eliminados, siempre que estén completadas. Las operaciones completadas se encuentran en la pestaña Portafolio / Historial. settings.net.xmrHeader=Red Monero settings.net.p2pHeader=Red Haveno settings.net.onionAddressLabel=Mi dirección onion settings.net.xmrNodesLabel=Utilizar nodos Monero personalizados settings.net.moneroPeersLabel=Pares conectados settings.net.connection=Conexión settings.net.connected=Conectado settings.net.useTorForXmrJLabel=Usar Tor para la red Monero settings.net.moneroNodesLabel=Nodos Monero para conectarse settings.net.useProvidedNodesRadio=Utilizar nodos Monero Core proporcionados settings.net.usePublicNodesRadio=Utilizar red pública Monero settings.net.useCustomNodesRadio=Utilizar nodos Monero Core personalizados settings.net.warn.usePublicNodes=Si utiliza nodos públicos de Monero, está sujeto a cualquier riesgo asociado con el uso de nodos remotos no confiables.\n\nPor favor, lea más detalles en [HYPERLINK:https://www.getmonero.org/resources/moneropedia/remote-node.html].\n\n¿Está seguro de que desea utilizar nodos públicos? settings.net.warn.usePublicNodes.useProvided=No, utilizar nodos proporcionados settings.net.warn.usePublicNodes.usePublic=Sí, utilizar la red pública settings.net.warn.useCustomNodes.B2XWarning=¡Por favor, asegúrese de que su nodo Monero es un nodo de confianza Monero Core!\n\nConectar a nodos que no siguen las reglas de consenso puede causar perjuicios a su cartera y causar problemas en el proceso de intercambio.\n\nLos usuarios que se conecten a los nodos que violan las reglas de consenso son responsables de cualquier daño que estos creen. Las disputas causadas por ello se decidirán en favor del otro participante. No se dará soporte técnico a usuarios que ignoren esta advertencia y los mecanismos de protección! settings.net.warn.invalidXmrConfig=La conexión a la red Monero falló debido a que su configuración es inválida.\n\nSu configuración se ha reestablecido para usar los nodos Monero proporcionados. Necesitará reiniciar la aplicación. settings.net.localhostXmrNodeInfo=Información complementaria: Haveno busca un nodo local Monero al inicio. Si lo encuentra, Haveno se comunicará a la red Monero exclusivamente a través de él. settings.net.p2PPeersLabel=Pares conectados settings.net.onionAddressColumn=Dirección onion settings.net.creationDateColumn=Establecido settings.net.connectionTypeColumn=Dentro/Fuera settings.net.sentDataLabel=Estadísticas de datos enviados settings.net.receivedDataLabel=Estadísticas de datos recibidos settings.net.chainHeightLabel=Altura del último bloque XMR settings.net.roundTripTimeColumn=Tiempo de ida y vuelta settings.net.sentBytesColumn=Enviado settings.net.receivedBytesColumn=Recibido settings.net.peerTypeColumn=Tipo de par settings.net.openTorSettingsButton=Abrir configuración Tor settings.net.versionColumn=Versión settings.net.subVersionColumn=Sub versión settings.net.heightColumn=Altura settings.net.needRestart=Necesita reiniciar la aplicación para aplicar ese cambio.\n¿Quiere hacerlo ahora? settings.net.notKnownYet=Aún no conocido... settings.net.sentData=Datos enviados: {0}, mensajes {1}, mensajes {2} mensajes por segundo settings.net.receivedData=Datos recibidos: {0}, mensajes {1}, mensajes por segundo {2} settings.net.chainHeight=Altura de la cadena de pares Monero: {0} settings.net.ips=[Dirección IP:puerto | host:puerto | dirección onion:puerto] (separado por coma). El puerto puede ser omitido si se utiliza el predeterminado (8333). settings.net.seedNode=Nodo semilla settings.net.directPeer=Par (directo) settings.net.initialDataExchange={0} [Bootstrapping] settings.net.peer=Par settings.net.inbound=entrante settings.net.outbound=saliente setting.about.aboutHaveno=Acerca de Haveno setting.about.about=Haveno es un software de código abierto que facilita el intercambio de monero por monedas nacionales (y otras criptomonedas) a través de una red descentralizada peer-to-peer de modo que se proteja fuertemente la privacidad del usuario. Aprenda más acerca de Haveno en la página web del proyecto. setting.about.web=Página web de Haveno setting.about.code=código fuente setting.about.agpl=Licencia AGPL setting.about.support=Apoye a Haveno setting.about.def=Haveno no es una compañía - es un proyecto abierto a la comunidad. Si quiere participar o ayudar a Haveno por favor siga los enlaces de abajo. setting.about.contribute=Contribuir setting.about.providers=Proveedores de datos setting.about.apisWithFee=Haveno usa Índices de Precios Haveno para los precios de mercado de traditional y crypto, y los Nodos de Mempool de Haveno para la estimación de tasas de minado. setting.about.apis=Haveno usa los Índices de Precios Haveno para los precios de mercado de traditional y crypto. setting.about.pricesProvided=Precios de mercado proporcionados por: setting.about.feeEstimation.label=Estimación de comisión de minería proporcionada por: setting.about.versionDetails=Detalles de la versión setting.about.version=Versión de la aplicación: setting.about.subsystems.label=Versión de subsistemas: setting.about.subsystems.val=Versión de red: {0}; Versión de mensajes P2P: {1}; Versión de Base de Datos local: {2}; Versión de protocolo de intercambio {3} setting.about.shortcuts=Atajos setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' o 'alt + {0}' o 'cmd + {0}' setting.about.shortcuts.menuNav=Navegar menú principal setting.about.shortcuts.menuNav.value=Para navegar por el menú principal pulse: 'Ctrl' or 'alt' or 'cmd' con una tecla numérica entre el '1-9' setting.about.shortcuts.close=Cerrar Haveno setting.about.shortcuts.close.value='Ctrl + {0}' o 'cmd + {0}' o 'Ctrl + {1}' o 'cmd + {1}' setting.about.shortcuts.closePopup=Cerrar la ventana emergente o ventana de diálogo setting.about.shortcuts.closePopup.value=Tecla 'ESCAPE' setting.about.shortcuts.chatSendMsg=Enviar mensaje en chat de intercambio setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' o 'alt + ENTER' o 'cmd + ENTER' setting.about.shortcuts.openDispute=Abrir disputa setting.about.shortcuts.openDispute.value=Seleccionar intercambios pendientes y pulsar: {0} setting.about.shortcuts.walletDetails=Abrir ventana de detalles de monedero setting.about.shortcuts.openEmergencyXmrWalletTool=Abrir herramienta de monedero de emergencia para el monedero XMR setting.about.shortcuts.showTorLogs=Cambiar nivel de registro para mensajes Tor entre DEBUG y WARN setting.about.shortcuts.manualPayoutTxWindow=Abrir ventana para pago manual desde la transacción de depósito multifirma 2de2 setting.about.shortcuts.removeStuckTrade=Abrir ventana emergente para moverel intercambio a la pestaña de intercambios abiertos de nuevo setting.about.shortcuts.removeStuckTrade.value=Seleccionar intercambio fallido y presione: {0} setting.about.shortcuts.registerArbitrator=Registrar árbitro (solo mediador/árbitro) setting.about.shortcuts.registerArbitrator.value=Navegar a cuenta y pulsar: {0} setting.about.shortcuts.registerMediator=Registrar mediador (solo mediador/árbitro) setting.about.shortcuts.registerMediator.value=Navegar a cuenta y pulsar: {0} setting.about.shortcuts.openSignPaymentAccountsWindow=Abrir ventana para firmado de edad de cuenta (solo árbitros legacy) setting.about.shortcuts.openSignPaymentAccountsWindow.value=Navegar a vista de árbitro legacy y pulsar: {0} setting.about.shortcuts.sendAlertMsg=Enviar alerta o actualizar mensaje (actividad privilegiada) setting.about.shortcuts.sendFilter=Establecer filtro (actividad privilegiada) setting.about.shortcuts.sendPrivateNotification=Enviar notificación privada a los pares (actividad privilegiada) setting.about.shortcuts.sendPrivateNotification.value=Abrir información de par en el avatar o disputa y pulsar: {0} setting.info.headline=Nueva función de autoconfirmación XMR setting.info.msg=Al vender XMR por XMR puede usar la función de autoconfirmación para verificar que la cantidad correcta de XMR se envió al monedero con lo que Haveno pueda marcar el intercambio como completo, haciendo los intercambios más rápidos para todos.\n\nLa autoconfirmación comprueba que la transacción de XMR en al menos 2 nodos exploradores XMR usando la clave de transacción entregada por el emisor XMR. Por defecto, Haveno usa nodos exploradores ejecutados por contribuyentes Haveno, pero recomendamos usar sus propios nodos exploradores para un máximo de privacidad y seguridad.\n\nTambién puede configurar la cantidad máxima de XMR por intercambio para la autoconfirmación, así como el número de confirmaciones en Configuración.\n\nVea más detalles (incluído cómo configurar su propio nodo explorador) en la wiki Haveno: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades] #################################################################### # Account #################################################################### account.tab.mediatorRegistration=Registro de mediador account.tab.refundAgentRegistration=Registro de agente de devolución de fondos account.tab.signing=Firmado account.info.headline=Bienvenido a su cuenta Haveno account.info.msg=Aquí puede añadir cuentas de intercambio para monedas nacionales y cryptos y crear una copia de su cartera y datos de cuenta.\n\nUna nueva cartera Monero fue creada la primera vez que inició Haveno.\n\nRecomendamos encarecidamente que escriba sus palabras de la semilla de la cartera Monero (mire pestaña arriba) y considere añadir una contraseña antes de enviar fondos. Los ingresos y retiros de Monero se administran en la sección \"Fondos\".\n\nNota de privacidad y seguridad: Debido a que Haveno es un exchange descentralizado, todos sus datos se guardan en su ordenador. No hay servidores, así que no tenemos acceso a su información personal, sus saldos, o incluso su dirección IP. Datos como número de cuenta bancaria, direcciones crypto y Monero, etc son solo compartidos con su par de intercambio para completar intercambios iniciados (en caso de una disputa el mediador o árbitro verá los mismos datos que el par de intercambio). account.menu.paymentAccount=Cuentas de moneda nacional account.menu.altCoinsAccountView=Cuentas de crypto account.menu.password=Contraseña de monedero account.menu.seedWords=Semilla del monedero account.menu.walletInfo=Información de monedero account.menu.backup=Copia de seguridad account.menu.notifications=Notificaciones account.menu.walletInfo.balance.headLine=Balances de monedero account.menu.walletInfo.balance.info=Esto muestrta el balance interno del monedero, incluyendo transacciones no confirmadas.\nPara XMR, el balance interno de monedero mostrado abajo debe cuadrar con la suma de balances 'Disponible' y 'Reservado' mostrado arriba a la derecha de esta ventana. account.menu.walletInfo.xpub.headLine=Claves centinela (xpub keys) account.menu.walletInfo.walletSelector={0} {1} monedero account.menu.walletInfo.path.headLine=ruta HD keychain account.menu.walletInfo.path.info=Si importa las palabras semilla en otro monedero (como Electru), tendrá que definir la ruta. Esto debería hacerse solo en casos de emergencia, cuando pierda acceso a el monedero Haveno y el directorio de datos.\nTenga en cuenta que gastar fondos desde un monedero no-Haveno puede estropear la estructura de datos interna de Haveno asociado a los datos de monedero, lo que puede llevar a intercambios fallidos.\n\nNUNCA envíe BSQ desde un monedero no-Haveno, ya que probablemente llevará a una transacción inválida de BSQ y le hará perder sus BSQ. account.menu.walletInfo.openDetails=Mostrar detalles en bruto y claves privadas ## TODO should we rename the following to a gereric name? account.arbitratorRegistration.pubKey=Llave pública account.arbitratorRegistration.register=Registrar account.arbitratorRegistration.registration={0} registro account.arbitratorRegistration.revoke=Revocar account.arbitratorRegistration.info.msg=Por favor, tenga en cuenta que necesita estar disponible 15 días después la revocación pues puede haber intercambios que le usen como {0}. El periodo máximo de intercambio permitido es de 8 días y el proceso de disputa puede llevar hasta 7 días.l account.arbitratorRegistration.warn.min1Language=Necesita especificar al menos 1 idioma.\nHemos añadido el idioma por defecto para usted. account.arbitratorRegistration.removedSuccess=Ha eliminado su registro de la red Haveno con éxito. account.arbitratorRegistration.removedFailed=No se pudo eliminar el registro.{0} account.arbitratorRegistration.registerSuccess=Se ha registrado con éxito en la red Haveno. account.arbitratorRegistration.registerFailed=No se pudo completar el registro.{0} account.crypto.yourCryptoAccounts=Sus cuentas de crypto account.crypto.popup.wallet.msg=Por favor, asegúrese que sigue los requisitos para el uso de carteras {0} como se describe en la página web {1}.\nUsando carteras desde exchanges centralizados donde (a) usted no controla sus claves o (b) que no usan monederos compatibles con el software es un riesgo: ¡puede llevar a la perdida de los fondos intercambiados!\nEl mediador o árbitro no es un especialista en {2} y no puede ayudar en tales casos. account.crypto.popup.wallet.confirm=Entiendo y confirmo que sé qué monedero tengo que utilizar. # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=Para intercambiar UPX en haveno es preciso que usted entienda y cumpla los siguientes requerimientos:\n\nPara enviar upx es preciso que use el monedero oficial de uPlexa GUI o el de uPlexa CLI con la opción "store-tx-info" habilitada (predeterminada en las nuevas versiones). Por favor, asegúrese de poder acceder a las llaves tx, ya que podrían requerirse en caso de disputa.\nuplexa-wallet-cli (utilice el comando get_tx_key)\nuplexa-wallet-gui (vaya a la pestaña historial y haga clic en el botón (P) para prueba de pago).\nEn los exploradores de bloques normales la transferencia no es verificable.\nUsted necesita proporcionar al árbitro los siguientes datos en caso de disputa:\n- La llave privada tx\n- El hash de la transacción\n- La dirección publica de recepción\nSi no proporciona los datos arriba señalados, o si utilizó una cartera incompatible, perderá la disputa.\nLa parte que envía UPX es responsable de proporcionar las evidencias de la transferencia de UPX al árbitro en caso de disputa.\nNo se requiere ID de pago, solo la dirección pública normal.\nSi no está seguro sobre el proceso, visite el canal de conflictos de uPlexa https://discord.gg/vhdNSrV) o el Chat de Telegram de uPlexa (https://t.me/uplexaOfficial) para encontrar más información. # suppress inspection "UnusedProperty" account.crypto.popup.arq.msg=El ARQ de intercambio de Haveno requiere que entienda y cumpla los siguientes requerimientos:\n\nPara el ARQ de envío, necesita utilizar la cartera oficial ArQmA GUI o una cartera ArQmA CLI con la opción store-tx-info habilitada (predeterminada en nuevas versiones). Por favor asegúrese de poder acceder a la llave tx ya que podría ser requerido en caso de disputa.\narqma-wallet-cli (utilice el comando get_tx_key)\narqma-wallet-gui (vaya a la pestaña historial y pulse el botón (P) para prueba de pago)\n\nEn los exploradores de bloques normales la transferencia no es verificable.\n\nNecesita proveer al mediador o al árbitro los siguientes datos en caso de disputa:\n- El tx de la llave privada\n- El hash de transacción\n- La dirección publica de recepción\n\nNo proveer los datos arriba señalados, o si utilizó una cartera incompatible, resultará en perder la disputa. La parte ARQ que envía es responsable de proveer verificación de la transferencia ARQ al mediador o al árbitro en caso de disputa.\n\nNo se requiere ID de pago, solo la dirección pública normal.\nSi no está seguro sobre el proceso visite el canal ArQmA en discord (https://discord.gg/s9BQpJT) o el foro de ArQmA (https://labs.arqma.com) para encontrar más información. # suppress inspection "UnusedProperty" account.crypto.popup.xmr.msg=Intercambiar XMR en Haveno requiere entender y cumplir los siguientes requisitos\n\nPruebas de pago:\n\n\nSi está vendiendo XMR, debe ser capaz de entregar la siguiente información en caso de disputa:\n\n- La clave de transacción (Tx key, Tx Secret Key o Tx Private Key)\n- La ID de transacción (Tx ID o Tx Hash)\n- La dirección de destino (recipient's address)\n\nVea la wiki para detalles sobre dónde encontrar esta información en los principales monederos XMR:\nhttps://haveno.exchange/wiki/Trading_Monero#Proving_payments\n\n\nNo entregar estos datos de transacción resultará en pérdida de la disputa.\n\nTenga en cuenta que Haveno ahora ofrece confirmación automática de transacciones XMR para realizar intercambios más rápido, pero necesita habilitarlo en Configuración. \n\nVea la wiki para más información sobre la función de autoconfirmación.\n[HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades] # suppress inspection "UnusedProperty" account.crypto.popup.msr.msg=Para intercambiar MSR en Haveno es necesario que usted entienda y cumpla los siguientes requisitos:\n\nPara enviar MSR es necesario que use el monedero oficial de Masari monedero GUI o Masari monedero CLI con la opción "store-tx-info" habilitada (predeterminada en las nuevas versiones) o el monedero oficial Masari Web (https://wallet.getmasari.org). Por favor, asegúrese de poder acceder a las llaves tx, ya que podrían requerirse en caso de alguna disputa. \nmasari-wallet-cli (utilice el comando get_tx_key)\nmasari-wallet-gui (vaya a la pestaña historial y haga clic en el botón (P) para prueba de pago). \n\nMonedero Masari Web (vaya a Cuenta -> historial transacción y vea los detalles de su transacción enviada.)\n\nPuede lograr verificación de la transacción dentro del monedero:\nmasari-wallet-cli : usando el comando (check_tx_key).\nmasari-wallet-gui: vaya a la pestaña Avanzado > Comprobar/Revisar.\nVerificación de transacción también se puede lograr desde el explorador de bloques\nAbrir explorador de bloques (https://explorer.getmasari.org), use la barra de búsqueda para buscar el hash de transacción. \nCuando encuentre su transacción, navegue hacia el final donde dice "Comprobar Envío" y rellene los detalles necesarios. \nUsted necesita compartir esta información con el mediador o árbitro en caso de disputa:\n- La clave privada tx\n- El hash de transacción\n- La dirección pública del que recibe\n\nSi no proporciona los datos arriba señalados, o si utilizó una cartera incompatible, perderá la disputa. El que envía MSR es responsable de entregar la verificación de transferencia al mediador o árbitro en caso de disputa.\n\nNo se requiere el ID de pago, sólo la dirección pública normal.\nSi no está seguro sobre el proceso, pida ayuda visitando el canal oficial de Masari en Discord (https://discord.gg/sMCwMqs). # suppress inspection "UnusedProperty" account.crypto.popup.blur.msg=Intercambiar BLUR en Haveno requiere que entienda y cumpla los siguientes requisitos:\n\nPara enviar BLUR debe usar Blur Network CLI o la cartera GUI. \n\nSi utiliza la cartera CLI, un hash de transacción (tx ID) se mostrará después de hacer una transferencia. Deberá guardar esta información. Inmediatamente después de enviar la transferencia, deberá utilizar el comando 'get_tx_key' para obtener la clave privada de transacción. Si no realiza este paso, es posible que no pueda obtener la llave mas tarde. \n\nSi utiliza la cartera Blur Network GUI, la clave privada de transacción y el ID de transacción puede ser encontrada convenientemente en la pestaña "Historia". Inmediatamente después del envío, localice la transacción de interés. Pulse en el símbolo "?" en la esquina inferior derecha de la caja que contiene la transacción. Debe guardar esta información. \n\nEn caso de que sea necesaria una disputa, deberá presentar lo siguiente al mediador o al árbitro: 1.) el ID de transacción, 2.) la clave privada de transacción, y 3.) la dirección de destino. El mediador o árbitro entonces verificará la transferencia BLUR utilizando el Visor de Transacción Blur (https://blur.cash/#tx-viewer).\n\nSi no se proporciona la información requerida al mediador o al árbitro se perderá el caso de disputa. En todos los casos de disputa, el remitente BLUR asume el 100% de la responsabilidad en la verificación de las transacciones a un mediador o a un árbitro.. \n\nSi no comprende estos requisitos, no realice transacciones en Haveno. Primero, busque ayuda en Blur Network Discord (https://discord.gg/dMWaqVW). # suppress inspection "UnusedProperty" account.crypto.popup.solo.msg=Para intercambiar Solo en Haveno es necesario que usted entienda y cumpla los siguientes requisitos:\n\nPara enviar Solo es necesario que use el monedero Solo Network Monedero CLI\n\nSi usted usa el monedero CLI el hash de transacción (tx ID) aparecerá después de la transferencia. Usted debe guardar esta información. Inmediatamente después de mandar de la transferencia, usted debe usar el comando 'get_tx_key' para guardar la clave privada de transacción. Si usted no guarda esta información, es posible que no podrá coleccionar esta clave después .\n\nUsted necesita compartir esta información con el mediador o árbitro en caso de disputa:\n1.) el ID de transacción, 2.) la clave privada de transacción y 3.) la dirección pública de recepción. El mediador o árbitro podrá verificar la transferencia de Solo usando el explorador de bloques Solo, buscando la transacción y usando la función "Prove sending" (https://explorer.minesolo.com/).\n\nSi no proporciona los datos arriba señalados al mediador o árbitro perderá la disputa. En todos los casos de disputa, el emisor de Solo es 100% responsable de entregar la verificación de envío al mediador o árbitro.\n\nSi no está seguro o no entiendo estos requisitos, no haga intercambio/transacción en Haveno. Primero, pida ayuda visitando el canal oficial de Solo Network en Discord (https://discord.minesolo.com/). # suppress inspection "UnusedProperty" account.crypto.popup.cash2.msg=Intercambiar CASH2 en Haveno requiere que usted comprenda y cumpla con los siguientes requisitos:\n\nPara enviar CASH2, debe utilizar la cartera Cash2 versión 3 o superior. \n\nDespués de que se envía una transacción, se mostrará el ID de transacción. Debe guardar esta información. Inmediatamente después de enviar la transacción, debe usar el comando 'getTxKey' en simplewallet para recuperar la llave secreta de la transacción. \n\nEn el caso de que sea necesario un arbitraje, debe presentar lo siguiente a un mediador o a un árbitro: 1) ID de transacción, 2) la llave secreta de la transacción y 3) la dirección Cash2 del destinatario. El mediador o el árbitro entonces verificará la transferencia CASH2 usando el Explorador de Bloques Cash2 (https://blocks.cash2.org).\n\nSi no se proporciona la información requerida al mediador o al árbitro, se perderá el caso de disputa. En todos los casos de disputa, el remitente de CASH2 lleva el 100% de la carga de la responsabilidad de verificar las transacciones a un mediador o a un árbitro.\n\nSi no comprende estos requisitos, no realice transacciones en Haveno. Primero, busque ayuda en Cash2 Discord (https://discord.gg/FGfXAYN). # suppress inspection "UnusedProperty" account.crypto.popup.qwertycoin.msg=Intercambiar Qwertycoin en Haveno requiere que usted comprenda y cumpla los siguientes requisitos:\n\nPara enviar QWC, debe utilizar la cartera oficial QWC versión 5.1.3 o superior. \n\nDespués de que se envía una transacción, se mostrará el ID de transacción. Debe guardar esta información. Inmediatamente después de enviar la transacción, debe usar el comando 'get_Tx_Key' en simplewallet para recuperar la llave secreta de la transacción.\n\nEn el caso de que sea necesario un arbitraje, debe presentar lo siguiente a un mediador o un árbitro: 1) ID de transacción, 2) la llave secreta de la transacción y 3) la dirección QWC del destinatario. El mediador o el árbitro entonces verificará la transferencia QWC usando el Explorador de Bloques QWC (https://explorer.qwertycoin.org).\n\nSi no se proporciona la información requerida al mediador o al árbitro, se perderá el caso de disputa. En todos los casos de disputa, el remitente de QWC lleva el 100% de la carga de la responsabilidad de verificar las transacciones a un mediador o un árbitro.\n\nSi no comprende estos requisitos, no realice transacciones en Haveno. Primero, busque ayuda en QWC Discord (https://discord.gg/rUkfnpC). # suppress inspection "UnusedProperty" account.crypto.popup.drgl.msg=Intercambiar Dragonglass on Haveno requiere que usted comprenda y cumpla los siguientes requisitos:\n\nDebido a la privacidad que Dragonglass provee, una transacción no es verificable en la blockchain pública. Si es requerido, usted puede probar su pago a través del uso de su TXN-Private-Key.\nLa TXN-clave privada es una one-time clave generada automáticamente para cada transacción que solo puede ser accedida desde dentro de su cartera DRGL.\nO por DRGL-wallet GUI (dentro del diálogo de detalles de transacción) o por la Dragonglass CLI simplewallet (usando el comando "get_tx_key").\n\nDRGL version 'Oathkeeper' y superior son REQUERIDAS para ambos.\n\nEn caso de que sea necesario un arbitraje, debe presentar lo siguiente a un mediador o un árbitro:\n- La TXN-clave privada\n- El hash de transacción\n- La dirección pública del destinatario\n\nLa verificación de pago puede ser hecha usando los datos de arriba en (http://drgl.info/#check_txn).\n\nNo proporcionar los datos anteriores, o si usted usó una cartera incompatible, resultará en la pérdida del caso de disputa. El Dragonglass remitente es responsable de proveer verificación de la transferencia DRGL al mediador o al árbitro en caso de una disputa. El uso de PaymentID no es requerido.\n\nSi usted no está seguro sobre cualquier parte de este proceso, visite Dragonglass en Discord (http://discord.drgl.info) para ayuda. # suppress inspection "UnusedProperty" account.crypto.popup.ZEC.msg=Al usar Zcash solo puede usar las direcciones transparentes (que comienzan con t), no las direcciones-z (privadas), porque el mediador o el árbitro no sería capaz de verificar la transacción con direcciones-z. # suppress inspection "UnusedProperty" account.crypto.popup.XZC.msg=Al usar Zcoin puede usar únicamente las direcciones transparentes (trazables) y no las no-trazables, porque el mediador o el árbitro no sería capaz de verificar la transacción con direcciones no trazables en el explorador de bloques. # suppress inspection "UnusedProperty" account.crypto.popup.grin.msg=GRIN requiere un proceso interactivo entre el emisor y el receptor para crear la transacción. Asegúrese de seguir las instrucciones de la web del proyecto GRIN para enviar y recibir GRIN con seguridad (el receptor necesita estar en línea o al menos estar en línea durante un cierto periodo de tiempo).\nHaveno solo soporta el Grinbox (Wallet713) monedero URL formato.\n\nEl emisor GRIN requiere proveer prueba que ha enviado GRIN correctamente. Si el monedero no puede proveer esa prueba, una posible controversia será resuelta a favor del GRIN receptor. Por favor asegúrese que usa el último Grinbox software que soporta la prueba de transacción y que usted entiende el proceso de transferir y recibir GRIN así como la forma de crear la prueba.\n\nVer https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only para más información sobre la herramienta de prueba Grinbox. # suppress inspection "UnusedProperty" account.crypto.popup.beam.msg=BEAM requiere un proceso interactivo entre el emisor y el receptor para crear la transacción.\n\nAsegúrese de seguir la instrucciones de la página web del proyecto BEAM para enviar y recibir BEAM con seguridad (el receptor necesita estar el línea o por lo menos estar en línea durante cierto periodo de tiempo).\n\nEl emisor BEAM requiere proveer prueba de que envió BEAM correctamente. Asegúrese de usar software de monedero que pueda producir tal prueba. Si el monedero no provee la prueba, una posible controversia será resuelta en favor del BEAM receptor. # suppress inspection "UnusedProperty" account.crypto.popup.pars.msg=Intercambiar ParsiCoin en Haveno requiere que usted comprenda y cumpla con los siguientes requisitos:\n\nPara enviar PARS, debe usar la cartera oficial ParsiCoin version 3.0.0 o superior.\n\nPuede comprobar su hash de transacción y la clave de la transacción en la sección de Transacciones en su cartera GUI (ParsiPay). Necesita pulsar el botón derecho en la transacción y pulsar mostrar detalles.\n\nEn el caso de que sea necesario arbitraje, debe presentar lo siguiente al mediador o al árbitro: 1) El hash de transacción, 2) La llave de la transacción, y 3) La dirección del receptor PARS. El árbitro entonces verificará la transferencia PARS usando el ParsiCoin Explorador de Bloques (http://explorer.parsicoin.net/#check_payment).\n\nSi no se proporciona la información requerida al mediador o al árbitro, se perderá el caso de disputa. En todos los casos de disputa, el remitente de ParsiCoin lleva 100% de la carga de la responsabilidad de verificar las transacciones a un mediador o un árbitro\n\nSi no comprende estos requisitos, no realice transacciones en Haveno. Primero, busque ayuda en ParsiCoin Discord (https://discord.gg/c7qmFNh). # suppress inspection "UnusedProperty" account.crypto.popup.blk-burnt.msg=Para intercambiar blackcoins quemados. usted necesita saber lo siguiente:\n\nBlackcoins quemados son indestructibles. Para intercambiarlos en Haveno, los guiones de output tienen que estar en la forma: OP_RETURN OP_PUSHDATA, seguidos por bytes de datos asociados que, después de codificarlos en hexadecimal, construyen direcciones. Por ejemplo, blackcoins quemados con una dirección 666f6f ("foo" en UTF-8) tendrán el siguiente guion:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\nPara crear blackcoins quemados, uno puede usar el comando RPC "quemar" disponible en algunas carteras.\n\nPara posibles casos de uso, uno puede mirar en https://ibo.laboratorium.ee .\n\nComo los blackcoins quemados son undestructibles, no pueden ser revendidos. "Vender" blackcoins quemados significa quemar blackcoins comunes (con datos asociados igual a la dirección de destino).\n\nEn caso de una disputa, el vendedor BLK necesita proveer el hash de transacción. # suppress inspection "UnusedProperty" account.crypto.popup.liquidmonero.msg=Comerciar L-XMR en Haveno requiere que entienda lo siguiente:\n\nAl recibir L-XMR de un intercambio en Haveno, no puede usar la app de monedero móvil Blockstream Green Wallet o un exchange/monedero que custodie sus fondos. Solo debe recibir L-XMR en el monedero Liquid Elements Core, u otro monedero L-XMR que le permita obtener la clave cegadora para su dirección L-XMR cegada.\n\nEn caso de ser necesaria mediación, o si se lleva a cabo una disputa, debe entregar la clave cegadora para su dirección receptora de L-XMR al mediador o agente de devolución de fondos Haveno para que verifique los detalles de su Transacción Confidencial en su nodo completo Elements Core.\n\nSi no entrega la información requerida al mediador o al agente de devolución de fondos se resolverá en una pérdida del caso en disputa. En todos los casos de disputa, el receptor de L-XMR es responsable del 100% de la carga de aportar la prueba criptográfica al mediador o agente de devolución de fondos.\n\nSi no entiende estos requerimientos, no intercambio L-XMR en Haveno. account.traditional.yourTraditionalAccounts=Sus cuentas de moneda nacional: account.backup.title=Copia de seguridad del monedero account.backup.location=Ubicación de la copia de seguridad account.backup.selectLocation=Seleccionar localización de la copia de seguridad account.backup.backupNow=Hacer copia de seguridad ahora (la copia de seguridad no está encriptada!) account.backup.appDir=Directorio de datos de aplicación account.backup.openDirectory=Abrir directorio account.backup.openLogFile=Abrir archivo de registro account.backup.success=Copia de seguridad guardada con éxito en:\n{0} account.backup.directoryNotAccessible=El directorio que ha elegido no es accesible. {0} account.password.removePw.button=Eliminar contraseña account.password.removePw.headline=Eliminar protección por contraseña del monedero account.password.setPw.button=Establecer contraseña account.password.setPw.headline=Establecer protección por contraseña del monedero account.password.info=Con la protección de contraseña habilitada, deberá ingresar su contraseña al iniciar la aplicación, al retirar monero de su billetera y al mostrar sus palabras de semilla. account.seed.backup.title=Haz una copia de seguridad de las palabras clave de tu billetera. account.seed.info=Por favor, anota tanto las palabras clave de tu billetera como la fecha. Puedes recuperar tu billetera en cualquier momento con las palabras clave y la fecha.\n\nDeberías anotar las palabras clave en una hoja de papel. No las guardes en la computadora.\n\nPor favor, ten en cuenta que las palabras clave NO son un reemplazo para una copia de seguridad.\nNecesitas crear una copia de seguridad de todo el directorio de la aplicación desde la pantalla "Cuenta/Copia de seguridad" para recuperar el estado y los datos de la aplicación. account.seed.backup.warning=Por favor, ten en cuenta que las palabras clave no son un reemplazo para una copia de seguridad.\nNecesitas crear una copia de seguridad de todo el directorio de la aplicación desde la pantalla "Cuenta/Copia de seguridad" para recuperar el estado y los datos de la aplicación. account.seed.warn.noPw.msg=No ha establecido una contraseña de cartera que proteja la visualización de las palabras semilla.\n\n¿Quiere que se muestren las palabras semilla? account.seed.warn.noPw.yes=Sí, y no preguntar de nuevo account.seed.enterPw=Introducir contraseña para ver las palabras semilla account.seed.restore.info=Por favor haga una copia de seguridad antes de aplicar la restauración desde las palabras semilla. Tenga en cuenta que la restauración de cartera solo es para casos de emergencia y puede causar problemas con la base de datos interna del monedero.\nNo es el modo de aplicar una restauración de copia de seguridad! Por favor use una copia de seguridad desde el archivo de directorio de la aplicación para restaurar un estado de aplicación anterior.\n\nDespués de restaurar la aplicación se cerrará automáticamente. Después de reiniciar la aplicacion se resincronizará con la red Monero. Esto puede llevar un tiempo y consumir mucha CPU, especialemente si la cartera es antigua y tiene muchas transacciones. Por favor evite interrumpir este proceso, o podría tener que borrar el archivo de la cadena SPV de nuevo o repetir el proceso de restauración. account.seed.restore.ok=Ok, adelante con la restauración y el apagado de Haveno. #################################################################### # Mobile notifications #################################################################### account.notifications.setup.title=Configuración account.notifications.download.label=Descargar aplicación móvil account.notifications.waitingForWebCam=Esperando a la cámara web... account.notifications.webCamWindow.headline=Escanear código QR desde el teléfono account.notifications.webcam.label=Usar webcam account.notifications.webcam.button=Escanear código QR account.notifications.noWebcam.button=No tengo cámara web account.notifications.erase.label=Eliminar notificaciones en el teléfono account.notifications.erase.title=Limpiar notificaciones account.notifications.email.label=Emparejar token account.notifications.email.prompt=Introducir token de emparejamiento recibido por emai account.notifications.settings.title=Configuración account.notifications.useSound.label=Reproducir sonido de notificación en el teléfono account.notifications.trade.label=Recibir mensajes de intercambio account.notifications.market.label=Recibir alertas de oferta account.notifications.price.label=Recibir alertas de precio account.notifications.priceAlert.title=Alertas de precio account.notifications.priceAlert.high.label=Notificar si el precio de XMR está por encima account.notifications.priceAlert.low.label=Notificar si el precio de XMR está por debajo account.notifications.priceAlert.setButton=Establecer alerta de precio account.notifications.priceAlert.removeButton=Eliminar alerta de precio account.notifications.trade.message.title=Estado de intercambio cambiado account.notifications.trade.message.msg.conf=La transacción de depósito para el intercambio con ID {0} está confirmado. Por favor abra su aplicación Haveno e inicie el pago. account.notifications.trade.message.msg.started=El comprador de XMR ha iniciado el pago para el intercambio con ID {0} account.notifications.trade.message.msg.completed=El intercambio con ID {0} se ha completado. account.notifications.offer.message.title=Su oferta fue tomada. account.notifications.offer.message.msg=Su oferta con ID {0} se ha tomado. account.notifications.dispute.message.title=Nuevo mensaje de disputa account.notifications.dispute.message.msg=Ha recibido un mensaje de disputa para el intercambio con ID {0} account.notifications.marketAlert.title=Altertas de oferta account.notifications.marketAlert.selectPaymentAccount=Ofertas que concuerden con la cuenta de pago account.notifications.marketAlert.offerType.label=Tipo de oferta en la que estoy interesado account.notifications.marketAlert.offerType.buy=Ofertas de compra (quiero vender XMR) account.notifications.marketAlert.offerType.sell=Ofertas de venta (quiero comprar XMR) account.notifications.marketAlert.trigger=Distancia de precio en la oferta (%) account.notifications.marketAlert.trigger.info=Con distancia de precio establecida, solamente recibirá una alerta cuando se publique una oferta que satisfaga (o exceda) sus requerimientos. Por ejemplo: quiere vender XMR, pero solo venderá con un 2% de premium sobre el precio de mercado actual. Configurando este campo a 2% le asegurará que solo recibirá alertas de ofertas con precios que estén al 2% (o más) sobre el precio de mercado actual. account.notifications.marketAlert.trigger.prompt=Porcentaje de distancia desde el precio de mercado (e.g. 2.50%, -0.50%, etc) account.notifications.marketAlert.addButton=Añadir alerta de oferta account.notifications.marketAlert.manageAlertsButton=Gestionar alertas de oferta account.notifications.marketAlert.manageAlerts.title=Gestionar alertas de oferta account.notifications.marketAlert.manageAlerts.header.paymentAccount=Cuenta de pago account.notifications.marketAlert.manageAlerts.header.trigger=Precio de ejecución account.notifications.marketAlert.manageAlerts.header.offerType=Tipo de oferta account.notifications.marketAlert.message.title=Alerta de oferta account.notifications.marketAlert.message.msg.below=por debajo account.notifications.marketAlert.message.msg.above=por encima account.notifications.marketAlert.message.msg=Una nueva oferta "{0} {1}" con el precio {2} ({3} {4} precio de mercado) y método de pago "{5}" se publicó en el libro de ofertas de Haveno.\nID de la oferta: {6}. account.notifications.priceAlert.message.title=Alerta de precio para {0} account.notifications.priceAlert.message.msg=Su alerta de precio se ejecutó. El precio actual de {0} es {1} {2} account.notifications.noWebCamFound.warning=No se ha encontrado una webcam.\n\nPor favor use la opción de email para enviar el token y clave de encriptación desde su teléfono móvil a la aplicación Haveno. account.notifications.priceAlert.warning.highPriceTooLow=El precio superior debe ser mayor que el precio inferior. account.notifications.priceAlert.warning.lowerPriceTooHigh=El precio inferior debe ser más bajo que el precio superior. #################################################################### # Windows #################################################################### inputControlWindow.headline=Seleccionar inputs de transacción inputControlWindow.balanceLabel=Saldo disponible contractWindow.title=Detalles de la disputa contractWindow.dates=Fecha oferta / Fecha intercambio contractWindow.xmrAddresses=Dirección Monero comprador XMR / vendedor XMR contractWindow.onions=Dirección de red de comprador XMR / Vendedor XMR contractWindow.accountAge=Edad de cuenta del comprador XMR / vendedor XMR contractWindow.numDisputes=No. de disputas del comprador XMR / Vendedor XMR contractWindow.contractHash=Hash del contrato displayAlertMessageWindow.headline=Información importante! displayAlertMessageWindow.update.headline=Información de actualización importante! displayAlertMessageWindow.update.download=Descargar: displayUpdateDownloadWindow.downloadedFiles=Archivos: displayUpdateDownloadWindow.downloadingFile=Descargando: {0} displayUpdateDownloadWindow.verifiedSigs=Firma verificada con claves: displayUpdateDownloadWindow.status.downloading=Descargando archivos... displayUpdateDownloadWindow.status.verifying=Verificando firma... displayUpdateDownloadWindow.button.label=Descargar instalador y verificar firma displayUpdateDownloadWindow.button.downloadLater=Descargar más tarde displayUpdateDownloadWindow.button.ignoreDownload=Ignorar esta versión displayUpdateDownloadWindow.headline=¡Una nueva versión de Haveno está disponible! displayUpdateDownloadWindow.download.failed.headline=Descarga fallida displayUpdateDownloadWindow.download.failed=Descarga fallida.\nPor favor descargue y verifique manualmente en [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.installer.failed=No se puede determinar el instalador correcto. Por favor, descargue y verifique manualmente en [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.verify.failed=Verificación fallida.\nPor favor descargue y verifique manualmente en [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.success=La nueva versión ha sido descargada con éxito y la firma verificada.\n\nPor favor abra el directorio de descargas, cierre la aplicación e instale la nueva versión. displayUpdateDownloadWindow.download.openDir=Abrir directorio de descargas disputeSummaryWindow.title=Resumen disputeSummaryWindow.openDate=Fecha de apertura de ticket disputeSummaryWindow.role=Rol del trader disputeSummaryWindow.payout=Pago de la cantidad de intercambio disputeSummaryWindow.payout.getsTradeAmount=XMR {0} obtiene la cantidad de pago de intercambio disputeSummaryWindow.payout.getsAll=Cantidad máxima de pago XMR {0} disputeSummaryWindow.payout.custom=Pago personalizado disputeSummaryWindow.payoutAmount.buyer=Cantidad de pago del comprador disputeSummaryWindow.payoutAmount.seller=Cantidad de pago del vendedor disputeSummaryWindow.payoutAmount.invert=Usar perdedor como publicador disputeSummaryWindow.reason=Razón de la disputa disputeSummaryWindow.tradePeriodEnd=Fin de periodo de intercambio disputeSummaryWindow.extraInfo=Información extra disputeSummaryWindow.delayedPayoutStatus=Estado de pago retrasado # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BUG=Bug # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.USABILITY=Usabilidad # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Violación del protocolo # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.NO_REPLY=Sin respuesta # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SCAM=Estafa # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OTHER=Otro # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BANK_PROBLEMS=Banco # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OPTION_TRADE=Intercambio de opciones # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Comerciante no responde # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Cuenta de emisor errónea # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PEER_WAS_LATE=El par actuó tarde # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=El intercambio ya había acabado disputeSummaryWindow.summaryNotes=Nota de resumen disputeSummaryWindow.addSummaryNotes=Añadir notas de sumario disputeSummaryWindow.close.button=Cerrar ticket # Do no change any line break or order of tokens as the structure is used for signature verification # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.msg=Ticket cerrado el {0}\n{1} dirección de nodo: {2}\n\nResumen:\nID de intercambio: {3}\nMoneda: {4}\nCantidad del intercambio: {5}\nCantidad de pago para el comprador de XMR: {6}\nCantidad de pago para el vendedor de XMR: {7}\n\nMotivo de la disputa: {8}\n\nNotas resumidas:\n{9}\n # Do no change any line break or order of tokens as the structure is used for signature verification disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3} disputeSummaryWindow.close.nextStepsForMediation=\nSiguientes pasos:\nAbrir intercambio y aceptar o rechazar la sugerencia del mediador disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nSiguientes pasos:\nNo es necesario que realice ninguna otra acción. Si el árbitro decidió a su favor, verá una transacción de "Reembolso desde arbitraje" en Fondos / Transacciones disputeSummaryWindow.close.closePeer=Necesitar cerrar también el ticket del par de intercambio! disputeSummaryWindow.close.txDetails.headline=Publicar transacción de devolución de fondos # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.buyer=El comprador recibe {0} en la dirección: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=El vendedor recibe {0} en la dirección: {1}\n disputeSummaryWindow.close.txDetails=Gastando: {0}\n{1}{2}Tasa de transacción: {3}\n\n¿Está seguro de que quiere publicar esta transacción?\n disputeSummaryWindow.close.noPayout.headline=Cerrar sin realizar algún pago disputeSummaryWindow.close.noPayout.text=¿Quiere cerrar sin realizar algún pago? emptyWalletWindow.headline=Herramienta de monedero {0} de emergencia emptyWalletWindow.info=Por favor usar sólo en caso de emergencia si no puede acceder a sus fondos desde la Interfaz de Usuario (UI).\n\nPor favor, tenga en cuenta que todas las ofertas abiertas se cerrarán automáticamente al usar esta herramienta.\n\nAntes de usar esta herramienta, por favor realice una copia de seguridad del directorio de datos. Puede hacerlo en \"Cuenta/Copia de Seguridad\".\n\nPor favor repórtenos su problema y envíe un reporte de fallos en Github en el foro de Haveno para que podamos investigar qué causa el problema. emptyWalletWindow.balance=Su balance disponible en cartera emptyWalletWindow.address=Su dirección de destino emptyWalletWindow.button=Enviar todos los fondos emptyWalletWindow.openOffers.warn=Tiene ofertas abiertas que se eliminarán si vacía el monedero.\n¿Está seguro de que quiere vaciar su monedero? emptyWalletWindow.openOffers.yes=Sí, estoy seguro emptyWalletWindow.sent.success=El balance de su monedero fue transferido con éxito. enterPrivKeyWindow.headline=Introduzca la clave privada para registrarse filterWindow.headline=Editar lista de filtro filterWindow.offers=Ofertas filtradas (separadas por coma) filterWindow.onions=Banned from trading addresses (comma sep.) filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.) filterWindow.accounts=Cuentas de intercambio filtradas:\nFormato: lista de [ID método de pago | campo de datos | valor] separada por coma. filterWindow.bannedCurrencies=Códigos de moneda filtrados (separados por coma) filterWindow.bannedPaymentMethods=ID's de métodos de pago filtrados (separados por coma) filterWindow.bannedAccountWitnessSignerPubKeys=Filtro de cuenta de las claves públicas de testigo de firmado (claves públicas en hexadecimal, separado por coma) filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) filterWindow.arbitrators=Árbitros filtrados (direcciones onion separadas por coma) filterWindow.mediators=Mediadores filtrados (direcciones onion separadas por coma) filterWindow.refundAgents=Agentes de devolución de fondos filtrados (direcciones onion separadas por coma) filterWindow.seedNode=Nodos semilla filtrados (direcciones onion separadas por coma) filterWindow.priceRelayNode=nodos de retransmisión de precio filtrados (direcciones onion separadas por coma) filterWindow.xmrNode=Nodos Monero filtrados (direcciones + puerto separadas por coma) filterWindow.preventPublicXmrNetwork=Prevenir uso de la red Monero pública filterWindow.disableAutoConf=Deshabilitar autoconfirmación filterWindow.autoConfExplorers=Exploradores de autoconfirmación filtrados (direcciones separadas por coma) filterWindow.disableTradeBelowVersion=Versión mínima requerida para intercambios. filterWindow.add=Añadir filtro filterWindow.remove=Eliminar filtro filterWindow.xmrFeeReceiverAddresses=Direcciones de recepción de la tasa XMR filterWindow.disableApi=Deshabilitar API filterWindow.disableMempoolValidation=Deshabilitar validación de Mempool offerDetailsWindow.minXmrAmount=Cantidad mínima XMR offerDetailsWindow.min=(mínimo {0}) offerDetailsWindow.distance=(distancia del precio de mercado: {0}) offerDetailsWindow.myTradingAccount=MI cuenta de intercambio offerDetailsWindow.offererBankId=(ID/BIC/SWIFT del banco del creador) offerDetailsWindow.offerersBankName=(nombre del banco del creador) offerDetailsWindow.bankId=ID de banco (v.g BIC o SWIFT) offerDetailsWindow.countryBank=País del banco del creador offerDetailsWindow.commitment=Compromiso offerDetailsWindow.agree=Estoy de acuerdo offerDetailsWindow.tac=Términos y condiciones: offerDetailsWindow.confirm.maker.buy=Confirmar: Crear oferta para comprar XMR con {0} offerDetailsWindow.confirm.maker.sell=Confirmar: Crear oferta para vender XMR por {0} offerDetailsWindow.confirm.taker.buy=Confirmar: Aceptar oferta para comprar XMR con {0} offerDetailsWindow.confirm.taker.sell=Confirmar: Aceptar oferta para vender XMR por {0} offerDetailsWindow.creationDate=Fecha de creación offerDetailsWindow.makersOnion=Dirección onion del creador offerDetailsWindow.challenge=Frase de contraseña de la oferta offerDetailsWindow.challenge.copy=Copiar frase de contraseña para compartir con tu contraparte qRCodeWindow.headline=Código QR qRCodeWindow.msg=Por favor, utilice este código QR para fondear su billetera Haveno desde su billetera externa. qRCodeWindow.request=Solicitud de pago:\n{0} selectDepositTxWindow.headline=Seleccione transacción de depósito para la disputa selectDepositTxWindow.msg=La transacción de depósito no se almacenó en el intercambio.\nPor favor seleccione una de las transacciones multifirma existentes de su monedero en que se usó la transacción de depósito en el intercambio fallido.\nPuede encontrar la transacción correcta abriendo la ventana de detalles del intercambio (clic en la ID de intercambio en la lista) y a continuación el output de la transacción del pago de tasa de intercambio a la siguiente transacción donde puede ver la transacción de depósito multifirma (la dirección comienza con 3). La ID de transacción debería ser visible en la lista presentada aquí. Una vez haya encontrado la transacción correcta selecciónela aquí y continúe.\n\nDisculpe los inconvenientes, pero este error ocurre muy poco a menudo y en el futuro intentaremos encontrar mejores maneras de resolverlo. selectDepositTxWindow.select=Selección depósito de transacción sendAlertMessageWindow.headline=Enviar notificación global sendAlertMessageWindow.alertMsg=Mensaje de alerta: sendAlertMessageWindow.enterMsg=Introducir mensaje sendAlertMessageWindow.isSoftwareUpdate=Notificación de descarga de software sendAlertMessageWindow.isUpdate=Lanzamiento completo sendAlertMessageWindow.isPreRelease=Pre-lanzamiento sendAlertMessageWindow.version=Nueva versión no. sendAlertMessageWindow.send=Enviar notificación sendAlertMessageWindow.remove=Eliminar notificación sendPrivateNotificationWindow.headline=Enviar mensaje privado sendPrivateNotificationWindow.privateNotification=Notificación privada sendPrivateNotificationWindow.enterNotification=Introducir notificación sendPrivateNotificationWindow.send=Enviar notificación privada showWalletDataWindow.walletData=Datos del monedero showWalletDataWindow.includePrivKeys=Incluir claves privadas: setXMRTxKeyWindow.headline=Prueba de envío de XMR setXMRTxKeyWindow.note=Añadiendo la información de transacción a continuación, se habilita la autoconfirmación para intercambios más rápidos. Ver más: https://haveno.exchange/wiki/Trading_Monero setXMRTxKeyWindow.txHash=ID de transacción (opcional) setXMRTxKeyWindow.txKey=Clave de transacción (opcional) # We do not translate the tac because of the legal nature. We would need translations checked by lawyers # in each language which is too expensive atm. tacWindow.headline=Acuerdo de usuario tacWindow.agree=Estoy de acuerdo tacWindow.disagree=No estoy de acuerdo, salir tacWindow.arbitrationSystem=Disputa resolución tradeDetailsWindow.headline=Intercambio tradeDetailsWindow.disputedPayoutTxId=ID transacción de pago en disputa: tradeDetailsWindow.tradeDate=Fecha de intercambio tradeDetailsWindow.txFee=Comisión de minado tradeDetailsWindow.tradePeersOnion=Dirección onion de par de intercambio tradeDetailsWindow.tradePeersPubKeyHash=Hash de las llaves públicas de pares de intercambio tradeDetailsWindow.tradeState=Estado del intercambio tradeDetailsWindow.agentAddresses=Árbitro/Mediador tradeDetailsWindow.detailData=Detalle de datos txDetailsWindow.headline=Detalles de transacción txDetailsWindow.xmr.noteSent=Ha enviado XMR txDetailsWindow.xmr.noteReceived=Has recibido XMR. txDetailsWindow.sentTo=Enviado a txDetailsWindow.receivedWith=Recibido con txDetailsWindow.txId=TxId closedTradesSummaryWindow.headline=Resume de historia de intercambio closedTradesSummaryWindow.totalAmount.title=Cantidad intercambiada total closedTradesSummaryWindow.totalAmount.value={0} ({1} con el precio de mercado actual) closedTradesSummaryWindow.totalVolume.title=Cantidad total intercambiada en {0} closedTradesSummaryWindow.totalMinerFee.title=Suma de todas las trasas de minado closedTradesSummaryWindow.totalMinerFee.value={0} ({1} de la cantidad total intercambiada) closedTradesSummaryWindow.totalTradeFeeInXmr.title=Suma de todas las tasas de intercambio pagadas en XMR closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} de la cantidad total intercambiada) walletPasswordWindow.headline=Introducir contraseña para desbloquear xmrConnectionError.headline=Error de conexión con Monero xmrConnectionError.providedNodes=Error al conectar con el/los nodo(s) Monero proporcionado(s).\n\n¿Desea usar el siguiente nodo Monero disponible? xmrConnectionError.customNodes=Error al conectar con su/s nodo(s) Monero personalizado(s).\n\n¿Desea usar el siguiente nodo Monero disponible? xmrConnectionError.localNode=Haveno estaba previamente conectado a un nodo Monero local, pero ya no es accesible.\n\nAsegúrese de que su nodo local esté en ejecución y completamente sincronizado, o elija otra opción para continuar. xmrConnectionError.localNode.start=Iniciar nodo local xmrConnectionError.localNode.start.error=Error al iniciar el nodo local xmrConnectionError.localNode.fallback=Conectar al siguiente mejor nodo torNetworkSettingWindow.header=Confirmación de red Tor torNetworkSettingWindow.noBridges=No utilizar bridges torNetworkSettingWindow.providedBridges=Conectar con los bridges proporcionados torNetworkSettingWindow.customBridges=Introducir bridges personalizados torNetworkSettingWindow.transportType=Tipo de transporte: torNetworkSettingWindow.obfs3=obfs3 torNetworkSettingWindow.obfs4=obfs4 (recomendado) torNetworkSettingWindow.meekAmazon=meek-amazon torNetworkSettingWindow.meekAzure=meek-azure torNetworkSettingWindow.enterBridge=Introducir uno o más bridge relays (una por linea) torNetworkSettingWindow.enterBridgePrompt=type address:port torNetworkSettingWindow.restartInfo=Necesita reiniciar para aplicar los cambios torNetworkSettingWindow.openTorWebPage=Abrir página web del proyecto Tor torNetworkSettingWindow.deleteFiles.header=¿Problemas de conexión? torNetworkSettingWindow.deleteFiles.info=Si tiene problemas de conexión repetidos al iniciar, borrar archivos Tor desactualizados podría ayudar. Para hacerlo haga clic en el botón inferior y luego reinicie. torNetworkSettingWindow.deleteFiles.button=Borrar archivos Tor desactualizados y finalizar. torNetworkSettingWindow.deleteFiles.progress=Cerrado de Tor en proceso torNetworkSettingWindow.deleteFiles.success=Archivos Tor desactualizados borrados. Por favor, reinice. torNetworkSettingWindow.bridges.header=¿Está Tor bloqueado? torNetworkSettingWindow.bridges.info=Si Tor está bloqueado por su proveedor de internet o por su país puede intentar usar puentes Tor.\nVisite la página web Tor en https://bridges.torproject.org para saber más acerca de los puentes y transportes conectables. feeOptionWindow.headline=Elija la moneda para el pago de la comisiones de intercambio feeOptionWindow.info=Puede elegir pagar la tasa de intercambio en BSQ o XMR. Si elige BSQ apreciará la comisión de intercambio descontada. feeOptionWindow.optionsLabel=Elija moneda para el pago de comisiones de intercambio feeOptionWindow.useXMR=Usar XMR feeOptionWindow.fee={0} (≈ {1}) feeOptionWindow.xmrFeeWithFiatAndPercentage={0} (≈ {1} / {2}) feeOptionWindow.xmrFeeWithPercentage={0} ({1}) #################################################################### # Popups #################################################################### popup.headline.notification=Notificación popup.headline.instruction=Por favor, tenga en cuenta: popup.headline.attention=Atención popup.headline.backgroundInfo=Información general popup.headline.feedback=Completado popup.headline.confirmation=Confirmación popup.headline.information=Información popup.headline.warning=Atención popup.headline.error=Error popup.doNotShowAgain=No mostrar de nuevo popup.reportError.log=Abrir archivo de registro popup.reportError.gitHub=Reportar al rastreador de problemas de Github popup.reportError={0}\n\nPara ayudarnos a mejorar el software por favor reporte el fallo en nuestro rastreador de fallos en https://github.com/haveno-dex/haveno/issues.\nEl mensaje de error será copiado al portapapeles cuando haga clic en cualquiera de los botones inferiores.\nHará el depurado de fallos más fácil si puede adjuntar el archivo haveno.log presionando "Abrir archivo de log", guardando una copia y adjuntándola en su informe de errores. popup.error.tryRestart=Por favor pruebe reiniciar la aplicación y comprobar su conexión a la red para ver si puede resolver el problema. popup.error.takeOfferRequestFailed=Un error ocurrió cuando alguien intentó tomar una de sus ofertas:\n{0} error.spvFileCorrupted=Ocurrió un error al leer el archivo de cadena SPV.\nPuede ser que el archivo de cadena SPV se haya corrompido.\n\nMensaje de error: {0}\n\n¿Quiere borrarlo y comenzar una resincronización? error.deleteAddressEntryListFailed=No se pudo borrar el archivo AddressEntryList.\nError: {0} error.closedTradeWithUnconfirmedDepositTx=La transacción de depósito de el intercambio cerrado con la ID de intercambio {0} aún no se ha confirmado.\n\nPor favor haga una resincronización SPV en \"Configuración/Información de red\" para ver si la transacción es válida. error.closedTradeWithNoDepositTx=El depósito de transacción de el intercambio cerrado con ID de intercambio {0} es inválido.\nPor favor reinicie la aplicación para limpiar la lista de intercambios cerrados. popup.warning.walletNotInitialized=La cartera aún no sea ha iniciado popup.warning.osxKeyLoggerWarning=Debido a medidas de seguridad más estrictas en macOS 10.14 y siguientes, al iniciar una aplicación Java (Haveno usa Java) causa un popup de alarma en macOS ('Haveno would like to receive keystrokes from any application').\n\nPara evitar esto por favor abra su 'Configuración macOS' y vaya a 'Seguridad y privacidad' -> 'Privacidad¡ -> 'Monitorización de inputs' y elimine 'Haveno' de la lista a la derecha.\n\nHaveno actualizara a una nueva versión de Java para evitar que este problema tan pronto como se resuelvan las limitaciones técnicas (el paquete de Java para la versión requerida de Java aún no se ha emitido). popup.warning.wrongVersion=Probablemente tenga una versión de Haveno incorrecta para este ordenador.\nLa arquitectura de su ordenador es: {0}.\nLos binarios de Haveno instalados son: {1}.\nPor favor cierre y reinstale la versión correcta ({2}). popup.warning.incompatibleDB=¡Hemos detectado archivos de base de datos incompatibles!\n\nEstos archivos de base de datos no son compatibles con nuestro actual código base:\n{0}\n\nHemos hecho una copia de seguridad de los archivos corruptos y aplicado los valores por defecto a la nueva versión de base de datos.\n\nLa copia de seguridad se localiza en:\n{1}/db/backup_of_corrupted_data.\n\nPor favor, compruebe si tiene la última versión de Haveno instalada.\nPuede descargarla en:\n[HYPERLINK:https://haveno.exchange/downloads]\n\nPor favor, reinicie la aplicación. popup.warning.startupFailed.twoInstances=Ya está ejecutando Haveno. No puede ejecutar dos instancias de Haveno. popup.warning.tradePeriod.halfReached=Su intercambio con ID {0} ha alcanzado la mitad de el periodo máximo permitido de intercambio y aún no está completada.\n\nEl periodo de intercambio termina el {1}\n\nPor favor, compruebe el estado de su intercambio en \"Portafolio/Intercambios abiertos\" para más información. popup.warning.tradePeriod.ended=Su intercambio con ID {0} ha alcanzado el periodo máximo de intercambio y no se ha completado.\n\nEl periodo de intercambio finalizó en {1}\n\nPor favor, compruebe su intrecambio en \"Portafolio/Intercambios abiertos\" para contactar con el mediador. popup.warning.noTradingAccountSetup.headline=No ha configurado una cuenta de intercambio popup.warning.noTradingAccountSetup.msg=Necesita configurar una moneda nacional o cuenta de crypto antes de crear una oferta.\n¿Quiere configurar una cuenta? popup.warning.noArbitratorsAvailable=No hay árbitros disponibles. popup.warning.noMediatorsAvailable=No hay mediadores disponibles. popup.warning.notFullyConnected=Necesita esperar hasta que esté completamente conectado a la red.\nPuede llevar hasta 2 minutos al inicio. popup.warning.notSufficientConnectionsToXmrNetwork=Necesita esperar hasta que tenga al menos {0} conexiones a la red Monero. popup.warning.downloadNotComplete=Tiene que esperar hasta que finalice la descarga de los bloques Monero que faltan. popup.warning.walletNotSynced=La billetera Haveno no está sincronizada con la última altura de la cadena de bloques. Por favor, espere hasta que la billetera se sincronice o verifique su conexión. popup.warning.removeOffer=¿Está seguro que quiere eliminar la oferta? popup.warning.tooLargePercentageValue=No puede establecer un porcentaje del 100% o superior. popup.warning.examplePercentageValue=Por favor, introduzca un número de porcentaje como \"5.4\" para 5.4% popup.warning.noPriceFeedAvailable=No hay una fuente de precios disponible para esta moneda. No puede utilizar un precio basado en porcentaje.\nPor favor, seleccione un precio fijo. popup.warning.sendMsgFailed=El envío de mensaje a su compañero de intercambio falló.\nPor favor, pruebe de nuevo y si continúa fallando, reporte el fallo. popup.warning.messageTooLong=Su mensaje excede el tamaño máximo permitido. Por favor, envíelo por partes o súbalo a un servicio como https://pastebin.com popup.warning.lockedUpFunds=Ha bloqueado fondos de un intercambio fallido.\nBalance bloqueado: {0}\nDirección de depósito TX: {1}\nID de intercambio: {2}.\n\nPor favor, abra un ticket de soporte seleccionando el intercambio en la pantalla de intercambios pendientes y haciendo clic en \"alt + o\" o \"option + o\"." popup.warning.moneroConnection=Hubo un problema al conectar con la red de Monero.\n\n{0} popup.warning.makerTxInvalid=Esta oferta no es válida. Por favor seleccione otra oferta diferente.\n\n takeOffer.cancelButton=Cancelar toma de oferta takeOffer.warningButton=Ignorar y continuar de todos modos # suppress inspection "UnusedProperty" popup.warning.nodeBanned=Uno de los nodos {0} ha sido baneado. # suppress inspection "UnusedProperty" popup.warning.priceRelay=retransmisión de precio popup.warning.seed=semilla popup.warning.mandatoryUpdate.trading=Por favor, actualice a la última versión de Haveno. Se lanzó una actualización obligatoria que inhabilita intercambios con versiones anteriores. Por favor, lea el Foro de Haveno para más información\n popup.warning.noFilter=No recibimos un objeto de filtro de los nodos semilla. Por favor, informe a los administradores de la red que registren un objeto de filtro. popup.warning.burnXMR=Esta transacción no es posible, ya que las comisiones de minado de {0} excederían la cantidad a transferir de {1}. Por favor, espere a que las comisiones de minado bajen o hasta que haya acumulado más XMR para transferir. popup.warning.openOffer.makerFeeTxRejected=La tasa de transacción para la oferta con ID {0} se rechazó por la red Monero.\nID de transacción={1}\nLa oferta se ha eliminado para evitar futuros problemas.\nPor favor vaya a \"Configuración/Información de red\" y haga una resincronización SPV.\nPara más ayuda por favor contacte con el equipo de soporte de Haveno en el canal de Haveno en Keybase. popup.warning.trade.txRejected.tradeFee=tasa de intercambio popup.warning.trade.txRejected.deposit=depósito popup.warning.trade.txRejected=La transacción {0} para el intercambio con ID {1} se rechazó por la red Monero.\nID de transacción={2}\nEl intercambio se movió a intercambios fallidos.\nPor favor vaya a \"Configuración/Información de red\" y haga una resincronización SPV.\nPara más ayuda por favor contacte con el equipo de soporte de Haveno en el canal de Haveno en Keybase. popup.warning.openOfferWithInvalidMakerFeeTx=La transacción de tasa de creador para la oferta con ID {0} es inválida.\nID de transacción={1}.\nPor favor vaya a \"Configuración/Información de red\" y haga una resincronización SPV.\nPara más ayuda por favor contacte con el equipo de soporte de Haveno en el canal de Haveno de Keybase. popup.info.securityDepositInfo=Para asegurarse de que ambos comerciantes siguen el protocolo de intercambio, ambos necesitan pagar un depósito de seguridad.\n\nEl depósito se guarda en su monedero de intercambio hasta que el intercambio se complete, y entonces se devuelve.\n\nPor favor, tenga en cuenta que al crear una nueva oferta, Haveno necesita estar en ejecución para que otro comerciante la tome. Para mantener sus ofertas en línea, mantenga Haveno funcionando y asegúrese de que su computadora está en línea también (Ej. asegúrese de que no pasa a modo standby...el monitor en standby no es problema!) popup.info.cashDepositInfo=Por favor asegúrese de que tiene una oficina bancaria donde pueda hacer el depósito de efectivo.\nEl ID del banco (BIC/SWIFT) de del vendedor es: {0} popup.info.cashDepositInfo.confirm=Confirmo que puedo hacer el depósito popup.info.shutDownWithOpenOffers=Haveno se está cerrando, pero hay ofertas abiertas.\n\nEstas ofertas no estarán disponibles en la red P2P mientras Haveno esté cerrado, pero serán re-publicadas a la red P2P la próxima vez que inicie Haveno.\n\nPara mantener sus ofertas en línea, mantenga Haveno ejecutándose y asegúrese de que la computadora permanece en línea también (Ej. asegúrese de que no se pone en modo standby... el monitor en espera no es un problema). popup.info.qubesOSSetupInfo=Parece que está ejecutando Haveno en Qubes OS\n\nAsegúrese de que su Haveno qube esté configurado de acuerdo con nuestra Guía de configuración en [HYPERLINK:https://haveno.exchange/wiki/Running_Haveno_on_Qubes] popup.warn.downGradePrevention=Degradar desde la versión {0} a la versión {1} no está soportado. Por favor use la última versión de Haveno. popup.privateNotification.headline=Notificación privada importante! popup.securityRecommendation.headline=Recomendación de seguridad importante popup.securityRecommendation.msg=Nos gustaría recordarle que considere usar protección por contraseña para su cartera, si no la ha activado ya.\n\nTambién es muy recomendable que escriba en un papel las palabras semilla del monedero. Esas palabras semilla son como una contraseña maestra para recuperar su cartera Monero.\nEn la sección \"Semilla de cartera\" encontrará más información.\n\nAdicionalmente, debería hacer una copia de seguridad completa del directorio de aplicación en la sección \"Copia de seguridad\" popup.xmrLocalNode.msg=Haveno ha detectado un nodo de Monero ejecutándose en esta máquina (en el localhost).\n\nPor favor, asegúrese de que el nodo esté completamente sincronizado antes de iniciar Haveno. popup.shutDownInProgress.headline=Cerrando aplicación... popup.shutDownInProgress.msg=Cerrar la aplicación puede llevar unos segundos.\nPor favor no interrumpa el proceso. popup.attention.forTradeWithId=Se requiere atención para el intercambio con ID {0} popup.attention.reasonForPaymentRuleChange=La versión 1.5.5 introduce un cambio crítico en las reglas de intercambio acerca del campo \"motivo de pago\" de las transferencias bancarias. Por favor deje este campo vacío -- NO USE NUNCA Más la ID de intercambio como \"motivo de pago\". popup.info.multiplePaymentAccounts.headline=Múltiples cuentas de pago disponibles popup.info.multiplePaymentAccounts.msg=Tiene múltiples cuentes de pago disponibles para esta oferta. Por favor, asegúrese de que ha elegido la correcta. popup.accountSigning.selectAccounts.headline=Seleccionar cuentas de pago popup.accountSigning.selectAccounts.description=Basado en el método de pago y el momento de tiempo todas las cuentas de pago que estén conectadas a una disputa donde ocurra el pago a el comprador será seleccionada por usted para firmarlas. popup.accountSigning.selectAccounts.signAll=Firma todos los métodos de pago popup.accountSigning.selectAccounts.datePicker=Seleccione momento de tiempo hasta que las cuentas sean firmadas popup.accountSigning.confirmSelectedAccounts.headline=Confirmar cuentas de pago seleccionadas popup.accountSigning.confirmSelectedAccounts.description=Basado en su valor introducido, {0} cuentas de pago serán seleccionadas. popup.accountSigning.confirmSelectedAccounts.button=Confirmar cuentas de pago popup.accountSigning.signAccounts.headline=Confirmar firma de cuentas de pago popup.accountSigning.signAccounts.description=Según su selección, se firmarán {0} cuentas de pago. popup.accountSigning.signAccounts.button=Firmar cuentas de pago popup.accountSigning.signAccounts.ECKey=Introduzca clave privada del árbitro popup.accountSigning.signAccounts.ECKey.error=ECKey de mal árbitro popup.accountSigning.success.headline=Felicidades popup.accountSigning.success.description=Todas las cuentas de pago {0} se firmaron con éxito! popup.accountSigning.generalInformation=Encontrará el estado de firma de todas sus cuentas en la sección de cuentas.\n\nPara más información, por favor visite [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. popup.accountSigning.signedByArbitrator=Una de sus cuentas de pago ha sido verificada y firmada por un árbitro. Intercambiar con esta cuenta firmará automáticamente la cuenta de su par de intercambio después de un intercambio exitoso.\n\n{0} popup.accountSigning.signedByPeer=Una de sus cuentas de pago ha sido verificada y firmada por un par de intercambio. Su límite inicial de intercambio ha sido elevado y podrá firmar otras cuentas en {0} días desde ahora.\n\n{1} popup.accountSigning.peerLimitLifted=El límite inicial para una de sus cuentas se ha elevado.\n\n{0} popup.accountSigning.peerSigner=Una de sus cuentas es suficiente antigua para firmar otras cuentas de pago y el límite inicial para una de sus cuentas se ha elevado.\n\n{0} popup.accountSigning.singleAccountSelect.headline=Importar edad de cuenta de testigos no firmados popup.accountSigning.confirmSingleAccount.headline=Confirmar el testigo de cuenta seleccionado popup.accountSigning.confirmSingleAccount.selectedHash=Hash del testigo seleccionado popup.accountSigning.confirmSingleAccount.button=Firmar testigo de edad de cuenta popup.accountSigning.successSingleAccount.description=Se seleccionón el testigo {0} popup.accountSigning.successSingleAccount.success.headline=Éxito popup.accountSigning.unsignedPubKeys.headline=Claves públicas no firmadas popup.accountSigning.unsignedPubKeys.sign=Firmar claves públicas popup.accountSigning.unsignedPubKeys.signed=Las claves públicas se firmaron popup.accountSigning.unsignedPubKeys.result.signed=Claves públicas firmadas popup.accountSigning.unsignedPubKeys.result.failed=Error al firmar popup.info.buyerAsTakerWithoutDeposit.headline=No se requiere depósito del comprador popup.info.buyerAsTakerWithoutDeposit=Tu oferta no requerirá un depósito de seguridad ni una tarifa del comprador de XMR.\n\nPara aceptar tu oferta, debes compartir una frase de acceso con tu compañero de comercio fuera de Haveno.\n\nLa frase de acceso se genera automáticamente y se muestra en los detalles de la oferta después de la creación. #################################################################### # Notifications #################################################################### notification.trade.headline=Notificación de intercambio con ID {0} notification.ticket.headline=Ticket de soporte de intercambio con ID {0} notification.trade.completed=El intercambio se ha completado y puede retirar sus fondos. notification.trade.accepted=Su oferta ha sido aceptada por un {0} XMR notification.trade.unlocked=Su intercambio tiene al menos una confirmación en la cadena de bloques.\nPuede comenzar el pago ahora. notification.trade.paymentSent=El comprador de XMR ha comenzado el pago. notification.trade.selectTrade=Seleccionar intercambio notification.trade.peerOpenedDispute=Su pareja de intercambio ha abierto un {0}. notification.trade.disputeClosed={0} se ha cerrado. notification.walletUpdate.headline=Actualizar monedero de intercambio. notification.walletUpdate.msg=Su monedero de intercambio tiene fondos suficientes.\nCantidad: {0} notification.takeOffer.walletUpdate.msg=Su monedero de intercambio ya tiene fondos suficientes de un intento de toma de oferta anterior.\nCantidad: {0} notification.tradeCompleted.headline=Intercambio completado notification.tradeCompleted.msg=Ahora puede retirar sus fondos a una billetera externa de Monero o mantenerlos en su billetera Haveno. #################################################################### # System Tray #################################################################### systemTray.show=Mostrar ventana de aplicación systemTray.hide=Esconder ventana de aplicación systemTray.info=Información sobre Haveno systemTray.exit=Salir systemTray.tooltip=Haveno: Una red de intercambio de monero descentralizada #################################################################### # GUI Util #################################################################### guiUtil.accountExport.savedToPath=Las cuentas de intercambio se han guardado en el directorio:\n{0} guiUtil.accountExport.noAccountSetup=No tiene cuentas de intercambio configuradas para exportar. guiUtil.accountExport.selectPath=Seleccionar directorio a {0} # suppress inspection "TrailingSpacesInProperty" guiUtil.accountExport.tradingAccount=Cuenta de intercambio con id {0}\n # suppress inspection "TrailingSpacesInProperty" guiUtil.accountImport.noImport=No hemos importado la cuenta de intercambio con id {0} porque ya existe.\n guiUtil.accountExport.exportFailed=La exportación a CSV ha fallado por un error.\nError = {0} guiUtil.accountExport.selectExportPath=Seleccionar directorio de exportación. guiUtil.accountImport.imported=Cuenta de intercambio importada desde la ruta:\n{0}\n\nCuentas importadas:\n{1} guiUtil.accountImport.noAccountsFound=No se han encontrado cuentas de intercambio exportadas en la ruta: {0}.\nEl nombre del archivo es {1}." guiUtil.openWebBrowser.warning=Va a abrir una página web en el navegador de su sistema.\n¿Quiere abrir la página web ahora?\n\nSi no está usando \"Navegador Tor\" como su navegador de sistema predeterminado, se conectará a la página web en la red abierta.\n\nURL: \"{0}\" guiUtil.openWebBrowser.doOpen=Abrir la página web y no preguntar de nuevo guiUtil.openWebBrowser.copyUrl=Copiar URL y cancelar guiUtil.ofTradeAmount=de cantidad de intercambio guiUtil.requiredMinimum=(mínimo requerido) #################################################################### # Component specific #################################################################### list.currency.select=Seleccionar moneda list.currency.showAll=Mostrar todo list.currency.editList=Editar lista de monedas table.placeholder.noItems=Actualmente no hay {0} disponible/s table.placeholder.noData=Actualmente no hay datos disponibles table.placeholder.processingData=Procesando datos... peerInfoIcon.tooltip.tradePeer=Pareja de intercambio peerInfoIcon.tooltip.maker=Creador peerInfoIcon.tooltip.trade.traded={0} dirección onion: {1}\nYa ha intercambiado en {2} ocasión/es con esa persona.\n{3} peerInfoIcon.tooltip.trade.notTraded=Dirección onion: {0}\nNo ha intercambiado con esta persona.\n{2} peerInfoIcon.tooltip.age=Cuenta de pago creada hace {0} peerInfoIcon.tooltip.unknownAge=Edad de cuenta de pago no conocida. tooltip.openPopupForDetails=Abrir popup para detalles tooltip.invalidTradeState.warning=Esta transacción está en un estado inválido. Abra la ventana de detalles para obtener más información tooltip.openBlockchainForAddress=Abrir explorador de cadena de bloques externo para la dirección: {0} tooltip.openBlockchainForTx=Abrir explorador de cadena de bloques externo para la la transacción: {0} confidence.unknown=Estado de transacción desconocido confidence.seen=Visto por {0} par/es / 0 confirmaciones confidence.confirmed={0} confirmaciones confidence.invalid=La transacción es inválida peerInfo.title=Información del par peerInfo.nrOfTrades=Número de intercambios completados peerInfo.notTradedYet=No ha comerciado con este usuario. peerInfo.setTag=Configurar etiqueta para ese par peerInfo.age.noRisk=Antigüedad de la cuenta de pago peerInfo.age.chargeBackRisk=Tiempo desde el firmado peerInfo.unknownAge=Edad desconocida addressTextField.openWallet=Abrir su cartera Monero predeterminada addressTextField.copyToClipboard=Copiar dirección al portapapeles addressTextField.addressCopiedToClipboard=La dirección se ha copiado al portapapeles addressTextField.openWallet.failed=Fallo al abrir la cartera Monero predeterminada. ¿Tal vez no tenga una instalada? peerInfoIcon.tooltip={0}\nEtiqueta: {1} txIdTextField.copyIcon.tooltip=Copiar ID de transacción al monedero txIdTextField.blockExplorerIcon.tooltip=Abrir un explorador de bloques con esta ID de transacción txIdTextField.missingTx.warning.tooltip=Falta la transacción requerida #################################################################### # Navigation #################################################################### navigation.account=\"Cuenta\" navigation.account.walletSeed=\"Cuenta/Semilla de cartera\" navigation.funds.availableForWithdrawal=\"Fondos/Enviar fondos"\" navigation.portfolio.myOpenOffers=\"Portafolio/Mis ofertas abiertas\" navigation.portfolio.pending=\"Portafolio/Intercambios abiertos\" navigation.portfolio.closedTrades=\"Portafolio/Historial\" navigation.funds.depositFunds=\"Fondos/Recibir fondos\" navigation.settings.preferences=\"Ajustes/Preferencias\" # suppress inspection "UnusedProperty" navigation.funds.transactions=\"Fondos/Transacciones\" navigation.support=\"Soporte\" #################################################################### # Formatter #################################################################### formatter.formatVolumeLabel={0} cantidad{1} formatter.makerTaker=Creador como {0} {1} / Tomador como {2} {3} formatter.makerTaker.locked=Creador como {0} {1} / Tomador como {2} {3} 🔒 formatter.youAreAsMaker=Usted es: {1} {0} (creador) / El tomador es: {3} {2} formatter.youAreAsTaker=Usted es: {1} {0} (tomador) / Creador es: {3} {2} formatter.youAre=Usted es {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Está creando una oferta a {0} {1} formatter.youAreCreatingAnOffer.traditional.locked=Está creando una oferta a {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Está creando una oferta a {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.crypto.locked=Está creando una oferta a {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} como creador formatter.asTaker={0} {1} como tomador #################################################################### # Domain specific #################################################################### # we use enum values here # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" XMR_MAINNET=Red principal de Monero # suppress inspection "UnusedProperty" XMR_LOCAL=Red de prueba de Monero # suppress inspection "UnusedProperty" XMR_STAGENET=Stagenet Monero time.year=Año time.month=Mes time.week=Semana time.day=Día time.hour=Hora time.minute10=10 minutos time.hours=horas time.days=días time.1hour=1 hora time.1day=1 día time.minute=minuto time.second=segundo time.minutes=minutos time.seconds=segundos password.enterPassword=Introducir contraseña password.confirmPassword=Confirmar contraseña password.tooLong=La contraseña debe de tener menos de 500 caracteres. password.deriveKey=Derivar clave desde contraseña password.walletDecrypted=El monedero se desencriptó con éxito y se eliminó la protección por contraseña. password.wrongPw=Ha introducido la contraseña incorrecta.\n\nPor favor, introduzca nuevamente la contraseña, evitando errores. password.walletEncrypted=El monedero se encriptó con éxito y se activó la protección por contraseña. password.passwordsDoNotMatch=Las 2 contraseñas introducidas no coinciden. password.forgotPassword=¿Ha olvidado la contraseña? password.backupReminder=Por favor, al establecer una contraseña para la cartera, tenga en cuenta que todas las copias de seguridad creadas de la cartera no encriptada serán borradas automáticamente password.backupWasDone=Ya he hecho una copia de seguridad password.setPassword=Establecer password (ya tengo una copia de seguridad) password.makeBackup=Hacer copia de seguridad seed.seedWords=Palabras semilla de la cartera seed.enterSeedWords=Introduzca palabras semilla de la cartera seed.date=Fecha de la cartera seed.restore.title=Restaurar monederos desde las palabras semilla seed.restore=Restaurar monederos seed.creationDate=Fecha de creación seed.warn.walletNotEmpty.msg=Su cartera de Monero no está vacía.\n\nDebe vaciar esta cartera antes de intentar restaurar a otra más antigua, ya que mezclar monederos puede llevar a copias de seguridad inválidas.\n\nPor favor, finalice sus intercambios, cierre todas las ofertas abiertas y vaya a la sección Fondos para retirar sus moneros.\nEn caso de que no pueda acceder a sus moneros puede utilizar la herramienta de emergencia para vaciar el monedero.\nPara abrir la herramienta de emergencia pulse \"alt + e\" o \"Cmd/Ctrl + e\". seed.warn.walletNotEmpty.restore=Quiero restaurar de todos modos seed.warn.walletNotEmpty.emptyWallet=Vaciaré mi monedero antes seed.warn.notEncryptedAnymore=Sus carteras están cifradas.\n\nDespués de restaurarlas, las carteras no estarán cifradas y tendrá que introducir una nueva contraseña.\n\n¿Quiere continuar? seed.warn.walletDateEmpty=Como no ha especificado una fecha específica para el monedero, haveno tendrá que escanear la cadena de bloques desde el 2013.10.09 (la fecha de BIP39).\n\nLos monederos BIP39 se introdujeron en haveno en 2017.06.28 (publicación v.0.5). Puede ahorrar tiempo utilizando esa fecha.\n\nIdealmente, debería especificar la fecha en que su semilla fue creada.\n\n\nEstá seguro de que quiere continuar sin especificar una fecha para el monedero? seed.restore.success=Las carteras se restauraron con éxito con las nuevas palabras semilla.\n\nDebe cerrar y reiniciar la aplicación seed.restore.error=Un error ocurrió el restaurar los monederos con las palabras semilla. {0} seed.restore.openOffers.warn=Tiene ofertas abiertas que serán eliminadas si restaura desde las palabras semilla.\n¿Está seguro de que quiere continuar? #################################################################### # Payment methods #################################################################### payment.account=Cuenta payment.account.no=Número de cuenta payment.account.name=Nombre de cuenta payment.account.username=Nombre de usuario payment.account.phoneNr=Número de teléfono payment.account.owner.fullname=Nombre completo del propietario de la cuenta payment.account.fullName=Nombre completo payment.account.state=Estado/Provincia/Región payment.account.city=Ciudad payment.bank.country=País del banco payment.account.name.email=Nombre completo / correo electrónico del titular de la cuenta: payment.account.name.emailAndHolderId=Nombre completo / correo electrónico del titular de la cuenta: {0} payment.bank.name=Nombre del banco payment.select.account=Seleccione tipo de cuenta payment.select.region=Seleccione región payment.select.country=Seleccione país payment.select.bank.country=Seleccione país del banco payment.foreign.currency=¿Está seguro de que quiere elegir una moneda diferente que la del país por defecto? payment.restore.default=No, restaurar moneda por defecto. payment.email=Email payment.country=País payment.extras=Requerimientos extra payment.email.mobile=Email o número de móvil payment.crypto.address=Dirección crypto payment.crypto.tradeInstantCheckbox=Intercambio instantáneo (en una hora) con esta crypto payment.crypto.tradeInstant.popup=Para intercambios instantáneos se requiere que ambos pares estén en linea para poder completar el intercambio en menos de 1 hora.\n\nSi tiene ofertas abiertas y no está disponible, por favor deshabilite esas ofertas en la pantalla 'Portafolio'. payment.crypto=Crypto payment.select.crypto=Seleccionar o buscar crypto payment.secret=Pregunta secreta payment.answer=Respuesta payment.wallet=ID de cartera: payment.amazon.site=Compre una tarjeta regalo en payment.ask=Pregunte en el Chat de Intercambio payment.uphold.accountId=Nombre de usuario, correo electrónico o núm de teléfono payment.moneyBeam.accountId=Correo electrónico o núm. de telefóno payment.popmoney.accountId=Correo electrónico o núm. de telefóno payment.promptPay.promptPayId=Citizen ID/Tax ID o número de teléfono payment.supportedCurrencies=Monedas soportadas payment.supportedCurrenciesForReceiver=Monedas para recibir fondos payment.limitations=Límitaciones: payment.salt="Salt" de la verificación de edad de la cuenta. payment.error.noHexSalt=El "salt" necesitar estar en formato HEX.\nSolo se recomienda editar el "salt" si quiere transferir el "salt" desde una cuenta antigua para mantener su edad de cuenta. La edad de cuenta se verifica usando el "salt" de la cuenta y datos de identificación de cuenta (Ej. IBAN). payment.accept.euro=Aceptar tratos desde estos países Euro. payment.accept.nonEuro=Aceptar tratos desde estos países no-Euro payment.accepted.countries=Países aceptados payment.accepted.banks=Bancos aceptados (ID) payment.mobile=Número de móvil payment.postal.address=Dirección postal payment.national.account.id.AR=Número CBU shared.accountSigningState=Status de firmado de cuentas #new payment.crypto.address.dyn=Dirección {0}: payment.crypto.receiver.address=Dirección crypto del receptor payment.accountNr=Número de cuenta payment.emailOrMobile=Email o número de móvil payment.useCustomAccountName=Utilizar nombre de cuenta personalizado payment.maxPeriod=Periodo máximo de intercambio payment.maxPeriodAndLimit=Duración máxima de intercambio: {0} / Compra máx: {1} / Venta máx: {2} / Edad de cuenta: {3} payment.maxPeriodAndLimitCrypto=Duración máxima de intercambio: {0} / Límite máximo de intercambio: {1} payment.currencyWithSymbol=Moneda: {0} payment.nameOfAcceptedBank=Nombre de banco aceptado payment.addAcceptedBank=Añadir banco aceptado payment.clearAcceptedBanks=Despejar bancos aceptados payment.bank.nameOptional=Nombre del banco (opcional) payment.bankCode=Código bancario payment.bankId=ID bancario (BIC/SWIFT) payment.bankIdOptional=ID bancaria (BIC/SWIFT) (opcional) payment.branchNr=Número de sucursal payment.branchNrOptional=Número de sucursal (opcional) payment.accountNrLabel=Número de cuenta (IBAN) payment.accountType=Tipo de cuenta payment.checking=Comprobando payment.savings=Ahorros payment.personalId=ID personal: payment.zelle.info=Zelle es un servicio de transmisión de dinero que funciona mejor *a través* de otro banco..\n\n1. Compruebe esta página para ver si (y cómo) trabaja su banco con Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Preste atención a los límites de transferencia -límites de envío- que varían entre bancos, y que los bancos especifican a menudo diferentes límites diarios, semanales y mensuales..\n\n3. Si su banco no trabaja con Zelle, aún puede usarlo a través de la app móvil de Zelle, pero sus límites de transferencia serán mucho menores.\n\n4. El nombre especificado en su cuenta Haveno DEBE ser igual que el nombre en su cuenta de Zelle/bancaria. \n\nSi no puede completar una transacción Zelle tal como se especifica en el contrato, puede perder algo (o todo) el depósito de seguridad!\n\nDebido a que Zelle tiene cierto riesgo de reversión de pago, se aconseja que los vendedores contacten con los compradores no firmados a través de email o SMS para verificar que el comprador realmente tiene la cuenta de Zelle especificada en Haveno. payment.fasterPayments.newRequirements.info=Algunos bancos han comenzado a verificar el nombre completo del receptor para las transferencias Faster Payments. Su cuenta actual Faster Payments no especifica un nombre completo.\n\nConsidere recrear su cuenta Faster Payments en Haveno para proporcionarle a los futuros compradores {0} un nombre completo.\n\nCuando vuelva a crear la cuenta, asegúrese de copiar el UK Short Code de forma precisa , el número de cuenta y los valores salt de la cuenta anterior a su cuenta nueva para la verificación de edad. Esto asegurará que la edad de su cuenta existente y el estado de la firma se conserven. payment.moneyGram.info=Al utilizar MoneyGram, el comprador de XMR tiene que enviar el número de autorización y una foto del recibo al vendedor de XMR por correo electrónico. El recibo debe mostrar claramente el nobre completo del vendedor, país, estado y cantidad. El email del vendedor se mostrará al comprador durante el proceso de intercambio. payment.westernUnion.info=Al utilizar Western Union, el comprador de XMR tiene que enviar el número de seguimiento (MTCN) y una foto del recibo al vendedor de XMR por correo electrónico. El recibo debe mostrar claramente el como el nombre completo del vendedor, país, ciudad y cantidad. Al comprador se le mostrará el correo electrónico del vendedor en el proceso de intercambio. payment.halCash.info=Al usar HalCash el comprador de XMR necesita enviar al vendedor de XMR el código HalCash a través de un mensaje de texto desde el teléfono móvil.\n\nPor favor asegúrese de que no excede la cantidad máxima que su banco le permite enviar con HalCash. La cantidad mínima por retirada es de 10 EUR y el máximo son 600 EUR. Para retiros frecuentes es 3000 por receptor al día y 6000 por receptor al mes. Por favor compruebe estos límites con su banco y asegúrese que son los mismos aquí expuestos.\n\nLa cantidad de retiro debe ser un múltiplo de 10 EUR ya que no se puede retirar otras cantidades desde el cajero automático. La Interfaz de Usuario en la pantalla crear oferta y tomar oferta ajustará la cantidad de XMR para que la cantidad de EUR sea correcta. No puede usar precios basados en el mercado ya que la cantidad de EUR cambiaría con el cambio de precios.\n\nEn caso de disputa el comprador de XMR necesita proveer la prueba de que ha enviado EUR. # suppress inspection "UnusedMessageFormatParameter" payment.limits.info=Por favor, tenga en cuenta que todas las transferencias bancarias tienen cierto riesgo de reversión de pago.\n\nPara disminuir este riesgo, Haveno establece límites por intercambio en función del nivel estimado de riesgo de reversión de pago para el método usado.\n\nPara este método de pago, su límite por intercambio para comprar y vender es {2}.\n\nEste límite solo aplica al tamaño de un intercambio: puede poner tantos intercambios como quira.\n\nConsulte detalles en la wiki [HYPERLINK:https://docs.haveno.exchange/overview/account_limits]. # suppress inspection "UnusedProperty" payment.limits.info.withSigning=Para limitar el riesgo de devolución de cargo, Haveno establece límites por compra basados en los 2 siguientes factores:\n\n1. Riesgo general de devolución de cargo para el método de pago\n2. Estado de firmado de cuenta\n\nEsta cuenta de pago aún no ha sido firmada, con lo que ha sido limitada para comprar {0} por intercambio. Después de firmarse, los límites de compra se incrementarán de esta manera:\n\n● Antes de ser firmada, y hasta 30 días después de la firma, su límite por intercambio de compra será {0}\n● 30 días después de la firma, su límite de compra por intercambio será de {1}\n● 60 días después de la firma, su límite de compra por intercambio será de {2}\n\nLos límites de venta no se ven afectados por el firmado de cuentas. Puede vender {2} en un solo \nintercambio inmediatamente.\n\nEstos límites solo aplican al tamaño de un intercambio. Puede hacer tantos intercambios como quiera.\n\n Consulte detalles en la wiki [HYPERLINK:https://docs.haveno.exchange/overview/account_limits].\n\n payment.cashDeposit.info=Por favor confirme que su banco permite enviar depósitos de efectivo a cuentas de otras personas. Por ejemplo, Bank of America y Wells Fargo ya no permiten estos depósitos. payment.revolut.info=Revolut requiere el 'nombre de usuario' como ID de cuenta y no el número de teléfono o el e-mail que se requería anteriormente. payment.account.revolut.addUserNameInfo={0}\nSu cuenta de Revolut ({1}) no tiene un "nombre de usuario".\nPor favor introduzca su "nombre de usuario" en Revolut para actualizar sus datos de cuenta.\nEsto no afectará a su estado de edad de firmado de cuenta. payment.revolut.addUserNameInfo.headLine=Actualizar cuenta Revolut payment.cashapp.info=Tenga en cuenta que Cash App tiene un mayor riesgo de contracargos que la mayoría de las transferencias bancarias. payment.venmo.info=Tenga en cuenta que Venmo tiene un mayor riesgo de contracargos que la mayoría de las transferencias bancarias. payment.paypal.info=Tenga en cuenta que PayPal tiene un mayor riesgo de contracargos que la mayoría de las transferencias bancarias. payment.amazonGiftCard.upgrade=El método de pago Tarjetas regalo Amazon requiere que se especifique el país payment.account.amazonGiftCard.addCountryInfo={0}\nSu cuenta actual de Tarjeta regalo Amazon ({1}) no tiene un País especificado.\nPor favor introduzca el país de su Tarjeta regalo Amazon para actualizar sus datos de cuenta.\nEsto no afectará el estatus de edad de su cuenta. payment.amazonGiftCard.upgrade.headLine=Actualizar cuenta Tarjeta regalo Amazon payment.usPostalMoneyOrder.info=Los intercambios usando US Postal Money Orders (USPMO) en Haveno requiere que entienda lo siguiente:\n\n- Los compradores de XMR deben escribir la dirección del vendedor en los campos de "Payer" y "Payee" y tomar una foto en alta resolución de la USPMO y del sobre con la prueba de seguimiento antes de enviar.\n- Los compradores de XMR deben enviar la USPMO con confirmación de entrega.\n\nEn caso de que sea necesaria la mediación, se requerirá al comprador que entregue las fotos al mediador o agente de devolución de fondos, junto con el número de serie de la USPMO, número de oficina postal, y la cantidad de USD, para que puedan verificar los detalles en la web de US Post Office.\n\nNo entregar la información requerida al Mediador o Árbitro resultará en pérdida del caso de disputa. \n\nEn todos los casos de disputa, el emisor de la USPMO tiene el 100% de responsabilidad en aportar la evidencia al Mediador o Árbitro.\n\nSi no entiende estos requerimientos, no comercie usando USPMO en Haveno. payment.payByMail.info=Comerciar utilizando Pay by Mail en Haveno requiere que comprendas lo siguiente:\n\ \n\ ● El comprador de XMR debe empaquetar el efectivo en una bolsa de efectivo a prueba de manipulaciones.\n\ ● El comprador de XMR debe filmar o tomar fotos de alta resolución del proceso de empaquetado del efectivo con la dirección y el número de seguimiento ya colocados en el paquete.\n\ ● El comprador de XMR debe enviar el paquete de efectivo al vendedor de XMR con Confirmación de Entrega y el seguro adecuado.\n\ ● El vendedor de XMR debe filmar la apertura del paquete, asegurándose de que el número de seguimiento proporcionado por el remitente sea visible en el video.\n\ ● El creador de la oferta debe indicar cualquier término o condición especial en el campo 'Información Adicional' de la cuenta de pago.\n\ ● El tomador de la oferta acepta los términos y condiciones del creador de la oferta al aceptar la oferta.\n\ \n\ Las transacciones Pay by Mail colocan la responsabilidad de actuar honestamente directamente en ambos participantes.\n\ \n\ ● Las transacciones Pay by Mail tienen acciones menos verificables que otros intercambios tradicionales. Esto dificulta mucho más la gestión de disputas.\n\ ● Intenta resolver las disputas directamente con tu par utilizando el chat de trader. Esta es tu ruta más prometedora para resolver cualquier disputa de Pay by Mail.\n\ ● Los árbitros pueden considerar tu caso y hacer una sugerencia, pero NO están garantizados para ayudar.\n\ ● Los árbitros tomarán una decisión basada en la evidencia proporcionada. Por lo tanto, sigue y documenta los procesos anteriores para contar con evidencia en caso de disputa.\n\ ● Las solicitudes de reembolso por fondos perdidos como resultado de intercambios Pay By Mail en Haveno NO serán consideradas.\n\ \n\ Si no comprendes estos requisitos, no realices intercambios utilizando Pay by Mail en Haveno. payment.payByMail.contact=Información de contacto payment.payByMail.contact.prompt=El sobre con nombre o pseudónimo debería ser dirigido a payment.f2f.contact=Información de contacto payment.f2f.contact.prompt=Cómo le gustaría ser contactado por el par de intercambio? (dirección email, número de teléfono...) payment.f2f.city=Ciudad para la reunión 'cara a cara' payment.f2f.city.prompt=La ciudad se mostrará con la oferta payment.shared.optionalExtra=Información adicional opcional payment.shared.extraInfo=Información adicional payment.shared.extraInfo.offer=Información adicional de la oferta payment.shared.extraInfo.prompt.paymentAccount=Defina cualquier término especial, condiciones o detalles que quiera mostrar junto a sus ofertas para esta cuenta de pago (otros usuarios podrán ver esta información antes de aceptar las ofertas). payment.shared.extraInfo.prompt.offer=Defina cualquier término, condición o detalle especial que le gustaría mostrar con su oferta. payment.shared.extraInfo.noDeposit=Detalles de contacto y términos de la oferta payment.f2f.info=Los intercambios 'Cara a Cara' tienen diferentes reglas y riesgos que las transacciones en línea.\n\nLas principales diferencias son:\n● Los pares de intercambio necesitan intercambiar información acerca del punto de reunión y la hora usando los detalles de contacto proporcionados.\n● Los pares de intercambio tienen que traer sus portátiles y hacer la confirmación de 'pago enviado' y 'pago recibido' en el lugar de reunión.\n● Si un creador tiene 'términos y condiciones' especiales necesita declararlos en el campo de texto 'información adicional' en la cuenta.\n● Tomando una oferta el tomador está de acuerdo con los 'términos y condiciones' declarados por el creador.\n● En caso de disputa el árbitro no puede ayudar mucho ya que normalmente es complicado obtener evidencias no manipulables de lo que ha pasado en una reunión. En estos casos los fondos XMR pueden bloquearse indefinidamente o hasta que los pares lleguen a un acuerdo.\n\nPara asegurarse de que comprende las diferencias con los intercambios 'Cara a Cara' por favor lea las instrucciones y recomendaciones en: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=Abrir paǵina web payment.f2f.offerbook.tooltip.countryAndCity=País y ciudad: {0} / {1} payment.shared.extraInfo.tooltip=Información adicional: {0} payment.japan.bank=Banco payment.japan.branch=Branch payment.japan.account=Cuenta payment.japan.recipient=Nombre payment.australia.payid=PayID payment.payid=PayID conectado a una institución financiera. Como la dirección email o el número de móvil. payment.payid.info=Un PayID como un número de teléfono, dirección email o Australian Business Number (ABN), que puede conectar con seguridad a su banco, unión de crédito o cuenta de construcción de sociedad. Necesita haber creado una PayID con su institución financiera australiana. Tanto para enviar y recibir las instituciones financieras deben soportar PayID. Para más información por favor compruebe [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=Para pagar con Tarjeta eGift Amazon. necesitará enviar una Tarjeta eGift Amazon al vendedor XMR a través de su cuenta Amazon.\n\nHaveno mostrará la dirección e-mail del vendedor de XMR o el número de teléfono donde la tarjeta de regalo deberá enviarse. Por favor vea la wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Amazon_eGift_card] para más detalles y mejores prácticas.\n\nNotas importantes:\n- Pruebe a enviar las tarjetas regalo en cantidades de 100USD o menores, ya que Amazon está señalando tarjetas regalo mayores como fraudulentas.\n- Intente usar textos para el mensaje de la tarjeta regalo creíbles y creativos ("Feliz cumpleaños!").\n- Las tarjetas Amazon eGift pueden ser redimidas únicamente en la web de Amazon en la que se compraron (por ejemplo, una tarjeta comprada en amazon.it solo puede ser redimida en amazon.it) payment.paysafe.info=Por su protección, desaconsejamos encarecidamente el uso de PINs de Paysafecard para pagos.\n\n\ Las transacciones realizadas mediante PINs no pueden ser verificadas de forma independiente para la resolución de disputas. Si surge un problema, recuperar los fondos puede no ser posible.\n\n\ Para garantizar la seguridad de las transacciones con resolución de disputas, utilice siempre métodos de pago que proporcionen registros verificables. # We use constants from the code so we do not use our normal naming convention # dynamic values are not recognized by IntelliJ # Only translate general terms NATIONAL_BANK=Transferencia bancaria nacional SAME_BANK=Transferir con el mismo banco SPECIFIC_BANKS=Transferencias con bancos específicos US_POSTAL_MONEY_ORDER=Giro postal US Postal CASH_DEPOSIT=Depósito en efectivo PAY_BY_MAIL=Efectivo por Correo MONEY_GRAM=MoneyGram WESTERN_UNION=Western Union F2F=Cara a cara (en persona) JAPAN_BANK=Japan Bank Furikomi AUSTRALIA_PAYID=PayID australiano # suppress inspection "UnusedProperty" NATIONAL_BANK_SHORT=Bancos nacionales # suppress inspection "UnusedProperty" SAME_BANK_SHORT=Mismo banco # suppress inspection "UnusedProperty" SPECIFIC_BANKS_SHORT=Bancos específicos # suppress inspection "UnusedProperty" US_POSTAL_MONEY_ORDER_SHORT=Giro postal US # suppress inspection "UnusedProperty" CASH_DEPOSIT_SHORT=Depósito en efectivo # suppress inspection "UnusedProperty" PAY_BY_MAIL_SHORT=EfectivoPorCorreo # suppress inspection "UnusedProperty" MONEY_GRAM_SHORT=MoneyGram # suppress inspection "UnusedProperty" WESTERN_UNION_SHORT=Western Union # suppress inspection "UnusedProperty" F2F_SHORT=F2F # suppress inspection "UnusedProperty" JAPAN_BANK_SHORT=Japan Furikomi # suppress inspection "UnusedProperty" AUSTRALIA_PAYID_SHORT=PayID # Do not translate brand names # suppress inspection "UnusedProperty" UPHOLD=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY=Popmoney # suppress inspection "UnusedProperty" REVOLUT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY=WeChat Pay # suppress inspection "UnusedProperty" SEPA=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT=Pagos instantáneos SEPA # suppress inspection "UnusedProperty" FASTER_PAYMENTS=Faster Payments # suppress inspection "UnusedProperty" SWISH=Swish # suppress inspection "UnusedProperty" ZELLE=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS=Cryptos # suppress inspection "UnusedProperty" PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Tarjeta Amazon eGift # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT=Cryptos instant # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY=OKPay # suppress inspection "UnusedProperty" CASH_APP=Cash App # suppress inspection "UnusedProperty" VENMO=Venmo # suppress inspection "UnusedProperty" UPHOLD_SHORT=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM_SHORT=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY_SHORT=Popmoney # suppress inspection "UnusedProperty" REVOLUT_SHORT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY_SHORT=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY_SHORT=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY_SHORT=WeChat Pay # suppress inspection "UnusedProperty" SEPA_SHORT=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT_SHORT=SEPA Instant # suppress inspection "UnusedProperty" FASTER_PAYMENTS_SHORT=Faster Payments # suppress inspection "UnusedProperty" SWISH_SHORT=Swish # suppress inspection "UnusedProperty" ZELLE_SHORT=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY_SHORT=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER_SHORT=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH_SHORT=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS_SHORT=Cryptos # suppress inspection "UnusedProperty" PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Tarjeta Amazon eGift # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT_SHORT=Cryptos instant # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY_SHORT=OKPay # suppress inspection "UnusedProperty" CASH_APP_SHORT=Cash App # suppress inspection "UnusedProperty" VENMO_SHORT=Venmo #################################################################### # Validation #################################################################### validation.empty=No se permiten entradas vacías validation.NaN=El valor introducido no es válido validation.notAnInteger=El valor introducido no es entero validation.zero=El 0 no es un valor permitido. validation.negative=No se permiten entradas negativas. validation.traditional.tooSmall=No se permite introducir un valor menor que el mínimo posible validation.traditional.tooLarge=No se permiten entradas más grandes que la mayor posible. validation.xmr.fraction=El valor introducido resulta en un valor de monero menor a 1 satoshi validation.xmr.tooLarge=No se permiten valores mayores que {0}. validation.xmr.tooSmall=Valores menores que {0} no se permiten. validation.passwordTooShort=El password introducido es muy corto. Necesita tener al menos 8 caracteres. validation.passwordTooLong=La clave introducida es demasiado larga. Máximo 50 caracteres. validation.sortCodeNumber={0} debe consistir en {1} números. validation.sortCodeChars={0} debe consistir en {1} caracteres validation.bankIdNumber={0} debe consistir en {1} números. validation.accountNr=El número de cuenta debe consistir en {0} números. validation.accountNrChars=El número de cuenta debe consistir en {0} caracteres. validation.xmr.invalidAddress=La dirección no es correcta. Por favor compruebe el formato de la dirección. validation.integerOnly=Por favor, introduzca sólo números enteros. validation.inputError=Su entrada causó un error:\n{0} validation.xmr.exceedsMaxTradeLimit=Su límite de intercambio es {0}. validation.nationalAccountId={0} debe consistir de {1} número(s). #new validation.invalidInput=Entrada inválida: {0} validation.accountNrFormat=El número de cuenta debe ser del formato: {0} # suppress inspection "UnusedProperty" validation.crypto.wrongStructure=La validación de dirección falló porque no se corresponde con la estructura de una dirección {0}. # suppress inspection "UnusedProperty" validation.crypto.ltz.zAddressesNotSupported=Las drecciones LTZ deben empezar con L. Las direcciones que empiecen por z no están soportadas. # suppress inspection "UnusedProperty" validation.crypto.zAddressesNotSupported=Las direcciones ZEC deben empezar con t. Las direcciones empezando con z no están soportadas. # suppress inspection "UnusedProperty" validation.crypto.invalidAddress=La dirección no es una dirección {0} válida! {1} # suppress inspection "UnusedProperty" validation.crypto.liquidBitcoin.invalidAddress=Direcciones de segwit nativas (las que empiezan con 'lq') no son compatibles. validation.bic.invalidLength=La longitud del valor introducido debe ser 8 u 11. validation.bic.letters=El código de banco y país deben ser letras validation.bic.invalidLocationCode=BIC contiene un código de localización inválido validation.bic.invalidBranchCode=BIC contiene una sucursal inválida validation.bic.sepaRevolutBic=Cuentas Revolut Sepa no soportadas. validation.btc.invalidFormat=Formato inválido para una dirección Bitcoin. validation.email.invalidAddress=Dirección inválida validation.iban.invalidCountryCode=Código de país inválido validation.iban.checkSumNotNumeric=El checksum debe ser numérico validation.iban.nonNumericChars=Detectado carácter no alfanumérico validation.iban.checkSumInvalid=El checksum de IBAN es inválido validation.iban.invalidLength=El número debe tener una longitud de 15 a 34 caracteres. validation.interacETransfer.invalidAreaCode=Código de area no canadiense validation.interacETransfer.invalidPhone=Por favor introduzca un número de teléfono de 11 dígitos (p.ej. 1-123-456-7890) o una dirección email. validation.interacETransfer.invalidQuestion=Debe contener solamente letras, números, espacios y/o símbolos ' _ , . ? - validation.interacETransfer.invalidAnswer=Debe ser una palabra y contener solamente letras, números, y/o el símbolo - validation.inputTooLarge=El valor introducido no debe ser mayor que {0} validation.inputTooSmall=Lo introducido tiene que ser mayor que {0} validation.inputToBeAtLeast=El valor introducido tiene que ser al menos {0} validation.amountBelowDust=No se permite una cantidad por debajo del límite de polvo {0} validation.length=La longitud debe estar entre {0} y {1} validation.fixedLength=La longitud debe ser {0} validation.pattern=El valor introducido debe ser de formato: {0} validation.noHexString=El valor introducido no es un formato HEX. validation.advancedCash.invalidFormat=Tiene que ser una dirección de email o ID de cartera de formato: X000000000000 validation.invalidUrl=No es una URL válida validation.mustBeDifferent=Su entrada debe ser diferente del valor actual validation.cannotBeChanged=Este parámetro no se puede cambiar validation.numberFormatException=Excepción en formato de número {0} validation.mustNotBeNegative=El valor introducido no debe ser negativo validation.phone.missingCountryCode=Se necesitan dos letras de código de país para validar el número de teléfono validation.phone.invalidCharacters=Número de teléfono {0} tiene caracteres inválidos validation.phone.insufficientDigits=No hay suficientes dígitos en {0} para ser un número válido de teléfono validation.phone.tooManyDigits=Hay demasiados dígitos en {0} para ser un número de teléfono válido. validation.phone.invalidDialingCode=El código de país para el número {0} es inválido para el país {1}. El código de país correcto es {2}. validation.invalidAddressList=La lista de direcciones válidas debe ser separada por coma ================================================ FILE: core/src/main/resources/i18n/displayStrings_fa.properties ================================================ # Keep display strings organized by domain # Naming convention: We use camelCase and dot separated name spaces. # Use as many sub spaces as required to make the structure clear, but as little as possible. # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break # in the display but only in the editor. # Please use in all language files the exact same order of the entries, that way a comparison is easier. # Please try to keep the length of the translated string similar to English. If it is longer it might break layout or # get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. #################################################################### # Shared #################################################################### shared.readMore=بیشتر بخوانید shared.openHelp=بخش «راهنما» را باز کنید shared.warning=هشدار shared.close=بستن shared.cancel=لغو shared.ok=باشه shared.yes=بله shared.no=خیر shared.iUnderstand=فهمیدم shared.na=بدون پاسخ shared.shutDown=خاموش shared.reportBug=Report bug on GitHub shared.buyMonero=خرید بیتکوین shared.sellMonero=بیتکوین بفروشید shared.buyCurrency=خرید {0} shared.sellCurrency=فروش {0} shared.buyCurrency.locked=بخر {0} 🔒 shared.sellCurrency.locked=فروش {0} 🔒 shared.buyingXMRWith=خرید بیتکوین با {0} shared.sellingXMRFor=فروش بیتکوین با {0} shared.buyingCurrency=خرید {0} ( فروش بیتکوین) shared.sellingCurrency=فروش {0} (خرید بیتکوین) shared.buy=خرید shared.sell=فروش shared.buying=خریدن shared.selling=فروختن shared.P2P=P2P shared.oneOffer=پیشنهاد shared.multipleOffers=پیشنهادها shared.Offer=پیشنهاد shared.offerVolumeCode={0} Offer Volume shared.openOffers=پیشنهادهای باز shared.trade=معامله shared.trades=معاملات shared.openTrades=معاملات باز shared.dateTime=تاریخ/زمان shared.price=قیمت shared.priceWithCur=قیمت در {0} shared.priceInCurForCur=قیمت در {0} برای 1 {1} shared.fixedPriceInCurForCur=قیمت مقطوع در {0} برای 1 {1} shared.amount=مقدار shared.txFee=Transaction Fee shared.tradeFee=Trade Fee shared.buyerSecurityDeposit=Buyer Deposit shared.sellerSecurityDeposit=Seller Deposit shared.amountWithCur=مقدار در {0} shared.volumeWithCur=حجم در {0} shared.currency=ارز shared.market=بازار shared.deviation=Deviation shared.paymentMethod=نحوه پرداخت shared.tradeCurrency=ارز معامله shared.offerType=نوع پیشنهاد shared.details=جزئیات shared.address=آدرس shared.balanceWithCur=تراز در {0} shared.utxo=Unspent transaction output shared.txId=شناسه تراکنش shared.confirmations=تاییدیه‌ها shared.revert=بازگرداندن تراکنش shared.select=انتخاب shared.usage=کاربرد shared.state=وضعیت shared.tradeId=شناسه معامله shared.offerId=شناسه پیشنهاد shared.bankName=نام بانک shared.acceptedBanks=بانک‌های مورد پذیرش shared.amountMinMax=مقدار (حداقل - حداکثر) shared.amountHelp=اگر پیشنهادی دسته‌ی حداقل و حداکثر مقدار دارد، شما می توانید هر مقداری در محدوده پیشنهاد را معامله کنید. shared.remove=حذف shared.goTo=به {0} بروید shared.XMRMinMax=بیتکوین (حداقل - حداکثر) shared.removeOffer=حذف پیشنهاد shared.dontRemoveOffer=پیشنهاد را حذف نکنید shared.editOffer=ویرایش پیشنهاد shared.openLargeQRWindow=Open large QR code window shared.tradingAccount=حساب معاملات shared.faq=Visit FAQ page shared.yesCancel=بله، لغو شود shared.nextStep=گام بعدی shared.selectTradingAccount=حساب معاملات را انتخاب کنید shared.fundFromSavingsWalletButton=اعمال وجه از کیف پول هاونو shared.fundFromExternalWalletButton=برای تهیه پول، کیف پول بیرونی خود را باز کنید shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? shared.belowInPercent= ٪ زیر قیمت بازار shared.aboveInPercent= ٪ بالای قیمت بازار shared.enterPercentageValue=ارزش ٪ را وارد کنید shared.OR=یا shared.notEnoughFunds=You don't have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. shared.waitingForFunds=در انتظار دریافت وجه... shared.TheXMRBuyer=خریدار بیتکوین shared.You=شما shared.sendingConfirmation=در حال ارسال تاییدیه... shared.sendingConfirmationAgain=لطفاً تاییدیه را دوباره ارسال نمایید shared.exportCSV=Export to CSV shared.exportJSON=به JSON خروجی بگیر shared.summary=Show summary shared.noDateAvailable=تاریخ موجود نیست shared.noDetailsAvailable=جزئیاتی در دسترس نیست shared.notUsedYet=هنوز مورد استفاده قرار نگرفته shared.date=تاریخ shared.sendFundsDetailsWithFee=ارسال: {0}\n\nبه آدرس گیرنده: {1}\n\nهزینه اضافی ماینر: {2}\n\nآیا مطمئن هستید که می‌خواهید این مبلغ را ارسال کنید؟ # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=کپی در کلیپ‌بورد shared.language=زبان shared.country=کشور shared.applyAndShutDown=اعمال و خاموش کردن shared.selectPaymentMethod=انتخاب روش پرداخت shared.accountNameAlreadyUsed=That account name is already used for another saved account.\nPlease choose another name. shared.askConfirmDeleteAccount=از حذف حساب انتخاب شده مطمئن هستید؟ shared.cannotDeleteAccount=You cannot delete that account because it is being used in an open offer (or in an open trade). shared.noAccountsSetupYet=هنوز هیچ حساب کاربری تنظیم نشده است shared.manageAccounts=مدیریت حساب‌ها shared.addNewAccount=افزودن حساب جدید shared.ExportAccounts=صادر کردن حساب‌ها shared.importAccounts=وارد کردن حساب‌ها shared.createNewAccount=ایجاد حساب جدید shared.createNewAccountDescription=جزئیات حساب شما به‌طور محلی بر روی دستگاه شما ذخیره شده و تنها با هم‌تجارت شما و داور در صورت باز شدن یک اختلاف به اشتراک گذاشته می‌شود. shared.saveNewAccount=ذخیره‌ی حساب جدید shared.selectedAccount=حساب انتخاب شده shared.deleteAccount=حذف حساب shared.errorMessageInline=\nپیغام خطا: {0} shared.errorMessage=پیام خطا shared.information=اطلاعات shared.name=نام shared.id=شناسه shared.dashboard=داشبورد shared.accept=پذیرش shared.balance=موجودی shared.save=ذخیره shared.onionAddress=آدرس شبکه Onion shared.supportTicket=بلیط پشتیبانی shared.dispute=مناقشه shared.mediationCase=mediation case shared.seller=فروشنده shared.buyer=خریدار shared.allEuroCountries=تمام کشورهای یورو shared.acceptedTakerCountries=کشورهای هدف برای پذیرش طرف معامله shared.tradePrice=قیمت معامله shared.tradeAmount=مقدار معامله shared.tradeVolume=حجم معامله shared.invalidKey=کلید وارد شده صحیح نیست. shared.enterPrivKey=کلید خصوصی را برای بازگشایی وارد کنید shared.payoutTxId=شناسه تراکنش پرداخت shared.contractAsJson=قرارداد در قالب JSON shared.viewContractAsJson=مشاهده‌ی قرارداد در قالب JSON: shared.contract.title=قرارداد برای معامله با شناسه ی {0} shared.paymentDetails=جزئیات پرداخت XMR {0} shared.securityDeposit=سپرده‌ی اطمینان shared.yourSecurityDeposit=سپرده ی اطمینان شما shared.contract=قرارداد shared.messageArrived=پیام رسید. shared.messageStoredInMailbox=پیام در پیام‌های دریافتی ذخیره شد. shared.messageSendingFailed=ارسال پیام ناموفق بود. خطا: {0} shared.unlock=باز کردن shared.toReceive=قابل دریافت shared.toSpend=قابل خرج کردن shared.xmrAmount=مقدار بیتکوین shared.yourLanguage=زبان‌های شما shared.addLanguage=افزودن زبان shared.total=مجموع shared.totalsNeeded=وجه مورد نیاز shared.tradeWalletAddress=آدرس کیف‌پول معاملات shared.tradeWalletBalance=موجودی کیف‌پول معاملات shared.reserveExactAmount=فقط وجوه مورد نیاز را رزرو کنید. نیاز به هزینه استخراج و حدود ۲۰ دقیقه زمان قبل از فعال شدن پیشنهاد شما دارد. shared.makerTxFee=سفارش گذار: {0} shared.takerTxFee=پذیرنده سفارش: {0} shared.iConfirm=تایید می‌کنم shared.openURL=باز {0} shared.fiat=فیات shared.crypto=کریپتو shared.preciousMetals=فلزات گرانبها shared.all=همه shared.edit=ویرایش shared.advancedOptions=گزینه‌های پیشرفته shared.interval=دوره shared.actions=عملیات shared.buyerUpperCase=خریدار shared.sellerUpperCase=فروشنده shared.new=جدید shared.learnMore=بیشتر بدانید shared.dismiss=رد کردن shared.selectedArbitrator=داور انتخاب شده shared.selectedMediator=Selected mediator shared.selectedRefundAgent=داور انتخاب شده shared.mediator=واسط shared.arbitrator=داور shared.refundAgent=داور shared.refundAgentForSupportStaff=Refund agent shared.delayedPayoutTxId=Delayed payout transaction ID shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later. shared.numItemsLabel=Number of entries: {0} shared.filter=فیلتر shared.enabled=Enabled #################################################################### # UI views #################################################################### #################################################################### # MainView #################################################################### mainView.menu.market=بازار mainView.menu.buyXmr=خرید بیتکوین mainView.menu.sellXmr=فروش بیتکوین mainView.menu.portfolio=سبد سرمایه mainView.menu.funds=وجوه mainView.menu.support=پشتیبانی mainView.menu.settings=تنظیمات mainView.menu.account=حساب mainView.marketPriceWithProvider.label=قیمت بازار بر اساس {0} mainView.marketPrice.havenoInternalPrice=قیمت آخرین معامله‌ی Haveno mainView.marketPrice.tooltip.havenoInternalPrice=قیمت بازارهای خارجی موجود نیست.\nقیمت نمایش داده شده، از آخرین معامله‌ی Haveno برای ارز موردنظر اتخاذ شده است. mainView.marketPrice.tooltip=قیمت بازار توسط {0}{1} ارائه شده است\nآخرین به روز رسانی: {2}\nURL لینک Node ارائه دهنده: {3} mainView.balance.available=موجودی در دسترس mainView.balance.reserved=رزرو شده در پیشنهادها mainView.balance.pending=قفل شده در معاملات mainView.balance.reserved.short=اندوخته mainView.balance.pending.short=قفل شده mainView.footer.usingTor=(via Tor) mainView.footer.localhostMoneroNode=(لوکال هاست) mainView.footer.clearnet=(via clearnet) mainView.footer.xmrInfo={0} {1} mainView.footer.xmrFeeRate=/ Fee rate: {0} sat/vB mainView.footer.xmrInfo.initializing=اتصال به شبکه Haveno mainView.footer.xmrInfo.synchronizingWith=همگام‌سازی با {0} در بلوک: {1} / {2} mainView.footer.xmrInfo.connectedTo=متصل شده به {0} در بلوک {1} mainView.footer.xmrInfo.synchronizingWalletWith=همگام‌سازی کیف‌پول با {0} در بلوک: {1} / {2} mainView.footer.xmrInfo.syncedWith=همگام‌سازی با {0} در بلوک {1} انجام شده است mainView.footer.xmrInfo.connectingTo=در حال ایجاد ارتباط با mainView.footer.xmrInfo.connectionFailed=Connection failed to mainView.footer.xmrPeers=Monero network peers: {0} mainView.footer.p2pPeers=Haveno network peers: {0} mainView.bootstrapState.connectionToTorNetwork=(1/4) در حال ارتباط با شبکه Tor ... mainView.bootstrapState.torNodeCreated=(2/4) گره Tor ایجاد شد mainView.bootstrapState.hiddenServicePublished=(3/4) سرویس پنهان منتشر شد mainView.bootstrapState.initialDataReceived=(4/4) داده های اولیه دریافت شد mainView.bootstrapWarning.noSeedNodesAvailable=عدم وجود Node های اولیه mainView.bootstrapWarning.noNodesAvailable=Node ها و همتایان اولیه موجود نیستند mainView.bootstrapWarning.bootstrappingToP2PFailed=Bootstrapping to Haveno network failed mainView.p2pNetworkWarnMsg.noNodesAvailable=Nodeی برای درخواست داده موجود نیست.\nلطفاً ارتباط اینترنت خود را بررسی کنید یا برنامه را مجدداً راه اندازی کنید. mainView.p2pNetworkWarnMsg.connectionToP2PFailed=Connecting to the Haveno network failed (reported error: {0}).\nPlease check your internet connection or try to restart the application. mainView.walletServiceErrorMsg.timeout=ارتباط با شبکه‌ی بیتکوین به دلیل وقفه، ناموفق بود. mainView.walletServiceErrorMsg.connectionError=ارتباط با شبکه‌ی بیتکوین به دلیل یک خطا: {0}، ناموفق بود. mainView.walletServiceErrorMsg.rejectedTxException=A transaction was rejected from the network.\n\n{0} mainView.networkWarning.allConnectionsLost=اتصال شما به تمام {0} همتایان شبکه قطع شد.\nشاید ارتباط کامپیوتر شما قطع شده است یا کامپیوتر در حالت Standby است. mainView.networkWarning.localhostMoneroLost=اتصال شما به Node لوکال هاست بیتکوین قطع شد.\nلطفاً به منظور اتصال به سایر Nodeهای بیتکوین، برنامه‌ی Haveno یا Node لوکال هاست بیتکوین را مجددا راه اندازی کنید. mainView.version.update=(به روز رسانی موجود است) #################################################################### # MarketView #################################################################### market.tabs.offerBook=دفتر پیشنهادها market.tabs.spreadCurrency=Offers by Currency market.tabs.spreadPayment=Offers by Payment Method market.tabs.trades=معاملات # OfferBookChartView market.offerBook.buyCrypto=خرید {0} (فروش {1}) market.offerBook.sellCrypto=فروش {1} (خرید {0}) market.offerBook.buyWithTraditional=خرید {0} market.offerBook.sellWithTraditional=فروش {0} market.offerBook.sellOffersHeaderLabel=فروش {0} به market.offerBook.buyOffersHeaderLabel=خرید {0} از market.offerBook.buy=می‌خواهم بیتکوین بخرم. market.offerBook.sell=می‌خواهم بیتکوین بفروشم. # SpreadView market.spread.numberOfOffersColumn=تمام پیشنهادها ({0}) market.spread.numberOfBuyOffersColumn=خرید بیتکوین ({0}) market.spread.numberOfSellOffersColumn=فروش بیتکوین ({0}) market.spread.totalAmountColumn=مجموع بیتکوین ({0}) market.spread.spreadColumn=تفاوت نرخ market.spread.expanded=Expanded view # TradesChartsView market.trades.nrOfTrades=معاملات: {0} market.trades.tooltip.volumeBar=Volume: {0} / {1}\nNo. of trades: {2}\nDate: {3} market.trades.tooltip.candle.open=باز: market.trades.tooltip.candle.close=بسته: market.trades.tooltip.candle.high=بالا: market.trades.tooltip.candle.low=پایین: market.trades.tooltip.candle.average=میانگین: market.trades.tooltip.candle.median=Median: market.trades.tooltip.candle.date=تاریخ: market.trades.showVolumeInUSD=Show volume in USD #################################################################### # OfferView #################################################################### offerbook.createOffer=ایجاد پیشنهاد offerbook.takeOffer=برداشتن پیشنهاد offerbook.takeOfferToBuy=پیشنهاد خرید {0} را بردار offerbook.takeOfferToSell=پیشنهاد فروش {0} را بردار offerbook.takeOffer.enterChallenge=عبارت عبور پیشنهاد را وارد کنید offerbook.trader=معامله‌گر offerbook.offerersBankId=شناسه بانک سفارش‌گذار (BIC/SWIFT): {0} offerbook.offerersBankName= نام بانک سفارش‌گذار : {0} offerbook.offerersBankSeat=کشور بانک سفارش‌گذار: {0} offerbook.offerersAcceptedBankSeatsEuro=بانک‌های کشورهای پذیرفته شده (پذیرنده): تمام کشورهای یورو offerbook.offerersAcceptedBankSeats=بانک‌های کشورهای پذیرفته شده (پذیرنده): \n{0} offerbook.availableOffers=پیشنهادهای موجود offerbook.filterByCurrency=فیلتر بر اساس ارز offerbook.filterByPaymentMethod=فیلتر بر اساس روش پرداخت offerbook.matchingOffers=پیشنهادات متناسب با حساب‌های من offerbook.filterNoDeposit=هیچ سپرده‌ای offerbook.noDepositOffers=پیشنهادهایی بدون ودیعه (نیاز به عبارت عبور) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info=This account was verified and {0} offerbook.timeSinceSigning.info.arbitrator=signed by an arbitrator and can sign peer accounts offerbook.timeSinceSigning.info.peer=signed by a peer, waiting %d days for limits to be lifted offerbook.timeSinceSigning.info.peerLimitLifted=signed by a peer and limits were lifted offerbook.timeSinceSigning.info.signer=signed by peer and can sign peer accounts (limits lifted) offerbook.timeSinceSigning.info.banned=account was banned offerbook.timeSinceSigning.daysSinceSigning={0} روز offerbook.timeSinceSigning.daysSinceSigning.long={0} since signing offerbook.xmrAutoConf=Is auto-confirm enabled offerbook.buyXmrWith=با XMR خرید کنید: offerbook.sellXmrFor=فروش XMR برای: offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers' payment accounts. offerbook.timeSinceSigning.notSigned=Not signed yet offerbook.timeSinceSigning.notSigned.ageDays={0} روز offerbook.timeSinceSigning.notSigned.noNeed=بدون پاسخ shared.notSigned=This account has not been signed yet and was created {0} days ago shared.notSigned.noNeed=This account type does not require signing shared.notSigned.noNeedDays=This account type does not require signing and was created {0} days ago shared.notSigned.noNeedAlts=Crypto accounts do not feature signing or aging offerbook.nrOffers=تعداد پیشنهادها: {0} offerbook.volume={0} (حداقل - حداکثر) offerbook.deposit=Deposit XMR (%) offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed. offerbook.createNewOffer=پیشنهاد ایجاد کنید به {0} {1} offerbook.createOfferToBuy=پیشنهاد جدید برای خرید {0} ایجاد کن offerbook.createOfferToSell=پیشنهاد جدید برای فروش {0} ایجاد کن offerbook.createOfferToBuy.withTraditional=پیشنهاد جدید برای خرید {0} با {1} ایجاد کن offerbook.createOfferToSell.forTraditional=پیشنهاد جدید برای فروش {0} به ازای {1} ایجاد کن offerbook.createOfferToBuy.withCrypto=پیشنهاد جدید برای فروش {0} (خرید {1}) offerbook.createOfferToSell.forCrypto=پیشنهاد جدید برای خرید {0} (فروش {1}) offerbook.takeOfferButton.tooltip=پیشنهاد را برای {0} بردار offerbook.yesCreateOffer=بلی، ایجاد پیشنهاد offerbook.setupNewAccount=تنظیم یک حساب معاملات جدید offerbook.removeOffer.success=حذف پیشنهاد موفقیت آمیز بود. offerbook.removeOffer.failed=حذف پیشنهاد ناموفق بود:\n{0} offerbook.deactivateOffer.failed=غیرفعالسازی پیشنهاد ناموفق بود:\n{0} offerbook.activateOffer.failed=انتشار پیشنهاد ناموفق بود:\n{0} offerbook.withdrawFundsHint=می‌توانید مبلغی را که از صفحه {0} پرداخت کرده اید، بردارید. offerbook.warning.noTradingAccountForCurrency.headline=No payment account for selected currency offerbook.warning.noTradingAccountForCurrency.msg=You don't have a payment account set up for the selected currency.\n\nWould you like to create an offer for another currency instead? offerbook.warning.noMatchingAccount.headline=No matching payment account. offerbook.warning.noMatchingAccount.msg=This offer uses a payment method you haven't set up yet. \n\nWould you like to set up a new payment account now? offerbook.warning.counterpartyTradeRestrictions=This offer cannot be taken due to counterparty trade restrictions offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\nAfter successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\nFor more information on account signing, please see the documentation at [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- The buyer's account has not been signed by an arbitrator or a peer\n- The time since signing of the buyer's account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- Your account has not been signed by an arbitrator or a peer\n- The time since signing of your account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=این روش پرداخت موقتاً تا {1} به {0} محدود شده است زیرا همه خریداران حساب‌های جدیدی دارند.\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=پیشنهاد شما تنها مختص خریدارانی خواهد بود که حساب‌هایی با امضا و سنین پیر دارند زیرا این مبلغ {0} را بیشتر می‌کند.\n\n{1} offerbook.warning.wrongTradeProtocol=این پیشنهاد نیاز به نسخه پروتکل متفاوتی مانند پروتکل نسخه نرم‌افزار خودتان دارد.\n\nلطفا پس از نصب آخرین آپدیت نرم‌افزار دوباره تلاش کنید. در غیر این صورت، کاربری که این پیشنهاد را ایجاد کرده است، از نسخه‌ای قدیمی‌تر استفاده می‌کند.\n\nکاربران نمی توانند با نسخه‌های پروتکل معاملاتی ناسازگار، معامله کنند. offerbook.warning.userIgnored=شما آدرس onion کاربر را به لیست بی‌اعتنایی خودتان افزوده‌اید. offerbook.warning.offerBlocked=پیشنهاد توسط توسعه دهندگان Haveno مسدود شد.\nاحتمالاً هنگام گرفتن پیشنهاد، یک اشکال خارج از کنترل موجب پدید آمدن مشکلاتی شده است. offerbook.warning.currencyBanned=ارز مورد استفاده در آن پیشنهاد، توسط توسعه‌دهندگان Haveno مسدود شد.\nبرای اطلاعات بیشتر، لطفاً از انجمن Haveno بازدید نمایید. offerbook.warning.paymentMethodBanned=روش پرداخت مورد استفاده در آن پیشنهاد، توسط توسعه دهندگان Haveno مسدود شد.\nلطفاً برای اطلاعات بیشتر، از انجمن Haveno بازدید نمایید. offerbook.warning.nodeBlocked=آدرس onion آن معامله گر، توسط توسعه دهندگان Haveno مسدود شد.\nاحتمالاً هنگام گرفتن پیشنهاد از جانب آن معامله گر، یک اشکال ناامن موجب پدید آمدن مسائلی شده است. offerbook.warning.offerWasAlreadyUsedInTrade=You cannot take this offer because you already took it earlier. It could be that your previous take-offer attempt resulted in a failed trade. offerbook.info.sellAtMarketPrice=با قیمت روز بازار خواهید فروخت (به روز رسانی در هر دقیقه). offerbook.info.buyAtMarketPrice=با قیمت روز بازار خرید خواهید کرد (به روز رسانی در هر دقیقه). offerbook.info.sellBelowMarketPrice={0} کمتر از قیمت روز فعلی بازار، دریافت خواهید کرد (به روز رسانی در هر دقیقه). offerbook.info.buyAboveMarketPrice={0} بیشتر از قیمت روز فعلی بازار، پرداخت خواهید کرد (به روز رسانی در هر دقیقه). offerbook.info.sellAboveMarketPrice={0} بیشتر از قیمت روز فعلی بازار، دریافت خواهید کرد (به روز رسانی در هر دقیقه). offerbook.info.buyBelowMarketPrice={0} کمتر از قیمت روز فعلی بازار، پرداخت خواهید کرد (به روز رسانی در هر دقیقه). offerbook.info.buyAtFixedPrice=با این قیمت مقطوع خرید خواهید کرد. offerbook.info.sellAtFixedPrice=با این قیمت مقطوع، خواهید فروخت. offerbook.info.noArbitrationInUserLanguage=در صورت اختلاف، لطفا توجه داشته باشید که داوری برای این پیشنهاد در {0} مدیریت خواهد شد. زبان در حال حاضر {1} تنظیم شده است. offerbook.info.roundedFiatVolume=مقدار برای حفظ حریم خصوصی شما گرد شده است. #################################################################### # Offerbook / Create offer #################################################################### createOffer.amount.prompt=مقدار را به بیتکوین وارد کنید. createOffer.price.prompt=قیمت را وارد کنید createOffer.volume.prompt=مقدار را در {0} وارد کنید createOffer.amountPriceBox.amountDescription=مقدار XMR برای {0} createOffer.amountPriceBox.buy.volumeDescription=مقدار در {0} به منظور خرج کردن createOffer.amountPriceBox.sell.volumeDescription=مقدار در {0} به منظور دریافت نمودن createOffer.amountPriceBox.minAmountDescription=حداقل مقدار بیتکوین createOffer.securityDeposit.prompt=سپرده‌ی اطمینان createOffer.fundsBox.title=پیشنهاد خود را تامین وجه نمایید createOffer.fundsBox.offerFee=کارمزد معامله createOffer.fundsBox.networkFee=کارمزد استخراج createOffer.fundsBox.placeOfferSpinnerInfo=انتشار پیشنهاد در حال انجام است ... createOffer.fundsBox.paymentLabel=معامله Haveno با شناسه‌ی {0} createOffer.fundsBox.fundsStructure=({0} سپرده‌ی اطمینان، {1} کارمزد معامله، {2} کارمزد تراکنش) createOffer.success.headline=پیشنهاد شما، ایجاد شده است. createOffer.success.info=شما می توانید پیشنهادهای باز خود را در \"سبد سهام/پیشنهادهای باز من\" مدیریت نمایید. createOffer.info.sellAtMarketPrice=شما همیشه به نرخ روز بازار خواهید فروخت، زیرا قیمت پیشنهادتان به طور مداوم به روزرسانی خواهد شد. createOffer.info.buyAtMarketPrice=شما همیشه به نرخ روز بازار خرید خواهید کرد، زیرا قیمت پیشنهادتان به طور مداوم به روزرسانی خواهد شد. createOffer.info.sellAboveMarketPrice=شما همیشه {0}% بیشتر از نرخ روز فعلی بازار دریافت خواهید کرد، زیرا قیمت پیشنهادتان به طور مداوم به روز رسانی خواهد شد. createOffer.info.buyBelowMarketPrice=شما همیشه {0}% کمتر از نرخ روز فعلی بازار پرداخت خواهید کرد، زیرا قیمت پیشنهادتان به طور مداوم به روز رسانی خواهد شد. createOffer.warning.sellBelowMarketPrice=شما همیشه {0}% کمتر از نرخ روز فعلی بازار دریافت خواهید کرد، زیرا قیمت پیشنهادتان به طور مداوم به روز رسانی خواهد شد. createOffer.warning.buyAboveMarketPrice=شما همیشه {0}% کمتر از نرخ روز فعلی بازار پرداخت خواهید کرد، زیرا قیمت پیشنهادتان به طور مداوم به روز رسانی خواهد شد. createOffer.tradeFee.descriptionXMROnly=کارمزد معامله createOffer.tradeFee.descriptionBSQEnabled=انتخاب ارز برای کارمزد معامله createOffer.triggerPrice.prompt=Set optional trigger price createOffer.triggerPrice.label=Deactivate offer if market price is {0} createOffer.triggerPrice.tooltip=As protection against drastic price movements you can set a trigger price which deactivates the offer if the market price reaches that value. createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0} createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0} # new entries createOffer.placeOfferButton.buy=بررسی: ایجاد پیشنهاد خرید XMR با {0} createOffer.placeOfferButton.sell=بررسی: ایجاد پیشنهاد فروش XMR به ازای {0} createOffer.createOfferFundWalletInfo.headline=پیشنهاد خود را تامین وجه نمایید # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=مقدار معامله:{0}\n createOffer.createOfferFundWalletInfo.msg=شما باید {0} را برای این پیشنهاد واریز کنید.\n\n\ این وجوه در کیف پول محلی شما رزرو می‌شوند و هنگامی که کسی پیشنهاد شما را قبول کند، به یک کیف پول مولتی‌سیگ قفل خواهند شد.\n\n\ مقدار این مبلغ مجموع موارد زیر است:\n\ {1}\ - ودیعه امنیتی شما: {2}\n\ - هزینه معامله: {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=یک خطا هنگام قرار دادن پیشنهاد، رخ داده است:\n\n{0}\n\nهیچ پولی تاکنون از کیف پول شما کم نشده است.\nلطفاً برنامه را مجدداً راه اندازی کرده و ارتباط اینترنت خود را بررسی نمایید. createOffer.setAmountPrice=تنظیم مقدار و قیمت createOffer.warnCancelOffer=شما در حال حاضر، آن پیشنهاد را تامین وجه کرده‌اید.\nاگر اکنون لغو کنید، وجوه شما در کیف پول محلی Haveno باقی خواهد ماند و در صفحه ی "وجوه/ارسال وجوه" برای برداشت در دسترس خواهد بود. آیا مطمئن هستید که می‌خواهید لغو کنید؟ createOffer.timeoutAtPublishing=یک وقفه در انتشار پیشنهاد، رخ داده است. createOffer.errorInfo=\n\nهزینه سفارش گذار، از قبل پرداخت شده است. در بدترین حالت، شما آن هزینه را از دست داده‌اید.\nلطفاً سعی کنید برنامه را مجدداً راه اندازی کرده و ارتباط اینترنت خود را بررسی کنند تا تا ببینید آیا می‌توانید این مشکل را حل کنید یا خیر. createOffer.tooLowSecDeposit.warning=شما سپرده‌های اطمینان را با مقداری کمتر از مقدار پیش‌فرض {0} تنظیم کرده‌‍اید.\nآیا شما مطمئن هستید که می‌خواهید از یک سپرده اطمینان کمتر استفاده کنید؟ createOffer.tooLowSecDeposit.makerIsSeller=در صورتی که همتای معاملاتی، از پروتکل معامله پیروی نکند، این کار محافظت کمتری برای شما دارد. createOffer.tooLowSecDeposit.makerIsBuyer=از آنجا که شما سپرده کمتری برای ضمانت دارید، طرف معامله شما اطمینان کمی به این معامله دارد.\nمعامله‌گران ممکن است ترجیح دهند به جای پیشنهادهای شما، پیشنهادهای دیگر را بپذیرند. createOffer.resetToDefault=خیر، راه اندازی مجدد برای ارزش پیشفرض createOffer.useLowerValue=بلی، استفاده از ارزش پایین تر من createOffer.priceOutSideOfDeviation=قیمتی که شما وارد کرده اید، بیشتر از حداکثر انحراف مجاز از قیمت روز بازار است.\nحداکثر انحراف مجاز، {0} است و می تواند در اولویت ها، تنظیم شود. createOffer.changePrice=تغییر قیمت createOffer.tac=با انتشار این پیشنهاد، می‌پذیرم که با هر معامله گری که شرایط تعیین شده در این صفحه را دارا می‌باشد، معامله کنم. createOffer.currencyForFee=هزینه‌ی معامله createOffer.setDeposit=تنظیم سپرده‌ی اطمینان خریدار (%) createOffer.setDepositAsBuyer=تنظیم سپرده‌ی اطمینان من به عنوان خریدار (%) createOffer.setDepositForBothTraders=Set both traders' security deposit (%) createOffer.securityDepositInfo=سپرده‌ی اطمینان خریدار شما {0} خواهد بود createOffer.securityDepositInfoAsBuyer=سپرده‌ی اطمینان شما به عنوان خریدار {0} خواهد بود createOffer.minSecurityDepositUsed=حداقل سپرده امنیتی استفاده می‌شود createOffer.buyerAsTakerWithoutDeposit=هیچ سپرده‌ای از خریدار مورد نیاز نیست (محافظت شده با پس‌عبارت) createOffer.myDeposit=سپرده امنیتی من (%) createOffer.myDepositInfo=ودیعه امنیتی شما {0} خواهد بود #################################################################### # Offerbook / Take offer #################################################################### takeOffer.amount.prompt=مقدار را به بیتکوین وارد کنید. takeOffer.amountPriceBox.buy.amountDescription=مقدار بیتکوین به منظور فروش takeOffer.amountPriceBox.sell.amountDescription=مقدار بیتکوین به منظور خرید takeOffer.amountPriceBox.priceDescription=قیمت به ازای هر بیتکوین در {0} takeOffer.amountPriceBox.amountRangeDescription=محدوده‌ی مقدار ممکن takeOffer.amountPriceBox.warning.invalidXmrDecimalPlaces=مقداری که شما وارد کرده‌اید، از تعداد عددهای اعشاری مجاز فراتر رفته است.\nمقدار به 4 عدد اعشاری تنظیم شده است. takeOffer.validation.amountSmallerThanMinAmount=مقدار نمی‌تواند کوچکتر از حداقل مقدار تعیین شده در پیشنهاد باشد. takeOffer.validation.amountLargerThanOfferAmount=مقدار ورودی نمی‌تواند بالاتر از مقدار تعیین شده در پیشنهاد باشد. takeOffer.validation.amountLargerThanOfferAmountMinusFee=مقدار ورودی، باعث ایجاد تغییر جزئی برای فروشنده بیتکوین می شود. takeOffer.fundsBox.title=معامله خود را تأمین وجه نمایید takeOffer.fundsBox.isOfferAvailable=بررسی کنید آیا پیشنهاد در دسترس است... takeOffer.fundsBox.tradeAmount=مبلغ فروش takeOffer.fundsBox.offerFee=کارمزد معامله takeOffer.fundsBox.networkFee=کل کارمزد استخراج takeOffer.fundsBox.takeOfferSpinnerInfo=پذیرفتن پیشنهاد: {0} takeOffer.fundsBox.paymentLabel=معامله Haveno با شناسه‌ی {0} takeOffer.fundsBox.fundsStructure=({0} سپرده‌ی اطمینان، {1} هزینه‌ی معامله، {2} هزینه تراکنش شبکه) takeOffer.fundsBox.noFundingRequiredTitle=نیاز به تأمین مالی نیست takeOffer.fundsBox.noFundingRequiredDescription=برای پذیرش این پیشنهاد، رمزعبور آن را از فروشنده خارج از هاونئو دریافت کنید. takeOffer.success.headline=با موفقیت یک پیشنهاد را قبول کرده‌اید. takeOffer.success.info=شما می‌توانید وضعیت معامله‌ی خود را در \"سبد سهام /معاملات باز\" ببینید. takeOffer.error.message=هنگام قبول کردن پیشنهاد، اتفاقی رخ داده است.\n\n{0} # new entries takeOffer.takeOfferButton.buy=بررسی: قبول پیشنهاد خرید XMR با {0} takeOffer.takeOfferButton.sell=بررسی: قبول پیشنهاد فروش XMR به ازای {0} takeOffer.noPriceFeedAvailable=امکان پذیرفتن پیشنهاد وجود ندارد. پیشنهاد از قیمت درصدی مبتنی بر قیمت روز بازار استفاده می‌کند و قیمت‌های بازار هم‌اکنون در دسترس نیست. takeOffer.takeOfferFundWalletInfo.headline=معامله خود را تأمین وجه نمایید # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=مقدار معامله: {0}\n takeOffer.takeOfferFundWalletInfo.msg=باید {0} را برای پذیرش این پیشنهاد واریز کنید.\n\nمبلغ مجموع موارد زیر است:\n{1}- سپرده امنیتی شما: {2}\n- هزینه معامله: {3} takeOffer.alreadyPaidInFunds=اگر شما در حال حاضر در وجوه، پرداختی داشته اید، می توانید آن را در صفحه ی \"وجوه/ارسال وجوه\" برداشت کنید. takeOffer.paymentInfo=اطلاعات پرداخت takeOffer.setAmountPrice=تنظیم مقدار takeOffer.alreadyFunded.askCancel=شما در حال حاضر، آن پیشنهاد را تامین وجه کرده‌اید.\nاگر اکنون لغو کنید، وجوه شما در کیف پول محلی Haveno باقی خواهد ماند و در صفحه ی "وجوه/ارسال وجوه" برای برداشت در دسترس خواهد بود. آیا مطمئن هستید که می‌خواهید لغو کنید؟ takeOffer.failed.offerNotAvailable=درخواست پذیرفتن پیشنهاد ناموفق بود، چون پیشنهاد دیگر در دسترس نیست. شاید معامله‌گر دیگری همزمان پیشنهاد را برداشته است. takeOffer.failed.offerTaken=شما نمی توانید آن پیشنهاد را بپذیرید، چون قبلاً توسط معامله‌گر دیگری پذیرفته شده است. takeOffer.failed.offerRemoved=شما نمی‌توانید آن پیشنهاد را بپذیرید، چون پیشنهاد در این فاصله حذف شده است. takeOffer.failed.offererNotOnline=درخواست پذیرش پیشنهاد ناموفق بود، زیرا سفارش گذار دیگر آنلاین نیست. takeOffer.failed.offererOffline=شما نمی‌توانید آن پیشنهاد را بپذیرید، زیرا سفارش گذار آفلاین است. takeOffer.warning.connectionToPeerLost=You lost connection to the maker.\nThey might have gone offline or has closed the connection to you because of too many open connections.\n\nIf you can still see their offer in the offerbook you can try to take the offer again. takeOffer.error.noFundsLost=\n\nهیچ پولی تاکنون از کیف پول شما کم نشده است.\nلطفاً برنامه را مجدداً راه اندازی کرده و ارتباط اینترنت خود را بررسی نمایید تا ببینید آیا می‌توانید مشکل را حل کنید یا خیر. # suppress inspection "TrailingSpacesInProperty" takeOffer.error.feePaid=\n\n takeOffer.error.depositPublished=\n\nتراکنش سپرده از قبل منتشر شده است.\nلطفاً سعی کنید تا برنامه را مجدداً راه اندازی کرده و ارتباط اینترنت خود را بررسی کنند تا ببینید آیا می‌توانید این مشکل را حل کنید یا خیر.\nاگر مشکل همچنان پابرجا است، لطفاً برای پشتیبانی با توسعه دهندگان تماس بگیرید. takeOffer.error.payoutPublished=\n\nتراکنش پرداخت از قبل منتشر شده است.\nلطفاً سعی کنید تا برنامه را مجدداً راه اندازی کرده و ارتباط اینترنت خود را بررسی کنند تا تا ببینید آیا می‌توانید این مشکل را حل کنید یا خیر.\nاگر مشکل همچنان پابرجا است، لطفاً برای پشتیبانی با توسعه دهندگان تماس بگیرید. takeOffer.tac=با پذیرفتن این پیشنهاد، من قبول می‌کنم تا با شرایط تعیین شده در این صفحه معامله کنم. #################################################################### # Offerbook / Edit offer #################################################################### openOffer.header.triggerPrice=قیمت نشان‌شده openOffer.triggerPrice=Trigger price {0} openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\nPlease edit the offer to define a new trigger price editOffer.setPrice=تنظیم قیمت editOffer.confirmEdit=تأیید: ویرایش پیشنهاد editOffer.publishOffer=انتشار پیشنهاد شما. editOffer.failed=ویرایش پیشنهاد، ناموفق بود:\n{0} editOffer.success=پیشنهاد شما با موفقیت ویرایش شد. editOffer.invalidDeposit=The buyer's security deposit is not within the constraints defined by Haveno and can no longer be edited. #################################################################### # Portfolio #################################################################### portfolio.tab.openOffers=معاملات باز من portfolio.tab.pendingTrades=معاملات باز portfolio.tab.history=تاریخچه portfolio.tab.failed=ناموفق portfolio.tab.editOpenOffer=ویرایش پیشنهاد portfolio.closedTrades.deviation.help=Percentage price deviation from market portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} portfolio.pending.syncing=در حال همگام‌سازی کیف پول معامله portfolio.pending.syncing.blockRemaining=در حال همگام‌سازی کیف پول معامله — ۱ بلاک باقی‌مانده portfolio.pending.syncing.blocksRemaining=در حال همگام‌سازی کیف پول معامله — {0} بلاک باقی‌مانده portfolio.pending.step1.waitForConf=برای تأییدیه بلاک چین منتظر باشید portfolio.pending.step2_buyer.additionalConf=واریزها به ۱۰ تأیید رسیده‌اند.\nبرای امنیت بیشتر، توصیه می‌کنیم قبل از ارسال پرداخت، {0} تأیید صبر کنید.\nاقدام زودهنگام با مسئولیت خودتان است. portfolio.pending.step2_buyer.startPayment=آغاز پرداخت portfolio.pending.step2_seller.waitPaymentSent=صبر کنید تا پرداخت شروع شود portfolio.pending.step3_buyer.waitPaymentArrived=صبر کنید تا پرداخت حاصل شود portfolio.pending.step3_seller.confirmPaymentReceived=تأیید رسید پرداخت portfolio.pending.step5.completed=تکمیل شده portfolio.pending.step3_seller.autoConf.status.label=Auto-confirm status portfolio.pending.autoConf=Auto-confirmed portfolio.pending.autoConf.blocks=XMR confirmations: {0} / Required: {1} portfolio.pending.autoConf.state.xmr.txKeyReused=Transaction key re-used. Please open a dispute. portfolio.pending.autoConf.state.confirmations=XMR confirmations: {0}/{1} portfolio.pending.autoConf.state.txNotFound=Transaction not seen in mem-pool yet portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=No valid transaction ID / transaction key portfolio.pending.autoConf.state.filterDisabledFeature=Disabled by developers. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FEATURE_DISABLED=Auto-confirm feature is disabled. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=Trade amount exceeds auto-confirm amount limit # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.INVALID_DATA=Peer provided invalid data. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=Payout transaction was already published. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.DISPUTE_OPENED=Dispute was opened. Auto-confirm is deactivated for that trade. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.REQUESTS_STARTED=Transaction proof requests started # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PENDING=Success results: {0}/{1}; {2} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.COMPLETED=Proof at all services succeeded # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. No auto-confirm possible. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. portfolio.pending.step1.info.you=تراکنش واریز منتشر شد.\nشما می‌توانید پس از ۱۰ تأیید (~{0} دقیقه باقی مانده) پرداخت را آغاز کنید. portfolio.pending.step1.info.buyer=تراکنش واریز منتشر شد.\nخریدار XMR می‌تواند پس از ۱۰ تأیید (~{0} دقیقه باقی مانده) پرداخت را آغاز کند. portfolio.pending.step1.warn=The deposit transaction is still not confirmed. This sometimes happens in rare cases when the funding fee of one trader from an external wallet was too low. portfolio.pending.step1.openForDispute=The deposit transaction is still not confirmed. You can wait longer or contact the mediator for assistance. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2.confReached=Your trade has reached at least one blockchain confirmation.\n\n portfolio.pending.step2_buyer.refTextWarn=Important: when making the payment, leave the \"reason for payment\" field empty. DO NOT put the trade ID or any other text like 'monero', 'XMR', or 'Haveno'. You are free to discuss via trader chat if an alternate \"reason for payment\" would be suitable to you both. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees=If your bank charges you any fees to make the transfer, you are responsible for paying those fees. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.crypto=لطفاً از کیف پول {0} خارجی شما انتقال دهید.\n{1} به فروشنده بیتکوین\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.cash=لطفاً به یک بانک بروید و {0} را به فروشنده ی بیتکوین پرداخت نمایید.\n\n portfolio.pending.step2_buyer.cash.extra=مورد الزامی مهم:\nبعد از اینکه پرداخت را انجام دادید، روی کاغذ رسید بنویسید: بدون استرداد.\nسپس آن را به 2 قسمت پاره کنید، از آن ها عکس بگیرید و به آدرس ایمیل فروشنده‌ی بیتکوین ارسال نمایید. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.moneyGram=لطفاً {0} را توسط مانی‌گرام، به فروشنده ی بیتکوین پرداخت نمایید.\n\n portfolio.pending.step2_buyer.moneyGram.extra=مورد الزامی مهم:\nبعد از اینکه پرداخت را انجام دادید، شماره مجوز و یک عکس از رسید را با ایمیل به فروشنده‌ی بیتکوین ارسال کنید.\nرسید باید به طور واضح نام کامل، کشور، ایالت فروشنده و مقدار را نشان دهد. ایمیل فروشنده: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.westernUnion=لطفاً {0} را با استفاده از Western Union به فروشنده‌ی بیتکوین پرداخت کنید.\n\n portfolio.pending.step2_buyer.westernUnion.extra=مورد الزامی مهم:\nبعد از اینکه پرداخت را انجام دادید، MTCN (عدد پیگیری) و یک عکس از رسید را با ایمیل به فروشنده‌ی بیتکوین ارسال کنید.\nرسید باید به طور واضح نام کامل، کشور، ایالت فروشنده و مقدار را نشان دهد. ایمیل فروشنده: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=لطفاً {0} را توسط \"US Postal Money Order\" به فروشنده‌ی بیتکوین پرداخت کنید.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=لطفا با استفاده از راه‌های ارتباطی ارائه شده توسط فروشنده با وی تماس بگیرید و قرار ملاقاتی را برای پرداخت {0} تنظیم کنید.\n portfolio.pending.step2_buyer.startPaymentUsing=آغاز پرداخت با استفاده از {0} portfolio.pending.step2_buyer.recipientsAccountData=Recipients {0} portfolio.pending.step2_buyer.amountToTransfer=مبلغ انتقال portfolio.pending.step2_buyer.sellersAddress=آدرس {0} فروشنده portfolio.pending.step2_buyer.buyerAccount=حساب پرداخت مورد استفاده portfolio.pending.step2_buyer.paymentSent=پرداخت آغاز شد portfolio.pending.step2_buyer.showEarly=جزئیات پرداخت را زود نمایش دهید portfolio.pending.step2_buyer.warn=You still have not done your {0} payment!\nPlease note that the trade has to be completed by {1}. portfolio.pending.step2_buyer.openForDispute=You have not completed your payment!\nThe max. period for the trade has elapsed.Please contact the mediator for assistance. portfolio.pending.step2_buyer.paperReceipt.headline=آیا کاغذ رسید را برای فروشنده‌ی بیتکوین فرستادید؟ portfolio.pending.step2_buyer.paperReceipt.msg=به یاد داشته باشید:\nباید روی کاغذ رسید بنویسید: غیر قابل استرداد.\nبعد آن را به 2 قسمت پاره کنید، عکس بگیرید و آن را به آدرس ایمیل فروشنده ارسال کنید. portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=شماره و رسید مجوز را ارسال کنید portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=شما باید شماره مجوز و یک عکس از رسید را با ایمیل به فروشنده‌ی بیتکوین ارسال نمایید.\nرسید باید به طور واضح نام کامل، کشور، ایالت فروشنده و مقدار را نشان دهد. ایمیل فروشنده: {0}.\nآیا شماره مجوز و قرارداد را برای فروشنده فرستادید؟ portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=MTCN و رسید را ارسال کنید portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=شما باید MTCN (شماره پیگیری) و یک عکس از رسید را با ایمیل به فروشنده‌ی بیتکوین ارسال نمایید.\nرسید باید به طور واضح نام کامل، کشور، ایالت فروشنده و مقدار را نشان دهد. ایمیل فروشنده: {0}.\n\nآیا MTCN و قرارداد را برای فروشنده فرستادید؟ portfolio.pending.step2_buyer.halCashInfo.headline=ارسال کد HalCash portfolio.pending.step2_buyer.halCashInfo.msg=باید کد HalCash و شناسه‌ی معامله ({0}) را به فروشنده بیتکوین پیامک بفرستید. شماره موبایل فروشنده بیتکوین {1} است. آیا کد را برای فروشنده فرستادید؟ portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Some banks might verify the receiver's name. Faster Payments accounts created in old Haveno clients do not provide the receiver's name, so please use trade chat to obtain it (if needed). portfolio.pending.step2_buyer.confirmStart.headline=تأیید کنید که پرداخت را آغاز کرده‌اید portfolio.pending.step2_buyer.confirmStart.msg=آیا شما پرداخت {0} را به شریک معاملاتی خود آغاز کردید؟ portfolio.pending.step2_buyer.confirmStart.yes=بلی، پرداخت را آغاز کرده‌ام portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=You have not provided proof of payment portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=You have not entered the transaction ID and the transaction key.\n\nBy not providing this data the peer cannot use the auto-confirm feature to release the XMR as soon the XMR has been received.\nBeside that, Haveno requires that the sender of the XMR transaction is able to provide this information to the mediator or arbitrator in case of a dispute.\nSee more details on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=Input is not a 32 byte hexadecimal value portfolio.pending.step2_buyer.confirmStart.warningButton=Ignore and continue anyway portfolio.pending.step2_seller.waitPayment.headline=برای پرداخت منتظر باشید portfolio.pending.step2_seller.f2fInfo.headline=اطلاعات تماس خریدار portfolio.pending.step2_seller.waitPayment.msg=تراکنش سپرده، حداقل یک تأییدیه بلاکچین دارد.شما\nباید تا آغاز پرداخت {0} از جانب خریدار بیتکوین، صبر نمایید. portfolio.pending.step2_seller.warn=خریدار بیت‌کوین هنوز پرداخت {0} را انجام نداده است.\nشما باید تا آغاز پرداخت از جانب او، صبر نمایید.\nاگر معامله تا {1} تکمیل نشد، داور بررسی خواهد کرد. portfolio.pending.step2_seller.openForDispute=The XMR buyer has not started their payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the mediator for assistance. tradeChat.chatWindowTitle=Chat window for trade with ID '{0}' tradeChat.openChat=Open chat window tradeChat.rules=You can communicate with your trade peer to resolve potential problems with this trade.\nIt is not mandatory to reply in the chat.\nIf a trader violates any of the rules below, open a dispute and report it to the mediator or arbitrator.\n\nChat rules:\n\t● Do not send any links (risk of malware). You can send the transaction ID and the name of a block explorer.\n\t● Do not send your seed words, private keys, passwords or other sensitive information!\n\t● Do not encourage trading outside of Haveno (no security).\n\t● Do not engage in any form of social engineering scam attempts.\n\t● If a peer is not responding and prefers to not communicate via chat, respect their decision.\n\t● Keep conversation scope limited to the trade. This chat is not a messenger replacement or troll-box.\n\t● Keep conversation friendly and respectful. # suppress inspection "UnusedProperty" message.state.UNDEFINED=تعریف نشده # suppress inspection "UnusedProperty" message.state.SENT=پیام ارسال شد # suppress inspection "UnusedProperty" message.state.ARRIVED=پیام به همتا رسید # suppress inspection "UnusedProperty" message.state.STORED_IN_MAILBOX=Message of payment sent but not yet received by peer # suppress inspection "UnusedProperty" message.state.ACKNOWLEDGED=همتا رسید پیام را تأیید کرد # suppress inspection "UnusedProperty" message.state.FAILED=ارسال پیام ناموفق بود portfolio.pending.step3_buyer.wait.headline=برای تأییدیه‌ی پرداخت فروشنده‌ی بیتکوین منتظر باشید portfolio.pending.step3_buyer.wait.info=برای تأییدیه رسید پرداخت {0} از جانب فروشنده‌ی بیتکوین، منتظر باشید portfolio.pending.step3_buyer.wait.msgStateInfo.label=وضعیت پیام آغاز شدن پرداخت portfolio.pending.step3_buyer.warn.part1a=بر بلاکچین {0} portfolio.pending.step3_buyer.warn.part1b=در ارائه دهنده‌ی پرداخت شما (برای مثال بانک) portfolio.pending.step3_buyer.warn.part2=The XMR seller still has not confirmed your payment. Please check {0} if the payment sending was successful. portfolio.pending.step3_buyer.openForDispute=The XMR seller has not confirmed your payment! The max. period for the trade has elapsed. You can wait longer and give the trading peer more time or request assistance from the mediator. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.part=شریک معاملاتی شما تأیید کرده که پرداخت {0} را آغاز نموده است.\n\n portfolio.pending.step3_seller.crypto.explorer=در کاوشگر بلاکچین محبوبتان {0} portfolio.pending.step3_seller.crypto.wallet=در کیف‌پول {0} شما portfolio.pending.step3_seller.crypto={0} لطفا بررسی کنید {1} که آیا تراکنش مربوط به آدرس شما\n{2}\n تعداد تاییدیه‌های کافی بر روی بلاکچین دریافت کرده است یا خیر.\nمبلغ پرداخت باید {3} باشد\nشما می‌توانید آدرس {4} خود را پس از بستن پنجره از صفحه اصلی کپی کنید. portfolio.pending.step3_seller.postal={0}Please check if you have received {1} with \"US Postal Money Order\" from the XMR buyer. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.payByMail={0}Please check if you have received {1} with \"Pay by Mail\" from the XMR buyer. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.bank=Your trading partner has confirmed that they have initiated the {0} payment.\n\nPlease go to your online banking web page and check if you have received {1} from the XMR buyer. portfolio.pending.step3_seller.cash=چون پرداخت از طریق سپرده‌ی نقدی انجام شده است، خریدار XMR باید عبارت \"غیر قابل استرداد\" را روی رسید کاغذی بنویسد، آن را به 2 قسمت پاره کند و از طریق ایمیل به شما یک عکس ارسال کند.\n\nبه منظور اجتناب از استرداد وجه، تنها در صورتی تایید کنید که ایمیل را دریافت کرده باشید و از صحت رسید کاغذی مطمئن باشید.\nاگر مطمئن نیستید، {0} portfolio.pending.step3_seller.moneyGram=خریدار باید شماره مجوز و عکسی از رسید را به ایمیل شما ارسال کند.\nرسید باید به طور واضح نام کامل شما ، کشور، ایالت فروشنده و مقدار را نشان دهد. لطفاً ایمیل خود را بررسی کنید که آیا شماره مجوز را دریافت کرده‌اید یا خیر.\n\nپس از بستن پنجره، نام و آدرس خریدار بیتکوین را برای برداشت پول از مانی‌گرام خواهید دید.\n\nتنها پس از برداشت موفقیت آمیز پول، رسید را تأیید کنید! portfolio.pending.step3_seller.westernUnion=خریدار باید MTCN (شماره پیگیری) و عکسی از رسید را به ایمیل شما ارسال کند.\nرسید باید به طور واضح نام کامل شما، کشور، ایالت فروشنده و مقدار را نشان دهد. لطفاً ایمیل خود را بررسی کنید که آیا MTCN را دریافت کرده اید یا خیر.\nپس از بستن پنجره، نام و آدرس خریدار بیتکوین را برای برداشت پول از Western Union خواهید دید.\nتنها پس از برداشت موفقیت آمیز پول، رسید را تأیید کنید! portfolio.pending.step3_seller.halCash=خریدار باید کد HalCash را برای شما با پیامک بفرستد. علاوه‌ برآن شما از HalCash پیامی را محتوی اطلاعات موردنیاز برای برداشت EUR از خودپردازهای پشتیبان HalCash دریافت خواهید کرد.\n\nپس از اینکه پول را از دستگاه خودپرداز دریافت کردید، لطفا در اینجا رسید پرداخت را تایید کنید. portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon eGift Card by email or by text message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted confirm the payment receipt. portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=تأیید رسید پرداخت portfolio.pending.step3_seller.amountToReceive=مبلغ قابل دریافت portfolio.pending.step3_seller.yourAddress=آدرس {0} شما portfolio.pending.step3_seller.buyersAddress=آدرس {0} خریدار portfolio.pending.step3_seller.yourAccount=حساب معاملاتی شما portfolio.pending.step3_seller.xmrTxHash=شناسه تراکنش portfolio.pending.step3_seller.xmrTxKey=Transaction key portfolio.pending.step3_seller.buyersAccount=Buyers account data portfolio.pending.step3_seller.confirmReceipt=تأیید رسید پرداخت portfolio.pending.step3_seller.buyerStartedPayment=خریدار بیتکوین پرداخت {0} را آغاز کرده است.\n{1} portfolio.pending.step3_seller.buyerStartedPayment.crypto=تأییدیه‌های بلاکچین را در کیف پول آلتکوین خود یا بلاکچین اکسپلورر بررسی کنید و هنگامی که تأییدیه های بلاکچین کافی دارید، پرداخت را تأیید کنید. portfolio.pending.step3_seller.buyerStartedPayment.traditional=حساب معاملاتی خود را بررسی کنید (برای مثال بانک) و وقتی وجه را دریافت کردید، تأیید نمایید. portfolio.pending.step3_seller.warn.part1a=در بلاکچین {0} portfolio.pending.step3_seller.warn.part1b=در ارائه دهنده‌ی پرداخت شما (برای مثال بانک) portfolio.pending.step3_seller.warn.part2=You still have not confirmed the receipt of the payment. Please check {0} if you have received the payment. portfolio.pending.step3_seller.openForDispute=You have not confirmed the receipt of the payment!\nThe max. period for the trade has elapsed.\nPlease confirm or request assistance from the mediator. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=آیا وجه {0} را از شریک معاملاتی خود دریافت کرده‌اید؟\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Please note, that as soon you have confirmed the receipt, the locked trade amount will be released to the XMR buyer and the security deposit will be refunded.\n\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=تأیید کنید که وجه را دریافت کرده‌اید portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=بله وجه را دریافت کرده‌ام portfolio.pending.step3_seller.onPaymentReceived.signer=IMPORTANT: By confirming receipt of payment, you are also verifying the account of the counterparty and signing it accordingly. Since the account of the counterparty hasn't been signed yet, you should delay confirmation of the payment as long as possible to reduce the risk of a chargeback. portfolio.pending.step5_buyer.groupTitle=خلاصه‌ای از معامله‌ی کامل شده portfolio.pending.step5_buyer.tradeFee=کارمزد معامله portfolio.pending.step5_buyer.makersMiningFee=کارمزد استخراج portfolio.pending.step5_buyer.takersMiningFee=کل کارمزد استخراج portfolio.pending.step5_buyer.refunded=سپرده اطمینان مسترد شده portfolio.pending.step5_buyer.withdrawXMR=برداشت بیتکوین شما portfolio.pending.step5_buyer.amount=مبلغ قابل برداشت portfolio.pending.step5_buyer.withdrawToAddress=برداشت به آدرس portfolio.pending.step5_buyer.moveToHavenoWallet=Keep funds in Haveno wallet portfolio.pending.step5_buyer.withdrawExternal=برداشت به کیف پول خارجی portfolio.pending.step5_buyer.alreadyWithdrawn=وجوه شما در حال حاضر برداشت شده است.\nلطفاً تاریخچه‌ی تراکنش را بررسی کنید. portfolio.pending.step5_buyer.confirmWithdrawal=تأیید درخواست برداشت portfolio.pending.step5_buyer.amountTooLow=مقدار مورد انتقال کمتر از هزینه تراکنش و حداقل ارزش tx (dust) است. portfolio.pending.step5_buyer.withdrawalCompleted.headline=برداشت تکمیل شد portfolio.pending.step5_buyer.withdrawalCompleted.msg=معاملات تکمیل شده‌ی شما در \"سبد سهام/تاریخچه\" ذخیره شده است.\nشما میتوانید تمام تراکنش‌های بیتکوین خود را در \"وجوه/تراکنش‌ها\" مرور کنید. portfolio.pending.step5_buyer.bought=شما خریده‌اید portfolio.pending.step5_buyer.paid=پرداخت کرده‌اید portfolio.pending.step5_seller.sold=فروخته‌اید portfolio.pending.step5_seller.received=دریافت کرده‌اید tradeFeedbackWindow.title=تبریک، معامله شما کامل شد. tradeFeedbackWindow.msg.part1=دوست داریم تجربه شما را بشنویم. این امر به ما کمک می کند تا نرم افزار را بهبود بخشیم و مشکلات را حل کنیم. اگر می خواهید بازخوردی ارائه کنید، لطفا این نظرسنجی کوتاه (بدون نیاز به ثبت نام) را در زیر پر کنید: tradeFeedbackWindow.msg.part2=اگر سوالی دارید یا مشکلی را تجربه کرده‌اید، لطفا با سایر کاربران و شرکت کننده ها از طریق انجمن Haveno که در ذیل ارائه شده، به اشتراک بگذارید: tradeFeedbackWindow.msg.part3=بابت استفاده از Haveno، از شما متشکریم! portfolio.pending.role=نقش من portfolio.pending.tradeInformation=اطلاعات معامله portfolio.pending.remainingTime=زمان باقیمانده portfolio.pending.remainingTimeDetail={0} (تا {1}) portfolio.pending.remainingTimeDetail.startsAfter=پس از {0} تأیید شروع می‌شود portfolio.pending.tradePeriodInfo=پس از {0} تأیید، دوره معامله آغاز می‌شود. بر اساس روش پرداخت استفاده شده، حداکثر دوره معامله مجاز متفاوت اعمال می‌شود. portfolio.pending.tradePeriodWarning=اگر مهلت به پایان برسد، هر دو معامله گر می توانند یک مناقشه را باز کنند. portfolio.pending.tradeNotCompleted=معامله به موقع (تا {0}) تکمیل نشد portfolio.pending.tradeProcess=فرآیند معامله portfolio.pending.openAgainDispute.msg=If you are not sure that the message to the mediator or arbitrator arrived (e.g. if you did not get a response after 1 day) feel free to open a dispute again with Cmd/Ctrl+o. You can also ask for additional help on the Haveno forum at [HYPERLINK:https://haveno.community]. portfolio.pending.openAgainDispute.button=باز کردن مجدد مناقشه portfolio.pending.openSupportTicket.headline=باز کردن تیکت پشتیبانی portfolio.pending.openSupportTicket.msg=Please use this function only in emergency cases if you don't see a \"Open support\" or \"Open dispute\" button.\n\nWhen you open a support ticket the trade will be interrupted and handled by a mediator or arbitrator. portfolio.pending.timeLockNotOver=You have to wait until ≈{0} ({1} more blocks) before you can open an arbitration dispute. portfolio.pending.error.depositTxNull=The deposit transaction is null. You cannot open a dispute without a valid deposit transaction. Please go to \"Settings/Network info\" and do a SPV resync.\n\nFor further help please contact the Haveno support channel at the Haveno Keybase team. portfolio.pending.mediationResult.error.depositTxNull=The deposit transaction is null. You can move the trade to failed trades. portfolio.pending.mediationResult.error.delayedPayoutTxNull=The delayed payout transaction is null. You can move the trade to failed trades. portfolio.pending.error.depositTxNotConfirmed=The deposit transaction is not confirmed. You can not open an arbitration dispute with an unconfirmed deposit transaction. Please wait until it is confirmed or go to \"Settings/Network info\" and do a SPV resync.\n\nFor further help please contact the Haveno support channel at the Haveno Keybase team. portfolio.pending.support.headline.getHelp=Need help? portfolio.pending.support.text.getHelp=If you have any problems you can try to contact the trade peer in the trade chat or ask the Haveno community at https://haveno.community. If your issue still isn't resolved, you can request more help from a mediator. portfolio.pending.support.button.getHelp=Open Trader Chat portfolio.pending.support.headline.halfPeriodOver=Check payment portfolio.pending.support.headline.periodOver=Trade period is over portfolio.pending.support.headline.depositTxMissing=تراکنش واریز مفقود شده portfolio.pending.support.depositTxMissing=برای این معامله، تراکنش واریز وجود ندارد. برای دریافت کمک با داور، یک تیکت پشتیبانی باز کنید. portfolio.pending.mediationRequested=Mediation requested portfolio.pending.refundRequested=Refund requested portfolio.pending.openSupport=باز کردن تیکت پشتیبانی portfolio.pending.supportTicketOpened=تیکت پشتیبانی باز شد portfolio.pending.communicateWithArbitrator=لطفا در صفحه‌ی \"پشتیبانی\" با داور در ارتباط باشید. portfolio.pending.communicateWithMediator=Please communicate in the \"Support\" screen with the mediator. portfolio.pending.disputeOpenedByUser=شما در حال حاضر یک مناقشه باز کرده‌اید.\n{0} portfolio.pending.disputeOpenedByPeer=طرف معامله شما یک مناقشه باز کرده است\n{0} portfolio.pending.noReceiverAddressDefined=آدرسی برای گیرنده تعیین نشده است portfolio.pending.mediationResult.headline=Suggested payout from mediation portfolio.pending.mediationResult.info.noneAccepted=Complete the trade by accepting the mediator's suggestion for the trade payout. portfolio.pending.mediationResult.info.selfAccepted=You have accepted the mediator's suggestion. Waiting for peer to accept as well. portfolio.pending.mediationResult.info.peerAccepted=Your trade peer has accepted the mediator's suggestion. Do you accept as well? portfolio.pending.mediationResult.button=View proposed resolution portfolio.pending.mediationResult.popup.headline=Mediation result for trade with ID: {0} portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator's suggestion for trade {0} portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader's security deposit) as compensation for their work. Both traders agreeing to the mediator's suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator's suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. portfolio.pending.failedTrade.missingDepositTx=یک تراکنش واریز مفقود است.\n\nاین تراکنش برای تکمیل معامله لازم است. لطفاً اطمینان حاصل کنید که کیف پول شما به‌طور کامل با بلاک‌چین مونرو همگام‌سازی شده است.\n\nمی‌توانید این معامله را به بخش «معاملات ناموفق» منتقل کنید تا غیرفعال شود. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Move trade to failed trades portfolio.pending.failedTrade.warningIcon.tooltip=Click to open details about the issues of this trade portfolio.failed.revertToPending.popup=Do you want to move this trade to open trades? portfolio.failed.revertToPending=Move trade to open trades portfolio.closed.completed=تکمیل شده portfolio.closed.ticketClosed=Arbitrated portfolio.closed.mediationTicketClosed=Mediated portfolio.closed.canceled=لغو شده است portfolio.failed.Failed=ناموفق portfolio.failed.unfail=Before proceeding, make sure you have a backup of your data directory!\nDo you want to move this trade back to open trades?\nThis is a way to unlock funds stuck in a failed trade. portfolio.failed.cantUnfail=This trade cannot be moved back to open trades at the moment. \nTry again after completion of trade(s) {0} portfolio.failed.depositTxNull=The trade cannot be reverted to a open trade. Deposit transaction is null. portfolio.failed.delayedPayoutTxNull=The trade cannot be reverted to a open trade. Delayed payout transaction is null. #################################################################### # Funds #################################################################### funds.tab.deposit=دریافت وجوه funds.tab.withdrawal=ارسال وجوه funds.tab.reserved=وجوه اندوخته funds.tab.locked=وجوه قفل شده funds.tab.transactions=تراکنش‌ها funds.deposit.unused=استفاده نشده funds.deposit.usedInTx=مورد استفاده در تراکنش (های) {0} funds.deposit.fundHavenoWallet=تأمین مالی کیف پول Haveno  funds.deposit.noAddresses=آدرس‌هایی برای سپرده ایجاد نشده است funds.deposit.fundWallet=تأمین مالی کیف پول شما funds.deposit.withdrawFromWallet=ارسال وجه از کیف‌پول funds.deposit.amount=مبلغ به XMR (اختیاری) funds.deposit.generateAddress=ایجاد آدرس جدید funds.deposit.generateAddressSegwit=Native segwit format (Bech32) funds.deposit.selectUnused=لطفاً به جای ایجاد یک آدرس جدید، یک آدرس استفاده نشده را از جدول بالا انتخاب کنید. funds.withdrawal.arbitrationFee=هزینه‌ی داوری funds.withdrawal.inputs=انتخاب ورودی‌ها funds.withdrawal.useAllInputs=استفاده از تمام ورودی‌های موجود funds.withdrawal.useCustomInputs=استفاده از ورودی‌های سفارشی funds.withdrawal.receiverAmount=مقدار گیرنده funds.withdrawal.senderAmount=مقدار فرستنده funds.withdrawal.feeExcluded=این مبلغ کارمزد تراکنش در شبکه را شامل نمی‌شود funds.withdrawal.feeIncluded=این مبلغ کارمزد تراکنش در شبکه را شامل می‌شود funds.withdrawal.fromLabel=برداشت از آدرس funds.withdrawal.toLabel=برداشت به آدرس funds.withdrawal.memoLabel=Withdrawal memo funds.withdrawal.memo=Optionally fill memo funds.withdrawal.withdrawButton=برداشت انتخاب شد funds.withdrawal.noFundsAvailable=وجهی برای برداشت وجود ندارد funds.withdrawal.confirmWithdrawalRequest=تأیید درخواست برداشت funds.withdrawal.withdrawMultipleAddresses=برداشت از چندین آدرس ({0}) funds.withdrawal.withdrawMultipleAddresses.tooltip=برداشت از چندین آدرس\n{0} funds.withdrawal.notEnoughFunds=وجه کافی در کیف پول خود ندارید. funds.withdrawal.selectAddress=یک آدرس مرجع از جدول انتخاب کنید funds.withdrawal.setAmount=مبلغ مورد نظر برای برداشت را تعیین نمایید funds.withdrawal.fillDestAddress=آدرس مقصد خود را پر کنید funds.withdrawal.warn.noSourceAddressSelected=یک آدرس مرجع از جدول بالا انتخاب نمایید funds.withdrawal.warn.amountExceeds=وجه کافی موجود از آدرس انتخاب شده ندارید.\nدرنظر بگیرید که چندین آدرس را در جدول بالا انتخاب کنید یا هزینه را تغییر دهید تا کارمزد تراکنش در شبکه را نیز شامل گردد. funds.reserved.noFunds=هیچ وجهی در پیشنهادهای باز اندوخته نشده است. funds.reserved.reserved=اندوخته‌ی کیف پول محلی برای پیشنهاد با شناسه: {0} funds.locked.noFunds=هیچ وجهی در معاملات قفل نشده است funds.locked.locked=قفل شده به صورت چند امضایی برای معامله با شناسه‌ی {0} funds.tx.direction.sentTo=ارسال به: funds.tx.direction.receivedWith=دریافت با: funds.tx.direction.genesisTx=از تراکنش پیدایش: funds.tx.createOfferFee=سفارش‌گذار و هزینه تراکنش: {0} funds.tx.takeOfferFee=پذیرنده و هزینه تراکنش: {0} funds.tx.multiSigDeposit=سپرده چند امضایی: {0} funds.tx.multiSigPayout=پرداخت چند امضایی: {0} funds.tx.disputePayout=پرداخت مناقشه: {0} funds.tx.disputeLost=مورد مناقشه‌ی شکست خورده: {0} funds.tx.collateralForRefund=Refund collateral: {0} funds.tx.timeLockedPayoutTx=Time locked payout tx: {0} funds.tx.refund=Refund from arbitration: {0} funds.tx.unknown=دلیل ناشناخته: {0} funds.tx.noFundsFromDispute=عدم بازپرداخت از مناقشه funds.tx.receivedFunds=وجوه دریافت شده funds.tx.withdrawnFromWallet=برداشت شده از کیف پول funds.tx.memo=Memo funds.tx.noTxAvailable=هیچ تراکنشی موجود نیست funds.tx.revert=عودت funds.tx.txSent=تراکنش به طور موفقیت آمیز به یک آدرس جدید در کیف پول محلی Haveno ارسال شد. funds.tx.direction.self=ارسال شده به خودتان funds.tx.dustAttackTx=Received dust funds.tx.dustAttackTx.popup=This transaction is sending a very small XMR amount to your wallet and might be an attempt from chain analysis companies to spy on your wallet.\n\nIf you use that transaction output in a spending transaction they will learn that you are likely the owner of the other address as well (coin merge).\n\nTo protect your privacy the Haveno wallet ignores such dust outputs for spending purposes and in the balance display. You can set the threshold amount when an output is considered dust in the settings. #################################################################### # Support #################################################################### support.tab.mediation.support=Mediation support.tab.arbitration.support=Arbitration support.tab.legacyArbitration.support=Legacy Arbitration support.tab.ArbitratorsSupportTickets={0}'s tickets support.sigCheck.button=Check signature support.sigCheck.popup.header=Verify dispute result signature support.sigCheck.popup.msg.label=Summary message support.sigCheck.popup.msg.prompt=Copy & paste summary message from dispute support.sigCheck.popup.result=Validation result support.sigCheck.popup.success=Signature is valid support.sigCheck.popup.failed=Signature verification failed support.sigCheck.popup.invalidFormat=Message is not of expected format. Copy & paste summary message from dispute. support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? support.reOpenButton.label=Re-open support.sendNotificationButton.label=اعلان خصوصی support.reportButton.label=Report support.fullReportButton.label=All disputes support.noTickets=هیچ تیکتی به صورت باز وجود ندارد support.sendingMessage=در حال ارسال پیام ... support.receiverNotOnline=Receiver is not online. Message is saved to their mailbox. support.sendMessageError=ارسال پیام ناموفق بود. خطا: {0} support.receiverNotKnown=Receiver not known support.wrongVersion=پیشنهاد در آن مناقشه با یک نسخه‌ی قدیمی از Haveno ایجاد شده است.\nشما نمی توانید آن مناقشه را با نسخه‌ی برنامه‌ی خودتان ببندید.\n\nلطفاً از یک نسخه‌ی قدیمی‌تر با پروتکل نسخه‌ی {0} استفاده کنید support.openFile=انتخاب فایل به منظور پیوست (حداکثر اندازه فایل: {0} کیلوبایت) support.attachmentTooLarge=مجموع اندازه ضمائم شما {0} کیلوبایت است و از حداکثر اندازه ی مجاز پیام {1} کیلوبایت، بیشتر شده است. support.maxSize=حداکثر اندازه‌ی مجاز فایل {0} کیلوبایت است. support.attachment=ضمیمه support.tooManyAttachments=شما نمی‌توانید بیشتر از 3 ضمیمه در یک پیام ارسال کنید. support.save=ذخیره فایل در دیسک support.messages=پیام‌ها support.input.prompt=Enter message... support.send=ارسال support.addAttachments=افزودن ضمیمه support.closeTicket=بستن تیکت support.attachments=ضمیمه‌ها: support.savedInMailbox=پیام در صندوق پستی گیرنده ذخیره شد support.arrived=پیام به گیرنده تحویل داده شد support.acknowledged=تحویل پیام از طرف گیرنده تأیید شد support.error=گیرنده نتوانست پیام را پردازش کند. خطا: {0} support.buyerAddress=آدرس خریدار بیتکوین support.sellerAddress=آدرس فروشنده بیتکوین support.role=نقش support.agent=Support agent support.state=حالت support.chat=Chat support.preparing=در حال آماده‌سازی support.requested=درخواست شده support.closed=بسته support.open=باز support.process=Process support.buyerMaker=خریدار/سفارش گذار بیتکوین support.sellerMaker=فروشنده/سفارش گذار بیتکوین support.buyerTaker=خریدار/پذیرنده‌ی بیتکوین support.sellerTaker=فروشنده/پذیرنده‌ی بیتکوین support.initialInfo=Please enter a description of your problem in the text field below. Add as much information as possible to speed up dispute resolution time.\n\nHere is a check list for information you should provide:\n\t● If you are the XMR buyer: Did you make the Fiat or Crypto transfer? If so, did you click the 'payment started' button in the application?\n\t● If you are the XMR seller: Did you receive the Fiat or Crypto payment? If so, did you click the 'payment received' button in the application?\n\t● Which version of Haveno are you using?\n\t● Which operating system are you using?\n\t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\t Sometimes the data directory gets corrupted and leads to strange bugs. \n\t See: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nPlease make yourself familiar with the basic rules for the dispute process:\n\t● You need to respond to the {0}'s requests within 2 days.\n\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\t● The maximum period for a dispute is 14 days.\n\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\nYou can read more about the dispute process at: {2} support.systemMsg=پیغام سیستم: {0} support.youOpenedTicket=شما یک درخواست برای پشتیبانی باز کردید.\n\n{0}\n\nنسخه Haveno شما: {1} support.youOpenedDispute=شما یک درخواست برای یک اختلاف باز کردید.\n\n{0}\n\nنسخه Haveno شما: {1} support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nHaveno version: {1} support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nHaveno version: {1} support.mediatorsDisputeSummary=System message: Mediator's dispute summary:\n{0} support.mediatorsAddress=Mediator's node address: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. support.info.disputeReOpened=Dispute ticket has been re-opened. #################################################################### # Settings #################################################################### settings.tab.preferences=اولویت‌ها settings.tab.network=اطلاعات شبکه settings.tab.about=درباره setting.preferences.general=اولویت‌های عمومی setting.preferences.explorer=Monero Explorer setting.preferences.deviation=حداکثر تفاوت از قیمت روز بازار setting.preferences.avoidStandbyMode=حالت «آماده باش» را نادیده بگیر setting.preferences.useSoundForNotifications=پخش صداها برای اعلان‌ها setting.preferences.autoConfirmXMR=XMR auto-confirm setting.preferences.autoConfirmEnabled=Enabled setting.preferences.autoConfirmRequiredConfirmations=Required confirmations setting.preferences.autoConfirmMaxTradeSize=Max. trade amount (XMR) setting.preferences.autoConfirmServiceAddresses=Monero Explorer URLs (uses Tor, except for localhost, LAN IP addresses, and *.local hostnames) setting.preferences.deviationToLarge=مقادیر بزرگتر از {0}% مجاز نیست. setting.preferences.txFee=Withdrawal transaction fee (satoshis/vbyte) setting.preferences.useCustomValue=استفاده از ارزش سفارشی setting.preferences.txFeeMin=Transaction fee must be at least {0} satoshis/vbyte setting.preferences.txFeeTooLarge=Your input is above any reasonable value (>5000 satoshis/vbyte). Transaction fee is usually in the range of 50-400 satoshis/vbyte. setting.preferences.ignorePeers=Ignored peers [onion address:port] setting.preferences.ignoreDustThreshold=Min. non-dust output value setting.preferences.currenciesInList=ارزها در لیست قیمت روز بازار setting.preferences.prefCurrency=ارز مطلوب setting.preferences.displayTraditional=نمایش ارزهای ملی setting.preferences.noTraditional=هیچ ارز ملی انتخاب نشده است setting.preferences.cannotRemovePrefCurrency=شما نمی‌توانید ارز مطلوب انتخاب شده‌ی خود را حذف کنید setting.preferences.displayCryptos=نمایش آلت‌کوین‌ها setting.preferences.noCryptos=هیچ آلتکوینی انتخاب نشده است setting.preferences.addTraditional=افزودن ارز ملی setting.preferences.addCrypto=افزودن آلتکوین setting.preferences.displayOptions=نمایش گزینه‌ها setting.preferences.showOwnOffers=نمایش پیشنهادهای من در دفتر پیشنهاد setting.preferences.useAnimations=استفاده از انیمیشن‌ها setting.preferences.useDarkMode=حالت تاریک را استفاده کنید setting.preferences.useLightMode=حالت روشن را استفاده کنید setting.preferences.sortWithNumOffers=مرتب سازی لیست‌ها با تعداد معاملات/پیشنهادها setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API setting.preferences.notifyOnPreRelease=Receive pre-release notifications setting.preferences.resetAllFlags=تنظیم مجدد تمام پرچم‌های \"دوباره نشان نده\" settings.preferences.languageChange=اعمال تغییر زبان به تمام صفحات مستلزم یک راه‌اندازی مجدد است. settings.preferences.supportLanguageWarning=In case of a dispute, please note that arbitration is handled in {0}. settings.preferences.editCustomExplorer.headline=Explorer Settings settings.preferences.editCustomExplorer.description=Choose a system defined explorer from the list on the left, and/or customize to suit your own preferences. settings.preferences.editCustomExplorer.available=Available explorers settings.preferences.editCustomExplorer.chosen=Chosen explorer settings settings.preferences.editCustomExplorer.name=نام settings.preferences.editCustomExplorer.txUrl=Transaction URL settings.preferences.editCustomExplorer.addressUrl=Address URL setting.info.headline=ویژگی جدید حفظ حریم خصوصی داده‌ها settings.preferences.sensitiveDataRemoval.msg=برای محافظت از حریم خصوصی شما و سایر معامله‌گران، Haveno قصد دارد داده‌های حساس مربوط به معاملات قدیمی را حذف کند. این موضوع به ویژه برای معاملات فیات که ممکن است شامل جزئیات حساب بانکی باشند اهمیت دارد.\n\nتوصیه می‌شود این مقدار را تا حد امکان پایین تنظیم کنید، مثلاً ۶۰ روز. این بدان معناست که معاملاتی که بیش از ۶۰ روز از آنها گذشته و تکمیل شده‌اند، داده‌های حساس آنها پاک خواهد شد. معاملات تکمیل شده در تب «پرتفوی / تاریخچه» یافت می‌شوند. settings.net.xmrHeader=شبکه بیتکوین settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=آدرس onion من settings.net.xmrNodesLabel=استفاده از گره‌های Monero اختصاصی settings.net.moneroPeersLabel=همتایان متصل settings.net.connection=اتصال settings.net.connected=متصل settings.net.useTorForXmrJLabel=استفاده از Tor برای شبکه مونرو settings.net.moneroNodesLabel=گره‌های Monero در دسترس settings.net.useProvidedNodesRadio=استفاده از نودهای بیتکوین ارائه شده settings.net.usePublicNodesRadio=استفاده از شبکه بیتکوین عمومی settings.net.useCustomNodesRadio=استفاده از نودهای بیتکوین اختصاصی settings.net.warn.usePublicNodes=If you use public Monero nodes, you are subject to any risk of using untrusted remote nodes.\n\nPlease read more details at [HYPERLINK:https://www.getmonero.org/resources/moneropedia/remote-node.html].\n\nAre you sure you want to use public nodes? settings.net.warn.usePublicNodes.useProvided=خیر، از نودهای فراهم شده استفاده کنید. settings.net.warn.usePublicNodes.usePublic=بلی، از شبکه عمومی استفاده کنید. settings.net.warn.useCustomNodes.B2XWarning=لطفا مطمئن شوید که گره بیت‌کوین شما یک گره مورد اعتماد Monero Core است!\n\nمتصل شدن به گره‌هایی که از قوانین مورد اجماع موجود در Monero Core پیروی نمی‌کنند می‌تواند باعث خراب شدن کیف پول شما شود و در فرآیند معامله مشکلاتی را به وجود بیاورد.\n\nکاربرانی که از گره‌های ناقض قوانین مورد اجماع استفاده می‌کند مسئول هر گونه آسیب ایجاد شده هستند. اگر هر گونه اختلافی به وجود بیاید به نفع دیگر گره‌هایی که از قوانین مورد اجماع پیروی می‌کنند درمورد آن تصمیم گیری خواهد شد. به کاربرانی که این هشدار و سازوکار محافظتی را نادیده می‌گیرند هیچ‌گونه پشتیبانی فنی ارائه نخواهد شد! settings.net.warn.invalidXmrConfig=Connection to the Monero network failed because your configuration is invalid.\n\nYour configuration has been reset to use the provided Monero nodes instead. You will need to restart the application. settings.net.localhostXmrNodeInfo=Background information: Haveno looks for a local Monero node when starting. If it is found, Haveno will communicate with the Monero network exclusively through it. settings.net.p2PPeersLabel=همتایان متصل settings.net.onionAddressColumn=آدرس Onion settings.net.creationDateColumn=تثبیت شده settings.net.connectionTypeColumn=درون/بیرون settings.net.sentDataLabel=Sent data statistics settings.net.receivedDataLabel=Received data statistics settings.net.chainHeightLabel=Latest XMR block height settings.net.roundTripTimeColumn=تاخیر چرخشی settings.net.sentBytesColumn=ارسال شده settings.net.receivedBytesColumn=دریافت شده settings.net.peerTypeColumn=نوع همتا settings.net.openTorSettingsButton=تنظیمات Tor را باز کنید.   settings.net.versionColumn=Version settings.net.subVersionColumn=Subversion settings.net.heightColumn=Height settings.net.needRestart=به منظور اعمال آن تغییر باید برنامه را مجدداً راه اندازی کنید.\nآیا می‌خواهید این کار را هم اکنون انجام دهید؟ settings.net.notKnownYet=هنوز شناخته شده نیست ... settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec settings.net.chainHeight=Monero Peers chain height: {0} settings.net.ips=[آدرس آی پی: پورت | نام میزبان: پورت | آدرس Onion : پورت] (جدا شده با ویرگول). اگر از پیش فرض (8333) استفاده می شود، پورت می تواند حذف شود. settings.net.seedNode=گره ی اصلی settings.net.directPeer=همتا (مستقیم) settings.net.initialDataExchange={0} [Bootstrapping] settings.net.peer=همتا settings.net.inbound=وارد شونده settings.net.outbound=خارج شونده setting.about.aboutHaveno=درباره Haveno setting.about.about=Haveno یک پروژه منبع باز و یک شبکه غیر متمرکز از کاربرانی است که می‌خواهند بیت‌کوین را با ارزهای ملی (یا رمزارزهای جایگزین) به روشی امن تبادل کنند. در وب سایت ما با Haveno بیشتر آشنا شوید. setting.about.web=صفحه وب Haveno setting.about.code=کد منبع setting.about.agpl=مجوز AGPL setting.about.support=پشتیبانی از Haveno setting.about.def=Haveno یک شرکت نیست — یک پروژه اجتماعی است و برای مشارکت آزاد است. اگر می‌خواهید در آن مشارکت کنید یا از آن حمایت نمایید، لینک‌‌های زیر را دنبال کنید. setting.about.contribute=مشارکت setting.about.providers=ارائه دهندگان داده setting.about.apisWithFee=Haveno uses Haveno Price Indices for Fiat and Crypto market prices, and Haveno Mempool Nodes for mining fee estimation. setting.about.apis=Haveno uses Haveno Price Indices for Fiat and Crypto market prices. setting.about.pricesProvided=قیمت‌های بازار ارائه شده توسط setting.about.feeEstimation.label=برآورد کارمزد استخراج ارائه شده توسط setting.about.versionDetails=جزئیات نسخه setting.about.version=نسخه برنامه setting.about.subsystems.label=نسخه‌های زیرسیستم‌ها setting.about.subsystems.val=نسخه ی شبکه: {0}; نسخه ی پیام همتا به همتا: {1}; نسخه ی Local DB: {2}; نسخه پروتکل معامله: {3} setting.about.shortcuts=Short cuts setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' or 'alt + {0}' or 'cmd + {0}' setting.about.shortcuts.menuNav=Navigate main menu setting.about.shortcuts.menuNav.value=To navigate the main menu press: 'Ctrl' or 'alt' or 'cmd' with a numeric key between '1-9' setting.about.shortcuts.close=Close Haveno setting.about.shortcuts.close.value='Ctrl + {0}' or 'cmd + {0}' or 'Ctrl + {1}' or 'cmd + {1}' setting.about.shortcuts.closePopup=Close popup or dialog window setting.about.shortcuts.closePopup.value='ESCAPE' key setting.about.shortcuts.chatSendMsg=Send trader chat message setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' or 'alt + ENTER' or 'cmd + ENTER' setting.about.shortcuts.openDispute=Open dispute setting.about.shortcuts.openDispute.value=Select pending trade and click: {0} setting.about.shortcuts.walletDetails=Open wallet details window setting.about.shortcuts.openEmergencyXmrWalletTool=Open emergency wallet tool for XMR wallet setting.about.shortcuts.showTorLogs=Toggle log level for Tor messages between DEBUG and WARN setting.about.shortcuts.manualPayoutTxWindow=Open window for manual payout from 2of2 Multisig deposit tx setting.about.shortcuts.removeStuckTrade=Open popup to move failed trade to open trades tab again setting.about.shortcuts.removeStuckTrade.value=Select failed trade and press: {0} setting.about.shortcuts.registerArbitrator=Register arbitrator (mediator/arbitrator only) setting.about.shortcuts.registerArbitrator.value=Navigate to account and press: {0} setting.about.shortcuts.registerMediator=Register mediator (mediator/arbitrator only) setting.about.shortcuts.registerMediator.value=Navigate to account and press: {0} setting.about.shortcuts.openSignPaymentAccountsWindow=Open window for account age signing (legacy arbitrators only) setting.about.shortcuts.openSignPaymentAccountsWindow.value=Navigate to legacy arbitrator view and press: {0} setting.about.shortcuts.sendAlertMsg=Send alert or update message (privileged activity) setting.about.shortcuts.sendFilter=Set Filter (privileged activity) setting.about.shortcuts.sendPrivateNotification=Send private notification to peer (privileged activity) setting.about.shortcuts.sendPrivateNotification.value=Open peer info at avatar and press: {0} setting.info.headline=New XMR auto-confirm Feature setting.info.msg=When selling XMR for XMR you can use the auto-confirm feature to verify that the correct amount of XMR was sent to your wallet so that Haveno can automatically mark the trade as complete, making trades quicker for everyone.\n\nAuto-confirm checks the XMR transaction on at least 2 XMR explorer nodes using the private transaction key provided by the XMR sender. By default, Haveno uses explorer nodes run by Haveno contributors, but we recommend running your own XMR explorer node for maximum privacy and security.\n\nYou can also set the maximum amount of XMR per trade to auto-confirm as well as the number of required confirmations here in Settings.\n\nSee more details (including how to set up your own explorer node) on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades] #################################################################### # Account #################################################################### account.tab.mediatorRegistration=Mediator registration account.tab.refundAgentRegistration=Refund agent registration account.tab.signing=Signing account.info.headline=به حساب Haveno خود خوش آمدید account.info.msg=Here you can add trading accounts for national currencies & cryptos and create a backup of your wallet & account data.\n\nA new Monero wallet was created the first time you started Haveno.\n\nWe strongly recommend that you write down your Monero wallet seed words (see tab on the top) and consider adding a password before funding. Monero deposits and withdrawals are managed in the \"Funds\" section.\n\nPrivacy & security note: because Haveno is a decentralized exchange, all your data is kept on your computer. There are no servers, so we have no access to your personal info, your funds, or even your IP address. Data such as bank account numbers, crypto & Monero addresses, etc are only shared with your trading partner to fulfill trades you initiate (in case of a dispute the mediator or arbitrator will see the same data as your trading peer). account.menu.paymentAccount=حساب های ارز ملی account.menu.altCoinsAccountView=حساب های آلت کوین account.menu.password=رمز کیف پول account.menu.seedWords=رمز پشتیبان کیف پول account.menu.walletInfo=Wallet info account.menu.backup=پشتیبان account.menu.notifications=اعلان‌ها account.menu.walletInfo.balance.headLine=Wallet balances account.menu.walletInfo.balance.info=This shows the internal wallet balance including unconfirmed transactions.\nFor XMR, the internal wallet balance shown below should match the sum of the 'Available' and 'Reserved' balances shown in the top right of this window. account.menu.walletInfo.xpub.headLine=Watch keys (xpub keys) account.menu.walletInfo.walletSelector={0} {1} wallet account.menu.walletInfo.path.headLine=HD keychain paths account.menu.walletInfo.path.info=If you import seed words into another wallet (like Electrum), you'll need to define the path. This should only be done in emergency cases when you lose access to the Haveno wallet and data directory.\nKeep in mind that spending funds from a non-Haveno wallet can bungle the internal Haveno data structures associated with the wallet data, which can lead to failed trades.\n\nNEVER send BSQ from a non-Haveno wallet, as it will probably lead to an invalid BSQ transaction and losing your BSQ. account.menu.walletInfo.openDetails=Show raw wallet details and private keys ## TODO should we rename the following to a gereric name? account.arbitratorRegistration.pubKey=کلید عمومی account.arbitratorRegistration.register=Register account.arbitratorRegistration.registration={0} registration account.arbitratorRegistration.revoke=ابطال account.arbitratorRegistration.info.msg=Please note that you need to stay available for 15 days after revoking as there might be trades which are using you as {0}. The max. allowed trade period is 8 days and the dispute process might take up to 7 days. account.arbitratorRegistration.warn.min1Language=شما باید حداقل 1 زبان را انتخاب کنید.\nما زبان پیشفرض را برای شما اضافه کردیم. account.arbitratorRegistration.removedSuccess=You have successfully removed your registration from the Haveno network. account.arbitratorRegistration.removedFailed=Could not remove registration.{0} account.arbitratorRegistration.registerSuccess=You have successfully registered to the Haveno network. account.arbitratorRegistration.registerFailed=Could not complete registration.{0} account.crypto.yourCryptoAccounts=حساب‌های آلت‌کوین شما account.crypto.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don't control your keys or (b) which don't use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is not a {2} specialist and cannot help in such cases. account.crypto.popup.wallet.confirm=من می فهمم و تأیید می کنم که می دانم از کدام کیف پول باید استفاده کنم. # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=Trading UPX on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending UPX, you need to use either the official uPlexa GUI wallet or uPlexa CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\nuplexa-wallet-cli (use the command get_tx_key)\nuplexa-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The UPX sender is responsible for providing verification of the UPX transfer to the arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit uPlexa discord channel (https://discord.gg/vhdNSrV) or the uPlexa Telegram Chat (https://t.me/uplexaOfficial) to find more information. # suppress inspection "UnusedProperty" account.crypto.popup.arq.msg=Trading ARQ on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending ARQ, you need to use either the official ArQmA GUI wallet or ArQmA CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\narqma-wallet-cli (use the command get_tx_key)\narqma-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the mediator or arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The ARQ sender is responsible for providing verification of the ARQ transfer to the mediator or arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit ArQmA discord channel (https://discord.gg/s9BQpJT) or the ArQmA forum (https://labs.arqma.com) to find more information. # suppress inspection "UnusedProperty" account.crypto.popup.xmr.msg=Trading XMR on Haveno requires that you understand the following requirement.\n\nIf selling XMR, you must be able to provide the following information to a mediator or arbitrator in case of a dispute:\n- the transaction key (Tx Key, Tx Secret Key or Tx Private Key)\n- the transaction ID (Tx ID or Tx Hash)\n- the destination address (recipient's address)\n\nSee the wiki for details on where to find this information on popular Monero wallets [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Proving_payments].\nFailure to provide the required transaction data will result in losing disputes.\n\nAlso note that Haveno now offers automatic confirming for XMR transactions to make trades quicker, but you need to enable it in Settings.\n\nSee the wiki for more information about the auto-confirm feature: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. # suppress inspection "UnusedProperty" account.crypto.popup.msr.msg=Trading MSR on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending MSR, you need to use either the official Masari GUI wallet, Masari CLI wallet with the store-tx-info flag enabled (enabled by default) or the Masari web wallet (https://wallet.getmasari.org). Please be sure you can access the tx key as that would be required in case of a dispute.\nmasari-wallet-cli (use the command get_tx_key)\nmasari-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nMasari Web Wallet (goto Account -> transaction history and view details on your sent transaction)\n\nVerification can be accomplished in-wallet.\nmasari-wallet-cli : using command (check_tx_key).\nmasari-wallet-gui : on the Advanced > Prove/Check page.\nVerification can be accomplished in the block explorer \nOpen block explorer (https://explorer.getmasari.org), use the search bar to find your transaction hash.\nOnce transaction is found, scroll to bottom to the 'Prove Sending' area and fill in details as needed.\nYou need to provide the mediator or arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The MSR sender is responsible for providing verification of the MSR transfer to the mediator or arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process, ask for help on the Official Masari Discord (https://discord.gg/sMCwMqs). # suppress inspection "UnusedProperty" account.crypto.popup.blur.msg=Trading BLUR on Haveno requires that you understand and fulfill the following requirements:\n\nTo send BLUR you must use the Blur Network CLI or GUI Wallet. \n\nIf you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\nIf you are using the Blur Network GUI Wallet, the transaction private key and transaction ID can be found conveniently in the "History" tab. Immediately after sending, locate the transaction of interest. Click the "?" symbol in the lower-right corner of the box containing the transaction. You must save this information. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the BLUR transfer using the Blur Transaction Viewer (https://blur.cash/#tx-viewer).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the BLUR sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the Blur Network Discord (https://discord.gg/dMWaqVW). # suppress inspection "UnusedProperty" account.crypto.popup.solo.msg=Trading Solo on Haveno requires that you understand and fulfill the following requirements:\n\nTo send Solo you must use the Solo Network CLI Wallet. \n\nIf you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the Solo transfer using the Solo Block Explorer by searching for the transaction and then using the "Prove sending" function (https://explorer.minesolo.com/).\n\nfailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the Solo sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the Solo Network Discord (https://discord.minesolo.com/). # suppress inspection "UnusedProperty" account.crypto.popup.cash2.msg=Trading CASH2 on Haveno requires that you understand and fulfill the following requirements:\n\nTo send CASH2 you must use the Cash2 Wallet version 3 or higher. \n\nAfter a transaction is sent, the transaction ID will be displayed. You must save this information. Immediately after sending the transaction, you must use the command 'getTxKey' in simplewallet to retrieve the transaction secret key. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, 2) the transaction secret key, and 3) the recipient's Cash2 address. The mediator or arbitrator will then verify the CASH2 transfer using the Cash2 Block Explorer (https://blocks.cash2.org).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the CASH2 sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the Cash2 Discord (https://discord.gg/FGfXAYN). # suppress inspection "UnusedProperty" account.crypto.popup.qwertycoin.msg=Trading Qwertycoin on Haveno requires that you understand and fulfill the following requirements:\n\nTo send QWC you must use the official QWC Wallet version 5.1.3 or higher. \n\nAfter a transaction is sent, the transaction ID will be displayed. You must save this information. Immediately after sending the transaction, you must use the command 'get_Tx_Key' in simplewallet to retrieve the transaction secret key. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, 2) the transaction secret key, and 3) the recipient's QWC address. The mediator or arbitrator will then verify the QWC transfer using the QWC Block Explorer (https://explorer.qwertycoin.org).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the QWC sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the QWC Discord (https://discord.gg/rUkfnpC). # suppress inspection "UnusedProperty" account.crypto.popup.drgl.msg=Trading Dragonglass on Haveno requires that you understand and fulfill the following requirements:\n\nBecause of the privacy Dragonglass provides, a transaction is not verifiable on the public blockchain. If required, you can prove your payment through the use of your TXN-Private-Key.\nThe TXN-Private Key is a one-time key automatically generated for every transaction that can only be accessed from within your DRGL wallet.\nEither by DRGL-wallet GUI (inside transaction details dialog) or by the Dragonglass CLI simplewallet (using command "get_tx_key").\n\nDRGL version 'Oathkeeper' and higher are REQUIRED for both.\n\nIn case of a dispute, you must provide the mediator or arbitrator the following data:\n- The TXN-Private key\n- The transaction hash\n- The recipient's public address\n\nVerification of payment can be made using the above data as inputs at (http://drgl.info/#check_txn).\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The Dragonglass sender is responsible for providing verification of the DRGL transfer to the mediator or arbitrator in case of a dispute. Use of PaymentID is not required.\n\nIf you are unsure about any part of this process, visit Dragonglass on Discord (http://discord.drgl.info) for help. # suppress inspection "UnusedProperty" account.crypto.popup.ZEC.msg=When using Zcash you can only use the transparent addresses (starting with t), not the z-addresses (private), because the mediator or arbitrator would not be able to verify the transaction with z-addresses. # suppress inspection "UnusedProperty" account.crypto.popup.XZC.msg=When using Zcoin you can only use the transparent (traceable) addresses, not the untraceable addresses, because the mediator or arbitrator would not be able to verify the transaction with untraceable addresses at a block explorer. # suppress inspection "UnusedProperty" account.crypto.popup.grin.msg=GRIN requires an interactive process between the sender and receiver to create the transaction. Be sure to follow the instructions from the GRIN project web page to reliably send and receive GRIN (the receiver needs to be online or at least be online during a certain time frame). \n\nHaveno supports only the Grinbox (Wallet713) wallet URL format. \n\nThe GRIN sender is required to provide proof that they have sent GRIN successfully. If the wallet cannot provide that proof, a potential dispute will be resolved in favor of the GRIN receiver. Please be sure that you use the latest Grinbox software which supports the transaction proof and that you understand the process of transferring and receiving GRIN as well as how to create the proof. \n\nSee https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only for more information about the Grinbox proof tool. # suppress inspection "UnusedProperty" account.crypto.popup.beam.msg=BEAM requires an interactive process between the sender and receiver to create the transaction. \n\nBe sure to follow the instructions from the BEAM project web page to reliably send and receive BEAM (the receiver needs to be online or at least be online during a certain time frame). \n\nThe BEAM sender is required to provide proof that they sent BEAM successfully. Be sure to use wallet software which can produce such a proof. If the wallet cannot provide the proof a potential dispute will be resolved in favor of the BEAM receiver. # suppress inspection "UnusedProperty" account.crypto.popup.pars.msg=Trading ParsiCoin on Haveno requires that you understand and fulfill the following requirements:\n\nTo send PARS you must use the official ParsiCoin Wallet version 3.0.0 or higher. \n\nYou can Check your Transaction Hash and Transaction Key on Transactions Section on your GUI Wallet (ParsiPay) You need to right Click on the Transaction and then click on show details. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the Transaction Hash, 2) the Transaction Key, and 3) the recipient's PARS address. The mediator or arbitrator will then verify the PARS transfer using the ParsiCoin Block Explorer (http://explorer.parsicoin.net/#check_payment).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the ParsiCoin sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the ParsiCoin Discord (https://discord.gg/c7qmFNh). # suppress inspection "UnusedProperty" account.crypto.popup.blk-burnt.msg=To trade burnt blackcoins, you need to know the following:\n\nBurnt blackcoins are unspendable. To trade them on Haveno, output scripts need to be in the form: OP_RETURN OP_PUSHDATA, followed by associated data bytes which, after being hex-encoded, constitute addresses. For example, burnt blackcoins with an address 666f6f (“foo” in UTF-8) will have the following script:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\nTo create burnt blackcoins, one may use the “burn” RPC command available in some wallets.\n\nFor possible use cases, one may look at https://ibo.laboratorium.ee .\n\nAs burnt blackcoins are unspendable, they can not be reselled. “Selling” burnt blackcoins means burning ordinary blackcoins (with associated data equal to the destination address).\n\nIn case of a dispute, the BLK seller needs to provide the transaction hash. # suppress inspection "UnusedProperty" account.crypto.popup.liquidmonero.msg=Trading L-XMR on Haveno requires that you understand the following:\n\nWhen receiving L-XMR for a trade on Haveno, you cannot use the mobile Blockstream Green Wallet app or a custodial/exchange wallet. You must only receive L-XMR into the Liquid Elements Core wallet, or another L-XMR wallet which allows you to obtain the blinding key for your blinded L-XMR address.\n\nIn the event mediation is necessary, or if a trade dispute arises, you must disclose the blinding key for your receiving L-XMR address to the Haveno mediator or refund agent so they can verify the details of your Confidential Transaction on their own Elements Core full node.\n\nFailure to provide the required information to the mediator or refund agent will result in losing the dispute case. In all cases of dispute, the L-XMR receiver bears 100% of the burden of responsibility in providing cryptographic proof to the mediator or refund agent.\n\nIf you do not understand these requirements, do not trade L-XMR on Haveno. account.traditional.yourTraditionalAccounts=حساب‌های ارزهای ملی شما account.backup.title=کیف پول پشتیبان account.backup.location=محل پشتیبان‌گیری account.backup.selectLocation=انتخاب محل پشتیبان گیری account.backup.backupNow=پشتیبان گیری در حال حاضر (پشتیبان رمزگذاری نشده است!) account.backup.appDir=راهنمای داده های برنامه account.backup.openDirectory=باز کردن راهنما account.backup.openLogFile=باز کردن فایل گزارش account.backup.success=پشتیبان به طور موفقیت آمیز در {0} ذخیره شد. account.backup.directoryNotAccessible=راهنمایی که انتخاب کرده اید، قابل دسترسی نیست. \n{0} account.password.removePw.button=حذف رمز account.password.removePw.headline=حذف رمز محافظ برای کیف پول account.password.setPw.button=تنظیم رمز account.password.setPw.headline=تنظیم رمز محافظ برای کیف پول account.password.info=در صورت فعال بودن حفاظت از رمز عبور، شما باید هنگام راه‌اندازی برنامه، در هنگام برداشت مونرو از کیف پول خود و هنگام نمایش کلمات اصلی نهال خود، رمز عبور خود را وارد کنید. account.seed.backup.title=پشتیبان‌گیری از کلمات انگیزه کیف پول خود account.seed.info=لطفاً همه‌ی کلمات انگیزه کیف پول و تاریخ را یادداشت کنید. شما هر زمان که با کلمات انگیزه و تاریخ آنها را وارد کنید، می‌توانید کیف پول خود را بازیابی کنید.\n\nشما باید کلمات انگیزه را روی یک ورق کاغذ یادداشت کنید. آنها را در کامپیوتر ذخیره نکنید.\n\nلطفاً توجه داشته باشید که کلمات انگیزه جایگزینی برای یک فایل پشتیبان نمی‌باشند.\nشما باید یک نسخه پشتیبان از کل دایرکتوری برنامه از صفحه \"حساب/پشتیبان\" ایجاد کنید تا وضعیت و داده‌های برنامه را بازیابی کنید. account.seed.backup.warning=لطفاً توجه داشته باشید که کلمات انگیزه جایگزینی برای یک فایل پشتیبان نمی‌باشند.\nشما باید یک نسخه پشتیبان از کل دایرکتوری برنامه از صفحه "حساب/پشتیبان" ایجاد کنید تا وضعیت و داده‌های برنامه را بازیابی کنید. account.seed.warn.noPw.msg=شما یک رمز عبور کیف پول تنظیم نکرده اید که از نمایش کلمات رمز خصوصی محافظت کند.\n\nآیا می خواهید کلمات رمز خصوصی نشان داده شود؟ account.seed.warn.noPw.yes=بلی، و دوباره از من نپرس account.seed.enterPw=وارد کردن رمز عبور به منظور مشاهده ی کلمات رمز خصوصی account.seed.restore.info=لطفا قبل از درخواست بازیابی، از کلماتSeed ، یک نسخه پشتیبان تهیه کنید. توجه داشته باشید که بازیابی کیف پول تنها در موارد اضطراری صورت می گیرد و ممکن است منجر به بروز مسائلی در رابطه با پایگاه داده کیف پول داخلی شود.\n این روش، راه مناسبی برای درخواست یک پشتیبان نیست! لطفا از یک پشتیبان دایرکتوری داده برنامه برای بازیابی وضعیت قبلی استفاده کنید.\n\nپس از بازیابی، برنامه به صورت خودکار متوقف می شود. بعد از اینکه مجددا برنامه را اجرا کنید، این برنامه دوباره با شبکه بیت کوین همگام سازی خواهد شد. این کار زمانبر خواهد بود و همچنین تا حد زیادی پردازنده را به کار خواهد گرفت، مخصوصا اگر کیف پول، قدیمی بوده و شامل تراکنش های زیادی باشد. لطفا از ایجاد وقفه برای فرآیند اجتناب کنید. در غیر اینصورت ممکن است که مجددا نیاز به حذف فایل زنجیره SPV و یا تکرار فرآیند بازیابی شوید. account.seed.restore.ok=بسیار خب، Haveno را بازیابی و خاموش نمایید. #################################################################### # Mobile notifications #################################################################### account.notifications.setup.title=آماده سازی account.notifications.download.label=بارگزاری برنامه موبایل account.notifications.waitingForWebCam=منتظر دوربین... account.notifications.webCamWindow.headline=اسکن کد QR از طریق تلفن account.notifications.webcam.label=استفاده از دوربین account.notifications.webcam.button=اسکن کد QR account.notifications.noWebcam.button=دوربین ندارم account.notifications.erase.label=پاک کردن اعلان‌ها در تلفن account.notifications.erase.title=پاک کردن اعلان‌ها account.notifications.email.label=توکن جفت سازی account.notifications.email.prompt=توکن جفت سازی که از طریق ایمیل دریافت کرده‌اید را وارد کنید account.notifications.settings.title=تنظیمات account.notifications.useSound.label=پخش کردن صدای اعلان روی تلفن account.notifications.trade.label=دریافت پیام‌های معامله account.notifications.market.label=دریافت هشدارهای مربوط به پیشنهادها account.notifications.price.label=دریافت هشدارهای مربوط به قیمت account.notifications.priceAlert.title=هشدارهای قیمت account.notifications.priceAlert.high.label=با خبر کردن در صورتی که قیمت XMR بالاتر باشد account.notifications.priceAlert.low.label=با خبر کردن در صورتی که قیمت XMR پایین‌تر باشد account.notifications.priceAlert.setButton=تنظیم هشدار قیمت account.notifications.priceAlert.removeButton=حذف هشدار قیمت account.notifications.trade.message.title=تغییر وضعیت معامله account.notifications.trade.message.msg.conf=تراکنش سپرده برای معامله با شناسه {0} تایید شده است. لطفا برنامه Haveno خود را بازکنید و پرداخت را شروع کنید. account.notifications.trade.message.msg.started=خریدار XMR پرداخت با شناسه {0} را آغاز کرده است. account.notifications.trade.message.msg.completed=معامله با شناسه {0} انجام شد. account.notifications.offer.message.title=پیشنهاد شما پذیرفته شد account.notifications.offer.message.msg=پیشنهاد شما با شناسه {0} پذیرفته شد account.notifications.dispute.message.title=پیغام جدید مربوط به اختلاف account.notifications.dispute.message.msg=شما یک پیغام مرتبط با اختلاف برای معامله با شناسه {0} دریافت کردید account.notifications.marketAlert.title=هشدارهای مربوط به پیشنهادها account.notifications.marketAlert.selectPaymentAccount=پیشنهادهای مرتبط با حساب پرداخت account.notifications.marketAlert.offerType.label=نوع پیشنهادهایی که من به آنها علاقمندم account.notifications.marketAlert.offerType.buy=پیشنهادهای خرید (می‌خواهم XMR بفروشم) account.notifications.marketAlert.offerType.sell=پیشنهادهای فروش (می‌خواهم XMR بخرم) account.notifications.marketAlert.trigger=فاصله قیمتی پیشنهاد (%) account.notifications.marketAlert.trigger.info=با تنظیم یک فاصله قیمتی، تنها در صورتی هشدار دریافت می‌کنید که پیشنهادی با پیشنیازهای شما (یا بهتر از آن) منتشر بشود. برای مثال: شما می‌خواهید XMR بفروشید، ولی می‌خواهید با 2% حق صراف نسبت به قیمت بازار آن را بفروشید. تنظیم این فیلد روی 2% به شما این اطمینان را می‌دهد که تنها بابت پیشنهادهایی هشدار دریافت کنید که حداقل 2% (یا بیشتر) بالای قیمت فعلی بازار هستند. account.notifications.marketAlert.trigger.prompt=درصد فاصله از قیمت بازار (برای مثال 2.50%, -0.50%) account.notifications.marketAlert.addButton=اضافه کردن هشدار برای پیشنهادها account.notifications.marketAlert.manageAlertsButton=مدیریت هشدارهای مربوط به پیشنهادها account.notifications.marketAlert.manageAlerts.title=مدیریت هشدارهای مربوط به پیشنهادها account.notifications.marketAlert.manageAlerts.header.paymentAccount=حساب پرداخت account.notifications.marketAlert.manageAlerts.header.trigger=قیمت نشان‌شده account.notifications.marketAlert.manageAlerts.header.offerType=نوع پیشنهاد account.notifications.marketAlert.message.title=هشدار پیشنهاد account.notifications.marketAlert.message.msg.below=پایین account.notifications.marketAlert.message.msg.above=بالای account.notifications.marketAlert.message.msg=پیشنهاد جدید '{0} {1}' با قیمت {2} ({3} {4} قیمت بازار) و روش پرداخت '{5}' در دفتر پیشنهادات Haveno منتشر شده است.\nشناسه پیشنهاد: {6}. account.notifications.priceAlert.message.title=هشدار قیمت برای {0} account.notifications.priceAlert.message.msg=هشدار قیمت شما فعال شده است. قیمت {0} فعلی {1} {2} است account.notifications.noWebCamFound.warning=دوبین پیدا نشد.\n\nلطفا از گزینه ایمیل برای ارسال توکن و کلید رمزنگاری از تلفن همراهتان به برنامه Haveno استفاده کنید. account.notifications.priceAlert.warning.highPriceTooLow=قیمت بالاتر باید از قیمت پایین‌تر بزرگتر باشد. account.notifications.priceAlert.warning.lowerPriceTooHigh=قیمت پایین‌تر باید از قیمت بالاتر کوچکتر باشد. #################################################################### # Windows #################################################################### inputControlWindow.headline=Select inputs for transaction inputControlWindow.balanceLabel=موجودی در دسترس contractWindow.title=جزئیات مناقشه contractWindow.dates=تاریخ پیشنهاد / تاریخ معامله contractWindow.xmrAddresses=آدرس بیت‌کوین خریدار XMR / فروشنده XMR contractWindow.onions=آدرس شبکه خریدار XMR / فروشنده XMR contractWindow.accountAge=Account age XMR buyer / XMR seller contractWindow.numDisputes=تعداد اختلافات خریدار XMR / فروشنده XMR contractWindow.contractHash=هش قرارداد displayAlertMessageWindow.headline=اطلاعات مهم! displayAlertMessageWindow.update.headline=اطلاعات به روز مهم! displayAlertMessageWindow.update.download=دانلود: displayUpdateDownloadWindow.downloadedFiles=فایل ها: displayUpdateDownloadWindow.downloadingFile=در حال دانلود: {0} displayUpdateDownloadWindow.verifiedSigs=امضا با کلیدها تایید شده است: displayUpdateDownloadWindow.status.downloading=در حال دانلود فایل ها... displayUpdateDownloadWindow.status.verifying=در حال اعتبارسنجی امضا... displayUpdateDownloadWindow.button.label=دانلود نصب کننده و تأیید امضا displayUpdateDownloadWindow.button.downloadLater=بعداً دانلود کن displayUpdateDownloadWindow.button.ignoreDownload=نادیده گرفتن این نسخه displayUpdateDownloadWindow.headline=یک به روز رسانی جدید برای Haveno موجود است! displayUpdateDownloadWindow.download.failed.headline=دانلود ناموفق بود displayUpdateDownloadWindow.download.failed=Download failed.\nPlease download and verify manually at [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.installer.failed=Unable to determine the correct installer. Please download and verify manually at [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.verify.failed=Verification failed.\nPlease download and verify manually at [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.success=نسخه ی جدید به طور موفقیت آمیز دانلود و امضا تأیید شد.\n\nلطفاً راهنمای دانلود را باز کرده، برنامه را ببندید و نسخه ی جدید را نصب نمایید. displayUpdateDownloadWindow.download.openDir=باز کردن راهنمای دانلود disputeSummaryWindow.title=خلاصه disputeSummaryWindow.openDate=تاریخ ایجاد تیکت disputeSummaryWindow.role=نقش معامله گر disputeSummaryWindow.payout=پرداختی مقدار معامله disputeSummaryWindow.payout.getsTradeAmount=XMR {0} پرداختی مبلغ معامله را دریافت می کند disputeSummaryWindow.payout.getsAll=Max. payout to XMR {0} disputeSummaryWindow.payout.custom=پرداخت سفارشی disputeSummaryWindow.payoutAmount.buyer=مقدار پرداختی خریدار disputeSummaryWindow.payoutAmount.seller=مقدار پرداختی فروشنده disputeSummaryWindow.payoutAmount.invert=استفاده از بازنده به عنوان منتشر کننده disputeSummaryWindow.reason=دلیل اختلاف disputeSummaryWindow.tradePeriodEnd=Trade period end disputeSummaryWindow.extraInfo=Extra information disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BUG=اشکال # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.USABILITY=قابلیت استفاده # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PROTOCOL_VIOLATION=نقض پروتکل # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.NO_REPLY=بدون پاسخ # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SCAM=کلاهبرداری # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OTHER=سایر # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BANK_PROBLEMS=بانک # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OPTION_TRADE=Option trade # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Trader not responding # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled disputeSummaryWindow.summaryNotes=نکات خلاصه disputeSummaryWindow.addSummaryNotes=افزودن نکات خلاصه disputeSummaryWindow.close.button=بستن تیکت # Do no change any line break or order of tokens as the structure is used for signature verification # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.msg=Ticket closed on {0}\n{1} node address: {2}\n\nSummary:\nTrade ID: {3}\nCurrency: {4}\nTrade amount: {5}\nPayout amount for XMR buyer: {6}\nPayout amount for XMR seller: {7}\n\nReason for dispute: {8}\n\nSummary notes:\n{9}\n # Do no change any line break or order of tokens as the structure is used for signature verification disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3} disputeSummaryWindow.close.nextStepsForMediation=\nNext steps:\nOpen trade and accept or reject suggestion from mediator disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\nNo further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions disputeSummaryWindow.close.closePeer=شما باید همچنین تیکت همتایان معامله را هم ببندید! disputeSummaryWindow.close.txDetails.headline=Publish refund transaction # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.buyer=Buyer receives {0} on address: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=Seller receives {0} on address: {1}\n disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3}\n\nAre you sure you want to publish this transaction? disputeSummaryWindow.close.noPayout.headline=Close without any payout disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? emptyWalletWindow.headline=ابزار اضطراری کیف پول {0} emptyWalletWindow.info=لطفاً تنها در مورد اضطراری از آن استفاده کنید اگر نمی توانید به وجه خود از UI دسترسی داشته باشید.\n\nلطفاً توجه داشته باشید که تمام معاملات باز به طور خودکار در هنگام استفاده از این ابزار، بسته خواهد شد.\n\nقبل از به کار گیری این ابزار، از راهنمای داده ی خود پشتیبان بگیرید. می توانید این کار را در \"حساب/پشتیبان\" انجام دهید.\n\nلطفاً مشکل خود را به ما گزارش کنید و گزارش مشکل را در GitHub یا تالار گفتگوی Haveno بایگانی کنید تا ما بتوانیم منشأ مشکل را بررسی نماییم. emptyWalletWindow.balance=موجودی در دسترس کیف‌پول شما emptyWalletWindow.address=آدرس مقصد شما emptyWalletWindow.button=ارسال تمام وجوه emptyWalletWindow.openOffers.warn=شما معاملات بازی دارید که اگر کیف پول را خالی کنید، حذف خواهند شد.\nآیا شما مطمئن هستید که می خواهید کیف پول را خالی کنید؟ emptyWalletWindow.openOffers.yes=بله، مطمئن هستم emptyWalletWindow.sent.success=تراز کیف پول شما به طور موفقیت آمیز منتقل شد. enterPrivKeyWindow.headline=Enter private key for registration filterWindow.headline=ویرایش لیست فیلتر filterWindow.offers=پیشنهادهای فیلتر شده (جدا شده با ویرگول) filterWindow.onions=Banned from trading addresses (comma sep.) filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.) filterWindow.accounts=داده های حساب معاملاتی فیلترشده:\nفرمت: لیست جدا شده با ویرگول [شناسه روش پرداخت، زمینه داده، ارزش] filterWindow.bannedCurrencies=کدهای ارز فیلترشده (جدا شده با ویرگول) filterWindow.bannedPaymentMethods=شناسه‌های روش پرداخت فیلتر شده (جدا شده با ویرگول) filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) filterWindow.arbitrators=داوران فیلتر شده (آدرس های Onion جدا شده با ویرگول) filterWindow.mediators=Filtered mediators (comma sep. onion addresses) filterWindow.refundAgents=Filtered refund agents (comma sep. onion addresses) filterWindow.seedNode=گره های seed فیلتر شده (آدرس های Onion جدا شده با ویرگول) filterWindow.priceRelayNode=گره های رله قیمت فیلترشده (آدرس های Onion جدا شده با ویرگول) filterWindow.xmrNode=گره‌های بیت‌کوین فیلترشده (آدرس + پورت جدا شده با ویرگول) filterWindow.preventPublicXmrNetwork=جلوگیری از استفاده ازشبکه عمومی بیت‌کوین filterWindow.disableAutoConf=Disable auto-confirm filterWindow.autoConfExplorers=Filtered auto-confirm explorers (comma sep. addresses) filterWindow.disableTradeBelowVersion=Min. version required for trading filterWindow.add=افزودن فیلتر filterWindow.remove=حذف فیلتر filterWindow.xmrFeeReceiverAddresses=XMR fee receiver addresses filterWindow.disableApi=Disable API filterWindow.disableMempoolValidation=Disable Mempool Validation offerDetailsWindow.minXmrAmount=حداقل مقدار XMR offerDetailsWindow.min=(حداقل {0}) offerDetailsWindow.distance=(فاصله از قیمت روز بازار: {0}) offerDetailsWindow.myTradingAccount=حساب معاملاتی من offerDetailsWindow.offererBankId=(/BIC/SWIFT/شناسه بانک سفارش گذار) offerDetailsWindow.offerersBankName=(نام بانک سفارش گذار) offerDetailsWindow.bankId=شناسه بانک (برای مثال BIC یا SWIFT) offerDetailsWindow.countryBank=کشور بانک سفارش‌گذار offerDetailsWindow.commitment=تعهد offerDetailsWindow.agree=من موافقم offerDetailsWindow.tac=شرایط و الزامات offerDetailsWindow.confirm.maker.buy=تأیید: ایجاد پیشنهاد خرید XMR با {0} offerDetailsWindow.confirm.maker.sell=تأیید: ایجاد پیشنهاد فروش XMR به ازای {0} offerDetailsWindow.confirm.taker.buy=تأیید: قبول پیشنهاد خرید XMR با {0} offerDetailsWindow.confirm.taker.sell=تأیید: قبول پیشنهاد فروش XMR به ازای {0} offerDetailsWindow.creationDate=تاریخ ایجاد offerDetailsWindow.makersOnion=آدرس Onion سفارش گذار offerDetailsWindow.challenge=Passphrase de l'offre offerDetailsWindow.challenge.copy=عبارت عبور را برای به اشتراک‌گذاری با همتا کپی کنید qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. qRCodeWindow.request=درخواست پرداخت:\n{0} selectDepositTxWindow.headline=تراکنش سپرده را برای مناقشه انتخاب کنید selectDepositTxWindow.msg=تراکنش سپرده در معامله ذخیره نشده بود.\nلطفاً یکی از تراکنش های چندامضایی موجود از کیف پول خود را انتخاب کنید که تراکنش سپرده در معامله ی ناموفق، استفاده شده بود.\n\nشما می توانید تراکنش صحیح را با باز کردن پنجره جزئیات معامله (با کلیک بر روی شناسه معامله در لیست) و دنبال کردن خروجی تراکنش پرداخت هزینه معامله تا تراکنش بعدی که در آن شما می توانید تراکنش سپرده ی چند امضایی را ببینید (آدرس با 3 شروع می شود)، پیدا کنید. آن شناسه تراکنش باید در لیست ارائه شده در اینجا قابل مشاهده باشد. هنگامی که تراکنش صحیح را یافتید، آن تراکنش را در اینجا انتخاب نموده و ادامه دهید.\n\nبا عرض پوزش برای این مشکل، اما این خطا ندرتاً رخ دهد و در آینده ما سعی خواهیم کرد راه های بهتری برای حل آن پیدا کنیم. selectDepositTxWindow.select=انتخاب تراکنش سپرده sendAlertMessageWindow.headline=ارسال اطلاع رسانی جهانی sendAlertMessageWindow.alertMsg=پیام هشدار sendAlertMessageWindow.enterMsg=وارد کردن پیام sendAlertMessageWindow.isSoftwareUpdate=Software download notification sendAlertMessageWindow.isUpdate=Is full release sendAlertMessageWindow.isPreRelease=Is pre-release sendAlertMessageWindow.version=شماره نسخه جدید sendAlertMessageWindow.send=ارسال اطلاع رسانی sendAlertMessageWindow.remove=حذف اطلاع رسانی sendPrivateNotificationWindow.headline=ارسال پیام اختصاصی sendPrivateNotificationWindow.privateNotification=اعلان خصوصی sendPrivateNotificationWindow.enterNotification=وارد کردن اعلان sendPrivateNotificationWindow.send=ارسال اطلاع رسانی خصوصی showWalletDataWindow.walletData=داده های کیف پول showWalletDataWindow.includePrivKeys=شامل کلیدهای خصوصی setXMRTxKeyWindow.headline=Prove sending of XMR setXMRTxKeyWindow.note=Adding tx info below enables auto-confirm for quicker trades. See more: https://haveno.exchange/wiki/Trading_Monero setXMRTxKeyWindow.txHash=Transaction ID (optional) setXMRTxKeyWindow.txKey=Transaction key (optional) # We do not translate the tac because of the legal nature. We would need translations checked by lawyers # in each language which is too expensive atm. tacWindow.headline=موافقتنامه کاربر tacWindow.agree=من موافقم tacWindow.disagree=من مخالفم و خارج می شوم tacWindow.arbitrationSystem=Dispute resolution tradeDetailsWindow.headline=معامله tradeDetailsWindow.disputedPayoutTxId=شناسه تراکنش پرداختی مورد مناقشه: tradeDetailsWindow.tradeDate=تاریخ معامله tradeDetailsWindow.txFee=کارمزد استخراج tradeDetailsWindow.tradePeersOnion=آدرس Onion همتایان معامله: tradeDetailsWindow.tradePeersPubKeyHash=Trading peers pubkey hash tradeDetailsWindow.tradeState=وضعیت معامله tradeDetailsWindow.agentAddresses=Arbitrator/Mediator tradeDetailsWindow.detailData=Detail data txDetailsWindow.headline=Transaction Details txDetailsWindow.xmr.noteSent=شما XMR ارسال کرده‌اید. txDetailsWindow.xmr.noteReceived=شما XMR دریافت کرده‌اید. txDetailsWindow.sentTo=ارسال به txDetailsWindow.receivedWith=دریافت با txDetailsWindow.txId=TxId closedTradesSummaryWindow.headline=Trade history summary closedTradesSummaryWindow.totalAmount.title=Total trade amount closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) closedTradesSummaryWindow.totalTradeFeeInXmr.title=Sum of all trade fees paid in XMR closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} of total trade amount) walletPasswordWindow.headline=وارد کردن رمز عبور به منظور باز کردن xmrConnectionError.headline=خطای اتصال به Monero xmrConnectionError.providedNodes=اتصال به گره(های) Monero ارائه‌شده با خطا مواجه شد.\n\nآیا می‌خواهید از بهترین گره Monero بعدی استفاده کنید؟ xmrConnectionError.customNodes=اتصال به گره(های) Monero سفارشی شما با خطا مواجه شد.\n\nآیا می‌خواهید از بهترین گره Monero بعدی استفاده کنید؟ xmrConnectionError.localNode=Haveno قبلاً به یک گره محلی Monero متصل بود، اما اکنون در دسترس نیست.\n\nاطمینان حاصل کنید که گره محلی شما در حال اجرا و کاملاً همگام‌سازی شده است، یا گزینه دیگری را برای ادامه انتخاب کنید. xmrConnectionError.localNode.start=راه‌اندازی گره محلی xmrConnectionError.localNode.start.error=خطا در راه‌اندازی گره محلی xmrConnectionError.localNode.fallback=اتصال به بهترین گره بعدی torNetworkSettingWindow.header=تنظیمات شبکه Tor  torNetworkSettingWindow.noBridges=از پل ها استفاده نکنید torNetworkSettingWindow.providedBridges=اتصال با پل های ارائه شده torNetworkSettingWindow.customBridges=وارد کردن پل های سفارشی torNetworkSettingWindow.transportType=نوع انتقال torNetworkSettingWindow.obfs3=obfs3 torNetworkSettingWindow.obfs4=obfs4 (توصیه شده) torNetworkSettingWindow.meekAmazon=meek-amazon torNetworkSettingWindow.meekAzure=meek-azure torNetworkSettingWindow.enterBridge=یک یا چند رله وارد کنید (یکی در هر خط) torNetworkSettingWindow.enterBridgePrompt=نوع آدرس:پورت torNetworkSettingWindow.restartInfo=برای اعمال تغییرات باید برنامه را مجدداً راه اندازی کنید. torNetworkSettingWindow.openTorWebPage=صفحه وب پروژه ی Tor را باز کنید torNetworkSettingWindow.deleteFiles.header=مشکلاتی برای اتصال وجود دارد؟ torNetworkSettingWindow.deleteFiles.info=اگر برای اتصال در راه اندازی مشکلات مکرر دارید، حذف فایل های قدیمی Tor می تواند گره گشا باشد. برای انجام آن، روی دکمه زیر کلیک کرده و پس از آن برنامه را مجدداً راه اندازی کنید. torNetworkSettingWindow.deleteFiles.button=حذف فایل های قدیمی Tor و خاموش کردن torNetworkSettingWindow.deleteFiles.progress=خاموش کردن Tor در حال انجام است torNetworkSettingWindow.deleteFiles.success=حذف فایل های قدیمی Tor با موفقیت انجام شد. لطفاً مجدداً راه اندازی کنید. torNetworkSettingWindow.bridges.header=آیا Tor مسدود شده است؟ torNetworkSettingWindow.bridges.info=اگر Tor توسط ارائه دهنده اینترنت شما یا توسط کشور شما مسدود شده است، شما می توانید از پل های Tor استفاده کنید.\nاز صفحه وب Tor در https://bridges.torproject.org دیدن کنید تا مطالب بیشتری در مورد پل ها و نقل و انتقالات قابل اتصال یاد بگیرید. feeOptionWindow.headline=انتخاب ارز برای پرداخت هزینه معامله feeOptionWindow.info=شما می توانید انتخاب کنید که هزینه معامله را در BSQ یا در XMR بپردازید. اگر BSQ را انتخاب می کنید، از تخفیف هزینه معامله برخوردار می شوید. feeOptionWindow.optionsLabel=انتخاب ارز برای پرداخت کارمزد معامله feeOptionWindow.useXMR=استفاده از XMR feeOptionWindow.fee={0} (≈ {1}) feeOptionWindow.xmrFeeWithFiatAndPercentage={0} (≈ {1} / {2}) feeOptionWindow.xmrFeeWithPercentage={0} ({1}) #################################################################### # Popups #################################################################### popup.headline.notification=اطلاع رسانی popup.headline.instruction=لطفاً درنظر داشته باشید: popup.headline.attention=توجه popup.headline.backgroundInfo=اطلاعات پس زمینه popup.headline.feedback=تکمیل شده popup.headline.confirmation=تائیدیه popup.headline.information=اطلاعات popup.headline.warning=هشدار popup.headline.error=خطا popup.doNotShowAgain=دوباره نشان نده popup.reportError.log=باز کردن فایل گزارش popup.reportError.gitHub=گزارش به پیگیر مسائل GitHub  popup.reportError={0}\n\nTo help us to improve the software please report this bug by opening a new issue at https://github.com/haveno-dex/haveno/issues.\nThe above error message will be copied to the clipboard when you click either of the buttons below.\nIt will make debugging easier if you include the haveno.log file by pressing "Open log file", saving a copy, and attaching it to your bug report. popup.error.tryRestart=لطفاً سعی کنید برنامه را مجدداً راه اندازی کنید و اتصال شبکه خود را بررسی کنید تا ببینید آیا می توانید مشکل را حل کنید یا خیر. popup.error.takeOfferRequestFailed=وقتی کسی تلاش کرد تا یکی از پیشنهادات شما را بپذیرد خطایی رخ داد:\n{0} error.spvFileCorrupted=هنگام خواندن فایل زنجیره SPV خطایی رخ داد.\nممکن است فایل زنجیره SPV خراب شده باشد.\n\nپیغام خطا: {0}\n\nآیا می خواهید آن را حذف کنید و یک همگام سازی را شروع نمایید؟ error.deleteAddressEntryListFailed=قادر به حذف فایل AddressEntryList نیست.\nخطا: {0} error.closedTradeWithUnconfirmedDepositTx=The deposit transaction of the closed trade with the trade ID {0} is still unconfirmed.\n\nPlease do a SPV resync at \"Setting/Network info\" to see if the transaction is valid. error.closedTradeWithNoDepositTx=The deposit transaction of the closed trade with the trade ID {0} is null.\n\nPlease restart the application to clean up the closed trades list. popup.warning.walletNotInitialized=کیف پول هنوز راه اندازی اولیه نشده است popup.warning.osxKeyLoggerWarning=Due to stricter security measures in macOS 10.14 and above, launching a Java application (Haveno uses Java) causes a popup warning in macOS ('Haveno would like to receive keystrokes from any application').\n\nTo avoid that issue please open your 'macOS Settings' and go to 'Security & Privacy' -> 'Privacy' -> 'Input Monitoring' and Remove 'Haveno' from the list on the right side.\n\nHaveno will upgrade to a newer Java version to avoid that issue as soon the technical limitations (Java packager for the required Java version is not shipped yet) are resolved. popup.warning.wrongVersion=شما احتمالاً نسخه اشتباه Haveno را برای این رایانه دارید.\nمعماری کامپیوتر شما این است: {0}.\nباینری Haveno که شما نصب کرده اید،عبارت است از: {1}.\nلطفاً نسخه فعلی را خاموش کرده و مجدداً نصب نمایید ({2}). popup.warning.incompatibleDB=We detected incompatible data base files!\n\nThose database file(s) are not compatible with our current code base:\n{0}\n\nWe made a backup of the corrupted file(s) and applied the default values to a new database version.\n\nThe backup is located at:\n{1}/db/backup_of_corrupted_data.\n\nPlease check if you have the latest version of Haveno installed.\nYou can download it at: [HYPERLINK:https://haveno.exchange/downloads].\n\nPlease restart the application. popup.warning.startupFailed.twoInstances=Haveno در حال اجرا است. شما نمیتوانید دو نمونه از Haveno را اجرا کنید. popup.warning.tradePeriod.halfReached=معامله شما با شناسه {0} نیمی از حداکثر مجاز دوره زمانی معامله را به پایان رسانده و هنوز کامل نشده است. \n\nدوره معامله در {1} به پایان می رسد\n\n لطفا وضعیت معامله خود را در \"سبد سهام/معاملات باز\" برای اطلاعات بیشتر، بررسی کنید. popup.warning.tradePeriod.ended=Your trade with ID {0} has reached the max. allowed trading period and is not completed.\n\nThe trade period ended on {1}\n\nPlease check your trade at \"Portfolio/Open trades\" for contacting the arbitrator. popup.warning.noTradingAccountSetup.headline=شما یک حساب معاملاتی را راه اندازی نکرده اید popup.warning.noTradingAccountSetup.msg=قبل از اینکه بتوانید یک پیشنهاد ایجاد کنید، باید یک ارز ملی یا حساب کاربری آلت کوین را تنظیم کنید. \nآیا می خواهید یک حساب کاربری را راه اندازی کنید؟ popup.warning.noArbitratorsAvailable=هیچ داوری در دسترس نیست. popup.warning.noMediatorsAvailable=There are no mediators available. popup.warning.notFullyConnected=شما باید منتظر بمانید تا به طور کامل به شبکه متصل شوید. \nاین ممکن است در هنگام راه اندازی حدود 2 دقیقه طول بکشد. popup.warning.notSufficientConnectionsToXmrNetwork=شما باید منتظر بمانید تا حداقل {0} اتصال به شبکه بیتکوین داشته باشید. popup.warning.downloadNotComplete=شما باید منتظر بمانید تا بارگیری بلاک های بیتکوین باقیمانده کامل شود. popup.warning.walletNotSynced=کیف پول Haveno با ارتفاع آخرین بلوکچین هماهنگ نشده است. لطفاً منتظر بمانید تا کیف پول هماهنگ شود یا اتصال خود را بررسی کنید. popup.warning.removeOffer=آیا شما مطمئن هستید که می خواهید این پیشنهاد را حذف کنید؟ popup.warning.tooLargePercentageValue=شما نمیتوانید درصد 100٪ یا بیشتر را تنظیم کنید. popup.warning.examplePercentageValue=لطفا یک عدد درصد مانند \"5.4\" برای 5.4% وارد کنید popup.warning.noPriceFeedAvailable=برای این ارز هیچ خوراک قیمتی وجود ندارد. شما نمیتوانید از یک درصد بر اساس قیمت استفاده کنید. \nلطفا قیمت مقطوع را انتخاب کنید. popup.warning.sendMsgFailed=ارسال پیام به شریک معاملاتی شما ناموفق بود. \nلطفا دوباره امتحان کنید و اگر همچنان ناموفق بود، گزارش یک اشکال را ارسال کنید. popup.warning.messageTooLong=پیام شما بیش از حداکثر اندازه مجاز است. لطفا آن را در چند بخش ارسال کنید یا آن را در یک سرویس مانند https://pastebin.com آپلود کنید. popup.warning.lockedUpFunds=You have locked up funds from a failed trade.\nLocked up balance: {0} \nDeposit tx address: {1}\nTrade ID: {2}.\n\nPlease open a support ticket by selecting the trade in the open trades screen and pressing \"alt + o\" or \"option + o\"." popup.warning.moneroConnection=مشکلی در اتصال به شبکه مونرو رخ داده است.\n\n{0} popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n takeOffer.cancelButton=Cancel take-offer takeOffer.warningButton=Ignore and continue anyway # suppress inspection "UnusedProperty" popup.warning.nodeBanned=One of the {0} nodes got banned. # suppress inspection "UnusedProperty" popup.warning.priceRelay=رله قیمت popup.warning.seed=دانه popup.warning.mandatoryUpdate.trading=Please update to the latest Haveno version. A mandatory update was released which disables trading for old versions. Please check out the Haveno Forum for more information. popup.warning.noFilter=ما شیء فیلتر را از گره‌های اولیه دریافت نکردیم. لطفاً به مدیران شبکه اطلاع دهید که یک شیء فیلتر ثبت کنند. popup.warning.burnXMR=This transaction is not possible, as the mining fees of {0} would exceed the amount to transfer of {1}. Please wait until the mining fees are low again or until you've accumulated more XMR to transfer. popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Monero network.\nTransaction ID={1}.\nThe offer has been removed to avoid further problems.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. popup.warning.trade.txRejected.tradeFee=trade fee popup.warning.trade.txRejected.deposit=deposit popup.warning.trade.txRejected=The {0} transaction for trade with ID {1} was rejected by the Monero network.\nTransaction ID={2}\nThe trade has been moved to failed trades.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. popup.warning.openOfferWithInvalidMakerFeeTx=The maker fee transaction for offer with ID {0} is invalid.\nTransaction ID={1}.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. popup.info.securityDepositInfo=برای اطمینان از اینکه هر دو معامله گر پروتکل معامله را رعایت می‌کنند، هر دو معامله گر باید مبلغی را تحت عنوان سپرده اطمینان پرداخت کنند.\n\nاین سپرده در کیف‌پول معامله شما نگهداری می‌شود و زمانی که معامله شما با موفقیت انجام شد به خود شما بازگردانده خواهد شد.\n\nلطفا توجه کنید: اگر می‌خواهید یک پیشنهاد جدید ایجاد کنید، Haveno باید برای در سمت معامله دیگر اجرا باشد تا بتوانند آن را بپذیرد. برای اینکه پیشنهادات شما برخط بمانند، بگذارید Haveno در حال اجرابماند و همچنین مطمئن شوید که این کامپیوتر به اینترنت متصل است. (به عنوان مثال مطمئن شوید که به حالت آماده باش نمی‌رود.. البته حالت آماده باش برای نمایشگر ایرادی ندارد). popup.info.cashDepositInfo=لطفا مطمئن شوید که شما یک شعبه بانک در منطقه خود دارید تا بتوانید سپرده نقدی را بپردازید. شناسه بانکی (BIC/SWIFT) بانک فروشنده: {0}. popup.info.cashDepositInfo.confirm=تأیید می کنم که می توانم سپرده را ایجاد کنم popup.info.shutDownWithOpenOffers=Haveno در حال خاموش شدن است ولی پیشنهاداتی وجود دارند که باز هستند.\n\nزمانی که Haveno بسته باشد این پیشنهادات در شبکه P2P در دسترس نخواهند بود، ولی هر وقت دوباره Haveno را باز کنید این پیشنهادات دوباره در شبکه P2P منتشر خواهند شد.\n\n برای اینکه پیشنهادات شما برخط بمانند، بگذارید Haveno در حال اجرابماند و همچنین مطمئن شوید که این کامپیوتر به اینترنت متصل است. (به عنوان مثال مطمئن شوید که به حالت آماده باش نمی‌رود.. البته حالت آماده باش برای نمایشگر ایرادی ندارد). popup.info.qubesOSSetupInfo=It appears you are running Haveno on Qubes OS. \n\nPlease make sure your Haveno qube is setup according to our Setup Guide at [HYPERLINK:https://haveno.exchange/wiki/Running_Haveno_on_Qubes]. popup.warn.downGradePrevention=Downgrade from version {0} to version {1} is not supported. Please use the latest Haveno version. popup.privateNotification.headline=اعلان خصوصی مهم! popup.securityRecommendation.headline=توصیه امنیتی مهم popup.securityRecommendation.msg=ما می خواهیم به شما یادآوری کنیم که استفاده از رمز محافظت برای کیف پول خود را در نظر بگیرید اگر از قبل آن را فعال نکرده اید.\n\nهمچنین شدیداً توصیه می شود که کلمات رمز خصوصی کیف پول را بنویسید. این کلمات رمز خصوصی مانند یک رمزعبور اصلی برای بازیابی کیف پول بیتکوین شما هستند. \nدر قسمت \"کلمات رمز خصوصی کیف پول\" اطلاعات بیشتری کسب می کنید.\n\n علاوه بر این شما باید از پوشه داده های کامل نرم افزار در بخش \"پشتیبان گیری\" پشتیبان تهیه کنید. popup.shutDownInProgress.headline=خاموش شدن در حال انجام است popup.shutDownInProgress.msg=خاتمه دادن به برنامه می تواند چند ثانیه طول بکشد.\n لطفا این روند را قطع نکنید. popup.attention.forTradeWithId=توجه الزامی برای معامله با شناسه {0} popup.attention.reasonForPaymentRuleChange=Version 1.5.5 introduces a critical trade rule change regarding the \"reason for payment\" field in bank transfers. Please leave this field empty -- DO NOT use the trade ID as \"reason for payment\" anymore. popup.info.multiplePaymentAccounts.headline=Multiple payment accounts available popup.info.multiplePaymentAccounts.msg=You have multiple payment accounts available for this offer. Please make sure you've picked the right one. popup.accountSigning.selectAccounts.headline=Select payment accounts popup.accountSigning.selectAccounts.description=Based on the payment method and point of time all payment accounts that are connected to a dispute where a payout to the buyer occurred will be selected for you to sign. popup.accountSigning.selectAccounts.signAll=Sign all payment methods popup.accountSigning.selectAccounts.datePicker=Select point of time until which accounts will be signed popup.accountSigning.confirmSelectedAccounts.headline=Confirm selected payment accounts popup.accountSigning.confirmSelectedAccounts.description=Based on your input, {0} payment accounts will be selected. popup.accountSigning.confirmSelectedAccounts.button=Confirm payment accounts popup.accountSigning.signAccounts.headline=Confirm signing of payment accounts popup.accountSigning.signAccounts.description=Based on your selection, {0} payment accounts will be signed. popup.accountSigning.signAccounts.button=Sign payment accounts popup.accountSigning.signAccounts.ECKey=Enter private arbitrator key popup.accountSigning.signAccounts.ECKey.error=Bad arbitrator ECKey popup.accountSigning.success.headline=Congratulations popup.accountSigning.success.description=All {0} payment accounts were successfully signed! popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\nFor further information, please visit [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer's account after a successful trade.\n\n{0} popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you'll be able to sign other accounts in {0} days from now.\n\n{1} popup.accountSigning.peerLimitLifted=The initial limit for one of your accounts has been lifted.\n\n{0} popup.accountSigning.peerSigner=One of your accounts is mature enough to sign other payment accounts and the initial limit for one of your accounts has been lifted.\n\n{0} popup.accountSigning.singleAccountSelect.headline=Import unsigned account age witness popup.accountSigning.confirmSingleAccount.headline=Confirm selected account age witness popup.accountSigning.confirmSingleAccount.selectedHash=Selected witness hash popup.accountSigning.confirmSingleAccount.button=Sign account age witness popup.accountSigning.successSingleAccount.description=Witness {0} was signed popup.accountSigning.successSingleAccount.success.headline=Success popup.accountSigning.unsignedPubKeys.headline=Unsigned Pubkeys popup.accountSigning.unsignedPubKeys.sign=Sign Pubkeys popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign popup.info.buyerAsTakerWithoutDeposit.headline=هیچ پیش‌پرداختی از خریدار مورد نیاز نیست popup.info.buyerAsTakerWithoutDeposit=پیشنهاد شما نیاز به ودیعه امنیتی یا هزینه از خریدار XMR ندارد.\n\nبرای پذیرفتن پیشنهاد شما، باید یک پس‌عبارت را با شریک تجاری خود خارج از Haveno به اشتراک بگذارید.\n\nپس‌عبارت به‌طور خودکار تولید می‌شود و پس از ایجاد در جزئیات پیشنهاد نمایش داده می‌شود. #################################################################### # Notifications #################################################################### notification.trade.headline=اعلان برای معامله با شناسه {0} notification.ticket.headline=تیکت پشتیبانی برای معامله با شناسه {0} notification.trade.completed=معامله اکنون کامل شده است و می توانید وجوه خود را برداشت کنید. notification.trade.accepted=پیشنهاد شما توسط XMR {0} پذیرفته شده است. notification.trade.unlocked=معامله شما دارای حداقل یک تایید بلاک چین است.\n شما اکنون می توانید پرداخت را شروع کنید. notification.trade.paymentSent=خریدار XMR پرداخت را آغاز کرده است. notification.trade.selectTrade=انتخاب معامله notification.trade.peerOpenedDispute=همتای معامله شما یک {0} را باز کرده است. notification.trade.disputeClosed={0} بسته شده است. notification.walletUpdate.headline=به روز رسانی کیف پول معاملاتی notification.walletUpdate.msg=کیف پول معاملاتی شما به میزان کافی تأمین وجه شده است.\nمبلغ: {0} notification.takeOffer.walletUpdate.msg=کیف پول معاملاتی شما قبلاً از یک تلاش اخذ پیشنهاد، تأمین وجه شده است.\nمبلغ: {0} notification.tradeCompleted.headline=معامله تکمیل شد notification.tradeCompleted.msg=شما اکنون می‌توانید وجوه خود را به کیف پول خارجی مونرو برداشت کنید یا آنها را در کیف پول Haveno خود نگه دارید. #################################################################### # System Tray #################################################################### systemTray.show=نمایش پنجره ی برنامه systemTray.hide=مخفی کردن پنجره ی برنامه systemTray.info=اطلاعات درباره ی Haveno  systemTray.exit=خروج systemTray.tooltip=Haveno: A decentralized monero exchange network #################################################################### # GUI Util #################################################################### guiUtil.accountExport.savedToPath=حساب های معاملاتی در مسیر ذیل ذخیره شد:\n{0} guiUtil.accountExport.noAccountSetup=شما حساب های معاملاتی برای صادرات ندارید. guiUtil.accountExport.selectPath=انتخاب مسیر به {0} # suppress inspection "TrailingSpacesInProperty" guiUtil.accountExport.tradingAccount=حساب معاملاتی با شناسه {0}\n # suppress inspection "TrailingSpacesInProperty" guiUtil.accountImport.noImport=ما حساب معاملاتی با شناسه {0} را وارد نکردیم چون در حال حاضر وجود دارد.\n guiUtil.accountExport.exportFailed=صادرات به CSV به دلیل یک خطا ناموفق بود.\n خطا = {0} guiUtil.accountExport.selectExportPath=انتخاب مسیر صدور guiUtil.accountImport.imported=حساب معاملاتی وارد شده از مسیر:\n {0}\n\n حساب های وارد شده:\n {1} guiUtil.accountImport.noAccountsFound=هیچ حساب معاملاتی صادر شده ای در مسیر یافت نشد: {0}.\n نام فایل {1} است." guiUtil.openWebBrowser.warning=شما می خواهید یک صفحه وب را در مرورگر وب سیستم خود باز کنید. آیا شما می خواهید صفحه وب را اکنون باز کنید؟\n\n اگر شما از مرورگر \"Tor Browser\" به عنوان مرورگر پیش فرض وب خود استفاده نمی کنید، شما در شبکه پاک به اینترنت متصل خواهید شد.\n\nURL: \"{0}\" guiUtil.openWebBrowser.doOpen=صفحه وب را باز کن و دوباره سوال نپرس guiUtil.openWebBrowser.copyUrl=کپی URL و لغو guiUtil.ofTradeAmount=از مبلغ معامله guiUtil.requiredMinimum=(required minimum) #################################################################### # Component specific #################################################################### list.currency.select=انتخاب ارز list.currency.showAll=نمایش همه list.currency.editList=ویرایش لیست ارز table.placeholder.noItems=در حال حاضر هیچ {0} موجود نیست table.placeholder.noData=در حال حاضر داده ای موجود نیست table.placeholder.processingData=Processing data... peerInfoIcon.tooltip.tradePeer=همتای معامله peerInfoIcon.tooltip.maker=سفارش گذار peerInfoIcon.tooltip.trade.traded={0} آدرس پیازی: {1}\nتاکنون {2} بار(ها) با آن همتا معامله داشته اید\n{3} peerInfoIcon.tooltip.trade.notTraded={0} آدرس پیازی: {1}\nتاکنون با آن همتا معامله نداشته اید.\n{2} peerInfoIcon.tooltip.age=حساب معاملاتی {0} قبل ایجاد شده است. peerInfoIcon.tooltip.unknownAge=عمر حساب پرداخت ناشناخته است. tooltip.openPopupForDetails=باز کردن پنجره برای جزئیات tooltip.invalidTradeState.warning=This trade is in an invalid state. Open the details window for more information tooltip.openBlockchainForAddress=مرورگرهای بلاک چین خارجی را برای آدرس باز کنید: {0} tooltip.openBlockchainForTx=باز کردن مرورگر بلاک چین خارجی برای تراکنش: {0} confidence.unknown=وضعیت معامله ناشناخته confidence.seen=دیده شده توسط {0} همتا (s) / تأیید 0 confidence.confirmed=تأیید {0} confidence.invalid=تراکنش نامعتبر است peerInfo.title=اطلاعات همتا peerInfo.nrOfTrades=تعداد معاملات انجام شده peerInfo.notTradedYet=شما تاکنون با آن کاربر معامله نداشته اید. peerInfo.setTag=تنظیم برچسب برای آن همتا peerInfo.age.noRisk=عمر حساب پرداخت peerInfo.age.chargeBackRisk=Time since signing peerInfo.unknownAge=عمر شناخته شده نیست addressTextField.openWallet=باز کردن کیف پول بیتکوین پیشفرضتان addressTextField.copyToClipboard=رونوشت آدرس در حافظه ی موقتی addressTextField.addressCopiedToClipboard=آدرس در حافظه موقتی رونوشت شد addressTextField.openWallet.failed=باز کردن یک برنامه کیف پول بیتکوین پیش فرض، ناموفق بوده است. شاید شما برنامه را نصب نکرده باشید؟ peerInfoIcon.tooltip={0}\nتگ: {1} txIdTextField.copyIcon.tooltip=رونوشت شناسه تراکنش در حافظه ی موقتی txIdTextField.blockExplorerIcon.tooltip=Open a blockchain explorer with this transaction ID txIdTextField.missingTx.warning.tooltip=Missing required transaction #################################################################### # Navigation #################################################################### navigation.account=\"حساب\" navigation.account.walletSeed=\"حساب/رمز پشتیبان کیف پول\" navigation.funds.availableForWithdrawal=\"Funds/Send funds\" navigation.portfolio.myOpenOffers=\"سبد سهام /پیشنهادهای باز من\" navigation.portfolio.pending=\"سبد سهام /معاملات باز\" navigation.portfolio.closedTrades=\"سبد سهام /تاریخچه\" navigation.funds.depositFunds=\"وجوه/دریافت وجوه\" navigation.settings.preferences=\"تنظیمات/اولویت ها\" # suppress inspection "UnusedProperty" navigation.funds.transactions=\"وجوه/تراکنش ها\" navigation.support=\"پشتیبانی\" #################################################################### # Formatter #################################################################### formatter.formatVolumeLabel={0} مبلغ {1} formatter.makerTaker=سفارش گذار به عنوان {0} {1} / پذیرنده به عنوان {2} {3} formatter.makerTaker.locked=سفارش گذار به عنوان {0} {1} / پذیرنده به عنوان {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=شما {0} {1} ({2} {3}) هستید formatter.youAreCreatingAnOffer.traditional=شما در حال ایجاد یک پیشنهاد به {0} {1} هستید formatter.youAreCreatingAnOffer.traditional.locked=🔒 شما در حال ایجاد یک پیشنهاد به {0} {1} هستید formatter.youAreCreatingAnOffer.crypto=شما در حال ایجاد یک پیشنهاد به {0} {1} ({2} {3}) هستید formatter.youAreCreatingAnOffer.crypto.locked=🔒 شما در حال ایجاد یک پیشنهاد به {0} {1} ({2} {3}) هستید formatter.asMaker={0} {1} به عنوان سفارش گذار formatter.asTaker={0} {1} به عنوان پذیرنده #################################################################### # Domain specific #################################################################### # we use enum values here # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" XMR_MAINNET=Monero Mainnet # suppress inspection "UnusedProperty" XMR_LOCAL=Monero Local Testnet # suppress inspection "UnusedProperty" XMR_STAGENET=Monero Stagenet time.year=سال time.month=ماه time.week=هفته time.day=روز time.hour=ساعت time.minute10=10 دقیقه time.hours=ساعات time.days=روزها time.1hour=1 ساعت time.1day=1 روز time.minute=دقیقه time.second=ثانیه time.minutes=دقائق time.seconds=ثانیه ها password.enterPassword=رمز عبور password.confirmPassword=تایید رمز عبور password.tooLong=رمز عبور باید کمتر از 500 کاراکتر باشد. password.deriveKey=کلید را از رمز عبور استنتاج کنید password.walletDecrypted=کیف پول با موفقیت رمزگشایی شد و حفاظت با رمز عبور حذف شد. password.wrongPw=شما رمز عبور را اشتباه وارد کرده اید.\n\n لطفا سعی کنید رمز عبور خود را وارد کنید و با دقت خطاها و اشتباهات املایی را بررسی کنید. password.walletEncrypted=کیف پول به طور موفقیت آمیز کدگذاری و حفاظت کیف پول فعال شد. password.passwordsDoNotMatch=2 رمز عبوری که وارد نموده اید باهم مطابقت ندارند. password.forgotPassword=رمز عبور را فراموش کرده اید؟ password.backupReminder=Please note that when setting a wallet password all automatically created backups from the unencrypted wallet will be deleted.\n\nIt is highly recommended that you make a backup of the application directory and write down your seed words before setting a password! password.backupWasDone=I have already made a backup password.setPassword=Set Password (I already made a backup) password.makeBackup=Make Backup seed.seedWords=کلمات seed کیف‌پول seed.enterSeedWords=کلمات seed کیف‌پول را وارد کنید seed.date=تاریخ کیف‌پول seed.restore.title=بازگرداندن کیف های پول از کلمات رمز خصوصی seed.restore=بازگرداندن کیف های پول seed.creationDate=تاریخ ایجاد seed.warn.walletNotEmpty.msg=Your Monero wallet is not empty.\n\nYou must empty this wallet before attempting to restore an older one, as mixing wallets together can lead to invalidated backups.\n\nPlease finalize your trades, close all your open offers and go to the Funds section to withdraw your monero.\nIn case you cannot access your monero you can use the emergency tool to empty the wallet.\nTo open the emergency tool press \"Alt+e\" or \"Cmd/Ctrl+e\". seed.warn.walletNotEmpty.restore=میخواهم به هر حال بازگردانی کنم seed.warn.walletNotEmpty.emptyWallet=من ابتدا کیف پول هایم را خالی می کنم seed.warn.notEncryptedAnymore=کیف های پول شما رمزگذاری شده اند. \n\nپس از بازگرداندن، کیف های پول دیگر رمزگذاری نخواهند شد و شما باید رمز عبور جدید را تنظیم کنید.\n\n آیا می خواهید ادامه دهید؟ seed.warn.walletDateEmpty=As you have not specified a wallet date, haveno will have to scan the blockchain from 2013.10.09 (the BIP39 epoch date).\n\nBIP39 wallets were first introduced in haveno on 2017.06.28 (release v0.5). So you could save time by using that date.\n\nIdeally you should specify the date your wallet seed was created.\n\n\nAre you sure you want to go ahead without specifying a wallet date? seed.restore.success=کیف های پول با کلمات کلمات رمز خصوصی جدید بازیابی شده است. \n\nشما باید برنامه را خاموش و مجددا راه اندازی کنید. seed.restore.error=هنگام بازگرداندن کیف پول با کلمات رمز خصوصی، خطایی روی داد. {0} seed.restore.openOffers.warn=You have open offers which will be removed if you restore from seed words.\nAre you sure that you want to continue? #################################################################### # Payment methods #################################################################### payment.account=حساب payment.account.no=شماره حساب payment.account.name=نام حساب payment.account.username=Username payment.account.phoneNr=Phone number payment.account.owner.fullname=نام کامل مالک حساب payment.account.fullName=نام کامل (اول، وسط، آخر) payment.account.state=ایالت/استان/ناحیه payment.account.city=شهر payment.bank.country=کشور بانک payment.account.name.email=نام کامل/ایمیل مالک حساب payment.account.name.emailAndHolderId=نام کامل/ایمیل/{0} مالک حساب payment.bank.name=نام بانک payment.select.account=انتخاب نوع حساب payment.select.region=انتخاب ناحیه payment.select.country=انتخاب کشور payment.select.bank.country=انتخاب کشور بانک payment.foreign.currency=آیا مطمئن هستید که می خواهید ارزی به جز ارز پیش فرض کشور انتخاب کنید؟ payment.restore.default=خیر، ارز پیشفرض را تعیین کن payment.email=ایمیل payment.country=کشور payment.extras=الزامات اضافی payment.email.mobile=ایمیل یا شماره موبایل payment.crypto.address=آدرس آلت‌کوین payment.crypto.tradeInstantCheckbox=Trade instant (within 1 hour) with this Crypto payment.crypto.tradeInstant.popup=For instant trading it is required that both trading peers are online to be able to complete the trade in less than 1 hour.\n\nIf you have offers open and you are not available please disable those offers under the 'Portfolio' screen. payment.crypto=آلت‌کوین payment.select.crypto=Select or search Crypto payment.secret=سوال محرمانه payment.answer=پاسخ payment.wallet=شناسه کیف پول payment.amazon.site=Buy giftcard at payment.ask=Ask in Trader Chat payment.uphold.accountId=نام کاربری یا ایمیل یا شماره تلفن payment.moneyBeam.accountId=ایمیل یا شماره تلفن payment.popmoney.accountId=ایمیل یا شماره تلفن payment.promptPay.promptPayId=شناسه شهروندی/شناسه مالیاتی یا شماره تلفن payment.supportedCurrencies=ارزهای مورد حمایت payment.supportedCurrenciesForReceiver=Currencies for receiving funds payment.limitations=محدودیت‌ها payment.salt=داده‌های تصافی برای اعتبارسنجی سن حساب payment.error.noHexSalt=The salt needs to be in HEX format.\nIt is only recommended to edit the salt field if you want to transfer the salt from an old account to keep your account age. The account age is verified by using the account salt and the identifying account data (e.g. IBAN). payment.accept.euro=پذیرش معاملات از این کشورهای Euro ای payment.accept.nonEuro=پذیرش معاملات از کشورهای غیر Euro ای payment.accepted.countries=کشورهای پذیرفته شده payment.accepted.banks=بانک های پذیرفته شده (شناسه) payment.mobile=شماره موبایل payment.postal.address=آدرس پستی payment.national.account.id.AR=شماره CBU shared.accountSigningState=Account signing status #new payment.crypto.address.dyn=آدرس {0} payment.crypto.receiver.address=آدرس آلت‌کوین گیرنده payment.accountNr=شماره حساب payment.emailOrMobile=ایمیل یا شماره موبایل payment.useCustomAccountName=استفاده از نام حساب سفارشی payment.maxPeriod=حداکثر دوره ی زمانی مجاز معامله payment.maxPeriodAndLimit=Max. trade duration: {0} / Max. buy: {1} / Max. sell: {2} / Account age: {3} payment.maxPeriodAndLimitCrypto=حداکثر طول مدت معامله: {0} / حداکثر معامله: {1} payment.currencyWithSymbol=ارز: {0} payment.nameOfAcceptedBank=نام بانک پذیرفته شده payment.addAcceptedBank=افزودن بانک پذیرفته شده payment.clearAcceptedBanks=پاک کردن بانک های پذیرفته شده payment.bank.nameOptional=نام بانک (اختیاری) payment.bankCode=کد بانک payment.bankId=شناسه بانک (BIC/SWIFT) payment.bankIdOptional=شناسه بانک (BIC/SWIFT) (اختیاری) payment.branchNr=شماره شعبه payment.branchNrOptional=شماره شعبه (اختیاری) payment.accountNrLabel=شماره حساب (IBAN) payment.accountType=نوع حساب payment.checking=بررسی payment.savings=اندوخته ها payment.personalId=شناسه شخصی payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle's somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver's full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account's age and signing status are preserved. payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=زمانی که از HalCash استفاده می‌کنید، خریدار باید کد HalCash را از طریق پیام کوتاه موبایل به فروشنده XMR ارسال کند.\n\nلطفا مطمئن شوید که از حداکثر میزانی که بانک شما برای انتقال از طریق HalCash مجاز می‌داند تجاوز نکرده‌اید. حداقل مقداردر هر برداشت معادل 10 یورو و حداکثر مقدار 600 یورو می‌باشد. این محدودیت برای برداشت‌های تکراری برای هر گیرنده در روز 3000 یورو و در ماه 6000 یورو می‌باشد. لطفا این محدودیت‌ها را با بانک خود مطابقت دهید و مطمئن شوید که آنها هم همین محدودی‌ها را دارند.\n\nمقدار برداشت باید شریبی از 10 یورو باشد چرا که مقادیر غیر از این را نمی‌توانید از طریق ATM برداشت کنید. رابط کاربری در صفحه ساخت پینشهاد و پذیرش پیشنهاد مقدار XMR را به گونه‌ای تنظیم می‌کنند که مقدار EUR درست باشد. شما نمی‌توانید از قیمت بر مبنای بازار استفاده کنید چون مقدار یورو با تغییر قیمت‌ها عوض خواهد شد.\n\nدر صورت بروز اختلاف خریدار XMR باید شواهد مربوط به ارسال یورو را ارائه دهد. # suppress inspection "UnusedMessageFormatParameter" payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/overview/account_limits]. # suppress inspection "UnusedProperty" payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/overview/account_limits]. payment.cashDeposit.info=لطفا مطمئن شوید که بانک شما اجازه پرداخت سپرده نفد به حساب دیگر افراد را می‌دهد. برای مثال، Bank of America و Wells Fargo دیگر اجازه چنین پرداخت‌هایی را نمی‌دهند. payment.revolut.info=Revolut requires the 'Username' as account ID not the phone number or email as it was the case in the past. payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a 'Username'.\nPlease enter your Revolut 'Username' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account payment.cashapp.info=لطفاً توجه داشته باشید که Cash App ریسک بازپرداخت بالاتری نسبت به بیشتر انتقالات بانکی دارد. payment.venmo.info=لطفاً توجه داشته باشید که Venmo ریسک بازپرداخت بالاتری نسبت به بیشتر انتقالات بانکی دارد. payment.paypal.info=لطفاً توجه داشته باشید که PayPal ریسک بازپرداخت بالاتری نسبت به بیشتر انتقالات بانکی دارد. payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account payment.payByMail.contact=اطلاعات تماس payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.f2f.contact=اطلاعات تماس payment.f2f.contact.prompt=How would you like to be contacted by the trading peer? (email address, phone number,...) payment.f2f.city=شهر جهت ملاقات 'رو در رو' payment.f2f.city.prompt=نام شهر به همراه پیشنهاد نمایش داده خواهد شد payment.shared.optionalExtra=اطلاعات اضافی اختیاری payment.shared.extraInfo=اطلاعات اضافی payment.shared.extraInfo.offer=اطلاعات اضافی پیشنهاد payment.shared.extraInfo.prompt.paymentAccount=هرگونه اصطلاحات، شرایط یا جزئیات خاصی که می‌خواهید همراه با پیشنهادات شما برای این حساب پرداخت نمایش داده شود را تعریف کنید (کاربران قبل از پذیرش پیشنهادات این اطلاعات را مشاهده خواهند کرد). payment.shared.extraInfo.prompt.offer=هر اصطلاح، شرایط یا جزئیات خاصی که مایلید همراه با پیشنهاد خود نمایش داده شود را تعریف کنید. payment.shared.extraInfo.noDeposit=جزئیات تماس و شرایط پیشنهاد payment.f2f.info.openURL=باز کردن صفحه وب payment.f2f.offerbook.tooltip.countryAndCity=Country and city: {0} / {1} payment.shared.extraInfo.tooltip=اطلاعات اضافی: {0} payment.japan.bank=بانک payment.japan.branch=Branch payment.japan.account=حساب payment.japan.recipient=نام payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=برای حفاظت از شما، به شدت از استفاده از پین‌های Paysafecard برای پرداخت جلوگیری می‌کنیم.\n\n\ تراکنش‌های انجام شده از طریق پین‌ها نمی‌توانند به طور مستقل برای حل اختلاف تأیید شوند. اگر مشکلی پیش آید، بازیابی وجوه ممکن است غیرممکن باشد.\n\n\ برای اطمینان از امنیت تراکنش و حل اختلاف، همیشه از روش‌های پرداختی استفاده کنید که سوابق قابل تاییدی ارائه می‌دهند. # We use constants from the code so we do not use our normal naming convention # dynamic values are not recognized by IntelliJ # Only translate general terms NATIONAL_BANK=انتقال بانک ملی SAME_BANK=انتقال با همان بانک SPECIFIC_BANKS=نقل و انتقالات با بانک های مشخص US_POSTAL_MONEY_ORDER=US Postal Money Order CASH_DEPOSIT=سپرده ی نقدی PAY_BY_MAIL=Pay By Mail MONEY_GRAM=مانی گرام WESTERN_UNION=Western Union F2F=رو در رو (به طور فیزیکی) JAPAN_BANK=Japan Bank Furikomi AUSTRALIA_PAYID=Australian PayID # suppress inspection "UnusedProperty" NATIONAL_BANK_SHORT=بانک های ملی # suppress inspection "UnusedProperty" SAME_BANK_SHORT=همان بانک # suppress inspection "UnusedProperty" SPECIFIC_BANKS_SHORT=بانک های مشخص # suppress inspection "UnusedProperty" US_POSTAL_MONEY_ORDER_SHORT=US Money Order # suppress inspection "UnusedProperty" CASH_DEPOSIT_SHORT=سپرده ی نقدی # suppress inspection "UnusedProperty" PAY_BY_MAIL_SHORT=PayByMail # suppress inspection "UnusedProperty" MONEY_GRAM_SHORT=مانی گرام # suppress inspection "UnusedProperty" WESTERN_UNION_SHORT=Western Union # suppress inspection "UnusedProperty" F2F_SHORT=رو در رو # suppress inspection "UnusedProperty" JAPAN_BANK_SHORT=Japan Furikomi # suppress inspection "UnusedProperty" AUSTRALIA_PAYID_SHORT=PayID # Do not translate brand names # suppress inspection "UnusedProperty" UPHOLD=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY=Popmoney # suppress inspection "UnusedProperty" REVOLUT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY=WeChat Pay # suppress inspection "UnusedProperty" SEPA=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT=پرداخت های فوری SEPA # suppress inspection "UnusedProperty" FASTER_PAYMENTS=پرداخت سریع تر # suppress inspection "UnusedProperty" SWISH=Swish # suppress inspection "UnusedProperty" ZELLE=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS=آلت کوین ها # suppress inspection "UnusedProperty" PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT=Cryptos Instant # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY=OKPay # suppress inspection "UnusedProperty" CASH_APP=درخواست نقدی # suppress inspection "UnusedProperty" VENMO=Venmo # suppress inspection "UnusedProperty" UPHOLD_SHORT=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM_SHORT=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY_SHORT=Popmoney\n # suppress inspection "UnusedProperty" REVOLUT_SHORT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY_SHORT=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY_SHORT=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY_SHORT=پرداخت WeChat # suppress inspection "UnusedProperty" SEPA_SHORT=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT_SHORT=SEPA Instant # suppress inspection "UnusedProperty" FASTER_PAYMENTS_SHORT=پرداخت سریع تر # suppress inspection "UnusedProperty" SWISH_SHORT=Swish # suppress inspection "UnusedProperty" ZELLE_SHORT=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY_SHORT=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER_SHORT=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH_SHORT=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS_SHORT=آلت کوین ها # suppress inspection "UnusedProperty" PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT_SHORT=Cryptos Instant # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY_SHORT=OKPay # suppress inspection "UnusedProperty" CASH_APP_SHORT=درخواست نقدی # suppress inspection "UnusedProperty" VENMO_SHORT=Venmo #################################################################### # Validation #################################################################### validation.empty=ورودی خالی مجاز نیست. validation.NaN=ورودی، یک عدد معتبر نیست. validation.notAnInteger=ورودی یک مقدار صحیح نیست. validation.zero=ورودی 0 مجاز نیست. validation.negative=یک مقدار منفی مجاز نیست. validation.traditional.tooSmall=ورودی کوچکتر از حداقل مقدار ممکن مجاز نیست. validation.traditional.tooLarge=ورودی بزرگتر از حداکثر مقدار ممکن مجاز نیست. validation.xmr.fraction=Input will result in a monero value of less than 1 satoshi validation.xmr.tooLarge=ورودی بزرگتر از {0} مجاز نیست. validation.xmr.tooSmall=ورودی کوچکتر از {0} مجاز نیست. validation.passwordTooShort=The password you entered is too short. It needs to have a min. of 8 characters. validation.passwordTooLong=رمز عبور که شما وارد کرده اید خیلی طولانی است.رمز عبور بیش از 50 کاراکتر نمی تواند باشد. validation.sortCodeNumber={0} باید شامل {1} عدد باشد. validation.sortCodeChars={0} باید شامل {1} کاراکتر باشد. validation.bankIdNumber={0} باید شامل {1} عدد باشد. validation.accountNr=عدد حساب باید متشکل از {0} عدد باشد. validation.accountNrChars=عدد حساب باید متشکل از {0} کاراکتر باشد. validation.xmr.invalidAddress=آدرس درست نیست. لطفا فرمت آدرس را بررسی کنید validation.integerOnly=لطفا فقط اعداد صحیح را وارد کنید. validation.inputError=ورودی شما یک خطا ایجاد کرد: {0} validation.xmr.exceedsMaxTradeLimit=حدمعامله شما {0} است. validation.nationalAccountId={0} باید شامل {1} عدد باشد. #new validation.invalidInput=ورودی نامعتبر: {0} validation.accountNrFormat=شماره حساب باید از فرمت {0} باشد # suppress inspection "UnusedProperty" validation.crypto.wrongStructure=تأیید آدرس ناموفق بود زیرا آن با ساختار یک آدرس {0} مطابقت ندارد. # suppress inspection "UnusedProperty" validation.crypto.ltz.zAddressesNotSupported=LTZ address must start with L. Addresses starting with z are not supported. # suppress inspection "UnusedProperty" validation.crypto.zAddressesNotSupported=ZEC addresses must start with t. Addresses starting with z are not supported. # suppress inspection "UnusedProperty" validation.crypto.invalidAddress=آدرس یک آدرس {0} معتبر نیست! {1} # suppress inspection "UnusedProperty" validation.crypto.liquidBitcoin.invalidAddress=Native segwit addresses (those starting with 'lq') are not supported. validation.bic.invalidLength=Input length must be 8 or 11 validation.bic.letters=کد بانک و کد کشور باید حروف باشند validation.bic.invalidLocationCode=BIC حاوی کد مکان نامعتبر است validation.bic.invalidBranchCode=BIC حاوی کد شعبه نامعتبر است validation.bic.sepaRevolutBic=حساب های Revolut Sepa پشتیبانی نمی شود. validation.btc.invalidFormat=Invalid format for a Bitcoin address. validation.email.invalidAddress=آدرس نامعتبر است validation.iban.invalidCountryCode=کد کشور نامعتبر است validation.iban.checkSumNotNumeric=سرجمع باید عددی باشد validation.iban.nonNumericChars=کاراکتر غیر الفبایی و غیر عددی شناسایی شد validation.iban.checkSumInvalid=سرجمع IBAN نامعتبر است validation.iban.invalidLength=Number must have a length of 15 to 34 chars. validation.interacETransfer.invalidAreaCode=کد ناحیه غیر کانادایی validation.interacETransfer.invalidPhone=Please enter a valid 11 digit phone number (ex: 1-123-456-7890) or an email address validation.interacETransfer.invalidQuestion=باید فقط شامل حروف، اعداد، فاصله و یا نمادهای ' _ , . ? - باشد validation.interacETransfer.invalidAnswer=باید یک کلمه باشد و فقط شامل حروف، اعداد و یا نماد - باشد validation.inputTooLarge=ورودی نباید بزرگتر از {0} باشد validation.inputTooSmall=ورودی باید بزرگتر از {0} باشد validation.inputToBeAtLeast=Input has to be at least {0} validation.amountBelowDust=An amount below the dust limit of {0} satoshi is not allowed. validation.length=طول باید بین {0} و {1} باشد validation.fixedLength=Length must be {0} validation.pattern=ورودی باید در این قالب باشد: {0} validation.noHexString=ورودی در قالب HEX نیست validation.advancedCash.invalidFormat=باید یک ایمیل درست باشد و یا یک شناسه کیف‌پول در قالب: X000000000000 validation.invalidUrl=این یک URL معتبر نیست validation.mustBeDifferent=Your input must be different from the current value validation.cannotBeChanged=Parameter cannot be changed validation.numberFormatException=Number format exception {0} validation.mustNotBeNegative=ورودی نباید منفی باشد validation.phone.missingCountryCode=Need two letter country code to validate phone number validation.phone.invalidCharacters=Phone number {0} contains invalid characters validation.phone.insufficientDigits=There are not enough digits in {0} to be a valid phone number validation.phone.tooManyDigits=There are too many digits in {0} to be a valid phone number validation.phone.invalidDialingCode=Country dialing code for number {0} is invalid for country {1}. The correct dialing code is {2}. validation.invalidAddressList=Must be comma separated list of valid addresses ================================================ FILE: core/src/main/resources/i18n/displayStrings_fr.properties ================================================ # Keep display strings organized by domain # Naming convention: We use camelCase and dot separated name spaces. # Use as many sub spaces as required to make the structure clear, but as little as possible. # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break # in the display but only in the editor. # Please use in all language files the exact same order of the entries, that way a comparison is easier. # Please try to keep the length of the translated string similar to English. If it is longer it might break layout or # get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. #################################################################### # Shared #################################################################### shared.readMore=En savoir plus shared.openHelp=Ouvrir l'aide shared.warning=Attention shared.close=Fermer shared.cancel=Annuler shared.ok=OK shared.yes=Oui shared.no=Non shared.iUnderstand=Je comprends shared.na=N/A shared.shutDown=Éteindre shared.reportBug=Signaler le bug sur Github shared.buyMonero=Achat Monero shared.sellMonero=Vendre des Moneros shared.buyCurrency=Achat {0} shared.sellCurrency=Vendre {0} shared.buyCurrency.locked=Achat {0} 🔒 shared.sellCurrency.locked=Vendre {0} 🔒 shared.buyingXMRWith=achat XMR avec {0} shared.sellingXMRFor=vendre XMR pour {0} shared.buyingCurrency=achat {0} (vente XMR) shared.sellingCurrency=vente {0} (achat XMR) shared.buy=acheter shared.sell=vendre shared.buying=achat shared.selling=vente shared.P2P=P2P shared.oneOffer=ordre shared.multipleOffers=ordres shared.Offer=Ordre shared.offerVolumeCode={0} Volume d'offre shared.openOffers=ordres ouverts shared.trade=transaction shared.trades=transactions shared.openTrades=Échanges en cours shared.dateTime=Date/Heure shared.price=Prix shared.priceWithCur=Prix en {0} shared.priceInCurForCur=Prix en {0} pour 1 {1} shared.fixedPriceInCurForCur=Prix fixé en {0} pour 1 {1} shared.amount=Montant shared.txFee=Frais de transaction shared.tradeFee=Frais de transaction shared.buyerSecurityDeposit=Dépôt de l'acheteur shared.sellerSecurityDeposit=Dépôt du vendeur shared.amountWithCur=Montant en {0} shared.volumeWithCur=Volume en {0} shared.currency=Devise shared.market=Marché shared.deviation=Déviation shared.paymentMethod=Mode de paiement shared.tradeCurrency=Devise d'échange shared.offerType=Type d'ordre shared.details=Détails shared.address=Adresse shared.balanceWithCur=Balance en {0} shared.utxo=Transaction de sortie non-dépensée shared.txId=ID de la transaction shared.confirmations=Confirmations shared.revert=Revertir le Tx shared.select=Sélectionner shared.usage=Utilisation shared.state=Statut shared.tradeId=ID de la transaction shared.offerId=ID de l'ordre shared.bankName=Nom de la banque shared.acceptedBanks=Banques acceptées shared.amountMinMax=Montant (min-max) shared.amountHelp=Si un ordre comporte un montant minimum et un montant maximum, alors vous pouvez échanger n'importe quel montant dans cette fourchette. shared.remove=Enlever shared.goTo=Aller à {0} shared.XMRMinMax=XMR (min - max) shared.removeOffer=Retirer l'ordre shared.dontRemoveOffer=Ne pas retirer l'ordre shared.editOffer=Éditer l'ordre shared.openLargeQRWindow=Ouvrez et agrandissez la fenêtre du code QR shared.tradingAccount=Compte de trading shared.faq=Visitez la page FAQ shared.yesCancel=Oui, annuler shared.nextStep=Étape suivante shared.selectTradingAccount=Sélectionner le compte de trading shared.fundFromSavingsWalletButton=Appliquer les fonds depuis le portefeuille Haveno shared.fundFromExternalWalletButton=Ouvrez votre portefeuille externe pour provisionner shared.openDefaultWalletFailed=L'ouverture de l'application de portefeuille Monero par défaut a échoué. Êtes-vous sûr de l'avoir installée? shared.belowInPercent=% sous le prix du marché shared.aboveInPercent=% au-dessus du prix du marché shared.enterPercentageValue=Entrez la valeur en % shared.OR=OU shared.notEnoughFunds=Il n'y a pas suffisamment de fonds dans votre portefeuille Haveno pour payer cette transaction. La transaction a besoin de {0} Votre solde disponible est de {1}. \n\nVeuillez ajouter des fonds à partir d'un portefeuille Monero externe ou recharger votre portefeuille Haveno dans «Fonds / Dépôts > Recevoir des Fonds». shared.waitingForFunds=En attente des fonds... shared.TheXMRBuyer=L'acheteur de XMR shared.You=Vous shared.sendingConfirmation=Envoi de la confirmation... shared.sendingConfirmationAgain=Veuillez envoyer de nouveau la confirmation shared.exportCSV=Exporter en CSV shared.exportJSON=Exporter vers JSON shared.summary=Afficher le résumé shared.noDateAvailable=Pas de date disponible shared.noDetailsAvailable=Pas de détails disponibles shared.notUsedYet=Pas encore utilisé shared.date=Date shared.sendFundsDetailsWithFee=Envoyer : {0}\n\nÀ l'adresse de réception : {1}\n\nFrais supplémentaires pour le mineur : {2}\n\nÊtes-vous sûr de vouloir envoyer ce montant ? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno détecte que la transaction produira une sortie inférieure au seuil de fraction minimum (non autorisé par les règles de consensus Monero). Au lieu de cela, ces fractions ({0} satoshi {1}) seront ajoutées aux frais de traitement minier.\n\n\n shared.copyToClipboard=Copier dans le presse-papiers shared.language=Langue shared.country=Pays shared.applyAndShutDown=Appliquer et éteindre shared.selectPaymentMethod=Sélectionner un mode de paiement shared.accountNameAlreadyUsed=Ce nom de compte a été utilisé par un compte enregistré. Veuillez utiliser un autre nom. shared.askConfirmDeleteAccount=Voulez-vous vraiment supprimer le compte sélectionné? shared.cannotDeleteAccount=Vous ne pouvez pas supprimer ce compte car il est utilisé dans des devis (ou dans des transactions). shared.noAccountsSetupYet=Il n'y a pas encore de comptes établis. shared.manageAccounts=Gérer les comptes shared.addNewAccount=Ajouter un nouveau compte shared.ExportAccounts=Exporter les comptes shared.importAccounts=Importer les comptes shared.createNewAccount=Créer un nouveau compte shared.createNewAccountDescription=Les détails de votre compte sont stockés localement sur votre appareil et partagés uniquement avec votre pair de trading et l'arbitre si un litige est ouvert. shared.saveNewAccount=Sauvegarder un nouveau compte shared.selectedAccount=Sélectionner un compte shared.deleteAccount=Supprimer le compte shared.errorMessageInline=\nMessage d'erreur: {0} shared.errorMessage=Message d'erreur shared.information=Information shared.name=Nom shared.id=ID shared.dashboard=Tableau de bord shared.accept=Accepter shared.balance=Solde shared.save=Sauvegarder shared.onionAddress=Adresse Onion shared.supportTicket=Ticket de support shared.dispute=conflit shared.mediationCase=Litige en médiation shared.seller=vendeur shared.buyer=acheteur shared.allEuroCountries=Tous les pays de la zone Euro shared.acceptedTakerCountries=Pays acceptés par le taker shared.tradePrice=Prix de l'échange shared.tradeAmount=Montant de l'échange shared.tradeVolume=Volume d'échange shared.invalidKey=La clé que vous avez entrée n'était pas correcte. shared.enterPrivKey=Entrer la clé privée pour déverrouiller shared.payoutTxId=ID du versement de la transaction shared.contractAsJson=Contrat au format JSON shared.viewContractAsJson=Voir le contrat en format JSON shared.contract.title=Contrat pour la transaction avec l'ID : {0} shared.paymentDetails=XMR {0} détails du paiement shared.securityDeposit=Dépôt de garantie shared.yourSecurityDeposit=Votre dépôt de garantie shared.contract=Contrat shared.messageArrived=Message reçu. shared.messageStoredInMailbox=Message stocké dans la boîte de réception. shared.messageSendingFailed=Échec de l'envoi du message. Erreur: {0} shared.unlock=Déverrouiller shared.toReceive=à recevoir shared.toSpend=à dépenser shared.xmrAmount=Montant en XMR shared.yourLanguage=Vos langues shared.addLanguage=Ajouter une langue shared.total=Total shared.totalsNeeded=Fonds nécessaires shared.tradeWalletAddress=Adresse du portefeuille de trading shared.tradeWalletBalance=Solde du portefeuille de trading shared.reserveExactAmount=Réservez uniquement les fonds nécessaires. Nécessite des frais de minage et environ 20 minutes avant que votre offre ne soit mise en ligne. shared.makerTxFee=Maker: {0} shared.takerTxFee=Taker: {0} shared.iConfirm=Je confirme shared.openURL=Ouvert {0} shared.fiat=Fiat shared.crypto=Crypto shared.preciousMetals=Métaux précieux shared.all=Tout shared.edit=Modifier shared.advancedOptions=Options avancées shared.interval=Intervalle shared.actions=Actions shared.buyerUpperCase=Acheteur shared.sellerUpperCase=Vendeur shared.new=NOUVEAU shared.learnMore=En savoir plus shared.dismiss=Rejeter shared.selectedArbitrator=Arbitre sélectionné shared.selectedMediator=Médiateur sélectionné shared.selectedRefundAgent=Arbitre sélectionné shared.mediator=Médiateur shared.arbitrator=Arbitre shared.refundAgent=Arbitre shared.refundAgentForSupportStaff=Agent de remboursement shared.delayedPayoutTxId=ID de versement de la transaction délayé shared.delayedPayoutTxReceiverAddress=Transaction à versement delayé envoyée à shared.unconfirmedTransactionsLimitReached=Vous avez trop de transactions non confirmées pour le moment. Veuillez réessayer plus tard. shared.numItemsLabel=Nombres d'entrées: {0} shared.filter=Filtre shared.enabled=Activé #################################################################### # UI views #################################################################### #################################################################### # MainView #################################################################### mainView.menu.market=Marché mainView.menu.buyXmr=Achat XMR mainView.menu.sellXmr=Vendre des XMR mainView.menu.portfolio=Portfolio mainView.menu.funds=Fonds mainView.menu.support=Assistance mainView.menu.settings=Paramètres mainView.menu.account=Compte mainView.marketPriceWithProvider.label=Prix du marché par {0} mainView.marketPrice.havenoInternalPrice=Cours de la dernière transaction Haveno mainView.marketPrice.tooltip.havenoInternalPrice=Il n'y a pas de cours de marché disponible depuis une source externe.\nLe cours affiché est celui de la dernière transaction Haveno pour cette devise. mainView.marketPrice.tooltip=Le prix de marché est fourni par {0}{1}\nDernière mise à jour: {2}\nURL du noeud: {3} mainView.balance.available=Solde disponible mainView.balance.reserved=Réservé en ordres mainView.balance.pending=Bloqué en transactions mainView.balance.reserved.short=Réservé mainView.balance.pending.short=Vérouillé mainView.footer.usingTor=(à travers Tor) mainView.footer.localhostMoneroNode=(localhost) mainView.footer.clearnet=(à travers clearnet) mainView.footer.xmrInfo={0} {1} mainView.footer.xmrFeeRate=/ Taux des frais: {0} sat/vB mainView.footer.xmrInfo.initializing=Connexion au réseau Haveno en cours mainView.footer.xmrInfo.synchronizingWith=Synchronisation avec {0} au bloc : {1} / {2} mainView.footer.xmrInfo.connectedTo=Connecté à {0} au bloc {1} mainView.footer.xmrInfo.synchronizingWalletWith=Synchronisation du portefeuille avec {0} au bloc : {1} / {2} mainView.footer.xmrInfo.syncedWith=Synchronisé avec {0} au bloc {1} mainView.footer.xmrInfo.connectingTo=Se connecte à mainView.footer.xmrInfo.connectionFailed=Échec de la connexion à mainView.footer.xmrPeers=Pairs du réseau Monero: {0} mainView.footer.p2pPeers=Pairs du réseau Haveno: {0} mainView.footer.version=Version {0} mainView.bootstrapState.connectionToTorNetwork=(1/4) Connection au réseau Tor... mainView.bootstrapState.torNodeCreated=(2/4) Noeud Tor créé mainView.bootstrapState.hiddenServicePublished=(3/4) Hidden Service published mainView.bootstrapState.initialDataReceived=(4/4) Données initiales reçues mainView.bootstrapWarning.noSeedNodesAvailable=Pas de seed nodes disponible mainView.bootstrapWarning.noNodesAvailable=Pas de noeuds de seed ou de pairs disponibles mainView.bootstrapWarning.bootstrappingToP2PFailed=L'initialisation du réseau Haveno a échoué mainView.p2pNetworkWarnMsg.noNodesAvailable=Il n'y a pas de noeud de seed ou de persisted pairs disponibles pour demander des données.\nVeuillez vérifier votre connexion Internet ou essayer de redémarrer l'application. mainView.p2pNetworkWarnMsg.connectionToP2PFailed=La connexion au réseau Haveno a échoué (erreur signalé: {0}).\nVeuillez vérifier votre connexion internet ou essayez de redémarrer l'application. mainView.walletServiceErrorMsg.timeout=La connexion au réseau Monero a échoué car le délai d'attente a expiré. mainView.walletServiceErrorMsg.connectionError=La connexion au réseau Monero a échoué à cause d'une erreur: {0} mainView.walletServiceErrorMsg.rejectedTxException=Le réseau a rejeté une transaction.\n\n{0} mainView.networkWarning.allConnectionsLost=Vous avez perdu la connexion avec tous les {0} pairs du réseau.\nVous avez peut-être perdu votre connexion Internet ou votre ordinateur était passé en mode veille. mainView.networkWarning.localhostMoneroLost=Vous avez perdu la connexion avec le localhost Monero node.\nVeuillez redémarrer l'application Haveno pour vous connecter à d'autres Monero nodes ou redémarrer le localhost Monero node. mainView.version.update=(Mise à jour disponible) #################################################################### # MarketView #################################################################### market.tabs.offerBook=Livre des ordres market.tabs.spreadCurrency=Offres par devise market.tabs.spreadPayment=Offres par mode de paiement market.tabs.trades=Échanges # OfferBookChartView market.offerBook.buyCrypto=Achat {0} (vente {1}) market.offerBook.sellCrypto=Vente {0} (achat {1}) market.offerBook.buyWithTraditional=Achat {0} market.offerBook.sellWithTraditional=Vente {0} market.offerBook.sellOffersHeaderLabel=Vendre des {0} à market.offerBook.buyOffersHeaderLabel=Acheter des {0} à market.offerBook.buy=Je veux acheter des Moneros market.offerBook.sell=Je veux vendre des Moneros # SpreadView market.spread.numberOfOffersColumn=Tout les ordres ({0}) market.spread.numberOfBuyOffersColumn=Achat XMR ({0}) market.spread.numberOfSellOffersColumn=Vente XMR ({0}) market.spread.totalAmountColumn=Total XMR ({0}) market.spread.spreadColumn=Écart market.spread.expanded=Vue étendue # TradesChartsView market.trades.nrOfTrades=Échanges: {0} market.trades.tooltip.volumeBar=Volume: {0} / {1}\nNombre de trades: {2}\nDate: {3} market.trades.tooltip.candle.open=Ouvrir: market.trades.tooltip.candle.close=Fermer: market.trades.tooltip.candle.high=Haut: market.trades.tooltip.candle.low=Bas: market.trades.tooltip.candle.average=Moyenne: market.trades.tooltip.candle.median=Médiane: market.trades.tooltip.candle.date=Date: market.trades.showVolumeInUSD=Afficher le volume en USD #################################################################### # OfferView #################################################################### offerbook.createOffer=Créer un ordre offerbook.takeOffer=Accepter un ordre offerbook.takeOfferToBuy=Accepter l'ordre d'achat {0} offerbook.takeOfferToSell=Accepter l'ordre de vente {0} offerbook.takeOffer.enterChallenge=Entrez la phrase secrète de l'offre offerbook.trader=Échanger offerbook.offerersBankId=ID de la banque du maker (BIC/SWIFT): {0} offerbook.offerersBankName=Nom de la banque du maker: {0} offerbook.offerersBankSeat=Pays du siège de la banque du maker: {0} offerbook.offerersAcceptedBankSeatsEuro=Pays acceptés où se situe le siège de la banque (taker): tout les pays de la zone euro offerbook.offerersAcceptedBankSeats=Pays acceptés où se situe le siège de la banque (taker)\n{0} offerbook.availableOffers=Ordres disponibles offerbook.filterByCurrency=Filtrer par devise offerbook.filterByPaymentMethod=Filtrer par mode de paiement offerbook.matchingOffers=Offres correspondants à mes comptes offerbook.filterNoDeposit=Aucun dépôt offerbook.noDepositOffers=Offres sans dépôt (passphrase requise) offerbook.timeSinceSigning=Informations du compte offerbook.timeSinceSigning.info=Ce compte a été vérifié et {0} offerbook.timeSinceSigning.info.arbitrator=signé par un arbitre et pouvant signer des comptes pairs offerbook.timeSinceSigning.info.peer=Signé par un pair, attendre %d jours pour que les limites soient levées offerbook.timeSinceSigning.info.peerLimitLifted=signé par un pair et les limites ont été levées offerbook.timeSinceSigning.info.signer=signé par un pair et pouvant signer des comptes de pairs (limites levées) offerbook.timeSinceSigning.info.banned=Ce compte a été banni offerbook.timeSinceSigning.daysSinceSigning={0} jours offerbook.timeSinceSigning.daysSinceSigning.long={0} depuis la signature offerbook.xmrAutoConf=Est-ce-que la confirmation automatique est activée offerbook.buyXmrWith=Acheter XMR avec : offerbook.sellXmrFor=Vendre XMR pour : offerbook.timeSinceSigning.help=Lorsque vous effectuez avec succès une transaction avec un pair disposant d'un compte de paiement signé, votre compte de paiement est signé.\n{0} Jours plus tard, la limite initiale de {1} est levée et votre compte peut signer les comptes de paiement d'un autre pair. offerbook.timeSinceSigning.notSigned=Pas encore signé offerbook.timeSinceSigning.notSigned.ageDays={0} jours offerbook.timeSinceSigning.notSigned.noNeed=N/A shared.notSigned=Ce compte n'a pas encore été signé et a été créée il y'a {0} jours shared.notSigned.noNeed=Ce type de compte ne nécessite pas de signature shared.notSigned.noNeedDays=Ce type de compte ne nécessite pas de signature et a été créée il y'a {0} jours shared.notSigned.noNeedAlts=Les comptes pour crypto ne supportent pas la signature ou le vieillissement offerbook.nrOffers=Nombre d'ordres: {0} offerbook.volume={0} (min - max) offerbook.deposit=Déposer XMR (%) offerbook.deposit.help=Les deux parties à la transaction ont payé un dépôt pour assurer que la transaction se déroule normalement. Ce montant sera remboursé une fois la transaction terminée. offerbook.createNewOffer=Créer une offre à {0} {1} offerbook.createOfferToBuy=Créer un nouvel ordre d'achat pour {0} offerbook.createOfferToSell=Créer un nouvel ordre de vente pour {0} offerbook.createOfferToBuy.withTraditional=Créer un nouvel ordre d'achat pour {0} avec {1} offerbook.createOfferToSell.forTraditional=Créer un nouvel ordre de vente pour {0} for {1} offerbook.createOfferToBuy.withCrypto=Créer un nouvel ordre de vente pour {0} (achat{1}) offerbook.createOfferToSell.forCrypto=Créer un nouvel ordre d'achat pour {0} (vente{1}) offerbook.takeOfferButton.tooltip=Accepter un ordre pour {0} offerbook.yesCreateOffer=Oui, créer un ordre offerbook.setupNewAccount=Configurer un nouveau compte de change offerbook.removeOffer.success=L'ordre a bien été retiré. offerbook.removeOffer.failed=Le retrait de l'ordre a échoué:\n{0} offerbook.deactivateOffer.failed=La désactivation de l'ordre a échoué:\n{0} offerbook.activateOffer.failed=La publication de l'ordre a échoué:\n{0} offerbook.withdrawFundsHint=Vous pouvez retirer les fonds investis depuis l'écran {0}. offerbook.warning.noTradingAccountForCurrency.headline=Aucun compte de paiement pour la devise sélectionnée offerbook.warning.noTradingAccountForCurrency.msg=Vous n'avez pas de compte de paiement mis en place pour la devise sélectionnée.\n\nVoudriez-vous créer une offre pour une autre devise à la place? offerbook.warning.noMatchingAccount.headline=Pas de compte de paiement correspondant offerbook.warning.noMatchingAccount.msg=Cette offre utilise un mode de paiement que vous n'avez pas créé. \n\nVoulez-vous créer un nouveau compte de paiement maintenant? offerbook.warning.counterpartyTradeRestrictions=Cette offre ne peut être acceptée en raison de restrictions d'échange imposées par les contreparties offerbook.warning.newVersionAnnouncement=Grâce à cette version du logiciel, les partenaires commerciaux peuvent confirmer et vérifier les comptes de paiement de chacun pour créer un réseau de comptes de paiement de confiance.\n\nUne fois la transaction réussie, votre compte de paiement sera vérifié et les restrictions de transaction seront levées après une certaine période de temps (cette durée est basée sur la méthode de vérification).\n\nPour plus d'informations sur la vérification de votre compte, veuillez consulter le document sur https://docs.haveno.exchange/payment-methods#account-signing popup.warning.tradeLimitDueAccountAgeRestriction.seller=Le montant de transaction autorisé est limité à {0} en raison des restrictions de sécurité basées sur les critères suivants:\n- Le compte de l'acheteur n'a pas été signé par un arbitre ou par un pair\n- Le délai depuis la signature du compte de l'acheteur est inférieur à 30 jours\n- Le mode de paiement pour cette offre est considéré comme présentant un risque de rétrofacturation bancaire\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=Le montant de transaction autorisé est limité à {0} en raison des restrictions de sécurité basées sur les critères suivants:\n- Votre compte n'a pas été signé par un arbitre ou par un pair\n- Le délai depuis la signature de votre compte est inférieur à 30 jours\n- Le mode de paiement pour cette offre est considéré comme présentant un risque de rétrofacturation bancaire\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=Ce mode de paiement est temporairement limité à {0} jusqu'à {1} car tous les acheteurs ont de nouveaux comptes.\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=Votre offre sera limitée aux acheteurs avec des comptes signés et anciens car elle dépasse {0}.\n\n{1} offerbook.warning.wrongTradeProtocol=Cet ordre exige une version de protocole différente de celle utilisée actuellement par votre logiciel.\n\nVeuillez vérifier que vous avez bien la dernière version d'installée, il est possible que l'utilisateur qui a créé cet ordre utilise une ancienne version.\n\nIl n'est pas possible de trader avec des versions différentes de protocole. offerbook.warning.userIgnored=Vous avez ajouté l'adresse onion de cet utilisateur à votre liste noire. offerbook.warning.offerBlocked=L'ordre a été bloqué par des développeurs de Haveno.\nIl s'agit peut être d'un bug qui cause des problèmes lors de l'acceptation de cet ordre. offerbook.warning.currencyBanned=La devise utilisée pour cet ordre a été bloquée par les développeurs de Haveno.\nVeuillez visiter le Forum Haveno pour obtenir plus d'informations. offerbook.warning.paymentMethodBanned=Le mode de paiement utilisé pour cet ordre a été bloqué par les développeurs de Haveno.\nVeuillez visiter le Forum Haveno pour obtenir plus d'informations. offerbook.warning.nodeBlocked=L'adresse onion de ce trader a été bloquée par les développeurs de Haveno.\nIl s'agit peut être d'un bug qui cause des problèmes lors de l'acceptation de cet ordre. offerbook.warning.requireUpdateToNewVersion=Votre version Haveno n'est plus compatible avec les transactions. Veuillez mettre à jour la dernière version de Haveno via https://haveno.exchange/downloads offerbook.warning.offerWasAlreadyUsedInTrade=Vous ne pouvez pas prendre la commande car vous avez déjà terminé l'opération. Il se peut que votre précédente tentative de prise de commandes ait entraîné l'échec de la transaction. offerbook.info.sellAtMarketPrice=Vous vendrez au prix du marché (mis à jour chaque minute). offerbook.info.buyAtMarketPrice=Vous achèterez au prix du marché (mis à jour chaque minute). offerbook.info.sellBelowMarketPrice=Vous obtiendrez {0} de moins que le prix actuel du marché (mis à jour chaque minute). offerbook.info.buyAboveMarketPrice=Vous paierez {0} de plus que le prix actuel du marché (mis à jour chaque minute). offerbook.info.sellAboveMarketPrice=Vous obtiendrez {0} de plus que le prix actuel du marché (mis à jour chaque minute). offerbook.info.buyBelowMarketPrice=Vous paierez {0} de moins que le prix actuel du marché (mis à jour chaque minute). offerbook.info.buyAtFixedPrice=Vous achèterez à ce prix déterminé. offerbook.info.sellAtFixedPrice=Vous vendrez à ce prix déterminé. offerbook.info.noArbitrationInUserLanguage=En cas de litige, veuillez noter que l'arbitrage de cet ordre sera traité par {0}. La langue est actuellement définie sur {1}. offerbook.info.roundedFiatVolume=Le montant a été arrondi pour accroître la confidentialité de votre transaction. #################################################################### # Offerbook / Create offer #################################################################### createOffer.amount.prompt=Entrer le montant en XMR createOffer.price.prompt=Entrer le prix createOffer.volume.prompt=Entrer le montant en {0} createOffer.amountPriceBox.amountDescription=Somme en Monero à {0} createOffer.amountPriceBox.buy.volumeDescription=Somme en {0} à envoyer createOffer.amountPriceBox.sell.volumeDescription=Montant en {0} à recevoir createOffer.amountPriceBox.minAmountDescription=Montant minimum de XMR createOffer.securityDeposit.prompt=Dépôt de garantie createOffer.fundsBox.title=Financer votre ordre createOffer.fundsBox.offerFee=Frais de transaction createOffer.fundsBox.networkFee=Frais de minage createOffer.fundsBox.placeOfferSpinnerInfo=Publication de l'ordre en cours ... createOffer.fundsBox.paymentLabel=Transaction Haveno avec l'ID {0} createOffer.fundsBox.fundsStructure=({0} dépôt de garantie, {1} frais de transaction, {2} frais de minage) createOffer.success.headline=Votre offre a été créée createOffer.success.info=Vous pouvez gérer vos ordres en cours dans \"Portfolio/Mes ordres\". createOffer.info.sellAtMarketPrice=Vous vendrez toujours au prix du marché car le prix de votre ordre sera continuellement mis à jour. createOffer.info.buyAtMarketPrice=Vous achèterez toujours au prix du marché car le prix de votre ordre sera continuellement mis à jour. createOffer.info.sellAboveMarketPrice=Vous recevrez toujours {0}% de plus que le prix actuel du marché car le prix de votre ordre sera continuellement mis à jour. createOffer.info.buyBelowMarketPrice=Vous paierez toujours {0}% de moins que le prix actuel du marché car prix de votre ordre sera continuellement mis à jour. createOffer.warning.sellBelowMarketPrice=Vous obtiendrez toujours {0}% de moins que le prix actuel du marché car le prix de votre ordre sera continuellement mis à jour. createOffer.warning.buyAboveMarketPrice=Vous paierez toujours {0}% de plus que le prix actuel du marché car le prix de votre ordre sera continuellement mis à jour. createOffer.tradeFee.descriptionXMROnly=Frais de transaction createOffer.tradeFee.descriptionBSQEnabled=Choisir la devise des frais de transaction createOffer.triggerPrice.prompt=Réglez le prix de déclenchement, optionnel createOffer.triggerPrice.label=Désactiver l'offre si le prix du marché est {0} createOffer.triggerPrice.tooltip=Afin de protéger contre les brusques variations des prix, vous pouvez mettre en place un prix de déclenchement qui désactive l'offre si le prix du marché atteint cette valeur. createOffer.triggerPrice.invalid.tooLow=La valeur doit être supérieure à {0} createOffer.triggerPrice.invalid.tooHigh=La valuer doit être inférieure à {0] # new entries createOffer.placeOfferButton.buy=Vérifier : Créer une offre pour acheter XMR avec {0} createOffer.placeOfferButton.sell=Vérifier : Créer une offre pour vendre XMR contre {0} createOffer.createOfferFundWalletInfo.headline=Financer votre ordre # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=Montant du trade: {0}\n\n createOffer.createOfferFundWalletInfo.msg=Vous devez déposer {0} à cette offre.\n\n\ Ces fonds sont réservés dans votre portefeuille local et seront verrouillés dans un portefeuille multisignature dès qu'une personne acceptera votre offre.\n\n\ Le montant est la somme de :\n\ {1}\ - Votre dépôt de garantie : {2}\n\ - Frais de transaction : {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=Une erreur s'est produite lors du placement de cet ordre:\n\n{0}\n\nAucun fonds n'a été prélevé sur votre portefeuille pour le moment.\nVeuillez redémarrer l'application et vérifier votre connexion réseau. createOffer.setAmountPrice=Définir le montant et le prix createOffer.warnCancelOffer=Vous avez déjà financé cet ordre.\nSi vous annulez maintenant, vos fonds seront envoyés dans votre portefeuille haveno local et seront disponible pour retrait dans l'onglet \"Fonds/Envoyer des fonds\".\nÊtes-vous certain de vouloir annuler ? createOffer.timeoutAtPublishing=Un timeout est survenu au moment de la publication de l'ordre. createOffer.errorInfo=\n\nLes frais de maker ont déjà été payés. Dans le pire des cas, vous avez perdu ces frais.\nVeuillez essayer de redémarrer votre application et vérifier votre connexion réseau pour voir si vous pouvez résoudre le problème. createOffer.tooLowSecDeposit.warning=Vous avez défini le dépôt de garantie à une valeur inférieure à la valeur par défaut recommandée de {0}.\nÊtes-vous sûr de vouloir utiliser un dépôt de garantie moins élevé ? createOffer.tooLowSecDeposit.makerIsSeller=Ceci vous donne moins de protection dans le cas où le pair de trading ne suit pas le protocole de transaction. createOffer.tooLowSecDeposit.makerIsBuyer=cela offre moins de protection pour le pair que de suivre le protocole de trading car vous avez moins de dépôt à risque. D'autres utilisateurs préféreront peut-être accepter d'autres ordres que le vôtre. createOffer.resetToDefault=Non, revenir à la valeur par défaut createOffer.useLowerValue=Oui, utiliser ma valeur la plus basse createOffer.priceOutSideOfDeviation=Le prix que vous avez fixé est en dehors de l'écart max. du prix du marché autorisé\nL'écart maximum autorisé est {0} et peut être ajusté dans les préférences. createOffer.changePrice=Modifier le prix createOffer.tac=En plaçant cet ordre vous acceptez d'effectuer des transactions avec n'importe quel trader remplissant les conditions affichées à l'écran. createOffer.currencyForFee=Frais de transaction createOffer.setDeposit=Etablir le dépôt de garantie de l'acheteur (%) createOffer.setDepositAsBuyer=Définir mon dépôt de garantie en tant qu'acheteur (%) createOffer.setDepositForBothTraders=Établissez le dépôt de sécurité des deux traders (%) createOffer.securityDepositInfo=Le dépôt de garantie de votre acheteur sera de {0} createOffer.securityDepositInfoAsBuyer=Votre dépôt de garantie en tant qu'acheteur sera de {0} createOffer.minSecurityDepositUsed=Le dépôt de sécurité minimum est utilisé createOffer.buyerAsTakerWithoutDeposit=Aucun dépôt requis de la part de l'acheteur (protégé par un mot de passe) createOffer.myDeposit=Mon dépôt de garantie (%) createOffer.myDepositInfo=Votre dépôt de garantie sera de {0} #################################################################### # Offerbook / Take offer #################################################################### takeOffer.amount.prompt=Entrez le montant en XMR takeOffer.amountPriceBox.buy.amountDescription=Montant en XMR à vendre takeOffer.amountPriceBox.sell.amountDescription=Montant de XMR à acheter takeOffer.amountPriceBox.priceDescription=Prix par Monero en {0} takeOffer.amountPriceBox.amountRangeDescription=Fourchette du montant possible takeOffer.amountPriceBox.warning.invalidXmrDecimalPlaces=Le montant que vous avez saisi dépasse le nombre maximum de décimales autorisées.\nLe montant a été défini à 4 décimales près. takeOffer.validation.amountSmallerThanMinAmount=Le montant ne peut pas être plus petit que le montant minimum défini dans l'ordre. takeOffer.validation.amountLargerThanOfferAmount=La saisie ne peut pas être plus grande que le montant défini dans l'ordre. takeOffer.validation.amountLargerThanOfferAmountMinusFee=La somme saisie va créer des dusts résultantes de la transaction pour le vendeur de XMR. takeOffer.fundsBox.title=Provisionner votre trade takeOffer.fundsBox.isOfferAvailable=Vérifiez si l'ordre est disponible... takeOffer.fundsBox.tradeAmount=Montant à vendre takeOffer.fundsBox.offerFee=Frais de transaction du trade takeOffer.fundsBox.networkFee=Total des frais de minage takeOffer.fundsBox.takeOfferSpinnerInfo=Acceptation de l'offre : {0} takeOffer.fundsBox.paymentLabel=Transaction Haveno avec l'ID {0} takeOffer.fundsBox.fundsStructure=({0} dépôt de garantie, {1} frais de transaction, {2} frais de minage) takeOffer.fundsBox.noFundingRequiredTitle=Aucun financement requis takeOffer.fundsBox.noFundingRequiredDescription=Obtenez la phrase secrète de l'offre auprès du vendeur en dehors de Haveno pour accepter cette offre. takeOffer.success.headline=Vous avez accepté un ordre avec succès. takeOffer.success.info=Vous pouvez voir vos transactions dans \"Portfolio/Échanges en cours\". takeOffer.error.message=Une erreur s'est produite pendant l’'acceptation de l'ordre.\n\n{0} # new entries takeOffer.takeOfferButton.buy=Vérifier : Accepter une offre pour acheter XMR avec {0} takeOffer.takeOfferButton.sell=Vérifier : Accepter une offre pour vendre XMR contre {0} takeOffer.noPriceFeedAvailable=Vous ne pouvez pas accepter cet ordre, car celui-ci utilise un prix en pourcentage basé sur le prix du marché, mais il n'y a pas de prix de référence de disponible. takeOffer.takeOfferFundWalletInfo.headline=Provisionner votre trade # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Montant du trade: {0}\n takeOffer.takeOfferFundWalletInfo.msg=Vous devez déposer {0} pour accepter cette offre.\n\nLe montant est la somme de :\n{1}- Votre dépôt de garantie : {2}\n- Frais de transaction : {3} takeOffer.alreadyPaidInFunds=Si vous avez déjà provisionner des fonds vous pouvez les retirer dans l'onglet \"Fonds/Envoyer des fonds\". takeOffer.paymentInfo=Informations de paiement takeOffer.setAmountPrice=Définir le montant takeOffer.alreadyFunded.askCancel=Vous avez déjà provisionner cet ordre.\nSi vous annulez maintenant, vos fonds seront envoyés dans votre portefeuille haveno local et seront disponible pour retrait dans l'onglet \"Fonds/Envoyer des fonds\".\nVoulez vous vraiment annuler? takeOffer.failed.offerNotAvailable=La demande de prise d'ordre a échoué car l'ordre n'est plus disponible. Peut-être qu'un autre trader a accepté l'ordre entre-temps. takeOffer.failed.offerTaken=Vous ne pouvez pas saisir cet ordre car elle a déjà été pris par un autre trader. takeOffer.failed.offerRemoved=Vous ne pouvez pas saisir cet ordre car elle a été supprimée entre-temps. takeOffer.failed.offererNotOnline=La demande de prise de l'ordre a échoué parce que le maker n'est plus en ligne. takeOffer.failed.offererOffline=Vous ne pouvez pas saisir cet ordre car le maker n'est pas connecté. takeOffer.warning.connectionToPeerLost=Vous avez perdu la connexion avec le maker.\nIl se peut qu'ils se soient déconnectés ou qu'ils aient interrompu la connexion avec vous en raison d'un trop grand nombre de connexions en cours.\n\nSi vous pouvez encore voir leur offre dans le livre des ordres, vous pouvez essayer d'accepter une nouvelle fois l'offre. takeOffer.error.noFundsLost=\n\nAucun fonds n'a quitté votre portefeuille pour le moment.\nVeuillez essayer de redémarrer votre application et vérifier votre connexion réseau pour voir si vous pouvez résoudre le problème. # suppress inspection "TrailingSpacesInProperty" takeOffer.error.feePaid=\nnot valid\n takeOffer.error.depositPublished=\n\nLa transaction du dépôt de garantie à déjà été publiée.\nVeuillez redémarrer l'application et vérifier votre connexion réseau pour voir si le problème peut être résolu.\nSi le problème persiste, merci de contacter les développeurs afin d'obtenir de l'aide. takeOffer.error.payoutPublished=\n\nLe versement de la transaction à déjà été publiée.\nVeuillez redémarrer l'application et vérifier votre connexion réseau pour voir si le problème peut être résolu.\nSi le problème persiste, veuillez contacter les développeurs afin d'obtenir de l'aide. takeOffer.tac=En acceptant cet ordre vous acceptez les conditions de transactions définies à l'écran. #################################################################### # Offerbook / Edit offer #################################################################### openOffer.header.triggerPrice=Prix de déclenchement openOffer.triggerPrice=Prix de déclenchement {0} openOffer.triggered=Cette offre a été désactivée car le prix du marché a atteint votre prix de déclenchement\nVeuillez éditer votre offre pour définir un nouveau prix de déclenchement editOffer.setPrice=Définir le prix editOffer.confirmEdit=Confirmation: Modification de l'ordre editOffer.publishOffer=Publication de votre ordre. editOffer.failed=Échec de la modification de l'ordre:\n{0} editOffer.success=Votre ordre a été modifié avec succès. editOffer.invalidDeposit=Le dépôt de garantie de l'acheteur ne respecte pas le cadre des contraintes définies par Haveno DAO et ne peut plus être modifié. #################################################################### # Portfolio #################################################################### portfolio.tab.openOffers=Mes ordres en cours portfolio.tab.pendingTrades=Échanges en cours portfolio.tab.history=Historique portfolio.tab.failed=Échec portfolio.tab.editOpenOffer=Éditer l'ordre portfolio.closedTrades.deviation.help=Pourcentage de déviation du prix par rapport au marché portfolio.pending.invalidTx=Il y'a un problème avec une transaction manquante ou invalide.\n\nVeuillez NE PAS envoyer le payement Traditional ou crypto.\n\nOuvrez un ticket de support pour avoir l'aide d'un médiateur.\n\nMessage d'erreur: {0} portfolio.pending.syncing=Synchronisation du portefeuille de trading portfolio.pending.syncing.blockRemaining=Synchronisation du portefeuille de trading — 1 bloc restant portfolio.pending.syncing.blocksRemaining=Synchronisation du portefeuille de trading — {0} blocs restants portfolio.pending.step1.waitForConf=Attendre la confirmation de la blockchain portfolio.pending.step2_buyer.additionalConf=Les dépôts ont atteint 10 confirmations.\nPour plus de sécurité, nous recommandons d’attendre {0} confirmations avant d’envoyer le paiement.\nProcédez plus tôt à vos propres risques. portfolio.pending.step2_buyer.startPayment=Initier le paiement portfolio.pending.step2_seller.waitPaymentSent=Patientez jusqu'à ce que le paiement soit commencé. portfolio.pending.step3_buyer.waitPaymentArrived=Patientez jusqu'à la réception du paiement portfolio.pending.step3_seller.confirmPaymentReceived=Confirmation de paiement reçu portfolio.pending.step5.completed=Terminé portfolio.pending.step3_seller.autoConf.status.label=Statut de l'auto-confirmation portfolio.pending.autoConf=Auto-confirmé portfolio.pending.autoConf.blocks=Confirmations XMR : {0}/ Requises: {1} portfolio.pending.autoConf.state.xmr.txKeyReused=Clé de transaction réutilisée. Veuillez ouvrir une contestation portfolio.pending.autoConf.state.confirmations=Confirmations XMR: {0}/{1} portfolio.pending.autoConf.state.txNotFound=Transaction pas encore vue dans le mem-pool portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=Pas d'ID de transaction valide / de clé de transaction portfolio.pending.autoConf.state.filterDisabledFeature=Désactivé par les développeurs # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FEATURE_DISABLED=La fonctionnalité d'auto-confirmation est désactivée. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=Le montant du trade dépasse la limite de l'auto-confirmation # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.INVALID_DATA=Le pair a fourni des données invalides. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=Le versement de la transaction a déjà été publié. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.DISPUTE_OPENED=La contestation a été ouverte. L'auto-confirmation est désactivée pour ce trade. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.REQUESTS_STARTED=Requête de preuve de transaction lancée # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PENDING=Résultats ayant succédé: {0}/{1}; {2} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.COMPLETED=Preuve à tous les services réussies. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.ERROR=Une erreur lors de la demande du service a eu lieu. L'auto-confirmation n'est pas possible. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=Un service a retourné un échec. L'auto-confirmation n'est pas possible. portfolio.pending.step1.info.you=La transaction de dépôt a été publiée.\nVous pouvez commencer le paiement après 10 confirmations (environ {0} minutes restantes). portfolio.pending.step1.info.buyer=La transaction de dépôt a été publiée.\nL'acheteur XMR peut commencer le paiement après 10 confirmations (environ {0} minutes restantes). portfolio.pending.step1.warn=La transaction de dépôt n'est toujours pas confirmée. Cela se produit parfois dans de rares occasions lorsque les frais de financement d'un trader en provenance d'un portefeuille externe sont trop bas. portfolio.pending.step1.openForDispute=La transaction de dépôt n'est toujours pas confirmée. Vous pouvez attendre plus longtemps ou contacter le médiateur pour obtenir de l'aide. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2.confReached=Votre trade a atteint au moins une confirmation de la part de la blockchain.\n\n portfolio.pending.step2_buyer.refTextWarn=Important: Quand vous effectuez le paiement, laissez le champ \"raison du paiement\" vide. NE METTEZ PAS l'ID du trade ou n'importe quel autre texte, par exemple 'monero', 'XMR' ou 'Haveno'. Vous êtez autorisés à discuter via le chat des trader si un autre \"raison du paiement\" est préférable pour vous deux. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees=Si votre banque vous facture des frais pour effectuer le transfert, vous êtes responsable de payer ces frais. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.crypto=Veuillez transférer à partir de votre portefeuille externe {0}.\n{1} au vendeur de XMR.\n\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.cash=Veuillez vous rendre dans une banque et payer {0} au vendeur de XMR.\n portfolio.pending.step2_buyer.cash.extra=CONDITIONS REQUISES: \nAprès avoir effectué le paiement veuillez écrire sur le reçu papier : PAS DE REMBOURSEMENT.\nPuis déchirer le en 2, prenez en une photo et envoyer le à l'adresse email du vendeur de XMR. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.moneyGram=Veuillez s'il vous plaît payer {0} au vendeur de XMR en utilisant MoneyGram.\n\n portfolio.pending.step2_buyer.moneyGram.extra=CONDITIONS REQUISES:\nAprès avoir effectué le paiement envoyez le numéro d'autorisation et une photo du reçu par e-mail au vendeur de XMR.\nLe reçu doit faire clairement figurer le nom complet du vendeur, son pays, l'état et le montant. Le mail du vendeur est: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.westernUnion=Veuillez s'il vous plaît payer {0} au vendeur de XMR en utilisant Western Union.\n\n portfolio.pending.step2_buyer.westernUnion.extra=CONDITIONS REQUISES:\nAprès avoir effectué le paiement envoyez le MTCN (numéro de suivi) et une photo du reçu par e-mail au vendeur de XMR.\nLe reçu doit faire clairement figurer le nom complet du vendeur, son pays, l'état et le montant. Le mail du vendeur est: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Merci d'envoyer {0} par \"US Postal Money Order\" au vendeur de XMR.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Veuillez envoyer {0} en utlisant \"Pay by Mail\" au vendeur de XMR. Les instructions spécifiques sont dans le contrat de trade, ou si ce n'est pas clair, vous pouvez poser des questions via le chat des trader. Pour plus de détails sur Pay by Mail, allez sur le wiki Haveno \n[LIEN:https://docs.haveno.exchange/overview/payment_methods/Pay_By_Mail]\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Veuillez payer {0} via la méthode de paiement spécifiée par le vendeur de XMR. Vous trouverez les informations du compte du vendeur à l'écran suivant.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Veuillez s'il vous plaît contacter le vendeur de XMR via le contact fourni, et planifiez un rendez-vous pour effectuer le paiement {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Initier le paiement en utilisant {0} portfolio.pending.step2_buyer.recipientsAccountData=Destinataires {0} portfolio.pending.step2_buyer.amountToTransfer=Montant à transférer portfolio.pending.step2_buyer.sellersAddress=Adresse {0} du vendeur portfolio.pending.step2_buyer.buyerAccount=Votre compte de paiement à utiliser portfolio.pending.step2_buyer.paymentSent=Paiement initié portfolio.pending.step2_buyer.showEarly=Afficher les détails du paiement dès le départ portfolio.pending.step2_buyer.fillInBsqWallet=Payer depuis le portefeuille BSQ portfolio.pending.step2_buyer.warn=Vous n'avez toujours pas effectué votre {0} paiement !\nVeuillez noter que l'échange doit être achevé avant {1}. portfolio.pending.step2_buyer.openForDispute=Vous n'avez pas effectué votre paiement !\nLe délai maximal alloué pour l'échange est écoulé, veuillez contacter le médiateur pour obtenir de l'aide. portfolio.pending.step2_buyer.paperReceipt.headline=Avez-vous envoyé le reçu papier au vendeur de XMR? portfolio.pending.step2_buyer.paperReceipt.msg=Rappelez-vous: \nVous devez écrire sur le reçu papier: PAS DE REMBOURSEMENT.\nEnsuite, veuillez le déchirer en 2, faire une photo et l'envoyer à l'adresse email du vendeur. portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Envoyer le numéro d'autorisation ainsi que le reçu portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=Vous devez envoyez le numéro d'autorisation et une photo du reçu par email au vendeur de XMR.\nLe reçu doit faire clairement figurer le nom complet du vendeur, son pays, l'état, et le montant. Le mail du vendeur est: {0}.\n\nAvez-vous envoyé le numéro d'autorisation et le contrat au vendeur ? portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=Envoyer le MTCN et le reçu portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Vous devez envoyez le MTCN (numéro de suivi) et une photo du reçu par email au vendeur de XMR.\nLe reçu doit clairement faire figurer le nom complet du vendeur, son pays, l'état et le montant. Le mail du vendeur est: {0}.\n\nAvez-vous envoyé le MTCN et le contrat au vendeur ? portfolio.pending.step2_buyer.halCashInfo.headline=Envoyer le code HalCash portfolio.pending.step2_buyer.halCashInfo.msg=Vous devez envoyez un message au format texte SMS avec le code HalCash ainsi que l'ID de la transaction ({0}) au vendeur de XMR.\nLe numéro de mobile du vendeur est {1}.\n\nAvez-vous envoyé le code au vendeur ? portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Certaines banques pourraient vérifier le nom du receveur. Des comptes de paiement plus rapides créés dans des clients Haveno plus anciens ne fournissent pas le nom du receveur, veuillez donc utiliser le chat de trade pour l'obtenir (si nécessaire). portfolio.pending.step2_buyer.confirmStart.headline=Confirmez que vous avez initié le paiement portfolio.pending.step2_buyer.confirmStart.msg=Avez-vous initié le {0} paiement auprès de votre partenaire de trading? portfolio.pending.step2_buyer.confirmStart.yes=Oui, j'ai initié le paiement portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=Vous n'avez pas fourni de preuve de paiement portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=Lorsque vous terminez une transaction XMR / XMR, vous pouvez utiliser la fonction de confirmation automatique pour vérifier si le montant correct de XMR a été envoyé à votre portefeuille, afin que Haveno puisse automatiquement marquer la transaction comme terminée et pour que tout le monde puisse aller plus vite. \n\nConfirmez automatiquement que les transactions XMR sont vérifiées sur au moins 2 nœuds d'explorateur de blocs XMR à l'aide de la clé de transaction fournie par l'expéditeur XMR. Par défaut, Haveno utilise un nœud d'explorateur de blocs exécuté par des contributeurs Haveno, mais nous vous recommandons d'exécuter votre propre nœud d'explorateur de blocs XMR pour maximiser la confidentialité et la sécurité. \n\nVous pouvez également définir le nombre maximum de XMR par transaction dans «Paramètres» pour confirmer automatiquement et le nombre de confirmations requises. \n\nPlus de détails sur Haveno Wiki (y compris comment configurer votre propre nœud d'explorateur de blocs): [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades] portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=La sasie n'est pas une valeur hexadécimale de 32 bits portfolio.pending.step2_buyer.confirmStart.warningButton=Ignorer et continuer tout de même portfolio.pending.step2_seller.waitPayment.headline=En attende du paiement portfolio.pending.step2_seller.f2fInfo.headline=Coordonnées de l'acheteur portfolio.pending.step2_seller.waitPayment.msg=La transaction de dépôt a été vérifiée au moins une fois sur la blockchain\nVous devez attendre que l'acheteur de XMR lance le {0} payment. portfolio.pending.step2_seller.warn=L'acheteur de XMR n'a toujours pas effectué le paiement {0}.\nVeuillez attendre qu'il effectue celui-ci.\nSi la transaction n'est pas effectuée le {1}, un arbitre enquêtera. portfolio.pending.step2_seller.openForDispute=L'acheteur de XMR n'a pas initié son paiement !\nLa période maximale autorisée pour ce trade est écoulée.\nVous pouvez attendre plus longtemps et accorder plus de temps à votre pair de trading ou contacter le médiateur pour obtenir de l'aide. tradeChat.chatWindowTitle=Fenêtre de discussion pour la transaction avec l'ID '{0}' tradeChat.openChat=Ouvrir une fenêtre de discussion tradeChat.rules=Vous pouvez communiquer avec votre pair de trading pour résoudre les problèmes potentiels liés à cet échange.\nIl n'est pas obligatoire de répondre sur le chat.\nSi un trader enfreint l'une des règles ci-dessous, ouvrez un litige et signalez-le au médiateur ou à l'arbitre.\n\nRègles sur le chat:\n\t● N'envoyez pas de liens (risque de malware). Vous pouvez envoyer l'ID de transaction et le nom d'un explorateur de blocs.\n\t● N'envoyez pas les mots de votre seed, clés privées, mots de passe ou autre information sensible !\n\t● N'encouragez pas le trading en dehors de Haveno (non sécurisé).\n\t● Ne vous engagez dans aucune forme d'escroquerie d'ingénierie sociale.\n\t● Si un pair ne répond pas et préfère ne pas communiquer par chat, respectez sa décision.\n\t● Limitez la portée de la conversation à l'échange en cours. Ce chat n'est pas une alternative à messenger ou une troll-box.\n\t● Entretenez une conversation amicale et respectueuse. # suppress inspection "UnusedProperty" message.state.UNDEFINED=Indéfini # suppress inspection "UnusedProperty" message.state.SENT=Message envoyé # suppress inspection "UnusedProperty" message.state.ARRIVED=Message reçu par le pair # suppress inspection "UnusedProperty" message.state.STORED_IN_MAILBOX=Message de paiement bien envoyé mais pas encore reçu par le pair # suppress inspection "UnusedProperty" message.state.ACKNOWLEDGED=Le pair a confirmé la réception du message # suppress inspection "UnusedProperty" message.state.FAILED=Echec de l'envoi du message portfolio.pending.step3_buyer.wait.headline=Attendre la confirmation de paiement du vendeur XMR portfolio.pending.step3_buyer.wait.info=En attente de la confirmation du vendeur XMR pour la réception du paiement {0}. portfolio.pending.step3_buyer.wait.msgStateInfo.label=État du message de lancement du paiement portfolio.pending.step3_buyer.warn.part1a=sur la {0} blockchain portfolio.pending.step3_buyer.warn.part1b=chez votre prestataire de paiement (par ex. banque) portfolio.pending.step3_buyer.warn.part2=Le vendeur de XMR n'a toujours pas confirmé votre paiement. . Veuillez vérifier {0} si l'envoi du paiement a bien fonctionné. portfolio.pending.step3_buyer.openForDispute=Le vendeur de XMR n'a pas confirmé votre paiement ! Le délai maximal alloué pour ce trade est écoulé. Vous pouvez attendre plus longtemps et accorder plus de temps à votre pair de trading ou contacter le médiateur pour obtenir de l'aide. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.part=Votre partenaire de trading a confirmé qu'il a initié le paiement {0}.\n portfolio.pending.step3_seller.crypto.explorer=Sur votre explorateur blockchain {0} favori portfolio.pending.step3_seller.crypto.wallet=Dans votre portefeuille {0} portfolio.pending.step3_seller.crypto={0}Veuillez s'il vous plaît vérifier {1} que la transaction vers votre adresse de réception\n{2}\ndispose de suffisamment de confirmations sur la blockchain.\nLe montant du paiement doit être {3}\n\nVous pouvez copier & coller votre adresse {4} à partir de l'écran principal après avoir fermé ce popup. portfolio.pending.step3_seller.postal={0}Veuillez vérifier si vous avez reçu {1} avec \"US Postal Money Order\" de la part de l'acheteur de XMR. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.payByMail={0}Veuillez vérifier si vous avez reçu {1} avec \"Pay by Mail\" de la part de l'acheteur de XMR # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.bank=Votre partenaire de trading a confirmé qu'il a initié le {0} paiement.\n\nVeuillez vous rendre sur votre banque en ligne et vérifier si vous avez reçu {1} de la part de l'acheteur de XMR. portfolio.pending.step3_seller.cash=Du fait que le paiement est réalisé via Cash Deposit l'acheteur de XMR doit inscrire \"NO REFUND\" sur le reçu papier, le déchirer en 2 et vous envoyer une photo par email.\n\nPour éviter un risque de rétrofacturation, ne confirmez que si vous recevez le mail et que vous êtes sûr que le reçu papier est valide.\nSi vous n'êtes pas sûr, {0} portfolio.pending.step3_seller.moneyGram=L'acheteur doit vous envoyer le numéro d'autorisation et une photo du reçu par e-mail .\nLe reçu doit faire clairement figurer votre nom complet, votre pays, l'état et le montant. Veuillez s'il vous plaît vérifier que vous avez bien reçu par e-mail le numéro d'autorisation.\n\nAprès avoir fermé ce popup vous verrez le nom de l'acheteur de XMR et l'adresse où retirer l'argent depuis MoneyGram.\n\nN'accusez réception qu'après avoir retiré l'argent avec succès! portfolio.pending.step3_seller.westernUnion=L'acheteur doit vous envoyer le MTCN (numéro de suivi) et une photo du reçu par e-mail .\nLe reçu doit faire clairement figurer votre nom complet, votre pays, l'état et le montant. Veuillez s'il vous plaît vérifier si vous avez reçu par e-mail le MTCN.\n\nAprès avoir fermé ce popup vous verrez le nom de l'acheteur de XMR et l'adresse où retirer l'argent depuis Western Union.\n\nN'accusez réception qu'après avoir retiré l'argent avec succès! portfolio.pending.step3_seller.halCash=L'acheteur doit vous envoyer le code HalCash par message texte SMS. Par ailleurs, vous recevrez un message de la part d'HalCash avec les informations nécessaires pour retirer les EUR depuis un DAB Bancaire supportant HalCash.\n\nAprès avoir retiré l'argent au DAB, veuillez confirmer ici la réception du paiement ! portfolio.pending.step3_seller.amazonGiftCard=L'acheteur vous a envoyé une e-carte cadeau Amazon via email ou SMS vers votre téléphone. Veuillez récupérer maintenant la carte cadeau sur votre compte Amazon, et une fois activée, confirmez le reçu de paiement. portfolio.pending.step3_seller.bankCheck=\n\nVeuillez également vérifier que le nom de l'expéditeur indiqué sur le contrat de l'échange correspond au nom qui apparaît sur votre relevé bancaire:\nNom de l'expéditeur, associé au contrat de l'échange: {0}\n\nSi les noms ne sont pas exactement identiques, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=ne confirmez pas la réception du paiement. Au lieu de cela, ouvrez un litige en appuyant sur \"alt + o\" ou \"option + o\".\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=Confirmer la réception du paiement portfolio.pending.step3_seller.amountToReceive=Montant à recevoir portfolio.pending.step3_seller.yourAddress=Votre adresse {0} portfolio.pending.step3_seller.buyersAddress=Adresse {0} des acheteurs portfolio.pending.step3_seller.yourAccount=Votre compte de trading portfolio.pending.step3_seller.xmrTxHash=ID de la transaction portfolio.pending.step3_seller.xmrTxKey=Clé de Transaction portfolio.pending.step3_seller.buyersAccount=Données du compte de l'acheteur portfolio.pending.step3_seller.confirmReceipt=Confirmer la réception du paiement portfolio.pending.step3_seller.buyerStartedPayment=L'acheteur XMR a commencé le {0} paiement.\n{1} portfolio.pending.step3_seller.buyerStartedPayment.crypto=Vérifiez la présence de confirmations par la blockchain dans votre portefeuille crypto ou sur un explorateur de blocs et confirmez le paiement lorsque vous aurez suffisamment de confirmations sur la blockchain. portfolio.pending.step3_seller.buyerStartedPayment.traditional=Vérifiez sur votre compte de trading (par ex. compte bancaire) et confirmez quand vous avez reçu le paiement. portfolio.pending.step3_seller.warn.part1a=sur la {0} blockchain portfolio.pending.step3_seller.warn.part1b=Auprès de votre prestataire de paiement (par ex. banque) portfolio.pending.step3_seller.warn.part2=Vous n'avez toujours pas confirmé la réception du paiement. Veuillez vérifier {0} si vous avez reçu le paiement. portfolio.pending.step3_seller.openForDispute=Vous n'avez pas confirmé la réception du paiement !\nLe délai maximal alloué pour ce trade est écoulé.\nVeuillez confirmer ou demander l'aide du médiateur. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=Avez-vous reçu le paiement {0} de votre partenaire de trading?\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.name=Veuillez également vérifier que le nom de l'expéditeur indiqué sur le contrat de l'échange correspond au nom qui apparaît sur votre relevé bancaire:\nNom de l'expéditeur, avec le contrat de l'échange: {0}\n\nSi les noms ne sont pas exactement identiques, ne confirmez pas la réception du paiement. Au lieu de cela, ouvrez un litige en appuyant sur \"alt + o\" ou \"option + o\".\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Veuillez noter que dès que vous aurez confirmé la réception, le montant verrouillé pour l'échange sera remis à l'acheteur de XMR et le dépôt de garantie vous sera remboursé.\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Confirmez que vous avez bien reçu le paiement portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=Oui, j'ai reçu le paiement portfolio.pending.step3_seller.onPaymentReceived.signer=IMPORTANT : En confirmant la réception du paiement, vous vérifiez également le compte de la contrepartie et le signez en conséquence. Comme le compte de la contrepartie n'a pas encore été signé, vous devriez retarder la confirmation du paiement le plus longtemps possible afin de réduire le risque de rétrofacturation. portfolio.pending.step5_buyer.groupTitle=Résumé de l'opération réalisée portfolio.pending.step5_buyer.tradeFee=Frais de transaction portfolio.pending.step5_buyer.makersMiningFee=Frais de minage portfolio.pending.step5_buyer.takersMiningFee=Total des frais de minage portfolio.pending.step5_buyer.refunded=Dépôt de garantie remboursé portfolio.pending.step5_buyer.withdrawXMR=Retirer vos Moneros portfolio.pending.step5_buyer.amount=Montant à retirer portfolio.pending.step5_buyer.withdrawToAddress=Retirer vers l'adresse portfolio.pending.step5_buyer.moveToHavenoWallet=Garder les fonds dans le portefeuille Haveno portfolio.pending.step5_buyer.withdrawExternal=Retrait vers un portefeuille externe portfolio.pending.step5_buyer.alreadyWithdrawn=Vos fonds ont déjà été retirés. Merci de vérifier votre historique de transactions. portfolio.pending.step5_buyer.confirmWithdrawal=Confirmer la demande de retrait portfolio.pending.step5_buyer.amountTooLow=Le montant à transférer est inférieur aux frais de transaction et à la valeur min. possible du tx (dust). portfolio.pending.step5_buyer.withdrawalCompleted.headline=Retrait effectué portfolio.pending.step5_buyer.withdrawalCompleted.msg=Vos transactions terminées sont stockées sous /"Historique du portefeuille\".\nVous pouvez voir toutes vos transactions en monero dans \"Fonds/Transactions\" portfolio.pending.step5_buyer.bought=Vous avez acheté portfolio.pending.step5_buyer.paid=Vous avez payé portfolio.pending.step5_seller.sold=Vous avez vendu portfolio.pending.step5_seller.received=Vous avez reçu tradeFeedbackWindow.title=Félicitations pour avoir achevé votre trade tradeFeedbackWindow.msg.part1=Nous aimerions avoir de vos commentaires sur votre expérience. Cela nous aidera à améliorer le logiciel et à aplanir les aspérités. Si vous souhaitez nous faire part de vos commentaires, veuillez remplir ce court sondage (aucune inscription requise) à l'adresse suivante: tradeFeedbackWindow.msg.part2=Si vous avez la moindre question, ou rencontrez un problème, veuillez s'il vous plaît vous mettre en relation avec les autres utilisateurs et contributeurs via le forum Haveno sur: tradeFeedbackWindow.msg.part3=Merci d'utiliser Haveno! portfolio.pending.role=Mon rôle portfolio.pending.tradeInformation=Information sur le trade portfolio.pending.remainingTime=Temps restant portfolio.pending.remainingTimeDetail={0} (jusqu'’à {1}) portfolio.pending.remainingTimeDetail.startsAfter=Commence après {0} confirmations portfolio.pending.tradePeriodInfo=Après {0} confirmations, la période de transaction commence. Selon le mode de paiement utilisé, une période maximale de transaction différente est appliquée. portfolio.pending.tradePeriodWarning=Si le délai est dépassé, l'es deux participants du trade peuvent ouvrir un litige. portfolio.pending.tradeNotCompleted=Trade inachevé dans le temps imparti (jusqu'à {0}) portfolio.pending.tradeProcess=Processus de transaction portfolio.pending.openAgainDispute.msg=Si vous n'êtes pas certain que le message addressé au médiateur ou à l'arbitre soit arrivé (par exemple si vous n'avez pas reçu de réponse dans un délai de 1 jour), n'hésitez pas à réouvrir un litige avec Cmd/ctrl+O. Vous pouvez aussi demander de l'aide en complément sur le forum haveno à [LIEN:https://haveno.community]. portfolio.pending.openAgainDispute.button=Ouvrir à nouveau le litige portfolio.pending.openSupportTicket.headline=Ouvrir un ticket d'assistance portfolio.pending.openSupportTicket.msg=S'il vous plaît n'utilisez seulement cette fonction qu'en cas d'urgence si vous ne pouvez pas voir le bouton \"Open support\" ou \"Ouvrir un litige\.\n\nLorsque vous ouvrez un ticket de support, l'échange sera interrompu et pris en charge par le médiateur ou par l'arbitre. portfolio.pending.timeLockNotOver=Vous devez patienter jusqu'au ≈{0} ({1} blocs de plus) avant de pouvoir ouvrir ouvrir un arbitrage pour le litige. portfolio.pending.error.depositTxNull=La transaction de dépôt est nulle. Vous ne pouvez pas ouvrir un litige sans une transaction de dépôt valide. Allez dans \"Paramètres/Info sur le réseau\" et faites une resynchronisation SPV.\n\nPour obtenir de l'aide, le canal support de l'équipe Haveno est disponible sur Keybase. portfolio.pending.mediationResult.error.depositTxNull=La transaction de dépôt est nulle. Vous pouvez déplacer le trade vers les trades n'ayant pas réussi. portfolio.pending.mediationResult.error.delayedPayoutTxNull=Le paiement de la transaction différée est nul. Vous pouvez déplacer le trade vers les trades échoués. portfolio.pending.error.depositTxNotConfirmed=La transaction de dépôt n'est pas confirmée. Vous ne pouvez pas ouvrir un arbitrage pour le litige avec une transaction de dépôt non confirmée. Veuillez patienter jusqu'à ce qu'elle soit confirmée ou allez à \"Paramètres/Info sur le réseau réseau\" et faites une resynchronisation SPV.\n\nPour obtenir de l'aide, le canal support de l'équipe Haveno est disponible sur Keybase. portfolio.pending.support.headline.getHelp=Besoin d'aide ? portfolio.pending.support.text.getHelp=Si vous rencontrez des problèmes, vous pouvez essayer de contacter votre pair de trading dans le chat de l'échange ou demander à la communauté Haveno sur https://haveno.community. Si votre problème n'est toujours pas résolu, vous pouvez demander l'aide d'un médiateur. portfolio.pending.support.button.getHelp=Ouvrir le chat de trade portfolio.pending.support.headline.halfPeriodOver=Vérifier le paiement portfolio.pending.support.headline.periodOver=Le délai alloué pour ce trade est écoulé. portfolio.pending.support.headline.depositTxMissing=Transaction de dépôt manquante portfolio.pending.support.depositTxMissing=Une transaction de dépôt est manquante pour cette opération. Ouvrez un ticket de support pour contacter un arbitre et obtenir de l’aide. portfolio.pending.mediationRequested=Médiation demandée portfolio.pending.refundRequested=Remboursement demandé portfolio.pending.openSupport=Ouvrir un ticket d'assistance portfolio.pending.supportTicketOpened=Ticket d'assistance ouvert portfolio.pending.communicateWithArbitrator=Veuillez communiquer avec l'arbitre depuis l'écran "Support". portfolio.pending.communicateWithMediator=Veuillez communiquer avec le médiateur dans l'onglet \"Support \". portfolio.pending.disputeOpenedByUser=Vous avez déjà ouvert un litige.\n{0} portfolio.pending.disputeOpenedByPeer=Votre pair de trading à ouvert un litige\n{0} portfolio.pending.noReceiverAddressDefined=Aucune adresse de destinataire définie portfolio.pending.mediationResult.headline=Montant suggéré par la médiation portfolio.pending.mediationResult.info.noneAccepted=Terminez la transaction en acceptant la suggestion du médiateur concernant le paiement de la transaction. portfolio.pending.mediationResult.info.selfAccepted=Vous avez accepté la suggestion du médiateur. En attente que le pair l'accepte également. portfolio.pending.mediationResult.info.peerAccepted=Votre pair de trading a accepté la suggestion du médiateur. L'acceptez-vous également ? portfolio.pending.mediationResult.button=Voir la résolution proposée portfolio.pending.mediationResult.popup.headline=Résultat de la médiation pour la transaction avec l'ID: {0} portfolio.pending.mediationResult.popup.headline.peerAccepted=Votre pair de trading a accepté la suggestion du médiateur pour la transaction {0} portfolio.pending.mediationResult.popup.info=Les frais recommandés par le médiateur sont les suivants: \nVous paierez: {0} \nVotre partenaire commercial paiera: {1} \n\nVous pouvez accepter ou refuser ces frais de médiation. \n\nEn acceptant, vous avez vérifié l'opération de paiement du contrat. Si votre partenaire commercial accepte et vérifie également, le paiement sera effectué et la transaction sera clôturée. \n\nSi l'un de vous ou les deux refusent la proposition, vous devrez attendre le {2} (bloc {3}) pour commencer le deuxième tour de discussion sur le différend avec l'arbitre, et ce dernier étudiera à nouveau le cas. Le paiement sera fait en fonction de ses résultats. \n\nL'arbitre peut facturer une somme modique (la limite supérieure des honoraires: la marge de la transaction) en compensation de son travail. Les deux commerçants conviennent que la suggestion du médiateur est une voie agréable. La demande d'arbitrage concerne des circonstances particulières, par exemple si un professionnel est convaincu que le médiateur n'a pas fait une recommandation de d'indemnisation équitable (ou si l'autre partenaire n'a pas répondu). \n\nPlus de détails sur le nouveau modèle d'arbitrage: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=Vous avez accepté la proposition de paiement du médiateur, mais il semble que votre contrepartie ne l'ait pas acceptée. \n\nUne fois que le temps de verrouillage atteint {0} (bloc {1}), vous pouvez ouvrir le second tour de litige pour que l'arbitre réétudie le cas et prend une nouvelle décision de dépenses. \n\nVous pouvez trouver plus d'informations sur le modèle d'arbitrage sur:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=Refuser et demander un arbitrage portfolio.pending.mediationResult.popup.alreadyAccepted=Vous avez déjà accepté portfolio.pending.failedTrade.taker.missingTakerFeeTx=Le frais de transaction du preneur est manquant.\n\nSans ce tx, le trade ne peut être complété. Aucun fonds ont été verrouillés et aucun frais de trade a été payé. Vous pouvez déplacer ce trade vers les trade échoués. portfolio.pending.failedTrade.maker.missingTakerFeeTx=Le frais de transaction du pair preneur est manquant.\n\nSans ce tx, le trade ne peut être complété. Aucun fonds ont été verrouillés. Votre offre est toujours valable pour les autres traders, vous n'avez donc pas perdu le frais de maker. Vous pouvez déplacer ce trade vers les trades échoués. portfolio.pending.failedTrade.missingDepositTx=Une transaction de dépôt est manquante.\n\nCette transaction est nécessaire pour compléter la transaction. Veuillez vous assurer que votre portefeuille est entièrement synchronisé avec la blockchain Monero.\n\nVous pouvez déplacer cette transaction dans la section « Transactions échouées » pour la désactiver. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=La transaction de paiement différée est manquante, mais les fonds ont été verrouillés dans la transaction de dépôt.\n\nVeuillez NE PAS envoyer de Fiat ou d'crypto au vendeur de XMR, car avec le tx de paiement différé, le jugemenbt ne peut être ouvert. À la place, ouvrez un ticket de médiation avec Cmd/Ctrl+O. Le médiateur devrait suggérer que les deux pair reçoivent tous les deux le montant total de leurs dépôts de sécurité (le vendeur aussi doit reçevoir le montant total du trade). De cette manière, il n'y a pas de risque de non sécurité, et seuls les frais du trade sont perdus.\n\nVous pouvez demander le remboursement des frais de trade perdus ici;\n[LIEN:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=La transaction de paiement différée est manquante, mais les fonds ont été verrouillés dans la transaction de dépôt.\n\nSi l'acheteur n'a pas non plus la transaction de paiement différée, il sera informé du fait de ne PAS envoyer le paiement et d'ouvrir un ticket de médiation à la place. Vous devriez aussi ouvrir un ticket de médiation avec Cmd/Ctrl+o.\n\nSi l'acheteur n'a pas encore envoyé le paiement, le médiateur devrait suggérer que les deux pairs reçoivent le montant total de leurs dépôts de sécurité (le vendeur doit aussi reçevoir le montant total du trade). Sinon, le montant du trade revient à l'acheteur.\n\nVous pouvez effectuer une demande de remboursement pour les frais de trade perdus ici: [LIEN:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=Il y'a eu une erreur durant l'exécution du protocole de trade.\n\nErreur: {0}\n\nIl est possible que cette erreur ne soit pas critique, et que le trade puisse être complété normalement. Si vous n'en êtes pas sûr, ouvrez un ticket de médiation pour avoir des conseils de la part des médiateurs de Haveno.\n\nSi cette erreur est critique et que le trade ne peut être complété, il est possible que vous ayez perdu le frais du trade. Effectuez une demande de remboursement ici: [LIEN:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.missingContract=Le contrat de trade n'est pas complété.\n\nCe trade ne peut être complété et il est possible que vous ayiez perdu votre frais de trade. Dans ce cas, vous pouvez demander un remboursement des frais de trade perdus ici: [LIEN:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=Le protocole de trade a rencontré quelques problèmes/\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=Le protocole de trade a rencontré un problème critique.\n\n{0}\n\nVoulez-vous déplacer ce trade vers les trades échoués?\n\nVous ne pouvez pas ouvrir de médiations ou de jugements depuis la liste des trades échoués, mais vous pouvez redéplacer un trade échoué vers l'écran des trades ouverts quand vous le souhaitez. portfolio.pending.failedTrade.txChainValid.moveToFailed=Il y a des problèmes avec cet accord de transaction. \n\n{0}\n\nLa transaction de devis a été validée et les fonds ont été bloqués. Déplacer la transaction vers une transaction échouée uniquement si elle est certaine. Cela peut empêcher les options disponibles pour résoudre le problème. \n\nÊtes-vous sûr de vouloir déplacer cette transaction vers la transaction échouée? \n\nVous ne pouvez pas ouvrir une médiation ou un arbitrage dans une transaction échouée, mais vous pouvez déplacer une transaction échouée vers la transaction incomplète à tout moment. portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Déplacer le trade vers les trades échoués portfolio.pending.failedTrade.warningIcon.tooltip=Cliquer pour avoir plus de détails à propos des problèmes ayant eu lieu lors de ce trade portfolio.failed.revertToPending.popup=Voulez-vous déplacer ce trade vers les trades ouverts? portfolio.failed.revertToPending=Déplacer le trade vers les trades ouverts portfolio.closed.completed=Terminé portfolio.closed.ticketClosed=Arbitré portfolio.closed.mediationTicketClosed=Ayant fait l'objet d'une médiation portfolio.closed.canceled=Annulé portfolio.failed.Failed=Échec portfolio.failed.unfail=Avant de procéder, veuillez vous assurer que vous avez une sauvegarde de votre répertoire de données!\nVoulez-vous redéplacer de trade vers les trades ouverts?\nC'est une manière de déverrouiller les fonds coincés dans un trade échoué. portfolio.failed.cantUnfail=Ce trade ne peut être redéplacé vers les trades ouverts pour l'instant.\nVeuillez réessayer après la complétion du/des trade(s) {0} portfolio.failed.depositTxNull=Le trade ne peut être reconverti en trade ouvert. La transaction de dépôt est nulle. portfolio.failed.delayedPayoutTxNull=Le trade ne peut être reconverti en trade ouvert. La transaction de paiement différée est nulle. #################################################################### # Funds #################################################################### funds.tab.deposit=Recevoir des fonds funds.tab.withdrawal=Envoyer des fonds funds.tab.reserved=Fonds reservés funds.tab.locked=Fonds vérouillés funds.tab.transactions=Transactions funds.deposit.unused=Inutilisé funds.deposit.usedInTx=Utilisé dans {0} transaction(s) funds.deposit.fundHavenoWallet=Alimenter le portefeuille Haveno funds.deposit.noAddresses=Aucune adresse de dépôt n'a encore été générée funds.deposit.fundWallet=Alimenter votre portefeuille funds.deposit.withdrawFromWallet=Transférer des fonds depuis le portefeuille funds.deposit.amount=Montant en XMR (optionnel) funds.deposit.generateAddress=Générer une nouvelle adresse funds.deposit.generateAddressSegwit=Format segwit natif (Bech32) funds.deposit.selectUnused=Merci de sélectionner une adresse inutilisée dans le champ ci-dessus plutôt que d'en générer une nouvelle. funds.withdrawal.arbitrationFee=Frais d'arbitrage funds.withdrawal.inputs=Sélection de la valeur à saisir funds.withdrawal.useAllInputs=Utiliser toutes les valeurs disponibles funds.withdrawal.useCustomInputs=Utiliser une valeur de saisie personnalisée funds.withdrawal.receiverAmount=Montant du destinataire funds.withdrawal.sendMax=Envoyer max disponible funds.withdrawal.senderAmount=Montant de l'expéditeur funds.withdrawal.feeExcluded=Montant excluant les frais de minage funds.withdrawal.feeIncluded=Montant incluant frais de minage funds.withdrawal.fromLabel=Retirer depuis l'adresse funds.withdrawal.toLabel=Retirer vers l'adresse funds.withdrawal.memoLabel=Résumé du retrait funds.withdrawal.memo=Optionnellement, complétez le mémo funds.withdrawal.withdrawButton=Retrait selectionné funds.withdrawal.noFundsAvailable=Aucun fonds n'est disponible pour le retrait funds.withdrawal.confirmWithdrawalRequest=Confirmer la requête de retrait funds.withdrawal.withdrawMultipleAddresses=Retrait depuis plusieurs adresses ({0}) funds.withdrawal.withdrawMultipleAddresses.tooltip=Retrait depuis plusieurs adresses:\n{0} funds.withdrawal.notEnoughFunds=Vous ne disposez pas de suffisamment de fonds dans votre portefeuille. funds.withdrawal.selectAddress=Sélectionnez une adresse source depuis le champ funds.withdrawal.setAmount=Définir le montant à retirer funds.withdrawal.fillDestAddress=Complétez votre adresse de destination funds.withdrawal.warn.noSourceAddressSelected=Vous devez sélectionner une adresse source dans le champ ci-dessus. funds.withdrawal.warn.amountExceeds=Vous ne disposez pas de fonds suffisants provenant de l'adresse sélectionnée.\nEnvisagez de sélectionner plusieurs adresses dans le champ ci-dessus ou changez les frais pour inclure les frais du mineur. funds.reserved.noFunds=Aucun fonds n'est réservé pour les ordres en cours funds.reserved.reserved=Réversé dans votre portefeuille local pour l'ordre avec l'ID: {0} funds.locked.noFunds=Aucun fonds n'est verrouillé dans les trades funds.locked.locked=Vérouillé en multisig pour le trade avec l'ID: {0} funds.tx.direction.sentTo=Envoyer à: funds.tx.direction.receivedWith=Reçu depuis: funds.tx.direction.genesisTx=Depuis le tx Genesis: funds.tx.createOfferFee=Frais du maker et du tx: {0} funds.tx.takeOfferFee=Frais du taker et du tx: {0} funds.tx.multiSigDeposit=Dépôt multisig: {0} funds.tx.multiSigPayout=Versement Multisig: {0} funds.tx.disputePayout=Versement du litige: {0} funds.tx.disputeLost=Cas de litige perdu: {0} funds.tx.collateralForRefund=Remboursement du dépôt de garantie: {0} funds.tx.timeLockedPayoutTx=Tx de paiement verrouillée dans le temps: {0} funds.tx.refund=Remboursement venant de l'arbitrage: {0} funds.tx.unknown=Raison inconnue: {0} funds.tx.noFundsFromDispute=Aucun remboursement en cas de litige funds.tx.receivedFunds=Fonds reçus funds.tx.withdrawnFromWallet=Retiré depuis le portefeuille funds.tx.memo=Résumé funds.tx.noTxAvailable=Pas de transactions disponibles funds.tx.revert=Revertir funds.tx.txSent=Transaction envoyée avec succès vers une nouvelle adresse dans le portefeuille local haveno. funds.tx.direction.self=Envoyé à vous même funds.tx.dustAttackTx=dust reçues funds.tx.dustAttackTx.popup=Cette transaction va envoyer un faible montant en XMR sur votre portefeuille ce qui pourrait constituer une tentative d'espionnage de la part de sociétés qui analyse la chaine.\n\nSi vous utilisez cette transaction de sortie des données dans le cadre d'une transaction représentant une dépense il sera alors possible de comprendre que vous êtes probablement aussi le propriétaire de l'autre adresse (coin merge).\n\nAfin de protéger votre vie privée, le portefeuille Haveno ne tient pas compte de ces "dust outputs" dans le cadre des transactions de vente et dans l'affichage de la balance. Vous pouvez définir une quantité seuil lorsqu'une "output" est considérée comme poussière dans les réglages. #################################################################### # Support #################################################################### support.tab.mediation.support=Médiation support.tab.arbitration.support=Arbitrage support.tab.legacyArbitration.support=Conclusion d'arbitrage support.tab.ArbitratorsSupportTickets=Tickets de {0} support.sigCheck.button=Vérifier la signature support.sigCheck.popup.info=Collez le message récapitulatif du processus d'arbitrage. Avec cet outil, n'importe quel utilisateur peut vérifier si la signature de l'arbitre correspond au message récapitulatif. support.sigCheck.popup.header=Vérifier la signature du résultat du litige support.sigCheck.popup.msg.label=Message de résumé support.sigCheck.popup.msg.prompt=Copiez et collez le message résumant le litige support.sigCheck.popup.result=Résultat de la validation support.sigCheck.popup.success=La signature est valide support.sigCheck.popup.failed=Vérification de la signature échouée support.sigCheck.popup.invalidFormat=Le message n'est pas au format attendu. Copiez et collez le message résumant la dispute. support.reOpenByTrader.prompt=Êtes-vous sûr de vouloir réouvrir le litige? support.reOpenButton.label=Réouvrir support.sendNotificationButton.label=Notification privée support.reportButton.label=Effectuer un rapport support.fullReportButton.label=Tous les litiges support.noTickets=Il n'y a pas de tickets ouverts support.sendingMessage=Envoi du message... support.receiverNotOnline=Le destinataire n'est pas en ligne. Le message est enregistré dans leur boîte mail. support.sendMessageError=Échec de l'envoi du message. Erreur: {0} support.receiverNotKnown=Destinataire inconnu support.wrongVersion=L'ordre relatif au litige en question a été créé avec une ancienne version de Haveno.\nVous ne pouvez pas clore ce litige avec votre version de l'application.\n\nVeuillez utiliser une version plus ancienne avec la version du protocole {0} support.openFile=Ouvrir le fichier à joindre (taille max. du fichier : {0} kb) support.attachmentTooLarge=La taille totale de vos pièces jointes est de {0} ko ce qui dépasse la taille maximale autorisée de {1} ko pour les messages. support.maxSize=La taille maximale autorisée pour le fichier est {0} kB. support.attachment=Pièces jointes support.tooManyAttachments=Vous ne pouvez envoyer plus de 3 pièces jointes dans un message. support.save=Sauvegarder le fichier sur le disque support.messages=Messages support.input.prompt=Entrer le message... support.send=Envoyer support.addAttachments=Ajouter des pièces jointes support.closeTicket=Fermer le ticket support.attachments=Pièces jointes: support.savedInMailbox=Message sauvegardé dans la boîte mail du destinataire support.arrived=Message reçu par le destinataire support.acknowledged=Réception du message confirmée par le destinataire support.error=Le destinataire n'a pas pu traiter le message. Erreur : {0} support.buyerAddress=Adresse de l'acheteur XMR support.sellerAddress=Adresse du vendeur XMR support.role=Rôle support.agent=Agent d'assistance support.state=État support.chat=Chat support.preparing=Préparation support.requested=Demandé support.closed=Fermé support.open=Ouvert support.process=Processus support.buyerMaker=Acheteur XMR/Maker support.sellerMaker=Vendeur XMR/Maker support.buyerTaker=Acheteur XMR/Taker support.sellerTaker=Vendeur XMR/Taker support.backgroundInfo=Haveno n'est pas une entreprise, donc il gère les litiges différemment.\n\nLes traders peuvent communiquer au sein de l'application via une discussion sécurisée sur l'écran des transactions ouvertes pour tenter de résoudre les litiges eux-mêmes. Si cela n'est pas suffisant, un médiateur évaluera la situation et décidera d'un paiement des fonds de transaction. support.initialInfo=Veuillez entrer une description de votre problème dans le champ texte ci-dessous. Ajoutez autant d'informations que possible pour accélérer le temps de résolution du litige.\n\nVoici une check list des informations que vous devez fournir :\n● Si vous êtes l'acheteur XMR : Avez-vous effectué le paiement Fiat ou Crypto ? Si oui, avez-vous cliqué sur le bouton "paiement commencé" dans l'application ?\n● Si vous êtes le vendeur XMR : Avez-vous reçu le paiement Fiat ou Crypto ? Si oui, avez-vous cliqué sur le bouton "paiement reçu" dans l'application ?\n● Quelle version de Haveno utilisez-vous ?\n● Quel système d'exploitation utilisez-vous ?\n● Si vous avez rencontré un problème avec des transactions qui ont échoué, veuillez envisager de passer à un nouveau répertoire de données.\nParfois, le répertoire de données est corrompu et conduit à des bogues étranges. \nVoir : https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nVeuillez vous familiariser avec les règles de base du processus de règlement des litiges :\n● Vous devez répondre aux demandes des {0} dans les 2 jours.\n● Les médiateurs répondent dans un délai de 2 jours. Les arbitres répondent dans un délai de 5 jours ouvrables.\n● Le délai maximum pour un litige est de 14 jours.\n● Vous devez coopérer avec les {1} et fournir les renseignements qu'ils demandent pour faire valoir votre cause.\n● Vous avez accepté les règles décrites dans le document de litige dans l'accord d'utilisation lorsque vous avez lancé l'application pour la première fois.\n\nVous pouvez en apprendre davantage sur le processus de litige à l'adresse suivante {2} support.systemMsg=Message du système: {0} support.youOpenedTicket=Vous avez ouvert une demande de support.\n\n{0}\n\nHaveno version: {1} support.youOpenedDispute=Vous avez ouvert une demande de litige.\n\n{0}\n\nHaveno version: {1} support.youOpenedDisputeForMediation=Vous avez demandé une médiation.\n\n{0}\n\nVersion de Haveno: {1} support.peerOpenedTicket=Votre pair de trading a demandé une assistance en raison de problèmes techniques.\n\n{0}\n\nVersion de Haveno: {1} support.peerOpenedDispute=Votre pair de trading a fait une demande de litige.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDisputeForMediation=Votre pair de trading a demandé une médiation.\n\n{0}\n\nVersion de Haveno: {1} support.mediatorsDisputeSummary=Message système: Résumé de la dispute du médiateur:\n{0} support.mediatorsAddress=Adresse du nœud du médiateur: {0} support.warning.disputesWithInvalidDonationAddress=La transaction de paiement différé a été utilisée pour une adresse de destinataire indisponible. Il ne correspond aux paramètres dans aucun DAO de l'adresse de donation valide. \n\nCela peut être une escroquerie. Veuillez informer le développeur et ne fermez pas le dossier jusqu'à ce que le problème est résolu! \n\nAdresse pour les litiges: {0} \n\nAdresse de donation dans tous les paramètres DAO: {1} \n\nTransaction: {2} {3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nVoulez-vous toujours fermer le litige? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nVous ne devez pas effectuer le paiement. support.warning.traderCloseOwnDisputeWarning=Les traders peuvent uniquement fermer eux-même les tickets d'assistance quand le trade a été payé. support.info.disputeReOpened=Le ticket de litige a été réouvert. #################################################################### # Settings #################################################################### settings.tab.preferences=Préférences settings.tab.network=Info sur le réseau settings.tab.about=À propos setting.preferences.general=Préférences générales setting.preferences.explorer=Exploreur Monero setting.preferences.deviation=Ecart maximal par rapport au prix du marché setting.preferences.avoidStandbyMode=Éviter le mode veille setting.preferences.useSoundForNotifications=Jouer des sons pour les notifications setting.preferences.autoConfirmXMR=Auto-confirmation XMR setting.preferences.autoConfirmEnabled=Activé setting.preferences.autoConfirmRequiredConfirmations=Confirmations requises setting.preferences.autoConfirmMaxTradeSize=Montant maximum du trade (XMR) setting.preferences.autoConfirmServiceAddresses=URLs de l'explorateur de Monero (utilise Tor, à part pour l'hôte local, les addresses IP locales, et les noms de domaine en *.local) setting.preferences.deviationToLarge=Les valeurs supérieures à {0}% ne sont pas autorisées. setting.preferences.txFee=Frais de transaction du retrait (satoshis/vbyte) setting.preferences.useCustomValue=Utiliser une valeur personnalisée setting.preferences.txFeeMin=Les frais de transaction doivent être d'au moins {0} satoshis/vBit setting.preferences.txFeeTooLarge=Votre sasie est au-delà de toute valeur raisonnable (plus de 5000 satoshis/vBit). Les frais de transaction sont habituellement de l'ordre de 50-400 satoshis/vBit. setting.preferences.ignorePeers=Pairs ignorés [adresse onion:port] setting.preferences.ignoreDustThreshold=Valeur de l'output considérée comme "non-dust" minimale setting.preferences.currenciesInList=Devises disponibles dans le flux de cotation du marché setting.preferences.prefCurrency=Devise privilégiée setting.preferences.displayTraditional=Afficher les monnaies nationales setting.preferences.noTraditional=Il n'y a pas de devise nationale sélectionnée setting.preferences.cannotRemovePrefCurrency=Vous ne pouvez pas enlever la devise choisie pour l'affichage. setting.preferences.displayCryptos=Afficher les cryptos setting.preferences.noCryptos=Il n'y a pas d'cryptos sélectionnés setting.preferences.addTraditional=Ajouter une devise nationale setting.preferences.addCrypto=Ajouter un crypto setting.preferences.displayOptions=Afficher les options setting.preferences.showOwnOffers=Montrer mes ordres dans le livre des ordres setting.preferences.useAnimations=Utiliser des animations setting.preferences.useDarkMode=Utiliser le mode sombre setting.preferences.useLightMode=Utiliser le mode clair setting.preferences.sortWithNumOffers=Trier les listes de marché avec le nombre d'ordres/de transactions setting.preferences.onlyShowPaymentMethodsFromAccount=Masquer les méthodes de paiement non supportées setting.preferences.denyApiTaker=Refuser les preneurs utilisant l'API setting.preferences.notifyOnPreRelease=Recevoir les notifications de pré-sortie setting.preferences.resetAllFlags=Réinitialiser toutes les balises de notification \"Don't show again\" settings.preferences.languageChange=Un redémarrage est nécessaire pour appliquer le changement de langue à tous les écrans. settings.preferences.supportLanguageWarning=En cas de litige, veuillez noter que l'arbitrage est traité en {0}. settings.preferences.editCustomExplorer.headline=Paramètres de l'explorateur settings.preferences.editCustomExplorer.description=Choisissez un explorateur défini par le système depuis la liste à gauche, et/où customisez-le pour satisfaire vos préférences. settings.preferences.editCustomExplorer.available=Explorateurs disponibles settings.preferences.editCustomExplorer.chosen=Paramètres choisis pour l'explorateur settings.preferences.editCustomExplorer.name=Nom settings.preferences.editCustomExplorer.txUrl=URL de la transaction settings.preferences.editCustomExplorer.addressUrl=Addresse URL setting.info.headline=Nouvelle fonctionnalité de confidentialité des données settings.preferences.sensitiveDataRemoval.msg=Pour protéger la vie privée de vous-même et des autres traders, Haveno a l'intention de supprimer les données sensibles des anciennes transactions. Cela est particulièrement important pour les transactions en fiat qui peuvent inclure des informations bancaires.\n\nIl est recommandé de régler ce délai aussi bas que possible, par exemple 60 jours. Cela signifie que les transactions datant de plus de 60 jours verront leurs données sensibles supprimées, tant qu'elles sont terminées. Les transactions terminées se trouvent dans l'onglet Portefeuille / Historique. settings.net.xmrHeader=Réseau Monero settings.net.p2pHeader=Le réseau Haveno settings.net.onionAddressLabel=Mon adresse onion settings.net.xmrNodesLabel=Utiliser des nœuds Monero personnalisés settings.net.moneroPeersLabel=Pairs connectés settings.net.connection=Connexion settings.net.connected=Connecté settings.net.useTorForXmrJLabel=Utiliser Tor pour le réseau Monero settings.net.moneroNodesLabel=Nœuds Monero pour se connecter à settings.net.useProvidedNodesRadio=Utiliser les nœuds Monero Core fournis settings.net.usePublicNodesRadio=Utiliser le réseau Monero public settings.net.useCustomNodesRadio=Utiliser des nœuds Monero Core personnalisés settings.net.warn.usePublicNodes=Si vous utilisez des nœuds publics Monero, vous êtes exposé à tout risque lié à l'utilisation de nœuds distants non fiables.\n\nVeuillez lire plus de détails sur [HYPERLINK:https://www.getmonero.org/resources/moneropedia/remote-node.html].\n\nÊtes-vous sûr de vouloir utiliser des nœuds publics ? settings.net.warn.usePublicNodes.useProvided=Non, utiliser les nœuds fournis. settings.net.warn.usePublicNodes.usePublic=Oui, utiliser un réseau public settings.net.warn.useCustomNodes.B2XWarning=Veuillez vous assurer que votre nœud Monero est un nœud Monero Core de confiance !\n\nLa connexion à des nœuds qui ne respectent pas les règles du consensus de Monero Core peut corrompre votre portefeuille et causer des problèmes dans le processus de trading.\n\nLes utilisateurs qui se connectent à des nœuds qui ne respectent pas les règles du consensus sont responsables des dommages qui en résultent. Tout litige qui en résulte sera tranché en faveur de l'autre pair. Aucune assistance technique ne sera apportée aux utilisateurs qui ignorent ces mécanismes d'alertes et de protections ! settings.net.warn.invalidXmrConfig=La connection au réseau Monero a échoué car votre configuration est invalide.\n\nVotre configuration a été réinitialisée afin d'utiliser les noeuds Monero fournis à la place. Vous allez avoir besoin de relancer l'application. settings.net.localhostXmrNodeInfo=Information additionnelle : Haveno cherche un noeud Monero local au démarrage. Si il est trouvé, Haveno communiquera avec le réseau Monero uniquement à travers ce noeud. settings.net.p2PPeersLabel=Pairs connectés settings.net.onionAddressColumn=Adresse onion settings.net.creationDateColumn=Établi settings.net.connectionTypeColumn=In/Out settings.net.sentDataLabel=Statistiques des données envoyées settings.net.receivedDataLabel=Statistiques des données reçues settings.net.chainHeightLabel=Hauteur du dernier block XMR settings.net.roundTripTimeColumn=Roundtrip settings.net.sentBytesColumn=Envoyé settings.net.receivedBytesColumn=Reçu settings.net.peerTypeColumn=Type de pair settings.net.openTorSettingsButton=Ouvrez les paramètres de Tor settings.net.versionColumn=Version settings.net.subVersionColumn=Sous-version settings.net.heightColumn=Hauteur settings.net.needRestart=Vous devez redémarrer l'application pour appliquer cette modification.\nVous voulez faire cela maintenant? settings.net.notKnownYet=Pas encore connu... settings.net.sentData=Données envoyées: {0}, {1} messages, {2} messages/seconde settings.net.receivedData=Données reçues: {0}, {1} messages, {2} messages/seconde settings.net.chainHeight=Hauteur de la chaîne des pairs Monero: {0} settings.net.ips=[IP address:port | host name:port | onion address:port] (séparés par des virgules). Le port peut être ignoré si utilisé par défaut (8333). settings.net.seedNode=Seed node settings.net.directPeer=Pair (direct) settings.net.initialDataExchange={0}[Amorçage] settings.net.peer=Pair settings.net.inbound=inbound settings.net.outbound=outbound setting.about.aboutHaveno=À propos de Haveno setting.about.about=Haveno est un logiciel libre qui facilite l'échange de Moneros avec les devises nationales (et d'autres cryptomonnaies) au moyen d'un réseau pair-to-pair décentralisé, de manière à protéger au mieux la vie privée des utilisateurs. Pour en savoir plus sur Haveno, consultez la page Web du projet. setting.about.web=Page web de Haveno setting.about.code=Code source setting.about.agpl=Licence AGPL setting.about.support=Soutenir Haveno setting.about.def=Haveno n'est pas une entreprise, c'est un projet ouvert vers la communauté. Si vous souhaitez participer ou soutenir Haveno, veuillez suivre les liens ci-dessous. setting.about.contribute=Contribuer setting.about.providers=Fournisseurs de données setting.about.apisWithFee=Haveno utilise les indices de prix Haveno pour les prix des marchés Traditional et Crypto, et Haveno utilise les noeuds du Mempool pour estimer les frais de minage. setting.about.apis=Haveno utilise les indices de prix Haveno pour les prix des marchés Traditional et Crypto. setting.about.pricesProvided=Prix de marché fourni par setting.about.feeEstimation.label=Estimation des frais de minage fournie par setting.about.versionDetails=Détails sur la version setting.about.version=Version de l'application setting.about.subsystems.label=Versions des sous-systèmes setting.about.subsystems.val=Version du réseau: {0}; version des messages P2P: {1}; Version DB Locale: {2}; Version du protocole de trading: {3} setting.about.shortcuts=Raccourcis setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' ou 'alt + {0}' ou 'cmd + {0}' setting.about.shortcuts.menuNav=Naviguer dans le menu principal setting.about.shortcuts.menuNav.value=Pour naviguer dans le menu principal, appuyez sur: 'Ctrl' ou 'alt' ou 'cmd' avec une touche numérique entre '1-9'. setting.about.shortcuts.close=Fermer Haveno setting.about.shortcuts.close.value='Ctrl + {0}' ou 'cmd + {0}' ou 'Ctrl + {1}' ou 'cmd + {1}' setting.about.shortcuts.closePopup=Fermer le popup ou la fenêtre de dialogue setting.about.shortcuts.closePopup.value=Touche 'ECHAP' setting.about.shortcuts.chatSendMsg=Envoyer un message chat au trader setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTRÉE' ou 'alt + ENTREE' ou 'cmd + ENTRÉE' setting.about.shortcuts.openDispute=Ouvrir un litige setting.about.shortcuts.openDispute.value=Sélectionnez l'échange en cours et cliquez sur: {0} setting.about.shortcuts.walletDetails=Ouvrir la fenêtre avec les détails sur le portefeuille setting.about.shortcuts.openEmergencyXmrWalletTool=Ouvrir l'outil de portefeuille d'urgence pour XMR setting.about.shortcuts.showTorLogs=Basculer le niveau de log pour les messages Tor entre DEBUG et WARN setting.about.shortcuts.manualPayoutTxWindow=Ouvrir la fenêtre pour le paiement manuel à partir du tx de dépôt Multisig 2of2 setting.about.shortcuts.removeStuckTrade=Ouvrez la popup pour déplacer ce trade échoué vers l'onglet des trades ouverts. setting.about.shortcuts.removeStuckTrade.value=Sélectionnez l'échange échoué et appuyez sur: {0} setting.about.shortcuts.registerArbitrator=Inscrire l'arbitre (médiateur/arbitre seulement) setting.about.shortcuts.registerArbitrator.value=Naviguez jusqu'au compte et appuyez sur: {0} setting.about.shortcuts.registerMediator=Inscrire le médiateur (médiateur/arbitre seulement) setting.about.shortcuts.registerMediator.value=Naviguez jusqu'au compte et appuyez sur: {0} setting.about.shortcuts.openSignPaymentAccountsWindow=Ouvrir la fenêtre pour la signature de l'âge du compte (anciens arbitres seulement) setting.about.shortcuts.openSignPaymentAccountsWindow.value=Naviguer vers l'ancienne vue de l'arbitre et appuyer sur: {0} setting.about.shortcuts.sendAlertMsg=Envoyer un message d'alerte ou de mise à jour (activité privilégiée) setting.about.shortcuts.sendFilter=Définir le filtre (activité privilégiée) setting.about.shortcuts.sendPrivateNotification=Envoyer une notification privée à un pair (activité privilégiée) setting.about.shortcuts.sendPrivateNotification.value=Ouvrez l'information du pair via l'avatar et appuyez sur: {0} setting.info.headline=Nouvelle fonctionnalité, l'auto-confirmation XMR setting.info.msg=Vous n'avez pas saisi l'ID et la clé de transaction. \n\nSi vous ne fournissez pas ces données, votre partenaire commercial ne peut pas utiliser la fonction de confirmation automatique pour libérer rapidement le XMR après avoir reçu le XMR.\nEn outre, Haveno demande aux expéditeurs XMR de fournir ces informations aux médiateurs et aux arbitres en cas de litige.\nPlus de détails sont dans Haveno Wiki: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades] #################################################################### # Account #################################################################### account.tab.mediatorRegistration=Enregistrement du médiateur account.tab.refundAgentRegistration=Enregistrement de l'agent de remboursement account.tab.signing=Signature en cours account.info.headline=Bienvenue sur votre compte Haveno account.info.msg=Ici, vous pouvez ajouter des comptes de trading en devises nationales et en cryptos et créer une sauvegarde de votre portefeuille ainsi que des données de votre compte.\n\nUn nouveau portefeuille Monero a été créé un premier lancement de Haveno.\n\nNous vous recommandons vivement d'écrire les mots-clés de votre seed de portefeuille Monero (voir l'onglet en haut) et d'envisager d'ajouter un mot de passe avant le transfert de fonds. Les dépôts et retraits de Monero sont gérés dans la section \"Fonds\".\n\nNotice de confidentialité et de sécurité : Haveno étant une plateforme d'échange décentralisée, toutes vos données sont conservées sur votre ordinateur. Il n'y a pas de serveurs, nous n'avons donc pas accès à vos informations personnelles, à vos fonds ou même à votre adresse IP. Les données telles que les numéros de compte bancaire, les adresses crypto & Monero, etc ne sont partagées avec votre pair de trading que pour effectuer les transactions que vous initiez (en cas de litige, le médiateur et l’arbitre verront les mêmes données que votre pair de trading). account.menu.paymentAccount=Comptes en devise nationale account.menu.altCoinsAccountView=Compte Cryptos account.menu.password=Mot de passe du portefeuille account.menu.seedWords=Seed du portefeuille account.menu.walletInfo=Information du portefeuille account.menu.backup=Sauvegarde account.menu.notifications=Notifications account.menu.walletInfo.balance.headLine=Solde du portefeuille account.menu.walletInfo.balance.info=Ceci montre le solde du portefeuille interne en incluant les transactions non-confirmées.\nPour le XMR, le solde du portefeuille interne affiché ci-dessous devrait correspondre à la somme des soldes 'Disponibles' et 'Réservés' affichés en haut à droite de cette fenêtre. account.menu.walletInfo.xpub.headLine=Afficher les clés (clés xpub) account.menu.walletInfo.walletSelector={0} {1} portefeuille account.menu.walletInfo.path.headLine=Chemin du trousseau HD account.menu.walletInfo.path.info=Si vous importez vos mots de graine dans un autre portefeuille (comme Electrum), vous aurez besoin de définir le chemin. Ceci devrait être effectué uniquement en cas d'urgence quand vous perdez accès au portefeuille Haveno et au répertoire de données.\nGardez à l'esprit que dépenser des fonds depuis un portefeuille autre que Haveno peut dérégler les structures de données internes de Haveno associées au données du portefeuille, ce qui peut mener à des trades échoués.\n\nN'envoyez JAMAIS de BSQ depuis un portefeuille autre que Haveno, cela va probablement conduire à une transaction BSQ invalide, vous faisant ainsi perdre votre BSQ. account.menu.walletInfo.openDetails=Afficher les détails bruts du portefeuille et les clés privées ## TODO should we rename the following to a gereric name? account.arbitratorRegistration.pubKey=Clé publique account.arbitratorRegistration.register=S'inscrire account.arbitratorRegistration.registration={0} Enregistrement account.arbitratorRegistration.revoke=Révoquer account.arbitratorRegistration.info.msg=Veuillez noter que vous devez rester disponible pendant 15 jours après la révocation, car il se peut que des échanges vous impliquent comme {0}. Le délai d'échange maximal autorisé est de 8 jours et la procédure de contestation peut prendre jusqu'à 7 jours. account.arbitratorRegistration.warn.min1Language=Vous devez définir au moins 1 langue.\nNous avons ajouté la langue par défaut pour vous. account.arbitratorRegistration.removedSuccess=Vous avez supprimé votre inscription au réseau Haveno avec succès. account.arbitratorRegistration.removedFailed=Impossible de supprimer l'enregistrement.{0} account.arbitratorRegistration.registerSuccess=Vous vous êtes inscrit au réseau Haveno avec succès. account.arbitratorRegistration.registerFailed=Impossible de terminer l'enregistrement.{0} account.crypto.yourCryptoAccounts=Vos comptes crypto account.crypto.popup.wallet.msg=Veuillez vous assurer que vous respectez les exigences relatives à l'utilisation des {0} portefeuilles, selon les conditions présentées sur la page {1} du site.\nL'utilisation des portefeuilles provenant de plateformes de trading centralisées où (a) vous ne contrôlez pas vos clés ou (b) qui ne disposent pas d'un portefeuille compatible est risquée : cela peut entraîner la perte des fonds échangés!\nLe médiateur et l'arbitre ne sont pas des spécialistes {2} et ne pourront pas intervenir dans ce cas. account.crypto.popup.wallet.confirm=Je comprends et confirme que je sais quel portefeuille je dois utiliser. # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=Pour échanger UPX sur Haveno, vous devez comprendre et respecter les exigences suivantes: \n\nPour envoyer UPX, vous devez utiliser le portefeuille officiel UPXmA GUI ou le portefeuille UPXmA CLI avec le logo store-tx-info activé (valeur par défaut dans la nouvelle version) . Assurez-vous d'avoir accès à la clé tx, car elle est nécessaire dans l'état du litige. monero-wallet-cli (à l'aide de la commande get_Tx_key) monero-wallet-gui: sur la page Avancé> Preuve / Vérification. \n\nCes transactions ne sont pas vérifiables dans le navigateur blockchain ordinaire. \n\nEn cas de litige, vous devez fournir à l'arbitre les informations suivantes: \n\n- Clé privée Tx- hachage de transaction- adresse publique du destinataire \n\nSi vous ne fournissez pas les informations ci-dessus ou si vous utilisez un portefeuille incompatible, vous perdrez le litige. En cas de litige, l'expéditeur UPX est responsable de fournir la vérification du transfert UPX à l'arbitre. \n\nAucun paiement d'identité n'est requis, juste une adresse publique commune. \n\nSi vous n'êtes pas sûr du processus, veuillez visiter le canal UPXmA Discord (https://discord.gg/vhdNSrV) ou le groupe d'échanges Telegram (https://t.me/uplexaOfficial) pour plus d'informations. # suppress inspection "UnusedProperty" account.crypto.popup.arq.msg=Le trading d'ARQ sur Haveno exige que vous compreniez et remplissiez les exigences suivantes:\n\nPour envoyer des ARQ, vous devez utiliser soit le portefeuille officiel ArQmA GUI soit le portefeuille ArQmA CLI avec le flag store-tx-info activé (par défaut dans les nouvelles versions). Veuillez vous assurer que vous pouvez accéder à la tx key car cela pourrait être nécessaire en cas de litige.\narqma-wallet-cli (utiliser la commande get_tx_key)\narqma-wallet-gui (allez dans l'onglet historique et cliquez sur le bouton (P) pour accéder à la preuve de paiement).\n\nAvec un l'explorateur de bloc normal, le transfert n'est pas vérifiable.\n\nVous devez fournir au médiateur ou à l'arbitre les données suivantes en cas de litige:\n- Le tx de la clé privée\n- Le hash de la transaction\n- L'adresse publique du destinataire\n\nSi vous manquez de communiquer les données ci-dessus ou si vous utilisez un portefeuille incompatible, vous perdrez le litige. L'expéditeur des ARQ est responsable de la transmission au médiateur ou à l'arbitre de la vérification du transfert ces informations relatives au litige.\n\nIl n'est pas nécessaire de fournir l'ID du paiement, seulement l'adresse publique normale.\nSi vous n'êtes pas sûr de ce processus, visitez le canal discord ArQmA (https://discord.gg/s9BQpJT) ou le forum ArQmA (https://labs.arqma.com) pour obtenir plus d'informations. # suppress inspection "UnusedProperty" account.crypto.popup.xmr.msg=Pour échanger XMR sur Haveno, vous devez comprendre et respecter les exigences suivantes: \n\nSi vous vendez XMR, en cas de litige, vous devez fournir au médiateur ou à l'arbitre les informations suivantes: - clé de transaction (clé publique Tx, clé Tx, clé privée Tx) - ID de transaction (ID Tx Ou hachage Tx) - Adresse de destination de la transaction (adresse du destinataire) \n\nConsultez plus d'informations sur le portefeuille Monero dans le wiki: https: //haveno.exchange/wiki/Trading_Monero#Proving_payments \n\nSi vous ne fournissez pas les données de transaction requises, vous serez directement jugé échoue dans le litige. \n\nNotez également que Haveno fournit désormais la fonction de confirmation automatique des transactions XMR pour effectuer plus rapidement des transactions, mais vous devez l'activer dans les paramètres. \n\nPour plus d'informations sur la fonction de confirmation automatique, veuillez consulter le Wiki: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades] # suppress inspection "UnusedProperty" account.crypto.popup.msr.msg=Le navigateur blockchain pour échanger MSR sur Haveno vous oblige à comprendre et à respecter les exigences suivantes: \n\nLors de l'envoi de MSR, vous devez utiliser le portefeuille officiel Masari GUI, le portefeuille Masari CLI avec le logo store-tx-info activé (activé par défaut) ou le portefeuille web Masari (https://wallet.getmasari.org). Assurez-vous d'avoir accès à la clé tx, car cela est nécessaire en cas de litige. monero-wallet-cli (à l'aide de la commande get_Tx_key) monero-wallet-gui: sur la page Avancé> Preuve / Vérification. \n\nLe portefeuille web Masari (accédez à Compte-> Historique des transactions et vérifiez les détails de la transaction que vous avez envoyés) \n\nLa vérification peut être effectuée dans le portefeuille. monero-wallet-cli: utilisez la commande (check_tx_key). monero-wallet-gui: sur la page Avancé> Preuve / Vérification La vérification peut être effectuée dans le navigateur blockchain. Ouvrez le navigateur blockchain (https://explorer.getmasari.org) et utilisez la barre de recherche pour trouver votre hachage de transaction. Une fois que vous avez trouvé la transaction, faites défiler jusqu'à la zone «certificat à envoyer» en bas et remplissez les détails requis. En cas de litige, vous devez fournir les informations suivantes au médiateur ou à l'arbitre: - Clé privée Tx- Hachage de transaction- Adresse publique du destinataire \n\nAucun ID de transaction n'est requis, seule une adresse publique normale est requise. Si vous ne fournissez pas les informations ci-dessus ou si vous utilisez un portefeuille incompatible, vous perdrez le litige. En cas de litige, l'expéditeur XMR est responsable de fournir la vérification du transfert XMR au médiateur ou un arbitre. \n\nSi vous n'êtes pas sûr du processus, veuillez visiter le Masari Discord officiel (https://discord.gg/sMCwMqs) pour obtenir de l'aide. # suppress inspection "UnusedProperty" account.crypto.popup.blur.msg=ntes: \n\nPour envoyer des informations anonymes, vous devez utiliser un portefeuille CLI ou GUI de réseau anonyme. Si vous utilisez un portefeuille CLI, le hachage de la transaction (tx ID) sera affiché après la transmission. Vous devez enregistrer ces informations. Après l'envoi de la transmission, vous devez immédiatement utiliser la commande «get_tx_key» pour récupérer la clé privée de la transaction. Si vous ne parvenez pas à effectuer cette étape, vous ne pourrez peut-être pas récupérer la clé ultérieurement. \n\nSi vous utilisez le portefeuille Blur Network GUI, vous pouvez facilement trouver la clé privée de transaction et l'ID de transaction dans l'onglet «Historique». Localisez la transaction d'intérêt immédiatement après l'envoi. Cliquez sur le symbole «?» dans le coin inférieur droit de la boîte contenant la transaction. Vous devez enregistrer ces informations. \n\nSi un arbitrage est nécessaire, vous devez fournir les informations suivantes au médiateur ou à l'arbitre: 1.) ID de transaction, 2.) clé privée de transaction, 3.) adresse du destinataire. Le processus de médiation ou d'arbitrage utilisera le visualiseur de transactions BLUR (https://blur.cash/#tx-viewer) pour vérifier les transferts BLUR. \n\nLe défaut de fournir les informations nécessaires au médiateur ou à l'arbitre entraînera la perte du litige. Dans tous les litiges, l'expéditeur anonyme porte à 100% la responsabilité de vérifier la transaction avec le médiateur ou l'arbitre. \n\nSi vous ne comprenez pas ces exigences, n'échangez pas sur Haveno. Tout d'abord, demandez de l'aide dans Blur Network Discord (https://discord.gg/dMWaqVW). # suppress inspection "UnusedProperty" account.crypto.popup.solo.msg=Echanger Solo sur Haveno nécessite que vous compreniez et remplissiez les conditions suivantes: \n\nPour envoyer Solo, vous devez utiliser la version 5.1.3 ou supérieure du portefeuille Web Solo CLI. \n\nSi vous utilisez un portefeuille CLI, après l'envoi de la transaction, ID de transaction sera affiché. Vous devez enregistrer ces informations. Après avoir envoyé la transaction, vous devez immédiatement utiliser la commande «get_tx_key» pour récupérer la clé de transaction. Si vous ne parvenez pas à effectuer cette étape, vous ne pourrez peut-être pas récupérer la clé ultérieurement. \n\nSi un arbitrage est nécessaire, vous devez fournir les informations suivantes au médiateur ou à l'arbitre: 1) ID de transaction, 2) clé de transaction, 3) adresse du destinataire. Le médiateur ou l'arbitre utilisera l’explorateur de blocs Solo (https://explorer.Solo.org) pour rechercher des transactions puis utilisera la fonction «envoyer une preuve» (https://explorer.minesolo.com/). \n\nLe défaut de fournir les informations nécessaires au médiateur ou à l'arbitre entraînera la perte de l'affaire. Dans tous les cas de litige, l'expéditeur de QWC assume à 100% la responsabilité lors de la vérification de la transaction avec le médiateur ou l'arbitre. \n\nSi vous ne comprenez pas ces exigences, n'échangez pas sur Haveno. Tout d'abord, demandez de l'aide dans Solo Discord (https://discord.minesolo.com/). # suppress inspection "UnusedProperty" account.crypto.popup.cash2.msg=Pour échanger CASH2 sur Haveno, vous devez comprendre et respecter les exigences suivantes: \n\nPour envoyer CASH2, vous devez utiliser la version 3 ou supérieure du portefeuille CASH2. \n\nAprès l'envoi de la transaction, ID de la transaction s'affiche. Vous devez enregistrer ces informations. Après avoir envoyé la transaction, vous devez utiliser la commande «getTxKey» dans simplewallet pour récupérer immédiatement la clé de transaction.\n\nSi un arbitrage est nécessaire, vous devez fournir les informations suivantes au médiateur ou à l'arbitre: 1) ID de transaction, 2) clé de transaction, 3) adresse CASH2 du destinataire. Le médiateur ou l'arbitre utilisera l’explorateur de blocs CASH2 (https://blocks.cash2.org) pour vérifier le transfert CASH2. \n\nLe défaut de fournir les informations nécessaires au médiateur ou à l'arbitre entraînera la perte de l'affaire. Dans tous les cas de litige, l'expéditeur de CASH2 assume à 100% la responsabilité lors de la vérification de la transaction avec le médiateur ou l'arbitre. \n\nSi vous ne comprenez pas ces exigences, n'échangez pas sur Haveno. Tout d'abord, demandez de l'aide dans le Discord Cash2 (https://discord.gg/FGfXAYN). # suppress inspection "UnusedProperty" account.crypto.popup.qwertycoin.msg=Pour échanger Qwertycoin sur Haveno, vous devez comprendre et respecter les exigences suivantes: \n\nPour envoyer Qwertycoin, vous devez utiliser la version 5.1.3 ou supérieure du portefeuille Qwertycoin. \n\nAprès l'envoi de la transaction, ID de la transaction s'affiche. Vous devez enregistrer ces informations. Après avoir envoyé la transaction, vous devez utiliser la commande «get_Tx_Key» dans simplewallet pour récupérer immédiatement la clé de transaction. \n\nSi un arbitrage est nécessaire, vous devez fournir les informations suivantes au médiateur ou à l'arbitre: 1) ID de transaction, 2) clé de transaction, 3) adresse QWC du destinataire. Le médiateur ou l'arbitre utilisera l’explorateur de blocs QWC (https://explorer.qwertycoin.org) pour vérifier les transferts QWC. \n\nLe défaut de fournir les informations nécessaires au médiateur ou à l'arbitre entraînera la perte de l'affaire. Dans tous les cas de litige, l'expéditeur de QWC assume à 100% la responsabilité lors de la vérification de la transaction avec le médiateur ou l'arbitre. \n\nSi vous ne comprenez pas ces exigences, n'échangez pas sur Haveno. Tout d'abord, demandez de l'aide dans QWC Discord (https://discord.gg/rUkfnpC). # suppress inspection "UnusedProperty" account.crypto.popup.drgl.msg=Echanger Dragonglass sur Haveno vous oblige à comprendre et à respecter les exigences suivantes: ~\n\nComme Dragonglass offre une protection de la confidentialité, les transactions ne peuvent pas être vérifiées sur la blockchain publique. Si nécessaire, vous pouvez prouver votre paiement en utilisant votre TXN-Private-Key. TXN-Private est une clé d'un temps générée automatiquement, utilisée pour chaque transaction qui est accessible uniquement à partir du portefeuille DESP. Soit via DRGL-wallet GUI (boîte de dialogue des détails de transaction interne), soit via Dragonglass CLI simplewallet (en utilisant la commande "get_tx_key"). \n\nLes deux nécessitent la version DRGL de «Oathkeeper» ou supérieure. \n\nEn cas de litige, vous devez fournir les informations suivantes au médiateur ou à l'arbitre: \n\n- txn-Privite-ket- hachage de transaction- adresse publique du destinataire ~\n\nLa vérification du paiement peut utiliser les données ci-dessus comme entrée (http://drgl.info/#check_txn).\n\nSi vous ne fournissez pas les informations ci-dessus ou si vous utilisez un portefeuille incompatible, vous perdrez le litige. L'expéditeur Dragonglass est responsable de fournir la vérification de transfert DRGL au médiateur ou à l'arbitre en cas de litige. Aucun ID de paiement n'est requis. \n\nSi vous n'êtes pas sûr d'une partie de ce processus, veuillez visiter Dragonglass sur (http://discord.drgl.info) pour obtenir de l'aide. # suppress inspection "UnusedProperty" account.crypto.popup.ZEC.msg=Lors de l'utilisation de Zcash, vous ne pouvez utiliser que les adresses transparentes (commençant par t), et non les z-adresses (privées), car le médiateur ou l'arbitre ne seraient pas en mesure de vérifier la transaction avec les z-adresses. # suppress inspection "UnusedProperty" account.crypto.popup.XZC.msg=Lors de l'utilisation de Zcoin, vous ne pouvez utiliser que les adresses transparentes (traçables), et non les adresses intraçables, car le médiateur ou l'arbitre ne seraient pas en mesure de vérifier la transaction avec des adresses intraçables dans un explorateur de blocs. # suppress inspection "UnusedProperty" account.crypto.popup.grin.msg=GRIN nécessite un échange interactif entre l'émetteur et le récepteur pour créer la transaction. Assurez-vous de suivre les instructions de la page Web du projet GRIN pour envoyer et recevoir des GRIN de façon fiable (le récepteur doit être en ligne au moins pendant un certain temps).\n\nHaveno ne supporte que le portefeuille Grinbox (Wallet713) format URL.\n\nL'expéditeur des GRIN doit fournir la preuve qu'il a envoyé les GRIN avec succès. Si le portefeuille ne peut pas fournir cette preuve, un litige potentiel sera résolu en faveur du destinataire des GRIN. Veuillez vous assurer que vous utilisez le dernier logiciel Grinbox qui supporte la preuve de transaction et que vous comprenez le processus de transfert et de réception des GRIN ainsi que la façon de créer la preuve.\n\nVisitez https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only pour plus d'informations sur l'outil de preuve de Grinbox. # suppress inspection "UnusedProperty" account.crypto.popup.beam.msg=BEAM nécessite un processus interactif entre l'émetteur et le récepteur pour créer la transaction.\n\nAssurez-vous de suivre les instructions de la page Web du projet BEAM pour envoyer et recevoir les BEAM de façon fiable (le récepteur doit être en ligne pendant au moins un certain temps).\n\nL'expéditeur de BEAM est tenu de fournir la preuve qu'il a envoyé BEAM avec succès. Assurez-vous d'utiliser un portefeuille qui peut produire une telle preuve. Si le portefeuille ne peut fournir la preuve, un litige potentiel sera résolu en faveur du récepteur des BEAM. # suppress inspection "UnusedProperty" account.crypto.popup.pars.msg=Echanger ParsiCoin sur Haveno nécessite que vous compreniez et remplissiez les conditions suivantes: \n\nPour envoyer PARS, vous devez utiliser la version 3.0.0 ou supérieure du portefeuille ParsiCoin officiel. \n\nVous pouvez vérifier votre hachage de transaction et votre clé de transaction dans la section transaction du portefeuille GUI (ParsiPay). Vous devez cliquer avec le bouton droit de la souris sur «Transaction» puis cliquer sur «Afficher les détails». \n\nSi l'arbitrage est à 100% nécessaire, vous devez fournir au médiateur ou à l'arbitre les éléments suivants: 1) hachage de transaction, 2) clé de transaction et 3) adresse PARS du destinataire. Le médiateur ou l'arbitre utilisera l’explorateur de blocs ParsiCoin (http://explorer.parsicoin.net/#check_payment) pour vérifier les transmissions PARS. \n\nSi vous ne comprenez pas ces exigences, n'échangez pas sur Haveno. Tout d'abord, demandez de l'aide sur le ParsiCoin Discord (https://discord.gg/c7qmFNh). # suppress inspection "UnusedProperty" account.crypto.popup.blk-burnt.msg=Pour échanger les monnaies brûlées, vous devez savoir ce qui suit: \n\nLes monnaies brûlées ne peuvent pas être dépensée. Pour les échanger sur Haveno, le script de sortie doit prendre la forme suivante: OP_RETURN OP_PUSHDATA, suivi des octets de données pertinents, ces octets forment l'adresse après le codage hexadécimal. Par exemple, une devise brûlée avec l'adresse 666f6f ("foo" en UTF-8) aura le script suivant: \n\nOP_RETURN OP_PUSHDATA 666f6f \n\nPour créer de la monnaie brûlée, vous pouvez utiliser la commande RPC «brûler», disponible dans certains portefeuilles. \n\nPour d'éventuelles situations, vous pouvez vérifier https://ibo.laboratorium.ee \n\nPuisque la monnaie brûlée ne peut pas être utilisée, elle ne peut pas être revendue. «Vendre» une devise brûlée signifie brûler la devise d'origine (données associées à l'adresse de destination). \n\nEn cas de litige, le vendeur BLK doit fournir le hachage de la transaction. # suppress inspection "UnusedProperty" account.crypto.popup.liquidmonero.msg=Pour échanger L-XMR sur Haveno, vous devez comprendre les termes suivants: \n\nLorsque vous acceptez des transactions L-XMR sur Haveno, vous ne pouvez pas utiliser Blockstream Green Wallet sur le téléphone mobile ou un portefeuille de dépôt / commercial. Vous ne devez recevoir du L-XMR que dans le portefeuille Liquid Elements Core ou un autre portefeuille L-XMR avec une adresse L-XMR et une clé de sécurité qui vous permettre d'être anonyme. \n\nEn cas de médiation ou en cas de litige de transaction, vous devez divulguer la clé de sécurité de l'adresse L-XMR au médiateur Haveno ou à l'agent de remboursement afin qu'ils puissent vérifier les détails de votre transaction anonyme sur leur propre nœud complet Elements Core. \n\nSi vous ne comprenez pas ou ne comprenez pas ces exigences, n'échangez pas de L-XMR sur Haveno. account.traditional.yourTraditionalAccounts=Vos comptes en devise nationale account.backup.title=Sauvegarder le portefeuille account.backup.location=Emplacement de la sauvegarde account.backup.selectLocation=Sélectionner l'emplacement de sauvegarde account.backup.backupNow=Sauvegarder maintenant (la sauvegarde n'est pas cryptée !) account.backup.appDir=Répertoire des données de l'application account.backup.openDirectory=Ouvrir le répertoire account.backup.openLogFile=Ouvrir le fichier de log account.backup.success=Sauvegarder réussite vers l'emplacement:\n{0} account.backup.directoryNotAccessible=Le répertoire que vous avez choisi n'est pas accessible. {0} account.password.removePw.button=Supprimer le mot de passe account.password.removePw.headline=Supprimer la protection par mot de passe du portefeuille account.password.setPw.button=Définir un mot de passe account.password.setPw.headline=Définir le mot de passe de protection pour le portefeuille account.password.info=Avec la protection par mot de passe, vous devrez entrer votre mot de passe au démarrage de l'application, lors du retrait de monero depuis votre portefeuille et lors de l'affichage de vos mots de la graine (seed). account.seed.backup.title=Sauvegardez les mots de votre portefeuille. account.seed.info=Veuillez noter les mots de passe de votre portefeuille ainsi que la date. Vous pouvez récupérer votre portefeuille à tout moment avec les mots de passe et la date.\n\nVous devriez noter les mots de passe sur une feuille de papier. Ne les enregistrez pas sur l'ordinateur.\n\nVeuillez noter que les mots de passe ne remplacent pas une sauvegarde.\nVous devez créer une sauvegarde de l'ensemble du répertoire de l'application à partir de l'écran "Compte/Sauvegarde" pour récupérer l'état et les données de l'application. account.seed.backup.warning=Veuillez noter que les mots de passe ne remplacent pas une sauvegarde.\nVous devez créer une sauvegarde de l'ensemble du répertoire de l'application à partir de l'écran "Compte/Sauvegarde" pour récupérer l'état et les données de l'application. account.seed.warn.noPw.msg=Vous n'avez pas configuré un mot de passe de portefeuille qui protégerait l'affichage des mots composant la seed.\n\nVoulez-vous afficher les mots composant la seed? account.seed.warn.noPw.yes=Oui, et ne me le demander plus à l'avenir account.seed.enterPw=Entrer le mot de passe afficher les mots composant la seed account.seed.restore.info=Veuillez effectuer une sauvegarde avant de procéder à une restauration à partir du mot de passe. Sachez que la restauration d'un portefeuille n'est a faire qu'en cas d'urgence et qu'elle peut causer des problèmes avec la base de données interne du portefeuille.\nCe n'est pas une façon de faire une sauvegarde ! Veuillez utiliser une sauvegarde à partir du répertoire de données de l'application pour restaurer l'état antérieur de l'application.\n\nAprès la restauration, l'application s'arrêtera automatiquement. Après le redémarrage de l'application, elle sera resynchronisée avec le réseau Monero. Cela peut prendre un certain temps et peut consommer beaucoup de puissance sur le CPU, surtout si le portefeuille était plus vieux et contient beaucoup de transactions. Veuillez éviter d'interrompre ce processus, sinon vous devrez peut-être supprimer à nouveau le fichier de chaîne SPV ou répéter le processus de restauration. account.seed.restore.ok=Ok, effectuer la restauration et arrêter Haveno #################################################################### # Mobile notifications #################################################################### account.notifications.setup.title=Configurer account.notifications.download.label=Télécharger l'application mobile account.notifications.waitingForWebCam=En attente de la webcam.... account.notifications.webCamWindow.headline=Scanner le code QR depuis le téléphone account.notifications.webcam.label=Utiliser la webcam account.notifications.webcam.button=Scanner le QR code account.notifications.noWebcam.button=Je ne possède pas de webcam account.notifications.erase.label=Effacer les notifications sur le téléphone account.notifications.erase.title=Effacer les notifications account.notifications.email.label=Pairing token account.notifications.email.prompt=Entrez le Pairing token que vous avez reçu par mail account.notifications.settings.title=Paramètres account.notifications.useSound.label=Activer l'alerte de notification sur le téléphone account.notifications.trade.label=Recevoir des messages pour le trade account.notifications.market.label=Recevoir des alertes sur les ordres account.notifications.price.label=Recevoir des alertes de prix account.notifications.priceAlert.title=Alertes de prix account.notifications.priceAlert.high.label=Me prévenir si le prix du XMR est supérieur à account.notifications.priceAlert.low.label=Me prévenir si le prix du XMR est inférieur à account.notifications.priceAlert.setButton=Définir l'alerte de prix account.notifications.priceAlert.removeButton=Retirer l'alerte de prix account.notifications.trade.message.title=L'état du trade a été modifié. account.notifications.trade.message.msg.conf=La transaction de dépôt pour l'échange avec ID {0} est confirmée. Veuillez ouvrir votre application Haveno et initier le paiement. account.notifications.trade.message.msg.started=L'acheteur de XMR a initié le paiement pour la transaction avec ID {0}. account.notifications.trade.message.msg.completed=La transaction avec l'ID {0} est terminée. account.notifications.offer.message.title=Votre ordre a été accepté account.notifications.offer.message.msg=Votre ordre avec l'ID {0} a été accepté account.notifications.dispute.message.title=Nouveau message de litige account.notifications.dispute.message.msg=Vous avez reçu un message de contestation pour le trade avec l'ID {0} account.notifications.marketAlert.title=Alertes sur les ordres account.notifications.marketAlert.selectPaymentAccount=Ordres correspondants au compte de paiement account.notifications.marketAlert.offerType.label=Type d'ordre qui m'intéresse account.notifications.marketAlert.offerType.buy=Ordres d'achat (je veux vendre des XMR) account.notifications.marketAlert.offerType.sell=Ordres de vente (je veux acheter des XMR) account.notifications.marketAlert.trigger=Écart par rapport au prix de l'ordre (%) account.notifications.marketAlert.trigger.info=Avec la définition d'une distance par rapport au prix, vous ne recevrez une alerte que lorsqu'un odre qui répondra (ou dépassera) vos exigences sera publié. Exemple : vous voulez vendre des XMR, mais vous ne vendrez qu'avec une prime de 2% par rapport au prix actuel du marché. En réglant ce champ à 2%, vous ne recevrez que des alertes pour les ordres dont les prix sont de 2% (ou plus) au dessus du prix actuel du marché. account.notifications.marketAlert.trigger.prompt=Écart en pourcentage par rapport au prix du marché (par ex. 2,50 %, -0,50 %, etc.) account.notifications.marketAlert.addButton=Ajouter une alerte pour les ordres account.notifications.marketAlert.manageAlertsButton=Gérer les alertes des ordres account.notifications.marketAlert.manageAlerts.title=Gérer les alertes pour les ordres account.notifications.marketAlert.manageAlerts.header.paymentAccount=Compte de paiement account.notifications.marketAlert.manageAlerts.header.trigger=Prix de déclenchement account.notifications.marketAlert.manageAlerts.header.offerType=Type d'ordre account.notifications.marketAlert.message.title=Alerte d'ordre account.notifications.marketAlert.message.msg.below=en dessous de account.notifications.marketAlert.message.msg.above=au dessus de account.notifications.marketAlert.message.msg=Un nouvel ordre '{0} {1}'' avec le prix {2} ({3} {4} prix de marché) avec le moyen de paiement '{5}' a été publiée dans le livre des ordres de Haveno.\nID de l'ordre: {6}. account.notifications.priceAlert.message.title=Alerte de prix pour {0} account.notifications.priceAlert.message.msg=Votre alerte de prix a été déclenchée. l'actuel {0} le prix est {1}. {2} account.notifications.noWebCamFound.warning=Aucune webcam n'a été trouvée.\n\nUtilisez l'option mail pour envoyer le jeton et la clé de cryptage depuis votre téléphone portable vers l'application Haveno. account.notifications.priceAlert.warning.highPriceTooLow=Le prix le plus élevé doit être supérieur au prix le plus bas. account.notifications.priceAlert.warning.lowerPriceTooHigh=Le prix le plus bas doit être inférieur au prix le plus élevé. #################################################################### # Windows #################################################################### inputControlWindow.headline=Sélectionnez les entrées pour la transaction inputControlWindow.balanceLabel=Solde disponible contractWindow.title=Détails du conflit contractWindow.dates=Date de l'ordre / date de l'échange contractWindow.xmrAddresses=Adresse Monero XMR acheteur / vendeur XMR contractWindow.onions=Adresse réseau de l'acheteur de XMR / du vendeur de XMR contractWindow.accountAge=Âge du compte acheteur XMR / vendeur XMR contractWindow.numDisputes=Nombre de litiges de l'acheteur de XMR / du vendeur de XMR contractWindow.contractHash=Contracter le hash displayAlertMessageWindow.headline=Information importante! displayAlertMessageWindow.update.headline=Information de mise à jour importante! displayAlertMessageWindow.update.download=Télécharger: displayUpdateDownloadWindow.downloadedFiles=Dossiers: displayUpdateDownloadWindow.downloadingFile=Téléchargement: {0} displayUpdateDownloadWindow.verifiedSigs=Signature vérifiée avec les clés : displayUpdateDownloadWindow.status.downloading=Téléchargement des fichiers en cours... displayUpdateDownloadWindow.status.verifying=Vérification de la signature.... displayUpdateDownloadWindow.button.label=Télécharger le programme d'installation et vérifier la signature displayUpdateDownloadWindow.button.downloadLater=Télécharger plus tard displayUpdateDownloadWindow.button.ignoreDownload=Ignorer cette version displayUpdateDownloadWindow.headline=Une nouvelle mise à jour Haveno est disponible ! displayUpdateDownloadWindow.download.failed.headline=Echec du téléchargement displayUpdateDownloadWindow.download.failed=Téléchargement échoué. Veuillez télécharger et vérifier via [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.installer.failed=Impossible de déterminer le bon programme d'installation. Veuillez télécharger et vérifier manuellement via [HYPERLINK:https://haveno.exchange/downloads] . displayUpdateDownloadWindow.verify.failed=Vérification échouée. Veuillez télécharger et vérifier manuellement via [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.success=La nouvelle version a été téléchargée avec succès et la signature vérifiée.\n\nVeuillez ouvrir le répertoire de téléchargement, fermer l'application et installer la nouvelle version. displayUpdateDownloadWindow.download.openDir=Ouvrir le répertoire de téléchargement disputeSummaryWindow.title=Résumé disputeSummaryWindow.openDate=Date d'ouverture du ticket disputeSummaryWindow.role=Rôle du trader disputeSummaryWindow.payout=Versement du montant de l'opération disputeSummaryWindow.payout.getsTradeAmount=XMR {0} obtient le montant du versement de la transaction disputeSummaryWindow.payout.getsAll=Payement maximum en XMR {0} disputeSummaryWindow.payout.custom=Versement personnalisé disputeSummaryWindow.payoutAmount.buyer=Montant du versement de l'acheteur disputeSummaryWindow.payoutAmount.seller=Montant du versement au vendeur disputeSummaryWindow.payoutAmount.invert=Utiliser le perdant comme publicateur disputeSummaryWindow.reason=Motif du litige disputeSummaryWindow.tradePeriodEnd=Fin de la période de trade disputeSummaryWindow.extraInfo=Informations additionnelles disputeSummaryWindow.delayedPayoutStatus=Statut du paiement différé # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BUG=Bug # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.USABILITY=Utilisabilité # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Violation du protocole # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.NO_REPLY=Pas de réponse # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SCAM=Scam # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OTHER=Autre # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BANK_PROBLEMS=Banque # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OPTION_TRADE=Transaction facultative # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Le trader ne répond pas # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Mauvais compte d'expéditeur # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PEER_WAS_LATE=La pair a expiré # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=La transaction s'est stabilisée. disputeSummaryWindow.summaryNotes=Notes de synthèse disputeSummaryWindow.addSummaryNotes=Ajouter des notes de synthèse disputeSummaryWindow.close.button=Fermer le ticket # Do no change any line break or order of tokens as the structure is used for signature verification # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.msg=Le ticket a été fermé {0}\n {1} Adresse du noeud: {2} \n\nRésumé: \nID de transaction: {3} \nDevise: {4} \n Montant de la transaction: {5} \nMontant du paiement de l'acheteur XMR: {6} \nMontant du paiement du vendeur XMR: {7} \n\nRaison du litige: {8} \n\nRésumé: {9} \n\n # Do no change any line break or order of tokens as the structure is used for signature verification disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3} disputeSummaryWindow.close.nextStepsForMediation=\n\nÉtape suivante:\nOuvrez la transaction inachevée, acceptez ou rejetez la suggestion du médiateur disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\nÉtape suivante: \nAucune autre action n'est requise de votre part. Si l'arbitre rend une décision en votre faveur, vous verrez la transaction «Remboursement d'arbitrage» sur la page Fonds / Transactions disputeSummaryWindow.close.closePeer=Vous devez également clore le ticket des pairs de trading ! disputeSummaryWindow.close.txDetails.headline=Publier la transaction de remboursement # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.buyer=L'acheteur reçoit {0} à l'adresse: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=Le vendeur reçoit {0} à l'adresse: {1}\n disputeSummaryWindow.close.txDetails=Dépenser: {0}\n{1}{2}Frais de transaction: {3}\n\nÊtes-vous sûr de vouloir publier cette transaction ? disputeSummaryWindow.close.noPayout.headline=Fermé sans paiement disputeSummaryWindow.close.noPayout.text=Voulez-vous fermer sans paiement ? emptyWalletWindow.headline={0} Outil de secours du portefeuille emptyWalletWindow.info=Veuillez utiliser ceci qu'en cas d'urgence si vous ne pouvez pas accéder à vos fonds à partir de l'interface utilisateur.\n\nVeuillez remarquer que touts les ordres en attente seront automatiquement fermés lors de l'utilisation de cet outil.\n\nAvant d'utiliser cet outil, veuillez sauvegarder votre répertoire de données. Vous pouvez le faire sur \"Compte/sauvegarde\".\n\nVeuillez nous signaler votre problème et déposer un rapport de bug sur GitHub ou sur le forum Haveno afin que nous puissions enquêter sur la source du problème. emptyWalletWindow.balance=Votre solde disponible sur le portefeuille emptyWalletWindow.address=Votre adresse de destination emptyWalletWindow.button=Envoyer tous les fonds emptyWalletWindow.openOffers.warn=Vous avez des ordres en cours qui seront supprimées si vous videz votre portefeuille.\nVous êtes certain de vouloir vider votre portefeuille ? emptyWalletWindow.openOffers.yes=Oui, j'en suis certain emptyWalletWindow.sent.success=Le solde de votre portefeuille a été transféré avec succès. enterPrivKeyWindow.headline=Entrer la clé privée pour l'enregistrement filterWindow.headline=Modifier la liste de filtres filterWindow.offers=Ordres filtrés (séparer avec une virgule) filterWindow.onions=Banni des addresses de trading (virgule de séparation) filterWindow.bannedFromNetwork=Banni des addresses réseau (virgule de séparation) filterWindow.accounts=Données filtrées du compte de trading:\nFormat: séparer par une virgule liste des [ID du mode de paiement | champ de données | valeur]. filterWindow.bannedCurrencies=Codes des devises filtrées (séparer avec une virgule.) filterWindow.bannedPaymentMethods=IDs des modes de paiements filtrés (séparer avec une virgule.) filterWindow.bannedAccountWitnessSignerPubKeys=Clé publique filtrée du signataire du témoin de compte (clé publique hexadécimale séparée par des virgules) filterWindow.bannedPrivilegedDevPubKeys=Clé publique filtrée de développeur privilégiée (clé publique hexadécimale séparée par des virgules) filterWindow.arbitrators=Arbitres filtrés (adresses onion séparées par une virgule) filterWindow.mediators=Médiateurs filtrés (adresses onion sep. par une virgule) filterWindow.refundAgents=Agents de remboursement filtrés (adresses onion sep. par virgule) filterWindow.seedNode=Nœuds de seed filtrés (adresses onion séparées par une virgule) filterWindow.priceRelayNode=Nœuds relais avec prix filtrés (adresses onion séparées par une virgule) filterWindow.xmrNode=Nœuds Monero filtrés (adresses séparées par une virgule + port) filterWindow.preventPublicXmrNetwork=Empêcher l'utilisation du réseau public Monero filterWindow.disableAutoConf=Désactiver la confirmation automatique filterWindow.autoConfExplorers=Explorateur d'auto-confirmations filtrés (addresses à virgule de séparation) filterWindow.disableTradeBelowVersion=Version min. nécessaire pour pouvoir échanger filterWindow.add=Ajouter le filtre filterWindow.remove=Retirer le filtre filterWindow.xmrFeeReceiverAddresses=Adresse de réception des frais XMR filterWindow.disableApi=Désactiver l'API filterWindow.disableMempoolValidation=Désactiver la validation du Mempool offerDetailsWindow.minXmrAmount=Montant XMR min. offerDetailsWindow.min=(min. {0}) offerDetailsWindow.distance=(écart par rapport au prix de marché: {0}) offerDetailsWindow.myTradingAccount=Mon compte de trading offerDetailsWindow.offererBankId=(ID/BIC/SWIFT de la banque du maker) offerDetailsWindow.offerersBankName=(nom de la banque du maker) offerDetailsWindow.bankId=Identifiant bancaire (par ex. BIC ou SWIFT) offerDetailsWindow.countryBank=Pays de la banque du Maker offerDetailsWindow.commitment=Engagement offerDetailsWindow.agree=J'accepte offerDetailsWindow.tac=Conditions d'utilisation offerDetailsWindow.confirm.maker.buy=Confirmer : Créer une offre pour acheter XMR avec {0} offerDetailsWindow.confirm.maker.sell=Confirmer : Créer une offre pour vendre XMR contre {0} offerDetailsWindow.confirm.taker.buy=Confirmer : Accepter une offre pour acheter XMR avec {0} offerDetailsWindow.confirm.taker.sell=Confirmer : Accepter une offre pour vendre XMR contre {0} offerDetailsWindow.creationDate=Date de création offerDetailsWindow.makersOnion=Adresse onion du maker offerDetailsWindow.challenge=Phrase secrète de l'offre offerDetailsWindow.challenge.copy=Copier la phrase secrète à partager avec votre pair qRCodeWindow.headline=QR Code qRCodeWindow.msg=Veuillez utiliser le code QR pour recharger du portefeuille externe au portefeuille Haveno. qRCodeWindow.request=Demande de paiement:\n{0} selectDepositTxWindow.headline=Sélectionner la transaction de dépôt en cas de litige selectDepositTxWindow.msg=La transaction de dépôt n'a pas été incluse dans l"échange.\nVeuillez sélectionner l'une des transactions multisig existantes de votre portefeuille qui contient la transaction de dépôt utilisée lors de l'échec de l'échange.\n\nVous trouverez la bonne transaction en ouvrant la fenêtre des détails de la transaction (cliquez sur l'ID de la transaction dans la liste) et en retraçant les frais de transaction de sortie de la prochaine transaction où vous serez en mesure de voir la transaction de dépôt multisig (l'adresse commence par un 3). Cet ID de transaction doit être visible dans la liste ici. Une fois que vous aurez trouvé la bonne transaction, sélectionnez cette transaction la et continuez.\n\nDésolé pour le désagrément mais ce genre d'erreur devrait se produire très rarement et à l'avenir nous trouverons de meilleurs moyens pour le résoudre. selectDepositTxWindow.select=Sélectionner la transaction de dépôt sendAlertMessageWindow.headline=Envoyer une notification globale sendAlertMessageWindow.alertMsg=Message d'alerte sendAlertMessageWindow.enterMsg=Entrer le message sendAlertMessageWindow.isSoftwareUpdate=Notification de téléchargement du logiciel sendAlertMessageWindow.isUpdate=Est version complète sendAlertMessageWindow.isPreRelease=Est version pré-complète sendAlertMessageWindow.version=Nouvelle version N° sendAlertMessageWindow.send=Envoyer une notification sendAlertMessageWindow.remove=Supprimer une notification sendPrivateNotificationWindow.headline=Envoyer un message privé sendPrivateNotificationWindow.privateNotification=Notification privée sendPrivateNotificationWindow.enterNotification=Entrer la notification sendPrivateNotificationWindow.send=Envoyer une notification privée showWalletDataWindow.walletData=Données du portefeuille showWalletDataWindow.includePrivKeys=Inclure les clés privées setXMRTxKeyWindow.headline=La preuve XMR a été envoyée. setXMRTxKeyWindow.note=Ajoutez les informations tx au-dessous pour confirmer automatiquement les transactions plus rapidement. Plus d'informations: https://haveno.exchange/wiki/Trading_Monero setXMRTxKeyWindow.txHash=ID de transaction (en option) setXMRTxKeyWindow.txKey=Clé de transaction (en option) # We do not translate the tac because of the legal nature. We would need translations checked by lawyers # in each language which is too expensive atm. tacWindow.headline=Conditions d'utilisation tacWindow.agree=Je suis d'accord tacWindow.disagree=Je ne suis pas d'accord et je quitte tacWindow.arbitrationSystem=Règlement du litige tradeDetailsWindow.headline=Échange tradeDetailsWindow.disputedPayoutTxId=ID de la transaction de versement contestée tradeDetailsWindow.tradeDate=Date de l'échange tradeDetailsWindow.txFee=Frais de minage tradeDetailsWindow.tradePeersOnion=Adresse onion du pair de trading tradeDetailsWindow.tradePeersPubKeyHash=Valeur de hachage de la clé publique du partenaire commercial tradeDetailsWindow.tradeState=État du trade tradeDetailsWindow.agentAddresses=Arbitre/Médiateur tradeDetailsWindow.detailData=Données détaillées txDetailsWindow.headline=Détails de la transaction txDetailsWindow.xmr.noteSent=Vous avez envoyé du XMR. txDetailsWindow.xmr.noteReceived=Vous avez reçu XMR. txDetailsWindow.sentTo=Envoyé à txDetailsWindow.receivedWith=Reçu avec txDetailsWindow.txId=ID de transaction closedTradesSummaryWindow.headline=Résumé de l'historique de trade closedTradesSummaryWindow.totalAmount.title=Montant total du trade closedTradesSummaryWindow.totalAmount.value={0} ({1} avec le prix courant du marché) closedTradesSummaryWindow.totalVolume.title=Montant total échangé en {0} closedTradesSummaryWindow.totalMinerFee.title=Somme de tous les frais de mineur closedTradesSummaryWindow.totalMinerFee.value={0} ({1} du montant total du trade) closedTradesSummaryWindow.totalTradeFeeInXmr.title=Somme de tous les frais de trade payés en XMR closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} du montant total du trade) walletPasswordWindow.headline=Entrer le mot de passe pour déverouiller xmrConnectionError.headline=Erreur de connexion Monero xmrConnectionError.providedNodes=Erreur lors de la connexion aux nœuds Monero fournis.\n\nVoulez-vous utiliser le prochain nœud Monero disponible ? xmrConnectionError.customNodes=Erreur lors de la connexion à vos nœuds Monero personnalisés.\n\nVoulez-vous utiliser le prochain nœud Monero disponible ? xmrConnectionError.localNode=Haveno était précédemment connecté à un nœud Monero local, mais il n’est plus accessible.\n\nAssurez-vous que votre nœud local est en fonctionnement et entièrement synchronisé, ou choisissez une autre option pour continuer. xmrConnectionError.localNode.start=Démarrer le nœud local xmrConnectionError.localNode.start.error=Erreur lors du démarrage du nœud local xmrConnectionError.localNode.fallback=Se connecter au prochain nœud disponible torNetworkSettingWindow.header=Paramètres du réseau Tor torNetworkSettingWindow.noBridges=Ne pas utiliser de bridges. torNetworkSettingWindow.providedBridges=Se connecter avec les bridges proposés torNetworkSettingWindow.customBridges=Entrer des bridges personnalisés torNetworkSettingWindow.transportType=Type de transport torNetworkSettingWindow.obfs3=obfs3 torNetworkSettingWindow.obfs4=obfs4 (recommandé) torNetworkSettingWindow.meekAmazon=meek-amazon torNetworkSettingWindow.meekAzure=meek-azure torNetworkSettingWindow.enterBridge=Entrer un ou plusieurs relais bridge (un par ligne) torNetworkSettingWindow.enterBridgePrompt=inscrire l'adresse:port torNetworkSettingWindow.restartInfo=Vous devez redémarrer pour appliquer les changements torNetworkSettingWindow.openTorWebPage=Ouvrir la page web du projet Tor torNetworkSettingWindow.deleteFiles.header=Problèmes de connexion ? torNetworkSettingWindow.deleteFiles.info=Si vous rencontrez des problèmes de connexion récurrents au démarrage, la suppression des fichiers Tor obsolètes pourrait vous aider. Pour ce faire, cliquez sur le bouton ci-dessous et ensuite redémarrez. torNetworkSettingWindow.deleteFiles.button=Supprimer les fichiers Tor obsolètes et éteindre torNetworkSettingWindow.deleteFiles.progress=Arrêt de Tor en cours torNetworkSettingWindow.deleteFiles.success=Fichiers Tor obsolètes supprimés avec succès. Veuillez redémarrer. torNetworkSettingWindow.bridges.header=Tor est-il bloqué? torNetworkSettingWindow.bridges.info=Si Tor est bloqué par votre fournisseur Internet ou dans votre pays, vous pouvez essayer d'utiliser les passerelles Tor.\nVisitez la page web de Tor sur: https://bridges.torproject.org pour en savoir plus sur les bridges et les pluggable transports. feeOptionWindow.headline=Choisissez la devise pour le paiement des frais de transaction feeOptionWindow.info=Vous pouvez choisir de payer les frais de transaction en BSQ ou en XMR. Si vous choisissez BSQ, vous bénéficierez de frais de transaction réduits. feeOptionWindow.optionsLabel=Choisissez la devise pour le paiement des frais de transaction feeOptionWindow.useXMR=Utiliser XMR feeOptionWindow.fee={0} (≈ {1}) feeOptionWindow.xmrFeeWithFiatAndPercentage={0} (environ {1}/{2}) feeOptionWindow.xmrFeeWithPercentage={0} ({1}) #################################################################### # Popups #################################################################### popup.headline.notification=Notification popup.headline.instruction=Merci de noter: popup.headline.attention=Attention popup.headline.backgroundInfo=Information sur les antécédents popup.headline.feedback=Terminé popup.headline.confirmation=Confirmation popup.headline.information=Information popup.headline.warning=Attention popup.headline.error=Erreur popup.doNotShowAgain=Ne plus montrer popup.reportError.log=Ouvrir le dossier de log popup.reportError.gitHub=Signaler au Tracker de problème GitHub popup.reportError={0}\n\nAfin de nous aider à améliorer le logiciel, veuillez signaler ce bug en ouvrant un nouveau ticket de support sur https://github.com/haveno-dex/haveno/issues.\nLe message d'erreur ci-dessus sera copié dans le presse-papier lorsque vous cliquerez sur l'un des boutons ci-dessous.\nCela facilitera le dépannage si vous incluez le fichier haveno.log en appuyant sur "ouvrir le fichier de log", en sauvegardant une copie, et en l'attachant à votre rapport de bug. popup.error.tryRestart=Veuillez essayer de redémarrer votre application et vérifier votre connexion réseau pour voir si vous pouvez résoudre ce problème. popup.error.takeOfferRequestFailed=Une erreur est survenue pendant que quelqu'un essayait d'accepter l'un de vos ordres:\n{0} error.spvFileCorrupted=Une erreur est survenue pendant la lecture du fichier de la chaîne SPV.\nIl se peut que le fichier de la chaîne SPV soit corrompu.\n\nMessage d'erreur: {0}\n\nVoulez-vous l'effacer et lancer une resynchronisation? error.deleteAddressEntryListFailed=Impossible de supprimer le dossier AddressEntryList.\nErreur: {0}. error.closedTradeWithUnconfirmedDepositTx=La transaction de dépôt de l'échange fermé avec l'ID d'échange {0} n'est pas encore confirmée.\n\nVeuillez effectuer une resynchronisation SPV à \"Paramètres/Info sur le réseau\" pour voir si la transaction est valide. error.closedTradeWithNoDepositTx=La transaction de dépôt de l'échange fermé avec l'ID d'échange {0} est nulle.\n\nVeuillez redémarrer l'application pour nettoyer la liste des transactions fermées. popup.warning.walletNotInitialized=Le portefeuille n'est pas encore initialisé popup.warning.osxKeyLoggerWarning=En raison de mesures de sécurité plus strictes dans MacOS 10.14 et dans la version supérieure, le lancement d'une application Java (Haveno utilise Java) provoquera un avertissement pop-up dans MacOS (« Haveno souhaite recevoir les frappes de toute application »). \n\nPour éviter ce problème, veuillez ouvrir «Paramètres MacOS», puis allez dans «Sécurité et confidentialité» -> «Confidentialité» -> «Surveillance des entrées», puis supprimez «Haveno» de la liste de droite. \n\nUne fois les limitations techniques résolues (le packager Java de la version Java requise n'a pas été livré), Haveno effectuera une mise à niveau vers la nouvelle version Java pour éviter ce problème. popup.warning.wrongVersion=Vous avez probablement une mauvaise version de Haveno sur cet ordinateur.\nL'architecture de votre ordinateur est: {0}.\nLa binary Haveno que vous avez installé est: {1}.\nVeuillez éteindre et réinstaller une bonne version ({2}). popup.warning.incompatibleDB=Nous avons détecté un fichier de base de données incompatible!\n\nCes fichiers de base de données ne sont pas compatibles avec notre base de code actuelle: {0}\n\nNous avons sauvegardé les fichiers endommagés et appliqué les valeurs par défaut à la nouvelle version de la base de données.\n\nLa sauvegarde se trouve dans: \n\n{1} / db / backup_of_corrupted_data. \n\nVeuillez vérifier si vous avez installé la dernière version de Haveno. \n\nVous pouvez télécharger: \n\n[HYPERLINK:https://haveno.exchange/downloads] \n\nVeuillez redémarrer l'application. popup.warning.startupFailed.twoInstances=Haveno est déjà lancé. Vous ne pouvez pas lancer deux instances de haveno. popup.warning.tradePeriod.halfReached=Votre transaction avec ID {0} a atteint la moitié de la période de trading maximale autorisée et n'est toujours pas terminée.\n\nLa période de trade se termine le {1}.\n\nVeuillez vérifier l'état de votre transaction dans \"Portfolio/échanges en cours\" pour obtenir de plus amples informations. popup.warning.tradePeriod.ended=Votre échange avec l'ID {0} a atteint la période de trading maximale autorisée et n'est pas terminé.\n\nLa période d'échange s'est terminée le {1}.\n\nVeuillez vérifier votre transaction sur \"Portfolio/Echanges en cours\" pour contacter le médiateur. popup.warning.noTradingAccountSetup.headline=Vous n'avez pas configuré de compte de trading popup.warning.noTradingAccountSetup.msg=Vous devez configurer une devise nationale ou un compte crypto avant de pouvoir créer un ordre.\nVoulez-vous configurer un compte ? popup.warning.noArbitratorsAvailable=Les arbitres ne sont pas disponibles. popup.warning.noMediatorsAvailable=Il n'y a pas de médiateurs disponibles. popup.warning.notFullyConnected=Vous devez attendre d'être complètement connecté au réseau.\nCela peut prendre jusqu'à 2 minutes au démarrage. popup.warning.notSufficientConnectionsToXmrNetwork=Vous devez attendre d'avoir au minimum {0} connexions au réseau Monero. popup.warning.downloadNotComplete=Vous devez attendre que le téléchargement des blocs Monero manquants soit terminé. popup.warning.walletNotSynced=Le portefeuille Haveno n'est pas synchronisé avec la hauteur la plus récente de la blockchain. Veuillez patienter jusqu'à ce que le portefeuille soit synchronisé ou vérifiez votre connexion. popup.warning.removeOffer=Vous êtes certain de vouloir retirer cet ordre? popup.warning.tooLargePercentageValue=Vous ne pouvez pas définir un pourcentage de 100% ou plus grand. popup.warning.examplePercentageValue=Merci de saisir un nombre sous la forme d'un pourcentage tel que \"5.4\" pour 5.4% popup.warning.noPriceFeedAvailable=Il n'y a pas de flux pour le prix de disponible pour cette devise. Vous ne pouvez pas utiliser un prix basé sur un pourcentage.\nVeuillez sélectionner le prix fixé. popup.warning.sendMsgFailed=L'envoi du message à votre partenaire d'échange a échoué.\nMerci d'essayer de nouveau et si l'échec persiste merci de reporter le bug. popup.warning.messageTooLong=Votre message dépasse la taille maximale autorisée. Veuillez l'envoyer en plusieurs parties ou le télécharger depuis un service comme https://pastebin.com. popup.warning.lockedUpFunds=Vous avez des fonds bloqués d'une transaction qui a échoué.\nSolde bloqué: {0}\nAdresse de la tx de dépôt: {1}\nID de l'échange: {2}.\n\nVeuillez ouvrir un ticket de support en sélectionnant la transaction dans l'écran des transactions ouvertes et en appuyant sur \"alt + o\" ou \"option + o\". popup.warning.moneroConnection=Il y a eu un problème de connexion au réseau Monero.\n\n{0} popup.warning.makerTxInvalid=Cette offre n'est pas valide. Veuillez choisir une autre offre.\n\n takeOffer.cancelButton=Annuler la prise de l'offre takeOffer.warningButton=Ignorer et continuer tout de même # suppress inspection "UnusedProperty" popup.warning.nodeBanned=Un des noeuds {0} a été banni. # suppress inspection "UnusedProperty" popup.warning.priceRelay=Relais de prix popup.warning.seed=seed popup.warning.mandatoryUpdate.trading=Veuillez faire une mise à jour vers la dernière version de Haveno. Une mise à jour obligatoire a été publiée, laquelle désactive le trading sur les anciennes versions. Veuillez consulter le Forum Haveno pour obtenir plus d'informations. popup.warning.noFilter=Nous n'avons pas reçu d'objet de filtre des nœuds de seed. Veuillez informer les administrateurs du réseau d'enregistrer un objet de filtre. popup.warning.burnXMR=Cette transaction n'est pas possible, car les frais de minage de {0} dépasseraient le montant à transférer de {1}. Veuillez patienter jusqu'à ce que les frais de minage soient de nouveau bas ou jusqu'à ce que vous ayez accumulé plus de XMR à transférer. popup.warning.openOffer.makerFeeTxRejected=La transaction de frais de maker pour l'offre avec ID {0} a été rejetée par le réseau Monero.\nID de transaction={1}.\nL'offre a été retirée pour éviter d'autres problèmes.\nAllez dans \"Paramètres/Info sur le réseau réseau\" et faites une resynchronisation SPV.\nPour obtenir de l'aide, le canal support de l'équipe Haveno disposible sur Keybase. popup.warning.trade.txRejected.tradeFee=frais de transaction popup.warning.trade.txRejected.deposit=dépôt popup.warning.trade.txRejected=La transaction {0} pour le trade qui a pour ID {1} a été rejetée par le réseau Monero.\nID de transaction={2}.\nLe trade a été déplacé vers les échanges échoués.\nAllez dans \"Paramètres/Info sur le réseau\" et effectuez une resynchronisation SPV.\nPour obtenir de l'aide, le canal support de l'équipe Haveno est disponible sur Keybase. popup.warning.openOfferWithInvalidMakerFeeTx=La transaction de frais de maker pour l'offre avec ID {0} n'est pas valide.\nID de transaction={1}.\nAllez dans \"Paramètres/Info sur le réseau réseau\" et faites une resynchronisation SPV.\nPour obtenir de l'aide, le canal support de l'équipe Haveno est disponible sur Keybase. popup.info.securityDepositInfo=Afin de s'assurer que les deux traders suivent le protocole de trading, les deux traders doivent payer un dépôt de garantie.\n\nCe dépôt est conservé dans votre portefeuille d'échange jusqu'à ce que votre transaction soit terminée avec succès, et ensuite il vous sera restitué.\n\nRemarque : si vous créez un nouvel ordre, Haveno doit être en cours d'exécution pour qu'un autre trader puisse l'accepter. Pour garder vos ordres en ligne, laissez Haveno en marche et assurez-vous que cet ordinateur reste en ligne aussi (pour cela, assurez-vous qu'il ne passe pas en mode veille....le mode veille du moniteur ne pose aucun problème). popup.info.cashDepositInfo=Veuillez vous assurer d'avoir une succursale de l'établissement bancaire dans votre région afin de pouvoir effectuer le dépôt en espèces.\nL'identifiant bancaire (BIC/SWIFT) de la banque du vendeur est: {0}. popup.info.cashDepositInfo.confirm=Je confirme que je peux effectuer le dépôt. popup.info.shutDownWithOpenOffers=Haveno est en cours de fermeture, mais des ordres sont en attente.\n\nCes ordres ne seront pas disponibles sur le réseau P2P si Haveno est éteint, mais ils seront republiés sur le réseau P2P la prochaine fois que vous lancerez Haveno.\n\nPour garder vos ordres en ligne, laissez Haveno en marche et assurez-vous que cet ordinateur reste aussi en ligne (pour cela, assurez-vous qu'il ne passe pas en mode veille...la veille du moniteur ne pose aucun problème). popup.info.qubesOSSetupInfo=Il semble que vous exécutez Haveno sous Qubes OS.\n\nVeuillez vous assurer que votre Haveno qube est mis en place de la manière expliquée dans notre guide [LIEN:https://haveno.exchange/wiki/Running_Haveno_on_Qubes]. popup.warn.downGradePrevention=La rétrogradation depuis la version {0} vers la version {1} n'est pas supportée. Veuillez utiliser la dernière version de Haveno. popup.privateNotification.headline=Notification privée importante! popup.securityRecommendation.headline=Recommendation de sécurité importante popup.securityRecommendation.msg=Nous vous rappelons d'envisager d'utiliser la protection par mot de passe pour votre portefeuille si vous ne l'avez pas déjà activé.\n\nIl est également fortement recommandé d'écrire les mots de la seed de portefeuille. Ces mots de la seed sont comme un mot de passe principal pour récupérer votre portefeuille Monero.\nVous trouverez plus d'informations à ce sujet dans l'onglet \"seed du portefeuille\".\n\nDe plus, il est recommandé de sauvegarder le dossier complet des données de l'application dans l'onglet \"Sauvegarde". popup.xmrLocalNode.msg=Haveno a détecté un nœud Monero en cours d'exécution sur cette machine (sur l'hôte local).\n\nVeuillez vous assurer que le nœud est complètement synchronisé avant de démarrer Haveno. popup.shutDownInProgress.headline=Fermeture en cours popup.shutDownInProgress.msg=La fermeture de l'application nécessite quelques secondes.\nVeuillez ne pas interrompre ce processus. popup.attention.forTradeWithId=Attention requise la transaction avec l'ID {0} popup.attention.reasonForPaymentRuleChange=La version 1.5.5 introduit un changement critique de règle de trade concernant le champ \"raison du paiement\" dans les transferts banquaires. Veuillez laisser ce champ vide -- N'UTILISEZ PAS l'ID de trade comme \"raison de paiement\". popup.info.multiplePaymentAccounts.headline=Comptes de paiement multiples disponibles popup.info.multiplePaymentAccounts.msg=Vous disposez de plusieurs comptes de paiement disponibles pour cet ordre. Assurez-vous de choisir le bon. popup.accountSigning.selectAccounts.headline=Sélectionner les comptes de paiement popup.accountSigning.selectAccounts.description=En fonction du mode de paiement et du moment, tous les comptes de paiement qui sont liés à un litige où un paiement à l'acheteur a eu lieu seront sélectionnés afin que vous les signiez. popup.accountSigning.selectAccounts.signAll=Signer tous les modes de paiement popup.accountSigning.selectAccounts.datePicker=Sélectionnez le moment jusqu'auquel les comptes seront signés popup.accountSigning.confirmSelectedAccounts.headline=Confirmer les comptes de paiement sélectionnés popup.accountSigning.confirmSelectedAccounts.description=Suite à votre saisie, {0} comptes de paiement seront sélectionnés. popup.accountSigning.confirmSelectedAccounts.button=Confirmer les comptes de paiement popup.accountSigning.signAccounts.headline=Confirmer la signature des comptes de paiement popup.accountSigning.signAccounts.description=Suite à votre sélection, {0} comptes de paiement seront signés. popup.accountSigning.signAccounts.button=Signer les comptes de paiement popup.accountSigning.signAccounts.ECKey=Entrez la clé privée d'arbitrage popup.accountSigning.signAccounts.ECKey.error=Mauvaise ECKey de l'arbitre popup.accountSigning.success.headline=Félicitations popup.accountSigning.success.description=Tous les {0} comptes de paiement ont été signés avec succès ! popup.accountSigning.generalInformation=Vous trouverez l'état de signature de tous vos comptes dans la section compte.\n\nPour plus d'informations, veuillez consulter [LIEN:https://docs.haveno.exchange/payment-methods#account-signing]. popup.accountSigning.signedByArbitrator=Un de vos comptes de paiement a été vérifié et signé par un arbitre. Echanger avec ce compte signera automatiquement le compte de votre pair de trading après un échange réussi.\n\n{0} popup.accountSigning.signedByPeer=Un de vos comptes de paiement a été vérifié et signé par un pair de trading. Votre limite de trading initiale sera levée et vous pourrez signer d'autres comptes dans les {0} jours à venir.\n\n{1} popup.accountSigning.peerLimitLifted=La limite initiale pour l'un de vos comptes a été levée.\n\n{0} popup.accountSigning.peerSigner=Un de vos comptes est suffisamment mature pour signer d'autres comptes de paiement et la limite initiale pour un de vos comptes a été levée.\n\n{0} popup.accountSigning.singleAccountSelect.headline=Importer le témoin non-signé de l'âge du compte popup.accountSigning.confirmSingleAccount.headline=Confirmer le témoin de l'âge du compte sélectionné popup.accountSigning.confirmSingleAccount.selectedHash=Hash du témoin sélectionné popup.accountSigning.confirmSingleAccount.button=Signer le témoin de l'âge du compte popup.accountSigning.successSingleAccount.description=Le témoin {0} a été signé popup.accountSigning.successSingleAccount.success.headline=Succès popup.accountSigning.unsignedPubKeys.headline=Clés publiques non signées popup.accountSigning.unsignedPubKeys.sign=Signer les clés publiques popup.accountSigning.unsignedPubKeys.signed=Les clés publiques ont été signées popup.accountSigning.unsignedPubKeys.result.signed=Clés publiques signées popup.accountSigning.unsignedPubKeys.result.failed=Échec de la signature popup.info.buyerAsTakerWithoutDeposit.headline=Aucun dépôt requis de la part de l'acheteur popup.info.buyerAsTakerWithoutDeposit=Votre offre ne nécessitera pas de dépôt de sécurité ni de frais de la part de l'acheteur XMR.\n\nPour accepter votre offre, vous devez partager un mot de passe avec votre partenaire commercial en dehors de Haveno.\n\nLe mot de passe est généré automatiquement et affiché dans les détails de l'offre après sa création. #################################################################### # Notifications #################################################################### notification.trade.headline=Notification pour la transaction avec l'ID {0} notification.ticket.headline=Ticket de support pour l'échange avec l'ID {0} notification.trade.completed=La transaction est maintenant terminée et vous pouvez retirer vos fonds. notification.trade.accepted=Votre ordre a été accepté par un XMR {0}. notification.trade.unlocked=Votre échange avait au moins une confirmation sur la blockchain.\nVous pouvez effectuer le paiement maintenant. notification.trade.paymentSent=L'acheteur de XMR a initié le paiement. notification.trade.selectTrade=Choisir un trade notification.trade.peerOpenedDispute=Votre pair de trading a ouvert un {0}. notification.trade.disputeClosed=Le {0} a été fermé notification.walletUpdate.headline=Mise à jour du portefeuille de trading notification.walletUpdate.msg=Votre portefeuille de trading est suffisamment approvisionné.\nMontant: {0} notification.takeOffer.walletUpdate.msg=Votre portefeuille de trading était déjà suffisamment approvisionné à la suite d'une précédente tentative d'achat de l'ordre.\nMontant: {0} notification.tradeCompleted.headline=Le trade est terminé notification.tradeCompleted.msg=Vous pouvez retirer vos fonds vers un portefeuille Monero externe ou les conserver dans votre portefeuille Haveno. #################################################################### # System Tray #################################################################### systemTray.show=Montrer la fenêtre de l'application systemTray.hide=Cacher la fenêtre de l'application systemTray.info=Informations au sujet de Haveno systemTray.exit=Sortir systemTray.tooltip=Haveno: Une plateforme d'échange décentralisée sur le réseau monero #################################################################### # GUI Util #################################################################### guiUtil.accountExport.savedToPath=Les comptes de trading sont sauvegardés vers l'arborescence:\n{0} guiUtil.accountExport.noAccountSetup=Vous n'avez pas de comptes de trading configurés pour exportation. guiUtil.accountExport.selectPath=Sélectionner l'arborescence vers {0} # suppress inspection "TrailingSpacesInProperty" guiUtil.accountExport.tradingAccount=Compte de trading avec l'ID {0}\n # suppress inspection "TrailingSpacesInProperty" guiUtil.accountImport.noImport=Nous n'avons pas importé de compte de trading avec l'id {0} car il existe déjà.\n guiUtil.accountExport.exportFailed=Echec de l'export à CSV à cause d'une erreur.\nErreur = {0} guiUtil.accountExport.selectExportPath=Sélectionner l'arborescence d'export guiUtil.accountImport.imported=Compte de trading importé depuis l'arborescence:\n{0}\n\nComptes importés:\n{1} guiUtil.accountImport.noAccountsFound=Aucun compte de trading exporté n'a été trouvé sur l'arborescence {0}.\nLe nom du fichier est {1}." guiUtil.openWebBrowser.warning=Vous allez ouvrir une page Web dans le navigateur Web de votre système.\nVoulez-vous ouvrir la page web maintenant ?\n\nSi vous n'utilisez pas le \"Navigateur Tor\" comme navigateur web par défaut, vous vous connecterez à la page web en clair.\n\nURL: \"{0}\" guiUtil.openWebBrowser.doOpen=Ouvrir la page web et ne plus me le demander guiUtil.openWebBrowser.copyUrl=Copier l'URL et annuler guiUtil.ofTradeAmount=du montant du trade guiUtil.requiredMinimum=(minimum requis) #################################################################### # Component specific #################################################################### list.currency.select=Sélectionner la devise list.currency.showAll=Tout montrer list.currency.editList=Modifier la liste des devises table.placeholder.noItems=Il y a {0} disponible pour le moment table.placeholder.noData=Il n'y a actuellement aucune donnée disponible table.placeholder.processingData=Traitement des données en cours... peerInfoIcon.tooltip.tradePeer=Du pair de trading peerInfoIcon.tooltip.maker=du maker peerInfoIcon.tooltip.trade.traded={0} adresse onion: {1}\nVous avez déjà échangé a {2} reprise(s) avec ce pair\n{3} peerInfoIcon.tooltip.trade.notTraded={0} adresse onion: {1}\nvous n'avez pas échangé avec ce pair jusqu'à présent.\n{2} peerInfoIcon.tooltip.age=Compte de paiement créé il y a {0}. peerInfoIcon.tooltip.unknownAge=Ancienneté du compte de paiement inconnue. tooltip.openPopupForDetails=Ouvrir le popup pour obtenir des détails tooltip.invalidTradeState.warning=Le trade est dans un état invalide. Ouvrez la fenêtre des détails pour plus d'informations tooltip.openBlockchainForAddress=Ouvrir un explorateur de blockchain externe pour l'adresse: {0} tooltip.openBlockchainForTx=Ouvrir un explorateur de blockchain externe pour la transaction: {0} confidence.unknown=Statut de transaction inconnu confidence.seen=Vu par {0} pair(s) / 0 confirmations confidence.confirmed={0} confirmations confidence.invalid=La transaction est invalide peerInfo.title=info sur le pair peerInfo.nrOfTrades=Nombre d'opérations effectuées peerInfo.notTradedYet=Vous n'avez pas encore échangé avec cet utilisateur. peerInfo.setTag=Définir un tag pour ce pair peerInfo.age.noRisk=Âge du compte de paiement peerInfo.age.chargeBackRisk=Temps depuis la signature peerInfo.unknownAge=Âge inconnu addressTextField.openWallet=Ouvrir votre portefeuille Monero par défaut addressTextField.copyToClipboard=Copier l'adresse dans le presse-papiers addressTextField.addressCopiedToClipboard=L'adresse a été copiée dans le presse-papier addressTextField.openWallet.failed=L'ouverture d'un portefeuille Monero par défaut a échoué. Peut-être que vous n'en avez aucun d'installé? peerInfoIcon.tooltip={0}\nTag: {1} txIdTextField.copyIcon.tooltip=Copier l'ID de la transaction dans le presse-papiers txIdTextField.blockExplorerIcon.tooltip=Ouvrir l'explorateur de blockchain avec cet ID de transaction txIdTextField.missingTx.warning.tooltip=Transaction requise manquante #################################################################### # Navigation #################################################################### navigation.account=\"Compte\" navigation.account.walletSeed=\"Compte/Seed du portefeuille\" navigation.funds.availableForWithdrawal=\"Fonds/Envoyer des fonds\" navigation.portfolio.myOpenOffers=\"Portfolio/Mes ordres en cours\" navigation.portfolio.pending=\"Portfolio/Échanges en cours\" navigation.portfolio.closedTrades=\"Portfolio/Historique\" navigation.funds.depositFunds=\"Fonds/Recevoir des fonds\" navigation.settings.preferences=\"Paramètres/Préférences\" # suppress inspection "UnusedProperty" navigation.funds.transactions=\"Fonds/Transactions\" navigation.support=\"Assistance\" #################################################################### # Formatter #################################################################### formatter.formatVolumeLabel={0} montant{1} formatter.makerTaker=Maker comme {0} {1} / Taker comme {2} {3} formatter.makerTaker.locked=Maker comme {0} {1} / Taker comme {2} {3} 🔒 formatter.youAreAsMaker=Vous êtes {1} {0} (maker) / Le preneur est: {3} {2} formatter.youAreAsTaker=Vous êtes: {1} {0} (preneur) / Le maker est: {3} {2} formatter.youAre=Vous êtes {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Vous êtes en train de créer un ordre pour {0} {1} formatter.youAreCreatingAnOffer.traditional.locked=Vous êtes en train de créer un ordre pour {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Vous êtes en train de créer un ordre pour {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.crypto.locked=Vous êtes en train de créer un ordre pour {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} en tant que maker formatter.asTaker={0} {1} en tant que taker #################################################################### # Domain specific #################################################################### # we use enum values here # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" XMR_MAINNET=Monero Mainnet # suppress inspection "UnusedProperty" XMR_LOCAL=Monero Local Testnet # suppress inspection "UnusedProperty" XMR_STAGENET=Monero Stagenet time.year=Année time.month=Mois time.week=Semaine time.day=Jour time.hour=Heure time.minute10=10 Minutes time.hours=heures time.days=jours time.1hour=1 heure time.1day=1 jour time.minute=minute time.second=seconde time.minutes=minutes time.seconds=secondes password.enterPassword=Entrer le mot de passe password.confirmPassword=Confirmer le mot de passe password.tooLong=Le mot de passe doit contenir moins de 500 caractères. password.deriveKey=Récupérer la clé à partir du mot de passe password.walletDecrypted=Portefeuille décrypté avec succès et protection par mot de passe désactivée. password.wrongPw=Vous avez entré un mot de passe incorrect.\n\nVeuillez réessayer d'entrer votre mot de passe, en vérifiant soigneusement qu'il ne contient pas de fautes de frappe ou d'orthographe. password.walletEncrypted=Portefeuille crypté avec succès et protection par mot de passe activée. password.passwordsDoNotMatch=Les 2 mots de passe entrés ne correspondent pas. password.forgotPassword=Mot de passe oublié? password.backupReminder=Veuillez noter que lors de la définition d'un mot de passe de portefeuille, toutes les sauvegardes créées automatiquement à partir du portefeuille non crypté seront supprimées.\n\nIl est fortement recommandé de faire une sauvegarde du répertoire de l'application et d'écrire les mots source avant de définir un mot de passe! password.backupWasDone=J'ai déjà effectué une sauvegarde password.setPassword=Saisir le mot de passe (J'ai déjà effectué une sauvegarde) password.makeBackup=Effectuer une sauvegarde seed.seedWords=Mots qui composent la seed du portefeuille seed.enterSeedWords=Entrer les mots qui composent la seed du portefeuille seed.date=Date du portefeuille seed.restore.title=Restaurer les portefeuilles à partir des mots de la seed seed.restore=Restaurer les portefeuilles seed.creationDate=Date de création seed.warn.walletNotEmpty.msg=Votre portefeuille Monero n'est pas vide.\n\nVous devez vider ce portefeuille avant d'essayer de restaurer un portefeuille plus ancien, en effet mélanger les portefeuilles peut entraîner l'invalidation des sauvegardes.\n\nVeuillez finaliser vos trades, fermer toutes vos offres ouvertes et aller dans la section Fonds pour retirer votre Monero.\nDans le cas où vous ne pouvez pas accéder à votre monero, vous pouvez utiliser l'outil d'urgence afin de vider votre portefeuille.\nPour ouvrir l'outil d'urgence, pressez \"alt + e\" ou \"Cmd/Ctrl + e\". seed.warn.walletNotEmpty.restore=Je veux quand même restaurer. seed.warn.walletNotEmpty.emptyWallet=Je viderai mes portefeuilles en premier. seed.warn.notEncryptedAnymore=Vos portefeuilles sont cryptés.\n\nAprès la restauration, les portefeuilles ne seront plus cryptés et vous devrez définir un nouveau mot de passe.\n\nSouhaitez-vous continuer ? seed.warn.walletDateEmpty=Puisque vous n'avez pas spécifié la date du portefeuille, Haveno devra scanner la blockchain après le 09/10/2013 (date de création du BIP39). \n\nLe portefeuille BIP39 a été lancé pour la première fois sur Haveno le 28/06/2017 (version v0.5). Par conséquent, vous pouvez utiliser cette date pour gagner du temps. \n\nIdéalement, vous devez indiquer la date à laquelle la graine de départ du portefeuille est créée. \n\n\nÊtes-vous sûr de vouloir continuer sans spécifier la date du portefeuille? seed.restore.success=Portefeuilles restaurés avec succès grâce aux nouveaux mots de la seed.\n\nVous devez arrêter et redémarrer l'application. seed.restore.error=Une erreur est survenue lors de la restauration des portefeuilles avec les mots composant la seed.{0} seed.restore.openOffers.warn=Vous avez des offres ouvertes qui seront retirées si vous restaurer à partir des mots sources.\nÊtes-vous sûr de vouloir continuer. #################################################################### # Payment methods #################################################################### payment.account=Compte payment.account.no=N° de compte payment.account.name=Nom du compte payment.account.username=Nom de l'utilisateur payment.account.phoneNr=Numéro de téléphone payment.account.owner.fullname=Nom et prénoms du propriétaire du compte payment.account.fullName=Nom complet (prénom, deuxième prénom, nom de famille) payment.account.state=État/Département/Région payment.account.city=Ville payment.bank.country=Pays de la banque payment.account.name.email=Nom complet du propriétaire du compte / email payment.account.name.emailAndHolderId=Nom complet du propriétaire du compte / email / {0} payment.bank.name=Nom de la banque payment.select.account=Sélectionner le type de compte payment.select.region=Sélectionner la région payment.select.country=Sélectionner le pays payment.select.bank.country=Sélectionner le pays de la banque payment.foreign.currency=Êtes-vous sûr de vouloir choisir une devise autre que celle du pays par défaut? payment.restore.default=Non, restaurer la devise par défaut payment.email=Email payment.country=Pays payment.extras=Exigences particulières payment.email.mobile=Email ou N° de portable payment.crypto.address=Adresse Crypto payment.crypto.tradeInstantCheckbox=Échanger instantanément (en 1 heure) avec cet Crypto payment.crypto.tradeInstant.popup=Pour négocier immédiatement, il est nécessaire que les deux pairs de trading soient en ligne afin de pouvoir effectuer l'échange en moins d' 1 heure.\n\nSi vous avez des ordres en cours et que vous n'êtes pas disponible, veuillez désactiver ces ordres sur la page " Portfolio ". payment.crypto=Crypto payment.select.crypto=Sélectionner ou chercher l'Crypto payment.secret=Question secrète payment.answer=Réponse payment.wallet=ID du portefeuille payment.amazon.site=Acheter la carte cadeau à payment.ask=Demander dans le chat de trade payment.uphold.accountId=Nom d'utilisateur ou email ou N° de téléphone payment.moneyBeam.accountId=Email ou N° de téléphone payment.popmoney.accountId=Email ou N° de téléphone payment.promptPay.promptPayId=N° de carte d'identité/d'identification du contribuable ou numéro de téléphone payment.supportedCurrencies=Devises acceptées payment.supportedCurrenciesForReceiver=Devises pour reçevoir des fonds payment.limitations=Restrictions payment.salt=Salage de la vérification de l'âge des comptes payment.error.noHexSalt=Le salt doit être au format HEX .\nIl est recommandé de modifier le champ du salt uniquement si vous souhaitez effectuer le transfert d'un salt d'un précédent compte pour conserver l'âge du compte. L'âge du compte est vérifié en utilisant le salt du compte et les données d'identification du compte (par exemple l'IBAN). payment.accept.euro=Accepter les transactions en provenance de ces pays de la zone Euro payment.accept.nonEuro=Accepter les transactions en provenance de ces pays hors zone Euro payment.accepted.countries=Pays acceptés payment.accepted.banks=Banques acceptées (ID) payment.mobile=N° de téléphone portable payment.postal.address=Adresse postale payment.national.account.id.AR=Numéro CBU shared.accountSigningState=État de la signature du compte #new payment.crypto.address.dyn={0} adresse payment.crypto.receiver.address=Adresse crypto du destinataire payment.accountNr=Numéro de compte payment.emailOrMobile=Email ou N° de portable payment.useCustomAccountName=Utiliser un nom de compte personnalisé payment.maxPeriod=Durée d'échange max. autorisée payment.maxPeriodAndLimit=Durée maximale de l'échange : {0} / Achat maximum : {1} / Vente maximum : {2} / Âge du compte : {3} payment.maxPeriodAndLimitCrypto=Durée maximale de trade: {0} / Limite maximale de trading {1} payment.currencyWithSymbol=Devise: {0} payment.nameOfAcceptedBank=Nom de la banque acceptée payment.addAcceptedBank=Ajouter une banque acceptée payment.clearAcceptedBanks=Supprimer des banques acceptées payment.bank.nameOptional=Nom de la banque (facultatif) payment.bankCode=Code de la banque payment.bankId=Identifiant bancaire (BIC/SWIFT) payment.bankIdOptional=ID de la banque (BIC/SWIFT) (optionnel): payment.branchNr=N° de la succursale payment.branchNrOptional=N° de succursale (facultatif) payment.accountNrLabel=N° de compte (IBAN) payment.accountType=Type de compte payment.checking=Vérification payment.savings=Épargne payment.personalId=Pièce d'identité payment.makeOfferToUnsignedAccount.warning=Avec la récente montée en prix du XMR, soyez informés que vendre {0} ou moins cause un risque plus élevé qu'avant.\n\nIl est hautement recommandé de:\n- faire des offres au dessus de {0}, ainsi vous traiterez uniquement avec des acheteurs signés/de confiance\n- garder les offres pour vendre en desous de {0} à une valeur d'environ 100 USD, cette valeur a (historiquement) découragé les arnaqueurs\n\nLes développeurs de Haveno travaillent sur des meilleurs moyens de sécuriser le modèle de compte de paiement pour des trades plus petits. Rejoignez la discussion ici : [LIEN:https://github.com/bisq-network/bisq/discussions/5339]. payment.takeOfferFromUnsignedAccount.warning=Avec la récente montée en prix du XMR, soyez informés que vendre {0} ou moins cause un risque plus élevé qu'avant.\n\nIl est hautement recommandé de:\n- prendre les offres d'acheteurs signés uniquement\n- garder les offres pour vendre en desous de {0} à une valeur d'environ 100 USD, cette valeur a (historiquement) découragé les arnaqueurs\n\nLes développeurs de Haveno travaillent sur des meilleurs moyens de sécuriser le modèle de compte de paiement pour des trades plus petits. Rejoignez la discussion ici : [LIEN:https://github.com/bisq-network/bisq/discussions/5339]. payment.zelle.info=Zelle est un service de transfert d'argent, qui fonctionne bien pour transférer de l'argent vers d'autres banques. \n\n1. Consultez cette page pour voir si (et comment) votre banque coopère avec Zelle: \n[HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Faites particulièrement attention à votre limite de transfert - les limites de versement varient d'une banque à l'autre, et les banques spécifient généralement des limites quotidiennes, hebdomadaires et mensuelles. \n\n3. Si votre banque ne peut pas utiliser Zelle, vous pouvez toujours l'utiliser via l'application mobile Zelle, mais votre limite de transfert sera bien inférieure. \n\n4. Le nom indiqué sur votre compte Haveno doit correspondre à celui du compte Zelle / bancaire. \n\nSi vous ne parvenez pas à réaliser la transaction Zelle comme stipulé dans le contrat commercial, vous risquez de perdre une partie (ou la totalité) de votre marge.\n\nComme Zelle présente un risque élevé de rétrofacturation, il est recommandé aux vendeurs de contacter les acheteurs non signés par e-mail ou SMS pour confirmer que les acheteurs ont le compte Zelle spécifié dans Haveno. payment.fasterPayments.newRequirements.info=Certaines banques ont déjà commencé à vérifier le nom complet du destinataire du paiement rapide. Votre compte de paiement rapide actuel ne remplit pas le nom complet. \n\nPensez à recréer votre compte de paiement rapide dans Haveno pour fournir un nom complet aux futurs {0} acheteurs. \n\nLors de la recréation d'un compte, assurez-vous de copier l'indicatif bancaire, le numéro de compte et le sel de vérification de l'âge de l'ancien compte vers le nouveau compte. Cela garantira que votre âge du compte et état de signature existant sont conservés. payment.moneyGram.info=Lors de l'utilisation de MoneyGram, l'acheteur de XMR doit envoyer le numéro d'autorisation et une photo du reçu par email au vendeur de XMR. Le reçu doit clairement mentionner le nom complet du vendeur, le pays, la région et le montant. L'email du vendeur sera donné à l'acheteur durant le processus de transaction. payment.westernUnion.info=Lors de l'utilisation de Western Union, l'acheteur XMR doit envoyer le MTCN (numéro de suivi) et une photo du reçu par e-mail au vendeur de XMR. Le reçu doit indiquer clairement le nom complet du vendeur, la ville, le pays et le montant. L'acheteur verra ensuite s'afficher l'email du vendeur pendant le processus de transaction. payment.halCash.info=Lors de l'utilisation de HalCash, l'acheteur de XMR doit envoyer au vendeur de XMR le code HalCash par SMS depuis son téléphone portable.\n\nVeuillez vous assurer de ne pas dépasser le montant maximum que votre banque vous permet d'envoyer avec HalCash. Le montant minimum par retrait est de 10 EUR et le montant maximum est de 600 EUR. Pour les retraits récurrents, il est de 3000 EUR par destinataire par jour et 6000 EUR par destinataire par mois. Veuillez vérifier ces limites auprès de votre banque pour vous assurer qu'elles utilisent les mêmes limites que celles indiquées ici.\n\nLe montant du retrait doit être un multiple de 10 EUR car vous ne pouvez pas retirer d'autres montants à un distributeur automatique. Pendant les phases de create-offer et take-offer l'affichage de l'interface utilisateur ajustera le montant en XMR afin que le montant en euros soit correct. Vous ne pouvez pas utiliser le prix basé sur le marché, car le montant en euros varierait en fonction de l'évolution des prix.\n\nEn cas de litige, l'acheteur de XMR doit fournir la preuve qu'il a envoyé la somme en EUR. # suppress inspection "UnusedMessageFormatParameter" payment.limits.info=Sachez que tous les virements bancaires comportent un certain risque de rétrofacturation. Pour mitiger ce risque, Haveno fixe des limites par trade en fonction du niveau estimé de risque de rétrofacturation pour la méthode de paiement utilisée.\n\nPour cette méthode de paiement, votre limite de trading pour l'achat et la vente est de {2}.\n\nCette limite ne s'applique qu'à la taille d'une seule transaction. Vous pouvez effectuer autant de transactions que vous le souhaitez.\n\nVous trouverez plus de détails sur le wiki [HYPERLINK:https://docs.haveno.exchange/overview/account_limits]. # suppress inspection "UnusedProperty" payment.limits.info.withSigning=Afin de limiter le risque de rétrofacturation des achats, Haveno fixe des limites d'achat par transaction pour ce compte de paiement basé sur les 2 facteurs suivants :\n\n1. Risque de rétrofacturation pour le mode de paiement\n2. Statut de signature du compte\n\nCe compte de paiement n'est pas encore signé, il est donc limité à l'achat de {0} par trade. Après sa signature, les limites d'achat augmenteront comme suit :\n\n● Avant la signature, et jusqu'à 30 jours après la signature, votre limite d'achat par trade sera de {0}\n● 30 jours après la signature, votre limite d'achat par trade sera de {1}\n● 60 jours après la signature, votre limite d'achat par trade sera de {2}\n\nLes limites de vente ne sont pas affectées par la signature du compte. Vous pouvez vendre {2} en un seul trade immédiatement.\n\nCes limites s'appliquent uniquement à la taille d'un seul trade-vous pouvez placer autant de trades que vous voulez.\n\n Pour plus d'nformations, rendez vous à [LIEN:https://docs.haveno.exchange/overview/account_limits]. payment.cashDeposit.info=Veuillez confirmer que votre banque vous permet d'envoyer des dépôts en espèces sur le compte d'autres personnes. Par exemple, Bank of America et Wells Fargo n'autorisent plus de tels dépôts. payment.revolut.info=Revolut nécessite le 'Nom d'utilisateur' en tant qu'ID de compte et non pas le numéro de téléphone ou l'email comme ça l'était avant. payment.account.revolut.addUserNameInfo={0}\nVotre compte Revolut existant ({1}) n'a pas de "Nom d'utilisateur".\nVeuillez entrer votre "Nom d'utilisateur" Revolut pour mettre à jour les données de votre compte.\nCeci n'affectera pas l'âge du compte. payment.revolut.addUserNameInfo.headLine=Mettre à jour le compte Revolut payment.cashapp.info=Veuillez noter que Cash App présente un risque de rétrofacturation plus élevé que la plupart des virements bancaires. payment.venmo.info=Veuillez noter que Venmo présente un risque de rétrofacturation plus élevé que la plupart des virements bancaires. payment.paypal.info=Veuillez noter que PayPal présente un risque de rétrofacturation plus élevé que la plupart des virements bancaires. payment.amazonGiftCard.upgrade=La méthode de paiement via carte cadeaux Amazon nécessite que le pays soit spécifié. payment.account.amazonGiftCard.addCountryInfo={0}\nVotre compte carte cadeau Amazon existant ({1}) n'a pas de pays spécifé.\nVeuillez entrer le pays de votre compte carte cadeau Amazon pour mettre à jour les données de votre compte.\nCeci n'affectera pas le statut de l'âge du compte. payment.amazonGiftCard.upgrade.headLine=Mettre à jour le compte des cartes cadeaux Amazon payment.usPostalMoneyOrder.info=Pour échanger US Postal Money Orders (USPMO) sur Haveno, vous devez comprendre les termes suivants: \n\n- L'acheteur XMR doit écrire le nom du vendeur XMR dans les champs expéditeur et bénéficiaire, et prendre une photo à haute résolution de USPMO et de l'enveloppe avec une preuve de suivi avant l'envoi. \n\n- L'acheteur XMR doit envoyer USPMO avec la confirmation de livraison au vendeur XMR. \n\nSi une médiation est nécessaire, ou s'il y a un différend de transaction, vous devrez envoyer la photo avec le numéro USPMO, le numéro du bureau de poste et le montant de la transaction au médiateur Haveno ou à l'agent de remboursement afin qu'ils puissent vérifier les détails sur le site web de la poste américaine. \n\nSi vous ne fournissez pas les données de transaction requises, vous perdrez directement dans le différend. \n\nDans tous les cas de litige, l'expéditeur de l'USPMO assume à 100% la responsabilité lors de la fourniture de preuves / certification au médiateur ou à l'arbitre. \n\nSi vous ne comprenez pas ces exigences, veuillez ne pas échanger USPMO sur Haveno. payment.payByMail.info=Effectuer un échange en utilisant Pay by Mail sur Haveno nécessite que vous compreniez ce qui suit :\n\ \n\ ● L'acheteur de Monero doit emballer l'argent liquide dans un sac d'argent inviolable.\n\ ● L'acheteur de Monero doit filmer ou prendre des photos haute résolution du processus d'emballage en espèces avec l'adresse et le numéro de suivi déjà apposés sur l'emballage.\n\ ● L'acheteur de Monero doit envoyer le colis en espèces au vendeur Monero avec une confirmation de livraison et une assurance appropriée.\n\ ● Le vendeur de Monero doit filmer l'ouverture du colis, en s'assurant que le numéro de suivi fourni par l'expéditeur est visible dans la vidéo.\n\ ● Le créateur de l'offre doit indiquer toutes les conditions particulières dans le champ « Informations supplémentaires » du compte de paiement.\n\ ● Le preneur de l'offre accepte les conditions générales du créateur de l'offre en acceptant l'offre.\n\ \n\ Les transactions Pay by Mail imposent la responsabilité d'agir honnêtement pour les deux pairs.\n\ \n\ ● Les transactions Pay by Mail ont des actions moins vérifiables que les autres transactions Fiat. Cela rend la gestion des litiges beaucoup plus difficile.\n\ ● Essayez de résoudre les litiges directement avec votre pair en utilisant le chat de trade. C'est la voie la plus prometteuse pour résoudre tout litige Pay by Mail.\n\ ● Les médiateurs peuvent examiner votre cas et faire une suggestion, mais il n'est PAS garanti qu'ils puissent vous aider.\n\ ● Les médiateurs prendront une décision sur la base des preuves qui leur auront été fournies. Par conséquent, veuillez suivre et rédiger un document sur les processus ci-dessus pour avoir des preuves en cas de litige. Pour les transactions Pay by Mail, la décision des médiateurs est définitive.\n\ ● Les demandes de remboursement de fonds perdus résultant de transactions Pay By Mail vers Haveno ne seront PAS prises en compte.\n\ \n\ Si vous ne comprenez pas ces exigences, n'échangez pas en utilisant Pay by Mail sur Haveno. payment.payByMail.contact=information de contact payment.payByMail.contact.prompt=Nom ou nym à qui l'enveloppe devrait être addressée payment.f2f.contact=information de contact payment.f2f.contact.prompt=Comment voudriez-vous être contacté par le pair de trading? (addresse mail, numéro de téléphone,...) payment.f2f.city=Ville pour la rencontre en face à face payment.f2f.city.prompt=La ville sera affichée en même temps que l'ordre payment.shared.optionalExtra=Informations complémentaires facultatives payment.shared.extraInfo=Informations complémentaires payment.shared.extraInfo.offer=Informations supplémentaires sur l'offre payment.shared.extraInfo.prompt.paymentAccount=Définissez n'importe quels termes spécifiques, conditons ou détails que vous souhaiteriez voir affichés avec vos offres pour ce compte de paiement (les utilisateurs verront ces informations avant d'accepter les offres). payment.shared.extraInfo.prompt.offer=Définissez tous les termes, conditions ou détails spéciaux que vous souhaitez afficher avec votre offre. payment.shared.extraInfo.noDeposit=Coordonnées et conditions de l'offre payment.f2f.info=Les transactions en 'face à face' ont des règles différentes et comportent des risques différents de ceux des transactions en ligne.\n\nLes principales différences sont les suivantes:\n● Les pairs de trading doivent échanger des informations sur le lieu et l'heure de la réunion en utilisant les coordonnées de contanct qu'ils ont fournies.\n● Les pairs de trading doivent apporter leur ordinateur portable et faire la confirmation du 'paiement envoyé' et du 'paiement reçu' sur le lieu de la réunion.\n● Si un maker a des 'termes et conditions' spéciaux, il doit les indiquer dans le champ 'Informations supplémentaires' dans le compte.\n● En acceptant une offre, le taker accepte les 'termes et conditions' du maker.\n● En cas de litige, le médiateur ou l'arbitre ne peut pas beaucoup aider car il est généralement difficile d'obtenir des preuves irréfutables de ce qui s'est passé lors de la réunion. Dans ce cas, les fonds en XMR peuvent être bloqué s indéfiniment tant que les pairs ne parviennent pas à un accord.\n\nPour vous assurer de bien comprendre les spécificités des transactions 'face à face', veuillez lire les instructions et les recommandations à [LIEN:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=Ouvrir la page web payment.f2f.offerbook.tooltip.countryAndCity=Pays et ville: {0} / {1} payment.shared.extraInfo.tooltip=Informations complémentaires: {0} payment.japan.bank=Banque payment.japan.branch=Filiale payment.japan.account=Compte payment.japan.recipient=Nom payment.australia.payid=ID de paiement payment.payid=ID de paiement lié à une institution financière. Comme l'addresse email ou le téléphone portable. payment.payid.info=Un PayID, tel qu'un numéro de téléphone, une adresse électronique ou un numéro d'entreprise australien (ABN), que vous pouvez lier en toute sécurité à votre compte bancaire, votre crédit mutuel ou votre société de crédit immobilier. Vous devez avoir déjà créé un PayID auprès de votre institution financière australienne. Les institutions financières émettrices et réceptrices doivent toutes deux prendre en charge PayID. Pour plus d'informations, veuillez consulter [LIEN:https://payid.com.au/faqs/]. payment.amazonGiftCard.info=Pour payer avec une carte cadeau Amazon eGift Card, vous devrez envoyer une carte cadeau Amazon eGift Card au vendeur de XMR via votre compte Amazon. \n\nHaveno indiquera l'adresse e-mail ou le numéro de téléphone du vendeur XMR où la carte cadeau doit être envoyée, et vous devrez inclure l'ID du trade dans le champ de messagerie de la carte cadeau. Veuillez consulter le wiki [LIEN:https://docs.haveno.exchange/overview/payment_methods/Amazon_eGift_card] pour plus de détails et pour les meilleures pratiques à adopter. \n\nTrois remarques importantes :\n- essayez d'envoyer des cartes-cadeaux d'un montant inférieur ou égal à 100 USD, car Amazon est connu pour signaler les cartes-cadeaux plus importantes comme frauduleuses\n- essayez d'utiliser un texte créatif et crédible pour le message de la carte cadeau (par exemple, "Joyeux anniversaire Susan !") ainsi que l'ID du trade (et utilisez le chat du trader pour indiquer à votre pair de trading le texte de référence que vous avez choisi afin qu'il puisse vérifier votre paiement).\n- Les cartes cadeaux électroniques Amazon ne peuvent être échangées que sur le site Amazon où elles ont été achetées (par exemple, une carte cadeau achetée sur amazon.it ne peut être échangée que sur amazon.it). payment.paysafe.info=Pour votre protection, nous déconseillons fortement d'utiliser les PINs Paysafecard pour les paiements.\n\n\ Les transactions effectuées via des PINs ne peuvent pas être vérifiées de manière indépendante pour la résolution des litiges. En cas de problème, la récupération des fonds peut ne pas être possible.\n\n\ Pour garantir la sécurité des transactions et la résolution des litiges, utilisez toujours des méthodes de paiement qui fournissent des preuves vérifiables. # We use constants from the code so we do not use our normal naming convention # dynamic values are not recognized by IntelliJ # Only translate general terms NATIONAL_BANK=Virement bancaire national SAME_BANK=Transfert avec la même banque SPECIFIC_BANKS=Transferts avec des banques spécifiques US_POSTAL_MONEY_ORDER=US Postal Money Order CASH_DEPOSIT=Dépôt en espèces PAY_BY_MAIL=Cash via courrier MONEY_GRAM=MoneyGram WESTERN_UNION=Western Union F2F=Face à face (en personne) JAPAN_BANK=Banque japonaise Furikomi AUSTRALIA_PAYID=PayID australien # suppress inspection "UnusedProperty" NATIONAL_BANK_SHORT=Banques nationales # suppress inspection "UnusedProperty" SAME_BANK_SHORT=Banque identique # suppress inspection "UnusedProperty" SPECIFIC_BANKS_SHORT=Banque spécifiques # suppress inspection "UnusedProperty" US_POSTAL_MONEY_ORDER_SHORT=US Money Order # suppress inspection "UnusedProperty" CASH_DEPOSIT_SHORT=Dépôt en espèces # suppress inspection "UnusedProperty" PAY_BY_MAIL_SHORT=Cash via courrier # suppress inspection "UnusedProperty" MONEY_GRAM_SHORT=MoneyGram # suppress inspection "UnusedProperty" WESTERN_UNION_SHORT=Western Union # suppress inspection "UnusedProperty" F2F_SHORT=F2F # suppress inspection "UnusedProperty" JAPAN_BANK_SHORT=Furikomi japonais # suppress inspection "UnusedProperty" AUSTRALIA_PAYID_SHORT=ID de paiement # Do not translate brand names # suppress inspection "UnusedProperty" UPHOLD=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY=Popmoney # suppress inspection "UnusedProperty" REVOLUT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY=WeChat Pay # suppress inspection "UnusedProperty" SEPA=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT=SEPA Instant Payments # suppress inspection "UnusedProperty" FASTER_PAYMENTS=Faster Payments # suppress inspection "UnusedProperty" SWISH=Swish # suppress inspection "UnusedProperty" ZELLE=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS=Cryptos # suppress inspection "UnusedProperty" PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=eCarte cadeau Amazon # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT=Cryptos Instant # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY=OKPay # suppress inspection "UnusedProperty" CASH_APP=Cash App # suppress inspection "UnusedProperty" VENMO=Venmo # suppress inspection "UnusedProperty" UPHOLD_SHORT=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM_SHORT=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY_SHORT=Popmoney # suppress inspection "UnusedProperty" REVOLUT_SHORT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY_SHORT=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY_SHORT=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY_SHORT=WeChat Pay # suppress inspection "UnusedProperty" SEPA_SHORT=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT_SHORT=SEPA Instant # suppress inspection "UnusedProperty" FASTER_PAYMENTS_SHORT=Paiements plus rapides # suppress inspection "UnusedProperty" SWISH_SHORT=Swish # suppress inspection "UnusedProperty" ZELLE_SHORT=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY_SHORT=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER_SHORT=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH_SHORT=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS_SHORT=Cryptos # suppress inspection "UnusedProperty" PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=eCarte cadeau Amazon # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT_SHORT=Cryptos Instant # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY_SHORT=OKPay # suppress inspection "UnusedProperty" CASH_APP_SHORT=Cash App # suppress inspection "UnusedProperty" VENMO_SHORT=Venmo #################################################################### # Validation #################################################################### validation.empty=Un champ vide n'est pas autorisé. validation.NaN=La valeur saisie n'est pas un nombre valide. validation.notAnInteger=Input is not an integer value. validation.zero=La saisie d'une valeur égale à 0 n'est pas autorisé. validation.negative=Une valeur négative n'est pas autorisée. validation.traditional.tooSmall=La saisie d'une valeur plus petite que le montant minimal possible n'est pas autorisée. validation.traditional.tooLarge=La saisie d'une valeur supérieure au montant maximal possible n'est pas autorisée. validation.xmr.fraction=L'entrée résultera dans une valeur monero plus petite qu'1 satoshi validation.xmr.tooLarge=La saisie d'une valeur supérieure à {0} n'est pas autorisée. validation.xmr.tooSmall=La saisie d'une valeur inférieure à {0} n'est pas autorisée. validation.passwordTooShort=Le mot de passe que vous avez saisi est trop court. Il doit comporter un minimum de 8 caractères. validation.passwordTooLong=Le mot de passe que vous avez saisi est trop long. Il ne doit pas contenir plus de 50 caractères. validation.sortCodeNumber={0} doit être composer de {1} chiffres. validation.sortCodeChars={0} doit être composer de {1} caractères. validation.bankIdNumber={0} doit être composer de {1} chiffres. validation.accountNr=Le numéro du compte doit comporter {0} chiffres. validation.accountNrChars=Le numéro du compte doit comporter {0} caractères. validation.xmr.invalidAddress=L'adresse n'est pas correcte. Veuillez vérifier le format de l'adresse. validation.integerOnly=Veuillez seulement entrer des nombres entiers. validation.inputError=Votre saisie a causé une erreur:\n{0} validation.xmr.exceedsMaxTradeLimit=Votre seuil maximum d'échange est {0}. validation.nationalAccountId={0} doit être composé de {1} nombres. #new validation.invalidInput=La valeur saisie est invalide: {0} validation.accountNrFormat=Le numéro du compte doit être au format: {0} # suppress inspection "UnusedProperty" validation.crypto.wrongStructure=La validation de l'adresse a échoué car elle ne concorde pas avec la structure d'une adresse {0}. # suppress inspection "UnusedProperty" validation.crypto.ltz.zAddressesNotSupported=L'adresse LTZ doit commencer par L. Les adresses commençant par z ne sont pas supportées. # suppress inspection "UnusedProperty" validation.crypto.zAddressesNotSupported=L'adresse ZEC doit commencer par un t. Les adresses commençant par un z ne sont pas supportées. # suppress inspection "UnusedProperty" validation.crypto.invalidAddress=Address is not a valid {0} address! {1} # suppress inspection "UnusedProperty" validation.crypto.liquidBitcoin.invalidAddress=Les addresses segwit natives (celles qui commences par 'lq') ne sont pas supportées. validation.bic.invalidLength=La longueur de l'entrée doit être de 8 ou 11 validation.bic.letters=Le code de la banque et le code du pays doivent être constitués de lettres validation.bic.invalidLocationCode=Le BIC contient un code de localisation invalide validation.bic.invalidBranchCode=Le BIC contient un branch code invalide. validation.bic.sepaRevolutBic=Les comptes Sepa de Revolut ne sont pas pris en charge. validation.btc.invalidFormat=Format invalide pour une addresse Bitcoin. validation.email.invalidAddress=Adresse invalide validation.iban.invalidCountryCode=Code du pays invalide validation.iban.checkSumNotNumeric=La checksum doit être numérique validation.iban.nonNumericChars=Caractère non-alphanumérique détecté validation.iban.checkSumInvalid=La checksum de l'IBAN n'est pas valide validation.iban.invalidLength=Le nombre doit avoir une longueur de 15 à 34 caractères. validation.interacETransfer.invalidAreaCode=Indicatif régional non Canadien validation.interacETransfer.invalidPhone=Veuillez entrer un numéro de téléphone valide à 11 chiffres (par exemple 1-123-456-7890) ou une addresse email validation.interacETransfer.invalidQuestion=Ne doit contenir que des lettres, des chiffres, des espaces et/ou les symboles ' _ , . ? - validation.interacETransfer.invalidAnswer=Doit être composé d'un seul mot et contenir que des lettres, des chiffres et/ou le symbole - validation.inputTooLarge=La valeur saisie ne doit pas être supérieure à {0} validation.inputTooSmall=La valeur saisie doit être supérieure à {0} validation.inputToBeAtLeast=La valeur saisie doit être au minimum {0} validation.amountBelowDust=Un montant en-dessous de la limite de dust de {0} satoshi nest pas autorisé. validation.length=La longueur doit être comprise entre {0} et {1} validation.fixedLength=La longueur doit être de {0} validation.pattern=La valeur saisie doit être au format: {0} validation.noHexString=La valeur saisie n'est pas au format HEX. validation.advancedCash.invalidFormat=Doit être un email valide ou un identifiant de portefeuille de type: X000000000000 validation.invalidUrl=Ceci n'est pas une URL valide validation.mustBeDifferent=Votre saisie doit être différente de la valeur actuelle. validation.cannotBeChanged=Le paramètre ne peut pas être modifié validation.numberFormatException=Number format exception {0} validation.mustNotBeNegative=La saisie ne doit pas être négative validation.phone.missingCountryCode=Un code pays à deux lettres est nécessaire pour valider le numéro de téléphone validation.phone.invalidCharacters=Le numéro de téléphone {0} contient des caractères invalides. validation.phone.insufficientDigits=Il n'y a pas assez de chiffres dans {0} pour être un numéro de téléphone valide validation.phone.tooManyDigits=Il y'a trop de chiffres dans {0} pour être un numéro de téléphone valide validation.phone.invalidDialingCode=L'indicatif de pays du numéro {0} est invalide pour le pays {1}. Le bon indicatif est {2}. validation.invalidAddressList=Doit être une liste d'addresses valide séparées par des virgules ================================================ FILE: core/src/main/resources/i18n/displayStrings_it.properties ================================================ # Keep display strings organized by domain # Naming convention: We use camelCase and dot separated name spaces. # Use as many sub spaces as required to make the structure clear, but as little as possible. # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break # in the display but only in the editor. # Please use in all language files the exact same order of the entries, that way a comparison is easier. # Please try to keep the length of the translated string similar to English. If it is longer it might break layout or # get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. #################################################################### # Shared #################################################################### shared.readMore=Leggi di più shared.openHelp=Apri la Guida shared.warning=Attenzione shared.close=Chiudi shared.cancel=Annulla shared.ok=OK shared.yes=Si shared.no=No shared.iUnderstand=Capisco shared.na=N/A shared.shutDown=Spegni shared.reportBug=Report bug on GitHub shared.buyMonero=Acquista monero shared.sellMonero=Vendi monero shared.buyCurrency=Acquista {0} shared.sellCurrency=Vendi {0} shared.buyCurrency.locked=Acquista {0} 🔒 shared.sellCurrency.locked=Vendi {0} 🔒 shared.buyingXMRWith=acquistando XMR con {0} shared.sellingXMRFor=vendendo XMR per {0} shared.buyingCurrency=comprando {0} (vendendo XMR) shared.sellingCurrency=vendendo {0} (comprando XMR) shared.buy=compra shared.sell=vendi shared.buying=comprando shared.selling=vendendo shared.P2P=P2P shared.oneOffer=offerta shared.multipleOffers=offerte shared.Offer=Offerta shared.offerVolumeCode={0} Offer Volume shared.openOffers=offerte aperte shared.trade=scambio shared.trades=scambi shared.openTrades=scambi aperti shared.dateTime=Data/Ora shared.price=Prezzo shared.priceWithCur=Prezzo in {0} shared.priceInCurForCur=Prezzo in {0} per 1 {1} shared.fixedPriceInCurForCur=Prezzo fissato in {0} per 1 {1} shared.amount=Importo shared.txFee=Commissioni di Transazione shared.tradeFee=Trade Fee shared.buyerSecurityDeposit=Deposito Acquirente shared.sellerSecurityDeposit=Deposito Venditore shared.amountWithCur=Importo in {0} shared.volumeWithCur=Volume in {0} shared.currency=Valuta shared.market=Mercato shared.deviation=Deviation shared.paymentMethod=Metodo di pagamento shared.tradeCurrency=Valuta di scambio shared.offerType=Tipo di offerta shared.details=Dettagli shared.address=Indirizzo shared.balanceWithCur=Saldo in {0} shared.utxo=Unspent transaction output shared.txId=ID Transazione shared.confirmations=Conferme shared.revert=Storno Tx shared.select=Seleziona shared.usage=Utilizzo shared.state=Stato shared.tradeId=ID Scambio shared.offerId=ID Offerta shared.bankName=Nome Banca shared.acceptedBanks=Banche accettate shared.amountMinMax=Importo (min - max) shared.amountHelp=Se un'offerta ha un valore massimo e minimo definito, puoi scambiare qualsiasi valore compreso in questo range shared.remove=Rimuovi shared.goTo=Vai a {0} shared.XMRMinMax=XMR (min - max) shared.removeOffer=Rimuovi offerta shared.dontRemoveOffer=Non rimuovere offerta shared.editOffer=Modifica offerta shared.openLargeQRWindow=Open large QR code window shared.tradingAccount=Account di scambio shared.faq=Visit FAQ page shared.yesCancel=Si, annulla shared.nextStep=Passo successivo shared.selectTradingAccount=Seleziona conto di trading shared.fundFromSavingsWalletButton=Applica fondi dal portafoglio Haveno shared.fundFromExternalWalletButton=Apri il tuo portafoglio esterno per aggiungere fondi shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? shared.belowInPercent=Sotto % del prezzo di mercato shared.aboveInPercent=Sopra % del prezzo di mercato shared.enterPercentageValue=Immetti il valore % shared.OR=OPPURE shared.notEnoughFunds=You don't have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. shared.waitingForFunds=In attesa dei fondi... shared.TheXMRBuyer=L'acquirente di XMR shared.You=Tu shared.sendingConfirmation=Invio della conferma in corso... shared.sendingConfirmationAgain=Invia nuovamente la conferma shared.exportCSV=Export to CSV shared.exportJSON=Esporta in JSON shared.summary=Show summary shared.noDateAvailable=Nessuna data disponibile shared.noDetailsAvailable=Dettagli non disponibili shared.notUsedYet=Non ancora usato shared.date=Data shared.sendFundsDetailsWithFee=Invio: {0}\n\nAll'indirizzo di ricezione: {1}\n\nCommissione mineraria aggiuntiva: {2}\n\nSei sicuro di voler inviare questa somma? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Copia negli appunti shared.language=Lingua shared.country=Paese shared.applyAndShutDown=Applica e chiudi shared.selectPaymentMethod=Seleziona il metodo di pagamento shared.accountNameAlreadyUsed=That account name is already used for another saved account.\nPlease choose another name. shared.askConfirmDeleteAccount=Vuoi davvero cancellare l'account selezionato? shared.cannotDeleteAccount=You cannot delete that account because it is being used in an open offer (or in an open trade). shared.noAccountsSetupYet=Non ci sono ancora account impostati shared.manageAccounts=Gestisci gli account shared.addNewAccount=Aggiungi nuovo account shared.ExportAccounts=Esporta Account shared.importAccounts=Importa Account shared.createNewAccount=Crea nuovo account shared.createNewAccountDescription=I dettagli del tuo account sono memorizzati localmente sul tuo dispositivo e condivisi solo con il tuo partner commerciale e l'arbitro se viene aperta una disputa. shared.saveNewAccount=Salva nuovo account shared.selectedAccount=Account selezionato shared.deleteAccount=Elimina account shared.errorMessageInline=\nMessaggio di errore: {0} shared.errorMessage=Messaggio di errore shared.information=Informazione shared.name=Nome shared.id=ID shared.dashboard=Dashboard shared.accept=Accetta shared.balance=Saldo shared.save=Salva shared.onionAddress=Indirizzo onion shared.supportTicket=ticket di supporto shared.dispute=disputa shared.mediationCase=caso di mediazione shared.seller=venditore shared.buyer=acquirente shared.allEuroCountries=Tutti i paesi Euro shared.acceptedTakerCountries=Paesi accettati dall'acquirente shared.tradePrice=Prezzo di scambio shared.tradeAmount=Importo dello scambio shared.tradeVolume=Volume di scambio shared.invalidKey=La chiave inserita non è valida. shared.enterPrivKey=Inserisci la chiave privata per sbloccare shared.payoutTxId=ID transazione di pagamento shared.contractAsJson=Contratto in formato JSON shared.viewContractAsJson=Visualizza il contratto in formato JSON shared.contract.title=Contratto per lo scambio con ID: {0} shared.paymentDetails=Dettagli pagamento XMR {0}: shared.securityDeposit=Deposito di sicurezza shared.yourSecurityDeposit=Il tuo deposito di sicurezza shared.contract=Contratto shared.messageArrived=Messaggio arrivato. shared.messageStoredInMailbox=Messaggio salvato nella posta. shared.messageSendingFailed=Invio del messaggio fallito. Errore: {0} shared.unlock=Sblocca shared.toReceive=per ricevere shared.toSpend=da spendere shared.xmrAmount=Importo XMR shared.yourLanguage=Le tue lingue shared.addLanguage=Aggiungi lingua shared.total=Totale shared.totalsNeeded=Fondi richiesti shared.tradeWalletAddress=Indirizzo del portafoglio per gli scambi shared.tradeWalletBalance=Saldo del portafogli per gli scambi shared.reserveExactAmount=Riserva solo i fondi necessari. Richiede una tassa di mining e circa 20 minuti prima che la tua offerta diventi attiva. shared.makerTxFee=Maker: {0} shared.takerTxFee=Taker: {0} shared.iConfirm=Confermo shared.openURL=Aperti {0} shared.fiat=Fiat shared.crypto=Crypto shared.preciousMetals=Metalli Preziosi shared.all=Tutti shared.edit=Modifica shared.advancedOptions=Opzioni avanzate shared.interval=Intervallo shared.actions=Azioni shared.buyerUpperCase=Acquirente shared.sellerUpperCase=Venditore shared.new=NUOVO shared.learnMore=Leggi di più shared.dismiss=Chiudi shared.selectedArbitrator=Arbitro selezionato shared.selectedMediator=Mediatore selezionato shared.selectedRefundAgent=Arbitro selezionato shared.mediator=Mediatore shared.arbitrator=Arbitro shared.refundAgent=Arbitro shared.refundAgentForSupportStaff=Agente di rimborso shared.delayedPayoutTxId=Delayed payout transaction ID shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to shared.unconfirmedTransactionsLimitReached=Al momento, hai troppe transazioni non confermate. Per favore riprova più tardi. shared.numItemsLabel=Number of entries: {0} shared.filter=Filtro shared.enabled=Enabled #################################################################### # UI views #################################################################### #################################################################### # MainView #################################################################### mainView.menu.market=Mercato mainView.menu.buyXmr=Compra XMR mainView.menu.sellXmr=Vendi XMR mainView.menu.portfolio=Portafoglio mainView.menu.funds=Fondi mainView.menu.support=Supporto mainView.menu.settings=Impostazioni mainView.menu.account=Account mainView.marketPriceWithProvider.label=Prezzo di mercato per {0} mainView.marketPrice.havenoInternalPrice=Prezzo dell'ultimo scambio su Haveno mainView.marketPrice.tooltip.havenoInternalPrice=Non è disponibile alcun prezzo di mercato da fornitori terzi di feed dei prezzi.\nIl prezzo visualizzato è l'ultimo prezzo di scambio su Haveno per quella valuta. mainView.marketPrice.tooltip=Il prezzo di mercato é fornito da {0}{1}\nUltimo aggiornamento: {2}\nURL del nodo del provider: {3} mainView.balance.available=Saldo disponibile mainView.balance.reserved=Riservati nelle offerte mainView.balance.pending=Bloccati in scambi mainView.balance.reserved.short=Riservati mainView.balance.pending.short=Bloccati mainView.footer.usingTor=(via Tor) mainView.footer.localhostMoneroNode=(localhost) mainView.footer.clearnet=(via clearnet) mainView.footer.xmrInfo={0} {1} mainView.footer.xmrFeeRate=/ Fee rate: {0} sat/vB mainView.footer.xmrInfo.initializing=Connessione alla rete Haveno mainView.footer.xmrInfo.synchronizingWith=Sincronizzazione con {0} al blocco: {1} / {2} mainView.footer.xmrInfo.connectedTo=Connesso a {0} al blocco {1} mainView.footer.xmrInfo.synchronizingWalletWith=Sincronizzazione del portafoglio con {0} al blocco: {1} / {2} mainView.footer.xmrInfo.syncedWith=Sincronizzato con {0} al blocco {1} mainView.footer.xmrInfo.connectingTo=Connessione a mainView.footer.xmrInfo.connectionFailed=Connessione fallita mainView.footer.xmrPeers=Monero network peers: {0} mainView.footer.p2pPeers=Haveno network peers: {0} mainView.bootstrapState.connectionToTorNetwork=(1/4) Connessione alla rete Tor... mainView.bootstrapState.torNodeCreated=(2/4) Nodo Tor creato mainView.bootstrapState.hiddenServicePublished=(3/4) Servizio Nascosto pubbblicato mainView.bootstrapState.initialDataReceived=(4/4) Dati iniziali ricevuti mainView.bootstrapWarning.noSeedNodesAvailable=Nessun nodo seme disponibile mainView.bootstrapWarning.noNodesAvailable=Nessun nodo seme e peer disponibili mainView.bootstrapWarning.bootstrappingToP2PFailed=Il bootstrap sulla rete Haveno non è riuscito mainView.p2pNetworkWarnMsg.noNodesAvailable=Non ci sono nodi seed o peer persistenti disponibili per la richiesta di dati.\nControlla la tua connessione Internet o prova a riavviare l'applicazione. mainView.p2pNetworkWarnMsg.connectionToP2PFailed=Connessione alla rete Haveno non riuscita (errore segnalato: {0}).\nControlla la tua connessione Internet o prova a riavviare l'applicazione. mainView.walletServiceErrorMsg.timeout=Connessione alla rete Monero fallita a causa di un timeout. mainView.walletServiceErrorMsg.connectionError=Connessione alla rete Monero fallita a causa di un errore: {0} mainView.walletServiceErrorMsg.rejectedTxException=Una transazione è stata rifiutata dalla rete.\n\n{0} mainView.networkWarning.allConnectionsLost=Hai perso la connessione a tutti i {0} peer di rete.\nForse hai perso la connessione a Internet o il computer era in modalità standby. mainView.networkWarning.localhostMoneroLost=Hai perso la connessione al nodo Monero in localhost.\nRiavvia l'applicazione Haveno per connetterti ad altri nodi Monero o riavvia il nodo Monero in localhost. mainView.version.update=(Aggiornamento disponibile) #################################################################### # MarketView #################################################################### market.tabs.offerBook=Registro offerte market.tabs.spreadCurrency=Offers by Currency market.tabs.spreadPayment=Offers by Payment Method market.tabs.trades=Scambi # OfferBookChartView market.offerBook.buyCrypto=Compra {0} (vendi {1}) market.offerBook.sellCrypto=Vendi {0} (compra {1}) market.offerBook.buyWithTraditional=Acquista {0} market.offerBook.sellWithTraditional=Vendi {0} market.offerBook.sellOffersHeaderLabel=Vendi {0} a market.offerBook.buyOffersHeaderLabel=Compra {0} da market.offerBook.buy=Voglio comprare monero market.offerBook.sell=Voglio vendere monero # SpreadView market.spread.numberOfOffersColumn=Tutte le offerte ({0}) market.spread.numberOfBuyOffersColumn=Acquista XMR ({0}) market.spread.numberOfSellOffersColumn=Vendi XMR ({0}) market.spread.totalAmountColumn=Totale XMR ({0}) market.spread.spreadColumn=Spread market.spread.expanded=Expanded view # TradesChartsView market.trades.nrOfTrades=Scambi: {0} market.trades.tooltip.volumeBar=Volume: {0} / {1}\nNo. of trades: {2}\nDate: {3} market.trades.tooltip.candle.open=Aperti: market.trades.tooltip.candle.close=Chiusi: market.trades.tooltip.candle.high=Alto: market.trades.tooltip.candle.low=Basso: market.trades.tooltip.candle.average=Media: market.trades.tooltip.candle.median=Mediana: market.trades.tooltip.candle.date=Data: market.trades.showVolumeInUSD=Show volume in USD #################################################################### # OfferView #################################################################### offerbook.createOffer=Crea offerta offerbook.takeOffer=Accetta offerta offerbook.takeOfferToBuy=Accetta l'offerta per acquistare {0} offerbook.takeOfferToSell=Accetta l'offerta per vendere {0} offerbook.takeOffer.enterChallenge=Inserisci la passphrase dell'offerta offerbook.trader=Trader offerbook.offerersBankId=ID banca del Maker (BIC/SWIFT): {0} offerbook.offerersBankName=Nome della banca del Maker: {0} offerbook.offerersBankSeat=Sede del paese bancario del Maker: {0} offerbook.offerersAcceptedBankSeatsEuro=Sede accettata dei paesi della banca (acquirente): tutti i paesi dell'Euro offerbook.offerersAcceptedBankSeats=Sede accettata dei paesi bancari (acquirente):\n  {0} offerbook.availableOffers=Offerte disponibili offerbook.filterByCurrency=Filtra per valuta offerbook.filterByPaymentMethod=Filtra per metodo di pagamento offerbook.matchingOffers=Offerte che corrispondono ai miei account offerbook.filterNoDeposit=Nessun deposito offerbook.noDepositOffers=Offerte senza deposito (passphrase richiesta) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info=Questo account è stato verificato e {0} offerbook.timeSinceSigning.info.arbitrator=firmato da un arbitro e può firmare account peer offerbook.timeSinceSigning.info.peer=signed by a peer, waiting %d days for limits to be lifted offerbook.timeSinceSigning.info.peerLimitLifted=firmato da un peer e i limiti sono stati alzati offerbook.timeSinceSigning.info.signer=firmato da un peer e può firmare account peer (limiti alzati) offerbook.timeSinceSigning.info.banned= \nl'account è stato bannato offerbook.timeSinceSigning.daysSinceSigning={0} giorni offerbook.timeSinceSigning.daysSinceSigning.long={0} dalla firma offerbook.xmrAutoConf=Is auto-confirm enabled offerbook.buyXmrWith=Compra XMR con: offerbook.sellXmrFor=Vendi XMR per: offerbook.timeSinceSigning.help=Quando completi correttamente un'operazione con un peer che ha un account di pagamento firmato, il tuo account di pagamento viene firmato.\n{0} giorni dopo, il limite iniziale di {1} viene alzato e il tuo account può firmare account di pagamento di altri peer. offerbook.timeSinceSigning.notSigned=Non ancora firmato offerbook.timeSinceSigning.notSigned.ageDays={0} giorni offerbook.timeSinceSigning.notSigned.noNeed=N/A shared.notSigned=This account has not been signed yet and was created {0} days ago shared.notSigned.noNeed=This account type does not require signing shared.notSigned.noNeedDays=This account type does not require signing and was created {0} days ago shared.notSigned.noNeedAlts=Crypto accounts do not feature signing or aging offerbook.nrOffers=N. di offerte: {0} offerbook.volume={0} (min - max) offerbook.deposit=Deposit XMR (%) offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed. offerbook.createNewOffer=Crea offerta per {0} {1} offerbook.createOfferToBuy=Crea una nuova offerta per comprare {0} offerbook.createOfferToSell=Crea una nuova offerta per vendere {0} offerbook.createOfferToBuy.withTraditional=Crea una nuova offerta per acquistare {0} con {1} offerbook.createOfferToSell.forTraditional=Crea una nuova offerta per vendere {0} per {1} offerbook.createOfferToBuy.withCrypto=Crea una nuova offerta per vendere {0} (acquista {1}) offerbook.createOfferToSell.forCrypto=Crea una nuova offerta per acquistare {0} (vendi {1}) offerbook.takeOfferButton.tooltip=Accetta offera per {0} offerbook.yesCreateOffer=Sì, crea offerta offerbook.setupNewAccount=Imposta un nuovo account di scambio offerbook.removeOffer.success=L'offerta è stata rimossa con successo. offerbook.removeOffer.failed=Rimozione offerta fallita:\n{0} offerbook.deactivateOffer.failed=Disattivazione dell'offerta fallita:\n{0} offerbook.activateOffer.failed=Pubblicazione dell'offerta fallita:\n{0} offerbook.withdrawFundsHint=Puoi ritirare i fondi versati dalla schermata {0}. offerbook.warning.noTradingAccountForCurrency.headline=No payment account for selected currency offerbook.warning.noTradingAccountForCurrency.msg=You don't have a payment account set up for the selected currency.\n\nWould you like to create an offer for another currency instead? offerbook.warning.noMatchingAccount.headline=No matching payment account. offerbook.warning.noMatchingAccount.msg=This offer uses a payment method you haven't set up yet. \n\nWould you like to set up a new payment account now? offerbook.warning.counterpartyTradeRestrictions=Questa offerta non può essere accettata a causa di restrizioni di scambio della controparte offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\nAfter successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\nFor more information on account signing, please see the documentation at [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. popup.warning.tradeLimitDueAccountAgeRestriction.seller=L'importo di scambio consentito è limitato a {0} a causa delle restrizioni di sicurezza basate sui seguenti criteri:\n- L'account dell'acquirente non è stato firmato da un arbitro o da un pari\n- Il tempo trascorso dalla firma dell'account dell'acquirente non è di almeno 30 giorni\n- Il metodo di pagamento per questa offerta è considerato rischioso per le richieste di storno bancarie\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=L'importo di scambio consentito è limitato a {0} a causa delle restrizioni di sicurezza basate sui seguenti criteri:\n- Il tuo account non è stato firmato da un arbitro o da un pari\n- Il tempo trascorso dalla firma del tuo account non è di almeno 30 giorni\n- Il metodo di pagamento per questa offerta è considerato rischioso per le richieste di storno bancarie\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=Questo metodo di pagamento è temporaneamente limitato a {0} fino a {1} perché tutti gli acquirenti hanno nuovi account.\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=La tua offerta sarà limitata ai compratori con account firmati e datati perché supera {0}.\n\n{1} offerbook.warning.wrongTradeProtocol=Questa offerta richiede una versione di protocollo diversa da quella utilizzata nella versione del tuo software.\n\nVerifica di aver installato l'ultima versione, altrimenti l'utente che ha creato l'offerta ha utilizzato una versione precedente.\n\nGli utenti non possono effettuare scambi con una versione di protocollo di scambio incompatibile. offerbook.warning.userIgnored=Hai aggiunto l'indirizzo onion dell'utente al tuo elenco di persone da ignorare. offerbook.warning.offerBlocked=Tale offerta è stata bloccata dagli sviluppatori Haveno.\nProbabilmente c'è un bug non gestito che causa problemi quando si accetta tale offerta. offerbook.warning.currencyBanned=La valuta utilizzata in quell'offerta è stata bloccata dagli sviluppatori Haveno.\nPer ulteriori informazioni, visitare il forum di Haveno. offerbook.warning.paymentMethodBanned=Il metodo di pagamento utilizzato in quell'offerta è stato bloccato dagli sviluppatori Haveno.\nPer ulteriori informazioni, visitare il forum di Haveno. offerbook.warning.nodeBlocked=L'indirizzo onion di quel trader è stato bloccato dagli sviluppatori Haveno.\nProbabilmente c'è un bug non gestito che causa problemi quando si accettano offerte da quel trader. offerbook.warning.offerWasAlreadyUsedInTrade=You cannot take this offer because you already took it earlier. It could be that your previous take-offer attempt resulted in a failed trade. offerbook.info.sellAtMarketPrice=Venderai al prezzo di mercato (aggiornato ogni minuto). offerbook.info.buyAtMarketPrice=Acquisterai al prezzo di mercato (aggiornato ogni minuto). offerbook.info.sellBelowMarketPrice=Otterrai {0} in meno del prezzo di mercato corrente (aggiornato ogni minuto). offerbook.info.buyAboveMarketPrice=Pagherai {0} in più rispetto all'attuale prezzo di mercato (aggiornato ogni minuto). offerbook.info.sellAboveMarketPrice=Otterrai {0} in più rispetto all'attuale prezzo di mercato (aggiornato ogni minuto). offerbook.info.buyBelowMarketPrice=Pagherai {0} in meno del prezzo di mercato corrente (aggiornato ogni minuto). offerbook.info.buyAtFixedPrice=Comprerai a questo prezzo fisso. offerbook.info.sellAtFixedPrice=Venderai a questo prezzo fisso. offerbook.info.noArbitrationInUserLanguage=In caso di disputa, si ricorda che l'arbitrato per questa offerta verrà gestito in {0}. La lingua è attualmente impostata su {1}. offerbook.info.roundedFiatVolume=L'importo è stato arrotondato per aumentare la privacy del tuo scambio. #################################################################### # Offerbook / Create offer #################################################################### createOffer.amount.prompt=Inserisci quantità in XMR createOffer.price.prompt=Inserisci prezzo createOffer.volume.prompt=Inserisci importo in {0} createOffer.amountPriceBox.amountDescription=Quantità di XMR a {0} createOffer.amountPriceBox.buy.volumeDescription=Quantità in {0} da spendere createOffer.amountPriceBox.sell.volumeDescription=Quantità in {0} da ricevere createOffer.amountPriceBox.minAmountDescription=Quantità minima di XMR createOffer.securityDeposit.prompt=Deposito di sicurezza createOffer.fundsBox.title=Finanzia la tua offerta createOffer.fundsBox.offerFee=Commissione di scambio createOffer.fundsBox.networkFee=Commissione di mining createOffer.fundsBox.placeOfferSpinnerInfo=Pubblicazione dell'offerta in corso ... createOffer.fundsBox.paymentLabel=Scambio Haveno con ID {0} createOffer.fundsBox.fundsStructure=({0} deposito cauzionale, {1} commissione di scambio, {2} commissione di mining) createOffer.success.headline=La tua offerta è stata creata createOffer.success.info=Puoi gestire le tue offerte aperte su \"Portafoglio/Le mie offerte aperte\". createOffer.info.sellAtMarketPrice=Venderai sempre al prezzo di mercato poiché il prezzo della tua offerta verrà continuamente aggiornato. createOffer.info.buyAtMarketPrice=Acquisterai sempre al prezzo di mercato poiché il prezzo della tua offerta verrà costantemente aggiornato. createOffer.info.sellAboveMarketPrice=Otterrai sempre il {0}% in più rispetto al prezzo di mercato corrente poiché il prezzo della tua offerta verrà costantemente aggiornato. createOffer.info.buyBelowMarketPrice=Pagherai sempre il {0}% in meno rispetto al prezzo di mercato corrente poiché il prezzo della tua offerta verrà costantemente aggiornato. createOffer.warning.sellBelowMarketPrice=Otterrai sempre il {0}% in meno rispetto al prezzo di mercato corrente poiché il prezzo della tua offerta verrà costantemente aggiornato. createOffer.warning.buyAboveMarketPrice=Pagherai sempre il {0}% in più rispetto al prezzo di mercato corrente poiché il prezzo della tua offerta verrà costantemente aggiornato. createOffer.tradeFee.descriptionXMROnly=Commissione di scambio createOffer.tradeFee.descriptionBSQEnabled=Seleziona la valuta della commissione di scambio createOffer.triggerPrice.prompt=Set optional trigger price createOffer.triggerPrice.label=Deactivate offer if market price is {0} createOffer.triggerPrice.tooltip=As protection against drastic price movements you can set a trigger price which deactivates the offer if the market price reaches that value. createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0} createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0} # new entries createOffer.placeOfferButton.buy=Revisiona: Crea offerta per acquistare XMR con {0} createOffer.placeOfferButton.sell=Revisiona: Crea offerta per vendere XMR in cambio di {0} createOffer.createOfferFundWalletInfo.headline=Finanzia la tua offerta # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Importo di scambio: {0} \n createOffer.createOfferFundWalletInfo.msg=Devi depositare {0} per questa offerta.\n\n\ Questi fondi sono riservati nel tuo portafoglio locale e verranno bloccati in un portafoglio multisig una volta che qualcuno accetta la tua offerta.\n\n\ L'importo è la somma di:\n\ {1}\ - Il tuo deposito di sicurezza: {2}\n\ - Tassa di trading: {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=Si è verificato un errore durante l'immissione dell'offerta:\n\n{0}\n\nNon sono ancora usciti fondi dal tuo portafoglio.\nRiavvia l'applicazione e controlla la connessione di rete. createOffer.setAmountPrice=Imposta importo e prezzo createOffer.warnCancelOffer=Hai già finanziato quell'offerta.\nSe annulli adesso, i tuoi fondi rimarranno nel tuo portafoglio Haveno locale e saranno disponibili per il prelievo nella schermata "Fondi/Invia fondi". Sei sicuro di voler annullare? createOffer.timeoutAtPublishing=Si è verificato un timeout durante la pubblicazione dell'offerta. createOffer.errorInfo=\n\nLa maker fee è già pagata. Nel peggiore dei casi hai perso quella commissione.\nProva a riavviare l'applicazione e controlla la connessione di rete per vedere se riesci a risolvere il problema. createOffer.tooLowSecDeposit.warning=Il deposito cauzionale è stato impostato su un valore inferiore rispetto al valore predefinito consigliato di {0}.\nSei sicuro di voler utilizzare un deposito di sicurezza inferiore? createOffer.tooLowSecDeposit.makerIsSeller=Ti garantisce una minore protezione nel caso in cui il peer di scambio non segua il protocollo di negoziazione. createOffer.tooLowSecDeposit.makerIsBuyer=Offre una protezione minore per al peer di trading che con cui commerci poiché hai meno depositi a rischio. Altri utenti potrebbero preferire altre offerte anziché le tue. createOffer.resetToDefault=No, ripristina il valore predefinito createOffer.useLowerValue=Sì, usa il mio valore più basso createOffer.priceOutSideOfDeviation=Il prezzo che hai inserito è al di fuori del massimo consentito dalla deviazione dal prezzo di mercato.\nIl massimo consentito per la deviazione è {0} e può essere regolato nelle preferenze. createOffer.changePrice=Cambia prezzo createOffer.tac=Con la pubblicazione di questa offerta, accetto di negoziare con qualsiasi operatore che soddisfi le condizioni definite in questa schermata. createOffer.currencyForFee=Commissione di scambio createOffer.setDeposit=Imposta il deposito cauzionale dell'acquirente (%) createOffer.setDepositAsBuyer=Imposta il mio deposito cauzionale come acquirente (%) createOffer.setDepositForBothTraders=Set both traders' security deposit (%) createOffer.securityDepositInfo=Il deposito cauzionale dell'acquirente sarà {0} createOffer.securityDepositInfoAsBuyer=Il tuo deposito cauzionale come acquirente sarà {0} createOffer.minSecurityDepositUsed=Il deposito di sicurezza minimo è utilizzato createOffer.buyerAsTakerWithoutDeposit=Nessun deposito richiesto dal compratore (protetto da passphrase) createOffer.myDeposit=Il mio deposito di sicurezza (%) createOffer.myDepositInfo=Il tuo deposito di sicurezza sarà {0} #################################################################### # Offerbook / Take offer #################################################################### takeOffer.amount.prompt=Inserisci importo in XMR takeOffer.amountPriceBox.buy.amountDescription=Importo di XMR da vendere takeOffer.amountPriceBox.sell.amountDescription=Importo di XMR da acquistare takeOffer.amountPriceBox.priceDescription=Prezzo per monero in {0} takeOffer.amountPriceBox.amountRangeDescription=Range di importo possibile takeOffer.amountPriceBox.warning.invalidXmrDecimalPlaces=L'importo che hai inserito supera il numero di decimali permessi.\nL'importo è stato regolato a 4 decimali. takeOffer.validation.amountSmallerThanMinAmount=L'importo non può essere più piccolo dell'importo minimo definito nell'offerta. takeOffer.validation.amountLargerThanOfferAmount=L'importo inserito non può essere più alto dell'importo definito nell'offerta. takeOffer.validation.amountLargerThanOfferAmountMinusFee=Questo importo inserito andrà a creare un resto di basso valore per il venditore di XMR. takeOffer.fundsBox.title=Finanzia il tuo scambio takeOffer.fundsBox.isOfferAvailable=Controlla se l'offerta è disponibile ... takeOffer.fundsBox.tradeAmount=Importo da vendere takeOffer.fundsBox.offerFee=Commissione di scambio takeOffer.fundsBox.networkFee=Totale commissioni di mining takeOffer.fundsBox.takeOfferSpinnerInfo=Accettare l'offerta: {0} takeOffer.fundsBox.paymentLabel=Scambia Haveno con ID {0} takeOffer.fundsBox.fundsStructure=({0} deposito cauzionale, {1} commissione commerciale, {2} commissione mineraria) takeOffer.fundsBox.noFundingRequiredTitle=Nessun finanziamento richiesto takeOffer.fundsBox.noFundingRequiredDescription=Ottieni la passphrase dell'offerta dal venditore fuori da Haveno per accettare questa offerta. takeOffer.success.headline=Hai accettato con successo un'offerta. takeOffer.success.info=Puoi vedere lo stato del tuo scambio su \"Portafoglio/Scambi aperti\". takeOffer.error.message=Si è verificato un errore durante l'accettazione dell'offerta.\n\n{0} # new entries takeOffer.takeOfferButton.buy=Revisiona: Accetta offerta per acquistare XMR con {0} takeOffer.takeOfferButton.sell=Revisiona: Accetta offerta per vendere XMR in cambio di {0} takeOffer.noPriceFeedAvailable=Non puoi accettare questa offerta poiché utilizza un prezzo in percentuale basato sul prezzo di mercato ma non è disponibile alcun feed di prezzi. takeOffer.takeOfferFundWalletInfo.headline=Finanzia il tuo scambio # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Importo di scambio: {0} \n takeOffer.takeOfferFundWalletInfo.msg=Devi depositare {0} per accettare questa offerta.\n\nL'importo è la somma di:\n{1}- Il tuo deposito di sicurezza: {2}\n- Commissione di trading: {3} takeOffer.alreadyPaidInFunds=Se hai già pagato in fondi puoi effettuare il ritiro nella schermata \"Fondi/Invia fondi\". takeOffer.paymentInfo=Informazioni sul pagamento takeOffer.setAmountPrice=Importo stabilito takeOffer.alreadyFunded.askCancel=Hai già finanziato quell'offerta.\nSe annulli adesso, i tuoi fondi rimarranno nel tuo portafoglio Haveno locale e saranno disponibili per il prelievo nella schermata "Fondi/Invia fondi". Sei sicuro di voler annullare? takeOffer.failed.offerNotAvailable=Accettazione dell'offerta non riuscita perché l'offerta non è più disponibile. Nel frattempo, un altro trader potrebbe aver già accettato l'offerta. takeOffer.failed.offerTaken=Non puoi accettare questa offerta perché l'offerta è già stata presa da un altro trader. takeOffer.failed.offerRemoved=Non puoi accettare quell'offerta perché nel frattempo l'offerta è stata rimossa. takeOffer.failed.offererNotOnline=Richiesta di accettazione dell'ooferta non riuscita perché il maker non è più online. takeOffer.failed.offererOffline=Non puoi accettare l'offerta poiché chi l'ha formulata è offline. takeOffer.warning.connectionToPeerLost=Hai perso la connessione con il maker.\nPotrebbe essersi scollegato o aver chiuso la connessione verso di te a causa di troppe connessioni aperte.\nSe riesci ancora a vedere l'offerta nel registro offerte, puoi provare a riprenderla. takeOffer.error.noFundsLost=\n\nNon è ancora uscito alcun fondo dal tuo portafoglio.\nProva a riavviare l'applicazione e controlla la connessione di rete per vedere se riesci a risolvere il problema. # suppress inspection "TrailingSpacesInProperty" takeOffer.error.feePaid=\n\n takeOffer.error.depositPublished=\n\nLa transazione di deposito è già stata pubblicata.\nProva a riavviare l'applicazione e verifica la connessione di rete per cercare di risolvere il problema.\nSe il problema persiste, contatta gli sviluppatori per ricevere supporto. takeOffer.error.payoutPublished=\n\nLa transazione di pagamento è già stata pubblicata.\nProva a riavviare l'applicazione e verifica la connessione di rete per cercare di risolvere il problema.\nSe il problema persiste, contatta gli sviluppatori per ricevere supporto. takeOffer.tac=Accettando questa offerta, accetto le condizioni commerciali definite in questa schermata. #################################################################### # Offerbook / Edit offer #################################################################### openOffer.header.triggerPrice=Prezzo di attivazione openOffer.triggerPrice=Trigger price {0} openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\nPlease edit the offer to define a new trigger price editOffer.setPrice=Imposta prezzo editOffer.confirmEdit=Conferma: modifica offerta editOffer.publishOffer=Pubblica la tua offerta. editOffer.failed=Modifica dell'offerta fallita:\n{0} editOffer.success=La tua offerta è stata modificata con successo. editOffer.invalidDeposit=Il deposito di sicurezza dell'acquirente non rientra nei vincoli definiti dalla DAO di Haveno e non può più essere modificato. #################################################################### # Portfolio #################################################################### portfolio.tab.openOffers=Le mie offerte aperte portfolio.tab.pendingTrades=Scambi aperti portfolio.tab.history=Storia portfolio.tab.failed=Fallita portfolio.tab.editOpenOffer=Modifica offerta portfolio.closedTrades.deviation.help=Percentage price deviation from market portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} portfolio.pending.syncing=Sincronizzazione portafoglio di trading portfolio.pending.syncing.blockRemaining=Sincronizzazione portafoglio di trading — 1 blocco rimasto portfolio.pending.syncing.blocksRemaining=Sincronizzazione portafoglio di trading — {0} blocchi rimasti portfolio.pending.step1.waitForConf=Attendi la conferma della blockchain portfolio.pending.step2_buyer.additionalConf=I depositi hanno raggiunto 10 conferme.\nPer maggiore sicurezza, consigliamo di attendere {0} conferme prima di inviare il pagamento.\nProcedi in anticipo a tuo rischio. portfolio.pending.step2_buyer.startPayment=Inizia il pagamento portfolio.pending.step2_seller.waitPaymentSent=Attendi fino all'avvio del pagamento portfolio.pending.step3_buyer.waitPaymentArrived=Attendi fino all'arrivo del pagamento portfolio.pending.step3_seller.confirmPaymentReceived=Conferma la ricezione del pagamento portfolio.pending.step5.completed=Completato portfolio.pending.step3_seller.autoConf.status.label=Auto-confirm status portfolio.pending.autoConf=Auto-confirmed portfolio.pending.autoConf.blocks=XMR confirmations: {0} / Required: {1} portfolio.pending.autoConf.state.xmr.txKeyReused=Transaction key re-used. Please open a dispute. portfolio.pending.autoConf.state.confirmations=XMR confirmations: {0}/{1} portfolio.pending.autoConf.state.txNotFound=Transaction not seen in mem-pool yet portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=No valid transaction ID / transaction key portfolio.pending.autoConf.state.filterDisabledFeature=Disabled by developers. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FEATURE_DISABLED=Auto-confirm feature is disabled. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=Trade amount exceeds auto-confirm amount limit # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.INVALID_DATA=Peer provided invalid data. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=Payout transaction was already published. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.DISPUTE_OPENED=Dispute was opened. Auto-confirm is deactivated for that trade. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.REQUESTS_STARTED=Transaction proof requests started # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PENDING=Success results: {0}/{1}; {2} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.COMPLETED=Proof at all services succeeded # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. No auto-confirm possible. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. portfolio.pending.step1.info.you=La transazione del deposito è stata pubblicata.\nPuoi avviare il pagamento dopo 10 conferme (circa {0} minuti rimanenti). portfolio.pending.step1.info.buyer=La transazione del deposito è stata pubblicata.\nL'acquirente XMR può avviare il pagamento dopo 10 conferme (circa {0} minuti rimanenti). portfolio.pending.step1.warn=La transazione di deposito non è ancora confermata. Questo accade raramente e nel caso in cui la commissione di transazione di un trader proveniente da un portafoglio esterno è troppo bassa. portfolio.pending.step1.openForDispute=La transazione di deposito non è ancora confermata. Puoi attendere più a lungo o contattare il mediatore per ricevere assistenza. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2.confReached=Your trade has reached at least one blockchain confirmation.\n\n portfolio.pending.step2_buyer.refTextWarn=Important: when making the payment, leave the \"reason for payment\" field empty. DO NOT put the trade ID or any other text like 'monero', 'XMR', or 'Haveno'. You are free to discuss via trader chat if an alternate \"reason for payment\" would be suitable to you both. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees=If your bank charges you any fees to make the transfer, you are responsible for paying those fees. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.crypto=Trasferisci dal tuo portafoglio esterno {0}\n{1} al venditore XMR.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.cash=Vai in banca e paga {0} al venditore XMR.\n\n portfolio.pending.step2_buyer.cash.extra=REQUISITI IMPORTANTI:\nDopo aver effettuato il pagamento scrivi sulla ricevuta cartacea: NESSUN RIMBORSO.\nQuindi strappalo in 2 parti, fai una foto e inviala all'indirizzo e-mail del venditore XMR. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.moneyGram=Si prega di pagare {0} al venditore XMR utilizzando MoneyGram.\n\n portfolio.pending.step2_buyer.moneyGram.extra=REQUISITO IMPORTANTE:\nDopo aver effettuato il pagamento, invia il numero di autorizzazione e una foto della ricevuta via e-mail al venditore XMR.\nLa ricevuta deve mostrare chiaramente il nome completo, il paese, lo stato e l'importo del venditore. L'email del venditore è: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.westernUnion=Si prega di pagare {0} al venditore XMR utilizzando Western Union.\n\n portfolio.pending.step2_buyer.westernUnion.extra=REQUISITO IMPORTANTE:\nDopo aver effettuato il pagamento, invia l'MTCN (numero di tracciamento) e una foto della ricevuta via e-mail al venditore XMR.\nLa ricevuta deve mostrare chiaramente il nome completo, la città, il paese e l'importo del venditore. L'email del venditore è: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Invia {0} tramite \"Vaglia Postale Statunitense\" al venditore XMR.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Contatta il venditore XMR tramite il contatto fornito e organizza un incontro per pagare {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Inizia il pagamento utilizzando {0} portfolio.pending.step2_buyer.recipientsAccountData=Recipients {0} portfolio.pending.step2_buyer.amountToTransfer=Importo da trasferire portfolio.pending.step2_buyer.sellersAddress=Indirizzo {0} del venditore portfolio.pending.step2_buyer.buyerAccount=Il tuo conto di pagamento da utilizzare portfolio.pending.step2_buyer.paymentSent=Il pagamento è iniziato portfolio.pending.step2_buyer.showEarly=Mostra i dettagli di pagamento in anticipo portfolio.pending.step2_buyer.warn=Non hai ancora effettuato il tuo pagamento {0}!\nSi prega di notare che lo scambio è stato completato da {1}. portfolio.pending.step2_buyer.openForDispute=Non hai completato il pagamento!\nÈ trascorso il massimo periodo di scambio. Si prega di contattare il mediatore per assistenza. portfolio.pending.step2_buyer.paperReceipt.headline=Hai inviato la ricevuta cartacea al venditore XMR? portfolio.pending.step2_buyer.paperReceipt.msg=Ricorda:\nDevi scrivere sulla ricevuta cartacea: NESSUN RIMBORSO.\nQuindi strappala in 2 parti, fai una foto e inviala all'indirizzo e-mail del venditore XMR. portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Invia numero di autorizzazione e ricevuta portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=È necessario inviare il numero di Autorizzazione e una foto della ricevuta via e-mail al venditore XMR.\nLa ricevuta deve indicare chiaramente il nome completo, il paese, lo stato e l'importo del venditore. L'email del venditore è: {0}.\n\nHai inviato il numero di Autorizzazione e il contratto al venditore? portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=Invia MTCN e ricevuta portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Devi inviare l'MTCN (numero di tracciamento) e una foto della ricevuta via e-mail al venditore XMR.\nLa ricevuta deve indicare chiaramente il nome completo, la città, il paese e l'importo del venditore. L'email del venditore è: {0}.\n\nHai inviato l'MTCN e la ricevuta al venditore? portfolio.pending.step2_buyer.halCashInfo.headline=Invia il codice HalCash portfolio.pending.step2_buyer.halCashInfo.msg=È necessario inviare un messaggio di testo con il codice HalCash e l'ID dello scambio ({0}) al venditore XMR.\nIl numero di cellulare del venditore è {1}.\n\nHai inviato il codice al venditore? portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Alcune banche potrebbero richiedere il nome del destinatario. Gli account di Pagamento Veloci creati dai vecchi client Haveno non forniscono il nome del destinatario, quindi (se necessario) utilizza la chat dell scambio per fartelo comunicare. portfolio.pending.step2_buyer.confirmStart.headline=Conferma di aver avviato il pagamento portfolio.pending.step2_buyer.confirmStart.msg=Hai avviato il pagamento {0} al tuo partner commerciale? portfolio.pending.step2_buyer.confirmStart.yes=Sì, ho avviato il pagamento portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=You have not provided proof of payment portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=You have not entered the transaction ID and the transaction key.\n\nBy not providing this data the peer cannot use the auto-confirm feature to release the XMR as soon the XMR has been received.\nBeside that, Haveno requires that the sender of the XMR transaction is able to provide this information to the mediator or arbitrator in case of a dispute.\nSee more details on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=Input is not a 32 byte hexadecimal value portfolio.pending.step2_buyer.confirmStart.warningButton=Ignore and continue anyway portfolio.pending.step2_seller.waitPayment.headline=In attesa del pagamento portfolio.pending.step2_seller.f2fInfo.headline=Informazioni di contatto dell'acquirente portfolio.pending.step2_seller.waitPayment.msg=La transazione di deposito necessita di almeno una conferma blockchain.\nDevi attendere fino a quando l'acquirente XMR invia il pagamento {0}. portfolio.pending.step2_seller.warn=L'acquirente XMR non ha ancora effettuato il pagamento {0}.\nDevi aspettare fino a quando non invia il pagamento.\nSe lo scambio non sarà completato il {1}, l'arbitro comincierà ad indagare. portfolio.pending.step2_seller.openForDispute=L'acquirente XMR non ha ancora inviato il pagamento!\nIl periodo massimo consentito per lo scambio è trascorso.\nPuoi aspettare più a lungo e dare più tempo al partner di scambio oppure puoi contattare il mediatore per ricevere assistenza. tradeChat.chatWindowTitle=Finestra di chat per scambi con ID ' {0} ' tradeChat.openChat=Apri la finestra di chat tradeChat.rules=Puoi comunicare con il tuo peer di trading per risolvere potenziali problemi con questo scambio.\nNon è obbligatorio rispondere nella chat.\nSe un trader viola una delle seguenti regole, apri una controversia ed effettua una segnalazione al mediatore o all'arbitro.\n\nRegole della chat:\n● Non inviare nessun link (rischio di malware). È possibile inviare l'ID transazione e il nome di un block explorer.\n● Non inviare parole del seed, chiavi private, password o altre informazioni sensibili!\n● Non incoraggiare il trading al di fuori di Haveno (non garantisce nessuna sicurezza).\n● Non intraprendere alcuna forma di tentativo di frode di ingegneria sociale.\n● Se un peer non risponde e preferisce non comunicare tramite chat, rispettane la decisione.\n● Limita l'ambito della conversazione allo scambio. Questa chat non è una sostituzione di messenger o un troll-box.\n● Mantieni la conversazione amichevole e rispettosa.\n  # suppress inspection "UnusedProperty" message.state.UNDEFINED=Non definito # suppress inspection "UnusedProperty" message.state.SENT=Messaggio inviato # suppress inspection "UnusedProperty" message.state.ARRIVED=Il messaggio è arrivato al peer # suppress inspection "UnusedProperty" message.state.STORED_IN_MAILBOX=Messaggio di pagamento inviato ma non ancora ricevuto dal peer # suppress inspection "UnusedProperty" message.state.ACKNOWLEDGED=Il peer ha confermato la ricezione de messaggio # suppress inspection "UnusedProperty" message.state.FAILED=Invio del messaggio fallito portfolio.pending.step3_buyer.wait.headline=Attendi la conferma del pagamento del venditore XMR portfolio.pending.step3_buyer.wait.info=In attesa della conferma del venditore XMR per la ricezione del pagamento {0}. portfolio.pending.step3_buyer.wait.msgStateInfo.label=Stato del messaggio di pagamento avviato portfolio.pending.step3_buyer.warn.part1a=sulla {0} blockchain portfolio.pending.step3_buyer.warn.part1b=presso il tuo fornitore di servizi di pagamento (ad es. banca) portfolio.pending.step3_buyer.warn.part2=Il venditore XMR non ha ancora confermato il pagamento. Controlla {0} se l'invio del pagamento è andato a buon fine. portfolio.pending.step3_buyer.openForDispute=Il venditore XMR non ha confermato il tuo pagamento! Il max. periodo per lo scambio è trascorso. Puoi aspettare più a lungo e dare più tempo al peer di trading o richiedere assistenza al mediatore. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.part=Il tuo partner commerciale ha confermato di aver avviato il pagamento {0}.\n\n portfolio.pending.step3_seller.crypto.explorer=sul tuo {0} blockchain explorer preferito portfolio.pending.step3_seller.crypto.wallet=sul tuo portafoglio {0} portfolio.pending.step3_seller.crypto={0}Controlla {1} se la transazione è indirizzata correttamente al tuo indirizzo di ricezione\n{2}\nha già sufficienti conferme sulla blockchain.\nL'importo del pagamento deve essere {3}\n\nPuoi copiare e incollare il tuo indirizzo {4} dalla schermata principale dopo aver chiuso questo popup. portfolio.pending.step3_seller.postal={0}Please check if you have received {1} with \"US Postal Money Order\" from the XMR buyer. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.payByMail={0}Please check if you have received {1} with \"Pay by Mail\" from the XMR buyer. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.bank=Your trading partner has confirmed that they have initiated the {0} payment.\n\nPlease go to your online banking web page and check if you have received {1} from the XMR buyer. portfolio.pending.step3_seller.cash=Poiché il pagamento viene effettuato tramite deposito in contanti, l'acquirente XMR deve scrivere \"NESSUN RIMBORSO\" sulla ricevuta cartacea, strapparlo in 2 parti e inviarti una foto via e-mail.\n\nPer evitare il rischio di storno, conferma solamente se hai ricevuto l'e-mail e se sei sicuro che la ricevuta cartacea sia valida.\nSe non sei sicuro, {0} portfolio.pending.step3_seller.moneyGram=L'acquirente deve inviarti il numero di autorizzazione e una foto della ricevuta via e-mail.\nLa ricevuta deve mostrare chiaramente il tuo nome completo, il paese, lo stato e l'importo. Controlla nella tua e-mail se hai ricevuto il numero di autorizzazione.\n\nDopo aver chiuso il popup, vedrai il nome e l'indirizzo dell'acquirente XMR per effettuare il ritiro dell'importo da MoneyGram.\n\nConferma la ricevuta solo dopo aver ricevuto con successo i soldi! portfolio.pending.step3_seller.westernUnion=L'acquirente deve inviarti l'MTCN (numero di tracciamento) e una foto della ricevuta via e-mail.\nLa ricevuta deve mostrare chiaramente il tuo nome completo, la città, il paese e l'importo. Controlla nella tua e-mail se hai ricevuto l'MTCN.\n\nDopo aver chiuso il popup, vedrai il nome e l'indirizzo dell'acquirente XMR per effettuare il ritiro dell'importo da Western Union.\n\nConferma la ricevuta solo dopo aver ricevuto con successo i soldi! portfolio.pending.step3_seller.halCash=L'acquirente deve inviarti il codice HalCash come messaggio di testo. Riceverai un secondo un messaggio da HalCash con le informazioni richieste per poter ritirare gli EUR da un bancomat supportato da HalCash.\n\nDopo aver ritirato i soldi dal bancomat, conferma qui la ricevuta del pagamento! portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon eGift Card by email or by text message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted confirm the payment receipt. portfolio.pending.step3_seller.bankCheck=\n\nVerifica inoltre che il nome del mittente specificato nel contratto dello scambio corrisponda al nome che appare sul tuo estratto conto bancario:\nNome del mittente, per contratto di scambio: {0}\n\nSe i nomi non sono esattamente gli stessi, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=non confermare la ricevuta del pagamento. Apri una disputa premendo \"alt + o\" oppure \"option + o\".\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=Conferma pagamento ricevuto portfolio.pending.step3_seller.amountToReceive=Importo da ricevere portfolio.pending.step3_seller.yourAddress=Il tuo indirizzo {0} portfolio.pending.step3_seller.buyersAddress=Indirizzo dell'acquirente {0} portfolio.pending.step3_seller.yourAccount=Il tuo conto di trading portfolio.pending.step3_seller.xmrTxHash=ID Transazione portfolio.pending.step3_seller.xmrTxKey=Transaction key portfolio.pending.step3_seller.buyersAccount=Buyers account data portfolio.pending.step3_seller.confirmReceipt=Conferma pagamento ricevuto portfolio.pending.step3_seller.buyerStartedPayment=L'acquirente XMR ha avviato il pagamento {0}.\n{1} portfolio.pending.step3_seller.buyerStartedPayment.crypto=Controlla le conferme blockchain sul tuo portafoglio crypto o block explorer e conferma il pagamento quando hai sufficienti conferme blockchain portfolio.pending.step3_seller.buyerStartedPayment.traditional=Controlla sul tuo conto di trading (ad es. Conto bancario) e conferma quando hai ricevuto il pagamento. portfolio.pending.step3_seller.warn.part1a=sulla {0} blockchain portfolio.pending.step3_seller.warn.part1b=presso il tuo fornitore di servizi di pagamento (ad es. banca) portfolio.pending.step3_seller.warn.part2=Non hai ancora confermato la ricevuta del pagamento. Controlla {0} se hai ricevuto il pagamento. portfolio.pending.step3_seller.openForDispute=Non hai confermato la ricevuta del pagamento!\nIl max. periodo per lo scambio è trascorso.\nConferma o richiedi assistenza al mediatore. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=Hai ricevuto il pagamento {0} dal tuo partner commerciale?\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.name=Verifica inoltre che il nome del mittente specificato nel contratto di scambio corrisponda al nome che appare sul tuo estratto conto bancario:\nNome del mittente, per contratto di scambio: {0}\n\nSe i nomi non sono uguali, non confermare la ricevuta del pagamento. Apri invece una disputa premendo \"alt + o\" oppure \"option + o\".\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Tieni presente che non appena avrai confermato la ricevuta, l'importo commerciale bloccato verrà rilasciato all'acquirente XMR e il deposito cauzionale verrà rimborsato. portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Conferma di aver ricevuto il pagamento portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=Si, ho ricevuto il pagamento portfolio.pending.step3_seller.onPaymentReceived.signer=IMPORTANTE: confermando la ricezione del pagamento, stai anche verificando il conto della controparte e, di conseguenza, lo stai firmando. Poiché il conto della controparte non è stato ancora firmato, è necessario ritardare la conferma del pagamento il più a lungo possibile per ridurre il rischio di uno storno di addebito. portfolio.pending.step5_buyer.groupTitle=Riepilogo degli scambi completati portfolio.pending.step5_buyer.tradeFee=Commissione di scambio portfolio.pending.step5_buyer.makersMiningFee=Commissione di mining portfolio.pending.step5_buyer.takersMiningFee=Totale commissioni di mining portfolio.pending.step5_buyer.refunded=Deposito di sicurezza rimborsato portfolio.pending.step5_buyer.withdrawXMR=Preleva i tuoi monero portfolio.pending.step5_buyer.amount=Importo da prelevare portfolio.pending.step5_buyer.withdrawToAddress=Ritirare all'indirizzo portfolio.pending.step5_buyer.moveToHavenoWallet=Keep funds in Haveno wallet portfolio.pending.step5_buyer.withdrawExternal=Ritira verso un portafoglio esterno portfolio.pending.step5_buyer.alreadyWithdrawn=I tuoi fondi sono già stati ritirati.\nSi prega di controllare la cronologia delle transazioni. portfolio.pending.step5_buyer.confirmWithdrawal=Conferma richiesta di prelievo portfolio.pending.step5_buyer.amountTooLow=L'importo da trasferire è inferiore alla commissione di transazione e al min. valore tx possibile (polvere). portfolio.pending.step5_buyer.withdrawalCompleted.headline=Prelievo completato portfolio.pending.step5_buyer.withdrawalCompleted.msg=Gli scambi completati vengono archiviati in \"Portafoglio/Storia\".\nPuoi rivedere tutte le tue transazioni monero in \"Fondi/Transazioni\" portfolio.pending.step5_buyer.bought=Hai acquistato portfolio.pending.step5_buyer.paid=Hai pagato portfolio.pending.step5_seller.sold=Hai venduto portfolio.pending.step5_seller.received=Hai ricevuto tradeFeedbackWindow.title=Congratulazioni per aver concluso il tuo scambio tradeFeedbackWindow.msg.part1=Ci piacerebbe avere notizie sulla tua esperienza. Ci aiuterà a migliorare il software e a correggere eventuali errori. Se desideri fornire un feedback, compila questo breve sondaggio (non è richiesta la registrazione) all'indirizzo: tradeFeedbackWindow.msg.part2=In caso di domande o problemi, si prega di mettersi in contatto con altri utenti e collaboratori tramite il forum Haveno all'indirizzo: tradeFeedbackWindow.msg.part3=Grazie per aver usato Haveno! portfolio.pending.role=Il mio ruolo portfolio.pending.tradeInformation=Informazioni sullo scambio portfolio.pending.remainingTime=Tempo rimanente portfolio.pending.remainingTimeDetail={0} (fino a {1}) portfolio.pending.remainingTimeDetail.startsAfter=Inizia dopo {0} conferme portfolio.pending.tradePeriodInfo=Dopo {0} conferme, inizia il periodo di scambio. In base al metodo di pagamento utilizzato, viene applicato un periodo massimo di scambio diverso. portfolio.pending.tradePeriodWarning=Se il periodo viene superato, entrambi i trader possono aprire una disputa. portfolio.pending.tradeNotCompleted=Scambio non completato in tempo (fino a {0}) portfolio.pending.tradeProcess=Processo dello scambio portfolio.pending.openAgainDispute.msg=If you are not sure that the message to the mediator or arbitrator arrived (e.g. if you did not get a response after 1 day) feel free to open a dispute again with Cmd/Ctrl+o. You can also ask for additional help on the Haveno forum at [HYPERLINK:https://haveno.community]. portfolio.pending.openAgainDispute.button=Apri nuovamente la disputa portfolio.pending.openSupportTicket.headline=Apri ticket di supporto portfolio.pending.openSupportTicket.msg=Utilizza questa funzione solo in caso di emergenza se non viene visualizzato il pulsante \"Apri supporto\" o \"Apri disputa\".\n\nQuando apri un ticket di supporto, lo scambio verrà interrotto e gestito da un mediatore o da un arbitro. portfolio.pending.timeLockNotOver=Devi aspettare fino a ≈ {0} ({1} più blocchi) prima di poter aprire una controversia arbitrale. portfolio.pending.error.depositTxNull=La transazione di deposito è nulla. Non è possibile aprire una disputa senza una transazione di deposito valida. Vai su \"Impostazioni/Informazioni di rete\" ed esegui una risincronizzazione SPV.\n\nPer ulteriore assistenza, contatta il canale di supporto Haveno nel team di Haveno Keybase. portfolio.pending.mediationResult.error.depositTxNull=The deposit transaction is null. You can move the trade to failed trades. portfolio.pending.mediationResult.error.delayedPayoutTxNull=The delayed payout transaction is null. You can move the trade to failed trades. portfolio.pending.error.depositTxNotConfirmed=La transazione di deposito non è confermata. Non è possibile aprire una disputa arbitrale con una transazione di deposito non confermata. Attendi fino alla conferma o vai su \"Impostazioni/Informazioni di rete\" ed esegui una risincronizzazione SPV.\n\nPer ulteriore assistenza, contatta il canale di supporto Haveno nel team di Haveno Keybase. portfolio.pending.support.headline.getHelp=Ho bisogno di aiuto? portfolio.pending.support.text.getHelp=In caso di problemi, puoi provare a contattare il peer di scambio tramite la chat oppure puoi chiedere aiuto alla comunità Haveno all'indirizzo https://haveno.community. Se il problema persiste, puoi richiedere ulteriore aiuto ad un mediatore. portfolio.pending.support.button.getHelp=Apri la chat dello scambio portfolio.pending.support.headline.halfPeriodOver=Controlla il pagamento portfolio.pending.support.headline.periodOver=Il periodo di scambio è finito portfolio.pending.support.headline.depositTxMissing=Transazione di deposito mancante portfolio.pending.support.depositTxMissing=Manca una transazione di deposito per questa operazione. Apri un ticket di supporto per contattare un arbitro per assistenza. portfolio.pending.mediationRequested=Mediazione richiesta portfolio.pending.refundRequested=Rimborso richiesto portfolio.pending.openSupport=Apri ticket di supporto portfolio.pending.supportTicketOpened=Ticket di supporto aperto portfolio.pending.communicateWithArbitrator=Si prega di comunicare nella schermata \"Supporto\" con l'arbitro. portfolio.pending.communicateWithMediator=Si prega di comunicare nella schermata \"Supporto\" con il mediatore. portfolio.pending.disputeOpenedByUser=Hai già aperto una disputa.\n{0} portfolio.pending.disputeOpenedByPeer=Il tuo pari commerciale ha aperto una controversia\n{0} portfolio.pending.noReceiverAddressDefined=Nessun indirizzo del destinatario definito portfolio.pending.mediationResult.headline=Pagamento suggerito dalla mediazione portfolio.pending.mediationResult.info.noneAccepted=Completa lo scambio accettando il suggerimento del mediatore per il pagamento dello stesso. portfolio.pending.mediationResult.info.selfAccepted=Hai accettato il suggerimento del mediatore. In attesa che anche il peer accetti. portfolio.pending.mediationResult.info.peerAccepted=Il tuo pari commerciale ha accettato il suggerimento del mediatore. Accetti anche tu? portfolio.pending.mediationResult.button=Visualizza la risoluzione proposta portfolio.pending.mediationResult.popup.headline=Risultato della mediazione per gli scambi con ID: {0} portfolio.pending.mediationResult.popup.headline.peerAccepted=Il tuo pari commerciale ha accettato il suggerimento del mediatore per lo scambio {0} portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader's security deposit) as compensation for their work. Both traders agreeing to the mediator's suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator's suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=Rifiuta e richiedi l'arbitrato portfolio.pending.mediationResult.popup.alreadyAccepted=Hai già accettato portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. portfolio.pending.failedTrade.missingDepositTx=Manca una transazione di deposito.\n\nQuesta transazione è necessaria per completare lo scambio. Assicurati che il tuo portafoglio sia completamente sincronizzato con la blockchain di Monero.\n\nPuoi spostare questo scambio nella sezione "Scambi Falliti" per disattivarlo. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Move trade to failed trades portfolio.pending.failedTrade.warningIcon.tooltip=Click to open details about the issues of this trade portfolio.failed.revertToPending.popup=Do you want to move this trade to open trades? portfolio.failed.revertToPending=Move trade to open trades portfolio.closed.completed=Completato portfolio.closed.ticketClosed=Arbitrato portfolio.closed.mediationTicketClosed=Mediato portfolio.closed.canceled=Annullato portfolio.failed.Failed=Fallito portfolio.failed.unfail= \nPrima di procedere, assicurati di avere un backup della tua directory dei dati!\nVuoi riportare questo scambio nella sezione degli scambi aperti?\nQuesto è un modo per rientrare in possesso dei fondi bloccati in uno scambio fallito.\n  portfolio.failed.cantUnfail= \nAl momento questo scambio non può tornare nella sezione degli scambi aperti.\nRiprova dopo il completamento degli scambi {0} portfolio.failed.depositTxNull=The trade cannot be reverted to a open trade. Deposit transaction is null. portfolio.failed.delayedPayoutTxNull=The trade cannot be reverted to a open trade. Delayed payout transaction is null. #################################################################### # Funds #################################################################### funds.tab.deposit=Ricevi fondi funds.tab.withdrawal=Invia fondi funds.tab.reserved=Fondi riservati funds.tab.locked=Fondi bloccati funds.tab.transactions=Transazioni funds.deposit.unused=Non usato funds.deposit.usedInTx=Utilizzato in {0} transazioni funds.deposit.fundHavenoWallet=Finanzia portafoglio Haveno funds.deposit.noAddresses=Non sono stati ancora generati indirizzi di deposito funds.deposit.fundWallet=Finanzia il tuo portafoglio funds.deposit.withdrawFromWallet=Invia fondi dal portafoglio funds.deposit.amount=Importo in XMR (facoltativo) funds.deposit.generateAddress=Crea nuovo indirizzo funds.deposit.generateAddressSegwit=Native segwit format (Bech32) funds.deposit.selectUnused=Seleziona un indirizzo inutilizzato dalla tabella sopra anziché generarne uno nuovo. funds.withdrawal.arbitrationFee=Commissione arbitraggio funds.withdrawal.inputs=Selezione input funds.withdrawal.useAllInputs=Utilizza tutti gli input disponibili funds.withdrawal.useCustomInputs=Utilizza input personalizzati funds.withdrawal.receiverAmount=Importo del destinatario funds.withdrawal.sendMax=Inviare massimo disponibile funds.withdrawal.senderAmount=Importo del mittente funds.withdrawal.feeExcluded=L'importo esclude la commissione di mining funds.withdrawal.feeIncluded=L'importo include la commissione di mining funds.withdrawal.fromLabel=Ritirare dall'indirizzo funds.withdrawal.toLabel=Ritirare all'indirizzo funds.withdrawal.memoLabel=Withdrawal memo funds.withdrawal.memo=Optionally fill memo funds.withdrawal.withdrawButton=Ritira selezionato funds.withdrawal.noFundsAvailable=Non sono disponibili fondi per il prelievo funds.withdrawal.confirmWithdrawalRequest=Conferma richiesta di prelievo funds.withdrawal.withdrawMultipleAddresses=Ritira da più indirizzi ({0}) funds.withdrawal.withdrawMultipleAddresses.tooltip=Ritira da più indirizzi\n({0}) funds.withdrawal.notEnoughFunds=Non hai abbastanza fondi nel tuo portafoglio. funds.withdrawal.selectAddress=Seleziona un indirizzo sorgente dalla tabella funds.withdrawal.setAmount=Imposta l'importo da prelevare funds.withdrawal.fillDestAddress=Inserisci il tuo indirizzo di destinazione funds.withdrawal.warn.noSourceAddressSelected=È necessario selezionare un indirizzo di origine nella tabella sopra. funds.withdrawal.warn.amountExceeds=Non hai fondi sufficienti disponibili presso l'indirizzo selezionato.\nConsidera di selezionare più indirizzi nella tabella sopra o di cambiare la tariffa per includere la commissione del miner. funds.reserved.noFunds=Nessun fondo è riservato nelle offerte aperte funds.reserved.reserved=Riservato nel portafoglio locale per l'offerta con ID: {0} funds.locked.noFunds=Nessun fondo è bloccato nelle negoziazioni funds.locked.locked=Bloccato in multisig per lo scambio con ID: {0} funds.tx.direction.sentTo=Inviato a: funds.tx.direction.receivedWith=Ricevuto con: funds.tx.direction.genesisTx=Da tx Genesi: funds.tx.createOfferFee=Commissione per maker e tx: {0} funds.tx.takeOfferFee=Commissione per taker e tx: {0} funds.tx.multiSigDeposit=Deposito multisig: {0} funds.tx.multiSigPayout=Pagamento Multisig: {0} funds.tx.disputePayout=Pagamento disputa: {0} funds.tx.disputeLost=Caso di disputa persa : {0} funds.tx.collateralForRefund=Garanzia di rimborso: {0} funds.tx.timeLockedPayoutTx=Tx di pagamento bloccata: {0} funds.tx.refund=Rimborso dell'arbitrato: {0} funds.tx.unknown=Motivo sconosciuto: {0} funds.tx.noFundsFromDispute=Nessun rimborso dalla controversia funds.tx.receivedFunds=Fondi ricevuti funds.tx.withdrawnFromWallet=Prelevato dal portafoglio funds.tx.memo=Memo funds.tx.noTxAvailable=Nessuna transazione disponibile funds.tx.revert=Storna funds.tx.txSent=Transazione inviata con successo ad un nuovo indirizzo nel portafoglio Haveno locale. funds.tx.direction.self=Invia a te stesso funds.tx.dustAttackTx=Polvere ricevuta funds.tx.dustAttackTx.popup=Questa transazione sta inviando un importo XMR molto piccolo al tuo portafoglio e potrebbe essere un tentativo da parte delle società di chain analysis per spiare il tuo portafoglio.\n\nSe usi quell'output della transazione in una transazione di spesa, scopriranno che probabilmente sei anche il proprietario dell'altro indirizzo (combinazione di monete).\n\nPer proteggere la tua privacy, il portafoglio Haveno ignora tali output di polvere a fini di spesa e nella visualizzazione del saldo. È possibile impostare la soglia al di sotto della quale un output è considerato polvere.\n  #################################################################### # Support #################################################################### support.tab.mediation.support=Mediazione support.tab.arbitration.support=Arbitrato support.tab.legacyArbitration.support=Arbitrato Legacy support.tab.ArbitratorsSupportTickets=I ticket di {0} support.sigCheck.button=Check signature support.sigCheck.popup.header=Verify dispute result signature support.sigCheck.popup.msg.label=Summary message support.sigCheck.popup.msg.prompt=Copy & paste summary message from dispute support.sigCheck.popup.result=Validation result support.sigCheck.popup.success=Signature is valid support.sigCheck.popup.failed=Signature verification failed support.sigCheck.popup.invalidFormat=Message is not of expected format. Copy & paste summary message from dispute. support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? support.reOpenButton.label=Re-open support.sendNotificationButton.label=Notifica privata support.reportButton.label=Report support.fullReportButton.label=All disputes support.noTickets=Non ci sono ticket aperti support.sendingMessage=Inviando il messaggio ... support.receiverNotOnline=Il destinatario non è online. Il messaggio viene salvato nella loro casella di posta. support.sendMessageError=Invio messaggio non riuscito. Errore: {0} support.receiverNotKnown=Receiver not known support.wrongVersion=L'offerta in quella controversia è stata creata con una versione precedente di Haveno.\nNon puoi chiudere quella controversia con la versione della tua applicazione.\n\nUtilizza una versione precedente con la versione del protocollo {0} support.openFile=Apri il file da allegare (dimensione massima del file: {0}) support.attachmentTooLarge=La dimensione dei tuoi allegati è di {0} e supera il massimo consentito di {1} kB. support.maxSize=La dimensione massima del file permessa è di {0} kB. support.attachment=Allegato support.tooManyAttachments=Non è possibile inviare più di 3 allegati in un messaggio. support.save=Salva il file sul computer support.messages=Messaggi support.input.prompt=Inserisci messaggio... support.send=Invia support.addAttachments=Aggiungi allegati support.closeTicket=Chiudi ticket support.attachments=Allegati: support.savedInMailbox=Messaggio salvato nella cassetta postale del destinatario support.arrived=Il messaggio è arrivato al destinatario support.acknowledged=Arrivo del messaggio confermato dal destinatario support.error=Il destinatario non ha potuto elaborare il messaggio. Errore: {0} support.buyerAddress=Indirizzo XMR dell'acquirente support.sellerAddress=Indirizzo XMR del venditore support.role=Ruolo support.agent=Support agent support.state=Stato support.chat=Chat support.preparing=In preparazione support.requested=Richiesto support.closed=Chiuso support.open=Aperto support.process=Process support.buyerMaker=Acquirente/Maker XMR support.sellerMaker=Venditore/Maker XMR support.buyerTaker=Acquirente/Taker XMR support.sellerTaker=Venditore/Taker XMR support.backgroundInfo=Haveno non è un'azienda, quindi gestisce le controversie in modo diverso.\n\nI commercianti possono comunicare all'interno dell'applicazione tramite una chat sicura sulla schermata delle negoziazioni aperte per cercare di risolvere le controversie da soli. Se ciò non è sufficiente, un arbitro valuterà la situazione e deciderà un pagamento dei fondi commerciali. support.initialInfo=Inserisci una descrizione del tuo problema nel campo di testo qui sotto. Aggiungi quante più informazioni possibili per accelerare i tempi di risoluzione della disputa.\n\nEcco una lista delle informazioni che dovresti fornire:\n● Se sei l'acquirente XMR: hai effettuato il trasferimento Traditional o Cryptocurrency? In tal caso, hai fatto clic sul pulsante "pagamento avviato" nell'applicazione?\n● Se sei il venditore XMR: hai ricevuto il pagamento Traditional o Cryptocurrency? In tal caso, hai fatto clic sul pulsante "pagamento ricevuto" nell'applicazione?\n● Quale versione di Haveno stai usando?\n● Quale sistema operativo stai usando?\n● Se si è verificato un problema con transazioni non riuscite, prendere in considerazione la possibilità di passare a una nuova directory di dati.\n  A volte la directory dei dati viene danneggiata e porta a strani bug.\n  Vedi: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nAcquisire familiarità con le regole di base per la procedura di disputa:\n● È necessario rispondere alle richieste di {0} entro 2 giorni.\n● I mediatori rispondono entro 2 giorni. Gli arbitri rispondono entro 5 giorni lavorativi.\n● Il periodo massimo per una disputa è di 14 giorni.\n● È necessario collaborare con {1} e fornire le informazioni richieste per presentare il proprio caso.\n● Hai accettato le regole delineate nel documento di contestazione nel contratto con l'utente al primo avvio dell'applicazione.\n\nPuoi leggere ulteriori informazioni sulla procedura di contestazione all'indirizzo: {2}\n  support.systemMsg=Messaggio di sistema: {0} support.youOpenedTicket=Hai aperto una richiesta di supporto.\n\n{0}\n\nVersione Haveno: {1} support.youOpenedDispute=Hai aperto una richiesta per una controversia.\n\n{0}\n\nVersione Haveno: {1} support.youOpenedDisputeForMediation=Hai richiesto la mediazione.\n\n{0}\n\nVersione Haveno: {1} support.peerOpenedTicket=Il tuo peer di trading ha richiesto supporto a causa di problemi tecnici.\n\n{0}\n\nVersione Haveno: {1} support.peerOpenedDispute=Il tuo peer di trading ha richiesto una controversia.\n\n{0}\n\nVersione Haveno: {1} support.peerOpenedDisputeForMediation=Il tuo peer di trading ha richiesto la mediazione.\n\n{0}\n\nVersione Haveno: {1} support.mediatorsDisputeSummary=System message: Mediator's dispute summary:\n{0} support.mediatorsAddress=Indirizzo nodo del mediatore: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. support.info.disputeReOpened=Dispute ticket has been re-opened. #################################################################### # Settings #################################################################### settings.tab.preferences=Preferenze settings.tab.network=Informazioni della Rete settings.tab.about=Circa setting.preferences.general=Preferenze generali setting.preferences.explorer=Monero Explorer setting.preferences.deviation=Deviazione massima del prezzo di mercato setting.preferences.avoidStandbyMode=Evita modalità standby setting.preferences.useSoundForNotifications=Riproduci suoni per le notifiche setting.preferences.autoConfirmXMR=XMR auto-confirm setting.preferences.autoConfirmEnabled=Enabled setting.preferences.autoConfirmRequiredConfirmations=Required confirmations setting.preferences.autoConfirmMaxTradeSize=Max. trade amount (XMR) setting.preferences.autoConfirmServiceAddresses=Monero Explorer URLs (uses Tor, except for localhost, LAN IP addresses, and *.local hostnames) setting.preferences.deviationToLarge=Non sono ammessi valori superiori a {0}%. setting.preferences.txFee=Withdrawal transaction fee (satoshis/vbyte) setting.preferences.useCustomValue=Usa valore personalizzato setting.preferences.txFeeMin=Transaction fee must be at least {0} satoshis/vbyte setting.preferences.txFeeTooLarge=Your input is above any reasonable value (>5000 satoshis/vbyte). Transaction fee is usually in the range of 50-400 satoshis/vbyte. setting.preferences.ignorePeers=Peer ignorati [indirizzo:porta onion] setting.preferences.ignoreDustThreshold=Valore minimo di output non-dust setting.preferences.currenciesInList=Valute nell'elenco dei feed dei prezzi di mercato setting.preferences.prefCurrency=Valuta preferita setting.preferences.displayTraditional=Mostra valute nazionali setting.preferences.noTraditional=Non ci sono valute nazionali selezionate setting.preferences.cannotRemovePrefCurrency=Non è possibile rimuovere la valuta di visualizzazione preferita selezionata setting.preferences.displayCryptos=Visualizza crypto setting.preferences.noCryptos=Non ci sono crypto selezionate setting.preferences.addTraditional=Aggiungi valuta nazionale setting.preferences.addCrypto=Aggiungi crypto setting.preferences.displayOptions=Mostra opzioni setting.preferences.showOwnOffers=Mostra le mie offerte nel libro delle offerte setting.preferences.useAnimations=Usa animazioni setting.preferences.useDarkMode=Usa modalità notte setting.preferences.useLightMode=Usa la modalità chiara setting.preferences.sortWithNumOffers=Ordina le liste di mercato con n. di offerte/scambi setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API setting.preferences.notifyOnPreRelease=Receive pre-release notifications setting.preferences.resetAllFlags=Ripristina tutti i flag \"Non mostrare più\" settings.preferences.languageChange=Per applicare la modifica della lingua a tutte le schermate è necessario riavviare. settings.preferences.supportLanguageWarning=In caso di controversia, tenere presente che l'arbitrato è gestito in {0}. settings.preferences.editCustomExplorer.headline=Explorer Settings settings.preferences.editCustomExplorer.description=Choose a system defined explorer from the list on the left, and/or customize to suit your own preferences. settings.preferences.editCustomExplorer.available=Available explorers settings.preferences.editCustomExplorer.chosen=Chosen explorer settings settings.preferences.editCustomExplorer.name=Nome settings.preferences.editCustomExplorer.txUrl=Transaction URL settings.preferences.editCustomExplorer.addressUrl=Address URL setting.info.headline=Nuova funzione per la privacy dei dati settings.preferences.sensitiveDataRemoval.msg=Per proteggere la privacy tua e degli altri trader, Haveno intende rimuovere i dati sensibili dalle vecchie transazioni. Questo è particolarmente importante per le transazioni in valuta fiat che possono includere dettagli del conto bancario.\n\nSi consiglia di impostare questo valore il più basso possibile, ad esempio 60 giorni. Questo significa che le transazioni completate da più di 60 giorni avranno i dati sensibili cancellati. Le transazioni completate si trovano nella scheda Portafoglio / Cronologia. settings.net.xmrHeader=Network Monero settings.net.p2pHeader=Rete Haveno settings.net.onionAddressLabel=Il mio indirizzo onion settings.net.xmrNodesLabel=Usa nodi Monero personalizzati settings.net.moneroPeersLabel=Peer connessi settings.net.connection=Connessione settings.net.connected=Connesso settings.net.useTorForXmrJLabel=Usa Tor per la rete Monero settings.net.moneroNodesLabel=Nodi Monero a cui connettersi settings.net.useProvidedNodesRadio=Usa i nodi Monero Core forniti settings.net.usePublicNodesRadio=Usa la rete pubblica di Monero settings.net.useCustomNodesRadio=Usa nodi Monero Core personalizzati settings.net.warn.usePublicNodes=If you use public Monero nodes, you are subject to any risk of using untrusted remote nodes.\n\nPlease read more details at [HYPERLINK:https://www.getmonero.org/resources/moneropedia/remote-node.html].\n\nAre you sure you want to use public nodes? settings.net.warn.usePublicNodes.useProvided=No, utilizza i nodi forniti settings.net.warn.usePublicNodes.usePublic=Sì, usa la rete pubblica settings.net.warn.useCustomNodes.B2XWarning=Assicurati che il tuo nodo Monero sia un nodo Monero Core di fiducia!\n\nLa connessione a nodi che non seguono le regole di consenso di Monero Core potrebbe corrompere il tuo portafoglio e causare problemi nel processo di scambio.\n\nGli utenti che si connettono a nodi che violano le regole di consenso sono responsabili per qualsiasi danno risultante. Eventuali controversie risultanti saranno decise a favore dell'altro pari. Nessun supporto tecnico verrà fornito agli utenti che ignorano questo meccanismo di avvertimento e protezione! settings.net.warn.invalidXmrConfig=Connessione alla rete Monero non riuscita perché la configurazione non è valida.\n\nLa tua configurazione è stata ripristinata per utilizzare invece i nodi Monero forniti. Dovrai riavviare l'applicazione. settings.net.localhostXmrNodeInfo=Informazioni di base: Haveno cerca un nodo Monero locale all'avvio. Se viene trovato, Haveno comunicherà con la rete Monero esclusivamente attraverso di esso. settings.net.p2PPeersLabel=Peer connessi settings.net.onionAddressColumn=Indirizzo onion settings.net.creationDateColumn=Stabilito settings.net.connectionTypeColumn=Dentro/Fuori settings.net.sentDataLabel=Sent data statistics settings.net.receivedDataLabel=Received data statistics settings.net.chainHeightLabel=Latest XMR block height settings.net.roundTripTimeColumn=Ritorno settings.net.sentBytesColumn=Inviato settings.net.receivedBytesColumn=Ricevuto settings.net.peerTypeColumn=Tipo di peer settings.net.openTorSettingsButton=Apri impostazioni di Tor settings.net.versionColumn=Versione settings.net.subVersionColumn=Sottoversione settings.net.heightColumn=Altezza settings.net.needRestart=È necessario riavviare l'applicazione per applicare tale modifica.\nVuoi farlo adesso? settings.net.notKnownYet=Non ancora noto... settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec settings.net.chainHeight=Monero Peers chain height: {0} settings.net.ips=[Indirizzo IP:porta | hostname:porta | indirizzo onion:porta] (separato da una virgola). La porta può essere omessa se è usata quella predefinita (8333). settings.net.seedNode=Nodo seme settings.net.directPeer=Peer (diretto) settings.net.initialDataExchange={0} [Bootstrapping] settings.net.peer=Peer settings.net.inbound=in entrata settings.net.outbound=in uscita setting.about.aboutHaveno=Riguardo Haveno setting.about.about=Haveno è un software open source che facilita lo scambio di monero con valute nazionali (e altre criptovalute) attraverso una rete peer-to-peer decentralizzata in modo da proteggere fortemente la privacy degli utenti. Leggi di più riguardo Haveno sulla pagina web del progetto. setting.about.web=Pagina web Haveno setting.about.code=Codice sorgente setting.about.agpl=Licenza AGPL setting.about.support=Supporta Haveno setting.about.def=Haveno non è un'azienda, è un progetto aperto alla comunità. Se vuoi partecipare o supportare Haveno, segui i link qui sotto. setting.about.contribute=Contribuisci setting.about.providers=Fornitori di dati setting.about.apisWithFee=Haveno uses Haveno Price Indices for Fiat and Crypto market prices, and Haveno Mempool Nodes for mining fee estimation. setting.about.apis=Haveno uses Haveno Price Indices for Fiat and Crypto market prices. setting.about.pricesProvided=Prezzi di mercato forniti da setting.about.feeEstimation.label=Previsione della commissione di mining fornita da setting.about.versionDetails=Dettagli versione setting.about.version=Versione applicazione setting.about.subsystems.label=Versioni di sottosistemi setting.about.subsystems.val=Versione di rete: {0}; Versione del messaggio P2P: {1}; Versione DB locale: {2}; Versione del protocollo di scambio: {3} setting.about.shortcuts=Scorciatoie setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' o 'alt + {0}' o 'cmd + {0}' setting.about.shortcuts.menuNav=Naviga il menu principale setting.about.shortcuts.menuNav.value=Per navigare nel menu principale premere: 'Ctrl' o 'alt' o 'cmd' con un tasto numerico tra '1-9' setting.about.shortcuts.close=Chiudi Haveno setting.about.shortcuts.close.value='Ctrl + {0}' o 'cmd + {0}' o 'Ctrl + {1}' o 'cmd + {1}' setting.about.shortcuts.closePopup=Chiudi popup o finestra di dialogo setting.about.shortcuts.closePopup.value=Tasto 'ESC' setting.about.shortcuts.chatSendMsg=Invia messaggio chat al trader setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' o 'alt + ENTER' o 'cmd + ENTER' setting.about.shortcuts.openDispute=Apri disputa setting.about.shortcuts.openDispute.value=Seleziona lo scambio in sospeso e fai clic: {0} setting.about.shortcuts.walletDetails=Apri la finestra dei dettagli del portafoglio setting.about.shortcuts.openEmergencyXmrWalletTool=Apri lo strumento portafoglio di emergenza per il portafoglio XMR setting.about.shortcuts.showTorLogs=Attiva / disattiva il livello di registro per i messaggi Tor tra DEBUG e WARN setting.about.shortcuts.manualPayoutTxWindow=Apri la finestra per il pagamento manuale da una transazione di deposito Multisig 2di2 setting.about.shortcuts.removeStuckTrade=Open popup to move failed trade to open trades tab again setting.about.shortcuts.removeStuckTrade.value=Select failed trade and press: {0} setting.about.shortcuts.registerArbitrator=Registra arbitro (solo mediatore/arbitro) setting.about.shortcuts.registerArbitrator.value=Passare all'account e premere: {0} setting.about.shortcuts.registerMediator=Registra mediatore (solo mediatore/arbitro) setting.about.shortcuts.registerMediator.value=Passare all'account e premere: {0} setting.about.shortcuts.openSignPaymentAccountsWindow=Apri la finestra aperta per firnare l'età dell'account (solo arbitri legacy) setting.about.shortcuts.openSignPaymentAccountsWindow.value=Passare alla visualizzazione dell'arbitro legacy e premere: {0} setting.about.shortcuts.sendAlertMsg=Invia avviso o aggiorna messaggio (attività privilegiata) setting.about.shortcuts.sendFilter=Imposta Filtro (attività privilegiata) setting.about.shortcuts.sendPrivateNotification=Invia notifica privata al peer (attività privilegiata) setting.about.shortcuts.sendPrivateNotification.value=Open peer info at avatar and press: {0} setting.info.headline=New XMR auto-confirm Feature setting.info.msg=When selling XMR for XMR you can use the auto-confirm feature to verify that the correct amount of XMR was sent to your wallet so that Haveno can automatically mark the trade as complete, making trades quicker for everyone.\n\nAuto-confirm checks the XMR transaction on at least 2 XMR explorer nodes using the private transaction key provided by the XMR sender. By default, Haveno uses explorer nodes run by Haveno contributors, but we recommend running your own XMR explorer node for maximum privacy and security.\n\nYou can also set the maximum amount of XMR per trade to auto-confirm as well as the number of required confirmations here in Settings.\n\nSee more details (including how to set up your own explorer node) on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades] #################################################################### # Account #################################################################### account.tab.mediatorRegistration=Registrazione del mediatore account.tab.refundAgentRegistration=Registrazione agente di rimborso account.tab.signing=Signing account.info.headline=Benvenuto nel tuo Account Haveno account.info.msg=Qui puoi aggiungere conti di trading per valute nazionali e crypto e creare un backup dei tuoi dati di portafoglio e conto.\n\nUn nuovo portafoglio Monero è stato creato la prima volta che hai avviato Haveno.\n\nTi consigliamo vivamente di annotare le parole del seme del portafoglio Monero (vedi la scheda in alto) e prendere in considerazione l'aggiunta di una password prima del finanziamento. I depositi e prelievi di monero sono gestiti nella sezione \"Fondi\".\n\nInformativa sulla privacy e sulla sicurezza: poiché Haveno è un exchange decentralizzato, tutti i tuoi dati vengono conservati sul tuo computer. Non ci sono server, quindi non abbiamo accesso alle tue informazioni personali, ai tuoi fondi o persino al tuo indirizzo IP. Dati come numeri di conto bancario, crypto e indirizzi Monero, ecc. vengono condivisi con il proprio partner commerciale per adempiere alle negoziazioni avviate (in caso di controversia il mediatore o l'arbitro vedrà gli stessi dati del proprio peer di negoziazione). account.menu.paymentAccount=Conti in valuta nazionale account.menu.altCoinsAccountView=Conti crypto account.menu.password=Password portafoglio account.menu.seedWords=Seme portafoglio account.menu.walletInfo=Wallet info account.menu.backup=Backup account.menu.notifications=Notifiche account.menu.walletInfo.balance.headLine=Wallet balances account.menu.walletInfo.balance.info=This shows the internal wallet balance including unconfirmed transactions.\nFor XMR, the internal wallet balance shown below should match the sum of the 'Available' and 'Reserved' balances shown in the top right of this window. account.menu.walletInfo.xpub.headLine=Watch keys (xpub keys) account.menu.walletInfo.walletSelector={0} {1} wallet account.menu.walletInfo.path.headLine=HD keychain paths account.menu.walletInfo.path.info=If you import seed words into another wallet (like Electrum), you'll need to define the path. This should only be done in emergency cases when you lose access to the Haveno wallet and data directory.\nKeep in mind that spending funds from a non-Haveno wallet can bungle the internal Haveno data structures associated with the wallet data, which can lead to failed trades.\n\nNEVER send BSQ from a non-Haveno wallet, as it will probably lead to an invalid BSQ transaction and losing your BSQ. account.menu.walletInfo.openDetails=Show raw wallet details and private keys ## TODO should we rename the following to a gereric name? account.arbitratorRegistration.pubKey=Chiave pubblica account.arbitratorRegistration.register=Registrare account.arbitratorRegistration.registration={0} registrazione account.arbitratorRegistration.revoke=Revoca account.arbitratorRegistration.info.msg=Nota che è necessario rimanere disponibili per 15 giorni dopo la revoca poiché potrebbero esserci operazioni che ti utilizzano come {0}. Il periodo massimo di scambio consentito è di 8 giorni e il processo di contestazione potrebbe richiedere fino a 7 giorni. account.arbitratorRegistration.warn.min1Language=Devi impostare almeno 1 lingua.\nAbbiamo aggiunto la lingua predefinita per te. account.arbitratorRegistration.removedSuccess=Hai rimosso con successo la tua registrazione dalla rete Haveno. account.arbitratorRegistration.removedFailed=Impossibile rimuovere la registrazione. {0} account.arbitratorRegistration.registerSuccess=Ti sei registrato correttamente alla rete Haveno. account.arbitratorRegistration.registerFailed=Impossibile completare la registrazione. {0} account.crypto.yourCryptoAccounts=I tuoi conti crypto account.crypto.popup.wallet.msg=Assicurati di seguire i requisiti per l'uso di {0} portafogli come descritto nella pagina web {1}.\nL'uso di portafogli da exchange centralizzati in cui (a) non controlli le tue chiavi o (b) che non usano software di portafoglio compatibile è rischioso: può portare alla perdita dei fondi scambiati!\nIl mediatore o l'arbitro non è uno specialista {2} e non può essere d'aiuto in questi casi. account.crypto.popup.wallet.confirm=Capisco e confermo di sapere quale portafoglio devo usare. # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=Trading UPX on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending UPX, you need to use either the official uPlexa GUI wallet or uPlexa CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\nuplexa-wallet-cli (use the command get_tx_key)\nuplexa-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The UPX sender is responsible for providing verification of the UPX transfer to the arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit uPlexa discord channel (https://discord.gg/vhdNSrV) or the uPlexa Telegram Chat (https://t.me/uplexaOfficial) to find more information. # suppress inspection "UnusedProperty" account.crypto.popup.arq.msg=Il trading di ARQ su Haveno richiede di comprendere e soddisfare i seguenti requisiti:\n\nPer inviare ARQ, è necessario utilizzare il portafoglio GUI ArQmA ufficiale o il portafoglio CLI ArQmA con il flag store-tx-info abilitato (impostazione predefinita nelle nuove versioni). Assicurati di poter accedere alla chiave tx come sarebbe richiesto in caso di controversia.\narqma-wallet-cli (utilizzare il comando get_tx_key)\narqma-wallet-gui (vai allo storico trnsazioni e fai clic sul pulsante (P) per la prova del pagamento)\n\nNegli explorer di blocchi normali il trasferimento non è verificabile.\n\nÈ necessario fornire al mediatore o all'arbitro i seguenti dati in caso di controversia:\n- La chiave privata tx\n- L'hash della transazione\n- L'indirizzo pubblico del destinatario\n\nIl mancato conferimento dei dati di cui sopra o l'utilizzo di un portafoglio incompatibile comporterà la perdita del caso di contestazione. Il mittente ARQ è responsabile di fornire la verifica del trasferimento ARQ al mediatore o all'arbitro in caso di controversia.\n\nNon è richiesto un ID di pagamento, ma solo il normale indirizzo pubblico.\nSe non si è sicuri di tale processo, visitare il canale discord ArQmA (https://discord.gg/s9BQpJT) o il forum ArQmA (https://labs.arqma.com) per trovare ulteriori informazioni.\n  # suppress inspection "UnusedProperty" account.crypto.popup.xmr.msg=Trading XMR on Haveno requires that you understand the following requirement.\n\nIf selling XMR, you must be able to provide the following information to a mediator or arbitrator in case of a dispute:\n- the transaction key (Tx Key, Tx Secret Key or Tx Private Key)\n- the transaction ID (Tx ID or Tx Hash)\n- the destination address (recipient's address)\n\nSee the wiki for details on where to find this information on popular Monero wallets [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Proving_payments].\nFailure to provide the required transaction data will result in losing disputes.\n\nAlso note that Haveno now offers automatic confirming for XMR transactions to make trades quicker, but you need to enable it in Settings.\n\nSee the wiki for more information about the auto-confirm feature: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. # suppress inspection "UnusedProperty" account.crypto.popup.msr.msg=Trading MSR on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending MSR, you need to use either the official Masari GUI wallet, Masari CLI wallet with the store-tx-info flag enabled (enabled by default) or the Masari web wallet (https://wallet.getmasari.org). Please be sure you can access the tx key as that would be required in case of a dispute.\nmasari-wallet-cli (use the command get_tx_key)\nmasari-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nMasari Web Wallet (goto Account -> transaction history and view details on your sent transaction)\n\nVerification can be accomplished in-wallet.\nmasari-wallet-cli : using command (check_tx_key).\nmasari-wallet-gui : on the Advanced > Prove/Check page.\nVerification can be accomplished in the block explorer \nOpen block explorer (https://explorer.getmasari.org), use the search bar to find your transaction hash.\nOnce transaction is found, scroll to bottom to the 'Prove Sending' area and fill in details as needed.\nYou need to provide the mediator or arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The MSR sender is responsible for providing verification of the MSR transfer to the mediator or arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process, ask for help on the Official Masari Discord (https://discord.gg/sMCwMqs). # suppress inspection "UnusedProperty" account.crypto.popup.blur.msg=Trading BLUR on Haveno requires that you understand and fulfill the following requirements:\n\nTo send BLUR you must use the Blur Network CLI or GUI Wallet. \n\nIf you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\nIf you are using the Blur Network GUI Wallet, the transaction private key and transaction ID can be found conveniently in the "History" tab. Immediately after sending, locate the transaction of interest. Click the "?" symbol in the lower-right corner of the box containing the transaction. You must save this information. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the BLUR transfer using the Blur Transaction Viewer (https://blur.cash/#tx-viewer).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the BLUR sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the Blur Network Discord (https://discord.gg/dMWaqVW). # suppress inspection "UnusedProperty" account.crypto.popup.solo.msg=Trading Solo on Haveno requires that you understand and fulfill the following requirements:\n\nTo send Solo you must use the Solo Network CLI Wallet. \n\nIf you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the Solo transfer using the Solo Block Explorer by searching for the transaction and then using the "Prove sending" function (https://explorer.minesolo.com/).\n\nfailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the Solo sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the Solo Network Discord (https://discord.minesolo.com/). # suppress inspection "UnusedProperty" account.crypto.popup.cash2.msg=Trading CASH2 on Haveno requires that you understand and fulfill the following requirements:\n\nTo send CASH2 you must use the Cash2 Wallet version 3 or higher. \n\nAfter a transaction is sent, the transaction ID will be displayed. You must save this information. Immediately after sending the transaction, you must use the command 'getTxKey' in simplewallet to retrieve the transaction secret key. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, 2) the transaction secret key, and 3) the recipient's Cash2 address. The mediator or arbitrator will then verify the CASH2 transfer using the Cash2 Block Explorer (https://blocks.cash2.org).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the CASH2 sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the Cash2 Discord (https://discord.gg/FGfXAYN). # suppress inspection "UnusedProperty" account.crypto.popup.qwertycoin.msg=Trading Qwertycoin on Haveno requires that you understand and fulfill the following requirements:\n\nTo send QWC you must use the official QWC Wallet version 5.1.3 or higher. \n\nAfter a transaction is sent, the transaction ID will be displayed. You must save this information. Immediately after sending the transaction, you must use the command 'get_Tx_Key' in simplewallet to retrieve the transaction secret key. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, 2) the transaction secret key, and 3) the recipient's QWC address. The mediator or arbitrator will then verify the QWC transfer using the QWC Block Explorer (https://explorer.qwertycoin.org).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the QWC sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the QWC Discord (https://discord.gg/rUkfnpC). # suppress inspection "UnusedProperty" account.crypto.popup.drgl.msg=Il trading di Dragonglass su Haveno richiede di comprendere e soddisfare i seguenti requisiti:\n\nA causa della privacy fornita da Dragonglass, una transazione non è verificabile sulla blockchain pubblica. Se necessario, puoi provare il tuo pagamento utilizzando la tua chiave privata TXN.\nLa chiave privata TXN è una chiave una tantum generata automaticamente per ogni transazione a cui è possibile accedere solo dal proprio portafoglio DRGL.\nOppore dalla GUI del wallet DRGL (nella finestra di dialogo dei dettagli della transazione) o dalla semplice interfaccia CLI di Dragonglass (usando il comando "get_tx_key").\n\nPer entrambi è richiesta la versione DRGL 'Oathkeeper' e successive.\n\nIn caso di controversia, è necessario fornire al mediatore o all'arbitro i seguenti dati:\n- La chiave privata TXN\n- L'hash della transazione\n- L'indirizzo pubblico del destinatario\n\nLa verifica del pagamento può essere effettuata utilizzando i dati sopra riportati come input su (http://drgl.info/#check_txn).\n\nIl mancato conferimento dei dati di cui sopra o l'utilizzo di un portafoglio incompatibile comporterà la perdita del caso di disputa. Il mittente Dragonglass è responsabile di fornire la verifica del trasferimento DRGL al mediatore o all'arbitro in caso di controversia. L'uso di PaymentID non è richiesto.\n\nIn caso di dubbi su qualsiasi parte di questo processo, visitare Dragonglass su Discord (http://discord.drgl.info) per assistenza.\n  # suppress inspection "UnusedProperty" account.crypto.popup.ZEC.msg=Quando si utilizza Zcash è possibile utilizzare solo gli indirizzi trasparenti (a partire da t), non gli indirizzi z (privati), poiché il mediatore o l'arbitro non sarebbero in grado di verificare la transazione con gli indirizzi z. # suppress inspection "UnusedProperty" account.crypto.popup.XZC.msg=Quando si utilizza Zcoin è possibile utilizzare solo gli indirizzi trasparenti (tracciabili), non quelli non rintracciabili, poiché il mediatore o l'arbitro non sarebbero in grado di verificare la transazione con indirizzi non rintracciabili in un explorer di blocchi. # suppress inspection "UnusedProperty" account.crypto.popup.grin.msg=GRIN richiede un processo interattivo tra il mittente e il destinatario per creare la transazione. Assicurati di seguire le istruzioni dalla pagina web del progetto GRIN per inviare e ricevere in modo affidabile GRIN (il ricevitore deve essere online o almeno essere online durante un certo periodo di tempo).\n\nHaveno supporta solo il formato URL del portafoglio Grinbox (Wallet713).\n\nIl mittente GRIN è tenuto a dimostrare di aver inviato GRIN correttamente. Se il portafoglio non è in grado di fornire tale prova, una potenziale disputa verrà risolta a favore del destinatario GRIN. Assicurati di utilizzare il software Grinbox più recente che supporti la prova delle transazioni e di comprendere il processo di trasferimento e ricezione di GRIN e come creare la prova.\n\nVedi https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only per ulteriori informazioni sullo strumento Grinbox proof. # suppress inspection "UnusedProperty" account.crypto.popup.beam.msg=BEAM richiede un processo interattivo tra il mittente e il destinatario per creare la transazione.\n\nAssicurati di seguire le istruzioni dalla pagina web del progetto BEAM per inviare e ricevere in modo affidabile BEAM (il ricevitore deve essere online o almeno essere online durante un certo periodo di tempo).\n\nIl mittente BEAM è tenuto a fornire la prova di aver inviato BEAM correttamente. Assicurati di utilizzare il software del portafoglio che può produrre tale prova. Se il portafoglio non è in grado di fornire la prova, una potenziale disputa verrà risolta a favore del destinatario BEAM. # suppress inspection "UnusedProperty" account.crypto.popup.pars.msg=Trading ParsiCoin on Haveno requires that you understand and fulfill the following requirements:\n\nTo send PARS you must use the official ParsiCoin Wallet version 3.0.0 or higher. \n\nYou can Check your Transaction Hash and Transaction Key on Transactions Section on your GUI Wallet (ParsiPay) You need to right Click on the Transaction and then click on show details. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the Transaction Hash, 2) the Transaction Key, and 3) the recipient's PARS address. The mediator or arbitrator will then verify the PARS transfer using the ParsiCoin Block Explorer (http://explorer.parsicoin.net/#check_payment).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the ParsiCoin sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the ParsiCoin Discord (https://discord.gg/c7qmFNh). # suppress inspection "UnusedProperty" account.crypto.popup.blk-burnt.msg=To trade burnt blackcoins, you need to know the following:\n\nBurnt blackcoins are unspendable. To trade them on Haveno, output scripts need to be in the form: OP_RETURN OP_PUSHDATA, followed by associated data bytes which, after being hex-encoded, constitute addresses. For example, burnt blackcoins with an address 666f6f (“foo” in UTF-8) will have the following script:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\nTo create burnt blackcoins, one may use the “burn” RPC command available in some wallets.\n\nFor possible use cases, one may look at https://ibo.laboratorium.ee .\n\nAs burnt blackcoins are unspendable, they can not be reselled. “Selling” burnt blackcoins means burning ordinary blackcoins (with associated data equal to the destination address).\n\nIn case of a dispute, the BLK seller needs to provide the transaction hash. # suppress inspection "UnusedProperty" account.crypto.popup.liquidmonero.msg=Il trading di L-XMR su Haveno richiede la comprensione di quanto segue:\n\nQuando ricevi L-XMR per uno scambio su Haveno, non puoi utilizzare l'applicazione mobile Blockstream Green Wallet o un portafoglio di custodia/scambio. Devi ricevere L-XMR solo nel portafoglio Liquid Elements Core o in un altro portafoglio L-XMR che ti consenta di ottenere la chiave per il tuo indirizzo L-XMR.\n\nNel caso in cui sia necessaria la mediazione o in caso di disputa nello scambio, è necessario divulgare la chiave di ricezione per il proprio indirizzo L-XMR al mediatore Haveno o all'agente di rimborso in modo che possano verificare i dettagli della propria Transazione riservata sul proprio full node Elements Core.\n\nLa mancata fornitura delle informazioni richieste dal mediatore o dall'agente di rimborso comporterà la perdita della disputa. In tutti i casi di disputa, il ricevente L-XMR si assume al 100% l'onere della responsabilità nel fornire prove crittografiche al mediatore o all'agente di rimborso.\n\nSe non comprendi i sopracitati requisiti, non scambiare L-XMR su Haveno. account.traditional.yourTraditionalAccounts=I tuoi conti in valuta nazionale account.backup.title=Portafoglio di backup account.backup.location=Posizione di backup account.backup.selectLocation=Seleziona la posizione di backup account.backup.backupNow=Esegui il backup ora (il backup non è crittografato!) account.backup.appDir=Cartella dei dati dell'applicazione account.backup.openDirectory=Apri cartella account.backup.openLogFile=Apri il file di registro account.backup.success=Backup salvato correttamente in:\n{0} account.backup.directoryNotAccessible=La cartella che hai scelto non è accessibile. {0} account.password.removePw.button=Rimuovi password account.password.removePw.headline=Rimuovi la protezione con password per il portafoglio account.password.setPw.button=Imposta password account.password.setPw.headline=Imposta la protezione con password per il portafoglio account.password.info=Con la protezione tramite password, dovrai inserire la password all'avvio dell'applicazione, quando prelevi monero dal tuo portafoglio e quando mostri le tue parole chiave di ripristino. account.seed.backup.title=Effettua il backup delle parole chiave del tuo portafoglio. account.seed.info=Per favore, annota sia le parole chiave del tuo portafoglio che la data. Puoi recuperare il tuo portafoglio in qualsiasi momento con le parole chiave e la data.\n\nDovresti annotare le parole chiave su un foglio di carta. Non salvarle sul computer.\n\nPer favore, nota che le parole chiave NON sostituiscono un backup.\nÈ necessario creare un backup dell'intera directory dell'applicazione dalla schermata "Account/Backup" per recuperare lo stato e i dati dell'applicazione. account.seed.backup.warning=Per favore, nota che le parole chiave non sostituiscono un backup.\nÈ necessario creare un backup dell'intera directory dell'applicazione dalla schermata "Account/Backup" per recuperare lo stato e i dati dell'applicazione. account.seed.warn.noPw.msg=Non hai impostato una password per il portafoglio che protegga la visualizzazione delle parole del seed.\n\nVuoi visualizzare le parole del seed? account.seed.warn.noPw.yes=Sì, e non chiedermelo più account.seed.enterPw=Immettere la password per visualizzare le parole chiave account.seed.restore.info=Effettuare un backup prima di cominciare il ripristino dalle parole del seed. Tenere presente che il ripristino del portafoglio è solo per casi di emergenza e potrebbe causare problemi con il database del portafoglio interno.\nNon è un modo per effettuare un backup! Utilizzare un backup della directory dei dati dell'applicazione per ripristinare uno stato dell'applicazione precedente.\n\nDopo aver ripristinato, l'applicazione si spegnerà automaticamente. Dopo aver riavviato l'applicazione, si risincronizzerà con la rete Monero. Questo può richiedere del tempo e può consumare molta CPU, soprattutto se il portafoglio era vecchio e aveva molte transazioni. Evita di interrompere tale processo, altrimenti potrebbe essere necessario eliminare nuovamente il file della catena SPV o ripetere il processo di ripristino. account.seed.restore.ok=Ok, fai il ripristino e spegni Haveno #################################################################### # Mobile notifications #################################################################### account.notifications.setup.title=Setup account.notifications.download.label=Scarica app mobile account.notifications.waitingForWebCam=In attesa della webcam... account.notifications.webCamWindow.headline=Scansiona il codice QR dal telefono account.notifications.webcam.label=Utilizza webcam account.notifications.webcam.button=Scansiona il codice QR account.notifications.noWebcam.button=Non ho una webcam account.notifications.erase.label=Cancella notifiche sul telefono account.notifications.erase.title=Cancella notifiche account.notifications.email.label=Token di associazione account.notifications.email.prompt=Inserisci il token di associazione che hai ricevuto via e-mail account.notifications.settings.title=Impostazioni account.notifications.useSound.label=Riproduci l'audio di notifica sul telefono account.notifications.trade.label=Ricevi messaggi commerciali account.notifications.market.label=Ricevi avvisi sulle offerte account.notifications.price.label=Ricevi avvisi sui prezzi account.notifications.priceAlert.title=Avvisi sui prezzi account.notifications.priceAlert.high.label=Notifica se il prezzo XMR è superiore account.notifications.priceAlert.low.label=Notifica se il prezzo XMR è inferiore account.notifications.priceAlert.setButton=Imposta un avviso di prezzo account.notifications.priceAlert.removeButton=Rimuovi avviso di prezzo account.notifications.trade.message.title=Lo stato dello scambio è cambiato account.notifications.trade.message.msg.conf=La transazione di deposito per lo scambio con ID {0} è confermata. Si prega di aprire l'applicazione Haveno e avviare il pagamento. account.notifications.trade.message.msg.started=L'acquirente XMR ha avviato il pagamento per lo scambio con ID {0}. account.notifications.trade.message.msg.completed=Lo scambio con ID {0} è completato. account.notifications.offer.message.title=La tua offerta è stata presa account.notifications.offer.message.msg=La tua offerta con ID {0} è stata accettata account.notifications.dispute.message.title=Nuovo messaggio di contestazione account.notifications.dispute.message.msg=Hai ricevuto un messaggio di contestazione per lo scambio con ID {0} account.notifications.marketAlert.title=Offri avvisi account.notifications.marketAlert.selectPaymentAccount=Offre un account di pagamento corrispondente account.notifications.marketAlert.offerType.label=Tipo di offerta che mi interessa account.notifications.marketAlert.offerType.buy=Acquista offerte (voglio vendere XMR) account.notifications.marketAlert.offerType.sell=Offerte di vendita (Voglio comprare XMR) account.notifications.marketAlert.trigger=Distanza prezzo offerta (%) account.notifications.marketAlert.trigger.info=Con una distanza di prezzo impostata, riceverai un avviso solo quando viene pubblicata un'offerta che soddisfa (o supera) i tuoi requisiti. Esempio: vuoi vendere XMR, ma venderai solo con un premio del 2% dal prezzo di mercato attuale. Se si imposta questo campo su 2%, si riceveranno avvisi solo per offerte con prezzi superiori del 2% (o più) dal prezzo di mercato corrente.\n  account.notifications.marketAlert.trigger.prompt=Distanza percentuale dal prezzo di mercato (ad es. 2,50%, -0,50%, ecc.) account.notifications.marketAlert.addButton=Aggiungi avviso offerta account.notifications.marketAlert.manageAlertsButton=Gestisci avvisi di offerta account.notifications.marketAlert.manageAlerts.title=Gestisci avvisi di offerta account.notifications.marketAlert.manageAlerts.header.paymentAccount=Conto di Pagamento account.notifications.marketAlert.manageAlerts.header.trigger=Prezzo di attivazione account.notifications.marketAlert.manageAlerts.header.offerType=Tipo di offerta account.notifications.marketAlert.message.title=Avviso di offerta account.notifications.marketAlert.message.msg.below=sotto account.notifications.marketAlert.message.msg.above=sopra account.notifications.marketAlert.message.msg=Una nuova '{0} {1}' offerta con prezzo {2} ({3} {4} prezzo di mercato) e metodo di pagamento '{5}' è stata pubblicata sulla pagina delle offerte Haveno.\nID offerta: {6}. account.notifications.priceAlert.message.title=Avviso di prezzo per {0} account.notifications.priceAlert.message.msg=Il tuo avviso di prezzo è stato attivato. L'attuale prezzo {0} è {1} {2} account.notifications.noWebCamFound.warning=Nessuna webcam trovata.\n\nUtilizzare l'opzione e-mail per inviare il token e la chiave di crittografia dal telefono cellulare all'applicazione Haveno. account.notifications.priceAlert.warning.highPriceTooLow=Il prezzo più alto deve essere maggiore del prezzo più basso. account.notifications.priceAlert.warning.lowerPriceTooHigh=Il prezzo più basso deve essere inferiore al prezzo più alto. #################################################################### # Windows #################################################################### inputControlWindow.headline=Select inputs for transaction inputControlWindow.balanceLabel=Saldo disponibile contractWindow.title=Dettagli disputa contractWindow.dates=Data dell'offerta / Data di scambio contractWindow.xmrAddresses=Indirizzo Monero acquirente XMR / venditore XMR contractWindow.onions=Indirizzo di rete acquirente XMR / venditore XMR contractWindow.accountAge=Età account acquirente XMR / venditore XMR contractWindow.numDisputes=Numero di controversie acquirente XMR / venditore XMR contractWindow.contractHash=Hash contratto displayAlertMessageWindow.headline=Informazioni importanti! displayAlertMessageWindow.update.headline=Informazioni importanti sull'aggiornamento! displayAlertMessageWindow.update.download=Download: displayUpdateDownloadWindow.downloadedFiles=File: displayUpdateDownloadWindow.downloadingFile=Download: {0} displayUpdateDownloadWindow.verifiedSigs=Firma verificata con chiavi: displayUpdateDownloadWindow.status.downloading=Download file... displayUpdateDownloadWindow.status.verifying=Verifica firma... displayUpdateDownloadWindow.button.label=Scarica il programma di installazione e verifica la firma displayUpdateDownloadWindow.button.downloadLater=Scarica più tardi displayUpdateDownloadWindow.button.ignoreDownload=Ignora questa versione displayUpdateDownloadWindow.headline=È disponibile un nuovo aggiornamento di Haveno! displayUpdateDownloadWindow.download.failed.headline=Download fallito displayUpdateDownloadWindow.download.failed=Download failed.\nPlease download and verify manually at [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.installer.failed=Unable to determine the correct installer. Please download and verify manually at [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.verify.failed=Verification failed.\nPlease download and verify manually at [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.success=La nuova versione è stata scaricata correttamente e la firma è stata verificata.\n\nAprire la cartella di download, chiudere l'applicazione e installare la nuova versione. displayUpdateDownloadWindow.download.openDir=Apri la cartella di download disputeSummaryWindow.title=Sommario disputeSummaryWindow.openDate=Data di apertura del ticket disputeSummaryWindow.role=Ruolo del trader disputeSummaryWindow.payout=Pagamento dell'importo di scambio disputeSummaryWindow.payout.getsTradeAmount=XMR {0} ottiene il pagamento dell'importo commerciale disputeSummaryWindow.payout.getsAll=Max. payout to XMR {0} disputeSummaryWindow.payout.custom=Pagamento personalizzato disputeSummaryWindow.payoutAmount.buyer=Importo pagamento dell'acquirente disputeSummaryWindow.payoutAmount.seller=Importo pagamento del venditore disputeSummaryWindow.payoutAmount.invert=Utilizza perdente come editore disputeSummaryWindow.reason=Motivo della disputa disputeSummaryWindow.tradePeriodEnd=Trade period end disputeSummaryWindow.extraInfo=Extra information disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BUG=Errore # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.USABILITY=Usabilità # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Violazione del protocollo # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.NO_REPLY=Nessuna risposta # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SCAM=Truffa # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OTHER=Altro # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BANK_PROBLEMS=Banca # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OPTION_TRADE=Option trade # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Trader not responding # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled disputeSummaryWindow.summaryNotes=Note di sintesi disputeSummaryWindow.addSummaryNotes=Aggiungi note di sitensi disputeSummaryWindow.close.button=Chiudi ticket # Do no change any line break or order of tokens as the structure is used for signature verification # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.msg=Ticket closed on {0}\n{1} node address: {2}\n\nSummary:\nTrade ID: {3}\nCurrency: {4}\nTrade amount: {5}\nPayout amount for XMR buyer: {6}\nPayout amount for XMR seller: {7}\n\nReason for dispute: {8}\n\nSummary notes:\n{9}\n # Do no change any line break or order of tokens as the structure is used for signature verification disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3} disputeSummaryWindow.close.nextStepsForMediation=\nNext steps:\nOpen trade and accept or reject suggestion from mediator disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\nNo further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions disputeSummaryWindow.close.closePeer=Devi chiudere anche il ticket dei peer di trading! disputeSummaryWindow.close.txDetails.headline=Pubblica transazione di rimborso # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.buyer=L'acquirente riceve {0} all'indirizzo: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=Il venditore riceve {0} all'indirizzo: {1}\n disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3}\n\nAre you sure you want to publish this transaction? disputeSummaryWindow.close.noPayout.headline=Close without any payout disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? emptyWalletWindow.headline={0} strumento portafoglio di emergenza emptyWalletWindow.info=Utilizzalo solo in caso di emergenza se non puoi accedere al tuo fondo dall'interfaccia utente.\n\nSi noti che tutte le offerte aperte verranno chiuse automaticamente quando si utilizza questo strumento.\n\nPrima di utilizzare questo strumento, eseguire il backup della directory dei dati. Puoi farlo in \"Account/Backup\".\n\nTi preghiamo di segnalarci il tuo problema e di presentare una segnalazione di bug su GitHub o sul forum Haveno in modo da poter esaminare la causa del problema. emptyWalletWindow.balance=Il saldo disponibile del tuo portafoglio emptyWalletWindow.address=Il tuo indirizzo di destinazione emptyWalletWindow.button=Invia tutti i fondi emptyWalletWindow.openOffers.warn=Hai offerte aperte che verranno rimosse se svuoti il portafoglio.\nSei sicuro di voler svuotare il tuo portafoglio? emptyWalletWindow.openOffers.yes=Sì, ne sono sicuro emptyWalletWindow.sent.success=Il saldo del tuo portafoglio è stato trasferito correttamente. enterPrivKeyWindow.headline=Immettere la chiave privata per la registrazione filterWindow.headline=Modifica elenco filtri filterWindow.offers=Offerte filtrate (separate con una virgola) filterWindow.onions=Banned from trading addresses (comma sep.) filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.) filterWindow.accounts=Dati dell'account di trading filtrati:\nFormato: virgola sep. elenco di [ID metodo di pagamento | campo dati | valore] filterWindow.bannedCurrencies=Codici valuta filtrati (separati con una virgola) filterWindow.bannedPaymentMethods=ID dei metodi di pagamento filtrati (separati con una virgola) filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) filterWindow.arbitrators=Arbitri filtrati (indirizzi onion separati con una virgola) filterWindow.mediators=Mediatori filtrati (indirizzi onion separati con una virgola) filterWindow.refundAgents=Agenti di rimborso filtrati (virgola sep. indirizzi onion) filterWindow.seedNode=Nodi seme filtrati (separati con una virgola) filterWindow.priceRelayNode=Prezzo filtrato dai nodi relay (virgola sep. indirizzi onion) filterWindow.xmrNode=Nodi Monero filtrati (indirizzo + porta separati con una virgola) filterWindow.preventPublicXmrNetwork=Impedisci l'utilizzo della rete pubblica Monero filterWindow.disableAutoConf=Disable auto-confirm filterWindow.autoConfExplorers=Filtered auto-confirm explorers (comma sep. addresses) filterWindow.disableTradeBelowVersion=Versione minima richiesta per il trading filterWindow.add=Aggiungi filtro filterWindow.remove=Rimuovi filtro filterWindow.xmrFeeReceiverAddresses=XMR fee receiver addresses filterWindow.disableApi=Disable API filterWindow.disableMempoolValidation=Disable Mempool Validation offerDetailsWindow.minXmrAmount=Importo XMR minimo offerDetailsWindow.min=(min. {0}) offerDetailsWindow.distance=(distanza dal prezzo di mercato: {0}) offerDetailsWindow.myTradingAccount=Il mio account di scambio offerDetailsWindow.offererBankId=(ID banca del produttore/BIC/SWIFT) offerDetailsWindow.offerersBankName=(nome della banca del maker) offerDetailsWindow.bankId=ID Banca (es. BIC o SWIFT) offerDetailsWindow.countryBank=Paese della banca del maker offerDetailsWindow.commitment=Impegno offerDetailsWindow.agree=Accetto offerDetailsWindow.tac=Termini e condizioni offerDetailsWindow.confirm.maker.buy=Conferma: Crea offerta per acquistare XMR con {0} offerDetailsWindow.confirm.maker.sell=Conferma: Crea offerta per vendere XMR in cambio di {0} offerDetailsWindow.confirm.taker.buy=Conferma: Accetta offerta per acquistare XMR con {0} offerDetailsWindow.confirm.taker.sell=Conferma: Accetta offerta per vendere XMR in cambio di {0} offerDetailsWindow.creationDate=Data di creazione offerDetailsWindow.makersOnion=Indirizzo .onion del maker offerDetailsWindow.challenge=Passphrase dell'offerta offerDetailsWindow.challenge.copy=Copia la frase segreta da condividere con il tuo interlocutore qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. qRCodeWindow.request=Richiesta di pagamento:\n{0} selectDepositTxWindow.headline=Seleziona la transazione di deposito per la disputa selectDepositTxWindow.msg=La transazione di deposito non è stata archiviata nello scambio.\nSeleziona una delle transazioni multisig esistenti dal tuo portafoglio che era la transazione di deposito utilizzata nello scambio fallito.\n\nPuoi trovare la transazione corretta aprendo la finestra dei dettagli dello scambio (fai clic sull'ID dello scambio nell'elenco) e seguendo l'output della commissione di scambio della transazione successiva in cui vedi la transazione di deposito multisig (l'indirizzo inizia con 3). Tale ID transazione dovrebbe essere visibile nell'elenco presentato qui. Una volta trovata la transazione corretta, seleziona quella transazione e continua.\n\nCi scusiamo per l'inconveniente, ma questo caso di errore dovrebbe accadere molto raramente e in futuro cercheremo di trovare modi migliori per risolverlo.\n  selectDepositTxWindow.select=Seleziona la transazione di deposito sendAlertMessageWindow.headline=Invia notifica globale sendAlertMessageWindow.alertMsg=Messaggio d'avvertimento sendAlertMessageWindow.enterMsg=Inserisci messaggio sendAlertMessageWindow.isSoftwareUpdate=Software download notification sendAlertMessageWindow.isUpdate=Is full release sendAlertMessageWindow.isPreRelease=Is pre-release sendAlertMessageWindow.version=Nuova versione numero sendAlertMessageWindow.send=Invia notifica sendAlertMessageWindow.remove=Rimuovi notifica sendPrivateNotificationWindow.headline=Invia messaggio privato sendPrivateNotificationWindow.privateNotification=Notifica privata sendPrivateNotificationWindow.enterNotification=Inserisci notifica sendPrivateNotificationWindow.send=Invia notifica privata showWalletDataWindow.walletData=Dati portafoglio showWalletDataWindow.includePrivKeys=Includi chiavi private setXMRTxKeyWindow.headline=Prove sending of XMR setXMRTxKeyWindow.note=Adding tx info below enables auto-confirm for quicker trades. See more: https://haveno.exchange/wiki/Trading_Monero setXMRTxKeyWindow.txHash=Transaction ID (optional) setXMRTxKeyWindow.txKey=Transaction key (optional) # We do not translate the tac because of the legal nature. We would need translations checked by lawyers # in each language which is too expensive atm. tacWindow.headline=Accordo per gli utenti tacWindow.agree=Accetto tacWindow.disagree=Non accetto ed esco tacWindow.arbitrationSystem=Risoluzione disputa tradeDetailsWindow.headline=Scambio tradeDetailsWindow.disputedPayoutTxId=ID transazione di pagamento contestato tradeDetailsWindow.tradeDate=Data di scambio tradeDetailsWindow.txFee=Commissione di mining tradeDetailsWindow.tradePeersOnion=Indirizzi onion peer di trading tradeDetailsWindow.tradePeersPubKeyHash=Trading peers pubkey hash tradeDetailsWindow.tradeState=Stato di scambio tradeDetailsWindow.agentAddresses=Arbitro/Mediatore tradeDetailsWindow.detailData=Detail data txDetailsWindow.headline=Transaction Details txDetailsWindow.xmr.noteSent=Hai inviato XMR. txDetailsWindow.xmr.noteReceived=Hai ricevuto XMR. txDetailsWindow.sentTo=Inviato a txDetailsWindow.receivedWith=Ricevuto con txDetailsWindow.txId=TxId closedTradesSummaryWindow.headline=Trade history summary closedTradesSummaryWindow.totalAmount.title=Total trade amount closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) closedTradesSummaryWindow.totalTradeFeeInXmr.title=Sum of all trade fees paid in XMR closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} of total trade amount) walletPasswordWindow.headline=Inserisci la password per sbloccare xmrConnectionError.headline=Errore di connessione a Monero xmrConnectionError.providedNodes=Errore durante la connessione ai nodi Monero forniti.\n\nVuoi usare il prossimo nodo Monero disponibile? xmrConnectionError.customNodes=Errore durante la connessione ai tuoi nodi Monero personalizzati.\n\nVuoi usare il prossimo nodo Monero disponibile? xmrConnectionError.localNode=Haveno era precedentemente connesso a un nodo Monero locale, ma non è più raggiungibile.\n\nAssicurati che il tuo nodo locale sia in esecuzione e completamente sincronizzato, oppure scegli un’altra opzione per continuare. xmrConnectionError.localNode.start=Avvia nodo locale xmrConnectionError.localNode.start.error=Errore durante l’avvio del nodo locale xmrConnectionError.localNode.fallback=Connetti al prossimo nodo disponibile torNetworkSettingWindow.header=Impostazioni rete Tor torNetworkSettingWindow.noBridges=Non usare bridge torNetworkSettingWindow.providedBridges=Connetti con i bridge forniti torNetworkSettingWindow.customBridges=Inserisci bridge personalizzati torNetworkSettingWindow.transportType=Tipo di trasporto torNetworkSettingWindow.obfs3=obfs3 torNetworkSettingWindow.obfs4=obfs4 (consigliato) torNetworkSettingWindow.meekAmazon=meek-amazon torNetworkSettingWindow.meekAzure=meek-azure torNetworkSettingWindow.enterBridge=Immettere uno o più bridge relays (uno per riga) torNetworkSettingWindow.enterBridgePrompt=tipo indirizzo:porta torNetworkSettingWindow.restartInfo=Devi riavviare per applicare i cambiamenti torNetworkSettingWindow.openTorWebPage=Apri il sito web del progetto Tor torNetworkSettingWindow.deleteFiles.header=Problemi di connessione? torNetworkSettingWindow.deleteFiles.info=Se si verificano ripetuti problemi di connessione all'avvio, l'eliminazione di file Tor obsoleti potrebbe essere d'aiuto. Per fare ciò, fai clic sul pulsante in basso e riavvia in seguito. torNetworkSettingWindow.deleteFiles.button=Rimuovi i file Tor obsoleti e spegni torNetworkSettingWindow.deleteFiles.progress=Spegnimento Tor in corso torNetworkSettingWindow.deleteFiles.success=File obsoleti di Tor eliminati con successo. Riavvia. torNetworkSettingWindow.bridges.header=Tor è bloccato? torNetworkSettingWindow.bridges.info=Se Tor è bloccato dal tuo provider di servizi Internet o dal tuo paese, puoi provare a utilizzare i bridge Tor.\nVisitare la pagina Web Tor all'indirizzo: https://bridges.torproject.org per ulteriori informazioni sui bridge e sui trasporti collegabili.\n  feeOptionWindow.headline=Scegli la valuta per il pagamento delle commissioni commerciali feeOptionWindow.info=Puoi scegliere di pagare la commissione commerciale in BSQ o in XMR. Se scegli BSQ approfitti della commissione commerciale scontata. feeOptionWindow.optionsLabel=Scegli la valuta per il pagamento delle commissioni commerciali feeOptionWindow.useXMR=Usa XMR feeOptionWindow.fee={0} (≈ {1}) feeOptionWindow.xmrFeeWithFiatAndPercentage={0} (≈ {1} / {2}) feeOptionWindow.xmrFeeWithPercentage={0} ({1}) #################################################################### # Popups #################################################################### popup.headline.notification=Notifica popup.headline.instruction=Nota bene: popup.headline.attention=Attenzione popup.headline.backgroundInfo=Informazioni di base popup.headline.feedback=Completato popup.headline.confirmation=Conferma popup.headline.information=Informazione popup.headline.warning=Attenzione popup.headline.error=Errore popup.doNotShowAgain=Non mostrare di nuovo popup.reportError.log=Apri file di registro popup.reportError.gitHub=Segnala sugli errori di GitHub popup.reportError={0}\n\nPer aiutarci a migliorare il software, segnala questo errore aprendo un nuova segnalazione su https://github.com/haveno-dex/haveno/issues.\nIl messaggio di errore sopra verrà copiato negli appunti quando si fa clic su uno dei pulsanti di seguito.\nFaciliterà il debug se includi il file haveno.log premendo "Apri file di registro", salvando una copia e allegandolo alla tua segnalazione di bug.\n  popup.error.tryRestart=Prova a riavviare l'applicazione e controlla la connessione di rete per vedere se riesci a risolvere il problema. popup.error.takeOfferRequestFailed=Si è verificato un errore quando qualcuno ha tentato di accettare una delle tue offerte:\n{0} error.spvFileCorrupted=Si è verificato un errore durante la lettura del file della catena SPV.\nÈ possibile che il file della catena SPV sia danneggiato.\n\nMessaggio di errore: {0}\n\nVuoi cancellarlo ed iniziare una nuova sincronizzazione? error.deleteAddressEntryListFailed=Impossibile eliminare il file AddressEntryList.\nErrore: {0} error.closedTradeWithUnconfirmedDepositTx=La transazione di deposito dello scambio chiuso con ID {0} non è ancora confermata.\n\nEffettuare una risincronizzazione SPV in \"Setting/Network info\" per verificare se la transazione è valida.\n  error.closedTradeWithNoDepositTx=La transazione di deposito dello scambio chiuso con ID {0} è nulla.\n\nRiavvia l'applicazione per ripulire l'elenco delle transazioni chiuse. popup.warning.walletNotInitialized=Il portafoglio non è ancora inizializzato popup.warning.osxKeyLoggerWarning=Due to stricter security measures in macOS 10.14 and above, launching a Java application (Haveno uses Java) causes a popup warning in macOS ('Haveno would like to receive keystrokes from any application').\n\nTo avoid that issue please open your 'macOS Settings' and go to 'Security & Privacy' -> 'Privacy' -> 'Input Monitoring' and Remove 'Haveno' from the list on the right side.\n\nHaveno will upgrade to a newer Java version to avoid that issue as soon the technical limitations (Java packager for the required Java version is not shipped yet) are resolved. popup.warning.wrongVersion=Probabilmente hai la versione Haveno sbagliata per questo computer.\nL'architettura del tuo computer è: {0}.\nIl binario Haveno che hai installato è: {1}.\nChiudere e reinstallare la versione corretta ({2}). popup.warning.incompatibleDB=We detected incompatible data base files!\n\nThose database file(s) are not compatible with our current code base:\n{0}\n\nWe made a backup of the corrupted file(s) and applied the default values to a new database version.\n\nThe backup is located at:\n{1}/db/backup_of_corrupted_data.\n\nPlease check if you have the latest version of Haveno installed.\nYou can download it at: [HYPERLINK:https://haveno.exchange/downloads].\n\nPlease restart the application. popup.warning.startupFailed.twoInstances=Haveno è già in esecuzione. Non è possibile eseguire due istanze di Haveno. popup.warning.tradePeriod.halfReached=Il tuo scambio con ID {0} ha raggiunto la metà del massimo periodo di negoziazione consentito e non è ancora completato.\n\nIl periodo di scambio termina il {1}\n\nPer ulteriori informazioni, controllare lo stato dello scambio in \"Portafoglio/Scambi aperti\". popup.warning.tradePeriod.ended= \nIl tuo scambio con ID {0} ha raggiunto il limite massimo del periodo di scambio consentito e non è stato completato.\n\nIl periodo di scambio è terminato il {1}\n\nPer favore verifica il tuo trade su \"Portafoglio/Scambi aperti\" per contattare il mediatore. popup.warning.noTradingAccountSetup.headline=Non hai impostato un account di trading popup.warning.noTradingAccountSetup.msg=È necessario impostare un conto in valuta nazionale o crypto prima di poter creare un'offerta.\nVuoi configurare un conto? popup.warning.noArbitratorsAvailable=Non ci sono arbitri disponibili. popup.warning.noMediatorsAvailable=Non ci sono mediatori disponibili. popup.warning.notFullyConnected=È necessario attendere fino a quando non si è completamente connessi alla rete.\nQuesto potrebbe richiedere fino a circa 2 minuti all'avvio. popup.warning.notSufficientConnectionsToXmrNetwork=Devi aspettare fino a quando non hai almeno {0} connessioni alla rete Monero. popup.warning.downloadNotComplete=Devi aspettare fino al completamento del download dei blocchi Monero mancanti. popup.warning.walletNotSynced=Il portafoglio Haveno non è sincronizzato con l'altezza più recente della blockchain. Si prega di attendere finché il portafoglio si sincronizza o controllare la connessione. popup.warning.removeOffer=Sei sicuro di voler rimuovere quell'offerta? popup.warning.tooLargePercentageValue=Non è possibile impostare una percentuale del 100% o superiore. popup.warning.examplePercentageValue=Inserisci un numero percentuale come \"5.4\" per il 5,4% popup.warning.noPriceFeedAvailable=Non è disponibile alcun feed di prezzi per la valuta. Non è possibile utilizzare un prezzo basato su percentuale.\nSeleziona il prezzo fisso. popup.warning.sendMsgFailed=Invio del messaggio al tuo partner commerciale non riuscito.\nTi preghiamo di riprovare e se continua a fallire segnalare un bug. popup.warning.messageTooLong=Il tuo messaggio supera la dimensione massima consentita. Si prega di inviarlo in più parti o caricarlo su un servizio come https://pastebin.com. popup.warning.lockedUpFunds=Hai bloccato i fondi da uno scambio fallito.\nSaldo bloccato: {0}\nIndirizzo tx deposito: {1}\nID scambio: {2}.\n\nApri un ticket di supporto selezionando lo scambio nella schermata degli scambi aperti e premendo \"alt + o\" o \"option + o\"." popup.warning.moneroConnection=Si è verificato un problema durante la connessione alla rete Monero.\n\n{0} popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n takeOffer.cancelButton=Cancel take-offer takeOffer.warningButton=Ignore and continue anyway # suppress inspection "UnusedProperty" popup.warning.nodeBanned=One of the {0} nodes got banned. # suppress inspection "UnusedProperty" popup.warning.priceRelay=ripetitore di prezzo popup.warning.seed=seme popup.warning.mandatoryUpdate.trading=Si prega di aggiornare Haveno all'ultima versione. È stato rilasciato un aggiornamento obbligatorio che disabilita il trading per le vecchie versioni. Per ulteriori informazioni, consultare il forum Haveno. popup.warning.noFilter=Non abbiamo ricevuto un oggetto filtro dai nodi seme. Si prega di informare gli amministratori di rete di registrare un oggetto filtro. popup.warning.burnXMR=Questa transazione non è possibile, poiché le commissioni di mining di {0} supererebbero l'importo da trasferire di {1}. Attendi fino a quando le commissioni di mining non saranno nuovamente basse o fino a quando non avrai accumulato più XMR da trasferire. popup.warning.openOffer.makerFeeTxRejected=La commissione della transazione del creatore dell'offerta con ID {0} è stata rifiutata dalla rete Monero.\nTransazione ID={1}.\nL'offerta è stata rimossa per evitare ulteriori problemi.\nVai su \"Impostazioni/Informazioni di rete\" ed esegui una risincronizzazione SPV.\nPer ulteriore assistenza, contattare il canale di supporto Haveno nel team di Haveno Keybase. popup.warning.trade.txRejected.tradeFee=commissione di scambio popup.warning.trade.txRejected.deposit=deposita popup.warning.trade.txRejected=The {0} transaction for trade with ID {1} was rejected by the Monero network.\nTransaction ID={2}\nThe trade has been moved to failed trades.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. popup.warning.openOfferWithInvalidMakerFeeTx=La commissione della transazione del creatore dell'offerta con ID {0} non è valida.\nTransazione ID={1}.\nVai su \"Impostazioni/Informazioni di rete\" ed esegui una risincronizzazione SPV.\nPer ulteriore assistenza, contattare il canale di supporto Haveno nel team di Haveno Keybase. popup.info.securityDepositInfo=Per garantire che i trader seguano il protocollo di scambio, entrambi devono pagare un deposito cauzionale.\n\nQuesto deposito viene conservato nel tuo portafoglio di scambio fino a quando la tua transazione non è stata completata con successo, quindi ti viene rimborsato.\n\nNota: se stai creando una nuova offerta, Haveno deve essere in esecuzione per essere accettato da un altro trader. Per mantenere le tue offerte online, mantieni Haveno in funzione e assicurati che anche questo computer rimanga online (ad esempio, assicurati che non passi alla modalità standby ... monitor standby va bene). popup.info.cashDepositInfo=Assicurati di avere una filiale bancaria nella tua zona per poter effettuare il deposito in contanti.\nL'ID bancario (BIC/SWIFT) della banca del venditore è: {0}. popup.info.cashDepositInfo.confirm=Confermo di poter effettuare il deposito popup.info.shutDownWithOpenOffers=Haveno viene chiuso, ma ci sono offerte aperte.\n\nQueste offerte non saranno disponibili sulla rete P2P mentre Haveno rimane chiuso, ma verranno ripubblicate sulla rete P2P al prossimo avvio di Haveno.\n\nPer mantenere le tue offerte attive è necessario che Haveno rimanga in funzione ed il computer online (assicurati che non vada in modalità standby. Il solo monitor in standby non è un problema). popup.info.qubesOSSetupInfo=It appears you are running Haveno on Qubes OS. \n\nPlease make sure your Haveno qube is setup according to our Setup Guide at [HYPERLINK:https://haveno.exchange/wiki/Running_Haveno_on_Qubes]. popup.warn.downGradePrevention=Downgrade from version {0} to version {1} is not supported. Please use the latest Haveno version. popup.privateNotification.headline=Notifica privata importante! popup.securityRecommendation.headline=Raccomandazione di sicurezza importante popup.securityRecommendation.msg=Vorremmo ricordarti di prendere in considerazione l'utilizzo della protezione con password per il tuo portafoglio se non l'avessi già abilitato.\n\nSi consiglia inoltre di annotare le parole seme del portafoglio. Le parole seme sono come una password principale per recuperare il tuo portafoglio Monero.\nNella sezione \"Wallet Seed\" trovi ulteriori informazioni.\n\nInoltre, è necessario eseguire il backup della cartella completa dei dati dell'applicazione nella sezione \"Backup\". popup.shutDownInProgress.headline=Arresto in corso popup.shutDownInProgress.msg=La chiusura dell'applicazione può richiedere un paio di secondi.\nNon interrompere il processo. popup.attention.forTradeWithId=Attenzione richiesta per gli scambi con ID {0} popup.attention.reasonForPaymentRuleChange=Version 1.5.5 introduces a critical trade rule change regarding the \"reason for payment\" field in bank transfers. Please leave this field empty -- DO NOT use the trade ID as \"reason for payment\" anymore. popup.info.multiplePaymentAccounts.headline=Disponibili più conti di pagamento popup.info.multiplePaymentAccounts.msg=Hai più account di pagamento disponibili per questa offerta. Assicurati di aver scelto quello giusto. popup.accountSigning.selectAccounts.headline=Seleziona conti di pagamento popup.accountSigning.selectAccounts.description=In base al metodo di pagamento e al momento in cui verranno selezionati tutti i conti di pagamento collegati a una controversia in cui si è verificato un pagamento popup.accountSigning.selectAccounts.signAll=Firma tutti i metodi di pagamento popup.accountSigning.selectAccounts.datePicker=Seleziona il momento in cui verranno firmati gli account popup.accountSigning.confirmSelectedAccounts.headline=Conferma account di pagamento selezionati popup.accountSigning.confirmSelectedAccounts.description=In base ai dati inseriti, verranno selezionati {0} account di pagamento. popup.accountSigning.confirmSelectedAccounts.button=Conferma conti di pagamento popup.accountSigning.signAccounts.headline=Conferma la firma dei conti di pagamento popup.accountSigning.signAccounts.description=In base alla tua selezione, verranno firmati {0} account di pagamento. popup.accountSigning.signAccounts.button=Firma conti di pagamento popup.accountSigning.signAccounts.ECKey=Immettere la chiave dell'arbitro privato popup.accountSigning.signAccounts.ECKey.error=ECKey dell'arbitro errata popup.accountSigning.success.headline=Congratulazioni popup.accountSigning.success.description=Tutti gli account di pagamento {0} sono stati firmati correttamente! popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\nFor further information, please visit [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. popup.accountSigning.signedByArbitrator=Uno dei tuoi conti di pagamento è stato verificato e firmato da un arbitro. Il trading con questo account firmerà automaticamente l'account del tuo peer di trading dopo una negoziazione di successo.\n\n{0} popup.accountSigning.signedByPeer=Uno dei tuoi conti di pagamento è stato verificato e firmato da un peer di trading. Il limite di trading iniziale verrà revocato e sarai in grado di firmare altri account tra {0} giorni da adesso.\n\n{1} popup.accountSigning.peerLimitLifted=Il limite iniziale per uno dei tuoi account è stato revocato.\n\n{0} popup.accountSigning.peerSigner=Uno dei tuoi account è abbastanza maturo per firmare altri account di pagamento e il limite iniziale per uno dei tuoi account è stato revocato.\n\n{0} popup.accountSigning.singleAccountSelect.headline=Import unsigned account age witness popup.accountSigning.confirmSingleAccount.headline=Confirm selected account age witness popup.accountSigning.confirmSingleAccount.selectedHash=Selected witness hash popup.accountSigning.confirmSingleAccount.button=Sign account age witness popup.accountSigning.successSingleAccount.description=Witness {0} was signed popup.accountSigning.successSingleAccount.success.headline=Success popup.accountSigning.unsignedPubKeys.headline=Unsigned Pubkeys popup.accountSigning.unsignedPubKeys.sign=Sign Pubkeys popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign popup.info.buyerAsTakerWithoutDeposit.headline=Nessun deposito richiesto dal compratore popup.info.buyerAsTakerWithoutDeposit=La tua offerta non richiederà un deposito di sicurezza o una commissione da parte dell'acquirente XMR.\n\nPer accettare la tua offerta, devi condividere una passphrase con il tuo partner commerciale al di fuori di Haveno.\n\nLa passphrase viene generata automaticamente e mostrata nei dettagli dell'offerta dopo la creazione. #################################################################### # Notifications #################################################################### notification.trade.headline=Notifica per scambi con ID {0} notification.ticket.headline=Biglietto di supporto per scambi con ID {0} notification.trade.completed=Il commercio è ora completato e puoi ritirare i tuoi fondi. notification.trade.accepted=La tua offerta è stata accettata da un XMR {0}. notification.trade.unlocked=Il tuo trade ha almeno una conferma blockchain.\nPuoi iniziare il pagamento ora. notification.trade.paymentSent=L'acquirente XMR ha avviato il pagamento. notification.trade.selectTrade=Seleziona scambio notification.trade.peerOpenedDispute=Il tuo peer di trading ha aperto un {0}. notification.trade.disputeClosed={0} è stato chiuso. notification.walletUpdate.headline=Aggiornamento del portafoglio di trading notification.walletUpdate.msg=Il tuo portafoglio di trading è sufficientemente finanziato.\nImporto: {0} notification.takeOffer.walletUpdate.msg=Il tuo portafoglio di trading era già sufficientemente finanziato da un precedente tentativo di offerta.\nImporto: {0} notification.tradeCompleted.headline=Scambio completato notification.tradeCompleted.msg=Puoi prelevare i tuoi fondi sul tuo portafoglio Monero esterno o mantenerli nel tuo portafoglio Haveno. #################################################################### # System Tray #################################################################### systemTray.show=Mostra la finestra dell'applicazione systemTray.hide=Nascondi la finestra dell'applicazione systemTray.info=Informazioni su Haveno systemTray.exit=Esci systemTray.tooltip=Haveno: una rete di scambio decentralizzata di monero #################################################################### # GUI Util #################################################################### guiUtil.accountExport.savedToPath=Account di trading salvati nel percorso:\n{0} guiUtil.accountExport.noAccountSetup=Non hai account di trading impostati per l'esportazione. guiUtil.accountExport.selectPath=Seleziona il percorso per {0} # suppress inspection "TrailingSpacesInProperty" guiUtil.accountExport.tradingAccount=Account di trading con id {0}\n # suppress inspection "TrailingSpacesInProperty" guiUtil.accountImport.noImport=Non abbiamo importato un account di trading con ID {0} perché esiste già. guiUtil.accountExport.exportFailed=L'esportazione CSV non è andata a buon fine a causa di un errore.\nErrore = {0} guiUtil.accountExport.selectExportPath=Seleziona il percorso di esportazione guiUtil.accountImport.imported=Conto di trading importato dalla cartella:\n{0}\n\nAccount importati:\n{1} guiUtil.accountImport.noAccountsFound=Non è stato trovato alcun conto di trading esportato nella cartella: {0}.\nIl nome del file è {1}." guiUtil.openWebBrowser.warning=Stai per aprire una pagina web nel tuo browser.\nVuoi aprire la pagina web adesso?\n\nSe non si utilizza \"Tor Browser\" come Web browser predefinito, ci si collegherà alla pagina Web in chiaro.\n\nURL: \"{0}\" guiUtil.openWebBrowser.doOpen=Apri la pagina web e non chiedere più guiUtil.openWebBrowser.copyUrl=Copia URL e annulla guiUtil.ofTradeAmount=di importo commerciale guiUtil.requiredMinimum=(minimo richiesto) #################################################################### # Component specific #################################################################### list.currency.select=Seleziona valuta list.currency.showAll=Mostra tutto list.currency.editList=Modifica elenco valuta table.placeholder.noItems=Attualmente non ci sono {0} disponibili table.placeholder.noData=Attualmente non ci sono dati disponibili table.placeholder.processingData=Elaborazione dei dati... peerInfoIcon.tooltip.tradePeer=Peer di scambio peerInfoIcon.tooltip.maker=Del Maker peerInfoIcon.tooltip.trade.traded={0} Indirizzo onion: {1}\nHai già scambiato {2} volte in quel peer\n{3} peerInfoIcon.tooltip.trade.notTraded={0} Indirizzo onion: {1}\nFinora non hai commerciato con quel peer.\n{2} peerInfoIcon.tooltip.age=Conto di pagamento creato {0} fa. peerInfoIcon.tooltip.unknownAge=Età dell'account di pagamento non nota. tooltip.openPopupForDetails=Apri il popup per i dettagli tooltip.invalidTradeState.warning=This trade is in an invalid state. Open the details window for more information tooltip.openBlockchainForAddress=Apri Explorer blockchain esterno per indirizzo: {0} tooltip.openBlockchainForTx=Apri Explorer blockchain esterno per la transazione: {0} confidence.unknown=Stato della transazione sconosciuto confidence.seen=Visto da {0} conferme peer / 0 confidence.confirmed={0} conferma(e) confidence.invalid=La transazione non è valida peerInfo.title=Info peer peerInfo.nrOfTrades=Numero di scambi effettuati peerInfo.notTradedYet=Finora non hai commerciato con quell'utente. peerInfo.setTag=Imposta tag per questo peer peerInfo.age.noRisk=Età del conto di pagamento peerInfo.age.chargeBackRisk=Tempo dall'iscrizione peerInfo.unknownAge=Età sconosciuta addressTextField.openWallet=Apri il tuo portafoglio Monero predefinito addressTextField.copyToClipboard=Copia l'indirizzo negli appunti addressTextField.addressCopiedToClipboard=L'indirizzo è stato copiato negli appunti addressTextField.openWallet.failed=Il tentativo di aprire un portafoglio monero predefinito è fallito. Forse non ne hai installato uno? peerInfoIcon.tooltip={0}\nTag: {1} txIdTextField.copyIcon.tooltip=Copia l'ID transazione negli appunti txIdTextField.blockExplorerIcon.tooltip=Open a blockchain explorer with this transaction ID txIdTextField.missingTx.warning.tooltip=Missing required transaction #################################################################### # Navigation #################################################################### navigation.account=\"Conto\" navigation.account.walletSeed=\"Conto/Seed wallet\" navigation.funds.availableForWithdrawal=\"Funds/Send funds\" navigation.portfolio.myOpenOffers=\"Portfolio/Le mie offerte aperte\" navigation.portfolio.pending=\"Portafoglio/Scambi aperti\" navigation.portfolio.closedTrades=\"Portafoglio/Storia\" navigation.funds.depositFunds=\"Fondi/Ricevifondi\" navigation.settings.preferences=\"Impostazioni/Preferenze\" # suppress inspection "UnusedProperty" navigation.funds.transactions=\"Fondi/Transazioni\" navigation.support=\"Supporto\" #################################################################### # Formatter #################################################################### formatter.formatVolumeLabel={0} importo{1} formatter.makerTaker=Maker come {0} {1} / Taker come {2} {3} formatter.makerTaker.locked=Maker come {0} {1} / Taker come {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=Sei {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Stai creando un'offerta per {0} {1} formatter.youAreCreatingAnOffer.traditional.locked=Stai creando un'offerta per {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Stai creando un'offerta per {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.crypto.locked=Stai creando un'offerta per {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} come maker formatter.asTaker={0} {1} come taker #################################################################### # Domain specific #################################################################### # we use enum values here # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" XMR_MAINNET=Mainnet Monero # suppress inspection "UnusedProperty" XMR_LOCAL=Testnet Monero # suppress inspection "UnusedProperty" XMR_STAGENET=Regtest Monero time.year=Anno time.month=Mese time.week=Settimana time.day=Giorno time.hour=Ora time.minute10=10 Minuti time.hours=ore time.days=giorni time.1hour=1 ora time.1day=1 giorno time.minute=minuto time.second=secondo time.minutes=minuti time.seconds=secondi password.enterPassword=Inserisci password password.confirmPassword=Conferma password password.tooLong=La password deve contenere meno di 500 caratteri. password.deriveKey=Deriva la chiave dalla password password.walletDecrypted=Portafoglio decodificato correttamente e protezione con password rimossa. password.wrongPw=Hai inserito la password errata.\n\nProva a inserire nuovamente la password, verificando attentamente errori di battitura o errori di ortografia. password.walletEncrypted=Portafoglio crittografato correttamente e protezione con password abilitata. password.passwordsDoNotMatch=Le 2 password inserite non corrispondono. password.forgotPassword=Password dimenticata? password.backupReminder=Please note that when setting a wallet password all automatically created backups from the unencrypted wallet will be deleted.\n\nIt is highly recommended that you make a backup of the application directory and write down your seed words before setting a password! password.backupWasDone=I have already made a backup password.setPassword=Set Password (I already made a backup) password.makeBackup=Make Backup seed.seedWords=Parole seed del portafoglio seed.enterSeedWords=Inserisci le parole del seed del portafoglio seed.date=Data portafoglio seed.restore.title=Ripristina portafogli dalle parole del seed seed.restore=Ripristina portafogli seed.creationDate=Data di creazione seed.warn.walletNotEmpty.msg=Your Monero wallet is not empty.\n\nYou must empty this wallet before attempting to restore an older one, as mixing wallets together can lead to invalidated backups.\n\nPlease finalize your trades, close all your open offers and go to the Funds section to withdraw your monero.\nIn case you cannot access your monero you can use the emergency tool to empty the wallet.\nTo open the emergency tool press \"Alt+e\" or \"Cmd/Ctrl+e\". seed.warn.walletNotEmpty.restore=Voglio comunque effettuare il ripristino seed.warn.walletNotEmpty.emptyWallet=Prima svuoterò i miei portafogli seed.warn.notEncryptedAnymore=I tuoi portafogli sono crittografati.\n\nDopo il ripristino, i portafogli non saranno più crittografati ed è necessario impostare una nuova password.\n\nVuoi procedere? seed.warn.walletDateEmpty=As you have not specified a wallet date, haveno will have to scan the blockchain from 2013.10.09 (the BIP39 epoch date).\n\nBIP39 wallets were first introduced in haveno on 2017.06.28 (release v0.5). So you could save time by using that date.\n\nIdeally you should specify the date your wallet seed was created.\n\n\nAre you sure you want to go ahead without specifying a wallet date? seed.restore.success=Portafogli ripristinati con successo con le nuove parole mnemoniche.\n\nÈ necessario arrestare e riavviare l'applicazione. seed.restore.error=Si è verificato un errore durante il ripristino dei portafogli con le parole del seme. {0} seed.restore.openOffers.warn=You have open offers which will be removed if you restore from seed words.\nAre you sure that you want to continue? #################################################################### # Payment methods #################################################################### payment.account=Account payment.account.no=Account n° payment.account.name=Nome conto payment.account.username=Username payment.account.phoneNr=Phone number payment.account.owner.fullname=Nome completo del proprietario del conto payment.account.fullName=Nome completo (nome, secondo nome, cognome) payment.account.state=Stato/Provincia/Regione payment.account.city=Città payment.bank.country=Paese della banca payment.account.name.email=Nome completo / email del proprietario dell'account payment.account.name.emailAndHolderId=Nome completo del proprietario dell'account / email / {0} payment.bank.name=Nome Banca payment.select.account=Seleziona il tipo di account payment.select.region=Seleziona regione payment.select.country=Seleziona nazione payment.select.bank.country=Seleziona il paese della banca payment.foreign.currency=Sei sicuro di voler scegliere una valuta diversa dalla valuta predefinita del paese? payment.restore.default=No, ripristina valuta predefinita payment.email=Email payment.country=Paese payment.extras=Requisiti extra payment.email.mobile=Email o numero di telefono cellulare payment.crypto.address=Indirizzo crypto payment.crypto.tradeInstantCheckbox=Fai trading istantaneo (entro 1 ora) con questa Crypto payment.crypto.tradeInstant.popup=Per il trading istantaneo è necessario che entrambi i peer di trading siano online per poter completare lo scambio in meno di 1 ora.\n\nSe le tue offerte sono aperte ma non sei momentaneamente disponibile, disabilita tali offerte nella schermata "Portafoglio". payment.crypto=Crypto payment.select.crypto=Select or search Crypto payment.secret=Domanda segreta payment.answer=Risposta payment.wallet=ID portafoglio payment.amazon.site=Buy giftcard at payment.ask=Ask in Trader Chat payment.uphold.accountId=Nome utente o e-mail o n. di telefono payment.moneyBeam.accountId=Email o numero di telefono fisso payment.popmoney.accountId=Email o numero di telefono fisso payment.promptPay.promptPayId=Codice fiscale/P.IVA o n. di telefono payment.supportedCurrencies=Valute supportate payment.supportedCurrenciesForReceiver=Currencies for receiving funds payment.limitations=Limitazioni payment.salt=Sale per la verifica dell'età dell'account payment.error.noHexSalt=The salt needs to be in HEX format.\nIt is only recommended to edit the salt field if you want to transfer the salt from an old account to keep your account age. The account age is verified by using the account salt and the identifying account data (e.g. IBAN). payment.accept.euro=Accetta operazioni da questi paesi dell'Euro payment.accept.nonEuro=Accetta operazioni da questi paesi non Euro payment.accepted.countries=Paesi accettati payment.accepted.banks=Banche accettate (ID) payment.mobile=N. di cellulare payment.postal.address=Indirizzo postale payment.national.account.id.AR=Numero CBU shared.accountSigningState=Stato della firma dell'account #new payment.crypto.address.dyn={0} indirizzi payment.crypto.receiver.address=Indirizzo crypto del destinatario payment.accountNr=Numero conto payment.emailOrMobile=Email o numero di telefono cellulare payment.useCustomAccountName=Usa nome dell'account personalizzato payment.maxPeriod=Max. periodo di scambio consentito payment.maxPeriodAndLimit=Max. durata dello scambio: {0} / Max. acquisto: {1} / Max. vendita: {2} / Età dell'account: {3} payment.maxPeriodAndLimitCrypto=Max. durata commerciale: {0} / Max. limite commerciale: {1} payment.currencyWithSymbol=Valuta: {0} payment.nameOfAcceptedBank=Nome della banca accettata payment.addAcceptedBank=Aggiungi banca accettata payment.clearAcceptedBanks=Cancella banche accettate payment.bank.nameOptional=Nome della banca (opzionale) payment.bankCode=Codice bancario payment.bankId=ID banca (BIC/SWIFT) payment.bankIdOptional=ID banca (BIC/SWIFT) (opzionale) payment.branchNr=Filiale n. payment.branchNrOptional=Filiale n. (opzionale) payment.accountNrLabel=Conto n. (IBAN) payment.accountType=Tipologia conto payment.checking=Verifica payment.savings=Risparmi payment.personalId=ID personale payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle's somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. payment.fasterPayments.newRequirements.info=Alcune banche hanno iniziato a verificare il nome completo del destinatario per i trasferimenti di Faster Payments (UK). Il tuo attuale account Faster Payments non specifica un nome completo.\n\nTi consigliamo di ricreare il tuo account Faster Payments in Haveno per fornire ai futuri acquirenti {0} un nome completo.\n\nQuando si ricrea l'account, assicurarsi di copiare il codice di ordinamento preciso, il numero di account e i valori salt della verifica dell'età dal vecchio account al nuovo account. Ciò garantirà il mantenimento dell'età del tuo account esistente e lo stato della firma.\n  payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=Quando utilizza HalCash, l'acquirente XMR deve inviare al venditore XMR il codice HalCash tramite un messaggio di testo dal proprio telefono cellulare.\n\nAssicurati di non superare l'importo massimo che la tua banca ti consente di inviare con HalCash. L'importo minimo per prelievo è di 10 EURO, l'importo massimo è di 600 EURO. Per prelievi ripetuti è di 3000 EURO per destinatario al giorno e 6000 EURO per destintario al mese. Verifica i limiti con la tua banca per accertarti che utilizzino gli stessi limiti indicati qui.\n\nL'importo del prelievo deve essere un multiplo di 10 EURO in quanto non è possibile prelevare altri importi da un bancomat. L'interfaccia utente nella schermata di creazione offerta e accettazione offerta modificherà l'importo XMR in modo che l'importo in EURO sia corretto. Non è possibile utilizzare il prezzo di mercato poiché l'importo in EURO cambierebbe al variare dei prezzi.\n\nIn caso di controversia, l'acquirente XMR deve fornire la prova di aver inviato gli EURO. # suppress inspection "UnusedMessageFormatParameter" payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/overview/account_limits]. # suppress inspection "UnusedProperty" payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/overview/account_limits]. payment.cashDeposit.info=Conferma che la tua banca ti consente di inviare depositi in contanti su conti di altre persone. Ad esempio, Bank of America e Wells Fargo non consentono più tali depositi. payment.revolut.info=Revolut requires the 'Username' as account ID not the phone number or email as it was the case in the past. payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a 'Username'.\nPlease enter your Revolut 'Username' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account payment.cashapp.info=Si prega di notare che Cash App ha un rischio di chargeback più elevato rispetto alla maggior parte dei bonifici bancari. payment.venmo.info=Si prega di notare che Venmo ha un rischio di chargeback più elevato rispetto alla maggior parte dei bonifici bancari. payment.paypal.info=Si prega di notare che PayPal ha un rischio di chargeback più elevato rispetto alla maggior parte dei bonifici bancari. payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account payment.payByMail.contact=Informazioni di contatto payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.f2f.contact=Informazioni di contatto payment.f2f.contact.prompt=How would you like to be contacted by the trading peer? (email address, phone number,...) payment.f2f.city=Città per l'incontro 'Faccia a faccia' payment.f2f.city.prompt=La città verrà visualizzata con l'offerta payment.shared.optionalExtra=Ulteriori informazioni opzionali payment.shared.extraInfo=Informazioni aggiuntive payment.shared.extraInfo.offer=Informazioni aggiuntive sull'offerta payment.shared.extraInfo.prompt.paymentAccount=Definisci eventuali termini, condizioni o dettagli speciali che desideri vengano visualizzati con le tue offerte per questo account di pagamento (gli utenti vedranno queste informazioni prima di accettare le offerte). payment.shared.extraInfo.prompt.offer=Definisci eventuali termini, condizioni o dettagli speciali che desideri mostrare con la tua offerta. payment.shared.extraInfo.noDeposit=Dettagli di contatto e termini dell'offerta payment.f2f.info.openURL=Apri sito web payment.f2f.offerbook.tooltip.countryAndCity=Paese e città: {0} / {1} payment.shared.extraInfo.tooltip=Ulteriori informazioni: {0} payment.japan.bank=Banca payment.japan.branch=Filiale payment.japan.account=Account payment.japan.recipient=Nome payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=Per la tua protezione, sconsigliamo vivamente di utilizzare i PIN di Paysafecard per i pagamenti.\n\n\ Le transazioni effettuate tramite PIN non possono essere verificate in modo indipendente per la risoluzione delle controversie. Se si verifica un problema, il recupero dei fondi potrebbe non essere possibile.\n\n\ Per garantire la sicurezza delle transazioni con risoluzione delle controversie, utilizza sempre metodi di pagamento che forniscono registrazioni verificabili. # We use constants from the code so we do not use our normal naming convention # dynamic values are not recognized by IntelliJ # Only translate general terms NATIONAL_BANK=Bonifico bancario nazionale SAME_BANK=Trasferimento con la stessa banca SPECIFIC_BANKS=Trasferimenti con banche specifiche US_POSTAL_MONEY_ORDER=Vaglia Postale USA CASH_DEPOSIT=Deposito contanti PAY_BY_MAIL=Pay By Mail MONEY_GRAM=MoneyGram WESTERN_UNION=Western Union F2F=Faccia a faccia (di persona) JAPAN_BANK=Japan Bank Furikomi AUSTRALIA_PAYID=Australian PayID # suppress inspection "UnusedProperty" NATIONAL_BANK_SHORT=Banche nazionali # suppress inspection "UnusedProperty" SAME_BANK_SHORT=Stessa banca # suppress inspection "UnusedProperty" SPECIFIC_BANKS_SHORT=Banche specifiche # suppress inspection "UnusedProperty" US_POSTAL_MONEY_ORDER_SHORT=US Money Order # suppress inspection "UnusedProperty" CASH_DEPOSIT_SHORT=Deposito contanti # suppress inspection "UnusedProperty" PAY_BY_MAIL_SHORT=PayByMail # suppress inspection "UnusedProperty" MONEY_GRAM_SHORT=MoneyGram # suppress inspection "UnusedProperty" WESTERN_UNION_SHORT=Western Union # suppress inspection "UnusedProperty" F2F_SHORT=F2F # suppress inspection "UnusedProperty" JAPAN_BANK_SHORT=Japan Furikomi # suppress inspection "UnusedProperty" AUSTRALIA_PAYID_SHORT=PayID # Do not translate brand names # suppress inspection "UnusedProperty" UPHOLD=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY=Popmoney # suppress inspection "UnusedProperty" REVOLUT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY=WeChat Pay # suppress inspection "UnusedProperty" SEPA=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT=Pagamenti istantanei SEPA # suppress inspection "UnusedProperty" FASTER_PAYMENTS=Faster Payments # suppress inspection "UnusedProperty" SWISH=Swish # suppress inspection "UnusedProperty" ZELLE=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS=Crypto # suppress inspection "UnusedProperty" PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT=Crypto Instant # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY=OKPay # suppress inspection "UnusedProperty" CASH_APP=Cash App # suppress inspection "UnusedProperty" VENMO=Venmo # suppress inspection "UnusedProperty" UPHOLD_SHORT=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM_SHORT=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY_SHORT=Popmoney # suppress inspection "UnusedProperty" REVOLUT_SHORT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY_SHORT=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY_SHORT=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY_SHORT=WeChat Pay # suppress inspection "UnusedProperty" SEPA_SHORT=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT_SHORT=SEPA Istantaneo # suppress inspection "UnusedProperty" FASTER_PAYMENTS_SHORT=Faster Payments # suppress inspection "UnusedProperty" SWISH_SHORT=Swish # suppress inspection "UnusedProperty" ZELLE_SHORT=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY_SHORT=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER_SHORT=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH_SHORT=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS_SHORT=Crypto # suppress inspection "UnusedProperty" PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT_SHORT=Crypto Instant # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY_SHORT=OKPay # suppress inspection "UnusedProperty" CASH_APP_SHORT=Cash App # suppress inspection "UnusedProperty" VENMO_SHORT=Venmo #################################################################### # Validation #################################################################### validation.empty=Un input vuoto non è consentito. validation.NaN=L'input non è un numero valido. validation.notAnInteger=L'input non è un valore intero. validation.zero=Un input di 0 non è consentito. validation.negative=Un valore negativo non è consentito. validation.traditional.tooSmall=Non è consentito un input inferiore al minimo possibile. validation.traditional.tooLarge=Non è consentito un input maggiore del massimo possibile. validation.xmr.fraction=Input will result in a monero value of less than 1 satoshi validation.xmr.tooLarge=L'immissione maggiore di {0} non è consentita. validation.xmr.tooSmall=L'immissione inferiore a {0} non è consentita. validation.passwordTooShort=The password you entered is too short. It needs to have a min. of 8 characters. validation.passwordTooLong=La password inserita è troppo lunga. Non può contenere più di 50 caratteri. validation.sortCodeNumber={0} deve essere composto da {1} numeri. validation.sortCodeChars={0} deve essere composto da {1} caratteri. validation.bankIdNumber={0} deve essere composto da {1} numeri. validation.accountNr=Il numero di conto deve essere composto da {0} numeri. validation.accountNrChars=Il numero di conto deve contenere {0} caratteri. validation.xmr.invalidAddress=L'indirizzo non è corretto Si prega di controllare il formato dell'indirizzo. validation.integerOnly=Inserisci solo numeri interi. validation.inputError=Il tuo input ha causato un errore:\n{0} validation.xmr.exceedsMaxTradeLimit=Il tuo limite commerciale è {0}. validation.nationalAccountId={0} deve essere composto da {1} numeri. #new validation.invalidInput=Input non valido: {0} validation.accountNrFormat=Il numero di conto deve essere nel formato: {0} # suppress inspection "UnusedProperty" validation.crypto.wrongStructure=Convalida dell'indirizzo non riuscita perché non corrisponde alla struttura di un indirizzo {0}. # suppress inspection "UnusedProperty" validation.crypto.ltz.zAddressesNotSupported=LTZ address must start with L. Addresses starting with z are not supported. # suppress inspection "UnusedProperty" validation.crypto.zAddressesNotSupported=ZEC addresses must start with t. Addresses starting with z are not supported. # suppress inspection "UnusedProperty" validation.crypto.invalidAddress=L'indirizzo non è un indirizzo {0} valido! {1} # suppress inspection "UnusedProperty" validation.crypto.liquidBitcoin.invalidAddress=Gli indirizzi segwit nativi (quelli che iniziano con 'lq') non sono supportati. validation.bic.invalidLength=Input length must be 8 or 11 validation.bic.letters=Il codice bancario e quello nazionale devono essere lettere validation.bic.invalidLocationCode=BIC contiene un codice di posizione non valido validation.bic.invalidBranchCode=BIC contiene un codice di filiale non valido validation.bic.sepaRevolutBic=Gli account Revolut Sepa non sono supportati. validation.btc.invalidFormat=Invalid format for a Bitcoin address. validation.email.invalidAddress=Indirizzo non valido validation.iban.invalidCountryCode=Codice paese non valido validation.iban.checkSumNotNumeric=Il checksum deve essere numerico validation.iban.nonNumericChars=Rilevato carattere non alfanumerico validation.iban.checkSumInvalid=Il checksum IBAN non è valido validation.iban.invalidLength=Number must have a length of 15 to 34 chars. validation.interacETransfer.invalidAreaCode=Prefisso non canadese validation.interacETransfer.invalidPhone=Please enter a valid 11 digit phone number (ex: 1-123-456-7890) or an email address validation.interacETransfer.invalidQuestion=Deve contenere solo lettere, numeri, spazi e / o i simboli ' _ , . ? - validation.interacETransfer.invalidAnswer=Deve essere una parola e contenere solo lettere, numeri e/o il simbolo - validation.inputTooLarge=L'input non deve essere maggiore di {0} validation.inputTooSmall=L'input deve essere maggiore di {0} validation.inputToBeAtLeast=L'input deve essere almeno di {0} validation.amountBelowDust=An amount below the dust limit of {0} satoshi is not allowed. validation.length=La lunghezza deve essere compresa tra {0} e {1} validation.fixedLength=Length must be {0} validation.pattern=L'input deve essere nel formato: {0} validation.noHexString=L'input non è in formato HEX. validation.advancedCash.invalidFormat=Deve essere un ID e-mail o portafoglio valido del formato: X000000000000 validation.invalidUrl=Questo URL non è valido validation.mustBeDifferent=L'input deve essere diverso dal valore corrente validation.cannotBeChanged=Il parametro non può essere modificato validation.numberFormatException=Eccezione formato numero {0} validation.mustNotBeNegative=L'input non deve essere negativo validation.phone.missingCountryCode=È necessario un codice paese di due lettere per convalidare il numero di telefono validation.phone.invalidCharacters=Il numero di telefono {0} contiene caratteri non validi validation.phone.insufficientDigits=There are not enough digits in {0} to be a valid phone number validation.phone.tooManyDigits=There are too many digits in {0} to be a valid phone number validation.phone.invalidDialingCode=Country dialing code for number {0} is invalid for country {1}. The correct dialing code is {2}. validation.invalidAddressList=Deve essere un elenco separato da virgole di indirizzi validi ================================================ FILE: core/src/main/resources/i18n/displayStrings_ja.properties ================================================ # Keep display strings organized by domain # Naming convention: We use camelCase and dot separated name spaces. # Use as many sub spaces as required to make the structure clear, but as little as possible. # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break # in the display but only in the editor. # Please use in all language files the exact same order of the entries, that way a comparison is easier. # Please try to keep the length of the translated string similar to English. If it is longer it might break layout or # get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. #################################################################### # Shared #################################################################### shared.readMore=続きを読む shared.openHelp=ヘルプ shared.warning=注意 shared.close=閉じる shared.cancel=キャンセル shared.ok=OK shared.yes=はい shared.no=いいえ shared.iUnderstand=了解 shared.na=N/A shared.shutDown=終了 shared.reportBug=Githubでバグを報告 shared.buyMonero=ビットコインを買う shared.sellMonero=ビットコインを売る shared.buyCurrency={0}を買う shared.sellCurrency={0}を売る shared.buyCurrency.locked={0}を買う 🔒 shared.sellCurrency.locked={0}を売る 🔒 shared.buyingXMRWith=XMRを{0}で買う shared.sellingXMRFor=XMRを{0}で売る shared.buyingCurrency={0}を購入中 (XMRを売却中) shared.sellingCurrency={0}を売却中 (XMRを購入中) shared.buy=買う shared.sell=売る shared.buying=購入中 shared.selling=売却中 shared.P2P=P2P shared.oneOffer=オファー shared.multipleOffers=オファー shared.Offer=オファー shared.offerVolumeCode={0} オファー量 shared.openOffers=オープンオファー shared.trade=トレード shared.trades=トレード shared.openTrades=オープントレード shared.dateTime=日付/時間 shared.price=価格 shared.priceWithCur={0}の価格 shared.priceInCurForCur=1{1}当りの{0}の価格 shared.fixedPriceInCurForCur=1 {1}あたりの{0}で価格を固定 shared.amount=金額 shared.txFee=トランザクション手数料 shared.tradeFee=トレード手数料 shared.buyerSecurityDeposit=買い手敷金 shared.sellerSecurityDeposit=売り手敷金 shared.amountWithCur={0}の金額 shared.volumeWithCur={0}取引高 shared.currency=通貨 shared.market=相場 shared.deviation=偏差 shared.paymentMethod=支払い方法 shared.tradeCurrency=取引通貨 shared.offerType=オファーの種類 shared.details=詳細 shared.address=アドレス shared.balanceWithCur={0}での残高 shared.utxo=Unspent transaction output shared.txId=トランザクションID shared.confirmations=確認 shared.revert=トランザクションを取り消す shared.select=選択 shared.usage=使用量 shared.state=ステータス shared.tradeId=トレードID shared.offerId=オファーID shared.bankName=銀行名 shared.acceptedBanks=利用可能な銀行 shared.amountMinMax=金額(下限 - 上限) shared.amountHelp=オファーに最小金額と最大金額が設定されている場合は、この範囲内の任意の金額で取引できます。 shared.remove=取り消す shared.goTo={0} へ shared.XMRMinMax=XMR (下限 - 上限) shared.removeOffer=オファー取消 shared.dontRemoveOffer=オファー取り消さない shared.editOffer=オファーを編集 shared.openLargeQRWindow=大きいQRコードウィンドウを開く shared.tradingAccount=取引アカウント shared.faq=FAQを参照する shared.yesCancel=はい、取り消します shared.nextStep=次へ shared.selectTradingAccount=取引アカウントを選択 shared.fundFromSavingsWalletButton=Havenoウォレットから資金を適用 shared.fundFromExternalWalletButton=外部のwalletを開く shared.openDefaultWalletFailed=ビットコインウォレットのアプリを開けませんでした。インストールされているか確認して下さい。 shared.belowInPercent=市場価格から%以下 shared.aboveInPercent=市場価格から%以上 shared.enterPercentageValue=%を入力 shared.OR=または shared.notEnoughFunds=このトランザクションには、Havenoウォレットに資金が足りません。\n{0}が必要ですが、Havenoウォレットには{1}しかありません。\n\n外部のビットコインウォレットから入金するか、または「資金 > 資金の受取」でHavenoウォレットに入金してください。 shared.waitingForFunds=資金を待っています shared.TheXMRBuyer=XMR買い手 shared.You=あなた shared.sendingConfirmation=承認を送信中 shared.sendingConfirmationAgain=もう一度承認を送信してください shared.exportCSV=CSVにエクスポート shared.exportJSON=JSONにエクスポート shared.summary=Show summary shared.noDateAvailable=日付がありません shared.noDetailsAvailable=詳細不明 shared.notUsedYet=未使用 shared.date=日付 shared.sendFundsDetailsWithFee=送信中: {0}\n\n受取アドレス: {1}\n\n追加のマイナー手数料: {2}\n\nこの金額を送信してもよろしいですか? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Havenoがこのトランザクションはダストの最小閾値以下のおつりアウトプットを生じることを検出しました(それにしたがって、ビットコインのコンセンサス・ルールによって許されない)。代わりに、その ({0} satoshi{1}) のダストはマイニング手数料に追加されます。\n\n\n shared.copyToClipboard=クリップボードにコピー shared.language=言語 shared.country=国 shared.applyAndShutDown=適用して終了 shared.selectPaymentMethod=支払い方法を選ぶ shared.accountNameAlreadyUsed=そのアカウント名は既に使用されています。\n別の名前を使用してください。 shared.askConfirmDeleteAccount=選択したアカウントを本当に削除しますか? shared.cannotDeleteAccount=このアカウントはトレード中(それともオファーを入れている中)ため消去することはできません。 shared.noAccountsSetupYet=アカウントが設定されていません shared.manageAccounts=アカウント管理 shared.addNewAccount=アカウントを追加 shared.ExportAccounts=アカウントをエクスポート shared.importAccounts=アカウントをインポート shared.createNewAccount=新しいアカウントを作る shared.createNewAccountDescription=あなたのアカウント詳細は、デバイスにローカルに保存され、取引相手および紛争が発生した場合には仲裁人とのみ共有されます。 shared.saveNewAccount=新しいアカウントを保存する shared.selectedAccount=選択したアカウント shared.deleteAccount=アカウントを削除 shared.errorMessageInline=\nエラーメッセージ: {0} shared.errorMessage=エラーメッセージ shared.information=情報 shared.name=名前 shared.id=ID shared.dashboard=ダッシュボード shared.accept=同意 shared.balance=残高 shared.save=保存 shared.onionAddress=Onionアドレス shared.supportTicket=サポートチケット shared.dispute=係争 shared.mediationCase=調停事件 shared.seller=売り手 shared.buyer=買い手 shared.allEuroCountries=ユーロ全諸国 shared.acceptedTakerCountries=取引可能なテイカーの国 shared.tradePrice=取引価格 shared.tradeAmount=取引額 shared.tradeVolume=取引量 shared.invalidKey=入力されたキーが正しくありません shared.enterPrivKey=アンロックの為にプライベートキーを入力 shared.payoutTxId=支払いトランザクションID shared.contractAsJson=JSON形式の契約 shared.viewContractAsJson=JSON形式で見る shared.contract.title=次のIDとのトレードの契約: {0} shared.paymentDetails=XMR {0} 支払い詳細 shared.securityDeposit=セキュリティデポジット shared.yourSecurityDeposit=あなたのセキュリティデポジット shared.contract=契約 shared.messageArrived=メッセージが来ました。 shared.messageStoredInMailbox=メッセージが受信箱に入っています shared.messageSendingFailed=メッセージ送信失敗。エラー: {0} shared.unlock=ロック解除 shared.toReceive=受け取る shared.toSpend=費やす shared.xmrAmount=XMR金額 shared.yourLanguage=あなたの言語 shared.addLanguage=言語を追加 shared.total=合計 shared.totalsNeeded=必要な資金 shared.tradeWalletAddress=トレードウォレットアドレス shared.tradeWalletBalance=トレードウォレット残高 shared.reserveExactAmount=必要な資金のみを予約してください。オファーが公開されるまでにマイニング手数料と約20分が必要です。 shared.makerTxFee=メイカー: {0} shared.takerTxFee=テイカー: {0} shared.iConfirm=確認します shared.openURL={0} をオープン shared.fiat=法定通貨 shared.crypto=暗号通貨 shared.preciousMetals=貴金属 shared.all=全て shared.edit=編集 shared.advancedOptions=高度なオプション shared.interval=間隔 shared.actions=アクション shared.buyerUpperCase=買い手 shared.sellerUpperCase=売り手 shared.new=新 shared.learnMore=もっと詳しく知る shared.dismiss=却下する shared.selectedArbitrator=選択された調停人 shared.selectedMediator=選択された調停者 shared.selectedRefundAgent=選択された仲裁者 shared.mediator=調停者 shared.arbitrator=仲裁者 shared.refundAgent=仲裁者 shared.refundAgentForSupportStaff=仲裁者 shared.delayedPayoutTxId=遅延支払いトランザクションID shared.delayedPayoutTxReceiverAddress=遅延支払いトランザクション送り先 shared.unconfirmedTransactionsLimitReached=現在、非確認されたトランザクションが多すぎます。しばらく待ってからもう一度試して下さい。 shared.numItemsLabel=記載事項の数: {0} shared.filter=フィルター shared.enabled=有効されました #################################################################### # UI views #################################################################### #################################################################### # MainView #################################################################### mainView.menu.market=相場 mainView.menu.buyXmr=XMRを購入 mainView.menu.sellXmr=XMRを売却 mainView.menu.portfolio=ポートフォリオ mainView.menu.funds=資金 mainView.menu.support=サポート mainView.menu.settings=設定 mainView.menu.account=アカウント mainView.marketPriceWithProvider.label={0} による市場価格 mainView.marketPrice.havenoInternalPrice=Havenoにおける最新の取引価格 mainView.marketPrice.tooltip.havenoInternalPrice=利用可能な外部価格フィードプロバイダーからの市場価格がありません。\n表示されている価格は、その通貨の最新のHaveno取引価格です。 mainView.marketPrice.tooltip=市場価格は{0}{1}に提供されています\n最終更新: {2}\n提供者のノードのURL: {3} mainView.balance.available=利用可能残高 mainView.balance.reserved=オファーのために予約済み mainView.balance.pending=トレードにロック中 mainView.balance.reserved.short=予約済 mainView.balance.pending.short=ロック中 mainView.footer.usingTor=(Tor 経由) mainView.footer.localhostMoneroNode=(ローカルホスト) mainView.footer.clearnet=(clearnet 経由) mainView.footer.xmrInfo={0} {1} mainView.footer.xmrFeeRate=/ 手数料率: {0} サトシ/vB mainView.footer.xmrInfo.initializing=Havenoネットワークに接続中 mainView.footer.xmrInfo.synchronizingWith=ブロック {1} / {2} で {0} と同期しています mainView.footer.xmrInfo.connectedTo=ブロック {1} で {0} に接続しました mainView.footer.xmrInfo.synchronizingWalletWith=ブロック {1} / {2} で {0} のウォレットを同期しています mainView.footer.xmrInfo.syncedWith=ブロック {1} で {0} と同期しました mainView.footer.xmrInfo.connectingTo=接続中: mainView.footer.xmrInfo.connectionFailed=接続失敗 mainView.footer.xmrPeers=Moneroネットワークピア: {0} mainView.footer.p2pPeers=Havenoネットワークピア: {0} mainView.bootstrapState.connectionToTorNetwork=(1/4) Torネットワークに接続中... mainView.bootstrapState.torNodeCreated=(2/4) Torノードが作成されました mainView.bootstrapState.hiddenServicePublished=(3/4) 秘匿サービスを公開しました mainView.bootstrapState.initialDataReceived=(4/4) 初期データを受信しました mainView.bootstrapWarning.noSeedNodesAvailable=シードノードが見つかりません mainView.bootstrapWarning.noNodesAvailable=シードノードとピアが見つかりません mainView.bootstrapWarning.bootstrappingToP2PFailed=Havenoネットワークとの同期に失敗しました mainView.p2pNetworkWarnMsg.noNodesAvailable=データを要求するためのシードノードと永続ピアが見つかりません。\nインターネット接続を確認するか、アプリケーションを再起動してみてください。 mainView.p2pNetworkWarnMsg.connectionToP2PFailed=Havenoネットワークへの接続に失敗しました(報告されたエラー: {0})。\nインターネット接続を確認するか、アプリケーションを再起動してみてください。 mainView.walletServiceErrorMsg.timeout=タイムアウトのためビットコインネットワークへの接続に失敗しました mainView.walletServiceErrorMsg.connectionError=次のエラーのためビットコインネットワークへの接続に失敗しました: {0} mainView.walletServiceErrorMsg.rejectedTxException=トランザクションはネットワークに拒否されました。\n\n{0} mainView.networkWarning.allConnectionsLost=全ての{0}のネットワークピアへの接続が切断されました。\nインターネット接続が切断されたか、コンピュータがスタンバイモードになった可能性があります。 mainView.networkWarning.localhostMoneroLost=ローカルホストビットコインノードへの接続が切断されました。\nHavenoアプリケーションを再起動して他のビットコインノードに接続するか、ローカルホストのビットコインノードを再起動してください。 mainView.version.update=(更新が利用可能) #################################################################### # MarketView #################################################################### market.tabs.offerBook=オファーブック market.tabs.spreadCurrency=通貨別のオファー market.tabs.spreadPayment=支払い方法別のオファー market.tabs.trades=取引 # OfferBookChartView market.offerBook.buyCrypto={0}を買う({1}を売る) market.offerBook.sellCrypto={0}を売る({1}を買う) market.offerBook.buyWithTraditional={0}を買う market.offerBook.sellWithTraditional={0}を売る market.offerBook.sellOffersHeaderLabel=以下に{0}を売る market.offerBook.buyOffersHeaderLabel=以下から{0}を買う market.offerBook.buy=ビットコインを買いたい market.offerBook.sell=ビットコインを売りたい # SpreadView market.spread.numberOfOffersColumn=全てのオファー ({0}) market.spread.numberOfBuyOffersColumn=XMRを買う ({0}) market.spread.numberOfSellOffersColumn=XMRを売る ({0}) market.spread.totalAmountColumn=XMR合計 ({0}) market.spread.spreadColumn=スプレッド market.spread.expanded=拡張された表示 # TradesChartsView market.trades.nrOfTrades=取引: {0} market.trades.tooltip.volumeBar=取引量: {0} / {1}\n取引数: {2}\n日付: {3} market.trades.tooltip.candle.open=オープン: market.trades.tooltip.candle.close=クローズ: market.trades.tooltip.candle.high=最高: market.trades.tooltip.candle.low=最低: market.trades.tooltip.candle.average=平均: market.trades.tooltip.candle.median=中央値: market.trades.tooltip.candle.date=日付: market.trades.showVolumeInUSD=米ドル建ての貿易量を表示 #################################################################### # OfferView #################################################################### offerbook.createOffer=オファーを作る offerbook.takeOffer=オファーを受ける offerbook.takeOfferToBuy={0}購入オファーを受ける offerbook.takeOfferToSell={0}売却オファーを受ける offerbook.takeOffer.enterChallenge=オファーのパスフレーズを入力してください offerbook.trader=取引者 offerbook.offerersBankId=メイカーの銀行ID (BIC/SWIFT): {0} offerbook.offerersBankName=メーカーの銀行名: {0} offerbook.offerersBankSeat=メーカーの銀行の国名: {0} offerbook.offerersAcceptedBankSeatsEuro=利用可能な銀行の国名(テイカー): 全ユーロ諸国 offerbook.offerersAcceptedBankSeats=利用可能な銀行の国名(テイカー):\n{0} offerbook.availableOffers=利用可能なオファー offerbook.filterByCurrency=通貨でフィルター offerbook.filterByPaymentMethod=支払い方法でフィルター offerbook.matchingOffers=アカウントと一致するオファー offerbook.filterNoDeposit=デポジットなし offerbook.noDepositOffers=預金不要のオファー(パスフレーズ必須) offerbook.timeSinceSigning=アカウント情報 offerbook.timeSinceSigning.info=このアカウントは認証されまして、{0} offerbook.timeSinceSigning.info.arbitrator=調停人に署名されました。ピアアカウントも署名できます offerbook.timeSinceSigning.info.peer=ピアが署名しました。%d日間後に制限の解除を待ち中 offerbook.timeSinceSigning.info.peerLimitLifted=ピアが署名しました。制限は解除されました offerbook.timeSinceSigning.info.signer=ピアが署名しました。ピアアカウントも署名できます(制限は解除されました) offerbook.timeSinceSigning.info.banned=このアカウントは禁止されました offerbook.timeSinceSigning.daysSinceSigning={0}日 offerbook.timeSinceSigning.daysSinceSigning.long=署名する後から {0} offerbook.xmrAutoConf=自動確認は有効されますか? offerbook.buyXmrWith=XMRを購入: offerbook.sellXmrFor=XMRを売る: offerbook.timeSinceSigning.help=署名された支払いアカウントを持っているピアと成功にトレードすると、自身の支払いアカウントも署名されることになります。\n{0} 日後に、{1} という初期の制限は解除され、他のピアの支払いアカウントを署名できるようになります。 offerbook.timeSinceSigning.notSigned=まだ署名されていません offerbook.timeSinceSigning.notSigned.ageDays={0}日 offerbook.timeSinceSigning.notSigned.noNeed=N/A shared.notSigned=このアカウントはまだ署名されていない。{0} 日間前に作成されました shared.notSigned.noNeed=この種類のアカウントは署名を必要しません shared.notSigned.noNeedDays=この種類のアカウントは署名を必要しません。{0} 日間前に作成されました shared.notSigned.noNeedAlts=アルトコインのアカウントには署名や熟成という機能がありません offerbook.nrOffers=オファー数: {0} offerbook.volume={0} (下限 - 上限) offerbook.deposit=XMRの敷金(%) offerbook.deposit.help=トレードを保証するため、両方の取引者が支払う敷金。トレードが完了されたら、返還されます。 offerbook.createNewOffer={0} {1}にオファーを作成する offerbook.createOfferToBuy={0} を購入するオファーを作成 offerbook.createOfferToSell={0} を売却するオファーを作成 offerbook.createOfferToBuy.withTraditional={1} で {0} を購入するオファーを作成 offerbook.createOfferToSell.forTraditional={1} で {0} を売却するオファーを作成 offerbook.createOfferToBuy.withCrypto={0} を売却する({1}購入)オファーを作成 offerbook.createOfferToSell.forCrypto={0} を購入する({1}売却)オファーを作成 offerbook.takeOfferButton.tooltip={0} のオファーを受ける offerbook.yesCreateOffer=はい、オファーを作成します offerbook.setupNewAccount=新しいトレードアカウントを設定 offerbook.removeOffer.success=オファーの削除に成功しました。 offerbook.removeOffer.failed=オファー削除に失敗:\n{0} offerbook.deactivateOffer.failed=オファー無効化に失敗:\n{0} offerbook.activateOffer.failed=オファー公開に失敗:\n{0} offerbook.withdrawFundsHint=あなたが支払った資金を{0}画面から出金できます。 offerbook.warning.noTradingAccountForCurrency.headline=指定の通貨では支払いアカウントがありません offerbook.warning.noTradingAccountForCurrency.msg=選択した通貨の支払いアカウントがありません。\n\n他の通貨でオファーを作成しますか? offerbook.warning.noMatchingAccount.headline=一致する支払いアカウントがありません offerbook.warning.noMatchingAccount.msg=このオファーは、まだ設定されない支払い方法を利用します。\n\n今すぐ新しい支払いアカウントを設定しますか? offerbook.warning.counterpartyTradeRestrictions=相手方のトレード制限のせいでこのオファーを受けることができません offerbook.warning.newVersionAnnouncement=このバージョンのソフトウェアでは、トレードするピアがお互いの支払いアカウントを署名・検証でき、信頼できる支払いアカウントのネットワークを作れるようにします。\n\n検証されたアカウントと成功にトレードしたら、自身の支払いアカウントも署名されることになり、一定の時間が過ぎたらトレード制限は解除されます(時間の長さは検証方法によって異なります)。\n\nアカウント署名について詳しくは、ドキュメンテーションを参照して下さい:[HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing] popup.warning.tradeLimitDueAccountAgeRestriction.seller=許可されたトレード金額は以下のセキュリティ基準に基づいて {0} に制限されました:\n- 買い手のアカウントは調停人やピアに署名されていません\n- 買い手のアカウントが署名された時から30日未満がたちました\n- このオファーの支払い方法は、銀行のチャージバックのリスクが高いと考えられます\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=許可されたトレード金額は以下のセキュリティ基準に基づいて {0} に制限されました:\n- このアカウントは調停人やピアに署名されていません\n- このアカウントが署名された時から30日未満がたちました\n- このオファーの支払い方法は、銀行のチャージバックのリスクが高いと考えられます\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=この支払い方法は、すべての購入者が新しいアカウントを持っているため、{0}までの一時的な制限があります{1}。\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=あなたのオファーは、署名済みで経年の古いアカウントを持つバイヤーに限定されます。なぜなら、それが {0} を超えているためです。\n\n{1} offerbook.warning.wrongTradeProtocol=そのオファーには、ご使用のソフトウェアのバージョンで使用されているものとは異なるプロトコルバージョンが必要です。\n\n最新バージョンがインストールされているかどうかを確認してください。そうでなければ、オファーを作成したユーザーが古いバージョンを使用しています。\n\nユーザーは、互換性のないトレードプロトコルバージョンと取引することはできません。 offerbook.warning.userIgnored=そのユーザのonionアドレスを無視リストに追加しました。 offerbook.warning.offerBlocked=そのオファーはHaveno開発者によってブロックされました。\nおそらくそのオファーを受けるときに問題が引きおこさる未処理のバグがあります。 offerbook.warning.currencyBanned=そのオファーで使用されている通貨はHaveno開発者によってブロックされています。\n詳しくはHavenoフォーラムをご覧ください。 offerbook.warning.paymentMethodBanned=そのオファーで使用されている支払い方法はHaveno開発者によってブロックされています。\n詳しくはHavenoフォーラムをご覧ください。 offerbook.warning.nodeBlocked=そのonionアドレスはHaveno開発者によってブロックされました。\nおそらくその取引者からのオファーを受けるときに問題が引きおこさる未処理のバグがあります。 offerbook.warning.requireUpdateToNewVersion=このHavenoのバージョンはもはやトレードする互換性がありません。\n[HYPERLINK:https://haveno.exchange/downloads] で最新のHavenoバージョンに更新してください。 offerbook.warning.offerWasAlreadyUsedInTrade=このオファーを以前に受けましたせいで、現在受けることができません。以前のオファー受け入り試みは失敗トレードに終わりましたかもしれません。 offerbook.info.sellAtMarketPrice=市場価格で売却されるでしょう(毎分更新されます)。 offerbook.info.buyAtMarketPrice=市場価格で購入されるでしょう(毎分更新されます)。 offerbook.info.sellBelowMarketPrice=現在の市場価格よりも{0}以下で入手するでしょう(毎分更新されます)。 offerbook.info.buyAboveMarketPrice=現在の市場価格よりも{0}以上で支払いされるでしょう(毎分更新されます)。 offerbook.info.sellAboveMarketPrice=現在の市場価格よりも{0}以上で入手するでしょう(毎分更新されます)。 offerbook.info.buyBelowMarketPrice=現在の市場価格よりも{0}以下で支払いされるでしょう(毎分更新されます)。 offerbook.info.buyAtFixedPrice=固定された価格で購入します。 offerbook.info.sellAtFixedPrice=固定された価格で売却します。 offerbook.info.noArbitrationInUserLanguage=係争が発生した場合、このオファーの仲裁は{0}で処理されます。 言語は現在{1}に設定されています。 offerbook.info.roundedFiatVolume=金額は取引のプライバシーを高めるために四捨五入されました。 #################################################################### # Offerbook / Create offer #################################################################### createOffer.amount.prompt=XMRの金額を入力 createOffer.price.prompt=価格を入力 createOffer.volume.prompt={0}の金額を入力 createOffer.amountPriceBox.amountDescription=以下の金額でXMRを{0} createOffer.amountPriceBox.buy.volumeDescription=支払う{0}の金額 createOffer.amountPriceBox.sell.volumeDescription=受け取る{0}の金額 createOffer.amountPriceBox.minAmountDescription=XMRの最小額 createOffer.securityDeposit.prompt=セキュリティデポジット createOffer.fundsBox.title=あなたのオファーへ入金 createOffer.fundsBox.offerFee=取引手数料 createOffer.fundsBox.networkFee=マイニング手数料 createOffer.fundsBox.placeOfferSpinnerInfo=オファー公開の処理中 ... createOffer.fundsBox.paymentLabel=次のIDとのHavenoトレード: {0} createOffer.fundsBox.fundsStructure=({0} セキュリティデポジット, {1} 取引手数料, {2}マイニング手数料) createOffer.success.headline=お客様のオファーが作成されました createOffer.success.info=自分のオファーは「ポートフォリオ/私のオープンオファー」で管理できます createOffer.info.sellAtMarketPrice=オファーの価格は継続的に更新されるため、常に市場価格で売却するでしょう。 createOffer.info.buyAtMarketPrice=オファーの価格は継続的に更新されるため、常に市場価格で購入するでしょう。 createOffer.info.sellAboveMarketPrice=オファーの価格は継続的に更新されるため、常に現在の市場価格より{0}%以上で入手するでしょう。 createOffer.info.buyBelowMarketPrice=オファーの価格は継続的に更新されるため、常に現在の市場価格より{0}%以下で支払いするでしょう。 createOffer.warning.sellBelowMarketPrice=オファーの価格は継続的に更新されるため、常に現在の市場価格より{0}%以下で入手するでしょう。 createOffer.warning.buyAboveMarketPrice=オファーの価格は継続的に更新されるため、常に現在の市場価格より{0}%以上で支払いするでしょう。 createOffer.tradeFee.descriptionXMROnly=取引手数料 createOffer.tradeFee.descriptionBSQEnabled=トレード手数料通貨を選択 createOffer.triggerPrice.prompt=任意選択価格トリガーを設定する createOffer.triggerPrice.label=市場価格が{0}になる場合、オファーを無効にする createOffer.triggerPrice.tooltip=価格の著しい変化から保護するため、市場価格が特定の価値に達する時にオファーは無効される価格トリガーを設定できます。 createOffer.triggerPrice.invalid.tooLow=価値は{0}より高くなければなりません createOffer.triggerPrice.invalid.tooHigh=価値は{0}より低くなければなりません # new entries createOffer.placeOfferButton.buy=確認:{0}でXMRを購入するオファーを作成 createOffer.placeOfferButton.sell=確認:{0}でXMRを売却するオファーを作成 createOffer.createOfferFundWalletInfo.headline=あなたのオファーへ入金 # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- 取引額: {0}\n createOffer.createOfferFundWalletInfo.msg=このオファーには {0} をデポジットする必要があります。\n\n\ この資金はあなたのローカルウォレットに予約され、誰かがあなたのオファーを受け入れるとマルチシグウォレットにロックされます。\n\n\ 金額は以下の合計です:\n\ {1}\ - あなたの保証金: {2}\n\ - 取引手数料: {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=オファーを出す時にエラーが発生しました:\n\n{0}\n\nウォレットにまだ資金がありません。\nアプリケーションを再起動してネットワーク接続を確認してください。 createOffer.setAmountPrice=取引額と価格を入力して下さい createOffer.warnCancelOffer=そのオファーは既に資金提供されています。\n今キャンセルすると、あなたの資金はローカルのHavenoウォレットに残り、\"資金/送金する\"画面で引き出し可能です。\nキャンセルしてもよろしいですか? createOffer.timeoutAtPublishing=オファーの公開中にタイムアウトが発生しました。 createOffer.errorInfo=\n\nメイカー手数料は既に支払い済みです。 最悪の場合、あなたはその手数料を失っています。\nアプリケーションを再起動し、ネットワーク接続を確認して問題を解決できるかどうかを確認してください。 createOffer.tooLowSecDeposit.warning=セキュリティデポジットが推奨されるデフォルト値{0}よりも低い値に設定されました。\nより低いセキュリティデポジットを使用してよろしいですか? createOffer.tooLowSecDeposit.makerIsSeller=取引相手がトレードプロトコルに従わない場合、あなたへの保護は少なくなります。 createOffer.tooLowSecDeposit.makerIsBuyer=リスクに対してセキュリティデポジットが少ないため、あなたのトレードプロトコルでは取引相手への保護が少なくなります。 他のユーザーはあなたの代わりに他のオファーを選ぶかもしれません。 createOffer.resetToDefault=いいえ、既定の値に戻します createOffer.useLowerValue=はい、私の低い値を使用します createOffer.priceOutSideOfDeviation=入力した価格は、市場価格からの最大許容偏差を超えています。\n最大許容偏差は{0}で、設定で調整できます。 createOffer.changePrice=価格を変更 createOffer.tac=このオファーを公開することで、この画面で定義された条件を満たす取引者と取引することに同意します。 createOffer.currencyForFee=取引手数料 createOffer.setDeposit=買い手のセキュリティデポジット (%) createOffer.setDepositAsBuyer=購入時のセキュリティデポジット (%) createOffer.setDepositForBothTraders=両方の取引者の保証金を設定する(%) createOffer.securityDepositInfo=あなたの買い手のセキュリティデポジットは{0}です createOffer.securityDepositInfoAsBuyer=あなたの購入時のセキュリティデポジットは{0}です createOffer.minSecurityDepositUsed=最低セキュリティデポジットが使用されます createOffer.buyerAsTakerWithoutDeposit=購入者に保証金は不要(パスフレーズ保護) createOffer.myDeposit=私の保証金(%) createOffer.myDepositInfo=あなたのセキュリティデポジットは{0}です #################################################################### # Offerbook / Take offer #################################################################### takeOffer.amount.prompt=XMRの金額を入力 takeOffer.amountPriceBox.buy.amountDescription=XMR売却額 takeOffer.amountPriceBox.sell.amountDescription=XMR購入額 takeOffer.amountPriceBox.priceDescription={0}のビットコインあたりの価格 takeOffer.amountPriceBox.amountRangeDescription=可能な金額の範囲 takeOffer.amountPriceBox.warning.invalidXmrDecimalPlaces=入力した金額が、許容される小数点以下の桁数を超えています。\n金額は小数点以下第4位に調整されています。 takeOffer.validation.amountSmallerThanMinAmount=金額はオファーで示された下限額を下回ることができません takeOffer.validation.amountLargerThanOfferAmount=オファーで示された上限額を上回る金額は入力できません takeOffer.validation.amountLargerThanOfferAmountMinusFee=その入力額はXMRの売り手にダストチェンジを引き起こします。 takeOffer.fundsBox.title=あなたのトレードへ入金 takeOffer.fundsBox.isOfferAvailable=オファーが有効か確認中... takeOffer.fundsBox.tradeAmount=売却額 takeOffer.fundsBox.offerFee=取引手数料 takeOffer.fundsBox.networkFee=合計マイニング手数料 takeOffer.fundsBox.takeOfferSpinnerInfo=オファーを受け入れる: {0} takeOffer.fundsBox.paymentLabel=次のIDとのHavenoトレード: {0} takeOffer.fundsBox.fundsStructure=({0} セキュリティデポジット, {1} 取引手数料, {2}マイニング手数料) takeOffer.fundsBox.noFundingRequiredTitle=資金は必要ありません takeOffer.fundsBox.noFundingRequiredDescription=このオファーを受けるには、Haveno外で売り手からオファーパスフレーズを取得してください。 takeOffer.success.headline=オファー受け入れに成功しました takeOffer.success.info=あなたのトレード状態は「ポートフォリオ/オープントレード」で見られます takeOffer.error.message=オファーの受け入れ時にエラーが発生しました。\n\n{0} # new entries takeOffer.takeOfferButton.buy=確認:{0}でXMRを購入するオファーを受け入れ takeOffer.takeOfferButton.sell=確認:{0}でXMRを売却するオファーを受け入れ takeOffer.noPriceFeedAvailable=そのオファーは市場価格に基づくパーセント値を使用していますが、使用可能な価格フィードがないため、利用することはできません。 takeOffer.takeOfferFundWalletInfo.headline=あなたのオファーへ入金 # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount= - 取引額: {0}\n takeOffer.takeOfferFundWalletInfo.msg=このオファーを受けるには、{0} を預ける必要があります。\n\n金額は以下の合計です:\n{1}- あなたの保証金: {2}\n- 取引手数料: {3} takeOffer.alreadyPaidInFunds=あなたがすでに資金を支払っている場合は「資金/送金する」画面でそれを出金することができます。 takeOffer.paymentInfo=支払い情報 takeOffer.setAmountPrice=金額を設定 takeOffer.alreadyFunded.askCancel=そのオファーは既に資金提供されています。\n今キャンセルすると、あなたの資金はローカルのHavenoウォレットに残り、\"資金/送金する\"画面で引き出し可能です。\nキャンセルしてもよろしいですか? takeOffer.failed.offerNotAvailable=オファーが利用できなくなったため、オファー受け入れに失敗しました。 この間に別のトレーダーがオファーを受けた可能性があります。 takeOffer.failed.offerTaken=そのオファーは既に別の取引者によって受け取られため、そのオファーは受け取れません。 takeOffer.failed.offerRemoved=そのオファーはこの間に削除されたため、そのオファーは受け取れません takeOffer.failed.offererNotOnline=メイカーがオンラインになっていないため、オファー受け入れに失敗しました。 takeOffer.failed.offererOffline=このオファーはメーカーがオフラインのため受け取れません takeOffer.warning.connectionToPeerLost=メイカーとの接続が切れました。\n相手がオフラインになったか、オープンな接続が多すぎるため、あなたへの接続を閉じた可能性があります。\n\nまだオファーブックに相手のオファーが表示されている場合は、もう一度そのオファーを受け取って下さい。 takeOffer.error.noFundsLost=\n\nあなたのウォレットにはまだ資金がありません\nアプリケーションを再起動し、ネットワーク接続を確認して問題を解決できるかどうかを確認してください。 # suppress inspection "TrailingSpacesInProperty" takeOffer.error.feePaid=\n\n takeOffer.error.depositPublished=\n\nデポジットトランザクションは既に公開されています。\nアプリケーションを再起動し、ネットワーク接続を確認して問題を解決できるかどうかを確認してください。\nそれでも問題が解決しない場合は、開発者に連絡してください。 takeOffer.error.payoutPublished=\n\n支払いトランザクションは既に公開されています。\nアプリケーションを再起動し、ネットワーク接続を確認して問題を解決できるかどうかを確認してください。\nそれでも問題が解決しない場合は、開発者に連絡してください。 takeOffer.tac=このオファーを受けることで、この画面で定義されている取引条件に同意します。 #################################################################### # Offerbook / Edit offer #################################################################### openOffer.header.triggerPrice=価格トリガー openOffer.triggerPrice=価格トリガー{0} openOffer.triggered=市場価格は価格トリガーに達しましたため、オファーが無効にされました。\nオファーには新しい価格トリガーを設定して下さい。 editOffer.setPrice=価格設定 editOffer.confirmEdit=承認: オファーを編集 editOffer.publishOffer=あなたのオファーの公開。 editOffer.failed=オファー編集に失敗:\n{0} editOffer.success=オファー編集に成功しました editOffer.invalidDeposit=買い手のセキュリティデポジットはHaveno DAOによって定義された制約の範囲内ではなく、もう編集することはできません。 #################################################################### # Portfolio #################################################################### portfolio.tab.openOffers=私のオープンなオファー portfolio.tab.pendingTrades=オープンなトレード portfolio.tab.history=履歴 portfolio.tab.failed=失敗 portfolio.tab.editOpenOffer=オファーを編集 portfolio.closedTrades.deviation.help=市場からの割合価格偏差 portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} portfolio.pending.syncing=取引ウォレットを同期中 portfolio.pending.syncing.blockRemaining=取引ウォレットを同期中 — 残り1ブロック portfolio.pending.syncing.blocksRemaining=取引ウォレットを同期中 — 残り{0}ブロック portfolio.pending.step1.waitForConf=ブロックチェーンの承認をお待ち下さい portfolio.pending.step2_buyer.additionalConf=入金は10承認に達しました。\n追加の安全のため、支払いを送信する前に{0}承認を待つことをお勧めします。\n早めに進める場合は自己責任となります。 portfolio.pending.step2_buyer.startPayment=支払い開始 portfolio.pending.step2_seller.waitPaymentSent=支払いが始まるまでお待ち下さい portfolio.pending.step3_buyer.waitPaymentArrived=支払いが到着するまでお待ち下さい portfolio.pending.step3_seller.confirmPaymentReceived=支払いを受領したことを確認して下さい portfolio.pending.step5.completed=完了 portfolio.pending.step3_seller.autoConf.status.label=自動確認のステータス portfolio.pending.autoConf=自動確認されました portfolio.pending.autoConf.blocks=XMR承認: {0} / 必要: {1} portfolio.pending.autoConf.state.xmr.txKeyReused=トランザクション・キーは再利用されました。係争を開始して下さい。 portfolio.pending.autoConf.state.confirmations=XMR承認: {0}/{1} portfolio.pending.autoConf.state.txNotFound=トランザクションはまだメモリプールに見られません portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=有効なトランザクションID/トランザクション・キーはありません portfolio.pending.autoConf.state.filterDisabledFeature=開発者により無効されました # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FEATURE_DISABLED=自動確認機能は無効されました。{0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=トレード金額は自動確認の金額制限を越えます # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.INVALID_DATA=ピアは無効データを提供しました。{0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=支払いトランザクションはすでに公開されました。 # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.DISPUTE_OPENED=係争は開始されました。そのトレードでは自動確認が無効にされました。 # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.REQUESTS_STARTED=トランザクション証明依頼を開始しました # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PENDING=成功の成果: {0}/{1}; {2} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.COMPLETED=全てのサービスでは、証明が成功に終わりました # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.ERROR=サービスリクエストにはエラーが生じました。自動確認できません。 # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=サービスは失敗を返しました。自動確認できません。 portfolio.pending.step1.info.you=入金トランザクションが公開されました。\n10回の承認後に支払いを開始できます(残り約{0}分)。 portfolio.pending.step1.info.buyer=入金トランザクションが公開されました。\nXMRの購入者は10回の承認後に支払いを開始できます(残り約{0}分)。 portfolio.pending.step1.warn=デポジットトランザクションがまだ承認されていません。外部ウォレットからの取引者の資金調達手数料が低すぎるときには、例外的なケースで起こるかもしれません。 portfolio.pending.step1.openForDispute=デポジットトランザクションがまだ承認されていません。もう少し待つか、助けを求めて調停人に連絡できます。 # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2.confReached=トレードは少なくとも1つのブロックチェーン承認に達しました。\n\n portfolio.pending.step2_buyer.refTextWarn=注意点:支払う時に、\"支払理由\"のフィールドを空白にしておいて下さい。いかなる場合でも、トレードIDそれとも「ビットコイン」、「XMR」、「Haveno」などを入力しないで下さい。両者にとって許容できる別の\"支払理由\"があれば、自由に取引者チャットで話し合いをして下さい。 # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees=銀行口座振替を行うには手数料がある場合、その手数料を払う責任があります。 # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.crypto=外部{0}ウォレットから転送してください\nXMRの売り手へ{1}。\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.cash=銀行に行き、XMRの売り手へ{0}を支払ってください。\n\n portfolio.pending.step2_buyer.cash.extra=重要な要件:\n支払いが完了したら、領収書に「返金無し(NO REFUNDS)」と記載してください。\nそれからそれを2部に分け、写真を撮り、そしてXMRの売り手のEメールアドレスへそれを送ってください。 # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.moneyGram=MoneyGramを使用してXMR売り手へ{0}をお支払いください。\n\n portfolio.pending.step2_buyer.moneyGram.extra=重要な要件: \n支払いが完了したら、認証番号と領収書の写真を電子メールでXMRの売り手へ送信して下さい。\n領収書には、売り手の氏名、国、都道府県、および金額を明確に表示する必要があります。売り手のメールアドレス: {0} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.westernUnion=Western Unionを使用してXMRの売り手へ{0}をお支払いください。\n\n portfolio.pending.step2_buyer.westernUnion.extra=重要な要件: \n支払いが完了したら、MTCN(追跡番号)と領収書の写真を電子メールでXMRの売り手へ送信して下さい。\n領収書には、売り手の氏名、市区町村、国、金額が明確に示されている必要があります。売り手のメールアドレス: {0} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal={0}を「米国の郵便為替」でXMRの売り手に送付してください。\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=\"郵送で現金\"で、{0}をXMR売り手に送って下さい。詳細な指示はトレード契約書に書いてあります、そして分からない点があれば取引者チャットで質問できます。「郵送で現金」について詳しくはHavenoのWikiを参照:[HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Pay_By_Mail]\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=特定された支払い方法で{0}をXMRの売り手に支払ってお願いします。売り手のアカウント詳細は次の画面に表示されます。\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=提供された連絡先でXMRの売り手に連絡し、{0}を支払うためのミーティングを準備してください。\n\n portfolio.pending.step2_buyer.startPaymentUsing={0}を使用して支払いを開始 portfolio.pending.step2_buyer.recipientsAccountData=受領者 {0} portfolio.pending.step2_buyer.amountToTransfer=振替金額 portfolio.pending.step2_buyer.sellersAddress=売り手の{0}アドレス portfolio.pending.step2_buyer.buyerAccount=使用されるあなたの支払いアカウント portfolio.pending.step2_buyer.paymentSent=支払いが開始されました portfolio.pending.step2_buyer.showEarly=支払いの詳細を早めに表示する portfolio.pending.step2_buyer.warn={0}の支払いはまだ完了していません!\nトレードは{1}までに完了しなければなりません。 portfolio.pending.step2_buyer.openForDispute=支払いを完了していません!\nトレードの最大期間が経過しました。助けを求めるには調停人に連絡してください。 portfolio.pending.step2_buyer.paperReceipt.headline=領収書をXMRの売り手へ送付しましたか? portfolio.pending.step2_buyer.paperReceipt.msg=覚えておいてください:\n領収書に「返金無し(NO REFUNDS)」と記載してください。\nそれからそれを2部に分け、写真を撮り、そしてXMRの売り手のEメールアドレスへそれを送ってください。 portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=認証番号と領収書を送信 portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=認証番号と領収書の写真を電子メールでXMRの売り手へ送信する必要があります。\n領収書には、売り手の氏名、国、都道府県、および金額を明確に表示する必要があります。売却者のメールアドレス: {0}\n\n認証番号と契約書を売り手へ送付しましたか? portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=MTCNと領収書を送信 portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=あなたはMTCN(追跡番号)とレシートの写真をXMRの売り手にEメールで送る必要があります。\n領収書には、売り手の氏名、市区町村、国、金額が明確に示されている必要があります。 販売者のメールアドレス: {0}\n\nMTCNと契約書を売り手へ送付しましたか? portfolio.pending.step2_buyer.halCashInfo.headline=HalCashコードを送信 portfolio.pending.step2_buyer.halCashInfo.msg=HalCashコードと取引ID({0})を含むテキストメッセージをXMRの売り手に送信する必要があります。\n売り手の携帯電話番号は {1} です。\n\n売り手にコードを送信しましたか? portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=銀行によっては、受信者の名前を検証する場合があります。 旧バージョンのHavenoクライアントで作成した「Faster Payments」アカウントでは、受信者の名前は提供されませんので、(必要ならば)トレードチャットで尋ねて下さい。 portfolio.pending.step2_buyer.confirmStart.headline=支払いが開始したことを確認 portfolio.pending.step2_buyer.confirmStart.msg=トレーディングパートナーへの{0}支払いを開始しましたか? portfolio.pending.step2_buyer.confirmStart.yes=はい、支払いを開始しました portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=支払証明を提出していません portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=トランザクションIDとトランザクション・キーを入力していません。\n\nこのデータを提供しなければ、ピアはXMRを受取る直後にXMRを解放するため自動確認機能を利用できません。\nその上、係争の場合にHavenoはXMRトランザクションの送信者がこの情報を調停者や調停人に送れることを必要とします。\n詳しくはHavenoのWikiを参照 [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades] 。 portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=入力が32バイトの16進値ではありません。 portfolio.pending.step2_buyer.confirmStart.warningButton=無視して続ける portfolio.pending.step2_seller.waitPayment.headline=支払いをお待ちください portfolio.pending.step2_seller.f2fInfo.headline=買い手の連絡先 portfolio.pending.step2_seller.waitPayment.msg=デポジットトランザクションには、少なくとも1つのブロックチェーン承認があります。\nXMRの買い手が{0}の支払いを開始するまで待つ必要があります。 portfolio.pending.step2_seller.warn=XMRの買い手はまだ{0}の支払いを行っていません。\n支払いが開始されるまで待つ必要があります。\n取引が{1}で完了していない場合は、調停人が調査します。 portfolio.pending.step2_seller.openForDispute=XMRの買い手は支払いを開始していません!\nトレードの許可された最大期間が経過しました。\nもっと長く待ってトレードピアにもっと時間を与えるか、助けを求めるために調停者に連絡することができます。 tradeChat.chatWindowTitle=トレードID '{0}' のチャットウィンドウ tradeChat.openChat=チャットウィンドウを開く tradeChat.rules=このトレードに対する潜在的な問題を解決するため、トレードピアと連絡できます。\nチャットに返事する義務はありません。\n取引者が以下のルールを破ると、係争を開始して調停者や調停人に報告して下さい。\n\nチャット・ルール:\n\t●リンクを送らないこと(マルウェアの危険性)。トランザクションIDとブロックチェーンエクスプローラの名前を送ることができます。\n\t●シードワード、プライベートキー、パスワードなどの機密な情報を送らないこと。\n\t●Haveno外のトレードを助長しないこと(セキュリティーがありません)。\n\t●ソーシャル・エンジニアリングや詐欺の行為に参加しないこと。\n\t●チャットで返事されない場合、それともチャットでの連絡が断られる場合、ピアの決断を尊重すること。\n\t●チャットの範囲をトレードに集中しておくこと。チャットはメッセンジャーの代わりや釣りをする場所ではありません。\n\t●礼儀正しく丁寧に話すこと。 # suppress inspection "UnusedProperty" message.state.UNDEFINED=未定義 # suppress inspection "UnusedProperty" message.state.SENT=メッセージ送信済 # suppress inspection "UnusedProperty" message.state.ARRIVED=相手からのメールが来ました # suppress inspection "UnusedProperty" message.state.STORED_IN_MAILBOX=支払いのメッセージは送りしましたが、まだピアに受信されていません。 # suppress inspection "UnusedProperty" message.state.ACKNOWLEDGED=相手がメッセージ受信を確認 # suppress inspection "UnusedProperty" message.state.FAILED=メッセージ送信失敗 portfolio.pending.step3_buyer.wait.headline=XMRの売り手の支払い承認をお待ち下さい portfolio.pending.step3_buyer.wait.info={0}の支払いを受け取るためのXMRの売り手の承認を待っています。 portfolio.pending.step3_buyer.wait.msgStateInfo.label=支払いはメッセージステータスを開始 portfolio.pending.step3_buyer.warn.part1a={0} ブロックチェーン上で portfolio.pending.step3_buyer.warn.part1b=支払いプロバイダ(銀行など)で portfolio.pending.step3_buyer.warn.part2=XMRの売り手はまだあなたの支払いを確認していません!支払いの送信が成功したかどうか{0}を確認してください。 portfolio.pending.step3_buyer.openForDispute=XMRの売り手があなたの支払いを確認していません!トレードの最大期間が経過しました。もっと長く待って取引相手にもっと時間を与えるか、調停人から援助を求めることができます。 # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.part=あなたのトレード相手は、彼らが{0}の支払いを開始したことを確認しました。\n\n portfolio.pending.step3_seller.crypto.explorer=あなたの好きな{0}ブロックチェーンエクスプローラで portfolio.pending.step3_seller.crypto.wallet=あなたの{0}ウォレットで portfolio.pending.step3_seller.crypto={0}あなたの受け取りアドレスへのトランザクションが{1}かどうかを確認してください\n{2}\nはすでに十分なブロックチェーンの承認があります。\n支払い額は{3}です\n\nポップアップを閉じた後、メイン画面から{4}アドレスをコピーして貼り付けることができます。 portfolio.pending.step3_seller.postal={0}\"米国の郵便為替\"でXMRの買い手から{1}を受け取ったか確認して下さい。 # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.payByMail={0}\"郵送で現金\"でXMRの買い手から{1}を受け取ったか確認して下さい。 # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.bank=トレード相手は{0}の支払いを開始した確認をしました。\n\nオンラインバンキングのWebページにアクセスして、XMRの買い手から{1}を受け取ったか確認してください。 portfolio.pending.step3_seller.cash=支払いは現金入金で行われるので、XMRの買い手は領収書に「返金無し(NO REFUND)」と記入し、2部に分けて写真を電子メールで送ってください。\n\nチャージバックのリスクを回避するために、Eメールを受信したかどうか、および領収書が有効であることが確実であるかどうかを確認してください。\nよくわからない場合は、{0} portfolio.pending.step3_seller.moneyGram=買い手は承認番号と領収書の写真を電子メールで送信する必要があります。\n領収書には、氏名、国、州、および金額を明確に記載する必要があります。 認証番号を受け取った場合は、メールを確認してください。\n\nそのポップアップを閉じた後、あなたはMoneyGramからお金を得るためのXMR買い手の名前と住所を見られるでしょう。\n\nあなたが正常にお金を得た後にのみ領収書を承認してください! portfolio.pending.step3_seller.westernUnion=買い手はMTCN(追跡番号)と領収書の写真をEメールで送信する必要があります。\n領収書には、氏名、市区町村、国、金額が明確に記載されている必要があります。 MTCNを受け取った場合は、メールを確認してください。\n\nそのポップアップを閉じた後、あなたはWestern Unionからお金を得るためのXMR買い手の名前と住所を見られるでしょう。\n\nあなたが正常にお金を得た後にのみ領収書を承認してください! portfolio.pending.step3_seller.halCash=買い手はHalCashコードをテキストメッセージとして送信する必要があります。それに加えて、HalCash対応ATMからEURを出金するために必要な情報を含むメッセージがHalCashから届きます。\n\nあなたはATMからお金を得た後、ここで支払いの領収書を承認して下さい! portfolio.pending.step3_seller.amazonGiftCard=買い手はEメールアドレス、それともSMSで携帯電話番号までアマゾンeGiftカードを送りました。アマゾンアカウントにeGiftカードを受け取って、済ましたら支払いの受領を確認して下さい。 portfolio.pending.step3_seller.bankCheck=\n\nまた、トレード契約書に記載されている送付者の名前が、銀行取引明細書のものと一致することも確認してください:\nトレード契約書のとおり、送信者の名前: {0}\n\n名前がここに表示されているものと同じではない場合、{1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=支払いの受領を確認せず、「alt + o」または「option + o」を入力して係争を開始して下さい。\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=支払い受領を承認 portfolio.pending.step3_seller.amountToReceive=受取額 portfolio.pending.step3_seller.yourAddress=あなたの{0} アドレス portfolio.pending.step3_seller.buyersAddress=買い手の{0} アドレス portfolio.pending.step3_seller.yourAccount=あなたのトレードアカウント portfolio.pending.step3_seller.xmrTxHash=トランザクションID portfolio.pending.step3_seller.xmrTxKey=トランザクション・キー portfolio.pending.step3_seller.buyersAccount=買い手のアカウント・データ portfolio.pending.step3_seller.confirmReceipt=支払い受領を確認 portfolio.pending.step3_seller.buyerStartedPayment=XMRの買い手が{0}の支払いを開始しました。\n{1} portfolio.pending.step3_seller.buyerStartedPayment.crypto=あなたのアルトコインウォレットやブロックエクスプローラーでブロックチェーンの確認を確認し、十分なブロックチェーンの承認があるときに支払いを確認してください。 portfolio.pending.step3_seller.buyerStartedPayment.traditional=あなたのトレードアカウント(例えば銀行口座)をチェックして、あなたが支払いを受領した時に承認して下さい。 portfolio.pending.step3_seller.warn.part1a={0} blockchain上で portfolio.pending.step3_seller.warn.part1b=支払いプロバイダ(銀行など)で portfolio.pending.step3_seller.warn.part2=あなたはまだ支払いの受領を承認していません。支払い{0}を受け取ったかどうかを確認してください。 portfolio.pending.step3_seller.openForDispute=支払いの受領を承認していません!\nトレードの最大期間が経過しました。\n確認するか、調停人に助けを求めて下さい。 # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=あなたの取引相手から{0}の支払いを受けましたか?\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.name=また、銀行取引明細書に記載されている送付者の名前が、トレード契約書のものと一致していることも確認してください:\nトレード契約書とおり、送信者の名前: {0}\n\n送付者の名前がここに表示されているものと異なる場合は、支払いの受領を承認しないで下さい。「alt + o」または「option + o」を入力して係争を開始して下さい。\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=領収書の確認が済むとすぐに、ロックされたトレード金額がXMRの買い手に解放され、保証金が返金されます。\n\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=支払いを受け取ったことを確認 portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=はい、支払いを受け取りました portfolio.pending.step3_seller.onPaymentReceived.signer=重要:支払いの受け取りを承認すると、相手方のアカウントを検証して署名することになります。相手方のアカウントはまだ署名されていないので、支払取り消しリスクを減らすために支払いの承認をできる限り延期して下さい。 portfolio.pending.step5_buyer.groupTitle=完了したトレードのまとめ portfolio.pending.step5_buyer.tradeFee=取引手数料 portfolio.pending.step5_buyer.makersMiningFee=マイニング手数料 portfolio.pending.step5_buyer.takersMiningFee=合計マイニング手数料 portfolio.pending.step5_buyer.refunded=返金されたセキュリティデポジット portfolio.pending.step5_buyer.withdrawXMR=ビットコインを出金する portfolio.pending.step5_buyer.amount=出金額 portfolio.pending.step5_buyer.withdrawToAddress=出金先アドレス portfolio.pending.step5_buyer.moveToHavenoWallet=資金をHavenoウォレットに保管する portfolio.pending.step5_buyer.withdrawExternal=外部ウォレットに出金する portfolio.pending.step5_buyer.alreadyWithdrawn=資金はすでに出金されています。\nトランザクション履歴を確認してください。 portfolio.pending.step5_buyer.confirmWithdrawal=出金リクエストを承認 portfolio.pending.step5_buyer.amountTooLow=振込金額は、トランザクション手数料および最小可能送信金額(ダスト)よりも少なくなります。 portfolio.pending.step5_buyer.withdrawalCompleted.headline=引き出し完了 portfolio.pending.step5_buyer.withdrawalCompleted.msg=完了した取引は「ポートフォリオ/履歴」に保存されます。\nあなたはすべてのあなたのビットコイン取引を「資金/トランザクション」で見直すことができます portfolio.pending.step5_buyer.bought=購入しました portfolio.pending.step5_buyer.paid=支払いました portfolio.pending.step5_seller.sold=支払いました portfolio.pending.step5_seller.received=受け取りました tradeFeedbackWindow.title=おめでとうございます、トレードが完了しました tradeFeedbackWindow.msg.part1=私達はあなたの体験についての連絡をお待ちしております。 それは私達のソフトウェアを改良して、荒削りな部分を洗練させる手助けになります。 ご意見をお寄せになりたい場合は、こちらの簡単なアンケートにご記入ください(登録不要): tradeFeedbackWindow.msg.part2=ご質問がある場合、または問題が発生した場合は、次のHavenoフォーラムで他のユーザーや貢献者に連絡してください: tradeFeedbackWindow.msg.part3=Havenoを使ってくれてありがとう! portfolio.pending.role=私の役割 portfolio.pending.tradeInformation=トレード情報 portfolio.pending.remainingTime=残り時間 portfolio.pending.remainingTimeDetail={0} ({1}まで) portfolio.pending.remainingTimeDetail.startsAfter={0} 回の承認後に開始します portfolio.pending.tradePeriodInfo={0} 確認後、取引期間が開始されます。使用された支払い方法に基づき、異なる最大取引期間が適用されます。 portfolio.pending.tradePeriodWarning=この期間を超えた場合、両方の取引者が係争を開始できます。 portfolio.pending.tradeNotCompleted=時間内に完了してないトレード({0}まで) portfolio.pending.tradeProcess=トレードプロセス portfolio.pending.openAgainDispute.msg=調停人や調停者へのメッセージが到着したことに確信が持てない場合(例えば、1日経っても返事がない場合)、「command/ctrl+o」で再度係争を申し立てる、あるいは [HYPERLINK:https://haveno.community] でHaveno掲示板からさらにサポートを受けることができます。 portfolio.pending.openAgainDispute.button=もう一度係争を開始 portfolio.pending.openSupportTicket.headline=サポートチケットをオープン portfolio.pending.openSupportTicket.msg=この機能を \"サポートをオープン\" や \"係争を開始\" ボタンが表示されていない緊急の場合のみに利用して下さい。\n\nサポートチケットをオープンすると、トレードは割り込まれ調停人や調停者によって扱われます。 portfolio.pending.timeLockNotOver=係争仲裁を開始するには、≈{0} ({1} ブロック) 確認まで待たなければなりません。 portfolio.pending.error.depositTxNull=入金トランザクションは無効とされました。有効な入金トランザクションがなければ、係争を開始できません。\"設定/ネットワーク情報\"を開いてSPV再同期を行って下さい。\n\nさらにサポートを受けるため、Haveno Keybaseチームのサポートチャンネルに連絡して下さい。 portfolio.pending.mediationResult.error.depositTxNull=デポジットトランザクションは無効とされました。「失敗トレード」へ送れます。 portfolio.pending.mediationResult.error.delayedPayoutTxNull=遅延支払いトランザクションは無効とされました。「失敗トレード」へ送れます。 portfolio.pending.error.depositTxNotConfirmed=入金トランザクションは承認されていません。非確認された入金トランザクションで係争を開始できません。承認まで待つか、\"設定/ネットワーク情報\"を開いてSPV再同期を行って下さい。\n\nさらにサポートを受けるため、Haveno Keybaseチームのサポートチャンネルに連絡して下さい。 portfolio.pending.support.headline.getHelp=助けが必要ですか? portfolio.pending.support.text.getHelp=問題があれば、トレードチャットにトレードピアと連絡してみるか、 https://haveno.community でHavenoコミュニティーから助けを求めることができます。それでも問題は解決されない場合、調停者からさらに助けを求めることもできます。 portfolio.pending.support.button.getHelp=取引者チャットを開く portfolio.pending.support.headline.halfPeriodOver=支払いを確認 portfolio.pending.support.headline.periodOver=トレード期間は終了しました portfolio.pending.support.headline.depositTxMissing=入金トランザクションが見つかりません portfolio.pending.support.depositTxMissing=この取引には入金トランザクションが見つかりません。サポートチケットを開いて、仲裁者に連絡してサポートを受けてください。 portfolio.pending.mediationRequested=調停は依頼されました portfolio.pending.refundRequested=返金は請求されました portfolio.pending.openSupport=サポートチケットをオープン portfolio.pending.supportTicketOpened=サポートチケットがオープンされた portfolio.pending.communicateWithArbitrator=「サポート」画面で調停人と連絡を取ってください。 portfolio.pending.communicateWithMediator=\"サポート\" 画面で調停者と連絡を取ってください。 portfolio.pending.disputeOpenedByUser=あなたは既に係争を開始しています\n{0} portfolio.pending.disputeOpenedByPeer=あなたのトレード相手は係争を開始しました\n{0} portfolio.pending.noReceiverAddressDefined=受信者のアドレスが定義されていません portfolio.pending.mediationResult.headline=調停から提案された支払い portfolio.pending.mediationResult.info.noneAccepted=調停者のトレード支払い提案に応じることでトレードを完了する。 portfolio.pending.mediationResult.info.selfAccepted=調停者の提案を受け入れました。ピアの受け入りを待ち。 portfolio.pending.mediationResult.info.peerAccepted=トレードピアは調停者の提案を受け入れました。同じく提案に応じますか? portfolio.pending.mediationResult.button=提案解決法を表示する portfolio.pending.mediationResult.popup.headline=トレードID {0} の調停の結果 portfolio.pending.mediationResult.popup.headline.peerAccepted=トレードピアは、トレード {0} に関する調停者の提案を受け入れました。 portfolio.pending.mediationResult.popup.info=調停者の資金分け提案は以下のとおり:\nあなたの分: {0}\nトレードピアの分: {1}\n\nこの支払い提案を受け取るまたは断ることができます。\n\n受け取ることで、支払い提案のトランザクションを署名します。トレードピアも同じく受け取って署名すると、支払いは完了しトレードは成立されます。\n\n片当事者もしくは両当事者が提案を断ると、2回目の係争を開始するのに{2} (ブロック {3}) まで待つ必要があります。調停人は再びに問題を検討し、調査結果に基づいて支払い提案を申し出ます。\n\n仕事に対する補償として、調停人は手数料を徴収するかもしれない(手数料の上限:取引者のセキュリティデポジット)。両当事者が提案に応じるのは最高の結果です。調停を依頼するのは異例の事態のためです、例えば取引者が調停者の支払い提案は不正だということを確信している場合(それともピアが無反応になる場合)。\n\n新しい仲裁モデルの詳しくは: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=調停者の支払い提案に応じましたが、トレードピアは断りましたそうです。\n\n{0}のロック時間が終わったら(ブロック{1})、2回目の係争を開始できます、そして調停人は再びに問題を検討し調査結果に基づいて支払い提案を申し出るでしょう。\n\n新しい仲裁モデルの詳しくは: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=拒絶して仲裁を求める portfolio.pending.mediationResult.popup.alreadyAccepted=すでに受け入れています portfolio.pending.failedTrade.taker.missingTakerFeeTx=欠測テイカー手数料のトランザクション。\n\nこのtxがなければ、トレードを完了できません。資金はロックされず、トレード手数料は支払いませんでした。「失敗トレード」へ送ることができます。 portfolio.pending.failedTrade.maker.missingTakerFeeTx=ピアのテイカー手数料のトランザクションは欠測します。\n\nこのtxがなければ、トレードを完了できません。資金はロックされませんでした。あなたのオファーがまだ他の取引者には有効ですので、メイカー手数料は失っていません。このトレードを「失敗トレード」へ送ることができます。 portfolio.pending.failedTrade.missingDepositTx=入金トランザクションが見つかりません。\n\nこのトランザクションは取引を完了するために必要です。Moneroブロックチェーンとウォレットが完全に同期されていることを確認してください。\n\nこの取引を「失敗した取引」セクションに移動して、無効化することができます。 portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=遅延支払いトランザクションは欠測しますが、資金は入金トランザクションにロックされました。\n\nこの法定通貨・アルトコイン支払いをXMR売り手に送信しないで下さい。遅延支払いtxがなければ、係争仲裁は開始されることができません。代りに、「Cmd/Ctrl+o」で調停チケットをオープンして下さい。調停者はおそらく両方のピアへセキュリティデポジットの全額を払い戻しを提案します(売り手はトレード金額も払い戻しを受ける)。このような方法でセキュリティーのリスクがなし、トレード手数料のみが失われます。\n\n失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=遅延支払いトランザクションは欠測しますが、資金は入金トランザクションにロックされました。\n\n買い手の遅延支払いトランザクションが同じく欠測される場合、相手は支払いを送信せず調停チケットをオープンするように指示されます。同様に「Cmd/Ctrl+o」で調停チケットをオープンするのは賢明でしょう。\n\n買い手はまだ支払いを送信しなかった場合、調停者はおそらく両方のピアへセキュリティデポジットの全額を払い戻しを提案します(売り手はトレード金額も払い戻しを受ける)。さもなければ、トレード金額は買い手に支払われるでしょう。\n\n失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=トレードプロトコルの実行にはエラーが生じました。\n\nエラー: {0}\n\nクリティカル・エラーではない可能性はあり、トレードは普通に完了できるかもしれない。迷う場合は調停チケットをオープンして、Haveno調停者からアドバイスを受けることができます。\n\nクリティカル・エラーでトレードが完了できなかった場合はトレード手数料は失われた可能性があります。失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.missingContract=トレード契約書は設定されません。\n\nトレードは完了できません。トレード手数料は失われた可能性もあります。その場合は失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=トレードプロトコルは問題に遭遇しました。\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=トレードプロトコルは深刻な問題に遭遇しました。\n\n{0}\n\nトレードを「失敗トレード」へ送りますか?\n\n「失敗トレード」画面から調停・仲裁を開始できませんけど、失敗トレードがいつでも「オープントレード」へ戻されることができます。 portfolio.pending.failedTrade.txChainValid.moveToFailed=トレードプロトコルは問題に遭遇しました。\n\n{0}\n\nトレードのトランザクションは公開され、資金はロックされました。絶対に確信している場合のみにトレードを「失敗トレード」へ送りましょう。問題を解決できる選択肢に邪魔する可能性はあります。\n\nトレードを「失敗トレード」へ送りますか?\n\n「失敗トレード」画面から調停・仲裁を開始できませんけど、失敗トレードがいつでも「オープントレード」へ戻されることができます。 portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=トレードを「失敗トレード」へ送る portfolio.pending.failedTrade.warningIcon.tooltip=このトレードに関する問題の詳細を開くのにクリックする portfolio.failed.revertToPending.popup=このトレードを「オープントレード」に送りますか? portfolio.failed.revertToPending=トレードを「オープントレード」へ送る portfolio.closed.completed=完了 portfolio.closed.ticketClosed=仲裁をされました portfolio.closed.mediationTicketClosed=調停をされました portfolio.closed.canceled=キャンセルされています portfolio.failed.Failed=失敗 portfolio.failed.unfail=進む前に、必ずデータディレクトリーをバックアップしといて下さい!\nこのトレードを「オープントレード」へ戻しますか?\n失敗トレードにはまり込まれている資金を解放させる方法の1つです。 portfolio.failed.cantUnfail=このトレードは現在のところ「オープントレード」へ戻されることができません。\nトレード {0} が完了された後にもう一度試して下さい。 portfolio.failed.depositTxNull=このトレードは「オープントレード」へ戻されることができません。入金トランザクションは無効とされました。 portfolio.failed.delayedPayoutTxNull=このトレードは「オープントレード」へ戻されることができません。遅延支払いトランザクションは無効とされました。 #################################################################### # Funds #################################################################### funds.tab.deposit=資金を受け取る funds.tab.withdrawal=送金する funds.tab.reserved=予約された資金 funds.tab.locked=ロックされた資金 funds.tab.transactions=トランザクション funds.deposit.unused=未使用 funds.deposit.usedInTx={0}トランザクションで使われています funds.deposit.fundHavenoWallet=Havenoウォレットに入金 funds.deposit.noAddresses=デポジットアドレスはまだ生成されていません funds.deposit.fundWallet=あなたのウォレットに入金 funds.deposit.withdrawFromWallet=ウォレットから資金を送金 funds.deposit.amount=XMRの金額(オプション) funds.deposit.generateAddress=新しいアドレスの生成 funds.deposit.generateAddressSegwit=ネイティブセグウィットのフォーマット(Bech32) funds.deposit.selectUnused=新しいアドレスを生成するのではなく、上の表から未使用のアドレスを選択してください。 funds.withdrawal.arbitrationFee=調停手数料 funds.withdrawal.inputs=インプット選択 funds.withdrawal.useAllInputs=全ての利用可能なインプットを使用 funds.withdrawal.useCustomInputs=任意のインプットを使用 funds.withdrawal.receiverAmount=受信者の金額 funds.withdrawal.senderAmount=送信者の金額 funds.withdrawal.feeExcluded=マイニング手数料を含まない金額 funds.withdrawal.feeIncluded=マイニング手数料を含む金額 funds.withdrawal.fromLabel=アドレスから出金 funds.withdrawal.toLabel=出金先アドレス funds.withdrawal.memoLabel=出金メモ funds.withdrawal.memo=任意入力メモ funds.withdrawal.withdrawButton=選択された出金 funds.withdrawal.noFundsAvailable=出金のための利用可能な資金がありません funds.withdrawal.confirmWithdrawalRequest=出金リクエストを承認 funds.withdrawal.withdrawMultipleAddresses=複数アドレスからの出金({0}) funds.withdrawal.withdrawMultipleAddresses.tooltip=複数アドレスからの出金:\n{0} funds.withdrawal.notEnoughFunds=あなたのウォレットに十分な資金がありません。 funds.withdrawal.selectAddress=表から送信元アドレスを選択 funds.withdrawal.setAmount=出金額を設定 funds.withdrawal.fillDestAddress=あなたの出金先アドレスを記入 funds.withdrawal.warn.noSourceAddressSelected=上の表で送信元アドレスを選択する必要があります。 funds.withdrawal.warn.amountExceeds=選択されたアドレスからは十分な利用可能な資金が得られません。\n上の表で複数の住所を選択するか、またはマイニング料金を含むように手数料の切り替えを変更することを検討してください。 funds.reserved.noFunds=予約された資金はオープンなオファーにはありません funds.reserved.reserved=次のIDとのオファーはローカルウォレットで予約されています: {0} funds.locked.noFunds=ロックされた資金はトレードにはありません funds.locked.locked=次のIDとのトレードはマルチシグでロック中です: {0} funds.tx.direction.sentTo=送信 to: funds.tx.direction.receivedWith=次に予約済: funds.tx.direction.genesisTx=ジェネシスTXから: funds.tx.createOfferFee=メイカーとTXの手数料: {0} funds.tx.takeOfferFee=テイカーとTXの手数料: {0} funds.tx.multiSigDeposit=マルチシグデポジット: {0} funds.tx.multiSigPayout=マルチシグ支払い: {0} funds.tx.disputePayout=係争の支払い: {0} funds.tx.disputeLost=係争事案が消失: {0} funds.tx.collateralForRefund=担保の払い戻し: {0} funds.tx.timeLockedPayoutTx=時間ロック支払いtx: {0} funds.tx.refund=仲裁からの払い戻し: {0} funds.tx.unknown=不明な理由: {0} funds.tx.noFundsFromDispute=係争からの返金はありません funds.tx.receivedFunds=受取済み資金 funds.tx.withdrawnFromWallet=ウォレットからの出金 funds.tx.memo=メモ funds.tx.noTxAvailable=利用できるトランザクションがありません funds.tx.revert=元に戻す funds.tx.txSent=トランザクションはローカルHavenoウォレットの新しいアドレスに正常に送信されました。 funds.tx.direction.self=自分自身に送信済み funds.tx.dustAttackTx=ダストを受取りました funds.tx.dustAttackTx.popup=このトランザクションはごくわずかなXMR金額をあなたのウォレットに送っているので、あなたのウォレットを盗もうとするチェーン解析会社による試みかもしれません。\n\nあなたが支払い取引でそのトランザクションアウトプットを使うならば、彼らはあなたが他のアドレスの所有者である可能性が高いことを学びます(コインマージ)。\n\nあなたのプライバシーを保護するために、Havenoウォレットは、支払い目的および残高表示において、そのようなダストアウトプットを無視します。 設定でアウトプットがダストと見なされるときのしきい値を設定できます。 #################################################################### # Support #################################################################### support.tab.mediation.support=調停 support.tab.arbitration.support=仲裁 support.tab.legacyArbitration.support=レガシー仲裁 support.tab.ArbitratorsSupportTickets={0} のチケット support.sigCheck.button=Check signature support.sigCheck.popup.info=仲裁プロセスの要約メッセージを貼り付けてください。このツールを使用すると、どんなユーザーでも仲裁者の署名が要約メッセージと一致するかどうかを確認できます。 support.sigCheck.popup.header=係争結果の署名を検証する support.sigCheck.popup.msg.label=概要メッセージ support.sigCheck.popup.msg.prompt=係争からの概要メッセージをコピーして貼り付ける support.sigCheck.popup.result=検証結果 support.sigCheck.popup.success=有効な署名です support.sigCheck.popup.failed=署名検証失敗 support.sigCheck.popup.invalidFormat=メッセージは期待されるフォーマットではありません。係争からの概要メッセージをコピーして貼り付けて下さい。 support.reOpenByTrader.prompt=係争を再開しても本当によろしいですか? support.reOpenButton.label=再開する support.sendNotificationButton.label=プライベート通知 support.reportButton.label=報告する support.fullReportButton.label=全ての係争 support.noTickets=オープンなチケットはありません support.sendingMessage=メッセージを送信中 support.receiverNotOnline=受信者はオンラインではありません。 メッセージは彼のメールボックスに保存されます。 support.sendMessageError=メッセージ送信失敗。エラー: {0} support.receiverNotKnown=Receiver not known support.wrongVersion=その係争の申し出はHavenoの古いバージョンで作成されました。\nあなたのアプリケーションのバージョンではその係争を閉じることはできません。\n\n次のより古いバージョンを使用してください:プロトコルバージョン{0} support.openFile=添付ファイルを開く(最大ファイルサイズ: {0} kb) support.attachmentTooLarge=添付ファイルの合計サイズは{0} kbで、最大サイズを超えています。 許容されるメッセージサイズは{1} KBです。 support.maxSize=許容された最大ファイルサイズは{0} KBです。 support.attachment=添付ファイル support.tooManyAttachments=1つのメッセージに3つを超える添付ファイルは送信できません support.save=ファイルをディスクに保存 support.messages=メッセージ support.input.prompt=メッセージを入力... support.send=送信 support.addAttachments=添付ファイルを追加 support.closeTicket=チケットを閉じる support.attachments=添付ファイル: support.savedInMailbox=メッセージ受信箱に保存されました support.arrived=メッセージが受信者へ届きました support.acknowledged=受信者からメッセージ到着が確認されました support.error=受信者がメッセージを処理できませんでした。エラー: {0} support.buyerAddress=XMR買い手のアドレス support.sellerAddress=XMR売り手のアドレス support.role=役割 support.agent=サポート代理人 support.state=状態 support.chat=Chat support.preparing=準備中 support.requested=リクエスト済 support.closed=閉鎖 support.open=オープン support.process=Process support.buyerMaker=XMR 買い手/メイカー support.sellerMaker=XMR 売り手/メイカー support.buyerTaker=XMR 買い手/テイカー support.sellerTaker=XMR 売り手/テイカー support.backgroundInfo=Havenoは企業ではないため、紛争の処理が異なります。\n\n取引者は、アプリケーション内でセキュアなチャットを使用してオープンな取引画面でコミュニケーションを取り、自分自身で紛争を解決しようとすることができます。それが十分でない場合、仲裁者が状況を評価し、取引資金の支払いを決定します。 support.initialInfo=下のテキストフィールドに問題の説明を入力してください。係争解決の時間を短縮するために、可能な限り多くの情報を追加してください。\n\n提供する必要がある情報のチェックリストを次に示します:\n\t●XMR買い手の場合:法定通貨またはアルトコインの送金を行いましたか?その場合、アプリケーションの「支払い開始」ボタンをクリックしましたか?\n\t●XMR売り手の場合:法定通貨またはアルトコインの支払いを受け取りましたか?その場合、アプリケーションの「支払いを受け取った」ボタンをクリックしましたか?\n\t●どのバージョンのHavenoを使用していますか?\n\t●どのオペレーティングシステムを使用していますか?\n\t●失敗したトランザクションで問題が発生した場合は、新しいデータディレクトリへの切り替えを検討してください。\n\t データディレクトリが破損し、不可解なバグが発生している場合があります。\n\t 参照:https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\n係争プロセスの基本的なルールをよく理解してください:\n\t●2日以内に{0}の要求に応答する必要があります。\n\t●調停者は2日以内に返事をするでしょう。調停人は5営業日以内に返事をするでしょう。\n\t●係争の最大期間は14日間です。\n\t●{1}と協力し、彼らがあなたの主張をするために、要求された情報を提供する必要があります\n\t●あなたは申請を最初に開始したときに、ユーザー契約の係争文書に記載されている規則を受け入れています。\n\n係争プロセスの詳細については、{2} をご覧ください。 support.systemMsg=システムメッセージ: {0} support.youOpenedTicket=サポートのリクエスト開始しました。\n\n{0}\n\nHavenoバージョン: {1} support.youOpenedDispute=係争のリクエスト開始しました。\n\n{0}\n\nHavenoバージョン: {1} support.youOpenedDisputeForMediation=調停を求めました。\n\n{0}\n\nHavenoバージョン: {1} support.peerOpenedTicket=トレードピアは技術的な問題によるサポートを要求しました。\n\n{0}\n\nHavenoバージョン: {1} support.peerOpenedDispute=トレードピアは係争を求めました。\n\n{0}\n\nHavenoバージョン: {1} support.peerOpenedDisputeForMediation=トレードピアは調停を求めました。\n\n{0}\n\nHavenoバージョン: {1} support.mediatorsDisputeSummary=システム・メッセージ:調停者の係争概要:\n{0} support.mediatorsAddress=調停人のノードアドレス: {0} support.warning.disputesWithInvalidDonationAddress=遅延支払いトランザクションは無効な受信アドレスを利用しました。有効な寄付アドレスに対するDAOパラメーターと合っていません。\n\n詐欺の未遂かもしれません。開発者に報告して、問題が解決される前に事件を閉じないで下さい!\n\nこの係争に利用されたアドレス: {0}\n\nDAOパラメーターに合う寄付アドレス: {1}\n\nトレードID: {2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\n係争を閉じても本当によろしいですか? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\n支払いを送信してはいけません。 support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. support.info.disputeReOpened=Dispute ticket has been re-opened. #################################################################### # Settings #################################################################### settings.tab.preferences=設定 settings.tab.network=ネットワーク情報 settings.tab.about=About setting.preferences.general=一般設定 setting.preferences.explorer=ビットコインのエクスプローラ setting.preferences.deviation=市場価格からの最大偏差 setting.preferences.avoidStandbyMode=スタンバイモードを避ける setting.preferences.useSoundForNotifications=通知音の再生 setting.preferences.autoConfirmXMR=XMR自動確認 setting.preferences.autoConfirmEnabled=有効されました setting.preferences.autoConfirmRequiredConfirmations=必要承認 setting.preferences.autoConfirmMaxTradeSize=最大トレード金額(XMR) setting.preferences.autoConfirmServiceAddresses=モネロエクスプローラURL(localhost、LANのIPアドレス、または*.localのホストネーム以外はTorを利用します) setting.preferences.deviationToLarge={0}%以上の値は許可されていません。 setting.preferences.txFee=出金トランザクション手数料 (satoshis/vbyte) setting.preferences.useCustomValue=任意の値を使う setting.preferences.txFeeMin=トランザクション手数料は少なくとも{0} satoshis/vbyte でなければなりません setting.preferences.txFeeTooLarge=あなたの入力は妥当な値(> 5000 satoshis / vbyte)を超えています。トランザクション手数料は通常 50-400 satoshis/vbyteの範囲です。 setting.preferences.ignorePeers=無視されたピア [onion アドレス:ポート] setting.preferences.ignoreDustThreshold=最小の非ダストアウトプット値 setting.preferences.currenciesInList=市場価格フィードリストの通貨 setting.preferences.prefCurrency=希望する通貨 setting.preferences.displayTraditional=各国通貨の表示 setting.preferences.noTraditional=各国通貨が選択されていません setting.preferences.cannotRemovePrefCurrency=選択した希望の表示通貨は削除できません setting.preferences.displayCryptos=アルトコインの表示 setting.preferences.noCryptos=選択されたアルトコインがありません setting.preferences.addTraditional=各国通貨を追加する setting.preferences.addCrypto=アルトコインを追加する setting.preferences.displayOptions=表示設定 setting.preferences.showOwnOffers=オファーブックに自分のオファーを表示 setting.preferences.useAnimations=アニメーションを使用 setting.preferences.useDarkMode=ダークモードを利用 setting.preferences.useLightMode=ライトモードを使用する setting.preferences.sortWithNumOffers=市場リストをオファー/トレードの数で並び替える setting.preferences.onlyShowPaymentMethodsFromAccount=サポートされていない支払い方法を非表示にする setting.preferences.denyApiTaker=APIを使用するテイカーを拒否する setting.preferences.notifyOnPreRelease=Receive pre-release notifications setting.preferences.resetAllFlags=「次回から表示しない」フラグを全てリセット settings.preferences.languageChange=言語の変更をすべての画面に適用するには再起動が必要です。 settings.preferences.supportLanguageWarning=係争が発生した場合、仲裁は{0}で処理されることに注意してください。 settings.preferences.editCustomExplorer.headline=エクスプローラー設定 settings.preferences.editCustomExplorer.description=左のリストからシステム定義エクスプローラを選択、それともニーズや好みに合わせてカスタマイズする。 settings.preferences.editCustomExplorer.available=利用可能なエクスプローラ settings.preferences.editCustomExplorer.chosen=選択したエクスプローラ設定 settings.preferences.editCustomExplorer.name=名義 settings.preferences.editCustomExplorer.txUrl=トランザクションURL settings.preferences.editCustomExplorer.addressUrl=アドレスURL setting.info.headline=新しいデータプライバシー機能 settings.preferences.sensitiveDataRemoval.msg=ご自身および他のトレーダーのプライバシーを保護するため、Havenoは過去の取引から機密データを削除する予定です。これは銀行口座情報を含む可能性のある法定通貨の取引で特に重要です。\n\n可能な限り低く設定することをお勧めします。例えば60日です。これは、60日以上前の完了した取引の機密データが削除されることを意味します。完了した取引はポートフォリオ/履歴タブで確認できます。 settings.net.xmrHeader=ビットコインのネットワーク settings.net.p2pHeader=Havenoネットワーク settings.net.onionAddressLabel=私のonionアドレス settings.net.xmrNodesLabel=任意のモネロノードを使う settings.net.moneroPeersLabel=接続されたピア settings.net.connection=接続 settings.net.connected=接続されました settings.net.useTorForXmrJLabel=MoneroネットワークにTorを使用 settings.net.moneroNodesLabel=接続するMoneroノード: settings.net.useProvidedNodesRadio=提供されたMonero Core ノードを使う settings.net.usePublicNodesRadio=ビットコインの公共ネットワークを使用 settings.net.useCustomNodesRadio=任意のビットコインノードを使う settings.net.warn.usePublicNodes=パブリックなMoneroノードを使用する場合、信頼できないリモートノードを使用するリスクにさらされる可能性があります。\n\n詳細については、[HYPERLINK:https://www.getmonero.org/resources/moneropedia/remote-node.html] をご覧ください。\n\nパブリックノードを使用することを確認してもよろしいですか? settings.net.warn.usePublicNodes.useProvided=いいえ、提供されたノードを使用します settings.net.warn.usePublicNodes.usePublic=はい、公共ネットワークを使います settings.net.warn.useCustomNodes.B2XWarning=あなたのMoneroノードが信頼できるMonero Coreノードであることを確認してください!\n\nMonero Coreのコンセンサスルールに従わないノードに接続すると、ウォレットが破損し、トレードプロセスに問題が生じる可能性があります。\n\nコンセンサスルールに違反するノードへ接続したユーザーは、引き起こされるいかなる損害に対しても責任を負います。 結果として生じる係争は、他のピアによって決定されます。この警告と保護のメカニズムを無視しているユーザーには、テクニカルサポートは提供されません! settings.net.warn.invalidXmrConfig=無効な設定によりビットコインネットワークとの接続は失敗しました。\n\n代りに提供されたビットコインノードを利用するのに設定はリセットされました。アプリを再起動する必要があります。 settings.net.localhostXmrNodeInfo=バックグラウンド情報:Havenoが起動時に、ローカルビットコインノードを探します。見つかれば、Havenoはそのノードを排他的に介してビットコインネットワークと接続します。 settings.net.p2PPeersLabel=接続されたピア settings.net.onionAddressColumn=Onionアドレス settings.net.creationDateColumn=既定 settings.net.connectionTypeColumn=イン/アウト settings.net.sentDataLabel=通信されたデータ統計 settings.net.receivedDataLabel=受信されたデータ統計 settings.net.chainHeightLabel=XMRの最新ブロック高さ settings.net.roundTripTimeColumn=往復 settings.net.sentBytesColumn=送信済 settings.net.receivedBytesColumn=受信済 settings.net.peerTypeColumn=ピアタイプ settings.net.openTorSettingsButton=Torの設定を開く settings.net.versionColumn=バージョン settings.net.subVersionColumn=サブバージョン settings.net.heightColumn=高さ settings.net.needRestart=その変更を適用するには、アプリケーションを再起動する必要があります。\n今すぐ行いますか? settings.net.notKnownYet=まだわかりません... settings.net.sentData=通信されたデータ: {0}, {1} メッセージ、 {2} メッセージ/秒 settings.net.receivedData=受信されたデータ: {0}, {1} メッセージ、 {2} メッセージ/秒 settings.net.chainHeight=Monero Peers chain height: {0} settings.net.ips=[IPアドレス:ポート | ホスト名:ポート | onionアドレス:ポート](コンマ区切り)。デフォルト(8333)が使用される場合、ポートは省略できます。 settings.net.seedNode=シードノード settings.net.directPeer=ピア (ダイレクト) settings.net.initialDataExchange={0} [ ブートストラップ 中] settings.net.peer=ピア settings.net.inbound=インバウンド settings.net.outbound=アウトバウンド setting.about.aboutHaveno=Havenoについて setting.about.about=Havenoは、ユーザーのプライバシーを強力に保護する方法で、非中央集権型のピアツーピアネットワークを介して各国通貨(およびその他の暗号通貨)とのビットコインの交換を容易にするオープンソースソフトウェアです。 Havenoの詳細については、プロジェクトのWebページをご覧ください。 setting.about.web=Havenoホームページ setting.about.code=ソースコード setting.about.agpl=AGPLライセンス setting.about.support=Havenoをサポートする setting.about.def=Havenoは会社ではなく、開かれたコミュニティのプロジェクトです。Havenoにサポートしたい時は下のURLをチェックしてください。 setting.about.contribute=貢献 setting.about.providers=データプロバイダー setting.about.apisWithFee=Havenoは、法定通貨とアルトコインの市場価格の推定にHaveno物価指数を利用し、マイニング手数料の推定にMempoolノードを使用します。 setting.about.apis=Havenoは、法定通貨とアルトコインの市場価格の推定にHaveno物価指数を利用します。 setting.about.pricesProvided=市場価格を提供している: setting.about.feeEstimation.label=推定マイニング手数料の提供: setting.about.versionDetails=バージョン詳細 setting.about.version=アプリのバージョン setting.about.subsystems.label=サブシステムのバージョン setting.about.subsystems.val=ネットワークバージョン: {0}; P2Pメッセージバージョン: {1}; ローカルDBバージョン: {2}; トレードプロトコルバージョン: {3} setting.about.shortcuts=ショートカット setting.about.shortcuts.ctrlOrAltOrCmd=「Ctrl + {0}」または「Alt + {0}」それとも「Cmd + {0}」 setting.about.shortcuts.menuNav=メインメニューをナビゲートする setting.about.shortcuts.menuNav.value=メインメニューをナビゲートするのに:「Ctrl」または「Alt」それとも「Cmd」キーと1-9の数字キーを押して下さい。 setting.about.shortcuts.close=Havenoを閉じる setting.about.shortcuts.close.value=「Ctrl + {0}」か「cmd + {0}」、それとも「Ctrl + {1}」か「cmd + {1}」 setting.about.shortcuts.closePopup=ポップアップやダイアログ・ウィンドウを閉じる setting.about.shortcuts.closePopup.value=エスケープキー setting.about.shortcuts.chatSendMsg=取引者チャットメッセージを送る setting.about.shortcuts.chatSendMsg.value=「Ctrl + ENTER」または「Alt + ENTER」それとも「cmd + ENTER」 setting.about.shortcuts.openDispute=係争を開始 setting.about.shortcuts.openDispute.value=未決トレードを選択してクリックする:{0} setting.about.shortcuts.walletDetails=ウォレット詳細ウィンドウを開く setting.about.shortcuts.openEmergencyXmrWalletTool=XMRウォレットに対する緊急ウォレットツールを開く setting.about.shortcuts.showTorLogs=TorメッセージのログレベルをDEBUGとWARNを切り替える setting.about.shortcuts.manualPayoutTxWindow=2of2マルチシグ入金txから手動支払いのウィンドウを開く setting.about.shortcuts.removeStuckTrade=Open popup to move failed trade to open trades tab again setting.about.shortcuts.removeStuckTrade.value=Select failed trade and press: {0} setting.about.shortcuts.registerArbitrator=調停人を登録(調停者/調停人のみ) setting.about.shortcuts.registerArbitrator.value=アカウントへナビゲートして押す: {0} setting.about.shortcuts.registerMediator=調停者を登録(調停者/調停人のみ) setting.about.shortcuts.registerMediator.value=アカウントへナビゲートして押す: {0} setting.about.shortcuts.openSignPaymentAccountsWindow=アカウント年齢署名のウィンドウを開く(レガシー調停人のみ) setting.about.shortcuts.openSignPaymentAccountsWindow.value=レガシー調停人表示へナビゲートして押す: {0} setting.about.shortcuts.sendAlertMsg=アラートまたはアップデートメッセージを送る(特権的行為) setting.about.shortcuts.sendFilter=フィルターを設定する(特権的行為) setting.about.shortcuts.sendPrivateNotification=ピアにプライベート通知を送る(特権的行為) setting.about.shortcuts.sendPrivateNotification.value=アバターからピア情報を開いて押す:{0} setting.info.headline=新しいXMR自動確認の機能 setting.info.msg=XMR売ってXMR買う場合、Havenoが自動的にトレードを完了としてマークできるように自動確認機能で適正量のXMRはウォレットに送られたかを検証できます。その際、皆にトレードをより早く完了できるようにします。\n\n自動確認はXMR送信者が提供するプライベート・トランザクション・キーを利用して少なくとも2つのXMRエクスプローラノードでXMRトランザクションを確認します。デフォルト設定でHavenoは貢献者に管理されるエクスプローラノードを利用しますが、最大のプライバシーやセキュリティーのため自分のXMRエクスプローラノードを管理するのをおすすめします。\n\n1つのトレードにつき自動確認する最大額のXMR、そして必要承認の数をこの画面で設定できます。\n\nHavenoのWikiから詳細(自分のエクスプローラノードを管理する方法も含めて)を参照できます: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades] #################################################################### # Account #################################################################### account.tab.mediatorRegistration=調停者登録 account.tab.refundAgentRegistration=仲裁人登録 account.tab.signing=署名中 account.info.headline=あなたのHavenoアカウントへようこそ! account.info.msg=ここでは、各国通貨とアルトコインのトレードアカウントを追加したり、ウォレットやアカウントデータのバックアップを作成することができます。\n\nHavenoは最初に起動した時、新しいビットコインウォレットが自動的に作られました。\n\nビットコインウォレットのシードワードを書き留めて(上部のタブを参照)、入金の前にパスワードを追加することを検討することを強くお勧めします。 ビットコインの入出金は「資金」セクションで管理されます。\n\nプライバシーとセキュリティに関するメモ: Havenoは非中央集権型の交換であるため、すべてのデータはコンピュータに保存されています。 サーバーがないので、私たちはあなたの個人情報、あなたの資金、あるいはあなたのIPアドレスにさえもアクセスできません。 銀行口座番号、アルトコイン&ビットコインのアドレスなどのデータは、あなたが開始したトレードを遂行するためにあなたのトレード相手とだけ共有されます(係争の場合には調停人があなたのトレードピアと同じデータを見るでしょう)。 account.menu.paymentAccount=各国通貨口座 account.menu.altCoinsAccountView=アルトコインアカウント account.menu.password=ウォレットのパスワード account.menu.seedWords=ウォレットシード account.menu.walletInfo=ウォレット情報 account.menu.backup=バックアップ account.menu.notifications=通知 account.menu.walletInfo.balance.headLine=ウォレット残高 account.menu.walletInfo.balance.info=非確認されたトランザクションも含めて、内部ウォレット残高を表示します。\nXMRの場合、下に表示される「内部ウォレット残高」はこのウィンドウの右上に表示される「利用可能」と「予約済」の和に等しいはずです。 account.menu.walletInfo.xpub.headLine=ウォッチキー(xpubキー) account.menu.walletInfo.walletSelector={0} {1} ウォレット account.menu.walletInfo.path.headLine=HDキーチェーンのパス account.menu.walletInfo.path.info=他のウォレット(例えばElectrum)にシードワードをインポートすると、パスを設定しなければなりません。 Havenoウォレットとデータディレクトリーをアクセスできないような緊急事態の場合のみにして下さい。\nHavenoでないウォレットからその資金を遣うと、ウォレットデータと繋がっているHaveno内部データ構造は破損される可能性があり、失敗トレードにつながる可能性があります。\n\n絶対にHavenoでないウォレットからBSQを遣わないで下さい。無効なトランザクションになる可能性が高い、そしてBSQを失うことになるでしょう。 account.menu.walletInfo.openDetails=生ウォレット詳細、秘密鍵を表示する ## TODO should we rename the following to a gereric name? account.arbitratorRegistration.pubKey=パブリックキー account.arbitratorRegistration.register=登録する account.arbitratorRegistration.registration={0} 登録 account.arbitratorRegistration.revoke=取り消し account.arbitratorRegistration.info.msg={0} としてあなたを使用しているトレードが存在する可能性があるので、取り消し後の15日間にかけて必ず待機しておいて下さい。許可されるトレード期間は8日間で、係争処理には最長で7日間かかる場合があります。 account.arbitratorRegistration.warn.min1Language=少なくとも1つの言語を設定する必要があります。\nデフォルトの言語を追加しました。 account.arbitratorRegistration.removedSuccess=Havenoネットワークから登録を正常に削除しました。 account.arbitratorRegistration.removedFailed=登録を削除できませんでした。{0} account.arbitratorRegistration.registerSuccess=Havenoネットワークに正常に登録しました。 account.arbitratorRegistration.registerFailed=登録を完了できませんでした。{0} account.crypto.yourCryptoAccounts=あなたのアルトコインアカウント account.crypto.popup.wallet.msg={1} のWebページに記載されているように、{0}ウォレットの使用に関する要件に必ず従ってください。\n(a)あなたが自分で鍵を管理していない、または(b)互換性のあるウォレットソフトウェアを使用していないような、中央集権化された取引所でウォレットを使用することは危険です。トレード資金の損失につながる可能性があります!\n調停者や調停人は{2}スペシャリストではなく、そのような場合には手助けできません。 account.crypto.popup.wallet.confirm=どのウォレットを使うべきか理解しており、承認する # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=Trading UPX on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending UPX, you need to use either the official uPlexa GUI wallet or uPlexa CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\nuplexa-wallet-cli (use the command get_tx_key)\nuplexa-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The UPX sender is responsible for providing verification of the UPX transfer to the arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit uPlexa discord channel (https://discord.gg/vhdNSrV) or the uPlexa Telegram Chat (https://t.me/uplexaOfficial) to find more information. # suppress inspection "UnusedProperty" account.crypto.popup.arq.msg=HavenoでARQをトレードするには、次の要件を理解し、満たす必要があります。\n\nARQを送信するには、store-tx-infoフラグを有効(新しいバージョンではデフォルト)にした公式のArQmA GUIウォレットまたはArQmA CLIウォレットのいずれかを使用する必要があります。係争が発生した場合に必要になるため、txキーにアクセスできることを確認してください。\narqma-wallet-cli(コマンドget_tx_keyを使用)\narqma-wallet-gui(履歴タブに移動し、支払い証明のために(P)ボタンをクリックします)\n\n通常のブロックエクスプローラーでは、転送は検証できません。\n\n係争の場合、調停人に次のデータを提供する必要があります。\n-txプライベートキー\n-トランザクションハッシュ\n-受信者のパブリックアドレス\n\n上記のデータを提供しない場合、または互換性のないウォレットを使用した場合は、係争のケースが失われます。 ARQ送信者は、係争の場合にARQ転送の検証を調停人に提供する責任があります。\n\n支払いIDは不要で、通常のパブリックアドレスのみです。\nこのプロセスがわからない場合は、ArQmA Discordチャンネル( https://discord.gg/s9BQpJT )またはArQmAフォーラム( https://labs.arqma.com )にアクセスして、詳細を確認してください。 # suppress inspection "UnusedProperty" account.crypto.popup.xmr.msg=HavenoでXMRをトレードするには、以下の要件を理解し、満たす必要があります。\n\nXMRを売る場合、係争を解決するため調停者や調停人に以下の情報を提供できる必要があります:\n- トランザクションキー(Txキー、Tx秘密キー、Txプライベートキー)\n- トランザクションID(TxID、Txハッシュ)\n- 宛先アドレス(受領者のアドレス)\n\n人気のモネロウォレットからこういう情報を見つける方法について、HavenoのWikiを参照して下さい [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Proving_payments]\n必要のトランザクションデータを提供しなければ、係争で不利な裁定を下されます。\n\nHavenoではXMRトランザクションに自動確認機能を提供しますが、設で有効にする必要があります。\n\n自動確認機能について詳しくはWikiで参照して下さい:\n[HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades] # suppress inspection "UnusedProperty" account.crypto.popup.msr.msg=Trading MSR on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending MSR, you need to use either the official Masari GUI wallet, Masari CLI wallet with the store-tx-info flag enabled (enabled by default) or the Masari web wallet (https://wallet.getmasari.org). Please be sure you can access the tx key as that would be required in case of a dispute.\nmasari-wallet-cli (use the command get_tx_key)\nmasari-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nMasari Web Wallet (goto Account -> transaction history and view details on your sent transaction)\n\nVerification can be accomplished in-wallet.\nmasari-wallet-cli : using command (check_tx_key).\nmasari-wallet-gui : on the Advanced > Prove/Check page.\nVerification can be accomplished in the block explorer \nOpen block explorer (https://explorer.getmasari.org), use the search bar to find your transaction hash.\nOnce transaction is found, scroll to bottom to the 'Prove Sending' area and fill in details as needed.\nYou need to provide the mediator or arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The MSR sender is responsible for providing verification of the MSR transfer to the mediator or arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process, ask for help on the Official Masari Discord (https://discord.gg/sMCwMqs). # suppress inspection "UnusedProperty" account.crypto.popup.blur.msg=HavenoでBLURをトレードするには、次の要件を理解し、満たす必要があります。\n\nBLURを送信するには、Blur Network CLIまたはGUI ウォレットを使用する必要があります。\n\nCLIウォレットを使用している場合、転送の送信後にトランザクションハッシュ(tx ID)が表示されます。この情報を保存する必要があります。転送を送信した直後に、コマンド「get_tx_key」を使用してトランザクションプライベートキーを取得する必要があります。この手順を実行しないと、後でキーを取得できない場合があります。\n\nBlur Network GUIウォレットを使用している場合、トランザクションのプライベートキーとトランザクションIDは「履歴」タブで簡単に見つけることができます。送信後すぐに、目的のトランザクションを見つけてください。このトランザクションを含むボックスの右下隅にある「?」記号をクリックしてください。この情報を保存する必要があります。\n\n調停が必要な場合は、1) トランザクションID、2) トランザクションプライベートキー、3) 受信者のアドレス を調停人に提示する必要があります。調停人は、Blur Transaction Viewer( https://blur.cash/#tx-viewer )を使用してBLUR転送を検証します。\n\n必要な情報を調停人に提供しないと、係争のケースが失われます。係争のすべての場合において、BLUR送信者は、調停人に対する取引を確認する責任の100%を負担します。\n\nこれらの要件を理解していない場合は、Havenoで取引しないでください。まず、Blur Network Discord( https://discord.gg/dMWaqVW )で助けを求めてください。 # suppress inspection "UnusedProperty" account.crypto.popup.solo.msg=Trading Solo on Haveno requires that you understand and fulfill the following requirements:\n\nTo send Solo you must use the Solo Network CLI Wallet. \n\nIf you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the Solo transfer using the Solo Block Explorer by searching for the transaction and then using the "Prove sending" function (https://explorer.minesolo.com/).\n\nfailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the Solo sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the Solo Network Discord (https://discord.minesolo.com/). # suppress inspection "UnusedProperty" account.crypto.popup.cash2.msg=HavenoでCASH2をトレードするには、次の要件を理解し、満たす必要があります。\n\nCASH2を送信するには、Cash2 Walletバージョン3以降を使用する必要があります。\n\nトランザクションが送信された後、トランザクションIDが表示されます。この情報を保存する必要があります。トランザクションを送信した直後に、simplewalletのコマンド「getTxKey」を使用して、トランザクションのプライベートキーを取得する必要があります。\n\n調停が必要な場合は、1) トランザクションID、2) トランザクションプライベートキー、および 3) 受信者のCash2アドレス を調停人に提示する必要があります。その後、調停人は、Cash2 Block Explorer( https://blocks.cash2.org )を使用してCASH2転送を検証します。\n\n必要な情報を調停人に提供しないと、係争のケースが失われます。係争のすべての場合において、CASH2の送信者が、仲裁人への取引を確認する責任を100%負います。\n\nこれらの要件を理解していない場合は、Havenoで取引しないでください。まず、Cash2 Discord( https://discord.gg/FGfXAYN )で助けを求めてください。 # suppress inspection "UnusedProperty" account.crypto.popup.qwertycoin.msg=HavenoでQwertycoinをトレードするには、次の要件を理解し、満たす必要があります。\n\nQWCを送信するには、公式のQWCウォレットバージョン5.1.3以降を使用する必要があります。\n\nトランザクションが送信された後、トランザクションIDが表示されます。この情報を保存する必要があります。トランザクションを送信した直後に、simplewalletのコマンド「get_Tx_Key」を使用してトランザクションのプライベートキーを取得する必要があります。\n\n調停が必要な場合は、1) トランザクションID、2) トランザクションプライベートキー、3)受信者のQWCアドレス を調停人に提示する必要があります。その後、調停人はQWC Block Explorer( https://explorer.qwertycoin.org )を使用してQWC転送を検証します。\n\n必要な情報を調停人に提供しないと、係争のケースが失われます。係争のすべての場合において、QWCの送信者が、調停人のトレードの検証における100%の責任を負います。\n\nこれらの要件を理解していない場合は、Havenoで取引しないでください。まず、QWC Discord( https://discord.gg/rUkfnpC )で助けを求めてください。 # suppress inspection "UnusedProperty" account.crypto.popup.drgl.msg=HavenoでDragonglassをトレードするには、次の要件を理解し、満たす必要があります。\n\nDragonglassが提供するプライバシーのため、トランザクションはパブリックブロックチェーンでは検証できません。必要に応じて、TXN-Private-Keyを使用して支払いを証明できます。\nTXN-Private-Keyは、DRGLウォレット内からのみアクセスできるトランザクションごとに自動的に生成されるワンタイムキーです。\nDRGLウォレットGUI(トランザクション詳細ダイアログ内)またはDragonglass CLIシンプルウォレット(コマンド「get_tx_key」を使用)のいずれか。\n\nDRGLバージョン「Oathkeeper」以降には両方が「必要」です。\n\n係争が発生した場合、調停人に次のデータを提供する必要があります。\n-TXN-Private-Key\n-トランザクションハッシュ\n-受信者のパブリックアドレス\n\n上記のデータを( http://drgl.info/#check_txn )の入力として使用して、支払いの検証を行うことができます。\n\n上記のデータを提供しない場合、または互換性のないウォレットを使用した場合は、係争のケースが失われます。 Dragonglassの送信者は、係争の場合にDRGL転送の調停人に検証を提供する責任があります。 PaymentIDの使用は必要ありません。\n\nこのプロセスについて不明な点がある場合は、Dragonglass on Discord( http://discord.drgl.info )にアクセスしてください。 # suppress inspection "UnusedProperty" account.crypto.popup.ZEC.msg=Zcashを使用する場合、調停者や調停人はzアドレスを持つトランザクションを検証できないため、zアドレス(プライベート)ではなく、透過アドレス(tで始まる)のみを使用できます。 # suppress inspection "UnusedProperty" account.crypto.popup.XZC.msg=Zcoinを使用する場合、調停者や調停人はブロックエクスプローラーで追跡不可能なアドレスを持つトランザクションを検証できないため、追跡不可能なアドレスではなく、透過(追跡可能な)アドレスのみを使用できます。 # suppress inspection "UnusedProperty" account.crypto.popup.grin.msg=GRINでは、トランザクションを作成するために送信者と受信者の間の対話型プロセスが必要です。 GRINプロジェクトのWebページの指示に従って、GRINを確実に送受信してください(受信者はオンラインであるか、特定の時間枠内で少なくともオンラインである必要があります)。\n\nHavenoは、Grinbox(Wallet713)ウォレットURL形式のみをサポートします。\n\nGRIN送信者は、GRINが正常に送信されたことを証明する必要があります。ウォレットがその証拠を提供できない場合、起こり得る係争はGRIN受信者に有利に解決されるでしょう。トランザクションプルーフをサポートする最新のGrinboxソフトウェアを使用し、GRINの送受信プロセスとプルーフの作成方法を理解してください。\n\nGrinboxプルーフツールの詳細については、https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only を参照してください。 # suppress inspection "UnusedProperty" account.crypto.popup.beam.msg=BEAMではトランザクションを作成するために、送信者と受信者の間で対話型プロセスが必要です。\n\n必ずBEAMプロジェクトのWebページの指示に従って、BEAMを確実に送受信してください(受信者はオンラインであるか、特定の時間枠で少なくともオンラインである必要があります)。\n\nBEAM送信者は、BEAMが正常に送信されたことを証明する必要があります。そのような証拠を作成できるウォレットソフトウェアを使用してください。ウォレットが証拠を提供できない場合、起こり得る論争はBEAM受信者に有利に解決されるでしょう。 # suppress inspection "UnusedProperty" account.crypto.popup.pars.msg=ParsiCoinをHavenoでトレードするには、次の要件を理解し、満たす必要があります。\n\nPARSを送信するには、公式のParsiCoin Walletバージョン3.0.0以降を使用する必要があります。\n\nGUIウォレット(ParsiPay)のTransactionsセクションでトランザクションハッシュとトランザクションキーを確認できます。Transactionを右クリックして、show detailsをクリックします。\n\n調停が必要な場合は、1) トランザクションハッシュ、2) トランザクションキー、および3) 受信者のPARSア​​ドレス を調停人に提示する必要があります。その後、調停人は、ParsiCoin Block Explorer( http://explorer.parsicoin.net/#check_payment )を使用してPARS転送を検証します。\n\n必要な情報を調停人に提供しないと、係争のケースが失われます。係争のすべての場合において、ParsiCoinの送信者が、係争人のトランザクションの検証における100%の責任を負います。\n\nこれらの要件を理解していない場合は、Havenoで取引しないでください。まず、ParsiCoin Discord( https://discord.gg/c7qmFNh )で助けを求めてください。 # suppress inspection "UnusedProperty" account.crypto.popup.blk-burnt.msg=To trade burnt blackcoins, you need to know the following:\n\nBurnt blackcoins are unspendable. To trade them on Haveno, output scripts need to be in the form: OP_RETURN OP_PUSHDATA, followed by associated data bytes which, after being hex-encoded, constitute addresses. For example, burnt blackcoins with an address 666f6f (“foo” in UTF-8) will have the following script:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\nTo create burnt blackcoins, one may use the “burn” RPC command available in some wallets.\n\nFor possible use cases, one may look at https://ibo.laboratorium.ee .\n\nAs burnt blackcoins are unspendable, they can not be reselled. “Selling” burnt blackcoins means burning ordinary blackcoins (with associated data equal to the destination address).\n\nIn case of a dispute, the BLK seller needs to provide the transaction hash. # suppress inspection "UnusedProperty" account.crypto.popup.liquidmonero.msg=HavenoでL-XMRをトレードするには、以下を理解する必要があります:\n\nHavenoでのトレードにL-XMRを受け取る場合、モバイル用「Blockstream Green」ウォレットアプリそれとも取引場などの第三者によって保管されるウォレットの利用は不可能です。「Liquid Elements Core」ウォレット、あるいは機密L-XMRアドレスの「blindingキー」が入手可能のウォレットのみにL-XMRを受け取って下さい。\n\n調停が必要になる場合、あるいはトレード係争が開始される場合、調停者や調停人が「Elements Core」フルノードで機密トランザクションを検証できるように、受取アドレスのblindingキーを明かす必要があります。\n\n調停者や調停人に必要な情報を提供しなければ、係争で不利な裁定を下されます。全ての係争には、調停者や調停人に暗号証明を提供するのは100%受信者の責任です。\n\n以上の条件を理解しない場合、HavenoでL-XMRのトレードをしないで下さい。 account.traditional.yourTraditionalAccounts=あなたの各国通貨口座 account.backup.title=ウォレットのバックアップ account.backup.location=バックアップの場所 account.backup.selectLocation=バックアップの場所を選択 account.backup.backupNow=今すぐバックアップ(バックアップは暗号化されていません!) account.backup.appDir=アプリケーションデータディレクトリー account.backup.openDirectory=ディレクトリーを開く account.backup.openLogFile=ログファイルを開く account.backup.success=次の場所へのバックアップに成功しました:\n{0} account.backup.directoryNotAccessible=選択されたディレクトリーにはアクセスできません。{0} account.password.removePw.button=パスワードを削除 account.password.removePw.headline=ウォレットのパスワード保護を削除 account.password.setPw.button=パスワードをセット account.password.setPw.headline=ウォレットのパスワード保護をセット account.password.info=パスワード保護が有効な場合、アプリケーションを起動する際、ウォレットからモネロを引き出す際、およびシードワードを表示する際にパスワードを入力する必要があります。 account.seed.backup.title=ウォレットのシードワードをバックアップしてください。 account.seed.info=ウォレットのシードワードと日付を両方記録してください。シードワードと日付があれば、いつでもウォレットを回復できます。\n\nシードワードは紙に記録してください。コンピュータに保存しないでください。\n\nシードワードはバックアップの代替ではないことに注意してください。\nアプリケーションの状態とデータを回復するには、\"アカウント/バックアップ\"画面からアプリケーションディレクトリ全体のバックアップを作成する必要があります。 account.seed.backup.warning=シードワードはバックアップの代替ではありません。\nアプリケーションの状態とデータを回復するには、\"アカウント/バックアップ\"画面からアプリケーションディレクトリ全体のバックアップを作成する必要があります。 account.seed.warn.noPw.msg=シードワードの表示を保護するためのウォレットパスワードを設定していません。 \n\nシードワードを表示しますか? account.seed.warn.noPw.yes=はい、そして次回から確認しないで下さい account.seed.enterPw=シードワードを見るためにパスワードを入力 account.seed.restore.info=シードワードから復元を適用する前に、バックアップを作成してください。ウォレットの復元は緊急時のみであり、内部ウォレットデータベースに問題を引き起こす可能性があることに注意してください。 \nこれはバックアップを適用する方法ではありません!以前のアプリケーションの状態を復元するには、アプリケーションデータディレクトリのバックアップを使用してください。\n\nアプリケーションを復元すると、自動的にシャットダウンします。アプリケーションを再起動すると、ビットコインネットワークと再同期します。特にウォレットが古く、多くのトランザクションがあった場合、これには時間がかかり、CPUを多く消費する可能性があります。このプロセスは中断しないでください。さもなければ、SPVチェーンファイルを再度削除するか、復元プロセスを繰り返す必要があります。 account.seed.restore.ok=わかりました、復元してHavenoをシャットダウンしてください #################################################################### # Mobile notifications #################################################################### account.notifications.setup.title=セットアップ account.notifications.download.label=モバイルアプリをダウンロードする account.notifications.waitingForWebCam=webcamを待っています... account.notifications.webCamWindow.headline=携帯でQRコードをスキャン account.notifications.webcam.label=ウェブカメラを使う account.notifications.webcam.button=QRコードをスキャンする account.notifications.noWebcam.button=ウェブカメラを持ってない account.notifications.erase.label=スマホの通知を削除 account.notifications.erase.title=通知を削除 account.notifications.email.label=ペア通貨 account.notifications.email.prompt=メールで受け取るペア通貨を入力 account.notifications.settings.title=設定 account.notifications.useSound.label=スマホの通知音を再生 account.notifications.trade.label=トレードメッセージを受信 account.notifications.market.label=オファーアラートを受信 account.notifications.price.label=価格アラートを受信 account.notifications.priceAlert.title=価格アラート account.notifications.priceAlert.high.label=XMR価格が次を上回ったら通知 account.notifications.priceAlert.low.label=XMR価格が次を下回ったら通知 account.notifications.priceAlert.setButton=価格アラートをセット account.notifications.priceAlert.removeButton=価格アラートを削除 account.notifications.trade.message.title=トレード状態が変わった account.notifications.trade.message.msg.conf=ID {0}とのトレードのデポジットトランザクションが承認されました。 Havenoアプリケーションを開いて支払いを開始してください。 account.notifications.trade.message.msg.started=XMRの買い手がID {0}とのトレードの支払いを開始しました。 account.notifications.trade.message.msg.completed=ID {0}とのトレードが完了しました。 account.notifications.offer.message.title=オファーが受け入れられました account.notifications.offer.message.msg=ID {0}とのオファーが受け入れられました account.notifications.dispute.message.title=新しい係争メッセージ account.notifications.dispute.message.msg=ID {0}とのトレードに関する係争メッセージを受け取りました account.notifications.marketAlert.title=オファーのアラート account.notifications.marketAlert.selectPaymentAccount=支払いアカウントと一致するオファー account.notifications.marketAlert.offerType.label=興味のあるオファータイプ account.notifications.marketAlert.offerType.buy=購入のオファー(XMRを売りたい) account.notifications.marketAlert.offerType.sell=売却のオファー(XMRを買いたい) account.notifications.marketAlert.trigger=オファー価格の乖離 (%) account.notifications.marketAlert.trigger.info=価格乖離を設定すると、要件を満たす(または超える)オファーが公開されたときにのみアラートを受信します。例:XMRを売りたい時、現在の市場価格に対して2%のプレミアムでのみ販売。このフィールドを2%に設定すると、現在の市場価格よりも2%(またはそれ以上)高い価格のオファーのアラートのみを受け取るようになります。 account.notifications.marketAlert.trigger.prompt=市場価格からの乖離の割合(例:2.50%、-0.50%など) account.notifications.marketAlert.addButton=オファーアラートを追加 account.notifications.marketAlert.manageAlertsButton=オファーアラートを管理 account.notifications.marketAlert.manageAlerts.title=オファーアラートを管理 account.notifications.marketAlert.manageAlerts.header.paymentAccount=支払いアカウント account.notifications.marketAlert.manageAlerts.header.trigger=価格トリガー account.notifications.marketAlert.manageAlerts.header.offerType=オファーの種類 account.notifications.marketAlert.message.title=オファーのアラート account.notifications.marketAlert.message.msg.below=以下 account.notifications.marketAlert.message.msg.above=以上 account.notifications.marketAlert.message.msg=新しい「{0} {1}」オファーが{2} ({3} {4}市場価格)の価格、支払い方法[{5}」でHavenoオファーブックに発行されました。\nオファーID: {6}。 account.notifications.priceAlert.message.title={0}で価格アラート account.notifications.priceAlert.message.msg=価格アラートがトリガーされました。現在の{0}価格は{1} {2}です account.notifications.noWebCamFound.warning=ウェブカメラが見つかりません。\n\n電子メールオプションを使用して、トークンと暗号化キーを携帯電話からHavenoアプリケーションに送信してください。 account.notifications.priceAlert.warning.highPriceTooLow=最高価格は最低価格よりも高い必要があります account.notifications.priceAlert.warning.lowerPriceTooHigh=最低価格は最高価格よりも低い必要があります #################################################################### # Windows #################################################################### inputControlWindow.headline=Select inputs for transaction inputControlWindow.balanceLabel=利用可能残高 contractWindow.title=係争の詳細 contractWindow.dates=オファーの日付 / トレードの日付 contractWindow.xmrAddresses=ビットコインアドレス XMR買い手 / XMR売り手 contractWindow.onions=ネットワークアドレス XMR買い手 / XMR売り手 contractWindow.accountAge=アカウント年齢 XMR買い手 / XMR売り手 contractWindow.numDisputes=調停人の数 XMRの買い手 / XMRの売り手 contractWindow.contractHash=契約ハッシュ displayAlertMessageWindow.headline=重要な情報! displayAlertMessageWindow.update.headline=重要な更新情報! displayAlertMessageWindow.update.download=ダウンロード: displayUpdateDownloadWindow.downloadedFiles=ファイル: displayUpdateDownloadWindow.downloadingFile=ダウンロード中: {0} displayUpdateDownloadWindow.verifiedSigs=署名がキーで検証されました: displayUpdateDownloadWindow.status.downloading=ファイルダウンロード中… displayUpdateDownloadWindow.status.verifying=署名を検証中... displayUpdateDownloadWindow.button.label=インストーラーをダウンロードして署名を検証 displayUpdateDownloadWindow.button.downloadLater=後でダウンロード displayUpdateDownloadWindow.button.ignoreDownload=このバージョンを無視 displayUpdateDownloadWindow.headline=新しいHavenoの更新が利用可能です! displayUpdateDownloadWindow.download.failed.headline=ダウンロードに失敗 displayUpdateDownloadWindow.download.failed=ダウンロード失敗。\n[HYPERLINK:https://haveno.exchange/downloads] から手動でダウンロード、確認してください。 displayUpdateDownloadWindow.installer.failed=正しいインストーラーを判別できません。 [HYPERLINK:https://haveno.exchange/downloads] から手動でダウンロードして検証してください。 displayUpdateDownloadWindow.verify.failed=検証失敗。\n[HYPERLINK:https://haveno.exchange/downloads] から手動でダウンロードして確認してください。 displayUpdateDownloadWindow.success=新しいバージョンが正常にダウンロードされ、署名が検証されました。\n\nダウンロードディレクトリを開き、アプリケーションを終了して新しいバージョンをインストールしてください。 displayUpdateDownloadWindow.download.openDir=ダウンロードフォルダを開く disputeSummaryWindow.title=概要 disputeSummaryWindow.openDate=チケットオープン日 disputeSummaryWindow.role=取引者の役割 disputeSummaryWindow.payout=トレード金額の支払い disputeSummaryWindow.payout.getsTradeAmount=XMR {0}はトレード金額の支払いを受け取ります disputeSummaryWindow.payout.getsAll=XMRへの最高額支払い {0} disputeSummaryWindow.payout.custom=任意の支払い disputeSummaryWindow.payoutAmount.buyer=買い手の支払額 disputeSummaryWindow.payoutAmount.seller=売り手の支払額 disputeSummaryWindow.payoutAmount.invert=発行者として敗者を使用 disputeSummaryWindow.reason=係争の理由 disputeSummaryWindow.tradePeriodEnd=Trade period end disputeSummaryWindow.extraInfo=Extra information disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BUG=バグ # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.USABILITY=使いやすさ # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PROTOCOL_VIOLATION=プロトコル違反 # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.NO_REPLY=返信無し # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SCAM=詐欺 # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OTHER=その他 # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BANK_PROBLEMS=銀行 # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OPTION_TRADE=オプション・トレード # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Trader not responding # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=間違った送信者アカウント # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PEER_WAS_LATE=ピアが遅れました # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=トレードはすでに決められました disputeSummaryWindow.summaryNotes=概要ノート disputeSummaryWindow.addSummaryNotes=概要ノートを追加 disputeSummaryWindow.close.button=チケットを閉じる # Do no change any line break or order of tokens as the structure is used for signature verification # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.msg=チケットは {0} に閉じられました\n{1} ノードアドレス: {2}\n\nまとめ\nトレードID: {3}\n通貨: {4}\nトレード金額: {5}\n買い手のXMR支払額: {6}\n売り手のXMR支払額 {7}\n\n係争の理由: {8}\n\n概要ノート:\n{9}\n # Do no change any line break or order of tokens as the structure is used for signature verification disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3} disputeSummaryWindow.close.nextStepsForMediation=\n次のステップ:\nトレードをオープンして、調停者からの提案を受け入れるまたは拒否する disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n次のステップ:\nこれ以上の行動が必要ありません。調停人があなたに有利に決める場合、「仲裁からの払い戻し」というトランザクションは「資金/トランザクション」に表示されます。 disputeSummaryWindow.close.closePeer=取引相手のチケットも閉じる必要があります! disputeSummaryWindow.close.txDetails.headline=払い戻しトランザクションを公開する # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.buyer=買い手が {0} を受けます、入金先アドレス: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=売り手が {0} を受けます、入金先アドレス: {1}\n disputeSummaryWindow.close.txDetails=支払う金額: {0}\n{1}{2}トランザクション手数料: {3}\n\nこのトランザクションを発行してもよろしいですか? disputeSummaryWindow.close.noPayout.headline=支払いなしで閉じる disputeSummaryWindow.close.noPayout.text=支払いなしで閉じてもよろしいですか? emptyWalletWindow.headline={0} 緊急ウォレットツール emptyWalletWindow.info=UIから資金にアクセスできない緊急時にのみ使用してください。\n\nこのツールを使用すると、開いているオファーはすべて自動的に閉じられることに注意してください。\n\nこのツールを使用する前に、データディレクトリをバックアップしてください。これは「アカウント/バックアップ」で行えます。\n\n問題の原因を調査できるように、問題を報告し、GitHubまたはHavenoフォーラムにバグレポートを提出してください。 emptyWalletWindow.balance=あなたの利用可能なウォレット残高 emptyWalletWindow.address=あなたの宛先アドレス emptyWalletWindow.button=全ての資金を送る emptyWalletWindow.openOffers.warn=ウォレット空にすると削除されるオープンオファーがあります。 \n本当にウォレットを空にしますか? emptyWalletWindow.openOffers.yes=はい、そうです emptyWalletWindow.sent.success=あなたのウォレットの残高は正常に送金されました。 enterPrivKeyWindow.headline=登録のためにプライベートキーを入力 filterWindow.headline=フィルターリストを編集 filterWindow.offers=フィルター済オファー(コンマ区切り) filterWindow.onions=トレードアドレスから追放された(コンマ区切り) filterWindow.bannedFromNetwork=ネットワークアドレスから追放された(コンマ区切り) filterWindow.accounts=フィルター済トレードアカウントデータ:\n形式: コンマ区切りのリスト [支払方法id | データフィールド | 値] filterWindow.bannedCurrencies=フィルター済通貨コード(コンマ区切り) filterWindow.bannedPaymentMethods=フィルター済支払方法ID(コンマ区切り) filterWindow.bannedAccountWitnessSignerPubKeys=フィルター済アカウントWitness署名者パブリックキー(コンマ区切りパブリックキーの16進値) filterWindow.bannedPrivilegedDevPubKeys=フィルター済特権的開発者パブリックキー(コンマ区切りパブリックキーの16進値) filterWindow.arbitrators=フィルター済調停人(コンマ区切り onionアドレス) filterWindow.mediators=フィルター済調停者(コンマ区切り onionアドレス) filterWindow.refundAgents=フィルター済仲裁人(コンマ区切り onionアドレス) filterWindow.seedNode=フィルター済シードノード(コンマ区切り onionアドレス) filterWindow.priceRelayNode=フィルター済価格中継ノード(コンマ区切り onionアドレス) filterWindow.xmrNode=フィルター済ビットコインノード(コンマ区切り アドレス+ポート) filterWindow.preventPublicXmrNetwork=パブリックビットコインネットワークの使用を防止 filterWindow.disableAutoConf=自動確認を無効にする filterWindow.autoConfExplorers=フィルター済自動確認エクスプローラ(コンマ区切りアドレス) filterWindow.disableTradeBelowVersion=トレードに必要な最低バージョン filterWindow.add=フィルターを追加 filterWindow.remove=フィルターを削除 filterWindow.xmrFeeReceiverAddresses=XMR手数料受信アドレス filterWindow.disableApi=APIを無効化 filterWindow.disableMempoolValidation=Disable Mempool Validation offerDetailsWindow.minXmrAmount=最小のXMR金額 offerDetailsWindow.min=(最小 {0}) offerDetailsWindow.distance=(市場価格からの乖離: {0}) offerDetailsWindow.myTradingAccount=私のトレードアカウント offerDetailsWindow.offererBankId=(メイカーの銀行ID/BIC/SWIFT) offerDetailsWindow.offerersBankName=(メイカーの銀行名) offerDetailsWindow.bankId=銀行ID(例:BICまたはSWIFT) offerDetailsWindow.countryBank=メイカーの銀行の国名 offerDetailsWindow.commitment=約束 offerDetailsWindow.agree=同意します offerDetailsWindow.tac=取引条件 offerDetailsWindow.confirm.maker.buy=確定:{0}でXMRを購入するオファーを作成 offerDetailsWindow.confirm.maker.sell=確定:{0}でXMRを売却するオファーを作成 offerDetailsWindow.confirm.taker.buy=確定:{0}でXMRを購入するオファーを受け入れ offerDetailsWindow.confirm.taker.sell=確定:{0}でXMRを売却するオファーを受け入れ offerDetailsWindow.creationDate=作成日 offerDetailsWindow.makersOnion=メイカーのonionアドレス offerDetailsWindow.challenge=オファーパスフレーズ offerDetailsWindow.challenge.copy=ピアと共有するためにパスフレーズをコピーする qRCodeWindow.headline=QRコード qRCodeWindow.msg=外部ウォレットからHavenoウォレットへ送金するのに、このQRコードを利用して下さい。 qRCodeWindow.request=支払いリクエスト:\n{0} selectDepositTxWindow.headline=係争の為のデポジットトランザクションを選択 selectDepositTxWindow.msg=このデポジットトランザクションはトレードに保存されませんでした。\n失敗したトレードで使用されたデポジットトランザクションであった、既存のマルチシグトランザクションのいずれかをウォレットから選択してください。\n\n正しいトランザクションを見つけるためには、トレード詳細ウィンドウを開き(リストのトレードIDをクリック)、マルチシグデポジットトランザクション(アドレスは3で始まります)が表示されている次のトランザクションへの、トレード手数料の支払いトランザクションアウトプットをたどってください。そのトランザクションIDは、ここに表示されているリストに見つかるはずです。正しいトランザクションが見つかったら、ここでそのトランザクションを選択して続行します。\n\nご不便をおかけして申し訳ありませんが、そのエラーのケースはごくまれにしか発生しません。今後、より良い解決方法を探します。 selectDepositTxWindow.select=デポジットトランザクションを選択 sendAlertMessageWindow.headline=グローバル通知を送信 sendAlertMessageWindow.alertMsg=警告メッセージ sendAlertMessageWindow.enterMsg=メッセージを入力 sendAlertMessageWindow.isSoftwareUpdate=Software download notification sendAlertMessageWindow.isUpdate=Is full release sendAlertMessageWindow.isPreRelease=Is pre-release sendAlertMessageWindow.version=新バージョンナンバー sendAlertMessageWindow.send=通知を送信 sendAlertMessageWindow.remove=通知を削除 sendPrivateNotificationWindow.headline=プライベートメッセージを送信 sendPrivateNotificationWindow.privateNotification=プライベート通知 sendPrivateNotificationWindow.enterNotification=通知を入力 sendPrivateNotificationWindow.send=プライベート通知を送信 showWalletDataWindow.walletData=ウォレットデータ showWalletDataWindow.includePrivKeys=プライベートキーを含む setXMRTxKeyWindow.headline=XMR送金を証明 setXMRTxKeyWindow.note=以下にtx情報を追加すると、より早いトレードのため自動確認を有効にします。詳しくは:https://haveno.exchange/wiki/Trading_Monero setXMRTxKeyWindow.txHash=トランザクションID(任意) setXMRTxKeyWindow.txKey=トランザクション・キー(任意) # We do not translate the tac because of the legal nature. We would need translations checked by lawyers # in each language which is too expensive atm. tacWindow.headline=ユーザー規約 tacWindow.agree=同意します tacWindow.disagree=同意せずにに終了 tacWindow.arbitrationSystem=紛争解決 tradeDetailsWindow.headline=トレード tradeDetailsWindow.disputedPayoutTxId=係争中の支払い取引ID tradeDetailsWindow.tradeDate=取引日 tradeDetailsWindow.txFee=マイニング手数料 tradeDetailsWindow.tradePeersOnion=トレード相手のonionアドレス tradeDetailsWindow.tradePeersPubKeyHash=トレードピアのパブリックキーハッシュ tradeDetailsWindow.tradeState=トレード状態 tradeDetailsWindow.agentAddresses=仲裁者 / 調停人 tradeDetailsWindow.detailData=詳細データ txDetailsWindow.headline=トランザクション詳細 txDetailsWindow.xmr.noteSent=XMRを送金しました。 txDetailsWindow.xmr.noteReceived=XMRを受け取りました。 txDetailsWindow.sentTo=送信先 txDetailsWindow.receivedWith=受け取りました。 txDetailsWindow.txId=TxId closedTradesSummaryWindow.headline=Trade history summary closedTradesSummaryWindow.totalAmount.title=Total trade amount closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) closedTradesSummaryWindow.totalTradeFeeInXmr.title=Sum of all trade fees paid in XMR closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} of total trade amount) walletPasswordWindow.headline=アンロックするためにパスワードを入力してください xmrConnectionError.headline=Monero 接続エラー xmrConnectionError.providedNodes=指定された Monero ノードへの接続に失敗しました。\n\n次に利用可能な Monero ノードを使用しますか? xmrConnectionError.customNodes=カスタム Monero ノードへの接続に失敗しました。\n\n次に利用可能な Monero ノードを使用しますか? xmrConnectionError.localNode=Haveno は以前、ローカルの Monero ノードに接続されていましたが、現在は接続できません。\n\nローカルノードが起動して完全に同期されていることを確認するか、別のオプションを選択して続行してください。 xmrConnectionError.localNode.start=ローカルノードを起動 xmrConnectionError.localNode.start.error=ローカルノードの起動中にエラーが発生しました xmrConnectionError.localNode.fallback=次に最適なノードに接続 torNetworkSettingWindow.header=Torネットワークの設定 torNetworkSettingWindow.noBridges=ブリッジを使わない torNetworkSettingWindow.providedBridges=提供されているブリッジと接続 torNetworkSettingWindow.customBridges=カスタムブリッジを入力してください torNetworkSettingWindow.transportType=転送タイプ torNetworkSettingWindow.obfs3=obfs3 torNetworkSettingWindow.obfs4=obfs4 (推奨) torNetworkSettingWindow.meekAmazon=meek-amazon torNetworkSettingWindow.meekAzure=meek-azure torNetworkSettingWindow.enterBridge=1つ以上のブリッジリレーを入力してください(1行あたり1つ) torNetworkSettingWindow.enterBridgePrompt=アドレス:ポート番号を入力してください torNetworkSettingWindow.restartInfo=変更を適用するために再起動してください torNetworkSettingWindow.openTorWebPage=TorプロジェクトのWebページを開く torNetworkSettingWindow.deleteFiles.header=接続に問題がありますか? torNetworkSettingWindow.deleteFiles.info=起動時に接続の問題が繰り返される場合は、古いTorファイルを削除すると解決する可能性があります。そのためには、下のボタンをクリックしてから再起動してください。 torNetworkSettingWindow.deleteFiles.button=Torの古いファイルを削除してシャットダウンする torNetworkSettingWindow.deleteFiles.progress=Torをシャットダウン中 torNetworkSettingWindow.deleteFiles.success=Torの古いファイルの削除に成功しました。再起動してください。 torNetworkSettingWindow.bridges.header=Torはブロックされていますか? torNetworkSettingWindow.bridges.info=Torがあなたのインターネットプロバイダや国にブロックされている場合、Torブリッジによる接続を試みることができます。\nTorのWebページ https://bridges.torproject.org にアクセスして、ブリッジとプラガブル転送について学べます feeOptionWindow.headline=取引手数料の支払いに使用する通貨を選択してください feeOptionWindow.info=あなたは取引手数料の支払いにBSQまたはXMRを選択できます。 BSQを選択した場合は、割引された取引手数料に気付くでしょう。 feeOptionWindow.optionsLabel=取引手数料の支払いに使用する通貨を選択してください feeOptionWindow.useXMR=XMRを使用 feeOptionWindow.fee={0} (≈ {1}) feeOptionWindow.xmrFeeWithFiatAndPercentage={0} (≈ {1} / {2}) feeOptionWindow.xmrFeeWithPercentage={0} ({1}) #################################################################### # Popups #################################################################### popup.headline.notification=通知 popup.headline.instruction=ご注意ください: popup.headline.attention=注意 popup.headline.backgroundInfo=バックグラウンド情報 popup.headline.feedback=完了 popup.headline.confirmation=承認 popup.headline.information=情報 popup.headline.warning=注意 popup.headline.error=エラー popup.doNotShowAgain=次回から表示しない popup.reportError.log=ログファイルを開く popup.reportError.gitHub=GitHub issue trackerに報告 popup.reportError={0}\n\nソフトウェアの改善に役立てるため、https://github.com/haveno-dex/haveno/issues で新しい issue を開いてこのバグを報告してください。\n下のボタンのいずれかをクリックすると、上記のエラーメッセージがクリップボードにコピーされます。\n「ログファイルを開く」を押して、コピーを保存し、バグレポートに添付されるhaveno.logファイル含めると、デバッグが容易になります。 popup.error.tryRestart=アプリケーションを再起動し、ネットワーク接続を確認して問題を解決できるかどうかを確認してください。 popup.error.takeOfferRequestFailed=誰かがあなたのいずれかのオファーを受けようと時にエラーが発生しました:\n{0} error.spvFileCorrupted=SPVチェーンファイルの読み込み中にエラーが発生しました。\nSPVチェーンファイルが破損している可能性があります。\n\nエラーメッセージ: {0} \n\n削除して再同期を開始しますか? error.deleteAddressEntryListFailed=AddressEntryListファイルを削除できませんでした。\nエラー: {0} error.closedTradeWithUnconfirmedDepositTx=トレードID{0}で識別されるトレードのデポジットトランザクションはまだ承認されていません。\n\nトランザクションは有効かどうかを確認するため、\"設定/ネットワーク情報\"を開いてSPV再同期を行って下さい。 error.closedTradeWithNoDepositTx=トレードID{0}で識別されるトレードのデポジットトランザクションは無効とされました。\n\n閉じられたトレードリストを更新するため、アプリケーションを再起動して下さい。 popup.warning.walletNotInitialized=ウォレットはまだ初期化されていません popup.warning.osxKeyLoggerWarning=macOS 10.14以上の厳しいセキュリティー対策のため、Javaアプリケーション(HavenoはJavaを利用します)はmacOSで警告用のポップアップ・ウィンドウを生じます(「Havenoは他のアプリからキー操作をアクセスしたい」)。\n\nこの問題を解決するのに、macOS設定を開いて、「セキュリティとプライバシー -> プライバシー -> 入力監視」において右側のリストからHavenoを外して下さい。\n\n技術的な限界は克服されたら(必要のJavaバージョンパッケージャーがまだリリースされていません)、問題を避けるためにHavenoは新しいJavaバージョンにアップグレードします。 popup.warning.wrongVersion=このコンピューターのHavenoバージョンが間違っている可能性があります。\nコンピューターのアーキテクチャ: {0}\nインストールしたHavenoバイナリ: {1}\nシャットダウンして、次の正しいバージョンを再インストールしてください({2})。 popup.warning.incompatibleDB=互換性のないデータベースファイルが検出されました!\n\nこういうデータベースファイルは、現在のコードベースと互換性がありません:\n{0}\n\n破損したファイルのバックアップを作成し、デフォルト値を新しいデータベースバージョンに適用しました。\n\nバックアップは次の場所にあります。\n{1}/db/backup_of_corrupted_data.\n\nHavenoの最新バージョンがインストールされているかどうかを確認してください。\n以下からダウンロードできます。\n[HYPERLINK:https://haveno.exchange/downloads]\n\nアプリケーションを再起動してください。 popup.warning.startupFailed.twoInstances=Havenoは既に起動中です。Havenoを2つ起動することはできません。 popup.warning.tradePeriod.halfReached=ID {0}とのトレードは許可された最大トレード期間の半分に達しましたが、まだ完了していません\n\n取引期間は{1}で終了します\n\n詳細については、「ポートフォリオ/オープントレード」でトレード状態を確認してください。 popup.warning.tradePeriod.ended=ID {0}とのトレードは許可された最大トレード期間に達しましたが、まだ完了していません。\n\nトレード期間は{1}で終了しました\n\n調停者に連絡するには、「ポートフォリオ/オープントレード」であなたのトレードを確認してください。 popup.warning.noTradingAccountSetup.headline=トレードアカウントが設定されていません popup.warning.noTradingAccountSetup.msg=オファーを作成する前に、国内通貨またはアルトコインのアカウントを設定する必要があります。\nアカウントを設定しますか? popup.warning.noArbitratorsAvailable=利用可能な調停人がいません。 popup.warning.noMediatorsAvailable=利用可能な調停人がいません。 popup.warning.notFullyConnected=ネットワークへ完全に接続するまで待つ必要があります。\n起動までに約2分かかります。 popup.warning.notSufficientConnectionsToXmrNetwork=少なくとも{0}のビットコインネットワークへの接続が確立されるまでお待ちください。 popup.warning.downloadNotComplete=欠落しているビットコインブロックのダウンロードが完了するまで待つ必要があります。 popup.warning.walletNotSynced=Havenoウォレットのブロックチェーン高さは正しく同期されていません。アプリを最近起動した場合、1つのビットコインブロックが発行されるまで待って下さい。\n\nブロックチェーン高さは\"設定/ネットワーク情報\"に表示されます。 2つ以上のブロックが発行されても問題が解決されない場合、フリーズしている可能性があります。その場合には、SPV再同期を行って下さい [HYPERLINK: https://haveno.exchange/wiki/Resyncing_SPV_file ]。 popup.warning.removeOffer=本当にオファーを削除しますか? popup.warning.tooLargePercentageValue=100%以上のパーセントを設定できません popup.warning.examplePercentageValue=パーセントの数字を入力してください。5.4%は「5.4」のように入力します。 popup.warning.noPriceFeedAvailable=その通貨で利用できる価格フィードはありません。パーセントベースの価格は使用できません。 固定価格を選択してください。 popup.warning.sendMsgFailed=トレード相手へのメッセージの送信に失敗しました。\nもう一度試してください。失敗し続ける場合はバグを報告してください。 popup.warning.messageTooLong=メッセージが許容サイズ上限を超えています。いくつかに分けて送信するか、 https://pastebin.com のようなサービスにアップロードしてください。 popup.warning.lockedUpFunds=失敗したトレードから残高をロックしました。\nロックされた残高: {0} \nデポジットtxアドレス: {1} \nトレードID: {2}。\n\nオープントレード画面でこのトレードを選択し、「alt + o」または「option + o」を押してサポートチケットを開いてください。 popup.warning.moneroConnection=Moneroネットワークへの接続中に問題が発生しました。\n\n{0} popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n takeOffer.cancelButton=Cancel take-offer takeOffer.warningButton=無視して続ける # suppress inspection "UnusedProperty" popup.warning.nodeBanned={0}ノードの1つが禁止されました。 # suppress inspection "UnusedProperty" popup.warning.priceRelay=価格中継 popup.warning.seed=シード popup.warning.mandatoryUpdate.trading=最新のHavenoバージョンに更新してください。古いバージョンのトレードを無効にする必須の更新プログラムがリリースされました。詳細については、Havenoフォーラムをご覧ください。 popup.warning.noFilter=シードノードからフィルターオブジェクトを受け取っていません。ネットワーク管理者にフィルターオブジェクトを登録するように通知してください。 popup.warning.burnXMR={0}のマイニング手数料が{1}の送金額を超えるため、このトランザクションは利用不可です。マイニング手数料が再び低くなるか、送金するXMRがさらに蓄積されるまでお待ちください。 popup.warning.openOffer.makerFeeTxRejected=ID{0}で識別されるオファーのためのメイカー手数料トランザクションがビットコインネットワークに拒否されました。\nトランザクションID= {1} 。\n更なる問題を避けるため、そのオファーは削除されました。\n\"設定/ネットワーク情報\"を開いてSPV再同期を行って下さい。\nさらにサポートを受けるため、Haveno Keybaseチームのサポートチャンネルに連絡して下さい。 popup.warning.trade.txRejected.tradeFee=トレード手数料 popup.warning.trade.txRejected.deposit=デポジット popup.warning.trade.txRejected=ID{1}で識別されるトレードのための{0}トランザクションがビットコインネットワークに拒否されました。\nトランザクションID= {2} \nトレードは「失敗トレード」へ送られました。\n\"設定/ネットワーク情報\"を開いてSPV再同期を行って下さい。\nさらにサポートを受けるため、Haveno Keybaseチームのサポートチャンネルに連絡して下さい。 popup.warning.openOfferWithInvalidMakerFeeTx=ID{0}で識別されるオファーのためのメイカー手数料トランザクションが無効とされました。\nトランザクションID= {1} 。\n更なる問題を避けるため、そのオファーは削除されました。\n\"設定/ネットワーク情報\"を開いてSPV再同期を行って下さい。\nさらにサポートを受けるため、Haveno Keybaseチームのサポートチャンネルに連絡して下さい。 popup.info.securityDepositInfo=両方の取引者がトレードプロトコルに従うことを保証するために、両方のトレーダーはセキュリティデポジットを支払う必要があります。\n\nこのデポジットはあなたのトレードがうまく完了するまであなたのトレードウォレットに保管され、それからあなたに返金されます。\n\n注意してください:あなたが新しいオファーを作成しているなら、他の取引者がそれを受けるためにHavenoを実行しておく必要があります。オファーをオンラインにしておくには、Havenoを実行したままにして、このコンピュータもオンラインにしたままにします(つまり、スタンバイモードに切り替わらないようにします…モニターのスタンバイは大丈夫です)。 popup.info.cashDepositInfo=あなたの地域の銀行支店が現金デポジットが作成できることを確認してください。\n売り手の銀行ID(BIC / SWIFT)は{0}です。 popup.info.cashDepositInfo.confirm=デポジットを作成できるか確認します popup.info.shutDownWithOpenOffers=Havenoはシャットダウン中ですが、オファーはあります。\n\nこれらのオファーは、Havenoがシャットダウンされている間はP2Pネットワークでは利用できませんが、次回Havenoを起動したときにP2Pネットワークに再公開されます。\n\nオファーをオンラインに保つには、Havenoを実行したままにして、このコンピュータもオンラインにしたままにします(つまり、スタンバイモードにならないようにしてください。モニタースタンバイは問題ありません)。 popup.info.qubesOSSetupInfo=Qubes OS内でHavenoを実行しているようです。\n\nHavenoのqubeはセットアップガイドに従って設定されていることを確かめて下さい: [HYPERLINK:https://haveno.exchange/wiki/Running_Haveno_on_Qubes] popup.warn.downGradePrevention=バージョン{0}からバージョン{1}に戻すことはサポートされていません。最新のHavenoバージョンを利用して下さい。 popup.privateNotification.headline=重要なプライベート通知! popup.securityRecommendation.headline=重要なセキュリティ勧告 popup.securityRecommendation.msg=ウォレットのパスワード保護をまだ有効にしてない場合は、使用することを検討してください。\n\nウォレットシードワードを書き留めることも強くお勧めします。 これらのシードワードは、あなたのビットコインウォレットを復元するためのマスターパスワードのようなものです。\n「ウォレットシード」セクションにてより詳細な情報を確認できます。\n\nまた、「バックアップ」セクションのアプリケーションデータフォルダ全体をバックアップするべきでしょう。 popup.xmrLocalNode.msg=Haveno检测到本机(localhost上)运行了一个门罗币节点。\n\n在启动Haveno之前,请确保节点已完全同步。 popup.shutDownInProgress.headline=シャットダウン中 popup.shutDownInProgress.msg=アプリケーションのシャットダウンには数秒かかることがあります。\nこのプロセスを中断しないでください。 popup.attention.forTradeWithId=ID {0}とのトレードには注意が必要です popup.attention.reasonForPaymentRuleChange=バージョン1.5.5から、銀行振込の\"支払理由\"フィールドに関するトレードルールに非常に重要な変更があります。このフィールドを必ず空白にしておいて下さい -- いかなる場合でも、トレードIDを\"支払理由\"に入力しないで下さい。 popup.info.multiplePaymentAccounts.headline=複数の支払いアカウントが使用可能です popup.info.multiplePaymentAccounts.msg=このオファーに使用できる支払いアカウントが複数あります。あなたが正しいものを選んだことを確認してください。 popup.accountSigning.selectAccounts.headline=支払いアカウントを選択 popup.accountSigning.selectAccounts.description=支払い方法そして時点に基づいて、買い手への支払いが起こった係争と繋がっている全てのアカウントは署名されるように選択されます。 popup.accountSigning.selectAccounts.signAll=全ての支払い方法を署名 popup.accountSigning.selectAccounts.datePicker=アカウント署名のは終了する時点を選択して下さい。 popup.accountSigning.confirmSelectedAccounts.headline=選択された支払いアカウントを確認 popup.accountSigning.confirmSelectedAccounts.description=入力に基づいて、{0}口の支払いアカウントは選択されます popup.accountSigning.confirmSelectedAccounts.button=支払いアカウントを確認 popup.accountSigning.signAccounts.headline=支払いアカウントの署名を確認 popup.accountSigning.signAccounts.description=選択に基づいて、{0}口の支払いアカウントは署名されます popup.accountSigning.signAccounts.button=支払いアカウントに署名 popup.accountSigning.signAccounts.ECKey=プライベート調停人キーを入力 popup.accountSigning.signAccounts.ECKey.error=不良の調停人ECKey popup.accountSigning.success.headline=おめでとう popup.accountSigning.success.description=全{0}口の支払いアカウントは成功裏に署名されました! popup.accountSigning.generalInformation=全てのアカウントの署名状態はアカウント画面に表示されます。\n\n詳しくは: [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing] popup.accountSigning.signedByArbitrator=支払いアカウントの1つは調停人に検証、署名されました。このアカウントからトレードを行ったら、トレードピアと成功にトレードする後に相手のアカウントを自動的に署名します。\n\n{0} popup.accountSigning.signedByPeer=支払いアカウントの1つはトレードピアに検証、署名されました。{0} 日後に、初期のトレード制限は解除され、他の支払いアカウントを署名できるようになります。\n\n{1} popup.accountSigning.peerLimitLifted=支払いアカウントの1つにおいて初期の制限は解除されました。\n\n{0} popup.accountSigning.peerSigner=支払いアカウントの1つは十分に熟成されて、初期の制限は解除されました。\n\n{0} popup.accountSigning.singleAccountSelect.headline=署名されないアカウント年齢witnessをインポート popup.accountSigning.confirmSingleAccount.headline=選択されたアカウント年齢witnessを確認 popup.accountSigning.confirmSingleAccount.selectedHash=選択されたwitnessのハッシュ popup.accountSigning.confirmSingleAccount.button=アカウント年齢witnessを署名 popup.accountSigning.successSingleAccount.description=Witness {0} は署名された popup.accountSigning.successSingleAccount.success.headline=成功 popup.accountSigning.unsignedPubKeys.headline=無署名のパブリックキー popup.accountSigning.unsignedPubKeys.sign=パブリックキーを署名 popup.accountSigning.unsignedPubKeys.signed=パブリックキーは署名されました popup.accountSigning.unsignedPubKeys.result.signed=署名されたパブリックキー popup.accountSigning.unsignedPubKeys.result.failed=署名が失敗しました popup.info.buyerAsTakerWithoutDeposit.headline=購入者による保証金は不要 popup.info.buyerAsTakerWithoutDeposit=あなたのオファーには、XMR購入者からのセキュリティデポジットや手数料は必要ありません。\n\nオファーを受け入れるには、Haveno外で取引相手とパスフレーズを共有する必要があります。\n\nパスフレーズは自動的に生成され、作成後にオファーの詳細に表示されます。 #################################################################### # Notifications #################################################################### notification.trade.headline=ID {0}とのトレードの通知 notification.ticket.headline=ID {0}とのトレード用サポートチケット notification.trade.completed=これでトレードは完了し、資金を出金することができます。 notification.trade.accepted=あなたのオファーはXMR {0}によって承認されました。 notification.trade.unlocked=あなたのトレードには少なくとも1つのブロックチェーン承認があります。\nあなたは今、支払いを始めることができます。 notification.trade.paymentSent=XMRの買い手が支払いを開始しました。 notification.trade.selectTrade=取引を選択 notification.trade.peerOpenedDispute=あなたの取引相手は{0}をオープンしました。 notification.trade.disputeClosed={0}は閉じられました。 notification.walletUpdate.headline=トレードウォレット更新 notification.walletUpdate.msg=あなたのトレードウォレットは十分に入金されています。\n金額: {0} notification.takeOffer.walletUpdate.msg=あなたのトレードウォレットは、以前のオファー受け入れの試みからすでに十分な資金を得ています。\n金額: {0} notification.tradeCompleted.headline=取引完了 notification.tradeCompleted.msg=あなたは今、外部のモネロウォレットに資金を出金するか、Havenoウォレットに保持することができます。 #################################################################### # System Tray #################################################################### systemTray.show=アプリケーションウィンドウを表示 systemTray.hide=アプリケーションウィンドウを隠す systemTray.info=Havenoについての情報 systemTray.exit=終了 systemTray.tooltip=Haveno: 分散的ビットコイン取引ネットワーク #################################################################### # GUI Util #################################################################### guiUtil.accountExport.savedToPath=取引アカウントを下記パスに保存しました:\n{0} guiUtil.accountExport.noAccountSetup=取引アカウントのエクスポート設定がされていません guiUtil.accountExport.selectPath={0}のパスを選択 # suppress inspection "TrailingSpacesInProperty" guiUtil.accountExport.tradingAccount=ID {0}の取引アカウント\n # suppress inspection "TrailingSpacesInProperty" guiUtil.accountImport.noImport=ID {0}の取引アカウントは既に存在するためインポートしませんでした。\n guiUtil.accountExport.exportFailed=エラーのため、CSVへのエクスポートに失敗しました。\nエラー= {0} guiUtil.accountExport.selectExportPath=エクスポートパスを選択 guiUtil.accountImport.imported=パスからインポートされた取引アカウント:\n{0}\n\nインポートされたアカウント:\n{1} guiUtil.accountImport.noAccountsFound=エクスポートされた取引アカウントは次のパスに見つかりませんでした: {0}。\nファイル名は{1}です。" guiUtil.openWebBrowser.warning=あなたのシステムウェブブラウザでWebページを開こうとしています。\n今すぐWebページを開きますか?\n\nあなたがデフォルトのシステムウェブブラウザとして「Torブラウザ」を使用していない場合は、クリアネットでWebページに接続します。\n\nURL: \"{0}\" guiUtil.openWebBrowser.doOpen=Webページを開き、次回から確認しない guiUtil.openWebBrowser.copyUrl=URLをコピーしてキャンセル guiUtil.ofTradeAmount=取引額に対して guiUtil.requiredMinimum=(必要な最低限) #################################################################### # Component specific #################################################################### list.currency.select=通貨を選択 list.currency.showAll=全て表示する list.currency.editList=通貨リストを編集する table.placeholder.noItems=現在利用可能な{0}がありません table.placeholder.noData=現在利用可能なデータがありません table.placeholder.processingData=データ処理中... peerInfoIcon.tooltip.tradePeer=トレード相手 peerInfoIcon.tooltip.maker=メイカーの peerInfoIcon.tooltip.trade.traded={0} onionアドレス: {1}\n既にその相手と{2}回トレードしました\n{3} peerInfoIcon.tooltip.trade.notTraded={0} onionアドレス: {1}\nあなたは今までこの相手とトレードしていません\n{2} peerInfoIcon.tooltip.age=支払いアカウントが{0}前に作成されました。 peerInfoIcon.tooltip.unknownAge=支払いアカウントの年齢は不明です。 tooltip.openPopupForDetails=詳細についてのポップアップを開く tooltip.invalidTradeState.warning=このトレードは無効な状態とされました。詳しくは詳細ウィンドウを開く tooltip.openBlockchainForAddress=外部ブロックチェーンエクスプローラーで次のアドレスを開く: {0} tooltip.openBlockchainForTx=外部ブロックチェーンエクスプローラーで次のトランザクションを開く: {0} confidence.unknown=不明なトランザクションステータス confidence.seen={0} 人のピアに見られた / 0 承認 confidence.confirmed={0} 承認 confidence.invalid=トランザクションが不正です peerInfo.title=ピア情報 peerInfo.nrOfTrades=完了した取引数 peerInfo.notTradedYet=あなたは今までそのユーザーと取引していません。 peerInfo.setTag=そのピアにタグを設定する peerInfo.age.noRisk=支払いアカウントの年齢 peerInfo.age.chargeBackRisk=署名する後経過時間 peerInfo.unknownAge=年齢不明 addressTextField.openWallet=既定のビットコインウォレットを開く addressTextField.copyToClipboard=アドレスをクリップボードにコピー addressTextField.addressCopiedToClipboard=アドレスをクリップボードにコピーしました addressTextField.openWallet.failed=既定のビットコインウォレットが開けませんでした。何らかのビットコインウォレットはインストールされていますか? peerInfoIcon.tooltip={0}\nタグ: {1} txIdTextField.copyIcon.tooltip=トランザクションIDをクリップボードにコピー txIdTextField.blockExplorerIcon.tooltip=このトランザクションIDをブロックチェーンエクスプローラで開く txIdTextField.missingTx.warning.tooltip=必要なトランザクションは欠測 #################################################################### # Navigation #################################################################### navigation.account=「アカウント」 navigation.account.walletSeed=「アカウント/ウォレットシード」 navigation.funds.availableForWithdrawal=\"資金/送金する\" navigation.portfolio.myOpenOffers=「ポートフォリオ/私の公開オファー navigation.portfolio.pending=「ポートフォリオ/オープントレード」 navigation.portfolio.closedTrades=「ポートフォリオ/履歴」 navigation.funds.depositFunds=「資金/資金の受取」 navigation.settings.preferences=「設定/設定」 # suppress inspection "UnusedProperty" navigation.funds.transactions=「資金/トランザクション」 navigation.support=「サポート」 #################################################################### # Formatter #################################################################### formatter.formatVolumeLabel={0} 額{1} formatter.makerTaker=メイカーは{0} {1} / テイカーは{2} {3} formatter.makerTaker.locked=メイカーは{0} {1} / テイカーは{2} {3} 🔒 formatter.youAreAsMaker=あなたは:{1} {0}(メイカー) / テイカーは:{3} {2} formatter.youAreAsTaker=あなたは:{1} {0}(テイカー) / メイカーは{3} {2} formatter.youAre=あなたは{0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=あなたはオファーを{0} {1}に作成中です formatter.youAreCreatingAnOffer.traditional.locked=あなたはオファーを{0} {1}に作成中です 🔒 formatter.youAreCreatingAnOffer.crypto=あなたはオファーを{0} {1} ({2} {3})に作成中です formatter.youAreCreatingAnOffer.crypto.locked=あなたはオファーを{0} {1} ({2} {3})に作成中です 🔒 formatter.asMaker={0} {1}のメイカー formatter.asTaker={0} {1}のテイカー #################################################################### # Domain specific #################################################################### # we use enum values here # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" XMR_MAINNET=Monero Mainnet # suppress inspection "UnusedProperty" XMR_LOCAL=Monero Local Testnet # suppress inspection "UnusedProperty" XMR_STAGENET=Monero Stagenet time.year=年 time.month=月 time.week=週 time.day=日 time.hour=時 time.minute10=10分 time.hours=時 time.days=日 time.1hour=1時間 time.1day=1日間 time.minute=分 time.second=秒 time.minutes=分 time.seconds=秒 password.enterPassword=パスワードを入力してください password.confirmPassword=パスワードを確認してください password.tooLong=パスワードは500文字以下にしてください password.deriveKey=パスワードから鍵を引き出す password.walletDecrypted=ウォレットは正常に復号化され、パスワード保護が解除されました。 password.wrongPw=間違ったパスワードを入力しています。\n\nパスワードをもう一度入力してみてください。入力ミスやスペルミスがないか慎重に確認してください。 password.walletEncrypted=ウォレットは正常に暗号化され、パスワード保護が有効になりました。 password.passwordsDoNotMatch=入力した2つのパスワードが一致しません。 password.forgotPassword=パスワードを忘れましたか? password.backupReminder=ウォレットパスワードを設定すると、暗号化されていないウォレットから自動的に作成されたすべてのバックアップが削除されます。\n\nパスワードを設定する前に、アプリケーションディレクトリのバックアップを作成してシードワードを書き留めておくことを強く推奨します。 password.backupWasDone=私は既にバックアップを取りました password.setPassword=Set Password (I already made a backup) password.makeBackup=Make Backup seed.seedWords=ウォレットシードワード seed.enterSeedWords=ウォレットシードワードを入力してください seed.date=ウォレットの日付 seed.restore.title=シードワードからウォレットを復元する seed.restore=ウォレットを復元する seed.creationDate=作成日 seed.warn.walletNotEmpty.msg=あなたのビットコインウォレットは空ではありません。\n\nウォレットを混在させると無効なバックアップになる可能性があるため、古いウォレットを復元する前に、このウォレットを空にする必要があります。\n\nあなたの取引を終了し、あなたの全てのオープンオファーを閉じて、あなたのビットコインを出金するために資金セクションに行ってください。\nあなたが自身のビットコインにアクセスできない場合、ウォレットを空にするために緊急ツールを使うことができます。\n緊急ツールを開くには \"Alt+e\" か \"Cmd/Ctrl+e\" を押してください。 seed.warn.walletNotEmpty.restore=とにかく復元したい seed.warn.walletNotEmpty.emptyWallet=最初にウォレットを空にしたい seed.warn.notEncryptedAnymore=あなたの財布は暗号化されています。\n\n復元後、ウォレットは暗号化されなくなり、新しいパスワードを設定する必要があります。\n\n続行しますか? seed.warn.walletDateEmpty=ウォレット日を特定しなかったため、Havenoは2013.10.09(BIP39エポック日)からブロックチェーンをスキャンしなければなりません。\n\nBIP39のウォレットは2017.06.28(リリースv0.5)にHavenoに使われ始めたので、その日を利用して時間を節約できます。\n\n理想的に、ウォレット・シードが作成された日を特定すべきです。\n\n\nウォレット日を特定せずに続いても本当によろしいですか? seed.restore.success=ウォレットは、新しいシードワードで正常に復元されました。\n\nアプリケーションをシャットダウンして再起動する必要があります。 seed.restore.error=シードワードを使用したウォレットの復元中にエラーが発生しました。{0} seed.restore.openOffers.warn=シードワードから復元すると削除されるオープンオファーがあります。 \n本当に続いてもよろしいですか? #################################################################### # Payment methods #################################################################### payment.account=アカウント payment.account.no=アカウント番号 payment.account.name=アカウント名 payment.account.username=ユーザ名 payment.account.phoneNr=電話番号 payment.account.owner.fullname=アカウント所有者の氏名 payment.account.fullName=氏名(名、ミドルネーム、姓) payment.account.state=州/県/区 payment.account.city=市区町村 payment.bank.country=銀行の国名 payment.account.name.email=アカウント所有者の氏名/メール payment.account.name.emailAndHolderId=アカウント所有者の氏名/メール/{0} payment.bank.name=銀行名 payment.select.account=アカウントタイプを選択してください payment.select.region=地域を選択してください payment.select.country=国を選択してください payment.select.bank.country=銀行の国名を選択 payment.foreign.currency=国のデフォルト通貨以外の通貨を選択してもよろしいですか? payment.restore.default=いいえ、デフォルトの通貨を復元します payment.email=メール payment.country=国 payment.extras=追加要件 payment.email.mobile=メールまたは携帯電話番号 payment.crypto.address=アルトコインアドレス payment.crypto.tradeInstantCheckbox=このアルトコインでインスタントトレード(1時間以内) payment.crypto.tradeInstant.popup=インスタントトレードでは、両方の取引者が1時間以内に取引を完了できるようにオンラインになっている必要があります。\n\n既にオープンなオファーがあり利用できない場合は「ポートフォリオ」画面でそれらのオファーを無効にしてください。 payment.crypto=アルトコイン payment.select.crypto=アルトコイン選択、または検索する payment.secret=秘密の質問 payment.answer=答え payment.wallet=ウォレットID payment.amazon.site=Buy giftcard at payment.ask=Ask in Trader Chat payment.uphold.accountId=ユーザーネームかメールか電話番号 payment.moneyBeam.accountId=メールか電話番号 payment.popmoney.accountId=メールか電話番号 payment.promptPay.promptPayId=市民ID/納税者番号または電話番号 payment.supportedCurrencies=サポートされている通貨 payment.supportedCurrenciesForReceiver=資金を受け取るための通貨 payment.limitations=制限事項 payment.salt=アカウント年齢を検証するためのソルト payment.error.noHexSalt=ソルトはHEXフォーマットである必要があります。\nアカウントの年齢を維持するために古いアカウントからソルトを送金したい場合は、ソルトフィールドを編集することをお勧めします。 アカウントの年齢は、アカウントソルトおよび識別口座データ(例えば、IBAN)を使用することによって検証されます。 payment.accept.euro=これらのユーロ圏の利用可能なトレード payment.accept.nonEuro=これらの非ユーロ圏の利用可能なトレード payment.accepted.countries=利用可能な国 payment.accepted.banks=利用可能な銀行 (ID) payment.mobile=携帯電話番号 payment.postal.address=郵便住所 payment.national.account.id.AR=CBU番号 shared.accountSigningState=アカウント署名状況 #new payment.crypto.address.dyn={0}アドレス payment.crypto.receiver.address=受取人のアルトコインアドレス payment.accountNr=アカウント番号 payment.emailOrMobile=メールまたは携帯電話番号 payment.useCustomAccountName=任意のアカウント名を使う payment.maxPeriod=許可された最大トレード期間 payment.maxPeriodAndLimit=最大トレード期間: {0} / 最大買い: {1} / 最大売り: {2}/アカウントの年齢: {3} payment.maxPeriodAndLimitCrypto=最大トレード期間: {0} / 最大トレード制限: {1} payment.currencyWithSymbol=通貨: {0} payment.nameOfAcceptedBank=利用可能な銀行の名前 payment.addAcceptedBank=利用可能な銀行を追加 payment.clearAcceptedBanks=利用可能な銀行を削除 payment.bank.nameOptional=銀行名(オプション) payment.bankCode=銀行コード payment.bankId=銀行ID (BIC/SWIFT) payment.bankIdOptional=銀行ID (BIC/SWIFT)(オプション) payment.branchNr=支店番号 payment.branchNrOptional=支店番号(オプション) payment.accountNrLabel=口座番号 (IBAN) payment.accountType=口座種別 payment.checking=当座口座 payment.savings=普通口座 payment.personalId=個人ID payment.zelle.info=Zelleは他の銀行を介して利用するとよりうまくいく送金サービスです。\n\n1. あなたの銀行がZelleと協力するか(そして利用の方法)をここから確認して下さい: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. 送金制限に注意して下さい。制限は銀行によって異なり、1日、1週、1月当たりの制限に分けられていることが多い。\n\n3. 銀行がZelleと協力しない場合でも、Zelleのモバイルアプリ版を使えますが、送金制限ははるかに低くなります。\n\n4. Havenoアカウントで特定される名前は必ずZelleアカウントと銀行口座に特定される名前と合う必要があります。\n\nトレード契約書とおりにZelleトランザクションを完了できなければ、一部(あるいは全て)のセキュリティデポジットを失う可能性はあります。\n\nZelleにおいてやや高い支払取り消しリスクがあるので、売り手はメールやSMSで無署名買い手に連絡して、Havenoに特定されるZelleアカウントの所有者かどうかを確かめるようにおすすめします。 payment.fasterPayments.newRequirements.info=「Faster Payments」で送金する場合、銀行が受信者の姓名を確認するケースが最近多くなりました。現在の「Faster Payments」アカウントは姓名を特定しません。\n\nこれからの{0}買い手に姓名を提供するため、Haveno内に新しい「Faster Payments」アカウントを作成するのを検討して下さい。\n\n新しいアカウントを作成すると、完全に同じ分類コード、アカウントの口座番号、そしてアカウント年齢検証ソルト値を古いアカウントから新しいアカウントにコピーして下さい。こうやって現在のアカウントの年齢そして署名状況は維持されます。 payment.moneyGram.info=MoneyGramを使用する場合、XMRの買い手は認証番号と領収書の写真をEメールでXMRの売り手に送信する必要があります。領収書には、売り手の氏名、市区町村、国、金額を明確に記載する必要があります。トレードプロセスにて、売り手のEメールは買い手に表示されます。 payment.westernUnion.info=Western Unionを使用する場合、XMRの買い手はMTCN(追跡番号)と領収書の写真をEメールでXMRの売り手に送信する必要があります。領収書には、売り手の氏名、市区町村、国、金額を明確に記載する必要があります。トレードプロセスにて、売り手のEメールは買い手に表示されます。 payment.halCash.info=HalCashを使用する場合、XMRの買い手は携帯電話からのテキストメッセージを介してXMRの売り手にHalCashコードを送信する必要があります。\n\n銀行がHalCashで送金できる最大額を超えないようにしてください。 1回の出金あたりの最小金額は10EURで、最大金額は600EURです。繰り返し出金する場合は、1日に受取人1人あたり3000EUR、1ヶ月に受取人1人あたり6000EURです。あなたの銀行でも、ここに記載されているのと同じ制限を使用しているか、これらの制限を銀行と照合して確認してください。\n\n出金額は10の倍数EURでなければ、ATMから出金できません。 オファーの作成画面およびオファー受け入れ画面のUIは、EUR金額が正しくなるようにXMR金額を調整します。価格の変化とともにEURの金額は変化するため、市場ベースの価格を使用することはできません。\n\n係争が発生した場合、XMRの買い手はEURを送ったという証明を提出する必要があります。 # suppress inspection "UnusedMessageFormatParameter" payment.limits.info=すべての銀行振込にはある程度の支払取り消しのリスクがあることに気を付けて下さい。\n\nこのリスクを軽減するために、Havenoは使用する支払い方法での支払取り消しリスクの推定レベルに基づいてトレードごとの制限を設定します。\n\n現在使用する支払い方法では、トレードごとの売買制限は{2}です。\n\n制限は各トレードの量のみに適用されることに注意して下さい。トレードできる合計回数には制限はありません。\n\n詳しくはWikiを調べて下さい [HYPERLINK:https://docs.haveno.exchange/overview/account_limits] 。 # suppress inspection "UnusedProperty" payment.limits.info.withSigning=支払取り消しのリスクを軽減するために、Havenoはこの支払いアカウントに下記の2つの要因に基づいてトレードごとの制限を設定します。\n\n1.使用する支払い方法での支払取り消しリスクの推定レベル\n2.アカウントの署名状況\n\nこの支払いアカウントはまだ無署名ですので、トレードごとに{0}の買い制限があります。 アカウントが署名される後、トレードごとの制限は以下のように成長します:\n\n●署名の前、そして署名から30日間までに、1トレードあたりの買い制限は{0}になります\n●署名から30日間後に、1トレードあたりの買い制限は{1}になります\n●署名から60日間後に、1トレードあたりの買い制限は{2}になります\n\n売り制限は署名状況に関係がありません。現在のところ、1トレードあたりに{2}を売ることができます。\n\n制限は各トレードの量のみに適用されることに注意して下さい。取引できる合計回数には制限はありません。\n\n詳しくは: [HYPERLINK:https://docs.haveno.exchange/overview/account_limits] payment.cashDeposit.info=あなたの銀行が他の人の口座に現金入金を送ることを許可していることを確認してください。たとえば、Bank of America と Wells Fargo では、こうした預金は許可されなくなりました。 payment.revolut.info=以前の場合と違って、Revolutは電話番号やメールアドレスではなく「ユーザ名」をアカウントIDとして要求します。 payment.account.revolut.addUserNameInfo={0}\n現在の「Revolut」アカウント({1})には「ユーザ名」がありません。 \nアカウントデータを更新するのにRevolutの「ユーザ名」を入力して下さい。\nアカウント年齢署名状況に影響を及ぼしません。 payment.revolut.addUserNameInfo.headLine=Revolutアカウントをアップデートする payment.cashapp.info=Cash App はほとんどの銀行振込よりもチャージバックリスクが高いことにご注意ください。 payment.venmo.info=Venmo はほとんどの銀行振込よりもチャージバックリスクが高いことにご注意ください。 payment.paypal.info=PayPal はほとんどの銀行振込よりもチャージバックリスクが高いことにご注意ください。 payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account payment.usPostalMoneyOrder.info=Havenoでアメリカ合衆国郵便為替(USPMO)をトレードするには、以下を理解する必要があります:\n\n-送る前に、XMR買い手は必ずXMR売り手の名前を支払人そして支払先フィールド両方に書いて、追跡証明も含めるUSPMOそして封筒の高解像度写真を取る必要があります。\n-XMR買い手は必ず配達確認を利用してXMR売り手にUSPMOを送る必要があります。\n\n調停が必要になる場合、あるいはトレード係争が開始される場合、調停者や調停人がアメリカ合衆国郵便のサイトで詳細を確認できるように、取った写真、USPMOシリアル番号、郵便局番号、そしてドル金額を送る必要があります。\n\n調停者や調停人に必要な情報を提供しなければ、係争で不利な裁定を下されます。\n\n全ての係争には、調停者や調停人に証明を提供するのは100%USPMO送付者の責任です。\n\n以上の条件を理解しない場合、HavenoでUSPMOのトレードをしないで下さい。 payment.payByMail.info=Havenoを利用したPay by Mailでの取引には、以下を理解する必要があります:\n\ ● XMRの買い手は現金を封がれた袋に入れる必要があります。\n\ ● XMRの買い手は、送り先の住所とトラッキング番号が包装に貼られた状態で、現金の包装プロセスをビデオ撮影したり、高解像度の写真を撮影する必要があります。\n\ ● XMRの買い手は、配達確認と適切な保険をつけて、現金のパッケージをXMRの売り手に送らなければなりません。\n\ ● XMRの売り手は、パッケージを開封する様子をビデオに撮影し、送り手が提供したトラッキング番号がビデオで見えるようにする必要があります。\n\ ● オファーの作成者は、支払いアカウントの「追加情報」フィールドに特別な条件を記述する必要があります。\n\ ● オファーを受けることで、オファーの作成者の条件に同意するものとします。\n\ \n\ Pay by Mail取引は、正直に行動する責任を両者に完全に負わせます。\n\ \n\ ● 他の伝統的な取引に比べて、Pay by Mail取引には検証可能なアクションが少ないため、紛争の処理が難しくなります。\n\ ● 紛争は直接取引者同士でトレーダーチャットを使用して解決しようとしてください。これがPay by Mail紛争を解決するための最も有望な方法です。\n\ ● 仲裁人はあなたのケースを考慮し提案することができますが、彼らが助けることを保証するものではありません。\n\ ● 仲裁人は彼らに提供された証拠に基づいて判断を下します。したがって、紛争の場合は上記のプロセスに従い、証拠を取ってください。\n\ ● Pay By Mail取引によって失われた資金の払い戻しリクエストはHavenoでは考慮されません。\n\ \n\ これらの要件が理解できない場合は、HavenoでのPay by Mail取引を行わないでください。 payment.payByMail.contact=連絡情報 payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.f2f.contact=連絡情報 payment.f2f.contact.prompt=トレードピアからどのように連絡を受け取りたいのでしょうか?(メールアドレス、電話番号…) payment.f2f.city=「対面」で会うための市区町村 payment.f2f.city.prompt=オファーとともに市区町村が表示されます payment.shared.optionalExtra=オプションの追加情報 payment.shared.extraInfo=追加情報 payment.shared.extraInfo.offer=追加のオファー情報 payment.shared.extraInfo.prompt.paymentAccount=この支払いアカウントのオファーと一緒に表示したい特別な契約条件または詳細を定義して下さい(オファーを受ける前に、ユーザはこの情報を見れます)。 payment.shared.extraInfo.prompt.offer=提供内容と共に表示したい特別な用語、条件、または詳細を定義してください。 payment.shared.extraInfo.noDeposit=連絡先詳細およびオファー条件 payment.f2f.info=「対面」トレードには違うルールがあり、オンライントレードとは異なるリスクを伴います。\n\n主な違いは以下の通りです。\n●取引者は、提供される連絡先の詳細を使用して、出会う場所と時間に関する情報を交換する必要があります。\n●取引者は自分のノートパソコンを持ってきて、集合場所で「送金」と「入金」の確認をする必要があります。\n●メイカーに特別な「取引条件」がある場合は、アカウントの「追加情報」テキストフィールドにその旨を記載する必要があります。\n●オファーを受けると、テイカーはメイカーの「トレード条件」に同意したものとします。\n●係争が発生した場合、集合場所で何が起きたのかについての改ざん防止証明を入手することは通常困難であるため、調停者や調停人はあまりサポートをできません。このような場合、XMRの資金は無期限に、または取引者が合意に達するまでロックされる可能性があります。\n\n「対面」トレードでの違いを完全に理解しているか確認するためには、次のURLにある手順と推奨事項をお読みください:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=Webページを開く payment.f2f.offerbook.tooltip.countryAndCity=国と都市: {0} / {1} payment.shared.extraInfo.tooltip=追加情報: {0} payment.japan.bank=銀行 payment.japan.branch=支店 payment.japan.account=口座 payment.japan.recipient=名義 payment.australia.payid=PayID payment.payid=金融機関と繋がっているPayID。例えばEメールアドレスそれとも携帯電話番号。 payment.payid.info=銀行、信用金庫、あるいは住宅金融組合アカウントと安全に繋がれるPayIDとして使われる電話番号、Eメールアドレス、それともオーストラリア企業番号(ABN)。すでにオーストラリアの金融機関とPayIDを作った必要があります。送金と受取の金融機関は両方PayIDをサポートする必要があります。詳しくは以下を訪れて下さい [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=アマゾンeGiftカードで支払うには、アマゾンアカウントを使ってeGiftカードをXMR売り手に送る必要があります。\n\nHavenoはeGiftカードの送り先になるXMR売り手のメールアドレスそれとも電話番号を表示します。そしてeGiftカードのメッセージフィールドに、必ずトレードIDを入力して下さい。最良の慣行について詳しくはWikiを参照して下さい:[HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Amazon_eGift_card]\n\n3つの注意点:\n- 可能であれば、100米ドル価格以下のeGiftカードを送って下さい。それ以上の価格はアマゾンに不正な取引というフラグが立てられることがあります。\n- eGiftカードのメッセージフィールドに、トレードIDと一緒に信ぴょう性のあるメッセージを入力して下さい。(例えば隆さん、「お誕生日おめでとう!」)。(そして確認のため、取引者チャットでトレードピアにメッセージの内容を伝えて下さい)。\n- アマゾンeGiftカードは買われたサイトのみに交換できます(例えば、amazon.jpから買われたカードはamazon.jpのみに交換できます)。 payment.paysafe.info=あなたの保護のため、支払いにPaysafecard PINの使用は強くお勧めしません。\n\n\ PINを使用した取引は、紛争解決のために独立して確認することができません。問題が発生した場合、資金の回収が不可能になることがあります。\n\n\ 取引の安全性と紛争解決を確保するため、常に確認可能な記録を提供する支払い方法を使用してください。 # We use constants from the code so we do not use our normal naming convention # dynamic values are not recognized by IntelliJ # Only translate general terms NATIONAL_BANK=国立銀行振替 SAME_BANK=同じ銀行での送金 SPECIFIC_BANKS=特定銀行での送金 US_POSTAL_MONEY_ORDER=米国郵便為替 CASH_DEPOSIT=現金入金 PAY_BY_MAIL=郵送で現金 MONEY_GRAM=MoneyGram WESTERN_UNION=Western Union F2F=対面(直接) JAPAN_BANK=日本全銀振込 AUSTRALIA_PAYID=オーストラリアのPayID # suppress inspection "UnusedProperty" NATIONAL_BANK_SHORT=国立銀行 # suppress inspection "UnusedProperty" SAME_BANK_SHORT=同じ銀行 # suppress inspection "UnusedProperty" SPECIFIC_BANKS_SHORT=特定の銀行 # suppress inspection "UnusedProperty" US_POSTAL_MONEY_ORDER_SHORT=米国為替 # suppress inspection "UnusedProperty" CASH_DEPOSIT_SHORT=現金入金 # suppress inspection "UnusedProperty" PAY_BY_MAIL_SHORT=郵送で現金 # suppress inspection "UnusedProperty" MONEY_GRAM_SHORT=MoneyGram # suppress inspection "UnusedProperty" WESTERN_UNION_SHORT=Western Union # suppress inspection "UnusedProperty" F2F_SHORT=対面 # suppress inspection "UnusedProperty" JAPAN_BANK_SHORT=日本全銀振込 # suppress inspection "UnusedProperty" AUSTRALIA_PAYID_SHORT=PayID # Do not translate brand names # suppress inspection "UnusedProperty" UPHOLD=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY=Popmoney # suppress inspection "UnusedProperty" REVOLUT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY=WeChat Pay # suppress inspection "UnusedProperty" SEPA=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT=SEPAインスタント支払い # suppress inspection "UnusedProperty" FASTER_PAYMENTS=Faster Payments # suppress inspection "UnusedProperty" SWISH=Swish # suppress inspection "UnusedProperty" ZELLE=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS=アルトコイン # suppress inspection "UnusedProperty" PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=アマゾンeGiftカード # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT=アルトコイン インスタント # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY=OKPay # suppress inspection "UnusedProperty" CASH_APP=Cash App # suppress inspection "UnusedProperty" VENMO=Venmo # suppress inspection "UnusedProperty" UPHOLD_SHORT=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM_SHORT=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY_SHORT=Popmoney # suppress inspection "UnusedProperty" REVOLUT_SHORT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY_SHORT=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY_SHORT=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY_SHORT=WeChat Pay # suppress inspection "UnusedProperty" SEPA_SHORT=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT_SHORT=SEPA インスタント # suppress inspection "UnusedProperty" FASTER_PAYMENTS_SHORT=Faster Payments # suppress inspection "UnusedProperty" SWISH_SHORT=Swish # suppress inspection "UnusedProperty" ZELLE_SHORT=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY_SHORT=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER_SHORT=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH_SHORT=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS_SHORT=アルトコイン # suppress inspection "UnusedProperty" PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=アマゾンeGiftカード # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT_SHORT=アルトコイン インスタント # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY_SHORT=OKPay # suppress inspection "UnusedProperty" CASH_APP_SHORT=Cash App # suppress inspection "UnusedProperty" VENMO_SHORT=Venmo #################################################################### # Validation #################################################################### validation.empty=空白の入力は許可されていません。 validation.NaN=入力が不正な数です。 validation.notAnInteger=入力が整数値ではありません。 validation.zero=0の入力は許可されていません。 validation.negative=負の値は許可されていません。 validation.traditional.tooSmall=可能な最小量より小さい入力は許可されていません。 validation.traditional.tooLarge=可能な最大量より大きい入力は許可されていません。 validation.xmr.fraction=この入力では1サトシ以下のビットコイン値が生成されます。 validation.xmr.tooLarge={0}より大きい入力は許可されていません。 validation.xmr.tooSmall={0}より小さい入力は許可されていません。 validation.passwordTooShort=入力したパスワードが短すぎます。最低8文字が必要です。 validation.passwordTooLong=入力したパスワードが長すぎます。 50文字を超えることはできません。 validation.sortCodeNumber={0}は{1}個の数字で構成されている必要があります。 validation.sortCodeChars={0}は{1}文字で構成されている必要があります。 validation.bankIdNumber={0}は{1}個の数字で構成されている必要があります。 validation.accountNr=アカウント番号は{0}個の数字で構成されている必要があります。 validation.accountNrChars=アカウント番号は{0}文字で構成されている必要があります。 validation.xmr.invalidAddress=アドレスが正しくありません。アドレス形式を確認してください。 validation.integerOnly=整数のみを入力してください。 validation.inputError=入力エラーを起こしました:\n{0} validation.xmr.exceedsMaxTradeLimit=あなたのトレード制限は{0}です。 validation.nationalAccountId={0}は{1}個の数字で構成されている必要があります。 #new validation.invalidInput=不正な入力: {0} validation.accountNrFormat=アカウント番号は次の形式である必要があります: {0} # suppress inspection "UnusedProperty" validation.crypto.wrongStructure={0}アドレスの構造と一致しないためアドレス検証に失敗しました。 # suppress inspection "UnusedProperty" validation.crypto.ltz.zAddressesNotSupported=LTZアドレスは必ずLで始まる必要があります。zで始まるアドレスはサポートされていません。 # suppress inspection "UnusedProperty" validation.crypto.zAddressesNotSupported=ZECアドレスは必ずtで始まる必要があります。zで始まるアドレスはサポートされていません。 # suppress inspection "UnusedProperty" validation.crypto.invalidAddress=アドレスが無効な{0}アドレスです!{1} # suppress inspection "UnusedProperty" validation.crypto.liquidBitcoin.invalidAddress=ネイティブsegwitアドレス(lqで始まるアドレス)はサポートされていません。 validation.bic.invalidLength=入力長が8か11であるべきです validation.bic.letters=銀行コードと国コードは英字でなければなりません validation.bic.invalidLocationCode=BICに不正なロケーションコードが含まれています validation.bic.invalidBranchCode=BICに不正な支店コードが含まれています validation.bic.sepaRevolutBic=Revolut Sepaのアカウントはサポートされていません。 validation.btc.invalidFormat=ビットコインアドレスにとって無効な形式です。 validation.email.invalidAddress=不正なアドレス validation.iban.invalidCountryCode=不正な国コード validation.iban.checkSumNotNumeric=チェックサムは数値でなければなりません validation.iban.nonNumericChars=英数字以外の文字が検出されました validation.iban.checkSumInvalid=IBANチェックサムが不正です validation.iban.invalidLength=数字の長さは15〜34文字でなければなりません。 validation.interacETransfer.invalidAreaCode=非カナダ圏のコード validation.interacETransfer.invalidPhone=有効な11桁の電話番号(例えば1-123-456-7890)それともEメールアドレスを入力して下さい。 validation.interacETransfer.invalidQuestion=文字、数字、スペースおよび/または記号 ' _ , . ? - だけを含める必要があります validation.interacETransfer.invalidAnswer=1つの単語で、文字、数字、- 記号のみを含む必要があります validation.inputTooLarge=入力は{0}より大きくてはいけません validation.inputTooSmall=入力は{0}より大きくなければなりません validation.inputToBeAtLeast=入力は少なくとも{0}でなければなりません validation.amountBelowDust=ダスト制限である{0}サトシ未満の金額は許可されていません。 validation.length=長さは{0}から{1}の間である必要があります validation.fixedLength=Length must be {0} validation.pattern=入力は次の形式である必要があります: {0} validation.noHexString=入力がHEXフォーマットではありません。 validation.advancedCash.invalidFormat=有効なメールアドレスか次のウォレットID形式である必要があります: X000000000000 validation.invalidUrl=有効なURLではありません validation.mustBeDifferent=入力する値は現在の値と異なるべきです。 validation.cannotBeChanged=パラメーターは変更できません validation.numberFormatException=例外の数値フォーマット {0} validation.mustNotBeNegative=負の値は入力できません validation.phone.missingCountryCode=電話番号を検証するのに2文字国コードが必要です validation.phone.invalidCharacters=電話番号 {0} には無効な文字が含まれている validation.phone.insufficientDigits={0} には桁数が不十分で有効電話番号になりません validation.phone.tooManyDigits={0} には桁数が多過ぎて有効電話番号になりません validation.phone.invalidDialingCode=電話番号 {0} の国番号は国の {1} にとって間違っています。正しい国番号は {2} です。 validation.invalidAddressList=有効アドレスのコンマ区切りリストでなければなりません ================================================ FILE: core/src/main/resources/i18n/displayStrings_pt-br.properties ================================================ # Keep display strings organized by domain # Naming convention: We use camelCase and dot separated name spaces. # Use as many sub spaces as required to make the structure clear, but as little as possible. # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break # in the display but only in the editor. # Please use in all language files the exact same order of the entries, that way a comparison is easier. # Please try to keep the length of the translated string similar to English. If it is longer it might break layout or # get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. #################################################################### # Shared #################################################################### shared.readMore=Leia mais shared.openHelp=Abrir a Ajuda shared.warning=Aviso shared.close=Fechar shared.cancel=Cancelar shared.ok=OK shared.yes=Sim shared.no=Não shared.iUnderstand=Eu compreendo shared.na=N/D shared.shutDown=Desligar shared.reportBug=Report bug on GitHub shared.buyMonero=Comprar monero shared.sellMonero=Vender monero shared.buyCurrency=Comprar {0} shared.sellCurrency=Vender {0} shared.buyCurrency.locked=Comprar {0} 🔒 shared.sellCurrency.locked=Vender {0} 🔒 shared.buyingXMRWith=comprando XMR com {0} shared.sellingXMRFor=vendendo XMR por {0} shared.buyingCurrency=comprando {0} (vendendo XMR) shared.sellingCurrency=vendendo {0} (comprando XMR) shared.buy=comprar shared.sell=vender shared.buying=comprando shared.selling=vendendo shared.P2P=P2P shared.oneOffer=oferta shared.multipleOffers=ofertas shared.Offer=Oferta shared.offerVolumeCode={0} Offer Volume shared.openOffers=ofertas abertas shared.trade=negociação shared.trades=negociações shared.openTrades=abrir negociações shared.dateTime=Data/Hora shared.price=Preço shared.priceWithCur=Preço em {0} shared.priceInCurForCur=Preço em {0} para 1 {1} shared.fixedPriceInCurForCur=Preço em {0} fixo para 1 {1} shared.amount=Quantidade shared.txFee=Taxa de negociação shared.tradeFee=Trade Fee shared.buyerSecurityDeposit=Depósito do comprador shared.sellerSecurityDeposit=Depósito do vendedor shared.amountWithCur=Quantidade em {0} shared.volumeWithCur=Volume em {0} shared.currency=Moeda shared.market=Mercado shared.deviation=Deviation shared.paymentMethod=Método de pagamento shared.tradeCurrency=Moeda negociada shared.offerType=Tipo de oferta shared.details=Detalhes shared.address=Endereço shared.balanceWithCur=Saldo em {0} shared.utxo=Unspent transaction output shared.txId=ID da Transação shared.confirmations=Confirmações shared.revert=Reverter transação shared.select=Selecionar shared.usage=Uso shared.state=Status shared.tradeId=ID da Negociação shared.offerId=ID da Oferta shared.bankName=Nome do banco shared.acceptedBanks=Bancos aceitos shared.amountMinMax=Quantidade (min - max) shared.amountHelp=Se uma oferta possuir uma quantia mínima e máxima definidas, você poderá negociar qualquer quantia dentro dessa faixa. shared.remove=Remover shared.goTo=Ir para {0} shared.XMRMinMax=XMR (min - max) shared.removeOffer=Remover oferta shared.dontRemoveOffer=Não remover a oferta shared.editOffer=Editar oferta shared.openLargeQRWindow=Open large QR code window shared.tradingAccount=Conta de negociação shared.faq=Visit FAQ page shared.yesCancel=Sim, cancelar shared.nextStep=Próximo passo shared.selectTradingAccount=Selecionar conta de negociação shared.fundFromSavingsWalletButton=Aplicar fundos da carteira Haveno shared.fundFromExternalWalletButton=Abrir sua carteira externa para prover fundos shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? shared.belowInPercent=% abaixo do preço de mercado shared.aboveInPercent=% acima do preço de mercado shared.enterPercentageValue=Insira a % shared.OR=OU shared.notEnoughFunds=You don't have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. shared.waitingForFunds=Aguardando pagamento... shared.TheXMRBuyer=O comprador de XMR shared.You=Você shared.sendingConfirmation=Enviando confirmação... shared.sendingConfirmationAgain=Por favor, envie a confirmação novamente shared.exportCSV=Export to CSV shared.exportJSON=Exportar para JSON shared.summary=Show summary shared.noDateAvailable=Sem data disponível shared.noDetailsAvailable=Sem detalhes disponíveis shared.notUsedYet=Ainda não usado shared.date=Data shared.sendFundsDetailsWithFee=Enviando: {0}\n\nPara o endereço de recebimento: {1}\n\nTaxa adicional do minerador: {2}\n\nTem certeza de que deseja enviar esse valor? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Copiar para área de transferência shared.language=Idioma shared.country=País shared.applyAndShutDown=Aplicar e desligar shared.selectPaymentMethod=Selecionar método de pagamento shared.accountNameAlreadyUsed=That account name is already used for another saved account.\nPlease choose another name. shared.askConfirmDeleteAccount=Você realmente quer apagar a conta selecionada? shared.cannotDeleteAccount=You cannot delete that account because it is being used in an open offer (or in an open trade). shared.noAccountsSetupYet=Ainda não há contas configuradas shared.manageAccounts=Gerenciar contas shared.addNewAccount=Adicionar conta nova shared.ExportAccounts=Exportar Contas shared.importAccounts=Importar Contas shared.createNewAccount=Criar nova conta shared.createNewAccountDescription=Os detalhes da sua conta são armazenados localmente no seu dispositivo e compartilhados apenas com seu parceiro de negociação e o árbitro, caso uma disputa seja aberta. shared.saveNewAccount=Salvar nova conta shared.selectedAccount=Conta selecionada shared.deleteAccount=Apagar conta shared.errorMessageInline=\nMensagem de erro: {0} shared.errorMessage=Mensagem de erro shared.information=Informação shared.name=Nome shared.id=ID shared.dashboard=Painel shared.accept=Aceitar shared.balance=Saldo shared.save=Salvar shared.onionAddress=Endereço Onion shared.supportTicket=solicitação de suporte shared.dispute=disputa shared.mediationCase=mediação shared.seller=vendedor shared.buyer=comprador shared.allEuroCountries=Todos os países do Euro shared.acceptedTakerCountries=Países tomadores aceitos shared.tradePrice=Preço de negociação shared.tradeAmount=Quantidade negociada shared.tradeVolume=Volume de negociação shared.invalidKey=A chave que você inseriu não estava correta shared.enterPrivKey=Insira a chave privada para destravar shared.payoutTxId=ID da transação de pagamento shared.contractAsJson=Contrato em formato JSON shared.viewContractAsJson=Ver contrato em formato JSON shared.contract.title=Contrato para negociação com ID: {0} shared.paymentDetails=Detalhes de pagamento do {0} de XMR shared.securityDeposit=Depósito de segurança shared.yourSecurityDeposit=Seu depósito de segurança shared.contract=Contrato shared.messageArrived=Chegou mensagem. shared.messageStoredInMailbox=Mensagem guardada na caixa de correio. shared.messageSendingFailed=Falha no envio da mensagem. Erro: {0} shared.unlock=Destravar shared.toReceive=a receber shared.toSpend=a ser gasto shared.xmrAmount=Quantidade de XMR shared.yourLanguage=Seus idiomas shared.addLanguage=Adicionar idioma shared.total=Total shared.totalsNeeded=Fundos necessária shared.tradeWalletAddress=Endereço da carteira de negociação shared.tradeWalletBalance=Saldo da carteira de negociação shared.reserveExactAmount=Reserve apenas os fundos necessários. Requer uma taxa de mineração e cerca de 20 minutos antes que sua oferta seja publicada. shared.makerTxFee=Ofertante: {0} shared.takerTxFee=Aceitador: {0} shared.iConfirm=Eu confirmo shared.openURL=Aberto {0} shared.fiat=Fiat shared.crypto=Cripto shared.preciousMetals=Metais Preciosos shared.all=Todos shared.edit=Editar shared.advancedOptions=Opções avançadas shared.interval=Intervalo shared.actions=Ações shared.buyerUpperCase=Comprador shared.sellerUpperCase=Vendedor shared.new=NOVO shared.blindVoteTxId=ID de transação de voto fechado shared.proposal=Proposta shared.votes=Votos shared.learnMore=Saiba mais shared.dismiss=Dispensar shared.selectedArbitrator=Árbitro escolhido shared.selectedMediator=Mediador selecionado shared.selectedRefundAgent=Árbitro escolhido shared.mediator=Mediador shared.arbitrator=Árbitro shared.refundAgent=Árbitro shared.refundAgentForSupportStaff=Árbitro shared.delayedPayoutTxId=Delayed payout transaction ID shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to shared.unconfirmedTransactionsLimitReached=No momento, você possui muitas transações não-confirmadas. Tente novamente mais tarde. shared.numItemsLabel=Number of entries: {0} shared.filter=Filtro shared.enabled=Enabled #################################################################### # UI views #################################################################### #################################################################### # MainView #################################################################### mainView.menu.market=Mercado mainView.menu.buyXmr=Comprar XMR mainView.menu.sellXmr=Vender XMR mainView.menu.portfolio=Portfolio mainView.menu.funds=Fundos mainView.menu.support=Suporte mainView.menu.settings=Configurações mainView.menu.account=Conta mainView.marketPriceWithProvider.label=Preço de mercado por {0} mainView.marketPrice.havenoInternalPrice=Preço da última negociação Haveno mainView.marketPrice.tooltip.havenoInternalPrice=Não foi encontrado preço de mercado nos provedores externos.\nO preço exibido corresponde ao último preço de negociação no Haveno para essa moeda. mainView.marketPrice.tooltip=Preço de Mercado fornecido por {0}{1}\nÚltima atualização: {2}\nURL do provedor: {3} mainView.balance.available=Saldo disponível mainView.balance.reserved=Reservado em ofertas mainView.balance.pending=Travado em negociações mainView.balance.reserved.short=Reservado mainView.balance.pending.short=Travado mainView.footer.usingTor=(via Tor) mainView.footer.localhostMoneroNode=(localhost) mainView.footer.clearnet=(via clearnet) mainView.footer.xmrInfo={0} {1} mainView.footer.xmrFeeRate=/ Fee rate: {0} sat/vB mainView.footer.xmrInfo.initializing=Conectando-se à rede Haveno mainView.footer.xmrInfo.synchronizingWith=Sincronizando com {0} no bloco: {1} / {2} mainView.footer.xmrInfo.connectedTo=Conectado a {0} no bloco {1} mainView.footer.xmrInfo.synchronizingWalletWith=Sincronizando a carteira com {0} no bloco: {1} / {2} mainView.footer.xmrInfo.syncedWith=Sincronizado com {0} no bloco {1} mainView.footer.xmrInfo.connectingTo=Conectando-se a mainView.footer.xmrInfo.connectionFailed=Falha na conexão à mainView.footer.xmrPeers=Monero network peers: {0} mainView.footer.p2pPeers=Haveno network peers: {0} mainView.bootstrapState.connectionToTorNetwork=(1/4) Conectando-se à rede Tor... mainView.bootstrapState.torNodeCreated=(2/4) Nó da rede Tor criado mainView.bootstrapState.hiddenServicePublished=(3/4) Serviço Oculto publicado mainView.bootstrapState.initialDataReceived=(4/4) Dados iniciais recebidos mainView.bootstrapWarning.noSeedNodesAvailable=Nenhum nó semente disponível mainView.bootstrapWarning.noNodesAvailable=Sem nós semente e pares disponíveis mainView.bootstrapWarning.bootstrappingToP2PFailed=A inicialização para a rede Haveno falhou mainView.p2pNetworkWarnMsg.noNodesAvailable=Não há nós semente ou pares persistentes para requisição de dados.\nPor gentileza verifique sua conexão com a internet ou tente reiniciar o programa. mainView.p2pNetworkWarnMsg.connectionToP2PFailed=Falha ao conectar com a rede Haveno (erro reportado: {0}).\nPor gentileza verifique sua conexão ou tente reiniciar o programa. mainView.walletServiceErrorMsg.timeout=Não foi possível conectar-se à rede Monero, pois o tempo limite expirou. mainView.walletServiceErrorMsg.connectionError=Não foi possível conectar-se à rede Monero, devido ao seguinte erro: {0} mainView.walletServiceErrorMsg.rejectedTxException=Uma transação foi rejeitada pela rede.\n\n{0} mainView.networkWarning.allConnectionsLost=Você perdeu sua conexão com todos os pontos da rede {0}.\nTalvez você tenha perdido a conexão com a internet ou seu computador estava em modo de espera. mainView.networkWarning.localhostMoneroLost=Você perdeu a conexão ao nó Monero do localhost.\nPor favor, reinicie o aplicativo Haveno para conectar-se a outros nós Monero ou reinicie o nó Monero do localhost. mainView.version.update=(Atualização disponível) #################################################################### # MarketView #################################################################### market.tabs.offerBook=Livro de ofertas market.tabs.spreadCurrency=Offers by Currency market.tabs.spreadPayment=Offers by Payment Method market.tabs.trades=Negociações # OfferBookChartView market.offerBook.buyCrypto=Comprar {0} (vender {1}) market.offerBook.sellCrypto=Vender {0} (comprar {1}) market.offerBook.buyWithTraditional=Comprar {0} market.offerBook.sellWithTraditional=Vender {0} market.offerBook.sellOffersHeaderLabel=Vender {0} para market.offerBook.buyOffersHeaderLabel=Comprar {0} de market.offerBook.buy=Eu quero comprar monero market.offerBook.sell=Eu quero vender monero # SpreadView market.spread.numberOfOffersColumn=Todas as ofertas ({0}) market.spread.numberOfBuyOffersColumn=Comprar XMR ({0}) market.spread.numberOfSellOffersColumn=Vender XMR ({0}) market.spread.totalAmountColumn=Total de XMR ({0}) market.spread.spreadColumn=Spread market.spread.expanded=Expanded view # TradesChartsView market.trades.nrOfTrades=Negociações: {0} market.trades.tooltip.volumeBar=Volume: {0} / {1}\nNo. of trades: {2}\nDate: {3} market.trades.tooltip.candle.open=Abrir: market.trades.tooltip.candle.close=Fechar: market.trades.tooltip.candle.high=Alta: market.trades.tooltip.candle.low=Baixa: market.trades.tooltip.candle.average=Média: market.trades.tooltip.candle.median=Mediana: market.trades.tooltip.candle.date=Data: market.trades.showVolumeInUSD=Show volume in USD #################################################################### # OfferView #################################################################### offerbook.createOffer=Criar oferta offerbook.takeOffer=Aceitar oferta offerbook.takeOfferToBuy=Comprar {0} offerbook.takeOfferToSell=Vender {0} offerbook.takeOffer.enterChallenge=Digite a senha da oferta offerbook.trader=Trader offerbook.offerersBankId=ID do banco do ofertante (BIC/SWIFT): {0} offerbook.offerersBankName=Nome do banco do ofertante: {0} offerbook.offerersBankSeat=País da sede do banco do ofertante: {0} offerbook.offerersAcceptedBankSeatsEuro=Países sedes aceitos pelo banco (tomador): Todos os países da zona do Euro offerbook.offerersAcceptedBankSeats=Países aceitos como sede bancária (tomador):\n{0} offerbook.availableOffers=Ofertas disponíveis offerbook.filterByCurrency=Filtrar por moeda offerbook.filterByPaymentMethod=Filtrar por método de pagamento offerbook.matchingOffers=Ofertas que correspondem às minhas contas offerbook.filterNoDeposit=Sem depósito offerbook.noDepositOffers=Ofertas sem depósito (senha necessária) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info=Esta conta foi verificada e {0} offerbook.timeSinceSigning.info.arbitrator=assinada por um árbitro e pode assinar contas de pares offerbook.timeSinceSigning.info.peer=signed by a peer, waiting %d days for limits to be lifted offerbook.timeSinceSigning.info.peerLimitLifted=assinada por um par e limites foram levantados offerbook.timeSinceSigning.info.signer=assinada por um par e pode assinar contas de pares (limites levantados) offerbook.timeSinceSigning.info.banned=conta foi banida offerbook.timeSinceSigning.daysSinceSigning={0} dias offerbook.timeSinceSigning.daysSinceSigning.long={0} desde a assinatura offerbook.xmrAutoConf=Is auto-confirm enabled offerbook.buyXmrWith=Compre XMR com: offerbook.sellXmrFor=Venda XMR por: offerbook.timeSinceSigning.help=Quando você completa uma negociação bem sucedida com um par que tem uma conta de pagamento assinada, a sua conta de pagamento é assinada.\n{0} dias depois, o limite inicial de {1} é levantado e sua conta pode assinar as contas de pagamento de outros pares. offerbook.timeSinceSigning.notSigned=Ainda não assinada offerbook.timeSinceSigning.notSigned.ageDays={0} dias offerbook.timeSinceSigning.notSigned.noNeed=N/D shared.notSigned=This account has not been signed yet and was created {0} days ago shared.notSigned.noNeed=This account type does not require signing shared.notSigned.noNeedDays=This account type does not require signing and was created {0} days ago shared.notSigned.noNeedAlts=Crypto accounts do not feature signing or aging offerbook.nrOffers=N.º de ofertas: {0} offerbook.volume={0} (mín. - máx.) offerbook.deposit=Deposit XMR (%) offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed. offerbook.createNewOffer=Criar oferta para {0} {1} offerbook.createOfferToBuy=Criar oferta para comprar {0} offerbook.createOfferToSell=Criar oferta para vender {0} offerbook.createOfferToBuy.withTraditional=Criar nova oferta para comprar {0} com {1} offerbook.createOfferToSell.forTraditional=Criar nova oferta para vender {0} por {1} offerbook.createOfferToBuy.withCrypto=Criar oferta para vender {0} (comprar {1}) offerbook.createOfferToSell.forCrypto=Criar oferta para comprar {0} (vender {1}) offerbook.takeOfferButton.tooltip=Aceitar oferta {0} offerbook.yesCreateOffer=Sim, criar oferta offerbook.setupNewAccount=Configurar uma nova conta de negociação offerbook.removeOffer.success=Remoção de oferta bem sucedida offerbook.removeOffer.failed=Remoção da oferta falhou:\n{0} offerbook.deactivateOffer.failed=Erro ao desativar oferta:\n{0} offerbook.activateOffer.failed=Erro ao publicar oferta:\n{0} offerbook.withdrawFundsHint=Você pode retirar fundos que você pagou da tela {0}. offerbook.warning.noTradingAccountForCurrency.headline=No payment account for selected currency offerbook.warning.noTradingAccountForCurrency.msg=You don't have a payment account set up for the selected currency.\n\nWould you like to create an offer for another currency instead? offerbook.warning.noMatchingAccount.headline=No matching payment account. offerbook.warning.noMatchingAccount.msg=This offer uses a payment method you haven't set up yet. \n\nWould you like to set up a new payment account now? offerbook.warning.counterpartyTradeRestrictions=Esta oferta não pode ser tomada por restrições de negociação da outra parte offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\nAfter successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\nFor more information on account signing, please see the documentation at [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. popup.warning.tradeLimitDueAccountAgeRestriction.seller=A quantia permitida para a negociação está limitada a {0} devido a restrições de segurança baseadas nos seguintes critérios:\n- A conta do comprador não foi assinada por um árbitro ou um par\n- A conta do comprador foi assinada há menos de 30 dias\n- O meio de pagamento para essa oferta é considerado de risco para estornos bancários.\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=A quantia permitida para a negociação está limitada a {0} devido a restrições de segurança baseadas nos seguintes critérios:\n- A sua conta não foi assinada por um árbitro ou um par\n- A sua conta foi assinada há menos de 30 dias\n- O meio de pagamento para essa oferta é considerado de risco para estornos bancários.\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=Este método de pagamento está temporariamente limitado a {0} até {1} porque todos os compradores têm novas contas.\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=Sua oferta será limitada a compradores com contas assinadas e antigas porque excede {0}.\n\n{1} offerbook.warning.wrongTradeProtocol=Essa oferta requer uma versão do protocolo diferente da usada em sua versão do software.\n\nVerifique se você possui a versão mais nova instalada, caso contrário o usuário que criou a oferta usou uma versão ultrapassada.\n\nUsuários não podem negociar com uma versão incompatível do protocolo. offerbook.warning.userIgnored=Você adicionou o endereço onion à sua lista de endereços ignorados. offerbook.warning.offerBlocked=Essa oferta foi bloqueada pelos desenvolvedores do Haveno.\nProvavelmente há um problema não resolvido associado àquela oferta. offerbook.warning.currencyBanned=A moeda usada nesta oferta foi bloqueada pelos desenvolvedores do Haveno.\nPor favor, visite o Fórum do Haveno para maiores informações. offerbook.warning.paymentMethodBanned=O método de pagamento usado nesta oferta foi bloqueado pelos desenvolvedores do Haveno.\nPor favor, visite o Fórum do Haveno para maiores informações. offerbook.warning.nodeBlocked=O endereço onion daquele negociador foi bloqueado pelos desenvolvedores do Haveno.\nProvavelmente há um problema não resolvido associado àquele negociador. offerbook.warning.offerWasAlreadyUsedInTrade=You cannot take this offer because you already took it earlier. It could be that your previous take-offer attempt resulted in a failed trade. offerbook.info.sellAtMarketPrice=Você irá vender a preço de mercado (atualizado a cada minuto). offerbook.info.buyAtMarketPrice=Você irá comprar a preço de mercado (atualizado a cada minuto). offerbook.info.sellBelowMarketPrice=Você irá receber {0} a menos do que o atual preço de mercado (atualizado a cada minuto). offerbook.info.buyAboveMarketPrice=Você irá pagar {0} a mais do que o atual preço de mercado (atualizado a cada minuto). offerbook.info.sellAboveMarketPrice=Você irá receber {0} a mais do que o atual preço de mercado (atualizado a cada minuto). offerbook.info.buyBelowMarketPrice=Você irá pagar {0} a menos do que o atual preço de mercado (atualizado a cada minuto). offerbook.info.buyAtFixedPrice=Você irá comprar nesse preço fixo. offerbook.info.sellAtFixedPrice=Você irá vender neste preço fixo. offerbook.info.noArbitrationInUserLanguage=Em caso de disputa, a arbitragem para essa oferta será realizada em {0}. O idioma atualmente está definido como {1}. offerbook.info.roundedFiatVolume=O valor foi arredondado para aumentar a privacidade da sua negociação. #################################################################### # Offerbook / Create offer #################################################################### createOffer.amount.prompt=Insira o valor em XMR createOffer.price.prompt=Insira o preço createOffer.volume.prompt=Insira o valor em {0} createOffer.amountPriceBox.amountDescription=Quantia em XMR para {0} createOffer.amountPriceBox.buy.volumeDescription=Valor em {0} a ser gasto createOffer.amountPriceBox.sell.volumeDescription=Valor em {0} a ser recebido createOffer.amountPriceBox.minAmountDescription=Quantia mínima de XMR createOffer.securityDeposit.prompt=Depósito de segurança createOffer.fundsBox.title=Financie sua oferta createOffer.fundsBox.offerFee=Taxa de negociação createOffer.fundsBox.networkFee=Taxa de mineração createOffer.fundsBox.placeOfferSpinnerInfo=Sua oferta está sendo publicada... createOffer.fundsBox.paymentLabel=Negociação Haveno com ID {0} createOffer.fundsBox.fundsStructure=({0} para o depósito de segurança, {1} para a taxa de transação e {2} para a taxa de mineração) createOffer.success.headline=Sua oferta foi criada createOffer.success.info=Você pode gerenciar suas ofertas abertas em \"Portfolio/Minhas ofertas\". createOffer.info.sellAtMarketPrice=Você irá sempre vender a preço de mercado e o preço de sua oferta será atualizado constantemente. createOffer.info.buyAtMarketPrice=Você irá sempre comprar a preço de mercado e o preço de sua oferta será atualizado constantemente. createOffer.info.sellAboveMarketPrice=Você irá sempre receber {0}% a mais do que o atual preço de mercado e o preço de sua oferta será atualizado constantemente. createOffer.info.buyBelowMarketPrice=Você irá sempre pagar {0}% a menos do que o atual preço de mercado e o preço de sua oferta será atualizado constantemente. createOffer.warning.sellBelowMarketPrice=Você irá sempre receber {0}% a menos do que o atual preço de mercado e o preço da sua oferta será atualizado constantemente. createOffer.warning.buyAboveMarketPrice=Você irá sempre pagar {0}% a mais do que o atual preço de mercado e o preço da sua oferta será atualizada constantemente. createOffer.tradeFee.descriptionXMROnly=Taxa de negociação createOffer.tradeFee.descriptionBSQEnabled=Escolha a moeda da taxa de transação createOffer.triggerPrice.prompt=Set optional trigger price createOffer.triggerPrice.label=Deactivate offer if market price is {0} createOffer.triggerPrice.tooltip=As protection against drastic price movements you can set a trigger price which deactivates the offer if the market price reaches that value. createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0} createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0} # new entries createOffer.placeOfferButton.buy=Revisar: Criar oferta para comprar XMR com {0} createOffer.placeOfferButton.sell=Revisar: Criar oferta para vender XMR por {0} createOffer.createOfferFundWalletInfo.headline=Financiar sua oferta # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Quantia da negociação: {0} \n createOffer.createOfferFundWalletInfo.msg=Você precisa depositar {0} para esta oferta.\n\n\ Esses fundos são reservados em sua carteira local e serão bloqueados em uma carteira multisig assim que alguém aceitar sua oferta.\n\n\ O valor é a soma de:\n\ {1}\ - Seu depósito de segurança: {2}\n\ - Taxa de negociação: {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=Um erro ocorreu ao emitir uma oferta:\n\n{0}\n\nNenhum fundo foi retirado de sua carteira até agora.\nPor favor, reinicie o programa e verifique sua conexão de internet. createOffer.setAmountPrice=Definir quantidade e preço createOffer.warnCancelOffer=Você já alocou fundos para essa oferta.\nSe cancelar agora, seus fundos permanecerão em sua carteira local da Haveno e estarão disponíveis para saque na tela de \"Fundos/Enviar fundos\".\nTem certeza de que deseja cancelar? createOffer.timeoutAtPublishing=Um erro ocorreu ao publicar a oferta: tempo esgotado. createOffer.errorInfo=\n\nA taxa já está paga. No pior dos casos, você perdeu essa taxa.\nPor favor, tente reiniciar o seu aplicativo e verifique sua conexão de rede para ver se você pode resolver o problema. createOffer.tooLowSecDeposit.warning=Você definiu o depósito de segurança para um valor mais baixo do que o valor padrão recomendado de {0}.\nVocê tem certeza de que deseja usar um depósito de segurança menor? createOffer.tooLowSecDeposit.makerIsSeller=Há menos proteção caso a outra parte da negociação não siga o protocolo de negociação. createOffer.tooLowSecDeposit.makerIsBuyer=Os seus compradores se sentirão menos seguros, pois você terá menos moneros sob risco. Isso pode fazer com que eles prefiram escolher outras ofertas ao invés da sua. createOffer.resetToDefault=Não, voltar ao valor padrão createOffer.useLowerValue=Sim, usar meu valor mais baixo createOffer.priceOutSideOfDeviation=O preço submetido está fora do desvio máximo com relação ao preço de mercado.\nO desvio máximo é {0} e pode ser alterado nas preferências. createOffer.changePrice=Alterar preço createOffer.tac=Ao publicar essa oferta, eu concordo em negociar com qualquer trader que preencha as condições definidas nesta tela. createOffer.currencyForFee=Taxa de negociação createOffer.setDeposit=Definir o depósito de segurança do comprador (%) createOffer.setDepositAsBuyer=Definir o meu depósito de segurança como comprador (%) createOffer.setDepositForBothTraders=Set both traders' security deposit (%) createOffer.securityDepositInfo=O seu depósito de segurança do comprador será de {0} createOffer.securityDepositInfoAsBuyer=O seu depósito de segurança como comprador será de {0} createOffer.minSecurityDepositUsed=O depósito de segurança mínimo é utilizado createOffer.buyerAsTakerWithoutDeposit=Nenhum depósito necessário do comprador (protegido por senha) createOffer.myDeposit=Meu depósito de segurança (%) createOffer.myDepositInfo=Seu depósito de segurança será {0} #################################################################### # Offerbook / Take offer #################################################################### takeOffer.amount.prompt=Insira a quantia em XMR takeOffer.amountPriceBox.buy.amountDescription=Quantia de XMR para vender takeOffer.amountPriceBox.sell.amountDescription=Quantia de XMR para comprar takeOffer.amountPriceBox.priceDescription=Preço por monero em {0} takeOffer.amountPriceBox.amountRangeDescription=Quantias permitidas takeOffer.amountPriceBox.warning.invalidXmrDecimalPlaces=A quantia que você inseriu excede o número máximo de casas decimais permitida.\nA quantia foi ajustada para 4 casas decimais. takeOffer.validation.amountSmallerThanMinAmount=A quantia não pode ser inferior à quantia mínima definida na oferta. takeOffer.validation.amountLargerThanOfferAmount=A quantia inserida não pode ser superior à quantia definida na oferta. takeOffer.validation.amountLargerThanOfferAmountMinusFee=Essa quantia inserida criaria um troco pequeno demais para o vendedor de XMR. takeOffer.fundsBox.title=Financiar sua negociação takeOffer.fundsBox.isOfferAvailable=Verificando se a oferta está disponível ... takeOffer.fundsBox.tradeAmount=Quantia a ser vendida takeOffer.fundsBox.offerFee=Taxa de negociação takeOffer.fundsBox.networkFee=Total em taxas de mineração takeOffer.fundsBox.takeOfferSpinnerInfo=Aceitando a oferta: {0} takeOffer.fundsBox.paymentLabel=negociação Haveno com ID {0} takeOffer.fundsBox.fundsStructure=({0} depósito de segurança, {1} taxa de transação, {2} taxa de mineração) takeOffer.fundsBox.noFundingRequiredTitle=Sem financiamento necessário takeOffer.fundsBox.noFundingRequiredDescription=Obtenha a frase secreta da oferta com o vendedor fora do Haveno para aceitar esta oferta. takeOffer.success.headline=Você aceitou uma oferta com sucesso. takeOffer.success.info=Você pode ver o status de sua negociação em \"Portfolio/Negociações em aberto\". takeOffer.error.message=Ocorreu um erro ao aceitar a oferta.\n\n{0} # new entries takeOffer.takeOfferButton.buy=Revisar: Aceitar oferta para comprar XMR com {0} takeOffer.takeOfferButton.sell=Revisar: Aceitar oferta para vender XMR por {0} takeOffer.noPriceFeedAvailable=Você não pode aceitar essa oferta pois ela usa uma porcentagem do preço baseada no preço de mercado, mas o canal de preços está indisponível no momento. takeOffer.takeOfferFundWalletInfo.headline=Financiar sua negociação # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Quantia a negociar: {0} \n takeOffer.takeOfferFundWalletInfo.msg=Você precisa depositar {0} para aceitar esta oferta.\n\nO valor é a soma de:\n{1}- Seu depósito de segurança: {2}\n- Taxa de negociação: {3} takeOffer.alreadyPaidInFunds=Se você já pagou por essa oferta, você pode retirar seus fundos na seção \"Fundos/Enviar fundos\". takeOffer.paymentInfo=Informações de pagamento takeOffer.setAmountPrice=Definir quantia takeOffer.alreadyFunded.askCancel=Você já alocou fundos para essa oferta.\nSe cancelar agora, seus fundos permanecerão em sua carteira local da Haveno e estarão disponíveis para saque na tela de \"Fundos/Enviar fundos\".\nTem certeza de que deseja cancelar? takeOffer.failed.offerNotAvailable=Pedido de aceitar oferta de negociação falhou pois a oferta não está mais disponível. Talvez outro negociador tenha aceitado a oferta neste período. takeOffer.failed.offerTaken=Não foi possível aceitar a oferta, pois ela já foi aceita por outro negociador. takeOffer.failed.offerRemoved=Não é possível aceitar a oferta pois ela foi removida. takeOffer.failed.offererNotOnline=Erro ao aceitar a oferta: o ofertante não está mais online. takeOffer.failed.offererOffline=Erro ao aceitar a oferta: o ofertante está offline. takeOffer.warning.connectionToPeerLost=Você perdeu a conexão com o ofertante.\nEle pode ter ficado offline ou teve a conexão com você fechada em decorrência de muitas conexões abertas.\n\nSe você ainda pode ver a oferta dele no livro de ofertas você pode tentar aceitá-la novamente. takeOffer.error.noFundsLost=\n\nA sua carteira ainda não realizou o pagamento.\nPor favor, reinicie o programa e verifique a sua conexão com a internet. # suppress inspection "TrailingSpacesInProperty" takeOffer.error.feePaid=\n\n takeOffer.error.depositPublished=\n\nA transação do depósito já foi publicada.\nPor favor, reinicie o programa e verifique sua conexão de internet para tentar resolver o problema.\nSe o problema persistir, entre em contato com os desenvolvedores. takeOffer.error.payoutPublished=\n\nA transação de pagamento já foi publicada.\nPor favor, reinicie o programa e verifique sua conexão de internet para tentar resolver o problema.\nSe o problema persistir, entre em contato com os desenvolvedores. takeOffer.tac=Ao aceitar essa oferta, eu concordo com as condições de negociação definidas nesta tela. #################################################################### # Offerbook / Edit offer #################################################################### openOffer.header.triggerPrice=Preço gatilho openOffer.triggerPrice=Trigger price {0} openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\nPlease edit the offer to define a new trigger price editOffer.setPrice=Definir preço editOffer.confirmEdit=Editar oferta editOffer.publishOffer=Publicando a sua oferta. editOffer.failed=Erro ao editar oferta:\n{0} editOffer.success=A sua oferta foi editada com sucesso. editOffer.invalidDeposit=O depósito de segurança do comprador não está dentro dos limites definidos pela DAO Haveno e não pode mais ser editado. #################################################################### # Portfolio #################################################################### portfolio.tab.openOffers=Minhas ofertas em aberto portfolio.tab.pendingTrades=Negociações em aberto portfolio.tab.history=Histórico portfolio.tab.failed=Falha portfolio.tab.editOpenOffer=Editar oferta portfolio.closedTrades.deviation.help=Percentage price deviation from market portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} portfolio.pending.syncing=Sincronizando carteira de negociação portfolio.pending.syncing.blockRemaining=Sincronizando carteira de negociação — 1 bloco restante portfolio.pending.syncing.blocksRemaining=Sincronizando carteira de negociação — {0} blocos restantes portfolio.pending.step1.waitForConf=Aguardar confirmação da blockchain portfolio.pending.step2_buyer.additionalConf=Depósitos alcançaram 10 confirmações.\nPara maior segurança, recomendamos aguardar {0} confirmações antes de enviar o pagamento.\nProssiga antecipadamente por sua própria conta e risco. portfolio.pending.step2_buyer.startPayment=Iniciar pagamento portfolio.pending.step2_seller.waitPaymentSent=Aguardar início do pagamento portfolio.pending.step3_buyer.waitPaymentArrived=Aguardar recebimento do pagamento portfolio.pending.step3_seller.confirmPaymentReceived=Confirmar recebimento do pagamento portfolio.pending.step5.completed=Concluído portfolio.pending.step3_seller.autoConf.status.label=Auto-confirm status portfolio.pending.autoConf=Auto-confirmed portfolio.pending.autoConf.blocks=XMR confirmations: {0} / Required: {1} portfolio.pending.autoConf.state.xmr.txKeyReused=Transaction key re-used. Please open a dispute. portfolio.pending.autoConf.state.confirmations=XMR confirmations: {0}/{1} portfolio.pending.autoConf.state.txNotFound=Transaction not seen in mem-pool yet portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=No valid transaction ID / transaction key portfolio.pending.autoConf.state.filterDisabledFeature=Disabled by developers. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FEATURE_DISABLED=Auto-confirm feature is disabled. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=Trade amount exceeds auto-confirm amount limit # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.INVALID_DATA=Peer provided invalid data. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=Payout transaction was already published. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.DISPUTE_OPENED=Dispute was opened. Auto-confirm is deactivated for that trade. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.REQUESTS_STARTED=Transaction proof requests started # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PENDING=Success results: {0}/{1}; {2} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.COMPLETED=Proof at all services succeeded # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. No auto-confirm possible. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. portfolio.pending.step1.info.you=A transação de depósito foi publicada.\nVocê poderá iniciar o pagamento após 10 confirmações (~{0} minutos restantes). portfolio.pending.step1.info.buyer=A transação de depósito foi publicada.\nO comprador de XMR poderá iniciar o pagamento após 10 confirmações (~{0} minutos restantes). portfolio.pending.step1.warn=A transação do depósito ainda não foi confirmada.\nIsto pode ocorrer em casos raros em que a taxa de financiamento de um dos negociadores enviada a partir de uma carteira externa foi muito baixa. portfolio.pending.step1.openForDispute=A transação de depósito ainda não foi confirmada. Você pode aguardar um pouco mais ou entrar em contato com o mediador para pedir assistência. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2.confReached=Your trade has reached at least one blockchain confirmation.\n\n portfolio.pending.step2_buyer.refTextWarn=Important: when making the payment, leave the \"reason for payment\" field empty. DO NOT put the trade ID or any other text like 'monero', 'XMR', or 'Haveno'. You are free to discuss via trader chat if an alternate \"reason for payment\" would be suitable to you both. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees=If your bank charges you any fees to make the transfer, you are responsible for paying those fees. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.crypto=Transfira com a sua carteira {0} externa\n{1} para o vendedor de XMR.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.cash=Vá ao banco e pague {0} ao vendedor de XMR.\n\n portfolio.pending.step2_buyer.cash.extra=IMPORTANTE:\nApós executar o pagamento, escreva no comprovante de depósito: SEM REEMBOLSO\nEntão rasgue-o em 2 partes, tire uma foto e envie-a para o e-mail do vendedor de XMR. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.moneyGram=Pague {0} ao vendedor de XMR usando MoneyGram.\n\n portfolio.pending.step2_buyer.moneyGram.extra=IMPORTANTE:\nApós ter feito o pagamento, envie o número de autorização e uma foto do comprovante por e-mail para o vendedor de XMR.\nO comprovante deve exibir claramente o nome completo, o país e o estado do vendedor, assim como a quantia. O e-mail do vendedor é: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.westernUnion=Pague {0} ao vendedor de XMR usando Western Union.\n\n portfolio.pending.step2_buyer.westernUnion.extra=IMPORTANTE:\nApós ter feito o pagamento, envie o número de rastreamento (MTCN) e uma foto do comprovante por e-mail para o vendedor de XMR.\nO comprovante deve exibir claramente o nome completo, o país e o estado do vendedor, assim como a quantia. O e-mail do vendedor é: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Envie {0} através de \"US Postal Money Order\" para o vendedor de XMR.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Por favor, entre em contato com o vendedor de XMR através do contato fornecido e combine um encontro para pagá-lo {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Iniciar pagamento usando {0} portfolio.pending.step2_buyer.recipientsAccountData=Recipients {0} portfolio.pending.step2_buyer.amountToTransfer=Quantia a ser transferida portfolio.pending.step2_buyer.sellersAddress=Endereço {0} do vendedor portfolio.pending.step2_buyer.buyerAccount=A sua conta de pagamento a ser usada portfolio.pending.step2_buyer.paymentSent=Pagamento iniciado portfolio.pending.step2_buyer.showEarly=Mostrar os detalhes do pagamento antecipadamente portfolio.pending.step2_buyer.warn=Você ainda não realizou seu pagamento de {0}!\nEssa negociação deve ser completada até {1}. portfolio.pending.step2_buyer.openForDispute=Você ainda não completou o seu pagamento!\nO período máximo para a negociação já passou. Entre em contato com o mediador para pedir assistência. portfolio.pending.step2_buyer.paperReceipt.headline=Você enviou o comprovante de depósito para o vendedor de XMR? portfolio.pending.step2_buyer.paperReceipt.msg=Lembre-se:\nVocê deve escrever no comprovante de depósito: SEM REEMBOLSO\nA seguir, rasgue-o em duas partes, tire uma foto e envie-a para o e-mail do vendedor de XMR. portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Enviar o número de autorização e o comprovante de depósito portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=Você previsa enviar por-email para o vendedor XMR o número de autorização e uma foto com o comprovante de depósito.\nO comprovante deve mostrar claramente o nome completo, o país e o estado do vendedor, assim como a quantia. O e-mail do vendedor é: {0}.\n\nVocê enviou o número de autorização e o contrato ao vendedor? portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=Enviar MTCN e comprovante portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Você precisa enviar o MTCN (número de rastreamento) e uma foto do recibo por e-mail para o vendedor de XMR.\nO recibo deve mostrar claramente o nome completo do vendedor, a cidade, o país e a quantia. O e-mail do vendedor é: {0}.\n\nVocê enviou o MTCN e o contrato para o vendedor? portfolio.pending.step2_buyer.halCashInfo.headline=Enviar código HalCash portfolio.pending.step2_buyer.halCashInfo.msg=Você precisa enviar uma mensagem de texto com o código HalCash, bem como o ID da negociação ({0}) para o vendedor XMR.\nO nº do telefone do vendedor é {1}.\n\nVocê enviou o código para o vendedor? portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Alguns bancos podem verificar o nome do destinatário. Contas de pagamento rápido criadas numa versão antiga da Haveno não fornecem o nome do destinatário, então, por favor, use o chat de negociação pra obtê-lo (caso necessário). portfolio.pending.step2_buyer.confirmStart.headline=Confirme que você iniciou o pagamento portfolio.pending.step2_buyer.confirmStart.msg=Você iniciou o pagamento {0} para o seu parceiro de negociação? portfolio.pending.step2_buyer.confirmStart.yes=Sim, iniciei o pagamento portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=You have not provided proof of payment portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=You have not entered the transaction ID and the transaction key.\n\nBy not providing this data the peer cannot use the auto-confirm feature to release the XMR as soon the XMR has been received.\nBeside that, Haveno requires that the sender of the XMR transaction is able to provide this information to the mediator or arbitrator in case of a dispute.\nSee more details on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=Input is not a 32 byte hexadecimal value portfolio.pending.step2_buyer.confirmStart.warningButton=Ignore and continue anyway portfolio.pending.step2_seller.waitPayment.headline=Aguardar pagamento portfolio.pending.step2_seller.f2fInfo.headline=Informações de contato do comprador portfolio.pending.step2_seller.waitPayment.msg=A transação de depósito tem pelo menos uma confirmação blockchain do protocolo.\nVocê precisa aguardar até que o comprador de XMR inicie o pagamento de {0}. portfolio.pending.step2_seller.warn=O comprador de XMR ainda não fez o pagamento de {0}.\nVocê precisa esperar até que ele inicie o pagamento.\nCaso a negociação não conclua em {1}, o árbitro irá investigar. portfolio.pending.step2_seller.openForDispute=O comprador de XMR ainda não iniciou o pagamento!\nO período máximo permitido para a negociação expirou.\nVocê pode aguardar mais um pouco, dando mais tempo para o seu parceiro de negociação, ou você pode entrar em contato com o mediador para pedir assistência. tradeChat.chatWindowTitle=Abrir janela de conversa para a negociação com ID "{0}" tradeChat.openChat=Abrir janela de conversa tradeChat.rules=Você pode conversar com seu par da negociação para resolver potenciais problemas desta negociação.\nNão é obrigatório responder no chat.\nSe um negociante violar qualquer das regras abaixo, abra uma disputa e reporte o caso ao mediador ou árbitro.\n\nRegras do chat:\n\t● Não envie nenhum link (risco de malware). Você pode enviar a ID de transação e o nome de um explorador de blocos.\n\t● Não envie suas palavras-semente, chaves privadas, senhas ou outras informações sensíveis!\n\t● Não encoraje negociações fora da Haveno (sem segurança).\n\t● Não tente aplicar golpes por meio de qualquer forma de engenharia social.\n\t● Se o par não responder e preferir não se comunicar pelo chat, respeite essa decisão.\n\t● Mantenha o escopo da conversa limitado à negociação. Este chat não é um substituto de aplicativos de mensagens ou local para trolagens.\n\t● Mantenha a conversa amigável e respeitosa. # suppress inspection "UnusedProperty" message.state.UNDEFINED=Indefinido # suppress inspection "UnusedProperty" message.state.SENT=Mensagem enviada # suppress inspection "UnusedProperty" message.state.ARRIVED=A mensagem chegou ao destinário # suppress inspection "UnusedProperty" message.state.STORED_IN_MAILBOX=Mensagem do pagamento enviada mas ainda não recebida pelo par # suppress inspection "UnusedProperty" message.state.ACKNOWLEDGED=O destinário confirmou o recebimento da mensagem # suppress inspection "UnusedProperty" message.state.FAILED=Erro ao enviar a mensagem portfolio.pending.step3_buyer.wait.headline=Aguarde confirmação de pagamento do vendedor de XMR. portfolio.pending.step3_buyer.wait.info=Aguardando o vendedor de XMR confirmar o recebimento do pagamento de {0}. portfolio.pending.step3_buyer.wait.msgStateInfo.label=Status da mensagem de pagamento iniciado portfolio.pending.step3_buyer.warn.part1a=na blockchain {0} portfolio.pending.step3_buyer.warn.part1b=no seu provedor de pagamentos (ex: seu banco) portfolio.pending.step3_buyer.warn.part2=O vendedor de XMR ainda não confirmou o seu pagamento. Por favor, verifique em {0} se o pagamento foi enviado com sucesso. portfolio.pending.step3_buyer.openForDispute=O vendedor de XMR não confirmou o seu pagamento! O período máximo para essa negociação expirou. Você pode aguardar mais um pouco, dando mais tempo para o seu parceiro de negociação, ou você pode pedir a assistência de um mediador. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.part=Seu parceiro de negociação confirmou que iniciou o pagamento de {0}.\n\n portfolio.pending.step3_seller.crypto.explorer=no seu explorador da blockchain {0} preferido portfolio.pending.step3_seller.crypto.wallet=em sua carteira {0} portfolio.pending.step3_seller.crypto={0}Verifique em {1} se a transação para o seu endereço de recebimento\n{2}\njá tem confirmações suficientes na blockchain.\nA quantia do pagamento deve ser {3}\n\nVocê pode copiar e colar seu endereço {4} na janela principal, após fechar esse popup. portfolio.pending.step3_seller.postal={0}Please check if you have received {1} with \"US Postal Money Order\" from the XMR buyer. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.payByMail={0}Please check if you have received {1} with \"Pay by Mail\" from the XMR buyer. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.bank=Your trading partner has confirmed that they have initiated the {0} payment.\n\nPlease go to your online banking web page and check if you have received {1} from the XMR buyer. portfolio.pending.step3_seller.cash=Como o pagamento é realizado através de depósito de dinheiro em espécie, o comprador de XMR obrigatoriamente deve escrever \"SEM REEMBOLSO\" no comprovante de depósito, rasgá-lo em duas partes e enviar uma foto do comprovante para você por e-mail.\n\nPara reduzir a chance de um reembolso (restituição do valor depositado para o comprador), confirme apenas se você tiver recebido o e-mail e tiver certeza de que o comprovante de depósito é autêntico.\nSe você não tiver certeza, {0} portfolio.pending.step3_seller.moneyGram=O comprador deve enviar o Número de Autorização e uma foto do recibo por e-mail.\nO recibo deve mostrar claramente o seu nome completo, país, estado e a quantia. Por favor verifique seu e-mail se recebeu o Número de Autorização.\n\nDepois de fechar esse pop-up, verá o nome e o endereço do comprador do XMR para retirar o dinheiro da MoneyGram.\n\nConfirme apenas o recebimento depois de ter conseguido o dinheiro com sucesso! portfolio.pending.step3_seller.westernUnion=O comprador deve enviar-lhe o MTCN (número de rastreamento) e uma foto do recibo por e-mail.\nO recibo deve mostrar claramente seu nome completo, cidade, país e a quantia Por favor verifique no seu e-mail se você recebeu o MTCN.\n\nDepois de fechar esse pop-up, você verá o nome e endereço do comprador de XMR para receber o dinheiro da Western Union.\n\nConfirme apenas o recebimento depois de ter conseguido o dinheiro com sucesso! portfolio.pending.step3_seller.halCash=O comprador deve-lhe enviar o código HalCash como mensagem de texto. Além disso, você receberá uma mensagem do HalCash com as informações necessárias para sacar o EUR de uma ATM que suporte o HalCash.\n\nDepois de retirar o dinheiro na ATM, confirme aqui o recibo do pagamento! portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon eGift Card by email or by text message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted confirm the payment receipt. portfolio.pending.step3_seller.bankCheck=\n\nVerifique também se o nome de quem envia o pagamento no contrato de negociação é o mesmo que aparece em seu extrato bancário:\nNome do pagante, pelo contrato de negociação: {0}\n\nSe os nomes não forem exatamente iguais, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=não confirme o recebimento do pagamento. Em vez disso, abra uma disputa pressionando \"alt + o\" or \"option + o\".\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=Confirmar recebimento do pagamento portfolio.pending.step3_seller.amountToReceive=Quantia a receber portfolio.pending.step3_seller.yourAddress=Seu endereço {0} portfolio.pending.step3_seller.buyersAddress=Endereço {0} do comprador portfolio.pending.step3_seller.yourAccount=Sua conta de negociação portfolio.pending.step3_seller.xmrTxHash=ID da Transação portfolio.pending.step3_seller.xmrTxKey=Transaction key portfolio.pending.step3_seller.buyersAccount=Buyers account data portfolio.pending.step3_seller.confirmReceipt=Confirmar recebimento do pagamento portfolio.pending.step3_seller.buyerStartedPayment=O comprador de XMR iniciou o pagamento {0}.\n{1} portfolio.pending.step3_seller.buyerStartedPayment.crypto=Verifique as confirmações de transação em sua carteira crypto ou explorador de blockchain e confirme o pagamento quando houver confirmações suficientes. portfolio.pending.step3_seller.buyerStartedPayment.traditional=Verifique em sua conta de negociação (ex: sua conta bancária) e confirme que recebeu o pagamento. portfolio.pending.step3_seller.warn.part1a=na blockchain {0} portfolio.pending.step3_seller.warn.part1b=no seu provedor de pagamentos (ex: banco) portfolio.pending.step3_seller.warn.part2=Você ainda não confirmou o recebimento do pagamento. Por favor, verifique em {0} se você recebeu o pagamento. portfolio.pending.step3_seller.openForDispute=Você ainda não confirmou o recebimento do pagamento!\nO período máximo para a negociação expirou.\nPor favor, confirme o recebimento do pagamento ou entre em contato com o mediador para pedir assistência. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=Você recebeu o pagamento de {0} do seu parceiro de negociação?\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.name=Verifique também se o nome de quem envia o pagamento no contrato de negociação é o mesmo que aparece em seu extrato bancário:\nNome do pagante, pelo contrato de negociação: {0}\n\nSe os nomes não forem exatamente iguais, não confirme o recebimento do pagamento. Em vez disso, abra uma disputa pressionando \"alt + o\" or \"option + o\".\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Assim que você confirmar o recebimento do pagamento, o valor da transação será liberado para o comprador de XMR e o depósito de segurança será devolvido.\n\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Confirmar recebimento do pagamento portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=Sim, eu recebi o pagamento portfolio.pending.step3_seller.onPaymentReceived.signer=IMPORTANTE: Ao confirmar o recebimento do pagamento, você também estará verificando a conta do seu par e a assinando. Como a conta do seu par ainda não foi assinada, você deve segurar a confirmação do pagamento o máximo de tempo possível para reduzir o risco de estorno. portfolio.pending.step5_buyer.groupTitle=Resumo da negociação portfolio.pending.step5_buyer.tradeFee=Taxa de negociação portfolio.pending.step5_buyer.makersMiningFee=Taxa de mineração portfolio.pending.step5_buyer.takersMiningFee=Total em taxas de mineração portfolio.pending.step5_buyer.refunded=Depósito de segurança devolvido portfolio.pending.step5_buyer.withdrawXMR=Retirar seus moneros portfolio.pending.step5_buyer.amount=Quantia a ser retirada portfolio.pending.step5_buyer.withdrawToAddress=Enviar para o endereço portfolio.pending.step5_buyer.moveToHavenoWallet=Keep funds in Haveno wallet portfolio.pending.step5_buyer.withdrawExternal=Retirar para carteira externa portfolio.pending.step5_buyer.alreadyWithdrawn=Seus fundos já foram retirados.\nFavor verifique o histórico de transações. portfolio.pending.step5_buyer.confirmWithdrawal=Confirmar solicitação de retirada portfolio.pending.step5_buyer.amountTooLow=A quantia a ser transferida é inferior à taxa de transação e o valor mínimo de transação (poeira). portfolio.pending.step5_buyer.withdrawalCompleted.headline=Retirada concluída portfolio.pending.step5_buyer.withdrawalCompleted.msg=Suas negociações concluídas estão salvas em \"Portfolio/Histórico\".\nVocê pode rever todas as suas transações monero em \"Fundos/Transações\" portfolio.pending.step5_buyer.bought=Você comprou portfolio.pending.step5_buyer.paid=Você pagou portfolio.pending.step5_seller.sold=Você vendeu portfolio.pending.step5_seller.received=Você recebeu tradeFeedbackWindow.title=Parabéns por concluir a negociação tradeFeedbackWindow.msg.part1=Gostaríamos de saber como está sendo a sua experiência. Ela nos ajuda a corrigir e aperfeiçoar o software. Caso queira nos deixar a sua opinião, preencha nosso pequeno questionário (não é necessário registrar-se) em: tradeFeedbackWindow.msg.part2=Se você tem dúvidas ou está tendo problemas, por favor entre em contato com outros usuários e contribuidores através do fórum Haveno em: tradeFeedbackWindow.msg.part3=Obrigado por usar Haveno! portfolio.pending.role=Minha função portfolio.pending.tradeInformation=Informação da negociação portfolio.pending.remainingTime=Tempo restante portfolio.pending.remainingTimeDetail={0} (até {1}) portfolio.pending.remainingTimeDetail.startsAfter=Começa após {0} confirmações portfolio.pending.tradePeriodInfo=Após {0} confirmações, o período de negociação começa. Com base no método de pagamento utilizado, é aplicado um período máximo de negociação diferente. portfolio.pending.tradePeriodWarning=Se o período expirar, os dois negociantes poderão abrir uma disputa. portfolio.pending.tradeNotCompleted=Negociação não completada a tempo (até {0}) portfolio.pending.tradeProcess=Processo de negociação portfolio.pending.openAgainDispute.msg=If you are not sure that the message to the mediator or arbitrator arrived (e.g. if you did not get a response after 1 day) feel free to open a dispute again with Cmd/Ctrl+o. You can also ask for additional help on the Haveno forum at [HYPERLINK:https://haveno.community]. portfolio.pending.openAgainDispute.button=Abrir disputa novamente portfolio.pending.openSupportTicket.headline=Abrir ticket de suporte portfolio.pending.openSupportTicket.msg=Por favor, apenas use esta função em casos de emergência quando não houver um botão para "Abrir ticket de suporte" ou "Abrir disputa".\n\nQuando você abrir um ticket de suporte, a negociação será interrompida e tratada por um mediador ou árbitro. portfolio.pending.timeLockNotOver=Você precisa aguardar até ≈{0} (mais {1} blocos) para conseguir abrir uma disputa com um árbitro. portfolio.pending.error.depositTxNull=A transação de depósito está ausente. Você não pode abrir uma disputa sem uma transação de depósito válida. Por favor, vá até "Configurações/Informações da rede" e ressincronize o arquivo SPV.\n\nPara mais informações, por favor acesse o canal #support do time da Haveno na Keybase. portfolio.pending.mediationResult.error.depositTxNull=The deposit transaction is null. You can move the trade to failed trades. portfolio.pending.mediationResult.error.delayedPayoutTxNull=The delayed payout transaction is null. You can move the trade to failed trades. portfolio.pending.error.depositTxNotConfirmed=A transação de depósito não está confirmada. Você não pode abrir uma disputa com uma transação não-confirmada. Por favor, espere até que a transação seja confirmada ou vá até "Configurações/Informações da rede" e ressincronize o arquivo SPV.\n\nPara mais informações, por favor acesse o canal #support do time da Haveno na Keybase. portfolio.pending.support.headline.getHelp=Precisa de ajuda? portfolio.pending.support.text.getHelp=Caso tenha problemas, você pode tentar contactar o par de negociação no chat ou solicitar ajuda na comunidade da Haveno em https://haveno.community. Se o problema persistir, você pode solicitar ajuda adicional a um mediador. portfolio.pending.support.button.getHelp=Abrir Chat de Negociante portfolio.pending.support.headline.halfPeriodOver=Verifique o pagamento portfolio.pending.support.headline.periodOver=O período de negociação acabou portfolio.pending.support.headline.depositTxMissing=Transação de depósito ausente portfolio.pending.support.depositTxMissing=Está faltando uma transação de depósito para esta negociação. Abra um ticket de suporte para contatar um árbitro para assistência. portfolio.pending.mediationRequested=Mediação requerida portfolio.pending.refundRequested=Reembolso requerido portfolio.pending.openSupport=Abrir ticket de suporte portfolio.pending.supportTicketOpened=Ticket de suporte aberto portfolio.pending.communicateWithArbitrator=Por favor, vá até a seção \"Suporte\" e entre em contato com o árbitro. portfolio.pending.communicateWithMediator=Por favor, entre em contato com o mediador na seção \"Suporte\". portfolio.pending.disputeOpenedByUser=Você já abriu uma disputa.\n{0} portfolio.pending.disputeOpenedByPeer=Seu parceiro de negociação abriu uma disputa\n{0} portfolio.pending.noReceiverAddressDefined=Nenhum endereço de recebimento definido portfolio.pending.mediationResult.headline=Sugestão de pagamento da mediação portfolio.pending.mediationResult.info.noneAccepted=Completar a negociação aceitando a sugestão do mediador para o pagamento portfolio.pending.mediationResult.info.selfAccepted=Você aceitou a sugestão do mediador. Aguardando o parceiro de negociação aceitar também. portfolio.pending.mediationResult.info.peerAccepted=O seu parceiro de negociação aceitou a sugestão do mediador. Você também aceita portfolio.pending.mediationResult.button=Ver solução proposta portfolio.pending.mediationResult.popup.headline=Resultado da mediação para a negociação com ID: {0} portfolio.pending.mediationResult.popup.headline.peerAccepted=O seu parceiro de negociação aceitou a sugestão do mediador para a negociação {0} portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader's security deposit) as compensation for their work. Both traders agreeing to the mediator's suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator's suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=Rejeitar e solicitar arbitramento portfolio.pending.mediationResult.popup.alreadyAccepted=Você já aceitou portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. portfolio.pending.failedTrade.missingDepositTx=Uma transação de depósito está faltando.\n\nEssa transação é necessária para concluir a negociação. Por favor, certifique-se de que sua carteira esteja totalmente sincronizada com a blockchain do Monero.\n\nVocê pode mover esta negociação para a seção "Negociações Falhas" para desativá-la. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Move trade to failed trades portfolio.pending.failedTrade.warningIcon.tooltip=Click to open details about the issues of this trade portfolio.failed.revertToPending.popup=Do you want to move this trade to open trades? portfolio.failed.revertToPending=Move trade to open trades portfolio.closed.completed=Concluído portfolio.closed.ticketClosed=Arbitrado portfolio.closed.mediationTicketClosed=Mediado portfolio.closed.canceled=Cancelado portfolio.failed.Failed=Falha portfolio.failed.unfail=Before proceeding, make sure you have a backup of your data directory!\nDo you want to move this trade back to open trades?\nThis is a way to unlock funds stuck in a failed trade. portfolio.failed.cantUnfail=This trade cannot be moved back to open trades at the moment. \nTry again after completion of trade(s) {0} portfolio.failed.depositTxNull=The trade cannot be reverted to a open trade. Deposit transaction is null. portfolio.failed.delayedPayoutTxNull=The trade cannot be reverted to a open trade. Delayed payout transaction is null. #################################################################### # Funds #################################################################### funds.tab.deposit=Receber fundos funds.tab.withdrawal=Enviar fundos funds.tab.reserved=Fundos reservados funds.tab.locked=Fundos travados funds.tab.transactions=Transações funds.deposit.unused=Não utilizado funds.deposit.usedInTx=Utilizado em {0} transação(ões) funds.deposit.fundHavenoWallet=Financiar carteira Haveno funds.deposit.noAddresses=Nenhum endereço de depósito foi gerado ainda funds.deposit.fundWallet=Financiar sua carteira funds.deposit.withdrawFromWallet=Enviar fundos da carteira funds.deposit.amount=Quantia em XMR (opcional) funds.deposit.generateAddress=Gerar um endereço novo funds.deposit.generateAddressSegwit=Native segwit format (Bech32) funds.deposit.selectUnused=Selecione um endereço não utilizado da tabela acima ao invés de gerar um novo. funds.withdrawal.arbitrationFee=Taxa de arbitragem funds.withdrawal.inputs=Escolha dos inputs funds.withdrawal.useAllInputs=Usar todos inputs disponíveis funds.withdrawal.useCustomInputs=Usar inputs personalizados funds.withdrawal.receiverAmount=Quantia do destinatário funds.withdrawal.senderAmount=Quantia do remetente funds.withdrawal.feeExcluded=Quantia excluindo a taxa de mineração funds.withdrawal.feeIncluded=Quantia incluindo a taxa de mineração funds.withdrawal.fromLabel=Retirar do endereço funds.withdrawal.toLabel=Enviar para o endereço funds.withdrawal.memoLabel=Withdrawal memo funds.withdrawal.memo=Optionally fill memo funds.withdrawal.withdrawButton=Retirar selecionados funds.withdrawal.noFundsAvailable=Não há fundos disponíveis para retirada funds.withdrawal.confirmWithdrawalRequest=Confirmar solicitação de retirada funds.withdrawal.withdrawMultipleAddresses=Retirar de múltiplos endereços ({0}) funds.withdrawal.withdrawMultipleAddresses.tooltip=Retirar de múltiplos endereços:\n{0} funds.withdrawal.notEnoughFunds=Você não possui saldo suficiente em sua carteira. funds.withdrawal.selectAddress=Selecione um endereço de origem da tabela funds.withdrawal.setAmount=Defina quantia a ser retirada funds.withdrawal.fillDestAddress=Preencha seu endereço de destino funds.withdrawal.warn.noSourceAddressSelected=Você precisa selecionar um endereço de origem na tabela acima. funds.withdrawal.warn.amountExceeds=Você não tem saldo suficiente no endereço selecionado.\nTente selecionar múltiplos endereços na tabela acima ou modificar a opção para incluir a taxa do minerador. funds.reserved.noFunds=Não há fundos reservados em ofertas abertas funds.reserved.reserved=Reservado na carteira local para oferta com ID: {0} funds.locked.noFunds=Não há fundos travados em negociações funds.locked.locked=Travado em multisig para negociação com ID: {0} funds.tx.direction.sentTo=Enviado para: funds.tx.direction.receivedWith=Recebido com: funds.tx.direction.genesisTx=Da transação gênese: funds.tx.createOfferFee=Taxa de oferta e transação: {0} funds.tx.takeOfferFee=Taxa de aceitação e transação: {0} funds.tx.multiSigDeposit=Depósito Multisig: {0} funds.tx.multiSigPayout=Pagamento Multisig: {0} funds.tx.disputePayout=Pagamento de disputa: {0} funds.tx.disputeLost=Caso com disputa perdida: {0} funds.tx.collateralForRefund=Colateral de reembolso: {0} funds.tx.timeLockedPayoutTx=transação de pagamento com trava temporal: {0} funds.tx.refund=Reembolso de árbitro: {0} funds.tx.unknown=Razão desconhecida: {0} funds.tx.noFundsFromDispute=Nenhum reembolso de disputa funds.tx.receivedFunds=Fundos recebidos funds.tx.withdrawnFromWallet=Retirado da carteira funds.tx.memo=Memo funds.tx.noTxAvailable=Sem transações disponíveis funds.tx.revert=Reverter funds.tx.txSent=Transação enviada com sucesso para um novo endereço em sua carteira Haveno local. funds.tx.direction.self=Enviar para você mesmo funds.tx.dustAttackTx=Poeira recebida funds.tx.dustAttackTx.popup=Esta transação está enviando uma quantia muito pequena de XMR para a sua carteira e pode ser uma tentativa das empresas de análise da blockchain para espionar a sua carteira.\n\nSe você usar esse output em uma transação eles decobrirão que você provavelmente também é o proprietário de outros endereços (mistura de moedas).\n\nPara proteger sua privacidade a carteira Haveno ignora tais outputs de poeira para fins de consumo e na tela de saldo. Você pode definir a quantia limite a partir da qual um output é considerado poeira nas configurações." #################################################################### # Support #################################################################### support.tab.mediation.support=Mediação support.tab.arbitration.support=Arbitragem support.tab.legacyArbitration.support=Arbitração antiga support.tab.ArbitratorsSupportTickets=Tickets de {0} support.sigCheck.button=Check signature support.sigCheck.popup.header=Verify dispute result signature support.sigCheck.popup.msg.label=Summary message support.sigCheck.popup.msg.prompt=Copy & paste summary message from dispute support.sigCheck.popup.result=Validation result support.sigCheck.popup.success=Signature is valid support.sigCheck.popup.failed=Signature verification failed support.sigCheck.popup.invalidFormat=Message is not of expected format. Copy & paste summary message from dispute. support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? support.reOpenButton.label=Re-open support.sendNotificationButton.label=Notificação privada support.reportButton.label=Report support.fullReportButton.label=All disputes support.noTickets=Não há tickets de suporte abertos support.sendingMessage=Enviando mensagem... support.receiverNotOnline=O recipiente não está online. A mensagem será salva na caixa postal dele. support.sendMessageError=Erro ao enviar a mensagem: {0} support.receiverNotKnown=Receiver not known support.wrongVersion=A oferta nessa disputa foi criada com uma versão anterior do Haveno.\nVocê não pode fechar aquela disputa com a sua versão atual do programa.\n\nFavor utilizar uma versão mais velha com protocolo versão {0} support.openFile=Abrir arquivo para anexar (tamanho máximo: {0} kb) support.attachmentTooLarge=O tamanho total de seus arquivos anexados é {0} kb e excede o máximo permitido para mensagens de {1} kb. support.maxSize=O tamanho máximo permitido é {0} kB. support.attachment=Anexo support.tooManyAttachments=Você não pode enviar mais de 3 anexos em uma mensagem. support.save=Salvar arquivo para o disco support.messages=Mensagens support.input.prompt=Insira sua mensagem... support.send=Enviar support.addAttachments=Adicionar arquivos support.closeTicket=Fechar ticket support.attachments=Anexos: support.savedInMailbox=Mensagem guardada na caixa de correio do destinatário. support.arrived=Mensagem chegou no destinatário support.acknowledged=O destinatário confirmou a chegada da mensagem support.error=O destinatário não pôde processar a mensagem. Erro: {0} support.buyerAddress=Endereço do comprador de XMR support.sellerAddress=Endereço do vendedor de XMR support.role=Função support.agent=Support agent support.state=Estado support.chat=Chat support.preparing=Preparando support.requested=Solicitado support.closed=Fechado support.open=Aberto support.process=Process support.buyerMaker=Comprador de XMR / Ofetante support.sellerMaker=Vendedor de XMR / Ofertante support.buyerTaker=Comprador de XMR / Aceitador da oferta support.sellerTaker=Vendedor de XMR / Aceitador da oferta support.backgroundInfo=Haveno não é uma empresa, portanto, trata as disputas de forma diferente.\n\nOs negociantes podem se comunicar dentro do aplicativo por meio de um chat seguro na tela de negociações em aberto para tentar resolver disputas por conta própria. Se isso não for suficiente, um árbitro avaliará a situação e decidirá o pagamento dos fundos da negociação. support.initialInfo=Por favor, entre a descrição do seu problema no campo de texto abaixo. Informe o máximo de informações que puder para agilizar a resolução da disputa.\n\nSegue uma lista com as informações que você deve fornecer:\n\t● Se você está comprando XMR: Você fez a transferência do dinheiro ou crypto? Caso afirmativo, você clicou no botão 'pagamento iniciado' no aplicativo?\n\t● Se você está vendendo XMR: Você recebeu o pagamento em dinheiro ou em crypto? Caso afirmativo, você clicou no botão 'pagamento recebido' no aplicativo?\n\t● Qual versão da Haveno você está usando?\n\t● Qual sistema operacional você está usando?\n\t● Se seu problema é com falhas em transações, por favor considere usar um novo diretório de dados.\n\t Às vezes, o diretório de dados pode ficar corrompido, causando bugs estranhos.\n\t Veja mais: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nPor favor, familiarize-se com as regras básicas do processo de disputa:\n\t● Você precisa responder às solicitações de {0}' dentro do prazo de 2 dias.\n\t● Mediadores respondem dentro de 2 dias. Ábitros respondem dentro de 5 dias úteis.\n\t● O período máximo para uma disputa é de 14 dias.\n\t● Você deve cooperar com o {1} e providenciar as informações requisitadas para comprovar o seu caso.\n\t● Você aceitou as regras estipuladas nos termos de acordo do usuário que foi exibido na primeira vez em que você iniciou o aplicativo. \nVocê pode saber mais sobre o processo de disputa em: {2} support.systemMsg=Mensagem do sistema: {0} support.youOpenedTicket=Você abriu um pedido de suporte.\n\n{0}\n\nHaveno versão: {1} support.youOpenedDispute=Você abriu um pedido para uma disputa.\n\n{0}\n\nHaveno versão: {1} support.youOpenedDisputeForMediation=Você solicitou mediação.\n\n{0}\n\nVersão do Haveno: {1} support.peerOpenedTicket=O seu parceiro de negociação solicitou suporte devido a problemas técnicos.\n\n{0}\n\nVersão do Haveno: {1} support.peerOpenedDispute=O seu parceiro de negociação solicitou uma disputa.\n\n{0}\n\nVersão do Haveno: {1} support.peerOpenedDisputeForMediation=O seu parceiro de negociação solicitou mediação.\n\n{0}\n\nVersão do Haveno: {1} support.mediatorsDisputeSummary=System message: Mediator's dispute summary:\n{0} support.mediatorsAddress=Endereço do nó do mediador: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. support.info.disputeReOpened=Dispute ticket has been re-opened. #################################################################### # Settings #################################################################### settings.tab.preferences=Preferências settings.tab.network=Informações da rede settings.tab.about=Sobre setting.preferences.general=Preferências gerais setting.preferences.explorer=Monero Explorer setting.preferences.deviation=Desvio máx. do preço do mercado setting.preferences.avoidStandbyMode=Impedir modo de economia de energia setting.preferences.useSoundForNotifications=Reproduzir sons para notificações setting.preferences.autoConfirmXMR=XMR auto-confirm setting.preferences.autoConfirmEnabled=Enabled setting.preferences.autoConfirmRequiredConfirmations=Required confirmations setting.preferences.autoConfirmMaxTradeSize=Max. trade amount (XMR) setting.preferences.autoConfirmServiceAddresses=Monero Explorer URLs (uses Tor, except for localhost, LAN IP addresses, and *.local hostnames) setting.preferences.deviationToLarge=Valores acima de {0}% não são permitidos. setting.preferences.txFee=Withdrawal transaction fee (satoshis/vbyte) setting.preferences.useCustomValue=Usar valor personalizado setting.preferences.txFeeMin=Transaction fee must be at least {0} satoshis/vbyte setting.preferences.txFeeTooLarge=Your input is above any reasonable value (>5000 satoshis/vbyte). Transaction fee is usually in the range of 50-400 satoshis/vbyte. setting.preferences.ignorePeers=Pares ignorados [endereço onion:porta] setting.preferences.ignoreDustThreshold=Mín. valor de output não-poeira setting.preferences.currenciesInList=Moedas na lista de preços de mercado setting.preferences.prefCurrency=Moeda de preferência setting.preferences.displayTraditional=Exibir moedas nacionais setting.preferences.noTraditional=Não há moedas nacionais selecionadas setting.preferences.cannotRemovePrefCurrency=Você não pode remover a moeda preferencial de exibição selecionada setting.preferences.displayCryptos=Exibir cryptos setting.preferences.noCryptos=Não há cryptos selecionadas setting.preferences.addTraditional=Adicionar moeda nacional setting.preferences.addCrypto=Adicionar crypto setting.preferences.displayOptions=Opções de exibição setting.preferences.showOwnOffers=Exibir minhas ofertas no livro de ofertas setting.preferences.useAnimations=Usar animações setting.preferences.useDarkMode=Usar modo escuro setting.preferences.useLightMode=Usar modo claro setting.preferences.sortWithNumOffers=Ordenar pelo nº de ofertas/negociações setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API setting.preferences.notifyOnPreRelease=Receive pre-release notifications setting.preferences.resetAllFlags=Esquecer marcações \"Não exibir novamente\" settings.preferences.languageChange=Aplicar a mudança de idioma em todas as telas requer uma reinicialização. settings.preferences.supportLanguageWarning=Em caso de disputa, por favor note que a arbitração é feita em {0}. settings.preferences.editCustomExplorer.headline=Explorer Settings settings.preferences.editCustomExplorer.description=Choose a system defined explorer from the list on the left, and/or customize to suit your own preferences. settings.preferences.editCustomExplorer.available=Available explorers settings.preferences.editCustomExplorer.chosen=Chosen explorer settings settings.preferences.editCustomExplorer.name=Nome settings.preferences.editCustomExplorer.txUrl=Transaction URL settings.preferences.editCustomExplorer.addressUrl=Address URL setting.info.headline=Nova funcionalidade de privacidade de dados settings.preferences.sensitiveDataRemoval.msg=Para proteger a privacidade sua e de outros traders, o Haveno pretende remover dados sensíveis de negociações antigas. Isso é particularmente importante para negociações com moedas fiduciárias que podem incluir detalhes de conta bancária.\n\nÉ recomendado definir o menor valor possível, por exemplo, 60 dias. Isso significa que negociações com mais de 60 dias terão os dados sensíveis removidos, desde que estejam concluídas. Negociações concluídas podem ser encontradas na aba Portfólio / Histórico. settings.net.xmrHeader=Rede Monero settings.net.p2pHeader=Rede Haveno settings.net.onionAddressLabel=Meu endereço onion settings.net.xmrNodesLabel=Usar nodos personalizados do Monero settings.net.moneroPeersLabel=Pares conectados settings.net.connection=Conexão settings.net.connected=Conectado settings.net.useTorForXmrJLabel=Usar Tor na rede Monero settings.net.moneroNodesLabel=Conexão a nodos do Monero settings.net.useProvidedNodesRadio=Usar nodos do Monero Core fornecidos settings.net.usePublicNodesRadio=Usar rede pública do Monero settings.net.useCustomNodesRadio=Usar nodos personalizados do Monero Core settings.net.warn.usePublicNodes=If you use public Monero nodes, you are subject to any risk of using untrusted remote nodes.\n\nPlease read more details at [HYPERLINK:https://www.getmonero.org/resources/moneropedia/remote-node.html].\n\nAre you sure you want to use public nodes? settings.net.warn.usePublicNodes.useProvided=Não, usar os nodos fornecidos settings.net.warn.usePublicNodes.usePublic=Sim, usar rede pública settings.net.warn.useCustomNodes.B2XWarning=Certifique-se de que o seu nodo Monero é um nodo Monero Core confiável!\n\nAo se conectar a nodos que não estão seguindo as regras de consenso do Monero Core, você pode corromper a sua carteira e causar problemas no processo de negociação.\n\nOs usuários que se conectam a nodos que violam as regras de consenso são responsáveis pelos danos que forem criados por isso. As disputas causadas por esse motivo serão decididas a favor do outro negociante. Nenhum suporte técnico será fornecido para os usuários que ignorarem esse aviso e os mecanismos de proteção! settings.net.warn.invalidXmrConfig=A conexão com a rede Monero falhou porque suas configurações são inválidas.\n\nSuas configurações foram resetadas para utilizar os nós fornecidos da rede Monero. É necessário reiniciar o aplicativo. settings.net.localhostXmrNodeInfo=Informações básicas: Haveno busca por um nó Monero local na inicialização. Caso encontre, Haveno irá comunicar com a rede Monero exclusivamente através deste nó. settings.net.p2PPeersLabel=Pares conectados settings.net.onionAddressColumn=Endereço onion settings.net.creationDateColumn=Estabelecida settings.net.connectionTypeColumn=Entrada/Saída settings.net.sentDataLabel=Sent data statistics settings.net.receivedDataLabel=Received data statistics settings.net.chainHeightLabel=Latest XMR block height settings.net.roundTripTimeColumn=Ping settings.net.sentBytesColumn=Enviado settings.net.receivedBytesColumn=Recebido settings.net.peerTypeColumn=Tipo settings.net.openTorSettingsButton=Abrir configurações do Tor settings.net.versionColumn=Versão settings.net.subVersionColumn=Subversão settings.net.heightColumn=Altura settings.net.needRestart=Você precisa reiniciar o programa para aplicar esta alteração.\nDeseja fazer isso agora? settings.net.notKnownYet=Ainda desconhecido... settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec settings.net.chainHeight=Monero Peers chain height: {0} settings.net.ips=[Endeço IP:porta | nome do host:porta | endereço onion:porta] (seperados por vírgulas). A porta pode ser omitida quando a porta padrão (8333) for usada. settings.net.seedNode=Nó semente settings.net.directPeer=Par (direto) settings.net.initialDataExchange={0} [Bootstrapping] settings.net.peer=Par settings.net.inbound=entrada settings.net.outbound=saída setting.about.aboutHaveno=Sobre Haveno setting.about.about=Haveno é um software de código aberto que facilita a troca de Monero por moedas nacionais (e outras criptomoedas) através de uma rede ponto-a-ponto descentralizada, protegendo a privacidade dos usuários. Descubra mais sobre o Haveno no site do projeto. setting.about.web=Site do Haveno setting.about.code=Código fonte setting.about.agpl=Licença AGPL setting.about.support=Suporte Haveno setting.about.def=Haveno não é uma empresa — é um projeto aberto à participação da comunidade. Se você tem interesse em participar do projeto ou apoiá-lo, visite os links abaixo. setting.about.contribute=Contribuir setting.about.providers=Provedores de dados setting.about.apisWithFee=Haveno uses Haveno Price Indices for Fiat and Crypto market prices, and Haveno Mempool Nodes for mining fee estimation. setting.about.apis=Haveno uses Haveno Price Indices for Fiat and Crypto market prices. setting.about.pricesProvided=Preços de mercado fornecidos por setting.about.feeEstimation.label=Estimativa da taxa de mineração fornecida por setting.about.versionDetails=Detalhes da versão setting.about.version=Versão do programa setting.about.subsystems.label=Versões dos subsistemas setting.about.subsystems.val=Versão da rede: {0}; Versão de mensagens P2P: {1}; Versão do banco de dados local: {2}; Versão do protocolo de negociação: {3} setting.about.shortcuts=atalhos setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' ou 'alt + {0}' ou 'cmd + {0}' setting.about.shortcuts.menuNav=Navegar para o menu principal setting.about.shortcuts.menuNav.value=Para ir ao menu principal, pressione: "ctr" ou "alt" ou "cmd" com um botão numérico de 1 a 9 setting.about.shortcuts.close=Fechar Haveno setting.about.shortcuts.close.value='Ctrl + {0}' ou 'cmd + {0}' ou 'Ctrl + {1}' ou 'cmd + {1}' setting.about.shortcuts.closePopup=Fechar popup ou janela de diálogo setting.about.shortcuts.closePopup.value=botão "Esc" setting.about.shortcuts.chatSendMsg=Enviar mensagem de chat ao negociador setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' ou 'alt + ENTER' ou 'cmd + ENTER' setting.about.shortcuts.openDispute=Abrir disputa setting.about.shortcuts.openDispute.value=Selecione negociação pendente e clique: {0} setting.about.shortcuts.walletDetails=Abrir janela de detalhes da carteira setting.about.shortcuts.openEmergencyXmrWalletTool=Abrir ferramenta de emergência da carteira XMR setting.about.shortcuts.showTorLogs=Ativar registro de logs para mensagens Tor de níveis entre DEBUG e WARN setting.about.shortcuts.manualPayoutTxWindow=Abrir janela de pagamento manual do multisig 2-de-2 da transação de depósito setting.about.shortcuts.removeStuckTrade=Open popup to move failed trade to open trades tab again setting.about.shortcuts.removeStuckTrade.value=Select failed trade and press: {0} setting.about.shortcuts.registerArbitrator=Registrar árbitro (apenas mediador/árbitro) setting.about.shortcuts.registerArbitrator.value=Navegue até à conta e pressione: {0} setting.about.shortcuts.registerMediator=Registrar mediador (apenas mediador/árbitro) setting.about.shortcuts.registerMediator.value=Navegue até à conta e pressione: {0} setting.about.shortcuts.openSignPaymentAccountsWindow=Abrir janela para assinar idade da conta (apenas para árbitros legados) setting.about.shortcuts.openSignPaymentAccountsWindow.value=Navegue até a tela de árbitro legado e pressione: {0} setting.about.shortcuts.sendAlertMsg=Enviar alerta ou atualizar mensagem (atividade privilegiada) setting.about.shortcuts.sendFilter=Definir Filtro (atividade privilegiada) setting.about.shortcuts.sendPrivateNotification=Enviar notificação privada ao par (atividade privilegiada) setting.about.shortcuts.sendPrivateNotification.value=Open peer info at avatar and press: {0} setting.info.headline=New XMR auto-confirm Feature setting.info.msg=When selling XMR for XMR you can use the auto-confirm feature to verify that the correct amount of XMR was sent to your wallet so that Haveno can automatically mark the trade as complete, making trades quicker for everyone.\n\nAuto-confirm checks the XMR transaction on at least 2 XMR explorer nodes using the private transaction key provided by the XMR sender. By default, Haveno uses explorer nodes run by Haveno contributors, but we recommend running your own XMR explorer node for maximum privacy and security.\n\nYou can also set the maximum amount of XMR per trade to auto-confirm as well as the number of required confirmations here in Settings.\n\nSee more details (including how to set up your own explorer node) on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades] #################################################################### # Account #################################################################### account.tab.mediatorRegistration=Registro de mediador account.tab.refundAgentRegistration=Registro de agente de reembolsos account.tab.signing=Signing account.info.headline=Bem vindo à sua conta Haveno account.info.msg=Aqui você pode adicionar contas de negociação para moedas nacionais & cryptos e criar um backup da sua carteira e dados da sua conta.\n\nUma nova carteira Monero foi criada na primeira vez em que você iniciou a Haveno.\nNós encorajamos fortemente que você anote as palavras semente da sua carteira Monero (veja a aba no topo) e considere adicionar uma senha antes de depositar fundos. Depósitos e retiradas de Monero são gerenciados na seção "Fundos".\n\nNota de privacidade & segurança: visto que a Haveno é uma exchange decentralizada, todos os seus dados são mantidos no seu computador. Não existem servidores, então não temos acesso às suas informações pessoais, seus fundos ou até mesmo ao seu endereço IP. Dados como número de conta bancária, endereços de Monero & crypto, etc apenas são compartilhados com seu parceiro de negociação para completar as negociações iniciadas por você (em caso de disputa, o mediador ou árbitro verá as mesmas informações que seu parceiro de negociação). account.menu.paymentAccount=Contas de moedas nacionais account.menu.altCoinsAccountView=Contas de cryptos account.menu.password=Senha da carteira account.menu.seedWords=Semente da carteira account.menu.walletInfo=Wallet info account.menu.backup=Backup account.menu.notifications=Notificações account.menu.walletInfo.balance.headLine=Wallet balances account.menu.walletInfo.balance.info=This shows the internal wallet balance including unconfirmed transactions.\nFor XMR, the internal wallet balance shown below should match the sum of the 'Available' and 'Reserved' balances shown in the top right of this window. account.menu.walletInfo.xpub.headLine=Watch keys (xpub keys) account.menu.walletInfo.walletSelector={0} {1} wallet account.menu.walletInfo.path.headLine=HD keychain paths account.menu.walletInfo.path.info=If you import seed words into another wallet (like Electrum), you'll need to define the path. This should only be done in emergency cases when you lose access to the Haveno wallet and data directory.\nKeep in mind that spending funds from a non-Haveno wallet can bungle the internal Haveno data structures associated with the wallet data, which can lead to failed trades.\n\nNEVER send BSQ from a non-Haveno wallet, as it will probably lead to an invalid BSQ transaction and losing your BSQ. account.menu.walletInfo.openDetails=Show raw wallet details and private keys ## TODO should we rename the following to a gereric name? account.arbitratorRegistration.pubKey=Chave pública account.arbitratorRegistration.register=Registro account.arbitratorRegistration.registration=Registro de {0} account.arbitratorRegistration.revoke=Revogar account.arbitratorRegistration.info.msg=Por favor, note que você precisa estar disponível por 15 dias após a revogação visto que podem existir negociações em que você é usado como {0}. O período máximo de negociação é de 8 dias e o processo de disputa pode durar até 7 dias. account.arbitratorRegistration.warn.min1Language=Você precisa escolher pelo menos 1 idioma.\nNós adicionamos o idioma padrão para você. account.arbitratorRegistration.removedSuccess=O seu registro foi removido com sucesso da rede Haveno. account.arbitratorRegistration.removedFailed=Não foi possível remover o registro.{0} account.arbitratorRegistration.registerSuccess=Você se registrou com sucesso na rede Haveno. account.arbitratorRegistration.registerFailed=Não foi possível completar o registro.{0} account.crypto.yourCryptoAccounts=Suas contas de cryptos account.crypto.popup.wallet.msg=Por favor, certifique-se de seguir os requisitos para uso de carteiras {0} como descritos na página {1}.\nUsar carteiras de exchanges centralizadas onde (a) você não tem controle das suas chaves privadas ou (b) não se usa um software de carteira compatível é arriscado: você pode perder seus fundos negociados!\nO mediador ou árbitro não é um especialista em {2} e não pode ajudar em tais casos. account.crypto.popup.wallet.confirm=Eu entendo e confirmo que sei qual carteira preciso usar. # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=Trading UPX on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending UPX, you need to use either the official uPlexa GUI wallet or uPlexa CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\nuplexa-wallet-cli (use the command get_tx_key)\nuplexa-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The UPX sender is responsible for providing verification of the UPX transfer to the arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit uPlexa discord channel (https://discord.gg/vhdNSrV) or the uPlexa Telegram Chat (https://t.me/uplexaOfficial) to find more information. # suppress inspection "UnusedProperty" account.crypto.popup.arq.msg=Trading ARQ on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending ARQ, you need to use either the official ArQmA GUI wallet or ArQmA CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\narqma-wallet-cli (use the command get_tx_key)\narqma-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the mediator or arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The ARQ sender is responsible for providing verification of the ARQ transfer to the mediator or arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit ArQmA discord channel (https://discord.gg/s9BQpJT) or the ArQmA forum (https://labs.arqma.com) to find more information. # suppress inspection "UnusedProperty" account.crypto.popup.xmr.msg=Trading XMR on Haveno requires that you understand the following requirement.\n\nIf selling XMR, you must be able to provide the following information to a mediator or arbitrator in case of a dispute:\n- the transaction key (Tx Key, Tx Secret Key or Tx Private Key)\n- the transaction ID (Tx ID or Tx Hash)\n- the destination address (recipient's address)\n\nSee the wiki for details on where to find this information on popular Monero wallets [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Proving_payments].\nFailure to provide the required transaction data will result in losing disputes.\n\nAlso note that Haveno now offers automatic confirming for XMR transactions to make trades quicker, but you need to enable it in Settings.\n\nSee the wiki for more information about the auto-confirm feature: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. # suppress inspection "UnusedProperty" account.crypto.popup.msr.msg=Negociar MSR Haveno requer que você entenda e cumpra os seguintes requisitos:\n\nPara enviar MSR, você deve usar uma destas: carteira oficial GUI Masari, carteira CLI Masari com a opção store-tx-info habilitada (habilitada por padrão) ou a carteira web Masari (https://wallet.getmasari.org). Por favor, certifique-se que você tenha acesso à chave da transação pois esta seria necessária em caso de uma disputa.\nmasari-wallet-cli (use o comando get_tx_key)\nmasari-wallet-gui (vá até a aba histórico e clique no botão (P) para prova de pagamento)\n\nCarteira Web Masari (vá até Conta -> histórico de transações e veja os detalhes da sua transação enviada.)\n\nVerificação pode ser feita dentro da carteira.\nmasari-wallet-cli : usando um comando (check_tx_key).\nmasari-wallet-gui : na página Avançado > Comprovar/Verificar.\nVerificação pode ser feita via explorador de blocos.\nAbra o explorador de blocos (https://explorer.getmasari.org) e use a barra de busca para encontrar o hash da sua transação.\nAssim que a transação for encontrada, role até o final da seção 'Comprovar envio' e preencha os detalhes conforme necessário.\nVocê precisa fornecer os seguintes dados ao mediador ou árbitro em caso de uma disputa:\n- Chave privada da transação\n- Hash da transação\n- Endereço público do destinatário\n\nA impossibilidade de fornecer as informações acima ou uso de uma carteira incompatível resultará na perda do caso de disputa. Em caso de uma disputa, o remetente de MSR é responsável por providenciar, ao mediador ou árbitro, a verificação do envio de MSR.\n\nNão é necessário um ID de pagamento, apenas o endereço público convencional.\nCaso tenha dúvidas sobre este processo, solicite ajuda no Discord oficial Masari (https://discord.gg/sMCwMqs). # suppress inspection "UnusedProperty" account.crypto.popup.blur.msg=Trading BLUR on Haveno requires that you understand and fulfill the following requirements:\n\nTo send BLUR you must use the Blur Network CLI or GUI Wallet. \n\nIf you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\nIf you are using the Blur Network GUI Wallet, the transaction private key and transaction ID can be found conveniently in the "History" tab. Immediately after sending, locate the transaction of interest. Click the "?" symbol in the lower-right corner of the box containing the transaction. You must save this information. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the BLUR transfer using the Blur Transaction Viewer (https://blur.cash/#tx-viewer).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the BLUR sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the Blur Network Discord (https://discord.gg/dMWaqVW). # suppress inspection "UnusedProperty" account.crypto.popup.solo.msg=Negociar Solo na Haveno requer que você entenda e cumpra os seguintes requisitos:\n\nPara enviar Solo, você deve usar a carteira CLI Solo Network. \n\nSe você estiver usando a carteira CLI, um hash de transação (tx ID) será exibido após o envio de uma transferência. Você deve salvar essa informação. Imediatamente após o envio, você deve utilizar o comando 'get_tx_key' para obter a chave privada da transação. Se você não executar este passo devidamente, você pode não conseguir obter a chave depois.\n\nEm um evento onde uma arbitragem for necessária, você deve apresentar o seguinte para um árbitro ou mediador: 1.) ID da transação, 2.) a chave privada da transação, e 3.) o endereço do destinatário. O mediador ou árbitro irá então verificar a transferência Solo usando o explorador de blocos Solo, buscando pela transação e então usando a função "Provar envio" (https://explorer.minesolo.com/).\n\nA impossibilidade de fornecer as informações requeridas ao mediador ou árbitro resultará na perda do caso de disputa. Em todos os casos de disputa, In all cases of dispute, o remetente de Solo arca 100% com a responsabilidade de verificar as transações para um árbitro ou mediador.\n\nCaso não entenda estes requisitos, não negocie na Haveno. Procure ajuda no Discord da Solo Network primeiro. (https://discord.minesolo.com/). # suppress inspection "UnusedProperty" account.crypto.popup.cash2.msg=Negociar CASH2 na Haveno requer que você entenda e cumpra os seguintes requisitos:\n\nPara enviar CASH2, você deve usar a carteira Cash2 versão 3 ou superior. \n\nApós o envio de uma transação, o ID da transação será exibido. Você deve salvar essa informação. Imediatamente após enviar a transação, você deve utilizar o comando 'getTxKey' na simplewallet para obter a chave secreta da transação. Se você não executar este passo devidamente, você pode não conseguir obter a chave depois.\n\nEm um evento onde uma arbitragem for necessária, você deve apresentar o seguinte para um árbitro ou mediador: 1.) ID da transação, 2.) a chave secreta da transação, e 3.) o endereço do destinatário Cash2. O mediador ou árbitro irá então verificar a transferência CASH2 usando o explorador de blocos CASH2 (https://blocks.cash2.org).\n\nA impossibilidade de fornecer as informações requeridas ao mediador ou árbitro resultará na perda do caso de disputa. Em todos os casos de disputa, In all cases of dispute, o remetente de CASH2 arca 100% com a responsabilidade de verificar as transações para um árbitro ou mediador.\n\nCaso não entenda estes requisitos, não negocie na Haveno. Procure ajuda no Discord da Cash2 primeiro. (https://discord.gg/FGfXAYN). # suppress inspection "UnusedProperty" account.crypto.popup.qwertycoin.msg=Trading Qwertycoin on Haveno requires that you understand and fulfill the following requirements:\n\nTo send QWC you must use the official QWC Wallet version 5.1.3 or higher. \n\nAfter a transaction is sent, the transaction ID will be displayed. You must save this information. Immediately after sending the transaction, you must use the command 'get_Tx_Key' in simplewallet to retrieve the transaction secret key. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, 2) the transaction secret key, and 3) the recipient's QWC address. The mediator or arbitrator will then verify the QWC transfer using the QWC Block Explorer (https://explorer.qwertycoin.org).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the QWC sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the QWC Discord (https://discord.gg/rUkfnpC). # suppress inspection "UnusedProperty" account.crypto.popup.drgl.msg=Trading Dragonglass on Haveno requires that you understand and fulfill the following requirements:\n\nBecause of the privacy Dragonglass provides, a transaction is not verifiable on the public blockchain. If required, you can prove your payment through the use of your TXN-Private-Key.\nThe TXN-Private Key is a one-time key automatically generated for every transaction that can only be accessed from within your DRGL wallet.\nEither by DRGL-wallet GUI (inside transaction details dialog) or by the Dragonglass CLI simplewallet (using command "get_tx_key").\n\nDRGL version 'Oathkeeper' and higher are REQUIRED for both.\n\nIn case of a dispute, you must provide the mediator or arbitrator the following data:\n- The TXN-Private key\n- The transaction hash\n- The recipient's public address\n\nVerification of payment can be made using the above data as inputs at (http://drgl.info/#check_txn).\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The Dragonglass sender is responsible for providing verification of the DRGL transfer to the mediator or arbitrator in case of a dispute. Use of PaymentID is not required.\n\nIf you are unsure about any part of this process, visit Dragonglass on Discord (http://discord.drgl.info) for help. # suppress inspection "UnusedProperty" account.crypto.popup.ZEC.msg=Ao usar Zcash você só pode usar endereços transparentes(que começam com t), não os z-addresses (privados), pois o mediador e o árbitro não conseguiriam verificar a transação com endereços privados num explorador de blocos. # suppress inspection "UnusedProperty" account.crypto.popup.XZC.msg=Ao usar Zcoin você só pode usar endereços transparentes(rastreáveis), não os inrrastreáveis, pois o mediador e o árbitro não conseguiriam verificar a transação com endereços irrastreáveis num explorador de blocos. # suppress inspection "UnusedProperty" account.crypto.popup.grin.msg=GRIN requer um processo interativo entre o remetente e o destinatário para criar a transação. Certifique-se de seguir as instruções da página web do projeto GRIN para enviar e receber de forma confiável o GRIN (o destinatário precisa estar online ou pelo menos estar online durante um determinado período de tempo).\n\nHaveno suporta apenas o formato de URL da carteira Grinbox (Wallet713).\n\nO remetente GRIN é obrigado a fornecer prova de que ele enviou GRIN com sucesso. Se a carteira não puder fornecer essa prova, uma potencial disputa será resolvida em favor do destinatário de GRIN. Certifique-se de usar o software Grinbox mais recente, que suporta a prova de transação e que você entende o processo de transferência e receção do GRIN, bem como criar a prova.\n\nConsulte https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only para obter mais informações sobre a ferramenta de prova Grinbox. # suppress inspection "UnusedProperty" account.crypto.popup.beam.msg=BEAM requer um processo interativo entre o remetente e o destinatário para criar a transação.\n\nCertifique-se de seguir as instruções da página Web do projeto BEAM para enviar e receber BEAM de forma confiável (o destinatário precisa estar online ou pelo menos estar online durante um determinado período de tempo).\n\nO remetente BEAM é obrigado a fornecer prova de que ele enviou o BEAM com sucesso. Certifique-se de usar uma carteira que possa produzir tal prova. Se a carteira não puder fornecer a prova, uma disputa potencial será resolvida em favor do destinarário do BEAM. # suppress inspection "UnusedProperty" account.crypto.popup.pars.msg=Trading ParsiCoin on Haveno requires that you understand and fulfill the following requirements:\n\nTo send PARS you must use the official ParsiCoin Wallet version 3.0.0 or higher. \n\nYou can Check your Transaction Hash and Transaction Key on Transactions Section on your GUI Wallet (ParsiPay) You need to right Click on the Transaction and then click on show details. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the Transaction Hash, 2) the Transaction Key, and 3) the recipient's PARS address. The mediator or arbitrator will then verify the PARS transfer using the ParsiCoin Block Explorer (http://explorer.parsicoin.net/#check_payment).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the ParsiCoin sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the ParsiCoin Discord (https://discord.gg/c7qmFNh). # suppress inspection "UnusedProperty" account.crypto.popup.blk-burnt.msg=To trade burnt blackcoins, you need to know the following:\n\nBurnt blackcoins are unspendable. To trade them on Haveno, output scripts need to be in the form: OP_RETURN OP_PUSHDATA, followed by associated data bytes which, after being hex-encoded, constitute addresses. For example, burnt blackcoins with an address 666f6f (“foo” in UTF-8) will have the following script:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\nTo create burnt blackcoins, one may use the “burn” RPC command available in some wallets.\n\nFor possible use cases, one may look at https://ibo.laboratorium.ee .\n\nAs burnt blackcoins are unspendable, they can not be reselled. “Selling” burnt blackcoins means burning ordinary blackcoins (with associated data equal to the destination address).\n\nIn case of a dispute, the BLK seller needs to provide the transaction hash. # suppress inspection "UnusedProperty" account.crypto.popup.liquidmonero.msg=Para negociar com L-XMR na Haveno é preciso entender o seguinte:\n\nQuando se recebe L-XMR de uma negociação na Haveno, você não pode usar a carteira móvel Blockstream Green ou uma carteira de exchange. Você só pode receber L-XMR numa carteira Liquid Elements Core, ou outra carteira L-XMR que lhe permita obter a blinding key para o seu endereço blinded de L-XMR.\n\nNo caso de mediação ou se uma disputa acontecer, você precisa divulgar ao mediador, ou agente de reembolsos, a blinding key do seu endereço receptor de L-XMR para que ele possa verificar os detalhes da sua Transação Confidencial no node próprio deles.\n\nCaso essa informação não seja fornecida ao mediador ou agente de reembolsos você corre o risco de perder a disputa. Em todos os casos de disputa o recebedor de L-XMR tem 100% de responsabilidade em fornecer a prova criptográfica ao mediador ou agente de reembolsos.\n\nSe você não entendeu esses requisitos, por favor não negocie L-XMR na Haveno. account.traditional.yourTraditionalAccounts=Suas contas de moeda nacional account.backup.title=Backup da carteira account.backup.location=Local de backup account.backup.selectLocation=Selecione local para backup account.backup.backupNow=Fazer backup agora (o backup não é criptografado!) account.backup.appDir=Pasta de dados do programa account.backup.openDirectory=Abrir pasta account.backup.openLogFile=Abrir arquivo de Log account.backup.success=Backup salvo com sucesso em:\n{0} account.backup.directoryNotAccessible=A pasta escolhida não está acessível. {0} account.password.removePw.button=Remover senha account.password.removePw.headline=Remover proteção de senha da carteira account.password.setPw.button=Definir senha account.password.setPw.headline=Definir proteção de senha da carteira account.password.info=Com a proteção por senha ativada, você precisará inserir sua senha ao iniciar o aplicativo, ao retirar monero de sua carteira e ao exibir suas palavras-semente. account.seed.backup.title=Faça backup das palavras-chave da sua carteira. account.seed.info=Por favor, anote tanto as palavras-chave da sua carteira quanto a data. Você pode recuperar sua carteira a qualquer momento com as palavras-chave e a data.\n\nVocê deve anotar as palavras-chave em uma folha de papel. Não as salve no computador.\n\nPor favor, observe que as palavras-chave não substituem uma cópia de segurança.\nVocê precisa criar uma cópia de segurança do diretório completo do aplicativo na tela "Conta/Backup" para recuperar o estado e os dados do aplicativo. account.seed.backup.warning=Por favor, observe que as palavras-chave não substituem uma cópia de segurança.\nVocê precisa criar uma cópia de segurança de todo o diretório de aplicativos na tela "Conta/Backup" para recuperar o estado e os dados do aplicativo. account.seed.warn.noPw.msg=Você não definiu uma senha para carteira, que protegeria a exibição das palavras-semente.\n\nGostaria de exibir as palavras-semente? account.seed.warn.noPw.yes=Sim, e não me pergunte novamente account.seed.enterPw=Digite a senha para ver a semente da carteira account.seed.restore.info=Faça um backup antes de aplicar a restauração a partir de palavras-semente. Esteja ciente de que a restauração da carteira é apenas para casos de emergência e pode causar problemas com a base de dados interna da carteira.\nNão é uma maneira de aplicar um backup! Por favor, use um backup do diretório de dados do programa para restaurar um estado anterior do programa.\n\nDepois de restaurado, o programa será desligado automaticamente. Após ser reiniciado, o programa será ressincronizado com a rede Monero. Isso pode demorar um pouco e aumenta ro consumo de CPU, especialmente se a carteira for mais antiga e tiver muitas transações. Por favor, evite interromper esse processo, caso contrário, você pode precisar excluir o diretório da corrente SPV novamente ou repetir o processo de restauração. account.seed.restore.ok=Ok, restaurar e desligar o Haveno #################################################################### # Mobile notifications #################################################################### account.notifications.setup.title=Configurações account.notifications.download.label=Baixar app móvel account.notifications.waitingForWebCam=Aguardando webcam... account.notifications.webCamWindow.headline=Escanear código QR do celular account.notifications.webcam.label=Usar webcam account.notifications.webcam.button=Escanear código QR account.notifications.noWebcam.button=Eu não tenho uma webcam account.notifications.erase.label=Limpar notificações no celular account.notifications.erase.title=Limpar notificações account.notifications.email.label=Token de pareamento account.notifications.email.prompt=Insira o token de pareamento que você recebeu por e-mail account.notifications.settings.title=Configurações account.notifications.useSound.label=Reproduzir som de notificação no celular account.notifications.trade.label=Receber mensagens de negociação account.notifications.market.label=Receber alertas de oferta account.notifications.price.label=Receber alertas de preço account.notifications.priceAlert.title=Alertas de preço account.notifications.priceAlert.high.label=Avisar se o preço do XMR estiver acima de account.notifications.priceAlert.low.label=Avisar se o preço do XMR estiver abaixo de account.notifications.priceAlert.setButton=Definir alerta de preço account.notifications.priceAlert.removeButton=Remover alerta de preço account.notifications.trade.message.title=O estado da negociação mudou account.notifications.trade.message.msg.conf=A transação de depósito para a negociação com o ID {0} foi confirmada. Por favor, abra o seu aplicativo Haveno e realize o pagamento. account.notifications.trade.message.msg.started=O comprador de XMR iniciou o pagarmento para a negociação com o ID {0}. account.notifications.trade.message.msg.completed=A negociação com o ID {0} foi completada. account.notifications.offer.message.title=A sua oferta foi aceita account.notifications.offer.message.msg=A sua oferta com o ID {0} foi aceita account.notifications.dispute.message.title=Nova mensagem de disputa account.notifications.dispute.message.msg=Você recebeu uma mensagem de disputa pela negociação com o ID {0} account.notifications.marketAlert.title=Alertas de oferta account.notifications.marketAlert.selectPaymentAccount=Ofertas correspondendo à conta de pagamento account.notifications.marketAlert.offerType.label=Tenho interesse em account.notifications.marketAlert.offerType.buy=Ofertas de compra (eu quero vender XMR) account.notifications.marketAlert.offerType.sell=Ofertas de venda (eu quero comprar XMR) account.notifications.marketAlert.trigger=Distância do preço da oferta (%) account.notifications.marketAlert.trigger.info=Ao definir uma distância de preço, você só irá receber um alerta quando alguém publicar uma oferta que atinge (ou excede) os seus critérios. Por exemplo: você quer vender XMR, mas você só irá vender a um prêmio de 2% sobre o preço de mercado atual. Ao definir esse campo para 2%, você só irá receber alertas de ofertas cujos preços estão 2% (ou mais) acima do preço de mercado atual. account.notifications.marketAlert.trigger.prompt=Distância percentual do preço do mercado (ex: 2.50%, -0.50%, etc.) account.notifications.marketAlert.addButton=Inserir alerta de oferta account.notifications.marketAlert.manageAlertsButton=Gerenciar alertas de oferta account.notifications.marketAlert.manageAlerts.title=Gerenciar alertas de oferta account.notifications.marketAlert.manageAlerts.header.paymentAccount=Conta de pagamento account.notifications.marketAlert.manageAlerts.header.trigger=Preço gatilho account.notifications.marketAlert.manageAlerts.header.offerType=Tipo de oferta account.notifications.marketAlert.message.title=Alerta de oferta account.notifications.marketAlert.message.msg.below=abaixo account.notifications.marketAlert.message.msg.above=acima account.notifications.marketAlert.message.msg=Uma nova oferta '{0} {1}' com preço {2} ({3} {4} preço de mercado) e com o método de pagamento '{5}' foi publicada no livro de ofertas do Haveno.\nID da oferta: {6}. account.notifications.priceAlert.message.title=Alerta de preço para {0} account.notifications.priceAlert.message.msg=O seu preço de alerta foi atingido. O preço atual da {0} é {1} {2} account.notifications.noWebCamFound.warning=Nenhuma webcam foi encontrada.\n\nPor favor, use a opção e-mail para enviar o token e a chave de criptografia do seu celular para o Haveno account.notifications.priceAlert.warning.highPriceTooLow=O preço mais alto deve ser maior do que o preço mais baixo account.notifications.priceAlert.warning.lowerPriceTooHigh=O preço mais baixo deve ser menor do que o preço mais alto #################################################################### # Windows #################################################################### inputControlWindow.headline=Select inputs for transaction inputControlWindow.balanceLabel=Saldo disponível contractWindow.title=Detalhes da disputa contractWindow.dates=Data da oferta / Data da negociação contractWindow.xmrAddresses=Endereço monero do comprador de XMR / vendedor de XMR contractWindow.onions=Endereço de rede comprador de XMR / vendendor de XMR contractWindow.accountAge=Idade da conta do comprador de XMR / vendedor de XMR contractWindow.numDisputes=Nº de disputas comprador de XMR / vendedor de XMR: contractWindow.contractHash=Hash do contrato displayAlertMessageWindow.headline=Informação importante! displayAlertMessageWindow.update.headline=Informação importante de atualização! displayAlertMessageWindow.update.download=Download: displayUpdateDownloadWindow.downloadedFiles=Arquivos: displayUpdateDownloadWindow.downloadingFile=Baixando: {0} displayUpdateDownloadWindow.verifiedSigs=Assinatura verificada com as chaves: displayUpdateDownloadWindow.status.downloading=Baixando arquivos... displayUpdateDownloadWindow.status.verifying=Verificando assinatura... displayUpdateDownloadWindow.button.label=Baixar instalador e verificar assinatura displayUpdateDownloadWindow.button.downloadLater=Baixar depois displayUpdateDownloadWindow.button.ignoreDownload=Ignorar essa versão displayUpdateDownloadWindow.headline=Uma nova atualização para o Haveno está disponível! displayUpdateDownloadWindow.download.failed.headline=Erro no download displayUpdateDownloadWindow.download.failed=Download failed.\nPlease download and verify manually at [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.installer.failed=Unable to determine the correct installer. Please download and verify manually at [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.verify.failed=Verification failed.\nPlease download and verify manually at [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.success=A nova versão foi baixada com sucesso e teve a sua assinatura verificada.\n\nPara usá-la, abra a pasta de downloads, feche o programa e instale a nova versão. displayUpdateDownloadWindow.download.openDir=Abrir pasta de download disputeSummaryWindow.title=Resumo disputeSummaryWindow.openDate=Data da abertura do ticket disputeSummaryWindow.role=Função do negociador disputeSummaryWindow.payout=Pagamento da quantia negociada disputeSummaryWindow.payout.getsTradeAmount={0} XMR fica com o pagamento da negociação disputeSummaryWindow.payout.getsAll=Max. payout to XMR {0} disputeSummaryWindow.payout.custom=Pagamento personalizado disputeSummaryWindow.payoutAmount.buyer=Quantia do pagamento do comprador disputeSummaryWindow.payoutAmount.seller=Quantia de pagamento do vendedor disputeSummaryWindow.payoutAmount.invert=Usar perdedor como publicador disputeSummaryWindow.reason=Motivo da disputa disputeSummaryWindow.tradePeriodEnd=Trade period end disputeSummaryWindow.extraInfo=Extra information disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BUG=Bug (problema técnico) # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.USABILITY=Usabilidade # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Violação de protocolo # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.NO_REPLY=Sem resposta # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SCAM=Golpe (Scam) # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OTHER=Outro # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BANK_PROBLEMS=Banco # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OPTION_TRADE=Option trade # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Trader not responding # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled disputeSummaryWindow.summaryNotes=Notas de resumo disputeSummaryWindow.addSummaryNotes=Adicionar notas de resumo disputeSummaryWindow.close.button=Fechar ticket # Do no change any line break or order of tokens as the structure is used for signature verification # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.msg=Ticket closed on {0}\n{1} node address: {2}\n\nSummary:\nTrade ID: {3}\nCurrency: {4}\nTrade amount: {5}\nPayout amount for XMR buyer: {6}\nPayout amount for XMR seller: {7}\n\nReason for dispute: {8}\n\nSummary notes:\n{9}\n # Do no change any line break or order of tokens as the structure is used for signature verification disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3} disputeSummaryWindow.close.nextStepsForMediation=\nNext steps:\nOpen trade and accept or reject suggestion from mediator disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\nNo further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions disputeSummaryWindow.close.closePeer=Você também precisa fechar o ticket dos parceiros de negociação! disputeSummaryWindow.close.txDetails.headline=Publicar transação de reembolso # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.buyer=Comprador recebe {0} no endereço: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=Vendedor recebe {0} no endereço: {1}\n disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3}\n\nAre you sure you want to publish this transaction? disputeSummaryWindow.close.noPayout.headline=Close without any payout disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? emptyWalletWindow.headline={0} ferramenta de emergência da carteira emptyWalletWindow.info=Por favor, utilize essa opção apenas em caso de emergência, caso você não consiga acessar seus fundos a partir do programa.\n\nNote que todas as ofertas abertas serão fechadas automaticamente quando você utilizar esta ferramenta.\n\nAntes de usar esta ferramenta, faça um backup da sua pasta de dados. Você pode fazer isso em \"Conta/Backup\".\n\nHavendo qualquer problema, avise-nos através do GitHub ou do fórum Haveno, para que assim possamos investigar o que causou o problema. emptyWalletWindow.balance=Seu saldo disponível na carteira emptyWalletWindow.address=Seu endereço de destino emptyWalletWindow.button=Enviar todos os fundos emptyWalletWindow.openOffers.warn=Você possui ofertas abertas que serão removidas se você esvaziar sua carteira.\nTem certeza de que deseja esvaziar sua carteira? emptyWalletWindow.openOffers.yes=Sim, tenho certeza emptyWalletWindow.sent.success=O conteúdo da sua carteira foi transferido com sucesso. enterPrivKeyWindow.headline=Insira a chave privada para o registro filterWindow.headline=Editar lista de filtragem filterWindow.offers=Ofertas filtradas (sep. por vírgula): filterWindow.onions=Banned from trading addresses (comma sep.) filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.) filterWindow.accounts=Dados de conta de negociação filtrados:\nFormato: lista separada por vírgulas de [id do método de pagamento | dados | valor] filterWindow.bannedCurrencies=Códigos de moedas filtrados (sep. por vírgula) filterWindow.bannedPaymentMethods=IDs de método de pagamento filtrados (sep. por vírgula) filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) filterWindow.arbitrators=Árbitros filtrados (endereços onion sep. por vírgula) filterWindow.mediators=Mediadores filtrados (endereços onion separados por vírgula) filterWindow.refundAgents=Agentes de reembolso filtrados (endereços onion separados por vírgula) filterWindow.seedNode=Nós de semente filtrados (endereços onion sep. por vírgula) filterWindow.priceRelayNode=Nós de transmissão de preço filtrados (endereços onion sep. por vírgula) filterWindow.xmrNode=Nós de Monero filtrados (endereços + portas sep. por vírgula) filterWindow.preventPublicXmrNetwork=Prevenir uso da rede de Monero pública filterWindow.disableAutoConf=Disable auto-confirm filterWindow.autoConfExplorers=Filtered auto-confirm explorers (comma sep. addresses) filterWindow.disableTradeBelowVersion=Versão mínima necessária para negociação filterWindow.add=Adicionar filtro filterWindow.remove=Remover filtro filterWindow.xmrFeeReceiverAddresses=XMR fee receiver addresses filterWindow.disableApi=Disable API filterWindow.disableMempoolValidation=Disable Mempool Validation offerDetailsWindow.minXmrAmount=Quantia mín. em XMR offerDetailsWindow.min=(mín. {0}) offerDetailsWindow.distance=(distância do preço de mercado: {0}) offerDetailsWindow.myTradingAccount=Minha conta de negociação offerDetailsWindow.offererBankId=(ID/BIC/SWIFT do banco do ofertante) offerDetailsWindow.offerersBankName=(nome do banco do ofertante) offerDetailsWindow.bankId=ID do banco (ex: BIC ou SWIFT) offerDetailsWindow.countryBank=País do banco do ofertante offerDetailsWindow.commitment=Compromisso offerDetailsWindow.agree=Eu concordo offerDetailsWindow.tac=Termos e condições offerDetailsWindow.confirm.maker.buy=Confirmar: Criar oferta para comprar XMR com {0} offerDetailsWindow.confirm.maker.sell=Confirmar: Criar oferta para vender XMR por {0} offerDetailsWindow.confirm.taker.buy=Confirmar: Aceitar oferta para comprar XMR com {0} offerDetailsWindow.confirm.taker.sell=Confirmar: Aceitar oferta para vender XMR por {0} offerDetailsWindow.creationDate=Criada em offerDetailsWindow.makersOnion=Endereço onion do ofertante offerDetailsWindow.challenge=Passphrase da oferta offerDetailsWindow.challenge.copy=Copiar frase secreta para compartilhar com seu par qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. qRCodeWindow.request=Solicitação de pagamento:\n{0} selectDepositTxWindow.headline=Selecionar transação de depósito para disputa selectDepositTxWindow.msg=A transação do depósito não foi armazenada na negociação.\nPor favor, selecione a transação multisig da sua carteira utilizada como transação de depósito na negociação que falhou.\n\nVocê pode verificar qual foi a transação abrindo a janela de detalhe de negociações (clique no ID da negociação na lista) e seguindo a saída (output) da transação de pagamento da taxa de negociação para a próxima transação onde você verá a transação de depósito multisig (o endereço começa com o número 3). Esse ID de transação deve estar visível na lista apresentada aqui. Uma vez encontrada a transação, selecione-a aqui e continue.\n\nDesculpe o transtorno, este erro deve ocorrer muito pouco e no futuro vamos procurar melhores formas de resolvê-lo. selectDepositTxWindow.select=Selecionar transação de depósito sendAlertMessageWindow.headline=Enviar notificação global sendAlertMessageWindow.alertMsg=Mensagem de alerta sendAlertMessageWindow.enterMsg=Digitar mensagem sendAlertMessageWindow.isSoftwareUpdate=Software download notification sendAlertMessageWindow.isUpdate=Is full release sendAlertMessageWindow.isPreRelease=Is pre-release sendAlertMessageWindow.version=Nº da nova versão sendAlertMessageWindow.send=Enviar notificação sendAlertMessageWindow.remove=Remover notificação sendPrivateNotificationWindow.headline=Enviar mensagem privada sendPrivateNotificationWindow.privateNotification=Notificação privada sendPrivateNotificationWindow.enterNotification=Digite notificação sendPrivateNotificationWindow.send=Enviar notificação privada showWalletDataWindow.walletData=Dados da carteira showWalletDataWindow.includePrivKeys=Incluir chaves privadas setXMRTxKeyWindow.headline=Prove sending of XMR setXMRTxKeyWindow.note=Adding tx info below enables auto-confirm for quicker trades. See more: https://haveno.exchange/wiki/Trading_Monero setXMRTxKeyWindow.txHash=Transaction ID (optional) setXMRTxKeyWindow.txKey=Transaction key (optional) # We do not translate the tac because of the legal nature. We would need translations checked by lawyers # in each language which is too expensive atm. tacWindow.headline=Acordo de usuário tacWindow.agree=Eu concordo tacWindow.disagree=Eu não concordo e desisto tacWindow.arbitrationSystem=Resolução de disputas tradeDetailsWindow.headline=Negociação tradeDetailsWindow.disputedPayoutTxId=ID de transação do pagamento disputado tradeDetailsWindow.tradeDate=Data da negociação tradeDetailsWindow.txFee=Taxa de mineração tradeDetailsWindow.tradePeersOnion=Endereço onion dos parceiros de negociação tradeDetailsWindow.tradePeersPubKeyHash=Trading peers pubkey hash tradeDetailsWindow.tradeState=Estado da negociação tradeDetailsWindow.agentAddresses=Árbitro/Mediador tradeDetailsWindow.detailData=Detail data txDetailsWindow.headline=Transaction Details txDetailsWindow.xmr.noteSent=Você enviou XMR. txDetailsWindow.xmr.noteReceived=Você recebeu XMR. txDetailsWindow.sentTo=Enviado para txDetailsWindow.receivedWith=Recebido com txDetailsWindow.txId=TxId closedTradesSummaryWindow.headline=Trade history summary closedTradesSummaryWindow.totalAmount.title=Total trade amount closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) closedTradesSummaryWindow.totalTradeFeeInXmr.title=Sum of all trade fees paid in XMR closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} of total trade amount) walletPasswordWindow.headline=Digite senha para abrir: xmrConnectionError.headline=Erro de conexão com Monero xmrConnectionError.providedNodes=Erro ao conectar aos nós Monero fornecidos.\n\nDeseja usar o próximo melhor nó Monero disponível? xmrConnectionError.customNodes=Erro ao conectar aos seus nós Monero personalizados.\n\nDeseja usar o próximo melhor nó Monero disponível? xmrConnectionError.localNode=O Haveno estava previamente conectado a um nó Monero local, mas ele não está mais acessível.\n\nCertifique-se de que seu nó local está em execução e totalmente sincronizado, ou escolha outra opção para continuar. xmrConnectionError.localNode.start=Iniciar nó local xmrConnectionError.localNode.start.error=Erro ao iniciar o nó local xmrConnectionError.localNode.fallback=Conectar ao próximo melhor nó torNetworkSettingWindow.header=Configurações de rede do Tor torNetworkSettingWindow.noBridges=Não usar pontes torNetworkSettingWindow.providedBridges=Conectar com as pontes fornecidas torNetworkSettingWindow.customBridges=Adicionar pontes personalizadas torNetworkSettingWindow.transportType=Tipo de transporte torNetworkSettingWindow.obfs3=obfs3 torNetworkSettingWindow.obfs4=obfs4 (recomendado) torNetworkSettingWindow.meekAmazon=meek-amazon torNetworkSettingWindow.meekAzure=meek-azure torNetworkSettingWindow.enterBridge=Insira uma ou mais pontes de retransmissão (uma por linha) torNetworkSettingWindow.enterBridgePrompt=digite endereço:porta torNetworkSettingWindow.restartInfo=Você precisa reiniciar o programa para aplicar as modificações torNetworkSettingWindow.openTorWebPage=Abrir site do projeto Tor torNetworkSettingWindow.deleteFiles.header=Problemas de conexão? torNetworkSettingWindow.deleteFiles.info=Caso você está tendo problemas de conexão durante a inicialização, tente deletar os arquivos desatualizados do Tor. Para fazer isso, clique no botão abaixo e reinicialize o programa em seguida. torNetworkSettingWindow.deleteFiles.button=Apagar arquivos desatualizados do Tor e desligar torNetworkSettingWindow.deleteFiles.progress=Desligando Tor... torNetworkSettingWindow.deleteFiles.success=Os arquivos desatualizados do Tor foram deletados com sucesso. Por favor, reinicie o aplicativo. torNetworkSettingWindow.bridges.header=O Tor está bloqueado? torNetworkSettingWindow.bridges.info=Se o Tor estiver bloqueado pelo seu provedor de internet ou em seu país, você pode tentar usar pontes do Tor.\nVisite a página do Tor em https://bridges.torproject.org para aprender mais sobre pontes e transportadores plugáveis. feeOptionWindow.headline=Escolha a moeda para pagar a taxa de negociação feeOptionWindow.info=Você pode optar por pagar a taxa de negociação em BSQ ou XMR. As taxas de negociação são reduzidas quando pagas com BSQ. feeOptionWindow.optionsLabel=Escolha a moeda para pagar a taxa de negociação feeOptionWindow.useXMR=Usar XMR feeOptionWindow.fee={0} (≈ {1}) feeOptionWindow.xmrFeeWithFiatAndPercentage={0} (≈ {1} / {2}) feeOptionWindow.xmrFeeWithPercentage={0} ({1}) #################################################################### # Popups #################################################################### popup.headline.notification=Notificação popup.headline.instruction=Favor observar: popup.headline.attention=Atenção popup.headline.backgroundInfo=Informação preliminar popup.headline.feedback=Concluído popup.headline.confirmation=Confirmação popup.headline.information=Informação popup.headline.warning=Aviso popup.headline.error=Erro popup.doNotShowAgain=Não mostrar novamente popup.reportError.log=Abrir arquivo de log popup.reportError.gitHub=Reportar à lista de problemas no GitHub popup.reportError={0}\n\nPara nos ajudar a melhorar o aplicativo, reporte o bug criando um relatório (Issue) em nossa página do GitHub em https://github.com/haveno-dex/haveno/issues.\n\nA mensagem de erro exibida acima será copiada para a área de transferência quando você clicar qualquer um dos botões abaixo.\nA solução de problemas será mais fácil se você anexar o arquivo haveno.log ao clicar em "Abrir arquivo de log", salvando uma cópia e incluindo-a em seu relatório do problema (Issue) no GitHub. popup.error.tryRestart=Por favor, reinicie o aplicativo e verifique sua conexão de Internet para ver se o problema foi resolvido. popup.error.takeOfferRequestFailed=Houve um quando alguém tentou aceitar uma de suas ofertas:\n{0} error.spvFileCorrupted=Houve um erro ao ler o arquivo SPV chain.\nPode ser que o arquivo SPV chain esteja corrompido.\n\nMensagem de erro: {0}\n\nDeseja remover o arquivo e re-sincronizar? error.deleteAddressEntryListFailed=Não foi possível apagar o arquivo AddressEntryList.\nErro: {0} error.closedTradeWithUnconfirmedDepositTx=A transação de depósito da negociação já fechada com ID {0} ainda está não-confirmada.\n\nPor favor ressincronize o arquivo SPV em "Configurações/Informações da rede" para verificar se a transação é válida. error.closedTradeWithNoDepositTx=A transação de depósito da negociação já fechada com ID {0} está ausente.\n\nPor favor, reinicie o aplicativo para atualizar a lista de negociações encerradas. popup.warning.walletNotInitialized=A carteira ainda não foi inicializada popup.warning.osxKeyLoggerWarning=Due to stricter security measures in macOS 10.14 and above, launching a Java application (Haveno uses Java) causes a popup warning in macOS ('Haveno would like to receive keystrokes from any application').\n\nTo avoid that issue please open your 'macOS Settings' and go to 'Security & Privacy' -> 'Privacy' -> 'Input Monitoring' and Remove 'Haveno' from the list on the right side.\n\nHaveno will upgrade to a newer Java version to avoid that issue as soon the technical limitations (Java packager for the required Java version is not shipped yet) are resolved. popup.warning.wrongVersion=Você provavelmente está usando a versão incorreta do Haveno para este computador.\nA arquitetura do seu computador é: {0}.\nO binário do Haveno que você instalou é: {1}.\nPor favor, feche o programa e instale a versão correta ({2}). popup.warning.incompatibleDB=We detected incompatible data base files!\n\nThose database file(s) are not compatible with our current code base:\n{0}\n\nWe made a backup of the corrupted file(s) and applied the default values to a new database version.\n\nThe backup is located at:\n{1}/db/backup_of_corrupted_data.\n\nPlease check if you have the latest version of Haveno installed.\nYou can download it at: [HYPERLINK:https://haveno.exchange/downloads].\n\nPlease restart the application. popup.warning.startupFailed.twoInstances=O Haveno já está sendo executado. Você não pode executar duas instâncias do Haveno ao mesmo tempo. popup.warning.tradePeriod.halfReached=Sua negociação com ID {0} chegou à metade do período máximo permitido e ainda não foi concluída.\n\nO período de negociação acaba em {1}\n\nFavor verifique o estado de sua negociação em \"Portfolio/Negociações em aberto\" para mais informações. popup.warning.tradePeriod.ended=Sua negociação com ID {0} atingiu o período máximo de negociação e não foi finalizada.\n\nO período de negociação terminou em {1}.\n\nPor favor, verifique sua negociação em "Portfolio/Negociações em aberto" para contactar o mediador. popup.warning.noTradingAccountSetup.headline=Você ainda não configurou uma conta para negociação popup.warning.noTradingAccountSetup.msg=Você precisa criar uma conta em moeda nacional ou crypto para poder criar uma oferta.\nCriar uma conta? popup.warning.noArbitratorsAvailable=Não há árbitros disponíveis. popup.warning.noMediatorsAvailable=Não há mediadores disponíveis. popup.warning.notFullyConnected=Você precisa aguardar até estar totalmente conectado à rede.\nIsto pode levar até 2 minutos na inicialização do programa. popup.warning.notSufficientConnectionsToXmrNetwork=Você precisa esperar até ter pelo menos {0} conexões à rede Monero. popup.warning.downloadNotComplete=Você precisa aguardar até que termine o download dos blocos de Monero restantes popup.warning.walletNotSynced=A carteira Haveno não está sincronizada com a altura mais recente da blockchain. Por favor, aguarde até que a carteira seja sincronizada ou verifique sua conexão. popup.warning.removeOffer=Tem certeza que deseja remover essa oferta? popup.warning.tooLargePercentageValue=Você não pode definir uma porcentagem superior a 100%. popup.warning.examplePercentageValue=Digite um número percentual, como \"5.4\" para 5.4% popup.warning.noPriceFeedAvailable=Não há feed de preços disponível para essa moeda. Você não pode usar um preço porcentual.\nPor favor selecione um preço fixo. popup.warning.sendMsgFailed=O envio da mensagem para seu parceiro de negociação falhou.\nFavor tentar novamente, e se o erro persistir reportar o erro (bug report). popup.warning.btcChangeBelowDustException=Esta transação cria um troco menor do que o limite poeira (546 Satoshi) e seria rejeitada pela rede Monero.\nVocê precisa adicionar a quantia poeira ao montante de envio para evitar gerar uma saída de poeira.\nA saída de poeira é {0}. popup.warning.messageTooLong=Sua mensagem excede o tamanho máximo permitido. Favor enviá-la em várias partes ou utilizando um serviço como https://pastebin.com. popup.warning.lockedUpFunds=Você possui fundos travados em uma negociação com erro.\nSaldo travado: {0}\nEndereço da transação de depósito: {1}\nID da negociação: {2}.\n\nPor favor, abra um ticket de suporte selecionando a negociação na tela de negociações em aberto e depois pressionando "\alt+o\" ou \"option+o\". popup.warning.moneroConnection=Houve um problema ao conectar-se à rede Monero.\n\n{0} popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n takeOffer.cancelButton=Cancel take-offer takeOffer.warningButton=Ignore and continue anyway # suppress inspection "UnusedProperty" popup.warning.nodeBanned=One of the {0} nodes got banned. # suppress inspection "UnusedProperty" popup.warning.priceRelay=transmissão de preço popup.warning.seed=semente popup.warning.mandatoryUpdate.trading=Faça o update para a última versão do Haveno. Um update obrigatório foi lançado e desabilita negociações em versões antigas. Por favor, veja o Fórum do Haveno para mais informações. popup.warning.noFilter=Não recebemos um objeto de filtro dos nós seed. Por favor, informe aos administradores da rede para registrar um objeto de filtro. popup.warning.burnXMR=Esta transação não é possível, pois as taxas de mineração de {0} excederiam o montante a transferir de {1}. Aguarde até que as taxas de mineração estejam novamente baixas ou até você ter acumulado mais XMR para transferir. popup.warning.openOffer.makerFeeTxRejected=A transação de taxa de ofertante para a oferta com ID {0} foi rejeitada pela rede Monero.\nID da transação: {1}.\nA oferta foi removida para evitar problemas adicionais.\nPor favor, vá até "Configurações/Informações da rede" e ressincronize o arquivo SPV.\nPara mais informações, por favor acesse o canal #support do time da Haveno na Keybase. popup.warning.trade.txRejected.tradeFee=taxa de negociação popup.warning.trade.txRejected.deposit=depósito popup.warning.trade.txRejected=The {0} transaction for trade with ID {1} was rejected by the Monero network.\nTransaction ID={2}\nThe trade has been moved to failed trades.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. popup.warning.openOfferWithInvalidMakerFeeTx=A transação de taxa de ofertante para a oferta com ID {0} é inválida.\nID da transação: {1}.\nPor favor, vá até "Configurações/Informações da rede" e ressincronize o arquivo SPV.\nPara mais informações, por favor acesse o canal #support do time da Haveno na Keybase. popup.info.securityDepositInfo=Para garantir que ambas as partes sigam o protocolo de negociação, tanto o vendedor quanto o comprador precisam fazer um depósito de segurança.\n\nEste depósito permanecerá em sua carteira local até que a negociação seja concluída com sucesso. Depois, ele será devolvido para você.\n\nAtenção: se você está criando uma nova oferta, é necessário que você mantenha o programa aberto, para que outro usuário possa aceitar a sua oferta. Para manter suas ofertas online, mantenha o Haveno sempre aberto e conectado à internet (por exemplo: verifique-se de que as funções de economia de energia do seu computador estão desativadas). popup.info.cashDepositInfo=Certifique-se de que você possui uma agência bancária em sua região para poder fazer o depósito em dinheiro.\nO ID (BIC/SWIFT) do banco do vendedor é: {0}. popup.info.cashDepositInfo.confirm=Eu confirmo que posso fazer o depósito popup.info.shutDownWithOpenOffers=O Haveno está desligando, mas há ofertas abertas.\n\nEstas ofertas não ficarão disponíveis na rede P2P enquanto o Haveno estiver desligado, mas elas serão republicadas na rede assim que você reiniciar o programa.\n\nPara manter suas ofertas online, mantenha o Haveno aberto e certifique-se de que o seu computador continua online (ex: certifique-se de que o computador não está entrando em modo de hibernação). popup.info.qubesOSSetupInfo=It appears you are running Haveno on Qubes OS. \n\nPlease make sure your Haveno qube is setup according to our Setup Guide at [HYPERLINK:https://haveno.exchange/wiki/Running_Haveno_on_Qubes]. popup.warn.downGradePrevention=Downgrade from version {0} to version {1} is not supported. Please use the latest Haveno version. popup.privateNotification.headline=Notificação privada importante! popup.securityRecommendation.headline=Recomendação de segurança importante popup.securityRecommendation.msg=Lembre-se de proteger a sua carteira com uma senha, caso você já não tenha criado uma.\n\nRecomendamos que você escreva num papel as palavras da semente de sua carteira. Essas palavras funcionam como uma senha mestra para recuperar a sua carteira Monero, caso o seu computador apresente algum problema.\nVocê irá encontrar mais informações na seção \"Semente da carteira\".\n\nTambém aconselhamos que você faça um backup completo da pasta de dados do programa na seção \"Backup\". popup.shutDownInProgress.headline=Desligando popup.shutDownInProgress.msg=O desligamento do programa pode levar alguns segundos.\nPor favor, não interrompa este processo. popup.attention.forTradeWithId=Atenção para a negociação com ID {0} popup.attention.reasonForPaymentRuleChange=Version 1.5.5 introduces a critical trade rule change regarding the \"reason for payment\" field in bank transfers. Please leave this field empty -- DO NOT use the trade ID as \"reason for payment\" anymore. popup.info.multiplePaymentAccounts.headline=Múltiplas contas de pagamento disponíveis popup.info.multiplePaymentAccounts.msg=Você tem várias contas de pagamento disponíveis para esta oferta. Por favor, verifique se você escolheu a correta. popup.accountSigning.selectAccounts.headline=Selecionar contas de pagamento popup.accountSigning.selectAccounts.description=Baseado no método de pagamento e ponto no tempo, todas as contas de pagamento que estiverem ligadas a um disputa em que o pagamento ocorreu em favor do comprador serão selecionadas para que você assine-as. popup.accountSigning.selectAccounts.signAll=Assinar todas as contas de pagamento popup.accountSigning.selectAccounts.datePicker=Selecione o ponto no tempo até o qual as contas serão assinadas popup.accountSigning.confirmSelectedAccounts.headline=Confirmar contas de pagamento selecionadas popup.accountSigning.confirmSelectedAccounts.description=Baseado na sua seleção, {0} contas de pagamento serão selecionadas. popup.accountSigning.confirmSelectedAccounts.button=Confirmar contas de pagamento popup.accountSigning.signAccounts.headline=Confirmar assinatura de contas de pagamento. popup.accountSigning.signAccounts.description=Baseado na sua seleção, {0} contas de pagamento serão assinadas. popup.accountSigning.signAccounts.button=Assinar contas de pagamento popup.accountSigning.signAccounts.ECKey=Insira a chave privada de árbitro popup.accountSigning.signAccounts.ECKey.error=Chave ECKey de árbitro errada. popup.accountSigning.success.headline=Parabéns popup.accountSigning.success.description=Todas as {0} contas de pagamento foram assinadas com sucesso! popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\nFor further information, please visit [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. popup.accountSigning.signedByArbitrator=Uma de suas contas de pagamento foi verificada e assinada por um árbitro. Ao negociar com essa conta você automaticamente assinará a conta de seu par após uma negociação bem succedida.\n\n{0} popup.accountSigning.signedByPeer=Uma de suas contas de pagamento foi verificada e assinada por um par de negociação. Seu limite de negociação inicial será aumentado e você poderá assinar outras contas em {0} dias.\n\n{1} popup.accountSigning.peerLimitLifted=O limite inicial para uma de suas contas acaba de ser aumentado. popup.accountSigning.peerSigner=Uma das suas contas é antiga o suficiente para assinar outras contas de pagamento e o limite para uma de suas contas acaba de ser aumentado\n\n{0} popup.accountSigning.singleAccountSelect.headline=Import unsigned account age witness popup.accountSigning.confirmSingleAccount.headline=Confirm selected account age witness popup.accountSigning.confirmSingleAccount.selectedHash=Selected witness hash popup.accountSigning.confirmSingleAccount.button=Sign account age witness popup.accountSigning.successSingleAccount.description=Witness {0} was signed popup.accountSigning.successSingleAccount.success.headline=Success popup.accountSigning.unsignedPubKeys.headline=Unsigned Pubkeys popup.accountSigning.unsignedPubKeys.sign=Sign Pubkeys popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign popup.info.buyerAsTakerWithoutDeposit.headline=Nenhum depósito exigido do comprador popup.info.buyerAsTakerWithoutDeposit=Sua oferta não exigirá um depósito de segurança ou taxa do comprador de XMR.\n\nPara aceitar sua oferta, você deve compartilhar uma senha com seu parceiro de negociação fora do Haveno.\n\nA senha é gerada automaticamente e exibida nos detalhes da oferta após a criação. #################################################################### # Notifications #################################################################### notification.trade.headline=Notificação para o oferta com ID {0} notification.ticket.headline=Ticket de suporte para a oferta com ID {0} notification.trade.completed=A negociação foi concluída e você já pode retirar seus fundos. notification.trade.accepted=Sua oferta foi aceita por um {0}. notification.trade.unlocked=Sua negociação tem pelo menos uma confirmação da blockchain.\nVocê já pode iniciar o pagamento. notification.trade.paymentSent=O comprador XMR iniciou o pagamento notification.trade.selectTrade=Selecionar negociação notification.trade.peerOpenedDispute=Seu parceiro de negociação abriu um {0}. notification.trade.disputeClosed=A {0} foi fechada. notification.walletUpdate.headline=Update da carteira de negociação notification.walletUpdate.msg=Sua carteira Haveno tem saldo suficiente.\nQuantia: {0} notification.takeOffer.walletUpdate.msg=Sua carteira Haveno já tinha saldo suficiente de uma tentativa anterior de aceitar oferta.\nQuantia: {0} notification.tradeCompleted.headline=Negociação concluída notification.tradeCompleted.msg=Você pode retirar seus fundos para uma carteira externa de Monero ou mantê-los em sua carteira Haveno. #################################################################### # System Tray #################################################################### systemTray.show=Mostrar janela do applicativo systemTray.hide=Esconder janela do applicativo systemTray.info=Informações sobre Haveno systemTray.exit=Sair systemTray.tooltip=Haveno: a rede de exchange decentralizada de monero #################################################################### # GUI Util #################################################################### guiUtil.accountExport.savedToPath=Contas de negociação salvas na pasta:\n{0} guiUtil.accountExport.noAccountSetup=Você não tem contas de negociação para exportar. guiUtil.accountExport.selectPath=Selecione pasta de {0} # suppress inspection "TrailingSpacesInProperty" guiUtil.accountExport.tradingAccount=Conta de negociação com ID {0} # suppress inspection "TrailingSpacesInProperty" guiUtil.accountImport.noImport=Não importamos a conta de negociação com id {0} pois ela já existe.\n guiUtil.accountExport.exportFailed=Exportar para CSV falhou pois houve um erro.\nErro = {0} guiUtil.accountExport.selectExportPath=Selecionar pasta para exportar guiUtil.accountImport.imported=Conta de negociação importada da pasta:\n{0}\n\nContas importadas:\n{1} guiUtil.accountImport.noAccountsFound=Nenhuma conta de negociação exportada foi encontrada em: {0}.\nNome do arquivo é {1}." guiUtil.openWebBrowser.warning=Você abrirá uma página web em seu navegador padrão.\nDeseja abrir a página agora?\n\nSe você não estiver usando o \"Tor Browser\" como seu navegador padrão você conectará à página pela internet aberta (clear net).\n\nURL: \"{0}\" guiUtil.openWebBrowser.doOpen=Abrir a página e não perguntar novamente guiUtil.openWebBrowser.copyUrl=Copiar URL e fechar guiUtil.ofTradeAmount=da quantia da negociação guiUtil.requiredMinimum=(mínimo requerido) #################################################################### # Component specific #################################################################### list.currency.select=Selecione a moeda list.currency.showAll=Ver todos list.currency.editList=Editar lista de moedas table.placeholder.noItems=Atualmente não há {0} disponíveis table.placeholder.noData=Não há dados disponíveis no momento table.placeholder.processingData=Processando dados... peerInfoIcon.tooltip.tradePeer=Parceiro de negociação peerInfoIcon.tooltip.maker=do ofertante peerInfoIcon.tooltip.trade.traded={0} endereço onion: {1}\nVocê já negociou {2} vez(es) com esse parceiro\n{3} peerInfoIcon.tooltip.trade.notTraded=Endereço onion do {0}: {1}\nVocê ainda não negociou com esse usuário.\n{2} peerInfoIcon.tooltip.age=Conta de pagamento criada {0} atrás. peerInfoIcon.tooltip.unknownAge=Idade da conta de pagamento desconhecida. tooltip.openPopupForDetails=Abrir popup para mais detalhes tooltip.invalidTradeState.warning=This trade is in an invalid state. Open the details window for more information tooltip.openBlockchainForAddress=Abrir um explorer de blockchain externo para o endereço: {0} tooltip.openBlockchainForTx=Abrir um explorer de blockchain externo para a transação: {0} confidence.unknown=Transação com estado desconhecido confidence.seen=Visto por {0} par(es) / 0 confirmações confidence.confirmed={0} confirmação(ões) confidence.invalid=A transação é inválida peerInfo.title=Informação do par peerInfo.nrOfTrades=Nº de negociações concluídas peerInfo.notTradedYet=Você ainda não negociou com este usuário. peerInfo.setTag=Definir um rótulo para este par peerInfo.age.noRisk=Idade da conta de pagamento peerInfo.age.chargeBackRisk=Tempo desde a assinatura peerInfo.unknownAge=Idade desconhecida addressTextField.openWallet=Abrir a sua carteira Monero padrão addressTextField.copyToClipboard=Copiar endereço para área de transferência addressTextField.addressCopiedToClipboard=Endereço copiado para área de transferência addressTextField.openWallet.failed=Erro ao abrir a carteira padrão Monero. Talvez você não possua uma instalada. peerInfoIcon.tooltip={0}\nRótulo: {1} txIdTextField.copyIcon.tooltip=Copiar ID da transação txIdTextField.blockExplorerIcon.tooltip=Open a blockchain explorer with this transaction ID txIdTextField.missingTx.warning.tooltip=Missing required transaction #################################################################### # Navigation #################################################################### navigation.account=\"Conta\" navigation.account.walletSeed=\"Conta/Semente da carteira\" navigation.funds.availableForWithdrawal=\"Funds/Send funds\" navigation.portfolio.myOpenOffers=\"Portfolio/Minhas ofertas\" navigation.portfolio.pending=\"Portfolio/Negociações em aberto\" navigation.portfolio.closedTrades=\"Portfólio/Histórico\" navigation.funds.depositFunds=\"Fundos/Receber fundos\" navigation.settings.preferences=\"Configurações/Preferências\" # suppress inspection "UnusedProperty" navigation.funds.transactions=\"Fundos/Transações\" navigation.support=\"Suporte\" #################################################################### # Formatter #################################################################### formatter.formatVolumeLabel={0} quantia{1} formatter.makerTaker=Ofertante: {1} de {0} / Aceitador: {3} de {2} formatter.makerTaker.locked=Ofertante: {1} de {0} / Aceitador: {3} de {2} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=Você está {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Você está criando uma oferta para {0} {1} formatter.youAreCreatingAnOffer.traditional.locked=Você está criando uma oferta para {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Você está criando uma oferta para {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.crypto.locked=Você está criando uma oferta para {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} como ofertante formatter.asTaker={0} {1} como aceitador #################################################################### # Domain specific #################################################################### # we use enum values here # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" XMR_MAINNET=Mainnet do Monero # suppress inspection "UnusedProperty" XMR_LOCAL=Testnet do Monero # suppress inspection "UnusedProperty" XMR_STAGENET=Stagenet do Monero time.year=Ano time.month=Mês time.week=Semana time.day=Dia time.hour=Hora time.minute10=10 Minutos time.hours=horas time.days=dias time.1hour=1 hora time.1day=1 dia time.minute=minuto time.second=segundo time.minutes=minutos time.seconds=segundos password.enterPassword=Insira a senha password.confirmPassword=Confirme a senha password.tooLong=A senha deve ter menos de 500 caracteres. password.deriveKey=Derivando chave a partir da senha password.walletDecrypted=A carteira foi decifrada com sucesso e a proteção por senha removida password.wrongPw=Você digitou a senha incorreta.\n\nFavor tentar novamente, verificando com cuidado erros de digitação ou ortografia. password.walletEncrypted=A carteira foi encriptada e a proteção por senha foi ativada com sucesso. password.passwordsDoNotMatch=As 2 senhas inseridas não são iguais. password.forgotPassword=Esqueceu a senha? password.backupReminder=Please note that when setting a wallet password all automatically created backups from the unencrypted wallet will be deleted.\n\nIt is highly recommended that you make a backup of the application directory and write down your seed words before setting a password! password.backupWasDone=I have already made a backup password.setPassword=Set Password (I already made a backup) password.makeBackup=Make Backup seed.seedWords=Semente da carteira seed.enterSeedWords=Insira a semente da carteira seed.date=Data da carteira seed.restore.title=Recuperar carteira a partir das palavras semente seed.restore=Recuperar carteira seed.creationDate=Criada em seed.warn.walletNotEmpty.msg=Your Monero wallet is not empty.\n\nYou must empty this wallet before attempting to restore an older one, as mixing wallets together can lead to invalidated backups.\n\nPlease finalize your trades, close all your open offers and go to the Funds section to withdraw your monero.\nIn case you cannot access your monero you can use the emergency tool to empty the wallet.\nTo open the emergency tool press \"Alt+e\" or \"Cmd/Ctrl+e\". seed.warn.walletNotEmpty.restore=Desejo recuperar mesmo assim seed.warn.walletNotEmpty.emptyWallet=Esvaziarei as carteiras primeiro seed.warn.notEncryptedAnymore=Suas carteiras estão encriptadas.\n\nApós a restauração, as carteiras não estarão mais encriptadas e você deverá definir uma nova senha.\n\nDeseja continuar? seed.warn.walletDateEmpty=As you have not specified a wallet date, haveno will have to scan the blockchain from 2013.10.09 (the BIP39 epoch date).\n\nBIP39 wallets were first introduced in haveno on 2017.06.28 (release v0.5). So you could save time by using that date.\n\nIdeally you should specify the date your wallet seed was created.\n\n\nAre you sure you want to go ahead without specifying a wallet date? seed.restore.success=Carteiras recuperadas com sucesso com as novas palavras semente.\n\nVocê precisa desligar e reiniciar o aplicativo. seed.restore.error=Ocorreu um erro ao restaurar as carteiras com palavras semente.{0} seed.restore.openOffers.warn=You have open offers which will be removed if you restore from seed words.\nAre you sure that you want to continue? #################################################################### # Payment methods #################################################################### payment.account=Conta payment.account.no=Nº da conta payment.account.name=Nome da conta payment.account.username=Username payment.account.phoneNr=Phone number payment.account.owner.fullname=Nome completo do titular da conta payment.account.fullName=Nome completo (nome e sobrenome) payment.account.state=Estado/Província/Região payment.account.city=Cidade payment.bank.country=País do banco payment.account.name.email=Nome completo / e-mail do titular da conta payment.account.name.emailAndHolderId=Nome completo / e-mail / {0} do titular da conta payment.bank.name=Nome do banco payment.select.account=Selecione o tipo de conta payment.select.region=Selecionar região payment.select.country=Selecionar país payment.select.bank.country=Selecionar país do banco payment.foreign.currency=Tem certeza que deseja selecionar uma moeda que não seja a moeda padrão do pais? payment.restore.default=Não, restaurar para a moeda padrão payment.email=E-mail payment.country=País payment.extras=Requerimentos adicionais payment.email.mobile=E-mail ou celular payment.crypto.address=Endereço crypto payment.crypto.tradeInstantCheckbox=Negócio instantâneo (dentro de 1 hora) com esta Crypto payment.crypto.tradeInstant.popup=Para negociação instantânea, é necessário que os dois pares de negociação estejam online para concluir a negociação em menos de 1 hora.\n\nSe você tem ofertas abertas e você não está disponível, por favor desative essas ofertas na tela 'Portfolio'. payment.crypto=Crypto payment.select.crypto=Select or search Crypto payment.secret=Pergunta secreta payment.answer=Resposta payment.wallet=ID da carteira payment.amazon.site=Buy giftcard at payment.ask=Ask in Trader Chat payment.uphold.accountId=Nome de usuário, e-mail ou nº de telefone payment.moneyBeam.accountId=E-mail ou nº de telefone payment.popmoney.accountId=E-mail ou nº de telefone payment.promptPay.promptPayId=ID de cidadão/ID de impostos ou nº de telefone payment.supportedCurrencies=Moedas disponíveis payment.supportedCurrenciesForReceiver=Currencies for receiving funds payment.limitations=Limitações payment.salt=Sal para verificação da idade da conta payment.error.noHexSalt=The salt needs to be in HEX format.\nIt is only recommended to edit the salt field if you want to transfer the salt from an old account to keep your account age. The account age is verified by using the account salt and the identifying account data (e.g. IBAN). payment.accept.euro=Aceitar negociações destes países do Euro payment.accept.nonEuro=Aceitar negociações desses países fora do Euro payment.accepted.countries=Países aceitos payment.accepted.banks=Bancos aceitos (ID) payment.mobile=Celular payment.postal.address=CEP payment.national.account.id.AR=Número CBU shared.accountSigningState=Status de assinatura da conta #new payment.crypto.address.dyn=Endereço {0} payment.crypto.receiver.address=Endereço crypto do destinatário payment.accountNr=Nº da conta payment.emailOrMobile=E-mail ou celular payment.useCustomAccountName=Usar nome personalizado payment.maxPeriod=Período máximo de negociação permitido payment.maxPeriodAndLimit=Duração máxima da negociação: {0} / Max. compra: {1} / Max. venda: {2} / Idade de conta: {3} payment.maxPeriodAndLimitCrypto=Duração máxima de negociação: {0} / Limite de negociação: {1} payment.currencyWithSymbol=Moeda: {0} payment.nameOfAcceptedBank=Nome do banco aceito payment.addAcceptedBank=Adicionar banco aceito payment.clearAcceptedBanks=Limpar bancos aceitos payment.bank.nameOptional=Nome do banco (opcional) payment.bankCode=Código do banco payment.bankId=ID do banco (BIC/SWIFT) payment.bankIdOptional=ID do banco (BIC/SWIFT) (opcional) payment.branchNr=Nº da agência payment.branchNrOptional=Nº da agência (opcional) payment.accountNrLabel=Nº da conta (IBAN) payment.accountType=Tipo de conta payment.checking=Conta Corrente payment.savings=Poupança payment.personalId=Identificação pessoal payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle's somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver's full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account's age and signing status are preserved. payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=Ao usar o HalCash, o comprador de XMR precisa enviar ao vendedor de XMR o código HalCash através de uma mensagem de texto do seu telefone.\n\nPor favor, certifique-se de não exceder a quantia máxima que seu banco lhe permite enviar com o HalCash. O valor mínimo de saque é de 10 euros e valor máximo é de 600 EUR. Para saques repetidos é de 3000 euros por destinatário por dia e 6000 euros por destinatário por mês. Por favor confirme esses limites com seu banco para ter certeza de que eles usam os mesmos limites mencionados aqui.\n\nO valor de saque deve ser um múltiplo de 10 euros, pois você não pode sacar notas diferentes de uma ATM. Esse valor em XMR será ajustado na telas de criar e aceitar ofertas para que a quantia de EUR esteja correta. Você não pode usar o preço com base no mercado, pois o valor do EUR estaria mudando com a variação dos preços.\n\nEm caso de disputa, o comprador de XMR precisa fornecer a prova de que enviou o EUR. # suppress inspection "UnusedMessageFormatParameter" payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/overview/account_limits]. # suppress inspection "UnusedProperty" payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/overview/account_limits]. payment.cashDeposit.info=Certifique-se de que o seu banco permite a realização de depósitos em espécie na conta de terceiros. payment.revolut.info=Revolut requires the 'Username' as account ID not the phone number or email as it was the case in the past. payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a 'Username'.\nPlease enter your Revolut 'Username' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account payment.cashapp.info=Por favor, esteja ciente de que o Cash App tem um risco maior de estorno do que a maioria das transferências bancárias. payment.venmo.info=Por favor, esteja ciente de que o Venmo tem um risco maior de estorno do que a maioria das transferências bancárias. payment.paypal.info=Por favor, esteja ciente de que o PayPal tem um risco maior de estorno do que a maioria das transferências bancárias. payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account payment.payByMail.contact=Informações para contato payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.f2f.contact=Informações para contato payment.f2f.contact.prompt=How would you like to be contacted by the trading peer? (email address, phone number,...) payment.f2f.city=Cidade para se encontrar 'Cara-a-cara' payment.f2f.city.prompt=A cidade será exibida na oferta payment.shared.optionalExtra=Informações adicionais opcionais payment.shared.extraInfo=Informações adicionais payment.shared.extraInfo.offer=Informações adicionais sobre a oferta payment.shared.extraInfo.prompt.paymentAccount=Defina quaisquer termos, condições ou detalhes especiais que você gostaria que fossem exibidos com suas ofertas para esta conta de pagamento (os usuários verão estas informações antes de aceitar as ofertas). payment.shared.extraInfo.prompt.offer=Defina quaisquer termos, condições ou detalhes especiais que você gostaria de exibir com sua oferta. payment.shared.extraInfo.noDeposit=Detalhes de contato e termos da oferta payment.f2f.info.openURL=Abrir site payment.f2f.offerbook.tooltip.countryAndCity=País e cidade: {0} / {1} payment.shared.extraInfo.tooltip=Informações adicionais: {0} payment.japan.bank=Banco payment.japan.branch=Ramo payment.japan.account=Conta payment.japan.recipient=Nome payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=Para sua proteção, desaconselhamos fortemente o uso de PINs do Paysafecard para pagamento.\n\n\ Transações feitas por PINs não podem ser verificadas de forma independente para resolução de disputas. Se ocorrer um problema, a recuperação de fundos pode não ser possível.\n\n\ Para garantir a segurança das transações com resolução de disputas, sempre utilize métodos de pagamento que forneçam registros verificáveis. # We use constants from the code so we do not use our normal naming convention # dynamic values are not recognized by IntelliJ # Only translate general terms NATIONAL_BANK=Transferência bancária nacional SAME_BANK=Transferência para mesmo banco SPECIFIC_BANKS=Transferência com bancos específicos US_POSTAL_MONEY_ORDER=US Postal Money Order CASH_DEPOSIT=Depósito em dinheiro (cash deposit) PAY_BY_MAIL=Pay By Mail MONEY_GRAM=MoneyGram WESTERN_UNION=Western Union F2F=Face a face (pessoalmente) JAPAN_BANK=Japan Bank Furikomi AUSTRALIA_PAYID=Australian PayID # suppress inspection "UnusedProperty" NATIONAL_BANK_SHORT=Bancos nacionais # suppress inspection "UnusedProperty" SAME_BANK_SHORT=Mesmo banco # suppress inspection "UnusedProperty" SPECIFIC_BANKS_SHORT=Bancos específicos # suppress inspection "UnusedProperty" US_POSTAL_MONEY_ORDER_SHORT=US Money Order # suppress inspection "UnusedProperty" CASH_DEPOSIT_SHORT=Depósito em dinheiro (cash deposit) # suppress inspection "UnusedProperty" PAY_BY_MAIL_SHORT=PayByMail # suppress inspection "UnusedProperty" MONEY_GRAM_SHORT=MoneyGram # suppress inspection "UnusedProperty" WESTERN_UNION_SHORT=Western Union # suppress inspection "UnusedProperty" F2F_SHORT=F2F # suppress inspection "UnusedProperty" JAPAN_BANK_SHORT=Japan Furikomi # suppress inspection "UnusedProperty" AUSTRALIA_PAYID_SHORT=PayID # Do not translate brand names # suppress inspection "UnusedProperty" UPHOLD=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY=Popmoney # suppress inspection "UnusedProperty" REVOLUT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY=WeChat Pay # suppress inspection "UnusedProperty" SEPA=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT=SEPA Instant Payments # suppress inspection "UnusedProperty" FASTER_PAYMENTS=Faster Payments # suppress inspection "UnusedProperty" SWISH=Swish # suppress inspection "UnusedProperty" ZELLE=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS=Cryptos # suppress inspection "UnusedProperty" PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT=Instant Cryptos # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY=OKPay # suppress inspection "UnusedProperty" CASH_APP=Cash App # suppress inspection "UnusedProperty" VENMO=Venmo # suppress inspection "UnusedProperty" UPHOLD_SHORT=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM_SHORT=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY_SHORT=Popmoney # suppress inspection "UnusedProperty" REVOLUT_SHORT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY_SHORT=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY_SHORT=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY_SHORT=WeChat Pay # suppress inspection "UnusedProperty" SEPA_SHORT=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT_SHORT=SEPA Instant # suppress inspection "UnusedProperty" FASTER_PAYMENTS_SHORT=Faster Payments # suppress inspection "UnusedProperty" SWISH_SHORT=Swish # suppress inspection "UnusedProperty" ZELLE_SHORT=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY_SHORT=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER_SHORT=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH_SHORT=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS_SHORT=Cryptos # suppress inspection "UnusedProperty" PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT_SHORT=Cryptos Instant # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY_SHORT=OKPay # suppress inspection "UnusedProperty" CASH_APP_SHORT=Cash App # suppress inspection "UnusedProperty" VENMO_SHORT=Venmo #################################################################### # Validation #################################################################### validation.empty=Obrigatório validation.NaN=Número inválido validation.notAnInteger=A quantia não é um valor inteiro. validation.zero=Número 0 não é permitido validation.negative=Valores negativos não são permitidos. validation.traditional.tooSmall=Quantia menor do que a mínima permitida. validation.traditional.tooLarge=Quantia maior do que a máxima permitida. validation.xmr.fraction=Input will result in a monero value of less than 1 satoshi validation.xmr.tooLarge=Quantia máx. permitida: {0} validation.xmr.tooSmall=Quantia mín. permitida: {0} validation.passwordTooShort=The password you entered is too short. It needs to have a min. of 8 characters. validation.passwordTooLong=A senha inserida é muito longa. Não pode ser maior do que 50 caracteres validation.sortCodeNumber={0} deve consistir de {1} números. validation.sortCodeChars={0} deve consistir de {1} caracteres. validation.bankIdNumber={0} deve consistir de {1} números. validation.accountNr=O número de conta deve conter {0} números. validation.accountNrChars=O número da conta deve conter {0} caracteres. validation.xmr.invalidAddress=O endereço está incorreto. Por favor, verifique o formato do endereço. validation.integerOnly=Por favor, insira apesar números inteiros validation.inputError=Os dados inseridos causaram um erro:\n{0} validation.xmr.exceedsMaxTradeLimit=Seu limite de negociação é {0}. validation.nationalAccountId={0} deve consistir de {1} números. #new validation.invalidInput=Entrada inválida: {0} validation.accountNrFormat=O número da conta deve estar no formato: {0} # suppress inspection "UnusedProperty" validation.crypto.wrongStructure=Validação do endereço falhou pois este não é compatível com a estrutura de um endereço {0}. # suppress inspection "UnusedProperty" validation.crypto.ltz.zAddressesNotSupported=LTZ address must start with L. Addresses starting with z are not supported. # suppress inspection "UnusedProperty" validation.crypto.zAddressesNotSupported=ZEC addresses must start with t. Addresses starting with z are not supported. # suppress inspection "UnusedProperty" validation.crypto.invalidAddress=Endereço não é um endereço {0} válido! {1} # suppress inspection "UnusedProperty" validation.crypto.liquidBitcoin.invalidAddress=Endereços nativos de Segwit (começando com "lq") não são suportados. validation.bic.invalidLength=Input length must be 8 or 11 validation.bic.letters=Banco e código de país devem ser letras validation.bic.invalidLocationCode=BIC contém código de localização inválido validation.bic.invalidBranchCode=BIC contém código da agência inválido validation.bic.sepaRevolutBic=Contas Revolut Sepa não são suportadas. validation.btc.invalidFormat=Invalid format for a Bitcoin address. validation.email.invalidAddress=Endereço inválido validation.iban.invalidCountryCode=Código de país inválido validation.iban.checkSumNotNumeric=Código verificador deve ser numérico validation.iban.nonNumericChars=Caractere não alfanumérico detectado validation.iban.checkSumInvalid=Código de verificação IBAN é inválido validation.iban.invalidLength=Number must have a length of 15 to 34 chars. validation.interacETransfer.invalidAreaCode=Código de área não é canadense. validation.interacETransfer.invalidPhone=Please enter a valid 11 digit phone number (ex: 1-123-456-7890) or an email address validation.interacETransfer.invalidQuestion=Deve conter somente letras, números, espaços e/ou os símbolos ' _ , . ? - validation.interacETransfer.invalidAnswer=Deve ser uma palavra e conter apenas letras, números e/ou o símbolo - validation.inputTooLarge=Não deve ser maior do que {0} validation.inputTooSmall=Deve ser maior do que {0} validation.inputToBeAtLeast=O input tem de ser pelo menos {0} validation.amountBelowDust=An amount below the dust limit of {0} satoshi is not allowed. validation.length=Comprimento deve ser entre {0} e {1} validation.fixedLength=Length must be {0} validation.pattern=Input deve ser no formato: {0} validation.noHexString=O input não está no formato hexadecimal validation.advancedCash.invalidFormat=Deve ser um e-mail válido ou uma ID de carteira no formato: X000000000000 validation.invalidUrl=Essa URL não é válida validation.mustBeDifferent=Seu input precisa ser diferente do valor atual. validation.cannotBeChanged=O parâmetro não pode ser alterado validation.numberFormatException=Exceção do formato do número {0} validation.mustNotBeNegative=O input não deve ser negativo validation.phone.missingCountryCode=Precisa do código do país com duas letras para validar o número de telefone validation.phone.invalidCharacters=O número de telefone {0} contém caracteres inválidos. validation.phone.insufficientDigits=There are not enough digits in {0} to be a valid phone number validation.phone.tooManyDigits=There are too many digits in {0} to be a valid phone number validation.phone.invalidDialingCode=Country dialing code for number {0} is invalid for country {1}. The correct dialing code is {2}. validation.invalidAddressList=Precisa ser uma lista delimitada por vírgulas de endereços válidos ================================================ FILE: core/src/main/resources/i18n/displayStrings_pt.properties ================================================ # Keep display strings organized by domain # Naming convention: We use camelCase and dot separated name spaces. # Use as many sub spaces as required to make the structure clear, but as little as possible. # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break # in the display but only in the editor. # Please use in all language files the exact same order of the entries, that way a comparison is easier. # Please try to keep the length of the translated string similar to English. If it is longer it might break layout or # get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. #################################################################### # Shared #################################################################### shared.readMore=Ler mais shared.openHelp=Abrir a Ajuda shared.warning=Aviso shared.close=Fechar shared.cancel=Cancelar shared.ok=OK shared.yes=Sim shared.no=Não shared.iUnderstand=Eu compreendo shared.na=N/D shared.shutDown=Desligar shared.reportBug=Reportar erro no GitHub shared.buyMonero=Comprar monero shared.sellMonero=Vender monero shared.buyCurrency=Comprar {0} shared.sellCurrency=Vender {0} shared.buyCurrency.locked=Comprar {0} 🔒 shared.sellCurrency.locked=Vender {0} 🔒 shared.buyingXMRWith=comprando XMR com {0} shared.sellingXMRFor=vendendo XMR por {0} shared.buyingCurrency=comprando {0} (vendendo XMR) shared.sellingCurrency=vendendo {0} (comprando XMR) shared.buy=comprar shared.sell=vender shared.buying=comprando shared.selling=vendendo shared.P2P=P2P shared.oneOffer=oferta shared.multipleOffers=ofertas shared.Offer=Oferta shared.offerVolumeCode={0} Offer Volume shared.openOffers=ofertas abertas shared.trade=negócio shared.trades=negócios shared.openTrades=negócios abertos shared.dateTime=Data/Hora shared.price=Preço shared.priceWithCur=Preço em {0} shared.priceInCurForCur=Preço em {0} para 1 {1} shared.fixedPriceInCurForCur=Preço fixo em {0} para 1 {1} shared.amount=Quantia shared.txFee=Taxa da transação shared.tradeFee=Taxa de Negócio shared.buyerSecurityDeposit=Depósito do comprador shared.sellerSecurityDeposit=Depósito do vendedor shared.amountWithCur=Quantia em {0} shared.volumeWithCur=Volume em {0} shared.currency=Moeda shared.market=Mercado shared.deviation=Deviation shared.paymentMethod=Método de pagamento shared.tradeCurrency=Moeda de negócio shared.offerType=Tipo de oferta shared.details=Detalhes shared.address=Endereço shared.balanceWithCur=Saldo em {0} shared.utxo=Unspent transaction output shared.txId=ID de transação shared.confirmations=Confirmações shared.revert=Reverter Tx shared.select=Selecionar shared.usage=Uso shared.state=Estado shared.tradeId=ID do Negócio shared.offerId=ID de Oferta shared.bankName=Nome do banco shared.acceptedBanks=Bancos aceites shared.amountMinMax=Quantia (mín - máx) shared.amountHelp=Se a oferta tem uma quantia mínima ou máxima definida, poderá negociar qualquer quantia dentro deste intervalo. shared.remove=Remover shared.goTo=Ir para {0} shared.XMRMinMax=XMR (mín - máx) shared.removeOffer=Remover oferta shared.dontRemoveOffer=Não remover a oferta shared.editOffer=Editar oferta shared.openLargeQRWindow=Abrir QR-Code em janela grande shared.tradingAccount=Conta de negociação shared.faq=Visit FAQ page shared.yesCancel=Sim, cancelar shared.nextStep=Próximo passo shared.selectTradingAccount=Selecionar conta de negociação shared.fundFromSavingsWalletButton=Aplicar fundos da carteira Haveno shared.fundFromExternalWalletButton=Abrir sua carteira externa para o financiamento shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? shared.belowInPercent=Abaixo % do preço de mercado shared.aboveInPercent=Acima % do preço de mercado shared.enterPercentageValue=Insira % do valor shared.OR=OU shared.notEnoughFunds=You don't have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. shared.waitingForFunds=Esperando pelos fundos... shared.TheXMRBuyer=O comprador de XMR shared.You=Você shared.sendingConfirmation=Enviando confirmação... shared.sendingConfirmationAgain=Por favor envia a confirmação de novo shared.exportCSV=Export to CSV shared.exportJSON=Exportar para JSON shared.summary=Show summary shared.noDateAvailable=Sem dada disponível shared.noDetailsAvailable=Sem detalhes disponíveis shared.notUsedYet=Ainda não usado shared.date=Data shared.sendFundsDetailsWithFee=Enviando: {0}\n\nPara o endereço de recebimento: {1}\n\nTaxa adicional do minerador: {2}\n\nTem certeza de que deseja enviar este valor? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Copiar para área de transferência shared.language=Idioma shared.country=País shared.applyAndShutDown=Aplicar e desligar shared.selectPaymentMethod=Selecionar método de pagamento shared.accountNameAlreadyUsed=That account name is already used for another saved account.\nPlease choose another name. shared.askConfirmDeleteAccount=Você realmente quer apagar a conta selecionada? shared.cannotDeleteAccount=You cannot delete that account because it is being used in an open offer (or in an open trade). shared.noAccountsSetupYet=Ainda não há contas configuradas shared.manageAccounts=Gerir contas shared.addNewAccount=Adicionar uma nova conta shared.ExportAccounts=Exportar Contas shared.importAccounts=Importar Contas shared.createNewAccount=Criar nova conta shared.createNewAccountDescription=Os detalhes da sua conta são armazenados localmente no seu dispositivo e compartilhados apenas com seu parceiro de negociação e o árbitro, caso uma disputa seja aberta. shared.saveNewAccount=Guardar nova conta shared.selectedAccount=Conta selecionada shared.deleteAccount=Apagar conta shared.errorMessageInline=\nMensagem de erro: {0} shared.errorMessage=Mensagem de erro shared.information=Informação shared.name=Nome shared.id=ID shared.dashboard=Painel shared.accept=Aceitar shared.balance=Saldo shared.save=Guardar shared.onionAddress=Endereço onion shared.supportTicket=solicitação de suporte shared.dispute=disputa shared.mediationCase=caso de mediação shared.seller=vendedor shared.buyer=comprador shared.allEuroCountries=Todos os países do Euro shared.acceptedTakerCountries=Países aceites para aceitador shared.tradePrice=Preço de negócio shared.tradeAmount=Quantia de negócio shared.tradeVolume=Volume de negócio shared.invalidKey=A chave que você inseriu não estava correta shared.enterPrivKey=Coloque a chave privada para desbloquear shared.payoutTxId=ID de transação de pagamento shared.contractAsJson=Contrato em formato JSON shared.viewContractAsJson=Ver contrato em formato JSON shared.contract.title=Contrato para negócio com ID: {0} shared.paymentDetails=Detalhes do pagamento do {0} de XMR shared.securityDeposit=Depósito de segurança shared.yourSecurityDeposit=O seu depósito de segurança shared.contract=Contrato shared.messageArrived=Mensagem chegou. shared.messageStoredInMailbox=Mensagem guardada na caixa de correio. shared.messageSendingFailed=Falha no envio da mensagem. Erro: {0} shared.unlock=Desbloquear shared.toReceive=a receber shared.toSpend=a enviar shared.xmrAmount=Quantia de XMR shared.yourLanguage=Os seus idiomas shared.addLanguage=Adicionar idioma shared.total=Total shared.totalsNeeded=Fundos necessários shared.tradeWalletAddress=Endereço da carteira do negócio shared.tradeWalletBalance=Saldo da carteira de negócio shared.reserveExactAmount=Reserve apenas os fundos necessários. Requer uma taxa de mineração e ~20 minutos antes que sua oferta seja publicada. shared.makerTxFee=Ofertante: {0} shared.takerTxFee=Aceitador: {0} shared.iConfirm=Eu confirmo shared.openURL=Abrir {0} shared.fiat=Moeda fiduciária shared.crypto=Cripto shared.preciousMetals=Metais Preciosos shared.all=Tudo shared.edit=Editar shared.advancedOptions=Opções avançadas shared.interval=Intervalo shared.actions=Ações shared.buyerUpperCase=Comprador shared.sellerUpperCase=Vendedor shared.new=NOVO shared.learnMore=Saber mais shared.dismiss=Ignorar shared.selectedArbitrator=Árbitro selecionado shared.selectedMediator=Mediador selecionado shared.selectedRefundAgent=Árbitro selecionado shared.mediator=Mediador shared.arbitrator=Árbitro shared.refundAgent=Árbitro shared.refundAgentForSupportStaff=Agente de Reembolso shared.delayedPayoutTxId=Delayed payout transaction ID shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later. shared.numItemsLabel=Number of entries: {0} shared.filter=Filtro shared.enabled=Enabled #################################################################### # UI views #################################################################### #################################################################### # MainView #################################################################### mainView.menu.market=Mercado mainView.menu.buyXmr=Comprar XMR mainView.menu.sellXmr=Vender XMR mainView.menu.portfolio=Portefólio mainView.menu.funds=Fundos mainView.menu.support=Apoio mainView.menu.settings=Definições mainView.menu.account=Conta mainView.marketPriceWithProvider.label=Preço de mercado por {0} mainView.marketPrice.havenoInternalPrice=Preço do último negócio do Haveno mainView.marketPrice.tooltip.havenoInternalPrice=Não há preço de mercado de fornecedores de feed de preço externos disponíveis.\nO preço exibido é o mais recente preço de negócio do Haveno para essa moeda. mainView.marketPrice.tooltip=O preço de mercado é fornecido por {0} {1}\nÚltima atualização: {2}\nURL do nó do provedor: {3} mainView.balance.available=Saldo disponível mainView.balance.reserved=Reservado em ofertas mainView.balance.pending=Bloqueado em negócios mainView.balance.reserved.short=Reservado mainView.balance.pending.short=Bloqueado mainView.footer.usingTor=(via Tor) mainView.footer.localhostMoneroNode=(localhost) mainView.footer.clearnet=(via clearnet) mainView.footer.xmrInfo={0} {1} mainView.footer.xmrFeeRate=/ Fee rate: {0} sat/vB mainView.footer.xmrInfo.initializing=Conectando à rede Haveno mainView.footer.xmrInfo.synchronizingWith=Sincronizando com {0} no bloco: {1} / {2} mainView.footer.xmrInfo.connectedTo=Conectado a {0} no bloco {1} mainView.footer.xmrInfo.synchronizingWalletWith=Sincronizando a carteira com {0} no bloco: {1} / {2} mainView.footer.xmrInfo.syncedWith=Sincronizado com {0} no bloco {1} mainView.footer.xmrInfo.connectingTo=Conectando à mainView.footer.xmrInfo.connectionFailed=Connection failed to mainView.footer.xmrPeers=Monero network peers: {0} mainView.footer.p2pPeers=Haveno network peers: {0} mainView.bootstrapState.connectionToTorNetwork=(1/4) Conectando à rede Tor.... mainView.bootstrapState.torNodeCreated=(2/4) Nó da rede Tor criado mainView.bootstrapState.hiddenServicePublished=(3/4) Serviço Oculto publicado mainView.bootstrapState.initialDataReceived=(4/4) Dados iniciais recebidos mainView.bootstrapWarning.noSeedNodesAvailable=Nenhum nó semente disponível mainView.bootstrapWarning.noNodesAvailable=Sem nós semente e pares disponíveis mainView.bootstrapWarning.bootstrappingToP2PFailed=O bootstrap para a rede do Haveno falhou mainView.p2pNetworkWarnMsg.noNodesAvailable=Não há nós de semente ou pares persistentes disponíveis para solicitar dados.\nPor favor, verifique a sua conexão de Internet ou tente reiniciar o programa. mainView.p2pNetworkWarnMsg.connectionToP2PFailed=A conexão com a rede do Haveno falhou (erro reportado: {0}).\nPor favor, verifique sua conexão com a Internet ou tente reiniciar o programa. mainView.walletServiceErrorMsg.timeout=A conexão com a rede Monero falhou por causa de tempo esgotado. mainView.walletServiceErrorMsg.connectionError=A conexão com a rede Monero falhou devido ao erro: {0} mainView.walletServiceErrorMsg.rejectedTxException=Uma transação foi rejeitada pela rede.\n\n{0} mainView.networkWarning.allConnectionsLost=Você perdeu a conexão com todos os pares de rede de {0} .\nTalvez você tenha perdido sua conexão de internet ou o seu computador estivesse no modo de espera. mainView.networkWarning.localhostMoneroLost=Perdeu a conexão ao nó Monero do localhost.\nPor favor recomeçar o programa do Haveno para conectar à outros nós Monero ou recomeçar o nó Monero do localhost. mainView.version.update=(Atualização disponível) #################################################################### # MarketView #################################################################### market.tabs.offerBook=Livro de ofertas market.tabs.spreadCurrency=Offers by Currency market.tabs.spreadPayment=Offers by Payment Method market.tabs.trades=Negócios # OfferBookChartView market.offerBook.buyCrypto=Eu quero comprar {0} (vender {1}) market.offerBook.sellCrypto=Eu quero vender {0} (comprar {1}) market.offerBook.buyWithTraditional=Comprar {0} market.offerBook.sellWithTraditional=Vender {0} market.offerBook.sellOffersHeaderLabel=Vender {0} para market.offerBook.buyOffersHeaderLabel=Comprar {0} de market.offerBook.buy=Eu quero comprar monero market.offerBook.sell=Eu quero vender monero # SpreadView market.spread.numberOfOffersColumn=Todas as ofertas ({0}) market.spread.numberOfBuyOffersColumn=Comprar XMR ({0}) market.spread.numberOfSellOffersColumn=Vender XMR ({0}) market.spread.totalAmountColumn=Total de XMR ({0}) market.spread.spreadColumn=Spread market.spread.expanded=Expanded view # TradesChartsView market.trades.nrOfTrades=Negócios: {0} market.trades.tooltip.volumeBar=Volume: {0} / {1}\nNo. of trades: {2}\nDate: {3} market.trades.tooltip.candle.open=Abrir: market.trades.tooltip.candle.close=Fechar: market.trades.tooltip.candle.high=Alta: market.trades.tooltip.candle.low=Baixa: market.trades.tooltip.candle.average=Média: market.trades.tooltip.candle.median=Mediano: market.trades.tooltip.candle.date=Data: market.trades.showVolumeInUSD=Show volume in USD #################################################################### # OfferView #################################################################### offerbook.createOffer=Criar oferta offerbook.takeOffer=Aceitar oferta offerbook.takeOfferToBuy=Aceitar oferta para comprar {0} offerbook.takeOfferToSell=Aceitar oferta para vender {0} offerbook.takeOffer.enterChallenge=Digite a senha da oferta offerbook.trader=Negociador offerbook.offerersBankId=ID do banco do ofertante (BIC/SWIFT): {0} offerbook.offerersBankName=Nome do banco do ofertante: {0} offerbook.offerersBankSeat=Sede do banco do ofertante: {0} offerbook.offerersAcceptedBankSeatsEuro=Sedes do banco aceites (aceitador): Todos os países do Euro offerbook.offerersAcceptedBankSeats=Sede do banco aceite (aceitador):\n {0} offerbook.availableOffers=Ofertas disponíveis offerbook.filterByCurrency=Filtrar por moeda offerbook.filterByPaymentMethod=Filtrar por método de pagamento offerbook.matchingOffers=Ofertas que correspondem às minhas contas offerbook.filterNoDeposit=Sem depósito offerbook.noDepositOffers=Ofertas sem depósito (senha necessária) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info=Esta conta foi verificada e {0} offerbook.timeSinceSigning.info.arbitrator=assinada pelo árbitro e pode assinar contas de pares offerbook.timeSinceSigning.info.peer=signed by a peer, waiting %d days for limits to be lifted offerbook.timeSinceSigning.info.peerLimitLifted=assinada por um par e os limites foram aumentados offerbook.timeSinceSigning.info.signer=assinada por um par e pode assinar contas de pares (limites aumentados) offerbook.timeSinceSigning.info.banned=account was banned offerbook.timeSinceSigning.daysSinceSigning={0} dias offerbook.timeSinceSigning.daysSinceSigning.long={0} desde a assinatura offerbook.xmrAutoConf=Is auto-confirm enabled offerbook.buyXmrWith=Compre XMR com: offerbook.sellXmrFor=Venda XMR por: offerbook.timeSinceSigning.help=Quando você completa com sucesso um negócio com um par que tenha uma conta de pagamento assinada, a sua conta de pagamento é assinada .\n{0} dias depois, o limite inicial de {1} é aumentado e a sua conta pode assinar contas de pagamento de outros pares. offerbook.timeSinceSigning.notSigned=Ainda não assinada offerbook.timeSinceSigning.notSigned.ageDays={0} dias offerbook.timeSinceSigning.notSigned.noNeed=N/D shared.notSigned=This account has not been signed yet and was created {0} days ago shared.notSigned.noNeed=This account type does not require signing shared.notSigned.noNeedDays=This account type does not require signing and was created {0} days ago shared.notSigned.noNeedAlts=Crypto accounts do not feature signing or aging offerbook.nrOffers=Nº de ofertas: {0} offerbook.volume={0} (mín - máx) offerbook.deposit=Deposit XMR (%) offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed. offerbook.createNewOffer=Criar oferta para {0} {1} offerbook.createOfferToBuy=Criar nova oferta para comprar {0} offerbook.createOfferToSell=Criar nova oferta para vender {0} offerbook.createOfferToBuy.withTraditional=Criar nova oferta para comprar {0} com {1} offerbook.createOfferToSell.forTraditional=Criar nova oferta para vender {0} por {1} offerbook.createOfferToBuy.withCrypto=Criar nova oferta para vender {0} (comprar {1}) offerbook.createOfferToSell.forCrypto=Criar nova oferta para comprar {0} (vender {1}) offerbook.takeOfferButton.tooltip=Aceitar oferta por {0} offerbook.yesCreateOffer=Sim, criar oferta offerbook.setupNewAccount=Configurar uma nova conta de negociação offerbook.removeOffer.success=Remoção da oferta bem sucedida offerbook.removeOffer.failed=Remoção da oferta falhou:\n{0} offerbook.deactivateOffer.failed=A desativação da oferta falhou:\n{0} offerbook.activateOffer.failed=A publicação da oferta falhou:\n{0} offerbook.withdrawFundsHint=Você pode levantar fundos que você pagou a partir do ecrã de {0}. offerbook.warning.noTradingAccountForCurrency.headline=No payment account for selected currency offerbook.warning.noTradingAccountForCurrency.msg=You don't have a payment account set up for the selected currency.\n\nWould you like to create an offer for another currency instead? offerbook.warning.noMatchingAccount.headline=No matching payment account. offerbook.warning.noMatchingAccount.msg=This offer uses a payment method you haven't set up yet. \n\nWould you like to set up a new payment account now? offerbook.warning.counterpartyTradeRestrictions=Esta oferta não pode ser aceite devido às restrições de negócio da contraparte offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\nAfter successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\nFor more information on account signing, please see the documentation at [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. popup.warning.tradeLimitDueAccountAgeRestriction.seller=A quantia de negócio é limitada à {0} devido à restrições de segurança baseadas nos seguinte critérios:\n- A conta do comprador não foi assinada por um árbitro ou um par\n- O tempo decorrido desde a assinatura da conta do comprador não é de pelo menos 30 dias\n- O método de pagamento para esta oferta é considerado arriscado para estornos bancários\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=A quantia de negócio é limitada à {0} devido à restrições de segurança baseadas nos seguinte critérios:\n- A sua conta não foi assinada por um árbitro ou um par\n- O tempo decorrido desde a assinatura da sua conta não é de pelo menos 30 dias\n- O método de pagamento para esta oferta é considerado arriscado para estornos bancários\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=Este método de pagamento está temporariamente limitado a {0} até {1} porque todos os compradores têm novas contas.\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=Sua oferta será limitada a compradores com contas assinadas e antigas porque excede {0}.\n\n{1} offerbook.warning.wrongTradeProtocol=Essa oferta requer uma versão de protocolo diferente da usada na sua versão do software.\n\nPor favor, verifique se você tem a versão mais recente instalada, caso contrário, o usuário que criou a oferta usou uma versão mais antiga.\n\nOs utilizadores não podem negociar com uma versão de protocolo de negócio incompatível. offerbook.warning.userIgnored=Você adicionou o endereço onion daquele utilizador à sua lista de endereços ignorados. offerbook.warning.offerBlocked=Essa oferta foi bloqueada pelos desenvolvedores do Haveno.\nProvavelmente, há um erro não tratado causando problemas ao aceitar a oferta. offerbook.warning.currencyBanned=A moeda usada nessa oferta foi bloqueada pelos desenvolvedores do Haveno.\nPor favor, visite o Fórum Haveno para mais informações. offerbook.warning.paymentMethodBanned=O método de pagamento usado nessa oferta foi bloqueado pelos desenvolvedores do Haveno.\nPor favor, visite o Fórum Haveno para mais informações. offerbook.warning.nodeBlocked=O endereço onion desse negociador foi bloqueado pelos desenvolvedores do Haveno.\nProvavelmente, há um erro não tratado causando problemas ao aceitar ofertas desse negociador. offerbook.warning.offerWasAlreadyUsedInTrade=You cannot take this offer because you already took it earlier. It could be that your previous take-offer attempt resulted in a failed trade. offerbook.info.sellAtMarketPrice=Venderá ao preço de mercado (atualizado à cada minuto). offerbook.info.buyAtMarketPrice=Comprará ao preço de mercado (atualizado à cada minuto). offerbook.info.sellBelowMarketPrice=Receberá menos {0} do que o atual preço de mercado (atualizado cada minuto). offerbook.info.buyAboveMarketPrice=Pagará mais {0} do que o atual preço do mercado (atualizado cada minuto). offerbook.info.sellAboveMarketPrice=Receberá mais {0} do que o atual preço do mercado (atualizado à cada minuto). offerbook.info.buyBelowMarketPrice=Pagará menos {0} do que o atual preço do mercado (atualizado à cada minuto). offerbook.info.buyAtFixedPrice=Comprará à este preço fixo. offerbook.info.sellAtFixedPrice=Venderá à este preço fixo. offerbook.info.noArbitrationInUserLanguage=Em caso de disputa, saiba que a arbitragem para esta oferta será tratada em {0}. O idioma está atualmente definido para {1}. offerbook.info.roundedFiatVolume=A quantia foi arredondada para aumentar a privacidade do seu negócio. #################################################################### # Offerbook / Create offer #################################################################### createOffer.amount.prompt=Escreva a quantia em XMR createOffer.price.prompt=Escreva o preço createOffer.volume.prompt=Escreva a quantia em {0} createOffer.amountPriceBox.amountDescription=Quantia de XMR para {0} createOffer.amountPriceBox.buy.volumeDescription=Quantia em {0} a ser gasto createOffer.amountPriceBox.sell.volumeDescription=Quantia em {0} a ser recebido createOffer.amountPriceBox.minAmountDescription=Quantia mínima de XMR createOffer.securityDeposit.prompt=Depósito de segurança createOffer.fundsBox.title=Financiar sua oferta createOffer.fundsBox.offerFee=Taxa de negócio createOffer.fundsBox.networkFee=Taxa de mineração createOffer.fundsBox.placeOfferSpinnerInfo=Oferta sendo publicada... createOffer.fundsBox.paymentLabel=negócio do Haveno com ID {0} createOffer.fundsBox.fundsStructure=({0} depósito de segurança, {1} taxa de negócio, {2} taxa de mineração) createOffer.success.headline=Sua oferta foi criada createOffer.success.info=Você pode gerir as suas ofertas abertas em \"Portefólio/As minhas ofertas abertas\". createOffer.info.sellAtMarketPrice=Venderá sempre ao preço do mercado pois o preço da sua oferta será atualizado continuamente. createOffer.info.buyAtMarketPrice=Comprará sempre ao preço do mercado pois o preço da sua oferta será atualizado continuamente. createOffer.info.sellAboveMarketPrice=Receberá sempre mais {0}% do que o atual preço de mercado pois o preço da sua oferta será atualizado continuamente. createOffer.info.buyBelowMarketPrice=Pagará sempre menos {0}% do que o atual preço de mercado pois o preço da sua oferta será atualizado continuamente. createOffer.warning.sellBelowMarketPrice=Receberá sempre menos {0}% do que o atual preço de mercado pois o preço da sua oferta será atualizado continuamente. createOffer.warning.buyAboveMarketPrice=Pagará sempre mais {0}% do que o atual preço de mercado pois o preço da sua oferta será atualizado continuamente. createOffer.tradeFee.descriptionXMROnly=taxa de negócio createOffer.tradeFee.descriptionBSQEnabled=Selecione a moeda da taxa de negócio createOffer.triggerPrice.prompt=Set optional trigger price createOffer.triggerPrice.label=Deactivate offer if market price is {0} createOffer.triggerPrice.tooltip=As protection against drastic price movements you can set a trigger price which deactivates the offer if the market price reaches that value. createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0} createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0} # new entries createOffer.placeOfferButton.buy=Revisar: Criar oferta para comprar XMR com {0} createOffer.placeOfferButton.sell=Revisar: Criar oferta para vender XMR por {0} createOffer.createOfferFundWalletInfo.headline=Financiar sua oferta # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Quantia de negócio: {0} \n createOffer.createOfferFundWalletInfo.msg=Você precisa depositar {0} para esta oferta.\n\n\ Esses fundos são reservados em sua carteira local e serão bloqueados em uma carteira multisig assim que alguém aceitar sua oferta.\n\n\ O valor é a soma de:\n\ {1}\ - Seu depósito de segurança: {2}\n\ - Taxa de negociação: {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=Ocorreu um erro ao colocar a oferta:\n\n{0}\n\nAinda nenhuns fundos saíram da sua carteira.\nPor favor, reinicie seu programa e verifique sua conexão de rede. createOffer.setAmountPrice=Definir preço e quantia createOffer.warnCancelOffer=Você já financiou essa oferta.\nSe você cancelar agora, seus fundos permanecerão na sua carteira Haveno local e estarão disponíveis para retirada na tela \"Fundos/Enviar fundos\".\nTem certeza de que deseja cancelar? createOffer.timeoutAtPublishing=Tempo esgotado durante a publicação da oferta. createOffer.errorInfo=\n\nA taxa de ofertante já está paga. No pior dos casos você perdeu essa taxa.\nPor favor tente recomeçar o seu programa e verificar a sua conexão de rede para ver se consegue resolver o problema. createOffer.tooLowSecDeposit.warning=Você definiu o depósito de segurança para um valor inferior ao valor padrão recomendado de {0}.\nTem certeza de que deseja usar um depósito de segurança inferior? createOffer.tooLowSecDeposit.makerIsSeller=Dá-te menos proteção caso o gteu par de negociação não siga o protocolo de negócio. createOffer.tooLowSecDeposit.makerIsBuyer=Dá menos proteção para o par de negociação que você segue o protocolo de negócio, pois você tem menos depósito em risco. Outros utilizadores podem preferir receber outras ofertas em vez da sua. createOffer.resetToDefault=Não, voltar ao valor padrão createOffer.useLowerValue=Sim, usar meu valor mais baixo createOffer.priceOutSideOfDeviation=O preço que você inseriu está fora do valor máx. de desvio permitido do preço de mercado.\nO máx. desvio permitido é {0} e pode ser ajustado nas preferências. createOffer.changePrice=Alterar preço createOffer.tac=Com a publicação dessa oferta, concordo em negociar com qualquer negociador que preencha as condições definidas nesse ecrã. createOffer.currencyForFee=Taxa de negócio createOffer.setDeposit=Definir o depósito de segurança do comprador (%) createOffer.setDepositAsBuyer=Definir o meu depósito de segurança enquanto comprador (%) createOffer.setDepositForBothTraders=Set both traders' security deposit (%) createOffer.securityDepositInfo=O depósito de segurança do seu comprador será {0} createOffer.securityDepositInfoAsBuyer=O seu depósito de segurança enquanto comprador será {0} createOffer.minSecurityDepositUsed=O depósito de segurança mínimo é utilizado createOffer.buyerAsTakerWithoutDeposit=Nenhum depósito exigido do comprador (protegido por frase secreta) createOffer.myDeposit=Meu depósito de segurança (%) createOffer.myDepositInfo=Seu depósito de segurança será {0} #################################################################### # Offerbook / Take offer #################################################################### takeOffer.amount.prompt=Insira a quantia de XMR takeOffer.amountPriceBox.buy.amountDescription=Quantia de XMR a vender takeOffer.amountPriceBox.sell.amountDescription=Quantia de XMR a comprar takeOffer.amountPriceBox.priceDescription=Preço por monero em {0} takeOffer.amountPriceBox.amountRangeDescription=Intervalo de quantia possível takeOffer.amountPriceBox.warning.invalidXmrDecimalPlaces=A quantia introduzida excede o número de casas décimas permitido.\nA quantia foi ajustada para 4 casas decimais. takeOffer.validation.amountSmallerThanMinAmount=A quantia não pode ser inferior à quantia mínima definida na oferta. takeOffer.validation.amountLargerThanOfferAmount=A quantia inserida não pode ser superior à quantia definida na oferta. takeOffer.validation.amountLargerThanOfferAmountMinusFee=Essa quantia inseria criaria troco poeira para o vendedor de XMR. takeOffer.fundsBox.title=Financiar o seu negócio takeOffer.fundsBox.isOfferAvailable=Verificar se a oferta está disponível ... takeOffer.fundsBox.tradeAmount=Quantia para vender takeOffer.fundsBox.offerFee=Taxa de negócio takeOffer.fundsBox.networkFee=Total de taxas de mineração takeOffer.fundsBox.takeOfferSpinnerInfo=Aceitando a oferta: {0} takeOffer.fundsBox.paymentLabel=negócio do Haveno com ID {0} takeOffer.fundsBox.fundsStructure=({0} depósito de segurança, {1} taxa de negócio, {2} taxa de mineração) takeOffer.fundsBox.noFundingRequiredTitle=Nenhum financiamento necessário takeOffer.fundsBox.noFundingRequiredDescription=Obtenha a senha da oferta com o vendedor fora do Haveno para aceitar esta oferta. takeOffer.success.headline=Você aceitou uma oferta com sucesso. takeOffer.success.info=Você pode ver o estado de seu negócio em \"Portefólio/Negócios abertos\". takeOffer.error.message=Ocorreu um erro ao aceitar a oferta .\n\n{0} # new entries takeOffer.takeOfferButton.buy=Revisar: Aceitar oferta para comprar XMR com {0} takeOffer.takeOfferButton.sell=Revisar: Aceitar oferta para vender XMR por {0} takeOffer.noPriceFeedAvailable=Você não pode aceitar aquela oferta pois ela utiliza uma percentagem do preço baseada no preço de mercado, mas o feed de preços está indisponível no momento. takeOffer.takeOfferFundWalletInfo.headline=Financiar seu negócio # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Quantia de negócio: {0} \n takeOffer.takeOfferFundWalletInfo.msg=Você precisa depositar {0} para aceitar esta oferta.\n\nO valor é a soma de:\n{1}- Seu depósito de segurança: {2}\n- Taxa de negociação: {3} takeOffer.alreadyPaidInFunds=Se você já pagou com seus fundos você pode levantá-los na janela \"Fundos/Enviar fundos\". takeOffer.paymentInfo=Informações de pagamento takeOffer.setAmountPrice=Definir quantia takeOffer.alreadyFunded.askCancel=Você já financiou essa oferta.\nSe você cancelar agora, seus fundos permanecerão na sua carteira Haveno local e estarão disponíveis para retirada na tela \"Fundos/Enviar fundos\".\nTem certeza de que deseja cancelar? takeOffer.failed.offerNotAvailable=Pedido para aceitar oferta falhou porque a oferta já não está disponível. Talvez um outro negociador aceitou a oferta entretanto. takeOffer.failed.offerTaken=Não é possível aceitar a oferta pois ela já foi aceita por outro negociador. takeOffer.failed.offerRemoved=Não é possível aceitar a oferta pois ela foi removida. takeOffer.failed.offererNotOnline=Aceitação da oferta falhou pois o ofertante já não está online. takeOffer.failed.offererOffline=Não pode aceitar a oferta porque o ofertante está offline. takeOffer.warning.connectionToPeerLost=Perdeu a conexão ao ofertante.\nEle pode ter ficado offline ou fechado a conexão consigo devido à demasiadas conexões abertas.\n\nSe ainda consegue ver a sua oferta no livro de ofertas pode tentar aceitar a oferta de novo. takeOffer.error.noFundsLost=\n\nAinda não saíram nenhuns fundos da sua carteira.\nPor favor, tente reiniciar o seu programa e verifique sua conexão de rede para ver se você pode resolver o problema. # suppress inspection "TrailingSpacesInProperty" takeOffer.error.feePaid=\n\n takeOffer.error.depositPublished=\n\nA transação de depósito já está publicada.\nPor favor, tente reiniciar o seu programa e verifique sua conexão de rede para ver se você pode resolver o problema.\nSe o problema persistir, por favor contacte os desenvolvedores para obter apoio. takeOffer.error.payoutPublished=\n\nA transação de depósito já está publicada.\nPor favor, tente reiniciar o seu programa e verifique sua conexão de rede para ver se você pode resolver o problema.\nSe o problema persistir, por favor contacte os desenvolvedores para obter apoio. takeOffer.tac=Ao aceitar esta oferta, eu concordo com as condições de negócio definidas neste ecrã. #################################################################### # Offerbook / Edit offer #################################################################### openOffer.header.triggerPrice=Preço de desencadeamento openOffer.triggerPrice=Trigger price {0} openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\nPlease edit the offer to define a new trigger price editOffer.setPrice=Definir preço editOffer.confirmEdit=Confirmar: Editar oferta editOffer.publishOffer=Publicando sua oferta. editOffer.failed=A edição da oferta falhou:\n{0} editOffer.success=Sua oferta foi editada com sucesso. editOffer.invalidDeposit=O depósito de segurança do comprador não está dentro dos limites definidos pela OAD do Haveno e não pode mais ser editado. #################################################################### # Portfolio #################################################################### portfolio.tab.openOffers=As minhas ofertas abertas portfolio.tab.pendingTrades=Negócios abertos portfolio.tab.history=Histórico portfolio.tab.failed=Falhou portfolio.tab.editOpenOffer=Editar oferta portfolio.closedTrades.deviation.help=Percentage price deviation from market portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} portfolio.pending.syncing=Sincronizando a carteira de negociação portfolio.pending.syncing.blockRemaining=Sincronizando a carteira de negociação — 1 bloco restante portfolio.pending.syncing.blocksRemaining=Sincronizando a carteira de negociação — {0} blocos restantes portfolio.pending.step1.waitForConf=Esperando confirmação da blockchain portfolio.pending.step2_buyer.additionalConf=Os depósitos alcançaram 10 confirmações.\nPara maior segurança, recomendamos aguardar {0} confirmações antes de enviar o pagamento.\nProceda antecipadamente por sua própria conta e risco. portfolio.pending.step2_buyer.startPayment=Iniciar pagamento portfolio.pending.step2_seller.waitPaymentSent=Aguardar até que o pagamento inicie portfolio.pending.step3_buyer.waitPaymentArrived=Aguardar até que o pagamento chegue portfolio.pending.step3_seller.confirmPaymentReceived=Confirmar pagamento recebido portfolio.pending.step5.completed=Concluído portfolio.pending.step3_seller.autoConf.status.label=Auto-confirm status portfolio.pending.autoConf=Auto-confirmed portfolio.pending.autoConf.blocks=XMR confirmations: {0} / Required: {1} portfolio.pending.autoConf.state.xmr.txKeyReused=Transaction key re-used. Please open a dispute. portfolio.pending.autoConf.state.confirmations=XMR confirmations: {0}/{1} portfolio.pending.autoConf.state.txNotFound=Transaction not seen in mem-pool yet portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=No valid transaction ID / transaction key portfolio.pending.autoConf.state.filterDisabledFeature=Disabled by developers. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FEATURE_DISABLED=Auto-confirm feature is disabled. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=Trade amount exceeds auto-confirm amount limit # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.INVALID_DATA=Peer provided invalid data. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=Payout transaction was already published. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.DISPUTE_OPENED=Dispute was opened. Auto-confirm is deactivated for that trade. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.REQUESTS_STARTED=Transaction proof requests started # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PENDING=Success results: {0}/{1}; {2} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.COMPLETED=Proof at all services succeeded # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. No auto-confirm possible. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. portfolio.pending.step1.info.you=A transação de depósito foi publicada.\nVocê pode iniciar o pagamento após 10 confirmações (aprox. {0} minutos restantes). portfolio.pending.step1.info.buyer=A transação de depósito foi publicada.\nO comprador de XMR pode iniciar o pagamento após 10 confirmações (aprox. {0} minutos restantes). portfolio.pending.step1.warn=A transação de depósito ainda não foi confirmada. Isso pode acontecer em casos raros, quando a taxa de financiamento de um negociador proveniente de uma carteira externa foi muito baixa. portfolio.pending.step1.openForDispute=A transação de depósito ainda não foi confirmada. Você pode esperar mais tempo ou entrar em contato com o mediador para obter assistência. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2.confReached=Your trade has reached at least one blockchain confirmation.\n\n portfolio.pending.step2_buyer.refTextWarn=Important: when making the payment, leave the \"reason for payment\" field empty. DO NOT put the trade ID or any other text like 'monero', 'XMR', or 'Haveno'. You are free to discuss via trader chat if an alternate \"reason for payment\" would be suitable to you both. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees=If your bank charges you any fees to make the transfer, you are responsible for paying those fees. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.crypto=Por favor transfira da sua carteira externa {0}\n{1} para o vendedor de.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.cash=Por favor vá à um banco e pague{0} ao vendedor de XMR.\n\n portfolio.pending.step2_buyer.cash.extra=REQUERIMENTO IMPORTANTE:\nDepois de ter feito o pagamento escreva no recibo de papel: SEM REEMBOLSOS.\nEm seguida, rasgue-o em 2 partes, tire uma foto e envie-a para o endereço de e-mail do vendedor de XMR. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.moneyGram=Por favor pague {0} ao vendedor de XMR usando MoneyGram.\n\n portfolio.pending.step2_buyer.moneyGram.extra=REQUERIMENTO IMPORTANTE:\nDepois de ter feito o pagamento envie o Número de autorização e uma foto do recibo por email para o vendedor de XMR\nO recibo deve mostrar claramente o nome completo do vendedor, o país, o estado e a quantia. O email do vendedor é: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.westernUnion=Por favor pague {0} ao vendedor de XMR usando Western Union.\n\n portfolio.pending.step2_buyer.westernUnion.extra=REQUISITO IMPORTANTE:\nDepois de ter feito o pagamento, envie o MTCN (número de rastreamento) e uma foto do recibo por e-mail para o vendedor de XMR.\nO recibo deve mostrar claramente o nome completo do vendedor, a cidade, o país e a quantia. O e-mail do vendedor é: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Por favor envie {0} por \"US Postal Money Order\" para o vendedor de XMR.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Por favor contacte o vendedor de XMR pelo contacto fornecido e marque um encontro para pagar {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Iniciar pagamento usando {0} portfolio.pending.step2_buyer.recipientsAccountData=Recipients {0} portfolio.pending.step2_buyer.amountToTransfer=Quantia a transferir portfolio.pending.step2_buyer.sellersAddress=Endereço {0} do vendedor portfolio.pending.step2_buyer.buyerAccount=A sua conta de pagamento a ser usada portfolio.pending.step2_buyer.paymentSent=Pagamento iniciado portfolio.pending.step2_buyer.showEarly=Mostrar os detalhes de pagamento antecipadamente portfolio.pending.step2_buyer.warn=Você ainda não fez o seu pagamento de {0}!\nSaiba que o negócio tem de ser concluído até {1}. portfolio.pending.step2_buyer.openForDispute=Você não completou o seu pagamento!\nO período máx. para o negócio acabou. Por favor entre em contacto com o mediador para assistência. portfolio.pending.step2_buyer.paperReceipt.headline=Você enviou o recibo de papel para o vendedor de XMR? portfolio.pending.step2_buyer.paperReceipt.msg=Lembre-se:\nPrecisa escrever no recibo de papel: SEM REEMBOLSOS.\nEm seguida, rasgue-o em 2 partes, tire uma foto e envie-a para o endereço de e-mail do vendedor de XMR. portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Enviar Número de autorização e recibo portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=Você precisa enviar o Número de Autorização e uma foto do recibo por e-mail para o vendedor de XMR.\nO recibo deve mostrar claramente o nome completo do vendedor, o país, o estado e a quantia. O e-mail do vendedor é: {0}.\n\nVocê enviou o Número de Autorização e o contrato para o vendedor? portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=Enviar MTCN e recibo portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Você precisa enviar o MTCN (número de rastreamento) e uma foto do recibo por e-mail para o vendedor de XMR.\nO recibo deve mostrar claramente o nome completo do vendedor, a cidade, o país e a quantia. O e-mail do vendedor é: {0}.\n\nVocê enviou o MTCN e o contrato para o vendedor? portfolio.pending.step2_buyer.halCashInfo.headline=Enviar o código HalCash portfolio.pending.step2_buyer.halCashInfo.msg=Você precisa enviar uma mensagem de texto com o código HalCash, bem como o ID da negociação ({0}) para o vendedor XMR.\nO nº do telemóvel do vendedor é {1}.\n\nVocê enviou o código para o vendedor? portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Some banks might verify the receiver's name. Faster Payments accounts created in old Haveno clients do not provide the receiver's name, so please use trade chat to obtain it (if needed). portfolio.pending.step2_buyer.confirmStart.headline=Confirme que você iniciou o pagamento portfolio.pending.step2_buyer.confirmStart.msg=Você iniciou o pagamento de {0} para o seu parceiro de negociação? portfolio.pending.step2_buyer.confirmStart.yes=Sim, iniciei o pagamento portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=You have not provided proof of payment portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=You have not entered the transaction ID and the transaction key.\n\nBy not providing this data the peer cannot use the auto-confirm feature to release the XMR as soon the XMR has been received.\nBeside that, Haveno requires that the sender of the XMR transaction is able to provide this information to the mediator or arbitrator in case of a dispute.\nSee more details on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=Input is not a 32 byte hexadecimal value portfolio.pending.step2_buyer.confirmStart.warningButton=Ignore and continue anyway portfolio.pending.step2_seller.waitPayment.headline=Aguardar o pagamento portfolio.pending.step2_seller.f2fInfo.headline=Informação do contacto do comprador portfolio.pending.step2_seller.waitPayment.msg=A transação de depósito tem pelo menos uma confirmação da blockchain.\nVocê precisa esperar até que o comprador de XMR inicie o pagamento {0}. portfolio.pending.step2_seller.warn=O comprador do XMR ainda não efetuou o pagamento de {0}.\nVocê precisa esperar até que eles tenham iniciado o pagamento.\nSe o negócio não for concluído em {1}, o árbitro irá investigar. portfolio.pending.step2_seller.openForDispute=O comprador de XMR não iniciou o seu pagamento!\nO período máx. permitido para o negócio acabou.\nVocê pode esperar e dar mais tempo ao seu par de negociação ou entrar em contacto com o mediador para assistência. tradeChat.chatWindowTitle=Janela de chat para o negócio com o ID '{0}' tradeChat.openChat=Abrir janela de chat tradeChat.rules=Você pode comunicar com o seu par de negociação para resolver problemas com este negócio.\nNão é obrigatório responder no chat.\nSe algum negociante infringir alguma das regras abaixo, abra uma disputa e reporte-o ao mediador ou ao árbitro.\n\nRegras do chat:\n\t● Não envie nenhum link (risco de malware). Você pode enviar o ID da transação e o nome de um explorador de blocos.\n\t● Não envie as suas palavras-semente, chaves privadas, senhas ou outra informação sensitiva!\n\t● Não encoraje negócios fora do Haveno (sem segurança).\n\t● Não engaje em nenhuma forma de scams de engenharia social.\n\t● Se um par não responde e prefere não comunicar pelo chat, respeite a sua decisão.\n\t● Mantenha o âmbito da conversa limitado ao negócio. Este chat não é um substituto para o messenger ou uma caixa para trolls.\n\t● Mantenha a conversa amigável e respeitosa. # suppress inspection "UnusedProperty" message.state.UNDEFINED=Indefinido # suppress inspection "UnusedProperty" message.state.SENT=Mensagem enviada # suppress inspection "UnusedProperty" message.state.ARRIVED=A mensagem chegou ao par # suppress inspection "UnusedProperty" message.state.STORED_IN_MAILBOX=Mensagem de pagamento enviada mais ainda não recebida pelo par # suppress inspection "UnusedProperty" message.state.ACKNOWLEDGED=O par confirmou a recepção da mensagem # suppress inspection "UnusedProperty" message.state.FAILED=Falha de envio de mensagem portfolio.pending.step3_buyer.wait.headline=Aguarde confirmação de pagamento do vendedor de XMR. portfolio.pending.step3_buyer.wait.info=Aguardando confirmação do vendedor de XMR para o recibo do pagamento de {0}. portfolio.pending.step3_buyer.wait.msgStateInfo.label=Pagamento iniciado mensagem de estado portfolio.pending.step3_buyer.warn.part1a=na blockchain {0} portfolio.pending.step3_buyer.warn.part1b=no seu provedor de pagamentos (ex: banco) portfolio.pending.step3_buyer.warn.part2=O vendedor de XMR ainda não confirmou o seu pagamento. Por favor confirme se o envio do pagamento de {0} foi bem-sucedido. portfolio.pending.step3_buyer.openForDispute=O vendedor de Haveno não confirmou o seu pagamento! O período máx. para o negócio acabou. Você pode esperar e dar mais tempo ao seu par de negociação or pedir assistência de um mediador. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.part=O seu parceiro de negociação confirmou que começou o pagamento de {0}.\n\n portfolio.pending.step3_seller.crypto.explorer=no seu explorador de blockchain de {0} favorito portfolio.pending.step3_seller.crypto.wallet=na sua carteira de {0} portfolio.pending.step3_seller.crypto={0} Por favor verifique {1} se a transação para o seu endereço recipiente\n{2}\njá possui confirmações suficientes da blockchain.\nA quantia de pagamento deve ser {3}\n\nVocê pode copiar e colar o seu endereço {4} do ecrã principal depois de fechar o pop-up. portfolio.pending.step3_seller.postal={0}Please check if you have received {1} with \"US Postal Money Order\" from the XMR buyer. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.payByMail={0}Please check if you have received {1} with \"Pay by Mail\" from the XMR buyer. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.bank=Your trading partner has confirmed that they have initiated the {0} payment.\n\nPlease go to your online banking web page and check if you have received {1} from the XMR buyer. portfolio.pending.step3_seller.cash=Como o pagamento é feito via Depósito em Dinheiro, o comprador do XMR deve escrever "SEM REEMBOLSO" no recibo de papel, rasgá-lo em 2 partes e enviar uma foto por e-mail.\n\nPara evitar o risco de estorno, confirme apenas se você recebeu o e-mail e se tiver certeza de que o recibo de papel é válido.\nSe você não tiver certeza, {0} portfolio.pending.step3_seller.moneyGram=O comprador deve enviar o Número de Autorização e uma foto do recibo por e-mail.\nO recibo deve mostrar claramente o seu nome completo, país, estado e a quantia. Por favor verifique seu e-mail se recebeu o Número de Autorização.\n\nDepois de fechar esse pop-up, verá o nome e o endereço do comprador do XMR para levantar o dinheiro da MoneyGram.\n\nConfirme apenas o recebimento depois de ter conseguido o dinheiro com sucesso! portfolio.pending.step3_seller.westernUnion=O comprador deve enviar-lhe o MTCN (número de rastreamento) e uma foto do recibo por e-mail.\nO recibo deve mostrar claramente seu nome completo, cidade, país e a quantia Por favor verifique no seu e-mail se você recebeu o MTCN.\n\nDepois de fechar esse pop-up, você verá o nome e endereço do comprador de XMR para levantar o dinheiro da Western Union.\n\nConfirme apenas o recebimento depois de ter conseguido o dinheiro com sucesso! portfolio.pending.step3_seller.halCash=O comprador deve-lhe enviar o código HalCash como mensagem de texto. Além disso, você receberá uma mensagem do HalCash com as informações necessárias para retirar o EUR de uma ATM que suporte o HalCash.\n\nDepois de levantar o dinheiro na ATM, confirme aqui o recibo do pagamento! portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon eGift Card by email or by text message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted confirm the payment receipt. portfolio.pending.step3_seller.bankCheck=\n\nVerifique também se o nome do remetente especificado no contrato de negócio corresponde ao nome que aparece no seu extrato bancário:\nNome do remetente, por contrato de negócio: {0}\n\nSe os nomes não forem exatamente iguais, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=não confirme a recepção do pagamento. Em vez disso, abra uma disputa pressionando \"alt + o\" ou \"option + o\".\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=Confirmar recibo de pagamento portfolio.pending.step3_seller.amountToReceive=Quantia a receber portfolio.pending.step3_seller.yourAddress=Seu endereço de {0} portfolio.pending.step3_seller.buyersAddress=Endereço de {0} do comprador: portfolio.pending.step3_seller.yourAccount=Sua conta de negociação portfolio.pending.step3_seller.xmrTxHash=ID de transação portfolio.pending.step3_seller.xmrTxKey=Transaction key portfolio.pending.step3_seller.buyersAccount=Buyers account data portfolio.pending.step3_seller.confirmReceipt=Confirmar recibo de pagamento portfolio.pending.step3_seller.buyerStartedPayment=O comprador de XMR começou o pagamento de {0}.\n{1} portfolio.pending.step3_seller.buyerStartedPayment.crypto=Verifique as confirmações da blockchain na sua carteira crypto ou explorador de blocos e confirme o pagamento quando houverem confirmações da blockchain suficientes. portfolio.pending.step3_seller.buyerStartedPayment.traditional=Verifique em sua conta de negociação (por exemplo, sua conta bancária) e confirme que recebeu o pagamento. portfolio.pending.step3_seller.warn.part1a=na blockchain {0} portfolio.pending.step3_seller.warn.part1b=em seu provedor de pagamentos (ex: banco) portfolio.pending.step3_seller.warn.part2=Você ainda não confirmou a receção do pagamento. Por favor verifique {0} se você recebeu o pagamento. portfolio.pending.step3_seller.openForDispute=Você não confirmou a receção do pagamento!\nO período máx. para o negócio acabou.\nPor favor confirme ou peça assistência do mediador. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=Você recebeu o pagamento de {0} do seu parceiro de negociação?\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.name=Verifique também se o nome do remetente especificado no contrato de negócio corresponde ao nome que aparece no seu extrato bancário:\nNome do remetente, por contrato de negócio: {0}\n\nSe os nomes não forem exatamente iguais, não confirme a recepção do pagamento. Em vez disso, abra uma disputa pressionando \"alt + o\" ou \"option + o\".\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Observe que, assim que você confirmar a recepção, o valor da transação bloqueada será liberado para o comprador de XMR e o depósito de segurança será reembolsado.\n\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Confirme que recebeu o pagamento portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=Sim, eu recebi o pagamento portfolio.pending.step3_seller.onPaymentReceived.signer=IMPORTANTE: Ao confirmar a recepção do pagamento, você também está verificando a conta da contraparte e assinando-a. Como a conta da contraparte ainda não foi assinada, você deve adiar a confirmação do pagamento o máximo possível para reduzir o risco de estorno. portfolio.pending.step5_buyer.groupTitle=Resumo do negócio completo portfolio.pending.step5_buyer.tradeFee=Taxa de negócio portfolio.pending.step5_buyer.makersMiningFee=Taxa de mineração portfolio.pending.step5_buyer.takersMiningFee=Total das taxas de mineração portfolio.pending.step5_buyer.refunded=Depósito de segurança reembolsado portfolio.pending.step5_buyer.withdrawXMR=Levantar seus moneros portfolio.pending.step5_buyer.amount=Quantia a levantar portfolio.pending.step5_buyer.withdrawToAddress=Levantar para o endereço portfolio.pending.step5_buyer.moveToHavenoWallet=Keep funds in Haveno wallet portfolio.pending.step5_buyer.withdrawExternal=Levantar para carteira externa portfolio.pending.step5_buyer.alreadyWithdrawn=Seus fundos já foram levantados.\nPor favor, verifique o histórico de transações. portfolio.pending.step5_buyer.confirmWithdrawal=Confirmar solicitação de levantamento portfolio.pending.step5_buyer.amountTooLow=A quantia a ser transferida é inferior à taxa de transação e o mín. valor de transação possível (poeira). portfolio.pending.step5_buyer.withdrawalCompleted.headline=Levantamento completado portfolio.pending.step5_buyer.withdrawalCompleted.msg=Os seus negócios concluídos são armazenadas em \ "Portefólio/Histórico\".\nVocê pode analisar todas as suas transações de monero em \"Fundos/Transações\" portfolio.pending.step5_buyer.bought=Você comprou portfolio.pending.step5_buyer.paid=Você pagou portfolio.pending.step5_seller.sold=Você vendeu portfolio.pending.step5_seller.received=Você recebeu tradeFeedbackWindow.title=Felicitações por completar o seu negócio tradeFeedbackWindow.msg.part1=Adoraríamos ouvir sobre sua experiência. Isso nos ajudará a aperfeiçoar o software e a suavizar as arestas. Se você gostaria de fornecer feedback, preencha este pequeno questionário (sem necessidade de registo) em: tradeFeedbackWindow.msg.part2=Se tiver alguma dúvida ou algum problema, entre em contacto com outros usuários e colaboradores através do fórum Haveno em: tradeFeedbackWindow.msg.part3=Obrigado por usar Haveno! portfolio.pending.role=O meu cargo portfolio.pending.tradeInformation=Informação do negócio portfolio.pending.remainingTime=Tempo restante portfolio.pending.remainingTimeDetail={0} (até {1}) portfolio.pending.remainingTimeDetail.startsAfter=Começa após {0} confirmações portfolio.pending.tradePeriodInfo=Após {0} confirmações, o período de negociação começa. Com base no método de pagamento utilizado, aplica-se um período máximo de negociação diferente. portfolio.pending.tradePeriodWarning=Se o período é excedido ambos os negociadores podem abrir disputa. portfolio.pending.tradeNotCompleted=Negócio não completo à tempo (até {0}) portfolio.pending.tradeProcess=Processo de negócio portfolio.pending.openAgainDispute.msg=If you are not sure that the message to the mediator or arbitrator arrived (e.g. if you did not get a response after 1 day) feel free to open a dispute again with Cmd/Ctrl+o. You can also ask for additional help on the Haveno forum at [HYPERLINK:https://haveno.community]. portfolio.pending.openAgainDispute.button=Abrir disputa novamente portfolio.pending.openSupportTicket.headline=Abrir bilhete de apoio portfolio.pending.openSupportTicket.msg=Por favor, use esta função apenas em casos de emergência, se você não vir o botão \"Abrir apoio\" ou \"Abrir disputa\".\n\nQuando você abre um bilhete de apoio, o negócio será interrompido e tratado por um mediador ou árbitro. portfolio.pending.timeLockNotOver=Você deve esperar ≈{0} (mais {1} blocos) antes que você possa abrir uma disputa de arbitragem. portfolio.pending.error.depositTxNull=A transação de depósito é null. Você não pode abrir a disputa sem uma transação de depósito válida. Por favor vá à \"Definições/Informação da Rede\" e re-sincronize o ficheiro SPV.\n\nPara mais ajuda por favor contacte o canal de apoio do Haveno na equipa Keybase do Haveno. portfolio.pending.mediationResult.error.depositTxNull=The deposit transaction is null. You can move the trade to failed trades. portfolio.pending.mediationResult.error.delayedPayoutTxNull=The delayed payout transaction is null. You can move the trade to failed trades. portfolio.pending.error.depositTxNotConfirmed=A transação de depósito não foi confirmada. Você pode abrir uma disputa de arbitragem com uma transação de depósito não confirmada. Por favor espere até que seja confirmada ou vá à \"Definições/Informação da Rede\" e re-sincronize o ficheiro SPV.\n\nPara mais ajuda por favor contacte o canal de apoio do Haveno na equipa Keybase do Haveno. portfolio.pending.support.headline.getHelp=Precisa de ajuda? portfolio.pending.support.text.getHelp=Se tiver algum problema você pode tentar contactar o par de negociação no chat do negócio or perguntar à comunidade do Haveno em https://haveno.community. Se o seu problema ainda não for resolvido, você pode pedir mais ajuda à um mediador. portfolio.pending.support.button.getHelp=Open Trader Chat portfolio.pending.support.headline.halfPeriodOver=Verificar o pagamento portfolio.pending.support.headline.periodOver=O período de negócio acabou portfolio.pending.support.headline.depositTxMissing=Transação de depósito ausente portfolio.pending.support.depositTxMissing=Está faltando uma transação de depósito para esta negociação. Abra um ticket de suporte para contatar um árbitro para assistência. portfolio.pending.mediationRequested=Mediação solicitada portfolio.pending.refundRequested=Reembolso pedido portfolio.pending.openSupport=Abrir bilhete de apoio portfolio.pending.supportTicketOpened=Bilhete de apoio aberto portfolio.pending.communicateWithArbitrator=Por favor comunique no ecrã \"Apoio\" com o árbitro. portfolio.pending.communicateWithMediator=Por favor comunique com o mediador no ecrã \"Apoio\". portfolio.pending.disputeOpenedByUser=Você já abriu uma disputa.\n{0} portfolio.pending.disputeOpenedByPeer=O seu par de negociação abriu uma disputa\n{0} portfolio.pending.noReceiverAddressDefined=Nenhum endereço de recipiente definido portfolio.pending.mediationResult.headline=Pagamento sugerido pela mediação portfolio.pending.mediationResult.info.noneAccepted=Conclua o negócio aceitando a sugestão do mediador para o pagamento do negócio. portfolio.pending.mediationResult.info.selfAccepted=Você aceitou a sugestão do mediador. À espera que o par também a aceite. portfolio.pending.mediationResult.info.peerAccepted=O seu par de negócio aceitou a sugestão do mediador. Você também a aceita? portfolio.pending.mediationResult.button=Ver a resolução proposta portfolio.pending.mediationResult.popup.headline=Resultado da mediação para o negócio com o ID: {0} portfolio.pending.mediationResult.popup.headline.peerAccepted=O seu par de negócio aceitou a sugestão do mediador para o negócio {0} portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader's security deposit) as compensation for their work. Both traders agreeing to the mediator's suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator's suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=Rejeitar e solicitar arbitragem portfolio.pending.mediationResult.popup.alreadyAccepted=Você já aceitou portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. portfolio.pending.failedTrade.missingDepositTx=Uma transação de depósito está faltando.\n\nEssa transação é necessária para concluir a negociação. Certifique-se de que sua carteira esteja totalmente sincronizada com a blockchain do Monero.\n\nVocê pode mover esta negociação para a seção "Negociações com Falha" para desativá-la. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Move trade to failed trades portfolio.pending.failedTrade.warningIcon.tooltip=Click to open details about the issues of this trade portfolio.failed.revertToPending.popup=Do you want to move this trade to open trades? portfolio.failed.revertToPending=Move trade to open trades portfolio.closed.completed=Concluído portfolio.closed.ticketClosed=Arbitrado portfolio.closed.mediationTicketClosed=Mediado portfolio.closed.canceled=Cancelado portfolio.failed.Failed=Falhado portfolio.failed.unfail=Before proceeding, make sure you have a backup of your data directory!\nDo you want to move this trade back to open trades?\nThis is a way to unlock funds stuck in a failed trade. portfolio.failed.cantUnfail=This trade cannot be moved back to open trades at the moment. \nTry again after completion of trade(s) {0} portfolio.failed.depositTxNull=The trade cannot be reverted to a open trade. Deposit transaction is null. portfolio.failed.delayedPayoutTxNull=The trade cannot be reverted to a open trade. Delayed payout transaction is null. #################################################################### # Funds #################################################################### funds.tab.deposit=Receber fundos funds.tab.withdrawal=Enviar fundos funds.tab.reserved=Fundos reservados funds.tab.locked=Fundos bloqueados funds.tab.transactions=Transações funds.deposit.unused=Não utilizado funds.deposit.usedInTx=Utilizado em {0} transação(s) funds.deposit.fundHavenoWallet=Financiar carteira Haveno funds.deposit.noAddresses=Ainda não foi gerado um endereço de depósito funds.deposit.fundWallet=Financiar sua carteira funds.deposit.withdrawFromWallet=Enviar fundos da carteira funds.deposit.amount=Quantia em XMR (opcional) funds.deposit.generateAddress=Gerar um endereço novo funds.deposit.generateAddressSegwit=Native segwit format (Bech32) funds.deposit.selectUnused=Favor selecione um endereço não utilizado da tabela acima ao invés de gerar um novo. funds.withdrawal.arbitrationFee=Taxa de arbitragem funds.withdrawal.inputs=Seleção de inputs funds.withdrawal.useAllInputs=Usar todos os inputs disponíveis funds.withdrawal.useCustomInputs=Usar inputs personalizados funds.withdrawal.receiverAmount=Quantia do recipiente funds.withdrawal.senderAmount=Quantia do remetente funds.withdrawal.feeExcluded=A quantia exclui a taxa de mineração funds.withdrawal.feeIncluded=A quantia inclui a taxa de mineração funds.withdrawal.fromLabel=Levantar do endereço funds.withdrawal.toLabel=Levantar para o endereço funds.withdrawal.memoLabel=Withdrawal memo funds.withdrawal.memo=Optionally fill memo funds.withdrawal.withdrawButton=Levantar selecionados funds.withdrawal.noFundsAvailable=Não há fundos disponíveis para levantamento funds.withdrawal.confirmWithdrawalRequest=Confirmar pedido de levantamento funds.withdrawal.withdrawMultipleAddresses=Levantar de múltiplos endereços ({0}) funds.withdrawal.withdrawMultipleAddresses.tooltip=Levantar de múltiplos endereços:\n{0} funds.withdrawal.notEnoughFunds=Você não possui fundos suficientes na sua carteira. funds.withdrawal.selectAddress=Selecione um endereço de origem da tabela funds.withdrawal.setAmount=Defina quantia a levantar funds.withdrawal.fillDestAddress=Preencha seu endereço de destino funds.withdrawal.warn.noSourceAddressSelected=Você precisa selecionar um endereço de origem na tabela acima. funds.withdrawal.warn.amountExceeds=Você não tem fundos suficientes disponíveis no endereço selecionado.\nConsidere selecionar vários endereços na tabela acima ou alterar acima para incluir a taxa do mineiro. funds.reserved.noFunds=Não há fundos reservados em ofertas abertas funds.reserved.reserved=Reservado na carteira local par a oferte com o ID: {0} funds.locked.noFunds=Não há fundos bloqueados em negócios funds.locked.locked=Bloqueado em transação multi-assinatura para o negócio com o ID: {0} funds.tx.direction.sentTo=Enviado para: funds.tx.direction.receivedWith=Recebido com: funds.tx.direction.genesisTx=Da tx Genesis: funds.tx.createOfferFee=Taxa do ofertante e da tx: {0} funds.tx.takeOfferFee=Taxa do aceitador e da tx: {0} funds.tx.multiSigDeposit=Depósito multi-assinatura: {0} funds.tx.multiSigPayout=Pagamento multi-assinatura: {0} funds.tx.disputePayout=Pagamento de disputa: {0} funds.tx.disputeLost=Caso de disputa perdido: {0} funds.tx.collateralForRefund=Colateral do reembolso: {0} funds.tx.timeLockedPayoutTx=Tx de pagamento com trava temporal: {0} funds.tx.refund=Reembolso da arbitragem: {0} funds.tx.unknown=Razão desconhecida: {0} funds.tx.noFundsFromDispute=Nenhum reembolso de disputa funds.tx.receivedFunds=Fundos recebidos funds.tx.withdrawnFromWallet=Levantado da carteira funds.tx.memo=Memo funds.tx.noTxAvailable=Sem transações disponíveis funds.tx.revert=Reverter funds.tx.txSent=Transação enviada com sucesso para um novo endereço em sua carteira Haveno local. funds.tx.direction.self=Enviado à você mesmo funds.tx.dustAttackTx=Poeira recebida funds.tx.dustAttackTx.popup=Esta transação está enviando uma quantia muito pequena de XMR para a sua carteira e pode ser uma tentativa das empresas de análise da blockchain para espionar a sua carteira.\n\nSe você usar esse output em uma transação eles decobrirão que você provavelmente também é o proprietário de outros endereços (mistura de moedas).\n\nPara proteger sua privacidade a carteira Haveno ignora tais outputs de poeira para fins de consumo e no ecrã de saldo. Você pode definir a quantia limite a partir da qual um output é considerado poeira nas definições." #################################################################### # Support #################################################################### support.tab.mediation.support=Mediação support.tab.arbitration.support=Arbitragem support.tab.legacyArbitration.support=Arbitragem Antiga support.tab.ArbitratorsSupportTickets=Bilhetes de {0} support.sigCheck.button=Check signature support.sigCheck.popup.header=Verify dispute result signature support.sigCheck.popup.msg.label=Summary message support.sigCheck.popup.msg.prompt=Copy & paste summary message from dispute support.sigCheck.popup.result=Validation result support.sigCheck.popup.success=Signature is valid support.sigCheck.popup.failed=Signature verification failed support.sigCheck.popup.invalidFormat=Message is not of expected format. Copy & paste summary message from dispute. support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? support.reOpenButton.label=Re-open support.sendNotificationButton.label=Notificação privada support.reportButton.label=Report support.fullReportButton.label=All disputes support.noTickets=Não há bilhetes abertos support.sendingMessage=Enviando mensagem... support.receiverNotOnline=O recipiente não está online. A mensagem foi guardada na caixa de correio. support.sendMessageError=Falha de envio de mensagem. Erro: {0} support.receiverNotKnown=Receiver not known support.wrongVersion=A oferta nessa disputa foi criada com uma versão mais antiga do Haveno.\nVocê não pode fechar essa disputa com sua versão do programa.\n\nPor favor, use uma versão mais antiga com a versão do protocolo {0} support.openFile=Abrir ficheiro para anexar (tamanho máx.: {0} kb) support.attachmentTooLarge=O tamanho total de seus anexos anexados é {0} kb e excede o máximo permitido para mensagens de {1} kb. support.maxSize=O tamanho de ficheiro máx. permitido é {0} kB. support.attachment=Anexo support.tooManyAttachments=Você não pode enviar mais do que 3 anexos em uma mensagem. support.save=Guardar ficheiro no disco support.messages=Mensagens support.input.prompt=Inserir a mensagem... support.send=Enviar support.addAttachments=Adicionar anexos support.closeTicket=Fechar bilhete support.attachments=Anexos: support.savedInMailbox=Mensagem guardada na caixa de correio do recipiente. support.arrived=Mensagem chegou em seu destino. support.acknowledged=Chegada de mensagem confirmada pelo recipiente support.error=O recipiente não pode processar a mensagem. Erro: {0} support.buyerAddress=Endereço do comprador de XMR support.sellerAddress=Endereço do vendedor de XMR support.role=Cargo support.agent=Support agent support.state=Estado support.chat=Chat support.preparing=Preparando support.requested=Solicitado support.closed=Fechado support.open=Aberto support.process=Process support.buyerMaker=Comprador de XMR/Ofertante support.sellerMaker=Vendedor de XMR/Ofertante support.buyerTaker=Comprador de XMR/Aceitador support.sellerTaker=Vendedor de XMR/Aceitador support.backgroundInfo=Haveno não é uma empresa, portanto, lida com disputas de forma diferente.\n\nOs traders podem se comunicar dentro do aplicativo por meio de um chat seguro na tela de negociações abertas para tentar resolver as disputas por conta própria. Se isso não for suficiente, um árbitro avaliará a situação e decidirá um pagamento dos fundos da negociação. support.initialInfo=Digite uma descrição do seu problema no campo de texto abaixo. Adicione o máximo de informações possível para acelerar o tempo de resolução da disputa.\n\nAqui está uma lista do que você deve fornecer:\n● Se você é o comprador de XMR: Você fez a transferência da Fiat ou Crypto? Se sim, você clicou no botão 'pagamento iniciado' no programa?\n● Se você é o vendedor de XMR: Você recebeu o pagamento da Fiat ou Crypto? Se sim, você clicou no botão 'pagamento recebido' no programa?\n\t● Qual versão do Haveno você está usando?\n\t● Qual sistema operacional você está usando?\n\t ● Se você encontrou um problema com transações com falha, considere mudar para um novo diretório de dados.\n\t Às vezes, o diretório de dados é corrompido e leva a erros estranhos.\n\t Consulte: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nFamiliarize-se com as regras básicas do processo de disputa:\n\t● Você precisa responder às solicitações do {0} dentro de 2 dias.\n\t● Os mediadores respondem entre 2 dias. Os árbitros respondem dentro de 5 dias úteis.\n\t ● O período máximo para uma disputa é de 14 dias.\n\t ● Você precisa cooperar com o {1} e fornecer as informações solicitadas para justificar o seu caso.\n\t● Você aceitou as regras descritas no documento de disputa no contrato do usuário quando iniciou o programa.\n\nVocê pode ler mais sobre o processo de disputa em: {2} support.systemMsg=Mensagem do sistema: {0} support.youOpenedTicket=Você abriu um pedido para apoio.\n\n{0}\n\nHaveno versão: {1} support.youOpenedDispute=Você abriu um pedido para uma disputa.\n\n{0}\n\nHaveno versão: {1} support.youOpenedDisputeForMediation=Você solicitou mediação.\n\n{0}\n\nVersão Haveno: {1} support.peerOpenedTicket=O seu par de negociação solicitou suporte devido a problemas técnicos.\n\n{0}\n\nVersão Haveno: {1} support.peerOpenedDispute=O seu par de negociação solicitou uma disputa.\n\n{0}\n\nVersão Haveno: {1} support.peerOpenedDisputeForMediation=O seu par de negociação solicitou uma mediação.\n\n{0}\n\nVersão Haveno: {1} support.mediatorsDisputeSummary=System message: Mediator's dispute summary:\n{0} support.mediatorsAddress=Endereço do nó do mediador: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. support.info.disputeReOpened=Dispute ticket has been re-opened. #################################################################### # Settings #################################################################### settings.tab.preferences=Preferências settings.tab.network=Informação da rede settings.tab.about=Sobre setting.preferences.general=Preferências gerais setting.preferences.explorer=Monero Explorer setting.preferences.deviation=Máx. desvio do preço de mercado setting.preferences.avoidStandbyMode=Evite o modo espera setting.preferences.useSoundForNotifications=Reproduzir sons para notificações setting.preferences.autoConfirmXMR=XMR auto-confirm setting.preferences.autoConfirmEnabled=Enabled setting.preferences.autoConfirmRequiredConfirmations=Required confirmations setting.preferences.autoConfirmMaxTradeSize=Max. trade amount (XMR) setting.preferences.autoConfirmServiceAddresses=Monero Explorer URLs (uses Tor, except for localhost, LAN IP addresses, and *.local hostnames) setting.preferences.deviationToLarge=Valores acima de {0}% não são permitidos. setting.preferences.txFee=Withdrawal transaction fee (satoshis/vbyte) setting.preferences.useCustomValue=Usar valor personalizado setting.preferences.txFeeMin=Transaction fee must be at least {0} satoshis/vbyte setting.preferences.txFeeTooLarge=Your input is above any reasonable value (>5000 satoshis/vbyte). Transaction fee is usually in the range of 50-400 satoshis/vbyte. setting.preferences.ignorePeers=Pares ignorados [endereço onion:porta] setting.preferences.ignoreDustThreshold=Mín. valor de output não-poeira setting.preferences.currenciesInList=Moedas na lista de feed de preço de mercado setting.preferences.prefCurrency=Moeda preferrida setting.preferences.displayTraditional=Mostrar moedas nacionais setting.preferences.noTraditional=Não há moedas nacionais selecionadas setting.preferences.cannotRemovePrefCurrency=Você não pode remover a moeda preferida de exibição selecionada setting.preferences.displayCryptos=Mostrar cryptos setting.preferences.noCryptos=Não há cryptos selecionadas setting.preferences.addTraditional=Adicionar moeda nacional setting.preferences.addCrypto=Adicionar crypto setting.preferences.displayOptions=Mostrar opções setting.preferences.showOwnOffers=Mostrar as minhas próprias ofertas no livro de ofertas setting.preferences.useAnimations=Usar animações setting.preferences.useDarkMode=Usar o modo escuro setting.preferences.useLightMode=Usar modo claro setting.preferences.sortWithNumOffers=Ordenar listas de mercado por nº de ofertas/negociações: setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API setting.preferences.notifyOnPreRelease=Receive pre-release notifications setting.preferences.resetAllFlags=Reiniciar todos os marcadores \"Não mostrar novamente\" settings.preferences.languageChange=Para aplicar a mudança de língua em todas os ecrãs requer uma reinicialização. settings.preferences.supportLanguageWarning=Em caso de disputa, por favor saiba que a arbitragem será tratada em {0}. settings.preferences.editCustomExplorer.headline=Explorer Settings settings.preferences.editCustomExplorer.description=Choose a system defined explorer from the list on the left, and/or customize to suit your own preferences. settings.preferences.editCustomExplorer.available=Available explorers settings.preferences.editCustomExplorer.chosen=Chosen explorer settings settings.preferences.editCustomExplorer.name=Nome settings.preferences.editCustomExplorer.txUrl=Transaction URL settings.preferences.editCustomExplorer.addressUrl=Address URL setting.info.headline=Nova funcionalidade de privacidade de dados settings.preferences.sensitiveDataRemoval.msg=Para proteger a privacidade sua e de outros negociadores, o Haveno pretende remover dados sensíveis de negociações antigas. Isso é particularmente importante para negociações com moedas fiduciárias que podem incluir detalhes de conta bancária.\n\nRecomenda-se definir o prazo o mais baixo possível, por exemplo, 60 dias. Isso significa que negociações com mais de 60 dias terão os dados sensíveis removidos, desde que estejam concluídas. Negociações concluídas podem ser encontradas na aba Portfólio / Histórico. settings.net.xmrHeader=Rede Monero settings.net.p2pHeader=Rede do Haveno settings.net.onionAddressLabel=O meu endereço onion settings.net.xmrNodesLabel=Usar nós de Monero personalizados settings.net.moneroPeersLabel=Pares conectados settings.net.connection=Conexão settings.net.connected=Conectado settings.net.useTorForXmrJLabel=Usar Tor para a rede de Monero settings.net.moneroNodesLabel=Nós de Monero para conectar settings.net.useProvidedNodesRadio=Usar nós de Monero Core providenciados settings.net.usePublicNodesRadio=Usar rede de Monero pública settings.net.useCustomNodesRadio=Usar nós de Monero Core personalizados settings.net.warn.usePublicNodes=If you use public Monero nodes, you are subject to any risk of using untrusted remote nodes.\n\nPlease read more details at [HYPERLINK:https://www.getmonero.org/resources/moneropedia/remote-node.html].\n\nAre you sure you want to use public nodes? settings.net.warn.usePublicNodes.useProvided=Não, usar nós providenciados settings.net.warn.usePublicNodes.usePublic=Sim, usar a rede pública settings.net.warn.useCustomNodes.B2XWarning=Por favor, certifique-se de que seu nó Monero é um nó confiável do Monero Core!\n\nConectar-se a nós que não seguem as regras de consenso do Monero Core pode corromper a sua carteira e causar problemas no processo de negócio.\n\nOs usuários que se conectam a nós que violam regras de consenso são responsáveis por qualquer dano resultante. Quaisquer disputas resultantes serão decididas em favor do outro par. Nenhum suporte técnico será dado aos usuários que ignorarem esses alertas e mecanismos de proteção! settings.net.warn.invalidXmrConfig=A conexão à rede Monero falhou porque sua configuração é inválida.\n\nA sua configuração foi redefinida para usar os nós de Monero fornecidos. Você precisará reiniciar o programa. settings.net.localhostXmrNodeInfo=Background information: Haveno looks for a local Monero node when starting. If it is found, Haveno will communicate with the Monero network exclusively through it. settings.net.p2PPeersLabel=Pares conectados settings.net.onionAddressColumn=Endereço onion settings.net.creationDateColumn=Estabelecida settings.net.connectionTypeColumn=Entrando/Saindo settings.net.sentDataLabel=Sent data statistics settings.net.receivedDataLabel=Received data statistics settings.net.chainHeightLabel=Latest XMR block height settings.net.roundTripTimeColumn=Ida-e-volta settings.net.sentBytesColumn=Enviado settings.net.receivedBytesColumn=Recebido settings.net.peerTypeColumn=Tipo de par settings.net.openTorSettingsButton=Abrir definições de Tor settings.net.versionColumn=Versão settings.net.subVersionColumn=Sub-versão settings.net.heightColumn=Altura settings.net.needRestart=Você precisa reiniciar o programa para aplicar essa alteração.\nVocê quer fazer isso agora?? settings.net.notKnownYet=Ainda desconhecido... settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec settings.net.chainHeight=Monero Peers chain height: {0} settings.net.ips=[endereço IP:porta | nome de host:porta | endereço onion:porta] (separado por vírgula). A porta pode ser omitida se a padrão for usada (8333). settings.net.seedNode=Nó semente settings.net.directPeer=Par (direto) settings.net.initialDataExchange={0} [Bootstrapping] settings.net.peer=Par settings.net.inbound=entrante settings.net.outbound=sainte setting.about.aboutHaveno=Sobre Haveno setting.about.about=O Haveno é um software de código aberto que facilita a troca de moneros com moedas nacionais (e outras criptomoedas) por meio de uma rede par-à-par descentralizada, de uma maneira que protege fortemente a privacidade do utilizador. Saiba mais sobre o Haveno na nossa página web do projeto. setting.about.web=página da web do Haveno setting.about.code=Código fonte setting.about.agpl=Licença AGPL setting.about.support=Apoio Haveno setting.about.def=O Haveno não é uma empresa - é um projeto aberto à comunidade. Se você quiser participar ou apoiar o Haveno, siga os links abaixo. setting.about.contribute=Contribuir setting.about.providers=Provedores de dados setting.about.apisWithFee=Haveno uses Haveno Price Indices for Fiat and Crypto market prices, and Haveno Mempool Nodes for mining fee estimation. setting.about.apis=Haveno uses Haveno Price Indices for Fiat and Crypto market prices. setting.about.pricesProvided=Preços de mercado fornecidos por setting.about.feeEstimation.label=Taxa de mineração fornecida por setting.about.versionDetails=Detalhes da versão setting.about.version=Versão do programa setting.about.subsystems.label=Versão de subsistemas setting.about.subsystems.val=Versão da rede: {0}; Versão de mensagem P2P: {1}; Versão da base de dados local: {2}; Versão do protocolo de negócio: {3} setting.about.shortcuts=Atalhos setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' ou 'alt + {0}' ou 'cmd + {0}' setting.about.shortcuts.menuNav=Navigar o menu principal setting.about.shortcuts.menuNav.value=Para navigar o menu principal pressione:: 'Ctrl' ou 'alt' ou 'cmd' juntamente com uma tecla numérica entre '1-9' setting.about.shortcuts.close=Fechar o Haveno setting.about.shortcuts.close.value='Ctrl + {0}' ou 'cmd + {0}' ou 'Ctrl + {1}' ou 'cmd + {1}' setting.about.shortcuts.closePopup=Fechar popup ou janela de diálogo setting.about.shortcuts.closePopup.value=Tecla "ESCAPE" setting.about.shortcuts.chatSendMsg=Enviar uma mensagem ao negociador setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' ou 'alt + ENTER' ou 'cmd + ENTER' setting.about.shortcuts.openDispute=Abrir disputa setting.about.shortcuts.openDispute.value=Selecionar negócio pendente e clicar: {0} setting.about.shortcuts.walletDetails=Abrir janela de detalhes da carteira setting.about.shortcuts.openEmergencyXmrWalletTool=Abrir ferramenta e emergência da carteira para a carteira de XMR setting.about.shortcuts.showTorLogs=Escolher o nível de log para mensagens Tor entre DEBUG e WARN setting.about.shortcuts.manualPayoutTxWindow=Abrir janela para pagamento manual da tx de depósito multi-assinatura 2de2 setting.about.shortcuts.removeStuckTrade=Open popup to move failed trade to open trades tab again setting.about.shortcuts.removeStuckTrade.value=Select failed trade and press: {0} setting.about.shortcuts.registerArbitrator=Registrar árbitro (apenas mediador/árbitro) setting.about.shortcuts.registerArbitrator.value=Navigar para conta e pressionar: {0} setting.about.shortcuts.registerMediator=Registar mediador (apenas medidor/árbitro) setting.about.shortcuts.registerMediator.value=Navigar para conta e pressionar: {0} setting.about.shortcuts.openSignPaymentAccountsWindow=Abrir janela para assinatura de idade da conta (apenas árbitros legacy) setting.about.shortcuts.openSignPaymentAccountsWindow.value=Navigar para o ecrã de árbitro legacy e pressionar: {0} setting.about.shortcuts.sendAlertMsg=Enviar alerta ou mensagem de atualização (atividade privilegiada) setting.about.shortcuts.sendFilter=Definir Filtro (atividade privilegiada) setting.about.shortcuts.sendPrivateNotification=Enviar notificação privada ao par (atividade privilegiada) setting.about.shortcuts.sendPrivateNotification.value=Open peer info at avatar and press: {0} setting.info.headline=New XMR auto-confirm Feature setting.info.msg=When selling XMR for XMR you can use the auto-confirm feature to verify that the correct amount of XMR was sent to your wallet so that Haveno can automatically mark the trade as complete, making trades quicker for everyone.\n\nAuto-confirm checks the XMR transaction on at least 2 XMR explorer nodes using the private transaction key provided by the XMR sender. By default, Haveno uses explorer nodes run by Haveno contributors, but we recommend running your own XMR explorer node for maximum privacy and security.\n\nYou can also set the maximum amount of XMR per trade to auto-confirm as well as the number of required confirmations here in Settings.\n\nSee more details (including how to set up your own explorer node) on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades] #################################################################### # Account #################################################################### account.tab.mediatorRegistration=Registo do Mediador account.tab.refundAgentRegistration=Registro de agente de reembolso account.tab.signing=Signing account.info.headline=Bem vindo à sua conta Haveno account.info.msg=Aqui você pode adicionar contas de negociação para moedas nacionais e cryptos e criar um backup da sua carteira e dos dados da conta.\n\nUma nova carteira de Monero foi criada na primeira vez que você iniciou o Haveno.\n\nÉ altamente recomendável que você anote as sua palavras-semente da carteira do Monero (consulte a guia na parte superior) e considere adicionar uma senha antes do financiamento. Depósitos e retiradas de Monero são gerenciados na secção \"Fundos\".\n\nNota sobre privacidade e segurança: como o Haveno é uma exchange descentralizada, todos os seus dados são mantidos no seu computador. Como não há servidores, não temos acesso às suas informações pessoais, fundos ou mesmo seu endereço IP. Dados como números de contas bancárias, endereços de crypto e Monero etc. são compartilhados apenas com seu par de negociação para realizar negociações iniciadas (no caso de uma disputa, o mediador ou o árbitro verá os mesmos dados que o seu parceiro de negociação). account.menu.paymentAccount=Contas de moedas nacionais account.menu.altCoinsAccountView=Contas de cryptos account.menu.password=Senha da carteira account.menu.seedWords=Semente da carteira account.menu.walletInfo=Wallet info account.menu.backup=Backup account.menu.notifications=Notificações account.menu.walletInfo.balance.headLine=Wallet balances account.menu.walletInfo.balance.info=This shows the internal wallet balance including unconfirmed transactions.\nFor XMR, the internal wallet balance shown below should match the sum of the 'Available' and 'Reserved' balances shown in the top right of this window. account.menu.walletInfo.xpub.headLine=Watch keys (xpub keys) account.menu.walletInfo.walletSelector={0} {1} wallet account.menu.walletInfo.path.headLine=HD keychain paths account.menu.walletInfo.path.info=If you import seed words into another wallet (like Electrum), you'll need to define the path. This should only be done in emergency cases when you lose access to the Haveno wallet and data directory.\nKeep in mind that spending funds from a non-Haveno wallet can bungle the internal Haveno data structures associated with the wallet data, which can lead to failed trades.\n\nNEVER send BSQ from a non-Haveno wallet, as it will probably lead to an invalid BSQ transaction and losing your BSQ. account.menu.walletInfo.openDetails=Show raw wallet details and private keys ## TODO should we rename the following to a gereric name? account.arbitratorRegistration.pubKey=Chave pública account.arbitratorRegistration.register=Registrar account.arbitratorRegistration.registration=Registro de {0} account.arbitratorRegistration.revoke=Revogar account.arbitratorRegistration.info.msg=Saiba que precisa estar disponível até 15 dias depois depois da revogação porque podem existir negócios que o estejam a usar como {0}. O período máx. de negócio permitido é de 8 dias e a disputa pode levar até 7 dias. account.arbitratorRegistration.warn.min1Language=Precisa definir pelo menos 1 idioma.\nAdicionamos o idioma padrão para você. account.arbitratorRegistration.removedSuccess=Você removeu com sucesso seu registro da rede do Haveno. account.arbitratorRegistration.removedFailed=Não foi possível remover o registro.{0} account.arbitratorRegistration.registerSuccess=Você se registrou com sucesso na rede do Haveno. account.arbitratorRegistration.registerFailed=Não foi possível completar o registro.{0} account.crypto.yourCryptoAccounts=As suas contas de cryptos account.crypto.popup.wallet.msg=Certifique-se de seguir os requisitos para o uso das carteiras de {0}, conforme descrito na página da web de {1}.\nO uso de carteiras de exchanges centralizadas nas quais (a) você não controla suas chaves ou (b) que não utiliza software de carteira compatível é arriscado: pode levar à perda dos fundos negociados!\nO mediador ou árbitro não é um especialista em {2} e não pode ajudar nesses casos. account.crypto.popup.wallet.confirm=Eu entendo e confirmo que eu sei qual carteira que preciso usar. # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=Negociar UPX no Haveno exige que você entenda e cumpra os seguintes requerimentos:\n\nPara enviar o UPX, você precisa usar a carteira GUI oficial do uPlexa ou a carteira CLI do uPlexa com o sinalizador store-tx-info ativado (padrão em novas versões). Certifique-se de que você pode acessar a chave da tx, pois isso seria necessário em caso de disputa.\nuplexa-wallet-cli (use o comando get_tx_key)\nuplexa-wallet-gui (vá para a aba do histoórico e clique no botão (P) para prova de pagamento)\n\nEm exploradores de blocos normais, a transferência não é verificável.\n\nVocê precisa fornecer ao árbitro os seguintes dados em caso de disputa:\n- A chave privada da tx\n- O hash da transação\n- Endereço público do destinatário\n\nA falha no fornecimento dos dados acima, ou se você usou uma carteira incompatível, resultará na perda do caso da disputa. O remetente de UPX é responsável por fornecer a verificação da transferência de UPX ao árbitro em caso de disputa.\n\nNão é necessário um ID de pagamento, apenas o endereço público normal.\nSe você não tiver certeza sobre esse processo, visite o canal de discord do uPlexa (https://discord.gg/vhdNSrV) ou o chat do Telegram do uPlexa (https://t.me/uplexaOfficial) para encontrar mais informações. # suppress inspection "UnusedProperty" account.crypto.popup.arq.msg=Negociar o ARQ no Haveno requer que você entenda e atenda aos seguintes requerimentos:\n\nPara enviar o ARQ, você precisa usar a wallet oficial do ArQmA GUI ou a carteira do ArQmA CLI com o marcador store-tx-info ativado (padrão em novas versões). Por favor, certifique-se que você pode acessar a chave da tx porque isso seria necessário em caso de uma disputa.\narqma-wallet-cli (use o comando get_tx_key)\narqma-wallet-gui (vá para a aba do histórico e clique no botão (P) para comprovar o pagamento)\n\nEm exploradores de blocos normais, a transferência não é verificável.\n\nVocê precisa fornecer ao mediador ou árbitro os seguintes dados em caso de disputa:\n- A chave privada da tx\n- O hash da transação\n- o endereço público do destinatário\n\nA falha em fornecer os dados acima, ou se você usou uma carteira incompatível, resultará na perda do caso de disputa. O remetente do ARQ é responsável por fornecer a verificação da transferência do ARQ ao mediador ou árbitro em caso de disputa.\n\nNão é necessário um código de pagamento, apenas o endereço público normal.\nSe você não tiver certeza sobre esse processo, visite o canal de discord do ArQmA (https://discord.gg/s9BQpJT) ou o fórum do ArQmA (https://labs.arqma.com) para obter mais informações. # suppress inspection "UnusedProperty" account.crypto.popup.xmr.msg=Trading XMR on Haveno requires that you understand the following requirement.\n\nIf selling XMR, you must be able to provide the following information to a mediator or arbitrator in case of a dispute:\n- the transaction key (Tx Key, Tx Secret Key or Tx Private Key)\n- the transaction ID (Tx ID or Tx Hash)\n- the destination address (recipient's address)\n\nSee the wiki for details on where to find this information on popular Monero wallets [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Proving_payments].\nFailure to provide the required transaction data will result in losing disputes.\n\nAlso note that Haveno now offers automatic confirming for XMR transactions to make trades quicker, but you need to enable it in Settings.\n\nSee the wiki for more information about the auto-confirm feature: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. # suppress inspection "UnusedProperty" account.crypto.popup.msr.msg=Negociar MSR no Haveno requer que você entenda e cumpra os seguintes requerimentos:\n\nPara enviar MSR, você precisa usar a carteira GUI oficial do Masari ou a carteira CLI do Masari com o marcador store-tx-info ativado (ativado por padrão) ou a carteira web do Masari (https://wallet.getmasari.org). Por favor, certifique-se que você pode acessar a chave da tx porque isso seria necessário em caso de uma disputa.\nmasari-wallet-cli (use o comando get_tx_key)\nmasari-wallet-gui (vá para a aba do histórico e clique no botão (P) para comprovar o pagamento)\n\nMasari Web Wallet (vá para Account -> histórico de transação e veja os detalhes da sua transação enviada)\n\nA verificação pode ser realizada na carteira.\nmasari-wallet-cli: usando o comando (check_tx_key).\nmasari-wallet-gui: na aba Advanced > Prove/Check.\nA verificação pode ser realizada no eplorador de blocos\nExplorador de blocos aberto (https://explorer.getmasari.org), use a barra de procurar para encontrar o hash da transação.\nUma que vez que a transação for encontrada, desça até ao baixo da àrea 'Prove Sending' e preencha os detalhes necessários.\nVocê precisa fornecer ao mediador ou ao árbitro os seguintes dados em caso de disputa:\n- A chave privada da tx\n- O hash da transação\n- o endereço público do destinatário\n\nFalha em fornecer os dados acima, ou se você usou uma carteira incompatível, resultará na perda do caso de disputa. O remetente da XMR é responsável por fornecer a verificação da transferência da MSR para o mediador ou o árbitro no caso de uma disputa.\n\nNão é necessário um código de pagamento, apenas o endereço público normal.\nSe você não tem certeza sobre o processo, peça ajuda no Discord official do Masari (https://discord.gg/sMCwMqs). # suppress inspection "UnusedProperty" account.crypto.popup.blur.msg=Negociar o BLUR no Haveno requer que você entenda e cumpra os seguintes requerimentos:\n\nPara enviar o BLUR você deve usar a carteira CLI da Blur Network ou a carteira GUI.\n\nSe você estiver usando a carteira CLI, um hash da transação (tx ID) será exibido após uma transferência ser enviada. Você deve guardar esta informação. Imediatamente após o envio da transferência, você deve usar o comando 'get_tx_key' para recuperar a chave privada da transação. Se você não conseguir executar essa etapa, talvez não consiga recuperar a chave mais tarde.\n\nSe você estiver usando a carteira GUI do Blur Network, a chave privada da transação e a ID da transação podem ser encontradas convenientemente na aba "Histórico". Imediatamente após o envio, localize a transação de interesse. Clique no símbolo "?" no canto inferior direito da caixa que contém a transação. Você deve guardar esta informação.\n\nCaso a arbitragem seja necessária, você deve apresentar o seguinte à um mediador ou árbitro: 1.) a ID da transação, 2.) a chave privada da transação e 3.) o endereço do destinatário. O mediador ou árbitro verificará a transferência do BLUR usando o Blur Transaction Viewer (https://blur.cash/#tx-viewer).\n\nO não fornecimento das informações necessárias ao mediador ou árbitro resultará na perda da disputa. Em todos os casos de disputa, o remetente de BLUR tem 100% de responsabilidade na verificação de transações para um mediador ou árbitro.\n\nSe você não entender esses requerimentos não negocie no Haveno. Primeiro, procure ajuda no Discord da Rede de Blur (https://discord.gg/dMWaqVW). # suppress inspection "UnusedProperty" account.crypto.popup.solo.msg=Negociar Solo no Haveno requer que você entenda e cumpra os seguintes requerimentos:\n\nPara enviar o Solo, você deve usar a carteira CLI do Solo.\n\nSe você está a usar a carteira CLI, um hash da transação (tx ID) aparecerá depois de a transação ser feita. Você deve guardar esta informação. Imediatamente após o envio da transação, você deve usar o comando 'get_tx_key' para recuperar a chave secreta da transação. Se você não conseguir executar este passo, talvez não seja possível recuperar a chave mais tarde.\n\nCaso a arbitragem seja necessária, você deve apresentar o seguinte à um mediador ou árbitro: 1.) o ID da transação, 2.) a chave privada da transação e 3.) o endereço do recipiente. O mediador ou árbitro então verificará a transferência do Solo usando o Explorador de Blocos Solo (https://explorer.minesolo.com/).\n\nO não fornecimento das informações necessárias ao mediador ou árbitro resultará na perda da disputa. Em todos os casos de disputa, o remetente de Solo tem 100% da responsabilidade na verificação de transações para um mediador ou árbitro.\n\nSe você não entender esses requerimentos, não negocie no Haveno. Primeiro, procure ajuda no Discord da Rede do Solo (https://discord.minesolo.com/). # suppress inspection "UnusedProperty" account.crypto.popup.cash2.msg=Negociar o CASH2 no Haveno requer que você entenda e cumpra os seguintes requerimentos:\n\nPara enviar CASH2, você deve usar a versão Cash2 Wallet versão 3 ou superior.\n\nDepois que uma transação é enviada, a ID da transação será exibida. Você deve guardar esta informação. Imediatamente após o envio da transação, você deve usar o comando 'getTxKey' no simplewallet para recuperar a chave secreta da transação.\n\nCaso a arbitragem seja necessária, você deve apresentar o seguinte à um mediador ou árbitro: 1) a ID da transação, 2) a chave secreta da transação e 3) o endereço Cash2 do destinatário. O mediador ou árbitro irá então verificar a transferência do CASH2 usando o Explorador de Blocos do Cash2 (https://blocks.cash2.org).\n\nO não fornecimento das informações necessárias ao mediador ou árbitro resultará na perda da disputa. Em todos os casos de disputa, o remetente do CASH2 tem 100% de responsabilidade na verificação de transações para um mediador ou árbitro.\n\nSe você não entender esses requerimentos, não negocie no Haveno. Primeiro procure ajuda no Discord do Cash2 (https://discord.gg/FGfXAYN). # suppress inspection "UnusedProperty" account.crypto.popup.qwertycoin.msg=Negociar Qwertycoin no Haveno requer que você entenda e cumpra os seguintes requerimentos:\n\nPara enviar o QWC, você deve usar a versão oficial do QWC Wallet 5.1.3 ou superior.\n\nDepois que uma transação é enviada, o ID da transação será exibida. Você deve guardar esta informação. Imediatamente após o envio da transação, você deve usar o comando 'get_Tx_Key' na simplewallet para recuperar a chave secreta da transação.\n\nCaso a arbitragem seja necessária, você deve apresentar o seguinte à um mediador ou árbitro: 1) o ID da transação, 2) a chave secreta da transação e 3) o endereço QWC do destinatário. O mediador ou árbitro então verificará a transferência do QWC usando o Explorador de Blocos QWC (https://explorer.qwertycoin.org).\n\nO não fornecimento das informações necessárias ao mediador ou árbitro resultará na perda da disputa. Em todos os casos de disputa, o remetente QWC tem 100% da responsabilidade na verificação de transações para um mediador ou árbitro.\n\nSe você não entender esses requerimentos, não negocie no Haveno. Primeiro, procure ajuda no QWC Discord (https://discord.gg/rUkfnpC). # suppress inspection "UnusedProperty" account.crypto.popup.drgl.msg=Negociar Dragonglass no Haveno requer que você entenda e cumpra os seguintes requerimentos:\n\nPor causa da privacidade que a Dragonglass fornece, uma transação não é verificável na blockchain pública. Se necessário, você pode comprovar seu pagamento através do uso de sua chave privada TXN.\nA chave privade TXN é uma chave única gerada automaticamente para cada transação que só pode ser acessada dentro da sua carteira DRGL.\nTanto pela GUI do DRGL-wallet (dentro da janela de detalhes da transação) ou pelo simplewallet da CLI do Dragonglass (usando o comando "get_tx_key").\n\nA versão do DRGL 'Oathkeeper' e superior são NECESSÁRIAS para ambos.\n\nEm caso de disputa, você deve fornecer ao mediador ou árbitro os seguintes dados:\n- A chave privada TXN\n- O hash da transação\n- o endereço público do destinatário\n\nA verificação do pagamento pode ser feita usando os dados acima como inputs em (http://drgl.info/#check_txn).\n\nA falha em fornecer os dados acima, ou se você usou uma carteira incompatível, resultará na perda disputa. O remetente da Dragonglass é responsável por fornecer a verificação da transferência do DRGL para o mediador ou árbitro em caso de disputa. O uso de PaymentID não é obrigatório.\n\nSe você não tiver certeza sobre qualquer parte deste processo, visite Dragonglass on Discord (http://discord.drgl.info) para obter ajuda. # suppress inspection "UnusedProperty" account.crypto.popup.ZEC.msg=Ao usar o Zcash você só pode usar os endereços transparentes (começando com t), e não os endereços z (privados), porque o mediador ou árbitro não seria capaz de verificar a transação com endereços z. # suppress inspection "UnusedProperty" account.crypto.popup.XZC.msg=Ao usar a Zcoin, você só pode usar os endereços transparentes (rastreáveis) e não os endereços não rastreáveis, porque o mediador ou árbitro não seria capaz de verificar a transação com endereços não rastreáveis num explorador de blocos. # suppress inspection "UnusedProperty" account.crypto.popup.grin.msg=GRIN requer um processo interativo entre o remetente e o recipiente para criar a transação. Certifique-se de seguir as instruções da página web do projeto GRIN para enviar e receber de forma confiável o GRIN (o recipiente precisa estar online ou pelo menos estar online durante um determinado período de tempo).\n\nO Haveno suporta apenas o formato de URL da carteira Grinbox (Wallet713).\n\nO remetente GRIN é obrigado a fornecer prova de que eles enviaram GRIN com sucesso. Se a carteira não puder fornecer essa prova, uma disputa potencial será resolvida em favor do recipiente de GRIN. Por favor, certifique-se de usar o software Grinbox mais recente que suporta a prova da transação e que você entende o processo de transferência e receção do GRIN, bem como criar a prova.\n\nConsulte https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only para obter mais informações sobre a ferramenta de prova Grinbox. # suppress inspection "UnusedProperty" account.crypto.popup.beam.msg=O BEAM requer um processo interativo entre o remetente e o recipiente para criar a transação.\n\nCertifique-se de seguir as instruções da página Web do projeto BEAM para enviar e receber BEAM de forma confiável (o recipiente precisa estar online ou pelo menos estar online durante um determinado período de tempo).\n\nO remetente BEAM é obrigado a fornecer prova de que eles enviaram o BEAM com sucesso. Certifique-se de usar software de carteira que pode produzir tal prova. Se a carteira não puder fornecer a prova, uma disputa potencial será resolvida em favor do recipiente de BEAM. # suppress inspection "UnusedProperty" account.crypto.popup.pars.msg=A negociação de ParsiCoin no Haveno exige que você entenda e cumpra os seguintes requerimentos:\n\nPara enviar PARS você deve usar a versão oficial da Carteira ParsiCoin 3.0.0 ou superior.\n\nVocê pode verificar o Hash da Transação e a Chave da Transação na secção das Transações na sua carteira GUI (ParsiPay) Você precisa clicar com o lado direito na transação e, em seguida, clicar em mostrar detalhes.\n\nEm caso de arbitragem, você deve apresentar o seguinte para um mediador ou árbitro: 1) o Hash da Transação, 2) a Chave da Transação, e 3) endereço PARS do recipiente. O mediador ou árbitro irá então verificar a transferência PARS usando o Explorador de Blocos da ParsiCoin (http://explorer.parsicoin.net/#check_payment).\n\nFalha em fornecer as informações necessárias ao mediador ou árbitro resultará na perda do caso de disputa. Em todos os casos de disputa, o remetente da ParsiCoin carrega 100% da carga de responsabilidade em verificar as transações à um mediador ou árbitro.\n\nSe você não entender esses requerimentos, não negocie no Haveno. Primeiro procure ajuda no Discord da ParsiCoin (https://discord.gg/c7qmFNh). # suppress inspection "UnusedProperty" account.crypto.popup.blk-burnt.msg=Para negociar blackcoins queimados, você precisa saber o seguinte:\n\nBlackcoins queimados não podem ser gastos. Para os negociar no Haveno, os output scripts precisam estar na forma: OP_RETURN OP_PUSHDATA, seguido pelos data bytes que, após serem codificados em hex, constituem endereços. Por exemplo, blackcoins queimados com um endereço 666f6f (“foo” em UTF-8) terá o seguinte script:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\nPara criar blackcoins queimados, deve-se usar o comando RPC “burn” disponível em algumas carteiras.\n\nPara casos possíveis, confira https://ibo.laboratorium.ee .\n\nComo os blackcoins queimados não podem ser gastos, eles não podem voltar a ser vendidos. “Vender” blackcoins queimados significa queimar blackcoins comuns (com os dados associados iguais ao endereço de destino).\n\nEm caso de disputa, o vendedor de BLK precisa providenciar o hash da transação. # suppress inspection "UnusedProperty" account.crypto.popup.liquidmonero.msg=A negociação de L-XMR no Haveno exige que você entenda o seguinte:\n\nAo receber L-XMR para um negócio no Haveno, você não pode usar a aplicação móvel Blockstream Green Wallet ou uma carteira de custódia / exchange. Você só deve receber o L-XMR na carteira Liquid Elements Core ou em outra carteira L-XMR que permita obter a chave ofuscante para o seu endereço L-XMR cego.\n\nNo caso de ser necessária mediação, ou se surgir uma disputa de negócio, você deve divulgar a chave ofuscante do seu endereço L-XMR de recebimento ao mediador ou agente de reembolso Haveno, para que eles possam verificar os detalhes da sua Transação Confidencial no seu próprio Elements Core full node.\n\nO não fornecimento das informações necessárias ao mediador ou ao agente de reembolso resultará na perda do caso de disputa. Em todos os casos de disputa, o recipiente de L-XMR suporta 100% da responsabilidade ao fornecer prova criptográfica ao mediador ou ao agente de reembolso.\n\nSe você não entender esses requerimentos, não negocie o L-XMR no Haveno. account.traditional.yourTraditionalAccounts=A sua conta de moeda nacional account.backup.title=Carteira de backup account.backup.location=Localizacao do backup account.backup.selectLocation=Selecione localização para backup account.backup.backupNow=Fazer backup agora (o backup não é criptografado) account.backup.appDir=Diretório de dados do programa account.backup.openDirectory=Abrir diretório account.backup.openLogFile=Abrir ficheiro de log account.backup.success=Backup guardado com sucesso em:\n{0} account.backup.directoryNotAccessible=O diretório escolhido não é acessível. {0} account.password.removePw.button=Remover senha account.password.removePw.headline=Remover proteção com senha da carteira account.password.setPw.button=Definir senha account.password.setPw.headline=Definir proteção de senha da carteira account.password.info=Com a proteção por senha habilitada, você precisará inserir sua senha ao iniciar o aplicativo, ao retirar monero de sua carteira e ao exibir suas palavras-semente. account.seed.backup.title=Faça backup das palavras-chave da sua carteira account.seed.info=Por favor, anote tanto as palavras-chave da sua carteira quanto a data. Você pode recuperar sua carteira a qualquer momento com as palavras-chave e a data.\n\nVocê deve anotar as palavras-chave em uma folha de papel. Não as salve no computador.\n\nPor favor, observe que as palavras-chave não substituem uma cópia de segurança.\nVocê precisa criar uma cópia de segurança do diretório completo do aplicativo na tela "Conta/Backup" para recuperar o estado e os dados do aplicativo. account.seed.backup.warning=Por favor, observe que as palavras-chave não substituem uma cópia de segurança.\nVocê precisa criar uma cópia de segurança do diretório completo do aplicativo na tela "Conta/Backup" para recuperar o estado e os dados do aplicativo. account.seed.warn.noPw.msg=Você não definiu uma senha da carteira que protegeria a exibição das palavras-semente.\n\nVocê quer exibir as palavras-semente? account.seed.warn.noPw.yes=Sim, e não me pergunte novamente account.seed.enterPw=Digite a senha para ver palavras-semente account.seed.restore.info=Por favor, faça um backup antes de aplicar a restauração a partir de palavras-semente. Esteja ciente de que a restauração da carteira é apenas para casos de emergência e pode causar problemas com a base de dados interna da carteira.\nNão é uma maneira de aplicar um backup! Por favor, use um backup do diretório de dados do programa para restaurar um estado anterior do programa.\n\nDepois de restaurar o programa será desligado automaticamente. Depois de ter reiniciado o programa, ele será ressincronizado com a rede Monero. Isso pode demorar um pouco e consumir muito do CPU, especialmente se a carteira for mais antiga e tiver muitas transações. Por favor, evite interromper esse processo, caso contrário, você pode precisar excluir o ficheiro da corrente do SPV novamente ou repetir o processo de restauração. account.seed.restore.ok=Ok, restaurar e desligar Haveno #################################################################### # Mobile notifications #################################################################### account.notifications.setup.title=Configuração account.notifications.download.label=Baixar a aplicação móvel account.notifications.waitingForWebCam=Esperando pela webcam... account.notifications.webCamWindow.headline=Scannear código QR à partir do telemóvel account.notifications.webcam.label=Usar webcam account.notifications.webcam.button=Scannear código QR account.notifications.noWebcam.button=Eu não tenho webcam account.notifications.erase.label=Limpar notificações no telemóvel account.notifications.erase.title=Limpar notificações account.notifications.email.label=Token de emparelhamento account.notifications.email.prompt=Inserir o token de emparelhamento recebido por email account.notifications.settings.title=Definições account.notifications.useSound.label=Reproduzir som de notificação no telemóvel account.notifications.trade.label=Receber mensagens de negócio account.notifications.market.label=Receber alertas de oferta account.notifications.price.label=Receber alertas de preço account.notifications.priceAlert.title=Alertas de preço account.notifications.priceAlert.high.label=Notificar se o preço de XMR está acima de account.notifications.priceAlert.low.label=Notificar se o preço de XMR está abaixo de account.notifications.priceAlert.setButton=Definir alerta de preço account.notifications.priceAlert.removeButton=Remover alerta de preço account.notifications.trade.message.title=Estado do negócio mudou account.notifications.trade.message.msg.conf=A transação do depósito para o negócio com o ID {0} está confirmada. Por favor, abra seu programa Haveno e inicie o pagamento. account.notifications.trade.message.msg.started=O comprador do XMR iniciou o pagamento para o negócio com o ID {0}. account.notifications.trade.message.msg.completed=O negócio com o ID {0} está completo. account.notifications.offer.message.title=A sua oferta foi aceite account.notifications.offer.message.msg=A sua oferta com o ID {0} foi aceite account.notifications.dispute.message.title=Nova mensagem de disputa account.notifications.dispute.message.msg=Recebeu uma menagem de disputa para o negócio com o ID {0} account.notifications.marketAlert.title=Alertas de ofertas account.notifications.marketAlert.selectPaymentAccount=Ofertas compatíveis com a conta de pagamento account.notifications.marketAlert.offerType.label=Tipo de oferta que me interessa account.notifications.marketAlert.offerType.buy=Ofertas de compra (Eu quero vender XMR) account.notifications.marketAlert.offerType.sell=Ofertas de venda (eu quero comprar XMR) account.notifications.marketAlert.trigger=Distância do preço da oferta (%) account.notifications.marketAlert.trigger.info=Com uma distância de preço definida, você só receberá um alerta quando uma oferta que atenda (ou exceda) os seus requerimentos for publicada. Exemplo: você quer vender XMR, mas você só venderá à um ganho de 2% sobre o atual preço de mercado. Definir esse campo como 2% garantirá que você receba apenas alertas para ofertas com preços que estão 2% (ou mais) acima do atual preço de mercado. account.notifications.marketAlert.trigger.prompt=Distância da percentagem do preço de mercado (ex: 2.50%, -0.50%, etc) account.notifications.marketAlert.addButton=Adicionar alerta de oferta account.notifications.marketAlert.manageAlertsButton=Gerir alertas de oferta account.notifications.marketAlert.manageAlerts.title=Gerir alertas de oferta account.notifications.marketAlert.manageAlerts.header.paymentAccount=Conta de pagamento account.notifications.marketAlert.manageAlerts.header.trigger=Preço de desencadeamento account.notifications.marketAlert.manageAlerts.header.offerType=Tipo de oferta account.notifications.marketAlert.message.title=Alerta de oferta account.notifications.marketAlert.message.msg.below=abaixo de account.notifications.marketAlert.message.msg.above=acima de account.notifications.marketAlert.message.msg=Uma nova '{0} {1}' com o preço de {2} ({3} {4} preço de mercado) e método de pagamento '{5}' foi publicada no livro de ofertas do Haveno.\nID da oferta: {6}. account.notifications.priceAlert.message.title=Alerta de preço para {0} account.notifications.priceAlert.message.msg=O teu alerta de preço foi desencadeado. O preço atual de {0} é de {1} {2} account.notifications.noWebCamFound.warning=Nenhuma webcam foi encontrada.\n\nPor favor use a opção email para enviar o token e a chave de criptografia do seu telemóvel para o programa da Haveno. account.notifications.priceAlert.warning.highPriceTooLow=O preço mais alto deve ser maior que o preço mais baixo. account.notifications.priceAlert.warning.lowerPriceTooHigh=O preço mais baixo deve ser menor que o preço mais alto. #################################################################### # Windows #################################################################### inputControlWindow.headline=Select inputs for transaction inputControlWindow.balanceLabel=Saldo disponível contractWindow.title=Detalhes da disputa contractWindow.dates=Data de oferta / Data de negócio contractWindow.xmrAddresses=Endereço monero comprador XMR / vendendor XMR contractWindow.onions=Endereço de rede comprador de XMR / vendendor de XMR contractWindow.accountAge=Idade da conta do comprador de XMR / vendedor de XMR contractWindow.numDisputes=Nº de disputas comprador de XMR / vendedor de XMR: contractWindow.contractHash=Hash do contrato displayAlertMessageWindow.headline=Informação importante! displayAlertMessageWindow.update.headline=Informação importante de atualização! displayAlertMessageWindow.update.download=Download: displayUpdateDownloadWindow.downloadedFiles=Ficheiros: displayUpdateDownloadWindow.downloadingFile=Descarregando: {0} displayUpdateDownloadWindow.verifiedSigs=Assinaturas verificadas com chaves: displayUpdateDownloadWindow.status.downloading=Descarregando ficheiros... displayUpdateDownloadWindow.status.verifying=Verificando assinatura... displayUpdateDownloadWindow.button.label=Descarregar o instalador e verificar a assinatura displayUpdateDownloadWindow.button.downloadLater=Descarregar depois displayUpdateDownloadWindow.button.ignoreDownload=Ignorar esta versão displayUpdateDownloadWindow.headline=Uma nova atualização do Haveno está disponível! displayUpdateDownloadWindow.download.failed.headline=Download falhou displayUpdateDownloadWindow.download.failed=Download failed.\nPlease download and verify manually at [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.installer.failed=Unable to determine the correct installer. Please download and verify manually at [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.verify.failed=Verification failed.\nPlease download and verify manually at [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.success=A nova versão foi descarregada com sucesso e a assinatura foi verificada.\n\nPor favor, abra o diretório de download, desligue o programa e instale a nova versão. displayUpdateDownloadWindow.download.openDir=Abrir diretório de download disputeSummaryWindow.title=Resumo disputeSummaryWindow.openDate=Data de abertura do bilhete disputeSummaryWindow.role=Função do negociador disputeSummaryWindow.payout=Pagamento da quantia de negócio disputeSummaryWindow.payout.getsTradeAmount={0} de XMR fica com o pagamento da quantia de negócio disputeSummaryWindow.payout.getsAll=Max. payout to XMR {0} disputeSummaryWindow.payout.custom=Pagamento personalizado disputeSummaryWindow.payoutAmount.buyer=Quantia de pagamento do comprador disputeSummaryWindow.payoutAmount.seller=Quantia de pagamento do vendedor disputeSummaryWindow.payoutAmount.invert=Usar perdedor como publicador disputeSummaryWindow.reason=Razão da disputa disputeSummaryWindow.tradePeriodEnd=Trade period end disputeSummaryWindow.extraInfo=Extra information disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BUG=Erro # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.USABILITY=Usabilidade # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Violação de protocolo # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.NO_REPLY=Sem resposta # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SCAM=Golpe # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OTHER=Outro # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BANK_PROBLEMS=Banco # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OPTION_TRADE=Option trade # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Trader not responding # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled disputeSummaryWindow.summaryNotes=Notas de resumo disputeSummaryWindow.addSummaryNotes=Adicionar notas de resumo disputeSummaryWindow.close.button=Fechar bilhete # Do no change any line break or order of tokens as the structure is used for signature verification # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.msg=Ticket closed on {0}\n{1} node address: {2}\n\nSummary:\nTrade ID: {3}\nCurrency: {4}\nTrade amount: {5}\nPayout amount for XMR buyer: {6}\nPayout amount for XMR seller: {7}\n\nReason for dispute: {8}\n\nSummary notes:\n{9}\n # Do no change any line break or order of tokens as the structure is used for signature verification disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3} disputeSummaryWindow.close.nextStepsForMediation=\nNext steps:\nOpen trade and accept or reject suggestion from mediator disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\nNo further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions disputeSummaryWindow.close.closePeer=Você também precisa fechar o bilhete dos pares de negociação! disputeSummaryWindow.close.txDetails.headline=Publicar transação de reembolso # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.buyer=O comprador recebe {0} no endereço: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=O vendedor recebe {0} no endereço: {1}\n disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3}\n\nAre you sure you want to publish this transaction? disputeSummaryWindow.close.noPayout.headline=Close without any payout disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? emptyWalletWindow.headline={0} ferramenta de emergência da carteira emptyWalletWindow.info=Por favor, use isso apenas em caso de emergência, se você não puder aceder o seu fundo a partir da interface do utilizador.\n\nPor favor, note que todas as ofertas abertas serão fechadas automaticamente ao usar esta ferramenta.\n\nAntes de usar essa ferramenta, faça backup do seu diretório de dados. Você pode fazer isso em \"Conta/Backup\".\n\nPor favor comunique-nos o seu problema e envie um relatório de erros no Github ou no fórum Haveno para que possamos investigar o que causou o problema. emptyWalletWindow.balance=O saldo disponível da sua carteira: emptyWalletWindow.address=Seu endereço de destino emptyWalletWindow.button=Enviar todos os fundos emptyWalletWindow.openOffers.warn=Você tem ofertas abertas que serão removidas se você esvaziar a carteira.\nTem certeza de que deseja esvaziar a sua carteira? emptyWalletWindow.openOffers.yes=Sim, tenho certeza emptyWalletWindow.sent.success=O saldo da sua carteira foi transferido com sucesso. enterPrivKeyWindow.headline=Inserir chave privada para registro filterWindow.headline=Editar lista de filtragem filterWindow.offers=Ofertas filtradas (sep. por vírgula): filterWindow.onions=Banned from trading addresses (comma sep.) filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.) filterWindow.accounts=Dados da conta de negociação filtrados:\nFormato: lista de [id de método de pagamento | campo de dados | valor] sep. por vírgula filterWindow.bannedCurrencies=Códigos de moedas filtrados (sep. por vírgula) filterWindow.bannedPaymentMethods=IDs de método de pagamento filtrados (sep. por vírgula) filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) filterWindow.arbitrators=Árbitros filtrados (endereços onion sep. por vírgula) filterWindow.mediators=Mediadores filtrados (endereços onion separados por vírgula) filterWindow.refundAgents=Agentes de reembolso filtrados (endereços onion sep. por virgula) filterWindow.seedNode=Nós de semente filtrados (endereços onion sep. por vírgula) filterWindow.priceRelayNode=Nós de transmissão de preço filtrados (endereços onion sep. por vírgula) filterWindow.xmrNode=Nós de Monero filtrados (endereços + portas sep. por vírgula) filterWindow.preventPublicXmrNetwork=Prevenir uso da rede de Monero pública filterWindow.disableAutoConf=Disable auto-confirm filterWindow.autoConfExplorers=Filtered auto-confirm explorers (comma sep. addresses) filterWindow.disableTradeBelowVersion=Mín. versão necessária para negociação filterWindow.add=Adicionar filtro filterWindow.remove=Remover filtro filterWindow.xmrFeeReceiverAddresses=XMR fee receiver addresses filterWindow.disableApi=Disable API filterWindow.disableMempoolValidation=Disable Mempool Validation offerDetailsWindow.minXmrAmount=Quantia mín. de XMR offerDetailsWindow.min=(mín. {0}) offerDetailsWindow.distance=(distância do preço de mercado: {0}) offerDetailsWindow.myTradingAccount=Minha conta de negociação offerDetailsWindow.bankId=ID do banco (ex. BIC ou SWIFT) offerDetailsWindow.countryBank=País do banco do ofertante offerDetailsWindow.commitment=Compromisso offerDetailsWindow.agree=Eu concordo offerDetailsWindow.tac=Termos e condições offerDetailsWindow.confirm.maker.buy=Confirmar: Criar oferta para comprar XMR com {0} offerDetailsWindow.confirm.maker.sell=Confirmar: Criar oferta para vender XMR por {0} offerDetailsWindow.confirm.taker.buy=Confirmar: Aceitar oferta para comprar XMR com {0} offerDetailsWindow.confirm.taker.sell=Confirmar: Aceitar oferta para vender XMR por {0} offerDetailsWindow.creationDate=Data de criação offerDetailsWindow.makersOnion=Endereço onion do ofertante offerDetailsWindow.challenge=Passphrase da oferta offerDetailsWindow.challenge.copy=Copiar frase secreta para compartilhar com seu parceiro qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. qRCodeWindow.request=Pedido de pagamento:\n{0} selectDepositTxWindow.headline=Selecionar transação de depósito para disputa selectDepositTxWindow.msg=A transação de depósito não foi armazenada no negócio.\nPor favor, selecione uma das transações multi-assinatura existentes na sua carteira, que foi a transação de depósito usada no negócio falhado.\n\nVocê pode encontrar a transação correta abrindo a janela de detalhes de negócio (clique na ID do negócio na lista) e seguindo o output da transação de pagamento da taxa de negociação para a próxima transação onde você ver a transação de depósito multi-assinatura (o endereço começa com 3). Esse ID de transação deve estar visível na lista apresentada aqui. Depois de encontrar a transação correta, selecione a transação aqui e continue.\n\nDesculpe pela inconveniência, mas esse caso de erro deve acontecer muito raramente e, no futuro, tentaremos encontrar maneiras melhores de resolvê-lo. selectDepositTxWindow.select=Selecionar transação de depósito sendAlertMessageWindow.headline=Enviar notificação global sendAlertMessageWindow.alertMsg=Mensagem de alerta sendAlertMessageWindow.enterMsg=Digitar mensagem sendAlertMessageWindow.isSoftwareUpdate=Software download notification sendAlertMessageWindow.isUpdate=Is full release sendAlertMessageWindow.isPreRelease=Is pre-release sendAlertMessageWindow.version=Nº da nova versão sendAlertMessageWindow.send=Enviar notificação sendAlertMessageWindow.remove=Remover notificação sendPrivateNotificationWindow.headline=Enviar mensagem privada sendPrivateNotificationWindow.privateNotification=Notificação privada sendPrivateNotificationWindow.enterNotification=Digite notificação sendPrivateNotificationWindow.send=Enviar notificação privada showWalletDataWindow.walletData=Dados da carteira showWalletDataWindow.includePrivKeys=Incluir chaves privadas setXMRTxKeyWindow.headline=Prove sending of XMR setXMRTxKeyWindow.note=Adding tx info below enables auto-confirm for quicker trades. See more: https://haveno.exchange/wiki/Trading_Monero setXMRTxKeyWindow.txHash=Transaction ID (optional) setXMRTxKeyWindow.txKey=Transaction key (optional) # We do not translate the tac because of the legal nature. We would need translations checked by lawyers # in each language which is too expensive atm. tacWindow.headline=Acordo de utilizador tacWindow.agree=Eu concordo tacWindow.disagree=Eu não concordo e desisto tacWindow.arbitrationSystem=Resolução da disputa tradeDetailsWindow.headline=Negócio tradeDetailsWindow.disputedPayoutTxId=ID de transação do pagamento disputado tradeDetailsWindow.tradeDate=Data de negócio tradeDetailsWindow.txFee=Taxa de mineração tradeDetailsWindow.tradePeersOnion=Endereço onion dos parceiros de negociação tradeDetailsWindow.tradePeersPubKeyHash=Trading peers pubkey hash tradeDetailsWindow.tradeState=Estado de negócio tradeDetailsWindow.agentAddresses=Árbitro/Mediador tradeDetailsWindow.detailData=Detail data txDetailsWindow.headline=Transaction Details txDetailsWindow.xmr.noteSent=Você enviou XMR. txDetailsWindow.xmr.noteReceived=Você recebeu XMR. txDetailsWindow.sentTo=Enviado para txDetailsWindow.receivedWith=Recebido com txDetailsWindow.txId=TxId closedTradesSummaryWindow.headline=Trade history summary closedTradesSummaryWindow.totalAmount.title=Total trade amount closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) closedTradesSummaryWindow.totalTradeFeeInXmr.title=Sum of all trade fees paid in XMR closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} of total trade amount) walletPasswordWindow.headline=Digite senha para abrir: xmrConnectionError.headline=Erro de conexão com Monero xmrConnectionError.providedNodes=Erro ao conectar aos nós Monero fornecidos.\n\nDeseja usar o próximo melhor nó Monero disponível? xmrConnectionError.customNodes=Erro ao conectar aos seus nós Monero personalizados.\n\nDeseja usar o próximo melhor nó Monero disponível? xmrConnectionError.localNode=O Haveno estava anteriormente conectado a um nó Monero local, mas ele não está mais acessível.\n\nCertifique-se de que o seu nó local esteja em execução e totalmente sincronizado, ou escolha outra opção para continuar. xmrConnectionError.localNode.start=Iniciar nó local xmrConnectionError.localNode.start.error=Erro ao iniciar o nó local xmrConnectionError.localNode.fallback=Conectar ao próximo melhor nó torNetworkSettingWindow.header=Definições de redes Tor torNetworkSettingWindow.noBridges=Não usar pontes torNetworkSettingWindow.providedBridges=Conectar com as pontes providenciadas torNetworkSettingWindow.customBridges=Inserir pontes personalizadas torNetworkSettingWindow.transportType=Tipo de transporte torNetworkSettingWindow.obfs3=obfs3 torNetworkSettingWindow.obfs4=obfs4 (recomendado) torNetworkSettingWindow.meekAmazon=meek-amazon torNetworkSettingWindow.meekAzure=meek-azure torNetworkSettingWindow.enterBridge=Insira uma ou mais pontes repetidoras (uma por linha) torNetworkSettingWindow.enterBridgePrompt=digite endereço:porta torNetworkSettingWindow.restartInfo=Precisa de reiniciar para aplicar as mudanças torNetworkSettingWindow.openTorWebPage=Abrir à página web do projeto Tor torNetworkSettingWindow.deleteFiles.header=Problemas de conexão? torNetworkSettingWindow.deleteFiles.info=Se tem repetidamente problemas de conexão no início, apagar os ficheiros de Tor desatualizados pode ajudar. Para fazê-lo clique no botão em baixo e reinicia de seguida. torNetworkSettingWindow.deleteFiles.button=Apagar ficheiros de Tor desatualizados e desligar torNetworkSettingWindow.deleteFiles.progress=Desligar Tor em progresso torNetworkSettingWindow.deleteFiles.success=Ficheiros de Tor desatualizados apagados com sucesso. Por favor reinicie. torNetworkSettingWindow.bridges.header=O Tor está bloqueado? torNetworkSettingWindow.bridges.info=Se o Tor estiver bloqueado pelo seu fornecedor de internet ou pelo seu país, você pode tentar usar pontes Tor.\nVisite a página web do Tor em: https://bridges.torproject.org para saber mais sobre pontes e transportes conectáveis. feeOptionWindow.headline=Escolha a moeda para o pagamento da taxa de negócio feeOptionWindow.info=Pode escolher pagar a taxa de negócio em BSQ ou em XMR. Se escolher BSQ tira proveito da taxa de negócio descontada. feeOptionWindow.optionsLabel=Escolha a moeda para o pagamento da taxa de negócio feeOptionWindow.useXMR=Usar XMR feeOptionWindow.fee={0} (≈ {1}) feeOptionWindow.xmrFeeWithFiatAndPercentage={0} (≈ {1} / {2}) feeOptionWindow.xmrFeeWithPercentage={0} ({1}) #################################################################### # Popups #################################################################### popup.headline.notification=Notificação popup.headline.instruction=Favor observar: popup.headline.attention=Atenção popup.headline.backgroundInfo=Informação preliminar popup.headline.feedback=Concluído popup.headline.confirmation=Comfirmação popup.headline.information=Informação popup.headline.warning=Aviso popup.headline.error=Erro popup.doNotShowAgain=Não mostrar novamente popup.reportError.log=Abrir ficheiro de log popup.reportError.gitHub=Relatar ao GitHub issue tracker popup.reportError={0}\n\nPara nos ajudar a melhorar o software, por favor reporte este erro abrindo um novo issue em https://github.com/haveno-dex/haveno/issues.\nA mensagem de erro acima será copiada para a área de transferência quando você clicar num dos botões abaixo.\nSerá mais fácil fazer a depuração se você incluir o ficheiro haveno.log clicando "Abrir arquivo de log", salvando uma cópia e anexando-a ao seu relatório de erros. popup.error.tryRestart=Por favor tente reiniciar o programa e verifique a sua conexão de Internet para ver se pode resolver o problema. popup.error.takeOfferRequestFailed=Ocorreu um erro quando alguém tentou aceitar uma das suas ofertas:\n{0} error.spvFileCorrupted=Ocorreu um erro ao ler o ficheiro da corrente SPV .\nPode ser que o ficheiro da corrente SPV esteja corrompido.\n\nMensagem de erro: {0}\n\nVocê deseja apagá-lo e iniciar uma resincronização? error.deleteAddressEntryListFailed=Não foi possível apagar o ficheiro AddressEntryList.\nErro: {0} error.closedTradeWithUnconfirmedDepositTx=A transação de depósito do negócio fechado com o ID de negócio {0} ainda não foi confirmada.\n\nPor favor re-sinronize o ficheiro SPV em \"Definições/Informação da Rede\" para ver se a transação é inválida. error.closedTradeWithNoDepositTx=A transação de depósito do negócio fechado com o ID de negócio {0} é null.\n\nPor favor reinicie o programa para limpar a lista de negócios fechados. popup.warning.walletNotInitialized=A carteira ainda não foi inicializada popup.warning.osxKeyLoggerWarning=Due to stricter security measures in macOS 10.14 and above, launching a Java application (Haveno uses Java) causes a popup warning in macOS ('Haveno would like to receive keystrokes from any application').\n\nTo avoid that issue please open your 'macOS Settings' and go to 'Security & Privacy' -> 'Privacy' -> 'Input Monitoring' and Remove 'Haveno' from the list on the right side.\n\nHaveno will upgrade to a newer Java version to avoid that issue as soon the technical limitations (Java packager for the required Java version is not shipped yet) are resolved. popup.warning.wrongVersion=Você provavelmente tem a versão errada do Haveno para este computador.\nA arquitetura do seu computador é: {0}.\nO binário Haveno que você instalou é: {1}.\nPor favor, desligue e reinstale a versão correta ({2}). popup.warning.incompatibleDB=We detected incompatible data base files!\n\nThose database file(s) are not compatible with our current code base:\n{0}\n\nWe made a backup of the corrupted file(s) and applied the default values to a new database version.\n\nThe backup is located at:\n{1}/db/backup_of_corrupted_data.\n\nPlease check if you have the latest version of Haveno installed.\nYou can download it at: [HYPERLINK:https://haveno.exchange/downloads].\n\nPlease restart the application. popup.warning.startupFailed.twoInstances=Haveno já está em execução. Você não pode executar duas instâncias do Haveno. popup.warning.tradePeriod.halfReached=Sua negociação com o ID {0} atingiu a metade do valor máx. do período de negociação permitido e ainda não está concluído.\n\nO período de negócio termina em {1}\n\nPor favor, verifique o seu estado de negócio em \"Portefólio/Ofertas abertas\" para mais informações. popup.warning.tradePeriod.ended=O seu negócio com o ID {0} atingiu o limite do máx. período de negociação permitido e não está concluído.\n\nO período de negócio terminou em {1}\n\nPor favor, verifique o seu negócio em \"Portefólio/Negócios abertos\" para entrar em contacto com o mediador. popup.warning.noTradingAccountSetup.headline=Você ainda não configurou uma conta de negociação popup.warning.noTradingAccountSetup.msg=Você precisa configurar uma conta de moeda nacional ou crypto antes de criar uma oferta.\nVocê quer configurar uma conta? popup.warning.noArbitratorsAvailable=Não há árbitros disponíveis. popup.warning.noMediatorsAvailable=Não há mediadores disponíveis. popup.warning.notFullyConnected=Você precisa esperar até estar totalmente conectado à rede.\nIsso pode levar cerca de 2 minutos na inicialização. popup.warning.notSufficientConnectionsToXmrNetwork=Você precisa esperar até que você tenha pelo menos {0} conexões com a rede Monero. popup.warning.downloadNotComplete=Você precisa esperar até que o download dos blocos de Monero ausentes esteja completo. popup.warning.walletNotSynced=A carteira Haveno não está sincronizada com a altura mais recente da blockchain. Por favor, aguarde até que a carteira seja sincronizada ou verifique a sua conexão. popup.warning.removeOffer=Tem certeza de que deseja remover essa oferta? popup.warning.tooLargePercentageValue=Você não pode definir uma percentagem superior à 100%. popup.warning.examplePercentageValue=Por favor digitar um número percentual como \"5.4\" para 5.4% popup.warning.noPriceFeedAvailable=Não há feed de preço disponível para essa moeda. Você não pode usar um preço baseado em percentagem.\nPor favor, selecione o preço fixo. popup.warning.sendMsgFailed=Enviar mensagem para seu par de negociação falhou.\nPor favor, tente novamente e se continuar a falhar relate um erro. popup.warning.messageTooLong=Sua mensagem excede o tamanho máx. permitido. Por favor enviá-la em várias partes ou carregá-la utilizando um serviço como https://pastebin.com. popup.warning.lockedUpFunds=Você trancou fundos de um negócio falhado..\nSaldo trancado: {0} \nEndereço da tx de Depósito: {1}\nID de negócio: {2}.\n\nPor favor abra um bilhete de apoio selecionando o negócio no ecrã de negócios abertos e pressione \"alt + o\" ou \"option + o\"." popup.warning.moneroConnection=Houve um problema ao conectar-se à rede Monero.\n\n{0} popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n takeOffer.cancelButton=Cancel take-offer takeOffer.warningButton=Ignore and continue anyway # suppress inspection "UnusedProperty" popup.warning.nodeBanned=One of the {0} nodes got banned. # suppress inspection "UnusedProperty" popup.warning.priceRelay=transmissão de preço popup.warning.seed=semente popup.warning.mandatoryUpdate.trading=Por favor, atualize para a versão mais recente do Haveno. Uma atualização obrigatória que desativa negociação para versões antigas foi lançada. Por favor, confira o Fórum Haveno para mais informações. popup.warning.noFilter=Não recebemos um objeto de filtro dos nós sementes. Por favor, informe os administradores da rede para registrar um objeto de filtro. popup.warning.burnXMR=Esta transação não é possível, pois as taxas de mineração de {0} excederia o montante a transferir de {1}. Aguarde até que as taxas de mineração estejam novamente baixas ou até você ter acumulado mais XMR para transferir. popup.warning.openOffer.makerFeeTxRejected=A transação da taxa de ofertante para a oferta com o ID {0} foi rejeitada pela rede do Monero.\nID da transação={1}.\nA oferta foi removida para evitar futuros problemas.\nPor favor vá à \"Definições/Informação da Rede\" e re-sincronize o ficheiro SPV.\nPara mais ajuda por favor contacte o canal de apoio do Haveno na equipa Keybase do Haveno. popup.warning.trade.txRejected.tradeFee=taxa de negócio popup.warning.trade.txRejected.deposit=depósito popup.warning.trade.txRejected=The {0} transaction for trade with ID {1} was rejected by the Monero network.\nTransaction ID={2}\nThe trade has been moved to failed trades.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. popup.warning.openOfferWithInvalidMakerFeeTx=A transação de taxa de ofertante para a oferta com o ID {0} é inválida\nID da transação={1}.\nPor favor vá à \"Definições/Informação da Rede\" e re-sincronize o ficheiro SPV.\nPara mais ajuda por favor contacte o canal de apoio do Haveno na equipa Keybase do Haveno. popup.info.securityDepositInfo=Para garantir que ambos os negociadores seguem o protocolo de negócio, ambos os negociadores precisam pagar um depósito de segurança.\n\nEsse depósito é mantido na sua carteira de negócio até que o seu negócio seja concluído com sucesso, e então lhe será reembolsado.\n\nPor favor note: se você está criando uma nova oferta, o Haveno precisa estar em execução para que um outro negociador a aceite. Para manter suas ofertas online, mantenha o Haveno em execução e certifique-se de que este computador permaneça online também (ou seja, certifique-se de que ele não alterne para o modo de espera... o modo de espera do monitor não causa problema). popup.info.cashDepositInfo=Por favor, certifique-se de que você tem uma agência bancária na sua área para poder fazer o depósito em dinheiro.\nO ID do banco (BIC/SWIFT) do vendedor é: {0}. popup.info.cashDepositInfo.confirm=Eu confirmo que eu posso fazer o depósito popup.info.shutDownWithOpenOffers=Haveno está sendo fechado, mas há ofertas abertas. \n\nEstas ofertas não estarão disponíveis na rede P2P enquanto o Haveno estiver desligado, mas elas serão publicadas novamente na rede P2P na próxima vez que você iniciar o Haveno.\n\nPara manter suas ofertas on-line, mantenha o Haveno em execução e certifique-se de que este computador também permaneça online (ou seja, certifique-se de que ele não entra no modo de espera... o modo de espera do monitor não causa problema). popup.info.qubesOSSetupInfo=It appears you are running Haveno on Qubes OS. \n\nPlease make sure your Haveno qube is setup according to our Setup Guide at [HYPERLINK:https://haveno.exchange/wiki/Running_Haveno_on_Qubes]. popup.warn.downGradePrevention=Downgrade from version {0} to version {1} is not supported. Please use the latest Haveno version. popup.privateNotification.headline=Notificação privada importante! popup.securityRecommendation.headline=Recomendação de segurança importante popup.securityRecommendation.msg=Gostaríamos de lembrá-lo de considerar a possibilidade de usar a proteção por senha para sua carteira, caso você ainda não tenha ativado isso.\n\nTambém é altamente recomendável anotar as palavras-semente da carteira. Essas palavras-semente são como uma senha mestre para recuperar sua carteira Monero.\nNa secção \"Semente da Carteira\", você encontrará mais informações.\n\nAlém disso, você deve fazer o backup da pasta completa de dados do programa na secção \"Backup\". popup.shutDownInProgress.headline=Desligando popup.shutDownInProgress.msg=Desligar o programa pode demorar alguns segundos.\nPor favor não interrompa este processo. popup.attention.forTradeWithId=Atenção necessária para o negócio com ID {0} popup.attention.reasonForPaymentRuleChange=Version 1.5.5 introduces a critical trade rule change regarding the \"reason for payment\" field in bank transfers. Please leave this field empty -- DO NOT use the trade ID as \"reason for payment\" anymore. popup.info.multiplePaymentAccounts.headline=Múltiplas contas de pagamento disponíveis popup.info.multiplePaymentAccounts.msg=Você tem várias contas de pagamento disponíveis para esta oferta. Por favor, verifique se você escolheu a correta. popup.accountSigning.selectAccounts.headline=Selecionar contas de pagamento popup.accountSigning.selectAccounts.description=Com base no método de pagamento e no momento, todas as contas de pagamento conectadas a uma disputa em que ocorreu um pagamento ao comprador serão selecionadas para você assinar. popup.accountSigning.selectAccounts.signAll=Assinar todos os métodos de pagamento popup.accountSigning.selectAccounts.datePicker=Selecione o ponto no tempo até o qual as contas serão assinadas popup.accountSigning.confirmSelectedAccounts.headline=Confirmar contas de pagamento selecionadas popup.accountSigning.confirmSelectedAccounts.description=Com base no seu input, {0} contas de pagamento serão selecionadas. popup.accountSigning.confirmSelectedAccounts.button=Confirmar contas de pagamento popup.accountSigning.signAccounts.headline=Confirmar a assinatura de contas de pagamento popup.accountSigning.signAccounts.description=Com base na sua seleção, {0} contas de pagamento serão assinadas. popup.accountSigning.signAccounts.button=Assinar contas de pagamento popup.accountSigning.signAccounts.ECKey=Inserir a chave privada do árbitro popup.accountSigning.signAccounts.ECKey.error=Má ECKey do árbitro popup.accountSigning.success.headline=Parabéns popup.accountSigning.success.description=Todas as contas de pagamento de {0} foram assinadas com sucesso! popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\nFor further information, please visit [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. popup.accountSigning.signedByArbitrator=Uma das suas contas de pagamento foi verificada e assinada por um árbitro. Fazendo negócios com esta conta assinará automaticamente a conta do seu par de negociação após um negócio bem-sucedido.\n\n{0} popup.accountSigning.signedByPeer=Uma das suas contas de pagamento foi verificada e assinada por um par de negociação. Seu limite inicial de negociação será aumentado e você poderá assinar outras contas dentro de {0} dias a partir de agora.\n\n{1} popup.accountSigning.peerLimitLifted=O limite inicial de uma das suas contas foi aumentado.\n\n{0} popup.accountSigning.peerSigner=Uma das suas contas tem maturidade suficiente para assinar outras contas de pagamento e o limite inicial de uma delas foi aumentado.\n\n{0} popup.accountSigning.singleAccountSelect.headline=Import unsigned account age witness popup.accountSigning.confirmSingleAccount.headline=Confirm selected account age witness popup.accountSigning.confirmSingleAccount.selectedHash=Selected witness hash popup.accountSigning.confirmSingleAccount.button=Sign account age witness popup.accountSigning.successSingleAccount.description=Witness {0} was signed popup.accountSigning.successSingleAccount.success.headline=Success popup.accountSigning.unsignedPubKeys.headline=Unsigned Pubkeys popup.accountSigning.unsignedPubKeys.sign=Sign Pubkeys popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign popup.info.buyerAsTakerWithoutDeposit.headline=Nenhum depósito exigido do comprador popup.info.buyerAsTakerWithoutDeposit=Sua oferta não exigirá um depósito de segurança ou taxa do comprador de XMR.\n\nPara aceitar sua oferta, você deve compartilhar uma senha com seu parceiro comercial fora do Haveno.\n\nA senha é gerada automaticamente e exibida nos detalhes da oferta após a criação. #################################################################### # Notifications #################################################################### notification.trade.headline=Notificação para o oferta com ID {0} notification.ticket.headline=Bilhete de apoio para o negócio com ID {0} notification.trade.completed=O negócio completou e você já pode levantar seus fundos. notification.trade.accepted=Sua oferta foi aceite por um {0} de XMR. notification.trade.unlocked=Seu negócio tem pelo menos uma confirmação da blockchain.\nVocê pode começar o pagamento agora. notification.trade.paymentSent=O comprador de XMR iniciou o pagamento notification.trade.selectTrade=Selecionar negócio notification.trade.peerOpenedDispute=Seu par de negociação abriu um {0}. notification.trade.disputeClosed=A {0} foi fechada. notification.walletUpdate.headline=Atualização da carteira de negociação notification.walletUpdate.msg=A sua carteira está suficientemente financiada.\nQuantia: {0} notification.takeOffer.walletUpdate.msg=A sua carteira de negociação já estava suficientemente financiada por uma tentativa de aceitação de oferta anterior.\nQuantia: {0} notification.tradeCompleted.headline=Negócio concluído notification.tradeCompleted.msg=Você pode sacar seus fundos para uma carteira externa de Monero ou mantê-los em sua carteira Haveno. #################################################################### # System Tray #################################################################### systemTray.show=Mostrar janela do programa systemTray.hide=Esconder janela do programa systemTray.info=Informação sobre Haveno systemTray.exit=Sair systemTray.tooltip=Haveno: Uma rede de echange de monero descentralizada #################################################################### # GUI Util #################################################################### guiUtil.accountExport.savedToPath=Contas de negociação guardadas em:\n{0} guiUtil.accountExport.noAccountSetup=Você não tem contas de negociação prontas para exportar. guiUtil.accountExport.selectPath=Selecione diretório de {0} # suppress inspection "TrailingSpacesInProperty" guiUtil.accountExport.tradingAccount=Conta de negociação com id {0}\n # suppress inspection "TrailingSpacesInProperty" guiUtil.accountImport.noImport=Nós não importamos a conta de negociação com o id {0} porque já existe.\n guiUtil.accountExport.exportFailed=A exportação para CSV falhou devido à um erro.\nErro = {0} guiUtil.accountExport.selectExportPath=Selecionar diretório para exportar guiUtil.accountImport.imported=Conta de negociação importada de:\n{0}\n\nContas importadas:\n{1} guiUtil.accountImport.noAccountsFound=Nenhuma conta de negociação exportada foi encontrada em: {0}.\nO nome do ficheiro é {1}. " guiUtil.openWebBrowser.warning=Você vai abrir uma página web no navegador da web do seu sistema.\nVocê quer abrir a página web agora?\n\nSe você não estiver usando o \"Navegador Tor\" como seu navegador web padrão do sistema, você irá se conectar à página web em rede transparente.\n\nURL: \"{0}\" guiUtil.openWebBrowser.doOpen=Abrir a página web e não perguntar novamente guiUtil.openWebBrowser.copyUrl=Copiar URL e cancelar guiUtil.ofTradeAmount=da quantia de negócio guiUtil.requiredMinimum=(mínimo requerido) #################################################################### # Component specific #################################################################### list.currency.select=Selecione a moeda list.currency.showAll=Ver todos list.currency.editList=Editar lista de moedas table.placeholder.noItems=Atualmente não há {0} disponíveis table.placeholder.noData=Não há dados disponíveis no momento table.placeholder.processingData=Processando os dados... peerInfoIcon.tooltip.tradePeer=Do par de negociação peerInfoIcon.tooltip.maker=Do ofertante peerInfoIcon.tooltip.trade.traded={0} endereço onion: {1}\nVocê já negociou {2} vez(es) com esse par\n{3} peerInfoIcon.tooltip.trade.notTraded={0} endereço onion: {1}\nVocê não negociou com esse par até agora.\n{2} peerInfoIcon.tooltip.age=Conta de pagamento criada há {0}. peerInfoIcon.tooltip.unknownAge=Idade de conta de pagamento desconhecida. tooltip.openPopupForDetails=Abrir popup para mais detalhes tooltip.invalidTradeState.warning=This trade is in an invalid state. Open the details window for more information tooltip.openBlockchainForAddress=Abrir um explorador de blockchain externo para endereço: {0} tooltip.openBlockchainForTx=Abrir um explorador de blockchain externo para transação: {0} confidence.unknown=Estado da transação desconhecido confidence.seen=Visto por {0} par(es) / 0 confirmações confidence.confirmed={0} confirmação(ões) confidence.invalid=A transação é inválida peerInfo.title=Informação do par peerInfo.nrOfTrades=Número de negócios completos peerInfo.notTradedYet=Você não negociou com esse utilizador até agora. peerInfo.setTag=Definir o rótulo para esse par peerInfo.age.noRisk=Idade da conta de pagamento peerInfo.age.chargeBackRisk=Tempo desde a assinatura peerInfo.unknownAge=Idade desconhecida addressTextField.openWallet=Abrir sua carteira Monero padrão addressTextField.copyToClipboard=Copiar endereço para área de transferência addressTextField.addressCopiedToClipboard=Endereço copiado para área de transferência addressTextField.openWallet.failed=Abrir a programa de carteira Monero padrão falhou. Talvez você não tenha um instalado? peerInfoIcon.tooltip={0}\nRótulo: {1} txIdTextField.copyIcon.tooltip=Copiar ID de transação txIdTextField.blockExplorerIcon.tooltip=Open a blockchain explorer with this transaction ID txIdTextField.missingTx.warning.tooltip=Missing required transaction #################################################################### # Navigation #################################################################### navigation.account=\"Conta\" navigation.account.walletSeed=\"Conta/Semente da carteira\" navigation.funds.availableForWithdrawal=\"Funds/Send funds\" navigation.portfolio.myOpenOffers=\"Portefólio/As minhas ofertas abertas\" navigation.portfolio.pending=\"Portefólio/Negócios abertos\" navigation.portfolio.closedTrades=\"Portefólio/Histórico\" navigation.funds.depositFunds=\"Fundos/Receber fundos\" navigation.settings.preferences=\"Definições/Preferências\" # suppress inspection "UnusedProperty" navigation.funds.transactions=\"Fundos/Transações\" navigation.support=\"Apoio\" #################################################################### # Formatter #################################################################### formatter.formatVolumeLabel={0} quantia{1} formatter.makerTaker=Ofertante como {0} {1} / Aceitador como {2} {3} formatter.makerTaker.locked=Ofertante como {0} {1} / Aceitador como {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=Você é {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Você está criando uma oferta para {0} {1} formatter.youAreCreatingAnOffer.traditional.locked=Você está criando uma oferta para {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Você está criando uma oferta para {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.crypto.locked=Você está criando uma oferta para {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} como ofertante formatter.asTaker={0} {1} como aceitador #################################################################### # Domain specific #################################################################### # we use enum values here # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" XMR_MAINNET=Mainnet de Monero # suppress inspection "UnusedProperty" XMR_LOCAL=Testnet de Monero # suppress inspection "UnusedProperty" XMR_STAGENET=Stagenet Monero time.year=Ano time.month=Mês time.week=Semana time.day=Dia time.hour=Hora time.minute10=10 Minutos time.hours=horas time.days=dias time.1hour=1 hora time.1day=1 dia time.minute=minuto time.second=segundo time.minutes=minutos time.seconds=segundos password.enterPassword=Inserir senha password.confirmPassword=Confirmar senha password.tooLong=A senha deve ter menos de 500 caracteres. password.deriveKey=Derivar chave a partir da senha password.walletDecrypted=A carteira foi descriptografada com sucesso e a proteção por senha removida. password.wrongPw=Você digitou a senha errada.\n\nPor favor, tente digitar sua senha novamente, verificando com atenção se há erros de ortografia. password.walletEncrypted=Carteira encriptada com sucesso e proteção por senha ativada. password.passwordsDoNotMatch=As 2 senhas inseridas não são iguais. password.forgotPassword=Esqueceu a senha? password.backupReminder=Please note that when setting a wallet password all automatically created backups from the unencrypted wallet will be deleted.\n\nIt is highly recommended that you make a backup of the application directory and write down your seed words before setting a password! password.backupWasDone=I have already made a backup password.setPassword=Set Password (I already made a backup) password.makeBackup=Make Backup seed.seedWords=Palavras-semente da carteira seed.enterSeedWords=Inserir as palavras-semente da carteira seed.date=Data da carteira seed.restore.title=Restaurar carteira a partir de palavras-semente seed.restore=Restaurar carteiras seed.creationDate=Data de criação seed.warn.walletNotEmpty.msg=Your Monero wallet is not empty.\n\nYou must empty this wallet before attempting to restore an older one, as mixing wallets together can lead to invalidated backups.\n\nPlease finalize your trades, close all your open offers and go to the Funds section to withdraw your monero.\nIn case you cannot access your monero you can use the emergency tool to empty the wallet.\nTo open the emergency tool press \"Alt+e\" or \"Cmd/Ctrl+e\". seed.warn.walletNotEmpty.restore=Eu desejo restaurar de qualquer forma seed.warn.walletNotEmpty.emptyWallet=Eu esvaziarei as carteiras primeiro seed.warn.notEncryptedAnymore=Suas carteiras são encriptadas.\n\nApós a restauração, as carteiras não serão mais encriptadas e você deverá definir uma nova senha.\n\nVocê quer continuar? seed.warn.walletDateEmpty=As you have not specified a wallet date, haveno will have to scan the blockchain from 2013.10.09 (the BIP39 epoch date).\n\nBIP39 wallets were first introduced in haveno on 2017.06.28 (release v0.5). So you could save time by using that date.\n\nIdeally you should specify the date your wallet seed was created.\n\n\nAre you sure you want to go ahead without specifying a wallet date? seed.restore.success=Carteiras restauradas com sucesso com as novas palavras-semente.\n\nVocê precisa desligar e reiniciar o programa. seed.restore.error=Um erro ocorreu ao restaurar as carteiras com palavras-semente.{0} seed.restore.openOffers.warn=You have open offers which will be removed if you restore from seed words.\nAre you sure that you want to continue? #################################################################### # Payment methods #################################################################### payment.account=Conta payment.account.no=Nº da conta payment.account.name=Nome da conta payment.account.username=Username payment.account.phoneNr=Phone number payment.account.owner.fullname=Nome completo do titular da conta payment.account.fullName=Nome completo (primeiro, nome do meio, último) payment.account.state=Estado/Província/Região payment.account.city=Cidade payment.bank.country=País do banco payment.account.name.email=Nome completo do titular da conta / email payment.account.name.emailAndHolderId=Nome completo do titular da conta / email / {0} payment.bank.name=Nome do banco payment.select.account=Selecione o tipo de conta payment.select.region=Selecionar região payment.select.country=Selecionar país payment.select.bank.country=Selecionar país do banco payment.foreign.currency=Tem certeza que deseja selecionar uma moeda que não seja a moeda padrão do pais? payment.restore.default=Não, resturar para a moeda padrão payment.email=Email payment.country=País payment.extras=Requerimentos adicionais payment.email.mobile=Email ou nº de telemóvel payment.crypto.address=Endereço de crypto payment.crypto.tradeInstantCheckbox=Negócio instantâneo (dentro de 1 hora) com esta Crypto payment.crypto.tradeInstant.popup=Para negociação instantânea, é necessário que os dois pares de negociação estejam online para concluir o negócio em menos de 1 hora..\n\nSe você tem ofertas abertas e você não está disponível, por favor desative essas ofertas no ecrã 'Portefólio'. payment.crypto=Crypto payment.select.crypto=Select or search Crypto payment.secret=Pergunta secreta payment.answer=Resposta payment.wallet=ID da carteira payment.amazon.site=Buy giftcard at payment.ask=Ask in Trader Chat payment.uphold.accountId=Nome de utilizador, email ou nº de telemóvel payment.moneyBeam.accountId=Email ou nº de telemóvel payment.popmoney.accountId=Email ou nº de telemóvel payment.promptPay.promptPayId=ID de cidadão/ID de impostos ou nº de telemóvel payment.supportedCurrencies=Moedas suportadas payment.supportedCurrenciesForReceiver=Currencies for receiving funds payment.limitations=Limitações payment.salt=Sal para verificação da idade da conta payment.error.noHexSalt=The salt needs to be in HEX format.\nIt is only recommended to edit the salt field if you want to transfer the salt from an old account to keep your account age. The account age is verified by using the account salt and the identifying account data (e.g. IBAN). payment.accept.euro=Aceitar negócios destes países do Euro payment.accept.nonEuro=Aceitar negócios desses países fora do Euro payment.accepted.countries=Países aceites payment.accepted.banks=Bancos aceites (ID) payment.mobile=Nº de telemóvel payment.postal.address=Morada postal payment.national.account.id.AR=Numero CBU shared.accountSigningState=Estado da assinatura da conta #new payment.crypto.address.dyn=Endereço de {0} payment.crypto.receiver.address=Endereço crypto do recipiente payment.accountNr=Número da conta payment.emailOrMobile=Email ou nº de telemóvel payment.useCustomAccountName=Usar nome de conta personalizado: payment.maxPeriod=Período máx. de negócio payment.maxPeriodAndLimit=Duração máx. de negócio: {0} / Compra máx.: {1} / Venda máx.: {2} / Idade da conta: {3} payment.maxPeriodAndLimitCrypto=Período máx. de negócio {0} / Limite máx. de negócio: {1} payment.currencyWithSymbol=Moeda: {0} payment.nameOfAcceptedBank=Nome do banco aceite payment.addAcceptedBank=Adicionar banco aceite payment.clearAcceptedBanks=Limpar bancos aceites payment.bank.nameOptional=Nome do banco (opcional) payment.bankCode=Código do Banco payment.bankId=ID do banco (BIC/SWIFT): payment.bankIdOptional=ID do banco (BIC/SWIFT) (opcional) payment.branchNr=Nº da agência: payment.branchNrOptional=Nº da agência (opcional): payment.accountNrLabel=Nº da conta (IBAN) payment.accountType=Tipo de conta payment.checking=Conta Corrente payment.savings=Poupança payment.personalId=ID pessoal payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle's somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver's full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account's age and signing status are preserved. payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=Ao usar o HalCash, o comprador de XMR precisa enviar ao vendedor de XMR o código HalCash através de uma mensagem de texto do seu telemóvel.\n\nPor favor, certifique-se de não exceder a quantia máxima que seu banco lhe permite enviar com o HalCash. A quantia mín. de levantamento é de 10 euros e a quantia máx. é de 600 EUR. Para levantamentos repetidos é de 3000 euros por recipiente por dia e 6000 euros por recipiente por mês. Por favor confirme esses limites com seu banco para ter certeza de que eles usam os mesmos limites mencionados aqui.\n\nA quantia de levantamento deve ser um múltiplo de 10 euros, pois você não pode levantar outras quantias de uma ATM. A interface do utilizador no ecrã para criar oferta e aceitar ofertas ajustará a quantia de XMR para que a quantia de EUR esteja correta. Você não pode usar o preço com base no mercado, pois o valor do EUR estaria mudando com a variação dos preços.\n\nEm caso de disputa, o comprador de XMR precisa fornecer a prova de que enviou o EUR. # suppress inspection "UnusedMessageFormatParameter" payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/overview/account_limits]. # suppress inspection "UnusedProperty" payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/overview/account_limits]. payment.cashDeposit.info=Por favor, confirme que seu banco permite-lhe enviar depósitos em dinheiro para contas de outras pessoas. Por exemplo, o Bank of America e o Wells Fargo não permitem mais esses depósitos. payment.revolut.info=Revolut requires the 'Username' as account ID not the phone number or email as it was the case in the past. payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a 'Username'.\nPlease enter your Revolut 'Username' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account payment.cashapp.info=Esteja ciente de que o Cash App tem um risco de estorno maior do que a maioria das transferências bancárias. payment.venmo.info=Esteja ciente de que o Venmo tem um risco de estorno maior do que a maioria das transferências bancárias. payment.paypal.info=Esteja ciente de que o PayPal tem um risco de estorno maior do que a maioria das transferências bancárias. payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account payment.payByMail.contact=Informação de contacto payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.f2f.contact=Informação de contacto payment.f2f.contact.prompt=How would you like to be contacted by the trading peer? (email address, phone number,...) payment.f2f.city=Cidade para o encontro 'Face à face' payment.f2f.city.prompt=A cidade será exibida com a oferta payment.shared.optionalExtra=Informação adicional opcional payment.shared.extraInfo=Informação adicional payment.shared.extraInfo.offer=Informações adicionais sobre a oferta payment.shared.extraInfo.prompt.paymentAccount=Defina quaisquer termos especiais, condições ou detalhes que você gostaria de exibir com suas ofertas para esta conta de pagamento (os usuários verão essas informações antes de aceitar as ofertas). payment.shared.extraInfo.prompt.offer=Defina quaisquer termos, condições ou detalhes especiais que você gostaria de exibir com sua oferta. payment.shared.extraInfo.noDeposit=Detalhes de contato e termos da oferta payment.f2f.info.openURL=Abrir página web payment.f2f.offerbook.tooltip.countryAndCity=País e cidade: {0} / {1} payment.shared.extraInfo.tooltip=Informação adicional: {0} payment.japan.bank=Banco payment.japan.branch=Agência payment.japan.account=Conta payment.japan.recipient=Nome payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=Para sua proteção, desaconselhamos fortemente o uso de PINs do Paysafecard para pagamento.\n\n\ Transações feitas por PINs não podem ser verificadas de forma independente para resolução de disputas. Se ocorrer um problema, a recuperação dos fundos pode não ser possível.\n\n\ Para garantir a segurança das transações com resolução de disputas, sempre use métodos de pagamento que forneçam registros verificáveis. # We use constants from the code so we do not use our normal naming convention # dynamic values are not recognized by IntelliJ # Only translate general terms NATIONAL_BANK=Transferência bancária nacional SAME_BANK=Transferência para mesmo banco SPECIFIC_BANKS=Transferência com banco escpecífico US_POSTAL_MONEY_ORDER=US Postal Money Order CASH_DEPOSIT=Depósito em dinheiro PAY_BY_MAIL=Pay By Mail MONEY_GRAM=MoneyGram WESTERN_UNION=Western Union F2F=Face à face (em pessoa) JAPAN_BANK=Japan Bank Furikomi AUSTRALIA_PAYID=Australian PayID # suppress inspection "UnusedProperty" NATIONAL_BANK_SHORT=Bancos nacionais # suppress inspection "UnusedProperty" SAME_BANK_SHORT=Mesmo banco # suppress inspection "UnusedProperty" SPECIFIC_BANKS_SHORT=Bancos específicos # suppress inspection "UnusedProperty" US_POSTAL_MONEY_ORDER_SHORT=US Money Order # suppress inspection "UnusedProperty" CASH_DEPOSIT_SHORT=Depósito em dinheiro # suppress inspection "UnusedProperty" PAY_BY_MAIL_SHORT=PayByMail # suppress inspection "UnusedProperty" MONEY_GRAM_SHORT=MoneyGram # suppress inspection "UnusedProperty" WESTERN_UNION_SHORT=Western Union # suppress inspection "UnusedProperty" F2F_SHORT=F2F # suppress inspection "UnusedProperty" JAPAN_BANK_SHORT=Japan Furikomi # suppress inspection "UnusedProperty" AUSTRALIA_PAYID_SHORT=PayID # Do not translate brand names # suppress inspection "UnusedProperty" UPHOLD=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY=Popmoney # suppress inspection "UnusedProperty" REVOLUT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY=WeChat Pay # suppress inspection "UnusedProperty" SEPA=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT=SEPA Instant Payments # suppress inspection "UnusedProperty" FASTER_PAYMENTS=Faster Payments # suppress inspection "UnusedProperty" SWISH=Swish # suppress inspection "UnusedProperty" ZELLE=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS=Cryptos # suppress inspection "UnusedProperty" PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT=Cryptos Instantâneas # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY=OKPay # suppress inspection "UnusedProperty" CASH_APP=Cash App # suppress inspection "UnusedProperty" VENMO=Venmo # suppress inspection "UnusedProperty" UPHOLD_SHORT=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM_SHORT=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY_SHORT=Popmoney # suppress inspection "UnusedProperty" REVOLUT_SHORT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY_SHORT=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY_SHORT=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY_SHORT=WeChat Pay # suppress inspection "UnusedProperty" SEPA_SHORT=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT_SHORT=SEPA Instant # suppress inspection "UnusedProperty" FASTER_PAYMENTS_SHORT=Faster Payments # suppress inspection "UnusedProperty" SWISH_SHORT=Swish # suppress inspection "UnusedProperty" ZELLE_SHORT=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY_SHORT=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER_SHORT=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH_SHORT=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS_SHORT=Cryptos # suppress inspection "UnusedProperty" PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT_SHORT=Cryptos Instant # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY_SHORT=OKPay # suppress inspection "UnusedProperty" CASH_APP_SHORT=Cash App # suppress inspection "UnusedProperty" VENMO_SHORT=Venmo #################################################################### # Validation #################################################################### validation.empty=Campo vazio não é permitido validation.NaN=Número inválido validation.notAnInteger=O input não é um número inteiro. validation.zero=Número 0 não é permitido validation.negative=Valores negativos não são permitidos. validation.traditional.tooSmall=Input menor do que a quantia mínima permitida. validation.traditional.tooLarge=Input maior do que a quantia máxima permitida. validation.xmr.fraction=Input will result in a monero value of less than 1 satoshi validation.xmr.tooLarge=O input maior que {0} não é permitido. validation.xmr.tooSmall=Input menor que {0} não é permitido. validation.passwordTooShort=The password you entered is too short. It needs to have a min. of 8 characters. validation.passwordTooLong=A senha inserida é muito longa. Não pode ser maior do que 50 caracteres. validation.sortCodeNumber={0} deve consistir de {1} números. validation.sortCodeChars={0} deve consistir de {1} caracteres. validation.bankIdNumber={0} deve consistir de {1 números. validation.accountNr=O número de conta deve conter {0} números. validation.accountNrChars=O número da conta deve conter {0} caracteres. validation.xmr.invalidAddress=O endereço está incorreto. Por favor verificar o formato do endereço. validation.integerOnly=Por favor, insira apenas números inteiros validation.inputError=O seu input causou um erro:\n{0} validation.xmr.exceedsMaxTradeLimit=O seu limite de negócio é de {0}. validation.nationalAccountId={0} tem de ser constituído por {1} números #new validation.invalidInput=Input inválido: {0} validation.accountNrFormat=O número da conta deve estar no formato: {0} # suppress inspection "UnusedProperty" validation.crypto.wrongStructure=Validação do endereço falhou pois este não é compatível com a estrutura de um endereço {0}. # suppress inspection "UnusedProperty" validation.crypto.ltz.zAddressesNotSupported=LTZ address must start with L. Addresses starting with z are not supported. # suppress inspection "UnusedProperty" validation.crypto.zAddressesNotSupported=ZEC addresses must start with t. Addresses starting with z are not supported. # suppress inspection "UnusedProperty" validation.crypto.invalidAddress=Endereço não é um endereço {0} válido! {1} # suppress inspection "UnusedProperty" validation.crypto.liquidBitcoin.invalidAddress=Native segwit addresses (those starting with 'lq') are not supported. validation.bic.invalidLength=Input length must be 8 or 11 validation.bic.letters=Banco e Código de país devem ser letras validation.bic.invalidLocationCode=BIC contém código de localização inválido validation.bic.invalidBranchCode=BIC contém código da agência inválido validation.bic.sepaRevolutBic=Contas Revolut SEPA não são suportadas. validation.btc.invalidFormat=Invalid format for a Bitcoin address. validation.email.invalidAddress=Endereço inválido validation.iban.invalidCountryCode=Código de país inválido validation.iban.checkSumNotNumeric=Soma de verificação deve ser numérica validation.iban.nonNumericChars=Carácter não alfanumérico detectado validation.iban.checkSumInvalid=Soma de verificação dp IBAN é inválida validation.iban.invalidLength=Number must have a length of 15 to 34 chars. validation.interacETransfer.invalidAreaCode=Código de área não é canadense. validation.interacETransfer.invalidPhone=Please enter a valid 11 digit phone number (ex: 1-123-456-7890) or an email address validation.interacETransfer.invalidQuestion=Deve conter apenas letras, números, espaços e/ou símbolos ' _ , . ? - validation.interacETransfer.invalidAnswer=Deve ser uma palavra e conter apenas letras, números e/ou o símbolo - validation.inputTooLarge=O input não deve ser maior que {0} validation.inputTooSmall=O input deve ser maior que {0} validation.inputToBeAtLeast=O input tem de ser pelo menos {0} validation.amountBelowDust=An amount below the dust limit of {0} satoshi is not allowed. validation.length=O comprimento deve estar entre {0} e {1} validation.fixedLength=Length must be {0} validation.pattern=O input deve ter o formato: {0} validation.noHexString=O input não está no formato HEX. validation.advancedCash.invalidFormat=Deve ser um email válido ou id de carteira de formato: X000000000000 validation.invalidUrl=Este não é um URL válido validation.mustBeDifferent=O seu input deve ser diferente do valor atual validation.cannotBeChanged=O parâmetro não pode ser alterado validation.numberFormatException=Exceção do formato do número {0} validation.mustNotBeNegative=O input não deve ser negativo validation.phone.missingCountryCode=É preciso o código do país de duas letras para validar o número de telefone validation.phone.invalidCharacters=O número de telfone {0} contém carácteres inválidos validation.phone.insufficientDigits=There are not enough digits in {0} to be a valid phone number validation.phone.tooManyDigits=There are too many digits in {0} to be a valid phone number validation.phone.invalidDialingCode=Country dialing code for number {0} is invalid for country {1}. The correct dialing code is {2}. validation.invalidAddressList=Deve ser um lista de endereços válidos separados por vírgulas ================================================ FILE: core/src/main/resources/i18n/displayStrings_ru.properties ================================================ # Keep display strings organized by domain # Naming convention: We use camelCase and dot separated name spaces. # Use as many sub spaces as required to make the structure clear, but as little as possible. # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break # in the display but only in the editor. # Please use in all language files the exact same order of the entries, that way a comparison is easier. # Please try to keep the length of the translated string similar to English. If it is longer it might break layout or # get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. #################################################################### # Shared #################################################################### shared.readMore=Подробнее shared.openHelp=Открыть раздел помощи shared.warning=Предупреждение shared.close=Закрыть shared.cancel=Отменить shared.ok=Ок shared.yes=Да shared.no=Нет shared.iUnderstand=Я понимаю shared.na=Н/Д shared.shutDown=Закрыть shared.reportBug=Report bug on GitHub shared.buyMonero=Купить биткойн shared.sellMonero=Продать биткойн shared.buyCurrency=Купить {0} shared.sellCurrency=Продать {0} shared.buyCurrency.locked=Купить {0} 🔒 shared.sellCurrency.locked=Продать {0} 🔒 shared.buyingXMRWith=покупка ВТС за {0} shared.sellingXMRFor=продажа ВТС за {0} shared.buyingCurrency=покупка {0} (продажа ВТС) shared.sellingCurrency=продажа {0} (покупка ВТС) shared.buy=покупки shared.sell=продажи shared.buying=покупка shared.selling=продажа shared.P2P=P2P shared.oneOffer=предложение shared.multipleOffers=предложения shared.Offer=Предложение shared.offerVolumeCode={0} Offer Volume shared.openOffers=текущие предложения shared.trade=сделка shared.trades=сделки shared.openTrades=текущие сделки shared.dateTime=Дата/время shared.price=Курс shared.priceWithCur=Цена в {0} shared.priceInCurForCur=Цена в {0} за 1 {1} shared.fixedPriceInCurForCur=Фиксированная цена в {0} за 1 {1} shared.amount=Количество shared.txFee=Transaction Fee shared.tradeFee=Trade Fee shared.buyerSecurityDeposit=Buyer Deposit shared.sellerSecurityDeposit=Seller Deposit shared.amountWithCur=Количество в {0} shared.volumeWithCur=Объём в {0} shared.currency=Валюта shared.market=Рынок shared.deviation=Deviation shared.paymentMethod=Способ оплаты shared.tradeCurrency=Торговая валюта shared.offerType=Тип предложения shared.details=Подробности shared.address=Адрес shared.balanceWithCur=Баланс в {0} shared.utxo=Unspent transaction output shared.txId=Идентификатор транзакции shared.confirmations=Подтверждения shared.revert=Отменить транзакцию shared.select=Выбрать shared.usage=Использование shared.state=Статус shared.tradeId=Идентификатор сделки shared.offerId=Идентификатор предложения shared.bankName=Название банка shared.acceptedBanks=Одобренные банки shared.amountMinMax=Количество (мин. — макс.) shared.amountHelp=Если предложение включает диапазон сделки, вы можете обменять любую сумму в этом диапазоне. shared.remove=Удалить shared.goTo=Перейти к {0} shared.XMRMinMax=ВТС (мин. — макс.) shared.removeOffer=Удалить предложение shared.dontRemoveOffer=Не удалять предложение shared.editOffer=Изменить предложение shared.openLargeQRWindow=Open large QR code window shared.tradingAccount=Торговый счёт shared.faq=Visit FAQ page shared.yesCancel=Да, отменить shared.nextStep=Далее shared.selectTradingAccount=Выбрать торговый счёт shared.fundFromSavingsWalletButton=Применить средства из кошелька Haveno shared.fundFromExternalWalletButton=Открыть внешний кошелёк для пополнения shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? shared.belowInPercent=% ниже рыночного курса shared.aboveInPercent=% выше рыночного курса shared.enterPercentageValue=Ввести величину в % shared.OR=ИЛИ shared.notEnoughFunds=You don't have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. shared.waitingForFunds=Ожидание средств... shared.TheXMRBuyer=Покупатель ВТС shared.You=Вы shared.sendingConfirmation=Отправка подтверждения... shared.sendingConfirmationAgain=Отправьте подтверждение повторно shared.exportCSV=Export to CSV shared.exportJSON=Экспорт в JSON shared.summary=Show summary shared.noDateAvailable=Дата не указана shared.noDetailsAvailable=Подробности не указаны shared.notUsedYet=Ещё не использовано shared.date=Дата shared.sendFundsDetailsWithFee=Отправка: {0}\n\nНа получающий адрес: {1}\n\nДополнительная комиссия майнера: {2}\n\nВы уверены, что хотите отправить эту сумму? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Скопировать в буфер shared.language=Язык shared.country=Страна shared.applyAndShutDown=Применить и закрыть приложение shared.selectPaymentMethod=Выбрать способ оплаты shared.accountNameAlreadyUsed=That account name is already used for another saved account.\nPlease choose another name. shared.askConfirmDeleteAccount=Вы действительно хотите удалить выбранный счёт? shared.cannotDeleteAccount=You cannot delete that account because it is being used in an open offer (or in an open trade). shared.noAccountsSetupYet=Нет настроенных счетов shared.manageAccounts=Управление счетами shared.addNewAccount=Добавить новый счёт shared.ExportAccounts=Экспортировать счета shared.importAccounts=Импортировать счета shared.createNewAccount=Создать новый счёт shared.createNewAccountDescription=Данные вашей учетной записи хранятся локально на вашем устройстве и передаются только вашему торговому партнеру и арбитру, если открывается спор. shared.saveNewAccount=Сохранить новый счёт shared.selectedAccount=Выбранный счёт shared.deleteAccount=Удалить счёт shared.errorMessageInline=\nОшибка: {0} shared.errorMessage=Сообщение об ошибке shared.information=Информация shared.name=Имя shared.id=Идентификатор shared.dashboard=Панель управления shared.accept=Принять shared.balance=Баланс shared.save=Сохранить shared.onionAddress=Onion-адрес shared.supportTicket=запрос в службу поддержки shared.dispute=спор shared.mediationCase=mediation case shared.seller=продавец shared.buyer=покупатель shared.allEuroCountries=Все страны Еврозоны shared.acceptedTakerCountries=Одобренные страны для тейкера shared.tradePrice=Цена сделки shared.tradeAmount=Сумма сделки shared.tradeVolume=Объём сделки shared.invalidKey=Введён неправильный ключ. shared.enterPrivKey=Введите приватный ключ для разблокировки shared.payoutTxId=Идентификатор транзакции выплаты shared.contractAsJson=Контракт в формате JSON shared.viewContractAsJson=Просмотреть контракт в формате JSON shared.contract.title=Контракт сделки с идентификатором: {0} shared.paymentDetails=Подробности платежа ВТС {0} shared.securityDeposit=Залог shared.yourSecurityDeposit=Ваш залог shared.contract=Контракт shared.messageArrived=Сообщение получено. shared.messageStoredInMailbox=Сообщение сохранено в почтовом ящике. shared.messageSendingFailed=Ошибка отправки сообщения: {0} shared.unlock=Разблокировать shared.toReceive=получить shared.toSpend=потратить shared.xmrAmount=Сумма ВТС shared.yourLanguage=Ваши языки shared.addLanguage=Добавить язык shared.total=Всего shared.totalsNeeded=Требуемая сумма shared.tradeWalletAddress=Адрес кошелька сделки shared.tradeWalletBalance=Баланс кошелька сделки shared.reserveExactAmount=Резервируйте только необходимые средства. Требуется комиссия за майнинг и ~20 минут, прежде чем ваше предложение станет активным. shared.makerTxFee=Мейкер: {0} shared.takerTxFee=Тейкер: {0} shared.iConfirm=Подтверждаю shared.openURL=Открыть {0} shared.fiat=Нац. валюта shared.crypto=Криптовалюта shared.preciousMetals=Драгоценные металлы shared.all=Все shared.edit=Редактировать shared.advancedOptions=Дополнительные настройки shared.interval=Интервал shared.actions=Действия shared.buyerUpperCase=Покупатель shared.sellerUpperCase=Продавец shared.new=НОВОЕ shared.learnMore=Узнать больше shared.dismiss=Отмена shared.selectedArbitrator=Выбранный арбитр shared.selectedMediator=Selected mediator shared.selectedRefundAgent=Выбранный арбитр shared.mediator=Посредник shared.arbitrator=Арбитр shared.refundAgent=Арбитр shared.refundAgentForSupportStaff=Refund agent shared.delayedPayoutTxId=Delayed payout transaction ID shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later. shared.numItemsLabel=Number of entries: {0} shared.filter=Фильтр shared.enabled=Enabled #################################################################### # UI views #################################################################### #################################################################### # MainView #################################################################### mainView.menu.market=Рынок mainView.menu.buyXmr=Купить XMR mainView.menu.sellXmr=Продать XMR mainView.menu.portfolio=Сделки mainView.menu.funds=Средства mainView.menu.support=Поддержка mainView.menu.settings=Настройки mainView.menu.account=Счёт mainView.marketPriceWithProvider.label=Рыночный курс {0} mainView.marketPrice.havenoInternalPrice=Курс последней сделки в Haveno mainView.marketPrice.tooltip.havenoInternalPrice=Нет данных от источника рыночного курса.\nПредоставлен курс последней сделки в Haveno для этой валютной пары. mainView.marketPrice.tooltip=Рыночный курс предоставлен {0}{1}\nОбновление: {2}\nURL источника данных: {3} mainView.balance.available=Доступный баланс mainView.balance.reserved=Выделено на предложения mainView.balance.pending=Используется в сделках mainView.balance.reserved.short=Выделено mainView.balance.pending.short=В сделках mainView.footer.usingTor=(via Tor) mainView.footer.localhostMoneroNode=(локальный узел) mainView.footer.clearnet=(via clearnet) mainView.footer.xmrInfo={0} {1} mainView.footer.xmrFeeRate=/ Fee rate: {0} sat/vB mainView.footer.xmrInfo.initializing=Подключение к сети Haveno mainView.footer.xmrInfo.synchronizingWith=Синхронизация с {0} на блоке: {1} / {2} mainView.footer.xmrInfo.connectedTo=Подключено к {0} на блоке {1} mainView.footer.xmrInfo.synchronizingWalletWith=Синхронизация кошелька с {0} на блоке: {1} / {2} mainView.footer.xmrInfo.syncedWith=Синхронизировано с {0} на блоке {1} mainView.footer.xmrInfo.connectingTo=Подключение к mainView.footer.xmrInfo.connectionFailed=Connection failed to mainView.footer.xmrPeers=Monero network peers: {0} mainView.footer.p2pPeers=Haveno network peers: {0} mainView.bootstrapState.connectionToTorNetwork=(1/4) Подключение к сети Tor... mainView.bootstrapState.torNodeCreated=(2/4) Создан узел Tor mainView.bootstrapState.hiddenServicePublished=(3/4) Скрытый сервис опубликован mainView.bootstrapState.initialDataReceived=(4/4) Исходные данные получены mainView.bootstrapWarning.noSeedNodesAvailable=Нет доступных исходных узлов mainView.bootstrapWarning.noNodesAvailable=Нет доступных исходных узлов и пиров mainView.bootstrapWarning.bootstrappingToP2PFailed=Bootstrapping to Haveno network failed mainView.p2pNetworkWarnMsg.noNodesAvailable=Отсутствуют исходные узлы или постоянные пиры для запроса данных.\nПроверьте подключение к интернету или перезапустите приложение. mainView.p2pNetworkWarnMsg.connectionToP2PFailed=Connecting to the Haveno network failed (reported error: {0}).\nPlease check your internet connection or try to restart the application. mainView.walletServiceErrorMsg.timeout=Подключение к сети Биткойн не удалось из-за истечения времени ожидания. mainView.walletServiceErrorMsg.connectionError=Не удалось подключиться к сети Биткойн из-за ошибки: {0} mainView.walletServiceErrorMsg.rejectedTxException=A transaction was rejected from the network.\n\n{0} mainView.networkWarning.allConnectionsLost=Сбой соединения со всеми {0} узлами сети.\nВозможно, вы отключились от интернета, или Ваш компьютер перешел в режим ожидания. mainView.networkWarning.localhostMoneroLost=Сбой соединения с локальным узлом Биткойн.\nПерезапустите приложение для подключения к другим узлам Биткойн или перезапустите свой локальный узел Биткойн. mainView.version.update=(Имеется обновление) #################################################################### # MarketView #################################################################### market.tabs.offerBook=Предложения market.tabs.spreadCurrency=Offers by Currency market.tabs.spreadPayment=Offers by Payment Method market.tabs.trades=Сделки # OfferBookChartView market.offerBook.buyCrypto=Хочу купить {0} (продать {1}) market.offerBook.sellCrypto=Хочу продать {0} (купить {1}) market.offerBook.buyWithTraditional=Купить {0} market.offerBook.sellWithTraditional=Продать {0} market.offerBook.sellOffersHeaderLabel=Продать {0} market.offerBook.buyOffersHeaderLabel=Купить {0} market.offerBook.buy=Хочу купить биткойн market.offerBook.sell=Хочу продать биткойн # SpreadView market.spread.numberOfOffersColumn=Все предложения ({0}) market.spread.numberOfBuyOffersColumn=Купить XMR ({0}) market.spread.numberOfSellOffersColumn=Продать XMR ({0}) market.spread.totalAmountColumn=Итого XMR ({0}) market.spread.spreadColumn=Спред market.spread.expanded=Expanded view # TradesChartsView market.trades.nrOfTrades=Сделки: {0} market.trades.tooltip.volumeBar=Volume: {0} / {1}\nNo. of trades: {2}\nDate: {3} market.trades.tooltip.candle.open=Открыт: market.trades.tooltip.candle.close=Закрыт: market.trades.tooltip.candle.high=Высший: market.trades.tooltip.candle.low=Низший: market.trades.tooltip.candle.average=Средний: market.trades.tooltip.candle.median=Median: market.trades.tooltip.candle.date=Дата: market.trades.showVolumeInUSD=Show volume in USD #################################################################### # OfferView #################################################################### offerbook.createOffer=Создать предложение offerbook.takeOffer=Принять предложение offerbook.takeOfferToBuy=Принять предложение купить {0} offerbook.takeOfferToSell=Принять предложение продать {0} offerbook.takeOffer.enterChallenge=Введите фразу-пароль предложения offerbook.trader=Трейдер offerbook.offerersBankId=Идент. банка (BIC/SWIFT) мейкера: {0} offerbook.offerersBankName=Название банка мейкера: {0} offerbook.offerersBankSeat=Местоположение банка мейкера: {0} offerbook.offerersAcceptedBankSeatsEuro=Допустимые страны банка тейкера: все страны еврозоны offerbook.offerersAcceptedBankSeats=Допустимые страны банка тейкера:\n {0} offerbook.availableOffers=Доступные предложения offerbook.filterByCurrency=Фильтровать по валюте offerbook.filterByPaymentMethod=Фильтровать по способу оплаты offerbook.matchingOffers=Предложения, соответствующие моим аккаунтам offerbook.filterNoDeposit=Нет депозита offerbook.noDepositOffers=Предложения без депозита (требуется пароль) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info=This account was verified and {0} offerbook.timeSinceSigning.info.arbitrator=signed by an arbitrator and can sign peer accounts offerbook.timeSinceSigning.info.peer=signed by a peer, waiting %d days for limits to be lifted offerbook.timeSinceSigning.info.peerLimitLifted=signed by a peer and limits were lifted offerbook.timeSinceSigning.info.signer=signed by peer and can sign peer accounts (limits lifted) offerbook.timeSinceSigning.info.banned=account was banned offerbook.timeSinceSigning.daysSinceSigning={0} дн. offerbook.timeSinceSigning.daysSinceSigning.long={0} since signing offerbook.xmrAutoConf=Is auto-confirm enabled offerbook.buyXmrWith=Купить XMR с помощью: offerbook.sellXmrFor=Продать XMR за: offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers' payment accounts. offerbook.timeSinceSigning.notSigned=Not signed yet offerbook.timeSinceSigning.notSigned.ageDays={0} дн. offerbook.timeSinceSigning.notSigned.noNeed=Н/Д shared.notSigned=This account has not been signed yet and was created {0} days ago shared.notSigned.noNeed=This account type does not require signing shared.notSigned.noNeedDays=This account type does not require signing and was created {0} days ago shared.notSigned.noNeedAlts=Crypto accounts do not feature signing or aging offerbook.nrOffers=Кол-во предложений: {0} offerbook.volume={0} (мин. ⁠— макс.) offerbook.deposit=Deposit XMR (%) offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed. offerbook.createNewOffer=Создать предложение для {0} {1} offerbook.createOfferToBuy=Создать новое предложение на покупку {0} offerbook.createOfferToSell=Создать новое предложение на продажу {0} offerbook.createOfferToBuy.withTraditional=Создать новое предложение: купить {0} за {1} offerbook.createOfferToSell.forTraditional=Создать новое предложение: продать {0} за {1} offerbook.createOfferToBuy.withCrypto=Создать новое предложение: продать {0} (купить {1}) offerbook.createOfferToSell.forCrypto=Создать новое предложение: купить {0} (продать {1}) offerbook.takeOfferButton.tooltip=Принять предложение {0} offerbook.yesCreateOffer=Да, создать предложение offerbook.setupNewAccount=Создать новый торговый счёт offerbook.removeOffer.success=Предложение удалено. offerbook.removeOffer.failed=Не удалось удалить предложение:\n{0} offerbook.deactivateOffer.failed=Не удалось деактивировать предложение:\n{0} offerbook.activateOffer.failed=Не удалось опубликовать предложение:\n{0} offerbook.withdrawFundsHint=Вы можете вывести внесённые средства в разделе «{0}». offerbook.warning.noTradingAccountForCurrency.headline=No payment account for selected currency offerbook.warning.noTradingAccountForCurrency.msg=You don't have a payment account set up for the selected currency.\n\nWould you like to create an offer for another currency instead? offerbook.warning.noMatchingAccount.headline=No matching payment account. offerbook.warning.noMatchingAccount.msg=This offer uses a payment method you haven't set up yet. \n\nWould you like to set up a new payment account now? offerbook.warning.counterpartyTradeRestrictions=This offer cannot be taken due to counterparty trade restrictions offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\nAfter successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\nFor more information on account signing, please see the documentation at [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- The buyer's account has not been signed by an arbitrator or a peer\n- The time since signing of the buyer's account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- Your account has not been signed by an arbitrator or a peer\n- The time since signing of your account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=Этот способ оплаты временно ограничен до {0} до {1}, поскольку все покупатели имеют новые аккаунты.\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=Ваше предложение будет ограничено для покупателей с подписанными и старыми аккаунтами, потому что оно превышает {0}.\n\n{1} offerbook.warning.wrongTradeProtocol=Это предложение требует другой версии протокола, чем та, что используется в вашей версии приложения.\n\nПроверьте, установлена ли у вас новейшая версия приложения. Если да, то пользователь, создавший предложение, использовал старую версию.\n\nПри использовании несовместимой версии торгового протокола торговля невозможна. offerbook.warning.userIgnored=Onion-адрес данного пользователя добавлен в чёрный список. offerbook.warning.offerBlocked=Это предложение заблокировано разработчиками Haveno.\nВероятно, принятие этого предложения вызывает необрабатываемую ошибку. offerbook.warning.currencyBanned=Валюта, используемая в этом предложении, заблокирована разработчиками Haveno.\nПодробности можно узнать на форуме Haveno. offerbook.warning.paymentMethodBanned=Метод платежа, использованный в этом предложении, заблокирован разработчиками Haveno.\nПодробности можно узнать на форуме Haveno. offerbook.warning.nodeBlocked=Onion-адрес этого трейдера заблокирован разработчиками Haveno.\nВероятно, принятие предложения от данного трейдера вызывает необрабатываемую ошибку. offerbook.warning.offerWasAlreadyUsedInTrade=You cannot take this offer because you already took it earlier. It could be that your previous take-offer attempt resulted in a failed trade. offerbook.info.sellAtMarketPrice=Продажа по рыночному курсу (обновляется ежеминутно). offerbook.info.buyAtMarketPrice=Покупка по рыночному курсу (обновляется ежеминутно). offerbook.info.sellBelowMarketPrice=Вы получите на {0} меньше текущего рыночного курса (обновляется ежеминутно). offerbook.info.buyAboveMarketPrice=Вы заплатите на {0} больше текущего рыночного курса (обновляется ежеминутно). offerbook.info.sellAboveMarketPrice=Вы получите на {0} больше текущего рыночного курса (обновляется ежеминутно). offerbook.info.buyBelowMarketPrice=Вы заплатите на {0} меньше текущего рыночного курса (обновляется ежеминутно). offerbook.info.buyAtFixedPrice=Вы купите по этому фиксированному курсу. offerbook.info.sellAtFixedPrice=Вы продадите по этому фиксированному курсу. offerbook.info.noArbitrationInUserLanguage=В случае возникновения спора он будет рассматриваться арбитром на другом языке ({0}). Текущий язык: {1}. offerbook.info.roundedFiatVolume=Сумма округлена, чтобы повысить конфиденциальность сделки. #################################################################### # Offerbook / Create offer #################################################################### createOffer.amount.prompt=Введите сумму в ВТС createOffer.price.prompt=Введите курс createOffer.volume.prompt=Введите сумму в {0} createOffer.amountPriceBox.amountDescription=Количество XMR для {0} createOffer.amountPriceBox.buy.volumeDescription=Сумма затрат в {0} createOffer.amountPriceBox.sell.volumeDescription=Сумма в {0} к получению createOffer.amountPriceBox.minAmountDescription=Мин. количество ВТС createOffer.securityDeposit.prompt=Залог createOffer.fundsBox.title=Обеспечить своё предложение createOffer.fundsBox.offerFee=Комиссия за сделку createOffer.fundsBox.networkFee=Комиссия майнера createOffer.fundsBox.placeOfferSpinnerInfo=Публикация предложения... createOffer.fundsBox.paymentLabel=Сделка Haveno с идентификатором {0} createOffer.fundsBox.fundsStructure=({0} — залог, {1} — комиссия за сделку, {2} — комиссия майнера) createOffer.success.headline=Ваше предложение создано createOffer.success.info=Вы можете управлять текущими предложениями в разделе \«Сделки/Мои текущие предложения\». createOffer.info.sellAtMarketPrice=Вы всегда будете продавать по рыночному курсу, так как курс вашего предложения будет постоянно обновляться. createOffer.info.buyAtMarketPrice=Вы всегда будете покупать по рыночному курсу, так как курс вашего предложения будет постоянно обновляться. createOffer.info.sellAboveMarketPrice=Вы всегда получите на {0}% больше текущего рыночного курса, так как курс вашего предложения будет постоянно обновляться. createOffer.info.buyBelowMarketPrice=Вы всегда заплатите на {0}% меньше текущего рыночного курса, так как курс вашего предложения будет постоянно обновляться. createOffer.warning.sellBelowMarketPrice=Вы всегда получите на {0}% меньше текущего рыночного курса, так как курс вашего предложения будет постоянно обновляться. createOffer.warning.buyAboveMarketPrice=Вы всегда заплатите на {0}% больше текущего рыночного курса, так как курс вашего предложения будет постоянно обновляться. createOffer.tradeFee.descriptionXMROnly=Комиссия за сделку createOffer.tradeFee.descriptionBSQEnabled=Выбрать валюту комиссии за сделку createOffer.triggerPrice.prompt=Set optional trigger price createOffer.triggerPrice.label=Deactivate offer if market price is {0} createOffer.triggerPrice.tooltip=As protection against drastic price movements you can set a trigger price which deactivates the offer if the market price reaches that value. createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0} createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0} # new entries createOffer.placeOfferButton.buy=Проверка: Создать предложение на покупку XMR за {0} createOffer.placeOfferButton.sell=Проверка: Создать предложение на продажу XMR за {0} createOffer.createOfferFundWalletInfo.headline=Обеспечить своё предложение # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Сумма сделки: {0} \n createOffer.createOfferFundWalletInfo.msg=Вам нужно внести депозит {0} для этого предложения.\n\n\ Эти средства резервируются в вашем локальном кошельке и будут заблокированы в мультиподписном кошельке, как только кто-то примет ваше предложение.\n\n\ Сумма состоит из:\n\ {1}\ - Ваш залог: {2}\n\ - Торговая комиссия: {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=Ошибка при создании предложения:\n\n{0}\n\nВаши средства остались в кошельке.\nПерезагрузите приложение и проверьте сетевое соединение. createOffer.setAmountPrice=Указать сумму и курс createOffer.warnCancelOffer=Вы уже профинансировали данное предложение.\nПри отмене сейчас ваши средства останутся на вашем местном кошельке Haveno и будут доступны для вывода в разделе "Средства/Отправить средства".\nВы уверены, что хотите отменить? createOffer.timeoutAtPublishing=Время для публикации предложения истекло. createOffer.errorInfo=\n\nКомиссия мейкера уже оплачена. В худшем случае вы её потеряете.\nПерезагрузите приложение и проверьте сетевое соединение, чтобы попытаться устранить проблему. createOffer.tooLowSecDeposit.warning=Установленная сумма залога ниже рекомендуемой по умолчанию ({0}).\nИспользовать более низкую сумму залога? createOffer.tooLowSecDeposit.makerIsSeller=Вы будете меньше защищены, если ваш контрагент нарушит торговый протокол. createOffer.tooLowSecDeposit.makerIsBuyer=Ваш контрагент будет меньше защищён, поскольку вы рискуете меньшей суммой в случае нарушения торгового протокола. Пользователи могут предпочесть другие предложения вашему. createOffer.resetToDefault=Сбросить до значения по умолчанию createOffer.useLowerValue=Использовать выбранную сумму createOffer.priceOutSideOfDeviation=Введенный курс превышает максимально допустимое отклонение от рыночного курса.\nДопустимое отклонение составляет {0}. Его размер можно установить в настройках. createOffer.changePrice=Изменить курс createOffer.tac=Публикуя данное предложение, я выражаю согласие торговать с любым трейдером, соответствующим условиям, указанным на экране. createOffer.currencyForFee=Комиссия за сделку createOffer.setDeposit=Установить сумму залога покупателя (%) createOffer.setDepositAsBuyer=Установить мой залог как покупателя (%) createOffer.setDepositForBothTraders=Set both traders' security deposit (%) createOffer.securityDepositInfo=Сумма залога покупателя: {0} createOffer.securityDepositInfoAsBuyer=Сумма вашего залога: {0} createOffer.minSecurityDepositUsed=Минимальный залог используется createOffer.buyerAsTakerWithoutDeposit=Залог от покупателя не требуется (защищено паролем) createOffer.myDeposit=Мой залог (%) createOffer.myDepositInfo=Ваш залог составит {0} #################################################################### # Offerbook / Take offer #################################################################### takeOffer.amount.prompt=Введите сумму в ВТС takeOffer.amountPriceBox.buy.amountDescription=Сумма XMR для продажи takeOffer.amountPriceBox.sell.amountDescription=Сумма XMR для покупки takeOffer.amountPriceBox.priceDescription=Цена за биткойн в {0} takeOffer.amountPriceBox.amountRangeDescription=Возможный диапазон суммы takeOffer.amountPriceBox.warning.invalidXmrDecimalPlaces=Слишком много знаков после запятой.\nКоличество знаков скорректировано до 4. takeOffer.validation.amountSmallerThanMinAmount=Сумма не может быть меньше минимальной суммы, указанной в предложении. takeOffer.validation.amountLargerThanOfferAmount=Введённая сумма не может превышать сумму, указанную в предложении. takeOffer.validation.amountLargerThanOfferAmountMinusFee=Указанная сумма придет к появлению «пыли» у продавца XMR. takeOffer.fundsBox.title=Обеспечьте свою сделку takeOffer.fundsBox.isOfferAvailable=Проверка доступности предложения... takeOffer.fundsBox.tradeAmount=Сумма для продажи takeOffer.fundsBox.offerFee=Комиссия за сделку takeOffer.fundsBox.networkFee=Oбщая комиссия майнера takeOffer.fundsBox.takeOfferSpinnerInfo=Принятие предложения: {0} takeOffer.fundsBox.paymentLabel=Сделка в Haveno с идентификатором {0} takeOffer.fundsBox.fundsStructure=({0} — залог, {1} — комиссия за сделку, {2} — комиссия майнера) takeOffer.fundsBox.noFundingRequiredTitle=Не требуется финансирование takeOffer.fundsBox.noFundingRequiredDescription=Получите пароль предложения от продавца вне Haveno, чтобы принять это предложение. takeOffer.success.headline=Вы успешно приняли предложение. takeOffer.success.info=Статус вашей сделки отображается в разделе \«Папка/Текущие сделки\». takeOffer.error.message=Ошибка при принятии предложения:\n\n{0} # new entries takeOffer.takeOfferButton.buy=Проверка: Принять предложение на покупку XMR за {0} takeOffer.takeOfferButton.sell=Проверка: Принять предложение на продажу XMR за {0} takeOffer.noPriceFeedAvailable=Нельзя принять это предложение, поскольку в нем используется процентный курс на основе рыночного курса, источник которого недоступен. takeOffer.takeOfferFundWalletInfo.headline=Обеспечьте свою сделку # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Сумма сделки: {0} \n takeOffer.takeOfferFundWalletInfo.msg=Вам нужно внести депозит в размере {0} для принятия этого предложения.\n\nСумма составляет:\n{1}- Ваш залог: {2}\n- Торговая комиссия: {3} takeOffer.alreadyPaidInFunds=Если вы уже внесли средства, их можно вывести в разделе \«Средства/Отправить средства\». takeOffer.paymentInfo=Информация о платеже takeOffer.setAmountPrice=Задайте сумму takeOffer.alreadyFunded.askCancel=Вы уже профинансировали данное предложение.\nПри отмене сейчас ваши средства останутся на вашем местном кошельке Haveno и будут доступны для вывода в разделе "Средства/Отправить средства".\nВы уверены, что хотите отменить? takeOffer.failed.offerNotAvailable=Запрос принять предложение отменён, так как предложение больше недоступно. Возможно, его уже принял другой трейдер. takeOffer.failed.offerTaken=Невозможно принять это предложение, так как его уже принял другой трейдер. takeOffer.failed.offerRemoved=Невозможно принять это предложение, так как оно уже удалено. takeOffer.failed.offererNotOnline=Не удалось принять предложение, так как его создатель уже не в сети. takeOffer.failed.offererOffline=Невозможно принять это предложение, так как его создатель не в сети. takeOffer.warning.connectionToPeerLost=You lost connection to the maker.\nThey might have gone offline or has closed the connection to you because of too many open connections.\n\nIf you can still see their offer in the offerbook you can try to take the offer again. takeOffer.error.noFundsLost=\n\nСредства ещё не сняты с вашего кошелька.\nПерезапустите приложение и проверьте сетевое соединение, чтобы попытаться решить эту проблему. # suppress inspection "TrailingSpacesInProperty" takeOffer.error.feePaid=\n\n takeOffer.error.depositPublished=\n\nВнесение депозита завершено.\nПерезапустите приложение и проверьте сетевое соединение, чтобы попытаться решить эту проблему.\nЕсли это не поможет, свяжитесь с разработчиками. takeOffer.error.payoutPublished=\n\nВыплата завершена.\nПерезапустите приложение и проверьте сетевое соединение, чтобы попытаться решить эту проблему.\nЕсли это не поможет, свяжитесь с разработчиками. takeOffer.tac=Принимая это предложение, я соглашаюсь с условиями сделки, указанными на экране. #################################################################### # Offerbook / Edit offer #################################################################### openOffer.header.triggerPrice=Начальная цена openOffer.triggerPrice=Trigger price {0} openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\nPlease edit the offer to define a new trigger price editOffer.setPrice=Укажите курс editOffer.confirmEdit=Подтвердите: изменить предложение editOffer.publishOffer=Публикация вашего предложения. editOffer.failed=Не удалось изменить предложение:\n{0} editOffer.success=Ваше предложение успешно отредактировано. editOffer.invalidDeposit=Сумма залога покупателя не регулируется ДАО Haveno и больше не подлежит изменению. #################################################################### # Portfolio #################################################################### portfolio.tab.openOffers=Мои текущие предложения portfolio.tab.pendingTrades=Текущие сделки portfolio.tab.history=История portfolio.tab.failed=Не удалось portfolio.tab.editOpenOffer=Изменить предложение portfolio.closedTrades.deviation.help=Percentage price deviation from market portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} portfolio.pending.syncing=Синхронизация торгового кошелька portfolio.pending.syncing.blockRemaining=Синхронизация торгового кошелька — остался 1 блок portfolio.pending.syncing.blocksRemaining=Синхронизация торгового кошелька — осталось {0} блоков portfolio.pending.step1.waitForConf=Ожидание подтверждения в блокчейне portfolio.pending.step2_buyer.additionalConf=Депозиты достигли 10 подтверждений.\nДля дополнительной безопасности мы рекомендуем дождаться {0} подтверждений перед отправкой платежа.\nРанее действия осуществляются на ваш страх и риск. portfolio.pending.step2_buyer.startPayment=Сделать платеж portfolio.pending.step2_seller.waitPaymentSent=Дождитесь начала платежа portfolio.pending.step3_buyer.waitPaymentArrived=Дождитесь получения платежа portfolio.pending.step3_seller.confirmPaymentReceived=Подтвердите получение платежа portfolio.pending.step5.completed=Завершено portfolio.pending.step3_seller.autoConf.status.label=Auto-confirm status portfolio.pending.autoConf=Auto-confirmed portfolio.pending.autoConf.blocks=XMR confirmations: {0} / Required: {1} portfolio.pending.autoConf.state.xmr.txKeyReused=Transaction key re-used. Please open a dispute. portfolio.pending.autoConf.state.confirmations=XMR confirmations: {0}/{1} portfolio.pending.autoConf.state.txNotFound=Transaction not seen in mem-pool yet portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=No valid transaction ID / transaction key portfolio.pending.autoConf.state.filterDisabledFeature=Disabled by developers. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FEATURE_DISABLED=Auto-confirm feature is disabled. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=Trade amount exceeds auto-confirm amount limit # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.INVALID_DATA=Peer provided invalid data. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=Payout transaction was already published. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.DISPUTE_OPENED=Dispute was opened. Auto-confirm is deactivated for that trade. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.REQUESTS_STARTED=Transaction proof requests started # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PENDING=Success results: {0}/{1}; {2} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.COMPLETED=Proof at all services succeeded # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. No auto-confirm possible. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. portfolio.pending.step1.info.you=Транзакция депозита опубликована.\nВы сможете начать оплату после 10 подтверждений (примерно через {0} минут). portfolio.pending.step1.info.buyer=Транзакция депозита опубликована.\nПокупатель XMR сможет начать оплату после 10 подтверждений (примерно через {0} минут). portfolio.pending.step1.warn=The deposit transaction is still not confirmed. This sometimes happens in rare cases when the funding fee of one trader from an external wallet was too low. portfolio.pending.step1.openForDispute=The deposit transaction is still not confirmed. You can wait longer or contact the mediator for assistance. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2.confReached=Your trade has reached at least one blockchain confirmation.\n\n portfolio.pending.step2_buyer.refTextWarn=Important: when making the payment, leave the \"reason for payment\" field empty. DO NOT put the trade ID or any other text like 'monero', 'XMR', or 'Haveno'. You are free to discuss via trader chat if an alternate \"reason for payment\" would be suitable to you both. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees=If your bank charges you any fees to make the transfer, you are responsible for paying those fees. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.crypto=Переведите {1} с внешнего кошелька {0}\nпродавцу ВТС.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.cash=Обратитесь в банк и заплатите {0} продавцу ВТС.\n\n portfolio.pending.step2_buyer.cash.extra=ВАЖНОЕ ТРЕБОВАНИЕ:\nПосле оплаты напишите на бумажной квитанции «ВОЗВРАТУ НЕ ПОДЛЕЖИТ».\nЗатем разорвите квитанцию на 2 части, сфотографируйте её и отошлите на электронный адрес продавца ВТС. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.moneyGram=Заплатите {0} продавцу XMR через MoneyGram.\n\n portfolio.pending.step2_buyer.moneyGram.extra=ВАЖНОЕ ТРЕБОВАНИЕ:\nПосле оплаты отправьте продавцу XMR по электронной почте код подтверждения и фото квитанции.\nВ квитанции должно быть четко указано полное имя продавца, страна (штат) и сумма. Электронный адрес продавца: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.westernUnion=Заплатите {0} продавцу XMR через Western Union.\n\n portfolio.pending.step2_buyer.westernUnion.extra=ВАЖНОЕ ТРЕБОВАНИЕ: \nПосле оплаты отправьте по электронной почте продавцу XMR контрольный номер MTCN и фото квитанции.\nВ квитанции должно быть четко указано полное имя продавца, город, страна и сумма. Электронный адрес продавца: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Отправьте {0} \«Почтовым денежным переводом США\» продавцу XMR.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Свяжитесь с продавцом XMR с помощью указанных контактных данных и договоритесь о встрече для оплаты {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Начать оплату, используя {0} portfolio.pending.step2_buyer.recipientsAccountData=Recipients {0} portfolio.pending.step2_buyer.amountToTransfer=Сумма для перевода portfolio.pending.step2_buyer.sellersAddress={0}-адрес продавца portfolio.pending.step2_buyer.buyerAccount=Используемый платёжный счет portfolio.pending.step2_buyer.paymentSent=Платёж начат portfolio.pending.step2_buyer.showEarly=Показать платёжные реквизиты заранее portfolio.pending.step2_buyer.warn=You still have not done your {0} payment!\nPlease note that the trade has to be completed by {1}. portfolio.pending.step2_buyer.openForDispute=You have not completed your payment!\nThe max. period for the trade has elapsed.Please contact the mediator for assistance. portfolio.pending.step2_buyer.paperReceipt.headline=Вы отослали бумажную квитанцию продавцу ВТС? portfolio.pending.step2_buyer.paperReceipt.msg=Помните:\nВам необходимо написать на бумажной квитанции «ВОЗВРАТУ НЕ ПОДЛЕЖИТ».\nЗатем разорвите её пополам, сфотографируйте и отошлите по электронной почте продавцу ВТС. portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Отправить код подтверждения и квитанцию portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=Вам необходимо отправить по электронной почте продавцу XMR код подтверждения и фото квитанции.\nВ квитанции должно быть четко указано полное имя продавца, страна (штат) и сумма. Электронный адрес продавца: {0}.\n\nВы отправили продавцу код подтверждения и квитанцию? portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=Отправить MTCN и квитанцию portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Вам необходимо отправить по электронной почте продавцу XMR контрольный номер MTCN и фотографию квитанции.\nВ квитанции должно быть четко указано полное имя продавца, город, страна и сумма. Адрес электронной почты продавца: {0}. \n\nВы отправили MTCN и контракт продавцу? portfolio.pending.step2_buyer.halCashInfo.headline=Отправить код HalCash portfolio.pending.step2_buyer.halCashInfo.msg=Вам необходимо отправить сообщение с кодом HalCash и идентификатором сделки ({0}) продавцу XMR.\nНомер моб. тел. продавца: {1}\n\nВы отправили код продавцу? portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Some banks might verify the receiver's name. Faster Payments accounts created in old Haveno clients do not provide the receiver's name, so please use trade chat to obtain it (if needed). portfolio.pending.step2_buyer.confirmStart.headline=Подтвердите начало платежа portfolio.pending.step2_buyer.confirmStart.msg=Вы начали платеж {0} своему контрагенту? portfolio.pending.step2_buyer.confirmStart.yes=Да portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=You have not provided proof of payment portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=You have not entered the transaction ID and the transaction key.\n\nBy not providing this data the peer cannot use the auto-confirm feature to release the XMR as soon the XMR has been received.\nBeside that, Haveno requires that the sender of the XMR transaction is able to provide this information to the mediator or arbitrator in case of a dispute.\nSee more details on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=Input is not a 32 byte hexadecimal value portfolio.pending.step2_buyer.confirmStart.warningButton=Ignore and continue anyway portfolio.pending.step2_seller.waitPayment.headline=Ожидайте платеж portfolio.pending.step2_seller.f2fInfo.headline=Контактная информация покупателя portfolio.pending.step2_seller.waitPayment.msg=Депозитная транзакция подтверждена в блокчейне не менее одного раза.\nДождитесь начала платежа в {0} покупателем XMR. portfolio.pending.step2_seller.warn=Покупатель XMR все еще не завершил платеж в {0}.\nДождитесь начала оплаты.\nЕсли сделка не завершится {1}, арбитр начнет разбирательство. portfolio.pending.step2_seller.openForDispute=The XMR buyer has not started their payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the mediator for assistance. tradeChat.chatWindowTitle=Chat window for trade with ID '{0}' tradeChat.openChat=Open chat window tradeChat.rules=You can communicate with your trade peer to resolve potential problems with this trade.\nIt is not mandatory to reply in the chat.\nIf a trader violates any of the rules below, open a dispute and report it to the mediator or arbitrator.\n\nChat rules:\n\t● Do not send any links (risk of malware). You can send the transaction ID and the name of a block explorer.\n\t● Do not send your seed words, private keys, passwords or other sensitive information!\n\t● Do not encourage trading outside of Haveno (no security).\n\t● Do not engage in any form of social engineering scam attempts.\n\t● If a peer is not responding and prefers to not communicate via chat, respect their decision.\n\t● Keep conversation scope limited to the trade. This chat is not a messenger replacement or troll-box.\n\t● Keep conversation friendly and respectful. # suppress inspection "UnusedProperty" message.state.UNDEFINED=Неопределено # suppress inspection "UnusedProperty" message.state.SENT=Сообщение отправлено # suppress inspection "UnusedProperty" message.state.ARRIVED=Сообщение прибыло к контрагенту # suppress inspection "UnusedProperty" message.state.STORED_IN_MAILBOX=Message of payment sent but not yet received by peer # suppress inspection "UnusedProperty" message.state.ACKNOWLEDGED=Контрагент подтвердил получение сообщения # suppress inspection "UnusedProperty" message.state.FAILED=Сбой отправки сообщения portfolio.pending.step3_buyer.wait.headline=Ожидание подтверждения оплаты продавцом ВТС portfolio.pending.step3_buyer.wait.info=Ожидание подтверждения продавцом ВТС получения оплаты в {0}. portfolio.pending.step3_buyer.wait.msgStateInfo.label=Статус сообщения о начале платежа portfolio.pending.step3_buyer.warn.part1a=в блокчейне {0} portfolio.pending.step3_buyer.warn.part1b=у вашего поставщика платёжных услуг (напр., банка) portfolio.pending.step3_buyer.warn.part2=The XMR seller still has not confirmed your payment. Please check {0} if the payment sending was successful. portfolio.pending.step3_buyer.openForDispute=The XMR seller has not confirmed your payment! The max. period for the trade has elapsed. You can wait longer and give the trading peer more time or request assistance from the mediator. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.part=Ваш контрагент подтвердил начало оплаты в {0}.\n\n portfolio.pending.step3_seller.crypto.explorer=в вашем любимом обозревателе блоков {0} portfolio.pending.step3_seller.crypto.wallet=в вашем кошельке {0} portfolio.pending.step3_seller.crypto={0}Проверьте {1}, была ли транзакция в ваш адрес\n{2}\nподтверждена достаточное количество раз.\nСумма платежа должна составлять {3}.\n\n Вы можете скопировать и вставить свой адрес {4} из главного окна после закрытия этого окна. portfolio.pending.step3_seller.postal={0}Please check if you have received {1} with \"US Postal Money Order\" from the XMR buyer. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.payByMail={0}Please check if you have received {1} with \"Pay by Mail\" from the XMR buyer. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.bank=Your trading partner has confirmed that they have initiated the {0} payment.\n\nPlease go to your online banking web page and check if you have received {1} from the XMR buyer. portfolio.pending.step3_seller.cash=Так как оплата осуществляется наличными на счёт, покупатель XMR должен написать \«НЕ ПОДЛЕЖИТ ВОЗВРАТУ\» на квитанции, разорвать её на 2 части и отправить вам её фото по электронной почте.\n\nЧтобы избежать возврата платёжа, подтверждайте его получение только после получения этого фото, если вы не сомневаетесь в подлинности квитанции.\nЕсли вы не уверены, {0} portfolio.pending.step3_seller.moneyGram=Покупатель обязан отправить вам по электронной почте код подтверждения и фото квитанции.\nВ квитанции должно быть четко указано ваше полное имя, страна (штат) и сумма. Убедитесь, что вы получили код подтверждения по электронной почте.\n\nПосле закрытия этого окна вы увидите имя и адрес покупателя XMR, которые необходимо указать для получения денег от MoneyGram.\n\nПодтвердите получение только после того, как вы успешно заберете деньги! portfolio.pending.step3_seller.westernUnion=Покупатель обязан отправить вам по электронной почте контрольный номер MTCN и фото квитанции.\nВ квитанции должно быть четко указано ваше полное имя, город, страна и сумма. Убедитесь, что вы получили номер MTCN по электронной почте.\n\nПосле закрытия этого окна вы увидите имя и адрес покупателя XMR, которые необходимо указать для получения денег от Western Union. \n\nПодтвердите получение только после того, как вы успешно заберете деньги! portfolio.pending.step3_seller.halCash=Покупатель должен отправить вам код HalCash в текстовом сообщении. Кроме того, вы получите сообщение от HalCash с информацией, необходимой для снятия EUR в банкомате, поддерживающем HalCash.\n\nПосле того, как вы заберете деньги из банкомата, подтвердите получение платежа в приложении! portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon eGift Card by email or by text message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted confirm the payment receipt. portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=Подтвердите получение платежа portfolio.pending.step3_seller.amountToReceive=Сумма поступления portfolio.pending.step3_seller.yourAddress=Ваш адрес {0} portfolio.pending.step3_seller.buyersAddress=Адрес {0} покупателя portfolio.pending.step3_seller.yourAccount=Ваш торговый счёт portfolio.pending.step3_seller.xmrTxHash=Идентификатор транзакции portfolio.pending.step3_seller.xmrTxKey=Transaction key portfolio.pending.step3_seller.buyersAccount=Buyers account data portfolio.pending.step3_seller.confirmReceipt=Подтвердить получение платежа portfolio.pending.step3_seller.buyerStartedPayment=Покупатель ВТС начал оплату в {0}.\n{1} portfolio.pending.step3_seller.buyerStartedPayment.crypto=Проверьте количество подтверждений в блокчейне в своём алтькойн-кошельке или обозревателе блоков и подтвердите платеж, если подтверждений достаточно. portfolio.pending.step3_seller.buyerStartedPayment.traditional=Проверьте получение на свой торговый счёт (напр. банковский счёт) и подтвердите после получения платежа. portfolio.pending.step3_seller.warn.part1a=в блокчейне {0} portfolio.pending.step3_seller.warn.part1b=у вашего поставщика платёжных услуг (напр., банка) portfolio.pending.step3_seller.warn.part2=You still have not confirmed the receipt of the payment. Please check {0} if you have received the payment. portfolio.pending.step3_seller.openForDispute=You have not confirmed the receipt of the payment!\nThe max. period for the trade has elapsed.\nPlease confirm or request assistance from the mediator. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=Вы получили платеж в {0} от своего контрагента?\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Please note, that as soon you have confirmed the receipt, the locked trade amount will be released to the XMR buyer and the security deposit will be refunded.\n\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Подтвердите получение платежа portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=Да, я получил (-а) платёж portfolio.pending.step3_seller.onPaymentReceived.signer=IMPORTANT: By confirming receipt of payment, you are also verifying the account of the counterparty and signing it accordingly. Since the account of the counterparty hasn't been signed yet, you should delay confirmation of the payment as long as possible to reduce the risk of a chargeback. portfolio.pending.step5_buyer.groupTitle=Детали завершённой сделки portfolio.pending.step5_buyer.tradeFee=Комиссия за сделку portfolio.pending.step5_buyer.makersMiningFee=Комиссия майнера portfolio.pending.step5_buyer.takersMiningFee=Oбщая комиссия майнера portfolio.pending.step5_buyer.refunded=Сумма возмещённого залога portfolio.pending.step5_buyer.withdrawXMR=Вывести биткойны portfolio.pending.step5_buyer.amount=Сумма для вывода portfolio.pending.step5_buyer.withdrawToAddress=Вывести на адрес portfolio.pending.step5_buyer.moveToHavenoWallet=Keep funds in Haveno wallet portfolio.pending.step5_buyer.withdrawExternal=Вывести на внешний кошелёк portfolio.pending.step5_buyer.alreadyWithdrawn=Ваши средства уже сняты.\nПросмотрите журнал транзакций. portfolio.pending.step5_buyer.confirmWithdrawal=Подтвердите запрос на вывод portfolio.pending.step5_buyer.amountTooLow=Сумма перевода ниже комиссии за транзакцию и минимально возможного значения («пыли»). portfolio.pending.step5_buyer.withdrawalCompleted.headline=Вывод выполнен portfolio.pending.step5_buyer.withdrawalCompleted.msg=Ваши завершенные сделки хранятся в разделе \«Сделки/История\».\nВсе ваши биткойн-транзакции указаны в разделе \«Средства/Транзакции\». portfolio.pending.step5_buyer.bought=Вы купили portfolio.pending.step5_buyer.paid=Вы заплатили portfolio.pending.step5_seller.sold=Вы продали portfolio.pending.step5_seller.received=Вы получили tradeFeedbackWindow.title=Поздравляем с завершением сделки! tradeFeedbackWindow.msg.part1=Мы были бы рады услышать ваши отзывы. Они помогут нам улучшить приложение и исправить любые ошибки. Если вы хотите оставить отзыв, заполните эту небольшую форму (регистрация не требуется): tradeFeedbackWindow.msg.part2=Если у вас возникли вопросы или сложности, свяжитесь с другими пользователями и разработчиками приложения на форуме Haveno: tradeFeedbackWindow.msg.part3=Спасибо, что пользуетесь Haveno! portfolio.pending.role=Моя роль portfolio.pending.tradeInformation=Информация о сделке portfolio.pending.remainingTime=Оставшееся время portfolio.pending.remainingTimeDetail={0} (до {1}) portfolio.pending.remainingTimeDetail.startsAfter=Начнётся после {0} подтверждений portfolio.pending.tradePeriodInfo=После {0} подтверждений начинается торговый период. В зависимости от используемого способа оплаты применяется разный максимально допустимый торговый период. portfolio.pending.tradePeriodWarning=При превышении срока оба трейдера могут начать спор. portfolio.pending.tradeNotCompleted=Сделка не завершена вовремя (до {0}) portfolio.pending.tradeProcess=Процесс сделки portfolio.pending.openAgainDispute.msg=If you are not sure that the message to the mediator or arbitrator arrived (e.g. if you did not get a response after 1 day) feel free to open a dispute again with Cmd/Ctrl+o. You can also ask for additional help on the Haveno forum at [HYPERLINK:https://haveno.community]. portfolio.pending.openAgainDispute.button=Начать спор заново portfolio.pending.openSupportTicket.headline=Обратиться за поддержкой portfolio.pending.openSupportTicket.msg=Please use this function only in emergency cases if you don't see a \"Open support\" or \"Open dispute\" button.\n\nWhen you open a support ticket the trade will be interrupted and handled by a mediator or arbitrator. portfolio.pending.timeLockNotOver=You have to wait until ≈{0} ({1} more blocks) before you can open an arbitration dispute. portfolio.pending.error.depositTxNull=The deposit transaction is null. You cannot open a dispute without a valid deposit transaction. Please go to \"Settings/Network info\" and do a SPV resync.\n\nFor further help please contact the Haveno support channel at the Haveno Keybase team. portfolio.pending.mediationResult.error.depositTxNull=The deposit transaction is null. You can move the trade to failed trades. portfolio.pending.mediationResult.error.delayedPayoutTxNull=The delayed payout transaction is null. You can move the trade to failed trades. portfolio.pending.error.depositTxNotConfirmed=The deposit transaction is not confirmed. You can not open an arbitration dispute with an unconfirmed deposit transaction. Please wait until it is confirmed or go to \"Settings/Network info\" and do a SPV resync.\n\nFor further help please contact the Haveno support channel at the Haveno Keybase team. portfolio.pending.support.headline.getHelp=Need help? portfolio.pending.support.text.getHelp=If you have any problems you can try to contact the trade peer in the trade chat or ask the Haveno community at https://haveno.community. If your issue still isn't resolved, you can request more help from a mediator. portfolio.pending.support.button.getHelp=Open Trader Chat portfolio.pending.support.headline.halfPeriodOver=Check payment portfolio.pending.support.headline.periodOver=Время сделки истекло portfolio.pending.support.headline.depositTxMissing=Отсутствует депозитная транзакция portfolio.pending.support.depositTxMissing=Для этой сделки отсутствует депозитная транзакция. Откройте тикет в службу поддержки, чтобы связаться с арбитром для получения помощи. portfolio.pending.mediationRequested=Mediation requested portfolio.pending.refundRequested=Refund requested portfolio.pending.openSupport=Обратиться за поддержкой portfolio.pending.supportTicketOpened=Запрос на поддержку отправлен portfolio.pending.communicateWithArbitrator=Свяжитесь с арбитром в разделе \«Поддержка\». portfolio.pending.communicateWithMediator=Please communicate in the \"Support\" screen with the mediator. portfolio.pending.disputeOpenedByUser=Вы уже начали спор.\n{0} portfolio.pending.disputeOpenedByPeer=Ваш контрагент начал спор\n{0} portfolio.pending.noReceiverAddressDefined=Адрес получателя не установлен portfolio.pending.mediationResult.headline=Suggested payout from mediation portfolio.pending.mediationResult.info.noneAccepted=Complete the trade by accepting the mediator's suggestion for the trade payout. portfolio.pending.mediationResult.info.selfAccepted=You have accepted the mediator's suggestion. Waiting for peer to accept as well. portfolio.pending.mediationResult.info.peerAccepted=Your trade peer has accepted the mediator's suggestion. Do you accept as well? portfolio.pending.mediationResult.button=View proposed resolution portfolio.pending.mediationResult.popup.headline=Mediation result for trade with ID: {0} portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator's suggestion for trade {0} portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader's security deposit) as compensation for their work. Both traders agreeing to the mediator's suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator's suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. portfolio.pending.failedTrade.missingDepositTx=Отсутствует транзакция депозита.\n\nЭта транзакция необходима для завершения сделки. Пожалуйста, убедитесь, что ваш кошелёк полностью синхронизирован с блокчейном Monero.\n\nВы можете переместить эту сделку в раздел "Неудачные сделки", чтобы деактивировать её. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Move trade to failed trades portfolio.pending.failedTrade.warningIcon.tooltip=Click to open details about the issues of this trade portfolio.failed.revertToPending.popup=Do you want to move this trade to open trades? portfolio.failed.revertToPending=Move trade to open trades portfolio.closed.completed=Завершена portfolio.closed.ticketClosed=Arbitrated portfolio.closed.mediationTicketClosed=Mediated portfolio.closed.canceled=Отменена portfolio.failed.Failed=Не удалась portfolio.failed.unfail=Before proceeding, make sure you have a backup of your data directory!\nDo you want to move this trade back to open trades?\nThis is a way to unlock funds stuck in a failed trade. portfolio.failed.cantUnfail=This trade cannot be moved back to open trades at the moment. \nTry again after completion of trade(s) {0} portfolio.failed.depositTxNull=The trade cannot be reverted to a open trade. Deposit transaction is null. portfolio.failed.delayedPayoutTxNull=The trade cannot be reverted to a open trade. Delayed payout transaction is null. #################################################################### # Funds #################################################################### funds.tab.deposit=Получить средства funds.tab.withdrawal=Отправить средства funds.tab.reserved=Выделенные средства funds.tab.locked=В сделках funds.tab.transactions=Транзакции funds.deposit.unused=Не использован funds.deposit.usedInTx=Использован в {0} транзакциях funds.deposit.fundHavenoWallet=Пополнить кошелёк Haveno funds.deposit.noAddresses=Адреса для перевода средств ещё не созданы funds.deposit.fundWallet=Пополнить кошелёк funds.deposit.withdrawFromWallet=Отправить средства из кошелька funds.deposit.amount=Сумма в ВТС (необязательно) funds.deposit.generateAddress=Создать новый адрес funds.deposit.generateAddressSegwit=Native segwit format (Bech32) funds.deposit.selectUnused=Выберите неиспользованный адрес из таблицы выше вместо создания нового. funds.withdrawal.arbitrationFee=Комиссия арбитра funds.withdrawal.inputs=Выбор адресов funds.withdrawal.useAllInputs=Использовать все доступные адреса funds.withdrawal.useCustomInputs=Использовать выбранные адреса funds.withdrawal.receiverAmount=Сумма для получения funds.withdrawal.senderAmount=Сумма для отправления funds.withdrawal.feeExcluded=Сумма не включает комиссию майнера funds.withdrawal.feeIncluded=Сумма включает комиссию майнера funds.withdrawal.fromLabel=Вывести с адреса funds.withdrawal.toLabel=Вывести на адрес funds.withdrawal.memoLabel=Withdrawal memo funds.withdrawal.memo=Optionally fill memo funds.withdrawal.withdrawButton=Снять указанную сумму funds.withdrawal.noFundsAvailable=Нет доступных для вывода средств funds.withdrawal.confirmWithdrawalRequest=Подтвердите запрос на вывод funds.withdrawal.withdrawMultipleAddresses=Снять с нескольких адресов ({0}) funds.withdrawal.withdrawMultipleAddresses.tooltip=Снять с нескольких адресов:\n{0} funds.withdrawal.notEnoughFunds=В вашем кошельке недостаточно средств. funds.withdrawal.selectAddress=Выберите адрес-источник из таблицы funds.withdrawal.setAmount=Укажите сумму для вывода funds.withdrawal.fillDestAddress=Введите адрес получателя funds.withdrawal.warn.noSourceAddressSelected=Необходимо выбрать адрес-источник в таблице выше. funds.withdrawal.warn.amountExceeds=Сумма превышает доступную на данном адресе.\nВыберите несколько адресов в таблице выше или включите в сумму комиссию майнера. funds.reserved.noFunds=Не выделено средств для текущих предложений funds.reserved.reserved=Выделено в локальном кошельке на предложение с идент.: {0} funds.locked.noFunds=Средства в сделках не используются funds.locked.locked=Заблокировано в multisig-адресе для сделки с идентификатором: {0} funds.tx.direction.sentTo=Отправлено: funds.tx.direction.receivedWith=Получено: funds.tx.direction.genesisTx=Из первичной транзакции: funds.tx.createOfferFee=Комиссия мейкера и плата за сделку: {0} funds.tx.takeOfferFee=Комиссия тейкера и плата за сделку: {0} funds.tx.multiSigDeposit=Депозит на multisig-адрес: {0} funds.tx.multiSigPayout=Выплата с multisig-адреса: {0} funds.tx.disputePayout=Выплата по спору: {0} funds.tx.disputeLost=Проигранный спор: {0} funds.tx.collateralForRefund=Refund collateral: {0} funds.tx.timeLockedPayoutTx=Time locked payout tx: {0} funds.tx.refund=Refund from arbitration: {0} funds.tx.unknown=Неизвестная причина: {0} funds.tx.noFundsFromDispute=Без возмещения по спору funds.tx.receivedFunds=Полученные средства funds.tx.withdrawnFromWallet=Выведено из кошелька funds.tx.memo=Memo funds.tx.noTxAvailable=Транзакции отсутствуют funds.tx.revert=Отменить funds.tx.txSent=Транзакция успешно отправлена на новый адрес локального кошелька Haveno. funds.tx.direction.self=Транзакция внутри кошелька funds.tx.dustAttackTx=Полученная «пыль» funds.tx.dustAttackTx.popup=Вы получили очень маленькую сумму XMR, что может являться попыткой компаний, занимающихся анализом блокчейна, проследить за вашим кошельком.\n\nЕсли вы воспользуетесь этими средствами для совершения исходящей транзакции, они смогут узнать, что вы также являетесь вероятным владельцем другого адреса (т. н. «объединение монет»).\n\nДля защиты вашей конфиденциальности кошелёк Haveno игнорирует такую «пыль» при совершении исходящих транзакций и отображении баланса. Вы можете самостоятельно установить сумму, которая будет рассматриваться в качестве «пыли» в настройках. #################################################################### # Support #################################################################### support.tab.mediation.support=Mediation support.tab.arbitration.support=Arbitration support.tab.legacyArbitration.support=Legacy Arbitration support.tab.ArbitratorsSupportTickets={0}'s tickets support.sigCheck.button=Check signature support.sigCheck.popup.header=Verify dispute result signature support.sigCheck.popup.msg.label=Summary message support.sigCheck.popup.msg.prompt=Copy & paste summary message from dispute support.sigCheck.popup.result=Validation result support.sigCheck.popup.success=Signature is valid support.sigCheck.popup.failed=Signature verification failed support.sigCheck.popup.invalidFormat=Message is not of expected format. Copy & paste summary message from dispute. support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? support.reOpenButton.label=Re-open support.sendNotificationButton.label=Личное уведомление support.reportButton.label=Report support.fullReportButton.label=All disputes support.noTickets=Нет текущих обращений support.sendingMessage=Отправка сообщения... support.receiverNotOnline=Receiver is not online. Message is saved to their mailbox. support.sendMessageError=Сбой отправки сообщения. Ошибка: {0} support.receiverNotKnown=Receiver not known support.wrongVersion=Предложение, по которому открыт этот спор, было создано в устаревшей версии Haveno.\nНевозможно закрыть спор с помощью текущей версии приложения.\n\nВоспользуйтесь старой версией протокола: {0} support.openFile=Открыть файл для отправки (макс. размер файла: {0} Кб) support.attachmentTooLarge=Общий объём вложенных файлов составляет {0} Кб, что превышает максимально допустимый размер сообщения ({1} Кб). support.maxSize=Максимально допустимый объём файла: {0} Кб. support.attachment=Вложенный файл support.tooManyAttachments=Нельзя отправлять более 3 файлов в одном сообщении. support.save=Сохранить файл на диск support.messages=Сообщения support.input.prompt=Enter message... support.send=Отправить support.addAttachments=Прикрепить файлы support.closeTicket=Закрыть запрос support.attachments=Вложенные файлы: support.savedInMailbox=Сообщение сохранено в почтовом ящике получателя support.arrived=Сообщение доставлено адресату support.acknowledged=Прибытие сообщения подтверждено получателем support.error=Получателю не удалось обработать сообщение. Ошибка: {0} support.buyerAddress=Адрес покупателя ВТС support.sellerAddress=Адрес продавца ВТС support.role=Роль support.agent=Support agent support.state=Состояние support.chat=Chat support.preparing=В процессе support.requested=Запрошено support.closed=Закрыто support.open=Открыто support.process=Process support.buyerMaker=Покупатель ВТС/мейкер support.sellerMaker=Продавец ВТС/мейкер support.buyerTaker=Покупатель ВТС/тейкер support.sellerTaker=Продавец XMR/тейкер support.initialInfo=Please enter a description of your problem in the text field below. Add as much information as possible to speed up dispute resolution time.\n\nHere is a check list for information you should provide:\n\t● If you are the XMR buyer: Did you make the Fiat or Crypto transfer? If so, did you click the 'payment started' button in the application?\n\t● If you are the XMR seller: Did you receive the Fiat or Crypto payment? If so, did you click the 'payment received' button in the application?\n\t● Which version of Haveno are you using?\n\t● Which operating system are you using?\n\t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\t Sometimes the data directory gets corrupted and leads to strange bugs. \n\t See: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nPlease make yourself familiar with the basic rules for the dispute process:\n\t● You need to respond to the {0}'s requests within 2 days.\n\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\t● The maximum period for a dispute is 14 days.\n\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\nYou can read more about the dispute process at: {2} support.systemMsg=Системное сообщение: {0} support.youOpenedTicket=Вы запросили поддержку.\n\n{0}\n\nВерсия Haveno: {1} support.youOpenedDispute=Вы начали спор.\n\n{0}\n\nВерсия Haveno: {1} support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nHaveno version: {1} support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nHaveno version: {1} support.mediatorsDisputeSummary=System message: Mediator's dispute summary:\n{0} support.mediatorsAddress=Mediator's node address: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. support.info.disputeReOpened=Dispute ticket has been re-opened. #################################################################### # Settings #################################################################### settings.tab.preferences=Параметры settings.tab.network=Информация о сети settings.tab.about=О проекте setting.preferences.general=Основные настройки setting.preferences.explorer=Monero Explorer setting.preferences.deviation=Макс. отклонение от рыночного курса setting.preferences.avoidStandbyMode=Избегать режима ожидания setting.preferences.useSoundForNotifications=Воспроизводить звуки для уведомлений setting.preferences.autoConfirmXMR=XMR auto-confirm setting.preferences.autoConfirmEnabled=Enabled setting.preferences.autoConfirmRequiredConfirmations=Required confirmations setting.preferences.autoConfirmMaxTradeSize=Max. trade amount (XMR) setting.preferences.autoConfirmServiceAddresses=Monero Explorer URLs (uses Tor, except for localhost, LAN IP addresses, and *.local hostnames) setting.preferences.deviationToLarge=Значения выше {0}% запрещены. setting.preferences.txFee=Withdrawal transaction fee (satoshis/vbyte) setting.preferences.useCustomValue=Задать своё значение setting.preferences.txFeeMin=Transaction fee must be at least {0} satoshis/vbyte setting.preferences.txFeeTooLarge=Your input is above any reasonable value (>5000 satoshis/vbyte). Transaction fee is usually in the range of 50-400 satoshis/vbyte. setting.preferences.ignorePeers=Игнорируемые пиры [onion-адрес:порт] setting.preferences.ignoreDustThreshold=Мин. значение, не являющееся «пылью» setting.preferences.currenciesInList=Валюты в перечне источника рыночного курса setting.preferences.prefCurrency=Предпочитаемая валюта setting.preferences.displayTraditional=Показать нац. валюты setting.preferences.noTraditional=Национальные валюты не выбраны setting.preferences.cannotRemovePrefCurrency=Нельзя удалить выбранную предпочитаемую валюту setting.preferences.displayCryptos=Показать альткойны setting.preferences.noCryptos=Альткойны не выбраны setting.preferences.addTraditional=Добавить национальную валюту setting.preferences.addCrypto=Добавить альткойн setting.preferences.displayOptions=Параметры отображения setting.preferences.showOwnOffers=Показать мои предложения в списке предложений setting.preferences.useAnimations=Использовать анимацию setting.preferences.useDarkMode=Использовать тёмный режим setting.preferences.useLightMode=Использовать светлый режим setting.preferences.sortWithNumOffers=Сортировать списки по кол-ву предложений/сделок setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API setting.preferences.notifyOnPreRelease=Receive pre-release notifications setting.preferences.resetAllFlags=Сбросить все флажки \«Не показывать снова\» settings.preferences.languageChange=Изменение языка во всех разделах вступит в силу после перезагрузки приложения. settings.preferences.supportLanguageWarning=In case of a dispute, please note that arbitration is handled in {0}. settings.preferences.editCustomExplorer.headline=Explorer Settings settings.preferences.editCustomExplorer.description=Choose a system defined explorer from the list on the left, and/or customize to suit your own preferences. settings.preferences.editCustomExplorer.available=Available explorers settings.preferences.editCustomExplorer.chosen=Chosen explorer settings settings.preferences.editCustomExplorer.name=Имя settings.preferences.editCustomExplorer.txUrl=Transaction URL settings.preferences.editCustomExplorer.addressUrl=Address URL setting.info.headline=Новая функция защиты данных settings.preferences.sensitiveDataRemoval.msg=Для защиты вашей и других трейдеров конфиденциальности Haveno намерен удалять конфиденциальные данные из старых сделок. Это особенно важно для сделок с фиатной валютой, которые могут включать данные банковских счетов.\n\nРекомендуется установить минимальный срок, например, 60 дней. Это означает, что сделки старше 60 дней будут очищены от конфиденциальных данных, при условии, что они завершены. Завершённые сделки находятся во вкладке «Портфель / История». settings.net.xmrHeader=Сеть Биткойн settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=Мой onion-адрес settings.net.xmrNodesLabel=Использовать особые узлы Monero settings.net.moneroPeersLabel=Подключенные пиры settings.net.connection=Соединение settings.net.connected=Подключено settings.net.useTorForXmrJLabel=Использовать Tor для сети Monero settings.net.moneroNodesLabel=Узлы Monero для подключения settings.net.useProvidedNodesRadio=Использовать предоставленные узлы Monero Core settings.net.usePublicNodesRadio=Использовать общедоступную сеть Monero settings.net.useCustomNodesRadio=Использовать особые узлы Monero Core settings.net.warn.usePublicNodes=If you use public Monero nodes, you are subject to any risk of using untrusted remote nodes.\n\nPlease read more details at [HYPERLINK:https://www.getmonero.org/resources/moneropedia/remote-node.html].\n\nAre you sure you want to use public nodes? settings.net.warn.usePublicNodes.useProvided=Нет, использовать предоставленные узлы settings.net.warn.usePublicNodes.usePublic=Да, использовать общедоступную сеть settings.net.warn.useCustomNodes.B2XWarning=Убедитесь, что ваш узел Биткойн является доверенным узлом Monero Core! \n\nПодключение к узлам, не следующим правилам консенсуса Monero Core, может повредить ваш кошелек и вызвать проблемы в процессе торговли.\n\nПользователи, подключающиеся к узлам, нарушающим правила консенсуса, несут ответственность за любой причиненный ущерб. Любые споры в таком случае будут решаться в пользу вашего контрагента. Пользователям, игнорирующим это предупреждение и механизмы защиты, техническая поддержка предоставляться не будет! settings.net.warn.invalidXmrConfig=Connection to the Monero network failed because your configuration is invalid.\n\nYour configuration has been reset to use the provided Monero nodes instead. You will need to restart the application. settings.net.localhostXmrNodeInfo=Background information: Haveno looks for a local Monero node when starting. If it is found, Haveno will communicate with the Monero network exclusively through it. settings.net.p2PPeersLabel=Подключенные пиры settings.net.onionAddressColumn=Onion-адрес settings.net.creationDateColumn=Создано settings.net.connectionTypeColumn=Вх./Вых. settings.net.sentDataLabel=Sent data statistics settings.net.receivedDataLabel=Received data statistics settings.net.chainHeightLabel=Latest XMR block height settings.net.roundTripTimeColumn=Задержка settings.net.sentBytesColumn=Отправлено settings.net.receivedBytesColumn=Получено settings.net.peerTypeColumn=Тип узла settings.net.openTorSettingsButton=Открыть настройки Tor settings.net.versionColumn=Version settings.net.subVersionColumn=Subversion settings.net.heightColumn=Height settings.net.needRestart=Необходимо перезагрузить приложение, чтобы применить это изменение.\nСделать это сейчас? settings.net.notKnownYet=Пока неизвестно... settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec settings.net.chainHeight=Monero Peers chain height: {0} settings.net.ips=[IP-адрес:порт | хост:порт | onion-адрес:порт] (через запятые). Порт можно не указывать, если используется порт по умолчанию (8333). settings.net.seedNode=Исходный узел settings.net.directPeer=Пир (прямой) settings.net.initialDataExchange={0} [Bootstrapping] settings.net.peer=Пир settings.net.inbound=входящий settings.net.outbound=выходящий setting.about.aboutHaveno=О Haveno setting.about.about=Haveno — это программа с открытым исходным кодом, предназначенная для обмена биткойна на национальные валюты (и другие криптовалюты) через децентрализованную Р2Р-сеть, обеспечивающая надежную защиту конфиденциальности. Узнайте больше о Haveno на веб-странице нашего проекта. setting.about.web=Веб-страница Haveno setting.about.code=Исходный код setting.about.agpl=Лицензия AGPL setting.about.support=Поддержать Haveno setting.about.def=Haveno не является компанией, а представляет собой общественный проект, открытый для участия. Если вы хотите принять участие или поддержать Haveno, перейдите по ссылкам ниже. setting.about.contribute=Помочь setting.about.providers=Источники данных setting.about.apisWithFee=Haveno uses Haveno Price Indices for Fiat and Crypto market prices, and Haveno Mempool Nodes for mining fee estimation. setting.about.apis=Haveno uses Haveno Price Indices for Fiat and Crypto market prices. setting.about.pricesProvided=Рыночный курс предоставлен setting.about.feeEstimation.label=Расчёт комиссии майнера предоставлен setting.about.versionDetails=Подробности версии setting.about.version=Версия приложения setting.about.subsystems.label=Версии подсистем setting.about.subsystems.val=Версия сети: {0}; версия P2P-сообщений: {1}; версия локальной базы данных: {2}; версия торгового протокола: {3} setting.about.shortcuts=Short cuts setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' or 'alt + {0}' or 'cmd + {0}' setting.about.shortcuts.menuNav=Navigate main menu setting.about.shortcuts.menuNav.value=To navigate the main menu press: 'Ctrl' or 'alt' or 'cmd' with a numeric key between '1-9' setting.about.shortcuts.close=Close Haveno setting.about.shortcuts.close.value='Ctrl + {0}' or 'cmd + {0}' or 'Ctrl + {1}' or 'cmd + {1}' setting.about.shortcuts.closePopup=Close popup or dialog window setting.about.shortcuts.closePopup.value='ESCAPE' key setting.about.shortcuts.chatSendMsg=Send trader chat message setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' or 'alt + ENTER' or 'cmd + ENTER' setting.about.shortcuts.openDispute=Open dispute setting.about.shortcuts.openDispute.value=Select pending trade and click: {0} setting.about.shortcuts.walletDetails=Open wallet details window setting.about.shortcuts.openEmergencyXmrWalletTool=Open emergency wallet tool for XMR wallet setting.about.shortcuts.showTorLogs=Toggle log level for Tor messages between DEBUG and WARN setting.about.shortcuts.manualPayoutTxWindow=Open window for manual payout from 2of2 Multisig deposit tx setting.about.shortcuts.removeStuckTrade=Open popup to move failed trade to open trades tab again setting.about.shortcuts.removeStuckTrade.value=Select failed trade and press: {0} setting.about.shortcuts.registerArbitrator=Register arbitrator (mediator/arbitrator only) setting.about.shortcuts.registerArbitrator.value=Navigate to account and press: {0} setting.about.shortcuts.registerMediator=Register mediator (mediator/arbitrator only) setting.about.shortcuts.registerMediator.value=Navigate to account and press: {0} setting.about.shortcuts.openSignPaymentAccountsWindow=Open window for account age signing (legacy arbitrators only) setting.about.shortcuts.openSignPaymentAccountsWindow.value=Navigate to legacy arbitrator view and press: {0} setting.about.shortcuts.sendAlertMsg=Send alert or update message (privileged activity) setting.about.shortcuts.sendFilter=Set Filter (privileged activity) setting.about.shortcuts.sendPrivateNotification=Send private notification to peer (privileged activity) setting.about.shortcuts.sendPrivateNotification.value=Open peer info at avatar and press: {0} setting.info.headline=New XMR auto-confirm Feature setting.info.msg=When selling XMR for XMR you can use the auto-confirm feature to verify that the correct amount of XMR was sent to your wallet so that Haveno can automatically mark the trade as complete, making trades quicker for everyone.\n\nAuto-confirm checks the XMR transaction on at least 2 XMR explorer nodes using the private transaction key provided by the XMR sender. By default, Haveno uses explorer nodes run by Haveno contributors, but we recommend running your own XMR explorer node for maximum privacy and security.\n\nYou can also set the maximum amount of XMR per trade to auto-confirm as well as the number of required confirmations here in Settings.\n\nSee more details (including how to set up your own explorer node) on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades] #################################################################### # Account #################################################################### account.tab.mediatorRegistration=Mediator registration account.tab.refundAgentRegistration=Refund agent registration account.tab.signing=Signing account.info.headline=Добро пожаловать в ваш счёт Haveno account.info.msg=Here you can add trading accounts for national currencies & cryptos and create a backup of your wallet & account data.\n\nA new Monero wallet was created the first time you started Haveno.\n\nWe strongly recommend that you write down your Monero wallet seed words (see tab on the top) and consider adding a password before funding. Monero deposits and withdrawals are managed in the \"Funds\" section.\n\nPrivacy & security note: because Haveno is a decentralized exchange, all your data is kept on your computer. There are no servers, so we have no access to your personal info, your funds, or even your IP address. Data such as bank account numbers, crypto & Monero addresses, etc are only shared with your trading partner to fulfill trades you initiate (in case of a dispute the mediator or arbitrator will see the same data as your trading peer). account.menu.paymentAccount=Счета в нац. валюте account.menu.altCoinsAccountView=Альткойн-счета account.menu.password=Пароль кошелька account.menu.seedWords=Мнемоническая фраза account.menu.walletInfo=Wallet info account.menu.backup=Резервное копирование account.menu.notifications=Уведомления account.menu.walletInfo.balance.headLine=Wallet balances account.menu.walletInfo.balance.info=This shows the internal wallet balance including unconfirmed transactions.\nFor XMR, the internal wallet balance shown below should match the sum of the 'Available' and 'Reserved' balances shown in the top right of this window. account.menu.walletInfo.xpub.headLine=Watch keys (xpub keys) account.menu.walletInfo.walletSelector={0} {1} wallet account.menu.walletInfo.path.headLine=HD keychain paths account.menu.walletInfo.path.info=If you import seed words into another wallet (like Electrum), you'll need to define the path. This should only be done in emergency cases when you lose access to the Haveno wallet and data directory.\nKeep in mind that spending funds from a non-Haveno wallet can bungle the internal Haveno data structures associated with the wallet data, which can lead to failed trades.\n\nNEVER send BSQ from a non-Haveno wallet, as it will probably lead to an invalid BSQ transaction and losing your BSQ. account.menu.walletInfo.openDetails=Show raw wallet details and private keys ## TODO should we rename the following to a gereric name? account.arbitratorRegistration.pubKey=Публичный ключ account.arbitratorRegistration.register=Register account.arbitratorRegistration.registration={0} registration account.arbitratorRegistration.revoke=Аннулировать account.arbitratorRegistration.info.msg=Please note that you need to stay available for 15 days after revoking as there might be trades which are using you as {0}. The max. allowed trade period is 8 days and the dispute process might take up to 7 days. account.arbitratorRegistration.warn.min1Language=Необходимо указать хотя бы 1 язык.\nМы добавили язык по умолчанию. account.arbitratorRegistration.removedSuccess=You have successfully removed your registration from the Haveno network. account.arbitratorRegistration.removedFailed=Could not remove registration.{0} account.arbitratorRegistration.registerSuccess=You have successfully registered to the Haveno network. account.arbitratorRegistration.registerFailed=Could not complete registration.{0} account.crypto.yourCryptoAccounts=Ваши альткойн-счета account.crypto.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don't control your keys or (b) which don't use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is not a {2} specialist and cannot help in such cases. account.crypto.popup.wallet.confirm=Я понимаю и подтверждаю, что знаю, какой кошелёк нужно использовать. # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=Trading UPX on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending UPX, you need to use either the official uPlexa GUI wallet or uPlexa CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\nuplexa-wallet-cli (use the command get_tx_key)\nuplexa-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The UPX sender is responsible for providing verification of the UPX transfer to the arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit uPlexa discord channel (https://discord.gg/vhdNSrV) or the uPlexa Telegram Chat (https://t.me/uplexaOfficial) to find more information. # suppress inspection "UnusedProperty" account.crypto.popup.arq.msg=Trading ARQ on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending ARQ, you need to use either the official ArQmA GUI wallet or ArQmA CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\narqma-wallet-cli (use the command get_tx_key)\narqma-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the mediator or arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The ARQ sender is responsible for providing verification of the ARQ transfer to the mediator or arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit ArQmA discord channel (https://discord.gg/s9BQpJT) or the ArQmA forum (https://labs.arqma.com) to find more information. # suppress inspection "UnusedProperty" account.crypto.popup.xmr.msg=Trading XMR on Haveno requires that you understand the following requirement.\n\nIf selling XMR, you must be able to provide the following information to a mediator or arbitrator in case of a dispute:\n- the transaction key (Tx Key, Tx Secret Key or Tx Private Key)\n- the transaction ID (Tx ID or Tx Hash)\n- the destination address (recipient's address)\n\nSee the wiki for details on where to find this information on popular Monero wallets [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Proving_payments].\nFailure to provide the required transaction data will result in losing disputes.\n\nAlso note that Haveno now offers automatic confirming for XMR transactions to make trades quicker, but you need to enable it in Settings.\n\nSee the wiki for more information about the auto-confirm feature: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. # suppress inspection "UnusedProperty" account.crypto.popup.msr.msg=Trading MSR on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending MSR, you need to use either the official Masari GUI wallet, Masari CLI wallet with the store-tx-info flag enabled (enabled by default) or the Masari web wallet (https://wallet.getmasari.org). Please be sure you can access the tx key as that would be required in case of a dispute.\nmasari-wallet-cli (use the command get_tx_key)\nmasari-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nMasari Web Wallet (goto Account -> transaction history and view details on your sent transaction)\n\nVerification can be accomplished in-wallet.\nmasari-wallet-cli : using command (check_tx_key).\nmasari-wallet-gui : on the Advanced > Prove/Check page.\nVerification can be accomplished in the block explorer \nOpen block explorer (https://explorer.getmasari.org), use the search bar to find your transaction hash.\nOnce transaction is found, scroll to bottom to the 'Prove Sending' area and fill in details as needed.\nYou need to provide the mediator or arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The MSR sender is responsible for providing verification of the MSR transfer to the mediator or arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process, ask for help on the Official Masari Discord (https://discord.gg/sMCwMqs). # suppress inspection "UnusedProperty" account.crypto.popup.blur.msg=Trading BLUR on Haveno requires that you understand and fulfill the following requirements:\n\nTo send BLUR you must use the Blur Network CLI or GUI Wallet. \n\nIf you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\nIf you are using the Blur Network GUI Wallet, the transaction private key and transaction ID can be found conveniently in the "History" tab. Immediately after sending, locate the transaction of interest. Click the "?" symbol in the lower-right corner of the box containing the transaction. You must save this information. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the BLUR transfer using the Blur Transaction Viewer (https://blur.cash/#tx-viewer).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the BLUR sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the Blur Network Discord (https://discord.gg/dMWaqVW). # suppress inspection "UnusedProperty" account.crypto.popup.solo.msg=Trading Solo on Haveno requires that you understand and fulfill the following requirements:\n\nTo send Solo you must use the Solo Network CLI Wallet. \n\nIf you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the Solo transfer using the Solo Block Explorer by searching for the transaction and then using the "Prove sending" function (https://explorer.minesolo.com/).\n\nfailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the Solo sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the Solo Network Discord (https://discord.minesolo.com/). # suppress inspection "UnusedProperty" account.crypto.popup.cash2.msg=Trading CASH2 on Haveno requires that you understand and fulfill the following requirements:\n\nTo send CASH2 you must use the Cash2 Wallet version 3 or higher. \n\nAfter a transaction is sent, the transaction ID will be displayed. You must save this information. Immediately after sending the transaction, you must use the command 'getTxKey' in simplewallet to retrieve the transaction secret key. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, 2) the transaction secret key, and 3) the recipient's Cash2 address. The mediator or arbitrator will then verify the CASH2 transfer using the Cash2 Block Explorer (https://blocks.cash2.org).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the CASH2 sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the Cash2 Discord (https://discord.gg/FGfXAYN). # suppress inspection "UnusedProperty" account.crypto.popup.qwertycoin.msg=Trading Qwertycoin on Haveno requires that you understand and fulfill the following requirements:\n\nTo send QWC you must use the official QWC Wallet version 5.1.3 or higher. \n\nAfter a transaction is sent, the transaction ID will be displayed. You must save this information. Immediately after sending the transaction, you must use the command 'get_Tx_Key' in simplewallet to retrieve the transaction secret key. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, 2) the transaction secret key, and 3) the recipient's QWC address. The mediator or arbitrator will then verify the QWC transfer using the QWC Block Explorer (https://explorer.qwertycoin.org).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the QWC sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the QWC Discord (https://discord.gg/rUkfnpC). # suppress inspection "UnusedProperty" account.crypto.popup.drgl.msg=Trading Dragonglass on Haveno requires that you understand and fulfill the following requirements:\n\nBecause of the privacy Dragonglass provides, a transaction is not verifiable on the public blockchain. If required, you can prove your payment through the use of your TXN-Private-Key.\nThe TXN-Private Key is a one-time key automatically generated for every transaction that can only be accessed from within your DRGL wallet.\nEither by DRGL-wallet GUI (inside transaction details dialog) or by the Dragonglass CLI simplewallet (using command "get_tx_key").\n\nDRGL version 'Oathkeeper' and higher are REQUIRED for both.\n\nIn case of a dispute, you must provide the mediator or arbitrator the following data:\n- The TXN-Private key\n- The transaction hash\n- The recipient's public address\n\nVerification of payment can be made using the above data as inputs at (http://drgl.info/#check_txn).\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The Dragonglass sender is responsible for providing verification of the DRGL transfer to the mediator or arbitrator in case of a dispute. Use of PaymentID is not required.\n\nIf you are unsure about any part of this process, visit Dragonglass on Discord (http://discord.drgl.info) for help. # suppress inspection "UnusedProperty" account.crypto.popup.ZEC.msg=When using Zcash you can only use the transparent addresses (starting with t), not the z-addresses (private), because the mediator or arbitrator would not be able to verify the transaction with z-addresses. # suppress inspection "UnusedProperty" account.crypto.popup.XZC.msg=When using Zcoin you can only use the transparent (traceable) addresses, not the untraceable addresses, because the mediator or arbitrator would not be able to verify the transaction with untraceable addresses at a block explorer. # suppress inspection "UnusedProperty" account.crypto.popup.grin.msg=При создании транзакции в GRIN требуется взаимодействие в реальном времени между отправителем и получателем. Следуйте инструкциям на веб-сайте проекта GRIN, чтобы узнать, как отправлять и получать GRIN (получатель должен находиться в сети в момент отправки перевода или в течение определенного периода). \n\nHaveno поддерживает кошельки только в формате Grinbox (Wallet713). \n\nОтправитель GRIN должен предоставить доказательство успешной отправки перевода. Если отправитель не сможет предоставить это доказательство, потенциальный спор будет решен в пользу получателя GRIN. Убедитесь, что используете последнюю версию Grinbox с поддержкой доказательства транзакций и что понимаете, как нужно отправлять и получать GRIN, а также как создавать доказательство перевода. \n\nЧтобы узнать подробности работы с инструментом доказательства транзакции в Grinbox, см. https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only. # suppress inspection "UnusedProperty" account.crypto.popup.beam.msg=При создании транзакции в BEAM требуется взаимодействие в реальном времени между отправителем и получателем. \n\nСледуйте инструкциям на веб-сайте проекта BEAM, чтобы узнать, как отправлять и получать BEAM (получатель должен находиться в сети в момент отправки перевода или в течение определенного периода). \n\nОтправитель BEAM должен предоставить доказательство успешной отправки перевода. Используйте кошелёк, позволяющий получить доказательство перевода. Если отправитель не сможет предоставить это доказательство, потенциальный спор будет решен в пользу получателя BEAM. # suppress inspection "UnusedProperty" account.crypto.popup.pars.msg=Trading ParsiCoin on Haveno requires that you understand and fulfill the following requirements:\n\nTo send PARS you must use the official ParsiCoin Wallet version 3.0.0 or higher. \n\nYou can Check your Transaction Hash and Transaction Key on Transactions Section on your GUI Wallet (ParsiPay) You need to right Click on the Transaction and then click on show details. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the Transaction Hash, 2) the Transaction Key, and 3) the recipient's PARS address. The mediator or arbitrator will then verify the PARS transfer using the ParsiCoin Block Explorer (http://explorer.parsicoin.net/#check_payment).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the ParsiCoin sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the ParsiCoin Discord (https://discord.gg/c7qmFNh). # suppress inspection "UnusedProperty" account.crypto.popup.blk-burnt.msg=To trade burnt blackcoins, you need to know the following:\n\nBurnt blackcoins are unspendable. To trade them on Haveno, output scripts need to be in the form: OP_RETURN OP_PUSHDATA, followed by associated data bytes which, after being hex-encoded, constitute addresses. For example, burnt blackcoins with an address 666f6f (“foo” in UTF-8) will have the following script:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\nTo create burnt blackcoins, one may use the “burn” RPC command available in some wallets.\n\nFor possible use cases, one may look at https://ibo.laboratorium.ee .\n\nAs burnt blackcoins are unspendable, they can not be reselled. “Selling” burnt blackcoins means burning ordinary blackcoins (with associated data equal to the destination address).\n\nIn case of a dispute, the BLK seller needs to provide the transaction hash. # suppress inspection "UnusedProperty" account.crypto.popup.liquidmonero.msg=Trading L-XMR on Haveno requires that you understand the following:\n\nWhen receiving L-XMR for a trade on Haveno, you cannot use the mobile Blockstream Green Wallet app or a custodial/exchange wallet. You must only receive L-XMR into the Liquid Elements Core wallet, or another L-XMR wallet which allows you to obtain the blinding key for your blinded L-XMR address.\n\nIn the event mediation is necessary, or if a trade dispute arises, you must disclose the blinding key for your receiving L-XMR address to the Haveno mediator or refund agent so they can verify the details of your Confidential Transaction on their own Elements Core full node.\n\nFailure to provide the required information to the mediator or refund agent will result in losing the dispute case. In all cases of dispute, the L-XMR receiver bears 100% of the burden of responsibility in providing cryptographic proof to the mediator or refund agent.\n\nIf you do not understand these requirements, do not trade L-XMR on Haveno. account.traditional.yourTraditionalAccounts=Ваши счета в нац. валюте account.backup.title=Резервный кошелёк account.backup.location=Место хранения резервной копии account.backup.selectLocation=Выбрать место сохранения резервной копии account.backup.backupNow=Создать резервную копию (резервная копия не зашифрована!) account.backup.appDir=Каталог данных приложения account.backup.openDirectory=Открыть каталог account.backup.openLogFile=Открыть файл журнала account.backup.success=Резервная копия успешно сохранена в:\n{0} account.backup.directoryNotAccessible=Выбранный вами каталог недоступен. {0} account.password.removePw.button=Удалить пароль account.password.removePw.headline=Удалить защиту паролем для кошелька account.password.setPw.button=Установить пароль account.password.setPw.headline=Установить пароль для защиты кошелька account.password.info=При включенной защите паролем, вам потребуется вводить пароль при запуске приложения, при выводе монеро из вашего кошелька и при отображении ваших сидовых слов. account.seed.backup.title=Сделайте резервную копию семени вашего кошелька account.seed.info=Пожалуйста, запишите как семена вашего кошелька, так и дату. Вы можете восстановить свой кошелек в любое время, используя семена и дату.\n\nВы должны записать семена на лист бумаги. Не сохраняйте их на компьютере.\n\nОбратите внимание, что семена не заменяют резервное копирование. Вам нужно создать резервную копию всего каталога приложения с экрана "Аккаунт/Резервное копирование", чтобы восстановить состояние и данные приложения. account.seed.backup.warning=Пожалуйста, обратите внимание, что слова-семена НЕ являются заменой для резервной копии.\nНеобходимо создать резервную копию всего каталога приложения с экрана "Аккаунт/Резервное копирование" для восстановления состояния и данных приложения. account.seed.warn.noPw.msg=Вы не установили пароль от кошелька для защиты мнемонической фразы.\n\nОтобразить мнемоническую фразу на экране? account.seed.warn.noPw.yes=Да и не спрашивать снова account.seed.enterPw=Введите пароль, чтобы увидеть мнемоническую фразу account.seed.restore.info=Создайте резервную копию перед восстановлением с помощью мнемонической фразы. Помните, что восстановление кошелька используется в экстренных случаях и может вызвать сбой внутренней базы данных кошелька.\nЭто не способ резервного копирования! Используйте резервную копию из каталога данных приложения для восстановления его предыдущего состояния.\n\nПосле восстановления приложение автоматически закроется. Когда вы повторно запустите приложение, оно снова синхронизируется с сетью Биткойн. Это может занять долгое время и привести к высокой нагрузке на центральный процессор, особенно если кошелёк был создан давно и хранил много транзакций. Не прерывайте данный процесс. Иначе вам придется удалить файл цепи SPV или повторить процесс восстановления сначала. account.seed.restore.ok=Восстановить и закрыть Haveno #################################################################### # Mobile notifications #################################################################### account.notifications.setup.title=Установка account.notifications.download.label=Скачать мобильное приложение account.notifications.waitingForWebCam=Ожидание веб-камеры... account.notifications.webCamWindow.headline=Сканировать код QR с телефона account.notifications.webcam.label=Использовать веб-камеру account.notifications.webcam.button=Сканировать код QR account.notifications.noWebcam.button=Нет веб-камеры account.notifications.erase.label=Очистить уведомления на телефоне account.notifications.erase.title=Очистить уведомления account.notifications.email.label=Токен сопряжения account.notifications.email.prompt=Введите токен сопряжения, полученный по электронной почте account.notifications.settings.title=Настройки account.notifications.useSound.label=Проигрывать уведомление на телефоне account.notifications.trade.label=Получать сообщения по сделке account.notifications.market.label=Получать оповещения о предложении account.notifications.price.label=Получать оповещения о курсе account.notifications.priceAlert.title=Оповещения о курсе account.notifications.priceAlert.high.label=Уведомить, если курс XMR выше account.notifications.priceAlert.low.label=Уведомить, если курс XMR ниже account.notifications.priceAlert.setButton=Установить оповещение о курсе account.notifications.priceAlert.removeButton=Удалить оповещение о курсе account.notifications.trade.message.title=Состояние сделки изменилось account.notifications.trade.message.msg.conf=Депозит по сделке с идентификатором {0} внесен. Откройте приложение Haveno и начните платеж. account.notifications.trade.message.msg.started=Покупатель XMR начал платеж по сделке с идентификатором {0}. account.notifications.trade.message.msg.completed=Сделка с идентификатором {0} завершена. account.notifications.offer.message.title=Ваше предложение было принято account.notifications.offer.message.msg=Ваше предложение с идентификатором {0} было принято account.notifications.dispute.message.title=Новое сообщение по спору account.notifications.dispute.message.msg=Получено сообщение по спору в сделке с идентификатором {0} account.notifications.marketAlert.title=Оповещения о предложении account.notifications.marketAlert.selectPaymentAccount=Предложения, соответствующие платежному счету account.notifications.marketAlert.offerType.label=Интересующий тип предложения account.notifications.marketAlert.offerType.buy=Предложения купить (хочу продать XMR) account.notifications.marketAlert.offerType.sell=Предложения продать (хочу купить XMR) account.notifications.marketAlert.trigger=Отклонение предложения от курса (%) account.notifications.marketAlert.trigger.info=Если задано отклонение от курса, вы получите оповещение только при публикации предложения, соответствующего вашим требованиям (или превышающего их). Например: вы хотите продать XMR, но только с надбавкой 2% к текущему рыночному курсу. Указав 2% в этом поле, вы получите оповещение только о предложениях с курсом, превышающим текущий рыночный курс на 2% (или более). account.notifications.marketAlert.trigger.prompt=Отклонение в процентах от рыночного курса (напр., 2,50%, -0,50% и т. д.) account.notifications.marketAlert.addButton=Добавить оповещение о предложении account.notifications.marketAlert.manageAlertsButton=Управление оповещениями о предложениях account.notifications.marketAlert.manageAlerts.title=Управление оповещениями о предложениях account.notifications.marketAlert.manageAlerts.header.paymentAccount=Платёжный счёт account.notifications.marketAlert.manageAlerts.header.trigger=Начальная цена account.notifications.marketAlert.manageAlerts.header.offerType=Тип предложения account.notifications.marketAlert.message.title=Оповещение о предложении account.notifications.marketAlert.message.msg.below=ниже account.notifications.marketAlert.message.msg.above=выше account.notifications.marketAlert.message.msg=Новое предложение «{0} {1}» с ценой {2} (рыночная цена — {3} {4}) и методом платежа «{5}» было опубликовано в Haveno.\nИдент. предложения: {6}. account.notifications.priceAlert.message.title=Оповещение о цене для {0} account.notifications.priceAlert.message.msg=Ваше оповещение о цене сработало. Текущая цена {0} — {1} {2} account.notifications.noWebCamFound.warning=Веб-камера не найдена.\n\nВоспользуйтесь электронной почтой для отправки токена и ключа шифрования с вашего мобильного телефона в приложение Haveno. account.notifications.priceAlert.warning.highPriceTooLow=Более высокая цена должна быть выше более низкой цены. account.notifications.priceAlert.warning.lowerPriceTooHigh=Более низкая цена должна быть ниже более высокой цены. #################################################################### # Windows #################################################################### inputControlWindow.headline=Select inputs for transaction inputControlWindow.balanceLabel=Доступный баланс contractWindow.title=Подробности спора contractWindow.dates=Дата предложения / Дата сделки contractWindow.xmrAddresses=Биткойн-адрес покупателя XMR / продавца XMR contractWindow.onions=Сетевой адрес покупателя XMR / продавца XMR contractWindow.accountAge=Возраст счёта (покупатель/продавец XMR) contractWindow.numDisputes=Кол-во споров покупателя XMR / продавца XMR contractWindow.contractHash=Хеш контракта displayAlertMessageWindow.headline=Важная информация! displayAlertMessageWindow.update.headline=Информация о важном обновлении! displayAlertMessageWindow.update.download=Загрузить: displayUpdateDownloadWindow.downloadedFiles=Файлы: displayUpdateDownloadWindow.downloadingFile=Загрузka: {0} displayUpdateDownloadWindow.verifiedSigs=Подпись, подтвержденная ключами: displayUpdateDownloadWindow.status.downloading=Файлы загружаются... displayUpdateDownloadWindow.status.verifying=Проверка подписи... displayUpdateDownloadWindow.button.label=Загрузите установщик и проверьте подпись displayUpdateDownloadWindow.button.downloadLater=Скачать позже displayUpdateDownloadWindow.button.ignoreDownload=Игнорировать эту версию displayUpdateDownloadWindow.headline=Доступно новое обновление Haveno! displayUpdateDownloadWindow.download.failed.headline=Загрузка не удалась displayUpdateDownloadWindow.download.failed=Download failed.\nPlease download and verify manually at [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.installer.failed=Unable to determine the correct installer. Please download and verify manually at [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.verify.failed=Verification failed.\nPlease download and verify manually at [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.success=Новая версия загружена, а её подпись проверена.\n\nОткройте директорию загрузки, закройте приложение и установите новую версию. displayUpdateDownloadWindow.download.openDir=Открыть директорию загрузки disputeSummaryWindow.title=Сводка disputeSummaryWindow.openDate=Дата обращения за поддержкой disputeSummaryWindow.role=Роль трейдера disputeSummaryWindow.payout=Выплата суммы сделки disputeSummaryWindow.payout.getsTradeAmount={0} XMR получит выплату суммы сделки disputeSummaryWindow.payout.getsAll=Max. payout to XMR {0} disputeSummaryWindow.payout.custom=Пользовательская выплата disputeSummaryWindow.payoutAmount.buyer=Сумма выплаты покупателя disputeSummaryWindow.payoutAmount.seller=Сумма выплаты продавца disputeSummaryWindow.payoutAmount.invert=Проигравший публикует disputeSummaryWindow.reason=Причина спора disputeSummaryWindow.tradePeriodEnd=Trade period end disputeSummaryWindow.extraInfo=Extra information disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BUG=Ошибка # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.USABILITY=Удобство использования # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Нарушение протокола # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.NO_REPLY=Отсутствие ответа # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SCAM=Мошенничество # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OTHER=Другое # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BANK_PROBLEMS=Банк # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OPTION_TRADE=Option trade # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Trader not responding # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled disputeSummaryWindow.summaryNotes=Примечания disputeSummaryWindow.addSummaryNotes=Добавить примечания disputeSummaryWindow.close.button=Закрыть обращение # Do no change any line break or order of tokens as the structure is used for signature verification # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.msg=Ticket closed on {0}\n{1} node address: {2}\n\nSummary:\nTrade ID: {3}\nCurrency: {4}\nTrade amount: {5}\nPayout amount for XMR buyer: {6}\nPayout amount for XMR seller: {7}\n\nReason for dispute: {8}\n\nSummary notes:\n{9}\n # Do no change any line break or order of tokens as the structure is used for signature verification disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3} disputeSummaryWindow.close.nextStepsForMediation=\nNext steps:\nOpen trade and accept or reject suggestion from mediator disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\nNo further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions disputeSummaryWindow.close.closePeer=Вам также необходимо закрыть обращение контрагента! disputeSummaryWindow.close.txDetails.headline=Publish refund transaction # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.buyer=Buyer receives {0} on address: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=Seller receives {0} on address: {1}\n disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3}\n\nAre you sure you want to publish this transaction? disputeSummaryWindow.close.noPayout.headline=Close without any payout disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? emptyWalletWindow.headline=Аварийный кошелёк {0} emptyWalletWindow.info=Используйте этот инструмент только в экстренном случае, если вам недоступны средства из пользовательского интерфейса.\n\nУчтите, что все открытые предложения будут автоматически закрыты при использовании этого инструмента.\n\nПрежде чем воспользоваться этим инструментом, создайте резервную копию своего каталога данных. Это можно сделать в разделе \«Счёт/Резервное копирование\».\n\nСообщите нам о неисправности и создайте отчёт о ней в Github или на форуме Haveno, чтобы мы могли выявить её причину. emptyWalletWindow.balance=Доступный баланс кошелька emptyWalletWindow.address=Адрес получателя emptyWalletWindow.button=Отправить все средства emptyWalletWindow.openOffers.warn=У вас есть открытые предложения, которые будут удалены, если вы выведите все средства с кошелька.\nВывести все средства? emptyWalletWindow.openOffers.yes=Да, я уверен (-а) emptyWalletWindow.sent.success=Все средства с вашего кошелька были успешно отправлены. enterPrivKeyWindow.headline=Enter private key for registration filterWindow.headline=Изменить список фильтров filterWindow.offers=Отфильтрованные предложения (через запят.) filterWindow.onions=Banned from trading addresses (comma sep.) filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.) filterWindow.accounts=Отфильтрованные данные торгового счёта:\nФормат: список, через запятые [идентификатор метода платежа | поле данных | значение] filterWindow.bannedCurrencies=Отфильтрованные коды валют (через запят.) filterWindow.bannedPaymentMethods=Отфильтрованные идент. методов платежа (через запят.) filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) filterWindow.arbitrators=Отфильтрованные арбитры (onion-адреса через запят.) filterWindow.mediators=Filtered mediators (comma sep. onion addresses) filterWindow.refundAgents=Filtered refund agents (comma sep. onion addresses) filterWindow.seedNode=Отфильтрованные исходные узлы (onion-адреса через запят.) filterWindow.priceRelayNode=Отфильтрованные ретрансляторы курса (onion-адреса через запят.) filterWindow.xmrNode=Отфильтрованные узлы Биткойн (адреса + порты через запят.) filterWindow.preventPublicXmrNetwork=Не использовать общедоступную сеть Биткойн filterWindow.disableAutoConf=Disable auto-confirm filterWindow.autoConfExplorers=Filtered auto-confirm explorers (comma sep. addresses) filterWindow.disableTradeBelowVersion=Мин. версия, необходимая для торговли filterWindow.add=Добавить фильтр filterWindow.remove=Удалить фильтр filterWindow.xmrFeeReceiverAddresses=XMR fee receiver addresses filterWindow.disableApi=Disable API filterWindow.disableMempoolValidation=Disable Mempool Validation offerDetailsWindow.minXmrAmount=Мин. количество XMR offerDetailsWindow.min=(мин. {0}) offerDetailsWindow.distance=(отклонение от рыночного курса: {0}) offerDetailsWindow.myTradingAccount=Мой торговый счёт offerDetailsWindow.offererBankId=(Идент./BIC/SWIFT банка мейкера) offerDetailsWindow.offerersBankName=(Название банка мейкера) offerDetailsWindow.bankId=Идентификатор банка (напр., BIC или SWIFT) offerDetailsWindow.countryBank=Страна банка мейкера offerDetailsWindow.commitment=Обязательство offerDetailsWindow.agree=Подтверждаю offerDetailsWindow.tac=Пользовательское соглашение offerDetailsWindow.confirm.maker.buy=Подтверждение: Создать предложение на покупку XMR за {0} offerDetailsWindow.confirm.maker.sell=Подтверждение: Создать предложение на продажу XMR за {0} offerDetailsWindow.confirm.taker.buy=Подтверждение: Принять предложение на покупку XMR за {0} offerDetailsWindow.confirm.taker.sell=Подтверждение: Принять предложение на продажу XMR за {0} offerDetailsWindow.creationDate=Дата создания offerDetailsWindow.makersOnion=Onion-адрес мейкера offerDetailsWindow.challenge=Пароль предложения offerDetailsWindow.challenge.copy=Скопируйте кодовую фразу, чтобы поделиться с партнёром qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. qRCodeWindow.request=Запрос платежа:\n{0} selectDepositTxWindow.headline=Выберите транзакцию ввода средств для включения в спор selectDepositTxWindow.msg=Транзакция ввода средств не сохранилась в сделке.\nВыберите в своём кошельке одну из существующих multisig-транзакций, которая использовалась для ввода средств в неудавшейся сделке.\n\nНужную транзакцию можно найти, открыв окно сведений о сделке (щелкните на идентификаторе сделки в списке). Multisig-транзакция ввода средств (её адрес начинается с 3) следует сразу за транзакцией оплаты торгового сбора. Этот идентификатор транзакции должен быть виден в представленном здесь списке. После того, как вы найдёте нужную транзакцию, выберите её здесь и продолжите.\n\nПриносим извинения за неудобство. Такие ошибки случаются редко, и мы работаем над тем, чтобы найти наилучший способ их решения. selectDepositTxWindow.select=Выберите транзакцию ввода средств sendAlertMessageWindow.headline=Отправить глобальное уведомление sendAlertMessageWindow.alertMsg=Предупреждение sendAlertMessageWindow.enterMsg=Введите сообщение sendAlertMessageWindow.isSoftwareUpdate=Software download notification sendAlertMessageWindow.isUpdate=Is full release sendAlertMessageWindow.isPreRelease=Is pre-release sendAlertMessageWindow.version=Номер новой версии sendAlertMessageWindow.send=Отправить уведомление sendAlertMessageWindow.remove=Удалить уведомление sendPrivateNotificationWindow.headline=Отправить личное сообщение sendPrivateNotificationWindow.privateNotification=Личное уведомление sendPrivateNotificationWindow.enterNotification=Введите уведомление sendPrivateNotificationWindow.send=Отправить личное уведомление showWalletDataWindow.walletData=Данные кошелька showWalletDataWindow.includePrivKeys=Добавить приватные ключи setXMRTxKeyWindow.headline=Prove sending of XMR setXMRTxKeyWindow.note=Adding tx info below enables auto-confirm for quicker trades. See more: https://haveno.exchange/wiki/Trading_Monero setXMRTxKeyWindow.txHash=Transaction ID (optional) setXMRTxKeyWindow.txKey=Transaction key (optional) # We do not translate the tac because of the legal nature. We would need translations checked by lawyers # in each language which is too expensive atm. tacWindow.headline=Пользовательское соглашение tacWindow.agree=Подтверждаю tacWindow.disagree=Не согласен (-сна) и выхожу tacWindow.arbitrationSystem=Dispute resolution tradeDetailsWindow.headline=Сделка tradeDetailsWindow.disputedPayoutTxId=Идент. оспоренной транзакции выплаты: tradeDetailsWindow.tradeDate=Дата сделки tradeDetailsWindow.txFee=Комиссия майнера tradeDetailsWindow.tradePeersOnion=Оnion-адрес контрагента tradeDetailsWindow.tradePeersPubKeyHash=Trading peers pubkey hash tradeDetailsWindow.tradeState=Статус сделки tradeDetailsWindow.agentAddresses=Arbitrator/Mediator tradeDetailsWindow.detailData=Detail data txDetailsWindow.headline=Transaction Details txDetailsWindow.xmr.noteSent=Вы отправили XMR. txDetailsWindow.xmr.noteReceived=Вы получили XMR. txDetailsWindow.sentTo=Отправлено в txDetailsWindow.receivedWith=Получено с txDetailsWindow.txId=TxId closedTradesSummaryWindow.headline=Trade history summary closedTradesSummaryWindow.totalAmount.title=Total trade amount closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) closedTradesSummaryWindow.totalTradeFeeInXmr.title=Sum of all trade fees paid in XMR closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} of total trade amount) walletPasswordWindow.headline=Введите пароль для разблокировки xmrConnectionError.headline=Ошибка подключения к Monero xmrConnectionError.providedNodes=Ошибка подключения к указанным узлам Monero.\n\nХотите использовать следующий лучший доступный узел Monero? xmrConnectionError.customNodes=Ошибка подключения к вашим пользовательским узлам Monero.\n\nХотите использовать следующий лучший доступный узел Monero? xmrConnectionError.localNode=Haveno ранее был подключен к локальному узлу Monero, но теперь он недоступен.\n\nУбедитесь, что ваш локальный узел запущен и полностью синхронизирован, или выберите другой вариант для продолжения. xmrConnectionError.localNode.start=Запустить локальный узел xmrConnectionError.localNode.start.error=Ошибка при запуске локального узла xmrConnectionError.localNode.fallback=Подключиться к следующему лучшему узлу torNetworkSettingWindow.header=Настройки сети Тоr torNetworkSettingWindow.noBridges=Не использовать мосты torNetworkSettingWindow.providedBridges=Подключиться через предоставленные мосты torNetworkSettingWindow.customBridges=Ввести свои мосты torNetworkSettingWindow.transportType=Тип транспортировки torNetworkSettingWindow.obfs3=obfs3 torNetworkSettingWindow.obfs4=obfs4 (рекомендуется) torNetworkSettingWindow.meekAmazon=meek-amazon torNetworkSettingWindow.meekAzure=meek-azure torNetworkSettingWindow.enterBridge=Введите один или более мостовой узел (по одному на строку) torNetworkSettingWindow.enterBridgePrompt=печатать адрес:порт torNetworkSettingWindow.restartInfo=Чтобы изменения вступили в силу, необходимо перезапустить приложение torNetworkSettingWindow.openTorWebPage=Открыть веб-страницу проекта Tor torNetworkSettingWindow.deleteFiles.header=Проблемы с подключением? torNetworkSettingWindow.deleteFiles.info=Если при запуске повторно возникают проблемы с подключением, попробуйте удалить устаревшие файлы Tor. Для этого нажмите на кнопку ниже и перезагрузите приложение. torNetworkSettingWindow.deleteFiles.button=Удалить устаревшие файлы Tor и завершить работу torNetworkSettingWindow.deleteFiles.progress=Tor завершает работу torNetworkSettingWindow.deleteFiles.success=Устаревшие файлы Tor успешно удалены. Пожалуйста, перезапустите приложение. torNetworkSettingWindow.bridges.header=Tor сеть заблокирована? torNetworkSettingWindow.bridges.info=Если Tor заблокирован вашим интернет-провайдером или правительством, попробуйте использовать мосты Tor.\nПосетите веб-страницу Tor по адресу: https://bridges.torproject.org, чтобы узнать больше о мостах и подключаемых транспортных протоколах. feeOptionWindow.headline=Выберите валюту для оплаты торгового сбора feeOptionWindow.info=Вы можете оплатить комиссию за сделку в BSQ или XMR. Если вы выберите BSQ, то сумма комиссии будет ниже. feeOptionWindow.optionsLabel=Выберите валюту для оплаты комиссии за сделку feeOptionWindow.useXMR=Использовать ВТС feeOptionWindow.fee={0} (≈ {1}) feeOptionWindow.xmrFeeWithFiatAndPercentage={0} (≈ {1} / {2}) feeOptionWindow.xmrFeeWithPercentage={0} ({1}) #################################################################### # Popups #################################################################### popup.headline.notification=Уведомление popup.headline.instruction=Обратите внимание: popup.headline.attention=Внимание popup.headline.backgroundInfo=Справочная информация popup.headline.feedback=Завершено popup.headline.confirmation=Подтверждение popup.headline.information=Информация popup.headline.warning=Предупреждение popup.headline.error=Ошибка popup.doNotShowAgain=Не показывать снова popup.reportError.log=Открыть файл журнала popup.reportError.gitHub=Сообщить о проблеме в Github popup.reportError={0}\n\nЧтобы помочь нам улучшить приложение, просьба сообщить об ошибке, открыв новую тему на https://github.com/haveno-dex/haveno/issues. \nСообщение об ошибке будет скопировано в буфер обмена при нажатии любой из кнопок ниже.\nЕсли вы прикрепите к отчету о неисправности файл журнала haveno.log, нажав «Открыть файл журнала» и сохранив его копию, это поможет нам разобраться с проблемой быстрее. popup.error.tryRestart=Попробуйте перезагрузить приложение и проверьте подключение к сети, чтобы попробовать решить проблему. popup.error.takeOfferRequestFailed=Произошла ошибка, когда контрагент попытался принять одно из ваших предложений:\n{0} error.spvFileCorrupted=Произошла ошибка при чтении файла цепи SPV.\nВозможно, файл цепи SPV повреждён.\n\nСообщение об ошибке: {0}\n\nУдалить файл SPV и начать повторную синхронизацию? error.deleteAddressEntryListFailed=Не удалось удалить файл AddressEntryList. \nОшибка: {0} error.closedTradeWithUnconfirmedDepositTx=The deposit transaction of the closed trade with the trade ID {0} is still unconfirmed.\n\nPlease do a SPV resync at \"Setting/Network info\" to see if the transaction is valid. error.closedTradeWithNoDepositTx=The deposit transaction of the closed trade with the trade ID {0} is null.\n\nPlease restart the application to clean up the closed trades list. popup.warning.walletNotInitialized=Кошелёк ещё не инициализирован popup.warning.osxKeyLoggerWarning=Due to stricter security measures in macOS 10.14 and above, launching a Java application (Haveno uses Java) causes a popup warning in macOS ('Haveno would like to receive keystrokes from any application').\n\nTo avoid that issue please open your 'macOS Settings' and go to 'Security & Privacy' -> 'Privacy' -> 'Input Monitoring' and Remove 'Haveno' from the list on the right side.\n\nHaveno will upgrade to a newer Java version to avoid that issue as soon the technical limitations (Java packager for the required Java version is not shipped yet) are resolved. popup.warning.wrongVersion=Вероятно, у вас установлена не та версия Haveno.\nАрхитектура Вашего компьютера: {0}.\nУстановленная версия Haveno: {1}.\nЗакройте приложение и установите нужную версию ({2}). popup.warning.incompatibleDB=We detected incompatible data base files!\n\nThose database file(s) are not compatible with our current code base:\n{0}\n\nWe made a backup of the corrupted file(s) and applied the default values to a new database version.\n\nThe backup is located at:\n{1}/db/backup_of_corrupted_data.\n\nPlease check if you have the latest version of Haveno installed.\nYou can download it at: [HYPERLINK:https://haveno.exchange/downloads].\n\nPlease restart the application. popup.warning.startupFailed.twoInstances=Haveno уже запущен. Нельзя запустить два экземпляра Haveno. popup.warning.tradePeriod.halfReached=Половина макс. допустимого срока сделки с идентификатором {0} истекла, однако она до сих пор не завершена.\n\nСрок сделки заканчивается {1}\n\nДополнительную информацию о состоянии сделки можно узнать в разделе \«Сделки/Текущие сделки\». popup.warning.tradePeriod.ended=Your trade with ID {0} has reached the max. allowed trading period and is not completed.\n\nThe trade period ended on {1}\n\nPlease check your trade at \"Portfolio/Open trades\" for contacting the arbitrator. popup.warning.noTradingAccountSetup.headline=Вы не создали торговый счёт popup.warning.noTradingAccountSetup.msg=Перед созданием предложения необходимо создать счета в национальной валюте или альткойнах. \nСоздать счёт? popup.warning.noArbitratorsAvailable=Нет доступных арбитров. popup.warning.noMediatorsAvailable=There are no mediators available. popup.warning.notFullyConnected=Необходимо дождаться полного подключения к сети.\nОно может занять до 2 минут. popup.warning.notSufficientConnectionsToXmrNetwork=Необходимо дождаться не менее {0} соединений с сетью Биткойн. popup.warning.downloadNotComplete=Необходимо дождаться завершения загрузки недостающих блоков сети Биткойн. popup.warning.walletNotSynced=Кошелек Haveno не синхронизирован с последней высотой блокчейна. Пожалуйста, подождите, пока кошелек синхронизируется, или проверьте ваше соединение. popup.warning.removeOffer=Действительно хотите удалить это предложение? popup.warning.tooLargePercentageValue=Нельзя установить процент в размере 100% или выше. popup.warning.examplePercentageValue=Введите процент, например \«5,4\» для 5,4% popup.warning.noPriceFeedAvailable=Источник рыночного курса для этой валюты отсутствует. Невозможно использовать процентный курс.\nПросьба указать фиксированный курс. popup.warning.sendMsgFailed=Не удалось отправить сообщение вашему контрагенту .\nПопробуйте еще раз, и если неисправность повторится, сообщите о ней. popup.warning.messageTooLong=Ваше сообщение превышает макс. разрешённый размер. Разбейте его на несколько частей или загрузите в веб-приложение для работы с отрывками текста, например https://pastebin.com. popup.warning.lockedUpFunds=You have locked up funds from a failed trade.\nLocked up balance: {0} \nDeposit tx address: {1}\nTrade ID: {2}.\n\nPlease open a support ticket by selecting the trade in the open trades screen and pressing \"alt + o\" or \"option + o\"." popup.warning.moneroConnection=Возникла проблема с подключением к сети Monero.\n\n{0} popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n takeOffer.cancelButton=Cancel take-offer takeOffer.warningButton=Ignore and continue anyway # suppress inspection "UnusedProperty" popup.warning.nodeBanned=One of the {0} nodes got banned. # suppress inspection "UnusedProperty" popup.warning.priceRelay=ретранслятор курса popup.warning.seed=мнемоническая фраза popup.warning.mandatoryUpdate.trading=Обновите Haveno до последней версии. Вышло обязательное обновление, которое делает невозможной торговлю в старых версиях приложения. Посетите форум Haveno, чтобы узнать подробности. popup.warning.noFilter=Мы не получили объект фильтра от узлов-источников. Пожалуйста, сообщите администраторам сети, чтобы они зарегистрировали объект фильтра. popup.warning.burnXMR=Данную транзакцию невозможно завершить, так как плата за нее ({0}) превышает сумму перевода ({1}). Подождите, пока плата за транзакцию не снизится или пока у вас не появится больше XMR для завершения перевода. popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Monero network.\nTransaction ID={1}.\nThe offer has been removed to avoid further problems.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. popup.warning.trade.txRejected.tradeFee=trade fee popup.warning.trade.txRejected.deposit=deposit popup.warning.trade.txRejected=The {0} transaction for trade with ID {1} was rejected by the Monero network.\nTransaction ID={2}\nThe trade has been moved to failed trades.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. popup.warning.openOfferWithInvalidMakerFeeTx=The maker fee transaction for offer with ID {0} is invalid.\nTransaction ID={1}.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. popup.info.securityDepositInfo=Чтобы гарантировать соблюдение торгового протокола трейдерами, им обоим необходимо внести залог.\n\nЗалог останется в вашем кошельке до успешного завершения сделки, а затем будет возвращен вам.\n\nОбратите внимание, что если вы создаёте новое предложение, приложение Haveno должно быть подключено к сети, чтобы его могли принять другие трейдеры. Чтобы ваши предложения были доступны в сети, компьютер и приложение должны быть включены и подключены к сети (убедитесь, что компьютер не перешёл в режим ожидания; переход монитора в спящий режим не влияет на работу приложения). popup.info.cashDepositInfo=Убедитесь, что в вашем районе есть отделение банка, где можно произвести перевод наличных.\nИдентификатор (BIC/SWIFT) банка продавца: {0}. popup.info.cashDepositInfo.confirm=Я подтверждаю, что могу внести оплату popup.info.shutDownWithOpenOffers=Haveno закрывается, но у вас есть открытые предложения.\n\nЭти предложения будут недоступны в сети P2P, пока приложение Haveno закрыто, но будут повторно опубликованы в сети P2P при следующем запуске Haveno.\n\nЧтобы ваши предложения были доступны в сети, компьютер и приложение должны быть включены и подключены к сети (убедитесь, что компьютер не перешёл в режим ожидания; переход монитора в спящий режим не влияет на работу приложения). popup.info.qubesOSSetupInfo=It appears you are running Haveno on Qubes OS. \n\nPlease make sure your Haveno qube is setup according to our Setup Guide at [HYPERLINK:https://haveno.exchange/wiki/Running_Haveno_on_Qubes]. popup.warn.downGradePrevention=Downgrade from version {0} to version {1} is not supported. Please use the latest Haveno version. popup.privateNotification.headline=Важное личное уведомление! popup.securityRecommendation.headline=Важная рекомендация по безопасности popup.securityRecommendation.msg=Рекомендуем вам защитить свой кошелёк паролем, если вы ещё этого не сделали.\n\nТакже убедительно рекомендуем записать вашу мнемоническую фразу. Она поможет восстановить ваш кошелёк Биткойн.\nДополнительная информация указана в разделе \«Мнемоническая фраза\».\n\nВам также следует сохранить копию папки со всеми данными программы в разделе \«Резервное копирование\». popup.shutDownInProgress.headline=Завершение работы popup.shutDownInProgress.msg=Завершение работы приложения может занять несколько секунд.\nПросьба не прерывать этот процесс. popup.attention.forTradeWithId=Обратите внимание на сделку с идентификатором {0} popup.attention.reasonForPaymentRuleChange=Version 1.5.5 introduces a critical trade rule change regarding the \"reason for payment\" field in bank transfers. Please leave this field empty -- DO NOT use the trade ID as \"reason for payment\" anymore. popup.info.multiplePaymentAccounts.headline=Доступно несколько платёжных счетов popup.info.multiplePaymentAccounts.msg=У вас есть несколько платёжных счетов, доступных для этого предложения. Просьба убедиться, что вы выбрали правильный счёт. popup.accountSigning.selectAccounts.headline=Select payment accounts popup.accountSigning.selectAccounts.description=Based on the payment method and point of time all payment accounts that are connected to a dispute where a payout to the buyer occurred will be selected for you to sign. popup.accountSigning.selectAccounts.signAll=Sign all payment methods popup.accountSigning.selectAccounts.datePicker=Select point of time until which accounts will be signed popup.accountSigning.confirmSelectedAccounts.headline=Confirm selected payment accounts popup.accountSigning.confirmSelectedAccounts.description=Based on your input, {0} payment accounts will be selected. popup.accountSigning.confirmSelectedAccounts.button=Confirm payment accounts popup.accountSigning.signAccounts.headline=Confirm signing of payment accounts popup.accountSigning.signAccounts.description=Based on your selection, {0} payment accounts will be signed. popup.accountSigning.signAccounts.button=Sign payment accounts popup.accountSigning.signAccounts.ECKey=Enter private arbitrator key popup.accountSigning.signAccounts.ECKey.error=Bad arbitrator ECKey popup.accountSigning.success.headline=Congratulations popup.accountSigning.success.description=All {0} payment accounts were successfully signed! popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\nFor further information, please visit [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer's account after a successful trade.\n\n{0} popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you'll be able to sign other accounts in {0} days from now.\n\n{1} popup.accountSigning.peerLimitLifted=The initial limit for one of your accounts has been lifted.\n\n{0} popup.accountSigning.peerSigner=One of your accounts is mature enough to sign other payment accounts and the initial limit for one of your accounts has been lifted.\n\n{0} popup.accountSigning.singleAccountSelect.headline=Import unsigned account age witness popup.accountSigning.confirmSingleAccount.headline=Confirm selected account age witness popup.accountSigning.confirmSingleAccount.selectedHash=Selected witness hash popup.accountSigning.confirmSingleAccount.button=Sign account age witness popup.accountSigning.successSingleAccount.description=Witness {0} was signed popup.accountSigning.successSingleAccount.success.headline=Success popup.accountSigning.unsignedPubKeys.headline=Unsigned Pubkeys popup.accountSigning.unsignedPubKeys.sign=Sign Pubkeys popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign popup.info.buyerAsTakerWithoutDeposit.headline=Депозит от покупателя не требуется popup.info.buyerAsTakerWithoutDeposit=Ваше предложение не потребует залога или комиссии от покупателя XMR.\n\nЧтобы принять ваше предложение, вы должны поделиться парольной фразой с вашим торговым партнером вне Haveno.\n\nПарольная фраза генерируется автоматически и отображается в деталях предложения после его создания. #################################################################### # Notifications #################################################################### notification.trade.headline=Уведомление о сделке с идентификатором {0} notification.ticket.headline=Запрос в службу поддержки для сделки с идентификатором {0} notification.trade.completed=Сделка завершена, и вы можете вывести свои средства. notification.trade.accepted=Ваше предложение принял {0} ВТС. notification.trade.unlocked=Ваша сделка была подтверждена в блокчейне не менее одного раза.\nМожете начать оплату. notification.trade.paymentSent=Покупатель ВТС начал оплату. notification.trade.selectTrade=Выбрать сделку notification.trade.peerOpenedDispute=Ваш контрагент открыл {0}. notification.trade.disputeClosed={0} закрыт. notification.walletUpdate.headline=Обновление торгового кошелька notification.walletUpdate.msg=Ваш торговый кошелёк содержит достаточно средств.\nСумма: {0} notification.takeOffer.walletUpdate.msg=Ваш торговый кошелёк уже содержит достаточно средств с прошлой попытки принять предложение.\nСумма: {0} notification.tradeCompleted.headline=Сделка завершена notification.tradeCompleted.msg=Теперь вы можете вывести свои средства на внешний кошелек Monero или оставить их на кошельке Haveno. #################################################################### # System Tray #################################################################### systemTray.show=Показать окно приложения systemTray.hide=Скрыть окно приложения systemTray.info=Информация о Haveno systemTray.exit=Выход systemTray.tooltip=Haveno: A decentralized monero exchange network #################################################################### # GUI Util #################################################################### guiUtil.accountExport.savedToPath=Торговые счета в:\n{0} guiUtil.accountExport.noAccountSetup=У вас нет торговых счетов для экспортирования. guiUtil.accountExport.selectPath=Выбрать путь к {0} # suppress inspection "TrailingSpacesInProperty" guiUtil.accountExport.tradingAccount=Торговый счёт с идентификатором {0}\n # suppress inspection "TrailingSpacesInProperty" guiUtil.accountImport.noImport=Торговый счёт с идентификатором {0} не был импортирован, так как он уже существует.\n guiUtil.accountExport.exportFailed=Экспорт в CSV не удался из-за ошибки.\nОшибка = {0} guiUtil.accountExport.selectExportPath=Выбрать директорию для экспорта guiUtil.accountImport.imported=Торговый счёт импортирован из:\n{0}\n\nИмпортированные счета:\n{1} guiUtil.accountImport.noAccountsFound=Экспортированные торговые счета не найдены в: {0}.\nИмя файла {1}. guiUtil.openWebBrowser.warning=Вы собираетесь открыть веб-страницу в веб-браузере.\nСделать это сейчас? \n\nЕсли вы не используете \«Tor\» в качестве веб-браузера по умолчанию, вы откроете веб-страницу в клирнете.\n\nURL: \«{0}\» guiUtil.openWebBrowser.doOpen=Открыть веб-страницу и не спрашивать снова guiUtil.openWebBrowser.copyUrl=Скопировать URL и отменить guiUtil.ofTradeAmount=от суммы сделки guiUtil.requiredMinimum=(required minimum) #################################################################### # Component specific #################################################################### list.currency.select=Выбрать валюту list.currency.showAll=Показать все list.currency.editList=Редактировать перечень валют table.placeholder.noItems=Нет доступных {0} table.placeholder.noData=Данные недоступны table.placeholder.processingData=Processing data... peerInfoIcon.tooltip.tradePeer=контрагента peerInfoIcon.tooltip.maker=мейкера peerInfoIcon.tooltip.trade.traded=Onion-адрес {0}: {1}\nВы уже торговали с данным контрагентом {2} раз (-a).\n{3} peerInfoIcon.tooltip.trade.notTraded=Onion-адрес {0}: {1}\nВы ещё не торговали с этим контрагентом.\n {2} peerInfoIcon.tooltip.age=Платёжный счёт создан {0} назад. peerInfoIcon.tooltip.unknownAge=Возраст платёжного счёта неизвестен. tooltip.openPopupForDetails=Открыть окно с подробностями tooltip.invalidTradeState.warning=This trade is in an invalid state. Open the details window for more information tooltip.openBlockchainForAddress=Открыть адрес {0} во внешнем обозревателе блоков tooltip.openBlockchainForTx=Открыть транзакцию {0} во внешнем обозревателе блоков confidence.unknown=Статус транзакции неизвестен confidence.seen=Замечена {0} пиром (-ами) / 0 подтверждений confidence.confirmed={0} подтверждение(ий) confidence.invalid=Недействительная транзакция peerInfo.title=Данные трейдера peerInfo.nrOfTrades=Количество завершенных сделок peerInfo.notTradedYet=Вы ещё не торговали с этим пользователем. peerInfo.setTag=Установить метку для данного участника peerInfo.age.noRisk=Возраст платёжного счёта peerInfo.age.chargeBackRisk=Time since signing peerInfo.unknownAge=Возраст неизвестен addressTextField.openWallet=Открыть Биткойн-кошелёк по умолчанию addressTextField.copyToClipboard=Скопировать адрес в буфер addressTextField.addressCopiedToClipboard=Адрес скопирован в буфер обмена addressTextField.openWallet.failed=Не удалось открыть Биткойн-кошелёк, использующийся по умолчанию. Возможно, у вас не установлено его приложение? peerInfoIcon.tooltip={0}\nМетка: {1} txIdTextField.copyIcon.tooltip=Скопировать идентификатор транзакции в буфер txIdTextField.blockExplorerIcon.tooltip=Open a blockchain explorer with this transaction ID txIdTextField.missingTx.warning.tooltip=Missing required transaction #################################################################### # Navigation #################################################################### navigation.account=\«Счёт\» navigation.account.walletSeed=\«Счёт/Мнемоническая фраза\» navigation.funds.availableForWithdrawal=\"Funds/Send funds\" navigation.portfolio.myOpenOffers=\«Сделки/Мои текущие предложения\» navigation.portfolio.pending=\«Сделки/Текущие сделки\» navigation.portfolio.closedTrades=\«Сделки/История\» navigation.funds.depositFunds=\«Средства/Получить средства\» navigation.settings.preferences=\«Настройки/Параметры\» # suppress inspection "UnusedProperty" navigation.funds.transactions=\«Средства/Транзакции\» navigation.support=\«Поддержка\» #################################################################### # Formatter #################################################################### formatter.formatVolumeLabel={0} сумма {1} formatter.makerTaker=Мейкер как {0} {1} / Тейкер как {2} {3} formatter.makerTaker.locked=Мейкер как {0} {1} / Тейкер как {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=Вы {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Вы создаете предложение {0} {1} formatter.youAreCreatingAnOffer.traditional.locked=Вы создаете предложение {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Вы создаете предложение {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.crypto.locked=Вы создаете предложение {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} как мейкер formatter.asTaker={0} {1} как тейкер #################################################################### # Domain specific #################################################################### # we use enum values here # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" XMR_MAINNET=XMR Mainnet # suppress inspection "UnusedProperty" XMR_LOCAL=XMR Testnet # suppress inspection "UnusedProperty" XMR_STAGENET=XMR Stagenet time.year=Год time.month=Месяц time.week=Неделя time.day=День time.hour=Час time.minute10=10 минут time.hours=ч. time.days=дн. time.1hour=1 час time.1day=1 день time.minute=минута time.second=секунда time.minutes=мин. time.seconds=сек. password.enterPassword=Введите пароль password.confirmPassword=Подтвердите пароль password.tooLong=Пароль не должен превышать 500 символов. password.deriveKey=Извлечь ключ из пароля password.walletDecrypted=Кошелёк успешно расшифрован, защита паролем удалена. password.wrongPw=Вы ввели неверный пароль.\n\nПопробуйте снова, обратив внимание на возможные ошибки ввода. password.walletEncrypted=Кошелёк успешно зашифрован, защита паролем включена. password.passwordsDoNotMatch=Введённые вами 2 пароля не совпадают. password.forgotPassword=Забыли пароль? password.backupReminder=Please note that when setting a wallet password all automatically created backups from the unencrypted wallet will be deleted.\n\nIt is highly recommended that you make a backup of the application directory and write down your seed words before setting a password! password.backupWasDone=I have already made a backup password.setPassword=Set Password (I already made a backup) password.makeBackup=Make Backup seed.seedWords=Мнемоническая фраза кошелька seed.enterSeedWords=Введите мнемоническую фразу кошелька seed.date=Дата создания кошелька seed.restore.title=Восстановить кошельки с помощью мнемонической фразы seed.restore=Восстановить кошельки seed.creationDate=Дата создания seed.warn.walletNotEmpty.msg=Your Monero wallet is not empty.\n\nYou must empty this wallet before attempting to restore an older one, as mixing wallets together can lead to invalidated backups.\n\nPlease finalize your trades, close all your open offers and go to the Funds section to withdraw your monero.\nIn case you cannot access your monero you can use the emergency tool to empty the wallet.\nTo open the emergency tool press \"Alt+e\" or \"Cmd/Ctrl+e\". seed.warn.walletNotEmpty.restore=Всё равно хочу восстановить seed.warn.walletNotEmpty.emptyWallet=Вывести все средства с моих кошельков seed.warn.notEncryptedAnymore=Ваши кошельки зашифрованы.\n\nПосле восстановления кошельки больше не будут зашифрованы, и вам потребуется установить новый пароль.\n\nПродолжить? seed.warn.walletDateEmpty=As you have not specified a wallet date, haveno will have to scan the blockchain from 2013.10.09 (the BIP39 epoch date).\n\nBIP39 wallets were first introduced in haveno on 2017.06.28 (release v0.5). So you could save time by using that date.\n\nIdeally you should specify the date your wallet seed was created.\n\n\nAre you sure you want to go ahead without specifying a wallet date? seed.restore.success=Кошельки успешно восстановлены с новой мнемонической фразой.\n\nЗакройте и перезапустите приложение. seed.restore.error=Произошла ошибка при восстановлении кошельков с помощью мнемонической фразы.{0} seed.restore.openOffers.warn=You have open offers which will be removed if you restore from seed words.\nAre you sure that you want to continue? #################################################################### # Payment methods #################################################################### payment.account=Счёт payment.account.no=Номер счёта payment.account.name=Название счёта payment.account.username=Username payment.account.phoneNr=Phone number payment.account.owner.fullname=Полное имя владельца счёта payment.account.fullName=Полное имя (имя, отчество, фамилия) payment.account.state=Штат/Провинция/Область payment.account.city=Город payment.bank.country=Страна банка payment.account.name.email=Полное имя / электронный адрес владельца счета payment.account.name.emailAndHolderId=Полное имя / электронный адрес / {0} владельца счета payment.bank.name=Название банка payment.select.account=Выбрать тип счёта payment.select.region=Выбрать регион payment.select.country=Выбрать страну payment.select.bank.country=Выбрать страну банка payment.foreign.currency=Вы уверены, что хотите выбрать валюту, отличную от национальной валюты по умолчанию? payment.restore.default=Нет, восстановить валюту по умолчанию payment.email=Электронный адрес payment.country=Страна payment.extras=Дополнительные требования payment.email.mobile=Эл. адрес или номер моб. тел. payment.crypto.address=Альткойн-адрес payment.crypto.tradeInstantCheckbox=Совершайте мгновенные сделки (в течение 1 часа) с этим альткойном payment.crypto.tradeInstant.popup=Для ускоренной торговли требуется, чтобы оба контрагента были в сети и могли завершить сделку менее чем за 1 час.\n\nЕсли у вас есть текущие предложения, но вы не можете находиться в сети, отключите их в разделе «Папка». payment.crypto=Альткойны payment.select.crypto=Select or search Crypto payment.secret=Секретный вопрос payment.answer=Ответ payment.wallet=Идентификатор кошелька payment.amazon.site=Buy giftcard at payment.ask=Ask in Trader Chat payment.uphold.accountId=Имя пользователя, эл. адрес или тел. номер payment.moneyBeam.accountId=Эл. адрес или тел. номер payment.popmoney.accountId=Эл. адрес или тел. номер payment.promptPay.promptPayId=Удостовер. личности / налог. номер или номер телефона payment.supportedCurrencies=Поддерживаемые валюты payment.supportedCurrenciesForReceiver=Currencies for receiving funds payment.limitations=Ограничения payment.salt=Модификатор («соль») для подтверждения возраста счёта payment.error.noHexSalt=The salt needs to be in HEX format.\nIt is only recommended to edit the salt field if you want to transfer the salt from an old account to keep your account age. The account age is verified by using the account salt and the identifying account data (e.g. IBAN). payment.accept.euro=Принять сделки из этих стран, использующих EUR payment.accept.nonEuro=Принять сделки из этих стран, не использующих EUR payment.accepted.countries=Одобренные страны payment.accepted.banks=Одобренные банки (идент.) payment.mobile=Номер моб. тел. payment.postal.address=Почтовый адрес payment.national.account.id.AR=Номер CBU shared.accountSigningState=Account signing status #new payment.crypto.address.dyn={0}-адрес payment.crypto.receiver.address=Альткойн-адрес получателя payment.accountNr=Номер счёта payment.emailOrMobile=Эл. адрес или номер моб. тел. payment.useCustomAccountName=Использовать своё название счёта payment.maxPeriod=Макс. допустимый срок сделки payment.maxPeriodAndLimit=Max. trade duration: {0} / Max. buy: {1} / Max. sell: {2} / Account age: {3} payment.maxPeriodAndLimitCrypto=Макс. срок сделки: {0} / Макс. торговый лимит: {1} payment.currencyWithSymbol=Валюта: {0} payment.nameOfAcceptedBank=Название одобренного банка payment.addAcceptedBank=Добавить одобренный банк payment.clearAcceptedBanks=Очистить одобренные банки payment.bank.nameOptional=Название банка (необязательно) payment.bankCode=Код банка payment.bankId=Идентификатор банка (BIC/SWIFT): payment.bankIdOptional=Идентификатор банка (BIC/SWIFT) (необяз.) payment.branchNr=Номер отделения payment.branchNrOptional=Номер отделения (необяз.) payment.accountNrLabel=Номер счёта (IBAN) payment.accountType=Тип счёта payment.checking=Текущий payment.savings=Сберегательный payment.personalId=Личный идентификатор payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle's somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver's full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account's age and signing status are preserved. payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=Используя HalCash, покупатель XMR обязуется отправить продавцу XMR код HalCash через СМС с мобильного телефона.\n\nУбедитесь, что не вы не превысили максимальную сумму, которую ваш банк позволяет отправить с HalCash. Минимальная сумма на вывод средств составляет 10 EUR, а и максимальная — 600 EUR. При повторном выводе средств лимит составляет 3000 EUR на получателя в день и 6000 EUR на получателя в месяц. Просьба сверить эти лимиты с вашим банком и убедиться, что лимиты банка соответствуют лимитам, указанным здесь.\n\nВыводимая сумма должна быть кратна 10 EUR, так как другие суммы снять из банкомата невозможно. Приложение само отрегулирует сумму XMR, чтобы она соответствовала сумме в EUR, во время создания или принятия предложения. Вы не сможете использовать текущий рыночный курс, так как сумма в EUR будет меняться с изменением курса.\n\nВ случае спора покупателю XMR необходимо предоставить доказательство отправки EUR. # suppress inspection "UnusedMessageFormatParameter" payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/overview/account_limits]. # suppress inspection "UnusedProperty" payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/overview/account_limits]. payment.cashDeposit.info=Убедитесь, что ваш банк позволяет отправлять денежные переводы на счета других лиц. Например, Bank of America и Wells Fargo больше не разрешают такие переводы. payment.revolut.info=Revolut requires the 'Username' as account ID not the phone number or email as it was the case in the past. payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a 'Username'.\nPlease enter your Revolut 'Username' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account payment.cashapp.info=Обратите внимание, что Cash App имеет более высокий риск возврата платежей, чем большинство банковских переводов. payment.venmo.info=Обратите внимание, что Venmo имеет более высокий риск возврата платежей, чем большинство банковских переводов. payment.paypal.info=Обратите внимание, что PayPal имеет более высокий риск возврата платежей, чем большинство банковских переводов. payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account payment.payByMail.contact=Контактная информация payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.f2f.contact=Контактная информация payment.f2f.contact.prompt=How would you like to be contacted by the trading peer? (email address, phone number,...) payment.f2f.city=Город для личной встречи payment.f2f.city.prompt=Город будет указан в предложении payment.shared.optionalExtra=Дополнительная необязательная информация payment.shared.extraInfo=Дополнительная информация payment.shared.extraInfo.offer=Дополнительная информация о предложении payment.shared.extraInfo.prompt.paymentAccount=Определите любые специальные термины, условия или детали, которые вы хотите, чтобы отображались с вашими предложениями для этого платежного аккаунта (пользователи увидят эту информацию перед принятием предложений). payment.shared.extraInfo.prompt.offer=Определите любые специальные условия, требования или детали, которые вы хотели бы указать в своем предложении. payment.shared.extraInfo.noDeposit=Контактные данные и условия предложения payment.f2f.info='Face to Face' trades have different rules and come with different risks than online transactions.\n\nThe main differences are:\n● The trading peers need to exchange information about the meeting location and time by using their provided contact details.\n● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n● In case of a dispute the mediator or arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to an agreement.\n\nTo be sure you fully understand the differences with 'Face to Face' trades please read the instructions and recommendations at: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=Открыть веб-страницу payment.f2f.offerbook.tooltip.countryAndCity=Country and city: {0} / {1} payment.shared.extraInfo.tooltip=Дополнительная информация: {0} payment.japan.bank=Банк payment.japan.branch=Branch payment.japan.account=Счёт payment.japan.recipient=Имя payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=Для вашей защиты мы настоятельно не рекомендуем использовать PIN-коды Paysafecard для платежей.\n\n\ Транзакции, выполненные с помощью PIN-кодов, не могут быть независимо подтверждены для разрешения споров. В случае возникновения проблемы возврат средств может быть невозможен.\n\n\ Чтобы обеспечить безопасность транзакций с возможностью разрешения споров, всегда используйте методы оплаты, предоставляющие проверяемые записи. # We use constants from the code so we do not use our normal naming convention # dynamic values are not recognized by IntelliJ # Only translate general terms NATIONAL_BANK=Перевод национальным банком SAME_BANK=Перевод в тот же банк SPECIFIC_BANKS=Перевод через определённый банк US_POSTAL_MONEY_ORDER=Почтовый денежный перевод США CASH_DEPOSIT=Внесение наличных PAY_BY_MAIL=Pay By Mail MONEY_GRAM=MoneyGram WESTERN_UNION=Western Union F2F=Личная встреча JAPAN_BANK=Japan Bank Furikomi AUSTRALIA_PAYID=Australian PayID # suppress inspection "UnusedProperty" NATIONAL_BANK_SHORT=Национальные банки # suppress inspection "UnusedProperty" SAME_BANK_SHORT=Тот же банк # suppress inspection "UnusedProperty" SPECIFIC_BANKS_SHORT=Определенные банки # suppress inspection "UnusedProperty" US_POSTAL_MONEY_ORDER_SHORT=Денежный перевод США # suppress inspection "UnusedProperty" CASH_DEPOSIT_SHORT=Внесение наличных # suppress inspection "UnusedProperty" PAY_BY_MAIL_SHORT=PayByMail # suppress inspection "UnusedProperty" MONEY_GRAM_SHORT=MoneyGram # suppress inspection "UnusedProperty" WESTERN_UNION_SHORT=Western Union # suppress inspection "UnusedProperty" F2F_SHORT=Личная встреча # suppress inspection "UnusedProperty" JAPAN_BANK_SHORT=Japan Furikomi # suppress inspection "UnusedProperty" AUSTRALIA_PAYID_SHORT=PayID # Do not translate brand names # suppress inspection "UnusedProperty" UPHOLD=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY=Popmoney # suppress inspection "UnusedProperty" REVOLUT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY=WeChat Pay # suppress inspection "UnusedProperty" SEPA=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT=SEPA Instant Payments # suppress inspection "UnusedProperty" FASTER_PAYMENTS=Faster Payments # suppress inspection "UnusedProperty" SWISH=Swish # suppress inspection "UnusedProperty" ZELLE=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS=Альткойны # suppress inspection "UnusedProperty" PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT=Альткойны (мгновенно) # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY=OKPay # suppress inspection "UnusedProperty" CASH_APP=Cash App # suppress inspection "UnusedProperty" VENMO=Venmo # suppress inspection "UnusedProperty" UPHOLD_SHORT=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM_SHORT=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY_SHORT=Popmoney # suppress inspection "UnusedProperty" REVOLUT_SHORT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY_SHORT=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY_SHORT=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY_SHORT=WeChat Pay # suppress inspection "UnusedProperty" SEPA_SHORT=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT_SHORT=SEPA Instant # suppress inspection "UnusedProperty" FASTER_PAYMENTS_SHORT=Faster Payments # suppress inspection "UnusedProperty" SWISH_SHORT=Swish # suppress inspection "UnusedProperty" ZELLE_SHORT=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY_SHORT=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER_SHORT=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH_SHORT=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS_SHORT=Альткойны # suppress inspection "UnusedProperty" PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT_SHORT=Альткойны (мгновенно) # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY_SHORT=OKPay # suppress inspection "UnusedProperty" CASH_APP_SHORT=Cash App # suppress inspection "UnusedProperty" VENMO_SHORT=Venmo #################################################################### # Validation #################################################################### validation.empty=Нужно ввести данные. validation.NaN=Введённое число недопустимо. validation.notAnInteger=Введённое число не является целым. validation.zero=Введённое значение не может быть равно 0. validation.negative=Отрицательное значение недопустимо. validation.traditional.tooSmall=Ввод значения меньше минимально возможного не допускается. validation.traditional.tooLarge=Ввод значения больше максимально возможного не допускается. validation.xmr.fraction=Input will result in a monero value of less than 1 satoshi validation.xmr.tooLarge=Значение не может превышать {0}. validation.xmr.tooSmall=Значение не может быть меньше {0}. validation.passwordTooShort=The password you entered is too short. It needs to have a min. of 8 characters. validation.passwordTooLong=Введенный пароль слишком длинный. Его длина не должна превышать 50 символов. validation.sortCodeNumber={0} должен состоять из {1} цифр. validation.sortCodeChars={0} должен состоять из {1} символов. validation.bankIdNumber={0} должен состоять из {1} цифр. validation.accountNr=Номер счёта должен состоять из {0} цифр. validation.accountNrChars=Номер счёта должен состоять из {0} символов. validation.xmr.invalidAddress=Неправильный адрес. Проверьте формат адреса. validation.integerOnly=Введите только целые числа. validation.inputError=Введённое значение вызвало ошибку:\n{0} validation.xmr.exceedsMaxTradeLimit=Ваш торговый лимит составляет {0}. validation.nationalAccountId={0} должен состоять из {1} цифр. #new validation.invalidInput=Недействительное значение: {0} validation.accountNrFormat=Допустимый формат номера счёта: {0} # suppress inspection "UnusedProperty" validation.crypto.wrongStructure=Сбой проверки адреса. Адрес не соответствует структуре адреса {0}. # suppress inspection "UnusedProperty" validation.crypto.ltz.zAddressesNotSupported=LTZ address must start with L. Addresses starting with z are not supported. # suppress inspection "UnusedProperty" validation.crypto.zAddressesNotSupported=ZEC addresses must start with t. Addresses starting with z are not supported. # suppress inspection "UnusedProperty" validation.crypto.invalidAddress=Адрес не является действительным адресом {0}! {1} # suppress inspection "UnusedProperty" validation.crypto.liquidBitcoin.invalidAddress=Native segwit addresses (those starting with 'lq') are not supported. validation.bic.invalidLength=Input length must be 8 or 11 validation.bic.letters=Код банка и страны должен быть буквенным validation.bic.invalidLocationCode=BIC содержит недействительный код местности validation.bic.invalidBranchCode=BIC содержит недействительный код отделения validation.bic.sepaRevolutBic=Счета SEPA в Revolut не поддерживаются. validation.btc.invalidFormat=Invalid format for a Bitcoin address. validation.email.invalidAddress=Недействительный адрес validation.iban.invalidCountryCode=Код страны недействителен validation.iban.checkSumNotNumeric=Контрольная сумма должна иметь числовой формат validation.iban.nonNumericChars=Введен не буквенно-цифровой знак validation.iban.checkSumInvalid=Контрольная сумма IBAN недействительна validation.iban.invalidLength=Number must have a length of 15 to 34 chars. validation.interacETransfer.invalidAreaCode=Код не канадского региона validation.interacETransfer.invalidPhone=Please enter a valid 11 digit phone number (ex: 1-123-456-7890) or an email address validation.interacETransfer.invalidQuestion=Должен содержать только буквы, цифры, пробелы и/или символы ' _ , . ? - validation.interacETransfer.invalidAnswer=Должен состоять из одного слова, содержащего только буквы, цифры и/или символ - validation.inputTooLarge=Значение не должно превышать {0} validation.inputTooSmall=Значение должно быть более {0} validation.inputToBeAtLeast=Значение должно быть не менее {0} validation.amountBelowDust=An amount below the dust limit of {0} satoshi is not allowed. validation.length=Длина должна составлять от {0} до {1} validation.fixedLength=Length must be {0} validation.pattern=Формат значения: {0} validation.noHexString=Значение не соответствует шестнадцатеричному формату. validation.advancedCash.invalidFormat=Требуется действительный электронный адрес или идентификатор кошелька в формате: x000000000000 validation.invalidUrl=Недопустимый URL-адрес validation.mustBeDifferent=Your input must be different from the current value validation.cannotBeChanged=Неизменяемый параметр validation.numberFormatException=Исключение числового формата {0} validation.mustNotBeNegative=Значение не может быть отрицательным validation.phone.missingCountryCode=Need two letter country code to validate phone number validation.phone.invalidCharacters=Phone number {0} contains invalid characters validation.phone.insufficientDigits=There are not enough digits in {0} to be a valid phone number validation.phone.tooManyDigits=There are too many digits in {0} to be a valid phone number validation.phone.invalidDialingCode=Country dialing code for number {0} is invalid for country {1}. The correct dialing code is {2}. validation.invalidAddressList=Must be comma separated list of valid addresses ================================================ FILE: core/src/main/resources/i18n/displayStrings_th.properties ================================================ # Keep display strings organized by domain # Naming convention: We use camelCase and dot separated name spaces. # Use as many sub spaces as required to make the structure clear, but as little as possible. # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break # in the display but only in the editor. # Please use in all language files the exact same order of the entries, that way a comparison is easier. # Please try to keep the length of the translated string similar to English. If it is longer it might break layout or # get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. #################################################################### # Shared #################################################################### shared.readMore=อ่านเพิ่มเติม shared.openHelp=การช่วยเหลือ shared.warning=คำเตือน shared.close=ปิด shared.cancel=ยกเลิก shared.ok=ตกลง shared.yes=ใช่ shared.no=ไม่ใช่ shared.iUnderstand=ฉันเข้าใจ shared.na=ไม่พร้อมใช้งาน shared.shutDown=ปิดใช้งาน shared.reportBug=Report bug on GitHub shared.buyMonero=ซื้อ monero (บิตคอยน์) shared.sellMonero=ขาย monero (บิตคอยน์) shared.buyCurrency=ซื้อ {0} shared.sellCurrency=ขาย {0} shared.buyCurrency.locked=ซื้อ {0} 🔒 shared.sellCurrency.locked=ขาย {0} 🔒 shared.buyingXMRWith=การซื้อ XMR กับ {0} shared.sellingXMRFor=การขาย XMR แก่ {0} shared.buyingCurrency=การซื้อ {0} (การขาย XMR) shared.sellingCurrency=การขาย {0} (การซื้อ XMR) shared.buy=ซื้อ shared.sell=ขาย shared.buying=การซื้อ shared.selling=การขาย shared.P2P=P2P shared.oneOffer=เสนอ shared.multipleOffers=ข้อเสนอ shared.Offer=การเสนอ shared.offerVolumeCode={0} Offer Volume shared.openOffers=เปิดข้อเสนอ shared.trade=ซื้อขาย shared.trades=การซื้อขายแลกเปลี่ยน shared.openTrades=เปิดตลาดการซื้อขาย shared.dateTime=วันที่/เวลา shared.price=ราคา shared.priceWithCur=ราคาใน {0} shared.priceInCurForCur=ราคาใน {0} แก่ 1 {1} shared.fixedPriceInCurForCur=ราคาคงที่ ใน {0} แก่ 1 {1} shared.amount=จำนวน shared.txFee=Transaction Fee shared.tradeFee=Trade Fee shared.buyerSecurityDeposit=Buyer Deposit shared.sellerSecurityDeposit=Seller Deposit shared.amountWithCur=จำนวนใน {0} shared.volumeWithCur=ปริมาณการซื้อขายใน {0} shared.currency=เงินตรา shared.market=ตลาด shared.deviation=Deviation shared.paymentMethod=วิธีการชำระเงิน shared.tradeCurrency=สกุลเงินตราการค้า shared.offerType=ประเภทข้อเสนอ shared.details=รายละเอียด shared.address=ที่อยู่ shared.balanceWithCur=ยอดคงเหลือใน {0} shared.utxo=Unspent transaction output shared.txId=เลขอ้างอิงการทำธุรกรรม shared.confirmations=การยืนยัน shared.revert=ย้อนกลับสู่การทำธุรกรรม shared.select=เลือก shared.usage=การใช้งาน shared.state=สถานะ shared.tradeId=ID ทางการค้า shared.offerId=ID ข้อเสนอ shared.bankName=ชื่อธนาคาร shared.acceptedBanks=ธนาคารที่ได้รับการยอมรับ shared.amountMinMax=ยอดจำนวน (ต่ำสุด-สูงสุด) shared.amountHelp=หากข้อเสนอนั้นถูกจัดอยู่ในระดับเซ็ทขั้นต่ำและสูงสุด คุณสามารถซื้อขายได้ทุกช่วงระดับของจำนวนที่มีอยู่ shared.remove=ลบออก shared.goTo=ไปที่ {0} shared.XMRMinMax=XMR (ต่ำสุด-สูงสุด) shared.removeOffer=ลบข้อเสนอ shared.dontRemoveOffer=ห้ามลบข้อเสนอ shared.editOffer=แก้ไขข้อเสนอ shared.openLargeQRWindow=Open large QR code window shared.tradingAccount=บัญชีการซื้อขาย shared.faq=Visit FAQ page shared.yesCancel=ใช่ ยกเลิก shared.nextStep=ขั้นถัดไป shared.selectTradingAccount=เลือกบัญชีการซื้อขาย shared.fundFromSavingsWalletButton=ใช้เงินจากกระเป๋าเงิน Haveno shared.fundFromExternalWalletButton=เริ่มทำการระดมเงินทุนหาแหล่งเงินจากกระเป๋าสตางค์ภายนอกของคุณ shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? shared.belowInPercent=ต่ำกว่า % จากราคาตลาด shared.aboveInPercent=สูงกว่า % จากราคาตาด shared.enterPercentageValue=เข้าสู่ % ตามมูลค่า shared.OR=หรือ shared.notEnoughFunds=You don't have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. shared.waitingForFunds=กำลังรอเงิน ... shared.TheXMRBuyer=ผู้ซื้อ XMR shared.You=คุณ shared.sendingConfirmation=กำลังส่งการยืนยัน ... shared.sendingConfirmationAgain=โปรดยืนยันการส่งอีกครั้ง shared.exportCSV=Export to CSV shared.exportJSON=Export to JSON shared.summary=Show summary shared.noDateAvailable=ไม่มีวันที่ให้แสดง shared.noDetailsAvailable=ไม่มีรายละเอียด shared.notUsedYet=ยังไม่ได้ใช้งาน shared.date=วันที่ shared.sendFundsDetailsWithFee=กำลังส่ง: {0}\n\nไปยังที่อยู่ผู้รับ: {1}\n\nค่าธรรมเนียมเหมืองเพิ่มเติม: {2}\n\nคุณแน่ใจหรือไม่ว่าต้องการส่งจำนวนนี้? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=คัดลอกไปที่คลิปบอร์ด shared.language=ภาษา shared.country=ประเทศ shared.applyAndShutDown=ใช้และปิดใช้งาน shared.selectPaymentMethod=เลือกวิธีการชำระเงิน shared.accountNameAlreadyUsed=That account name is already used for another saved account.\nPlease choose another name. shared.askConfirmDeleteAccount=คุณต้องการลบบัญชีที่เลือกหรือไม่ shared.cannotDeleteAccount=You cannot delete that account because it is being used in an open offer (or in an open trade). shared.noAccountsSetupYet=บัญชียังไม่ได้มีการตั้งค่าใดๆ shared.manageAccounts=จัดการบัญชี shared.addNewAccount=เพิ่มบัญชีใหม่ shared.ExportAccounts=บัญชีส่งออก shared.importAccounts=บัญชีนำเข้า shared.createNewAccount=สร้างบัญชีใหม่ shared.createNewAccountDescription=รายละเอียดบัญชีของคุณถูกจัดเก็บไว้ในอุปกรณ์ของคุณและจะแบ่งปันเฉพาะกับคู่ค้าของคุณและผู้ตัดสินหากมีการเปิดข้อพิพาท shared.saveNewAccount=บันทึกบัญชีใหม่ shared.selectedAccount=บัญชีที่เลือก shared.deleteAccount=ลบบัญชี shared.errorMessageInline=\nเกิดข้อผิดพลาด: {0} shared.errorMessage=เกิดข้อผิดพลาด shared.information=ข้อมูล shared.name=ชื่อ shared.id=ID shared.dashboard=Dashboard (หน้าแสดงผลรวม) shared.accept=ยอมรับ shared.balance=คงเหลือ shared.save=บันทึก shared.onionAddress=ที่อยู่ onion shared.supportTicket=ศูนย์ช่วยเหลือ shared.dispute=ข้อพิพาท shared.mediationCase=mediation case shared.seller=ผู้ขาย shared.buyer=ผู้ซื้อ shared.allEuroCountries=ทุกประเทศในทวีปยูโร shared.acceptedTakerCountries=ประเทศที่รับการยอมรับ shared.tradePrice=ราคาการซื้อขาย shared.tradeAmount=ยอดจำนวนการซื้อขาย shared.tradeVolume=ปริมาณการซื้อขาย shared.invalidKey=คีย์ที่คุณป้อนไม่ถูกต้อง shared.enterPrivKey=ป้อนคีย์ส่วนตัวเพื่อปลดล็อก shared.payoutTxId=ID ธุรกรรมการชำระเงิน shared.contractAsJson=สัญญาในรูปแบบ JSON shared.viewContractAsJson=ดูสัญญาในรูปแบบ JSON: shared.contract.title=สัญญาการซื้อขายด้วยรหัส ID: {0} shared.paymentDetails=XMR {0} รายละเอียดการชำระเงิน shared.securityDeposit=เงินประกัน shared.yourSecurityDeposit=เงินประกันของคุณ shared.contract=สัญญา shared.messageArrived=มีข้อความเข้าแล้ว shared.messageStoredInMailbox=ข้อความถูกเก็บไว้ในกล่องจดหมาย shared.messageSendingFailed=การส่งข้อความล้มเหลว เกิดข้อผิดพลาด: {0} shared.unlock=ปลดล็อค shared.toReceive=รับ shared.toSpend=จ่าย shared.xmrAmount=XMR ยอดจำนวน shared.yourLanguage=ภาษาของคุณ shared.addLanguage=เพิ่มภาษา shared.total=ยอดทั้งหมด shared.totalsNeeded=เงินที่จำเป็น shared.tradeWalletAddress=ที่อยู่ Trade wallet shared.tradeWalletBalance=ยอดคงเหลือของ Trade wallet shared.reserveExactAmount=สำรองเฉพาะเงินที่จำเป็น ต้องใช้ค่าธรรมเนียมการขุดและเวลาประมาณ 20 นาทีก่อนที่ข้อเสนอของคุณจะเผยแพร่ shared.makerTxFee=ผู้ทำ: {0} shared.takerTxFee=ผู้รับ: {0} shared.iConfirm=ฉันยืนยัน shared.openURL=เปิด {0} shared.fiat=คำสั่ง shared.crypto=คริปโต shared.preciousMetals=โลหะมีค่า shared.all=ทั้งหมด shared.edit=แก้ไข shared.advancedOptions=ทางเลือกขั้นสูง shared.interval=ระยะห่าง shared.actions=การปฏิบัติการ shared.buyerUpperCase=ผู้ซื้อ shared.sellerUpperCase=ผู้ขาย shared.new=NEW shared.learnMore=Learn more shared.dismiss=Dismiss shared.selectedArbitrator=ผู้ไกล่เกลี่ยที่ได้รับการแต่งตั้ง shared.selectedMediator=Selected mediator shared.selectedRefundAgent=ผู้ไกล่เกลี่ยที่ได้รับการแต่งตั้ง shared.mediator=ผู้ไกล่เกลี่ย shared.arbitrator=ผู้ไกล่เกลี่ย shared.refundAgent=ผู้ไกล่เกลี่ย shared.refundAgentForSupportStaff=Refund agent shared.delayedPayoutTxId=Delayed payout transaction ID shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later. shared.numItemsLabel=Number of entries: {0} shared.filter=ตัวกรอง shared.enabled=Enabled #################################################################### # UI views #################################################################### #################################################################### # MainView #################################################################### mainView.menu.market=ตลาด mainView.menu.buyXmr=ซื้อ XMR mainView.menu.sellXmr=ขาย XMR mainView.menu.portfolio=แฟ้มผลงาน mainView.menu.funds=เงิน mainView.menu.support=สนับสนุน mainView.menu.settings=ตั้งค่า mainView.menu.account=บัญชี mainView.marketPriceWithProvider.label=ราคาตลาดโดย {0} mainView.marketPrice.havenoInternalPrice=ราคาของการซื้อขาย Haveno ล่าสุด mainView.marketPrice.tooltip.havenoInternalPrice=ไม่มีราคาตลาดจากผู้ให้บริการด้านราคาภายนอก\nราคาที่แสดงเป็นราคาล่าสุดของ Haveno สำหรับสกุลเงินนั้น mainView.marketPrice.tooltip=ราคาตลาดจัดทำโดย {0} {1} \nอัปเดตล่าสุด: {2} \nnode URL ของผู้ให้บริการ: {3} mainView.balance.available=ยอดคงเหลือที่พร้อมใช้งาน mainView.balance.reserved=ข้อเสนอได้รับการจองแล้ว mainView.balance.pending=ล็อคในการซื้อขาย mainView.balance.reserved.short=จองแล้ว mainView.balance.pending.short=ถูกล็อคไว้ mainView.footer.usingTor=(via Tor) mainView.footer.localhostMoneroNode=(แม่ข่ายเฉพาะที่) mainView.footer.clearnet=(via clearnet) mainView.footer.xmrInfo={0} {1} mainView.footer.xmrFeeRate=/ Fee rate: {0} sat/vB mainView.footer.xmrInfo.initializing=เชื่อมต่อกับเครือข่าย Haveno mainView.footer.xmrInfo.synchronizingWith=กำลังซิงโครไนซ์กับ {0} ที่บล็อก: {1} / {2} mainView.footer.xmrInfo.connectedTo=เชื่อมต่อไปยัง {0} ที่บล็อก {1} mainView.footer.xmrInfo.synchronizingWalletWith=กำลังซิงโครไนซ์กระเป๋าสตางค์กับ {0} ที่บล็อก: {1} / {2} mainView.footer.xmrInfo.syncedWith=ซิงค์กับ {0} ที่บล็อก {1} เสร็จสมบูรณ์ mainView.footer.xmrInfo.connectingTo=Connecting to mainView.footer.xmrInfo.connectionFailed=Connection failed to mainView.footer.xmrPeers=Monero network peers: {0} mainView.footer.p2pPeers=Haveno network peers: {0} mainView.bootstrapState.connectionToTorNetwork=(1/4) เชื่อมต่อไปยัง Tor network... mainView.bootstrapState.torNodeCreated=(2/4) Tor node ถูกสร้างแล้ว mainView.bootstrapState.hiddenServicePublished=(3/4) บริการที่ซ่อนอยู่ถูกเผยแพร่ mainView.bootstrapState.initialDataReceived=(4/4) ได้รับข้อมูลเบื้องต้นเรียบร้อย mainView.bootstrapWarning.noSeedNodesAvailable=ไม่มีแหล่งข้อมูลในโหนดเครือข่ายให้ใช้งาน mainView.bootstrapWarning.noNodesAvailable=ไม่มีแหล่งข้อมูลในโหนดเครือข่ายและpeers(ระบบเพียร์)ให้ใช้งาน mainView.bootstrapWarning.bootstrappingToP2PFailed=Bootstrapping to Haveno network failed mainView.p2pNetworkWarnMsg.noNodesAvailable=ไม่มีแหล่งข้อมูลในโหนดเครือข่ายและ peers (ระบบเพียร์) พร้อมให้บริการสำหรับการขอข้อมูล\nโปรดตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณหรือลองรีสตาร์ทแอพพลิเคชัน mainView.p2pNetworkWarnMsg.connectionToP2PFailed=Connecting to the Haveno network failed (reported error: {0}).\nPlease check your internet connection or try to restart the application. mainView.walletServiceErrorMsg.timeout=การเชื่อมต่อกับเครือข่าย Monero ล้มเหลวเนื่องจากหมดเวลา mainView.walletServiceErrorMsg.connectionError=การเชื่อมต่อกับเครือข่าย Monero ล้มเหลวเนื่องจากข้อผิดพลาด: {0} mainView.walletServiceErrorMsg.rejectedTxException=A transaction was rejected from the network.\n\n{0} mainView.networkWarning.allConnectionsLost=คุณสูญเสียการเชื่อมต่อกับ {0} เครือข่าย peers\nบางทีคุณอาจขาดการเชื่อมต่ออินเทอร์เน็ตหรืออาจเป็นเพราะคอมพิวเตอร์ของคุณอยู่ในโหมดสแตนด์บาย mainView.networkWarning.localhostMoneroLost=คุณสูญเสียการเชื่อมต่อไปยังโหนดเครือข่าย Monero localhost (แม่ข่ายเฉพาะที่)\nโปรดรีสตาร์ทแอ็พพลิเคชัน Haveno เพื่อเชื่อมต่อโหนด Monero อื่นหรือรีสตาร์ทโหนด Monero localhost mainView.version.update=(การอัพเดตพร้อมใช้งาน) #################################################################### # MarketView #################################################################### market.tabs.offerBook=การจองข้อเสนอ market.tabs.spreadCurrency=Offers by Currency market.tabs.spreadPayment=Offers by Payment Method market.tabs.trades=การซื้อขาย # OfferBookChartView market.offerBook.buyCrypto=ซื้อ {0} (ขาย {1}) market.offerBook.sellCrypto=ขาย {0} (ซื้อ {1}) market.offerBook.buyWithTraditional=ซื้อ {0} market.offerBook.sellWithTraditional=ขาย {0} market.offerBook.sellOffersHeaderLabel=ขาย {0} ไปยัง market.offerBook.buyOffersHeaderLabel=ซื้อ {0} จาก market.offerBook.buy=ฉันต้องการจะซื้อ monero market.offerBook.sell=ฉันต้องการจะขาย monero # SpreadView market.spread.numberOfOffersColumn=ข้อเสนอทั้งหมด ({0}) market.spread.numberOfBuyOffersColumn=ซื้อ XMR ({0}) market.spread.numberOfSellOffersColumn=ขาย XMR ({0}) market.spread.totalAmountColumn=ยอด XMR ทั้งหมด ({0}) market.spread.spreadColumn=กระจาย market.spread.expanded=Expanded view # TradesChartsView market.trades.nrOfTrades=การซื้อขาย: {0} market.trades.tooltip.volumeBar=Volume: {0} / {1}\nNo. of trades: {2}\nDate: {3} market.trades.tooltip.candle.open=เปิด: market.trades.tooltip.candle.close=ปิด: market.trades.tooltip.candle.high=สูง: market.trades.tooltip.candle.low=ต่ำ: market.trades.tooltip.candle.average=เฉลี่ย: market.trades.tooltip.candle.median=Median: market.trades.tooltip.candle.date=วันที่: market.trades.showVolumeInUSD=Show volume in USD #################################################################### # OfferView #################################################################### offerbook.createOffer=สร้างข้อเสนอ offerbook.takeOffer=รับข้อเสนอ offerbook.takeOfferToBuy=Take offer to buy {0} offerbook.takeOfferToSell=Take offer to sell {0} offerbook.takeOffer.enterChallenge=กรอกพาสเฟรสข้อเสนอ offerbook.trader=Trader (เทรดเดอร์) offerbook.offerersBankId=รหัสธนาคารของผู้สร้าง (BIC / SWIFT): {0} offerbook.offerersBankName=ชื่อธนาคารของผู้สร้าง: {0} offerbook.offerersBankSeat=ตำแหน่งประเทศของธนาคารของผู้สร้าง: {0} offerbook.offerersAcceptedBankSeatsEuro=ยอมรับตำแหน่งประเทศของธนาคาร (ผู้รับ): ทุกประเทศในทวีปยูโร offerbook.offerersAcceptedBankSeats=ยอมรับตำแหน่งประเทศของธนาคาร (ผู้รับ):\n {0} offerbook.availableOffers=ข้อเสนอที่พร้อมใช้งาน offerbook.filterByCurrency=กรองตามสกุลเงิน offerbook.filterByPaymentMethod=ตัวกรองตามวิธีการชำระเงิน offerbook.matchingOffers=ข้อเสนอที่ตรงกับบัญชีของฉัน offerbook.filterNoDeposit=ไม่มีเงินมัดจำ offerbook.noDepositOffers=ข้อเสนอที่ไม่มีเงินมัดจำ (ต้องการรหัสผ่าน) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info=This account was verified and {0} offerbook.timeSinceSigning.info.arbitrator=signed by an arbitrator and can sign peer accounts offerbook.timeSinceSigning.info.peer=signed by a peer, waiting %d days for limits to be lifted offerbook.timeSinceSigning.info.peerLimitLifted=signed by a peer and limits were lifted offerbook.timeSinceSigning.info.signer=signed by peer and can sign peer accounts (limits lifted) offerbook.timeSinceSigning.info.banned=account was banned offerbook.timeSinceSigning.daysSinceSigning={0} วัน offerbook.timeSinceSigning.daysSinceSigning.long={0} since signing offerbook.xmrAutoConf=Is auto-confirm enabled offerbook.buyXmrWith=ซื้อ XMR ด้วย: offerbook.sellXmrFor=ขาย XMR สำหรับ: offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers' payment accounts. offerbook.timeSinceSigning.notSigned=Not signed yet offerbook.timeSinceSigning.notSigned.ageDays={0} วัน offerbook.timeSinceSigning.notSigned.noNeed=ไม่พร้อมใช้งาน shared.notSigned=This account has not been signed yet and was created {0} days ago shared.notSigned.noNeed=This account type does not require signing shared.notSigned.noNeedDays=This account type does not require signing and was created {0} days ago shared.notSigned.noNeedAlts=Crypto accounts do not feature signing or aging offerbook.nrOffers=No. ของข้อเสนอ: {0} offerbook.volume={0} (ต่ำสุด - สูงสุด) offerbook.deposit=Deposit XMR (%) offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed. offerbook.createNewOffer=สร้างข้อเสนอให้กับ {0} {1} offerbook.createOfferToBuy=Create new offer to buy {0} offerbook.createOfferToSell=Create new offer to sell {0} offerbook.createOfferToBuy.withTraditional=Create new offer to buy {0} with {1} offerbook.createOfferToSell.forTraditional=Create new offer to sell {0} for {1} offerbook.createOfferToBuy.withCrypto=Create new offer to sell {0} (buy {1}) offerbook.createOfferToSell.forCrypto=Create new offer to buy {0} (sell {1}) offerbook.takeOfferButton.tooltip=รับข้อเสนอเพื่อ {0} offerbook.yesCreateOffer=ใช่ สร้างข้อเสนอ offerbook.setupNewAccount=ตั้งค่าบัญชีการซื้อขายใหม่ offerbook.removeOffer.success=นำข้อเสนอออกเรียบร้อยแล้ว offerbook.removeOffer.failed=เกิดข้อผิดพลาดในการลบข้อเสนอ:\n{0} offerbook.deactivateOffer.failed=เกิดข้อผิดพลาดในการยกเลิกข้อเสนอ: \n{0} offerbook.activateOffer.failed=การเผยแพร่ข้อเสนอล้มเหลว: \n{0} offerbook.withdrawFundsHint=คุณสามารถถอนเงินที่คุณชำระมาได้จาก {0} หน้าจอ offerbook.warning.noTradingAccountForCurrency.headline=No payment account for selected currency offerbook.warning.noTradingAccountForCurrency.msg=You don't have a payment account set up for the selected currency.\n\nWould you like to create an offer for another currency instead? offerbook.warning.noMatchingAccount.headline=No matching payment account. offerbook.warning.noMatchingAccount.msg=This offer uses a payment method you haven't set up yet. \n\nWould you like to set up a new payment account now? offerbook.warning.counterpartyTradeRestrictions=This offer cannot be taken due to counterparty trade restrictions offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\nAfter successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\nFor more information on account signing, please see the documentation at [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- The buyer's account has not been signed by an arbitrator or a peer\n- The time since signing of the buyer's account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- Your account has not been signed by an arbitrator or a peer\n- The time since signing of your account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=วิธีการชำระเงินนี้ถูก จำกัด ชั่วคราวไปยัง {0} จนถึง {1} เนื่องจากผู้ซื้อทุกคนมีบัญชีใหม่\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=ข้อเสนอของคุณจะถูก จำกัด เฉพาะผู้ซื้อที่มีบัญชีที่ได้ลงนามและมีอายุ เนื่องจากมันเกิน {0}.\n\n{1} offerbook.warning.wrongTradeProtocol=ข้อเสนอดังกล่าวต้องใช้โปรโตคอลเวอร์ชันอื่นเหมือนกับเวอร์ชันที่ใช้ในซอฟต์แวร์เวอร์ชันของคุณ\n\nโปรดตรวจสอบว่าคุณได้ติดตั้งเวอร์ชั่นล่าสุด อีกนัยหนึ่งผู้ใช้ที่สร้างข้อเสนอได้ใช้รุ่นที่เก่ากว่า\n\nผู้ใช้ไม่สามารถซื้อขายกับโปรโตคอลการค้าเวอร์ชั่นซอฟต์แวร์ที่แตกต่างกันได้ offerbook.warning.userIgnored=คุณได้เพิ่มที่อยู่ onion ของผู้ใช้ลงในรายการที่ไม่สนใจแล้ว offerbook.warning.offerBlocked=ข้อเสนอดังกล่าวถูกบล็อกโดยนักพัฒนาซอฟต์แวร์ Haveno\nอาจมีข้อบกพร่องที่ไม่ได้รับการจัดการซึ่งก่อให้เกิดปัญหาเมื่อมีข้อเสนอนั้น offerbook.warning.currencyBanned=สกุลเงินที่ใช้ในข้อเสนอนั้นถูกบล็อกโดยนักพัฒนา Haveno\nสามารถอ่านข้อมูลเพิ่มเติมได้ที่ฟอรั่มของ Haveno offerbook.warning.paymentMethodBanned=วิธีการชำระเงินที่ใช้ในข้อเสนอนั้นถูกบล็อกโดยนักพัฒนา Haveno\nกรุณาเข้าไปอ่านที่ Forum ของ Haveno สำหรับข้อมูลเพิ่มเติม offerbook.warning.nodeBlocked=ที่อยู่ onion ของผู้ซื้อขายรายนั้นถูกบล็อกโดยนักพัฒนา Haveno\nอาจมีข้อบกพร่องที่ไม่ได้รับการจัดการ ซึ่งก่อให้เกิดปัญหาเมื่อรับข้อเสนอจากผู้ซื้อขายรายนั้น offerbook.warning.offerWasAlreadyUsedInTrade=You cannot take this offer because you already took it earlier. It could be that your previous take-offer attempt resulted in a failed trade. offerbook.info.sellAtMarketPrice=คุณจะขายในราคาตลาด (อัปเดตทุกนาที) offerbook.info.buyAtMarketPrice=คุณจะซื้อในราคาตลาด (อัปเดตทุกนาที) offerbook.info.sellBelowMarketPrice=คุณจะได้รับ {0} น้อยกว่าราคาตลาดในปัจจุบัน (อัปเดตทุกนาที) offerbook.info.buyAboveMarketPrice=คุณจะจ่าย {0} มากกว่าราคาตลาดในปัจจุบัน (อัปเดตทุกนาที) offerbook.info.sellAboveMarketPrice=คุณจะได้รับ {0} มากกว่าราคาตลาดในปัจจุบัน (อัปเดตทุกนาที) offerbook.info.buyBelowMarketPrice=คุณจะจ่าย {0} น้อยกว่าราคาตลาดในปัจจุบัน (อัปเดตทุกนาที) offerbook.info.buyAtFixedPrice=คุณจะซื้อในราคาที่ถูกกำหนดไว้ offerbook.info.sellAtFixedPrice=คุณจะขายในราคาที่ถูกกำหนดไว้ offerbook.info.noArbitrationInUserLanguage=ในกรณีที่มีข้อพิพาท โปรดทราบว่ากระบวนการไกล่เกลี่ยสำหรับข้อเสนอนี้จะได้รับการจัดการ {0} ภาษาที่มีการตั้งค่าในปัจจุบัน {1} offerbook.info.roundedFiatVolume=จำนวนเงินจะปัดเศษเพื่อเพิ่มความเป็นส่วนตัวในการค้าของคุณ #################################################################### # Offerbook / Create offer #################################################################### createOffer.amount.prompt=ป้อนจำนวนเงินใน XMR createOffer.price.prompt=ป้อนราคา createOffer.volume.prompt=ป้อนจำนวนเงินใน {0} createOffer.amountPriceBox.amountDescription=ยอดจำนวน XMR ถึง {0} createOffer.amountPriceBox.buy.volumeDescription=ยอดจำนวน {0} ที่ต้องจ่าย createOffer.amountPriceBox.sell.volumeDescription=จำนวนเงิน {0} ที่ได้รับ createOffer.amountPriceBox.minAmountDescription=จำนวนเงินขั้นต่ำของ XMR createOffer.securityDeposit.prompt=เงินประกัน createOffer.fundsBox.title=เงินทุนสำหรับข้อเสนอของคุณ createOffer.fundsBox.offerFee=ค่าธรรมเนียมการซื้อขาย createOffer.fundsBox.networkFee=ค่าธรรมเนียมการขุด createOffer.fundsBox.placeOfferSpinnerInfo=การประกาศข้อเสนออยู่ระหว่างดำเนินการ ... createOffer.fundsBox.paymentLabel=การซื้อขาย Haveno ด้วย ID {0} createOffer.fundsBox.fundsStructure=({0} เงินประกัน {1} ค่าธรรมเนียมการซื้อขาย {2} ค่าธรรมเนียมการขุด) createOffer.success.headline=ข้อเสนอของคุณได้ถูกสร้างขึ้นแล้ว createOffer.success.info=คุณสามารถจัดการข้อเสนอแบบเปิดของคุณได้ที่ \"Portfolio (แฟ้มผลงาน) / My open offers (ข้อเสนอแบบเปิดของฉัน) \" createOffer.info.sellAtMarketPrice=คุณจะขายในราคาตลาดเสมอ เนื่องจากราคาข้อเสนอของคุณจะได้รับการอัพเดตอย่างต่อเนื่อง createOffer.info.buyAtMarketPrice=คุณจะซื้อในราคาตลาดเสมอ เนื่องจากราคาข้อเสนอของคุณจะได้รับการอัพเดตอย่างต่อเนื่อง createOffer.info.sellAboveMarketPrice=คุณจะได้รับ {0}% มากกว่าราคาตลาดในปัจจุบันเนื่องจากราคาข้อเสนอของคุณจะได้รับการอัพเดตอย่างต่อเนื่อง createOffer.info.buyBelowMarketPrice=คุณจะจ่าย {0}% น้อยกว่าราคาตลาดในปัจจุบันเนื่องจากราคาข้อเสนอของคุณจะได้รับการอัพเดตอย่างต่อเนื่อง createOffer.warning.sellBelowMarketPrice=คุณจะได้รับ {0}% น้อยกว่าราคาตลาดในปัจจุบันเนื่องจากราคาข้อเสนอของคุณจะได้รับการอัพเดตอย่างต่อเนื่อง createOffer.warning.buyAboveMarketPrice=คุณจะต้องจ่ายเงิน {0}% มากกว่าราคาตลาดในปัจจุบันเนื่องจากราคาข้อเสนอของคุณจะได้รับการอัพเดตอย่างต่อเนื่อง createOffer.tradeFee.descriptionXMROnly=ค่าธรรมเนียมการซื้อขาย createOffer.tradeFee.descriptionBSQEnabled=เลือกสกุลเงินค่าธรรมเนียมในการเทรด createOffer.triggerPrice.prompt=Set optional trigger price createOffer.triggerPrice.label=Deactivate offer if market price is {0} createOffer.triggerPrice.tooltip=As protection against drastic price movements you can set a trigger price which deactivates the offer if the market price reaches that value. createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0} createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0} # new entries createOffer.placeOfferButton.buy=ตรวจสอบ: สร้างข้อเสนอเพื่อซื้อ XMR ด้วย {0} createOffer.placeOfferButton.sell=ตรวจสอบ: สร้างข้อเสนอเพื่อขาย XMR เป็น {0} createOffer.createOfferFundWalletInfo.headline=เงินทุนสำหรับข้อเสนอของคุณ # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- ปริมาณการซื้อขาย: {0} createOffer.createOfferFundWalletInfo.msg=คุณจำเป็นต้องฝากเงิน {0} เพื่อข้อเสนอนี้\n\n\ เงินเหล่านี้จะถูกสงวนไว้ในกระเป๋าเงินในเครื่องของคุณ และจะถูกล็อกในกระเป๋าเงินมัลติซิกเมื่อมีคนรับข้อเสนอของคุณ\n\n\ จำนวนเงินคือผลรวมของ:\n\ {1}\ - เงินประกันของคุณ: {2}\n\ - ค่าธรรมเนียมการซื้อขาย: {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=เกิดข้อผิดพลาดขณะใส่ข้อเสนอ: \n\n{0} \n\nยังไม่มีการโอนเงินจาก wallet ของคุณเลย\nโปรดเริ่มแอปพลิเคชันใหม่และตรวจสอบการเชื่อมต่อเครือข่ายของคุณ createOffer.setAmountPrice=กำหนดจำนวนและราคา createOffer.warnCancelOffer=คุณได้ทำการเติมเงินสำหรับข้อเสนอนี้แล้ว\nหากคุณต้องการยกเลิกตอนนี้ จะทำให้เงินของคุณยังคงอยู่ในกระเป๋าสตางค์ Haveno ท้องถิ่นของคุณและสามารถถอนได้ในหน้าจอ \"เงิน / ส่งเงิน\"\nคุณแน่ใจหรือไม่ว่าคุณต้องการยกเลิก? createOffer.timeoutAtPublishing=มีกำหนดเวลาในการเผยแพร่ข้อเสนอ createOffer.errorInfo=\n\nมีการชำระค่าธรรมเนียมผู้สร้างแล้ว ในกรณีที่คุณต้องสูญเสียค่าธรรมเนียมนั้นไป\nโปรดลองเริ่มแอปพลิเคชันของคุณใหม่และตรวจสอบการเชื่อมต่อเครือข่ายของคุณเพื่อดูว่าคุณสามารถแก้ไขปัญหาได้หรือไม่ createOffer.tooLowSecDeposit.warning=คุณได้ตั้งค่าเงินประกันเป็นค่าต่ำกว่าค่าเริ่มต้นที่แนะนำไว้ที่ {0} \nคุณแน่ใจหรือไม่ว่าต้องการใช้เงินประกันที่ต่ำกว่า createOffer.tooLowSecDeposit.makerIsSeller=มันทำให้คุณได้รับความคุ้มครองน้อยลงในกรณีที่ผู้ค้าไม่ปฏิบัติตามโปรโตคอลทางการซื้อขาย createOffer.tooLowSecDeposit.makerIsBuyer=จะให้การคุ้มครองที่น้อยกว่าสำหรับผู้ซื้อขายที่ทำตามโปรโตคอลการค้าเนื่องจากคุณมีเงินฝากน้อยลง ผู้ใช้รายอื่นอาจต้องการรับข้อเสนอจากที่อื่นมากกว่าของคุณ createOffer.resetToDefault=ไม่ รีเซ็ตเป็นค่าเริ่มต้น createOffer.useLowerValue=ใช่ ใช้ค่าต่ำกว่าของฉัน createOffer.priceOutSideOfDeviation=ราคาที่คุณป้อนอยู่เกินออกจากส่วนเบี่ยงเบนที่ได้รับอนุญาตจากราคาตลาด\nค่าเบี่ยงเบนสูงสุดที่อนุญาตคือ {0} และสามารถปรับได้ตามความต้องการ createOffer.changePrice=เปลี่ยนราคา createOffer.tac=ด้วยการเผยแพร่ข้อเสนอพิเศษนี้ ฉันยอมรับการซื้อขายกับผู้ค้ารายย่อยที่ปฏิบัติตามเงื่อนไขที่กำหนดไว้บนหน้าจอนี้ createOffer.currencyForFee=ค่าธรรมเนียมการซื้อขาย createOffer.setDeposit=Set buyer's security deposit (%) createOffer.setDepositAsBuyer=Set my security deposit as buyer (%) createOffer.setDepositForBothTraders=Set both traders' security deposit (%) createOffer.securityDepositInfo=Your buyer's security deposit will be {0} createOffer.securityDepositInfoAsBuyer=Your security deposit as buyer will be {0} createOffer.minSecurityDepositUsed=เงินประกันความปลอดภัยขั้นต่ำถูกใช้ createOffer.buyerAsTakerWithoutDeposit=ไม่ต้องวางมัดจำจากผู้ซื้อ (ป้องกันด้วยรหัสผ่าน) createOffer.myDeposit=เงินประกันความปลอดภัยของฉัน (%) createOffer.myDepositInfo=เงินประกันความปลอดภัยของคุณจะเป็น {0} #################################################################### # Offerbook / Take offer #################################################################### takeOffer.amount.prompt=ป้อนจำนวนเงินใน XMR takeOffer.amountPriceBox.buy.amountDescription=จำนวน XMR ที่จะขาย takeOffer.amountPriceBox.sell.amountDescription=จำนวน XMR ที่จะซื้อ takeOffer.amountPriceBox.priceDescription=ราคาต่อ monero ใน {0} takeOffer.amountPriceBox.amountRangeDescription=ช่วงจำนวนที่เป็นไปได้ takeOffer.amountPriceBox.warning.invalidXmrDecimalPlaces=จำนวนเงินที่คุณป้อนเกินจำนวนตำแหน่งทศนิยมที่อนุญาต\nจำนวนเงินได้รับการปรับเป็นตำแหน่งทศนิยม 4 ตำแหน่ง takeOffer.validation.amountSmallerThanMinAmount=จำนวนเงินต้องไม่น้อยกว่าจำนวนเงินขั้นต่ำที่ระบุไว้ในข้อเสนอ takeOffer.validation.amountLargerThanOfferAmount=จำนวนเงินที่ป้อนต้องไม่สูงกว่าจำนวนที่กำหนดไว้ในข้อเสนอ takeOffer.validation.amountLargerThanOfferAmountMinusFee=จำนวนเงินที่ป้อนจะสร้างการเปลี่ยนแปลง dust (Monero ที่มีขนาดเล็กมาก) สำหรับผู้ขาย XMR takeOffer.fundsBox.title=ทุนการซื้อขายของคุณ takeOffer.fundsBox.isOfferAvailable=ตรวจสอบว่ามีข้อเสนออื่นๆหรือไม่ ... takeOffer.fundsBox.tradeAmount=จำนวนที่จะขาย takeOffer.fundsBox.offerFee=ค่าธรรมเนียมการซื้อขาย takeOffer.fundsBox.networkFee=ยอดรวมค่าธรรมเนียมการขุด takeOffer.fundsBox.takeOfferSpinnerInfo=ยอมรับข้อเสนอ: {0} takeOffer.fundsBox.paymentLabel=การซื้อขาย Haveno ด้วย ID {0} takeOffer.fundsBox.fundsStructure=({0} เงินประกัน {1} ค่าธรรมเนียมการซื้อขาย {2} ค่าธรรมเนียมการขุด) takeOffer.fundsBox.noFundingRequiredTitle=ไม่ต้องใช้เงินทุน takeOffer.fundsBox.noFundingRequiredDescription=รับรหัสผ่านข้อเสนอจากผู้ขายภายนอก Haveno เพื่อรับข้อเสนอนี้ takeOffer.success.headline=คุณได้รับข้อเสนอเป็นที่เรีบยร้อยแล้ว takeOffer.success.info=คุณสามารถดูสถานะการค้าของคุณได้ที่ \ "Portfolio (แฟ้มผลงาน) / เปิดการซื้อขาย \" takeOffer.error.message=เกิดข้อผิดพลาดขณะรับข้อเสนอ\n\n{0} # new entries takeOffer.takeOfferButton.buy=ตรวจสอบ: ยอมรับข้อเสนอเพื่อซื้อ XMR ด้วย {0} takeOffer.takeOfferButton.sell=ตรวจสอบ: ยอมรับข้อเสนอเพื่อขาย XMR เป็น {0} takeOffer.noPriceFeedAvailable=คุณไม่สามารถรับข้อเสนอดังกล่าวเนื่องจากใช้ราคาร้อยละตามราคาตลาด แต่ไม่มีฟีดราคาที่พร้อมใช้งาน takeOffer.takeOfferFundWalletInfo.headline=ทุนการซื้อขายของคุณ # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- ปริมาณการซื้อขาย: {0} takeOffer.takeOfferFundWalletInfo.msg=คุณต้องฝากเงิน {0} เพื่อรับข้อเสนอนี้。\n\nจำนวนเงินคือผลรวมของ:\n{1}- เงินมัดจำของคุณ: {2}\n- ค่าธรรมเนียมการซื้อขาย: {3} takeOffer.alreadyPaidInFunds=หากคุณได้ชำระเงินแล้วคุณสามารถถอนเงินออกได้ในหน้าจอ \"เงิน / ส่งเงิน \" takeOffer.paymentInfo=ข้อมูลการชำระเงิน takeOffer.setAmountPrice=ตั้งยอดจำนวน takeOffer.alreadyFunded.askCancel=คุณได้ทำการเติมเงินสำหรับข้อเสนอนี้แล้ว\nหากคุณต้องการยกเลิกตอนนี้ จะทำให้เงินของคุณยังคงอยู่ในกระเป๋าสตางค์ Haveno ท้องถิ่นของคุณและสามารถถอนได้ในหน้าจอ \"เงิน / ส่งเงิน\"\nคุณแน่ใจหรือไม่ว่าคุณต้องการยกเลิก? takeOffer.failed.offerNotAvailable=การขอข้อเสนอล้มเหลวเนื่องจากข้อเสนอไม่พร้อมใช้งานอีกต่อไป บางทีผู้ค้ารายอื่นอาจรับข้อเสนอนี้ไปแล้ว takeOffer.failed.offerTaken=คุณไม่สามารถรับข้อเสนอดังกล่าวได้เนื่องจากข้อเสนอนี้ได้ถูกดำเนินการโดยผู้ค้ารายอื่นแล้ว takeOffer.failed.offerRemoved=คุณไม่สามารถรับข้อเสนอดังกล่าวได้เนื่องจากข้อเสนอถูกลบออกไปแล้ว takeOffer.failed.offererNotOnline=คำขอข้อเสนอล้มเหลว เนื่องจากผู้สร้างไม่ได้ออนไลน์อยู่ในระบบ takeOffer.failed.offererOffline=คุณไม่สามารถรับข้อเสนอดังกล่าวได้เนื่องจากผู้สร้างออฟไลน์ takeOffer.warning.connectionToPeerLost=You lost connection to the maker.\nThey might have gone offline or has closed the connection to you because of too many open connections.\n\nIf you can still see their offer in the offerbook you can try to take the offer again. takeOffer.error.noFundsLost=\n\nยังไม่มีเงินเหลือ wallet อยู่เลย\nโปรดลองเริ่มแอปพลิเคชันของคุณใหม่และตรวจสอบการเชื่อมต่อเครือข่ายของคุณเพื่อดูว่าคุณสามารถแก้ไขปัญหาได้หรือไม่ # suppress inspection "TrailingSpacesInProperty" takeOffer.error.feePaid=\n\n takeOffer.error.depositPublished=\n\nธุรกรรมเงินฝากได้รับการเผยแพร่เป็นที่เรียบร้อยแล้ว\nโปรดลองเริ่มแอปพลิเคชันของคุณใหม่และตรวจสอบการเชื่อมต่อเครือข่ายของคุณเพื่อดูว่าคุณสามารถแก้ไขปัญหาได้หรือไม่?\nหากปัญหายังคงอยู่โปรดติดต่อนักพัฒนาซอฟต์แวร์เพื่อขอความช่วยเหลือ takeOffer.error.payoutPublished=\n\nมีการเผยแพร่รายการการชำระแล้ว\nโปรดลองเริ่มแอปพลิเคชันของคุณใหม่และตรวจสอบการเชื่อมต่อเครือข่ายของคุณเพื่อดูว่าคุณสามารถแก้ไขปัญหาได้หรือไม่?\nหากปัญหายังคงอยู่โปรดติดต่อนักพัฒนาซอฟต์แวร์เพื่อขอความช่วยเหลือ takeOffer.tac=ด้วยข้อเสนอนี้ฉันยอมรับเงื่อนไขทางการค้าตามที่กำหนดไว้ในหน้าจอนี้ #################################################################### # Offerbook / Edit offer #################################################################### openOffer.header.triggerPrice=ราคาเงื่อนไขที่ตั้งไว้ openOffer.triggerPrice=Trigger price {0} openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\nPlease edit the offer to define a new trigger price editOffer.setPrice=ตั้งราคา editOffer.confirmEdit=ยืนยัน: แก้ไขข้อเสนอ editOffer.publishOffer=กำลังเผยแพร่ข้อเสนอของคุณ editOffer.failed=การแก้ไขข้อเสนอล้มเหลว: \n{0} editOffer.success=ข้อเสนอของคุณได้รับการแก้ไขเรียบร้อยแล้ว editOffer.invalidDeposit=The buyer's security deposit is not within the constraints defined by Haveno and can no longer be edited. #################################################################### # Portfolio #################################################################### portfolio.tab.openOffers=ข้อเสนอแบบเปิดของฉัน portfolio.tab.pendingTrades=เปิดการซื้อขาย portfolio.tab.history=ประวัติ portfolio.tab.failed=ผิดพลาด portfolio.tab.editOpenOffer=แก้ไขข้อเสนอ portfolio.closedTrades.deviation.help=Percentage price deviation from market portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} portfolio.pending.syncing=กำลังซิงค์กระเป๋าเงินสำหรับการซื้อขาย portfolio.pending.syncing.blockRemaining=กำลังซิงค์กระเป๋าเงินสำหรับการซื้อขาย — เหลืออีก 1 บล็อก portfolio.pending.syncing.blocksRemaining=กำลังซิงค์กระเป๋าเงินสำหรับการซื้อขาย — เหลืออีก {0} บล็อก portfolio.pending.step1.waitForConf=รอการยืนยันของบล็อกเชน portfolio.pending.step2_buyer.additionalConf=ยอดฝากถึง 10 การยืนยันแล้ว\nเพื่อความปลอดภัยเพิ่มเติม เราแนะนำให้รอ {0} การยืนยันก่อนทำการชำระเงิน\nดำเนินการล่วงหน้าตามความเสี่ยงของคุณเอง portfolio.pending.step2_buyer.startPayment=เริ่มการชำระเงิน portfolio.pending.step2_seller.waitPaymentSent=รอจนกว่าการชำระเงินจะเริ่มขึ้น portfolio.pending.step3_buyer.waitPaymentArrived=รอจนกว่าจะถึงการชำระเงิน portfolio.pending.step3_seller.confirmPaymentReceived=การยืนยันการชำระเงินที่ได้รับ portfolio.pending.step5.completed=เสร็จสิ้น portfolio.pending.step3_seller.autoConf.status.label=Auto-confirm status portfolio.pending.autoConf=Auto-confirmed portfolio.pending.autoConf.blocks=XMR confirmations: {0} / Required: {1} portfolio.pending.autoConf.state.xmr.txKeyReused=Transaction key re-used. Please open a dispute. portfolio.pending.autoConf.state.confirmations=XMR confirmations: {0}/{1} portfolio.pending.autoConf.state.txNotFound=Transaction not seen in mem-pool yet portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=No valid transaction ID / transaction key portfolio.pending.autoConf.state.filterDisabledFeature=Disabled by developers. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FEATURE_DISABLED=Auto-confirm feature is disabled. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=Trade amount exceeds auto-confirm amount limit # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.INVALID_DATA=Peer provided invalid data. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=Payout transaction was already published. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.DISPUTE_OPENED=Dispute was opened. Auto-confirm is deactivated for that trade. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.REQUESTS_STARTED=Transaction proof requests started # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PENDING=Success results: {0}/{1}; {2} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.COMPLETED=Proof at all services succeeded # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. No auto-confirm possible. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. portfolio.pending.step1.info.you=การทำธุรกรรมฝากเงินได้ถูกเผยแพร่แล้ว\nคุณสามารถเริ่มการชำระเงินได้หลังจากได้รับการยืนยัน 10 ครั้ง (ประมาณ {0} นาที) portfolio.pending.step1.info.buyer=การทำธุรกรรมฝากเงินได้ถูกเผยแพร่แล้ว\nผู้ซื้อ XMR สามารถเริ่มการชำระเงินได้หลังจากได้รับการยืนยัน 10 ครั้ง (ประมาณ {0} นาที) portfolio.pending.step1.warn=The deposit transaction is still not confirmed. This sometimes happens in rare cases when the funding fee of one trader from an external wallet was too low. portfolio.pending.step1.openForDispute=The deposit transaction is still not confirmed. You can wait longer or contact the mediator for assistance. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2.confReached=Your trade has reached at least one blockchain confirmation.\n\n portfolio.pending.step2_buyer.refTextWarn=Important: when making the payment, leave the \"reason for payment\" field empty. DO NOT put the trade ID or any other text like 'monero', 'XMR', or 'Haveno'. You are free to discuss via trader chat if an alternate \"reason for payment\" would be suitable to you both. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees=If your bank charges you any fees to make the transfer, you are responsible for paying those fees. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.crypto=โปรดโอนจาก wallet {0} ภายนอก\n{1} ให้กับผู้ขาย XMR\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.cash=โปรดไปที่ธนาคารและจ่ายเงิน {0} ให้กับผู้ขาย XMR\n portfolio.pending.step2_buyer.cash.extra=ข้อกำหนดที่สำคัญ: \nหลังจากที่คุณได้ชำระเงินแล้วให้เขียนลงในใบเสร็จรับเงิน: NO REFUNDS (ไม่มีการคืนเงิน)\nจากนั้นแบ่งออกเป็น 2 ส่วนถ่ายรูปและส่งไปที่ที่อยู่อีเมลของผู้ขาย XMR # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.moneyGram=โปรดชำระเงิน {0} ให้กับผู้ขาย XMR โดยใช้ MoneyGram\n portfolio.pending.step2_buyer.moneyGram.extra=ข้อกำหนดที่สำคัญ: \nหลังจากที่คุณได้ชำระเงินแล้วให้ส่งหมายเลข Authorization (การอนุมัติ) และรูปใบเสร็จรับเงินไปยังผู้ขาย XMR ทางอีเมล\nใบเสร็จจะต้องแสดงชื่อเต็มของผู้ขาย ประเทศ รัฐ และจำนวนเงินทั้งหมดของผู้ขาย อีเมลของผู้ขายคือ: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.westernUnion=โปรดชำระเงิน {0} ให้กับผู้ขาย XMR โดยใช้ Western Union portfolio.pending.step2_buyer.westernUnion.extra=ข้อกำหนดที่สำคัญ: \nหลังจากที่คุณได้ชำระเงินแล้วให้ส่ง MTCN (หมายเลขติดตาม) และรูปใบเสร็จรับเงินไปยังผู้ขาย XMR ทางอีเมล\nใบเสร็จจะต้องแสดงชื่อเต็ม เมือง ประเทศ และจำนวนเงินทั้งหมดของผู้ขาย อีเมลของผู้ขายคือ: {0} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=โปรดส่ง {0} โดยธนาณัติ \"US Postal Money Order \" ไปยังผู้ขาย XMR\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=กรุณาติดต่อผู้ขายของ XMR ตามรายชื่อที่ได้รับและนัดประชุมเพื่อจ่ายเงิน {0}\n\n portfolio.pending.step2_buyer.startPaymentUsing=เริ่มต้นการชำระเงินโดยใช้ {0} portfolio.pending.step2_buyer.recipientsAccountData=Recipients {0} portfolio.pending.step2_buyer.amountToTransfer=จำนวนเงินที่จะโอน portfolio.pending.step2_buyer.sellersAddress=ที่อยู่ของผู้ขาย {0} portfolio.pending.step2_buyer.buyerAccount=บัญชีการชำระเงินที่ต้องการใข้งาน portfolio.pending.step2_buyer.paymentSent=การชำระเงินเริ่มต้นแล้ว portfolio.pending.step2_buyer.showEarly=แสดงรายละเอียดการชำระเงินล่วงหน้า portfolio.pending.step2_buyer.warn=You still have not done your {0} payment!\nPlease note that the trade has to be completed by {1}. portfolio.pending.step2_buyer.openForDispute=You have not completed your payment!\nThe max. period for the trade has elapsed.Please contact the mediator for assistance. portfolio.pending.step2_buyer.paperReceipt.headline=คุณได้ส่งใบเสร็จรับเงินให้กับผู้ขาย XMR หรือไม่? portfolio.pending.step2_buyer.paperReceipt.msg=ข้อควรจำ: \nคุณต้องเขียนลงในใบเสร็จรับเงิน: NO REFUNDS (ไม่มีการคืนเงิน)\nจากนั้นแบ่งออกเป็น 2 ส่วนถ่ายรูปและส่งไปที่ที่อยู่อีเมลของผู้ขาย XMR portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=ส่งหมายเลขการอนุมัติและใบเสร็จรับเงิน portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=คุณต้องส่งหมายเลขการอนุมัติและรูปใบเสร็จรับเงินทางอีเมลไปยังผู้ขาย XMR \nใบเสร็จจะต้องแสดงชื่อเต็มของประเทศ รัฐ และจำนวนเงินทั้งหมดของผู้ขาย อีเมลของผู้ขายคือ: {0} .\n\nคุณได้ส่งหมายเลขการอนุมัติและทำสัญญากับผู้ขายหรือไม่?\n portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=ส่ง MTCN (หมายเลขติดตาม) และใบเสร็จรับเงิน portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=คุณต้องส่ง MTCN (หมายเลขติดตาม) และรูปใบเสร็จรับเงินทางอีเมลไปยังผู้ขาย XMR \nใบเสร็จจะต้องแสดงชื่อเต็ม เมือง ประเทศ และจำนวนเงินทั้งหมดของผู้ขาย อีเมลของผู้ขายคือ: {0} .\n\nคุณได้ส่ง MTCN และทำสัญญากับผู้ขายหรือไม่ portfolio.pending.step2_buyer.halCashInfo.headline=ส่งรหัส HalCash portfolio.pending.step2_buyer.halCashInfo.msg=คุณต้องส่งข้อความที่มีรหัส HalCash พร้อมกับ IDการค้า ({0}) ไปยังผู้ขาย XMR \nเบอร์โทรศัพท์มือถือของผู้ขาย คือ {1}\n\nคุณได้ส่งรหัสให้กับผู้ขายหรือยัง? portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Some banks might verify the receiver's name. Faster Payments accounts created in old Haveno clients do not provide the receiver's name, so please use trade chat to obtain it (if needed). portfolio.pending.step2_buyer.confirmStart.headline=ยืนยันว่าคุณได้เริ่มต้นการชำระเงินแล้ว portfolio.pending.step2_buyer.confirmStart.msg=คุณได้เริ่มต้นการ {0} การชำระเงินให้กับคู่ค้าของคุณแล้วหรือยัง portfolio.pending.step2_buyer.confirmStart.yes=ใช่ฉันได้เริ่มต้นการชำระเงินแล้ว portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=You have not provided proof of payment portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=You have not entered the transaction ID and the transaction key.\n\nBy not providing this data the peer cannot use the auto-confirm feature to release the XMR as soon the XMR has been received.\nBeside that, Haveno requires that the sender of the XMR transaction is able to provide this information to the mediator or arbitrator in case of a dispute.\nSee more details on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=Input is not a 32 byte hexadecimal value portfolio.pending.step2_buyer.confirmStart.warningButton=Ignore and continue anyway portfolio.pending.step2_seller.waitPayment.headline=รอการชำระเงิน portfolio.pending.step2_seller.f2fInfo.headline=ข้อมูลการติดต่อของผู้ซื้อ portfolio.pending.step2_seller.waitPayment.msg=ธุรกรรมการฝากเงินมีการยืนยันบล็อกเชนอย่างน้อยหนึ่งรายการ\nคุณต้องรอจนกว่าผู้ซื้อ XMR จะเริ่มการชำระเงิน {0} portfolio.pending.step2_seller.warn=ผู้ซื้อ XMR ยังไม่ได้ทำ {0} การชำระเงิน\nคุณต้องรอจนกว่าผู้ซื้อจะเริ่มชำระเงิน\nหากการซื้อขายยังไม่เสร็จสิ้นในวันที่ {1} ผู้ไกล่เกลี่ยจะดำเนินการตรวจสอบ portfolio.pending.step2_seller.openForDispute=The XMR buyer has not started their payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the mediator for assistance. tradeChat.chatWindowTitle=Chat window for trade with ID '{0}' tradeChat.openChat=Open chat window tradeChat.rules=You can communicate with your trade peer to resolve potential problems with this trade.\nIt is not mandatory to reply in the chat.\nIf a trader violates any of the rules below, open a dispute and report it to the mediator or arbitrator.\n\nChat rules:\n\t● Do not send any links (risk of malware). You can send the transaction ID and the name of a block explorer.\n\t● Do not send your seed words, private keys, passwords or other sensitive information!\n\t● Do not encourage trading outside of Haveno (no security).\n\t● Do not engage in any form of social engineering scam attempts.\n\t● If a peer is not responding and prefers to not communicate via chat, respect their decision.\n\t● Keep conversation scope limited to the trade. This chat is not a messenger replacement or troll-box.\n\t● Keep conversation friendly and respectful. # suppress inspection "UnusedProperty" message.state.UNDEFINED=ไม่ได้กำหนด # suppress inspection "UnusedProperty" message.state.SENT=ข้อความที่ถูกส่ง # suppress inspection "UnusedProperty" message.state.ARRIVED=ข้อความถึง เน็ตเวิร์ก peer แล้ว # suppress inspection "UnusedProperty" message.state.STORED_IN_MAILBOX=Message of payment sent but not yet received by peer # suppress inspection "UnusedProperty" message.state.ACKNOWLEDGED=เน็ตเวิร์ก peer ยืนยันการรับข้อความแล้ว # suppress inspection "UnusedProperty" message.state.FAILED=การส่งข้อความล้มเหลว portfolio.pending.step3_buyer.wait.headline=รอการยืนยันการชำระเงินของผู้ขาย XMR portfolio.pending.step3_buyer.wait.info=กำลังรอการยืนยันจากผู้ขาย XMR สำหรับการรับ {0} การชำระเงิน portfolio.pending.step3_buyer.wait.msgStateInfo.label=เริ่มต้นสถานะการชำระเงิน portfolio.pending.step3_buyer.warn.part1a=ใน {0} บล็อกเชน portfolio.pending.step3_buyer.warn.part1b=ที่ผู้ให้บริการการชำระเงิน (เช่น ธนาคาร) portfolio.pending.step3_buyer.warn.part2=The XMR seller still has not confirmed your payment. Please check {0} if the payment sending was successful. portfolio.pending.step3_buyer.openForDispute=The XMR seller has not confirmed your payment! The max. period for the trade has elapsed. You can wait longer and give the trading peer more time or request assistance from the mediator. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.part=พันธมิตรทางการค้าของคุณได้ยืนยันว่าพวกเขาได้เริ่มต้น {0} การชำระเงิน\n\n portfolio.pending.step3_seller.crypto.explorer=ผู้สำรวจบล็อกเชน {0} ที่ถูกใจของคุณ portfolio.pending.step3_seller.crypto.wallet=ณ กระเป๋าสตางค์ {0} ของคุณ portfolio.pending.step3_seller.crypto={0}โปรดตรวจสอบ {1} หากการทำธุรกรรมส่วนที่อยู่รับของคุณ\n{2}\nมีการยืนยันบล็อกเชนแล้วเรียบร้อย\nยอดการชำระเงินต้องเป็น {3}\n\nคุณสามารถคัดลอกและวาง {4} ข้อมูลที่อยู่ของคุณได้จากหน้าจอหลักหลังจากปิดหน้าต่างป๊อปอัพ portfolio.pending.step3_seller.postal={0}Please check if you have received {1} with \"US Postal Money Order\" from the XMR buyer. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.payByMail={0}Please check if you have received {1} with \"Pay by Mail\" from the XMR buyer. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.bank=Your trading partner has confirmed that they have initiated the {0} payment.\n\nPlease go to your online banking web page and check if you have received {1} from the XMR buyer. portfolio.pending.step3_seller.cash=เนื่องจากการชำระเงินผ่าน Cash Deposit (ฝากเงินสด) ผู้ซื้อ XMR จะต้องเขียน \"NO REFUND \" ในใบเสร็จรับเงินและให้แบ่งออกเป็น 2 ส่วนและส่งรูปถ่ายทางอีเมล\n\nเพื่อหลีกเลี่ยงความเสี่ยงจากการปฏิเสธการชำระเงิน ให้ยืนยันเฉพาะถ้าคุณได้รับอีเมลและหากคุณแน่ใจว่าใบเสร็จถูกต้องแล้ว\nถ้าคุณไม่แน่ใจ {0} portfolio.pending.step3_seller.moneyGram=ผู้ซื้อต้องส่งหมายเลขอนุมัติและรูปใบเสร็จรับเงินทางอีเมล\nใบเสร็จรับเงินต้องแสดงชื่อเต็มของคุณ ประเทศ รัฐ และจำนวนเงิน โปรดตรวจสอบอีเมลของคุณหากคุณได้รับหมายเลขการให้สิทธิ์\n\nหลังจากปิดป๊อปอัปคุณจะเห็นชื่อและที่อยู่ของผู้ซื้อ XMR เพื่อรับเงินจาก MoneyGram\n\nยืนยันเฉพาะใบเสร็จหลังจากที่คุณได้รับเงินเรียบร้อยแล้ว! portfolio.pending.step3_seller.westernUnion=ผู้ซื้อต้องส่ง MTCN (หมายเลขติดตาม) และรูปใบเสร็จรับเงินทางอีเมล\nใบเสร็จรับเงินต้องแสดงชื่อ เมือง ประเทศ และจำนวนเงินทั้งหมดไว้อย่างชัดเจน โปรดตรวจสอบอีเมลของคุณหากคุณได้รับ MTCN\n\nหลังจากปิดป๊อปอัปคุณจะเห็นชื่อและที่อยู่ของผู้ซื้อ XMR สำหรับการขอรับเงินจาก Western Union \n\nยืนยันเฉพาะใบเสร็จหลังจากที่คุณได้รับเงินเรียบร้อยแล้ว! portfolio.pending.step3_seller.halCash=ผู้ซื้อต้องส่งข้อความรหัส HalCash ให้คุณ ในขณะเดียวกันคุณจะได้รับข้อความจาก HalCash พร้อมกับคำขอข้อมูลจำเป็นในการถอนเงินยูโรุจากตู้เอทีเอ็มที่รองรับ HalCash \n\n หลังจากที่คุณได้รับเงินจากตู้เอทีเอ็มโปรดยืนยันใบเสร็จรับเงินจากการชำระเงินที่นี่ ! portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon eGift Card by email or by text message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted confirm the payment receipt. portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=ใบเสร็จยืนยันการชำระเงิน portfolio.pending.step3_seller.amountToReceive=จำนวนเงินที่ได้รับ portfolio.pending.step3_seller.yourAddress=ที่อยู่ {0} ของคุณ portfolio.pending.step3_seller.buyersAddress=ที่อยู่ {0} ผู้ซื้อ portfolio.pending.step3_seller.yourAccount=บัญชีการซื้อขายของคุณ portfolio.pending.step3_seller.xmrTxHash=เลขอ้างอิงการทำธุรกรรม portfolio.pending.step3_seller.xmrTxKey=Transaction key portfolio.pending.step3_seller.buyersAccount=Buyers account data portfolio.pending.step3_seller.confirmReceipt=ใบเสร็จยืนยันการชำระเงิน portfolio.pending.step3_seller.buyerStartedPayment=ผู้ซื้อ XMR ได้เริ่มการชำระเงิน {0}\n{1} portfolio.pending.step3_seller.buyerStartedPayment.crypto=ตรวจสอบการยืนยันบล็อกเชนที่ crypto wallet ของคุณหรือบล็อก explorer และยืนยันการชำระเงินเมื่อคุณมีการยืนยันบล็อกเชนที่เพียงพอ portfolio.pending.step3_seller.buyerStartedPayment.traditional=ตรวจสอบบัญชีการซื้อขายของคุณ (เช่น บัญชีธนาคาร) และยืนยันเมื่อคุณได้รับการชำระเงิน portfolio.pending.step3_seller.warn.part1a=ใน {0} บล็อกเชน portfolio.pending.step3_seller.warn.part1b=ที่ผู้ให้บริการการชำระเงิน (เช่น ธนาคาร) portfolio.pending.step3_seller.warn.part2=You still have not confirmed the receipt of the payment. Please check {0} if you have received the payment. portfolio.pending.step3_seller.openForDispute=You have not confirmed the receipt of the payment!\nThe max. period for the trade has elapsed.\nPlease confirm or request assistance from the mediator. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=คุณได้รับ {0} การชำระเงินจากคู่ค้าของคุณหรือไม่\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Please note, that as soon you have confirmed the receipt, the locked trade amount will be released to the XMR buyer and the security deposit will be refunded.\n\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=ยืนยันว่าคุณได้รับการชำระเงินแล้ว portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=ใช่ ฉันได้รับการชำระเงินแล้ว portfolio.pending.step3_seller.onPaymentReceived.signer=IMPORTANT: By confirming receipt of payment, you are also verifying the account of the counterparty and signing it accordingly. Since the account of the counterparty hasn't been signed yet, you should delay confirmation of the payment as long as possible to reduce the risk of a chargeback. portfolio.pending.step5_buyer.groupTitle=ผลสรุปการซื้อขายที่เสร็จสิ้น portfolio.pending.step5_buyer.tradeFee=ค่าธรรมเนียมการซื้อขาย portfolio.pending.step5_buyer.makersMiningFee=ค่าธรรมเนียมการขุด portfolio.pending.step5_buyer.takersMiningFee=ยอดรวมค่าธรรมเนียมการขุด portfolio.pending.step5_buyer.refunded=เงินประกันความปลอดภัยที่ถูกคืน portfolio.pending.step5_buyer.withdrawXMR=ถอนเงิน monero ของคุณ portfolio.pending.step5_buyer.amount=จำนวนเงินที่จะถอน portfolio.pending.step5_buyer.withdrawToAddress=ถอนไปยังที่อยู่ portfolio.pending.step5_buyer.moveToHavenoWallet=Keep funds in Haveno wallet portfolio.pending.step5_buyer.withdrawExternal=ถอนไปยัง wallet ภายนอก portfolio.pending.step5_buyer.alreadyWithdrawn=เงินทุนของคุณถูกถอนออกไปแล้ว\nโปรดตรวจสอบประวัติการทำธุรกรรม portfolio.pending.step5_buyer.confirmWithdrawal=ยืนยันคำขอถอนเงิน portfolio.pending.step5_buyer.amountTooLow=จำนวนเงินที่โอนจะต่ำกว่าค่าธรรมเนียมการทำธุรกรรมและมูลค่าต่ำกว่าที่น่าจะเป็น (dust หน่วยเล็กสุดของ monero) portfolio.pending.step5_buyer.withdrawalCompleted.headline=การถอนเสร็จสิ้น portfolio.pending.step5_buyer.withdrawalCompleted.msg=การซื้อขายที่เสร็จสิ้นของคุณจะถูกเก็บไว้ภายใต้ \"Portfolio (แฟ้มผลงาน) / ประวัติ\" \nคุณสามารถตรวจสอบการทำธุรกรรม Monero ทั้งหมดภายใต้ \"เงิน / ธุรกรรม \" portfolio.pending.step5_buyer.bought=คุณได้ซื้อ portfolio.pending.step5_buyer.paid=คุณได้จ่าย portfolio.pending.step5_seller.sold=คุณได้ขาย portfolio.pending.step5_seller.received=คุณได้รับ tradeFeedbackWindow.title=ขอแสดงความยินดีกับการซื้อขายที่เสร็จสมบูรณ์ของคุณ tradeFeedbackWindow.msg.part1=เรายินดีที่จะฟังความเห็นเกี่ยวกับประสบการณ์ของคุณ มันจะช่วยให้เราปรับปรุงซอฟต์แวร์และระบบดียิ่งขึ้น หากคุณต้องการแสดงความคิดเห็นโปรดกรอกแบบสำรวจสั้น ๆ (ไม่ต้องลงทะเบียน) ที่: tradeFeedbackWindow.msg.part2=หากคุณมีข้อสงสัยหรือประสบปัญหาใด ๆ โปรดติดต่อกับผู้ใช้และผู้สนับสนุนคนอื่น ๆ ผ่านทางฟอรัม Haveno ที่: tradeFeedbackWindow.msg.part3=ขอบคุณที่ใช้ Haveno! portfolio.pending.role=บทบาทของฉัน portfolio.pending.tradeInformation=ข้อมูลทางการซื้อขาย portfolio.pending.remainingTime=เวลาที่เหลือ portfolio.pending.remainingTimeDetail={0} (จนถึง {1}) portfolio.pending.remainingTimeDetail.startsAfter=เริ่มหลังจากยืนยัน {0} ครั้ง portfolio.pending.tradePeriodInfo=หลังจากได้รับการยืนยัน {0} ครั้ง ระยะเวลาการซื้อขายจะเริ่มต้นขึ้น โดยจะใช้ระยะเวลาการซื้อขายสูงสุดที่แตกต่างกันตามวิธีการชำระเงินที่ใช้ portfolio.pending.tradePeriodWarning=หากเกินระยะเวลานักซื้อขายทั้งสองฝ่ายสามารถเปิดข้อพิพาทได้ portfolio.pending.tradeNotCompleted=การซื้อขายไม่เสร็จสิ้นภายในเวลา (จนถึง {0}) portfolio.pending.tradeProcess=กระบวนการทางการซื้อขาย portfolio.pending.openAgainDispute.msg=If you are not sure that the message to the mediator or arbitrator arrived (e.g. if you did not get a response after 1 day) feel free to open a dispute again with Cmd/Ctrl+o. You can also ask for additional help on the Haveno forum at [HYPERLINK:https://haveno.community]. portfolio.pending.openAgainDispute.button=เปิดข้อพิพาทอีกครั้ง portfolio.pending.openSupportTicket.headline=เปิดปุ่มช่วยเหลือ portfolio.pending.openSupportTicket.msg=Please use this function only in emergency cases if you don't see a \"Open support\" or \"Open dispute\" button.\n\nWhen you open a support ticket the trade will be interrupted and handled by a mediator or arbitrator. portfolio.pending.timeLockNotOver=You have to wait until ≈{0} ({1} more blocks) before you can open an arbitration dispute. portfolio.pending.error.depositTxNull=The deposit transaction is null. You cannot open a dispute without a valid deposit transaction. Please go to \"Settings/Network info\" and do a SPV resync.\n\nFor further help please contact the Haveno support channel at the Haveno Keybase team. portfolio.pending.mediationResult.error.depositTxNull=The deposit transaction is null. You can move the trade to failed trades. portfolio.pending.mediationResult.error.delayedPayoutTxNull=The delayed payout transaction is null. You can move the trade to failed trades. portfolio.pending.error.depositTxNotConfirmed=The deposit transaction is not confirmed. You can not open an arbitration dispute with an unconfirmed deposit transaction. Please wait until it is confirmed or go to \"Settings/Network info\" and do a SPV resync.\n\nFor further help please contact the Haveno support channel at the Haveno Keybase team. portfolio.pending.support.headline.getHelp=Need help? portfolio.pending.support.text.getHelp=If you have any problems you can try to contact the trade peer in the trade chat or ask the Haveno community at https://haveno.community. If your issue still isn't resolved, you can request more help from a mediator. portfolio.pending.support.button.getHelp=Open Trader Chat portfolio.pending.support.headline.halfPeriodOver=Check payment portfolio.pending.support.headline.periodOver=Trade period is over portfolio.pending.support.headline.depositTxMissing=การฝากธุรกรรมหายไป portfolio.pending.support.depositTxMissing=รายการฝากสำหรับการซื้อขายนี้หายไป กรุณาเปิดตั๋วสนับสนุนเพื่อติดต่อผู้ตัดสินเพื่อขอความช่วยเหลือ portfolio.pending.mediationRequested=Mediation requested portfolio.pending.refundRequested=Refund requested portfolio.pending.openSupport=เปิดปุ่มช่วยเหลือ portfolio.pending.supportTicketOpened=ปุ่มช่วยเหลือถูกเปิดแล้ว portfolio.pending.communicateWithArbitrator=กรุณาติดต่อโดยไปที่ \"ช่วยเหลือและสนับสนุน \" กับผู้ไกล่เกลี่ย portfolio.pending.communicateWithMediator=Please communicate in the \"Support\" screen with the mediator. portfolio.pending.disputeOpenedByUser=คุณได้เปิดข้อพิพาทแล้ว\n{0} portfolio.pending.disputeOpenedByPeer=ผู้ร่วมการค้าของคุณได้เปิดประเด็นการอภิปรายขึ้น\n{0} portfolio.pending.noReceiverAddressDefined=ไม่ได้ระบุที่อยู่ผู้รับ portfolio.pending.mediationResult.headline=Suggested payout from mediation portfolio.pending.mediationResult.info.noneAccepted=Complete the trade by accepting the mediator's suggestion for the trade payout. portfolio.pending.mediationResult.info.selfAccepted=You have accepted the mediator's suggestion. Waiting for peer to accept as well. portfolio.pending.mediationResult.info.peerAccepted=Your trade peer has accepted the mediator's suggestion. Do you accept as well? portfolio.pending.mediationResult.button=View proposed resolution portfolio.pending.mediationResult.popup.headline=Mediation result for trade with ID: {0} portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator's suggestion for trade {0} portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader's security deposit) as compensation for their work. Both traders agreeing to the mediator's suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator's suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. portfolio.pending.failedTrade.missingDepositTx=ธุรกรรมเงินมัดจำหายไป\n\nธุรกรรมนี้จำเป็นสำหรับการดำเนินการซื้อขายให้เสร็จสมบูรณ์ กรุณาตรวจสอบให้แน่ใจว่า Wallet ของคุณได้ซิงค์กับบล็อกเชน Monero อย่างสมบูรณ์แล้ว\n\nคุณสามารถย้ายการซื้อขายนี้ไปยังส่วน "การซื้อขายที่ล้มเหลว" เพื่อปิดการใช้งานได้ portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Move trade to failed trades portfolio.pending.failedTrade.warningIcon.tooltip=Click to open details about the issues of this trade portfolio.failed.revertToPending.popup=Do you want to move this trade to open trades? portfolio.failed.revertToPending=Move trade to open trades portfolio.closed.completed=เสร็จสิ้น portfolio.closed.ticketClosed=Arbitrated portfolio.closed.mediationTicketClosed=Mediated portfolio.closed.canceled=ยกเลิกแล้ว portfolio.failed.Failed=ผิดพลาด portfolio.failed.unfail=Before proceeding, make sure you have a backup of your data directory!\nDo you want to move this trade back to open trades?\nThis is a way to unlock funds stuck in a failed trade. portfolio.failed.cantUnfail=This trade cannot be moved back to open trades at the moment. \nTry again after completion of trade(s) {0} portfolio.failed.depositTxNull=The trade cannot be reverted to a open trade. Deposit transaction is null. portfolio.failed.delayedPayoutTxNull=The trade cannot be reverted to a open trade. Delayed payout transaction is null. #################################################################### # Funds #################################################################### funds.tab.deposit=รับเงิน funds.tab.withdrawal=ส่งเงิน funds.tab.reserved=เงินที่ถูกจองไว้ funds.tab.locked=เงินที่ถูกล็อคไว้ funds.tab.transactions=การทำธุรกรรม funds.deposit.unused=ไม่ได้ใช้ funds.deposit.usedInTx=ใช้ใน {0} ธุรกรรม(ต่าง ๆ ) funds.deposit.fundHavenoWallet=เติมเงิน Haveno wallet funds.deposit.noAddresses=ยังไม่มีการสร้างที่อยู่ของเงินฝาก funds.deposit.fundWallet=เติมเงินใน wallet ของคุณ funds.deposit.withdrawFromWallet=ส่งเงินทุนจากกระเป๋าสตางค์ของคุณ funds.deposit.amount=จำนวนเงินใน XMR (ตัวเลือก) funds.deposit.generateAddress=สร้างที่อยู่ใหม่ funds.deposit.generateAddressSegwit=Native segwit format (Bech32) funds.deposit.selectUnused=โปรดเลือกที่อยู่ที่ไม่ได้ใช้จากตารางด้านบนแทนที่จะสร้างที่อยู่ใหม่ funds.withdrawal.arbitrationFee=ค่าธรรมเนียมอนุญาโตตุลาการ funds.withdrawal.inputs=การคัดเลือกปัจจัยการนำเข้า funds.withdrawal.useAllInputs=ใช้ปัจจัยการนำเข้าที่มีอยู่ทั้งหมด funds.withdrawal.useCustomInputs=ใช้ปัจจัยการนำเข้าที่กำหนดเอง funds.withdrawal.receiverAmount=จำนวนของผู้รับ funds.withdrawal.senderAmount=จำนวนของผู้ส่ง funds.withdrawal.feeExcluded=จำนวนเงินไม่รวมค่าธรรมเนียมการขุด funds.withdrawal.feeIncluded=จำนวนเงินรวมค่าธรรมเนียมการขุด funds.withdrawal.fromLabel=ถอนจากที่อยู่ funds.withdrawal.toLabel=ถอนไปยังที่อยู่ funds.withdrawal.memoLabel=Withdrawal memo funds.withdrawal.memo=Optionally fill memo funds.withdrawal.withdrawButton=การถอนที่ถูกเลือก funds.withdrawal.noFundsAvailable=ไม่มีเงินที่ใช้ถอนได้ funds.withdrawal.confirmWithdrawalRequest=ยืนยันคำขอถอนเงิน funds.withdrawal.withdrawMultipleAddresses=ถอนจากที่อยู่หลายแห่ง ({0}) funds.withdrawal.withdrawMultipleAddresses.tooltip=ถอนจากที่อยู่หลายแห่ง \n{0} funds.withdrawal.notEnoughFunds=คุณมีเงินไม่เพียงพอใน wallet ของคุณ funds.withdrawal.selectAddress=เลือกแหล่งที่อยู่จากตาราง funds.withdrawal.setAmount=กำหนดจำนวนที่จะถอน funds.withdrawal.fillDestAddress=กรอกที่อยู่ปลายทางของคุณ funds.withdrawal.warn.noSourceAddressSelected=คุณต้องเลือกแหล่งที่อยู่ในตารางด้านบน funds.withdrawal.warn.amountExceeds=คุณมีเงินไม่เพียงพอจากที่อยู่ที่คุณเลือก\nพิจารณาเลือกที่อยู่หลายแห่งในตารางด้านบนหรือเปลี่ยนปรับค่าธรรมเนียมที่รวมค่าธรรมเนียมของผู้ขุด funds.reserved.noFunds=ไม่มีเงินสำรองในข้อเสนอแบบเปิด funds.reserved.reserved=สำรองใน wallet ท้องถิ่นเพื่อข้อเสนอด้วย ID: {0} funds.locked.noFunds=ไม่มีเงินถูกล็อคในการซื้อขาย funds.locked.locked=ถูกล็อคใน multisig สำหรับการซื้อขายด้วย ID: {0} funds.tx.direction.sentTo=ส่งไปยัง: funds.tx.direction.receivedWith=ได้รับโดย: funds.tx.direction.genesisTx=จากการทำธุรกรรมทั่วไป : funds.tx.createOfferFee=ผู้สร้างและค่าธรรมเนียมการทำธุรกรรม: {0} funds.tx.takeOfferFee=ค่าธรรมเนียมของผู้รับและการทำธุรกรรม: {0} funds.tx.multiSigDeposit=เงินฝาก Multisig (การรองรับหลายลายเซ็น): {0} funds.tx.multiSigPayout=การจ่ายเงิน Multisig (การรองรับหลายลายเซ็น): {0} funds.tx.disputePayout=การจ่ายเงินข้อพิพาท: {0} funds.tx.disputeLost=กรณีการสูญเสียข้อพิพาท: {0} funds.tx.collateralForRefund=Refund collateral: {0} funds.tx.timeLockedPayoutTx=Time locked payout tx: {0} funds.tx.refund=Refund from arbitration: {0} funds.tx.unknown=เหตุผลที่ไม่ระบุ: {0} funds.tx.noFundsFromDispute=ไม่มีการคืนเงินจากการพิพาท funds.tx.receivedFunds=เงินที่ได้รับ funds.tx.withdrawnFromWallet=ถอนออกจาก wallet funds.tx.memo=Memo funds.tx.noTxAvailable=ไม่มีธุรกรรมใด ๆ funds.tx.revert=กลับสู่สภาพเดิม funds.tx.txSent=ธุรกรรมถูกส่งสำเร็จไปยังที่อยู่ใหม่ใน Haveno wallet ท้องถิ่นแล้ว funds.tx.direction.self=ส่งถึงตัวคุณเอง funds.tx.dustAttackTx=Received dust funds.tx.dustAttackTx.popup=This transaction is sending a very small XMR amount to your wallet and might be an attempt from chain analysis companies to spy on your wallet.\n\nIf you use that transaction output in a spending transaction they will learn that you are likely the owner of the other address as well (coin merge).\n\nTo protect your privacy the Haveno wallet ignores such dust outputs for spending purposes and in the balance display. You can set the threshold amount when an output is considered dust in the settings. #################################################################### # Support #################################################################### support.tab.mediation.support=Mediation support.tab.arbitration.support=Arbitration support.tab.legacyArbitration.support=Legacy Arbitration support.tab.ArbitratorsSupportTickets={0}'s tickets support.sigCheck.button=Check signature support.sigCheck.popup.header=Verify dispute result signature support.sigCheck.popup.msg.label=Summary message support.sigCheck.popup.msg.prompt=Copy & paste summary message from dispute support.sigCheck.popup.result=Validation result support.sigCheck.popup.success=Signature is valid support.sigCheck.popup.failed=Signature verification failed support.sigCheck.popup.invalidFormat=Message is not of expected format. Copy & paste summary message from dispute. support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? support.reOpenButton.label=Re-open support.sendNotificationButton.label=การแจ้งเตือนส่วนตัว support.reportButton.label=Report support.fullReportButton.label=All disputes support.noTickets=ไม่มีการเปิดรับคำขอร้องหรือความช่วยเหลือ support.sendingMessage=กำลังส่งข้อความ... support.receiverNotOnline=Receiver is not online. Message is saved to their mailbox. support.sendMessageError=การส่งข้อความล้มเหลว ข้อผิดพลาด: {0} support.receiverNotKnown=Receiver not known support.wrongVersion=ข้อเสนอในข้อพิพาทดังกล่าวได้รับการสร้างขึ้นโดยใช้ Haveno เวอร์ชั่นเก่ากว่า\nคุณไม่สามารถปิดข้อพิพาทดังกล่าวกับแอปพลิเคชั่นเวอร์ชั่นของคุณได้\n\nโปรดใช้เวอร์ชันที่เก่ากว่ากับเวอร์ชั่นโปรโตคอล {0} support.openFile=เปิดไฟล์ที่จะแนบ (ขนาดไฟล์สูงสุด: {0} kb) support.attachmentTooLarge=ขนาดไฟล์แนบทั้งหมด {0} กิโลไบต์และเกินจำนวนไฟล์สูงสุด ขนาดข้อความที่อนุญาตเท่ากับ {1} kB support.maxSize=ขนาดไฟล์สูงสุดที่อนุญาตคือ {0} kB support.attachment=แนบไฟล์ support.tooManyAttachments=คุณไม่สามารถส่งไฟล์แนบได้มากกว่า 3 ไฟล์ในข้อความเดียว support.save=บันทึกไฟล์ลงในดิสก์ support.messages=ข้อความ support.input.prompt=Enter message... support.send=ส่ง support.addAttachments=เพิ่มไฟล์แนบ support.closeTicket=ยุติคำร้องขอและความช่วยเหลือ support.attachments=ไฟล์ที่แนบมา: support.savedInMailbox=ข้อความถูกบันทึกไว้ในกล่องจดหมายของผู้รับ support.arrived=ข้อความถึงผู้รับแล้ว support.acknowledged=ข้อความได้รับการยืนยันจากผู้รับแล้ว support.error=ผู้รับไม่สามารถประมวลผลข้อความได้ ข้อผิดพลาด: {0} support.buyerAddress=ที่อยู่ของผู้ซื้อ XMR support.sellerAddress=ที่อยู่ของผู้ขาย XMR support.role=บทบาท support.agent=Support agent support.state=สถานะ support.chat=Chat support.preparing=กำลังเตรียม support.requested=ร้องขอ support.closed=ปิดแล้ว support.open=เปิด support.process=Process support.buyerMaker=XMR ผู้ซื้อ / ผู้สร้าง support.sellerMaker= XMR ผู้ขาย/ ผู้สร้าง support.buyerTaker=XMR ผู้ซื้อ / ผู้รับ support.sellerTaker=XMR ผู้ขาย / ผู้รับ support.initialInfo=Please enter a description of your problem in the text field below. Add as much information as possible to speed up dispute resolution time.\n\nHere is a check list for information you should provide:\n\t● If you are the XMR buyer: Did you make the Fiat or Crypto transfer? If so, did you click the 'payment started' button in the application?\n\t● If you are the XMR seller: Did you receive the Fiat or Crypto payment? If so, did you click the 'payment received' button in the application?\n\t● Which version of Haveno are you using?\n\t● Which operating system are you using?\n\t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\t Sometimes the data directory gets corrupted and leads to strange bugs. \n\t See: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nPlease make yourself familiar with the basic rules for the dispute process:\n\t● You need to respond to the {0}'s requests within 2 days.\n\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\t● The maximum period for a dispute is 14 days.\n\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\nYou can read more about the dispute process at: {2} support.systemMsg=ระบบข้อความ: {0} support.youOpenedTicket=You opened a request for support.\n\n{0}\n\nHaveno version: {1} support.youOpenedDispute=You opened a request for a dispute.\n\n{0}\n\nHaveno version: {1} support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nHaveno version: {1} support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nHaveno version: {1} support.mediatorsDisputeSummary=System message: Mediator's dispute summary:\n{0} support.mediatorsAddress=Mediator's node address: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. support.info.disputeReOpened=Dispute ticket has been re-opened. #################################################################### # Settings #################################################################### settings.tab.preferences=การตั้งค่า settings.tab.network=ข้อมูลเครือข่าย settings.tab.about=เกี่ยวกับ setting.preferences.general=การตั้งค่าทั่วไป setting.preferences.explorer=Monero Explorer setting.preferences.deviation=สูงสุด ส่วนเบี่ยงเบนจากราคาตลาด setting.preferences.avoidStandbyMode=หลีกเลี่ยงโหมดแสตนบายด์ setting.preferences.useSoundForNotifications=เล่นเสียงสำหรับการแจ้งเตือน setting.preferences.autoConfirmXMR=XMR auto-confirm setting.preferences.autoConfirmEnabled=Enabled setting.preferences.autoConfirmRequiredConfirmations=Required confirmations setting.preferences.autoConfirmMaxTradeSize=Max. trade amount (XMR) setting.preferences.autoConfirmServiceAddresses=Monero Explorer URLs (uses Tor, except for localhost, LAN IP addresses, and *.local hostnames) setting.preferences.deviationToLarge=ค่าที่สูงกว่า {0}% ไม่ได้รับอนุญาต setting.preferences.txFee=Withdrawal transaction fee (satoshis/vbyte) setting.preferences.useCustomValue=ใช้ค่าที่กำหนดเอง setting.preferences.txFeeMin=Transaction fee must be at least {0} satoshis/vbyte setting.preferences.txFeeTooLarge=Your input is above any reasonable value (>5000 satoshis/vbyte). Transaction fee is usually in the range of 50-400 satoshis/vbyte. setting.preferences.ignorePeers=Ignored peers [onion address:port] setting.preferences.ignoreDustThreshold=Min. non-dust output value setting.preferences.currenciesInList=สกุลเงินอยู่ในหน้ารายการราคาตลาด setting.preferences.prefCurrency=สกุลเงินที่ต้องการ setting.preferences.displayTraditional=แสดงสกุลเงินของประเทศ setting.preferences.noTraditional=ไม่มีสกุลเงินประจำชาติที่เลือกไว้ setting.preferences.cannotRemovePrefCurrency=คุณไม่สามารถลบสกุลเงินในการแสดงผลที่เลือกไว้ได้ setting.preferences.displayCryptos=แสดง cryptos setting.preferences.noCryptos=ไม่มี cryptos ที่เลือก setting.preferences.addTraditional=เพิ่มสกุลเงินประจำชาติ setting.preferences.addCrypto=เพิ่ม crypto setting.preferences.displayOptions=แสดงตัวเลือกเพิ่มเติม setting.preferences.showOwnOffers=แสดงข้อเสนอของฉันเองในสมุดข้อเสนอ setting.preferences.useAnimations=ใช้ภาพเคลื่อนไหว setting.preferences.useDarkMode=ใช้โหมดมืด setting.preferences.useLightMode=ใช้โหมดสว่าง setting.preferences.sortWithNumOffers=จัดเรียงรายการโดยเลขของข้อเสนอ / การซื้อขาย setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API setting.preferences.notifyOnPreRelease=Receive pre-release notifications setting.preferences.resetAllFlags=รีเซ็ตทั้งหมด \"ไม่ต้องแสดงอีกครั้ง \" ปักธง settings.preferences.languageChange=หากต้องการเปลี่ยนภาษากับทุกหน้าต้องทำการรีสตาร์ท settings.preferences.supportLanguageWarning=In case of a dispute, please note that arbitration is handled in {0}. settings.preferences.editCustomExplorer.headline=Explorer Settings settings.preferences.editCustomExplorer.description=Choose a system defined explorer from the list on the left, and/or customize to suit your own preferences. settings.preferences.editCustomExplorer.available=Available explorers settings.preferences.editCustomExplorer.chosen=Chosen explorer settings settings.preferences.editCustomExplorer.name=ชื่อ settings.preferences.editCustomExplorer.txUrl=Transaction URL settings.preferences.editCustomExplorer.addressUrl=Address URL setting.info.headline=คุณสมบัติความเป็นส่วนตัวข้อมูลใหม่ settings.preferences.sensitiveDataRemoval.msg=เพื่อปกป้องความเป็นส่วนตัวของคุณและผู้ซื้อขายอื่นๆ Haveno ตั้งใจที่จะลบข้อมูลที่ละเอียดอ่อนจากการซื้อขายเก่า ซึ่งมีความสำคัญอย่างยิ่งสำหรับการซื้อขายฟิอาทที่อาจมีรายละเอียดบัญชีธนาคาร\n\nแนะนำให้ตั้งค่านี้ให้ต่ำที่สุดเท่าที่จะเป็นไปได้ เช่น 60 วัน ซึ่งหมายความว่าการซื้อขายที่เกิน 60 วันจะถูกลบข้อมูลที่ละเอียดอ่อน ตราบใดที่การซื้อขายนั้นเสร็จสมบูรณ์ การซื้อขายที่เสร็จสมบูรณ์สามารถดูได้ในแท็บพอร์ตโฟลิโอ / ประวัติ settings.net.xmrHeader=เครือข่าย Monero settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=ที่อยู่ onion ของฉัน settings.net.xmrNodesLabel=ใช้โหนดเครือข่าย Monero ที่กำหนดเอง settings.net.moneroPeersLabel=เชื่อมต่อกับเน็ตเวิร์ก peers แล้ว settings.net.connection=การเชื่อมต่อ settings.net.connected=เชื่อมต่อ settings.net.useTorForXmrJLabel=ใช้ Tor สำหรับเครือข่าย Monero settings.net.moneroNodesLabel=ใช้โหนดเครือข่าย Monero เพื่อเชื่อมต่อ settings.net.useProvidedNodesRadio=ใช้โหนดเครือข่าย Monero ที่ให้มา settings.net.usePublicNodesRadio=ใช้เครือข่าย Monero สาธารณะ settings.net.useCustomNodesRadio=ใช้โหนดเครือข่าย Monero Core ที่กำหนดเอง settings.net.warn.usePublicNodes=If you use public Monero nodes, you are subject to any risk of using untrusted remote nodes.\n\nPlease read more details at [HYPERLINK:https://www.getmonero.org/resources/moneropedia/remote-node.html].\n\nAre you sure you want to use public nodes? settings.net.warn.usePublicNodes.useProvided=ไม่ ใช้โหนดที่ให้มา settings.net.warn.usePublicNodes.usePublic=ใช่ ใช้เครือข่ายสาธารณะ settings.net.warn.useCustomNodes.B2XWarning=โปรดตรวจสอบว่าโหนด Monero ของคุณเป็นโหนด Monero Core ที่เชื่อถือได้!\n\nการเชื่อมต่อกับโหนดที่ไม่ปฏิบัติตามกฎกติกาการยินยอมของ Monero Core อาจทำให้ wallet ของคุณเกิดปัญหาในกระบวนการทางการซื้อขายได้\n\nผู้ใช้ที่เชื่อมต่อกับโหนดที่ละเมิดกฎเป็นเอกฉันท์นั้นจำเป็นต้องรับผิดชอบต่อความเสียหายที่สร้างขึ้น ข้อพิพาทที่เกิดจากการที่จะได้รับการตัดสินใจจาก เน็ตกเวิร์ก Peer คนอื่น ๆ จะไม่มีการสนับสนุนด้านเทคนิคแก่ผู้ใช้ที่ไม่สนใจคำเตือนและกลไกการป้องกันของเรา! settings.net.warn.invalidXmrConfig=Connection to the Monero network failed because your configuration is invalid.\n\nYour configuration has been reset to use the provided Monero nodes instead. You will need to restart the application. settings.net.localhostXmrNodeInfo=Background information: Haveno looks for a local Monero node when starting. If it is found, Haveno will communicate with the Monero network exclusively through it. settings.net.p2PPeersLabel=เชื่อมต่อกับเน็ตเวิร์ก peers แล้ว settings.net.onionAddressColumn=ที่อยู่ Onion settings.net.creationDateColumn=ที่จัดตั้งขึ้น settings.net.connectionTypeColumn=เข้า/ออก settings.net.sentDataLabel=Sent data statistics settings.net.receivedDataLabel=Received data statistics settings.net.chainHeightLabel=Latest XMR block height settings.net.roundTripTimeColumn=ไป - กลับ settings.net.sentBytesColumn=ส่งแล้ว settings.net.receivedBytesColumn=ได้รับแล้ว settings.net.peerTypeColumn=ประเภทเน็ตเวิร์ก peer settings.net.openTorSettingsButton=เปิดการตั้งค่าของ Tor settings.net.versionColumn=Version settings.net.subVersionColumn=Subversion settings.net.heightColumn=Height settings.net.needRestart=คุณต้องรีสตาร์ทแอ็พพลิเคชั่นเพื่อทำให้การเปลี่ยนแปลงนั้นเป็นผล\nคุณต้องการทำตอนนี้หรือไม่ settings.net.notKnownYet=ยังไม่ทราบ ... settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec settings.net.chainHeight=Monero Peers chain height: {0} settings.net.ips=[ที่อยู่ IP: พอร์ต | ชื่อโฮสต์: พอร์ต | ที่อยู่ onion: พอร์ต] (คั่นด้วยเครื่องหมายจุลภาค) Port สามารถละเว้นได้ถ้าใช้ค่าเริ่มต้น (8333) settings.net.seedNode=แหล่งโหนดข้อมูล settings.net.directPeer=Peer (โดยตรง) settings.net.initialDataExchange={0} [Bootstrapping] settings.net.peer=เน็ตเวิร์ก Peer settings.net.inbound=ขาเข้า settings.net.outbound=ขาออก setting.about.aboutHaveno=เกี่ยวกับ Haveno setting.about.about=Haveno เป็นโครงการอิสระโดยอำนวยความสะดวกในการแลกเงินบิตคอยน์กับสกุลเงินของประเทศต่างๆ (และสกุลเงินดิจิตอลอื่นๆ) ผ่านเครือข่ายเข้าถึงกระจายอำนาจอย่างถัดเทียมในรูปแบบการปกป้องข้อมูลส่วนบุคคล เรียนรู้เพิ่มเติมเกี่ยวกับ Haveno ในหน้าเว็บของโปรเจคของเรา setting.about.web=หน้าเว็บ Haveno setting.about.code=โค๊ดแหล่งที่มา setting.about.agpl=ใบอนุญาต AGPL setting.about.support=สนับสนุน Haveno setting.about.def=Haveno ไม่ใช่ บริษัท แต่เป็นโปรเจคชุมชนและเปิดให้คนมีส่วนร่วม ถ้าคุณต้องการจะเข้าร่วมหรือสนับสนุน Haveno โปรดทำตามลิงค์ข้างล่างนี้ setting.about.contribute=สนับสนุน setting.about.providers=ผู้ให้บริการข้อมูล setting.about.apisWithFee=Haveno uses Haveno Price Indices for Fiat and Crypto market prices, and Haveno Mempool Nodes for mining fee estimation. setting.about.apis=Haveno uses Haveno Price Indices for Fiat and Crypto market prices. setting.about.pricesProvided=ราคาตลาดจัดโดย setting.about.feeEstimation.label=การประมาณค่าธรรมเนียมการขุดโดย setting.about.versionDetails=รายละเอียดของเวอร์ชั่น setting.about.version=เวอร์ชั่นของแอปพลิเคชั่น setting.about.subsystems.label=เวอร์ชั่นของระบบย่อย setting.about.subsystems.val=เวอร์ชั่นของเครือข่าย: {0}; เวอร์ชั่นข้อความ P2P: {1}; เวอร์ชั่นฐานข้อมูลท้องถิ่น: {2}; เวอร์ชั่นโปรโตคอลการซื้อขาย: {3} setting.about.shortcuts=Short cuts setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' or 'alt + {0}' or 'cmd + {0}' setting.about.shortcuts.menuNav=Navigate main menu setting.about.shortcuts.menuNav.value=To navigate the main menu press: 'Ctrl' or 'alt' or 'cmd' with a numeric key between '1-9' setting.about.shortcuts.close=Close Haveno setting.about.shortcuts.close.value='Ctrl + {0}' or 'cmd + {0}' or 'Ctrl + {1}' or 'cmd + {1}' setting.about.shortcuts.closePopup=Close popup or dialog window setting.about.shortcuts.closePopup.value='ESCAPE' key setting.about.shortcuts.chatSendMsg=Send trader chat message setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' or 'alt + ENTER' or 'cmd + ENTER' setting.about.shortcuts.openDispute=Open dispute setting.about.shortcuts.openDispute.value=Select pending trade and click: {0} setting.about.shortcuts.walletDetails=Open wallet details window setting.about.shortcuts.openEmergencyXmrWalletTool=Open emergency wallet tool for XMR wallet setting.about.shortcuts.showTorLogs=Toggle log level for Tor messages between DEBUG and WARN setting.about.shortcuts.manualPayoutTxWindow=Open window for manual payout from 2of2 Multisig deposit tx setting.about.shortcuts.removeStuckTrade=Open popup to move failed trade to open trades tab again setting.about.shortcuts.removeStuckTrade.value=Select failed trade and press: {0} setting.about.shortcuts.registerArbitrator=Register arbitrator (mediator/arbitrator only) setting.about.shortcuts.registerArbitrator.value=Navigate to account and press: {0} setting.about.shortcuts.registerMediator=Register mediator (mediator/arbitrator only) setting.about.shortcuts.registerMediator.value=Navigate to account and press: {0} setting.about.shortcuts.openSignPaymentAccountsWindow=Open window for account age signing (legacy arbitrators only) setting.about.shortcuts.openSignPaymentAccountsWindow.value=Navigate to legacy arbitrator view and press: {0} setting.about.shortcuts.sendAlertMsg=Send alert or update message (privileged activity) setting.about.shortcuts.sendFilter=Set Filter (privileged activity) setting.about.shortcuts.sendPrivateNotification=Send private notification to peer (privileged activity) setting.about.shortcuts.sendPrivateNotification.value=Open peer info at avatar and press: {0} setting.info.headline=New XMR auto-confirm Feature setting.info.msg=When selling XMR for XMR you can use the auto-confirm feature to verify that the correct amount of XMR was sent to your wallet so that Haveno can automatically mark the trade as complete, making trades quicker for everyone.\n\nAuto-confirm checks the XMR transaction on at least 2 XMR explorer nodes using the private transaction key provided by the XMR sender. By default, Haveno uses explorer nodes run by Haveno contributors, but we recommend running your own XMR explorer node for maximum privacy and security.\n\nYou can also set the maximum amount of XMR per trade to auto-confirm as well as the number of required confirmations here in Settings.\n\nSee more details (including how to set up your own explorer node) on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades] #################################################################### # Account #################################################################### account.tab.mediatorRegistration=Mediator registration account.tab.refundAgentRegistration=Refund agent registration account.tab.signing=Signing account.info.headline=ยินดีต้อนรับสู่บัญชี Haveno ของคุณ account.info.msg=Here you can add trading accounts for national currencies & cryptos and create a backup of your wallet & account data.\n\nA new Monero wallet was created the first time you started Haveno.\n\nWe strongly recommend that you write down your Monero wallet seed words (see tab on the top) and consider adding a password before funding. Monero deposits and withdrawals are managed in the \"Funds\" section.\n\nPrivacy & security note: because Haveno is a decentralized exchange, all your data is kept on your computer. There are no servers, so we have no access to your personal info, your funds, or even your IP address. Data such as bank account numbers, crypto & Monero addresses, etc are only shared with your trading partner to fulfill trades you initiate (in case of a dispute the mediator or arbitrator will see the same data as your trading peer). account.menu.paymentAccount=บัญชีสกุลเงินของประเทศ account.menu.altCoinsAccountView=บัญชี Crypto (เหรียญทางเลือก) account.menu.password=รหัส Wallet account.menu.seedWords=รหัสลับ Wallet account.menu.walletInfo=Wallet info account.menu.backup=การสำรองข้อมูล account.menu.notifications=การแจ้งเตือน account.menu.walletInfo.balance.headLine=Wallet balances account.menu.walletInfo.balance.info=This shows the internal wallet balance including unconfirmed transactions.\nFor XMR, the internal wallet balance shown below should match the sum of the 'Available' and 'Reserved' balances shown in the top right of this window. account.menu.walletInfo.xpub.headLine=Watch keys (xpub keys) account.menu.walletInfo.walletSelector={0} {1} wallet account.menu.walletInfo.path.headLine=HD keychain paths account.menu.walletInfo.path.info=If you import seed words into another wallet (like Electrum), you'll need to define the path. This should only be done in emergency cases when you lose access to the Haveno wallet and data directory.\nKeep in mind that spending funds from a non-Haveno wallet can bungle the internal Haveno data structures associated with the wallet data, which can lead to failed trades.\n\nNEVER send BSQ from a non-Haveno wallet, as it will probably lead to an invalid BSQ transaction and losing your BSQ. account.menu.walletInfo.openDetails=Show raw wallet details and private keys ## TODO should we rename the following to a gereric name? account.arbitratorRegistration.pubKey=กุญแจสาธารณะ account.arbitratorRegistration.register=Register account.arbitratorRegistration.registration={0} registration account.arbitratorRegistration.revoke=เพิกถอน account.arbitratorRegistration.info.msg=Please note that you need to stay available for 15 days after revoking as there might be trades which are using you as {0}. The max. allowed trade period is 8 days and the dispute process might take up to 7 days. account.arbitratorRegistration.warn.min1Language=คุณต้องตั้งค่าภาษาอย่างน้อย 1 ภาษา\nเราได้เพิ่มภาษาเริ่มต้นให้กับคุณแล้ว account.arbitratorRegistration.removedSuccess=You have successfully removed your registration from the Haveno network. account.arbitratorRegistration.removedFailed=Could not remove registration.{0} account.arbitratorRegistration.registerSuccess=You have successfully registered to the Haveno network. account.arbitratorRegistration.registerFailed=Could not complete registration.{0} account.crypto.yourCryptoAccounts=บัญชี crypto (เหรียญทางเลือก) ของคุณ account.crypto.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don't control your keys or (b) which don't use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is not a {2} specialist and cannot help in such cases. account.crypto.popup.wallet.confirm=ฉันเข้าใจและยืนยันว่าฉันรู้ว่า wallet ใดที่ฉันต้องการใช้ # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=Trading UPX on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending UPX, you need to use either the official uPlexa GUI wallet or uPlexa CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\nuplexa-wallet-cli (use the command get_tx_key)\nuplexa-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The UPX sender is responsible for providing verification of the UPX transfer to the arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit uPlexa discord channel (https://discord.gg/vhdNSrV) or the uPlexa Telegram Chat (https://t.me/uplexaOfficial) to find more information. # suppress inspection "UnusedProperty" account.crypto.popup.arq.msg=Trading ARQ on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending ARQ, you need to use either the official ArQmA GUI wallet or ArQmA CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\narqma-wallet-cli (use the command get_tx_key)\narqma-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the mediator or arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The ARQ sender is responsible for providing verification of the ARQ transfer to the mediator or arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit ArQmA discord channel (https://discord.gg/s9BQpJT) or the ArQmA forum (https://labs.arqma.com) to find more information. # suppress inspection "UnusedProperty" account.crypto.popup.xmr.msg=Trading XMR on Haveno requires that you understand the following requirement.\n\nIf selling XMR, you must be able to provide the following information to a mediator or arbitrator in case of a dispute:\n- the transaction key (Tx Key, Tx Secret Key or Tx Private Key)\n- the transaction ID (Tx ID or Tx Hash)\n- the destination address (recipient's address)\n\nSee the wiki for details on where to find this information on popular Monero wallets [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Proving_payments].\nFailure to provide the required transaction data will result in losing disputes.\n\nAlso note that Haveno now offers automatic confirming for XMR transactions to make trades quicker, but you need to enable it in Settings.\n\nSee the wiki for more information about the auto-confirm feature: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. # suppress inspection "UnusedProperty" account.crypto.popup.msr.msg=Trading MSR on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending MSR, you need to use either the official Masari GUI wallet, Masari CLI wallet with the store-tx-info flag enabled (enabled by default) or the Masari web wallet (https://wallet.getmasari.org). Please be sure you can access the tx key as that would be required in case of a dispute.\nmasari-wallet-cli (use the command get_tx_key)\nmasari-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nMasari Web Wallet (goto Account -> transaction history and view details on your sent transaction)\n\nVerification can be accomplished in-wallet.\nmasari-wallet-cli : using command (check_tx_key).\nmasari-wallet-gui : on the Advanced > Prove/Check page.\nVerification can be accomplished in the block explorer \nOpen block explorer (https://explorer.getmasari.org), use the search bar to find your transaction hash.\nOnce transaction is found, scroll to bottom to the 'Prove Sending' area and fill in details as needed.\nYou need to provide the mediator or arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The MSR sender is responsible for providing verification of the MSR transfer to the mediator or arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process, ask for help on the Official Masari Discord (https://discord.gg/sMCwMqs). # suppress inspection "UnusedProperty" account.crypto.popup.blur.msg=Trading BLUR on Haveno requires that you understand and fulfill the following requirements:\n\nTo send BLUR you must use the Blur Network CLI or GUI Wallet. \n\nIf you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\nIf you are using the Blur Network GUI Wallet, the transaction private key and transaction ID can be found conveniently in the "History" tab. Immediately after sending, locate the transaction of interest. Click the "?" symbol in the lower-right corner of the box containing the transaction. You must save this information. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the BLUR transfer using the Blur Transaction Viewer (https://blur.cash/#tx-viewer).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the BLUR sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the Blur Network Discord (https://discord.gg/dMWaqVW). # suppress inspection "UnusedProperty" account.crypto.popup.solo.msg=Trading Solo on Haveno requires that you understand and fulfill the following requirements:\n\nTo send Solo you must use the Solo Network CLI Wallet. \n\nIf you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the Solo transfer using the Solo Block Explorer by searching for the transaction and then using the "Prove sending" function (https://explorer.minesolo.com/).\n\nfailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the Solo sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the Solo Network Discord (https://discord.minesolo.com/). # suppress inspection "UnusedProperty" account.crypto.popup.cash2.msg=Trading CASH2 on Haveno requires that you understand and fulfill the following requirements:\n\nTo send CASH2 you must use the Cash2 Wallet version 3 or higher. \n\nAfter a transaction is sent, the transaction ID will be displayed. You must save this information. Immediately after sending the transaction, you must use the command 'getTxKey' in simplewallet to retrieve the transaction secret key. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, 2) the transaction secret key, and 3) the recipient's Cash2 address. The mediator or arbitrator will then verify the CASH2 transfer using the Cash2 Block Explorer (https://blocks.cash2.org).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the CASH2 sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the Cash2 Discord (https://discord.gg/FGfXAYN). # suppress inspection "UnusedProperty" account.crypto.popup.qwertycoin.msg=Trading Qwertycoin on Haveno requires that you understand and fulfill the following requirements:\n\nTo send QWC you must use the official QWC Wallet version 5.1.3 or higher. \n\nAfter a transaction is sent, the transaction ID will be displayed. You must save this information. Immediately after sending the transaction, you must use the command 'get_Tx_Key' in simplewallet to retrieve the transaction secret key. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, 2) the transaction secret key, and 3) the recipient's QWC address. The mediator or arbitrator will then verify the QWC transfer using the QWC Block Explorer (https://explorer.qwertycoin.org).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the QWC sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the QWC Discord (https://discord.gg/rUkfnpC). # suppress inspection "UnusedProperty" account.crypto.popup.drgl.msg=Trading Dragonglass on Haveno requires that you understand and fulfill the following requirements:\n\nBecause of the privacy Dragonglass provides, a transaction is not verifiable on the public blockchain. If required, you can prove your payment through the use of your TXN-Private-Key.\nThe TXN-Private Key is a one-time key automatically generated for every transaction that can only be accessed from within your DRGL wallet.\nEither by DRGL-wallet GUI (inside transaction details dialog) or by the Dragonglass CLI simplewallet (using command "get_tx_key").\n\nDRGL version 'Oathkeeper' and higher are REQUIRED for both.\n\nIn case of a dispute, you must provide the mediator or arbitrator the following data:\n- The TXN-Private key\n- The transaction hash\n- The recipient's public address\n\nVerification of payment can be made using the above data as inputs at (http://drgl.info/#check_txn).\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The Dragonglass sender is responsible for providing verification of the DRGL transfer to the mediator or arbitrator in case of a dispute. Use of PaymentID is not required.\n\nIf you are unsure about any part of this process, visit Dragonglass on Discord (http://discord.drgl.info) for help. # suppress inspection "UnusedProperty" account.crypto.popup.ZEC.msg=When using Zcash you can only use the transparent addresses (starting with t), not the z-addresses (private), because the mediator or arbitrator would not be able to verify the transaction with z-addresses. # suppress inspection "UnusedProperty" account.crypto.popup.XZC.msg=When using Zcoin you can only use the transparent (traceable) addresses, not the untraceable addresses, because the mediator or arbitrator would not be able to verify the transaction with untraceable addresses at a block explorer. # suppress inspection "UnusedProperty" account.crypto.popup.grin.msg=GRIN requires an interactive process between the sender and receiver to create the transaction. Be sure to follow the instructions from the GRIN project web page to reliably send and receive GRIN (the receiver needs to be online or at least be online during a certain time frame). \n\nHaveno supports only the Grinbox (Wallet713) wallet URL format. \n\nThe GRIN sender is required to provide proof that they have sent GRIN successfully. If the wallet cannot provide that proof, a potential dispute will be resolved in favor of the GRIN receiver. Please be sure that you use the latest Grinbox software which supports the transaction proof and that you understand the process of transferring and receiving GRIN as well as how to create the proof. \n\nSee https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only for more information about the Grinbox proof tool. # suppress inspection "UnusedProperty" account.crypto.popup.beam.msg=BEAM requires an interactive process between the sender and receiver to create the transaction. \n\nBe sure to follow the instructions from the BEAM project web page to reliably send and receive BEAM (the receiver needs to be online or at least be online during a certain time frame). \n\nThe BEAM sender is required to provide proof that they sent BEAM successfully. Be sure to use wallet software which can produce such a proof. If the wallet cannot provide the proof a potential dispute will be resolved in favor of the BEAM receiver. # suppress inspection "UnusedProperty" account.crypto.popup.pars.msg=Trading ParsiCoin on Haveno requires that you understand and fulfill the following requirements:\n\nTo send PARS you must use the official ParsiCoin Wallet version 3.0.0 or higher. \n\nYou can Check your Transaction Hash and Transaction Key on Transactions Section on your GUI Wallet (ParsiPay) You need to right Click on the Transaction and then click on show details. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the Transaction Hash, 2) the Transaction Key, and 3) the recipient's PARS address. The mediator or arbitrator will then verify the PARS transfer using the ParsiCoin Block Explorer (http://explorer.parsicoin.net/#check_payment).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the ParsiCoin sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the ParsiCoin Discord (https://discord.gg/c7qmFNh). # suppress inspection "UnusedProperty" account.crypto.popup.blk-burnt.msg=To trade burnt blackcoins, you need to know the following:\n\nBurnt blackcoins are unspendable. To trade them on Haveno, output scripts need to be in the form: OP_RETURN OP_PUSHDATA, followed by associated data bytes which, after being hex-encoded, constitute addresses. For example, burnt blackcoins with an address 666f6f (“foo” in UTF-8) will have the following script:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\nTo create burnt blackcoins, one may use the “burn” RPC command available in some wallets.\n\nFor possible use cases, one may look at https://ibo.laboratorium.ee .\n\nAs burnt blackcoins are unspendable, they can not be reselled. “Selling” burnt blackcoins means burning ordinary blackcoins (with associated data equal to the destination address).\n\nIn case of a dispute, the BLK seller needs to provide the transaction hash. # suppress inspection "UnusedProperty" account.crypto.popup.liquidmonero.msg=Trading L-XMR on Haveno requires that you understand the following:\n\nWhen receiving L-XMR for a trade on Haveno, you cannot use the mobile Blockstream Green Wallet app or a custodial/exchange wallet. You must only receive L-XMR into the Liquid Elements Core wallet, or another L-XMR wallet which allows you to obtain the blinding key for your blinded L-XMR address.\n\nIn the event mediation is necessary, or if a trade dispute arises, you must disclose the blinding key for your receiving L-XMR address to the Haveno mediator or refund agent so they can verify the details of your Confidential Transaction on their own Elements Core full node.\n\nFailure to provide the required information to the mediator or refund agent will result in losing the dispute case. In all cases of dispute, the L-XMR receiver bears 100% of the burden of responsibility in providing cryptographic proof to the mediator or refund agent.\n\nIf you do not understand these requirements, do not trade L-XMR on Haveno. account.traditional.yourTraditionalAccounts=บัญชีสกุลเงินของคุณ account.backup.title=สำรองข้อมูล wallet account.backup.location=ที่ตั้งการสำรองข้อมูล account.backup.selectLocation=เลือกตำแหน่งการสำรอง account.backup.backupNow=สำรองข้อมูลตอนนี้ (สำรองข้อมูลไม่ได้เข้ารหัส!) account.backup.appDir=สารบบข้อมูลแอ็พพลิเคชั่น account.backup.openDirectory=เปิดสารบบ account.backup.openLogFile=เปิดการเข้าสู่ไฟล์ account.backup.success=สำรองข้อมูลสำเร็จแล้วบันทึกไว้ที่: \n{0} account.backup.directoryNotAccessible=สารบบที่คุณเลือกไม่สามารถเข้าถึงได้ {0} account.password.removePw.button=ลบรหัสผ่าน account.password.removePw.headline=ลบรหัสผ่านการป้องกันสำหรับ wallet account.password.setPw.button=ตั้งรหัสผ่าน account.password.setPw.headline=ตั้งรหัสผ่านการป้องกันสำหรับ wallet account.password.info=เมื่อเปิดใช้งานการป้องกันด้วยรหัสผ่าน คุณจะต้องป้อนรหัสผ่านของคุณที่จุดเริ่มต้นของแอปพลิเคชัน ขณะถอนเงินมอเนโรออกจากกระเป๋าของคุณ และเมื่อแสดง seed words ของคุณ account.seed.backup.title=สำรองคำพูดเมล็ดกระเป๋าของคุณ account.seed.info=โปรดบันทึกทั้งคำพูดเมล็ดกระเป๋าและวันที่ คุณสามารถกู้คืนกระเป๋าของคุณได้ทุกเมื่อด้วยคำพูดเมล็ดและวันที่นั้น\n\nคุณควรจะบันทึกคำพูดเมล็ดลงในกระดาษ อย่าบันทึกไว้ในคอมพิวเตอร์\n\nโปรดทราบว่าคำพูดเมล็ดนั้นไม่ใช่การแทนที่สำหรับการสำรองข้อมูล\nคุณต้องสร้างสำรองข้อมูลของไดเรกทอรีแอปพลิเคชันทั้งหมดจากหน้าจอ "บัญชี/สำรอง" เพื่อกู้คืนสถานะและข้อมูลของแอปพลิเคชัน account.seed.backup.warning=โปรดทราบว่าคำพูดเมล็ดนั้นไม่ใช่การแทนที่สำหรับการสำรองข้อมูล\nคุณต้องสร้างสำรองข้อมูลของไดเรกทอรีแอปพลิเคชันทั้งหมดจากหน้าจอ "บัญชี/สำรอง" เพื่อกู้คืนสถานะและข้อมูลของแอปพลิเคชัน account.seed.warn.noPw.msg=คุณยังไม่ได้ตั้งรหัสผ่าน wallet ซึ่งจะช่วยป้องกันการแสดงผลของรหัสสำรองข้อมูล wallet \n\nคุณต้องการแสดงรหัสสำรองข้อมูล wallet หรือไม่ account.seed.warn.noPw.yes=ใช่ และไม่ต้องถามฉันอีก account.seed.enterPw=ป้อนรหัสผ่านเพื่อดูรหัสสำรองข้อมูล wallet account.seed.restore.info=Please make a backup before applying restore from seed words. Be aware that wallet restore is only for emergency cases and might cause problems with the internal wallet database.\nIt is not a way for applying a backup! Please use a backup from the application data directory for restoring a previous application state.\n\nAfter restoring the application will shut down automatically. After you have restarted the application it will resync with the Monero network. This can take a while and can consume a lot of CPU, especially if the wallet was older and had many transactions. Please avoid interrupting that process, otherwise you might need to delete the SPV chain file again or repeat the restore process. account.seed.restore.ok=Ok, do the restore and shut down Haveno #################################################################### # Mobile notifications #################################################################### account.notifications.setup.title=ติดตั้ง account.notifications.download.label=ดาวน์โหลดแอปพลิเคชั่นบนมือถือ account.notifications.waitingForWebCam=กำลังเปิดกล้องเว็บแคม ... account.notifications.webCamWindow.headline=สแกน QR โค้ดจากโทรศัพท์ account.notifications.webcam.label=ใช้เว็บแคม account.notifications.webcam.button=สแกน QR โค้ด account.notifications.noWebcam.button=ฉันไม่มีเว็บแคม account.notifications.erase.label=ล้างการแจ้งเตือนบนโทรศัพท์ account.notifications.erase.title=ล้างการแจ้งเตือน account.notifications.email.label=การจับคู่โทเค็น account.notifications.email.prompt=ป้อนคู่โทเค็นที่คุณได้รับทางอีเมล์ account.notifications.settings.title=ตั้งค่า account.notifications.useSound.label=เปิดเสียงการแจ้งเตือนบนโทรศัพท์ account.notifications.trade.label=ได้รับข้อความทางการค้า account.notifications.market.label=ได้รับการแจ้งเตือนข้อเสนอ account.notifications.price.label=ได้รับการแจ้งเตือนราคา account.notifications.priceAlert.title=แจ้งเตือนราคา account.notifications.priceAlert.high.label=แจ้งเตือนหากราคา XMR สูงกว่า account.notifications.priceAlert.low.label=แจ้งเตือนหากราคา XMR ต่ำกว่า account.notifications.priceAlert.setButton=ตั้งค่าการเตือนราคา account.notifications.priceAlert.removeButton=ลบการเตือนราคา account.notifications.trade.message.title=การเปลี่ยนแปลงสถานะทางการค้า account.notifications.trade.message.msg.conf=ธุรกรรมทางการค้าจากผู้ค้า ID {0} ได้รับการยืนยันแล้ว โปรดเปิดแอปพลิเคชัน Haveno ของคุณและเริ่มการรับการชำระเงิน account.notifications.trade.message.msg.started=ผู้ซื้อ XMR ได้เริ่มต้นการชำระเงินสำหรับผู้ค้าที่มี ID {0} account.notifications.trade.message.msg.completed=การค้ากับ ID {0} เสร็จสมบูรณ์ account.notifications.offer.message.title=ข้อเสนอของคุณถูกยอมรับ account.notifications.offer.message.msg=ข้อเสนอของคุณที่มี ID {0} ถูกยอมรับ account.notifications.dispute.message.title=มีข้อความใหม่เกี่ยวกับข้อพิพาท account.notifications.dispute.message.msg=คุณได้รับข้อความการพิพาททางการค้ากับ ID {0} account.notifications.marketAlert.title=เสนอการแจ้งเตือน account.notifications.marketAlert.selectPaymentAccount=เสนอบัญชีการชำระเงินที่ตรงกัน account.notifications.marketAlert.offerType.label=ประเภทข้อเสนอพิเศษที่ฉันสนใจ account.notifications.marketAlert.offerType.buy=ซื้อข้อเสนอพิเศษ (ฉันต้องการขาย XMR) account.notifications.marketAlert.offerType.sell=ข้อเสนอพิเศษในการขาย (ฉันต้องการซื้อ XMR) account.notifications.marketAlert.trigger=ระดับของราคาที่เสนอ (%) account.notifications.marketAlert.trigger.info=เมื่อตั้งระดับของราคา คุณจะได้รับการแจ้งเตือนเมื่อมีการเผยแพร่ข้อเสนอที่ตรงกับความต้องการของคุณ (หรือมากกว่า) \nตัวอย่าง: หากคุณต้องการขาย XMR แต่คุณจะขายในราคาที่สูงกว่า 2% จากราคาตลาดปัจจุบันเท่านั้น\n การตั้งค่าฟิลด์นี้เป็น 2% จะทำให้คุณมั่นใจได้ว่าจะได้รับการแจ้งเตือนสำหรับข้อเสนอเฉพาะในราคาที่สูงกว่าราคาตลาดปัจจุบันที่ 2% (หรือมากกว่า) account.notifications.marketAlert.trigger.prompt=เปอร์เซ็นต์ระดับราคาจากราคาตลาด (เช่น 2.50%, -0.50% ฯลฯ ) account.notifications.marketAlert.addButton=เพิ่มการแจ้งเตือนข้อเสนอพิเศษ account.notifications.marketAlert.manageAlertsButton=จัดการการแจ้งเตือนข้อเสนอพิเศษ account.notifications.marketAlert.manageAlerts.title=จัดการการแจ้งเตือนข้อเสนอพิเศษ account.notifications.marketAlert.manageAlerts.header.paymentAccount=บัญชีการชำระเงิน account.notifications.marketAlert.manageAlerts.header.trigger= ราคาเงื่อนไขที่ตั้งไว้ account.notifications.marketAlert.manageAlerts.header.offerType=ประเภทข้อเสนอ account.notifications.marketAlert.message.title=แจ้งเตือนข้อเสนอ account.notifications.marketAlert.message.msg.below=ต่ำกว่า account.notifications.marketAlert.message.msg.above=สูงกว่า account.notifications.marketAlert.message.msg=ข้อเสนอใหม่ '{0} {1}' 'ด้วยราคา {2} ({3} {4} ราคาตลาด) และวิธีการชำระเงิน' '{5}' 'ถูกเผยแพร่ลงในหนังสือข้อเสนอ Haveno\nรหัสข้อเสนอพิเศษ: {6} account.notifications.priceAlert.message.title=การแจ้งเตือนราคาสำหรับ {0} account.notifications.priceAlert.message.msg=คุณได้รับการแจ้งเตือนราคาของคุณ {0} ราคาปัจจุบันคือ {1} {2} account.notifications.noWebCamFound.warning=ไม่พบเว็บแคม\n\nโปรดใช้ตัวเลือกอีเมลเพื่อส่งรหัสโทเค็น (รหัสเหรียญ) และคีย์เข้ารหัสจากโทรศัพท์มือถือของคุณไปยังแอพพลิเคชัน Haveno account.notifications.priceAlert.warning.highPriceTooLow=ราคาที่สูงกว่าต้องเป็นจำนวนที่มากเหนือราคาที่ต่ำกว่า account.notifications.priceAlert.warning.lowerPriceTooHigh=ราคาที่ต่ำกว่าต้องต่ำกว่าราคาที่สูงขึ้น #################################################################### # Windows #################################################################### inputControlWindow.headline=Select inputs for transaction inputControlWindow.balanceLabel=ยอดคงเหลือที่พร้อมใช้งาน contractWindow.title=รายละเอียดข้อพิพาท contractWindow.dates=วันที่เสนอ / วันที่ซื้อขาย contractWindow.xmrAddresses=ที่อยู่ Monero ผู้ซื้อ XMR / ผู้ขาย XMR contractWindow.onions=ที่อยู่เครือข่ายผู้ซื้อ XMR / ผู้ขาย XMR contractWindow.accountAge=Account age XMR buyer / XMR seller contractWindow.numDisputes=เลขที่ข้อพิพาทผู้ซื้อ XMR / ผู้ขาย XMR contractWindow.contractHash=สัญญา hash displayAlertMessageWindow.headline=ข้อมูลสำคัญ! displayAlertMessageWindow.update.headline=ข้อมูลอัปเดตที่สำคัญ! displayAlertMessageWindow.update.download=ดาวน์โหลด: displayUpdateDownloadWindow.downloadedFiles=ไฟล์: displayUpdateDownloadWindow.downloadingFile=กำลังดาวน์โหลด: {0} displayUpdateDownloadWindow.verifiedSigs=ลายเซ็นยืนยันด้วยคีย์: displayUpdateDownloadWindow.status.downloading=กำลังดาวน์โหลดไฟล์ ... displayUpdateDownloadWindow.status.verifying=กำลังตรวจสอบลายเซ็น ... displayUpdateDownloadWindow.button.label=ดาวน์โหลดตัวติดตั้งและยืนยันลายเซ็น displayUpdateDownloadWindow.button.downloadLater=ดาวน์โหลดในภายหลัง displayUpdateDownloadWindow.button.ignoreDownload=ไม่สนใจเวอร์ชั่นนี้ displayUpdateDownloadWindow.headline=การอัปเดต Haveno ใหม่พร้อมแล้ว! displayUpdateDownloadWindow.download.failed.headline=การดาวน์โหลดล้มเหลว displayUpdateDownloadWindow.download.failed=Download failed.\nPlease download and verify manually at [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.installer.failed=Unable to determine the correct installer. Please download and verify manually at [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.verify.failed=Verification failed.\nPlease download and verify manually at [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.success=ดาวน์โหลดเวอร์ชั่นใหม่เรียบร้อยแล้วและได้รับการยืนยันลายเซ็นแล้ว\n\nโปรดเปิดสารบบดาวน์โหลด หลังจากนั้นปิดโปรแกรมและติดตั้งเวอร์ชั่นใหม่ displayUpdateDownloadWindow.download.openDir=เปิดสารบบดาวน์โหลด disputeSummaryWindow.title=สรุป disputeSummaryWindow.openDate=วันที่ยื่นการเปิดคำขอและความช่วยเหลือ disputeSummaryWindow.role=บทบาทของผู้ค้า disputeSummaryWindow.payout=การจ่ายเงินของจำนวนการซื้อขาย disputeSummaryWindow.payout.getsTradeAmount=XMR {0} รับการจ่ายเงินของปริมาณการซื้อขาย: disputeSummaryWindow.payout.getsAll=Max. payout to XMR {0} disputeSummaryWindow.payout.custom=การชำระเงินที่กำหนดเอง disputeSummaryWindow.payoutAmount.buyer=จำนวนเงินที่จ่ายของผู้ซื้อ disputeSummaryWindow.payoutAmount.seller=จำนวนเงินที่จ่ายของผู้ขาย disputeSummaryWindow.payoutAmount.invert=ใช้ผู้แพ้เป็นผู้เผยแพร่ disputeSummaryWindow.reason=เหตุผลในการพิพาท disputeSummaryWindow.tradePeriodEnd=Trade period end disputeSummaryWindow.extraInfo=Extra information disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BUG=ปัญหา # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.USABILITY=การใช้งาน # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PROTOCOL_VIOLATION=การละเมิดโปรโตคอล # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.NO_REPLY=ไม่มีการตอบ # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SCAM=การหลอกลวง # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OTHER=อื่น ๆ # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BANK_PROBLEMS=ธนาคาร # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OPTION_TRADE=Option trade # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Trader not responding # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled disputeSummaryWindow.summaryNotes=สรุปบันทึกย่อ disputeSummaryWindow.addSummaryNotes=เพิ่มสรุปบันทึกย่อ: disputeSummaryWindow.close.button=ปิดการยื่นคำขอและความช่วยเหลือ # Do no change any line break or order of tokens as the structure is used for signature verification # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.msg=Ticket closed on {0}\n{1} node address: {2}\n\nSummary:\nTrade ID: {3}\nCurrency: {4}\nTrade amount: {5}\nPayout amount for XMR buyer: {6}\nPayout amount for XMR seller: {7}\n\nReason for dispute: {8}\n\nSummary notes:\n{9}\n # Do no change any line break or order of tokens as the structure is used for signature verification disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3} disputeSummaryWindow.close.nextStepsForMediation=\nNext steps:\nOpen trade and accept or reject suggestion from mediator disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\nNo further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions disputeSummaryWindow.close.closePeer=คุณจำเป็นต้องยุติคำขอความช่วยเหลือคู่ค้าด้วย ! disputeSummaryWindow.close.txDetails.headline=Publish refund transaction # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.buyer=Buyer receives {0} on address: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=Seller receives {0} on address: {1}\n disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3}\n\nAre you sure you want to publish this transaction? disputeSummaryWindow.close.noPayout.headline=Close without any payout disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? emptyWalletWindow.headline={0} กระเป๋าสตางค์ฉุกเฉิน emptyWalletWindow.info=โปรดใช้ในกรณีฉุกเฉินเท่านั้นหากคุณไม่สามารถเข้าถึงเงินจาก UI ได้\n\nโปรดทราบว่าข้อเสนอแบบเปิดทั้งหมดจะถูกปิดโดยอัตโนมัติเมื่อใช้เครื่องมือนี้\n\nก่อนที่คุณจะใช้เครื่องมือนี้โปรดสำรองข้อมูลในสารบบข้อมูลของคุณ คุณสามารถดำเนินการได้ที่ \"บัญชี / การสำรองข้อมูล \" \n\nโปรดรายงานปัญหาของคุณและส่งรายงานข้อบกพร่องเกี่ยวกับ GitHub หรือที่ฟอรัม Haveno เพื่อให้เราสามารถตรวจสอบสิ่งที่เป็นสาเหตุของปัญหาได้ emptyWalletWindow.balance=ยอดในกระเป๋าสตางค์ที่คงเหลือที่มีอยู่ emptyWalletWindow.address=ที่อยู่ปลายทางของคุณ emptyWalletWindow.button=ส่งเงินทั้งหมด emptyWalletWindow.openOffers.warn=คุณมีข้อเสนอแบบเปิดซึ่งจะถูกปลดออกในกรณีที่คุณทำให้ กระเป๋าสตางค์ไม่มีเงินเหลืออยู่เลย\nคุณแน่ใจหรือไม่ว่าต้องการให้กระเป๋าสตางค์ของคุณนั้นว่างเปล่า? emptyWalletWindow.openOffers.yes=ใช่ ฉันแน่ใจ emptyWalletWindow.sent.success=ยอดคงเหลือในกระเป๋าสตางค์ของคุณได้รับการโอนเรียบร้อยแล้ว enterPrivKeyWindow.headline=Enter private key for registration filterWindow.headline=แก้ไขรายการตัวกรอง filterWindow.offers=ข้อเสนอที่ได้รับการกรอง (คั่นด้วยเครื่องหมายจุลภาค) filterWindow.onions=Banned from trading addresses (comma sep.) filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.) filterWindow.accounts=ข้อมูลบัญชีการซื้อขายที่ถูกกรอง: \nรูปแบบ: เครื่องหมายจุลภาค รายการของ [id วิธีการชำระเงิน | ด้านข้อมูล | มูลค่า] filterWindow.bannedCurrencies=รหัสโค้ดสกุลเงินที่ได้รับการกรอง (คั่นด้วยเครื่องหมายจุลภาค) filterWindow.bannedPaymentMethods=รหัส ID วิธีการชำระเงินที่ได้รับการกรอง (คั่นด้วยเครื่องหมายจุลภาค) filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) filterWindow.arbitrators=ผู้ไกล่เกลี่ยที่ได้รับการคัดกรอง (คั่นด้วยเครื่องหมายจุลภาค ที่อยู่ onion) filterWindow.mediators=Filtered mediators (comma sep. onion addresses) filterWindow.refundAgents=Filtered refund agents (comma sep. onion addresses) filterWindow.seedNode=แหล่งข้อมูลในโหนดเครือข่ายที่ได้รับการกรอง (คั่นด้วยเครื่องหมายจุลภาค ที่อยู่ onion) filterWindow.priceRelayNode=โหนดผลัดเปลี่ยนราคาที่ได้รับการกรอง (คั่นด้วยเครื่องหมายจุลภาค ที่อยู่ onion) filterWindow.xmrNode=โหนด Monero ที่ได้รับการกรองแล้ว (คั่นด้วยเครื่องหมายจุลภาค ที่อยู่ + พอร์ต) filterWindow.preventPublicXmrNetwork=ป้องกันการใช้เครือข่าย Monero สาธารณะ filterWindow.disableAutoConf=Disable auto-confirm filterWindow.autoConfExplorers=Filtered auto-confirm explorers (comma sep. addresses) filterWindow.disableTradeBelowVersion=Min. version required for trading filterWindow.add=เพิ่มตัวกรอง filterWindow.remove=ลบตัวกรอง filterWindow.xmrFeeReceiverAddresses=XMR fee receiver addresses filterWindow.disableApi=Disable API filterWindow.disableMempoolValidation=Disable Mempool Validation offerDetailsWindow.minXmrAmount=จำนวน XMR ต่ำสุด offerDetailsWindow.min=(ต่ำสุด. {0}) offerDetailsWindow.distance=(ระดับราคาจากราคาตลาด: {0}) offerDetailsWindow.myTradingAccount=บัญชีการซื้อขายของฉัน offerDetailsWindow.offererBankId=(รหัส ID ธนาคารของผู้สร้าง / BIC / SWIFT) offerDetailsWindow.offerersBankName=(ชื่อธนาคารของผู้สร้าง) offerDetailsWindow.bankId=รหัส ID ธนาคาร (เช่น BIC หรือ SWIFT) offerDetailsWindow.countryBank=ประเทศของธนาคารของผู้สร้าง offerDetailsWindow.commitment=ข้อผูกมัด offerDetailsWindow.agree=ฉันเห็นด้วย offerDetailsWindow.tac=ข้อตกลงและเงื่อนไข offerDetailsWindow.confirm.maker.buy=ยืนยัน: สร้างข้อเสนอเพื่อซื้อ XMR ด้วย {0} offerDetailsWindow.confirm.maker.sell=ยืนยัน: สร้างข้อเสนอเพื่อขาย XMR เป็น {0} offerDetailsWindow.confirm.taker.buy=ยืนยัน: ยอมรับข้อเสนอเพื่อซื้อ XMR ด้วย {0} offerDetailsWindow.confirm.taker.sell=ยืนยัน: ยอมรับข้อเสนอเพื่อขาย XMR เป็น {0} offerDetailsWindow.creationDate=วันที่สร้าง offerDetailsWindow.makersOnion=ที่อยู่ onion ของผู้สร้าง offerDetailsWindow.challenge=รหัสผ่านสำหรับข้อเสนอ offerDetailsWindow.challenge.copy=คัดลอกวลีรหัสเพื่อแชร์กับเพื่อนของคุณ qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. qRCodeWindow.request=คำขอชำระเงิน: \n{0} selectDepositTxWindow.headline=เลือกรายการเงินฝากสำหรับกรณีพิพาท selectDepositTxWindow.msg=ธุรกรรมเงินฝากไม่ได้เก็บไว้ในการซื้อขาย\nโปรดเลือกหนึ่งในธุรกรรม multisig (การรองรับหลายลายเซ็น) ที่มีอยู่จากกระเป๋าสตางค์ของคุณซึ่งเป็นรายการฝากเงินที่ใช้ในการซื้อขายที่มีเกิดความผิดพลาด\n\nคุณสามารถค้นหารายการที่ถูกต้องได้โดยการเปิดหน้าต่างรายละเอียดทางการซื้อขาย (คลิกที่ ID การซื้อขายในรายการ) และทำรายการธุรกรรมการชำระเงินค่าธรรมเนียมการซื้อขายต่อไปยังรายการถัดไปที่คุณเห็นรายการเงินฝาก multisig (ที่อยู่เริ่มต้นด้วย 3) ID ธุรกรรมนี้ควรปรากฏในรายการที่นำเสนอที่นี่ เมื่อคุณพบรายการถูกต้องเลือกรายการที่นี่และดำเนินต่อไป\n\nขออภัยในความไม่สะดวก แต่กรณีข้อผิดพลาดดังกล่าวควรเกิดขึ้นน้อยมากและในอนาคตเราจะพยายามหาวิธีที่ดีกว่าในการแก้ไข selectDepositTxWindow.select=เลือกรายการเงินฝาก sendAlertMessageWindow.headline=ส่งการแจ้งเตือนทั่วโลก sendAlertMessageWindow.alertMsg=ข้อความแจ้งเตือน sendAlertMessageWindow.enterMsg=ใส่ข้อความ sendAlertMessageWindow.isSoftwareUpdate=Software download notification sendAlertMessageWindow.isUpdate=Is full release sendAlertMessageWindow.isPreRelease=Is pre-release sendAlertMessageWindow.version=หมายเลขเวอร์ชชั่นรุ่นใหม่ sendAlertMessageWindow.send=ส่งการแจ้งเตือน sendAlertMessageWindow.remove=นำการแจ้งเตือนออก sendPrivateNotificationWindow.headline=ส่งข้อความส่วนตัว sendPrivateNotificationWindow.privateNotification=การแจ้งเตือนส่วนตัว sendPrivateNotificationWindow.enterNotification=ป้อนการแจ้งเตือน sendPrivateNotificationWindow.send=ส่งการแจ้งเตือนส่วนตัว showWalletDataWindow.walletData=ข้อมูล Wallet  showWalletDataWindow.includePrivKeys=รวมคีย์ส่วนตัว setXMRTxKeyWindow.headline=Prove sending of XMR setXMRTxKeyWindow.note=Adding tx info below enables auto-confirm for quicker trades. See more: https://haveno.exchange/wiki/Trading_Monero setXMRTxKeyWindow.txHash=Transaction ID (optional) setXMRTxKeyWindow.txKey=Transaction key (optional) # We do not translate the tac because of the legal nature. We would need translations checked by lawyers # in each language which is too expensive atm. tacWindow.headline=ข้อตกลงการใช้ tacWindow.agree=ฉันเห็นด้วย tacWindow.disagree=ฉันไม่เห็นด้วยและออก tacWindow.arbitrationSystem=Dispute resolution tradeDetailsWindow.headline=ซื้อขาย tradeDetailsWindow.disputedPayoutTxId=รหัส ID ธุรกรรมการจ่ายเงินที่พิพาท tradeDetailsWindow.tradeDate=วันที่ซื้อขาย tradeDetailsWindow.txFee=ค่าธรรมเนียมการขุด tradeDetailsWindow.tradePeersOnion=ที่อยู่ของ onion คู่ค้า tradeDetailsWindow.tradePeersPubKeyHash=Trading peers pubkey hash tradeDetailsWindow.tradeState=สถานะการค้า tradeDetailsWindow.agentAddresses=Arbitrator/Mediator tradeDetailsWindow.detailData=Detail data txDetailsWindow.headline=Transaction Details txDetailsWindow.xmr.noteSent=คุณได้ส่ง XMR แล้ว txDetailsWindow.xmr.noteReceived=คุณได้รับ XMR แล้ว txDetailsWindow.sentTo=ส่งไปยัง txDetailsWindow.receivedWith=ได้รับด้วย txDetailsWindow.txId=TxId closedTradesSummaryWindow.headline=Trade history summary closedTradesSummaryWindow.totalAmount.title=Total trade amount closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) closedTradesSummaryWindow.totalTradeFeeInXmr.title=Sum of all trade fees paid in XMR closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} of total trade amount) walletPasswordWindow.headline=ป้อนรหัสผ่านเพื่อปลดล็อก xmrConnectionError.headline=ข้อผิดพลาดการเชื่อมต่อ Monero xmrConnectionError.providedNodes=เกิดข้อผิดพลาดในการเชื่อมต่อกับโหนด Monero ที่ให้มา\n\nคุณต้องการใช้โหนด Monero ที่ดีที่สุดถัดไปหรือไม่? xmrConnectionError.customNodes=เกิดข้อผิดพลาดในการเชื่อมต่อกับโหนด Monero ที่กำหนดเองของคุณ\n\nคุณต้องการใช้โหนด Monero ที่ดีที่สุดถัดไปหรือไม่? xmrConnectionError.localNode=Haveno เคยเชื่อมต่อกับโหนด Monero ในเครื่อง แต่ตอนนี้ไม่สามารถเข้าถึงได้\n\nโปรดตรวจสอบว่าโหนดในเครื่องของคุณกำลังทำงานและซิงค์เต็มที่ หรือเลือกตัวเลือกอื่นเพื่อดำเนินการต่อ xmrConnectionError.localNode.start=เริ่มโหนดในเครื่อง xmrConnectionError.localNode.start.error=เกิดข้อผิดพลาดในการเริ่มโหนดในเครื่อง xmrConnectionError.localNode.fallback=เชื่อมต่อกับโหนดที่ดีที่สุดถัดไป torNetworkSettingWindow.header=ตั้งค่าเครือข่าย Tor torNetworkSettingWindow.noBridges=อย่าใช้สะพาน torNetworkSettingWindow.providedBridges=เชื่อมต่อกับสะพานที่ให้ไว้ torNetworkSettingWindow.customBridges=ป้อนสะพานที่กำหนดเอง torNetworkSettingWindow.transportType=ประเภทการขนส่ง torNetworkSettingWindow.obfs3=obfs3 torNetworkSettingWindow.obfs4=obfs4 (แนะนำ) torNetworkSettingWindow.meekAmazon=meek-amazon torNetworkSettingWindow.meekAzure=meek-azure torNetworkSettingWindow.enterBridge=ป้อนหนึ่งสะพานผลัดเปลี่ยนหรือหลายรายการ (หนึ่งรายการต่อบรรทัด) torNetworkSettingWindow.enterBridgePrompt=ที่อยู่ประเภท: พอร์ต torNetworkSettingWindow.restartInfo=คุณต้องรีสตาร์ทใหม่เพื่อใช้การเปลี่ยนแปลง torNetworkSettingWindow.openTorWebPage=เปิดหน้าเว็บของโครงการ Tor torNetworkSettingWindow.deleteFiles.header=กำลังเจอปัญหาการเชื่อมต่ออยู่หรือ torNetworkSettingWindow.deleteFiles.info=ถ้าคุณมีปัญหาการเชื่อมต่อซ้ำเมื่อเริ่มต้น การลบไฟล์ Tor ที่ล้าสมัยอาจช่วยได้ เมื่อต้องการทำเช่นนั้นคลิกที่ปุ่มด้านล่างและรีสตาร์ทใหม่ torNetworkSettingWindow.deleteFiles.button=ลบไฟล์ Tor ที่ล้าสมัยออกแล้วปิดลง torNetworkSettingWindow.deleteFiles.progress=ปิด Tor ที่กำลังดำเนินอยู่ torNetworkSettingWindow.deleteFiles.success=ไฟล์ Tor ที่ล้าสมัยถูกลบแล้ว โปรดรีสตาร์ท torNetworkSettingWindow.bridges.header=Tor ถูกบล็อกหรือไม่ torNetworkSettingWindow.bridges.info=ถ้า Tor ถูกปิดกั้นโดยผู้ให้บริการอินเทอร์เน็ตหรือประเทศของคุณ คุณสามารถลองใช้ Tor bridges\nไปที่หน้าเว็บของ Tor ที่ https://bridges.torproject.org เพื่อเรียนรู้เพิ่มเติมเกี่ยวกับสะพานและการขนส่งแบบ pluggable feeOptionWindow.headline=เลือกสกุลเงินสำหรับการชำระค่าธรรมเนียมการซื้อขาย feeOptionWindow.info=คุณสามารถเลือกที่จะชำระค่าธรรมเนียมทางการค้าใน BSQ หรือใน XMR แต่ถ้าคุณเลือก BSQ คุณจะได้รับส่วนลดค่าธรรมเนียมการซื้อขาย feeOptionWindow.optionsLabel=เลือกสกุลเงินสำหรับการชำระค่าธรรมเนียมการซื้อขาย feeOptionWindow.useXMR=ใช้ XMR feeOptionWindow.fee={0} (≈ {1}) feeOptionWindow.xmrFeeWithFiatAndPercentage={0} (≈ {1} / {2}) feeOptionWindow.xmrFeeWithPercentage={0} ({1}) #################################################################### # Popups #################################################################### popup.headline.notification=การแจ้งเตือน popup.headline.instruction=โปรดทราบ: popup.headline.attention=ฟังทางนี้ด้วย popup.headline.backgroundInfo=ข้อมูลพื้นฐาน popup.headline.feedback=เสร็จสิ้น popup.headline.confirmation=การยืนยัน popup.headline.information=ข้อมูล popup.headline.warning=คำเตือน popup.headline.error=ผิดพลาด popup.doNotShowAgain=ไม่ต้องแสดงอีกครั้ง popup.reportError.log=เปิดไฟล์ที่บันทึก popup.reportError.gitHub=รายงานไปที่ตัวติดตามปัญหา GitHub popup.reportError={0}\n\nTo help us to improve the software please report this bug by opening a new issue at https://github.com/haveno-dex/haveno/issues.\nThe above error message will be copied to the clipboard when you click either of the buttons below.\nIt will make debugging easier if you include the haveno.log file by pressing "Open log file", saving a copy, and attaching it to your bug report. popup.error.tryRestart=โปรดลองเริ่มแอปพลิเคชั่นของคุณใหม่และตรวจสอบการเชื่อมต่อเครือข่ายของคุณเพื่อดูว่าคุณสามารถแก้ไขปัญหาได้หรือไม่ popup.error.takeOfferRequestFailed=เกิดข้อผิดพลาดขึ้นเมื่อมีคนพยายามรับข้อเสนอของคุณ: \n{0} error.spvFileCorrupted=เกิดข้อผิดพลาดขณะอ่านไฟล์ chain SPV \nอาจเป็นเพราะไฟล์ chain SPV เสียหาย\n\nเกิดข้อผิดพลาด: {0} \n\nคุณต้องการลบและเริ่มการซิงค์ใหม่หรือไม่ error.deleteAddressEntryListFailed=ไม่สามารถลบไฟล์ AddressEntryList ได้\nข้อผิดพลาด: {0} error.closedTradeWithUnconfirmedDepositTx=The deposit transaction of the closed trade with the trade ID {0} is still unconfirmed.\n\nPlease do a SPV resync at \"Setting/Network info\" to see if the transaction is valid. error.closedTradeWithNoDepositTx=The deposit transaction of the closed trade with the trade ID {0} is null.\n\nPlease restart the application to clean up the closed trades list. popup.warning.walletNotInitialized=wallet ยังไม่ได้เริ่มต้น popup.warning.osxKeyLoggerWarning=Due to stricter security measures in macOS 10.14 and above, launching a Java application (Haveno uses Java) causes a popup warning in macOS ('Haveno would like to receive keystrokes from any application').\n\nTo avoid that issue please open your 'macOS Settings' and go to 'Security & Privacy' -> 'Privacy' -> 'Input Monitoring' and Remove 'Haveno' from the list on the right side.\n\nHaveno will upgrade to a newer Java version to avoid that issue as soon the technical limitations (Java packager for the required Java version is not shipped yet) are resolved. popup.warning.wrongVersion=คุณอาจมีเวอร์ชั่น Haveno ไม่เหมาะสำหรับคอมพิวเตอร์นี้\nสถาปัตยกรรมคอมพิวเตอร์ของคุณคือ: {0} .\nเลขฐานสอง Haveno ที่คุณติดตั้งคือ: {1} .\nโปรดปิดตัวลงและติดตั้งรุ่นที่ถูกต้องอีกครั้ง ({2}) popup.warning.incompatibleDB=We detected incompatible data base files!\n\nThose database file(s) are not compatible with our current code base:\n{0}\n\nWe made a backup of the corrupted file(s) and applied the default values to a new database version.\n\nThe backup is located at:\n{1}/db/backup_of_corrupted_data.\n\nPlease check if you have the latest version of Haveno installed.\nYou can download it at: [HYPERLINK:https://haveno.exchange/downloads].\n\nPlease restart the application. popup.warning.startupFailed.twoInstances=Haveno กำลังทำงานอยู่ คุณไม่สามารถเรียกใช้ Haveno พร้อมกันได้ popup.warning.tradePeriod.halfReached=การซื้อขายของคุณที่มีรหัส ID {0} ได้ถึงครึ่งหนึ่งของจำนวนสูงสุดแล้ว อนุญาตให้ซื้อขายได้และยังไม่สมบูรณ์\n\nช่วงเวลาการซื้อขายสิ้นสุดวันที่ {1} \n\nโปรดตรวจสอบสถานะการค้าของคุณที่ \"Portfolio (แฟ้มผลงาน) / เปิดการซื้อขาย \" สำหรับข้อมูลเพิ่มเติม popup.warning.tradePeriod.ended=Your trade with ID {0} has reached the max. allowed trading period and is not completed.\n\nThe trade period ended on {1}\n\nPlease check your trade at \"Portfolio/Open trades\" for contacting the arbitrator. popup.warning.noTradingAccountSetup.headline=คุณยังไม่ได้ตั้งค่าบัญชีการซื้อขาย popup.warning.noTradingAccountSetup.msg=คุณต้องตั้งค่าสกุลเงินประจำชาติหรือบัญชี crypto (เหรียญทางเลือก) ก่อนจึงจะสามารถสร้างข้อเสนอได้\nคุณต้องการตั้งค่าบัญชีหรือไม่ popup.warning.noArbitratorsAvailable=ไม่มีผู้ไกล่เกลี่ยสำหรับทำการ popup.warning.noMediatorsAvailable=There are no mediators available. popup.warning.notFullyConnected=คุณต้องรอจนกว่าคุณจะเชื่อมต่อกับเครือข่ายอย่างสมบูรณ์\nอาจใช้เวลาประมาณ 2 นาทีเมื่อเริ่มต้น popup.warning.notSufficientConnectionsToXmrNetwork=คุณต้องรอจนกว่าจะมีการเชื่อมต่อกับเครือข่าย Monero อย่างน้อย {0} รายการ popup.warning.downloadNotComplete=คุณต้องรอจนกว่าการดาวน์โหลดบล็อค Monero ที่ขาดหายไปจะเสร็จสมบูรณ์ popup.warning.walletNotSynced=กระเป๋า Haveno ไม่ได้ปรับข้อมูลกับความสูงของบล็อกเชนล่าสุด โปรดรอให้กระเป๋าปรับข้อมูลหรือตรวจสอบการเชื่อมต่อของคุณ popup.warning.removeOffer=คุณแน่ใจหรือไม่ว่าต้องการนำข้อเสนอนั้นออก popup.warning.tooLargePercentageValue=คุณไม่สามารถกำหนดเปอร์เซ็นต์เป็น 100% หรือมากกว่าได้ popup.warning.examplePercentageValue=โปรดป้อนตัวเลขเปอร์เซ็นต์เช่น \"5.4 \" เป็น 5.4% popup.warning.noPriceFeedAvailable=ไม่มีฟีดราคาสำหรับสกุลเงินดังกล่าว คุณไม่สามารถใช้ราคาตามเปอร์เซ็นต์ได้\nโปรดเลือกราคาที่ถูกกำหนดไว้แแล้ว popup.warning.sendMsgFailed=การส่งข้อความไปยังคู่ค้าของคุณล้มเหลว\nโปรดลองอีกครั้งและหากยังคงเกิดขึ้นขึ้นเนื่อง โปรดรายงานข้อผิดพลาดต่อไป popup.warning.messageTooLong=ข้อความของคุณเกินขีดจำกัดสูงสุดที่อนุญาต โปรดแบ่งส่งเป็นหลายส่วนหรืออัปโหลดไปยังบริการเช่น https://pastebin.com popup.warning.lockedUpFunds=You have locked up funds from a failed trade.\nLocked up balance: {0} \nDeposit tx address: {1}\nTrade ID: {2}.\n\nPlease open a support ticket by selecting the trade in the open trades screen and pressing \"alt + o\" or \"option + o\"." popup.warning.moneroConnection=เกิดปัญหาในการเชื่อมต่อกับเครือข่าย Monero\n\n{0} popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n takeOffer.cancelButton=Cancel take-offer takeOffer.warningButton=Ignore and continue anyway # suppress inspection "UnusedProperty" popup.warning.nodeBanned=One of the {0} nodes got banned. # suppress inspection "UnusedProperty" popup.warning.priceRelay=ราคาผลัดเปลี่ยน popup.warning.seed=รหัสลับเพื่อกู้ข้อมูล popup.warning.mandatoryUpdate.trading=Please update to the latest Haveno version. A mandatory update was released which disables trading for old versions. Please check out the Haveno Forum for more information. popup.warning.noFilter=เราไม่ได้รับวัตถุกรองจากโหนดต้นทาง กรุณาแจ้งผู้ดูแลระบบเครือข่ายให้ลงทะเบียนวัตถุกรอง popup.warning.burnXMR=This transaction is not possible, as the mining fees of {0} would exceed the amount to transfer of {1}. Please wait until the mining fees are low again or until you've accumulated more XMR to transfer. popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Monero network.\nTransaction ID={1}.\nThe offer has been removed to avoid further problems.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. popup.warning.trade.txRejected.tradeFee=trade fee popup.warning.trade.txRejected.deposit=deposit popup.warning.trade.txRejected=The {0} transaction for trade with ID {1} was rejected by the Monero network.\nTransaction ID={2}\nThe trade has been moved to failed trades.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. popup.warning.openOfferWithInvalidMakerFeeTx=The maker fee transaction for offer with ID {0} is invalid.\nTransaction ID={1}.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. popup.info.securityDepositInfo=เพื่อให้แน่ใจว่าเทรดเดอร์ทั้งคู่นั้นได้ปฏิบัติตามข้อสนธิสัญญาในการค้า เทรดเดอร์จำเป็นต้องทำการชำระค่าประกัน\n\nค่าประกันนี้คือถูกเก็บไว้ในกระเป๋าสตางค์การเทรดของคุณจนกว่าการเทรดของคุณจะดำเนินการสำเร็จ และคุณจะได้รับมันคืนหลังจากนั้น \n\nโปรดทราบ: หากคุณกำลังสร้างข้อเสนอขึ้นมาใหม่ Haveno จำเป็นที่ต้องดำเนินงานต่อเนื่องไปยังเทรดเดอร์รายอื่น และเพื่อที่สถานะข้อเสนอทางออนไลน์ของคุณจะยังคงอยู่ Haveno จะยังคงดำเนินงานต่อเนื่อง และโปรดมั่นใจว่าเครื่องคอมพิวเตอร์นี้กำลังออนไลน์อยู่ด้วยเช่นกัน (ยกตัวอย่างเช่น ตรวจเช็คว่าสวิทช์ไฟไม่ได้อยู่ในโหมดแสตนบายด์...หน้าจอแสตนบายด์คือปกติดี) popup.info.cashDepositInfo=โปรดตรวจสอบว่าคุณมีสาขาธนาคารในพื้นที่ของคุณเพื่อสามารถฝากเงินได้\nรหัสธนาคาร (BIC / SWIFT) ของธนาคารผู้ขายคือ: {0} popup.info.cashDepositInfo.confirm=ฉันยืนยันว่าฉันสามารถฝากเงินได้ popup.info.shutDownWithOpenOffers=Haveno คือกำลังจะปิดลง แต่ยังคงมีการเปิดขายข้อเสนอปกติ\nข้อเสนอเหล่านี้จะไม่ใข้งานได้บนเครือข่าย P2P network ในขณะที่ Haveno ปิดตัวลง แต่จะมีการเผยแพร่บนเครือข่าย P2P ครั้งถัดไปเมื่อคุณมีการเริ่มใช้งาน Haveno.\n\nในการคงสถานะข้อเสนอแบบออนไลน์ คือเปิดใข้งาน Haveno และทำให้มั่นใจว่าคอมพิวเตอร์เครื่องนี้กำลังออนไลน์อยู่ด้วยเช่นกัน (เช่น ตรวจสอบว่าคอมพิวเตอร์ไม่ได้อยู่ในโหมดแสตนบายด์...หน้าจอแสตนบายด์ไม่มีปัญหา) popup.info.qubesOSSetupInfo=It appears you are running Haveno on Qubes OS. \n\nPlease make sure your Haveno qube is setup according to our Setup Guide at [HYPERLINK:https://haveno.exchange/wiki/Running_Haveno_on_Qubes]. popup.warn.downGradePrevention=Downgrade from version {0} to version {1} is not supported. Please use the latest Haveno version. popup.privateNotification.headline=การแจ้งเตือนส่วนตัวที่สำคัญ! popup.securityRecommendation.headline=ข้อเสนอแนะด้านความปลอดภัยที่สำคัญ popup.securityRecommendation.msg=เราขอแจ้งเตือนให้คุณพิจารณาใช้การป้องกันด้วยรหัสผ่านสำหรับ wallet ของคุณ หากยังไม่ได้เปิดใช้งาน\n\nขอแนะนำให้เขียนรหัสลับป้องกัน wallet รหัสลับเหล่านี้เหมือนกับรหัสผ่านหลักสำหรับการกู้คืน Monero wallet ของคุณ\nไปที่ \"กระเป๋าสตางค์ \" คุณจะพบข้อมูลเพิ่มเติม\n\nนอกจากนี้คุณควรสำรองโฟลเดอร์ข้อมูลแอ็พพลิเคชั่นทั้งหมดไว้ที่ส่วน \"สำรองข้อมูล \" popup.shutDownInProgress.headline=การปิดระบบอยู่ระหว่างดำเนินการ popup.shutDownInProgress.msg=การปิดแอพพลิเคชั่นอาจใช้เวลาสักครู่\nโปรดอย่าขัดจังหวะกระบวนการนี้ popup.attention.forTradeWithId=ต้องให้ความสำคัญสำหรับการซื้อขายด้วย ID {0} popup.attention.reasonForPaymentRuleChange=Version 1.5.5 introduces a critical trade rule change regarding the \"reason for payment\" field in bank transfers. Please leave this field empty -- DO NOT use the trade ID as \"reason for payment\" anymore. popup.info.multiplePaymentAccounts.headline=Multiple payment accounts available popup.info.multiplePaymentAccounts.msg=You have multiple payment accounts available for this offer. Please make sure you've picked the right one. popup.accountSigning.selectAccounts.headline=Select payment accounts popup.accountSigning.selectAccounts.description=Based on the payment method and point of time all payment accounts that are connected to a dispute where a payout to the buyer occurred will be selected for you to sign. popup.accountSigning.selectAccounts.signAll=Sign all payment methods popup.accountSigning.selectAccounts.datePicker=Select point of time until which accounts will be signed popup.accountSigning.confirmSelectedAccounts.headline=Confirm selected payment accounts popup.accountSigning.confirmSelectedAccounts.description=Based on your input, {0} payment accounts will be selected. popup.accountSigning.confirmSelectedAccounts.button=Confirm payment accounts popup.accountSigning.signAccounts.headline=Confirm signing of payment accounts popup.accountSigning.signAccounts.description=Based on your selection, {0} payment accounts will be signed. popup.accountSigning.signAccounts.button=Sign payment accounts popup.accountSigning.signAccounts.ECKey=Enter private arbitrator key popup.accountSigning.signAccounts.ECKey.error=Bad arbitrator ECKey popup.accountSigning.success.headline=Congratulations popup.accountSigning.success.description=All {0} payment accounts were successfully signed! popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\nFor further information, please visit [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer's account after a successful trade.\n\n{0} popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you'll be able to sign other accounts in {0} days from now.\n\n{1} popup.accountSigning.peerLimitLifted=The initial limit for one of your accounts has been lifted.\n\n{0} popup.accountSigning.peerSigner=One of your accounts is mature enough to sign other payment accounts and the initial limit for one of your accounts has been lifted.\n\n{0} popup.accountSigning.singleAccountSelect.headline=Import unsigned account age witness popup.accountSigning.confirmSingleAccount.headline=Confirm selected account age witness popup.accountSigning.confirmSingleAccount.selectedHash=Selected witness hash popup.accountSigning.confirmSingleAccount.button=Sign account age witness popup.accountSigning.successSingleAccount.description=Witness {0} was signed popup.accountSigning.successSingleAccount.success.headline=Success popup.accountSigning.unsignedPubKeys.headline=Unsigned Pubkeys popup.accountSigning.unsignedPubKeys.sign=Sign Pubkeys popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign popup.info.buyerAsTakerWithoutDeposit.headline=ไม่ต้องมีเงินมัดจำจากผู้ซื้อ popup.info.buyerAsTakerWithoutDeposit=ข้อเสนอของคุณจะไม่ต้องการเงินมัดจำหรือค่าธรรมเนียมจากผู้ซื้อ XMR\n\nในการยอมรับข้อเสนอของคุณ คุณต้องแบ่งปันรหัสผ่านกับคู่ค้าการค้าของคุณภายนอก Haveno\n\nรหัสผ่านจะถูกสร้างโดยอัตโนมัติและแสดงในรายละเอียดข้อเสนอหลังจากการสร้าง #################################################################### # Notifications #################################################################### notification.trade.headline=การแจ้งเตือนการซื้อขายด้วย ID {0} notification.ticket.headline=ศูนย์ช่วยเหลือสนับสนุนการซื้อขายด้วย ID {0} notification.trade.completed=การค้าเสร็จสิ้นแล้วและคุณสามารถถอนเงินของคุณได้ notification.trade.accepted=ข้อเสนอของคุณได้รับการยอมรับจาก XMR {0} แล้ว notification.trade.unlocked=การซื้อขายของคุณมีการยืนยัน blockchain อย่างน้อยหนึ่งรายการ\nคุณสามารถเริ่มการชำระเงินได้เลย notification.trade.paymentSent=ผู้ซื้อ XMR ได้เริ่มการชำระเงินแล้ว notification.trade.selectTrade=เลือกการซื้อขาย notification.trade.peerOpenedDispute=เครือข่ายทางการค้าของคุณได้เริ่มต้นเปิดที่ {0} notification.trade.disputeClosed={0} ถูกปิดแล้ว notification.walletUpdate.headline=อัพเดตกระเป๋าสตางค์การซื้อขาย notification.walletUpdate.msg=กระเป๋าสตางค์ของคุณได้รับเงินเพียงพอ\nจำนวนเงิน: {0} notification.takeOffer.walletUpdate.msg=wallet ของคุณได้รับการสนับสนุนจากการเสนอราคาก่อนหน้านี้\nจำนวนเงิน: {0} notification.tradeCompleted.headline=การื้อขายเสร็จสิ้น notification.tradeCompleted.msg=คุณสามารถถอนเงินของคุณไปยังแหล่งเงินกระเป๋าสตางค์นอก Monero ของคุณหรือเก็บไว้ในกระเป๋าสตางค์ Haveno ของคุณได้。 #################################################################### # System Tray #################################################################### systemTray.show=แสดงหน้าต่างแอ็พพลิเคชั่น systemTray.hide=ซ่อนหน้าต่างแอ็พพลิเคชั่น systemTray.info=ข้อมูลเกี่ยวกับ Haveno systemTray.exit=ออก systemTray.tooltip=Haveno: A decentralized monero exchange network #################################################################### # GUI Util #################################################################### guiUtil.accountExport.savedToPath=บัญชีการค้าที่บันทึกไว้ในเส้นทาง: \n{0} guiUtil.accountExport.noAccountSetup=คุณไม่มีบัญชีการซื้อขายที่ตั้งค่าไว้สำหรับการส่งออก guiUtil.accountExport.selectPath=เลือกเส้นทางไปที่ {0} # suppress inspection "TrailingSpacesInProperty" guiUtil.accountExport.tradingAccount=บัญชีการซื้อขายด้วยรหัส ID {0}\n # suppress inspection "TrailingSpacesInProperty" guiUtil.accountImport.noImport=เราไม่ได้นำเข้าบัญชีการซื้อขายที่มี id {0} เนื่องจากมีอยู่แล้วในระบบ\n guiUtil.accountExport.exportFailed=การส่งออกไปยัง CSV ล้มเหลวเนื่องจากข้อผิดพลาด\nข้อผิดพลาด = {0} guiUtil.accountExport.selectExportPath=เลือกเส้นทางการส่งออก guiUtil.accountImport.imported=บัญชีการซื้อขายที่นำเข้าจากเส้นทาง: \n{0} \n\nบัญชีที่นำเข้า: \n{1} guiUtil.accountImport.noAccountsFound=ไม่พบบัญชีการค้าที่ส่งออกระหว่างทาง: {0} .\nชื่อไฟล์คือ {1} " guiUtil.openWebBrowser.warning=คุณกำลังจะเปิดเว็บเพจในเว็บเบราเซอร์ของระบบ\nคุณต้องการเปิดเว็บเพจตอนนี้หรือไม่\n\nหากคุณไม่ได้ใช้ \"Tor Browser \" เป็นเว็บเบราเซอร์ระบบเริ่มต้นของคุณ คุณจะเชื่อมต่อกับเว็บเพจโดยไม่ต้องแจ้งให้ทราบล่วงหน้า\n\nURL: \"{0} \" guiUtil.openWebBrowser.doOpen=เปิดหน้าเว็บและไม่ต้องถามอีก guiUtil.openWebBrowser.copyUrl=คัดลอก URL และยกเลิก guiUtil.ofTradeAmount=ของปริมาณการซื้อขาย guiUtil.requiredMinimum=(required minimum) #################################################################### # Component specific #################################################################### list.currency.select=เลือกสกุลเงิน list.currency.showAll=แสดงทั้งหมด list.currency.editList=แก้ไขรายการสกุลเงิน table.placeholder.noItems=ขณะนี้ไม่มี {0} พร้อมใช้งาน table.placeholder.noData=ขณะนี้ไม่มีข้อมูลที่พร้อมใช้งาน table.placeholder.processingData=Processing data... peerInfoIcon.tooltip.tradePeer=การซื้อขายของระบบเน็ตเวิร์ก peer peerInfoIcon.tooltip.maker=ของผู้สร้าง peerInfoIcon.tooltip.trade.traded={0} ที่อยู่ onion : {1} \nคุณได้ทำการซื้อขาย {2} ครั้ง(หลาย)แล้วด้วย peer นั้น\n{3} peerInfoIcon.tooltip.trade.notTraded={0} ที่อยู่ onion: {1} \nคุณยังไม่เคยทำการซื้อขายกับคู่ peer นั้นมาก่อน\n{2} peerInfoIcon.tooltip.age=บัญชีการชำระเงินที่สร้างขึ้น {0} ที่ผ่านมา peerInfoIcon.tooltip.unknownAge=อายุบัญชีการชำระเงินที่ไม่รู้จัก tooltip.openPopupForDetails=เปิดป๊อปอัปเพื่ออ่านรายละเอียด tooltip.invalidTradeState.warning=This trade is in an invalid state. Open the details window for more information tooltip.openBlockchainForAddress=เปิดตัวสำรวจ blockchain ภายนอกตามที่อยู่: {0} tooltip.openBlockchainForTx=เปิดตัวสำรวจ blockchain ภายนอกสำหรับธุรกรรม: {0} confidence.unknown=สถานะธุรกรรมที่ไม่รู้จัก confidence.seen=เห็นโดย {0} peer (s) / 0 การยืนยัน confidence.confirmed={0} การยืนยัน confidence.invalid=ธุรกรรมไม่ถูกต้อง peerInfo.title=ข้อมูล Peer peerInfo.nrOfTrades=จำนวนการซื้อขายที่เสร็จสิ้นแล้ว peerInfo.notTradedYet=คุณยังไม่เคยซื้อขายกับผู้ใช้รายนั้น peerInfo.setTag=ตั้งค่าแท็กสำหรับ peer นั้น peerInfo.age.noRisk=อายุบัญชีการชำระเงิน peerInfo.age.chargeBackRisk=Time since signing peerInfo.unknownAge=อายุ ที่ไม่ที่รู้จัก addressTextField.openWallet=เปิดกระเป๋าสตางค์ Monero เริ่มต้นของคุณ addressTextField.copyToClipboard=คัดลอกที่อยู่ไปยังคลิปบอร์ด addressTextField.addressCopiedToClipboard=ที่อยู่ถูกคัดลอกไปยังคลิปบอร์ดแล้ว addressTextField.openWallet.failed=การเปิดแอปพลิเคชั่นเริ่มต้นกระเป๋าสตางค์ Monero ล้มเหลว บางทีคุณอาจยังไม่ได้ติดตั้งไว้ peerInfoIcon.tooltip={0} \nแท็ก: {1} txIdTextField.copyIcon.tooltip=คัดลอก ID ธุรกรรมไปยังคลิปบอร์ด txIdTextField.blockExplorerIcon.tooltip=Open a blockchain explorer with this transaction ID txIdTextField.missingTx.warning.tooltip=Missing required transaction #################################################################### # Navigation #################################################################### navigation.account=\"บัญชี\" navigation.account.walletSeed=\ "บัญชี / รหัสลับป้องกันกระเป๋าสตางค์\" navigation.funds.availableForWithdrawal=\"Funds/Send funds\" navigation.portfolio.myOpenOffers=\"แฟ้มผลงาน / ข้อเสนอของฉัน \" navigation.portfolio.pending=\"แฟ้มผลงาน / เปิดการซื้อขาย \" navigation.portfolio.closedTrades=\"แฟ้มผลงาน/ประวัติ\" navigation.funds.depositFunds=\"เงิน / รับเงิน \" navigation.settings.preferences=\ "การตั้งค่า / สิ่งที่ชอบ \" # suppress inspection "UnusedProperty" navigation.funds.transactions=\"เงิน / ธุรกรรม\" navigation.support=\"ช่วยเหลือและสนับสนุน\" #################################################################### # Formatter #################################################################### formatter.formatVolumeLabel={0} จำนวนยอด{1} formatter.makerTaker=ผู้สร้าง เป็น {0} {1} / ผู้รับเป็น {2} {3} formatter.makerTaker.locked=ผู้สร้าง เป็น {0} {1} / ผู้รับเป็น {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=คุณคือ {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=คุณกำลังสร้างข้อเสนอให้ {0} {1} formatter.youAreCreatingAnOffer.traditional.locked=คุณกำลังสร้างข้อเสนอให้ {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=คุณกำลังสร้างข้อเสนอให้กับ {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.crypto.locked=คุณกำลังสร้างข้อเสนอให้กับ {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} ในฐานะผู้สร้าง formatter.asTaker={0} {1} ในฐานะคนรับ #################################################################### # Domain specific #################################################################### # we use enum values here # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" XMR_MAINNET=Monero Mainnet # suppress inspection "UnusedProperty" XMR_LOCAL=Monero Local Testnet # suppress inspection "UnusedProperty" XMR_STAGENET=Monero Stagenet time.year=ปี time.month=เดือน time.week=สัปดาห์ time.day=วัน time.hour=ชั่วโมง time.minute10=10 นาที time.hours=ชั่วโมง time.days=วัน time.1hour=1 ชั่วโมง time.1day=1 วัน time.minute=นาที time.second=วินาที time.minutes=นาที time.seconds=วินาที password.enterPassword=ใส่รหัสผ่าน password.confirmPassword=ยืนยันรหัสผ่าน password.tooLong=รหัสผ่านต้องมีอักขระไม่เกิน 500 ตัว password.deriveKey=ดึงข้อมูลจากรหัสผ่าน password.walletDecrypted=กระเป๋าสตางค์ถูกถอดรหัสสำเร็จและการป้องกันรหัสผ่านได้มีการออกแล้ว password.wrongPw=คุณป้อนรหัสผ่านไม่ถูกต้อง\n\nโปรดลองป้อนรหัสผ่านอีกครั้งโดยละเอียด เพื่อตรวจสอบความผิดพลาดในการพิมพ์หรือสะกด password.walletEncrypted=เปิดใช้งานกระเป๋าสตางค์ที่เข้ารหัสแล้วและเปิดใช้งานการป้องกันด้วยรหัสผ่านแล้ว password.passwordsDoNotMatch=รหัสผ่าน 2 รายการที่คุณป้อนไม่ตรงกัน password.forgotPassword=ลืมรหัสผ่านหรือเปล่า? password.backupReminder=Please note that when setting a wallet password all automatically created backups from the unencrypted wallet will be deleted.\n\nIt is highly recommended that you make a backup of the application directory and write down your seed words before setting a password! password.backupWasDone=I have already made a backup password.setPassword=Set Password (I already made a backup) password.makeBackup=Make Backup seed.seedWords=รหัสลับป้องกันกระเป๋าสตางค์ seed.enterSeedWords=ป้อนรหัสลับกระเป๋าสตางค์ seed.date=วันที่ในกระเป๋าสตางค์ seed.restore.title=เรียกคืนกระเป๋าสตางค์จากรหัสลับ seed.restore=เรียกกระเป๋าสตางค์คืน seed.creationDate=วันที่สร้าง seed.warn.walletNotEmpty.msg=Your Monero wallet is not empty.\n\nYou must empty this wallet before attempting to restore an older one, as mixing wallets together can lead to invalidated backups.\n\nPlease finalize your trades, close all your open offers and go to the Funds section to withdraw your monero.\nIn case you cannot access your monero you can use the emergency tool to empty the wallet.\nTo open the emergency tool press \"Alt+e\" or \"Cmd/Ctrl+e\". seed.warn.walletNotEmpty.restore=ฉันต้องการเรียกคืนอีกครั้ง seed.warn.walletNotEmpty.emptyWallet=ฉันจะทำให้กระเป๋าสตางค์ของฉันว่างเปล่าก่อน seed.warn.notEncryptedAnymore=กระเป๋าสตางค์ของคุณได้รับการเข้ารหัสแล้ว\n\nหลังจากเรียกคืน wallets จะไม่ได้รับการเข้ารหัสและคุณต้องตั้งรหัสผ่านใหม่\n\nคุณต้องการดำเนินการต่อหรือไม่ seed.warn.walletDateEmpty=As you have not specified a wallet date, haveno will have to scan the blockchain from 2013.10.09 (the BIP39 epoch date).\n\nBIP39 wallets were first introduced in haveno on 2017.06.28 (release v0.5). So you could save time by using that date.\n\nIdeally you should specify the date your wallet seed was created.\n\n\nAre you sure you want to go ahead without specifying a wallet date? seed.restore.success=กระเป๋าสตางค์ได้รับการกู้คืนข้อมูลด้วยรหัสลับเพื่อป้องกันและกู้คืนกระเป๋าสตางค์ด้วยรหัสลับใหม่แล้ว\n\nคุณจำเป็นต้องปิดและรีสตาร์ทแอ็พพลิเคชั่น seed.restore.error=เกิดข้อผิดพลาดขณะกู้คืนกระเป๋าสตางค์ด้วยรหัสลับ {0} seed.restore.openOffers.warn=You have open offers which will be removed if you restore from seed words.\nAre you sure that you want to continue? #################################################################### # Payment methods #################################################################### payment.account=บัญชี payment.account.no=หมายเลขบัญชี payment.account.name=ชื่อบัญชี payment.account.username=Username payment.account.phoneNr=Phone number payment.account.owner.fullname=ชื่อเต็มของเจ้าของบัญชี payment.account.fullName=ชื่อเต็ม (ชื่อจริง, ชื่อกลาง, นามสกุล) payment.account.state=รัฐ / จังหวัด / ภูมิภาค payment.account.city=เมือง payment.bank.country=ประเทศของธนาคาร payment.account.name.email=ชื่อเต็มของเจ้าของบัญชี / อีเมล payment.account.name.emailAndHolderId=ชื่อเต็มของเจ้าของบัญชี / อีเมล / {0} payment.bank.name=ชื่อธนาคาร payment.select.account=เลือกประเภทบัญชี payment.select.region=เลือกภูมิภาค payment.select.country=เลือกประเทศ payment.select.bank.country=เลือกประเทศของธนาคาร payment.foreign.currency=คุณแน่ใจหรือไม่ว่าต้องการเลือกสกุลเงินอื่นที่ไม่ใช่สกุลเงินเริ่มต้นของประเทศ payment.restore.default=ไม่ เรียกคืนสกุลเงินเริ่มต้น payment.email=อีเมล payment.country=ประเทศ payment.extras=ข้อกำหนดเพิ่มเติม payment.email.mobile=อีเมลหรือหมายเลขโทรศัพท์มือถือ payment.crypto.address=ที่อยู่ Crypto (เหรียญทางเลือก) payment.crypto.tradeInstantCheckbox=Trade instant (within 1 hour) with this Crypto payment.crypto.tradeInstant.popup=For instant trading it is required that both trading peers are online to be able to complete the trade in less than 1 hour.\n\nIf you have offers open and you are not available please disable those offers under the 'Portfolio' screen. payment.crypto=Crypto (เหรียญทางเลือก) payment.select.crypto=Select or search Crypto payment.secret=คำถามลับ payment.answer=คำตอบ payment.wallet=ID กระเป๋าสตางค์ payment.amazon.site=Buy giftcard at payment.ask=Ask in Trader Chat payment.uphold.accountId=ชื่อผู้ใช้ หรือ อีเมล หรือ หมายเลขโทรศัพท์ payment.moneyBeam.accountId=อีเมลหรือหมายเลขโทรศัพท์ payment.popmoney.accountId=อีเมลหรือหมายเลขโทรศัพท์ payment.promptPay.promptPayId=รหัสบัตรประชาชน/รหัสประจำตัวผู้เสียภาษี หรือเบอร์โทรศัพท์ payment.supportedCurrencies=สกุลเงินที่ได้รับการสนับสนุน payment.supportedCurrenciesForReceiver=Currencies for receiving funds payment.limitations=ข้อจำกัด payment.salt=ข้อมูลแบบสุ่มสำหรับการตรวจสอบอายุบัญชี payment.error.noHexSalt=The salt needs to be in HEX format.\nIt is only recommended to edit the salt field if you want to transfer the salt from an old account to keep your account age. The account age is verified by using the account salt and the identifying account data (e.g. IBAN). payment.accept.euro=ยอมรับการซื้อขายจากประเทศยุโรปเหล่านี้ payment.accept.nonEuro=ยอมรับการซื้อขายจากประเทศนอกสหภาพยุโรปเหล่านี้ payment.accepted.countries=ประเทศที่ยอมรับ payment.accepted.banks=ธนาคารที่ยอมรับ (ID) payment.mobile=เบอร์มือถือ payment.postal.address=รหัสไปรษณีย์ payment.national.account.id.AR=หมายเลข CBU shared.accountSigningState=Account signing status #new payment.crypto.address.dyn={0} ที่อยู่ payment.crypto.receiver.address=ที่อยู่เหรียญทางเลือกของผู้รับ payment.accountNr=หมายเลขบัญชี payment.emailOrMobile=อีเมลหรือหมายเลขโทรศัพท์มือถือ payment.useCustomAccountName=ใช้ชื่อบัญชีที่กำหนดเอง payment.maxPeriod=ระยะเวลาสูงสุดการค้าที่อนุญาต payment.maxPeriodAndLimit=Max. trade duration: {0} / Max. buy: {1} / Max. sell: {2} / Account age: {3} payment.maxPeriodAndLimitCrypto=ระยะเวลาสูงสุดทางการค้า: {0} / ขีดจำกัดสูงสุดทางการค้า: {1} payment.currencyWithSymbol=สกุลเงิน: {0} payment.nameOfAcceptedBank=ชื่อธนาคารที่ได้รับการยอมรับ payment.addAcceptedBank=เพิ่มธนาคารที่ได้รับการยอมรับ payment.clearAcceptedBanks=เคลียร์ธนาคารที่ได้รับการยอมรับ payment.bank.nameOptional=ชื่อธนาคาร (สามารถเลือกได้) payment.bankCode=รหัสธนาคาร payment.bankId=รหัสธนาคาร (BIC / SWIFT) payment.bankIdOptional=รหัสธนาคาร (BIC / SWIFT) (ไม่จำเป็นต้องกรอก) payment.branchNr=เลขที่สาขา payment.branchNrOptional=เลขที่สาขา (สามารถเลือกได้) payment.accountNrLabel=เลขที่บัญชี (IBAN) payment.accountType=ประเภทบัญชี payment.checking=การตรวจสอบ payment.savings=ออมทรัพย์ payment.personalId=รหัส ID ประจำตัวบุคคล payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle's somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver's full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account's age and signing status are preserved. payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=เมื่อมีการใช้งาน HalCash ผู้ซื้อ XMR จำเป็นต้องส่งรหัส Halcash ให้กับผู้ขายทางข้อความโทรศัพท์มือถือ\n\nโปรดตรวจสอบว่าไม่เกินจำนวนเงินสูงสุดที่ธนาคารของคุณอนุญาตให้คุณส่งด้วย HalCash จำนวนเงินขั้นต่ำในการเบิกถอนคือ 10 EUR และสูงสุดในจำนวนเงิน 600 EUR สำหรับการถอนซ้ำเป็น 3000 EUR ต่อผู้รับและต่อวัน และ 6000 EUR ต่อผู้รับและต่อเดือน โปรดตรวจสอบข้อจำกัดจากทางธนาคารคุณเพื่อให้มั่นใจได้ว่าทางธนาคารได้มีการใช้มาตรฐานข้อกำหนดเดียวกันกับดังที่ระบุไว้ ณ ที่นี่\n\nจำนวนเงินที่ถอนจะต้องเป็นจำนวนเงินหลาย 10 EUR เนื่องจากคุณไม่สามารถถอนเงินอื่น ๆ ออกจากตู้เอทีเอ็มได้ UI ในหน้าจอสร้างข้อเสนอและรับข้อเสนอจะปรับจำนวนเงิน XMR เพื่อให้จำนวนเงิน EUR ถูกต้อง คุณไม่สามารถใช้ราคาตลาดเป็นจำนวนเงิน EUR ซึ่งจะเปลี่ยนแปลงไปตามราคาที่มีการปรับเปลี่ยน\n\nในกรณีที่มีข้อพิพาทผู้ซื้อ XMR ต้องแสดงหลักฐานว่าได้ส่ง EUR แล้ว # suppress inspection "UnusedMessageFormatParameter" payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/overview/account_limits]. # suppress inspection "UnusedProperty" payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/overview/account_limits]. payment.cashDeposit.info=โปรดยืนยันว่าธนาคารของคุณได้อนุมัติให้คุณสามารถส่งเงินสดให้กับบัญชีบุคคลอื่นได้ ตัวอย่างเช่น บางธนาคารที่ไม่ได้มีการบริการถ่ายโอนเงินสดอย่าง Bank of America และ Wells Fargo payment.revolut.info=Revolut requires the 'Username' as account ID not the phone number or email as it was the case in the past. payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a 'Username'.\nPlease enter your Revolut 'Username' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account payment.cashapp.info=โปรดทราบว่า Cash App มีความเสี่ยงในการเรียกเงินคืนสูงกว่าการโอนเงินผ่านธนาคารส่วนใหญ่ payment.venmo.info=โปรดทราบว่า Venmo มีความเสี่ยงในการเรียกเงินคืนสูงกว่าการโอนเงินผ่านธนาคารส่วนใหญ่ payment.paypal.info=โปรดทราบว่า PayPal มีความเสี่ยงในการเรียกเงินคืนสูงกว่าการโอนเงินผ่านธนาคารส่วนใหญ่ payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account payment.payByMail.contact=ข้อมูลติดต่อ payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.f2f.contact=ข้อมูลติดต่อ payment.f2f.contact.prompt=How would you like to be contacted by the trading peer? (email address, phone number,...) payment.f2f.city=เมืองสำหรับการประชุมแบบเห็นหน้ากัน payment.f2f.city.prompt=ชื่อเมืองจะแสดงพร้อมกับข้อเสนอ payment.shared.optionalExtra=ข้อมูลตัวเลือกเพิ่มเติม payment.shared.extraInfo=ข้อมูลเพิ่มเติม payment.shared.extraInfo.offer=ข้อมูลเพิ่มเติมเกี่ยวกับข้อเสนอ payment.shared.extraInfo.prompt.paymentAccount=กำหนดคำศัพท์ เงื่อนไข หรือรายละเอียดพิเศษใดๆ ที่คุณต้องการให้แสดงพร้อมข้อเสนอของคุณสำหรับบัญชีการชำระเงินนี้ (ผู้ใช้จะเห็นข้อมูลนี้ก่อนที่จะยอมรับข้อเสนอ) payment.shared.extraInfo.prompt.offer=กำหนดเงื่อนไขพิเศษ ข้อกำหนด หรือรายละเอียดใด ๆ ที่คุณต้องการแสดงพร้อมกับข้อเสนอของคุณ payment.shared.extraInfo.noDeposit=รายละเอียดการติดต่อและเงื่อนไขข้อเสนอ payment.f2f.info.openURL=เปิดหน้าเว็บ payment.f2f.offerbook.tooltip.countryAndCity=Country and city: {0} / {1} payment.shared.extraInfo.tooltip=ข้อมูลเพิ่มเติม: {0} payment.japan.bank=ธนาคาร payment.japan.branch=Branch payment.japan.account=บัญชี payment.japan.recipient=ชื่อ payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=เพื่อความปลอดภัยของคุณ เราขอแนะนำอย่างยิ่งให้หลีกเลี่ยงการใช้ Paysafecard PINs ในการชำระเงิน\n\n\ ธุรกรรมที่ดำเนินการผ่าน PIN ไม่สามารถตรวจสอบได้อย่างอิสระสำหรับการระงับข้อพิพาท หากเกิดปัญหา อาจไม่สามารถกู้คืนเงินได้\n\n\ เพื่อความปลอดภัยของธุรกรรมและรองรับการระงับข้อพิพาท โปรดใช้วิธีการชำระเงินที่มีบันทึกการทำธุรกรรมที่ตรวจสอบได้ # We use constants from the code so we do not use our normal naming convention # dynamic values are not recognized by IntelliJ # Only translate general terms NATIONAL_BANK=การโอนเงินผ่านธนาคารแห่งชาติ SAME_BANK=โอนเงินผ่านธนาคารเดียวกัน SPECIFIC_BANKS=การโอนเงินกับธนาคารเฉพาะ US_POSTAL_MONEY_ORDER=US Postal Money Order ใบสั่งซื้อทางไปรษณีย์ของสหรัฐฯ CASH_DEPOSIT=ฝากเงินสด PAY_BY_MAIL=Pay By Mail MONEY_GRAM=MoneyGram WESTERN_UNION=Western Union F2F=เห็นหน้ากัน (แบบตัวต่อตัว) JAPAN_BANK=Japan Bank Furikomi AUSTRALIA_PAYID=Australian PayID # suppress inspection "UnusedProperty" NATIONAL_BANK_SHORT=ธนาคารแห่งชาติ # suppress inspection "UnusedProperty" SAME_BANK_SHORT=ธนาคารเดียวกัน # suppress inspection "UnusedProperty" SPECIFIC_BANKS_SHORT=ธนาคารเฉพาะ # suppress inspection "UnusedProperty" US_POSTAL_MONEY_ORDER_SHORT=US Money Order ใบสั่งทางการเงินของสหรัฐฯ # suppress inspection "UnusedProperty" CASH_DEPOSIT_SHORT=ฝากเงินสด # suppress inspection "UnusedProperty" PAY_BY_MAIL_SHORT=PayByMail # suppress inspection "UnusedProperty" MONEY_GRAM_SHORT=MoneyGram # suppress inspection "UnusedProperty" WESTERN_UNION_SHORT=Western Union # suppress inspection "UnusedProperty" F2F_SHORT=F2F # suppress inspection "UnusedProperty" JAPAN_BANK_SHORT=Japan Furikomi # suppress inspection "UnusedProperty" AUSTRALIA_PAYID_SHORT=PayID # Do not translate brand names # suppress inspection "UnusedProperty" UPHOLD=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY=Popmoney # suppress inspection "UnusedProperty" REVOLUT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY=WeChat Pay # suppress inspection "UnusedProperty" SEPA=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT=การชำระเงินทันใจของ SEPA # suppress inspection "UnusedProperty" FASTER_PAYMENTS=Faster Payments # suppress inspection "UnusedProperty" SWISH=Swish # suppress inspection "UnusedProperty" ZELLE=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS=Cryptos # suppress inspection "UnusedProperty" PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT=Cryptos Instant # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY=OKPay # suppress inspection "UnusedProperty" CASH_APP=Cash App # suppress inspection "UnusedProperty" VENMO=Venmo # suppress inspection "UnusedProperty" UPHOLD_SHORT=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM_SHORT=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY_SHORT=Popmoney # suppress inspection "UnusedProperty" REVOLUT_SHORT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY_SHORT=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY_SHORT=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY_SHORT=WeChat Pay # suppress inspection "UnusedProperty" SEPA_SHORT=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT_SHORT=SEPA Instant # suppress inspection "UnusedProperty" FASTER_PAYMENTS_SHORT=Faster Payments # suppress inspection "UnusedProperty" SWISH_SHORT=Swish # suppress inspection "UnusedProperty" ZELLE_SHORT=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY_SHORT=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER_SHORT=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH_SHORT=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS_SHORT=Cryptos # suppress inspection "UnusedProperty" PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT_SHORT=Cryptos Instant # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY_SHORT=OKPay # suppress inspection "UnusedProperty" CASH_APP_SHORT=Cash App # suppress inspection "UnusedProperty" VENMO_SHORT=Venmo #################################################################### # Validation #################################################################### validation.empty=ไม่อนุญาตให้ใส่ข้อมูลที่ว่างเปล่า validation.NaN=การป้อนข้อมูลไม่ใช่ตัวเลขที่ถูกต้อง validation.notAnInteger=ค่าที่ป้อนไม่ใช่ค่าจำนวนเต็ม validation.zero=ไม่อนุญาตให้ป้อนข้อมูลเป็น 0 validation.negative=ไม่อนุญาตให้ใช้ค่าลบ validation.traditional.tooSmall=ไม่อนุญาตให้ป้อนข้อมูลที่มีขนาดเล็กกว่าจำนวนเป็นไปได้ต่ำสุด validation.traditional.tooLarge=ไม่อนุญาตให้ป้อนข้อมูลที่มีขนาดใหญ่กว่าจำนวนสูงสุดที่เป็นไปได้ validation.xmr.fraction=Input will result in a monero value of less than 1 satoshi validation.xmr.tooLarge=ไม่อนุญาตให้ป้อนข้อมูลขนาดใหญ่กว่า {0} validation.xmr.tooSmall=ไม่อนุญาตให้ป้อนข้อมูลที่มีขนาดเล็กกว่า {0} validation.passwordTooShort=The password you entered is too short. It needs to have a min. of 8 characters. validation.passwordTooLong=รหัสผ่านที่คุณป้อนยาวเกินไป ต้องมีความยาวไม่เกิน 50 ตัว validation.sortCodeNumber={0} ต้องประกอบด้วย {1} ตัวเลข validation.sortCodeChars={0} ต้องประกอบด้วย {1} ตัวอักษร validation.bankIdNumber={0} ต้องประกอบด้วย {1} ตัวเลข validation.accountNr=หมายเลขบัญชีต้องประกอบด้วย {0} ตัวเลข validation.accountNrChars=หมายเลขบัญชีต้องประกอบด้วย {0} ตัวอักษร validation.xmr.invalidAddress=ที่อยู่ไม่ถูกต้อง โปรดตรวจสอบแบบฟอร์มที่อยู่ validation.integerOnly=โปรดป้อนตัวเลขจำนวนเต็มเท่านั้น validation.inputError=การป้อนข้อมูลของคุณเกิดข้อผิดพลาด: \n{0} validation.xmr.exceedsMaxTradeLimit=ขีดจำกัดการเทรดของคุณคือ {0} validation.nationalAccountId={0} ต้องประกอบด้วย {1} ตัวเลข #new validation.invalidInput=ใส่ข้อมูลไม่ถูกต้อง: {0} validation.accountNrFormat=หมายเลขบัญชีต้องเป็นรูปแบบ: {0} # suppress inspection "UnusedProperty" validation.crypto.wrongStructure=การตรวจสอบความถูกต้องของที่อยู่ล้มเหลวเนื่องจากไม่ตรงกับโครงสร้างของที่อยู่ {0} # suppress inspection "UnusedProperty" validation.crypto.ltz.zAddressesNotSupported=LTZ address must start with L. Addresses starting with z are not supported. # suppress inspection "UnusedProperty" validation.crypto.zAddressesNotSupported=ZEC addresses must start with t. Addresses starting with z are not supported. # suppress inspection "UnusedProperty" validation.crypto.invalidAddress=ที่อยู่ไม่ใช่ที่อยู่ {0} ที่ถูกต้อง! {1} # suppress inspection "UnusedProperty" validation.crypto.liquidBitcoin.invalidAddress=Native segwit addresses (those starting with 'lq') are not supported. validation.bic.invalidLength=Input length must be 8 or 11 validation.bic.letters=รหัสธนาคารและรหัสประเทศต้องเป็นตัวอักษร validation.bic.invalidLocationCode=BIC มีรหัสตำแหน่งไม่ถูกต้อง validation.bic.invalidBranchCode=BIC มีรหัสสาขาไม่ถูกต้อง validation.bic.sepaRevolutBic=บัญชี Revolut Sepa ไม่รองรับ validation.btc.invalidFormat=Invalid format for a Bitcoin address. validation.email.invalidAddress=ที่อยู่ไม่ถูกต้อง validation.iban.invalidCountryCode=รหัสประเทศไม่ถูกต้อง validation.iban.checkSumNotNumeric=Checksum ต้องเป็นตัวเลข validation.iban.nonNumericChars=ไม่พบอักขระที่เป็นตัวเลขและตัวอักษร validation.iban.checkSumInvalid=การตรวจสอบ IBAN ไม่ถูกต้อง validation.iban.invalidLength=Number must have a length of 15 to 34 chars. validation.interacETransfer.invalidAreaCode=รหัสพื้นที่ที่ไม่ใช่แคนาดา validation.interacETransfer.invalidPhone=Please enter a valid 11 digit phone number (ex: 1-123-456-7890) or an email address validation.interacETransfer.invalidQuestion=ต้องประกอบด้วยตัวอักษร ตัวเลข เว้นวรรค และ/หรือ สัญลักษณ์ ' _ , . ? - validation.interacETransfer.invalidAnswer=ต้องเป็นคำเดียว และประกอบด้วยตัวอักษร ตัวเลข และ/หรือ สัญลักษณ์ - เท่านั้น validation.inputTooLarge=ข้อมูลที่ป้อนต้องไม่เป็นจำนวนที่มากกว่า {0} validation.inputTooSmall=การป้อนเข้าจะต้องมีจำนวนมากกว่า {0} validation.inputToBeAtLeast=Input has to be at least {0} validation.amountBelowDust=An amount below the dust limit of {0} satoshi is not allowed. validation.length=ความยาวจะต้องอยู่ระหว่าง {0} และ {1} validation.fixedLength=Length must be {0} validation.pattern=การป้อนเข้าจะต้องเป็นรูปแบบ {0} validation.noHexString=การป้อนเข้านั้นคือไม่ใช่รูปแบบของ HEX validation.advancedCash.invalidFormat=จะต้องเป็นอีเมลหรือรหัสกระเป๋าสตางค์ที่ใช้งานได้: X000000000000 validation.invalidUrl=This is not a valid URL validation.mustBeDifferent=Your input must be different from the current value validation.cannotBeChanged=Parameter cannot be changed validation.numberFormatException=Number format exception {0} validation.mustNotBeNegative=Input must not be negative validation.phone.missingCountryCode=Need two letter country code to validate phone number validation.phone.invalidCharacters=Phone number {0} contains invalid characters validation.phone.insufficientDigits=There are not enough digits in {0} to be a valid phone number validation.phone.tooManyDigits=There are too many digits in {0} to be a valid phone number validation.phone.invalidDialingCode=Country dialing code for number {0} is invalid for country {1}. The correct dialing code is {2}. validation.invalidAddressList=Must be comma separated list of valid addresses ================================================ FILE: core/src/main/resources/i18n/displayStrings_tr.properties ================================================ # Keep display strings organized by domain # Naming convention: We use camelCase and dot separated name spaces. # Use as many sub spaces as required to make the structure clear, but as little as possible. # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break # in the display but only in the editor. # Please use in all language files the exact same order of the entries, that way a comparison is easier. # Please try to keep the length of the translated string similar to English. If it is longer it might break layout or # get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. #################################################################### # Shared #################################################################### shared.readMore=Daha fazla oku shared.openHelp=Yardımı Aç shared.warning=Uyarı shared.close=Kapat shared.closeAnywayDanger=Yine de kapat (TEHLİKE!) shared.okWait=Tamam, bekleyeceğim shared.cancel=İptal shared.ok=Tamam shared.yes=Evet shared.no=Hayır shared.iUnderstand=Anladım shared.continueAnyway=Yine de devam et shared.na=Mevcut değil shared.shutDown=Kapat shared.reportBug=GitHub'da hata bildir shared.buyMonero=Monero Satın Al shared.sellMonero=Monero Sat shared.buyCurrency={0} satın al shared.sellCurrency={0} sat shared.buyCurrency.locked={0} satın al 🔒 shared.sellCurrency.locked={0} sat 🔒 shared.buyingXMRWith={0} ile XMR satın alınıyor shared.sellingXMRFor={0} karşılığında XMR satılıyor shared.buyingCurrency={0} satın alınıyor (XMR satılıyor) shared.sellingCurrency={0} satılıyor (XMR satın alınıyor) shared.buy=satın al shared.sell=sat shared.buying=satın alınıyor shared.selling=satılıyor shared.P2P=Eşler Arası shared.oneOffer=teklif shared.multipleOffers=teklifler shared.Offer=Teklif shared.offerVolumeCode={0} Teklif Hacmi shared.openOffers=açık teklifler shared.trade=işlem shared.trades=işlemler shared.openTrades=açık işlemler shared.dateTime=Tarih/Saat shared.price=Fiyat shared.priceWithCur={0} cinsinde shared.priceInCurForCur={1} / {0} cinsinde shared.fixedPriceInCurForCur={1} için sabit fiyat {0} cinsinden shared.amount=Miktar shared.txFee=İşlem Ücreti shared.tradeFee=İşlem Ücreti shared.buyerSecurityDeposit=Alıcı Depozitosu shared.sellerSecurityDeposit=Satıcı Depozitosu shared.amountWithCur={0} cinsinden miktar shared.volumeWithCur={0} cinsinden hacim shared.currency=Para Birimi shared.market=Piyasa shared.deviation=Sapma shared.paymentMethod=Ödeme yöntemi shared.tradeCurrency=İşlem para birimi shared.offerType=Teklif türü shared.details=Ayrıntılar shared.address=Adres shared.balanceWithCur={0} cinsinden bakiye shared.utxo=Harcanmamış işlem çıktısı shared.txId=İşlem ID shared.confirmations=Onaylar shared.revert=İşlemi Geri Al shared.select=Seç shared.usage=Kullanım shared.state=Durum shared.tradeId=İşlem ID shared.offerId=Teklif ID shared.traderId=Tacir ID shared.bankName=Banka adı shared.acceptedBanks=Kabul edilen bankalar shared.amountMinMax=Miktar (min - max) shared.amountHelp=Bir teklifin minimum ve maksimum miktarı ayarlanmışsa, bu aralıktaki herhangi bir miktarla işlem yapabilirsiniz. shared.remove=Kaldır shared.goTo={0}'e git shared.XMRMinMax=XMR (min - max) shared.removeOffer=Teklifi kaldır shared.dontRemoveOffer=Teklifi kaldırma shared.editOffer=Teklifi düzenle shared.openLargeQRWindow=Büyük QR kodu penceresini aç shared.chooseTradingAccount=İşlem hesabını seç shared.faq=SSS sayfasını ziyaret et shared.yesCancel=Evet, iptal et shared.nextStep=Sonraki adım shared.fundFromSavingsWalletButton=Haveno cüzdanından fonları uygula shared.fundFromExternalWalletButton=Fonlama için harici cüzdanını aç shared.openDefaultWalletFailed=Bir Monero cüzdan uygulaması açılamadı. Yüklü olduğundan emin misiniz? shared.belowInPercent=Piyasa fiyatının altında % shared.aboveInPercent=Piyasa fiyatının üzerinde % shared.enterPercentageValue=% değeri gir shared.OR=VEYA shared.notEnoughFunds=Haveno cüzdanınızda bu işlem için yeterli fon yok—{0} gerekli ama sadece {1} mevcut.\n\nLütfen harici bir cüzdandan fon ekleyin veya Haveno cüzdanınıza Fonlar > Fon Al kısmından fon aktarın. shared.waitingForFunds=Fonlar bekleniyor... shared.yourDepositTransactionId=Depozito işlem ID'niz shared.peerDepositTransactionId=Eşin depozito işlem ID'si shared.makerDepositTransactionId=Yapıcı'nın depozito işlem ID'si shared.takerDepositTransactionId=Alıcı'nın depozito işlem ID'si shared.TheXMRBuyer=XMR alıcısı shared.You=Sen shared.preparingConfirmation=Onay hazırlanıyor... shared.sendingConfirmation=Onay gönderiliyor... shared.sendingConfirmationAgain=Lütfen tekrar onay gönderin shared.exportCSV=CSV'ye Aktar shared.exportJSON=JSON'a Aktar shared.summary=Özeti Göster shared.noDateAvailable=Geçerli tarih yok shared.noDetailsAvailable=Geçerli detay yok shared.notUsedYet=Henüz kullanılmadı shared.date=Tarih shared.sendFundsDetailsWithFee=Gönderilen: {0}\n\nAlıcı adresi: {1}\n\nEk madenci ücreti: {2}\n\nBu tutarı göndermek istediğinizden emin misiniz? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno, bu işlemin minimum toz eşiğinin altında bir değişim çıktısı oluşturacağını (ve bu nedenle Monero konsensüs kuralları tarafından izin verilmediğini) tespit etti. Bunun yerine, bu toz ({0} satoshi{1}) madencilik ücretine eklenecektir.\n\n\n shared.copyToClipboard=Panoya kopyala shared.copiedToClipboard=Panoya kopyalandı! shared.language=Dil shared.country=Ülke shared.applyAndShutDown=Uygula ve kapat shared.selectPaymentMethod=Ödeme yöntemini seç shared.accountNameAlreadyUsed=Bu hesap adı başka bir kayıtlı hesap için zaten kullanılıyor.\nLütfen başka bir ad seçin. shared.askConfirmDeleteAccount=Seçilen hesabı gerçekten silmek istiyor musunuz? shared.cannotDeleteAccount=Bu hesabı silemezsiniz çünkü açık bir teklif (veya açık bir işlem) kullanılıyor. shared.noAccountsSetupYet=Henüz hesap ayarlanmadı shared.manageAccounts=Hesapları yönet shared.addNewAccount=Yeni hesap ekle shared.ExportAccounts=Hesapları Dışa Aktar shared.importAccounts=Hesapları İçe Aktar shared.createNewAccount=Yeni hesap oluştur shared.createNewAccountDescription=Hesap bilgileriniz yerel olarak cihazınızda saklanır ve yalnızca ticaret ortağınızla ve bir anlaşmazlık açılırsa hakemle paylaşılır. shared.saveNewAccount=Yeni hesabı kaydet shared.selectedAccount=Seçilen hesap shared.deleteAccount=Hesabı sil shared.errorMessageInline=\nHata mesajı: {0} shared.errorMessage=Hata mesajı shared.information=Bilgi shared.name=Ad shared.id=Kimlik shared.dashboard=Gösterge Paneli shared.accept=Kabul et shared.balance=Bakiye shared.save=Kaydet shared.onionAddress=Onion adresi shared.supportTicket=destek bileti shared.dispute=anlaşmazlık shared.mediationCase=arabuluculuk vakası shared.seller=satıcı shared.buyer=alıcı shared.allEuroCountries=Tüm Euro ülkeleri shared.acceptedTakerCountries=Kabul edilen alıcı ülkeler shared.tradePrice=İşlem fiyatı shared.tradeAmount=İşlem miktarı shared.tradeVolume=İşlem hacmi shared.reservedAmount=Ayrılmış miktar shared.invalidKey=Girdiğiniz anahtar doğru değil. shared.enterPrivKey=Kilidi açmak için özel anahtar girin shared.payoutTxId=Ödeme işlem ID'si shared.contractAsJson=JSON formatında sözleşme shared.viewContractAsJson=JSON formatında sözleşmeyi görüntüle shared.contract.title=İşlem kimliği ile sözleşme: {0} shared.paymentDetails=XMR {0} ödeme detayları shared.securityDeposit=Güvenlik depozitosu shared.yourSecurityDeposit=Sizin güvenlik depozitonuz shared.contract=Sözleşme shared.messageArrived=Mesaj geldi. shared.messageStoredInMailbox=Mesaj posta kutusunda saklandı. shared.messageSendingFailed=Mesaj gönderme başarısız oldu. Hata: {0} shared.unlock=Kilidi aç shared.toReceive=almak için shared.toSpend=harcamak için shared.xmrAmount=XMR miktarı shared.yourLanguage=Dilleriniz shared.addLanguage=Dil ekle shared.total=Toplam shared.totalsNeeded=Gereken fonlar shared.tradeWalletAddress=İşlem cüzdan adresi shared.tradeWalletBalance=İşlem cüzdan bakiyesi shared.reserveExactAmount=Yalnızca gerekli fonları ayırın. Teklifinizin aktif hale gelmesi için bir madencilik ücreti ve yaklaşık 20 dakika gereklidir. shared.makerTxFee=Yapıcı: {0} shared.takerTxFee=Alıcı: {0} shared.iConfirm=Onaylıyorum shared.openURL={0}'i aç shared.fiat=Fiat shared.crypto=Kripto shared.preciousMetals=Değerli Madenler shared.traditional=Nakit shared.otherAssets=diğer varlıklar shared.other=Diğer shared.all=Hepsi shared.edit=Düzenle shared.advancedOptions=Gelişmiş seçenekler shared.interval=Aralık shared.actions=Eylemler shared.buyerUpperCase=Alıcı shared.sellerUpperCase=Satıcı shared.new=YENİ shared.learnMore=Daha fazla bilgi edin shared.dismiss=Kapat shared.selectedArbitrator=Seçilen hakem shared.selectedMediator=Seçilen arabulucu shared.selectedRefundAgent=Seçilen hakem shared.mediator=Arabulucu shared.arbitrator=Hakem shared.refundAgent=Hakem shared.refundAgentForSupportStaff=İade temsilcisi shared.delayedPayoutTxId=Gecikmiş ödeme işlem kimliği shared.delayedPayoutTxReceiverAddress=Gecikmiş ödeme işlemi gönderildi shared.unconfirmedTransactionsLimitReached=Şu anda çok fazla onaylanmamış işleminiz var. Lütfen daha sonra tekrar deneyin. shared.numItemsLabel=Girdi sayısı: {0} shared.filter=Filtre shared.enabled=Etkin shared.pending=Beklemede shared.me=Ben shared.maker=Yapıcı shared.taker=Alıcı #################################################################### # UI views #################################################################### #################################################################### # MainView #################################################################### mainView.menu.market=Piyasa mainView.menu.buyXmr=XMR Satın Al mainView.menu.sellXmr=XMR Sat mainView.menu.portfolio=Portföy mainView.menu.funds=Fonlar mainView.menu.support=Destek mainView.menu.settings=Ayarlar mainView.menu.account=Hesap mainView.marketPriceWithProvider.label=Fiyat {0} tarafından mainView.marketPrice.havenoInternalPrice=Son Haveno işlem fiyatı mainView.marketPrice.tooltip.havenoInternalPrice=Harici fiyat besleme sağlayıcılarından piyasa fiyatı yok.\n\ Görüntülenen fiyat, o para birimi için en son Haveno işlem fiyatıdır. mainView.marketPrice.tooltip={0}{1} Piyasası\nSon güncelleme: {2}\nSağlayıcı düğüm URL'si: {3} mainView.balance.available=Bakiye mainView.balance.reserved=Tekliflerde ayrılmış mainView.balance.pending=Beklemedeki bakiye mainView.balance.reserved.short=Ayrılmış mainView.balance.pending.short=Beklemede mainView.footer.usingTor=(Tor üzerinden) mainView.footer.localhostMoneroNode=(localhost) mainView.footer.clearnet=(clearnet üzerinden) mainView.footer.xmrInfo={0} {1} mainView.footer.xmrFeeRate=/ Ücret oranı: {0} sat/vB mainView.footer.xmrInfo.initializing=Haveno ağına bağlanıyor mainView.footer.xmrInfo.synchronizingWith={0} ile senkronize ediliyor blok: {1} / {2} mainView.footer.xmrInfo.connectedTo={0}'a bağlandı blok {1} mainView.footer.xmrInfo.synchronizingWalletWith={0} ile cüzdan senkronize ediliyor blok: {1} / {2} mainView.footer.xmrInfo.syncedWith={0} ile senkronize edildi blok {1} mainView.footer.xmrInfo.connectingTo=Bağlanıyor mainView.footer.xmrInfo.connectionFailed=Bağlantı başarısız oldu mainView.footer.xmrPeers=Monero ağ eşleri: {0} mainView.footer.p2pPeers=Haveno ağ eşleri: {0} mainView.bootstrapState.connectionToTorNetwork=(1/4) Tor ağına bağlanılıyor... mainView.bootstrapState.torNodeCreated=(2/4) Tor düğümü oluşturuldu mainView.bootstrapState.hiddenServicePublished=(3/4) Gizli Hizmet yayımlandı mainView.bootstrapState.initialDataReceived=(4/4) İlk veri alındı mainView.bootstrapWarning.noSeedNodesAvailable=Köken düğümü yok mainView.bootstrapWarning.noNodesAvailable=Köken düğümü ve eş yok mainView.bootstrapWarning.bootstrappingToP2PFailed=Haveno ağına başlatma başarısız oldu mainView.p2pNetworkWarnMsg.noNodesAvailable=Veri isteği için köken düğümü veya kayıtlı eş yok.\nLütfen internet bağlantınızı kontrol edin veya uygulamayı yeniden başlatmayı deneyin. mainView.p2pNetworkWarnMsg.connectionToP2PFailed=Haveno ağına bağlanma başarısız oldu (rapor edilen hata: {0}).\nLütfen internet bağlantınızı kontrol edin veya uygulamayı yeniden başlatmayı deneyin. mainView.walletServiceErrorMsg.timeout=Zaman aşımından dolayı Monero ağına bağlanma başarısız oldu. mainView.walletServiceErrorMsg.connectionError=Hata nedeniyle Monero ağına bağlanma başarısız oldu: {0} mainView.walletServiceErrorMsg.rejectedTxException=Bir işlem ağdan reddedildi.\n\n{0} mainView.networkWarning.allConnectionsLost=Tüm {0} ağ eşlerine bağlantınız kesildi.\nBelki internet bağlantınızı kaybettiniz ya da bilgisayarınız bekleme modundaydı. mainView.networkWarning.localhostMoneroLost=Yerel Monero düğümüne bağlantınızı kaybettiniz.\nDiğer Monero düğümlerine bağlanmak için Haveno uygulamasını yeniden başlatın veya yerel Monero düğümünü yeniden başlatın. mainView.version.update=(Güncelleme mevcut) mainView.status.connections=Gelen bağlantılar: {0}\nGiden bağlantılar: {1} #################################################################### # MarketView #################################################################### market.tabs.offerBook=Teklif kitabı market.tabs.spreadCurrency=Para Birimine Göre Teklifler market.tabs.spreadPayment=Ödeme Yöntemine Göre Teklifler market.tabs.trades=İşlemler # OfferBookChartView market.offerBook.sellOffersHeaderLabel={0} sat market.offerBook.buyOffersHeaderLabel={0} al market.offerBook.buy=Monero almak istiyorum market.offerBook.sell=Monero satmak istiyorum # SpreadView market.spread.numberOfOffersColumn=Tüm teklifler ({0}) market.spread.numberOfBuyOffersColumn=XMR al ({0}) market.spread.numberOfSellOffersColumn=XMR sat ({0}) market.spread.totalAmountColumn=Toplam XMR ({0}) market.spread.spreadColumn=Fark market.spread.expanded=Genişletilmiş görünüm # TradesChartsView market.trades.nrOfTrades=İşlemler: {0} market.trades.tooltip.volumeBar=Hacim: {0} / {1}\nİşlem sayısı: {2}\nTarih: {3} market.trades.tooltip.candle.open=Açılış: market.trades.tooltip.candle.close=Kapanış: market.trades.tooltip.candle.high=En Yüksek: market.trades.tooltip.candle.low=En Düşük: market.trades.tooltip.candle.average=Ortalama: market.trades.tooltip.candle.median=Medyan: market.trades.tooltip.candle.date=Tarih: market.trades.showVolumeInUSD=Hacmi USD olarak göster #################################################################### # OfferView #################################################################### offerbook.createOffer=Teklif oluştur offerbook.takeOffer=Teklif al offerbook.takeOffer.createAccount=Hesap oluştur ve teklifi al offerbook.takeOffer.enterChallenge=Teklif şifresini girin offerbook.trader=Yatırımcı offerbook.offerersBankId=Yapıcının banka kimliği (BIC/SWIFT): {0} offerbook.offerersBankName=Yapıcının banka adı: {0} offerbook.offerersBankSeat=Yapıcının banka ülke merkezi: {0} offerbook.offerersAcceptedBankSeatsEuro=Kabul edilen banka ülkeleri (alıcı): Tüm Euro ülkeleri offerbook.offerersAcceptedBankSeats=Kabul edilen banka ülkeleri (alıcı):\n {0} offerbook.availableOffersToBuy={0} ile {1} satın al offerbook.availableOffersToSell={0} için {1} sat offerbook.filterByCurrency=Para birimini seç offerbook.filterByPaymentMethod=Ödeme yöntemini seç offerbook.matchingOffers=Uygun Teklif offerbook.filterNoDeposit=Depozito yok offerbook.noDepositOffers=Depozitosuz teklifler (şifre gereklidir) offerbook.timeSinceSigning=Hesap bilgisi offerbook.timeSinceSigning.info.arbitrator=bir hakem tarafından imzalandı ve eş hesaplarını imzalayabilir offerbook.timeSinceSigning.info.peer=bir eş tarafından imzalandı, limitlerin kaldırılması için %d gün bekleniyor offerbook.timeSinceSigning.info.peerLimitLifted=bir eş tarafından imzalandı ve limitler kaldırıldı offerbook.timeSinceSigning.info.signer=bir eş tarafından imzalandı ve eş hesaplarını imzalayabilir (limitler kaldırıldı) offerbook.timeSinceSigning.info.banned=hesap yasaklandı offerbook.timeSinceSigning.daysSinceSigning={0} gün offerbook.timeSinceSigning.daysSinceSigning.long=imzalandığından beri {0} gün offerbook.timeSinceSigning.tooltip.accountLimit=Hesap limiti: {0} offerbook.timeSinceSigning.tooltip.accountLimitLifted=Hesap limiti kaldırıldı offerbook.timeSinceSigning.tooltip.info.unsigned=Bu hesap henüz imzalanmadı offerbook.timeSinceSigning.tooltip.info.signed=Bu hesap imzalandı offerbook.timeSinceSigning.tooltip.info.signedAndLifted=Bu hesap imzalandı ve eş hesaplarını imzalayabilir offerbook.timeSinceSigning.tooltip.checkmark.buyXmr=imzalı bir hesaptan XMR al offerbook.timeSinceSigning.tooltip.checkmark.wait=minimal {0} gün bekleyin offerbook.timeSinceSigning.tooltip.learnMore=Daha fazla bilgi edin offerbook.xmrAutoConf=Otomatik onay etkin mi offerbook.buyXmrWith=XMR satın al: offerbook.sellXmrFor=XMR'i şunlar için satın: offerbook.timeSinceSigning.help=Bir imzalı ödeme hesabı olan bir eş ile başarılı bir şekilde işlem yaptığınızda, ödeme hesabınız imzalanır.\n\ {0} gün sonra, başlangıç limiti {1} kaldırılır ve hesabınız diğer eşlerin ödeme hesaplarını imzalayabilir. offerbook.timeSinceSigning.notSigned=Henüz imzalanmadı offerbook.timeSinceSigning.notSigned.ageDays={0} gün offerbook.timeSinceSigning.notSigned.noNeed=Gerekli değil shared.notSigned.noNeedDays=Bu hesap türü imzalama gerektirmez ve {0} gün önce oluşturulmuştur shared.notSigned.noNeedAlts=Kripto para hesaplarında imzalama veya yaşlanma özelliği yoktur offerbook.nrOffers=Teklif sayısı: {0} offerbook.volume={0} (min - maks) offerbook.deposit=Mevduat XMR (%) offerbook.deposit.help=Her yatırımcı tarafından işlemi garanti altına almak için ödenen mevduat. İşlem tamamlandığında geri verilecektir. offerbook.createNewOffer=Teklif oluştur {0} {1} offerbook.createOfferDisabled.tooltip=Bir seferde sadece bir teklif oluşturabilirsiniz offerbook.takeOfferButton.tooltip=Teklifi al {0} offerbook.setupNewAccount=Yeni bir ticaret hesabı kur offerbook.removeOffer.success=Teklif kaldırma başarılı oldu. offerbook.removeOffer.failed=Teklif kaldırma başarısız oldu:\n{0} offerbook.deactivateOffer.failed=Teklifi devre dışı bırakma başarısız oldu:\n{0} offerbook.activateOffer.failed=Teklifi yayınlama başarısız oldu:\n{0} offerbook.withdrawFundsHint=Teklif kaldırıldı. Bu teklif için artık fonlar ayrılmadı. \ Mevcut fonları {0} ekranında dış bir cüzdana gönderebilirsiniz. offerbook.warning.noTradingAccountForCurrency.headline=Seçilen para birimi için ödeme hesabı yok offerbook.warning.noTradingAccountForCurrency.msg=Seçilen para birimi için bir ödeme hesabı kurmadınız. offerbook.warning.noMatchingAccount.headline=Eşleşen ödeme hesabı yok. offerbook.warning.noMatchingAccount.msg=Bu teklif, henüz kurmadığınız bir ödeme yöntemi kullanıyor. \n\nŞimdi yeni bir ödeme hesabı kurmak ister misiniz? offerbook.warning.counterpartyTradeRestrictions=Karşı taraf ticaret kısıtlamaları nedeniyle bu teklif alınamaz offerbook.warning.newVersionAnnouncement=Bu yazılım sürümü ile, ticaret yapan eşler birbirlerinin ödeme hesaplarını doğrulayabilir ve imzalayabilir, böylece güvenilir ödeme hesapları ağı oluşturulabilir.\n\n\ Doğrulanmış ödeme hesabı olan bir eş ile başarılı bir şekilde ticaret yaptıktan sonra, ödeme hesabınız imzalanır ve ticaret limitleri belirli bir zaman aralığından sonra kaldırılır (bu aralığın uzunluğu doğrulama yöntemine bağlıdır).\n\n\ Hesap imzalama hakkında daha fazla bilgi için, lütfen [HYPERLINK:https://docs.haveno.exchange/overview/account_limits/#account-signing] belgelere bakın. popup.warning.tradeLimitDueAccountAgeRestriction.seller=İzin verilen ticaret miktarı, aşağıdaki kriterlere dayanan güvenlik kısıtlamaları nedeniyle {0} ile sınırlıdır:\n\ - Alıcının hesabı bir hakem veya eş tarafından imzalanmamış\n\ - Alıcının hesabının imzalanmasından bu yana en az 30 gün geçmemiş\n\ - Bu teklif için ödeme yöntemi, banka geri ödemeleri için riskli kabul edilir\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=İzin verilen ticaret miktarı, aşağıdaki kriterlere dayanan güvenlik kısıtlamaları nedeniyle {0} ile sınırlıdır:\n\ - Hesabınız bir hakem veya eş tarafından imzalanmamış\n\ - Hesabınızın imzalanmasından bu yana en az 30 gün geçmemiş\n\ - Bu teklif için ödeme yöntemi, banka geri ödemeleri için riskli kabul edilir\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=Bu ödeme yöntemi, tüm alıcıların yeni hesapları olduğu için geçici olarak {0} ile sınırlıdır {1} kadar.\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=Teklifiniz {0} aşan imzasız ve yaşlanmış hesaplara sahip alıcılara sınırlı olacak.\n\n{1} offerbook.warning.wrongTradeProtocol=Bu teklif, yazılımınızın sürümünde kullanılan protokol sürümünden farklı bir protokol sürümü gerektiriyor.\n\nLütfen en son sürümün yüklü olup olmadığını kontrol edin, aksi takdirde teklifi oluşturan kullanıcı eski bir sürüm kullanmıştır.\n\nKullanıcılar, uyumsuz bir ticaret protokol sürümü ile ticaret yapamaz. offerbook.warning.userIgnored=Bu kullanıcının onion adresini engelleme listenize eklediniz. offerbook.warning.offerBlocked=Bu teklif Haveno geliştiricileri tarafından engellendi.\nMuhtemelen, bu teklifi alırken sorunlara neden olan bir hata var. offerbook.warning.currencyBanned=Bu teklifte kullanılan para birimi Haveno geliştiricileri tarafından engellendi.\nDaha fazla bilgi için Haveno Forum'u ziyaret edin. offerbook.warning.paymentMethodBanned=Bu teklifte kullanılan ödeme yöntemi Haveno geliştiricileri tarafından engellendi.\nDaha fazla bilgi için Haveno Forum'u ziyaret edin. offerbook.warning.nodeBlocked=Bu yatırımcının onion adresi Haveno geliştiricileri tarafından engellendi.\nMuhtemelen, bu yatırımcıdan teklifler alırken sorunlara neden olan bir hata var. offerbook.warning.requireUpdateToNewVersion=Sizin Haveno sürümünüz artık ticaret için uyumlu değil.\n\ Lütfen en son Haveno sürümüne güncelleyin. offerbook.warning.offerWasAlreadyUsedInTrade=Bu teklifi alamazsınız çünkü daha önce aldınız. \ Önceki teklif alma girişiminiz başarısız bir ticaretle sonuçlanmış olabilir. offerbook.warning.arbitratorNotValidated=Bu teklif kabul edilemez çünkü hakem kayıtlı değil. offerbook.warning.signatureNotValidated=Bu teklif, hakemin imzası geçersiz olduğu için alınamaz offerbook.info.sellAtMarketPrice=Piyasa fiyatından satış yapacaksınız (her dakika güncellenir). offerbook.info.buyAtMarketPrice=Piyasa fiyatından alım yapacaksınız (her dakika güncellenir). offerbook.info.sellBelowMarketPrice=Geçerli piyasa fiyatından {0} daha az alacaksınız (her dakika güncellenir). offerbook.info.buyAboveMarketPrice=Geçerli piyasa fiyatından {0} daha fazla ödeyeceksiniz (her dakika güncellenir). offerbook.info.sellAboveMarketPrice=Geçerli piyasa fiyatından {0} daha fazla alacaksınız (her dakika güncellenir). offerbook.info.buyBelowMarketPrice=Geçerli piyasa fiyatından {0} daha az ödeyeceksiniz (her dakika güncellenir). offerbook.info.buyAtFixedPrice=Bu sabit fiyattan alım yapacaksınız. offerbook.info.sellAtFixedPrice=Bu sabit fiyattan satış yapacaksınız. offerbook.info.roundedFiatVolume=İşleminizin gizliliğini artırmak için miktar yuvarlandı. #################################################################### # Offerbook / Create offer #################################################################### createOffer.amount.prompt=XMR miktarını girin createOffer.price.prompt=Fiyatı girin createOffer.volume.prompt={0} cinsinden miktar girin createOffer.amountPriceBox.amountDescription={0} XMR miktarı createOffer.amountPriceBox.buy.volumeDescription=Harcanacak {0} miktarı createOffer.amountPriceBox.sell.volumeDescription=Alınacak {0} miktarı createOffer.amountPriceBox.minAmountDescription=Minimum XMR miktarı createOffer.securityDeposit.prompt=Güvenlik teminatı createOffer.fundsBox.title=Teklifinizi finanse edin createOffer.fundsBox.offerFee=İşlem ücreti createOffer.fundsBox.networkFee=Madencilik ücreti createOffer.fundsBox.placeOfferSpinnerInfo=Teklif yayınlanıyor ... createOffer.fundsBox.paymentLabel=Haveno ticareti ID {0} createOffer.fundsBox.fundsStructure=({0} güvenlik teminatı, {1} işlem ücreti) createOffer.success.headline=Teklifiniz oluşturuldu createOffer.success.info=Açık tekliflerinizi \"Portföy/Açık tekliflerim\" bölümünde yönetebilirsiniz. createOffer.info.sellAtMarketPrice=Teklifinizin fiyatı sürekli güncelleneceği için her zaman piyasa fiyatından satış yapacaksınız. createOffer.info.buyAtMarketPrice=Teklifinizin fiyatı sürekli güncelleneceği için her zaman piyasa fiyatından alım yapacaksınız. createOffer.info.sellAboveMarketPrice=Teklifinizin fiyatı sürekli güncelleneceği için her zaman geçerli piyasa fiyatından {0}% daha fazla alacaksınız. createOffer.info.buyBelowMarketPrice=Teklifinizin fiyatı sürekli güncelleneceği için her zaman geçerli piyasa fiyatından {0}% daha az ödeyeceksiniz. createOffer.warning.sellBelowMarketPrice=Teklifinizin fiyatı sürekli güncelleneceği için her zaman geçerli piyasa fiyatından {0}% daha az alacaksınız. createOffer.warning.buyAboveMarketPrice=Teklifinizin fiyatı sürekli güncelleneceği için her zaman geçerli piyasa fiyatından {0}% daha fazla ödeyeceksiniz. createOffer.tradeFee.descriptionXMROnly=İşlem ücreti createOffer.tradeFee.description=İşlem ücreti createOffer.triggerPrice.prompt=tetikleyici fiyat ayarı createOffer.triggerPrice.label=Teklif, piyasa fiyatı {0} olduğunda devre dışı bırakılacak createOffer.triggerPrice.tooltip=Drastik fiyat hareketlerine karşı koruma olarak, piyasa fiyatı bu değere ulaştığında \ teklifi devre dışı bırakacak bir tetikleyici fiyat ayarlayabilirsiniz. createOffer.triggerPrice.invalid.tooLow=Değer {0}'den yüksek olmalıdır createOffer.triggerPrice.invalid.tooHigh=Değer {0}'den düşük olmalıdır # new entries createOffer.placeOfferButton.buy=Gözden Geçir: {0} ile XMR satın almak için teklif oluştur createOffer.placeOfferButton.sell=Gözden Geçir: {0} karşılığında XMR satmak için teklif oluştur createOffer.createOfferFundWalletInfo.headline=Teklifinizi finanse edin # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Ticaret miktarı: {0} \n createOffer.createOfferFundWalletInfo.msg=Bu teklife {0} yatırmanız gerekiyor.\n\n\ Bu fonlar yerel cüzdanınızda rezerve edilir ve birisi teklifinizi kabul ettiğinde bir multisig cüzdanda kilitlenir.\n\n\ Tutarın toplamı şudur:\n\ {1}\ - Güvenlik depozitonuz: {2}\n\ - İşlem ücreti: {3} # only first part "Bir teklif verirken bir hata oluştu:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=Bir teklif verirken bir hata oluştu:\n\n{0}\n\n\ Henüz cüzdanınızdan hiçbir fon çıkmadı.\n\ Lütfen uygulamanızı yeniden başlatın ve ağ bağlantınızı kontrol edin. createOffer.setAmountPrice=Miktar ve fiyat belirleyin createOffer.warnCancelOffer=Bu teklifi zaten finanse ettiniz.\nŞimdi iptal ederseniz, fonlarınız yerel Haveno cüzdanınızda kalacak ve \"Fonlar/Fon gönder\" ekranında çekilebilir durumda olacaktır.\nİptal etmek istediğinizden emin misiniz? createOffer.timeoutAtPublishing=Teklif yayınlanırken zaman aşımı oluştu. createOffer.errorInfo=\n\nYapıcı ücreti zaten ödendi. En kötü durumda, o ücreti kaybetmiş olabilirsiniz.\nLütfen uygulamanızı yeniden başlatın ve ağ bağlantınızı kontrol edin. createOffer.tooLowSecDeposit.warning=Güvenlik teminatını önerilen varsayılan değerden {0} daha düşük bir değere ayarladınız.\n\ Daha düşük bir güvenlik teminatı kullanmak istediğinizden emin misiniz? createOffer.tooLowSecDeposit.makerIsSeller=Ticaret ortağının ticaret protokolüne uymaması durumunda size daha az koruma sağlar. createOffer.tooLowSecDeposit.makerIsBuyer=Ticaret ortağına, ticaret protokolüne uymanız için daha az güvence sağlar çünkü daha az depozito riski taşır. \ Diğer kullanıcılar sizin teklifiniz yerine başka teklifleri tercih edebilir. createOffer.resetToDefault=Hayır, varsayılan değere sıfırla createOffer.useLowerValue=Evet, daha düşük değerimi kullan createOffer.priceOutSideOfDeviation=Girdiğiniz fiyat, piyasa fiyatından izin verilen maksimum sapmanın dışındadır.\nİzin verilen maksimum sapma {0} ve tercihlerde ayarlanabilir. createOffer.changePrice=Fiyat değiştir createOffer.tac=Bu teklifi yayınlayarak, bu ekranda tanımlanan koşulları yerine getiren herhangi bir tüccarla ticaret yapmayı kabul ediyorum. createOffer.setDeposit=Alıcının güvenlik teminatını ayarla (%) createOffer.setDepositAsBuyer=Alıcı olarak benim güvenlik teminatımı ayarla (%) createOffer.setDepositForBothTraders=Tüccarların güvenlik teminatı (%) createOffer.securityDepositInfo=Alıcının güvenlik teminatı {0} olacak createOffer.securityDepositInfoAsBuyer=Alıcı olarak güvenlik teminatınız {0} olacak createOffer.minSecurityDepositUsed=Minimum güvenlik depozitosu kullanılır createOffer.buyerAsTakerWithoutDeposit=Alıcıdan depozito gerekmez (şifre korumalı) createOffer.myDeposit=Güvenlik depozitam (%) createOffer.myDepositInfo=Güvenlik depozitonuz {0} olacaktır. #################################################################### # Offerbook / Take offer #################################################################### takeOffer.amount.prompt=Miktarı girin XMR olarak takeOffer.amountPriceBox.buy.amountDescription=Satılacak XMR miktarı takeOffer.amountPriceBox.sell.amountDescription=Alınacak XMR miktarı takeOffer.amountPriceBox.buy.amountDescriptionCrypto=Satılacak XMR miktarı takeOffer.amountPriceBox.sell.amountDescriptionCrypto=Alınacak XMR miktarı takeOffer.amountPriceBox.priceDescription={0} başına monero fiyatı takeOffer.amountPriceBox.amountRangeDescription=Olası miktar aralığı takeOffer.amountPriceBox.warning.invalidXmrDecimalPlaces=Girdiğiniz miktar izin verilen ondalık basamak sayısını aşıyor.\nMiktar 4 ondalık basamağa ayarlandı. takeOffer.validation.amountSmallerThanMinAmount=Miktar, teklifte belirtilen minimum miktardan küçük olamaz. takeOffer.validation.amountLargerThanOfferAmount=Girdi miktarı, teklifte belirtilen miktardan yüksek olamaz. takeOffer.validation.amountLargerThanOfferAmountMinusFee=Bu girdi miktarı, XMR satıcısı için toz değişimi oluşturur. takeOffer.fundsBox.title=İşleminizi finanse edin takeOffer.fundsBox.isOfferAvailable=Teklifin hala mevcut olup olmadığını kontrol ediyor ... takeOffer.fundsBox.tradeAmount=Satılacak miktar takeOffer.fundsBox.offerFee=İşlem ücreti takeOffer.fundsBox.networkFee=Toplam madencilik ücretleri takeOffer.fundsBox.takeOfferSpinnerInfo=Teklif alınıyor: {0} takeOffer.fundsBox.paymentLabel=ID {0} ile Haveno işlemi takeOffer.fundsBox.fundsStructure=({0} güvenlik teminatı, {1} işlem ücreti) takeOffer.fundsBox.noFundingRequiredTitle=Fonlama gerekmez takeOffer.fundsBox.noFundingRequiredDescription=Bu teklifi almak için satıcıdan passphrase'i Haveno dışında alınız. takeOffer.success.headline=Teklifi başarıyla aldınız. takeOffer.success.info=İşleminizin durumunu \"Portföy/Açık işlemler\" kısmında görebilirsiniz. takeOffer.error.message=Teklif alımı sırasında bir hata oluştu.\n\n{0} # new entries takeOffer.takeOfferButton.buy=Gözden Geçir: {0} ile XMR satın alma teklifini kabul et takeOffer.takeOfferButton.sell=Gözden Geçir: {0} karşılığında XMR satma teklifini kabul et takeOffer.noPriceFeedAvailable=Bu teklifi alamazsınız çünkü piyasa fiyatına dayalı yüzdelik bir fiyat kullanıyor ancak fiyat beslemesi mevcut değil. takeOffer.takeOfferFundWalletInfo.headline=İşleminizi finanse edin # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- İşlem miktarı: {0} \n takeOffer.takeOfferFundWalletInfo.msg=Bu teklifi kabul etmek için {0} yatırmanız gerekiyor.\n\nMiktar, şu kalemlerin toplamıdır:\n{1}- Güvenlik depozitonuz: {2}\n- İşlem ücreti: {3} takeOffer.alreadyPaidInFunds=Eğer zaten fon yatırdıysanız, \"Fonlar/Fon gönder\" ekranında çekebilirsiniz. takeOffer.setAmountPrice=Miktar ayarla takeOffer.alreadyFunded.askCancel=Bu teklifi zaten finanse ettiniz.\nŞimdi iptal ederseniz, fonlarınız yerel Haveno cüzdanınızda kalacak ve \"Fonlar/Fon gönder\" ekranında çekilebilir olacaktır.\nİptal etmek istediğinizden emin misiniz? takeOffer.failed.offerNotAvailable=Teklif artık mevcut olmadığı için teklif alma isteği başarısız oldu. Belki başka bir tüccar bu teklifi almıştır. takeOffer.failed.offerTaken=Bu teklifi alamazsınız çünkü teklif başka bir tüccar tarafından zaten alınmış. takeOffer.failed.offerInvalid=Bu teklifi alamazsınız çünkü yapıcı imzası geçersiz. takeOffer.failed.offerRemoved=Bu teklifi alamazsınız çünkü teklif bu arada kaldırılmış. takeOffer.failed.offererNotOnline=Teklif alımı isteği başarısız oldu çünkü yapıcı artık çevrimdışı. takeOffer.failed.offererOffline=Bu teklifi alamazsınız çünkü yapıcı çevrimdışı. takeOffer.warning.connectionToPeerLost=Yapıcıya olan bağlantıyı kaybettiniz.\nÇevrimdışına gitmiş olabilir veya çok fazla açık bağlantı nedeniyle bağlantıyı kapatmış olabilir.\n\nTeklifi teklif kitabında hala görebiliyorsanız, teklifi tekrar almaya çalışabilirsiniz. takeOffer.error.noFundsLost=\n\nHenüz cüzdanınızdan fon çıkmadı.\nUygulamanızı yeniden başlatmayı deneyin ve sorunu çözmek için ağ bağlantınızı kontrol edin. # suppress inspection "TrailingSpacesInProperty" takeOffer.error.feePaid=\n\n takeOffer.error.depositPublished=\n\nGüvence işlemi zaten yayınlandı.\nUygulamanızı yeniden başlatmayı deneyin ve sorunu çözmek için ağ bağlantınızı kontrol edin.\nSorun devam ederse lütfen geliştiricilerle iletişime geçin. takeOffer.error.payoutPublished=\n\nÖdeme işlemi zaten yayınlandı.\nUygulamanızı yeniden başlatmayı deneyin ve sorunu çözmek için ağ bağlantınızı kontrol edin.\nSorun devam ederse lütfen geliştiricilerle iletişime geçin. takeOffer.tac=Bu teklifi alarak bu ekranda tanımlanan işlem koşullarını kabul ediyorum. #################################################################### # Offerbook / Edit offer #################################################################### openOffer.header.triggerPrice=Tetikleme fiyatı openOffer.triggerPrice=Tetikleme fiyatı {0} openOffer.triggered=Piyasa fiyatı tetikleme fiyatınıza ulaştığı için teklif devre dışı bırakıldı.\n\ Yeni bir tetikleme fiyatı tanımlamak için teklifi düzenleyin editOffer.setPrice=Fiyat ayarla editOffer.confirmEdit=Onayla: Teklifi düzenle editOffer.publishOffer=Teklifinizi yayınlıyor. editOffer.failed=Teklif düzenleme başarısız:\n{0} editOffer.success=Teklifiniz başarıyla düzenlendi. editOffer.invalidDeposit=Alıcının güvenlik teminatı Haveno tarafından tanımlanan sınırlamalar içinde değil ve artık düzenlenemez. #################################################################### # Portfolio #################################################################### portfolio.tab.openOffers=Açık tekliflerim portfolio.tab.pendingTrades=Açık işlemler portfolio.tab.history=Tarihçe portfolio.tab.failed=Başarısız portfolio.tab.editOpenOffer=Teklifi düzenle portfolio.tab.duplicateOffer=Teklifi kopyala portfolio.context.offerLikeThis=Bunun gibi yeni bir teklif oluştur... portfolio.context.notYourOffer=Yapıcı olduğunuz teklifleri yalnızca kopyalayabilirsiniz. portfolio.closedTrades.deviation.help=Piyasa fiyatından yüzdelik fiyat sapması portfolio.pending.invalidTx=Eksik veya geçersiz işlem ile ilgili bir sorun var.\n\n\ Lütfen geleneksel veya kripto para birimi ödemesini GÖNDERMEYİN.\n\n\ Yardım almak için bir Destek bileti açın.\n\n\ Hata mesajı: {0} portfolio.pending.unconfirmedTooLong=İşlem {0} üzerindeki güvence işlemleri {1} saat sonra hala onaylanmamış durumda. \ Güvence işlemlerini bir blok zinciri gezgini kullanarak kontrol edin; eğer onaylanmışlarsa ancak Haveno'da \ onaylanmış olarak gösterilmiyorlarsa, Haveno'yu yeniden başlatmayı deneyin.\n\n\ Sorun devam ederse, Haveno desteğiyle iletişime geçin [HYPERLINK:https://matrix.to/#/#haveno:monero.social]. portfolio.pending.syncing=Ticaret cüzdanı senkronize ediliyor portfolio.pending.syncing.blockRemaining=Ticaret cüzdanı senkronize ediliyor — 1 blok kaldı portfolio.pending.syncing.blocksRemaining=Ticaret cüzdanı senkronize ediliyor — {0} blok kaldı portfolio.pending.step1.waitForConf=Blok zinciri onaylarını bekleyin portfolio.pending.step2_buyer.additionalConf=Mevduatlar 10 onayı ulaştı.\nEkstra güvenlik için, ödeme göndermeden önce {0} onayı beklemenizi öneririz.\nErken ilerlemek kendi riskinizdedir. portfolio.pending.step2_buyer.startPayment=Ödemeyi başlat portfolio.pending.step2_seller.waitPaymentSent=Ödeme gönderilene kadar bekle portfolio.pending.step3_buyer.waitPaymentArrived=Ödeme gelene kadar bekle portfolio.pending.step3_seller.confirmPaymentReceived=Ödemenin alındığını onayla portfolio.pending.step5.completed=Tamamlandı portfolio.pending.step3_seller.autoConf.status.label=Otomatik onay durumu portfolio.pending.autoConf=Otomatik onaylandı portfolio.pending.autoConf.blocks=XMR onayları: {0} / Gerekli: {1} portfolio.pending.autoConf.state.xmr.txKeyReused=İşlem anahtarı tekrar kullanıldı. Lütfen bir ihtilaf açın. portfolio.pending.autoConf.state.confirmations=XMR onayları: {0}/{1} portfolio.pending.autoConf.state.txNotFound=İşlem henüz bellek havuzunda görülmedi portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=Geçerli işlem kimliği / işlem anahtarı yok portfolio.pending.autoConf.state.filterDisabledFeature=Geliştiriciler tarafından devre dışı bırakıldı. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FEATURE_DISABLED=Otomatik onay özelliği devre dışı bırakıldı. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=İşlem miktarı otomatik onay limitini aşıyor # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.INVALID_DATA=Karşı taraf geçersiz veri sağladı. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=Ödeme işlemi zaten yayımlandı. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.DISPUTE_OPENED=Uyuşmazlık açıldı. Otomatik onay bu işlem için devre dışı bırakıldı. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.REQUESTS_STARTED=İşlem kanıtı talepleri başlatıldı # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PENDING=Başarılı sonuçlar: {0}/{1}; {2} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.COMPLETED=Tüm hizmetlerde kanıt başarıyla tamamlandı # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.ERROR=Bir hizmet talebinde hata oluştu. Otomatik onay mümkün değil. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=Bir hizmet başarısızlıkla sonuçlandı. Otomatik onay mümkün değil. portfolio.pending.step1.info.you=Depozito işlemi yayımlandı.\n10 onaydan sonra ödemeye başlayabilirsiniz (~{0} dakika kaldı). portfolio.pending.step1.info.buyer=Depozito işlemi yayınlandı.\nXMR alıcısı 10 onaydan sonra ödemeye başlayabilir (~{0} dakika kaldı). portfolio.pending.step1.warn=Yatırım işlemi henüz onaylanmadı. Bu genellikle yaklaşık 20 dakika sürer, ancak ağ yoğunsa daha uzun sürebilir. portfolio.pending.step1.openForDispute=Yatırım işlemi hala onaylanmadı. \ 20 dakikadan çok daha uzun süre beklediyseniz, Haveno desteği ile iletişime geçin. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2.confReached=İşleminiz 10 onaya ulaştı.\n\n portfolio.pending.step2_buyer.refTextWarn=Önemli: ödeme yaparken, \"ödeme nedeni\" alanını boş bırakın. \ İşlem ID'si veya 'monero', 'XMR' veya 'Haveno' gibi başka bir metin koymayın. \ Eğer her iki taraf için de uygun olacak alternatif bir \"ödeme nedeni\" tartışmak isterseniz, trader sohbetini kullanabilirsiniz. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees=Bankanız transfer yapmak için sizden herhangi bir ücret alıyorsa, bu ücretleri ödemekten siz sorumlusunuz. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees.swift=SWIFT ödemesini göndermek için SHA (paylaşılan ücret modeli) kullanmanız gerekmektedir. \ Daha fazla ayrıntı için [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/SWIFT#Use_the_correct_fee_option] adresine bakınız. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.crypto=Lütfen dış {0} cüzdanınızdan\n{1} XMR satıcısına transfer yapın.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.cash=Lütfen bir bankaya gidin ve {0} tutarını XMR satıcısına ödeyin.\n\n portfolio.pending.step2_buyer.cash.extra=ÖNEMLİ GEREKLİLİK:\nÖdeme yaptıktan sonra kağıt makbuzun üzerine: İADE YOK yazın. Ardından iki parçaya ayırın, bir fotoğraf çekin ve XMR satıcısının e-posta adresine gönderin. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.moneyGram=Lütfen MoneyGram kullanarak {0} tutarını XMR satıcısına ödeyin.\n\n portfolio.pending.step2_buyer.moneyGram.extra=ÖNEMLİ GEREKLİLİK:\nÖdeme yaptıktan sonra yetkilendirme numarasını ve makbuzun bir fotoğrafını e-posta ile XMR satıcısına gönderin.\n\ Makbuz, satıcının tam adını, ülkesini, eyaletini ve miktarı açıkça göstermelidir. Satıcının e-postası: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.westernUnion=Lütfen Western Union kullanarak {0} tutarını XMR satıcısına ödeyin.\n\n portfolio.pending.step2_buyer.westernUnion.extra=ÖNEMLİ GEREKLİLİK:\nÖdeme yaptıktan sonra MTCN (izleme numarası) ve makbuzun bir fotoğrafını e-posta ile XMR satıcısına gönderin.\n\ Makbuz, satıcının tam adını, şehrini, ülkesini ve miktarı açıkça göstermelidir. Satıcının e-postası: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Lütfen "US Postal Money Order" kullanarak {0} tutarını XMR satıcısına gönderin.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Lütfen "Pay by Mail" kullanarak {0} tutarını XMR satıcısına gönderin. \ Belirli talimatlar işlem sözleşmesinde bulunmaktadır, veya belirsizse trader sohbeti aracılığıyla sorular sorabilirsiniz. \ Pay by Mail hakkında daha fazla ayrıntı için Haveno wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Pay_By_Mail] adresine bakın.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Lütfen belirtilen ödeme yöntemini kullanarak {0} tutarını XMR satıcısına ödeyin. Satıcının hesap bilgilerini bir sonraki ekranda bulacaksınız.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Lütfen sağlanan iletişim bilgileriyle XMR satıcısıyla iletişime geçin ve {0} ödemesi için bir buluşma düzenleyin.\n\n portfolio.pending.step2_buyer.startPaymentUsing={0} kullanarak ödemeye başlayın portfolio.pending.step2_buyer.recipientsAccountData=Alıcının {0} portfolio.pending.step2_buyer.amountToTransfer=Transfer edilecek miktar portfolio.pending.step2_buyer.sellersAddress=Satıcının {0} adresi portfolio.pending.step2_buyer.buyerAccount=Kullanılacak ödeme hesabınız portfolio.pending.step2_buyer.paymentSent=Ödeme gönderildi portfolio.pending.step2_buyer.showEarly=Ödeme bilgilerini erken göster portfolio.pending.step2_buyer.warn=Hala {0} ödemenizi yapmadınız!\nLütfen işlemin {1} tarihine kadar tamamlanması gerektiğini unutmayın. portfolio.pending.step2_buyer.openForDispute=Ödemenizi henüz tamamlamadınız\nİşlemin maksimum süresi doldu, ancak yine de ödemeyi tamamlayabilirsiniz.\n\ Yardım gerekiyorsa bir arabulucu ile iletişime geçin. portfolio.pending.step2_buyer.paperReceipt.headline=Kağıt makbuzu XMR satıcısına gönderdiniz mi? portfolio.pending.step2_buyer.paperReceipt.msg=Unutmayın:\n\ Kağıt makbuzun üzerine: İADE YOK yazmanız gerekiyor.\n\ Ardından iki parçaya ayırın, bir fotoğraf çekin ve XMR satıcısının e-posta adresine gönderin. portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Yetkilendirme numarası ve makbuz gönder portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=Yetkilendirme numarasını ve makbuzun bir fotoğrafını e-posta ile XMR satıcısına göndermeniz gerekiyor.\n\ Makbuz, satıcının tam adını, ülkesini, eyaletini ve miktarı açıkça göstermelidir. Satıcının e-postası: {0}.\n\n\ Yetkilendirme numarasını ve sözleşmeyi satıcıya gönderdiniz mi? portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=MTCN ve makbuz gönder portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=MTCN (izleme numarası) ve makbuzun bir fotoğrafını e-posta ile XMR satıcısına göndermeniz gerekiyor.\n\ Makbuz, satıcının tam adını, şehrini, ülkesini ve miktarı açıkça göstermelidir. Satıcının e-postası: {0}.\n\n\ MTCN ve sözleşmeyi satıcıya gönderdiniz mi? portfolio.pending.step2_buyer.halCashInfo.headline=HalCash kodunu gönder portfolio.pending.step2_buyer.halCashInfo.msg=HalCash kodunu ve işlem kimliğini ({0}) XMR satıcısına \ kısa mesaj olarak göndermeniz gerekiyor. Satıcının cep telefonu numarası: {1}.\n\n\ Kodu satıcıya gönderdiniz mi? portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Bazı bankalar alıcının adını doğrulayabilir. \ Eski Haveno istemcilerinde oluşturulan Faster Payments hesapları alıcının adını sağlamaz, \ bu nedenle gerektiğinde işlem sohbeti aracılığıyla bu bilgiyi edinmek için kullanın. portfolio.pending.step2_buyer.confirmStart.headline=Ödemeye başladığınızı onaylayın portfolio.pending.step2_buyer.confirmStart.msg=İşlem ortağınıza {0} ödemesini başlattınız mı? portfolio.pending.step2_buyer.confirmStart.yes=Evet, ödemeye başladım portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=Ödeme kanıtı sağlamadınız portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=İşlem kimliğini ve işlem anahtarını girmediniz.\n\n\ Bu verileri sağlamadan, karşı taraf XMR'yi aldığında otomatik onay özelliğini kullanamaz.\n\ Ayrıca, Haveno, bir uyuşmazlık durumunda XMR işleminin göndericisinin bu bilgileri sağlayabilmesini gerektirir.\n\ Daha fazla bilgi için Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades] adresine bakın. portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=Girdi 32 baytlık bir onaltılık değer değil portfolio.pending.step2_buyer.confirmStart.warningButton=Yoksay ve yine de devam et portfolio.pending.step2_seller.waitPayment.headline=Ödeme bekleniyor portfolio.pending.step2_seller.f2fInfo.headline=Alıcının iletişim bilgileri portfolio.pending.step2_seller.waitPayment.msg=Yatırım işlemi kilidi açıldı.\nXMR alıcısının {0} ödemesini başlatmasını beklemeniz gerekiyor. portfolio.pending.step2_seller.warn=XMR alıcısı hala {0} ödemesini yapmadı.\nÖdeme başlatılana kadar beklemeniz gerekiyor.\nİşlem {1} tarihinde tamamlanmadıysa, arabulucu durumu inceleyecektir. portfolio.pending.step2_seller.openForDispute=XMR alıcısı ödemesine başlamadı!\nİşlem için izin verilen maksimum süre doldu.\nKarşı tarafa daha fazla zaman tanıyabilir veya arabulucu ile iletişime geçebilirsiniz. disputeChat.chatWindowTitle=İşlem ID'si ile ilgili uyuşmazlık sohbet penceresi '{0}' tradeChat.chatWindowTitle=İşlem ID'si ile ilgili trader sohbet penceresi '{0}' tradeChat.openChat=Sohbet penceresini aç tradeChat.rules=Bu işlemle ilgili olası sorunları çözmek için işlem ortağınızla iletişim kurabilirsiniz.\n\ Sohbette yanıt vermek zorunlu değildir.\n\ Bir trader aşağıdaki kurallardan herhangi birini ihlal ederse, uyuşmazlık açın ve durumu arabulucuya bildirin.\n\n\ Sohbet kuralları:\n\ \t● Bağlantı göndermeyin (zararlı yazılım riski). İşlem kimliğini ve bir blok gezgininin adını gönderebilirsiniz.\n\ \t● Seed kelimelerinizi, özel anahtarlarınızı, şifrelerinizi veya diğer hassas bilgilerinizi göndermeyin!\n\ \t● Haveno dışında işlem yapmaya teşvik etmeyin (güvenlik yok).\n\ \t● Sosyal mühendislik dolandırıcılık girişimlerinde bulunmayın.\n\ \t● Bir eş yanıt vermiyorsa ve sohbet yoluyla iletişim kurmak istemiyorsa, kararına saygı gösterin.\n\ \t● Sohbeti işlemin kapsamı ile sınırlı tutun. Bu sohbet bir mesajlaşma uygulaması veya troll kutusu değildir.\n\ \t● Sohbeti dostça ve saygılı tutun. # suppress inspection "UnusedProperty" message.state.UNDEFINED=Tanımsız # suppress inspection "UnusedProperty" message.state.SENT=Mesaj gönderildi # suppress inspection "UnusedProperty" message.state.ARRIVED=Mesaj alıcıya ulaştı # suppress inspection "UnusedProperty" message.state.STORED_IN_MAILBOX=Ödeme mesajı gönderildi ancak alıcı tarafından henüz alınmadı # suppress inspection "UnusedProperty" message.state.ACKNOWLEDGED=Alıcı mesajı aldığını doğruladı # suppress inspection "UnusedProperty" message.state.FAILED=Mesaj gönderimi başarısız oldu portfolio.pending.step3_buyer.wait.headline=XMR satıcısının ödeme onayını bekleyin portfolio.pending.step3_buyer.wait.info=XMR satıcısının {0} ödemesini aldığına dair onayı bekleniyor. portfolio.pending.step3_buyer.wait.msgStateInfo.label=Ödeme başlatıldı mesaj durumu portfolio.pending.step3_buyer.warn.part1a={0} blok zincirinde portfolio.pending.step3_buyer.warn.part1b=ödeme sağlayıcınızda (örneğin, banka) portfolio.pending.step3_buyer.warn.part2=XMR satıcısı hala ödemenizi onaylamadı. Lütfen {0} kontrol edin ve \ ödemenin başarılı olup olmadığını doğrulayın. portfolio.pending.step3_buyer.openForDispute=XMR satıcısı ödemenizi onaylamadı! Maksimum ticaret süresi doldu. \ Daha uzun süre bekleyebilir ve ticaret partnerinize daha fazla zaman tanıyabilir veya hakemden yardım isteyebilirsiniz. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.part=Ticaret partneriniz {0} ödemesini başlattığını doğruladı.\n\n portfolio.pending.step3_seller.crypto.explorer=favori {0} blok zinciri gezgininizde portfolio.pending.step3_seller.crypto.wallet={0} cüzdanınızda portfolio.pending.step3_seller.crypto={0}Lütfen alıcı adresinize yapılan işlemin\n\ {2}\n\ yeterli blok zinciri onayına sahip olup olmadığını kontrol edin.\nÖdeme tutarı {3} olmalıdır\n\n\ Bu açılır pencereyi kapattıktan sonra ana ekrandan {4} adresinizi kopyalayıp yapıştırabilirsiniz. portfolio.pending.step3_seller.postal={0}XMR alıcısından \"ABD Posta Para Havalesi\" ile {1} alıp almadığınızı kontrol edin. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.payByMail={0}XMR alıcısından \"Posta ile Öde\" ile {1} alıp almadığınızı kontrol edin. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.bank=Ticaret partneriniz {0} ödemesini başlattığını doğruladı.\n\n\ Lütfen çevrimiçi bankacılık web sayfanıza gidin ve XMR alıcısından {1} alıp almadığınızı kontrol edin. portfolio.pending.step3_seller.cash=Ödeme Nakit Depozito yoluyla yapıldığından, XMR alıcısı makbuzun üzerine \"İADE YOK\" yazmalı, iki parçaya ayırmalı ve size e-posta ile bir fotoğraf göndermelidir.\n\n\ Geri ödeme riskini önlemek için, yalnızca e-postayı aldıysanız ve makbuzun geçerli olduğundan eminseniz onaylayın.\n\ Emin değilseniz, {0} portfolio.pending.step3_seller.moneyGram=Alıcı, yetkilendirme numarasını ve makbuzun fotoğrafını size e-posta ile göndermelidir.\n\ Makbuz, tam adınızı, ülkenizi, eyaletinizi ve tutarı açıkça göstermelidir. Lütfen e-postanızı kontrol edin ve yetkilendirme numarasını aldığınızdan emin olun.\n\n\ Bu açılır pencereyi kapattıktan sonra, XMR alıcısının adını ve adresini MoneyGram'dan parayı almak için göreceksiniz.\n\n\ Parayı başarıyla aldıktan sonra alımı onaylayın! portfolio.pending.step3_seller.westernUnion=Alıcı, MTCN (takip numarası) ve makbuzun fotoğrafını size e-posta ile göndermelidir.\n\ Makbuz, tam adınızı, şehrinizi, ülkenizi ve tutarı açıkça göstermelidir. Lütfen e-postanızı kontrol edin ve MTCN'yi aldığınızdan emin olun.\n\n\ Bu açılır pencereyi kapattıktan sonra, XMR alıcısının adını ve adresini Western Union'dan parayı almak için göreceksiniz.\n\n\ Parayı başarıyla aldıktan sonra alımı onaylayın! portfolio.pending.step3_seller.halCash=Alıcı, size HalCash kodunu kısa mesaj olarak göndermelidir. Bunun yanı sıra, HalCash destekleyen bir ATM'den EUR çekmek için gerekli bilgileri içeren bir mesaj alacaksınız.\n\n\ ATM'den parayı aldıktan sonra burada ödemenin alındığını onaylayın! portfolio.pending.step3_seller.amazonGiftCard=Alıcı, size Amazon eGift Kartı'nı e-posta veya cep telefonunuza \ kısa mesaj olarak gönderdi. Lütfen şimdi Amazon hesabınızda Amazon eGift Kartı'nı kullanın ve kabul edildikten \ sonra ödeme alımını onaylayın. portfolio.pending.step3_seller.bankCheck=\n\nLütfen ayrıca ticaret sözleşmesinde belirtilen gönderici adının banka ekstrenizde görünen adla eşleştiğini doğrulayın:\nTicaret sözleşmesine göre gönderici adı: {0}\n\n\ İsimler tam olarak aynı değilse, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=ödeme alımını onaylamayın. Bunun yerine, "alt + o" veya "option + o" tuşlarına basarak bir anlaşmazlık açın.\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=Ödeme alımını onayla portfolio.pending.step3_seller.amountToReceive=Alınacak tutar portfolio.pending.step3_seller.yourAddress=Sizin {0} adresiniz portfolio.pending.step3_seller.buyersAddress=Alıcının {0} adresi portfolio.pending.step3_seller.yourAccount=Ticaret hesabınız portfolio.pending.step3_seller.xmrTxHash=İşlem Kimliği portfolio.pending.step3_seller.xmrTxKey=İşlem anahtarı portfolio.pending.step3_seller.buyersAccount=Alıcının hesap verileri portfolio.pending.step3_seller.confirmReceipt=Ödeme alımını onayla portfolio.pending.step3_seller.buyerStartedPayment=XMR alıcısı {0} ödemesini başlattı.\n{1} portfolio.pending.step3_seller.buyerStartedPayment.crypto=Kripto para cüzdanınızda veya blok gezgininde blok zinciri onaylarını kontrol edin ve yeterli blok zinciri onayına sahip olduğunuzda ödemeyi onaylayın. portfolio.pending.step3_seller.buyerStartedPayment.traditional=Ticaret hesabınızda (örneğin banka hesabı) kontrol edin ve ödemeyi aldığınızda onaylayın. portfolio.pending.step3_seller.warn.part1a={0} blok zincirinde portfolio.pending.step3_seller.warn.part1b=ödeme sağlayıcınızda (örneğin, banka) portfolio.pending.step3_seller.warn.part2=Ödeme alımını hala onaylamadınız. \ Lütfen {0} kontrol edin ve ödemeyi aldığınızı doğrulayın. portfolio.pending.step3_seller.openForDispute=Ödeme alımını henüz onaylamadınız.\n\ Maksimum ticaret süresi doldu.\nLütfen onaylayın veya hakemden yardım isteyin. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=Ticaret partnerinizden {0} ödemesini aldınız mı?\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.name=Lütfen ayrıca ticaret sözleşmesinde belirtilen gönderici adının banka ekstrenizde görünen adla eşleştiğini doğrulayın:\nTicaret sözleşmesine göre gönderici adı: {0}\n\nİsimler tam olarak aynı değilse, ödeme alımını onaylamayın. Bunun yerine, "alt + o" veya "option + o" tuşlarına basarak bir anlaşmazlık açın.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Lütfen unutmayın, ödeme alımını onayladığınız anda, kilitli ticaret tutarı XMR alıcısına serbest bırakılacak ve güvenlik depozitosu iade edilecektir.\n\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Ödemeyi aldığınızı onaylayın portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=Evet, ödemeyi aldım portfolio.pending.step3_seller.onPaymentReceived.signer=ÖNEMLİ: Ödeme alımını onaylayarak, karşı tarafın hesabını da \ doğrulamış ve buna göre imzalamış olursunuz. Karşı tarafın hesabı henüz imzalanmadığından, geri ödeme riskini azaltmak için \ ödeme onayını mümkün olduğunca geciktirmelisiniz. portfolio.pending.step5_buyer.groupTitle=Tamamlanan ticaret özeti portfolio.pending.step5_buyer.groupTitle.mediated=Bu ticaret arabuluculukla çözüldü portfolio.pending.step5_buyer.groupTitle.arbitrated=Bu ticaret hakemlikle çözüldü portfolio.pending.step5_buyer.tradeFee=Ticaret ücreti portfolio.pending.step5_buyer.makersMiningFee=Madencilik ücreti portfolio.pending.step5_buyer.takersMiningFee=Toplam madencilik ücretleri portfolio.pending.step5_buyer.refunded=İade edilen güvenlik depozitosu portfolio.pending.step5_buyer.amountTooLow=Transfer edilecek tutar, işlem ücretinden ve mümkün olan minimum işlem değerinden (toz) daha düşük. portfolio.pending.step5_buyer.tradeCompleted.headline=Ticaret tamamlandı portfolio.pending.step5_buyer.tradeCompleted.msg=Tamamlanan ticaretleriniz \"Portföy/Tarihçe\" altında saklanır.\nTüm Monero işlemlerinizi \"Fonlar/İşlemler\" altında inceleyebilirsiniz. portfolio.pending.step5_buyer.bought=Satın aldınız portfolio.pending.step5_buyer.paid=Ödediniz portfolio.pending.step5_seller.sold=Satış yaptınız portfolio.pending.step5_seller.received=Aldınız tradeFeedbackWindow.title=Ticaretiniz başarıyla tamamlandı! tradeFeedbackWindow.msg.part1=Deneyiminiz hakkında geri bildirim almak isteriz. Yazılımı geliştirmemize ve pürüzleri gidermemize yardımcı olacaktır. tradeFeedbackWindow.msg.part2=Herhangi bir sorunuz varsa veya herhangi bir sorun yaşadıysanız, diğer kullanıcılar ve katkıda bulunanlarla Matrix sohbet odamızda iletişime geçin: tradeFeedbackWindow.msg.part3=Haveno'yu kullandığınız için teşekkürler! portfolio.pending.role=Rolüm portfolio.pending.tradeInformation=Ticaret bilgileri portfolio.pending.remainingTime=Kalan süre portfolio.pending.remainingTimeDetail={0} (kadar {1}) portfolio.pending.remainingTimeDetail.startsAfter={0} onaydan sonra başlar portfolio.pending.tradePeriodInfo={0} onaydan sonra işlem süresi başlar. Kullanılan ödeme yöntemine bağlı olarak farklı bir azami işlem süresi uygulanır. portfolio.pending.tradePeriodWarning=Süre aşılırsa her iki tüccar da bir anlaşmazlık açabilir. portfolio.pending.tradeNotCompleted=Ticaret zamanında tamamlanmadı (kadar {0}) portfolio.pending.tradeProcess=Ticaret süreci portfolio.pending.stillNotResolved=Sorununuz çözülmezse, [Matrix sohbet odamızda](https://matrix.to/#/#haveno:monero.social) destek talep edebilirsiniz. portfolio.pending.openAgainDispute.msg=Hakeme gönderilen mesajın ulaştığından emin değilseniz (örneğin, 1 gün içinde yanıt almadıysanız), ekibe ulaşmaktan çekinmeyin. portfolio.pending.openAgainDispute.button=Anlaşmazlığı tekrar aç portfolio.pending.openSupportTicket.headline=Destek bileti aç portfolio.pending.openSupportTicket.msg=Lütfen bu işlevi yalnızca acil durumlarda kullanın, eğer \ \"Destek aç\" veya \"Anlaşmazlık aç\" butonlarını görmüyorsanız.\n\nDestek bileti açtığınızda ticaret kesintiye uğrar ve \ bir hakem tarafından ele alınır. portfolio.pending.timeLockNotOver=Arabuluculuk anlaşmazlığı açmadan önce ≈{0} ({1} daha fazla blok) beklemeniz gerekiyor. portfolio.pending.error.depositTxNull=Bir depozito işlemi null. Geçersiz bir depozito işlemiyle \ anlaşmazlık açamazsınız.\n\n\ Daha fazla yardım için, lütfen Matrix sohbet odamızda Haveno desteği ile iletişime geçin. portfolio.pending.mediationResult.error.depositTxNull=Depozito işlemi null. Ticareti başarısız \ işlemler olarak taşıyabilirsiniz. portfolio.pending.mediationResult.error.delayedPayoutTxNull=Gecikmiş ödeme işlemi null. Ticareti başarısız \ işlemler olarak taşıyabilirsiniz. portfolio.pending.error.depositTxNotConfirmed=Depozito işlemleri onaylanmadı ve kullanılabilir değil. Bekleyen bir depozito işlemiyle \ anlaşmazlık açamazsınız. Lütfen her iki depozito işlemi de onaylanana ve kullanılabilir olana kadar bekleyin.\n\n\ Daha fazla yardım için, lütfen Matrix sohbet odamızda Haveno desteği ile iletişime geçin. portfolio.pending.support.headline.getHelp=Yardıma mı ihtiyacınız var? portfolio.pending.support.button.getHelp=Tüccar Sohbetini Aç portfolio.pending.support.headline.halfPeriodOver=Ödemeyi kontrol edin portfolio.pending.support.headline.periodOver=Ticaret süresi doldu portfolio.pending.support.headline.depositTxMissing=Eksik yatırma işlemi portfolio.pending.support.depositTxMissing=Bu işlem için bir para yatırma işlemi eksik. Yardım almak için bir tahkimciyle iletişime geçmek üzere bir destek talebi açın. portfolio.pending.arbitrationRequested=Arabuluculuk talep edildi portfolio.pending.mediationRequested=Arabuluculuk talep edildi portfolio.pending.refundRequested=İade talep edildi portfolio.pending.openSupport=Destek bileti aç portfolio.pending.supportTicketOpened=Destek bileti açıldı portfolio.pending.communicateWithArbitrator=Lütfen \"Destek\" ekranında hakemle iletişime geçin. portfolio.pending.communicateWithMediator=Lütfen \"Destek\" ekranında arabulucu ile iletişime geçin. portfolio.pending.disputeOpenedByUser=Zaten bir anlaşmazlık açtınız.\n{0} portfolio.pending.disputeOpenedByPeer=Ticaret ortağınız bir anlaşmazlık açtı\n{0} portfolio.pending.noReceiverAddressDefined=Alıcı adresi tanımlanmamış portfolio.pending.mediationResult.headline=Arabuluculuk sonucunda önerilen ödeme portfolio.pending.mediationResult.info.noneAccepted=Arabulucunun ticaret ödemesi için önerisini kabul ederek ticareti tamamlayın. portfolio.pending.mediationResult.info.selfAccepted=Arabulucunun önerisini kabul ettiniz. Karşı tarafın da kabul etmesini bekliyor. portfolio.pending.mediationResult.info.peerAccepted=Ticaret ortağınız arabulucunun önerisini kabul etti. Siz de kabul ediyor musunuz? portfolio.pending.mediationResult.button=Önerilen çözümü görüntüle portfolio.pending.mediationResult.popup.headline=Ticaret Kimliği ile arabuluculuk sonucu: {0} portfolio.pending.mediationResult.popup.headline.peerAccepted=Ticaret {0} için ticaret ortağınız arabulucunun önerisini kabul etti portfolio.pending.mediationResult.popup.info=Arabulucu şu şekilde bir ödeme önermiştir:\n\ Siz alıyorsunuz: {0}\n\ Ticaret ortağınız alıyor: {1}\n\n\ Bu önerilen ödemeyi kabul edebilir veya reddedebilirsiniz.\n\n\ Kabul ederseniz, önerilen ödeme işlemini imzalarsınız. \ Ticaret ortağınız da kabul eder ve imzalarsa, ödeme tamamlanır ve ticaret kapanır.\n\n\ Bir veya ikiniz de öneriyi reddederseniz, {2} tarihine kadar (blok {3}) \ hakemle ikinci anlaşmazlık için beklemek zorundasınız. Hakem, tekrar inceleyip bulgularına göre ödeme yapacaktır.\n\n\ Hakem, çalışmaları için küçük bir ücret (maksimum ücret: tüccarın güvenlik depozitosu) talep edebilir. \ Her iki tüccarın da arabulucunun önerisini kabul etmesi mutlu yoldur - hakem talep etmek, \ arabulucunun adil bir ödeme önerisi yapmadığından emin olunması durumunda istisnai durumlar içindir \ (veya diğer taraf yanıt vermiyorsa).\n\n\ Yeni arabuluculuk modeli hakkında daha fazla ayrıntı: [HYPERLINK:https://haveno.exchange/wiki/Dispute_resolution#Level_3:_Arbitration] portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=Arabulucunun önerdiği ödemeyi kabul ettiniz \ ancak ticaret ortağınızın kabul etmemiş gibi görünüyor.\n\n\ Kilit süresi sona erdiğinde {0} (blok {1}), \ hakemle ikinci tur anlaşmazlık açabilirsiniz. Hakem durumu tekrar inceleyip bulgularına göre ödeme yapacaktır.\n\n\ Arabuluculuk modeli hakkında daha fazla ayrıntıyı şu adreste bulabilirsiniz:\ [HYPERLINK:https://haveno.exchange/wiki/Dispute_resolution#Level_3:_Arbitration] portfolio.pending.mediationResult.popup.openArbitration=Reddet ve hakem talep et portfolio.pending.mediationResult.popup.alreadyAccepted=Zaten kabul ettiniz portfolio.pending.failedTrade.taker.missingTakerFeeTx=Alıcı ücret işlemi eksik.\n\n\ Bu işlem olmadan, ticaret tamamlanamaz. Hiçbir fon kilitlenmedi ve ticaret ücreti ödenmedi. \ Bu ticareti başarısız ticaretler arasına taşıyabilirsiniz. portfolio.pending.failedTrade.maker.missingTakerFeeTx=Karşı tarafın alıcı ücret işlemi eksik.\n\n\ Bu işlem olmadan, ticaret tamamlanamaz. Hiçbir fon kilitlenmedi. \ Teklifiniz diğer tüccarlar için hala mevcut, bu yüzden üretici ücretini kaybetmediniz. \ Bu ticareti başarısız ticaretler arasına taşıyabilirsiniz. portfolio.pending.failedTrade.missingDepositTx=Bir teminat işlemi eksik.\n\nBu işlem, ticareti tamamlamak için gereklidir. Lütfen cüzdanınızın Monero blok zinciri ile tamamen senkronize olduğundan emin olun.\n\nBu ticareti devre dışı bırakmak için "Başarısız İşlemler" bölümüne taşıyabilirsiniz. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Gecikmiş ödeme işlemi eksik, \ ancak fonlar depozito işleminde kilitlendi.\n\n\ Lütfen geleneksel veya kripto para ödemesini XMR satıcısına göndermeyin, çünkü gecikmiş ödeme işlemi olmadan arabuluculuk \ açılamaz. Bunun yerine, Cmd/Ctrl+o ile bir arabuluculuk bileti açın. \ Arabulucu, her iki tarafın da güvenlik mevduatlarının tamamını geri almasını önermelidir \ (satıcı da tam ticaret miktarını geri alır). \ Bu şekilde, güvenlik riski yoktur ve yalnızca ticaret ücretleri kaybedilir. \n\n\ Kaybedilen ticaret ücretleri için burada geri ödeme talebinde bulunabilirsiniz: \ [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Gecikmiş ödeme işlemi eksik \ ancak fonlar depozito işleminde kilitlendi.\n\n\ Eğer alıcı da gecikmiş ödeme işlemini eksikse, onlara ödemeyi göndermemeleri ve \ bir arabuluculuk bileti açmaları talimatı verilecektir. Siz de Cmd/Ctrl+o ile bir arabuluculuk bileti açmalısınız. \n\n\ Eğer alıcı henüz ödeme yapmadıysa, arabulucu her iki tarafın da güvenlik mevduatlarının \ tamamını geri almasını önermelidir (satıcı da tam ticaret miktarını geri alır). \ Aksi takdirde ticaret miktarı alıcıya gitmelidir. \n\n\ Kaybedilen ticaret ücretleri için burada geri ödeme talebinde bulunabilirsiniz: \ [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=Ticaret protokolü yürütülürken bir hata oluştu.\n\n\ Hata: {0}\n\n\ Bu hata kritik olmayabilir ve ticaret normal şekilde tamamlanabilir. Emin değilseniz, \ Haveno arabulucularından tavsiye almak için bir arabuluculuk bileti açın. \n\n\ Eğer hata kritikse ve ticaret tamamlanamazsa, ticaret ücretinizi kaybetmiş olabilirsiniz. \ Kaybedilen ticaret ücretleri için burada geri ödeme talebinde bulunun: \ [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.missingContract=Ticaret sözleşmesi ayarlanmadı.\n\n\ Ticaret tamamlanamaz ve ticaret ücretinizi kaybetmiş olabilirsiniz. \ Eğer öyleyse, kaybedilen ticaret ücretleri için burada geri ödeme talebinde bulunun: \ [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=Ticaret protokolü bazı sorunlarla karşılaştı.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=Ticaret protokolü ciddi bir sorunla karşılaştı.\n\n{0}\n\n\ Ticareti başarısız ticaretler arasına taşımak ister misiniz?\n\n\ Başarısız ticaretler görünümünden arabuluculuk veya tahkim açamazsınız, ancak başarısız bir ticareti istediğiniz zaman \ açık ticaretler ekranına geri taşıyabilirsiniz. portfolio.pending.failedTrade.txChainValid.moveToFailed=Ticaret protokolü bazı sorunlarla karşılaştı.\n\n{0}\n\n\ Ticaret işlemleri yayınlandı ve fonlar kilitlendi. Ticareti başarısız ticaretler arasına yalnızca gerçekten emin \ iseniz taşıyın. Sorunu çözme seçeneklerini engelleyebilir.\n\n\ Ticareti başarısız ticaretler arasına taşımak ister misiniz?\n\n\ Başarısız ticaretler görünümünden arabuluculuk veya tahkim açamazsınız, ancak başarısız bir ticareti istediğiniz zaman \ açık ticaretler ekranına geri taşıyabilirsiniz. portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Ticareti başarısız ticaretler arasına taşı portfolio.pending.failedTrade.warningIcon.tooltip=Bu ticaretin sorunları hakkında ayrıntıları açmak için tıklayın portfolio.failed.revertToPending.popup=Bu ticareti açık ticaretler arasına taşımak istiyor musunuz? portfolio.failed.revertToPending.failed=Bu ticareti açık ticaretler arasına taşımak başarısız oldu. portfolio.failed.revertToPending=Ticareti açık ticaretler arasına taşı portfolio.closed.completed=Tamamlandı portfolio.closed.ticketClosed=Hakem Kararıyla portfolio.closed.mediationTicketClosed=Arabuluculukla portfolio.closed.canceled=İptal Edildi portfolio.failed.Failed=Başarısız portfolio.failed.unfail=Devam etmeden önce, veri dizininizin bir yedeğine sahip olduğunuzdan emin olun!\n\ Bu ticareti açık ticaretler arasına geri taşımak istiyor musunuz?\n\ Bu, başarısız bir ticarette sıkışmış fonları açmanın bir yoludur. portfolio.failed.cantUnfail=Bu ticaret şu anda açık ticaretler arasına taşınamaz. \n\ {0} ticaret(lerinin) tamamlanmasından sonra tekrar deneyin. portfolio.failed.depositTxNull=Ticaret açık bir ticarete geri dönüştürülemez. Para yatırma işlemi null. portfolio.failed.delayedPayoutTxNull=Ticaret açık bir ticarete geri dönüştürülemez. Gecikmiş ödeme işlemi null. portfolio.failed.penalty.msg=Bu, {0}/{1} bir ceza ücreti olarak {2} tutarında ücret tahsil edecek ve kalan ticaret \ fonlarını cüzdanlarına geri gönderecek. Göndermek istediğinizden emin misiniz?\n\n\ Diğer Bilgiler:\n\ İşlem Ücreti: {3}\n\ Rezerv Tx Hash: {4} portfolio.failed.error.msg=Ticaret kaydı mevcut değil. #################################################################### # Funds #################################################################### funds.tab.deposit=Fon al funds.tab.withdrawal=Fon gönder funds.tab.reserved=Rezerve edilen fonlar funds.tab.locked=Kilitli fonlar funds.tab.transactions=İşlemler funds.deposit.unused=Kullanılmamış funds.deposit.usedInTx={0} işlemde kullanıldı funds.deposit.baseAddress=Temel adres funds.deposit.offerFunding=Teklif finansmanı için rezerve ({0}) funds.deposit.tradePayout=Ticaret ödemesi için rezerve ({0}) funds.deposit.fundHavenoWallet=Haveno cüzdanını finanse et funds.deposit.noAddresses=Henüz herhangi bir para yatırma adresi oluşturulmadı funds.deposit.fundWallet=Cüzdanınızı finanse edin funds.deposit.withdrawFromWallet=Cüzdandan fon gönder funds.deposit.amount=XMR miktarı (isteğe bağlı) funds.deposit.generateAddress=Yeni adres oluştur funds.deposit.generateAddressSegwit=Yerli segwit formatı (Bech32) funds.deposit.selectUnused=Lütfen yukarıdaki tablodan kullanılmamış bir adres seçin, yeni bir tane oluşturmaktansa. funds.withdrawal.arbitrationFee=Arabuluculuk ücreti funds.withdrawal.inputs=Girdi seçimi funds.withdrawal.useAllInputs=Mevcut tüm girdileri kullan funds.withdrawal.useCustomInputs=Özel girdileri kullan funds.withdrawal.receiverAmount=Alıcının miktarı funds.withdrawal.senderAmount=Gönderenin miktarı funds.withdrawal.feeExcluded=Miktar madenci ücretini içermez funds.withdrawal.feeIncluded=Miktar madenci ücretini içerir funds.withdrawal.fromLabel=Adresten çek funds.withdrawal.toLabel=Adrese çek funds.withdrawal.memoLabel=Çekme notu funds.withdrawal.memo=İsteğe bağlı olarak not ekleyin funds.withdrawal.withdrawButton=Seçilenleri çek funds.withdrawal.noFundsAvailable=Çekmek için mevcut fon yok funds.withdrawal.confirmWithdrawalRequest=Çekme talebini onayla funds.withdrawal.withdrawMultipleAddresses=Birden fazla adresten çek ({0}) funds.withdrawal.withdrawMultipleAddresses.tooltip=Birden fazla adresten çek:\n{0} funds.withdrawal.notEnoughFunds=Cüzdanınızda yeterli fon yok. funds.withdrawal.selectAddress=Tablodan bir kaynak adres seçin funds.withdrawal.setAmount=Çekilecek miktarı belirleyin funds.withdrawal.fillDestAddress=Hedef adresinizi doldurun funds.withdrawal.warn.noSourceAddressSelected=Yukarıdaki tablodan bir kaynak adres seçmeniz gerekiyor. funds.withdrawal.warn.amountExceeds=Seçilen adresten yeterli fon yok.\n\ Yukarıdaki tablodan birden fazla adres seçmeyi düşünün veya madenci ücretini içerecek şekilde ücreti değiştirin. funds.withdrawal.warn.amountMissing=Çekilecek bir miktar girin. funds.withdrawal.txFee=Çekme işlem ücreti (satoshis/vbyte) funds.withdrawal.useCustomFeeValueInfo=Özel bir işlem ücreti değeri girin funds.withdrawal.useCustomFeeValue=Özel değeri kullan funds.withdrawal.txFeeMin=İşlem ücreti en az {0} satoshis/vbyte olmalıdır funds.withdrawal.txFeeTooLarge=Girdiğiniz değer makul değerin üzerinde (>5000 satoshis/vbyte). İşlem ücreti genellikle 50-400 satoshis/vbyte arasında olur. funds.reserved.noFunds=Açık tekliflerde rezerve edilmiş fon yok funds.reserved.reserved=Teklif ID'si ile yerel cüzdanda rezerve: {0} funds.locked.noFunds=İşlemlerde kilitli fon yok funds.locked.locked=Ticaret ID'si ile multisig'de kilitli: {0} funds.tx.direction.sentTo=Gönderildi: funds.tx.direction.receivedWith=Alındı: funds.tx.direction.genesisTx=Genesis işleminden: funds.tx.createOfferFee=Üretici ve işlem ücreti: {0} funds.tx.takeOfferFee=Alıcı ve işlem ücreti: {0} funds.tx.multiSigDeposit=Multisig depozito: {0} funds.tx.multiSigPayout=Multisig ödeme: {0} funds.tx.disputePayout=Uyuşmazlık ödemesi: {0} funds.tx.disputeLost=Uyuşmazlık kaybedildi: {0} funds.tx.collateralForRefund=Teminat geri ödemesi: {0} funds.tx.timeLockedPayoutTx=Zaman kilitli ödeme işlemi: {0} funds.tx.refund=Arabuluculuktan geri ödeme: {0} funds.tx.unknown=Bilinmeyen neden: {0} funds.tx.noFundsFromDispute=Uyuşmazlıktan geri ödeme yok funds.tx.receivedFunds=Alınan fonlar funds.tx.withdrawnFromWallet=Cüzdandan çekildi funds.tx.memo=Not funds.tx.noTxAvailable=Mevcut işlem yok funds.tx.revert=Geri al funds.tx.txSent=İşlem yerel Haveno cüzdanında yeni bir adrese başarıyla gönderildi. funds.tx.direction.self=Kendinize gönderildi funds.tx.dustAttackTx=Toz alındı funds.tx.dustAttackTx.popup=Bu işlem cüzdanınıza çok küçük bir XMR miktarı gönderiyor ve zincir analizi şirketlerinin \ cüzdanınızı izlemeye çalışıyor olabileceği bir girişim olabilir.\n\n\ Bu işlem çıktısını harcama işleminde kullanırsanız, diğer adresin de muhtemelen size ait olduğunu öğrenirler \ (coin merge).\n\n\ Gizliliğinizi korumak için Haveno cüzdanı bu tür toz çıktıları harcama amaçlı ve bakiye \ görüntülemede görmezden gelir. Bir çıktının toz olarak kabul edildiği eşik miktarını ayarlarda belirleyebilirsiniz. #################################################################### # Support #################################################################### support.tab.mediation.support=Arabuluculuk support.tab.refund.support=Geri Ödeme support.tab.arbitration.support=Arbitraj support.tab.legacyArbitration.support=Eski Arbitraj support.tab.ArbitratorsSupportTickets={0}'nin biletleri support.tab.SignedOffers=İmzalı Teklifler support.prompt.signedOffer.penalty.msg=Bu, üreticiden bir ceza ücreti alacak ve kalan işlem fonlarını cüzdanına iade edecektir. Göndermek istediğinizden emin misiniz?\n\n\ Teklif ID'si: {0}\n\ Üretici Ceza Ücreti: {1}\n\ Rezerv Tx Madenci Ücreti: {2}\n\ Rezerv Tx Hash: {3}\n\ Rezerv Tx Anahtar Görüntüleri: {4}\n\ support.contextmenu.penalize.msg=Rezerv işlemi yayımlayarak {0}'i cezalandır support.prompt.signedOffer.error.msg=İmzalı Teklif kaydı mevcut değil; yöneticiyle iletişime geçin. support.info.submitTxHex=Rezerv işlemi aşağıdaki sonuçla yayımlandı:\n support.result.success=İşlem hex başarıyla gönderildi. support.sigCheck.button=İmzayı kontrol et support.sigCheck.popup.info=Arbitraj sürecinin özet mesajını yapıştırın. Bu araçla, herhangi bir kullanıcı hakemin imzasının özet mesajla eşleşip eşleşmediğini kontrol edebilir. support.sigCheck.popup.header=Uyuşmazlık sonucu imzasını doğrula support.sigCheck.popup.msg.label=Özet mesaj support.sigCheck.popup.msg.prompt=Uyuşmazlıktan özet mesajı kopyala ve yapıştır support.sigCheck.popup.result=Doğrulama sonucu support.sigCheck.popup.success=İmza geçerli support.sigCheck.popup.failed=İmza doğrulaması başarısız oldu support.sigCheck.popup.invalidFormat=Mesaj beklenen formatta değil. Uyuşmazlıktan özet mesajı kopyala ve yapıştır. support.reOpenByTrader.prompt=Uyuşmazlığı tekrar açmak istediğinizden emin misiniz? support.reOpenByTrader.failed=Uyuşmazlığı tekrar açma başarısız oldu. support.reOpenButton.label=Tekrar aç support.sendNotificationButton.label=Özel bildirim support.reportButton.label=Rapor et support.fullReportButton.label=Tüm uyuşmazlıklar support.noTickets=Açık bilet yok support.sendingMessage=Mesaj Gönderiliyor... support.receiverNotOnline=Alıcı çevrimdışı. Mesaj posta kutusuna kaydedildi. support.sendMessageError=Mesaj gönderme başarısız oldu. Hata: {0} support.receiverNotKnown=Alıcı bilinmiyor support.wrongVersion=Bu uyuşmazlıktaki teklif Haveno'nun eski bir sürümü ile oluşturulmuş.\n\ Bu uyuşmazlığı uygulamanızın sürümü ile kapatamazsınız.\n\n\ Lütfen protokol sürümü {0} olan eski bir sürüm kullanın. support.openFile=Eklenecek dosyayı aç (maks. dosya boyutu: {0} kb) support.attachmentTooLarge=Eklerinizin toplam boyutu {0} kb ve izin verilen en fazla mesaj boyutunu {1} kB aşıyor. support.maxSize=İzin verilen en fazla dosya boyutu {0} kB'dir. support.attachment=Ek support.tooManyAttachments=Bir mesajda 3'ten fazla ek gönderemezsiniz. support.save=Dosyayı diske kaydet support.messages=Mesajlar support.input.prompt=Mesaj girin... support.send=Gönder support.addAttachments=Ekleri ekle support.closeTicket=Bileti kapat support.attachments=Ekler: support.savedInMailbox=Mesaj alıcının posta kutusuna kaydedildi support.arrived=Mesaj alıcıya ulaştı support.transient=Mesaj alıcıya doğru yolda support.acknowledged=Mesajın alıcı tarafından alındığı teyit edildi support.error=Alıcı mesajı işleyemedi. Hata: {0} support.errorTimeout=zamanaşımı. Mesajı tekrar göndermeyi deneyin. support.buyerAddress=XMR alıcı adresi support.sellerAddress=XMR satıcı adresi support.role=Rol support.agent=Destek temsilcisi support.state=Durum support.chat=Sohbet support.preparing=Hazırlanıyor support.requested=Talep edildi support.closed=Kapalı support.open=Açık support.moreButton=DAHA FAZLA... support.sendLogFiles=Günlük Dosyalarını Gönder support.uploadTraderChat=Tüccar Sohbetini Yükle support.process=Süreç support.buyerMaker=XMR Alıcı/Üretici support.sellerMaker=XMR Satıcı/Üretici support.buyerTaker=XMR Alıcı/Alıcı support.sellerTaker=XMR Satıcı/Alıcı support.sendLogs.title=Günlük Dosyalarını Gönder support.sendLogs.backgroundInfo=Bir hata ile karşılaştığınızda, hakemler ve destek personeli genellikle sorunu teşhis etmek için günlük dosyalarınızın kopyalarını ister.\n\n\ 'Gönder' düğmesine bastığınızda, günlük dosyalarınız sıkıştırılacak ve doğrudan hakeme iletilecektir. support.sendLogs.step1=Günlük Dosyalarının Zip Arşivini Oluştur support.sendLogs.step2=Hakeme Bağlantı İsteği support.sendLogs.step3=Arşivlenmiş Günlük Verilerini Yükle support.sendLogs.send=Gönder support.sendLogs.cancel=İptal support.sendLogs.init=Başlatılıyor support.sendLogs.retry=Göndermeyi yeniden deniyor support.sendLogs.stopped=Transfer durduruldu support.sendLogs.progress=Transfer ilerlemesi: %.0f%% support.sendLogs.finished=Transfer tamamlandı! support.sendLogs.command=Yeniden denemek için 'Gönder' düğmesine basın veya iptal etmek için 'Durdur' düğmesine basın support.txKeyImages=Anahtar Görüntüleri support.txHash=İşlem Hash support.txHex=İşlem Hex support.signature=İmza support.maker.penalty.fee=Üretici Ceza Ücreti support.tx.miner.fee=Madenci Ücreti support.backgroundInfo=Haveno bir şirket değildir, bu yüzden uyuşmazlıkları farklı şekilde ele alır.\n\n\ Tüccarlar, açık işlemler ekranında güvenli sohbet üzerinden iletişim kurarak uyuşmazlıkları kendi başlarına çözmeye çalışabilirler. \ Eğer bu yeterli olmazsa, bir hakem durumu değerlendirecek ve işlem fonlarının \ ödemesine karar verecektir. support.initialInfo=Lütfen aşağıdaki metin alanına sorununuzun bir açıklamasını girin. \ Uyuşmazlık çözüm süresini hızlandırmak için mümkün olduğunca fazla bilgi ekleyin.\n\n\ Sağlamanız gereken bilgiler için bir kontrol listesi:\n\ \t● XMR alıcısıysanız: Geleneksel veya Kripto para transferi yaptınız mı? Eğer öyleyse, uygulamada 'ödeme başlatıldı' \ düğmesine tıkladınız mı?\n\ \t● XMR satıcısıysanız: Geleneksel veya Kripto para ödemesini aldınız mı? Eğer öyleyse, uygulamada 'ödeme alındı' \ düğmesine tıkladınız mı?\n\ \t● Hangi Haveno sürümünü kullanıyorsunuz?\n\ \t● Hangi işletim sistemini kullanıyorsunuz?\n\ \t● Başarısız işlemlerle ilgili bir sorunla karşılaştıysanız, yeni bir veri dizinine geçmeyi düşünün.\n\ \t Bazen veri dizini bozulur ve garip hatalara yol açar.\n\ \t Bkz: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\n\ Lütfen uyuşmazlık süreciyle ilgili temel kuralları öğrenin:\n\ \t● {0}'nin taleplerine 2 gün içinde yanıt vermelisiniz.\n\ \t● Arabulucular 2 gün içinde yanıt verir. Hakemler 5 iş günü içinde yanıt verir.\n\ \t● Bir uyuşmazlık için maksimum süre 14 gündür.\n\ \t● {1} ile işbirliği yapmalı ve davanızı oluşturmak için talep ettikleri bilgileri sağlamalısınız.\n\ \t● Uygulamayı ilk başlattığınızda kullanıcı sözleşmesinde uyuşmazlık belgesinde belirtilen kuralları kabul ettiniz.\n\n\ Uyuşmazlık süreci hakkında daha fazla bilgiyi şu adreste okuyabilirsiniz: {2} support.systemMsg=Sistem mesajı: {0} support.youOpenedTicket=Destek talebinde bulundunuz.\n\n{0}\n\nHaveno sürümü: {1} support.youOpenedDispute=Uyuşmazlık talebinde bulundunuz.\n\n{0}\n\nHaveno sürümü: {1} support.youOpenedDisputeForMediation=Arabuluculuk talebinde bulundunuz.\n\n{0}\n\nHaveno sürümü: {1} support.peerOpenedTicket=İşlem eşiniz teknik sorunlar nedeniyle destek talebinde bulundu.\n\n{0}\n\nHaveno sürümü: {1} support.peerOpenedDispute=İşlem eşiniz uyuşmazlık talebinde bulundu.\n\n{0}\n\nHaveno sürümü: {1} support.peerOpenedDisputeForMediation=İşlem eşiniz arabuluculuk talebinde bulundu.\n\n{0}\n\nHaveno sürümü: {1} support.mediatorsDisputeSummary=Sistem mesajı: Arabulucunun uyuşmazlık özeti:\n{0} support.mediatorReceivedLogs=Sistem mesajı: Arabulucu günlükleri aldı: {0} support.mediatorsAddress=Arabulucunun düğüm adresi: {0} support.warning.disputesWithInvalidDonationAddress=Gecikmiş ödeme işlemi geçersiz bir alıcı adresi kullanmıştır. \ Bu adres, geçerli bağış adresleri için DAO parametre değerlerinden hiçbiriyle eşleşmemektedir.\n\nBu bir dolandırıcılık girişimi olabilir. \ Lütfen bu durumu geliştiricilere bildirin ve durum çözülmeden bu davayı kapatmayın!\n\n\ Uyuşmazlıkta kullanılan adres: {0}\n\n\ Tüm DAO parametre bağış adresleri: {1}\n\n\ İşlem ID'si: {2}\ {3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nUyuşmazlığı yine de kapatmak istiyor musunuz? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nÖdemeyi yapmamalısınız. support.warning.traderCloseOwnDisputeWarning=Tüccarlar destek biletlerini ancak işlem ödendikten sonra kendileri kapatabilirler. support.info.disputeReOpened=Uyuşmazlık bileti tekrar açıldı. #################################################################### # Settings #################################################################### settings.tab.preferences=Tercihler settings.tab.network=Ağ bilgisi settings.tab.about=Hakkında setting.preferences.general=Genel tercihler setting.preferences.explorer=Monero Gezgini setting.preferences.deviation=Piyasa fiyatından maksimum sapma setting.preferences.avoidStandbyMode=Bekleme modundan kaçın setting.preferences.useSoundForNotifications=Bildirimler için sesleri çal setting.preferences.autoConfirmXMR=XMR otomatik onay setting.preferences.autoConfirmEnabled=Etkin setting.preferences.autoConfirmRequiredConfirmations=Gerekli onaylar setting.preferences.autoConfirmMaxTradeSize=Maks. işlem miktarı (XMR) setting.preferences.autoConfirmServiceAddresses=Monero Gezgini URL'leri (localhost, LAN IP adresleri ve *.local ana bilgisayar adları hariç Tor kullanır) setting.preferences.deviationToLarge={0}% üzerindeki değerler izin verilmez. setting.preferences.txFee=BSQ Çekme işlem ücreti (satoshi/vbyte) setting.preferences.useCustomValue=Özel değeri kullan setting.preferences.ignorePeers=Yok sayılan eşler [onion address:port] setting.preferences.ignoreDustThreshold=Min. toz olmayan çıkış değeri setting.preferences.currenciesInList=Piyasa fiyat listesinde para birimleri setting.preferences.prefCurrency=Tercih edilen para birimi setting.preferences.displayTraditional=Geleneksel para birimlerini göster setting.preferences.noTraditional=Seçilmiş geleneksel para birimleri yok setting.preferences.cannotRemovePrefCurrency=Seçtiğiniz tercih edilen görüntüleme para birimini kaldıramazsınız setting.preferences.displayCryptos=Kripto paraları göster setting.preferences.noCryptos=Seçilmiş kripto paralar yok setting.preferences.addTraditional=Geleneksel para birimi ekle setting.preferences.addCrypto=Kripto para ekle setting.preferences.displayOptions=Görüntüleme seçenekleri setting.preferences.showOwnOffers=Teklif defterinde kendi tekliflerini göster setting.preferences.useAnimations=Animasyonları kullan setting.preferences.useDarkMode=Karanlık modu kullan setting.preferences.useLightMode=Aydınlık modu kullan setting.preferences.sortWithNumOffers=Piyasaları teklif sayısına göre sırala setting.preferences.onlyShowPaymentMethodsFromAccount=Olmayan ödeme yöntemlerini gizle setting.preferences.denyApiTaker=API kullanan alıcıları reddet setting.preferences.notifyOnPreRelease=Ön sürüm bildirimlerini al setting.preferences.resetAllFlags=Tüm \"Tekrar gösterme\" bayraklarını sıfırla settings.preferences.languageChange=Dil değişikliğinin tüm ekranlarda uygulanması için yeniden başlatma gereklidir. settings.preferences.supportLanguageWarning=Bir uyuşmazlık durumunda, hakemlik işlemleri {0} dilinde yapılır. settings.preferences.editCustomExplorer.headline=Gezgin Ayarları settings.preferences.editCustomExplorer.description=Soldaki listeden bir sistem tanımlı gezgini seçin ve/veya \ kendi tercihlerinize göre özelleştirin. settings.preferences.editCustomExplorer.available=Mevcut gezginler settings.preferences.editCustomExplorer.chosen=Seçilmiş gezgin ayarları settings.preferences.editCustomExplorer.name=İsim settings.preferences.editCustomExplorer.txUrl=İşlem URL'si settings.preferences.editCustomExplorer.addressUrl=Adres URL'si setting.info.headline=Yeni veri gizliliği özelliği settings.preferences.sensitiveDataRemoval.msg=Kendinizin ve diğer tacirlerin gizliliğini korumak için Haveno, eski işlemlerden hassas verileri kaldırmayı planlamaktadır. Bu, banka hesap bilgilerini içerebilecek fiat işlemleri için özellikle önemlidir.\n\nVeri silme eşiğinin mümkün olduğunca düşük, örneğin 60 gün olarak ayarlanması önerilir. Bu, 60 günden eski ve tamamlanmış işlemlerde hassas verilerin temizleneceği anlamına gelir. Tamamlanmış işlemler Portföy / Geçmiş sekmesinde bulunabilir. settings.net.xmrHeader=Monero ağı settings.net.p2pHeader=Haveno ağı settings.net.onionAddressLabel=Onion adresim settings.net.xmrNodesLabel=Özel Monero düğümleri kullan settings.net.moneroPeersLabel=Bağlı eşler settings.net.connection=Bağlantı settings.net.connected=Bağlı settings.net.useTorForXmrJLabel=Monero ağı için Tor kullan settings.net.useTorForXmrAfterSyncRadio=Cüzdan senkronize edildikten sonra settings.net.useTorForXmrOffRadio=Asla settings.net.useTorForXmrOnRadio=Her zaman settings.net.moneroNodesLabel=Bağlanılacak Monero düğümleri settings.net.useProvidedNodesRadio=Sağlanan Monero düğümlerini kullan settings.net.usePublicNodesRadio=Genel Monero ağını kullan settings.net.useCustomNodesRadio=Özel Monero düğümlerini kullan settings.net.warn.usePublicNodes=Genel Monero düğümlerini kullanırsanız, güvenilmeyen uzak düğümleri kullanmanın getirdiği tüm risklere tabisiniz.\n\nDaha fazla ayrıntı için lütfen [HYPERLINK:https://www.getmonero.org/resources/moneropedia/remote-node.html] adresini okuyun.\n\nGenel düğümleri kullanmak istediğinizden emin misiniz? settings.net.warn.usePublicNodes.useProvided=Hayır, sağlanan düğümleri kullan settings.net.warn.usePublicNodes.usePublic=Evet, genel ağı kullan settings.net.warn.useCustomNodes.B2XWarning=Lütfen Monero düğümünüzün güvenilir bir Monero düğümü olduğundan emin olun!\n\n\ Monero konsensüs kurallarını takip etmeyen düğümlere bağlanmak cüzdanınızı bozabilir ve işlem sürecinde sorunlara yol açabilir.\n\n\ Konsensüs kurallarını ihlal eden düğümlere bağlanan kullanıcılar, ortaya çıkan herhangi bir hasardan sorumludur. \ Ortaya çıkan herhangi bir uyuşmazlık diğer taraf lehine karar verilecektir. \ Bu uyarıyı ve koruma mekanizmalarını görmezden gelen kullanıcılara teknik destek verilmeyecektir! settings.net.warn.invalidXmrConfig=Monero ağına bağlantı yapılandırmanız geçersiz olduğu için başarısız oldu.\n\nYapılandırmanız sağlanan Monero düğümlerini kullanacak şekilde sıfırlandı. Uygulamayı yeniden başlatmanız gerekecek. settings.net.localhostXmrNodeInfo=Arka plan bilgisi: Haveno, başlatıldığında yerel bir Monero düğümü arar. Bulunursa, Haveno Monero ağıyla yalnızca bu düğüm aracılığıyla iletişim kuracaktır. settings.net.p2PPeersLabel=Bağlı eşler settings.net.onionAddressColumn=Onion adresi settings.net.creationDateColumn=Kuruluş settings.net.connectionTypeColumn=G/Ç settings.net.sentDataLabel=Gönderilen veri istatistikleri settings.net.receivedDataLabel=Alınan veri istatistikleri settings.net.chainHeightLabel=Son XMR blok yüksekliği settings.net.roundTripTimeColumn=Gidiş-dönüş süresi settings.net.sentBytesColumn=Gönderilen settings.net.receivedBytesColumn=Alınan settings.net.peerTypeColumn=Eş türü settings.net.openTorSettingsButton=Tor ayarlarını aç settings.net.versionColumn=Sürüm settings.net.subVersionColumn=Alt sürüm settings.net.heightColumn=Yükseklik settings.net.needRestart=Bu değişikliği uygulamak için uygulamayı yeniden başlatmanız gerekiyor.\nŞimdi yapmak istiyor musunuz? settings.net.notKnownYet=Henüz bilinmiyor... settings.net.sentData=Gönderilen veri: {0}, {1} mesaj, {2} mesaj/saniye settings.net.receivedData=Alınan veri: {0}, {1} mesaj, {2} mesaj/saniye settings.net.chainHeight=Monero Eşlerinin blok yüksekliği: {0} settings.net.ips=[IP adresi:port | ana bilgisayar adı:port | onion adresi:port] (virgülle ayrılmış). Varsayılan kullanılıyorsa port belirtilebilir ({0}). settings.net.seedNode=Tohum düğümü settings.net.directPeer=Eş (doğrudan) settings.net.initialDataExchange={0} [Başlatılıyor] settings.net.peer=Eş settings.net.inbound=gelen settings.net.outbound=giden settings.net.rescanOutputsLabel=Çıktıları Yeniden Tara settings.net.rescanOutputsButton=Cüzdan Çıktılarını Yeniden Tara settings.net.rescanOutputsSuccess=Cüzdan çıktılarını yeniden taramak istediğinize emin misiniz? settings.net.rescanOutputsFailed=Cüzdan çıktılarını yeniden tarayamadı.\nHata: {0} setting.about.aboutHaveno=Haveno Hakkında setting.about.about=Haveno, kullanıcı gizliliğini güçlü bir şekilde koruyan merkezi olmayan bir eşler arası ağ aracılığıyla monero'nun ulusal para birimleri (ve diğer kripto para birimleri) ile değişimini kolaylaştıran açık kaynaklı bir yazılımdır. Haveno hakkında daha fazla bilgi edinmek için proje web sayfamızı ziyaret edin. setting.about.web=Haveno web sayfası setting.about.code=Kaynak kodu setting.about.agpl=AGPL Lisansı setting.about.support=Haveno'yu Destekleyin setting.about.def=Haveno bir şirket değildir—topluluğa açık bir projedir. Haveno'yu desteklemek istiyorsanız lütfen aşağıdaki bağlantıları takip edin. setting.about.contribute=Katkıda Bulunun setting.about.providers=Veri sağlayıcılar setting.about.apisWithFee=Haveno, Geleneksel ve Kripto Para piyasası fiyatları için fiyat endeksleri kullanır setting.about.apis=Haveno, Geleneksel ve Kripto Para piyasası fiyatları için Fiyat Endeksleri kullanır. setting.about.pricesProvided=Piyasa fiyatları tarafından sağlanır setting.about.feeEstimation.label=Madencilik ücreti tahmini tarafından sağlanır setting.about.versionDetails=Sürüm detayları setting.about.version=Uygulama sürümü setting.about.subsystems.label=Alt sistemlerin sürümleri setting.about.subsystems.val=Ağ sürümü: {0}; P2P mesaj sürümü: {1}; Yerel DB sürümü: {2}; Ticaret protokolü sürümü: {3} setting.about.shortcuts=Kısayollar setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' veya 'alt + {0}' veya 'cmd + {0}' setting.about.shortcuts.menuNav=Ana menüde gezin setting.about.shortcuts.menuNav.value=Ana menüde gezinmek için şuna basın: 'Ctrl' veya 'alt' veya 'cmd' ve '1-9' arasındaki bir sayı tuşu setting.about.shortcuts.close=Haveno'yu kapat setting.about.shortcuts.close.value='Ctrl + {0}' veya 'cmd + {0}' veya 'Ctrl + {1}' veya 'cmd + {1}' setting.about.shortcuts.closePopup=Açılır pencereyi veya iletişim penceresini kapat setting.about.shortcuts.closePopup.value='ESCAPE' tuşu setting.about.shortcuts.chatSendMsg=Trader sohbet mesajı gönder setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' veya 'alt + ENTER' veya 'cmd + ENTER' setting.about.shortcuts.openDispute=Uyuşmazlık aç setting.about.shortcuts.openDispute.value=Bekleyen işlemi seçin ve tıklayın: {0} setting.about.shortcuts.walletDetails=Cüzdan detayları penceresini aç setting.about.shortcuts.openEmergencyXmrWalletTool=Acil durum cüzdan aracı penceresini aç setting.about.shortcuts.showTorLogs=Tor mesajları için log seviyesini DEBUG ve WARN arasında değiştir setting.about.shortcuts.manualPayoutTxWindow=2of2 Multisig depozit işleminden manuel ödeme penceresini aç setting.about.shortcuts.removeStuckTrade=Başarısız işlemi tekrar açık işlemler sekmesine taşımak için açılır pencereyi aç setting.about.shortcuts.removeStuckTrade.value=Başarısız işlemi seçin ve basın: {0} setting.about.shortcuts.registerArbitrator=Arabulucu kaydettir (arabulucu/hakem sadece) setting.about.shortcuts.registerArbitrator.value=Hesaba gidin ve basın: {0} setting.about.shortcuts.registerMediator=Arabulucu kaydettir (arabulucu/hakem sadece) setting.about.shortcuts.registerMediator.value=Hesaba gidin ve basın: {0} setting.about.shortcuts.openSignPaymentAccountsWindow=Hesap yaşı imzalama penceresini aç (eski hakemler sadece) setting.about.shortcuts.openSignPaymentAccountsWindow.value=Eski hakem görünümüne gidin ve basın: {0} setting.about.shortcuts.sendAlertMsg=Uyarı veya güncelleme mesajı gönder (ayrıcalıklı etkinlik) setting.about.shortcuts.sendFilter=Filtre Ayarla (ayrıcalıklı etkinlik) setting.about.shortcuts.sendPrivateNotification=Eşe özel bildirim gönder (ayrıcalıklı etkinlik) setting.about.shortcuts.sendPrivateNotification.value=Eş bilgilerini avatar üzerinde açın ve basın: {0} #################################################################### # Account #################################################################### account.tab.arbitratorRegistration=Hakem kaydı account.tab.mediatorRegistration=Arabulucu kaydı account.tab.refundAgentRegistration=İade temsilcisi kaydı account.tab.signing=İmzalama account.menu.paymentAccount=Geleneksel para hesapları account.menu.altCoinsAccountView=Kripto para hesapları account.menu.password=Cüzdan şifresi account.menu.seedWords=Cüzdan kelimeleri account.menu.walletInfo=Cüzdan bilgileri account.menu.backup=Yedekleme account.menu.notifications=Bildirimler account.menu.walletInfo.balance.headLine=Cüzdan bakiyeleri account.menu.walletInfo.balance.info=Bu, onaylanmamış işlemler de dahil olmak üzere iç cüzdan bakiyesini gösterir.\n\ XMR için, aşağıda gösterilen iç cüzdan bakiyesi, bu pencerenin sağ üst köşesinde gösterilen 'Kullanılabilir' ve 'Ayrılmış' bakiyelerin toplamına eşit olmalıdır. account.menu.walletInfo.xpub.headLine=İzleme anahtarları (xpub anahtarları) account.menu.walletInfo.walletSelector={0} {1} cüzdan account.menu.walletInfo.path.headLine=HD anahtar zinciri yolları account.menu.walletInfo.path.info=Eğer seed kelimelerini başka bir cüzdana (örneğin Electrum) aktarırsanız, \ yolu tanımlamanız gerekecek. Bu sadece acil durumlarda Haveno cüzdanına ve veri dizinine erişimi kaybettiğinizde yapılmalıdır.\n\ Unutmayın ki, Haveno dışındaki bir cüzdandan fon harcamak, cüzdan verileriyle ilişkili Haveno iç veri \ yapılarını bozabilir, bu da başarısız ticaretlere yol açabilir.\n\n\ BSQ'yu Haveno dışındaki bir cüzdandan ASLA göndermeyin, çünkü bu muhtemelen geçersiz bir BSQ işlemi ve BSQ kaybına yol açar. account.menu.walletInfo.openDetails=Ham cüzdan detaylarını ve özel anahtarları göster ## TODO genel bir isimle yeniden adlandırmalı mıyız? account.arbitratorRegistration.pubKey=Genel anahtar account.arbitratorRegistration.register=Kaydol account.arbitratorRegistration.registration={0} kaydı account.arbitratorRegistration.revoke=İptal et account.arbitratorRegistration.info.msg=Lütfen kaydınızı iptal ettikten sonra 15 gün boyunca ulaşılabilir kalmanız gerektiğini unutmayın, çünkü {0} olarak sizi kullanan ticaretler olabilir. Maksimum izin verilen ticaret süresi 8 gündür ve anlaşmazlık süreci 7 güne kadar sürebilir. account.arbitratorRegistration.warn.min1Language=En az 1 dil ayarlamanız gerekmektedir.\nSizin için varsayılan dili ekledik. account.arbitratorRegistration.removedSuccess=Kayıt işleminizi Haveno ağından başarıyla kaldırdınız. account.arbitratorRegistration.removedFailed=Kaydı kaldıramadı.{0} account.arbitratorRegistration.registerSuccess=Haveno ağına başarıyla kaydoldunuz. account.arbitratorRegistration.registerFailed=Kaydı tamamlayamadı.{0} account.crypto.yourCryptoAccounts=Kripto para hesaplarınız account.crypto.popup.wallet.msg=Lütfen {0} cüzdanlarının kullanım gereksinimlerini {1} web sayfasında \ açıklanan şekilde takip ettiğinizden emin olun.\nAnahtarlarını kontrol etmediğiniz merkezi borsalardan cüzdan kullanmak veya \ uyumlu cüzdan yazılımı kullanmayan cüzdanlar kullanmak risklidir: ticaret fonlarının kaybına yol açabilir!\nArabulucu veya hakem \ {2} uzmanı değildir ve bu tür durumlarda yardımcı olamaz. account.crypto.popup.wallet.confirm=Hangi cüzdanı kullanmam gerektiğini anladığımı ve onayladığımı kabul ediyorum. # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=Haveno'da UPX ticareti yapmak için aşağıdaki gereksinimleri \ anladığınızı ve yerine getirdiğinizi kabul etmeniz gerekmektedir:\n\n\ UPX gönderimi için, ya resmi uPlexa GUI cüzdanını ya da store-tx-info bayrağı etkin olan uPlexa CLI cüzdanını \ kullanmanız gerekmektedir (yeni sürümlerde varsayılan). Lütfen işlem anahtarına erişebildiğinizden emin olun, \ çünkü bir anlaşmazlık durumunda bu gerekecektir.\n\ uplexa-wallet-cli (get_tx_key komutunu kullanın)\n\ uplexa-wallet-gui (geçmiş sekmesine gidin ve ödeme kanıtı için (P) butonuna tıklayın)\n\n\ Normal blok kaşiflerinde transfer doğrulanamaz.\n\n\ Bir anlaşmazlık durumunda hakeme aşağıdaki verileri sağlamanız gerekmektedir:\n\ - İşlem özel anahtarı\n\ - İşlem hash'i\n\ - Alıcının genel adresi\n\n\ Yukarıdaki verileri sağlayamamak veya uyumsuz bir cüzdan kullanmak, anlaşmazlık durumunda kaybetmenize yol açacaktır. \ Anlaşmazlık durumunda hakeme UPX transferinin doğrulanmasını sağlamak UPX göndericisinin sorumluluğundadır.\n\n\ Sadece normal genel adres gereklidir, ödeme kimliği gerekli değildir.\n\ Bu süreç hakkında emin değilseniz uPlexa discord kanalını (https://discord.gg/vhdNSrV) \ veya uPlexa Telegram Sohbetini (https://t.me/uplexaOfficial) ziyaret ederek daha fazla bilgi bulabilirsiniz. # suppress inspection "UnusedProperty" account.crypto.popup.arq.msg=Haveno'da ARQ ticareti yapmak için aşağıdaki gereksinimleri \ anladığınızı ve yerine getirdiğinizi kabul etmeniz gerekmektedir:\n\n\ ARQ gönderimi için, ya resmi ArQmA GUI cüzdanını ya da store-tx-info bayrağı etkin olan ArQmA CLI cüzdanını \ kullanmanız gerekmektedir (yeni sürümlerde varsayılan). \ Lütfen işlem anahtarına erişebildiğinizden emin olun, \ çünkü bir anlaşmazlık durumunda bu gerekecektir.\n\ arqma-wallet-cli (get_tx_key komutunu kullanın)\n\ arqma-wallet-gui (geçmiş sekmesine gidin ve ödeme kanıtı için (P) butonuna tıklayın)\n\n\ Normal blok kaşiflerinde transfer doğrulanamaz.\n\n\ Bir anlaşmazlık durumunda hakeme aşağıdaki verileri sağlamanız gerekmektedir:\n\ - İşlem özel anahtarı\n\ - İşlem hash'i\n\ - Alıcının genel adresi\n\n\ Yukarıdaki verileri sağlayamamak veya uyumsuz bir cüzdan kullanmak, anlaşmazlık durumunda kaybetmenize yol açacaktır. \ Anlaşmazlık durumunda hakeme ARQ transferinin doğrulanmasını \ sağlamak ARQ göndericisinin sorumluluğundadır.\n\n\ Sadece normal genel adres gereklidir, ödeme kimliği gerekli değildir.\n\ Bu süreç hakkında emin değilseniz ArQmA discord kanalını (https://discord.gg/s9BQpJT) \ veya ArQmA forumunu (https://labs.arqma.com) ziyaret ederek daha fazla bilgi bulabilirsiniz. # suppress inspection "UnusedProperty" account.crypto.popup.xmr.msg=Haveno'da XMR ticareti yapmak için aşağıdaki gereksinimi anlamanız gerekmektedir.\n\n\ XMR satıyorsanız, bir anlaşmazlık durumunda arabulucuya veya \ hakeme aşağıdaki bilgileri sağlayabilmeniz gerekmektedir:\n\ - İşlem anahtarı (Tx Anahtarı, Tx Gizli Anahtarı veya Tx Özel Anahtarı)\n\ - İşlem kimliği (Tx Kimliği veya Tx Hash)\n\ - Hedef adres (alıcının adresi)\n\n\ Popüler Monero cüzdanlarında bu bilgilerin nerede bulunacağını öğrenmek için wiki'yi inceleyin [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Proving_payments].\n\ Gerekli işlem verilerini sağlayamamak anlaşmazlıkları kaybetmenize yol açar.\n\n\ Ayrıca Haveno'nun artık XMR işlemlerini otomatik onaylama özelliği sunduğunu, ancak bu özelliği Ayarlar'dan etkinleştirmeniz gerektiğini unutmayın.\n\n\ Otomatik onaylama özelliği hakkında daha fazla bilgi için wiki'yi inceleyin: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. # suppress inspection "UnusedProperty" account.crypto.popup.msr.msg=Haveno'da MSR ticareti yapmak, aşağıdaki gereksinimleri anladığınızı \ ve yerine getirdiğinizi kabul etmenizi gerektirir:\n\n\ MSR göndermek için resmi Masari GUI cüzdanını, store-tx-info bayrağı etkin olan (varsayılan olarak etkin) \ Masari CLI cüzdanını veya Masari web cüzdanını (https://wallet.getmasari.org) kullanmanız gerekir. Bir anlaşmazlık durumunda gerekli olacağı \ için tx anahtarına erişebildiğinizden emin olun.\n\ masari-wallet-cli (get_tx_key komutunu kullanın)\n\ masari-wallet-gui (geçmiş sekmesine gidin ve ödeme kanıtı için (P) düğmesine tıklayın)\n\n\ Masari Web Cüzdan (Hesap -> işlem geçmişine gidin ve gönderdiğiniz işlemin detaylarını görüntüleyin)\n\n\ Doğrulama cüzdan içinde yapılabilir.\n\ masari-wallet-cli : check_tx_key komutunu kullanarak.\n\ masari-wallet-gui : Gelişmiş > Kanıtla/Kontrol et sayfasında.\n\ Doğrulama blok kaşifinde yapılabilir \n\ Blok kaşifini açın (https://explorer.getmasari.org), işlem hash'inizi bulmak için arama çubuğunu kullanın.\n\ İşlem bulunduğunda, 'Gönderim Kanıtı' alanına gidin ve gerekli bilgileri doldurun.\n\ Bir anlaşmazlık durumunda arabulucuya veya hakeme aşağıdaki verileri sağlamanız gerekecektir:\n\ - Tx özel anahtarı\n\ - İşlem hash'i\n\ - Alıcının genel adresi\n\n\ Yukarıdaki verileri sağlamamak veya uyumsuz bir cüzdan kullanmak, anlaşmazlık durumunu kaybetmenize \ neden olacaktır. MSR göndericisi, bir anlaşmazlık durumunda arabulucuya veya hakeme MSR \ transferinin doğrulamasını sağlama sorumluluğunu taşır.\n\n\ Ödeme kimliği gerekli değildir, sadece normal genel adres gereklidir.\n\ Bu süreç hakkında emin değilseniz, Resmi Masari Discord'da (https://discord.gg/sMCwMqs) yardım isteyin. # suppress inspection "UnusedProperty" account.crypto.popup.blur.msg=Haveno'da BLUR ticareti yapmak, aşağıdaki gereksinimleri anladığınızı \ ve yerine getirdiğinizi kabul etmenizi gerektirir:\n\n\ BLUR göndermek için Blur Network CLI veya GUI Cüzdanını kullanmalısınız.\n\n\ CLI cüzdanını kullanıyorsanız, bir transfer gönderildikten sonra bir işlem hash'i (tx ID) görüntülenecektir. Bu bilgiyi \ kaydetmelisiniz. Transferi gönderdikten hemen sonra, işlem özel anahtarını almak için 'get_tx_key' komutunu \ kullanmalısınız. Bu adımı gerçekleştirmezseniz, anahtarı daha sonra almanız mümkün olmayabilir.\n\n\ Blur Network GUI Cüzdanını kullanıyorsanız, işlem özel anahtarı ve işlem ID'si "Geçmiş" sekmesinde kolayca bulunabilir.\ Gönderdikten hemen sonra, ilgili işlemi bulun. İşlemi içeren kutunun sağ alt \ köşesindeki "?" simgesine tıklayın. Bu bilgiyi kaydetmelisiniz.\n\n\ Arabuluculuk gerekli olduğunda, arabulucuya veya hakeme şu bilgileri sunmanız gerekecektir: 1.) işlem ID'si, \ 2.) işlem özel anahtarı ve 3.) alıcının adresi. Arabulucu veya hakem, Blur İşlem Görüntüleyicisini \ (https://blur.cash/#tx-viewer) kullanarak BLUR transferini doğrulayacaktır.\n\n\ Gerekli bilgileri arabulucuya veya hakeme sağlamamak, anlaşmazlık durumunu kaybetmenize neden olacaktır. Tüm anlaşmazlık durumlarında, \ BLUR göndericisi, işlemleri arabulucuya veya hakeme doğrulama sorumluluğunu %100 oranında taşır.\n\n\ Bu gereksinimleri anlamıyorsanız, Haveno'da ticaret yapmayın. Öncelikle Blur Network Discord'da (https://discord.gg/dMWaqVW) yardım isteyin. # suppress inspection "UnusedProperty" account.crypto.popup.solo.msg=Haveno'da Solo ticareti yapmak, aşağıdaki gereksinimleri \ anladığınızı ve yerine getirdiğinizi kabul etmenizi gerektirir:\n\n\ Solo göndermek için Solo Network CLI Cüzdanını kullanmalısınız. \n\n\ CLI cüzdanını kullanıyorsanız, bir transfer gönderildikten sonra bir işlem hash'i (tx ID) görüntülenecektir. \ Bu bilgiyi kaydetmelisiniz. Transferi gönderdikten hemen sonra, işlem özel anahtarını almak için 'get_tx_key' \ komutunu kullanmalısınız. Bu adımı gerçekleştirmezseniz, anahtarı daha sonra almanız mümkün olmayabilir. \n\n\ Arabuluculuk gerekli olduğunda, arabulucuya veya hakeme şu bilgileri sunmanız gerekecektir: 1) işlem ID'si, \ 2) işlem özel anahtarı ve 3) alıcının adresi. Arabulucu veya hakem, Solo transferini Solo Blok Kaşifi'ni kullanarak \ işlemi aratarak ve ardından "Gönderim kanıtı" işlevini kullanarak doğrulayacaktır (https://explorer.minesolo.com/).\n\n\ Gerekli bilgileri arabulucuya veya hakeme sağlamamak, anlaşmazlık durumunu kaybetmenize neden olacaktır. Tüm anlaşmazlık durumlarında, \ Solo göndericisi, işlemleri arabulucuya veya hakeme doğrulama sorumluluğunu %100 oranında taşır. \n\n\ Bu gereksinimleri anlamıyorsanız, Haveno'da ticaret yapmayın. Öncelikle Solo Network Discord'da (https://discord.minesolo.com/) yardım isteyin. # suppress inspection "UnusedProperty" account.crypto.popup.cash2.msg=Haveno'da CASH2 ticareti yapmak, aşağıdaki gereksinimleri anladığınızı \ ve yerine getirdiğinizi kabul etmenizi gerektirir:\n\n\ CASH2 göndermek için Cash2 Cüzdan sürüm 3 veya daha yüksek bir sürümünü kullanmalısınız. \n\n\ Bir işlem gönderildikten sonra, işlem ID'si görüntülenecektir. Bu bilgiyi kaydetmelisiniz. Transferi \ gönderdikten hemen sonra, işlem gizli anahtarını almak için simplewallet'te 'getTxKey' \ komutunu kullanmalısınız. \n\n\ Arabuluculuk gerekli olduğunda, arabulucuya veya hakeme şu bilgileri sunmanız gerekecektir: 1) işlem ID'si, \ 2) işlem gizli anahtarı ve 3) alıcının Cash2 adresi. Arabulucu veya hakem, CASH2 transferini Cash2 Blok Kaşifi'ni \ kullanarak doğrulayacaktır (https://blocks.cash2.org).\n\n\ Gerekli bilgileri arabulucuya veya hakeme sağlamamak, anlaşmazlık durumunu kaybetmenize neden olacaktır. Tüm anlaşmazlık durumlarında, \ CASH2 göndericisi, işlemleri arabulucuya veya hakeme doğrulama sorumluluğunu %100 oranında taşır. \n\n\ Bu gereksinimleri anlamıyorsanız, Haveno'da ticaret yapmayın. Öncelikle Cash2 Discord'da (https://discord.gg/FGfXAYN) yardım isteyin. # suppress inspection "UnusedProperty" account.crypto.popup.qwertycoin.msg=Haveno'da Qwertycoin ticareti yapmak, aşağıdaki gereksinimleri anladığınızı \ ve yerine getirdiğinizi kabul etmenizi gerektirir:\n\n\ QWC göndermek için resmi QWC Cüzdan sürüm 5.1.3 veya daha yüksek bir sürümünü kullanmalısınız. \n\n\ Bir işlem gönderildikten sonra, işlem ID'si görüntülenecektir. Bu bilgiyi kaydetmelisiniz. Transferi \ gönderdikten hemen sonra, işlem gizli anahtarını almak için simplewallet'te 'get_Tx_Key' \ komutunu kullanmalısınız. \n\n\ Arabuluculuk gerekli olduğunda, arabulucuya veya hakeme şu bilgileri sunmanız gerekecektir: 1) işlem ID'si, \ 2) işlem gizli anahtarı ve 3) alıcının QWC adresi. Arabulucu veya hakem, QWC transferini QWC Blok Kaşifi'ni \ kullanarak doğrulayacaktır (https://explorer.qwertycoin.org).\n\n\ Gerekli bilgileri arabulucuya veya hakeme sağlamamak, anlaşmazlık durumunu kaybetmenize neden olacaktır. Tüm anlaşmazlık durumlarında, \ QWC göndericisi, işlemleri arabulucuya veya hakeme doğrulama sorumluluğunu %100 oranında taşır. \n\n\ Bu gereksinimleri anlamıyorsanız, Haveno'da ticaret yapmayın. Öncelikle QWC Discord'da (https://discord.gg/rUkfnpC) yardım isteyin. # suppress inspection "UnusedProperty" account.crypto.popup.drgl.msg=Haveno'da Dragonglass ticareti yapmak, aşağıdaki gereksinimleri anladığınızı \ ve yerine getirdiğinizi kabul etmenizi gerektirir:\n\n\ Dragonglass'ın sağladığı gizlilik nedeniyle, bir işlem genel blockchain üzerinde doğrulanamaz. Gerektiğinde, ödemenizi \ TXN-Private-Key'inizi kullanarak kanıtlayabilirsiniz.\n\ TXN-Private Key, her işlem için otomatik olarak oluşturulan ve yalnızca DRGL cüzdanınızdan \ erişilebilen tek seferlik bir anahtardır.\n\ DRGL-cüzdan GUI'si (işlem detayları diyalogu içinde) veya Dragonglass CLI simplewallet'i (get_tx_key komutunu kullanarak) ile erişilebilir.\n\n\ Her ikisi için de 'Oathkeeper' ve daha yüksek sürüm DRGL gereklidir.\n\n\ Bir anlaşmazlık durumunda, arabulucuya veya hakeme şu verileri sağlamanız gerekecektir:\n\ - TXN-Private key\n\ - İşlem hash'i\n\ - Alıcının genel adresi\n\n\ Ödeme doğrulaması, yukarıdaki veriler kullanılarak (http://drgl.info/#check_txn) adresinde yapılabilir.\n\n\ Yukarıdaki verileri sağlamamak veya uyumsuz bir cüzdan kullanmak, anlaşmazlık durumunu kaybetmenize \ neden olacaktır. Bir anlaşmazlık durumunda, Dragonglass göndericisi DRGL transferinin doğrulamasını \ arabulucuya veya hakeme sağlamakla sorumludur. PaymentID kullanımı gerekli değildir.\n\n\ Bu sürecin herhangi bir bölümünden emin değilseniz, yardım için Dragonglass Discord'da (http://discord.drgl.info) ziyaret edin. # suppress inspection "UnusedProperty" account.crypto.popup.ZEC.msg=Zcash kullanırken sadece şeffaf adresleri (t ile başlayan) kullanabilirsiniz, \ z-adreslerini (özel) kullanamazsınız, çünkü arabulucu veya hakem z-adresleri ile işlemi doğrulayamaz. # suppress inspection "UnusedProperty" account.crypto.popup.XZC.msg=Zcoin kullanırken sadece şeffaf (izlenebilir) adresleri kullanabilirsiniz, \ izlenemez adresleri kullanamazsınız, çünkü arabulucu veya hakem blok kaşifinde izlenemez adreslerle işlemi doğrulayamaz. # suppress inspection "UnusedProperty" account.crypto.popup.grin.msg=GRIN, işlemi oluşturmak için gönderen ve alıcı arasında etkileşimli bir süreç gerektirir. \ GRIN göndermek ve almak için GRIN proje web sayfasındaki talimatları takip ettiğinizden emin olun \ (alıcı çevrimiçi olmalı veya en azından belirli bir zaman diliminde çevrimiçi olmalıdır). \n\n\ Haveno sadece Grinbox (Wallet713) cüzdan URL formatını destekler. \n\n\ GRIN göndericisi, GRIN'i başarıyla gönderdiklerine dair kanıt sağlamakla yükümlüdür. Eğer cüzdan bu kanıtı sağlayamıyorsa, \ olası bir anlaşmazlık GRIN alıcısı lehine çözülecektir. \ Lütfen işlem kanıtını destekleyen en son Grinbox yazılımını kullandığınızdan ve GRIN transferi ve alım sürecini nasıl \ gerçekleştireceğinizi ve kanıt oluşturma sürecini anladığınızdan emin olun. \n\n\ Daha fazla bilgi için https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only \ adresindeki Grinbox kanıt aracı belgelerine bakın. # suppress inspection "UnusedProperty" account.crypto.popup.beam.msg=BEAM, işlemi oluşturmak için gönderen ve alıcı arasında etkileşimli \ bir süreç gerektirir. \n\n\ BEAM göndermek ve almak için BEAM proje web sayfasındaki talimatları takip ettiğinizden emin olun \ (alıcı çevrimiçi olmalı veya en azından belirli bir zaman diliminde çevrimiçi olmalıdır). \n\n\ BEAM göndericisi, BEAM'i başarıyla gönderdiklerine dair kanıt sağlamakla yükümlüdür. \ Böyle bir kanıt üretebilen cüzdan yazılımını kullandığınızdan emin olun. Cüzdan kanıt sağlayamıyorsa, olası \ bir anlaşmazlık BEAM alıcısı lehine çözülecektir. # suppress inspection "UnusedProperty" account.crypto.popup.pars.msg=Haveno'da ParsiCoin ticareti yapmak, aşağıdaki gereksinimleri \ anladığınızı ve yerine getirdiğinizi kabul etmenizi gerektirir:\n\n\ PARS göndermek için resmi ParsiCoin Cüzdanı sürüm 3.0.0 veya daha yüksek bir sürüm kullanmalısınız. \n\n\ İşlem Hash'inizi ve İşlem Anahtarınızı GUI Cüzdanınızdaki (ParsiPay) İşlemler bölümünden kontrol edebilirsiniz. \ İşlem üzerine sağ tıklayıp detayları göster'e tıklamanız gerekmektedir. \n\n\ Bir anlaşmazlık durumunda, arabulucuya veya hakeme şu bilgileri sağlamanız gerekecektir: 1) İşlem Hash'i, \ 2) İşlem Anahtarı ve 3) alıcının PARS adresi. Arabulucu veya hakem, ParsiCoin transferini ParsiCoin Blok Kaşifi'ni \ (http://explorer.parsicoin.net/#check_payment) kullanarak doğrulayacaktır.\n\n\ Gerekli bilgileri arabulucuya veya hakeme sağlayamamak, anlaşmazlık durumunu kaybetmenize neden olacaktır. Anlaşmazlık durumlarında, \ ParsiCoin göndericisi, işlemleri arabulucuya veya hakeme doğrulama sorumluluğunun %100'ünü taşır. \n\n\ Bu gereksinimleri anlamıyorsanız, Haveno'da ticaret yapmayın. İlk olarak, ParsiCoin Discord'da yardım arayın (https://discord.gg/c7qmFNh). # suppress inspection "UnusedProperty" account.crypto.popup.blk-burnt.msg=Yakılmış blackcoin'leri ticaret yapmak için, aşağıdaki bilgileri bilmeniz gerekmektedir:\n\n\ Yakılmış blackcoin'ler harcanamaz. Haveno'da bunları ticaret yapmak için, çıktı betikleri şu formda olmalıdır: \ OP_RETURN OP_PUSHDATA, ardından hex kodlaması yapıldıktan sonra adresleri oluşturan ilişkili veri baytları.\n\ Örneğin, adresi 666f6f (“foo” UTF-8'de) olan yakılmış blackcoin'ler şu betiğe sahip olacaktır:\n\n\ OP_RETURN OP_PUSHDATA 666f6f\n\n\ Yakılmış blackcoin'ler oluşturmak için, bazı cüzdanlarda bulunan “burn” RPC komutunu kullanabilirsiniz.\n\n\ Olası kullanım durumları için, https://ibo.laboratorium.ee adresine bakabilirsiniz.\n\n\ Yakılmış blackcoin'ler harcanamaz olduğundan, yeniden satılamazlar. “Yakılmış” blackcoin'ler satmak, \ normal blackcoin'leri (ilişkili veriler hedef adresle eşit olacak şekilde) yakmak anlamına gelir.\n\n\ Bir anlaşmazlık durumunda, BLK satıcısının işlem hash'ini sağlaması gerekmektedir. # suppress inspection "UnusedProperty" account.crypto.popup.liquidmonero.msg=Haveno'da L-XMR ticareti yapmak, aşağıdakileri anlamanızı gerektirir:\n\n\ Haveno'da bir ticaret için L-XMR alırken, mobil Blockstream Green Wallet uygulamasını veya \ bir saklama/cüzdan borsası cüzdanını kullanamazsınız. L-XMR'yi yalnızca Liquid Elements Core cüzdanına veya \ kör L-XMR adresiniz için körleme anahtarını elde etmenize izin veren başka bir L-XMR cüzdanına almanız gerekmektedir.\n\n\ Arabuluculuk gerekli olursa veya bir ticaret anlaşmazlığı ortaya çıkarsa, arabulucuya veya geri ödeme \ ajanına alıcı L-XMR adresinizin körleme anahtarını açıklamanız gerekecektir, böylece \ kendi Elements Core full node'larında Gizli İşleminizin ayrıntılarını doğrulayabilirler.\n\n\ Gerekli bilgileri arabulucuya veya geri ödeme ajana sağlayamamak, anlaşmazlık durumunu kaybetmenize \ neden olacaktır. Tüm anlaşmazlık durumlarında, L-XMR alıcısı, arabulucuya veya geri ödeme \ ajana kriptografik kanıt sağlama sorumluluğunun %100'ünü taşır.\n\n\ Bu gereksinimleri anlamıyorsanız, Haveno'da L-XMR ticareti yapmayın. account.traditional.yourTraditionalAccounts=Ulusal para birimi hesaplarınız account.backup.title=Cüzdan yedeği account.backup.location=Yedekleme konumu account.backup.selectLocation=Yedekleme konumunu seçin account.backup.backupNow=Şimdi yedekleyin (yedekleme şifrelenmemiştir!) account.backup.appDir=Uygulama veri dizini account.backup.openDirectory=Dizini aç account.backup.openLogFile=Günlük dosyasını aç account.backup.success=Yedekleme başarıyla kaydedildi:\n{0} account.backup.directoryNotAccessible=Seçtiğiniz dizin erişilebilir değil. {0} account.password.removePw.button=Şifreyi kaldır account.password.removePw.headline=Cüzdan şifre korumasını kaldır account.password.setPw.button=Şifre ayarla account.password.setPw.headline=Cüzdan için şifre koruması ayarla account.password.info=Şifre koruması etkinleştirildiğinde, uygulama başlatıldığında, cüzdanınızdan monero çekilirken ve anahtar kelimelerinizi görüntülerken şifrenizi girmeniz gerekecek. account.seed.backup.title=Cüzdan anahtar kelimelerinizi yedekleyin account.seed.info=Lütfen hem cüzdan anahtar kelimelerini hem de tarihi not edin. Anahtar kelimeler ve tarih ile cüzdanınızı istediğiniz zaman kurtarabilirsiniz.\n\nAnahtar kelimeleri bir kağıt parçasına yazmalısınız. Bilgisayarda kaydetmeyin.\n\nLütfen anahtar kelimelerin bir yedekleme yerine geçmediğini unutmayın.\nUygulama durumunu ve verilerini kurtarmak için "Hesap/Yedekleme" ekranından tüm uygulama dizininin bir yedeğini oluşturmanız gerekmektedir. account.seed.backup.warning=Lütfen anahtar kelimelerin bir yedekleme yerine geçmediğini unutmayın.\nUygulama durumunu ve verilerini kurtarmak için "Hesap/Yedekleme" ekranından tüm uygulama dizininin bir yedeğini oluşturmanız gerekmektedir. account.seed.warn.noPw.msg=Cüzdan anahtar kelimelerinin görüntülenmesini koruyacak bir şifre ayarlamadınız.\n\n\ Anahtar kelimeleri görüntülemek istiyor musunuz? account.seed.warn.noPw.yes=Evet, ve bir daha sorma account.seed.enterPw=Anahtar kelimeleri görüntülemek için şifreyi girin account.seed.restore.info=Anahtar kelimelerden geri yükleme uygulamadan önce bir yedekleme yapmanız gerekmektedir. Cüzdan geri yüklemenin \ yalnızca acil durumlar için olduğunu ve iç cüzdan veritabanında sorunlara neden olabileceğini unutmayın.\n\ Bir yedekleme uygulamak için bir yol değildir! Önceki uygulama durumunu geri yüklemek için uygulama \ veri dizininden bir yedekleme kullanın.\n\n\ Geri yüklemeden sonra uygulama otomatik olarak kapanacaktır. Uygulamayı yeniden başlattıktan sonra Monero ağı ile yeniden \ senkronize olacaktır. Bu biraz zaman alabilir ve özellikle eski bir cüzdansa ve birçok işlem varsa çok fazla \ CPU tüketebilir. Bu süreci kesintiye uğratmaktan kaçının, aksi takdirde SPV zincir dosyasını \ tekrar silmeniz veya geri yükleme sürecini tekrarlamanız gerekebilir. account.seed.restore.ok=Tamam, geri yükle ve Haveno'yu kapat account.keys.clipboard.warning=Cüzdan özel anahtarlarının son derece hassas finansal veriler olduğunu unutmayın.\n\n\ ● Özel anahtarlarınızı isteyen herhangi bir kişiye vermemelisiniz, paranızı kullanma konusunda tamamen güvenilir olmadıklarından emin değilseniz! \n\n\ ● Özel anahtar verilerini panoya kopyalamamalısınız, tamamen güvenli bir bilgisayar ortamı çalıştırdığınızdan emin olmadıkça, kötü amaçlı yazılım riskleri olmadığından emin olun. \n\n\ Pek çok kişi bu şekilde Monero'sunu kaybetti. Herhangi bir şüpheniz varsa, bu diyaloğu hemen kapatın ve bilgili birinden yardım isteyin. #################################################################### # Mobile notifications #################################################################### account.notifications.setup.title=Kurulum account.notifications.download.label=Mobil uygulamayı indir account.notifications.waitingForWebCam=Web kamerası bekleniyor... account.notifications.webCamWindow.headline=Telefondan QR kodu tarayın account.notifications.webcam.label=Web kamerası kullan account.notifications.webcam.button=QR kodu tara account.notifications.noWebcam.button=Web kameram yok account.notifications.erase.label=Telefondaki bildirimleri temizle account.notifications.erase.title=Bildirimleri temizle account.notifications.email.label=Eşleştirme kodu account.notifications.email.prompt=E-posta ile aldığınız eşleştirme kodunu girin account.notifications.settings.title=Ayarlar account.notifications.useSound.label=Telefonda bildirim sesi çal account.notifications.trade.label=Ticaret mesajlarını al account.notifications.market.label=Teklif uyarılarını al account.notifications.price.label=Fiyat uyarılarını al account.notifications.priceAlert.title=Fiyat uyarıları account.notifications.priceAlert.high.label=XMR fiyatı üstündeyse bildir account.notifications.priceAlert.low.label=XMR fiyatı altındaysa bildir account.notifications.priceAlert.setButton=Fiyat uyarısını ayarla account.notifications.priceAlert.removeButton=Fiyat uyarısını kaldır account.notifications.trade.message.title=Ticaret durumu değişti account.notifications.trade.message.msg.conf=ID {0} olan ticaretin depozito işlemi onaylandı. \ Lütfen Haveno uygulamanızı açın ve ödemeyi başlatın. account.notifications.trade.message.msg.started=ID {0} olan ticaret için XMR alıcısı ödemeyi başlattı. account.notifications.trade.message.msg.completed=ID {0} olan ticaret tamamlandı. account.notifications.offer.message.title=Teklifiniz alındı account.notifications.offer.message.msg=ID {0} olan teklifiniz alındı account.notifications.dispute.message.title=Yeni uyuşmazlık mesajı account.notifications.dispute.message.msg=ID {0} olan ticaret için bir uyuşmazlık mesajı aldınız account.notifications.marketAlert.title=Teklif uyarıları account.notifications.marketAlert.selectPaymentAccount=Ödeme hesabına uyan teklifler account.notifications.marketAlert.offerType.label=İlgilendiğim teklif türü account.notifications.marketAlert.offerType.buy=Alım teklifleri (XMR satmak istiyorum) account.notifications.marketAlert.offerType.sell=Satış teklifleri (XMR almak istiyorum) account.notifications.marketAlert.trigger=Teklif fiyat mesafesi (%) account.notifications.marketAlert.trigger.info=Bir fiyat mesafesi ayarlandığında, yalnızca gereksinimlerinizi karşılayan \ (veya aşan) bir teklif yayınlandığında uyarı alırsınız. Örnek: XMR satmak istiyorsunuz, ancak yalnızca mevcut \ piyasa fiyatına %2 primle satacaksınız. Bu alanı %2 olarak ayarlamak, yalnızca fiyatları mevcut piyasa fiyatının \ %2 (veya daha fazla) üzerinde olan teklifler için uyarı almanızı sağlar. account.notifications.marketAlert.trigger.prompt=Piyasa fiyatından yüzdelik mesafe (örneğin %2.50, -%0.50, vb.) account.notifications.marketAlert.addButton=Teklif uyarısı ekle account.notifications.marketAlert.manageAlertsButton=Teklif uyarılarını yönet account.notifications.marketAlert.manageAlerts.title=Teklif uyarılarını yönet account.notifications.marketAlert.manageAlerts.header.paymentAccount=Ödeme hesabı account.notifications.marketAlert.manageAlerts.header.trigger=Tetikleyici fiyat account.notifications.marketAlert.manageAlerts.header.offerType=Teklif türü account.notifications.marketAlert.message.title=Teklif uyarısı account.notifications.marketAlert.message.msg.below=altında account.notifications.marketAlert.message.msg.above=üstünde account.notifications.marketAlert.message.msg=Haveno teklif defterine {2} ({3} {4} piyasa fiyatı) fiyatıyla \ ve ödeme yöntemi '{5}' olan yeni bir '{0} {1}' teklifi yayınlandı.\n\ Teklif ID: {6}. account.notifications.priceAlert.message.title={0} için fiyat uyarısı account.notifications.priceAlert.message.msg=Fiyat uyarınız tetiklendi. Mevcut {0} fiyatı {1} {2} account.notifications.noWebCamFound.warning=Web kamerası bulunamadı.\n\n\ Lütfen eşleştirme kodunu ve şifreleme anahtarını mobil telefonunuzdan Haveno uygulamasına göndermek için e-posta seçeneğini kullanın. account.notifications.priceAlert.warning.highPriceTooLow=Yüksek fiyat, düşük fiyattan büyük olmalıdır. account.notifications.priceAlert.warning.lowerPriceTooHigh=Düşük fiyat, yüksek fiyattan düşük olmalıdır. #################################################################### # Windows #################################################################### inputControlWindow.headline=İşlem için girdileri seçin inputControlWindow.balanceLabel=Bakiye contractWindow.title=Uyuşmazlık detayları contractWindow.dates=Teklif tarihi / Ticaret tarihi contractWindow.xmrAddresses=Monero adresi XMR alıcı / XMR satıcı contractWindow.onions=Ağ adresi XMR alıcı / XMR satıcı contractWindow.accountAge=Hesap yaşı XMR alıcı / XMR satıcı contractWindow.numDisputes=Uyuşmazlık sayısı XMR alıcı / XMR satıcı contractWindow.contractHash=Sözleşme hash displayAlertMessageWindow.headline=Önemli bilgi! displayAlertMessageWindow.update.headline=Önemli güncelleme bilgisi! displayAlertMessageWindow.update.download=İndir: displayUpdateDownloadWindow.downloadedFiles=Dosyalar: displayUpdateDownloadWindow.downloadingFile=İndiriliyor: {0} displayUpdateDownloadWindow.verifiedSigs=Anahtarlarla doğrulanan imza: displayUpdateDownloadWindow.status.downloading=Dosyalar indiriliyor... displayUpdateDownloadWindow.status.verifying=İmza doğrulanıyor... displayUpdateDownloadWindow.button.label=Yükleyiciyi indir ve imzayı doğrula displayUpdateDownloadWindow.button.downloadLater=Daha sonra indir displayUpdateDownloadWindow.button.ignoreDownload=Bu sürümü yok say displayUpdateDownloadWindow.headline=Yeni bir Haveno güncellemesi mevcut! displayUpdateDownloadWindow.download.failed.headline=İndirme başarısız displayUpdateDownloadWindow.download.failed=İndirme başarısız oldu.\n\ Lütfen manuel olarak indirip doğrulayın: [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.installer.failed=Doğru yükleyici belirlenemedi. Lütfen manuel olarak indirip doğrulayın: \ [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.verify.failed=Doğrulama başarısız oldu.\n\ Lütfen manuel olarak indirip doğrulayın: [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.success=Yeni sürüm başarıyla indirildi ve imza doğrulandı.\n\n\ Lütfen indirme dizinini açın, uygulamayı kapatın ve yeni sürümü yükleyin. displayUpdateDownloadWindow.download.openDir=İndirme dizinini aç disputeSummaryWindow.title=Özet disputeSummaryWindow.openDate=Ticket açılma tarihi disputeSummaryWindow.role=Açan kişinin rolü disputeSummaryWindow.payout=Ticaret miktarı ödemesi disputeSummaryWindow.payout.getsTradeAmount=XMR {0} ticaret miktarı ödemesi alır disputeSummaryWindow.payout.getsAll=Maks. ödeme XMR {0} disputeSummaryWindow.payout.custom=Özel ödeme disputeSummaryWindow.payoutAmount.buyer=Alıcının ödeme miktarı disputeSummaryWindow.payoutAmount.seller=Satıcının ödeme miktarı disputeSummaryWindow.payoutAmount.invert=Kaybedeni yayıncı olarak kullan disputeSummaryWindow.reason=Uyuşmazlık nedeni disputeSummaryWindow.tradePeriodEnd=Ticaret dönemi sonu disputeSummaryWindow.extraInfo=Ek bilgi disputeSummaryWindow.delayedPayoutStatus=Gecikmeli Ödeme Durumu # IntelliJ tarafından tanınmayan dinamik değerler # inspection "UnusedProperty" uyarısını bastır disputeSummaryWindow.reason.BUG=Hata # inspection "UnusedProperty" uyarısını bastır disputeSummaryWindow.reason.USABILITY=Kullanılabilirlik # inspection "UnusedProperty" uyarısını bastır disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Protokol ihlali # inspection "UnusedProperty" uyarısını bastır disputeSummaryWindow.reason.NO_REPLY=Cevap yok # inspection "UnusedProperty" uyarısını bastır disputeSummaryWindow.reason.SCAM=Dolandırıcılık # inspection "UnusedProperty" uyarısını bastır disputeSummaryWindow.reason.OTHER=Diğer # inspection "UnusedProperty" uyarısını bastır disputeSummaryWindow.reason.BANK_PROBLEMS=Banka sorunları # inspection "UnusedProperty" uyarısını bastır disputeSummaryWindow.reason.OPTION_TRADE=Opsiyon ticareti # inspection "UnusedProperty" uyarısını bastır disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Satıcı yanıt vermiyor # inspection "UnusedProperty" uyarısını bastır disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Yanlış gönderen hesabı # inspection "UnusedProperty" uyarısını bastır disputeSummaryWindow.reason.PEER_WAS_LATE=Karşı taraf geç kaldı # inspection "UnusedProperty" uyarısını bastır disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Ticaret zaten tamamlandı disputeSummaryWindow.summaryNotes=Özet notlar disputeSummaryWindow.addSummaryNotes=Özet notlar ekle disputeSummaryWindow.close.button=Bileti kapat # Satır sonu veya token sırasını değiştirmeyin, yapı imza doğrulama için kullanılıyor # inspection "TrailingSpacesInProperty" uyarısını bastır disputeSummaryWindow.close.msg=Bilet {0} tarihinde kapatıldı\n\ {1} düğüm adresi: {2}\n\n\ Özet:\n\ Ticaret ID: {3}\n\ Para birimi: {4}\n\ Uyuşmazlık nedeni: {5}\n\ Ticaret miktarı: {6}\n\ XMR alıcı için ödeme miktarı: {7}\n\ XMR satıcı için ödeme miktarı: {8}\n\n\ Özet notlar:\n{9}\n # Satır sonu veya token sırasını değiştirmeyin, yapı imza doğrulama için kullanılıyor disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3} disputeSummaryWindow.close.nextStepsForMediation=\nSonraki adımlar:\n\ Ticareti açın ve arabulucudan gelen öneriyi kabul edin veya reddedin disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nSonraki adımlar:\n\ Bir hakemle uyuşmazlık açıldı. Uyuşmazlığı çözmek için "Destek" sekmesinde hakemle sohbet edebilirsiniz. disputeSummaryWindow.close.closePeer=Alım-satım taraflarının biletini de kapatmanız gerekiyor! disputeSummaryWindow.close.txDetails.headline=Geri ödeme işlemini yayınla # inspection "TrailingSpacesInProperty" uyarısını bastır disputeSummaryWindow.close.txDetails.buyer=Alıcı {0} adresine {1} alır\n # inspection "TrailingSpacesInProperty" uyarısını bastır disputeSummaryWindow.close.txDetails.seller=Satıcı {0} adresine {1} alır\n disputeSummaryWindow.close.txDetails=Harcanıyor: {0}\n\ {1}{2}\ İşlem ücreti: {3}\n\n\ Bu işlemi yayınlamak istediğinizden emin misiniz? disputeSummaryWindow.close.noPayout.headline=Ödeme yapmadan kapat disputeSummaryWindow.close.noPayout.text=Herhangi bir ödeme yapmadan kapatmak istiyor musunuz? disputeSummaryWindow.close.alreadyPaid.headline=Ödeme zaten yapıldı disputeSummaryWindow.close.alreadyPaid.text=Bu uyuşmazlık için başka bir ödeme yapmak üzere istemciyi yeniden başlatın emptyWalletWindow.headline={0} acil durum cüzdan aracı emptyWalletWindow.info=Bunu yalnızca arayüzden fonlarınıza erişemiyorsanız acil durumlarda kullanın.\n\n\ Bu aracı kullanırken, tüm açık teklifler otomatik olarak kapatılacaktır.\n\n\ Bu aracı kullanmadan önce, veri dizininizi yedekleyin. \ Bunu "Hesap/Yedekleme" altında yapabilirsiniz.\n\n\ Lütfen sorunuzu bildirin ve GitHub'da veya Haveno forumunda bir hata raporu oluşturun ki sorunun ne olduğunu araştırabilelim. emptyWalletWindow.balance=Mevcut cüzdan bakiyeniz emptyWalletWindow.address=Hedef adresiniz emptyWalletWindow.button=Tüm fonları gönder emptyWalletWindow.openOffers.warn=Açık teklifleriniz var ve cüzdanı boşaltırsanız bu teklifler kaldırılacaktır.\nCüzdanınızı boşaltmak istediğinizden emin misiniz? emptyWalletWindow.openOffers.yes=Evet, eminim emptyWalletWindow.sent.success=Cüzdan bakiyeniz başarıyla transfer edildi. enterPrivKeyWindow.headline=Kayıt için özel anahtar girin filterWindow.headline=Filtre listesini düzenle filterWindow.offers=Filtrelenmiş teklifler (virgülle ayrılmış) filterWindow.onions=Alışverişten yasaklanan adresler (virgülle ayrılmış) filterWindow.bannedFromNetwork=Ağdan yasaklanan adresler (virgülle ayrılmış) filterWindow.accounts=Filtrelenmiş alışveriş hesabı verileri:\nFormat: virgülle ayrılmış [ödeme yöntemi id | veri alanı | değer] listesi filterWindow.bannedCurrencies=Filtrelenmiş para birimi kodları (virgülle ayrılmış) filterWindow.bannedPaymentMethods=Filtrelenmiş ödeme yöntemi kimlikleri (virgülle ayrılmış) filterWindow.bannedAccountWitnessSignerPubKeys=Filtrelenmiş hesap tanık imzalayıcı açık anahtarları (virgülle ayrılmış açık anahtarların hex kodları) filterWindow.bannedPrivilegedDevPubKeys=Filtrelenmiş ayrıcalıklı geliştirici açık anahtarları (virgülle ayrılmış açık anahtarların hex kodları) filterWindow.arbitrators=Filtrelenmiş hakemler (virgülle ayrılmış onion adresleri) filterWindow.mediators=Filtrelenmiş arabulucular (virgülle ayrılmış onion adresleri) filterWindow.refundAgents=Filtrelenmiş geri ödeme ajanları (virgülle ayrılmış onion adresleri) filterWindow.seedNode=Filtrelenmiş seed düğümleri (virgülle ayrılmış onion adresleri) filterWindow.priceRelayNode=Filtrelenmiş fiyat aktarma düğümleri (virgülle ayrılmış adresler) filterWindow.xmrNode=Filtrelenmiş Monero düğümleri (virgülle ayrılmış adresler + port) filterWindow.preventPublicXmrNetwork=Genel Monero ağının kullanımını engelle filterWindow.disableAutoConf=Otomatik onayı devre dışı bırak filterWindow.autoConfExplorers=Filtrelenmiş otomatik onay gezginleri (virgülle ayrılmış adresler) filterWindow.disableTradeBelowVersion=Alım satım için gereken minimum sürüm filterWindow.add=Filtre ekle filterWindow.remove=Filtreyi kaldır filterWindow.xmrFeeReceiverAddresses=XMR ücret alıcı adresleri filterWindow.disableApi=API'yi devre dışı bırak filterWindow.disableMempoolValidation=Mempool doğrulamasını devre dışı bırak offerDetailsWindow.minXmrAmount=Min. XMR miktarı offerDetailsWindow.min=(min. {0}) offerDetailsWindow.distance=(piyasa fiyatından uzaklık: {0}) offerDetailsWindow.myTradingAccount=Alışveriş hesabım offerDetailsWindow.bankId=Banka kimliği (ör. BIC veya SWIFT) offerDetailsWindow.countryBank=Yapıcı'nın banka ülkesi offerDetailsWindow.commitment=Taahhüt offerDetailsWindow.agree=Kabul ediyorum offerDetailsWindow.tac=Şartlar ve koşullar offerDetailsWindow.confirm.maker.buy=Onayla: {0} ile XMR satın almak için teklif oluştur offerDetailsWindow.confirm.maker.sell=Onayla: {0} karşılığında XMR satmak için teklif oluştur offerDetailsWindow.confirm.taker.buy=Onayla: {0} ile XMR satın alma teklifini kabul et offerDetailsWindow.confirm.taker.sell=Onayla: {0} karşılığında XMR satma teklifini kabul et offerDetailsWindow.creationDate=Oluşturma tarihi offerDetailsWindow.makersOnion=Yapıcı'nın onion adresi offerDetailsWindow.challenge=Teklif şifresi offerDetailsWindow.challenge.copy=Parolanızı eşinizle paylaşmak için kopyalayın qRCodeWindow.headline=QR Kodu qRCodeWindow.msg=Harici cüzdanınızdan Haveno cüzdanınızı finanse etmek için bu QR kodunu kullanın. qRCodeWindow.request=Ödeme talebi:\n{0} selectDepositTxWindow.headline=Uyuşmazlık için yatırma işlemini seçin selectDepositTxWindow.msg=Yatırma işlemi ticarette saklanmadı.\n\ Lütfen cüzdanınızdaki mevcut çoklu imza işlemlerinden birini seçin ve \ başarısız ticarette kullanılan yatırma işlemi olarak işaretleyin.\n\n\ Doğru işlemi, ticaret detayları penceresini açarak (listeden ticaret kimliğine tıklayın) \ ve ticaret ücreti ödeme işlem çıktısını, çoklu imza yatırma işlemini (adres 3 ile başlar) \ gördüğünüz sonraki işleme kadar takip ederek bulabilirsiniz. Bu işlem kimliği burada sunulan listede görünmelidir. \ Doğru işlemi bulduğunuzda, burada o işlemi seçin ve devam edin.\n\n\ Bu rahatsızlık için özür dileriz, ancak bu hata durumu çok nadiren gerçekleşmelidir \ ve gelecekte bunu çözmek için daha iyi yollar bulmaya çalışacağız. selectDepositTxWindow.select=Yatırma işlemini seçin sendAlertMessageWindow.headline=Global bildirim gönder sendAlertMessageWindow.alertMsg=Uyarı mesajı sendAlertMessageWindow.enterMsg=Mesajı girin sendAlertMessageWindow.isSoftwareUpdate=Yazılım güncelleme bildirimi sendAlertMessageWindow.isUpdate=Tam sürüm sendAlertMessageWindow.isPreRelease=Ön sürüm sendAlertMessageWindow.version=Yeni sürüm no. sendAlertMessageWindow.send=Bildirimi gönder sendAlertMessageWindow.remove=Bildirimi kaldır sendPrivateNotificationWindow.headline=Özel mesaj gönder sendPrivateNotificationWindow.privateNotification=Özel bildirim sendPrivateNotificationWindow.enterNotification=Bildirim girin sendPrivateNotificationWindow.send=Özel bildirimi gönder showWalletDataWindow.walletData=Cüzdan verileri showWalletDataWindow.includePrivKeys=Özel anahtarları dahil et setXMRTxKeyWindow.headline=XMR gönderimini kanıtla setXMRTxKeyWindow.note=Aşağıdaki işlem bilgisini eklemek, daha hızlı ticaret için otomatik onayı etkinleştirir. Daha fazlasını görün: https://haveno.exchange/wiki/Trading_Monero setXMRTxKeyWindow.txHash=İşlem Kimliği (isteğe bağlı) setXMRTxKeyWindow.txKey=İşlem anahtarı (isteğe bağlı) # Yasal nedenlerden dolayı tac'ı çevirmiyoruz. Her dilde avukatlar tarafından kontrol edilmiş çevirilere ihtiyacımız var # Ve bu şu anda çok maliyetli. tacWindow.headline=Kullanıcı sözleşmesi tacWindow.agree=Kabul ediyorum tacWindow.disagree=Kabul etmiyorum ve çıkıyorum tacWindow.arbitrationSystem=Uyuşmazlık çözümü tradeDetailsWindow.headline=Ticaret tradeDetailsWindow.disputedPayoutTxId=Uyuşmazlık konusu olan ödeme işlem kimliği tradeDetailsWindow.tradeDate=Ticaret tarihi tradeDetailsWindow.txFee=Madencilik ücreti tradeDetailsWindow.tradePeersOnion=Ticaret ortaklarının onion adresi tradeDetailsWindow.tradePeersPubKeyHash=Ticaret ortaklarının açık anahtar hash'i tradeDetailsWindow.tradeState=Ticaret durumu tradeDetailsWindow.tradePhase=Ticaret aşaması tradeDetailsWindow.agentAddresses=Hakem/Arabulucu tradeDetailsWindow.detailData=Detay verileri txDetailsWindow.headline=İşlem Detayları txDetailsWindow.xmr.noteSent=XMR gönderdiniz. txDetailsWindow.xmr.noteReceived=XMR aldınız. txDetailsWindow.sentTo=Gönderilen adres txDetailsWindow.receivedWith=Alındı ile txDetailsWindow.txId=İşlem Kimliği (TxId) closedTradesSummaryWindow.headline=Ticaret geçmişi özeti closedTradesSummaryWindow.totalAmount.title=Toplam ticaret miktarı closedTradesSummaryWindow.totalAmount.value={0} (mevcut piyasa fiyatıyla {1}) closedTradesSummaryWindow.totalVolume.title={0} içinde toplam ticaret hacmi closedTradesSummaryWindow.totalMinerFee.title=Tüm madenci ücretlerinin toplamı closedTradesSummaryWindow.totalMinerFee.value={0} (toplam ticaret miktarının {1}'i) closedTradesSummaryWindow.totalTradeFeeInXmr.title=XMR olarak ödenen tüm ticaret ücretlerinin toplamı closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} (toplam ticaret miktarının {1}'i) walletPasswordWindow.headline=Kilidi açmak için şifre girin xmrConnectionError.headline=Monero bağlantı hatası xmrConnectionError.providedNodes=Sağlanan Monero düğüm(ler)ine bağlanırken hata oluştu.\n\nMevcut en iyi Monero düğümüne bağlanmak ister misiniz? xmrConnectionError.customNodes=Özel Monero düğüm(ler)inize bağlanırken hata oluştu.\n\nMevcut en iyi Monero düğümüne bağlanmak ister misiniz? xmrConnectionError.localNode=Haveno daha önce yerel bir Monero düğümüne bağlıydı, ancak artık erişilemiyor.\n\nYerel düğümünüzün çalıştığından ve tamamen senkronize edildiğinden emin olun veya devam etmek için başka bir seçenek belirleyin. xmrConnectionError.localNode.start=Yerel düğümü başlat xmrConnectionError.localNode.start.error=Yerel düğüm başlatılırken hata oluştu xmrConnectionError.localNode.fallback=Mevcut en iyi düğüme bağlan torNetworkSettingWindow.header=Tor ağı ayarları torNetworkSettingWindow.noBridges=Köprüleri kullanma torNetworkSettingWindow.providedBridges=Sağlanan köprülerle bağlan torNetworkSettingWindow.customBridges=Özel köprüleri girin torNetworkSettingWindow.transportType=Taşıma türü torNetworkSettingWindow.obfs3=obfs3 torNetworkSettingWindow.obfs4=obfs4 (önerilen) torNetworkSettingWindow.meekAmazon=meek-amazon torNetworkSettingWindow.meekAzure=meek-azure torNetworkSettingWindow.enterBridge=Bir veya daha fazla köprü rölesi girin (satır başına bir tane) torNetworkSettingWindow.enterBridgePrompt=adres:port yazın torNetworkSettingWindow.restartInfo=Değişikliklerin uygulanması için yeniden başlatmanız gerekiyor torNetworkSettingWindow.openTorWebPage=Tor projesi web sayfasını aç torNetworkSettingWindow.deleteFiles.header=Bağlantı sorunları mı? torNetworkSettingWindow.deleteFiles.info=Başlangıçta tekrarlanan bağlantı sorunları yaşıyorsanız, eski Tor dosyalarını silmek yardımcı olabilir. Bunu yapmak için aşağıdaki düğmeye tıklayın ve ardından yeniden başlatın. torNetworkSettingWindow.deleteFiles.button=Eski Tor dosyalarını sil ve kapat torNetworkSettingWindow.deleteFiles.progress=Tor kapatma işlemi devam ediyor torNetworkSettingWindow.deleteFiles.success=Eski Tor dosyaları başarıyla silindi. Lütfen yeniden başlatın. torNetworkSettingWindow.bridges.header=Tor engellendi mi? torNetworkSettingWindow.bridges.info=İnternet sağlayıcınız veya ülkeniz tarafından Tor engelleniyorsa, Tor köprülerini kullanmayı deneyebilirsiniz.\n\ Köprüler ve takılabilir taşıma hakkında daha fazla bilgi edinmek için: https://bridges.torproject.org web sayfasını ziyaret edin. feeOptionWindow.useXMR=XMR kullan feeOptionWindow.fee={0} (≈ {1}) feeOptionWindow.xmrFeeWithFiatAndPercentage={0} (≈ {1} / {2}) feeOptionWindow.xmrFeeWithPercentage={0} ({1}) #################################################################### # Popups #################################################################### popup.headline.notification=Bildirim popup.headline.instruction=Lütfen dikkat: popup.headline.attention=Dikkat popup.headline.backgroundInfo=Arka plan bilgisi popup.headline.feedback=Tamamlandı popup.headline.confirmation=Onay popup.headline.information=Bilgi popup.headline.warning=Uyarı popup.headline.error=Hata popup.doNotShowAgain=Tekrar gösterme popup.reportError.log=Günlük dosyasını aç popup.reportError.gitHub=GitHub hata izleyicisine bildir popup.reportError={0}\n\nYazılımı geliştirmemize yardımcı olmak için lütfen bu hatayı https://github.com/haveno-dex/haveno/issues adresinde yeni bir sorun açarak bildirin.\n\ Yukarıdaki hata mesajı, aşağıdaki düğmelerden birine tıkladığınızda panoya kopyalanacaktır.\n\ Hata ayıklamayı kolaylaştırmak için \"Günlük dosyasını aç\" düğmesine basarak günlük dosyasını kaydedip hata raporunuza eklemeniz yararlı olacaktır. popup.error.tryRestart=Lütfen uygulamanızı yeniden başlatmayı ve ağ bağlantınızı kontrol etmeyi deneyin. popup.error.takeOfferRequestFailed=Teklif alınırken bir hata oluştu:\n{0} error.spvFileCorrupted=SPV zincir dosyası okunurken bir hata oluştu.\nSPV zincir dosyası bozulmuş olabilir.\n\nHata mesajı: {0}\n\nSilip yeniden senkronize etmek istiyor musunuz? error.deleteAddressEntryListFailed=AddressEntryList dosyası silinemedi.\nHata: {0} error.closedTradeWithUnconfirmedDepositTx=Ticaret ID'si {0} olan kapalı ticaretin yatırma işlemi \ hala onaylanmadı.\n\n\ İşlemin geçerli olup olmadığını görmek için \"Ayarlar/Ağ bilgisi\" bölümünde bir SPV yeniden senkronizasyonu yapın. error.closedTradeWithNoDepositTx=Ticaret ID'si {0} olan kapalı ticaretin yatırma işlemi null.\n\n\ Kapalı ticaret listesini temizlemek için uygulamayı yeniden başlatın. popup.warning.walletNotInitialized=Cüzdan henüz başlatılmadı popup.warning.wrongVersion=Muhtemelen bu bilgisayar için yanlış Haveno sürümünü kullanıyorsunuz.\n\ Bilgisayarınızın mimarisi: {0}.\n\ Yüklediğiniz Haveno ikilisi: {1}.\n\ Lütfen kapatın ve doğru sürümü ({2}) yeniden yükleyin. popup.warning.incompatibleDB=Uyumsuz veri tabanı dosyaları tespit ettik!\n\n\ Bu veri tabanı dosyaları mevcut kod tabanımızla uyumlu değil:\n{0}\n\n\ Bozuk dosyanın yedeğini aldık ve yeni bir veri tabanı sürümüne varsayılan değerleri uyguladık.\n\n\ Yedek şu konumda bulunuyor:\n\ {1}/db/backup_of_corrupted_data.\n\n\ En son Haveno sürümünün yüklü olup olmadığını kontrol edin.\n\ Bunu şu adresten indirebilirsiniz: [HYPERLINK:https://haveno.exchange/downloads].\n\n\ Lütfen uygulamayı yeniden başlatın. popup.warning.startupFailed.twoInstances=Haveno zaten çalışıyor. İki Haveno örneği çalıştıramazsınız. popup.warning.tradePeriod.halfReached=Ticaret ID'si {0} olan ticaretiniz, izin verilen maksimum ticaret süresinin yarısına ulaştı ve hala tamamlanmadı.\n\nTicaret süresi {1} tarihinde sona eriyor\n\nDaha fazla bilgi için \"Portföy/Açık ticaretler\" bölümünde ticaret durumunuzu kontrol edin. popup.warning.tradePeriod.ended=Ticaret ID'si {0} olan ticaretiniz, izin verilen maksimum ticaret süresine ulaştı ve tamamlanmadı.\n\n\ Ticaret süresi {1} tarihinde sona erdi\n\n\ Hakemle iletişime geçmek için \"Portföy/Açık ticaretler\" bölümünde ticaretinizi kontrol edin. popup.warning.noTradingAccountSetup.headline=Bir ticaret hesabı kurmadınız popup.warning.noTradingAccountSetup.msg=Bir teklif oluşturmadan önce bir ulusal para birimi veya kripto para hesabı kurmanız gerekmektedir.\nHesap kurmak istiyor musunuz? popup.warning.noArbitratorsAvailable=Mevcut hakem yok. popup.warning.noMediatorsAvailable=Mevcut arabulucu yok. popup.warning.notFullyConnected=Ağa tamamen bağlanana kadar beklemeniz gerekiyor.\nBaşlangıçta yaklaşık 2 dakika sürebilir. popup.warning.notSufficientConnectionsToXmrNetwork=Monero ağına en az {0} bağlantınız olana kadar beklemeniz gerekmektedir. popup.warning.downloadNotComplete=Eksik Monero bloklarının indirilmesi tamamlanana kadar beklemeniz gerekmektedir. popup.warning.walletNotSynced=Haveno cüzdanı en son blok zinciri yüksekliği ile senkronize değil. Lütfen cüzdanın senkronize olmasını bekleyin veya bağlantınızı kontrol edin. popup.warning.removeOffer=Bu teklifi kaldırmak istediğinizden emin misiniz? popup.warning.tooLargePercentageValue=%100 veya daha büyük bir yüzde değeri belirleyemezsiniz. popup.warning.examplePercentageValue=Lütfen \"5.4\" gibi bir yüzde sayısı girin popup.warning.noPriceFeedAvailable=Bu para birimi için fiyat beslemesi yok. Yüzde tabanlı fiyat kullanamazsınız.\nLütfen sabit fiyatı seçin. popup.warning.sendMsgFailed=Ticaret ortağınıza mesaj gönderme başarısız oldu.\nLütfen tekrar deneyin ve başarısız olmaya devam ederse bir hata bildirin. popup.warning.messageTooLong=Mesajınız izin verilen maksimum boyutu aşıyor. Lütfen birkaç parça halinde gönderin veya https://pastebin.com gibi bir servise yükleyin. popup.warning.lockedUpFunds=Başarısız bir ticaretten kilitli fonlarınız var.\n\ Kilitli bakiye: {0} \n\ Yatırma tx adresi: {1}\n\ Ticaret ID'si: {2}.\n\n\ Lütfen açık ticaretler ekranında ticareti seçerek ve \"alt + o\" veya \"option + o\" tuşlarına basarak bir destek bileti açın. popup.warning.moneroConnection=Monero ağına bağlanırken bir sorun oluştu.\n\n{0} popup.warning.makerTxInvalid=Bu teklif geçerli değil. Lütfen farklı bir teklif seçin.\n\n takeOffer.cancelButton=Teklifi iptal et takeOffer.warningButton=Yine de yoksay ve devam et # suppress inspection "UnusedProperty" popup.warning.nodeBanned={0} düğümlerinden biri yasaklandı. # suppress inspection "UnusedProperty" popup.warning.priceRelay=fiyat rölesi popup.warning.seed=anahtar kelime popup.warning.mandatoryUpdate.trading=Lütfen en son Haveno sürümüne güncelleyin. \ Eski sürümler için ticareti devre dışı bırakan zorunlu bir güncelleme yayınlandı. \ Daha fazla bilgi için lütfen Haveno Forumunu kontrol edin. popup.warning.noFilter=Tohum düğümlerinden bir filtre nesnesi almadık. Lütfen ağ yöneticilerine bir filtre nesnesi kaydetmeleri için bilgi verin. popup.warning.burnXMR=Bu işlem mümkün değil, çünkü {0} tutarındaki madencilik ücretleri, transfer edilecek {1} tutarını aşacaktır. \ Lütfen madencilik ücretleri tekrar düşük olana kadar bekleyin veya transfer etmek için daha fazla XMR biriktirin. popup.warning.openOffer.makerFeeTxRejected=Teklif ID'si {0} olan teklif için Monero ağı tarafından yapıcı ücret işlemi reddedildi.\n\ İşlem ID'si={1}.\n\ Daha fazla sorun yaşamamak için teklif kaldırıldı.\n\ Lütfen \"Ayarlar/Ağ bilgisi\" bölümüne gidin ve bir SPV yeniden senkronizasyonu yapın.\n\ Daha fazla yardım için lütfen Haveno destek kanalına Haveno Keybase takımında başvurun. popup.warning.trade.txRejected.tradeFee=işlem ücreti popup.warning.trade.txRejected.deposit=yatırma popup.warning.trade.txRejected=Ticaret ID'si {1} olan ticaret için {0} işlemi Monero ağı tarafından reddedildi.\n\ İşlem ID'si={2}\n\ Ticaret başarısız ticaretlere taşındı.\n\ Lütfen \"Ayarlar/Ağ bilgisi\" bölümüne gidin ve bir SPV yeniden senkronizasyonu yapın.\n\ Daha fazla yardım için lütfen Haveno destek kanalına Haveno Keybase takımında başvurun. popup.warning.openOfferWithInvalidMakerFeeTx=ID {0} olan teklif için yapıcı ücret işlemi geçersiz.\n\ İşlem ID={1}.\n\ Lütfen "Ayarlar/Ağ bilgisi" bölümüne gidin ve bir SPV yeniden senkronizasyonu yapın.\n\ Daha fazla yardım için lütfen Haveno Keybase takımındaki Haveno destek kanalına başvurun. popup.info.cashDepositInfo=Nakit yatırma işlemini gerçekleştirebilmek için bölgenizde bir banka şubesinin olduğundan emin olun.\n\ Satıcının bankasının banka ID'si (BIC/SWIFT) şudur: {0}. popup.info.cashDepositInfo.confirm=Yatırma işlemini gerçekleştirebileceğimi onaylıyorum popup.info.shutDownWithOpenOffers=Haveno kapatılıyor, ancak açık teklifler var. \n\n\ Bu teklifler Haveno kapalıyken P2P ağında mevcut olmayacak, ancak Haveno'yu bir sonraki başlattığınızda tekrar P2P ağına yayınlanacaklar.\n\n\ Tekliflerinizi çevrimiçi tutmak için Haveno'yu çalışır durumda tutun ve bu bilgisayarın da çevrimiçi kalmasını sağlayın \ (yani, uyku moduna geçmemesini sağlayın... monitör bekleme modu sorun yaratmaz). popup.info.shutDownWithTradeInit={0}\n\ Bu ticaret henüz başlatmayı tamamlamadı; şimdi kapatmak muhtemelen onu bozacaktır. Lütfen bir dakika bekleyin ve tekrar deneyin. popup.info.shutDownWithDisputeInit=Haveno kapatılıyor, ancak bekleyen bir Uyuşmazlık sistemi mesajı var.\n\ Lütfen kapatmadan önce bir dakika bekleyin. popup.info.shutDownQuery=Haveno'dan çıkmak istediğinize emin misiniz? popup.info.qubesOSSetupInfo=Görünüşe göre Haveno'yu Qubes OS üzerinde çalıştırıyorsunuz. \n\n\ Lütfen Haveno küpünüzün Kurulum Kılavuzumuza göre ayarlandığından emin olun [HYPERLINK:https://haveno.exchange/wiki/Running_Haveno_on_Qubes]. popup.info.p2pStatusIndicator.red={0}\n\n\ Düğümünüz P2P ağına bağlı değil. Haveno bu durumda çalışamaz. popup.info.p2pStatusIndicator.yellow={0}\n\n\ Düğümünüzün Tor giriş bağlantısı yok. Haveno çalışacaktır, ancak bu durum birkaç saat boyunca devam ederse bağlantı sorunlarına işaret edebilir. popup.info.p2pStatusIndicator.green={0}\n\n\ İyi haber, P2P bağlantı durumunuz sağlıklı görünüyor! popup.info.firewallSetupInfo=Görünüşe göre bu makine gelen Tor bağlantılarını engelliyor. \ Bu, Qubes/VirtualBox/Whonix gibi VM ortamlarında olabilir. \n\n\ Ortamınızı gelen Tor bağlantılarını kabul edecek şekilde ayarlayın, aksi takdirde kimse tekliflerinizi alamaz. popup.warn.downGradePrevention=Sürüm {0} den sürüm {1} e düşürme desteklenmiyor. Lütfen en son Haveno sürümünü kullanın. popup.warn.daoRequiresRestart=DAO durumu senkronize edilirken bir sorun oluştu. Sorunu çözmek için uygulamayı yeniden başlatmalısınız. popup.privateNotification.headline=Önemli özel bildirim! popup.xmrLocalNode.msg=Haveno bu makinede (localhost'ta) çalışan bir Monero düğümü tespit etti.\n\n\ Düğümün tamamen senkronize olduğundan emin olun Haveno'yu başlatmadan önce. popup.shutDownInProgress.headline=Kapatma işlemi devam ediyor popup.shutDownInProgress.msg=Uygulamayı kapatmak birkaç saniye sürebilir.\nLütfen bu işlemi kesmeyin. popup.attention.forTradeWithId=ID'si {0} olan ticaret için bildirim popup.attention.welcome.stagenet=Haveno test sürümüne hoş geldiniz!\n\n\ Bu platform Haveno'nun protokolünü test etmenizi sağlar. Talimatları izlediğinizden emin olun [HYPERLINK:https://github.com/haveno-dex/haveno/blob/master/docs/installing.md].\n\n\ Herhangi bir sorunla karşılaşırsanız, lütfen bir sorun bildirerek bize bildirin [HYPERLINK:https://github.com/haveno-dex/haveno/issues/new].\n\n\ Bu bir test sürümüdür. Gerçek para kullanmayın! popup.attention.welcome.mainnet=Haveno'ya hoş geldiniz!\n\n\ Bu platform, Monero'yu itibari para birimleri veya diğer kripto para birimleriyle merkezi olmayan bir şekilde ticaret yapmanızı sağlar.\n\n\ Yeni bir ödeme hesabı oluşturarak ve ardından bir teklif yaparak veya teklif alarak başlayın.\n\n\ Herhangi bir sorunla karşılaşırsanız, lütfen bir sorun bildirerek bize bildirin [HYPERLINK:https://github.com/haveno-dex/haveno/issues/new]. popup.attention.welcome.mainnet.test=Haveno'ya hoş geldiniz!\n\n\ Bu platform, Monero'yu itibari para birimleri veya diğer kripto para birimleriyle merkezi olmayan bir şekilde ticaret yapmanızı sağlar.\n\n\ Yeni bir ödeme hesabı oluşturarak ve ardından bir teklif yaparak veya teklif alarak başlayın.\n\n\ Herhangi bir sorunla karşılaşırsanız, lütfen bir sorun bildirerek bize bildirin [HYPERLINK:https://github.com/haveno-dex/haveno/issues/new].\n\n\ Haveno kısa bir süre önce kamu testi için yayınlandı. Lütfen küçük miktarlar kullanın! popup.info.multiplePaymentAccounts.headline=Birden fazla ödeme hesabı mevcut popup.info.multiplePaymentAccounts.msg=Bu teklif için birden fazla ödeme hesabınız var. Lütfen doğru olanı seçtiğinizden emin olun. popup.accountSigning.selectAccounts.headline=Ödeme hesaplarını seçin popup.accountSigning.selectAccounts.description=Ödeme yöntemi ve zaman noktasına bağlı olarak, bir alıcıya ödeme yapılan bir uyuşmazlıkla bağlantılı tüm ödeme hesapları sizin için seçilecektir. popup.accountSigning.selectAccounts.signAll=Tüm ödeme yöntemlerini imzala popup.accountSigning.selectAccounts.datePicker=Hesapların imzalanacağı zamanı seçin popup.accountSigning.confirmSelectedAccounts.headline=Seçilen ödeme hesaplarını onaylayın popup.accountSigning.confirmSelectedAccounts.description=Girdiğiniz bilgilere göre, {0} ödeme hesabı seçilecektir. popup.accountSigning.confirmSelectedAccounts.button=Ödeme hesaplarını onayla popup.accountSigning.signAccounts.headline=Ödeme hesaplarının imzalanmasını onaylayın popup.accountSigning.signAccounts.description=Seçiminize göre, {0} ödeme hesabı imzalanacaktır. popup.accountSigning.signAccounts.button=Ödeme hesaplarını imzala popup.accountSigning.signAccounts.ECKey=Özel hakem anahtarını girin popup.accountSigning.signAccounts.ECKey.error=Geçersiz hakem ECKey popup.accountSigning.success.headline=Tebrikler popup.accountSigning.success.description=Tüm {0} ödeme hesabı başarıyla imzalandı! popup.accountSigning.generalInformation=Tüm hesaplarınızın imzalama durumunu hesap bölümünde bulabilirsiniz.\n\n\ Daha fazla bilgi için lütfen [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing] adresini ziyaret edin. popup.accountSigning.signedByArbitrator=Ödeme hesaplarınızdan biri bir hakem tarafından doğrulandı ve imzalandı. Bu hesapla ticaret yapmak, başarılı bir ticaretten sonra otomatik olarak ticaret ortağınızın hesabını imzalayacaktır.\n\n{0} popup.accountSigning.signedByPeer=Ödeme hesaplarınızdan biri bir ticaret ortağı tarafından doğrulandı ve imzalandı. Başlangıçtaki ticaret limitiniz kaldırılacak ve {0} gün içinde diğer hesapları imzalayabileceksiniz.\n\n{1} popup.accountSigning.peerLimitLifted=Hesaplarınızdan biri için başlangıç limiti kaldırıldı.\n\n{0} popup.accountSigning.peerSigner=Hesaplarınızdan biri diğer ödeme hesaplarını imzalayacak kadar olgun \ ve hesaplarınızdan biri için başlangıç limiti kaldırıldı.\n\n{0} popup.accountSigning.singleAccountSelect.headline=İmzalanmamış hesap yaş tanığını içe aktarın popup.accountSigning.confirmSingleAccount.headline=Seçilen hesap yaş tanığını onaylayın popup.accountSigning.confirmSingleAccount.selectedHash=Seçilen tanık hash popup.accountSigning.confirmSingleAccount.button=Hesap yaş tanığını imzala popup.accountSigning.successSingleAccount.description=Tanık {0} imzalandı popup.accountSigning.successSingleAccount.success.headline=Başarı popup.accountSigning.unsignedPubKeys.headline=İmzalanmamış Pubkey'ler popup.accountSigning.unsignedPubKeys.sign=Pubkey'leri imzala popup.accountSigning.unsignedPubKeys.signed=Pubkey'ler imzalandı popup.accountSigning.unsignedPubKeys.result.signed=İmzalanmış pubkey'ler popup.accountSigning.unsignedPubKeys.result.failed=İmzalama başarısız oldu popup.info.buyerAsTakerWithoutDeposit.headline=Alıcıdan depozito gerekmez popup.info.buyerAsTakerWithoutDeposit=Teklifiniz, XMR alıcısından güvenlik depozitosu veya ücret talep etmeyecektir.\n\nTeklifinizi kabul etmek için, ticaret ortağınızla Haveno dışında bir şifre paylaşmalısınız.\n\nŞifre otomatik olarak oluşturulur ve oluşturulduktan sonra teklif detaylarında görüntülenir. popup.info.torMigration.msg=Haveno düğümünüz muhtemelen eski bir Tor v2 adresi kullanıyor. \ Lütfen Haveno düğümünüzü bir Tor v3 adresine geçirin. \ Önceden veri dizininizi yedeklediğinizden emin olun. #################################################################### # Notifications #################################################################### notification.trade.headline=ID {0} olan ticaret için bildirim notification.ticket.headline=ID {0} olan ticaret için destek bileti notification.trade.completed=Ticaret şimdi tamamlandı ve fonlarınızı çekebilirsiniz. notification.trade.accepted=Teklifiniz bir XMR {0} tarafından kabul edildi. notification.trade.unlocked=Ticaretiniz onaylandı.\nÖdemeyi şimdi başlatabilirsiniz. notification.trade.paymentSent=XMR alıcısı ödemeyi gönderdi. notification.trade.selectTrade=Ticareti seçin notification.trade.peerOpenedDispute=Ticaret ortağınız bir {0} açtı. notification.trade.disputeClosed={0} kapatıldı. notification.walletUpdate.headline=Ticaret cüzdanı güncellemesi notification.walletUpdate.msg=Ticaret cüzdanınız yeterince fonlanmıştır.\nMiktar: {0} notification.takeOffer.walletUpdate.msg=Ticaret cüzdanınız önceki bir teklif alma girişiminden yeterince fonlanmıştı.\nMiktar: {0} notification.tradeCompleted.headline=Ticaret tamamlandı notification.tradeCompleted.msg=Fonlarınızı harici bir Monero cüzdanına çekebilir veya Haveno cüzdanınızda tutabilirsiniz. #################################################################### # System Tray #################################################################### systemTray.show=Uygulama penceresini göster systemTray.hide=Uygulama penceresini gizle systemTray.info=Haveno hakkında bilgi systemTray.exit=Çıkış systemTray.tooltip=Haveno: Merkezi olmayan bir Monero borsa ağı #################################################################### # GUI Util #################################################################### guiUtil.accountExport.savedToPath=Ticaret hesapları şu yola kaydedildi:\n{0} guiUtil.accountExport.noAccountSetup=Dışa aktarmak için ticaret hesaplarınız yok. guiUtil.accountExport.selectPath={0} için yolu seçin # suppress inspection "TrailingSpacesInProperty" guiUtil.accountExport.tradingAccount=Kimlik numarası {0} olan ticaret hesabı\n # suppress inspection "TrailingSpacesInProperty" guiUtil.accountImport.noImport=Kimlik numarası {0} olan ticaret hesabını içe aktarmadık çünkü zaten mevcut.\n guiUtil.accountExport.exportFailed=CSV'ye dışa aktarma bir hata nedeniyle başarısız oldu.\nHata = {0} guiUtil.accountExport.selectExportPath=Dışa aktarma yolunu seçin guiUtil.accountImport.imported=Ticaret hesabı şu yoldan içe aktarıldı:\n{0}\n\nİçe aktarılan hesaplar:\n{1} guiUtil.accountImport.noAccountsFound=Şu yolda dışa aktarılmış ticaret hesabı bulunamadı: {0}.\nDosya adı {1}. guiUtil.openWebBrowser.warning=\ Sistem web tarayıcınızda bir web sayfası açmak üzeresiniz.\n\ Web sayfasını şimdi açmak istiyor musunuz?\n\n\ Eğer varsayılan sistem web tarayıcınız olarak \"Tor Browser\" kullanmıyorsanız,\ web sayfasına açık ağda bağlanacaksınız.\n\n\ URL: \"{0}\" guiUtil.openWebBrowser.doOpen=Web sayfasını aç ve tekrar sorma guiUtil.openWebBrowser.copyUrl=URL'yi kopyala ve iptal et guiUtil.ofTradeAmount=işlem miktarının guiUtil.requiredMinimum=(gerekli minimum) #################################################################### # Component specific #################################################################### list.currency.select=Para birimini seçin list.currency.showAll=Hepsini göster list.currency.editList=Para birimi listesini düzenle table.placeholder.noItems=Şu anda mevcut {0} yok table.placeholder.noData=Şu anda mevcut veri yok table.placeholder.processingData=Veriler işleniyor... peerInfoIcon.tooltip.tradePeer=Ticaret ortağının peerInfoIcon.tooltip.maker=Oluşturanın peerInfoIcon.tooltip.trade.traded={0} onion adresi: {1}\nBu ortak ile {2} kez ticaret yaptınız\n{3} peerInfoIcon.tooltip.trade.notTraded={0} onion adresi: {1}\nBu ortak ile henüz ticaret yapmadınız.\n{2} peerInfoIcon.tooltip.age=Ödeme hesabı {0} önce oluşturuldu. peerInfoIcon.tooltip.unknownAge=Ödeme hesabının yaşı bilinmiyor. peerInfoIcon.tooltip.dispute={0}\nİhtilaf sayısı: {1}.\n{2} tooltip.openPopupForDetails=Ayrıntılar için açılır pencereyi aç tooltip.invalidTradeState.warning=Bu ticaret geçersiz bir durumda. Daha fazla bilgi için ayrıntılar penceresini açın tooltip.openBlockchainForAddress=Adresi dış blok zinciri gezgininde aç: {0} tooltip.openBlockchainForTx=İşlemi dış blok zinciri gezgininde aç: {0} confidence.unknown=Bilinmeyen işlem durumu confidence.seen={0} ortak tarafından görüldü / 0 onay confidence.confirmed={0} onay confidence.invalid=İşlem geçersiz peerInfo.title=Ortak bilgisi peerInfo.nrOfTrades=Tamamlanan ticaret sayısı peerInfo.notTradedYet=Bu kullanıcı ile henüz ticaret yapmadınız. peerInfo.setTag=Bu ortak için etiket ayarla peerInfo.age.noRisk=Ödeme hesabının yaşı peerInfo.age.chargeBackRisk=İmzadan beri geçen süre peerInfo.unknownAge=Yaş bilinmiyor addressTextField.openWallet=Varsayılan Monero cüzdanınızı açın addressTextField.copyToClipboard=Adresi panoya kopyala addressTextField.addressCopiedToClipboard=Adres panoya kopyalandı addressTextField.openWallet.failed=Varsayılan Monero cüzdan uygulamasını açma başarısız oldu. Belki yüklü değildir? explorerAddressTextField.copyToClipboard=Adresi panoya kopyala explorerAddressTextField.blockExplorerIcon.tooltip=Bu adres ile blok zinciri gezgini aç explorerAddressTextField.missingTx.warning.tooltip=Gerekli adres eksik peerInfoIcon.tooltip={0}\nEtiket: {1} txIdTextField.copyIcon.tooltip=İşlem kimliğini panoya kopyala txIdTextField.blockExplorerIcon.tooltip=Bu işlem kimliği ile blok zinciri gezgini aç txIdTextField.missingTx.warning.tooltip=Gerekli işlem eksik #################################################################### # Navigation #################################################################### navigation.account="Hesap" navigation.account.walletSeed="Hesap/Cüzdan anahtarı" navigation.funds.availableForWithdrawal="Fonlar/Fon gönder" navigation.portfolio.myOpenOffers="Portföy/Açık tekliflerim" navigation.portfolio.pending="Portföy/Açık ticaretler" navigation.portfolio.closedTrades="Portföy/Tarihçe" navigation.funds.depositFunds="Fonlar/Fon al" navigation.settings.preferences="Ayarlar/Tercihler" # suppress inspection "UnusedProperty" navigation.funds.transactions="Fonlar/İşlemler" navigation.support="Destek" #################################################################### # Formatter #################################################################### formatter.formatVolumeLabel={0} miktar{1} formatter.makerTaker=Yapan olarak {0} {1} / Alan olarak {2} {3} formatter.makerTaker.locked=Yapıcı olarak {0} {1} / Alan olarak {2} {3} 🔒 formatter.youAreAsMaker=Yapan sizsiniz: {1} {0} (maker) / Alan: {3} {2} formatter.youAreAsTaker=Alan sizsiniz: {1} {0} (taker) / Yapan: {3} {2} formatter.youAre=Şu anda sizsiniz {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Şu anda bir teklif oluşturuyorsunuz: {0} {1} formatter.youAreCreatingAnOffer.traditional.locked=Şu anda bir teklif oluşturuyorsunuz: {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Şu anda bir teklif oluşturuyorsunuz: {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.crypto.locked=Şu anda bir teklif oluşturuyorsunuz: {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} olarak formatter.asTaker={0} {1} olarak #################################################################### # Domain specific #################################################################### # we use enum values here # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" XMR_MAINNET=Monero Ana Ağı # suppress inspection "UnusedProperty" XMR_LOCAL=Monero Yerel Test Ağı # suppress inspection "UnusedProperty" XMR_STAGENET=Monero Stagenet time.year=Yıl time.month=Ay time.halfYear=Altı ay time.quarter=Çeyrek time.week=Hafta time.day=Gün time.hour=Saat time.minute10=10 Dakika time.hours=saat time.days=gün time.1hour=1 saat time.1day=1 gün time.minute=dakika time.second=saniye time.minutes=dakika time.seconds=saniye password.enterPassword=Şifre girin password.confirmPassword=Şifreyi doğrula password.tooLong=Şifre 500 karakterden az olmalıdır. password.deriveKey=Şifreden anahtar türet password.walletDecrypted=Cüzdan başarıyla şifresiz hale getirildi ve şifre koruması kaldırıldı. password.wrongPw=Yanlış şifre girdiniz.\n\nLütfen şifrenizi dikkatlice kontrol ederek tekrar girin, yazım hatalarına dikkat edin. password.walletEncrypted=Cüzdan başarıyla şifrelendi ve şifre koruması etkinleştirildi. password.passwordsDoNotMatch=Girdiğiniz iki şifre eşleşmiyor. password.forgotPassword=Şifrenizi mi unuttunuz? password.backupReminder=Şifre ayarlarken tüm otomatik olarak oluşturulan şifresiz cüzdan yedekleri silinecektir.\n\n\ Şifre ayarlamadan önce uygulama dizininin yedeğini almanız ve seed kelimelerinizi yazmanız şiddetle tavsiye edilir! password.setPassword=Şifre Ayarla (Yedeğimi zaten aldım) password.makeBackup=Yedek Al seed.seedWords=Cüzdan seed kelimeleri seed.enterSeedWords=Cüzdan seed kelimelerini girin seed.date=Cüzdan tarihi seed.restore.title=Seed kelimelerinden cüzdanları geri yükle seed.restore=Cüzdanları geri yükle seed.creationDate=Oluşturma tarihi seed.warn.walletNotEmpty.msg=Monero cüzdanınız boş değil.\n\n\ Daha eski bir cüzdanı geri yüklemeye çalışmadan önce bu cüzdanı boşaltmalısınız, \ çünkü cüzdanları karıştırmak yedeklerin geçersiz hale gelmesine yol açabilir.\n\n\ Lütfen ticaretlerinizi tamamlayın, tüm açık tekliflerinizi kapatın ve Monero'nuzu çekmek için Fonlar bölümüne gidin.\n\ Monero'nuza erişemiyorsanız cüzdanı boşaltmak için acil durum aracını kullanabilirsiniz.\n\ Acil durum aracını açmak için \"Alt+e\" veya \"Cmd/Ctrl+e\" tuşlarına basın. seed.warn.walletNotEmpty.restore=Yine de geri yüklemek istiyorum seed.warn.walletNotEmpty.emptyWallet=Önce cüzdanlarımı boşaltacağım seed.warn.notEncryptedAnymore=Cüzdanlarınız şifrelidir.\n\n\ Geri yüklemeden sonra cüzdanlar artık şifreli olmayacak ve yeni bir şifre ayarlamanız gerekecek.\n\n\ Devam etmek istiyor musunuz? seed.warn.walletDateEmpty=Cüzdan tarihi belirtmediğiniz için Haveno 2013.10.09 tarihinden itibaren blok zincirini taramak zorunda kalacak (BIP39 epoch tarihi).\n\n\ BIP39 cüzdanları Haveno'da ilk olarak 2017.06.28'de (v0.5 sürümü) tanıtıldı. Bu yüzden o tarihi kullanarak zaman kazanabilirsiniz.\n\n\ İdeal olarak, cüzdan seed'inizin oluşturulduğu tarihi belirtmelisiniz.\n\n\n\ Cüzdan tarihi belirtmeden devam etmek istediğinizden emin misiniz? seed.restore.success=Cüzdanlar yeni seed kelimeleriyle başarıyla geri yüklendi.\n\nUygulamayı kapatıp yeniden başlatmanız gerekiyor. seed.restore.error=Seed kelimeleriyle cüzdanları geri yüklerken bir hata oluştu.{0} seed.restore.openOffers.warn=Seed kelimelerinden geri yüklerseniz açık teklifleriniz kaldırılacaktır.\n\ Devam etmek istediğinizden emin misiniz? #################################################################### # Payment methods #################################################################### payment.account=Hesap payment.account.no=Hesap no. payment.account.name=Hesap adı payment.account.username=Kullanıcı adı payment.account.phoneNr=Telefon numarası payment.account.owner.fullname=Hesap sahibi tam adı payment.account.fullName=Tam ad (ad, orta ad, soyad) payment.account.state=Eyalet/İl/Bölge payment.account.city=Şehir payment.account.address=Adres payment.bank.country=Bankanın ülkesi payment.account.name.email=Hesap sahibi tam adı / email payment.account.name.emailAndHolderId=Hesap sahibi tam adı / email / {0} payment.bank.name=Banka adı payment.select.account=Hesap türünü seçin payment.select.region=Bölgeyi seçin payment.select.country=Ülkeyi seçin payment.select.bank.country=Bankanın ülkesini seçin payment.foreign.currency=Ülkenin varsayılan para birimi dışındaki bir para birimini seçmek istediğinize emin misiniz? payment.restore.default=Hayır, varsayılan para birimini geri yükle payment.email=Email payment.country=Ülke payment.extras=Ek gereksinimler payment.email.mobile=Email veya telefon numarası payment.email.mobile.cashtag=Cashtag, email veya telefon numarası payment.email.mobile.username=Kullanıcı adı, email veya telefon numarası payment.crypto.address=Kripto para adresi payment.crypto.tradeInstantCheckbox=Bu Kripto Para ile anında ticaret yap (1 saat içinde) payment.crypto.tradeInstant.popup=Anında ticaret yapabilmek için her iki ticaret ortağının da çevrimiçi \ olması ve ticareti 1 saatten kısa sürede tamamlaması gerekmektedir.\n\n\ Eğer açık teklifleriniz varsa ve müsait değilseniz \ lütfen bu teklifleri 'Portföy' ekranında devre dışı bırakın. payment.crypto=Kripto para payment.select.crypto=Kripto Para seçin veya arayın payment.secret=Gizli soru payment.answer=Cevap payment.wallet=Cüzdan ID payment.capitual.cap=CAP Kodu payment.upi.virtualPaymentAddress=Sanal Ödeme Adresi # suppress inspection "UnusedProperty" payment.swift.headline=Uluslararası SWIFT Havale # suppress inspection "UnusedProperty" payment.swift.title.bank=Alıcı Banka # suppress inspection "UnusedProperty" payment.swift.title.intermediary=Ara Banka (genişletmek için tıklayın) # suppress inspection "UnusedProperty" payment.swift.country.bank=Alıcı Banka Ülkesi # suppress inspection "UnusedProperty" payment.swift.country.intermediary=Ara Banka Ülkesi # suppress inspection "UnusedProperty" payment.swift.swiftCode.bank=Alıcı Banka SWIFT Kodu # suppress inspection "UnusedProperty" payment.swift.swiftCode.intermediary=Ara Banka SWIFT Kodu # suppress inspection "UnusedProperty" payment.swift.name.bank=Alıcı Banka adı # suppress inspection "UnusedProperty" payment.swift.name.intermediary=Ara Banka adı # suppress inspection "UnusedProperty" payment.swift.branch.bank=Alıcı Banka şubesi # suppress inspection "UnusedProperty" payment.swift.branch.intermediary=Ara Banka şubesi # suppress inspection "UnusedProperty" payment.swift.address.bank=Alıcı Banka adresi # suppress inspection "UnusedProperty" payment.swift.address.intermediary=Ara Banka adresi # suppress inspection "UnusedProperty" payment.swift.address.beneficiary=Alıcı adresi # suppress inspection "UnusedProperty" payment.swift.phone.beneficiary=Alıcı telefon numarası payment.swift.account=Hesap No. (veya IBAN) payment.swift.use.intermediary=Ara Banka Kullan payment.swift.showPaymentInfo=Ödeme Bilgilerini Göster... payment.account.owner.address=Hesap sahibi adresi payment.transferwiseUsd.address=(ABD merkezli olmalı, banka adresini kullanmayı düşünün) payment.amazon.site=Hediye kartı satın al payment.ask=Tüccar Sohbetinde Sor payment.uphold.accountId=Kullanıcı adı veya email veya telefon numarası payment.moneyBeam.accountId=Email veya telefon numarası payment.popmoney.accountId=Email veya telefon numarası payment.promptPay.promptPayId=Vatandaşlık Kimlik/ Vergi Kimlik veya telefon numarası payment.supportedCurrencies=Desteklenen para birimleri payment.supportedCurrenciesForReceiver=Fon alma para birimleri payment.limitations=Kısıtlamalar payment.salt=Hesap yaşı doğrulama için tuz payment.error.noHexSalt=Tuz HEX formatında olmalıdır.\n\ Sadece eski bir hesaptan tuz aktarıp hesap yaşını korumak istiyorsanız tuz alanını düzenlemeniz önerilir. \ Hesap yaşı, hesap tuzu ve tanımlayıcı hesap verileri (örneğin IBAN) kullanılarak doğrulanır. payment.accept.euro=Bu Euro ülkelerinden gelen işlemleri kabul et payment.accept.nonEuro=Bu Euro dışı ülkelerden gelen işlemleri kabul et payment.accepted.countries=Kabul edilen ülkeler payment.accepted.banks=Kabul edilen bankalar (ID) payment.mobile=Mobil numara payment.postal.address=Posta adresi payment.national.account.id.AR=CBU numarası shared.accountSigningState=Hesap imzalama durumu #new payment.crypto.address.dyn={0} adresi payment.crypto.receiver.address=Alıcının kripto para adresi payment.accountNr=Hesap numarası payment.emailOrMobile=E-posta veya mobil no. payment.useCustomAccountName=Özel hesap adı kullan payment.maxPeriod=Maksimum izin verilen ticaret süresi payment.maxPeriodAndLimit=Maksimum ticaret süresi: {0} / Maksimum satın alma: {1} / Maksimum satış: {2} / Hesap yaşı: {3} payment.maxPeriodAndLimitCrypto=Maksimum ticaret süresi: {0} / Maksimum ticaret limiti: {1} payment.currencyWithSymbol=Para birimi: {0} payment.nameOfAcceptedBank=Kabul edilen bankanın adı payment.addAcceptedBank=Kabul edilen banka ekle payment.clearAcceptedBanks=Kabul edilen bankaları temizle payment.bank.nameOptional=Banka adı (isteğe bağlı) payment.bankCode=Banka kodu payment.bankId=Banka kimliği (BIC/SWIFT) payment.bankIdOptional=Banka kimliği (BIC/SWIFT) (isteğe bağlı) payment.branchNr=Şube numarası payment.branchNrOptional=Şube numarası (isteğe bağlı) payment.accountNrLabel=Hesap numarası (IBAN) payment.iban=IBAN payment.tikkie.iban=Tikkie'de Haveno ticareti için kullanılan IBAN payment.accountType=Hesap türü payment.checking=Vadesiz payment.savings=Vadeli payment.personalId=Kişisel kimlik payment.zelle.info=Zelle, başka bir banka *aracılığıyla* en iyi şekilde çalışan bir para transfer hizmetidir.\n\n\ 1. Bankanızın Zelle ile çalışıp çalışmadığını ve nasıl çalıştığını görmek için bu sayfayı kontrol edin: [HYPERLINK:https://www.zellepay.com/get-started]\n\n\ 2. Transfer limitlerinize özellikle dikkat edin—gönderme limitleri bankadan bankaya değişir ve bankalar genellikle ayrı günlük, haftalık ve aylık limitler belirler.\n\n\ 3. Bankanız Zelle ile çalışmıyorsa, Zelle mobil uygulaması aracılığıyla hala kullanabilirsiniz, ancak transfer limitleriniz çok daha düşük olacaktır.\n\n\ 4. Haveno hesabınızda belirtilen isim, Zelle/bank hesabınızdaki isimle EŞLEŞMELİDİR.\n\n\ Ticaret sözleşmenizde belirtilen şekilde bir Zelle işlemini tamamlayamazsanız, güvenlik depozitonuzun bir kısmını (veya tamamını) kaybedebilirsiniz.\n\n\ Zelle'in biraz daha yüksek geri ödeme riski nedeniyle, satıcılara, imzasız alıcılarla e-posta veya SMS yoluyla iletişime geçerek Haveno'da belirtilen \ Zelle hesabına gerçekten sahip olduklarını doğrulamalarını öneririz. payment.fasterPayments.newRequirements.info=Bazı bankalar, Faster Payments transferleri için alıcının tam adını \ doğrulamaya başladı. Mevcut Faster Payments hesabınız tam bir isim belirtmiyor.\n\n\ Lütfen gelecekteki {0} alıcılarına tam bir isim sağlamak için Haveno'da Faster Payments hesabınızı yeniden oluşturmayı düşünün.\n\n\ Hesabı yeniden oluşturduğunuzda, eski hesabınızdaki tam sort kodunu, hesap numarasını ve hesap yaşı doğrulama \ tuzu değerlerini yeni hesabınıza kopyaladığınızdan emin olun. Bu, mevcut hesabınızın yaşı ve imzalama \ durumunun korunmasını sağlayacaktır. payment.fasterPayments.ukSortCode="Birleşik Krallık sort kodu" payment.moneyGram.info=MoneyGram kullanırken XMR alıcısının, Yetkilendirme numarasını ve makbuzun bir fotoğrafını e-posta ile XMR satıcısına göndermesi gerekir. \ Makbuz, satıcının tam adını, ülkesini, eyaletini ve miktarını açıkça göstermelidir. Satıcının e-postası ticaret sürecinde alıcıya gösterilecektir. payment.westernUnion.info=Western Union kullanırken XMR alıcısının, MTCN (izleme numarası) ve makbuzun bir fotoğrafını e-posta ile XMR satıcısına göndermesi gerekir. \ Makbuz, satıcının tam adını, şehri, ülkeyi ve miktarı açıkça göstermelidir. Satıcının e-postası ticaret sürecinde alıcıya gösterilecektir. payment.halCash.info=HalCash kullanırken XMR alıcısının, HalCash kodunu mobil telefonundan bir kısa mesaj ile XMR satıcısına göndermesi gerekir.\n\n\ Lütfen HalCash ile göndermenize izin verilen maksimum miktarı aşmadığınızdan emin olun. \ Minimum çekim miktarı 10 EUR ve maksimum miktar 600 EUR'dur. Tekrarlanan çekimlerde, \ alıcı başına günlük 3000 EUR ve aylık 6000 EUR'dur. Lütfen bu limitlerin bankanızın \ burada belirtilenlerle aynı limitleri kullandığından emin olmak için çapraz kontrol yapın.\n\n\ Çekim miktarı, ATM'den başka miktarlar çekemeyeceğiniz için 10 EUR'nun katları olmalıdır. \ Teklif oluşturma ve teklif alma ekranında kullanıcı arayüzü, EUR miktarının doğru olması için XMR miktarını ayarlayacaktır. Piyasa \ bazlı fiyat kullanamazsınız çünkü EUR miktarı değişen fiyatlarla değişir.\n\n\ Bir anlaşmazlık durumunda, XMR alıcısının EUR gönderdiğine dair kanıt sunması gerekir. # suppress inspection "UnusedMessageFormatParameter" payment.limits.info=Lütfen tüm banka transferlerinin belirli bir miktarda geri ödeme riski taşıdığını unutmayın. Bu riski azaltmak için, \ Haveno, kullanılan ödeme yönteminin tahmini geri ödeme riski seviyesine dayalı olarak işlem başına limitler belirler.\n\ \n\ Bu ödeme yöntemi için, satın alma ve satma işlemlerinde işlem başına limitiniz {2}.\n\ \n\ Bu limit yalnızca tek bir işlemin boyutuna uygulanır—istediğiniz kadar işlem yapabilirsiniz.\n\ \n\ Daha fazla ayrıntı için wiki sayfasına bakın [HYPERLINK:https://docs.haveno.exchange/overview/account_limits]. # suppress inspection "UnusedProperty" payment.limits.info.withSigning=Geri ödeme riskini sınırlamak için, Haveno, bu ödeme hesabı türü için işlem başına limitler belirler \ aşağıdaki 2 faktöre dayanır:\n\n\ 1. Ödeme yöntemi için genel geri ödeme riski\n\ 2. Hesap imzalama durumu\n\ \n\ Bu ödeme hesabı henüz imzalanmamış, bu nedenle işlem başına satın alma limiti {0} ile sınırlıdır. \ İmzalamadan sonra, satın alma limitleri şu şekilde artacaktır:\n\ \n\ ● İmzalamadan önce ve imzalamadan sonraki 30 gün boyunca, işlem başına satın alma limitiniz {0} olacaktır\n\ ● İmzalamadan 30 gün sonra, işlem başına satın alma limitiniz {1} olacaktır\n\ ● İmzalamadan 60 gün sonra, işlem başına satın alma limitiniz {2} olacaktır\n\ \n\ Hesap imzalama, satış limitlerini etkilemez. Bir işlemde hemen {2} satabilirsiniz.\n\ \n\ Bu limitler yalnızca tek bir işlemin boyutuna uygulanır—istediğiniz kadar işlem yapabilirsiniz. \n\ \n\ Daha fazla ayrıntı için wiki sayfasına bakın [HYPERLINK:https://docs.haveno.exchange/overview/account_limits]. payment.cashDeposit.info=Lütfen bankanızın başka kişilerin hesaplarına nakit yatırma işlemlerine izin verdiğini onaylayın. \ Örneğin, Bank of America ve Wells Fargo gibi bankalar bu tür yatırımlara artık izin vermemektedir. payment.revolut.info=Revolut, hesap kimliği olarak telefon numarası veya e-posta yerine 'Kullanıcı Adı'nı gerektirir. payment.account.revolut.addUserNameInfo={0}\n\ Mevcut Revolut hesabınız ({1}) 'Kullanıcı Adı'na sahip değil.\n\ Hesap verilerinizi güncellemek için lütfen Revolut 'Kullanıcı Adınızı' girin.\n\ Bu, hesap yaş imza durumunuzu etkilemeyecektir. payment.revolut.addUserNameInfo.headLine=Revolut hesabını güncelle payment.cashapp.info=Lütfen Cash App'in çoğu banka transferinden daha yüksek geri ödeme riski taşıdığını unutmayın. payment.venmo.info=Lütfen Venmo'nun çoğu banka transferinden daha yüksek geri ödeme riski taşıdığını unutmayın. payment.paypal.info=Lütfen PayPal'in çoğu banka transferinden daha yüksek geri ödeme riski taşıdığını unutmayın. payment.amazonGiftCard.upgrade=Amazon hediye kartları ödeme yöntemi için ülkenin belirtilmesi gerekmektedir. payment.account.amazonGiftCard.addCountryInfo={0}\n\ Mevcut Amazon Hediye Kartı hesabınız ({1}) belirtilen bir ülkeye sahip değil.\n\ Hesap verilerinizi güncellemek için lütfen Amazon Hediye Kartı Ülkenizi girin.\n\ Bu, hesap yaş durumunuzu etkilemeyecektir. payment.amazonGiftCard.upgrade.headLine=Amazon Hediye Kartı hesabını güncelle payment.swift.info.account=Haveno'da SWIFT kullanımı için temel yönergeleri dikkatlice inceleyin:\n\ \n\ - tüm alanları eksiksiz ve doğru şekilde doldurun \n\ - alıcı, ödeme yapanın belirttiği para biriminde ödeme yapmalıdır \n\ - alıcı, paylaşılan ücret modeli (SHA) kullanarak ödeme yapmalıdır \n\ - alıcı ve satıcı ücretlerle karşılaşabilir, bu yüzden bankalarının ücret tarifelerini önceden kontrol etmelidirler \n\ \n\ SWIFT, diğer ödeme yöntemlerinden daha karmaşıktır, bu yüzden lütfen wiki'deki tam rehberi inceleyin [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/SWIFT]. payment.swift.info.buyer=SWIFT ile monero satın almak için şunları yapmalısınız:\n\ \n\ - ödeme yapanın belirttiği para biriminde ödeme yapın \n\ - ödeme göndermek için paylaşılan ücret modeli (SHA) kullanın\n\ \n\ Ceza almamak ve sorunsuz ticaretler yapmak için lütfen wiki'deki daha fazla rehberi inceleyin [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/SWIFT]. payment.swift.info.seller=SWIFT gönderenler, ödemeleri göndermek için paylaşılan ücret modeli (SHA) kullanmak zorundadır.\n\ \n\ SHA kullanmayan bir SWIFT ödemesi alırsanız, bir arabuluculuk bileti açın.\n\ \n\ Ceza almamak ve sorunsuz ticaretler yapmak için lütfen wiki'deki daha fazla rehberi inceleyin [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/SWIFT]. payment.imps.info.account=Lütfen şunları dahil ettiğinizden emin olun:\n\n\ ● Hesap sahibi tam adı\n\ ● Hesap numarası\n\ ● IFSC numarası\n\n\ Bu detaylar, ödeme gönderip alacağınız banka hesabınızla eşleşmelidir.\n\n\ Bir işlemde gönderilebilecek maksimum miktarın Rs. 200.000 olduğunu unutmayın. Bu miktarın üzerindeki işlemler için birden fazla işlem gerekecektir. Ancak, günde maksimum Rs. 1.000.000 gönderilebileceğini unutmayın.\n\n\ Bazı bankaların müşterileri için farklı limitleri vardır. payment.imps.info.buyer=Lütfen yalnızca Haveno'da sağlanan hesap bilgilerine ödeme gönderin.\n\n\ Maksimum ticaret boyutu işlem başına Rs. 200.000'dir.\n\n\ Ticaretiniz Rs. 200.000'den fazla ise birden fazla transfer yapmanız gerekecektir. Ancak, günde maksimum Rs. 1.000.000 gönderilebileceğini unutmayın.\n\n\ Bazı bankaların müşterileri için farklı limitleri olduğunu unutmayın. payment.imps.info.seller=Bir ticarette Rs. 200.000'den fazla almayı planlıyorsanız, alıcının birden fazla transfer yapması gerektiğini beklemelisiniz. Ancak, günde maksimum Rs. 1.000.000 gönderilebileceğini unutmayın.\n\n\ Bazı bankaların müşterileri için farklı limitleri olduğunu unutmayın. payment.neft.info.account=Lütfen şunları dahil ettiğinizden emin olun:\n\n\ ● Hesap sahibi tam adı\n\ ● Hesap numarası\n\ ● IFSC numarası\n\n\ Bu detaylar, ödeme gönderip alacağınız banka hesabınızla eşleşmelidir.\n\n\ Bir işlemde gönderilebilecek maksimum miktarın Rs. 50.000 olduğunu unutmayın. Bu miktarın üzerindeki işlemler için birden fazla işlem gerekecektir.\n\n\ Bazı bankaların müşterileri için farklı limitleri vardır. payment.neft.info.buyer=Lütfen yalnızca Haveno'da sağlanan hesap bilgilerine ödeme gönderin.\n\n\ Maksimum ticaret boyutu işlem başına Rs. 50.000'dir.\n\n\ Ticaretiniz Rs. 50.000'den fazla ise birden fazla transfer yapmanız gerekecektir.\n\n\ Bazı bankaların müşterileri için farklı limitleri olduğunu unutmayın. payment.neft.info.seller=Bir ticarette Rs. 50.000'den fazla almayı planlıyorsanız, alıcının birden fazla transfer yapması gerektiğini beklemelisiniz.\n\n\ Bazı bankaların müşterileri için farklı limitleri olduğunu unutmayın. payment.paytm.info.account=PayTM hesabınızdaki e-posta veya telefon numaranızla eşleşen e-posta veya telefon numaranızı girdiğinizden emin olun. \n\n\ KYC'siz bir PayTM hesabı kuran kullanıcılar şu sınırlamalara tabidir: \n\n\ ● İşlem başına maksimum 5.000 Rs gönderilebilir.\n\ ● Birinin PayTM cüzdanında maksimum 10.000 Rs tutulabilir.\n\n\ Ticaret başına 5.000 Rs üzeri bir miktar ticareti yapmak istiyorsanız, PayTM ile KYC işlemini tamamlamanız gerekecektir. KYC ile kullanıcılar şu sınırlamalara tabidir:\n\n\ ● İşlem başına maksimum 100.000 Rs gönderilebilir.\n\ ● Birinin PayTM cüzdanında maksimum 100.000 Rs tutulabilir.\n\n\ Kullanıcıların hesap limitlerinin de farkında olması gerekir. PayTM hesap limitlerinin üzerindeki ticaretler muhtemelen birden fazla günde gerçekleştirilmeli veya iptal edilmelidir. payment.paytm.info.buyer=Lütfen yalnızca sağlanan e-posta adresine veya telefon numarasına ödeme gönderin.\n\n\ Ticaret başına 5.000 Rs üzeri bir miktar ticareti yapmak istiyorsanız, PayTM ile KYC işlemini tamamlamanız gerekecektir.\n\n\ KYC olmadan işlem başına 5.000 Rs gönderilebilir.\n\n\ KYC ile kullanıcılar işlem başına 100.000 Rs gönderebilir. payment.paytm.info.seller=Ticaret başına 5.000 Rs üzeri bir miktar ticareti yapmak istiyorsanız, PayTM ile KYC işlemini tamamlamanız gerekecektir. KYC ile kullanıcılar şu sınırlamalara tabidir:\n\n\ ● İşlem başına maksimum 100.000 Rs gönderilebilir.\n\ ● PayTM cüzdanınızda maksimum 100.000 Rs tutulabilir.\n\n\ Kullanıcıların hesap limitlerinin de farkında olması gerekir. PayTM cüzdanınızda maksimum 100.000 Rs tutulabileceği için lütfen düzenli olarak rupee transferi yapın. payment.rtgs.info.account=RTGS, Rs. 200.000 veya üzeri büyük işlemler için kullanılır.\n\n\ RTGS ödeme hesabınızı kurarken lütfen şunları dahil ettiğinizden emin olun:\n\n\ ● Hesap sahibi tam adı\n\ ● Hesap numarası\n\ ● IFSC numarası\n\n\ Bu detaylar, ödeme gönderip alacağınız banka hesabınızla eşleşmelidir.\n\n\ İşlem başına gönderilebilecek minimum ticaret tutarının 200.000 Rs olduğunu unutmayın. Bu miktarın altındaki işlemler ya iptal edilir ya da her iki tüccarın başka bir ödeme yöntemi (örneğin IMPS veya UPI) üzerinde anlaşması gerekir. payment.rtgs.info.buyer=Lütfen yalnızca Haveno'da sağlanan hesap bilgilerine ödeme gönderin.\n\n\ İşlem başına gönderilebilecek minimum ticaret tutarının 200.000 Rs olduğunu unutmayın. Bu miktarın altındaki işlemler ya iptal edilir ya da her iki tüccarın başka bir ödeme yöntemi (örneğin IMPS veya UPI) üzerinde anlaşması gerekir. payment.rtgs.info.seller=İşlem başına gönderilebilecek minimum ticaret tutarının 200.000 Rs olduğunu unutmayın. Bu miktarın altındaki işlemler ya iptal edilir ya da her iki tüccarın başka bir ödeme yöntemi (örneğin IMPS veya UPI) üzerinde anlaşması gerekir. payment.upi.info.account=Lütfen Sanal Ödeme Adresinizi (VPA) veya UPI ID'nizi eklediğinizden emin olun. Bu, ortasında “@” işareti bulunan bir e-posta kimliği gibi formatlanmıştır. Örneğin, UPI kimliğiniz “alıcı_adı@banka_adı” veya “telefon_numarası@banka_adı” olabilir. \n\n\ UPI için işlem başına gönderilebilecek maksimum limitin 100.000 Rs olduğunu unutmayın. \n\n\ Ticaret başına 100.000 Rs üzeri bir miktar ticareti yapmak istiyorsanız, işlemlerin muhtemelen birden fazla transferde gerçekleşeceğini unutmayın. \n\n\ Bazı bankaların müşterileri için farklı limitleri olduğunu unutmayın. payment.upi.info.buyer=Lütfen yalnızca Haveno'da sağlanan VPA / UPI ID'ye ödeme gönderin. \n\n\ İşlem başına maksimum ticaret boyutu 100.000 Rs'dir. \n\n\ Ticaretiniz 100.000 Rs üzerinde ise birden fazla transfer yapmanız gerekecektir. \n\n\ Bazı bankaların müşterileri için farklı limitleri olduğunu unutmayın. payment.upi.info.seller=Ticaret başına 100.000 Rs üzerinde almak istiyorsanız, alıcının birden fazla transfer yapmasını beklemelisiniz. \n\n\ Bazı bankaların müşterileri için farklı limitleri olduğunu unutmayın. payment.celpay.info.account=Lütfen Celcius hesabınıza kayıtlı e-postayı eklediğinizden emin olun. \ Bu, fon gönderdiğinizde doğru hesaptan görüneceğini ve fon aldığınızda hesabınıza kredilendirileceğini sağlar.\n\n\ CelPay kullanıcıları 24 saat içinde $2,500 (veya diğer para birimi/kripto eşdeğeri) gönderme ile sınırlıdır.\n\n\ CelPay hesap limitlerinin üzerindeki ticaretler muhtemelen birden fazla günde gerçekleştirilmeli veya iptal edilmelidir.\n\n\ CelPay, birden fazla stablecoin'i destekler:\n\n\ ● USD Stablecoin'ler; DAI, TrueUSD, USDC, ZUSD, BUSD, GUSD, PAX, USDT (ERC20)\n\ ● CAD Stablecoin'ler; TrueCAD\n\ ● GBP Stablecoin'ler; TrueGBP\n\ ● HKD Stablecoin'ler; TrueHKD\n\ ● AUD Stablecoin'ler; TrueAUD\n\n\ XMR Alıcılar, XMR Satıcısına eşleşen herhangi bir para birimi stablecoin gönderebilir. payment.celpay.info.buyer=Lütfen yalnızca XMR Satıcısı tarafından sağlanan e-posta adresine bir ödeme bağlantısı göndererek ödeme yapın.\n\n\ CelPay, 24 saat içinde $2,500 (veya diğer para birimi/kripto eşdeğeri) gönderme ile sınırlıdır.\n\n\ CelPay hesap limitlerinin üzerindeki ticaretler muhtemelen birden fazla günde gerçekleştirilmeli veya iptal edilmelidir.\n\n\ CelPay, birden fazla stablecoin'i destekler:\n\n\ ● USD Stablecoin'ler; DAI, TrueUSD, USDC, ZUSD, BUSD, GUSD, PAX, USDT (ERC20)\n\ ● CAD Stablecoin'ler; TrueCAD\n\ ● GBP Stablecoin'ler; TrueGBP\n\ ● HKD Stablecoin'ler; TrueHKD\n\ ● AUD Stablecoin'ler; TrueAUD\n\n\ XMR Alıcılar, XMR Satıcısına eşleşen herhangi bir para birimi stablecoin gönderebilir. payment.celpay.info.seller=XMR Satıcıları, güvenli bir ödeme bağlantısı aracılığıyla ödeme almayı beklemelidir. \ Lütfen e-posta ödeme bağlantısının XMR Alıcısı tarafından sağlanan e-posta adresini içerdiğinden emin olun.\n\n\ CelPay kullanıcıları 24 saat içinde $2,500 (veya diğer para birimi/kripto eşdeğeri) gönderme ile sınırlıdır.\n\n\ CelPay hesap limitlerinin üzerindeki ticaretler muhtemelen birden fazla günde gerçekleştirilmeli veya iptal edilmelidir.\n\n\ CelPay, birden fazla stablecoin'i destekler:\n\n\ ● USD Stablecoin'ler; DAI, TrueUSD, USDC, ZUSD, BUSD, GUSD, PAX, USDT (ERC20)\n\ ● CAD Stablecoin'ler; TrueCAD\n\ ● GBP Stablecoin'ler; TrueGBP\n\ ● HKD Stablecoin'ler; TrueHKD\n\ ● AUD Stablecoin'ler; TrueAUD\n\n\ XMR Satıcıları, XMR Alıcısından eşleşen herhangi bir para birimi stablecoin almayı beklemelidir. XMR Alıcısının eşleşen herhangi bir para birimi stablecoin göndermesi mümkündür. payment.celpay.supportedCurrenciesForReceiver=Desteklenen para birimleri (lütfen dikkat edin: aşağıdaki tüm para birimleri Celcius uygulaması içinde desteklenen stablecoin'lerdir. Ticaretler stablecoin'ler içindir, fiat değil.) payment.nequi.info.account=Nequi hesabınızla ilişkilendirilen telefon numaranızı eklediğinizden emin olun.\n\n\ Kullanıcılar bir Nequi hesabı kurduklarında, ödeme limitleri ayda maksimum ~ 7,000,000 COP olarak belirlenir.\n\n\ Tek seferde 7,000,000 COP'dan fazla işlem yapmak istiyorsanız, Bancolombia ile KYC tamamlamanız ve yaklaşık 15,000 COP \ tutarında bir ücret ödemeniz gerekecek. Bundan sonra, tüm işlemler %0,4 vergiye tabi olacaktır. Lütfen en son vergilerden haberdar olduğunuzdan emin olun.\n\n\ Kullanıcılar ayrıca hesap limitlerinin farkında olmalıdır. Yukarıdaki limitleri aşarsanız, işleminiz iptal edilebilir ve ceza uygulanabilir. payment.nequi.info.buyer=Lütfen ödemeyi yalnızca XMR Satıcısının Haveno hesabında belirtilen telefon numarasına gönderin.\n\n\ Kullanıcılar bir Nequi hesabı kurduklarında, ödeme limitleri ayda maksimum ~ 7,000,000 COP olarak belirlenir.\n\n\ Tek seferde 7,000,000 COP'dan fazla işlem yapmak istiyorsanız, Bancolombia ile KYC tamamlamanız ve yaklaşık 15,000 COP \ tutarında bir ücret ödemeniz gerekecek. Bundan sonra, tüm işlemler %0,4 vergiye tabi olacaktır. Lütfen en son vergilerden haberdar olduğunuzdan emin olun.\n\n\ Kullanıcılar ayrıca hesap limitlerinin farkında olmalıdır. Yukarıdaki limitleri aşarsanız, işleminiz iptal edilebilir ve ceza uygulanabilir. payment.nequi.info.seller=Lütfen alınan ödemenin XMR Alıcısının Haveno hesabında belirtilen telefon numarasına uygun olup olmadığını kontrol edin.\n\n\ Kullanıcılar bir Nequi hesabı kurduklarında, ödeme limitleri ayda maksimum ~ 7,000,000 COP olarak belirlenir.\n\n\ Tek seferde 7,000,000 COP'dan fazla işlem yapmak istiyorsanız, Bancolombia ile KYC tamamlamanız ve yaklaşık 15,000 COP \ tutarında bir ücret ödemeniz gerekecek. Bundan sonra, tüm işlemler %0,4 vergiye tabi olacaktır. Lütfen en son vergilerden haberdar olduğunuzdan emin olun.\n\n\ Kullanıcılar ayrıca hesap limitlerinin farkında olmalıdır. Yukarıdaki limitleri aşarsanız, işleminiz iptal edilebilir ve ceza uygulanabilir. payment.bizum.info.account=Bizum kullanmak için İspanya'da bir banka hesabına (IBAN) sahip olmanız ve hizmete kayıtlı olmanız gerekir.\n\n\ Bizum, €0.50 ile €1,000 arasındaki işlemler için kullanılabilir.\n\n\ Bizum kullanarak gönderebileceğiniz/alabileceğiniz maksimum işlem tutarı günde €2,000'dır.\n\n\ Bizum kullanıcıları ayda maksimum 150 işlem yapabilirler.\n\n\ Ancak, her banka yukarıdaki limitler dahilinde kendi limitlerini belirleyebilir.\n\n\ Kullanıcılar ayrıca hesap limitlerinin farkında olmalıdır. Yukarıdaki limitleri aşarsanız, işleminiz iptal edilebilir ve ceza uygulanabilir. payment.bizum.info.buyer=Lütfen ödemeyi yalnızca Haveno'da belirtilen XMR Satıcısının cep telefonu numarasına gönderin.\n\n\ Maksimum işlem tutarı ödeme başına €1,000'dır. Bizum kullanarak gönderebileceğiniz maksimum işlem tutarı günde €2,000'dır.\n\n\ Yukarıdaki limitleri aşarsanız, işleminiz iptal edilebilir ve ceza uygulanabilir. payment.bizum.info.seller=Lütfen ödemenin, Haveno'da belirtilen XMR Alıcısının cep telefonu numarasından geldiğinden emin olun.\n\n\ Maksimum işlem tutarı ödeme başına €1,000'dır. Bizum kullanarak alabileceğiniz maksimum işlem tutarı günde €2,000'dır.\n\n\ Yukarıdaki limitleri aşarsanız, işleminiz iptal edilebilir ve ceza uygulanabilir. payment.pix.info.account=Lütfen seçtiğiniz Pix Anahtarını eklediğinizden emin olun. Dört tür anahtar vardır: \ CPF (Doğal Kişiler Kütüğü) veya CNPJ (Ulusal Hukuki Kişiler Sicili), e-posta adresi, telefon numarası veya sistem tarafından üretilen rastgele bir \ anahtar olan evrensel benzersiz tanımlayıcı (UUID). Sahip olduğunuz her Pix hesabı için farklı bir anahtar kullanılmalıdır. \ Bireyler sahip oldukları her hesap için beş anahtara kadar oluşturabilirler.\n\n\ Haveno'da işlem yaparken, XMR Alıcılarının Pix Anahtarlarını ödeme açıklaması olarak kullanmaları gerekir, böylece XMR Satıcılarının ödemenin kendilerinden geldiğini kolayca tanımlamaları sağlanır. payment.pix.info.buyer=Lütfen ödemeyi yalnızca XMR Satıcısının Haveno hesabında belirtilen Pix Anahtarına gönderin.\n\n\ Ödemeyi kendinizden geldiğini kolayca tanımlaması için Pix Anahtarınızı ödeme referansı olarak kullanın. payment.pix.info.seller=Lütfen alınan ödeme açıklamasının, XMR Alıcısının Haveno hesabında belirtilen Pix Anahtarı ile eşleştiğini kontrol edin. payment.pix.key=Pix Anahtarı (CPF, CNPJ, E-posta, Telefon numarası veya UUID) payment.monese.info.account=Monese, GBP, EUR ve RON* kullanıcıları için bir banka uygulamasıdır. Monese, kullanıcıların \ diğer Monese hesaplarına herhangi bir desteklenen para biriminde anında ve ücretsiz para göndermesine olanak tanır.\n\n\ *Monese'de bir RON hesabı açmak için Romanya'da yaşamanız veya Romen vatandaşlığınızın olması gerekmektedir.\n\n\ Haveno'da Monese hesabınızı kurarken, Monese hesabınızla eşleşen adınızı ve telefon numaranızı eklediğinizden emin olun. \ Bu, para gönderdiğinizde doğru hesaptan geldiğini ve para aldığınızda hesabınıza kredi olarak işlendiğini \ sağlayacaktır. payment.monese.info.buyer=Lütfen ödemeyi yalnızca XMR Satıcısının Haveno hesabında belirtilen telefon numarasına gönderin. Lütfen ödeme açıklamasını boş bırakın. payment.monese.info.seller=XMR Satıcıları, ödemenin XMR Alıcısının Haveno hesabında gösterilen telefon numarası / isimden gelmesini beklemelidir. payment.satispay.info.account=Satispay kullanmak için İtalya'da bir banka hesabına (IBAN) sahip olmanız ve hizmete kayıtlı olmanız gerekmektedir.\n\n\ Satispay hesap limitleri bireysel olarak belirlenir. Daha yüksek tutarlarda işlem yapmak istiyorsanız, limitlerinizi artırmak için Satispay \ desteğiyle iletişime geçmeniz gerekecektir. Kullanıcılar ayrıca hesap limitlerinin farkında olmalıdır. Eğer yukarıdaki limitleri aşarsanız \ işleminiz iptal edilebilir ve ceza uygulanabilir. payment.satispay.info.buyer=Lütfen ödemeyi yalnızca Haveno'da belirtilen XMR Satıcısının cep telefonu numarasına gönderin.\n\n\ Satispay hesap limitleri bireysel olarak belirlenir. Daha yüksek tutarlarda işlem yapmak istiyorsanız, limitlerinizi artırmak için Satispay \ desteğiyle iletişime geçmeniz gerekecektir. Kullanıcılar ayrıca hesap limitlerinin farkında olmalıdır. Eğer yukarıdaki limitleri aşarsanız \ işleminiz iptal edilebilir ve ceza uygulanabilir. payment.satispay.info.seller=Lütfen ödemenin, Haveno'da belirtilen XMR Alıcısının cep telefonu numarası / isminden geldiğinden emin olun.\n\n\ Satispay hesap limitleri bireysel olarak belirlenir. Daha yüksek tutarlarda işlem yapmak istiyorsanız, limitlerinizi artırmak için Satispay \ desteğiyle iletişime geçmeniz gerekecektir. Kullanıcılar ayrıca hesap limitlerinin farkında olmalıdır. Eğer yukarıdaki limitleri aşarsanız \ işleminiz iptal edilebilir ve ceza uygulanabilir. payment.tikkie.info.account=Tikkie kullanmak için Hollanda'da bir banka hesabına (IBAN) sahip olmanız ve hizmete kayıtlı olmanız gerekmektedir.\n\n\ Bir kişiye Tikkie ödeme talebi gönderdiğinizde, talep başına maksimum €750 talep edebilirsiniz.\n\n\ Tikkie hesabı başına 24 saat içinde talep edebileceğiniz maksimum miktar €2,500'dır.\n\n\ Ancak, her banka bu limitler dahilinde müşterileri için kendi limitlerini belirleyebilir.\n\n\ Kullanıcılar ayrıca hesap limitlerinin farkında olmalıdır. Eğer yukarıdaki limitleri aşarsanız, işleminiz iptal edilebilir ve ceza uygulanabilir. payment.tikkie.info.buyer=Lütfen işlem sohbetinde XMR Satıcısından bir ödeme bağlantısı talep edin. XMR Satıcısı size \ işlemin doğru tutarına uygun bir ödeme bağlantısı gönderdikten sonra ödeme işlemine devam edin.\n\n\ XMR Satıcısı bir Tikkie ödemesi talep ettiğinde, talep başına maksimum €750 talep edebilir. Eğer \ işlem bu tutarın üzerindeyse, XMR Satıcısının işlem tutarını tamamlamak için birden fazla talepte bulunması gerekecektir. Bir günde \ talep edebileceğiniz maksimum miktar €2,500'dır.\n\n\ Ancak, her banka bu limitler dahilinde müşterileri için kendi limitlerini belirleyebilir.\n\n\ Kullanıcılar ayrıca hesap limitlerinin farkında olmalıdır. Eğer yukarıdaki limitleri aşarsanız, işleminiz iptal edilebilir ve ceza uygulanabilir. payment.tikkie.info.seller=Lütfen işlem sohbetinde XMR Alıcısına bir ödeme bağlantısı gönderin. XMR \ Alıcısı size ödeme gönderdikten sonra, IBAN bilgilerinin Haveno'da belirtilen bilgilerle eşleştiğini kontrol edin.\n\n\ XMR Satıcısı bir Tikkie ödemesi talep ettiğinde, talep başına maksimum €750 talep edebilir. Eğer \ işlem bu tutarın üzerindeyse, XMR Satıcısının işlem tutarını tamamlamak için birden fazla talepte bulunması gerekecektir. Bir günde \ talep edebileceğiniz maksimum miktar €2,500'dır.\n\n\ Ancak, her banka bu limitler dahilinde müşterileri için kendi limitlerini belirleyebilir.\n\n\ Kullanıcılar ayrıca hesap limitlerinin farkında olmalıdır. Eğer yukarıdaki limitleri aşarsanız, işleminiz iptal edilebilir ve ceza uygulanabilir. payment.verse.info.account=Verse, EUR, SEK, HUF, DKK ve PLN cinsinden ödeme gönderebilen ve alabilen çoklu para birimi ödeme yöntemidir.\n\n\ Haveno'da Verse hesabınızı kurarken, Verse hesabınızdaki kullanıcı adınızla eşleşen kullanıcı adınızı eklediğinizden emin olun. \ Bu, para gönderdiğinizde doğru hesaptan geldiğini ve para aldığınızda hesabınıza kredi olarak işlendiğini \ sağlayacaktır.\n\n\ Verse kullanıcıları, ödeme hesaplarından yapılan veya ödeme hesaplarına alınan toplam ödemeler için yılda €10,000 \ (veya eşdeğer yabancı para tutarı) gönderme veya alma ile sınırlıdır. Bu limit, Verse talep üzerine artırılabilir. payment.verse.info.buyer=Lütfen ödemeyi yalnızca XMR Satıcısının Haveno hesabında sağladığı kullanıcı adına gönderin. \ Lütfen ödeme açıklamasını boş bırakın.\n\n\ Verse kullanıcıları, ödeme hesaplarından yapılan veya ödeme hesaplarına alınan toplam ödemeler için yılda €10,000 \ (veya eşdeğer yabancı para tutarı) gönderme veya alma ile sınırlıdır. Bu limit, Verse talep üzerine artırılabilir. payment.verse.info.seller=XMR Satıcıları, ödemenin XMR Alıcısının Haveno hesabında gösterilen kullanıcı adından gelmesini beklemelidir.\n\n\ Verse kullanıcıları, ödeme hesaplarından yapılan veya ödeme hesaplarına alınan toplam ödemeler için yılda €10,000 \ (veya eşdeğer yabancı para tutarı) gönderme veya alma ile sınırlıdır. Bu limit, Verse talep üzerine artırılabilir. payment.achTransfer.info.account=Haveno'da ACH'yi bir ödeme yöntemi olarak eklerken kullanıcılar, ACH transferi \ göndermenin ve almanın maliyetinin farkında olmalıdır. payment.achTransfer.info.buyer=ACH transferi göndermenin size ne kadara mal olacağını bildiğinizden emin olun.\n\n\ Ödeme yaparken, yalnızca XMR Satıcısının hesabında sağlanan ödeme bilgilerine ACH transferi kullanarak gönderin. payment.achTransfer.info.seller=ACH transferi almanın size ne kadara mal olacağını bildiğinizden emin olun.\n\n\ Ödeme alırken, ödemenin XMR Alıcısının hesabından bir ACH transferi olarak alındığını kontrol edin. payment.domesticWire.info.account=Haveno'da Yerel Havale Transferini bir ödeme yöntemi olarak eklerken kullanıcılar, \ havale transferi göndermenin ve almanın maliyetinin farkında olmalıdır. payment.domesticWire.info.buyer=Havale transferi göndermenin size ne kadara mal olacağını bildiğinizden emin olun.\n\n\ Ödeme yaparken, yalnızca XMR Satıcısının hesabında sağlanan ödeme bilgilerine gönderin. payment.domesticWire.info.seller=Havale transferi almanın size ne kadara mal olacağını bildiğinizden emin olun.\n\n\ Ödeme alırken, ödemenin XMR Alıcısının hesabından alındığını kontrol edin. payment.strike.info.account=Lütfen Strike kullanıcı adınızı eklediğinizden emin olun.\n\n\ Haveno'da Strike yalnızca fiat para birimi ödemeleri için kullanılır.\n\n\ Strike limitlerinin farkında olduğunuzdan emin olun:\n\n\ Yalnızca e-posta, isim ve telefon numarası ile kayıt olan kullanıcılar için limitler:\n\n\ ● Mevduat başına maksimum $100\n\ ● Haftalık toplam maksimum $1,000 mevduat\n\ ● Ödeme başına maksimum $100\n\n\ Strike'a daha fazla bilgi sağlayarak limitlerini artıran kullanıcılar için limitler:\n\n\ ● Mevduat başına maksimum $1,000\n\ ● Haftalık toplam maksimum $1,000 mevduat\n\ ● Ödeme başına maksimum $1,000\n\n\ Bu limitleri aşarsanız, işleminiz iptal edilebilir ve ceza uygulanabilir. payment.strike.info.buyer=Ödemeyi yalnızca XMR Satıcısının Haveno'da sağlanan Strike kullanıcı adına gönderin.\n\n\ Ödeme başına maksimum $1,000 işlem yapılabilir.\n\n\ Bu limitleri aşarsanız, işleminiz iptal edilebilir ve ceza uygulanabilir. payment.strike.info.seller=Ödemenin XMR Alıcısının Haveno'da sağlanan Strike kullanıcı adından alındığından emin olun.\n\n\ Ödeme başına maksimum $1,000 işlem yapılabilir.\n\n\ Bu limitleri aşarsanız, işleminiz iptal edilebilir ve ceza uygulanabilir. payment.transferwiseUsd.info.account=ABD bankacılık düzenlemeleri nedeniyle, USD ödemeleri göndermek ve almak, çoğu diğer \ para biriminden daha fazla sınırlamaya tabidir. Bu nedenle USD, Haveno Wise ödeme yöntemine eklenmemiştir.\n\n\ Wise-USD ödeme yöntemi, Haveno kullanıcılarının USD cinsinden ticaret yapmasına olanak tanır.\n\n\ Wise, eski adıyla Wise hesabı olan herkes, Haveno'da Wise-USD'yi bir ödeme yöntemi olarak ekleyebilir. \ Bu, USD ile XMR alıp satmalarını sağlar.\n\n\ Haveno'da ticaret yaparken XMR Alıcıları, ödeme nedeni için herhangi bir referans kullanmamalıdır. Ödeme nedeni \ gerekliyse, yalnızca Wise-USD hesap sahibinin tam adını kullanmalıdırlar. payment.transferwiseUsd.info.buyer=Ödemeyi yalnızca XMR Satıcısının Haveno Wise-USD hesabındaki e-posta adresine gönderin. payment.transferwiseUsd.info.seller=Alınan ödemenin, Haveno'daki Wise-USD hesabındaki XMR Alıcısının adıyla eşleştiğini kontrol edin. payment.usPostalMoneyOrder.info=Haveno'da ABD Posta Havale Emirleri (USPMO) kullanarak ticaret yapmak için aşağıdakileri anladığınızdan emin olmanız gerekmektedir:\n\ \n\ - XMR alıcıları, hem Ödeyen hem de Alacaklı alanlarına XMR Satıcısının adını yazmalı ve USPMO'nun ve zarfın yüksek çözünürlüklü bir fotoğrafını izleme kanıtı ile birlikte göndermeden önce çekmelidir.\n\ - XMR alıcıları, USPMO'yu Teslimat Onayı ile XMR satıcısına göndermelidir.\n\ \n\ Arabuluculuk gerekirse veya bir ticaret anlaşmazlığı olursa, fotoğrafları Haveno arabulucusuna veya iade ajanına, USPMO Seri Numarası, Postane Numarası ve dolar miktarı ile birlikte göndermeniz gerekecektir, böylece detayları ABD Posta Servisi web sitesinde doğrulayabilirler.\n\n\ Arabulucuya veya Hakeme gerekli bilgileri sağlayamamak, anlaşmazlık davasını kaybetmenize neden olacaktır.\n\n\ Tüm anlaşmazlık davalarında, USPMO gönderen, arabulucuya veya hakeme kanıt sağlama sorumluluğunu %100 taşır.\n\n\ Bu gereksinimleri anlamıyorsanız, Haveno'da USPMO kullanarak ticaret yapmayın. payment.payByMail.info=Haveno'da Posta ile Ödeme kullanarak ticaret yapmak için aşağıdakileri anladığınızdan emin olmanız gerekmektedir:\n\ \n\ ● XMR alıcısı, nakiti sahteciliğe karşı dayanıklı bir torbada paketlemelidir.\n\ ● XMR alıcısı, nakit paketleme sürecini, adres ve takip numarası pakete zaten yapıştırılmış olarak filme almalı veya yüksek çözünürlüklü fotoğraflarını çekmelidir.\n\ ● XMR alıcısı, nakit paketi Teslimat Onayı ve uygun sigorta ile XMR satıcısına göndermelidir.\n\ ● XMR satıcısı, paketi açarken, gönderici tarafından sağlanan takip numarasının videoda görünür olduğundan emin olarak filme almalıdır.\n\ ● Teklif veren, ödeme hesabının 'Ek Bilgiler' alanında herhangi bir özel şart veya koşulu belirtmelidir.\n\ ● Teklifi kabul eden, teklifi alarak teklif verenin şart ve koşullarını kabul eder.\n\ \n\ Posta ile Ödeme ticaretleri, her iki tarafın da dürüst davranma sorumluluğunu üstlenir.\n\ \n\ ● Posta ile Ödeme ticaretleri, diğer geleneksel ticaretlerden daha az doğrulanabilir işlemlere sahiptir. Bu, anlaşmazlıkların çözümünü çok daha zor hale getirir.\n\ ● Anlaşmazlıkları doğrudan trader sohbetini kullanarak çözmeye çalışın. Bu, herhangi bir Posta ile Ödeme anlaşmazlığını çözmenin en umut verici yoludur.\n\ ● Hakemler, davanızı değerlendirip bir öneride bulunabilir, ancak yardım edecekleri GARANTİ EDİLEMEZ.\n\ ● Hakemler, kendilerine sağlanan kanıtlara dayanarak bir karar verecektir. Bu nedenle, anlaşmazlık durumunda kanıt sağlamak için yukarıdaki süreçleri takip edip belgelendirin.\n\ ● Haveno'ya, Posta ile Ödeme ticaretlerinden kaynaklanan kayıp fonlar için geri ödeme talepleri değerlendirilmeyecektir.\n\ \n\ Bu gereksinimleri anlamıyorsanız, Haveno'da Posta ile Ödeme kullanarak ticaret yapmayın. payment.payByMail.contact=İletişim bilgileri payment.payByMail.contact.prompt=Zarfın üzerinde bulunması gereken isim veya takma ad payment.payByMail.extraInfo.prompt=Tekliflerinize lütfen şunları belirtin: \n\n\ Bulunduğunuz ülke (örneğin, Fransa); \n\ Ticaret kabul edeceğiniz ülkeler/bölgeler (örneğin, Fransa, AB veya herhangi bir Avrupa ülkesi); \n\ Herhangi bir özel şart/koşul; \n\ Diğer detaylar. payment.tradingRestrictions=Lütfen yapıcı tarafın şartlarını ve koşullarını gözden geçirin.\n\ Gereksinimleri karşılamıyorsanız bu ticareti yapmayın. payment.cashAtAtm.info=Kartsız Nakit: Kod kullanarak ATM'den kartsız para çekme\n\n\ Bu ödeme yöntemini kullanmak için:\n\n\ 1. Kabul ettiğiniz bankaları, bölgeleri veya teklifle birlikte gösterilecek diğer şartları listeleyerek bir Kartsız Nakit ödeme hesabı oluşturun.\n\n\ 2. Ödeme hesabı ile bir teklif oluşturun veya bir teklifi kabul edin.\n\n\ 3. Teklif kabul edildiğinde, ödeme detaylarını paylaşmak ve ödemeyi tamamlamak için eşinizle sohbet edin.\n\n\ Ticaret sözleşmenizde belirtilen şekilde işlemi tamamlayamazsanız, güvenlik teminatınızın bir kısmını (veya tamamını) kaybedebilirsiniz. payment.cashAtAtm.extraInfo.prompt=Tekliflerinize lütfen şunları belirtin: \n\n\ Kabul ettiğiniz bankalar / konumlar; \n\ Herhangi bir özel şart/koşul; \n\ Diğer detaylar. payment.f2f.contact=İletişim bilgileri payment.f2f.contact.prompt=Alım satım eşiniz tarafından nasıl iletişime geçilmek istersiniz? (e-posta adresi, telefon numarası,...) payment.f2f.city='Yüz yüze' buluşma için şehir payment.f2f.city.prompt=Şehir teklifle birlikte gösterilecektir payment.shared.optionalExtra=İsteğe bağlı ek bilgi payment.shared.extraInfo=Ek bilgi payment.shared.extraInfo.offer=Ek teklif bilgileri payment.shared.extraInfo.prompt.paymentAccount=Bu ödeme hesabınız için tekliflerinize eklemek istediğiniz özel şart, koşul veya detayları tanımlayın (kullanıcılar bu bilgileri teklifleri kabul etmeden önce görecektir). payment.shared.extraInfo.prompt.offer=Teklifinizle birlikte göstermek istediğiniz özel terimleri, koşulları veya detayları tanımlayın. payment.shared.extraInfo.noDeposit=İletişim detayları ve teklif şartları payment.f2f.info='Yüz Yüze' ticaretler farklı kurallara sahiptir ve çevrimiçi işlemlerden farklı riskler içerir.\n\n\ Başlıca farklar şunlardır:\n\ ● Ticaret eşleri, sağlanan iletişim bilgilerini kullanarak buluşma yeri ve zamanını paylaşmalıdır.\n\ ● Ticaret eşleri, laptoplarını getirmeli ve ödeme gönderildi ve ödeme alındı onaylarını buluşma yerinde yapmalıdır.\n\ ● Eğer yapıcı tarafın özel 'şart ve koşulları' varsa, bunları hesaplarındaki 'Ek bilgi' metin alanında belirtmelidir.\n\ ● Bir teklifi kabul ederek, kabul eden taraf yapıcı tarafın belirtilen 'şart ve koşullarını' kabul eder.\n\ ● Anlaşmazlık durumunda, arabulucu veya hakem genellikle buluşmada ne olduğunu kesin delillerle tespit etmek zor olduğu için pek \ yardımcı olamaz. Bu tür durumlarda XMR fonları süresiz olarak veya ticaret eşleri anlaşmaya varana \ kadar kilitlenebilir.\n\n\ 'Yüz Yüze' ticaretlerin farklarını tam olarak anladığınızdan emin olmak için lütfen şu adresteki talimatları \ ve tavsiyeleri okuyun: [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/F2F] payment.f2f.info.openURL=Web sayfasını aç payment.f2f.offerbook.tooltip.countryAndCity=Ülke ve şehir: {0} / {1} payment.shared.extraInfo.tooltip=Ek bilgi: {0} payment.ifsc=IFS Kodu payment.ifsc.validation=IFSC formatı: XXXX0999999 payment.japan.bank=Banka payment.japan.branch=Şube payment.japan.account=Hesap payment.japan.recipient=Alıcı Adı payment.australia.payid=PayID payment.payid=Finansal kuruma bağlı PayID. E-posta adresi veya cep telefonu gibi. payment.payid.info=PayID, telefon numarası, e-posta adresi veya Avustralya İş Numarası (ABN) gibi, banka, kredi birliği veya yapı \ toplum hesabınıza güvenli bir şekilde bağlayabileceğiniz bir kimliktir. Avustralya finans kurumunuzla zaten bir PayID oluşturmuş \ olmanız gerekmektedir. Hem gönderici hem de alıcı finans kurumlarının PayID'yi desteklemesi gerekmektedir. payment.amazonGiftCard.info=Amazon eGift Kart ile ödeme yapmak için, Amazon hesabınız aracılığıyla bir Amazon eGift Kartı'nı XMR satıcısına göndermeniz gerekecek. \n\n\ Daha fazla ayrıntı ve en iyi uygulamalar için lütfen wiki'ye bakın: [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Amazon_eGift_card] \n\n\ Üç önemli not:\n\ - Amazon'un daha büyük hediye kartlarını sahtekarlık olarak işaretlediği bilindiğinden, 100 USD veya daha küçük tutarlarda hediye kartları göndermeye çalışın\n\ - hediye kartının mesajı için yaratıcı, inanılabilir bir metin kullanmaya çalışın (örneğin, "Doğum günün kutlu olsun Metin Torun!") ve ticaret \ kimliğini ekleyin (ve ticaret sohbetinde ticaret eşinize seçtiğiniz referans metnini söyleyin, böylece ödemenizi doğrulayabilirler)\n\ - Amazon eGift Kartları, yalnızca satın alındıkları Amazon web sitesinde kullanılabilir (örneğin, amazon.it üzerinden satın alınan bir hediye kartı yalnızca amazon.it üzerinde kullanılabilir) payment.paysafe.info=Sizin korumanız için, Paysafecard PIN'lerini ödeme için kullanmanızı kesinlikle önermiyoruz.\n\n\ PIN'ler ile yapılan işlemler, ihtilaf çözümü için bağımsız olarak doğrulanamaz. Bir sorun oluşursa, fonların geri alınması mümkün olmayabilir.\n\n\ İhtilaf çözümü ile işlem güvenliğini sağlamak için, her zaman doğrulanabilir kayıtlar sağlayan ödeme yöntemlerini kullanın. # We use constants from the code so we do not use our normal naming convention # dynamic values are not recognized by IntelliJ # Only translate general terms NATIONAL_BANK=Ulusal banka transferi SAME_BANK=Aynı banka transferi SPECIFIC_BANKS=Belirli bankalarla transferler US_POSTAL_MONEY_ORDER=ABD Posta Havalesi CASH_DEPOSIT=Nakit Yatırma PAY_BY_MAIL=Postayla Ödeme CASH_AT_ATM=ATM'de Nakit MONEY_GRAM=MoneyGram WESTERN_UNION=Western Union F2F=Yüz yüze (kişisel) JAPAN_BANK=Japon Bankası Furikomi AUSTRALIA_PAYID=Avustralya PayID # suppress inspection "UnusedProperty" NATIONAL_BANK_SHORT=Ulusal bankalar # suppress inspection "UnusedProperty" SAME_BANK_SHORT=Aynı banka # suppress inspection "UnusedProperty" SPECIFIC_BANKS_SHORT=Belirli bankalar # suppress inspection "UnusedProperty" US_POSTAL_MONEY_ORDER_SHORT=ABD Havalesi # suppress inspection "UnusedProperty" CASH_DEPOSIT_SHORT=Nakit Yatırma # suppress inspection "UnusedProperty" PAY_BY_MAIL_SHORT=Postayla Ödeme # suppress inspection "UnusedProperty" CASH_AT_ATM_SHORT=ATM'de Nakit # suppress inspection "UnusedProperty" MONEY_GRAM_SHORT=MoneyGram # suppress inspection "UnusedProperty" WESTERN_UNION_SHORT=Western Union # suppress inspection "UnusedProperty" F2F_SHORT=Yüz yüze # suppress inspection "UnusedProperty" JAPAN_BANK_SHORT=Japon Furikomi # suppress inspection "UnusedProperty" AUSTRALIA_PAYID_SHORT=PayID # Do not translate brand names # suppress inspection "UnusedProperty" UPHOLD=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY=Popmoney # suppress inspection "UnusedProperty" REVOLUT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY=WeChat Pay # suppress inspection "UnusedProperty" SEPA=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT=SEPA Anında Ödemeler # suppress inspection "UnusedProperty" FASTER_PAYMENTS=Faster Payments # suppress inspection "UnusedProperty" SWISH=Swish # suppress inspection "UnusedProperty" ZELLE=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS=Kripto Paralar # suppress inspection "UnusedProperty" PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE=Wise # suppress inspection "UnusedProperty" TRANSFERWISE_USD=Wise-USD # suppress inspection "UnusedProperty" PAYSERA=Paysera # suppress inspection "UnusedProperty" PAXUM=Paxum # suppress inspection "UnusedProperty" NEFT=Hindistan/NEFT # suppress inspection "UnusedProperty" RTGS=Hindistan/RTGS # suppress inspection "UnusedProperty" IMPS=Hindistan/IMPS # suppress inspection "UnusedProperty" UPI=Hindistan/UPI # suppress inspection "UnusedProperty" PAYTM=Hindistan/PayTM # suppress inspection "UnusedProperty" NEQUI=Nequi # suppress inspection "UnusedProperty" BIZUM=Bizum # suppress inspection "UnusedProperty" PIX=Pix # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon Hediye Kartı # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT=Anında Kripto Para # suppress inspection "UnusedProperty" CAPITUAL=Capitual # suppress inspection "UnusedProperty" CELPAY=CelPay # suppress inspection "UnusedProperty" MONESE=Monese # suppress inspection "UnusedProperty" SATISPAY=Satispay # suppress inspection "UnusedProperty" TIKKIE=Tikkie # suppress inspection "UnusedProperty" VERSE=Verse # suppress inspection "UnusedProperty" STRIKE=Strike # suppress inspection "UnusedProperty" SWIFT=SWIFT International Wire Transfer # suppress inspection "UnusedProperty" ACH_TRANSFER=ACH Transfer # suppress inspection "UnusedProperty" DOMESTIC_WIRE_TRANSFER=Domestic Wire Transfer # suppress inspection "UnusedProperty" BSQ_SWAP=BSQ Swap # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY=OKPay # suppress inspection "UnusedProperty" CASH_APP=Cash App # suppress inspection "UnusedProperty" VENMO=Venmo PAYPAL=PayPal # suppress inspection "UnusedProperty" UPHOLD_SHORT=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM_SHORT=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY_SHORT=Popmoney # suppress inspection "UnusedProperty" REVOLUT_SHORT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY_SHORT=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY_SHORT=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY_SHORT=WeChat Pay # suppress inspection "UnusedProperty" SEPA_SHORT=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT_SHORT=SEPA Anında # suppress inspection "UnusedProperty" FASTER_PAYMENTS_SHORT=Faster Payments # suppress inspection "UnusedProperty" SWISH_SHORT=Swish # suppress inspection "UnusedProperty" ZELLE_SHORT=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY_SHORT=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER_SHORT=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH_SHORT=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS_SHORT=Kripto Paralar # suppress inspection "UnusedProperty" PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" TRANSFERWISE_USD_SHORT=Wise-USD # suppress inspection "UnusedProperty" PAYSERA_SHORT=Paysera # suppress inspection "UnusedProperty" PAXUM_SHORT=Paxum # suppress inspection "UnusedProperty" NEFT_SHORT=NEFT # suppress inspection "UnusedProperty" RTGS_SHORT=RTGS # suppress inspection "UnusedProperty" IMPS_SHORT=IMPS # suppress inspection "UnusedProperty" UPI_SHORT=UPI # suppress inspection "UnusedProperty" PAYTM_SHORT=PayTM # suppress inspection "UnusedProperty" NEQUI_SHORT=Nequi # suppress inspection "UnusedProperty" BIZUM_SHORT=Bizum # suppress inspection "UnusedProperty" PIX_SHORT=Pix # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon Hediye Kartı # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT_SHORT=Anında Kripto Para # suppress inspection "UnusedProperty" CAPITUAL_SHORT=Capitual # suppress inspection "UnusedProperty" CELPAY_SHORT=CelPay # suppress inspection "UnusedProperty" MONESE_SHORT=Monese # suppress inspection "UnusedProperty" SATISPAY_SHORT=Satispay # suppress inspection "UnusedProperty" TIKKIE_SHORT=Tikkie # suppress inspection "UnusedProperty" VERSE_SHORT=Verse # suppress inspection "UnusedProperty" STRIKE_SHORT=Strike # suppress inspection "UnusedProperty" SWIFT_SHORT=SWIFT # suppress inspection "UnusedProperty" ACH_TRANSFER_SHORT=ACH # suppress inspection "UnusedProperty" DOMESTIC_WIRE_TRANSFER_SHORT=Domestic Wire # suppress inspection "UnusedProperty" BSQ_SWAP_SHORT=BSQ Swap # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY_SHORT=OKPay # suppress inspection "UnusedProperty" CASH_APP_SHORT=Cash App # suppress inspection "UnusedProperty" VENMO_SHORT=Venmo PAYPAL_SHORT=PayPal #################################################################### # Validation #################################################################### validation.empty=Boş girişe izin verilmez. validation.NaN=Giriş geçerli bir sayı değil. validation.notAnInteger=Giriş bir tam sayı değeri değil. validation.zero=0 değeri girişi izin verilmez. validation.negative=Negatif bir değere izin verilmez. validation.traditional.tooSmall=Minimum mümkün miktardan küçük girişe izin verilmez. validation.traditional.tooLarge=Maksimum mümkün miktardan büyük girişe izin verilmez. validation.xmr.fraction=Giriş, 1 satoshiden daha az bir monero değerine yol açacak validation.xmr.tooLarge={0} değerinden büyük girişe izin verilmez. validation.xmr.tooSmall={0} değerinden küçük girişe izin verilmez. validation.passwordTooShort=Girdiğiniz şifre çok kısa. En az 8 karakter olmalıdır. validation.passwordTooLong=Girdiğiniz şifre çok uzun. 50 karakterden uzun olamaz. validation.sortCodeNumber={0} {1} rakamdan oluşmalıdır. validation.sortCodeChars={0} {1} karakterden oluşmalıdır. validation.bankIdNumber={0} {1} rakamdan oluşmalıdır. validation.accountNr=Hesap numarası {0} rakamdan oluşmalıdır. validation.accountNrChars=Hesap numarası {0} karakterden oluşmalıdır. validation.xmr.invalidAddress=Adres doğru değil. Lütfen adres formatını kontrol edin. validation.integerOnly=Lütfen sadece tam sayılar girin. validation.inputError=Girdiğiniz bir hata oluşturdu:\n{0} validation.xmr.exceedsMaxTradeLimit=İşlem limitiniz {0}. validation.nationalAccountId={0} {1} rakamdan oluşmalıdır. #new validation.invalidInput=Geçersiz giriş: {0} validation.accountNrFormat=Hesap numarası şu formatta olmalıdır: {0} # suppress inspection "UnusedProperty" validation.crypto.wrongStructure=Adres doğrulaması, {0} adres yapısına uymadığı için başarısız oldu. # suppress inspection "UnusedProperty" validation.crypto.ltz.zAddressesNotSupported=LTZ adresi L ile başlamalıdır. z ile başlayan adresler desteklenmez. # suppress inspection "UnusedProperty" validation.crypto.zAddressesNotSupported=ZEC adresleri t ile başlamalıdır. z ile başlayan adresler desteklenmez. # suppress inspection "UnusedProperty" validation.crypto.invalidAddress=Adres geçerli bir {0} adresi değil! {1} # suppress inspection "UnusedProperty" validation.crypto.liquidBitcoin.invalidAddress=Yerel segwit adresleri ('lq' ile başlayanlar) desteklenmez. validation.bic.invalidLength=Giriş uzunluğu 8 veya 11 olmalıdır. validation.bic.letters=Banka ve Ülke kodu harflerden oluşmalıdır. validation.bic.invalidLocationCode=BIC geçersiz konum kodu içeriyor. validation.bic.invalidBranchCode=BIC geçersiz şube kodu içeriyor. validation.bic.sepaRevolutBic=Revolut Sepa hesapları desteklenmez. validation.btc.invalidFormat=Bitcoin adresi için geçersiz format. validation.email.invalidAddress=Geçersiz adres. validation.iban.invalidCountryCode=Ülke kodu geçersiz. validation.iban.checkSumNotNumeric=Kontrol numarası sayısal olmalıdır. validation.iban.nonNumericChars=Alfasayısal olmayan karakter tespit edildi. validation.iban.checkSumInvalid=IBAN kontrol numarası geçersiz. validation.iban.invalidLength=Numara 15 ile 34 karakter arasında olmalıdır. validation.iban.sepaNotSupported=SEPA bu ülkede desteklenmiyor. validation.interacETransfer.invalidAreaCode=Kanada dışı alan kodu. validation.interacETransfer.invalidPhone=Lütfen geçerli bir 11 haneli telefon numarası (ör: 1-123-456-7890) veya bir e-posta adresi girin. validation.interacETransfer.invalidQuestion=Sadece harfler, sayılar, boşluklar ve/veya ' _ , . ? - ' sembollerini içermelidir. validation.interacETransfer.invalidAnswer=Tek kelime olmalı ve yalnızca harfler, sayılar ve/veya '-' sembolünü içermelidir. validation.inputTooLarge=Giriş {0}'den büyük olmamalıdır. validation.inputTooSmall=Giriş {0}'den büyük olmalıdır. validation.inputToBeAtLeast=Giriş en az {0} olmalıdır. validation.amountBelowDust=Toz limitinin altında bir miktar olan {0} satoshi'ye izin verilmez. validation.length=Uzunluk {0} ile {1} arasında olmalıdır. validation.fixedLength=Uzunluk {0} olmalıdır. validation.pattern=Giriş şu formatta olmalıdır: {0} validation.noHexString=Giriş HEX formatında değil. validation.advancedCash.invalidFormat=Geçerli bir e-posta veya şu formatta cüzdan kimliği olmalıdır: X000000000000 validation.invalidUrl=Geçersiz URL. validation.mustBeDifferent=Girişiniz mevcut değerden farklı olmalıdır. validation.cannotBeChanged=Parametre değiştirilemez. validation.numberFormatException=Sayı formatı hatası {0} validation.mustNotBeNegative=Giriş negatif olmamalıdır. validation.phone.missingCountryCode=Telefon numarasını doğrulamak için iki harfli ülke kodu gerekli. validation.phone.invalidCharacters=Telefon numarası {0} geçersiz karakterler içeriyor. validation.phone.insufficientDigits={0}'da geçerli bir telefon numarası olacak kadar yeterli basamak yok. validation.phone.tooManyDigits={0}'da geçerli bir telefon numarası için fazla basamak var. validation.phone.invalidDialingCode=Numara {0} için ülke alan kodu ülke {1} için geçersiz. \ Doğru alan kodu {2}. validation.invalidAddressList=Geçerli adreslerin virgülle ayrılmış listesi olmalıdır. validation.capitual.invalidFormat=Geçerli bir CAP kodu şu formatta olmalıdır: CAP-XXXXXX (6 alfasayısal karakter). ================================================ FILE: core/src/main/resources/i18n/displayStrings_vi.properties ================================================ # Keep display strings organized by domain # Naming convention: We use camelCase and dot separated name spaces. # Use as many sub spaces as required to make the structure clear, but as little as possible. # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break # in the display but only in the editor. # Please use in all language files the exact same order of the entries, that way a comparison is easier. # Please try to keep the length of the translated string similar to English. If it is longer it might break layout or # get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. #################################################################### # Shared #################################################################### shared.readMore=Đọc thêm shared.openHelp=Mở Trợ giúp shared.warning=Cảnh báo shared.close=Đóng shared.cancel=Hủy shared.ok=OK shared.yes=Có shared.no=Không shared.iUnderstand=Tôi hiểu shared.na=Không áp dụng shared.shutDown=Đóng shared.reportBug=Report bug on GitHub shared.buyMonero=Mua monero shared.sellMonero=Bán monero shared.buyCurrency=Mua {0} shared.sellCurrency=Bán {0} shared.buyCurrency.locked=Mua {0} 🔒 shared.sellCurrency.locked=Bán {0} 🔒 shared.buyingXMRWith=đang mua XMR với {0} shared.sellingXMRFor=đang bán XMR với {0} shared.buyingCurrency=đang mua {0} (đang bán XMR) shared.sellingCurrency=đang bán {0} (đang mua XMR) shared.buy=mua shared.sell=bán shared.buying=đang mua shared.selling=đang bán shared.P2P=P2P shared.oneOffer=chào giá shared.multipleOffers=nhiều chào giá shared.Offer=Chào giá shared.offerVolumeCode={0} Offer Volume shared.openOffers=Các lệnh đang mở shared.trade=giao dịch shared.trades=nhiều giao dịch shared.openTrades=Các giao dịch mở shared.dateTime=Ngày/Giờ shared.price=Giá shared.priceWithCur=Giá bằng {0} shared.priceInCurForCur=Giá bằng {0} với 1 {1} shared.fixedPriceInCurForCur=Giá cố định bằng {0} với 1 {1} shared.amount=Số tiền shared.txFee=Transaction Fee shared.tradeFee=Trade Fee shared.buyerSecurityDeposit=Buyer Deposit shared.sellerSecurityDeposit=Seller Deposit shared.amountWithCur=Thành tiền bằng {0} shared.volumeWithCur=Khối lượng bằng {0} shared.currency=Tiền tệ shared.market=Thị trường shared.deviation=Deviation shared.paymentMethod=Hình thức thanh toán shared.tradeCurrency=Loại tiền tệ giao dịch shared.offerType=Loại chào giá shared.details=Thông tin chi tiết shared.address=Địa chỉ shared.balanceWithCur=Số dư bằng {0} shared.utxo=Unspent transaction output shared.txId=ID giao dịch shared.confirmations=Xác nhận shared.revert=Khôi phục Tx shared.select=Chọn shared.usage=Sử dụng shared.state=Trạng thái shared.tradeId=ID giao dịch shared.offerId=ID chào giá shared.bankName=Tên ngân hàng shared.acceptedBanks=Các NH được chấp nhận shared.amountMinMax=Số tiền (min - max) shared.amountHelp=Nếu mức chào giá được đặt mức tối thiểu và tối đa, bạn có thể giao dịch với bất kỳ số tiền nào trong phạm vi này shared.remove=Xoá shared.goTo=Đi đến {0} shared.XMRMinMax=XMR (min - max) shared.removeOffer=Bỏ chào giá shared.dontRemoveOffer=Không được bỏ chào giá shared.editOffer=Chỉnh sửa chào giá shared.openLargeQRWindow=Open large QR code window shared.tradingAccount=Tài khoản giao dịch shared.faq=Visit FAQ page shared.yesCancel=Có, hủy shared.nextStep=Bước tiếp theo shared.selectTradingAccount=Chọn tài khoản giao dịch shared.fundFromSavingsWalletButton=Áp dụng tiền từ ví Haveno shared.fundFromExternalWalletButton=Mở ví ngoài để nộp tiền shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? shared.belowInPercent=Thấp hơn % so với giá thị trường shared.aboveInPercent=Cao hơn % so với giá thị trường shared.enterPercentageValue=Nhập giá trị % shared.OR=HOẶC shared.notEnoughFunds=You don't have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. shared.waitingForFunds=Đợi nộp tiền... shared.TheXMRBuyer=Người mua XMR shared.You=Bạn shared.sendingConfirmation=Gửi xác nhận... shared.sendingConfirmationAgain=Hãy gửi lại xác nhận shared.exportCSV=Export to CSV shared.exportJSON=Truy xuất ra JSON shared.summary=Show summary shared.noDateAvailable=Ngày tháng không hiển thị shared.noDetailsAvailable=Không có thông tin shared.notUsedYet=Chưa được sử dụng shared.date=Ngày shared.sendFundsDetailsWithFee=Đang gửi: {0}\n\nĐến địa chỉ nhận: {1}\n\nPhí thợ đào bổ sung: {2}\n\nBạn có chắc chắn muốn gửi số tiền này không? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Sao chép đến clipboard shared.language=Ngôn ngữ shared.country=Quốc gia shared.applyAndShutDown=Áp dụng và tắt shared.selectPaymentMethod=Chọn phương thức thanh toán shared.accountNameAlreadyUsed=That account name is already used for another saved account.\nPlease choose another name. shared.askConfirmDeleteAccount=Bạn có thực sự muốn xóa tài khoản được chọn không? shared.cannotDeleteAccount=You cannot delete that account because it is being used in an open offer (or in an open trade). shared.noAccountsSetupYet=Chưa có tài khoản nào được thiết lập shared.manageAccounts=Quản lý tài khoản shared.addNewAccount=Thêm tài khoản mới shared.ExportAccounts=Truy xuất tài khoản shared.importAccounts=Truy nhập tài khoản shared.createNewAccount=Tạo tài khoản mới shared.createNewAccountDescription=Thông tin tài khoản của bạn được lưu trữ cục bộ trên thiết bị của bạn và chỉ được chia sẻ với đối tác giao dịch của bạn và trọng tài nếu xảy ra tranh chấp. shared.saveNewAccount=Lưu tài khoản mới shared.selectedAccount=Tài khoản được chọn shared.deleteAccount=Xóa tài khoản shared.errorMessageInline=\nThông báo lỗi: {0} shared.errorMessage=Thông báo lỗi shared.information=Thông tin shared.name=Tên shared.id=ID shared.dashboard=Bảng thống kê shared.accept=Chấp nhận shared.balance=Số dư shared.save=Lưu shared.onionAddress=Địa chỉ Onion shared.supportTicket=vé hỗ trợ shared.dispute=tranh chấp shared.mediationCase=mediation case shared.seller=người bán shared.buyer=người mua shared.allEuroCountries=Tất cả các nước Châu ÂU shared.acceptedTakerCountries=Các quốc gia tiếp nhận được chấp nhận shared.tradePrice=Giá giao dịch shared.tradeAmount=Khoản tiền giao dịch shared.tradeVolume=Khối lượng giao dịch shared.invalidKey=Mã khóa bạn vừa nhập không đúng. shared.enterPrivKey=Nhập Private key để mở khóa shared.payoutTxId=ID giao dịch chi trả shared.contractAsJson=Hợp đồng định dạng JSON shared.viewContractAsJson=Xem hợp đồng định dạng JSON shared.contract.title=Hợp đồng giao dịch có ID: {0} shared.paymentDetails=Thông tin thanh toán XMR {0} shared.securityDeposit=Tiền đặt cọc shared.yourSecurityDeposit=Tiền đặt cọc của bạn shared.contract=Hợp đồng shared.messageArrived=Tin nhắn đến. shared.messageStoredInMailbox=Tin nhắn lưu trong hộp thư. shared.messageSendingFailed=Tin nhắn chưa gửi được. Lỗi: {0} shared.unlock=Mở khóa shared.toReceive=nhận shared.toSpend=chi shared.xmrAmount=Số lượng XMR shared.yourLanguage=Ngôn ngữ của bạn shared.addLanguage=Thêm ngôn ngữ shared.total=Tổng shared.totalsNeeded=Số tiền cần shared.tradeWalletAddress=Địa chỉ ví giao dịch shared.tradeWalletBalance=Số dư ví giao dịch shared.reserveExactAmount=Chỉ giữ lại số tiền cần thiết. Yêu cầu phí khai thác và khoảng 20 phút trước khi đề nghị của bạn được công khai. shared.makerTxFee=Người tạo: {0} shared.takerTxFee=Người nhận: {0} shared.iConfirm=Tôi xác nhận shared.openURL=Mở {0} shared.fiat=Tiền pháp định shared.crypto=Tiền mã hóa shared.preciousMetals=Kim loại quý shared.all=Tất cả shared.edit=Chỉnh sửa shared.advancedOptions=Tùy chọn nâng cao shared.interval=Khoảng thời gian shared.actions=Hoạt động shared.buyerUpperCase=Người mua shared.sellerUpperCase=Người bán shared.new=MỚI shared.learnMore=Tìm hiểu thêm shared.dismiss=Hủy shared.selectedArbitrator=Trọng tài được chọn shared.selectedMediator=Selected mediator shared.selectedRefundAgent=Trọng tài được chọn shared.mediator=Người hòa giải shared.arbitrator=Trọng tài shared.refundAgent=Trọng tài shared.refundAgentForSupportStaff=Refund agent shared.delayedPayoutTxId=Delayed payout transaction ID shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later. shared.numItemsLabel=Number of entries: {0} shared.filter=Bộ lọc shared.enabled=Enabled #################################################################### # UI views #################################################################### #################################################################### # MainView #################################################################### mainView.menu.market=Thị trường mainView.menu.buyXmr=Mua XMR mainView.menu.sellXmr=Bán XMR mainView.menu.portfolio=Danh Mục mainView.menu.funds=Số tiền mainView.menu.support=Hỗ trợ mainView.menu.settings=Cài đặt mainView.menu.account=Tài khoản mainView.marketPriceWithProvider.label=Giá thị trường theo {0} mainView.marketPrice.havenoInternalPrice=Giá giao dịch Haveno gần nhất mainView.marketPrice.tooltip.havenoInternalPrice=Không có giá thị trường từ nhà cung cấp bên ngoài.\nGiá hiển thị là giá giao dịch Haveno gần nhất với đồng tiền này. mainView.marketPrice.tooltip=Giá thị trường được cung cấp bởi {0}{1}\nCập nhật mới nhất: {2}\nURL nút nhà cung cấp: {3} mainView.balance.available=Số dư hiện có mainView.balance.reserved=Phần được bảo lưu trong báo giá mainView.balance.pending=Khóa trong giao dịch mainView.balance.reserved.short=Bảo lưu mainView.balance.pending.short=Bị khóa mainView.footer.usingTor=(via Tor) mainView.footer.localhostMoneroNode=(Máy chủ nội bộ) mainView.footer.clearnet=(via clearnet) mainView.footer.xmrInfo={0} {1} mainView.footer.xmrFeeRate=/ Fee rate: {0} sat/vB mainView.footer.xmrInfo.initializing=Đang kết nối với mạng Haveno mainView.footer.xmrInfo.synchronizingWith=Đang đồng bộ với {0} tại khối: {1} / {2} mainView.footer.xmrInfo.synchronizingWalletWith=Đang đồng bộ hóa ví với {0} tại khối: {1} / {2} mainView.footer.xmrInfo.connectedTo=Kết nối với {0} tại khối {1} mainView.footer.xmrInfo.syncedWith=Đã đồng bộ với {0} tại khối {1} mainView.footer.xmrInfo.connectingTo=Đang kết nối với mainView.footer.xmrInfo.connectionFailed=Connection failed to mainView.footer.xmrPeers=Monero network peers: {0} mainView.footer.p2pPeers=Haveno network peers: {0} mainView.bootstrapState.connectionToTorNetwork=(1/4) Kết nối với mạng ... mainView.bootstrapState.torNodeCreated=(2/4) Nút Tor được tạo mainView.bootstrapState.hiddenServicePublished=(3/4) Dịch vụ ẩn được công bố mainView.bootstrapState.initialDataReceived=(4/4) Nhận dữ liệu ban đầu mainView.bootstrapWarning.noSeedNodesAvailable=Không có seed nodes khả dụng mainView.bootstrapWarning.noNodesAvailable=Không có seed nodes và đối tác ngang hàng khả dụng mainView.bootstrapWarning.bootstrappingToP2PFailed=Bootstrapping to Haveno network failed mainView.p2pNetworkWarnMsg.noNodesAvailable=Không có seed nodes hay đối tác ngang hàng để yêu cầu dữ liệu.\nVui lòng kiểm tra kết nối internet hoặc thử khởi động lại ứng dụng. mainView.p2pNetworkWarnMsg.connectionToP2PFailed=Connecting to the Haveno network failed (reported error: {0}).\nPlease check your internet connection or try to restart the application. mainView.walletServiceErrorMsg.timeout=Kết nối tới mạng Monero không thành công do hết thời gian chờ. mainView.walletServiceErrorMsg.connectionError=Kết nối tới mạng Monero không thành công do lỗi: {0} mainView.walletServiceErrorMsg.rejectedTxException=A transaction was rejected from the network.\n\n{0} mainView.networkWarning.allConnectionsLost=Mất kết nối tới tất cả mạng ngang hàng {0}.\nCó thể bạn mất kết nối internet hoặc máy tính đang ở chế độ standby. mainView.networkWarning.localhostMoneroLost=Mất kết nối tới nút Monero máy chủ nội bộ.\nVui lòng khởi động lại ứng dụng Haveno để nối với nút Monero khác hoặc khởi động lại nút Monero máy chủ nội bộ. mainView.version.update=(Có cập nhật) #################################################################### # MarketView #################################################################### market.tabs.offerBook=Danh mục chào giá market.tabs.spreadCurrency=Offers by Currency market.tabs.spreadPayment=Offers by Payment Method market.tabs.trades=Các giao dịch # OfferBookChartView market.offerBook.buyCrypto=Mua {0} (bán {1}) market.offerBook.sellCrypto=Bán {0} (mua {1}) market.offerBook.buyWithTraditional=Mua {0} market.offerBook.sellWithTraditional=Bán {0} market.offerBook.sellOffersHeaderLabel=Bán {0} cho market.offerBook.buyOffersHeaderLabel=Mua {0} từ market.offerBook.buy=Tôi muốn mua monero market.offerBook.sell=Tôi muốn bán monero # SpreadView market.spread.numberOfOffersColumn=Tất cả chào giá ({0}) market.spread.numberOfBuyOffersColumn=Mua XMR ({0}) market.spread.numberOfSellOffersColumn=Bán XMR ({0}) market.spread.totalAmountColumn=Tổng số XMR ({0}) market.spread.spreadColumn=Chênh lệch giá market.spread.expanded=Expanded view # TradesChartsView market.trades.nrOfTrades=Các giao dịch: {0} market.trades.tooltip.volumeBar=Volume: {0} / {1}\nNo. of trades: {2}\nDate: {3} market.trades.tooltip.candle.open=Mở: market.trades.tooltip.candle.close=Đóng: market.trades.tooltip.candle.high=Cao: market.trades.tooltip.candle.low=Thấp: market.trades.tooltip.candle.average=Trung bình: market.trades.tooltip.candle.median=Median: market.trades.tooltip.candle.date=Ngày: market.trades.showVolumeInUSD=Show volume in USD #################################################################### # OfferView #################################################################### offerbook.createOffer=Tạo chào giá offerbook.takeOffer=Nhận chào giá offerbook.takeOfferToBuy=Nhận chào giá mua {0} offerbook.takeOfferToSell=Nhận chào giá bán {0} offerbook.takeOffer.enterChallenge=Nhập mật khẩu đề nghị offerbook.trader=Trader offerbook.offerersBankId=ID ngân hàng của người tạo (BIC/SWIFT): {0} offerbook.offerersBankName=Tên ngân hàng của người tạo: {0} offerbook.offerersBankSeat=Quốc gia có ngân hàng của người tạo: {0} offerbook.offerersAcceptedBankSeatsEuro=Các quốc gia có ngân hàng được chấp thuận (người nhận): Tất cả các nước Châu Âu offerbook.offerersAcceptedBankSeats=Các quốc gia có ngân hàng được chấp thuận (người nhận):\n {0} offerbook.availableOffers=Các chào giá hiện có offerbook.filterByCurrency=Lọc theo tiền tệ offerbook.filterByPaymentMethod=Lọc theo phương thức thanh toán offerbook.matchingOffers=Các ưu đãi phù hợp với tài khoản của tôi offerbook.filterNoDeposit=Không đặt cọc offerbook.noDepositOffers=Các ưu đãi không yêu cầu đặt cọc (cần mật khẩu) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info=This account was verified and {0} offerbook.timeSinceSigning.info.arbitrator=signed by an arbitrator and can sign peer accounts offerbook.timeSinceSigning.info.peer=signed by a peer, waiting %d days for limits to be lifted offerbook.timeSinceSigning.info.peerLimitLifted=signed by a peer and limits were lifted offerbook.timeSinceSigning.info.signer=signed by peer and can sign peer accounts (limits lifted) offerbook.timeSinceSigning.info.banned=account was banned offerbook.timeSinceSigning.daysSinceSigning={0} ngày offerbook.timeSinceSigning.daysSinceSigning.long={0} since signing offerbook.xmrAutoConf=Is auto-confirm enabled offerbook.buyXmrWith=Mua XMR với: offerbook.sellXmrFor=Bán XMR để: offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers' payment accounts. offerbook.timeSinceSigning.notSigned=Not signed yet offerbook.timeSinceSigning.notSigned.ageDays={0} ngày offerbook.timeSinceSigning.notSigned.noNeed=Không áp dụng shared.notSigned=This account has not been signed yet and was created {0} days ago shared.notSigned.noNeed=This account type does not require signing shared.notSigned.noNeedDays=This account type does not require signing and was created {0} days ago shared.notSigned.noNeedAlts=Crypto accounts do not feature signing or aging offerbook.nrOffers=Số chào giá: {0} offerbook.volume={0} (min - max) offerbook.deposit=Deposit XMR (%) offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed. offerbook.createNewOffer=Tạo ưu đãi cho {0} {1} offerbook.createOfferToBuy=Tạo chào giá mua mới {0} offerbook.createOfferToSell=Tạo chào giá bán mới {0} offerbook.createOfferToBuy.withTraditional=Tạo chào giá mua {0} bằng {1} offerbook.createOfferToSell.forTraditional=Tạo chào giá bán {0} lấy {1} offerbook.createOfferToBuy.withCrypto=Tạo chào giá bán {0} (mua {1}) offerbook.createOfferToSell.forCrypto=Tạo chào giá mua {0} (bán {1}) offerbook.takeOfferButton.tooltip=Nhận chào giá cho {0} offerbook.yesCreateOffer=Vâng, tạo lệnh offerbook.setupNewAccount=Thiết lập tài khoản giao dịch mới offerbook.removeOffer.success=Xoá nh thành công. offerbook.removeOffer.failed=Xoá lệnh không thành công:\n{0} offerbook.deactivateOffer.failed=Huỷ kích hoạt chào giá không thành công:\n{0} offerbook.activateOffer.failed=Công bố lệnh không thành công:\n{0} offerbook.withdrawFundsHint=Bạn có thể rút bạn đã thanh toán từ màn hình {0}. offerbook.warning.noTradingAccountForCurrency.headline=No payment account for selected currency offerbook.warning.noTradingAccountForCurrency.msg=You don't have a payment account set up for the selected currency.\n\nWould you like to create an offer for another currency instead? offerbook.warning.noMatchingAccount.headline=No matching payment account. offerbook.warning.noMatchingAccount.msg=This offer uses a payment method you haven't set up yet. \n\nWould you like to set up a new payment account now? offerbook.warning.counterpartyTradeRestrictions=This offer cannot be taken due to counterparty trade restrictions offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\nAfter successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\nFor more information on account signing, please see the documentation at [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- The buyer's account has not been signed by an arbitrator or a peer\n- The time since signing of the buyer's account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- Your account has not been signed by an arbitrator or a peer\n- The time since signing of your account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=Phương thức thanh toán này tạm thời chỉ được giới hạn đến {0} cho đến {1} vì tất cả các người mua đều có tài khoản mới.\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=Đề nghị của bạn sẽ bị giới hạn chỉ đối với các người mua có tài khoản đã ký và có tuổi vì nó vượt quá {0}.\n\n{1} offerbook.warning.wrongTradeProtocol=Lệnh này cần phiên bản giao thức khác với được sử dụng trong phiên bản phần mềm của bạn.\n\nHãy kiểm tra xem bạn đã cài đặt phiên bản mới nhất chưa, nếu không người dùng đã tạo lệnh đã sử dụng phiên bản cũ.\n\nNgười dùng không thể giao dịch với phiên bản giao thức giao dịch không tương thích. offerbook.warning.userIgnored=Bạn đã thêm địa chỉ onion của người dùng vào danh sách bỏ qua. offerbook.warning.offerBlocked=Báo giá này bị chặn bởi các lập trình viên Haveno.\nCó thể có sự cố chưa được xử lý dẫn tới vấn đề khi nhận lệnh này. offerbook.warning.currencyBanned=Loại tiền sử dụng trong báo giá này bị chặn bởi các lập trình viên Haveno.\nTruy cập diễn đàn Haveno để biết thêm thông tin. offerbook.warning.paymentMethodBanned=Phương thức thanh toán sử dụng trong báo giá này bị chặn bởi các lập trình viên Haveno.\nTruy cập diễn đàn Haveno để biết thêm thông tin. offerbook.warning.nodeBlocked=Địa chỉ onion của Thương gia bị chặn bởi các lập trình viên Haveno.\nCó thể có sự cố chưa được xử lý dẫn tới vấn đề khi nhận báo giá từ Thương gia này. offerbook.warning.offerWasAlreadyUsedInTrade=You cannot take this offer because you already took it earlier. It could be that your previous take-offer attempt resulted in a failed trade. offerbook.info.sellAtMarketPrice=Bạn sẽ bán với giá thị trường (cập nhật mỗi phút). offerbook.info.buyAtMarketPrice=Bạn sẽ mua với giá thị trường (cập nhật mỗi phút). offerbook.info.sellBelowMarketPrice=Bạn sẽ nhận {0} thấp hơn so với giá thị trường hiện tại (cập nhật mỗi phút). offerbook.info.buyAboveMarketPrice=Bạn sẽ trả {0} cao hơn so với giá thị trường hiện tại (cập nhật mỗi phút). offerbook.info.sellAboveMarketPrice=Bạn sẽ nhận {0} cao hơn so với giá thị trường hiện tại (cập nhật mỗi phút). offerbook.info.buyBelowMarketPrice=Bạn sẽ trả {0} thấp hơn so với giá thị trường hiện tại (cập nhật mỗi phút). offerbook.info.buyAtFixedPrice=Bạn sẽ mua với giá cố định này. offerbook.info.sellAtFixedPrice=Bạn sẽ bán với giá cố định này. offerbook.info.noArbitrationInUserLanguage=Trong trường hợp có tranh chấp, xin lưu ý rằng việc xử lý tranh chấp sẽ được thực hiện bằng tiếng{0}. Ngôn ngữ hiện tại là tiếng{1}. offerbook.info.roundedFiatVolume=Số lượng đã được làm tròn để tăng tính bảo mật cho giao dịch #################################################################### # Offerbook / Create offer #################################################################### createOffer.amount.prompt=Nhập số tiền bằng XMR createOffer.price.prompt=Nhập giá createOffer.volume.prompt=Nhập số tiền bằng {0} createOffer.amountPriceBox.amountDescription=Số tiền XMR đến {0} createOffer.amountPriceBox.buy.volumeDescription=Số tiền {0} để chi trả createOffer.amountPriceBox.sell.volumeDescription=Số tiền {0} để nhận createOffer.amountPriceBox.minAmountDescription=Số tiền XMR nhỏ nhất createOffer.securityDeposit.prompt=Tiền đặt cọc createOffer.fundsBox.title=Nộp tiền cho chào giá của bạn createOffer.fundsBox.offerFee=Phí giao dịch createOffer.fundsBox.networkFee=Phí đào createOffer.fundsBox.placeOfferSpinnerInfo=Báo giá đang được công bố createOffer.fundsBox.paymentLabel=giao dịch Haveno với ID {0} createOffer.fundsBox.fundsStructure=({0} tiền đặt cọc, {1} phí giao dịch, {2} phí đào) createOffer.success.headline=Đề nghị của bạn đã được tạo ra createOffer.success.info=Bạn có thể quản lý báo giá hiện hành của bạn tại \"Portfolio/ báo giá hiện tại của bạn\". createOffer.info.sellAtMarketPrice=Bạn sẽ luôn bán với giá thị trường vì báo giá của bạn sẽ luôn được cập nhật. createOffer.info.buyAtMarketPrice=Bạn sẽ luôn mua với giá thị trường vì báo giá của bạn sẽ luôn được cập nhật. createOffer.info.sellAboveMarketPrice=Bạn sẽ luôn nhận {0}% cao hơn so với giá thị trường hiện tại vì báo giá của bạn sẽ luôn được cập nhật. createOffer.info.buyBelowMarketPrice=Bạn sẽ luôn trả {0}% thấp hơn so với giá thị trường hiện tại vì báo giá của bạn sẽ luôn được cập nhật. createOffer.warning.sellBelowMarketPrice=Bạn sẽ luôn nhận {0}% thấp hơn so với giá thị trường hiện tại vì báo giá của bạn sẽ luôn được cập nhật. createOffer.warning.buyAboveMarketPrice=Bạn sẽ luôn trả {0}% cao hơn so với giá thị trường hiện tại vì báo giá của bạn sẽ luôn được cập nhật. createOffer.tradeFee.descriptionXMROnly=Phí giao dịch createOffer.tradeFee.descriptionBSQEnabled=Chọn loại tiền trả phí giao dịch createOffer.triggerPrice.prompt=Set optional trigger price createOffer.triggerPrice.label=Deactivate offer if market price is {0} createOffer.triggerPrice.tooltip=As protection against drastic price movements you can set a trigger price which deactivates the offer if the market price reaches that value. createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0} createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0} # new entries createOffer.placeOfferButton.buy=Xem lại: Tạo đề nghị mua XMR bằng {0} createOffer.placeOfferButton.sell=Xem lại: Tạo đề nghị bán XMR lấy {0} createOffer.createOfferFundWalletInfo.headline=Nộp tiền cho báo giá của bạn # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Khoản tiền giao dịch: {0} \n createOffer.createOfferFundWalletInfo.msg=Bạn cần nạp {0} cho lời đề nghị này.\n\n\ Số tiền này sẽ được giữ trong ví cục bộ của bạn và sẽ được khóa vào ví multisig ngay khi có người chấp nhận lời đề nghị của bạn.\n\n\ Số tiền bao gồm:\n\ {1}\ - Tiền đặt cọc bảo đảm của bạn: {2}\n\ - Phí giao dịch: {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=Có lỗi xảy ra khi đặt chào giá:\n\n{0}\n\nKhông còn tiền trong ví của bạn.\nHãy khởi động lại ứng dụng và kiểm tra kết nối mạng. createOffer.setAmountPrice=Đặt số tiền và giá createOffer.warnCancelOffer=Bạn đã tài trợ cho đề xuất này rồi. Nếu bạn hủy bây giờ, số tiền của bạn sẽ tiếp tục ở trong ví Haveno địa phương của bạn và sẵn sàng để rút tại màn hình "Vốn/Gửi vốn". Bạn có chắc chắn muốn hủy không? createOffer.timeoutAtPublishing=Lỗi hết thời gian xảy ra khi công bố lệnh createOffer.errorInfo=\n\nPhí người tạo đã được thanh toán. Trong trường hợp xấu nhất bạn mất phí này.\nHãy khởi động lại ứng dụng và kiểm tra kết nối mạng để xem bạn có thể xử lý vấn đề này không. createOffer.tooLowSecDeposit.warning=Bạn đã cài đặt tiền gửi đặt cọc thấp hơn giá trị mặc định khuyến cáo {0}.\nBạn có chắc bạn muốn sử dụng tiền gửi đặt cọc thấp hơn không? createOffer.tooLowSecDeposit.makerIsSeller=Bạn ít được bảo vệ hơn khi đối tác giao dịch không tuân thủ giao thức giao dịch. createOffer.tooLowSecDeposit.makerIsBuyer=Đối tác giao dịch ít được bảo vệ hơn khi bạn tuân thủ giao thức giao dịch vì bạn có ít tiền đặt cọc chịu rủi ro. Người dùng khác có thể muốn nhận chào giá khác thay cho chào giá của bạn. createOffer.resetToDefault=Không, cài đặt lại giá trị mặc định createOffer.useLowerValue=Vâng, sử dụng giá trị thấp hơn createOffer.priceOutSideOfDeviation=Giá bạn vừa nhập ngoài sai lệch cho phép tối đa so với giá thị trường.\nSai lệch cho phép tối đa là {0} và có thể điều chỉnh trong quyền ưu tiên. createOffer.changePrice=Thay đổi giá createOffer.tac=Với việc công bố chào giá này, tôi đồng ý giao dịch với bất cứ Thương gia nào đáp ứng các điều kiện nêu rõ trên màn hình này. createOffer.currencyForFee=Phí giao dịch createOffer.setDeposit=Cài đặt tiền đặt cọc của người mua (%) createOffer.setDepositAsBuyer=Cài đặt tiền đặt cọc của tôi với vai trò người mua (%) createOffer.setDepositForBothTraders=Set both traders' security deposit (%) createOffer.securityDepositInfo=Số tiền đặt cọc cho người mua của bạn sẽ là {0} createOffer.securityDepositInfoAsBuyer=Số tiền đặt cọc của bạn với vai trò người mua sẽ là {0} createOffer.minSecurityDepositUsed=Khoản tiền đặt cọc bảo mật tối thiểu được sử dụng createOffer.buyerAsTakerWithoutDeposit=Không cần đặt cọc từ người mua (được bảo vệ bằng mật khẩu) createOffer.myDeposit=Tiền đặt cọc bảo mật của tôi (%) createOffer.myDepositInfo=Khoản tiền đặt cọc của bạn sẽ là {0} #################################################################### # Offerbook / Take offer #################################################################### takeOffer.amount.prompt=Nhập giá trị bằng XMR takeOffer.amountPriceBox.buy.amountDescription=Số lượng XMR bán takeOffer.amountPriceBox.sell.amountDescription=Số lượng XMR mua takeOffer.amountPriceBox.priceDescription=Giá mỗi monero bằng {0} takeOffer.amountPriceBox.amountRangeDescription=Số lượng khả dụng takeOffer.amountPriceBox.warning.invalidXmrDecimalPlaces=Giá trị bạn vừa nhập vượt quá số ký tự thập phân cho phép.\nGiá trị phải được điều chỉnh về 4 số thập phân. takeOffer.validation.amountSmallerThanMinAmount=Số tiền không được nhỏ hơn giá trị nhỏ nhất của lệnh. takeOffer.validation.amountLargerThanOfferAmount=Số tiền nhập không được cao hơn số tiền cao nhất của lệnh takeOffer.validation.amountLargerThanOfferAmountMinusFee=Giá trị nhập này sẽ làm thay đổi đối với người bán XMR. takeOffer.fundsBox.title=Nộp tiền cho giao dịch của bạn takeOffer.fundsBox.isOfferAvailable=Kiểm tra xem có chào giá không ... takeOffer.fundsBox.tradeAmount=Số tiền để bán takeOffer.fundsBox.offerFee=Phí giao dịch takeOffer.fundsBox.networkFee=Tổng phí đào takeOffer.fundsBox.takeOfferSpinnerInfo=Chấp nhận đề xuất: {0} takeOffer.fundsBox.paymentLabel=giao dịch Haveno có ID {0} takeOffer.fundsBox.fundsStructure=({0} tiền gửi đại lý, {1} phí giao dịch, {2} phí đào) takeOffer.fundsBox.noFundingRequiredTitle=Không cần tài trợ takeOffer.fundsBox.noFundingRequiredDescription=Lấy mật khẩu giao dịch từ người bán ngoài Haveno để nhận đề nghị này. takeOffer.success.headline=Bạn đã nhận báo giá thành công. takeOffer.success.info=Bạn có thể xem trạng thái giao dịch của bạn tại \"Portfolio/Các giao dịch mở\". takeOffer.error.message=Có lỗi xảy ra khi nhận báo giá.\n\n{0} # new entries takeOffer.takeOfferButton.buy=Xem lại: Chấp nhận đề nghị mua XMR bằng {0} takeOffer.takeOfferButton.sell=Xem lại: Chấp nhận đề nghị bán XMR lấy {0} takeOffer.noPriceFeedAvailable=Bạn không thể nhận báo giá này do sử dụng giá phần trăm dựa trên giá thị trường nhưng không có giá cung cấp. takeOffer.takeOfferFundWalletInfo.headline=Nộp tiền cho giao dịch của bạn # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Giá trị giao dịch: {0} \n takeOffer.takeOfferFundWalletInfo.msg=Bạn cần phải deposit {0} để chấp nhận đề nghị này.\n\nSố tiền là tổng của:\n{1}- Khoản tiền đặt cọc của bạn: {2}\n- Phí giao dịch: {3} takeOffer.alreadyPaidInFunds=Bạn đã thanh toán, bạn có thể rút số tiền này tại màn hình \"Vốn/Gửi vốn\". takeOffer.paymentInfo=Thông tin thanh toán takeOffer.setAmountPrice=Cài đặt số tiền takeOffer.alreadyFunded.askCancel=Bạn đã tài trợ cho đề xuất này rồi. Nếu bạn hủy bây giờ, số tiền của bạn sẽ tiếp tục ở trong ví Haveno địa phương của bạn và sẵn sàng để rút tại màn hình "Vốn/Gửi vốn". Bạn có chắc chắn muốn hủy không? takeOffer.failed.offerNotAvailable=Nhận yêu cầu chào giá không thành công do chào giá không còn tồn tại. Có thể trong lúc chờ đợi, Thương gia khác đã nhận chào giá này. takeOffer.failed.offerTaken=Bạn không thể nhận chào giá này vì đã được nhận bởi Thương gia khác. takeOffer.failed.offerRemoved=Bạn không thể nhận chào giá này vì trong lúc chời đợi, chào giá này đã bị gỡ bỏ. takeOffer.failed.offererNotOnline=Nhận báo giá không thành công vì người tạo không còn online. takeOffer.failed.offererOffline=Bạn không thể nhận báo giá vì người tạo đã offline. takeOffer.warning.connectionToPeerLost=You lost connection to the maker.\nThey might have gone offline or has closed the connection to you because of too many open connections.\n\nIf you can still see their offer in the offerbook you can try to take the offer again. takeOffer.error.noFundsLost=\n\nVí của bạn không còn tiền.\nHãy khởi động lại ứng dụng và kiểm tra kết nối mạng để xem bạn có thể xử lý vấn đề này hay không. # suppress inspection "TrailingSpacesInProperty" takeOffer.error.feePaid=\n\n takeOffer.error.depositPublished=\n\nGiao dịch đặt cọc đã được công bố.\nHãy khởi động lại ứng dụng và kiểm tra kết nối mạng để xem bạn có thể xử lý vấn đề này hay không.\nNếu vấn đề vẫn không được xử lý, hãy liên hệ các lập trình viên để được hỗ trợ. takeOffer.error.payoutPublished=\n\nGiao dịch hoàn tiền đã được công bố.\nHãy khởi động lại ứng dụng và kiểm tra kết nối mạng để xem bạn có thể xử lý vấn đề này hay không.\nNếu vấn đề vẫn không được xử lý, hãy liên hệ các lập trình viên để được hỗ trợ. takeOffer.tac=Bằng cách nhận báo giá này, tôi đồng ý với các điều khoản giao dịch nêu trong màn hình này. #################################################################### # Offerbook / Edit offer #################################################################### openOffer.header.triggerPrice=Giá khởi phát openOffer.triggerPrice=Trigger price {0} openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\nPlease edit the offer to define a new trigger price editOffer.setPrice=Cài đặt giá editOffer.confirmEdit=Xác nhận: Chỉnh sửa báo giá editOffer.publishOffer=Công bố báo giá. editOffer.failed=Chỉnh sửa báo giá không thành công:\n{0} editOffer.success=Báo giá của bạn đã được chỉnh sửa thành công. editOffer.invalidDeposit=Số tiền đặt cọc cho người mua không nằm trong giới hạn quy định bởi DAO Haveno và không thể chỉnh sửa được nữa #################################################################### # Portfolio #################################################################### portfolio.tab.openOffers=Các báo giá mở của tôi portfolio.tab.pendingTrades=Các giao dịch mở portfolio.tab.history=Lịch sử portfolio.tab.failed=Không thành công portfolio.tab.editOpenOffer=Chỉnh sửa báo giá portfolio.closedTrades.deviation.help=Percentage price deviation from market portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} portfolio.pending.syncing=Đang đồng bộ ví giao dịch portfolio.pending.syncing.blockRemaining=Đang đồng bộ ví giao dịch — còn 1 khối portfolio.pending.syncing.blocksRemaining=Đang đồng bộ ví giao dịch — còn {0} khối portfolio.pending.step1.waitForConf=Đợi xác nhận blockchain portfolio.pending.step2_buyer.additionalConf=Tiền gửi đã đạt 10 xác nhận.\nĐể tăng cường bảo mật, chúng tôi khuyên bạn chờ {0} xác nhận trước khi gửi thanh toán.\nTiến hành sớm là rủi ro của bạn. portfolio.pending.step2_buyer.startPayment=Bắt đầu thanh toán portfolio.pending.step2_seller.waitPaymentSent=Đợi đến khi bắt đầu thanh toán portfolio.pending.step3_buyer.waitPaymentArrived=Đợi đến khi khoản thanh toán đến portfolio.pending.step3_seller.confirmPaymentReceived=Xác nhận đã nhận được thanh toán portfolio.pending.step5.completed=Hoàn thành portfolio.pending.step3_seller.autoConf.status.label=Auto-confirm status portfolio.pending.autoConf=Auto-confirmed portfolio.pending.autoConf.blocks=XMR confirmations: {0} / Required: {1} portfolio.pending.autoConf.state.xmr.txKeyReused=Transaction key re-used. Please open a dispute. portfolio.pending.autoConf.state.confirmations=XMR confirmations: {0}/{1} portfolio.pending.autoConf.state.txNotFound=Transaction not seen in mem-pool yet portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=No valid transaction ID / transaction key portfolio.pending.autoConf.state.filterDisabledFeature=Disabled by developers. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FEATURE_DISABLED=Auto-confirm feature is disabled. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=Trade amount exceeds auto-confirm amount limit # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.INVALID_DATA=Peer provided invalid data. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=Payout transaction was already published. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.DISPUTE_OPENED=Dispute was opened. Auto-confirm is deactivated for that trade. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.REQUESTS_STARTED=Transaction proof requests started # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PENDING=Success results: {0}/{1}; {2} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.COMPLETED=Proof at all services succeeded # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. No auto-confirm possible. # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. portfolio.pending.step1.info.you=Giao dịch đặt cọc đã được thực hiện.\nBạn có thể bắt đầu thanh toán sau 10 lần xác nhận (khoảng {0} phút còn lại). portfolio.pending.step1.info.buyer=Giao dịch đặt cọc đã được thực hiện.\nNgười mua XMR có thể bắt đầu thanh toán sau 10 lần xác nhận (khoảng {0} phút còn lại). portfolio.pending.step1.warn=The deposit transaction is still not confirmed. This sometimes happens in rare cases when the funding fee of one trader from an external wallet was too low. portfolio.pending.step1.openForDispute=The deposit transaction is still not confirmed. You can wait longer or contact the mediator for assistance. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2.confReached=Your trade has reached at least one blockchain confirmation.\n\n portfolio.pending.step2_buyer.refTextWarn=Important: when making the payment, leave the \"reason for payment\" field empty. DO NOT put the trade ID or any other text like 'monero', 'XMR', or 'Haveno'. You are free to discuss via trader chat if an alternate \"reason for payment\" would be suitable to you both. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees=If your bank charges you any fees to make the transfer, you are responsible for paying those fees. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.crypto=Hãy chuyển từ ví ngoài {0} của bạn\n{1} cho người bán XMR.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.cash=Hãy đến ngân hàng và thanh toán {0} cho người bán XMR.\n\n portfolio.pending.step2_buyer.cash.extra=YÊU CẦU QUAN TRỌNG:\nSau khi bạn đã thanh toán xong hãy viết lên giấy biên nhận: KHÔNG HOÀN TRẢ.\nSau đó xé thành 2 phần, chụp ảnh và gửi tới email của người bán XMR. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.moneyGram=Vui lòng trả {0} cho người bán XMR qua MoneyGram.\n\n portfolio.pending.step2_buyer.moneyGram.extra=Yêu cầu quan trọng:\nSau khi bạn hoàn thành chi trả hãy gửi Số xác thực và hình chụp hoá đơn qua email cho người bán XMR.\nHoá đơn phải chỉ rõ họ tên đầy đủ, quốc gia, tiểu bang và số tiền. Email người bán là: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.westernUnion=Hãy thanh toán {0} cho người bán XMR bằng cách sử dụng Western Union.\n\n portfolio.pending.step2_buyer.westernUnion.extra=YÊU CẦU QUAN TRỌNG:\nSau khi bạn đã thanh toán xong hãy gửi MTCN (số theo dõi) và ảnh giấy biên nhận bằng email cho người bán XMR.\nGiấy biên nhận phải ghi rõ họ tên của người bán, thành phố, quốc gia và số tiền. Địa chỉ email của người bán là: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Hãy gửi {0} bằng \"Phiếu chuyển tiền US\" cho người bán XMR.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Vui lòng liên hệ người bán XMR và cung cấp số liên hệ và sắp xếp cuộc hẹn để thanh toán {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Thanh toán bắt đầu sử dụng {0} portfolio.pending.step2_buyer.recipientsAccountData=Recipients {0} portfolio.pending.step2_buyer.amountToTransfer=Số tiền chuyển portfolio.pending.step2_buyer.sellersAddress=Địa chỉ của người bán {0} portfolio.pending.step2_buyer.buyerAccount=Tài khoản thanh toán sẽ sử dụng portfolio.pending.step2_buyer.paymentSent=Bắt đầu thanh toán portfolio.pending.step2_buyer.showEarly=Hiển thị thông tin thanh toán sớm portfolio.pending.step2_buyer.warn=You still have not done your {0} payment!\nPlease note that the trade has to be completed by {1}. portfolio.pending.step2_buyer.openForDispute=You have not completed your payment!\nThe max. period for the trade has elapsed.Please contact the mediator for assistance. portfolio.pending.step2_buyer.paperReceipt.headline=Bạn đã gửi giấy biên nhận cho người bán XMR chưa? portfolio.pending.step2_buyer.paperReceipt.msg=Remember:\nBạn cần phải viết trên giấy biên nhận: KHÔNG HOÀN TRẢ.\nSau đó xé thành 2 phần, chụp ảnh và gửi đến email của người bán XMR. portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Gửi số xác nhận và hoá đơn portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=Bạn cần gửi số xác thực và ảnh chụp của hoá đơn qua email đến người bán XMR.\nHoá đơn phải ghi rõ họ tên đầy đủ người bán, quốc gia, tiểu bang và số lượng. Email người bán là: {0}.\n\nBạn đã gửi số xác thực và hợp đồng cho người bán? portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=Gửi MTCN và biên nhận portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Bạn cần phải gửi MTCN (số theo dõi) và ảnh chụp giấy biên nhận bằng email cho người bán XMR.\nGiấy biên nhận phải nêu rõ họ tên, thành phố, quốc gia của người bán và số tiền. Địa chỉ email của người bán là: {0}.\n\nBạn đã gửi MTCN và hợp đồng cho người bán chưa? portfolio.pending.step2_buyer.halCashInfo.headline=Gửi mã HalCash portfolio.pending.step2_buyer.halCashInfo.msg=Bạn cần nhắn tin mã HalCash và mã giao dịch ({0}) tới người bán XMR. \nSố điện thoại của người bán là {1}.\n\nBạn đã gửi mã tới người bán chưa? portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Some banks might verify the receiver's name. Faster Payments accounts created in old Haveno clients do not provide the receiver's name, so please use trade chat to obtain it (if needed). portfolio.pending.step2_buyer.confirmStart.headline=Xác nhận rằng bạn đã bắt đầu thanh toán portfolio.pending.step2_buyer.confirmStart.msg=Bạn đã kích hoạt thanh toán {0} cho Đối tác giao dịch của bạn chưa? portfolio.pending.step2_buyer.confirmStart.yes=Có, tôi đã bắt đầu thanh toán portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=You have not provided proof of payment portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=You have not entered the transaction ID and the transaction key.\n\nBy not providing this data the peer cannot use the auto-confirm feature to release the XMR as soon the XMR has been received.\nBeside that, Haveno requires that the sender of the XMR transaction is able to provide this information to the mediator or arbitrator in case of a dispute.\nSee more details on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=Input is not a 32 byte hexadecimal value portfolio.pending.step2_buyer.confirmStart.warningButton=Ignore and continue anyway portfolio.pending.step2_seller.waitPayment.headline=Đợi thanh toán portfolio.pending.step2_seller.f2fInfo.headline=Thông tin liên lạc của người mua portfolio.pending.step2_seller.waitPayment.msg=Giao dịch đặt cọc có ít nhất một xác nhận blockchain.\nBạn cần phải đợi cho đến khi người mua XMR bắt đầu thanh toán {0}. portfolio.pending.step2_seller.warn=Người mua XMR vẫn chưa thanh toán {0}.\nBạn cần phải đợi cho đến khi người mua bắt đầu thanh toán.\nNếu giao dịch không được hoàn thành vào {1} trọng tài sẽ điều tra. portfolio.pending.step2_seller.openForDispute=The XMR buyer has not started their payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the mediator for assistance. tradeChat.chatWindowTitle=Chat window for trade with ID '{0}' tradeChat.openChat=Open chat window tradeChat.rules=You can communicate with your trade peer to resolve potential problems with this trade.\nIt is not mandatory to reply in the chat.\nIf a trader violates any of the rules below, open a dispute and report it to the mediator or arbitrator.\n\nChat rules:\n\t● Do not send any links (risk of malware). You can send the transaction ID and the name of a block explorer.\n\t● Do not send your seed words, private keys, passwords or other sensitive information!\n\t● Do not encourage trading outside of Haveno (no security).\n\t● Do not engage in any form of social engineering scam attempts.\n\t● If a peer is not responding and prefers to not communicate via chat, respect their decision.\n\t● Keep conversation scope limited to the trade. This chat is not a messenger replacement or troll-box.\n\t● Keep conversation friendly and respectful. # suppress inspection "UnusedProperty" message.state.UNDEFINED=Không xác định # suppress inspection "UnusedProperty" message.state.SENT=Tin nhắn được gửi # suppress inspection "UnusedProperty" message.state.ARRIVED=Tin nhắn đã nhận # suppress inspection "UnusedProperty" message.state.STORED_IN_MAILBOX=Message of payment sent but not yet received by peer # suppress inspection "UnusedProperty" message.state.ACKNOWLEDGED=Người nhận xác nhận tin nhắn # suppress inspection "UnusedProperty" message.state.FAILED=Gửi tin nhắn không thành công portfolio.pending.step3_buyer.wait.headline=Đợi người bán XMR xác nhận thanh toán portfolio.pending.step3_buyer.wait.info=Đợi người bán XMR xác nhận đã nhận thanh toán {0}. portfolio.pending.step3_buyer.wait.msgStateInfo.label=Thông báo trạng thái thanh toán đã bắt đầu portfolio.pending.step3_buyer.warn.part1a=trên blockchain {0} portfolio.pending.step3_buyer.warn.part1b=tại nhà cung cấp thanh toán của bạn (VD: ngân hàng) portfolio.pending.step3_buyer.warn.part2=The XMR seller still has not confirmed your payment. Please check {0} if the payment sending was successful. portfolio.pending.step3_buyer.openForDispute=The XMR seller has not confirmed your payment! The max. period for the trade has elapsed. You can wait longer and give the trading peer more time or request assistance from the mediator. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.part=Đối tác giao dịch của bạn đã xác nhận rằng họ đã kích hoạt thanh toán {0}.\n\n portfolio.pending.step3_seller.crypto.explorer=Trên trình duyệt blockchain explorer {0} ưa thích của bạn portfolio.pending.step3_seller.crypto.wallet=Trên ví {0} của bạn portfolio.pending.step3_seller.crypto={0}Vui lòng kiểm tra {1} xem giao dịch tới địa chỉ nhận của bạn \n{2}\nđã nhận được đủ xác nhận blockchain hay chưa.\nSố tiền thanh toán phải là {3}\n\nBạn có thể copy & paste địa chỉ {4} của bạn từ màn hình chính sau khi đóng cửa sổ này. portfolio.pending.step3_seller.postal={0}Please check if you have received {1} with \"US Postal Money Order\" from the XMR buyer. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.payByMail={0}Please check if you have received {1} with \"Pay by Mail\" from the XMR buyer. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.bank=Your trading partner has confirmed that they have initiated the {0} payment.\n\nPlease go to your online banking web page and check if you have received {1} from the XMR buyer. portfolio.pending.step3_seller.cash=Vì thanh toán được thực hiện qua Tiền gửi tiền mặt nên người mua XMR phải viết rõ \"KHÔNG HOÀN LẠI\" trên giấy biên nhận, xé làm 2 phần và gửi ảnh cho bạn qua email.\n\nĐể tránh bị đòi tiền lại, chỉ xác nhận bạn đã nhận được email và bạn chắc chắn giấy biên nhận là có hiệu lực.\nNếu bạn không chắc chắn, {0} portfolio.pending.step3_seller.moneyGram=Người mua phải gửi mã số xác nhận và ảnh chụp của hoá đơn qua email.\nHoá đơn cần ghi rõ họ tên đầy đủ, quốc gia, tiêu bang và số lượng. Vui lòng kiểm tra email nếu bạn nhận được số xác thực.\n\nSau khi popup đóng, bạn sẽ thấy tên người mua XMR và địa chỉ để nhận tiền từ MoneyGram.\n\nChỉ xác nhận hoá đơn sau khi bạn hoàn thành việc nhận tiền. portfolio.pending.step3_seller.westernUnion=Người mua phải gửi cho bạn MTCN (số theo dõi) và ảnh giấy biên nhận qua email.\nGiấy biên nhận phải ghi rõ họ tên của bạn, thành phố, quốc gia và số tiền. Hãy kiểm tra email xem bạn đã nhận được MTCN chưa.\n\nSau khi đóng cửa sổ này, bạn sẽ thấy tên và địa chỉ của người mua XMR để nhận tiền từ Western Union.\n\nChỉ xác nhận giấy biên nhận sau khi bạn đã nhận tiền thành công! portfolio.pending.step3_seller.halCash=Người mua phải gửi mã HalCash cho bạn bằng tin nhắn. Ngoài ra, bạn sẽ nhận được một tin nhắn từ HalCash với thông tin cần thiết để rút EUR từ một máy ATM có hỗ trợ HalCash. \n\nSau khi nhận được tiền từ ATM vui lòng xác nhận lại biên lai thanh toán tại đây! portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon eGift Card by email or by text message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted confirm the payment receipt. portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=Xác nhận đã nhận được thanh toán portfolio.pending.step3_seller.amountToReceive=Số tiền nhận được portfolio.pending.step3_seller.yourAddress=Địa chỉ {0} của bạn portfolio.pending.step3_seller.buyersAddress=Địa chỉ {0} của người mua portfolio.pending.step3_seller.yourAccount=tài khoản giao dịch của bạn portfolio.pending.step3_seller.xmrTxHash=ID giao dịch portfolio.pending.step3_seller.xmrTxKey=Transaction key portfolio.pending.step3_seller.buyersAccount=Buyers account data portfolio.pending.step3_seller.confirmReceipt=Xác nhận đã nhận được thanh toán portfolio.pending.step3_seller.buyerStartedPayment=Người mua XMR đã bắt đầu thanh toán {0}.\n{1} portfolio.pending.step3_seller.buyerStartedPayment.crypto=Kiểm tra xác nhận blockchain ở ví crypto của bạn hoặc block explorer và xác nhận thanh toán nếu bạn nhận được đủ xác nhận blockchain. portfolio.pending.step3_seller.buyerStartedPayment.traditional=Kiểm tra tại tài khoản giao dịch của bạn (VD: Tài khoản ngân hàng) và xác nhận khi bạn đã nhận được thanh toán. portfolio.pending.step3_seller.warn.part1a=trên {0} blockchain portfolio.pending.step3_seller.warn.part1b=tại nhà cung cấp thanh toán của bạn (VD: ngân hàng) portfolio.pending.step3_seller.warn.part2=You still have not confirmed the receipt of the payment. Please check {0} if you have received the payment. portfolio.pending.step3_seller.openForDispute=You have not confirmed the receipt of the payment!\nThe max. period for the trade has elapsed.\nPlease confirm or request assistance from the mediator. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=Bạn đã nhận được thanh toán {0} từ Đối tác giao dịch của bạn?\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Please note, that as soon you have confirmed the receipt, the locked trade amount will be released to the XMR buyer and the security deposit will be refunded.\n\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Xác nhận rằng bạn đã nhận được thanh toán portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=Vâng, tôi đã nhận được thanh toán portfolio.pending.step3_seller.onPaymentReceived.signer=IMPORTANT: By confirming receipt of payment, you are also verifying the account of the counterparty and signing it accordingly. Since the account of the counterparty hasn't been signed yet, you should delay confirmation of the payment as long as possible to reduce the risk of a chargeback. portfolio.pending.step5_buyer.groupTitle=Tóm tắt giao dịch đã hoàn thành portfolio.pending.step5_buyer.tradeFee=Phí giao dịch portfolio.pending.step5_buyer.makersMiningFee=Phí đào portfolio.pending.step5_buyer.takersMiningFee=Tổng phí đào portfolio.pending.step5_buyer.refunded=tiền gửi đặt cọc được hoàn lại portfolio.pending.step5_buyer.withdrawXMR=Rút monero của bạn portfolio.pending.step5_buyer.amount=Số tiền được rút portfolio.pending.step5_buyer.withdrawToAddress=rút tới địa chỉ portfolio.pending.step5_buyer.moveToHavenoWallet=Keep funds in Haveno wallet portfolio.pending.step5_buyer.withdrawExternal=rút tới ví ngoài portfolio.pending.step5_buyer.alreadyWithdrawn=Số tiền của bạn đã được rút.\nVui lòng kiểm tra lịch sử giao dịch. portfolio.pending.step5_buyer.confirmWithdrawal=Xác nhận yêu cầu rút portfolio.pending.step5_buyer.amountTooLow=Số tiền chuyển nhỏ hơn phí giao dịch và giá trị tx tối thiểu (dust). portfolio.pending.step5_buyer.withdrawalCompleted.headline=Rút hoàn tất portfolio.pending.step5_buyer.withdrawalCompleted.msg=Các giao dịch đã hoàn thành của bạn được lưu trong \"Portfolio/Lịch sử\".\nBạn có thể xem lại tất cả giao dịch monero của bạn tại \"Vốn/Giao dịch\" portfolio.pending.step5_buyer.bought=Bạn đã mua portfolio.pending.step5_buyer.paid=Bạn đã thanh toán portfolio.pending.step5_seller.sold=Bạn đã bán portfolio.pending.step5_seller.received=Bạn đã nhận tradeFeedbackWindow.title=Chúc mừng giao dịch của bạn được hoàn thành. tradeFeedbackWindow.msg.part1=Chúng tôi rất vui lòng được nghe phản hồi của bạn. Điều đó sẽ giúp chúng tôi cải thiện phần mềm và hoàn thiện trải nghiệm người dùng. Nếu bạn muốn cung cấp phản hồi, vui lòng điền vào cuộc khảo sát ngắn dưới đây (không yêu cầu đăng nhập) ở: tradeFeedbackWindow.msg.part2=nếu bạn có câu hỏi hay vấn đề, vui lòng liên hệ với người dùng khác và các nhàn đóng góp qua Haveno forum ở: tradeFeedbackWindow.msg.part3=Cám ơn bạn đã sử dụng Haveno! portfolio.pending.role=Vai trò của tôi portfolio.pending.tradeInformation=Thông tin giao dịch portfolio.pending.remainingTime=Thời gian còn lại portfolio.pending.remainingTimeDetail={0} (cho đến khi {1}) portfolio.pending.remainingTimeDetail.startsAfter=Bắt đầu sau {0} lần xác nhận portfolio.pending.tradePeriodInfo=Sau {0} xác nhận, thời gian giao dịch bắt đầu. Tùy theo phương thức thanh toán được sử dụng, sẽ áp dụng thời gian giao dịch tối đa khác nhau. portfolio.pending.tradePeriodWarning=Nếu quá thời gian giao dịch, cả hai Thương gia đều có thể mở khiếu nại. portfolio.pending.tradeNotCompleted=giao dịch không được hoàn thành đúng thời gian (cho đến khi {0}) portfolio.pending.tradeProcess=Quá trình giao dịch portfolio.pending.openAgainDispute.msg=If you are not sure that the message to the mediator or arbitrator arrived (e.g. if you did not get a response after 1 day) feel free to open a dispute again with Cmd/Ctrl+o. You can also ask for additional help on the Haveno forum at [HYPERLINK:https://haveno.community]. portfolio.pending.openAgainDispute.button=Mở khiếu nại lần nữa portfolio.pending.openSupportTicket.headline=Mở vé hỗ trợ portfolio.pending.openSupportTicket.msg=Please use this function only in emergency cases if you don't see a \"Open support\" or \"Open dispute\" button.\n\nWhen you open a support ticket the trade will be interrupted and handled by a mediator or arbitrator. portfolio.pending.timeLockNotOver=You have to wait until ≈{0} ({1} more blocks) before you can open an arbitration dispute. portfolio.pending.error.depositTxNull=The deposit transaction is null. You cannot open a dispute without a valid deposit transaction. Please go to \"Settings/Network info\" and do a SPV resync.\n\nFor further help please contact the Haveno support channel at the Haveno Keybase team. portfolio.pending.mediationResult.error.depositTxNull=The deposit transaction is null. You can move the trade to failed trades. portfolio.pending.mediationResult.error.delayedPayoutTxNull=The delayed payout transaction is null. You can move the trade to failed trades. portfolio.pending.error.depositTxNotConfirmed=The deposit transaction is not confirmed. You can not open an arbitration dispute with an unconfirmed deposit transaction. Please wait until it is confirmed or go to \"Settings/Network info\" and do a SPV resync.\n\nFor further help please contact the Haveno support channel at the Haveno Keybase team. portfolio.pending.support.headline.getHelp=Need help? portfolio.pending.support.text.getHelp=If you have any problems you can try to contact the trade peer in the trade chat or ask the Haveno community at https://haveno.community. If your issue still isn't resolved, you can request more help from a mediator. portfolio.pending.support.button.getHelp=Open Trader Chat portfolio.pending.support.headline.halfPeriodOver=Check payment portfolio.pending.support.headline.periodOver=Trade period is over portfolio.pending.support.headline.depositTxMissing=Thiếu giao dịch ký quỹ portfolio.pending.support.depositTxMissing=Giao dịch gửi tiền cho thương vụ này bị thiếu. Mở phiếu hỗ trợ để liên hệ với trọng tài để được trợ giúp. portfolio.pending.mediationRequested=Mediation requested portfolio.pending.refundRequested=Refund requested portfolio.pending.openSupport=Mở đơn hỗ trợ portfolio.pending.supportTicketOpened=Đơn hỗ trợ đã mở portfolio.pending.communicateWithArbitrator=Vui lòng liên lạc với trong tài qua màn hình \"Hỗ trợ\". portfolio.pending.communicateWithMediator=Please communicate in the \"Support\" screen with the mediator. portfolio.pending.disputeOpenedByUser=Bạn đã mở một khiếu nại.\n{0} portfolio.pending.disputeOpenedByPeer=Đối tác giao dịch của bạn đã mở một khiếu nại\n{0} portfolio.pending.noReceiverAddressDefined=Không có địa chỉ người nhận portfolio.pending.mediationResult.headline=Suggested payout from mediation portfolio.pending.mediationResult.info.noneAccepted=Complete the trade by accepting the mediator's suggestion for the trade payout. portfolio.pending.mediationResult.info.selfAccepted=You have accepted the mediator's suggestion. Waiting for peer to accept as well. portfolio.pending.mediationResult.info.peerAccepted=Your trade peer has accepted the mediator's suggestion. Do you accept as well? portfolio.pending.mediationResult.button=View proposed resolution portfolio.pending.mediationResult.popup.headline=Mediation result for trade with ID: {0} portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator's suggestion for trade {0} portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader's security deposit) as compensation for their work. Both traders agreeing to the mediator's suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator's suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. portfolio.pending.failedTrade.missingDepositTx=Một giao dịch ký quỹ đang bị thiếu.\n\nGiao dịch này là bắt buộc để hoàn tất giao dịch. Vui lòng đảm bảo ví của bạn được đồng bộ hoàn toàn với blockchain Monero.\n\nBạn có thể chuyển giao dịch này đến mục "Giao dịch thất bại" để vô hiệu hóa nó. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Move trade to failed trades portfolio.pending.failedTrade.warningIcon.tooltip=Click to open details about the issues of this trade portfolio.failed.revertToPending.popup=Do you want to move this trade to open trades? portfolio.failed.revertToPending=Move trade to open trades portfolio.closed.completed=Hoàn thành portfolio.closed.ticketClosed=Arbitrated portfolio.closed.mediationTicketClosed=Mediated portfolio.closed.canceled=Đã hủy portfolio.failed.Failed=Không thành công portfolio.failed.unfail=Before proceeding, make sure you have a backup of your data directory!\nDo you want to move this trade back to open trades?\nThis is a way to unlock funds stuck in a failed trade. portfolio.failed.cantUnfail=This trade cannot be moved back to open trades at the moment. \nTry again after completion of trade(s) {0} portfolio.failed.depositTxNull=The trade cannot be reverted to a open trade. Deposit transaction is null. portfolio.failed.delayedPayoutTxNull=The trade cannot be reverted to a open trade. Delayed payout transaction is null. #################################################################### # Funds #################################################################### funds.tab.deposit=Nhận vốn funds.tab.withdrawal=Gửi vốn funds.tab.reserved=Vốn bảo toàn funds.tab.locked=Vốn bị khóa funds.tab.transactions=Giao dịch funds.deposit.unused=Không sử dụng funds.deposit.usedInTx=Sử dụng trong {0} giao dịch funds.deposit.fundHavenoWallet=Nộp tiền ví Haveno funds.deposit.noAddresses=Chưa có địa chỉ nộp tiền được tạo funds.deposit.fundWallet=Nộp tiền cho ví của bạn funds.deposit.withdrawFromWallet=Chuyển tiền từ ví funds.deposit.amount=Số tiền bằng XMR (tùy chọn) funds.deposit.generateAddress=Tạo địa chỉ mới funds.deposit.generateAddressSegwit=Native segwit format (Bech32) funds.deposit.selectUnused=Vui lòng chọn địa chỉ chưa sử dụng từ bảng trên hơn là tạo một địa chỉ mới. funds.withdrawal.arbitrationFee=Phí trọng tài funds.withdrawal.inputs=Lựa chọn số liệu đầu vào funds.withdrawal.useAllInputs=Sử dụng tất cả số liệu đầu vào sẵn có funds.withdrawal.useCustomInputs=Sử dụng số liệu đầu vào thông dụng funds.withdrawal.receiverAmount=Số tiền của người nhận funds.withdrawal.senderAmount=Số tiền của người gửi funds.withdrawal.feeExcluded=Số tiền không bao gồm phí đào funds.withdrawal.feeIncluded=Số tiền bao gồm phí đào funds.withdrawal.fromLabel=Rút từ địa chỉ funds.withdrawal.toLabel=rút tới địa chỉ funds.withdrawal.memoLabel=Withdrawal memo funds.withdrawal.memo=Optionally fill memo funds.withdrawal.withdrawButton=Rút được chọn funds.withdrawal.noFundsAvailable=Không còn tiền để rút funds.withdrawal.confirmWithdrawalRequest=Xác nhận yêu cầu rút funds.withdrawal.withdrawMultipleAddresses=Rút từ nhiều địa chỉ ({0}) funds.withdrawal.withdrawMultipleAddresses.tooltip=Rút từ nhiều địa chỉ:\n{0} funds.withdrawal.notEnoughFunds=Bạn không còn đủ tiền trong ví. funds.withdrawal.selectAddress=Chọn địa chỉ nguồn từ bảng funds.withdrawal.setAmount=Cài đặt số tiền được rút funds.withdrawal.fillDestAddress=Điền địa chỉ đến của bạn funds.withdrawal.warn.noSourceAddressSelected=Bạn cần chọn địa chỉ nguồn ở bảng trên. funds.withdrawal.warn.amountExceeds=Bạn không có đủ tiền từ địa chỉ được chọn.\nXem xét chọn nhiều địa chỉ ở bảng trên hoặc thay đổi cài đặt phí để bao gồm phí đào. funds.reserved.noFunds=Không có tiền dự trữ trong báo giá mở funds.reserved.reserved=Dự trữ trong ví nội bộ để chào giá với ID: {0} funds.locked.noFunds=Không có tiền bị khóa trong giao dịch funds.locked.locked=Khóa trong multisig để giao dịch với ID: {0} funds.tx.direction.sentTo=Gửi đến: funds.tx.direction.receivedWith=Nhận với: funds.tx.direction.genesisTx=Từ giao dịch gốc: funds.tx.createOfferFee=Người tạo và phí tx: {0} funds.tx.takeOfferFee=Người nhận và phí tx: {0} funds.tx.multiSigDeposit=Tiền gửi Multisig: {0} funds.tx.multiSigPayout=Tiền trả Multisig: {0} funds.tx.disputePayout=Tiền trả khiếu nại: {0} funds.tx.disputeLost=Vụ khiếu nại bị thua: {0} funds.tx.collateralForRefund=Refund collateral: {0} funds.tx.timeLockedPayoutTx=Time locked payout tx: {0} funds.tx.refund=Refund from arbitration: {0} funds.tx.unknown=Không rõ lý do: {0} funds.tx.noFundsFromDispute=KHÔNG HOÀN LẠI từ khiếu nại funds.tx.receivedFunds=Vốn đã nhận funds.tx.withdrawnFromWallet=rút từ ví funds.tx.memo=Memo funds.tx.noTxAvailable=Không có giao dịch nào funds.tx.revert=Khôi phục funds.tx.txSent=GIao dịch đã gửi thành công tới địa chỉ mới trong ví Haveno nội bộ. funds.tx.direction.self=Gửi cho chính bạn funds.tx.reimbursementRequestTxFee=Yêu cầu bồi hoàn funds.tx.compensationRequestTxFee=Yêu cầu bồi thường funds.tx.dustAttackTx=Số dư nhỏ đã nhận funds.tx.dustAttackTx.popup=Giao dịch này đang gửi một lượng XMR rất nhỏ vào ví của bạn và có thể đây là cách các công ty phân tích chuỗi đang tìm cách theo dõi ví của bạn.\nNếu bạn sử dụng đầu ra giao dịch đó cho một giao dịch chi tiêu, họ sẽ phát hiện ra rằng rất có thể bạn cũng là người sở hửu cái ví kia (nhập coin). \n\nĐể bảo vệ quyền riêng tư của bạn, ví Haveno sẽ bỏ qua các đầu ra có số dư nhỏ dành cho mục đích chi tiêu cũng như hiển thị số dư. Bạn có thể thiết lập ngưỡng khi một đầu ra được cho là có số dư nhỏ trong phần cài đặt. #################################################################### # Support #################################################################### support.tab.mediation.support=Mediation support.tab.arbitration.support=Arbitration support.tab.legacyArbitration.support=Legacy Arbitration support.tab.ArbitratorsSupportTickets={0}'s tickets support.sigCheck.button=Check signature support.sigCheck.popup.header=Verify dispute result signature support.sigCheck.popup.msg.label=Summary message support.sigCheck.popup.msg.prompt=Copy & paste summary message from dispute support.sigCheck.popup.result=Validation result support.sigCheck.popup.success=Signature is valid support.sigCheck.popup.failed=Signature verification failed support.sigCheck.popup.invalidFormat=Message is not of expected format. Copy & paste summary message from dispute. support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? support.reOpenButton.label=Re-open support.sendNotificationButton.label=Thông báo riêng tư support.reportButton.label=Report support.fullReportButton.label=All disputes support.noTickets=Không có đơn hỗ trợ được mở support.sendingMessage=Đang gửi tin nhắn... support.receiverNotOnline=Receiver is not online. Message is saved to their mailbox. support.sendMessageError=Gửi tin nhắn thất bại. Lỗi: {0} support.receiverNotKnown=Receiver not known support.wrongVersion=Báo giá trong khiếu nại này được tạo với phiên bản Haveno cũ.\nBạn không thể đóng khiếu nại với phiên bản ứng dụng của bạn.\n\nVui lòng sử dụng phiên bản giao thức cũ hơn {0} support.openFile=Mở file để đính kèm (dung lượng file tối đa: {0} kb) support.attachmentTooLarge=Tổng dung lượng file đính kèm là {0} kb và vượt quá dung lượng tối đa cho phép {1} kB. support.maxSize=Dung lượng file tối đa cho phép là {0} kB. support.attachment=File đính kèm support.tooManyAttachments=Bạn không thể gửi quá 3 file đính kèm trong một tin. support.save=Lưu file vào đĩa support.messages=Tin nhắn support.input.prompt=Enter message... support.send=Gửi support.addAttachments=Thêm file đính kèm support.closeTicket=Đóng đơn hỗ trợ support.attachments=File đính kèm: support.savedInMailbox=Tin nhắn lưu trong hộp thư của người nhận support.arrived=Tin nhắn đã đến người nhận support.acknowledged=xác nhận tin nhắn đã được gửi bởi người nhận support.error=Người nhận không thể tiến hành gửi tin nhắn: Lỗi: {0} support.buyerAddress=Địa chỉ người mua XMR support.sellerAddress=Địa chỉ người bán XMR support.role=Vai trò support.agent=Support agent support.state=Trạng thái support.chat=Chat support.preparing=Đang chuẩn bị support.requested=Yêu cầu support.closed=Đóng support.open=Mở support.process=Process support.buyerMaker=Người mua XMR/Người tạo support.sellerMaker=Người bán XMR/Người tạo support.buyerTaker=Người mua XMR/Người nhận support.sellerTaker=Người bán XMR/Người nhận support.initialInfo=Please enter a description of your problem in the text field below. Add as much information as possible to speed up dispute resolution time.\n\nHere is a check list for information you should provide:\n\t● If you are the XMR buyer: Did you make the Fiat or Crypto transfer? If so, did you click the 'payment started' button in the application?\n\t● If you are the XMR seller: Did you receive the Fiat or Crypto payment? If so, did you click the 'payment received' button in the application?\n\t● Which version of Haveno are you using?\n\t● Which operating system are you using?\n\t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\t Sometimes the data directory gets corrupted and leads to strange bugs. \n\t See: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nPlease make yourself familiar with the basic rules for the dispute process:\n\t● You need to respond to the {0}'s requests within 2 days.\n\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\t● The maximum period for a dispute is 14 days.\n\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\nYou can read more about the dispute process at: {2} support.systemMsg=Tin nhắn hệ thống: {0} support.youOpenedTicket=Bạn đã mở yêu cầu hỗ trợ.\n\n{0}\n\nPhiên bản Haveno: {1} support.youOpenedDispute=Bạn đã mở yêu cầu giải quyết tranh chấp.\n\n{0}\n\nPhiên bản Haveno: {1} support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nHaveno version: {1} support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nHaveno version: {1} support.mediatorsDisputeSummary=System message: Mediator's dispute summary:\n{0} support.mediatorsAddress=Mediator's node address: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. support.info.disputeReOpened=Dispute ticket has been re-opened. #################################################################### # Settings #################################################################### settings.tab.preferences=Tham khảo settings.tab.network=Thông tin mạng settings.tab.about=Về setting.preferences.general=Tham khảo chung setting.preferences.explorer=Monero Explorer setting.preferences.deviation=Sai lệch tối đa so với giá thị trường setting.preferences.avoidStandbyMode=Tránh để chế độ chờ setting.preferences.useSoundForNotifications=Phát âm thanh cho thông báo setting.preferences.autoConfirmXMR=XMR auto-confirm setting.preferences.autoConfirmEnabled=Enabled setting.preferences.autoConfirmRequiredConfirmations=Required confirmations setting.preferences.autoConfirmMaxTradeSize=Max. trade amount (XMR) setting.preferences.autoConfirmServiceAddresses=Monero Explorer URLs (uses Tor, except for localhost, LAN IP addresses, and *.local hostnames) setting.preferences.deviationToLarge=Giá trị không được phép lớn hơn {0}%. setting.preferences.txFee=Withdrawal transaction fee (satoshis/vbyte) setting.preferences.useCustomValue=Sử dụng giá trị thông dụng setting.preferences.txFeeMin=Transaction fee must be at least {0} satoshis/vbyte setting.preferences.txFeeTooLarge=Your input is above any reasonable value (>5000 satoshis/vbyte). Transaction fee is usually in the range of 50-400 satoshis/vbyte. setting.preferences.ignorePeers=Bỏ qua đối tác[địa chỉ onion:cổng] setting.preferences.ignoreDustThreshold=Giá trị đầu ra tối thiểu không phải số dư nhỏ setting.preferences.currenciesInList=Tiền tệ trong danh sách cung cấp giá thị trường setting.preferences.prefCurrency=Tiền tệ ưu tiên setting.preferences.displayTraditional=Hiển thị tiền tệ các nước setting.preferences.noTraditional=Không có tiền tệ nước nào được chọn setting.preferences.cannotRemovePrefCurrency=Bạn không thể gỡ bỏ tiền tệ ưu tiên được chọn setting.preferences.displayCryptos=Hiển thị crypto setting.preferences.noCryptos=Không có crypto nào được chọn setting.preferences.addTraditional=Bổ sung tiền tệ các nước setting.preferences.addCrypto=Bổ sung crypto setting.preferences.displayOptions=Hiển thị các phương án setting.preferences.showOwnOffers=Hiển thị Báo giá của tôi trong danh mục Báo giá setting.preferences.useAnimations=Sử dụng hoạt ảnh setting.preferences.useDarkMode=Sử dụng chế độ tối setting.preferences.useLightMode=Sử dụng chế độ sáng setting.preferences.sortWithNumOffers=Sắp xếp danh sách thị trường với số chào giá/giao dịch setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API setting.preferences.notifyOnPreRelease=Receive pre-release notifications setting.preferences.resetAllFlags=Cài đặt lại tất cả nhãn \"Không hiển thị lại\" settings.preferences.languageChange=Áp dụng thay đổi ngôn ngữ cho tất cả màn hình yêu cầu khởi động lại. settings.preferences.supportLanguageWarning=In case of a dispute, please note that arbitration is handled in {0}. settings.preferences.editCustomExplorer.headline=Explorer Settings settings.preferences.editCustomExplorer.description=Choose a system defined explorer from the list on the left, and/or customize to suit your own preferences. settings.preferences.editCustomExplorer.available=Available explorers settings.preferences.editCustomExplorer.chosen=Chosen explorer settings settings.preferences.editCustomExplorer.name=Tên settings.preferences.editCustomExplorer.txUrl=Transaction URL settings.preferences.editCustomExplorer.addressUrl=Address URL setting.info.headline=Tính năng bảo mật dữ liệu mới settings.preferences.sensitiveDataRemoval.msg=Để bảo vệ quyền riêng tư của bạn và các nhà giao dịch khác, Haveno dự định sẽ xóa dữ liệu nhạy cảm khỏi các giao dịch cũ. Điều này đặc biệt quan trọng đối với các giao dịch tiền pháp định có thể bao gồm thông tin tài khoản ngân hàng.\n\nBạn nên đặt giá trị này càng thấp càng tốt, ví dụ 60 ngày. Điều đó có nghĩa là các giao dịch đã hoàn thành từ hơn 60 ngày trước sẽ bị xóa dữ liệu nhạy cảm. Các giao dịch đã hoàn thành có thể được tìm thấy trong tab Danh mục đầu tư / Lịch sử. settings.net.xmrHeader=Mạng Monero settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=Địa chỉ onion của tôi settings.net.xmrNodesLabel=Sử dụng nút Monero thông dụng settings.net.moneroPeersLabel=Các đối tác được kết nối settings.net.connection=Kết nối settings.net.connected=Kết nối settings.net.useTorForXmrJLabel=Sử dụng Tor cho mạng Monero settings.net.moneroNodesLabel=nút Monero để kết nối settings.net.useProvidedNodesRadio=Sử dụng các nút Monero Core đã cung cấp settings.net.usePublicNodesRadio=Sử dụng mạng Monero công cộng settings.net.useCustomNodesRadio=Sử dụng nút Monero Core thông dụng settings.net.warn.usePublicNodes=If you use public Monero nodes, you are subject to any risk of using untrusted remote nodes.\n\nPlease read more details at [HYPERLINK:https://www.getmonero.org/resources/moneropedia/remote-node.html].\n\nAre you sure you want to use public nodes? settings.net.warn.usePublicNodes.useProvided=Không, sử dụng nút được cung cấp settings.net.warn.usePublicNodes.usePublic=Vâng, sử dụng nút công cộng settings.net.warn.useCustomNodes.B2XWarning=Vui lòng chắc chắn rằng nút Monero của bạn là nút Monero Core đáng tin cậy!\n\nKết nối với nút không tuân thủ nguyên tắc đồng thuận Monero Core có thể làm hỏng ví của bạn và gây ra các vấn đề trong quá trình giao dịch.\n\nNgười dùng kết nối với nút vi phạm nguyên tắc đồng thuận chịu trách nhiệm đối với các thiệt hại mà việc này gây ra. Các khiếu nại do điều này gây ra sẽ được quyết định theo hướng có lợi cho đối tác bên kia. Sẽ không có hỗ trợ về mặt kỹ thuật nào cho người dùng không tuân thủ cơ chế cảnh báo và bảo vệ của chúng tôi! settings.net.warn.invalidXmrConfig=Connection to the Monero network failed because your configuration is invalid.\n\nYour configuration has been reset to use the provided Monero nodes instead. You will need to restart the application. settings.net.localhostXmrNodeInfo=Background information: Haveno looks for a local Monero node when starting. If it is found, Haveno will communicate with the Monero network exclusively through it. settings.net.p2PPeersLabel=Các đối tác được kết nối settings.net.onionAddressColumn=Địa chỉ onion settings.net.creationDateColumn=Đã thiết lập settings.net.connectionTypeColumn=Vào/Ra settings.net.sentDataLabel=Sent data statistics settings.net.receivedDataLabel=Received data statistics settings.net.chainHeightLabel=Latest XMR block height settings.net.roundTripTimeColumn=Khứ hồi settings.net.sentBytesColumn=Đã gửi settings.net.receivedBytesColumn=Đã nhận settings.net.peerTypeColumn=Kiểu đối tác settings.net.openTorSettingsButton=Mở cài đặt Tor settings.net.versionColumn=Version settings.net.subVersionColumn=Subversion settings.net.heightColumn=Height settings.net.needRestart=Bạn cần khởi động lại ứng dụng để thay đổi.\nBạn có muốn khởi động bây giờ không? settings.net.notKnownYet=Chưa biết... settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec settings.net.chainHeight=Monero Peers chain height: {0} settings.net.ips=[Địa chỉ IP:tên cổng | máy chủ:cổng | Địa chỉ onion:cổng] (tách bằng dấu phẩy). Cổng có thể bỏ qua nếu sử dụng mặc định (8333). settings.net.seedNode=nút cung cấp thông tin settings.net.directPeer=Đối tác (trực tiếp) settings.net.initialDataExchange={0} [Bootstrapping] settings.net.peer=Đối tác settings.net.inbound=chuyến về settings.net.outbound=chuyến đi setting.about.aboutHaveno=Về Haveno setting.about.about=Haveno là một phần mềm mã nguồn mở nhằm hỗ trợ quá trình trao đổi giữa monero và tiền tệ quốc gia (và các loại tiền crypto khác) thông qua một mạng lưới ngang hàng phi tập trung hoạt động trên cơ sở bảo vệ tối đa quyền riêng tư của người dùng. Vui lòng tìm hiểu thêm về Haveno trên trang web dự án của chúng tôi. setting.about.web=Trang web Haveno setting.about.code=Mã nguồn setting.about.agpl=Giấy phép AGPL setting.about.support=Hỗ trợ Haveno setting.about.def=Haveno không phải là một công ty mà là một dự án mở cho cả cộng đồng. Nếu bạn muốn tham gia hoặc hỗ trợ Haveno, vui lòng truy cập link dưới đây. setting.about.contribute=Góp vốn setting.about.providers=Nhà cung cấp dữ liệu setting.about.apisWithFee=Haveno uses Haveno Price Indices for Fiat and Crypto market prices, and Haveno Mempool Nodes for mining fee estimation. setting.about.apis=Haveno uses Haveno Price Indices for Fiat and Crypto market prices. setting.about.pricesProvided=Giá thị trường cung cấp bởi setting.about.feeEstimation.label=Ước tính phí đào cung cấp bởi setting.about.versionDetails=Thông tin về phiên bản setting.about.version=Phiên bản ứng dụng setting.about.subsystems.label=Các phiên bản của hệ thống con setting.about.subsystems.val=Phiên bản mạng: {0}; Phiên bản tin nhắn P2P: {1}; Phiên bản DB nội bộ: {2}; Phiên bản giao thức giao dịch: {3} setting.about.shortcuts=Short cuts setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' or 'alt + {0}' or 'cmd + {0}' setting.about.shortcuts.menuNav=Navigate main menu setting.about.shortcuts.menuNav.value=To navigate the main menu press: 'Ctrl' or 'alt' or 'cmd' with a numeric key between '1-9' setting.about.shortcuts.close=Close Haveno setting.about.shortcuts.close.value='Ctrl + {0}' or 'cmd + {0}' or 'Ctrl + {1}' or 'cmd + {1}' setting.about.shortcuts.closePopup=Close popup or dialog window setting.about.shortcuts.closePopup.value='ESCAPE' key setting.about.shortcuts.chatSendMsg=Send trader chat message setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' or 'alt + ENTER' or 'cmd + ENTER' setting.about.shortcuts.openDispute=Open dispute setting.about.shortcuts.openDispute.value=Select pending trade and click: {0} setting.about.shortcuts.walletDetails=Open wallet details window setting.about.shortcuts.openEmergencyXmrWalletTool=Open emergency wallet tool for XMR wallet setting.about.shortcuts.showTorLogs=Toggle log level for Tor messages between DEBUG and WARN setting.about.shortcuts.manualPayoutTxWindow=Open window for manual payout from 2of2 Multisig deposit tx setting.about.shortcuts.removeStuckTrade=Open popup to move failed trade to open trades tab again setting.about.shortcuts.removeStuckTrade.value=Select failed trade and press: {0} setting.about.shortcuts.registerArbitrator=Register arbitrator (mediator/arbitrator only) setting.about.shortcuts.registerArbitrator.value=Navigate to account and press: {0} setting.about.shortcuts.registerMediator=Register mediator (mediator/arbitrator only) setting.about.shortcuts.registerMediator.value=Navigate to account and press: {0} setting.about.shortcuts.openSignPaymentAccountsWindow=Open window for account age signing (legacy arbitrators only) setting.about.shortcuts.openSignPaymentAccountsWindow.value=Navigate to legacy arbitrator view and press: {0} setting.about.shortcuts.sendAlertMsg=Send alert or update message (privileged activity) setting.about.shortcuts.sendFilter=Set Filter (privileged activity) setting.about.shortcuts.sendPrivateNotification=Send private notification to peer (privileged activity) setting.about.shortcuts.sendPrivateNotification.value=Open peer info at avatar and press: {0} setting.info.headline=New XMR auto-confirm Feature setting.info.msg=When selling XMR for XMR you can use the auto-confirm feature to verify that the correct amount of XMR was sent to your wallet so that Haveno can automatically mark the trade as complete, making trades quicker for everyone.\n\nAuto-confirm checks the XMR transaction on at least 2 XMR explorer nodes using the private transaction key provided by the XMR sender. By default, Haveno uses explorer nodes run by Haveno contributors, but we recommend running your own XMR explorer node for maximum privacy and security.\n\nYou can also set the maximum amount of XMR per trade to auto-confirm as well as the number of required confirmations here in Settings.\n\nSee more details (including how to set up your own explorer node) on the Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades] #################################################################### # Account #################################################################### account.tab.mediatorRegistration=Mediator registration account.tab.refundAgentRegistration=Refund agent registration account.tab.signing=Signing account.info.headline=Chào mừng đến với tài khoản Haveno của bạn account.info.msg=Here you can add trading accounts for national currencies & cryptos and create a backup of your wallet & account data.\n\nA new Monero wallet was created the first time you started Haveno.\n\nWe strongly recommend that you write down your Monero wallet seed words (see tab on the top) and consider adding a password before funding. Monero deposits and withdrawals are managed in the \"Funds\" section.\n\nPrivacy & security note: because Haveno is a decentralized exchange, all your data is kept on your computer. There are no servers, so we have no access to your personal info, your funds, or even your IP address. Data such as bank account numbers, crypto & Monero addresses, etc are only shared with your trading partner to fulfill trades you initiate (in case of a dispute the mediator or arbitrator will see the same data as your trading peer). account.menu.paymentAccount=Tài khoản tiền tệ quốc gia account.menu.altCoinsAccountView=Tài khoản Crypto account.menu.password=Password ví account.menu.seedWords=Mã sao lưu dự phòng ví account.menu.walletInfo=Wallet info account.menu.backup=Dự phòng account.menu.notifications=Thông báo account.menu.walletInfo.balance.headLine=Wallet balances account.menu.walletInfo.balance.info=This shows the internal wallet balance including unconfirmed transactions.\nFor XMR, the internal wallet balance shown below should match the sum of the 'Available' and 'Reserved' balances shown in the top right of this window. account.menu.walletInfo.xpub.headLine=Watch keys (xpub keys) account.menu.walletInfo.walletSelector={0} {1} wallet account.menu.walletInfo.path.headLine=HD keychain paths account.menu.walletInfo.path.info=If you import seed words into another wallet (like Electrum), you'll need to define the path. This should only be done in emergency cases when you lose access to the Haveno wallet and data directory.\nKeep in mind that spending funds from a non-Haveno wallet can bungle the internal Haveno data structures associated with the wallet data, which can lead to failed trades.\n\nNEVER send BSQ from a non-Haveno wallet, as it will probably lead to an invalid BSQ transaction and losing your BSQ. account.menu.walletInfo.openDetails=Show raw wallet details and private keys ## TODO should we rename the following to a gereric name? account.arbitratorRegistration.pubKey=Public key (địa chỉ ví) account.arbitratorRegistration.register=Register account.arbitratorRegistration.registration={0} registration account.arbitratorRegistration.revoke=Hủy account.arbitratorRegistration.info.msg=Please note that you need to stay available for 15 days after revoking as there might be trades which are using you as {0}. The max. allowed trade period is 8 days and the dispute process might take up to 7 days. account.arbitratorRegistration.warn.min1Language=Bạn cần cài đặt ít nhất 1 ngôn ngữ.\nChúng tôi thêm ngôn ngữ mặc định cho bạn. account.arbitratorRegistration.removedSuccess=You have successfully removed your registration from the Haveno network. account.arbitratorRegistration.removedFailed=Could not remove registration.{0} account.arbitratorRegistration.registerSuccess=You have successfully registered to the Haveno network. account.arbitratorRegistration.registerFailed=Could not complete registration.{0} account.crypto.yourCryptoAccounts=Tài khoản crypto của bạn account.crypto.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don't control your keys or (b) which don't use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is not a {2} specialist and cannot help in such cases. account.crypto.popup.wallet.confirm=Tôi hiểu và xác nhận rằng tôi đã biết loại ví mình cần sử dụng. # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=Trading UPX on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending UPX, you need to use either the official uPlexa GUI wallet or uPlexa CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\nuplexa-wallet-cli (use the command get_tx_key)\nuplexa-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The UPX sender is responsible for providing verification of the UPX transfer to the arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit uPlexa discord channel (https://discord.gg/vhdNSrV) or the uPlexa Telegram Chat (https://t.me/uplexaOfficial) to find more information. # suppress inspection "UnusedProperty" account.crypto.popup.arq.msg=Trading ARQ on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending ARQ, you need to use either the official ArQmA GUI wallet or ArQmA CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\narqma-wallet-cli (use the command get_tx_key)\narqma-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the mediator or arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The ARQ sender is responsible for providing verification of the ARQ transfer to the mediator or arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit ArQmA discord channel (https://discord.gg/s9BQpJT) or the ArQmA forum (https://labs.arqma.com) to find more information. # suppress inspection "UnusedProperty" account.crypto.popup.xmr.msg=Trading XMR on Haveno requires that you understand the following requirement.\n\nIf selling XMR, you must be able to provide the following information to a mediator or arbitrator in case of a dispute:\n- the transaction key (Tx Key, Tx Secret Key or Tx Private Key)\n- the transaction ID (Tx ID or Tx Hash)\n- the destination address (recipient's address)\n\nSee the wiki for details on where to find this information on popular Monero wallets [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Proving_payments].\nFailure to provide the required transaction data will result in losing disputes.\n\nAlso note that Haveno now offers automatic confirming for XMR transactions to make trades quicker, but you need to enable it in Settings.\n\nSee the wiki for more information about the auto-confirm feature: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. # suppress inspection "UnusedProperty" account.crypto.popup.msr.msg=Trading MSR on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending MSR, you need to use either the official Masari GUI wallet, Masari CLI wallet with the store-tx-info flag enabled (enabled by default) or the Masari web wallet (https://wallet.getmasari.org). Please be sure you can access the tx key as that would be required in case of a dispute.\nmasari-wallet-cli (use the command get_tx_key)\nmasari-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nMasari Web Wallet (goto Account -> transaction history and view details on your sent transaction)\n\nVerification can be accomplished in-wallet.\nmasari-wallet-cli : using command (check_tx_key).\nmasari-wallet-gui : on the Advanced > Prove/Check page.\nVerification can be accomplished in the block explorer \nOpen block explorer (https://explorer.getmasari.org), use the search bar to find your transaction hash.\nOnce transaction is found, scroll to bottom to the 'Prove Sending' area and fill in details as needed.\nYou need to provide the mediator or arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The MSR sender is responsible for providing verification of the MSR transfer to the mediator or arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process, ask for help on the Official Masari Discord (https://discord.gg/sMCwMqs). # suppress inspection "UnusedProperty" account.crypto.popup.blur.msg=Trading BLUR on Haveno requires that you understand and fulfill the following requirements:\n\nTo send BLUR you must use the Blur Network CLI or GUI Wallet. \n\nIf you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\nIf you are using the Blur Network GUI Wallet, the transaction private key and transaction ID can be found conveniently in the "History" tab. Immediately after sending, locate the transaction of interest. Click the "?" symbol in the lower-right corner of the box containing the transaction. You must save this information. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the BLUR transfer using the Blur Transaction Viewer (https://blur.cash/#tx-viewer).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the BLUR sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the Blur Network Discord (https://discord.gg/dMWaqVW). # suppress inspection "UnusedProperty" account.crypto.popup.solo.msg=Trading Solo on Haveno requires that you understand and fulfill the following requirements:\n\nTo send Solo you must use the Solo Network CLI Wallet. \n\nIf you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, 2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the Solo transfer using the Solo Block Explorer by searching for the transaction and then using the "Prove sending" function (https://explorer.minesolo.com/).\n\nfailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the Solo sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the Solo Network Discord (https://discord.minesolo.com/). # suppress inspection "UnusedProperty" account.crypto.popup.cash2.msg=Trading CASH2 on Haveno requires that you understand and fulfill the following requirements:\n\nTo send CASH2 you must use the Cash2 Wallet version 3 or higher. \n\nAfter a transaction is sent, the transaction ID will be displayed. You must save this information. Immediately after sending the transaction, you must use the command 'getTxKey' in simplewallet to retrieve the transaction secret key. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, 2) the transaction secret key, and 3) the recipient's Cash2 address. The mediator or arbitrator will then verify the CASH2 transfer using the Cash2 Block Explorer (https://blocks.cash2.org).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the CASH2 sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the Cash2 Discord (https://discord.gg/FGfXAYN). # suppress inspection "UnusedProperty" account.crypto.popup.qwertycoin.msg=Trading Qwertycoin on Haveno requires that you understand and fulfill the following requirements:\n\nTo send QWC you must use the official QWC Wallet version 5.1.3 or higher. \n\nAfter a transaction is sent, the transaction ID will be displayed. You must save this information. Immediately after sending the transaction, you must use the command 'get_Tx_Key' in simplewallet to retrieve the transaction secret key. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, 2) the transaction secret key, and 3) the recipient's QWC address. The mediator or arbitrator will then verify the QWC transfer using the QWC Block Explorer (https://explorer.qwertycoin.org).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the QWC sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the QWC Discord (https://discord.gg/rUkfnpC). # suppress inspection "UnusedProperty" account.crypto.popup.drgl.msg=Trading Dragonglass on Haveno requires that you understand and fulfill the following requirements:\n\nBecause of the privacy Dragonglass provides, a transaction is not verifiable on the public blockchain. If required, you can prove your payment through the use of your TXN-Private-Key.\nThe TXN-Private Key is a one-time key automatically generated for every transaction that can only be accessed from within your DRGL wallet.\nEither by DRGL-wallet GUI (inside transaction details dialog) or by the Dragonglass CLI simplewallet (using command "get_tx_key").\n\nDRGL version 'Oathkeeper' and higher are REQUIRED for both.\n\nIn case of a dispute, you must provide the mediator or arbitrator the following data:\n- The TXN-Private key\n- The transaction hash\n- The recipient's public address\n\nVerification of payment can be made using the above data as inputs at (http://drgl.info/#check_txn).\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The Dragonglass sender is responsible for providing verification of the DRGL transfer to the mediator or arbitrator in case of a dispute. Use of PaymentID is not required.\n\nIf you are unsure about any part of this process, visit Dragonglass on Discord (http://discord.drgl.info) for help. # suppress inspection "UnusedProperty" account.crypto.popup.ZEC.msg=When using Zcash you can only use the transparent addresses (starting with t), not the z-addresses (private), because the mediator or arbitrator would not be able to verify the transaction with z-addresses. # suppress inspection "UnusedProperty" account.crypto.popup.XZC.msg=When using Zcoin you can only use the transparent (traceable) addresses, not the untraceable addresses, because the mediator or arbitrator would not be able to verify the transaction with untraceable addresses at a block explorer. # suppress inspection "UnusedProperty" account.crypto.popup.grin.msg=GRIN yêu cầu một quá trình tương tác giữa người gửi và người nhận để thực hiện một giao dịch. Vui lòng làm theo hướng dẫn từ trang web của dự án GRIN để gửi và nhận GRIN đúng cách. (người nhận cần phải trực tuyến hoặc ít nhất là trực tuyến trong một khung thời gian nhất định).\n\nHaveno chỉ hỗ trợ ví Grinbox(wallet713) theo định dạng URL.\n\nNgười gửi GRIN phải cung cấp bằng chứng là họ đã gửi GRIN thành công. Nếu ví không thể cung cấp bằng chứng đó, nếu có tranh chấp thì sẽ được giải quyết theo hướng có lợi cho người nhận GRIN. Vui lòng đảm bảo rằng bạn sử dụng phần mềm Grinbox mới nhất có hỗ trợ bằng chứng giao dịch và bạn hiểu quy trình chuyển và nhận GRIN cũng như tạo bằng chứng. \n\nXem https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only để biết thêm thông tin về công cụ bằng chứng Grinbox. # suppress inspection "UnusedProperty" account.crypto.popup.beam.msg=BEAM yêu cầu một quá trình tương tác giữa người gửi và người nhận để thực hiện một giao dịch. \n\nVui lòng làm theo hướng dẫn từ trang web của dự án BEAM để gửi và nhận BEAM đúng cách. (người nhận cần phải trực tuyến hoặc ít nhất là trực tuyến trong một khung thời gian nhất định).\n\nNgười gửi BEAM phải cung cấp bằng chứng là họ đã gửi BEAM thành công. Vui lòng đảm bảo là bạn sử dụng phần mềm ví có thể tạo ra một bằng chứng như vậy. Nếu ví không thể cung cấp bằng chứng đó, nếu có tranh chấp thì sẽ được giải quyết theo hướng có lợi cho người nhận BEAM. # suppress inspection "UnusedProperty" account.crypto.popup.pars.msg=Trading ParsiCoin on Haveno requires that you understand and fulfill the following requirements:\n\nTo send PARS you must use the official ParsiCoin Wallet version 3.0.0 or higher. \n\nYou can Check your Transaction Hash and Transaction Key on Transactions Section on your GUI Wallet (ParsiPay) You need to right Click on the Transaction and then click on show details. \n\nIn the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the Transaction Hash, 2) the Transaction Key, and 3) the recipient's PARS address. The mediator or arbitrator will then verify the PARS transfer using the ParsiCoin Block Explorer (http://explorer.parsicoin.net/#check_payment).\n\nFailure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the ParsiCoin sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\nIf you do not understand these requirements, do not trade on Haveno. First, seek help at the ParsiCoin Discord (https://discord.gg/c7qmFNh). # suppress inspection "UnusedProperty" account.crypto.popup.blk-burnt.msg=To trade burnt blackcoins, you need to know the following:\n\nBurnt blackcoins are unspendable. To trade them on Haveno, output scripts need to be in the form: OP_RETURN OP_PUSHDATA, followed by associated data bytes which, after being hex-encoded, constitute addresses. For example, burnt blackcoins with an address 666f6f (“foo” in UTF-8) will have the following script:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\nTo create burnt blackcoins, one may use the “burn” RPC command available in some wallets.\n\nFor possible use cases, one may look at https://ibo.laboratorium.ee .\n\nAs burnt blackcoins are unspendable, they can not be reselled. “Selling” burnt blackcoins means burning ordinary blackcoins (with associated data equal to the destination address).\n\nIn case of a dispute, the BLK seller needs to provide the transaction hash. # suppress inspection "UnusedProperty" account.crypto.popup.liquidmonero.msg=Trading L-XMR on Haveno requires that you understand the following:\n\nWhen receiving L-XMR for a trade on Haveno, you cannot use the mobile Blockstream Green Wallet app or a custodial/exchange wallet. You must only receive L-XMR into the Liquid Elements Core wallet, or another L-XMR wallet which allows you to obtain the blinding key for your blinded L-XMR address.\n\nIn the event mediation is necessary, or if a trade dispute arises, you must disclose the blinding key for your receiving L-XMR address to the Haveno mediator or refund agent so they can verify the details of your Confidential Transaction on their own Elements Core full node.\n\nFailure to provide the required information to the mediator or refund agent will result in losing the dispute case. In all cases of dispute, the L-XMR receiver bears 100% of the burden of responsibility in providing cryptographic proof to the mediator or refund agent.\n\nIf you do not understand these requirements, do not trade L-XMR on Haveno. account.traditional.yourTraditionalAccounts=Các tài khoản tiền tệ quốc gia của bạn account.backup.title=Ví dự phòng account.backup.location=Vị trí sao lưu account.backup.selectLocation=Chọn vị trí dự phòng account.backup.backupNow=Dự phòng bây giờ (dự phòng không được mã hóa!) account.backup.appDir=Thư mục dữ liệu ứng dụng account.backup.openDirectory=Mở thư mục account.backup.openLogFile=Mở tệp nhật ký account.backup.success=Dự phòng đã lưu thành công tại:\n{0} account.backup.directoryNotAccessible=Thư mục bạn vừa chọn không thể truy cập được. {0} account.password.removePw.button=Gỡ bỏ mật khẩu account.password.removePw.headline=Gỡ bỏ bảo vệ mật khẩu cho ví account.password.setPw.button=Cài đặt mật khẩu account.password.setPw.headline=Cài đặt bảo vệ mật khẩu cho ví account.password.info=Khi bật tính năng bảo vệ bằng mật khẩu, bạn sẽ cần nhập mật khẩu khi khởi chạy ứng dụng, khi rút monero ra khỏi ví và khi hiển thị các từ khóa khôi phục. account.seed.backup.title=Sao lưu từ khóa hạt giống của ví của bạn account.seed.info=Vui lòng ghi chép cả từ khóa hạt giống ví và ngày tháng. Bạn có thể khôi phục ví bất cứ lúc nào với từ khóa hạt giống và ngày tháng đó.\n\nBạn nên ghi chép từ khóa hạt giống trên một tờ giấy. Không nên lưu chúng trên máy tính.\n\nLưu ý rằng từ khóa hạt giống KHÔNG thay thế cho việc sao lưu. Bạn cần tạo một bản sao lưu của toàn bộ thư mục ứng dụng từ màn hình "Tài khoản/Sao lưu" để khôi phục trạng thái và dữ liệu của ứng dụng. account.seed.backup.warning=Vui lòng lưu ý rằng từ khóa hạt giống KHÔNG thay thế cho việc sao lưu.\nBạn cần tạo một bản sao lưu của toàn bộ thư mục ứng dụng từ màn hình "Tài khoản/Sao lưu" để khôi phục trạng thái và dữ liệu của ứng dụng. account.seed.warn.noPw.msg=Bạn đã tạo mật khẩu ví để bảo vệ tránh hiển thị Seed words.\n\nBạn có muốn hiển thị Seed words? account.seed.warn.noPw.yes=Có và không hỏi lại account.seed.enterPw=Nhập mật khẩu để xem seed words account.seed.restore.info=Vui lòng tạo sao lưu dự phòng trước khi tiến hành khôi phục ví từ các từ khởi tạo. Phải hiểu rằng việc khôi phục ví chỉ nên thực hiện trong các trường hợp khẩn cấp và có thể gây sự cố với cơ sở dữ liệu ví bên trong.\nĐây không phải là một cách sao lưu dự phòng! Vui lòng sử dụng sao lưu dự phòng từ thư mục dữ liệu của ứng dụng để khôi phục trạng thái ban đầu của ứng dụng.\n\nSau khi khôi phục ứng dụng sẽ tự động tắt. Sau khi bạn khởi động lại, ứng dụng sẽ tái đồng bộ với mạng Monero. Quá trình này có thể mất một lúc và tiêu tốn khá nhiều CPU, đặc biệt là khi ví đã cũ và có nhiều giao dịch. Vui lòng không làm gián đoạn quá trình này, nếu không bạn có thể sẽ phảỉ xóa file chuỗi SPV một lần nữa hoặc lặp lại quy trình khôi phục. account.seed.restore.ok=Được, hãy thực hiện khôi phục và tắt ứng dụng Haveno #################################################################### # Mobile notifications #################################################################### account.notifications.setup.title=Cài đặt account.notifications.download.label=Tải ứng dụng di động account.notifications.waitingForWebCam=Đang chờ webcam... account.notifications.webCamWindow.headline=Quét mã QR từ điện thoại account.notifications.webcam.label=Dùng webcam account.notifications.webcam.button=Quét mã QR account.notifications.noWebcam.button=Tôi không có webcam account.notifications.erase.label=xóa thông báo trên điện thoại account.notifications.erase.title=Xóa thông báo account.notifications.email.label=Mã tài sản đảm bảo account.notifications.email.prompt=Nhập mã tài sản đảm bảo bạn nhận được từ email account.notifications.settings.title=Cài đặt account.notifications.useSound.label=Bật âm thanh thông báo trên điện thoại account.notifications.trade.label=Nhận tin nhắn giao dịch account.notifications.market.label=Nhận thông báo chào hàng account.notifications.price.label=Nhận thông báo về giá account.notifications.priceAlert.title=Thông báo về giá account.notifications.priceAlert.high.label=Thông báo nếu giá XMR cao hơn account.notifications.priceAlert.low.label=Thông báo nếu giá XMR thấp hơn account.notifications.priceAlert.setButton=Đặt thông báo giá account.notifications.priceAlert.removeButton=Gỡ thông báo giá account.notifications.trade.message.title=Trạng thái giao dịch thay đổi account.notifications.trade.message.msg.conf=Lệnh nạp tiền cho giao dịch có mã là {0} đã được xác nhận. Vui lòng mở ứng dụng Haveno và bắt đầu thanh toán. account.notifications.trade.message.msg.started=Người mua XMR đã tiến hành thanh toán cho giao dịch có mã là {0}. account.notifications.trade.message.msg.completed=Giao dịch có mã là {0} đã hoàn thành. account.notifications.offer.message.title=Chào giá của bạn đã được chấp nhận account.notifications.offer.message.msg=Chào giá mã {0} của bạn đã được chấp nhận account.notifications.dispute.message.title=Tin nhắn tranh chấp mới account.notifications.dispute.message.msg=Bạn nhận được một tin nhắn tranh chấp trong giao dịch có mã {0} account.notifications.marketAlert.title=Thông báo chào giá account.notifications.marketAlert.selectPaymentAccount=Tài khoản thanh toán khớp với chào giá account.notifications.marketAlert.offerType.label=Loại chào giátôi muốn account.notifications.marketAlert.offerType.buy=Chào mua (Tôi muốn bán XMR) account.notifications.marketAlert.offerType.sell=Chào bán (Tôi muốn mua XMR) account.notifications.marketAlert.trigger=Khoảng cách giá chào (%) account.notifications.marketAlert.trigger.info=Khi đặt khoảng cách giá, bạn chỉ nhận được thông báo khi có một chào giá bằng (hoặc cao hơn) giá bạn yêu cầu được đăng lên. Ví dụ: Bạn muốn bán XMR, nhưng chỉ bán với giá cao hơn thị trường hiện tại 2%. Đặt trường này ở mức 2% sẽ đảm bảo là bạn chỉ nhận được thông báo từ những chào giá cao hơn 2%(hoặc hơn) so với giá thị trường hiện tại. account.notifications.marketAlert.trigger.prompt=Khoảng cách phần trăm so với giá thị trường (vd: 2.50%, -0.50%, ...) account.notifications.marketAlert.addButton=Thêm thông báo chào giá account.notifications.marketAlert.manageAlertsButton=Quản lý thông báo chào giá account.notifications.marketAlert.manageAlerts.title=Quản lý thông báo chào giá account.notifications.marketAlert.manageAlerts.header.paymentAccount=tài khoản thanh toán account.notifications.marketAlert.manageAlerts.header.trigger=Giá khởi phát account.notifications.marketAlert.manageAlerts.header.offerType=Loại chào giá account.notifications.marketAlert.message.title=Thông báo chào giá account.notifications.marketAlert.message.msg.below=cao hơn account.notifications.marketAlert.message.msg.above=thấp hơn account.notifications.marketAlert.message.msg=một '{0} {1}' chào giá mới với giá {2} ({3} {4} giá thị trường) và hình thức thanh toán '{5}'đã được đăng lên danh mục chào giá của Haveno.\nMã chào giá: {6}. account.notifications.priceAlert.message.title=Thông báo giá cho {0} account.notifications.priceAlert.message.msg=Thông báo giá của bạn đã được kích hoạt. Giá {0} hiện tại là {1} {2} account.notifications.noWebCamFound.warning=Không tìm thấy webcam.\n\nVui lòng sử dụng lựa chọn email để gửi mã bảo mật và khóa mã hóa từ điện thoại di động của bạn tới ứng dùng Haveno. account.notifications.priceAlert.warning.highPriceTooLow=Giá cao hơn phải lớn hơn giá thấp hơn. account.notifications.priceAlert.warning.lowerPriceTooHigh=Giá thấp hơn phải nhỏ hơn giá cao hơn. #################################################################### # Windows #################################################################### inputControlWindow.headline=Select inputs for transaction inputControlWindow.balanceLabel=Số dư hiện có contractWindow.title=Thông tin khiếu nại contractWindow.dates=Ngày chào giá / Ngày giao dịch contractWindow.xmrAddresses=Địa chỉ Monero người mua XMR / người bán XMR contractWindow.onions=Địa chỉ mạng người mua XMR / người bán XMR contractWindow.accountAge=Tuổi tài khoản người mua XMR/người bán XMR contractWindow.numDisputes=Số khiếu nại người mua XMR / người bán XMR contractWindow.contractHash=Hash của hợp đồng displayAlertMessageWindow.headline=Thông tin quan trọng! displayAlertMessageWindow.update.headline=Thông tin cập nhật quan trọng! displayAlertMessageWindow.update.download=Download: displayUpdateDownloadWindow.downloadedFiles=Files: displayUpdateDownloadWindow.downloadingFile=Đang downloading: {0} displayUpdateDownloadWindow.verifiedSigs=Xác minh chữ ký có khóa: displayUpdateDownloadWindow.status.downloading=Đang downloading files... displayUpdateDownloadWindow.status.verifying=Xác minh chữ ký ... displayUpdateDownloadWindow.button.label=Download chương trình cài đặt và xác minh chữ ký displayUpdateDownloadWindow.button.downloadLater=Download sau displayUpdateDownloadWindow.button.ignoreDownload=Bỏ qua phiên bản này displayUpdateDownloadWindow.headline=Hiện có một cập nhật Haveno mới! displayUpdateDownloadWindow.download.failed.headline=Download không thành công displayUpdateDownloadWindow.download.failed=Download failed.\nPlease download and verify manually at [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.installer.failed=Unable to determine the correct installer. Please download and verify manually at [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.verify.failed=Verification failed.\nPlease download and verify manually at [HYPERLINK:https://haveno.exchange/downloads] displayUpdateDownloadWindow.success=Phiên bản mới đã được download thành công và chữ ký đã được xác minh.\n\nVui lòng mở thư mục download, tắt ứng dụng và cài đặt phiên bản mới. displayUpdateDownloadWindow.download.openDir=Mở thư mục download disputeSummaryWindow.title=Tóm tắt disputeSummaryWindow.openDate=Ngày mở đơn disputeSummaryWindow.role=Vai trò của người giao dịch disputeSummaryWindow.payout=Khoản tiền giao dịch hoàn lại disputeSummaryWindow.payout.getsTradeAmount=XMR {0} nhận được khoản tiền giao dịch hoàn lại disputeSummaryWindow.payout.getsAll=Max. payout to XMR {0} disputeSummaryWindow.payout.custom=Thuế hoàn lại disputeSummaryWindow.payoutAmount.buyer=Khoản tiền hoàn lại của người mua disputeSummaryWindow.payoutAmount.seller=Khoản tiền hoàn lại của người bán disputeSummaryWindow.payoutAmount.invert=Sử dụng người thua như người công bố disputeSummaryWindow.reason=Lý do khiếu nại disputeSummaryWindow.tradePeriodEnd=Trade period end disputeSummaryWindow.extraInfo=Extra information disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BUG=Sự cố # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.USABILITY=Khả năng sử dụng # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Vi phạm giao thức # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.NO_REPLY=Không có phản hồi # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SCAM=Scam # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OTHER=Khác # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BANK_PROBLEMS=Ngân hàng # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OPTION_TRADE=Option trade # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Trader not responding # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled disputeSummaryWindow.summaryNotes=Lưu ý tóm tắt disputeSummaryWindow.addSummaryNotes=Thêm lưu ý tóm tắt disputeSummaryWindow.close.button=Đóng đơn # Do no change any line break or order of tokens as the structure is used for signature verification # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.msg=Ticket closed on {0}\n{1} node address: {2}\n\nSummary:\nTrade ID: {3}\nCurrency: {4}\nTrade amount: {5}\nPayout amount for XMR buyer: {6}\nPayout amount for XMR seller: {7}\n\nReason for dispute: {8}\n\nSummary notes:\n{9}\n # Do no change any line break or order of tokens as the structure is used for signature verification disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3} disputeSummaryWindow.close.nextStepsForMediation=\nNext steps:\nOpen trade and accept or reject suggestion from mediator disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\nNo further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions disputeSummaryWindow.close.closePeer=Bạn cũng cần phải đóng Đơn Đối tác giao dịch! disputeSummaryWindow.close.txDetails.headline=Publish refund transaction # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.buyer=Buyer receives {0} on address: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=Seller receives {0} on address: {1}\n disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3}\n\nAre you sure you want to publish this transaction? disputeSummaryWindow.close.noPayout.headline=Close without any payout disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? emptyWalletWindow.headline=Công cụ ví khẩn cấp emptyWalletWindow.info=Vui lòng chỉ sử dụng trong trường hợp khẩn cấp nếu bạn không thể truy cập vốn của bạn từ UI.\n\nLưu ý rằng tất cả Báo giá mở sẽ được tự động đóng khi sử dụng công cụ này.\n\nTrước khi sử dụng công cụ này, vui lòng sao lưu dự phòng thư mục dữ liệu của bạn. Bạn có thể sao lưu tại \"Tài khoản/Sao lưu dự phòng\".\n\nVui lòng báo với chúng tôi vấn đề của bạn và lập báo cáo sự cố trên GitHub hoặc diễn đàn Haveno để chúng tôi có thể điều tra điều gì gây nên vấn đề đó. emptyWalletWindow.balance=Số dư ví hiện tại của bạn emptyWalletWindow.address=Địa chỉ đến của bạn emptyWalletWindow.button=Gửi tất cả vốn emptyWalletWindow.openOffers.warn=Bạn có chào giá mở sẽ được gỡ bỏ khi bạn rút hết trong ví.\nBạn có chắc chắn muốn rút hết ví của bạn? emptyWalletWindow.openOffers.yes=Vâng, tôi chắc chắn emptyWalletWindow.sent.success=Số dư trong ví của bạn đã được chuyển thành công. enterPrivKeyWindow.headline=Enter private key for registration filterWindow.headline=Chỉnh sửa danh sách lọc filterWindow.offers=Chào giá đã lọc (cách nhau bằng dấu phẩy) filterWindow.onions=Banned from trading addresses (comma sep.) filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.) filterWindow.accounts=Dữ liệu tài khoản giao dịch đã lọc:\nĐịnh dạng: cách nhau bằng dấu phẩy danh sách [ID phương thức thanh toán | trường dữ liệu | giá trị] filterWindow.bannedCurrencies=Mã tiền tệ đã lọc (cách nhau bằng dấu phẩy) filterWindow.bannedPaymentMethods=ID phương thức thanh toán đã lọc (cách nhau bằng dấu phẩy) filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) filterWindow.arbitrators=Các trọng tài đã lọc (địa chỉ onion cách nhau bằng dấu phẩy) filterWindow.mediators=Filtered mediators (comma sep. onion addresses) filterWindow.refundAgents=Filtered refund agents (comma sep. onion addresses) filterWindow.seedNode=Node cung cấp thông tin đã lọc (địa chỉ onion cách nhau bằng dấu phẩy) filterWindow.priceRelayNode=nút rơle giá đã lọc (địa chỉ onion cách nhau bằng dấu phẩy) filterWindow.xmrNode=nút Monero đã lọc (địa chỉ cách nhau bằng dấu phẩy + cửa) filterWindow.preventPublicXmrNetwork=Ngăn sử dụng mạng Monero công cộng filterWindow.disableAutoConf=Disable auto-confirm filterWindow.autoConfExplorers=Filtered auto-confirm explorers (comma sep. addresses) filterWindow.disableTradeBelowVersion=Phiên bản tối thiể yêu cầu cho giao dịch filterWindow.add=Thêm bộ lọc filterWindow.remove=Gỡ bỏ bộ lọc filterWindow.xmrFeeReceiverAddresses=XMR fee receiver addresses filterWindow.disableApi=Disable API filterWindow.disableMempoolValidation=Disable Mempool Validation offerDetailsWindow.minXmrAmount=Giá trị XMR tối thiểu offerDetailsWindow.min=(min. {0}) offerDetailsWindow.distance=(chênh lệch so với giá thị trường: {0}) offerDetailsWindow.myTradingAccount=itài khoản giao dịch của tôi offerDetailsWindow.offererBankId=(ID/BIC/SWIFT ngân hàng của người tạo) offerDetailsWindow.offerersBankName=(tên ngân hàng của người tạo) offerDetailsWindow.bankId=ID ngân hàng (VD: BIC hoặc SWIFT) offerDetailsWindow.countryBank=Quốc gia ngân hàng của người tạo offerDetailsWindow.commitment=Cam kết offerDetailsWindow.agree=Tôi đồng ý offerDetailsWindow.tac=Điều khoản và điều kiện offerDetailsWindow.confirm.maker.buy=Xác nhận: Tạo đề nghị mua XMR bằng {0} offerDetailsWindow.confirm.maker.sell=Xác nhận: Tạo đề nghị bán XMR lấy {0} offerDetailsWindow.confirm.taker.buy=Xác nhận: Chấp nhận đề nghị mua XMR bằng {0} offerDetailsWindow.confirm.taker.sell=Xác nhận: Chấp nhận đề nghị bán XMR lấy {0} offerDetailsWindow.creationDate=Ngày tạo offerDetailsWindow.makersOnion=Địa chỉ onion của người tạo offerDetailsWindow.challenge=Mã bảo vệ giao dịch offerDetailsWindow.challenge.copy=Sao chép cụm mật khẩu để chia sẻ với đối tác của bạn qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. qRCodeWindow.request=Yêu cầu thanh toán:\n{0} selectDepositTxWindow.headline=Chọn giao dịch tiền gửi để khiếu nại selectDepositTxWindow.msg=Giao dịch tiền gửi không được lưu trong danh mục giao dịch.\nVui lòng chọn một trong các giao dịch multisig hiện có từ ví của bạn là giao dịch tiền gửi sử dụng trong danh mục giao dịch không thành công.\n\nBạn có thể tìm đúng giao dịch bằng cách mở cửa sổ thông tin giao dịch (nhấp vào ID giao dịch trong danh sách) và tuân thủ yêu cầu thanh toán phí giao dịch cho giao dịch tiếp theo khi bạn thấy giao dịch tiền gửi multisig (địa chỉ bắt đầu bằng số 3). ID giao dịch sẽ thấy trong danh sách tại đây. Khi bạn thấy đúng giao dịch, chọn giao dịch đó và tiếp tục.\n\nXin lỗi vì sự bất tiện này nhưng thỉnh thoảng sẽ có lỗi xảy ra và trong tương lai chúng tôi sẽ tìm cách tốt hơn để xử lý vấn đề này. selectDepositTxWindow.select=Chọn giao dịch tiền gửi sendAlertMessageWindow.headline=Gửi thông báo toàn cầu sendAlertMessageWindow.alertMsg=Tin nhắn cảnh báo sendAlertMessageWindow.enterMsg=Nhận tin nhắn sendAlertMessageWindow.isSoftwareUpdate=Software download notification sendAlertMessageWindow.isUpdate=Is full release sendAlertMessageWindow.isPreRelease=Is pre-release sendAlertMessageWindow.version=Phiên bản mới số sendAlertMessageWindow.send=Gửi thông báo sendAlertMessageWindow.remove=Gỡ bỏ thông báo sendPrivateNotificationWindow.headline=Gửi tin nhắn riêng tư sendPrivateNotificationWindow.privateNotification=Thông báo riêng tư sendPrivateNotificationWindow.enterNotification=Nhập thông báo sendPrivateNotificationWindow.send=Gửi thông báo riêng tư showWalletDataWindow.walletData=Dữ liệu ví showWalletDataWindow.includePrivKeys=Bao gồm khóa cá nhân setXMRTxKeyWindow.headline=Prove sending of XMR setXMRTxKeyWindow.note=Adding tx info below enables auto-confirm for quicker trades. See more: https://haveno.exchange/wiki/Trading_Monero setXMRTxKeyWindow.txHash=Transaction ID (optional) setXMRTxKeyWindow.txKey=Transaction key (optional) # We do not translate the tac because of the legal nature. We would need translations checked by lawyers # in each language which is too expensive atm. tacWindow.headline=Thỏa thuận người dùng tacWindow.agree=Tôi đồng ý tacWindow.disagree=Tôi không đồng ý và thoát tacWindow.arbitrationSystem=Dispute resolution tradeDetailsWindow.headline=giao dịch tradeDetailsWindow.disputedPayoutTxId=ID giao dịch hoàn tiền khiếu nại tradeDetailsWindow.tradeDate=Ngày giao dịch tradeDetailsWindow.txFee=Phí đào tradeDetailsWindow.tradePeersOnion=Địa chỉ onion Đối tác giao dịch tradeDetailsWindow.tradePeersPubKeyHash=Trading peers pubkey hash tradeDetailsWindow.tradeState=Trạng thái giao dịch tradeDetailsWindow.agentAddresses=Arbitrator/Mediator tradeDetailsWindow.detailData=Detail data txDetailsWindow.headline=Transaction Details txDetailsWindow.xmr.noteSent=Bạn đã gửi XMR. txDetailsWindow.xmr.noteReceived=Bạn đã nhận được XMR. txDetailsWindow.sentTo=Gửi đến txDetailsWindow.receivedWith=Đã nhận với txDetailsWindow.txId=TxId closedTradesSummaryWindow.headline=Trade history summary closedTradesSummaryWindow.totalAmount.title=Total trade amount closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) closedTradesSummaryWindow.totalTradeFeeInXmr.title=Sum of all trade fees paid in XMR closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} of total trade amount) walletPasswordWindow.headline=Nhập mật khẩu để mở khóa xmrConnectionError.headline=Lỗi kết nối Monero xmrConnectionError.providedNodes=Lỗi khi kết nối tới (các) nút Monero được cung cấp.\n\nBạn có muốn sử dụng nút Monero khả dụng tốt nhất tiếp theo không? xmrConnectionError.customNodes=Lỗi khi kết nối tới (các) nút Monero tùy chỉnh của bạn.\n\nBạn có muốn sử dụng nút Monero khả dụng tốt nhất tiếp theo không? xmrConnectionError.localNode=Haveno trước đây đã kết nối với một nút Monero cục bộ, nhưng hiện không còn có thể kết nối được.\n\nHãy đảm bảo nút cục bộ của bạn đang chạy và đã được đồng bộ hoàn toàn, hoặc chọn một tùy chọn khác để tiếp tục. xmrConnectionError.localNode.start=Khởi động nút cục bộ xmrConnectionError.localNode.start.error=Lỗi khi khởi động nút cục bộ xmrConnectionError.localNode.fallback=Kết nối tới nút khả dụng tốt nhất tiếp theo torNetworkSettingWindow.header=Cài đặt mạng Tor torNetworkSettingWindow.noBridges=Không sử dụng cầu nối torNetworkSettingWindow.providedBridges=Nối với cầu nối được cung cấp torNetworkSettingWindow.customBridges=Nhập cầu nối thông dụng torNetworkSettingWindow.transportType=Loại hình vận chuyển torNetworkSettingWindow.obfs3=obfs3 torNetworkSettingWindow.obfs4=obfs4 (khuyến cáo) torNetworkSettingWindow.meekAmazon=meek-amazon torNetworkSettingWindow.meekAzure=meek-azure torNetworkSettingWindow.enterBridge=Nhập một hoặc nhiều chuyển tiếp cầu (một trên một dòng) torNetworkSettingWindow.enterBridgePrompt=địa chỉ loại:cổng torNetworkSettingWindow.restartInfo=Bạn cần phải khởi động lại để thay đổi torNetworkSettingWindow.openTorWebPage=Mở trang web dự án Tor torNetworkSettingWindow.deleteFiles.header=Sự cố kết nối? torNetworkSettingWindow.deleteFiles.info=Nếu sự cố kết nối lặp lại khi khởi động, có thể xóa các file Tor đã hết hạn. Để xóa, ấn vào nút dưới đây và sau đó khởi động lại. torNetworkSettingWindow.deleteFiles.button=Xóa các file Tor đã hết hạn và tắt torNetworkSettingWindow.deleteFiles.progress=Đang tắt Tor torNetworkSettingWindow.deleteFiles.success=Các file Tor lỗi thời đã xóa thành công. Vui lòng khởi động lại. torNetworkSettingWindow.bridges.header=Tor bị khoá? torNetworkSettingWindow.bridges.info=Nếu Tor bị kẹt do nhà cung cấp internet hoặc quốc gia của bạn, bạn có thể thử dùng cầu nối Tor.\nTruy cập trang web Tor tại: https://bridges.torproject.org để biết thêm về cầu nối và phương tiện vận chuyển kết nối được. feeOptionWindow.headline=Chọn đồng tiền để thanh toán phí giao dịch feeOptionWindow.info=Bạn có thể chọn thanh toán phí giao dịch bằng BSQ hoặc XMR. Nếu bạn chọn BSQ, bạn sẽ được khấu trừ phí giao dịch. feeOptionWindow.optionsLabel=Chọn đồng tiền để thanh toán phí giao dịch feeOptionWindow.useXMR=Sử dụng XMR feeOptionWindow.fee={0} (≈ {1}) feeOptionWindow.xmrFeeWithFiatAndPercentage={0} (≈ {1} / {2}) feeOptionWindow.xmrFeeWithPercentage={0} ({1}) #################################################################### # Popups #################################################################### popup.headline.notification=Thông báo popup.headline.instruction=Lưu ý rằng: popup.headline.attention=Chú ý popup.headline.backgroundInfo=Thông tin cơ bản popup.headline.feedback=Đã hoàn thành popup.headline.confirmation=Xác nhận popup.headline.information=Thông tin popup.headline.warning=Cảnh báo popup.headline.error=Lỗi popup.doNotShowAgain=Không hiển thị lại popup.reportError.log=Mở log file popup.reportError.gitHub=Báo cáo cho người theo dõi vấn đề GitHub popup.reportError={0}\n\nĐể giúp chúng tôi cải tiến phần mềm, vui lòng báo cáo lỗi này bằng cách mở một thông báo vấn đề mới tại https://github.com/haveno-dex/haveno/issues.\nTin nhắn lỗi phía trên sẽ được sao chép tới clipboard khi bạn ấn vào một nút bên dưới.\nSự cố sẽ được xử lý dễ dàng hơn nếu bạn đính kèm haveno.log file bằng cách nhấn "Mở log file", lưu bản sao, và đính kèm vào báo cáo lỗi. popup.error.tryRestart=Hãy khởi động lại ứng dụng và kiểm tra kết nối mạng để xem bạn có thể xử lý vấn đề này hay không. popup.error.takeOfferRequestFailed=Có lỗi xảy ra khi ai đó cố gắng để nhận một trong các chào giá của bạn:\n{0} error.spvFileCorrupted=Có lỗi xảy ra khi đọc SPV chain file.\nCó thể SPV chain file bị hỏng.\n\nTin nhắn lỗi: {0}\n\nBạn có muốn xóa và bắt đầu đồng bộ hóa? error.deleteAddressEntryListFailed=Không thể xóa AddressEntryList file.\nError: {0} error.closedTradeWithUnconfirmedDepositTx=The deposit transaction of the closed trade with the trade ID {0} is still unconfirmed.\n\nPlease do a SPV resync at \"Setting/Network info\" to see if the transaction is valid. error.closedTradeWithNoDepositTx=The deposit transaction of the closed trade with the trade ID {0} is null.\n\nPlease restart the application to clean up the closed trades list. popup.warning.walletNotInitialized=Ví chưa được kích hoạt popup.warning.osxKeyLoggerWarning=Due to stricter security measures in macOS 10.14 and above, launching a Java application (Haveno uses Java) causes a popup warning in macOS ('Haveno would like to receive keystrokes from any application').\n\nTo avoid that issue please open your 'macOS Settings' and go to 'Security & Privacy' -> 'Privacy' -> 'Input Monitoring' and Remove 'Haveno' from the list on the right side.\n\nHaveno will upgrade to a newer Java version to avoid that issue as soon the technical limitations (Java packager for the required Java version is not shipped yet) are resolved. popup.warning.wrongVersion=Có thể máy tính của bạn có phiên bản Haveno không đúng.\nCấu trúc máy tính của bạn là: {0}.\nHệ nhị phân Haveno bạn cài đặt là: {1}.\nVui lòng tắt máy và cài đặt lại phiên bản đúng ({2}). popup.warning.incompatibleDB=We detected incompatible data base files!\n\nThose database file(s) are not compatible with our current code base:\n{0}\n\nWe made a backup of the corrupted file(s) and applied the default values to a new database version.\n\nThe backup is located at:\n{1}/db/backup_of_corrupted_data.\n\nPlease check if you have the latest version of Haveno installed.\nYou can download it at: [HYPERLINK:https://haveno.exchange/downloads].\n\nPlease restart the application. popup.warning.startupFailed.twoInstances=Haveno đã chạy. Bạn không thể chạy hai chương trình Haveno. popup.warning.tradePeriod.halfReached=giao dịch của bạn với ID {0} đã qua một nửa thời gian giao dịch cho phép tối đa và vẫn chưa hoàn thành.\n\nThời gian giao dịch kết thúc vào {1}\n\nVui lòng kiểm tra trạng thái giao dịch của bạn tại \"Portfolio/Các giao dịch mở\" để biết thêm thông tin. popup.warning.tradePeriod.ended=Your trade with ID {0} has reached the max. allowed trading period and is not completed.\n\nThe trade period ended on {1}\n\nPlease check your trade at \"Portfolio/Open trades\" for contacting the arbitrator. popup.warning.noTradingAccountSetup.headline=Bạn chưa thiết lập tài khoản giao dịch popup.warning.noTradingAccountSetup.msg=Bạn cần thiết lập tiền tệ quốc gia hoặc tài khoản crypto trước khi tạo Báo giá.\nDBạn có muốn thiết lập tài khoản? popup.warning.noArbitratorsAvailable=Hiện không có trọng tài nào popup.warning.noMediatorsAvailable=There are no mediators available. popup.warning.notFullyConnected=Bạn cần phải đợi cho đến khi kết nối hoàn toàn với mạng.\nĐiều này mất khoảng 2 phút khi khởi động. popup.warning.notSufficientConnectionsToXmrNetwork=Bạn cần phải đợi cho đến khi bạn có ít nhất {0} kết nối với mạng Monero. popup.warning.downloadNotComplete=Bạn cần phải đợi cho đến khi download xong các block Monero còn thiếu. popup.warning.walletNotSynced=Ví Haveno chưa được đồng bộ với chiều cao khối chuỗi khối mới nhất. Vui lòng đợi cho đến khi ví được đồng bộ hoặc kiểm tra kết nối của bạn. popup.warning.removeOffer=Bạn có chắc bạn muốn gỡ bỏ Báo giá này? popup.warning.tooLargePercentageValue=Bạn không thể cài đặt phần trăm là 100% hoặc cao hơn. popup.warning.examplePercentageValue=Vui lòng nhập số phần trăm như \"5.4\" cho 5,4% popup.warning.noPriceFeedAvailable=Không có giá cung cấp cho tiền tệ này. Bạn không thể sử dụng giá dựa trên tỷ lệ.\nVui lòng chọn giá cố định. popup.warning.sendMsgFailed=Gửi tin nhắn Đối tác giao dịch không thành công.\nVui lòng thử lại và nếu tiếp tục không thành công thì báo cáo sự cố. popup.warning.messageTooLong=Tin nhắn của bạn vượt quá kích cỡ tối đa cho phép. Vui lòng gửi thành nhiều lần hoặc tải lên mạng như https://pastebin.com. popup.warning.lockedUpFunds=You have locked up funds from a failed trade.\nLocked up balance: {0} \nDeposit tx address: {1}\nTrade ID: {2}.\n\nPlease open a support ticket by selecting the trade in the open trades screen and pressing \"alt + o\" or \"option + o\"." popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n takeOffer.cancelButton=Cancel take-offer takeOffer.warningButton=Ignore and continue anyway # suppress inspection "UnusedProperty" popup.warning.nodeBanned=One of the {0} nodes got banned. # suppress inspection "UnusedProperty" popup.warning.priceRelay=rơle giá popup.warning.seed=seed popup.warning.mandatoryUpdate.trading=Please update to the latest Haveno version. A mandatory update was released which disables trading for old versions. Please check out the Haveno Forum for more information. popup.warning.noFilter=Chúng tôi không nhận được đối tượng bộ lọc từ các nút hạt giống. Vui lòng thông báo cho quản trị viên mạng để đăng ký một đối tượng bộ lọc. popup.warning.burnXMR=Không thể thực hiện giao dịch, vì phí đào {0} vượt quá số lượng {1} cần chuyển. Vui lòng chờ tới khi phí đào thấp xuống hoặc khi bạn tích lũy đủ XMR để chuyển. popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Monero network.\nTransaction ID={1}.\nThe offer has been removed to avoid further problems.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. popup.warning.trade.txRejected.tradeFee=trade fee popup.warning.trade.txRejected.deposit=deposit popup.warning.trade.txRejected=The {0} transaction for trade with ID {1} was rejected by the Monero network.\nTransaction ID={2}\nThe trade has been moved to failed trades.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. popup.warning.openOfferWithInvalidMakerFeeTx=The maker fee transaction for offer with ID {0} is invalid.\nTransaction ID={1}.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. popup.info.securityDepositInfo=Để đảm bảo cả hai người giao dịch đều tuân thủ giao thức giao dịch, cả hai cần phải trả một khoản tiền cọc. \n\nSố tiền cọc này được giữ ở ví giao dịch cho đến khi giao dịch của bạn được hoàn thành, sau đó nó sẽ được trả lại cho bạn. \nXin lưu ý: Nếu bạn tạo một chào giá mới, ứng dụng Haveno cần phải chạy để người giao dịch khác có thể nhận chào giá đó. Để giữ cho chào giá của bạn online, để Haveno chạy và đảm bảo là máy tính của bạn cũng online (nghĩa là đảm bảo là máy tính của bạn không chuyển qua chế độ standby, nếu màn hình chuyển qua chế độ standby thì không sao). popup.info.cashDepositInfo=Chắc chắn rằng khu vực của bạn có chi nhánh ngân hàng có thể gửi tiền mặt.\nID (BIC/SWIFT) ngân hàng của bên bán là: {0}. popup.info.cashDepositInfo.confirm=Tôi xác nhận tôi đã gửi tiền popup.info.shutDownWithOpenOffers=Haveno đang đóng, nhưng vẫn có các chào giá đang mở. \n\nNhững chào giá này sẽ không có tại mạng P2P khi Haveno đang đóng, nhưng chúng sẽ được công bố lại trên mạng P2P vào lần tiếp theo bạn khởi động Haveno.\nĐể giữ các chào giá luôn trực tuyến, vui lòng để Haveno chạy và đảm bảo là máy tính của bạn cũng đang trực tuyến(có nghĩa là đảm bảo là máy tính của bạn không chuyển về chế độ chờ...nếu màn hình về chế độ chờ thì không sao). popup.info.qubesOSSetupInfo=It appears you are running Haveno on Qubes OS. \n\nPlease make sure your Haveno qube is setup according to our Setup Guide at [HYPERLINK:https://haveno.exchange/wiki/Running_Haveno_on_Qubes]. popup.warn.downGradePrevention=Downgrade from version {0} to version {1} is not supported. Please use the latest Haveno version. popup.privateNotification.headline=Thông báo riêng tư quan trọng! popup.securityRecommendation.headline=Khuyến cáo an ninh quan trọng popup.securityRecommendation.msg=Chúng tôi muốn nhắc nhở bạn sử dụng bảo vệ bằng mật khẩu cho ví của bạn nếu bạn vẫn chưa sử dụng.\n\nChúng tôi cũng khuyên bạn nên viết Seed words ví của bạn ra giấy. Các Seed words này như là mật khẩu chủ để khôi phục ví Monero của bạn.\nBạn có thể xem thông tin ở mục \"Wallet Seed\".\n\nNgoài ra bạn nên sao lưu dự phòng folder dữ liệu ứng dụng đầy đủ ở mục \"Backup\". popup.shutDownInProgress.headline=Đang tắt ứng dụng popup.shutDownInProgress.msg=Tắt ứng dụng sẽ mất vài giây.\nVui lòng không gián đoạn quá trình này. popup.attention.forTradeWithId=Cần chú ý khi giao dịch có ID {0} popup.attention.reasonForPaymentRuleChange=Version 1.5.5 introduces a critical trade rule change regarding the \"reason for payment\" field in bank transfers. Please leave this field empty -- DO NOT use the trade ID as \"reason for payment\" anymore. popup.info.multiplePaymentAccounts.headline=Có sẵn nhiều tài khoản thanh toán popup.info.multiplePaymentAccounts.msg=Bạn có sẵn nhiều tài khoản thanh toán cho chào giá này. Vui lòng đảm bảo là bạn chọn đúng tài khoản. popup.accountSigning.selectAccounts.headline=Select payment accounts popup.accountSigning.selectAccounts.description=Based on the payment method and point of time all payment accounts that are connected to a dispute where a payout to the buyer occurred will be selected for you to sign. popup.accountSigning.selectAccounts.signAll=Sign all payment methods popup.accountSigning.selectAccounts.datePicker=Select point of time until which accounts will be signed popup.accountSigning.confirmSelectedAccounts.headline=Confirm selected payment accounts popup.accountSigning.confirmSelectedAccounts.description=Based on your input, {0} payment accounts will be selected. popup.accountSigning.confirmSelectedAccounts.button=Confirm payment accounts popup.accountSigning.signAccounts.headline=Confirm signing of payment accounts popup.accountSigning.signAccounts.description=Based on your selection, {0} payment accounts will be signed. popup.accountSigning.signAccounts.button=Sign payment accounts popup.accountSigning.signAccounts.ECKey=Enter private arbitrator key popup.accountSigning.signAccounts.ECKey.error=Bad arbitrator ECKey popup.accountSigning.success.headline=Congratulations popup.accountSigning.success.description=All {0} payment accounts were successfully signed! popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\nFor further information, please visit [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer's account after a successful trade.\n\n{0} popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you'll be able to sign other accounts in {0} days from now.\n\n{1} popup.accountSigning.peerLimitLifted=The initial limit for one of your accounts has been lifted.\n\n{0} popup.accountSigning.peerSigner=One of your accounts is mature enough to sign other payment accounts and the initial limit for one of your accounts has been lifted.\n\n{0} popup.accountSigning.singleAccountSelect.headline=Import unsigned account age witness popup.accountSigning.confirmSingleAccount.headline=Confirm selected account age witness popup.accountSigning.confirmSingleAccount.selectedHash=Selected witness hash popup.accountSigning.confirmSingleAccount.button=Sign account age witness popup.accountSigning.successSingleAccount.description=Witness {0} was signed popup.accountSigning.successSingleAccount.success.headline=Success popup.accountSigning.unsignedPubKeys.headline=Unsigned Pubkeys popup.accountSigning.unsignedPubKeys.sign=Sign Pubkeys popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign popup.info.buyerAsTakerWithoutDeposit.headline=Không cần đặt cọc từ người mua popup.info.buyerAsTakerWithoutDeposit=Lời đề nghị của bạn sẽ không yêu cầu khoản đặt cọc bảo mật hoặc phí từ người mua XMR.\n\nĐể chấp nhận lời đề nghị của bạn, bạn phải chia sẻ một mật khẩu với đối tác giao dịch ngoài Haveno.\n\nMật khẩu được tạo tự động và hiển thị trong chi tiết lời đề nghị sau khi tạo. #################################################################### # Notifications #################################################################### notification.trade.headline=Thông báo với giao dịch có ID {0} notification.ticket.headline=Vé hỗ trợ cho giao dịch có ID {0} notification.trade.completed=giao dịch đã hoàn thành và bạn có thể rút tiền. notification.trade.accepted=Chào giá của bạn đã được chấp thuận bởi XMR {0}. notification.trade.unlocked=giao dịch của bạn có ít nhất một xác nhận blockchain.\nBạn có thể bắt đầu thanh toán bây giờ. notification.trade.paymentSent=Người mua XMR đã bắt đầu thanh toán. notification.trade.selectTrade=Lựa chọn giao dịch notification.trade.peerOpenedDispute=Đối tác giao dịch của bạn đã mở một {0}. notification.trade.disputeClosed={0} đã đóng. notification.walletUpdate.headline=Cập nhật ví giao dịch notification.walletUpdate.msg=Ví giao dịch của bạn không được nạp đủ tiền.\nSố tiền: {0} notification.takeOffer.walletUpdate.msg=Ví giao dịch của bạn đã được nạp đủ tiền từ lần nhận báo giá trước.\nSố tiền: {0} notification.tradeCompleted.headline=giao dịch đã hoàn thành notification.tradeCompleted.msg=Bạn có thể rút tiền từ ví Monero ngoại của bạn hoặc giữ chúng trong ví Haveno của bạn. #################################################################### # System Tray #################################################################### systemTray.show=Hiển thị cửa sổ ứng dung systemTray.hide=Ẩn cửa sổ ứng dụng systemTray.info=Thông tin về Haveno systemTray.exit=Thoát systemTray.tooltip=Haveno: A decentralized monero exchange network #################################################################### # GUI Util #################################################################### guiUtil.accountExport.savedToPath=tài khoản giao dịch được lưu vào đường dẫn:\n{0} guiUtil.accountExport.noAccountSetup=Bạn không có tài khoản giao dịch được thiết lập để truy xuất. guiUtil.accountExport.selectPath=Chọn đường dẫn đến {0} # suppress inspection "TrailingSpacesInProperty" guiUtil.accountExport.tradingAccount=tài khoản giao dịch với ID {0}\n # suppress inspection "TrailingSpacesInProperty" guiUtil.accountImport.noImport=Chúng tôi không truy nhập tài khoản giao dịch với ID {0} do nó đã tồn tại.\n guiUtil.accountExport.exportFailed=Truy xuất tới CSV không thành công do có lỗi.\nError = {0} guiUtil.accountExport.selectExportPath=Lựa chọn đường dẫn truy xuất guiUtil.accountImport.imported=tài khoản giao dịch truy nhập từ đường dẫn:\n{0}\n\nTài khoản truy nhập:\n{1} guiUtil.accountImport.noAccountsFound=Không tìm thấy tài khoản giao dịch truy xuất tại đường dẫn: {0}.\nTên file là {1}." guiUtil.openWebBrowser.warning=Bạn sẽ mở một trang web trong trình duyệt trang web của hệ thống.\nBạn có muốn mở trang web bây giờ?\n\nNếu bạn không sử dụng \"Tor Browser\" là trình duyệt web hệ thống mặc định bạn sẽ kết nối với trang web tại clear net.\n\nURL: \"{0}\" guiUtil.openWebBrowser.doOpen=Mở trang web và không hỏi lại guiUtil.openWebBrowser.copyUrl=Copy URL và hủy guiUtil.ofTradeAmount=Giá trị giao dịch guiUtil.requiredMinimum=(required minimum) #################################################################### # Component specific #################################################################### list.currency.select=Lựa chọn tiền tệ list.currency.showAll=Hiển thị tất cả list.currency.editList=Chỉnh sửa danh sách tiền tệ table.placeholder.noItems=Hiện không có {0} nào table.placeholder.noData=Hiện không có dữ liệu nào table.placeholder.processingData=Processing data... peerInfoIcon.tooltip.tradePeer=Đối tác giao dịch peerInfoIcon.tooltip.maker=Người tạo peerInfoIcon.tooltip.trade.traded={0} Địa chỉ onion: {1}\nBạn đã giao dịch {2} lần với đối tác này\n{3} peerInfoIcon.tooltip.trade.notTraded={0} Địa chỉ onion: {1}\nBạn chưa từng giao dịch với đối tác này.\n{2} peerInfoIcon.tooltip.age=Tài khoản thanh toán được tạo cách đây {0}. peerInfoIcon.tooltip.unknownAge=Tuổi tài khoản thanh toán chưa biết. tooltip.openPopupForDetails=Mở cửa sổ để xem chi tiết tooltip.invalidTradeState.warning=This trade is in an invalid state. Open the details window for more information tooltip.openBlockchainForAddress=Mở blockchain explorer ngoài để xem địa chỉ: {0} tooltip.openBlockchainForTx=Mở blockchain explorer ngoài để xem giao dịch: {0} confidence.unknown=Trạng thái giao dịch chưa biết confidence.seen=Đã xem bởi {0} đối tác / 0 xác nhận confidence.confirmed={0} xác nhận confidence.invalid=Giao dịch không có hiệu lực peerInfo.title=Thông tin đối tác peerInfo.nrOfTrades=Số giao dịch đã hoàn thành peerInfo.notTradedYet=Bạn chưa từng giao dịch với người dùng này. peerInfo.setTag=Đặt nhãn cho đối tác này peerInfo.age.noRisk=Tuổi tài khoản thanh toán peerInfo.age.chargeBackRisk=Time since signing peerInfo.unknownAge=Tuổi chưa biết addressTextField.openWallet=Mở ví Monero mặc định của bạn addressTextField.copyToClipboard=Copy địa chỉ vào clipboard addressTextField.addressCopiedToClipboard=Địa chỉ đã được copy vào clipboard addressTextField.openWallet.failed=Mở ứng dụng ví Monero mặc định không thành công. Có lẽ bạn chưa cài đặt? peerInfoIcon.tooltip={0}\nTag: {1} txIdTextField.copyIcon.tooltip=Copy ID giao dịch vào clipboard txIdTextField.blockExplorerIcon.tooltip=Open a blockchain explorer with this transaction ID txIdTextField.missingTx.warning.tooltip=Missing required transaction #################################################################### # Navigation #################################################################### navigation.account=\"Tài khoản\" navigation.account.walletSeed=\"Tài khoản/Khởi tạo ví\" navigation.funds.availableForWithdrawal=\"Funds/Send funds\" navigation.portfolio.myOpenOffers=\"Portfolio/Các Báo giá mở của tôi\" navigation.portfolio.pending=\"Portfolio/Các giao dịch mở\" navigation.portfolio.closedTrades=\"Portfolio/Lịch sử\" navigation.funds.depositFunds=\"Vốn/Nhận vốn\" navigation.settings.preferences=\"Cài đặt/Tham khảo\" # suppress inspection "UnusedProperty" navigation.funds.transactions=\"Vốn/Giao dịch\" navigation.support=\"Hỗ trợ\" #################################################################### # Formatter #################################################################### formatter.formatVolumeLabel={0} giá trị {1} formatter.makerTaker=Người tạo là {0} {1} / Người nhận là {2} {3} formatter.makerTaker.locked=Người tạo là {0} {1} / Người nhận là {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=Bạn là {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Bạn đang tạo một chào giá đến {0} {1} formatter.youAreCreatingAnOffer.traditional.locked=Bạn đang tạo một chào giá đến {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Bạn đang tạo một chào giá đến {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.crypto.locked=Bạn đang tạo một chào giá đến {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} như người tạo formatter.asTaker={0} {1} như người nhận #################################################################### # Domain specific #################################################################### # we use enum values here # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" XMR_MAINNET=Monero Mainnet # suppress inspection "UnusedProperty" XMR_LOCAL=Monero Testnet # suppress inspection "UnusedProperty" XMR_STAGENET=Monero Regtest time.year=Năm time.month=Tháng time.week=Tuần time.day=Ngày time.hour=Giờ time.minute10=10 Phút time.hours=giờ time.days=ngày time.1hour=1 giờ time.1day=1 ngày time.minute=phút time.second=giây time.minutes=phút time.seconds=giây password.enterPassword=Nhập mật khẩu password.confirmPassword=Xác nhận mật khẩu password.tooLong=Mật khẩu phải ít hơn 500 ký tự. password.deriveKey=Lấy khóa từ mật khẩu password.walletDecrypted=Ví đã giải mã thành công và bảo vệ bằng mật khẩu bị gỡ bỏ. password.wrongPw=Bạn nhập sai mật khẩu.\n\nVui lòng nhập lại mật khẩu, kiểm tra lỗi do gõ phí hoặc lỗi chính tả cẩn thận. password.walletEncrypted=Ví đã được mã hóa thành công và bảo vệ bằng mật khẩu được kích hoạt. password.passwordsDoNotMatch=2 mật khẩu bạn nhập không khớp. password.forgotPassword=Quên mật khẩu? password.backupReminder=Please note that when setting a wallet password all automatically created backups from the unencrypted wallet will be deleted.\n\nIt is highly recommended that you make a backup of the application directory and write down your seed words before setting a password! password.backupWasDone=I have already made a backup password.setPassword=Set Password (I already made a backup) password.makeBackup=Make Backup seed.seedWords=Seed words ví seed.enterSeedWords=Nhập seed words ví seed.date=Ngày ví seed.restore.title=Khôi phục vú từ Seed words seed.restore=Khôi phục ví seed.creationDate=Ngày tạo seed.warn.walletNotEmpty.msg=Your Monero wallet is not empty.\n\nYou must empty this wallet before attempting to restore an older one, as mixing wallets together can lead to invalidated backups.\n\nPlease finalize your trades, close all your open offers and go to the Funds section to withdraw your monero.\nIn case you cannot access your monero you can use the emergency tool to empty the wallet.\nTo open the emergency tool press \"Alt+e\" or \"Cmd/Ctrl+e\". seed.warn.walletNotEmpty.restore=Tôi muốn khôi phục seed.warn.walletNotEmpty.emptyWallet=Tôi sẽ làm trống ví trước seed.warn.notEncryptedAnymore=Ví của bạn đã được mã hóa.\n\nSau khi khôi phục, ví sẽ không còn được mã hóa và bạn phải cài đặt mật khẩu mới.\n\nBạn có muốn tiếp tục? seed.warn.walletDateEmpty=As you have not specified a wallet date, haveno will have to scan the blockchain from 2013.10.09 (the BIP39 epoch date).\n\nBIP39 wallets were first introduced in haveno on 2017.06.28 (release v0.5). So you could save time by using that date.\n\nIdeally you should specify the date your wallet seed was created.\n\n\nAre you sure you want to go ahead without specifying a wallet date? seed.restore.success=Ví khôi phục thành công với từ khởi tạo mới.\n\nBạn cần phải tắt và khởi động lại ứng dụng. seed.restore.error=Có lỗi xảy ra khi khôi phục ví với Seed words.{0} seed.restore.openOffers.warn=You have open offers which will be removed if you restore from seed words.\nAre you sure that you want to continue? #################################################################### # Payment methods #################################################################### payment.account=Tài khoản payment.account.no=Tài khoản số payment.account.name=Tên tài khoản payment.account.username=Username payment.account.phoneNr=Phone number payment.account.owner.fullname=Họ tên chủ tài khoản payment.account.fullName=Họ tên (họ, tên lót, tên) payment.account.state=Bang/Tỉnh/Vùng payment.account.city=Thành phố payment.bank.country=Quốc gia của ngân hàng payment.account.name.email=Họ tên / email của chủ tài khoản payment.account.name.emailAndHolderId=Họ tên / email / {0} của chủ tài khoản payment.bank.name=Tên ngân hàng payment.select.account=Chọn loại tài khoản payment.select.region=Chọn vùng payment.select.country=Chọn quốc gia payment.select.bank.country=Chọn quốc gia của ngân hàng payment.foreign.currency=Bạn có muốn còn tiền tệ khác tiền tệ mặc định của quốc gia không? payment.restore.default=Không, khôi phục tiền tệ mặc định payment.email=Email payment.country=Quốc gia payment.extras=Yêu cầu thêm payment.email.mobile=Email hoặc số điện thoại payment.crypto.address=Địa chỉ Crypto payment.crypto.tradeInstantCheckbox=Giao dịch ngay với Crypto này (trong 1 giờ) payment.crypto.tradeInstant.popup=Để giao dịch ngay, cả hai đối tác giao dịch phải cùng trực tuyến để hoàn thành giao dịch trong vòng ít hơn 1 giờ. \n\nNếu bạn có chào giá đang mở mà bạn không trực tuyến, vui lòng tắt chúng ở phần 'Danh mục'. payment.crypto=Crypto payment.select.crypto=Select or search Crypto payment.secret=Câu hỏi bí mật payment.answer=Trả lời payment.wallet=ID ví payment.amazon.site=Buy giftcard at payment.ask=Ask in Trader Chat payment.uphold.accountId=Tên người dùng hoặc email hoặc số điện thoại payment.moneyBeam.accountId=Email hoặc số điện thoại payment.popmoney.accountId=Email hoặc số điện thoại payment.promptPay.promptPayId=ID công dân/ ID thuế hoặc số điện thoại payment.supportedCurrencies=Tiền tệ hỗ trợ payment.supportedCurrenciesForReceiver=Currencies for receiving funds payment.limitations=Hạn chế payment.salt=Salt để xác minh tuổi tài khoản payment.error.noHexSalt=The salt needs to be in HEX format.\nIt is only recommended to edit the salt field if you want to transfer the salt from an old account to keep your account age. The account age is verified by using the account salt and the identifying account data (e.g. IBAN). payment.accept.euro=Chấp nhận giao dịch từ các nước Châu Âu này payment.accept.nonEuro=Chấp nhận giao dịch từ các nước không thuộc Châu Âu này payment.accepted.countries=Các nước được chấp nhận payment.accepted.banks=Các ngân hàng được chấp nhận (ID) payment.mobile=Số điện thoại payment.postal.address=Địa chỉ bưu điện payment.national.account.id.AR=Số CBU shared.accountSigningState=Account signing status #new payment.crypto.address.dyn=Địa chỉ {0}  payment.crypto.receiver.address=Địa chỉ crypto của người nhận payment.accountNr=Số tài khoản payment.emailOrMobile=Email hoặc số điện thoại payment.useCustomAccountName=Sử dụng tên tài khoản thông dụng payment.maxPeriod=Thời gian giao dịch cho phép tối đa payment.maxPeriodAndLimit=Max. trade duration: {0} / Max. buy: {1} / Max. sell: {2} / Account age: {3} payment.maxPeriodAndLimitCrypto=Thời gian giao dịch tối đa: {0} / Giới hạn giao dịch tối đa: {1} payment.currencyWithSymbol=Tiền tệ: {0} payment.nameOfAcceptedBank=Tên NH được chấp nhận payment.addAcceptedBank=Thêm NH được chấp nhận payment.clearAcceptedBanks=Xóa NH được chấp nhận payment.bank.nameOptional=Tên ngân hàng (không bắt buộc) payment.bankCode=Mã ngân hàng payment.bankId=ID (BIC/SWIFT) ngân hàng payment.bankIdOptional=ID ngân hàng (BIC/SWIFT) (không bắt buộc) payment.branchNr=Chi nhánh số payment.branchNrOptional=Chi nhánh số (không bắt buộc) payment.accountNrLabel=Tài khoản số (IBAN) payment.accountType=Loại tài khoản payment.checking=Đang kiểm tra payment.savings=Tiết kiệm payment.personalId=ID cá nhân payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle's somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver's full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account's age and signing status are preserved. payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=Khi sử dụng HalCash người mua XMR cần phải gửi cho người bán XMR mã HalCash bằng tin nhắn điện thoại.\n\nVui lòng đảm bảo là lượng tiền này không vượt quá số lượng tối đa mà ngân hàng của bạn cho phép gửi khi dùng HalCash. Số lượng rút tối thiểu là 10 EUR và tối đa là 600 EUR. Nếu rút nhiều lần thì giới hạn sẽ là 3000 EUR/ người nhận/ ngày và 6000 EUR/người nhận/tháng. Vui lòng kiểm tra chéo những giới hạn này với ngân hàng của bạn để chắc chắn là họ cũng dùng những giới hạn như ghi ở đây.\n\nSố tiền rút phải là bội số của 10 EUR vì bạn không thể rút các mệnh giá khác từ ATM. Giao diện người dùng ở phần 'tạo chào giá' và 'chấp nhận chào giá' sẽ điều chỉnh lượng btc sao cho lượng EUR tương ứng sẽ chính xác. Bạn không thể dùng giá thị trường vì lượng EUR có thể sẽ thay đổi khi giá thay đổi.\n\nTrường hợp tranh chấp, người mua XMR cần phải cung cấp bằng chứng chứng minh mình đã gửi EUR. # suppress inspection "UnusedMessageFormatParameter" payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/overview/account_limits]. # suppress inspection "UnusedProperty" payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/overview/account_limits]. payment.cashDeposit.info=Vui lòng xác nhận rằng ngân hàng của bạn cho phép nạp tiền mặt vào tài khoản của người khác. Chẳng hạn, Ngân Hàng Mỹ và Wells Fargo không còn cho phép nạp tiền như vậy nữa. payment.revolut.info=Revolut requires the 'Username' as account ID not the phone number or email as it was the case in the past. payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a 'Username'.\nPlease enter your Revolut 'Username' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account payment.cashapp.info=Vui lòng lưu ý rằng Cash App có rủi ro bồi hoàn cao hơn so với hầu hết các chuyển khoản ngân hàng. payment.venmo.info=Vui lòng lưu ý rằng Venmo có rủi ro bồi hoàn cao hơn so với hầu hết các chuyển khoản ngân hàng. payment.paypal.info=Vui lòng lưu ý rằng PayPal có rủi ro bồi hoàn cao hơn so với hầu hết các chuyển khoản ngân hàng. payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account payment.payByMail.contact=thông tin liên hệ payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.f2f.contact=thông tin liên hệ payment.f2f.contact.prompt=How would you like to be contacted by the trading peer? (email address, phone number,...) payment.f2f.city=Thành phố để gặp mặt trực tiếp payment.f2f.city.prompt=Thành phố sẽ được hiển thị cùng báo giá payment.shared.optionalExtra=Thông tin thêm tuỳ chọn. payment.shared.extraInfo=thông tin thêm payment.shared.extraInfo.offer=Thông tin bổ sung về ưu đãi payment.shared.extraInfo.prompt.paymentAccount=Xác định bất kỳ điều khoản, điều kiện hoặc chi tiết đặc biệt nào bạn muốn hiển thị cùng với các ưu đãi của mình cho tài khoản thanh toán này (người dùng sẽ thấy thông tin này trước khi chấp nhận các ưu đãi). payment.shared.extraInfo.prompt.offer=Xác định bất kỳ thuật ngữ, điều kiện hoặc chi tiết đặc biệt nào bạn muốn hiển thị cùng với đề nghị của mình. payment.shared.extraInfo.noDeposit=Chi tiết liên hệ và điều khoản ưu đãi payment.f2f.info.openURL=Mở trang web payment.f2f.offerbook.tooltip.countryAndCity=Country and city: {0} / {1} payment.shared.extraInfo.tooltip=Thông tin thêm: {0} payment.japan.bank=Ngân hàng payment.japan.branch=Branch payment.japan.account=Tài khoản payment.japan.recipient=Tên payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=Vì sự bảo vệ của bạn, chúng tôi khuyến cáo không nên sử dụng mã PIN Paysafecard để thanh toán.\n\n\ Các giao dịch được thực hiện bằng mã PIN không thể được xác minh độc lập để giải quyết tranh chấp. Nếu có vấn đề xảy ra, có thể không thể khôi phục số tiền đã mất.\n\n\ Để đảm bảo an toàn giao dịch và có thể giải quyết tranh chấp, hãy luôn sử dụng các phương thức thanh toán có hồ sơ xác minh được. # We use constants from the code so we do not use our normal naming convention # dynamic values are not recognized by IntelliJ # Only translate general terms NATIONAL_BANK=Chuyển khoản ngân hàng trong nước SAME_BANK=Chuyển khoản cùng ngân hàng SPECIFIC_BANKS=Chuyển khoản với ngân hàng cụ thể US_POSTAL_MONEY_ORDER=Thư chuyển tiền US CASH_DEPOSIT=Tiền gửi tiền mặt PAY_BY_MAIL=Pay By Mail MONEY_GRAM=MoneyGram WESTERN_UNION=Western Union F2F=Giao dịch trực tiếp (gặp mặt) JAPAN_BANK=Japan Bank Furikomi AUSTRALIA_PAYID=Australian PayID # suppress inspection "UnusedProperty" NATIONAL_BANK_SHORT=Ngân hàng trong nước # suppress inspection "UnusedProperty" SAME_BANK_SHORT=Cùng ngân hàng # suppress inspection "UnusedProperty" SPECIFIC_BANKS_SHORT=Ngân hàng cụ thể # suppress inspection "UnusedProperty" US_POSTAL_MONEY_ORDER_SHORT=Thư chuyển tiền US # suppress inspection "UnusedProperty" CASH_DEPOSIT_SHORT=Tiền gửi tiền mặt # suppress inspection "UnusedProperty" PAY_BY_MAIL_SHORT=PayByMail # suppress inspection "UnusedProperty" MONEY_GRAM_SHORT=MoneyGram # suppress inspection "UnusedProperty" WESTERN_UNION_SHORT=Western Union # suppress inspection "UnusedProperty" F2F_SHORT=F2F # suppress inspection "UnusedProperty" JAPAN_BANK_SHORT=Japan Furikomi # suppress inspection "UnusedProperty" AUSTRALIA_PAYID_SHORT=PayID # Do not translate brand names # suppress inspection "UnusedProperty" UPHOLD=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY=Popmoney # suppress inspection "UnusedProperty" REVOLUT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY=WeChat Pay # suppress inspection "UnusedProperty" SEPA=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT=SEPA Instant Payments # suppress inspection "UnusedProperty" FASTER_PAYMENTS=Faster Payments # suppress inspection "UnusedProperty" SWISH=Swish # suppress inspection "UnusedProperty" ZELLE=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS=Cryptos # suppress inspection "UnusedProperty" PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT=Crypto ngay tức thì # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY=OKPay # suppress inspection "UnusedProperty" CASH_APP=Cash App # suppress inspection "UnusedProperty" VENMO=Venmo # suppress inspection "UnusedProperty" UPHOLD_SHORT=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM_SHORT=MoneyBeam (N26) # suppress inspection "UnusedProperty" POPMONEY_SHORT=Popmoney # suppress inspection "UnusedProperty" REVOLUT_SHORT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY_SHORT=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY_SHORT=AliPay # suppress inspection "UnusedProperty" WECHAT_PAY_SHORT=WeChat Pay # suppress inspection "UnusedProperty" SEPA_SHORT=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT_SHORT=SEPA Instant # suppress inspection "UnusedProperty" FASTER_PAYMENTS_SHORT=Faster Payments # suppress inspection "UnusedProperty" SWISH_SHORT=Swish # suppress inspection "UnusedProperty" ZELLE_SHORT=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY_SHORT=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER_SHORT=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH_SHORT=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS_SHORT=Cryptos # suppress inspection "UnusedProperty" PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT_SHORT=Crypto ngay tức thì # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY_SHORT=OKPay # suppress inspection "UnusedProperty" CASH_APP_SHORT=Cash App # suppress inspection "UnusedProperty" VENMO_SHORT=Venmo #################################################################### # Validation #################################################################### validation.empty=Không cho phép nhập trống. validation.NaN=Giá trị nhập là số không có hiệu lực. validation.notAnInteger=Giá trị nhập không phải là một số nguyên. validation.zero=Không cho phép nhập giá trị 0. validation.negative=Không cho phép nhập giá trị âm. validation.traditional.tooSmall=Không cho phép giá trị nhập nhỏ hơn giá trị có thể nhỏ nhất. validation.traditional.tooLarge=Không cho phép giá trị nhập lớn hơn giá trị có thể lớn nhất. validation.xmr.fraction=Input will result in a monero value of less than 1 satoshi validation.xmr.tooLarge=Không cho phép giá trị nhập lớn hơn {0}. validation.xmr.tooSmall=Không cho phép giá trị nhập nhỏ hơn {0}. validation.passwordTooShort=The password you entered is too short. It needs to have a min. of 8 characters. validation.passwordTooLong=Mật khẩu bạn vừa nhập quá dài. Không được quá 50 ký tự. validation.sortCodeNumber={0} phải có {1} số. validation.sortCodeChars={0} phải có {1} ký tự. validation.bankIdNumber={0} phải có {1} số. validation.accountNr=Số tài khoản phải có {0} số. validation.accountNrChars=Số tài khoản phải có {0} ký tự. validation.xmr.invalidAddress=Địa chỉ không đúng. Vui lỏng kiểm tra lại định dạng địa chỉ. validation.integerOnly=Vui lòng chỉ nhập số nguyên. validation.inputError=Giá trị nhập của bạn gây lỗi:\n{0} validation.xmr.exceedsMaxTradeLimit=Giới hạn giao dịch của bạn là {0}. validation.nationalAccountId={0} phải có {1} số. #new validation.invalidInput=Giá trị nhập không hợp lệ: {0} validation.accountNrFormat=Số tài khoản phải có định dạng : {0} # suppress inspection "UnusedProperty" validation.crypto.wrongStructure=Xác nhận địa chỉ không thành công vì không khớp với cấu trúc của {0} địa chỉ. # suppress inspection "UnusedProperty" validation.crypto.ltz.zAddressesNotSupported=LTZ address must start with L. Addresses starting with z are not supported. # suppress inspection "UnusedProperty" validation.crypto.zAddressesNotSupported=ZEC addresses must start with t. Addresses starting with z are not supported. # suppress inspection "UnusedProperty" validation.crypto.invalidAddress=Địa chỉ không phải là địa chỉ {0} hợp lệ! {1} # suppress inspection "UnusedProperty" validation.crypto.liquidBitcoin.invalidAddress=Native segwit addresses (those starting with 'lq') are not supported. validation.bic.invalidLength=Input length must be 8 or 11 validation.bic.letters=Mã NH và quốc gia phải là chữ validation.bic.invalidLocationCode=BIC chứa mã vị trí không hợp lệ validation.bic.invalidBranchCode=BIC chứa mã chi nhánh không hợp lệ validation.bic.sepaRevolutBic=Tài khoản Revolut Sepa không được hỗ trợ. validation.btc.invalidFormat=Invalid format for a Bitcoin address. validation.email.invalidAddress=Địa chỉ không hợp lệ validation.iban.invalidCountryCode=Mã quốc gia không hợp lệ validation.iban.checkSumNotNumeric=Mã kiểm tra phải là số validation.iban.nonNumericChars=Phát hiện ký tự không phải kiểu chữ-số validation.iban.checkSumInvalid=Mã kiểm tra IBAN không hợp lệ validation.iban.invalidLength=Number must have a length of 15 to 34 chars. validation.interacETransfer.invalidAreaCode=Mã vùng không phải Canada validation.interacETransfer.invalidPhone=Please enter a valid 11 digit phone number (ex: 1-123-456-7890) or an email address validation.interacETransfer.invalidQuestion=Chỉ được chứa chữ cái, số, khoảng trắng, và/hoặc các ký tự ' _ , . ? - validation.interacETransfer.invalidAnswer=Phải được viết liền và chỉ bao gồm chữ cái, số, và/hoặc ký tự - validation.inputTooLarge=Giá trị nhập không được lớn hơn {0} validation.inputTooSmall=Giá trị nhập phải lớn hơn {0} validation.inputToBeAtLeast=Giá trị nhập tối thiểu phải bằng {0} validation.amountBelowDust=An amount below the dust limit of {0} satoshi is not allowed. validation.length=Chiều dài phải nằm trong khoảng từ {0} đến {1} validation.fixedLength=Length must be {0} validation.pattern=Giá trị nhập phải có định dạng: {0} validation.noHexString=Giá trị nhập không ở định dạng HEX validation.advancedCash.invalidFormat=Phải là một địa chỉ email hợp lệ hoặc là ID ví với định dạng: X000000000000 validation.invalidUrl=Đây không phải là URL hợp lệ validation.mustBeDifferent=Your input must be different from the current value validation.cannotBeChanged=Thông số không thể thay đổi validation.numberFormatException=Ngoại lệ cho định dạng số {0} validation.mustNotBeNegative=Giá trị nhập không được là số âm validation.phone.missingCountryCode=Need two letter country code to validate phone number validation.phone.invalidCharacters=Phone number {0} contains invalid characters validation.phone.insufficientDigits=There are not enough digits in {0} to be a valid phone number validation.phone.tooManyDigits=There are too many digits in {0} to be a valid phone number validation.phone.invalidDialingCode=Country dialing code for number {0} is invalid for country {1}. The correct dialing code is {2}. validation.invalidAddressList=Must be comma separated list of valid addresses ================================================ FILE: core/src/main/resources/i18n/displayStrings_zh-hans.properties ================================================ # Keep display strings organized by domain # Naming convention: We use camelCase and dot separated name spaces. # Use as many sub spaces as required to make the structure clear, but as little as possible. # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break # in the display but only in the editor. # Please use in all language files the exact same order of the entries, that way a comparison is easier. # Please try to keep the length of the translated string similar to English. If it is longer it might break layout or # get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. #################################################################### # Shared #################################################################### shared.readMore=阅读更多 shared.openHelp=打开帮助 shared.warning=警告 shared.close=关闭 shared.cancel=取消 shared.ok=好的 shared.yes=是 shared.no=否 shared.iUnderstand=我了解 shared.na=N/A shared.shutDown=完全关闭 shared.reportBug=在 Github 报告错误 shared.buyMonero=买入比特币 shared.sellMonero=卖出比特币 shared.buyCurrency=买入 {0} shared.sellCurrency=卖出 {0} shared.buyCurrency.locked=买入 {0} 🔒 shared.sellCurrency.locked=卖出 {0} 🔒 shared.buyingXMRWith=用 {0} 买入 XMR shared.sellingXMRFor=卖出 XMR 为 {0} shared.buyingCurrency=买入 {0}(卖出 XMR) shared.sellingCurrency=卖出 {0}(买入 XMR) shared.buy=买 shared.sell=卖 shared.buying=买入 shared.selling=卖出 shared.P2P=P2P shared.oneOffer=报价 shared.multipleOffers=报价 shared.Offer=报价 shared.offerVolumeCode={0} 报价量 shared.openOffers=可用报价 shared.trade=交易 shared.trades=交易 shared.openTrades=进行中的交易 shared.dateTime=日期/时间 shared.price=价格 shared.priceWithCur={0} 价格 shared.priceInCurForCur=1 {1} 的 {0} 价格 shared.fixedPriceInCurForCur=1 {1} 的 {0} 的固定价格 shared.amount=数量 shared.txFee=交易手续费 shared.tradeFee=交易手续费 shared.buyerSecurityDeposit=买家保证金 shared.sellerSecurityDeposit=卖家保证金 shared.amountWithCur={0} 数量 shared.volumeWithCur={0} 总量 shared.currency=货币类型 shared.market=交易项目 shared.deviation=偏差 shared.paymentMethod=付款方式 shared.tradeCurrency=交易货币 shared.offerType=报价类型 shared.details=详情 shared.address=地址 shared.balanceWithCur={0} 余额 shared.utxo=Unspent transaction output shared.txId=交易记录 ID shared.confirmations=审核 shared.revert=还原 Tx shared.select=选择 shared.usage=使用状况 shared.state=状态 shared.tradeId=交易 ID shared.offerId=报价 ID shared.bankName=银行名称 shared.acceptedBanks=接受的银行 shared.amountMinMax=总额(最小 - 最大) shared.amountHelp=如果报价包含最小和最大限制,那么您可以在这个范围内的任意数量进行交易。 shared.remove=移除 shared.goTo=前往 {0} shared.XMRMinMax=XMR(最小 - 最大) shared.removeOffer=移除报价 shared.dontRemoveOffer=不要移除报价 shared.editOffer=编辑报价 shared.openLargeQRWindow=打开放大二维码窗口 shared.tradingAccount=交易账户 shared.faq=访问 FAQ 页面 shared.yesCancel=是的,取消 shared.nextStep=下一步 shared.selectTradingAccount=选择交易账户 shared.fundFromSavingsWalletButton=从 Haveno 钱包申请资金 shared.fundFromExternalWalletButton=从您的外部钱包充值 shared.openDefaultWalletFailed=打开默认的比特币钱包应用程序失败了。您确定您安装了吗? shared.belowInPercent=低于市场价格 % shared.aboveInPercent=高于市场价格 % shared.enterPercentageValue=输入 % 值 shared.OR=或者 shared.notEnoughFunds=您的 Haveno 钱包中没有足够的资金去支付这一交易 需要{0} 您可用余额为 {1}。\n\n请从外部比特币钱包注入资金或在“资金/存款”充值到您的 Haveno 钱包。 shared.waitingForFunds=等待资金充值... shared.TheXMRBuyer=XMR 买家 shared.You=您 shared.sendingConfirmation=发送确认... shared.sendingConfirmationAgain=请再次发送确认 shared.exportCSV=导出保存为 .csv shared.exportJSON=导出保存至 JSON shared.summary=Show summary shared.noDateAvailable=没有可用数据 shared.noDetailsAvailable=没有可用详细 shared.notUsedYet=尚未使用 shared.date=日期 shared.sendFundsDetailsWithFee=TOD发送:{0}\n\n接收地址:{1}\n\n额外矿工费:{2}\n\n您确定要发送此金额吗?O # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno 检测到,该交易将产生一个低于最低零头阈值的输出(不被比特币共识规则所允许)。相反,这些零头({0}satoshi{1})将被添加到挖矿手续费中。 shared.copyToClipboard=复制到剪贴板 shared.language=语言 shared.country=国家或地区 shared.applyAndShutDown=同意并关闭 shared.selectPaymentMethod=选择付款方式 shared.accountNameAlreadyUsed=这个账户名称已经被已保存的账户占用。\n请使用另外一个名称。 shared.askConfirmDeleteAccount=您确定想要删除被选定的账号吗? shared.cannotDeleteAccount=您不能删除这个账户,因为它正在被使用于报价或交易中。 shared.noAccountsSetupYet=还没有建立帐户。 shared.manageAccounts=管理账户 shared.addNewAccount=添加新的账户 shared.ExportAccounts=导出账户 shared.importAccounts=导入账户 shared.createNewAccount=创建新的账户 shared.createNewAccountDescription=您的账户详情存储在您的设备上,仅与您的交易对手和仲裁员在出现争议时共享。 shared.saveNewAccount=保存新的账户 shared.selectedAccount=选中的账户 shared.deleteAccount=删除账户 shared.errorMessageInline=\n错误信息:{0} shared.errorMessage=错误信息 shared.information=资料 shared.name=名称 shared.id=ID shared.dashboard=仪表盘 shared.accept=接受 shared.balance=余额 shared.save=保存 shared.onionAddress=匿名地址 shared.supportTicket=帮助话题 shared.dispute=纠纷 shared.mediationCase=调解事件 shared.seller=卖家 shared.buyer=买家 shared.allEuroCountries=所有欧元国家 shared.acceptedTakerCountries=接受的买家国家 shared.tradePrice=交易价格 shared.tradeAmount=交易金额 shared.tradeVolume=交易总量 shared.invalidKey=您输入的密码不正确。 shared.enterPrivKey=输入私钥解锁 shared.payoutTxId=支出交易 ID shared.contractAsJson=JSON 格式的合同 shared.viewContractAsJson=查看 JSON 格式的合同 shared.contract.title=交易 ID:{0} 的合同 shared.paymentDetails=XMR {0} 支付详情 shared.securityDeposit=保证金 shared.yourSecurityDeposit=你的保证金 shared.contract=合同 shared.messageArrived=消息送达。 shared.messageStoredInMailbox=消息保存在邮箱中。 shared.messageSendingFailed=消息发送失败。错误:{0} shared.unlock=解锁 shared.toReceive=接收 shared.toSpend=花费 shared.xmrAmount=XMR 总额 shared.yourLanguage=你的语言 shared.addLanguage=添加语言 shared.total=合计 shared.totalsNeeded=需要资金 shared.tradeWalletAddress=交易钱包地址 shared.tradeWalletBalance=交易钱包余额 shared.reserveExactAmount=仅保留必要的资金。需要支付矿工费用,并且大约需要 20 分钟后您的报价才会生效。 shared.makerTxFee=卖家:{0} shared.takerTxFee=买家:{0} shared.iConfirm=我确认 shared.openURL=打开 {0} shared.fiat=法定货币 shared.crypto=加密 shared.preciousMetals=贵金属 shared.all=全部 shared.edit=编辑 shared.advancedOptions=高级选项 shared.interval=取消 shared.actions=操作 shared.buyerUpperCase=买家 shared.sellerUpperCase=买家 shared.new=新 shared.learnMore=了解更多 shared.dismiss=忽略 shared.selectedArbitrator=选中的仲裁者 shared.selectedMediator=选择调解员 shared.selectedRefundAgent=选中的仲裁者 shared.mediator=调解员 shared.arbitrator=仲裁员 shared.refundAgent=仲裁员 shared.refundAgentForSupportStaff=退款助理 shared.delayedPayoutTxId=延迟支付交易 ID shared.delayedPayoutTxReceiverAddress=延迟交易交易已发送至 shared.unconfirmedTransactionsLimitReached=你现在有过多的未确认交易。请稍后尝试 shared.numItemsLabel=实体数:{0} shared.filter=过滤 shared.enabled=启用 #################################################################### # UI views #################################################################### #################################################################### # MainView #################################################################### mainView.menu.market=交易项目 mainView.menu.buyXmr=买入 XMR mainView.menu.sellXmr=卖出 XMR mainView.menu.portfolio=业务 mainView.menu.funds=资金 mainView.menu.support=帮助 mainView.menu.settings=设置 mainView.menu.account=账户 mainView.marketPriceWithProvider.label=交易所价格提供商:{0} mainView.marketPrice.havenoInternalPrice=最新 Haveno 交易的价格 mainView.marketPrice.tooltip.havenoInternalPrice=外部交易所供应商没有可用的市场价格。\n显示的价格是该货币的最新 Haveno 交易价格。 mainView.marketPrice.tooltip=交易所价格提供者 {0}{1}\n最后更新:{2}\n提供者节点 URL:{3} mainView.balance.available=可用余额 mainView.balance.reserved=保证金 mainView.balance.pending=冻结余额 mainView.balance.reserved.short=保证 mainView.balance.pending.short=冻结 mainView.footer.usingTor=(通过 Tor) mainView.footer.localhostMoneroNode=(本地主机) mainView.footer.clearnet=(通过 clearnet) mainView.footer.xmrInfo={0} {1} mainView.footer.xmrFeeRate=/ 矿工手费率:{0} 聪/字节 mainView.footer.xmrInfo.initializing=连接到 Haveno 网络 mainView.footer.xmrInfo.synchronizingWith=同步中,区块:{1} / {2},与 {0} 同步中 mainView.footer.xmrInfo.connectedTo=连接到 {0} 在区块 {1} 处 mainView.footer.xmrInfo.synchronizingWalletWith=同步中,区块:{1} / {2},与 {0} 的钱包同步中 mainView.footer.xmrInfo.syncedWith=已同步至 {0} 在区块 {1} mainView.footer.xmrInfo.connectingTo=连接至 mainView.footer.xmrInfo.connectionFailed=连接失败: mainView.footer.xmrPeers=Monero网络节点:{0} mainView.footer.p2pPeers=Haveno 网络节点:{0} mainView.footer.version=版本 {0} mainView.bootstrapState.connectionToTorNetwork=(1/4) 连接至 Tor 网络... mainView.bootstrapState.torNodeCreated=(2/4) Tor 节点已创建 mainView.bootstrapState.hiddenServicePublished=(3/4) 隐藏的服务已发布 mainView.bootstrapState.initialDataReceived=(4/4) 初始数据已接收 mainView.bootstrapWarning.noSeedNodesAvailable=没有可用的种子节点 mainView.bootstrapWarning.noNodesAvailable=没有可用的种子节点和节点 mainView.bootstrapWarning.bootstrappingToP2PFailed=启动 Haveno 网络失败 mainView.p2pNetworkWarnMsg.noNodesAvailable=没有可用种子节点或永久节点可请求数据。\n请检查您的互联网连接或尝试重启应用程序。 mainView.p2pNetworkWarnMsg.connectionToP2PFailed=连接至 Haveno 网络失败(错误报告:{0})。\n请检查您的互联网连接或尝试重启应用程序。 mainView.walletServiceErrorMsg.timeout=比特币网络连接超时。 mainView.walletServiceErrorMsg.connectionError=错误:{0} 比特币网络连接失败。 mainView.walletServiceErrorMsg.rejectedTxException=交易被网络拒绝。\n\n{0} mainView.networkWarning.allConnectionsLost=您失去了所有与 {0} 网络节点的连接。\n您失去了互联网连接或您的计算机处于待机状态。 mainView.networkWarning.localhostMoneroLost=您丢失了与本地主机比特币节点的连接。\n请重启 Haveno 应用程序连接到其他比特币节点或重新启动主机比特币节点。 mainView.version.update=(有更新可用) #################################################################### # MarketView #################################################################### market.tabs.offerBook=报价列表 market.tabs.spreadCurrency=Offers by Currency market.tabs.spreadPayment=Offers by Payment Method market.tabs.trades=行情图 # OfferBookChartView market.offerBook.buyCrypto=我想要买入 {0}(卖出 {1}) market.offerBook.sellCrypto=我想要卖出 {0}(买入 {1}) market.offerBook.buyWithTraditional=购买 {0} market.offerBook.sellWithTraditional=出售 {0} market.offerBook.sellOffersHeaderLabel=出售 {0} 到 market.offerBook.buyOffersHeaderLabel=购买 {0} 以 market.offerBook.buy=我想要买入比特币 market.offerBook.sell=我想要卖出比特币 # SpreadView market.spread.numberOfOffersColumn=所有报价({0}) market.spread.numberOfBuyOffersColumn=买入 XMR({0}) market.spread.numberOfSellOffersColumn=卖出 XMR({0}) market.spread.totalAmountColumn=总共 XMR({0}) market.spread.spreadColumn=差价 market.spread.expanded=Expanded view # TradesChartsView market.trades.nrOfTrades=交易:{0} market.trades.tooltip.volumeBar=Volume: {0} / {1}\nNo. of trades: {2}\nDate: {3} market.trades.tooltip.candle.open=打开: market.trades.tooltip.candle.close=关闭: market.trades.tooltip.candle.high=高: market.trades.tooltip.candle.low=低: market.trades.tooltip.candle.average=平均: market.trades.tooltip.candle.median=调解员: market.trades.tooltip.candle.date=日期: market.trades.showVolumeInUSD=Show volume in USD #################################################################### # OfferView #################################################################### offerbook.createOffer=创建报价 offerbook.takeOffer=接受报价 offerbook.takeOfferToBuy=接受报价来收购 {0} offerbook.takeOfferToSell=接受报价来出售 {0} offerbook.takeOffer.enterChallenge=输入报价密码 offerbook.trader=商人 offerbook.offerersBankId=卖家的银行 ID(BIC/SWIFT):{0} offerbook.offerersBankName=卖家的银行名称:{0} offerbook.offerersBankSeat=卖家的银行所在国家或地区:{0} offerbook.offerersAcceptedBankSeatsEuro=接受的银行所在国家(买家):所有欧元国家 offerbook.offerersAcceptedBankSeats=接受的银行所在国家(买家):\n {0} offerbook.availableOffers=可用报价 offerbook.filterByCurrency=以货币筛选 offerbook.filterByPaymentMethod=以支付方式筛选 offerbook.matchingOffers=匹配我的账户的报价 offerbook.filterNoDeposit=无押金 offerbook.noDepositOffers=无押金的报价(需要密码短语) offerbook.timeSinceSigning=账户信息 offerbook.timeSinceSigning.info=此账户已验证,{0} offerbook.timeSinceSigning.info.arbitrator=由仲裁员验证,并可以验证伙伴账户 offerbook.timeSinceSigning.info.peer=由对方验证,等待%d天限制被解除 offerbook.timeSinceSigning.info.peerLimitLifted=由对方验证,限制被取消 offerbook.timeSinceSigning.info.signer=由对方验证,并可验证对方账户(限制已取消) offerbook.timeSinceSigning.info.banned=账户已被封禁 offerbook.timeSinceSigning.daysSinceSigning={0} 天 offerbook.timeSinceSigning.daysSinceSigning.long=自验证{0} offerbook.xmrAutoConf=是否开启自动确认 offerbook.buyXmrWith=使用以下方式购买 XMR: offerbook.sellXmrFor=出售 XMR 以换取: offerbook.timeSinceSigning.help=当您成功地完成与拥有已验证付款帐户的伙伴交易时,您的付款帐户已验证。\n{0} 天后,最初的 {1} 的限制解除以及你的账户可以验证其他人的付款账户。 offerbook.timeSinceSigning.notSigned=尚未验证 offerbook.timeSinceSigning.notSigned.ageDays={0} 天 offerbook.timeSinceSigning.notSigned.noNeed=N/A shared.notSigned=此账户还没有被验证以及在{0}前创建 shared.notSigned.noNeed=此账户类型不适用验证 shared.notSigned.noNeedDays=此账户类型不适用验证且在{0}天创建 shared.notSigned.noNeedAlts=数字货币不适用账龄与签名 offerbook.nrOffers=报价数量:{0} offerbook.volume={0}(最小 - 最大) offerbook.deposit=XMR 保证金(%) offerbook.deposit.help=交易双方均已支付保证金确保这个交易正常进行。这会在交易完成时退还。 offerbook.createNewOffer=創建報價給 {0} {1} offerbook.createOfferToBuy=创建新的报价来买入 {0} offerbook.createOfferToSell=创建新的报价来卖出 {0} offerbook.createOfferToBuy.withTraditional=创建新的报价用 {1} 购买 {0} offerbook.createOfferToSell.forTraditional=创建新的报价以 {1} 出售 {0} offerbook.createOfferToBuy.withCrypto=创建新的卖出报价 {0} (买入 {1}) offerbook.createOfferToSell.forCrypto=创建新的买入报价 {0}(卖出 {1}) offerbook.takeOfferButton.tooltip=下单买入 {0} offerbook.yesCreateOffer=是的,创建报价 offerbook.setupNewAccount=设置新的交易账户 offerbook.removeOffer.success=撤销报价成功。 offerbook.removeOffer.failed=撤销报价失败:\n{0} offerbook.deactivateOffer.failed=报价停用失败:\n{0} offerbook.activateOffer.failed=报价发布失败:\n{0} offerbook.withdrawFundsHint=您可以从 {0} 中撤回您支付的资金。 offerbook.warning.noTradingAccountForCurrency.headline=选择的货币没有支付账户 offerbook.warning.noTradingAccountForCurrency.msg=您选择的货币还没有建立支付账户。\n\n你想要用其他货币创建一个报价吗? offerbook.warning.noMatchingAccount.headline=没有匹配的支付账户。 offerbook.warning.noMatchingAccount.msg=这个报价使用了您未创建过的支付方式。\n\n你现在想要创建一个新的支付账户吗? offerbook.warning.counterpartyTradeRestrictions=由于交易伙伴的交易限制,这个报价不能接受 offerbook.warning.newVersionAnnouncement=使用这个版本的软件,交易伙伴可以验证和验证彼此的支付帐户,以创建一个可信的支付帐户网络。\n\n交易成功后,您的支付帐户将被验证以及交易限制将在一定时间后解除(此时间基于验证方法)。\n\n有关验证帐户的更多信息,请参见文档 https://docs.haveno.exchange/payment-methods#account-signing popup.warning.tradeLimitDueAccountAgeRestriction.seller=基于以下标准的安全限制,允许的交易金额限制为 {0}:\n- 买方的帐目没有由仲裁员或伙伴验证\n- 买方帐户自验证之日起不足30天\n- 本报价的付款方式被认为存在银行退款的风险\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=基于以下标准的安全限制,允许的交易金额限制为{0}:\n- 你的买家帐户没有由仲裁员或伙伴验证\n- 自验证你的帐户以来的时间少于30天\n- 本报价的付款方式被认为存在银行退款的风险\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=这种付款方法暂时限于 {0} 到 {1},因为所有的买家都是新帐户。\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=您的报价将仅限于具有签署和经过时代化的帐户的买家,因为它超出了{0}。\n\n{1} offerbook.warning.wrongTradeProtocol=该报价要求的软件版本与您现在运行的版本不一致。\n\n请检查您是否运行最新版本,或者是该报价用户在使用一个旧的版本。\n用户不能与不兼容的交易协议版本进行交易。 offerbook.warning.userIgnored=您已添加该用户的匿名地址在您的忽略列表里。 offerbook.warning.offerBlocked=该报价被 Haveno 开发人员限制。\n接受该报价时,可能有一个未处理的漏洞导致了问题。 offerbook.warning.currencyBanned=该报价中使用的货币被 Haveno 开发人员阻止。\n请访问 Haveno 论坛了解更多信息。 offerbook.warning.paymentMethodBanned=该报价中使用的付款方式被 Haveno 开发人员阻止。\n请访问 Haveno 论坛了解更多信息。 offerbook.warning.nodeBlocked=该交易者的匿名地址被 Haveno 开发人员限制。\n当获取来自该交易者的报价,可能有一个未处理的漏洞导致了问题。 offerbook.warning.requireUpdateToNewVersion=您的 Haveno 版本不再兼容交易。\n请通过 https://haveno.exchange/downloads 更新到最新的 Haveno 版本。 offerbook.warning.offerWasAlreadyUsedInTrade=您不能吃单因为您已经完成了该操作。可能是你之前的吃单尝试导致了交易失败。 offerbook.info.sellAtMarketPrice=您会以市场价格进行出售(每分钟更新) offerbook.info.buyAtMarketPrice=您将以市场价格进行购买(每分钟更新)。 offerbook.info.sellBelowMarketPrice=您将以低于市场价 {0} 的价格进行出售(每分钟更新) offerbook.info.buyAboveMarketPrice=您将以高于市场价 {0} 的价格进行支付(每分钟更新) offerbook.info.sellAboveMarketPrice=您将以高于市场价 {0} 的价格进行出售(每分钟更新) offerbook.info.buyBelowMarketPrice=您将以低于市场价 {0} 的价格进行支付(每分钟更新) offerbook.info.buyAtFixedPrice=您会以这个固定价格购买。 offerbook.info.sellAtFixedPrice=您会以这个固定价格出售。 offerbook.info.noArbitrationInUserLanguage=如有任何争议,请注意此报价的仲裁将在 {0} 内处理。语言目前设置为{1}。 offerbook.info.roundedFiatVolume=金额四舍五入是为了增加您的交易隐私。 #################################################################### # Offerbook / Create offer #################################################################### createOffer.amount.prompt=输入 XMR 数量 createOffer.price.prompt=输入价格 createOffer.volume.prompt=输入 {0} 金额 createOffer.amountPriceBox.amountDescription=比特币数量 {0} createOffer.amountPriceBox.buy.volumeDescription=花费 {0} 数量 createOffer.amountPriceBox.sell.volumeDescription=接收 {0} 数量 createOffer.amountPriceBox.minAmountDescription=最小 XMR 数量 createOffer.securityDeposit.prompt=保证金 createOffer.fundsBox.title=为您的报价充值 createOffer.fundsBox.offerFee=挂单费 createOffer.fundsBox.networkFee=矿工手续费 createOffer.fundsBox.placeOfferSpinnerInfo=正在发布报价中... createOffer.fundsBox.paymentLabel=Haveno 交易 ID {0} createOffer.fundsBox.fundsStructure=({0} 保证金,{1} 交易费,{2} 采矿费) createOffer.success.headline=您的提议已被建立 createOffer.success.info=你可以在“业务/未完成报价”页面内管理您的未完成报价。 createOffer.info.sellAtMarketPrice=由于您的价格是持续更新的,因此您将始终以市场价格进行出售。 createOffer.info.buyAtMarketPrice=由于您的价格是持续更新的,因此您将始终以市场价格进行购买。 createOffer.info.sellAboveMarketPrice=由于您的价格是持续更新的,因此您将始终按照高于市场价 {0}% 的价格出售。 createOffer.info.buyBelowMarketPrice=由于您的价格是持续更新的,因此您将始终支付低于市场价 {0}% 的价格。 createOffer.warning.sellBelowMarketPrice=由于您的价格是持续更新的,因此您将始终按照低于市场价 {0}% 的价格出售。 createOffer.warning.buyAboveMarketPrice=由于您的价格是持续更新的,因此您将始终支付高于市场价 {0}% 的价格。 createOffer.tradeFee.descriptionXMROnly=挂单费 createOffer.tradeFee.descriptionBSQEnabled=选择手续费币种 createOffer.triggerPrice.prompt=Set optional trigger price createOffer.triggerPrice.label=Deactivate offer if market price is {0} createOffer.triggerPrice.tooltip=As protection against drastic price movements you can set a trigger price which deactivates the offer if the market price reaches that value. createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0} createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0} # new entries createOffer.placeOfferButton.buy=审核:创建以 {0} 买入 XMR 的报价 createOffer.placeOfferButton.sell=审核:创建以 {0} 卖出 XMR 的报价 createOffer.createOfferFundWalletInfo.headline=为您的报价充值 # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- 交易数量:{0}\n createOffer.createOfferFundWalletInfo.msg=您需要为此报价存入 {0}。\n\n\ 这些资金将保留在您的本地钱包中,并在有人接受您的报价后锁定到多签钱包中。\n\n\ 金额是以下各项的总和:\n\ {1}\ - 您的保证金:{2}\n\ - 交易费用:{3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=提交报价发生错误:\n\n{0}\n\n没有资金从您钱包中扣除。\n请检查您的互联网连接或尝试重启应用程序。 createOffer.setAmountPrice=设置数量和价格 createOffer.warnCancelOffer=您已经为该报价充值了。如果您现在取消,您的资金将保留在您的本地 Haveno 钱包中,并可在“资金/提现”界面进行提现。您确定要取消吗? createOffer.timeoutAtPublishing=发布报价时产生了一个错误。 createOffer.errorInfo=\n\n挂单费已经支付,在最坏的情况下,你会失去了这笔费用。 我们很抱歉,但请记住,这是一个很小的数量。\n请尝试重新启动应用程序并检查您的网络连接,看看是否可以解决问题。 createOffer.tooLowSecDeposit.warning=您设置的保证金低于推荐默认值 {0}。\n您确定要使用较低的保证金吗? createOffer.tooLowSecDeposit.makerIsSeller=在交易对手不遵循交易协议时,这给予您较少的保护。 createOffer.tooLowSecDeposit.makerIsBuyer=对于遵守交易协议的交易对象,您的保护金额较低,因为风险存款较小。 其他用户可能更喜欢采取其他报价,而不是您的。 createOffer.resetToDefault=不,恢复默认值 createOffer.useLowerValue=是的,使用我较低的值 createOffer.priceOutSideOfDeviation=您输入的价格超过了市场价差价的最大值。\n最大值为 {0},您可以在偏好中进行调整。 createOffer.changePrice=改变价格 createOffer.tac=发布该报价,我同意与满足该条件的任何交易者进行交易。 createOffer.currencyForFee=挂单费 createOffer.setDeposit=设置买家的保证金(%) createOffer.setDepositAsBuyer=设置自己作为买家的保证金(%) createOffer.setDepositForBothTraders=设置双方的保证金比例(%) createOffer.securityDepositInfo=您的买家的保证金将会是 {0} createOffer.securityDepositInfoAsBuyer=您作为买家的保证金将会是 {0} createOffer.minSecurityDepositUsed=最低安全押金已使用 createOffer.buyerAsTakerWithoutDeposit=无需买家支付押金(使用口令保护) createOffer.myDeposit=我的安全押金 (%) createOffer.myDepositInfo=您的保证金为 {0} #################################################################### # Offerbook / Take offer #################################################################### takeOffer.amount.prompt=输入 XMR 数量 takeOffer.amountPriceBox.buy.amountDescription=卖出比特币数量 takeOffer.amountPriceBox.sell.amountDescription=买入比特币数量 takeOffer.amountPriceBox.priceDescription=每个比特币的 {0} 价格 takeOffer.amountPriceBox.amountRangeDescription=可用数量范围 takeOffer.amountPriceBox.warning.invalidXmrDecimalPlaces=你输入的数量超过允许的小数位数。\n数量已被调整为4位小数。 takeOffer.validation.amountSmallerThanMinAmount=数量不能比报价内设置的最小数量小。 takeOffer.validation.amountLargerThanOfferAmount=数量不能比报价提供的总量大。 takeOffer.validation.amountLargerThanOfferAmountMinusFee=该输入数量可能会给卖家造成比特币碎片。 takeOffer.fundsBox.title=为交易充值 takeOffer.fundsBox.isOfferAvailable=检查报价是否有效... takeOffer.fundsBox.tradeAmount=卖出数量 takeOffer.fundsBox.offerFee=挂单费 takeOffer.fundsBox.networkFee=总共挖矿手续费 takeOffer.fundsBox.takeOfferSpinnerInfo=接受报价:{0} takeOffer.fundsBox.paymentLabel=Haveno 交易 ID {0} takeOffer.fundsBox.fundsStructure=({0} 保证金,{1} 交易费,{2} 采矿费) takeOffer.fundsBox.noFundingRequiredTitle=无需资金 takeOffer.fundsBox.noFundingRequiredDescription=从卖方处获取交易密码(在Haveno之外)以接受此报价。 takeOffer.success.headline=你已成功下单一个报价。 takeOffer.success.info=你可以在“业务/未完成交易”页面内查看您的未完成交易。 takeOffer.error.message=下单时发生了一个错误。\n\n{0} # new entries takeOffer.takeOfferButton.buy=审核:接受以 {0} 买入 XMR 的报价 takeOffer.takeOfferButton.sell=审核:接受以 {0} 卖出 XMR 的报价 takeOffer.noPriceFeedAvailable=您不能对这笔报价下单,因为它使用交易所价格百分比定价,但是您没有获得可用的价格。 takeOffer.takeOfferFundWalletInfo.headline=为交易充值 # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- 交易数量:{0}\n takeOffer.takeOfferFundWalletInfo.msg=您需要存入 {0} 以接受此报价。\n\n该金额为以下总和:\n{1}- 您的保证金:{2}\n- 交易费用:{3} takeOffer.alreadyPaidInFunds=如果你已经支付,你可以在“资金/提现”提现它。 takeOffer.paymentInfo=付款信息 takeOffer.setAmountPrice=设置数量 takeOffer.alreadyFunded.askCancel=您已经为该报价充值了。如果您现在取消,您的资金将保留在您的本地 Haveno 钱包中,并可在“资金/提现”界面进行提现。您确定要取消吗? takeOffer.failed.offerNotAvailable=请求失败,由于报价不再可用。 也许有交易者在此期间已经下单。 takeOffer.failed.offerTaken=您不能对该报价下单,因为该报价已经被其他交易者下单。 takeOffer.failed.offerRemoved=您不能对该报价下单,因为该报价已经在此期间被删除。 takeOffer.failed.offererNotOnline=下单失败,因为卖家已经不在线。 takeOffer.failed.offererOffline=您不能下单,因为卖家已经下线。 takeOffer.warning.connectionToPeerLost=您与卖家失去连接。\n因为太多连接,他或许已经下线或者关掉了与您的连接。\n\n如果您还是能在报价列表中看到他的报价,您可以再次尝试下单。 takeOffer.error.noFundsLost=\n\n你的钱包里还没有钱。 \n请尝试重启您的应用程序或者检查您的网络连接。 # suppress inspection "TrailingSpacesInProperty" takeOffer.error.feePaid=\n!\n takeOffer.error.depositPublished=\n\n您的保证金转账已经发布。\n请尝试重启您的应用程序或者检查您的网络连接。\n如果始终存在问题,请到帮助界面联系开发者。 takeOffer.error.payoutPublished=\n\n您的支付转账已经发布。\n请尝试重启您的应用程序或者检查您的网络连接。\n如果始终存在问题,请到帮助界面联系开发者。 takeOffer.tac=接受该报价,意味着我同意这交易界面中的条件。 #################################################################### # Offerbook / Edit offer #################################################################### openOffer.header.triggerPrice=触发价格 openOffer.triggerPrice=Trigger price {0} openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\nPlease edit the offer to define a new trigger price editOffer.setPrice=设定价格 editOffer.confirmEdit=确认:编辑报价 editOffer.publishOffer=发布您的报价。 editOffer.failed=报价编辑失败:\n{0} editOffer.success=您的报价已成功编辑。 editOffer.invalidDeposit=买方保证金不符合 Haveno DAO 规定,不能再次编辑。 #################################################################### # Portfolio #################################################################### portfolio.tab.openOffers=我的未完成报价 portfolio.tab.pendingTrades=未完成交易 portfolio.tab.history=历史记录 portfolio.tab.failed=失败 portfolio.tab.editOpenOffer=编辑报价 portfolio.closedTrades.deviation.help=与市场价格偏差百分比 portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} portfolio.pending.syncing=同步交易钱包 portfolio.pending.syncing.blockRemaining=同步交易钱包 — 剩余 1 个区块 portfolio.pending.syncing.blocksRemaining=同步交易钱包 — 剩余 {0} 个区块 portfolio.pending.step1.waitForConf=等待区块链确认 portfolio.pending.step2_buyer.additionalConf=存款已达到 10 个确认。\n为了额外安全,我们建议在发送付款前等待 {0} 个确认。\n提前操作风险自负。 portfolio.pending.step2_buyer.startPayment=开始付款 portfolio.pending.step2_seller.waitPaymentSent=等待直到付款 portfolio.pending.step3_buyer.waitPaymentArrived=等待直到付款到达 portfolio.pending.step3_seller.confirmPaymentReceived=确定收到付款 portfolio.pending.step5.completed=完成 portfolio.pending.step3_seller.autoConf.status.label=自动确认状态。 portfolio.pending.autoConf=自动确认 portfolio.pending.autoConf.blocks=XMR 确认数:{0} / 需求量:{2} portfolio.pending.autoConf.state.xmr.txKeyReused=交易密钥已重复使用。请发起纠纷处理。 portfolio.pending.autoConf.state.confirmations=XMR 确认:{0}/{1} portfolio.pending.autoConf.state.txNotFound=交易并未在内存池中检索。 portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=无有效交易 ID / 交易密钥 portfolio.pending.autoConf.state.filterDisabledFeature=由开发者禁用 # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FEATURE_DISABLED=自动确认功能已禁用。{0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=交易金额超过自动确认金额限制。 # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.INVALID_DATA=对等点提供不可用数据。{0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=支付交易已经发布 # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.DISPUTE_OPENED=已发起纠纷。该交易的自动确认已被禁用 # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.REQUESTS_STARTED=交易证明申请已经开始 # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PENDING=成功结果:{0}/{1} ;{2} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.COMPLETED=所有服务都已被证明。 # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.ERROR=您请求的服务发生了错误。没有自动确认。 # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=服务返回失败。没有自动确认。 portfolio.pending.step1.info.you=存款交易已发布。\n在获得 10 次确认后,您即可开始付款(约 {0} 分钟后)。 portfolio.pending.step1.info.buyer=存款交易已发布。\n在获得 10 次确认后,XMR 买方即可开始付款(约 {0} 分钟后)。 portfolio.pending.step1.warn=保证金交易仍未得到确认。这种情况可能会发生在外部钱包转账时使用的交易手续费用较低造成的。 portfolio.pending.step1.openForDispute=保证金交易仍未得到确认。请联系调解员协助。 # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2.confReached=Your trade has reached at least one blockchain confirmation.\n\n portfolio.pending.step2_buyer.refTextWarn=Important: when making the payment, leave the \"reason for payment\" field empty. DO NOT put the trade ID or any other text like 'monero', 'XMR', or 'Haveno'. You are free to discuss via trader chat if an alternate \"reason for payment\" would be suitable to you both. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees=If your bank charges you any fees to make the transfer, you are responsible for paying those fees. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.crypto=请从您的外部 {0} 钱包划转\n{1} 到 XMR 卖家。\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.cash=请到银行并支付 {0} 给 XMR 卖家。\n\n portfolio.pending.step2_buyer.cash.extra=重要要求:\n完成付款后在纸质收据上写下:不退款。\n然后将其撕成2份,拍照片并发送给 XMR 卖家的电子邮件地址。 # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.moneyGram=请使用 MoneyGram 向 XMR 卖家支付 {0}。\n\n portfolio.pending.step2_buyer.moneyGram.extra=重要要求:\n完成支付后,请通过电邮发送授权编号和照片给 XMR 卖家。\n收据必须清楚地向卖家写明您的全名、城市、国家或地区、数量。卖方的电子邮件是:{0}。 # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.westernUnion=请使用 Western Union 向 XMR 卖家支付 {0}。\n\n portfolio.pending.step2_buyer.westernUnion.extra=重要要求:\n完成支付后,请通过电邮发送 MTCN(追踪号码)和照片给 XMR 卖家。\n收据必须清楚地向卖家写明您的全名、城市、国家或地区、数量。卖方的电子邮件是:{0}。 # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=请用“美国邮政汇票”发送 {0} 给 XMR 卖家。\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=请通过提供的联系人与 XMR 卖家联系,并安排会议支付 {0}。\n\n portfolio.pending.step2_buyer.startPaymentUsing=使用 {0} 开始付款 portfolio.pending.step2_buyer.recipientsAccountData=接受 {0} portfolio.pending.step2_buyer.amountToTransfer=划转数量 portfolio.pending.step2_buyer.sellersAddress=卖家的 {0} 地址 portfolio.pending.step2_buyer.buyerAccount=您的付款帐户将被使用 portfolio.pending.step2_buyer.paymentSent=付款开始 portfolio.pending.step2_buyer.showEarly=提前显示付款详情 portfolio.pending.step2_buyer.warn=你还没有完成你的 {0} 付款!\n请注意,交易必须在 {1} 之前完成。 portfolio.pending.step2_buyer.openForDispute=您还没有完成您的付款!\n最大交易期限已过。请联系调解员寻求帮助。 portfolio.pending.step2_buyer.paperReceipt.headline=您是否将纸质收据发送给 XMR 卖家? portfolio.pending.step2_buyer.paperReceipt.msg=请牢记:\n完成付款后在纸质收据上写下:不退款。\n然后将其撕成2份,拍照片并发送给 XMR 卖家的电子邮件地址。 portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=发送授权编号和收据 portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=请通过电邮发送授权编号和照片给 XMR 卖家。\n收据必须清楚地向卖家写明您的全名、城市、国家或地区、数量。卖方的电子邮件是:{0}。\n\n您把授权编号和合同发给卖方了吗? portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=发送 MTCN 和收据 portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=请通过电邮发送 MTCN(追踪号码)和照片给 XMR 卖家。\n收据必须清楚地向卖家写明您的全名、城市、国家或地区、数量。卖方的电子邮件是:{0}。\n\n您把 MTCN 和合同发给卖方了吗? portfolio.pending.step2_buyer.halCashInfo.headline=请发送 HalCash 代码 portfolio.pending.step2_buyer.halCashInfo.msg=您需要向 XMR 卖家发送带有 HalCash 代码和交易 ID({0})的文本消息。\n\n卖方的手机号码是 {1} 。\n\n您是否已经将代码发送至卖家? portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=有些银行可能会要求接收方的姓名。在较旧的 Haveno 客户端创建的快速支付帐户没有提供收款人的姓名,所以请使用交易聊天来获得收款人姓名(如果需要)。 portfolio.pending.step2_buyer.confirmStart.headline=确定您已经付款 portfolio.pending.step2_buyer.confirmStart.msg=您是否向您的交易伙伴发起 {0} 付款? portfolio.pending.step2_buyer.confirmStart.yes=是的,我已经开始付款 portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=你没有提供任何付款证明 portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=您还没有输入交易 ID 以及交易密钥\n\n如果不提供此数据您的交易伙伴无法在收到 XMR 后使用自动确认功能以快速释放 XMR。\n另外,Haveno 要求 XMR 发送者在发生纠纷的时候能够向调解员和仲裁员提供这些信息。\n更多细节在 Haveno Wiki:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=输入并不是一个 32 字节的哈希值 portfolio.pending.step2_buyer.confirmStart.warningButton=忽略并继续 portfolio.pending.step2_seller.waitPayment.headline=等待付款 portfolio.pending.step2_seller.f2fInfo.headline=买家的合同信息 portfolio.pending.step2_seller.waitPayment.msg=存款交易至少有一个区块链确认。\n您需要等到 XMR 买家开始 {0} 付款。 portfolio.pending.step2_seller.warn=XMR 买家仍然没有完成 {0} 付款。\n你需要等到他开始付款。\n如果 {1} 交易尚未完成,仲裁员将进行调查。 portfolio.pending.step2_seller.openForDispute=XMR 买家尚未开始付款!\n允许的最长交易期限已经过去了。你可以继续等待给予交易双方更多时间,或联系仲裁员以争取解决纠纷。 tradeChat.chatWindowTitle=使用 ID “{0}” 进行交易的聊天窗口 tradeChat.openChat=打开聊天窗口 tradeChat.rules=您可以与您的伙伴沟通,以解决该交易的潜在问题。\n在聊天中不强制回复。\n如果交易员违反了下面的任何规则,打开纠纷并向调解员或仲裁员报告。\n聊天规则:\n\n\t●不要发送任何链接(有恶意软件的风险)。您可以发送交易 ID 和区块资源管理器的名称。\n\t●不要发送还原密钥、私钥、密码或其他敏感信息!\n\t●不鼓励 Haveno 以外的交易(无安全保障)。\n\t●不要参与任何形式的危害社会安全的计划。\n\t●如果对方没有回应,也不愿意通过聊天进行沟通,那就尊重对方的决定。\n\t●将谈话范围限制在行业内。这个聊天不是一个社交软件替代品或troll-box。\n\t●保持友好和尊重的交谈。 # suppress inspection "UnusedProperty" message.state.UNDEFINED=未定义 # suppress inspection "UnusedProperty" message.state.SENT=发出信息 # suppress inspection "UnusedProperty" message.state.ARRIVED=消息已抵达 # suppress inspection "UnusedProperty" message.state.STORED_IN_MAILBOX=已发送但尚未被对方接收的付款信息 # suppress inspection "UnusedProperty" message.state.ACKNOWLEDGED=对方确认消息回执 # suppress inspection "UnusedProperty" message.state.FAILED=发送消息失败 portfolio.pending.step3_buyer.wait.headline=等待 XMR 卖家付款确定 portfolio.pending.step3_buyer.wait.info=等待 XMR 卖家确认收到 {0} 付款。 portfolio.pending.step3_buyer.wait.msgStateInfo.label=支付开始消息状态 portfolio.pending.step3_buyer.warn.part1a=在 {0} 区块链 portfolio.pending.step3_buyer.warn.part1b=在您的支付供应商(例如:银行) portfolio.pending.step3_buyer.warn.part2=XMR 卖家仍然没有确认您的付款。如果付款发送成功,请检查 {0}。 portfolio.pending.step3_buyer.openForDispute=XMR 卖家还没有确认你的付款!最大交易期限已过。您可以等待更长时间,并给交易伙伴更多时间或请求调解员的帮助。 # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.part=您的交易伙伴已经确认他们已经发起了 {0} 付款。\n\n portfolio.pending.step3_seller.crypto.explorer=在您最喜欢的 {0} 区块链浏览器 portfolio.pending.step3_seller.crypto.wallet=在您的 {0} 钱包 portfolio.pending.step3_seller.crypto={0} 请检查 {1} 是否交易已经到您的接收地址\n{2}\n已经有足够的区块链确认了\n支付金额必须为 {3}\n\n关闭该弹出窗口后,您可以从主界面复制并粘贴 {4} 地址。 portfolio.pending.step3_seller.postal={0}Please check if you have received {1} with \"US Postal Money Order\" from the XMR buyer. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.payByMail={0}Please check if you have received {1} with \"Pay by Mail\" from the XMR buyer. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.bank=Your trading partner has confirmed that they have initiated the {0} payment.\n\nPlease go to your online banking web page and check if you have received {1} from the XMR buyer. portfolio.pending.step3_seller.cash=因为付款是通过现金存款完成的,XMR 买家必须在纸质收据上写“不退款”,将其撕成2份,并通过电子邮件向您发送照片。\n\n为避免退款风险,请仅确认您是否收到电子邮件,如果您确定收据有效。\n如果您不确定,{0} portfolio.pending.step3_seller.moneyGram=买方必须发送授权编码和一张收据的照片。\n收据必须清楚地显示您的全名、城市、国家或地区、数量。如果您收到授权编码,请查收邮件。\n\n关闭弹窗后,您将看到 XMR 买家的姓名和在 MoneyGram 的收款地址。\n\n只有在您成功收到钱之后,再确认收据! portfolio.pending.step3_seller.westernUnion=买方必须发送 MTCN(跟踪号码)和一张收据的照片。\n收据必须清楚地显示您的全名、城市、国家或地区、数量。如果您收到 MTCN,请查收邮件。\n\n关闭弹窗后,您将看到 XMR 买家的姓名和在 Western Union 的收款地址。\n\n只有在您成功收到钱之后,再确认收据! portfolio.pending.step3_seller.halCash=买方必须将 HalCash代码 用短信发送给您。除此之外,您将收到来自 HalCash 的消息,其中包含从支持 HalCash 的 ATM 中提取欧元所需的信息\n从 ATM 取款后,请在此确认付款收据! portfolio.pending.step3_seller.amazonGiftCard=XMR 买家已经发送了一张亚马逊电子礼品卡到您的邮箱或手机短信。请现在立即兑换亚马逊电子礼品卡到您的亚马逊账户中以及确认交易信息。 portfolio.pending.step3_seller.bankCheck=\n\n还请确认您的银行对帐单中的发件人姓名与委托合同中的发件人姓名相符:\n发件人姓名:{0}\n\n如果名称与此处显示的名称不同,则 {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=请不要确认,而是通过键盘组合键“alt + o”或“option + o”来打开纠纷。 portfolio.pending.step3_seller.confirmPaymentReceipt=确定付款收据 portfolio.pending.step3_seller.amountToReceive=接收数量: portfolio.pending.step3_seller.yourAddress=您的 {0} 地址 portfolio.pending.step3_seller.buyersAddress=卖家的 {0} 地址 portfolio.pending.step3_seller.yourAccount=您的交易账户 portfolio.pending.step3_seller.xmrTxHash=交易记录 ID portfolio.pending.step3_seller.xmrTxKey=交易密钥 portfolio.pending.step3_seller.buyersAccount=买方账号数据 portfolio.pending.step3_seller.confirmReceipt=确定付款收据 portfolio.pending.step3_seller.buyerStartedPayment=XMR 买家已经开始 {0} 的付款。\n{1} portfolio.pending.step3_seller.buyerStartedPayment.crypto=检查您的数字货币钱包或块浏览器的区块链确认,并确认付款时,您有足够的块链确认。 portfolio.pending.step3_seller.buyerStartedPayment.traditional=检查您的交易账户(例如银行帐户),并确认您何时收到付款。 portfolio.pending.step3_seller.warn.part1a=在 {0} 区块链 portfolio.pending.step3_seller.warn.part1b=在您的支付供应商(例如:银行) portfolio.pending.step3_seller.warn.part2=你还没有确认收到款项。如果您已经收到款项,请检查 {0}。 portfolio.pending.step3_seller.openForDispute=您尚未确认付款的收据!\n最大交易期已过\n请确认或请求调解员的协助。 # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=您是否收到了您交易伙伴的 {0} 付款?\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.name=还请确认您的银行对帐单中的发件人姓名与委托合同中的发件人姓名相符:\n每个交易合约的发送者姓名:{0}\n\n如果名称与此处显示的名称不一致,请不要通过确认付款,而是通过“alt + o”或“option + o”打开纠纷。\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=请注意,一旦您确认收到,冻结交易金额将被发放给 XMR 买家,保证金将被退还。 portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=确定您已经收到付款 portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=是的,我已经收到付款。 portfolio.pending.step3_seller.onPaymentReceived.signer=重要提示:通过确认收到付款,你也验证了对方的账户,并获得验证。因为对方的账户还没有验证,所以你应该尽可能的延迟付款的确认,以减少退款的风险。 portfolio.pending.step5_buyer.groupTitle=完成交易的概要 portfolio.pending.step5_buyer.tradeFee=挂单费 portfolio.pending.step5_buyer.makersMiningFee=矿工手续费 portfolio.pending.step5_buyer.takersMiningFee=总共挖矿手续费 portfolio.pending.step5_buyer.refunded=退还保证金 portfolio.pending.step5_buyer.withdrawXMR=提现您的比特币 portfolio.pending.step5_buyer.amount=提现数量 portfolio.pending.step5_buyer.withdrawToAddress=提现地址 portfolio.pending.step5_buyer.moveToHavenoWallet=在 Haveno 钱包中保留资金 portfolio.pending.step5_buyer.withdrawExternal=提现到外部钱包 portfolio.pending.step5_buyer.alreadyWithdrawn=您的资金已经提现。\n请查看交易历史记录。 portfolio.pending.step5_buyer.confirmWithdrawal=确定提现请求 portfolio.pending.step5_buyer.amountTooLow=转让金额低于交易费用和最低可能的tx值(零头)。 portfolio.pending.step5_buyer.withdrawalCompleted.headline=提现完成 portfolio.pending.step5_buyer.withdrawalCompleted.msg=您完成的交易存储在“业务/历史记录”下。\n您可以查看“资金/交易”下的所有比特币交易 portfolio.pending.step5_buyer.bought=您已经买入 portfolio.pending.step5_buyer.paid=您已经支付 portfolio.pending.step5_seller.sold=您已经卖出 portfolio.pending.step5_seller.received=您已经收到 tradeFeedbackWindow.title=恭喜您完成交易 tradeFeedbackWindow.msg.part1=我们很想听听您的体验如何。这将帮助我们改进软件,优化体验不好的地方。如欲提供意见,请填写这份简短的问卷(无需注册),网址: tradeFeedbackWindow.msg.part2=如果您有任何疑问或遇到任何问题,请通过 Haveno 论坛与其他用户和贡献者联系: tradeFeedbackWindow.msg.part3=感谢使用 Haveno portfolio.pending.role=我的角色 portfolio.pending.tradeInformation=交易信息 portfolio.pending.remainingTime=剩余时间 portfolio.pending.remainingTimeDetail={0}(直到 {1} ) portfolio.pending.remainingTimeDetail.startsAfter=在获得 {0} 次确认后开始 portfolio.pending.tradePeriodInfo=在获得 {0} 次确认后,交易期开始。根据所使用的付款方式,将适用不同的最大允许交易期。 portfolio.pending.tradePeriodWarning=如果超过了这个周期,双方均可以提出纠纷。 portfolio.pending.tradeNotCompleted=交易不会及时完成(直到 {0} ) portfolio.pending.tradeProcess=交易流程 portfolio.pending.openAgainDispute.msg=如果您不确定发送给调解员或仲裁员的消息是否已送达(例如,如果您在1天后没有收到回复),请放心使用 Cmd/Ctrl+o 再次打开纠纷。你也可以在 Haveno 论坛上寻求额外的帮助,网址是 https://haveno.community。 portfolio.pending.openAgainDispute.button=再次出现纠纷 portfolio.pending.openSupportTicket.headline=创建帮助话题 portfolio.pending.openSupportTicket.msg=请仅在紧急情况下使用此功能,如果您没有看到“提交支持”或“提交纠纷”按钮。\n\n当您发出工单时,交易将被中断并由调解员或仲裁员进行处理。 portfolio.pending.timeLockNotOver=你必须等到≈{0}(还需等待{1}个区块)才能提交纠纷。 portfolio.pending.error.depositTxNull=保证金交易无效。没有有效的保证金交易,你使用创建纠纷。请到“设置/网络信息”进行 SPV 重新同步。\n \n如需更多帮助,请联系 Haveno Keybase 团队的 Support 频道。 portfolio.pending.mediationResult.error.depositTxNull=保证金交易为空。你可以移动该交易至失败的交易。 portfolio.pending.mediationResult.error.delayedPayoutTxNull=延迟支付交易为空。你可以移动该交易至失败的交易。 portfolio.pending.error.depositTxNotConfirmed=保证金交易未确认。未经确认的存款交易不能发起纠纷或仲裁请求。请耐心等待,直到它被确认或进入“设置/网络信息”进行 SPV 重新同步。\n\n如需更多帮助,请联系 Haveno Keybase 团队的 Support 频道。 portfolio.pending.support.headline.getHelp=需要帮助? portfolio.pending.support.text.getHelp=如果您有任何问题,您可以尝试在交易聊天中联系交易伙伴,或在 https://haveno.community 询问 Haveno 社区。如果您的问题仍然没有解决,您可以向调解员取得更多的帮助。 portfolio.pending.support.button.getHelp=开启交易聊天 portfolio.pending.support.headline.halfPeriodOver=确认付款 portfolio.pending.support.headline.periodOver=交易期结束 portfolio.pending.support.headline.depositTxMissing=缺少存款交易 portfolio.pending.support.depositTxMissing=此交易缺少存款交易。请提交支持工单以联系仲裁员寻求帮助。 portfolio.pending.mediationRequested=已请求调解员协助 portfolio.pending.refundRequested=已请求退款 portfolio.pending.openSupport=创建帮助话题 portfolio.pending.supportTicketOpened=帮助话题已经创建 portfolio.pending.communicateWithArbitrator=请在“帮助”界面上与仲裁员联系。 portfolio.pending.communicateWithMediator=请在“支持”页面中与调解员进行联系。 portfolio.pending.disputeOpenedByUser=您创建了一个纠纷。\n{0} portfolio.pending.disputeOpenedByPeer=您的交易对象创建了一个纠纷。\n{0} portfolio.pending.noReceiverAddressDefined=没有定义接收地址 portfolio.pending.mediationResult.headline=调解费用的支出 portfolio.pending.mediationResult.info.noneAccepted=通过接受调解员关于交易的建议的支出来完成交易。 portfolio.pending.mediationResult.info.selfAccepted=你已经接受了调解员的建议。等待伙伴接受。 portfolio.pending.mediationResult.info.peerAccepted=你的伙伴已经接受了调解员的建议。你也接受吗? portfolio.pending.mediationResult.button=查看建议的解决方案 portfolio.pending.mediationResult.popup.headline=调解员在交易 ID:{0}上的建议 portfolio.pending.mediationResult.popup.headline.peerAccepted=你的伙伴已经接受了调解员的建议 portfolio.pending.mediationResult.popup.info=调解员建议的支出如下:\n你将支付:{0}\n你的交易伙伴将支付:{1}\n\n你可以接受或拒绝这笔调解费支出。\n\n通过接受,你验证了合约的支付交易。如果你的交易伙伴也接受和验证,支付将完成,交易将关闭。\n\n如果你们其中一人或双方都拒绝该建议,你将必须等到(2)({3}区块)与仲裁员展开第二轮纠纷讨论,仲裁员将再次调查该案件,并根据他们的调查结果进行支付。\n\n仲裁员可以收取少量费用(费用上限:交易的保证金)作为其工作的补偿。两个交易者都同意调解员的建议是愉快的路径请求仲裁是针对特殊情况的,比如如果一个交易者确信调解员没有提出公平的赔偿建议(或者如果另一个同伴没有回应)。\n\n关于新的仲裁模型的更多细节:https://docs.haveno.exchange/trading-rules.html#arbitration portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=您已经接受了调解员的建议支付但是似乎您的交易对手并没有接受。\n\n一旦锁定时间到{0}(区块{1})您可以打开第二轮纠纷让仲裁员重新研究该案件并重新作出支出决定。\n\n您可以找到更多关于仲裁模型的信息在:\nhttps://docs.haveno.exchange/trading-rules.html#arbitration portfolio.pending.mediationResult.popup.openArbitration=拒绝并请求仲裁 portfolio.pending.mediationResult.popup.alreadyAccepted=您已经接受了。 portfolio.pending.failedTrade.taker.missingTakerFeeTx=吃单交易费未找到。\n\n如果没有 tx,交易不能完成。没有资金被锁定以及没有支付交易费用。你可以将交易移至失败的交易。 portfolio.pending.failedTrade.maker.missingTakerFeeTx=挂单费交易未找到。\n\n如果没有 tx,交易不能完成。没有资金被锁定以及没有支付交易费用。你可以将交易移至失败的交易。 portfolio.pending.failedTrade.missingDepositTx=缺少一笔保证金交易。\n\n该交易是完成交易所必需的。请确保您的钱包已与 Monero 区块链完全同步。\n\n您可以将此交易移动到“失败的交易”部分以将其停用。 portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=延迟支付交易缺失,但是资金仍然被锁定在保证金交易中。\n\n请不要给比特币卖家发送法币或数字货币,因为没有延迟交易 tx,不能开启仲裁。使用 Cmd/Ctrl+o开启调解协助。调解员应该建议交易双方分别退回全部的保证金(卖方支付的交易金额也会全数返还)。这样的话不会有任何的安全问题只会损失交易手续费。\n\n你可以在这里为失败的交易提出赔偿要求:https://github.com/haveno-dex/haveno/issues portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=延迟支付交易确实但是资金仍然被锁定在保证金交易中。\n\n如果卖家仍然缺失延迟支付交易,他会接到请勿付款的指示并开启一个调节帮助。你也应该使用 Cmd/Ctrl+O 去打开一个调节协助\n\n如果买家还没有发送付款,调解员应该会建议交易双方分别退回全部的保证金(卖方支付的交易金额也会全数返还)。否则交易额应该判给买方。\n\n你可以在这里为失败的交易提出赔偿要求:https://github.com/haveno-dex/haveno/issues portfolio.pending.failedTrade.errorMsgSet=在处理交易协议是发生了一个错误\n\n错误:{0}\n\n这应该不是致命错误,您可以正常的完成交易。如果你仍担忧,打开一个调解协助并从 Haveno 调解员处得到建议。\n\n如果这个错误是致命的那么这个交易就无法完成,你可能会损失交易费。可以在这里为失败的交易提出赔偿要求:https://github.com/haveno-dex/haveno/issues portfolio.pending.failedTrade.missingContract=没有设置交易合同。\n\n这个交易无法完成,你可能会损失交易手续费。可以在这里为失败的交易提出赔偿要求:https://github.com/haveno-dex/haveno/issues portfolio.pending.failedTrade.info.popup=交易协议出现了问题。\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=交易协议出现了严重问题。\n\n{0}\n\n您确定想要将该交易移至失败的交易吗?\n\n您不能在失败的交易中打开一个调解或仲裁,但是你随时可以将失败的交易重新移至未完成交易。 portfolio.pending.failedTrade.txChainValid.moveToFailed=这个交易协议存在一些问题。\n\n{0}\n\n这个报价交易已经被发布以及资金已被锁定。只有在确定情况下将该交易移至失败交易。这可能会阻止解决问题的可用选项。\n\n您确定想要将该交易移至失败的交易吗?\n\n您不能在失败的交易中打开一个调解或仲裁,但是你随时可以将失败的交易重新移至未完成交易。 portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=将交易移至失败交易 portfolio.pending.failedTrade.warningIcon.tooltip=点击打开该交易的问题细节 portfolio.failed.revertToPending.popup=您想要将该交易移至未完成交易吗 portfolio.failed.revertToPending=将交易移至未完成交易 portfolio.closed.completed=完成 portfolio.closed.ticketClosed=已仲裁 portfolio.closed.mediationTicketClosed=已调解 portfolio.closed.canceled=已取消 portfolio.failed.Failed=失败 portfolio.failed.unfail=再继续之前,请保证你有一份根目录的备份!\n您想要将此交易移至未完成的交易吗?\n这是一个解锁卡在失败交易的资金的方法 portfolio.failed.cantUnfail=目前该交易暂无法移至未完成的交易。\n请在完成交易后重试{0} portfolio.failed.depositTxNull=交易无法恢复至未完成交易。保证金交易为空。 portfolio.failed.delayedPayoutTxNull=交易无法恢复至未完成交易。延迟支付交易为空。 #################################################################### # Funds #################################################################### funds.tab.deposit=存款 funds.tab.withdrawal=提现 funds.tab.reserved=保证金 funds.tab.locked=冻结资金 funds.tab.transactions=交易记录 funds.deposit.unused=尚未使用 funds.deposit.usedInTx=用在 {0} 交易 funds.deposit.fundHavenoWallet=充值 Haveno 钱包 funds.deposit.noAddresses=尚未生成存款地址 funds.deposit.fundWallet=充值您的钱包 funds.deposit.withdrawFromWallet=从钱包转出资金 funds.deposit.amount=XMR 数量(可选) funds.deposit.generateAddress=生成新的地址 funds.deposit.generateAddressSegwit=原生 segwit 格式(Bech32) funds.deposit.selectUnused=请从上表中选择一个未使用的地址,而不是生成一个新地址。 funds.withdrawal.arbitrationFee=仲裁费用 funds.withdrawal.inputs=充值选择 funds.withdrawal.useAllInputs=使用所有可用的充值地址 funds.withdrawal.useCustomInputs=使用自定义充值地址 funds.withdrawal.receiverAmount=接收者的数量 funds.withdrawal.senderAmount=发送者的数量 funds.withdrawal.feeExcluded=不含挖矿费的金额 funds.withdrawal.feeIncluded=包含挖矿费的金额 funds.withdrawal.fromLabel=从源地址提现 funds.withdrawal.toLabel=提现地址 funds.withdrawal.memoLabel=提现备注 funds.withdrawal.memo=可选备注 funds.withdrawal.withdrawButton=选定提现 funds.withdrawal.noFundsAvailable=没有可用资金提现 funds.withdrawal.confirmWithdrawalRequest=确定提现请求 funds.withdrawal.withdrawMultipleAddresses=从多个地址提现({0}) funds.withdrawal.withdrawMultipleAddresses.tooltip=从多个地址提现:\n{0} funds.withdrawal.notEnoughFunds=您钱包里没有足够的资金。 funds.withdrawal.selectAddress=从列表中选一个源地址 funds.withdrawal.setAmount=设置提现数量 funds.withdrawal.fillDestAddress=输入您的目标地址 funds.withdrawal.warn.noSourceAddressSelected=您需要从上面列表中选一个源地址。 funds.withdrawal.warn.amountExceeds=您的金额超过所选地址的可用金额。\n请考虑在上表中选择多个地址或调整手续费设置,来支付手续费。 funds.reserved.noFunds=未完成报价中没有已用资金 funds.reserved.reserved=报价 ID:{0} 接收在本地钱包中 funds.locked.noFunds=交易中没有冻结资金 funds.locked.locked=多重验证冻结交易 ID:{0} funds.tx.direction.sentTo=发送至: funds.tx.direction.receivedWith=接收到: funds.tx.direction.genesisTx=从初始 tx: funds.tx.createOfferFee=挂单和tx费用:{0} funds.tx.takeOfferFee=下单和tx费用:{0} funds.tx.multiSigDeposit=多重验证保证金:{0} funds.tx.multiSigPayout=多重验证花费:{0} funds.tx.disputePayout=纠纷花费:{0} funds.tx.disputeLost=失败的纠纷案件:{0} funds.tx.collateralForRefund=押金退款:{0} funds.tx.timeLockedPayoutTx=距锁定锁定支付 tx 的时间: {0} funds.tx.refund=仲裁退款:{0} funds.tx.unknown=未知原因:{0} funds.tx.noFundsFromDispute=没有退款的纠纷 funds.tx.receivedFunds=收到的资金: funds.tx.withdrawnFromWallet=从钱包提现 funds.tx.memo=备注 funds.tx.noTxAvailable=没有可用交易 funds.tx.revert=还原 funds.tx.txSent=交易成功发送到本地 Haveno 钱包中的新地址。 funds.tx.direction.self=内部钱包交易 funds.tx.dustAttackTx=接受零头 funds.tx.dustAttackTx.popup=这笔交易是发送一个非常小的比特币金额到您的钱包,可能是区块链分析公司尝试监控您的交易。\n\n如果您在交易中使用该交易输出,他们将了解到您很可能也是其他地址的所有者(资金归集)。\n\n为了保护您的隐私,Haveno 钱包忽略了这种零头的消费和余额显示。可以在设置中将输出视为零头时设置阈值量。 #################################################################### # Support #################################################################### support.tab.mediation.support=调解 support.tab.arbitration.support=仲裁 support.tab.legacyArbitration.support=历史仲裁 support.tab.ArbitratorsSupportTickets={0} 的工单 support.sigCheck.button=Check signature support.sigCheck.popup.info=请粘贴仲裁过程的摘要信息。使用这个工具,任何用户都可以检查仲裁者的签名是否与摘要信息相符。 support.sigCheck.popup.header=确认纠纷结果签名 support.sigCheck.popup.msg.label=总结消息 support.sigCheck.popup.msg.prompt=复制粘贴纠纷总结消息 support.sigCheck.popup.result=验证结果 support.sigCheck.popup.success=签名有效 support.sigCheck.popup.failed=签名验证失败 support.sigCheck.popup.invalidFormat=消息并不是正确的格式。请复制粘贴纠纷总结消息。 support.reOpenByTrader.prompt=您确定想要重新开启纠纷? support.reOpenButton.label=重新打开 support.sendNotificationButton.label=私人通知 support.reportButton.label=报告 support.fullReportButton.label=所有纠纷 support.noTickets=没有创建的话题 support.sendingMessage=发送消息... support.receiverNotOnline=收件人未在线。消息被保存到他们的邮箱。 support.sendMessageError=发送消息失败。错误:{0} support.receiverNotKnown=Receiver not known support.wrongVersion=纠纷中的订单创建于一个旧版本的 Haveno。\n您不能在当前版本关闭这个纠纷。\n\n请您使用旧版本/协议版本: {0} support.openFile=打开附件文件(文件最大大小:{0} kb) support.attachmentTooLarge=您的附件的总大小为 {0} kb,并超过最大值。 允许消息大小为 {1} kB。 support.maxSize=文件允许的最大大小 {0} kB。 support.attachment=附件 support.tooManyAttachments=您不能在一个消息里发送超过3个附件。 support.save=保存文件到磁盘 support.messages=消息 support.input.prompt=输入消息... support.send=发送 support.addAttachments=添加附件 support.closeTicket=关闭话题 support.attachments=附件: support.savedInMailbox=消息保存在收件人的信箱中 support.arrived=消息抵达收件人 support.acknowledged=收件人已确认接收消息 support.error=收件人无法处理消息。错误:{0} support.buyerAddress=XMR 买家地址 support.sellerAddress=XMR 卖家地址 support.role=角色 support.agent=Support agent support.state=状态 support.chat=Chat support.preparing=准备中 support.requested=请求 support.closed=关闭 support.open=打开 support.process=Process support.buyerMaker=XMR 买家/挂单者 support.sellerMaker=XMR 卖家/挂单者 support.buyerTaker=XMR 买家/买单者 support.sellerTaker=XMR 卖家/买单者 support.backgroundInfo=Haveno 不是一家公司,因此它以不同的方式处理纠纷。\n\n交易者可以在应用程序内通过打开交易屏幕上的安全聊天来尝试自行解决纠纷。如果这不够,仲裁员将评估情况并决定交易资金的支付。 support.initialInfo=请在下面的文本框中输入您的问题描述。添加尽可能多的信息,以加快解决纠纷的时间。\n\n以下是你应提供的资料核对表:\n\t●如果您是 XMR 买家:您是否使用法定货币或其他加密货币转账?如果是,您是否点击了应用程序中的“支付开始”按钮?\n\t●如果您是 XMR 卖家:您是否收到法定货币或其他加密货币的付款了?如果是,你是否点击了应用程序中的“已收到付款”按钮?\n\t●您使用的是哪个版本的 Haveno?\n\t●您使用的是哪种操作系统?\n\t●如果遇到操作执行失败的问题,请考虑切换到新的数据目录。\n\t有时数据目录会损坏,并导致奇怪的错误。\n详见:https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\n请熟悉纠纷处理的基本规则:\n\t●您需要在2天内答复 {0} 的请求。\n\t●调解员会在2天之内答复,仲裁员会在5天之内答复。\n\t●纠纷的最长期限为14天。\n\t●你需要与仲裁员合作,提供他们为你的案件所要求的信息。\n\t●当您第一次启动应用程序时,您接受了用户协议中争议文档中列出的规则。\n\n您可以通过 {2} 了解有关纠纷处理的更多信息 support.systemMsg=系统消息:{0} support.youOpenedTicket=您创建了帮助请求。\n\n{0}\n\nHaveno 版本:{1} support.youOpenedDispute=您创建了一个纠纷请求。\n\n{0}\n\nHaveno 版本:{1} support.youOpenedDisputeForMediation=您创建了一个调解请求。\n\n{0}\n\nHaveno 版本:{1} support.peerOpenedTicket=对方因技术问题请求获取帮助。\n\n{0}\n\nHaveno 版本:{1} support.peerOpenedDispute=对方创建了一个纠纷请求。\n\n{0}\n\nHaveno 版本:{1} support.peerOpenedDisputeForMediation=对方创建了一个调解请求。\n\n{0}\n\nHaveno 版本:{1} support.mediatorsDisputeSummary=系统消息:\n调解纠纷总结:\n{0} support.mediatorsAddress=仲裁员的节点地址:{0} support.warning.disputesWithInvalidDonationAddress=延迟支付交易已经被用于一个不可用接受者地址。它与有效捐赠地址的任何 DAO 中参数值均不匹配。\n\n这可能是一个骗局。请将该事件通知开发者,在问题解决之前不要关闭该案件!\n\n纠纷所用的地址:{0}\n\n所有 DAO 参数中捐赠地址:{1}\n\n交易:{2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\n您确定一定要关闭纠纷吗? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\n您不能进行支付。 support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. support.info.disputeReOpened=Dispute ticket has been re-opened. #################################################################### # Settings #################################################################### settings.tab.preferences=偏好 settings.tab.network=网络信息 settings.tab.about=关于我们 setting.preferences.general=通用偏好 setting.preferences.explorer=比特币区块浏览器 setting.preferences.deviation=与市场价格最大差价 setting.preferences.avoidStandbyMode=避免待机模式 setting.preferences.useSoundForNotifications=播放通知声音 setting.preferences.autoConfirmXMR=XMR 自动确认 setting.preferences.autoConfirmEnabled=启用 setting.preferences.autoConfirmRequiredConfirmations=已要求确认 setting.preferences.autoConfirmMaxTradeSize=最大交易量(XMR) setting.preferences.autoConfirmServiceAddresses=Monero Explorer 链接(使用Tor,但本地主机,LAN IP地址和 *.local 主机名除外) setting.preferences.deviationToLarge=值不允许大于30% setting.preferences.txFee=提现交易手续费(聪/字节) setting.preferences.useCustomValue=使用自定义值 setting.preferences.txFeeMin=交易手续费必须至少为{0} 聪/字节 setting.preferences.txFeeTooLarge=您输入的数额超过可接受值(>5000 聪/字节)。交易手续费一般在 50-400 聪/字节、 setting.preferences.ignorePeers=忽略节点 [洋葱地址:端口] setting.preferences.ignoreDustThreshold=最小无零头输出值 setting.preferences.currenciesInList=市场价的货币列表 setting.preferences.prefCurrency=首选货币 setting.preferences.displayTraditional=显示国家货币 setting.preferences.noTraditional=没有选定国家货币 setting.preferences.cannotRemovePrefCurrency=您不能删除您选定的首选货币 setting.preferences.displayCryptos=显示数字货币 setting.preferences.noCryptos=没有选定数字货币 setting.preferences.addTraditional=添加法定货币 setting.preferences.addCrypto=添加数字货币 setting.preferences.displayOptions=显示选项 setting.preferences.showOwnOffers=在报价列表中显示我的报价 setting.preferences.useAnimations=使用动画 setting.preferences.useDarkMode=使用夜间模式 setting.preferences.useLightMode=使用浅色模式 setting.preferences.sortWithNumOffers=使用“报价ID/交易ID”筛选列表 setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API setting.preferences.notifyOnPreRelease=Receive pre-release notifications setting.preferences.resetAllFlags=重置所有“不再提示”的提示 settings.preferences.languageChange=同意重启请求以更换语言 settings.preferences.supportLanguageWarning=如有任何争议,请注意仲裁在 {0} 处理。 settings.preferences.editCustomExplorer.headline=浏览设置。 settings.preferences.editCustomExplorer.description=从左侧列表中选择一个系统默认浏览器,或使用您偏好的自定义设置。 settings.preferences.editCustomExplorer.available=可用浏览器 settings.preferences.editCustomExplorer.chosen=已选择的浏览器设置 settings.preferences.editCustomExplorer.name=名称 settings.preferences.editCustomExplorer.txUrl=交易 URL settings.preferences.editCustomExplorer.addressUrl=地址 URL setting.info.headline=新的数据隐私功能 settings.preferences.sensitiveDataRemoval.msg=为了保护您和其他交易者的隐私,Haveno 计划从旧交易中删除敏感数据。这对于可能包含银行账户信息的法币交易尤其重要。\n\n建议将其设置得尽可能低,例如 60 天。这意味着超过 60 天且已完成的交易将被清除敏感数据。已完成的交易可在“投资组合 / 历史”标签中找到。 settings.net.xmrHeader=比特币网络 settings.net.p2pHeader=Haveno 网络 settings.net.onionAddressLabel=我的匿名地址 settings.net.xmrNodesLabel=使用自定义比特币主节点 settings.net.moneroPeersLabel=已连接节点 settings.net.connection=连接 settings.net.connected=连接 settings.net.useTorForXmrJLabel=使用 Tor 连接 Monero 网络 settings.net.moneroNodesLabel=需要连接 Monero settings.net.useProvidedNodesRadio=使用公共比特币核心节点 settings.net.usePublicNodesRadio=使用公共比特币网络 settings.net.useCustomNodesRadio=使用自定义比特币主节点 settings.net.warn.usePublicNodes=如果您使用公共的Monero节点,您将面临使用不受信任的远程节点带来的任何风险。\n\n请在[HYPERLINK:https://www.getmonero.org/resources/moneropedia/remote-node.html]上阅读更多详细信息。\n\n您确定要使用公共节点吗? settings.net.warn.usePublicNodes.useProvided=不,使用给定的节点 settings.net.warn.usePublicNodes.usePublic=使用公共网络 settings.net.warn.useCustomNodes.B2XWarning=请确保您的比特币节点是一个可信的比特币核心节点!\n\n连接到不遵循比特币核心共识规则的节点可能会损坏您的钱包,并在交易过程中造成问题。\n\n连接到违反共识规则的节点的用户应对任何由此造成的损害负责。任何由此产生的纠纷都将有利于另一方。对于忽略此警告和保护机制的用户,不提供任何技术支持! settings.net.warn.invalidXmrConfig=由于您的配置无效,无法连接至比特币网络。\n\n您的配置已经被重置为默认比特币节点。你需要重启 Haveno。 settings.net.localhostXmrNodeInfo=背景信息:Haveno 在启动时会在本地查找比特币节点。如果有,Haveno 将只通过它与比特币网络进行通信。 settings.net.p2PPeersLabel=已连接节点 settings.net.onionAddressColumn=匿名地址 settings.net.creationDateColumn=已建立连接 settings.net.connectionTypeColumn=入/出 settings.net.sentDataLabel=统计数据已发送 settings.net.receivedDataLabel=统计数据已接收 settings.net.chainHeightLabel=最新 XMR 区块高度 settings.net.roundTripTimeColumn=延迟 settings.net.sentBytesColumn=发送 settings.net.receivedBytesColumn=接收 settings.net.peerTypeColumn=节点类型 settings.net.openTorSettingsButton=打开 Tor 设置 settings.net.versionColumn=版本 settings.net.subVersionColumn=子版本 settings.net.heightColumn=高度 settings.net.needRestart=您需要重启应用程序以同意这次变更。\n您需要现在重启吗? settings.net.notKnownYet=至今未知... settings.net.sentData=已发送数据 {0},{1} 条消息,{2} 条消息/秒 settings.net.receivedData=已接收数据 {0},{1} 条消息,{2} 条消息/秒 settings.net.chainHeight=Monero Peers chain height: {0} settings.net.ips=添加逗号分隔的 IP 地址及端口,如使用8333端口可不填写。 settings.net.seedNode=种子节点 settings.net.directPeer=节点(直连) settings.net.initialDataExchange={0} [Bootstrapping] settings.net.peer=节点 settings.net.inbound=接收数据包 settings.net.outbound=发送数据包 setting.about.aboutHaveno=关于 Haveno setting.about.about=Haveno 是一款开源软件,它通过分散的对等网络促进了比特币与各国货币(以及其他加密货币)的交易,严格保护了用户隐私的方式。请到我们项目的网站阅读更多关于 Haveno 的信息。 setting.about.web=Haveno 网站 setting.about.code=源代码 setting.about.agpl=AGPL 协议 setting.about.support=支持 Haveno setting.about.def=Haveno 不是一个公司,而是一个社区项目,开放参与。如果您想参与或支持 Haveno,请点击下面连接。 setting.about.contribute=贡献 setting.about.providers=数据提供商 setting.about.apisWithFee=Haveno 使用 Haveno 价格指数来表示法币与虚拟货币的市场价格,并使用 Haveno 内存池节点来估算采矿费。 setting.about.apis=Haveno 使用 Haveno 价格指数来表示法币与数字货币的市场价格。 setting.about.pricesProvided=交易所价格提供商 setting.about.feeEstimation.label=矿工手续费估算提供商 setting.about.versionDetails=版本详情 setting.about.version=应用程序版本 setting.about.subsystems.label=子系统版本 setting.about.subsystems.val=网络版本:{0};P2P 消息版本:{1};本地数据库版本:{2};交易协议版本:{3} setting.about.shortcuts=快捷键 setting.about.shortcuts.ctrlOrAltOrCmd=“Ctrl + {0}”或“alt + {0}”或“cmd + {0}” setting.about.shortcuts.menuNav=主页面 setting.about.shortcuts.menuNav.value=使用“Ctrl”或“Alt”或“cmd” + 数字键“1-9”来切换不同的主页面 setting.about.shortcuts.close=关闭 Haveno setting.about.shortcuts.close.value=“Ctrl + {0}”或“cmd + {0}”或“Ctrl + {1}”或“cmd + {1}” setting.about.shortcuts.closePopup=关闭弹窗以及对话框 setting.about.shortcuts.closePopup.value=‘释放’ 键 setting.about.shortcuts.chatSendMsg=发送信息到交易伙伴 setting.about.shortcuts.chatSendMsg.value=“Ctrl + ENTER”或“alt + ENTER”或“cmd + ENTER” setting.about.shortcuts.openDispute=创建纠纷 setting.about.shortcuts.openDispute.value=选择未完成交易并点击:{0} setting.about.shortcuts.walletDetails=打开钱包详情窗口 setting.about.shortcuts.openEmergencyXmrWalletTool=打开应急 XMR 钱包工具 setting.about.shortcuts.showTorLogs=在 DEBUG 与 WARN 之间切换 Tor 日志等级 setting.about.shortcuts.manualPayoutTxWindow=打开窗口手动支付双重验证存款交易 setting.about.shortcuts.removeStuckTrade=Open popup to move failed trade to open trades tab again setting.about.shortcuts.removeStuckTrade.value=Select failed trade and press: {0} setting.about.shortcuts.registerArbitrator=注册仲裁员(仅限调解员/仲裁员) setting.about.shortcuts.registerArbitrator.value=切换至账户页面并按下:{0} setting.about.shortcuts.registerMediator=注册调解员(仅限调解员/仲裁员) setting.about.shortcuts.registerMediator.value=切换至账户页面并按下:{0} setting.about.shortcuts.openSignPaymentAccountsWindow=打开账龄验证窗口(仅限仲裁员) setting.about.shortcuts.openSignPaymentAccountsWindow.value=切换至仲裁页面并按下:{0} setting.about.shortcuts.sendAlertMsg=发送警报或更新消息(需要权限) setting.about.shortcuts.sendFilter=设置过滤器(需要权限) setting.about.shortcuts.sendPrivateNotification=发送私人通知到对等点(需要权限) setting.about.shortcuts.sendPrivateNotification.value=点击交易伙伴头像并按下:{0} 以显示更多信息 setting.info.headline=新 XMR 自动确认功能 setting.info.msg=当你完成 XMR/XMR 交易时,您可以使用自动确认功能来验证是否向您的钱包中发送了正确数量的 XMR,以便 Haveno 可以自动将交易标记为完成,从而使每个人都可以更快地进行交易。\n\n自动确认使用 XMR 发送方提供的交易密钥在至少 2 个 XMR 区块浏览器节点上检查 XMR 交易。在默认情况下,Haveno 使用由 Haveno 贡献者运行的区块浏览器节点,但是我们建议运行您自己的 XMR 区块浏览器节点以最大程度地保护隐私和安全。\n\n您还可以在``设置'中将每笔交易的最大 XMR 数量设置为自动确认以及所需确认的数量。\n\n在 Haveno Wiki 上查看更多详细信息(包括如何设置自己的区块浏览器节点):https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades #################################################################### # Account #################################################################### account.tab.mediatorRegistration=调解员注册 account.tab.refundAgentRegistration=退款助理注册 account.tab.signing=验证中 account.info.headline=欢迎来到 Haveno 账户 account.info.msg=在这里你可以设置交易账户的法定货币及数字货币,选择仲裁员和备份你的钱包及账户数据。\n\n当你开始运行 Haveno 就已经创建了一个空的比特币钱包。\n\n我们建议你在充值之前写下你比特币钱包的还原密钥(在左边的列表)和考虑添加密码。在“资金”选项中管理比特币存入和提现。\n\n隐私 & 安全:\nHaveno 是一个去中心化的交易所 – 意味着您的所有数据都保存在您的电脑上,没有服务器,我们无法访问您的个人信息,您的资金,甚至您的 IP 地址。如银行账号、数字货币、比特币地址等数据只分享给与您交易的人,以实现您发起的交易(如果有争议,仲裁员将会看到您的交易数据)。 account.menu.paymentAccount=法定货币账户 account.menu.altCoinsAccountView=数字货币账户 account.menu.password=钱包密码 account.menu.seedWords=钱包密钥 account.menu.walletInfo=钱包信息 account.menu.backup=备份 account.menu.notifications=通知 account.menu.walletInfo.balance.headLine=钱包余额 account.menu.walletInfo.balance.info=这里包括内部钱包余额包括未确认交易。\n对于 XMR,下方显示的内部钱包的余额将会是窗口右上方的“可用”与“保留”余额的总和。 account.menu.walletInfo.xpub.headLine=监控密钥(xpub keys) account.menu.walletInfo.walletSelector={0} {1} 钱包 account.menu.walletInfo.path.headLine=HD 密钥链路径 account.menu.walletInfo.path.info=如果您导入其他钱包(例如 Electrum)的种子词,你需要去确认路径。这个操作只能用于你失去 Haveno 钱包和数据目录的控制的紧急情况。\n请记住使用非 Haveno 钱包的资金可能会打乱 Haveno 内部与之相连的钱包数据结构,这可能导致交易失败。\n\n请不要将 BSQ 发送至非 Haveno 钱包,因为这可能让您的 BSQ 交易记录失效以及损失 BSQ. account.menu.walletInfo.openDetails=显示原始钱包详情与私钥 ## TODO should we rename the following to a gereric name? account.arbitratorRegistration.pubKey=公钥 account.arbitratorRegistration.register=注册 account.arbitratorRegistration.registration={0} 注册 account.arbitratorRegistration.revoke=撤销 account.arbitratorRegistration.info.msg=请注意,撤销后需要保留15天,因为可能有交易正在以你作为 {0}。最大允许的交易期限为8天,纠纷过程最多可能需要7天。 account.arbitratorRegistration.warn.min1Language=您需要设置至少1种语言。\n我们已经为您添加了默认语言。 account.arbitratorRegistration.removedSuccess=您已从 Haveno 网络成功删除仲裁员注册信息。 account.arbitratorRegistration.removedFailed=无法删除仲裁员。{0} account.arbitratorRegistration.registerSuccess=您已从 Haveno 网络成功注册您的仲裁员。 account.arbitratorRegistration.registerFailed=无法注册仲裁员。{0} account.crypto.yourCryptoAccounts=您的数字货币账户 account.crypto.popup.wallet.msg=请确保您按照 {1} 网页上所述使用 {0} 钱包的要求。\n使用集中式交易所的钱包,您无法控制密钥或使用不兼容的钱包软件,可能会导致交易资金的流失!\n调解员或仲裁员不是 {2} 专家,在这种情况下不能帮助。 account.crypto.popup.wallet.confirm=我了解并确定我知道我需要哪种钱包。 # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=在 Haveno 上交易 UPX 需要您了解并满足以下要求:\n\n要发送 UPX ,您需要使用官方的 UPXmA GUI 钱包或启用 store-tx-info 标志的 UPXmA CLI 钱包(在新版本中是默认的)。请确保您可以访问Tx密钥,因为在纠纷状态时需要。\nmonero-wallet-cli(使用get_Tx_key命令)\nmonero-wallet-gui:在高级>证明/检查页面。\n\n在普通的区块链浏览器中,这种交易是不可验证的。\n\n如有纠纷,你须向仲裁员提供下列资料:\n \n- Tx私钥\n- 交易哈希\n- 接收者的公开地址\n\n如未能提供上述资料,或使用不兼容的钱包,将会导致纠纷败诉。如果发生纠纷,UPX 发送方负责向仲裁员提供 UPX 转账的验证。\n\n不需要支付 ID,只需要普通的公共地址。\n \n如果您对该流程不确定,请访问 UPXmA Discord 频道(https://discord.gg/vhdNSrV)或 Telegram 交流群(https://t.me/uplexaOfficial)了解更多信息。\n\n # suppress inspection "UnusedProperty" account.crypto.popup.arq.msg=在 Haveno 上交易 ARQ 需要您了解并满足以下要求:\n\n要发送 ARQ ,您需要使用官方的 ArQmA GUI 钱包或启用 store-tx-info 标志的 ArQmA CLI 钱包(在新版本中是默认的)。请确保您可以访问Tx密钥,因为在纠纷状态时需要。\nmonero-wallet-cli(使用get_Tx_key命令)\nmonero-wallet-gui:在高级>证明/检查页面。\n\n在普通的区块链浏览器中,这种交易是不可验证的。\n\n如有纠纷,你须向调解员或仲裁员提供下列资料:\n\n- Tx私钥\n- 交易哈希\n- 接收者的公开地址\n\n如未能提供上述资料,或使用不兼容的钱包,将会导致纠纷败诉。如果发生纠纷,ARQ 发送方负责向调解员或仲裁员提供 ARQ 转账的验证。\n\n不需要交易 ID,只需要普通的公共地址。\n\n如果您对该流程不确定,请访问 ArQmA Discord 频道(https://discord.gg/s9BQpJT)或 ArQmA 论坛(https://labs.arqma.com)了解更多信息。 # suppress inspection "UnusedProperty" account.crypto.popup.xmr.msg=在 Haveno 上交易 XMR 需要你理解并满足以下要求。\n\n如果您出售 XMR,当您在纠纷中您必须要提供下列信息给调解员或仲裁员:\n- 交易密钥(Tx 公钥,Tx密钥,Tx私钥)\n- 交易 ID(Tx ID 或 Tx 哈希)\n- 交易目标地址(接收者地址)\n\n在 wiki 中查看更多关于 Monero 钱包的信息:\nhttps://haveno.exchange/wiki/Trading_Monero#Proving_payments\n\n如未能提供要求的交易数据将在纠纷中直接判负\n\n还要注意,Haveno 现在提供了自动确认 XMR 交易的功能,以使交易更快,但是您需要在设置中启用它。\n\n有关自动确认功能的更多信息,请参见 Wiki:\nhttps://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades # suppress inspection "UnusedProperty" account.crypto.popup.msr.msg=区块链浏览器在 Haveno 上交易 XMR 需要您了解并满足以下要求:\n\n发送MSR时,您需要使用官方的 Masari GUI 钱包、启用store-tx-info标记的Masari CLI钱包(默认启用)或Masari 网页钱包(https://wallet.getmasari.org)。请确保您可以访问的 tx 密钥,因为如果发生纠纷这是需要的。\nmonero-wallet-cli(使用get_Tx_key命令)\nmonero-wallet-gui:在高级>证明/检查页面。\n\nMasari 网页钱包(前往 帐户->交易历史和查看您发送的交易细节)\n\n验证可以在钱包中完成。\nmonero-wallet-cli:使用命令(check_tx_key)。\nmonero-wallet-gui:在高级>证明/检查页面\n验证可以在区块浏览器中完成\n打开区块浏览器(https://explorer.getmasari.org),使用搜索栏查找您的事务哈希。\n一旦找到交易,滚动到底部的“证明发送”区域,并填写所需的详细信息。\n如有纠纷,你须向调解员或仲裁员提供下列资料:\n- Tx私钥\n- 交易哈希\n- 接收者的公开地址\n\n不需要交易 ID,只需要正常的公共地址。\n如未能提供上述资料,或使用不兼容的钱包,将会导致纠纷败诉。如果发生纠纷,XMR 发送方负责向调解员或仲裁员提供 XMR 转账的验证。\n\n如果您对该流程不确定,请访问官方的 Masari Discord(https://discord.gg/sMCwMqs)上寻求帮助。 # suppress inspection "UnusedProperty" account.crypto.popup.blur.msg=在 Haveno 上交易 BLUR 需要你了解并满足以下要求:\n\n要发送匿名信息你必须使用匿名网络 CLI 或 GUI 钱包。\n如果您正在使用 CLI 钱包,在传输发送后将显示交易哈希(tx ID)。您必须保存此信息。在发送传输之后,您必须立即使用“get_tx_key”命令来检索交易私钥。如果未能执行此步骤,以后可能无法检索密钥。\n\n如果您使用 Blur Network GUI 钱包,可以在“历史”选项卡中方便地找到交易私钥和交易 ID。发送后立即定位感兴趣的交易。单击包含交易的框的右下角的“?”符号。您必须保存此信息。\n\n如果仲裁是必要的,您必须向调解员或仲裁员提供以下信息:1.)交易ID,2.)交易私钥,3.)收件人地址。调解或仲裁程序将使用 BLUR 事务查看器(https://blur.cash/#tx-viewer)验证 BLUR 转账。\n\n未能向调解员或仲裁员提供必要的信息将导致败诉。在所有争议的情况下,匿名发送方承担100%的责任来向调解员或仲裁员核实交易。\n\n如果你不了解这些要求,不要在 Haveno 上交易。首先,在 Blur Network Discord 中寻求帮助(https://discord.gg/dMWaqVW)。 # suppress inspection "UnusedProperty" account.crypto.popup.solo.msg=在 Haveno 上交易 Solo 需要您了解并满足以下要求:\n\n要发送 Solo,您必须使用 Solo CLI 网络钱包版本 5.1.3 或更高。\n\n如果您使用的是CLI钱包,则在发送交易之后,将显示交易ID。您必须保存此信息。在发送交易之后,您必须立即使用'get_tx_key'命令来检索交易密钥。如果未能执行此步骤,则以后可能无法检索密钥。\n\n如果仲裁是必要的,您必须向调解员或仲裁员提供以下信息:1)交易 ID,、2)交易密钥,3)收件人的地址。调解员或仲裁员将使用 Solo 区块资源管理器(https://explorer.Solo.org)搜索交易然后使用“发送证明”功能(https://explorer.minesolo.com/)\n\n未能向调解员或仲裁员提供必要的信息将导致败诉。在所有发生争议的情况下,在向调解员或仲裁员核实交易时,QWC 的发送方承担 100% 的责任。\n\n如果你不理解这些要求,不要在 Haveno 上交易。首先,在 Solo Discord 中寻求帮助(https://discord.minesolo.com/)。\n\n # suppress inspection "UnusedProperty" account.crypto.popup.cash2.msg=在 Haveno 上交易 CASH2 需要您了解并满足以下要求:\n\n要发送 CASH2,您必须使用 CASH2 钱包版本 3 或更高。\n\n在发送交易之后,将显示交易ID。您必须保存此信息。在发送交易之后,必须立即在 simplewallet 中使用命令“getTxKey”来检索交易密钥。\n\n如果仲裁是必要的,您必须向调解员或仲裁员提供以下信息:1)交易 ID,2)交易密钥,3)收件人的 CASH2 地址。调解员或仲裁员将使用 CASH2 区块资源管理器(https://blocks.cash2.org)验证 CASH2 转账。\n\n未能向调解员或仲裁员提供必要的信息将导致败诉。在所有发生争议的情况下,在向调解员或仲裁员核实交易时,CASH2 的发送方承担 100% 的责任。\n\n如果你不理解这些要求,不要在 Haveno 上交易。首先,在 Cash2 Discord 中寻求帮助(https://discord.gg/FGfXAYN)。 # suppress inspection "UnusedProperty" account.crypto.popup.qwertycoin.msg=在 Haveno 上交易 Qwertycoin 需要您了解并满足以下要求:\n\n要发送 Qwertycoin,您必须使用 Qwertycoin 钱包版本 5.1.3 或更高。\n\n在发送交易之后,将显示交易ID。您必须保存此信息。在发送交易之后,必须立即在 simplewallet 中使用命令“get_Tx_Key”来检索交易密钥。\n\n如果仲裁是必要的,您必须向调解员或仲裁员提供以下信息::1)交易 ID,、2)交易密钥,3)收件人的 QWC 地址。调解员或仲裁员将使用 QWC 区块资源管理器(https://explorer.qwertycoin.org)验证 QWC 转账。\n\n未能向调解员或仲裁员提供必要的信息将导致败诉。在所有发生争议的情况下,在向调解员或仲裁员核实交易时,QWC 的发送方承担 100% 的责任。\n\n如果你不理解这些要求,不要在 Haveno 上交易。首先,在 QWC Discord 中寻求帮助(https://discord.gg/rUkfnpC)。 # suppress inspection "UnusedProperty" account.crypto.popup.drgl.msg=在 Haveno 上交易 Dragonglass 需要您了解并满足以下要求:\n\n由于 Dragonglass 提供了隐私保护,所以交易不能在公共区块链上验证。如果需要,您可以通过使用您的 TXN-Private-Key 来证明您的付款。\nTXN-Private 密匙是自动生成的一次性密匙,用于只能从 DRGL 钱包中访问的每个交易。\n要么通过 DRGL-wallet GUI(内部交易细节对话框),要么通过 Dragonglass CLI simplewallet(使用命令“get_tx_key”)。\n\n两者都需要 DRGL 版本的“Oathkeeper”或更高版本。\n\n如有争议,你必须向调解员或仲裁员提供下列资料:\n\n- txn-Privite-ket\n- 交易哈希 \n- 接收者的公开地址\n\n付款验证可以使用上面的数据作为输入(http://drgl.info/#check_txn)。\n\n如未能提供上述资料,或使用不兼容的钱包,将会导致纠纷败诉。Dragonglass 发送方负责在发生争议时向调解员或仲裁员提供 DRGL 转账的验证。不需要使用付款 ID。\n\n如果您对这个过程的任何部分都不确定,请访问(http://discord.drgl.info)上的 Dragonglass 寻求帮助。 # suppress inspection "UnusedProperty" account.crypto.popup.ZEC.msg=当使用 Zcash 时,您只能使用透明地址(以 t 开头),而不能使用 z 地址(私有),因为调解员或仲裁员无法使用 z 地址验证交易。 # suppress inspection "UnusedProperty" account.crypto.popup.XZC.msg=在使用 Zcoin 时,您只能使用透明的(可跟踪的)地址,而不能使用不可跟踪的地址,因为调解员或仲裁员无法在区块资源管理器中使用不可跟踪的地址验证交易。 # suppress inspection "UnusedProperty" account.crypto.popup.grin.msg=GRIN 需要发送方和接收方之间的交互过程来创建交易。请确保遵循 GRIN 项目网页中的说明,以可靠地发送和接收 GRIN(接收方需要在线,或至少在一定时间内在线)。\n \nHaveno 只支持 Grinbox(Wallet713)钱包 URL 格式。\n\nGRIN 发送者需要提供他们已成功发送 GRIN 的证明。如果钱包不能提供证明,一个潜在的纠纷将被解决,有利于露齿微笑的接受者。请确保您使用了最新的支持交易证明的 Grinbox 软件,并且您了解传输和接收 GRIN 的过程以及如何创建证明。\n请参阅 https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only,以获得关于 Grinbox 证明工具的更多信息。\n # suppress inspection "UnusedProperty" account.crypto.popup.beam.msg=BEAM 需要发送方和接收方之间的交互过程来创建交易。\n\n\n确保遵循 BEAM 项目网页的指示可靠地发送和接收 BEAM(接收方需要在线,或者至少在一定的时间范围内在线)。\n\nBEAM 发送者需要提供他们成功发送 BEAM 的证明。一定要使用钱包软件,可以产生这样的证明。如果钱包不能提供证据,一个潜在的纠纷将得到解决,有利于 BEAM 接收者。 # suppress inspection "UnusedProperty" account.crypto.popup.pars.msg=在 Haveno 上交易 ParsiCoin 需要您了解并满足以下要求:\n\n要发送 PARS ,您必须使用官方 ParsiCoin Wallet 版本 3.0.0 或更高。\n\n您可以在 GUI 钱包(ParsiPay)的交易部分检查您的交易哈希和交易键,您需要右键单击“交易”,然后单击“显示详情”。\n\n如果仲裁是 100% 必要的,您必须向调解员或仲裁员提供以下内容:1)交易哈希,2)交易密钥,以及3)接收方的 PARS 地址。调解员或仲裁员将使用 ParsiCoin 区块链浏览器 (http://explorer.parsicoin.net/#check_payment)验证 PARS 传输。\n\n如果你不了解这些要求,不要在 Haveno 上交易。首先,在 ParsiCoin Discord 寻求帮助(https://discord.gg/c7qmFNh)。 # suppress inspection "UnusedProperty" account.crypto.popup.blk-burnt.msg=要交易烧毁的货币,你需要知道以下几点:\n\n烧毁的货币是不能花的。要在 Haveno 上交易它们,输出脚本需要采用以下形式:OP_RETURN OP_PUSHDATA,后跟相关的数据字节,这些字节经过十六进制编码后构成地址。例如,地址为666f6f(在UTF-8中的"foo")的烧毁的货币将有以下脚本:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\n要创建烧毁的货币,您可以使用“烧毁”RPC命令,它在一些钱包可用。\n\n对于可能的情况,可以查看 https://ibo.laboratorium.ee\n\n因为烧毁的货币是不能用的,所以不能重新出售。“出售”烧毁的货币意味着焚烧初始的货币(与目的地地址相关联的数据)。\n\n如果发生争议,BLK 卖方需要提供交易哈希。 # suppress inspection "UnusedProperty" account.crypto.popup.liquidmonero.msg=在 Haveno 上交易 L-XMR 你必须理解下述条款:\n\n当你在 Haveno 上接受 L-XMR 交易时,你不能使用手机 Blockstream Green Wallet 或者是一个托管/交易钱包。你必须只接收 L-XMR 到 Liquid Elements Core 钱包,或另一个 L-XMR 钱包且允许你获得匿名的 L-XMR 地址以及密钥。\n\n在需要进行调解的情况下,或者如果发生了交易纠纷,您必须将接收 L-XMR地址的安全密钥披露给 Haveno 调解员或退款代理,以便他们能够在他们自己的 Elements Core 全节点上验证您的匿名交易的细节。\n\n如果你不了解或了解这些要求,不要在 Haveno 上交易 L-XMR。 account.traditional.yourTraditionalAccounts=您的法定货币账户 account.backup.title=备份钱包 account.backup.location=备份路径 account.backup.selectLocation=选择备份路径 account.backup.backupNow=立即备份(备份没有被加密!) account.backup.appDir=应用程序数据目录 account.backup.openDirectory=打开目录 account.backup.openLogFile=打开日志文件 account.backup.success=备份成功保存在:\n{0} account.backup.directoryNotAccessible=您没有访问选择的目录的权限。 {0} account.password.removePw.button=移除密码 account.password.removePw.headline=移除钱包的密码保护 account.password.setPw.button=设置密码 account.password.setPw.headline=设置钱包的密码保护 account.password.info=启用密码保护后,在应用程序启动时、从您的钱包提取门罗币时以及显示种子词时,您将需要输入密码。 account.seed.backup.title=备份您的钱包种子词 account.seed.info=请记下钱包种子词和日期。您随时可以使用种子词和日期来恢复您的钱包。\n\n您应该将种子词写在一张纸上。不要将它们保存在电脑上。\n\n请注意,种子词并不能替代备份。\n您需要在“账户/备份”屏幕上创建整个应用程序目录的备份,以便恢复应用程序的状态和数据。 account.seed.backup.warning=请注意,种子词并不是备份的替代品。\n您需要在“账户/备份”屏幕上创建整个应用程序目录的备份,以便还原应用程序的状态和数据。 account.seed.warn.noPw.msg=您还没有设置一个可以保护还原密钥显示的钱包密码。\n\n要显示还原密钥吗? account.seed.warn.noPw.yes=是的,不要再问我 account.seed.enterPw=输入密码查看还原密钥 account.seed.restore.info=请在应用还原密钥还原之前进行备份。请注意,钱包还原仅用于紧急情况,可能会导致内部钱包数据库出现问题。\n这不是应用备份的方法!请使用应用程序数据目录中的备份来恢复以前的应用程序状态。\n恢复后,应用程序将自动关闭。重新启动应用程序后,它将重新与比特币网络同步。这可能需要一段时间,并且会消耗大量CPU,特别是在钱包较旧且有很多交易的情况下。请避免中断该进程,否则可能需要再次删除 SPV 链文件或重复还原过程。 account.seed.restore.ok=好的,立即执行回复并且关闭 Haveno #################################################################### # Mobile notifications #################################################################### account.notifications.setup.title=安装 account.notifications.download.label=下载手机应用 account.notifications.waitingForWebCam=等待网络摄像头... account.notifications.webCamWindow.headline=用手机扫描二维码 account.notifications.webcam.label=使用网络摄像头 account.notifications.webcam.button=扫描二维码 account.notifications.noWebcam.button=我没有网络摄像头 account.notifications.erase.label=在手机上清除通知 account.notifications.erase.title=清除通知 account.notifications.email.label=验证码 account.notifications.email.prompt=输入您通过电子邮件收到的验证码 account.notifications.settings.title=设置 account.notifications.useSound.label=在手机上播放提示声音 account.notifications.trade.label=接收交易信息 account.notifications.market.label=接收报价提醒 account.notifications.price.label=接收价格提醒 account.notifications.priceAlert.title=价格提醒 account.notifications.priceAlert.high.label=提醒条件:当 XMR 价格高于 account.notifications.priceAlert.low.label=提醒条件:当 XMR 价格低于 account.notifications.priceAlert.setButton=设置价格提醒 account.notifications.priceAlert.removeButton=取消价格提醒 account.notifications.trade.message.title=交易状态已变更 account.notifications.trade.message.msg.conf=ID 为 {0} 的交易的存款交易已被确认。请打开您的 Haveno 应用程序并开始付款。 account.notifications.trade.message.msg.started=XMR 买家已经开始支付 ID 为 {0} 的交易。 account.notifications.trade.message.msg.completed=ID 为 {0} 的交易已完成。 account.notifications.offer.message.title=您的报价已被接受 account.notifications.offer.message.msg=您的 ID 为 {0} 的报价已被接受 account.notifications.dispute.message.title=新的纠纷消息 account.notifications.dispute.message.msg=您收到了一个 ID 为 {0} 的交易纠纷消息 account.notifications.marketAlert.title=报价提醒 account.notifications.marketAlert.selectPaymentAccount=提供匹配的付款帐户 account.notifications.marketAlert.offerType.label=我感兴趣的报价类型 account.notifications.marketAlert.offerType.buy=买入报价(我想要出售 XMR ) account.notifications.marketAlert.offerType.sell=卖出报价(我想要购买 XMR ) account.notifications.marketAlert.trigger=报价距离(%) account.notifications.marketAlert.trigger.info=设置价格区间后,只有当满足(或超过)您的需求的报价发布时,您才会收到提醒。您想卖 XMR ,但你只能以当前市价的 2% 溢价出售。将此字段设置为 2% 将确保您只收到高于当前市场价格 2%(或更多)的报价的提醒。 account.notifications.marketAlert.trigger.prompt=与市场价格的百分比距离(例如 2.50%, -0.50% 等) account.notifications.marketAlert.addButton=添加报价提醒 account.notifications.marketAlert.manageAlertsButton=管理报价提醒 account.notifications.marketAlert.manageAlerts.title=管理报价提醒 account.notifications.marketAlert.manageAlerts.header.paymentAccount=支付账户 account.notifications.marketAlert.manageAlerts.header.trigger=触发价格 account.notifications.marketAlert.manageAlerts.header.offerType=报价类型 account.notifications.marketAlert.message.title=报价提醒 account.notifications.marketAlert.message.msg.below=低于 account.notifications.marketAlert.message.msg.above=高于 account.notifications.marketAlert.message.msg=价格为 {2}({3} {4}市场价)和支付方式为 {5} 的报价 {0} {1} 已发布到 Haveno 报价列表。\n报价ID: {6}。 account.notifications.priceAlert.message.title=价格提醒 {0} account.notifications.priceAlert.message.msg=您的价格提醒已被触发。当前 {0} 的价格为 {1} {2} account.notifications.noWebCamFound.warning=未找到网络摄像头。\n\n请使用电子邮件选项将代码和加密密钥从您的手机发送到 Haveno 应用程序。 account.notifications.priceAlert.warning.highPriceTooLow=较高的价格必须大于较低的价格。 account.notifications.priceAlert.warning.lowerPriceTooHigh=较低的价格必须低于较高的价格。 #################################################################### # Windows #################################################################### inputControlWindow.headline=Select inputs for transaction inputControlWindow.balanceLabel=可用余额 contractWindow.title=纠纷详情 contractWindow.dates=报价时间/交易时间 contractWindow.xmrAddresses=XMR 买家/XMR 卖家的比特币地址 contractWindow.onions=XMR 买家/XMR 卖家的网络地址 contractWindow.accountAge=XMR 买家/XMR 卖家的账龄 contractWindow.numDisputes=XMR 买家/XMR 卖家的纠纷编号 contractWindow.contractHash=合同哈希 displayAlertMessageWindow.headline=重要资料! displayAlertMessageWindow.update.headline=重要更新资料! displayAlertMessageWindow.update.download=下载: displayUpdateDownloadWindow.downloadedFiles=下载完成的文件: displayUpdateDownloadWindow.downloadingFile=正在下载:{0} displayUpdateDownloadWindow.verifiedSigs=验证验证: displayUpdateDownloadWindow.status.downloading=下载文件... displayUpdateDownloadWindow.status.verifying=验证验证中... displayUpdateDownloadWindow.button.label=下载安装程序并验证验证 displayUpdateDownloadWindow.button.downloadLater=稍后下载 displayUpdateDownloadWindow.button.ignoreDownload=忽略这个版本 displayUpdateDownloadWindow.headline=Haveno 有新的更新! displayUpdateDownloadWindow.download.failed.headline=下载失败 displayUpdateDownloadWindow.download.failed=下载失败。\n请到 https://haveno.io/downloads 下载并验证。 displayUpdateDownloadWindow.installer.failed=无法确定正确的安装程序。请通过 https://haveno.exchange/downloads 手动下载和验证。 displayUpdateDownloadWindow.verify.failed=验证失败。\n请到 https://haveno.io/downloads 手动下载和验证。 displayUpdateDownloadWindow.success=新版本成功下载并验证验证 。\n\n请打开下载目录,关闭应用程序并安装最新版本。 displayUpdateDownloadWindow.download.openDir=打开下载目录 disputeSummaryWindow.title=概要 disputeSummaryWindow.openDate=工单创建时间 disputeSummaryWindow.role=交易者的角色 disputeSummaryWindow.payout=交易金额支付 disputeSummaryWindow.payout.getsTradeAmount=XMR {0} 获得交易金额支付 disputeSummaryWindow.payout.getsAll=最大 XMR 支付数 {0} disputeSummaryWindow.payout.custom=自定义支付 disputeSummaryWindow.payoutAmount.buyer=买家支付金额 disputeSummaryWindow.payoutAmount.seller=卖家支付金额 disputeSummaryWindow.payoutAmount.invert=使用失败者作为发布者 disputeSummaryWindow.reason=纠纷的原因 disputeSummaryWindow.tradePeriodEnd=Trade period end disputeSummaryWindow.extraInfo=Extra information disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BUG=Bug # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.USABILITY=可用性 # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PROTOCOL_VIOLATION=违反协议 # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.NO_REPLY=不回复 # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SCAM=诈骗 # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OTHER=其他 # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BANK_PROBLEMS=银行 # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OPTION_TRADE=可选交易 # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Trader not responding # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=错误的发送者账号 # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PEER_WAS_LATE=交易伙伴已超时 # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=交易已稳定 disputeSummaryWindow.summaryNotes=总结说明 disputeSummaryWindow.addSummaryNotes=添加总结说明 disputeSummaryWindow.close.button=关闭话题 # Do no change any line break or order of tokens as the structure is used for signature verification # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.msg=工单已关闭{0}\n{1} 节点地址:{12}\n\n总结:\n交易 ID:{3}\n货币:{4}\n交易金额:{5}\nXMR 买家支付金额:{6}\nXMR 卖家支付金额:{7}\n\n纠纷原因:{8}\n\n总结:\n{9}\n # Do no change any line break or order of tokens as the structure is used for signature verification disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3} disputeSummaryWindow.close.nextStepsForMediation=\n\n下一个步骤:\n打开未完成交易,接受或拒绝建议的调解员的建议 disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\n下一个步骤:\n不需要您采取进一步的行动。如果仲裁员做出了对你有利的裁决,你将在 资金/交易 页中看到“仲裁退款”交易 disputeSummaryWindow.close.closePeer=你也需要关闭交易对象的话题! disputeSummaryWindow.close.txDetails.headline=发布交易退款 # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.buyer=买方收到{0}在地址:{1} # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=卖方收到{0}在地址:{1} disputeSummaryWindow.close.txDetails=费用:{0}\n{1}{2}交易费:{3}\n\n您确定要发布此交易吗? disputeSummaryWindow.close.noPayout.headline=未支付关闭 disputeSummaryWindow.close.noPayout.text=你想要在未作支付的情况下关闭吗? emptyWalletWindow.headline={0} 钱包急救工具 emptyWalletWindow.info=请在紧急情况下使用,如果您无法从 UI 中访问您的资金。\n\n请注意,使用此工具时,所有未结报价将自动关闭。\n\n在使用此工具之前,请备份您的数据目录。您可以在“帐户/备份”中执行此操作。\n\n请报告我们您的问题,并在 Github 或 Haveno 论坛上提交错误报告,以便我们可以调查导致问题的原因。 emptyWalletWindow.balance=您的可用钱包余额 emptyWalletWindow.address=输入您的目标地址 emptyWalletWindow.button=发送全部资金 emptyWalletWindow.openOffers.warn=您有已发布的报价,如果您清空钱包将被删除。\n你确定要清空你的钱包吗? emptyWalletWindow.openOffers.yes=是的,我确定 emptyWalletWindow.sent.success=您的钱包的余额已成功转移。 enterPrivKeyWindow.headline=输入密钥进行注册 filterWindow.headline=编辑筛选列表 filterWindow.offers=筛选报价(用逗号“,”隔开) filterWindow.onions=Banned from trading addresses (comma sep.) filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.) filterWindow.accounts=筛选交易账户数据:\n格式:逗号分割的 [付款方式ID|数据字段|值] filterWindow.bannedCurrencies=筛选货币代码(用逗号“,”隔开) filterWindow.bannedPaymentMethods=筛选支付方式 ID(用逗号“,”隔开) filterWindow.bannedAccountWitnessSignerPubKeys=已过滤的帐户证人签名者公钥(逗号分隔十六进制公钥) filterWindow.bannedPrivilegedDevPubKeys=已过滤的特权开发者公钥(逗号分隔十六进制公钥) filterWindow.arbitrators=筛选后的仲裁人(用逗号“,”隔开的洋葱地址) filterWindow.mediators=筛选后的调解员(用逗号“,”隔开的洋葱地址) filterWindow.refundAgents=筛选后的退款助理(用逗号“,”隔开的洋葱地址) filterWindow.seedNode=筛选后的种子节点(用逗号“,”隔开的洋葱地址) filterWindow.priceRelayNode=筛选后的价格中继节点(用逗号“,”隔开的洋葱地址) filterWindow.xmrNode=筛选后的比特币节点(用逗号“,”隔开的地址+端口) filterWindow.preventPublicXmrNetwork=禁止使用公共比特币网络 filterWindow.disableAutoConf=禁用自动确认 filterWindow.autoConfExplorers=已过滤自动确认浏览器(逗号分隔地址) filterWindow.disableTradeBelowVersion=交易最低所需要的版本 filterWindow.add=添加筛选 filterWindow.remove=移除筛选 filterWindow.xmrFeeReceiverAddresses=比特币手续费接收地址 filterWindow.disableApi=Disable API filterWindow.disableMempoolValidation=Disable Mempool Validation offerDetailsWindow.minXmrAmount=最小 XMR 数量 offerDetailsWindow.min=(最小 {0}) offerDetailsWindow.distance=(与市场价格的差距:{0}) offerDetailsWindow.myTradingAccount=我的交易账户 offerDetailsWindow.offererBankId=(卖家的银行 ID/BIC/SWIFT) offerDetailsWindow.offerersBankName=(卖家的银行名称): offerDetailsWindow.bankId=银行 ID(例如 BIC 或者 SWIFT ): offerDetailsWindow.countryBank=卖家银行所在国家或地区 offerDetailsWindow.commitment=承诺 offerDetailsWindow.agree=我同意 offerDetailsWindow.tac=条款和条件 offerDetailsWindow.confirm.maker.buy=确认:创建以 {0} 买入 XMR 的报价 offerDetailsWindow.confirm.maker.sell=确认:创建以 {0} 卖出 XMR 的报价 offerDetailsWindow.confirm.taker.buy=确认:接受以 {0} 买入 XMR 的报价 offerDetailsWindow.confirm.taker.sell=确认:接受以 {0} 卖出 XMR 的报价 offerDetailsWindow.creationDate=创建时间 offerDetailsWindow.makersOnion=卖家的匿名地址 offerDetailsWindow.challenge=提供密码 offerDetailsWindow.challenge.copy=复制助记词以与您的交易对手共享 qRCodeWindow.headline=二维码 qRCodeWindow.msg=请使用二维码从外部钱包充值至 Haveno 钱包 qRCodeWindow.request=付款请求:\n{0} selectDepositTxWindow.headline=选择纠纷的存款交易 selectDepositTxWindow.msg=存款交易未存储在交易中。\n请从您的钱包中选择一个现有的多重验证交易,这是在失败的交易中使用的存款交易。\n\n您可以通过打开交易详细信息窗口(点击列表中的交易 ID)并按照交易费用支付交易输出到您看到多重验证存款交易的下一个交易(地址从3开始),找到正确的交易。 该交易 ID 应在此处列出的列表中显示。 一旦您找到正确的交易,请在此处选择该交易并继续\n\n抱歉给您带来不便,但是错误的情况应该非常罕见,将来我们会尝试找到更好的解决方法。 selectDepositTxWindow.select=选择存款交易 sendAlertMessageWindow.headline=发送全球通知 sendAlertMessageWindow.alertMsg=提醒消息 sendAlertMessageWindow.enterMsg=输入消息: sendAlertMessageWindow.isSoftwareUpdate=Software download notification sendAlertMessageWindow.isUpdate=Is full release sendAlertMessageWindow.isPreRelease=Is pre-release sendAlertMessageWindow.version=新版本号 sendAlertMessageWindow.send=发送通知 sendAlertMessageWindow.remove=移除通知 sendPrivateNotificationWindow.headline=发送私信 sendPrivateNotificationWindow.privateNotification=私人通知 sendPrivateNotificationWindow.enterNotification=输入通知 sendPrivateNotificationWindow.send=发送私人通知 showWalletDataWindow.walletData=钱包数据 showWalletDataWindow.includePrivKeys=包含私钥 setXMRTxKeyWindow.headline=证明已发送 XMR setXMRTxKeyWindow.note=在下面添加 tx 信息可以更快的自动确认交易。更多信息::https://haveno.exchange/wiki/Trading_Monero setXMRTxKeyWindow.txHash=交易 ID (可选) setXMRTxKeyWindow.txKey=交易密钥 (可选) # We do not translate the tac because of the legal nature. We would need translations checked by lawyers # in each language which is too expensive atm. tacWindow.headline=用户协议 tacWindow.agree=我同意 tacWindow.disagree=我不同意并退出 tacWindow.arbitrationSystem=纠纷解决方案 tradeDetailsWindow.headline=交易 tradeDetailsWindow.disputedPayoutTxId=纠纷支付交易 ID tradeDetailsWindow.tradeDate=交易时间 tradeDetailsWindow.txFee=矿工手续费 tradeDetailsWindow.tradePeersOnion=交易伙伴匿名地址 tradeDetailsWindow.tradePeersPubKeyHash=交易伙伴公钥哈希值 tradeDetailsWindow.tradeState=交易状态 tradeDetailsWindow.agentAddresses=仲裁员/调解员 tradeDetailsWindow.detailData=详情数据 txDetailsWindow.headline=Transaction Details txDetailsWindow.xmr.noteSent=您已发送 XMR。 txDetailsWindow.xmr.noteReceived=你已收到XMR。 txDetailsWindow.sentTo=发送至 txDetailsWindow.receivedWith=已收到,带有 txDetailsWindow.txId=TxId closedTradesSummaryWindow.headline=Trade history summary closedTradesSummaryWindow.totalAmount.title=Total trade amount closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) closedTradesSummaryWindow.totalTradeFeeInXmr.title=Sum of all trade fees paid in XMR closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} of total trade amount) walletPasswordWindow.headline=输入密码解锁 xmrConnectionError.headline=Monero 连接错误 xmrConnectionError.providedNodes=连接到所提供的 Monero 节点时出错。\n\n是否要使用下一个最佳可用的 Monero 节点? xmrConnectionError.customNodes=连接到您的自定义 Monero 节点时出错。\n\n是否要使用下一个最佳可用的 Monero 节点? xmrConnectionError.localNode=Haveno 之前已连接到本地 Monero 节点,但目前已无法连接。\n\n请确保您的本地节点正在运行且已完全同步,或选择其他选项以继续。 xmrConnectionError.localNode.start=启动本地节点 xmrConnectionError.localNode.start.error=启动本地节点时出错 xmrConnectionError.localNode.fallback=连接到下一个最佳节点 torNetworkSettingWindow.header=Tor 网络设置 torNetworkSettingWindow.noBridges=不使用网桥 torNetworkSettingWindow.providedBridges=连接到提供的网桥 torNetworkSettingWindow.customBridges=输入自定义网桥 torNetworkSettingWindow.transportType=传输类型 torNetworkSettingWindow.obfs3=obfs3 torNetworkSettingWindow.obfs4=obfs4(推荐) torNetworkSettingWindow.meekAmazon=meek-amazon torNetworkSettingWindow.meekAzure=meek-azure torNetworkSettingWindow.enterBridge=输入一个或多个网桥中继节点(每行一个) torNetworkSettingWindow.enterBridgePrompt=输入地址:端口 torNetworkSettingWindow.restartInfo=您需要重新启动以应用更改 torNetworkSettingWindow.openTorWebPage=打开 Tor Project 网页 torNetworkSettingWindow.deleteFiles.header=连接问题? torNetworkSettingWindow.deleteFiles.info=如果您在启动时有重复的连接问题,删除过期的 Tor 文件可能会有所帮助。如果要尝试修复,请单击下面的按钮,然后重新启动。 torNetworkSettingWindow.deleteFiles.button=删除过期的 Tor 文件并关闭 torNetworkSettingWindow.deleteFiles.progress=关闭正在运行中的 Tor torNetworkSettingWindow.deleteFiles.success=过期的 Tor 文件被成功删除。请重新启动。 torNetworkSettingWindow.bridges.header=Tor 网络被屏蔽? torNetworkSettingWindow.bridges.info=如果 Tor 被您的 Internet 提供商或您的国家或地区屏蔽,您可以尝试使用 Tor 网桥。\n \n访问 Tor 网页:https://bridges.torproject.org,了解关于网桥和可插拔传输的更多信息。 feeOptionWindow.headline=选择货币支付交易手续费 feeOptionWindow.info=您可以选择用 BSQ 或 XMR 支付交易费用。如果您选择 BSQ ,您会感谢这些交易手续费折扣。 feeOptionWindow.optionsLabel=选择货币支付交易手续费 feeOptionWindow.useXMR=使用 XMR feeOptionWindow.fee={0}(≈ {1}) feeOptionWindow.xmrFeeWithFiatAndPercentage={0} (≈ {1} / {2}) feeOptionWindow.xmrFeeWithPercentage={0} ({1}) #################################################################### # Popups #################################################################### popup.headline.notification=通知 popup.headline.instruction=请注意: popup.headline.attention=注意 popup.headline.backgroundInfo=背景资料 popup.headline.feedback=完成 popup.headline.confirmation=确定 popup.headline.information=资料 popup.headline.warning=警告 popup.headline.error=错误 popup.doNotShowAgain=不要再显示 popup.reportError.log=打开日志文件 popup.reportError.gitHub=报告至 Github issue tracker popup.reportError={0}\n\n为了帮助我们改进软件,请在 https://github.com/haveno-dex/haveno/issues 上打开一个新问题来报告这个 bug 。\n\n当您单击下面任意一个按钮时,上面的错误消息将被复制到剪贴板。\n\n如果您通过按下“打开日志文件”,保存一份副本,并将其附加到 bug 报告中,如果包含 haveno.log 文件,那么调试就会变得更容易。 popup.error.tryRestart=请尝试重启您的应用程序或者检查您的网络连接。 popup.error.takeOfferRequestFailed=当有人试图接受你的报价时发生了一个错误:\n{0} error.spvFileCorrupted=读取 SPV 链文件时发生错误。\n可能是 SPV 链文件被破坏了。\n\n错误消息:{0}\n\n要删除它并开始重新同步吗? error.deleteAddressEntryListFailed=无法删除 AddressEntryList 文件。\n \n错误:{0} error.closedTradeWithUnconfirmedDepositTx=交易 ID 为 {0} 的已关闭交易的保证金交易仍未确认。\n \n请在“设置/网络信息”进行 SPV 重新同步,以查看交易是否有效。 error.closedTradeWithNoDepositTx=交易 ID 为 {0} 的保证金交易已被确认。\n\n请重新启动应用程序来清理已关闭的交易列表。 popup.warning.walletNotInitialized=钱包至今未初始化 popup.warning.osxKeyLoggerWarning=由于 MacOS 10.14 及更高版本中的安全措施更加严格,因此启动 Java 应用程序(Haveno 使用Java)会在 MacOS 中引发弹出警告(``Haveno 希望从任何应用程序接收击键').\n\n为了避免该问题,请打开“ MacOS 设置”,然后转到“安全和隐私”->“隐私”->“输入监视”,然后从右侧列表中删除“ Haveno”。\n\n一旦解决了技术限制(所需的 Java 版本的 Java 打包程序尚未交付),Haveno将升级到新的 Java 版本,以避免该问题。 popup.warning.wrongVersion=您这台电脑上可能有错误的 Haveno 版本。\n您的电脑的架构是:{0}\n您安装的 Haveno 二进制文件是:{1}\n请关闭并重新安装正确的版本({2})。 popup.warning.incompatibleDB=我们检测到不兼容的数据库文件!\n\n那些数据库文件与我们当前的代码库不兼容:\n{0}\n\n我们对损坏的文件进行了备份,并将默认值应用于新的数据库版本。\n\n备份位于:\n{1}/db/backup_of_corrupted_data。\n\n请检查您是否安装了最新版本的 Haveno\n您可以下载:\nhttps://haveno.exchange/downloads\n\n请重新启动应用程序。 popup.warning.startupFailed.twoInstances=Haveno 已经在运行。 您不能运行两个 Haveno 实例。 popup.warning.tradePeriod.halfReached=您与 ID {0} 的交易已达到最长交易期的一半,且仍未完成。\n\n交易期结束于 {1}\n\n请查看“业务/未完成交易”的交易状态,以获取更多信息。 popup.warning.tradePeriod.ended=您与 ID {0} 的已达到最长交易期,且未完成。\n\n交易期结束于 {1}\n\n请查看“业务/未完成交易”的交易状态,以从调解员获取更多信息。 popup.warning.noTradingAccountSetup.headline=您还没有设置交易账户 popup.warning.noTradingAccountSetup.msg=您需要设置法定货币或数字货币账户才能创建报价。\n您要设置帐户吗? popup.warning.noArbitratorsAvailable=没有仲裁员可用。 popup.warning.noMediatorsAvailable=没有调解员可用。 popup.warning.notFullyConnected=您需要等到您完全连接到网络\n在启动时可能需要2分钟。 popup.warning.notSufficientConnectionsToXmrNetwork=你需要等待至少有{0}个与比特币网络的连接点。 popup.warning.downloadNotComplete=您需要等待,直到丢失的比特币区块被下载完毕。 popup.warning.walletNotSynced=Haveno 钱包尚未与最新的区块链高度同步。请等待钱包同步完成或检查您的连接。 popup.warning.removeOffer=您确定要移除该报价吗? popup.warning.tooLargePercentageValue=您不能设置100%或更大的百分比。 popup.warning.examplePercentageValue=请输入百分比数字,如 5.4% 是“5.4” popup.warning.noPriceFeedAvailable=该货币没有可用的价格。 你不能使用基于百分比的价格。\n请选择固定价格。 popup.warning.sendMsgFailed=向您的交易对象发送消息失败。\n请重试,如果继续失败报告错误。 popup.warning.btcChangeBelowDustException=该交易创建的更改输出低于零头限制(546 聪),将被比特币网络拒绝。\n\n您需要将零头添加到发送量中,以避免生成零头输出。\n\n零头输出为{0}。 popup.warning.messageTooLong=您的信息超过最大允许的大小。请将其分成多个部分发送,或将其上传到 https://pastebin.com 之类的服务器。 popup.warning.lockedUpFunds=你已经从一个失败的交易中冻结了资金。\n冻结余额:{0}\n存款tx地址:{1}\n交易单号:{2}\n\n请通过选择待处理交易界面中的交易并点击“alt + o”或“option+ o”打开帮助话题。 popup.warning.moneroConnection=连接 Monero 网络时出现问题。\n\n{0} popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n takeOffer.cancelButton=Cancel take-offer takeOffer.warningButton=忽略并继续 # suppress inspection "UnusedProperty" popup.warning.nodeBanned=其中一个 {0} 节点已被禁用 # suppress inspection "UnusedProperty" popup.warning.priceRelay=价格传递 popup.warning.seed=种子 popup.warning.mandatoryUpdate.trading=请更新到最新的 Haveno 版本。强制更新禁止了旧版本进行交易。更多信息请访问 Haveno 论坛。 popup.warning.noFilter=我们没有从种子节点接收到过滤器对象。请通知网络管理员注册一个过滤器对象。 popup.warning.burnXMR=这笔交易是无法实现,因为 {0} 的挖矿手续费用会超过 {1} 的转账金额。请等到挖矿手续费再次降低或您积累了更多的 XMR 来转账。 popup.warning.openOffer.makerFeeTxRejected=交易 ID 为 {0} 的挂单费交易被比特币网络拒绝。\n交易 ID = {1}\n交易已被移至失败交易。\n请到“设置/网络信息”进行 SPV 重新同步。\n如需更多帮助,请联系 Haveno Keybase 团队的 Support 频道 popup.warning.trade.txRejected.tradeFee=交易手续费 popup.warning.trade.txRejected.deposit=押金 popup.warning.trade.txRejected=使用 ID {1} 进行交易的 {0} 交易被比特币网络拒绝。\n交易 ID = {2}\n交易已被移至失败交易。\n请到“设置/网络信息”进行 SPV 重新同步。\n如需更多帮助,请联系 Haveno Keybase 团队的 Support 频道 popup.warning.openOfferWithInvalidMakerFeeTx=交易 ID 为 {0} 的挂单费交易无效。\n交易 ID = {1}。\n请到“设置/网络信息”进行 SPV 重新同步。\n如需更多帮助,请联系 Haveno Keybase 团队的 Support 频道 popup.info.securityDepositInfo=为了确保双方都遵守交易协议,双方都需要支付保证金。\n\n这笔存款一直保存在您的交易钱包里,直到您的交易成功完成,然后再退还给您。\n\n请注意:如果您正在创建一个新的报价,Haveno 需要运行另一个交易员接受它。为了让您的报价在线,保持 Haveno 运行,并确保这台计算机也在线(即,确保它没有切换到待机模式…显示器可以待机)。 popup.info.cashDepositInfo=请确保您在您的地区有一个银行分行,以便能够进行现金存款。\n卖方银行的银行 ID(BIC/SWIFT)为:{0}。 popup.info.cashDepositInfo.confirm=我确认我可以支付保证金 popup.info.shutDownWithOpenOffers=Haveno 正在被关闭,但仍有公开的报价。\n\n当 Haveno 关闭时,这些提供将不能在 P2P 网络上使用,但是它们将在您下次启动 Haveno 时重新发布到 P2P 网络上。\n\n为了让您的报价在线,保持 Haveno 运行,并确保这台计算机也在线(即,确保它不会进入待机模式…显示器待机不是问题)。 popup.info.qubesOSSetupInfo=你似乎好像在 Qubes OS 上运行 Haveno。\n\n请确保您的 Haveno qube 是参考设置指南的说明设置的 https://haveno.exchange/wiki/Running_Haveno_on_Qubes popup.warn.downGradePrevention=不支持从 {0} 版本降级到 {1} 版本。请使用最新的 Haveno 版本。 popup.privateNotification.headline=重要私人通知! popup.securityRecommendation.headline=重要安全建议 popup.securityRecommendation.msg=如果您还没有启用,我们想提醒您考虑为您的钱包使用密码保护。\n\n强烈建议你写下钱包还原密钥。 那些还原密钥就是恢复你的比特币钱包的主密码。\n在“钱包密钥”部分,您可以找到更多信息\n\n此外,您应该在“备份”界面备份完整的应用程序数据文件夹。 popup.xmrLocalNode.msg=Haveno 侦测到本机(本地)运行着一个门罗币节点(位于 localhost)。\n\n请确保在启动 Haveno 之前,节点已完全同步。 popup.shutDownInProgress.headline=正在关闭 popup.shutDownInProgress.msg=关闭应用可能会花一点时间。\n请不要打断关闭过程。 popup.attention.forTradeWithId=交易 ID {0} 需要注意 popup.attention.reasonForPaymentRuleChange=Version 1.5.5 introduces a critical trade rule change regarding the \"reason for payment\" field in bank transfers. Please leave this field empty -- DO NOT use the trade ID as \"reason for payment\" anymore. popup.info.multiplePaymentAccounts.headline=多个支付账户可用 popup.info.multiplePaymentAccounts.msg=您有多个支付帐户在这个报价中可用。请确你做了正确的选择。 popup.accountSigning.selectAccounts.headline=选择付款账户 popup.accountSigning.selectAccounts.description=根据付款方式和时间点,所有与支付给买方的付款发生的争议有关的付款帐户将被选择让您验证。 popup.accountSigning.selectAccounts.signAll=验证所有付款方式 popup.accountSigning.selectAccounts.datePicker=选择要验证的帐户的时间点 popup.accountSigning.confirmSelectedAccounts.headline=确认选定的付款帐户 popup.accountSigning.confirmSelectedAccounts.description=根据您的输入,将选择 {0} 支付帐户。 popup.accountSigning.confirmSelectedAccounts.button=确认付款账户 popup.accountSigning.signAccounts.headline=确认验证付款账户 popup.accountSigning.signAccounts.description=根据您的选择,{0} 付款帐户将被验证。 popup.accountSigning.signAccounts.button=验证付款账户 popup.accountSigning.signAccounts.ECKey=输入仲裁员密钥 popup.accountSigning.signAccounts.ECKey.error=不正确的仲裁员 ECKey popup.accountSigning.success.headline=恭喜 popup.accountSigning.success.description=所有 {0} 支付账户已成功验证! popup.accountSigning.generalInformation=您将在帐户页面找到所有账户的验证状态。\n\n更多信息,请访问https://docs.haveno.exchange/payment-methods#account-signing. popup.accountSigning.signedByArbitrator=您的一个付款帐户已被认证以及被仲裁员验证。交易成功后,使用此帐户将自动验证您的交易伙伴的帐户。\n\n{0} popup.accountSigning.signedByPeer=您的一个付款帐户已经被交易伙伴验证和验证。您的初始交易限额将被取消,您将能够在{0}天后验证其他帐户。 popup.accountSigning.peerLimitLifted=您其中一个帐户的初始限额已被取消。\n\n{0} popup.accountSigning.peerSigner=您的一个帐户已足够成熟,可以验证其他付款帐户,您的一个帐户的初始限额已被取消。\n\n{0} popup.accountSigning.singleAccountSelect.headline=导入未验证账龄证据 popup.accountSigning.confirmSingleAccount.headline=确认所选账龄证据 popup.accountSigning.confirmSingleAccount.selectedHash=已选择证据哈希值 popup.accountSigning.confirmSingleAccount.button=验证账龄证据 popup.accountSigning.successSingleAccount.description=证据 {0} 已被验证 popup.accountSigning.successSingleAccount.success.headline=成功 popup.accountSigning.unsignedPubKeys.headline=未验证公钥 popup.accountSigning.unsignedPubKeys.sign=验证公钥 popup.accountSigning.unsignedPubKeys.signed=公钥已被验证 popup.accountSigning.unsignedPubKeys.result.signed=已验证公钥 popup.accountSigning.unsignedPubKeys.result.failed=未能验证公钥 popup.info.buyerAsTakerWithoutDeposit.headline=买家无需支付保证金 popup.info.buyerAsTakerWithoutDeposit=您的报价将不需要来自XMR买家的保证金或费用。\n\n要接受您的报价,您必须与交易伙伴在Haveno外共享一个密码短语。\n\n密码短语会自动生成,并在创建后显示在报价详情中。 #################################################################### # Notifications #################################################################### notification.trade.headline=交易 ID {0} 的通知 notification.ticket.headline=交易 ID {0} 的帮助话题 notification.trade.completed=交易现在完成,您可以提取资金。 notification.trade.accepted=您 XMR {0} 的报价被接受。 notification.trade.unlocked=您的交易至少有一个区块链确认。\n您现在可以开始付款。 notification.trade.paymentSent=XMR 买家已经开始付款。 notification.trade.selectTrade=选择交易 notification.trade.peerOpenedDispute=您的交易对象创建了一个 {0}。 notification.trade.disputeClosed=这个 {0} 被关闭。 notification.walletUpdate.headline=交易钱包更新 notification.walletUpdate.msg=您的交易钱包充值成功。\n金额:{0} notification.takeOffer.walletUpdate.msg=您的交易钱包已经从早期的下单尝试中得到足够的资金支持。\n金额:{0} notification.tradeCompleted.headline=交易完成 notification.tradeCompleted.msg=您可以将您的资金提现到外部的门罗币钱包,或者保留在您的 Haveno 钱包中。 #################################################################### # System Tray #################################################################### systemTray.show=显示应用程序窗口 systemTray.hide=隐藏应用程序窗口 systemTray.info=关于 Haveno 信息 systemTray.exit=退出 systemTray.tooltip=Haveno:去中心化比特币交易网络 #################################################################### # GUI Util #################################################################### guiUtil.accountExport.savedToPath=交易账户保存在路径:\n{0} guiUtil.accountExport.noAccountSetup=您没有交易账户设置导出。 guiUtil.accountExport.selectPath=选择路径 {0} # suppress inspection "TrailingSpacesInProperty" guiUtil.accountExport.tradingAccount=交易账户 ID {0}\n # suppress inspection "TrailingSpacesInProperty" guiUtil.accountImport.noImport=我们没有导入 ID {0} 的交易账户,因为它已经存在。\n guiUtil.accountExport.exportFailed=导出 .CSV 失败,因为发生了错误。\n错误 = {0} guiUtil.accountExport.selectExportPath=选择导出路径 guiUtil.accountImport.imported=交易账户导入路径:\n{0}\n\n导入账户:\n{1} guiUtil.accountImport.noAccountsFound=在路径 {0} 找不到导出的交易账户。\n文件名为 {1}。 guiUtil.openWebBrowser.warning=您将在系统网络浏览器中打开一个网页。\n你现在要打开网页吗?\n\n如果您没有使用“Tor 浏览器”作为默认的系统网络浏览器,则将以默认连接到网页。\n\n网址:“{0}” guiUtil.openWebBrowser.doOpen=打开网页并且不要再询问我 guiUtil.openWebBrowser.copyUrl=复制 URL 并取消 guiUtil.ofTradeAmount=的交易数量 guiUtil.requiredMinimum=(最低需求量) #################################################################### # Component specific #################################################################### list.currency.select=选择币种 list.currency.showAll=显示全部 list.currency.editList=编辑币种列表 table.placeholder.noItems=最近没有可用的 {0} table.placeholder.noData=最近没有可用数据 table.placeholder.processingData=处理数据… peerInfoIcon.tooltip.tradePeer=交易伙伴 peerInfoIcon.tooltip.maker=制造者 peerInfoIcon.tooltip.trade.traded={0} 匿名地址:{1}\n您已经与他交易过 {2} 次了\n{3} peerInfoIcon.tooltip.trade.notTraded={0} 匿名地址:{1}\n你还没有与他交易过。\n{2} peerInfoIcon.tooltip.age=支付账户在 {0} 前创建。 peerInfoIcon.tooltip.unknownAge=支付账户账龄未知。 tooltip.openPopupForDetails=打开弹出窗口的详细信息 tooltip.invalidTradeState.warning=这个交易处于不可用状态。打开详情窗口以发现更多细节。 tooltip.openBlockchainForAddress=使用外部区块链浏览器打开地址:{0} tooltip.openBlockchainForTx=使用外部区块链浏览器打开交易:{0} confidence.unknown=未知交易状态 confidence.seen=被 {0} 人查看 / 0 确定 confidence.confirmed={0} 确认(s) confidence.invalid=交易无效 peerInfo.title=对象资料 peerInfo.nrOfTrades=已完成交易数量 peerInfo.notTradedYet=你还没有与他交易过。 peerInfo.setTag=设置该对象的标签 peerInfo.age.noRisk=支付账户账龄 peerInfo.age.chargeBackRisk=自验证 peerInfo.unknownAge=账龄未知 addressTextField.openWallet=打开您的默认比特币钱包 addressTextField.copyToClipboard=复制地址到剪贴板 addressTextField.addressCopiedToClipboard=地址已被复制到剪贴板 addressTextField.openWallet.failed=打开默认的比特币钱包应用程序失败了。或许您没有安装? peerInfoIcon.tooltip={0}\n标识:{1} txIdTextField.copyIcon.tooltip=复制交易 ID 到剪贴板 txIdTextField.blockExplorerIcon.tooltip=使用外部区块链浏览器打开这个交易 ID txIdTextField.missingTx.warning.tooltip=所需的交易缺失 #################################################################### # Navigation #################################################################### navigation.account=“账户” navigation.account.walletSeed=“账户/钱包密钥” navigation.funds.availableForWithdrawal=“资金/提现” navigation.portfolio.myOpenOffers=“资料/未完成报价” navigation.portfolio.pending=“业务/未完成交易” navigation.portfolio.closedTrades=“资料/历史” navigation.funds.depositFunds=“资金/收到资金” navigation.settings.preferences=“设置/偏好” # suppress inspection "UnusedProperty" navigation.funds.transactions=“资金/交易记录” navigation.support=“帮助” #################################################################### # Formatter #################################################################### formatter.formatVolumeLabel={0} 数量 {1} formatter.makerTaker=卖家 {0} {1} / 买家 {2} {3} formatter.makerTaker.locked=卖家 {0} {1} / 买家 {2} {3} 🔒 formatter.youAreAsMaker=您是 {1} {0} 卖家 / 买家是 {3} {2} formatter.youAreAsTaker=您是 {1} {0} 买家 / 卖家是 {3} {2} formatter.youAre=您是 {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=您创建新的报价 {0} {1} formatter.youAreCreatingAnOffer.traditional.locked=您创建新的报价 {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=您正创建报价 {0} {1}({2} {3}) formatter.youAreCreatingAnOffer.crypto.locked=您正创建报价 {0} {1}({2} {3}) 🔒 formatter.asMaker={0} {1} 是卖家 formatter.asTaker={0} {1} 是买家 #################################################################### # Domain specific #################################################################### # we use enum values here # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" XMR_MAINNET=XMR Mainnet # suppress inspection "UnusedProperty" XMR_LOCAL=XMR Testnet # suppress inspection "UnusedProperty" XMR_STAGENET=XMR Stagenet time.year=年线 time.month=月线 time.week=周线 time.day=日线 time.hour=小时 time.minute10=10分钟 time.hours=小时 time.days=天 time.1hour=1小时 time.1day=1天 time.minute=分钟 time.second=秒 time.minutes=分钟 time.seconds=秒 password.enterPassword=输入密码 password.confirmPassword=确认密码 password.tooLong=你输入的密码太长,不要超过 500 个字符。 password.deriveKey=从密码中提取密钥 password.walletDecrypted=钱包成功解密并移除密码保护 password.wrongPw=你输入了错误的密码。\n\n请再次尝试输入密码,仔细检查拼写错误。 password.walletEncrypted=钱包成功加密并开启密码保护。 password.passwordsDoNotMatch=这2个密码您输入的不相同 password.forgotPassword=忘记密码? password.backupReminder=请注意,设置钱包密码时,所有未加密的钱包的自动创建的备份将被删除。\n\n强烈建议您备份应用程序的目录,并在设置密码之前记下您的还原密钥! password.backupWasDone=我已备份 password.setPassword=Set Password (I already made a backup) password.makeBackup=Make Backup seed.seedWords=钱包密钥 seed.enterSeedWords=输入钱包密钥 seed.date=钱包时间 seed.restore.title=使用还原密钥恢复钱包 seed.restore=恢复钱包 seed.creationDate=创建时间 seed.warn.walletNotEmpty.msg=你的比特币钱包不是空的。\n\n在尝试恢复较旧的钱包之前,您必须清空此钱包,因为将钱包混在一起会导致无效的备份。\n\n请完成您的交易,关闭所有您的未完成报价,并转到资金界面撤回您的比特币。\n如果您无法访问您的比特币,您可以使用紧急工具清空钱包。\n要打开该应急工具,请按“alt + e”或“Cmd/Ctrl + e” 。 seed.warn.walletNotEmpty.restore=无论如何我要恢复 seed.warn.walletNotEmpty.emptyWallet=我先清空我的钱包 seed.warn.notEncryptedAnymore=你的钱包被加密了。\n\n恢复后,钱包将不再加密,您必须设置新的密码。\n\n你要继续吗? seed.warn.walletDateEmpty=由于您尚未指定钱包日期,因此 Haveno 将必须扫描 2013.10.09(BIP39创始日期)之后的区块链。\n\nBIP39 钱包于 Haveno 于 2017.06.28 首次发布(版本 v0.5)。因此,您可以使用该日期来节省时间。\n\n理想情况下,您应指定创建钱包种子的日期。\n\n\n您确定要继续而不指定钱包日期吗? seed.restore.success=新的还原密钥成功地恢复了钱包。\n\n您需要关闭并重新启动应用程序。 seed.restore.error=使用还原密钥恢复钱包时出现错误。{0} seed.restore.openOffers.warn=您有公开报价,如果您从种子词恢复,则这些报价将被删除。\n您确定要继续吗? #################################################################### # Payment methods #################################################################### payment.account=账户 payment.account.no=账户编号 payment.account.name=账户名称 payment.account.username=用户昵称 payment.account.phoneNr=电话号码 payment.account.owner.fullname=账户拥有者姓名: payment.account.fullName=全称(名,中间名,姓) payment.account.state=州/省/地区 payment.account.city=城市 payment.bank.country=银行所在国家或地区 payment.account.name.email=账户拥有者姓名/电子邮箱 payment.account.name.emailAndHolderId=账户拥有者姓名/电子邮箱 / {0} payment.bank.name=银行名称 payment.select.account=选择账户类型 payment.select.region=选择地区 payment.select.country=选择国家或地区 payment.select.bank.country=选择银行所在国家或地区 payment.foreign.currency=你确定想选择一个与此国家或地区默认币种不同的货币? payment.restore.default=不,恢复默认值 payment.email=电子邮箱 payment.country=国家或地区 payment.extras=额外要求 payment.email.mobile=电子邮箱或手机号码 payment.crypto.address=数字货币地址 payment.crypto.tradeInstantCheckbox=使用数字货币进行即时交易( 1 小时内) payment.crypto.tradeInstant.popup=对于即时交易,要求交易双方都在线,能够在不到1小时内完成交易。\n \n如果你已经有未完成的报价以及你不能即时完成,请在资料页面禁用这些报价。 payment.crypto=数字货币 payment.select.crypto=选择或搜索数字货币 payment.secret=密保问题 payment.answer=答案 payment.wallet=钱包 ID payment.amazon.site=Buy giftcard at payment.ask=Ask in Trader Chat payment.uphold.accountId=用户名或电子邮箱或电话号码 payment.moneyBeam.accountId=电子邮箱或者电话号码 payment.popmoney.accountId=电子邮箱或者电话号码 payment.promptPay.promptPayId=公民身份证/税号或电话号码 payment.supportedCurrencies=支持的货币 payment.supportedCurrenciesForReceiver=收款 payment.limitations=限制条件 payment.salt=帐户年龄验证盐值 payment.error.noHexSalt=盐值需要十六进制的。\n如果您想要从旧帐户转移盐值以保留帐龄,只建议编辑盐值字段。帐龄通过帐户盐值和识别帐户数据(例如 IBAN )来验证。 payment.accept.euro=接受来自这些欧元国家的交易 payment.accept.nonEuro=接受来自这些非欧元国家的交易 payment.accepted.countries=接受的国家 payment.accepted.banks=接受的银行(ID) payment.mobile=手机号码 payment.postal.address=邮寄地址 payment.national.account.id.AR=CBU 号码 shared.accountSigningState=账户验证状态 #new payment.crypto.address.dyn={0} 地址 payment.crypto.receiver.address=接收者的数字货币地址 payment.accountNr=账号: payment.emailOrMobile=电子邮箱或手机号码 payment.useCustomAccountName=使用自定义名称 payment.maxPeriod=最大允许交易时限 payment.maxPeriodAndLimit=最大交易时间:{0}/ 最大买入:{1}/ 最大出售:{2}/账龄:{3} payment.maxPeriodAndLimitCrypto=最大交易期限:{0}/最大交易限额:{1} payment.currencyWithSymbol=货币:{0} payment.nameOfAcceptedBank=接受的银行名称 payment.addAcceptedBank=添加接受的银行 payment.clearAcceptedBanks=清除接受的银行 payment.bank.nameOptional=银行名称(可选) payment.bankCode=银行代码 payment.bankId=银行 ID (BIC/SWIFT): payment.bankIdOptional=银行 ID(BIC/SWIFT)(可选) payment.branchNr=分行编码 payment.branchNrOptional=分行编码(可选) payment.accountNrLabel=账号(IBAN) payment.accountType=账户类型 payment.checking=检查 payment.savings=保存 payment.personalId=个人 ID payment.zelle.info=Zelle是一项转账服务,转账到其他银行做的很好。\n\n1.检查此页面以查看您的银行是否(以及如何)与 Zelle 合作:\nhttps://www.zellepay.com/get-started\n\n2.特别注意您的转账限额-汇款限额因银行而异,银行通常分别指定每日,每周和每月的限额。\n\n3.如果您的银行不能使用 Zelle,您仍然可以通过 Zelle 移动应用程序使用它,但是您的转账限额会低得多。\n\n4.您的 Haveno 帐户上指定的名称必须与 Zelle/银行帐户上的名称匹配。 \n\n如果您无法按照贸易合同中的规定完成 Zelle 交易,则可能会损失部分(或全部)保证金。\n\n由于 Zelle 的拒付风险较高,因此建议卖家通过电子邮件或 SMS 与未签名的买家联系,以确认买家确实拥有 Haveno 中指定的 Zelle 帐户。 payment.fasterPayments.newRequirements.info=有些银行已经开始核实快捷支付收款人的全名。您当前的快捷支付帐户没有填写全名。\n\n请考虑在 Haveno 中重新创建您的快捷支付帐户,为将来的 {0} 买家提供一个完整的姓名。\n\n重新创建帐户时,请确保将银行区号、帐户编号和帐龄验证盐值从旧帐户复制到新帐户。这将确保您现有的帐龄和签名状态得到保留。 payment.moneyGram.info=使用 MoneyGram 时,XMR 买方必须将授权号码和收据的照片通过电子邮件发送给 XMR 卖方。收据必须清楚地显示卖方的全名、国家或地区、州和金额。买方将在交易过程中显示卖方的电子邮件。 payment.westernUnion.info=使用 Western Union 时,XMR 买方必须通过电子邮件将 MTCN(运单号)和收据照片发送给 XMR 卖方。收据上必须清楚地显示卖方的全名、城市、国家或地区和金额。买方将在交易过程中显示卖方的电子邮件。 payment.halCash.info=使用 HalCash 时,XMR 买方需要通过手机短信向 XMR 卖方发送 HalCash 代码。\n\n请确保不要超过银行允许您用半现金汇款的最高金额。每次取款的最低金额是 10 欧元,最高金额是 10 欧元。金额是 600 欧元。对于重复取款,每天每个接收者 3000 欧元,每月每个接收者 6000 欧元。请与您的银行核对这些限额,以确保它们使用与此处所述相同的限额。\n\n提现金额必须是 10 欧元的倍数,因为您不能从 ATM 机提取其他金额。 创建报价和下单屏幕中的 UI 将调整 XMR 金额,使 EUR 金额正确。你不能使用基于市场的价格,因为欧元的数量会随着价格的变化而变化。\n # suppress inspection "UnusedMessageFormatParameter" payment.limits.info=请注意,所有银行转账都有一定的退款风险。为了降低这一风险,Haveno 基于使用的付款方式的退款风险。\n\n对于付款方式,您的每笔交易的出售和购买的限额为{2}\n\n限制只应用在单笔交易,你可以尽可能多的进行交易。\n\n在 Haveno Wiki 查看更多信息[HYPERLINK:https://docs.haveno.exchange/overview/account_limits]。 # suppress inspection "UnusedProperty" payment.limits.info.withSigning=为了降低这一风险,Haveno 基于两个因素对该付款方式每笔交易设置了限制:\n\n1. 使用的付款方法的预估退款风险水平\n2. 您的付款方式的账龄\n\n这个付款账户还没有被验证,所以他每个交易最多购买{0}。在验证之后,购买限制会以以下规则逐渐增加:\n\n●签署前,以及签署后30天内,您的每笔最大交易将限制为{0}\n●签署后30天,每笔最大交易将限制为{1}\n●签署后60天,每笔最大交易将限制为{2}\n\n出售限制不会被账户验证状态限制,你可以理科进行单笔为{2}的交易\n\n限制只应用在单笔交易,你可以尽可能多的进行交易。\n\n在 Haveno Wiki 上查看更多:\nhttps://docs.haveno.exchange/overview/account_limits payment.cashDeposit.info=请确认您的银行允许您将现金存款汇入他人账户。例如,美国银行和富国银行不再允许此类存款。 payment.revolut.info=Revolut 要求使用“用户名”作为帐户 ID,而不是像以往的电话号码或电子邮件。 payment.account.revolut.addUserNameInfo={0}\n您现有的 Revolut 帐户({1})尚未设置“用户名”。\n请输入您的 Revolut ``用户名'以更新您的帐户数据。\n这不会影响您的账龄验证状态。 payment.revolut.addUserNameInfo.headLine=更新 Revolut 账户 payment.cashapp.info=请注意,Cash App 的退款风险高于大多数银行转账。 payment.venmo.info=请注意,Venmo 的退款风险高于大多数银行转账。 payment.paypal.info=请注意,PayPal 的退款风险高于大多数银行转账。 payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account payment.usPostalMoneyOrder.info=在 Haveno 上交易 US Postal Money Orders (USPMO)您必须理解下述条款:\n\n- XMR 买方必须在发送方和收款人字段中都写上 XMR 卖方的名称,并在发送之前对 USPMO 和信封进行高分辨率照片拍照,并带有跟踪证明。\n- XMR 买方必须将 USPMO 连同交货确认书一起发送给 XMR 卖方。\n\n如果需要调解,或有交易纠纷,您将需要将照片连同 USPMO 编号,邮局编号和交易金额一起发送给 Haveno 调解员或退款代理,以便他们进行验证美国邮局网站上的详细信息。\n\n如未能提供要求的交易数据将在纠纷中直接判负\n\n在所有争议案件中,USPMO 发送方在向调解人或仲裁员提供证据/证明时承担 100% 的责任。\n\n如果您不理解这些要求,请不要在 Haveno 上使用 USPMO 进行交易。 payment.payByMail.contact=联系方式 payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.f2f.contact=联系方式 payment.f2f.contact.prompt=您希望如何与交易伙伴联系?(电子邮箱、电话号码、…) payment.f2f.city=“面对面”会议的城市 payment.f2f.city.prompt=城市将与报价一同显示 payment.shared.optionalExtra=可选的附加信息 payment.shared.extraInfo=附加信息 payment.shared.extraInfo.offer=附加报价信息 payment.shared.extraInfo.prompt.paymentAccount=定义您希望在此支付账户的报价中显示的任何特殊术语、条件或细节(用户在接受报价之前将看到这些信息)。 payment.shared.extraInfo.prompt.offer=定义您希望随您的报价一起显示的任何特殊条款、条件或详细信息。 payment.shared.extraInfo.noDeposit=联系方式和优惠条款 payment.f2f.info=与网上交易相比,“面对面”交易有不同的规则,也有不同的风险。\n\n主要区别是:\n●交易伙伴需要使用他们提供的联系方式交换关于会面地点和时间的信息。\n●交易双方需要携带笔记本电脑,在会面地点确认“已发送付款”和“已收到付款”。\n●如果交易方有特殊的“条款和条件”,他们必须在账户的“附加信息”文本框中声明这些条款和条件。\n●在发生争议时,调解员或仲裁员不能提供太多帮助,因为通常很难获得有关会面上所发生情况的篡改证据。在这种情况下,XMR 资金可能会被无限期锁定,或者直到交易双方达成协议。\n\n为确保您完全理解“面对面”交易的不同之处,请阅读以下说明和建议:“https://docs.haveno.exchange/trading-rules.html#f2f-trading” payment.f2f.info.openURL=打开网页 payment.f2f.offerbook.tooltip.countryAndCity=国家或地区及城市:{0} / {1} payment.shared.extraInfo.tooltip=附加信息:{0} payment.japan.bank=银行 payment.japan.branch=分行 payment.japan.account=账户 payment.japan.recipient=名称 payment.australia.payid=PayID payment.payid=PayID 需链接至金融机构。例如电子邮件地址或手机。 payment.payid.info=PayID,如电话号码、电子邮件地址或澳大利亚商业号码(ABN),您可以安全地连接到您的银行、信用合作社或建立社会帐户。你需要在你的澳大利亚金融机构创建一个 PayID。发送和接收金融机构都必须支持 PayID。更多信息请查看[HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=为了保障您的安全,我们强烈不建议使用 Paysafecard PIN 进行支付。\n\n\ 通过 PIN 进行的交易无法被独立验证以解决争议。如果出现问题,资金可能无法追回。\n\n\ 为确保交易安全并支持争议解决,请始终使用提供可验证记录的支付方式。 # We use constants from the code so we do not use our normal naming convention # dynamic values are not recognized by IntelliJ # Only translate general terms NATIONAL_BANK=国内银行转账 SAME_BANK=同银行转账 SPECIFIC_BANKS=转到指定银行 US_POSTAL_MONEY_ORDER=美国邮政汇票 CASH_DEPOSIT=现金/ATM 存款 PAY_BY_MAIL=Pay By Mail MONEY_GRAM=MoneyGram WESTERN_UNION=西联汇款 F2F=面对面(当面交易) JAPAN_BANK=日本银行汇款 AUSTRALIA_PAYID=澳大利亚 PayID # suppress inspection "UnusedProperty" NATIONAL_BANK_SHORT=国内银行 # suppress inspection "UnusedProperty" SAME_BANK_SHORT=相同银行 # suppress inspection "UnusedProperty" SPECIFIC_BANKS_SHORT=指定银行 # suppress inspection "UnusedProperty" US_POSTAL_MONEY_ORDER_SHORT=美国汇票 # suppress inspection "UnusedProperty" CASH_DEPOSIT_SHORT=现金/ATM 存款 # suppress inspection "UnusedProperty" PAY_BY_MAIL_SHORT=PayByMail # suppress inspection "UnusedProperty" MONEY_GRAM_SHORT=MoneyGram # suppress inspection "UnusedProperty" WESTERN_UNION_SHORT=西联汇款 # suppress inspection "UnusedProperty" F2F_SHORT=F2F # suppress inspection "UnusedProperty" JAPAN_BANK_SHORT=Japan Furikomi # suppress inspection "UnusedProperty" AUSTRALIA_PAYID_SHORT=支付 ID # Do not translate brand names # suppress inspection "UnusedProperty" UPHOLD=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM=MoneyBeam(N26) # suppress inspection "UnusedProperty" POPMONEY=Popmoney # suppress inspection "UnusedProperty" REVOLUT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY=支付宝 # suppress inspection "UnusedProperty" WECHAT_PAY=微信支付 # suppress inspection "UnusedProperty" SEPA=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT=SEPA 即时支付 # suppress inspection "UnusedProperty" FASTER_PAYMENTS=更快的支付方式 # suppress inspection "UnusedProperty" SWISH=Swish # suppress inspection "UnusedProperty" ZELLE=Zelle(Zelle) # suppress inspection "UnusedProperty" CHASE_QUICK_PAY=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS=数字货币 # suppress inspection "UnusedProperty" PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=亚马逊电子礼品卡 # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT=Cryptos Instant # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY=OKPay # suppress inspection "UnusedProperty" CASH_APP=Cash App # suppress inspection "UnusedProperty" VENMO=Venmo # suppress inspection "UnusedProperty" UPHOLD_SHORT=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM_SHORT=MoneyBeam(N26) # suppress inspection "UnusedProperty" POPMONEY_SHORT=Popmoney # suppress inspection "UnusedProperty" REVOLUT_SHORT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY_SHORT=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY_SHORT=支付宝 # suppress inspection "UnusedProperty" WECHAT_PAY_SHORT=微信支付 # suppress inspection "UnusedProperty" SEPA_SHORT=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT_SHORT=SEPA Instant # suppress inspection "UnusedProperty" FASTER_PAYMENTS_SHORT=更快的支付方式 # suppress inspection "UnusedProperty" SWISH_SHORT=Swish # suppress inspection "UnusedProperty" ZELLE_SHORT=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY_SHORT=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER_SHORT=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH_SHORT=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS_SHORT=数字货币 # suppress inspection "UnusedProperty" PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=亚马逊电子礼品卡 # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT_SHORT=Cryptos Instant # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY_SHORT=OKPay # suppress inspection "UnusedProperty" CASH_APP_SHORT=Cash App # suppress inspection "UnusedProperty" VENMO_SHORT=Venmo #################################################################### # Validation #################################################################### validation.empty=不允许留空。 validation.NaN=输入的不是有效数字。 validation.notAnInteger=输入的不是整数。 validation.zero=不允许输入0。 validation.negative=不允许输入负值。 validation.traditional.tooSmall=不允许输入比最小可能值还小的数值。 validation.traditional.tooLarge=不允许输入比最大可能值还大的数值。 validation.xmr.fraction=此充值将会产生小于 1 聪的比特币数量。 validation.xmr.tooLarge=不允许充值大于{0} validation.xmr.tooSmall=不允许充值小于{0} validation.passwordTooShort=你输入的密码太短。最少 8 个字符。 validation.passwordTooLong=你输入的密码太长。最长不要超过50个字符。 validation.sortCodeNumber={0} 必须由 {1} 个数字构成。 validation.sortCodeChars={0} 必须由 {1} 个字符构成。 validation.bankIdNumber={0} 必须由 {1} 个数字构成。 validation.accountNr=账号必须由 {0} 个数字构成。 validation.accountNrChars=账户必须由 {0} 个字符构成。 validation.xmr.invalidAddress=地址不正确,请检查地址格式。 validation.integerOnly=请输入整数。 validation.inputError=您的输入引起了错误:\n{0} validation.xmr.exceedsMaxTradeLimit=您的交易限额为 {0}。 validation.nationalAccountId={0} 必须由{1}个数字组成。 #new validation.invalidInput=输入无效:{0} validation.accountNrFormat=帐号必须是格式:{0} # suppress inspection "UnusedProperty" validation.crypto.wrongStructure=地址验证失败,因为它与 {0} 地址的结构不匹配。 # suppress inspection "UnusedProperty" validation.crypto.ltz.zAddressesNotSupported=LTZ 地址需要以 L 开头。 不支持以 Z 开头的地址。 # suppress inspection "UnusedProperty" validation.crypto.zAddressesNotSupported=LTZ 地址需要以 L 开头。 不支持以 Z 开头的地址。 # suppress inspection "UnusedProperty" validation.crypto.invalidAddress=这个地址不是有效的{0}地址!{1} # suppress inspection "UnusedProperty" validation.crypto.liquidBitcoin.invalidAddress=不支持本地 segwit 地址(以“lq”开头的地址)。 validation.bic.invalidLength=输入长度既不是 8 也不是 11 validation.bic.letters=必须输入银行和国家或地区代码 validation.bic.invalidLocationCode=BIC 包含无效的地址代码 validation.bic.invalidBranchCode=BIC 包含无效的分行代码 validation.bic.sepaRevolutBic=不支持 Revolut Sepa 账户 validation.btc.invalidFormat=无效格式的比特币地址 validation.email.invalidAddress=无效地址 validation.iban.invalidCountryCode=国家或地区代码无效 validation.iban.checkSumNotNumeric=校验必须是数字 validation.iban.nonNumericChars=检测到非字母数字字符 validation.iban.checkSumInvalid=IBAN 校验无效 validation.iban.invalidLength=数字的长度必须为15到34个字符。 validation.interacETransfer.invalidAreaCode=非加拿大区号 validation.interacETransfer.invalidPhone=请输入可用的 11 为电话号码(例如 1-123-456-7890)或邮箱地址 validation.interacETransfer.invalidQuestion=必须只包含字母、数字、空格和/或符号“_ , . ? -” validation.interacETransfer.invalidAnswer=必须是一个单词,只包含字母、数字和/或符号- validation.inputTooLarge=输入不能大于 {0} validation.inputTooSmall=输入必须大于 {0} validation.inputToBeAtLeast=输入必须至少为 {0} validation.amountBelowDust=不允许低于 {0} 聪的零头限制。 validation.length=长度必须在 {0} 和 {1} 之间 validation.fixedLength=Length must be {0} validation.pattern=输入格式必须为:{0} validation.noHexString=输入不是十六进制格式。 validation.advancedCash.invalidFormat=必须是有效的电子邮箱或钱包 ID 的格式为:X000000000000 validation.invalidUrl=输入的不是有效 URL 链接。 validation.mustBeDifferent=您输入的值必须与当前值不同 validation.cannotBeChanged=参数不能更改 validation.numberFormatException=数字格式异常 {0} validation.mustNotBeNegative=不能输入负值 validation.phone.missingCountryCode=需要两个字母的国家或地区代码来验证电话号码 validation.phone.invalidCharacters=电话号码 {0} 包含无效字符 validation.phone.insufficientDigits={0} 中没有足够的数字作为有效的电话号码 validation.phone.tooManyDigits={0} 中的数字太多,不是有效的电话号码 validation.phone.invalidDialingCode=数字 {0} 中的国际拨号代码对于 {1} 无效。正确的拨号号码是 {2} 。 validation.invalidAddressList=使用逗号分隔有效地址列表 ================================================ FILE: core/src/main/resources/i18n/displayStrings_zh-hant.properties ================================================ # Keep display strings organized by domain # Naming convention: We use camelCase and dot separated name spaces. # Use as many sub spaces as required to make the structure clear, but as little as possible. # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break # in the display but only in the editor. # Please use in all language files the exact same order of the entries, that way a comparison is easier. # Please try to keep the length of the translated string similar to English. If it is longer it might break layout or # get truncated. We will need some adjustments in the UI code to support that but we want to keep effort at the minimum. #################################################################### # Shared #################################################################### shared.readMore=閲讀更多 shared.openHelp=打開幫助 shared.warning=警吿 shared.close=關閉 shared.cancel=取消 shared.ok=好的 shared.yes=是 shared.no=否 shared.iUnderstand=我瞭解 shared.na=N/A shared.shutDown=完全關閉 shared.reportBug=在 Github 報吿錯誤 shared.buyMonero=買入比特幣 shared.sellMonero=賣出比特幣 shared.buyCurrency=買入 {0} shared.sellCurrency=賣出 {0} shared.buyCurrency.locked=買入 {0} 🔒 shared.sellCurrency.locked=賣出 {0} 🔒 shared.buyingXMRWith=用 {0} 買入 XMR shared.sellingXMRFor=賣出 XMR 為 {0} shared.buyingCurrency=買入 {0}(賣出 XMR) shared.sellingCurrency=賣出 {0}(買入 XMR) shared.buy=買 shared.sell=賣 shared.buying=買入 shared.selling=賣出 shared.P2P=P2P shared.oneOffer=報價 shared.multipleOffers=報價 shared.Offer=報價 shared.offerVolumeCode={0} 報價量 shared.openOffers=可用報價 shared.trade=交易 shared.trades=交易 shared.openTrades=進行中的交易 shared.dateTime=日期/時間 shared.price=價格 shared.priceWithCur={0} 價格 shared.priceInCurForCur=1 {1} 的 {0} 價格 shared.fixedPriceInCurForCur=1 {1} 的 {0} 的固定價格 shared.amount=數量 shared.txFee=交易手續費 shared.tradeFee=交易手續費 shared.buyerSecurityDeposit=買家保證金 shared.sellerSecurityDeposit=賣家保證金 shared.amountWithCur={0} 數量 shared.volumeWithCur={0} 總量 shared.currency=貨幣類型 shared.market=交易項目 shared.deviation=Deviation shared.paymentMethod=付款方式 shared.tradeCurrency=交易貨幣 shared.offerType=報價類型 shared.details=詳情 shared.address=地址 shared.balanceWithCur={0} 餘額 shared.utxo=Unspent transaction output shared.txId=交易記錄 ID shared.confirmations=審核 shared.revert=還原 Tx shared.select=選擇 shared.usage=使用狀況 shared.state=狀態 shared.tradeId=交易 ID shared.offerId=報價 ID shared.bankName=銀行名稱 shared.acceptedBanks=接受的銀行 shared.amountMinMax=總額(最小 - 最大) shared.amountHelp=如果報價包含最小和最大限制,那麼您可以在這個範圍內的任意數量進行交易。 shared.remove=移除 shared.goTo=前往 {0} shared.XMRMinMax=XMR(最小 - 最大) shared.removeOffer=移除報價 shared.dontRemoveOffer=不要移除報價 shared.editOffer=編輯報價 shared.openLargeQRWindow=打開放大二維碼窗口 shared.tradingAccount=交易賬户 shared.faq=訪問 FAQ 頁面 shared.yesCancel=是的,取消 shared.nextStep=下一步 shared.selectTradingAccount=選擇交易賬户 shared.fundFromSavingsWalletButton=從 Haveno 錢包申請資金 shared.fundFromExternalWalletButton=從您的外部錢包充值 shared.openDefaultWalletFailed=打開默認的比特幣錢包應用程序失敗了。您確定您安裝了嗎? shared.belowInPercent=低於市場價格 % shared.aboveInPercent=高於市場價格 % shared.enterPercentageValue=輸入 % 值 shared.OR=或者 shared.notEnoughFunds=您的 Haveno 錢包中沒有足夠的資金去支付這一交易 需要{0} 您可用餘額為 {1}。\n\n請從外部比特幣錢包注入資金或在“資金/存款”充值到您的 Haveno 錢包。 shared.waitingForFunds=等待資金充值... shared.TheXMRBuyer=XMR 買家 shared.You=您 shared.sendingConfirmation=發送確認... shared.sendingConfirmationAgain=請再次發送確認 shared.exportCSV=導出保存為 .csv shared.exportJSON=導出保存至 JSON shared.summary=Show summary shared.noDateAvailable=沒有可用數據 shared.noDetailsAvailable=沒有可用詳細 shared.notUsedYet=尚未使用 shared.date=日期 shared.sendFundsDetailsWithFee=發送中:{0}\n\n至接收地址:{1}\n\n額外礦工費:{2}\n\n您確定要發送此金額嗎? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno 檢測到,該交易將產生一個低於最低零頭閾值的輸出(不被比特幣共識規則所允許)。相反,這些零頭({0}satoshi{1})將被添加到挖礦手續費中。 shared.copyToClipboard=複製到剪貼板 shared.language=語言 shared.country=國家或地區 shared.applyAndShutDown=同意並關閉 shared.selectPaymentMethod=選擇付款方式 shared.accountNameAlreadyUsed=這個賬户名稱已經被已保存的賬户佔用。\n請使用另外一個名稱。 shared.askConfirmDeleteAccount=您確定想要刪除被選定的賬號嗎? shared.cannotDeleteAccount=您不能刪除這個賬户,因為它正在被使用於報價或交易中。 shared.noAccountsSetupYet=還沒有建立帳户。 shared.manageAccounts=管理賬户 shared.addNewAccount=添加新的賬户 shared.ExportAccounts=導出賬户 shared.importAccounts=導入賬户 shared.createNewAccount=創建新的賬户 shared.createNewAccountDescription=您的帳戶詳細資料儲存在您的裝置上,僅在開啟爭議時與您的交易夥伴和仲裁者分享。 shared.saveNewAccount=保存新的賬户 shared.selectedAccount=選中的賬户 shared.deleteAccount=刪除賬户 shared.errorMessageInline=\n錯誤信息:{0} shared.errorMessage=錯誤信息 shared.information=資料 shared.name=名稱 shared.id=ID shared.dashboard=儀表盤 shared.accept=接受 shared.balance=餘額 shared.save=保存 shared.onionAddress=匿名地址 shared.supportTicket=幫助話題 shared.dispute=糾紛 shared.mediationCase=調解事件 shared.seller=賣家 shared.buyer=買家 shared.allEuroCountries=所有歐元國家 shared.acceptedTakerCountries=接受的買家國家 shared.tradePrice=交易價格 shared.tradeAmount=交易金額 shared.tradeVolume=交易總量 shared.invalidKey=您輸入的密碼不正確。 shared.enterPrivKey=輸入私鑰解鎖 shared.payoutTxId=支出交易 ID shared.contractAsJson=JSON 格式的合同 shared.viewContractAsJson=查看 JSON 格式的合同 shared.contract.title=交易 ID:{0} 的合同 shared.paymentDetails=XMR {0} 支付詳情 shared.securityDeposit=保證金 shared.yourSecurityDeposit=你的保證金 shared.contract=合同 shared.messageArrived=消息送達。 shared.messageStoredInMailbox=消息保存在郵箱中。 shared.messageSendingFailed=消息發送失敗。錯誤:{0} shared.unlock=解鎖 shared.toReceive=接收 shared.toSpend=花費 shared.xmrAmount=XMR 總額 shared.yourLanguage=你的語言 shared.addLanguage=添加語言 shared.total=合計 shared.totalsNeeded=需要資金 shared.tradeWalletAddress=交易錢包地址 shared.tradeWalletBalance=交易錢包餘額 shared.reserveExactAmount=僅保留必要的資金。需支付礦工費,約 20 分鐘後您的報價才會上線。 shared.makerTxFee=賣家:{0} shared.takerTxFee=買家:{0} shared.iConfirm=我確認 shared.openURL=打開 {0} shared.fiat=法定貨幣 shared.crypto=加密 shared.preciousMetals=貴金屬 shared.all=全部 shared.edit=編輯 shared.advancedOptions=高級選項 shared.interval=取消 shared.actions=操作 shared.buyerUpperCase=買家 shared.sellerUpperCase=買家 shared.new=新 shared.learnMore=瞭解更多 shared.dismiss=忽略 shared.selectedArbitrator=選中的仲裁者 shared.selectedMediator=選擇調解員 shared.selectedRefundAgent=選中的仲裁者 shared.mediator=調解員 shared.arbitrator=仲裁員 shared.refundAgent=仲裁員 shared.refundAgentForSupportStaff=退款助理 shared.delayedPayoutTxId=延遲支付交易 ID shared.delayedPayoutTxReceiverAddress=延遲交易交易已發送至 shared.unconfirmedTransactionsLimitReached=你現在有過多的未確認交易。請稍後嘗試 shared.numItemsLabel=Number of entries: {0} shared.filter=篩選 shared.enabled=啟用 #################################################################### # UI views #################################################################### #################################################################### # MainView #################################################################### mainView.menu.market=交易項目 mainView.menu.buyXmr=買入 XMR mainView.menu.sellXmr=賣出 XMR mainView.menu.portfolio=業務 mainView.menu.funds=資金 mainView.menu.support=幫助 mainView.menu.settings=設置 mainView.menu.account=賬户 mainView.marketPriceWithProvider.label=交易所價格提供商:{0} mainView.marketPrice.havenoInternalPrice=最新 Haveno 交易的價格 mainView.marketPrice.tooltip.havenoInternalPrice=外部交易所供應商沒有可用的市場價格。\n顯示的價格是該貨幣的最新 Haveno 交易價格。 mainView.marketPrice.tooltip=交易所價格提供者 {0}{1}\n最後更新:{2}\n提供者節點 URL:{3} mainView.balance.available=可用餘額 mainView.balance.reserved=保證金 mainView.balance.pending=凍結餘額 mainView.balance.reserved.short=保證 mainView.balance.pending.short=凍結 mainView.footer.usingTor=(via Tor) mainView.footer.localhostMoneroNode=(本地主機) mainView.footer.clearnet=(via clearnet) mainView.footer.xmrInfo={0} {1} mainView.footer.xmrFeeRate=/ Fee rate: {0} sat/vB mainView.footer.xmrInfo.initializing=連接到 Haveno 網路 mainView.footer.xmrInfo.synchronizingWith=同步中,區塊:{1} / {2},與 {0} 同步中 mainView.footer.xmrInfo.connectedTo=已连接至 {0},在区块 {1} 处 mainView.footer.xmrInfo.synchronizingWalletWith=同步中,區塊:{1} / {2},與 {0} 的錢包同步中 mainView.footer.xmrInfo.syncedWith=已同步至 {0} 在區塊 {1} mainView.footer.xmrInfo.connectingTo=連接至 mainView.footer.xmrInfo.connectionFailed=連接失敗: mainView.footer.xmrPeers=Monero網絡節點:{0} mainView.footer.p2pPeers=Haveno 網絡節點:{0} mainView.bootstrapState.connectionToTorNetwork=(1/4) 連接至 Tor 網絡... mainView.bootstrapState.torNodeCreated=(2/4) Tor 節點已創建 mainView.bootstrapState.hiddenServicePublished=(3/4) 隱藏的服務已發佈 mainView.bootstrapState.initialDataReceived=(4/4) 初始數據已接收 mainView.bootstrapWarning.noSeedNodesAvailable=沒有可用的種子節點 mainView.bootstrapWarning.noNodesAvailable=沒有可用的種子節點和節點 mainView.bootstrapWarning.bootstrappingToP2PFailed=啟動 Haveno 網絡失敗 mainView.p2pNetworkWarnMsg.noNodesAvailable=沒有可用種子節點或永久節點可請求數據。\n請檢查您的互聯網連接或嘗試重啟應用程序。 mainView.p2pNetworkWarnMsg.connectionToP2PFailed=連接至 Haveno 網絡失敗(錯誤報吿:{0})。\n請檢查您的互聯網連接或嘗試重啟應用程序。 mainView.walletServiceErrorMsg.timeout=比特幣網絡連接超時。 mainView.walletServiceErrorMsg.connectionError=錯誤:{0} 比特幣網絡連接失敗。 mainView.walletServiceErrorMsg.rejectedTxException=交易被網絡拒絕。\n\n{0} mainView.networkWarning.allConnectionsLost=您失去了所有與 {0} 網絡節點的連接。\n您失去了互聯網連接或您的計算機處於待機狀態。 mainView.networkWarning.localhostMoneroLost=您丟失了與本地主機比特幣節點的連接。\n請重啟 Haveno 應用程序連接到其他比特幣節點或重新啟動主機比特幣節點。 mainView.version.update=(有更新可用) #################################################################### # MarketView #################################################################### market.tabs.offerBook=報價列表 market.tabs.spreadCurrency=Offers by Currency market.tabs.spreadPayment=Offers by Payment Method market.tabs.trades=行情圖 # OfferBookChartView market.offerBook.buyCrypto=我想要買入 {0}(賣出 {1}) market.offerBook.sellCrypto=我想要賣出 {0}(買入 {1}) market.offerBook.buyWithTraditional=購買 {0} market.offerBook.sellWithTraditional=出售 {0} market.offerBook.sellOffersHeaderLabel=出售 {0} 到 market.offerBook.buyOffersHeaderLabel=購買 {0} 以 market.offerBook.buy=我想要買入比特幣 market.offerBook.sell=我想要賣出比特幣 # SpreadView market.spread.numberOfOffersColumn=所有報價({0}) market.spread.numberOfBuyOffersColumn=買入 XMR({0}) market.spread.numberOfSellOffersColumn=賣出 XMR({0}) market.spread.totalAmountColumn=總共 XMR({0}) market.spread.spreadColumn=差價 market.spread.expanded=Expanded view # TradesChartsView market.trades.nrOfTrades=交易:{0} market.trades.tooltip.volumeBar=Volume: {0} / {1}\nNo. of trades: {2}\nDate: {3} market.trades.tooltip.candle.open=打開: market.trades.tooltip.candle.close=關閉: market.trades.tooltip.candle.high=高: market.trades.tooltip.candle.low=低: market.trades.tooltip.candle.average=平均: market.trades.tooltip.candle.median=調解員: market.trades.tooltip.candle.date=日期: market.trades.showVolumeInUSD=Show volume in USD #################################################################### # OfferView #################################################################### offerbook.createOffer=創建報價 offerbook.takeOffer=接受報價 offerbook.takeOfferToBuy=接受報價來收購 {0} offerbook.takeOfferToSell=接受報價來出售 {0} offerbook.takeOffer.enterChallenge=輸入報價密碼 offerbook.trader=商人 offerbook.offerersBankId=賣家的銀行 ID(BIC/SWIFT):{0} offerbook.offerersBankName=賣家的銀行名稱:{0} offerbook.offerersBankSeat=賣家的銀行所在國家或地區:{0} offerbook.offerersAcceptedBankSeatsEuro=接受的銀行所在國家(買家):所有歐元國家 offerbook.offerersAcceptedBankSeats=接受的銀行所在國家(買家):\n {0} offerbook.availableOffers=可用報價 offerbook.filterByCurrency=以貨幣篩選 offerbook.filterByPaymentMethod=以支付方式篩選 offerbook.matchingOffers=符合我的帳戶的報價 offerbook.filterNoDeposit=無押金 offerbook.noDepositOffers=無押金的報價(需要密碼短語) offerbook.timeSinceSigning=賬户信息 offerbook.timeSinceSigning.info=此賬户已驗證,{0} offerbook.timeSinceSigning.info.arbitrator=由仲裁員驗證,並可以驗證夥伴賬户 offerbook.timeSinceSigning.info.peer=由對方驗證,等待%d天限制被解除 offerbook.timeSinceSigning.info.peerLimitLifted=由對方驗證,限制被取消 offerbook.timeSinceSigning.info.signer=由對方驗證,並可驗證對方賬户(限制已取消) offerbook.timeSinceSigning.info.banned=賬户已被封禁 offerbook.timeSinceSigning.daysSinceSigning={0} 天 offerbook.timeSinceSigning.daysSinceSigning.long=自驗證{0} offerbook.xmrAutoConf=是否開啟自動確認 offerbook.buyXmrWith=購買 XMR 使用: offerbook.sellXmrFor=出售 XMR 以換取: offerbook.timeSinceSigning.help=當您成功地完成與擁有已驗證付款帳户的夥伴交易時,您的付款帳户已驗證。\n{0} 天后,最初的 {1} 的限制解除以及你的賬户可以驗證其他人的付款賬户。 offerbook.timeSinceSigning.notSigned=尚未驗證 offerbook.timeSinceSigning.notSigned.ageDays={0} 天 offerbook.timeSinceSigning.notSigned.noNeed=N/A shared.notSigned=此賬户還沒有被驗證以及在{0}前創建 shared.notSigned.noNeed=此賬户類型不適用驗證 shared.notSigned.noNeedDays=此賬户類型不適用驗證且在{0}天創建 shared.notSigned.noNeedAlts=數字貨幣不適用賬齡與簽名 offerbook.nrOffers=報價數量:{0} offerbook.volume={0}(最小 - 最大) offerbook.deposit=XMR 保證金(%) offerbook.deposit.help=交易雙方均已支付保證金確保這個交易正常進行。這會在交易完成時退還。 offerbook.createNewOffer=創建報價給 {0} {1} offerbook.createOfferToBuy=創建新的報價來買入 {0} offerbook.createOfferToSell=創建新的報價來賣出 {0} offerbook.createOfferToBuy.withTraditional=創建新的報價用 {1} 購買 {0} offerbook.createOfferToSell.forTraditional=創建新的報價以 {1} 出售 {0} offerbook.createOfferToBuy.withCrypto=創建新的賣出報價 {0} (買入 {1}) offerbook.createOfferToSell.forCrypto=創建新的買入報價 {0}(賣出 {1}) offerbook.takeOfferButton.tooltip=下單買入 {0} offerbook.yesCreateOffer=是的,創建報價 offerbook.setupNewAccount=設置新的交易賬户 offerbook.removeOffer.success=撤銷報價成功。 offerbook.removeOffer.failed=撤銷報價失敗:\n{0} offerbook.deactivateOffer.failed=報價停用失敗:\n{0} offerbook.activateOffer.failed=報價發佈失敗:\n{0} offerbook.withdrawFundsHint=您可以從 {0} 中撤回您支付的資金。 offerbook.warning.noTradingAccountForCurrency.headline=選擇的貨幣沒有支付賬户 offerbook.warning.noTradingAccountForCurrency.msg=您選擇的貨幣還沒有建立支付賬户。\n\n你想要用其他貨幣創建一個報價嗎? offerbook.warning.noMatchingAccount.headline=沒有匹配的支付賬户。 offerbook.warning.noMatchingAccount.msg=這個報價使用了您未創建過的支付方式。\n\n你現在想要創建一個新的支付賬户嗎? offerbook.warning.counterpartyTradeRestrictions=由於交易夥伴的交易限制,這個報價不能接受 offerbook.warning.newVersionAnnouncement=使用這個版本的軟件,交易夥伴可以驗證和驗證彼此的支付帳户,以創建一個可信的支付帳户網絡。\n\n交易成功後,您的支付帳户將被驗證以及交易限制將在一定時間後解除(此時間基於驗證方法)。\n\n有關驗證帳户的更多信息,請參見文檔 https://docs.haveno.exchange/payment-methods#account-signing popup.warning.tradeLimitDueAccountAgeRestriction.seller=基於以下標準的安全限制,允許的交易金額限制為 {0}:\n- 買方的帳目沒有由仲裁員或夥伴驗證\n- 買方帳户自驗證之日起不足30天\n- 本報價的付款方式被認為存在銀行退款的風險\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=基於以下標準的安全限制,允許的交易金額限制為{0}:\n- 你的買家帳户沒有由仲裁員或夥伴驗證\n- 自驗證你的帳户以來的時間少於30天\n- 本報價的付款方式被認為存在銀行退款的風險\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=這個付款方式暫時只限於 {0} 到 {1},因為所有買家都是新帳戶。\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=您的報價將僅限於已簽署且歷史悠久的帳戶購買,因為它超過了{0}。\n\n{1} offerbook.warning.wrongTradeProtocol=該報價要求的軟件版本與您現在運行的版本不一致。\n\n請檢查您是否運行最新版本,或者是該報價用户在使用一箇舊的版本。\n用户不能與不兼容的交易協議版本進行交易。 offerbook.warning.userIgnored=您已添加該用户的匿名地址在您的忽略列表裏。 offerbook.warning.offerBlocked=該報價被 Haveno 開發人員限制。\n接受該報價時,可能有一個未處理的漏洞導致了問題。 offerbook.warning.currencyBanned=該報價中使用的貨幣被 Haveno 開發人員阻止。\n請訪問 Haveno 論壇瞭解更多信息。 offerbook.warning.paymentMethodBanned=該報價中使用的付款方式被 Haveno 開發人員阻止。\n請訪問 Haveno 論壇瞭解更多信息。 offerbook.warning.nodeBlocked=該交易者的匿名地址被 Haveno 開發人員限制。\n當獲取來自該交易者的報價,可能有一個未處理的漏洞導致了問題。 offerbook.warning.requireUpdateToNewVersion=您的 Haveno 版本不再兼容交易。\n請通過 https://haveno.exchange/downloads 更新到最新的 Haveno 版本。 offerbook.warning.offerWasAlreadyUsedInTrade=您不能吃單因為您已經完成了該操作。可能是你之前的吃單嘗試導致了交易失敗。 offerbook.info.sellAtMarketPrice=您會以市場價格進行出售(每分鐘更新) offerbook.info.buyAtMarketPrice=您將以市場價格進行購買(每分鐘更新)。 offerbook.info.sellBelowMarketPrice=您將以低於市場價 {0} 的價格進行出售(每分鐘更新) offerbook.info.buyAboveMarketPrice=您將以高於市場價 {0} 的價格進行支付(每分鐘更新) offerbook.info.sellAboveMarketPrice=您將以高於市場價 {0} 的價格進行出售(每分鐘更新) offerbook.info.buyBelowMarketPrice=您將以低於市場價 {0} 的價格進行支付(每分鐘更新) offerbook.info.buyAtFixedPrice=您會以這個固定價格購買。 offerbook.info.sellAtFixedPrice=您會以這個固定價格出售。 offerbook.info.noArbitrationInUserLanguage=如有任何爭議,請注意此報價的仲裁將在 {0} 內處理。語言目前設置為{1}。 offerbook.info.roundedFiatVolume=金額四捨五入是為了增加您的交易隱私。 #################################################################### # Offerbook / Create offer #################################################################### createOffer.amount.prompt=輸入 XMR 數量 createOffer.price.prompt=輸入價格 createOffer.volume.prompt=輸入 {0} 金額 createOffer.amountPriceBox.amountDescription=比特幣數量 {0} createOffer.amountPriceBox.buy.volumeDescription=花費 {0} 數量 createOffer.amountPriceBox.sell.volumeDescription=接收 {0} 數量 createOffer.amountPriceBox.minAmountDescription=最小 XMR 數量 createOffer.securityDeposit.prompt=保證金 createOffer.fundsBox.title=為您的報價充值 createOffer.fundsBox.offerFee=掛單費 createOffer.fundsBox.networkFee=礦工手續費 createOffer.fundsBox.placeOfferSpinnerInfo=正在發佈報價中... createOffer.fundsBox.paymentLabel=Haveno 交易 ID {0} createOffer.fundsBox.fundsStructure=({0} 保證金,{1} 交易費,{2} 採礦費) createOffer.success.headline=您的报价已经创建 createOffer.success.info=你可以在“業務/未完成報價”頁面內管理您的未完成報價。 createOffer.info.sellAtMarketPrice=由於您的價格是持續更新的,因此您將始終以市場價格進行出售。 createOffer.info.buyAtMarketPrice=由於您的價格是持續更新的,因此您將始終以市場價格進行購買。 createOffer.info.sellAboveMarketPrice=由於您的價格是持續更新的,因此您將始終按照高於市場價 {0}% 的價格出售。 createOffer.info.buyBelowMarketPrice=由於您的價格是持續更新的,因此您將始終支付低於市場價 {0}% 的價格。 createOffer.warning.sellBelowMarketPrice=由於您的價格是持續更新的,因此您將始終按照低於市場價 {0}% 的價格出售。 createOffer.warning.buyAboveMarketPrice=由於您的價格是持續更新的,因此您將始終支付高於市場價 {0}% 的價格。 createOffer.tradeFee.descriptionXMROnly=掛單費 createOffer.tradeFee.descriptionBSQEnabled=選擇手續費幣種 createOffer.triggerPrice.prompt=Set optional trigger price createOffer.triggerPrice.label=Deactivate offer if market price is {0} createOffer.triggerPrice.tooltip=As protection against drastic price movements you can set a trigger price which deactivates the offer if the market price reaches that value. createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0} createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0} # new entries createOffer.placeOfferButton.buy=審核:建立以 {0} 買入 XMR 的報價 createOffer.placeOfferButton.sell=審核:建立以 {0} 賣出 XMR 的報價 createOffer.createOfferFundWalletInfo.headline=為您的報價充值 # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- 交易數量:{0}\n createOffer.createOfferFundWalletInfo.msg=您需要為此報價存入 {0}。\n\n\ 這些資金會保留在您的本地錢包中,並在有人接受您的報價後鎖定到多重簽名錢包中。\n\n\ 金額總和為:\n\ {1}\ - 您的保證金:{2}\n\ - 交易費:{3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=提交報價發生錯誤:\n\n{0}\n\n沒有資金從您錢包中扣除。\n請檢查您的互聯網連接或嘗試重啟應用程序。 createOffer.setAmountPrice=設置數量和價格 createOffer.warnCancelOffer=您已經為該報價充值了。如果您現在取消,您的資金將保留在您的本地 Haveno 錢包,並可以在“資金/提現”界面進行提現。您確定要取消嗎? createOffer.timeoutAtPublishing=發佈報價時產生了一個錯誤。 createOffer.errorInfo=\n\n掛單費已經支付,在最壞的情況下,你會失去了這筆費用。 我們很抱歉,但請記住,這是一個很小的數量。\n請嘗試重新啟動應用程序並檢查您的網絡連接,看看是否可以解決問題。 createOffer.tooLowSecDeposit.warning=您設置的保證金低於推薦默認值 {0}。\n您確定要使用較低的保證金嗎? createOffer.tooLowSecDeposit.makerIsSeller=在交易對手不遵循交易協議時,這給予您較少的保護。 createOffer.tooLowSecDeposit.makerIsBuyer=對於遵守交易協議的交易對象,您的保護金額較低,因為風險存款較小。 其他用户可能更喜歡採取其他報價,而不是您的。 createOffer.resetToDefault=不,恢復默認值 createOffer.useLowerValue=是的,使用我較低的值 createOffer.priceOutSideOfDeviation=您輸入的價格超過了市場價差價的最大值。\n最大值為 {0},您可以在偏好中進行調整。 createOffer.changePrice=改變價格 createOffer.tac=發佈該報價,我同意與滿足該條件的任何交易者進行交易。 createOffer.currencyForFee=掛單費 createOffer.setDeposit=設置買家的保證金(%) createOffer.setDepositAsBuyer=設置自己作為買家的保證金(%) createOffer.setDepositForBothTraders=設置雙方的保證金比例(%) createOffer.securityDepositInfo=您的買家的保證金將會是 {0} createOffer.securityDepositInfoAsBuyer=您作為買家的保證金將會是 {0} createOffer.minSecurityDepositUsed=最低保證金已使用 createOffer.buyerAsTakerWithoutDeposit=買家無需支付保證金(通行密碼保護) createOffer.myDeposit=我的保證金(%) createOffer.myDepositInfo=您的保證金將為 {0} #################################################################### # Offerbook / Take offer #################################################################### takeOffer.amount.prompt=輸入 XMR 數量 takeOffer.amountPriceBox.buy.amountDescription=賣出比特幣數量 takeOffer.amountPriceBox.sell.amountDescription=買入比特幣數量 takeOffer.amountPriceBox.priceDescription=每個比特幣的 {0} 價格 takeOffer.amountPriceBox.amountRangeDescription=可用數量範圍 takeOffer.amountPriceBox.warning.invalidXmrDecimalPlaces=你輸入的數量超過允許的小數位數。\n數量已被調整為4位小數。 takeOffer.validation.amountSmallerThanMinAmount=數量不能比報價內設置的最小數量小。 takeOffer.validation.amountLargerThanOfferAmount=數量不能比報價提供的總量大。 takeOffer.validation.amountLargerThanOfferAmountMinusFee=該輸入數量可能會給賣家造成比特幣碎片。 takeOffer.fundsBox.title=為交易充值 takeOffer.fundsBox.isOfferAvailable=檢查報價是否有效... takeOffer.fundsBox.tradeAmount=賣出數量 takeOffer.fundsBox.offerFee=掛單費 takeOffer.fundsBox.networkFee=總共挖礦手續費 takeOffer.fundsBox.takeOfferSpinnerInfo=接受報價:{0} takeOffer.fundsBox.paymentLabel=Haveno 交易 ID {0} takeOffer.fundsBox.fundsStructure=({0} 保證金,{1} 交易費,{2} 採礦費) takeOffer.fundsBox.noFundingRequiredTitle=無需資金 takeOffer.fundsBox.noFundingRequiredDescription=從賣家那裡在 Haveno 之外獲取優惠密碼以接受此優惠。 takeOffer.success.headline=你已成功下單一個報價。 takeOffer.success.info=你可以在“業務/未完成交易”頁面內查看您的未完成交易。 takeOffer.error.message=下單時發生了一個錯誤。\n\n{0} # new entries takeOffer.takeOfferButton.buy=審核:接受以 {0} 買入 XMR 的報價 takeOffer.takeOfferButton.sell=審核:接受以 {0} 賣出 XMR 的報價 takeOffer.noPriceFeedAvailable=您不能對這筆報價下單,因為它使用交易所價格百分比定價,但是您沒有獲得可用的價格。 takeOffer.takeOfferFundWalletInfo.headline=為交易充值 # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- 交易數量:{0}\n takeOffer.takeOfferFundWalletInfo.msg=您需要存入 {0} 才能接受此報價。\n\n該金額是以下總和:\n{1}- 您的保證金:{2}\n- 交易費用:{3} takeOffer.alreadyPaidInFunds=如果你已經支付,你可以在“資金/提現”提現它。 takeOffer.paymentInfo=付款信息 takeOffer.setAmountPrice=設置數量 takeOffer.alreadyFunded.askCancel=您已經為該報價充值了。如果您現在取消,您的資金將保留在您的本地 Haveno 錢包,並可以在“資金/提現”界面進行提現。您確定要取消嗎? takeOffer.failed.offerNotAvailable=請求失敗,由於報價不再可用。 也許有交易者在此期間已經下單。 takeOffer.failed.offerTaken=您不能對該報價下單,因為該報價已經被其他交易者下單。 takeOffer.failed.offerRemoved=您不能對該報價下單,因為該報價已經在此期間被刪除。 takeOffer.failed.offererNotOnline=下單失敗,因為賣家已經不在線。 takeOffer.failed.offererOffline=您不能下單,因為賣家已經下線。 takeOffer.warning.connectionToPeerLost=您與賣家失去連接。\n因為太多連接,他或許已經下線或者關掉了與您的連接。\n\n如果您還是能在報價列表中看到他的報價,您可以再次嘗試下單。 takeOffer.error.noFundsLost=\n\n你的錢包裏還沒有錢。 \n請嘗試重啟您的應用程序或者檢查您的網絡連接。 # suppress inspection "TrailingSpacesInProperty" takeOffer.error.feePaid=\n!\n takeOffer.error.depositPublished=\n\n您的保證金轉賬已經發布。\n請嘗試重啟您的應用程序或者檢查您的網絡連接。\n如果始終存在問題,請到幫助界面聯繫開發者。 takeOffer.error.payoutPublished=\n\n您的支付轉賬已經發布。\n請嘗試重啟您的應用程序或者檢查您的網絡連接。\n如果始終存在問題,請到幫助界面聯繫開發者。 takeOffer.tac=接受該報價,意味着我同意這交易界面中的條件。 #################################################################### # Offerbook / Edit offer #################################################################### openOffer.header.triggerPrice=觸發價格 openOffer.triggerPrice=Trigger price {0} openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\nPlease edit the offer to define a new trigger price editOffer.setPrice=設定價格 editOffer.confirmEdit=確認:編輯報價 editOffer.publishOffer=發佈您的報價。 editOffer.failed=報價編輯失敗:\n{0} editOffer.success=您的報價已成功編輯。 editOffer.invalidDeposit=買方保證金不符合 Haveno DAO 規定,不能再次編輯。 #################################################################### # Portfolio #################################################################### portfolio.tab.openOffers=我的未完成報價 portfolio.tab.pendingTrades=未完成交易 portfolio.tab.history=歷史記錄 portfolio.tab.failed=失敗 portfolio.tab.editOpenOffer=編輯報價 portfolio.closedTrades.deviation.help=Percentage price deviation from market portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} portfolio.pending.syncing=同步交易錢包 portfolio.pending.syncing.blockRemaining=同步交易錢包 — 剩餘 1 個區塊 portfolio.pending.syncing.blocksRemaining=同步交易錢包 — 剩餘 {0} 個區塊 portfolio.pending.step1.waitForConf=等待區塊鏈確認 portfolio.pending.step2_buyer.additionalConf=存款已達 10 次確認。\n為了額外安全,我們建議在發送付款前等待 {0} 次確認。\n提前操作風險自負。 portfolio.pending.step2_buyer.startPayment=開始付款 portfolio.pending.step2_seller.waitPaymentSent=等待直到付款 portfolio.pending.step3_buyer.waitPaymentArrived=等待直到付款到達 portfolio.pending.step3_seller.confirmPaymentReceived=確定收到付款 portfolio.pending.step5.completed=完成 portfolio.pending.step3_seller.autoConf.status.label=自動確認狀態。 portfolio.pending.autoConf=自動確認 portfolio.pending.autoConf.blocks=XMR 確認數:{0} / 需求量:{2} portfolio.pending.autoConf.state.xmr.txKeyReused=交易密鑰已重複使用。請發起糾紛處理。 portfolio.pending.autoConf.state.confirmations=XMR 確認:{0}/{1} portfolio.pending.autoConf.state.txNotFound=交易並未在內存池中檢索。 portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=無有效交易 ID / 交易密鑰 portfolio.pending.autoConf.state.filterDisabledFeature=由開發者禁用 # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FEATURE_DISABLED=自動確認功能已禁用。{0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.TRADE_LIMIT_EXCEEDED=交易金額超過自動確認金額限制。 # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.INVALID_DATA=對等點提供不可用數據。{0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=支付交易已經發布 # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.DISPUTE_OPENED=已發起糾紛。該交易的自動確認已被禁用 # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.REQUESTS_STARTED=交易證明申請已經開始 # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PENDING=成功結果:{0}/{1} ;{2} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.COMPLETED=所有服務都已被證明。 # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.ERROR=您請求的服務發生了錯誤。沒有自動確認。 # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=服務返回失敗。沒有自動確認。 portfolio.pending.step1.info.you=存款交易已發布。\n在獲得 10 次確認後,您即可開始付款(約 {0} 分鐘後)。 portfolio.pending.step1.info.buyer=存款交易已發布。\n在獲得 10 次確認後,XMR 買方即可開始付款(約 {0} 分鐘後)。 portfolio.pending.step1.warn=保證金交易仍未得到確認。這種情況可能會發生在外部錢包轉賬時使用的交易手續費用較低造成的。 portfolio.pending.step1.openForDispute=保證金交易仍未得到確認。請聯繫調解員協助。 # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2.confReached=Your trade has reached at least one blockchain confirmation.\n\n portfolio.pending.step2_buyer.refTextWarn=Important: when making the payment, leave the \"reason for payment\" field empty. DO NOT put the trade ID or any other text like 'monero', 'XMR', or 'Haveno'. You are free to discuss via trader chat if an alternate \"reason for payment\" would be suitable to you both. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees=If your bank charges you any fees to make the transfer, you are responsible for paying those fees. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.crypto=請從您的外部 {0} 錢包劃轉\n{1} 到 XMR 賣家。\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.cash=請到銀行並支付 {0} 給 XMR 賣家。\n\n portfolio.pending.step2_buyer.cash.extra=重要要求:\n完成付款後在紙質收據上寫下:不退款。\n然後將其撕成2份,拍照片併發送給 XMR 賣家的電子郵件地址。 # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.moneyGram=請使用 MoneyGram 向 XMR 賣家支付 {0}。\n\n portfolio.pending.step2_buyer.moneyGram.extra=重要要求:\n完成支付後,請通過電郵發送授權編號和照片給 XMR 賣家。\n收據必須清楚地向賣家寫明您的全名、城市、國家或地區、數量。賣方的電子郵件是:{0}。 # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.westernUnion=請使用 Western Union 向 XMR 賣家支付 {0}。\n\n portfolio.pending.step2_buyer.westernUnion.extra=重要要求:\n完成支付後,請通過電郵發送 MTCN(追蹤號碼)和照片給 XMR 賣家。\n收據必須清楚地向賣家寫明您的全名、城市、國家或地區、數量。賣方的電子郵件是:{0}。 # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=請用“美國郵政匯票”發送 {0} 給 XMR 賣家。\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=請通過提供的聯繫人與 XMR 賣家聯繫,並安排會議支付 {0}。\n\n portfolio.pending.step2_buyer.startPaymentUsing=使用 {0} 開始付款 portfolio.pending.step2_buyer.recipientsAccountData=接受 {0} portfolio.pending.step2_buyer.amountToTransfer=劃轉數量 portfolio.pending.step2_buyer.sellersAddress=賣家的 {0} 地址 portfolio.pending.step2_buyer.buyerAccount=您的付款帳户將被使用 portfolio.pending.step2_buyer.paymentSent=付款開始 portfolio.pending.step2_buyer.showEarly=提前顯示付款詳情 portfolio.pending.step2_buyer.warn=你還沒有完成你的 {0} 付款!\n請注意,交易必須在 {1} 之前完成。 portfolio.pending.step2_buyer.openForDispute=您還沒有完成您的付款!\n最大交易期限已過。請聯繫調解員尋求幫助。 portfolio.pending.step2_buyer.paperReceipt.headline=您是否將紙質收據發送給 XMR 賣家? portfolio.pending.step2_buyer.paperReceipt.msg=請牢記:\n完成付款後在紙質收據上寫下:不退款。\n然後將其撕成2份,拍照片併發送給 XMR 賣家的電子郵件地址。 portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=發送授權編號和收據 portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=請通過電郵發送授權編號和照片給 XMR 賣家。\n收據必須清楚地向賣家寫明您的全名、城市、國家或地區、數量。賣方的電子郵件是:{0}。\n\n您把授權編號和合同發給賣方了嗎? portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=發送 MTCN 和收據 portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=請通過電郵發送 MTCN(追蹤號碼)和照片給 XMR 賣家。\n收據必須清楚地向賣家寫明您的全名、城市、國家或地區、數量。賣方的電子郵件是:{0}。\n\n您把 MTCN 和合同發給賣方了嗎? portfolio.pending.step2_buyer.halCashInfo.headline=請發送 HalCash 代碼 portfolio.pending.step2_buyer.halCashInfo.msg=您需要向 XMR 賣家發送帶有 HalCash 代碼和交易 ID({0})的文本消息。\n\n賣方的手機號碼是 {1} 。\n\n您是否已經將代碼發送至賣家? portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=有些銀行可能會要求接收方的姓名。在較舊的 Haveno 客户端創建的快速支付帳户沒有提供收款人的姓名,所以請使用交易聊天來獲得收款人姓名(如果需要)。 portfolio.pending.step2_buyer.confirmStart.headline=確定您已經付款 portfolio.pending.step2_buyer.confirmStart.msg=您是否向您的交易夥伴發起 {0} 付款? portfolio.pending.step2_buyer.confirmStart.yes=是的,我已經開始付款 portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=你沒有提供任何付款證明 portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=您還沒有輸入交易 ID 以及交易密鑰\n\n如果不提供此數據您的交易夥伴無法在收到 XMR 後使用自動確認功能以快速釋放 XMR。\n另外,Haveno 要求 XMR 發送者在發生糾紛的時候能夠向調解員和仲裁員提供這些信息。\n更多細節在 Haveno Wiki:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=輸入並不是一個 32 字節的哈希值 portfolio.pending.step2_buyer.confirmStart.warningButton=忽略並繼續 portfolio.pending.step2_seller.waitPayment.headline=等待付款 portfolio.pending.step2_seller.f2fInfo.headline=買家的合同信息 portfolio.pending.step2_seller.waitPayment.msg=存款交易至少有一個區塊鏈確認。\n您需要等到 XMR 買家開始 {0} 付款。 portfolio.pending.step2_seller.warn=XMR 買家仍然沒有完成 {0} 付款。\n你需要等到他開始付款。\n如果 {1} 交易尚未完成,仲裁員將進行調查。 portfolio.pending.step2_seller.openForDispute=XMR 買家尚未開始付款!\n允許的最長交易期限已經過去了。你可以繼續等待給予交易雙方更多時間,或聯繫仲裁員以爭取解決糾紛。 tradeChat.chatWindowTitle=使用 ID “{0}” 進行交易的聊天窗口 tradeChat.openChat=打開聊天窗口 tradeChat.rules=您可以與您的夥伴溝通,以解決該交易的潛在問題。\n在聊天中不強制回覆。\n如果交易員違反了下面的任何規則,打開糾紛並向調解員或仲裁員報吿。\n聊天規則:\n\n\t●不要發送任何鏈接(有惡意軟件的風險)。您可以發送交易 ID 和區塊資源管理器的名稱。\n\t●不要發送還原密鑰、私鑰、密碼或其他敏感信息!\n\t●不鼓勵 Haveno 以外的交易(無安全保障)。\n\t●不要參與任何形式的危害社會安全的計劃。\n\t●如果對方沒有迴應,也不願意通過聊天進行溝通,那就尊重對方的決定。\n\t●將談話範圍限制在行業內。這個聊天不是一個社交軟件替代品或troll-box。\n\t●保持友好和尊重的交談。 # suppress inspection "UnusedProperty" message.state.UNDEFINED=未定義 # suppress inspection "UnusedProperty" message.state.SENT=發出信息 # suppress inspection "UnusedProperty" message.state.ARRIVED=消息已抵達 # suppress inspection "UnusedProperty" message.state.STORED_IN_MAILBOX=已發送但尚未被對方接收的付款信息 # suppress inspection "UnusedProperty" message.state.ACKNOWLEDGED=對方確認消息回執 # suppress inspection "UnusedProperty" message.state.FAILED=發送消息失敗 portfolio.pending.step3_buyer.wait.headline=等待 XMR 賣家付款確定 portfolio.pending.step3_buyer.wait.info=等待 XMR 賣家確認收到 {0} 付款。 portfolio.pending.step3_buyer.wait.msgStateInfo.label=支付開始消息狀態 portfolio.pending.step3_buyer.warn.part1a=在 {0} 區塊鏈 portfolio.pending.step3_buyer.warn.part1b=在您的支付供應商(例如:銀行) portfolio.pending.step3_buyer.warn.part2=XMR 賣家仍然沒有確認您的付款。如果付款發送成功,請檢查 {0}。 portfolio.pending.step3_buyer.openForDispute=XMR 賣家還沒有確認你的付款!最大交易期限已過。您可以等待更長時間,並給交易夥伴更多時間或請求調解員的幫助。 # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.part=您的交易夥伴已經確認他們已經發起了 {0} 付款。\n\n portfolio.pending.step3_seller.crypto.explorer=在您最喜歡的 {0} 區塊鏈瀏覽器 portfolio.pending.step3_seller.crypto.wallet=在您的 {0} 錢包 portfolio.pending.step3_seller.crypto={0} 請檢查 {1} 是否交易已經到您的接收地址\n{2}\n已經有足夠的區塊鏈確認了\n支付金額必須為 {3}\n\n關閉該彈出窗口後,您可以從主界面複製並粘貼 {4} 地址。 portfolio.pending.step3_seller.postal={0}Please check if you have received {1} with \"US Postal Money Order\" from the XMR buyer. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.payByMail={0}Please check if you have received {1} with \"Pay by Mail\" from the XMR buyer. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.bank=Your trading partner has confirmed that they have initiated the {0} payment.\n\nPlease go to your online banking web page and check if you have received {1} from the XMR buyer. portfolio.pending.step3_seller.cash=因為付款是通過現金存款完成的,XMR 買家必須在紙質收據上寫“不退款”,將其撕成2份,並通過電子郵件向您發送照片。\n\n為避免退款風險,請僅確認您是否收到電子郵件,如果您確定收據有效。\n如果您不確定,{0} portfolio.pending.step3_seller.moneyGram=買方必須發送授權編碼和一張收據的照片。\n收據必須清楚地顯示您的全名、城市、國家或地區、數量。如果您收到授權編碼,請查收郵件。\n\n關閉彈窗後,您將看到 XMR 買家的姓名和在 MoneyGram 的收款地址。\n\n只有在您成功收到錢之後,再確認收據! portfolio.pending.step3_seller.westernUnion=買方必須發送 MTCN(跟蹤號碼)和一張收據的照片。\n收據必須清楚地顯示您的全名、城市、國家或地區、數量。如果您收到 MTCN,請查收郵件。\n\n關閉彈窗後,您將看到 XMR 買家的姓名和在 Western Union 的收款地址。\n\n只有在您成功收到錢之後,再確認收據! portfolio.pending.step3_seller.halCash=買方必須將 HalCash代碼 用短信發送給您。除此之外,您將收到來自 HalCash 的消息,其中包含從支持 HalCash 的 ATM 中提取歐元所需的信息\n從 ATM 取款後,請在此確認付款收據! portfolio.pending.step3_seller.amazonGiftCard=XMR 買家已經發送了一張亞馬遜電子禮品卡到您的郵箱或手機短信。請現在立即兑換亞馬遜電子禮品卡到您的亞馬遜賬户中以及確認交易信息。 portfolio.pending.step3_seller.bankCheck=\n\n還請確認您的銀行對帳單中的發件人姓名與委託合同中的發件人姓名相符:\n發件人姓名:{0}\n\n如果名稱與此處顯示的名稱不同,則 {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=請不要確認,而是通過鍵盤組合鍵“alt + o”或“option + o”來打開糾紛。 portfolio.pending.step3_seller.confirmPaymentReceipt=確定付款收據 portfolio.pending.step3_seller.amountToReceive=接收數量: portfolio.pending.step3_seller.yourAddress=您的 {0} 地址 portfolio.pending.step3_seller.buyersAddress=賣家的 {0} 地址 portfolio.pending.step3_seller.yourAccount=您的交易賬户 portfolio.pending.step3_seller.xmrTxHash=交易記錄 ID portfolio.pending.step3_seller.xmrTxKey=交易密鑰 portfolio.pending.step3_seller.buyersAccount=買方賬號數據 portfolio.pending.step3_seller.confirmReceipt=確定付款收據 portfolio.pending.step3_seller.buyerStartedPayment=XMR 買家已經開始 {0} 的付款。\n{1} portfolio.pending.step3_seller.buyerStartedPayment.crypto=檢查您的數字貨幣錢包或塊瀏覽器的區塊鏈確認,並確認付款時,您有足夠的塊鏈確認。 portfolio.pending.step3_seller.buyerStartedPayment.traditional=檢查您的交易賬户(例如銀行帳户),並確認您何時收到付款。 portfolio.pending.step3_seller.warn.part1a=在 {0} 區塊鏈 portfolio.pending.step3_seller.warn.part1b=在您的支付供應商(例如:銀行) portfolio.pending.step3_seller.warn.part2=你還沒有確認收到款項。如果您已經收到款項,請檢查 {0}。 portfolio.pending.step3_seller.openForDispute=您尚未確認付款的收據!\n最大交易期已過\n請確認或請求調解員的協助。 # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=您是否收到了您交易夥伴的 {0} 付款?\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.name=還請確認您的銀行對帳單中的發件人姓名與委託合同中的發件人姓名相符:\n每個交易合約的發送者姓名:{0}\n\n如果名稱與此處顯示的名稱不一致,請不要通過確認付款,而是通過“alt + o”或“option + o”打開糾紛。\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=請注意,一旦您確認收到,凍結交易金額將被髮放給 XMR 買家,保證金將被退還。 portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=確定您已經收到付款 portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=是的,我已經收到付款。 portfolio.pending.step3_seller.onPaymentReceived.signer=重要提示:通過確認收到付款,你也驗證了對方的賬户,並獲得驗證。因為對方的賬户還沒有驗證,所以你應該儘可能的延遲付款的確認,以減少退款的風險。 portfolio.pending.step5_buyer.groupTitle=完成交易的概要 portfolio.pending.step5_buyer.tradeFee=掛單費 portfolio.pending.step5_buyer.makersMiningFee=礦工手續費 portfolio.pending.step5_buyer.takersMiningFee=總共挖礦手續費 portfolio.pending.step5_buyer.refunded=退還保證金 portfolio.pending.step5_buyer.withdrawXMR=提現您的比特幣 portfolio.pending.step5_buyer.amount=提現數量 portfolio.pending.step5_buyer.withdrawToAddress=提現地址 portfolio.pending.step5_buyer.moveToHavenoWallet=在 Haveno 錢包中保留資金 portfolio.pending.step5_buyer.withdrawExternal=提現到外部錢包 portfolio.pending.step5_buyer.alreadyWithdrawn=您的資金已經提現。\n請查看交易歷史記錄。 portfolio.pending.step5_buyer.confirmWithdrawal=確定提現請求 portfolio.pending.step5_buyer.amountTooLow=轉讓金額低於交易費用和最低可能的tx值(零頭)。 portfolio.pending.step5_buyer.withdrawalCompleted.headline=提現完成 portfolio.pending.step5_buyer.withdrawalCompleted.msg=您完成的交易存儲在“業務/歷史記錄”下。\n您可以查看“資金/交易”下的所有比特幣交易 portfolio.pending.step5_buyer.bought=您已經買入 portfolio.pending.step5_buyer.paid=您已經支付 portfolio.pending.step5_seller.sold=您已經賣出 portfolio.pending.step5_seller.received=您已經收到 tradeFeedbackWindow.title=恭喜您完成交易 tradeFeedbackWindow.msg.part1=我們很想聽聽您的體驗如何。這將幫助我們改進軟件,優化體驗不好的地方。如欲提供意見,請填寫這份簡短的問卷(無需註冊),網址: tradeFeedbackWindow.msg.part2=如果您有任何疑問或遇到任何問題,請通過 Haveno 論壇與其他用户和貢獻者聯繫: tradeFeedbackWindow.msg.part3=感謝使用 Haveno portfolio.pending.role=我的角色 portfolio.pending.tradeInformation=交易信息 portfolio.pending.remainingTime=剩餘時間 portfolio.pending.remainingTimeDetail={0}(直到 {1} ) portfolio.pending.remainingTimeDetail.startsAfter=在確認數達到 {0} 之後開始 portfolio.pending.tradePeriodInfo=在獲得 {0} 次確認後,交易期限將開始。根據所使用的付款方式,將適用不同的最長允許交易期限。 portfolio.pending.tradePeriodWarning=如果超過了這個週期,雙方均可以提出糾紛。 portfolio.pending.tradeNotCompleted=交易不會及時完成(直到 {0} ) portfolio.pending.tradeProcess=交易流程 portfolio.pending.openAgainDispute.msg=如果您不確定發送給調解員或仲裁員的消息是否已送達(例如,如果您在1天后沒有收到回覆),請放心使用 Cmd/Ctrl+o 再次打開糾紛。你也可以在 Haveno 論壇上尋求額外的幫助,網址是 https://haveno.community。 portfolio.pending.openAgainDispute.button=再次出現糾紛 portfolio.pending.openSupportTicket.headline=創建幫助話題 portfolio.pending.openSupportTicket.msg=請僅在緊急情況下使用此功能,如果您沒有看到“提交支持”或“提交糾紛”按鈕。\n\n當您發出工單時,交易將被中斷並由調解員或仲裁員進行處理。 portfolio.pending.timeLockNotOver=你必須等到≈{0}(還需等待{1}個區塊)才能提交糾紛。 portfolio.pending.error.depositTxNull=保證金交易無效。沒有有效的保證金交易,你使用創建糾紛。請到“設置/網絡信息”進行 SPV 重新同步。\n \n如需更多幫助,請聯繫 Haveno Keybase 團隊的 Support 頻道。 portfolio.pending.mediationResult.error.depositTxNull=保證金交易為空。你可以移動該交易至失敗的交易。 portfolio.pending.mediationResult.error.delayedPayoutTxNull=延遲支付交易為空。你可以移動該交易至失敗的交易。 portfolio.pending.error.depositTxNotConfirmed=保證金交易未確認。未經確認的存款交易不能發起糾紛或仲裁請求。請耐心等待,直到它被確認或進入“設置/網絡信息”進行 SPV 重新同步。\n\n如需更多幫助,請聯繫 Haveno Keybase 團隊的 Support 頻道。 portfolio.pending.support.headline.getHelp=需要幫助? portfolio.pending.support.text.getHelp=如果您有任何問題,您可以嘗試在交易聊天中聯繫交易夥伴,或在 https://haveno.community 詢問 Haveno 社區。如果您的問題仍然沒有解決,您可以向調解員取得更多的幫助。 portfolio.pending.support.button.getHelp=開啟交易聊天 portfolio.pending.support.headline.halfPeriodOver=確認付款 portfolio.pending.support.headline.periodOver=交易期結束 portfolio.pending.support.headline.depositTxMissing=缺少存款交易 portfolio.pending.support.depositTxMissing=此交易缺少存款。請開啟支援工單以聯絡仲裁者協助處理。 portfolio.pending.mediationRequested=已請求調解員協助 portfolio.pending.refundRequested=已請求退款 portfolio.pending.openSupport=創建幫助話題 portfolio.pending.supportTicketOpened=幫助話題已經創建 portfolio.pending.communicateWithArbitrator=請在“幫助”界面上與仲裁員聯繫。 portfolio.pending.communicateWithMediator=請在“支持”頁面中與調解員進行聯繫。 portfolio.pending.disputeOpenedByUser=您創建了一個糾紛。\n{0} portfolio.pending.disputeOpenedByPeer=您的交易對象創建了一個糾紛。\n{0} portfolio.pending.noReceiverAddressDefined=沒有定義接收地址 portfolio.pending.mediationResult.headline=調解費用的支出 portfolio.pending.mediationResult.info.noneAccepted=通過接受調解員關於交易的建議的支出來完成交易。 portfolio.pending.mediationResult.info.selfAccepted=你已經接受了調解員的建議。等待夥伴接受。 portfolio.pending.mediationResult.info.peerAccepted=你的夥伴已經接受了調解員的建議。你也接受嗎? portfolio.pending.mediationResult.button=查看建議的解決方案 portfolio.pending.mediationResult.popup.headline=調解員在交易 ID:{0}上的建議 portfolio.pending.mediationResult.popup.headline.peerAccepted=你的夥伴已經接受了調解員的建議 portfolio.pending.mediationResult.popup.info=調解員建議的支出如下:\n你將支付:{0}\n你的交易夥伴將支付:{1}\n\n你可以接受或拒絕這筆調解費支出。\n\n通過接受,你驗證了合約的支付交易。如果你的交易夥伴也接受和驗證,支付將完成,交易將關閉。\n\n如果你們其中一人或雙方都拒絕該建議,你將必須等到(2)({3}區塊)與仲裁員展開第二輪糾紛討論,仲裁員將再次調查該案件,並根據他們的調查結果進行支付。\n\n仲裁員可以收取少量費用(費用上限:交易的保證金)作為其工作的補償。兩個交易者都同意調解員的建議是愉快的路徑請求仲裁是針對特殊情況的,比如如果一個交易者確信調解員沒有提出公平的賠償建議(或者如果另一個同伴沒有迴應)。\n\n關於新的仲裁模型的更多細節:https://docs.haveno.exchange/trading-rules.html#arbitration portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=您已經接受了調解員的建議支付但是似乎您的交易對手並沒有接受。\n\n一旦鎖定時間到{0}(區塊{1})您可以打開第二輪糾紛讓仲裁員重新研究該案件並重新作出支出決定。\n\n您可以找到更多關於仲裁模型的信息在:\nhttps://docs.haveno.exchange/trading-rules.html#arbitration portfolio.pending.mediationResult.popup.openArbitration=拒絕並請求仲裁 portfolio.pending.mediationResult.popup.alreadyAccepted=您已經接受了。 portfolio.pending.failedTrade.taker.missingTakerFeeTx=吃單交易費未找到。\n\n如果沒有 tx,交易不能完成。沒有資金被鎖定以及沒有支付交易費用。你可以將交易移至失敗的交易。 portfolio.pending.failedTrade.maker.missingTakerFeeTx=掛單費交易未找到。\n\n如果沒有 tx,交易不能完成。沒有資金被鎖定以及沒有支付交易費用。你可以將交易移至失敗的交易。 portfolio.pending.failedTrade.missingDepositTx=缺少一筆保證金交易。\n\n此交易是完成交易所必需的。請確保您的錢包已與 Monero 區塊鏈完全同步。\n\n您可以將此交易移至「失敗的交易」區段以停用它。 portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=延遲支付交易缺失,但是資金仍然被鎖定在保證金交易中。\n\n請不要給比特幣賣家發送法幣或數字貨幣,因為沒有延遲交易 tx,不能開啟仲裁。使用 Cmd/Ctrl+o開啟調解協助。調解員應該建議交易雙方分別退回全部的保證金(賣方支付的交易金額也會全數返還)。這樣的話不會有任何的安全問題只會損失交易手續費。\n\n你可以在這裏為失敗的交易提出賠償要求:https://github.com/haveno-dex/haveno/issues portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=延遲支付交易確實但是資金仍然被鎖定在保證金交易中。\n\n如果賣家仍然缺失延遲支付交易,他會接到請勿付款的指示並開啟一個調節幫助。你也應該使用 Cmd/Ctrl+O 去打開一個調節協助\n\n如果買家還沒有發送付款,調解員應該會建議交易雙方分別退回全部的保證金(賣方支付的交易金額也會全數返還)。否則交易額應該判給買方。\n\n你可以在這裏為失敗的交易提出賠償要求:https://github.com/haveno-dex/haveno/issues portfolio.pending.failedTrade.errorMsgSet=在處理交易協議是發生了一個錯誤\n\n錯誤:{0}\n\n這應該不是致命錯誤,您可以正常的完成交易。如果你仍擔憂,打開一個調解協助並從 Haveno 調解員處得到建議。\n\n如果這個錯誤是致命的那麼這個交易就無法完成,你可能會損失交易費。可以在這裏為失敗的交易提出賠償要求:https://github.com/haveno-dex/haveno/issues portfolio.pending.failedTrade.missingContract=沒有設置交易合同。\n\n這個交易無法完成,你可能會損失交易手續費。可以在這裏為失敗的交易提出賠償要求:https://github.com/haveno-dex/haveno/issues portfolio.pending.failedTrade.info.popup=交易協議出現了問題。\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=交易協議出現了嚴重問題。\n\n{0}\n\n您確定想要將該交易移至失敗的交易嗎?\n\n您不能在失敗的交易中打開一個調解或仲裁,但是你隨時可以將失敗的交易重新移至未完成交易。 portfolio.pending.failedTrade.txChainValid.moveToFailed=這個交易協議存在一些問題。\n\n{0}\n\n這個報價交易已經被髮布以及資金已被鎖定。只有在確定情況下將該交易移至失敗交易。這可能會阻止解決問題的可用選項。\n\n您確定想要將該交易移至失敗的交易嗎?\n\n您不能在失敗的交易中打開一個調解或仲裁,但是你隨時可以將失敗的交易重新移至未完成交易。 portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=將交易移至失敗交易 portfolio.pending.failedTrade.warningIcon.tooltip=點擊打開該交易的問題細節 portfolio.failed.revertToPending.popup=您想要將該交易移至未完成交易嗎 portfolio.failed.revertToPending=將交易移至未完成交易 portfolio.closed.completed=完成 portfolio.closed.ticketClosed=已仲裁 portfolio.closed.mediationTicketClosed=已調解 portfolio.closed.canceled=已取消 portfolio.failed.Failed=失敗 portfolio.failed.unfail=再繼續之前,請保證你有一份根目錄的備份!\n您想要將此交易移至未完成的交易嗎?\n這是一個解鎖卡在失敗交易的資金的方法 portfolio.failed.cantUnfail=目前該交易暫無法移至未完成的交易。\n請在完成交易後重試{0} portfolio.failed.depositTxNull=交易無法恢復至未完成交易。保證金交易為空。 portfolio.failed.delayedPayoutTxNull=交易無法恢復至未完成交易。延遲支付交易為空。 #################################################################### # Funds #################################################################### funds.tab.deposit=存款 funds.tab.withdrawal=提現 funds.tab.reserved=保證金 funds.tab.locked=凍結資金 funds.tab.transactions=交易記錄 funds.deposit.unused=尚未使用 funds.deposit.usedInTx=用在 {0} 交易 funds.deposit.fundHavenoWallet=充值 Haveno 錢包 funds.deposit.noAddresses=尚未生成存款地址 funds.deposit.fundWallet=充值您的錢包 funds.deposit.withdrawFromWallet=從錢包轉出資金 funds.deposit.amount=XMR 數量(可選) funds.deposit.generateAddress=生成新的地址 funds.deposit.generateAddressSegwit=原生 segwit 格式(Bech32) funds.deposit.selectUnused=請從上表中選擇一個未使用的地址,而不是生成一個新地址。 funds.withdrawal.arbitrationFee=仲裁費用 funds.withdrawal.inputs=充值選擇 funds.withdrawal.useAllInputs=使用所有可用的充值地址 funds.withdrawal.useCustomInputs=使用自定義充值地址 funds.withdrawal.receiverAmount=接收者的數量 funds.withdrawal.senderAmount=發送者的數量 funds.withdrawal.feeExcluded=不含挖礦費的金額 funds.withdrawal.feeIncluded=包含挖礦費的金額 funds.withdrawal.fromLabel=從源地址提現 funds.withdrawal.toLabel=提現地址 funds.withdrawal.memoLabel=提現備註 funds.withdrawal.memo=可選備註 funds.withdrawal.withdrawButton=選定提現 funds.withdrawal.noFundsAvailable=沒有可用資金提現 funds.withdrawal.confirmWithdrawalRequest=確定提現請求 funds.withdrawal.withdrawMultipleAddresses=從多個地址提現({0}) funds.withdrawal.withdrawMultipleAddresses.tooltip=從多個地址提現:\n{0} funds.withdrawal.notEnoughFunds=您錢包裏沒有足夠的資金。 funds.withdrawal.selectAddress=從列表中選一個源地址 funds.withdrawal.setAmount=設置提現數量 funds.withdrawal.fillDestAddress=輸入您的目標地址 funds.withdrawal.warn.noSourceAddressSelected=您需要從上面列表中選一個源地址。 funds.withdrawal.warn.amountExceeds=您的金額超過所選地址的可用金額。\n請考慮在上表中選擇多個地址或調整手續費設置,來支付手續費。 funds.reserved.noFunds=未完成報價中沒有已用資金 funds.reserved.reserved=報價 ID:{0} 接收在本地錢包中 funds.locked.noFunds=交易中沒有凍結資金 funds.locked.locked=多重驗證凍結交易 ID:{0} funds.tx.direction.sentTo=發送至: funds.tx.direction.receivedWith=接收到: funds.tx.direction.genesisTx=從初始 tx: funds.tx.createOfferFee=掛單和tx費用:{0} funds.tx.takeOfferFee=下單和tx費用:{0} funds.tx.multiSigDeposit=多重驗證保證金:{0} funds.tx.multiSigPayout=多重驗證花費:{0} funds.tx.disputePayout=糾紛花費:{0} funds.tx.disputeLost=失敗的糾紛案件:{0} funds.tx.collateralForRefund=押金退款:{0} funds.tx.timeLockedPayoutTx=距鎖定鎖定支付 tx 的時間: {0} funds.tx.refund=仲裁退款:{0} funds.tx.unknown=未知原因:{0} funds.tx.noFundsFromDispute=沒有退款的糾紛 funds.tx.receivedFunds=收到的資金: funds.tx.withdrawnFromWallet=從錢包提現 funds.tx.memo=備註 funds.tx.noTxAvailable=沒有可用交易 funds.tx.revert=還原 funds.tx.txSent=交易成功發送到本地 Haveno 錢包中的新地址。 funds.tx.direction.self=內部錢包交易 funds.tx.dustAttackTx=接受零頭 funds.tx.dustAttackTx.popup=這筆交易是發送一個非常小的比特幣金額到您的錢包,可能是區塊鏈分析公司嘗試監控您的交易。\n\n如果您在交易中使用該交易輸出,他們將瞭解到您很可能也是其他地址的所有者(資金歸集)。\n\n為了保護您的隱私,Haveno 錢包忽略了這種零頭的消費和餘額顯示。可以在設置中將輸出視為零頭時設置閾值量。 #################################################################### # Support #################################################################### support.tab.mediation.support=調解 support.tab.arbitration.support=仲裁 support.tab.legacyArbitration.support=歷史仲裁 support.tab.ArbitratorsSupportTickets={0} 的工單 support.sigCheck.button=Check signature support.sigCheck.popup.info=請貼上仲裁程序的摘要訊息。利用這個工具,任何使用者都可以檢查仲裁者的簽名是否與摘要訊息相符。 support.sigCheck.popup.header=確認糾紛結果簽名 support.sigCheck.popup.msg.label=總結消息 support.sigCheck.popup.msg.prompt=複製粘貼糾紛總結消息 support.sigCheck.popup.result=驗證結果 support.sigCheck.popup.success=簽名有效 support.sigCheck.popup.failed=簽名驗證失敗 support.sigCheck.popup.invalidFormat=消息並不是正確的格式。請複製粘貼糾紛總結消息。 support.reOpenByTrader.prompt=您確定想要重新開啟糾紛? support.reOpenButton.label=重新打開 support.sendNotificationButton.label=私人通知 support.reportButton.label=報吿 support.fullReportButton.label=所有糾紛 support.noTickets=沒有創建的話題 support.sendingMessage=發送消息... support.receiverNotOnline=收件人未在線。消息被保存到他們的郵箱。 support.sendMessageError=發送消息失敗。錯誤:{0} support.receiverNotKnown=Receiver not known support.wrongVersion=糾紛中的訂單創建於一箇舊版本的 Haveno。\n您不能在當前版本關閉這個糾紛。\n\n請您使用舊版本/協議版本: {0} support.openFile=打開附件文件(文件最大大小:{0} kb) support.attachmentTooLarge=您的附件的總大小為 {0} kb,並超過最大值。 允許消息大小為 {1} kB。 support.maxSize=文件允許的最大大小 {0} kB。 support.attachment=附件 support.tooManyAttachments=您不能在一個消息裏發送超過3個附件。 support.save=保存文件到磁盤 support.messages=消息 support.input.prompt=輸入消息... support.send=發送 support.addAttachments=添加附件 support.closeTicket=關閉話題 support.attachments=附件: support.savedInMailbox=消息保存在收件人的信箱中 support.arrived=消息抵達收件人 support.acknowledged=收件人已確認接收消息 support.error=收件人無法處理消息。錯誤:{0} support.buyerAddress=XMR 買家地址 support.sellerAddress=XMR 賣家地址 support.role=角色 support.agent=Support agent support.state=狀態 support.chat=Chat support.preparing=準備中 support.requested=已請求 support.closed=關閉 support.open=打開 support.process=Process support.buyerMaker=XMR 買家/掛單者 support.sellerMaker=XMR 賣家/掛單者 support.buyerTaker=XMR 買家/買單者 support.sellerTaker=XMR 賣家/買單者 support.backgroundInfo=Haveno 不是一家公司,因此它以不同的方式處理爭議。\n\n交易者可以在應用程式中透過在打開交易畫面上的安全聊天來嘗試自行解決爭議。\n如果這不夠,一名仲裁者將評估情況並決定交易資金的支付。 support.initialInfo=請在下面的文本框中輸入您的問題描述。添加儘可能多的信息,以加快解決糾紛的時間。\n\n以下是你應提供的資料核對表:\n\t●如果您是 XMR 買家:您是否使用法定貨幣或其他加密貨幣轉賬?如果是,您是否點擊了應用程序中的“支付開始”按鈕?\n\t●如果您是 XMR 賣家:您是否收到法定貨幣或其他加密貨幣的付款了?如果是,你是否點擊了應用程序中的“已收到付款”按鈕?\n\t●您使用的是哪個版本的 Haveno?\n\t●您使用的是哪種操作系統?\n\t●如果遇到操作執行失敗的問題,請考慮切換到新的數據目錄。\n\t有時數據目錄會損壞,並導致奇怪的錯誤。\n詳見:https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\n請熟悉糾紛處理的基本規則:\n\t●您需要在2天內答覆 {0} 的請求。\n\t●調解員會在2天之內答覆,仲裁員會在5天之內答覆。\n\t●糾紛的最長期限為14天。\n\t●你需要與仲裁員合作,提供他們為你的案件所要求的信息。\n\t●當您第一次啟動應用程序時,您接受了用户協議中爭議文檔中列出的規則。\n\n您可以通過 {2} 瞭解有關糾紛處理的更多信息 support.systemMsg=系統消息:{0} support.youOpenedTicket=您創建了幫助請求。\n\n{0}\n\nHaveno 版本:{1} support.youOpenedDispute=您創建了一個糾紛請求。\n\n{0}\n\nHaveno 版本:{1} support.youOpenedDisputeForMediation=您創建了一個調解請求。\n\n{0}\n\nHaveno 版本:{1} support.peerOpenedTicket=對方因技術問題請求獲取幫助。\n\n{0}\n\nHaveno 版本:{1} support.peerOpenedDispute=對方創建了一個糾紛請求。\n\n{0}\n\nHaveno 版本:{1} support.peerOpenedDisputeForMediation=對方創建了一個調解請求。\n\n{0}\n\nHaveno 版本:{1} support.mediatorsDisputeSummary=系統消息:\n調解糾紛總結:\n{0} support.mediatorsAddress=仲裁員的節點地址:{0} support.warning.disputesWithInvalidDonationAddress=延遲支付交易已經被用於一個不可用接受者地址。它與有效捐贈地址的任何 DAO 中參數值均不匹配。\n\n這可能是一個騙局。請將該事件通知開發者,在問題解決之前不要關閉該案件!\n\n糾紛所用的地址:{0}\n\n所有 DAO 參數中捐贈地址:{1}\n\n交易:{2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\n您確定一定要關閉糾紛嗎? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\n您不能進行支付。 support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. support.info.disputeReOpened=Dispute ticket has been re-opened. #################################################################### # Settings #################################################################### settings.tab.preferences=偏好 settings.tab.network=網絡信息 settings.tab.about=關於我們 setting.preferences.general=通用偏好 setting.preferences.explorer=比特幣區塊瀏覽器 setting.preferences.deviation=與市場價格最大差價 setting.preferences.avoidStandbyMode=避免待機模式 setting.preferences.useSoundForNotifications=播放通知音效 setting.preferences.autoConfirmXMR=XMR 自動確認 setting.preferences.autoConfirmEnabled=啟用 setting.preferences.autoConfirmRequiredConfirmations=已要求確認 setting.preferences.autoConfirmMaxTradeSize=最大交易量(XMR) setting.preferences.autoConfirmServiceAddresses=Monero Explorer 鏈接(使用Tor,但本地主機,LAN IP地址和 *.local 主機名除外) setting.preferences.deviationToLarge=值不允許大於30% setting.preferences.txFee=提現交易手續費(聰/字節) setting.preferences.useCustomValue=使用自定義值 setting.preferences.txFeeMin=交易手續費必須至少為{0} 聰/字節 setting.preferences.txFeeTooLarge=您輸入的數額超過可接受值(>5000 聰/字節)。交易手續費一般在 50-400 聰/字節、 setting.preferences.ignorePeers=忽略節點 [洋葱地址:端口] setting.preferences.ignoreDustThreshold=最小無零頭輸出值 setting.preferences.currenciesInList=市場價的貨幣列表 setting.preferences.prefCurrency=首選貨幣 setting.preferences.displayTraditional=顯示國家貨幣 setting.preferences.noTraditional=沒有選定國家貨幣 setting.preferences.cannotRemovePrefCurrency=您不能刪除您選定的首選貨幣 setting.preferences.displayCryptos=顯示數字貨幣 setting.preferences.noCryptos=沒有選定數字貨幣 setting.preferences.addTraditional=添加法定貨幣 setting.preferences.addCrypto=添加數字貨幣 setting.preferences.displayOptions=顯示選項 setting.preferences.showOwnOffers=在報價列表中顯示我的報價 setting.preferences.useAnimations=使用動畫 setting.preferences.useDarkMode=使用夜間模式 setting.preferences.useLightMode=使用淺色模式 setting.preferences.sortWithNumOffers=使用“報價ID/交易ID”篩選列表 setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API setting.preferences.notifyOnPreRelease=Receive pre-release notifications setting.preferences.resetAllFlags=重置所有“不再提示”的提示 settings.preferences.languageChange=同意重啟請求以更換語言 settings.preferences.supportLanguageWarning=如有任何爭議,請注意仲裁在 {0} 處理。 settings.preferences.editCustomExplorer.headline=瀏覽設置。 settings.preferences.editCustomExplorer.description=從左側列表中選擇一個系統默認瀏覽器,或使用您偏好的自定義設置。 settings.preferences.editCustomExplorer.available=可用瀏覽器 settings.preferences.editCustomExplorer.chosen=已選擇的瀏覽器設置 settings.preferences.editCustomExplorer.name=名稱 settings.preferences.editCustomExplorer.txUrl=交易 URL settings.preferences.editCustomExplorer.addressUrl=地址 URL setting.info.headline=新的資料隱私功能 settings.preferences.sensitiveDataRemoval.msg=為了保護您與其他交易者的隱私,Haveno 計劃從舊交易中移除敏感資料。這對於可能包含銀行帳戶資訊的法幣交易尤其重要。\n\n建議將此設定設為盡可能低,例如 60 天。這表示只要交易已完成且超過 60 天,敏感資料將被清除。已完成的交易可在「投資組合 / 歷史」標籤中找到。 settings.net.xmrHeader=比特幣網絡 settings.net.p2pHeader=Haveno 網絡 settings.net.onionAddressLabel=我的匿名地址 settings.net.xmrNodesLabel=使用自定义Monero节点 settings.net.moneroPeersLabel=已連接節點 settings.net.connection=連接 settings.net.connected=連接完成 settings.net.useTorForXmrJLabel=使用 Tor 連接 Monero 網絡 settings.net.moneroNodesLabel=需要連接 Monero settings.net.useProvidedNodesRadio=使用公共比特幣核心節點 settings.net.usePublicNodesRadio=使用公共比特幣網絡 settings.net.useCustomNodesRadio=使用自定義比特幣主節點 settings.net.warn.usePublicNodes=如果您使用公共的Monero节点,您将面临使用不受信任的远程节点带来的任何风险。\n\n请在[HYPERLINK:https://www.getmonero.org/resources/moneropedia/remote-node.html]阅读更多详细信息。\n\n您确定要使用公共节点吗? settings.net.warn.usePublicNodes.useProvided=不,使用給定的節點 settings.net.warn.usePublicNodes.usePublic=使用公共網絡 settings.net.warn.useCustomNodes.B2XWarning=請確保您的比特幣節點是一個可信的比特幣核心節點!\n\n連接到不遵循比特幣核心共識規則的節點可能會損壞您的錢包,並在交易過程中造成問題。\n\n連接到違反共識規則的節點的用户應對任何由此造成的損害負責。任何由此產生的糾紛都將有利於另一方。對於忽略此警吿和保護機制的用户,不提供任何技術支持! settings.net.warn.invalidXmrConfig=由於您的配置無效,無法連接至比特幣網絡。\n\n您的配置已經被重置為默認比特幣節點。你需要重啟 Haveno。 settings.net.localhostXmrNodeInfo=背景信息:Haveno 在啟動時會在本地查找比特幣節點。如果有,Haveno 將只通過它與比特幣網絡進行通信。 settings.net.p2PPeersLabel=已連接節點 settings.net.onionAddressColumn=匿名地址 settings.net.creationDateColumn=已建立連接 settings.net.connectionTypeColumn=入/出 settings.net.sentDataLabel=統計數據已發送 settings.net.receivedDataLabel=統計數據已接收 settings.net.chainHeightLabel=最新 XMR 區塊高度 settings.net.roundTripTimeColumn=延遲 settings.net.sentBytesColumn=發送 settings.net.receivedBytesColumn=接收 settings.net.peerTypeColumn=節點類型 settings.net.openTorSettingsButton=打開 Tor 設置 settings.net.versionColumn=版本 settings.net.subVersionColumn=子版本 settings.net.heightColumn=高度 settings.net.needRestart=您需要重啟應用程序以同意這次變更。\n您需要現在重啟嗎? settings.net.notKnownYet=至今未知... settings.net.sentData=已發送數據 {0},{1} 條消息,{2} 條消息/秒 settings.net.receivedData=已接收數據 {0},{1} 條消息,{2} 條消息/秒 settings.net.chainHeight=Monero Peers chain height: {0} settings.net.ips=添加逗號分隔的 IP 地址及端口,如使用8333端口可不填寫。 settings.net.seedNode=種子節點 settings.net.directPeer=節點(直連) settings.net.initialDataExchange={0} [Bootstrapping] settings.net.peer=節點 settings.net.inbound=接收數據包 settings.net.outbound=發送數據包 setting.about.aboutHaveno=關於 Haveno setting.about.about=Haveno 是一款開源軟件,它通過分散的對等網絡促進了比特幣與各國貨幣(以及其他加密貨幣)的交易,嚴格保護了用户隱私的方式。請到我們項目的網站閲讀更多關於 Haveno 的信息。 setting.about.web=Haveno 網站 setting.about.code=源代碼 setting.about.agpl=AGPL 協議 setting.about.support=支持 Haveno setting.about.def=Haveno 不是一個公司,而是一個社區項目,開放參與。如果您想參與或支持 Haveno,請點擊下面連接。 setting.about.contribute=貢獻 setting.about.providers=數據提供商 setting.about.apisWithFee=Haveno 使用 Haveno 價格指數來表示法幣與虛擬貨幣的市場價格,並使用 Haveno 內存池節點來估算採礦費。 setting.about.apis=Haveno 使用 Haveno 價格指數來表示法幣與數字貨幣的市場價格。 setting.about.pricesProvided=交易所價格提供商 setting.about.feeEstimation.label=礦工手續費估算提供商 setting.about.versionDetails=版本詳情 setting.about.version=應用程序版本 setting.about.subsystems.label=子系統版本 setting.about.subsystems.val=網絡版本:{0};P2P 消息版本:{1};本地數據庫版本:{2};交易協議版本:{3} setting.about.shortcuts=快捷鍵 setting.about.shortcuts.ctrlOrAltOrCmd=“Ctrl + {0}”或“alt + {0}”或“cmd + {0}” setting.about.shortcuts.menuNav=主頁面 setting.about.shortcuts.menuNav.value=使用“Ctrl”或“Alt”或“cmd” + 數字鍵“1-9”來切換不同的主頁面 setting.about.shortcuts.close=關閉 Haveno setting.about.shortcuts.close.value=“Ctrl + {0}”或“cmd + {0}”或“Ctrl + {1}”或“cmd + {1}” setting.about.shortcuts.closePopup=關閉彈窗以及對話框 setting.about.shortcuts.closePopup.value=‘釋放’ 鍵 setting.about.shortcuts.chatSendMsg=發送信息到交易夥伴 setting.about.shortcuts.chatSendMsg.value=“Ctrl + ENTER”或“alt + ENTER”或“cmd + ENTER” setting.about.shortcuts.openDispute=創建糾紛 setting.about.shortcuts.openDispute.value=選擇未完成交易並點擊:{0} setting.about.shortcuts.walletDetails=打開錢包詳情窗口 setting.about.shortcuts.openEmergencyXmrWalletTool=打開應急 XMR 錢包工具 setting.about.shortcuts.showTorLogs=在 DEBUG 與 WARN 之間切換 Tor 日誌等級 setting.about.shortcuts.manualPayoutTxWindow=打開窗口手動支付雙重驗證存款交易 setting.about.shortcuts.removeStuckTrade=Open popup to move failed trade to open trades tab again setting.about.shortcuts.removeStuckTrade.value=Select failed trade and press: {0} setting.about.shortcuts.registerArbitrator=註冊仲裁員(僅限調解員/仲裁員) setting.about.shortcuts.registerArbitrator.value=切換至賬户頁面並按下:{0} setting.about.shortcuts.registerMediator=註冊調解員(僅限調解員/仲裁員) setting.about.shortcuts.registerMediator.value=切換至賬户頁面並按下:{0} setting.about.shortcuts.openSignPaymentAccountsWindow=打開賬齡驗證窗口(僅限仲裁員) setting.about.shortcuts.openSignPaymentAccountsWindow.value=切換至仲裁頁面並按下:{0} setting.about.shortcuts.sendAlertMsg=發送警報或更新消息(需要權限) setting.about.shortcuts.sendFilter=設置過濾器(需要權限) setting.about.shortcuts.sendPrivateNotification=發送私人通知到對等點(需要權限) setting.about.shortcuts.sendPrivateNotification.value=點擊交易夥伴頭像並按下:{0} 以顯示更多信息 setting.info.headline=新 XMR 自動確認功能 setting.info.msg=當你完成 XMR/XMR 交易時,您可以使用自動確認功能來驗證是否向您的錢包中發送了正確數量的 XMR,以便 Haveno 可以自動將交易標記為完成,從而使每個人都可以更快地進行交易。\n\n自動確認使用 XMR 發送方提供的交易密鑰在至少 2 個 XMR 區塊瀏覽器節點上檢查 XMR 交易。在默認情況下,Haveno 使用由 Haveno 貢獻者運行的區塊瀏覽器節點,但是我們建議運行您自己的 XMR 區塊瀏覽器節點以最大程度地保護隱私和安全。\n\n您還可以在``設置'中將每筆交易的最大 XMR 數量設置為自動確認以及所需確認的數量。\n\n在 Haveno Wiki 上查看更多詳細信息(包括如何設置自己的區塊瀏覽器節點):https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades #################################################################### # Account #################################################################### account.tab.mediatorRegistration=調解員註冊 account.tab.refundAgentRegistration=退款助理註冊 account.tab.signing=驗證中 account.info.headline=歡迎來到 Haveno 賬户 account.info.msg=在這裏你可以設置交易賬户的法定貨幣及數字貨幣,選擇仲裁員和備份你的錢包及賬户數據。\n\n當你開始運行 Haveno 就已經創建了一個空的比特幣錢包。\n\n我們建議你在充值之前寫下你比特幣錢包的還原密鑰(在左邊的列表)和考慮添加密碼。在“資金”選項中管理比特幣存入和提現。\n\n隱私 & 安全:\nHaveno 是一個去中心化的交易所 – 意味着您的所有數據都保存在您的電腦上,沒有服務器,我們無法訪問您的個人信息,您的資金,甚至您的 IP 地址。如銀行賬號、數字貨幣、比特幣地址等數據只分享給與您交易的人,以實現您發起的交易(如果有爭議,仲裁員將會看到您的交易數據)。 account.menu.paymentAccount=法定貨幣賬户 account.menu.altCoinsAccountView=數字貨幣賬户 account.menu.password=錢包密碼 account.menu.seedWords=錢包密鑰 account.menu.walletInfo=Wallet info account.menu.backup=備份 account.menu.notifications=通知 account.menu.walletInfo.balance.headLine=Wallet balances account.menu.walletInfo.balance.info=This shows the internal wallet balance including unconfirmed transactions.\nFor XMR, the internal wallet balance shown below should match the sum of the 'Available' and 'Reserved' balances shown in the top right of this window. account.menu.walletInfo.xpub.headLine=Watch keys (xpub keys) account.menu.walletInfo.walletSelector={0} {1} wallet account.menu.walletInfo.path.headLine=HD keychain paths account.menu.walletInfo.path.info=If you import seed words into another wallet (like Electrum), you'll need to define the path. This should only be done in emergency cases when you lose access to the Haveno wallet and data directory.\nKeep in mind that spending funds from a non-Haveno wallet can bungle the internal Haveno data structures associated with the wallet data, which can lead to failed trades.\n\nNEVER send BSQ from a non-Haveno wallet, as it will probably lead to an invalid BSQ transaction and losing your BSQ. account.menu.walletInfo.openDetails=Show raw wallet details and private keys ## TODO should we rename the following to a gereric name? account.arbitratorRegistration.pubKey=公鑰 account.arbitratorRegistration.register=註冊 account.arbitratorRegistration.registration={0} 註冊 account.arbitratorRegistration.revoke=撤銷 account.arbitratorRegistration.info.msg=請注意,撤銷後需要保留15天,因為可能有交易正在以你作為 {0}。最大允許的交易期限為8天,糾紛過程最多可能需要7天。 account.arbitratorRegistration.warn.min1Language=您需要設置至少1種語言。\n我們已經為您添加了默認語言。 account.arbitratorRegistration.removedSuccess=您已從 Haveno 網絡成功刪除仲裁員註冊信息。 account.arbitratorRegistration.removedFailed=無法刪除仲裁員。{0} account.arbitratorRegistration.registerSuccess=您已從 Haveno 網絡成功註冊您的仲裁員。 account.arbitratorRegistration.registerFailed=無法註冊仲裁員。{0} account.crypto.yourCryptoAccounts=您的數字貨幣賬户 account.crypto.popup.wallet.msg=請確保您按照 {1} 網頁上所述使用 {0} 錢包的要求。\n使用集中式交易所的錢包,您無法控制密鑰或使用不兼容的錢包軟件,可能會導致交易資金的流失!\n調解員或仲裁員不是 {2} 專家,在這種情況下不能幫助。 account.crypto.popup.wallet.confirm=我瞭解並確定我知道我需要哪種錢包。 # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=在 Haveno 上交易 UPX 需要您瞭解並滿足以下要求:\n\n要發送 UPX ,您需要使用官方的 UPXmA GUI 錢包或啟用 store-tx-info 標誌的 UPXmA CLI 錢包(在新版本中是默認的)。請確保您可以訪問Tx密鑰,因為在糾紛狀態時需要。\nmonero-wallet-cli(使用get_Tx_key命令)\nmonero-wallet-gui:在高級>證明/檢查頁面。\n\n在普通的區塊鏈瀏覽器中,這種交易是不可驗證的。\n\n如有糾紛,你須向仲裁員提供下列資料:\n \n- Tx私鑰\n- 交易哈希\n- 接收者的公開地址\n\n如未能提供上述資料,或使用不兼容的錢包,將會導致糾紛敗訴。如果發生糾紛,UPX 發送方負責向仲裁員提供 UPX 轉賬的驗證。\n\n不需要支付 ID,只需要普通的公共地址。\n \n如果您對該流程不確定,請訪問 UPXmA Discord 頻道(https://discord.gg/vhdNSrV)或 Telegram 交流羣(https://t.me/uplexaOfficial)瞭解更多信息。\n\n # suppress inspection "UnusedProperty" account.crypto.popup.arq.msg=在 Haveno 上交易 ARQ 需要您瞭解並滿足以下要求:\n\n要發送 ARQ ,您需要使用官方的 ArQmA GUI 錢包或啟用 store-tx-info 標誌的 ArQmA CLI 錢包(在新版本中是默認的)。請確保您可以訪問Tx密鑰,因為在糾紛狀態時需要。\nmonero-wallet-cli(使用get_Tx_key命令)\nmonero-wallet-gui:在高級>證明/檢查頁面。\n\n在普通的區塊鏈瀏覽器中,這種交易是不可驗證的。\n\n如有糾紛,你須向調解員或仲裁員提供下列資料:\n\n- Tx私鑰\n- 交易哈希\n- 接收者的公開地址\n\n如未能提供上述資料,或使用不兼容的錢包,將會導致糾紛敗訴。如果發生糾紛,ARQ 發送方負責向調解員或仲裁員提供 ARQ 轉賬的驗證。\n\n不需要交易 ID,只需要普通的公共地址。\n\n如果您對該流程不確定,請訪問 ArQmA Discord 頻道(https://discord.gg/s9BQpJT)或 ArQmA 論壇(https://labs.arqma.com)瞭解更多信息。 # suppress inspection "UnusedProperty" account.crypto.popup.xmr.msg=在 Haveno 上交易 XMR 需要你理解並滿足以下要求。\n\n如果您出售 XMR,當您在糾紛中您必須要提供下列信息給調解員或仲裁員:\n- 交易密鑰(Tx 公鑰,Tx密鑰,Tx私鑰)\n- 交易 ID(Tx ID 或 Tx 哈希)\n- 交易目標地址(接收者地址)\n\n在 wiki 中查看更多關於 Monero 錢包的信息:\nhttps://haveno.exchange/wiki/Trading_Monero#Proving_payments\n\n如未能提供要求的交易數據將在糾紛中直接判負\n\n還要注意,Haveno 現在提供了自動確認 XMR 交易的功能,以使交易更快,但是您需要在設置中啟用它。\n\n有關自動確認功能的更多信息,請參見 Wiki:\nhttps://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades # suppress inspection "UnusedProperty" account.crypto.popup.msr.msg=區塊鏈瀏覽器在 Haveno 上交易 XMR 需要您瞭解並滿足以下要求:\n\n發送MSR時,您需要使用官方的 Masari GUI 錢包、啟用store-tx-info標記的Masari CLI錢包(默認啟用)或Masari 網頁錢包(https://wallet.getmasari.org)。請確保您可以訪問的 tx 密鑰,因為如果發生糾紛這是需要的。\nmonero-wallet-cli(使用get_Tx_key命令)\nmonero-wallet-gui:在高級>證明/檢查頁面。\n\nMasari 網頁錢包(前往 帳户->交易歷史和查看您發送的交易細節)\n\n驗證可以在錢包中完成。\nmonero-wallet-cli:使用命令(check_tx_key)。\nmonero-wallet-gui:在高級>證明/檢查頁面\n驗證可以在區塊瀏覽器中完成\n打開區塊瀏覽器(https://explorer.getmasari.org),使用搜索欄查找您的事務哈希。\n一旦找到交易,滾動到底部的“證明發送”區域,並填寫所需的詳細信息。\n如有糾紛,你須向調解員或仲裁員提供下列資料:\n- Tx私鑰\n- 交易哈希\n- 接收者的公開地址\n\n不需要交易 ID,只需要正常的公共地址。\n如未能提供上述資料,或使用不兼容的錢包,將會導致糾紛敗訴。如果發生糾紛,XMR 發送方負責向調解員或仲裁員提供 XMR 轉賬的驗證。\n\n如果您對該流程不確定,請訪問官方的 Masari Discord(https://discord.gg/sMCwMqs)上尋求幫助。 # suppress inspection "UnusedProperty" account.crypto.popup.blur.msg=在 Haveno 上交易 BLUR 需要你瞭解並滿足以下要求:\n\n要發送匿名信息你必須使用匿名網絡 CLI 或 GUI 錢包。\n如果您正在使用 CLI 錢包,在傳輸發送後將顯示交易哈希(tx ID)。您必須保存此信息。在發送傳輸之後,您必須立即使用“get_tx_key”命令來檢索交易私鑰。如果未能執行此步驟,以後可能無法檢索密鑰。\n\n如果您使用 Blur Network GUI 錢包,可以在“歷史”選項卡中方便地找到交易私鑰和交易 ID。發送後立即定位感興趣的交易。單擊包含交易的框的右下角的“?”符號。您必須保存此信息。\n\n如果仲裁是必要的,您必須向調解員或仲裁員提供以下信息:1.)交易ID,2.)交易私鑰,3.)收件人地址。調解或仲裁程序將使用 BLUR 事務查看器(https://blur.cash/#tx-viewer)驗證 BLUR 轉賬。\n\n未能向調解員或仲裁員提供必要的信息將導致敗訴。在所有爭議的情況下,匿名發送方承擔100%的責任來向調解員或仲裁員核實交易。\n\n如果你不瞭解這些要求,不要在 Haveno 上交易。首先,在 Blur Network Discord 中尋求幫助(https://discord.gg/dMWaqVW)。 # suppress inspection "UnusedProperty" account.crypto.popup.solo.msg=在 Haveno 上交易 Solo 需要您瞭解並滿足以下要求:\n\n要發送 Solo,您必須使用 Solo CLI 網絡錢包版本 5.1.3 或更高。\n\n如果您使用的是CLI錢包,則在發送交易之後,將顯示交易ID。您必須保存此信息。在發送交易之後,您必須立即使用'get_tx_key'命令來檢索交易密鑰。如果未能執行此步驟,則以後可能無法檢索密鑰。\n\n如果仲裁是必要的,您必須向調解員或仲裁員提供以下信息:1)交易 ID,、2)交易密鑰,3)收件人的地址。調解員或仲裁員將使用 Solo 區塊資源管理器(https://explorer.Solo.org)搜索交易然後使用“發送證明”功能(https://explorer.minesolo.com/)\n\n未能向調解員或仲裁員提供必要的信息將導致敗訴。在所有發生爭議的情況下,在向調解員或仲裁員核實交易時,QWC 的發送方承擔 100% 的責任。\n\n如果你不理解這些要求,不要在 Haveno 上交易。首先,在 Solo Discord 中尋求幫助(https://discord.minesolo.com/)。\n\n # suppress inspection "UnusedProperty" account.crypto.popup.cash2.msg=在 Haveno 上交易 CASH2 需要您瞭解並滿足以下要求:\n\n要發送 CASH2,您必須使用 CASH2 錢包版本 3 或更高。\n\n在發送交易之後,將顯示交易ID。您必須保存此信息。在發送交易之後,必須立即在 simplewallet 中使用命令“getTxKey”來檢索交易密鑰。\n\n如果仲裁是必要的,您必須向調解員或仲裁員提供以下信息:1)交易 ID,2)交易密鑰,3)收件人的 CASH2 地址。調解員或仲裁員將使用 CASH2 區塊資源管理器(https://blocks.cash2.org)驗證 CASH2 轉賬。\n\n未能向調解員或仲裁員提供必要的信息將導致敗訴。在所有發生爭議的情況下,在向調解員或仲裁員核實交易時,CASH2 的發送方承擔 100% 的責任。\n\n如果你不理解這些要求,不要在 Haveno 上交易。首先,在 Cash2 Discord 中尋求幫助(https://discord.gg/FGfXAYN)。 # suppress inspection "UnusedProperty" account.crypto.popup.qwertycoin.msg=在 Haveno 上交易 Qwertycoin 需要您瞭解並滿足以下要求:\n\n要發送 Qwertycoin,您必須使用 Qwertycoin 錢包版本 5.1.3 或更高。\n\n在發送交易之後,將顯示交易ID。您必須保存此信息。在發送交易之後,必須立即在 simplewallet 中使用命令“get_Tx_Key”來檢索交易密鑰。\n\n如果仲裁是必要的,您必須向調解員或仲裁員提供以下信息::1)交易 ID,、2)交易密鑰,3)收件人的 QWC 地址。調解員或仲裁員將使用 QWC 區塊資源管理器(https://explorer.qwertycoin.org)驗證 QWC 轉賬。\n\n未能向調解員或仲裁員提供必要的信息將導致敗訴。在所有發生爭議的情況下,在向調解員或仲裁員核實交易時,QWC 的發送方承擔 100% 的責任。\n\n如果你不理解這些要求,不要在 Haveno 上交易。首先,在 QWC Discord 中尋求幫助(https://discord.gg/rUkfnpC)。 # suppress inspection "UnusedProperty" account.crypto.popup.drgl.msg=在 Haveno 上交易 Dragonglass 需要您瞭解並滿足以下要求:\n\n由於 Dragonglass 提供了隱私保護,所以交易不能在公共區塊鏈上驗證。如果需要,您可以通過使用您的 TXN-Private-Key 來證明您的付款。\nTXN-Private 密匙是自動生成的一次性密匙,用於只能從 DRGL 錢包中訪問的每個交易。\n要麼通過 DRGL-wallet GUI(內部交易細節對話框),要麼通過 Dragonglass CLI simplewallet(使用命令“get_tx_key”)。\n\n兩者都需要 DRGL 版本的“Oathkeeper”或更高版本。\n\n如有爭議,你必須向調解員或仲裁員提供下列資料:\n\n- txn-Privite-ket\n- 交易哈希 \n- 接收者的公開地址\n\n付款驗證可以使用上面的數據作為輸入(http://drgl.info/#check_txn)。\n\n如未能提供上述資料,或使用不兼容的錢包,將會導致糾紛敗訴。Dragonglass 發送方負責在發生爭議時向調解員或仲裁員提供 DRGL 轉賬的驗證。不需要使用付款 ID。\n\n如果您對這個過程的任何部分都不確定,請訪問(http://discord.drgl.info)上的 Dragonglass 尋求幫助。 # suppress inspection "UnusedProperty" account.crypto.popup.ZEC.msg=當使用 Zcash 時,您只能使用透明地址(以 t 開頭),而不能使用 z 地址(私有),因為調解員或仲裁員無法使用 z 地址驗證交易。 # suppress inspection "UnusedProperty" account.crypto.popup.XZC.msg=在使用 Zcoin 時,您只能使用透明的(可跟蹤的)地址,而不能使用不可跟蹤的地址,因為調解員或仲裁員無法在區塊資源管理器中使用不可跟蹤的地址驗證交易。 # suppress inspection "UnusedProperty" account.crypto.popup.grin.msg=GRIN 需要發送方和接收方之間的交互過程來創建交易。請確保遵循 GRIN 項目網頁中的説明,以可靠地發送和接收 GRIN(接收方需要在線,或至少在一定時間內在線)。\n \nHaveno 只支持 Grinbox(Wallet713)錢包 URL 格式。\n\nGRIN 發送者需要提供他們已成功發送 GRIN 的證明。如果錢包不能提供證明,一個潛在的糾紛將被解決,有利於露齒微笑的接受者。請確保您使用了最新的支持交易證明的 Grinbox 軟件,並且您瞭解傳輸和接收 GRIN 的過程以及如何創建證明。\n請參閲 https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only,以獲得關於 Grinbox 證明工具的更多信息。\n # suppress inspection "UnusedProperty" account.crypto.popup.beam.msg=BEAM 需要發送方和接收方之間的交互過程來創建交易。\n\n\n確保遵循 BEAM 項目網頁的指示可靠地發送和接收 BEAM(接收方需要在線,或者至少在一定的時間範圍內在線)。\n\nBEAM 發送者需要提供他們成功發送 BEAM 的證明。一定要使用錢包軟件,可以產生這樣的證明。如果錢包不能提供證據,一個潛在的糾紛將得到解決,有利於 BEAM 接收者。 # suppress inspection "UnusedProperty" account.crypto.popup.pars.msg=在 Haveno 上交易 ParsiCoin 需要您瞭解並滿足以下要求:\n\n要發送 PARS ,您必須使用官方 ParsiCoin Wallet 版本 3.0.0 或更高。\n\n您可以在 GUI 錢包(ParsiPay)的交易部分檢查您的交易哈希和交易鍵,您需要右鍵單擊“交易”,然後單擊“顯示詳情”。\n\n如果仲裁是 100% 必要的,您必須向調解員或仲裁員提供以下內容:1)交易哈希,2)交易密鑰,以及3)接收方的 PARS 地址。調解員或仲裁員將使用 ParsiCoin 區塊鏈瀏覽器 (http://explorer.parsicoin.net/#check_payment)驗證 PARS 傳輸。\n\n如果你不瞭解這些要求,不要在 Haveno 上交易。首先,在 ParsiCoin Discord 尋求幫助(https://discord.gg/c7qmFNh)。 # suppress inspection "UnusedProperty" account.crypto.popup.blk-burnt.msg=要交易燒燬的貨幣,你需要知道以下幾點:\n\n燒燬的貨幣是不能花的。要在 Haveno 上交易它們,輸出腳本需要採用以下形式:OP_RETURN OP_PUSHDATA,後跟相關的數據字節,這些字節經過十六進制編碼後構成地址。例如,地址為666f6f(在UTF-8中的"foo")的燒燬的貨幣將有以下腳本:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\n要創建燒燬的貨幣,您可以使用“燒燬”RPC命令,它在一些錢包可用。\n\n對於可能的情況,可以查看 https://ibo.laboratorium.ee\n\n因為燒燬的貨幣是不能用的,所以不能重新出售。“出售”燒燬的貨幣意味着焚燒初始的貨幣(與目的地地址相關聯的數據)。\n\n如果發生爭議,BLK 賣方需要提供交易哈希。 # suppress inspection "UnusedProperty" account.crypto.popup.liquidmonero.msg=在 Haveno 上交易 L-XMR 你必須理解下述條款:\n\n當你在 Haveno 上接受 L-XMR 交易時,你不能使用手機 Blockstream Green Wallet 或者是一個託管/交易錢包。你必須只接收 L-XMR 到 Liquid Elements Core 錢包,或另一個 L-XMR 錢包且允許你獲得匿名的 L-XMR 地址以及密鑰。\n\n在需要進行調解的情況下,或者如果發生了交易糾紛,您必須將接收 L-XMR地址的安全密鑰披露給 Haveno 調解員或退款代理,以便他們能夠在他們自己的 Elements Core 全節點上驗證您的匿名交易的細節。\n\n如果你不瞭解或瞭解這些要求,不要在 Haveno 上交易 L-XMR。 account.traditional.yourTraditionalAccounts=您的法定貨幣賬户 account.backup.title=備份錢包 account.backup.location=備份路徑 account.backup.selectLocation=選擇備份路徑 account.backup.backupNow=立即備份(備份沒有被加密!) account.backup.appDir=應用程序數據目錄 account.backup.openDirectory=打開目錄 account.backup.openLogFile=打開日誌文件 account.backup.success=備份成功保存在:\n{0} account.backup.directoryNotAccessible=您沒有訪問選擇的目錄的權限。 {0} account.password.removePw.button=移除密碼 account.password.removePw.headline=移除錢包的密碼保護 account.password.setPw.button=設置密碼 account.password.setPw.headline=設置錢包的密碼保護 account.password.info=啟用密碼保護後,在應用程式啟動時、從您的錢包提取門羅幣時以及顯示種子詞語時,您將需要輸入密碼。 account.seed.backup.title=備份您的錢包種子詞 account.seed.info=請記下錢包種子詞和日期。您隨時可以使用種子詞和日期還原您的錢包。\n\n應該將種子詞寫在一張紙上,不要保存在電腦上。\n\n請注意,種子詞並不是備份的替代品。\n您需要在\"帳戶/備份\"畫面中創建整個應用程式目錄的備份,以還原應用程式的狀態和數據。 account.seed.backup.warning=請注意,種子詞並非備份的替代品。\n您需要從\"帳戶/備份\"畫面中創建整個應用程式目錄的備份,以還原應用程式的狀態和數據。 account.seed.warn.noPw.msg=您還沒有設置一個可以保護還原密鑰顯示的錢包密碼。\n\n要顯示還原密鑰嗎? account.seed.warn.noPw.yes=是的,不要再問我 account.seed.enterPw=輸入密碼查看還原密鑰 account.seed.restore.info=請在應用還原密鑰還原之前進行備份。請注意,錢包還原僅用於緊急情況,可能會導致內部錢包數據庫出現問題。\n這不是應用備份的方法!請使用應用程序數據目錄中的備份來恢復以前的應用程序狀態。\n恢復後,應用程序將自動關閉。重新啟動應用程序後,它將重新與比特幣網絡同步。這可能需要一段時間,並且會消耗大量CPU,特別是在錢包較舊且有很多交易的情況下。請避免中斷該進程,否則可能需要再次刪除 SPV 鏈文件或重複還原過程。 account.seed.restore.ok=好的,立即執行回覆並且關閉 Haveno #################################################################### # Mobile notifications #################################################################### account.notifications.setup.title=安裝 account.notifications.download.label=下載手機應用 account.notifications.waitingForWebCam=等待網絡攝像頭... account.notifications.webCamWindow.headline=用手機掃描二維碼 account.notifications.webcam.label=使用網絡攝像頭 account.notifications.webcam.button=掃描二維碼 account.notifications.noWebcam.button=我沒有網絡攝像頭 account.notifications.erase.label=在手機上清除通知 account.notifications.erase.title=清除通知 account.notifications.email.label=驗證碼 account.notifications.email.prompt=輸入您通過電子郵件收到的驗證碼 account.notifications.settings.title=設置 account.notifications.useSound.label=在手機上播放提示聲音 account.notifications.trade.label=接收交易信息 account.notifications.market.label=接收報價提醒 account.notifications.price.label=接收價格提醒 account.notifications.priceAlert.title=價格提醒 account.notifications.priceAlert.high.label=提醒條件:當 XMR 價格高於 account.notifications.priceAlert.low.label=提醒條件:當 XMR 價格低於 account.notifications.priceAlert.setButton=設置價格提醒 account.notifications.priceAlert.removeButton=取消價格提醒 account.notifications.trade.message.title=交易狀態已變更 account.notifications.trade.message.msg.conf=ID 為 {0} 的交易的存款交易已被確認。請打開您的 Haveno 應用程序並開始付款。 account.notifications.trade.message.msg.started=XMR 買家已經開始支付 ID 為 {0} 的交易。 account.notifications.trade.message.msg.completed=ID 為 {0} 的交易已完成。 account.notifications.offer.message.title=您的報價已被接受 account.notifications.offer.message.msg=您的 ID 為 {0} 的報價已被接受 account.notifications.dispute.message.title=新的糾紛消息 account.notifications.dispute.message.msg=您收到了一個 ID 為 {0} 的交易糾紛消息 account.notifications.marketAlert.title=報價提醒 account.notifications.marketAlert.selectPaymentAccount=提供匹配的付款帳户 account.notifications.marketAlert.offerType.label=我感興趣的報價類型 account.notifications.marketAlert.offerType.buy=買入報價(我想要出售 XMR ) account.notifications.marketAlert.offerType.sell=賣出報價(我想要購買 XMR ) account.notifications.marketAlert.trigger=報價距離(%) account.notifications.marketAlert.trigger.info=設置價格區間後,只有當滿足(或超過)您的需求的報價發佈時,您才會收到提醒。您想賣 XMR ,但你只能以當前市價的 2% 溢價出售。將此字段設置為 2% 將確保您只收到高於當前市場價格 2%(或更多)的報價的提醒。 account.notifications.marketAlert.trigger.prompt=與市場價格的百分比距離(例如 2.50%, -0.50% 等) account.notifications.marketAlert.addButton=添加報價提醒 account.notifications.marketAlert.manageAlertsButton=管理報價提醒 account.notifications.marketAlert.manageAlerts.title=管理報價提醒 account.notifications.marketAlert.manageAlerts.header.paymentAccount=支付賬户 account.notifications.marketAlert.manageAlerts.header.trigger=觸發價格 account.notifications.marketAlert.manageAlerts.header.offerType=報價類型 account.notifications.marketAlert.message.title=報價提醒 account.notifications.marketAlert.message.msg.below=低於 account.notifications.marketAlert.message.msg.above=高於 account.notifications.marketAlert.message.msg=價格為 {2}({3} {4}市場價)和支付方式為 {5} 的報價 {0} {1} 已發佈到 Haveno 報價列表。\n報價ID: {6}。 account.notifications.priceAlert.message.title=價格提醒 {0} account.notifications.priceAlert.message.msg=您的價格提醒已被觸發。當前 {0} 的價格為 {1} {2} account.notifications.noWebCamFound.warning=未找到網絡攝像頭。\n\n請使用電子郵件選項將代碼和加密密鑰從您的手機發送到 Haveno 應用程序。 account.notifications.priceAlert.warning.highPriceTooLow=較高的價格必須大於較低的價格。 account.notifications.priceAlert.warning.lowerPriceTooHigh=較低的價格必須低於較高的價格。 #################################################################### # Windows #################################################################### inputControlWindow.headline=Select inputs for transaction inputControlWindow.balanceLabel=可用餘額 contractWindow.title=糾紛詳情 contractWindow.dates=報價時間/交易時間 contractWindow.xmrAddresses=XMR 買家/XMR 賣家的比特幣地址 contractWindow.onions=XMR 買家/XMR 賣家的網絡地址 contractWindow.accountAge=XMR 買家/XMR 賣家的賬齡 contractWindow.numDisputes=XMR 買家/XMR 賣家的糾紛編號 contractWindow.contractHash=合同哈希 displayAlertMessageWindow.headline=重要資料! displayAlertMessageWindow.update.headline=重要更新資料! displayAlertMessageWindow.update.download=下載: displayUpdateDownloadWindow.downloadedFiles=下載完成的文件: displayUpdateDownloadWindow.downloadingFile=正在下載:{0} displayUpdateDownloadWindow.verifiedSigs=驗證驗證: displayUpdateDownloadWindow.status.downloading=下載文件... displayUpdateDownloadWindow.status.verifying=驗證驗證中... displayUpdateDownloadWindow.button.label=下載安裝程序並驗證驗證 displayUpdateDownloadWindow.button.downloadLater=稍後下載 displayUpdateDownloadWindow.button.ignoreDownload=忽略這個版本 displayUpdateDownloadWindow.headline=Haveno 有新的更新! displayUpdateDownloadWindow.download.failed.headline=下載失敗 displayUpdateDownloadWindow.download.failed=下載失敗。\n請到 https://haveno.io/downloads 下載並驗證。 displayUpdateDownloadWindow.installer.failed=無法確定正確的安裝程序。請通過 https://haveno.exchange/downloads 手動下載和驗證。 displayUpdateDownloadWindow.verify.failed=驗證失敗。\n請到 https://haveno.io/downloads 手動下載和驗證。 displayUpdateDownloadWindow.success=新版本成功下載並驗證驗證 。\n\n請打開下載目錄,關閉應用程序並安裝最新版本。 displayUpdateDownloadWindow.download.openDir=打開下載目錄 disputeSummaryWindow.title=概要 disputeSummaryWindow.openDate=工單創建時間 disputeSummaryWindow.role=交易者的角色 disputeSummaryWindow.payout=交易金額支付 disputeSummaryWindow.payout.getsTradeAmount=XMR {0} 獲得交易金額支付 disputeSummaryWindow.payout.getsAll=最大 XMR 支付數 {0} disputeSummaryWindow.payout.custom=自定義支付 disputeSummaryWindow.payoutAmount.buyer=買家支付金額 disputeSummaryWindow.payoutAmount.seller=賣家支付金額 disputeSummaryWindow.payoutAmount.invert=使用失敗者作為發佈者 disputeSummaryWindow.reason=糾紛的原因 disputeSummaryWindow.tradePeriodEnd=Trade period end disputeSummaryWindow.extraInfo=Extra information disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BUG=Bug # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.USABILITY=可用性 # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PROTOCOL_VIOLATION=違反協議 # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.NO_REPLY=不回覆 # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SCAM=詐騙 # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OTHER=其他 # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.BANK_PROBLEMS=銀行 # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.OPTION_TRADE=可選交易 # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Trader not responding # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=錯誤的發送者賬號 # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.PEER_WAS_LATE=交易夥伴已超時 # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=交易已穩定 disputeSummaryWindow.summaryNotes=總結説明 disputeSummaryWindow.addSummaryNotes=添加總結説明 disputeSummaryWindow.close.button=關閉話題 # Do no change any line break or order of tokens as the structure is used for signature verification # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.msg=工單已關閉{0}\n{1} 節點地址:{12}\n\n總結:\n交易 ID:{3}\n貨幣:{4}\n交易金額:{5}\nXMR 買家支付金額:{6}\nXMR 賣家支付金額:{7}\n\n糾紛原因:{8}\n\n總結:\n{9}\n # Do no change any line break or order of tokens as the structure is used for signature verification disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3} disputeSummaryWindow.close.nextStepsForMediation=\n\n下一個步驟:\n打開未完成交易,接受或拒絕建議的調解員的建議 disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\n下一個步驟:\n不需要您採取進一步的行動。如果仲裁員做出了對你有利的裁決,你將在 資金/交易 頁中看到“仲裁退款”交易 disputeSummaryWindow.close.closePeer=你也需要關閉交易對象的話題! disputeSummaryWindow.close.txDetails.headline=發佈交易退款 # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.buyer=買方收到{0}在地址:{1} # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=賣方收到{0}在地址:{1} disputeSummaryWindow.close.txDetails=費用:{0}\n{1}{2}交易費:{3}\n\n您確定要發佈此交易嗎? disputeSummaryWindow.close.noPayout.headline=未支付關閉 disputeSummaryWindow.close.noPayout.text=你想要在未作支付的情況下關閉嗎? emptyWalletWindow.headline={0} 錢包急救工具 emptyWalletWindow.info=請在緊急情況下使用,如果您無法從 UI 中訪問您的資金。\n\n請注意,使用此工具時,所有未結報價將自動關閉。\n\n在使用此工具之前,請備份您的數據目錄。您可以在“帳户/備份”中執行此操作。\n\n請報吿我們您的問題,並在 Github 或 Haveno 論壇上提交錯誤報吿,以便我們可以調查導致問題的原因。 emptyWalletWindow.balance=您的可用錢包餘額 emptyWalletWindow.address=輸入您的目標地址 emptyWalletWindow.button=發送全部資金 emptyWalletWindow.openOffers.warn=您有已發佈的報價,如果您清空錢包將被刪除。\n你確定要清空你的錢包嗎? emptyWalletWindow.openOffers.yes=是的,我確定 emptyWalletWindow.sent.success=您的錢包的餘額已成功轉移。 enterPrivKeyWindow.headline=輸入密鑰進行註冊 filterWindow.headline=編輯篩選列表 filterWindow.offers=篩選報價(用逗號“,”隔開) filterWindow.onions=Banned from trading addresses (comma sep.) filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.) filterWindow.accounts=篩選交易賬户數據:\n格式:逗號分割的 [付款方式ID|數據字段|值] filterWindow.bannedCurrencies=篩選貨幣代碼(用逗號“,”隔開) filterWindow.bannedPaymentMethods=篩選支付方式 ID(用逗號“,”隔開) filterWindow.bannedAccountWitnessSignerPubKeys=已過濾的帳户證人簽名者公鑰(逗號分隔十六進制公鑰) filterWindow.bannedPrivilegedDevPubKeys=已過濾的特權開發者公鑰(逗號分隔十六進制公鑰) filterWindow.arbitrators=篩選後的仲裁人(用逗號“,”隔開的洋葱地址) filterWindow.mediators=篩選後的調解員(用逗號“,”隔開的洋葱地址) filterWindow.refundAgents=篩選後的退款助理(用逗號“,”隔開的洋葱地址) filterWindow.seedNode=篩選後的種子節點(用逗號“,”隔開的洋葱地址) filterWindow.priceRelayNode=篩選後的價格中繼節點(用逗號“,”隔開的洋葱地址) filterWindow.xmrNode=篩選後的比特幣節點(用逗號“,”隔開的地址+端口) filterWindow.preventPublicXmrNetwork=禁止使用公共比特幣網絡 filterWindow.disableAutoConf=禁用自動確認 filterWindow.autoConfExplorers=已過濾自動確認瀏覽器(逗號分隔地址) filterWindow.disableTradeBelowVersion=交易最低所需要的版本 filterWindow.add=添加篩選 filterWindow.remove=移除篩選 filterWindow.xmrFeeReceiverAddresses=比特幣手續費接收地址 filterWindow.disableApi=Disable API filterWindow.disableMempoolValidation=Disable Mempool Validation offerDetailsWindow.minXmrAmount=最小 XMR 數量 offerDetailsWindow.min=(最小 {0}) offerDetailsWindow.distance=(與市場價格的差距:{0}) offerDetailsWindow.myTradingAccount=我的交易賬户 offerDetailsWindow.offererBankId=(賣家的銀行 ID/BIC/SWIFT) offerDetailsWindow.offerersBankName=(賣家的銀行名稱): offerDetailsWindow.bankId=銀行 ID(例如 BIC 或者 SWIFT ): offerDetailsWindow.countryBank=賣家銀行所在國家或地區 offerDetailsWindow.commitment=承諾 offerDetailsWindow.agree=我同意 offerDetailsWindow.tac=條款和條件 offerDetailsWindow.confirm.maker.buy=確認:建立以 {0} 買入 XMR 的報價 offerDetailsWindow.confirm.maker.sell=確認:建立以 {0} 賣出 XMR 的報價 offerDetailsWindow.confirm.taker.buy=確認:接受以 {0} 買入 XMR 的報價 offerDetailsWindow.confirm.taker.sell=確認:接受以 {0} 賣出 XMR 的報價 offerDetailsWindow.creationDate=創建時間 offerDetailsWindow.makersOnion=賣家的匿名地址 offerDetailsWindow.challenge=提供密碼 offerDetailsWindow.challenge.copy=複製密語以與對方分享 qRCodeWindow.headline=二維碼 qRCodeWindow.msg=請使用二維碼從外部錢包充值至 Haveno 錢包 qRCodeWindow.request=付款請求:\n{0} selectDepositTxWindow.headline=選擇糾紛的存款交易 selectDepositTxWindow.msg=存款交易未存儲在交易中。\n請從您的錢包中選擇一個現有的多重驗證交易,這是在失敗的交易中使用的存款交易。\n\n您可以通過打開交易詳細信息窗口(點擊列表中的交易 ID)並按照交易費用支付交易輸出到您看到多重驗證存款交易的下一個交易(地址從3開始),找到正確的交易。 該交易 ID 應在此處列出的列表中顯示。 一旦您找到正確的交易,請在此處選擇該交易並繼續\n\n抱歉給您帶來不便,但是錯誤的情況應該非常罕見,將來我們會嘗試找到更好的解決方法。 selectDepositTxWindow.select=選擇存款交易 sendAlertMessageWindow.headline=發送全球通知 sendAlertMessageWindow.alertMsg=提醒消息 sendAlertMessageWindow.enterMsg=輸入消息: sendAlertMessageWindow.isSoftwareUpdate=Software download notification sendAlertMessageWindow.isUpdate=Is full release sendAlertMessageWindow.isPreRelease=Is pre-release sendAlertMessageWindow.version=新版本號 sendAlertMessageWindow.send=發送通知 sendAlertMessageWindow.remove=移除通知 sendPrivateNotificationWindow.headline=發送私信 sendPrivateNotificationWindow.privateNotification=私人通知 sendPrivateNotificationWindow.enterNotification=輸入通知 sendPrivateNotificationWindow.send=發送私人通知 showWalletDataWindow.walletData=錢包數據 showWalletDataWindow.includePrivKeys=包含私鑰 setXMRTxKeyWindow.headline=證明已發送 XMR setXMRTxKeyWindow.note=在下面添加 tx 信息可以更快的自動確認交易。更多信息::https://haveno.exchange/wiki/Trading_Monero setXMRTxKeyWindow.txHash=交易 ID (可選) setXMRTxKeyWindow.txKey=交易密鑰 (可選) # We do not translate the tac because of the legal nature. We would need translations checked by lawyers # in each language which is too expensive atm. tacWindow.headline=用户協議 tacWindow.agree=我同意 tacWindow.disagree=我不同意並退出 tacWindow.arbitrationSystem=糾紛解決方案 tradeDetailsWindow.headline=交易 tradeDetailsWindow.disputedPayoutTxId=糾紛支付交易 ID tradeDetailsWindow.tradeDate=交易時間 tradeDetailsWindow.txFee=礦工手續費 tradeDetailsWindow.tradePeersOnion=交易夥伴匿名地址 tradeDetailsWindow.tradePeersPubKeyHash=交易夥伴公鑰哈希值 tradeDetailsWindow.tradeState=交易狀態 tradeDetailsWindow.agentAddresses=仲裁員/調解員 tradeDetailsWindow.detailData=Detail data txDetailsWindow.headline=Transaction Details txDetailsWindow.xmr.noteSent=您已發送XMR。 txDetailsWindow.xmr.noteReceived=您已收到 XMR。 txDetailsWindow.sentTo=發送至 txDetailsWindow.receivedWith=已收到與 txDetailsWindow.txId=TxId closedTradesSummaryWindow.headline=Trade history summary closedTradesSummaryWindow.totalAmount.title=Total trade amount closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) closedTradesSummaryWindow.totalTradeFeeInXmr.title=Sum of all trade fees paid in XMR closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} of total trade amount) walletPasswordWindow.headline=輸入密碼解鎖 xmrConnectionError.headline=Monero 連線錯誤 xmrConnectionError.providedNodes=連線至提供的 Monero 節點時發生錯誤。\n\n是否要使用下一個最佳可用的 Monero 節點? xmrConnectionError.customNodes=連線至您的自訂 Monero 節點時發生錯誤。\n\n是否要使用下一個最佳可用的 Monero 節點? xmrConnectionError.localNode=Haveno 先前已連線至本機的 Monero 節點,但目前已無法連線。\n\n請確認您的本機節點正在執行且已完全同步,或選擇其他選項以繼續。 xmrConnectionError.localNode.start=啟動本機節點 xmrConnectionError.localNode.start.error=啟動本機節點時發生錯誤 xmrConnectionError.localNode.fallback=連線至下一個最佳節點 torNetworkSettingWindow.header=Tor 網絡設置 torNetworkSettingWindow.noBridges=不使用網橋 torNetworkSettingWindow.providedBridges=連接到提供的網橋 torNetworkSettingWindow.customBridges=輸入自定義網橋 torNetworkSettingWindow.transportType=傳輸類型 torNetworkSettingWindow.obfs3=obfs3 torNetworkSettingWindow.obfs4=obfs4(推薦) torNetworkSettingWindow.meekAmazon=meek-amazon torNetworkSettingWindow.meekAzure=meek-azure torNetworkSettingWindow.enterBridge=輸入一個或多個網橋中繼節點(每行一個) torNetworkSettingWindow.enterBridgePrompt=輸入地址:端口 torNetworkSettingWindow.restartInfo=您需要重新啟動以應用更改 torNetworkSettingWindow.openTorWebPage=打開 Tor Project 網頁 torNetworkSettingWindow.deleteFiles.header=連接問題? torNetworkSettingWindow.deleteFiles.info=如果您在啟動時有重複的連接問題,刪除過期的 Tor 文件可能會有所幫助。如果要嘗試修復,請單擊下面的按鈕,然後重新啟動。 torNetworkSettingWindow.deleteFiles.button=刪除過期的 Tor 文件並關閉 torNetworkSettingWindow.deleteFiles.progress=關閉正在運行中的 Tor torNetworkSettingWindow.deleteFiles.success=過期的 Tor 文件被成功刪除。請重新啟動。 torNetworkSettingWindow.bridges.header=Tor 網絡被屏蔽? torNetworkSettingWindow.bridges.info=如果 Tor 被您的 Internet 提供商或您的國家或地區屏蔽,您可以嘗試使用 Tor 網橋。\n \n訪問 Tor 網頁:https://bridges.torproject.org,瞭解關於網橋和可插拔傳輸的更多信息。 feeOptionWindow.headline=選擇貨幣支付交易手續費 feeOptionWindow.optionsLabel=選擇貨幣支付交易手續費 feeOptionWindow.useXMR=使用 XMR feeOptionWindow.fee={0}(≈ {1}) feeOptionWindow.xmrFeeWithFiatAndPercentage={0} (≈ {1} / {2}) feeOptionWindow.xmrFeeWithPercentage={0} ({1}) #################################################################### # Popups #################################################################### popup.headline.notification=通知 popup.headline.instruction=請注意: popup.headline.attention=注意 popup.headline.backgroundInfo=背景資料 popup.headline.feedback=完成 popup.headline.confirmation=確定 popup.headline.information=資料 popup.headline.warning=警吿 popup.headline.error=錯誤 popup.doNotShowAgain=不要再顯示 popup.reportError.log=打開日誌文件 popup.reportError.gitHub=報吿至 Github issue tracker popup.reportError={0}\n\n為了幫助我們改進軟件,請在 https://github.com/haveno-dex/haveno/issues 上打開一個新問題來報吿這個 bug 。\n\n當您單擊下面任意一個按鈕時,上面的錯誤消息將被複制到剪貼板。\n\n如果您通過按下“打開日誌文件”,保存一份副本,並將其附加到 bug 報吿中,如果包含 haveno.log 文件,那麼調試就會變得更容易。 popup.error.tryRestart=請嘗試重啟您的應用程序或者檢查您的網絡連接。 popup.error.takeOfferRequestFailed=當有人試圖接受你的報價時發生了一個錯誤:\n{0} error.spvFileCorrupted=讀取 SPV 鏈文件時發生錯誤。\n可能是 SPV 鏈文件被破壞了。\n\n錯誤消息:{0}\n\n要刪除它並開始重新同步嗎? error.deleteAddressEntryListFailed=無法刪除 AddressEntryList 文件。\n \n錯誤:{0} error.closedTradeWithUnconfirmedDepositTx=交易 ID 為 {0} 的已關閉交易的保證金交易仍未確認。\n \n請在“設置/網絡信息”進行 SPV 重新同步,以查看交易是否有效。 error.closedTradeWithNoDepositTx=交易 ID 為 {0} 的保證金交易已被確認。\n\n請重新啟動應用程序來清理已關閉的交易列表。 popup.warning.walletNotInitialized=錢包至今未初始化 popup.warning.osxKeyLoggerWarning=由於 MacOS 10.14 及更高版本中的安全措施更加嚴格,因此啟動 Java 應用程序(Haveno 使用Java)會在 MacOS 中引發彈出警吿(``Haveno 希望從任何應用程序接收擊鍵').\n\n為了避免該問題,請打開“ MacOS 設置”,然後轉到“安全和隱私”->“隱私”->“輸入監視”,然後從右側列表中刪除“ Haveno”。\n\n一旦解決了技術限制(所需的 Java 版本的 Java 打包程序尚未交付),Haveno將升級到新的 Java 版本,以避免該問題。 popup.warning.wrongVersion=您這台電腦上可能有錯誤的 Haveno 版本。\n您的電腦的架構是:{0}\n您安裝的 Haveno 二進制文件是:{1}\n請關閉並重新安裝正確的版本({2})。 popup.warning.incompatibleDB=我們檢測到不兼容的數據庫文件!\n\n那些數據庫文件與我們當前的代碼庫不兼容:\n{0}\n\n我們對損壞的文件進行了備份,並將默認值應用於新的數據庫版本。\n\n備份位於:\n{1}/db/backup_of_corrupted_data。\n\n請檢查您是否安裝了最新版本的 Haveno\n您可以下載:\nhttps://haveno.exchange/downloads\n\n請重新啟動應用程序。 popup.warning.startupFailed.twoInstances=Haveno 已經在運行。 您不能運行兩個 Haveno 實例。 popup.warning.tradePeriod.halfReached=您與 ID {0} 的交易已達到最長交易期的一半,且仍未完成。\n\n交易期結束於 {1}\n\n請查看“業務/未完成交易”的交易狀態,以獲取更多信息。 popup.warning.tradePeriod.ended=您與 ID {0} 的已達到最長交易期,且未完成。\n\n交易期結束於 {1}\n\n請查看“業務/未完成交易”的交易狀態,以從調解員獲取更多信息。 popup.warning.noTradingAccountSetup.headline=您還沒有設置交易賬户 popup.warning.noTradingAccountSetup.msg=您需要設置法定貨幣或數字貨幣賬户才能創建報價。\n您要設置帳户嗎? popup.warning.noArbitratorsAvailable=沒有仲裁員可用。 popup.warning.noMediatorsAvailable=沒有調解員可用。 popup.warning.notFullyConnected=您需要等到您完全連接到網絡\n在啟動時可能需要2分鐘。 popup.warning.notSufficientConnectionsToXmrNetwork=你需要等待至少有{0}個與比特幣網絡的連接點。 popup.warning.downloadNotComplete=您需要等待,直到丟失的比特幣區塊被下載完畢。 popup.warning.walletNotSynced=Haveno 錢包尚未與最新的區塊鏈高度同步。請等待錢包同步完成或檢查您的連接。 popup.warning.removeOffer=您確定要移除該報價嗎? popup.warning.tooLargePercentageValue=您不能設置100%或更大的百分比。 popup.warning.examplePercentageValue=請輸入百分比數字,如 5.4% 是“5.4” popup.warning.noPriceFeedAvailable=該貨幣沒有可用的價格。 你不能使用基於百分比的價格。\n請選擇固定價格。 popup.warning.sendMsgFailed=向您的交易對象發送消息失敗。\n請重試,如果繼續失敗報吿錯誤。 popup.warning.messageTooLong=您的信息超過最大允許的大小。請將其分成多個部分發送,或將其上傳到 https://pastebin.com 之類的服務器。 popup.warning.lockedUpFunds=你已經從一個失敗的交易中凍結了資金。\n凍結餘額:{0}\n存款tx地址:{1}\n交易單號:{2}\n\n請通過選擇待處理交易界面中的交易並點擊“alt + o”或“option+ o”打開幫助話題。 popup.warning.moneroConnection=連接到 Monero 網路時發生問題。\n\n{0} popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n takeOffer.cancelButton=Cancel take-offer takeOffer.warningButton=忽略並繼續 # suppress inspection "UnusedProperty" popup.warning.nodeBanned=其中一個 {0} 節點已被禁用 # suppress inspection "UnusedProperty" popup.warning.priceRelay=價格傳遞 popup.warning.seed=種子 popup.warning.mandatoryUpdate.trading=請更新到最新的 Haveno 版本。強制更新禁止了舊版本進行交易。更多信息請訪問 Haveno 論壇。 popup.warning.noFilter=我們未從種子節點收到過濾器物件。請通知網路管理員註冊過濾器物件。 popup.warning.burnXMR=這筆交易是無法實現,因為 {0} 的挖礦手續費用會超過 {1} 的轉賬金額。請等到挖礦手續費再次降低或您積累了更多的 XMR 來轉賬。 popup.warning.openOffer.makerFeeTxRejected=交易 ID 為 {0} 的掛單費交易被比特幣網絡拒絕。\n交易 ID = {1}\n交易已被移至失敗交易。\n請到“設置/網絡信息”進行 SPV 重新同步。\n如需更多幫助,請聯繫 Haveno Keybase 團隊的 Support 頻道 popup.warning.trade.txRejected.tradeFee=交易手續費 popup.warning.trade.txRejected.deposit=押金 popup.warning.trade.txRejected=The {0} transaction for trade with ID {1} was rejected by the Monero network.\nTransaction ID={2}\nThe trade has been moved to failed trades.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. popup.warning.openOfferWithInvalidMakerFeeTx=交易 ID 為 {0} 的掛單費交易無效。\n交易 ID = {1}。\n請到“設置/網絡信息”進行 SPV 重新同步。\n如需更多幫助,請聯繫 Haveno Keybase 團隊的 Support 頻道 popup.info.securityDepositInfo=為了確保雙方都遵守交易協議,雙方都需要支付保證金。\n\n這筆存款一直保存在您的交易錢包裏,直到您的交易成功完成,然後再退還給您。\n\n請注意:如果您正在創建一個新的報價,Haveno 需要運行另一個交易員接受它。為了讓您的報價在線,保持 Haveno 運行,並確保這台計算機也在線(即,確保它沒有切換到待機模式…顯示器可以待機)。 popup.info.cashDepositInfo=請確保您在您的地區有一個銀行分行,以便能夠進行現金存款。\n賣方銀行的銀行 ID(BIC/SWIFT)為:{0}。 popup.info.cashDepositInfo.confirm=我確認我可以支付保證金 popup.info.shutDownWithOpenOffers=Haveno 正在被關閉,但仍有公開的報價。\n\n當 Haveno 關閉時,這些提供將不能在 P2P 網絡上使用,但是它們將在您下次啟動 Haveno 時重新發布到 P2P 網絡上。\n\n為了讓您的報價在線,保持 Haveno 運行,並確保這台計算機也在線(即,確保它不會進入待機模式…顯示器待機不是問題)。 popup.info.qubesOSSetupInfo=你似乎好像在 Qubes OS 上運行 Haveno。\n\n請確保您的 Haveno qube 是參考設置指南的説明設置的 https://haveno.exchange/wiki/Running_Haveno_on_Qubes popup.warn.downGradePrevention=不支持從 {0} 版本降級到 {1} 版本。請使用最新的 Haveno 版本。 popup.privateNotification.headline=重要私人通知! popup.securityRecommendation.headline=重要安全建議 popup.securityRecommendation.msg=如果您還沒有啟用,我們想提醒您考慮為您的錢包使用密碼保護。\n\n強烈建議你寫下錢包還原密鑰。 那些還原密鑰就是恢復你的比特幣錢包的主密碼。\n在“錢包密鑰”部分,您可以找到更多信息\n\n此外,您應該在“備份”界面備份完整的應用程序數據文件夾。 popup.shutDownInProgress.headline=正在關閉 popup.shutDownInProgress.msg=關閉應用可能會花一點時間。\n請不要打斷關閉過程。 popup.attention.forTradeWithId=交易 ID {0} 需要注意 popup.attention.reasonForPaymentRuleChange=Version 1.5.5 introduces a critical trade rule change regarding the \"reason for payment\" field in bank transfers. Please leave this field empty -- DO NOT use the trade ID as \"reason for payment\" anymore. popup.info.multiplePaymentAccounts.headline=多個支付賬户可用 popup.info.multiplePaymentAccounts.msg=您有多個支付帳户在這個報價中可用。請確你做了正確的選擇。 popup.accountSigning.selectAccounts.headline=選擇付款賬户 popup.accountSigning.selectAccounts.description=根據付款方式和時間點,所有與支付給買方的付款發生的爭議有關的付款帳户將被選擇讓您驗證。 popup.accountSigning.selectAccounts.signAll=驗證所有付款方式 popup.accountSigning.selectAccounts.datePicker=選擇要驗證的帳户的時間點 popup.accountSigning.confirmSelectedAccounts.headline=確認選定的付款帳户 popup.accountSigning.confirmSelectedAccounts.description=根據您的輸入,將選擇 {0} 支付帳户。 popup.accountSigning.confirmSelectedAccounts.button=確認付款賬户 popup.accountSigning.signAccounts.headline=確認驗證付款賬户 popup.accountSigning.signAccounts.description=根據您的選擇,{0} 付款帳户將被驗證。 popup.accountSigning.signAccounts.button=驗證付款賬户 popup.accountSigning.signAccounts.ECKey=輸入仲裁員密鑰 popup.accountSigning.signAccounts.ECKey.error=不正確的仲裁員 ECKey popup.accountSigning.success.headline=恭喜 popup.accountSigning.success.description=所有 {0} 支付賬户已成功驗證! popup.accountSigning.generalInformation=您將在帳户頁面找到所有賬户的驗證狀態。\n\n更多信息,請訪問https://docs.haveno.exchange/payment-methods#account-signing. popup.accountSigning.signedByArbitrator=您的一個付款帳户已被認證以及被仲裁員驗證。交易成功後,使用此帳户將自動驗證您的交易夥伴的帳户。\n\n{0} popup.accountSigning.signedByPeer=您的一個付款帳户已經被交易夥伴驗證和驗證。您的初始交易限額將被取消,您將能夠在{0}天后驗證其他帳户。 popup.accountSigning.peerLimitLifted=您其中一個帳户的初始限額已被取消。\n\n{0} popup.accountSigning.peerSigner=您的一個帳户已足夠成熟,可以驗證其他付款帳户,您的一個帳户的初始限額已被取消。\n\n{0} popup.accountSigning.singleAccountSelect.headline=Import unsigned account age witness popup.accountSigning.confirmSingleAccount.headline=確認所選賬齡證據 popup.accountSigning.confirmSingleAccount.selectedHash=已選擇證據哈希值 popup.accountSigning.confirmSingleAccount.button=驗證賬齡證據 popup.accountSigning.successSingleAccount.description=證據 {0} 已被驗證 popup.accountSigning.successSingleAccount.success.headline=成功 popup.accountSigning.unsignedPubKeys.headline=未驗證公鑰 popup.accountSigning.unsignedPubKeys.sign=驗證公鑰 popup.accountSigning.unsignedPubKeys.signed=公鑰已被驗證 popup.accountSigning.unsignedPubKeys.result.signed=已驗證公鑰 popup.accountSigning.unsignedPubKeys.result.failed=未能驗證公鑰 popup.info.buyerAsTakerWithoutDeposit.headline=買家無需支付保證金 popup.info.buyerAsTakerWithoutDeposit=您的報價不需要來自XMR買家的保證金或費用。\n\n要接受您的報價,您必須與您的交易夥伴在Haveno之外分享密碼短語。\n\n密碼短語會自動生成並在報價創建後顯示在報價詳情中。 #################################################################### # Notifications #################################################################### notification.trade.headline=交易 ID {0} 的通知 notification.ticket.headline=交易 ID {0} 的幫助話題 notification.trade.completed=交易現在完成,您可以提取資金。 notification.trade.accepted=您 XMR {0} 的報價被接受。 notification.trade.unlocked=您的交易至少有一個區塊鏈確認。\n您現在可以開始付款。 notification.trade.paymentSent=XMR 買家已經開始付款。 notification.trade.selectTrade=選擇交易 notification.trade.peerOpenedDispute=您的交易對象創建了一個 {0}。 notification.trade.disputeClosed=這個 {0} 被關閉。 notification.walletUpdate.headline=交易錢包更新 notification.walletUpdate.msg=您的交易錢包充值成功。\n金額:{0} notification.takeOffer.walletUpdate.msg=您的交易錢包已經從早期的下單嘗試中得到足夠的資金支持。\n金額:{0} notification.tradeCompleted.headline=交易完成 notification.tradeCompleted.msg=您可以將資金提取到外部的門羅幣錢包,或保留在您的 Haveno 錢包中。 #################################################################### # System Tray #################################################################### systemTray.show=顯示應用程序窗口 systemTray.hide=隱藏應用程序窗口 systemTray.info=關於 Haveno 信息 systemTray.exit=退出 systemTray.tooltip=Haveno:去中心化比特幣交易網絡 #################################################################### # GUI Util #################################################################### guiUtil.accountExport.savedToPath=交易賬户保存在路徑:\n{0} guiUtil.accountExport.noAccountSetup=您沒有交易賬户設置導出。 guiUtil.accountExport.selectPath=選擇路徑 {0} # suppress inspection "TrailingSpacesInProperty" guiUtil.accountExport.tradingAccount=交易賬户 ID {0}\n # suppress inspection "TrailingSpacesInProperty" guiUtil.accountImport.noImport=我們沒有導入 ID {0} 的交易賬户,因為它已經存在。\n guiUtil.accountExport.exportFailed=導出 .CSV 失敗,因為發生了錯誤。\n錯誤 = {0} guiUtil.accountExport.selectExportPath=選擇導出路徑 guiUtil.accountImport.imported=交易賬户導入路徑:\n{0}\n\n導入賬户:\n{1} guiUtil.accountImport.noAccountsFound=在路徑 {0} 找不到導出的交易賬户。\n文件名為 {1}。 guiUtil.openWebBrowser.warning=您將在系統網絡瀏覽器中打開一個網頁。\n你現在要打開網頁嗎?\n\n如果您沒有使用“Tor 瀏覽器”作為默認的系統網絡瀏覽器,則將以默認連接到網頁。\n\n網址:“{0}” guiUtil.openWebBrowser.doOpen=打開網頁並且不要再詢問我 guiUtil.openWebBrowser.copyUrl=複製 URL 並取消 guiUtil.ofTradeAmount=的交易數量 guiUtil.requiredMinimum=(最低需求量) #################################################################### # Component specific #################################################################### list.currency.select=選擇幣種 list.currency.showAll=顯示全部 list.currency.editList=編輯幣種列表 table.placeholder.noItems=最近沒有可用的 {0} table.placeholder.noData=最近沒有可用數據 table.placeholder.processingData=處理數據… peerInfoIcon.tooltip.tradePeer=交易夥伴 peerInfoIcon.tooltip.maker=製造者 peerInfoIcon.tooltip.trade.traded={0} 匿名地址:{1}\n您已經與他交易過 {2} 次了\n{3} peerInfoIcon.tooltip.trade.notTraded={0} 匿名地址:{1}\n你還沒有與他交易過。\n{2} peerInfoIcon.tooltip.age=支付賬户在 {0} 前創建。 peerInfoIcon.tooltip.unknownAge=支付賬户賬齡未知。 tooltip.openPopupForDetails=打開彈出窗口的詳細信息 tooltip.invalidTradeState.warning=這個交易處於不可用狀態。打開詳情窗口以發現更多細節。 tooltip.openBlockchainForAddress=使用外部區塊鏈瀏覽器打開地址:{0} tooltip.openBlockchainForTx=使用外部區塊鏈瀏覽器打開交易:{0} confidence.unknown=未知交易狀態 confidence.seen=被 {0} 人查看 / 0 確定 confidence.confirmed={0} 確認(s) confidence.invalid=交易無效 peerInfo.title=對象資料 peerInfo.nrOfTrades=已完成交易數量 peerInfo.notTradedYet=你還沒有與他交易過。 peerInfo.setTag=設置該對象的標籤 peerInfo.age.noRisk=支付賬户賬齡 peerInfo.age.chargeBackRisk=自驗證 peerInfo.unknownAge=賬齡未知 addressTextField.openWallet=打開您的默認比特幣錢包 addressTextField.copyToClipboard=複製地址到剪貼板 addressTextField.addressCopiedToClipboard=地址已被複制到剪貼板 addressTextField.openWallet.failed=打開默認的比特幣錢包應用程序失敗了。或許您沒有安裝? peerInfoIcon.tooltip={0}\n標識:{1} txIdTextField.copyIcon.tooltip=複製交易 ID 到剪貼板 txIdTextField.blockExplorerIcon.tooltip=使用外部區塊鏈瀏覽器打開這個交易 ID txIdTextField.missingTx.warning.tooltip=所需的交易缺失 #################################################################### # Navigation #################################################################### navigation.account=“賬户” navigation.account.walletSeed=“賬户/錢包密鑰” navigation.funds.availableForWithdrawal=“資金/提現” navigation.portfolio.myOpenOffers=“資料/未完成報價” navigation.portfolio.pending=“業務/未完成交易” navigation.portfolio.closedTrades=“資料/歷史” navigation.funds.depositFunds=“資金/收到資金” navigation.settings.preferences=“設置/偏好” # suppress inspection "UnusedProperty" navigation.funds.transactions=“資金/交易記錄” navigation.support=“幫助” #################################################################### # Formatter #################################################################### formatter.formatVolumeLabel={0} 數量 {1} formatter.makerTaker=賣家 {0} {1} / 買家 {2} {3} formatter.makerTaker.locked=賣家 {0} {1} / 買家 {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=您是 {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=您創建新的報價 {0} {1} formatter.youAreCreatingAnOffer.traditional.locked=您創建新的報價 {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=您正創建報價 {0} {1}({2} {3}) formatter.youAreCreatingAnOffer.crypto.locked=您正創建報價 {0} {1}({2} {3}) 🔒 formatter.asMaker={0} {1} 是賣家 formatter.asTaker={0} {1} 是買家 #################################################################### # Domain specific #################################################################### # we use enum values here # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" XMR_MAINNET=XMR Mainnet # suppress inspection "UnusedProperty" XMR_LOCAL=XMR Testnet # suppress inspection "UnusedProperty" XMR_STAGENET=XMR Stagenet time.year=年線 time.month=月線 time.week=周線 time.day=日線 time.hour=小時 time.minute10=10分鐘 time.hours=小時 time.days=天 time.1hour=1小時 time.1day=1天 time.minute=分鐘 time.second=秒 time.minutes=分鐘 time.seconds=秒 password.enterPassword=輸入密碼 password.confirmPassword=確認密碼 password.tooLong=你輸入的密碼太長,不要超過 500 個字符。 password.deriveKey=從密碼中提取密鑰 password.walletDecrypted=錢包成功解密並移除密碼保護 password.wrongPw=你輸入了錯誤的密碼。\n\n請再次嘗試輸入密碼,仔細檢查拼寫錯誤。 password.walletEncrypted=錢包成功加密並開啟密碼保護。 password.passwordsDoNotMatch=這2個密碼您輸入的不相同 password.forgotPassword=忘記密碼? password.backupReminder=請注意,設置錢包密碼時,所有未加密的錢包的自動創建的備份將被刪除。\n\n強烈建議您備份應用程序的目錄,並在設置密碼之前記下您的還原密鑰! password.backupWasDone=我已備份 password.setPassword=Set Password (I already made a backup) password.makeBackup=Make Backup seed.seedWords=錢包密鑰 seed.enterSeedWords=輸入錢包密鑰 seed.date=錢包時間 seed.restore.title=使用還原密鑰恢復錢包 seed.restore=恢復錢包 seed.creationDate=創建時間 seed.warn.walletNotEmpty.msg=你的比特幣錢包不是空的。\n\n在嘗試恢復較舊的錢包之前,您必須清空此錢包,因為將錢包混在一起會導致無效的備份。\n\n請完成您的交易,關閉所有您的未完成報價,並轉到資金界面撤回您的比特幣。\n如果您無法訪問您的比特幣,您可以使用緊急工具清空錢包。\n要打開該應急工具,請按“alt + e”或“Cmd/Ctrl + e” 。 seed.warn.walletNotEmpty.restore=無論如何我要恢復 seed.warn.walletNotEmpty.emptyWallet=我先清空我的錢包 seed.warn.notEncryptedAnymore=你的錢包被加密了。\n\n恢復後,錢包將不再加密,您必須設置新的密碼。\n\n你要繼續嗎? seed.warn.walletDateEmpty=由於您尚未指定錢包日期,因此 Haveno 將必須掃描 2013.10.09(BIP39創始日期)之後的區塊鏈。\n\nBIP39 錢包於 Haveno 於 2017.06.28 首次發佈(版本 v0.5)。因此,您可以使用該日期來節省時間。\n\n理想情況下,您應指定創建錢包種子的日期。\n\n\n您確定要繼續而不指定錢包日期嗎? seed.restore.success=新的還原密鑰成功地恢復了錢包。\n\n您需要關閉並重新啟動應用程序。 seed.restore.error=使用還原密鑰恢復錢包時出現錯誤。{0} seed.restore.openOffers.warn=您有公開報價,如果您從種子詞恢復,則這些報價將被刪除。\n您確定要繼續嗎? #################################################################### # Payment methods #################################################################### payment.account=賬户 payment.account.no=賬户編號 payment.account.name=賬户名稱 payment.account.username=用户暱稱 payment.account.phoneNr=電話號碼 payment.account.owner.fullname=賬户擁有者姓名: payment.account.fullName=全稱(名,中間名,姓) payment.account.state=州/省/地區 payment.account.city=城市 payment.bank.country=銀行所在國家或地區 payment.account.name.email=賬户擁有者姓名/電子郵箱 payment.account.name.emailAndHolderId=賬户擁有者姓名/電子郵箱 / {0} payment.bank.name=銀行名稱 payment.select.account=選擇賬户類型 payment.select.region=選擇地區 payment.select.country=選擇國家或地區 payment.select.bank.country=選擇銀行所在國家或地區 payment.foreign.currency=你確定想選擇一個與此國家或地區默認幣種不同的貨幣? payment.restore.default=不,恢復默認值 payment.email=電子郵箱 payment.country=國家或地區 payment.extras=額外要求 payment.email.mobile=電子郵箱或手機號碼 payment.crypto.address=數字貨幣地址 payment.crypto.tradeInstantCheckbox=使用數字貨幣進行即時交易( 1 小時內) payment.crypto.tradeInstant.popup=對於即時交易,要求交易雙方都在線,能夠在不到1小時內完成交易。\n \n如果你已經有未完成的報價以及你不能即時完成,請在資料頁面禁用這些報價。 payment.crypto=數字貨幣 payment.select.crypto=選擇或搜索數字貨幣 payment.secret=密保問題 payment.answer=答案 payment.wallet=錢包 ID payment.amazon.site=Buy giftcard at payment.ask=Ask in Trader Chat payment.uphold.accountId=用户名或電子郵箱或電話號碼 payment.moneyBeam.accountId=電子郵箱或者電話號碼 payment.popmoney.accountId=電子郵箱或者電話號碼 payment.promptPay.promptPayId=公民身份證/税號或電話號碼 payment.supportedCurrencies=支持的貨幣 payment.supportedCurrenciesForReceiver=Currencies for receiving funds payment.limitations=限制條件 payment.salt=帳户年齡驗證鹽值 payment.error.noHexSalt=鹽值需要十六進制的。\n如果您想要從舊帳户轉移鹽值以保留帳齡,只建議編輯鹽值字段。帳齡通過帳户鹽值和識別帳户數據(例如 IBAN )來驗證。 payment.accept.euro=接受來自這些歐元國家的交易 payment.accept.nonEuro=接受來自這些非歐元國家的交易 payment.accepted.countries=接受的國家 payment.accepted.banks=接受的銀行(ID) payment.mobile=手機號碼 payment.postal.address=郵寄地址 payment.national.account.id.AR=CBU 號碼 shared.accountSigningState=賬户驗證狀態 #new payment.crypto.address.dyn={0} 地址 payment.crypto.receiver.address=接收者的數字貨幣地址 payment.accountNr=賬號: payment.emailOrMobile=電子郵箱或手機號碼 payment.useCustomAccountName=使用自定義名稱 payment.maxPeriod=最大允許交易時限 payment.maxPeriodAndLimit=最大交易時間:{0}/ 最大買入:{1}/ 最大出售:{2}/賬齡:{3} payment.maxPeriodAndLimitCrypto=最大交易期限:{0}/最大交易限額:{1} payment.currencyWithSymbol=貨幣:{0} payment.nameOfAcceptedBank=接受的銀行名稱 payment.addAcceptedBank=添加接受的銀行 payment.clearAcceptedBanks=清除接受的銀行 payment.bank.nameOptional=銀行名稱(可選) payment.bankCode=銀行代碼 payment.bankId=銀行 ID (BIC/SWIFT): payment.bankIdOptional=銀行 ID(BIC/SWIFT)(可選) payment.branchNr=分行編碼 payment.branchNrOptional=分行編碼(可選) payment.accountNrLabel=賬號(IBAN) payment.accountType=賬户類型 payment.checking=檢查 payment.savings=保存 payment.personalId=個人 ID payment.zelle.info=Zelle是一項轉賬服務,轉賬到其他銀行做的很好。\n\n1.檢查此頁面以查看您的銀行是否(以及如何)與 Zelle 合作:\nhttps://www.zellepay.com/get-started\n\n2.特別注意您的轉賬限額-匯款限額因銀行而異,銀行通常分別指定每日,每週和每月的限額。\n\n3.如果您的銀行不能使用 Zelle,您仍然可以通過 Zelle 移動應用程序使用它,但是您的轉賬限額會低得多。\n\n4.您的 Haveno 帳户上指定的名稱必須與 Zelle/銀行帳户上的名稱匹配。 \n\n如果您無法按照貿易合同中的規定完成 Zelle 交易,則可能會損失部分(或全部)保證金。\n\n由於 Zelle 的拒付風險較高,因此建議賣家通過電子郵件或 SMS 與未簽名的買家聯繫,以確認買家確實擁有 Haveno 中指定的 Zelle 帳户。 payment.fasterPayments.newRequirements.info=有些銀行已經開始核實快捷支付收款人的全名。您當前的快捷支付帳户沒有填寫全名。\n\n請考慮在 Haveno 中重新創建您的快捷支付帳户,為將來的 {0} 買家提供一個完整的姓名。\n\n重新創建帳户時,請確保將銀行區號、帳户編號和帳齡驗證鹽值從舊帳户複製到新帳户。這將確保您現有的帳齡和簽名狀態得到保留。 payment.moneyGram.info=使用 MoneyGram 時,XMR 買方必須將授權號碼和收據的照片通過電子郵件發送給 XMR 賣方。收據必須清楚地顯示賣方的全名、國家或地區、州和金額。買方將在交易過程中顯示賣方的電子郵件。 payment.westernUnion.info=使用 Western Union 時,XMR 買方必須通過電子郵件將 MTCN(運單號)和收據照片發送給 XMR 賣方。收據上必須清楚地顯示賣方的全名、城市、國家或地區和金額。買方將在交易過程中顯示賣方的電子郵件。 payment.halCash.info=使用 HalCash 時,XMR 買方需要通過手機短信向 XMR 賣方發送 HalCash 代碼。\n\n請確保不要超過銀行允許您用半現金匯款的最高金額。每次取款的最低金額是 10 歐元,最高金額是 10 歐元。金額是 600 歐元。對於重複取款,每天每個接收者 3000 歐元,每月每個接收者 6000 歐元。請與您的銀行核對這些限額,以確保它們使用與此處所述相同的限額。\n\n提現金額必須是 10 歐元的倍數,因為您不能從 ATM 機提取其他金額。 創建報價和下單屏幕中的 UI 將調整 XMR 金額,使 EUR 金額正確。你不能使用基於市場的價格,因為歐元的數量會隨着價格的變化而變化。\n # suppress inspection "UnusedMessageFormatParameter" payment.limits.info=請注意,所有銀行轉賬都有一定的退款風險。為了降低這一風險,Haveno 基於使用的付款方式的退款風險。\n\n對於付款方式,您的每筆交易的出售和購買的限額為{2}\n\n限制只應用在單筆交易,你可以儘可能多的進行交易。\n\n在 Haveno Wiki 查看更多信息[HYPERLINK:https://docs.haveno.exchange/overview/account_limits]。 # suppress inspection "UnusedProperty" payment.limits.info.withSigning=為了降低這一風險,Haveno 基於兩個因素對該付款方式每筆交易設置了限制:\n\n1. 使用的付款方法的預估退款風險水平\n2. 您的付款方式的賬齡\n\n這個付款賬户還沒有被驗證,所以他每個交易最多購買{0}。在驗證之後,購買限制會以以下規則逐漸增加:\n\n●簽署前,以及簽署後30天內,您的每筆最大交易將限制為{0}\n●簽署後30天,每筆最大交易將限制為{1}\n●簽署後60天,每筆最大交易將限制為{2}\n\n出售限制不會被賬户驗證狀態限制,你可以理科進行單筆為{2}的交易\n\n限制只應用在單筆交易,你可以儘可能多的進行交易。\n\n在 Haveno Wiki 上查看更多:\nhttps://docs.haveno.exchange/overview/account_limits payment.cashDeposit.info=請確認您的銀行允許您將現金存款匯入他人賬户。例如,美國銀行和富國銀行不再允許此類存款。 payment.revolut.info=Revolut 要求使用“用户名”作為帳户 ID,而不是像以往的電話號碼或電子郵件。 payment.account.revolut.addUserNameInfo={0}\n您現有的 Revolut 帳户({1})尚未設置“用户名”。\n請輸入您的 Revolut ``用户名'以更新您的帳户數據。\n這不會影響您的賬齡驗證狀態。 payment.revolut.addUserNameInfo.headLine=更新 Revolut 賬户 payment.cashapp.info=請注意,Cash App 的退款風險高於大多數銀行轉帳。 payment.venmo.info=請注意,Venmo 的退款風險高於大多數銀行轉帳。 payment.paypal.info=請注意,PayPal 的退款風險高於大多數銀行轉帳。 payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account payment.usPostalMoneyOrder.info=在 Haveno 上交易 US Postal Money Orders (USPMO)您必須理解下述條款:\n\n- XMR 買方必須在發送方和收款人字段中都寫上 XMR 賣方的名稱,並在發送之前對 USPMO 和信封進行高分辨率照片拍照,並帶有跟蹤證明。\n- XMR 買方必須將 USPMO 連同交貨確認書一起發送給 XMR 賣方。\n\n如果需要調解,或有交易糾紛,您將需要將照片連同 USPMO 編號,郵局編號和交易金額一起發送給 Haveno 調解員或退款代理,以便他們進行驗證美國郵局網站上的詳細信息。\n\n如未能提供要求的交易數據將在糾紛中直接判負\n\n在所有爭議案件中,USPMO 發送方在向調解人或仲裁員提供證據/證明時承擔 100% 的責任。\n\n如果您不理解這些要求,請不要在 Haveno 上使用 USPMO 進行交易。 payment.payByMail.contact=聯繫方式 payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.f2f.contact=聯繫方式 payment.f2f.contact.prompt=您希望如何與交易夥伴聯繫?(電子郵箱、電話號碼、…) payment.f2f.city=“面對面”會議的城市 payment.f2f.city.prompt=城市將與報價一同顯示 payment.shared.optionalExtra=可選的附加信息 payment.shared.extraInfo=附加信息 payment.shared.extraInfo.offer=額外的優惠資訊 payment.shared.extraInfo.prompt.paymentAccount=定義您希望在此付款帳戶的報價中顯示的任何特殊術語、條件或細節(用戶在接受報價之前將看到這些資訊)。 payment.shared.extraInfo.prompt.offer=定義您希望在您的報價中顯示的任何特殊條款、條件或詳細資訊。 payment.shared.extraInfo.noDeposit=聯絡詳情及優惠條款 payment.f2f.info=與網上交易相比,“面對面”交易有不同的規則,也有不同的風險。\n\n主要區別是:\n●交易夥伴需要使用他們提供的聯繫方式交換關於會面地點和時間的信息。\n●交易雙方需要攜帶筆記本電腦,在會面地點確認“已發送付款”和“已收到付款”。\n●如果交易方有特殊的“條款和條件”,他們必須在賬户的“附加信息”文本框中聲明這些條款和條件。\n●在發生爭議時,調解員或仲裁員不能提供太多幫助,因為通常很難獲得有關會面上所發生情況的篡改證據。在這種情況下,XMR 資金可能會被無限期鎖定,或者直到交易雙方達成協議。\n\n為確保您完全理解“面對面”交易的不同之處,請閲讀以下説明和建議:“https://docs.haveno.exchange/trading-rules.html#f2f-trading” payment.f2f.info.openURL=打開網頁 payment.f2f.offerbook.tooltip.countryAndCity=國家或地區及城市:{0} / {1} payment.shared.extraInfo.tooltip=附加信息:{0} payment.japan.bank=銀行 payment.japan.branch=分行 payment.japan.account=賬户 payment.japan.recipient=名稱 payment.australia.payid=PayID payment.payid=PayID 需鏈接至金融機構。例如電子郵件地址或手機。 payment.payid.info=PayID,如電話號碼、電子郵件地址或澳大利亞商業號碼(ABN),您可以安全地連接到您的銀行、信用合作社或建立社會帳户。你需要在你的澳大利亞金融機構創建一個 PayID。發送和接收金融機構都必須支持 PayID。更多信息請查看[HYPERLINK:https://payid.com.au/faqs/] payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/overview/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=為了保護您的安全,我們強烈不建議使用 Paysafecard PIN 進行付款。\n\n\ 透過 PIN 進行的交易無法獨立驗證以進行爭議解決。如果發生問題,可能無法追回資金。\n\n\ 為確保交易安全並支持爭議解決,請始終使用可驗證記錄的付款方式。 # We use constants from the code so we do not use our normal naming convention # dynamic values are not recognized by IntelliJ # Only translate general terms NATIONAL_BANK=國內銀行轉賬 SAME_BANK=同銀行轉賬 SPECIFIC_BANKS=轉到指定銀行 US_POSTAL_MONEY_ORDER=美國郵政匯票 CASH_DEPOSIT=現金/ATM 存款 PAY_BY_MAIL=Pay By Mail MONEY_GRAM=MoneyGram WESTERN_UNION=西聯匯款 F2F=面對面(當面交易) JAPAN_BANK=日本銀行匯款 AUSTRALIA_PAYID=澳大利亞 PayID # suppress inspection "UnusedProperty" NATIONAL_BANK_SHORT=國內銀行 # suppress inspection "UnusedProperty" SAME_BANK_SHORT=相同銀行 # suppress inspection "UnusedProperty" SPECIFIC_BANKS_SHORT=指定銀行 # suppress inspection "UnusedProperty" US_POSTAL_MONEY_ORDER_SHORT=美國匯票 # suppress inspection "UnusedProperty" CASH_DEPOSIT_SHORT=現金/ATM 存款 # suppress inspection "UnusedProperty" PAY_BY_MAIL_SHORT=PayByMail # suppress inspection "UnusedProperty" MONEY_GRAM_SHORT=MoneyGram # suppress inspection "UnusedProperty" WESTERN_UNION_SHORT=西聯匯款 # suppress inspection "UnusedProperty" F2F_SHORT=F2F # suppress inspection "UnusedProperty" JAPAN_BANK_SHORT=Japan Furikomi # suppress inspection "UnusedProperty" AUSTRALIA_PAYID_SHORT=支付 ID # Do not translate brand names # suppress inspection "UnusedProperty" UPHOLD=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM=MoneyBeam(N26) # suppress inspection "UnusedProperty" POPMONEY=Popmoney # suppress inspection "UnusedProperty" REVOLUT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY=支付寶 # suppress inspection "UnusedProperty" WECHAT_PAY=微信支付 # suppress inspection "UnusedProperty" SEPA=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT=SEPA 即時支付 # suppress inspection "UnusedProperty" FASTER_PAYMENTS=更快的支付方式 # suppress inspection "UnusedProperty" SWISH=Swish # suppress inspection "UnusedProperty" ZELLE=Zelle(Zelle) # suppress inspection "UnusedProperty" CHASE_QUICK_PAY=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS=數字貨幣 # suppress inspection "UnusedProperty" PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=亞馬遜電子禮品卡 # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT=Cryptos Instant # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY=OKPay # suppress inspection "UnusedProperty" CASH_APP=Cash App # suppress inspection "UnusedProperty" VENMO=Venmo # suppress inspection "UnusedProperty" UPHOLD_SHORT=Uphold # suppress inspection "UnusedProperty" MONEY_BEAM_SHORT=MoneyBeam(N26) # suppress inspection "UnusedProperty" POPMONEY_SHORT=Popmoney # suppress inspection "UnusedProperty" REVOLUT_SHORT=Revolut # suppress inspection "UnusedProperty" PERFECT_MONEY_SHORT=Perfect Money # suppress inspection "UnusedProperty" ALI_PAY_SHORT=支付寶 # suppress inspection "UnusedProperty" WECHAT_PAY_SHORT=微信支付 # suppress inspection "UnusedProperty" SEPA_SHORT=SEPA # suppress inspection "UnusedProperty" SEPA_INSTANT_SHORT=SEPA Instant # suppress inspection "UnusedProperty" FASTER_PAYMENTS_SHORT=更快的支付方式 # suppress inspection "UnusedProperty" SWISH_SHORT=Swish # suppress inspection "UnusedProperty" ZELLE_SHORT=Zelle # suppress inspection "UnusedProperty" CHASE_QUICK_PAY_SHORT=Chase QuickPay # suppress inspection "UnusedProperty" INTERAC_E_TRANSFER_SHORT=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH_SHORT=HalCash # suppress inspection "UnusedProperty" BLOCK_CHAINS_SHORT=數字貨幣 # suppress inspection "UnusedProperty" PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=亞馬遜電子禮品卡 # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT_SHORT=Cryptos Instant # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY_SHORT=OKPay # suppress inspection "UnusedProperty" CASH_APP_SHORT=Cash App # suppress inspection "UnusedProperty" VENMO_SHORT=Venmo #################################################################### # Validation #################################################################### validation.empty=不允許留空。 validation.NaN=輸入的不是有效數字。 validation.notAnInteger=輸入的不是整數。 validation.zero=不允許輸入0。 validation.negative=不允許輸入負值。 validation.traditional.tooSmall=不允許輸入比最小可能值還小的數值。 validation.traditional.tooLarge=不允許輸入比最大可能值還大的數值。 validation.xmr.fraction=此充值將會產生小於 1 聰的比特幣數量。 validation.xmr.tooLarge=不允許充值大於{0} validation.xmr.tooSmall=不允許充值小於{0} validation.passwordTooShort=你輸入的密碼太短。最少 8 個字符。 validation.passwordTooLong=你輸入的密碼太長。最長不要超過50個字符。 validation.sortCodeNumber={0} 必須由 {1} 個數字構成。 validation.sortCodeChars={0} 必須由 {1} 個字符構成。 validation.bankIdNumber={0} 必須由 {1} 個數字構成。 validation.accountNr=賬號必須由 {0} 個數字構成。 validation.accountNrChars=賬户必須由 {0} 個字符構成。 validation.xmr.invalidAddress=地址不正確,請檢查地址格式。 validation.integerOnly=請輸入整數。 validation.inputError=您的輸入引起了錯誤:\n{0} validation.xmr.exceedsMaxTradeLimit=您的交易限額為 {0}。 validation.nationalAccountId={0} 必須由{1}個數字組成。 #new validation.invalidInput=輸入無效:{0} validation.accountNrFormat=帳號必須是格式:{0} # suppress inspection "UnusedProperty" validation.crypto.wrongStructure=地址驗證失敗,因為它與 {0} 地址的結構不匹配。 # suppress inspection "UnusedProperty" validation.crypto.ltz.zAddressesNotSupported=LTZ 地址需要以 L 開頭。 不支持以 Z 開頭的地址。 # suppress inspection "UnusedProperty" validation.crypto.zAddressesNotSupported=LTZ 地址需要以 L 開頭。 不支持以 Z 開頭的地址。 # suppress inspection "UnusedProperty" validation.crypto.invalidAddress=這個地址不是有效的{0}地址!{1} # suppress inspection "UnusedProperty" validation.crypto.liquidBitcoin.invalidAddress=不支持本地 segwit 地址(以“lq”開頭的地址)。 validation.bic.invalidLength=輸入長度既不是 8 也不是 11 validation.bic.letters=必須輸入銀行和國家或地區代碼 validation.bic.invalidLocationCode=BIC 包含無效的地址代碼 validation.bic.invalidBranchCode=BIC 包含無效的分行代碼 validation.bic.sepaRevolutBic=不支持 Revolut Sepa 賬户 validation.btc.invalidFormat=無效格式的比特幣地址 validation.email.invalidAddress=無效地址 validation.iban.invalidCountryCode=國家或地區代碼無效 validation.iban.checkSumNotNumeric=校驗必須是數字 validation.iban.nonNumericChars=檢測到非字母數字字符 validation.iban.checkSumInvalid=IBAN 校驗無效 validation.iban.invalidLength=數字的長度必須為15到34個字符。 validation.interacETransfer.invalidAreaCode=非加拿大區號 validation.interacETransfer.invalidPhone=請輸入可用的 11 為電話號碼(例如 1-123-456-7890)或郵箱地址 validation.interacETransfer.invalidQuestion=必須只包含字母、數字、空格和/或符號“_ , . ? -” validation.interacETransfer.invalidAnswer=必須是一個單詞,只包含字母、數字和/或符號- validation.inputTooLarge=輸入不能大於 {0} validation.inputTooSmall=輸入必須大於 {0} validation.inputToBeAtLeast=輸入必須至少為 {0} validation.amountBelowDust=不允許低於 {0} 聰的零頭限制。 validation.length=長度必須在 {0} 和 {1} 之間 validation.fixedLength=Length must be {0} validation.pattern=輸入格式必須為:{0} validation.noHexString=輸入不是十六進制格式。 validation.advancedCash.invalidFormat=必須是有效的電子郵箱或錢包 ID 的格式為:X000000000000 validation.invalidUrl=輸入的不是有效 URL 鏈接。 validation.mustBeDifferent=您輸入的值必須與當前值不同 validation.cannotBeChanged=參數不能更改 validation.numberFormatException=數字格式異常 {0} validation.mustNotBeNegative=不能輸入負值 validation.phone.missingCountryCode=需要兩個字母的國家或地區代碼來驗證電話號碼 validation.phone.invalidCharacters=電話號碼 {0} 包含無效字符 validation.phone.insufficientDigits={0} 中沒有足夠的數字作為有效的電話號碼 validation.phone.tooManyDigits={0} 中的數字太多,不是有效的電話號碼 validation.phone.invalidDialingCode=數字 {0} 中的國際撥號代碼對於 {1} 無效。正確的撥號號碼是 {2} 。 validation.invalidAddressList=使用逗號分隔有效地址列表 ================================================ FILE: core/src/main/resources/wallet/checkpoints.testnet.txt ================================================ TXT CHECKPOINTS 1 0 752 AAAAAAAAB+EH4QfhAAAH4AEAAAApmwX6UCEnJcYIKTa7HO3pFkqqNhAzJVBMdEuGAAAAAPSAvVCBUypCbBW/OqU0oIF7ISF84h2spOqHrFCWN9Zw6r6/T///AB0E5oOO AAAAAAAAD8QPxA/EAAAPwAEAAADHtJ8Nq3z30grJ9lTH6bLhKSHX+MxmkZn8z5wuAAAAAK0gXcQFtYSj/IB2KZ38+itS1Da0Dn/3XosOFJntz7A8OsC/T8D/Pxwf0no+ AAAAAAAALUAtQC1AAAAXoAEAAABwvpBfmfp76xvcOzhdR+OPnJ2aLD5znGpD8LkJAAAAALkv0fxOJYZ1dMLCyDV+3AB0y+BW8lP5/8xBMMqLbX7u+gPDT/D/DxwDvhrh AAAAAAAAqyWrJaslAAAfgAEAAADVvohqq6/37HpI1ny+8ocighkonisERvJ5nJwKAAAAABRFuGqIOs3bebDFZqd1DKPx/yZF4hv7t75rH8mL6OU4SgXDT8D/PxwRM21b AAAAAAAAyp/Kn8qfAAAnYAEAAAAJ32RQJkRW3NnJauV8zVdv1GyjywgAeyThAnA6AAAAAHgqS/OxxffyRWqPFV9a6kVP6TLL/BdPF/InquOuDahAmgbDT///AB0b6TAK AAAAAAAA0oLSgtKCAAAvQAEAAADIqR9HFGtw9hGv0+7AjLdBuE7qquf2/yroAR4GAAAAACEToV2EvK5Bsqy40yb6dolkX0wznLv0ZJH/QM+caUy46wfDT8D/Pxxm44G6 AAAAAAAA8g7yDvIOAAA3IAEAAAAt8TjFJtObUiEUbrI/cLpprIFFTeRZJK4R4BE5AAAAAPVSM5kPkEOEjDCOCZ07cr+ubMXqMwCJXzKST6Su6v0EOwnDT/D/DxyNS/k3 AAAAAAABDyAp3m5wAAA/AAEAAABgihej4EwDON/uSI4q/PU1KTneQP6WM8fWhFkKAAAAAKDeflgTtHdLOGbA+QnPM79mkIfcDH7lnxGfTQqC/oJS/6XdT64NFxwDDaN+ AAAAAAABXpJsk6OrAABG4AEAAABDwpe+kkSg7Pr/FmVn1S5zi9WRp8BU+kLGvYYHAAAAAKCXvBsx1I7hN2Eo98PxXy4Tw3zyHgWgs1UfLx729Ul8CFIXULUISByHDMLy AAAAAAABdVSVRkswAABOwAEAAABIf2vMbyVCfVVHzCqD8ZYxG72vae7zGJafxFkTAAAAADcEEJlk7N1JRcIoIsmZI6+5jlywPgT4G4OqXQo2cJFL+hcrUFYXTRwAc7t9 AAAAAAABikGNM6lgAABWoAIAAAD0utrWc3XGCMjFnVLY2ocg0K713zWvgtWY3g0nAAAAACziDeW1Nka5RMLLqnGkaHjFheB4KHHAQv1lBTrKCNy/9YlEUEhFahwRXHfc AAAAAAABnHQVLa7RAABegAIAAABEBuul85YGMli8USBHHC0ad5j5rcQVwggJFlxCAAAAALfYH6058oBlLDpYNfoMTsKkUZIYJjh+nclBp7AmapcIjfdLUK/EKhxRuD+j AAAAAAABxq+GQnGZAABmYAIAAADJF3gZfRY/yDiBRX+7IoiNFEpdgHdjmHFkl6UGAAAAAFOP3WV/zYiLQek74P7IM02aYogSMqU6XAaIAAx9absfg4xVUFslFhwAUAiq AAAAAAACDe0/RIMbAABuQAIAAAAUqP9xpNSMbK9Xqf19PZvHzDyfNu84cKFOORgDAAAAABHAgY55zJGNd9F/9bMiktFUpKnrLGjd8WcNnTCshkHr/z9lULzWEhxPZ+tZ AAAAAAACeRlhb4h5AAB2IAIAAAAmSSj8WnlKDsW+jhD7Qrz3SciMR3Fo6LjQ+dQKAAAAAFFkvzXQFwVJc3tMjEtoeq0OObsdnoZXSj7et4/OCdd0GmlmUK+1BBwDNPEF AAAAAAAEImRyPMVrAAB+AAIAAAD24cxQ35v7QgFi42X9JteDWBNnwKSn8mg+5gcCAAAAAA5lzaiXTzmJyur8qkatZl/9B/5VjLY/P2Of7ihNuDqkQ2xrUABFARzsKyX7 AAAAAAAGnm8xQk8/AACF4AIAAABid2hDHtNAIiJbaFjh1QXMTdvusmbuEixtQjsAAAAAACUTe1hKaqMB8LjZH/CVamDgQeQJ57gWwQ5D7AvypaH9eQSUULrKAhz9YvoV AAAAAAAJX9qHy4QOAACNwAIAAACD3Xy438Vxeih981r64SvX3nip+YPU44bu9nwBAAAAANv/I4We9HtL6dLAt8froYA+bCoCbWx2a1kqAsCtJGLLD42dUPlwARxHGmk6 AAAAAAANmIMsxov9AACVoAIAAABKWinKi+JAhvuv0gxGPorJlQP6LMfAb+GfHUsAAAAAAKprbcs2tZ18bf2k9i7KRhoMz4Xe+Grztn2r/RSfgcOqstuwUGeBARzyVgx8 AAAAAAAP53aE8+woAACdgAIAAACInn2w4Nu95pUfv680B8PW/q+/+OQmd5+wkdFGAAAAAFSOydwgMjtZe5QNWxZyBxEWbTwRUCkjWkq+HP/7XjZHfp3SUP//AB1TL/Br AAAAAAAP71mM1vQLAAClYAIAAABTldzbKHg+pR5pQHF+62BF9xuZwlWgng4gyM6CAAAAADvWQX2N47E47zfwfiwZGqgd2yrn7AflxMv9a2Dd9kmA8PfVUMD/Pxz7UFiz AAAAAAAQDnmr9xMrAACtQAIAAAAj3T7olHw3zDxcpEa7PM/D2eKvBEgtNmttuXU3AAAAAGCSpfHhYowdqbF6cg+1NmJx/O0mhu5FveZkzDJff3bYiGzaUPD/Dxz0zSq9 AAAAAAAQiFGmHDTZAAC1IAIAAAACHkE/Bzu71qowrV5ok0+dpGiH8bKsBCSpNXgDAAAAACJFJf5JMxqAfvSq33kwgVTbPD7Yl2+VBHiru7QP/D5ihczjUGYgCBzA/Yp5 AAAAAAAQ/SjJqv6TAAC9AAIAAADyH3i2pgjYtyCJiuP9rF+65ghrLvbRBnfRdc8GAAAAAD5y7GWBfuXxYOvK/QLSSrtVBlFFj8GSG3Vi4uYDreC9hdIEUTeKDhxz43cq AAAAAAARXr7F2OwgAADE4AIAAAD2pkIZ+whGiyUE/PVwCdofqQzNP3BDrs1X8t8DAAAAAJOCt/LzpROOqBISholWN+TxfXWC1qS1TJLRLnBhjpbds4oYUSuHDxxjbJGs AAAAAAAR2yMkdgZfAADMwAIAAAC8MUFtYvg740chOFJWUyf2iccIjmVniNGXLVMAAAAAAMNbskcDnn3YxI1xTFKBeJ4pdEJ7jSPXnBxx9TjeqhhjlEAdUVv2Axwa1Z8a AAAAAAATwMsdJwm3AADUoAIAAACzWdH7wqJxzWkSc6uzOoYFhXSu0O0dsZu332YCAAAAAF9TfrClFIYl9TKr8JHJ2vUYWrm8hILpEYcPyXOSwtsYyRsnUY8dAhznXP6S AAAAAAAV2XqP1tBPAADcgAIAAADhQryu3KQfuEhhQYhqEtYc9Iut/9gNXJ0R8J80AAAAAMIx1x+pIcYaDk9potb6rnDxzzh6F85FJAUw6VW8CCFM0pw/Uf//AB2GLNE2 AAAAAAAV4V2XudgyAADkYAIAAAAQSNHw4NwZ1yZRYHEQm94wjSE1Q+rumUebk2E8AAAAAIEY6ZkhEXAdgMOOxuqm0moqh0VBL6RI/B4g2l79CsOFozpAUcD/Pxzv6bRY AAAAAAAV/ykF5AF4AADsQAIAAABzz5AEEeoTmE5c8Lyai6xGQNIqsdJI1g5XhJI9AAAAANlVnBg+I1TzV/Oi6Z4G+uM8uasH4jZYE+1hBYuWqzUE8RxJUYrKHhwKupBR AAAAAAAWNsMetAeEAAD0IAIAAAArJRwoXb5YGsfVw1WuIec0YDV1vc8M3OZmCzYBAAAAAAc8ccHdUfKc2YgGiCQoFZk5v2xwO8b3CbRKpRqQFFigOwFWUfWAFRwGmb+r AAAAAAAWjHa3L3oeAAD8AAIAAAAmPcIuMPXhz8EmQvf3sweD7Sbusr8WSVBCTxEMAAAAAOYDL/aUzuKwKf7pnLa53Wlf2TOCnoWsHg6OjWo6BkeNW4BfUWcQCxwNpbY8 AAAAAAAXLEQGoi48AAED4AIAAACNT+2/IgcKzm9zotpKNgS3YxKCgcq0l88/rKwHAAAAAEbX7trLeerRGyAeLnQGaVdJAZVH6r92TuoB8kbN8evvtZ9pUTQRBhxB9Xol AAAAAAAYFg+3P/t1AAELwAIAAACAQlCtXvKP8CUGaFbL1xZujOQrnH/SMXfvaJcFAAAAAKfJgoikQxRLdzvNHUinrzNFdiDHD0+c1f0XB0gX10sfYXN8UVEwBhzNNUbI AAAAAAAZXCQ+cL42AAEToAIAAAD8/LokZtsHJpq2zPujbIkbJb3kjFFSH4+QyFEDAAAAABHu71n4aZX4iKT8Nh+hKxG8GZn76aeyyGe0aJIzzlLC+86AURSMARzcbzh+ AAAAAAAdfmQ/2mWRAAEbgAIAAABScFkVSUtlv0/fkEQ7WE+UTcsuni4hjgSuzNdoAAAAADRI8nT3Vajs4Z1eh/ljERDyXtefjOXDAr92jjFXvAYOHyiRUbziAB3S6uqP AAAAAAAdh0vZ0BvAAAEjYAIAAAB3/SAZbclg2lizZKG31TtHJVP1PyOHcUE2qioOAAAAAMnHLQ1zohIDZEz3JGBWZvpvTSkwcxgW1mrbsExUCeNDdm+RUQCvOBwLjyRj AAAAAAAdqupBpwQ8AAErQAIAAAAIA805vQ3P7RcPabnCWW6PIY16yHtk4Nv2HrEeAAAAAMc+4EMTUg+FGfsyNXBedo55Ulpi2+agdtcp6NJcAVwYYh2SUcArDhw6rtle AAAAAAAeOWPhArXtAAEzIAIAAACcrQqkCA86M4JYpP+oNCQkk+B89v/DTOJVeC4CAAAAAB7lUvx3mkkzmy3QBZ381FXVTBmQ8vmnzgbY5paxUJ0Y+yGUUfCKAxxtajMA AAAAAAAgbX6kn+AbAAE7AAIAAABjvwfpVGl7TCtDxfWW4cFjhJrAWGIY4/QPExACAAAAAExA5aaCfoBi0xuQ9pqbr+ifW+u09lZyt9jlCliSSRWI8e6ZUUgcARyBNUkV AAAAAAAm/x5h6qyeAAFC4AIAAADvscg/JoBhoewy+I391G2k2F7uiL8i4+1yE+AAAAAAADj4fIul26iU3SqhOc8r7KtWBeUg3O6cfWtbAoy7PUSQC4CkUZiiAByuVupX AAAAAAAtLLR0DET8AAFKwAIAAABPPD2AXBzPvMTtkiz1Ci5RZ7I0SIhdbeKWGHCgAAAAALJbQGNxmuVuU3bWlGlB2z01Q2jvapgIsqd7fvsrnNR/ItC9Uf//AB0nE5ZP AAAAAAAtNJd770zfAAFSoAIAAAD+tsbp4BBAebcAOAgkmD4lYP295fOIFazZyJ+GAAAAAAc26pXqkH5G5HRjR6xnHRvyfaYaq9JdSIU68+lODpqJ8wy+UcD/PxwAgLqZ AAAAAAAtVCObe2xrAAFagAIAAADGDivZ8jjsI9th+IYmCQtgYQeP8jYHw14Ws0MdAAAAAKEe8C9urGnH6zBVV9pv5epgmquOUipYL5Ggl/BisABV56O+UfD/Dxxkw5Ci AAAAAAAt0lQZq+qbAAFiYAIAAAAuhphvsvuTs6lB2TpMGTAVDt4zWW4FwqgtCfQOAAAAACSJgh3AuV/1DtPw/bVYneSZ469yzHp5ics9WGMFHvUu8NO/Ufz/Axyyolv1 AAAAAAAvyJCMFff9AAFqQAIAAABkTz8Sk9rXGV1O6cg7sEahvmL5CTkCcJ/T/uMBAAAAAFjPYZcyLJo7nYJJac+4YoVjMu6UQ0HlskSnFfJEMbi4NcTEUYMQARzRe0fc AAAAAAA04A2xONe+AAFyIAIAAAAuqhar8psbYvTPnpl4u5BShEl26c3ju3fIuLQAAAAAAOtJIYrfxrD6zyMThSuvIcxzNIfvSl0KkjNUqR5pkBZhcW/YUVgiARwFc1Jg AAAAAAA4z5MyT2OOAAF6AAIAAACC2qfe/ut36oe5ZftfaP2jJ8QOOohCRzu6lWAAAAAAAHx9tYVSpVi/ftDcfcjdUK6FxEnq350/LP6+O6fWhsCimX/vUcBqARyeNkQw AAAAAAA9nEKi94FpAAGB4AIAAADhKHMyTCqM8ihgkDumSv86VULAT6NoYGqn8GgBAAAAAJN9PuowPKZ3uf2+Lc/wpJPvWvtFxgnHGQFMsN50/xtgdsX7USfxABxGBYMV AAAAAABD2vqhzoFqAAGJwAIAAAAZOekiaS1n6doMUSCCs8quuvBPrIlJmwfzEK8AAAAAACLE/Y3QULBLrGheJKDQ1tIRAa1gW8nv/tKmVAFukDg2smQMUgfZABzain+3 AAAAAABLrAOUOshlAAGRoAIAAAA1LIZ7gSOZtIoKl/UGzVfNofLqxaT2oK2RJzEAAAAAACdNB0Q9SagWg2oNg3lqzdLn1vC9evzoPdI01SlEQi6CHCoaUuehAByhdGWM AAAAAABSG+FjB2HDAAGZgAIAAABaZVhjYb1h0M806cho68+yDCOcnRlNXPTU81UAAAAAAER003E/MTWg5v6Q7/6efO9RYF7tEv5YJWw6UrQmKxReW8MyUqzXABwVYsFP AAAAAABZ0GqNUFV7AAGhYAIAAAAh6PGWUKM7nUba6cp9L5NLy3Xdv4antZsStjYAAAAAANL8QK94/FjvOX3yWTsjEUTgyboyp40kAPzdQyQ+U+WpDBw+UoqEAByE4P3r AAAAAABoK0K4lPNQAAGpQAIAAABZm57wDw45xHPuEsweldfsUTJnhuQTHCfPvC8AAAAAADXgYcvy8F2up2S/7ow00167lQ2MT+yG5KFResc99AU+cP5FUkl3OBsqUpTK AAAAAACL7M+sgJttAAGxIAIAAABSZLr9oFo4gufkXqQ+iNiRsw7ELwC/gXopTjEAAAAAACuO+53UAeXseVlGv0vTR/xerDxrP+qBzEE8GAyFAN8eNVhGUtIdDhuMDv+L AAAAAADuQmc14/dDAAG5AAIAAAAfxp5X/HykRvLIe0AudyLfVbX3aMKzw56Bc8TTAAAAAH1WS1YLNe3Ty392krSabe9mLneGsPIISh5jt79lEsSYTENUUgvBAB1gzOlk AAAAAADuTNyqjbrwAAHA4AIAAAC4HjW56jRX/nCP3dntaI5uKgo9JpGzvab9OKR1AAAAAEUQ8nC6D/Lk9ywX1f4Fh2e91KDG+2nfhcRPlDK9z2a6KnJUUsBCMBxEhOVi AAAAAADudmlQAhE5AAHIwAIAAAA2F//Qh6Crdo4i/9nLmJeDqE5KiwBQ7CsKrusXAAAAAL2e55cd47RWbahFr5kjcsrP/anqEu0VvLp/ufU3ZaNBag5YUrAQDBzr3A7Q AAAAAADvGls6Oj3lAAHQoAIAAACc0hc4CW9d4kXgjXgn7QVt8SWxErwLHKNrRgAIAAAAAOrCjkpak4eNPNGucFxlWeYHS7NXLQ2AvmKwgD3ByweOd7NaUiwEAxw4WgcM AAAAAADxiQSIacWSAAHYgAIAAADfTsbtceXn5dhc6T4KbLq5rhHmAvJkIh/EYyEAAAAAALf6dN29NHyAmPbiAfUMcQ0sZrlFCgcNvJ5wrB8DOW4E5wlgUkzfABwyNx98 AAAAAAD6k7ZE84v9AAHgYAIAAAAxTkFe/u2tS0102gz0F636XHWMzm94F9SCmN4AAAAAAFy3gvHvgx6pY+uhZbp3t4WJmkADgwsm2gZ6xmb98BjMMVRgUgDTNxtGeOiV AAAAAAEevn03Gq2LAAHoQAIAAAC071p5e4e6BufOlwV8luT3OwO/Xgr2Qc9S8yoAAAAAABJ89xnW66dxP++b77YFdssFqZP5D9skWesOol5hUXBNhcZgUsD0DRsu2lUO AAAAAAGvaZj/t0tiAAHwIAIAAAC/3pXDbRMbGngge8h40tTyKMacsNAdDwOgnAAAAAAAANbKP1TFCpifOprBEY9A5up2H5A1lGO3YIo9JtyuKuYTTi9iUjB9Axu6gc2H AAAAAAJGxHHsrscSAAH4AAIAAABCVlIuquzF0nNNWM/iNX70BTgO2qAGbadwc9L/AAAAAKX0Bqo1Ruyt/R76Tq6kCj6Fi20/F2WK4P2ys/MnO5ZqAsB/Uv//AB0B9Xy9 AAAAAAJGzFT0kc71AAH/4AIAAAA6wgl2WyJSYfbfnMEdfrONyyCguplQpwqRpg1yAAAAACJB6he61NwAKXjkw2dMS61Novi4pf73qDyrL/agGCrZt85/UsD/PxwSzAYF AAAAAAJG6+EUHe6BAAIHwAIAAABieWmbu6Hmhs1OYlnGMHSsG+l8UujlvATHLFwKAAAAAGRmJvkqq6jq9N5DuA3b0Jo8IYeuzcCJmJMleHO0xxi9xfR/UvD/DxwEZ1RB AAAAAAJHahGSTmyxAAIPoAIAAAC8cal0DDF/lrFrBSgGKwyw+siDZKDpNFFUc0IIAAAAAM+68NvsIrg95XL4zTFvOOfGeBfbRNtUpdYH1Q8KzUNvGmOAUvz/Axwhb6E1 AAAAAAJJYtOLEGVxAAIXgAIAAAAhP3d8ACLZu07N6QXbe9GJHz4ce2QEKKVNb/YCAAAAADcRuOJZ5Ul0GaUvq7efr8SUjcRKTtGVl0VLm8IgA4nzUFeBUv//ABx9Sq4v AAAAAAJRPuJnH0F4AAIfYAIAAAC9GK5oSrM6UdjW3jsKZ1J+lII/SzV6ptDJRmHEAAAAAD+sz2k1WmRBNkdReWYNHj8Y3rzbAIkVWuX399BMuw4sMjKFUsD/PxxjCsDi AAAAAAJRXm6Gq2EEAAInQAIAAAC6HgDWohCHEwZodphJV/5uCJZX573n29cpxGEvAAAAAHcSWev2NiF4H87tlnAgG4eJevqIUhWFgCuZD3IwDRYy5LeFUvD/DxxaK0E6 AAAAAAJR3J8E2980AAIvIAIAAADCML077IuBXOclWVEQPQkU3S0tLN9Xiufj89oBAAAAAKkVj6a9TamRZgOjqgH7rqvIpr6DruWQ63bcvC0ulnao2+GGUvz/AxyjCura AAAAAAJTyNPxEMtnAAI3AAIAAADgbUMq9fjNElWkEUHBB3WyaansbKyaekKnM+ECAAAAAMT9MJ4nx1jCP//clpaJSEyus2B8dcBQ8hdSo5fskqJKXqaKUv//ABzDhR3+ AAAAAAJbhAOsQIaPAAI+4AIAAAB8SIGHcDiOw9fOCaIpUsXo0OENdGaGRU03xPcAAAAAAI/Q8maW+Ca+UWoTC/j7v7OW12WVr9Rq8Vx4cS0tAkgoB82MUsD/Pxsm9fDJ AAAAAAJ0bL9eXcHRAAJGwAIAAADgQybiIZ+Hm3+5A+cvfPWmTus+uDC6KaOeuzUAAAAAAD+J3Lp9nPE9O2rC2oDAM+VJ0Rn//sLJfDt/2iIaADNLrV+XUjWoJBt8qsmz AAAAAAKfzIPKFev8AAJOoAIAAAAJhv6KHK3RcknykQB76ZzgXai8ZuKuCj5QIBIAAAAAAEIy2wRXv0nyKdEbmA0WgWTc4BXDVs7Wwe9DrTORsMqVfA6iUnM3FRuWDRVx AAAAAAL3gLY0dDe3AAJWgAIAAACcPHftqN36iCglEyH1N9AmuCuu0vhXBMYUphAAAAAAAPncAh1bTcPAW2cGka4nHPyHrvhegr9txsvxgovEza5InganUkOxBRs2LLSU AAAAAAMzm65V6HGnAAJeYAIAAAAoRbvwNz5H7YG9aaacGe/E6v40hKr4zy6Ii1pYAAAAABzszkPCMRLo483xtIA3phv2NIzWyxF+VE9wnnQ4GhqLbG7IUv//AB1leSzC AAAAAAMzo5Fdy3mKAAJmQAIAAACC8rfZnzvSIg7jR1IYvKh1XvsYgtgq6F/b/wSVAAAAAJISWLGv5+9MDpbpWIIGmBiKP4k5hqC4xYnW7l7eM4c+sZPIUsD/PxyOTdhw AAAAAAMzwx19V5kWAAJuIAIAAABolZsowzHAqajNvAbgRlfPNC7YaDFlzTvXx94NAAAAAOChQr+AWSR3b3SAItlopgrxFpt/hF7eB1cJ4MB5aNWlsefIUvD/Dxwc0t5p AAAAAAM0QS/7ahcoAAJ2AAIAAABzx7zz/qSzilODLq73YVkeJVFNa4vL6OQOYrYMAAAAAMbCQMlQphASXCOhpjP8Uy3E/9PD7+9ngqVcP3p8tuaokijKUvz/AxygWypq AAAAAAM2ObLz7Q+pAAJ94AIAAACCq79emKNEX6eKlepmMrcSp+3BE4VzQQhKIjkCAAAAANi83P2DDyR0ZXV/Sr7RqkoTeD/FpL2wvGmsEH+QpIexZmnMUv//ABxnfIw1 AAAAAAM91wCROqzvAAKFwAIAAAC8MVxa+IGZit/POyncVDxAAJMHGcxvWMPHMCYAAAAAABh7r0VdkWAB/2/3FMFNlwl5owl3tKB39EJzcBPktePsTn/QUsD/PxvE+g/W AAAAAANdYyAdWjjvAAKNoAIAAACcY73RxW7ojNDmZmU/mEoJAVQf1x/lRE/D2CgAAAAAAIWWPpLRCvZ07NN/awNOE2G/ZEI7Zs+o16FpGPjXu2aCnPPQUvD/Dxsg2Sgl AAAAAAOflgdGKBC4AAKVgAIAAAB9L7ymtyKDNaQUZlqzrmBfYrQ3PNyKgvGVyg0AAAAAAGzjxSd6mt4hONV4yNJCXNtI5kGaAOarl8pQ6CnvIpcVj8PkUoYsERs8uJqz AAAAAAQPYo5U0WsbAAKdYAIAAAALGsD4CoHENlFplB07oaMxGk1H3J2VGNpPyC7XAAAAANCubRokr4vqFaZA49lVS0UnJgO6fmZXkkcwFcnUZNeK9+TnUsD/PxwbvVZh AAAAAAQPgcZ0CYpTAAKlQAIAAAA1D075NGOVeietHIpA5J6WYIEgCn/rlklgKXwyAAAAAA/bhIlBb3RCfgl0lWQ3tDenZyueEEd8k5nR+DLUwb0AbjDoUvD/DxzisGSm AAAAAAQP//byOgiDAAKtIAIAAAC3G/CSEGzDKmN6C0uwrcEcv+f+/BAZnAAd1BsOAAAAAH/xcDEgUt0ZUR/wezsoyB5yPwuGNDhKldTzy9j4WJjn6fLoUvz/AxxuLtSU AAAAAAQR267N8eQ5AAK1AAIAAAC232RyyJcwvmdnyYvvgpieIkm5ldC5SO2vpQECAAAAAJt8Wb09aJPi2M7TTDg/0lrzSzqri8ccE0KEiCNh+vSOSd7qUv//ABxTkpHg AAAAAAQYDshR405IAAK84AIAAAAzaso61E/wDlVYJMEBdbWjCrtpEgvI+fPoKD8AAAAAAEICoFVs8Esv9oEIdo0xZDR5vn2s/B6NlzpXH0jlbyur3FPwUt+lSxvCoJsA AAAAAAQlxiFA/bcHAALEwAIAAACJyvQlJWhVgJl2fa9BPzhnjDwQoKY4TsGf4CkAAAAAAKMqF5Otltu9ej7KxOoySYqJQYIjSnDBL8K4Y0THn7ePt+D8UuZsMxtp0Evm AAAAAAQ8XQh8vEl8AALMoAIAAACrt/Js013iZjVkkmqKYT1YLdOlK1iVKWCX4lwoAAAAACXrWgg+lG8DO4y0O/EHnDj9YSz78HDppy8ynl/KjOqd98MEU9ehbRyJxzIo AAAAAAQ8b1mmHJjgAALUgAIAAABimaPWrdZLn2djukCJQUy9hNyAhCvF+lT6RXBlAAAAAOWLtPPYtaOOMVFDqpKPJJU0jNP3XyAGvR3anFN63mxi2vQEU3VoGxw7WrRg AAAAAAQ8uFSfJyeuAALcYAIAAABF9EwyNczLmT/Paz2BULd90urE93+kBb5Yu7cXAAAAAMUu+SYK1S5/81jSvHeYuH9ewSuzyjDLsYAhyEgFie6oNy4FUx3aBhwNZDzp AAAAAAQ90KSmHAPSAALkQAIAAAAlm6yb07S4GvqbOZMVXE9rSVB08RfftjkOMToAAAAAAIxhSSEVSpFItTUY2xKDl0Xo3meZ3Nuwbbkpah+97OFQNcoFU4e2ARz84fR0 AAAAAARBcxirmBH/AALsIAIAAAA8NrxvZxdjJiF4V0K6o867bmOpSDUATynF+wsAAAAAALPlaH5zoQtWK+KoTgmXVfdm3YtKiEpJDHaY4V09Qv6Jph0JU8ChbRsPLTKS AAAAAARNmcfvhrwVAAL0AAIAAAB+xYMnzDu5TqoRRjwFtmWfwVrqLtokDNcdNSkjAAAAACpD204PPPKNB5+6Gqj6tbf76bFh2HVu4UNBWcH/R2tioAgQU84qYBwAO/8b AAAAAARNrrlHQ9jYAAL74AIAAABX4CQMCN2BSOeK2FOwWun85MZY0aRvqgUQL+BMAAAAAOkIEY4Xp0ZRTvlBe1Bq+KRCI3G+gt7xPnxmStCDuM/1kzMQU7MKGBxFYMSf AAAAAAROAF2obdnwAAMDwAIAAABlVVNU4Z++Ea0HAwzGRwfeRVK3z+it2kylqv0AAAAAAB7BxaCHtEeYjNYZxT5DVvE7fAItNQT2IBnWwn/bcznh47gQU6wCBhyg36s9 AAAAAARPSuuEFRIiAAMLoAIAAACX8rYYl7or7XVsyjAFi8wcLfu07Q6WL0f3SdwDAAAAAGuAB5oe2oBxQk4pT6VoSTcOMxyP9+lQNFdsl4nI2w+m2lURU6uAARyb2sol AAAAAART6SbQnr6HAAMTgAIAAADuaJ5NzcPH2sWRuY4eTcg6rgP/n7nUadcEpkwBAAAAAL//re0qZ4IetXKbNi1hN0fomNCNbIO1cEZGwmwTFG9MbekTU8AqYBs6gX+H AAAAAARk2yG+E6sbAAMbYAIAAADlVcYD1TvPuFLZ1KSBLWdROFaDVT067+y4ChkAAAAAAJwh1bYS4WKpzk31tV/fI8/TuqWz2dGwc8UGpxlNM9wQd4UYU7AKGBvq0uGw AAAAAASBPhQZaL9HAAMjQAIAAAAHv1+wty/Mc8x05uO3lJzB4RGrf5gHtmPSBBIAAAAAAGbJsimVKN/SuF81dtky3pUzl1PtJ2+lh5gcs3daRPzHM5ArU0u8GBvQVyRb AAAAAATJaNTfth0+AAMrIAIAAAAuMzm5wEEfiPHW3SUMAeZc41iuqFnDen+NHhQAAAAAAA+zOgnWUEtNxae+LwGlsa2tBzi04GHdRWdN8CPG7kq2Bd4uUxIvBhuGrY2U AAAAAAU6JGoNVaImAAMzAAIAAAB04jDDZN/c8eP0H5GvXUcM8nT7E91d7wbdRGOUAAAAAKMud0Kz1TjRtb5jqWuP8oDalIevoPCTwXnYZJZmMgWHJk9EU///AB0bSHSQ AAAAAAU6LE0VOKoJAAM64AIAAAAlFW+bWhmmO55+fXST/NcWTUCvy1u4SC6Jp3fsAAAAAIjuPZOIuIjGdIDgdfVSIojtcdLshGVRLtVM7QBiuXkpeJhEU8D/PxyxcUpU AAAAAAU6S6M0jslfAANCwAIAAACXYq2wcjgOOtJJr8UOLodplc4j5JOO6k3ABaMYAAAAALQ7Z87Y7eoEDJ5qT4++BkrLpZkJfdz/0E8Mqb6891e8pwFFU/D/Dxy1uCp4 AAAAAAU6vDulJzn3AANKoAIAAACFqDgxSaLO0BLa5gqB+btAZOsucZpsqPjFMl4FAAAAAFZAfhA/4soVk+NjlO+q9BS8G/bUUykfcR/CB2CnZy9NPVVGU/z/Axxhz8pw AAAAAAU8tP2d6TK3AANSgAIAAABLfZsDq/W1FJjW7GeoHlrjhk4j5Lyte+bLP5EBAAAAAJd0EH539WuDsZsC+yfl2aQFRuMt0w0zB32kjCQI3SmNzV5GU///AByEBpDB AAAAAAVEhhdvAwPJAANaYAIAAABAqiNitFvS+4t6ycn4+SH96gxyBOeuA0T+cSwAAAAAAIy1zLRKERjOPqwoTxduSoD1/EFfPd6jE/JDQfa183ZU2JVGU8D/PxushcdT AAAAAAVdG+4E2ZmHAANiQAIAAACo+dgJEfvcfw5vgc36kSUnXHjQNhiBDNpCOjQAAAAAABOj0Vd55g0iHOuJhByB8OCeMhGWn7E2pMXyJiZ0YjH55wNLU/D/DxvSrxup AAAAAAXWXLZFodnWAANqIAIAAADhMuNHOd48oM2po6B5PC7X0K9/6j6LYIKxCQMAAAAAAFI49ps8oC/6+O3v+e5G7A+Q874q2ylioLLQ/X9t6x6g28pLU/z/AxtzF/xY AAAAAAdFQX4kL1SMAANyAAIAAAAnOW9airS1KtSS50xHSgK812SKlcMh5ATMBAIAAAAAAB9IHpYBO00wEWj7eOTuzM/eB85ihD8vie2gkBbJOdaVGipTU4yWARvco2FI AAAAAAiHrpQcgHpsAAN54AIAAACNNBf5eMXbxBeeiE2ndFBXgAew4De9VXIOAapeAAAAAKJI4yvBNczk0Ru8RZOoQCGahbBEdSX0Y0hTh0/UzBL4WglmU///AB0DCOEg AAAAAAiHtnckY4JPAAOBwAIAAAAOPtZb5JfTcADkfQKd7V1jxm0oacCWkS32JdSgAAAAALMBQS3hzNPm0h//fYMMRfccTXijEueIFGeLs9i+CXc0Sw9mU8D/PxyC4WPT AAAAAAiH1gND76HbAAOJoAIAAAAfp8RsPgqFAEof8oe2ue5k+U6p0tXijAY3/VUzAAAAAD+nayQIsNM4J/nGt9h+FvFJWeRbJj26SS+S/9/FHwMdrhlmU/D/DxwAsP8X AAAAAAiIVDPCICALAAORgAIAAAAMZC5OdlYY6aNwWbJqgw3JYGjJ2ExC6auNFKkMAAAAAK56AIhzD+8IQHMXmxI6F2UQ4fFh+doWPtilxXGDvhEUoy1mU/z/AxxD3dCC AAAAAAiKSQW28hTbAAOZYAIAAADM4zRT7wCBOiz9Idp4iUu+NlTz3o7enYI/4r0AAAAAAMG3WB2q51jbjY+lg2jCvudbP+Cs50gqNN4LlIgYDJdcRGxmU///ABxgW9FE AAAAAAiR9EViMcATAAOhQAIAAAAS7i2cfA8P5BMHIxie5alVXebqq79/QvEmuygAAAAAAPsN/QqV6Ud3Cq50fPHuH6Wjy2NmbIbYZqb4yMwhs04A/w5nU8D/Pxsqcf8l AAAAAAiuvRMq/4jEAAOpIAIAAAAZaXbHINdCZEiH+OKz6YsPXmvul+VU0iJUwCoAAAAAAKNN8ybmD4qpYvq5kzLPMrBKTUhYhjV+ieVSl807M+b313FoU/D/DxsIlYOq AAAAAAkGf9xLhGNmAAOxAAIAAABiJOPFxznQ/4Gg9uDDh9nc0bEBlt2H8bbQNg4AAAAAAIGEq3nlIkxB0q5A8dK2AVhXXrYQETarjKFCqZFVFre1pZduUw1UBRvl6z8U AAAAAAnoiOjGVpp6AAO44AIAAAC6OPRj9KDlMSlIPrB6Bur+XEqj/REqKh97jQQAAAAAAKeIVEj7NGWqOTHttymDRIDoNkzQRZM45StoXB+Fiw4TLpN2Ux1LAhtZUamv AAAAAAsPUAuUBFoKAAPAwAIAAABiNKup6G2SKX1q++Okh7D7SadzdST0pMiIq4D3AAAAAODPlUAeDw72MkzVTZfxvlT8e137Nu0Wp4i1VzgaGUbs0vGHU+rwAB0Av13+ AAAAAAsPWG0B0t2YAAPIoAIAAADSdPV84Irt87+7NdGqux6qkaALxKjmQKnmuxMDAAAAALwQTJCjKl+wJ5+2ZqlOl7xj+mnC1OoKD9+fdpRd2di3ovaHU4A6PBwCBTKg AAAAAAsPefK5DQNtAAPQgAIAAADoBGt1ZPvaxTEytx7ccQ0KZ2IlSCInqiGXskodAAAAAOFKF1Iguqr8NomT5MLNeC5c0V4w2FgogLwcYkdt4ncdS/6HU6AODxxDq2Bw AAAAAAsP//mVeTcoAAPYYAIAAABtuc0Y0IdnzHkzD7nxy1IDz5YrlhmNw97Li1gGAAAAAGbDnJxB0GfmTIE0PA1FL8r0Imrq/DmFp0LRLPgKNMR6gA+IU6jDAxzZ+Ytz AAAAAAsSFjz5dzC5AAPgQAIAAABrheoTDcLvpYhfEqO0K9+9LKqdT8IaojmV5yIAAAAAANFQVuSLbPXHvpo2Dv1JPVlYt2TRU28yJcX/gKShQYsIxz2IU+rwABwqURwT AAAAAAsaWxT0g+RvAAPoIAIAAAD3zUZdrMAjy7puv+bv6Fe+D6F4xUxY7n9LKN0AAAAAAMw2xq3lqe0A3e57r5ykyoxEINwIjLAmoYwIS02BqmTgi5WIU4A6PBtuAxaL AAAAAAs6Hqk0RXMSAAPwAAIAAACiau54wKWk1y9vlpe1P1wGBlU9yRH0hzcUURQAAAAAAEJDLYAo+R9Y1QL3Yfv8tt096O3na4jGpX/m11OjzBAndJyJU6AODxst0B06 AAAAAAubF6NSjmDbAAP34AIAAADR+v2hWQ+g0xFuIvcHoO+DnR/cFOTxSjl3qQwAAAAAADUAlhbXvAB4uoWBCktmv8oUdjNjKBkyVWIdWe1o1UnvLq6NU6jDAxseTCwY AAAAAAx413LatcNPAAP/wAIAAACOhrprXP/GA470G3NgTrjlvGt5eg5YgKqMHwEAAAAAAELhYxIgo7cDYokiSgguhot3DmlvacmYwD01SmwD8DNNWM+YU+ZEAhtTJ5xz AAAAAA3adfdwdRs6AAQHoAIAAADJOH+79rpfDLbT5/u+9s56Q4vwGq92E+tbcAEAAAAAAC0RzoE9M4sEu05T8AbrcFTxpvzpPeVxWgcDe4qFjfDRd1GmU8ioARu1zBOB AAAAAA/66w++gvSKAAQPgAIAAACiR/IrS+KeQ6jvI3SjaIODtak82SJU9PE4yAAAAAAAAEZtaSIeGForZrROXUJXlvzV5mD89jksTXAK3kU9Pyn5IZezU2QxARt9fxVm AAAAABLhWxFHjeq+AAQXYAIAAABic9Epm0Tvz6y9GCxS8nrRMovcf9AxfYWtEQEAAAAAAAC12sBZX9AS0b+oNhKWvwY5NB9dUM5rFoYbA6Q9aJD+Bru+U064ABtKB73S AAAAABdm+ybJWukUAAQfQAIAAACYwxz2Ag/1Q4xKGuxxbwb2288Oa2H8GHcqMQAAAAAAAMUrI8P+eiKGEuR79w0jA3GSCRDSwD7alPiwmSKxScc0IvjLU+aCABuxiGYd AAAAABsReUXYg09RAAQnIAIAAAAegCR6uZe/FiwMRqUgVxwJZ+8EuFl6ISsjfQAAAAAAAF4cE87hE69H7vopdpJlydQXDMXU0Q7eG1YverWUUmxwSW/hUzaYABup6IQ5 AAAAACHanik9zNs7AAQvAAIAAADrADlQFy7QqizndusAxbSFVljtrJU2wzVyFwAAAAAAALtk9vYGNJ2w0PcwqcKxABpxkhvtcUFOBPR102HPaVJa0WrrU+UyUhrzJ17p AAAAADEGznIzZueRAAQ24AIAAAA3FsihN8ZAMcSKZNvXmmaw0qziqKcNicX1NwAAAAAAADU4nTOdDXFBl2XBYK9THmtjRqqBtZfC0puiP64blhWg8/TzU3bvJRqQV4cO AAAAAEHZgaSykw7UAAQ+wAIAAAAhAw+Be4N+kMF0C5pPakJcEzW8I/JjQJAjIQAAAAAAAL5eJ/LZWdH3mWlPkrYSiWXRnaiP1bqzRYkxoO6pCjBbEJwDVM79HxqLG1Wg AAAAAFEKfRWP+FoNAARGoAIAAACWP88FvTCHbYkkn++vDxdBkxrZV6OdgNthCQAAAAAAAIRt5mcgaMqAk/rRCmVnxpdQlGDeWY4FfxZl63cH57l7vUYWVAJUIBqIR6XI AAAAAFwNmsR/XcyJAAROgAIAAABgt8o+UYcEC/XmUEmX9jnipsPPmE6H6wXNjy4NAAAAAEKTOzLKltjQZb4DcVo7Cf/PuJfWEfh4Jmaay2ljumSx4R4tVP//AB262uKK AAAAAFwNoqeHQNRsAARWYAIAAAA/c/ZrBhjT1Y/kE9vZCNeuHkPg+iPbyAglNn4BAAAAAJ6/u2MY2WPdZpQuA3HNaaWJq7LbW1Maz6F8H8IntbubPz8tVMD/PxyQ1nJm AAAAAFwNwMKlW/KHAAReQAIAAADjxyOAFHEMt4XC/Qsl23T5Ylq6t5Bv40+8shEAAAAAAEyPJRcM9Bluj3F39zPHnDgEyrUXe64iVnwk0qZqE0GzRXktVPD/Dxx4idjj AAAAAFwOPUAh2W8EAARmIAIAAACZzjz6DZDof3W2E5kiXEmqsy7d1Y03C1covgEAAAAAAL3aiXGnkNLIexu0sF6xM8MfOYxbdnbnS0fxMkEQ4rhK668tVPz/AxxwNCXY AAAAAFwQJEoI41YMAARuAAIAAAC5UI5HV0UIWaC0s6t9aYNaSwtOMqXQFrvQKyQAAAAAADM/4MJG9Dc0Si6ffLuIV2Bf2a1iGb6ke1hLGx36WQbQ0fstVP//ABxXCwd8 AAAAAFwXc+VYfqWgAAR14AIAAAD27fIqLWuVX1SQC5eN1FprPVAu1S6jjdFpztgAAAAAALLHdOFzgKn/sCRCuICZMlKoDlYc0E3k6yxgauJVYvD43GcuVMD/Pxv4v1D7 AAAAAFwzkN11dsJ8AAR9wAIAAAAA+++XJWTjqXcbRFtr0tsKHoDXDN2zDL9VZjYAAAAAAK77pd6gozke4iD9e2K/QIP90cbeE9ARyPpZYHmfSHq9QvouVPD/DxumhKa5 AAAAAFyvQYEmGnKkAASFoAIAAAC4LY28OfxF2w5iSwgcfxjRQE03qHxg1ACqxw0AAAAAAL2DVSmnUi33pyc9odsWayrMM8AJxGc/SgVzCC8TomxNo4ovVPz/AxtpLQy2 AAAAAF5DhKdpQLQ3AASNgAIAAACL9u0JrUeVjhiRkVAkoWZ/Uc9lp3BunA32ugMAAAAAABR84a893LWPjGPMHNStnfKB87KlYXLnTjZkHd9UuEGLXmUyVP//ABtihmtQ AAAAAGNU6lJEafOQAASVYAIAAAAZnzRWyRwY/sHbc95fB1NThX7TtI87iXb04AAAAAAAAFpJIayG0G90ujdMFcA4FrZKCSLV+kzpQu621X2N8yru0eE3VLsLTBqUqBtJ AAAAAGy9hX1JM0/mAASdQAIAAACliVpV4SkfxXXyHxB637JPSt+6inXetxbtMgAAAAAAAMbNZzKgTFHwiyr57TJ33fg/XLl89ukLMN2ibxqi9XUkX1xEVF5gMxoCQgP7 AAAAAHyrfZsQ4xOcAASlIAIAAADNCjv/f81BdoEtcx+TjQNf+/Yg/vLwA01WCgAAAAAAAOZu4ZsZ7P60DJIPRH2IOWgMAfT6x3t/tebcFBDa48ObvgxMVJNmFRo8Jzh+ AAAAAJCwtKkNkcFwAAStAAIAAADFVehLRPgXvWorLjKOVF3NhcrUqM790KQaAQAAAAAAAM8u/NqgwqB6uKVX3AinlcvK7INqnMeXeppoL4+VzqJ+R81YVLzIDhpOJmeS AAAAAKjKu0oWNsugAAS04AIAAAD8XCCWPc2n/CEPr8jURxBTRLUOeiZAhVgQBgAAAAAAAAsl1sD/b4kDFdTy8VVvbNUhdlfEKxPBMQIEzyMkvPlPFdNmVF8yCxp/bc6f AAAAANHpwni4PXSsAAS8wAIAAABk5eKmFIrt0j7tWITcuHaP30kxhBReqw13AgAAAAAAANpTkxX/lbCpZlobCsLhzMIgfcYcMWjWCAxnSS2xDqSS/MpzVOfdBxr0tUdi AAAAAQoGRUpkYd18AATEoAIAAADGvrgrVFmQN9LCw4ZLvSxgZvQyvXWfvA63BgAAAAAAAJFRRJ6nDN2iJW2h/It/J7MAdMRLODkxHgWXrzskFlkBntWAVLaNBRoftJ5g AAAAAUq00Zj6SihbAATMgAIAAACWuCFO2QrKxRiyqD8GcSOK+XR2+onzmlfvAAAAAAAAAHKEJpkywLjNuxg8LMNZMYycixxDvGAbndmwFo4m4zf32BGQVFSTBBoYIfJW AAAAAXaWnlDdjVfvAATUYAIAAADRpMjU3QJ5qJoQAZCrAZU/OFORj5jcCGEbAgAAAAAAAErlc3tVrXxC0qHbmPXyThFive+P8XSUNRdXw1CC2FeDfM6nVFXaBRpAZhTo AAAAAa4jBOu1HXDKAATcQAIAAADIGJKZv08ec5TEeVinFBoOHyD1Crwi/wIsAgAAAAAAABqi+SPFsq48SMt3ACkb3Y2SfU+6HUS0oyAs6+iwFk4HlGW4VNY9BRrf5R4O AAAAAe4567vRTvHvAATkIAIAAABdDWQLDKxYqSReE7jLiLiCjtDMwe2IyVEmAwAAAAAAAKmZjKj1alojHDP+I1sJUwyuq6+GiFz6kSi0YD2tJBZEqUzMVO+mBRqhGzwF AAAAAo0G5MMJ4HJlAATsAAIAAADzjYslceey9XQGrHrAWMM4bDLtC3mg3+9aAQAAAAAAAL1LirEEiXYWJ0sZ+Wad+etnWmdCXttc6sKq3bo6jSqWDkLYVNiiAxoebsrU AAAAArXodqvl0kNXAATz4AIAAADXVF8AZA3Sky2x8gyiP322Efn+asg0FS6CAAAAAAAAADkIN5w788qhvrSx5RgOp6qPgryHi6qSqaLOqvIBJzFvcsP0VOCcBRpV9wBG AAAAAzIMe7BLy42yAAT7wAIAAAB/3mEuU34owlTQCxXWP2AcO5KMdnYSh4szAAAAAAAAAAoLdqMeHBpTGSPJd/P4Nq1wdAQVNj8rabuiE4dh1iXxoir/VDopAxpP2FWX AAAAA5xktnVqaxx0AAUDoAIAAABBJ8CEpRO9ClDXK856E2Frg7+U6MrAUq/QAgAAAAAAAF+JS2fhUcM7pEa7CYeWRFFzAUEvl4L8TkJumLRTiMnVsSkPVem5Aho/ZXkA AAAAA8qci5mj2FLmAAULgAIAAAAMzmjURBhUf3nPsdvsoP2iBxyCMYeN81ikQ4RvAAAAAEcO5r47sSCmQveUGs0dAEKF6Y/xNx+ekd/LiEcZt0pl0bYrVf//AB1EQgzk AAAAA8qck3yru1rJAAUTYAMAAAAEZzgmqVxYThXcKo2RC4sdpnlJUfUn4rTY+7YAAAAAABlOV4Lp9Qbe0hXeC312MEIE+bOaHFwdQzlx8JORyQg5uNcrVcD/Pxyxsba5 AAAAA8qcswLLQXpPAAUbQAMAAABnhxgADB6DBqI4cSVKnS+5Fe6dfeJ4ArFJXk8PAAAAAEN1d50UxPVas1Gapdh9ZbDgB0AQXQl/SvtKFtSamhgxcfkrVfD/DxwqOaWP AAAAA8qdMTNJcfh/AAUjIAMAAABcqRUsr3BNbri1tRrDtTfpglb8Bpmu2Jt26ccCAAAAAERQFZkZ31sb137etPVvo1/bR1LnZ51n7U9PUoc/CcBMuhosVfz/Axx/9MCn AAAAA8qfKfVCM/E/AAUrAAMAAADoWCDUvRNjOLswouQe/klR9JMvCn44zbgdivYAAAAAAAnZ6dzLtRxzZoHuTg6thYfx4FNSS+IHVC1XVGiWUOPd/0gsVf//ABwaJBt8 AAAAA8qnDP0lO9Q/AAUy4AMAAAAla3d3+b79GtZ1Edx1ZvFCEa1TmA+PDDZc9vAAAAAAAGhvGoDONSeuoOWT+jV/udY+9ZsDG6x/UAqF8pgY5y+pIWUsVcD/PxuBPXRM AAAAA8rGmRyxW2A/AAU6wAMAAAC37NdyJVbCpopI5xfzvf7+fe/LEWUVNOY09wQAAAAAAIOtO83VLVjH1dYxXIiHLLW84ajPa74lgP97mMd3uF9IFIEsVfD/DxuReASh AAAAA8tEyZrh2ZA/AAVCoAMAAAC4XE2laxjgWwPCJGOBJq5nxX0x6IdnoadGKgAAAAAAAI/TrSShj1sBsdaYq/XZWg/9FMkR0dxDK+UAAI6pCsx/eKEsVfz/AxsKs9/i AAAAA809S5Rj0xBBAAVKgAMAAADgKHxgwvd9Vbt5J0WIjVNjASd+t3UY8OaTpQEAAAAAALLDnvUznciqus7SK4byku6jN1E8/ffU3GcoQMJ27hIHxeAsVf//ABuz2K1r AAAAA9NMU3drthgkAAVSYAMAAADEVPZCFcfHUR4WpnflS/Y1JUUK1Quhr88kTwAAAAAAAIYAQD8ypxI80xzPUfpsMpDZ0ZuF2V2cSnIAAkL7zcixpDAuVcD/PxrPSVzb AAAAA9uoYZzS0i4dAAVaQAMAAADVDVyr1rv6CI6MLXUhdl4fsLn8jPBjS+hzznxZAAAAAImdkl73FBR1fyA2a8pRWme/ajRAjsoMKOW6Q0OYn/Ftr8s7Vfq9AB1ZnGey AAAAA9uobD1+9/L8AAViIAMAAAD26cXvE6tFL2AYYrihrp/JG0f/H3dHEvCdN618AAAAAML/DoA6Q4esDmifCL11dpd1fGSoJpGONGlnwFy4iQS0mNA7VYB+Lxyhcp70 AAAAA9uolsAvjwZ6AAVqAAMAAAAE6OEEsUaqL87dw/2+STkKbJ3ESb6SWLKqhWANAAAAACGiSXsw1jxsMa/8YfnQQ5jMtnqzPuJ3vgDpgoSg9ZcAvtc7VaDfCxzVG/4R AAAAA9upQMrx62QwAAVx4AMAAAAg9KFN6AEeJupUGGuWqa6WOAkpotZCa3iWWb4CAAAAAA46zm4VPNEEBiTlj+tjFgaRpeczU6eyZpTkoXxoRTNJOOc7Vej3AhxqftB3 AAAAA9ur50vFWBCmAAV5wAMAAADgtdnsaiKHUjUSdKyO9rISC9gVsXDiViC3hCYAAAAAANWpxpPQXyg3nUdMTgcGqFcvaYPARp7fqrsgIgmjJe2DYQQ8Vfq9ABxfFYul AAAAA9u17XOADDaJAAWBoAMAAADKy1/BL1xKFtMJndXLb8Vx46lWeMhmQ7LvJ+p5AAAAAHMOBQwv6cl0r2hrk5+iP5N5Hqcd486Cfbz/RBzs0GVaOVI8VcD/PxwLpY3q AAAAA9u2DOSffVX6AAWJgAMAAABdghg0aI0g8Dgcpe/7oU1Uh0CljFSk2yUFEpQWAAAAABb9uduV2QJrYmob1qHNfrL0Hh8KOJ33iHhBEx4B69J/s188VfD/DxxWQnqR AAAAA9u2ip0dNdOyAAWRYAMAAAACpcmdc98Bx5ElGCJpmgqdq1ExVfM4KTcN6JMGAAAAAA+NDjnyBRVh2lBgX3abQJy8pQWgCsfD4AsdCNdm6ILo9W88Vfz/AxwKE2jK AAAAA9u4bbcAT7bKAAWZQAMAAACLhIabAp7jwv3mSNRzL3ELkRF4kxGFPLKcDZUCAAAAAJpsw8N6b78pnAgUnc84elj8RnThgM+GLgBzVuPggdlW6Yg8Vf//ABylinZu AAAAA9u+XLLvS6XAAAWhIAMAAADuTBZGQUleqlPmKvYBnPhKdhfbM4+kxlYAxJEAAAAAABp0iXcSFYXh+XB2u0I84GwG5dZ55ErTqFFmYPIXCtF1l9g8VcD/PxuWkr6F AAAAA9vTz09h6BhHAAWpAAMAAABxCQdkFXjCINVrncQeQYiMzevh9Jqj4Uc8Nx8AAAAAALAe/Af0EDCquR8zaht7qe4ASwXOC21SZmh4UPBEVD0finU9VfD/DxsWZWFo AAAAA9xJYE7y56jRAAWw4AMAAAD+QAm9nzVV7qKALZexlXCGm3BYecVwIUvKCQgAAAAAAPWcLTpUDLRUMMH5rRRhc+7Svupz+dmqmMH/q4ecS1llK5Q9Vfz/AxshoQ/k AAAAA92WZEr246uBAAW4wAMAAACyDzb/QcbbK1F8Qhxm0L0NN7FHhDDDb9Z7jgMAAAAAAM6hONKVcDjMfczY5XgOU3AXQO9sunJr8ODJkZcAo4I+jZFAVf//ABuzJr6M AAAAA+J7bC3+xrNkAAXAoAMAAAAd9Z9tZtmgRdea2kGK+kHywgyXwR/e4nbfJTOaAAAAACCihnYjzn0maeY7guGILaYftQPMbqrB5RxCRgblbBphJmlDVcD/PxwBwqRF AAAAA+J7i6geQNLeAAXIgAMAAABKSiXgYrm0/4b1dWQWGzIaXJmKpEO/N/gv6XYxAAAAAK2uXbg1TjSb+oK+xkY6mg2/Fot83/+AaxaJYkxftUw+H3BDVfD/DxwJrw4h AAAAA+J8CdiccVEOAAXQYAMAAABe0G5xDBaF0O+W9Nlcgv9bzbx1m4WjHOiSMq4BAAAAAPa1LBreFLnOgnqWokBfjCBZLXcRGwdL5cyreZ4RmFYsqHFDVfz/AxwQsjKm AAAAA+J+AKKTO0fWAAXYQAMAAADynJY8/3kPoVkXYRtNpcq1i6L7Erlr0/DE2jsDAAAAAHXR1P7vnV3JEQhpYgkedbrijJPFWjoUvRgi2+AGTRFe6YNDVf//ABzi3Nlm AAAAA+KF46p2QyrWAAXgIAMAAABrQTsQOmJsxKURtSdYjBLrvzppCJ8sDvq3bUwAAAAAAN0UPHTez7gaHDvqEOpqmA1vf22pr0WrgAhdjvmBuuvDrpRDVcD/Pxvc465I AAAAA+KkOBbKr38kAAXoAAMAAAApELD16XMW3en8An3Hk09fDA12MZReuYv0hz0AAAAAAAQNAupIZDfmuA2EfQUZLca5phhrCl515Wb2Bj0tJLpHMK1DVfD/DxuwuqbZ AAAAA+MVeVcL77/zAAXv4AMAAACwcu7OiITeM0xYWuL4KcpI0nXrcXW52MNPIQ4AAAAAAJ9pD7e9Y3WOIBi0QcVrtv4DMwZBy05luSe5wL8X0TUZXANEVfz/AxuG38Wi AAAAA+S3vFNO7AFOAAX3wAMAAAD8DY5dybvmZtHzkb2MCnfeAg1wKLhZcZsmMwEAAAAAAA0ACjN92ARePUYgSiYdhH+2xqnLT0MiHhsHQzHKTlHezh1FVf//ABvIvQR4 AAAAA+kQxDZWzwkxAAX/oAMAAABZwBeChYc1WGb7gnliUUT4h/33CDqT3eKSzAAAAAAAAJECgzDSZ50z1Uex9v1z80afq4cHqUTmwLQrkI6Ne7dBIQdIVcD/PxpBcRHa AAAAA/SW4FTjgjnEAAYHgAMAAAB99C9N9myyM+0dVVzLswVAMRaCTAzCNoFwMAAAAAAAANlKNgxLv3QKuejwXH6Fq9oZ1jc3/9abx9GMR2hwHGj08UhNVfs5EhqiaRFf AAAABA2psNd9zfdzAAYPYAMAAADwIWHs9DgLZ5JwmgQ1wEjkmbYxL9CeRx00oXLEAAAAANyjWGt+8HJYzBJ8tKfqZKsbHhhGLd5lK63K7EK4cmnKXWdXVRuNAB0c0i9F AAAABA2pvwXiaRXwAAYXQAMAAACKVBXtUnXqzRQUESg4pas4o2FxmxvU7tY0iDUKAAAAAObmWP7j/BIimT/GM5t7SnpmFVxxOYIRokQ5RC/BJcqumHxXVcBGIxwngVoh AAAABA2p975WQclNAAYfIAMAAACJEOGb7UBRTsGZWIMR7VdukViDXHfq2nJz5xMOAAAAACHCmabaaGOlmvDmP8JzA0nwlPiivZMkMN5tlltZ9OUNorpXVbDRCBxY2X+x AAAABA2q3FmmCetSAAYnAAMAAAB8zxI9zzSwxWJ5acbuv+STT71lJEv5MQnNUoIAAAAAAGXuz7nLYbNz1+jaGfon66sJw6NnNpUGdj8XOl5A+LZAYt1XVWw0AhwhjweM AAAABA2uUfa+zpLWAAYu4AMAAABby3oyi5s0YQGoJL4YgeZxvjQSg0M63mzNEpgBAAAAAOKOEvCmud3vJgK7S9CpXb46LXcvzmKmGxQG/psfpZEMQ31YVRuNABx4foFS AAAABA279sEQG0HhAAY2wAMAAAAm9o1ixn25Tsogsjo7TD50gIOftd8aMJ9JZVgAAAAAACk8K6MPl+D21uEneZ6g2nZhat4usoilMAAplr3iEKgcnSBZVcBGIxvriREQ AAAABA3mH/dWEBkTAAY+oAMAAACop3yF16QUoLCQ7/Bb+gG0jjekYJUyYR5+9xUAAAAAAMDXwp4DxClySS4srmtYTbSWJFEfP+nisrQcCi2ZDfgItN5aVbDRCBvFJQo8 AAAABA5mfwzrRuIiAAZGgAMAAADoktnTykx4ODPp/ziVuka+FH+OumOk6CW2y8vpAAAAAGaQQHDedNzCdZNqJ0AIb58RKGXd+SYrPTHOMuw+yflfvNBdVcD/PxwqLl/g AAAABA5mno0KxwGiAAZOYAMAAABCfN++Qi0xmn6t5eHFHLnrI8Kqz3MozO9pcbIPAAAAAGO1hAlcn07rmKQoWdESq/WdAepWJGP0rMywfLnVYVZD0tRdVfD/DxxywpWT AAAABA5nHL2I93/SAAZWQAMAAABK8kD6JYF+4d6EsAoUddFGHT8ApD+/vy7WlpwMAAAAALm34EBHsjwyEAbRVLe3XJQQluOxehDgFKidlatgg63HtOBdVfz/AxxqAnMz AAAABA5pEc5+CHThAAZeIAMAAADBHMdmsf8IaBKV1Za0HlZKNIq2ZCCrsWan4xEDAAAAADMahE0Kl6HKZoRNYiZ5X9VHtub4/y5aVI6fg40av5i0rAVeVf//AByFj/Pk AAAABA5wiEL0fOtOAAZmAAMAAACRa9AZx70/+pDUWhQ1C/u9CfmjTDqsYiLIaHVnAAAAAIeOXgRU59TOVyLq7f2G5DHTdWzX0gwe9THsH/wpwU9SpHJeVcD/PxxlkBhV AAAABA5wp8wUBgrXAAZt4AMAAABGR9CXnJKPGJgeNNkxZ7TJ9UCR/VF+Zaupe+cOAAAAAEK/20MfaYQBJbYdnGFmMqFzP3gRNoHaGgpk/g4n0WVVhHleVfD/DxxQoS+8 AAAABA5xJfySNokHAAZ1wAMAAABIEaFuG02SMd0LoC6U+nBSOD+PFnRtGsm5fqYIAAAAAJZI8dx8K+YWH8sC5PQ0oFuhS1nLUx1+Z+xKO+N08tRYvoReVfz/AxxnTPMQ AAAABA5zGo+GyX2YAAZ9oAMAAADFZkTPZLFauOkADH9VqLSDjOF51ZBuAuDJk38BAAAAAJyGOJ7gUtJiNfVEKoATedK++5AMOKUyo0RlxRYuSaFWqaxeVf//ABxKi+KD AAAABA56vtYrECHXAAaFgAMAAAACBkw3AhywbOn9tSjZlZiy5qgRN8A1kjf6TpkAAAAAAEuyZWMpeWeHJAwnJjGa/S3cCsLDST7UgO1whTxcUmDHVwVfVcD/PxsQGKrV AAAABA6ZayzXZs4PAAaNYAMAAADyeUNeqJaXPsj2Eqd3LWfd9r19fB3XQ8kjn76zAAAAALmqkYv9l8FKuvIH4GB2EFXdo2K1sM1mHfyqVDdftdeM5yFfVcD/Pxxma4ae AAAABA6Ziov2xe1uAAaVQAMAAAAUE3+KlxMHCT7H8SL4lpMMdCtGutwrC5sI4RMHAAAAABJxW2ssZVpiPEd1SpwkClZyZqPSxsIKfxpFhUTt6L+4LipfVfD/DxwEkkTO AAAABA6aCLx09mueAAadIAMAAADtkiKrYF51LiRAjsC6PH+x3E3OyXoaQFjTRPwCAAAAAD/txqcKYjzJ0HEUYpuNWrGmphSIkJlHE128xug7y1uphDdfVfz/Axxk13AY AAAABA6cAX5tuGReAAalAAMAAABevOF7RcG58NV9RmrJOjWouXOfBOcL00/A8h4CAAAAANVfvrNlsrhkKH2fPYzj2+ptMFI6jPAnLFb/zVYalXA2xkNfVf//AByOMuB2 AAAABA6j5IZQwEdeAAas4AMAAABu0yVJakNaf70RYV3RXGfPckGRy73LDERbuSMAAAAAADQwABsBJp72x/kVpLbYmFC3fUIFs0oG+vLUhbUWa6+xIVZfVcD/PxsnlJQo AAAABA7DAMFs+2N6AAa0wAMAAADsmwOavy462L37eWX0o/1dS20qdBv9vx1SgxkAAAAAAI/oevffCAXl3Ww6fWzyG4UzkNCEc+kyNky86SD8iz5fK3JfVfD/Dxu88X3D AAAABA85gbLt7OP1AAa8oAMAAADBZ2pvyO69bSET9kaJFMINqY/mMiQx6ttTTQQAAAAAAO0lPbbkXS9C3uS9/gacHHfioum+YEwXJKUQLtHjGdJuNtNfVfz/AxsAA7mD AAAABBEKxCIwXCSUAAbEgAMAAACNWkeu+1vDNi6Hlo+Czh3/clqlIS3xknZmBwEAAAAAAFafGkRd8tuifxwroUoEMpf2Ih8/MEOdvZqWzFJRD++Xq85iVf//ABuASMtG AAAABBaczAU4Pyx3AAbMYAMAAADVW6iotnBQKZUcWOJXfO8B+FrCTnQBDB0tJQAAAAAAAJC+/LEhmAOZQB6KwQ2nZSZqjNnoLUtH+ZRRUtThNYCc1RhlVcD/PxrpTn+Q AAAABCWA3xBf2IY7AAbUQAMAAABRwvxzNsNKQ4Ps+pvzv3SeTQJ3hhUG3BZOpR0cAAAAAMi4kNGMy2OcLLBLKfIo06em6n21a4C4/LpR1u4iiwX6+il2VfjsAB3kLvD+ AAAABCWA55OYfLHRAAbcIAMAAAABI5Ksi50O44Vrg2NtO2s+/AXCpHvfv4auxtC0AAAAAHfhx75j2et63gMy+hPqlY+tCkgS6PbHjakwgZFZYDQ2xTt2VQA+OxyXfJX9 AAAABCWBCagwZ+WJAAbkAAMAAADnPrMemHR4DL9K8A2NkMFv3UfYi2e/IrCcFxYrAAAAAHIF/XpDGtlhE/VgTznxFBaENZkpgpkiTpPuQuBrHO2pE0R2VYDPDhyAsJss AAAABCWBkfqQFLRqAAbr4AMAAABBVqIz+eYH0e92BvRhoE/chNEkZIVBrkpVg2cOAAAAABvBUGUNyayAcxoLIrXu5n3ZNLsz1qxILHA7fDYDp4PnXk92VeCzAxzw5Rwi AAAABCWDs0QOx/fNAAbzwAMAAACw8cC4cy9kTgRLnhw4nqCXPVJLW4kZtbqs5FoAAAAAAH/YtlG8olQks3x5r0ZK2AS3h+T7w6vCZ7Fcfvb3rRhFWV12VfjsABz5TaiB AAAABCWMOGoJlQVZAAb7oAMAAACBYd/StyqEo3XQjYYhq3VLgGRwlF1ZyVkrNxAAAAAAAD3YlYyYbKn50Km7PURbc+eg/wLU0T8e/uo22Kg+w/8+WHF2VQA+Oxvzrx2w AAAABCWuTQH0yTuKAAcDgAMAAABpm5Ir0YJPipJonC/JwCoU00ngOwkEo35Yny8AAAAAAFgZXOYm3X0wGsMhLZXkh/kbEpK5MvMUIz9oT4Pdw/dVUZZ2VYDPDht2r9eA AAAABCYxfgUR8okwAAcLYAMAAADrNGwUR9HT0cTAKPLEZcLQvmE83HY8G+XK1AUAAAAAAPwDlTFL3HjWAcwGWl/gOw7y9KnE+YALnlRYdfroDD+bnvZ2VeCzAxsv8Xah AAAABCgn1uOKcwCtAAcTQAMAAADle7BbVFGbA1ZmWSKb38xSy5CkhbSg4MC2pQEAAAAAAP7hM34OePYUVxHvdF0s0qz4zx+bpDVAiSx1F621kVhcpe14VfjsABvptEQX AAAABC24x9T1mjn8AAcbIAMAAAD16Kbc8FWIaLs8KNZdmavtCF317Thjl9Aj1wAAAAAAAFvqp0RhleYvoikusysyUYaZbVVEDQVirxI0y0acqAokkUV7VQA+OxqXUigx AAAABD4YZ3WL0YYsAAcjAAMAAAC7EdY4bGVw42KRTx6/TWdhD2ygfyyoCALp+uu/AAAAAJjCpEdzy6uRRB6xaeG7XZ0/+0fzwyx+YKwjmvjKzS7LAFqAVcS1Rhy3QEYk AAAABD4Yg+EcCTHPAAcq4AMAAABQKNaqWoypgmryp6R2Hcxwf8gkUKuSKksUzZEMAAAAAMgJNML3Y8/eHRIJ07z4eMfoANxGuv/gaR9ZC+jvBPUgoWWAVXGtERyVYNpG AAAABD4Y9hefwCu/AAcywAMAAABTaYG0DduVqh3dYEMzcAydaUySw2n7XOsUgXwHAAAAAOEeRCZIbKIbWI4j4xbkZPhYMYIBz0PpFqwPxMYa2HG92W+AVVxrBBz40XPo AAAABD4avvHIZxAcAAc6oAMAAABD3upPSyyQRyI55ePG1/oKoYiO9rgdCT5EpkgDAAAAABTNOU3Ah9v+847h+8XrqV4q9LXBm4YnbEVrER5Bc4sEs3qAVdcaARxCt2Y3 AAAABD4h4lprAqlyAAdCgAMAAAAKsCVstPWhfutQDRs2G1I2Y6XYmoIFHjVwO9YAAAAAAD1VaxNDLxNcNQasmsgYQYlWZVzkgoupYfnhFC+3gMVBEYuAVcC1RhuVb399 AAAABD49s+3h+gwyAAdKYAMAAAB5kP7Z/9JUroKiOjGNzr8bChJy5e3wYzFgWEYAAAAAAIPpTbppVkgifUeSw16DgZYfQSC1RhDQIDQ6vjWfcDF6JKKAVXCtERswINIP AAAABD6ldBQIOO0CAAdSQAMAAADIeDN3cqV/mfVyzqAZ+6wjb4U025PHELRjgAQAAAAAADMganCl4PXIixh2OEnOZJQMLlQZq9xdEEY27HRKaT9stuGAVVxrBBuiuqvl AAAABEAoZBITjwkwAAdaIAMAAABK0DOvEh9bukMv/Iu6SwZkR4v/k6V/WG77KAIAAAAAAEDWTPsyzaaDgQZadY4JdAmWOAmZSMVb2m0ZIDYz6vwY4IGBVdcaARvfOOgz AAAABEYj1cLsFcb9AAdiAAMAAABi3z/LfNntDLv5RgPhbFWxIompog5ct93jggAAAAAAALvsXrzzEWUnXgymdKU6OxjCmdj1yNjzxiag28O57z6udrqEVcC1RhpgU/UI AAAABFYBrBSWgX4SAAdp4AMAAADiWmv/ruUwNMLXTFX6vt8hHLhEa4jUVBtXBQAAAAAAAIZ02QsYBZKPEBu0esUuDEIwxE/FBIwyvruabEuN37MiI1iOVaDWJBolx3ab AAAABGDIjw2LQqjQAAdxwAMAAACnwq18ptIbPTfDY1Fh8xOfOhCO4GHZoKE8YGowAAAAALF5L+tVdSsR6EiQUUZmgPEFtUnF9bhWVTbQlZJuTDYu+rWaVYarAB1iWi+7 AAAABGDImtLqmxw7AAd5oAMAAADYMKXjsOmy8h518Xg839rrht4itjKY4Xq6k10ZAAAAAD761TxnLQ2hgky6aNd4srGQlo53MAAPMu2i4l1oGuYnn9KaVYDhKhw6q8w8 AAAABGDIyehn/PHJAAeBgAMAAABpnsRp4XNsjJsCHiqwCObwk0gLsovGywANbUcAAAAAAEsuNz5kyi6H176knXdUjUEy5m+M7udOim8+uRykf/RUld2aVWC4Chxhx1ba AAAABGDJhj5dhF+hAAeJYAMAAAAVArzIoViZ/V8UbDs7zTxMJAyWBfwQvESANFEAAAAAADs4ousZHJtV1ZfZS1AeN8pzBR93lumkmbOVgCkpSkS43eyaVRiuAhxHrliF AAAABGDMd5Yzoi6gAAeRQAMAAAAJfFPHP0aw4EwoLbGS1UDCKNyuqdy6A5I23+QBAAAAAL9DwW/AOaH4DhURtgLyTgxqQ9/6Wswm+EOIoAoudOFOzQabVYarABzPGhPF AAAABGDYPPWMGXpbAAeZIAMAAACDvs5gK8q5uc93JPWW6k2oGFYYyQ6LRM0czJEAAAAAABeA9PYdS1ctVTIHb4UN49u1t+nvUDXiyKOPMXWynmxqvs6bVYDhKhsq7Lnn AAAABGEDbhs58+BfAAehAAMAAADR1+HOT/0iPgGwocH1bGszZJzsbQzvwJkCpSQAAAAAAFGyt3m5bVUEEAn1K4Yk70OyBT61cPD4PGsSKuhD8JZGSJ6eVWC4Chssn37w AAAABGGx9mLUtrOhAAeo4AMAAACWuw8v3A/KbwO5BfbnfNkMJ9GDN2zwHYwkSQMAAAAAALFSN4+bAopzPV8lViqdGK97iDFq15QZBeiEc2exj4e5Bg2iVRiuAhuCKHeJ AAAABGOfnrEJaHe5AAewwAMAAABp6iow9xuZP1aS7S1/6yZth4DXoyd0PnnmrwEAAAAAAGGdlYCu8GBEOScGcV6Rd2bnHVpZSwKZ0IlJIJ9WPUxPiiWmVYarABuDHDEp AAAABGnWpbbPe7MKAAe4oAMAAADJgAcvfEiD+RrNy6DJYXYwzPzZJJ4TbgBIcQAAAAAAAEV+Jmw8BOc1aVlymlXaRBItMMj9itfoboC1Oq4SxEeNSSKpVYDhKhrRMNCC AAAABHsPgE7lvndtAAfAgAMAAAApvyHZKCFPmOQkFClk8vajd4FzhBk0KnI0JwAAAAAAABla/LgUe5sORevJOlxJ8zM+fYqKx+VBMLrvXR2b5WE0TmquVVFFDBrrNrHc AAAABJOsB6e28aZQAAfIYAMAAADSjFP1Y56gT/WSDtSuLaRBenV3pEleqaossAkBAAAAABfcvdvNrRvXY10YvuVAQLJFpEC+ecRvVqlbN91/F8CTvA+9VafLAB11AV1q AAAABJOsEX9wFdQKAAfQQAMAAAC5moPMnG9zlXHWW6sB9wZZwgRa//unj4v/xxGaAAAAAOJHkru4ml+02bhwklOQlB3a1eCJvI6boZ6g8S+mIsxTVjm9VcDpMhwfz2P6 AAAABJOsOP8LlxovAAfYIAMAAAAYXqTB0EbF+6cVW2YFiR44be9iHK9gzeD4dx0rAAAAAHF6cLjPxIcfzegX3GD6rVTIxOE03vSrvoNzG/+3z37Mxzq9VXC6DBxlx1ud AAAABJOs156Z/B2UAAfgAAMAAABArP5uVS0l+ZDIrj8bLwjF7afOIy+e3hewjFgCAAAAAIfH135xBIJpntJJPYbi13FGSE6yDaU6YvxjszNqDZFHFzy9VZwuAxyddGmb AAAABJOvJbtcJxk5AAfn4AMAAADc9zqz1hE+SR2GugPJvM/b0mQaS0rCrXfxB4sAAAAAAF6Xn9iTpIaFIX+aCHJUU+J/BzCErYMigw9tm+QNUut9hYe9VafLABym+Nzo AAAABJO4p7GV4jLsAAfvwAMAAAB+dXSDTG6/RjJTQ8jns45eJ2UhBY6qIFDbuLoAAAAAAF7Gghh2pprYA9iQ+sb7G7JkYCK7zqz9KP11U5wq6kW2J9a9VcDpMhulYhQ5 AAAABJPeA747ow+zAAf3oAMAAABrtbllZojqwNloWNzxxXJsvW1+MvyLyv2IhBMAAAAAAJvvjZzpTha+DmZCPcMloqZccCrl2VXydEFv8wU7cupBvke+VXC6DBsYkP6D AAAABJRptRQJLpFKAAf/gAMAAACnSiYw/GGlk0BdcegXuPH+Z+aL8PtOlfwhQAkAAAAAAJuYpLpSdzl1TTUDy14Xbg3t7dfndzYpo6kR1Bf1VqJqTRq/VZwuAxt1beuC AAAABJYukPtFuG1XAAgHYAMAAAAFYLBQQN764xsAeEEMZZvAxt+aty0HZD4Y6AAAAAAAAB8g/iN6MlSmjzNfb23aIPCPioq5E6bAjqgLw5MNO+BeeFTBVafLABvkZIhY AAAABJ1XWGk7mgULAAgPQAMAAABwuRJKusA50HBP2plp1o3GkdFMl3UcZuiTTAAAAAAAAG0NWn3wR+rUZ7EEF6pQuLofCAcZGNExFjbU0c8azyyzKOLEVcDpMhouVp7K AAAABLbJ5BEDaO3mAAgXIAMAAAAzTYKHKYKm5UZprO7sno/m9Za5S0Dss3XGIQAAAAAAAOM3vtzQt0U+M4L8GZI4hmfOT/5g/wkZ+bRp9MkaiRducwbMVQuKExpDkCRW AAAABOPnTm4kdRvkAAgfAAMAAAAtPP5ofDAHikE78/KdCY9jNOzRJQ/ThFbYCgAAAAAAAAsXTPTONoRUocLV2OCD6OGFqkQZWRJczFUHYfi7RZeiS0LYVWfyDBq+gaYK AAAABVM0WSzkqQtmAAgm4AMAAABEWnheGDKJrOPUdYkbXdJ0zsRCGSrEatetxgkAAAAAADubmXa5tJzbcww1DyVC48atLa4EoW0Mh8Bm8H+XZHm4NUvmVajCAB2qsuPv AAAABVM0Y4womGtHAAguwAMAAAAkSVfpzjtOjinOr5Fd7Mq1b+uDFogWRzWNgn0eAAAAACxXLEA4RY9sqND2XRPx+lZ8QN/t7EHYO3YdzzsoN90xrEzmVQCqMBw6HyxN AAAABVM0jPxwPbCNAAg2oAMAAABkrZpOu/CdaWz/S3CfdRQvDxBWPv3YA7KKQw4KAAAAAM/VGETZVZ5WuQBaabEUpDfuh2qYJvTcm9nuRyb1JzDXYoDmVYAqDBwQyaHB AAAABVM1MvCvNAU/AAg+gAMAAABIYhunRCxkGDv/XMtkNWYPHtP1/cGL35UoXH0LAAAAALD3q4u2xP0sfk2QlqGSWiKuH8xdWwSnu4rwswhyE559cormVaAKAxxIR1ip AAAABVM3ysGrDV/nAAhGYAMAAABzoh2Sq9UVXFLtJaOUPUJGVJd1CuD8ks2eIJMBAAAAAJCWU/9udV3BMVyGpBzo7AfA5bt0VvGrvWIFmDXOjoNEQ5bmVajCABxJQmEQ AAAABVNCKgWactJpAAhOQAMAAAAYZPQNRZd/OIZOwUuRhlHRhcfb72esMNxbgyAAAAAAAKiZXQK7N+xOBkDa4RTVf74kaCWnh8/6Hyw1rTLro1vwEKbmVQCqMBtD6RZu AAAABVNrI6rKDjkoAAhWIAMAAABj61QKRkgeG6ub02TGHrBw4XVMITAAHzJ4kAwAAAAAAEe9iCwAbxsdjL6YR6yeom0Wo+ABiB0OzA+NfUh0Aig+Rb7mVYAqDBudH7+W AAAABVQRAt/xWWRiAAheAAMAAAAfEuAwHOKfZDd1XtIwN+sIkdyQ4a2G1GOLxgoAAAAAAFJHa3cHqGFqYfDBFC/+EY5OW58AB6upn89otOtfLltBYBPnVaAKAxsg+y4H AAAABVao09vKt4aXAAhl4AMAAAA/Pv4nIiJYht8DCJy520d2ehole2dLX/xNqQIAAAAAAMiBbKa+Ojl+aZaUvUs1//3k60ZK93NEknngSbP1ZmynzPHnVajCABti9R8D AAAABWA9kFxQOsaqAAhtwAMAAADz906CqNwG39uVredx1bP/lRU7WVAO77t2sAAAAAAAAG23WO+F0yfpkhyy3bgH4CelvMfms5eGS3YCl3fLDUhcTW7qVQCqMBqGTpST AAAABXkn/KxHvqo1AAh1oAMAAABBA//2tuEeMG/Ljw4cJ2jSXNIzQtK8yHVwFgAAAAAAAOzlVbnCnr1J6UYOPUTfetcNdqUuG0EZ5GbjslA3prtXRrXvVePpDRp6nMAP AAAABZWLtemGhca2AAh9gAcAACC7VwLF2J6k5vdTUjzXJQP/MFNErBLPe1iqBzEBAAAAAA3uPvHAlwCOo073b6LvNbFDMnCSS8KStcXsjVGOQj8TWXb7VY+jAB3TXECT AAAABZWLwjQTl+AsAAiFYAMAAAA99EHJG2sYFzte4qg4ILpcja8RFdX/yUk99Q8JAAAAAK9nPOfvkSApwjASoHfuPnwaxdtTlzZPnJwVEVxciFk8Con7VcDjKBwNzQCe AAAABZWL85SJkbolAAiNQAMAAABE6NjFXvBV2tVbHXajcZUGVsyDiX/3B1ZNcJ4NAAAAAEnMRJ1zhjQGK9K5rS8+Y4M2Znr9QIGTsXTHYwpco5zDy5L7VfA4Chxnafjc AAAABZWMuRZheTHKAAiVIAMAAACwQ79LwTe8qejlggrrhA6EYOgKDzm2D5XAtLICAAAAAKnsSrIpFyR9NPXEhmgZgAUJIoqUoMFACOP5XQJ/6wNrLJ37VTyOAhzTgiWC AAAABZWPzx3BFyf9AAidAAMAAACnX+Yhn25fkOyAHIyHzmi3UHjZ4dw0JHoTEL4AAAAAAMTE+z+aVP0BE+/HGXxvO3iPjMl2ZeLrEPZyluXhKTeiR6z7VY+jABwLvPkc AAAABZWcJzs/jxCIAAik4AMAAAD0awI8np2YHSb9jSlJ9hRcOuZIwNSDAfhm2VwAAAAAALyvOOnLwiuVhb/Gvlz8//RYkEu5vAiyV6iAYvK9b5uk5MT7VcDjKBtSVUoc AAAABZXJ3SpNHNDFAAiswAMAAADTgehe/lIKmzqdka23cqOxxIAjCKQGUOUwjw4AAAAAAGJ0mwydkn1jPI5jKniIg8tCkAx2qPcZRvlV8e7LRmXKnvT7VfA4ChvW+l8x AAAABZZ/0cpnd7B8AAi0oAMAAADEoWk210bHzJQTP6I8Xa7rGa03ott7wqRwjAEAAAAAAPRsnmLX9KWCe0ef1/qjhXUbvDT648cQHbOrU5tqDy2dGZL8VTyOAhu671EC AAAABZkSxNXTLzWuAAi8gAMAAAC0JVv6r8gy0nYBr65gZXpveCcylYPIMoTMKgIAAAAAAP9dPyl1k4GLKoBdG9m6Ry58WfbVNR+B7B1wOUd1P7dlFnr+VY+jABsGzZQT AAAABaU6s4IE01nzAAjEYAcAACAcOo91+7NVID+wH8/Ydoyw/izL77QoW+lUGAAAAAAAAB8I192MjDMBSQiIQuVN99GCbhGELZxrV0E/vsY2CHZ79s4GVgbSSRq3Io4S AAAABb4UJY5vnj91AAjMQAMAAAC+I+8bVPE0zSFr0cA+Taxyvqg+Wp/DwoBwGAAAAAAAAB+nuXJ7fRbbHotJKl3vRZ5uafHKhF6aj4LW1ZS2Uz9T+BMUVjQLNRosMWxF AAAABd54exbN/LXBAAjUIAMAAADUAFU4Vtu7zrvHfEbfkHbsWPYktWM8anaTFQAAAAAAALZyA/21/n5NOtI6UvjEOsHjCNKuDG2fvXOP3/73XHiyapojVq+dLBrt1AzV AAAABgbYcaeZijEWAAjcAAMAAAD+zX6fO6hLFl5AGITsaSEZF8byQRrFbZbWEgAAAAAAAPWpASIs/PvesA2V0NgbxyHv6jNmyt1cArkP2/WL7TT3jycxVqrBIBqgxU+Y AAAABjg27r4338hCAAjj4AQAAAAASfcYyrhqESVm2Wp1HrdIBLAn4QUmCn0MHQAAAAAAAC7auR8p3vDVo/rJ6LkIOMWkhGmSbrF69+CO75yDGfhFzP82VllfChp5t/GF AAAABnd1iEhN/c9oAAjrwAQAAACSx4N3P0L/b8JswVYoYPfRWllX0oKSpJ5DywkAAAAAAKGfe5TYmsWxX5n+wp4/QyVfTHjN8jPBLYLa8NQzBs/9fohBVqKSAB0PCKM+ AAAABnd1lgCC/iX4AAjzoAQAAAAo0Nt4bWH9JxJRS0jgIZJjCkLXlMk2g7mCkogAAAAAAJxQbGo3ec3MbT3NgsjrxEMPpDaVy9X9uvR9JBaDq5XLi5tBVoCoJBypyiz/ AAAABnd1zRQOntUvAAj7gAQAAADZYGS2IZ4gO/2pDKQSmUzHORLSS4ZnZ62ipHEAAAAAADBMhXiWR6LvVoRdrDTm7JREa+ke0MCs7iikgnR1MBQpg6ZBViAqCRwu1j0L AAAABnd2qWI9IZIMAAkDYAQAAAAPt6mOvuaSSDrSQ5LPqMvOWeiRcjp8mjws7fMAAAAAAKacgpHNEvTge0AHIMRgJygoi1d4sNx7XuPgbQYiutswk7FBVohKAhxVs7aD AAAABnd6Gpr3LI1gAAkLQAQAAAAKYrf8Ap7KAfUceh3rxhjOhfb09rf43/9YQmoAAAAAAH1LzyaNIymekZtFEwLr25VV3DQD7bOH8Fw5mEWsYWgHe7xBVqKSAByrqt5Z AAAABneH333fWIKRAAkTIAQAAAD5lPNMg8im7/BJ6g+zpMZpTcKpZxKdEBqP90oAAAAAALrX/Tzw1Uexv8MYEBSMo6LYD3Rtp5nGsYM2c9ripHOI98dBVoCoJBu/mSPh AAAABne+8wmACGcTAAkbAAQAAACyZCrC1UJnZ1cQmTzTLBuocIQZR1cUu+/zOxkAAAAAAPUZxaRjSUCBzxdVrc6gJEfULrch0Cfcgxn09mL6jxUaAdtBViAqCRva4gsT AAAABniX+1MJtFbqAAki4AQAAACKtFu5fqWczhha7v4HZ6ZU8Ns7jEp+jHk2tAYAAAAAAAyITWfR92W7/6STVluuu1Q1p/AdBXN0D2q6sz/NJQoRofxBVohKAhvQjJy+ AAAABnu+IlLChfpuAAkqwAQAAAA0m+bZrTiGP7+oNPNY+UMzF0/qAjqqfAy1HQAAAAAAAKK5KLsI5DK+8UaGtIGp/sERiVSRke3WJK1WPtBiT8r0cHRCVqKSABsfOixB AAAABoaUTumztn6mAAkyoAQAAADYAVCtGFFVRk3ec2Ys5NJhjZbFzgvMKRZiPAAAAAAAAGqpEogGM8xlwgl35y5LhBfTqp6gbE25ncTf2mOYNS9w8dVDVoCoJBqj0r3C AAAABqQ0U93rvy9hAAk6gAQAAAB71V4MSV5P+1RARFBGOgE0Rd32ZYKR8r5iEgAAAAAAAD2yvDKZnU13TCi102Aq90BZwsKwr6ShW1QGYtIr5jef4dxGViAqCRroq5SG AAAABtOJvqUDnYW1AAlCYAQAAAAr78t1vXPGdnkzx5dZZAYAQ4TMcC2X7s4gAQAAAAAAAJbJyF6JHCv1oGlZ8YXCmouIthhTwJ4ZVT6l/wsbixO/SitRVgweBRqKicv8 AAAABvvLEIyJUPoNAAlKQAQAAACvP4LXgGqKigKqPW6/1sze1Xq5FqEYxMBNExa6AAAAAChrx2nADZIppMaisXpqUx/ZQPczFk51w2XUJu7R8gFhXVFlVv//AB0qK6ep AAAABvvLGG+RNAHwAAlSIAQAAAC2hM5WBID9N01ZEZJYSHJ8WipAYw5m4G9tDaChAAAAALvjnWt3dk0AuGJdzZ8uwIae6c39x1FC1occdQ+sVJ8cHFZlVsD/Pxwll2jl AAAABvvLN/uwwCF8AAlaAAQAAACVs7N8CF6k2yMjH07TOm62FaeqKSyWj8fOZkc5AAAAACqZt2/QUZqvMDAIMl484rL53OVXMAnkgbwtyfb8gADDv2BlVvD/DxwCBrjK AAAABvvLtiwu8J+sAAlh4AQAAADMlTmerukQas+h7IVoZF/A7BBCCF5vZyAwWrEIAAAAAGyKpJZC1ErxPa9hT+P+MIyP4opWtwMNSqrgm+fPJfPmgYNlVvz/Axws8Fy0 AAAABvvNru4nsphsAAlpwAQAAAArxMUtTDaaCf2r+qYokIuocGZRbk94voq5fs8CAAAAAKGUdpcQmA5XbfgqOo5lXVFtCm7QBDEfYzdsWeeZtWJ13hJmVv//ABzFIrvx AAAABvvVi/wEwHVyAAlxoAQAAAA1Mc31II11CR+89THwkiIdUOo+5J56M+xfAVQAAAAAAHku7uE1j25+ZQiOsIK0qXLvtLiu4FOSfwTTdkc2sepnrdlnVsD/Pxtiiymp AAAABvv1GBuQ4AFyAAl5gAQAAAD6I6k1cTOabEW8J5aTkC9aFUjYkhGfGtMkjQYAAAAAAH1BZGHNu5m3KylxAprolNIPy7xZckagbEmEtVyqkc90/uVnVvD/DxtgcpHl AAAABvxzSJnBXjFyAAmBYAQAAAClpIiUqJ1rD53m1vm3Rrr5VcTcjiVYk9ArJgYAAAAAAHpMLwnJjauu577mycRpgrDAfPJRTc2HJnPfeWWO+k+6sAFoVvz/Axvs+0nN AAAABv5sCpKDVvFzAAmJQAQAAAAmZoVffHKxWH2sJGyw2niKqT5GYXnmevd7MgIAAAAAAAjZyObBnHcLdyS6IBnGb0ZK2/LkqTddWvxAPe2C+C2/r3poVv//ABvRmJc/ AAAABwUGfl3Xdwh8AAmRIAQAAABtXsIf645wh9a0A9JnshYbhtes+wYWXCNyZgAAAAAAAMhuKiyJYmFmVLNEbb5au8akWp7kZMDLbIHaEkL06kPSW3h1VhW0ABuTAK8L AAAABw43te4WgZ7IAAmZAAQAAABHropK27GZG3Toi+rWRlwSX3GoE3OkOZoMrAAAAAAAAF6sraSWwkCwtb9L39Rs0EI/LUGgYWs4+RmIS73RP+s+ERaDVtCEABu1nT1y AAAABxoYIkRGL33DAAmg4AQAAADmesNU8i+BHZ5NZhITHjT27OCqVniOV/aTfgAAAAAAAPTSPevbsinUXeGfXiOJJpeEWexsOSmS2XeU1dez1eVTheSKVlUrOBp13qgx AAAABziw6HldBf6SAAmowAQAAABinzEWdyypP85FK5uFV0p9XocmOUSxzQqYKgAAAAAAAKpOea7r32sxwuEx+V3noNAkGCJRkBrLUBXILEDR5G1Vr3SWVvcpIxq8bOhJ AAAAB0rOaL/o3r0uAAmwoAQAAAA9RlKvC1SZpLILBM1rmmmvDahYvBQ1xoL9O2gAAAAAAOemGWBvmuNs1KJ+b7UAnqvwlxfVaPgtQxQCgKTBD2mQFEydVukzXxzRV+Ms AAAAB0rOfeQa9s4KAAm4gAQAAABq4HUXUJHvAFHIcgv5Ne9KKVuWrGx6jZJiYj43AAAAAA3x2/pKDIRtRglw/VHZM4JkUOG/c+w5hE9XLMDAh/paInedVvrMFxwGcFB6 AAAAB0rO0rhza3BDAAnAYAQAAACkxyVqtuvDg/UwKt0n8Y0TR3dNLcZlGwZEvPcAAAAAAKVNVin3T86FBQsosbRMexsAzu7RdUhZTnSPfgpcsEEC5sGdVj7zBRxNwfxi AAAAB0rQJgnx7MM9AAnIQAQAAAA7BAd2O6RR49v+OqJ8S52UbGTFtzrh0DgcpxICAAAAAEY/q2ITHUbsBeuxORNrUXY/tix1SDo3R6oIcAvNKaV5CCCeVs98ARwMNdG9 AAAAB0rVc1GzQhLKAAnQIAQAAADtjOl2jXOAQgI7Ckb0Mi9XhOihzF3udo/oNqgAAAAAABns6GFcRwGeG9J8ETJJXpbS5KGKYRpzLEzVPGmgikYU1oOeVsAzXxt68K1h AAAAB0rqqHC4l2icAAnYAAQAAAAOLqo//8QiNR9zoAtB8pAyuzYWlkGYHBtsP0EAAAAAALuUHHCzCVRLnCWCxtM871a6vLM/dEp7yV58+i7MAspzbByfVvDMFxuD8cU3 AAAAB0s/fOzN7MfFAAnf4AQAAABIAjB6UCiIBOze1KpPbQFTKqsQYSISdBA2IwgAAAAAAO4E0nVJOqquxVkquYO/HXuYCW4+qPn2jZWV5QCvIToHUTugVjzzBRtdkUie AAAAB0ySzt0jQlQoAAnnwAQAAAC/0wgYE2cUmRR3YdoU41HQHWIBP6I1P3oeqQAAAAAAACh/kAcT0YKwxUY59V3pWO64KWv6jDgTRDrsAsokPVmQldmgVs98ARs0j/7C AAAAB1GlmoYOjeIrAAnvoAQAAAA1RrDBbYyH9Dq/0V5AYn+6wyeCI28PlXZlzQAAAAAAABVNHSz97NSH5OGIsNoEwGhGHwxaH7ta7WyI1Bdt/MC9BgyiVsAzXxp+r0si AAAAB1rxZi24R2gLAAn3gAQAAADbKAg0cHS1YFamGXpgogIUU+kD2kQedOmZQGcAAAAAAG9rq6ZfZ08L37+LnRMvQupATSxiYOsLhpvvpka0aD7fd+umVobgQxw0cnc5 AAAAB1rxg71QYycNAAn/YAQAAABxNtJRuCJb8vu4hnuSSURm0G605vmFrgFAdMQwAAAAACxftz+vxz1zfAJrRfhVPUReT06zhagCwAda7dM8AicQAw6nViH4EBxkds3g AAAAB1rx+rgqyGFMAAoHQAQAAADLNvRIEQUQTmsbRmiDJufhjPjtnusSGq2VUC8AAAAAACjp0z2GC2Z+wTns+pZgjxu1YdHr5JJ50QTGMhNo1tFJ1SOnVgg+BByE1C+d AAAAB1rz1qOwWu/rAAoPIAQAAAA2OWwCCR9tCl3dlGCpAqs5uDxhubitfqfi01cAAAAAABaC3HwT+pZVTSTSsVufNKNBUToHLpgZTqhsJMinZsfxQDqnVoIPARxEcp7L AAAAB1r7RlHGpUIEAAoXAAQAAAAyYuncwvJRuoTlydanvcsCkc030G59iL4oCk4AAAAAANuTn3IXFbJ3GmdG3Tpq6HDmTfGxD08AcnHfwCNs2T0YhVCnVoDgQxsD1pGT AAAAB1sZBQofzopqAAoe4AQAAABC8WIhrCV3NH4NXh9ZRc3EgSSnzn6fCdhGEyEAAAAAABOthKXl9edFLcPUZQjyoIcCNQ8ZkD8sOxvMt2wIXYt6B26nViD4EBux7Do2 AAAAB1uF3cdktl7UAAomwAQAAACYIRxhHEbh761sJmz3hjsDtm127qzFBvzpmhkAAAAAAFfDbtt9fJx1aZ6nGS+36yvk4p60q4FeVy2JjIMG4yhrOsWnVsD/PxyLJIal AAAAB1uF/UGEMH5OAAouoAQAAACMf5AeDzNEaP0tFVi543FBgrx1Wk+NVUhXnkwAAAAAAAmonKSCdIGOy5zCBrAhchUmGJyo7uH6sT/QRW5zSumjM9+nVvD/DxzvI/gA AAAAB1uGe3ICYPx+AAo2gAQAAABW1UDCo/rrkhREuE7d5rl+AO7R2mETUvHdehMAAAAAALOhHTERVinHQTGIMWi0cyu/Q6IZ4d0bMKUBt07aE+cmx/anVvz/AxxWJ8vv AAAAB1uIdDP7IvU+AAo+YAQAAAA4BzWDt97sxa42ow6Je9hgV8MnU06Wt3vH1FQAAAAAADpMoeqMZMX9tfz82QbRyW1xVkrkEUqw14c4D+Aj5e1paw6oVv//ABz1RbRh AAAAB1uQVzveKtg+AApGQAQAAABP57vI9kreAs3ZiC+RcbRkYfZNeukYSNAGe2sAAAAAAF6ItKd/eC46I8nyFj65gP3F1fKTRNxwTaFa2IWreJuH2CSoVsD/Pxuxuo73 AAAAB1uv41tqSmQ+AApOIAQAAACCB2dj7DPeUWiLHu5ronY3GgZcM5A+joWpDzgAAAAAADDfbuI5R6GkH2iAxuxpe0Z8JClANYTZEtm2M63dmuhf60KoVvD/Dxtk5syI AAAAB1wi1IJbcVTyAApWAAQAAADVzGhz/9Nwj/zv8dwCGhj56YZzznn7A4tZQggAAAAAACHjR5BwNBJE4kZE8F6bODu5loa8hDlnGnPvuABnRXaFW5uoVvz/AxszS1fE AAAAB12lV93ezNbMAApd4AAAADDsTduHG5L4jqlQE5+cLYgyJLjHP3PN3l5H0wMAAAAAAEQNJ86WFpNt8y5M9hzqQC6dUn0xouqMiqimIKczPmOTrCurVv//ABsmVZ36 AAAAB2ToX8Dmr96vAAplwAAAADDOS55fvrmCgBsjDyGkx10KC8zbOHrVqDDjAAAAAAAAAAEnBI6dANrGVKXFZksSWbhNDOjljFcIbi3yLk9Dbbw8XoGsVsD/PxrCkjsr AAAAB2+IEldtUhlfAAptoAQAAAAZbkID2Dmw3MSyDGDSeDogBP2i+wzVf/iPIQAAAAAAAFhFb8GKwJ2HoXHEL+DPcaUQQvobBaTKnLpQebSaAU3Y1dmyVj0AFhp1QCUX AAAAB4DwoqLZPNOZAAp1gAQAAAAdcjcKQAPCjb/mKuzCfep5rEgDJkYx7ePmlnwAAAAAAN67euhYO8duiy8wiSfHc2xcOHjWsKe3g5sdj9HINN3hDT6+VkGeAB3UXO5K AAAAB4Dwr1CCVdw1AAp9YAQAAAClBZaa44EGWK1Kc9PPtFUXL6OLwnAEJvcLobpaAAAAAPMlFK5Wrsm7TvgNyf1SUPH5YcyRZOJpEi+MfPZX9con3Fi+VkCQJxwlb0ga AAAAB4Dw4lit9iKuAAqFQAQAAACwfeCzuTokah4QMArq8GheBVb6yvz/5ibCd14AAAAAADX4Bw+xTByZ8yBklQQ5HSqdrNckKY5FJmQiCXxyj8jY9m2+VhDkCRwODr1T AAAAB4DxrnlcdzyUAAqNIAQAAADhmjo42bv7MotszDOQsJS811LowYlb+Cf+iekAAAAAAPEcW2KL1Cj+Y0k4hc3AInnSKhkElg975OvYifTJIMA+2YK+VgR5AhyXoxkg AAAAB4D03vwWe7PsAAqVAAQAAACsN30y/tDkgV78X0RiaRU075NiFIOFQ2LpoZEAAAAAAMSAcU5mdQfT9ykUT/i+mD2TRZQ5ORdFj/RLOBBzSjyY1pa+VkGeABzWh+Pu AAAAB4EBoQb+jaEMAAqc4AQAAAAitZMLW/2Wazm8+LTt35vpEH2B4hcykKZ4eUcAAAAAAM8v7Azlc33KKWU5cu8WPK3RnRD1XxbmpCW+2LKCz8YUU7G+VkCQJxv723rF AAAAB4Ey3g+8FvTtAAqkwAQAAAA87cyPH6EcDxZJKK7xfJOKtehIPMCpE1+e/QsAAAAAADa82YLfPH7WW6zWE/Oma/vWWmsrIURC8w2Ca+UwIUevnuW+VhDkCRubM13G AAAAB4HpDxj23ppqAAqsoAQAAAA5Bm/LV82PRVXOtwOMifI+NbSHmDe2MTzdWAEAAAAAAJ50jEFiFc93BI7oK04FrzpEcu+aeconSu4vbZmrOlnfso2/VgR5AhsAnH1W AAAAB4R3z3RnK/h0AAq0gAQAAACKozatNCpSRciftamCa1cFYDY4nWu1ngDXKQAAAAAAAHrZahDvch9TFthh5XYWy1E+ZtEY5kR1zeu8BspYO3mQs8HAVkGeABsJdNxm AAAAB4p4mPv58PHNAAq8YAQAAACX6b5cxtMVp4VY0vzM6bhLIFJjE9eKK6gH4uYAAAAAAP8Pqi4pH15B/zZ//hSYMyNuavXKshXhted6KRK9PyB69onEVsD/Pxy198is AAAAB4p4uGoZXxE7AArEQAQAAACMRIN+X8LF1jRQ8j0xGuW1TazMNMUP9SlE3PoAAAAAAE/Btbsz2j71y0kUBVZ0yTLcqfNmRtpveLZcAjSEoWPyLajEVvD/DxydVqDf AAAAB4p5NpqXj49rAArMIAQAAAC9/Bm+RL8TJkza/44RYV5ol5Srxrbaz++NxRkAAAAAAGzhYcyqXyISzGMwlNEbhr6MIsXAi/+2jSgfjGzkey8Esr7EVvz/AxwCXFXU AAAAB4p7L1yQUYgrAArUAAQAAAA5TEfr0g9tR2HyBahsmMDdQl0ExZNghE1EzwYAAAAAAKmMWcwojkByjPiQ9lo6ySjlheZGNLvb8W0bHlcmym06I9fEVv//ABznVUNf AAAAB4qDEmRzWWsrAArb4AQAAABoBUf+dIlZiRQ7C0HYmwLMiOPHGlseaj1qhdAAAAAAAOnqa0WTGFETmSC7Z+u1I5XQ1/cHgC1gO+DVzVc7BAQmje/EVsD/Pxtp0lip AAAAB4qiDqdvnGdPAArjwAQAAADa0UwajTHV6yT9ECTiEWI4pSwIeCtT7WOumj0AAAAAAPbCIp3+LjzULjOJKF0gHtVlCZpI0E5irYTFM6eIP7HWER3FVvD/DxvnIOI/ AAAAB4sf3ytAIDdVAArroAQAAAB3EWkl4rdHWRSMgUYygZP+49WmIWD4eTgARQMAAAAAAHOs6iyAJknWUDjsuilRul/+z9ebtFzgQ0AC3erwwn3yFPnFVvz/Axss0eOm AAAAB40YoSQCGPdWAArzgAQAAADsUoyHHkqcn1oA7m5zroErr+ed5OPNMDc5dQEAAAAAAOHYDjOWwnggcFLFo+1xHSB3NmgJ5/4z/0cWuDdTrxdrysrIVv//ABsZ2iEv AAAAB5T7qQcJ+/85AAr7YAQAAAAqaAhOVk26OJobY6rWMoYll6jCrnnIIcaohwAAAAAAAF0iMfxyV1OoMLdTDv1kswZIk31UeBzi7ywS3WBhP1tB3xvKVsD/Pxol2joz AAAAB7OtnATiD6E8AAsDQAQAAAArEdPlbur2VAq5m5+sUXszr5y9NVmXskGmLgAAAAAAAEQ/QAKoTbEjOIE6YmQ7d+ZbvwJyxJiqD5ZmplGOhPSqIqPRVt8LGhpJ9/fe AAAAB/x1QcsVFgAVAAsLIAQAAADh/wqxUEgjOVH3K2J1mJSvXNK2uoxqm09UDAAAAAAAAAEr3HwB/h3h8V4jEAcKbIQxpLQdRiAgdletLxbWXA81K9HcVqXFDxr8iPFc AAAACGvw3FlNjtyTAAsTAAQAAACZiSbV3Q5XHYrVgKljOEmcOPMpKiM3TvxmDwAAAAAAANwEtv6sKp13rsvN79dhBom8PHZJwdLNzk0ozx2ADu8rUePqVp8EDBpYRE9u AAAACKuYXf3Zgp4kAAsa4AQAAAAlX7OBapUHtRwVZLmd4IY1mBT8sayKuyv+ExIAAAAAABMSCj8JLavoSWgc0XQIHQDFr80V5gcEzRvRYBV3Hrs+DSr0VvSAAB1VDku7 AAAACKuYbZ4fRayTAAsiwAQAAAAFDtAQQchnQwGOhPgKrTPBxDuRwd0CBhbCgroAAAAAAAYTubfQ81qfcbKpckUq7sEWiwqk3+yRFOAT/cWsEXc7qj30VgA9IBwRWFey AAAACKuYrD69GM3PAAsqoAQAAABwCOwgtu/Yx1LA8KCzoccVqXUSUvkcSzaLqbcAAAAAANIa5ObvXkWA4xdHLbJ4mhMfZVnPTZAQHe9dLhp+aC1Ai0v0VkAPCBw1+Zs4 AAAACKuZpsE0ZWJ9AAsygAQAAAAiR3j8RDKqWKGiI7btKoFUPuSutApUI9ti3EcAAAAAALkWeqgJgPYCT3B02+CiEWp0uiZDwFCTMHuAu+/eFwtxulr0VtADAhxa3yA+ AAAACKudkMsRl7U3AAs6YAQAAADKZ1fQO775nmNeW0JabholgliFw1MOoS0AkVIAAAAAABe7zq6Jbq0swc5xbwJ1u19PnpN0xqS+qCPfAkqnUKqjhWr0VvSAABzND2A8 AAAACKutOPKGYQ/fAAtCQAQAAAD1cbwMFToQxcm0gjUMbwoFeNxLWICz2cDL+18AAAAAAJaHNnOo6MBVF39b8C4SSmonM4M62jbG7Ca7llI281TCvX/0VgA9IBsxHks8 AAAACKvmZKiDp4wQAAtKIAQAAADzX5tI9xzWMo3btsN+cild+qgni6awBsxpegoAAAAAABpqKLTpNLBQw7TX0qxYgLBL4stUitcN/ejmAhwylYdLecP0VkAPCBtT4Ofs AAAACKzUACDsx+whAAtSAAQAAAAB0wp8Xx80vlVgeocr2phgqu+M2ViXxLguIQcAAAAAAE7aE51S+BuFi2dO2vCZcB9GzyjazOXRbvp3eb2a2p+IX1b1VtADAhvLMkuV AAAACK9OyPuRh2JZAAtZ4AQAAAAc3uEgoFzGDGR13i7VsSUpTxZtP/TzjJ0J0WgAAAAAAKqPCYmDfGsA4+WiqcjqEYygnF9EWOXLuSCJNeEy+lXNo5j3VsD/PxwD+gRh AAAACK9O6ISxEIHiAAthwAQAAACEqKYoGAmxQ6eFa1PsoOvChIHQS0GkaETv3dUIAAAAAOJBuhtOxhOO2ystwiiS2bBHSUzzrJL2Yyp0y0vmxa/sqq73VvD/DxxTVqCl AAAACK9PZrUvQQASAAtpoAQAAACNANkXtOGN8BI8y1UpVZgL582wg5dYO7kN5MoKAAAAAG5eRh/0EaS//DFmYEL4u87/EdbSbtW9A6og3MPHFfXXnML3Vvz/AxwsiEhj AAAACK9RX3coAvjSAAtxgAQAAADlkN4QNFN7eMMLDBh5Vz3aGYczbwNUOqoepWYAAAAAAFnsYnDMVAK6rJgOcupue0vKuMlWIxJvIIS5gSBbYfJk5df3Vv//AByv+7Ke AAAACK9ZPYQGD9bXAAt5YAQAAACJmtDsIDi6C/F6G7DvUg5kaawKlZfopncWUosAAAAAAOngvGu2lBCVov5N+tj3fNjx6400HtApvGQW1M4wpgN64hH5VsD/PxszGwGM AAAACK93qepydkMfAAuBQAQAAACH2gQg/bowILKZv+5PCT5AV88PqxwxmePwIxgAAAAAAOrco+kve80uB22jkyoCIHLhiFs5/QRis8JkCcm1GQkEucH7VvD/Dxu+Otxk AAAACK/qGxjjpLPbAAuJIAQAAADc7zhT2l8UAVa+k7Ww9d05ZKkM0Lz1q8Ih+AsAAAAAAJeGEA6dSCjpt9x4IwABsCmlUafBL8SdEGfm8Uv7coENuEr8Vvz/AxtK2Kf2 AAAACLGEHi3mubVXAAuRAAQAAACVEClKPbm6UPuobs5tzE4Bd5EoQQGvshn99AMAAAAAAKm8cak7jQ/KZ4nfmafEaxswv3S79OYU4Xi0ICsUJq6G9Xb9Vv//ABvcSPPg AAAACLckJhDunL06AAuY4AQAAACYhNoOB/z+352Xoc7zyd3KlKVc5QIqiS2EEQAAAAAAAMjfIT2Fap+yj8xcKjjsOrXIj2APFO0Lgfa/5VWYce5uIjj/VsD/PxqoGbTF AAAACMaYOYsCFtC0AAugwAQAAAA08Q5xLoOz+ZqnrpOK4ZWIew4c9SCdJaCKIAAAAAAAAF9T8MwaIZT1E9HMXLFkYsxAo1rmixxRepOZrT3g8jp8FvwCV/D/DxreFhIl AAAACPEGNo1/Qb1QAAuooAQAAAB/KBPt0n4YaGRcdLbzNs9v+/fh3nKehI8hBwAAAAAAANLP3ZP9FS7vVobJIopxaGtP8DPjqi1vGqiSXhyBtEhEwm4JV9SWBRrroiWf AAAACWYJS4k3IaxaAAuwgAEAACCzYQaUr8LEcQZVx8Bz1uzGVEW3wkHmylb2eQ8AAAAAAPGFt5+tv7IJkLJFLNfSbLYUZ+HiBnWNFD0YVCjm0nRrE7MRV0unchyAQEpc AAAACWYJXSVEspESAAu4YAEAACCKznfJdiFSxDTHv8etLmOkyVQxHP0FjBdNmxkAAAAAACEFjGzom7jg7ARQnUpODZNXigWxrKuOjSauoPhvLzOOm74RV9KpHBwSGLlL AAAACWYJo5V8z6NfAAvAQAAAACDGs6z5IrLwlGGoVZRYhJhNLt7mslxjgdSCBl0IAAAAALDH/xOnFRMLOWs9jIxQfv7bymHHdKPbkFrz8wRNvhbmzMsRV3QqBxxbIAUF AAAACWYKvVZw4snyAAvIIAAAACDv2j20gm/S2B1ZVJh7v0ssWL8JsBRqeKalclkBAAAAAPL6s4/L+hEywOenVMRTkKbE8l5uoYOBAIY0RUzJ/X6tHc0RV53KARyxlDbs AAAACWYPJFpBL3veAAvQAAAAACDkj0YZKGbmtCz5wJSq98rv0Ltt6cu/vbdyv2QAAAAAAEzRbMN6g8p+xgiL2IEpWrDbYnEInbGQa4vSiDX68YeIQ9ARV0CnchvdI6kD AAAACWYgwGmCYlsrAAvX4AAAACDpnqj3lUtGrP5LP8ndLjhixGYi1kq7Ynlr8z0AAAAAAG136eohQv+Lge68NT44D9BQU2iLBvUUofAOQsQZvcGRwd8RV9CpHBvYLX5e AAAACWZnMKaHLdhfAAvfwAAAACBjYpK9J0jSZYy3CJfi7lq437Udtk5CnzptUQYAAAAAACDoIHb3yAMNdXMGD8wlUq+PBKymOPeXFikCi7UGNf3zH/4RV3QqBxs3bQVi AAAACWeAzeH6+JxBAAvnoAAAACD3KqlH7Hne7Pv2UFYViW412zgmcZSy4Z9wwQEAAAAAAKomOfH4vcp/slYt7WJWNqIUW0mbcu7W9j2RyMxyp8W0hFwSV53KARsNIYcl AAAACWvn0bJHsHdlAAvvgAAAACAzoCFXF4K81xl1M8urldGMwhY8WUs0UTLC3wAAAAAAAMoYUfklDxD+TAiQe6e5Spboq+ik3UW/wBRoDxzASGy4FGYSV0CnchobfxHc AAAACX2D4PN6j+vVAAv3YAAAACDAaEKufE2D8z6+rUycPfr8N9GBhQu/0i2uCgAAAAAAAEU8UzfRjnKWjKTF083b6hvVZcdEFDFX2IveEvaHa8DW9n4SV9CpHBptMaht AAAACcP0HfhGDcV3AAv/QAAAACAPBvCD08dCs8O6cY5m/iqbaIeupdnzr1sqCQAAAAAAAOjKI/rLkZZ8lU11SBjFm27ilJYQ9ix7XcKgnspIFR/py6ASV3QqBxoaybnr AAAAClTyf7EiS3p1AAwHIAAAACCjF6rahGnCOcCiLtMB2oyL2JhmEi59/+4GqQ8AAAAAALwKgFaIVAZOCu6vq6o7B+MmEP4dn4KBUhavRwDsvuTs8dchV4rTAB3gD+0T AAAAClTyiTsDUh+IAAwPAAAAACB7xPWsoy7gsqJDCaVkBxMUuUWy/bzCveUVQy0AAAAAAPrNV7jNZJ/W1g1tIEvX6KfJYtEZuRnt3tALfhhxZxUyye4hV4DiNBz/dQCW AAAAClTyr2hp37YGAAwW4AAAACCg8bwGI0AldwIgY++/lZJxZC2Svkzgg7vJK/wAAAAAAOSuKhwhakNG9hc3rgBgsnY3uNUtvcViibZOGAo9jN+VYvohV6A4DRwkHXKq AAAAClTzSB4EFh+/AAwewAAAACDN6l9XG23YeYT6vVNhjiDb1kYnYZdLM3wOaUIBAAAAAKI9cjQVScSAi9OaQCJx+ZhEuI18/I0L1R39LjRMun44LQYiVyhOAxwAvbqM AAAAClT1qvRs795BAAwmoAAAACAN+1NOs7lzykXHrSsVkskW/twY7bvN6CB4mmIAAAAAAIYM86jV2KF8gu/DJSeFBw43faVZHNqQgb7lgkAMPhnTHRIiV4rTABwnwauY AAAAClT/Nk4QVuAqAAwugAAAACB3GHAMSU9qWj1XrIHcsj/s9ECOJAP+89vpwiMAAAAAAOI66i9yhvdWEwOtTuk/PmcEAtASUW8WXWoth1Lcm+vqCh8iV4DiNBt3qu30 AAAAClUlY7Sd8veNAAw2YAAAACDbagFYiOn3qONtcYDfPi3jNvqVvsTRtkOKZRMAAAAAAAC5UrlB0xMPyyXpjTa7GgNvzskV8p9vSZ/ujrwFWp7w6zciV6A4DRuQbOl5 AAAAClWuN+3MH2fKAAw+QAAAACDe1jWdK4j4ezhkfjCiw1dLptipbUuMXSlHyAcAAAAAAFZFBYlkys7FZJY2GB6vuAvwDGua3xZ0nLH21D01DOnDg3QiVyhOAxu7/mxd AAAAClfQUJJCSgbPAAxGIAAAACBrUhWSnecvj8p9HAgfQY+hLr2ArzIzggv5KwMAAAAAACz7iGfsgWnovO+Sl7AE1YAH5yDQdPFqZbNb0O5MeGUzMBUjV4rTABtj8a98 AAAACl+0G5gNuk6rAAxOAAAAACDINoYXT9lri93GoJdoABHoUmlM8qZdZcAbAAAAAAAAABmSIfxzGMOpj1JZ70FHgDh2eXPUM+bJxgyCUlZCIeh2p3EkV4DiNBoIjRSp AAAACnNUyIxpCuAIAAxV4AAAACCg4+ZfFsis6/HCy3/biFTihvYMGZNTsQkdHgAAAAAAADtkVW7joxXh03DvCAjyzFO6vzVdOZAESTThqx+Wy9iWdsonV6A4DRrF4jMg AAAACqiUQLJN63tlAAxdwAAAACA2q6dFKIMdf/hbcP3OV3AG9N5XQdu6ZuCaxwwAAAAAADraVYXyVnQMl8/LgZFitYJL/rwOZnuVCogZjkDXzN33sHotV1MoTxzrGJpJ AAAACqiUWh/C2Fa9AAxloAAAACAZVxk6MxUZ0tHybd4ugHVItfbvtJRLJJiDQBQAAAAAABcKg2kHWMqqFFMVYDUXwVruuTD2nrwUb/nZQHKUv5zFf5AtVxTKExwwJj1g AAAACqiUwCYHV8zQAAxtgAAAACBXcsgySOMX4ZtWuU1pm2T6YlHAEHF1vKE1QREAAAAAAGSlph9XS/aU7XibOH64lPCrn02HMoKwQL4HTX8hkUWWbpstV4XyBByrdQqs AAAACqiWWD8Zf4sMAAx1YAAAACAfIcbaB7vAFOI+a9zN5GEHuolJkK/hziv7FZsAAAAAABGpAhLjc8bz2gKfgdjNYhNlvouMqXsXzycDNaHW4Hc9T6MtV6E8ARz7XP0R AAAACqicuKSra3eLAAx9QAAAACAsBQs/5zsqIDbEvgp7RRq0vVXzrVQs3m1QvhIAAAAAAMredFd9Rdt8wWDftNVWEor7Xcw2AZ25Cb1/+RxGMonvoKQtV0AoTxvEQBPt AAAACqi2OjrzGzFmAAyFIAAAACAIPa1HQtSmPj91nVzgBUhJDWVwVcZf6wx2YwkAAAAAAAsVNuhO7KZe2S34YbK5PGKww/sJ81KHZ66G16yaoDnMGr0tVxDKExuMNxFa AAAACqkcQJQR2hjTAAyNAAAAACAlCKe2uviHefp415fmmIMIX/62aD9nd0nF/gsAAAAAALEyONuUQmEC5aGyfTzhzfraLHhaQF3YxulJ4573HMq7YeYtV4TyBBshQ9uk AAAACqq0WfiM1b5pAAyU4AAAACAafs8WDFpKdD+pOxMmDa8xACwDFzxBuyj05wAAAAAAABJOMBPHz2tTDkZqd6tKFD9UPQoKauhHBruME5q1qqQdozkuV6E8ARvWuUmS AAAACrEUv4p4xGxeAAycwAAAACBM6y89ybcMlgL2VZnYtlCziv/qxiODN4C8NgAAAAAAAER2b2MNzwO0lf/JqBo7lUWym5gHN/CIYv4iMqZGhdwBAS8vV0AoTxrGHGVs AAAACsp8dnlp3ZsDAAykoAAAACAKwrgX4MJHd7cVHdmAmuRrNn+xPYK6rtxeDQ8AAAAAAMZs93uFRAyMOEWJznQhVkAY8wfWLSWTFQra7J+BxyaoDNQ0Vz5KThwKHPEh AAAACsp8kENUKqH1AAysgAAAACApiEkBVlBVUXQ4vsPhEaKtUnRaq4KAeVf9+B8AAAAAAGBimX2njLYGnWEvfSDM15kryfiGaWbef9GrLBQd0ctX/+U0V4+SExwRRYcg AAAACsp8917rqb9+AAy0YAIAACAzKRdYgFySze7wQhGMFU/WCoHlhcXqNbVg+SAAAAAAAKnAgog4fNoLrYSaPyy+J95FiN3d8QDavakEczHG//31rgI1V6PkBByTx0YD AAAACsp+k/3avoNhAAy8QAAAACCFfY7+VAAh/h6w7nJZxOVgoQcVIo02CZTYewYAAAAAABY4PvNoMhCcjK5Z00Xgfq1tA4uIXNTkKa/eMlw+K1VmSxA1Vyg5ARxW45DU AAAACsqFBn2I/u3dAAzEIAAAACAJO+lcds3UZ49FkLqD4LrGplyD1DIy6tXyITUAAAAAACRZQ7F2/UPxsMOE8ruPrnGrf8y5AwZ6WsBYxrfUH6g01yA1VwBKThspuomb AAAACsqe0HxCAJ+uAAzMAAAAACAdDM7lMkIHgKR3P9knznj7cbX374TEoPM/qwUAAAAAAPQ6Xz9PhbVfKO77q9Txn90luKwnV2WM+SwdVd3SJCpB2jU1V4CSExvAb5lD AAAACssF62O8rg4bAAzT4AAAACB8M9paVrpbd3x+HwdSgEkEbJSGuxzMp+zJSBAAAAAAAK78eXK2LczK9u840YOrWQyN5s0TZJUy+YjlJqzBouAiuEk1V6DkBBsj+jIM AAAACsyii09MyWomAAzbwAAAACBhrMn+7gVWqPtCghQSiEd9TZJvYb5B3qzKVAEAAAAAADxS/avVrpFTzjq315y8Simh1bPyCAUiyEp7v2YBdmr0ZFI1Vyg5ARsQny8w AAAACtMVCv2NNvHwAAzjoAAAACAQbWj+9G6K6FHtLcwhUI0aowYCjDNvvXVPwwAAAAAAAAJGlqbm4lE4UJ2CQilA8Vt4IDbxVcADfoNW5mTe7s8XpF01VwBKThparCuB AAAACulSADKEnlLxAAzrgAAAACAfGmvgiXm1EJKwqwj5nmOIuhTKpkls2IEFHgAAAAAAAAcPiZeYVoVIsiBsPy/R1mvdXupA2KWxn1Hd5yftqxqE4KQ2V4CSExpZYJdg AAAACwiJvSPzDmoRAAzzYAAAACDzOqdKLeaQU6WSL3dP7BYOQD7eC7Uw+Avc5tAAAAAAAFIUOhwpiR7VcGjaI0Q08Y61+mO6sBZAWx8g+tBGG6PsNKQ9V6BUYRxF2Ofu AAAACwiJ0doyLIRTAAz7QAAAACAmq7lxKvcWVdBN4IpDdPYvYCr3msYs86IBJZwGAAAAAEWqykCERMc/hXcKmBy5yj+aEu0E57lgXE1TImKH3npiYbc9VyhVGBwm2xxG AAAACwiKJNPJQBxRAA0DIAAAACA9gN9n+gxkPtvrXhPrwljwPzaNmwxVVImzJskAAAAAAEFr3B61lUkPSNo8yrhVFyA/p81nK99GWM4YW6oR4PgYRsE9V0oVBhx/f3y4 AAAACwiLcLolxewRAA0LAAAAACCXgdxFqoOFdVP23xjp4VuvYjZcCeLpGJyt6REAAAAAAPj0f9Bu0VqhngnmtUE4a8QMN+DeSTq1cp8cY35zsvYB5Mw9V1KFARxZhbYA AAAACwiQoFVLfCWIAA0S4AAAACAhrYUteje8uhYcTLEWi+wvDQ1AB9/c9E9iEfMAAAAAAL28qwO/GsbqNPnNh5jDjLYl6Iatm+dbVMZCTSEH3uMJcto9V4BUYRskrTAM AAAACwilXsHiVRsiAA0awAAAACDFf5es1goMe5OAW/4usRfn341elajC/atcRS4AAAAAAEOj7r6rfZawmEmGObiDVV5srIdpYo1as0SA85mpU7mIb+w9VyBVGBuY4DRR AAAACwj0m2hcUgM+AA0ioAAAACAo0lKKB3ibys/Dv5DnCHmsmfVAszkjoomI7Q8AAAAAAFl+dLXSJAHqsZ6yJWiM5IPzK8gOc0J69j7pCwsNUFILGgY+V0gVBhu/JeAn AAAACwomiZpepJyDAA0qgAAAACBlsz10UHGMxpXNItRUS7FdDrBZ+LNWKUmkLAEAAAAAAMlbuzcrB27NUBsPwi6ai7An0HpHOoyI29FLuKZ5ykrZlGk+V1KFARvdFqfQ AAAACw7vkTGcJJ1MAA0yYAAAACCBJLdPsYqFkAIcpQ2OTUp2NqC0nsW69+hYTAAAAAAAABuZtQf8cBhKGCMYFzZ+kBkNrD4L94dX7uoof9ZEod14o4Y/V4BUYRp+VfZC AAAACyHUjbhtHeH4AA06QAQAAADvnEzXU2v9aPnold6L/nbBALi09T9WmbxgLwAAAAAAAInKh/Kit9lSL3SK2HRJ/r2cCpdnewVCuWuw4zqV1BRNibJDVyBVGBpSMm47 AAAACzNgivvKJFXbAA1CIAAAACC1fZvk4wj6HcSk+Aed82Eli224tbA/CtH/CwAAAAAAAFF4VOZr3f3Be0of0zejnBiNm6v3lvOUAiiPFShN2eHFKf9WV2JxGRpQqjjg AAAAC0d9N59AwaTRAA1KAAAAACAsA52Girsag3GL4Ns6hkbBRTFO/dicvpaiBgAAAAAAAKGxSjF2+e3aON9cYhx0e/oHNxbxbIRtXEonDYBVXW5TeNFhV9jqDhok1tLt AAAAC2d+0CAKRv37AA1R4AAAACD5ke6bUV656OLmS2C9d+Ii/zkQiYJuSX9VDAAAAAAAAMqtHOMVi5WOF1wadcxZqTpr4txxcQXQViyFg7WoZFDGqZJuV/5OChqMrEIr AAAAC6fobOt3YlKqAA1ZwAAAACBV30euQTM5kg0p/6e7NS9/HxwF80scEdIwmzkAAAAAAFRUhjO2BzX/TD4MUlWJsdZHhhKtqQrPauYIVfSnIEFZz7B1V7+4YhyzAz7J AAAAC6fogV1w+/YdAA1hoAAAACAmLnNpH2dMW4TzFqp/A2LtSlB0xNPo5Dk0c4gfAAAAANvhlQYd9i2G32ypw2+ypBHbAh+bVADr5CSAsAPXv3rz4PN1Vy+uGBx1P5CG AAAAC6fo0yu5QTWLAA1pgAAAACBLDWxPmJBk5G7Lzg4bSltWW7/x4qd2q9KTL6oYAAAAAGarKFL0xpqeabWoiR6pj//U8yjz/rIRCUz9XzWn3NlVb/d1V4srBhy41Q4H AAAAC6fqGmUCWTbaAA1xYAAAACCt1qYUX39xOtd2H22yWiBXBWqJvudc9vx0JuIFAAAAAHQFCnOXvMvtaWZfSag4rTecLh/WTMz9Ni9B9MrFOshsefl1V+KKARy229eW AAAAC6fvN0yh38yJAA15QAAAACAAAS+r4dhhnTUIWVPJFcePDyFzeQlMMmuGPSIAAAAAAD3wSVovvE8CnxfpSnZgB2SlGfmN1MOKEwMEiecBiHs3wf11V4C4YhtFk1xR AAAAC6gDqusf+jMGAA2BIAAAACAKyVIqn1fcD05VhUTsUxl4er1JOLQqRdYfMisAAAAAADCXBftPyG7TDWGPy5SC+526LyZN40C2+iGiNA/8YRtPcQd2VyCuGBtHdZAB AAAAC6hVeWUYY+SZAA2JAAAAACAquT5NFiWNPL9VhBG9JSMjQQNLdbVRjHQyMRcAAAAAAF9LChYZd+1GDaAm0iGqYsLTMTp9CdqPG8utrNytNz982xN2V4grBhu8rusA AAAAC6mcs0z6CrqkAA2Q4AAAACC6n20QXE8+GeKh628OkHoavjtFuful3owVQwEAAAAAANp4z6iDsqBLj5ZfNGAHxgSSEDj8of4x7NmJS/VXOJJrejl2V+KKARux+xUE AAAAC665muyAphqxAA2YwAAAACBWTKjG8JhkoDZN++qkIZk9WwIwVq9WFHV3RgEAAAAAAFwrx5Px427sgiRsqedZd4r5Pa8k1IlAwTdWIERGeiMdd0d2V4C4Yhoh0hZW AAAAC8MtOWqbE6qmAA2goAQAAACSrvjG4XiPddKS2qT7BbjQH8vT7DAtz3Y5JAAAAAAAAFKRhxOtEuQLTG86Fxh+puIAezWHyEMUH1m/73bh/N1twJZ3VyCuGBpevjoT AAAADBObB49KZpemAA2ogAAAACBp8lxnn9BultC+5ENLAJIl6bG8R/KzGtsjEwAAAAAAAIYKZNtpjJ+CqbXx4Cxn1smxLMLXdgfopuQpRNqaYsme6Gp6V4grBhqOGFwp AAAADKG+eFoC/G5gAA2wYAAAAGCNeTilJobqdvLKRNYF8Mz1EPfKtsQiDEF6AQAAAAAAAIE6Yx7EeGGgM829XxFJ9eAIt3qX6MgNlhWTYi/ro2hT531+V+KKARqSfAtI AAAADrk8D0lbn77YAA24QAAAACDRApmVyVfAnUH76eDhAuPF++Lc36DjdBMrCxcAAAAAAIBd2pRJ/jQVmFPCRVx8vLgfsqaWQg4ImrJd/u99S5LcRIGZV///AB0dKszD AAAADrk8Fyxjgsa7AA3AIAAAADAJq9Rt70GCwIzFOKGtOi3aDGiHBGMp2dT6tBUAAAAAACpuBExz4ENprDpUdYuAsiSgzH1ZJQ4KilDgwZlTCbCgT7uZV8D/PxyhBEJk AAAADrk8NriDDuZHAA3IAAAAADCZH856VE1BGiD12cdt4NmjE9RDCGLxabi6vgoAAAAAAPie4AXBNxLSCyYCCt7ErCHHsQkzlYXi8FSQ+zf6/LB1n7yZV/D/DxzPVkWm AAAADrk8tOkBP2R3AA3P4AAAADBllXxbXovWrc28z6uOnfRyI70usegvXPDaCwsAAAAAAKzAOx7IjWpUiBEpyBDDI71RFAQr5NDEXzYN7YY0KDEbusaZV/z/AxyKY55a AAAADrk+rar6AV03AA3XwAAAADCUck5TvvRlQEsLvs1fq53zlF2JyOIb7kKvoQwAAAAAAHlKt/5WfhW3WzVE9o9NnqFeVlelsCBGhKEP6zKQN3U7Wu2ZV///ABxRqiTQ AAAADrlGkLLdCUA3AA3foAAAADCpnk5uDQgtq6xioQH4MlKwffjPhr9SBIzM0GkAAAAAADVlHBcb1hKkjIKZ9HUZSldpWevAv2cl2oHnYj87TGwiqu6ZV8D/PxsjfddK AAAADrlmHNJpKMw3AA3ngAAAADBTOzBBlxMQtLKn/nBPP8t46bX7OtxbX2j3xxkAAAAAAOMjSxmLHM6CK5czL/ViX6maYLZmLMoSvHPSVaNEpGDX8AOaV/D/DxtcSU4U AAAADrnkTVCZpvw3AA3vYAAAADCV1SlW+D9yPT/B18Emi67ZOojR9HjbIc50OA0AAAAAAHvQOMjeADF8AjKFuJe0zfOhYIQjppdn87ERjWfzO6I5LTeaV/z/AxtIT0LA AAAADrvdD0lbn7w4AA33QAAAADCLn3AV5sh3vbyELB8ikNrYMIa1cv91UsKHPwMAAAAAAPnDFPWhae6UBSmU34MVQYYbIZl2ougp4uBVfKSBPXAc7+2aV///ABudVN1h AAAADsPAFyxjgsQbAA3/IAAAADDRrs54sIAOMyYkS6KzIX3eN2frg2Suqq1y3AAAAAAAADoJf/ShL8NF6mzUBKdIt/SllfTgjfeiyO08fH3kZgRdyBCdV8D/PxqJ25ED AAAADuNMNriDDuOnAA4HAAAAACApqFfwSeTlbhtcBC/ycVJWZMQqsI0iNxP7AgAAAAAAAG7KEirKHr1/SPn4ZLT+nb3UTrm98+oWY1ywVDLJ03bdi7+fV/D/DxpBqWaI AAAAD2BQrXlgpguWAA4O4AAAACBO6cgvrBNqFQL2Fb2dgfCaiYgwDWT/diOQAQAAAAAAADCj6yID0P2rPA4JO4UVYlbh7il5VJrZ/Px3N0H1XnExI/ynV6sdBxo8UBxL AAAAEEJvSeNfseUBAA4WwAAAACCUty2OZD135FtFfuM5+s6KO0ooyKFhl+R+AAAAAAAAAEDupwwP+ihkzyOZcl9j4QH/KCaet4NiNx7FWxasDxBKW7S5V1DUBhp2t+yS AAAAEWZON940iQTDAA4eoAAAACCTqwQ/ZTyaMklDST2ZR79yvI0BV3+nldS0BQAAAAAAAKBkz9sK+MEQ3oYlxPKoio78AMMVj2r5BEiYRhjomIC5eFi/V+8TAhpBVQyD AAAAFExt2wOanLhVAA4mgAAAADDOgW8QhETnVAMmuwp4WP3M9eIoiNbmOsudAAAAAAAAAEmC3eDrRbEPwI/cBAFMlbIc4XSc+5xfdaXNM+wqW611DzjRV/ACAhrie5OZ AAAAFtE60M28lfB3AA4uYAAAACCaMvcGJ5duBEGiuSuFbxyV21K+OS20HwIYsc6jAAAAAHXoGMtj9AEm10IMjWbGHJcthL9C5749MXSNVnlDPWjN0GnnV///AB0ibsDK AAAAFtE62LDEePhaAA42QAAAACCD2XeXqwYyOFut0/Bw4J230XxOe8dU+iccjk5XAAAAABLcCaK5nEDzeR+0VVqR5QmwWJecqHcCuJWlvLlCfstjnm7nV8D/PxwUcmvk AAAAFtE6+DzkBRfmAA4+IAAAACD+p9SxJT1v/58zLCK66MMEfj7VITR0wUD2YtU5AAAAACLyMqj435DNbBe5x4yYNsEr7eOy2SLmfTmAfk1kL5Hg6njnV/D/DxwO0YHR AAAAFtE7dm1iNZYWAA5GAAAAACAmXrHDvr9BclgR+3cPgCZcjeFxeGlc1VkVywEAAAAAABOVjKBC3EcVYA+8Zkh466kzt8Ro8LhhiPHUwqIT/Kc9j4vnV/z/Axwh7DoQ AAAAFtE9by9a947WAA5N4AAAACBbXt5nncW8mu/CDOtfZe8mqHxsdRkDCZMa4AYAAAAAAM1mc/zfDzOVsR0tQ3NXfSI0MwRRiH7KsV8qg/aH+VxQl6fnV///ABwoxarL AAAAFtFFUjc9/3HWAA5VwAAAACAZRKfTiu/adJ1EvNiq9noHimMyMAFMWq8aqAAAAAAAAPjzuuE7t/EiD4vOLHgs0kppqK83s16zaMul3CzSQFA4O8bnV8D/PxvJTn1J AAAAFtFk3lbKHv3WAA5doAAAACBK4iv8mu+RYlZ0j+TlLY4pYSEvWMFjSVfQhwAAAAAAAFs66rCfYvUumiTXyqqlc2I0bX/lP7PcmYgDSEuL1vEEq+XnV/D/DxvKhjzi AAAAFtHjDtT6nS3WAA5lgAAAACCNZUYcP+lBC2xmxqiC/o62X5Rj1nw0HOdRMgMAAAAAANKp6yqkAA42E9C0y2nq1COWx+VPQkmVCCG7F3DsQyX9JgXoV/z/AxsJZn2u AAAAFtPb0M28le3XAA5tYAAAACAIeaBQ51vhvWNZGrLyld8/ubURRw5vfoWS9QAAAAAAAMZf7GAbrkzJOqd6goDTCvELuoveaD2tbuVtYo0tlKs5ZTnoV///ABtkjcWM AAAAFtu+2LDEePW6AA51QAAAACDthu7A1465am24IoOs3TQpAaVCuRhuQshTQwAAAAAAAIGGSZjgvyvLeypwcHuQhRkL2EEO2bllTY/MBWdbUFR2yPXoV8D/PxqfQu01 AAAAFvZm9JHgWhGbAA59IAAAACBCrF8KJWrOhnH5xSSV6TDFMA19LvSZtkV4Ak8AAAAAAE/F+F7/yAKr+bGyL/KByzwlIQG23GlHEE9iz8vPGu/SnGrtV8D/PxwPC+FQ AAAAFvZnD/37xi0HAA6FAAAAACBk38W9b6I07UNqdOYxc4Y0KAiFB/kNBtx+QMYvAAAAAHj7r3rJqK8yQmHzLo1Anru+u2LgidBd3MQlwKFJ5R+zPnTtV/D/DxyU2LPj AAAAFvZnjNV4naneAA6M4AAAACCts/sPg/7gvjr045kHsVMWTr1lNuqRfAAzhwQAAAAAAIekU6P/7aPz+z9neNBna5u6EIihC/4xSfTI6DfRjjMM+4TtV/z/AxyVbXQd AAAAFvZphZdxX6KeAA6UwAAAACB///tI8UDgtFCtwc16eSADS4zHoOv8WhybWgIAAAAAAOmNdM2v7Z140jUnjL0TAfFt7WZcORVfh88q0JbcZZIdCpPtV///AByR477o AAAAFvZxaJ9UZ4WeAA6coAAAACA35kkLMA8srR3Ly7phHOQNV8XB7Ydz0ibQpnwAAAAAAIHRkgLNQqTLL2rPrQganu7cXd8EZHOk+sMRRDjbDWk85KDtV8D/PxvIErHP AAAAFvaOuUulE9YtAA6kgAAAACC7mkp5UCwhDmm0tP2IQbL6pILaCdV6p4P2WAIAAAAAAI070Qh7ITtTUH4mIvzHd7CM1mt10pBOwf/TFSkMoC/VBLHtV/D/DxspgEGE AAAAFvcCWmhGMHbWAA6sYAAAACBAJHjxyGNNy1Uz5BMe4xROSDDJE6y257wRUQEAAAAAAAp5ACLSkVL6bfOLFZ3AzMnnUUKYSKrngn3hU6tnzCB/esztV/z/Axvld4Mp AAAAFvjC3QnI0fe4AA60QAAAACC8glNQvr9l2H94E346yBCP8pWddMfK3k8HygEAAAAAAHgygNfgRBo/5rOQ3MdPcD1Th48lpOl3tTEWqqJJLs2gofXtV///ABvDepcW AAAAFv+g5OzQtP+bAA68IAAAACAstVirUpNdahbB8xXA333A/QW0StRdUKYRHAAAAAAAAFk9boB2W8MhaBFMGlGwRrKBgFxeFZV2h1d6++Zduw3MIY/uV8D/Pxp2zSpc AAAAFxYY/anpchhYAA7EAAAAACCsVo/dfrgBLlSrtl9VxGhsjaDvtMrh+PeDDAAAAAAAAPp4Ty1aaYXxrO4Bwug+gHTDUa6yWmHwHrPA1Q68QzrKYEjwV/D/Dxoa09c2 AAAAF0opNlwiJFEKAA7L4AAAACDdlwmqgXh97cMuUST/SpkqPVeIU6HrDB1WCQAAAAAAABMtBumhPQQyqiLmeWH+PimZ1chF9K/t8UZQIkqgY//UfrP0V/z/Axp+wlqY AAAAF8PpthapYv43AA7TwAAAACCe7tZ+py5b17SGVln9UhIKp3bA+7FNubvPx30AAAAAAKYRNzQJxAaX5+/PTNgqXtXU6IgGaN00LA4S7GbqX2id2Lz9V24lfhwW02WK AAAAF8PpxgV5Pm7CAA7boAAAACCs9HWUfNPttzeHA5GhvZ6yrnMvhZCMirb+UQIAAAAAAMPA/zFACA5MO18V7ppOTJrUoASmIJSwoiwfXVvJ97/6uOH9V1uJHxztGdFh AAAAF8PqBgrXJ3DXAA7jgAAAAGAGkbS6J3x4hjksG5GIEpUU5VR2BKleBJeaPwEAAAAAAOBqhLFcrTGyPd3ngDLuTx5YbzrWuF7ClO7wdwnQulpjJef9V1biBxwZcbO3 AAAAF8PrBiBnPGVrAA7rYAAAADA66PNSRnC3EnCeNHIxoOu3TJ/GviIfxjV3w+gGAAAAAOqkKmorYOEV+txAyBHUKfYqQLoHKeYr4pXqXMw8ygAjtO39V5X4ARw5mS/l AAAAF8PvBneq5dl+AA7zQAAAAGC5hJxX5UuQj2aiZkninwe6Uw7l2ENwZdWtQfMBAAAAAMXBRS7u22PKI29+WDFcg8nWB0UMGhF0nxNWPffCSZyqvfP9V0AlfhtfelJk AAAAF8P/B9S5i6nLAA77IAAAACBWO9GezKda9BKZijDxwoPRS0I5edd31FwnpikAAAAAAL2miQQQxBlvJnA/wNvGJE3SI8h9XRuEB74ahGuj5IbQM/X9V1CJHxv2yCMG AAAAF8Q3CIScgcYRAA8DAAAAACACsa5w6mo1Mqg0uqXJON/u0lvfrh9eniM2AwoAAAAAAMPDIu9T6pV42XROMrJ9yJEe/Io6jq8uBUvsqh/D9RC1//b9V1TiBxv6KC3q AAAAF8UU4F7L3YA0AA8K4AAAACCaYKPHLjjZon6Oi+EmOmpfBSS2TqaTULKiwQMAAAAAANZu5dm8cf1duw1nlbaUkAtpQj4/FS6vZ+S8mhC1DhDONBb+V5X4ARvDPj1j AAAAF8jEjQ58YN7MAA8SwAAAACApg+5CF+1Y5UemFhe9k4du+q0izkCHfr+JvQEAAAAAAC+EDBqL5htuN2/AWmqMgcIw+PZcy98L+WrFUaz3TluKRWj+V0AlfhqekWsN AAAAF9JUBdIhwHQCAA8aoAAAACC9gPaJOxyR4sB3wh9YH2Ia4QBAybIbH0IgEwAAAAAAALQ+kaXBe64LG+7UgI1yXLDmSyzkpNWXsx16FumpHwyvtfIAWFCJHxocqACl AAAAF+lhi3Gksa/UAA8igAAAACB86iJZcNX2IAbFy2yMZpnlrMRDLa71SamQzlQAAAAAALRl8xi1PiSa9WETIipKRa8+e4txik6arEL9thVB7WHTo9wGWM3MUhwG+C/P AAAAF+lho4q/+yv3AA8qYAAAACDIco19HaOHr7zNGpdXGkKTxZwWc2t+haT+Fg0gAAAAAJogmWxqJA8zuYYV4fLaUydrKMqAlYck3cmS+amF4iwDJd4GWDOzFBzv+LiQ AAAAF+lh+hxYrih7AA8yQAAAACBC6fvFDly3JV0wvnjYHineSRy5E/eIHBgCVucAAAAAAMa1lmT1LxSNqozTDJA/vPohJZ6BR8iQGFqE5/XtPQNikQQHWMwsBRwA4SU7 AAAAF+ljgEGu9rWiAA86IAAAACDpxl2OioVVC+wy4iphmeQP3FED8mU2PJwW+IEAAAAAAOgfKGICFqGxzPrfN4k/JJn3a4RlrKbiSmJjVoTNzOpb+B4HWDNLARwvsUiR AAAAF+lpmBIoIuaMAA9CAAAAACB5zCaM4cCKtVpws707EbhDUxJdQ/uTdS4jrdcAAAAAAOkLl8ZFyJBRakfvGUI9mfI5CsUL1O4siQfuhw74uZrbvUsHWMDMUhsB+ANy AAAAF+mB+meMq8DhAA9J4AAAACCBGpsuJKZdyRJFdHAO/PLALcqTMeOH2BX2bQcAAAAAABg4VF0GRbSmkHNkdlQWBkKievJ8wSkN8SqGY4yUpFKyZXEHWDCzFBudWkbK AAAAF+njg70ezzn1AA9RwAAAACBYn3dlFRyQsal+immbQoZmRXQ+hCkZPQliSggAAAAAANr+/lIrGBNucTK1kIatfBwvyH4YmaQULm1odwY0gc0DB4oHWMwsBRsbD0Fa AAAAF+tpqRNnXS4DAA9ZoAAAACBLXmur35fmqFGvX4r3v2Q6yvPKn97pAdrRUQIAAAAAANvyrTNEtsDcU+N8qZDyCHI38kzlaWmOae6c+neSarvMAasHWDNLARvQ+xUX AAAAF/GCPmyJlP48AA9hgAAAACDs0MIYI5o2DQ043OoopmBgtuAfH2uZmIeaYAAAAAAAAJFv4eW7pIEuLhpZ6b7+8X0AZ9G1CBf5u1L+TBQJMzOiFx0IWMDMUhpZFAuP AAAAGAnkk9ESdEcCAA9pYAAAACB026HT6hrHIkBHtnXeboqIbegioESw6gp3FAAAAAAAAMgGwys8SvhXtPP9CfD+ml976MBcqHUpcU1qd0dn+31e80MJWDCzFBpbPkZe AAAAGGthi2TFtzsyAA9xQAAAACC43QNyh32I6SXPjgM8oRqRS13eCsnrBB0QBAAAAAAAAArYC/EIVg1qGGitevzJVw8HBCInCYbG1Xd9QKaeTzRoi8IMWMwsBRoZplIi AAAAGZkGN3lTQJ/lAA95IAAAACATtsQd2aTtsKLXSkjk5Hk6/MusGv+3HrYQBHB8AAAAAAmvcIZ7Q9FkGrD7PKh/zs0VzBPak6EXMoe1oBucTpbUpNQcWOreAB0XQD6e AAAAGZkGQIf+mdUvAA+BAAAAACAkgJi3I/FvgEIFaZi+aSsKPP1C4D5BMaA49F8FAAAAAPzpD/LpbGdNo5mPhoZcNK83D6OjIreudArh9cMN/j1rwNkcWIC6NxwuYvrf AAAAGZkGZMKr/roWAA+I4AAAACD+abxT5sqlUd7qM6FdyeTeL2ZQJW1lOyyfX8ouAAAAAIyGUUO9Dbd9W2GaIYeSIQjXqhW/j89Wad5c1vCtOD5n1vAcWKDuDRyOEX/T AAAAGZkG9a1hklWUAA+QwAAAACA/7/8wlF+gxKK+7RFjRvro3l8JQ7nF8Mx2m2cMAAAAAChT0omCty82fYKc4K1fGvtCxoojkLgCE5ReJ3ZJyc1pHwIdWKh7AxzXf+OY AAAAGZkJOVg34NsrAA+YoAAAACBwuTrewrflGxRKewrcD3eKAPpY3tnVMk5Jb3wAAAAAAPvbSIldgbB6ev2yiWTRSEY8obrnMdopntprNQ1dyOMFPxMdWOreABxcogTM AAAAGZkSSAORGwFHAA+ggAAAACCq1MaMa4HznUaIgg354gEE55MSnNKKbKLV3NUAAAAAALEPsa6l7rUYga/8qt9KP3MVGN52bH5gpdg6GtZui2/2PCcdWIC6NxsDb3Dr AAAAGZk2grD2A6l1AA+oYAAAACA6fUrFYsoXvsqZhFWxr3IGcSC2v6xs85RNdQ0AAAAAABKW/xfbjD8iVoooAhvZFnQM2pTxgOxd/s3OR/2V2H6H0kYdWKDuDRtswT5e AAAAGZnHbWaJpkowAA+wQAAAACB/VoYrep9hhXpaxwMonpIffw8TS3G6j7drCQQAAAAAABpfFemaOXaii6UsgRCcy1sd71Nuds5ARo+dW4vfq8LiTHYdWKh7AxuP49Z1 AAAAGZwLGDzYMOS6AA+4IAAAACCM9l7CkyNeaoIqqCN3G59f822JNUzUOGxoDgIAAAAAAJHVX5wYoU/IPJfYm5/I46Y1eqbCfVqb6u8QLrtowWODB2seWOreABsp/dna AAAAGaUZw5YSW1bEAA/AAAAAACDaM5JbH3pV6fqObJVaIOoJQUi2DFyI9ppPUAAAAAAAADZzt7bOgVfTz8r0FbZ0CRjfdhCodp1wM0qpq9nJQbJediEhWIC6NxqFv5ZG AAAAGcZ4RBWmZoIhAA/H4AAAACD7dSLpdOhvBPclOGqTozRk5hfsIEMu5UaHDgAAAAAAAEnUJI7uxqAaJUUUVFpVa73CPIGZEF9YG/HttxhVjsrmQ5gsWAWPIhp1jB4B AAAAGfihfGG8+LnwAA/PwAQAAADiHEuHWW1ZklGivlmZYj9znwwnJqCpGNhIDwAAAAAAAJGmS2QaFRA3YvIsnxSmTfWV0M65rUAfZjllMm2qjGoRHOw4WEgUFxps1ZL0 AAAAGkXX1sPIJ3OPAA/XoAAAACCek5gdfB/WFZhCZOq3nyDEsM+hKBgkW7BMIanqAAAAALOTZeH2dUvQl0uFTfxVlThOWIl5oDcNNKi9j4oytxmmbflDWEqZAB0fgqwl AAAAGkXX4++biFOZAA/fgAAAACBiUROpJ3OT6A5Smhk6fzCSo8vV7eTMAmBakX8fAAAAABHvJGkRac3nayOOezSoK+oPIJrfbLcSfvhwaZa7Scpgkv9DWIBSJhziOhXX AAAAGkXYGJ7pC9PBAA/nYAAAACCXcyqGu8QbOLMCKX6SYjQCjt/NvYfWBsbgTmcJAAAAAGRTv5BKOkCYA6o2JHJGlCFzZAzbjl8EgUxGvR/R1r2WKhdEWKCUCRwsyUnq AAAAGkXY61wfGdRiAA/vQAAAACB3vHjem5CCgl3wVyup7LvYBC/rEUs6CIxS8/MBAAAAAPnIq0Kn2+JbzUDP3JPDEVtTyhhrFbhivSVoEE1rB5z2/ydEWChlAhw85rx4 AAAAGkXcNlD3Ud7GAA/3IAAAACCIf8w4A2520c/Ar5ek7MpCu013kfuuWcR6kTEBAAAAAKhNF2miqDEMcHcAIRP+P3EGXcgcQ4ziDmwngs3+hBGgRTZEWEqZABzmBYOd AAAAGkXpYiRYMhA2AA//AAAAACBcHHeDBijYwSpqMLsGpTQAtUw1iRIeCEREYycAAAAAADF+30o5iPYdA59MiKDAdTFmzJ/6UpywZz7fPv87M0oFi0xEWIBSJhvq1zOt AAAAGkYeEXHbst3WABAG4AAAACDaineze2hPHvz26SGxuFvUKTNX7vt4/6xSqBcAAAAAAM5mDPWpxNCD980RIn6cUjsvDly2sH1AeZaLPzL28knM3HNEWKCUCRt0kLku AAAAGkbwzqfpthw3ABAOwAAAACACWljzZESvBbDsoMRUsoD2Jyj7gqnsABqeeAYAAAAAAOhxrWsyJ2sTRneS9+7dPoiX6Iv6Bn6TrruT8dT9YeAUw+NEWChlAhtUqsUl AAAAGko7w4AhwyV6ABAWoAAAACD64JqjLPJUGSp/3WxirmjOMmudQemxVJ8uXQEAAAAAAP2T5t3me5bEPS4e9Ex0g3JXfGfetEEPNfdATZJiGHx/PYpFWEqZABsGzgKp AAAAGldR4QMQ7gzuABAegAAAACCXh/9oenRKyDzyAxEgtjS075/tZZEWvJN4LwAAAAAAAFA/u7P5DAkZxWL5C4dVaPEYh0QbQ4/q4nKQGSTxM8tubcdIWIBSJhoazXsk AAAAGovYC29/k4SWABAmYAAAACACWUawi42K+bgsFrbwblASqkjdGGo1zxxHCAAAAAAAAG+BEDANOWuwW1OPmwysgU6mVTxZ3wC8p7GOxppdl72iSpZNWIv5CRrQy062 AAAAG0P0b5VrzjRpABAuQAAAACAKwk7jlqnxejDFcRR48xbevBFBPLWdC/CWBQAAAAAAAIuQqPVVcNLJ3NgVijFSv+o6dKf5mRFVvTKRPZfNiWvVhS9aWN/OBhrRqMwd AAAAHFnawu0xI3KAABA2IAAAACBRqbwADBQD5hXoUmWQpmPAM1hfT/YGJF17AAAAAAAAAPLqZbrZAW0hWccr4q1g4F3/7Wak0ixT1EKw1xEe6SLPqJJjWGZ2Axqk1BuZ AAAAHc5rHSnTyie6ABA+AAAAACAek3iuiZNhVjEfUrvCd9cvCc4QX7KhQFeTXcqTAAAAAFyp+8UoL+J0bou7QwwQwl8jpj+a+CAFJIy6I1G2BMujVJ94WP//AB0NnXJx AAAAHc5rJQzbrS+dABBF4AAAACBrKKX6DTkO+luuyus6eIoPCkByHzQnlVVIOi8pAAAAAMQRPLl36/0xICZhe2YY9yzRH4P9jMyG2xnzYZyxtvh+LqR4WMD/Pxwvr9zj AAAAHc5rRJj7OU8pABBNwAAAACAOY8dNVj7EEstbuMHIQ29oTw/OH40Q8N3XLoUeAAAAAIyBrRurXlv7W04t2g/qqgl4/on56mjlOF0KuXoju+uMAq54WPD/Dxyho9XR AAAAHc5rwsl5ac1ZABBVoAAAACCHn+hnN4WB4z3vfuzAiBP3s8bm86Hz57GP6ksMAAAAAJiG8/ij08IlLGG8jSDECQQDRfbW27+z9kJoHYgPjdEm/MR4WPz/Axz+f0l8 AAAAHc5tu4tyK8YZABBdgAAAACB4xfKGUihIPzceCQa34Kw4+g4wxFUbcSEa854CAAAAACYd430PGThY4atX4YHb++NgjRobesFzrSG2knSgkrvOXN54WP//AByamRFe AAAAHc51npNVM6kZABBlYAAAACDnzORywa2t8ymDsrF7Zih+kdOT8Qiwa3hgkJcAAAAAAC+ktVs8ZvUxKKORFhciz4QXTenOchzvIwz6dJfhPXIRR+l4WMD/PxtU7bs0 AAAAHc6VKrLhUzUZABBtQAAAACDTHvolquu7E1hE71XgTrBFe1EnFjsBmOsMAxkAAAAAAJ1LG3XCfZtHFzU+xbYmaK5xcYMRtfltTFGiYGYgTv5ecPB4WPD/Dxu+bXS/ AAAAHc8TWzER0WUZABB1IAAAACCoCYvhDpRgX+3m8UNxdtgsyKPJT3m7RyIg3woAAAAAANxonmhJUvxi37cUelXwYyCTp1b3/z08Dk8AtPSQAkHO2/F4WPz/Axs8zcOl AAAAHdEMHSnTyiUaABB9AAAAACDzal9DKSej0nIQYFgNG4g9Oxjmix1I4mGQsQAAAAAAAGPII6IBXdPyM8UlYzw/FTZg6/Y8W/jzbNKnKl+Mctxifmt5WP//ABsEOFzt AAAAHdjvJQzbrSz9ABCE4AAAACD8Gf5FSQZtRieF0awUNB7lU9rU6Cmb9TRtegAAAAAAAJz7hY1McEpRsNPsD5Kh4OUtWN/70PGZuiLS8IBsmGxMfVZ6WMD/PxqXFVKC AAAAHffpsqbJ7WVpABCMwAAAACCZ0YrKD/kpvaCAOLj+Kd5i+rWePiw6IcpmLgAAAAAAADpmcHP0ry6tHXS9I+g1ERf0hFvNJ0mUDt8crZh9QkqBIXV/WBa9ERreS5MY AAAAHmTB5It+3bGRABCUoAAAACDNthdKnuL/dBvbA2MptX3zKAmNcgChCqFMCwAAAAAAAEZEKNlVRoApTxmjU+yrmKp4po72oj25wLsdQtGT7yeEewCKWGwhChppfo+n AAAAHti41lG+Zi1rABCcgAAAACBybByxX89GCyyTxo0f9nU3dFV5VhlrnzBDAQAAAAAAAEAeJTpXr85JyqupGsUF1DCIaCuC3YDWOBdUm5n7YBNAz4ChWOzlDBrYFSNo AAAAH0DCOAJqJTzGABCkYAAAACDVq9KpdY9aY5Q1MIvVo7nAGMxgrLMJVE2wAAAAAAAAAGsPBLwFeIb0DAuM7pZGYZq1qr0z30rxAcOCVZZnIPbZ5Yi2WBOxDhppY+az AAAAH8UGWcJNE6aAABCsQAAAACBxo+l2U4TYTD+CzsJIgmB4ogMnVSr2mSYKBAAAAAAAACGcdEEpnl88Q9EY/02H2vLpcyURxXKTYSfSGkeSViJ8BeG+WN2iBhoXHhrb AAAAIKtMIeYpK0sdABC0IAAAACAlmUQ961o/SOEdm66LlU+ZY0k96LcNhcvPBQAAAAAAAO8sFj2o5/Al4HzxSNf+Y0aptZpbghNoqZukygbWhUBFEjLPWM/dBRrOI+Uw AAAAIb9GJJ7Eh8OfABC8AAAAACD4PTHZ47A6GIyYVtgPyadxNhWBFAznIAL4kAEAAAAAAIR/JRPqFJjHwyoiezZX5hhqe4K6TPGODkyU/ft/v1JBV6XaWM+eAB09ZvnU AAAAIb9GMVVnFAKlABDD4AAAACDPLPfONf4BCJ8xtZSgwlRl9nZn0EGGFwY6VnSTAAAAALE5+w7G8lL548+kMKvHqVX1OGhxsxlCAKF2OegO6+heQLPaWMCzJxwFQaXd AAAAIb9GZC/xRQadABDLwAAAACC1fOy+QGcbEP+SKZ2k1ATR7lDe54lKjl5uTj4AAAAAAECT5eYsQV/y68n5g2tQ80CR0PBboX+qNDn+8mDNhuvvRMPaWPDsCRyRxp/n AAAAIb9HL5oaCR5eABDToAAAACCNSLWKT2aBym4x2phPiSclub0cRKpK0XfTjGEAAAAAACv/LfaFKIuzcPDF+IFJ5jnZfPbb2+z7PxkZ3TvqpCrdvdPaWDx7AhwKz9dM AAAAIb9KXUK9GY0hABDbgAAAACB7tyk0/iYDQOn7D8Ho/BKG9RLn/oV4ibJ6lw0AAAAAAFpYzaB9r8ra2xfoD8KhXZWT1hB+U8RD/tfOuzGKzuW9yeTaWM+eABxbT2AU AAAAIb9XE+VJW1AMABDjYAAAACALEewDqb24gcmpo0zKZfpEx1uMkw/dh7R1NQwAAAAAAJnzKZIC4cj1Ow7xLdlSNNj+bMk5NklJXs6crz1bkP7GMfbaWMCzJxtWJv2G AAAAIb+J7m96Ylu5ABDrQAAAACAWXDe75vVY5VVUDzzxWj7lv3gm1theWYnGeiQAAAAAACOY80B9IzcTuW3+MpXebBGfwGTabO+epQOTpqFgZcHDBw/bWPDsCRu0RaLk AAAAIcBVWJg+fpJOABDzIAAAACCLpDRR+IKAjplttiT4SFozH1F2EWKqdlMXPQkAAAAAAGlgiZE2aUAyqkJQ+PdnxNsV/affmXQgJ/aQktiQNR2sczDbWDx7Ahtqji8C AAAAIcODATtO73xjABD7AAAAACD44Vi+G+EjwnytJSxf05dilDfH8iLpj0TAwAAAAAAAAAmv9DHV2sI5LsyWSP1ykwYtvAcLj10szRkDvFkXuP6p3K/bWM+eABsFky/D AAAAIdAkrxU3MLYDABEC4AAAACC18u2eZ8+XkDLz4x0777KJsItl2w3B5+SrFgAAAAAAANIubrNAIphXooj4B86Z8wP/KBZuRxsbNUYLSw56LXO68XzfWMCzJxqrjOCi AAAAIgEvLEZXMYaOABEKwAAAACD0y9QLo99p/HQgSVeS68LYPbhSjLalzlcnJAAAAAAAANKO3hwsA1VpU2nHPpLsQm1Me7BxUAsXTS9Gy87n4YjSWaDoWE2KExp9RJAC AAAAIl4yRQnMPS/jABESoAAAACAWs+1qrq5EXdPfVbFdgwExvp9QanLXPT+BBgAAAAAAANJvDKKJbnRSH0T8W57g5uO/9wz2OFao0lLn55LXrFRjBSD1WAE4DRprh+KX AAAAIteysNc7nEzeABEagAAAACB68civgdnSZEHo3oWaKH7/Gu6MHP+c88Q8BQAAAAAAAPjQHcd2czgltmo0osB/PueJS6J0fv8NuFLXES4nYRSkxeUFWXUADBrQa/dG AAAAI0UZ9ZK4KYikABEiYAAAACD6m6dcXg1Uf1BgpEcs9ayJ9n7T8VyAJJd4AAAAAAAAAEYvkKF77XgEuuauv/oi7Ag1VWWraiTScMj2LDuSIGBvsLMXWQ+XCxrTP3EO AAAAI+r4GI9tbC9qABEqQAAAACBOXhuDCJ5BuUZiTeWlcAJUZX+IUom7GBbXAwAAAAAAAAWcd82WhOyn8yAseY/HM0f5mlSEX8W0I/KOVIeslu18K1YfWaHKBBpVYQLY AAAAJOI87cDGNMqOABEyIAAAACDcBjCks0vM4Qr9HSk7bl8BfNb6rT6jE6VRAgAAAAAAAK5tVFqb5EEvt4R59IbJr2ZO5ezGuSi7VJ2Sai+aGU1hr5o2WS0KBhqWZD1p AAAAJfiPkz00UafSABE6AAAAACDVl+Kq3oZJ1ML2mnpxB4MpaO99Hgfm1vjjCYCWAAAAAKvHSZfL2encbK8ygyREFo7htEj9LMyeGR8Lue2SO6wnaAlGWQvWAB3IAjXS AAAAJfiPnKv4yyKWABFB4AAAACAwzcyYlI6v6ils+88OV/qNmhs++ruD3zS3rLxiAAAAAPEiwVglcd6yzhQxQzKQxVA35Kp0eMe8w6ZRKNngkWuHNS1GWcCCNRxQ418L AAAAJfiPwmcKsRWGABFJwAAAACAU9G7Cszf2caZJtfMP80FTw5LlXsLTG5PAGEYdAAAAAGXDFTnTgaVbK6OoB7VhHVlbDQHyMJHjO6P5VyJWGzPsPHlGWbBgDRyexdBp AAAAJfiQWVNSSOklABFRoAAAACCGWj9JAC1BHHEun18pyqcGrVK28QSglt8psxgEAAAAADkaWQECNJ/RJlwLGsDEfeYTRs8mfxr+bLdxzDkbB7dFTI1GWSxYAxwNdOFg AAAAJfiStLjlC09RABFZgAAAACA2CIjnbXcDQagjeppsEJ6WSzwCfSMnkI64vwIDAAAAAP5MUgbB04l0q4EohUxDRbDfWbEqVkDXB6PAzS9NkOKjwpdGWQvWABwmxdKA AAAAJficI31eiKDtABFhYAAAACCl/NdE1m77Aq9TVbu8a7L4On6LPNlPE9n3uWkAAAAAAFDzedInrn0IXofQRX6qyd3z7WNKqtnhKYkHwRCOfywHh5xGWcCCNRsidXuW AAAAJfjB3o9Eff77ABFpQAAAACD6dEfoCSLjOOjjVOjegIvRenfike5AQkTsEQQAAAAAAOX0y5+W1C11WSBsQzBfnPTEVpaq47g3rSOdiYHLKLP0O6ZGWbBgDRvJcJYP AAAAJflYkXEl6D9eABFxIAAAACBSUCZPRJayt/E5gRp+fCIFM0bl3qTrIvNzfAAAAAAAAF+qwK6aEbba1HCJeKM5ksBZAZ0p5cPO66meAfRSUNbkLNhGWSxYAxtJ/6D6 AAAAJfu0Qo+FPj+7ABF5AAAAACCeorbURAPAWKfeTe5nXZOHnbvtKiTA+fCtKQEAAAAAALq691c4t4yjFJFAy/rQIMKJmpDpfMkbl+vfTsmYH/WrNypHWQvWABuApynS AAAAJgUjBwkClkkRABGA4AAAACBQqNvTldL28auu4mat9plkSxtuixVx5pQ1NAAAAAAAANK4E9YdqK9PK5+2b2qz0LCX0kwWY5t+3QUJqlXpJw6vvxlJWcCCNRocSwSv AAAAJiUktlkpz6ExABGIwAAAACAb2u7EyLFpNtW8NDAnAg56uYi1IroiXA0PMgAAAAAAAMjNDVO86E8YzofVZ9Xpm5kASNy8VE6bIE4L0VWrJBRAz/1TWReSHxoYzg1n AAAAJmUACmqBJoJgABGQoAAAACAy4+Hcp7OXeuEIrblE5tu9vWBcGkeYLduMHgAAAAAAAEXsYB8budqg7vLn70pOh8hXNXYKG7AI2Cq+k7JlxXX2PipYWYXkBxqe1vuF AAAAJzuE2uHgBEpiABGYgAAAACARZaUcm39ahb1U9xJo46JRb+gn0M1MmckmAAAAAAAAAC9f5JwkWDxQ9EsDVP6/IDZiAmt5sF+z0b2lHoijQSsGQp5oWccHBxp8iAue AAAAKCNh63Gt+dTeABGgYAAAACBJSOgDgMi5ccn8aNS1xJMEjVuzCCwKgmp0AgAAAAAAAGdGivFyrmbHKM+jvTew4K43kzrBF0gPHc9X60yTF6S3cJV5WTl1BhrmKDrs AAAAKQq6H988JY1iABGoQAAAACBcg+xuB+sT+qBi2cCq5ZPA6Tz3XWSSToSflAIAAAAAABPUlyG58Dp40s+srO6T204fGYgiUqFzB99aBbIxafCBnHCNWf//AB1xXdeL AAAAKQq6J8JECJVFABGwIAAAACDo8E3IaE685EYCQnwiQYq5kzmG4opGZ9r7VgEAAAAAAARdDy/FITqcWuk3WiRBogLxQCJ9ksYI51e17gTmnM2lyYuNWcD/PxxHWHGH AAAAKQq6R05jlLTRABG4AAAAACBN9xbUgqvBlxz0Rin3+8+g4hDdyCJxgGw8tAMAAAAAALzoCgaI1tr6VtC35HRFXE7Ss8bOb1e6hi8HDc1m0DF9n6qNWfD/DxyqI+kY AAAAKQq6xX7hxTMBABG/4AAAACAM2vTrSTC1jZopRug9S0JdfgBJtrE4JFxj4AQAAAAAAIVIo498ZiPi6I29wB87PvS/Mb2LgALV0KR6racwFiC22MiNWfz/AxzEWqxl AAAAKQq8vkDahyvBABHHwAAAACDZNlIPofCU5/JyFUn8d7o1K9G3Kso8nrfkWAIAAAAAAHu0zkNbOIuwf/lVrGYPuxe09c6tVI0KTWAB+A8dHWCd0eaNWf//ABxAW52O AAAAKQrEoEm8kA3CABHPoAAAACC93qw1oijYSDyX6iP7uGcEm0leEM7+2XVogfUAAAAAAEp0sCVD5C4j352D5RT80udxPAotEg9dMzrIiIdPAaVZAQSOWcD/PxtwdqLr AAAAKQrkLGlIr5nCABHXgAAAACD9tm3xnNXWnojAYSMHrmRJS8ek9wqB7Lo7NQEAAAAAAJHiBXPe8XkL8T0x2OMp+51rpsU79r2n5wNy5WTOxv/DnweOWfD/Dxs2MEmv AAAAKQtiTOhpLrnDABHfYAAAACBB8QqBrZ3sNQ0nnwb63+PXvqYMZ3OUOIxNNQsAAAAAAKQn7PDBJXOPfkMe80rKIcRhylftkubG8bazibZF8RRKDBGOWfz/Axu56leF AAAAKQ1bDuErJ3nEABHnQAAAACA2yjXoEwpAO+/RahQytaJbCAj6MIfN5zkf+gIAAAAAAF3rvg+D+er26Ah8g7Tj0OycU+3AD7XEx7KHbcGnr5c+6z2OWf//ABsCIBFL AAAAKRU+FsQzCoGnABHvIAAAACDX0FjdPG5cbCDskkpHfvK/g5cqZVnuPpTH0gAAAAAAAI5iTjOj25WevLsOGpopFfCriUnc8ZSNiJM7NriSY6G0IaOOWcD/PxqD3fxk AAAAKTTKNlBSlqEzABH3AAAAACCf7A6c/Htass+5YkLu+0kLSZdKqNm0gUEDNgAAAAAAAKLB4LyZizGXfUh0IuglaV70JsWTb2aLqqxBr61yt5gcKPePWfD/DxqvHYjk AAAAKbDgZxMawdexABH+4AAAACAKfLMVOcg7Rn1+atkyJ5D8UvGS5syBk7XeBwAAAAAAAAm4pj/iZ4Y8LpUunrgf0dHD7d/FZ7sCfYCOrAxJ4KdXhdWXWRbKBhoZUVzs AAAAKjYGgQfdfDB5ABIGwAAAACAyZtFWnJIxA1FOqlwG5mZHrgzeKvfAYjSDMn3qAAAAAFfHZunHo5xgovA+d3C87DQTT9a5ubLUPGDJ6avFPdV90DizWf//AB1j1VSQ AAAAKjYGiOrlXzhcABIOoAAAACBp4jkCudMCYmZX/HU+hMd7mhL5yNRw093g/GgAAAAAAHuJWZec9EwCDvv2PoH5SLODHkh0T2ZNNwXH4THJ7rF77j6zWcD/PxwFF87R AAAAKjYGqHcE61foABIWgAAAACBpoDx4iuUQdAHuqO3YtsoxU529+JOUnCwhorQmAAAAAI7yFn4GIMzzROCOzGsEItGSZTyQFeVWo69M8yi1ASooAE6zWfD/DxzwA9ax AAAAKjYHJqeDG9YYABIeYAAAACD1OxTec0KEEAb8eKaweLuxHX/RHMODftyiXlMJAAAAAJy6d3QDs6YmtcFBJtapUq+Yy8zaYBhHhm7s2Pi3MHhVhGSzWfz/AxyyVHgj AAAAKjYJH2l73c7YABImQAAAACC66L4ir3gV4qNEVzPOqmXpLri41ZHKutXLB4YAAAAAANXwlim+KbvjKRt0vygcm1dM/c1INHI8h5Ifc2pWA1aX45CzWf//ABw1v+G4 AAAAKjYRAnFe5bHYABIuIAAAACDZgfNOFMOPArs1QU1s55xW+XbhxJheD67t3DAAAAAAAPNwWu5F17rexNksn07lVRbCaobOt9dSYFm5jA4ufqEQURy0WcD/PxsIk7wS AAAAKjYwapnHDhnhABI2AAAAACC+rYxJTMczKqdHMlG/Vak0qSsiMVeoPw4/fCkAAAAAADKbBggkpSu3CRKJ/eA8bUk5pVvcgDWLR2T640Hihm/V71q2WfD/Dxtquem3 AAAAKjat+yFXlanrABI94AAAACCUXnnyoYzcDi7xnZyRWIt1bkjbyyCBHkoNCgIAAAAAAC4EdKmqQrJuZ5yxcT338oj0ksG14jNf59G7h7kVl+F8CAu5Wfz/Axu4sXu1 AAAAKjhQcdVbMWo6ABJFwAAAACCA2lQoBaWKbrzmcoYVYEpTtr+CMQLJr3OHBwAAAAAAAIORLErgHJnVFPXtP6KWBVuvvsnD4tt4AAM3rkOE7c6hFEDDWUg2AhugJ28G AAAAKjvhLateuYglABJNoAAAACC3vp+x5b2G7Tmk+Lu5BXg1N+HZBwiAZ93sAgEAAAAAAFkae1nqzp8j7B1sz8DoOhxMhBvgytSdsUrUrH9mYeRhUbjDWZKNABtaVx2o AAAAKkmq9YHCvStKABJVgAAAACBVOhzBX1LBAvkNPiPLOlNltHrGN+d+PC49XAAAAAAAANQzBJki0xt97YDLOAVHWix7KZ5pSLmP+6szuqawEpEeoz/GWYBkIxrWLB0r AAAAKnEIW1VugctrABJdYAAAACCwR6uwtRWY3E1qupETRqTKh//Ou81QFnd3GgAAAAAAAMv47ZELz6VGx3UdxNphgp9gn/w1g81vmNPEkcyPJLC/QVvVWWf4HBr5isa1 AAAAKra5gwlRQukGABJlQAAAACDDHeBKsEYm0iMHHZAyW6tSTqiij9K7kMMeBwAAAAAAAI27SRurNd6vKBq8nOrr0glxgWVIJiFef4+ueZCBoe7osX7VWRk+BxoOs6V3 AAAAK81+Prvcy51HABJtIAAAACA6Q0aY80BEV/rdNcmc1TbnbvLGEPeUMezDBQAAAAAAABqkuj+SlYp9/G6arSBWsPyceMdZZv4lZg2U4HHZWS54cgLWWYbPARpZX8BE AAAAMCiRxy4VT21yABJ1AAAAACDimf/XAcqiAoS2njtWrR7WaZKTkUWS9j3MAQAAAAAAAAQJEZtRQeK720o2oOguzwNz8pzhAZEx6Ix8z5NDojuvamTYWYDhcxlrAq+X AAAAMbQCm3aOvLN6ABJ84AAAACD0UigcZTC6kv2G+1BXa+S/rFaoEzqvzlQ1i3eMAAAAAC3op89VEGywCWm1YReuPaLhojAcl79u3vof972zNBAr0Vr7Wf//AB2CV54K AAAAMbQCo1mWn7tdABKEwAAAACDFZTuFDiTswyDswQYnCzMzPNo9B3bq+HUfo3wQAAAAAGaTeXP9xCz8HJG9LpI861n9vBZ7kKagee5V6k8iK8WivV77WcD/Pxz1LFF8 AAAAMbQCwuW2K9rpABKMoAAAACBG2qIG1zDzlvpMe96RSEbXKj4Rzmkof46wmboGAAAAAI+g3C1yGdkn7zemaTWY/dtaxP8VPs6RwkwkkvxdhxHv+GP7WfD/DxwONuBK AAAAMbQDQRY0XFkZABKUgAAAACBlmCYeGEtpFftVv5oB5ZvaCrnf9hgoG9VrAzEPAAAAAG4+KMs5rd8ppMkAfC3eQI8gK9nWiZQRb0NhGoxN5nTXIW/7Wfz/Axxz6LjB AAAAMbQFOdgtHlHZABKcYAAAACCWBwc3Ka5loQ4tTcAKwNUI9rl5o7d+xBsQzE8AAAAAAHEmtXTQwHyscffq87mKiAzufJzxKsS9zAuLBb7rDJ+CiYj7Wf//ABz2T1Pa AAAAMbQNHOAQJjTZABKkQAAAACBtRhh6/1jOIihI+EdAk+Fpft/w3o6D2j7K1UQAAAAAABxOQbk0j1733bZGhZ/H9SUb6KqLPLpFutSlsWGGG6GrfMX7WcD/PxsX4nhw AAAAMbQsqP+cRcDZABKsIAAAACBQRr+bwait8bY9zdj9itlwM93EANTmexX7vSkAAAAAADwkQcbHRGEZPzdtvUya5lz+XimV3Qixo/pEIMhgydwoCnf8WfD/Dxu6U0Gq AAAAMbSq2X3Mw/DZABK0AAAAACComiAHNptFd6q4hwwEwhp4r3NPqlwD2Jpp6QUAAAAAAC8vpamz3Mn/BbJZOp8B26IkgLTQsjxUuXYZwfn1ujiB9AABWvz/AxumKSZX AAAAMbaVW6FO53ETABK74AAAACBCmGeBm23/p1MGJJLh4L3JZ6HqvoV9PwO6wAEAAAAAABLpB2/05OLsfrjIbjsjff2bJbO0WGBZOdSAAcixEnNjihIFWv//ABtV34Oy AAAAMb5mY4RWynj2ABLDwAAAACBJqspr8KTxFfdNv9smcox7nLQA6CkfEXYBDgAAAAAAALbIpjxV2mDPSNhJHHSt/1iJC//9ZkZes2YUp1273Wdd4g0GWsD/Pxp+IAoE AAAAMdEOeWJsqI7UABLLoAAAACDS3dWlc1fU5Oy0cD0riiJVQzj1U5AuGULPsjusAAAAAN//DaCp4LUdULP4YCJvchTJi59qdG3G6EpVu0SyVGzDDmcZWv//AB2AEZtA AAAAMdEOgUV0i5a3ABLTgAAAACAaHy9YbZwVjf7Gji/Zy5uOmbctSwSxQgi29ykPAAAAAMPXh9kioIIYpXV2flkl6rOdiECFbn07zT59d1jiCbcS5XIZWsD/PxzBvhnV AAAAMdEOoNGUF7ZDABLbYAAAACDvfsl9yPt48zcLzbKLUECZAN4lGpZgl1+8hi81AAAAALFlLWBwkUFH1t76ulgXnREK2R1n5DEtWnYdMYUqNwBASIUZWvD/Dxz+UwkZ AAAAMdEPHwISSDRzABLjQAAAACDNOViN+NsXgQihizZUOUz2V5j1V+15hgCGMJsOAAAAAFzTg/caSaQHK+Ex/exUF0Nq8ov8p/1zD+c95/XoP9w3RZwZWvz/AxymZdTl AAAAMdERF8QLCi0zABLrIAAAACCv0KjkkkpJPKCOuSyGO2ZsHb1iG+bQ/thLMhUAAAAAAGG49ycXfND27cOH/9TtU6cUU3bm+Mb968QeInDpsJgeTsQZWv//ABwMs7ue AAAAMdEY+svuEhAzABLzAAAAACAPS0RxBOuthTrB/P3BwC+nYBg0Swh6GxhaRyAAAAAAAJeAHTVrhTkfc7ZbzAqvXLWYl3JsGGncerSPkQcT+QzoOCkaWsD/PxuIJtFi AAAAMdE4hut6MZwzABL64AAAACDY4XNkZlBiDu51JziAOHNO8nR7cHWpCMUFGQMAAAAAAKUAgujGH3hmB4n7wE8IbHtKvxLBEYp0TfZJC//13IQqG7saWvD/DxvEHUWf AAAAMdG2t2mqr8wzABMCwAIAACC9qXXFo6mb94KIoUAVZpxaUO9uOqMuXbE4ygAAAAAAANlF+uo0EtBHUFIHY4DOsMSuyI4BsElbV8HYkY1Fxwhat98aWvz/AxutY3s3 AAAAMdOo+XXsvAxOABMKoAAAACCSsyLy1zfWHukZC3VeiAZmOzB5op+op3mZcwIAAAAAAEis13LP6krkG+dXnDGPbVp2v5tFTvBsBf36jsxd898OluobWv//ABvgd51C AAAAMds6AVj0nxQxABMSgAAAACBndbypGbV2a77lrjmKRtVZJ8nCrG8m/DXpkQAAAAAAACWCjWpXAnWWhi90iCUfANqMdsCkCmKT6JU43wiz6aq3P6AeWsD/PxrCYuSN AAAAMfrGIOUUKzO9ABMaYAAAACBYbuOZZmT8NZXCP3APQDx8tgOCIFSCkznjBgAAAAAAADHxpipwtdnRsTogbVQJDig8Zs/8qHsU8fkG4DZ7oDwVtCUhWvD/DxpBFFLV AAAAMnQi90KiA0RaABMiQAAAACB8maYs6bIlGBtH61y2JKobfzr5dw0V0fH2CAAAAAAAAKnJ2n+15GuIp0jiBnmlxxqvFeVDFcT3H5f7/huFxc9f288nWkbFBRpJ4cNM AAAAMyLwY6TGuYRzABMqIAAAACCzniwkHD//LHvyC8XFR33HztshVMzt4ZRIAAAAAAAAALdn0csJ2541WDW36Uo4Wk+CrM6oXKxeawZwlrvV3PBV+6dBWqMTCBqR/+/O AAAAM9FjZ5Zsgh3jABMyAAAAACCODFvHsxENpKphV4mFkzJ8D5DXixpe+5dOBQAAAAAAAOxQtzMxz0Gtg1L/GK7ydxsxzOj4QZb650pRO7o5X6BcrZZUWiVICBonfRtv AAAANIEUU4MM7yKvABM54AAAACDcAgFLU6txE+4uwS0009FD3u4nlajFvPQABgAAAAAAACAsivvdRcW8yvJuGCSsSOAiFeyiSJC6v13XnUhxv795TlJmWvf0BxoBOoSC AAAANUVC8hZAoX/WABNBwAAAACBIacl9XiGLwbrIxFHqoUxOJkyzEEa7w/g+XK3RAAAAAFlgbCTIxsYzi7SZFVq4M/fpZ3q1vP00hXGB4Tsz42lwdTt2Wq3cAB1VCKhw AAAANUVC+zxwkLqsABNJoAAAACAjl6t/OL/SPN2etQL9jzS3wp26uCSXH+F6oPZiAAAAABoRfErQpl0/p4a6Zn0Mrcuzpm6J6IFoTyJQY2CfI83Nj0l2WkArNxwMd5Yx AAAANUVDH9UwTbXFABNRgAAAACA5bnVE9fHOFNIZ/ScJazPzEzIgIP8Jm3zJ5BQAAAAAABOBleESxvmr2C8WB21wXuIGpYLdJbuecE+8uKyB9VdoUWR2WtDKDRxrAo6k AAAANUVDsjgvQbnGABNZYAAAACCUshRmUlXVDFjI5cJ1Llolc/rTrifhgBhoBNAAAAAAAFzJKs/tbe66phvaNheDFjoUzm0YaOEXtPymn2l+3b9LVoR2WrRyAxxAufV6 AAAANUVF+8QrEcnNABNhQAAAACALbO6LLuO7gRZcm/zAwa9kaS1mQI2iu4kxCQgAAAAAALoSrF6nCSltwfpcmoxoP9NAU1TR1qa8wtIUUPFv7DQ136d2Wq3cABwUqnuv AAAANUVPIfQaUiGJABNpIAAAACC/ayYcyPtAsGME1hkhjBxytk3vBCJbFvgM8S0AAAAAALRZ+R3b77LhMSnYmhfd8YNnjcLVX6bqmjnIxFYX4kGfGbl2WkArNxsP8qgO AAAANUVzurPXU5gYABNxAAAAACD7rBRqhe1H/xaBBFTaLS7THYy5AJjN54nHKAwAAAAAAADBlxvCAaPMq6UnU+w51MUDRnmT0+mdp7U6yTxxi+XJatN2WtDKDRvjR+PX AAAANUYCtYHV1DHgABN44AAAACDd4exkNOzu88lb8gmJ4fDZcYyymYKbXXXn6AMAAAAAAL5Sa7CVJCWJmZjBG3049Fn0bDf9xvocu7xjkAUi/pF7cyF4WrRyAxuz0WaS AAAANUhIxpoHocYNABOAwAAAACAi+H1GSs8D8F2cLZL2dsrKBni5LL46EKCVSwAAAAAAACMUqdCRLjVHp2OZONA6nYFtp4R57+5EXLZZpZ6IEQOs5xt5Wq3cABuEdXiF AAAANVEgFECYpMwuABOIoAAAACDABpXoHAQt3Al+914mgcJEBIYVp79yq9ISXwAAAAAAACQc7OrakgozDnEdRGRFhxD3U7NIeTsZAHbahgEQGFPN5Qp8WkArNxqTxgOy AAAANXRUKEq/FC/QABOQgAAAACCzEgj2kLmCAs0B+3e4jRg0M+mu2mfzs+3uFgAAAAAAAKjSsEZMIQWTXave1NmbblUKnC8b6HfYHRjyRRooZT0iFe6BWryXERrYgyBa AAAANeA/9Ixvdv9aABOYYAIAACAAMMuw3D9JfaMqrIdtZar4mon2tgaDvfPaAQAAAAAAAAyxoSJ+qbAp+Ty1aOUgUvDe3+uRf57w8Zr9GdidiQEJx0qNWmTUChq9w5bI AAAANpao7fTakzawABOgQAAAACAcRwCqSYDOKjbXymFpBYMHUpvtC62SaLiQAwAAAAAAAJSLMCr8uUaLUYnPUj2o5YdA9t0K34zkUU3odKXWos3Cv6qSWpYmAxrZHlQ9 AAAAN9fKvUq9vuB1ABOoIAAAACDMIxqV45PRXQvVUZEjenWc2jo+fd60AV0QAgAAAAAAAAKPZ2gsSWD8rtkvrkRQug/Ebix2UtlyxyjHdA8lelcfksmpWmbxAxql3T5H AAAAOMhnNyNNwgf+ABOwAAAAACD0jwlUXW+kFSmJkYr98v+xpAGCMN8CqC82AgAAAAAAADcg5ZQ8BdWOVV+spnyNA7Le/wxJK9ZXQVHVPasZfHMjbWTDWkZ4BRry5bg4 AAAAOgbYImm4kfEtABO34AAAACDLmI2eiyfFn9b6Vs2mkebX2Llky67Gpor7BAAAAAAAAO5BKXXuMbX0d3ngBnzn2IYyBB0WalSH63fLNHGhBk8g05DNWpsDAxoNSLJc AAAAPAHMe4/+VvD1ABO/wAAAACD3csS4EH4NPDoWBekyE+cert0IJSt12TvOAAAAAAAAAIg3FAXqSjIykeIi1BcGCl3RFnQWd5kT9NknfbynOdi1tfbfWhwBAxrZiTys AAAAPnYOdyWF940YABPHoAAAACBH9mo4Q1jZE21riGFMMwWk1EVa8YIJUtjQAgAAAAAAAJam/QAr5ZvugHponyI8/VTVWSBZrQ4c00jUnEJvzr3DOVbsWjECAhpIaXZU AAAAQMt/25p1Iv8oABPPgAAAACBwX9pYjWVpXAhu6HMxvVuPyd+eQF191ipQJf3KAAAAAArpRZ56wi+GauZJDTKNced7//+V9S+5iIc8P8UhcURrapkDW///AB1qtLbm AAAAQMt/4319BgcLABPXYAAAACAF2g7lzRf8tIvtQGoBKaAvCeoxd6ApejF1Bh6wAAAAAP8bbWYB+E4p9pGLDxDRxbIy2K+Y4iRjeZPwZIQTgZGByasDW8D/Pxx4hFvv AAAAQMuAAwmckiaXABPfQAAAACCcmVRcuhYkrvtZdWJ0qaAaQK3IzIMo7ZZTgHYmAAAAACXIbGnjjjxgimPyCraXRKGl/F8GzGgy35LbEp2Dfoe0F9gDW/D/DxywVZ/U AAAAQMuAgToawqTHABPnIAAAACB4GKTjnuK+BNSR2zIfycWNIiRYQj2PFVm3CgMAAAAAAMvDZ3YxPxDVL2g0wF2D3UE/Cxn4Eew4XoyZEDdCSrPZAgEEW/z/AxzZ54bC AAAAQMuCefwThJ2HABPvAAAAACAXMH0ZrlEz/Q6c8nE1+6EcIPuQdI7ADgTIoQ0AAAAAAB+7vF3YjZs9arqfFQqPQ1jaqOaSUc5t2FHUOtk27Q+Y1FQEW///AByKeWxN AAAAQMuKXQP2jICHABP24AAAACAwk2qOi7DhZFExOfqKuEqdARx9/G/BkKjL4wQAAAAAAKxMOGAHeRx9ogHMLi053ZPAFKW7UFmUv/jU2STWb+p5S6kEW8D/PxtlYFeE AAAAQMup6SOCrAyHABP+wAAAACBg0JtmrN5nCUoBmdECqk3lKUXke5dDC2P73AsAAAAAANdI2qGCTAXqPDBb7Uyl8LWPExdiSSW0w5f5/hVKBUS3/CQFW/D/DxuWrxT2 AAAAQMwoGaGzKjyHABQGoAAAACCQN59Xr9qQYM1wQX5ptZC9nw3f1iHG+ThAJQcAAAAAAM9CYRM49e5K8NfhFvAmboUXdc2DXHgD/ch6xy3z+2GksJQFW/z/AxsApTZb AAAAQM4g25p1IvyIABQOgAAAACC0gQlUSGuaO96/8GewKJpGvzI2lOwc6ic+JgIAAAAAAOrOvggGSNKRAib5rOzGlwaT17rxZVQdNH5Sk8a4soxYS3IGW///ABukd8K4 AAAAQNVEC8vOsmbsABQWYAAAACCg34XXPyaXRNzUNPAykDrSIvIXPWh+0aiU8QAAAAAAAIgXrj6Gf+8sJK763xq3/CDJ4ZDhY7jn4JdLq/Np6Hsjw0oMW58TURo8xbHD AAAAQO37sEK/KAF2ABQeQADgACC6azH1Z+HNnD8PGlByPCoTW83jRHCwt1njNgAAAAAAAI8cPbVkcMTIDTaoCRFQV9hQZ00bulyQiseo5ivdXzZWzXMOW+dEFBp0e5le AAAAQVGXuOLPTCXtABQmIAAAACBIb68RhxX7xmDp5uh2WvzV9/sf/eCQiukQCwAAAAAAAMaYphjrhuVOgdoEivDxMCupfPQSPrUpzHm2EitYh+86yk0QWzkRBRq9fcTK AAAAQt35AJkPyTfOABQuAAAAACCXw9VLRsauQv6zPo/D5rSbOHiujTsw5OEEAQAAAAAAAO3Eri3WCwyRYaJeggaY3SbFvmSpwfZHz4Abf2K9G2L5vnoVW7BrARoa9d0J AAAASGCXHcfIC/MhABQ14AAAACDHkXbCIM8k1iTeUUZ4S+ta0lofkjWmaOO9AAAAAAAAAGtmE7eUskixn4cyaFRO2EjbISUuA1pFHKa2hUv0I5iFLHEZWwDsWhmM/G3u AAAAUoo1YPUNpKUVABQ9wAAAACDAB1gJXV2IdGclllxzsWcKD9xWfk4T/zyjJqaBAAAAADw4nzgCnnztcCgclnD/o4QuUJGgGh/BV1bAQBriRGLOXzQ0W///AB3yTJ3i AAAAUoo1aNgVh6z4ABRFoAAAACAgaUS2PPQopb4GWVElkOcBYUF/duswkfPNV9NVAAAAAN4h8kd07tzUgjMExUvTHSb2LTnHLQ7eyxmAHyNHpB4zBDo0W8D/PxwrNUy0 AAAAUoo1iGQ1E8yEABRNgAAAACCmoFFJuO6ttA814H/M3+hRJLCA/Vahwfkwi1USAAAAAOxhzLTlGf04IuW4T67VAJZUc2Gdaopdteb2E+ILz7uRsEI0W/D/DxyUFCor AAAAUoo2BpSzREq0ABRVYAAAACCV35shfJoF2sFBuH/ASd3E/zQDynz8JImGpAEAAAAAAP5uLmX/nnAn3e1ecRGPDChmsbqSUjKepRQ4oHANRr0QTFM0W/z/Axw6BPtu AAAAUoo3/1asBkN0ABRdQAAAACDAUp0EgtPmAygKYT0YJH78jLBxjKi70msIEAAAAAAAAOthEbLzUndybgQgY1O5mRT+fw07venbiGNk0ekRn0TBa2s0W///ABy4d0xo AAAAUoo/4l6PDiZ0ABRlIAAAACBk44XD1Jq4XOZZP354k94ocgkLgl3gL5mDjAEAAAAAAGdcI1LV+MlAXHfwDz5GePOw6NyGJn1fbOUN9N7uKCA5TYU0W8D/PxsgPlJv AAAAUopfbn4bLbJ0ABRtAAAAACAZf9QEOXhkjiqN530eXp7QAmhM/3BHMoCrIwAAAAAAAEC50fga+6Ez+J+Y6qIjp/no6QN5AP178ZFe9PchOtXvbqA0W/D/DxvgFfez AAAAUordnvxLq+J0ABR04AAAACATcZVoLbbxEFuNsdjElP6Lydg1eFXqlKJ/KAAAAAAAAGn2Vf+lyTvKJR8q71MhVg4kGlIN5zrJqWYNatn+hFBhn7s0W/z/AxvB40Mh AAAAUozWYPUNpKJ1ABR8wAAAACCzHD86H+QLimSO4obI2B8VHrogOPHfoPVC2AEAAAAAADCpOpkUebs/ZJOMlyOhdeCLVg/WlOW0kG0chlBGGoCnI9c0W///ABtpoRVS AAAAUpS5aNgVh6pYABSEoAAAACA9fMBgEB6z2/Dz7nSwJYu38AkUxS47l1RokAAAAAAAAB8WyizNlUeoaEFAyOrbdyXxavZeYS3MCqB5FpF6Gw9t7Qc1W8D/PxoknERK AAAAUrRFiGQ1E8nkABSMgAAAACCFOm5jcVGBPos5MtjM8HTv/3t1pJcr4pTmCQAAAAAAACUmcDy/XVZ8Xwtj8IMPxSRXk+NzTfjnSiJ20w7aFYBjfs81W/D/DxqpF+K3 AAAAUzIGBiuy20erABSUYAAAACBlPBqnaT8HoNTB0XW1LhSxcXbUC2F4VrmUBQAAAAAAAB9wLoA+VFS373D+6uHhdg08zrUmyWpFn7s7S8Twg9iffgE5W/z/AxrZrspQ AAAAVSOH98qkejlIABScQAAAACDC+De1Tb5AG+D0NOP2hO+uUPMymYwFXG2sAgAAAAAAALNwh/0qcsWC0YNESW6wtCtzUO+0lu5z8rZ9OPwEKheeqn49W///ABrdXKHx AAAAXOQ1na3R43dpABSkIAAAACC9zJdVnhIWQtUtFGgwnaX9O6uOxXeCHanJAAAAAAAAADuREFWKmxbWORZdE5s5uTs9PObb13XNSe2Z3ttHWGwHj3pEW56sYBnN7C9A AAAAb1PlQUUXg7a5ABSsAAAAACB/5Cdwa4wEoP/DpZsgKmptNPt4kEz28r9RAAAAAAAAAKdi30YT+4ft542bxCTOD1eWQzIzQ3a44zqluafhByyZC5JSW1WYSRkG7Edj AAAAhX1SIdIdoYW8ABSz4AAAACDNx0IluupPiZNkc/AZnnyRpILt6VwQajY83wAAAAAAAB7/e0E1kHqlALIbPa9XOENZQu3Mb/ll+YPYCJcMlCOWRGtkW473AB3x+cKy AAAAhX1SKfn+ad2ZABS7wAAAACASi+hwG1Z/ribIeSIXoWRivy1bZ5GflQaAfAAAAAAAAJ6W1XkOhn1CLFbXT2bVpYeSASr3LNYo9ZlGUxZF+B83SXFkW4DjPRwI8m7f AAAAhX1SSpmBi0zLABTDoAAAACCGyuAEw4PYuDfZCTWuKjlMyDGHnmbnDXKzWgEAAAAAAHZlPzXsNlkD69UscVEQkVvd5jDQ7oLKZu6C43HwEnMH8HdkW+B4DxwqgCSz AAAAhX1SzReOEQmVABTLgAAAACA5YGodViTj91SMSRFe8DBd+yCQONpD7lZxbQEAAAAAADWYDwY9k5be2MjJ+tyxV+mULCMu80+be/CAlMI5fdsK6n5kWzjeAxz4ml2g AAAAhX1U1w/AKAx8ABTTYAAAACAqcrcj+HblvFIc1+FT0s6AkIM5/mM9+JKVnAAAAAAAAIWGJ89mxyRovK4M4TvJiMMVKEH71jiYnuSgUplQqvAayoVkW473ABxPbZj0 AAAAhX1c/vCIhB/6ABTbQAAAACCZOgt0XKjjZlivSmSR7tK57fsxuFHfjUNr7j0AAAAAAPUHReZ+cRfZmz09sZx1g8jTdkTlxpeMCBbjDIJTgUxneoxkW4DjPRtBCIlg AAAAhX19nnOp9IWPABTjIAAAACDkueuYRpduUcTxiWNvmu4EFV1g3jqKTVn1VQEAAAAAAHoChCo5z1pfO+Rb6wXmn9dTAzebQwebDLy6kZPCl+hwaJNkW+B4Dxsc0YzE AAAAhX4AHIAvthvmABTrAAAAACAWNFnx+QvHvRs9JkNKqsLjkK2iSr8Gw98a4gEAAAAAAPvNKXh3QnwAvYeS7wdEIGwSSCZwa8biAmL8rDPiWOr/dJpkWzjeAxtuk3Vp AAAAhYAKFLJGvIzgABTy4AAAACCClpL++IbL/b18jjTRxX63ElwKhdmnS+gYggAAAAAAAKlEr8OTHjL8sndgOW/hiB0gRAgqjoAf4sakEDi4aUc2n6FkW473ABvTPawm AAAAhYgx9Xqi1liqABT6wAAAACA0nku8ZTVNXsdHq23K7cMEgXPx/zArtEH7VAAAAAAAAHkQmBApj6+KUIdOx92uuHUC9hcEhOLl6WAEBNE5mIz12KtkW4DjPRpcqz7n AAAAhajReJwTPZ9vABUCoAAAACBIgs7uwpp23p6UJ0YR2BvMnZU1uhXDJbhjJwAAAAAAAH81xCJvWlFJ4hocTNxpbjICFeAOTfzcK002R9kx/wGeJs9kW+B4DxoJAeUY AAAAhitPhSHU2rqGABUKgAAAACC7rDWcjZpzkV5Jf1JqHJniKIrTrgvaRHBDBwAAAAAAAIJxv4re068WAKI8Kcba5p0t5fNVJafugdixbPufKAMenFBlWzjeAxp485/T AAAAiDVHtzjbTz5/ABUSYAAAACCvr3oIFa46qX1TqJy9leC4OWu+oLCOrXQtAwAAAAAAAGhCpkMY8FUBCPGxNeg6rF9iw0PUTgdLf+ObspWzpsbe1lNnW473ABoKjEMM AAAAkFGNUw04m+MwABUaQAAAACCzg8xi7ambUsYXODEmzDDXpsxUWx8YJUBtAAAAAAAAADc5BNQme+pVFLRmyym/4QtrQE9QCjtZ5wIfzDGZabhnGfptWzsnWRl8R0Nm AAAApAxzYhaVGKBuABUiIAAAACAJ3l6BeIrIlo9WfGtQgTP9hmhrNU23oP4f2DocAAAAAMFCFuiZVFdSBX/YiyAuTiUndGtVZHvFtXvw4UXBuvQuY5R9W2fYAB0jC590 AAAApAxza2sFuD7qABUqAAAAACBJzleRRY5iLPK6gbOCkPvpxNgSRyXxSiKdc0ABAAAAAJyZ8+tskjbX6JfmRNXmiZTqWRR9qYYRx5505RFEminRa5h9W8AZNhygBDmD AAAApAxzkLzINsC5ABUx4AAAACA4HTTZp7GKuqw9WdOWUkNFduHRyAMaX4McSQYDAAAAABzgF9fVoi3YGpLviZmjPmCGUhIrdw+B98YMMgRJ75iQc5x9W3CGDRwJVOvQ AAAApAx0JgPSMMf4ABU5wAAAACDRmnCVAgfdDdbGGrlHppsmz8/kuI4ii2GDswIAAAAAAEx0G1WpfOymqAUjFhove5ELAfnE31r5ZVJm/81oQjFL+aF9W5xhAxyYp8+a AAAApAx2ex/6GPyUABVBoAAAACBqbIWWiyOt9xeYZNHIT0cyUEC0B2SnJMcPyQAAAAAAAOJ1mlS2TMo5HeQmzgBtB7Ogz0Z0quwyNIPuoRklWdlH96l9W2fYABwRB/qD AAAApAx/z5CZueahABVJgAAAACApMtutygw8BChsPFpBL07YLRUT8qUjs2JaLQAAAAAAAKrNgr1PaU5xhV/zzdB2hURsFo6Gz9JDm5YP60uile1qFrJ9W8AZNhse1jyn AAAApAylIVMYPY7WABVRYAAAACB6ZCl6Zs1QBkgKADgFdmw1grYjpXtFIWoIjAAAAAAAANWAkhNajZemKT5sxWNB/xg8RJcwkYs0dGchx4lflkdsJbp9W3CGDRtpRHSz AAAApA06aF0STDeMABVZQAAAACCHmF8PWqzuxgHM4zGfoipwzNiDqmZi04bFBwAAAAAAANLgvrexzbUEnfXQOpcPTufGjz6s/pr390Bb7QsPORuCT8J9W5xhAxtJZzgd AAAApA+PhIT6hvICABVhIAAAACB/OuT65k264wfnZsL1sUCNv1hH8gNW43XxOwEAAAAAAEsvEMFdNtzf2w0+9HzpdhSC/27LmrJqFxk/IRSW4CgMnsp9W2fYABu7VDaX AAAApBjj9SSbceO8ABVpAAAAACDx/pwueLkjNybnV76aVCUl+zozpINpSByElgAAAAAAAH8akDkPLACRd/6iK2ZvmOpVxondOYZ3ir7KwLh2cUKTAdV9W8AZNhpgx4hp AAAApD41t6MfHcJCABVw4AAAACBByUZBiHE4BVzGP6MQ0KI9g9QdCvThxNffEgAAAAAAAANsycE9DYKCWX7A+qlHevHSP52D7sLD5STwuPr4gE13dfd9W3CGDRrbKIoF AAAApNN8wZ0tzUQ6ABV4wAAAACAz6zCll3TMYW9/oyMW+PzOMForVsbIa+tHCwAAAAAAAG8x5q1kCWc6M4bYE1FgMNPvJJQxyUx50xAwuGFTad0Af3B+W5xhAxrfU7co AAAApyiY6YVoi1P5ABWAoAAAACCz6/apm65pGp74UdcpwIHlAPmWt65YRcohAAAAAAAAALVjI3XSG9vKQlufM9WZ1YsXyTz5LdRGmQpFQcGywF/k7WuAW2fYABpLTnuK AAAAsHEVQxW9gmNwABWIgAAAACCyoh/YEuQ6MvEJvDHu/wQDIFYZu64pkL+lAAAAAAAAAOJNIPsgC7IJifqgQB28zEnII28OwUJbeIaPRvPxdvDfCs2GWwXBShnhgdef AAAAwaGUIiP2ByqNABWQYAAAACDEiRkpv/VQGZ66wPPIaR8Z2EYz3lPaivNGAAAAAAAAAAwQnP5bJPqh4BdLeIx+Po8vs0C87gelRJFLFnrUwnuCuamZW/BfTBkIaYpL AAAAxuhkPfrZdNa4ABWYQAAAACBRndnx/6N/sRP2xd8KfdaFz9yhSawHpC5XWotlAAAAAK5A+39U5EIw3c/nYlAQJnb85o48qG4pxjNqj0cRZHG/nDu6W///AB0gZeZD AAAAxuhkRd3hV96bABWgIAAAACBSafnRztDkdMAsmB8jCs2Z7v5hAAtSjHWu6WtdAAAAAPCyv5E51KkPLlpBwyYf96TPsV4UE3Mjbp2F36EyZneDQkK6W8D/PxzsLi5h AAAAxuhkZWoA4/4nABWoAAAAACDNSv2f8qfWxRpVjibyqgSMOUypKdMHd3RxRqoBAAAAAJFGEQbDKnNind26y9zmMED9dMW+fwXG/dwaUPQc/mlAski6W/D/Dxwe5jPG AAAAxuhk45p/FHxXABWv4AAAACAqGVxeRJ4VITYkez76q59GM6mnrD9GY3m5QbQEAAAAAK5Q1INBvF2Mp2xDWf1W3rYAOv0350B+SRV4Srsu490Phk66W/z/Axw3m9+3 AAAAxuhm3Fx31nUXABW3wAAAACDGdjzvHzJzu8BeaoCQSpg7f6cPM3XIOxFCiM4CAAAAAAjh/7eKqS1irOyBoTzAousOqTfcAWYpN3RZ1qItIUN4mFa6W///AByidNyO AAAAxuhuv2Ra3lgXABW/oAAAACB3k4w2Ac7Pvc2swW0/W7SfDoibZ28cmr9hlwEAAAAAAB3e+HFQT6xzv0lIX8v16rAUbIjB24/o0vUtlhcUJSAdfGW6W8D/Pxt+HpjR AAAAxuiOS4Pm/eQXABXHgAAAACDhg+B0btZWCtxVJXswTVjZ43tVI3ka/Z4GigAAAAAAALZfcAhsxOHbn6eX0Bwz5W5IaEucXW+EsjwdJqw+pEcBn4i6W/D/DxuUmHKI AAAAxukMfAIXfBQXABXPYAAAACCJUuTXm4ycnvY+Y/F0pYwxRX3vWQYDUuxMwQEAAAAAAFKtmxIcRCJAf6vfaQ+EACVy0/6sJq/7+IsWEQlhFuczeqS6W/z/AxtGNtOl AAAAxusFPfrZdNQYABXXQADg/3+3KljK2mUk7i/YeJ9ER50M9J8db/Ynisv8nAAAAAAAADoAIB7WHhcZZh6OrykZbRb4t4VXJph5z82U1oA50VSletC6W///ABsybEgq AAAAxvLoRd3hV9v7ABXfIAAAACAtkgxQGKR+1R0rIL6YC1AN92X5j7t/rs8wfwAAAAAAAJ8IhdxIW+Ph/XlgOlRyqldJ/VSFeUZi+haXQ7npPXD/TT+7W8D/Pxq5pLp4 AAAAxxJ0ZWoA4/uHABXnAAAAwCCKR6UgtjsOrnX1HvJH/FoNnpfkLzW0ui29LAAAAAAAAJpd9uSq/3j/12pUZxwB7THSnqninGDb3vZ+86uUvliSOV+9W/D/DxohQ4bk AAAAx43Eddvf0QeNABXu4AAAACDSFr3nppzBhYuEWZcAns8ebsuzFXpcKod6AQAAAAAAALSDdy83dtx8ZP0KmUh/VELym7QtBJi0bGSMWpKglK6+VwTCW7gGBBrueE4v AAAAyXyz7RsncUHcABX2wAAAACCeYiQAjdOgVmyMP/1hEGqWTIVJKwMWkuetAQAAAAAAAMroGwX8byHCCoYXeVYCibBBgKS8/Iy17slpxWkyHCLl/MfIW2B5ARr5DwGu AAAAzZo/VC+xAlpGABX+oAAAACB2zWT80WB/FL1JNnu5LWR11vj56d3k37iQAAAAAAAAALes4/dEB4rtAanw3oheqL3mKhPISR54Hs5jwvlwoUvpjlTaW6hmARqduvWQ AAAA0gWa1z4P92XmABYGgAAAACDsQETGfwYx4dxq9yLlsV3EG62Hw28NX+0JAQAAAAAAAO6QS6IRlqpQNbCPo3jYvh4IMhEaOxyYnuwzaeuAXBNRGzXsWz1bARrbZ+Zn AAAA1rgew2B1HfndABYOYAAAACCnZK1c/5qHBpn1CFb/Ms1mtEx7jgsPZQUxAAAAAAAAAL4EpUhK6O04jBkLxnbhVi6PAuJZaQVscg9HZglb7o4ohR/9Wzg+ARqrbbre AAAA203OQQZsu26SABYWQADg/z/YhSCLJeX+Ly06iYHqwCjpTzqqqBkkJtQTAQAAAAAAAJ99n1NG8ZKu30eEVfFx9Hl0w9LzH4N0ab9jtk8/u6O60cQQXGlSARoyZV/d AAAA4CKvRBrPcX/HABYeIAAAACAo/4JlJkhVf3rvL9pWeFpL0Wg1HGsZilvxAAAAAAAAAASo0MRrIXRJZx838aOoZKUuOdMPpPZbflAFpMQdWZs9bGIiXMlCARqkvxOx AAAA5QVluk2AfW0cABYmAAAAACD/HX1J+PY9GG0mmtGJ8KTRexT3tciRnvs0AAAAAAAAANEFJTL/6nve9E2i1B3wU8FTvLWu2xn2GE9R2SMZXS3wHOI0XGtDARppK7hd AAAA6h3mrc5teECeABYt4AAAACDgAWOvyEChDxJrqF5flEu7GrGKngyArVFeAAAAAAAAAOZeIyud/MIv9ZmYTCC5H5W88Elg3VTx34RdARlTcQueWodGXAM1ARoUsWQq AAAA7zhnKDgYb8nvABY1wAAA/z/Ciq9H4ldNuGvC2vKhDjjVL3OK0C8A8gPpAAAAAAAAAL6kirvBq5n9SXwkIJcwqsfbJn5PLWyxl3ZWyR2it9KCAiVYXMImARrsiAXX AAAA9Jj9ogrH79/nABY9oAAAACDDlaL1QJiTF+9c8fW25toI9VliKYQoQ6xdERUAAAAAAC3ZsnEs4DQQlO6nsFXHKEKNSYdjW/js5Q3L07IL6xkVkFBqXAT8AB1oenW3 AAAA9Jj9qg2zkrbaABZFgAAAACCKb75zeQds94fhp3Z71l8f2QM2QZ305YA8kSkAAAAAAAPU1f6dnCgPlaQe8nsKCnMmBySD8c9nQJQ8bWC92whrS1NqXAABPxz6wY6s AAAA9Jj9yhliHipDABZNYAAAACCpg/YwTStI80CKl7ipyVMgX8rA/sHODEdOhi4AAAAAAMHPQwsN1MjAjtcaLQVNYZwKVvL9lxn6JPULG0PsdrOLElZqXEDADxwOq6jg AAAA9Jj+SkgcS/fnABZVQAAAACA78JFy3gBH66ikEr38xdW6W3zbq/ld+D1SzxwAAAAAAJVOuevfRCABta/vKFbkvGmEP9i9lbHZLJDegpINmBfW0FhqXBDwAxwM6FTv AAAA9JkASwMFAy54ABZdIAAAACAaL3/hLI8wJ3eiq6QBf27/jeTNPB4C3ycQqCoAAAAAANF3xLFtuZHzVZ1CfeLKfEsAsuOUnVQdqx/VkqKqPJuyoVtqXAT8ABw9QuGt AAAA9JkITe6n4BCeABZlAAAAACBZEtw4C1H3hEUC3BhiM1iUNfXo571lHdd3pRUAAAAAAIpg9DgQPiwpPmirt/fmbr0aqrNHMR08LwsQOM3ASjNqPV9qXAABPxs+Uum1 AAAA9JkoWZ0zU7DTABZs4AAAACAp/Mmilk8Z6AgpujYzr5XrekmsfIHGvHcY7zoAAAAAAJcqK5/e9aeizamig7gz91CAwq7pBULYyeWxFQlkhvXjE2JqXEDADxuVDxhg AAAA9JmoiFdhIjGoABZ0wAAAACDzpxrVX1NBRiMIcTJkJdupwg4waMrdQtVCzQwAAAAAAOKF00aLWALocqskVcPpy80R7TDUp6lQJtpLuVXb60ctgmtqXBDwAxsXdM+f AAAA9JupQ0AYXDzbABZ8oAAgaTY5ob9IKKvyJ7Y51ATCwX3A1cTzZjhSLitlogAAAAAAAKbR1f1TTT+yNxiYfBabol6SXijjcDhVLRSBbXB3cgB6PIVqXAT8ABsc+NEe AAAA9KOsLuL1RGmnABaEgAAAACCNRCuQSsIWbAfvHdL+tS0uTHXa2JSjeVEGLAAAAAAAAG7yWJ9wS8OwGFRpE0CAHXgEDZPTkC2+vIagEc4aYjuMZsJqXAABPxq7KsQp AAAA9MO33W5o5RzXABaMYAAAACADzvybFmMtRvB7aJ9+zpTkWbVTbhhYhYdaFwAAAAAAAEJbyLuA9wB5zdxmmHVDD1RFyGz2mAqQUmtzMQqYhX+2XG1rXEDADxoPf0g4 AAAA9UPml5w3Z+mZABaUQACg7XvB0XBL3upyKq2BgPpIiNP53dHbyoHMcUpSBwAAAAAAAEYscVotjjIekjPdaNUEtI9yYqcD8qT0+5ebGl0jdluXD6FtXBDwAxoA2y0d AAAA90B2RoSyBYM8ABacIAAAACCtdUF8gCaZlEoMo+z/ab/2HROZTdIFFDJZAQAAAAAAAGBOkoOIuJeB8BT3xXYJUS8B/UG8fndBxpw2pfdvRlQ9ecZ0XBmGARq5zwGe AAAA/BFBSdZRB1gCABakAAAAACDDcPKuoKffp+I7JIoX7DKzZzXKjUzkGidsAQAAAAAAAEXsmh9JTHBEyOwlWe04CIDLXkB4wtzHCKrLolBI+VkvsaiAXCH7ABpZutzb AAABAVGnHV/0rANDABar4AAA/z+oL9R/Zfx01f+UfHGZHJzEJT73zQHmLdanAAAAAAAAAAfpaHl3FzUmKF3vzcyS9X3v6gvStSNnEfBFZc9E2vRUPUGXXGozARpBIo6E AAABBpydjL1XFS7BABazwAAAACDDGJpvP5aeAFTZ9EiuC2RdNcGR5A0XktRSAAAAAAAAAGOWuxEh43N1LYtuzRoZC4diwGluoy+uyl8lqRoSyNdCJzioXIQaARpk8IVR AAABC9NdzWJ+r1EyABa7oAAAACCtEhJY//0eyqQdEwshEekjl3PizN25B/fyB1dvAAAAAEkLPGmJN1fWIwXIxSP9VxlqlKuDuC8G8l5itNsv39bfdoS6XMr9AB2RtFyT AAABC9Nd1VcVdKLTABbDgAAAACC0q8JFPBOSUo9Og0P8BizNly4iwrA9w8mN5hsCAAAAAP+hVB1fy1CYQ3NIOYSIp0cQYQSjDkliFRg+MBej451qwYm6XIByPxxTC23b AAABC9Nd9SlwigD1ABbLYAAAACDFupZklKqRXby+hfirIUD3Nqehp3Cfw/fqo5wBAAAAAF4KSZRp+0AaObZuJ4J9eJQ3Rqo05irSFigS5/+NNniQHZC6XKDcDxwLp0Eh AAABC9NedHLc34FeABbTQAAAACAJUjOVcXIkM5efIagIeUP3GAEtu35i4vtSR24AAAAAAGF5v7b51cIsXIMRGFaCUGrBQ9BWxbRO9hN7wgjzTrg02pa6XCj3Axx98PSI AAABC9NgcZiONZLBABbbIAAAACCh/tiuXBT5gizkwH5whYmIcXA2fzBVCSphcYQDAAAAAJ7vULL178GQrHeduxmVte4nOJOSUM+cC+vAIZdHWWEIbp26XMr9ABw6wYlk AAABC9NoZi9TjeAtABbjAAAAACBj1yMgB0edxX6HM6/T3oLeKm/YIRuSfS1NYAQAAAAAAKjzCvlWGdTve4ip9s5XVIGQX981fpLJkp9m4AfwQ0wS5Ki6XIByPxuIOwm0 AAABC9OIOIpo7x2/ABbq4AAAACCYsRsV/rXHLMrzs5lRWjcYoueUu9yoZVbkozQAAAAAAEaF+Dl7YAg0By+h0LDwhTHH60EYqQh3TJPJnk6T+wGAaLe6XKDcDxsjxccC AAABC9QHgfa+dCunABbywAAAACDmf3ikVZtqCUrghYVudDOmNS1tDLZd/8KyPwQAAAAAADgBJ4NgqfRNQPq5cVemNbtNr3BGjjvJJHcjvn3akFJy2Mu6XCj3Axth6G82 AAABC9YEp6gUiHrkABb6oAAAACB1+yfClu9oCffzn1sTPT8d3fzBNGG2NYdNDQIAAAAAAHOgX9n3U7KqvP+1D8rFbbw0kCn2P2W3fktcot1212eUBuW6XMr9ABtAEZmR AAABC935Pm1s2bfbABcCgAAAACBfgvZB0xSaWcSZtS4EO0Eg05utphH6wU0/TwAAAAAAAMYqVLAg4EplmmMk7BmndQhBjFl8E9TJardsK51kHyFSYSG7XIByPxrCzaVn AAABC/3LmYLOHsNVABcKYAAAACDpdxuHCZFe2jAHx5qjrWm1+ERq9KwjXUkKHgAAAAAAAGn4MpX4xxlHfatZ5blXYokCpalRBsP32dY540LOS15dzBa8XKDcDxr1VkAm AAABDHzEU3Lb1AIiABcSQACA/y8yLTfpGG5PRvxlFsynWLKrnyOPhcqsCHolAgAAAAAAADrMbZp5BfoRMEZgvc03o54C+ZApPEWjO0tEzQ7gJ4O3yk3AXCj3AxoJ1XkQ AAABDlW+eSCx60IUABcaIAAA/z8I1h/PUyoEQ2TwZIpBpVu6QF1aoL9vQV2EAgAAAAAAAKT7FmTQCuREjb34+Z8aePfFu4A2/WnW80rtXuYjhvZcHRLMXPiGAhrnxSsX AAABEQDFdRo6fgZcABciAAAA/z/knC6s/W//yCNXGLAom3xomrvRuOgqGun+AAAAAAAAACMkj1fbMlkz1auPMGodJWihdO6CYAQYQWrnhDj3NSNdc/jbXBotAhonfG0W ================================================ FILE: core/src/main/resources/wallet/checkpoints.txt ================================================ TXT CHECKPOINTS 1 0 334 AAAAAAAAB+EH4QfhAAAH4AEAAABjl7tqvU/FIcDT9gcbVlA4nwtFUbxAtOawZzBpAAAAAKzkcK7NqciBjI/ldojNKncrWleVSgDfBCCn3VRrbSxXaw5/Sf//AB0z8Bkv AAAAAAAAD8EPwQ/BAAAPwAEAAADfP83Sx8MZ9RsrnZCvqzAwqB2Ma+ZesNAJrTfwAAAAACwESaNKhvRgz6WuE7UFdFk1xwzfRY/OIdIOPzX5yaAdjnWUSf//AB0GrNq5 AAAAAAAAF6EXoRehAAAXoAEAAADonWzAaUAKd30XT3NnHKobZMnLOuHdzm/xtehsAAAAAD8cUJA6NBIHHcqPHLc4IrfHw+6mjCGu3e+wRO81EvpnMVqrSf//AB1ffy8G AAAAAAAAH4EfgR+BAAAfgAEAAAAcYL1NItllvrX81+LuAq6qIdoXrrUiBRLemDJWAAAAAKut4Vhu9v71myuelA4ZqO3kP4eTuqb+uLQE8+CxjfkhsuLCSf//AB0pFg7j AAAAAAAAJ2EnYSdhAAAnYAEAAABGqWHkclp5E4ehRawBs45b5x4XYaqgtDMoSwqbAAAAALTV1vKUrdjXiTPqPUgmGGmaDGPvVaSoLzWx8iK3xMoSZ3zaSf//AB06PHGe AAAAAAAAL0EvQS9BAAAvQAEAAACEBXV8WKSX5CaMJjF7nEwm317Fjsj99uhjc4r6AAAAABvhLcOGl2UGckHYybNe0um9fV26bfiZMFaB2f8rNxSAOgrySf//AB0nOH0B AAAAAAAANyE3ITchAAA3IAEAAACSILqxEicC8TnZGcfxX9/p8FckbxdcatdcW8WKAAAAAEvnJo5A8+Y0mjJYV7kKVLj6Ul/9j/ODgf0w6gefxnUm+eoISv//AB0Usifw AAAAAAAAPwE/AT8BAAA/AAEAAACEwGnx0ildnoyPILwYNY1jLUALLFQP8DFmeQx/AAAAAOiv4/rCGnLDfU5qDhbuLSwqU96lkyYJaTm8IJmezWdu/eshSv//AB0sAszi AAAAAAAARuFG4UbhAABG4AEAAAD9pdvx0xs07by26iadZjBdkL97+LVOnWwpYrD/AAAAAK28IuIFDZQEvOwRV/f5ojcFS/6weEx11ir8PLW1uu5XhT5FSv//AB25x8ke AAAAAAAATsFOwU7BAABOwAEAAAAvgrh2cIRfqt3j/t0Nv1BA22K6KyXCPiyECMF0AAAAAO1z31AjyOj0d/uWX+TDy/5e40t9i1bD76Pz+cCydckTGFJqSv//AB3ZU5cE AAAAAAAAVqFWoVahAABWoAEAAAAin9TRvZzHe1UspvNuSaHkjGdwPF2/P6eEL9xnAAAAAGZsu6+oXMrRFQzwfmOzChP3du3xlBraNR1IelOBukZvsg+hSv//AB0GJxxm AAAAAAAAXoFegV6BAABegAEAAADMWpXIGkCsCDeXLbyq2mvPvJex7GJCYsBHb/DbAAAAAM5RkurS0BrKra+/IAxkYqN4q0lMod9qZ/oqjbLLUGH3DHPFSv//AB3B8ocA AAAAAAAAZmFmYWZhAABmYAEAAACsBE+FLo8osvJgV3Nhn8XcKfQicTbvieww/mCSAAAAAH+sbigSSrJl6CmsC6CyPQhLaDKza0QbkIv5vZli/8QtlFfsSv//AB3Nm/kq AAAAAAAAbkFuQW5BAABuQAEAAAALyHOUlLDHxaV1oJszrx5E/W6vcQZOjIkAp/H5AAAAADdPJYtimG1A8VllMhUVZ+MISrORF352dqsEbm4rfNWUy0kQS///AB3cT9YE AAAAAAAAdiF2IXYhAAB2IAEAAADmv3/X93kKY3hvqoeNDcf9jy/zZXMuRYYsZgdRAAAAAHANNC9lx7aDTf+2FTWKGJcBbwRIkTNyGQy+PSektTNVsVErS///AB2/sCUZ AAAAAAAAfgGs1C3SAAB+AAEAAABLA2DYNKMw7Hgz4w4fUj7gWgeTNh4ppzQhlk+YAAAAACe2SgIK8pTpA/7tk3aHBTNqIAkGEqBD9Hr0YqL15bVk+O46S2rYAB3TpDcH AAAAAAAAh1KPIu8gAACF4AEAAADOUzZxCACLCrZb56MIxIoEIWeq6nsqRTl+BDZeAAAAAED71QTWHLGhEIJrymjIHUFzaQjcvbP2UVS/r5ySz1DZxapLSyjEAB3+ZlwF AAAAAAAAkZmkySCQAACNwAEAAABYktUu5mb9Udk13yfunLPLcaMDAsC+/3mVeI0SAAAAABI/8VBwhXPOzYzAuO8REO+I8QPR9GREyfye8xg/SeKFr5ddS3G+AB3Rki4H AAAAAAAAnDAd9NKmAACVoAEAAACFilxtRYgzqoP3t+VtccYEy3EWXruBBLgvZN6NAAAAAOQIwRAptf27kuoO6436E4/6Oszg9p197r6xQAyFBC4Bcj9rS8OMAB0JvYvV AAAAAAAAqoNHCwIiAACdgAEAAAAaIxCXtqtiecgPJGdKLI7luahI4dRXFa2JtjWBAAAAAKgiuv5u2GAOP/zm1h0Q3xkn6v6bv2d8tExNIJ8UPGuo24x4S1dGZRzOIiEY AAAAAAAAvmyHarHKAAClYAEAAADnCfys/hFGQgTkzB2vSntj33KnQqWfTz7vloQwAAAAAABWa9XPFhrodgII7+bi7V7ZKY4LdK32GsS2u4/OLRurIOaES+WzQxxj7JoP AAAAAAAA3DRAqLy1AACtQAEAAADVXhtGjCJ5iXEnIDfWzAT9rHORPAAS0NdjDC4aAAAAAOq7qNFZRow8B/dQ23du37fM4u2Iq0TzBUvkToq3/g+geU+US29/OBxybZMS AAAAAAAA/+MWO9HLAAC1IAEAAAB0sfE3ZrOH9/bpE7QmobN2hQGrURvpzWzCCgwuAAAAAMHzQY7OI3TA/mkBu2E506clAApINgYlj7dWP5YemTVAoKOmS3UTOBwat3W4 AAAAAAABI9gdtV3rAAC9AAEAAABVUxeUXAvmF4DxYzNSXjTs3IfwlYe24l/4JjIQAAAAAILT2J+G8f74uNmyAW9JnWDFnSCl8rRYYeO/CGV2cQT5an60SxURKhzFK+US AAAAAAABU8Zb6gtBAADE4AEAAABzkQWrX5ewFL+e88S/O5SY51fz1vhH3tRv9uYaAAAAAJTaTzK11MIXSRfGlZ6U4TmS6ig5c5ursTao8hscqvB1UtzCS6e8IByBlYEF AAAAAAABkV70dMN4AADMwAEAAABzQ8F6sjmLrNKzryUaZi7JdLbAO010YgFpIj8KAAAAAP1LgY2K8OcupHdUOpaCsyr9fUIXlpzbdBt0avYxCqcvtHPPS29UFhwF+4oC AAAAAAAB66jAzOGJAADUoAEAAAChpKuAo68wSmzShRxQOiDBWPhqRJdaUTxsQ9wGAAAAAM4DdGfpuItjzlXnWMf7MSwgVK8RgKssPtXqwdXN88sT6OzfS1PsExwAKkUF AAAAAAACUNgZQS8CAADcgAEAAABGy7ccvE9OZlfB8ooqhI7/kAfljEnM/4AW/kMJAAAAAMW9Vt6y4cOodVR1e6CC+h8KszLNFJOJ1/MS2NVV9c3wI/LzSyScFRyE42MA AAAAAAACric7YGodAADkYAEAAADpBN+TLkJyTO13QgCm1Seg1aF+BfvBdkMUskcIAAAAAGLtcb3OjIaURBNKOaF4p0mBbLm7wNHFqhwx4c5GMsJKSB0BTFxnDxxWg7AB AAAAAAADMQiRgEj1AADsQAEAAACTTCvVpFYYC0BDQaOA0g9R0IYrODEd602VBUUJAAAAACmaFwLknPabw9Cm7uJ1EMw8ylpCfh0ACyzK+QcRaq9IIsYSTGS6DhxUI8IE AAAAAAADuew03p6EAAD0IAEAAAA6WlncsAaw4UiyqiobivpJ8SzBiun3s+wsJwkBAAAAADRA95fGOcOU0tS3CRxB1QIXkXUSwHLdWnY8eXTGzKBGLk8jTEIxDRyYDQAB AAAAAAAEUsD4KRv9AAD8AAEAAAB3mRBUPOiZ5hS9pWBQ/9aiGOv/JQZ5m+MPquYJAAAAAE547xZxUzivsp41YiAUODrIlX/mG1ZpjCF6FO7PapRSmI0yTJPkChyl2HIE AAAAAAAFC+ocnh7zAAED4AEAAABOjlzzxOS49jqc+IvrLbq6GUkYIQGuTlz1StEAAAAAAJ8qI0ToESsNe9gIlBQQbuXxe7bNZAeIg+G2YfolGqxr7R08TPSjBRxNzSsC AAAAAAAGcd18PyutAAELwAEAAAD7V8cczSEbPeTMwuI7UKfNtyqrkeYHN7Oiv98DAAAAAIioitnfaJJeiA5dUrflDO8iWHHGi0CizQvKEITNQ2A384hATP1oARyusfgB AAAAAAAMB8kDIyyqAAEToAEAAADD6WaCS7mIy8rpGowIPm8ADfcBKSRt1oHRacEAAAAAAIXKGhu6Zhlug2n/7LzalLkMyfbryxZSk3HmsnB0OWijnkdOTFoMARzC4UAe AAAAAAATi2pVCXgAAAEbgAEAAABuuUVfO7THPxPwWyd0NoSO21akwS9qnUPR9wkAAAAAAEiIfxRoBMCVZJ3RAgadDm8U2Emkwiaa+C718mk4wDOmGxVbTBi6AByPS6Mb AAAAAAAeYVnHFS1UAAEjYAEAAABPNXbFSDLkdMOHdA9poStLbQ+1SDWX1/qouBEAAAAAAOQ4wJ3d+Vwy2ler4Em9KqKYX13FSuZO5Uwrn8MuTnvKT8tnTA6AABwRqw5P AAAAAAAuIBCUxPZNAAErQAEAAAAMLHyqT8JOZ3uNrJxMIRGYEYT3nF1HzLZW6HcAAAAAADzTVc2lDiLbmEP5ncM8q1TEw0P1d1kZkZqI0qOstTWCE/V2TJggaRtSQENM AAAAAABBTaleRTLoAAEzIAEAAAAlZmyAjIigoxs18ypRBJig75D2ZTq/LE9Hu0wAAAAAAAxuq8lWaFwsEaZxcX1OTJA15F0DjrkhHxnywMPxRSpvcRmHTObtWxuucMl/ AAAAAABXPIQiUz9iAAE7AAEAAAAOhg3mXDWpTS4zW+fXmqu2493zkY5tZcYeWyMAAAAAAONqvCEnIp06lK4OIGegp1yrYWKdWy8BuSffQ7bAAloIl2+VTO1mRxv7EbsD AAAAAABzehv9C0l3AAFC4AEAAACT7yN15axxWGpjC1n+47CGMLk9SeWolSw6FSoAAAAAABA+E4IjPusGLerk7HrCivj8acYXn2aCEu372jArVFnn5EiiTKOyMRsHDdHI AAAAAACcCwa7161CAAFKwAEAAAAYu2Hy5/kJBwLEG10x8fl0i04a+YyQhOBQlQ4AAAAAAArXQ6PkzrrBgvhKUkTtzFqIxoUXiyHff/hsoJdImTapifOzTJ2OLxvHJJZv AAAAAADGcizpOqWNAAFSoAEAAACN8pQ3PhGo4sERrtXTpnOb0/zTY7SFu/P+LCwAAAAAAGOKxXywyaEdnE6m2j/R329FjWcT7Ik00IteEtdUebhr68u/TMp+HhusHmHS AAAAAAEIkam6YfY0AAFagAEAAADh30gW11VBqbrSQNM6ukmK6ZeqdJbifnUrqQ8AAAAAACDHJEysJCNUsN8EbEVwz6SQmJoqti5smpttkZ65j6jzJ6PMTGMyFRuA3aKW AAAAAAFnsxsL7bFwAAFiYAEAAAAc4v5GJY2z3zyrtqhu7k4ARQ3QE468HWBxBQMAAAAAALGcBXeoG0iIxmCRWbmxxJuqTw2MbxO7yaWFqtPGnA7vqD7ZTFZyDhuzuSCd AAAAAAHzSIpICAQtAAFqQAEAAABumKkpH8OkEjWXUUwyG9yx0g+ygjDmQu+ttgsAAAAAANaKSEb9lDLzLNfwpprjRq0Uvin0MuE2pITyu95WXMzbEnTlTCqLCRuVWmqp AAAAAALGiuyDfLOtAAFyIAEAAAAvM57vt+IeRLFZmxK3SNIwrK2425iKeJRQdgAAAAAAAP7NnzKGhchO/AA4XJp1+Iz4RdKW5I2Ta00HmYMK0VYfYyj1TNIcCBsMkObi AAAAAAO/G/yGVhusAAF6AAEAAACEIIeZFQWvEpVVTNBkT288n3B1KfgllqqgMQQAAAAAACiqr64Z2XrwiNMiuoDw3A8CUEq+Gh8m52ZSwRBwb40OElYBTVNZBRsC1GJ2 AAAAAAU4Cq0i+3IeAAGB4AEAAAD56JBkoYAvTbuvRNqKp7TLA01rjlf4uFC4kgIAAAAAAJNpGsZYATB7pFLumTP7IhNBHlmmwGrXksjxewgYcrfRG/MQTUyGBBvh1Pog AAAAAAb1olFOzFrgAAGJwAEAAADd91CQvr4E/QC9XVSUWn53X/IaASN04oT+WgIAAAAAAHpxEA2jK0VPFeGGO23aFIyDD5LA6ZgGwQ9prGOS6jy5M1ohTcsEBBsk2gT4 AAAAAAjrUYnT6V5sAAGRoAEAAABlHWfOi38ecTZB4glpgP105XtDQCquzJw4DgAAAAAAABST87fUh282dXEyCJyhQITFUHXXxa4fx7UMfHzLTIMSf64xTe6NAxvI2xyO AAAAAAsijirlaxLgAAGZgAEAAAAOpFxTktzl/dOPLErGEEBuLs8ml2TBa78QBQMAAAAAAB+2TsOI4Y1317Y5+F63s2rL6djntagB7QZyZUVj2ARuyylBTSn6AhuqgXZp AAAAAA3Hw+JhyZf6AAGhYAEAAAAi6PTZIB2swuWyr3tAauRsnHc1yE/xV4BkggAAAAAAADsWVAiBrukpas4xPzUaJeMtomg9TH+XAOeTxuaSr8HYQMxQTVKFAhs5IzHm AAAAABDnrS48ncEpAAGpQAEAAABOJQECIjzF1KnyO9RZ8dmmcQQBWSmy1syK8wAAAAAAAF5sBpLOm6f8Htf6nHUv6vCeEywzNMm6cF7aLHM+VdbHiABeTSbMARu5m1XJ AAAAABVJjeXJZ4UuAAGxIAEAAADu9Ez5w+BKEGyy3xS/gojNg7hei2czMafXTgAAAAAAABFxY2+JKb1awlsDJVEzV/ZNb72GbemXrE1P6F7mU9wqeCBqTc0tARvDXXY/ AAAAABv37IPNI3JzAAG5AAEAAACWH5gseRQiSptSk/gQzI4C1FcXmmTP9D+7NwAAAAAAAOWKot7bLs8JHrB3lAe+ZHerymH/PYljo7c8vtDmbd8qA5x3TTHcABvvW21U AAAAACUfql1QEymxAAHA4AEAAAAv8ohrUL/oduWhcjQhj2TAnR4Yf/5ZZfQIgAAAAAAAAElAgSvR2+lo/Oed1Io1FCCTJWyMZrjt+lhRLfLvvEQccQCMTTnzABsay2jS AAAAAC1pxu0ZLTeuAAHIwAEAAADI2DEKu2nCXMXNN1rJTISIwPY7cdtEJAwdaQAAAAAAAO4A1DU45uhYzf/CTTZf+ophrOJVgxxfedWYV9th+niwFXebTb3LABt8jSBp AAAAADdPD9bcmy/0AAHQoAEAAAB19iO31C1znhGXB2FJl8JChF9CSsb8CjsVcQAAAAAAAEKihOUu5fWUpyoyUnLOrWnULP+0/Ajx71vtFMyNb5xtEO2rTay1ABvUDtpP AAAAAEJoI+HXgBG2AAHYgAEAAAA9A+9n6SMQ8fEWH89uNjG80lqT5eQitayEowAAAAAAAHCWFzCW5z2xc8SyGudru79lXrtb2WYukdpyEUTFTurajHm7TfqYABtYmLhU AAAAAE+Wj98/9B88AAHgYAEAAACXSksZfv5mthMtrzxYxrcfEpISkD7UkyBTLgAAAAAAAMd5iTUg08WipCJhx8CS8T5hurf7gPrOAG8H7U/2rVtz5FnITbOTahrmxhR4 AAAAAGKCW50/mAYZAAHoQAEAAABNFN3lnjqvgymDGuYYf6pGUSXrx927xDXkMwAAAAAAAPKsZcG8icpZYbkeJtcPV5U8uAvDD09roLWB3nEWSIEhf0LUTfK5RBoalF1a AAAAAH/as/mBH3ZiAAHwIAEAAAATL2aG9zIfGmonitIhGPqRP3E+Dl/UC0v/KAAAAAAAAFB4Cs4JOIIEgOcbrugep4UTC8gp2hRS/bWJzFgWLwpm9J7eTSGUJhpFQ3Lb AAAAALQefiGP+OX+AAH4AAEAAADVoF/7eWfud4Z4fPa0j+ZNrwemFuEYU8f5HgAAAAAAAHmhgeO1687EXKlOfzCSyKj3L34b3Gt415bo3my4eJenIcfsTS+THRohHwoF AAAAAPhNq5KCvJBVAAH/4AEAAAAjYupMfkApQ+F7WuwrHqo61qpHgstMaZhuDgAAAAAAAJjY1qtpNn506DxHOfKO2yv3KZQ14pMjv2NeVvYXsSe9brj4TYUhExoLyjiL AAAAAWG2X3vudTCOAAIHwAEAAAC069TF2G2EdSVeCC24PEyE0aYjJUtXUyEKAAAAAAAAAHsOGnYqwwLFqEwJl4Rlz5ejh9cc+cPOTWZfXVncIjDa03gEThIqDBoCuZRI AAAAAgd0Jpp8J4p3AAIPoAEAAAA96H6SIrmh4lozEuOYB/Ocl4iDKvsfGyypAgAAAAAAAFtPgBPFjrDD9Abvtt7rIfpF/ZCmXtK/hiyElBCjLnAsIscUTs+7ChraDJcW AAAAAsNIQ2KTHCwRAAIXgAEAAACm2s1lZeANbPnchswuTipn5yI5c84iFEvVBAAAAAAAAJ2hK4JwdDtinLnXvrKRr/4jA/ow0xRerF5EztB/g36WndklTgTsCRql2r6K AAAAA4568Asa4obxAAIfYAEAAAAtJnC0MPYlp2pgsxXHCws0Qq9nv8uWrpHdCQAAAAAAACfx8m614tr8PBSy8Kext/FHqJjZJvH7VGT5PCP0UE80p182TuXhCBosiyag AAAABHFw4cEr+kswAAInQAEAAAAKXYjM0MVrm75MhKyuclCi1NxbqS9SeD3TBwAAAAAAACySAw5igb5Xvsd2sITcMW/r/LJIerluxxcIrzZTGVXpdq9JToZKCRod2gnt AAAABUprw7t3ffo+AAIvIAEAAADSRgUSvXLJuiZdImMAcIa1kIP2KBLd9VfdAwAAAAAAAObyH80swCvzJBXItN3SAM8ogjQDawFZivOgF6TUzi4x13FcTuNvCRoWqGlT AAAABiALqBnpgDLzAAI3AAEAAAAEdlPrXTB9zxKW5rFDpb0f9ZLDR4uHyt5rBwAAAAAAAMiW3XWBljdNYF3OLD6ME/g0Sawo1F/HNFUW11FluM+xDCNvTqWOCRqHj8OE AAAABvL7X0R+mhkXAAI+4AEAAAAwPla4OPbkCMpZTRG/HHYNIWL+nY3htxvrCAAAAAAAAPy77i4U+Lzixlo7w3Utr1lkWBDW2i83x8zth5Ch/JARaFKCTl3uCRoOcCKg AAAAB733nt1u769iAAJGwAEAAAAJX4QVGRlNwJoij6ar/fHoUXx91+SF2NiwAQAAAAAAABJbmEq55/zj1QA2CMl2Y5+RccWFa+eKzO/4dAGyq2RmA5SXTkttCxoExfRL AAAACG5gfCYS94FlAAJOoAEAAAAwfCgqsDrim0wjAIl5O/zJcrCaX21xOqobCAAAAAAAAOD0xYkK2PKCePrC5H16jhMbyyvoZYVMuEQHvKYmiom2FhmuTsrwDRoWLVAc AAAACP79cJpvSRE8AAJWgAEAAABXRhwPRJeSHLmbYJMdpuk9XbbUsLkkAo24BQAAAAAAANzBngmWFfbH3uV5Ibn+h/YmnIZttuByKXOAYZNz2qA9irvATpoRDhoDxfA8 AAAACY5HuhbVBVwFAAJeYAEAAAAf79lak+p5Wjc/JGFumn0LZTqkP0BwaGp9AgAAAAAAAPXPYczT7AW2lY4zD1T7vUC0MVSV8xf9J2MTzoLTxmY/JOrUTrFhDxpRYTHJ AAAAChFZgmZAeLs+AAJmQAEAAACUJ/cMj+wJKwxiyMUhln9V9gLz5IYLSaMQCAAAAAAAAB2r9e2ixMAVCFmJswQQWEWnpRiVgr4pAjAERg1xlQhnK1nmTmiGDhpKDsil AAAACpwlFy5EWSkdAAJuIAEAAABg7Z7T1Yqlqsyzt3YVJX5x3p+Q6tCovDM4BwAAAAAAANFu2klbATzndp0N9uY2vlDa6LPMt+TLFqlJQWHv7t3YTcD4Trp2DhrnmPoE AAAACyeIcihEzkv/AAJ2AAEAAADEg4HEOx0uvThscJcSiapp6XT/KB/t0n8bAwAAAAAAAPzsAUUCW4rIEbSG/JHwf1o5ohcMLu4QZiON2kVFr3C2yN8JT9dpDRo1CZm1 AAAAC73VXDIumXtpAAJ94AEAAACmS77xVScJJnHVjObyMbVAHYGDKTi6vDgHCwAAAAAAAOlTxivTggIx0puQOlthPcJsCIx1jzjl02blLi/e3xVkmIgbTz/UDBrwMAUO AAAADFr7Cx3r9hwYAAKFwAEAAACkmkpjgOn7AT64NAk6wxR2wJekP179y2QGAAAAAAAAAP0D8cC8X6ruMAajeJmA4tp7e7VsKB/M2VkpV04hazk2SQktTwspDBpqP8jm AAAADQDD92shJKLCAAKNoAEAAADEVRQMfQ6UxE2SR4wqglkM5HCaKDnfILUeAgAAAAAAAIDF6zgqmMVXOTTQlUNF41Jz7gJAJmgWAER/CytVTkiBX4o/T5wwDBqqr1mz AAAADaYn4I9tHzJ9AAKVgAEAAACW1DNxxihcn0Squo4M5sWH6A8lvBEbxOB1AgAAAAAAAM3n58PZtu3N0n69nzHusMHcYCtwflEcvElqVAIwzF0vAIRQTww1CxoGNrB5 AAAADloKPAUfc8b1AAKdYAEAAADQvnrUsCSWVD2f1Pr4ThpOj37uDO6+/bB/CQAAAAAAAM4luTI2jCl6KmxqQgCcPh8KqmfiVME5nP97OfQI4IauUvViT4cyCxonLq60 AAAADw4XATBzG8RtAAKlQAEAAAD1mgn8pFdHzWlsL+p4+TTld1I2NJ3hmhk/BQAAAAAAAF02L/fQ0eSSvHWa8uw4/tcYhScUjm8xpNa71eNTLgszG/ZzT35QChpQiMH5 AAAAD9GKm05qR8mfAAKtIAEAAAD2Mh/cMirqRe0nn9Iq1leTS9KCFva057IXAgAAAAAAAHfoU9TLw51Oniu2LYCbHyq0JcA3+7Y0D/RPDpIK1EXeIf2GT+OhChpQgJaI AAAAEI8lntC9UcgKAAK1AAEAAABZI0tpHBTemiDAT8aDGaaM9JMjcNSsdx2cAwAAAAAAAHXvjFwT3zqD3uCFgRyZucyBn4+FjLpKUl0euFNzZfNYMk2aT/ceCxr60a6O AAAAEURwkTBm0hBWAAK84AEAAACw0l+csvrkIyh0w5uQUifOvi9sDzN4DD/lCgAAAAAAAIsgmncZxVK6S4NyzNFMUetb9Fi18GyH0Zk9YlP5pacwQV6qTwKuCRpdiCRi AAAAEhSzp2ra1Su2AALEwAEAAAC4B8LeyLc19xu6Exlvadwm0sdeqDGGK9e0BAAAAAAAAJBhxxlqAJuWFrDLwak+cMYzThvW7+J5CIUKsDTGWf7ylXm+T1+LChoiXXen AAAAEtPkexWmwmgDAALMoAEAAAAOVsA9yv1hZqT0M/wRg93KXSEvffu/NWfgBgAAAAAAAHuzpeREEX+2b6wH0wGNPrt8qLDKEavOcaQOrDQ+CtMQhQnRT9aYChrRBrot AAAAE5Ikrv8DV4yUAALUgAEAAABgqLa1IT1k1FXzrrDIMLzqOLP/OEoNEcBfBQAAAAAAAKcijpw/NlTmWMDTDHopVig40nBlHhfaZb+mpe4+Az4IRPfhT4q3CRpgkXRL AAAAFGGeB1erGaGZAALcYAEAAADoP3M4pxcBIDcMFEoOH4bEhEDBBuBNy2kbCAAAAAAAAFNVavLdhzXOWO+njASenf6QnHLa761w7Y0m669TbsVo8Sr0TzGUCRrNjfIK AAAAFTQWW0ADOWr9AALkQAEAAAAa5Cec2+NWDxvJVbhp2TO6impbC9TycAeSCQAAAAAAAJwfJZ2A9SLpm7f2CwGKjZbc4TA/3oFGB/udJk7KUA08zn0FUC79CBopvPmW AAAAFhRfPDePwG9uAALsIAEAAABB6bFBFgVuPvzw/1n0Ud7MckylhKa3HN2RBwAAAAAAAK1J//wVA6Cm1OtFEXe1ixsLmnO2OfqJzKF//X4x92IlWW8WUMk8CBorBXkf AAAAFwkeDBwPjXffAAL0AAEAAABJtKkDwmds89DuFLLZf0tTKM+1mKWhdKSdAQAAAAAAAKmblvkfiTWWyaVnF2BTlnbhBIieMMWhEzm2d1ncX8UK95onUF6oBxpA6qlO AAAAGBBlsJgRRBm8AAL74AEAAACryGtgxuPGIrHtB53M5GUrd/kwx7qI6DNDBwAAAAAAAFzIff2abwkijY+AnPqk7dMjm9ZPSqbfo4jp0SU2VwCtfy44UL7fBhrISt0+ AAAAGTWxEEhUcQSWAAMDwAEAAAAgd13snTsmAw6NVYeXwd7zePNFxBWRZoBwBgAAAAAAAF9p3DIalr/1m5D5HHwIioy0Mp6vqQvXAc96m9ZQTcYtHedIUDg6BhoaJmL3 AAAAGnlueBNyjGqcAAMLoAEAAADxMAVyLCW6TmMToi8xd/5fHCSs9IP1C3YEAwAAAAAAADx5cYOZfXdScyl5oKbjMjjNawr0xA7EYHrK+JA9lZFNs0NaUIvbBRoA5+c0 AAAAG9GcmthZbdjQAAMTgAEAAACdb04J1XnJMBWoPpCB/ug6XIsbo8hlFrYfBAAAAAAAACU5kxe7XHxNrv6P4sTfrAzqfk6FkTzWZwMDdyQMrf6TpJBrUAh+BRqEKX33 AAAAHUCr5swt9qOtAAMbYAEAAAAOv2o9GRg/8y1XLWbrxHEtdKsre8il8scvAgAAAAAAABeUG1JXau20Seou1gCnOZlXswxzeP9ML1f0Pc1AZN7PCOZ9UO91BRpFpYNz AAAAHrHeyAV5YSc0AAMjQAEAAABAZmBk3O8vEZQmYHidW1GZ5iRfvDi6Q208AQAAAAAAANfNnMW7CBMDtOb30L0LvjerUrWi3VgqqQ4bziAHWSLiYxyPUMUTBRoLaE5N AAAAID7w/0q0w+tEAAMrIAEAAACEAu3/yPyJMhG/gP38DabMJ9/9EKi5dlFmAQAAAAAAAHvvIIvSBR3lDrxUytZSfsnG6fpXIKLJ1rmh1lUQKZ2IEjmhUOv6BBolhGb6 AAAAIdPAt1OPHJhgAAMzAAEAAAAL63cL6rTa61a2v+kslB1HkRAPoFYP+G7cAgAAAAAAAPgG0PwzKZZLrkwGcp6TuLOU1F3Wt4JkaXWywHxYCxtdJU6zUOrgBBqXcK0c AAAAI3D8CCdlC05fAAM64AEAAAATq9as/QRQZ7NbiW0sDnHluYjKSOxGVfqhAgAAAAAAANQbrFUPE8FvhfwF+W7JNRJHVh14heISWRWR7b5VfmSh9SPGUGL6BBqMTELu AAAAJQXwPTXDhACjAANCwAIAAAAxPP6YZ6jlPWT7RK6E2qfsj7lJ5hF8URf1AAAAAAAAABl/qnGZKjJp0IIx5S8o4aDy3kg4LNgKtRqq5z614rqxNATbUGuhBRomOKY5 AAAAJmwAf/q9P+cfAANKoAEAAACPblqJbabQH2TbrMtrZR8l+axIM5IqucqZAgAAAAAAAC09O54ycjSjp8dTzItlFq538Jt1bF0CvcEpFWOs4q/BovfrULEpBRpX6ilq AAAAJ/J3eKXIz1D3AANSgAIAAAABRZOAGGYwZlN50BoxbvPxBg86LPd3uQh1AgAAAAAAALzgN4eDziYqEw119Dzzwzurc5+pUwsJ+JDVgr6ARRJt1y4AUbGmBRp1B5Gy AAAAKVc6LSc1FRwRAANaYAIAAAAbH93FOSRPWG/NfO3rBAHvv2/MUpQiWVmSAQAAAAAAAKY7Csuyrndi76Fs/hyUdggrElc5MqdWbxukEnHoaooGquoQUTwfBRomGdg6 AAAAKuDYXV70w/Z4AANiQAEAAAB0fMxQfLC+i0WNqq+UwWj0ilVf2gmVqEyzAwAAAAAAABRg8vGFXXX8G+iq8htYsAT+yq4E/8aBucbP2mQfYiHXNnohUVyYBBojbQ3/ AAAALJecaPrZYKPwAANqIAEAAACzrQE86vYXPV1BiJTXT+NSxWzIsEoxnlV2AAAAAAAAAK2LAZvcSpmHGAjlUShbA7ZaLT3IfXRkE/gsz1XgLbsplOgwUUvXAxoIBach AAAALqSBJ3xxQCqZAANyAAIAAAA7RsEWj0exJINl8W5+sX/z0g7DzVIyBFg6AAAAAAAAAC4B+s05egCjPETbDmhebyCCcbBpdbT3u32O/8ay+YpJGotBUfp1AxqiOdh6 AAAAMOshluKShCFIAAN54AIAAAAzU7TKXCHZoH19IRxxk7aVgWVAAuZn96sbAQAAAAAAAPL2RBMH+xgPoT10EWBjnN1BWoWmBDPMkwMq5bpLx4sS2epOUW6BAhrKeVCM AAAANA/KxJsaP77MAAOBwAIAAABdlYLIwE/94xkZTJ0h0RYljq3MgLSP0nl1AgAAAAAAAD9CSF2KOy5Zk39VyYYdA1kJupx14DRVM/JLdZ6aCsr3mwxfUb4vAhqsfhrZ AAAAN6nk5ERfML0DAAOJoAIAAAAAtdqp/fl9ct41VWrFfomB/BW6gH/gb1R0AAAAAAAAADNrYZ0Eb5RiuhZPF7JFQBTvEDG3/WnZbaX8nL0zfFk3ltVuUZTeARobGMYL AAAAO+BasIUNWPGiAAORgAIAAACjLHUiLhYdG7zd5Ya2O01MT4kHDLgzWMW2AAAAAAAAAK5mqGpC6ZQ7ZnXyK0LXFF8BYbzSI3wGim3WOOFZb+C910l/UT2qARrazPpz AAAAQJs8dZBfNdApAAOZYAIAAAANsIJdf+Ui6vXdRtBYM9+/NS4SUweXL28yAQAAAAAAABsqQ5um7w9WRee0f7Vb7HNcdvueFJCBObOC9hVPuFhbS+qPUel/ARqqe+Tl AAAARdubuHv8LyGEAAOhQAIAAAAG0xnwlWIw3fYL/bp0yXP/ZCSRE1Z0hBBkAAAAAAAAAGfFE8jd5wD3Ca0oDZpjKeKdgNrievB4JELPLSNyKE6NYuigUWRhARoG14wH AAAAS5A5744UbeXgAAOpIAIAAABZRaZ2UpQ6X1IBwpWNV6Yxaay7pbHdBeEaAQAAAAAAAMQVh2dfNpqzbdKuLC0oArnPECnFYm3bzluL38N0NMKaTkqvUTcTARol+LMX AAAAUuOyUN2GANbtAAOxAAIAAABBCr6r8AfBJHlh0qzRMzk/r+qJrxnub7bZAAAAAAAAADfrE8EX9QmSiaQ6u9Whc/7wQEfbmAwMuExqGTDzucYUHjC+URXeABqMFDHt AAAAW/e2rO3bMUVEAAO44AIAAABXsaSPxeKYWXcGqFcGrESz7nUvDGdCbZURAAAAAAAAAPzO8HRUDoriCg9qFxl3JwrlqXrNON+E1NbYJ2d5pHIfje3OUU7JABroq+pP AAAAZfvBv1lJV2loAAPAwAIAAAAAATOj+83Z3UVyvRPuaE5JbdGPNpCpHpuVAAAAAAAAABEhIC2Z6rg2k3BxFgONOwCfOhA5to4O60uuuFykKaEX0/rdUSmkABoCIMgB AAAAckPqyEdtb+UEAAPIoAIAAABgqXEY9fKFjHPC/BwY23KMNf4kPkIea/iRAAAAAAAAAP598fQiaQ7fEf3dWvsPLTY040Kyx0sGDg9gbgI29y0yc3LtUWiJABr2oHhz AAAAgPBE6lomxjQnAAPQgAIAAAAmzJ9N+BjMCrhyPsO49PKhVuaftHPkRuFVAAAAAAAAAFG/m5KlGJnTl9fAp1tiV4HXoOk/8bTey9tBrIB//iE+y+D8UfLbchlwVoHM AAAAkn5c9io9/C99AAPYYAIAAADBZC/d504+FDryeR8KwyspNUcnYTIaGWpPAAAAAAAAAG1hClgMQacQYbiM42rd2qMyZcr1SUoe7bDVYbeXtIzY0nYKUjKHVBlIs5nQ AAAAqljeIvXX4fgVAAPgQAIAAABYIwxBtfqfi8hO52IzM3cCxgMoJxTnyCdTAAAAAAAAADh1tnsooz0WNQJmx8VhdQ+oRhYUvJMf4sdx18lRoEL9Z74YUldSQRk0RO1f AAAAyTb5Fqr47ML+AAPoIAIAAACX7rVSkZhpCkUal478RqfWOExWBYcrpuICAAAAAAAAACZt+4ZLsZe8wnMe31jhPrKk217aW2kZbfGAgDXUZRy+LLUmUpxnMRnQtVj1 AAAA8gbLBpqIXmztAAPwAAIAAAAEFLb5LL5O9l6rOGRcB7XZvziVEDD/8ysgAAAAAAAAADxSdp5+Gl5BFl6OSTmLOnqTASbVjBjHsHOw7YKx0ppLyPQ0UiIiJhkRNGjN AAABJubv3Tndk1a0AAP34AIAAAAKM90ssmxG+APltlb4X3ZFy5FZP4kFcZkcAAAAAAAAAJwm+n3C0u+pTKFYGN/E+YGiyya3lPrXI/ykIP8M4c3Wgu5CUiDcHBlSx+6b AAABbMQtf9ALFvRRAAP/wAIAAADB/4TpX5pz12CzfkRAVrdIZ/zYo4LhPMEQAAAAAAAAADmYdBvx9oBrJryElvTvvM1Syjaye4J8LftvHwVecsOk2HZRUsqwFhkbRYB2 AAABxaHL+NVHoUz/AAQHoAIAAADoHUz2iigStnsjLSd4bTeTg8yet2jbSbsLAAAAAAAAAJE1SXE8H/4K1acSmQz/coP+ODNPp1MO7xcyFtYOvWi9nINeUrYKEBlp4UEx AAACQ1UC6hrwW/WNAAQPgAIAAACPhaRB9cw7pd7dS+nZFiTvDtNnw3MJ/bsBAAAAAAAAAG7JA7pYje2+TKqKIqcQK1GlzMPAYxrdh99GQVjdxkEq4CdrUoX8ChlyPJP6 AAAC+twK7X+2FztrAAQXYAIAAAA1cxzFmotPC0zH2yyV3ebxRrdqmc5qW+4EAAAAAAAAAFGUvhEXMIryB1ag6FKcDCOu4svzxBRLWYEcJgwfBNFAY0x5UvNnCBmTtj5x AAAD6rWvenKyMXmGAAQfQAIAAACsDY1USGpUoXzLTDAkKHrzydCFUFE78vICAAAAAAAAAKokpuJ8XW7yyzLGH5Gz2xplbbp8koaWC6KK/BcOCTCpa9aIUvsLBxkl15zS AAAFCNHimKfmYljJAAQnIAIAAABUFjK7kH+hlwj6cEphh4oUPXWh7P3DkFUEAAAAAAAAAG9DyVe/GGRD9XOHS1cohSqeNooOwSvIT6wGPdmdZEzbOMGYUkISBhkH1JlO AAAGVOtvey/80pQOAAQvAAIAAAA0mHeB65bRwhvbelQLBBYh/jUUv8DtBLcCAAAAAAAAALY2n76ecRwvbQw8sOzw7hRlerxUpre5FS0sWl+35DMBDiGnUm66BBnBgNK3 AAAH/1t6zJkptGOSAAQ24AIAAADfcmfSNpovmr1d5j2K7S2+D65o3cadxYwDAAAAAAAAAPhaPQ0zPVLKuwrBNs1CMA28iE3BvPNX99H3d3XKWNo+WFu1UgyjAxmJ1R/W AAAKKbsJOIfFC1GEAAQ+wAIAAAA/VPbqGot5LQuRFHNPJ6UN33xeB1C8eakAAAAAAAAAADJ3SNw46mKUdoJEC3uGPBs+37NpUqZICKAs32SY1ijgvrnEUh8HAxlPXdfJ AAAMw6Su4LKwrErjAARGoAIAAACFcF4ulUEs9DkuVM0xVm7efLf1jIxUjt0BAAAAAAAAAIFj2Y/SNomyfmn93vY9LKpgDuFqgdsxcAjrjgVYn7widFvTUmZmAhkGG1Ph AAAQC71UHVphkj4dAAROgAIAAADgP0Ab19JISjI+zUtr+ZRaGjXeYaRCNVECAAAAAAAAAD6y3yI4LkP1tSfApriyMLzLk5RwQ7f5NXrT9lkfKJdJjmriUiz1ARmiymc1 AAAUEZ4ZJqWngOInAARWYAIAAADwfcFacIKz/xtkQBANbOWnmdV4PhYHzFABAAAAAAAAAG6cdoXTGFMoIltzbAilUhwYcqO4aXk6LAviFEIJt1VYyOTxUm6jARlAIc26 AAAY4DTsS2yFIG3GAAReQAIAAAC7sd2bA0Oh9qwHKXz2EnZUXK1tJz3YUZAAAAAAAAAAABHTtpH4Mq5+Z126IM8dCRM+zDPdicm76CtCnxt7fBKbT1sBU1NfARlEoVum AAAenV5ZFhaJCAWSAARmIAIAAADgYdTpsw4Pj43roethu8sLq5AnfZTTgwsAAAAAAAAAAHNuoPIB8WKYIFws2Vq9HY1JB6H8uFsPD8G6rfBoqeLyOYEQUyYgARmxDAjQ AAAlnIvscf0+S/mBAARuAAIAAAD9M61GTCYwmwz5WcXYnTL6Um3u/667T1oAAAAAAAAAAKMaMQbYsZ3TSGf0Udjf2VcvSzx+1irNj7tDD+3i9brlHBQhU7ECARnEQZUl AAAtZ7+ZYBB+PISnAAR14AIAAABIMc8GBdlAgDhBgYxqAsV4Wok+6Lo0aGsAAAAAAAAAAHRh3QIKzF7zCY8VulZBVJmhzbPwOcUE/3WAnd9YbU5cGMAwU5nbABkWcEB+ AAA2ljJSEaMyaO0FAAR9wAIAAABeKZ5rt3eIXLOREy9n8//nCLDNmmwGPL8AAAAAAAAAALkftQx9tn66T1C7W5JL6xyPbgfCegMyTVhLJU1G0hBzzt0/U6qzABlkLE1Y AABBzvUssHCgSb9TAASFoAIAAAC6PytCCOwElbLjdDRlyuK0TY8cd4tEz2sAAAAAAAAAANKH5S6ARcBgwc7kfRzHVZx7irjbWAU5+1X8V5qZjqFO/g5QU4ydABkmwMGA AABOmwVOeMsHc2uJAASNgAIAAADcDIDt9Mu4Dsyj1XYt+uR37C/CTLBqaJgAAAAAAAAAAETbmnnhaJC1AjaIKUWqGwtnW+w1Hb433Eme8bZBdgjpJypgU2yJABkjpMw4 AABdRsdNbDb1KtjbAASVYAIAAAAarq5w/eZlSZDo73f4MPqxlSWYgH1jzDYAAAAAAAAAACiEa2cQS2gGiskqmbA9Dzwg4Z9gwSF0hILuRl6+bz3UN9hwU1MwfBg7lmHj AABtguPJOmnD9J1AAASdQAIAAABmUzMXiUQto4/0BanzgHx9NAen4IW16Q4AAAAAAAAAAMVlK9IfJ6CHNRXFpTj8dB7mEgHXJBPBhW4JhnB842BN7nmAU0IoaRhr2iTz AACArwrQuiUdIFFfAASlIAIAAADcqCVUPO/WYrMZngKvoTxqrUsBiQGAAQQAAAAAAAAAAAGHdD1IHbUgFw8icB80oUSThERqVVdeLEbhg94otIVHAeaQU5qFXRixibXk AACWPeQvCcdpQdWbAAStAAIAAACys9IE+9H9pfG/qOg9b2e+cwfAWmTURBsAAAAAAAAAAM0Zs2jqdvVKYE1cUiLRkBEvNk3xpa83+8JMs/vTK5dkIAShU6KrURiCTR+s AACu7exs4xD7A/iuAAS04AIAAAAQRk0sLUuMJa1ZdBfAg3MNkZjuUjGl8j8AAAAAAAAAAGiejOiJWhvDUIQZ+cStIlPEBXjFUWuJ/y4QSpoKY8tTdMqvU9FfQRjdeDT7 AADNxIZL2EwV/jmGAAS8wAIAAAArHIYnynCwrIPXi9SmmWqeG1E7RslCshsAAAAAAAAAAMWrKgOiWAzUWX2/FBR48LCfOo+izuaTtNPCRxNYYpzohr3BU+ZrPxjH6MFA AADtjmjE/SjRXGm9AATEoAIAAADpWcsIJjhutzPI2IbHsfh+ioRrNUcy1BkAAAAAAAAAAO1gHyh8+IQmcQ/clbfzVr6Wa6F/COOxRMSMdB/crB/rZNHSU6KuOhjH/Npj AAEP6WisrWA2CCybAATMgAIAAABLsmfKSHJrF7yozzRT4sfUd4ze+yS2ETcAAAAAAAAAANB1cGqQoX60u5AEz1yLxlvFL5WmftI0wbD1gw6LYM7RwFnkU2K6Nxi4XNGm AAE0F1bcWn2c11U6AATUYAIAAACiw5fpBhLcKlyaQObfcw79B20LbpkfLAkAAAAAAAAAAJ2aASe6Uu8icLpRGmEQBC6asItujpoOKkDubRXc8ugefqHzU1gcLhgmDweu AAFf0LZuDMI3wQVTAATcQAIAAACDMnWpkXDGHgeWeOz86a4oPJ3fZg0YziAAAAAAAAAAAJ147/j4n1VrTsLaJh9plJi5kePFJfC29+U7jbeV5TwMcK0DVO4VKBixqVjj AAGSHBls3JawQjhAAATkIAIAAADqk8Ja9r3BmT4lRpKN++40NuECrlN90xgAAAAAAAAAAONJaW0QOG0g/swxcTI5Y5Dja9+7gfoEI9AGaOK0HNIRMKQUVOnbJBhKVXAa AAHIzx1CaD2mEO5gAATsAAIAAAAPavk4Mgp++zVN+dqY8+XAod4HFaLRBxYAAAAAAAAAALq6UKIRa2UCK0N6nJEsg9GMOaFh2I1dJhARQTx5VwtzUIckVJO4Hxhp5XAr AAIIXQzQMYMX3DJQAATz4AIAAABfim6ucWDYDDzRcTKcq91Q9mEwUkTiyhoAAAAAAAAAAGNlYsg+8U5nM/D07mezkVzm5if3fxIYMMjo7wMZlm3SKs42VHNpHxhfVFYE AAJIizmTz/JAfUm/AAT7wAIAAAAPJXFbyppgcSKMToKQ3eOwGRK6nRgwRBAAAAAAAAAAAPmc3IyXtbFEHExxUy/soFPQADf2akeZtu6hkPYgfIJZ4cVIVMCNHhis0IKo AAKKh4BnqEMeRTn8AAUDoAIAAABryq6Of7GLJ3NZ5TWvkiCqj49tO3NgohoAAAAAAAAAAPhpvLQRDzvi3PmC/oX5D4hFa798Pp++VaYGBUfacUbbx4tZVDDDGxhLrvor AALTJWBx7kRjY3l1AAULgAIAAAAr27E4C0e1x8x7E5mTPdihs5xqhtVakgkAAAAAAAAAAJRyWd2c95lOzQxx18cFywd2EGSnYiZQElZA7f2dn14YJLZrVGFIGxhRGzza AAMdCeP85TvOJPIDAAUTYAIAAAD+31BVesiYSho2I3lIOvZK4kMuuaysMxEAAAAAAAAAAA2KxHtLwtB5Iczh5LrDq6xugBYSJ5yiroqMNMfqbCkFOU5+VHR7GxiFv4U5 AANmZQTHJBTIcqplAAUbQAIAAACvJGyzrYOYeow2qZ0G7iROIGnKdkprxhgAAAAAAAAAAEqjhDBvu40xnz9VbmK220mLsKHT+k/dnJh/ckUWCCHXcAiRVHzdGxiXP4PJ AAOuvnpjW2Yg+9FGAAUjIAIAAAD5GNf87na5jTBRWxIHX81cw5YLIu06sRAAAAAAAAAAAKJQM880j/xFqgWWD6wicStgyihqdNDEHQBZijlxQyqylPSiVMoNGxh8Hgue AAP5Q90LP/fABJRQAAUrAAIAAAAravTkVTH3NGhn0jxHTncjDtrP4ewUGwMAAAAAAAAAAAz2K1twsw0W53aH90CPYQIWHsJwDt1bs2LA3wV/OJ3ZqQW0VC8BGRgFaH30 AARJ40ILi2s9531gAAUy4AIAAABUx0Uw+/UR2daHbXfkXqth2AXTyCekvxAAAAAAAAAAAIVkvibj0bsg3IQLFalyFxBhHG6W/Ob16bbLXbrsMcxNUrDHVMCjGhjD2lhm AASVkTpGAVaKeJ0JAAU6wAIAAADcCuFcrYcxYvJ9sv8z2fviGTqkkuHp0QUAAAAAAAAAADORZ8K98E9ap6pWsauJJWGefYUeJYbXccUZQCxkDtXn39TYVIe7GBhknHpS AATnFNcvmhfCEIsUAAVCoAIAAAD4QZrnTwgYXJxMZzomI2kf6UfWS3gwng8AAAAAAAAAAGviTewuZJ+ALDtXrEvc2ilz0Ae5pCOcQAVwdqd+i0xz6WrqVDqNFxgyfc4g AAU8rl4badO1uBLFAAVKgAIAAADohgw1YB6WZa7pbDfWSj6alUtuw+HTNgAAAAAAAAAAAJggHsJXPRATg1XbaCCv2fx5jRdX2cn83yIU7LsQvCZHJZj8VMAuFxixFpaM AAWTpGZSBCtYHUPeAAVSYAIAAACo7EaE50E1m38zUaYbSRK7GcPret/JPgsAAAAAAAAAAN+76k321Ttgblny9CWJGm+V8jHBXT7EA9F1tdmbktOb+1UPVfKIFxiUXtM1 AAXpTfXdPxpBx6liAAVaQAIAAABEaLzDhKHbwFL3iM4rjxRMHcjjIuJM0hIAAAAAAAAAAHTPj5EVdrg7q4Y4nUT4W+JX8oQ31qYqC/Jdx4tu6ZIVkcYgVXE8Fhg0fHpw AAZD91v1txqyGqYbAAViIAIAAAB0xRwcxTqvR4xkO7YS2mvReyaM2b3MxAAAAAAAAAAAAMzAomGKH5c9+sN4J0NbRjq9GMv9DygKkEMtPXhJejbMAvMzVfAXFxi3Kh3H AAabQ3fB6l1KDS6dAAVqAAIAAAA/mYFKNtKiBDsdS/YaQQ9xgo7KHey/VgAAAAAAAAAAALN2LtJ4rES7lT4kJiz+uVLQq+bTt/i3T9JOAJuWtsuWXWdGVd0TFxhkNued AAbyn0BqaMRnY/EsAAVx4AIAAAD2RpmBinoT4pBhSm1/93RvVJDm3MrRdRIAAAAAAAAAAEuJqkMTnRSvQtA85jDmCibXYY79jW3Vfn2mE1CimTZbAW1YVfWGFhhFIRcL AAdMHOWj80CPTV4vAAV5wAIAAABWsh5xyS6HtXpJujn24r5MuBNkDOhqgQMAAAAAAAAAAILMtOTaiINGA9Up/LXhxssWMKwdnEMw4QzSQhgoVx3isVtrVYsaFxgJPMyK AAejX6SPTLkyaECxAAWBoAMAAAD29DiiCRqzxKMc2pQxVQkbUg9XJDKVkg0AAAAAAAAAAHzG9cuYYQmV95kdCdPiN/nipIbtjwJbzlnj4M3cUvc3Ywl9VUMgFhi2Ml5r AAf+fN7K4F8UAv+UAAWJgAIAAAAGPC75AWvzKpBO6tYqfMEgz/o88kMZcgYAAAAAAAAAABi2Rh3rDU2fycZjoQFHQzKuqvd1DMewA/tx4ElOUD2MLp2PVY5BFhgmsH7a AAhZEkG11cDZSx+GAAWRYAMAAADEVA0hTsdntv2BMSH7YQNBAsw4/vKaHQMAAAAAAAAAAPd272QzhiykKFteshui+Sj+XFekHj9YlkOep5WBi2IfjnuhVciGFRgox3uT AAi2uXaQKvTZ+PGtAAWZQAMAAAB1PkcF7tLrq/Dj7GroyFUWlpRp7xOKLhQAAAAAAAAAANv05xtUiRP+8W4H00okNPbjrGfMdt4NeAXuVVw+tLcBvYSzVRUIFRingz3n AAkWlKuZ6iMxEO/QAAWhIAMAAADnlPPiXnH9CFZzKKtuau83Zm10erONegwAAAAAAAAAAHZHfpKkuUHl62wICqHNv7q1FFlwPkxkNjhO3yfEE/hzwtbFVQTdFBg6fjkM AAl3NgJIUTuzEvYHAAWpAAMAAACBWl5t1uTPvhf41vIgDfSapHFsxndIhQcAAAAAAAAAAP6nJYLyiWbEIybbqB8DsLiZ+pkphIOy0S/yW3dlqoP4J8fXVcRDFBiaEXGE AAnaslxFZ2oPdv/9AAWw4AMAAAB6aDb6o44WSorZWegzgtP03rmuEC0dzgUAAAAAAAAAAHzLZzcUwL2LXqKQ8ewJIinFp5b2aE3C2NfshA1FQ3I9w17pVcFNExgnIqOG AApDInpI0hyEKEPOAAW4wAMAAAAq86VtjVrbZTFxLkqqwp3ZWoV0/uwdxwcAAAAAAAAAAP5PNK63I/35bvFzoTOk2J4j01sw/q9znqdo6Jax+V/8bBn7VbqHEhjH1UZ/ AAqv7nbV8gI4gQVWAAXAoAcAACBnp0LqRjXO6+s3Xmh8h2kFduYbFjRPzggAAAAAAAAAAOx90gednMp8jwoRnPuL430VF6vl3TCv0MKRTfo+8Jzmph4NVnIUEhhp7WAg AAsfb9PKyG915FuEAAXIgAMAAAD53Ve9nlz9+3dVCpNYy7pH64I2hPQzYggAAAAAAAAAANHq67w9uwfVnA9Laa+5lRjg5PxlEsHNNJWztuvLrCejz44fVhQPEhisoAWx AAuPEqIEXhjZL5S4AAXQYAMAAACAFfcaPPZS1o82T4h404hrPHb85O4EqRAAAAAAAAAAAFuabbGwhWZQ+jLvOayxrCx1haRssKUd374jDMf82FoGop8xVlSpERi5kRXr AAwBORlyRT6CKd6yAAXYQAMAAAAuPeiomStbNHwU14SM96JPLWKddFRHEQwAAAAAAAAAAFC7sDvw917FPxkr13125Kr6GWDoAVgG29U0GXgH+ugLVBRDVomyEBgVk5G5 AAx593VzlH8jtrJaAAXgIAMAAAAd8AR5EiSXrsmLHjnhvf+uiZrMMp4DpQAAAAAAAAAAAHS7CaZQHNLKgrZmaLm472N/XgfLjwnI8WnyGL/wNEBNrsxTVnYeDxgcuacc AAz/UI/qgIbZgN+/AAXoAAQAAAD6czd/P1hFRda1NWOKpMwYw1kymz23sAMAAAAAAAAAAFIcelNdLVK8TQYGtH6yy+VDEXumqnd4De9k+zYzuSTmLMVkVk/mDRgbmM5N AA2QXhCcuWjDvZMgAAXv4AQAAABCUOD+y5YCqA4X4Ek7fhzNDP1lvcW8mAEAAAAAAAAAAFQt2ikzPQa5wHMP5SPZEzw5Yt6yFJIbTl9QVsVyqR7vqWd0VgnECxhMqn1Z AA47uLb+n9qZN5rSAAX3wAQAAADn3BPGQhTIArt8qQkINh9JExTc03nzYAoAAAAAAAAAAC4SBqkpKQ20aa6NvaSJFHQLVkKbjqulcxtzrRkoI1hDogaFVpGVChj2TOSo AA76M7POkqug+Z9NAAX/oAQAAAAHKZZL8pX2fjMkS4/MHTd+3Iy6+KVziQMAAAAAAAAAAKSFN4eizDLUmtQ5PfjBNPpmfmpaQcJ+wjCSqsN+dzFhKvGVVhuzCRhZxxSe AA/KDQ5jmSg3XpPsAAYHgAQAAAAZex89L6zCAW+MKtz3OioFPZF0GUfsuAQAAAAAAAAAADLy7iIjtSj6g5Q+8aIgDxvKKG1lW4AEriAH4zQlNwM6OWSnVvAoCRg6ZpfC ABCmKY/w5K9ccX99AAYPYAQAAADE/juFAwCo8NeGCeT0q8etEEIc7y5XXQcAAAAAAAAAAN17xvJ5b6zUu6ZM4I8ALN6/1Ga9wQD/uegKKCxXpIeROMS2VhShBxgC8+Oh ABGubW6S48XcALNeAAYXQAQAAAAgvLMxNKb+P5jwSIK6kkPHjWQH/NueTwcAAAAAAAAAAKqOJteTwad0SMWRI8hg+EKqq9ybNkMoXsmulA+kudEOyAvHVp+5Bhj0mtfn ABLaMjZMj0fVGWBNAAYfIAQAAADqsdQ60ilyN2qK707hr5NIfzorxtmkxAMAAAAAAAAAAAdmm8141IyLhH6c6nbSJ6VrVLyibtpcSvr9VSpQoK+0hBnaVqjwBhjZyz3h ABP8sJiJmTFclF8ZAAYnAAQAAABHPte37y/Ogowxj9XlhoNEpTVsnpO2BAQAAAAAAAAAAEQJyuW3svjxjqVfVYyb+nxfR3ihpTFypI/FfhctDtPSZMXrVsOkBhivm8HH ABUsJBbzwzhj/RTNAAYu4AQAAAD37yiBuKDLQVuoHoicebxfGwmBZ8lWRgMAAAAAAAAAAKSIaf6NZ3eCH6hVJROct30SxEDBYYLGN+lD3+p9k32qexb+VvSWBhhWKCct ABZeFeYwv+kd3mJ7AAY2wAQAAACJUWxLbjfVgA+fnjYJOiuNqEDXrOJFVgQAAAAAAAAAAJtNP14hQ/GkVCrywXZ4CgxX72aDphhfHF+Nih2UWbLkBVQPV0snBhhclofi ABeltF+z3kz9tZTwAAY+oAQAAADR7x73u97v/1ByQ7M+/tZHZYwwOKHmgQMAAAAAAAAAAETWTugwPNlatypt7HduiToghEoXJhzY1x2YAHP6aZITrskhV3YnBhhaaUYC ABjtTYrOHh0qfIZKAAZGgAEAACC8uMT/qxBBSnWtThC/67Ff5gIhu90AIQEAAAAAAAAAAO7KSzplx9PRqtr6XfVNa77PAZgw+qVEYJBCnVfp9e5DYsQyV/qoBRhCJKGr ABpRfLdgJt4nqRNjAAZOYAQAAADmLvKMuXk/T5zSpnpYwee1kxKbm+VWbwQAAAAAAAAAAGSfiOKI1oe8gZu13AlKjE939fRsL2osAVIllw97PFFXdsFEVzaEBRigCrKE ABu+78vXOVeKMGz1AAZWQAQAAAC7kyyQhvxEfu3SQkVNRZNvR/SRHikcvQEAAAAAAAAAAPrreBAfjCpsAEti4TRoZ1m3UmWvpBs/zI7c31XPADDj9oVXV6CbBRi2fCdV AB0mcOta9RlcQHkgAAZeIAEAACDNHXKw/agnsGcB2HlbfO3Wb/91M0h6wwEAAAAAAAAAAHO4pz6FxGfyz45cs3SgR4/MpfRnt7UwhSKVEaFATK6uQs5oV9Y/BRgO0kEv AB6mfdbrgpnwJCwPAAZmAAAAACBCLsOepCYOv0JCHb+GM1fEmFLawrQEGAUAAAAAAAAAAPnen2pzKIQ99/GjckUdhUzoQxvp3eaGMNlal3NweVcOMe56V/0mBRi+cadQ ACAtxfsiGV2z0xFqAAZt4AAAACDh2VuHjAFXFHz3IpMZ8BFDKffxiHj/SwMAAAAAAAAAAGA+JCR1kdW3pIFA/zuSI+PMFCTWoRZ0YDqTbhQyQcoHwmSNV2kmBRh4+yfw ACG1N1Lmnz65gcjUAAZ1wAAAADAhYiwmpOYsr6jkNMfgg/VAvMyDkjy0BgUAAAAAAAAAAEj6xboior1IrAoZHNGBEYjQ2Kje4gVpfSmkFC3OZu+gguugVyhyBRj9s3TO ACMnalaEJ1NTfQI5AAZ9oAAAACC5fXDa4zD0YCudAQ8FeDmjUAea4WhwkwMAAAAAAAAAADp+YYP4pmo57SYqnmIshNH5qkvcEszlYEEz5KjgAfqRAhGyV9wOBRgYcYix ACS1/dDcjk/vG+0YAAaFgAAAADBxOa6PS2sbIN7ep414bHCqZbmePOz24gIAAAAAAAAAAG6AhNzsYuvOL4V4labW7wL878syYnk9WxrOFWY2xQXkdULEVwj7BBiJ1sZx ACZKxHB79ab4pJEeAAaNYAAAACCxliE8dmLJ4CSzvJX4FUO7YjWxNmYHswAAAAAAAAAAANxevoKSYvotUK1wKjEkvrYhWtFRgroZhfZ6HQEdJRcMi03WV17eBBgvWejH ACfo3I9pX8Vg+F38AAaVQAAAACDVu7BS4xgWVM6zEOwFAjuj+wwdHZa4JQAAAAAAAAAAAP2sl0wjNKxVo9NGX5riTpBQZyeLPYvLOFh64GhzY1WMB5bnV9SOBBhlmsXv ACmjLx/5YpHTniFvAAadIAAAACAlCbO45PmCkMfJVR0YDrKkY/C5eIvVIQQAAAAAAAAAAKqNoGCd1mcCs+i1I7Wi1Lv1jroo7bHepqVSZRYRkszZKtD4V8RABBh++Xtk ACt9MvFVV3nfpKwMAAalAAAAACCjLo4nRVIWoC1HBLOjxHMc80EX1GjZfwIAAAAAAAAAAJSPpGVCwtMaN3M9ygOyzIL/rcBn/WA0efZNdz/PwTgB0qkLWNJVBBg9TuO0 AC1OOf5J2N/elcOrAAas4AAAACClkM9dyJtNLsIhRAlyMdEJwKK4hDGiZAMAAAAAAAAAAKN+LTmq8x2L18AKB/qXhP7oirNhCC8DYBEZ2sSg2HyNAg0eWHRRBBibZltl AC8hHXGkPdcuVDR1AAa0wAAAACD/cu5pp48ULk+ldNcleEBgQEIEpdQuKgQAAAAAAAAAAHU7sAH14dJP9aV9uCd4sKl/gx396hdhNq3gFi01w1CsF7wuWNTmAxhrynAR ADEl0i+0xC2+0MbMAAa8oAAAACDTxa+JdxdIkPF+GouU2Rntb4DyE592XwIAAAAAAAAAAC5Atl/p/FL5QjniJgRHOm9GFoTgaEkkyfndzyMUoN6hguBAWInVAxho9k9N ADMzpbAMPo21HkkvAAbEgAAAACD/RceD0JcG41ncx2CD4V5Rg55O1TH/swAAAAAAAAAAAIQVlwvcyDUpOhEO4jh5dEs+FTj1GaP2+QmNotoCqdQzwfFRWIWLAxjG3f4O ADVsV7hkrnpwZcyeAAbMYAAAACDwZ24cV3C7qIhqq9iL/T7CZs5oI8vNHgIAAAAAAAAAABjU10kV7C0+g1eqjT5QD4ZeokqZ0NKsN41JYm5nuMmzJ/ljWP91Axh6IlTj ADey3SPIpnwtZ+nrAAbUQAIAACBsEgkEvq5p/Rx9P3I6P9zBJyuGO7MslwEAAAAAAAAAABusRqpnJ9rdKjeMBijz6TSo4tfR+TPF98Qw2ggdq0CN9GJ1WHlDAxggViTG ADocpOmmgGMKNFU2AAbcIAAAACA+evIAa711Ial9nNhBzAxngI0BsdImywAAAAAAAAAAAI24Czc2VsD4Mg6PTX+zNtohrAfjoruMFLnRJPSjLyRnxDeFWEfMAhh7mQNN ADztMjVjN2XegxWfAAbkAAAAACD0IUtha4NA5/e01gxpnJLNymYvArTedgIAAAAAAAAAAIlSU/fahyQ4LTjcyd0FqnYoFn2u8Ty+LZs6tWv+iXkUCWaWWLmaAhg8hAWG AD/zSsNCs+c6akJ0AAbr4AIAACAXD60LaxzL3EQB17HI7oaMaXfWzhJ5WAAAAAAAAAAAAOYp8InMviL4HtmJZ+qXbpQZa8lqYCLwZB7gwEnpTc4LEhaoWJN+Ahi5Hn+P AEMbgr5qXa66E8ZpAAbzwAIAACDI57WbtoNcjZuu68zYzr0eIsRqJJqYnwAAAAAAAAAAAPX/tG09B7hqaw09bNTpqlu6LSU3NCZW9Wc5u2QaeMaXTr65WN9iAhhUYqAB AEZoYHlBPSBbr3gxAAb7oAIAACAH1iE5ncNJo/fFTdZtBxMs6MSJi+2LAwAAAAAAAAAAAEguaQj5jPDyXdaqhlieIH6e4iBSliCangqNRT67w1A0/5/LWLFPAhhCxCu3 AEnQoxdDwQphqRzIAAcDgAIAACDNtgpyTJf65DaaZkbLtrSlowAHWH30IgEAAAAAAAAAAPP4LeHjZPYWiIpTNZtu4pShjH6ZQAPVMaUuCUpSQJEGrDTdWFozAhgqr7Qk AE1kxf+UqjKJBDiUAAcLYAAAACCZ7QwAT9f0nw5QAqlk89D+vZBqpZ6tcgEAAAAAAAAAAFxkwm2ofXOccNo4M0KkS7ST+vKvrvhUEcxoEAL/sKsOpunuWHMcAhjUm2BO AFEftqLxzn0eR9qEAAcTQAIAACBx3BDH0Kh8Fnu1v8WZll4FxzOynULXAQIAAAAAAAAAACdBXTfPF0ByEQdNFp52bRgRyS8fEEbSJQgHrASPAxmcUVUBWT4bAhiVIS9H AFTc0w4mC90KhU9hAAcbIAAAACABgs9JpUR8CNbE/WGfkGy/79uKKptUwAAAAAAAAAAAANQNMFOfWTFb4YvHrCLuS3dwGavPGkXzJl6+H9boQj/TwIsSWaf2ARgA0Z/e AFjfmkHxo0TiewZzAAcjAAAAACD990Cw5Jz3W7PVFo+zWG92E9zFzYlnWwEAAAAAAAAAAC43sUTAus7Qfrfntk2pFs0xIfJCcAVVGusOxqZAKsfX8OQjWVTYARh/Xan1 AF0kV5ADzGNCCJLEAAcq4AAAACCoGJH1orG0in8CAflT/1tGl12En7IxOgEAAAAAAAAAABngc7dr0++rCbkOMxmNUC6BwrSKgFVWaXqbC9Z1kGs2mxo0Wa+eARhX3jJH AGIA7PpiYqjrFEnBAAcywAAAACA0cRAbvaP+MHZksyg6nvDpfZo4p+rNiAAAAAAAAAAAABDIq6hHm7ql4ISBUv08IonKUOHD5YyaT6qvvfWAPFRI3bhFWX6LARjkOoHT AGcZ3nr5KS2Pc8sQAAc6oAIAACCZ1qcMVHu6oaggSQvQLMN407xuIEaUOAEAAAAAAAAAALZqCwJM/fB9Ddl+GK1u8aQRsEUhKdO/4+brrlXe/sTdlUJYWTCNARi8JgoI AGwtVR5QbbP7+NYNAAdCgAAAACDc8lFxBEpdPYP10NB3dPTebmsFfyse9AAAAAAAAAAAAIqaQsinfGejMjtzlgVoO1H9HkGbtAp0H9y7Ea8y+i0itoRoWdxdARi51QOB AHHwiS0wpgoU4a2uAAdKYAIAACC4eVrYZELObYKJcY195BPnOHWbcXMDDgEAAAAAAAAAAC9LtwzWjKpXXeYF6TpIN+wGjqi8hahEW63uWpIUruMomsh5WTVHARjEptgJ AHgZ3qmCxIkEdFyPAAdSQAIAACBmozoTm7ZwYR1xZUlhh3dbrQPXC9HiUwAAAAAAAAAAACB01a+exTUMb8xLXN3oEu5GAt1QA4rgb6aIs/4iQJCf4gGLWeAwARhJp86v AH62plJTHFrWpLjpAAdaIAIAACCAG4FikzS+jnr16/ud8JwY4fgztfDvywAAAAAAAAAAAEDRygd/7+f7eXcRuqDAY+ypuO2Uaa4BKJgrRK0MJThkkTKeWek8ARgi/1Qi AIUTNK0P3kp+jZI2AAdiAAAAACB4IgQMwEdDBLImi4thEzFMfB7WimOt0AAAAAAAAAAAADxwUP3ctm4wgFMTeDsSsSxsYsDrwyO5JXbZSA4DQLmHw/avWQsxARh2Zc5p AIuvP/EAUEY/eZ48AAdp4AAAACChi8JZfSP7eFbEyvTKqNeidphjQ6m9pwAAAAAAAAAAABMYNEx8WGnQzO0yKAG2lntPZvbc3DGn+5T2DU5f3XYFNGi/WRj/ABhT01Vf AJOWbjJFnoH8w2ISAAdxwAAAACBhjkMKwWl3WZliupk0hyPpY4N8MGps+wAAAAAAAAAAAI3r4znB2OjMHzJ4LgRcsFKQed5nBd7UAnTHketY2a4yAojRWXP6ABjYNa/f AJujLZFGK2U7jmYBAAd5oAAAACDZXG7YWfHa/yfFmfHx3LEPqrz/R4FAbwAAAAAAAAAAAHeJUflLt8RCKl7vCcWt2zkGOhJvC8pwh05/YSs+PSwzbt7iWTDrABgbBtzU AKQ10C7DKtkVjGQHAAeBgAAAACC06p4elhmvzrndOTx91MCSdz009zy+4wAAAAAAAAAAAMhj08Y+iZ7h9uMg5zsbKn9utdrVasel4WhCGpMt79Ub2hLyWb3BABgwGeUD AK6dnX5YX6bYxakBAAeJYAAAACBpopofHqz8DGjp1cH5IH5ll/He1gn7swAAAAAAAAAAAEcfGy5Yer3X5Jf6aFd7YUvadTrELBgixG5yiuiz8mMcL8IFWkvOABhGVPBx ALhjXJ75VIpo6JKGAAeRQAAAACCYKvxq+oBeISruI6Wz3y+04UVBBB/nvwAAAAAAAAAAAEGhEf0MnVYLWlZWQM2e/GZkkCsKTxocdj1ppfi/z9xjrHgYWvbQABgNbgyL AMIJZ3KMyNccgCcuAAeZIAAAACBl165IP0v4xn2hf9HL0ZYPnORu9b/jzgAAAAAAAAAAAIqwdYAuU8F/SVUQIJH3F0Q8KyGIaWt/4QwykQuEUZIdeRkoWu2wABjQAO0Q AM1urjbAzuVkCJbTAAehAAAAACDjfICnqLhQ4+ia3QkJC2uJuHRJZRPviQAAAAAAAAAAAChd3HmF58NOsgXSwxKXJ/e8EIc+EeBUtI0aknhqQtfSyMg3WkWWABibOqeS ANrZM7W6oNFt8uZnAAeo4AAAACDdX5Qm4nuyoj9Y4VriMauNMXxBZ6AFMQAAAAAAAAAAAGLVx5fnY6BKSpQjKjfcc8C9mTHoCoOlzz1Xb5CwEbUZWrFJWsGRABgSLQaq AOiuWCg9RKgiU0urAAewwAAAACAjkiq3e+Ve/QXO4SOSork3k/Jmn6MNcgAAAAAAAAAAADy5mlToNW73oLuFQh7YzHg3NFhngjaNfLCu1d4M/pVhYrFZWoxXfhdJt4bf APijmUI2IMbP+2pXAAe4oAAAACBM/YPL1ZJj9/n3BvEUFYY4ICzVy1eJagAAAAAAAAAAALp9U/IU2smDwyedYEAoN8kvXRZmTbNpL4oGbgFQkLGCtH1pWkYhbBflmY/7 AQtIxK9jt5XeADXIAAfAgAAAACCBBqw8e44DUlaAnv6UjDiUF1QyVOVoawAAAAAAAAAAAM5recvHh4mNgVvBlCSK4/Jhi+vcsxryfJnhJD1eKEYyVzV6WvjpYRcRjX1F AR/fzayx9nuiacKMAAfIYAAAACDO+kcNtN8CqD3VGt9QEtvqAgOf6t7JAQAAAAAAAAAAAAJVqpnEuysv0y+KFVCd0QW055B31OyVHpJXOuquqbjba9qLWtyXXReVC7vR ATVqTsduFrCR3WRiAAfQQAAAACDRBJtmkoyHSm9OfuU/amIKUxGBd6iqEwAAAAAAAAAAAJqS+ximZ7/e6Oqu6Y2826uwkqPJ9R7yuHuQJ01UhIp/vsWcWqOJVRcEraJw AUz8BKngasmFxGtjAAfYIAAAACAwTKyssD5pyDS7/0vlMkowmJt4mKpnLwAAAAAAAAAAADezJ/Yacj838NQgdAlCR6Tp95q8LTWAQwdaZM9suE+3IlCuWklKURd9IESy AWXI367hnoV6V9zpAAfgAAAAACB5yZNwQc6+27xRkPbcbFuyJ+6YIwdfLgAAAAAAAAAAAHO/8/9O8Ui1rvLaH5hEzap0MPcwldOTvrvTlWSaj/9I1YTAWrcqUBcMmrXm AX7u8fTowemGBM0dAAfn4AAAACAYDV0zdLyxX+sWip70ujsF4q0gfETXKAAAAAAAAAAAAI1E1QMy+cLTC20QKlRrr7Af1S03Ni1yaPT9k4gGli3NZGfRWg1QSRdLHQYe AZpuw9LU3K9Yi6dQAAfvwAAAACAQZWFUV2wXJJWeqglMLb6HMwS5RpTYQgAAAAAAAAAAAMforTYxahXrERK4dhIyr2OWT/bCi0HXBX5YWXb/t2JMXQjjWlP7RReXH+lF Abc9mZiFlmDZF06CAAf3oAAAACDWa9SFVsinzt44IaA0SBcTLKubNk3sGAAAAAAAAAAAAKtczARVeFx0TAiZB0DG74AGATVTY4oYVswDDRTv6Uxox/f0WqnsQxdBDCJJ AdTr1i38PmyiFZ5sAAf/gAAAACB4ZuuNURVJDIWcDfYXO5ZrxMY3ImxKDQAAAAAAAAAAAJ5OdtT4l3HpapzTwfdfSuEc6sNOOITWBqJEjjdZZ9IUNbwGW0laQRdCG9nt AfPFgOhy1wDEwBC6AAgHYAAAACBqtiGOq5D48LeQU65Xa+B8IyHahrmtMQAAAAAAAAAAADlUYADGVX+lwB8xxK51Aof5PKccBvhJNPUFvzJh+wMl3tMWW0H4OBfe970O AhcowLPm4MSblXVuAAgPQAAAACATm0wBBag0mPkplc0zk1WJN80BEvQdAwAAAAAAAAAAAIlmBPoOz5WwdnN3xVsIAdIMAVXSmbuy/uYrbfC0dzkJ7cooW1ZvNxdWhKeI AjuG9ZjLnxX5qtizAAgXIAAAACDYzyjPVWQMfqZhUfRsLd3JS+GN/IvGIAAAAAAAAAAAAFHksafEa/I1XtqntoZ32R2wlMPh8K6/sbKQa8woDPSj/UU6Wyh6NBc545gl AmHxd50qYovic1m8AAgfAAAAACDwK5wok0gExoguH4yK872mRn0iLuXcCQAAAAAAAAAAAEBuodyPhcLelI0f+rpug3n/VDPohe/T0TQiidj9B+5DmWdNWxdaNhf6H6J4 AocJntsfncQRNhF5AAgm4AAAACD+Fi95ceJM/+CsiRaFFUEUz03k9JLKLgAAAAAAAAAAAKprEDp2Vq3kFitaooGc74BKhddkkRfoJqbsaUZb1sjhk3hdW3tPLxdc6RrD ArGmue8BkhE2O2eFAAguwAAAACDF7sKQecJh3F4TffJV/1PwlURf6D2UAAAAAAAAAAAAAEHVzdgjWDQ1xUVucTIqz3mb/0iksYF40IiT1IEJ93sWK69uW6cNLBcKESRc At9qSfVMp/iybC+YAAg2oAAAACDPsRJxflyQ79R2wRdtX1uoqpM32UidHQAAAAAAAAAAAFGjv1vfVTUk9oQdbi43LK7dar/Kp74eg7UNEIwO+tA6VzeAWy3XKReXHySN Aw+ZY3Zvo76cg6xfAAg+gAAAACDOK/C5+6SApOvZ7bjo+Vy60Ua1FuZ3GAAAAAAAAAAAAGQ3cXV+MYgH/kPPpmqjJAo7rUgxz5hU9n08OQubdnZ6H+2RW6EZKBePcQzX A0HfsrWhrUX/AS70AAhGYAAAACCke7zeOAAMRUgCSw8mtIrWKS8/eCzrJQAAAAAAAAAAANtrcrSw1Vf2lYZTvXiofzXjV7uhkbydtdfvFYJfbR9W+w2kWx9aJxctEW7n A3Ua0mxdHmZ6i9CgAAhOQAAAACDihzv+Fzl2xvyjuUOjjOhGzJ13p7hhDAAAAAAAAAAAAAa0s5HbL10ZtHHgWwcQShZ3cNG/WD6FxZh1d3MukiaGAsa1W5HBJRd47Eoj A6p/xq47klb8dUKnAAhWIAAAACDGSqyFPFpIQQ3a7mcBZCu5oKbb/qnbAwAAAAAAAAAAABrjq2aDGqfl7vaTs/7YYWxgGRjhdjRyh791ZinAmqQHG+/IW70vJxeCTESF A93yCPQZPI+uNBqlAAheAAAAACB72/e2Vw4+TvIo3pk/iVWSFrZ+7VvHGgAAAAAAAAAAAH3IV1wi9LLm56mnIQsX+BbEzUTE2/VXs8aaXzOM5jIJ+GfbW5ItJxcEKAsy BBFmqAAfbeNmLFCnAAhl4AAAACBmF+NNB4n+fX0oF/2E3qKoyM3F6ZZXDgAAAAAAAAAAANfsscIddXjoVx+x2mAvgTWqjkMmq0LNUHDuzFOMrwTs/FfvWy9OKhdhIu7X BEENCGeaYj2soniZAAhtwAAAACDirLPnHk5EOvSOgdOB3qfTXi6NXmn+FQAAAAAAAAAAAH8q2iJNxK+6bKNwELCZwCMiy13yT87bD/W4f7PKZO6uoBoFXHzZMRdx8oYe BGl9nUzOC8OwGd2XAAh1oAAAQCDTstfWGtLZX/vVVtngDweHdCNgCo2gFQAAAAAAAAAAANGSdDosGQp0Ifkv7+klBVede47aVoys7hOyV1GscExmnYMZXPQeNxchuuPn BI4RD4GFzbbLp2U9AAh9gAAAACASCzJkVi1J31nEAKDydkSNsqmqS/b0CAAAAAAAAAAAAFy0tSFQ/n3sIXt020JORC74skEFwkTrrrWfY425xI7zyU8qXKUYMhe0EqUw BLZPYwRzFg1IUIWVAAiFYADA/y9lULXa52VZWJ4+PhNSNwcra8SYlJ2mKAAAAAAAAAAAAFmIeDQ19QbSzPuttITlbW8dXf3UgGUKyuHjtD00ZOpzyvE7XDPWLxcdUI/b BOB0BjYhODsJmRMKAAiNQAAAACBrBb0sSgaz2FA6AzwlkzlqJaeeHcrbFAAAAAAAAAAAABsI3z1CzZo42LZq353F60ZPUDYzvYYQhf//cjY0UxWWoaJOXDVoMBe/Z7cq BQoZ1ZncIovd5BnrAAiVIAAAACCuVddkC3OOHBYJHMc2ZlJuf6Eq9mwEGQAAAAAAAAAAAPeCX+BxQnX+VFIfZuiYz3Q+1D3ZPxhctijfmVgj5O4tfVhgXIhvLhdtCFpM BTWEDv/oRyjopOpwAAidAAAAACAM1Taz6xzZwCjggfFFUAYnayk0Z8PlFwAAAAAAAAAAAHvBsnSJ2wHIXTikvG0igGEemAT1Btg60A0qM+vWY5kvdsdyXFBbLhdPuQ9V BWEBNNZ028ybHZsOAAik4AAAACAbYeiWFxCZGkf/gYfZRtk+T7M1acCWIgAAAAAAAAAAANAJhlj1NTHm5n/JRImGtaj5lNpC10YHnqvhD1XlYeJDED+FXBdhLhc1xK/b BYx5OM08JlUHiLPDAAiswAAAACAUwxRraAi40KvExvnuCdfkB396o3rPHAAAAAAAAAAAAGgUmmK5PCyfkfuqKXPKG3m6Efxe6MbM6fAYYeitAs2CVc6WXGwfLBd3+r94 BboqD0YQ/vPy0wHrAAi0oAAAQCAhMy8YgNByq9ZaTqGwn6zbMBFxp+fjDQAAAAAAAAAAAHGzphJHpM9riSBV8ngkfzPXb6kLSMdv2mn1gyaMuWX4rzmpXB0HLBcAIgYt Bef0E0Y8iSEIXavFAAi8gAAAACCqJG7LeBacfvZ1ehdco+WM6V0kwSloDgAAAAAAAAAAAOR/Y00XFPF2REkCTfUOyiXU8RgRXO8sRknJjuFeKUdQVM27XBFOLBfhaHV2 BhV1HVeWh249HUpfAAjEYAAAgCAqUgoJ8YtRhvZ5wz5zvinPnxERXFCyAQAAAAAAAAAAAJDPhQlQdAWfEiNUUxhvEUmEP/drtJ5rgoXJzNhf2QnMDU7NXDj/KRfWGLus BkV2AoDsDE62cnaTAAjMQAAAACC/Jis5jV91U1f7bLXc1DWFEpJRbN9KCgAAAAAAAAAAAF4Pe1dyXuphL2uI+WbWooL5R7btdffTz+USC8sK5iEd6MLfXEX7KRd8Gs1V BnV8GunthUu4x0TVAAjUIAAAACDXdi8B59cAJ+SzQJf8rDogYOr1lKuFBAAAAAAAAAAAAHkiwaMxpVidI1uP2PSAZvDJ1VPm+H9LJkbMzj0NxXYueFzwXHa7JReuEDNC Bqrp5qOY/694OiFZAAjcAAAAACBzxqRKrJh4FvPRDuMnFO3W+yo/+228DAAAAAAAAAAAABAw3deFEkJ3ewNS6pskyikSV+f0b60vDw1At9NVCQ6udvICXQP9JRc33+M3 Bt/8BpMe+l+0cvlVAAjj4AAAACA0pUTAWqILhKoJ/P6ZJlUA8/VY32WYHQAAAAAAAAAAAKQR1SRjcuTNE/r79SrUYD1HUiH5qda9A2NpSdUJXSEWEjEUXSx5IxennK4/ BxjR5kMqYX5AXwC1AAjrwAAAACCmOTRY8Cy7u4/sugJgpCO/mHoLwbZeEQAAAAAAAAAAAOZIsqYbzRmDz1PwaqkAMuLWLgo7eKT0+M9L+DDA29a5vFskXZsNHxfYMtrv B1m9nmok83qfrm9oAAjzoAAAgCDP8OB6s52w8x1N7YG6IzkXMVW5xXg5EQAAAAAAAAAAAHotddzlmB7EIaVN9wbT1Af2bckXDx4NbkjtHoocrXck6e02XQg6Hxe8Q7EK B5pN58sRlHzFOR//AAj7gAAAACBSYArI6b5PONfRi67pcVj7cWTbJI1rCQAAAAAAAAAAAKh1PP1FNXL/yUwEMhXpYBPx8f6iC77T2BDBwemNb+RO2JlHXTkwHBcAFrew B+HS6cYPTvV6+snbAAkDYAAAwCD/aBvTWiuCtoAhxbQRpupP82voj93gFgAAAAAAAAAAAIDkuWjcUvoGa94vzaFmniQN1z6cVysCVsBwbetPXZvp5rlZXdGjGxdcc04A CCrDlIUnc1LZ8Nj7AAkLQAAA/z9FV3KiSlGDfga9ToHe8ysq0WioRw7iEQAAAAAAAAAAANUoIlkSoxVo6aV0yNQwBWNFJQHWUc5jhRxig6Tw4XsQVS5rXT4hGherRNd9 CHfrzMTnNH4hN5sYAAkTIAAA/z+HbkJNgw5nAYXntDFX57V72pTVvvZIDAAAAAAAAAAAAIaAZN99p5iv3Uy5KzLVIn4oJdXuWwQtoiPuVvR37fNGt+h7XfWrFxeUDV+5 CM0W0f4hiP5fWVciAAkbAAAAACD949mnNSIOTWH92aZDY80eG/VA3HF4DwAAAAAAAAAAAHz7539g8IjaBVxv7M2N+/bPcmY6Nik4r0IZMxOPFbbAkSCNXSQPFhcRiJ3V CSh7DyaX/kiv4GZrAAki4AAAQCBkJtVoaqbUq7I+EEmnUlsQDTY688BYFQAAAAAAAAAAAN8JpiIqwffNT/xQxjdHpDrq3zyloPsBWmA2IQWQQO0LdzufXVyjFReQWFDz CYWm6xCIhvXC3VCXAAkqwAAgACBa35Ut2KFYNsnRSMn44AHqcTe/OkZ2BgAAAAAAAAAAAF4Xg+DOwujvmF1htTEQCwMy9wsWY5yhtvG2eTnKXEEkXcmwXd+OFBc2QzNB Cee2T+SyTIWzqlyJAAkyoAAAACAwzYQY7c/FVOgHRQts7iXBTtoSZjkkDgAAAAAAAAAAAMBIbtwe998H0gZ6M6rpIxqKxjCjoZalWQSeOQmxxfDZHKnEXdEgFhcHWk2u CkLRjbL/8nH1LHItAAk6gAAAACCOJE0sVbxAPKpdbq8PkiFw5BPrHgL7AgAAAAAAAAAAAOA7TZ33LY2yMqILsv81xDOpnxRn85H3W19iGA2W8G1qpMTWXT6yFRee+RYz Cp+8ywGs/RHkC2XvAAlCYAAAACBNintTpR6/Rnifd3mKRLpTtkf3SpB7BgAAAAAAAAAAAHMiuQ07JARU4fxwJSnQB4yP90UIdJITeDi20Qyg1qGFUWLpXdLbFRfl/eXh Cvv3cMfIKnGruuwSAAlKQAAAQCDg8QXQk2sKxfrBy12fCB1tGy/PfOAKCAAAAAAAAAAAAOiNwY4aE0xXaHdQI2qJS2bk7RCoBCNjsq8gyukcuqQlnb77XdC8FRdSYvKb C1i2W21eJGmELC2pAAlSIAAAACBhQw6GlSeLc+kpkzn8iZZFwb5N8t35CAAAAAAAAAAAAEEoFPY0abk13Y9PYgozJiGsehGP8W+ysLDDiBRgvvnxExUNXvJlFBcVxSxe C7uMRcpKNSmZq1xhAAlaAAAAgCBXprmLUh7/IK8/40oeg7BWe7seVLqsCgAAAAAAAAAAABBSeAj6PaVvE7iUEenN6XcKknPUytIDifV0wS1sL3Ow7VEeXngMExdc/Jps DCViZM3ipYUuK6mLAAlh4ADA/z/MI1qZk8vf6Ti1S5hQSFxGtqQI3SP+EAAAAAAAAAAAAG15Ry3Qz4Cf9jJWW23i+wUfX4egal2vebuVQS81f0NDU/QvXv8yEhdhE5lJ DJQooZqbnri+wMP4AAlpwAAA/z9MIHjQOI44RP5iQXI+lUMHS9OpdMFmEQAAAAAAAAAAAAtDll4GoIe0dybzWqiG31YwBXIVu0alghYkPiNlZawpYFNCXtQaEheBuYI2 DQOCnM/692T/c/vpAAlxoAAgACBpKU8xBXETUdlfq3ziC5HqY2sbxpLaBgAAAAAAAAAAAC2Podb2UVE3Lz98swMLSdirCyqr/Lp6GODK5wBbDTIfutpUXrwsEhd5M2Bk DXJv6TyZC6dJNCJfAAl5gAAAwCC821K+00n7cVERL9d6Jzo0wkE8NSY/BAAAAAAAAAAAANEvjjQwmKBYHwf7qsdzHSrM+7AC4wGhHxMBliBwDnPi7SJmXhkBERcyCjYC Dej8cVmdICmGlGmsAAmBYAAAgCAMNQrVauCVnJhLXbUm+wi+s6FYCIDGCwAAAAAAAAAAAHSgbycFfzUIokW6wOwg6yoHf88c9CrCsoU3LGXqo65Awhh8XkE7FBcTwPq1 Dkyiv/CRkmMKmkEzAAmJQADgACBD4xnvCIUw3EDxERdHu3/bAmPjwJ15DAAAAAAAAAAAAHiPGbRWrMMMyL6BA6eaJfNIo5XiUMXo0B7u72S1QqDZLpGNXrwgExchnuav DrYJPtE6p0g5WpqUAAmRIAAAACAUOgCoJeMzIDa34vdMEznyR1cSUIbpDQAAAAAAAAAAAKlIg5av2zB0dTVJcJhbennqbQ9kisot+0OheDaM0jzyNJueXjOjEReyl0xD DyhWrFlPqEc/c9+uAAmZAAAgACDcrvMMuRSy9GQxh8BFNt6wIDWYhvU4EQAAAAAAAAAAABVOP5QKoWx9atpYrUh6L+UeYRrgD4sj3ZOBngyn3WuRAeawXjl6EReMHTLH D5uvExILinkBRJMsAAmg4ADg/yfO2pclcPlRE5XVKCPZeF63mn30v+yHDgAAAAAAAAAAAPaDA4g+h9tjjVWtJ587WMHb07Mcfa1BoDJCANQd+z94wJDEXvaXEhf5l8cN EAgacoNqeblmwryDAAmowAAAACBrw4jKPLZgZpVWF50edwCXFDSCydrEBwAAAAAAAAAAAJzD7FqR8uJdE2JDv+AVjncVcclu70I3QzG/aH2H/bHffOnYXjV/FBdyiZsc EGp3iH6JaGGCuz8LAAmwoAAAQCAgBboCfncZ/s4WkKcsLjW+V4vlPQoDAwAAAAAAAAAAALs0nxXJbP7VZ+Fc3dhRpL6RN6QfYphMw8iCNdqEKGL6e/joXvLUERcyQVAj ENuF9IkxgIodMy/3AAm4gAAAACCTOuNmt+7x2vxFMkHRbDxRElZiABP9DAAAAAAAAAAAAAERAwV0KNK6qPCLxwDpQCkrbHi2jFc/Kldv0N5pJMkE+XP7XhnVERet5rYy EUyU1REypM3OngUzAAnAYAAAgCB898coFK4fvpBHu54gmeu1eEuspbAwCAAAAAAAAAAAAHQ93L5j0nbFj5FI6biHU0sUuxN+qzGCaK4mc2P3nQYZpEoMXxU6EBeWCMCJ EcjRYNcojJYAgaG7AAnIQAAAACCjAhx+1E7mE4bleOFpzqtEG3mGLs/VDgAAAAAAAAAAAFr7gwTRRSFysbOK9K7JXVQxXaq23POoYGxre0mJgYM5pEwfX/i0EBckQiPZ EkF8pCJpsBtpkPH6AAnQIADg/z9tCoMocT196c1IBJkoKNhgXJT3k1ysCgAAAAAAAAAAAPESYEteACbYsgdEuoUXLrDqFqut46D/RLAoOw+IITSkU6cxX6ybEBdqMa4t ErrgKoeevYMpfx+WAAnYAADg/y/Zjrsqarpkd5PIhR21HJ55cSMyymaaBAAAAAAAAAAAAKPhdir1YiPGjqsC309lxumCEY8aSu2HOTrVU6Ihc4oti3tDX+oHEBeswM1c EzihxS/9MDJCHTIfAAnf4AAAQCAGq48tARXjK5m5ygIMjtFJqlySqsdXBgAAAAAAAAAAAPJaEbuOk1ZnpJ4y6SR6yn+dY6fB5QaJHhb6QWgMXit2KCxWXxI6EBdJ5KeT E7TgpleovIarz4zNAAnnwADg/zelTTDUGas2KxqgEH2WV5EilGEY8VASBgAAAAAAAAAAAJVJFZNI4Kxh1AUjFLdY7k6MLVY47YSfxE1acJIuEMlMpcRmX6qSDhc6QrMB FD83ZUplM/sW8MloAAnvoAAAQCBUTUlenrKovQvyeNaLUYJBmxfT5hcuDQAAAAAAAAAAAIaxf5jfjr8MifLPauOJeh6OWDYxSUUPluG5E8hVh+rrWD95X96VDhf7Be0Q FMlwaWsoL8M1y3sGAAn3gAAAICD2It4VF1dZC7XR51SjkuBPy2c1ot7iBQAAAAAAAAAAAOto2NUucHczD9ZpnC5X1L05HcifvGpMh8gwIt7mZ2DW3BWLX04TDhdKX1k6 FVin/nTPxZEaUhFJAAn/YADg/zdLnpRRoj5nb9aeuWAA1TG1ZV/ig3PhBQAAAAAAAAAAAP7jjrVesvd94nsFrp03Zj496k0mgpUa+z2F/FaP1R7omhShXzPEEBcdNBS1 FdDmSXs4LVrTs1CaAAoHQAAAACDvuQVfiWazEOmUBkUubcV9HD6bQ4YDCQAAAAAAAAAAAIpAWQyuvdv0i6cfyxgL/cqONcYtUdP6eDTQOlOgmOc5h7CyX93+DxdiV5Q4 Fk7wqQvRa3I8HIU0AAoPIAAAACClMk0cUuhV+1lsN2AW0bLOzyOjOxhvBAAAAAAAAAAAAHvW0itHeUSYGKmni03ZQ4CQz/twR/T8aM+iCLhkzgVuGaXDX1axDheZrIxe FtgmMCmxHYDfZVDnAAoXAADg/ydUahAkyDODrqzZ/A9vg1WNyOWPKEjSBgAAAAAAAAAAAIF1YVHIYoFF9C7GrkXnuXeYn2j+2cNXhc5hrQ9iuMiyW5nWX3ITDxcvOs69 F13fK/nssCNHFGj5AAoe4AAAQCDp4pHELBlNIcdX51i41BvbBjtbfHsECgAAAAAAAAAAAIcgs1NcVR2veWwlmJK7rkICXTqwbukwNfe5scvzGUskkCDpXxciDxc/NQHe F+MYpAKDBczv4JWTAAomwAAAACBS1/Bd73vGgmzadPW9r4Vf4TzSyKulDgAAAAAAAAAAALu0Rd+bUPZVV1LffkjQnE9dvY5cjr8wr/G1X47UTZ6Atcr5X6GoDRcUdkaH GHax93pF7gaxq0XZAAouoAAAoCCqU8YdV933DGrl49Dn/tEwGvNAEx6tCAAAAAAAAAAAAFYHby9tEq0dadLbjJDzL1bca6HRgkdDHRIWmM7snd7JCQ8MYFeEDRdjn+Zp GQvX6N/GOKeq1c5DAAo2gAAAACC4g9jQzny0CwJI9mI2feK6gSKFkJPyBAAAAAAAAAAAACYuf376jLmC/rlDvhOOvKnh4emKgnHG9GJw3WPHFB2b//0dYLkhDRcLx0qQ GaVdn56xd5nImNufAAo+YAAAACAHKVwriCjnfwq5aifRmVw5M7Iuu3iqCAAAAAAAAAAAAMPe34CBzFKsON/ycucEEY0p3fNEZHYcCGAK7L4ZvypzBDUwYOP0DBd5TI71 GkD2FFHSldaV1ylFAApGQAAAoCCuIeiM9eCK9ZhitwDrjbRFc/59vyyECwAAAAAAAAAAAIXwebYkSFSbeLbTeMKy9Qdj4KCSw+pp3ozDK+//1/8PzOhCYIwfDRcwPZvi ================================================ FILE: core/src/main/resources/xmr_local.seednodes ================================================ # nodeaddress.onion:port [(@owner,@backup)] localhost:2002 (@devtest1) localhost:2003 (@devtest2) ================================================ FILE: core/src/main/resources/xmr_mainnet.seednodes ================================================ # nodeaddress.onion:port [(@owner,@backup)] k6wctqd5l2nhmietzs6zg4pric3ukeg7lazzz67ttbl75qwfr2q4yvqd.onion:2002(@anon1) 4gmfgn22tll7ajw3tdp7nru3fvgh5ukt7w53kfv5ymijldivsqtbzdqd.onion:2003(@anon1) 2svrnte3rr73wk45lw6eofifb2orng6fo2xq7jnwxlvcvspqsjbuztad.onion:1002(@anon2) im6hcl7hknvsrsns2newv4orfv3kd2ly5yvqtbfkiyzoohscyp5htgqd.onion:2002(@anon6) xephvvzd3orepnny7lbia4nkwie5t7wjivlvvz5lhbsck7ubavystead.onion:9992(@anon3 ,@s0) g4z6oi2wf62nwztwve6qe2hqswj4ezpom6hn7cuy5cxaidey4us76bid.onion:9993(@anon3 ,@s0) z47tltuwytd5icqq4hni2ammvlugp6pcwqboeu7ngawruualxjjuu3ad.onion:9992(@anon3 ,@s3) hxb5h34hjgyraycrrxlz5ar2q77mjgondzicwzayqwwvuaepssrn5zyd.onion:9993(@anon3 ,@s3) u6wwec5ddxswwyrz7rgzuiwowf33llab57y3xzmwwxvsofq2w4m6ihad.onion:1002(@anon4) oyui76zng5nhml6xlrysfmyqgoutlhwr3h7p35rlzr5fvtg6tzgavvqd.onion:9992(@anon5) nri52xbvmga4lm7yg7cwszxz2eamt6bb3pgi6bhkzexgk4flb5e5p7qd.onion:9993(@anon5) ================================================ FILE: core/src/main/resources/xmr_stagenet.seednodes ================================================ # nodeaddress.onion:port [(@owner)] dl57jitswby4yhzpqpu7pwq6iyqg2x6vkio73araparbftlqoqxhvqad.onion:2002 (@devtest1) 3cqlkowdu766sto5wrdqpntpsi7kezwkkakc532i6jeiyu7hha726ead.onion:3003 (@devtest1) ================================================ FILE: core/src/test/java/haveno/core/account/sign/SignedWitnessServiceTest.java ================================================ /* * 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 . */ package haveno.core.account.sign; import com.google.common.base.Charsets; import haveno.common.crypto.CryptoException; import haveno.common.crypto.KeyRing; import haveno.common.crypto.Sig; import haveno.common.util.Utilities; import haveno.core.account.witness.AccountAgeWitness; import haveno.core.filter.FilterManager; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.network.p2p.P2PService; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import haveno.network.p2p.storage.persistence.AppendOnlyDataStoreService; import org.bitcoinj.core.ECKey; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.math.BigInteger; import java.security.KeyPair; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Date; import static haveno.core.account.sign.SignedWitness.VerificationMethod.ARBITRATOR; import static haveno.core.account.sign.SignedWitness.VerificationMethod.TRADE; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyBoolean; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class SignedWitnessServiceTest { private SignedWitnessService signedWitnessService; private byte[] account1DataHash; private byte[] account2DataHash; private byte[] account3DataHash; private AccountAgeWitness aew1; private AccountAgeWitness aew2; private AccountAgeWitness aew3; private byte[] signature1; private byte[] signature2; private byte[] signature3; private byte[] signer1PubKey; private byte[] signer2PubKey; private byte[] signer3PubKey; private byte[] witnessOwner1PubKey; private byte[] witnessOwner2PubKey; private byte[] witnessOwner3PubKey; private long date1; private long date2; private long date3; private long tradeAmount1; private long tradeAmount2; private long tradeAmount3; private long SIGN_AGE_1 = SignedWitnessService.SIGNER_AGE_DAYS * 3 + 5; private long SIGN_AGE_2 = SignedWitnessService.SIGNER_AGE_DAYS * 2 + 4; private long SIGN_AGE_3 = SignedWitnessService.SIGNER_AGE_DAYS + 3; private KeyRing keyRing; private P2PService p2pService; private FilterManager filterManager; private ECKey arbitrator1Key; KeyPair peer1KeyPair; KeyPair peer2KeyPair; KeyPair peer3KeyPair; @BeforeEach public void setup() throws Exception { AppendOnlyDataStoreService appendOnlyDataStoreService = mock(AppendOnlyDataStoreService.class); ArbitratorManager arbitratorManager = mock(ArbitratorManager.class); when(arbitratorManager.isPublicKeyInList(any())).thenReturn(true); keyRing = mock(KeyRing.class); p2pService = mock(P2PService.class); filterManager = mock(FilterManager.class); signedWitnessService = new SignedWitnessService(keyRing, p2pService, arbitratorManager, null, appendOnlyDataStoreService, null, filterManager); account1DataHash = org.bitcoinj.core.Utils.sha256hash160(new byte[]{1}); account2DataHash = org.bitcoinj.core.Utils.sha256hash160(new byte[]{2}); account3DataHash = org.bitcoinj.core.Utils.sha256hash160(new byte[]{3}); long account1CreationTime = getTodayMinusNDays(SIGN_AGE_1 + 1); long account2CreationTime = getTodayMinusNDays(SIGN_AGE_2 + 1); long account3CreationTime = getTodayMinusNDays(SIGN_AGE_3 + 1); aew1 = new AccountAgeWitness(account1DataHash, account1CreationTime); aew2 = new AccountAgeWitness(account2DataHash, account2CreationTime); aew3 = new AccountAgeWitness(account3DataHash, account3CreationTime); arbitrator1Key = new ECKey(); peer1KeyPair = Sig.generateKeyPair(); peer2KeyPair = Sig.generateKeyPair(); peer3KeyPair = Sig.generateKeyPair(); signature1 = arbitrator1Key.signMessage(Utilities.encodeToHex(account1DataHash)).getBytes(Charsets.UTF_8); signature2 = Sig.sign(peer1KeyPair.getPrivate(), Utilities.encodeToHex(account2DataHash).getBytes(Charsets.UTF_8)); signature3 = Sig.sign(peer2KeyPair.getPrivate(), Utilities.encodeToHex(account3DataHash).getBytes(Charsets.UTF_8)); date1 = getTodayMinusNDays(SIGN_AGE_1); date2 = getTodayMinusNDays(SIGN_AGE_2); date3 = getTodayMinusNDays(SIGN_AGE_3); signer1PubKey = arbitrator1Key.getPubKey(); signer2PubKey = Sig.getPublicKeyBytes(peer1KeyPair.getPublic()); signer3PubKey = Sig.getPublicKeyBytes(peer2KeyPair.getPublic()); witnessOwner1PubKey = Sig.getPublicKeyBytes(peer1KeyPair.getPublic()); witnessOwner2PubKey = Sig.getPublicKeyBytes(peer2KeyPair.getPublic()); witnessOwner3PubKey = Sig.getPublicKeyBytes(peer3KeyPair.getPublic()); tradeAmount1 = 1000; tradeAmount2 = 1001; tradeAmount3 = 1001; } @Test public void testIsValidAccountAgeWitnessOk() { SignedWitness sw1 = new SignedWitness(ARBITRATOR, account1DataHash, signature1, signer1PubKey, witnessOwner1PubKey, date1, tradeAmount1); SignedWitness sw2 = new SignedWitness(TRADE, account2DataHash, signature2, signer2PubKey, witnessOwner2PubKey, date2, tradeAmount2); SignedWitness sw3 = new SignedWitness(TRADE, account3DataHash, signature3, signer3PubKey, witnessOwner3PubKey, date3, tradeAmount3); signedWitnessService.addToMap(sw1); signedWitnessService.addToMap(sw2); signedWitnessService.addToMap(sw3); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew3)); } @Test public void testIsValidAccountAgeWitnessArbitratorSignatureProblem() { signature1 = new byte[]{1, 2, 3}; SignedWitness sw1 = new SignedWitness(ARBITRATOR, account1DataHash, signature1, signer1PubKey, witnessOwner1PubKey, date1, tradeAmount1); SignedWitness sw2 = new SignedWitness(TRADE, account2DataHash, signature2, signer2PubKey, witnessOwner2PubKey, date2, tradeAmount2); SignedWitness sw3 = new SignedWitness(TRADE, account3DataHash, signature3, signer3PubKey, witnessOwner3PubKey, date3, tradeAmount3); signedWitnessService.addToMap(sw1); signedWitnessService.addToMap(sw2); signedWitnessService.addToMap(sw3); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); } @Test public void testIsValidAccountAgeWitnessPeerSignatureProblem() { signature2 = new byte[]{1, 2, 3}; SignedWitness sw1 = new SignedWitness(ARBITRATOR, account1DataHash, signature1, signer1PubKey, witnessOwner1PubKey, date1, tradeAmount1); SignedWitness sw2 = new SignedWitness(TRADE, account2DataHash, signature2, signer2PubKey, witnessOwner2PubKey, date2, tradeAmount2); SignedWitness sw3 = new SignedWitness(TRADE, account3DataHash, signature3, signer3PubKey, witnessOwner3PubKey, date3, tradeAmount3); signedWitnessService.addToMap(sw1); signedWitnessService.addToMap(sw2); signedWitnessService.addToMap(sw3); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); } @Test public void testIsValidSelfSignatureOk() throws Exception { KeyPair peer1KeyPair = Sig.generateKeyPair(); signer2PubKey = Sig.getPublicKeyBytes(peer1KeyPair.getPublic()); signature2 = Sig.sign(peer1KeyPair.getPrivate(), Utilities.encodeToHex(account2DataHash).getBytes(Charsets.UTF_8)); signature3 = Sig.sign(peer1KeyPair.getPrivate(), Utilities.encodeToHex(account3DataHash).getBytes(Charsets.UTF_8)); SignedWitness sw1 = new SignedWitness(ARBITRATOR, account1DataHash, signature1, signer1PubKey, signer2PubKey, date1, tradeAmount1); SignedWitness sw2 = new SignedWitness(TRADE, account2DataHash, signature2, signer2PubKey, signer2PubKey, date2, tradeAmount2); SignedWitness sw3 = new SignedWitness(TRADE, account3DataHash, signature3, signer2PubKey, signer2PubKey, date3, tradeAmount3); signedWitnessService.addToMap(sw1); signedWitnessService.addToMap(sw2); signedWitnessService.addToMap(sw3); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew3)); } @Test public void testIsValidSimpleLoopSignatureProblem() throws Exception { // A reasonable case where user1 is signed by user2 and later switches account and the new // account gets signed by user2. This is not allowed. KeyPair peer1KeyPair = Sig.generateKeyPair(); KeyPair peer2KeyPair = Sig.generateKeyPair(); byte[] user1PubKey = Sig.getPublicKeyBytes(peer1KeyPair.getPublic()); byte[] user2PubKey = Sig.getPublicKeyBytes(peer2KeyPair.getPublic()); signature2 = Sig.sign(peer1KeyPair.getPrivate(), Utilities.encodeToHex(account2DataHash).getBytes(Charsets.UTF_8)); signature3 = Sig.sign(peer2KeyPair.getPrivate(), Utilities.encodeToHex(account3DataHash).getBytes(Charsets.UTF_8)); SignedWitness sw1 = new SignedWitness(ARBITRATOR, account1DataHash, signature1, signer1PubKey, user1PubKey, date1, tradeAmount1); SignedWitness sw2 = new SignedWitness(TRADE, account2DataHash, signature2, user1PubKey, user2PubKey, date2, tradeAmount2); SignedWitness sw3 = new SignedWitness(TRADE, account3DataHash, signature3, user2PubKey, user1PubKey, date3, tradeAmount3); signedWitnessService.addToMap(sw1); signedWitnessService.addToMap(sw2); signedWitnessService.addToMap(sw3); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); } @Test public void testIsValidAccountAgeWitnessDateTooSoonProblem() { date3 = getTodayMinusNDays(SIGN_AGE_2 - 1); SignedWitness sw1 = new SignedWitness(ARBITRATOR, account1DataHash, signature1, signer1PubKey, witnessOwner1PubKey, date1, tradeAmount1); SignedWitness sw2 = new SignedWitness(TRADE, account2DataHash, signature2, signer2PubKey, witnessOwner2PubKey, date2, tradeAmount2); SignedWitness sw3 = new SignedWitness(TRADE, account3DataHash, signature3, signer3PubKey, witnessOwner3PubKey, date3, tradeAmount3); signedWitnessService.addToMap(sw1); signedWitnessService.addToMap(sw2); signedWitnessService.addToMap(sw3); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); } @Test public void testIsValidAccountAgeWitnessDateTooLateProblem() { date3 = getTodayMinusNDays(3); SignedWitness sw1 = new SignedWitness(ARBITRATOR, account1DataHash, signature1, signer1PubKey, witnessOwner1PubKey, date1, tradeAmount1); SignedWitness sw2 = new SignedWitness(TRADE, account2DataHash, signature2, signer2PubKey, witnessOwner2PubKey, date2, tradeAmount2); SignedWitness sw3 = new SignedWitness(TRADE, account3DataHash, signature3, signer3PubKey, witnessOwner3PubKey, date3, tradeAmount3); signedWitnessService.addToMap(sw1); signedWitnessService.addToMap(sw2); signedWitnessService.addToMap(sw3); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); } @Test public void testIsValidAccountAgeWitnessEndlessLoop() throws Exception { byte[] account1DataHash = org.bitcoinj.core.Utils.sha256hash160(new byte[]{1}); byte[] account2DataHash = org.bitcoinj.core.Utils.sha256hash160(new byte[]{2}); byte[] account3DataHash = org.bitcoinj.core.Utils.sha256hash160(new byte[]{3}); long account1CreationTime = getTodayMinusNDays(SIGN_AGE_1 + 1); long account2CreationTime = getTodayMinusNDays(SIGN_AGE_2 + 1); long account3CreationTime = getTodayMinusNDays(SIGN_AGE_3 + 1); AccountAgeWitness aew1 = new AccountAgeWitness(account1DataHash, account1CreationTime); AccountAgeWitness aew2 = new AccountAgeWitness(account2DataHash, account2CreationTime); AccountAgeWitness aew3 = new AccountAgeWitness(account3DataHash, account3CreationTime); KeyPair peer1KeyPair = Sig.generateKeyPair(); KeyPair peer2KeyPair = Sig.generateKeyPair(); KeyPair peer3KeyPair = Sig.generateKeyPair(); String account1DataHashAsHexString = Utilities.encodeToHex(account1DataHash); String account2DataHashAsHexString = Utilities.encodeToHex(account2DataHash); String account3DataHashAsHexString = Utilities.encodeToHex(account3DataHash); byte[] signature1 = Sig.sign(peer3KeyPair.getPrivate(), account1DataHashAsHexString.getBytes(Charsets.UTF_8)); byte[] signature2 = Sig.sign(peer1KeyPair.getPrivate(), account2DataHashAsHexString.getBytes(Charsets.UTF_8)); byte[] signature3 = Sig.sign(peer2KeyPair.getPrivate(), account3DataHashAsHexString.getBytes(Charsets.UTF_8)); byte[] signer1PubKey = Sig.getPublicKeyBytes(peer3KeyPair.getPublic()); byte[] signer2PubKey = Sig.getPublicKeyBytes(peer1KeyPair.getPublic()); byte[] signer3PubKey = Sig.getPublicKeyBytes(peer2KeyPair.getPublic()); byte[] witnessOwner1PubKey = Sig.getPublicKeyBytes(peer1KeyPair.getPublic()); byte[] witnessOwner2PubKey = Sig.getPublicKeyBytes(peer2KeyPair.getPublic()); byte[] witnessOwner3PubKey = Sig.getPublicKeyBytes(peer3KeyPair.getPublic()); long date1 = getTodayMinusNDays(SIGN_AGE_1); long date2 = getTodayMinusNDays(SIGN_AGE_2); long date3 = getTodayMinusNDays(SIGN_AGE_3); long tradeAmount1 = 1000; long tradeAmount2 = 1001; long tradeAmount3 = 1001; SignedWitness sw1 = new SignedWitness(TRADE, account1DataHash, signature1, signer1PubKey, witnessOwner1PubKey, date1, tradeAmount1); SignedWitness sw2 = new SignedWitness(TRADE, account2DataHash, signature2, signer2PubKey, witnessOwner2PubKey, date2, tradeAmount2); SignedWitness sw3 = new SignedWitness(TRADE, account3DataHash, signature3, signer3PubKey, witnessOwner3PubKey, date3, tradeAmount3); signedWitnessService.addToMap(sw1); signedWitnessService.addToMap(sw2); signedWitnessService.addToMap(sw3); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); } @Test public void testIsValidAccountAgeWitnessLongLoop() throws Exception { AccountAgeWitness aew = null; KeyPair signerKeyPair; KeyPair signedKeyPair = Sig.generateKeyPair(); int iterations = 1002; for (int i = 0; i < iterations; i++) { byte[] accountDataHash = org.bitcoinj.core.Utils.sha256hash160(String.valueOf(i).getBytes(Charsets.UTF_8)); long accountCreationTime = getTodayMinusNDays((iterations - i) * (SignedWitnessService.SIGNER_AGE_DAYS + 1)); aew = new AccountAgeWitness(accountDataHash, accountCreationTime); String accountDataHashAsHexString = Utilities.encodeToHex(accountDataHash); byte[] signature; byte[] signerPubKey; if (i == 0) { // use arbitrator key ECKey arbitratorKey = new ECKey(); signedKeyPair = Sig.generateKeyPair(); String signature1String = arbitratorKey.signMessage(accountDataHashAsHexString); signature = signature1String.getBytes(Charsets.UTF_8); signerPubKey = arbitratorKey.getPubKey(); } else { signerKeyPair = signedKeyPair; signedKeyPair = Sig.generateKeyPair(); signature = Sig.sign(signedKeyPair.getPrivate(), accountDataHashAsHexString.getBytes(Charsets.UTF_8)); signerPubKey = Sig.getPublicKeyBytes(signerKeyPair.getPublic()); } byte[] witnessOwnerPubKey = Sig.getPublicKeyBytes(signedKeyPair.getPublic()); long date = getTodayMinusNDays((iterations - i) * (SignedWitnessService.SIGNER_AGE_DAYS + 1)); SignedWitness sw = new SignedWitness(i == 0 ? ARBITRATOR : TRADE, accountDataHash, signature, signerPubKey, witnessOwnerPubKey, date, tradeAmount1); signedWitnessService.addToMap(sw); } assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew)); } private long getTodayMinusNDays(long days) { return Instant.ofEpochMilli(new Date().getTime()).minus(days, ChronoUnit.DAYS).toEpochMilli(); } @Test public void testSignAccountAgeWitness_withTooLowTradeAmount() throws CryptoException { long accountCreationTime = getTodayMinusNDays(SIGN_AGE_1 + 1); KeyPair peerKeyPair = Sig.generateKeyPair(); KeyPair signerKeyPair = Sig.generateKeyPair(); when(keyRing.getSignatureKeyPair()).thenReturn(signerKeyPair); AccountAgeWitness accountAgeWitness = new AccountAgeWitness(account1DataHash, accountCreationTime); signedWitnessService.signAndPublishAccountAgeWitness(BigInteger.ZERO, accountAgeWitness, peerKeyPair.getPublic()); verify(p2pService, never()).addPersistableNetworkPayload(any(PersistableNetworkPayload.class), anyBoolean()); } @Test public void testSignAccountAgeWitness_withSufficientTradeAmount() throws CryptoException { long accountCreationTime = getTodayMinusNDays(SIGN_AGE_1 + 1); KeyPair peerKeyPair = Sig.generateKeyPair(); KeyPair signerKeyPair = Sig.generateKeyPair(); when(keyRing.getSignatureKeyPair()).thenReturn(signerKeyPair); AccountAgeWitness accountAgeWitness = new AccountAgeWitness(account1DataHash, accountCreationTime); signedWitnessService.signAndPublishAccountAgeWitness(SignedWitnessService.MINIMUM_TRADE_AMOUNT_FOR_SIGNING, accountAgeWitness, peerKeyPair.getPublic()); verify(p2pService, times(1)).addPersistableNetworkPayload(any(PersistableNetworkPayload.class), anyBoolean()); } /* Signed witness tree Each edge in the graph represents one signature Arbitrator | sw1 | sw2 | sw3 */ @Test public void testBanFilterSingleTree() { SignedWitness sw1 = new SignedWitness(ARBITRATOR, account1DataHash, signature1, signer1PubKey, witnessOwner1PubKey, date1, tradeAmount1); SignedWitness sw2 = new SignedWitness(TRADE, account2DataHash, signature2, signer2PubKey, witnessOwner2PubKey, date2, tradeAmount2); SignedWitness sw3 = new SignedWitness(TRADE, account3DataHash, signature3, signer3PubKey, witnessOwner3PubKey, date3, tradeAmount3); signedWitnessService.addToMap(sw1); signedWitnessService.addToMap(sw2); signedWitnessService.addToMap(sw3); // Second account is banned, first account is still a signer but the other two are no longer signers when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); // First account is banned, no accounts in the tree below it are signers when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(false); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); } /* Signed witness trees Each edge in the graph represents one signature Arbitrator | | sw1 sw2 | sw3 */ @Test public void testBanFilterTwoTrees() { // Signer 2 is signed by arbitrator signer2PubKey = arbitrator1Key.getPubKey(); signature2 = arbitrator1Key.signMessage(Utilities.encodeToHex(account2DataHash)).getBytes(Charsets.UTF_8); SignedWitness sw1 = new SignedWitness(ARBITRATOR, account1DataHash, signature1, signer1PubKey, witnessOwner1PubKey, date1, tradeAmount1); SignedWitness sw2 = new SignedWitness(ARBITRATOR, account2DataHash, signature2, signer2PubKey, witnessOwner2PubKey, date2, tradeAmount2); SignedWitness sw3 = new SignedWitness(TRADE, account3DataHash, signature3, signer3PubKey, witnessOwner3PubKey, date3, tradeAmount3); signedWitnessService.addToMap(sw1); signedWitnessService.addToMap(sw2); signedWitnessService.addToMap(sw3); // Only second account is banned, first account is still a signer but the other two are no longer signers when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); // Only first account is banned, account2 and account3 are still signers when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(false); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew3)); } /* Signed witness tree Each edge in the graph represents one signature Arbitrator | | sw1 sw2 \ / sw3 */ @Test public void testBanFilterJoinedTrees() throws Exception { // Signer 2 is signed by arbitrator signer2PubKey = arbitrator1Key.getPubKey(); signature2 = arbitrator1Key.signMessage(Utilities.encodeToHex(account2DataHash)).getBytes(Charsets.UTF_8); // Peer1 owns both account1 and account2 // witnessOwner2PubKey = witnessOwner1PubKey; // peer2KeyPair = peer1KeyPair; // signature3 = Sig.sign(peer2KeyPair.getPrivate(), Utilities.encodeToHex(account3DataHash).getBytes(Charsets.UTF_8)); // sw1 also signs sw3 (not supported yet but a possible addition for a more robust system) var signature3p = Sig.sign(peer1KeyPair.getPrivate(), Utilities.encodeToHex(account3DataHash).getBytes(Charsets.UTF_8)); var signer3pPubKey = witnessOwner1PubKey; var date3p = date3; var tradeAmount3p = tradeAmount3; SignedWitness sw1 = new SignedWitness(ARBITRATOR, account1DataHash, signature1, signer1PubKey, witnessOwner1PubKey, date1, tradeAmount1); SignedWitness sw2 = new SignedWitness(ARBITRATOR, account2DataHash, signature2, signer2PubKey, witnessOwner2PubKey, date2, tradeAmount2); SignedWitness sw3 = new SignedWitness(TRADE, account3DataHash, signature3, signer3PubKey, witnessOwner3PubKey, date3, tradeAmount3); SignedWitness sw3p = new SignedWitness(TRADE, account3DataHash, signature3p, signer3pPubKey, witnessOwner3PubKey, date3p, tradeAmount3p); signedWitnessService.addToMap(sw1); signedWitnessService.addToMap(sw2); signedWitnessService.addToMap(sw3); signedWitnessService.addToMap(sw3p); // First account is banned, the other two are still signers when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew3)); // Second account is banned, the other two are still signers when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(false); when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew3)); // First and second account is banned, the third is no longer a signer when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); } } ================================================ FILE: core/src/test/java/haveno/core/account/sign/SignedWitnessTest.java ================================================ package haveno.core.account.sign; import com.google.common.base.Charsets; import haveno.common.crypto.Sig; import haveno.common.util.Utilities; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.Utils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.time.Instant; import static haveno.core.account.sign.SignedWitness.VerificationMethod.ARBITRATOR; import static haveno.core.account.sign.SignedWitness.VerificationMethod.TRADE; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; public class SignedWitnessTest { private ECKey arbitrator1Key; private byte[] witnessOwner1PubKey; private byte[] witnessHash; private byte[] witnessHashSignature; @BeforeEach public void setUp() { arbitrator1Key = new ECKey(); witnessOwner1PubKey = Sig.getPublicKeyBytes(Sig.generateKeyPair().getPublic()); witnessHash = Utils.sha256hash160(new byte[]{1}); witnessHashSignature = arbitrator1Key.signMessage(Utilities.encodeToHex(witnessHash)).getBytes(Charsets.UTF_8); } @Test public void testProtoRoundTrip() { SignedWitness signedWitness = new SignedWitness(ARBITRATOR, witnessHash, witnessHashSignature, arbitrator1Key.getPubKey(), witnessOwner1PubKey, Instant.now().getEpochSecond(), 100); assertEquals(signedWitness, SignedWitness.fromProto(signedWitness.toProtoMessage().getSignedWitness())); } @Test public void isImmutable() { byte[] signerPubkey = arbitrator1Key.getPubKey(); SignedWitness signedWitness = new SignedWitness(TRADE, witnessHash, witnessHashSignature, signerPubkey, witnessOwner1PubKey, Instant.now().getEpochSecond(), 100); byte[] originalWitnessHash = signedWitness.getAccountAgeWitnessHash().clone(); witnessHash[0] += 1; assertArrayEquals(originalWitnessHash, signedWitness.getAccountAgeWitnessHash()); byte[] originalWitnessHashSignature = signedWitness.getSignature().clone(); witnessHashSignature[0] += 1; assertArrayEquals(originalWitnessHashSignature, signedWitness.getSignature()); byte[] originalSignerPubkey = signedWitness.getSignerPubKey().clone(); signerPubkey[0] += 1; assertArrayEquals(originalSignerPubkey, signedWitness.getSignerPubKey()); byte[] originalwitnessOwner1PubKey = signedWitness.getWitnessOwnerPubKey().clone(); witnessOwner1PubKey[0] += 1; assertArrayEquals(originalwitnessOwner1PubKey, signedWitness.getWitnessOwnerPubKey()); } } ================================================ FILE: core/src/test/java/haveno/core/account/witness/AccountAgeWitnessServiceTest.java ================================================ /* * 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 . */ package haveno.core.account.witness; import haveno.common.crypto.CryptoException; import haveno.common.crypto.Hash; import haveno.common.crypto.KeyRing; import haveno.common.crypto.KeyStorage; import haveno.common.crypto.PubKeyRing; import haveno.common.crypto.Sig; import haveno.common.util.Utilities; import haveno.core.account.sign.SignedWitness; import haveno.core.account.sign.SignedWitnessService; import haveno.core.filter.FilterManager; import haveno.core.locale.CountryUtil; import haveno.core.offer.OfferPayload; import haveno.core.payment.ChargeBackRisk; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.SepaAccountPayload; import haveno.core.support.SupportType; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeResult; import haveno.core.support.dispute.arbitration.TraderDataItem; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.trade.Contract; import haveno.core.trade.HavenoUtils; import haveno.network.p2p.P2PService; import haveno.network.p2p.storage.persistence.AppendOnlyDataStoreService; import org.bitcoinj.core.ECKey; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.io.File; import java.io.IOException; import java.security.KeyPair; import java.security.PublicKey; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; import java.util.concurrent.TimeUnit; import static haveno.core.payment.payload.PaymentMethod.getPaymentMethod; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; // Restricted default Java security policy on Travis does not allow long keys, so test fails. // Using Utilities.removeCryptographyRestrictions(); did not work. //@Ignore public class AccountAgeWitnessServiceTest { private PublicKey publicKey; private KeyPair keypair; private SignedWitnessService signedWitnessService; private AccountAgeWitnessService service; private ChargeBackRisk chargeBackRisk; private FilterManager filterManager; private File dir1; private File dir2; private File dir3; @BeforeEach public void setup() throws IOException { KeyRing keyRing = mock(KeyRing.class); setupService(keyRing); keypair = Sig.generateKeyPair(); publicKey = keypair.getPublic(); // Setup temp storage dir dir1 = makeDir("temp_tests1"); dir2 = makeDir("temp_tests1"); dir3 = makeDir("temp_tests1"); } private void setupService(KeyRing keyRing) { chargeBackRisk = mock(ChargeBackRisk.class); AppendOnlyDataStoreService dataStoreService = mock(AppendOnlyDataStoreService.class); P2PService p2pService = mock(P2PService.class); ArbitratorManager arbitratorManager = mock(ArbitratorManager.class); when(arbitratorManager.isPublicKeyInList(any())).thenReturn(true); AppendOnlyDataStoreService appendOnlyDataStoreService = mock(AppendOnlyDataStoreService.class); filterManager = mock(FilterManager.class); signedWitnessService = new SignedWitnessService(keyRing, p2pService, arbitratorManager, null, appendOnlyDataStoreService, null, filterManager); service = new AccountAgeWitnessService(null, null, null, signedWitnessService, chargeBackRisk, null, dataStoreService, null, filterManager); } private File makeDir(String name) throws IOException { var dir = File.createTempFile(name, ""); dir.delete(); dir.mkdir(); return dir; } @Disabled @Test public void testIsTradeDateAfterReleaseDate() { Date ageWitnessReleaseDate = new GregorianCalendar(2017, Calendar.OCTOBER, 23).getTime(); Date tradeDate = new GregorianCalendar(2017, Calendar.NOVEMBER, 1).getTime(); assertTrue(service.isDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate, errorMessage -> { })); tradeDate = new GregorianCalendar(2017, Calendar.OCTOBER, 23).getTime(); assertTrue(service.isDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate, errorMessage -> { })); tradeDate = new GregorianCalendar(2017, Calendar.OCTOBER, 22, 0, 0, 1).getTime(); assertTrue(service.isDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate, errorMessage -> { })); tradeDate = new GregorianCalendar(2017, Calendar.OCTOBER, 22).getTime(); assertFalse(service.isDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate, errorMessage -> { })); tradeDate = new GregorianCalendar(2017, Calendar.OCTOBER, 21).getTime(); assertFalse(service.isDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate, errorMessage -> { })); } @Disabled @Test public void testVerifySignatureOfNonce() throws CryptoException { byte[] nonce = new byte[]{0x01}; byte[] signature = Sig.sign(keypair.getPrivate(), nonce); assertTrue(service.verifySignature(publicKey, nonce, signature, errorMessage -> { })); assertFalse(service.verifySignature(publicKey, nonce, new byte[]{0x02}, errorMessage -> { })); assertFalse(service.verifySignature(publicKey, new byte[]{0x03}, signature, errorMessage -> { })); assertFalse(service.verifySignature(publicKey, new byte[]{0x02}, new byte[]{0x04}, errorMessage -> { })); } @Test public void testArbitratorSignWitness() { KeyRing buyerKeyRing = new KeyRing(new KeyStorage(dir1), null, true); KeyRing sellerKeyRing = new KeyRing(new KeyStorage(dir2), null, true); // Setup dispute for arbitrator to sign both sides List disputes = new ArrayList<>(); PubKeyRing buyerPubKeyRing = buyerKeyRing.getPubKeyRing(); PubKeyRing sellerPubKeyRing = sellerKeyRing.getPubKeyRing(); PaymentAccountPayload buyerPaymentAccountPayload = new SepaAccountPayload(PaymentMethod.SEPA_ID, "1", CountryUtil.getAllSepaCountries()); PaymentAccountPayload sellerPaymentAccountPayload = new SepaAccountPayload(PaymentMethod.SEPA_ID, "2", CountryUtil.getAllSepaCountries()); AccountAgeWitness buyerAccountAgeWitness = service.getNewWitness(buyerPaymentAccountPayload, buyerPubKeyRing); service.addToMap(buyerAccountAgeWitness); AccountAgeWitness sellerAccountAgeWitness = service.getNewWitness(sellerPaymentAccountPayload, sellerPubKeyRing); service.addToMap(sellerAccountAgeWitness); long now = new Date().getTime() + 1000; Contract contract = mock(Contract.class); disputes.add(new Dispute(new Date().getTime(), "trade1", 0, true, true, true, buyerPubKeyRing, now - 1, now - 1, contract, null, null, null, "contractAsJson", null, null, sellerPaymentAccountPayload, buyerPaymentAccountPayload, null, true, SupportType.ARBITRATION)); disputes.get(0).setIsClosed(); disputes.get(0).getDisputeResultProperty().set(new DisputeResult( "trade1", 1, DisputeResult.Winner.BUYER, DisputeResult.Reason.OTHER.ordinal(), DisputeResult.SubtractFeeFrom.BUYER_ONLY, true, true, true, "summary", null, null, 100000, 0, null, now - 1)); // Filtermanager says nothing is filtered when(filterManager.isNodeAddressBanned(any())).thenReturn(false); when(filterManager.isCurrencyBanned(any())).thenReturn(false); when(filterManager.isPaymentMethodBanned(any())).thenReturn(false); when(filterManager.arePeersPaymentAccountDataBanned(any())).thenReturn(false); when(filterManager.isWitnessSignerPubKeyBanned(any())).thenReturn(false); when(chargeBackRisk.hasChargebackRisk(any(), any())).thenReturn(true); when(contract.getPaymentMethodId()).thenReturn(PaymentMethod.SEPA_ID); when(contract.getTradeAmount()).thenReturn(HavenoUtils.xmrToAtomicUnits(0.01)); when(contract.getBuyerPubKeyRing()).thenReturn(buyerPubKeyRing); when(contract.getSellerPubKeyRing()).thenReturn(sellerPubKeyRing); when(contract.getOfferPayload()).thenReturn(mock(OfferPayload.class)); when(contract.isBuyerMakerAndSellerTaker()).thenReturn(false); assertEquals(disputes.get(0).getBuyerPaymentAccountPayload(), buyerPaymentAccountPayload); assertEquals(disputes.get(0).getSellerPaymentAccountPayload(), sellerPaymentAccountPayload); List items = service.getTraderPaymentAccounts(now, getPaymentMethod(PaymentMethod.SEPA_ID), disputes); assertEquals(2, items.size()); // Setup a mocked arbitrator key ECKey arbitratorKey = mock(ECKey.class); when(arbitratorKey.signMessage(any())).thenReturn("1"); when(arbitratorKey.signMessage(any())).thenReturn("2"); when(arbitratorKey.getPubKey()).thenReturn("1".getBytes()); // Arbitrator signs both trader accounts items.forEach(item -> service.arbitratorSignAccountAgeWitness( item.getTradeAmount(), item.getAccountAgeWitness(), arbitratorKey, item.getPeersPubKey())); // Check that both accountAgeWitnesses are signed SignedWitness foundBuyerSignedWitness = signedWitnessService.getSignedWitnessSetByOwnerPubKey( buyerPubKeyRing.getSignaturePubKeyBytes()).stream() .findFirst() .orElse(null); assert foundBuyerSignedWitness != null; assertEquals(Utilities.bytesAsHexString(foundBuyerSignedWitness.getAccountAgeWitnessHash()), Utilities.bytesAsHexString(buyerAccountAgeWitness.getHash())); SignedWitness foundSellerSignedWitness = signedWitnessService.getSignedWitnessSetByOwnerPubKey( sellerPubKeyRing.getSignaturePubKeyBytes()).stream() .findFirst() .orElse(null); assert foundSellerSignedWitness != null; assertEquals(Utilities.bytesAsHexString(foundSellerSignedWitness.getAccountAgeWitnessHash()), Utilities.bytesAsHexString(sellerAccountAgeWitness.getHash())); } // Create a tree of signed witnesses Arb -(SWA)-> aew1 -(SW1)-> aew2 -(SW2)-> aew3 // Delete SWA signature, none of the account age witnesses are considered signed // Sign a dummy AccountAgeWitness using the signerPubkey from SW1; aew2 and aew3 are not considered signed. The // lost SignedWitness isn't possible to recover so aew1 is still not signed, but it's pubkey is a signer. @Test public void testArbitratorSignDummyWitness() throws CryptoException { ECKey arbitratorKey = new ECKey(); // Init 2 user accounts var user1KeyRing = new KeyRing(new KeyStorage(dir1), null, true); var user2KeyRing = new KeyRing(new KeyStorage(dir2), null, true); var user3KeyRing = new KeyRing(new KeyStorage(dir3), null, true); var pubKeyRing1 = user1KeyRing.getPubKeyRing(); var pubKeyRing2 = user2KeyRing.getPubKeyRing(); var pubKeyRing3 = user3KeyRing.getPubKeyRing(); var account1 = new SepaAccountPayload(PaymentMethod.SEPA_ID, "1", CountryUtil.getAllSepaCountries()); var account2 = new SepaAccountPayload(PaymentMethod.SEPA_ID, "2", CountryUtil.getAllSepaCountries()); var account3 = new SepaAccountPayload(PaymentMethod.SEPA_ID, "3", CountryUtil.getAllSepaCountries()); var aew1 = service.getNewWitness(account1, pubKeyRing1); var aew2 = service.getNewWitness(account2, pubKeyRing2); var aew3 = service.getNewWitness(account3, pubKeyRing3); // Backdate witness1 70 days aew1 = new AccountAgeWitness(aew1.getHash(), new Date().getTime() - TimeUnit.DAYS.toMillis(70)); aew2 = new AccountAgeWitness(aew2.getHash(), new Date().getTime() - TimeUnit.DAYS.toMillis(35)); aew3 = new AccountAgeWitness(aew3.getHash(), new Date().getTime() - TimeUnit.DAYS.toMillis(1)); service.addToMap(aew1); service.addToMap(aew2); service.addToMap(aew3); // Test as user1. It's still possible to sign as arbitrator since the ECKey is passed as an argument. setupService(user1KeyRing); // Arbitrator signs user1 service.arbitratorSignAccountAgeWitness(aew1, arbitratorKey, pubKeyRing1.getSignaturePubKeyBytes(), aew1.getDate()); // user1 signs user2 signAccountAgeWitness(aew2, pubKeyRing2.getSignaturePubKey(), aew2.getDate(), user1KeyRing); // user2 signs user3 signAccountAgeWitness(aew3, pubKeyRing3.getSignaturePubKey(), aew3.getDate(), user2KeyRing); signedWitnessService.signAndPublishAccountAgeWitness(SignedWitnessService.MINIMUM_TRADE_AMOUNT_FOR_SIGNING, aew2, pubKeyRing2.getSignaturePubKey()); assertTrue(service.accountIsSigner(aew1)); assertTrue(service.accountIsSigner(aew2)); assertFalse(service.accountIsSigner(aew3)); assertTrue(signedWitnessService.isSignedAccountAgeWitness(aew3)); // Remove SignedWitness signed by arbitrator @SuppressWarnings("OptionalGetWithoutIsPresent") var signedWitnessArb = signedWitnessService.getSignedWitnessMapValues().stream() .filter(sw -> sw.getVerificationMethod() == SignedWitness.VerificationMethod.ARBITRATOR) .findAny() .get(); signedWitnessService.removeSignedWitness(signedWitnessArb); assertEquals(signedWitnessService.getSignedWitnessMapValues().size(), 2); // Check that no account age witness is a signer assertFalse(service.accountIsSigner(aew1)); assertFalse(service.accountIsSigner(aew2)); assertFalse(service.accountIsSigner(aew3)); assertFalse(signedWitnessService.isSignedAccountAgeWitness(aew2)); // Sign dummy AccountAgeWitness using signer key from SW_1 assertEquals(signedWitnessService.getRootSignedWitnessSet(false).size(), 1); // TODO: move this to accountagewitnessservice @SuppressWarnings("OptionalGetWithoutIsPresent") var orphanedSignedWitness = signedWitnessService.getRootSignedWitnessSet(false).stream().findAny().get(); var dummyAccountAgeWitnessHash = Hash.getRipemd160hash(orphanedSignedWitness.getSignerPubKey()); var dummyAEW = new AccountAgeWitness(dummyAccountAgeWitnessHash, orphanedSignedWitness.getDate() - (TimeUnit.DAYS.toMillis(SignedWitnessService.SIGNER_AGE_DAYS + 1))); service.arbitratorSignAccountAgeWitness( dummyAEW, arbitratorKey, orphanedSignedWitness.getSignerPubKey(), dummyAEW.getDate()); assertFalse(service.accountIsSigner(aew1)); assertTrue(service.accountIsSigner(aew2)); assertFalse(service.accountIsSigner(aew3)); assertTrue(signedWitnessService.isSignedAccountAgeWitness(aew2)); } private void signAccountAgeWitness(AccountAgeWitness accountAgeWitness, PublicKey witnessOwnerPubKey, long time, KeyRing signerKeyRing) throws CryptoException { byte[] signature = Sig.sign(signerKeyRing.getSignatureKeyPair().getPrivate(), accountAgeWitness.getHash()); SignedWitness signedWitness = new SignedWitness(SignedWitness.VerificationMethod.TRADE, accountAgeWitness.getHash(), signature, signerKeyRing.getSignatureKeyPair().getPublic().getEncoded(), witnessOwnerPubKey.getEncoded(), time, SignedWitnessService.MINIMUM_TRADE_AMOUNT_FOR_SIGNING.longValueExact()); signedWitnessService.addToMap(signedWitness); } } ================================================ FILE: core/src/test/java/haveno/core/app/HavenoHelpFormatterTest.java ================================================ /* * 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 . */ package haveno.core.app; import haveno.common.config.HavenoHelpFormatter; import joptsimple.OptionParser; import org.junit.jupiter.api.Test; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Paths; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; public class HavenoHelpFormatterTest { @Test public void testHelpFormatter() throws IOException, URISyntaxException { OptionParser parser = new OptionParser(); parser.formatHelpWith(new HavenoHelpFormatter("Haveno Test", "haveno-test", "0.1.0")); parser.accepts("name", "The name of the Haveno node") .withRequiredArg() .ofType(String.class) .defaultsTo("Haveno"); parser.accepts("another-option", "This is a long description which will need to break over multiple linessssssssssss such " + "that no line is longer than 80 characters in the help output.") .withRequiredArg() .ofType(String.class) .defaultsTo("WAT"); parser.accepts("exactly-72-char-description", "012345678911234567892123456789312345678941234567895123456789612345678971") .withRequiredArg() .ofType(String.class); parser.accepts("exactly-72-char-description-with-spaces", " 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1") .withRequiredArg() .ofType(String.class); parser.accepts("90-char-description-without-spaces", "-123456789-223456789-323456789-423456789-523456789-623456789-723456789-823456789-923456789") .withRequiredArg() .ofType(String.class); parser.accepts("90-char-description-with-space-at-char-80", "-123456789-223456789-323456789-423456789-523456789-623456789-723456789-823456789 923456789") .withRequiredArg() .ofType(String.class); parser.accepts("90-char-description-with-spaces-at-chars-5-and-80", "-123 56789-223456789-323456789-423456789-523456789-623456789-723456789-823456789 923456789") .withRequiredArg() .ofType(String.class); parser.accepts("90-char-description-with-space-at-char-73", "-123456789-223456789-323456789-423456789-523456789-623456789-723456789-8 3456789-923456789") .withRequiredArg() .ofType(String.class); parser.accepts("1-char-description-with-only-a-space", " ") .withRequiredArg() .ofType(String.class); parser.accepts("empty-description", "") .withRequiredArg() .ofType(String.class); parser.accepts("no-description") .withRequiredArg() .ofType(String.class); parser.accepts("no-arg", "Some description"); parser.accepts("optional-arg", "Option description") .withOptionalArg(); parser.accepts("with-default-value", "Some option with a default value") .withRequiredArg() .ofType(String.class) .defaultsTo("Wat"); parser.accepts("data-dir", "Application data directory") .withRequiredArg() .ofType(File.class) .defaultsTo(new File("/Users/cbeams/Library/Application Support/Haveno")); parser.accepts("enum-opt", "Some option that accepts an enum value as an argument") .withRequiredArg() .ofType(AnEnum.class) .defaultsTo(AnEnum.foo); ByteArrayOutputStream actual = new ByteArrayOutputStream(); String expected = new String(Files.readAllBytes(Paths.get(getClass().getResource("cli-output.txt").toURI()))); if (System.getProperty("os.name").startsWith("Windows")) { // Load the expected content from a different file for Windows due to different path separator // And normalize line endings to LF in case the file has CRLF line endings expected = new String(Files.readAllBytes(Paths.get(getClass().getResource("cli-output_windows.txt").toURI()))) .replaceAll("\\r\\n?", "\n"); } parser.printHelpOn(new PrintStream(actual)); assertThat(actual.toString(), equalTo(expected)); } enum AnEnum {foo, bar, baz} } ================================================ FILE: core/src/test/java/haveno/core/arbitration/ArbitratorManagerTest.java ================================================ /* * 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 . */ package haveno.core.arbitration; import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorService; import haveno.core.user.User; import haveno.network.p2p.NodeAddress; import org.junit.jupiter.api.Test; import java.util.ArrayList; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; public class ArbitratorManagerTest { @Test public void testIsArbitratorAvailableForLanguage() { User user = mock(User.class); ArbitratorService arbitratorService = mock(ArbitratorService.class); ArbitratorManager manager = new ArbitratorManager(null, arbitratorService, user, null); ArrayList languagesOne = new ArrayList() {{ add("en"); add("de"); }}; ArrayList languagesTwo = new ArrayList() {{ add("en"); add("es"); }}; Arbitrator one = new Arbitrator(new NodeAddress("arbitrator:1"), null, languagesOne, 0L, null, "", null, null, null); Arbitrator two = new Arbitrator(new NodeAddress("arbitrator:2"), null, languagesTwo, 0L, null, "", null, null, null); manager.addDisputeAgent(one, () -> { }, errorMessage -> { }); manager.addDisputeAgent(two, () -> { }, errorMessage -> { }); assertTrue(manager.isAgentAvailableForLanguage("en")); assertFalse(manager.isAgentAvailableForLanguage("th")); } @Test public void testGetArbitratorLanguages() { User user = mock(User.class); ArbitratorService arbitratorService = mock(ArbitratorService.class); ArbitratorManager manager = new ArbitratorManager(null, arbitratorService, user, null); ArrayList languagesOne = new ArrayList() {{ add("en"); add("de"); }}; ArrayList languagesTwo = new ArrayList() {{ add("en"); add("es"); }}; Arbitrator one = new Arbitrator(new NodeAddress("arbitrator:1"), null, languagesOne, 0L, null, "", null, null, null); Arbitrator two = new Arbitrator(new NodeAddress("arbitrator:2"), null, languagesTwo, 0L, null, "", null, null, null); ArrayList nodeAddresses = new ArrayList() {{ add(two.getNodeAddress()); }}; manager.addDisputeAgent(one, () -> { }, errorMessage -> { }); manager.addDisputeAgent(two, () -> { }, errorMessage -> { }); assertThat(manager.getDisputeAgentLanguages(nodeAddresses), containsInAnyOrder("en", "es")); assertThat(manager.getDisputeAgentLanguages(nodeAddresses), not(containsInAnyOrder("de"))); } } ================================================ FILE: core/src/test/java/haveno/core/arbitration/ArbitratorTest.java ================================================ /* * 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 . */ package haveno.core.arbitration; import com.google.common.collect.Lists; import haveno.common.crypto.PubKeyRing; import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; import haveno.network.p2p.NodeAddress; import org.apache.commons.lang3.RandomUtils; import org.junit.jupiter.api.Disabled; import java.util.Date; @SuppressWarnings({"SameParameterValue", "UnusedAssignment"}) public class ArbitratorTest { @Disabled("TODO InvalidKeySpecException at haveno.common.crypto.Sig.getPublicKeyFromBytes(Sig.java:135)") public void testRoundtrip() { Arbitrator arbitrator = getArbitratorMock(); Arbitrator newVo = Arbitrator.fromProto(arbitrator.toProtoMessage().getArbitrator()); } public static Arbitrator getArbitratorMock() { return new Arbitrator(new NodeAddress("host", 1000), new PubKeyRing(getBytes(100), getBytes(100)), Lists.newArrayList(), new Date().getTime(), getBytes(100), "registrationSignature", null, null, null); } public static byte[] getBytes(@SuppressWarnings("SameParameterValue") int count) { return RandomUtils.nextBytes(count); } } ================================================ FILE: core/src/test/java/haveno/core/arbitration/MediatorTest.java ================================================ /* * 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 . */ package haveno.core.arbitration; import com.google.common.collect.Lists; import haveno.common.crypto.PubKeyRing; import haveno.core.support.dispute.mediation.mediator.Mediator; import haveno.network.p2p.NodeAddress; import org.junit.jupiter.api.Disabled; import java.util.Date; import static haveno.core.arbitration.ArbitratorTest.getBytes; public class MediatorTest { @Disabled("TODO InvalidKeySpecException at haveno.common.crypto.Sig.getPublicKeyFromBytes(Sig.java:135)") public void testRoundtrip() { Mediator Mediator = getMediatorMock(); //noinspection AccessStaticViaInstance Mediator.fromProto(Mediator.toProtoMessage().getMediator()); } public static Mediator getMediatorMock() { return new Mediator(new NodeAddress("host", 1000), new PubKeyRing(getBytes(100), getBytes(100)), Lists.newArrayList(), new Date().getTime(), getBytes(100), "registrationSignature", "email", "info", null); } } ================================================ FILE: core/src/test/java/haveno/core/arbitration/TraderDataItemTest.java ================================================ package haveno.core.arbitration; import haveno.core.account.witness.AccountAgeWitness; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.support.dispute.arbitration.TraderDataItem; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.math.BigInteger; import java.security.PublicKey; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.mockito.Mockito.mock; /* * 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 . */ public class TraderDataItemTest { private TraderDataItem traderDataItem1; private TraderDataItem traderDataItem2; private TraderDataItem traderDataItem3; private AccountAgeWitness accountAgeWitness1; private AccountAgeWitness accountAgeWitness2; private byte[] hash1 = "1".getBytes(); private byte[] hash2 = "2".getBytes(); @BeforeEach public void setup() { accountAgeWitness1 = new AccountAgeWitness(hash1, 123); accountAgeWitness2 = new AccountAgeWitness(hash2, 124); traderDataItem1 = new TraderDataItem(mock(PaymentAccountPayload.class), accountAgeWitness1, BigInteger.valueOf(546), mock(PublicKey.class)); traderDataItem2 = new TraderDataItem(mock(PaymentAccountPayload.class), accountAgeWitness1, BigInteger.valueOf(547), mock(PublicKey.class)); traderDataItem3 = new TraderDataItem(mock(PaymentAccountPayload.class), accountAgeWitness2, BigInteger.valueOf(548), mock(PublicKey.class)); } @Test public void testEquals() { assertEquals(traderDataItem1, traderDataItem2); assertNotEquals(traderDataItem1, traderDataItem3); assertNotEquals(traderDataItem2, traderDataItem3); } @Test public void testHashCode() { assertEquals(traderDataItem1.hashCode(), traderDataItem2.hashCode()); assertNotEquals(traderDataItem1.hashCode(), traderDataItem3.hashCode()); } } ================================================ FILE: core/src/test/java/haveno/core/crypto/EncryptionTest.java ================================================ /* * 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 . */ package haveno.core.crypto; import haveno.common.crypto.CryptoException; import haveno.common.crypto.KeyRing; import haveno.common.crypto.KeyStorage; import haveno.common.file.FileUtil; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import java.io.File; import java.io.IOException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; public class EncryptionTest { private KeyRing keyRing; private File dir; @BeforeEach public void setup() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, CryptoException { dir = File.createTempFile("temp_tests", ""); //noinspection ResultOfMethodCallIgnored dir.delete(); //noinspection ResultOfMethodCallIgnored dir.mkdir(); KeyStorage keyStorage = new KeyStorage(dir); keyRing = new KeyRing(keyStorage, null, true); } @AfterEach public void tearDown() throws IOException { FileUtil.deleteDirectory(dir); } } ================================================ FILE: core/src/test/java/haveno/core/crypto/SigTest.java ================================================ /* * 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 . */ package haveno.core.crypto; import haveno.common.crypto.CryptoException; import haveno.common.crypto.KeyRing; import haveno.common.crypto.KeyStorage; import haveno.common.crypto.Sig; import haveno.common.file.FileUtil; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.util.Random; import static org.junit.jupiter.api.Assertions.assertTrue; public class SigTest { private static final Logger log = LoggerFactory.getLogger(SigTest.class); private KeyRing keyRing; private File dir; @BeforeEach public void setup() throws IOException { dir = File.createTempFile("temp_tests", ""); //noinspection ResultOfMethodCallIgnored dir.delete(); //noinspection ResultOfMethodCallIgnored dir.mkdir(); KeyStorage keyStorage = new KeyStorage(dir); keyRing = new KeyRing(keyStorage, null, true); } @AfterEach public void tearDown() throws IOException { FileUtil.deleteDirectory(dir); } @Test public void testSignature() { long ts = System.currentTimeMillis(); log.trace("start "); for (int i = 0; i < 100; i++) { String msg = String.valueOf(new Random().nextInt()); String sig = null; try { sig = Sig.sign(keyRing.getSignatureKeyPair().getPrivate(), msg); } catch (CryptoException e) { log.error("sign failed"); e.printStackTrace(); assertTrue(false); } try { assertTrue(Sig.verify(keyRing.getSignatureKeyPair().getPublic(), msg, sig)); } catch (CryptoException e) { log.error("verify failed"); e.printStackTrace(); assertTrue(false); } } log.trace("took {} ms.", System.currentTimeMillis() - ts); } } ================================================ FILE: core/src/test/java/haveno/core/locale/BankUtilTest.java ================================================ package haveno.core.locale; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.Locale; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class BankUtilTest { @BeforeEach public void setup() { Locale.setDefault(new Locale("en", "US")); GlobalSettings.setLocale(new Locale("en", "US")); Res.setBaseCurrencyCode("XMR"); Res.setBaseCurrencyName("Monero"); } @Test public void testBankFieldsForArgentina() { final String argentina = "AR"; assertTrue(BankUtil.isHolderIdRequired(argentina)); assertEquals("CUIL/CUIT", BankUtil.getHolderIdLabel(argentina)); assertEquals("CUIT", BankUtil.getHolderIdLabelShort(argentina)); assertTrue(BankUtil.isNationalAccountIdRequired(argentina)); assertEquals("CBU number", BankUtil.getNationalAccountIdLabel(argentina)); assertTrue(BankUtil.isBankNameRequired(argentina)); assertTrue(BankUtil.isBranchIdRequired(argentina)); assertTrue(BankUtil.isAccountNrRequired(argentina)); assertEquals("Número de cuenta", BankUtil.getAccountNrLabel(argentina)); assertTrue(BankUtil.useValidation(argentina)); assertFalse(BankUtil.isBankIdRequired(argentina)); assertFalse(BankUtil.isStateRequired(argentina)); assertFalse(BankUtil.isAccountTypeRequired(argentina)); } } ================================================ FILE: core/src/test/java/haveno/core/locale/CurrencyUtilTest.java ================================================ /* * 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 . */ package haveno.core.locale; import haveno.asset.Asset; import haveno.asset.AssetRegistry; import haveno.asset.Coin; import haveno.asset.coins.Ether; import haveno.common.config.BaseCurrencyNetwork; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.ServiceLoader; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; public class CurrencyUtilTest { @BeforeEach public void setup() { Locale.setDefault(new Locale("en", "US")); Res.setBaseCurrencyCode("XMR"); Res.setBaseCurrencyName("Monero"); } @Test public void testGetTradeCurrency() { Optional euro = CurrencyUtil.getTradeCurrency("EUR"); Optional naira = CurrencyUtil.getTradeCurrency("NGN"); Optional fake = CurrencyUtil.getTradeCurrency("FAK"); assertTrue(euro.isPresent()); assertTrue(naira.isPresent()); assertFalse(fake.isPresent(), "Fake currency shouldn't exist"); } @Test public void testFindAsset() { MockAssetRegistry assetRegistry = new MockAssetRegistry(); // Add a mock coin which has no mainnet version, needs to fail if we are on mainnet MockTestnetCoin.Testnet mockTestnetCoin = new MockTestnetCoin.Testnet(); try { assetRegistry.addAsset(mockTestnetCoin); CurrencyUtil.findAsset(assetRegistry, "MOCK_COIN", BaseCurrencyNetwork.XMR_MAINNET); fail("Expected an IllegalArgumentException"); } catch (IllegalArgumentException e) { String wantMessage = "We are on mainnet and we could not find an asset with network type mainnet"; assertTrue(e.getMessage().startsWith(wantMessage), "Unexpected exception, want message starting with " + "'" + wantMessage + "', got '" + e.getMessage() + "'"); } // For testnet its ok assertEquals(CurrencyUtil.findAsset(assetRegistry, "MOCK_COIN", BaseCurrencyNetwork.XMR_LOCAL).get().getTickerSymbol(), "MOCK_COIN"); assertEquals(Coin.Network.TESTNET, mockTestnetCoin.getNetwork()); // For regtest its still found assertEquals(CurrencyUtil.findAsset(assetRegistry, "MOCK_COIN", BaseCurrencyNetwork.XMR_STAGENET).get().getTickerSymbol(), "MOCK_COIN"); // We test if we are not on mainnet to get the mainnet coin Coin ether = new Ether(); assertEquals(CurrencyUtil.findAsset(assetRegistry, "ETH", BaseCurrencyNetwork.XMR_LOCAL).get().getTickerSymbol(), "ETH"); assertEquals(CurrencyUtil.findAsset(assetRegistry, "ETH", BaseCurrencyNetwork.XMR_STAGENET).get().getTickerSymbol(), "ETH"); assertEquals(Coin.Network.MAINNET, ether.getNetwork()); } @Test public void testGetNameAndCodeOfRemovedAsset() { assertEquals("N/A (XYZ)", CurrencyUtil.getNameAndCode("XYZ")); } class MockAssetRegistry extends AssetRegistry { private List registeredAssets = new ArrayList<>(); MockAssetRegistry() { for (Asset asset : ServiceLoader.load(Asset.class)) { registeredAssets.add(asset); } } void addAsset(Asset asset) { registeredAssets.add(asset); } public Stream stream() { return registeredAssets.stream(); } } } ================================================ FILE: core/src/test/java/haveno/core/locale/MockTestnetCoin.java ================================================ /* * 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 . */ package haveno.core.locale; import haveno.asset.AddressValidationResult; import haveno.asset.Base58AddressValidator; import haveno.asset.Coin; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.params.MainNetParams; import org.bitcoinj.params.RegTestParams; import org.bitcoinj.params.TestNet3Params; public class MockTestnetCoin extends Coin { public MockTestnetCoin(Network network, NetworkParameters networkParameters) { super("MockTestnetCoin", "MOCK_COIN", new BSQAddressValidator(networkParameters), network); } public static class Mainnet extends MockTestnetCoin { public Mainnet() { super(Network.MAINNET, MainNetParams.get()); } } public static class Testnet extends MockTestnetCoin { public Testnet() { super(Network.TESTNET, TestNet3Params.get()); } } public static class Regtest extends MockTestnetCoin { public Regtest() { super(Network.STAGENET, RegTestParams.get()); } } public static class BSQAddressValidator extends Base58AddressValidator { public BSQAddressValidator(NetworkParameters networkParameters) { super(networkParameters); } @Override public AddressValidationResult validate(String address) { return super.validate(address); } } } ================================================ FILE: core/src/test/java/haveno/core/message/MarshallerTest.java ================================================ /* * 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 . */ package haveno.core.message; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import static org.junit.jupiter.api.Assertions.assertTrue; @Slf4j public class MarshallerTest { @Test public void getBaseEnvelopeTest() { protobuf.Ping Ping = protobuf.Ping.newBuilder().setNonce(100).build(); protobuf.Pong Pong = protobuf.Pong.newBuilder().setRequestNonce(1000).build(); protobuf.NetworkEnvelope envelope1 = protobuf.NetworkEnvelope.newBuilder().setPing(Ping).build(); protobuf.NetworkEnvelope envelope2 = protobuf.NetworkEnvelope.newBuilder().setPong(Pong).build(); log.info(Ping.toString()); log.info(Pong.toString()); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try { envelope1.writeDelimitedTo(outputStream); envelope2.writeDelimitedTo(outputStream); ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); protobuf.NetworkEnvelope envelope3 = protobuf.NetworkEnvelope.parseDelimitedFrom(inputStream); protobuf.NetworkEnvelope envelope4 = protobuf.NetworkEnvelope.parseDelimitedFrom(inputStream); log.info("message: {}", envelope3.getPing()); //log.info("peerseesd empty: '{}'",envelope3.getPong().equals(PB.NetworkEnvelope.) == ""); assertTrue(isPing(envelope3)); assertTrue(!isPing(envelope4)); log.info("3 = {} 4 = {}", isPing(envelope3), isPing(envelope4)); log.info(envelope3.toString()); log.info(envelope4.toString()); } catch (IOException e) { e.printStackTrace(); } } public boolean isPing(protobuf.NetworkEnvelope envelope) { return !envelope.getPing().getDefaultInstanceForType().equals(envelope.getPing()); } } ================================================ FILE: core/src/test/java/haveno/core/monetary/PriceTest.java ================================================ /* * 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 . */ package haveno.core.monetary; import org.junit.jupiter.api.Test; import static haveno.core.monetary.Price.parse; import static haveno.core.monetary.Price.valueOf; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; public class PriceTest { @Test public void testParse() { assertEquals( "0.10 XMR/USD", parse("USD", "0.1").toFriendlyString(), "Fiat value should be formatted with two decimals." ); assertEquals( "0.1234 XMR/EUR", parse("EUR", "0.1234").toFriendlyString(), "Fiat value should be given two decimals" ); assertEquals( "0.1235 XMR/EUR", parse("EUR", "0.12345").toFriendlyString(), "Too many decimals of fiat value should get rounded up properly." ); assertEquals( -100000000L, parse("LTC", "-1").getValue(), "Negative value should be parsed correctly." ); assertEquals( "0.0001 XMR/USD", parse("USD", "0,0001").toFriendlyString(), "Comma (',') as decimal separator should be converted to period ('.')" ); assertEquals( "10000.2346 XMR/LTC", parse("LTC", "10000,23456789").toFriendlyString(), "Too many decimals should get rounded up properly." ); assertEquals( "10000.2345 XMR/LTC", parse("LTC", "10000,23454999").toFriendlyString(), "Too many decimals should get rounded down properly." ); assertEquals( 1000023456789L, parse("LTC", "10000,23456789").getValue(), "Underlying long value should be correct." ); try { parse("XMR", "56789.123456789"); fail("Expected IllegalArgumentException to be thrown when too many decimals are used."); } catch (IllegalArgumentException iae) { assertEquals( "java.lang.ArithmeticException: Rounding necessary", iae.getMessage(), "Unexpected exception message." ); } } @Test public void testValueOf() { assertEquals( "0.0001 XMR/USD", valueOf("USD", 10000).toFriendlyString(), "Fiat value should have four decimals." ); assertEquals( "0.1234 XMR/EUR", valueOf("EUR", 12340000).toFriendlyString(), "Fiat value should be given two decimals" ); assertEquals( -1L, valueOf("LTC", -1L).getValue(), "Negative value should be parsed correctly." ); assertEquals( "10000.2346 XMR/LTC", valueOf("LTC", 1000023456789L).toFriendlyString(), "Too many decimals should get rounded up properly." ); assertEquals( "10000.2345 XMR/LTC", valueOf("LTC", 1000023454999L).toFriendlyString(), "Too many decimals should get rounded down properly." ); assertEquals( 1000023456789L, valueOf("LTC", 1000023456789L).getValue(), "Underlying long value should be correct." ); } } ================================================ FILE: core/src/test/java/haveno/core/network/p2p/seed/DefaultSeedNodeRepositoryTest.java ================================================ /* * 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 . */ package haveno.core.network.p2p.seed; import haveno.common.config.Config; import haveno.network.p2p.NodeAddress; import org.junit.jupiter.api.Test; import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class DefaultSeedNodeRepositoryTest { @Test public void getSeedNodes() { DefaultSeedNodeRepository DUT = new DefaultSeedNodeRepository(new Config()); assertFalse(DUT.getSeedNodeAddresses().isEmpty()); } @Test public void manualSeedNodes() { String seed1 = "asdf:8001"; String seed2 = "fdsa:6001"; String seedNodesOption = format("--%s=%s,%s", Config.SEED_NODES, seed1, seed2); DefaultSeedNodeRepository DUT = new DefaultSeedNodeRepository(new Config(seedNodesOption)); assertFalse(DUT.getSeedNodeAddresses().isEmpty()); assertEquals(2, DUT.getSeedNodeAddresses().size()); assertTrue(DUT.getSeedNodeAddresses().contains(new NodeAddress(seed1))); assertTrue(DUT.getSeedNodeAddresses().contains(new NodeAddress(seed2))); } } ================================================ FILE: core/src/test/java/haveno/core/notifications/MobileModelTest.java ================================================ /* * 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 . */ package haveno.core.notifications; import haveno.common.util.Tuple2; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.List; import static common.utils.GenUtils.assertEquals; @Slf4j public class MobileModelTest { @Test public void testParseDescriptor() { MobileModel mobileModel = new MobileModel(); List> list = Arrays.asList( new Tuple2<>("iPod Touch 5", false), new Tuple2<>("iPod Touch 6", false), new Tuple2<>("iPhone 4", false), new Tuple2<>("iPhone 4s", false), new Tuple2<>("iPhone 5", false), new Tuple2<>("iPhone 5c", false), new Tuple2<>("iPhone 5s", false), new Tuple2<>("iPhone 6", false), new Tuple2<>("iPhone 6 Plus", false), new Tuple2<>("iPhone 6s", true), new Tuple2<>("iPhone 6s Plus", true), new Tuple2<>("iPhone 7", true), new Tuple2<>("iPhone 7 Plus", true), new Tuple2<>("iPhone SE", false), // unclear new Tuple2<>("iPhone 8", true), new Tuple2<>("iPhone 8 Plus", true), new Tuple2<>("iPhone X", true), new Tuple2<>("iPhone XS", true), new Tuple2<>("iPhone XS Max", true), new Tuple2<>("iPhone XR", true), new Tuple2<>("iPhone 11", true), new Tuple2<>("iPhone 11 Pro", true), new Tuple2<>("iPhone 11 Pro Max", true), new Tuple2<>("iPhone 11S", true), // not sure if this model will exist, but based on past versioning it is possible // need to ensure it will be parsed correctly just in case new Tuple2<>("iPad 2", false), new Tuple2<>("iPad 3", false), new Tuple2<>("iPad 4", false), new Tuple2<>("iPad Air", false), new Tuple2<>("iPad Air 2", false), new Tuple2<>("iPad 5", false), new Tuple2<>("iPad 6", false), new Tuple2<>("iPad Mini", false), new Tuple2<>("iPad Mini 2", false), new Tuple2<>("iPad Mini 3", false), new Tuple2<>("iPad Mini 4", false), new Tuple2<>("iPad Pro 9.7 Inch", true), new Tuple2<>("iPad Pro 12.9 Inch", true), new Tuple2<>("iPad Pro 12.9 Inch 2. Generation", true), new Tuple2<>("iPad Pro 10.5 Inch", true) ); list.forEach(tuple -> { log.info(tuple.toString()); assertEquals("tuple: " + tuple, mobileModel.parseDescriptor(tuple.first), tuple.second); }); } } ================================================ FILE: core/src/test/java/haveno/core/offer/OfferMaker.java ================================================ /* * 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 . */ package haveno.core.offer; import com.natpryce.makeiteasy.Instantiator; import com.natpryce.makeiteasy.Maker; import com.natpryce.makeiteasy.Property; import static com.natpryce.makeiteasy.MakeItEasy.a; public class OfferMaker { public static final Property price = new Property<>(); public static final Property minAmount = new Property<>(); public static final Property amount = new Property<>(); public static final Property baseCurrencyCode = new Property<>(); public static final Property counterCurrencyCode = new Property<>(); public static final Property direction = new Property<>(); public static final Property useMarketBasedPrice = new Property<>(); public static final Property marketPriceMargin = new Property<>(); public static final Property id = new Property<>(); public static final Instantiator Offer = lookup -> new Offer( new OfferPayload(lookup.valueOf(id, "1234"), 0L, null, null, lookup.valueOf(direction, OfferDirection.BUY), lookup.valueOf(price, 100000L), lookup.valueOf(marketPriceMargin, 0.0), lookup.valueOf(useMarketBasedPrice, false), lookup.valueOf(amount, 100000L), lookup.valueOf(minAmount, 100000L), 0L, 0L, 0L, 0L, 0L, lookup.valueOf(baseCurrencyCode, "XMR"), lookup.valueOf(counterCurrencyCode, "USD"), "SEPA", "", null, null, null, null, "", 0L, 0L, 0L, false, false, 0L, 0L, false, null, null, 0, null, null, null, "My extra info")); public static final Maker btcUsdOffer = a(Offer); } ================================================ FILE: core/src/test/java/haveno/core/offer/OfferTest.java ================================================ /* * 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 . */ package haveno.core.offer; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class OfferTest { @Test public void testHasNoRange() { OfferPayload payload = mock(OfferPayload.class); when(payload.getMinAmount()).thenReturn(1000L); when(payload.getAmount()).thenReturn(1000L); Offer offer = new Offer(payload); assertFalse(offer.isRange()); } @Test public void testHasRange() { OfferPayload payload = mock(OfferPayload.class); when(payload.getMinAmount()).thenReturn(1000L); when(payload.getAmount()).thenReturn(2000L); Offer offer = new Offer(payload); assertTrue(offer.isRange()); } } ================================================ FILE: core/src/test/java/haveno/core/offer/OpenOfferManagerTest.java ================================================ package haveno.core.offer; import haveno.common.crypto.KeyRing; import haveno.common.crypto.KeyStorage; import haveno.common.file.CorruptedStorageFileHandler; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.common.persistence.PersistenceManager; import haveno.core.api.CoreContext; import haveno.core.api.XmrConnectionService; import haveno.core.trade.TradableList; import haveno.network.p2p.P2PService; import haveno.network.p2p.peers.PeerManager; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.nio.file.Files; import java.util.concurrent.atomic.AtomicBoolean; import static com.natpryce.makeiteasy.MakeItEasy.make; import static haveno.core.offer.OfferMaker.btcUsdOffer; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class OpenOfferManagerTest { private PersistenceManager> persistenceManager; private PersistenceManager signedOfferPersistenceManager; private CoreContext coreContext; @BeforeEach public void setUp() throws Exception { var corruptedStorageFileHandler = mock(CorruptedStorageFileHandler.class); var storageDir = Files.createTempDirectory("storage").toFile(); var keyRing = new KeyRing(new KeyStorage(storageDir)); persistenceManager = new PersistenceManager<>(storageDir, null, corruptedStorageFileHandler, keyRing); signedOfferPersistenceManager = new PersistenceManager<>(storageDir, null, corruptedStorageFileHandler, keyRing); coreContext = new CoreContext(); } @AfterEach public void tearDown() { persistenceManager.shutdown(); signedOfferPersistenceManager.shutdown(); } @Test public void testStartEditOfferForActiveOffer() { P2PService p2PService = mock(P2PService.class); OfferBookService offerBookService = mock(OfferBookService.class); XmrConnectionService xmrConnectionService = mock(XmrConnectionService.class); when(p2PService.getPeerManager()).thenReturn(mock(PeerManager.class)); final OpenOfferManager manager = new OpenOfferManager(coreContext, null, null, p2PService, xmrConnectionService, null, null, null, offerBookService, null, null, null, null, null, null, null, null, persistenceManager, signedOfferPersistenceManager, null); AtomicBoolean startEditOfferSuccessful = new AtomicBoolean(false); doAnswer(invocation -> { ((ResultHandler) invocation.getArgument(1)).handleResult(); return null; }).when(offerBookService).deactivateOffer(any(OfferPayload.class), any(ResultHandler.class), any(ErrorMessageHandler.class)); final OpenOffer openOffer = new OpenOffer(make(btcUsdOffer)); openOffer.setState(OpenOffer.State.AVAILABLE); ResultHandler resultHandler = () -> startEditOfferSuccessful.set(true); manager.editOpenOfferStart(openOffer, resultHandler, null); verify(offerBookService, times(1)).deactivateOffer(any(OfferPayload.class), any(ResultHandler.class), any(ErrorMessageHandler.class)); assertTrue(startEditOfferSuccessful.get()); } @Test public void testStartEditOfferForDeactivatedOffer() { P2PService p2PService = mock(P2PService.class); OfferBookService offerBookService = mock(OfferBookService.class); XmrConnectionService xmrConnectionService = mock(XmrConnectionService.class); when(p2PService.getPeerManager()).thenReturn(mock(PeerManager.class)); final OpenOfferManager manager = new OpenOfferManager(coreContext, null, null, p2PService, xmrConnectionService, null, null, null, offerBookService, null, null, null, null, null, null, null, null, persistenceManager, signedOfferPersistenceManager, null); AtomicBoolean startEditOfferSuccessful = new AtomicBoolean(false); ResultHandler resultHandler = () -> startEditOfferSuccessful.set(true); final OpenOffer openOffer = new OpenOffer(make(btcUsdOffer)); openOffer.setState(OpenOffer.State.DEACTIVATED); manager.editOpenOfferStart(openOffer, resultHandler, null); assertTrue(startEditOfferSuccessful.get()); } @Test public void testStartEditOfferForOfferThatIsCurrentlyEdited() { P2PService p2PService = mock(P2PService.class); OfferBookService offerBookService = mock(OfferBookService.class); XmrConnectionService xmrConnectionService = mock(XmrConnectionService.class); when(p2PService.getPeerManager()).thenReturn(mock(PeerManager.class)); final OpenOfferManager manager = new OpenOfferManager(coreContext, null, null, p2PService, xmrConnectionService, null, null, null, offerBookService, null, null, null, null, null, null, null, null, persistenceManager, signedOfferPersistenceManager, null); AtomicBoolean startEditOfferSuccessful = new AtomicBoolean(false); ResultHandler resultHandler = () -> startEditOfferSuccessful.set(true); final OpenOffer openOffer = new OpenOffer(make(btcUsdOffer)); openOffer.setState(OpenOffer.State.DEACTIVATED); manager.editOpenOfferStart(openOffer, resultHandler, null); assertTrue(startEditOfferSuccessful.get()); startEditOfferSuccessful.set(false); manager.editOpenOfferStart(openOffer, resultHandler, null); assertTrue(startEditOfferSuccessful.get()); } } ================================================ FILE: core/src/test/java/haveno/core/offer/availability/ArbitratorSelectionTest.java ================================================ /* * 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 . */ package haveno.core.offer.availability; import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; public class ArbitratorSelectionTest { @Test public void testGetLeastUsedArbitrator() { // We get least used selected List lastAddressesUsedInTrades; Set arbitrators; String result; lastAddressesUsedInTrades = Arrays.asList("arb1", "arb2", "arb1"); arbitrators = new HashSet<>(Arrays.asList("arb1", "arb2")); result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); assertEquals("arb2", result); // if all are same we use first according to alphanumeric sorting lastAddressesUsedInTrades = Arrays.asList("arb1", "arb2", "arb3"); arbitrators = new HashSet<>(Arrays.asList("arb1", "arb2", "arb3")); result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); assertEquals("arb1", result); lastAddressesUsedInTrades = Arrays.asList("arb1", "arb2", "arb3", "arb1"); arbitrators = new HashSet<>(Arrays.asList("arb1", "arb2", "arb3")); result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); assertEquals("arb2", result); lastAddressesUsedInTrades = Arrays.asList("arb1", "arb2", "arb3", "arb1", "arb2"); arbitrators = new HashSet<>(Arrays.asList("arb1", "arb2", "arb3")); result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); assertEquals("arb3", result); lastAddressesUsedInTrades = Arrays.asList("xxx", "ccc", "aaa"); arbitrators = new HashSet<>(Arrays.asList("aaa", "ccc", "xxx")); result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); assertEquals("aaa", result); lastAddressesUsedInTrades = Arrays.asList("333", "000", "111"); arbitrators = new HashSet<>(Arrays.asList("111", "333", "000")); result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); assertEquals("000", result); // if winner is not in our arb list we use our arb from arbitrators even if never used in trades lastAddressesUsedInTrades = Arrays.asList("arb1", "arb2", "arb3"); arbitrators = new HashSet<>(Arrays.asList("arb4")); result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); assertEquals("arb4", result); // if winner (arb2) is not in our arb list we use our arb from arbitrators lastAddressesUsedInTrades = Arrays.asList("arb1", "arb1", "arb1", "arb2"); arbitrators = new HashSet<>(Arrays.asList("arb1")); result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); assertEquals("arb1", result); // arb1 is used least lastAddressesUsedInTrades = Arrays.asList("arb1", "arb2", "arb2", "arb2", "arb1", "arb1", "arb2"); arbitrators = new HashSet<>(Arrays.asList("arb1", "arb2")); result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); assertEquals("arb1", result); } } ================================================ FILE: core/src/test/java/haveno/core/payment/PaymentAccountsTest.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.account.witness.AccountAgeWitness; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.offer.Offer; import haveno.core.payment.payload.PaymentAccountPayload; import org.junit.jupiter.api.Test; import java.util.Collections; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class PaymentAccountsTest { @Test public void testGetOldestPaymentAccountForOfferWhenNoValidAccounts() { PaymentAccounts accounts = new PaymentAccounts(Collections.emptySet(), mock(AccountAgeWitnessService.class)); PaymentAccount actual = accounts.getOldestPaymentAccountForOffer(mock(Offer.class)); assertNull(actual); } // @Test // public void testGetOldestPaymentAccountForOffer() { // AccountAgeWitnessService service = mock(AccountAgeWitnessService.class); // // PaymentAccount oldest = createAccountWithAge(service, 3); // Set accounts = Sets.newHashSet( // oldest, // createAccountWithAge(service, 2), // createAccountWithAge(service, 1)); // // BiFunction dummyValidator = (offer, account) -> true; // PaymentAccounts testedEntity = new PaymentAccounts(accounts, service, dummyValidator); // // PaymentAccount actual = testedEntity.getOldestPaymentAccountForOffer(mock(Offer.class)); // assertEquals(oldest, actual); // } private static PaymentAccount createAccountWithAge(AccountAgeWitnessService service, long age) { PaymentAccountPayload payload = mock(PaymentAccountPayload.class); PaymentAccount account = mock(PaymentAccount.class); when(account.getPaymentAccountPayload()).thenReturn(payload); AccountAgeWitness witness = mock(AccountAgeWitness.class); when(service.getAccountAge(eq(witness), any())).thenReturn(age); when(service.getMyWitness(payload)).thenReturn(witness); return account; } } ================================================ FILE: core/src/test/java/haveno/core/payment/ReceiptPredicatesTest.java ================================================ /* * 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 . */ package haveno.core.payment; import com.google.common.collect.Lists; import haveno.core.locale.CryptoCurrency; import haveno.core.offer.Offer; import haveno.core.payment.payload.PaymentMethod; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class ReceiptPredicatesTest { private final ReceiptPredicates predicates = new ReceiptPredicates(); @Test public void testIsMatchingCurrency() { Offer offer = mock(Offer.class); when(offer.getCounterCurrencyCode()).thenReturn("USD"); PaymentAccount account = mock(PaymentAccount.class); when(account.getTradeCurrencies()).thenReturn(Lists.newArrayList( new CryptoCurrency("BTC", "Bitcoin"), new CryptoCurrency("ETH", "Ether"))); assertFalse(predicates.isMatchingCurrency(offer, account)); } @Test public void testIsMatchingSepaOffer() { Offer offer = mock(Offer.class); PaymentMethod.SEPA = mock(PaymentMethod.class); when(offer.getPaymentMethod()).thenReturn(PaymentMethod.SEPA); assertTrue(predicates.isMatchingSepaOffer(offer, mock(SepaInstantAccount.class))); assertTrue(predicates.isMatchingSepaOffer(offer, mock(SepaAccount.class))); } @Test public void testIsMatchingSepaInstant() { Offer offer = mock(Offer.class); PaymentMethod.SEPA_INSTANT = mock(PaymentMethod.class); when(offer.getPaymentMethod()).thenReturn(PaymentMethod.SEPA_INSTANT); assertTrue(predicates.isMatchingSepaInstant(offer, mock(SepaInstantAccount.class))); assertFalse(predicates.isMatchingSepaInstant(offer, mock(SepaAccount.class))); } @Test public void testIsMatchingCountryCodes() { CountryBasedPaymentAccount account = mock(CountryBasedPaymentAccount.class); when(account.getCountry()).thenReturn(null); assertFalse(predicates.isMatchingCountryCodes(mock(Offer.class), account)); } @Test public void testIsSameOrSpecificBank() { PaymentMethod.SAME_BANK = mock(PaymentMethod.class); Offer offer = mock(Offer.class); when(offer.getPaymentMethod()).thenReturn(PaymentMethod.SAME_BANK); assertTrue(predicates.isOfferRequireSameOrSpecificBank(offer, mock(NationalBankAccount.class))); } @Test public void testIsEqualPaymentMethods() { PaymentMethod method = PaymentMethod.getDummyPaymentMethod("1"); Offer offer = mock(Offer.class); when(offer.getPaymentMethod()).thenReturn(method); PaymentAccount account = mock(PaymentAccount.class); when(account.getPaymentMethod()).thenReturn(method); assertTrue(predicates.isEqualPaymentMethods(offer, account)); } } ================================================ FILE: core/src/test/java/haveno/core/payment/ReceiptValidatorTest.java ================================================ /* * 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 . */ package haveno.core.payment; import haveno.core.offer.Offer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; public class ReceiptValidatorTest { private ReceiptValidator validator; private PaymentAccount account; private Offer offer; private ReceiptPredicates predicates; @BeforeEach public void setUp() { this.predicates = mock(ReceiptPredicates.class); this.account = mock(CountryBasedPaymentAccount.class); this.offer = mock(Offer.class); this.validator = new ReceiptValidator(offer, account, predicates); } @AfterEach public void tearDown() { verifyNoMoreInteractions(offer); } @Test public void testIsValidWhenCurrencyDoesNotMatch() { when(predicates.isMatchingCurrency(offer, account)).thenReturn(false); assertFalse(validator.isValid()); verify(predicates).isMatchingCurrency(offer, account); } @Test public void testIsValidWhenNotCountryBasedAccount() { account = mock(PaymentAccount.class); assertFalse(account instanceof CountryBasedPaymentAccount); when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(true); assertTrue(new ReceiptValidator(offer, account, predicates).isValid()); } @Test public void testIsValidWhenNotMatchingCodes() { when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(false); assertFalse(validator.isValid()); verify(predicates).isMatchingCountryCodes(offer, account); } @Test public void testIsValidWhenSepaOffer() { when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(true); when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(false); when(predicates.isMatchingSepaOffer(offer, account)).thenReturn(true); assertTrue(validator.isValid()); verify(predicates).isMatchingSepaOffer(offer, account); } @Test public void testIsValidWhenSepaInstant() { when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(true); when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(false); when(predicates.isMatchingSepaInstant(offer, account)).thenReturn(true); assertTrue(validator.isValid()); verify(predicates).isMatchingSepaOffer(offer, account); } @Test public void testIsValidWhenSpecificBankAccountAndOfferRequireSpecificBank() { account = mock(SpecificBanksAccount.class); when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(true); when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(true); when(predicates.isMatchingSepaOffer(offer, account)).thenReturn(false); when(predicates.isMatchingSepaInstant(offer, account)).thenReturn(false); when(predicates.isOfferRequireSameOrSpecificBank(offer, account)).thenReturn(true); when(predicates.isMatchingBankId(offer, account)).thenReturn(false); assertFalse(new ReceiptValidator(offer, account, predicates).isValid()); } @Test public void testIsValidWhenSameBankAccountAndOfferRequireSpecificBank() { account = mock(SameBankAccount.class); when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(true); when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(true); when(predicates.isMatchingSepaOffer(offer, account)).thenReturn(false); when(predicates.isMatchingSepaInstant(offer, account)).thenReturn(false); when(predicates.isOfferRequireSameOrSpecificBank(offer, account)).thenReturn(true); when(predicates.isMatchingBankId(offer, account)).thenReturn(false); assertFalse(new ReceiptValidator(offer, account, predicates).isValid()); } @Test public void testIsValidWhenSpecificBankAccount() { account = mock(SpecificBanksAccount.class); when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(true); when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(true); when(predicates.isMatchingSepaOffer(offer, account)).thenReturn(false); when(predicates.isMatchingSepaInstant(offer, account)).thenReturn(false); when(predicates.isOfferRequireSameOrSpecificBank(offer, account)).thenReturn(true); when(predicates.isMatchingBankId(offer, account)).thenReturn(true); assertTrue(new ReceiptValidator(offer, account, predicates).isValid()); } @Test public void testIsValidWhenSameBankAccount() { account = mock(SameBankAccount.class); when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(true); when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(true); when(predicates.isMatchingSepaOffer(offer, account)).thenReturn(false); when(predicates.isMatchingSepaInstant(offer, account)).thenReturn(false); when(predicates.isOfferRequireSameOrSpecificBank(offer, account)).thenReturn(true); when(predicates.isMatchingBankId(offer, account)).thenReturn(true); assertTrue(new ReceiptValidator(offer, account, predicates).isValid()); } @Test public void testIsValidWhenNationalBankAccount() { account = mock(NationalBankAccount.class); when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(true); when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(true); when(predicates.isMatchingSepaOffer(offer, account)).thenReturn(false); when(predicates.isMatchingSepaInstant(offer, account)).thenReturn(false); when(predicates.isOfferRequireSameOrSpecificBank(offer, account)).thenReturn(false); assertTrue(new ReceiptValidator(offer, account, predicates).isValid()); } @Test // Same or Specific Bank offers can't be taken by National Bank accounts. TODO: Consider partially relaxing to allow Specific Banks. public void testIsValidWhenNationalBankAccountAndOfferIsNot() { account = mock(NationalBankAccount.class); when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(false); when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(true); when(predicates.isMatchingSepaOffer(offer, account)).thenReturn(false); when(predicates.isMatchingSepaInstant(offer, account)).thenReturn(false); assertFalse(new ReceiptValidator(offer, account, predicates).isValid()); verify(predicates, never()).isOfferRequireSameOrSpecificBank(offer, account); verify(predicates, never()).isMatchingBankId(offer, account); } @Test // National or Same Bank offers can't be taken by Specific Banks accounts. TODO: Consider partially relaxing to allow National Bank. public void testIsValidWhenSpecificBanksAccountAndOfferIsNot() { account = mock(SpecificBanksAccount.class); when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(false); when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(true); when(predicates.isMatchingSepaOffer(offer, account)).thenReturn(false); when(predicates.isMatchingSepaInstant(offer, account)).thenReturn(false); assertFalse(new ReceiptValidator(offer, account, predicates).isValid()); verify(predicates, never()).isOfferRequireSameOrSpecificBank(offer, account); verify(predicates, never()).isMatchingBankId(offer, account); } @Test // National or Specific Bank offers can't be taken by Same Bank accounts. public void testIsValidWhenSameBankAccountAndOfferIsNot() { account = mock(SameBankAccount.class); when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(false); when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(true); when(predicates.isMatchingSepaOffer(offer, account)).thenReturn(false); when(predicates.isMatchingSepaInstant(offer, account)).thenReturn(false); assertFalse(new ReceiptValidator(offer, account, predicates).isValid()); verify(predicates, never()).isOfferRequireSameOrSpecificBank(offer, account); verify(predicates, never()).isMatchingBankId(offer, account); } @Test public void testIsValidWhenWesternUnionAccount() { account = mock(WesternUnionAccount.class); when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(true); when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(true); when(predicates.isMatchingSepaOffer(offer, account)).thenReturn(false); when(predicates.isMatchingSepaInstant(offer, account)).thenReturn(false); when(predicates.isOfferRequireSameOrSpecificBank(offer, account)).thenReturn(false); assertTrue(new ReceiptValidator(offer, account, predicates).isValid()); } @Test public void testIsValidWhenWesternIrregularAccount() { when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(true); when(predicates.isMatchingCountryCodes(offer, account)).thenReturn(true); when(predicates.isMatchingSepaOffer(offer, account)).thenReturn(false); when(predicates.isMatchingSepaInstant(offer, account)).thenReturn(false); when(predicates.isOfferRequireSameOrSpecificBank(offer, account)).thenReturn(false); assertTrue(validator.isValid()); } @Test public void testIsValidWhenMoneyGramAccount() { account = mock(MoneyGramAccount.class); when(predicates.isMatchingCurrency(offer, account)).thenReturn(true); when(predicates.isEqualPaymentMethods(offer, account)).thenReturn(true); assertTrue(new ReceiptValidator(offer, account, predicates).isValid()); verify(predicates, never()).isMatchingCountryCodes(offer, account); verify(predicates, never()).isMatchingSepaOffer(offer, account); verify(predicates, never()).isMatchingSepaInstant(offer, account); verify(predicates, never()).isOfferRequireSameOrSpecificBank(offer, account); } } ================================================ FILE: core/src/test/java/haveno/core/payment/TradeLimitsTest.java ================================================ /* * 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 . */ package haveno.core.payment; import org.junit.jupiter.api.Test; public class TradeLimitsTest { @Test public void testGetFirstMonthRiskBasedTradeLimit() { } } ================================================ FILE: core/src/test/java/haveno/core/payment/validation/CryptoAddressValidatorTest.java ================================================ /* * 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 . */ package haveno.core.payment.validation; import haveno.asset.AssetRegistry; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class CryptoAddressValidatorTest { @Test public void test() { CryptoAddressValidator validator = new CryptoAddressValidator(new AssetRegistry()); BaseCurrencyNetwork baseCurrencyNetwork = Config.baseCurrencyNetwork(); String currencyCode = baseCurrencyNetwork.getCurrencyCode(); Res.setBaseCurrencyCode(currencyCode); Res.setBaseCurrencyName(baseCurrencyNetwork.getCurrencyName()); CurrencyUtil.setBaseCurrencyCode(currencyCode); validator.setCurrencyCode("BTC"); assertTrue(validator.validate("17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem").isValid); validator.setCurrencyCode("XMR"); assertTrue(validator.validate("4AuUM6PedofLWKfRCX1fP3SoNZUzq6FSAbpevHRR6tVuMpZc3HznVeudmNGkEB75apjE7WKVgZZh1YvPVxZoHFN88NCdmWw").isValid); validator.setCurrencyCode("LTC"); assertTrue(validator.validate("Lg3PX8wRWmApFCoCMAsPF5P9dPHYQHEWKW").isValid); validator.setCurrencyCode("BOGUS"); assertFalse(validator.validate("1BOGUSADDR").isValid); } } ================================================ FILE: core/src/test/java/haveno/core/provider/price/MarketPriceFeedServiceTest.java ================================================ /* * 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 . */ package haveno.core.provider.price; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.junit.jupiter.api.Assertions.assertTrue; @Disabled public class MarketPriceFeedServiceTest { private static final Logger log = LoggerFactory.getLogger(MarketPriceFeedServiceTest.class); @Test public void testGetPrice() throws InterruptedException { PriceFeedService priceFeedService = new PriceFeedService(null, null, null); priceFeedService.setCurrencyCode("EUR"); priceFeedService.startRequestingPrices(tradeCurrency -> { log.debug(tradeCurrency.toString()); assertTrue(true); }, (errorMessage, throwable) -> { log.debug(errorMessage); assertTrue(false); } ); Thread.sleep(10000); } } ================================================ FILE: core/src/test/java/haveno/core/support/dispute/mediation/FileTransferSessionTest.java ================================================ /* * 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 . */ package haveno.core.support.dispute.mediation; import haveno.network.p2p.FileTransferPart; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.network.NetworkNode; import haveno.common.config.Config; import java.io.File; import java.io.FileWriter; import java.io.IOException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class FileTransferSessionTest implements FileTransferSession.FtpCallback { double notedProgressPct = -1.0; int progressInvocations = 0; boolean ftpCompleteStatus = false; String testTradeId = "foo"; int testTraderId = 123; String testClientId = "bar"; NetworkNode networkNode; NodeAddress counterpartyNodeAddress; @BeforeEach public void setUp() throws Exception { new Config(); // static methods like Config.appDataDir() require config to be created once networkNode = mock(NetworkNode.class); when(networkNode.getNodeAddress()).thenReturn(new NodeAddress("null:0000")); counterpartyNodeAddress = new NodeAddress("null:0000"); } @Test public void testSendCreate() { new FileTransferSender(networkNode, counterpartyNodeAddress, testTradeId, testTraderId, testClientId, true, this); assertEquals(0.0, notedProgressPct, 0.0); assertEquals(1, progressInvocations); } @Test public void testCreateZip() { FileTransferSender sender = new FileTransferSender(networkNode, counterpartyNodeAddress, testTradeId, testTraderId, testClientId, true, this); assertEquals(0.0, notedProgressPct, 0.0); assertEquals(1, progressInvocations); sender.createZipFileToSend(); File file = new File(sender.zipFilePath); assertTrue(file.getAbsoluteFile().exists()); assertTrue(file.getAbsoluteFile().length() > 0); file.deleteOnExit(); } @Test public void testSendInitialize() { // checks that the initial send request packet contains correct information try { int testVerifyDataSize = 13; FileTransferSender session = initializeSession(testVerifyDataSize); session.initSend(); FileTransferPart ftp = session.dataAwaitingAck.get(); assertEquals(ftp.tradeId, testTradeId); assertTrue(ftp.uid.length() > 0); assertEquals(0, ftp.messageData.size()); assertEquals(ftp.seqNumOrFileLength, testVerifyDataSize); assertEquals(-1, session.currentBlockSeqNum); return; } catch (IOException e) { e.printStackTrace(); } fail(); } @Test public void testSendSmallFile() { try { int testVerifyDataSize = 13; FileTransferSender session = initializeSession(testVerifyDataSize); // the first block contains zero data, as it is a "request to send" session.initSend(); simulateAckFromPeerAndVerify(session, 0, 0, 2); // the second block contains all the test file data (because it is a small file) session.sendNextBlock(); simulateAckFromPeerAndVerify(session, testVerifyDataSize, 1, 3); // the final invocation sends no data, and wraps up the session session.sendNextBlock(); assertEquals(1, session.currentBlockSeqNum); assertEquals(3, progressInvocations); assertEquals(1.0, notedProgressPct, 0.0); assertTrue(ftpCompleteStatus); } catch (IOException ioe) { ioe.printStackTrace(); fail(); } } @Test public void testSendOneFullBlock() { try { int testVerifyDataSize = FileTransferSession.FILE_BLOCK_SIZE; FileTransferSender session = initializeSession(testVerifyDataSize); // the first block contains zero data, as it is a "request to send" session.initSend(); simulateAckFromPeerAndVerify(session, 0, 0, 2); // the second block contains all the test file data (because it is a small file) session.sendNextBlock(); simulateAckFromPeerAndVerify(session, testVerifyDataSize, 1, 3); // the final invocation sends no data, and wraps up the session session.sendNextBlock(); assertEquals(1, session.currentBlockSeqNum); assertEquals(3, progressInvocations); assertEquals(1.0, notedProgressPct, 0.0); assertTrue(ftpCompleteStatus); } catch (IOException ioe) { ioe.printStackTrace(); fail(); } } @Test public void testSendTwoFullBlocks() { try { int testVerifyDataSize = FileTransferSession.FILE_BLOCK_SIZE * 2; FileTransferSender session = initializeSession(testVerifyDataSize); // the first block contains zero data, as it is a "request to send" session.initSend(); simulateAckFromPeerAndVerify(session, 0, 0, 2); // the second block contains half of the test file data session.sendNextBlock(); simulateAckFromPeerAndVerify(session, testVerifyDataSize / 2, 1, 3); // the third block contains half of the test file data session.sendNextBlock(); simulateAckFromPeerAndVerify(session, testVerifyDataSize / 2, 2, 4); // the final invocation sends no data, and wraps up the session session.sendNextBlock(); assertEquals(2, session.currentBlockSeqNum); assertEquals(4, progressInvocations); assertEquals(1.0, notedProgressPct, 0.0); assertTrue(ftpCompleteStatus); } catch (IOException ioe) { ioe.printStackTrace(); fail(); } } @Test public void testSendTwoFullBlocksPlusOneByte() { try { int testVerifyDataSize = 1 + FileTransferSession.FILE_BLOCK_SIZE * 2; FileTransferSender session = initializeSession(testVerifyDataSize); // the first block contains zero data, as it is a "request to send" session.initSend(); simulateAckFromPeerAndVerify(session, 0, 0, 2); session.sendNextBlock(); simulateAckFromPeerAndVerify(session, FileTransferSession.FILE_BLOCK_SIZE, 1, 3); session.sendNextBlock(); simulateAckFromPeerAndVerify(session, FileTransferSession.FILE_BLOCK_SIZE, 2, 4); // the fourth block contains one byte session.sendNextBlock(); simulateAckFromPeerAndVerify(session, 1, 3, 5); // the final invocation sends no data, and wraps up the session session.sendNextBlock(); assertEquals(3, session.currentBlockSeqNum); assertEquals(5, progressInvocations); assertEquals(1.0, notedProgressPct, 0.0); assertTrue(ftpCompleteStatus); } catch (IOException ioe) { ioe.printStackTrace(); fail(); } } private FileTransferSender initializeSession(int testSize) { try { FileTransferSender session = new FileTransferSender(networkNode, counterpartyNodeAddress, testTradeId, testTraderId, testClientId, true, this); // simulate a file for sending FileWriter fileWriter = new FileWriter(session.zipFilePath); char[] buf = new char[testSize]; for (int x = 0; x < testSize; x++) buf[x] = 'A'; fileWriter.write(buf); fileWriter.close(); assertFalse(ftpCompleteStatus); assertEquals(1, progressInvocations); assertEquals(0.0, notedProgressPct, 0.0); assertFalse(session.processAckForFilePart("not_expected_uid")); return session; } catch (IOException e) { e.printStackTrace(); } fail(); return null; } private void simulateAckFromPeerAndVerify(FileTransferSender session, int expectedDataSize, long expectedSeqNum, int expectedProgressInvocations) { FileTransferPart ftp = session.dataAwaitingAck.get(); assertEquals(expectedDataSize, ftp.messageData.size()); assertTrue(session.processAckForFilePart(ftp.uid)); assertEquals(expectedSeqNum, session.currentBlockSeqNum); assertEquals(expectedProgressInvocations, progressInvocations); } @Override public void onFtpProgress(double progressPct) { notedProgressPct = progressPct; progressInvocations++; } @Override public void onFtpComplete(FileTransferSession session) { ftpCompleteStatus = true; } @Override public void onFtpTimeout(String status, FileTransferSession session) { } } ================================================ FILE: core/src/test/java/haveno/core/trade/TradableListTest.java ================================================ /* * 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 . */ package haveno.core.trade; import haveno.core.offer.Offer; import haveno.core.offer.OfferPayload; import haveno.core.offer.OpenOffer; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.mock; import static protobuf.PersistableEnvelope.MessageCase.TRADABLE_LIST; public class TradableListTest { @Test public void protoTesting() { OfferPayload offerPayload = mock(OfferPayload.class, RETURNS_DEEP_STUBS); TradableList openOfferTradableList = new TradableList<>(); protobuf.PersistableEnvelope message = (protobuf.PersistableEnvelope) openOfferTradableList.toProtoMessage(); assertEquals(message.getMessageCase(), TRADABLE_LIST); // test adding an OpenOffer and convert toProto Offer offer = new Offer(offerPayload); OpenOffer openOffer = new OpenOffer(offer, 0, false); openOfferTradableList.add(openOffer); message = (protobuf.PersistableEnvelope) openOfferTradableList.toProtoMessage(); assertEquals(message.getMessageCase(), TRADABLE_LIST); assertEquals(1, message.getTradableList().getTradableList().size()); } } ================================================ FILE: core/src/test/java/haveno/core/user/PreferencesTest.java ================================================ /* * 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 . */ package haveno.core.user; import haveno.common.config.Config; import haveno.common.persistence.PersistenceManager; import haveno.core.api.XmrLocalNode; import haveno.core.locale.CountryUtil; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TraditionalCurrency; import haveno.core.xmr.nodes.XmrNodes; import haveno.core.locale.GlobalSettings; import haveno.core.locale.Res; import javafx.collections.ObservableList; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.Currency; import java.util.List; import java.util.Locale; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class PreferencesTest { private Preferences preferences; private PersistenceManager persistenceManager; private XmrNodes xmrNodes; @BeforeEach public void setUp() { final Locale en_US = new Locale("en", "US"); Locale.setDefault(en_US); GlobalSettings.setLocale(en_US); Res.setBaseCurrencyCode("XMR"); Res.setBaseCurrencyName("Monero"); persistenceManager = mock(PersistenceManager.class); Config config = new Config(); preferences = new Preferences( persistenceManager, config, null, null); xmrNodes = new XmrNodes(); XmrLocalNode xmrLocalNode = new XmrLocalNode(config, preferences, xmrNodes); } @Test public void testAddFiatCurrency() { final TraditionalCurrency usd = new TraditionalCurrency("USD"); final TraditionalCurrency usd2 = new TraditionalCurrency("USD"); final ObservableList traditionalCurrencies = preferences.getTraditionalCurrenciesAsObservable(); preferences.addTraditionalCurrency(usd); assertEquals(1, traditionalCurrencies.size()); preferences.addTraditionalCurrency(usd2); assertEquals(1, traditionalCurrencies.size()); } @Test public void testGetUniqueListOfFiatCurrencies() { PreferencesPayload payload = mock(PreferencesPayload.class); List traditionalCurrencies = CurrencyUtil.getMainTraditionalCurrencies(); final TraditionalCurrency usd = new TraditionalCurrency("USD"); traditionalCurrencies.add(usd); when(persistenceManager.getPersisted(anyString())).thenReturn(payload); when(payload.getUserLanguage()).thenReturn("en"); when(payload.getUserCountry()).thenReturn(CountryUtil.getDefaultCountry()); when(payload.getPreferredTradeCurrency()).thenReturn(usd); when(payload.getTraditionalCurrencies()).thenReturn(traditionalCurrencies); preferences.readPersisted(() -> { assertEquals(7, preferences.getTraditionalCurrenciesAsObservable().size()); assertTrue(preferences.getTraditionalCurrenciesAsObservable().contains(usd)); }); } @Test public void testGetUniqueListOfCryptoCurrencies() { PreferencesPayload payload = mock(PreferencesPayload.class); List cryptoCurrencies = CurrencyUtil.getMainCryptoCurrencies(); final CryptoCurrency dash = new CryptoCurrency("DASH", "Dash"); cryptoCurrencies.add(dash); when(persistenceManager.getPersisted(anyString())).thenReturn(payload); when(payload.getUserLanguage()).thenReturn("en"); when(payload.getUserCountry()).thenReturn(CountryUtil.getDefaultCountry()); when(payload.getPreferredTradeCurrency()).thenReturn(new TraditionalCurrency("USD")); when(payload.getCryptoCurrencies()).thenReturn(cryptoCurrencies); preferences.readPersisted(() -> { assertTrue(preferences.getCryptoCurrenciesAsObservable().contains(dash)); }); } @Test public void testUpdateOfPersistedFiatCurrenciesAfterLocaleChanged() { PreferencesPayload payload = mock(PreferencesPayload.class); List traditionalCurrencies = new ArrayList<>(); final TraditionalCurrency usd = new TraditionalCurrency(Currency.getInstance("USD"), new Locale("de", "AT")); traditionalCurrencies.add(usd); assertEquals("US-Dollar (USD)", usd.getNameAndCode()); when(persistenceManager.getPersisted(anyString())).thenReturn(payload); when(payload.getUserLanguage()).thenReturn("en"); when(payload.getUserCountry()).thenReturn(CountryUtil.getDefaultCountry()); when(payload.getPreferredTradeCurrency()).thenReturn(usd); when(payload.getTraditionalCurrencies()).thenReturn(traditionalCurrencies); preferences.readPersisted(() -> { assertEquals("US Dollar (USD)", preferences.getTraditionalCurrenciesAsObservable().get(0).getNameAndCode()); }); } } ================================================ FILE: core/src/test/java/haveno/core/user/UserPayloadModelVOTest.java ================================================ /* * 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 . */ package haveno.core.user; import com.google.common.collect.Lists; import haveno.core.alert.Alert; import haveno.core.arbitration.ArbitratorTest; import haveno.core.arbitration.MediatorTest; import haveno.core.filter.Filter; import haveno.core.proto.CoreProtoResolver; import org.junit.jupiter.api.Disabled; import java.util.HashSet; public class UserPayloadModelVOTest { @Disabled("TODO InvalidKeySpecException at haveno.common.crypto.Sig.getPublicKeyFromBytes(Sig.java:135)") public void testRoundtrip() { UserPayload vo = new UserPayload(); vo.setAccountId("accountId"); UserPayload newVo = UserPayload.fromProto(vo.toProtoMessage().getUserPayload(), new CoreProtoResolver()); } @Disabled("TODO InvalidKeySpecException at haveno.common.crypto.Sig.getPublicKeyFromBytes(Sig.java:135)") public void testRoundtripFull() { UserPayload vo = new UserPayload(); vo.setAccountId("accountId"); vo.setDisplayedAlert(new Alert("message", true, false, "version", new byte[]{12, -64, 12}, "string", null)); vo.setDevelopersFilter(new Filter(Lists.newArrayList(), Lists.newArrayList(), Lists.newArrayList(), Lists.newArrayList(), Lists.newArrayList(), Lists.newArrayList(), Lists.newArrayList(), Lists.newArrayList(), false, Lists.newArrayList(), null, Lists.newArrayList(), Lists.newArrayList(), Lists.newArrayList(), Lists.newArrayList(), null, 0, null, null, null, null, false, Lists.newArrayList(), new HashSet<>(), false, false)); vo.setRegisteredArbitrator(ArbitratorTest.getArbitratorMock()); vo.setRegisteredMediator(MediatorTest.getMediatorMock()); vo.setAcceptedArbitrators(Lists.newArrayList(ArbitratorTest.getArbitratorMock())); vo.setAcceptedMediators(Lists.newArrayList(MediatorTest.getMediatorMock())); UserPayload newVo = UserPayload.fromProto(vo.toProtoMessage().getUserPayload(), new CoreProtoResolver()); } } ================================================ FILE: core/src/test/java/haveno/core/util/FormattingUtilsTest.java ================================================ package haveno.core.util; import com.natpryce.makeiteasy.Maker; import com.natpryce.makeiteasy.Property; import haveno.core.locale.GlobalSettings; import haveno.core.locale.Res; import haveno.core.monetary.Price; import haveno.core.monetary.TraditionalMoney; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.Locale; import java.util.concurrent.TimeUnit; import static com.natpryce.makeiteasy.MakeItEasy.a; import static com.natpryce.makeiteasy.MakeItEasy.make; import static com.natpryce.makeiteasy.MakeItEasy.with; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; public class FormattingUtilsTest { private static final Property currencyCode = new Property<>(); private static final Property priceString = new Property<>(); private static final Maker usdPrice = a(lookup -> new Price(TraditionalMoney.parseTraditionalMoney(lookup.valueOf(currencyCode, "USD"), lookup.valueOf(priceString, "100")))); @BeforeEach public void setUp() { Locale.setDefault(new Locale("en", "US")); GlobalSettings.setLocale(new Locale("en", "US")); Res.setBaseCurrencyCode("XMR"); Res.setBaseCurrencyName("Monero"); } @Test public void testFormatDurationAsWords() { long oneDay = TimeUnit.DAYS.toMillis(1); long oneHour = TimeUnit.HOURS.toMillis(1); long oneMinute = TimeUnit.MINUTES.toMillis(1); long oneSecond = TimeUnit.SECONDS.toMillis(1); assertEquals("1 hour, 0 minutes", FormattingUtils.formatDurationAsWords(oneHour)); assertEquals("1 day, 0 hours, 0 minutes", FormattingUtils.formatDurationAsWords(oneDay)); assertEquals("2 days, 0 hours, 1 minute", FormattingUtils.formatDurationAsWords(oneDay * 2 + oneMinute)); assertEquals("2 days, 0 hours, 2 minutes", FormattingUtils.formatDurationAsWords(oneDay * 2 + oneMinute * 2)); assertEquals("1 hour, 0 minutes, 0 seconds", FormattingUtils.formatDurationAsWords(oneHour, true, true)); assertEquals("1 hour, 0 minutes, 1 second", FormattingUtils.formatDurationAsWords(oneHour + oneSecond, true, true)); assertEquals("1 hour, 0 minutes, 2 seconds", FormattingUtils.formatDurationAsWords(oneHour + oneSecond * 2, true, true)); assertEquals("2 days, 21 hours, 28 minutes", FormattingUtils.formatDurationAsWords(oneDay * 2 + oneHour * 21 + oneMinute * 28)); assertEquals("110 days", FormattingUtils.formatDurationAsWords(oneDay * 110, false, false)); assertEquals("10 days, 10 hours, 10 minutes, 10 seconds", FormattingUtils.formatDurationAsWords(oneDay * 10 + oneHour * 10 + oneMinute * 10 + oneSecond * 10, true, false)); assertEquals("1 hour, 2 seconds", FormattingUtils.formatDurationAsWords(oneHour + oneSecond * 2, true, false)); assertEquals("1 hour", FormattingUtils.formatDurationAsWords(oneHour + oneSecond * 2, false, false)); assertEquals("0 hours, 0 minutes, 1 second", FormattingUtils.formatDurationAsWords(oneSecond, true, true)); assertEquals("1 second", FormattingUtils.formatDurationAsWords(oneSecond, true, false)); assertEquals("0 hours", FormattingUtils.formatDurationAsWords(oneSecond, false, false)); assertEquals("", FormattingUtils.formatDurationAsWords(0)); assertTrue(FormattingUtils.formatDurationAsWords(0).isEmpty()); } @Test public void testFormatPrice() { assertEquals("100.0000", FormattingUtils.formatPrice(make(usdPrice))); assertEquals("7098.4700", FormattingUtils.formatPrice(make(usdPrice.but(with(priceString, "7098.4700"))))); } } ================================================ FILE: core/src/test/java/haveno/core/util/ProtoUtilTest.java ================================================ /* * 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 . */ package haveno.core.util; import haveno.core.offer.OfferDirection; import haveno.core.offer.OpenOffer; import org.junit.jupiter.api.Test; import protobuf.OpenOffer.State; import static haveno.common.proto.ProtoUtil.enumFromProto; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; @SuppressWarnings("UnusedAssignment") public class ProtoUtilTest { //TODO Use NetworkProtoResolver, PersistenceProtoResolver or ProtoResolver which are all in haveno.common. @Test public void testEnum() { OfferDirection direction = OfferDirection.SELL; OfferDirection direction2 = OfferDirection.BUY; OfferDirection realDirection = getDirection(direction); OfferDirection realDirection2 = getDirection(direction2); assertEquals("SELL", realDirection.name()); assertEquals("BUY", realDirection2.name()); } @Test public void testUnknownEnum() { State result = State.PB_ERROR; try { OpenOffer.State.valueOf(result.name()); fail(); } catch (IllegalArgumentException ignore) { } } @Test public void testUnknownEnumFix() { State result = State.PB_ERROR; try { enumFromProto(OpenOffer.State.class, result.name()); assertEquals(OpenOffer.State.AVAILABLE, enumFromProto(OpenOffer.State.class, "AVAILABLE")); } catch (IllegalArgumentException e) { fail(); } } public static OfferDirection getDirection(OfferDirection direction) { return OfferDirection.valueOf(direction.name()); } } ================================================ FILE: core/src/test/java/haveno/core/util/RegexValidatorTest.java ================================================ /* * 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 . */ package haveno.core.util; import haveno.core.locale.GlobalSettings; import haveno.core.locale.Res; import haveno.core.util.validation.RegexValidator; import haveno.core.util.validation.RegexValidatorFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.Locale; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class RegexValidatorTest { @BeforeEach public void setup() { Locale.setDefault(new Locale("en", "US")); GlobalSettings.setLocale(new Locale("en", "US")); Res.setBaseCurrencyCode("XMR"); Res.setBaseCurrencyName("Monero"); } @Test public void testAddressRegexValidator() { RegexValidator regexValidator = RegexValidatorFactory.addressRegexValidator(); assertTrue(regexValidator.validate("").isValid); assertFalse(regexValidator.validate(" ").isValid); // onion V2 addresses assertTrue(regexValidator.validate("abcdefghij234567.onion").isValid); assertTrue(regexValidator.validate("abcdefghijklmnop.onion,abcdefghijklmnop.onion").isValid); assertTrue(regexValidator.validate("abcdefghijklmnop.onion, abcdefghijklmnop.onion").isValid); assertTrue(regexValidator.validate("qrstuvwxyzABCDEF.onion,qrstuvwxyzABCDEF.onion,aaaaaaaaaaaaaaaa.onion").isValid); assertTrue(regexValidator.validate("GHIJKLMNOPQRSTUV.onion:9999").isValid); assertTrue(regexValidator.validate("WXYZ234567abcdef.onion,GHIJKLMNOPQRSTUV.onion:9999").isValid); assertTrue(regexValidator.validate("aaaaaaaaaaaaaaaa.onion:9999,WXYZ234567abcdef.onion:9999,2222222222222222.onion:9999").isValid); assertFalse(regexValidator.validate("abcd.onion").isValid); assertFalse(regexValidator.validate("abcdefghijklmnop,abcdefghijklmnop.onion").isValid); assertFalse(regexValidator.validate("abcdefghi2345689.onion:9999").isValid); assertFalse(regexValidator.validate("onion:9999,abcdefghijklmnop.onion:9999").isValid); assertFalse(regexValidator.validate("abcdefghijklmnop.onion:").isValid); // onion v3 addresses assertFalse(regexValidator.validate("32zzibxmqi2ybxpqyggwwuwz7a3lbvtzoloti7cxoevyvijexvgsfei.onion:8333").isValid); // 1 missing char assertTrue(regexValidator.validate("wizseedscybbttk4bmb2lzvbuk2jtect37lcpva4l3twktmkzemwbead.onion:8000").isValid); // ipv4 addresses assertTrue(regexValidator.validate("12.34.56.78").isValid); assertTrue(regexValidator.validate("12.34.56.78,87.65.43.21").isValid); assertTrue(regexValidator.validate("12.34.56.78:8888").isValid); assertFalse(regexValidator.validate("12.34.56.788").isValid); assertFalse(regexValidator.validate("12.34.56.78:").isValid); // ipv6 addresses assertTrue(regexValidator.validate("FE80:0000:0000:0000:0202:B3FF:FE1E:8329").isValid); assertTrue(regexValidator.validate("FE80::0202:B3FF:FE1E:8329").isValid); assertTrue(regexValidator.validate("FE80::0202:B3FF:FE1E:8329,FE80:0000:0000:0000:0202:B3FF:FE1E:8329").isValid); assertTrue(regexValidator.validate("::1").isValid); assertTrue(regexValidator.validate("fe80::").isValid); assertTrue(regexValidator.validate("2001::").isValid); assertTrue(regexValidator.validate("[::1]:8333").isValid); assertTrue(regexValidator.validate("[FE80::0202:B3FF:FE1E:8329]:8333").isValid); assertTrue(regexValidator.validate("[2001:db8::1]:80").isValid); assertTrue(regexValidator.validate("[aaaa::bbbb]:8333").isValid); assertFalse(regexValidator.validate("1200:0000:AB00:1234:O000:2552:7777:1313").isValid); // fqdn addresses assertTrue(regexValidator.validate("example.com").isValid); assertTrue(regexValidator.validate("mynode.local:8333").isValid); assertTrue(regexValidator.validate("foo.example.com,bar.example.com").isValid); assertTrue(regexValidator.validate("foo.example.com:8333,bar.example.com:8333").isValid); assertFalse(regexValidator.validate("mynode.local:65536").isValid); assertFalse(regexValidator.validate("-example.com").isValid); assertFalse(regexValidator.validate("example-.com").isValid); } @Test public void testOnionAddressRegexValidator() { RegexValidator regexValidator = RegexValidatorFactory.onionAddressRegexValidator(); assertTrue(regexValidator.validate("").isValid); assertFalse(regexValidator.validate(" ").isValid); // onion V2 addresses assertTrue(regexValidator.validate("abcdefghij234567.onion").isValid); assertTrue(regexValidator.validate("abcdefghijklmnop.onion,abcdefghijklmnop.onion").isValid); assertTrue(regexValidator.validate("abcdefghijklmnop.onion, abcdefghijklmnop.onion").isValid); assertTrue(regexValidator.validate("qrstuvwxyzABCDEF.onion,qrstuvwxyzABCDEF.onion,aaaaaaaaaaaaaaaa.onion").isValid); assertTrue(regexValidator.validate("GHIJKLMNOPQRSTUV.onion:9999").isValid); assertTrue(regexValidator.validate("WXYZ234567abcdef.onion,GHIJKLMNOPQRSTUV.onion:9999").isValid); assertTrue(regexValidator.validate("aaaaaaaaaaaaaaaa.onion:9999,WXYZ234567abcdef.onion:9999,2222222222222222.onion:9999").isValid); assertFalse(regexValidator.validate("abcd.onion").isValid); assertFalse(regexValidator.validate("abcdefghijklmnop,abcdefghijklmnop.onion").isValid); assertFalse(regexValidator.validate("abcdefghi2345689.onion:9999").isValid); assertFalse(regexValidator.validate("onion:9999,abcdefghijklmnop.onion:9999").isValid); assertFalse(regexValidator.validate("abcdefghijklmnop.onion:").isValid); // onion v3 addresses assertFalse(regexValidator.validate("32zzibxmqi2ybxpqyggwwuwz7a3lbvtzoloti7cxoevyvijexvgsfei.onion:8333").isValid); // 1 missing char assertTrue(regexValidator.validate("wizseedscybbttk4bmb2lzvbuk2jtect37lcpva4l3twktmkzemwbead.onion:8000").isValid); } @Test public void testLocalnetAddressRegexValidator() { RegexValidator regexValidator = RegexValidatorFactory.localnetAddressRegexValidator(); assertTrue(regexValidator.validate("").isValid); assertFalse(regexValidator.validate(" ").isValid); // onion V2 addresses assertFalse(regexValidator.validate("abcdefghij234567.onion").isValid); assertFalse(regexValidator.validate("abcdefghijklmnop.onion,abcdefghijklmnop.onion").isValid); assertFalse(regexValidator.validate("abcdefghijklmnop.onion, abcdefghijklmnop.onion").isValid); assertFalse(regexValidator.validate("qrstuvwxyzABCDEF.onion,qrstuvwxyzABCDEF.onion,aaaaaaaaaaaaaaaa.onion").isValid); assertFalse(regexValidator.validate("GHIJKLMNOPQRSTUV.onion:9999").isValid); assertFalse(regexValidator.validate("WXYZ234567abcdef.onion,GHIJKLMNOPQRSTUV.onion:9999").isValid); assertFalse(regexValidator.validate("aaaaaaaaaaaaaaaa.onion:9999,WXYZ234567abcdef.onion:9999,2222222222222222.onion:9999").isValid); assertFalse(regexValidator.validate("abcd.onion").isValid); assertFalse(regexValidator.validate("abcdefghijklmnop,abcdefghijklmnop.onion").isValid); assertFalse(regexValidator.validate("abcdefghi2345689.onion:9999").isValid); assertFalse(regexValidator.validate("onion:9999,abcdefghijklmnop.onion:9999").isValid); assertFalse(regexValidator.validate("abcdefghijklmnop.onion:").isValid); // onion v3 addresses assertFalse(regexValidator.validate("32zzibxmqi2ybxpqyggwwuwz7a3lbvtzoloti7cxoevyvijexvgsfei.onion:8333").isValid); // 1 missing char assertFalse(regexValidator.validate("wizseedscybbttk4bmb2lzvbuk2jtect37lcpva4l3twktmkzemwbead.onion:8000").isValid); // ipv4 addresses assertFalse(regexValidator.validate("12.34.56.78").isValid); assertFalse(regexValidator.validate("12.34.56.78,87.65.43.21").isValid); assertFalse(regexValidator.validate("12.34.56.78:8888").isValid); assertFalse(regexValidator.validate("12.34.56.788").isValid); assertFalse(regexValidator.validate("12.34.56.78:").isValid); // ipv4 local addresses assertTrue(regexValidator.validate("10.10.10.10").isValid); assertTrue(regexValidator.validate("172.19.1.1").isValid); assertTrue(regexValidator.validate("172.19.1.1").isValid); assertTrue(regexValidator.validate("192.168.1.1").isValid); assertTrue(regexValidator.validate("192.168.1.1,172.16.1.1").isValid); assertTrue(regexValidator.validate("192.168.1.1:8888,192.168.1.2:8888").isValid); assertFalse(regexValidator.validate("192.168.1.888").isValid); assertFalse(regexValidator.validate("192.168.1.1:").isValid); // ipv4 autolocal addresses assertTrue(regexValidator.validate("169.254.123.232").isValid); // ipv6 local addresses assertTrue(regexValidator.validate("fe80:2:3:4:5:6:7:8").isValid); assertTrue(regexValidator.validate("fe80::").isValid); assertTrue(regexValidator.validate("fc00::").isValid); assertTrue(regexValidator.validate("fd00::,fe80::1").isValid); assertTrue(regexValidator.validate("fd00::8").isValid); assertTrue(regexValidator.validate("fd00::7:8").isValid); assertTrue(regexValidator.validate("fd00::6:7:8").isValid); assertTrue(regexValidator.validate("fd00::5:6:7:8").isValid); assertTrue(regexValidator.validate("fd00::4:5:6:7:8").isValid); assertTrue(regexValidator.validate("fd00::3:4:5:6:7:8").isValid); assertTrue(regexValidator.validate("fd00:2:3:4:5:6:7:8").isValid); assertTrue(regexValidator.validate("fd00::0202:B3FF:FE1E:8329").isValid); assertTrue(regexValidator.validate("fd00::0202:B3FF:FE1E:8329,FE80::0202:B3FF:FE1E:8329").isValid); // ipv6 local with optional port at the end assertTrue(regexValidator.validate("[fd00::1]:8081").isValid); assertTrue(regexValidator.validate("[fd00::1]:8081,[fc00::1]:8081").isValid); assertTrue(regexValidator.validate("[FE80::0202:B3FF:FE1E:8329]:8333").isValid); // ipv6 loopback assertFalse(regexValidator.validate("::1").isValid); // ipv6 unicast assertFalse(regexValidator.validate("2001::").isValid); assertFalse(regexValidator.validate("[::1]:8333").isValid); assertFalse(regexValidator.validate("[2001:db8::1]:80").isValid); assertFalse(regexValidator.validate("[aaaa::bbbb]:8333").isValid); assertFalse(regexValidator.validate("1200:0000:AB00:1234:O000:2552:7777:1313").isValid); // *.local fqdn hostnames assertTrue(regexValidator.validate("mynode.local").isValid); assertTrue(regexValidator.validate("mynode.local:8081").isValid); // non-local fqdn hostnames assertFalse(regexValidator.validate("example.com").isValid); assertFalse(regexValidator.validate("foo.example.com,bar.example.com").isValid); assertFalse(regexValidator.validate("foo.example.com:8333,bar.example.com:8333").isValid); // invalid fqdn hostnames assertFalse(regexValidator.validate("mynode.local:65536").isValid); assertFalse(regexValidator.validate("-example.com").isValid); assertFalse(regexValidator.validate("example-.com").isValid); } @Test public void testLocalhostAddressRegexValidator() { RegexValidator regexValidator = RegexValidatorFactory.localhostAddressRegexValidator(); assertTrue(regexValidator.validate("").isValid); assertFalse(regexValidator.validate(" ").isValid); // onion V2 addresses assertFalse(regexValidator.validate("abcdefghij234567.onion").isValid); assertFalse(regexValidator.validate("abcdefghijklmnop.onion,abcdefghijklmnop.onion").isValid); assertFalse(regexValidator.validate("abcdefghijklmnop.onion, abcdefghijklmnop.onion").isValid); assertFalse(regexValidator.validate("qrstuvwxyzABCDEF.onion,qrstuvwxyzABCDEF.onion,aaaaaaaaaaaaaaaa.onion").isValid); assertFalse(regexValidator.validate("GHIJKLMNOPQRSTUV.onion:9999").isValid); assertFalse(regexValidator.validate("WXYZ234567abcdef.onion,GHIJKLMNOPQRSTUV.onion:9999").isValid); assertFalse(regexValidator.validate("aaaaaaaaaaaaaaaa.onion:9999,WXYZ234567abcdef.onion:9999,2222222222222222.onion:9999").isValid); assertFalse(regexValidator.validate("abcd.onion").isValid); assertFalse(regexValidator.validate("abcdefghijklmnop,abcdefghijklmnop.onion").isValid); assertFalse(regexValidator.validate("abcdefghi2345689.onion:9999").isValid); assertFalse(regexValidator.validate("onion:9999,abcdefghijklmnop.onion:9999").isValid); assertFalse(regexValidator.validate("abcdefghijklmnop.onion:").isValid); // onion v3 addresses assertFalse(regexValidator.validate("32zzibxmqi2ybxpqyggwwuwz7a3lbvtzoloti7cxoevyvijexvgsfei.onion:8333").isValid); // 1 missing char assertFalse(regexValidator.validate("wizseedscybbttk4bmb2lzvbuk2jtect37lcpva4l3twktmkzemwbead.onion:8000").isValid); // ipv4 addresses assertFalse(regexValidator.validate("12.34.56.78").isValid); assertFalse(regexValidator.validate("12.34.56.78,87.65.43.21").isValid); assertFalse(regexValidator.validate("12.34.56.78:8888").isValid); assertFalse(regexValidator.validate("12.34.56.788").isValid); assertFalse(regexValidator.validate("12.34.56.78:").isValid); // ipv4 loopback addresses assertTrue(regexValidator.validate("127.0.0.1").isValid); assertTrue(regexValidator.validate("127.0.1.1").isValid); // ipv4 local addresses assertFalse(regexValidator.validate("10.10.10.10").isValid); assertFalse(regexValidator.validate("172.19.1.1").isValid); assertFalse(regexValidator.validate("172.19.1.1").isValid); assertFalse(regexValidator.validate("192.168.1.1").isValid); assertFalse(regexValidator.validate("192.168.1.1,172.16.1.1").isValid); assertFalse(regexValidator.validate("192.168.1.1:8888,192.168.1.2:8888").isValid); assertFalse(regexValidator.validate("192.168.1.888").isValid); assertFalse(regexValidator.validate("192.168.1.1:").isValid); // ipv4 autolocal addresses assertFalse(regexValidator.validate("169.254.123.232").isValid); // ipv6 local addresses assertFalse(regexValidator.validate("fe80::").isValid); assertFalse(regexValidator.validate("fc00::").isValid); assertFalse(regexValidator.validate("fd00::8").isValid); assertFalse(regexValidator.validate("fd00::7:8").isValid); assertFalse(regexValidator.validate("fd00::6:7:8").isValid); assertFalse(regexValidator.validate("fd00::5:6:7:8").isValid); assertFalse(regexValidator.validate("fd00::3:4:5:6:7:8").isValid); assertFalse(regexValidator.validate("fd00::4:5:6:7:8").isValid); assertFalse(regexValidator.validate("fd00:2:3:4:5:6:7:8").isValid); assertFalse(regexValidator.validate("fd00::0202:B3FF:FE1E:8329").isValid); assertFalse(regexValidator.validate("FE80:0000:0000:0000:0202:B3FF:FE1E:8329").isValid); assertFalse(regexValidator.validate("FE80::0202:B3FF:FE1E:8329").isValid); assertFalse(regexValidator.validate("FE80::0202:B3FF:FE1E:8329,FE80:0000:0000:0000:0202:B3FF:FE1E:8329").isValid); // ipv6 local with optional port at the end assertFalse(regexValidator.validate("[fd00::1]:8081").isValid); assertFalse(regexValidator.validate("[fd00::1]:8081,[fc00::1]:8081").isValid); // ipv6 loopback assertTrue(regexValidator.validate("::1").isValid); assertTrue(regexValidator.validate("::2").isValid); assertTrue(regexValidator.validate("[::1]:8333").isValid); // ipv6 unicast assertFalse(regexValidator.validate("2001::").isValid); assertFalse(regexValidator.validate("[FE80::0202:B3FF:FE1E:8329]:8333").isValid); assertFalse(regexValidator.validate("[2001:db8::1]:80").isValid); assertFalse(regexValidator.validate("[aaaa::bbbb]:8333").isValid); assertFalse(regexValidator.validate("1200:0000:AB00:1234:O000:2552:7777:1313").isValid); // localhost fqdn hostnames assertTrue(regexValidator.validate("localhost").isValid); assertTrue(regexValidator.validate("localhost:8081").isValid); // local fqdn hostnames assertFalse(regexValidator.validate("mynode.local:8081").isValid); // non-local fqdn hostnames assertFalse(regexValidator.validate("example.com").isValid); assertFalse(regexValidator.validate("foo.example.com,bar.example.com").isValid); assertFalse(regexValidator.validate("foo.example.com:8333,bar.example.com:8333").isValid); // invalid fqdn hostnames assertFalse(regexValidator.validate("mynode.local:65536").isValid); assertFalse(regexValidator.validate("-example.com").isValid); assertFalse(regexValidator.validate("example-.com").isValid); } } ================================================ FILE: core/src/test/java/haveno/core/util/coin/CoinUtilTest.java ================================================ /* * 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 . */ package haveno.core.util.coin; import haveno.core.monetary.Price; import haveno.core.trade.HavenoUtils; import haveno.core.xmr.wallet.Restrictions; import org.bitcoinj.core.Coin; import org.junit.jupiter.api.Test; import java.math.BigInteger; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; public class CoinUtilTest { @Test public void testGetPercentOfAmount() { BigInteger bi = new BigInteger("703100000000"); assertEquals(new BigInteger("105465000000"), HavenoUtils.multiply(bi, .15)); } @Test public void testGetFeePerXmr() { assertEquals(HavenoUtils.xmrToAtomicUnits(1), HavenoUtils.multiply(HavenoUtils.xmrToAtomicUnits(1), 1.0)); assertEquals(HavenoUtils.xmrToAtomicUnits(0.1), HavenoUtils.multiply(HavenoUtils.xmrToAtomicUnits(0.1), 1.0)); assertEquals(HavenoUtils.xmrToAtomicUnits(0.01), HavenoUtils.multiply(HavenoUtils.xmrToAtomicUnits(0.1), 0.1)); assertEquals(HavenoUtils.xmrToAtomicUnits(0.015), HavenoUtils.multiply(HavenoUtils.xmrToAtomicUnits(0.3), 0.05)); } @Test public void testParseXmr() { String xmrStr = "0.266394780889"; BigInteger au = HavenoUtils.parseXmr(xmrStr); assertEquals(new BigInteger("266394780889"), au); assertEquals(xmrStr, "" + HavenoUtils.atomicUnitsToXmr(au)); assertEquals(xmrStr, HavenoUtils.formatXmr(au, false)); } @Test public void testMinCoin() { assertEquals(Coin.parseCoin("1"), CoinUtil.minCoin(Coin.parseCoin("1"), Coin.parseCoin("1"))); assertEquals(Coin.parseCoin("0.1"), CoinUtil.minCoin(Coin.parseCoin("0.1"), Coin.parseCoin("1"))); assertEquals(Coin.parseCoin("0.01"), CoinUtil.minCoin(Coin.parseCoin("0.1"), Coin.parseCoin("0.01"))); assertEquals(Coin.parseCoin("0"), CoinUtil.minCoin(Coin.parseCoin("0"), Coin.parseCoin("0.05"))); assertEquals(Coin.parseCoin("0"), CoinUtil.minCoin(Coin.parseCoin("0.05"), Coin.parseCoin("0"))); } @Test public void testMaxCoin() { assertEquals(Coin.parseCoin("1"), CoinUtil.maxCoin(Coin.parseCoin("1"), Coin.parseCoin("1"))); assertEquals(Coin.parseCoin("1"), CoinUtil.maxCoin(Coin.parseCoin("0.1"), Coin.parseCoin("1"))); assertEquals(Coin.parseCoin("0.1"), CoinUtil.maxCoin(Coin.parseCoin("0.1"), Coin.parseCoin("0.01"))); assertEquals(Coin.parseCoin("0.05"), CoinUtil.maxCoin(Coin.parseCoin("0"), Coin.parseCoin("0.05"))); assertEquals(Coin.parseCoin("0.05"), CoinUtil.maxCoin(Coin.parseCoin("0.05"), Coin.parseCoin("0"))); } @Test public void testGetAdjustedAmount() { BigInteger result = CoinUtil.getAdjustedAmount( HavenoUtils.xmrToAtomicUnits(0.1), Price.valueOf("USD", 1000_0000), Restrictions.getMinTradeAmount(), HavenoUtils.xmrToAtomicUnits(0.2), 1); assertEquals( HavenoUtils.formatXmr(Restrictions.getMinTradeAmount(), true), HavenoUtils.formatXmr(result, true), "Minimum trade amount allowed should be adjusted to the smallest trade allowed." ); try { CoinUtil.getAdjustedAmount( BigInteger.ZERO, Price.valueOf("USD", 1000_0000), HavenoUtils.xmrToAtomicUnits(0.1), HavenoUtils.xmrToAtomicUnits(0.2), 1); fail("Expected IllegalArgumentException to be thrown when amount is too low."); } catch (IllegalArgumentException iae) { assertEquals( "amount must be above minimum of 0.05 xmr but was 0.0 xmr", iae.getMessage(), "Unexpected exception message." ); } result = CoinUtil.getAdjustedAmount( HavenoUtils.xmrToAtomicUnits(0.1), Price.valueOf("USD", 1000_0000), Restrictions.getMinTradeAmount(), HavenoUtils.xmrToAtomicUnits(0.2), 1); assertEquals( "0.05 XMR", HavenoUtils.formatXmr(result, true), "Minimum allowed trade amount should not be adjusted." ); result = CoinUtil.getAdjustedAmount( HavenoUtils.xmrToAtomicUnits(0.1), Price.valueOf("USD", 1000_0000), Restrictions.getMinTradeAmount(), HavenoUtils.xmrToAtomicUnits(0.25), 1); assertEquals( "0.05 XMR", HavenoUtils.formatXmr(result, true), "Minimum trade amount allowed should respect maxTradeLimit and factor, if possible." ); result = CoinUtil.getAdjustedAmount( HavenoUtils.xmrToAtomicUnits(0.1), Price.valueOf("USD", 1000_0000), HavenoUtils.xmrToAtomicUnits(0.1), HavenoUtils.xmrToAtomicUnits(0.5), 1); assertEquals( "0.10 XMR", HavenoUtils.formatXmr(result, true), "Minimum trade amount allowed with low maxTradeLimit should still respect that limit, even if result does not respect the factor specified." ); } } ================================================ FILE: core/src/test/java/haveno/core/xmr/nodes/BtcNetworkConfigTest.java ================================================ /* * 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 . */ package haveno.core.xmr.nodes; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import haveno.core.xmr.setup.WalletConfig; import haveno.network.Socks5MultiDiscovery; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.PeerAddress; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.Collections; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; public class BtcNetworkConfigTest { private static final int MODE = 0; private WalletConfig delegate; @BeforeEach public void setUp() { delegate = mock(WalletConfig.class); } @Test public void testProposePeersWhenProxyPresentAndNoPeers() { XmrNetworkConfig config = new XmrNetworkConfig(delegate, mock(NetworkParameters.class), MODE, mock(Socks5Proxy.class)); config.proposePeers(Collections.emptyList()); verify(delegate, never()).setPeerNodes(any()); verify(delegate).setDiscovery(any(Socks5MultiDiscovery.class)); } @Test public void testProposePeersWhenProxyNotPresentAndNoPeers() { XmrNetworkConfig config = new XmrNetworkConfig(delegate, mock(NetworkParameters.class), MODE, null); config.proposePeers(Collections.emptyList()); verify(delegate, never()).setDiscovery(any(Socks5MultiDiscovery.class)); verify(delegate, never()).setPeerNodes(any()); } @Test public void testProposePeersWhenPeersPresent() { XmrNetworkConfig config = new XmrNetworkConfig(delegate, mock(NetworkParameters.class), MODE, null); config.proposePeers(Collections.singletonList(mock(PeerAddress.class))); verify(delegate, never()).setDiscovery(any(Socks5MultiDiscovery.class)); verify(delegate).setPeerNodes(any()); } } ================================================ FILE: core/src/test/java/haveno/core/xmr/nodes/XmrNodeConverterTest.java ================================================ /* * 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 . */ package haveno.core.xmr.nodes; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import haveno.core.xmr.nodes.XmrNodeConverter.Facade; import haveno.core.xmr.nodes.XmrNodes.XmrNode; import haveno.network.DnsLookupException; import org.bitcoinj.core.PeerAddress; import org.junit.jupiter.api.Test; import java.net.InetAddress; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class XmrNodeConverterTest { @Test public void testConvertOnionHost() { XmrNode node = mock(XmrNode.class); when(node.getOnionAddress()).thenReturn("aaa.onion"); //InetAddress inetAddress = mock(InetAddress.class); Facade facade = mock(Facade.class); //when(facade.onionHostToInetAddress(any())).thenReturn(inetAddress); PeerAddress peerAddress = new XmrNodeConverter(facade).convertOnionHost(node); // noinspection ConstantConditions assertEquals(node.getOnionAddress(), peerAddress.getHostname()); } @Test public void testConvertClearNode() { final String ip = "192.168.0.1"; XmrNode node = mock(XmrNode.class); when(node.getHostNameOrAddress()).thenReturn(ip); PeerAddress peerAddress = new XmrNodeConverter().convertClearNode(node); // noinspection ConstantConditions InetAddress inetAddress = peerAddress.getAddr(); assertEquals(ip, inetAddress.getHostAddress()); } @Test public void testConvertWithTor() throws DnsLookupException { InetAddress expected = mock(InetAddress.class); Facade facade = mock(Facade.class); when(facade.torLookup(any(), anyString())).thenReturn(expected); XmrNode node = mock(XmrNode.class); when(node.getHostNameOrAddress()).thenReturn("aaa.onion"); PeerAddress peerAddress = new XmrNodeConverter(facade).convertWithTor(node, mock(Socks5Proxy.class)); // noinspection ConstantConditions assertEquals(expected, peerAddress.getAddr()); } } ================================================ FILE: core/src/test/java/haveno/core/xmr/nodes/XmrNodesRepositoryTest.java ================================================ /* * 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 . */ package haveno.core.xmr.nodes; import com.google.common.collect.Lists; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import haveno.core.xmr.nodes.XmrNodes.XmrNode; import org.bitcoinj.core.PeerAddress; import org.junit.jupiter.api.Test; import java.util.Collections; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class XmrNodesRepositoryTest { @Test public void testGetPeerAddressesWhenClearNodes() { XmrNode node = mock(XmrNode.class); when(node.hasClearNetAddress()).thenReturn(true); XmrNodeConverter converter = mock(XmrNodeConverter.class, RETURNS_DEEP_STUBS); XmrNodesRepository repository = new XmrNodesRepository(converter, Collections.singletonList(node)); List peers = repository.getPeerAddresses(null, false); assertFalse(peers.isEmpty()); } @Test public void testGetPeerAddressesWhenConverterReturnsNull() { XmrNodeConverter converter = mock(XmrNodeConverter.class); when(converter.convertClearNode(any())).thenReturn(null); XmrNode node = mock(XmrNode.class); when(node.hasClearNetAddress()).thenReturn(true); XmrNodesRepository repository = new XmrNodesRepository(converter, Collections.singletonList(node)); List peers = repository.getPeerAddresses(null, false); verify(converter).convertClearNode(any()); assertTrue(peers.isEmpty()); } @Test public void testGetPeerAddressesWhenProxyAndClearNodes() { XmrNode node = mock(XmrNode.class); when(node.hasClearNetAddress()).thenReturn(true); XmrNode onionNode = mock(XmrNode.class); when(node.hasOnionAddress()).thenReturn(true); XmrNodeConverter converter = mock(XmrNodeConverter.class, RETURNS_DEEP_STUBS); XmrNodesRepository repository = new XmrNodesRepository(converter, Lists.newArrayList(node, onionNode)); List peers = repository.getPeerAddresses(mock(Socks5Proxy.class), true); assertEquals(2, peers.size()); } @Test public void testGetPeerAddressesWhenOnionNodesOnly() { XmrNode node = mock(XmrNode.class); when(node.hasClearNetAddress()).thenReturn(true); XmrNode onionNode = mock(XmrNode.class); when(node.hasOnionAddress()).thenReturn(true); XmrNodeConverter converter = mock(XmrNodeConverter.class, RETURNS_DEEP_STUBS); XmrNodesRepository repository = new XmrNodesRepository(converter, Lists.newArrayList(node, onionNode)); List peers = repository.getPeerAddresses(mock(Socks5Proxy.class), false); assertEquals(1, peers.size()); } } ================================================ FILE: core/src/test/java/haveno/core/xmr/nodes/XmrNodesSetupPreferencesTest.java ================================================ /* * 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 . */ package haveno.core.xmr.nodes; import haveno.core.user.Preferences; import haveno.core.xmr.nodes.XmrNodes.XmrNode; import org.junit.jupiter.api.Test; import java.util.List; import static haveno.core.xmr.nodes.XmrNodes.MoneroNodesOption.CUSTOM; import static haveno.core.xmr.nodes.XmrNodes.MoneroNodesOption.PUBLIC; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class XmrNodesSetupPreferencesTest { @Test public void testSelectPreferredNodesWhenPublicOption() { Preferences delegate = mock(Preferences.class); when(delegate.getMoneroNodesOptionOrdinal()).thenReturn(PUBLIC.ordinal()); XmrNodesSetupPreferences preferences = new XmrNodesSetupPreferences(delegate); List nodes = preferences.selectPreferredNodes(mock(XmrNodes.class)); assertTrue(nodes.isEmpty()); } @Test public void testSelectPreferredNodesWhenCustomOption() { Preferences delegate = mock(Preferences.class); when(delegate.getMoneroNodesOptionOrdinal()).thenReturn(CUSTOM.ordinal()); when(delegate.getMoneroNodes()).thenReturn("aaa.onion,bbb.onion"); XmrNodesSetupPreferences preferences = new XmrNodesSetupPreferences(delegate); List nodes = preferences.selectPreferredNodes(mock(XmrNodes.class)); assertEquals(2, nodes.size()); } } ================================================ FILE: core/src/test/java/haveno/core/xmr/wallet/RestrictionsTest.java ================================================ /* * 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 . */ package haveno.core.xmr.wallet; import org.bitcoinj.core.Coin; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @SuppressWarnings("ConstantConditions") public class RestrictionsTest { @Test public void testIsMinSpendableAmount() { Coin amount = null; Coin txFee = Coin.valueOf(20000); amount = Coin.ZERO; assertFalse(Restrictions.isAboveDust(amount.subtract(txFee))); amount = txFee; assertFalse(Restrictions.isAboveDust(amount.subtract(txFee))); amount = Restrictions.getMinNonDustOutput(); assertFalse(Restrictions.isAboveDust(amount.subtract(txFee))); amount = txFee.add(Restrictions.getMinNonDustOutput()); assertTrue(Restrictions.isAboveDust(amount.subtract(txFee))); amount = txFee.add(Restrictions.getMinNonDustOutput()).add(Coin.valueOf(1)); assertTrue(Restrictions.isAboveDust(amount.subtract(txFee))); } } ================================================ FILE: core/src/test/resources/haveno/core/app/cli-output.txt ================================================ Haveno Test version 0.1.0 Usage: haveno-test [options] Options: --name= (default: Haveno) The name of the Haveno node --another-option= (default: WAT) This is a long description which will need to break over multiple linessssssssssss such that no line is longer than 80 characters in the help output. --exactly-72-char-description= 012345678911234567892123456789312345678941234567895123456789612345678971 --exactly-72-char-description-with-spaces= 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1 --90-char-description-without-spaces= -123456789-223456789-323456789-423456789-523456789-623456789-723456789-823456789-923456789 --90-char-description-with-space-at-char-80= -123456789-223456789-323456789-423456789-523456789-623456789-723456789-823456789 923456789 --90-char-description-with-spaces-at-chars-5-and-80= -123 56789-223456789-323456789-423456789-523456789-623456789-723456789-823456789 923456789 --90-char-description-with-space-at-char-73= -123456789-223456789-323456789-423456789-523456789-623456789-723456789-8 3456789-923456789 --1-char-description-with-only-a-space= --empty-description= --no-description= --no-arg Some description --optional-arg= Option description --with-default-value= (default: Wat) Some option with a default value --data-dir= (default: /Users/cbeams/Library/Application Support/Haveno) Application data directory --enum-opt= (default: foo) Some option that accepts an enum value as an argument ================================================ FILE: core/src/test/resources/haveno/core/app/cli-output_windows.txt ================================================ Haveno Test version 0.1.0 Usage: haveno-test [options] Options: --name= (default: Haveno) The name of the Haveno node --another-option= (default: WAT) This is a long description which will need to break over multiple linessssssssssss such that no line is longer than 80 characters in the help output. --exactly-72-char-description= 012345678911234567892123456789312345678941234567895123456789612345678971 --exactly-72-char-description-with-spaces= 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1 --90-char-description-without-spaces= -123456789-223456789-323456789-423456789-523456789-623456789-723456789-823456789-923456789 --90-char-description-with-space-at-char-80= -123456789-223456789-323456789-423456789-523456789-623456789-723456789-823456789 923456789 --90-char-description-with-spaces-at-chars-5-and-80= -123 56789-223456789-323456789-423456789-523456789-623456789-723456789-823456789 923456789 --90-char-description-with-space-at-char-73= -123456789-223456789-323456789-423456789-523456789-623456789-723456789-8 3456789-923456789 --1-char-description-with-only-a-space= --empty-description= --no-description= --no-arg Some description --optional-arg= Option description --with-default-value= (default: Wat) Some option with a default value --data-dir= (default: \Users\cbeams\Library\Application Support\Haveno) Application data directory --enum-opt= (default: foo) Some option that accepts an enum value as an argument ================================================ FILE: core/src/test/resources/haveno/core/dao/node/full/rpc/getblock-result-verbosity-0.txt ================================================ 00000020a33d8cf2a1567a148dad1a4099599bafa631135262413a4bdd1182be5673471abe69039afc7c93936e3e2860da8cab522281fe4139a453d1 1a3d5cfe75e93761bf25b25fffff7f20010000000b020000000001010000000000000000000000000000000000000000000000000000000000000000 ffffffff05028b000101ffffffff02495b062a0100000017a914f2d479a78b981e4a2b05be5f89ef7c468ac48e78870000000000000000266a24aa21 a9ed9f5a62816cebb2044be9db74a26762020defe9e8b251b73af2abefad7d4b355c0120000000000000000000000000000000000000000000000000 0000000000000000000000000100000001274c7174e814eb731712b80187660f7a6ab2949a3271ef80037685cdde2d78c3020000006b483045022100 b6c2fa10587d6fed3a0eecfd098b160f69a850beca139fe03ef65bec4cba1c5b02204a833a16c22bbd32722243ea3270e672f646ee9406e8797e1109 3951e92efbd5012103dcca91c2ec7229f1b4f4c4f664c92d3303dddef8d38736f6a7f28de16f3ce416ffffffff03881300000000000017a9144c0e48 93237f85479f489b32c8ff0faf3ee2e1c987c247090000000000160014007128282856f8e8f3c75909f9f1474b55cb1f1605f8902500000000160014 9bc809698674ec7c01d35d438e9d0de1aa87b6c800000000010000000114facc8cf47a984cedf9ba84db10ad767e18c6fb6edbac39ce8f138a1e5b43 9100000000da0047304402201f00d9a4aab1a3a239f1ad95a910092c0c55423480d609eaad4599cf7ecb7f480220668b1a9cf5624b1c4ece6da3f64b c6021e509f588ae1006601acd8a9f83b357601483045022100982eca77a72a2bdba51b9231afd4521400bee1bb7830634eb26db2b0c621bc46022073 d7325916e2b5ceb1d2e510a5161fd9115105a8dafa94068864624bb10d190e014752210229713ad5c604c585128b3a5da6de20d78fc33bd3b595e999 1f4c0e1fee99f845210398ad45a74bf5a5c5a8ec31de6815d2e805a23e68c0f8001770e74bc4c17c5b3152aefeffffff01b06a21000000000017a914 4c0e4893237f85479f489b32c8ff0faf3ee2e1c9878a0000000100000000010133ba0d88494567a02bffc406b31bd5eb29f0b536a96326baca349b3a 96f241e90200000000ffffffff03881300000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987067e180000000000160014f6da24 d081a1b63dcacf4a5762a8ed91fd472c685b74b900000000001600144755561caa18d651bf59912545764811d0ab96f60247304402200d4f21475675 3861adf835f5d175835d3cd50a19edbf9881572ab7f831a030de0220181b94bbb7d7b0a7a6eee06881a3265cb28405f2d14055e1ff23ac6500471930 012102772467db1e5909e7e9f2b169daccf556e7e2981b145c756db649f3972d913c320000000001000000000101e06ec4548803dadb10ef6c66e4f3 1f319161dc9ec31631e967773b8e042836180200000000ffffffff03881300000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987 f4390900000000001600145230c895305a232ef2a4feb0a91e7d99e22fd515d20bd20000000000160014b6fbbd9053e47891fae7f3db7dd3966062b2 513c0247304402203eeb1713b582be1d74bf6a9f95c573dd41baeedf1efd1bc9a1ad1cccad97c4f70220799a399f53f9325f6cf9681b0138f80bd80f 3dc900a4d0ab5cc3c97d5be85f1801210255f56a7be9f88ccf5885ac2f8cd67320424d533d71083270a2891b2488ffb22b0000000001000000000101 4c701c32d3b0ce9408d8ec96d80934dbc6cb42df616e6e751dae82afdb46214e0200000000ffffffff03881300000000000017a9144c0e4893237f85 479f489b32c8ff0faf3ee2e1c987c02709000000000016001489c79bc0628d2d8b1cd91c2ed0e75db13e6f3f3a8a07a00600000000160014062d20a6 692350b7a39397c50857a7f725788da002483045022100ebb8e0ddab46b762e3a9555442cc7ee35c4353d9152e856c97251913902a5056022010d3a0 bb51d931a18174dc8ed0ffa37c5ff29f8e924b71d86850de31f3ea4c6e012102e0284cdeae6a8c971e2ea5004ebf9196ee9b3037d6f1ed039c4b5672 a69cddc60000000001000000000101c3f609b5166e32bd0c29168767621bfc56411a3ff9e84932fc2c612407566a370200000000ffffffff03881300 000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987006a1800000000001600141437b91493d1929b1b42a80e83229c347c28f937 eaaf90060000000016001491ad2cce99e8e4455de5f44559816b98213f3503024830450221008f8eee212f209ba2a179197bd4ba35a35ad7a3045990 25ddbd192a6e7e64c1920220242c297726948ad408ce54c9a0e0287b283c53dc68323537f24a7e3ecd8c526b012103f870bcd3a46e80e4b1236302e6 2318b412cc97ef096fc976a89deb569dc11ef1000000000100000001ba4f0ae59d7cb81f0c7edd63796387fde6825a3536953c2824d7d945c192bd20 020000006b483045022100ba97f6336b3bb3e07cf584010c7b8ab52957e34e462e71252c63f498d51f45b70220708dd78d2d9943f8c176055963ab70 6870274068fe7b1e5d87592a837336a5340121039978f14b2463d7d4790cdf2a37c2a3d872dd517ca91db7f6f7a858a7ac661c60ffffffff03881300 000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987c02709000000000016001413afc3a26c010dddaf2410a9d97b5054a4e8d309 04e05938000000001600145228ee46a95383b314396dda75e707b8bed830340000000001000000015d71dc04e56bc08b61180a2b9531a0747f56615f f27ec51cbc72ac7a800cc27a020000006a473044022073a8a8ee9cc490093e6de5708b3727cef35f41038713fab9a5c235b4b400b73102206f97f4fc 8faefb534e85c0f3773f2d67781c9871f681211276619054fc54015201210374e07f24beca2270cf305100652149a64c80d76611f775ec276658aeae 4ef0b5ffffffff03881300000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987006a1800000000001600141c397ba7ea8410dbd5 56c51f8184a14ab015114324b6a10600000000160014c38c3d890c415f2c13f6925c1ad1d4a7cb4f7dfe0000000001000000000102142a200a7460ad f754232337f48bf7c38ba411eed0cd2f98900ffd9f2adc1ed20100000000ffffffff99d67c2a69b04831bd229ea6022c91c68999c38d0ba92211b56f 15c5f8222bf50100000000ffffffff03e803000000000000220020223d978073802f79e6ecdc7591e5dc1f0ea7030d6466f73c6b90391bc72e886f00 00000000000000226a20758e9207848c631c6839b1382bb22a52b6ef0645d733389d7be2efb1e8b71454db972100000000001600145e41d2fb8de1c2 50410416d8dd153c685d3f9c7b024730440220073a37eb4371dc3d0cf218d6e9b8e6044275acd07402ccebdf24f65b60a3c1f70220647e71c173f992 fc0c5ec6c2b0b1653a95118098269e318c41d1bc33da3ff14f012103c3e858472f39d31c6defdf38b4778660501f0ccfa524b3dd8ba61117b7646635 0247304402202b8ef5de1c56328d3797265272540a054fc04c158b23ee6385b69b14486422c10220749f591fcf4ef995df8f1e7d9aa3cf0c045f1616 386b86b419197b360c871fca012102c73e60f00bc72b56568a9f371b9122b3ee29d41730670e98ff8da58e7bbfab280000000001000000000102913a 5817e0b3bddb0bc6869e12200b5115be11522cf76125229d47e184da48460100000000ffffffff684d66517d21106d1bcbba96964e31a532fec33c09 8cd621e2500c702340b6780100000000ffffffff03e803000000000000220020e3e81046fd9659b5725736efa404bc1c8f9b2ff6f0af7cb7ddacdcc6 1e1c72310000000000000000226a20c63aacf2e8be20752b6f689c0308967cbc335641f2948a4a7962fdde6c464730f2962100000000001600145e41 d2fb8de1c250410416d8dd153c685d3f9c7b02483045022100c9665b9abe7fcab10f775eeafc2391c1fee84c50b50df6d697b1db9a7eea5dd3022070 cb7aa57b8f5bf9f2eff11263cf2ea871ab7b9ddfc8e47671cee50ada547243012102016f9a6cc454bd1e74c28df36a079231a215812c60581d1e1745 e650f82bd1230248304502210094cdec8e08f32919b3f25c8672041305c848b4206256ad64f7090dc97dfd1bf002205c397a310cebc690fac04d1394 5e012d0031246d4574e92f97e3701ca729b6140121029b739486d7cf402b3db3913187fad7897e8a5ec3cd8607e9b5fc54a71958b03100000000 ================================================ FILE: core/src/test/resources/haveno/core/dao/node/full/rpc/getblock-result-verbosity-1.json ================================================ { "hash": "015f37a20d517645a11a6cdd316049f41bc77b4a4057b2dd092114b78147f42c", "confirmations": 12, "strippedsize": 2270, "size": 3178, "weight": 9988, "height": 139, "version": 536870912, "versionHex": "20000000", "merkleroot": "6137e975fe5c3d1ad153a43941fe812252ab8cda60283e6e93937cfc9a0369be", "tx": [ "09bbfd286d0399b57ac7c85956fccbe7e080cedd7a01723d8d68038f0cf57159", "e2fc769668f7306ca09865c10dd744ed5510f1cec35b8f854c9ed346229a303b", "b36a2c90dab09a0b99d30a3b132af37b79d8266a1510decc34683a2784228337", "f52b22f8c5156fb51122a90b8dc39989c6912c02a69e22bd3148b0692a7cd699", "4648da84e1479d222561f72c5211be15510b20129e86c60bdbbdb3e017583a91", "d21edc2a9ffd0f90982fcdd0ee11a48bc3f78bf437232354f7ad60740a202a14", "78b64023700c50e221d68c093cc3fe32a5314e9696bacb1b6d10217d51664d68", "dd4243c2743a2d2351814a14628e1976e6fb208e63bd2bbd441180c441205027", "aa33deb87512c2c417a0a9a17c58a36dcf2686fca8b50cc608ad952178a350c2", "719606705c6832f7180ab9db5e1ce6a51bad80a5cbf3b57b806dd10f3d7d5124", "16cd3283068d2965f26045e15a285e39a762af32ec388ae8c28fb6cb2c468768" ], "time": 1605510591, "mediantime": 1589548514, "nonce": 1, "bits": "207fffff", "difficulty": 4.656542373906925E-10, "chainwork": "0000000000000000000000000000000000000000000000000000000000000118", "nTx": 11, "previousblockhash": "1a477356be8211dd4b3a4162521331a6af9b5999401aad8d147a56a1f28c3da3", "nextblockhash": "7d8267341a57f1f626b450eb22b2dbf208f13ec176e1cc015aa4d2b2ea55016d" } ================================================ FILE: core/src/test/resources/haveno/core/dao/node/full/rpc/getblock-result-verbosity-2.json ================================================ { "hash": "015f37a20d517645a11a6cdd316049f41bc77b4a4057b2dd092114b78147f42c", "confirmations": 12, "strippedsize": 2270, "size": 3178, "weight": 9988, "height": 139, "version": 536870912, "versionHex": "20000000", "merkleroot": "6137e975fe5c3d1ad153a43941fe812252ab8cda60283e6e93937cfc9a0369be", "tx": [ { "txid": "09bbfd286d0399b57ac7c85956fccbe7e080cedd7a01723d8d68038f0cf57159", "hash": "ed1620cdd028c99d57ee0709f963976bb75cac93f5171f5a2f5dd9976e5c56bc", "version": 2, "size": 171, "vsize": 144, "weight": 576, "locktime": 0, "vin": [ { "coinbase": "028b000101", "sequence": 4294967295 } ], "vout": [ { "value": 50.00026953, "n": 0, "scriptPubKey": { "asm": "OP_HASH160 f2d479a78b981e4a2b05be5f89ef7c468ac48e78 OP_EQUAL", "hex": "a914f2d479a78b981e4a2b05be5f89ef7c468ac48e7887", "reqSigs": 1, "type": "scripthash", "addresses": [ "2NFPC3k5RjZ4GAQgLqJdiVdJ6gqWqEBryXq" ] } }, { "value": 0.0, "n": 1, "scriptPubKey": { "asm": "OP_RETURN aa21a9ed9f5a62816cebb2044be9db74a26762020defe9e8b251b73af2abefad7d4b355c", "hex": "6a24aa21a9ed9f5a62816cebb2044be9db74a26762020defe9e8b251b73af2abefad7d4b355c", "type": "nulldata" } } ], "hex": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff05028b000101ffffffff02495b062a0100000017a914f2d479a78b981e4a2b05be5f89ef7c468ac48e78870000000000000000266a24aa21a9ed9f5a62816cebb2044be9db74a26762020defe9e8b251b73af2abefad7d4b355c0120000000000000000000000000000000000000000000000000000000000000000000000000" }, { "txid": "e2fc769668f7306ca09865c10dd744ed5510f1cec35b8f854c9ed346229a303b", "hash": "e2fc769668f7306ca09865c10dd744ed5510f1cec35b8f854c9ed346229a303b", "version": 1, "size": 252, "vsize": 252, "weight": 1008, "locktime": 0, "vin": [ { "txid": "c3782ddecd85760380ef71329a94b26a7a0f668701b8121773eb14e874714c27", "vout": 2, "scriptSig": { "asm": "3045022100b6c2fa10587d6fed3a0eecfd098b160f69a850beca139fe03ef65bec4cba1c5b02204a833a16c22bbd32722243ea3270e672f646ee9406e8797e11093951e92efbd5[ALL] 03dcca91c2ec7229f1b4f4c4f664c92d3303dddef8d38736f6a7f28de16f3ce416", "hex": "483045022100b6c2fa10587d6fed3a0eecfd098b160f69a850beca139fe03ef65bec4cba1c5b02204a833a16c22bbd32722243ea3270e672f646ee9406e8797e11093951e92efbd5012103dcca91c2ec7229f1b4f4c4f664c92d3303dddef8d38736f6a7f28de16f3ce416" }, "sequence": 4294967295 } ], "vout": [ { "value": 5.0E-5, "n": 0, "scriptPubKey": { "asm": "OP_HASH160 4c0e4893237f85479f489b32c8ff0faf3ee2e1c9 OP_EQUAL", "hex": "a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987", "reqSigs": 1, "type": "scripthash", "addresses": [ "2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w" ] } }, { "value": 0.00608194, "n": 1, "scriptPubKey": { "asm": "0 007128282856f8e8f3c75909f9f1474b55cb1f16", "hex": "0014007128282856f8e8f3c75909f9f1474b55cb1f16", "reqSigs": 1, "type": "witness_v0_keyhash", "addresses": [ "bcrt1qqpcjs2pg2muw3u78tyylnu28fd2uk8ckr30ezc" ] } }, { "value": 6.30257669, "n": 2, "scriptPubKey": { "asm": "0 9bc809698674ec7c01d35d438e9d0de1aa87b6c8", "hex": "00149bc809698674ec7c01d35d438e9d0de1aa87b6c8", "reqSigs": 1, "type": "witness_v0_keyhash", "addresses": [ "bcrt1qn0yqj6vxwnk8cqwnt4pca8gdux4g0dkghc9rur" ] } } ], "hex": "0100000001274c7174e814eb731712b80187660f7a6ab2949a3271ef80037685cdde2d78c3020000006b483045022100b6c2fa10587d6fed3a0eecfd098b160f69a850beca139fe03ef65bec4cba1c5b02204a833a16c22bbd32722243ea3270e672f646ee9406e8797e11093951e92efbd5012103dcca91c2ec7229f1b4f4c4f664c92d3303dddef8d38736f6a7f28de16f3ce416ffffffff03881300000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987c247090000000000160014007128282856f8e8f3c75909f9f1474b55cb1f1605f89025000000001600149bc809698674ec7c01d35d438e9d0de1aa87b6c800000000" }, { "txid": "b36a2c90dab09a0b99d30a3b132af37b79d8266a1510decc34683a2784228337", "hash": "b36a2c90dab09a0b99d30a3b132af37b79d8266a1510decc34683a2784228337", "version": 1, "size": 301, "vsize": 301, "weight": 1204, "locktime": 138, "vin": [ { "txid": "91435b1e8a138fce39acdb6efbc6187e76ad10db84baf9ed4c987af48cccfa14", "vout": 0, "scriptSig": { "asm": "0 304402201f00d9a4aab1a3a239f1ad95a910092c0c55423480d609eaad4599cf7ecb7f480220668b1a9cf5624b1c4ece6da3f64bc6021e509f588ae1006601acd8a9f83b3576[ALL] 3045022100982eca77a72a2bdba51b9231afd4521400bee1bb7830634eb26db2b0c621bc46022073d7325916e2b5ceb1d2e510a5161fd9115105a8dafa94068864624bb10d190e[ALL] 52210229713ad5c604c585128b3a5da6de20d78fc33bd3b595e9991f4c0e1fee99f845210398ad45a74bf5a5c5a8ec31de6815d2e805a23e68c0f8001770e74bc4c17c5b3152ae", "hex": "0047304402201f00d9a4aab1a3a239f1ad95a910092c0c55423480d609eaad4599cf7ecb7f480220668b1a9cf5624b1c4ece6da3f64bc6021e509f588ae1006601acd8a9f83b357601483045022100982eca77a72a2bdba51b9231afd4521400bee1bb7830634eb26db2b0c621bc46022073d7325916e2b5ceb1d2e510a5161fd9115105a8dafa94068864624bb10d190e014752210229713ad5c604c585128b3a5da6de20d78fc33bd3b595e9991f4c0e1fee99f845210398ad45a74bf5a5c5a8ec31de6815d2e805a23e68c0f8001770e74bc4c17c5b3152ae" }, "sequence": 4294967294 } ], "vout": [ { "value": 0.0219, "n": 0, "scriptPubKey": { "asm": "OP_HASH160 4c0e4893237f85479f489b32c8ff0faf3ee2e1c9 OP_EQUAL", "hex": "a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987", "reqSigs": 1, "type": "scripthash", "addresses": [ "2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w" ] } } ], "hex": "010000000114facc8cf47a984cedf9ba84db10ad767e18c6fb6edbac39ce8f138a1e5b439100000000da0047304402201f00d9a4aab1a3a239f1ad95a910092c0c55423480d609eaad4599cf7ecb7f480220668b1a9cf5624b1c4ece6da3f64bc6021e509f588ae1006601acd8a9f83b357601483045022100982eca77a72a2bdba51b9231afd4521400bee1bb7830634eb26db2b0c621bc46022073d7325916e2b5ceb1d2e510a5161fd9115105a8dafa94068864624bb10d190e014752210229713ad5c604c585128b3a5da6de20d78fc33bd3b595e9991f4c0e1fee99f845210398ad45a74bf5a5c5a8ec31de6815d2e805a23e68c0f8001770e74bc4c17c5b3152aefeffffff01b06a21000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c9878a000000" }, { "txid": "f52b22f8c5156fb51122a90b8dc39989c6912c02a69e22bd3148b0692a7cd699", "hash": "a1cb920222f6470dae2b90af42ec4da2e541bc4de9cec11441ca2dd0032bca2c", "version": 1, "size": 254, "vsize": 173, "weight": 689, "locktime": 0, "vin": [ { "txid": "e941f2963a9b34caba2663a936b5f029ebd51bb306c4ff2ba0674549880dba33", "vout": 2, "scriptSig": { "asm": "", "hex": "" }, "txinwitness": [ "304402200d4f214756753861adf835f5d175835d3cd50a19edbf9881572ab7f831a030de0220181b94bbb7d7b0a7a6eee06881a3265cb28405f2d14055e1ff23ac650047193001", "02772467db1e5909e7e9f2b169daccf556e7e2981b145c756db649f3972d913c32" ], "sequence": 4294967295 } ], "vout": [ { "value": 5.0E-5, "n": 0, "scriptPubKey": { "asm": "OP_HASH160 4c0e4893237f85479f489b32c8ff0faf3ee2e1c9 OP_EQUAL", "hex": "a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987", "reqSigs": 1, "type": "scripthash", "addresses": [ "2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w" ] } }, { "value": 0.01605126, "n": 1, "scriptPubKey": { "asm": "0 f6da24d081a1b63dcacf4a5762a8ed91fd472c68", "hex": "0014f6da24d081a1b63dcacf4a5762a8ed91fd472c68", "reqSigs": 1, "type": "witness_v0_keyhash", "addresses": [ "bcrt1q7mdzf5yp5xmrmjk0fftk928dj875wtrgkwqr5x" ] } }, { "value": 0.12153947, "n": 2, "scriptPubKey": { "asm": "0 4755561caa18d651bf59912545764811d0ab96f6", "hex": "00144755561caa18d651bf59912545764811d0ab96f6", "reqSigs": 1, "type": "witness_v0_keyhash", "addresses": [ "bcrt1qga24v892rrt9r06ejyj52ajgz8g2h9hkwquzg4" ] } } ], "hex": "0100000000010133ba0d88494567a02bffc406b31bd5eb29f0b536a96326baca349b3a96f241e90200000000ffffffff03881300000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987067e180000000000160014f6da24d081a1b63dcacf4a5762a8ed91fd472c685b74b900000000001600144755561caa18d651bf59912545764811d0ab96f60247304402200d4f214756753861adf835f5d175835d3cd50a19edbf9881572ab7f831a030de0220181b94bbb7d7b0a7a6eee06881a3265cb28405f2d14055e1ff23ac6500471930012102772467db1e5909e7e9f2b169daccf556e7e2981b145c756db649f3972d913c3200000000" }, { "txid": "4648da84e1479d222561f72c5211be15510b20129e86c60bdbbdb3e017583a91", "hash": "16e1ae45d2c6ddaaefeed97a822a95a2933096bf377db147a63d9b730cc30d20", "version": 1, "size": 254, "vsize": 173, "weight": 689, "locktime": 0, "vin": [ { "txid": "183628048e3b7767e93116c39edc6191311ff3e4666cef10dbda038854c46ee0", "vout": 2, "scriptSig": { "asm": "", "hex": "" }, "txinwitness": [ "304402203eeb1713b582be1d74bf6a9f95c573dd41baeedf1efd1bc9a1ad1cccad97c4f70220799a399f53f9325f6cf9681b0138f80bd80f3dc900a4d0ab5cc3c97d5be85f1801", "0255f56a7be9f88ccf5885ac2f8cd67320424d533d71083270a2891b2488ffb22b" ], "sequence": 4294967295 } ], "vout": [ { "value": 5.0E-5, "n": 0, "scriptPubKey": { "asm": "OP_HASH160 4c0e4893237f85479f489b32c8ff0faf3ee2e1c9 OP_EQUAL", "hex": "a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987", "reqSigs": 1, "type": "scripthash", "addresses": [ "2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w" ] } }, { "value": 0.0060466, "n": 1, "scriptPubKey": { "asm": "0 5230c895305a232ef2a4feb0a91e7d99e22fd515", "hex": "00145230c895305a232ef2a4feb0a91e7d99e22fd515", "reqSigs": 1, "type": "witness_v0_keyhash", "addresses": [ "bcrt1q2gcv39fstg3jau4yl6c2j8nan83zl4g4w5ch6j" ] } }, { "value": 0.13765586, "n": 2, "scriptPubKey": { "asm": "0 b6fbbd9053e47891fae7f3db7dd3966062b2513c", "hex": "0014b6fbbd9053e47891fae7f3db7dd3966062b2513c", "reqSigs": 1, "type": "witness_v0_keyhash", "addresses": [ "bcrt1qkmammyznu3ufr7h870dhm5ukvp3ty5fudkl63e" ] } } ], "hex": "01000000000101e06ec4548803dadb10ef6c66e4f31f319161dc9ec31631e967773b8e042836180200000000ffffffff03881300000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987f4390900000000001600145230c895305a232ef2a4feb0a91e7d99e22fd515d20bd20000000000160014b6fbbd9053e47891fae7f3db7dd3966062b2513c0247304402203eeb1713b582be1d74bf6a9f95c573dd41baeedf1efd1bc9a1ad1cccad97c4f70220799a399f53f9325f6cf9681b0138f80bd80f3dc900a4d0ab5cc3c97d5be85f1801210255f56a7be9f88ccf5885ac2f8cd67320424d533d71083270a2891b2488ffb22b00000000" }, { "txid": "d21edc2a9ffd0f90982fcdd0ee11a48bc3f78bf437232354f7ad60740a202a14", "hash": "bae1180680abb989f5b89b5f49729d3eedcf757a12fd0b7649c944a4ba37eaee", "version": 1, "size": 255, "vsize": 173, "weight": 690, "locktime": 0, "vin": [ { "txid": "4e2146dbaf82ae1d756e6e61df42cbc6db3409d896ecd80894ceb0d3321c704c", "vout": 2, "scriptSig": { "asm": "", "hex": "" }, "txinwitness": [ "3045022100ebb8e0ddab46b762e3a9555442cc7ee35c4353d9152e856c97251913902a5056022010d3a0bb51d931a18174dc8ed0ffa37c5ff29f8e924b71d86850de31f3ea4c6e01", "02e0284cdeae6a8c971e2ea5004ebf9196ee9b3037d6f1ed039c4b5672a69cddc6" ], "sequence": 4294967295 } ], "vout": [ { "value": 5.0E-5, "n": 0, "scriptPubKey": { "asm": "OP_HASH160 4c0e4893237f85479f489b32c8ff0faf3ee2e1c9 OP_EQUAL", "hex": "a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987", "reqSigs": 1, "type": "scripthash", "addresses": [ "2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w" ] } }, { "value": 0.006, "n": 1, "scriptPubKey": { "asm": "0 89c79bc0628d2d8b1cd91c2ed0e75db13e6f3f3a", "hex": "001489c79bc0628d2d8b1cd91c2ed0e75db13e6f3f3a", "reqSigs": 1, "type": "witness_v0_keyhash", "addresses": [ "bcrt1q38rehsrz35kck8xershdpe6akylx70e63azh0p" ] } }, { "value": 1.11150986, "n": 2, "scriptPubKey": { "asm": "0 062d20a6692350b7a39397c50857a7f725788da0", "hex": "0014062d20a6692350b7a39397c50857a7f725788da0", "reqSigs": 1, "type": "witness_v0_keyhash", "addresses": [ "bcrt1qqckjpfnfydgt0gunjlzss4a87ujh3rdq7u4yrr" ] } } ], "hex": "010000000001014c701c32d3b0ce9408d8ec96d80934dbc6cb42df616e6e751dae82afdb46214e0200000000ffffffff03881300000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987c02709000000000016001489c79bc0628d2d8b1cd91c2ed0e75db13e6f3f3a8a07a00600000000160014062d20a6692350b7a39397c50857a7f725788da002483045022100ebb8e0ddab46b762e3a9555442cc7ee35c4353d9152e856c97251913902a5056022010d3a0bb51d931a18174dc8ed0ffa37c5ff29f8e924b71d86850de31f3ea4c6e012102e0284cdeae6a8c971e2ea5004ebf9196ee9b3037d6f1ed039c4b5672a69cddc600000000" }, { "txid": "78b64023700c50e221d68c093cc3fe32a5314e9696bacb1b6d10217d51664d68", "hash": "f072bb4fec0ae128ed678d24475cf719c0dd3ba0b44bf14f7300df60408ee365", "version": 1, "size": 255, "vsize": 173, "weight": 690, "locktime": 0, "vin": [ { "txid": "376a560724612cfc3249e8f93f1a4156fc1b62678716290cbd326e16b509f6c3", "vout": 2, "scriptSig": { "asm": "", "hex": "" }, "txinwitness": [ "30450221008f8eee212f209ba2a179197bd4ba35a35ad7a304599025ddbd192a6e7e64c1920220242c297726948ad408ce54c9a0e0287b283c53dc68323537f24a7e3ecd8c526b01", "03f870bcd3a46e80e4b1236302e62318b412cc97ef096fc976a89deb569dc11ef1" ], "sequence": 4294967295 } ], "vout": [ { "value": 5.0E-5, "n": 0, "scriptPubKey": { "asm": "OP_HASH160 4c0e4893237f85479f489b32c8ff0faf3ee2e1c9 OP_EQUAL", "hex": "a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987", "reqSigs": 1, "type": "scripthash", "addresses": [ "2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w" ] } }, { "value": 0.016, "n": 1, "scriptPubKey": { "asm": "0 1437b91493d1929b1b42a80e83229c347c28f937", "hex": "00141437b91493d1929b1b42a80e83229c347c28f937", "reqSigs": 1, "type": "witness_v0_keyhash", "addresses": [ "bcrt1qzsmmj9yn6xffkx6z4q8gxg5ux37z37fhr598r9" ] } }, { "value": 1.10145514, "n": 2, "scriptPubKey": { "asm": "0 91ad2cce99e8e4455de5f44559816b98213f3503", "hex": "001491ad2cce99e8e4455de5f44559816b98213f3503", "reqSigs": 1, "type": "witness_v0_keyhash", "addresses": [ "bcrt1qjxkjen5earjy2h0973z4nqttnqsn7dgrnsgy27" ] } } ], "hex": "01000000000101c3f609b5166e32bd0c29168767621bfc56411a3ff9e84932fc2c612407566a370200000000ffffffff03881300000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987006a1800000000001600141437b91493d1929b1b42a80e83229c347c28f937eaaf90060000000016001491ad2cce99e8e4455de5f44559816b98213f3503024830450221008f8eee212f209ba2a179197bd4ba35a35ad7a304599025ddbd192a6e7e64c1920220242c297726948ad408ce54c9a0e0287b283c53dc68323537f24a7e3ecd8c526b012103f870bcd3a46e80e4b1236302e62318b412cc97ef096fc976a89deb569dc11ef100000000" }, { "txid": "dd4243c2743a2d2351814a14628e1976e6fb208e63bd2bbd441180c441205027", "hash": "dd4243c2743a2d2351814a14628e1976e6fb208e63bd2bbd441180c441205027", "version": 1, "size": 252, "vsize": 252, "weight": 1008, "locktime": 0, "vin": [ { "txid": "20bd92c145d9d724283c9536355a82e6fd87637963dd7e0c1fb87c9de50a4fba", "vout": 2, "scriptSig": { "asm": "3045022100ba97f6336b3bb3e07cf584010c7b8ab52957e34e462e71252c63f498d51f45b70220708dd78d2d9943f8c176055963ab706870274068fe7b1e5d87592a837336a534[ALL] 039978f14b2463d7d4790cdf2a37c2a3d872dd517ca91db7f6f7a858a7ac661c60", "hex": "483045022100ba97f6336b3bb3e07cf584010c7b8ab52957e34e462e71252c63f498d51f45b70220708dd78d2d9943f8c176055963ab706870274068fe7b1e5d87592a837336a5340121039978f14b2463d7d4790cdf2a37c2a3d872dd517ca91db7f6f7a858a7ac661c60" }, "sequence": 4294967295 } ], "vout": [ { "value": 5.0E-5, "n": 0, "scriptPubKey": { "asm": "OP_HASH160 4c0e4893237f85479f489b32c8ff0faf3ee2e1c9 OP_EQUAL", "hex": "a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987", "reqSigs": 1, "type": "scripthash", "addresses": [ "2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w" ] } }, { "value": 0.006, "n": 1, "scriptPubKey": { "asm": "0 13afc3a26c010dddaf2410a9d97b5054a4e8d309", "hex": "001413afc3a26c010dddaf2410a9d97b5054a4e8d309", "reqSigs": 1, "type": "witness_v0_keyhash", "addresses": [ "bcrt1qzwhu8gnvqyxamteyzz5aj76s2jjw35cfctjvy8" ] } }, { "value": 9.45414148, "n": 2, "scriptPubKey": { "asm": "0 5228ee46a95383b314396dda75e707b8bed83034", "hex": "00145228ee46a95383b314396dda75e707b8bed83034", "reqSigs": 1, "type": "witness_v0_keyhash", "addresses": [ "bcrt1q2g5wu34f2wpmx9pedhd8tec8hzldsvp56hljn8" ] } } ], "hex": "0100000001ba4f0ae59d7cb81f0c7edd63796387fde6825a3536953c2824d7d945c192bd20020000006b483045022100ba97f6336b3bb3e07cf584010c7b8ab52957e34e462e71252c63f498d51f45b70220708dd78d2d9943f8c176055963ab706870274068fe7b1e5d87592a837336a5340121039978f14b2463d7d4790cdf2a37c2a3d872dd517ca91db7f6f7a858a7ac661c60ffffffff03881300000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987c02709000000000016001413afc3a26c010dddaf2410a9d97b5054a4e8d30904e05938000000001600145228ee46a95383b314396dda75e707b8bed8303400000000" }, { "txid": "aa33deb87512c2c417a0a9a17c58a36dcf2686fca8b50cc608ad952178a350c2", "hash": "aa33deb87512c2c417a0a9a17c58a36dcf2686fca8b50cc608ad952178a350c2", "version": 1, "size": 251, "vsize": 251, "weight": 1004, "locktime": 0, "vin": [ { "txid": "7ac20c807aac72bc1cc57ef25f61567f74a031952b0a18618bc06be504dc715d", "vout": 2, "scriptSig": { "asm": "3044022073a8a8ee9cc490093e6de5708b3727cef35f41038713fab9a5c235b4b400b73102206f97f4fc8faefb534e85c0f3773f2d67781c9871f681211276619054fc540152[ALL] 0374e07f24beca2270cf305100652149a64c80d76611f775ec276658aeae4ef0b5", "hex": "473044022073a8a8ee9cc490093e6de5708b3727cef35f41038713fab9a5c235b4b400b73102206f97f4fc8faefb534e85c0f3773f2d67781c9871f681211276619054fc54015201210374e07f24beca2270cf305100652149a64c80d76611f775ec276658aeae4ef0b5" }, "sequence": 4294967295 } ], "vout": [ { "value": 5.0E-5, "n": 0, "scriptPubKey": { "asm": "OP_HASH160 4c0e4893237f85479f489b32c8ff0faf3ee2e1c9 OP_EQUAL", "hex": "a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987", "reqSigs": 1, "type": "scripthash", "addresses": [ "2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w" ] } }, { "value": 0.016, "n": 1, "scriptPubKey": { "asm": "0 1c397ba7ea8410dbd556c51f8184a14ab0151143", "hex": "00141c397ba7ea8410dbd556c51f8184a14ab0151143", "reqSigs": 1, "type": "witness_v0_keyhash", "addresses": [ "bcrt1qrsuhhfl2ssgdh42kc50crp9pf2cp2y2ruplr8l" ] } }, { "value": 1.1126122, "n": 2, "scriptPubKey": { "asm": "0 c38c3d890c415f2c13f6925c1ad1d4a7cb4f7dfe", "hex": "0014c38c3d890c415f2c13f6925c1ad1d4a7cb4f7dfe", "reqSigs": 1, "type": "witness_v0_keyhash", "addresses": [ "bcrt1qcwxrmzgvg90jcylkjfwp45w55l957l07mtsydu" ] } } ], "hex": "01000000015d71dc04e56bc08b61180a2b9531a0747f56615ff27ec51cbc72ac7a800cc27a020000006a473044022073a8a8ee9cc490093e6de5708b3727cef35f41038713fab9a5c235b4b400b73102206f97f4fc8faefb534e85c0f3773f2d67781c9871f681211276619054fc54015201210374e07f24beca2270cf305100652149a64c80d76611f775ec276658aeae4ef0b5ffffffff03881300000000000017a9144c0e4893237f85479f489b32c8ff0faf3ee2e1c987006a1800000000001600141c397ba7ea8410dbd556c51f8184a14ab015114324b6a10600000000160014c38c3d890c415f2c13f6925c1ad1d4a7cb4f7dfe00000000" }, { "txid": "719606705c6832f7180ab9db5e1ce6a51bad80a5cbf3b57b806dd10f3d7d5124", "hash": "c670f473daee2ad2fabc2b8cc9e99d499c19ef9f2d3331345674a0d3b1b96639", "version": 1, "size": 425, "vsize": 263, "weight": 1052, "locktime": 0, "vin": [ { "txid": "d21edc2a9ffd0f90982fcdd0ee11a48bc3f78bf437232354f7ad60740a202a14", "vout": 1, "scriptSig": { "asm": "", "hex": "" }, "txinwitness": [ "30440220073a37eb4371dc3d0cf218d6e9b8e6044275acd07402ccebdf24f65b60a3c1f70220647e71c173f992fc0c5ec6c2b0b1653a95118098269e318c41d1bc33da3ff14f01", "03c3e858472f39d31c6defdf38b4778660501f0ccfa524b3dd8ba61117b7646635" ], "sequence": 4294967295 }, { "txid": "f52b22f8c5156fb51122a90b8dc39989c6912c02a69e22bd3148b0692a7cd699", "vout": 1, "scriptSig": { "asm": "", "hex": "" }, "txinwitness": [ "304402202b8ef5de1c56328d3797265272540a054fc04c158b23ee6385b69b14486422c10220749f591fcf4ef995df8f1e7d9aa3cf0c045f1616386b86b419197b360c871fca01", "02c73e60f00bc72b56568a9f371b9122b3ee29d41730670e98ff8da58e7bbfab28" ], "sequence": 4294967295 } ], "vout": [ { "value": 1.0E-5, "n": 0, "scriptPubKey": { "asm": "0 223d978073802f79e6ecdc7591e5dc1f0ea7030d6466f73c6b90391bc72e886f", "hex": "0020223d978073802f79e6ecdc7591e5dc1f0ea7030d6466f73c6b90391bc72e886f", "reqSigs": 1, "type": "witness_v0_scripthash", "addresses": [ "bcrt1qyg7e0qrnsqhhnehvm36erewuru82wqcdv3n0w0rtjqu3h3ew3phs27rfy6" ] } }, { "value": 0.0, "n": 1, "scriptPubKey": { "asm": "OP_RETURN 758e9207848c631c6839b1382bb22a52b6ef0645d733389d7be2efb1e8b71454", "hex": "6a20758e9207848c631c6839b1382bb22a52b6ef0645d733389d7be2efb1e8b71454", "type": "nulldata" } }, { "value": 0.02201563, "n": 2, "scriptPubKey": { "asm": "0 5e41d2fb8de1c250410416d8dd153c685d3f9c7b", "hex": "00145e41d2fb8de1c250410416d8dd153c685d3f9c7b", "reqSigs": 1, "type": "witness_v0_keyhash", "addresses": [ "bcrt1qteqa97udu8p9qsgyzmvd69fudpwnl8rmt8putj" ] } } ], "hex": "01000000000102142a200a7460adf754232337f48bf7c38ba411eed0cd2f98900ffd9f2adc1ed20100000000ffffffff99d67c2a69b04831bd229ea6022c91c68999c38d0ba92211b56f15c5f8222bf50100000000ffffffff03e803000000000000220020223d978073802f79e6ecdc7591e5dc1f0ea7030d6466f73c6b90391bc72e886f0000000000000000226a20758e9207848c631c6839b1382bb22a52b6ef0645d733389d7be2efb1e8b71454db972100000000001600145e41d2fb8de1c250410416d8dd153c685d3f9c7b024730440220073a37eb4371dc3d0cf218d6e9b8e6044275acd07402ccebdf24f65b60a3c1f70220647e71c173f992fc0c5ec6c2b0b1653a95118098269e318c41d1bc33da3ff14f012103c3e858472f39d31c6defdf38b4778660501f0ccfa524b3dd8ba61117b76466350247304402202b8ef5de1c56328d3797265272540a054fc04c158b23ee6385b69b14486422c10220749f591fcf4ef995df8f1e7d9aa3cf0c045f1616386b86b419197b360c871fca012102c73e60f00bc72b56568a9f371b9122b3ee29d41730670e98ff8da58e7bbfab2800000000" }, { "txid": "16cd3283068d2965f26045e15a285e39a762af32ec388ae8c28fb6cb2c468768", "hash": "a854736ce90b603599bf21be28bea909c3ffafaed57d5eacea1f1730b68db0d8", "version": 1, "size": 427, "vsize": 264, "weight": 1054, "locktime": 0, "vin": [ { "txid": "4648da84e1479d222561f72c5211be15510b20129e86c60bdbbdb3e017583a91", "vout": 1, "scriptSig": { "asm": "", "hex": "" }, "txinwitness": [ "3045022100c9665b9abe7fcab10f775eeafc2391c1fee84c50b50df6d697b1db9a7eea5dd3022070cb7aa57b8f5bf9f2eff11263cf2ea871ab7b9ddfc8e47671cee50ada54724301", "02016f9a6cc454bd1e74c28df36a079231a215812c60581d1e1745e650f82bd123" ], "sequence": 4294967295 }, { "txid": "78b64023700c50e221d68c093cc3fe32a5314e9696bacb1b6d10217d51664d68", "vout": 1, "scriptSig": { "asm": "", "hex": "" }, "txinwitness": [ "304502210094cdec8e08f32919b3f25c8672041305c848b4206256ad64f7090dc97dfd1bf002205c397a310cebc690fac04d13945e012d0031246d4574e92f97e3701ca729b61401", "029b739486d7cf402b3db3913187fad7897e8a5ec3cd8607e9b5fc54a71958b031" ], "sequence": 4294967295 } ], "vout": [ { "value": 1.0E-5, "n": 0, "scriptPubKey": { "asm": "0 e3e81046fd9659b5725736efa404bc1c8f9b2ff6f0af7cb7ddacdcc61e1c7231", "hex": "0020e3e81046fd9659b5725736efa404bc1c8f9b2ff6f0af7cb7ddacdcc61e1c7231", "reqSigs": 1, "type": "witness_v0_scripthash", "addresses": [ "bcrt1qu05pq3hajevm2ujhxmh6gp9urj8ektlk7zhhed7a4nwvv8suwgcstjh7k7" ] } }, { "value": 0.0, "n": 1, "scriptPubKey": { "asm": "OP_RETURN c63aacf2e8be20752b6f689c0308967cbc335641f2948a4a7962fdde6c464730", "hex": "6a20c63aacf2e8be20752b6f689c0308967cbc335641f2948a4a7962fdde6c464730", "type": "nulldata" } }, { "value": 0.0220133, "n": 2, "scriptPubKey": { "asm": "0 5e41d2fb8de1c250410416d8dd153c685d3f9c7b", "hex": "00145e41d2fb8de1c250410416d8dd153c685d3f9c7b", "reqSigs": 1, "type": "witness_v0_keyhash", "addresses": [ "bcrt1qteqa97udu8p9qsgyzmvd69fudpwnl8rmt8putj" ] } } ], "hex": "01000000000102913a5817e0b3bddb0bc6869e12200b5115be11522cf76125229d47e184da48460100000000ffffffff684d66517d21106d1bcbba96964e31a532fec33c098cd621e2500c702340b6780100000000ffffffff03e803000000000000220020e3e81046fd9659b5725736efa404bc1c8f9b2ff6f0af7cb7ddacdcc61e1c72310000000000000000226a20c63aacf2e8be20752b6f689c0308967cbc335641f2948a4a7962fdde6c464730f2962100000000001600145e41d2fb8de1c250410416d8dd153c685d3f9c7b02483045022100c9665b9abe7fcab10f775eeafc2391c1fee84c50b50df6d697b1db9a7eea5dd3022070cb7aa57b8f5bf9f2eff11263cf2ea871ab7b9ddfc8e47671cee50ada547243012102016f9a6cc454bd1e74c28df36a079231a215812c60581d1e1745e650f82bd1230248304502210094cdec8e08f32919b3f25c8672041305c848b4206256ad64f7090dc97dfd1bf002205c397a310cebc690fac04d13945e012d0031246d4574e92f97e3701ca729b6140121029b739486d7cf402b3db3913187fad7897e8a5ec3cd8607e9b5fc54a71958b03100000000" } ], "time": 1605510591, "mediantime": 1589548514, "nonce": 1, "bits": "207fffff", "difficulty": 4.656542373906925E-10, "chainwork": "0000000000000000000000000000000000000000000000000000000000000118", "nTx": 11, "previousblockhash": "1a477356be8211dd4b3a4162521331a6af9b5999401aad8d147a56a1f28c3da3", "nextblockhash": "7d8267341a57f1f626b450eb22b2dbf208f13ec176e1cc015aa4d2b2ea55016d" } ================================================ FILE: core/src/test/resources/haveno/core/dao/node/full/rpc/getnetworkinfo-result.json ================================================ { "version": 210000, "subversion": "/Satoshi:0.21.0/", "protocolversion": 70016, "localservices": "000000000100040d", "localservicesnames": [ "NETWORK", "BLOOM", "WITNESS", "NETWORK_LIMITED", "MY_CUSTOM_SERVICE" ], "localrelay": true, "timeoffset": -2, "networkactive": true, "connections": 9, "connections_in": 0, "connections_out": 9, "networks": [ { "name": "ipv4", "limited": false, "reachable": true, "proxy": "", "proxy_randomize_credentials": false }, { "name": "ipv6", "limited": false, "reachable": true, "proxy": "", "proxy_randomize_credentials": false }, { "name": "onion", "limited": true, "reachable": false, "proxy": "", "proxy_randomize_credentials": false } ], "relayfee": 1.0E-5, "incrementalfee": 1.0E-5, "localaddresses": [ { "address": "2001:0:2851:782c:2c65:3676:fde6:18f8", "port": 8333, "score": 26 } ], "warnings": "" } ================================================ FILE: core/src/test/resources/haveno/core/provider/mempool/badOfferTestData.json ================================================ { "37fba8bf119c289481eef031c0a35e126376f71d13d7cce35eb0d5e05799b5da": "hUWPf,37fba8bf119c289481eef031c0a35e126376f71d13d7cce35eb0d5e05799b5da,19910000,200,0,668994, tx_missing_from_blockchain_for_4_days", "b3bc726aa2aa6533cb1e61901ce351eecde234378fe650aee267388886aa6e4b": "ebdttmzh,b3bc726aa2aa6533cb1e61901ce351eecde234378fe650aee267388886aa6e4b,4000000,5000,1,669137, tx_missing_from_blockchain_for_2_days", "10f32fe53081466f003185a9ef0324d6cbe3f59334ee9ccb2f7155cbfad9c1de": "kmbyoexc,10f32fe53081466f003185a9ef0324d6cbe3f59334ee9ccb2f7155cbfad9c1de,33000000,332,0,668954, tx_not_found", "cd99836ac4246c3e3980edf95773060481ce52271b74dadeb41e18c42ed21188": "nlaIlAvE,cd99836ac4246c3e3980edf95773060481ce52271b74dadeb41e18c42ed21188,5000000,546,1,669262, invalid_missing_fee_address", "fc3cb16293895fea8ea5d2d8ab4e39d1b27f583e2c160468b586789a861efa74": "feescammer,fc3cb16293895fea8ea5d2d8ab4e39d1b27f583e2c160468b586789a861efa74,1000000,546,1,669442, invalid_missing_fee_address", "72cabb5c323c923b43c7f6551974f591dcee148778ee34f9131011ea0ca82813": "PBFICEAS,72cabb5c323c923b43c7f6551974f591dcee148778ee34f9131011ea0ca82813,2000000,546,1,672969, dust_fee_scammer", "1c8e4934f93b5bbd2823318d5d491698316216f2e4bc0d7cd353f6b16358d80e": "feescammer,1c8e4934f93b5bbd2823318d5d491698316216f2e4bc0d7cd353f6b16358d80e,2000000,546,1,669227, dust_fee_scammer", "17cbd95d8809dc8808a5c209208f59c4a80e09e012e62951668d30d716c44a96": "feescammer,17cbd95d8809dc8808a5c209208f59c4a80e09e012e62951668d30d716c44a96,2000000,546,1,669340, dust_fee_scammer" } ================================================ FILE: core/src/test/resources/haveno/core/provider/mempool/offerTestData.json ================================================ { "e125fdbd09ee86c01e16e1f12a31507cfb8703ed1bd5a221461adf33cb3e00d9": "7213472,e125fdbd09ee86c01e16e1f12a31507cfb8703ed1bd5a221461adf33cb3e00d9,200000000,200000,1,578672, unknown_fee_receiver_1PUXU1MQ", "44b00de808d0145f9a948fe1b020c5d4173402ba0b5a5ba69124c67e371bca18": "aAPLmh98,44b00de808d0145f9a948fe1b020c5d4173402ba0b5a5ba69124c67e371bca18,140000000,140000,1,578629, unknown_fee_receiver_1PUXU1MQ", "654a7a34321b57be6a553052d1d9e0f1764dd2fab7b64c9422e9953e4d9d127d": "pwdbdku,654a7a34321b57be6a553052d1d9e0f1764dd2fab7b64c9422e9953e4d9d127d,24980000,238000,1,554947, unknown_fee_receiver_18GzH11", "0636bafb14890edfb95465e66e2b1e15915f7fb595f9b653b9129c15ef4c1c4b": "msimscqb,0636bafb14890edfb95465e66e2b1e15915f7fb595f9b653b9129c15ef4c1c4b,1000000,10,0,662390", "2861f4526f40686d5cddc364035b561c13625996233a8b8705195041504ba3a1": "89284,2861f4526f40686d5cddc364035b561c13625996233a8b8705195041504ba3a1,900000,9,0,666473", "a571e8a2e9227025f897017db0a7cbd7baea98d7f119aea49c46d6535d79caba": "EHGVHSL,a571e8a2e9227025f897017db0a7cbd7baea98d7f119aea49c46d6535d79caba,1000000,5000,1,665825", "ac001c7eff1cfaaf45f955a9a353f113153cd21610e5e8449b15559592b25d6e": "M2CNGNN,ac001c7eff1cfaaf45f955a9a353f113153cd21610e5e8449b15559592b25d6e,600000,6,0,669043", "cdd49f58806253abfa6f6566d0659c2f51c28256ef19acdde6a23331d6f07348": "qHBsg,cdd49f58806253abfa6f6566d0659c2f51c28256ef19acdde6a23331d6f07348,25840000,258,0,611324", "aaf29059ba14264d9fa85fe6700c13b36b3b4aa2748745eafefabcf276dc2c25": "87822,aaf29059ba14264d9fa85fe6700c13b36b3b4aa2748745eafefabcf276dc2c25,1000000,10,0,668839", "9ab825e4eb298ceb74237faf576c0f5088430fdf05e61a4e9ae4028508b318e6": "9134295,9ab825e4eb298ceb74237faf576c0f5088430fdf05e61a4e9ae4028508b318e6,30000000,30000,1,666606", "768df499434d48f6dc3329e0abfd3bbc930b884b2caff9b7e4d7f1ec15c4c28d": "5D4EQC,768df499434d48f6dc3329e0abfd3bbc930b884b2caff9b7e4d7f1ec15c4c28d,10000000,101,0,668001", "9b517de9ef9c00a779b58271a301347e13fc9525be42100452f462526a6f8523": "23608,9b517de9ef9c00a779b58271a301347e13fc9525be42100452f462526a6f8523,5000000,5000,1,668593", "02f8976ca80f98f095c5656675aa6f40aafced65451917443c1e5057186f2592": "I3WzjuF,02f8976ca80f98f095c5656675aa6f40aafced65451917443c1e5057186f2592,1000000,10,0,666563", "995de91e69e2590aff67ae6e4f2d417bad6882b11cc095b2420fef7506209be8": "WlvThoI,995de91e69e2590aff67ae6e4f2d417bad6882b11cc095b2420fef7506209be8,1000000,5000,1,669231", "ca4a1f991c3f585e4fbbb5b5aeb0766ba3eb46bb1c3ff3714db7a8cadd0e557d": "ffhpgz0z,ca4a1f991c3f585e4fbbb5b5aeb0766ba3eb46bb1c3ff3714db7a8cadd0e557d,2000000,20,0,667351", "b9e1a791f4091910caeb70d1a5d56452bc9614c16b5b74281b2485551faeb46e": "jgtwzsn,b9e1a791f4091910caeb70d1a5d56452bc9614c16b5b74281b2485551faeb46e,1000000,10,0,666372", "dc06cd41f4b778553a0a5df4578c62eeb0c9c878b6f1a24c60b619a6749877c7": "AZhkSO,dc06cd41f4b778553a0a5df4578c62eeb0c9c878b6f1a24c60b619a6749877c7,200000000,200000,1,668526" } ================================================ FILE: core/src/test/resources/haveno/core/provider/mempool/txInfo.json ================================================ { "44b00de808d0145f9a948fe1b020c5d4173402ba0b5a5ba69124c67e371bca18": "{\"txid\":\"44b00de808d0145f9a948fe1b020c5d4173402ba0b5a5ba69124c67e371bca18\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":1,\"prevout\":{\"value\":147186800}}],\"vout\":[{\"scriptpubkey_address\":\"1PUXU1MQ82JC3Hx1NN5tZs3BaTAJVg72MC\",\"value\":140000},{\"scriptpubkey_address\":\"1HwN7DhxNQdFKzMbrQq5vRHzY4xXGTRcne\",\"value\":147000000}],\"size\":226,\"weight\":904,\"fee\":46800,\"status\":{\"confirmed\":true,\"block_height\":578630}}", "2861f4526f40686d5cddc364035b561c13625996233a8b8705195041504ba3a1": "{\"txid\":\"2861f4526f40686d5cddc364035b561c13625996233a8b8705195041504ba3a1\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":1393}},{\"vout\":1,\"prevout\":{\"value\":600000}},{\"vout\":2,\"prevout\":{\"value\":10872788}}],\"vout\":[{\"scriptpubkey_address\":\"1NsTgbTUKhveanGCmsawJKLf6asQhJP4p2\",\"value\":1384},{\"scriptpubkey_address\":\"bc1qlw44hxyqfwcmcuuvtktduhth5ah4djl63sc4eq\",\"value\":1500000},{\"scriptpubkey_address\":\"bc1qyty4urzh25j5qypqu7v9mzhwt3p0zvaxeehpxp\",\"value\":9967337}],\"size\":552,\"weight\":1557,\"fee\":5460,\"status\":{\"confirmed\":true,\"block_height\":666479}}", "0636bafb14890edfb95465e66e2b1e15915f7fb595f9b653b9129c15ef4c1c4b": "{\"txid\":\"0636bafb14890edfb95465e66e2b1e15915f7fb595f9b653b9129c15ef4c1c4b\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":7899}},{\"vout\":2,\"prevout\":{\"value\":54877439}}],\"vout\":[{\"scriptpubkey_address\":\"1FCUu7hqKCSsGhVJaLbGEoCWdZRJRNqq8w\",\"value\":7889},{\"scriptpubkey_address\":\"bc1qkj5l4wxl00ufdx6ygcnrck9fz5u927gkwqcgey\",\"value\":1600000},{\"scriptpubkey_address\":\"bc1qkw4a8u9l5w9fhdh3ue9v7e7celk4jyudzg5gk5\",\"value\":53276799}],\"size\":405,\"weight\":1287,\"fee\":650,\"status\":{\"confirmed\":true,\"block_height\":663140}}", "a571e8a2e9227025f897017db0a7cbd7baea98d7f119aea49c46d6535d79caba": "{\"txid\":\"a571e8a2e9227025f897017db0a7cbd7baea98d7f119aea49c46d6535d79caba\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":2,\"prevout\":{\"value\":798055}}],\"vout\":[{\"scriptpubkey_address\":\"38bZBj5peYS3Husdz7AH3gEUiUbYRD951t\",\"value\":5000},{\"scriptpubkey_address\":\"bc1qy69ekanm2twzqqr7vz9qcxypyta29wdm2t0ay8\",\"value\":600000},{\"scriptpubkey_address\":\"bc1qp6q2urrntp8tq67lhymftsq0dpqvqmpnus7hym\",\"value\":184830}],\"size\":254,\"weight\":689,\"fee\":8225,\"status\":{\"confirmed\":true,\"block_height\":665826}}", "ac001c7eff1cfaaf45f955a9a353f113153cd21610e5e8449b15559592b25d6e": "{\"txid\":\"ac001c7eff1cfaaf45f955a9a353f113153cd21610e5e8449b15559592b25d6e\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":4165}},{\"vout\":2,\"prevout\":{\"value\":593157}},{\"vout\":2,\"prevout\":{\"value\":595850}}],\"vout\":[{\"scriptpubkey_address\":\"16Y1WqYEbWygHz6kuhJxWXos3bw46JNNoZ\",\"value\":4159},{\"scriptpubkey_address\":\"bc1qkxjvjp2hyegjpw5jtlju7fcr7pv9en3u7cg7q7\",\"value\":600000},{\"scriptpubkey_address\":\"bc1q9x95y8ktsxg9jucky66da3v2s2har56cy3nkkg\",\"value\":575363}],\"size\":555,\"weight\":1563,\"fee\":13650,\"status\":{\"confirmed\":true,\"block_height\":669045}}", "cdd49f58806253abfa6f6566d0659c2f51c28256ef19acdde6a23331d6f07348": "{\"txid\":\"cdd49f58806253abfa6f6566d0659c2f51c28256ef19acdde6a23331d6f07348\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":4898}},{\"vout\":2,\"prevout\":{\"value\":15977562}}],\"vout\":[{\"scriptpubkey_address\":\"16SCUfnCLddxgoAYLUcsJcoE4VxBRzgTSz\",\"value\":4640},{\"scriptpubkey_address\":\"1N9Pb6DTJXh96QjzYLDFTZuBvFXgFPi18N\",\"value\":1292000},{\"scriptpubkey_address\":\"1C7tg4KT9wQvLR5xfPDqD4U35Ncwk3UQxm\",\"value\":14681720}],\"size\":406,\"weight\":1624,\"fee\":4100,\"status\":{\"confirmed\":true,\"block_height\":611325}}", "aaf29059ba14264d9fa85fe6700c13b36b3b4aa2748745eafefabcf276dc2c25": "{\"txid\":\"aaf29059ba14264d9fa85fe6700c13b36b3b4aa2748745eafefabcf276dc2c25\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":3512}},{\"vout\":2,\"prevout\":{\"value\":1481349}},{\"vout\":1,\"prevout\":{\"value\":600000}}],\"vout\":[{\"scriptpubkey_address\":\"14rNP2aC23hr6u8ALmksm3RgJys7CAD3No\",\"value\":3502},{\"scriptpubkey_address\":\"bc1qvctcjcrhznptmydv4hxwc4wd2km76shkl3jj29\",\"value\":1600000},{\"scriptpubkey_address\":\"bc1qsdzpvr6sehypswcwjsmmjzctjhy5hkwqvf2vh8\",\"value\":476289}],\"size\":555,\"weight\":1563,\"fee\":5070,\"status\":{\"confirmed\":true,\"block_height\":668841}}", "9ab825e4eb298ceb74237faf576c0f5088430fdf05e61a4e9ae4028508b318e6": "{\"txid\":\"9ab825e4eb298ceb74237faf576c0f5088430fdf05e61a4e9ae4028508b318e6\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":4533500}}],\"vout\":[{\"scriptpubkey_address\":\"1EKXx73oUhHaUh8JBimtiPGgHfwNmxYKAj\",\"value\":30000},{\"scriptpubkey_address\":\"bc1qde7asrvrnkn5st5q8u038fxt9tlrgyaxwju6hn\",\"value\":4500000}],\"size\":226,\"weight\":574,\"fee\":3500,\"status\":{\"confirmed\":true,\"block_height\":666607}}", "768df499434d48f6dc3329e0abfd3bbc930b884b2caff9b7e4d7f1ec15c4c28d": "{\"txid\":\"768df499434d48f6dc3329e0abfd3bbc930b884b2caff9b7e4d7f1ec15c4c28d\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":249699}},{\"vout\":1,\"prevout\":{\"value\":4023781}}],\"vout\":[{\"scriptpubkey_address\":\"1J8wtmJuurfBSrRb27urtoHzuQey1ipXPX\",\"value\":249598},{\"scriptpubkey_address\":\"bc1qfmyw7pwaqucprcsauqr6gvez9wep290r4amd3y\",\"value\":1500000},{\"scriptpubkey_address\":\"bc1q2lx0fymd3mmk4pzjq2k8hn7mk3hnctnjtu497t\",\"value\":2517382}],\"size\":405,\"weight\":1287,\"fee\":6500,\"status\":{\"confirmed\":true,\"block_height\":668002}}", "02f8976ca80f98f095c5656675aa6f40aafced65451917443c1e5057186f2592": "{\"txid\":\"02f8976ca80f98f095c5656675aa6f40aafced65451917443c1e5057186f2592\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":33860}},{\"vout\":1,\"prevout\":{\"value\":600000}},{\"vout\":2,\"prevout\":{\"value\":39304}},{\"vout\":3,\"prevout\":{\"value\":5000000}}],\"vout\":[{\"scriptpubkey_address\":\"1Le1auzXSpEnyMc6S9KNentnye3gTPLnuA\",\"value\":33850},{\"scriptpubkey_address\":\"bc1qs73jfmjzclsx9466pvpslfuqc2kkv5uc8u928a\",\"value\":1600000},{\"scriptpubkey_address\":\"bc1q85zlv50mddyuerze7heve0vcv4f80qsw2szv34\",\"value\":4031088}],\"size\":701,\"weight\":1829,\"fee\":8226,\"status\":{\"confirmed\":true,\"block_height\":666564}}", "9b517de9ef9c00a779b58271a301347e13fc9525be42100452f462526a6f8523": "{\"txid\":\"9b517de9ef9c00a779b58271a301347e13fc9525be42100452f462526a6f8523\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":2,\"prevout\":{\"value\":4466600}}],\"vout\":[{\"scriptpubkey_address\":\"38bZBj5peYS3Husdz7AH3gEUiUbYRD951t\",\"value\":5000},{\"scriptpubkey_address\":\"bc1qrl0dvwp6hpqlcj65qfhl70lz67yjvhlc8z73a4\",\"value\":750000},{\"scriptpubkey_address\":\"bc1qg55gnkhgg4zltdh76sdef33xzr7h95g3xsxesg\",\"value\":3709675}],\"size\":254,\"weight\":689,\"fee\":1925,\"status\":{\"confirmed\":true,\"block_height\":668843}}", "995de91e69e2590aff67ae6e4f2d417bad6882b11cc095b2420fef7506209be8": "{\"txid\":\"995de91e69e2590aff67ae6e4f2d417bad6882b11cc095b2420fef7506209be8\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":2,\"prevout\":{\"value\":36006164}}],\"vout\":[{\"scriptpubkey_address\":\"19qA2BVPoyXDfHKVMovKG7SoxGY7xrBV8c\",\"value\":5000},{\"scriptpubkey_address\":\"bc1qefxyxsq9tskaw0qarxf0hdxusxe64l8zsmsgrz\",\"value\":1600000},{\"scriptpubkey_address\":\"bc1q6fky0fxcg3zrz5t0xdyq5sh90h7m5sya0wf9gx\",\"value\":34390664}],\"size\":256,\"weight\":697,\"fee\":10500,\"status\":{\"confirmed\":true,\"block_height\":669233}}", "fc3cb16293895fea8ea5d2d8ab4e39d1b27f583e2c160468b586789a861efa74": "{\"txid\":\"fc3cb16293895fea8ea5d2d8ab4e39d1b27f583e2c160468b586789a861efa74\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":1,\"prevout\":{\"value\":49144}},{\"vout\":0,\"prevout\":{\"value\":750000}}],\"vout\":[{\"scriptpubkey_address\":\"bc1qgsx9y62ajme3gg8v9n9jfps2694uy9r6f9unj0\",\"value\":600000},{\"scriptpubkey_address\":\"bc1q6lqf0jehmaadwmdhap98rulflft27z00g0qphn\",\"value\":187144}],\"size\":372,\"weight\":834,\"fee\":12000,\"status\":{\"confirmed\":true,\"block_height\":669442}}", "ca4a1f991c3f585e4fbbb5b5aeb0766ba3eb46bb1c3ff3714db7a8cadd0e557d": "{\"txid\":\"ca4a1f991c3f585e4fbbb5b5aeb0766ba3eb46bb1c3ff3714db7a8cadd0e557d\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":10237}},{\"vout\":1,\"prevout\":{\"value\":600000}},{\"vout\":1,\"prevout\":{\"value\":900000}},{\"vout\":2,\"prevout\":{\"value\":8858290}}],\"vout\":[{\"scriptpubkey_address\":\"1DQXP1AXR1qkXficdkfXHHy2JkbtRGFQ1b\",\"value\":10217},{\"scriptpubkey_address\":\"bc1q9jfjvhvr42smvwylrlqcrefcdxagdzf52aquzm\",\"value\":2600000},{\"scriptpubkey_address\":\"bc1qc6qraj5h8qxvluh2um4rvunqn68fltc9kjfrk9\",\"value\":7753730}],\"size\":702,\"weight\":1833,\"fee\":4580,\"status\":{\"confirmed\":true,\"block_height\":667352}}", "4cdea8872a7d96210f378e0221dc1aae8ee9abb282582afa7546890fb39b7189": "{\"txid\":\"4cdea8872a7d96210f378e0221dc1aae8ee9abb282582afa7546890fb39b7189\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":23893}},{\"vout\":1,\"prevout\":{\"value\":1440000}},{\"vout\":2,\"prevout\":{\"value\":16390881}}],\"vout\":[{\"scriptpubkey_address\":\"1Kmrzq3WGCQsZw5kroEphuk1KgsEr65yB7\",\"value\":23858},{\"scriptpubkey_address\":\"bc1qyw5qql9m7rkse9mhcun225nrjpwycszsa5dpjg\",\"value\":7015000},{\"scriptpubkey_address\":\"bc1q90y3p6mg0pe3rvvzfeudq4mfxafgpc9rulruff\",\"value\":10774186}],\"size\":554,\"weight\":1559,\"fee\":41730,\"status\":{\"confirmed\":true,\"block_height\":668198}}", "b9e1a791f4091910caeb70d1a5d56452bc9614c16b5b74281b2485551faeb46e": "{\"txid\":\"b9e1a791f4091910caeb70d1a5d56452bc9614c16b5b74281b2485551faeb46e\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":1432}},{\"vout\":2,\"prevout\":{\"value\":30302311}}],\"vout\":[{\"scriptpubkey_address\":\"16MK64AGvKVF7xu9Xfjh8o7Xo4e1HMhUqq\",\"value\":1422},{\"scriptpubkey_address\":\"bc1qjp535w2zl3cxg02xgdx8yewtvn6twcnj86t73c\",\"value\":1600000},{\"scriptpubkey_address\":\"bc1qa58rfr0wumczmau0qehjwcsdkcgs5dmkg7url5\",\"value\":28698421}],\"size\":405,\"weight\":1287,\"fee\":3900,\"status\":{\"confirmed\":true,\"block_height\":666373}}", "dc06cd41f4b778553a0a5df4578c62eeb0c9c878b6f1a24c60b619a6749877c7": "{\"txid\":\"dc06cd41f4b778553a0a5df4578c62eeb0c9c878b6f1a24c60b619a6749877c7\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":230000000}}],\"vout\":[{\"scriptpubkey_address\":\"38bZBj5peYS3Husdz7AH3gEUiUbYRD951t\",\"value\":200000},{\"scriptpubkey_address\":\"bc1qaq3v7gjqaeyx7yzkcu59l8f47apkfutr927xa8\",\"value\":30000000},{\"scriptpubkey_address\":\"bc1qx9avgdnkal2jfcfjqdsdu7ly60awl4wcfgk6m0\",\"value\":199793875}],\"size\":255,\"weight\":690,\"fee\":6125,\"status\":{\"confirmed\":true,\"block_height\":668527}}", "ef1ea38b46402deb7df08c13a6dc379a65542a6940ac9d4ba436641ffd4bcb6e": "{\"txid\":\"ef1ea38b46402deb7df08c13a6dc379a65542a6940ac9d4ba436641ffd4bcb6e\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":61000}},{\"vout\":0,\"prevout\":{\"value\":6415500}}],\"vout\":[{\"scriptpubkey_address\":\"164hNDe95nNsQYVSVbeypn36HqT5uD5AoT\",\"value\":60908},{\"scriptpubkey_address\":\"1MEsN2jLyrcWBMjggSPs88xAnj6D38sQL3\",\"value\":2395500},{\"scriptpubkey_address\":\"1A3pYPW1zQcMpHUnSfPCxYWgCrUW93t2yV\",\"value\":3973352}],\"size\":408,\"weight\":1632,\"fee\":46740,\"status\":{\"confirmed\":true,\"block_height\":640441}}", "654a7a34321b57be6a553052d1d9e0f1764dd2fab7b64c9422e9953e4d9d127d": "{\"txid\":\"654a7a34321b57be6a553052d1d9e0f1764dd2fab7b64c9422e9953e4d9d127d\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":26000000}}],\"vout\":[{\"scriptpubkey_address\":\"18GzH11T5h2fpvUoBJDub7MgNJVw3FfqQ8\",\"value\":238000},{\"scriptpubkey_address\":\"1JYZ4cba5pjPxXqm5MDGUVvj2k3cZezRaR\",\"value\":3000000},{\"scriptpubkey_address\":\"12DNP86oaEXfEBkow4Kpkw2tNaqoECYhtc\",\"value\":22756800}],\"size\":260,\"weight\":1040,\"fee\":5200,\"status\":{\"confirmed\":true,\"block_height\":554950}}", "1c8e4934f93b5bbd2823318d5d491698316216f2e4bc0d7cd353f6b16358d80e": "{\"txid\":\"1c8e4934f93b5bbd2823318d5d491698316216f2e4bc0d7cd353f6b16358d80e\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":2,\"prevout\":{\"value\":563209}},{\"vout\":1,\"prevout\":{\"value\":600000}},{\"vout\":2,\"prevout\":{\"value\":214153}},{\"vout\":2,\"prevout\":{\"value\":116517}},{\"vout\":2,\"prevout\":{\"value\":135306}},{\"vout\":2,\"prevout\":{\"value\":261906}},{\"vout\":2,\"prevout\":{\"value\":598038}},{\"vout\":1,\"prevout\":{\"value\":600000}},{\"vout\":1,\"prevout\":{\"value\":600000}},{\"vout\":1,\"prevout\":{\"value\":600932}},{\"vout\":1,\"prevout\":{\"value\":600944}}],\"vout\":[{\"scriptpubkey_address\":\"19qA2BVPoyXDfHKVMovKG7SoxGY7xrBV8c\",\"value\":546},{\"scriptpubkey_address\":\"bc1qwcwu3mx0nmf290y8t0jlukhxujaul0fc4jxe44\",\"value\":4743164},{\"scriptpubkey_address\":\"bc1qduw8nd2sscyezk02xj3a3ks5adh8wmctqaew6g\",\"value\":145131}],\"size\":1746,\"weight\":3417,\"fee\":2164,\"status\":{\"confirmed\":true,\"block_height\":669227}}", "e125fdbd09ee86c01e16e1f12a31507cfb8703ed1bd5a221461adf33cb3e00d9": "{\"txid\":\"e125fdbd09ee86c01e16e1f12a31507cfb8703ed1bd5a221461adf33cb3e00d9\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":1,\"prevout\":{\"value\":4250960}}],\"vout\":[{\"scriptpubkey_address\":\"1PUXU1MQ82JC3Hx1NN5tZs3BaTAJVg72MC\",\"value\":200000},{\"scriptpubkey_address\":\"1MSkjSzF1dTKR121scX64Brvs4zhExVE8Q\",\"value\":4000000}],\"size\":225,\"weight\":900,\"fee\":50960,\"status\":{\"confirmed\":true,\"block_height\":578733}}", "051770f8d7f43a9b6ca10fefa6cdf4cb124a81eed26dc8af2e40f57d2589107b": "{\"txid\":\"051770f8d7f43a9b6ca10fefa6cdf4cb124a81eed26dc8af2e40f57d2589107b\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":0,\"prevout\":{\"value\":23985}},{\"vout\":1,\"prevout\":{\"value\":6271500}},{\"vout\":0,\"prevout\":{\"value\":2397000}},{\"vout\":2,\"prevout\":{\"value\":41281331}}],\"vout\":[{\"scriptpubkey_address\":\"16pULNutwpJ5E6EaxopQQDAVaFJXt8B18Z\",\"value\":23893},{\"scriptpubkey_address\":\"bc1q6hkhftt9v5kkcj9wr66ycqy23dqyle3h3wnv50\",\"value\":18365500},{\"scriptpubkey_address\":\"bc1q3ffqm4e4wxdg8jgcw0wlpw4vg9hgwnql3y9zn0\",\"value\":31546169}],\"size\":703,\"weight\":2476,\"fee\":38254,\"status\":{\"confirmed\":true,\"block_height\":667928}}", "72cabb5c323c923b43c7f6551974f591dcee148778ee34f9131011ea0ca82813": "{\"txid\":\"72cabb5c323c923b43c7f6551974f591dcee148778ee34f9131011ea0ca82813\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":1,\"prevout\":{\"value\":12000000}}],\"vout\":[{\"scriptpubkey_address\":\"3EfRGckBQQuk7cpU7SwatPv8kFD1vALkTU\",\"value\":546},{\"scriptpubkey_address\":\"bc1q6xthjqca0p83mua54e9t0sapxkvc7n3dvwssxc\",\"value\":2600000},{\"scriptpubkey_address\":\"bc1q3uaew9e6uqm6pth8nq7wh3wcwzxwh2q25fggcg\",\"value\":9388079}],\"size\":254,\"weight\":689,\"fee\":11375,\"status\":{\"confirmed\":true,\"block_height\":672972}}", "17cbd95d8809dc8808a5c209208f59c4a80e09e012e62951668d30d716c44a96": "{\"txid\":\"17cbd95d8809dc8808a5c209208f59c4a80e09e012e62951668d30d716c44a96\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":2,\"prevout\":{\"value\":2810563}}],\"vout\":[{\"scriptpubkey_address\":\"13sxMq8mTw7CTSqgGiMPfwo6ZDsVYrHLmR\",\"value\":546},{\"scriptpubkey_address\":\"bc1qklv4zsl598ujy2ntl5g3wqjxasu2f74egw0tlm\",\"value\":1603262},{\"scriptpubkey_address\":\"bc1qclesyfupj309620thesxmj4vcdscjfykdqz4np\",\"value\":1205124}],\"size\":256,\"weight\":697,\"fee\":1631,\"status\":{\"confirmed\":true,\"block_height\":669340}}", "cd99836ac4246c3e3980edf95773060481ce52271b74dadeb41e18c42ed21188": "{\"txid\":\"cd99836ac4246c3e3980edf95773060481ce52271b74dadeb41e18c42ed21188\",\"version\":1,\"locktime\":0,\"vin\":[{\"vout\":2,\"prevout\":{\"value\":200584}},{\"vout\":1,\"prevout\":{\"value\":600000}}],\"vout\":[{\"scriptpubkey_address\":\"bc1q7sd0k2a6p942848y5nsk9cqwdguhd7c04t2t3w\",\"value\":750000},{\"scriptpubkey_address\":\"bc1qrcez45uf02sg6zvk3mqmtlc9vnrvn50jcywlk5\",\"value\":49144}],\"size\":371,\"weight\":833,\"fee\":1440,\"status\":{\"confirmed\":true,\"block_height\":669442}}" } ================================================ FILE: core/src/test/resources/mainnet.seednodes ================================================ # nodeaddress.onion:port [(@owner,@backup)] 5quyxpxheyvzmb2d.onion:8000 (@miker) s67qglwhkgkyvr74.onion:8000 (@emzy) ef5qnzx6znifo3df.onion:8000 (@alexej996) jhgcy2won7xnslrb.onion:8000 (@wiz,@nicolasdorier) 3f3cu2yw7u457ztq.onion:8000 (@devinbileck,@ripcurlx) 723ljisnynbtdohi.onion:8000 (@emzy) rm7b56wbrcczpjvl.onion:8000 (@miker) fl3mmribyxgrv63c.onion:8000 (@devinbileck,@ripcurlx) ================================================ FILE: core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker ================================================ mock-maker-inline # enable mocking final classes in mockito ================================================ FILE: core/src/test/resources/regtest.seednodes ================================================ # For development you need to change that to your local onion addresses # 1. Run a seed node with prog args: --bitcoinNetwork=regtest --nodePort=8002 --appName=haveno_seed_node_rxdkppp3vicnbgqt.onion_8002 # 2. Find your local onion address in haveno_seed_node_rxdkppp3vicnbgqt.onion_8002/regtest/tor/hiddenservice/hostname # 3. Shut down the seed node # 4. Rename the directory with your local onion address # 5. Edit here your found onion address (new NodeAddress("YOUR_ONION.onion:8002") # nodeaddress.onion:port [(@owner)] rxdkppp3vicnbgqt.onion:8002 4ie52dse64kaarxw.onion:8002 ================================================ FILE: core/src/test/resources/testnet.seednodes ================================================ # nodeaddress.onion:port [(@owner)] snenz4mea65wigen.onion:8001 fjr5w4eckjghqtnu.onion:8001 3d56s6acbi3vk52v.onion:8001 74w2sttlo4qk6go3.onion:8001 gtif46mfxirv533z.onion:8001 jmc5ajqvtnzqaggm.onion:8001 ================================================ FILE: core/update_translations.sh ================================================ #!/usr/bin/env bash cd $(dirname $0) tx pull -l de,es,ja,pt,ru,zh_CN,zh_TW,vi,th_TH,fa,fr,pt_BR,it,cs translations="translations/haveno-desktop.displaystringsproperties" i18n="src/main/resources/i18n" mv "$translations/de.properties" "$i18n/displayStrings_de.properties" mv "$translations/es.properties" "$i18n/displayStrings_es.properties" mv "$translations/ja.properties" "$i18n/displayStrings_ja.properties" mv "$translations/pt.properties" "$i18n/displayStrings_pt.properties" mv "$translations/ru.properties" "$i18n/displayStrings_ru.properties" mv "$translations/zh_CN.properties" "$i18n/displayStrings_zh-hans.properties" mv "$translations/zh_TW.properties" "$i18n/displayStrings_zh-hant.properties" mv "$translations/vi.properties" "$i18n/displayStrings_vi.properties" mv "$translations/th_TH.properties" "$i18n/displayStrings_th.properties" mv "$translations/fa.properties" "$i18n/displayStrings_fa.properties" mv "$translations/fr.properties" "$i18n/displayStrings_fr.properties" mv "$translations/pt_BR.properties" "$i18n/displayStrings_pt-br.properties" mv "$translations/it.properties" "$i18n/displayStrings_it.properties" mv "$translations/cs.properties" "$i18n/displayStrings_cs.properties" rm -rf $translations ================================================ FILE: daemon/src/main/java/haveno/daemon/app/HavenoDaemon.java ================================================ /* * 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 . */ package haveno.daemon.app; import haveno.core.app.HavenoHeadlessApp; public class HavenoDaemon extends HavenoHeadlessApp { } ================================================ FILE: daemon/src/main/java/haveno/daemon/app/HavenoDaemonMain.java ================================================ /* * 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 . */ package haveno.daemon.app; import com.google.common.util.concurrent.ThreadFactoryBuilder; import haveno.common.UserThread; import haveno.common.app.AppModule; import haveno.common.crypto.IncorrectPasswordException; import haveno.common.handlers.ResultHandler; import haveno.common.persistence.PersistenceManager; import haveno.core.api.AccountServiceListener; import haveno.core.app.ConsoleInput; import haveno.core.app.CoreModule; import haveno.core.app.HavenoHeadlessAppMain; import haveno.daemon.grpc.GrpcServer; import lombok.extern.slf4j.Slf4j; import java.io.Console; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @Slf4j public class HavenoDaemonMain extends HavenoHeadlessAppMain { private GrpcServer grpcServer; public static void main(String[] args) { var keepRunning = true; while (keepRunning) { keepRunning = false; var daemon = new HavenoDaemonMain(); var ret = daemon.execute(args); if (ret == EXIT_SUCCESS) { UserThread.execute(() -> daemon.gracefulShutDown(() -> {})); } else if (ret == EXIT_RESTART) { AtomicBoolean shuttingDown = new AtomicBoolean(true); UserThread.execute(() -> daemon.gracefulShutDown(() -> shuttingDown.set(false), false)); keepRunning = true; // wait for graceful shutdown try { while (shuttingDown.get()) { Thread.sleep(1000); } PersistenceManager.reset(); } catch (InterruptedException e) { System.out.println("interrupted!"); } } } } ///////////////////////////////////////////////////////////////////////////////////// // First synchronous execution tasks ///////////////////////////////////////////////////////////////////////////////////// @Override protected void configUserThread() { final ThreadFactory threadFactory = new ThreadFactoryBuilder() .setNameFormat(this.getClass().getSimpleName()) .setDaemon(true) .build(); UserThread.setExecutor(Executors.newSingleThreadExecutor(threadFactory)); } @Override protected void launchApplication() { headlessApp = new HavenoDaemon(); UserThread.execute(this::onApplicationLaunched); } @Override protected void onApplicationLaunched() { super.onApplicationLaunched(); headlessApp.setGracefulShutDownHandler(this); } ///////////////////////////////////////////////////////////////////////////////////// // We continue with a series of synchronous execution tasks ///////////////////////////////////////////////////////////////////////////////////// @Override protected AppModule getModule() { return new CoreModule(config); } @Override protected void applyInjector() { super.applyInjector(); headlessApp.setInjector(injector); } @Override protected void startApplication() { // We need to be in user thread! We mapped at launchApplication already... headlessApp.startApplication(); // In headless mode we don't have an async behaviour so we trigger the setup by // calling onApplicationStarted. onApplicationStarted(); } @Override protected void onApplicationStarted() { super.onApplicationStarted(); } @Override public void gracefulShutDown(ResultHandler resultHandler, boolean exit) { super.gracefulShutDown(resultHandler, exit); if (grpcServer != null) grpcServer.shutdown(); // could be null if application attempted to shutdown early } /** * Start the grpcServer to allow logging in remotely. */ @Override protected CompletableFuture loginAccount() { CompletableFuture opened = super.loginAccount(); // Start rpc server in case login is coming in from rpc grpcServer = injector.getInstance(GrpcServer.class); CompletableFuture inputResult = new CompletableFuture(); try { if (opened.get()) { grpcServer.start(); return opened; } else { // Nonblocking, we need to stop if the login occurred through rpc. // TODO: add a mode to mask password ConsoleInput reader = new ConsoleInput(Integer.MAX_VALUE, Integer.MAX_VALUE, TimeUnit.MILLISECONDS); Thread t = new Thread(() -> { interactiveLogin(reader); }); // Handle asynchronous account opens. // Will need to also close and reopen account. AccountServiceListener accountListener = new AccountServiceListener() { @Override public void onAccountCreated() { onLogin(); } @Override public void onAccountOpened() { onLogin(); } private void onLogin() { log.info("Logged in successfully"); reader.cancel(); // closing the reader will stop all read attempts and end the interactive login thread } }; accountService.addListener(accountListener); // start server after the listener is registered grpcServer.start(); try { // Wait until interactive login or rpc. Check one more time if account is open to close race condition. if (!accountService.isAccountOpen()) { log.info("Interactive login required"); t.start(); t.join(); } } catch (InterruptedException e) { // expected } accountService.removeListener(accountListener); inputResult.complete(accountService.isAccountOpen()); } } catch (InterruptedException | ExecutionException e) { inputResult.completeExceptionally(e); } return inputResult; } /** * Asks user for login. TODO: Implement in the desktop app. * @return True if user logged in interactively. */ protected boolean interactiveLogin(ConsoleInput reader) { Console console = System.console(); if (console == null) { // The ConsoleInput class reads from system.in, can wait for input without a console. log.info("No console available, account must be opened through rpc"); try { // If user logs in through rpc, the reader will be interrupted through the event. reader.readLine(); } catch (InterruptedException | CancellationException ex) { log.info("Reader interrupted, continuing startup"); } return false; } String openedOrCreated = "Account unlocked\n"; boolean accountExists = accountService.accountExists(); while (!accountService.isAccountOpen()) { try { if (accountExists) { try { // readPassword will not return until the user inputs something // which is not suitable if we are waiting for rpc call which // could login the account. Must be able to interrupt the read. //new String(console.readPassword("Password:")); System.out.printf("Password:\n"); String password = reader.readLine(); accountService.openAccount(password); } catch (IncorrectPasswordException ipe) { System.out.printf("Incorrect password\n"); } } else { System.out.printf("Creating a new account\n"); System.out.printf("Password:\n"); String password = reader.readLine(); System.out.printf("Confirm:\n"); String passwordConfirm = reader.readLine(); if (password.equals(passwordConfirm)) { accountService.createAccount(password); openedOrCreated = "Account created\n"; } else { System.out.printf("Passwords did not match\n"); } } } catch (Exception ex) { log.debug(ex.getMessage()); return false; } } System.out.printf(openedOrCreated); return true; } } ================================================ FILE: daemon/src/main/java/haveno/daemon/grpc/GrpcAccountService.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.daemon.grpc; import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; import com.google.protobuf.ByteString; import haveno.common.UserThread; import haveno.common.crypto.IncorrectPasswordException; import haveno.core.api.CoreApi; import haveno.daemon.grpc.interceptor.CallRateMeteringInterceptor; import haveno.daemon.grpc.interceptor.GrpcCallRateMeter; import static haveno.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; import haveno.proto.grpc.AccountExistsReply; import haveno.proto.grpc.AccountExistsRequest; import haveno.proto.grpc.AccountGrpc.AccountImplBase; import static haveno.proto.grpc.AccountGrpc.getAccountExistsMethod; import static haveno.proto.grpc.AccountGrpc.getBackupAccountMethod; import static haveno.proto.grpc.AccountGrpc.getChangePasswordMethod; import static haveno.proto.grpc.AccountGrpc.getCloseAccountMethod; import static haveno.proto.grpc.AccountGrpc.getCreateAccountMethod; import static haveno.proto.grpc.AccountGrpc.getDeleteAccountMethod; import static haveno.proto.grpc.AccountGrpc.getIsAccountOpenMethod; import static haveno.proto.grpc.AccountGrpc.getOpenAccountMethod; import static haveno.proto.grpc.AccountGrpc.getRestoreAccountMethod; import haveno.proto.grpc.BackupAccountReply; import haveno.proto.grpc.BackupAccountRequest; import haveno.proto.grpc.ChangePasswordReply; import haveno.proto.grpc.ChangePasswordRequest; import haveno.proto.grpc.CloseAccountReply; import haveno.proto.grpc.CloseAccountRequest; import haveno.proto.grpc.CreateAccountReply; import haveno.proto.grpc.CreateAccountRequest; import haveno.proto.grpc.DeleteAccountReply; import haveno.proto.grpc.DeleteAccountRequest; import haveno.proto.grpc.IsAccountOpenReply; import haveno.proto.grpc.IsAccountOpenRequest; import haveno.proto.grpc.IsAppInitializedReply; import haveno.proto.grpc.IsAppInitializedRequest; import haveno.proto.grpc.OpenAccountReply; import haveno.proto.grpc.OpenAccountRequest; import haveno.proto.grpc.RestoreAccountReply; import haveno.proto.grpc.RestoreAccountRequest; import io.grpc.ServerInterceptor; import io.grpc.stub.StreamObserver; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.util.HashMap; import java.util.Optional; import static java.util.concurrent.TimeUnit.SECONDS; import lombok.extern.slf4j.Slf4j; @VisibleForTesting @Slf4j public class GrpcAccountService extends AccountImplBase { private final CoreApi coreApi; private final GrpcExceptionHandler exceptionHandler; private ByteArrayOutputStream restoreStream; // in memory stream for restoring account @Inject public GrpcAccountService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) { this.coreApi = coreApi; this.exceptionHandler = exceptionHandler; } @Override public void accountExists(AccountExistsRequest req, StreamObserver responseObserver) { try { var reply = AccountExistsReply.newBuilder() .setAccountExists(coreApi.accountExists()) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void isAccountOpen(IsAccountOpenRequest req, StreamObserver responseObserver) { try { var reply = IsAccountOpenReply.newBuilder() .setIsAccountOpen(coreApi.isAccountOpen()) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void createAccount(CreateAccountRequest req, StreamObserver responseObserver) { try { coreApi.createAccount(req.getPassword()); var reply = CreateAccountReply.newBuilder() .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void openAccount(OpenAccountRequest req, StreamObserver responseObserver) { try { coreApi.openAccount(req.getPassword()); var reply = OpenAccountReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { if (cause instanceof IncorrectPasswordException) cause = new IllegalStateException(cause); exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void isAppInitialized(IsAppInitializedRequest req, StreamObserver responseObserver) { UserThread.execute(() -> { try { var reply = IsAppInitializedReply.newBuilder().setIsAppInitialized(coreApi.isAppInitialized()).build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } }); } @Override public void changePassword(ChangePasswordRequest req, StreamObserver responseObserver) { try { coreApi.changePassword(req.getOldPassword(), req.getNewPassword()); var reply = ChangePasswordReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void closeAccount(CloseAccountRequest req, StreamObserver responseObserver) { try { coreApi.closeAccount(); var reply = CloseAccountReply.newBuilder() .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void deleteAccount(DeleteAccountRequest req, StreamObserver responseObserver) { try { coreApi.deleteAccount(() -> { var reply = DeleteAccountReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); // reply after shutdown }); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void backupAccount(BackupAccountRequest req, StreamObserver responseObserver) { // Send in large chunks to reduce unnecessary overhead. Typical backup will not be more than a few MB. // From current testing it appears that client gRPC-web is slow in processing the bytes on download. try { int bufferSize = 1024 * 1024 * 8; coreApi.backupAccount(bufferSize, (stream) -> { try { log.info("Sending bytes in chunks of: " + bufferSize); byte[] buffer = new byte[bufferSize]; int length; int total = 0; while ((length = stream.read(buffer, 0, bufferSize)) != -1) { total += length; var reply = BackupAccountReply.newBuilder() .setZipBytes(ByteString.copyFrom(buffer, 0, length)) .build(); responseObserver.onNext(reply); } log.info("Completed backup account total sent: " + total); stream.close(); responseObserver.onCompleted(); } catch (Exception ex) { exceptionHandler.handleException(log, ex, responseObserver); } }, (ex) -> exceptionHandler.handleException(log, ex, responseObserver)); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void restoreAccount(RestoreAccountRequest req, StreamObserver responseObserver) { try { // Fail fast since uploading and processing bytes takes resources. if (coreApi.accountExists()) throw new IllegalStateException("Cannot restore account if there is an existing account"); // If the entire zip is in memory, no need to write to disk. // Restore the account directly from the zip stream. if (!req.getHasMore() && req.getOffset() == 0) { var inputStream = req.getZipBytes().newInput(); coreApi.restoreAccount(inputStream, 1024 * 64, () -> { var reply = RestoreAccountReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); // reply after shutdown }); } else { if (req.getOffset() == 0) { log.info("RestoreAccount starting new chunked zip"); restoreStream = new ByteArrayOutputStream((int) req.getTotalLength()); } if (restoreStream.size() != req.getOffset()) { log.warn("Stream offset doesn't match current position"); IllegalStateException cause = new IllegalStateException("Stream offset doesn't match current position"); exceptionHandler.handleException(log, cause, responseObserver); } else { log.info("RestoreAccount writing chunk size " + req.getZipBytes().size()); req.getZipBytes().writeTo(restoreStream); } if (!req.getHasMore()) { var inputStream = new ByteArrayInputStream(restoreStream.toByteArray()); restoreStream.close(); restoreStream = null; coreApi.restoreAccount(inputStream, 1024 * 64, () -> { var reply = RestoreAccountReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); // reply after shutdown }); } else { var reply = RestoreAccountReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } } } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } final ServerInterceptor[] interceptors() { Optional rateMeteringInterceptor = rateMeteringInterceptor(); return rateMeteringInterceptor.map(serverInterceptor -> new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); } final Optional rateMeteringInterceptor() { return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ put(getAccountExistsMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getBackupAccountMethod().getFullMethodName(), new GrpcCallRateMeter(5, SECONDS)); put(getChangePasswordMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getCloseAccountMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getCreateAccountMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getDeleteAccountMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getIsAccountOpenMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getOpenAccountMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getRestoreAccountMethod().getFullMethodName(), new GrpcCallRateMeter(5, SECONDS)); }} ))); } } ================================================ FILE: daemon/src/main/java/haveno/daemon/grpc/GrpcDisputeAgentsService.java ================================================ package haveno.daemon.grpc; import com.google.inject.Inject; import haveno.core.api.CoreApi; import haveno.daemon.grpc.interceptor.CallRateMeteringInterceptor; import haveno.daemon.grpc.interceptor.GrpcCallRateMeter; import static haveno.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; import static haveno.proto.grpc.DisputeAgentsGrpc.DisputeAgentsImplBase; import static haveno.proto.grpc.DisputeAgentsGrpc.getRegisterDisputeAgentMethod; import static haveno.proto.grpc.DisputeAgentsGrpc.getUnregisterDisputeAgentMethod; import haveno.proto.grpc.RegisterDisputeAgentReply; import haveno.proto.grpc.RegisterDisputeAgentRequest; import haveno.proto.grpc.UnregisterDisputeAgentReply; import haveno.proto.grpc.UnregisterDisputeAgentRequest; import io.grpc.ServerInterceptor; import io.grpc.stub.StreamObserver; import java.util.HashMap; import java.util.Optional; import static java.util.concurrent.TimeUnit.SECONDS; import lombok.extern.slf4j.Slf4j; @Slf4j class GrpcDisputeAgentsService extends DisputeAgentsImplBase { private final CoreApi coreApi; private final GrpcExceptionHandler exceptionHandler; @Inject public GrpcDisputeAgentsService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) { this.coreApi = coreApi; this.exceptionHandler = exceptionHandler; } @Override public void registerDisputeAgent(RegisterDisputeAgentRequest req, StreamObserver responseObserver) { try { GrpcErrorMessageHandler errorMessageHandler = new GrpcErrorMessageHandler(getRegisterDisputeAgentMethod().getFullMethodName(), responseObserver, exceptionHandler, log); coreApi.registerDisputeAgent( req.getDisputeAgentType(), req.getRegistrationKey(), () -> { var reply = RegisterDisputeAgentReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); }, errorMessage -> { if (!errorMessageHandler.isErrorHandled()) errorMessageHandler.handleErrorMessage(errorMessage); }); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void unregisterDisputeAgent(UnregisterDisputeAgentRequest req, StreamObserver responseObserver) { try { GrpcErrorMessageHandler errorMessageHandler = new GrpcErrorMessageHandler(getUnregisterDisputeAgentMethod().getFullMethodName(), responseObserver, exceptionHandler, log); coreApi.unregisterDisputeAgent( req.getDisputeAgentType(), () -> { var reply = UnregisterDisputeAgentReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); }, errorMessage -> { if (!errorMessageHandler.isErrorHandled()) errorMessageHandler.handleErrorMessage(errorMessage); }); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } final ServerInterceptor[] interceptors() { Optional rateMeteringInterceptor = rateMeteringInterceptor(); return rateMeteringInterceptor.map(serverInterceptor -> new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); } final Optional rateMeteringInterceptor() { return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ // Do not limit devs' ability to test agent registration // and call validation in regtest arbitration daemons. put(getRegisterDisputeAgentMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); }} ))); } } ================================================ FILE: daemon/src/main/java/haveno/daemon/grpc/GrpcDisputesService.java ================================================ package haveno.daemon.grpc; import com.google.inject.Inject; import haveno.common.ThreadUtils; import haveno.common.UserThread; import haveno.common.config.Config; import haveno.common.proto.ProtoUtil; import haveno.core.api.CoreApi; import haveno.core.support.dispute.Attachment; import haveno.core.support.dispute.DisputeResult; import haveno.daemon.grpc.interceptor.CallRateMeteringInterceptor; import haveno.daemon.grpc.interceptor.GrpcCallRateMeter; import static haveno.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; import haveno.proto.grpc.DisputesGrpc.DisputesImplBase; import static haveno.proto.grpc.DisputesGrpc.getGetDisputeMethod; import static haveno.proto.grpc.DisputesGrpc.getGetDisputesMethod; import static haveno.proto.grpc.DisputesGrpc.getOpenDisputeMethod; import static haveno.proto.grpc.DisputesGrpc.getResolveDisputeMethod; import static haveno.proto.grpc.DisputesGrpc.getSendDisputeChatMessageMethod; import haveno.proto.grpc.GetDisputeReply; import haveno.proto.grpc.GetDisputeRequest; import haveno.proto.grpc.GetDisputesReply; import haveno.proto.grpc.GetDisputesRequest; import haveno.proto.grpc.OpenDisputeReply; import haveno.proto.grpc.OpenDisputeRequest; import haveno.proto.grpc.ResolveDisputeReply; import haveno.proto.grpc.ResolveDisputeRequest; import haveno.proto.grpc.SendDisputeChatMessageReply; import haveno.proto.grpc.SendDisputeChatMessageRequest; import io.grpc.ServerInterceptor; import io.grpc.stub.StreamObserver; import java.util.ArrayList; import java.util.HashMap; import java.util.Optional; import static java.util.concurrent.TimeUnit.SECONDS; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @Slf4j public class GrpcDisputesService extends DisputesImplBase { private final CoreApi coreApi; private final GrpcExceptionHandler exceptionHandler; @Inject public GrpcDisputesService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) { this.coreApi = coreApi; this.exceptionHandler = exceptionHandler; } @Override public void openDispute(OpenDisputeRequest req, StreamObserver responseObserver) { UserThread.execute(() -> { ThreadUtils.submitToPool(() -> { try { coreApi.openDispute(req.getTradeId(), () -> { var reply = OpenDisputeReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); }, (errorMessage, throwable) -> { log.info("Error in openDispute" + errorMessage); exceptionHandler.handleErrorMessage(log, errorMessage, responseObserver); }); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } }); }); } @Override public void getDispute(GetDisputeRequest req, StreamObserver responseObserver) { UserThread.execute(() -> { // offers are updated on user thread ThreadUtils.submitToPool(() -> { try { var dispute = coreApi.getDispute(req.getTradeId()); var reply = GetDisputeReply.newBuilder() .setDispute(dispute.toProtoMessage()) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleExceptionAsWarning(log, getClass().getName() + ".getDispute", cause, responseObserver); } }); }); } @Override public void getDisputes(GetDisputesRequest req, StreamObserver responseObserver) { UserThread.execute(() -> { ThreadUtils.submitToPool(() -> { try { var disputes = coreApi.getDisputes(); var disputesProtobuf = disputes.stream() .map(d -> d.toProtoMessage()) .collect(Collectors.toList()); var reply = GetDisputesReply.newBuilder() .addAllDisputes(disputesProtobuf) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } }); }); } @Override public void resolveDispute(ResolveDisputeRequest req, StreamObserver responseObserver) { UserThread.execute(() -> { ThreadUtils.submitToPool(() -> { try { var winner = ProtoUtil.enumFromProto(DisputeResult.Winner.class, req.getWinner().name()); var reason = ProtoUtil.enumFromProto(DisputeResult.Reason.class, req.getReason().name()); coreApi.resolveDispute(req.getTradeId(), winner, reason, req.getSummaryNotes(), req.getCustomPayoutAmount()); var reply = ResolveDisputeReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { cause.printStackTrace(); exceptionHandler.handleExceptionAsWarning(log, getClass().getName() + ".resolveDispute", cause, responseObserver); } }); }); } @Override public void sendDisputeChatMessage(SendDisputeChatMessageRequest req, StreamObserver responseObserver) { UserThread.execute(() -> { ThreadUtils.submitToPool(() -> { try { var attachmentsProto = req.getAttachmentsList(); var attachments = attachmentsProto.stream().map(a -> Attachment.fromProto(a)) .collect(Collectors.toList()); coreApi.sendDisputeChatMessage(req.getDisputeId(), req.getMessage(), new ArrayList(attachments)); var reply = SendDisputeChatMessageReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } }); }); } final ServerInterceptor[] interceptors() { Optional rateMeteringInterceptor = rateMeteringInterceptor(); return rateMeteringInterceptor.map(serverInterceptor -> new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); } final Optional rateMeteringInterceptor() { return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ put(getGetDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 20 : 1, SECONDS)); put(getGetDisputesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 1, SECONDS)); put(getResolveDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 40 : 1, SECONDS)); put(getOpenDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 20 : 1, SECONDS)); put(getSendDisputeChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 40 : 2, SECONDS)); }} ))); } } ================================================ FILE: daemon/src/main/java/haveno/daemon/grpc/GrpcErrorMessageHandler.java ================================================ /* * 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 . */ package haveno.daemon.grpc; import haveno.common.handlers.ErrorMessageHandler; import haveno.proto.grpc.AvailabilityResultWithDescription; import haveno.proto.grpc.TakeOfferReply; import io.grpc.stub.StreamObserver; import lombok.Getter; import org.slf4j.Logger; import protobuf.AvailabilityResult; import static haveno.proto.grpc.TradesGrpc.getTakeOfferMethod; import static java.lang.String.format; import static java.util.Arrays.stream; /** * An implementation of haveno.common.handlers.ErrorMessageHandler that avoids * an exception loop with the UI's haveno.common.taskrunner framework. * * The legacy ErrorMessageHandler is for reporting error messages only to the UI, but * some core api tasks (takeoffer) require one. This implementation works around * the problem of Task ErrorMessageHandlers not throwing exceptions to the gRPC client. * * Extra care is needed because exceptions thrown by an ErrorMessageHandler inside * a Task may be thrown back to the GrpcService object, and if a gRPC ErrorMessageHandler * responded by throwing another exception, the loop may only stop after the gRPC * stream is closed. * * A unique instance should be used for a single gRPC call. */ public class GrpcErrorMessageHandler implements ErrorMessageHandler { @Getter private boolean isErrorHandled = false; private final String fullMethodName; private final StreamObserver responseObserver; private final GrpcExceptionHandler exceptionHandler; private final Logger log; public GrpcErrorMessageHandler(String fullMethodName, StreamObserver responseObserver, GrpcExceptionHandler exceptionHandler, Logger log) { this.fullMethodName = fullMethodName; this.exceptionHandler = exceptionHandler; this.responseObserver = responseObserver; this.log = log; } @Override public synchronized void handleErrorMessage(String errorMessage) { // A task runner may call handleErrorMessage(String) more than once. // Throw only one exception if that happens, to avoid looping until the // grpc stream is closed if (!isErrorHandled) { this.isErrorHandled = true; log.error(errorMessage); if (takeOfferWasCalled()) { handleTakeOfferError(errorMessage); } else { exceptionHandler.handleErrorMessage(log, errorMessage, responseObserver); } } } private void handleTakeOfferError(String errorMessage) { // If the errorMessage originated from a UI purposed TaskRunner, it should // contain an AvailabilityResult enum name. If it does, derive the // AvailabilityResult enum from the errorMessage, wrap it in a new // AvailabilityResultWithDescription enum, then send the // AvailabilityResultWithDescription to the client instead of throwing // an exception. The client should use the grpc reply object's // AvailabilityResultWithDescription field if reply.hasTrade = false, and the // client can decide to throw an exception with the client friendly error // description, or take some other action based on the AvailabilityResult enum. // (Some offer availability problems are not fatal, and retries are appropriate.) try { var failureReason = getAvailabilityResultWithDescription(errorMessage); var reply = TakeOfferReply.newBuilder() .setFailureReason(failureReason) .build(); @SuppressWarnings("unchecked") var takeOfferResponseObserver = (StreamObserver) responseObserver; takeOfferResponseObserver.onNext(reply); takeOfferResponseObserver.onCompleted(); } catch (IllegalArgumentException ex) { exceptionHandler.handleErrorMessage(log, errorMessage, responseObserver); } } private AvailabilityResultWithDescription getAvailabilityResultWithDescription(String errorMessage) { AvailabilityResult proto = getAvailabilityResult(errorMessage); String description = getAvailabilityResultDescription(proto); return AvailabilityResultWithDescription.newBuilder() .setAvailabilityResult(proto) .setDescription(description) .build(); } private AvailabilityResult getAvailabilityResult(String errorMessage) { return stream(AvailabilityResult.values()) .filter((e) -> errorMessage.toUpperCase().contains(e.name())) .findFirst().orElseThrow(() -> new IllegalArgumentException( format("Could not find an AvailabilityResult in error message:%n%s", errorMessage))); } private String getAvailabilityResultDescription(AvailabilityResult proto) { return haveno.core.offer.AvailabilityResult.fromProto(proto).description(); } private boolean takeOfferWasCalled() { return fullMethodName.equals(getTakeOfferMethod().getFullMethodName()); } } ================================================ FILE: daemon/src/main/java/haveno/daemon/grpc/GrpcExceptionHandler.java ================================================ /* * 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 . */ package haveno.daemon.grpc; import com.google.inject.Inject; import com.google.inject.Singleton; import io.grpc.Status; import static io.grpc.Status.INVALID_ARGUMENT; import static io.grpc.Status.UNKNOWN; import io.grpc.StatusRuntimeException; import io.grpc.stub.StreamObserver; import java.util.function.Predicate; import org.slf4j.Logger; /** * The singleton instance of this class handles any expected core api Throwable by * wrapping its message in a gRPC StatusRuntimeException and sending it to the client. * An unexpected Throwable's message will be replaced with an 'unexpected' error message. */ @Singleton class GrpcExceptionHandler { private final Predicate isExpectedException = (t) -> t instanceof IllegalStateException || t instanceof IllegalArgumentException || t instanceof RuntimeException; @Inject public GrpcExceptionHandler() { } public synchronized void handleException(Logger log, Throwable t, StreamObserver responseObserver) { // Log the core api error (this is last chance to do that), wrap it in a new // gRPC StatusRuntimeException, then send it to the client in the gRPC response. log.error("", t); var grpcStatusRuntimeException = wrapException(t); responseObserver.onError(grpcStatusRuntimeException); throw grpcStatusRuntimeException; } public synchronized void handleExceptionAsWarning(Logger log, String calledMethod, Throwable t, StreamObserver responseObserver) { // Just log a warning instead of an error with full stack trace. log.warn(calledMethod + " -> " + t.getMessage()); var grpcStatusRuntimeException = wrapException(t); responseObserver.onError(grpcStatusRuntimeException); throw grpcStatusRuntimeException; } public void handleErrorMessage(Logger log, String errorMessage, StreamObserver responseObserver) { // This is used to wrap Task errors from the ErrorMessageHandler // interface, an interface that is not allowed to throw exceptions. log.error(errorMessage); var grpcStatusRuntimeException = new StatusRuntimeException( UNKNOWN.withDescription(errorMessage)); responseObserver.onError(grpcStatusRuntimeException); throw grpcStatusRuntimeException; } private StatusRuntimeException wrapException(Throwable t) { // We want to be careful about what kinds of exception messages we send to the // client. Expected core exceptions should be wrapped in an IllegalStateException // or IllegalArgumentException, with a consistently styled and worded error // message. But only a small number of the expected error types are currently // handled this way; there is much work to do to handle the variety of errors // that can occur in the api. In the meantime, we take care to not pass full, // unexpected error messages to the client. If the exception type is unexpected, // we omit details from the gRPC exception sent to the client. if (isExpectedException.test(t)) { if (t.getCause() != null) return new StatusRuntimeException(mapGrpcErrorStatus(t.getCause(), t.getCause().getMessage())); else return new StatusRuntimeException(mapGrpcErrorStatus(t, t.getMessage())); } else { return new StatusRuntimeException(mapGrpcErrorStatus(t, "unexpected error on server")); } } private Status mapGrpcErrorStatus(Throwable t, String description) { // We default to the UNKNOWN status, except were the mapping of a core api // exception to a gRPC Status is obvious. If we ever use a gRPC reverse-proxy // to support RESTful clients, we will need to have more specific mappings // to support correct HTTP 1.1. status codes. //noinspection SwitchStatementWithTooFewBranches switch (t.getClass().getSimpleName()) { // We go ahead and use a switch statement instead of if, in anticipation // of more, specific exception mappings. case "IllegalArgumentException": return INVALID_ARGUMENT.withDescription(description); default: return UNKNOWN.withDescription(description); } } } ================================================ FILE: daemon/src/main/java/haveno/daemon/grpc/GrpcGetTradeStatisticsService.java ================================================ package haveno.daemon.grpc; import com.google.inject.Inject; import haveno.core.api.CoreApi; import haveno.core.trade.statistics.TradeStatistics3; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.daemon.grpc.interceptor.CallRateMeteringInterceptor; import haveno.daemon.grpc.interceptor.GrpcCallRateMeter; import static haveno.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; import static haveno.proto.grpc.GetTradeStatisticsGrpc.GetTradeStatisticsImplBase; import static haveno.proto.grpc.GetTradeStatisticsGrpc.getGetTradeStatisticsMethod; import haveno.proto.grpc.GetTradeStatisticsReply; import haveno.proto.grpc.GetTradeStatisticsRequest; import io.grpc.ServerInterceptor; import io.grpc.stub.StreamObserver; import java.util.HashMap; import java.util.List; import java.util.Optional; import static java.util.concurrent.TimeUnit.SECONDS; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @Slf4j class GrpcGetTradeStatisticsService extends GetTradeStatisticsImplBase { private final CoreApi coreApi; private final TradeStatisticsManager tradeStatisticsManager; private final GrpcExceptionHandler exceptionHandler; @Inject public GrpcGetTradeStatisticsService(CoreApi coreApi, TradeStatisticsManager tradeStatisticsManager, GrpcExceptionHandler exceptionHandler) { this.coreApi = coreApi; this.tradeStatisticsManager = tradeStatisticsManager; this.exceptionHandler = exceptionHandler; } @Override public void getTradeStatistics(GetTradeStatisticsRequest req, StreamObserver responseObserver) { try { List tradeStatistics; synchronized (tradeStatisticsManager.getObservableTradeStatisticsList()) { tradeStatistics = tradeStatisticsManager.getObservableTradeStatisticsList().stream() .map(TradeStatistics3::toProtoTradeStatistics3) .collect(Collectors.toList()); } var reply = GetTradeStatisticsReply.newBuilder().addAllTradeStatistics(tradeStatistics).build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } final ServerInterceptor[] interceptors() { Optional rateMeteringInterceptor = rateMeteringInterceptor(); return rateMeteringInterceptor.map(serverInterceptor -> new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); } final Optional rateMeteringInterceptor() { return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ put(getGetTradeStatisticsMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); }} ))); } } ================================================ FILE: daemon/src/main/java/haveno/daemon/grpc/GrpcHelpService.java ================================================ /* * 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 . */ package haveno.daemon.grpc; import com.google.inject.Inject; import haveno.core.api.CoreApi; import haveno.daemon.grpc.interceptor.CallRateMeteringInterceptor; import haveno.daemon.grpc.interceptor.GrpcCallRateMeter; import static haveno.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; import haveno.proto.grpc.GetMethodHelpReply; import haveno.proto.grpc.GetMethodHelpRequest; import static haveno.proto.grpc.HelpGrpc.HelpImplBase; import static haveno.proto.grpc.HelpGrpc.getGetMethodHelpMethod; import io.grpc.ServerInterceptor; import io.grpc.stub.StreamObserver; import java.util.HashMap; import java.util.Optional; import static java.util.concurrent.TimeUnit.SECONDS; import lombok.extern.slf4j.Slf4j; @Slf4j class GrpcHelpService extends HelpImplBase { private final CoreApi coreApi; private final GrpcExceptionHandler exceptionHandler; @Inject public GrpcHelpService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) { this.coreApi = coreApi; this.exceptionHandler = exceptionHandler; } @Override public void getMethodHelp(GetMethodHelpRequest req, StreamObserver responseObserver) { try { String helpText = coreApi.getMethodHelp(req.getMethodName()); var reply = GetMethodHelpReply.newBuilder().setMethodHelp(helpText).build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } final ServerInterceptor[] interceptors() { Optional rateMeteringInterceptor = rateMeteringInterceptor(); return rateMeteringInterceptor.map(serverInterceptor -> new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); } final Optional rateMeteringInterceptor() { return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ put(getGetMethodHelpMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); }} ))); } } ================================================ FILE: daemon/src/main/java/haveno/daemon/grpc/GrpcNotificationsService.java ================================================ package haveno.daemon.grpc; import com.google.inject.Inject; import haveno.core.api.CoreApi; import haveno.core.api.NotificationListener; import haveno.daemon.grpc.interceptor.CallRateMeteringInterceptor; import haveno.daemon.grpc.interceptor.GrpcCallRateMeter; import static haveno.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; import haveno.proto.grpc.NotificationMessage; import haveno.proto.grpc.NotificationsGrpc.NotificationsImplBase; import static haveno.proto.grpc.NotificationsGrpc.getRegisterNotificationListenerMethod; import static haveno.proto.grpc.NotificationsGrpc.getSendNotificationMethod; import haveno.proto.grpc.RegisterNotificationListenerRequest; import haveno.proto.grpc.SendNotificationReply; import haveno.proto.grpc.SendNotificationRequest; import io.grpc.Context; import io.grpc.ServerInterceptor; import io.grpc.stub.ServerCallStreamObserver; import io.grpc.stub.StreamObserver; import java.util.HashMap; import java.util.Optional; import static java.util.concurrent.TimeUnit.SECONDS; import lombok.NonNull; import lombok.Value; import lombok.extern.slf4j.Slf4j; @Slf4j class GrpcNotificationsService extends NotificationsImplBase { private final CoreApi coreApi; private final GrpcExceptionHandler exceptionHandler; @Inject public GrpcNotificationsService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) { this.coreApi = coreApi; this.exceptionHandler = exceptionHandler; } @Override public void registerNotificationListener(RegisterNotificationListenerRequest request, StreamObserver responseObserver) { Context ctx = Context.current().fork(); // context is independent for long-lived request ctx.run(() -> { try { coreApi.addNotificationListener(new GrpcNotificationListener(responseObserver)); // No onNext / onCompleted, as the response observer should be kept open } catch (Throwable t) { exceptionHandler.handleException(log, t, responseObserver); } }); } @Override public void sendNotification(SendNotificationRequest request, StreamObserver responseObserver) { Context ctx = Context.current().fork(); // context is independent from notification delivery ctx.run(() -> { try { coreApi.sendNotification(request.getNotification()); responseObserver.onNext(SendNotificationReply.newBuilder().build()); responseObserver.onCompleted(); } catch (Throwable t) { exceptionHandler.handleException(log, t, responseObserver); } }); } @Value private static class GrpcNotificationListener implements NotificationListener { @NonNull StreamObserver responseObserver; @Override public void onMessage(@NonNull NotificationMessage message) { if (!((ServerCallStreamObserver) responseObserver).isCancelled()) { responseObserver.onNext(message); } } } final ServerInterceptor[] interceptors() { Optional rateMeteringInterceptor = rateMeteringInterceptor(); return rateMeteringInterceptor.map(serverInterceptor -> new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); } final Optional rateMeteringInterceptor() { return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ put(getRegisterNotificationListenerMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getSendNotificationMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); }} ))); } } ================================================ FILE: daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java ================================================ /* * 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 . */ package haveno.daemon.grpc; import com.google.inject.Inject; import haveno.common.config.Config; import haveno.core.api.CoreApi; import haveno.core.api.model.OfferInfo; import haveno.core.offer.Offer; import haveno.core.offer.OpenOffer; import haveno.daemon.grpc.interceptor.CallRateMeteringInterceptor; import haveno.daemon.grpc.interceptor.GrpcCallRateMeter; import static haveno.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; import haveno.proto.grpc.ActivateOfferReply; import haveno.proto.grpc.ActivateOfferRequest; import haveno.proto.grpc.CancelOfferReply; import haveno.proto.grpc.CancelOfferRequest; import haveno.proto.grpc.DeactivateOfferReply; import haveno.proto.grpc.DeactivateOfferRequest; import haveno.proto.grpc.EditOfferReply; import haveno.proto.grpc.EditOfferRequest; import haveno.proto.grpc.GetMyOfferReply; import haveno.proto.grpc.GetMyOfferRequest; import haveno.proto.grpc.GetMyOffersReply; import haveno.proto.grpc.GetMyOffersRequest; import haveno.proto.grpc.GetOfferReply; import haveno.proto.grpc.GetOfferRequest; import haveno.proto.grpc.GetOffersReply; import haveno.proto.grpc.GetOffersRequest; import static haveno.proto.grpc.OffersGrpc.OffersImplBase; import static haveno.proto.grpc.OffersGrpc.getCancelOfferMethod; import static haveno.proto.grpc.OffersGrpc.getEditOfferMethod; import static haveno.proto.grpc.OffersGrpc.getDeactivateOfferMethod; import static haveno.proto.grpc.OffersGrpc.getActivateOfferMethod; import static haveno.proto.grpc.OffersGrpc.getGetMyOfferMethod; import static haveno.proto.grpc.OffersGrpc.getGetMyOffersMethod; import static haveno.proto.grpc.OffersGrpc.getGetOfferMethod; import static haveno.proto.grpc.OffersGrpc.getGetOffersMethod; import static haveno.proto.grpc.OffersGrpc.getPostOfferMethod; import haveno.proto.grpc.PostOfferReply; import haveno.proto.grpc.PostOfferRequest; import io.grpc.ServerInterceptor; import io.grpc.stub.StreamObserver; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Optional; import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @Slf4j class GrpcOffersService extends OffersImplBase { private final CoreApi coreApi; private final GrpcExceptionHandler exceptionHandler; @Inject public GrpcOffersService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) { this.coreApi = coreApi; this.exceptionHandler = exceptionHandler; } @Override public void getOffer(GetOfferRequest req, StreamObserver responseObserver) { try { Offer offer = coreApi.getOffer(req.getId()); var reply = GetOfferReply.newBuilder() .setOffer(OfferInfo.toOfferInfo(offer).toProtoMessage()) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void getMyOffer(GetMyOfferRequest req, StreamObserver responseObserver) { try { OpenOffer openOffer = coreApi.getMyOffer(req.getId()); var reply = GetMyOfferReply.newBuilder() .setOffer(OfferInfo.toMyOfferInfo(openOffer).toProtoMessage()) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void getOffers(GetOffersRequest req, StreamObserver responseObserver) { try { List result = coreApi.getOffers(req.getDirection(), req.getCurrencyCode()) .stream().map(OfferInfo::toOfferInfo) .collect(Collectors.toList()); var reply = GetOffersReply.newBuilder() .addAllOffers(result.stream() .map(OfferInfo::toProtoMessage) .collect(Collectors.toList())) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void getMyOffers(GetMyOffersRequest req, StreamObserver responseObserver) { try { List result = new ArrayList(); for (OpenOffer offer : coreApi.getMyOffers(req.getDirection(), req.getCurrencyCode())) { result.add(OfferInfo.toMyOfferInfo(offer)); } var reply = GetMyOffersReply.newBuilder() .addAllOffers(result.stream() .map(OfferInfo::toProtoMessage) .collect(Collectors.toList())) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void postOffer(PostOfferRequest req, StreamObserver responseObserver) { GrpcErrorMessageHandler errorMessageHandler = new GrpcErrorMessageHandler(getPostOfferMethod().getFullMethodName(), responseObserver, exceptionHandler, log); try { coreApi.postOffer( req.getCurrencyCode(), req.getDirection(), req.getPrice(), req.getUseMarketBasedPrice(), req.getMarketPriceMarginPct(), req.getAmount(), req.getMinAmount(), req.getSecurityDepositPct(), req.getTriggerPrice(), req.getReserveExactAmount(), req.getPaymentAccountId(), req.getIsPrivateOffer(), req.getBuyerAsTakerWithoutDeposit(), req.getExtraInfo(), req.getSourceOfferId(), offer -> { // This result handling consumer's accept operation will return // the new offer to the gRPC client after async placement is done. OpenOffer openOffer = coreApi.getMyOffer(offer.getId()); OfferInfo offerInfo = OfferInfo.toMyOfferInfo(openOffer); PostOfferReply reply = PostOfferReply.newBuilder() .setOffer(offerInfo.toProtoMessage()) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); }, errorMessage -> { if (!errorMessageHandler.isErrorHandled()) errorMessageHandler.handleErrorMessage(errorMessage); }); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void editOffer(EditOfferRequest req, StreamObserver responseObserver) { GrpcErrorMessageHandler errorMessageHandler = new GrpcErrorMessageHandler(getEditOfferMethod().getFullMethodName(), responseObserver, exceptionHandler, log); try { coreApi.editOffer( req.getOfferId(), req.getCurrencyCode(), req.getPrice(), req.getUseMarketBasedPrice(), req.getMarketPriceMarginPct(), req.getTriggerPrice(), req.getPaymentAccountId(), req.getExtraInfo(), (offer) -> { OpenOffer openOffer = coreApi.getMyOffer(offer.getId()); OfferInfo offerInfo = OfferInfo.toMyOfferInfo(openOffer); EditOfferReply reply = EditOfferReply.newBuilder() .setOffer(offerInfo.toProtoMessage()) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); }, errorMessage -> { if (!errorMessageHandler.isErrorHandled()) errorMessageHandler.handleErrorMessage(errorMessage); }); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void deactivateOffer(DeactivateOfferRequest req, StreamObserver responseObserver) { GrpcErrorMessageHandler errorMessageHandler = new GrpcErrorMessageHandler(getDeactivateOfferMethod().getFullMethodName(), responseObserver, exceptionHandler, log); try { coreApi.deactivateOffer(req.getOfferId(), () -> { var reply = DeactivateOfferReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); }, errorMessage -> { if (!errorMessageHandler.isErrorHandled()) errorMessageHandler.handleErrorMessage(errorMessage); }); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void activateOffer(ActivateOfferRequest req, StreamObserver responseObserver) { GrpcErrorMessageHandler errorMessageHandler = new GrpcErrorMessageHandler(getActivateOfferMethod().getFullMethodName(), responseObserver, exceptionHandler, log); try { coreApi.activateOffer(req.getOfferId(), () -> { var reply = ActivateOfferReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); }, errorMessage -> { if (!errorMessageHandler.isErrorHandled()) errorMessageHandler.handleErrorMessage(errorMessage); }); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void cancelOffer(CancelOfferRequest req, StreamObserver responseObserver) { GrpcErrorMessageHandler errorMessageHandler = new GrpcErrorMessageHandler(getCancelOfferMethod().getFullMethodName(), responseObserver, exceptionHandler, log); try { coreApi.cancelOffer(req.getId(), () -> { var reply = CancelOfferReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); }, errorMessage -> { if (!errorMessageHandler.isErrorHandled()) errorMessageHandler.handleErrorMessage(errorMessage); }); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } final ServerInterceptor[] interceptors() { Optional rateMeteringInterceptor = rateMeteringInterceptor(); return rateMeteringInterceptor.map(serverInterceptor -> new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); } final Optional rateMeteringInterceptor() { return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ put(getGetOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, SECONDS)); put(getGetMyOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, SECONDS)); put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, SECONDS)); put(getGetMyOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); put(getPostOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); put(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); }} ))); } } ================================================ FILE: daemon/src/main/java/haveno/daemon/grpc/GrpcPaymentAccountsService.java ================================================ /* * 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 . */ package haveno.daemon.grpc; import com.google.inject.Inject; import haveno.common.config.Config; import haveno.core.api.CoreApi; import haveno.core.api.model.PaymentAccountForm; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccountFactory; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.proto.CoreProtoResolver; import haveno.daemon.grpc.interceptor.CallRateMeteringInterceptor; import haveno.daemon.grpc.interceptor.GrpcCallRateMeter; import static haveno.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; import haveno.proto.grpc.CreateCryptoCurrencyPaymentAccountReply; import haveno.proto.grpc.CreateCryptoCurrencyPaymentAccountRequest; import haveno.proto.grpc.CreatePaymentAccountReply; import haveno.proto.grpc.CreatePaymentAccountRequest; import haveno.proto.grpc.DeletePaymentAccountReply; import haveno.proto.grpc.DeletePaymentAccountRequest; import haveno.proto.grpc.GetCryptoCurrencyPaymentMethodsReply; import haveno.proto.grpc.GetCryptoCurrencyPaymentMethodsRequest; import haveno.proto.grpc.GetPaymentAccountFormReply; import haveno.proto.grpc.GetPaymentAccountFormRequest; import haveno.proto.grpc.GetPaymentAccountsReply; import haveno.proto.grpc.GetPaymentAccountsRequest; import haveno.proto.grpc.GetPaymentMethodsReply; import haveno.proto.grpc.GetPaymentMethodsRequest; import haveno.proto.grpc.PaymentAccountsGrpc.PaymentAccountsImplBase; import static haveno.proto.grpc.PaymentAccountsGrpc.getCreateCryptoCurrencyPaymentAccountMethod; import static haveno.proto.grpc.PaymentAccountsGrpc.getCreatePaymentAccountMethod; import static haveno.proto.grpc.PaymentAccountsGrpc.getGetPaymentAccountFormMethod; import static haveno.proto.grpc.PaymentAccountsGrpc.getGetPaymentAccountsMethod; import static haveno.proto.grpc.PaymentAccountsGrpc.getGetPaymentMethodsMethod; import haveno.proto.grpc.ValidateFormFieldReply; import haveno.proto.grpc.ValidateFormFieldRequest; import io.grpc.ServerInterceptor; import io.grpc.stub.StreamObserver; import java.util.HashMap; import java.util.Optional; import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @Slf4j class GrpcPaymentAccountsService extends PaymentAccountsImplBase { private final CoreApi coreApi; private final GrpcExceptionHandler exceptionHandler; @Inject public GrpcPaymentAccountsService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) { this.coreApi = coreApi; this.exceptionHandler = exceptionHandler; } @Override public void createPaymentAccount(CreatePaymentAccountRequest req, StreamObserver responseObserver) { try { PaymentAccount paymentAccount = coreApi.createPaymentAccount(PaymentAccountForm.fromProto(req.getPaymentAccountForm())); var reply = CreatePaymentAccountReply.newBuilder() .setPaymentAccount(paymentAccount.toProtoMessage()) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void getPaymentAccounts(GetPaymentAccountsRequest req, StreamObserver responseObserver) { try { var paymentAccounts = coreApi.getPaymentAccounts().stream() .map(PaymentAccount::toProtoMessage) .collect(Collectors.toList()); var reply = GetPaymentAccountsReply.newBuilder() .addAllPaymentAccounts(paymentAccounts).build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void getPaymentMethods(GetPaymentMethodsRequest req, StreamObserver responseObserver) { try { var paymentMethods = coreApi.getPaymentMethods().stream() .map(PaymentMethod::toProtoMessage) .collect(Collectors.toList()); var reply = GetPaymentMethodsReply.newBuilder() .addAllPaymentMethods(paymentMethods).build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void getPaymentAccountForm(GetPaymentAccountFormRequest req, StreamObserver responseObserver) { try { PaymentAccountForm form = null; if (req.getPaymentMethodId().isEmpty()) { PaymentAccount account = PaymentAccountFactory.getPaymentAccount(PaymentMethod.getPaymentMethod(req.getPaymentAccountPayload().getPaymentMethodId())); account.setAccountName("tmp"); account.init(PaymentAccountPayload.fromProto(req.getPaymentAccountPayload(), new CoreProtoResolver())); account.setAccountName(null); form = coreApi.getPaymentAccountForm(account); } else { form = coreApi.getPaymentAccountForm(req.getPaymentMethodId()); } var reply = GetPaymentAccountFormReply.newBuilder() .setPaymentAccountForm(form.toProtoMessage()) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void createCryptoCurrencyPaymentAccount(CreateCryptoCurrencyPaymentAccountRequest req, StreamObserver responseObserver) { try { PaymentAccount paymentAccount = coreApi.createCryptoCurrencyPaymentAccount(req.getAccountName(), req.getCurrencyCode(), req.getAddress(), req.getTradeInstant()); var reply = CreateCryptoCurrencyPaymentAccountReply.newBuilder() .setPaymentAccount(paymentAccount.toProtoMessage()) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void deletePaymentAccount(DeletePaymentAccountRequest req, StreamObserver responseObserver) { try { coreApi.deletePaymentAccount(req.getPaymentAccountId()); var reply = DeletePaymentAccountReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void getCryptoCurrencyPaymentMethods(GetCryptoCurrencyPaymentMethodsRequest req, StreamObserver responseObserver) { try { var paymentMethods = coreApi.getCryptoCurrencyPaymentMethods().stream() .map(PaymentMethod::toProtoMessage) .collect(Collectors.toList()); var reply = GetCryptoCurrencyPaymentMethodsReply.newBuilder() .addAllPaymentMethods(paymentMethods).build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void validateFormField(ValidateFormFieldRequest req, StreamObserver responseObserver) { try { coreApi.validateFormField(PaymentAccountForm.fromProto(req.getForm()), PaymentAccountFormField.FieldId.fromProto(req.getFieldId()), req.getValue()); var reply = ValidateFormFieldReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } final ServerInterceptor[] interceptors() { Optional rateMeteringInterceptor = rateMeteringInterceptor(); return rateMeteringInterceptor.map(serverInterceptor -> new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); } final Optional rateMeteringInterceptor() { return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ put(getCreatePaymentAccountMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 100 : 1, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); put(getCreateCryptoCurrencyPaymentAccountMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 100 : 1, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); put(getGetPaymentAccountsMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 100 : 1, SECONDS)); put(getGetPaymentMethodsMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 100 : 1, SECONDS)); put(getGetPaymentAccountFormMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 100 : 1, SECONDS)); }} ))); } } ================================================ FILE: daemon/src/main/java/haveno/daemon/grpc/GrpcPriceService.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.daemon.grpc; import haveno.common.config.Config; import com.google.inject.Inject; import haveno.core.api.CoreApi; import haveno.core.api.model.MarketDepthInfo; import haveno.core.api.model.MarketPriceInfo; import haveno.daemon.grpc.interceptor.CallRateMeteringInterceptor; import haveno.daemon.grpc.interceptor.GrpcCallRateMeter; import static haveno.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; import haveno.proto.grpc.MarketDepthReply; import haveno.proto.grpc.MarketDepthRequest; import haveno.proto.grpc.MarketPriceReply; import haveno.proto.grpc.MarketPriceRequest; import haveno.proto.grpc.MarketPricesReply; import haveno.proto.grpc.MarketPricesRequest; import static haveno.proto.grpc.PriceGrpc.getGetMarketPriceMethod; import haveno.proto.grpc.PriceGrpc.PriceImplBase; import io.grpc.ServerInterceptor; import io.grpc.stub.StreamObserver; import java.util.HashMap; import java.util.List; import java.util.Optional; import static java.util.concurrent.TimeUnit.SECONDS; import lombok.extern.slf4j.Slf4j; @Slf4j class GrpcPriceService extends PriceImplBase { private final CoreApi coreApi; private final GrpcExceptionHandler exceptionHandler; @Inject public GrpcPriceService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) { this.coreApi = coreApi; this.exceptionHandler = exceptionHandler; } @Override public void getMarketPrice(MarketPriceRequest req, StreamObserver responseObserver) { try { double marketPrice = coreApi.getMarketPrice(req.getCurrencyCode()); responseObserver.onNext(MarketPriceReply.newBuilder().setPrice(marketPrice).build()); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void getMarketPrices(MarketPricesRequest request, StreamObserver responseObserver) { try { responseObserver.onNext(mapMarketPricesReply(coreApi.getMarketPrices())); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void getMarketDepth(MarketDepthRequest req, StreamObserver responseObserver) { try { responseObserver.onNext(mapMarketDepthReply(coreApi.getMarketDepth(req.getCurrencyCode()))); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } private MarketPricesReply mapMarketPricesReply(List marketPrices) { MarketPricesReply.Builder builder = MarketPricesReply.newBuilder(); marketPrices.stream() .map(MarketPriceInfo::toProtoMessage) .forEach(builder::addMarketPrice); return builder.build(); } private MarketDepthReply mapMarketDepthReply(MarketDepthInfo marketDepth) { return MarketDepthReply.newBuilder().setMarketDepth(marketDepth.toProtoMessage()).build(); } final ServerInterceptor[] interceptors() { Optional rateMeteringInterceptor = rateMeteringInterceptor(); return rateMeteringInterceptor.map(serverInterceptor -> new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); } final Optional rateMeteringInterceptor() { return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ put(getGetMarketPriceMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 20 : 1, SECONDS)); }} ))); } } ================================================ FILE: daemon/src/main/java/haveno/daemon/grpc/GrpcServer.java ================================================ /* * 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 . */ package haveno.daemon.grpc; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.config.Config; import haveno.core.api.CoreContext; import haveno.daemon.grpc.interceptor.PasswordAuthInterceptor; import static io.grpc.ServerInterceptors.interceptForward; import java.io.IOException; import java.io.UncheckedIOException; import io.grpc.Metadata; import io.grpc.ServerCall; import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; import io.grpc.Server; import io.grpc.ServerBuilder; import lombok.extern.slf4j.Slf4j; @Singleton @Slf4j public class GrpcServer { private final Server server; @Inject public GrpcServer(CoreContext coreContext, Config config, PasswordAuthInterceptor passwordAuthInterceptor, GrpcAccountService accountService, GrpcDisputeAgentsService disputeAgentsService, GrpcDisputesService disputesService, GrpcHelpService helpService, GrpcOffersService offersService, GrpcPaymentAccountsService paymentAccountsService, GrpcPriceService priceService, GrpcShutdownService shutdownService, GrpcVersionService versionService, GrpcGetTradeStatisticsService tradeStatisticsService, GrpcTradesService tradesService, GrpcWalletsService walletsService, GrpcNotificationsService notificationsService, GrpcXmrConnectionService moneroConnectionsService, GrpcXmrNodeService moneroNodeService) { this.server = ServerBuilder.forPort(config.apiPort) .addService(shutdownService) .intercept(passwordAuthInterceptor) .addService(interceptForward(accountService, config.disableRateLimits ? interceptors() : accountService.interceptors())) .addService(interceptForward(disputeAgentsService, config.disableRateLimits ? interceptors() : disputeAgentsService.interceptors())) .addService(interceptForward(disputesService, config.disableRateLimits ? interceptors() : disputesService.interceptors())) .addService(interceptForward(helpService, config.disableRateLimits ? interceptors() : helpService.interceptors())) .addService(interceptForward(offersService, config.disableRateLimits ? interceptors() : offersService.interceptors())) .addService(interceptForward(paymentAccountsService, config.disableRateLimits ? interceptors() : paymentAccountsService.interceptors())) .addService(interceptForward(priceService, config.disableRateLimits ? interceptors() : priceService.interceptors())) .addService(interceptForward(tradeStatisticsService, config.disableRateLimits ? interceptors() : tradeStatisticsService.interceptors())) .addService(interceptForward(tradesService, config.disableRateLimits ? interceptors() : tradesService.interceptors())) .addService(interceptForward(versionService, config.disableRateLimits ? interceptors() : versionService.interceptors())) .addService(interceptForward(walletsService, config.disableRateLimits ? interceptors() : walletsService.interceptors())) .addService(interceptForward(notificationsService, config.disableRateLimits ? interceptors() : notificationsService.interceptors())) .addService(interceptForward(moneroConnectionsService, config.disableRateLimits ? interceptors() : moneroConnectionsService.interceptors())) .addService(interceptForward(moneroNodeService, config.disableRateLimits ? interceptors() : moneroNodeService.interceptors())) .build(); coreContext.setApiUser(true); } private ServerInterceptor[] interceptors() { return new ServerInterceptor[]{callLoggingInterceptor()}; } private ServerInterceptor callLoggingInterceptor() { return new ServerInterceptor() { @Override public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { log.debug("GRPC endpoint called: " + call.getMethodDescriptor().getFullMethodName()); return next.startCall(call, headers); } }; } public void start() { try { server.start(); log.info("listening on port {}", server.getPort()); } catch (IOException ex) { throw new UncheckedIOException(ex); } } public void shutdown() { log.info("Server shutdown started"); server.shutdown(); log.info("Server shutdown complete"); } } ================================================ FILE: daemon/src/main/java/haveno/daemon/grpc/GrpcShutdownService.java ================================================ /* * 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 . */ package haveno.daemon.grpc; import com.google.inject.Inject; import haveno.common.UserThread; import haveno.core.app.HavenoHeadlessApp; import haveno.proto.grpc.ShutdownServerGrpc; import haveno.proto.grpc.StopReply; import haveno.proto.grpc.StopRequest; import io.grpc.stub.StreamObserver; import static java.util.concurrent.TimeUnit.MILLISECONDS; import lombok.extern.slf4j.Slf4j; @Slf4j class GrpcShutdownService extends ShutdownServerGrpc.ShutdownServerImplBase { private final GrpcExceptionHandler exceptionHandler; @Inject public GrpcShutdownService(GrpcExceptionHandler exceptionHandler) { this.exceptionHandler = exceptionHandler; } @Override public void stop(StopRequest req, StreamObserver responseObserver) { try { log.info("Shutdown request received."); HavenoHeadlessApp.setOnGracefulShutDownHandler(new Runnable() { @Override public void run() { var reply = StopReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } }); UserThread.runAfter(HavenoHeadlessApp.getShutDownHandler(), 500, MILLISECONDS); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } } ================================================ FILE: daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.daemon.grpc; import com.google.inject.Inject; import haveno.common.config.Config; import haveno.core.api.CoreApi; import haveno.core.api.model.TradeInfo; import static haveno.core.api.model.TradeInfo.toTradeInfo; import haveno.core.trade.Trade; import haveno.daemon.grpc.interceptor.CallRateMeteringInterceptor; import haveno.daemon.grpc.interceptor.GrpcCallRateMeter; import static haveno.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; import haveno.proto.grpc.CompleteTradeReply; import haveno.proto.grpc.CompleteTradeRequest; import haveno.proto.grpc.ConfirmPaymentReceivedReply; import haveno.proto.grpc.ConfirmPaymentReceivedRequest; import haveno.proto.grpc.ConfirmPaymentSentReply; import haveno.proto.grpc.ConfirmPaymentSentRequest; import haveno.proto.grpc.GetChatMessagesReply; import haveno.proto.grpc.GetChatMessagesRequest; import haveno.proto.grpc.GetTradeReply; import haveno.proto.grpc.GetTradeRequest; import haveno.proto.grpc.GetTradesReply; import haveno.proto.grpc.GetTradesRequest; import haveno.proto.grpc.SendChatMessageReply; import haveno.proto.grpc.SendChatMessageRequest; import haveno.proto.grpc.TakeOfferReply; import haveno.proto.grpc.TakeOfferRequest; import haveno.proto.grpc.TradesGrpc.TradesImplBase; import static haveno.proto.grpc.TradesGrpc.getCompleteTradeMethod; import static haveno.proto.grpc.TradesGrpc.getConfirmPaymentReceivedMethod; import static haveno.proto.grpc.TradesGrpc.getConfirmPaymentSentMethod; import static haveno.proto.grpc.TradesGrpc.getGetChatMessagesMethod; import static haveno.proto.grpc.TradesGrpc.getGetTradeMethod; import static haveno.proto.grpc.TradesGrpc.getGetTradesMethod; import static haveno.proto.grpc.TradesGrpc.getSendChatMessageMethod; import static haveno.proto.grpc.TradesGrpc.getTakeOfferMethod; import static haveno.proto.grpc.TradesGrpc.getWithdrawFundsMethod; import io.grpc.ServerInterceptor; import io.grpc.stub.StreamObserver; import java.util.HashMap; import java.util.List; import java.util.Optional; import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @Slf4j class GrpcTradesService extends TradesImplBase { private final CoreApi coreApi; private final GrpcExceptionHandler exceptionHandler; @Inject public GrpcTradesService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) { this.coreApi = coreApi; this.exceptionHandler = exceptionHandler; } @Override public void getTrade(GetTradeRequest req, StreamObserver responseObserver) { try { Trade trade = coreApi.getTrade(req.getTradeId()); var reply = GetTradeReply.newBuilder() .setTrade(toTradeInfo(trade).toProtoMessage()) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (IllegalArgumentException cause) { // Offer makers may call 'gettrade' many times before a trade exists. // Log a 'trade not found' warning instead of a full stack trace. cause.printStackTrace(); exceptionHandler.handleExceptionAsWarning(log, "getTrade", cause, responseObserver); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void getTrades(GetTradesRequest req, StreamObserver responseObserver) { try { List trades = coreApi.getTrades() .stream().map(TradeInfo::toTradeInfo) .collect(Collectors.toList()); var reply = GetTradesReply.newBuilder() .addAllTrades(trades.stream() .map(TradeInfo::toProtoMessage) .collect(Collectors.toList())) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void takeOffer(TakeOfferRequest req, StreamObserver responseObserver) { GrpcErrorMessageHandler errorMessageHandler = new GrpcErrorMessageHandler(getTakeOfferMethod().getFullMethodName(), responseObserver, exceptionHandler, log); try { coreApi.takeOffer(req.getOfferId(), req.getPaymentAccountId(), req.getAmount(), req.getChallenge(), trade -> { TradeInfo tradeInfo = toTradeInfo(trade); var reply = TakeOfferReply.newBuilder() .setTrade(tradeInfo.toProtoMessage()) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); }, errorMessage -> { if (!errorMessageHandler.isErrorHandled()) errorMessageHandler.handleErrorMessage(errorMessage); }); } catch (Throwable cause) { cause.printStackTrace(); exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void confirmPaymentSent(ConfirmPaymentSentRequest req, StreamObserver responseObserver) { GrpcErrorMessageHandler errorMessageHandler = new GrpcErrorMessageHandler(getConfirmPaymentSentMethod().getFullMethodName(), responseObserver, exceptionHandler, log); try { coreApi.confirmPaymentSent(req.getTradeId(), () -> { var reply = ConfirmPaymentSentReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); }, errorMessage -> { if (!errorMessageHandler.isErrorHandled()) errorMessageHandler.handleErrorMessage(errorMessage); }); } catch (Throwable cause) { cause.printStackTrace(); exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void confirmPaymentReceived(ConfirmPaymentReceivedRequest req, StreamObserver responseObserver) { GrpcErrorMessageHandler errorMessageHandler = new GrpcErrorMessageHandler(getConfirmPaymentReceivedMethod().getFullMethodName(), responseObserver, exceptionHandler, log); try { coreApi.confirmPaymentReceived(req.getTradeId(), () -> { var reply = ConfirmPaymentReceivedReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); }, errorMessage -> { if (!errorMessageHandler.isErrorHandled()) errorMessageHandler.handleErrorMessage(errorMessage); }); } catch (Throwable cause) { cause.printStackTrace(); exceptionHandler.handleException(log, cause, responseObserver); } } // TODO: rename CompleteTradeRequest to CloseTradeRequest @Override public void completeTrade(CompleteTradeRequest req, StreamObserver responseObserver) { try { coreApi.closeTrade(req.getTradeId()); var reply = CompleteTradeReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void getChatMessages(GetChatMessagesRequest req, StreamObserver responseObserver) { try { var tradeChats = coreApi.getChatMessages(req.getTradeId()) .stream() .map(msg -> msg.toProtoNetworkEnvelope().getChatMessage()) .collect(Collectors.toList()); var reply = GetChatMessagesReply.newBuilder() .addAllMessage(tradeChats) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void sendChatMessage(SendChatMessageRequest req, StreamObserver responseObserver) { try { coreApi.sendChatMessage(req.getTradeId(), req.getMessage()); var reply = SendChatMessageReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } final ServerInterceptor[] interceptors() { Optional rateMeteringInterceptor = rateMeteringInterceptor(); return rateMeteringInterceptor.map(serverInterceptor -> new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); } final Optional rateMeteringInterceptor() { return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 1, SECONDS)); put(getGetTradesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 1, SECONDS)); put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); put(getConfirmPaymentSentMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); put(getCompleteTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); put(getWithdrawFundsMethod().getFullMethodName(), new GrpcCallRateMeter(3, MINUTES)); put(getGetChatMessagesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); put(getSendChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); }} ))); } } ================================================ FILE: daemon/src/main/java/haveno/daemon/grpc/GrpcVersionService.java ================================================ /* * 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 . */ package haveno.daemon.grpc; import com.google.common.annotations.VisibleForTesting; import haveno.common.config.Config; import com.google.inject.Inject; import haveno.core.api.CoreApi; import haveno.daemon.grpc.interceptor.CallRateMeteringInterceptor; import haveno.daemon.grpc.interceptor.GrpcCallRateMeter; import static haveno.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; import static haveno.proto.grpc.GetVersionGrpc.GetVersionImplBase; import static haveno.proto.grpc.GetVersionGrpc.getGetVersionMethod; import haveno.proto.grpc.GetVersionGrpc.GetVersionImplBase; import haveno.proto.grpc.GetVersionReply; import haveno.proto.grpc.GetVersionRequest; import io.grpc.ServerInterceptor; import io.grpc.stub.StreamObserver; import java.util.HashMap; import java.util.Optional; import static haveno.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; import static haveno.proto.grpc.GetVersionGrpc.getGetVersionMethod; import static java.util.concurrent.TimeUnit.SECONDS; import lombok.extern.slf4j.Slf4j; @VisibleForTesting @Slf4j public class GrpcVersionService extends GetVersionImplBase { private final CoreApi coreApi; private final GrpcExceptionHandler exceptionHandler; @Inject public GrpcVersionService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) { this.coreApi = coreApi; this.exceptionHandler = exceptionHandler; } @Override public void getVersion(GetVersionRequest req, StreamObserver responseObserver) { try { var reply = GetVersionReply.newBuilder().setVersion(coreApi.getVersion()).build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } final ServerInterceptor[] interceptors() { Optional rateMeteringInterceptor = rateMeteringInterceptor(); return rateMeteringInterceptor.map(serverInterceptor -> new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); } final Optional rateMeteringInterceptor() { return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ put(getGetVersionMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 5 : 1, SECONDS)); }} ))); } } ================================================ FILE: daemon/src/main/java/haveno/daemon/grpc/GrpcWalletsService.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.daemon.grpc; import com.google.inject.Inject; import haveno.common.UserThread; import haveno.common.config.Config; import haveno.core.api.CoreApi; import haveno.core.api.model.AddressBalanceInfo; import static haveno.core.api.model.XmrTx.toXmrTx; import haveno.daemon.grpc.interceptor.CallRateMeteringInterceptor; import haveno.daemon.grpc.interceptor.GrpcCallRateMeter; import static haveno.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; import haveno.proto.grpc.CreateXmrSweepTxsReply; import haveno.proto.grpc.CreateXmrSweepTxsRequest; import haveno.proto.grpc.CreateXmrTxReply; import haveno.proto.grpc.CreateXmrTxRequest; import haveno.proto.grpc.GetAddressBalanceReply; import haveno.proto.grpc.GetAddressBalanceRequest; import haveno.proto.grpc.GetBalancesReply; import haveno.proto.grpc.GetBalancesRequest; import haveno.proto.grpc.GetFundingAddressesReply; import haveno.proto.grpc.GetFundingAddressesRequest; import haveno.proto.grpc.GetXmrNewSubaddressReply; import haveno.proto.grpc.GetXmrNewSubaddressRequest; import haveno.proto.grpc.GetXmrPrimaryAddressReply; import haveno.proto.grpc.GetXmrPrimaryAddressRequest; import haveno.proto.grpc.GetXmrSeedReply; import haveno.proto.grpc.GetXmrSeedRequest; import haveno.proto.grpc.GetXmrTxsReply; import haveno.proto.grpc.GetXmrTxsRequest; import haveno.proto.grpc.LockWalletReply; import haveno.proto.grpc.LockWalletRequest; import haveno.proto.grpc.RelayXmrTxsReply; import haveno.proto.grpc.RelayXmrTxsRequest; import haveno.proto.grpc.RemoveWalletPasswordReply; import haveno.proto.grpc.RemoveWalletPasswordRequest; import haveno.proto.grpc.SetWalletPasswordReply; import haveno.proto.grpc.SetWalletPasswordRequest; import haveno.proto.grpc.UnlockWalletReply; import haveno.proto.grpc.UnlockWalletRequest; import haveno.proto.grpc.GetWalletHeightRequest; import haveno.proto.grpc.GetWalletHeightReply; import haveno.proto.grpc.WalletsGrpc.WalletsImplBase; import static haveno.proto.grpc.WalletsGrpc.getGetAddressBalanceMethod; import static haveno.proto.grpc.WalletsGrpc.getGetBalancesMethod; import static haveno.proto.grpc.WalletsGrpc.getGetFundingAddressesMethod; import static haveno.proto.grpc.WalletsGrpc.getLockWalletMethod; import static haveno.proto.grpc.WalletsGrpc.getRemoveWalletPasswordMethod; import static haveno.proto.grpc.WalletsGrpc.getSetWalletPasswordMethod; import static haveno.proto.grpc.WalletsGrpc.getUnlockWalletMethod; import io.grpc.ServerInterceptor; import io.grpc.stub.StreamObserver; import java.math.BigInteger; import java.util.HashMap; import java.util.List; import java.util.Optional; import static java.util.concurrent.TimeUnit.SECONDS; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import monero.wallet.model.MoneroDestination; import monero.wallet.model.MoneroTxWallet; @Slf4j class GrpcWalletsService extends WalletsImplBase { private final CoreApi coreApi; private final GrpcExceptionHandler exceptionHandler; @Inject public GrpcWalletsService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) { this.coreApi = coreApi; this.exceptionHandler = exceptionHandler; } @Override public void getBalances(GetBalancesRequest req, StreamObserver responseObserver) { UserThread.execute(() -> { // TODO (woodser): Balances.updateBalances() runs on UserThread for JFX components, so call from user thread, else the properties may not be updated. remove JFX properties or push delay into CoreWalletsService.getXmrBalances()? try { var balances = coreApi.getBalances(req.getCurrencyCode()); var reply = GetBalancesReply.newBuilder() .setBalances(balances.toProtoMessage()) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } }); } @Override public void getXmrSeed(GetXmrSeedRequest req, StreamObserver responseObserver) { try { var reply = GetXmrSeedReply.newBuilder() .setSeed(coreApi.getXmrSeed()) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void getXmrPrimaryAddress(GetXmrPrimaryAddressRequest req, StreamObserver responseObserver) { try { var reply = GetXmrPrimaryAddressReply.newBuilder() .setPrimaryAddress(coreApi.getXmrPrimaryAddress()) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void getXmrNewSubaddress(GetXmrNewSubaddressRequest req, StreamObserver responseObserver) { try { String subaddress = coreApi.getXmrNewSubaddress(); var reply = GetXmrNewSubaddressReply.newBuilder() .setSubaddress(subaddress) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void getXmrTxs(GetXmrTxsRequest req, StreamObserver responseObserver) { try { List xmrTxs = coreApi.getXmrTxs(); var reply = GetXmrTxsReply.newBuilder() .addAllTxs(xmrTxs.stream() .map(s -> toXmrTx(s).toProtoMessage()) .collect(Collectors.toList())) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void createXmrTx(CreateXmrTxRequest req, StreamObserver responseObserver) { try { MoneroTxWallet tx = coreApi.createXmrTx( req.getDestinationsList() .stream() .map(s -> new MoneroDestination(s.getAddress(), new BigInteger(s.getAmount()))) .collect(Collectors.toList())); log.info("Successfully created XMR tx, hash: {}", tx.getHash()); var reply = CreateXmrTxReply.newBuilder() .setTx(toXmrTx(tx).toProtoMessage()) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void createXmrSweepTxs(CreateXmrSweepTxsRequest req, StreamObserver responseObserver) { try { List xmrTxs = coreApi.createXmrSweepTxs(req.getAddress()); log.info("Successfully created XMR sweep txs, hashes: {}", xmrTxs.stream().map(MoneroTxWallet::getHash).collect(Collectors.toList())); var reply = CreateXmrSweepTxsReply.newBuilder() .addAllTxs(xmrTxs.stream() .map(s -> toXmrTx(s).toProtoMessage()) .collect(Collectors.toList())) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void relayXmrTxs(RelayXmrTxsRequest req, StreamObserver responseObserver) { try { List txHashes = coreApi.relayXmrTxs(req.getMetadatasList()); var reply = RelayXmrTxsReply.newBuilder() .addAllHashes(txHashes) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void getAddressBalance(GetAddressBalanceRequest req, StreamObserver responseObserver) { try { AddressBalanceInfo balanceInfo = coreApi.getAddressBalanceInfo(req.getAddress()); var reply = GetAddressBalanceReply.newBuilder() .setAddressBalanceInfo(balanceInfo.toProtoMessage()).build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void getFundingAddresses(GetFundingAddressesRequest req, StreamObserver responseObserver) { try { List balanceInfo = coreApi.getFundingAddresses(); var reply = GetFundingAddressesReply.newBuilder() .addAllAddressBalanceInfo( balanceInfo.stream() .map(AddressBalanceInfo::toProtoMessage) .collect(Collectors.toList())) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void setWalletPassword(SetWalletPasswordRequest req, StreamObserver responseObserver) { try { coreApi.setWalletPassword(req.getPassword(), req.getNewPassword()); var reply = SetWalletPasswordReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void removeWalletPassword(RemoveWalletPasswordRequest req, StreamObserver responseObserver) { try { coreApi.removeWalletPassword(req.getPassword()); var reply = RemoveWalletPasswordReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void lockWallet(LockWalletRequest req, StreamObserver responseObserver) { try { coreApi.lockWallet(); var reply = LockWalletReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void unlockWallet(UnlockWalletRequest req, StreamObserver responseObserver) { try { coreApi.unlockWallet(req.getPassword(), req.getTimeout()); var reply = UnlockWalletReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void getHeight(GetWalletHeightRequest req, StreamObserver responseObserver) { try { var height = coreApi.getHeight(); var targetHeight = coreApi.getTargetHeight(); var reply = GetWalletHeightReply.newBuilder() .setHeight(height) .setTargetHeight(targetHeight) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } final ServerInterceptor[] interceptors() { Optional rateMeteringInterceptor = rateMeteringInterceptor(); return rateMeteringInterceptor.map(serverInterceptor -> new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); } final Optional rateMeteringInterceptor() { return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ put(getGetBalancesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 100 : 1, SECONDS)); // TODO: why do tests make so many calls to get balances? put(getGetAddressBalanceMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); put(getGetFundingAddressesMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); // Trying to set or remove a wallet password several times before the 1st attempt has time to // persist the change to disk may corrupt the wallet, so allow only 1 attempt per 5 seconds. put(getSetWalletPasswordMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS, 5)); put(getRemoveWalletPasswordMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS, 5)); put(getLockWalletMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); put(getUnlockWalletMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); }} ))); } } ================================================ FILE: daemon/src/main/java/haveno/daemon/grpc/GrpcXmrConnectionService.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.daemon.grpc; import com.google.inject.Inject; import haveno.core.api.CoreApi; import haveno.daemon.grpc.interceptor.CallRateMeteringInterceptor; import haveno.daemon.grpc.interceptor.GrpcCallRateMeter; import static haveno.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; import haveno.proto.grpc.AddConnectionReply; import haveno.proto.grpc.AddConnectionRequest; import haveno.proto.grpc.CheckConnectionReply; import haveno.proto.grpc.CheckConnectionRequest; import haveno.proto.grpc.GetAutoSwitchReply; import haveno.proto.grpc.GetAutoSwitchRequest; import haveno.proto.grpc.GetBestConnectionReply; import haveno.proto.grpc.GetBestConnectionRequest; import haveno.proto.grpc.GetConnectionReply; import haveno.proto.grpc.GetConnectionRequest; import haveno.proto.grpc.GetConnectionsReply; import haveno.proto.grpc.GetConnectionsRequest; import haveno.proto.grpc.RemoveConnectionReply; import haveno.proto.grpc.RemoveConnectionRequest; import haveno.proto.grpc.SetAutoSwitchReply; import haveno.proto.grpc.SetAutoSwitchRequest; import haveno.proto.grpc.SetConnectionReply; import haveno.proto.grpc.SetConnectionRequest; import haveno.proto.grpc.UrlConnection; import static haveno.proto.grpc.XmrConnectionsGrpc.XmrConnectionsImplBase; import static haveno.proto.grpc.XmrConnectionsGrpc.getAddConnectionMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getCheckConnectionMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getGetBestConnectionMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getGetConnectionMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getGetConnectionsMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getRemoveConnectionMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getSetAutoSwitchMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getSetConnectionMethod; import io.grpc.ServerInterceptor; import io.grpc.stub.StreamObserver; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; import java.util.List; import java.util.Optional; import static java.util.concurrent.TimeUnit.SECONDS; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import monero.common.MoneroRpcConnection; @Slf4j class GrpcXmrConnectionService extends XmrConnectionsImplBase { private final CoreApi coreApi; private final GrpcExceptionHandler exceptionHandler; @Inject public GrpcXmrConnectionService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) { this.coreApi = coreApi; this.exceptionHandler = exceptionHandler; } @Override public void addConnection(AddConnectionRequest request, StreamObserver responseObserver) { handleRequest(responseObserver, () -> { coreApi.addXmrConnection(toMoneroRpcConnection(request.getConnection())); return AddConnectionReply.newBuilder().build(); }); } @Override public void removeConnection(RemoveConnectionRequest request, StreamObserver responseObserver) { handleRequest(responseObserver, () -> { coreApi.removeXmrConnection(validateUri(request.getUrl())); return RemoveConnectionReply.newBuilder().build(); }); } @Override public void getConnection(GetConnectionRequest request, StreamObserver responseObserver) { handleRequest(responseObserver, () -> { UrlConnection replyConnection = toUrlConnection(coreApi.getXmrConnection()); GetConnectionReply.Builder builder = GetConnectionReply.newBuilder(); if (replyConnection != null) { builder.setConnection(replyConnection); } return builder.build(); }); } @Override public void getConnections(GetConnectionsRequest request, StreamObserver responseObserver) { handleRequest(responseObserver, () -> { List connections = coreApi.getXmrConnections(); List replyConnections = connections.stream() .map(GrpcXmrConnectionService::toUrlConnection).collect(Collectors.toList()); return GetConnectionsReply.newBuilder().addAllConnections(replyConnections).build(); }); } @Override public void setConnection(SetConnectionRequest request, StreamObserver responseObserver) { handleRequest(responseObserver, () -> { if (request.getUrl() != null && !request.getUrl().isEmpty()) coreApi.setXmrConnection(validateUri(request.getUrl())); else if (request.hasConnection()) coreApi.setXmrConnection(toMoneroRpcConnection(request.getConnection())); else coreApi.setXmrConnection((MoneroRpcConnection) null); // disconnect from client return SetConnectionReply.newBuilder().build(); }); } @Override public void checkConnection(CheckConnectionRequest request, StreamObserver responseObserver) { handleRequest(responseObserver, () -> { MoneroRpcConnection connection = coreApi.checkXmrConnection(); UrlConnection replyConnection = toUrlConnection(connection); CheckConnectionReply.Builder builder = CheckConnectionReply.newBuilder(); if (replyConnection != null) { builder.setConnection(replyConnection); } return builder.build(); }); } @Override public void getBestConnection(GetBestConnectionRequest request, StreamObserver responseObserver) { handleRequest(responseObserver, () -> { MoneroRpcConnection connection = coreApi.getBestXmrConnection(); UrlConnection replyConnection = toUrlConnection(connection); GetBestConnectionReply.Builder builder = GetBestConnectionReply.newBuilder(); if (replyConnection != null) { builder.setConnection(replyConnection); } return builder.build(); }); } @Override public void setAutoSwitch(SetAutoSwitchRequest request, StreamObserver responseObserver) { handleRequest(responseObserver, () -> { coreApi.setXmrConnectionAutoSwitch(request.getAutoSwitch()); return SetAutoSwitchReply.newBuilder().build(); }); } @Override public void getAutoSwitch(GetAutoSwitchRequest request, StreamObserver responseObserver) { handleRequest(responseObserver, () -> { GetAutoSwitchReply.Builder builder = GetAutoSwitchReply.newBuilder(); builder.setAutoSwitch(coreApi.getXmrConnectionAutoSwitch()); return builder.build(); }); } private <_Reply> void handleRequest(StreamObserver<_Reply> responseObserver, RpcRequestHandler<_Reply> handler) { try { _Reply reply = handler.handleRequest(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @FunctionalInterface private interface RpcRequestHandler<_Reply> { _Reply handleRequest() throws Exception; } private static UrlConnection toUrlConnection(MoneroRpcConnection rpcConnection) { if (rpcConnection == null) return null; return UrlConnection.newBuilder() .setUrl(rpcConnection.getUri()) .setPriority(rpcConnection.getPriority()) .setOnlineStatus(toOnlineStatus(rpcConnection.isOnline())) .setAuthenticationStatus(toAuthenticationStatus(rpcConnection.isAuthenticated())) .build(); } private static UrlConnection.AuthenticationStatus toAuthenticationStatus(Boolean authenticated) { if (authenticated == null) return UrlConnection.AuthenticationStatus.NO_AUTHENTICATION; else if (authenticated) return UrlConnection.AuthenticationStatus.AUTHENTICATED; else return UrlConnection.AuthenticationStatus.NOT_AUTHENTICATED; } private static UrlConnection.OnlineStatus toOnlineStatus(Boolean online) { if (online == null) return UrlConnection.OnlineStatus.UNKNOWN; else if (online) return UrlConnection.OnlineStatus.ONLINE; else return UrlConnection.OnlineStatus.OFFLINE; } private static MoneroRpcConnection toMoneroRpcConnection(UrlConnection uriConnection) throws MalformedURLException { if (uriConnection == null) return null; return new MoneroRpcConnection( validateUri(uriConnection.getUrl()), nullIfEmpty(uriConnection.getUsername()), nullIfEmpty(uriConnection.getPassword())) .setPriority(uriConnection.getPriority()); } private static String validateUri(String url) throws MalformedURLException { if (url.isEmpty()) throw new IllegalArgumentException("URL is required"); return new URL(url).toString(); // validate and return } private static String nullIfEmpty(String value) { if (value == null || value.isEmpty()) { return null; } return value; } final ServerInterceptor[] interceptors() { Optional rateMeteringInterceptor = rateMeteringInterceptor(); return rateMeteringInterceptor.map(serverInterceptor -> new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); } private Optional rateMeteringInterceptor() { return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ int allowedCallsPerTimeWindow = 10; put(getAddConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getRemoveConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getGetConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getGetConnectionsMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getSetConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getCheckConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getGetBestConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getSetAutoSwitchMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); }} ))); } } ================================================ FILE: daemon/src/main/java/haveno/daemon/grpc/GrpcXmrNodeService.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.daemon.grpc; import com.google.inject.Inject; import haveno.core.api.CoreApi; import haveno.core.xmr.XmrNodeSettings; import haveno.daemon.grpc.interceptor.CallRateMeteringInterceptor; import haveno.daemon.grpc.interceptor.GrpcCallRateMeter; import static haveno.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; import haveno.proto.grpc.GetXmrNodeSettingsReply; import haveno.proto.grpc.GetXmrNodeSettingsRequest; import haveno.proto.grpc.IsXmrNodeOnlineReply; import haveno.proto.grpc.IsXmrNodeOnlineRequest; import haveno.proto.grpc.StartXmrNodeReply; import haveno.proto.grpc.StartXmrNodeRequest; import haveno.proto.grpc.StopXmrNodeReply; import haveno.proto.grpc.StopXmrNodeRequest; import haveno.proto.grpc.XmrNodeGrpc.XmrNodeImplBase; import static haveno.proto.grpc.XmrNodeGrpc.getGetXmrNodeSettingsMethod; import static haveno.proto.grpc.XmrNodeGrpc.getIsXmrNodeOnlineMethod; import static haveno.proto.grpc.XmrNodeGrpc.getStartXmrNodeMethod; import static haveno.proto.grpc.XmrNodeGrpc.getStopXmrNodeMethod; import io.grpc.ServerInterceptor; import io.grpc.stub.StreamObserver; import java.util.HashMap; import java.util.Optional; import static java.util.concurrent.TimeUnit.SECONDS; import lombok.extern.slf4j.Slf4j; import monero.common.MoneroError; @Slf4j public class GrpcXmrNodeService extends XmrNodeImplBase { private final CoreApi coreApi; private final GrpcExceptionHandler exceptionHandler; @Inject public GrpcXmrNodeService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) { this.coreApi = coreApi; this.exceptionHandler = exceptionHandler; } @Override public void isXmrNodeOnline(IsXmrNodeOnlineRequest request, StreamObserver responseObserver) { try { var reply = IsXmrNodeOnlineReply.newBuilder() .setIsRunning(coreApi.isXmrNodeOnline()) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void getXmrNodeSettings(GetXmrNodeSettingsRequest request, StreamObserver responseObserver) { try { var settings = coreApi.getXmrNodeSettings(); var builder = GetXmrNodeSettingsReply.newBuilder(); if (settings != null) { builder.setSettings(settings.toProtoMessage()); } var reply = builder.build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void startXmrNode(StartXmrNodeRequest request, StreamObserver responseObserver) { try { var settings = request.getSettings(); coreApi.startXmrNode(XmrNodeSettings.fromProto(settings)); var reply = StartXmrNodeReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (MoneroError me) { handleMoneroError(me, responseObserver); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } @Override public void stopXmrNode(StopXmrNodeRequest request, StreamObserver responseObserver) { try { coreApi.stopXmrNode(); var reply = StopXmrNodeReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (MoneroError me) { handleMoneroError(me, responseObserver); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } } private void handleMoneroError(MoneroError me, StreamObserver responseObserver) { // MoneroError is caused by the node startup failing, don't treat as unknown server error // by wrapping with a handled exception type. var headerLengthLimit = 8192; // MoneroErrors may print the entire monerod help text which causes a header overflow in grpc if (me.getMessage().length() > headerLengthLimit) { exceptionHandler.handleException(log, new IllegalStateException(me.getMessage().substring(0, headerLengthLimit - 1)), responseObserver); } else { exceptionHandler.handleException(log, new IllegalStateException(me), responseObserver); } } final ServerInterceptor[] interceptors() { Optional rateMeteringInterceptor = rateMeteringInterceptor(); return rateMeteringInterceptor.map(serverInterceptor -> new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); } private Optional rateMeteringInterceptor() { return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ int allowedCallsPerTimeWindow = 10; put(getIsXmrNodeOnlineMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getGetXmrNodeSettingsMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getStartXmrNodeMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getStopXmrNodeMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); }} ))); } } ================================================ FILE: daemon/src/main/java/haveno/daemon/grpc/interceptor/CallRateMeteringInterceptor.java ================================================ /* * 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 . */ package haveno.daemon.grpc.interceptor; import io.grpc.Metadata; import io.grpc.ServerCall; import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; import io.grpc.StatusRuntimeException; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import java.util.HashMap; import java.util.Map; import java.util.Optional; import static io.grpc.Status.PERMISSION_DENIED; import static java.lang.String.format; import static java.util.stream.Collectors.joining; @Slf4j public final class CallRateMeteringInterceptor implements ServerInterceptor { // Maps the gRPC server method names to rate meters. This allows one interceptor // instance to handle rate metering for any or all the methods in a Grpc*Service. protected final Map serviceCallRateMeters; public CallRateMeteringInterceptor(Map serviceCallRateMeters) { this.serviceCallRateMeters = serviceCallRateMeters; } @Override public ServerCall.Listener interceptCall(ServerCall serverCall, Metadata headers, ServerCallHandler serverCallHandler) { Optional> rateMeterKV = getRateMeterKV(serverCall); rateMeterKV.ifPresentOrElse( (kv) -> checkRateMeterAndMaybeCloseCall(kv, serverCall), () -> handleMissingRateMeterConfiguration(serverCall)); // We leave it to the gRPC framework to clean up if the server call was closed // above. But we still have to invoke startCall here because the method must // return a ServerCall.Listener. return serverCallHandler.startCall(serverCall, headers); } private void checkRateMeterAndMaybeCloseCall(Map.Entry rateMeterKV, ServerCall serverCall) { String methodName = rateMeterKV.getKey(); GrpcCallRateMeter rateMeter = rateMeterKV.getValue(); if (!rateMeter.checkAndIncrement()) handlePermissionDeniedWarningAndCloseCall(methodName, rateMeter, serverCall); else log.info(rateMeter.getCallsCountProgress(methodName)); } private void handleMissingRateMeterConfiguration(ServerCall serverCall) throws StatusRuntimeException { log.debug("The gRPC service's call rate metering interceptor does not" + " meter the {} method.", getRateMeterKey(serverCall)); } private void handlePermissionDeniedWarningAndCloseCall(String methodName, GrpcCallRateMeter rateMeter, ServerCall serverCall) throws StatusRuntimeException { String msg = getDefaultRateExceededError(methodName, rateMeter); log.warn(msg + "."); serverCall.close(PERMISSION_DENIED.withDescription(msg.toLowerCase()), new Metadata()); } private String getDefaultRateExceededError(String methodName, GrpcCallRateMeter rateMeter) { // The derived method name may not be an exact match to CLI's method name. String timeUnitName = StringUtils.chop(rateMeter.getTimeUnit().name().toLowerCase()); // Just print 'getversion', not the grpc method descriptor's // full-method-name: 'io.haveno.protobuffer.getversion/getversion'. String loggedMethodName = methodName.split("/")[1]; return format("The maximum allowed number of %s calls (%d/%s) has been exceeded", loggedMethodName, rateMeter.getAllowedCallsPerTimeWindow(), timeUnitName); } private Optional> getRateMeterKV(ServerCall serverCall) { String rateMeterKey = getRateMeterKey(serverCall); return serviceCallRateMeters.entrySet().stream() .filter((e) -> e.getKey().equals(rateMeterKey)).findFirst(); } private String getRateMeterKey(ServerCall serverCall) { // Get the rate meter map key from the server call method descriptor. The // returned String (e.g., 'io.haveno.protobuffer.Offers/CreateOffer') will match // a map entry key in the 'serviceCallRateMeters' constructor argument, if it // was defined in the Grpc*Service class' rateMeteringInterceptor method. return serverCall.getMethodDescriptor().getFullMethodName(); } @Override public String toString() { String rateMetersString = serviceCallRateMeters.entrySet() .stream() .map(Object::toString) .collect(joining("\n\t\t")); return "CallRateMeteringInterceptor {" + "\n\t" + "serviceCallRateMeters {" + "\n\t\t" + rateMetersString + "\n\t" + "}" + "\n" + "}"; } public static CallRateMeteringInterceptor valueOf(Map rateMeters) { return new CallRateMeteringInterceptor(new HashMap<>() {{ putAll(rateMeters); }}); } } ================================================ FILE: daemon/src/main/java/haveno/daemon/grpc/interceptor/GrpcCallRateMeter.java ================================================ package haveno.daemon.grpc.interceptor; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import java.util.ArrayDeque; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import static java.lang.String.format; import static java.lang.System.currentTimeMillis; @Slf4j public class GrpcCallRateMeter { @Getter private final int allowedCallsPerTimeWindow; @Getter private final TimeUnit timeUnit; @Getter private final int numTimeUnits; @Getter private transient final long timeUnitIntervalInMilliseconds; private transient final ArrayDeque callTimestamps; public GrpcCallRateMeter(int allowedCallsPerTimeWindow, TimeUnit timeUnit) { this(allowedCallsPerTimeWindow, timeUnit, 1); } public GrpcCallRateMeter(int allowedCallsPerTimeWindow, TimeUnit timeUnit, int numTimeUnits) { this.allowedCallsPerTimeWindow = allowedCallsPerTimeWindow; this.timeUnit = timeUnit; this.numTimeUnits = numTimeUnits; this.timeUnitIntervalInMilliseconds = timeUnit.toMillis(1) * numTimeUnits; this.callTimestamps = new ArrayDeque<>(); } public boolean checkAndIncrement() { synchronized (callTimestamps) { if (getCallsCount() < allowedCallsPerTimeWindow) { incrementCallsCount(); return true; } else { return false; } } } public int getCallsCount() { synchronized (callTimestamps) { removeStaleCallTimestamps(); return callTimestamps.size(); } } public String getCallsCountProgress(String calledMethodName) { synchronized (callTimestamps) { String shortTimeUnitName = StringUtils.chop(timeUnit.name().toLowerCase()); // Just print 'GetVersion has been called N times...', // not 'io.haveno.protobuffer.GetVersion/GetVersion has been called N times...' String loggedMethodName = calledMethodName.split("/")[1]; return format("%s has been called %d time%s in the last %s, rate limit is %d/%s", loggedMethodName, callTimestamps.size(), callTimestamps.size() == 1 ? "" : "s", shortTimeUnitName, allowedCallsPerTimeWindow, shortTimeUnitName); } } private void incrementCallsCount() { callTimestamps.add(currentTimeMillis()); } private void removeStaleCallTimestamps() { while (!callTimestamps.isEmpty() && isStale.test(callTimestamps.peek())) { callTimestamps.remove(); } } private final Predicate isStale = (t) -> { long stale = currentTimeMillis() - this.getTimeUnitIntervalInMilliseconds(); // Is the given timestamp before the current time minus 1 timeUnit in millis? return t < stale; }; @Override public String toString() { synchronized (callTimestamps) { return "GrpcCallRateMeter{" + "allowedCallsPerTimeWindow=" + allowedCallsPerTimeWindow + ", timeUnit=" + timeUnit.name() + ", timeUnitIntervalInMilliseconds=" + timeUnitIntervalInMilliseconds + ", callsCount=" + callTimestamps.size() + '}'; } } } ================================================ FILE: daemon/src/main/java/haveno/daemon/grpc/interceptor/GrpcServiceRateMeteringConfig.java ================================================ /* * 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 . */ package haveno.daemon.grpc.interceptor; import com.google.common.annotations.VisibleForTesting; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import io.grpc.ServerInterceptor; import lombok.extern.slf4j.Slf4j; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import static com.google.common.base.Preconditions.checkNotNull; import static haveno.common.file.FileUtil.deleteFileIfExists; import static haveno.common.file.FileUtil.renameFile; import static java.lang.String.format; import static java.lang.System.getProperty; import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.file.Files.readAllBytes; @VisibleForTesting @Slf4j public class GrpcServiceRateMeteringConfig { public static final String RATE_METERS_CONFIG_FILENAME = "ratemeters.json"; private static final String KEY_GRPC_SERVICE_CLASS_NAME = "grpcServiceClassName"; private static final String KEY_METHOD_RATE_METERS = "methodRateMeters"; private static final String KEY_ALLOWED_CALL_PER_TIME_WINDOW = "allowedCallsPerTimeWindow"; private static final String KEY_TIME_UNIT = "timeUnit"; private static final String KEY_NUM_TIME_UNITS = "numTimeUnits"; private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); private final List> methodRateMeters; private final String grpcServiceClassName; public GrpcServiceRateMeteringConfig(String grpcServiceClassName) { this(grpcServiceClassName, new ArrayList<>()); } public GrpcServiceRateMeteringConfig(String grpcServiceClassName, List> methodRateMeters) { this.grpcServiceClassName = grpcServiceClassName; this.methodRateMeters = methodRateMeters; } @SuppressWarnings("unused") public GrpcServiceRateMeteringConfig addMethodCallRateMeter(String methodName, int maxCalls, TimeUnit timeUnit) { return addMethodCallRateMeter(methodName, maxCalls, timeUnit, 1); } public GrpcServiceRateMeteringConfig addMethodCallRateMeter(String methodName, int maxCalls, TimeUnit timeUnit, int numTimeUnits) { methodRateMeters.add(new LinkedHashMap<>() {{ put(methodName, new GrpcCallRateMeter(maxCalls, timeUnit, numTimeUnits)); }}); return this; } public boolean isConfigForGrpcService(Class clazz) { return isConfigForGrpcService(clazz.getSimpleName()); } public boolean isConfigForGrpcService(String grpcServiceClassSimpleName) { return this.grpcServiceClassName.equals(grpcServiceClassSimpleName); } @Override public String toString() { return "GrpcServiceRateMeteringConfig{" + "\n" + " grpcServiceClassName='" + grpcServiceClassName + '\'' + "\n" + ", methodRateMeters=" + methodRateMeters + "\n" + '}'; } public static Optional getCustomRateMeteringInterceptor(File installationDir, Class grpcServiceClass) { File configFile = new File(installationDir, RATE_METERS_CONFIG_FILENAME); return configFile.exists() ? toServerInterceptor(configFile, grpcServiceClass) : Optional.empty(); } public static Optional toServerInterceptor(File configFile, Class grpcServiceClass) { // From a global rate metering config file, create a specific gRPC service // interceptor configuration in the form of an interceptor constructor argument, // a map. // Transforming json into the List> is a bit // convoluted due to Gson's loss of generic type information during deserialization. Optional grpcServiceConfig = getAllDeserializedConfigs(configFile) .stream().filter(x -> x.isConfigForGrpcService(grpcServiceClass)).findFirst(); if (grpcServiceConfig.isPresent()) { Map serviceCallRateMeters = new HashMap<>(); for (Map methodToRateMeterMap : grpcServiceConfig.get().methodRateMeters) { Map.Entry entry = methodToRateMeterMap.entrySet().stream().findFirst().orElseThrow(() -> new IllegalStateException("Gson deserialized a method rate meter configuration into an empty map.")); serviceCallRateMeters.put(entry.getKey(), entry.getValue()); } return Optional.of(new CallRateMeteringInterceptor(serviceCallRateMeters)); } else { return Optional.empty(); } } @SuppressWarnings("unchecked") private static List> getMethodRateMetersMap(Map gsonMap) { List> rateMeters = new ArrayList<>(); // Each gsonMap is a Map with a single entry: // {getVersion={allowedCallsPerTimeUnit=8.0, timeUnit=SECONDS, callsCount=0.0, isRunning=false}} // Convert it to a multiple entry Map, where the key // is a method name. for (Map singleEntryRateMeterMap : (List>) gsonMap.get(KEY_METHOD_RATE_METERS)) { log.debug("Gson's single entry {} {} = {}", gsonMap.get(KEY_GRPC_SERVICE_CLASS_NAME), singleEntryRateMeterMap.getClass().getSimpleName(), singleEntryRateMeterMap); Map.Entry entry = singleEntryRateMeterMap.entrySet().stream().findFirst().orElseThrow(() -> new IllegalStateException("Gson deserialized a method rate meter configuration into an empty map.")); String methodName = entry.getKey(); GrpcCallRateMeter rateMeter = getGrpcCallRateMeter(entry); rateMeters.add(new LinkedHashMap<>() {{ put(methodName, rateMeter); }}); } return rateMeters; } @SuppressWarnings({"rawtypes", "unchecked"}) public static List deserialize(File configFile) { verifyConfigFile(configFile); List serviceMethodConfigurations = new ArrayList<>(); // Gson cannot deserialize a json string to List // so easily for us, so we do it here before returning the list of configurations. List rawConfigList = gson.fromJson(toJson(configFile), ArrayList.class); // Gson gave us a list of maps with keys grpcServiceClassName, methodRateMeters: // String grpcServiceClassName // List methodRateMeters for (Object rawConfig : rawConfigList) { Map gsonMap = (Map) rawConfig; String grpcServiceClassName = (String) gsonMap.get(KEY_GRPC_SERVICE_CLASS_NAME); List> rateMeters = getMethodRateMetersMap(gsonMap); serviceMethodConfigurations.add(new GrpcServiceRateMeteringConfig(grpcServiceClassName, rateMeters)); } return serviceMethodConfigurations; } @SuppressWarnings("unchecked") private static GrpcCallRateMeter getGrpcCallRateMeter(Map.Entry gsonEntry) { Map valueMap = (Map) gsonEntry.getValue(); int allowedCallsPerTimeWindow = ((Number) valueMap.get(KEY_ALLOWED_CALL_PER_TIME_WINDOW)).intValue(); TimeUnit timeUnit = TimeUnit.valueOf((String) valueMap.get(KEY_TIME_UNIT)); int numTimeUnits = ((Number) valueMap.get(KEY_NUM_TIME_UNITS)).intValue(); return new GrpcCallRateMeter(allowedCallsPerTimeWindow, timeUnit, numTimeUnits); } private static void verifyConfigFile(File configFile) { if (configFile == null) throw new IllegalStateException("Cannot read null json config file."); if (!configFile.exists()) throw new IllegalStateException(format("cannot find json config file %s", configFile.getAbsolutePath())); } private static String toJson(File configFile) { try { return new String(readAllBytes(Paths.get(configFile.getAbsolutePath()))); } catch (IOException ex) { throw new IllegalStateException(format("Cannot read json string from file %s.", configFile.getAbsolutePath())); } } private static List allDeserializedConfigs; private static List getAllDeserializedConfigs(File configFile) { // We deserialize once, not for each gRPC service wanting an interceptor. if (allDeserializedConfigs == null) allDeserializedConfigs = deserialize(configFile); return allDeserializedConfigs; } @VisibleForTesting public static class Builder { private final List rateMeterConfigs = new ArrayList<>(); public void addCallRateMeter(String grpcServiceClassName, String methodName, int maxCalls, TimeUnit timeUnit) { addCallRateMeter(grpcServiceClassName, methodName, maxCalls, timeUnit, 1); } public void addCallRateMeter(String grpcServiceClassName, String methodName, int maxCalls, TimeUnit timeUnit, int numTimeUnits) { log.info("Adding call rate metering definition {}.{} ({}/{}ms).", grpcServiceClassName, methodName, maxCalls, timeUnit.toMillis(1) * numTimeUnits); rateMeterConfigs.stream().filter(c -> c.isConfigForGrpcService(grpcServiceClassName)) .findFirst().ifPresentOrElse( (config) -> config.addMethodCallRateMeter(methodName, maxCalls, timeUnit, numTimeUnits), () -> rateMeterConfigs.add(new GrpcServiceRateMeteringConfig(grpcServiceClassName) .addMethodCallRateMeter(methodName, maxCalls, timeUnit, numTimeUnits))); } public File build() { File tmpFile = serializeRateMeterDefinitions(); File configFile = Paths.get(getProperty("java.io.tmpdir"), "ratemeters.json").toFile(); try { deleteFileIfExists(configFile); renameFile(tmpFile, configFile); } catch (IOException ex) { throw new IllegalStateException(format("Could not create config file %s.", configFile.getAbsolutePath()), ex); } return configFile; } private File serializeRateMeterDefinitions() { String json = gson.toJson(rateMeterConfigs); File file = createTmpFile(); try (OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(checkNotNull(file), false), UTF_8)) { outputStreamWriter.write(json); } catch (Exception ex) { throw new IllegalStateException(format("Cannot write file for json string %s.", json), ex); } return file; } private File createTmpFile() { File file; try { file = File.createTempFile("ratemeters_", ".tmp", Paths.get(getProperty("java.io.tmpdir")).toFile()); } catch (IOException ex) { throw new IllegalStateException("Cannot create tmp ratemeters json file.", ex); } return file; } } } ================================================ FILE: daemon/src/main/java/haveno/daemon/grpc/interceptor/PasswordAuthInterceptor.java ================================================ /* * 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 . */ package haveno.daemon.grpc.interceptor; import com.google.inject.Inject; import haveno.common.config.Config; import io.grpc.Metadata; import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; import static io.grpc.Metadata.Key; import io.grpc.ServerCall; import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; import static io.grpc.Status.UNAUTHENTICATED; import io.grpc.StatusRuntimeException; import static java.lang.String.format; /** * Authorizes rpc server calls by comparing the value of the caller's * {@value PASSWORD_KEY} header to an expected value set at server startup time. * * @see haveno.common.config.Config#apiPassword */ public class PasswordAuthInterceptor implements ServerInterceptor { private static final String PASSWORD_KEY = "password"; private final String expectedPasswordValue; @Inject public PasswordAuthInterceptor(Config config) { this.expectedPasswordValue = config.apiPassword; } @Override public ServerCall.Listener interceptCall(ServerCall serverCall, Metadata headers, ServerCallHandler serverCallHandler) { var actualPasswordValue = headers.get(Key.of(PASSWORD_KEY, ASCII_STRING_MARSHALLER)); if (actualPasswordValue == null) throw new StatusRuntimeException(UNAUTHENTICATED.withDescription( format("missing '%s' rpc header value", PASSWORD_KEY))); if (!actualPasswordValue.equals(expectedPasswordValue)) throw new StatusRuntimeException(UNAUTHENTICATED.withDescription( format("incorrect '%s' rpc header value", PASSWORD_KEY))); return serverCallHandler.startCall(serverCall, headers); } } ================================================ FILE: daemon/src/main/resources/logback.xml ================================================ %hl2(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{40}: %msg %xEx%n) ================================================ FILE: daemon/src/test/java/haveno/daemon/grpc/interceptor/GrpcServiceRateMeteringConfigTest.java ================================================ /* * 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 . */ package haveno.daemon.grpc.interceptor; import haveno.daemon.grpc.GrpcVersionService; import io.grpc.ServerInterceptor; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.File; import java.nio.file.Paths; import java.util.Optional; import java.util.concurrent.TimeUnit; import static haveno.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; import static java.lang.System.getProperty; import static java.util.concurrent.TimeUnit.DAYS; import static java.util.concurrent.TimeUnit.HOURS; import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @Slf4j public class GrpcServiceRateMeteringConfigTest { private static final GrpcServiceRateMeteringConfig.Builder builder = new GrpcServiceRateMeteringConfig.Builder(); private static File configFile; @SuppressWarnings("OptionalUsedAsFieldOrParameterType") private static Optional versionServiceInterceptor; @BeforeAll public static void setup() { // This is the tested rate meter, it allows 3 calls every 2 seconds. builder.addCallRateMeter(GrpcVersionService.class.getSimpleName(), "getVersion", 3, SECONDS, 2); builder.addCallRateMeter(GrpcVersionService.class.getSimpleName(), "badMethodNameDoesNotBreakAnything", 100, DAYS); // The other Grpc*Service classes are not @VisibleForTesting, so we hardcode // the simple class name. builder.addCallRateMeter("GrpcOffersService", "createOffer", 5, MINUTES); builder.addCallRateMeter("GrpcTradesService", "takeOffer", 10, DAYS); builder.addCallRateMeter("GrpcWalletsService", "sendBtc", 3, HOURS); } @BeforeEach public void buildConfigFile() { if (configFile == null) configFile = builder.build(); } @Test public void testConfigFileBuild() { assertNotNull(configFile); assertTrue(configFile.exists()); assertTrue(configFile.length() > 0); String expectedConfigFilePath = Paths.get(getProperty("java.io.tmpdir"), "ratemeters.json").toString(); assertEquals(expectedConfigFilePath, configFile.getAbsolutePath()); } @Test public void testGetVersionCallRateMeter() { // Check the interceptor has 2 rate meters, for getVersion and badMethodNameDoesNotBreakAnything. CallRateMeteringInterceptor versionServiceInterceptor = buildInterceptor(); assertEquals(2, versionServiceInterceptor.serviceCallRateMeters.size()); // Check the rate meter config. GrpcCallRateMeter rateMeter = versionServiceInterceptor.serviceCallRateMeters.get("getVersion"); assertEquals(3, rateMeter.getAllowedCallsPerTimeWindow()); assertEquals(SECONDS, rateMeter.getTimeUnit()); assertEquals(2, rateMeter.getNumTimeUnits()); assertEquals(2 * 1000, rateMeter.getTimeUnitIntervalInMilliseconds()); // Do as many calls as allowed within rateMeter.getTimeUnitIntervalInMilliseconds(). doMaxIsAllowedChecks(true, rateMeter.getAllowedCallsPerTimeWindow(), rateMeter); // The next 3 calls are blocked because we've exceeded the 3calls/2s limit. doMaxIsAllowedChecks(false, rateMeter.getAllowedCallsPerTimeWindow(), rateMeter); // Let all of the rate meter's cached call timestamps become stale by waiting for // 2001 ms, then we can call getversion another 'allowedCallsPerTimeUnit' times. rest(1 + rateMeter.getTimeUnitIntervalInMilliseconds()); // All the stale call timestamps are gone and the call count is back to zero. assertEquals(0, rateMeter.getCallsCount()); doMaxIsAllowedChecks(true, rateMeter.getAllowedCallsPerTimeWindow(), rateMeter); // We've exceeded the call/second limit. assertFalse(rateMeter.checkAndIncrement()); // Let all of the call timestamps go stale again by waiting for 2001 ms. rest(1 + rateMeter.getTimeUnitIntervalInMilliseconds()); // Call twice, resting 0.5s after each call. for (int i = 0; i < 2; i++) { assertTrue(rateMeter.checkAndIncrement()); rest(500); } // Call the 3rd time, then let one of the rate meter's timestamps go stale. assertTrue(rateMeter.checkAndIncrement()); rest(1001); // The call count was decremented by one because one timestamp went stale. assertEquals(2, rateMeter.getCallsCount()); assertTrue(rateMeter.checkAndIncrement()); assertEquals(rateMeter.getAllowedCallsPerTimeWindow(), rateMeter.getCallsCount()); // We've exceeded the call limit again. assertFalse(rateMeter.checkAndIncrement()); } private void doMaxIsAllowedChecks(boolean expectedIsAllowed, int expectedCallsCount, GrpcCallRateMeter rateMeter) { for (int i = 1; i <= rateMeter.getAllowedCallsPerTimeWindow(); i++) { assertEquals(expectedIsAllowed, rateMeter.checkAndIncrement()); } assertEquals(expectedCallsCount, rateMeter.getCallsCount()); } @AfterAll public static void teardown() { if (configFile != null) configFile.deleteOnExit(); } private void rest(long milliseconds) { try { TimeUnit.MILLISECONDS.sleep(milliseconds); } catch (InterruptedException ignored) { } } private CallRateMeteringInterceptor buildInterceptor() { //noinspection OptionalAssignedToNull if (versionServiceInterceptor == null) { versionServiceInterceptor = getCustomRateMeteringInterceptor( configFile.getParentFile(), GrpcVersionService.class); } assertTrue(versionServiceInterceptor.isPresent()); return (CallRateMeteringInterceptor) versionServiceInterceptor.get(); } } ================================================ FILE: desktop/package/29CDFD3B.asc ================================================ -----BEGIN PGP PUBLIC KEY BLOCK----- mQINBFl5pBEBEACmse+BgUYi+WLTHR4xDwFE5LyEIT3a5t+lGolO3cVkfw5RI+7g FEpxXzWontiLxDdDi34nr1zXOIEjSgQ7HzdtnFiTRN4tIENCBul4YiCOiyBi5ofN ejAHqmeiO0KsDBQZBdyiK1iWi6yNbpG/rARwHu/Rx5ouT1YX1hV92Qh1bnU+4j4O FcePQRNl+4q/SrtKdm047Ikr/LBvy/WYBYe9BcQGhbHI4DrUOSnIuI/Zq7xLF8QS U/an/d0ftbSBZNX3anDiZjzSmR16seRQtvRO6mehWFNlgLMOGgFeJzPkByTd1TlV K/KaHKQ71FNkRiP87pwkHZI5zJPAQfve+KmYPwOyETUaX43XOuixqutUV6Lrd0ng bKe6q4nZDOWi5a4I3+hkrfzaGOKm9TlZoEmpJHh6pa5ULoDnEpKCg5Dgn3NGwokw 57sDAC2mtf41/uSkR20ALN1q4iOLXiHn+T6Z+Uq7aL3OcKGcBu4xC6Jfofmmdfdd QxEEaeHvAI9ETlKy3tsMhEs5XD6m90rCKLnb97Y8eT/xJL4/oDsxI0o7qICz1nFS 2IhV8xULZ2533vNQPMEbSLoTzgz1OEPYwI1b+YJDFlp1y0XRiEtDZiAFfgsJY7UE DizfuUFsK5LOkw2+NVmLphDVrDW1MXbhX1xspZDmBG9giE08sPtHj/EZHwARAQAB tDNDaHJpc3RvcGggQXR0ZW5lZGVyIDxjaHJpc3RvcGguYXR0ZW5lZGVyQGdtYWls LmNvbT6JAj0EEwEKACcFAll5pBECGwMFCQeGH4AFCwkIBwMFFQoJCAsFFgIDAQAC HgECF4AACgkQzV3BxSnN/Ts46g/+KJL3M6Bfr9K9muNjEeA6yBS5HmZ2tBJI3PiF pJC5MBO5H+3ukI8nyzYil3JhEqCcTUspBGbbkqbwbSQuS19VYBHHxhSAn7B/MHFC FnlbKEzS3cHyp95lGPLJ/y5FXXnSxdlC1EXFcuSjHWR27cCUGuH+1diuyh2eZoT5 fN27g5B5ej0ExXoCp8J4MtMhcHXjGy7Kiv0CbcY8vYEYbqd5GsMvk7UZIa+vWTMz JE1fp6msFfUFzHXYRhO/TKi8iRtVaUUcaOHz7kb226ckVnzIO3CjsGg7y19BYaWf C6Rw0XqPfCf7PoJjhRxbC/9ZWujy/pkaOtOBoq+IZECkiHsKUcZgNdU7xMyCE0a5 jOvJrzKna6MELPczTyeWqZvL0dKNhllw5WJIhzf5mcFqOb1OlNjWxC1BnOeNk51f +FDtjxOyp6P7uL0dPy7j4TA7aHgQNKy2Uvx3+Eu9EHKL2T35xXPvma1ZVybQlMBK z7rbjTIiKTf5LqTtFyE4Kx6IS29rygyJPxz81r4pbjoGUIxLnhxL+6LwxCPwmbkI fFRD+gk8ODmhgY947D6VBPPrrH4U9YiUJZ718b3tCJoubLPrGUfbFlKaGBloK+Ld 0ulJGZrQWxiK3y1KO1AF8k1ge9utJowLAq8rZOUdSPb/cjo3OsspqJR9OQQXNO0n 6WL3Y/a5Ag0EWXmkEQEQAMt06beoYe/vmAWR91y5BUIu1zNmQP2NNAZ1Jh1K3q7a AVEamyVmdF4i2JVF7fTnRGWDiKgjF2f9KJA2mC9v6EK6l7KK/7oQfFgympku8hSL jtp/TWIZZ1D9z16GdqmWaRGdMkqmjf7Wpy26A5TCsUbGvn1tm9P8PxqNfgCv3Cap FhPciK4o/e4gXY7tUbYMC65Dmq3OoJWWzAGqeDmbH4U5BcoZBk+SFyknF/5NWGuz E0yl6TRkgEhzneyBcaV1bmSVcWBpNozoyZC49JggrwFJExd5QQE06iWbx+OkWHYt ObJSKQd3liC1EcAFzI0BoZQ5ZE8VoTXpVQXQcsYtbWKj5BReiEIovi3/+CmjxUFS M7fjeelRwVWeh0/FnD7KxF5LshUDlrc/JIRxI9RYZcbhoXB1UMc/5SX5AT0+a86p Gay7yE0JQGtap1Hi5yf1yDMJr1i89u1LfKXbHb2jMOzyiDYR2kaPO0IDpDJ6kjPc fFAcNt/FpJw5U3mBKy8tHlIMoFd/5hTFBf9Pnrj3bmXx2dSd1Y3l6sQjhceSIALQ I95QfXY57a04mHURO/CCxwzLlKeI1Qp7zT9TiV7oBx85uY2VtrxPdPmPHF0y9Fnh K1Pq2VAN53WHGK9MEuyIV/VxebN7w2tDhVi9SI2UmdGuDdrLlCBhT0UeCYt2jFxF ABEBAAGJAiUEGAEKAA8FAll5pBECGwwFCQeGH4AACgkQzV3BxSnN/TsbkQ//dsg1 fvzYZDv989U/dcvZHWdQHhjRz1+Y2oSmRzsab+lbCMd9nbtHa4CNjc5UxFrZst83 7fBvUPrldNFCA94UOEEORRUJntLdcHhNnPK+pBkLzLcQbtww9nD94B6hqdLND5iW hnKuI7BXFg8uzH3fRrEhxNByfXv1Uyq9aolsbvRjfFsL7n/+02aKuBzIO5VbFedN 0aZ52mA1aooDKD69kppBWXs+sxPkHkpCexJUkr3ekjsH8jk10Over8DNj8QN4ii2 I3/xsRCCvrvcKNfm4LR49KJ+5YUUkOo1xWSwOzWHV9lpn2abMEqqIqnubvENclNi qIbE6vkAaILyubilgxTVbc6SntUarUO/59j2a0c+pDnHgLB799bnh0mAmXXCVO3Z 14GpaH15iaUCgRgxx9uP+lQIj6LtrPOsc5b5J6VLgdxQlDXejKe9PaM6+agtIBmb I24t36ljmRrha2QH90MhyDPrJ/U6ch/ilgTTNRWbfTsALRxzNmnHvj0Y55IsdYg3 bm71QT99x9kNuozo7I4MrGElS+9Pwy31lcY17OSy/K1wqpLCW1exc4SwJRsAImNW QLNcwMx1fIBhPiyuhRVsjoCEda5rO+NYF8U8u/UrXixNXsHGBgaynWO/rI9KFg0f NYeOG8Xnm4CxuWqUu0FDMv6BhkMCTz2X4xcnbtI= =9LRS -----END PGP PUBLIC KEY BLOCK----- ================================================ FILE: desktop/package/5BC5ED73.asc ================================================ -----BEGIN PGP PUBLIC KEY BLOCK----- mQINBFLubUkBEAC9dIbgokeCmvyELlpIW56AIgRPsqm5WqxXQyaoKGc2jwWsuHY2 10ekprWficlPS2AC/lV0Mj5rtEgintRYh0Do0gKVaiCL31/L2lPh9WVuLeYQ2Oyv 4p5u7BFHLOu+j3VynLI9MKlr7rT1gDuFLGp8eTfaYnIgFmZ1uTB48YoYw9AAnOpT qtxIYZ81jS7lPkQeeViGEqdJdTDZZUKeKaTnJL+yaq6kSFhUW9I4HPxS/oZGRuFn qefqmDyWypc5bl4CsxLHhhNGI4QrCEHZcQEGwx4Fn8qFXW+47e4KVBZrh0QxIjNJ Rg41DF/oBBsTMXJogVawKIlyQalE+WcKVQtKcUcCBw3cLaHzn/QMYrfQTMhB/3Sk kuN4TCx7HOyM9rFt7y+lz5buPdHlocqbISk6QtbiMCKyb5XwXVcE/MAas/LGE2il zxf7el9Sfey8Yd0t71SAJXrItdygz+iAxoTtnXbjIB/3YzkfSPD4nCAbbHmzx+C6 oV1Xw07usdXLBLQf5jPvKKzjO+xAMHyS7Sf6JJod2ACdJXBEuA2YhK9GNqojfJjI /w0GpV96tAHq3tb30QXZe5NxxIdiw4h5q+VGgIHwpRtNeqx2ngpxY8qHBm5UBYk0 KKX8msoDIwjnVtfcBFkuPiJlxQ48JRmh80vW4ZEZ3Rm2zRv1lsWpx/QhRwARAQAB tBxDaHJpcyBCZWFtcyA8Y2hyaXNAYmVhbXMuaW8+iQI3BBMBAgAiBQJS7m1JAhsD BgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRA9IU+PW8XtcxXHD/dAAY9mw9AT 5LeDFvtkZlK2hec0MPWnoUMBjxTCsaoH+fBCT2Bt53hwkHw4tm0mkxpq3E+zoy2B 1tOHSI7a6AxBREBXEh2OFDU2gDj/H8nR65gw+OAJSCzAH2s1+v30YcajsRZUEbhu wxXWg+e01wKsJZjjcef47Q3N6H6h/H5HrtCz2+s/E3JmDNlD8zR1ueqfG0LjvmD9 MJfjI8sHhRUBoQeLxUirn4oD0++jf3M4JIClZeq1ZHJBxvk+fyty4CTn7ekhqJvl p9z+AF3MgpmHfbvzDUeqSKFVuLUd3KijI4I9vgbv5/EZuXP+punbXKIOFqjCyLpP zToDrjupNIkddwvhNTaapHyxlR5fIRRRgGB2fVDkfsfNz9rIoEM6k7saAxgJrccz Ry193nic4IuyX/LVFVxpX8rSYvVNmbaLPSOre6o4pc+26Etj5uddsLQAxPOdk4m3 rd8lVNtKEHbQ/6IFC2wdH52v4kIc5NNIa3YnmjXzaQ3W0dPaS9VDruQm20+zHubs LIU0kh1O9gSiTsPK3IHAu0Y/usdYES/IwxdyUR+Lue0XTS/NaKvt3BqZ5wnIQRKo X1ka5iUwpmJ6OlI4eqc+3noHQfgNfYrhCR8g9A0FypHctE0pO2UTqCnaCmHuX4Gw +I3Q7IWvpF/mqeRp6eerByt6H3iwvA93uQINBFLubUkBEADRMq7zeNj6dXGCY7JC Uy2YqRvL5N5+AMF2iC4WZ/Lci8iALGcPcsSI8CwdTqGl9SOV/5qqBR3osz50tDoK H+NUjd0sN86kefTVhk9a2TlTKTUmFocqc4sJi2uLl8gBySoyBwucMD1JULvxmdOp i40n/YcIZ/NsUr5MZsLAxNRNbc9SiNhG6Ccq8mURbuwVx+S+qQEqgKAjMAeKeWDa +kFAzfBRi+CoN0yvOF1hDmcXe0zQuShPZU1/KbbSWc0nUcO78b05xK1da5+/iTaU 4GepVYO8o11YiYEV4DgVTTBilFST27vaAe8Re1VBlKlQdSM6tuJAc8IG7FbGyu33 mCzMNfj0niIErZIcFAsrwAeT3ea/d9ckp/xBK51hgRctaNl4Tw9GVudfrVspREGf oUBwOICUhpv51gbuvNWdyUvThYdIGWPGO7NMMCfWFkiJi/UKd5PDcnif1DXnsw4M FnV67AqWDr0neIxz46RjGvPBOERu7uFSrey70V5HA50rTETofr59dblnICDyS7Jn yVM1pLzrKgm+R1LXilrH9+1dmEU/oJlmbY6ikX3IQTUZLnLsP3I/u0V8YbAa3Q4p EqifZscPzw0A65FB1ihAjfj9Ar10LbPIOSbj8rLB2/hCA3TtkXvYxaq7jwOf68Gm 6M8Uh6h0EbVg/MkrAQhlPhtb1QARAQABiQIfBBgBAgAJBQJS7m1JAhsMAAoJED0h T49bxe1zZdoP/0bMLMiOQFg1/64QeI0n8OcNbcVsWh+1NWi7LtTFX3pKuiWhTOiS UJslD9Kwtbe9tqiOXxXoXO/XOPOZfa2hv6D7q9xyv5aGClFY5NXc7pNP3I6CqCh0 6VOy99X2m9H2rYE9RCg4CRt1rIT1Uzespx+kdQgJNBSmwFFT/DvpbPQ+LZBu3izp MK2qZXd2yoe4xv1Oo0dodU/OVgjkgQk38flphDUxOkkOy1meU42Oh6iY4BvuhelD a9eJgtXovWqCGoZErbfQZMgzpZVeHjvLEsOUye0nZlo/hpTjiHYhUJrjZN3Muik5 7BhHLm0MRu1o0kgAhE2Vd3qjKgMjQDnZGmn7bi3pSwdE6qob6B4A6dsN8R589tEN haxPnmjjyM+F4dw/O//Hb2dwOv0386Kv8lNINdY/1S6HRNeh+c4eh6MAd7nf+vWU JZjF6aPmr6Sa0VXVrMdsLo/7RBZxHtRBc8glQPM13hSYeU86a5Qn9AyHwS3fVgcc pKOk2kLJ9XMRuzD70qWItebghB5Yrtp1sL0LMhNYBkAMv73QxoW11fI/6T3fBqAS 1xGI0yMF/tFTIP1TRwJ0uEgK9vOYlS01OM4ajLGfcV/ZWelQDCM2cJXshq/extL1 C3Ba3TvZjzPPWR//c0wkF/4gg/V2A/9Jjam7BVS4JWd/bFRwZ5aR3qux =AWz+ -----END PGP PUBLIC KEY BLOCK----- ================================================ FILE: desktop/package/F379A1C6.asc ================================================ -----BEGIN PGP PUBLIC KEY BLOCK----- mQINBFWK4uEBEADjSnRHU294auU1BPH+50OvsWnIvMb6kzqRdY3xlxecRAMsC/Dh XyKVvY+wtC2a/1R+Cj5VO/geEDt0WBbwqj/zAi+x8ttrzZDn5CxmWvU6ulFCFKAr cmB/eZmBMQSJ/JSZw1DeD090/tafuYUDjfhcqE1ajh7WxSIbMudaAm5yd/AuHB3c +mlr5fjBwtBN1nyjfi9N3f7XJS8GrdJFC43/1FWHS3Z+GHydLkIcLS1keT5fYJbe VZGC/RzUJBxqN6UFxIRJhPIplyBFfQBpWIFFxZNr6VZWeQlGnFjX1v3//hmD7mnT 3aGqqkUFcI5q7De3nNm2wfVnV50bzqj+FiSZWUUpWvgD01uzxWxzCVERn8s1jana jLt3hfS8ly5kx311oZTyhXDR5z5LsrOjJv7U+hwhtDHAI0yyD7LPWCYFK2jwljYV Tli8KHchMOlV0Yxm62ebmO/orju4Rq+T4id2nfwJGimRY/DX+k7/1qSHdyjnoYn1 qqpVWD0UhjNLf337PThr20nA/FD3hjwnmIT5becHzrPGbRnr3Y2s77LFUe+nfGE3 wvQmmpSNccFIz/146lynxJHWMfSqOJMgJZWpSPFKd39BhxxP9g5Sou6wEnM+YWYT eOI1dGPejA4EHZec7s3j7hcx33rejydmsjW8yJjkRaFxYJk4jaoT7LgGiQARAQAB tCVNYW5mcmVkIEthcnJlciA8bWFuZnJlZEBiaXRzcXVhcmUuaW8+iQI9BBMBCgAn BQJViuLhAhsDBQkHhh+ABQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJEPW4RDbz eaHGLFIQAM/w8BBoZ7+K4hxZmjkt/lXDXddHg9jvfU3jgIR+CkThcHy7n1e+QvsG k0FozYsFyCaLwOiGR0/eUcUu+aegwRnx+eh/scElcAN40RYIr2nCU5vNGqmKBrP2 ShQu0z5CcqFoccIHZ7VSe42SYb3GJ8g2mtC1Um+ryytZtF0g7nJxGWe//4YmqavC TV4KU5akJGfFVPDW84qJJo34gwg80oshxnOQnXfoa+xlmSDQMaOTYH07cR9N64JW TUad4aTl7niFZPizzg9r4ltRwUzvyXD5CHQoKqGWvZO0pHvRRq5SHp5nDoKh5hZs lb/QJu6X8bTlLwhpOLsXPBPqoXQibRiICfAdVPBYnHMtvJ7RcuZyazvpYYjlgYsK kol+jUude4zAIEky1A/77wl1pBXURw8vk8CRPIqAAOniaTySk6b24fseYsMcmP1Z VLt4HxV0njBRAe51DV8AiT/gscTdg8/GsJRjzKedCs0SZIjBg5/1iULXRtQZrUd7 vkOZZCSRzMAf4iHGK0qFuUoEkoZacv+bfOfwse62F89ngM1iB9RdyPPZIuh70i8O Ebzzs6TBptq9WtV5LEXtkkHyfCugoIKegdKZmZBxnNT+XZMQQ68E8jeTiUA8MaPR hYJp2FL+DLsTcLHt+ffHmcYJ1+6/v8UMIx6wC2k862+h4Y1aBme9iQEiBBABCgAM BQJVwoK3BYMHhh+AAAoJEAxMpdRX1mvaFmYH/jx1ayv+6zNlYsFaL3idIBCmWQb7 Lu4qE8bhSGN557jc7HoYT1DAYubm4zV65KxMVs+AsrNoy9Q4mXxpzIsi3X/J/3qF L7hF+ZiZf8ms0FNScFW4rrWJpWaZ03zf97hx7D08oBxMtn++hA0/Ur7YN6fLtOe4 sv19D8U6UvjT1LsDIpDXDUuLNTAcpq5liOGL9PHa3przvekuVIROgosGbdfY34KO v9PfyL2H7Q6np7awjBE3GsbIMcEp+JsFgE8M3GJzke/absuqeNHpCgpIWPMoWCXb guKOsVipIBuRNhaRG4hRFAQAUYRe2UL9ZH6BBKfAZoOkgYP+Kv01XGhADXGJAiIE EAEKAAwFAlXCgr8FgweGH4AACgkQQBJQlmprLEaYSw//ccfvWZGxvi4R0EHM61pD +Jp0iTdMb+8L1lK6+pzVaQvPf06UWD9qjN79cWDI0HmVFVgFPE0qRbsIi33s7ltF Gc3Dw7ql2R7q1XS06pkT8ihesdYgauNA0802js5/RJK3joZEujNAQkz33O4daECf MWEVia0JrFZktUlwVTjKOzKyoBlUiV/Rg/ivnTRVXyfDIp7qCUHcIhz19U4zK0kh NKkgVxddKIeyivmUghzQbYEkAZlvBfLRXvnnK1TdouOgLOvHetf7LQDKpgHxJmre o8XjHrF08/mDfKRvqh8Vi/j5zj8Kyy7LIjF3rHzCJDjwDp4tgSDEekMgEzYLoiyn /y6lCxS78m+/EkCdE2Hncb81n6fgldQTSCChpfUbvqQAuewb9wonQp3gtqIdEwg1 WS4T78m1qNfFP9I3UYKWRdSplifJAhr2NAyaf9fSNVSGRZk5sDcaXRrraPj5DHR6 kgDITkv1ph6sB+4cu/6XmoZ8ZWAmmQFz4/EejlBu4khZHVPCojtGmbyOAAmV8M+a 4zWPXxdtOlKUCZpa7jOPkto4V804K5FsOn3qascdLdd/VYtjPWE/qoWs96/J81w5 SJIXZ0s3j/EWwRKtcxZO22x0IXDIA5oPY7gQC1JDT/dt1blbGQ9nCLIPL5QoxxAF noxMOtoVHlC98rjnPgCtuACJASIEEAEKAAwFAlXCgsYFgweGH4AACgkQeiIM+js9 3FtGugf+JPu6S1RNVbgv8n1cX9Krt3JnXi0ybzhlCILxe8lRqzk9OXsVzY1Zvnor 0L35jYa2Y8GSEgivKEZXcdJroCXmBJRWs3ck3SSevxmqDm1+nQ96TBFtno6m8hf4 3UoT0YnuryGffV0XEyO/m7ujIj5iF8UvWC6d+ve/oQw815IJROZNBjgRn6bhpgnq sWVWioSQg9Jzqs/h8rjFWrscbln/mBCxyn6PsjIO6N1ArBcB9s95iCxiz6MXiMrl vGbd6KIsaaG6H9IXfCFcOXkN+1pfr6439LRZMxC1hqHEqLWPV8iCwPyFJkHJUeyg 8hYuIFeEIvG4Z9ukEKrd27sh7eCgN4kCHAQQAQgABgUCVulMPQAKCRA97MEF9d0j ghURD/4p+kGPeqQZq4sq4v7wxYPLnihTIdD1rZXOtWa3wnVOf5o03MGpXaaQIyez LRgF1FSgkAV0v1kOJcUVOwZNXfivFAz5b5dV5cX8X/8AFc798gOQ2BpKDs8Gh4Vh V+aV9Zslac0QLKA8LKmJOlVCb+GpQKmwPZ8IFfr+NtMhRW/h1WSualLHYpmmfH0A GCnDM00w1pgGavtcTjIrvihA0uw4ySFT6QzI1z+1zmZlPsdpZEbpAeTyrGecDIRj FAOTsmbe2YOk6kzj3xhwL8hMjtfX4EZl9KW1bGR6/fy8fVaM8lHi0Pa4BgIeca+M ir+kHw7G1FgHrjiqOUuvCuK9uln1We0DttIi7RB1lYmX+Ds7XfSKj/OcrHWwxtJk phKofIyGt9b12tqjJKdyS+81FBjgsiUSGQJdThm2vefVKKMqc91OlGPg8q3804x2 3BlOHg98pz3TjOmrARSzhpGLz1KfS8o3YQYQ1HqymS1zyjuim5V+pf1s7bFg6RE9 d0ipnTwEXmXIuU8fu09DAm6Z4o9XP2RP49cCieDOQ0dp2YKIzae6RkZjCjUPujiI pZeN8rCageX6G0iuwKMOLfNn/g5ItHOm72U28aV6hMzTmHvdB+eaKl304N1fzjya +FSncOcO9SazYGEKjs46ec48k389lXZ7nMuZDMwPZgsDyzZWn4kCHAQSAQoABgUC WDhcJAAKCRDAwHYTL/p2lSfUD/sGJhNJiIrGYf9qw8qBQJyaQDoNBFHLvl2tUpOf +TVojDywJ+51askcL3L0hldbUg4wziPi2FV8AtRyerDKNcMUJYn6SQ3Rhx/7eFP/ vnUqJ7f8ZJEk7LzGYDZGQpnSe/eyXNARVjoPUFhjl6mTLtKPZWfaprs2e+yvQimy 2hgsiWOvc19ifsRg6KVSSTBqUS+FCSw0VRR1wt5cmrFRkuRfGoCHHd8mXkI1qSit xfFQxyURxHWxLkWnwN9y0G8cYvSOI/hgmfY/MYY6NRWKbmzXIve5n7qFKNFBR3n4 NA9oJwI2Mzop8GuwSU54QlmiG6N0Elqt7c+aU1bOGt3dWJS8e5J7VBZqoFrIzPAC DObdSkU3Y04ZQ5LcnAn0n6dpZRTX2Fv0Tcxv7MCEfQCDCeBs9xDrXIcEZLNyrBQE kcXbL4EUBjsq80fLV5/a5iyhS67pJc10mS5T8pkFd7hA6eTesRRbP2Do1ndiZCPw E2gugDmz6hjTAUwG6iLu2rwJ2aOOm3V3PmYZ/JM45zGTjKFb2sEzkuOG1YdHIt30 FXqEswItLMWQl5xTwuHId9mPvgKLz9h5ZYt8ML5G+QXFEVnKiU0pFWabDgpb81cS 0aAYQcSOUG5ObyBjHsZXwQKNpe9oEFN6xrbE7dp4FxtZpZXExLE+PUffxjOyGw0W Lj+WYokCPQQTAQoAJwIbAwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAUCWU795AUJ BaVOgwAKCRD1uEQ283mhxrjVD/41bwb1y38w1K3dlOavw6t2RwwvmBgNDlhLFrM1 ZK3Kjk+p9s2/8GoeGdPiVgrqv3okI+Ztme+R+jtWRPSozczfZyIdgR2/jdhS8P0L IUIbQlkn7cvvDb4Wf8lAUhnGF/a+Gmnpn+Ju65KcTxFBGSSt5q2iQVbsW+krhyoy nuD6C/2QLDKH+YPOahihmrTpLQkJ4IwdK+0LfoWqcgNB5JiRKd4fcgXEYTxxBMSc 5QwlRkU638PTkjGaBbb7I+RxWrk3Y75SyyFbD/svJm4JxQGFQCvPOiesSTQrQuCV opoZj0YKfZzUpgiVYQFm1MCLLhWs9nDxJ0d2lxropUTm+8BYuuy/pSki60GGbKv6 MnWUhExmde01U3wjxkHeXX9u2qswL/spORVtqtxDvWQeUyZyhIs1Slled+7RLOww bJNamKGVdBcN3XZwaxNeuBX1nppjKKeleS56C0BFuTVEptEsdRj62FVJIli1MH43 IxAN0iJUsO2XSljhmixQu37jfkLW4HlCLiZwYLCJDoXFtHZZ5nwURGeBGSeGyWzC tx7DJvXDEx/GWMJzU500X4iTc5gcvLLsTm5bxKthOITITHgvXXAMmc0YpLmmueZA UNMShQsxkzm3QOBCVgjW532OQHM66Plsact4hCYJ+p0GSQGcUgKmNGcfQKmLNhvR oT2NlYkCIgQTAQoADAUCWF7kZAWDB4YfgAAKCRBAElCWamssRtBxD/0RK9Rk9eCg jj61Vk8rA/Uvmz7ZEwHlunL6pucvy2RZL+ztMNSlLPYcvtByvSvUo8Q9G/YnjR6l EGMi5DERN5Euc2nMIlg82EWQyd3MEAcBxcqriLuKrybizce1o8pUExV84DJYchr/ A9ei93GiHbNodMxv5zt+4pu/e540DxATf9ME6EpJtbzJcUwsGUrPOtC9Xp13t1sL 4sHL1z3TVPOzOQ8HSlfMOUdYNoJg89MjTnX//rOpfSIq3FcEVURipOgLKDhiQEmQ tpeknv7uZDZuRLxK/J7IebmnbnmV3wq3l5LVCLRTzpCXtucTwnKoRYBcbT676F70 GbM7QPwdiyiq17lx3+YHElHm0KGfxn6iUSPGtEJv6RcO8glU/VIN6/N8HxH5Y9HE 28+eC094GnKD3xQtMzSrzTkp7q5NGZPBS4wT3mGwem4pfjuYbkWea3+71jnr6SHW ciGWo4kXuVp9Va1ZFuNxk8o0G8YdHV30oEXJwTyyxXFMdePraz95B4fjtJdpgsnq JLsFIIgdpECBeiyqy83pOCv5aoRiqz8xAYrkZWYKtw1xdMZ25LoyI/OgdmCIjrr1 q+VGPXi2CcB2Y4XmJCUCYUh1W7eGoXlOEWA3upONa+RkrOZ0bEQ/VuHqvWeVol7i uWqdxi9IrjPK2hhrMPMKeNphpjLLitfWM4kBIgQTAQoADAUCWF7kdQWDB4YfgAAK CRAMTKXUV9Zr2pLpB/9K4hu0ELE9D5ZvsFlD6lQUNvonuiZGX/xBsejYL/rEPWSv 2dJpb815rPkJtoqUZ5cem2sJhOKc+ZIKHZy0hiobqbePZXArT7dh5aIfMYfFfvYE YOZYUD9dbMjzjqHrpfUvht6IxekYfkm+XKYL1PGdxGZ4AiK9ZoRVnM/eK2p6+qcG VEPkHJXJKNRMvPJTniKsW3vryqM0J0Z6wDqH9IEWIhus1GeEcm/j7Hw0OO5gku2/ uI8SndS6dtCjruGcVLpG0quMdCFCsp/jtH1y1opFDT3cW7g7q3RQxw0dNzflraaz vQm+caO8QtZGVS6vegaOf2a4hfACKtt9EzVVLq93iQIiBBMBCgAMBQJYXuR9BYMH hh+AAAoJEMM+Vu/rhndFAXwP/2bAkgdM2CZ/WRXRAecBCCIz483+bS4yXaraSeJz bWl8Cp3aVMqRyLGcDo0UhYRTzDfgcX2YLlK0pBwAnvNd2hi8cqQC1RWOdYrgUMzN /StI9NOImov0n8kWqK+kqdxJIwz2Rs3crlwpDR5bosTzG/HwwxtNGB1h1T72RrGf ISOrqtRgHnAod9DBluONJaUv1QN95zHCVQqh0deAZLYtOlPhUbXYgfZQGlufUBpt SJAsA2W5yuN+Qiav7TetqQDN/zapPIxGSkfuN+t22ek65OyAbEHQP7Z1ltI/+PEM DaqU+Pzb9UJtZEpcHDCTB4YbI9+H6k/WAp6w2Cm3nNEJiaYAr8XocxxTMnjlVtnr OE/4++wuQE8EvcuvnxXkhyLbZnhAdpv1lUgzll3pAhe9w4RCpX4tz5JHB5EoQXAP LHIQRzWc8TzE4H+aUdSwrewQL3qxrgadJDST2Em/DFIj8EftwbJCLpm8jtp9fw7U gP4iv8hYhUBCRH24n5h8YwbwCOOGadLNfoUh6DHHa7ZdktOADQfcmOhA840FoL+v t85+sVCgGaBsoPkgS/ZvE5KRbtugiMuCO5OS3lIpzDjaw+2nH1Wq2uuuBya3owyJ GIxKtLNxg/pZhwnSsVwi3sDmmXLq6WDTCAfeR+8+PKgC6IgDUAYalpoxEq7grVi9 e/yiiQEiBBMBCgAMBQJYXuSFBYMHhh+AAAoJEHoiDPo7PdxbfaEH/AshsMxZBRt3 f5f4JrTQtk8veAqwKzHNUMbl9sQzBjEvg9V7SaoGp1cKPb1e9Sy1VDD496dBYgxc rUOG7Wp3XD/Gccht2a/EL+CJaJHyVzfskLTtFtnrMItLCR7uuqQEvK/Q2IxJpaMt wNKEpzDCgKzFXUjet/gB2j90dMWqPVDh/GRiktrcDVXaX3roMBenWmzeBpT8oUA2 Tw8gUr696Y7d2RgDHncTlm/qS5w1QyLT5CIXd1Os6+eA8MIjFUSKaDxNM9zCzGvv hkj7bBfi1xxmPonogGKd56tgBFZ7EGeFD/TGLjCtJLYz8pPP/F2az557xFJ41aVt Cq0wTtHSrsGJAiIEEwEKAAwFAlhe/hQFgweGH4AACgkQNmQEJH0guzLtoRAAvA1V ZPyE/RoTjTkZ0468R7txGSNiQBMHeKQzSj5vrFvFjuQOx1pKvPbBa//pfddmXsyN 4+fXkm+i3jhiww85VmfP+jaPZE7ha3gI1sLIXywBUEQXGtN+JrGdIfx4fm9Xj+Km a41o77XxnYxg/puqtoxXuFQfF+KcX3SgCzaGnhn+p2YfUtIgqaVkQl6H6vhKley8 pZcB58O9Eu0RbpGg/FWovOWY/Jg8DdbOpQmrp4tXD116rt8m0jEJcWk/DPexehHn Znt4Xi/oogBiccRDd/ebUeyjUkkrPk+IQjdYYOuN0i0nMUL9KsWLJwUHNa2IWv1e xgVg9dWuPk13K2hJFzdvGa19IVsBOEEXgfIyC2ZSqz0zFhAQQ/2saRDvITgQS10W duL55lv78YevjqeETEHW2DeXkzUiRwe64BUuu/9LFsSLuwCwLrvz3Yyh0T21MAAA /5sHsai4hRhxAhVoWfelKShzmZdh7bdqrxDrivutdcOn9Evdw3IQ9rsDtgyDrvmm Mok1eSYvZF61yhHvdVU6wQOET7u3T7eSFoAW7EknuAd4rSIZ2AqBchARGEbz3m7w aidB1KmedzlGNk0DEWcXiqpdgQdvalzxfJJSIOsJic7FH+p2xBnFYBVdS/ftgrC2 kuPY4dpfVviNxGLrRDd8fYfdVDolMW1pOWo7oQq5Ag0EVYri4QEQAOtygi1rXfDl /H18Evad7dz96ZFDGSQNoD9eC4UCGD5F2AqEil7pTNapIDqcGaz1MZl5k4B9CjH7 mutQukLXcHtdrc5eXYjMQZ/jVFjjv5j3fPgwWrz6LfxYD/jxw7uTgDHlgEo/Dv8D WMeE3wcycKhlG9KT/qdx+1b36ds7ecYeooYIxHSCAbQl+4mKjn4HNIhAGTcNe7i9 79rGApBNJgpSYnaqK7i3CFvIeMRWLQKk41s4sBrwZI+hEFnlZoJ3Le7Mh/0emcfs ZCk4YNwdfGiZWoic8ZMudx0JUkso/ELRxzx/bgNls+vpQb3SQ1zuFZ8xnOunEmaf DYbg/hJguAT3fnvGqqeO0305+OVflxcoUyxXDxLtY+4t6SEj2v3L9t+ZpbQg12+d lr3Eel+NltXibv2yVhwP0NpQq+CJ+nPDQWsCcK/FelP2ik1EZqasQPZZFBORKXNV JCmWXm+8GNbwN9wvVR9rmwh0h8v9RAbh7Q4inYnxiVVKIH14ZGQp8i3NW/k5sOuk RqM203tEV4LGCP+bwswcwPCmvfid3L8oQmPA7ezL6rmlehe7ctP4iHEX/xxGbRzD ZWNdNZrTdq0h1WR6ce7Ya52VNN1dBoqkbZmzQxD+NC/3dv/yl8MfnEeJDdvQCGQ2 0zCbGfXWc2T9ov4BK/a05cDBlXaIpH/fABEBAAGJAiUEGAEKAA8FAlWK4uECGwwF CQeGH4AACgkQ9bhENvN5ocZGxA/+I+GLTTFaHRy6ZNmAr6uEPQ59yXOE5k2ZrML7 F2nnIR0FJFydhnLSsxCt89zXxmxk4kA4h+M5jmyB4HiIGp0u0lC/zpklJwJ8+EKj KpSaL9zdo1hwojybGar78mF4qsQ2EZP0TIq41gOZ/qx7dVaDSu75cuQvgGakEQcx 89B5RGaZRKLlE68Mo2QXktNENnPFkkOPBoil8KX34DHIWJafncwu0vObcE31ifIZ j9j3FoeupnIW4HXEbsZBWkM0k/Fzx3wdvvYuEwR0JvihSJ4YEncB33weZ+u1+XTa cAWt98oubYMoR+M2d4+EAmOJVjz0oGXNvs/BBwSCem3c/oSt43R3lc7zMU8shZf8 bKS+TGYnV/kRWcNc2l0BTiRRUwFZ0/XvAcNXJsB1CyrvbWvrZiDIm6tA3xOJzFGY wLNTM1BqfNfrPbzov67vkkbxxRlTRx1x6LTFPV0H1FTZ5CSQgahjm9SwANb0jyU7 xR9hL3zBvKr7quR7mM1zzjnoGkNMdVsM02fBrmqfhABychMFMVVOWhyLLQO47YZB ghu/JigFHreRBbTOPLcCSfkH24EL91nDnfLp6KHLcz2DfU2W1lajwRfDm2rpbKx+ 6iAnmNBJV49ZaM7lFqPaJz942mVySd+4rygkuF1olWxN1EbzK0/bKRuzljIj5U+r vUTpzlk= =ZIr3 -----END PGP PUBLIC KEY BLOCK----- ================================================ FILE: desktop/package/README.md ================================================ Follow these instructions to create installers for the Haveno Java desktop application on each platform. > **Note** > These steps will delete the previously built Haveno binaries, so they'll need rebuilt after. ## Linux From x86_64 machine: 1. `sudo apt-get update` 2. `sudo apt install -y rpm libfuse2 flatpak flatpak-builder appstream` 3. `flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo` 4. `./gradlew clean build --refresh-keys --refresh-dependencies` (or `make clean && skip-tests` after refreshed) 5. `./gradlew packageInstallers` 6. Confirm prompts. 7. Path to installer is printed at the end. Execute to install, e.g.: `sudo dpkg -i .deb` or open `.deb` with Software Install. Note: Please see [flatpak.md](../../docs/flatpak.md) for information on distributing Haveno via Flatpak. Haveno data folder on Linux: `/home//.local/share/Haveno/` ## macOS From x86_64 machine: 1. `./gradlew clean build --refresh-keys --refresh-dependencies` (or `make clean && skip-tests` after refreshed) 2. `./gradlew packageInstallers` 3. Confirm prompts. 4. Path to installer printed at end. 5. `open ` 6. Open installer and drag Haveno.app to Applications. 7. `sudo xattr -rd com.apple.quarantine /Applications/Haveno.app` 8. Right click /Applications/Haveno.app > Open. Repeat again if necessary, despite being "damaged". Haveno data folder on Mac: `/Users//Library/Application Support/Haveno/` ## Windows 1. Enable .NET Framework 3.5: 1. Open the Control Panel on your Windows system. 2. Click on "Programs and Features" or "Uninstall a Program." 3. On the left-hand side, click on "Turn Windows features on or off." 4. In the "Windows Features" dialog box, scroll down and find the ".NET Framework 3.5 (includes .NET 2.0 and 3.0)" option. 5. Check the box next to it to select it. 6. Click "OK" to save the changes and exit the dialog box. 7. Windows will download and install the required files and components to enable the .NET Framework 3.5. This may take several minutes, depending on your internet connection speed and system configuration. 8. Once the installation is complete, you will need to restart your computer to apply the changes. 2. Install Wix Toolset 3: 3. Open MSYS2 for the following commands. 4. `export PATH=$PATH:$JAVA_HOME/bin:"C:\Program Files (x86)\WiX Toolset v3.14\bin"` 5. `./gradlew clean build --refresh-keys --refresh-dependencies` (or `make clean && skip-tests` after refreshed) 6. `./gradlew packageInstallers` 7. Confirm prompts. 8. Path to installer is printed at the end. Execute to install. Haveno data folder on Windows: `~\AppData\Roaming\Haveno\` ## Copying installer and rebuilding Haveno binaries 1. Copy the installer to a safe location because it will be deleted in the next step. 2. `make clean && make` (or `make clean && make skip-tests`) to rebuild Haveno apps. ## Additional Notes ### Icons Icons (Haveno.zip) were obtained from . ### Building for Linux The linux package requires the correct packaging tools installed. You may run into the following errors: ```sh Error: Invalid or unsupported type: [deb] ``` ```sh Error: Invalid or unsupported type: [rpm] ``` On Ubuntu, resolve by running `sudo apt install rpm`. For deb, ensure dpkg is installed. ```sh Exception in thread "main" java.io.IOException: Failed to rename /tmp/Haveno-stripped15820156885694375398.tmp to /storage/src/haveno/desktop/build/libs/fatJar/desktop-1.0.0-SNAPSHOT-all.jar at haveno.tools.Utils.renameFile(Utils.java:36) at io.github.zlika.reproducible.StipZipFile.strip(StipZipFile.java:35) at haveno.tools.DeterministicBuildTool.main(DeterministicBuildTool.java:24) ``` This may happen if the source folder is on a different hard drive than the system `tmp` folder. The tools-1.0.jar calls renameTo to rename the deterministic jar back to the fat jar location. You can temporarily change your temp directory on linux: ```sh export _JAVA_OPTIONS="-Djava.io.tmpdir=/storage/tmp" ``` ### Building for macOS Svg was converted into a 1024x1024 pixel PNG using , then converted to icns for macosx here #### Known Issues Signing is not implemented. ### Building for Windows Pngs were resized and pasted into the WixUi images using paint. [CloudConvert](https://cloudconvert.com) was used to convert the Haveno png icon to ico. #### Known Issues The installer's final step "Launch Haveno" has a different background color. The setup executable does not have an icon. ================================================ FILE: desktop/package/linux/Dockerfile ================================================ ### # # Quick dockerfile meant to help building. # Missing: # - crypto fixes to JDK # - various paths in the build script ### # pull base image FROM openjdk:8-jdk ENV version 0.0.1-SNAPSHOT RUN apt-get update && apt-get install -y --no-install-recommends openjfx && rm -rf /var/lib/apt/lists/* && apt-get install -y vim fakeroot COPY 64bitBuild.sh /root COPY haveno-$version.jar /root # cd to the Dex directory and execute the jar. #CMD cd ~/Dex && java -jar Dex.jar ================================================ FILE: desktop/package/linux/Haveno.desktop ================================================ [Desktop Entry] Comment=A decentralized, Tor-based, P2P Monero exchange network. Exec=sh -c "PATH=\"\\$HOME/.local/bin:\\$PATH\"; bin/Haveno %u" GenericName[en_US]=Monero Exchange GenericName=Monero Exchange Icon=exchange.haveno.Haveno Categories=Office;Finance;Java;P2P; Name[en_US]=Haveno Name=Haveno Terminal=false Type=Application MimeType= X-AppImage-Name=Haveno StartupWMClass=Haveno ================================================ FILE: desktop/package/linux/exchange.haveno.Haveno.metainfo.xml ================================================ exchange.haveno.Haveno exchange.haveno.Haveno.desktop Haveno Decentralized P2P exchange built on Monero and Tor Office Finance P2P cryptocurrency monero CC-BY-4.0 AGPL-3.0-only #e5a29f #562c63 pointing keyboard touch

    Haveno (pronounced ha‧ve‧no) is a platform for people who want to exchange Monero for fiat currencies like EUR, GBP, and USD or other cryptocurrencies like BTC, ETH, and BCH.

    • All communications are routed through Tor, to preserve your privacy
    • Trades are peer-to-peer: trades on Haveno will happen between people only, there is no central authority.
    • Trades are non-custodial: Haveno provides arbitration in case something goes wrong during the trade, but we will never have access to your funds.
    • There is No token, because we don't need it. Transactions between traders are secured by non-custodial multisignature transactions on the Monero network.
    https://github.com/haveno-dex/haveno/blob/master/desktop/package/linux/preview.png Recent Trades page woodser https://haveno.exchange https://github.com/haveno-dex/haveno/issues moderate moderate intense intense
    ================================================ FILE: desktop/package/linux/exchange.haveno.Haveno.yml ================================================ id: exchange.haveno.Haveno runtime: org.freedesktop.Platform runtime-version: "24.08" sdk: org.freedesktop.Sdk sdk-extensions: - org.freedesktop.Sdk.Extension.openjdk21 command: /app/bin/Haveno modules: - name: openjdk buildsystem: simple build-commands: - /usr/lib/sdk/openjdk21/install.sh - name: Haveno buildsystem: simple sources: # - type: git # url: https://github.com/haveno-dex/haveno - type: dir path: build - type: file path: package/linux/Haveno.desktop - type: file path: package/linux/exchange.haveno.Haveno.metainfo.xml - type: file path: package/linux/icon.png build-commands: - ls - pwd # TODO: consider switching from reading from a deb to reading from jpackage's image - mv temp-*/binaries/haveno_*.deb haveno.deb - ar x haveno.deb - tar xf data.tar.* - cp -r opt/haveno/lib /app/lib - install -D opt/haveno/bin/Haveno /app/bin/Haveno - mkdir -p /app/share/icons/hicolor/128x128/apps/ - mkdir -p /app/share/applications/ - mkdir -p /app/share/metainfo/ - mv icon.png /app/share/icons/hicolor/128x128/apps/exchange.haveno.Haveno.png - mv Haveno.desktop /app/share/applications/exchange.haveno.Haveno.desktop - mv exchange.haveno.Haveno.metainfo.xml /app/share/metainfo/ # TODO: xdg-open fails finish-args: - --env=PATH=/app/jre/bin:/usr/bin:$PATH # - --env=JAVA_HOME=/app/jre - --env=JAVA_HOME=/usr/lib/sdk/openjdk21/ - --device=dri - --talk-name=org.freedesktop.Notifications - --talk-name=org.freedesktop.secrets - --share=network - --share=ipc - --socket=x11 ================================================ FILE: desktop/package/linux/jpackage.deb/Haveno.desktop ================================================ [Desktop Entry] Name=Haveno GenericName=Monero Exchange Comment=A decentralized monero exchange network. Exec=/opt/haveno/bin/Haveno Icon=/opt/haveno/lib/Haveno.png Terminal=false Type=Application Categories=Network MimeType= StartupWMClass=haveno.desktop.app.HavenoApp ================================================ FILE: desktop/package/macosx/Info.plist ================================================ CFBundleVersion 1.2.3 CFBundleShortVersionString 1.2.3 CFBundleExecutable Haveno CFBundleName Haveno CFBundleIdentifier io.haveno.CAT CFBundleIconFile Haveno.icns LSApplicationCategoryType public.app-category.finance NSHumanReadableCopyright Copyright © 2024 - The Haveno developers LSAppNapIsDisabled NSSupportsAutomaticGraphicsSwitching NSHighResolutionCapable true CFBundleAllowMixedLocalizations LSMinimumSystemVersion 10.7.4 CFBundleDevelopmentRegion English CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType APPL ================================================ FILE: desktop/package/macosx/copy_dbs.sh ================================================ #!/bin/bash cd $(dirname $0)/../../../ version="0.0.1" # Set HAVENO_DIR as environment var to the path of your locally synced Haveno data directory e.g. HAVENO_DIR=~/Library/Application\ Support/Haveno dbDir=$HAVENO_DIR/btc_mainnet/db resDir=p2p/src/main/resources # Only commit new TradeStatistics3Store if you plan to add it to # https://github.com/bisq-network/bisq/blob/0345c795e2c227d827a1f239a323dda1250f4e69/common/src/main/java/haveno/common/app/Version.java#L40 as well. cp "$dbDir/TradeStatistics3Store" "$resDir/TradeStatistics3Store_${version}_BTC_MAINNET" cp "$dbDir/AccountAgeWitnessStore" "$resDir/AccountAgeWitnessStore_${version}_BTC_MAINNET" cp "$dbDir/DaoStateStore" "$resDir/DaoStateStore_BTC_MAINNET" cp "$dbDir/SignedWitnessStore" "$resDir/SignedWitnessStore_BTC_MAINNET" ================================================ FILE: desktop/package/macosx/finalize.sh ================================================ #!/bin/bash cd ../../ version="0.0.1-SNAPSHOT" target_dir="releases/$version" # Set HAVENO_GPG_USER as environment var to the email address used for gpg signing. e.g. HAVENO_GPG_USER=manfred@bitsquare.io # Set HAVENO_VM_PATH as environment var to the directory where your shared folders for virtual box are residing vmPath=$HAVENO_VM_PATH linux64=$vmPath/vm_shared_ubuntu win64=$vmPath/vm_shared_windows macos=$vmPath/vm_shared_macosx deployDir=deploy rm -r $target_dir mkdir -p $target_dir # sig key mkarrer cp "$target_dir/../../package/F379A1C6.asc" "$target_dir/" # sig key cbeams cp "$target_dir/../../package/5BC5ED73.asc" "$target_dir/" # sig key Christoph Atteneder cp "$target_dir/../../package/29CDFD3B.asc" "$target_dir/" # signing key cp "$target_dir/../../package/signingkey.asc" "$target_dir/" dmg="Haveno-$version.dmg" cp "$macos/$dmg" "$target_dir/" deb="haveno_$version-1_amd64.deb" deb64="Haveno-64bit-$version.deb" cp "$linux64/$deb" "$target_dir/$deb64" rpm="haveno-$version-1.x86_64.rpm" rpm64="Haveno-64bit-$version.rpm" cp "$linux64/$rpm" "$target_dir/$rpm64" exe="Haveno-$version.exe" exe64="Haveno-64bit-$version.exe" cp "$win64/$exe" "$target_dir/$exe64" rpi="jar-lib-for-raspberry-pi-$version.zip" cp "$deployDir/$rpi" "$target_dir/" cd "$target_dir" echo Create signatures gpg --digest-algo SHA256 --local-user $HAVENO_GPG_USER --output $dmg.asc --detach-sig --armor $dmg gpg --digest-algo SHA256 --local-user $HAVENO_GPG_USER --output $deb64.asc --detach-sig --armor $deb64 gpg --digest-algo SHA256 --local-user $HAVENO_GPG_USER --output $rpm64.asc --detach-sig --armor $rpm64 gpg --digest-algo SHA256 --local-user $HAVENO_GPG_USER --output $exe64.asc --detach-sig --armor $exe64 gpg --digest-algo SHA256 --local-user $HAVENO_GPG_USER --output $rpi.asc --detach-sig --armor $rpi echo Verify signatures gpg --digest-algo SHA256 --verify $dmg{.asc*,} gpg --digest-algo SHA256 --verify $deb64{.asc*,} gpg --digest-algo SHA256 --verify $rpm64{.asc*,} gpg --digest-algo SHA256 --verify $exe64{.asc*,} gpg --digest-algo SHA256 --verify $rpi{.asc*,} mkdir $win64/$version cp -r . $win64/$version open "." ================================================ FILE: desktop/package/macosx/insert_snapshot_version.sh ================================================ #!/bin/bash cd $(dirname $0)/../../../ version=0.0.1 find . -type f \( -name "finalize.sh" \ -o -name "create_app.sh" \ -o -name "build.gradle" \ -o -name "release.bat" \ -o -name "package.bat" \ -o -name "release.sh" \ -o -name "package.sh" \ -o -name "version.txt" \ -o -name "Dockerfile" \ \) -exec sed -i '' s/$version/"$version-SNAPSHOT"/ {} + ================================================ FILE: desktop/package/macosx/macos.entitlements ================================================ com.apple.security.cs.allow-jit com.apple.security.cs.allow-unsigned-executable-memory com.apple.security.cs.disable-executable-page-protection com.apple.security.cs.disable-library-validation com.apple.security.cs.allow-dyld-environment-variables ================================================ FILE: desktop/package/macosx/replace_version_number.sh ================================================ #!/bin/bash cd $(dirname $0)/../../../. oldVersion=1.6.2 newVersion=0.0.1 find . -type f \( -name "finalize.sh" \ -o -name "create_app.sh" \ -o -name "build.gradle" \ -o -name "release.bat" \ -o -name "package.bat" \ -o -name "release.sh" \ -o -name "package.sh" \ -o -name "version.txt" \ -o -name "Dockerfile" \ \) -exec sed -i '' s/"$oldVersion-SNAPSHOT"/$newVersion/ {} + find . -type f \( -name "Info.plist" \ -o -name "SeedNodeMain.java" \ -o -name "Version.java" \ -o -name "copy_dbs.sh" \ \) -exec sed -i '' s/$oldVersion/$newVersion/ {} + ================================================ FILE: desktop/package/package.gradle ================================================ import org.apache.tools.ant.taskdefs.condition.Os import java.time.LocalDateTime import java.util.regex.Pattern task jpackageSanityChecks { description 'Interactive sanity checks on the version of the code that will be packaged' doLast { if (!System.getenv("CI")) { executeCmd("git --no-pager log -5 --oneline") ant.input(message: "Above you see the current HEAD and its recent history.\n" + "Is this the right commit for packaging? (y=continue, n=abort)", addproperty: "sanity-check-1", validargs: "y,n") if (ant.properties['sanity-check-1'] == 'n') { ant.fail('Aborting') } executeCmd("git status --short --branch") ant.input(message: "Above you see any local changes that are not in the remote branch.\n" + "If you have any local changes, please abort, get them merged, get the latest branch and try again.\n" + "Continue with packaging? (y=continue, n=abort)", addproperty: "sanity-check-2", validargs: "y,n") if (ant.properties['sanity-check-2'] == 'n') { ant.fail('Aborting') } // TODO Evtl check programmatically in gradle (i.e. fail if below v11) executeCmd("java --version") ant.input(message: "Above you see the installed java version, which will be used to compile and build Haveno.\n" + "Is this java version ok for that? (y=continue, n=abort)", addproperty: "sanity-check-3", validargs: "y,n") if (ant.properties['sanity-check-3'] == 'n') { ant.fail('Aborting') } } else { println "CI environment detected, skipping interactive sanity checks" executeCmd("git --no-pager log -5 --oneline") executeCmd("git status --short --branch") executeCmd("java --version") } } } task getJavaBinariesDownloadURLs { description 'Find out which JDK will be used for jpackage and prepare to download it' dependsOn 'jpackageSanityChecks' doLast { // The build directory will be deleted next time the clean task runs // Therefore, we can use it to store any temp files (separate JDK for jpackage, etc) and resulting build artefacts // We create a temp folder in the build directory which holds all jpackage-related artefacts (not just the final installers) String tempRootDirName = 'temp-' + LocalDateTime.now().format('yyyy.MM.dd-HHmmssSSS') File tempRootDir = new File(project.buildDir, tempRootDirName) tempRootDir.mkdirs() ext.tempRootDir = tempRootDir println "Created temp root folder " + tempRootDir File binariesFolderPath = new File(tempRootDir, "binaries") binariesFolderPath.mkdirs() ext.binariesFolderPath = binariesFolderPath // Define the download URLs (and associated binary hashes) for the JDK used to package the installers // These JDKs are independent of what is installed on the building system // // If these specific versions are not hosted by AdoptOpenJDK anymore, or if different versions are desired, // simply update the links and associated hashes below // // See https://adoptopenjdk.net/releases.html?variant=openjdk15&jvmVariant=hotspot for latest download URLs // On the download page linked above, filter as follows to get the binary URL + associated SHA256: // - architecture: x64 // - operating system: // -- linux ( -> use the tar.gz JDK link) // -- macOS ( -> use the tar.gz JDK link) // -- windows ( -> use the .zip JDK link) Map jdk21Binaries = [ 'linux' : 'https://download.bell-sw.com/java/21.0.9+15/bellsoft-jdk21.0.9+15-linux-amd64-full.tar.gz', 'linux-sha256' : 'cada3343156c10dab634a7caca586941665af8f58a680664846adc2a27542968', 'linux-aarch64' : 'https://download.bell-sw.com/java/21.0.9+15/bellsoft-jdk21.0.9+15-linux-aarch64-full.tar.gz', 'linux-aarch64-sha256' : '09081d587a59f48d2900d62f3008f5a786a27932d68d64eeb4938aa715bf1ea8', 'mac' : 'https://download.bell-sw.com/java/21.0.9+15/bellsoft-jdk21.0.9+15-macos-amd64-full.tar.gz', 'mac-sha256' : 'f7ceb9743fe96da0e3b07f64aa23c6f54c39d134af01ed56869db0864c0cd13d', 'mac-aarch64' : 'https://download.bell-sw.com/java/21.0.9+15/bellsoft-jdk21.0.9+15-macos-aarch64-full.tar.gz', 'mac-aarch64-sha256' : '6af4be6c59c2ac4e2d89ee940e26f19ec09ea86fac0405058bc3d7f1f805b78c', 'windows' : 'https://download.bell-sw.com/java/21.0.9+15/bellsoft-jdk21.0.9+15-windows-amd64-full.zip', 'windows-sha256' : 'ff1260f6c234799cebeb4df08b68da3fd066ad6e4209c67dd3de7bc4fb7e9a35', 'windows-aarch64' : 'https://download.bell-sw.com/java/21.0.9+15/bellsoft-jdk21.0.9+15-windows-aarch64-full.zip', 'windows-aarch64-sha256': '5396566e494ebca5119681cb9961f69310b5b315d24ca1b4c897b71ebfc4fbed' ] String osKey String architecture = System.getProperty("os.arch").toLowerCase() if (Os.isFamily(Os.FAMILY_WINDOWS)) { if (architecture.contains("aarch64") || architecture.contains("arm")) { osKey = "windows-aarch64" } else { osKey = "windows" } } else if (Os.isFamily(Os.FAMILY_MAC)) { if (architecture.contains("aarch64") || architecture.contains("arm")) { osKey = "mac-aarch64" } else { osKey = "mac" } } else { if (architecture.contains("aarch64") || architecture.contains("arm")) { osKey = "linux-aarch64" } else { osKey = "linux" } } ext.osKey = osKey ext.jdk21Binary_DownloadURL = jdk21Binaries[osKey] ext.jdk21Binary_SHA256Hash = jdk21Binaries[osKey + '-sha256'] } } task retrieveAndExtractJavaBinaries { description 'Retrieve necessary Java binaries and extract them' dependsOn 'getJavaBinariesDownloadURLs' doLast { File tempRootDir = getJavaBinariesDownloadURLs.property("tempRootDir") as File // Folder where the jpackage JDK archive will be downloaded and extracted String jdkForJpackageDirName = "jdk-jpackage" File jdkForJpackageDir = new File(tempRootDir, jdkForJpackageDirName) jdkForJpackageDir.mkdirs() String jdkForJpackageArchiveURL = getJavaBinariesDownloadURLs.property('jdk21Binary_DownloadURL') String jdkForJpackageArchiveHash = getJavaBinariesDownloadURLs.property('jdk21Binary_SHA256Hash') String jdkForJpackageArchiveFileName = jdkForJpackageArchiveURL.tokenize('/').last() File jdkForJpackageFile = new File(jdkForJpackageDir, jdkForJpackageArchiveFileName) // Download necessary JDK binaries + verify hash ext.downloadAndVerifyArchive(jdkForJpackageArchiveURL, jdkForJpackageArchiveHash, jdkForJpackageFile) // Extract them String jpackageBinaryFileName if (Os.isFamily(Os.FAMILY_WINDOWS)) { ext.extractArchiveZip(jdkForJpackageFile, jdkForJpackageDir) jpackageBinaryFileName = 'jpackage.exe' } else { ext.extractArchiveTarGz(jdkForJpackageFile, jdkForJpackageDir) jpackageBinaryFileName = 'jpackage' } // Find jpackage in the newly extracted JDK // Don't rely on hardcoded paths to reach it, because the path depends on the version and platform jdkForJpackageDir.traverse(type: groovy.io.FileType.FILES, nameFilter: jpackageBinaryFileName) { println 'Using jpackage binary from ' + it ext.jpackageFilePath = it.path } } ext.downloadAndVerifyArchive = { String archiveURL, String archiveSHA256, File destinationArchiveFile -> println "Downloading ${archiveURL}" ant.get(src: archiveURL, dest: destinationArchiveFile) println 'Download saved to ' + destinationArchiveFile println 'Verifying checksum for downloaded binary ...' ant.jdkHash = archiveSHA256 ant.checksum(file: destinationArchiveFile, algorithm: 'SHA-256', property: '${jdkHash}', verifyProperty: 'hashMatches') if (ant.properties['hashMatches'] != 'true') { ant.fail('Checksum mismatch: Downloaded JDK binary has a different checksum than expected') } println 'Checksum verified' } ext.extractArchiveTarGz = { File tarGzFile, File destinationDir -> println "Extracting tar.gz ${tarGzFile}" // Gradle's tar extraction preserves permissions (crucial for jpackage to function correctly) copy { from tarTree(resources.gzip(tarGzFile)) into destinationDir } println "Extracted to ${destinationDir}" } ext.extractArchiveZip = { File zipFile, File destinationDir -> println "Extracting zip ${zipFile}..." ant.unzip(src: zipFile, dest: destinationDir) println "Extracted to ${destinationDir}" } } task packageInstallers { description 'Call jpackage to prepare platform-specific binaries for this platform' dependsOn 'retrieveAndExtractJavaBinaries' // Clean all previous artifacts and create a fresh shadowJar for the installers dependsOn rootProject.clean dependsOn ':core:havenoDeps' dependsOn ':desktop:shadowJar' doLast { String jPackageFilePath = retrieveAndExtractJavaBinaries.property('jpackageFilePath') File binariesFolderPath = file(getJavaBinariesDownloadURLs.property('binariesFolderPath')) File tempRootDir = getJavaBinariesDownloadURLs.property("tempRootDir") as File // The jpackageTempDir stores temp files used by jpackage for building the installers // It can be inspected in order to troubleshoot the packaging process File jpackageTempDir = new File(tempRootDir, "jpackage-temp") jpackageTempDir.mkdirs() // ALL contents of this folder will be included in the resulting installers // However, the fat jar is the only one we need // Therefore, this location should point to a folder that ONLY contains the fat jar // If later we will need to include other non-jar resources, we can do that by adding --resource-dir to the jpackage opts String fatJarFolderPath = "${project(':desktop').buildDir}/libs/fatJar" String mainJarName = shadowJar.getArchiveFileName().get() delete(fatJarFolderPath) mkdir(fatJarFolderPath) copy { from "${project(':desktop').buildDir}/libs/${mainJarName}" into fatJarFolderPath } // We convert the fat jar into a deterministic one by stripping out comments with date, etc. // jar file created from https://github.com/ManfredKarrer/tools executeCmd("java -jar \"${project(':desktop').projectDir}/package/tools-1.0.jar\" ${fatJarFolderPath}/${mainJarName}") // Store deterministic jar SHA-256 ant.checksum(file: "${fatJarFolderPath}/${mainJarName}", algorithm: 'SHA-256') copy { from "${fatJarFolderPath}/${mainJarName}.SHA-256" into binariesFolderPath } // TODO For non-modular applications: use jlink to create a custom runtime containing only the modules required // See jpackager argument documentation: // https://docs.oracle.com/en/java/javase/15/docs/specs/man/jpackage.html // Remove the -SNAPSHOT suffix from the version string (originally defined in build.gradle) // Having it in would have resulted in an invalid version property for several platforms (mac, linux/rpm) String appVersion = version.replaceAll("-SNAPSHOT", "") println "Packaging Haveno version ${appVersion}" // zip jar lib for Raspberry Pi only on macOS as there are path issues on Windows and it is only needed once // for the release if (Os.isFamily(Os.FAMILY_MAC)) { println "Zipping jar lib for raspberry pi" ant.zip(basedir: "${project(':desktop').buildDir}/app/lib", destfile: "${binariesFolderPath}/jar-lib-for-raspberry-pi-${appVersion}.zip") } //String appDescription = 'A decentralized monero exchange network.' String appCopyright = '© 2026 Haveno' String appNameAndVendor = 'Haveno' String commonOpts = new String( // Generic options " --dest \"${binariesFolderPath}\"" + " --name ${appNameAndVendor}" + //" --description \"${appDescription}\"" + // TODO: task managers show app description instead of name, so we disable it " --app-version ${appVersion}" + " --copyright \"${appCopyright}\"" + " --vendor ${appNameAndVendor}" + " --temp \"${jpackageTempDir}\"" + // Options for creating the application image " --input ${fatJarFolderPath}" + // Options for creating the application launcher " --main-jar ${mainJarName}" + " --main-class haveno.desktop.app.HavenoAppMain" + " --java-options -Xss1280k" + " --java-options -XX:MaxRAM=4g" + " --java-options --add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED" + " --java-options --add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED" + " --java-options --add-opens=java.base/java.lang.reflect=ALL-UNNAMED" + " --java-options --add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED" + " --java-options -Djava.net.preferIPv4Stack=true" + " --arguments --baseCurrencyNetwork=XMR_MAINNET" // Warning: this will cause guice reflection exceptions and lead to issues with the guice internal cache // resulting in the UI not loading // " --java-options -Djdk.module.illegalAccess=deny" + ) if (Os.isFamily(Os.FAMILY_WINDOWS)) { String windowsOpts = new String( " --icon \"${project(':desktop').projectDir}/package/windows/Haveno.ico\"" + " --resource-dir \"${project(':desktop').projectDir}/package/windows\"" + " --win-dir-chooser" + " --win-per-user-install" + " --win-menu" + " --win-shortcut" ) executeCmd(jPackageFilePath + commonOpts + windowsOpts + " --verbose > desktop/build/output.txt --type exe") } else if (Os.isFamily(Os.FAMILY_MAC)) { // See https://docs.oracle.com/en/java/javase/14/jpackage/override-jpackage-resources.html // for details of "--resource-dir" String macOpts = new String( " --resource-dir \"${project(':desktop').projectDir}/package/macosx\"" ) executeCmd(jPackageFilePath + commonOpts + macOpts + " --type dmg") } else { String linuxOpts = new String( " --icon ${project(':desktop').projectDir}/package/linux/icon.png" + // This defines the first part of the resulting packages (the application name) // deb requires lowercase letters, therefore the application name is written in lowercase " --linux-package-name haveno" + // This represents the linux package version (revision) // By convention, this is part of the deb/rpm package names, in addition to the software version " --linux-app-release 1" + " --linux-menu-group Network" + " --linux-shortcut" ) // Package deb executeCmd(jPackageFilePath + commonOpts + linuxOpts + " --resource-dir \"${project(':desktop').projectDir}/package/linux/jpackage.deb\"" + " --linux-deb-maintainer noreply@haveno.exchange" + " --type deb") // Clean jpackage temp folder, needs to be empty for the next packaging step (AppImage) jpackageTempDir.deleteDir() jpackageTempDir.mkdirs() executeCmd(jPackageFilePath + commonOpts + " --dest \"${jpackageTempDir}\"" + " --type app-image") // Path to the app-image directory: THIS IS NOT THE ACTUAL .AppImage FILE. // See JPackage documentation on --type app-image for more. String appImagePath = new String( "\"${binariesFolderPath}/${appNameAndVendor}\"" ) // Download AppImageTool Map AppImageToolBinaries = [ 'linux' : "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage", 'linux-aarch64' : "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-aarch64.AppImage", ] String osKey = getJavaBinariesDownloadURLs.property('osKey') File appDir = new File("${jpackageTempDir}/Haveno") File templateAppDir = new File("${project(':desktop').projectDir}/package/linux/Haveno.AppDir") File jpackDir = appDir appDir.mkdirs() File AppImageToolBinary = new File("${jpackageTempDir}/appimagetool.AppImage") // Adding a platform to the AppImageToolBinaries essentially adds it to the "supported" list of platforms able to make AppImages // However, be warned that any platform that doesn't support unix `ln` and `chmod` will not work with the current method. if (AppImageToolBinaries.containsKey(osKey)) { println "Downloading ${AppImageToolBinaries[osKey]}" ant.get(src: AppImageToolBinaries[osKey], dest: AppImageToolBinary) println 'Download saved to ' + jpackageTempDir project.exec { commandLine('chmod', '+x', AppImageToolBinary) } copy { from templateAppDir into appDir boolean includeEmptyDirs = true } project.exec { workingDir appDir commandLine 'ln', '-s', 'bin/Haveno', 'AppRun' } project.exec { commandLine "${AppImageToolBinary}", appDir, "${binariesFolderPath}/haveno_${appVersion}.AppImage" } } else { println "Your platform does not support AppImageTool ${AppImageToolVersion}" } // Clean jpackage temp folder, needs to be empty for the next packaging step (rpm) jpackageTempDir.deleteDir() jpackageTempDir.mkdirs() // Package rpm executeCmd(jPackageFilePath + commonOpts + linuxOpts + " --linux-rpm-license-type AGPLv3" + // https://fedoraproject.org/wiki/Licensing:Main?rd=Licensing#Good_Licenses " --type rpm") // Define Flatpak-related properties String flatpakManifestFile = 'package/linux/exchange.haveno.Haveno.yml' String linuxDir = 'package/linux' String flatpakOutputDir = 'package/linux/build' String flatpakExportDir = "${binariesFolderPath}/fpexport" String flatpakBundleFile = "${binariesFolderPath}/haveno.flatpak" // Read the default app name from the HavenoExecutable.java file def filer = file('../core/src/main/java/haveno/core/app/HavenoExecutable.java') def content = filer.text def matcher = Pattern.compile(/public static final String DEFAULT_APP_NAME = "(.*?)";/).matcher(content) def defaultAppName = "Haveno" if (matcher.find()) { defaultAppName = matcher.group(1) } else { throw new GradleException("DEFAULT_APP_NAME not found in HavenoExecutable.java") } // Copy the manifest to a new tmp one in the same place // and add a --filesystem=.local/share/${name} to the flatpak manifest def manifest = file(flatpakManifestFile) def newManifest = file('exchange.haveno.Haveno.yaml') newManifest.write(manifest.text.replace("- --share=network", "- --share=network\n - --filesystem=~/.local/share/${defaultAppName}:create")) flatpakManifestFile = 'exchange.haveno.Haveno.yaml' // Command to build the Flatpak exec { commandLine 'flatpak-builder', '--force-clean', flatpakOutputDir, flatpakManifestFile, '--user', '--install-deps-from=flathub' } // Command to export the Flatpak exec { commandLine 'flatpak', 'build-export', flatpakExportDir, flatpakOutputDir } // Command to create the Flatpak bundle exec { commandLine 'flatpak', 'build-bundle', flatpakExportDir, flatpakBundleFile, 'exchange.haveno.Haveno', '--runtime-repo=https://flathub.org/repo/flathub.flatpakrepo' } // delete the flatpak build directory delete(flatpakOutputDir) delete(flatpakExportDir) delete(flatpakManifestFile) println "Flatpak package created at ${flatpakBundleFile}" } // Env variable can be set by calling "export HAVENO_SHARED_FOLDER='Some value'" // This is to copy the final binary/ies to a shared folder for further processing if a VM is used. String envVariableSharedFolder = "$System.env.HAVENO_SHARED_FOLDER" println "Environment variable HAVENO_SHARED_FOLDER is: ${envVariableSharedFolder}" if (envVariableSharedFolder != "null") { ant.input(message: "Copy the created binary to a shared folder? (y=yes, n=no)", addproperty: "copy-to-shared-folder", validargs: "y,n") if (ant.properties['copy-to-shared-folder'] == 'y') { copy { from binariesFolderPath into envVariableSharedFolder } executeCmd("open " + envVariableSharedFolder) } } println "The binaries are ready:" binariesFolderPath.traverse { println it.path } } } def executeCmd(String cmd) { String shell String shellArg if (Os.isFamily(Os.FAMILY_WINDOWS)) { shell = 'cmd' shellArg = '/c' } else { shell = 'bash' shellArg = '-c' } println "Executing command:\n${cmd}\n" // See "Executing External Processes" section of // http://docs.groovy-lang.org/next/html/documentation/ def commands = [shell, shellArg, cmd] def process = commands.execute(null, project.rootDir) def result if (process.waitFor() == 0) { result = process.text println "Command output (stdout):\n${result}" } else { result = process.err.text println "Command output (stderr):\n${result}" } return result } ================================================ FILE: desktop/package/signingkey.asc ================================================ 29CDFD3B ================================================ FILE: desktop/package/windows/main.wxs ================================================ 1 1 1 1 !(loc.message.install.dir.exist) 1 INSTALLDIR_VALID="0" INSTALLDIR_VALID="1" WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed Not Installed JP_UPGRADABLE_FOUND JP_DOWNGRADABLE_FOUND ================================================ FILE: desktop/package/windows/overrides.wxi ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/CandleStickChart.css ================================================ /* * Copyright (c) 2008, 2013 Oracle and/or its affiliates. * All rights reserved. Use is subject to license terms. * * This file is available and licensed under the following license: * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the distribution. * - Neither the name of Oracle Corporation nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* ====== CANDLE STICK CHART =========================================================== */ .candlestick-tooltip-label { -fx-font-size: 0.75em; -fx-font-weight: bold; -fx-text-fill: #666666; -fx-padding: 2 5 2 0; } .candlestick-average-line { -fx-stroke: -bs-candle-stick-average-line; -fx-stroke-width: 1px; } .candlestick-line { -stick-line-fill: -bs-sell; -fx-stroke: -stick-line-fill; -fx-stroke-width: 1px; } .candlestick-line.close-above-open { -stick-line-fill: -bs-candle-stick-won; } .candlestick-line.open-above-close { -stick-line-fill: -bs-candle-stick-loss; } .candlestick-bar { -fx-padding: 5; -demo-bar-fill: -bs-sell; -fx-background-color: -demo-bar-fill; -fx-background-insets: 0; -fx-background-radius: 2px; -fx-border-radius: 2px; } .candlestick-bar.close-above-open { -demo-bar-fill: -bs-candle-stick-won; } .candlestick-bar.open-above-close { -demo-bar-fill: -bs-candle-stick-loss; } .candlestick-bar.empty { -demo-bar-fill: #cccccc; } .volume-bar { -fx-padding: 5; -fx-background-color: -bs-volume-transparent; -fx-background-insets: 0; -fx-background-radius: 2px; -fx-border-radius: 2px; } .chart-alternative-row-fill { -fx-fill: transparent; -fx-stroke: transparent; -fx-stroke-width: 0; } .chart-plot-background { -fx-background-color: -bs-background-color; } ================================================ FILE: desktop/src/main/java/haveno/desktop/DesktopModule.java ================================================ /* * 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 . */ package haveno.desktop; import com.google.inject.Singleton; import com.google.inject.name.Names; import haveno.common.app.AppModule; import haveno.common.config.Config; import haveno.core.locale.Res; import haveno.desktop.common.fxml.FxmlViewLoader; import haveno.desktop.common.view.ViewFactory; import haveno.desktop.common.view.ViewLoader; import haveno.desktop.common.view.guice.InjectorViewFactory; import java.util.ResourceBundle; import static haveno.common.config.Config.APP_NAME; public class DesktopModule extends AppModule { public DesktopModule(Config config) { super(config); } @Override protected void configure() { bind(ViewFactory.class).to(InjectorViewFactory.class); bind(ResourceBundle.class).toInstance(Res.getResourceBundle()); bind(ViewLoader.class).to(FxmlViewLoader.class).in(Singleton.class); bindConstant().annotatedWith(Names.named(APP_NAME)).to(config.appName); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/Navigation.java ================================================ /* * 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 . */ package haveno.desktop; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.persistable.NavigationPath; import haveno.common.proto.persistable.PersistedDataHost; import haveno.desktop.common.view.View; import haveno.desktop.common.view.ViewPath; import haveno.desktop.main.MainView; import haveno.desktop.main.market.MarketView; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.CopyOnWriteArraySet; import java.util.stream.Collectors; import javax.annotation.Nullable; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton public final class Navigation implements PersistedDataHost { private static final ViewPath DEFAULT_VIEW_PATH = ViewPath.to(MainView.class, MarketView.class); public interface Listener { void onNavigationRequested(ViewPath path, @Nullable Object data); } // New listeners can be added during iteration so we use CopyOnWriteArrayList to // prevent invalid array modification private final CopyOnWriteArraySet listeners = new CopyOnWriteArraySet<>(); private final PersistenceManager persistenceManager; private ViewPath currentPath; // Used for returning to the last important view. After setup is done we want to // return to the last opened view (e.g. sell/buy) private ViewPath returnPath; // this string is updated just before saving to disk so it reflects the latest currentPath situation. private final NavigationPath navigationPath = new NavigationPath(); // Persisted fields @Getter @Setter private ViewPath previousPath = DEFAULT_VIEW_PATH; @Inject public Navigation(PersistenceManager persistenceManager) { this.persistenceManager = persistenceManager; persistenceManager.initialize(navigationPath, PersistenceManager.Source.PRIVATE_LOW_PRIO); } @Override public void readPersisted(Runnable completeHandler) { persistenceManager.readPersisted(persisted -> { List> viewClasses = persisted.getPath().stream() .map(className -> { try { return (Class) Class.forName(className).asSubclass(View.class); } catch (ClassNotFoundException e) { log.warn("Could not find the viewPath class {}; exception: {}", className, e); } return null; }) .filter(Objects::nonNull) .collect(Collectors.toList()); if (!viewClasses.isEmpty()) { previousPath = new ViewPath(viewClasses); } completeHandler.run(); }, completeHandler); } @SafeVarargs public final void navigateTo(Class... viewClasses) { navigateTo(ViewPath.to(viewClasses), null); } @SafeVarargs public final void navigateToWithData(Object data, Class... viewClasses) { navigateTo(ViewPath.to(viewClasses), data); } public void navigateTo(ViewPath newPath, @Nullable Object data) { if (newPath == null) return; ArrayList> temp = new ArrayList<>(); for (int i = 0; i < newPath.size(); i++) { Class viewClass = newPath.get(i); temp.add(viewClass); if (currentPath == null || (currentPath.size() > i && viewClass != currentPath.get(i) && i != newPath.size() - 1)) { ArrayList> temp2 = new ArrayList<>(temp); for (int n = i + 1; n < newPath.size(); n++) { //noinspection unchecked Class[] newTemp = new Class[i + 1]; currentPath = ViewPath.to(temp2.toArray(newTemp)); navigateTo(currentPath, data); viewClass = newPath.get(n); temp2.add(viewClass); } } } currentPath = newPath; previousPath = currentPath; listeners.forEach((e) -> e.onNavigationRequested(currentPath, data)); requestPersistence(); } private void requestPersistence() { if (currentPath.tip() != null) { navigationPath.setPath(currentPath.stream().map(Class::getName).collect(Collectors.toUnmodifiableList())); } persistenceManager.requestPersistence(); } public void navigateToPreviousVisitedView() { if (previousPath == null || previousPath.size() == 0) previousPath = DEFAULT_VIEW_PATH; navigateTo(previousPath, null); } public void addListener(Listener listener) { listeners.add(listener); } public void removeListener(Listener listener) { listeners.remove(listener); } public ViewPath getReturnPath() { return returnPath; } public ViewPath getCurrentPath() { return currentPath; } public void setReturnPath(ViewPath returnPath) { this.returnPath = returnPath; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/app/HavenoApp.java ================================================ /* * 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 . */ package haveno.desktop.app; import static haveno.desktop.util.Layout.INITIAL_WINDOW_HEIGHT; import static haveno.desktop.util.Layout.INITIAL_WINDOW_WIDTH; import static haveno.desktop.util.Layout.MIN_WINDOW_HEIGHT; import static haveno.desktop.util.Layout.MIN_WINDOW_WIDTH; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import com.google.common.base.Joiner; import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.name.Names; import haveno.common.app.DevEnv; import haveno.common.app.Log; import haveno.common.config.Config; import haveno.common.crypto.Hash; import haveno.common.setup.GracefulShutDownHandler; import haveno.common.setup.UncaughtExceptionHandler; import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.core.offer.OpenOfferManager; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.mediation.MediationManager; import haveno.core.support.dispute.refund.RefundManager; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.user.Cookie; import haveno.core.user.CookieKey; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.xmr.wallet.WalletsManager; import haveno.desktop.common.view.CachingViewLoader; import haveno.desktop.common.view.View; import haveno.desktop.common.view.ViewLoader; import haveno.desktop.main.MainView; import haveno.desktop.main.debug.DebugView; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.FilterWindow; import haveno.desktop.main.overlays.windows.SendAlertMessageWindow; import haveno.desktop.main.overlays.windows.ShowWalletDataWindow; import haveno.desktop.util.CssTheme; import haveno.desktop.util.DisplayUtils; import haveno.desktop.util.ImageUtil; import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import javafx.application.Application; import javafx.geometry.BoundingBox; import javafx.geometry.Rectangle2D; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.stage.Modality; import javafx.stage.Screen; import javafx.stage.Stage; import javafx.stage.StageStyle; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.slf4j.LoggerFactory; @Slf4j public class HavenoApp extends Application implements UncaughtExceptionHandler { @Setter private static Consumer appLaunchedHandler; @Getter private static Runnable shutDownHandler; @Setter private static Runnable onGracefulShutDownHandler; @Setter private Injector injector; @Setter private GracefulShutDownHandler gracefulShutDownHandler; private Stage stage; private boolean popupOpened; private Scene scene; private boolean shutDownRequested; private MainView mainView; public HavenoApp() { shutDownHandler = this::stop; } /////////////////////////////////////////////////////////////////////////////////////////// // JavaFx Application implementation /////////////////////////////////////////////////////////////////////////////////////////// // NOTE: This method is not called on the JavaFX Application Thread. @Override public void init() { } @Override public void start(Stage stage) { this.stage = stage; appLaunchedHandler.accept(this); } public void startApplication(Runnable onApplicationStartedHandler) { log.info("Starting application"); try { mainView = loadMainView(injector); mainView.setOnApplicationStartedHandler(onApplicationStartedHandler); scene = createAndConfigScene(mainView, injector); setupStage(scene); } catch (Throwable throwable) { log.error("Error during app init", throwable); handleUncaughtException(throwable, false); } } @Override public void stop() { if (!shutDownRequested) { new Popup().headLine(Res.get("popup.shutDownInProgress.headline")) .backgroundInfo(Res.get("popup.shutDownInProgress.msg")) .hideCloseButton() .useAnimation(false) .show(); new Thread(() -> { gracefulShutDownHandler.gracefulShutDown(() -> { log.info("App shutdown complete"); if (onGracefulShutDownHandler != null) onGracefulShutDownHandler.run(); }); }).start(); shutDownRequested = true; } } /////////////////////////////////////////////////////////////////////////////////////////// // UncaughtExceptionHandler implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void handleUncaughtException(Throwable throwable, boolean doShutDown) { if (!shutDownRequested) { if (scene == null) { log.warn("Scene not available yet, we create a new scene. The bug might be caused by an exception in a constructor or by a circular dependency in Guice. throwable=" + throwable.toString()); scene = new Scene(new StackPane(), 1000, 650); CssTheme.loadSceneStyles(scene, CssTheme.CSS_THEME_LIGHT, false); stage.setScene(scene); stage.show(); } try { try { if (!popupOpened) { popupOpened = true; new Popup().error(Objects.requireNonNullElse(throwable.getMessage(), throwable.toString())) .onClose(() -> popupOpened = false) .show(); } } catch (Throwable throwable3) { log.error("Error at displaying Throwable."); throwable3.printStackTrace(); } if (doShutDown) stop(); } catch (Throwable throwable2) { // If printStackTrace cause a further exception we don't pass the throwable to the Popup. log.error(throwable2.toString()); if (doShutDown) stop(); } } } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private Scene createAndConfigScene(MainView mainView, Injector injector) { //Rectangle maxWindowBounds = new Rectangle(); Rectangle2D maxWindowBounds = new Rectangle2D(0, 0, 0, 0); try { maxWindowBounds = Screen.getPrimary().getBounds(); } catch (IllegalArgumentException e) { // Multi-screen environments may encounter IllegalArgumentException (Window must not be zero) // Just ignore the exception and continue, which means the window will use the minimum window size below // since we are unable to determine if we can use a larger size } Scene scene = new Scene(mainView.getRoot(), maxWindowBounds.getWidth() < INITIAL_WINDOW_WIDTH ? Math.max(maxWindowBounds.getWidth(), MIN_WINDOW_WIDTH) : INITIAL_WINDOW_WIDTH, maxWindowBounds.getHeight() < INITIAL_WINDOW_HEIGHT ? Math.max(maxWindowBounds.getHeight(), MIN_WINDOW_HEIGHT) : INITIAL_WINDOW_HEIGHT); addSceneKeyEventHandler(scene, injector); Preferences preferences = injector.getInstance(Preferences.class); var config = injector.getInstance(Config.class); preferences.getCssThemeProperty().addListener((ov) -> { CssTheme.loadSceneStyles(scene, preferences.getCssTheme(), config.useDevModeHeader); }); CssTheme.loadSceneStyles(scene, preferences.getCssTheme(), config.useDevModeHeader); // set initial background color scene.setFill(CssTheme.isDarkTheme() ? Color.BLACK : Color.WHITE); return scene; } private void setupStage(Scene scene) { stage.setOnCloseRequest(event -> { event.consume(); shutDownByUser(); }); // configure the primary stage String appName = injector.getInstance(Key.get(String.class, Names.named(Config.APP_NAME))); List postFixes = new ArrayList<>(); if (!Config.baseCurrencyNetwork().isMainnet()) { postFixes.add(Config.baseCurrencyNetwork().name()); } if (injector.getInstance(Config.class).useLocalhostForP2P) { postFixes.add("LOCALHOST"); } if (injector.getInstance(Config.class).useDevMode) { postFixes.add("DEV MODE"); } if (!postFixes.isEmpty()) { appName += " [" + Joiner.on(", ").join(postFixes) + "]"; } stage.setTitle(appName); stage.setScene(scene); stage.setMinWidth(MIN_WINDOW_WIDTH); stage.setMinHeight(MIN_WINDOW_HEIGHT); stage.getIcons().add(ImageUtil.getApplicationIconImage()); User user = injector.getInstance(User.class); layoutStageFromPersistedData(stage, user); addStageLayoutListeners(stage, user); // make the UI visible stage.show(); } private void layoutStageFromPersistedData(Stage stage, User user) { Cookie cookie = user.getCookie(); cookie.getAsOptionalDouble(CookieKey.STAGE_X).flatMap(x -> cookie.getAsOptionalDouble(CookieKey.STAGE_Y).flatMap(y -> cookie.getAsOptionalDouble(CookieKey.STAGE_W).flatMap(w -> cookie.getAsOptionalDouble(CookieKey.STAGE_H).map(h -> new BoundingBox(x, y, w, h))))) .ifPresent(stageBoundingBox -> { stage.setX(stageBoundingBox.getMinX()); stage.setY(stageBoundingBox.getMinY()); stage.setWidth(stageBoundingBox.getWidth()); stage.setHeight(stageBoundingBox.getHeight()); }); } private void addStageLayoutListeners(Stage stage, User user) { stage.widthProperty().addListener((observable, oldValue, newValue) -> { user.getCookie().putAsDouble(CookieKey.STAGE_W, (double) newValue); user.requestPersistence(); }); stage.heightProperty().addListener((observable, oldValue, newValue) -> { user.getCookie().putAsDouble(CookieKey.STAGE_H, (double) newValue); user.requestPersistence(); }); stage.xProperty().addListener((observable, oldValue, newValue) -> { user.getCookie().putAsDouble(CookieKey.STAGE_X, (double) newValue); user.requestPersistence(); }); stage.yProperty().addListener((observable, oldValue, newValue) -> { user.getCookie().putAsDouble(CookieKey.STAGE_Y, (double) newValue); user.requestPersistence(); }); } private MainView loadMainView(Injector injector) { CachingViewLoader viewLoader = injector.getInstance(CachingViewLoader.class); return (MainView) viewLoader.load(MainView.class); } private void addSceneKeyEventHandler(Scene scene, Injector injector) { scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEvent -> { if (Utilities.isCtrlPressed(KeyCode.W, keyEvent) || Utilities.isCtrlPressed(KeyCode.Q, keyEvent)) { shutDownByUser(); } else { if (Utilities.isAltOrCtrlPressed(KeyCode.M, keyEvent)) { injector.getInstance(SendAlertMessageWindow.class).show(); } else if (Utilities.isAltOrCtrlPressed(KeyCode.F, keyEvent)) { injector.getInstance(FilterWindow.class).show(); } else if (Utilities.isAltOrCtrlPressed(KeyCode.T, keyEvent)) { // Toggle between show tor logs and only show warnings. Helpful in case of connection problems String pattern = "org.berndpruenster.netlayer"; Level logLevel = ((Logger) LoggerFactory.getLogger(pattern)).getLevel(); if (logLevel != Level.DEBUG) { log.info("Set log level for org.berndpruenster.netlayer classes to DEBUG"); Log.setCustomLogLevel(pattern, Level.DEBUG); } else { log.info("Set log level for org.berndpruenster.netlayer classes to WARN"); Log.setCustomLogLevel(pattern, Level.WARN); } } else if (Utilities.isAltOrCtrlPressed(KeyCode.J, keyEvent)) { WalletsManager walletsManager = injector.getInstance(WalletsManager.class); if (walletsManager.areWalletsAvailable()) new ShowWalletDataWindow(walletsManager).show(); else new Popup().warning(Res.get("popup.warning.walletNotInitialized")).show(); } else if (DevEnv.isDevMode()) { if (Utilities.isAltOrCtrlPressed(KeyCode.Z, keyEvent)) showDebugWindow(scene, injector); } } }); } private void shutDownByUser() { promptUserAtShutdown().thenAccept(okToShutDown -> { if (okToShutDown) { stop(); } }); } private CompletableFuture promptUserAtShutdown() { final CompletableFuture resp = new CompletableFuture<>(); // check for trade or dispute issues String issues = checkTradesAtShutdown() + checkDisputesAtShutdown(); if (issues.length() > 0) { String key = Utilities.encodeToHex(Hash.getSha256Hash(issues)); if (injector.getInstance(Preferences.class).showAgain(key) && !DevEnv.isDevMode()) { new Popup().warning(issues) .actionButtonText(Res.get("shared.okWait")) .onAction(() -> resp.complete(false)) .closeButtonText(Res.get("shared.closeAnywayDanger")) .onClose(() -> resp.complete(true)) .dontShowAgainId(key) .width(800) .show(); return resp; } } // check for open offers if (injector.getInstance(OpenOfferManager.class).hasAvailableOpenOffers()) { String key = "showOpenOfferWarnPopupAtShutDown"; if (injector.getInstance(Preferences.class).showAgain(key) && !DevEnv.isDevMode()) { new Popup().warning(Res.get("popup.info.shutDownWithOpenOffers")) .actionButtonText(Res.get("shared.shutDown")) .onAction(() -> resp.complete(true)) .closeButtonText(Res.get("shared.cancel")) .onClose(() -> resp.complete(false)) .dontShowAgainId(key) .show(); return resp; } } // if no warning popup has been shown yet, prompt user if they really intend to shut down String key = "popup.info.shutDownQuery"; if (injector.getInstance(Preferences.class).showAgain(key) && !DevEnv.isDevMode()) { new Popup().headLine(Res.get(key)) .actionButtonText(Res.get("shared.yes")) .onAction(() -> resp.complete(true)) .closeButtonText(Res.get("shared.no")) .onClose(() -> resp.complete(false)) .dontShowAgainId(key) .show(); } else { resp.complete(true); } return resp; } private String checkTradesAtShutdown() { log.info("Checking trades at shutdown"); Instant fiveMinutesAgo = Instant.ofEpochSecond(Instant.now().getEpochSecond() - TimeUnit.MINUTES.toSeconds(5)); for (Trade trade : injector.getInstance(TradeManager.class).getObservableList()) { if (trade.getPhase().equals(Trade.Phase.DEPOSIT_REQUESTED) && trade.getTakeOfferDate().toInstant().isAfter(fiveMinutesAgo)) { String tradeDateString = DisplayUtils.formatDateTime(trade.getTakeOfferDate()); String tradeInfo = Res.get("shared.tradeId") + ": " + trade.getShortId() + " " + Res.get("shared.dateTime") + ": " + tradeDateString; return Res.get("popup.info.shutDownWithTradeInit", tradeInfo) + System.lineSeparator() + System.lineSeparator(); } } return ""; } private String checkDisputesAtShutdown() { log.info("Checking disputes at shutdown"); if (injector.getInstance(ArbitrationManager.class).hasPendingMessageAtShutdown() || injector.getInstance(MediationManager.class).hasPendingMessageAtShutdown() || injector.getInstance(RefundManager.class).hasPendingMessageAtShutdown()) { return Res.get("popup.info.shutDownWithDisputeInit") + System.lineSeparator() + System.lineSeparator(); } return ""; } // Used for debugging trade process private void showDebugWindow(Scene scene, Injector injector) { ViewLoader viewLoader = injector.getInstance(ViewLoader.class); View debugView = viewLoader.load(DebugView.class); Parent parent = (Parent) debugView.getRoot(); Stage stage = new Stage(); stage.setScene(new Scene(parent)); stage.setTitle("Debug window"); // Don't translate, just for dev stage.initModality(Modality.NONE); stage.initStyle(StageStyle.UTILITY); stage.initOwner(scene.getWindow()); stage.setX(this.stage.getX() + this.stage.getWidth() + 10); stage.setY(this.stage.getY()); stage.show(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java ================================================ /* * 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 . */ package haveno.desktop.app; import haveno.common.UserThread; import haveno.common.app.AppModule; import haveno.common.app.Version; import haveno.common.crypto.IncorrectPasswordException; import haveno.core.app.AvoidStandbyModeService; import haveno.core.app.HavenoExecutable; import haveno.core.locale.Res; import haveno.desktop.common.UITimer; import haveno.desktop.common.view.guice.InjectorViewFactory; import haveno.desktop.setup.DesktopPersistedDataHost; import haveno.desktop.util.ImageUtil; import javafx.application.Application; import javafx.application.Platform; import javafx.geometry.Pos; import javafx.scene.control.ButtonBar; import javafx.scene.control.ButtonType; import javafx.scene.control.Dialog; import javafx.scene.control.Label; import javafx.scene.control.PasswordField; import javafx.scene.image.ImageView; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.stage.Stage; import javafx.stage.Window; import lombok.extern.slf4j.Slf4j; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @Slf4j public class HavenoAppMain extends HavenoExecutable { private HavenoApp application; public HavenoAppMain() { super("Haveno Desktop", "haveno-desktop", HavenoExecutable.DEFAULT_APP_NAME, Version.VERSION); } public static void main(String[] args) { // For some reason the JavaFX launch process results in us losing the thread // context class loader: reset it. In order to work around a bug in JavaFX 8u25 // and below, you must include the following code as the first line of your // realMain method: Thread.currentThread().setContextClassLoader(HavenoAppMain.class.getClassLoader()); new HavenoAppMain().execute(args); } @Override public void onSetupComplete() { log.debug("onSetupComplete"); } /////////////////////////////////////////////////////////////////////////////////////////// // First synchronous execution tasks /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void configUserThread() { UserThread.setExecutor(Platform::runLater); UserThread.setTimerClass(UITimer.class); } @Override protected void launchApplication() { HavenoApp.setAppLaunchedHandler(application -> { HavenoAppMain.this.application = (HavenoApp) application; // Map to user thread! UserThread.execute(this::onApplicationLaunched); }); Application.launch(HavenoApp.class); } /////////////////////////////////////////////////////////////////////////////////////////// // As application is a JavaFX application we need to wait for onApplicationLaunched /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void onApplicationLaunched() { super.onApplicationLaunched(); application.setGracefulShutDownHandler(this); } @Override public void handleUncaughtException(Throwable throwable, boolean doShutDown) { application.handleUncaughtException(throwable, doShutDown); } /////////////////////////////////////////////////////////////////////////////////////////// // We continue with a series of synchronous execution tasks /////////////////////////////////////////////////////////////////////////////////////////// @Override protected AppModule getModule() { return new HavenoAppModule(config); } @Override protected void applyInjector() { super.applyInjector(); application.setInjector(injector); injector.getInstance(InjectorViewFactory.class).setInjector(injector); } @Override protected void readAllPersisted(Runnable completeHandler) { super.readAllPersisted(DesktopPersistedDataHost.getPersistedDataHosts(injector), completeHandler); } @Override protected void setupAvoidStandbyMode() { injector.getInstance(AvoidStandbyModeService.class).init(); } @Override protected void startApplication() { // We need to be in user thread! We mapped at launchApplication already. Once // the UI is ready we get onApplicationStarted called and start the setup there. application.startApplication(this::onApplicationStarted); } @Override protected void onApplicationStarted() { super.onApplicationStarted(); // Relevant to have this in the logs, for support cases // This can only be called after JavaFX is initialized, otherwise the version logged will be null // Therefore, calling this as part of onApplicationStarted() log.info("Using JavaFX {}", System.getProperty("javafx.version")); } @Override protected CompletableFuture loginAccount() { // attempt default login CompletableFuture result = super.loginAccount(); try { if (result.get()) return result; } catch (InterruptedException | ExecutionException e) { throw new IllegalStateException(e); } // login using dialog CompletableFuture dialogResult = new CompletableFuture<>(); Platform.setImplicitExit(false); Platform.runLater(() -> { // show password dialog until account open String errorMessage = null; while (!accountService.isAccountOpen()) { // create the password dialog PasswordDialog passwordDialog = new PasswordDialog(errorMessage); // wait for user to enter password Optional passwordResult = passwordDialog.showAndWait(); if (passwordResult.isPresent()) { try { accountService.openAccount(passwordResult.get()); dialogResult.complete(accountService.isAccountOpen()); } catch (IncorrectPasswordException e) { errorMessage = "Incorrect password"; } } else { // if the user cancelled the dialog, complete the passwordFuture exceptionally dialogResult.completeExceptionally(new Exception("Password dialog cancelled")); break; } } }); return dialogResult; } private class PasswordDialog extends Dialog { public PasswordDialog(String errorMessage) { setTitle("Enter Password"); setHeaderText("Please enter your Haveno password:"); // Add an icon to the dialog Stage stage = (Stage) getDialogPane().getScene().getWindow(); stage.getIcons().add(ImageUtil.getImageByPath("lock@2x.png")); // Create the password field PasswordField passwordField = new PasswordField(); passwordField.setPromptText("Password"); // Create the error message field Label errorMessageField = new Label(errorMessage); errorMessageField.setTextFill(Color.color(1, 0, 0)); // Create the version field Label versionField = new Label(Res.get("mainView.footer.version", Version.VERSION)); // Set the dialog content VBox vbox = new VBox(10); ImageView logoImageView = new ImageView(ImageUtil.getImageByPath("logo_splash_light_mode.png")); logoImageView.setFitWidth(342); logoImageView.setPreserveRatio(true); vbox.getChildren().addAll(logoImageView, passwordField, errorMessageField, versionField); vbox.setAlignment(Pos.TOP_CENTER); getDialogPane().setContent(vbox); // Add OK and Cancel buttons ButtonType okButton = new ButtonType("OK", ButtonBar.ButtonData.OK_DONE); ButtonType cancelButton = new ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); getDialogPane().getButtonTypes().addAll(okButton, cancelButton); // Convert the result to a string when the OK button is clicked setResultConverter(buttonType -> { if (buttonType == okButton) { return passwordField.getText(); } else { new Thread(() -> HavenoApp.getShutDownHandler().run()).start(); return null; } }); // Focus the password field when dialog is shown Window window = getDialogPane().getScene().getWindow(); if (window instanceof Stage) { Stage dialogStage = (Stage) window; dialogStage.focusedProperty().addListener((observable, oldValue, newValue) -> { if (newValue) { passwordField.requestFocus(); } }); } } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/app/HavenoAppModule.java ================================================ /* * 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 . */ package haveno.desktop.app; import haveno.common.app.AppModule; import haveno.common.config.Config; import haveno.core.app.CoreModule; import haveno.desktop.DesktopModule; public class HavenoAppModule extends AppModule { public HavenoAppModule(Config config) { super(config); } @Override protected void configure() { install(new CoreModule(config)); install(new DesktopModule(config)); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/common/UITimer.java ================================================ /* * 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 . */ package haveno.desktop.common; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.reactfx.FxTimer; import javafx.application.Platform; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.Duration; public class UITimer implements Timer { private final Logger log = LoggerFactory.getLogger(UITimer.class); private haveno.common.reactfx.Timer timer; public UITimer() { } @Override public Timer runLater(Duration delay, Runnable runnable) { executeDirectlyIfPossible(() -> { if (timer == null) { timer = FxTimer.create(delay, runnable); timer.restart(); } else { log.warn("runLater called on an already running timer."); } }); return this; } @Override public Timer runPeriodically(Duration interval, Runnable runnable) { executeDirectlyIfPossible(() -> { if (timer == null) { timer = FxTimer.createPeriodic(interval, runnable); timer.restart(); } else { log.warn("runPeriodically called on an already running timer."); } }); return this; } @Override public void stop() { executeDirectlyIfPossible(() -> { if (timer != null) { timer.stop(); timer = null; } }); } private void executeDirectlyIfPossible(Runnable runnable) { if (Platform.isFxApplicationThread()) { runnable.run(); } else { UserThread.execute(runnable); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/common/ViewfxException.java ================================================ /* * 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 . */ package haveno.desktop.common; import static java.lang.String.format; public class ViewfxException extends RuntimeException { public ViewfxException(Throwable cause, String format, Object... args) { super(format(format, args), cause); } public ViewfxException(String format, Object... args) { super(format(format, args)); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/common/fxml/FxmlViewLoader.java ================================================ /* * 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 . */ package haveno.desktop.common.fxml; import com.google.common.base.Joiner; import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.util.Utilities; import haveno.desktop.common.ViewfxException; import haveno.desktop.common.view.FxmlView; import haveno.desktop.common.view.View; import haveno.desktop.common.view.ViewFactory; import haveno.desktop.common.view.ViewLoader; import java.io.IOException; import java.lang.annotation.Annotation; import java.net.URL; import java.util.ResourceBundle; import javafx.fxml.FXMLLoader; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton public class FxmlViewLoader implements ViewLoader { private final ViewFactory viewFactory; private final ResourceBundle resourceBundle; @Inject public FxmlViewLoader(ViewFactory viewFactory, ResourceBundle resourceBundle) { this.viewFactory = viewFactory; this.resourceBundle = resourceBundle; } @SuppressWarnings("unchecked") public View load(Class viewClass) { FxmlView fxmlView = viewClass.getAnnotation(FxmlView.class); final Class convention; final Class defaultConvention = (Class) getDefaultValue(FxmlView.class, "convention"); final String specifiedLocation; final String defaultLocation = (String) getDefaultValue(FxmlView.class, "location"); if (fxmlView == null) { convention = defaultConvention; specifiedLocation = defaultLocation; } else { convention = fxmlView.convention(); specifiedLocation = fxmlView.location(); } if (convention == null || specifiedLocation == null) throw new IllegalStateException("Convention and location should never be null."); try { final String resolvedLocation; if (specifiedLocation.equals(defaultLocation)) resolvedLocation = convention.newInstance().apply(viewClass); else resolvedLocation = specifiedLocation; URL fxmlUrl = viewClass.getClassLoader().getResource(resolvedLocation); if (fxmlUrl == null) throw new ViewfxException( "Failed to load view class [%s] because FXML file at [%s] could not be loaded " + "as a classpath resource. Does it exist?", viewClass, specifiedLocation); return loadFromFxml(fxmlUrl); } catch (InstantiationException | IllegalAccessException ex) { throw new ViewfxException(ex, "Failed to load view from class %s", viewClass); } } private View loadFromFxml(URL fxmlUrl) { checkNotNull(fxmlUrl, "FXML URL must not be null"); try { FXMLLoader loader = new FXMLLoader(fxmlUrl, resourceBundle); loader.setControllerFactory(viewFactory); loader.load(); Object controller = loader.getController(); if (controller == null) throw new ViewfxException("Failed to load view from FXML file at [%s]. " + "Does it declare an fx:controller attribute?", fxmlUrl); if (!(controller instanceof View)) throw new ViewfxException("Controller of type [%s] loaded from FXML file at [%s] " + "does not implement [%s] as expected.", controller.getClass(), fxmlUrl, View.class); return (View) controller; } catch (IOException ex) { Throwable cause = ex.getCause(); if (cause != null) { cause.printStackTrace(); log.error(cause.toString()); // We want to show stackTrace in error popup String stackTrace = Utilities.toTruncatedString(Joiner.on("\n").join(cause.getStackTrace()), 800, false); throw new ViewfxException(cause, "%s at loading view class\nStack trace:\n%s", cause.getClass().getSimpleName(), stackTrace); } else { throw new ViewfxException(ex, "Failed to load view from FXML file at [%s]", fxmlUrl); } } } /** * Copied and adapted from Spring Framework v4.3.6's AnnotationUtils#defaultValue * method in order to make it possible to drop Haveno's dependency on Spring altogether. */ @SuppressWarnings("SameParameterValue") private static Object getDefaultValue(Class annotationType, String attributeName) { if (annotationType == null || attributeName == null || attributeName.length() == 0) { return null; } try { return annotationType.getDeclaredMethod(attributeName).getDefaultValue(); } catch (Exception ex) { return null; } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/common/model/Activatable.java ================================================ /* * 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 . */ package haveno.desktop.common.model; public interface Activatable { void _activate(); void _deactivate(); } ================================================ FILE: desktop/src/main/java/haveno/desktop/common/model/ActivatableDataModel.java ================================================ /* * 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 . */ package haveno.desktop.common.model; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public abstract class ActivatableDataModel implements Activatable, DataModel { protected final Logger log = LoggerFactory.getLogger(this.getClass()); @Override public final void _activate() { this.activate(); } protected void activate() { } @Override public final void _deactivate() { this.deactivate(); } protected void deactivate() { } } ================================================ FILE: desktop/src/main/java/haveno/desktop/common/model/ActivatableViewModel.java ================================================ /* * 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 . */ package haveno.desktop.common.model; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public abstract class ActivatableViewModel implements Activatable, ViewModel { protected final Logger log = LoggerFactory.getLogger(this.getClass()); @Override public final void _activate() { this.activate(); } protected void activate() { } @Override public final void _deactivate() { this.deactivate(); } protected void deactivate() { } } ================================================ FILE: desktop/src/main/java/haveno/desktop/common/model/ActivatableWithDataModel.java ================================================ /* * 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 . */ package haveno.desktop.common.model; public class ActivatableWithDataModel extends WithDataModel implements Activatable { public ActivatableWithDataModel(D dataModel) { super(dataModel); } @Override public final void _activate() { dataModel._activate(); this.activate(); } protected void activate() { } @Override public final void _deactivate() { dataModel._deactivate(); this.deactivate(); } protected void deactivate() { } } ================================================ FILE: desktop/src/main/java/haveno/desktop/common/model/DataModel.java ================================================ /* * 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 . */ package haveno.desktop.common.model; public interface DataModel extends Model { } ================================================ FILE: desktop/src/main/java/haveno/desktop/common/model/Model.java ================================================ /* * 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 . */ package haveno.desktop.common.model; public interface Model { } ================================================ FILE: desktop/src/main/java/haveno/desktop/common/model/ViewModel.java ================================================ /* * 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 . */ package haveno.desktop.common.model; public interface ViewModel extends Model { } ================================================ FILE: desktop/src/main/java/haveno/desktop/common/model/WithDataModel.java ================================================ /* * 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 . */ package haveno.desktop.common.model; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static com.google.common.base.Preconditions.checkNotNull; public class WithDataModel { protected final Logger log = LoggerFactory.getLogger(this.getClass()); public final D dataModel; protected WithDataModel(D dataModel) { this.dataModel = checkNotNull(dataModel, "Delegate object must not be null"); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/common/view/AbstractView.java ================================================ /* * 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 . */ package haveno.desktop.common.view; import javafx.fxml.FXML; import javafx.scene.Node; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public abstract class AbstractView implements View { protected final Logger log = LoggerFactory.getLogger(this.getClass()); protected @FXML R root; protected final M model; public AbstractView(M model) { this.model = model; } public AbstractView() { this(null); } public R getRoot() { return root; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/common/view/ActivatableView.java ================================================ /* * 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 . */ package haveno.desktop.common.view; import javafx.scene.Node; public abstract class ActivatableView extends InitializableView { public ActivatableView(M model) { super(model); } public ActivatableView() { this(null); } @Override protected void prepareInitialize() { if (root != null) { root.sceneProperty().addListener((ov, oldValue, newValue) -> { if (oldValue == null && newValue != null) activate(); else if (oldValue != null && newValue == null) deactivate(); }); } } protected void activate() { } protected void deactivate() { } } ================================================ FILE: desktop/src/main/java/haveno/desktop/common/view/ActivatableViewAndModel.java ================================================ /* * 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 . */ package haveno.desktop.common.view; import haveno.desktop.common.model.Activatable; import javafx.scene.Node; import static com.google.common.base.Preconditions.checkNotNull; public abstract class ActivatableViewAndModel extends ActivatableView { public ActivatableViewAndModel(M model) { super(checkNotNull(model, "Model must not be null")); } @Override protected void prepareInitialize() { if (root != null) { root.sceneProperty().addListener((ov, oldValue, newValue) -> { if (oldValue == null && newValue != null) { model._activate(); activate(); } else if (oldValue != null && newValue == null) { model._deactivate(); deactivate(); } }); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/common/view/CachingViewLoader.java ================================================ /* * 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 . */ package haveno.desktop.common.view; import com.google.inject.Inject; import com.google.inject.Singleton; import java.util.HashMap; import java.util.Map; @Singleton public class CachingViewLoader implements ViewLoader { private final Map, View> cache = new HashMap<>(); private final ViewLoader viewLoader; @Inject public CachingViewLoader(ViewLoader viewLoader) { this.viewLoader = viewLoader; } @Override public View load(Class viewClass) { if (cache.containsKey(viewClass)) return cache.get(viewClass); View view = viewLoader.load(viewClass); cache.put(viewClass, view); return view; } public void removeFromCache(Class viewClass) { cache.remove(viewClass); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/common/view/DefaultPathConvention.java ================================================ /* * 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 . */ package haveno.desktop.common.view; public class DefaultPathConvention implements FxmlView.PathConvention { /** * Convert a '.'-based fully-qualified name of {@code viewClass} to a '/'-based * resource path suffixed with ".fxml". */ @Override public String apply(Class viewClass) { return viewClass.getName().replace('.', '/').concat(".fxml"); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/common/view/FxmlView.java ================================================ /* * 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 . */ package haveno.desktop.common.view; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.function.Function; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface FxmlView { /** * The location of the FXML file associated with annotated {@link View} class. By default the location will be * determined by {@link #convention()}. */ String location() default ""; /** * The function used to determine the location of the FXML file associated with the annotated {@link View} class. * By default it is the fully-qualified view class name, converted to a resource path, replacing the * {@code .class} suffix replaced with {@code .fxml}. */ Class convention() default DefaultPathConvention.class; interface PathConvention extends Function, String> { } } ================================================ FILE: desktop/src/main/java/haveno/desktop/common/view/InitializableView.java ================================================ /* * 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 . */ package haveno.desktop.common.view; import javafx.fxml.Initializable; import javafx.scene.Node; import java.net.URL; import java.util.ResourceBundle; public abstract class InitializableView extends AbstractView implements Initializable { public InitializableView(M model) { super(model); } public InitializableView() { this(null); } @Override public final void initialize(URL location, ResourceBundle resources) { prepareInitialize(); initialize(); } protected void prepareInitialize() { } protected void initialize() { } } ================================================ FILE: desktop/src/main/java/haveno/desktop/common/view/View.java ================================================ /* * 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 . */ package haveno.desktop.common.view; import javafx.scene.Node; public interface View { Node getRoot(); } ================================================ FILE: desktop/src/main/java/haveno/desktop/common/view/ViewFactory.java ================================================ /* * 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 . */ package haveno.desktop.common.view; import javafx.util.Callback; public interface ViewFactory extends Callback, Object> { } ================================================ FILE: desktop/src/main/java/haveno/desktop/common/view/ViewLoader.java ================================================ /* * 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 . */ package haveno.desktop.common.view; public interface ViewLoader { View load(Class viewClass); } ================================================ FILE: desktop/src/main/java/haveno/desktop/common/view/ViewPath.java ================================================ /* * 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 . */ package haveno.desktop.common.view; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; public final class ViewPath extends ArrayList> { private ViewPath() { } public ViewPath(Collection> c) { super(c); } @SafeVarargs public static ViewPath to(Class... elements) { ViewPath path = new ViewPath(); List> list = Arrays.asList(elements); path.addAll(list); return path; } public static ViewPath from(ViewPath original) { ViewPath path = new ViewPath(); path.addAll(original); return path; } public Class tip() { if (size() == 0) return null; return get(size() - 1); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/common/view/guice/InjectorViewFactory.java ================================================ /* * 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 . */ package haveno.desktop.common.view.guice; import com.google.common.base.Preconditions; import com.google.inject.Injector; import com.google.inject.Singleton; import haveno.desktop.common.view.ViewFactory; @Singleton public class InjectorViewFactory implements ViewFactory { private Injector injector; public void setInjector(Injector injector) { this.injector = injector; } @Override public Object call(Class aClass) { Preconditions.checkNotNull(injector, "Injector has not yet been provided"); return injector.getInstance(aClass); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/AccountStatusTooltipLabel.java ================================================ /* * 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 . */ package haveno.desktop.components; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import haveno.common.UserThread; import haveno.core.account.sign.SignedWitnessService; import haveno.core.locale.Res; import haveno.core.offer.OfferRestrictions; import haveno.core.trade.HavenoUtils; import haveno.desktop.components.controlsfx.control.PopOver; import haveno.desktop.main.offer.offerbook.OfferBookListItem; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.GUIUtil; import javafx.geometry.Bounds; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.ContentDisplay; import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import java.util.concurrent.TimeUnit; public class AccountStatusTooltipLabel extends AutoTooltipLabel { public static final int DEFAULT_WIDTH = 300; private final Node textIcon; private final OfferBookListItem.WitnessAgeData witnessAgeData; private final String popupTitle; private PopOver popOver; private boolean keepPopOverVisible = false; public AccountStatusTooltipLabel(OfferBookListItem.WitnessAgeData witnessAgeData) { super(witnessAgeData.getDisplayString()); this.witnessAgeData = witnessAgeData; this.textIcon = FormBuilder.getIcon(witnessAgeData.getIcon()); this.popupTitle = witnessAgeData.isLimitLifted() ? Res.get("offerbook.timeSinceSigning.tooltip.accountLimitLifted") : Res.get("offerbook.timeSinceSigning.tooltip.accountLimit", HavenoUtils.formatXmr(OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT, true)); positionAndActivateIcon(); } private void positionAndActivateIcon() { textIcon.setOpacity(0.4); textIcon.getStyleClass().add("tooltip-icon"); popOver = createPopOver(); textIcon.setOnMouseEntered(e -> showPopup(textIcon)); textIcon.setOnMouseExited(e -> UserThread.runAfter(() -> { if (!keepPopOverVisible) { popOver.hide(); } }, 200, TimeUnit.MILLISECONDS) ); setGraphic(textIcon); setContentDisplay(ContentDisplay.RIGHT); } private PopOver createPopOver() { Label titleLabel = new Label(popupTitle); titleLabel.setMaxWidth(DEFAULT_WIDTH); titleLabel.setWrapText(true); titleLabel.setPadding(new Insets(10, 10, 0, 10)); titleLabel.getStyleClass().add("account-status-title"); Label infoLabel = new Label(witnessAgeData.getInfo()); infoLabel.setMaxWidth(DEFAULT_WIDTH); infoLabel.setWrapText(true); infoLabel.setPadding(new Insets(0, 10, 4, 10)); infoLabel.getStyleClass().add("small-text"); Label buyLabel = createDetailsItem( Res.get("offerbook.timeSinceSigning.tooltip.checkmark.buyXmr"), witnessAgeData.isAccountSigned() ); Label waitLabel = createDetailsItem( Res.get("offerbook.timeSinceSigning.tooltip.checkmark.wait", SignedWitnessService.SIGNER_AGE_DAYS), witnessAgeData.isLimitLifted() ); Hyperlink learnMoreLink = new ExternalHyperlink(Res.get("offerbook.timeSinceSigning.tooltip.learnMore"), null, "0.769em"); learnMoreLink.setMaxWidth(DEFAULT_WIDTH); learnMoreLink.setWrapText(true); learnMoreLink.setPadding(new Insets(10, 10, 2, 10)); learnMoreLink.getStyleClass().addAll("very-small-text"); learnMoreLink.setOnAction((e) -> GUIUtil.openWebPage("https://docs.haveno.exchange/overview/account_limits")); VBox vBox = new VBox(2, titleLabel, infoLabel, buyLabel, waitLabel, learnMoreLink); vBox.setPadding(new Insets(2, 0, 2, 0)); vBox.setAlignment(Pos.CENTER_LEFT); PopOver popOver = new PopOver(vBox); popOver.setArrowLocation(PopOver.ArrowLocation.LEFT_CENTER); vBox.setOnMouseEntered(mouseEvent -> keepPopOverVisible = true); vBox.setOnMouseExited(mouseEvent -> { keepPopOverVisible = false; popOver.hide(); }); return popOver; } private void showPopup(Node textIcon) { Bounds bounds = textIcon.localToScreen(textIcon.getBoundsInLocal()); popOver.show(textIcon, bounds.getMaxX() + 10, (bounds.getMinY() + bounds.getHeight() / 2) - 10); } private Label createDetailsItem(String text, boolean active) { Label label = new Label(text); label.setMaxWidth(DEFAULT_WIDTH); label.setWrapText(true); label.setPadding(new Insets(0, 10, 0, 10)); label.getStyleClass().add("small-text"); if (active) { label.setStyle("-fx-text-fill: -fx-accent"); } else { label.setStyle("-fx-text-fill: -bs-color-gray-dim"); } Text icon = FormBuilder.getSmallIconForLabel(active ? MaterialDesignIcon.CHECKBOX_MARKED_CIRCLE : MaterialDesignIcon.CLOSE_CIRCLE, label); icon.setLayoutY(4); if (active) { icon.getStyleClass().add("account-status-active-info-item"); } else { icon.getStyleClass().add("account-status-inactive-info-item"); } return label; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/AddressTextField.java ================================================ /* * 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 . */ package haveno.desktop.components; import com.jfoenix.controls.JFXTextField; import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; import haveno.common.UserThread; import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.Layout; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.Tooltip; import javafx.scene.layout.AnchorPane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.math.BigInteger; import java.net.URI; public class AddressTextField extends AnchorPane { private static final Logger log = LoggerFactory.getLogger(AddressTextField.class); private final StringProperty address = new SimpleStringProperty(); private final StringProperty paymentLabel = new SimpleStringProperty(); private final ObjectProperty amount = new SimpleObjectProperty<>(BigInteger.ZERO); private boolean wasPrimaryButtonDown; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public AddressTextField(String label) { JFXTextField textField = new HavenoTextField(); textField.setId("address-text-field"); textField.setEditable(false); textField.setLabelFloat(true); textField.getStyleClass().add("label-float"); textField.setPromptText(label); textField.textProperty().bind(address); String tooltipText = Res.get("addressTextField.openWallet"); textField.setTooltip(new Tooltip(tooltipText)); textField.setOnMousePressed(event -> wasPrimaryButtonDown = event.isPrimaryButtonDown()); textField.setOnMouseReleased(event -> { if (wasPrimaryButtonDown) openWallet(); wasPrimaryButtonDown = false; }); textField.focusTraversableProperty().set(focusTraversableProperty().get()); Label extWalletIcon = new Label(); extWalletIcon.setLayoutY(Layout.FLOATING_ICON_Y); extWalletIcon.getStyleClass().addAll("icon", "highlight"); extWalletIcon.setTooltip(new Tooltip(tooltipText)); AwesomeDude.setIcon(extWalletIcon, AwesomeIcon.SIGNIN); extWalletIcon.setOnMouseClicked(e -> openWallet()); Label copyLabel = new Label(); copyLabel.setLayoutY(Layout.FLOATING_ICON_Y); copyLabel.getStyleClass().addAll("icon", "highlight"); Tooltip.install(copyLabel, new Tooltip(Res.get("addressTextField.copyToClipboard"))); copyLabel.setGraphic(GUIUtil.getCopyIcon()); copyLabel.setOnMouseClicked(e -> { if (address.get() != null && address.get().length() > 0) Utilities.copyToClipboard(address.get()); Tooltip tp = new Tooltip(Res.get("shared.copiedToClipboard")); Node node = (Node) e.getSource(); UserThread.runAfter(() -> tp.hide(), 1); tp.show(node, e.getScreenX() + Layout.PADDING, e.getScreenY() + Layout.PADDING); }); AnchorPane.setRightAnchor(copyLabel, 30.0); AnchorPane.setRightAnchor(extWalletIcon, 5.0); AnchorPane.setRightAnchor(textField, 55.0); AnchorPane.setLeftAnchor(textField, 0.0); getChildren().addAll(textField, copyLabel, extWalletIcon); } private void openWallet() { try { Utilities.openURI(URI.create(getMoneroURI())); } catch (Exception e) { log.warn(e.getMessage()); new Popup().warning(Res.get("addressTextField.openWallet.failed")).show(); } } /////////////////////////////////////////////////////////////////////////////////////////// // Getters/Setters /////////////////////////////////////////////////////////////////////////////////////////// public void setAddress(String address) { this.address.set(address); } public String getAddress() { return address.get(); } public StringProperty addressProperty() { return address; } public BigInteger getAmount() { return amount.get(); } public ObjectProperty amountAsProperty() { return amount; } public void setAmount(BigInteger amount) { this.amount.set(amount); } public String getPaymentLabel() { return paymentLabel.get(); } public StringProperty paymentLabelProperty() { return paymentLabel; } public void setPaymentLabel(String paymentLabel) { this.paymentLabel.set(paymentLabel); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private String getMoneroURI() { if (amount.get().compareTo(BigInteger.ZERO) < 0) { log.warn("Amount must not be negative"); setAmount(BigInteger.ZERO); } return GUIUtil.getMoneroURI( address.get(), amount.get(), paymentLabel.get()); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/AddressWithIconAndDirection.java ================================================ /* * 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 . */ package haveno.desktop.components; import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.control.Tooltip; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class AddressWithIconAndDirection extends HBox { private static final Logger log = LoggerFactory.getLogger(AddressWithIconAndDirection.class); private final Hyperlink hyperlink; public AddressWithIconAndDirection(String text, String address, boolean received) { Label directionIcon = new Label(); directionIcon.getStyleClass().add("icon"); directionIcon.getStyleClass().add(received ? "received-funds-icon" : "sent-funds-icon"); AwesomeDude.setIcon(directionIcon, received ? AwesomeIcon.SIGNIN : AwesomeIcon.SIGNOUT); if (received) directionIcon.setRotate(180); directionIcon.setMouseTransparent(true); setAlignment(Pos.CENTER_LEFT); Label label = new AutoTooltipLabel(text); label.setMouseTransparent(true); HBox.setMargin(directionIcon, new Insets(0, 3, 0, 0)); HBox.setHgrow(label, Priority.ALWAYS); hyperlink = new ExternalHyperlink(address); HBox.setMargin(hyperlink, new Insets(0)); HBox.setHgrow(hyperlink, Priority.SOMETIMES); // You need to set max width to Double.MAX_VALUE to make HBox.setHgrow working like expected! // also pref width needs to be not default (-1) hyperlink.setMaxWidth(Double.MAX_VALUE); hyperlink.setPrefWidth(0); getChildren().addAll(directionIcon, label, hyperlink); } public void setOnAction(EventHandler handler) { hyperlink.setOnAction(handler); } public void setTooltip(Tooltip tooltip) { hyperlink.setTooltip(tooltip); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/AutoTooltipButton.java ================================================ /* * 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 . */ package haveno.desktop.components; import com.jfoenix.controls.JFXButton; import com.jfoenix.skins.JFXButtonSkin; import javafx.scene.Node; import javafx.scene.control.Skin; import static haveno.desktop.components.TooltipUtil.showTooltipIfTruncated; public class AutoTooltipButton extends JFXButton { public AutoTooltipButton() { super(); } public AutoTooltipButton(String text) { super(text); } public AutoTooltipButton(String text, Node graphic) { super(text, graphic); } public void updateText(String text) { setText(text); } @Override protected Skin createDefaultSkin() { return new AutoTooltipButtonSkin(this); } private class AutoTooltipButtonSkin extends JFXButtonSkin { public AutoTooltipButtonSkin(JFXButton button) { super(button); } @Override protected void layoutChildren(double x, double y, double w, double h) { super.layoutChildren(x, y, w, h); showTooltipIfTruncated(this, getSkinnable()); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/AutoTooltipCheckBox.java ================================================ /* * 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 . */ package haveno.desktop.components; import com.jfoenix.controls.JFXCheckBox; import com.jfoenix.skins.JFXCheckBoxSkin; import javafx.scene.control.Skin; import static haveno.desktop.components.TooltipUtil.showTooltipIfTruncated; public class AutoTooltipCheckBox extends JFXCheckBox { public AutoTooltipCheckBox() { super(); } public AutoTooltipCheckBox(String text) { super(text); } @Override protected Skin createDefaultSkin() { return new AutoTooltipCheckBoxSkin(this); } private class AutoTooltipCheckBoxSkin extends JFXCheckBoxSkin { public AutoTooltipCheckBoxSkin(JFXCheckBox checkBox) { super(checkBox); checkBox.setStyle("-jfx-checked-color: #0b65da"); } @Override protected void layoutChildren(double x, double y, double w, double h) { super.layoutChildren(x, y, w, h); showTooltipIfTruncated(this, getSkinnable()); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/AutoTooltipLabel.java ================================================ /* * 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 . */ package haveno.desktop.components; import javafx.scene.control.Label; import javafx.scene.control.Skin; import javafx.scene.control.skin.LabelSkin; import static haveno.desktop.components.TooltipUtil.showTooltipIfTruncated; public class AutoTooltipLabel extends Label { public AutoTooltipLabel() { super(); } public AutoTooltipLabel(String text) { super(text); } @Override protected Skin createDefaultSkin() { return new AutoTooltipLabelSkin(this); } private class AutoTooltipLabelSkin extends LabelSkin { public AutoTooltipLabelSkin(Label label) { super(label); } @Override protected void layoutChildren(double x, double y, double w, double h) { super.layoutChildren(x, y, w, h); showTooltipIfTruncated(this, getSkinnable()); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/AutoTooltipRadioButton.java ================================================ /* * 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 . */ package haveno.desktop.components; import com.jfoenix.controls.JFXRadioButton; import javafx.scene.control.Skin; import static haveno.desktop.components.TooltipUtil.showTooltipIfTruncated; public class AutoTooltipRadioButton extends JFXRadioButton { public AutoTooltipRadioButton() { super(); } public AutoTooltipRadioButton(String text) { super(text); } @Override protected Skin createDefaultSkin() { return new AutoTooltipRadioButtonSkin(this); } private class AutoTooltipRadioButtonSkin extends JFXRadioButtonSkinHavenoStyle { public AutoTooltipRadioButtonSkin(JFXRadioButton radioButton) { super(radioButton); } @Override protected void layoutChildren(double x, double y, double w, double h) { super.layoutChildren(x, y, w, h); showTooltipIfTruncated(this, getSkinnable()); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/AutoTooltipSlideToggleButton.java ================================================ package haveno.desktop.components; import com.jfoenix.controls.JFXToggleButton; import com.jfoenix.skins.JFXToggleButtonSkin; import javafx.scene.control.Skin; import static haveno.desktop.components.TooltipUtil.showTooltipIfTruncated; public class AutoTooltipSlideToggleButton extends JFXToggleButton { public AutoTooltipSlideToggleButton() { super(); } @Override protected Skin createDefaultSkin() { return new AutoTooltipSlideToggleButton.AutoTooltipSlideToggleButtonSkin(this); } private class AutoTooltipSlideToggleButtonSkin extends JFXToggleButtonSkin { public AutoTooltipSlideToggleButtonSkin(JFXToggleButton toggleButton) { super(toggleButton); } @Override protected void layoutChildren(double x, double y, double w, double h) { super.layoutChildren(x, y, w, h); showTooltipIfTruncated(this, getSkinnable()); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/AutoTooltipTableColumn.java ================================================ /* * 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 . */ package haveno.desktop.components; import de.jensd.fx.fontawesome.AwesomeIcon; import haveno.desktop.components.controlsfx.control.PopOver; import haveno.desktop.util.FormBuilder; import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.TableColumn; import javafx.scene.layout.HBox; public class AutoTooltipTableColumn extends TableColumn { private Label helpIcon; private PopOverWrapper popoverWrapper = new PopOverWrapper(); public AutoTooltipTableColumn(String text) { super(); setTitle(text); } public AutoTooltipTableColumn(String text, String help) { setTitleWithHelpText(text, help); } public void setTitle(String title) { setGraphic(new AutoTooltipLabel(title)); } public void setTitleWithHelpText(String title, String help) { helpIcon = FormBuilder.getSmallIcon(AwesomeIcon.QUESTION_SIGN); helpIcon.setOpacity(0.4); helpIcon.setOnMouseEntered(e -> popoverWrapper.showPopOver(() -> createInfoPopOver(help))); helpIcon.setOnMouseExited(e -> popoverWrapper.hidePopOver()); final AutoTooltipLabel label = new AutoTooltipLabel(title); final HBox hBox = new HBox(label, helpIcon); hBox.setStyle("-fx-alignment: center-left"); hBox.setSpacing(4); setGraphic(hBox); } private PopOver createInfoPopOver(String help) { Label helpLabel = new Label(help); helpLabel.setMaxWidth(300); helpLabel.setWrapText(true); return createInfoPopOver(helpLabel); } private PopOver createInfoPopOver(Node node) { node.getStyleClass().add("default-text"); PopOver infoPopover = new PopOver(node); if (helpIcon.getScene() != null) { infoPopover.setDetachable(false); infoPopover.setArrowLocation(PopOver.ArrowLocation.LEFT_CENTER); infoPopover.show(helpIcon, -10); } return infoPopover; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/AutoTooltipTextField.java ================================================ /* * 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 . */ package haveno.desktop.components; import com.jfoenix.controls.JFXTextField; import com.jfoenix.skins.JFXTextFieldSkin; import javafx.scene.control.Skin; import javafx.scene.control.TextField; public class AutoTooltipTextField extends JFXTextField { public AutoTooltipTextField() { super(); } public AutoTooltipTextField(String text) { super(text); } @Override protected Skin createDefaultSkin() { return new AutoTooltipTextFieldSkin(this); } private class AutoTooltipTextFieldSkin extends JFXTextFieldSkin { public AutoTooltipTextFieldSkin(TextField textField) { super(textField); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/AutoTooltipToggleButton.java ================================================ /* * 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 . */ package haveno.desktop.components; import javafx.scene.Node; import javafx.scene.control.Skin; import javafx.scene.control.ToggleButton; import javafx.scene.control.skin.ToggleButtonSkin; import static haveno.desktop.components.TooltipUtil.showTooltipIfTruncated; public class AutoTooltipToggleButton extends ToggleButton { public AutoTooltipToggleButton() { super(); } public AutoTooltipToggleButton(String text) { super(text); } public AutoTooltipToggleButton(String text, Node graphic) { super(text, graphic); } @Override protected Skin createDefaultSkin() { return new AutoTooltipToggleButtonSkin(this); } private class AutoTooltipToggleButtonSkin extends ToggleButtonSkin { public AutoTooltipToggleButtonSkin(ToggleButton toggleButton) { super(toggleButton); } @Override protected void layoutChildren(double x, double y, double w, double h) { super.layoutChildren(x, y, w, h); showTooltipIfTruncated(this, getSkinnable()); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/AutocompleteComboBox.java ================================================ /* * 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 . */ package haveno.desktop.components; import com.jfoenix.controls.JFXComboBox; import com.jfoenix.skins.JFXComboBoxListViewSkin; import haveno.common.UserThread; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.Event; import javafx.event.EventHandler; import javafx.scene.control.ListView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import org.apache.commons.lang3.StringUtils; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; /** * Implements searchable dropdown (an autocomplete like experience). * * Clients must use setAutocompleteItems() instead of setItems(). * * @param type of the ComboBox item; in the simplest case this can be a String */ public class AutocompleteComboBox extends JFXComboBox { private List list; private List extendedList; private List matchingList; private JFXComboBoxListViewSkin comboBoxListViewSkin; private boolean selectAllShortcut = false; private T lastCommittedValue; public AutocompleteComboBox() { this(FXCollections.observableArrayList()); } private AutocompleteComboBox(ObservableList items) { super(items); setEditable(true); clearOnFocus(); setEmptySkinToGetMoreControlOverListView(); fixSpaceKey(); setAutocompleteItems(items); reactToQueryChanges(); // Store last committed value so we can restore it if nothing selected valueProperty().addListener((obs, oldVal, newVal) -> { if (newVal != null) lastCommittedValue = newVal; }); // Restore last committed value when editor loses focus if no matches getEditor().focusedProperty().addListener((obs, wasFocused, isNowFocused) -> { if (!isNowFocused) { String input = getEditor().getText(); T matched = getConverter().fromString(input); boolean matchFound = getItems().stream() .anyMatch(item -> item.equals(matched)); if (!matchFound) { UserThread.execute(() -> { getSelectionModel().select(lastCommittedValue); getEditor().setText(asString(lastCommittedValue)); }); } } }); } /** * Set the complete list of ComboBox items. Use this instead of setItems(). */ public void setAutocompleteItems(List items, List allItems) { list = items; extendedList = allItems; matchingList = new ArrayList<>(list); setValue(null); getSelectionModel().clearSelection(); setItems(FXCollections.observableList(matchingList)); getEditor().setText(""); } public void setAutocompleteItems(List items) { setAutocompleteItems(items, null); } /** * Triggered when value change is *confirmed*. In practical terms * this is when user clicks item on the dropdown or hits [ENTER] * while typing in the text. * * This is in contrast to onAction event that is triggered * on every (unconfirmed) value change. The onAction is not really * suitable for the search enabled ComboBox. */ public final void setOnChangeConfirmed(EventHandler eh) { setOnHidden(e -> { var inputText = getEditor().getText(); // Case 1: fire if input text selects (matches) an item var selectedItem = getSelectionModel().getSelectedItem(); var inputTextItem = getConverter().fromString(inputText); if (selectedItem != null && selectedItem.equals(inputTextItem)) { eh.handle(e); getParent().requestFocus(); return; } // Case 2: fire if the text is empty if (inputText.isEmpty()) { eh.handle(e); getParent().requestFocus(); // Restore the last committed value UserThread.execute(() -> { getSelectionModel().select(lastCommittedValue); getEditor().setText(asString(lastCommittedValue)); }); } }); } // Clear selection and query when ComboBox gets new focus. This is usually what user // wants - to have a blank slate for a new search. The primary motivation though // was to work around UX glitches related to (starting) editing text when combobox // had specific item selected. private void clearOnFocus() { getEditor().focusedProperty().addListener((observableValue, hadFocus, hasFocus) -> { if (!hadFocus && hasFocus) { removeFilter(); forceRedraw(); } }); } // The ComboBox API does not provide enough control over the underlying // ListView that is used as a dropdown. The only way to get this control // is to set custom ListViewSkin. The default skin is null and so useless. private void setEmptySkinToGetMoreControlOverListView() { comboBoxListViewSkin = new JFXComboBoxListViewSkin<>(this); setSkin(comboBoxListViewSkin); } // By default pressing [SPACE] caused editor text to reset. The solution // is to suppress relevant event on the underlying ListViewSkin. private void fixSpaceKey() { comboBoxListViewSkin.getPopupContent().addEventFilter(KeyEvent.ANY, (KeyEvent event) -> { if (event.getCode() == KeyCode.SPACE) event.consume(); }); } private void filterBy(String query) { matchingList = (extendedList != null && query.length() > 0 ? extendedList : list) .stream() .filter(item -> StringUtils.containsIgnoreCase(asString(item), query)) .collect(Collectors.toList()); setValue(null); getSelectionModel().clearSelection(); setItems(FXCollections.observableList(matchingList)); int pos = getEditor().getCaretPosition(); if (pos > query.length()) pos = query.length(); getEditor().setText(query); getEditor().positionCaret(pos); } private void reactToQueryChanges() { getEditor().addEventHandler(KeyEvent.KEY_RELEASED, (KeyEvent event) -> { // ignore ctrl and command keys if (event.getCode() == KeyCode.CONTROL || event.getCode() == KeyCode.COMMAND || event.getCode() == KeyCode.META) { event.consume(); return; } // handle select all boolean isSelectAll = event.getCode() == KeyCode.A && (event.isControlDown() || event.isMetaDown()); if (isSelectAll) { getEditor().selectAll(); selectAllShortcut = true; event.consume(); return; } if (event.getCode() == KeyCode.A && selectAllShortcut) { // 'A' can be received after ctrl/cmd selectAllShortcut = false; event.consume(); return; } UserThread.execute(() -> { String query = getEditor().getText(); var exactMatch = list.stream().anyMatch(item -> asString(item).equalsIgnoreCase(query)); if (!exactMatch) { if (query.isEmpty()) removeFilter(); else filterBy(query); forceRedraw(); } }); }); } private void removeFilter() { matchingList = new ArrayList<>(list); setValue(null); getSelectionModel().clearSelection(); setItems(FXCollections.observableList(matchingList)); getEditor().setText(""); } private void forceRedraw() { adjustVisibleRowCount(); if (matchingListSize() > 0) { comboBoxListViewSkin.getPopupContent().autosize(); show(); if (comboBoxListViewSkin.getPopupContent() instanceof ListView listView) { listView.applyCss(); listView.layout(); } } else { hide(); } } private void adjustVisibleRowCount() { setVisibleRowCount(Math.min(10, matchingListSize())); } private String asString(T item) { return getConverter().toString(item); } private int matchingListSize() { return matchingList.size(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/BalanceTextField.java ================================================ /* * 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 . */ package haveno.desktop.components; import com.jfoenix.controls.JFXTextField; import haveno.core.trade.HavenoUtils; import haveno.core.util.coin.CoinFormatter; import javafx.scene.effect.BlurType; import javafx.scene.effect.DropShadow; import javafx.scene.effect.Effect; import javafx.scene.layout.AnchorPane; import javafx.scene.paint.Color; import javax.annotation.Nullable; import java.math.BigInteger; public class BalanceTextField extends AnchorPane { private BigInteger targetAmount; private final JFXTextField textField; private final Effect fundedEffect = new DropShadow(BlurType.THREE_PASS_BOX, Color.GREEN, 4, 0.0, 0, 0); private final Effect notFundedEffect = new DropShadow(BlurType.THREE_PASS_BOX, Color.ORANGERED, 4, 0.0, 0, 0); private CoinFormatter formatter; @Nullable private BigInteger balance; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public BalanceTextField(String label) { textField = new HavenoTextField(); textField.setLabelFloat(true); textField.getStyleClass().add("label-float"); textField.setPromptText(label); textField.setFocusTraversable(false); textField.setEditable(false); textField.setId("info-field"); AnchorPane.setRightAnchor(textField, 0.0); AnchorPane.setLeftAnchor(textField, 0.0); getChildren().addAll(textField); } public void setFormatter(CoinFormatter formatter) { this.formatter = formatter; } public void setBalance(BigInteger balance) { this.balance = balance; updateBalance(balance); } public void setTargetAmount(BigInteger targetAmount) { this.targetAmount = targetAmount; if (this.balance != null) updateBalance(balance); } /////////////////////////////////////////////////////////////////////////////////////////// // Private methods /////////////////////////////////////////////////////////////////////////////////////////// private void updateBalance(BigInteger balance) { if (formatter != null) textField.setText(HavenoUtils.formatXmr(balance, true)); //TODO: replace with new validation logic // if (targetAmount != null) { // if (balance.compareTo(targetAmount) >= 0) // textField.setEffect(fundedEffect); // else // textField.setEffect(notFundedEffect); // } else { // textField.setEffect(null); // } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/BusyAnimation.java ================================================ /* * 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 . */ package haveno.desktop.components; import com.jfoenix.controls.JFXSpinner; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; public class BusyAnimation extends JFXSpinner { private final BooleanProperty isRunningProperty = new SimpleBooleanProperty(); public BusyAnimation() { this(true); } public BusyAnimation(boolean isRunning) { getStyleClass().add("busyanimation"); isRunningProperty.set(isRunning); updateVisibility(); } public void play() { isRunningProperty.set(true); setProgress(-1); updateVisibility(); } public void stop() { isRunningProperty.set(false); setProgress(0); updateVisibility(); } public boolean isRunning() { return isRunningProperty.get(); } public BooleanProperty isRunningProperty() { return isRunningProperty; } private void updateVisibility() { setVisible(isRunning()); setManaged(isRunning()); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/ColoredDecimalPlacesWithZerosText.java ================================================ /* * 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 . */ package haveno.desktop.components; import haveno.common.util.Tuple2; import haveno.core.util.FormattingUtils; import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.layout.HBox; public class ColoredDecimalPlacesWithZerosText extends HBox { public ColoredDecimalPlacesWithZerosText(String number, int numberOfZerosToColorize) { super(); if (numberOfZerosToColorize <= 0) { getChildren().addAll(new Label(number)); } else if (number.contains(FormattingUtils.RANGE_SEPARATOR)) { String[] splitNumber = number.split(FormattingUtils.RANGE_SEPARATOR); Tuple2 numbers = getSplittedNumberNodes(splitNumber[0], numberOfZerosToColorize); getChildren().addAll(numbers.first, numbers.second); getChildren().add(new Label(FormattingUtils.RANGE_SEPARATOR)); numbers = getSplittedNumberNodes(splitNumber[1], numberOfZerosToColorize); getChildren().addAll(numbers.first, numbers.second); } else { Tuple2 numbers = getSplittedNumberNodes(number, numberOfZerosToColorize); getChildren().addAll(numbers.first, numbers.second); } setAlignment(Pos.CENTER_LEFT); } private Tuple2 getSplittedNumberNodes(String number, int numberOfZeros) { String placesBeforeZero = number.split("0{1," + Integer.toString(numberOfZeros) + "}$")[0]; String zeroDecimalPlaces = number.substring(placesBeforeZero.length()); Label first = new AutoTooltipLabel(placesBeforeZero); Label last = new Label(zeroDecimalPlaces); last.getStyleClass().add("zero-decimals"); return new Tuple2<>(first, last); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/ExplorerAddressTextField.java ================================================ /* * 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 . */ package haveno.desktop.components; import com.jfoenix.controls.JFXTextField; import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.core.user.Preferences; import haveno.desktop.util.GUIUtil; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; import javafx.scene.layout.AnchorPane; import lombok.Getter; import lombok.Setter; import javax.annotation.Nullable; public class ExplorerAddressTextField extends AnchorPane { @Setter private static Preferences preferences; @Getter private final TextField textField; private final Label copyLabel, missingAddressWarningIcon; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public ExplorerAddressTextField() { copyLabel = new Label(); copyLabel.setLayoutY(3); copyLabel.getStyleClass().addAll("icon", "highlight"); copyLabel.setTooltip(new Tooltip(Res.get("explorerAddressTextField.copyToClipboard"))); copyLabel.setGraphic(GUIUtil.getCopyIcon()); AnchorPane.setRightAnchor(copyLabel, 30.0); Tooltip tooltip = new Tooltip(Res.get("explorerAddressTextField.blockExplorerIcon.tooltip")); missingAddressWarningIcon = new Label(); missingAddressWarningIcon.getStyleClass().addAll("icon", "error-icon"); AwesomeDude.setIcon(missingAddressWarningIcon, AwesomeIcon.WARNING_SIGN); missingAddressWarningIcon.setTooltip(new Tooltip(Res.get("explorerAddressTextField.missingTx.warning.tooltip"))); missingAddressWarningIcon.setMinWidth(20); AnchorPane.setRightAnchor(missingAddressWarningIcon, 52.0); AnchorPane.setTopAnchor(missingAddressWarningIcon, 4.0); missingAddressWarningIcon.setVisible(false); missingAddressWarningIcon.setManaged(false); textField = new JFXTextField(); textField.setId("address-text-field"); textField.setEditable(false); textField.setTooltip(tooltip); AnchorPane.setRightAnchor(textField, 80.0); AnchorPane.setLeftAnchor(textField, 0.0); textField.focusTraversableProperty().set(focusTraversableProperty().get()); getChildren().addAll(textField, missingAddressWarningIcon, copyLabel); } public void setup(@Nullable String address) { if (address == null) { textField.setText(Res.get("shared.na")); textField.setId("address-text-field-error"); copyLabel.setVisible(false); copyLabel.setManaged(false); missingAddressWarningIcon.setVisible(true); missingAddressWarningIcon.setManaged(true); return; } textField.setText(address); copyLabel.setOnMouseClicked(e -> Utilities.copyToClipboard(address)); } public void cleanup() { textField.setOnMouseClicked(null); copyLabel.setOnMouseClicked(null); textField.setText(""); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/ExternalHyperlink.java ================================================ /* * 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 . */ package haveno.desktop.components; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; public class ExternalHyperlink extends HyperlinkWithIcon { public ExternalHyperlink(String text) { super(text, MaterialDesignIcon.LINK); } public ExternalHyperlink(String text, String style) { super(text, MaterialDesignIcon.LINK, style); } public ExternalHyperlink(String text, String style, String iconSize) { super(text, MaterialDesignIcon.LINK, style, iconSize); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/FundsTextField.java ================================================ /* * 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 . */ package haveno.desktop.components; import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.Layout; import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.scene.control.Label; import javafx.scene.control.Tooltip; import javafx.scene.layout.AnchorPane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class FundsTextField extends InfoTextField { public static final Logger log = LoggerFactory.getLogger(FundsTextField.class); private final StringProperty fundsStructure = new SimpleStringProperty(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public FundsTextField() { super(); textField.textProperty().unbind(); textField.textProperty().bind(Bindings.concat(textProperty())); // TODO: removed `, " ", fundsStructure` for haveno to fix "Funds needed: .123 XMR (null)" bug Label copyLabel = new Label(); copyLabel.setLayoutY(Layout.FLOATING_ICON_Y); copyLabel.getStyleClass().addAll("icon", "highlight"); Tooltip.install(copyLabel, new Tooltip(Res.get("shared.copyToClipboard"))); copyLabel.setGraphic(GUIUtil.getCopyIcon()); copyLabel.setOnMouseClicked(e -> { String text = getText(); if (text != null && text.length() > 0) { String copyText; String[] strings = text.split(" "); if (strings.length > 1) copyText = strings[0]; // exclude the BTC postfix else copyText = text; Utilities.copyToClipboard(copyText); } }); AnchorPane.setRightAnchor(copyLabel, 30.0); AnchorPane.setRightAnchor(infoIcon, 62.0); AnchorPane.setRightAnchor(textField, 55.0); getChildren().add(copyLabel); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters/Setters /////////////////////////////////////////////////////////////////////////////////////////// public void setFundsStructure(String fundsStructure) { this.fundsStructure.set(fundsStructure); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/HavenoTextArea.java ================================================ package haveno.desktop.components; import com.jfoenix.controls.JFXTextArea; import javafx.scene.control.Skin; public class HavenoTextArea extends JFXTextArea { @Override protected Skin createDefaultSkin() { return new JFXTextAreaSkinHavenoStyle(this); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/HavenoTextField.java ================================================ package haveno.desktop.components; import com.jfoenix.controls.JFXTextField; import haveno.desktop.util.GUIUtil; import javafx.scene.control.Skin; public class HavenoTextField extends JFXTextField { public HavenoTextField(String value) { super(value); GUIUtil.applyFilledStyle(this); } public HavenoTextField() { this(null); } @Override protected Skin createDefaultSkin() { return new JFXTextFieldSkinHavenoStyle<>(this, 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/HyperlinkWithIcon.java ================================================ /* * 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 . */ package haveno.desktop.components; import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.glyphs.GlyphIcons; import haveno.desktop.util.FormBuilder; import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.control.ContentDisplay; import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.text.Text; import lombok.Getter; public class HyperlinkWithIcon extends Hyperlink { @Getter private Node icon; public HyperlinkWithIcon(String text) { this(text, AwesomeIcon.INFO_SIGN); } public HyperlinkWithIcon(String text, String fontSize) { this(text, AwesomeIcon.INFO_SIGN, fontSize); } public HyperlinkWithIcon(String text, AwesomeIcon awesomeIcon, String fontSize) { super(text); Label icon = new Label(); AwesomeDude.setIcon(icon, awesomeIcon, fontSize); icon.setMinWidth(20); icon.setOpacity(0.7); icon.getStyleClass().addAll("hyperlink", "no-underline"); setPadding(new Insets(0)); icon.setPadding(new Insets(0)); setIcon(icon); } public HyperlinkWithIcon(String text, AwesomeIcon awesomeIcon) { this(text, awesomeIcon, "1.231em"); } public HyperlinkWithIcon(String text, GlyphIcons icon) { this(text, icon, null); } public HyperlinkWithIcon(String text, GlyphIcons icon, String style, String iconSize) { super(text); Text textIcon = FormBuilder.getIcon(icon, iconSize); textIcon.setOpacity(0.7); textIcon.getStyleClass().addAll("hyperlink", "no-underline"); if (style != null) { textIcon.getStyleClass().add(style); getStyleClass().add(style); } setPadding(new Insets(0)); setIcon(textIcon); } public HyperlinkWithIcon(String text, GlyphIcons icon, String style) { this(text, icon, style, "1.231em"); } public void hideIcon() { setGraphic(null); } public void setIcon(Node icon) { this.icon = icon; setGraphic(icon); setContentDisplay(ContentDisplay.RIGHT); setGraphicTextGap(7.0); } public void clear() { setText(""); setGraphic(null); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/InfoAutoTooltipLabel.java ================================================ /* * 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 . */ package haveno.desktop.components; import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.glyphs.GlyphIcons; import haveno.desktop.components.controlsfx.control.PopOver; import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.control.ContentDisplay; import javafx.scene.control.Label; import static haveno.desktop.util.FormBuilder.getIcon; public class InfoAutoTooltipLabel extends AutoTooltipLabel { public static final int DEFAULT_WIDTH = 300; private Node textIcon; private PopOverWrapper popoverWrapper = new PopOverWrapper(); private ContentDisplay contentDisplay; public InfoAutoTooltipLabel(String text, GlyphIcons icon, ContentDisplay contentDisplay, String info) { this(text, contentDisplay); setIcon(icon); positionAndActivateIcon(contentDisplay, info, DEFAULT_WIDTH); } public InfoAutoTooltipLabel(String text, AwesomeIcon icon, ContentDisplay contentDisplay, String info, double width) { super(text); setIcon(icon); positionAndActivateIcon(contentDisplay, info, width); } public InfoAutoTooltipLabel(String text, ContentDisplay contentDisplay) { super(text); this.contentDisplay = contentDisplay; } public void setIcon(GlyphIcons icon) { textIcon = getIcon(icon); } public void setIcon(GlyphIcons icon, String info) { setIcon(icon); positionAndActivateIcon(contentDisplay, info, DEFAULT_WIDTH); } public void setIcon(AwesomeIcon icon) { textIcon = getIcon(icon); } public void hideIcon() { textIcon = null; setGraphic(textIcon); } // May be required until https://bugs.openjdk.java.net/browse/JDK-8265835 is fixed. public void disableRolloverPopup() { textIcon.setOnMouseEntered(null); textIcon.setOnMouseExited(null); } private void positionAndActivateIcon(ContentDisplay contentDisplay, String info, double width) { textIcon.setOpacity(0.4); textIcon.getStyleClass().add("tooltip-icon"); textIcon.setOnMouseEntered(e -> popoverWrapper.showPopOver(() -> createInfoPopOver(info, width))); textIcon.setOnMouseExited(e -> popoverWrapper.hidePopOver()); setGraphic(textIcon); setContentDisplay(contentDisplay); } private PopOver createInfoPopOver(String info, double width) { Label helpLabel = new Label(info); helpLabel.setMaxWidth(width); helpLabel.setWrapText(true); helpLabel.setPadding(new Insets(10)); return createInfoPopOver(helpLabel); } private PopOver createInfoPopOver(Node node) { node.getStyleClass().add("default-text"); PopOver infoPopover = new PopOver(node); if (textIcon.getScene() != null) { infoPopover.setDetachable(false); infoPopover.setArrowLocation(PopOver.ArrowLocation.LEFT_CENTER); infoPopover.show(textIcon, -10); } return infoPopover; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/InfoDisplay.java ================================================ /* * 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 . */ package haveno.desktop.components; import de.jensd.fx.fontawesome.AwesomeIcon; import haveno.common.UserThread; import haveno.core.locale.Res; import haveno.desktop.util.FormBuilder; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.geometry.VPos; import javafx.scene.Parent; import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.control.OverrunStyle; import javafx.scene.layout.GridPane; import javafx.scene.text.TextFlow; /** * Convenience Component for info icon, info text and link display in a GridPane. * Only the properties needed are supported. * We need to extend from Parent so we can use it in FXML, but the InfoDisplay is not used as node, * but add the children nodes to the gridPane. */ public class InfoDisplay extends Parent { private final StringProperty text = new SimpleStringProperty(); private final IntegerProperty rowIndex = new SimpleIntegerProperty(0); private final IntegerProperty columnIndex = new SimpleIntegerProperty(0); private final ObjectProperty> onAction = new SimpleObjectProperty<>(); private final ObjectProperty gridPane = new SimpleObjectProperty<>(); private boolean useReadMore; private final Label icon = FormBuilder.getIcon(AwesomeIcon.INFO_SIGN); private final TextFlow textFlow; private final Label label; private final Hyperlink link; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public InfoDisplay() { icon.setId("non-clickable-icon"); icon.visibleProperty().bind(visibleProperty()); GridPane.setValignment(icon, VPos.TOP); GridPane.setMargin(icon, new Insets(-2, 0, 0, 0)); GridPane.setRowSpan(icon, 2); label = new AutoTooltipLabel(); label.textProperty().bind(text); label.setTextOverrun(OverrunStyle.WORD_ELLIPSIS); // width is set a frame later so we hide it first label.setVisible(false); link = new Hyperlink(Res.get("shared.readMore")); link.setPadding(new Insets(0, 0, 0, -2)); // We need that to know if we have a wrapping or not. // Did not find a way to get that from the API. Label testLabel = new AutoTooltipLabel(); testLabel.textProperty().bind(text); textFlow = new TextFlow(); textFlow.visibleProperty().bind(visibleProperty()); textFlow.getChildren().addAll(testLabel); testLabel.widthProperty().addListener((ov, o, n) -> { useReadMore = (double) n > textFlow.getWidth(); link.setText(Res.get(useReadMore ? "shared.readMore" : "shared.openHelp")); UserThread.execute(() -> textFlow.getChildren().setAll(label, link)); }); // update the width when the window gets resized ChangeListener listener = (ov2, oldValue2, windowWidth) -> { if (label.prefWidthProperty().isBound()) label.prefWidthProperty().unbind(); label.setPrefWidth((double) windowWidth - localToScene(0, 0).getX() - 35); }; // when clicking "Read more..." we expand and change the link to the Help link.setOnAction(new EventHandler() { @Override public void handle(ActionEvent actionEvent) { if (useReadMore) { label.setWrapText(true); link.setText(Res.get("shared.openHelp")); getScene().getWindow().widthProperty().removeListener(listener); if (label.prefWidthProperty().isBound()) label.prefWidthProperty().unbind(); label.prefWidthProperty().bind(textFlow.widthProperty()); link.setVisited(false); // focus border is a bit confusing here so we remove it link.getStyleClass().add("hide-focus"); link.setOnAction(onAction.get()); getParent().layout(); } else { onAction.get().handle(actionEvent); } } }); sceneProperty().addListener((ov, oldValue, newValue) -> { if (oldValue == null && newValue != null && newValue.getWindow() != null) { newValue.getWindow().widthProperty().addListener(listener); // localToScene does deliver 0 instead of the correct x position when scene property gets set, // so we delay for 1 render cycle UserThread.execute(() -> { label.setVisible(true); label.prefWidthProperty().unbind(); label.setPrefWidth(newValue.getWindow().getWidth() - localToScene(0, 0).getX() - 35); }); } }); } /////////////////////////////////////////////////////////////////////////////////////////// // Setters /////////////////////////////////////////////////////////////////////////////////////////// public void setText(String text) { this.text.set(text); UserThread.execute(() -> { if (getScene() != null) { label.setVisible(true); label.prefWidthProperty().unbind(); label.setPrefWidth(getScene().getWindow().getWidth() - localToScene(0, 0).getX() - 35); } }); } public void setGridPane(GridPane gridPane) { this.gridPane.set(gridPane); gridPane.getChildren().addAll(icon, textFlow); GridPane.setColumnIndex(icon, columnIndex.get()); GridPane.setColumnIndex(textFlow, columnIndex.get() + 1); GridPane.setRowIndex(icon, rowIndex.get()); GridPane.setRowIndex(textFlow, rowIndex.get()); } public void setRowIndex(int rowIndex) { this.rowIndex.set(rowIndex); GridPane.setRowIndex(icon, rowIndex); GridPane.setRowIndex(textFlow, rowIndex); } public void setColumnIndex(int columnIndex) { this.columnIndex.set(columnIndex); GridPane.setColumnIndex(icon, columnIndex); GridPane.setColumnIndex(textFlow, columnIndex + 1); } public final void setOnAction(EventHandler eventHandler) { onAction.set(eventHandler); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// public String getText() { return text.get(); } public StringProperty textProperty() { return text; } public int getColumnIndex() { return columnIndex.get(); } public IntegerProperty columnIndexProperty() { return columnIndex; } public int getRowIndex() { return rowIndex.get(); } public IntegerProperty rowIndexProperty() { return rowIndex; } public EventHandler getOnAction() { return onAction.get(); } public ObjectProperty> onActionProperty() { return onAction; } public GridPane getGridPane() { return gridPane.get(); } public ObjectProperty gridPaneProperty() { return gridPane; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/InfoInputTextField.java ================================================ /* * 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 . */ package haveno.desktop.components; import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; import haveno.desktop.components.controlsfx.control.PopOver; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.layout.AnchorPane; import lombok.Getter; import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkNotNull; public class InfoInputTextField extends AnchorPane { private final StringProperty text = new SimpleStringProperty(); @Getter private final InputTextField inputTextField; private final Label icon; private final PopOverWrapper popoverWrapper = new PopOverWrapper(); @Nullable private Node node; public InfoInputTextField() { this(0); } public InfoInputTextField(double inputLineExtension) { super(); inputTextField = new InputTextField(inputLineExtension); AnchorPane.setRightAnchor(inputTextField, 0.0); AnchorPane.setLeftAnchor(inputTextField, 0.0); icon = new Label(); icon.setLayoutY(3); AnchorPane.setLeftAnchor(icon, 7.0); icon.setOnMouseEntered(e -> { if (node != null) { popoverWrapper.showPopOver(() -> checkNotNull(createPopOver())); } }); icon.setOnMouseExited(e -> { if (node != null) { popoverWrapper.hidePopOver(); } }); hideIcon(); getChildren().addAll(inputTextField, icon); } /////////////////////////////////////////////////////////////////////////////////////////// // Public /////////////////////////////////////////////////////////////////////////////////////////// public void setContentForInfoPopOver(Node node) { setContentForPopOver(node, AwesomeIcon.INFO_SIGN); } public void setContentForWarningPopOver(Node node) { setContentForPopOver(node, AwesomeIcon.WARNING_SIGN, "warning"); } public void setContentForPrivacyPopOver(Node node) { setContentForPopOver(node, AwesomeIcon.EYE_CLOSE); } public void setContentForPopOver(Node node, AwesomeIcon awesomeIcon) { setContentForPopOver(node, awesomeIcon, null); } public void setContentForPopOver(Node node, AwesomeIcon awesomeIcon, @Nullable String style) { this.node = node; AwesomeDude.setIcon(icon, awesomeIcon); icon.getStyleClass().removeAll("icon", "info", "warning", style); icon.getStyleClass().addAll("icon", style == null ? "info" : style); icon.setManaged(true); icon.setVisible(true); } public void hideIcon() { icon.setManaged(false); icon.setVisible(false); } public void setIconsRightAligned() { AnchorPane.clearConstraints(icon); AnchorPane.clearConstraints(inputTextField); AnchorPane.setRightAnchor(icon, 7.0); AnchorPane.setLeftAnchor(inputTextField, 0.0); AnchorPane.setRightAnchor(inputTextField, 0.0); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters/Setters /////////////////////////////////////////////////////////////////////////////////////////// public void setText(String text) { this.text.set(text); } public String getText() { return text.get(); } public StringProperty textProperty() { return text; } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private PopOver createPopOver() { if (node == null) { return null; } node.getStyleClass().add("default-text"); PopOver popover = new PopOver(node); if (icon.getScene() != null) { popover.setDetachable(false); popover.setArrowLocation(PopOver.ArrowLocation.LEFT_TOP); popover.setArrowIndent(5); popover.show(icon, -17); } return popover; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/InfoTextField.java ================================================ /* * 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 . */ package haveno.desktop.components; import com.jfoenix.controls.JFXTextField; import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import haveno.desktop.components.controlsfx.control.PopOver; import haveno.desktop.util.Layout; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.layout.AnchorPane; import javafx.scene.text.Text; import lombok.Getter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static haveno.desktop.util.FormBuilder.getIcon; import static haveno.desktop.util.FormBuilder.getRegularIconForLabel; public class InfoTextField extends AnchorPane { public static final Logger log = LoggerFactory.getLogger(InfoTextField.class); @Getter protected final JFXTextField textField; private final StringProperty text = new SimpleStringProperty(); protected final Label infoIcon; private Label currentIcon; private PopOverWrapper popoverWrapper = new PopOverWrapper(); private PopOver.ArrowLocation arrowLocation; public InfoTextField() { arrowLocation = PopOver.ArrowLocation.RIGHT_TOP; textField = new HavenoTextField(); textField.setLabelFloat(true); textField.getStyleClass().add("label-float"); textField.setEditable(false); textField.textProperty().bind(text); textField.setFocusTraversable(false); textField.setId("info-field"); infoIcon = getIcon(AwesomeIcon.INFO_SIGN); infoIcon.setLayoutY(Layout.FLOATING_ICON_Y - 2); infoIcon.getStyleClass().addAll("icon", "info"); AnchorPane.setRightAnchor(infoIcon, 7.0); AnchorPane.setRightAnchor(textField, 0.0); AnchorPane.setLeftAnchor(textField, 0.0); hideIcons(); getChildren().addAll(textField, infoIcon); } /////////////////////////////////////////////////////////////////////////////////////////// // Public /////////////////////////////////////////////////////////////////////////////////////////// public void setContentForInfoPopOver(Node node) { currentIcon = infoIcon; hideIcons(); setActionHandlers(node); } public void setContent(MaterialDesignIcon icon, String info, String style, double opacity) { hideIcons(); currentIcon = new Label(); Text textIcon = getRegularIconForLabel(icon, currentIcon); setActionHandlers(new Label(info)); currentIcon.setLayoutY(5); textIcon.getStyleClass().addAll("icon", style); currentIcon.setOpacity(opacity); AnchorPane.setRightAnchor(currentIcon, 7.0); getChildren().add(currentIcon); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void hideIcons() { infoIcon.setManaged(false); infoIcon.setVisible(false); } private void setActionHandlers(Node node) { currentIcon.setManaged(true); currentIcon.setVisible(true); // As we don't use binding here we need to recreate it on mouse over to reflect the current state currentIcon.setOnMouseEntered(e -> popoverWrapper.showPopOver(() -> createPopOver(node))); currentIcon.setOnMouseExited(e -> popoverWrapper.hidePopOver()); } private PopOver createPopOver(Node node) { node.getStyleClass().add("default-text"); PopOver popover = new PopOver(node); if (currentIcon.getScene() != null) { popover.setDetachable(false); popover.setArrowLocation(arrowLocation); popover.setArrowIndent(5); popover.show(currentIcon, -17); } return popover; } /////////////////////////////////////////////////////////////////////////////////////////// // Getters/Setters /////////////////////////////////////////////////////////////////////////////////////////// public void setText(String text) { this.text.set(text); } public String getText() { return text.get(); } public StringProperty textProperty() { return text; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/InputTextArea.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.desktop.components; import com.jfoenix.controls.JFXTextArea; import haveno.core.util.validation.InputValidator; import haveno.desktop.util.validation.JFXInputValidator; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.scene.control.Skin; /** * TextArea with validation support. * If validator is set it supports on focus out validation with that validator. If a more sophisticated validation is * needed the validationResultProperty can be used for applying validation result done by external validation. * In case the isValid property in validationResultProperty get set to false we display a red border and an error * message within the errorMessageDisplay placed on the right of the text area. * The errorMessageDisplay gets closed when the ValidatingTextArea instance gets removed from the scene graph or when * hideErrorMessageDisplay() is called. * There can be only 1 errorMessageDisplays at a time we use static field for it. * The position is derived from the position of the textArea itself or if set from the layoutReference node. */ //TODO There are some rare situation where it behaves buggy. Needs further investigation and improvements. public class InputTextArea extends JFXTextArea { private final ObjectProperty validationResult = new SimpleObjectProperty<> (new InputValidator.ValidationResult(true)); private final JFXInputValidator jfxValidationWrapper = new JFXInputValidator(); private InputValidator validator; private String errorMessage = null; public InputValidator getValidator() { return validator; } public void setValidator(InputValidator validator) { this.validator = validator; } public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public InputTextArea() { super(); getValidators().add(jfxValidationWrapper); validationResult.addListener((ov, oldValue, newValue) -> { if (newValue != null) { jfxValidationWrapper.resetValidation(); if (!newValue.isValid) { if (!newValue.errorMessageEquals(oldValue)) { // avoid blinking validate(); // ensure that the new error message replaces the old one } if (this.errorMessage != null) { jfxValidationWrapper.applyErrorMessage(this.errorMessage); } else { jfxValidationWrapper.applyErrorMessage(newValue); } } validate(); } }); textProperty().addListener((o, oldValue, newValue) -> { refreshValidation(); }); focusedProperty().addListener((o, oldValue, newValue) -> { if (validator != null) { if (!oldValue && newValue) { this.validationResult.set(new InputValidator.ValidationResult(true)); } else { this.validationResult.set(validator.validate(getText())); } } }); } /////////////////////////////////////////////////////////////////////////////////////////// // Public methods /////////////////////////////////////////////////////////////////////////////////////////// public void resetValidation() { jfxValidationWrapper.resetValidation(); String input = getText(); if (input.isEmpty()) { validationResult.set(new InputValidator.ValidationResult(true)); } else { validationResult.set(validator.validate(input)); } } public void refreshValidation() { if (validator != null) { this.validationResult.set(validator.validate(getText())); } } public void setInvalid(String message) { validationResult.set(new InputValidator.ValidationResult(false, message)); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// public ObjectProperty validationResultProperty() { return validationResult; } protected Skin createDefaultSkin() { return new JFXTextAreaSkinHavenoStyle(this); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/InputTextField.java ================================================ /* * 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 . */ package haveno.desktop.components; import com.jfoenix.controls.JFXTextField; import haveno.core.util.validation.InputValidator; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.validation.JFXInputValidator; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.scene.control.Skin; /** * TextField with validation support. * If validator is set it supports on focus out validation with that validator. If a more sophisticated validation is * needed the validationResultProperty can be used for applying validation result done by external validation. * In case the isValid property in validationResultProperty get set to false we display a red border and an error * message within the errorMessageDisplay placed on the right of the text field. * The errorMessageDisplay gets closed when the ValidatingTextField instance gets removed from the scene graph or when * hideErrorMessageDisplay() is called. * There can be only 1 errorMessageDisplays at a time we use static field for it. * The position is derived from the position of the textField itself or if set from the layoutReference node. */ //TODO There are some rare situation where it behaves buggy. Needs further investigation and improvements. public class InputTextField extends JFXTextField { private final ObjectProperty validationResult = new SimpleObjectProperty<> (new InputValidator.ValidationResult(true)); private final JFXInputValidator jfxValidationWrapper = new JFXInputValidator(); private double inputLineExtension = 0; private InputValidator validator; private String errorMessage = null; public InputValidator getValidator() { return validator; } public void setValidator(InputValidator validator) { this.validator = validator; } public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public InputTextField() { super(); GUIUtil.applyFilledStyle(this); getValidators().add(jfxValidationWrapper); validationResult.addListener((ov, oldValue, newValue) -> { if (newValue != null) { jfxValidationWrapper.resetValidation(); if (!newValue.isValid) { if (!newValue.errorMessageEquals(oldValue)) { // avoid blinking validate(); // ensure that the new error message replaces the old one } if (this.errorMessage != null) { jfxValidationWrapper.applyErrorMessage(this.errorMessage); } else { jfxValidationWrapper.applyErrorMessage(newValue); } } validate(); } }); textProperty().addListener((o, oldValue, newValue) -> { refreshValidation(); }); focusedProperty().addListener((o, oldValue, newValue) -> { if (validator != null) { if (!oldValue && newValue) { this.validationResult.set(new InputValidator.ValidationResult(true)); } else { this.validationResult.set(validator.validate(getText())); } } }); } public InputTextField(double inputLineExtension) { this(); this.inputLineExtension = inputLineExtension; } /////////////////////////////////////////////////////////////////////////////////////////// // Public methods /////////////////////////////////////////////////////////////////////////////////////////// public void resetValidation() { jfxValidationWrapper.resetValidation(); String input = getText(); if (input.isEmpty()) { validationResult.set(new InputValidator.ValidationResult(true)); } else { validationResult.set(validator.validate(input)); } } public void refreshValidation() { if (validator != null) { this.validationResult.set(validator.validate(getText())); } } public void setInvalid(String message) { validationResult.set(new InputValidator.ValidationResult(false, message)); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// public ObjectProperty validationResultProperty() { return validationResult; } protected Skin createDefaultSkin() { return new JFXTextFieldSkinHavenoStyle<>(this, inputLineExtension); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/JFXRadioButtonSkinHavenoStyle.java ================================================ /* * 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 . */ package haveno.desktop.components; import com.jfoenix.controls.JFXRadioButton; import com.jfoenix.controls.JFXRippler; import com.jfoenix.transitions.JFXAnimationTimer; import com.jfoenix.transitions.JFXKeyFrame; import com.jfoenix.transitions.JFXKeyValue; import javafx.animation.Interpolator; import javafx.geometry.HPos; import javafx.geometry.VPos; import javafx.scene.Node; import javafx.scene.control.RadioButton; import javafx.scene.control.skin.RadioButtonSkin; import javafx.scene.input.MouseEvent; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.scene.shape.Circle; import javafx.scene.shape.Rectangle; import javafx.scene.shape.StrokeType; import javafx.scene.text.Text; import javafx.util.Duration; /** * Code copied and adapted from com.jfoenix.skins.JFXRadioButtonSkin */ public class JFXRadioButtonSkinHavenoStyle extends RadioButtonSkin { private final JFXRippler rippler; private double padding = 12; private Circle radio, dot; private final StackPane container; private JFXAnimationTimer timer; public JFXRadioButtonSkinHavenoStyle(JFXRadioButton control) { super(control); control.setSelectedColor(Color.web("#0b65da")); final double radioRadius = 7; radio = new Circle(radioRadius); radio.getStyleClass().setAll("radio"); radio.setStrokeWidth(1); radio.setStrokeType(StrokeType.INSIDE); radio.setFill(Color.TRANSPARENT); radio.setSmooth(true); dot = new Circle(radioRadius); dot.getStyleClass().setAll("dot"); dot.fillProperty().bind(control.selectedColorProperty()); dot.setScaleX(0); dot.setScaleY(0); dot.setSmooth(true); container = new StackPane(radio, dot); container.getStyleClass().add("radio-container"); rippler = new JFXRippler(container, JFXRippler.RipplerMask.CIRCLE) { @Override protected double computeRippleRadius() { double width = ripplerPane.getWidth(); double width2 = width * width; return Math.min(Math.sqrt(width2 + width2), RIPPLE_MAX_RADIUS) * 1.1 + 5; } @Override protected void setOverLayBounds(Rectangle overlay) { overlay.setWidth(ripplerPane.getWidth()); overlay.setHeight(ripplerPane.getHeight()); } protected void initControlListeners() { // if the control got resized the overlay rect must be rest control.layoutBoundsProperty().addListener(observable -> resetRippler()); if (getChildren().contains(control)) { control.boundsInParentProperty().addListener(observable -> resetRippler()); } control.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> createRipple(event.getX() + padding, event.getY() + padding)); // create fade out transition for the ripple control.addEventHandler(MouseEvent.MOUSE_RELEASED, e -> releaseRipple()); } @Override protected Node getMask() { double radius = ripplerPane.getWidth() / 2; return new Circle(radius, radius, radius); } @Override protected void positionControl(Node control) { } }; updateChildren(); // show focused state control.focusedProperty().addListener((o, oldVal, newVal) -> { if (!control.disableVisualFocusProperty().get()) { if (newVal) { if (!getSkinnable().isPressed()) { rippler.setOverlayVisible(true); } } else { rippler.setOverlayVisible(false); } } }); control.pressedProperty().addListener((o, oldVal, newVal) -> rippler.setOverlayVisible(false)); timer = new JFXAnimationTimer( new JFXKeyFrame(Duration.millis(200), JFXKeyValue.builder() .setTarget(dot.scaleXProperty()) .setEndValueSupplier(() -> getSkinnable().isSelected() ? 0.55 : 0) .setInterpolator(Interpolator.EASE_BOTH) .build(), JFXKeyValue.builder() .setTarget(dot.scaleYProperty()) .setEndValueSupplier(() -> getSkinnable().isSelected() ? 0.55 : 0) .setInterpolator(Interpolator.EASE_BOTH) .build(), JFXKeyValue.builder() .setTarget(radio.strokeProperty()) .setEndValueSupplier(() -> getSkinnable().isSelected() ? ((JFXRadioButton) getSkinnable()).getSelectedColor() : ((JFXRadioButton) getSkinnable()).getUnSelectedColor()) .setInterpolator(Interpolator.EASE_BOTH) .build() )); registerChangeListener(control.selectedColorProperty(), obs -> updateColors()); registerChangeListener(control.unSelectedColorProperty(), obs -> updateColors()); registerChangeListener(control.selectedProperty(), obs -> { boolean isSelected = getSkinnable().isSelected(); Color unSelectedColor = ((JFXRadioButton) getSkinnable()).getUnSelectedColor(); Color selectedColor = ((JFXRadioButton) getSkinnable()).getSelectedColor(); rippler.setRipplerFill(isSelected ? selectedColor : unSelectedColor); if (((JFXRadioButton) getSkinnable()).isDisableAnimation()) { // apply end values timer.applyEndValues(); } else { // play selection animation timer.reverseAndContinue(); } }); updateColors(); timer.applyEndValues(); } @Override protected void updateChildren() { super.updateChildren(); if (radio != null) { removeRadio(); getChildren().addAll(container, rippler); } } @Override protected void layoutChildren(final double x, final double y, final double w, final double h) { final RadioButton radioButton = getSkinnable(); final double contWidth = snapSizeX(container.prefWidth(-1)); final double contHeight = snapSizeY(container.prefHeight(-1)); final double computeWidth = Math.max(radioButton.prefWidth(-1), radioButton.minWidth(-1)); final double width = snapSizeX(contWidth); final double height = snapSizeY(contHeight); final double labelWidth = Math.min(computeWidth - contWidth, w - width); final double labelHeight = Math.min(radioButton.prefHeight(labelWidth), h); final double maxHeight = Math.max(contHeight, labelHeight); final double xOffset = computeXOffset(w, labelWidth + contWidth, radioButton.getAlignment().getHpos()) + x; final double yOffset = computeYOffset(h, maxHeight, radioButton.getAlignment().getVpos()) + x + 5; layoutLabelInArea(xOffset + contWidth + padding / 3, yOffset, labelWidth, maxHeight, radioButton.getAlignment()); ((Text) getChildren().get((getChildren().get(0) instanceof Text) ? 0 : 1)). textProperty().set(getSkinnable().textProperty().get()); container.resize(width, height); positionInArea(container, xOffset, yOffset, contWidth, maxHeight, 0, radioButton.getAlignment().getHpos(), radioButton.getAlignment().getVpos()); final double ripplerWidth = width + 2 * padding; final double ripplerHeight = height + 2 * padding; rippler.resizeRelocate((width / 2 + xOffset) - ripplerWidth / 2, (height / 2 + yOffset) + 2 - ripplerHeight / 2, ripplerWidth, ripplerHeight); } private void removeRadio() { // TODO: replace with removeIf for (int i = 0; i < getChildren().size(); i++) { if ("radio".equals(getChildren().get(i).getStyleClass().get(0))) { getChildren().remove(i); } } } private void updateColors() { boolean isSelected = getSkinnable().isSelected(); Color unSelectedColor = ((JFXRadioButton) getSkinnable()).getUnSelectedColor(); Color selectedColor = ((JFXRadioButton) getSkinnable()).getSelectedColor(); rippler.setRipplerFill(isSelected ? selectedColor : unSelectedColor); radio.setStroke(isSelected ? selectedColor : unSelectedColor); } @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { return super.computeMinWidth(height, topInset, rightInset, bottomInset, leftInset) + snapSize(radio.minWidth(-1)) + padding / 3; } @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { return super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset) + snapSizeX(radio.prefWidth(-1)) + padding / 3; } private static double computeXOffset(double width, double contentWidth, HPos hpos) { switch (hpos) { case LEFT: return 0; case CENTER: return (width - contentWidth) / 2; case RIGHT: return width - contentWidth; } return 0; } private static double computeYOffset(double height, double contentHeight, VPos vpos) { switch (vpos) { case TOP: return 0; case CENTER: return (height - contentHeight) / 2; case BOTTOM: return height - contentHeight; default: return 0; } } @Override public void dispose() { super.dispose(); timer.dispose(); timer = null; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/JFXTextAreaSkinHavenoStyle.java ================================================ package haveno.desktop.components; import com.jfoenix.adapters.ReflectionHelper; import com.jfoenix.controls.JFXTextArea; import com.jfoenix.skins.PromptLinesWrapper; import com.jfoenix.skins.ValidationPane; import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.control.ScrollPane; import javafx.scene.control.skin.TextAreaSkin; import javafx.scene.layout.Background; import javafx.scene.layout.BackgroundFill; import javafx.scene.layout.CornerRadii; import javafx.scene.layout.Region; import javafx.scene.paint.Color; import javafx.scene.text.Text; import java.lang.reflect.Field; import java.util.Arrays; /** * Code copied and adapted from com.jfoenix.skins.JFXTextAreaSkin */ public class JFXTextAreaSkinHavenoStyle extends TextAreaSkin { private boolean invalid = true; private ScrollPane scrollPane; private Text promptText; private ValidationPane errorContainer; private PromptLinesWrapper linesWrapper; public JFXTextAreaSkinHavenoStyle(JFXTextArea textArea) { super(textArea); // init text area properties scrollPane = (ScrollPane) getChildren().get(0); textArea.setWrapText(true); linesWrapper = new PromptLinesWrapper<>( textArea, promptTextFillProperty(), textArea.textProperty(), textArea.promptTextProperty(), () -> promptText); linesWrapper.init(() -> createPromptNode(), scrollPane); errorContainer = new ValidationPane<>(textArea); getChildren().addAll(linesWrapper.line, linesWrapper.focusedLine, linesWrapper.promptContainer, errorContainer); registerChangeListener(textArea.disableProperty(), obs -> linesWrapper.updateDisabled()); registerChangeListener(textArea.focusColorProperty(), obs -> linesWrapper.updateFocusColor()); registerChangeListener(textArea.unFocusColorProperty(), obs -> linesWrapper.updateUnfocusColor()); registerChangeListener(textArea.disableAnimationProperty(), obs -> errorContainer.updateClip()); } @Override protected void layoutChildren(final double x, final double y, final double w, final double h) { super.layoutChildren(x, y, w, h); final double height = getSkinnable().getHeight(); final double width = getSkinnable().getWidth(); linesWrapper.layoutLines(x - 2, y - 2, width, h, height, promptText == null ? 0 : promptText.getLayoutBounds().getHeight() + 3); errorContainer.layoutPane(x, height + linesWrapper.focusedLine.getHeight(), width, h); linesWrapper.updateLabelFloatLayout(); if (invalid) { invalid = false; // set the default background of text area viewport to white Region viewPort = (Region) scrollPane.getChildrenUnmodifiable().get(0); viewPort.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY))); // reapply css of scroll pane in case set by the user viewPort.applyCss(); errorContainer.invalid(w); // focus linesWrapper.invalid(); } } private void createPromptNode() { if (promptText != null || !linesWrapper.usePromptText.get()) { return; } promptText = new Text(); promptText.setManaged(false); promptText.getStyleClass().add("text"); promptText.visibleProperty().bind(linesWrapper.usePromptText); promptText.fontProperty().bind(getSkinnable().fontProperty()); promptText.textProperty().bind(getSkinnable().promptTextProperty()); promptText.fillProperty().bind(linesWrapper.animatedPromptTextFill); promptText.getTransforms().add(linesWrapper.promptTextScale); linesWrapper.promptContainer.getChildren().add(promptText); if (getSkinnable().isFocused() && ((JFXTextArea) getSkinnable()).isLabelFloat()) { promptText.setTranslateY(-Math.floor(scrollPane.getHeight())); linesWrapper.promptTextScale.setX(0.85); linesWrapper.promptTextScale.setY(0.85); } try { Field field = ReflectionHelper.getField(TextAreaSkin.class, "promptNode"); Object oldValue = field.get(this); if (oldValue != null) { removeHighlight(Arrays.asList(((Node) oldValue))); } field.set(this, promptText); } catch (Exception e) { e.printStackTrace(); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/JFXTextFieldSkinHavenoStyle.java ================================================ package haveno.desktop.components; import com.jfoenix.adapters.ReflectionHelper; import com.jfoenix.controls.base.IFXLabelFloatControl; import com.jfoenix.skins.PromptLinesWrapper; import com.jfoenix.skins.ValidationPane; import javafx.beans.property.DoubleProperty; import javafx.beans.value.ObservableDoubleValue; import javafx.scene.Node; import javafx.scene.control.TextField; import javafx.scene.control.skin.TextFieldSkin; import javafx.scene.layout.Pane; import javafx.scene.text.Text; import java.lang.reflect.Field; /** * Code copied and adapted from com.jfoenix.skins.JFXTextFieldSkin */ public class JFXTextFieldSkinHavenoStyle extends TextFieldSkin { private double inputLineExtension; private boolean invalid = true; private Text promptText; private Pane textPane; private Node textNode; private ObservableDoubleValue textRight; private DoubleProperty textTranslateX; private ValidationPane errorContainer; private PromptLinesWrapper linesWrapper; public JFXTextFieldSkinHavenoStyle(T textField, double inputLineExtension) { super(textField); textPane = (Pane) this.getChildren().get(0); this.inputLineExtension = inputLineExtension; // get parent fields textNode = ReflectionHelper.getFieldContent(TextFieldSkin.class, this, "textNode"); textTranslateX = ReflectionHelper.getFieldContent(TextFieldSkin.class, this, "textTranslateX"); textRight = ReflectionHelper.getFieldContent(TextFieldSkin.class, this, "textRight"); linesWrapper = new PromptLinesWrapper( textField, promptTextFillProperty(), textField.textProperty(), textField.promptTextProperty(), () -> promptText); linesWrapper.init(() -> createPromptNode(), textPane); ReflectionHelper.setFieldContent(TextFieldSkin.class, this, "usePromptText", linesWrapper.usePromptText); errorContainer = new ValidationPane<>(textField); getChildren().addAll(linesWrapper.line, linesWrapper.focusedLine, linesWrapper.promptContainer, errorContainer); registerChangeListener(textField.disableProperty(), obs -> linesWrapper.updateDisabled()); registerChangeListener(textField.focusColorProperty(), obs -> linesWrapper.updateFocusColor()); registerChangeListener(textField.unFocusColorProperty(), obs -> linesWrapper.updateUnfocusColor()); registerChangeListener(textField.disableAnimationProperty(), obs -> errorContainer.updateClip()); } @Override protected void layoutChildren(final double x, final double y, final double w, final double h) { super.layoutChildren(x, y, w, h); final double height = getSkinnable().getHeight(); final double width = getSkinnable().getWidth() + inputLineExtension; final double paddingLeft = getSkinnable().getPadding().getLeft(); linesWrapper.layoutLines(x, y, width, h, height, Math.floor(h)); errorContainer.layoutPane(x - paddingLeft, height + linesWrapper.focusedLine.getHeight(), width, h); if (getSkinnable().getWidth() > 0) { updateTextPos(); } linesWrapper.updateLabelFloatLayout(); if (invalid) { invalid = false; // update validation container errorContainer.invalid(w); // focus linesWrapper.invalid(); } } private void updateTextPos() { double textWidth = textNode.getLayoutBounds().getWidth(); final double promptWidth = promptText == null ? 0 : promptText.getLayoutBounds().getWidth(); switch (getSkinnable().getAlignment().getHpos()) { case CENTER: linesWrapper.promptTextScale.setPivotX(promptWidth / 2); double midPoint = textRight.get() / 2; double newX = midPoint - textWidth / 2; if (newX + textWidth <= textRight.get()) { textTranslateX.set(newX); } break; case LEFT: linesWrapper.promptTextScale.setPivotX(0); break; case RIGHT: linesWrapper.promptTextScale.setPivotX(promptWidth); break; } } private void createPromptNode() { if (promptText != null || !linesWrapper.usePromptText.get()) { return; } promptText = new Text(); promptText.setManaged(false); promptText.getStyleClass().add("text"); promptText.visibleProperty().bind(linesWrapper.usePromptText); promptText.fontProperty().bind(getSkinnable().fontProperty()); promptText.textProperty().bind(getSkinnable().promptTextProperty()); promptText.fillProperty().bind(linesWrapper.animatedPromptTextFill); promptText.setLayoutX(1); promptText.getTransforms().add(linesWrapper.promptTextScale); linesWrapper.promptContainer.getChildren().add(promptText); if (getSkinnable().isFocused() && ((IFXLabelFloatControl) getSkinnable()).isLabelFloat()) { promptText.setTranslateY(-Math.floor(textPane.getHeight())); linesWrapper.promptTextScale.setX(0.85); linesWrapper.promptTextScale.setY(0.85); } try { Field field = ReflectionHelper.getField(TextFieldSkin.class, "promptNode"); Object oldValue = field.get(this); if (oldValue != null) { textPane.getChildren().remove(oldValue); } field.set(this, promptText); } catch (Exception e) { e.printStackTrace(); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/MenuItem.java ================================================ /* * 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 . */ package haveno.desktop.components; import com.jfoenix.controls.JFXButton; import haveno.desktop.Navigation; import haveno.desktop.common.view.View; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ChangeListener; import javafx.geometry.Pos; import javafx.scene.control.Toggle; import javafx.scene.control.ToggleGroup; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; @Slf4j public class MenuItem extends JFXButton implements Toggle { private final Navigation navigation; private final ObjectProperty toggleGroupProperty = new SimpleObjectProperty<>(); private final Class viewClass; private final List> baseNavPath; private final BooleanProperty selectedProperty = new SimpleBooleanProperty(); private final ChangeListener listener; public MenuItem(Navigation navigation, ToggleGroup toggleGroup, String title, Class viewClass, List> baseNavPath) { this.navigation = navigation; this.viewClass = viewClass; this.baseNavPath = baseNavPath; setLabelText(title); setPrefHeight(40); setPrefWidth(240); setAlignment(Pos.CENTER_LEFT); toggleGroupProperty.set(toggleGroup); toggleGroup.getToggles().add(this); setUserData(getUid()); listener = (observable, oldValue, newValue) -> { Object userData = newValue.getUserData(); String uid = getUid(); if (newValue.isSelected() && userData != null && userData.equals(uid)) { getStyleClass().add("action-button"); } else { getStyleClass().remove("action-button"); } }; } /////////////////////////////////////////////////////////////////////////////////////////// // Toggle implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public ToggleGroup getToggleGroup() { return toggleGroupProperty.get(); } @Override public void setToggleGroup(ToggleGroup toggleGroup) { toggleGroupProperty.set(toggleGroup); } @Override public ObjectProperty toggleGroupProperty() { return toggleGroupProperty; } @Override public boolean isSelected() { return selectedProperty.get(); } @Override public BooleanProperty selectedProperty() { return selectedProperty; } @Override public void setSelected(boolean selected) { selectedProperty.set(selected); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void activate() { setOnAction((event) -> navigation.navigateTo(getNavPathClasses())); toggleGroupProperty.get().selectedToggleProperty().addListener(listener); } public void deactivate() { setOnAction(null); toggleGroupProperty.get().selectedToggleProperty().removeListener(listener); } public void setLabelText(String value) { setText(value.toUpperCase()); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// @NotNull private Class[] getNavPathClasses() { List> list = new ArrayList<>(baseNavPath); list.add(viewClass); //noinspection unchecked Class[] array = new Class[list.size()]; list.toArray(array); return array; } private String getUid() { return viewClass.getName(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/NewBadge.java ================================================ package haveno.desktop.components; import com.jfoenix.controls.JFXBadge; import haveno.core.locale.Res; import haveno.core.user.Preferences; import javafx.collections.MapChangeListener; import javafx.scene.Node; public class NewBadge extends JFXBadge { private final String key; public NewBadge(Node control, String key, Preferences preferences) { super(control); this.key = key; setText(Res.get("shared.new")); getStyleClass().add("new"); setEnabled(!preferences.getDontShowAgainMap().containsKey(key)); refreshBadge(); preferences.getDontShowAgainMapAsObservable().addListener((MapChangeListener) change -> { if (change.getKey().equals(key)) { setEnabled(!change.wasAdded()); refreshBadge(); } }); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/PasswordTextField.java ================================================ /* * 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 . */ package haveno.desktop.components; import com.jfoenix.controls.JFXPasswordField; import javafx.scene.control.Skin; public class PasswordTextField extends JFXPasswordField { public PasswordTextField() { super(); setLabelFloat(true); setMaxWidth(380); } @Override protected Skin createDefaultSkin() { return new JFXTextFieldSkinHavenoStyle<>(this, 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/PeerInfoIcon.java ================================================ /* * 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 . */ package haveno.desktop.components; import haveno.desktop.main.overlays.editor.PeerInfoWithTagEditor; import haveno.desktop.util.DisplayUtils; import haveno.core.alert.PrivateNotificationManager; import haveno.core.locale.Res; import haveno.core.offer.Offer; import haveno.core.trade.Trade; import haveno.core.user.Preferences; import haveno.network.p2p.NodeAddress; import com.google.common.base.Charsets; import javafx.scene.Group; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.control.Label; import javafx.scene.control.Tooltip; import javafx.scene.image.ImageView; import javafx.scene.input.MouseButton; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; import javafx.geometry.Point2D; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Map; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; @Slf4j public class PeerInfoIcon extends Group { protected Preferences preferences; protected final String fullAddress; protected String tooltipText; protected Label tagLabel; private Label numTradesLabel; protected Pane tagPane; protected Pane numTradesPane; protected int numTrades = 0; private final StringProperty tag; public PeerInfoIcon(NodeAddress nodeAddress, Preferences preferences) { this.preferences = preferences; this.fullAddress = nodeAddress != null ? nodeAddress.getFullAddress() : ""; this.tag = new SimpleStringProperty(""); } protected void createAvatar(Color ringColor) { double scaleFactor = getScaleFactor(); double outerSize = 26 * scaleFactor; Canvas outerBackground = new Canvas(outerSize, outerSize); GraphicsContext outerBackgroundGc = outerBackground.getGraphicsContext2D(); outerBackgroundGc.setFill(ringColor); outerBackgroundGc.fillOval(0, 0, outerSize, outerSize); outerBackground.setLayoutY(1 * scaleFactor); // inner circle int maxIndices = 15; int intValue = 0; try { MessageDigest md = MessageDigest.getInstance("SHA1"); byte[] bytes = md.digest(fullAddress.getBytes(Charsets.UTF_8)); intValue = Math.abs(((bytes[0] & 0xFF) << 24) | ((bytes[1] & 0xFF) << 16) | ((bytes[2] & 0xFF) << 8) | (bytes[3] & 0xFF)); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); log.error(e.toString()); } int index = (intValue % maxIndices) + 1; double saturation = (intValue % 1000) / 1000d; int red = (intValue >> 8) % 256; int green = (intValue >> 16) % 256; int blue = (intValue >> 24) % 256; Color innerColor = Color.rgb(red, green, blue); innerColor = innerColor.deriveColor(1, saturation, 0.8, 1); // reduce saturation and brightness double innerSize = scaleFactor * 22; Canvas innerBackground = new Canvas(innerSize, innerSize); GraphicsContext innerBackgroundGc = innerBackground.getGraphicsContext2D(); innerBackgroundGc.setFill(innerColor); innerBackgroundGc.fillOval(0, 0, innerSize, innerSize); innerBackground.setLayoutY(3 * scaleFactor); innerBackground.setLayoutX(2 * scaleFactor); ImageView avatarImageView = new ImageView(); avatarImageView.setId("avatar_" + index); avatarImageView.setLayoutX(0); avatarImageView.setLayoutY(1 * scaleFactor); avatarImageView.setFitHeight(scaleFactor * 26); avatarImageView.setFitWidth(scaleFactor * 26); numTradesPane = new Pane(); numTradesPane.relocate(scaleFactor * 18, scaleFactor * 14); numTradesPane.setMouseTransparent(true); ImageView numTradesCircle = new ImageView(); numTradesCircle.setId("image-green_circle_solid"); numTradesLabel = new AutoTooltipLabel(); numTradesLabel.relocate(scaleFactor * 5, scaleFactor * 1); numTradesLabel.setId("ident-num-label"); numTradesPane.getChildren().addAll(numTradesCircle, numTradesLabel); tagPane = new Pane(); tagPane.relocate(Math.round(scaleFactor * 18), scaleFactor * -2); tagPane.setMouseTransparent(true); ImageView tagCircle = new ImageView(); tagCircle.setId("image-blue_circle_solid"); tagLabel = new AutoTooltipLabel(); tagLabel.relocate(Math.round(scaleFactor * 5), scaleFactor * 1); tagLabel.setId("ident-num-label"); tagPane.getChildren().addAll(tagCircle, tagLabel); updatePeerInfoIcon(); getChildren().addAll(outerBackground, innerBackground, avatarImageView, tagPane, numTradesPane); } protected void addMouseListener(int numTrades, PrivateNotificationManager privateNotificationManager, @Nullable Trade trade, Offer offer, Preferences preferences, boolean useDevPrivilegeKeys, boolean isFiatCurrency, long peersAccountAge, long peersSignAge, String peersAccountAgeInfo, String peersSignAgeInfo, String accountSigningState) { final String accountAgeFormatted = isFiatCurrency ? peersAccountAge > -1 ? DisplayUtils.formatAccountAge(peersAccountAge) : Res.get("peerInfo.unknownAge") : null; final String signAgeFormatted = isFiatCurrency && peersSignAgeInfo != null ? peersSignAge > -1 ? DisplayUtils.formatAccountAge(peersSignAge) : Res.get("peerInfo.unknownAge") : null; setOnMouseClicked(e -> { if (e.getButton().equals(MouseButton.PRIMARY)) { new PeerInfoWithTagEditor(privateNotificationManager, trade, offer, preferences, useDevPrivilegeKeys) .fullAddress(fullAddress) .numTrades(numTrades) .accountAge(accountAgeFormatted) .signAge(signAgeFormatted) .accountAgeInfo(peersAccountAgeInfo) .signAgeInfo(peersSignAgeInfo) .accountSigningState(accountSigningState) .position(localToScene(new Point2D(0, 0))) .onSave(newTag -> { preferences.setTagForPeer(fullAddress, newTag); tag.set(newTag); }) .show(); } }); } protected double getScaleFactor() { return 1; } protected String getAccountAgeTooltip(Long accountAge) { return accountAge > -1 ? Res.get("peerInfoIcon.tooltip.age", DisplayUtils.formatAccountAge(accountAge)) : Res.get("peerInfoIcon.tooltip.unknownAge"); } protected void updatePeerInfoIcon() { if (numTrades > 0) { numTradesLabel.setText(numTrades > 99 ? "*" : String.valueOf(numTrades)); double scaleFactor = getScaleFactor(); if (numTrades > 9 && numTrades < 100) { numTradesLabel.relocate(scaleFactor * 2, scaleFactor * 1); } else { numTradesLabel.relocate(scaleFactor * 5, scaleFactor * 1); } } numTradesPane.setVisible(numTrades > 0); refreshTag(); } protected void refreshTag() { Map peerTagMap = preferences.getPeerTagMap(); if (peerTagMap.containsKey(fullAddress)) { tag.set(peerTagMap.get(fullAddress)); } Tooltip.install(this, new Tooltip(!tag.get().isEmpty() ? Res.get("peerInfoIcon.tooltip", tooltipText, tag.get()) : tooltipText)); if (!tag.get().isEmpty()) { tagLabel.setText(tag.get().substring(0, 1)); } tagPane.setVisible(!tag.get().isEmpty()); } protected StringProperty tagProperty() { return tag; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/PeerInfoIconDispute.java ================================================ /* * 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 . */ package haveno.desktop.components; import haveno.core.locale.Res; import haveno.core.user.Preferences; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; import static haveno.desktop.util.Colors.AVATAR_GREY; @Slf4j public class PeerInfoIconDispute extends PeerInfoIcon { public PeerInfoIconDispute(NodeAddress nodeAddress, String nrOfDisputes, long accountAge, Preferences preferences) { super(nodeAddress, preferences); tooltipText = Res.get("peerInfoIcon.tooltip.dispute", fullAddress, nrOfDisputes, getAccountAgeTooltip(accountAge)); // outer circle always display gray createAvatar(AVATAR_GREY); addMouseListener(numTrades, null, null, null, preferences, false, false, accountAge, 0L, null, null, null); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/PeerInfoIconMap.java ================================================ /* * 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 . */ package haveno.desktop.components; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import java.util.HashMap; import lombok.extern.slf4j.Slf4j; @Slf4j public class PeerInfoIconMap extends HashMap implements ChangeListener { @Override public PeerInfoIcon put(String key, PeerInfoIcon icon) { icon.tagProperty().addListener(this); return super.put(key, icon); } @Override public void changed(ObservableValue o, String oldVal, String newVal) { log.info("Updating avatar tags, the avatar map size is {}", size()); forEach((key, icon) -> { // We update all avatars, as some could be sharing the same tag. // We also temporarily remove listeners to prevent firing of // events while each icon's tagProperty is being reset. icon.tagProperty().removeListener(this); icon.refreshTag(); icon.tagProperty().addListener(this); }); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/PeerInfoIconSmall.java ================================================ package haveno.desktop.components; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.alert.PrivateNotificationManager; import haveno.core.offer.Offer; import haveno.core.trade.Trade; import haveno.core.user.Preferences; import haveno.network.p2p.NodeAddress; import javax.annotation.Nullable; public class PeerInfoIconSmall extends PeerInfoIconTrading { public PeerInfoIconSmall(NodeAddress nodeAddress, String role, Offer offer, Preferences preferences, AccountAgeWitnessService accountAgeWitnessService, boolean useDevPrivilegeKeys) { // We don't want to show number of trades in that case as it would be unreadable. // Also we don't need the privateNotificationManager as no interaction will take place with this icon. super(nodeAddress, role, 0, null, offer, preferences, accountAgeWitnessService, useDevPrivilegeKeys); } @Override protected double getScaleFactor() { return 0.6; } @Override protected void addMouseListener(int numTrades, PrivateNotificationManager privateNotificationManager, @Nullable Trade tradeModel, Offer offer, Preferences preferences, boolean useDevPrivilegeKeys, boolean isFiatCurrency, long peersAccountAge, long peersSignAge, String peersAccountAgeInfo, String peersSignAgeInfo, String accountSigningState) { } @Override protected void updatePeerInfoIcon() { numTradesPane.setVisible(false); tagPane.setVisible(false); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/PeerInfoIconTrading.java ================================================ /* * 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 . */ package haveno.desktop.components; import haveno.common.util.Tuple5; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.alert.PrivateNotificationManager; import haveno.core.locale.Res; import haveno.core.offer.Offer; import haveno.core.payment.payload.PaymentMethod; import haveno.core.trade.Trade; import haveno.core.user.Preferences; import haveno.network.p2p.NodeAddress; import javafx.scene.paint.Color; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import javax.annotation.Nullable; import java.util.Date; import static com.google.common.base.Preconditions.checkNotNull; import static haveno.desktop.util.Colors.AVATAR_BLUE; import static haveno.desktop.util.Colors.AVATAR_GREEN; import static haveno.desktop.util.Colors.AVATAR_ORANGE; import static haveno.desktop.util.Colors.AVATAR_RED; @Slf4j public class PeerInfoIconTrading extends PeerInfoIcon { private final AccountAgeWitnessService accountAgeWitnessService; private boolean isTraditionalCurrency; public PeerInfoIconTrading(NodeAddress nodeAddress, String role, int numTrades, PrivateNotificationManager privateNotificationManager, Offer offer, Preferences preferences, AccountAgeWitnessService accountAgeWitnessService, boolean useDevPrivilegeKeys) { this(nodeAddress, role, numTrades, privateNotificationManager, offer, null, preferences, accountAgeWitnessService, useDevPrivilegeKeys); } public PeerInfoIconTrading(NodeAddress nodeAddress, String role, int numTrades, PrivateNotificationManager privateNotificationManager, Trade Trade, Preferences preferences, AccountAgeWitnessService accountAgeWitnessService, boolean useDevPrivilegeKeys) { this(nodeAddress, role, numTrades, privateNotificationManager, Trade.getOffer(), Trade, preferences, accountAgeWitnessService, useDevPrivilegeKeys); } private PeerInfoIconTrading(NodeAddress nodeAddress, String role, int numTrades, PrivateNotificationManager privateNotificationManager, @Nullable Offer offer, @Nullable Trade trade, Preferences preferences, AccountAgeWitnessService accountAgeWitnessService, boolean useDevPrivilegeKeys) { super(nodeAddress, preferences); this.numTrades = numTrades; this.accountAgeWitnessService = accountAgeWitnessService; if (offer == null) { checkNotNull(trade, "Trade must not be null if offer is null."); offer = trade.getOffer(); } checkNotNull(offer, "Offer must not be null"); isTraditionalCurrency = offer.isTraditionalOffer(); initialize(role, offer, trade, privateNotificationManager, useDevPrivilegeKeys); } protected void initialize(String role, Offer offer, Trade trade, PrivateNotificationManager privateNotificationManager, boolean useDevPrivilegeKeys) { boolean hasTraded = numTrades > 0; Tuple5 peersAccount = getPeersAccountAge(trade, offer); Long accountAge = peersAccount.first; Long signAge = peersAccount.second; tooltipText = hasTraded ? Res.get("peerInfoIcon.tooltip.trade.traded", role, fullAddress, numTrades, getAccountAgeTooltip(accountAge)) : Res.get("peerInfoIcon.tooltip.trade.notTraded", role, fullAddress, getAccountAgeTooltip(accountAge)); createAvatar(getRingColor(offer, trade, accountAge, signAge)); addMouseListener(numTrades, privateNotificationManager, trade, offer, preferences, useDevPrivilegeKeys, isTraditionalCurrency, accountAge, signAge, peersAccount.third, peersAccount.fourth, peersAccount.fifth); } @Override protected String getAccountAgeTooltip(Long accountAge) { return isTraditionalCurrency ? super.getAccountAgeTooltip(accountAge) : ""; } protected Color getRingColor(Offer offer, Trade Trade, Long accountAge, Long signAge) { // outer circle // for cryptos we always display green Color ringColor = AVATAR_GREEN; if (isTraditionalCurrency) { switch (accountAgeWitnessService.getPeersAccountAgeCategory(hasChargebackRisk(Trade, offer) ? signAge : accountAge)) { case TWO_MONTHS_OR_MORE: ringColor = AVATAR_GREEN; break; case ONE_TO_TWO_MONTHS: ringColor = AVATAR_BLUE; break; case LESS_ONE_MONTH: ringColor = AVATAR_ORANGE; break; case UNVERIFIED: default: ringColor = AVATAR_RED; break; } } return ringColor; } /** * @param Trade Open trade for trading peer info to be shown * @param offer Open offer for trading peer info to be shown * @return account age, sign age, account info, sign info, sign state */ private Tuple5 getPeersAccountAge(@Nullable Trade Trade, @Nullable Offer offer) { AccountAgeWitnessService.SignState signState = null; long signAge = -1L; long accountAge = -1L; if (Trade != null) { offer = Trade.getOffer(); if (offer == null) { // unexpected return new Tuple5<>(signAge, accountAge, Res.get("peerInfo.age.noRisk"), null, null); } if (Trade instanceof Trade) { Trade trade = Trade; signState = accountAgeWitnessService.getSignState(trade); signAge = accountAgeWitnessService.getWitnessSignAge(trade, new Date()); accountAge = accountAgeWitnessService.getAccountAge(trade); } } else { checkNotNull(offer, "Offer must not be null if trade is null."); signState = accountAgeWitnessService.getSignState(offer); signAge = accountAgeWitnessService.getWitnessSignAge(offer, new Date()); accountAge = accountAgeWitnessService.getAccountAge(offer); } if (signState != null && hasChargebackRisk(Trade, offer)) { String signAgeInfo = Res.get("peerInfo.age.chargeBackRisk"); String accountSigningState = StringUtils.capitalize(signState.getDisplayString()); if (signState.equals(AccountAgeWitnessService.SignState.UNSIGNED)) { signAgeInfo = null; } return new Tuple5<>(accountAge, signAge, Res.get("peerInfo.age.noRisk"), signAgeInfo, accountSigningState); } return new Tuple5<>(accountAge, signAge, Res.get("peerInfo.age.noRisk"), null, null); } private static boolean hasChargebackRisk(@Nullable Trade Trade, @Nullable Offer offer) { Offer offerToCheck = Trade != null ? Trade.getOffer() : offer; return offerToCheck != null && PaymentMethod.hasChargebackRisk(offerToCheck.getPaymentMethod(), offerToCheck.getCounterCurrencyCode()); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/PopOverWrapper.java ================================================ /* * 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 . */ package haveno.desktop.components; import haveno.common.UserThread; import haveno.desktop.components.controlsfx.control.PopOver; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; public class PopOverWrapper { private PopOver popover; private Supplier popoverSupplier; private boolean hidePopover; private PopOverState state = PopOverState.HIDDEN; enum PopOverState { HIDDEN, SHOWING, SHOWN, HIDING } public void showPopOver(Supplier popoverSupplier) { this.popoverSupplier = popoverSupplier; hidePopover = false; if (state == PopOverState.HIDDEN) { state = PopOverState.SHOWING; popover = popoverSupplier.get(); UserThread.runAfter(() -> { state = PopOverState.SHOWN; if (hidePopover) { // For some reason, this can result in a brief flicker when invoked // from a 'runAfter' callback, rather than directly. So make the delay // very short (25ms) so that we don't reach here often: hidePopOver(); } }, 25, TimeUnit.MILLISECONDS); } } public void hidePopOver() { hidePopover = true; if (state == PopOverState.SHOWN) { state = PopOverState.HIDING; popover.hide(); UserThread.runAfter(() -> { state = PopOverState.HIDDEN; if (!hidePopover) { showPopOver(popoverSupplier); } }, 250, TimeUnit.MILLISECONDS); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/SimpleMarkdownLabel.java ================================================ /* * 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 . */ package haveno.desktop.components; import haveno.core.util.SimpleMarkdownParser; import haveno.desktop.util.GUIUtil; import javafx.scene.Node; import javafx.scene.control.Hyperlink; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; import java.util.List; import java.util.stream.Collectors; public class SimpleMarkdownLabel extends TextFlow { public SimpleMarkdownLabel(String markdown) { super(); getStyleClass().add("markdown-label"); if (markdown != null) { updateContent(markdown); } } public void updateContent(String markdown) { List items = SimpleMarkdownParser .parse(markdown) .stream() .map(node -> { if (node instanceof SimpleMarkdownParser.HyperlinkNode) { var item = ((SimpleMarkdownParser.HyperlinkNode) node); Hyperlink hyperlink = new Hyperlink(item.getText()); hyperlink.setOnAction(e -> GUIUtil.openWebPage(item.getHref())); return hyperlink; } else { var item = ((SimpleMarkdownParser.TextNode) node); return new Text(item.getText()); } }) .collect(Collectors.toList()); getChildren().setAll(items); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/TableGroupHeadline.java ================================================ /* * 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 . */ package haveno.desktop.components; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.geometry.Insets; import javafx.scene.control.Label; import javafx.scene.layout.GridPane; import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; import lombok.extern.slf4j.Slf4j; @Slf4j public class TableGroupHeadline extends Pane { private final Label label; private final StringProperty text = new SimpleStringProperty(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public TableGroupHeadline() { this(""); } public TableGroupHeadline(String title) { text.set(title); GridPane.setMargin(this, new Insets(-10, -10, -10, -10)); GridPane.setColumnSpan(this, 2); Pane bg = new StackPane(); bg.setId("table-group-headline"); bg.prefWidthProperty().bind(widthProperty()); bg.prefHeightProperty().bind(heightProperty()); label = new AutoTooltipLabel(); label.textProperty().bind(text); label.setLayoutX(8); label.setPadding(new Insets(-8, 7, 0, 5)); setActive(); getChildren().addAll(bg, label); } public void setInactive() { setId("titled-group-bg"); label.setId("titled-group-bg-label"); } private void setActive() { setId("titled-group-bg-active"); label.setId("titled-group-bg-label-active"); label.getStyleClass().add("highlight-static"); } public String getText() { return text.get(); } public StringProperty textProperty() { return text; } public void setText(String text) { this.text.set(text); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/TextFieldWithCopyIcon.java ================================================ /* * 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 . */ package haveno.desktop.components; import com.jfoenix.controls.JFXTextField; import haveno.common.UserThread; import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.Layout; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; import javafx.scene.layout.AnchorPane; public class TextFieldWithCopyIcon extends AnchorPane { private final StringProperty text = new SimpleStringProperty(); private final TextField textField; private boolean copyWithoutCurrencyPostFix; private boolean copyTextAfterDelimiter; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public TextFieldWithCopyIcon() { this(null); } public TextFieldWithCopyIcon(String customStyleClass) { Label copyLabel = new Label(); copyLabel.setLayoutY(Layout.FLOATING_ICON_Y); copyLabel.getStyleClass().addAll("icon", "highlight"); if (customStyleClass != null) copyLabel.getStyleClass().add(customStyleClass + "-icon"); copyLabel.setTooltip(new Tooltip(Res.get("shared.copyToClipboard"))); copyLabel.setGraphic(GUIUtil.getCopyIcon()); copyLabel.setOnMouseClicked(e -> { String text = getText(); if (text != null && text.length() > 0) { String copyText; if (copyWithoutCurrencyPostFix) { String[] strings = text.split(" "); if (strings.length > 1) copyText = strings[0]; // exclude the BTC postfix else copyText = text; } else if (copyTextAfterDelimiter) { String[] strings = text.split(" "); if (strings.length > 1) copyText = strings[2]; // exclude the part before / (slash included) else copyText = text; } else { copyText = text; } Utilities.copyToClipboard(copyText); Tooltip tp = new Tooltip(Res.get("shared.copiedToClipboard")); Node node = (Node) e.getSource(); UserThread.runAfter(() -> tp.hide(), 1); tp.show(node, e.getScreenX() + Layout.PADDING, e.getScreenY() + Layout.PADDING); } }); textField = new JFXTextField(); textField.setEditable(false); if (customStyleClass != null) textField.getStyleClass().add(customStyleClass); textField.textProperty().bindBidirectional(text); AnchorPane.setRightAnchor(copyLabel, 5.0); AnchorPane.setRightAnchor(textField, 30.0); AnchorPane.setLeftAnchor(textField, 0.0); AnchorPane.setTopAnchor(copyLabel, 0.0); AnchorPane.setBottomAnchor(copyLabel, 0.0); AnchorPane.setTopAnchor(textField, 0.0); AnchorPane.setBottomAnchor(textField, 0.0); textField.focusTraversableProperty().set(focusTraversableProperty().get()); getChildren().addAll(textField, copyLabel); } public void setPromptText(String value) { textField.setPromptText(value); } /////////////////////////////////////////////////////////////////////////////////////////// // Getter/Setter /////////////////////////////////////////////////////////////////////////////////////////// public String getText() { return text.get(); } public StringProperty textProperty() { return text; } public void setText(String text) { this.text.set(text); } public void setTooltip(Tooltip toolTip) { textField.setTooltip(toolTip); } public void setCopyWithoutCurrencyPostFix(boolean copyWithoutCurrencyPostFix) { this.copyWithoutCurrencyPostFix = copyWithoutCurrencyPostFix; } public void setCopyTextAfterDelimiter(boolean copyTextAfterDelimiter) { this.copyTextAfterDelimiter = copyTextAfterDelimiter; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/TextFieldWithIcon.java ================================================ /* * 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 . */ package haveno.desktop.components; import com.jfoenix.controls.JFXTextField; import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; import haveno.common.UserThread; import haveno.desktop.util.Layout; import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.layout.AnchorPane; import javafx.scene.text.TextAlignment; import lombok.Getter; public class TextFieldWithIcon extends AnchorPane { @Getter private final Label iconLabel; @Getter private final TextField textField; private final Label dummyTextField; public TextFieldWithIcon() { textField = new JFXTextField(); textField.setEditable(false); textField.setFocusTraversable(false); setLeftAnchor(textField, 0d); setRightAnchor(textField, 0d); dummyTextField = new Label(); dummyTextField.setWrapText(true); dummyTextField.setAlignment(Pos.CENTER_LEFT); dummyTextField.setTextAlignment(TextAlignment.LEFT); dummyTextField.setMouseTransparent(true); dummyTextField.setFocusTraversable(false); setLeftAnchor(dummyTextField, 0d); dummyTextField.setVisible(false); iconLabel = new Label(); iconLabel.setLayoutX(0); iconLabel.setTranslateX(-15); iconLabel.setLayoutY(Layout.FLOATING_ICON_Y - 2); dummyTextField.widthProperty().addListener((observable, oldValue, newValue) -> { iconLabel.setLayoutX(dummyTextField.widthProperty().get() + 35 + Layout.FLOATING_ICON_Y); }); getChildren().addAll(textField, dummyTextField, iconLabel); } public void setIcon(AwesomeIcon iconLabel) { UserThread.execute(() -> { AwesomeDude.setIcon(this.iconLabel, iconLabel); }); } public void setText(String text) { UserThread.execute(() -> { textField.setText(text); dummyTextField.setText(text); }); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/TitledGroupBg.java ================================================ /* * 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 . */ package haveno.desktop.components; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.GUIUtil; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.text.Text; public class TitledGroupBg extends Pane { private final HBox box; private final Label label; private final StringProperty text = new SimpleStringProperty(); private Text helpIcon; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public TitledGroupBg() { GridPane.setMargin(this, new Insets(-10, -10, -10, -10)); GridPane.setColumnSpan(this, 2); box = new HBox(); box.setSpacing(4); box.setLayoutX(4); box.setLayoutY(-8); box.setPadding(new Insets(0, 7, 0, 5)); box.setAlignment(Pos.CENTER_LEFT); label = new AutoTooltipLabel(); label.textProperty().bind(text); setActive(); box.getChildren().add(label); getChildren().add(box); } public void setInactive() { resetStyles(); getStyleClass().add("titled-group-bg"); label.getStyleClass().add("titled-group-bg-label"); } private void resetStyles() { getStyleClass().removeAll("titled-group-bg", "titled-group-bg-active"); label.getStyleClass().removeAll("titled-group-bg-label", "titled-group-bg-label-active"); } private void setActive() { resetStyles(); getStyleClass().add("titled-group-bg-active"); label.getStyleClass().add("titled-group-bg-label-active"); } public StringProperty textProperty() { return text; } public void setText(String text) { this.text.set(text); } public void setHelpUrl(String helpUrl) { if (helpIcon == null) { helpIcon = FormBuilder.getIcon(MaterialDesignIcon.HELP_CIRCLE_OUTLINE, "1em"); helpIcon.getStyleClass().addAll("icon", "link-icon"); box.getChildren().add(helpIcon); } helpIcon.setOnMouseClicked(e -> GUIUtil.openWebPage(helpUrl)); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/TooltipUtil.java ================================================ /* * 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 . */ package haveno.desktop.components; import javafx.scene.control.Labeled; import javafx.scene.control.SkinBase; import javafx.scene.control.Tooltip; import javafx.scene.text.Text; public class TooltipUtil { public static void showTooltipIfTruncated(SkinBase skinBase, Labeled labeled) { for (Object node : skinBase.getChildren()) { if (node instanceof Text) { String displayedText = ((Text) node).getText(); String untruncatedText = labeled.getText(); if (displayedText.equals(untruncatedText)) { if (labeled.getTooltip() != null) { labeled.setTooltip(null); } } else if (untruncatedText != null && !untruncatedText.trim().isEmpty()) { final Tooltip tooltip = new Tooltip(untruncatedText); // Force tooltip to use color, as it takes in some cases the color of the parent label // and can't be overridden by class or id tooltip.setStyle("-fx-text-fill: -bs-rd-tooltip-truncated;"); labeled.setTooltip(tooltip); } } } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/TxIdTextField.java ================================================ /* * 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 . */ package haveno.desktop.components; import com.jfoenix.controls.JFXTextField; import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; import haveno.common.UserThread; import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.core.trade.Trade; import haveno.core.user.BlockChainExplorer; import haveno.core.user.Preferences; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.components.indicator.TxConfidenceIndicator; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.Layout; import javafx.beans.value.ChangeListener; import javafx.scene.Cursor; import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; import javafx.scene.layout.AnchorPane; import lombok.Getter; import lombok.Setter; import monero.daemon.model.MoneroTx; import monero.wallet.model.MoneroWalletListener; import javax.annotation.Nullable; public class TxIdTextField extends AnchorPane { @Setter private static Preferences preferences; @Setter private static XmrWalletService xmrWalletService; @Getter private final TextField textField; private final Tooltip progressIndicatorTooltip; private final TxConfidenceIndicator txConfidenceIndicator; private final Label copyLabel, blockExplorerIcon, missingTxWarningIcon; private MoneroWalletListener walletListener; private ChangeListener tradeListener; private Trade trade; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public TxIdTextField() { txConfidenceIndicator = new TxConfidenceIndicator(); txConfidenceIndicator.setFocusTraversable(false); txConfidenceIndicator.setMaxSize(20, 20); txConfidenceIndicator.setId("funds-confidence"); txConfidenceIndicator.setLayoutY(1); txConfidenceIndicator.setProgress(0); txConfidenceIndicator.setVisible(false); AnchorPane.setRightAnchor(txConfidenceIndicator, 0.0); AnchorPane.setTopAnchor(txConfidenceIndicator, Layout.FLOATING_ICON_Y); progressIndicatorTooltip = new Tooltip("-"); txConfidenceIndicator.setTooltip(progressIndicatorTooltip); copyLabel = new Label(); copyLabel.setLayoutY(Layout.FLOATING_ICON_Y); copyLabel.getStyleClass().addAll("icon", "highlight"); copyLabel.setTooltip(new Tooltip(Res.get("txIdTextField.copyIcon.tooltip"))); copyLabel.setGraphic(GUIUtil.getCopyIcon()); copyLabel.setCursor(Cursor.HAND); AnchorPane.setRightAnchor(copyLabel, 30.0); Tooltip tooltip = new Tooltip(Res.get("txIdTextField.blockExplorerIcon.tooltip")); blockExplorerIcon = new Label(); blockExplorerIcon.getStyleClass().addAll("icon", "highlight"); blockExplorerIcon.setTooltip(tooltip); AwesomeDude.setIcon(blockExplorerIcon, AwesomeIcon.EXTERNAL_LINK); blockExplorerIcon.setMinWidth(20); AnchorPane.setRightAnchor(blockExplorerIcon, 52.0); AnchorPane.setTopAnchor(blockExplorerIcon, Layout.FLOATING_ICON_Y); missingTxWarningIcon = new Label(); missingTxWarningIcon.getStyleClass().addAll("icon", "error-icon"); AwesomeDude.setIcon(missingTxWarningIcon, AwesomeIcon.WARNING_SIGN); missingTxWarningIcon.setTooltip(new Tooltip(Res.get("txIdTextField.missingTx.warning.tooltip"))); missingTxWarningIcon.setMinWidth(20); AnchorPane.setRightAnchor(missingTxWarningIcon, 52.0); AnchorPane.setTopAnchor(missingTxWarningIcon, Layout.FLOATING_ICON_Y); missingTxWarningIcon.setVisible(false); missingTxWarningIcon.setManaged(false); textField = new JFXTextField(); textField.setId("address-text-field"); textField.setEditable(false); textField.setTooltip(tooltip); AnchorPane.setRightAnchor(textField, 80.0); AnchorPane.setLeftAnchor(textField, 0.0); textField.focusTraversableProperty().set(focusTraversableProperty().get()); getChildren().addAll(textField, missingTxWarningIcon, blockExplorerIcon, copyLabel, txConfidenceIndicator); } public void setup(@Nullable String txId) { setup(txId, null); } public void setup(@Nullable String txId, Trade trade) { this.trade = trade; if (walletListener != null) { xmrWalletService.removeWalletListener(walletListener); walletListener = null; } if (tradeListener != null) { trade.getDepositTxsUpdateCounter().removeListener(tradeListener); tradeListener = null; } if (txId == null) { textField.setText(Res.get("shared.na")); textField.setId("address-text-field-error"); blockExplorerIcon.setVisible(false); blockExplorerIcon.setManaged(false); copyLabel.setVisible(false); copyLabel.setManaged(false); txConfidenceIndicator.setVisible(false); missingTxWarningIcon.setVisible(true); missingTxWarningIcon.setManaged(true); return; } // subscribe for tx updates if (trade == null) { walletListener = new MoneroWalletListener() { @Override public void onNewBlock(long height) { updateConfidence(txId, trade, false, height); } }; xmrWalletService.addWalletListener(walletListener); // TODO: this only listens for new blocks, listen for double spend } else { tradeListener = (observable, oldValue, newValue) -> { updateConfidence(txId, trade, null, null); }; trade.getDepositTxsUpdateCounter().addListener(tradeListener); } textField.setText(txId); textField.setOnMouseClicked(mouseEvent -> openBlockExplorer(txId)); blockExplorerIcon.setOnMouseClicked(mouseEvent -> openBlockExplorer(txId)); copyLabel.setOnMouseClicked(e -> { Utilities.copyToClipboard(txId); Tooltip tp = new Tooltip(Res.get("shared.copiedToClipboard")); Node node = (Node) e.getSource(); UserThread.runAfter(() -> tp.hide(), 1); tp.show(node, e.getScreenX() + Layout.PADDING, e.getScreenY() + Layout.PADDING); }); txConfidenceIndicator.setVisible(true); // update off main thread new Thread(() -> updateConfidence(txId, trade, true, null)).start(); } public void cleanup() { if (xmrWalletService != null && walletListener != null) { xmrWalletService.removeWalletListener(walletListener); walletListener = null; } if (tradeListener != null) { trade.getDepositTxsUpdateCounter().removeListener(tradeListener); tradeListener = null; } trade = null; textField.setOnMouseClicked(null); blockExplorerIcon.setOnMouseClicked(null); copyLabel.setOnMouseClicked(null); textField.setText(""); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void openBlockExplorer(String txId) { if (preferences != null) { BlockChainExplorer blockChainExplorer = preferences.getBlockChainExplorer(); GUIUtil.openWebPage(blockChainExplorer.txUrl + txId, false); } } private synchronized void updateConfidence(String txId, Trade trade, Boolean useCache, Long height) { MoneroTx tx = null; try { if (trade == null) { tx = useCache ? xmrWalletService.getXmrConnectionService().getTxWithCache(txId) : xmrWalletService.getXmrConnectionService().getTx(txId); tx.setNumConfirmations(tx.isConfirmed() ? (height == null ? xmrWalletService.getXmrConnectionService().getLastInfo().getHeight() : height) - tx.getHeight(): 0l); // TODO: don't set if tx.getNumConfirmations() works reliably on non-local testnet } else { if (txId.equals(trade.getMaker().getDepositTxHash())) tx = trade.getMakerDepositTx(); else if (txId.equals(trade.getTaker().getDepositTxHash())) tx = trade.getTakerDepositTx(); } } catch (Exception e) { // do nothing } updateConfidence(tx, trade); } private void updateConfidence(MoneroTx tx, Trade trade) { UserThread.execute(() -> { GUIUtil.updateConfidence(tx, trade, progressIndicatorTooltip, txConfidenceIndicator); if (txConfidenceIndicator.getProgress() != 0) { AnchorPane.setRightAnchor(txConfidenceIndicator, 0.0); } if (txConfidenceIndicator.getProgress() >= 1.0 && walletListener != null) { xmrWalletService.removeWalletListener(walletListener); // unregister listener walletListener = null; } }); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/chart/ChartDataModel.java ================================================ /* * 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 . */ package haveno.desktop.components.chart; import haveno.desktop.common.model.ActivatableDataModel; import java.time.Instant; import java.time.temporal.TemporalAdjuster; import java.util.Map; import java.util.function.BinaryOperator; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; public abstract class ChartDataModel extends ActivatableDataModel { protected final TemporalAdjusterModel temporalAdjusterModel = new TemporalAdjusterModel(); protected Predicate dateFilter = e -> true; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public ChartDataModel() { super(); } @Override public void activate() { dateFilter = e -> true; } /////////////////////////////////////////////////////////////////////////////////////////// // TemporalAdjusterModel delegates /////////////////////////////////////////////////////////////////////////////////////////// void setTemporalAdjuster(TemporalAdjuster temporalAdjuster) { temporalAdjusterModel.setTemporalAdjuster(temporalAdjuster); } TemporalAdjuster getTemporalAdjuster() { return temporalAdjusterModel.getTemporalAdjuster(); } public long toTimeInterval(Instant instant) { return temporalAdjusterModel.toTimeInterval(instant); } /////////////////////////////////////////////////////////////////////////////////////////// // Date filter predicate /////////////////////////////////////////////////////////////////////////////////////////// public Predicate getDateFilter() { return dateFilter; } void setDateFilter(long from, long to) { dateFilter = value -> value >= from && value <= to; } /////////////////////////////////////////////////////////////////////////////////////////// // Data /////////////////////////////////////////////////////////////////////////////////////////// protected abstract void invalidateCache(); protected Map getMergedMap(Map map1, Map map2, BinaryOperator mergeFunction) { return Stream.concat(map1.entrySet().stream(), map2.entrySet().stream()) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, mergeFunction)); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/chart/ChartView.java ================================================ /* * 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 . */ package haveno.desktop.components.chart; import haveno.common.UserThread; import haveno.core.locale.Res; import haveno.desktop.common.view.ActivatableViewAndModel; import haveno.desktop.components.AutoTooltipSlideToggleButton; import haveno.desktop.components.AutoTooltipToggleButton; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.event.EventHandler; import javafx.geometry.Bounds; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.geometry.Side; import javafx.scene.Node; import javafx.scene.chart.Axis; import javafx.scene.chart.LineChart; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; import javafx.scene.control.Label; import javafx.scene.control.SplitPane; import javafx.scene.control.Toggle; import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleGroup; import javafx.scene.control.Tooltip; import javafx.scene.input.MouseEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.stage.PopupWindow; import javafx.stage.Stage; import javafx.util.Duration; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.time.temporal.TemporalAdjuster; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.stream.Stream; @Slf4j public abstract class ChartView> extends ActivatableViewAndModel { private Pane center; private SplitPane timelineNavigation; protected NumberAxis xAxis, yAxis; protected LineChart chart; private HBox timelineLabels, legendBox2, legendBox3; private final ToggleGroup timeIntervalToggleGroup = new ToggleGroup(); protected final Set> activeSeries = new HashSet<>(); protected final Map seriesIndexMap = new HashMap<>(); protected final Map legendToggleBySeriesName = new HashMap<>(); private final List dividerNodes = new ArrayList<>(); private final List dividerNodesTooltips = new ArrayList<>(); private ChangeListener widthListener; private ChangeListener timeIntervalChangeListener; private ListChangeListener nodeListChangeListener; private int maxSeriesSize; private boolean centerPanePressed; private double x; @Setter protected boolean isRadioButtonBehaviour; @Setter private int maxDataPointsForShowingSymbols = 100; private ChangeListener yAxisWidthListener; private EventHandler dividerMouseDraggedEventHandler; private final StringProperty fromProperty = new SimpleStringProperty(); private final StringProperty toProperty = new SimpleStringProperty(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public ChartView(T model) { super(model); root = new VBox(); } /////////////////////////////////////////////////////////////////////////////////////////// // Lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Override public void initialize() { // We need to call prepareInitialize as we are not using FXMLLoader prepareInitialize(); maxSeriesSize = 0; centerPanePressed = false; x = 0; // Series createSeries(); // Time interval HBox timeIntervalBox = getTimeIntervalBox(); // chart xAxis = getXAxis(); yAxis = getYAxis(); chart = getChart(); // Timeline navigation addTimelineNavigation(); // Legend HBox legendBox1 = initLegendsAndGetLegendBox(getSeriesForLegend1()); Collection> seriesForLegend2 = getSeriesForLegend2(); if (seriesForLegend2 != null && !seriesForLegend2.isEmpty()) { legendBox2 = initLegendsAndGetLegendBox(seriesForLegend2); } Collection> seriesForLegend3 = getSeriesForLegend3(); if (seriesForLegend3 != null && !seriesForLegend3.isEmpty()) { legendBox3 = initLegendsAndGetLegendBox(seriesForLegend3); } // Set active series/legends defineAndAddActiveSeries(); // Put all together VBox timelineNavigationBox = new VBox(); double paddingLeft = 15; double paddingRight = 89; // Y-axis width depends on data so we register a listener to get correct value yAxisWidthListener = (observable, oldValue, newValue) -> { double width = newValue.doubleValue(); if (width > 0) { double rightPadding = width + 14; VBox.setMargin(timeIntervalBox, new Insets(0, rightPadding, 0, paddingLeft)); VBox.setMargin(timelineNavigation, new Insets(0, rightPadding, 0, paddingLeft)); VBox.setMargin(timelineLabels, new Insets(0, rightPadding, 0, paddingLeft)); VBox.setMargin(legendBox1, new Insets(10, rightPadding, 0, paddingLeft)); if (legendBox2 != null) { VBox.setMargin(legendBox2, new Insets(-20, rightPadding, 0, paddingLeft)); } if (legendBox3 != null) { VBox.setMargin(legendBox3, new Insets(-20, rightPadding, 0, paddingLeft)); } if (model.getDividerPositions()[0] == 0 && model.getDividerPositions()[1] == 1) { resetTimeNavigation(); } } }; VBox.setMargin(timeIntervalBox, new Insets(0, paddingRight, 0, paddingLeft)); VBox.setMargin(timelineNavigation, new Insets(0, paddingRight, 0, paddingLeft)); VBox.setMargin(timelineLabels, new Insets(0, paddingRight, 0, paddingLeft)); VBox.setMargin(legendBox1, new Insets(0, paddingRight, 0, paddingLeft)); timelineNavigationBox.getChildren().addAll(timelineNavigation, timelineLabels, legendBox1); if (legendBox2 != null) { VBox.setMargin(legendBox2, new Insets(-20, paddingRight, 0, paddingLeft)); timelineNavigationBox.getChildren().add(legendBox2); } if (legendBox3 != null) { VBox.setMargin(legendBox3, new Insets(-20, paddingRight, 0, paddingLeft)); timelineNavigationBox.getChildren().add(legendBox3); } root.getChildren().addAll(timeIntervalBox, chart, timelineNavigationBox); // Listeners widthListener = (observable, oldValue, newValue) -> { timelineNavigation.setDividerPosition(0, model.getDividerPositions()[0]); timelineNavigation.setDividerPosition(1, model.getDividerPositions()[1]); }; timeIntervalChangeListener = (observable, oldValue, newValue) -> { if (newValue != null) { onTimeIntervalChanged(newValue); } }; nodeListChangeListener = c -> { while (c.next()) { if (c.wasAdded()) { c.getAddedSubList().stream() .filter(node -> node instanceof Text) .forEach(node -> node.getStyleClass().add("axis-tick-mark-text-node")); } } }; } @Override public void activate() { timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]); TemporalAdjuster temporalAdjuster = model.getTemporalAdjuster(); applyTemporalAdjuster(temporalAdjuster); findTimeIntervalToggleByTemporalAdjuster(temporalAdjuster).ifPresent(timeIntervalToggleGroup::selectToggle); defineAndAddActiveSeries(); initBoundsForTimelineNavigation(); // Apply listeners and handlers root.widthProperty().addListener(widthListener); xAxis.getChildrenUnmodifiable().addListener(nodeListChangeListener); yAxis.widthProperty().addListener(yAxisWidthListener); timeIntervalToggleGroup.selectedToggleProperty().addListener(timeIntervalChangeListener); timelineNavigation.setOnMousePressed(this::onMousePressedSplitPane); timelineNavigation.setOnMouseDragged(this::onMouseDragged); center.setOnMousePressed(this::onMousePressedCenter); center.setOnMouseReleased(this::onMouseReleasedCenter); addLegendToggleActionHandlers(getSeriesForLegend1()); addLegendToggleActionHandlers(getSeriesForLegend2()); addLegendToggleActionHandlers(getSeriesForLegend3()); addActionHandlersToDividers(); } @Override public void deactivate() { root.widthProperty().removeListener(widthListener); xAxis.getChildrenUnmodifiable().removeListener(nodeListChangeListener); yAxis.widthProperty().removeListener(yAxisWidthListener); timeIntervalToggleGroup.selectedToggleProperty().removeListener(timeIntervalChangeListener); timelineNavigation.setOnMousePressed(null); timelineNavigation.setOnMouseDragged(null); center.setOnMousePressed(null); center.setOnMouseReleased(null); removeLegendToggleActionHandlers(getSeriesForLegend1()); removeLegendToggleActionHandlers(getSeriesForLegend2()); removeLegendToggleActionHandlers(getSeriesForLegend3()); removeActionHandlersToDividers(); // clear data, reset states. We keep timeInterval state though activeSeries.clear(); chart.getData().clear(); legendToggleBySeriesName.values().forEach(e -> e.setSelected(false)); dividerNodes.clear(); dividerNodesTooltips.clear(); model.invalidateCache(); } /////////////////////////////////////////////////////////////////////////////////////////// // TimeInterval/TemporalAdjuster /////////////////////////////////////////////////////////////////////////////////////////// protected HBox getTimeIntervalBox() { ToggleButton year = getTimeIntervalToggleButton(Res.get("time.year"), TemporalAdjusterModel.Interval.YEAR, timeIntervalToggleGroup, "toggle-left"); ToggleButton halfYear = getTimeIntervalToggleButton(Res.get("time.halfYear"), TemporalAdjusterModel.Interval.HALF_YEAR, timeIntervalToggleGroup, "toggle-center"); ToggleButton quarter = getTimeIntervalToggleButton(Res.get("time.quarter"), TemporalAdjusterModel.Interval.QUARTER, timeIntervalToggleGroup, "toggle-center"); ToggleButton month = getTimeIntervalToggleButton(Res.get("time.month"), TemporalAdjusterModel.Interval.MONTH, timeIntervalToggleGroup, "toggle-center"); ToggleButton week = getTimeIntervalToggleButton(Res.get("time.week"), TemporalAdjusterModel.Interval.WEEK, timeIntervalToggleGroup, "toggle-center"); ToggleButton day = getTimeIntervalToggleButton(Res.get("time.day"), TemporalAdjusterModel.Interval.DAY, timeIntervalToggleGroup, "toggle-center"); HBox toggleBox = new HBox(); toggleBox.setSpacing(0); toggleBox.setAlignment(Pos.CENTER_LEFT); Region spacer = new Region(); HBox.setHgrow(spacer, Priority.ALWAYS); toggleBox.getChildren().addAll(spacer, year, halfYear, quarter, month, week, day); return toggleBox; } private ToggleButton getTimeIntervalToggleButton(String label, TemporalAdjusterModel.Interval interval, ToggleGroup toggleGroup, String style) { ToggleButton toggleButton = new AutoTooltipToggleButton(label); toggleButton.setUserData(interval); toggleButton.setToggleGroup(toggleGroup); toggleButton.setId(style); return toggleButton; } protected void applyTemporalAdjuster(TemporalAdjuster temporalAdjuster) { model.applyTemporalAdjuster(temporalAdjuster); findTimeIntervalToggleByTemporalAdjuster(temporalAdjuster) .map(e -> (TemporalAdjusterModel.Interval) e.getUserData()) .ifPresent(model::setDateFormatPattern); } /////////////////////////////////////////////////////////////////////////////////////////// // Chart /////////////////////////////////////////////////////////////////////////////////////////// protected NumberAxis getXAxis() { NumberAxis xAxis = new NumberAxis(); xAxis.setForceZeroInRange(false); xAxis.setAutoRanging(true); xAxis.setTickLabelFormatter(model.getTimeAxisStringConverter()); return xAxis; } protected NumberAxis getYAxis() { NumberAxis yAxis = new NumberAxis(); yAxis.setForceZeroInRange(true); yAxis.setSide(Side.RIGHT); yAxis.setTickLabelFormatter(model.getYAxisStringConverter()); return yAxis; } // Add implementation if update of the y axis is required at series change protected void onSetYAxisFormatter(XYChart.Series series) { } protected LineChart getChart() { LineChart chart = new LineChart<>(xAxis, yAxis); chart.setAnimated(false); chart.setLegendVisible(false); chart.setMinHeight(200); chart.setId("charts-dao"); return chart; } /////////////////////////////////////////////////////////////////////////////////////////// // Legend /////////////////////////////////////////////////////////////////////////////////////////// protected HBox initLegendsAndGetLegendBox(Collection> collection) { HBox hBox = new HBox(); hBox.setSpacing(10); collection.forEach(series -> { AutoTooltipSlideToggleButton toggle = new AutoTooltipSlideToggleButton(); toggle.setMinWidth(200); toggle.setAlignment(Pos.TOP_LEFT); String seriesId = getSeriesId(series); legendToggleBySeriesName.put(seriesId, toggle); toggle.setText(seriesId); toggle.setId("charts-legend-toggle" + seriesIndexMap.get(seriesId)); toggle.setSelected(false); hBox.getChildren().add(toggle); }); Region spacer = new Region(); HBox.setHgrow(spacer, Priority.ALWAYS); hBox.getChildren().add(spacer); return hBox; } private void addLegendToggleActionHandlers(@Nullable Collection> collection) { if (collection != null) { collection.forEach(series -> legendToggleBySeriesName.get(getSeriesId(series)).setOnAction(e -> onSelectLegendToggle(series))); } } private void removeLegendToggleActionHandlers(@Nullable Collection> collection) { if (collection != null) { collection.forEach(series -> legendToggleBySeriesName.get(getSeriesId(series)).setOnAction(null)); } } /////////////////////////////////////////////////////////////////////////////////////////// // Timeline navigation /////////////////////////////////////////////////////////////////////////////////////////// private void addTimelineNavigation() { Pane left = new Pane(); center = new Pane(); center.setId("chart-navigation-center-pane"); Pane right = new Pane(); timelineNavigation = new SplitPane(left, center, right); timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]); timelineNavigation.setMinHeight(25); timelineLabels = new HBox(); } // After initial chart data are created we apply the text from the x-axis ticks to our timeline navigation. protected void applyTimeLineNavigationLabels() { timelineLabels.getChildren().clear(); ObservableList> tickMarks = xAxis.getTickMarks(); int size = tickMarks.size(); for (int i = 0; i < size; i++) { Axis.TickMark tickMark = tickMarks.get(i); Number xValue = tickMark.getValue(); String xValueString; if (xAxis.getTickLabelFormatter() != null) { xValueString = xAxis.getTickLabelFormatter().toString(xValue); } else { xValueString = String.valueOf(xValue); } Label label = new Label(xValueString); label.setMinHeight(30); label.setId("chart-navigation-label"); Region spacer = new Region(); HBox.setHgrow(spacer, Priority.ALWAYS); if (i < size - 1) { timelineLabels.getChildren().addAll(label, spacer); } else { // After last label we don't add a spacer timelineLabels.getChildren().add(label); } } } private void onMousePressedSplitPane(MouseEvent e) { x = e.getX(); applyFromToDates(); showDividerTooltips(); } private void onMousePressedCenter(MouseEvent e) { centerPanePressed = true; applyFromToDates(); showDividerTooltips(); } private void onMouseReleasedCenter(MouseEvent e) { centerPanePressed = false; onTimelineChanged(); hideDividerTooltips(); } private void onMouseDragged(MouseEvent e) { if (centerPanePressed) { double newX = e.getX(); double width = timelineNavigation.getWidth(); double relativeDelta = (x - newX) / width; double leftPos = timelineNavigation.getDividerPositions()[0] - relativeDelta; double rightPos = timelineNavigation.getDividerPositions()[1] - relativeDelta; // Model might limit application of new values if we hit a boundary model.onTimelineMouseDrag(leftPos, rightPos); timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]); x = newX; applyFromToDates(); showDividerTooltips(); } } private void addActionHandlersToDividers() { // No API access to dividers ;-( only via css lookup hack (https://stackoverflow.com/questions/40707295/how-to-add-listener-to-divider-position?rq=1) // Need to be done after added to scene and call requestLayout and applyCss. We keep it in a list atm // and set action handler in activate. timelineNavigation.requestLayout(); timelineNavigation.applyCss(); dividerMouseDraggedEventHandler = event -> { applyFromToDates(); showDividerTooltips(); }; for (Node node : timelineNavigation.lookupAll(".split-pane-divider")) { dividerNodes.add(node); node.setOnMouseReleased(e -> { hideDividerTooltips(); onTimelineChanged(); }); node.addEventHandler(MouseEvent.MOUSE_DRAGGED, dividerMouseDraggedEventHandler); Tooltip tooltip = new Tooltip(""); dividerNodesTooltips.add(tooltip); tooltip.setShowDelay(Duration.millis(300)); tooltip.setShowDuration(Duration.seconds(3)); tooltip.textProperty().bind(dividerNodes.size() == 1 ? fromProperty : toProperty); Tooltip.install(node, tooltip); } } private void removeActionHandlersToDividers() { dividerNodes.forEach(node -> { node.setOnMouseReleased(null); node.removeEventHandler(MouseEvent.MOUSE_DRAGGED, dividerMouseDraggedEventHandler); }); for (int i = 0; i < dividerNodesTooltips.size(); i++) { Tooltip tooltip = dividerNodesTooltips.get(i); tooltip.textProperty().unbind(); Tooltip.uninstall(dividerNodes.get(i), tooltip); } } private void resetTimeNavigation() { timelineNavigation.setDividerPositions(0d, 1d); model.onTimelineNavigationChanged(0, 1); } private void showDividerTooltips() { showDividerTooltip(0); showDividerTooltip(1); } private void hideDividerTooltips() { dividerNodesTooltips.forEach(PopupWindow::hide); } private void showDividerTooltip(int index) { Node divider = dividerNodes.get(index); Bounds bounds = divider.localToScene(divider.getBoundsInLocal()); Tooltip tooltip = dividerNodesTooltips.get(index); double xOffset = index == 0 ? -90 : 10; Stage stage = (Stage) root.getScene().getWindow(); tooltip.show(stage, stage.getX() + bounds.getMaxX() + xOffset, stage.getY() + bounds.getMaxY() - 40); } /////////////////////////////////////////////////////////////////////////////////////////// // Series /////////////////////////////////////////////////////////////////////////////////////////// protected abstract void createSeries(); protected abstract Collection> getSeriesForLegend1(); // If a second legend is used this has to be overridden protected Collection> getSeriesForLegend2() { return null; } protected Collection> getSeriesForLegend3() { return null; } protected abstract void defineAndAddActiveSeries(); protected void activateSeries(XYChart.Series series) { if (activeSeries.contains(series)) { return; } chart.getData().add(series); activeSeries.add(series); legendToggleBySeriesName.get(getSeriesId(series)).setSelected(true); applyDataAndUpdate(); } /////////////////////////////////////////////////////////////////////////////////////////// // Data /////////////////////////////////////////////////////////////////////////////////////////// protected abstract CompletableFuture applyData(); private void applyDataAndUpdate() { long ts = System.currentTimeMillis(); applyData().whenComplete((r, t) -> { log.debug("applyData took {}", System.currentTimeMillis() - ts); long ts2 = System.currentTimeMillis(); updateChartAfterDataChange(); log.debug("updateChartAfterDataChange took {}", System.currentTimeMillis() - ts2); onDataApplied(); }); } /** * Implementations define which series will be used for setBoundsForTimelineNavigation */ protected abstract void initBoundsForTimelineNavigation(); /** * @param data The series data which determines the min/max x values for the time line navigation. * If not applicable initBoundsForTimelineNavigation requires custom implementation. */ protected void setBoundsForTimelineNavigation(ObservableList> data) { model.initBounds(data); xAxis.setLowerBound(model.getLowerBound().doubleValue()); xAxis.setUpperBound(model.getUpperBound().doubleValue()); } /////////////////////////////////////////////////////////////////////////////////////////// // Handlers triggering a data/chart update /////////////////////////////////////////////////////////////////////////////////////////// private void onTimeIntervalChanged(Toggle newValue) { TemporalAdjusterModel.Interval interval = (TemporalAdjusterModel.Interval) newValue.getUserData(); applyTemporalAdjuster(interval.getAdjuster()); model.invalidateCache(); applyDataAndUpdate(); } private void onTimelineChanged() { updateTimeLinePositions(); model.invalidateCache(); applyDataAndUpdate(); } private void updateTimeLinePositions() { double leftPos = timelineNavigation.getDividerPositions()[0]; double rightPos = timelineNavigation.getDividerPositions()[1]; model.onTimelineNavigationChanged(leftPos, rightPos); // We need to update as model might have adjusted the values timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]); fromProperty.set(model.getTimeAxisStringConverter().toString(model.getFromDate()).replace("\n", " ")); toProperty.set(model.getTimeAxisStringConverter().toString(model.getToDate()).replace("\n", " ")); } private void applyFromToDates() { double leftPos = timelineNavigation.getDividerPositions()[0]; double rightPos = timelineNavigation.getDividerPositions()[1]; model.applyFromToDates(leftPos, rightPos); fromProperty.set(model.getTimeAxisStringConverter().toString(model.getFromDate()).replace("\n", " ")); toProperty.set(model.getTimeAxisStringConverter().toString(model.getToDate()).replace("\n", " ")); } private void onSelectLegendToggle(XYChart.Series series) { boolean isSelected = legendToggleBySeriesName.get(getSeriesId(series)).isSelected(); // If we have set that flag we deselect all other toggles if (isRadioButtonBehaviour) { new ArrayList<>(chart.getData()).stream() // We need to copy to a new list to avoid ConcurrentModificationException .filter(activeSeries::contains) .forEach(seriesToRemove -> { chart.getData().remove(seriesToRemove); String seriesId = getSeriesId(seriesToRemove); activeSeries.remove(seriesToRemove); legendToggleBySeriesName.get(seriesId).setSelected(false); }); } if (isSelected) { chart.getData().add(series); activeSeries.add(series); applyDataAndUpdate(); if (isRadioButtonBehaviour) { // We support different y-axis formats only if isRadioButtonBehaviour is set, otherwise we would get // mixed data on y-axis onSetYAxisFormatter(series); } } else if (!isRadioButtonBehaviour) { // if isRadioButtonBehaviour we have removed it already via the code above chart.getData().remove(series); activeSeries.remove(series); updateChartAfterDataChange(); } } /////////////////////////////////////////////////////////////////////////////////////////// // Chart update after data change /////////////////////////////////////////////////////////////////////////////////////////// // Update of the chart data can be triggered by: // 1. activate() // 2. TimeInterval toggle change // 3. Timeline navigation change // 4. Legend/series toggle change // Timeline navigation and legend/series toggles get reset at activate. // Time interval toggle keeps its state at screen changes. protected void updateChartAfterDataChange() { // If a series got no data points after update we need to clear it from the chart cleanupDanglingSeries(); // Hides symbols if too many data points are created updateSymbolsVisibility(); // When series gets added/removed the JavaFx charts framework would try to apply styles by the index of // addition, but we want to use a static color assignment which is synced with the legend color. applySeriesStyles(); // Set tooltip on symbols applyTooltip(); } private void cleanupDanglingSeries() { List> activeSeriesList = new ArrayList<>(activeSeries); activeSeriesList.forEach(series -> { ObservableList> seriesOnChart = chart.getData(); if (series.getData().isEmpty()) { seriesOnChart.remove(series); } else if (!seriesOnChart.contains(series)) { seriesOnChart.add(series); } }); } private void updateSymbolsVisibility() { maxDataPointsForShowingSymbols = 100; long numDataPoints = chart.getData().stream() .map(XYChart.Series::getData) .mapToLong(List::size) .max() .orElse(0); boolean prevValue = chart.getCreateSymbols(); boolean newValue = numDataPoints < maxDataPointsForShowingSymbols; if (prevValue != newValue) { chart.setCreateSymbols(newValue); } } // The chart framework assigns the colored depending on the order it got added, but want to keep colors // the same so they match with the legend toggle. private void applySeriesStyles() { for (int index = 0; index < chart.getData().size(); index++) { XYChart.Series series = chart.getData().get(index); int staticIndex = seriesIndexMap.get(getSeriesId(series)); Set lines = getNodesForStyle(series.getNode(), ".default-color%d.chart-series-line"); Stream symbols = series.getData().stream().map(XYChart.Data::getNode) .flatMap(node -> getNodesForStyle(node, ".default-color%d.chart-line-symbol").stream()); Stream.concat(lines.stream(), symbols).forEach(node -> { removeStyles(node); node.getStyleClass().add("default-color" + staticIndex); }); } } private void applyTooltip() { chart.getData().forEach(series -> { series.getData().forEach(data -> { Node node = data.getNode(); if (node == null) { return; } String xValue = model.getTooltipDateConverter(data.getXValue()); String yValue = model.getYAxisStringConverter().toString(data.getYValue()); Tooltip.install(node, new Tooltip(Res.get("dao.factsAndFigures.supply.chart.tradeFee.toolTip", yValue, xValue))); }); }); } /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// private void removeStyles(Node node) { for (int i = 0; i < getMaxSeriesSize(); i++) { node.getStyleClass().remove("default-color" + i); } } private Set getNodesForStyle(Node node, String style) { Set result = new HashSet<>(); if (node != null) { for (int i = 0; i < getMaxSeriesSize(); i++) { result.addAll(node.lookupAll(String.format(style, i))); } } return result; } private int getMaxSeriesSize() { maxSeriesSize = Math.max(maxSeriesSize, chart.getData().size()); return maxSeriesSize; } private Optional findTimeIntervalToggleByTemporalAdjuster(TemporalAdjuster adjuster) { return timeIntervalToggleGroup.getToggles().stream() .filter(toggle -> ((TemporalAdjusterModel.Interval) toggle.getUserData()).getAdjuster().equals(adjuster)) .findAny(); } // We use the name as id as there is no other suitable data inside series protected String getSeriesId(XYChart.Series series) { return series.getName(); } protected void mapToUserThread(Runnable command) { UserThread.execute(command); } protected void onDataApplied() { // Once we have data applied we need to call initBoundsForTimelineNavigation again if (model.upperBound.longValue() == 0) { initBoundsForTimelineNavigation(); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/chart/ChartViewModel.java ================================================ /* * 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 . */ package haveno.desktop.components.chart; import haveno.common.util.Tuple2; import haveno.desktop.common.model.ActivatableWithDataModel; import haveno.desktop.util.DisplayUtils; import javafx.scene.chart.XYChart; import javafx.util.StringConverter; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.time.temporal.TemporalAdjuster; import java.util.Date; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @Slf4j public abstract class ChartViewModel extends ActivatableWithDataModel { private final static double LEFT_TIMELINE_SNAP_VALUE = 0.01; private final static double RIGHT_TIMELINE_SNAP_VALUE = 0.99; @Getter private final Double[] dividerPositions = new Double[]{0d, 1d}; @Getter protected Number lowerBound; @Getter protected Number upperBound; @Getter protected String dateFormatPatters = "dd MMM\nyyyy"; @Getter long fromDate; @Getter long toDate; public ChartViewModel(T dataModel) { super(dataModel); } @Override public void activate() { dividerPositions[0] = 0d; dividerPositions[1] = 1d; } /////////////////////////////////////////////////////////////////////////////////////////// // TimerInterval/TemporalAdjuster /////////////////////////////////////////////////////////////////////////////////////////// protected void applyTemporalAdjuster(TemporalAdjuster temporalAdjuster) { dataModel.setTemporalAdjuster(temporalAdjuster); } void setDateFormatPattern(TemporalAdjusterModel.Interval interval) { switch (interval) { case YEAR: dateFormatPatters = "yyyy"; break; case HALF_YEAR: case QUARTER: case MONTH: dateFormatPatters = "MMM\nyyyy"; break; default: dateFormatPatters = "MMM dd\nyyyy"; break; } } protected TemporalAdjuster getTemporalAdjuster() { return dataModel.getTemporalAdjuster(); } /////////////////////////////////////////////////////////////////////////////////////////// // TimelineNavigation /////////////////////////////////////////////////////////////////////////////////////////// void onTimelineNavigationChanged(double leftPos, double rightPos) { applyFromToDates(leftPos, rightPos); // TODO find better solution // The TemporalAdjusters map dates to the lower bound (e.g. 1.1.2016) but our from date is the date of // the first data entry so if we filter by that we would exclude the first year data in case YEAR was selected // A trade with data 3.May.2016 gets mapped to 1.1.2016 and our from date will be April 2016, so we would // filter that. It is a bit tricky to sync the TemporalAdjusters with our date filter. To include at least in // the case when we have not set the date filter (left =0 / right =1) we set from date to epoch time 0 and // to date to one year ahead to be sure we include all. long from, to; // We only manipulate the from, to variables for the date filter, not the fromDate, toDate properties as those // are used by the view for tooltip over the time line navigation dividers if (leftPos < LEFT_TIMELINE_SNAP_VALUE) { from = 0; } else { from = fromDate; } if (rightPos > RIGHT_TIMELINE_SNAP_VALUE) { to = new Date().getTime() / 1000 + TimeUnit.DAYS.toSeconds(365); } else { to = toDate; } dividerPositions[0] = leftPos; dividerPositions[1] = rightPos; dataModel.setDateFilter(from, to); } void applyFromToDates(double leftPos, double rightPos) { // We need to snap into the 0 and 1 values once we are close as otherwise once navigation has been used we // would not get back to exact 0 or 1. Not clear why but might be rounding issues from values at x positions of // drag operations. if (leftPos < LEFT_TIMELINE_SNAP_VALUE) { leftPos = 0; } if (rightPos > RIGHT_TIMELINE_SNAP_VALUE) { rightPos = 1; } long lowerBoundAsLong = lowerBound.longValue(); long totalRange = upperBound.longValue() - lowerBoundAsLong; fromDate = (long) (lowerBoundAsLong + totalRange * leftPos); toDate = (long) (lowerBoundAsLong + totalRange * rightPos); } void onTimelineMouseDrag(double leftPos, double rightPos) { // Limit drag operation if we have hit a boundary if (leftPos > LEFT_TIMELINE_SNAP_VALUE) { dividerPositions[1] = rightPos; } if (rightPos < RIGHT_TIMELINE_SNAP_VALUE) { dividerPositions[0] = leftPos; } } void initBounds(List> data1, List> data2) { Tuple2 xMinMaxTradeFee = getMinMax(data1); Tuple2 xMinMaxCompensationRequest = getMinMax(data2); lowerBound = Math.min(xMinMaxTradeFee.first, xMinMaxCompensationRequest.first); upperBound = Math.max(xMinMaxTradeFee.second, xMinMaxCompensationRequest.second); } void initBounds(List> data) { Tuple2 xMinMaxTradeFee = getMinMax(data); lowerBound = xMinMaxTradeFee.first; upperBound = xMinMaxTradeFee.second; } /////////////////////////////////////////////////////////////////////////////////////////// // Chart /////////////////////////////////////////////////////////////////////////////////////////// StringConverter getTimeAxisStringConverter() { return new StringConverter<>() { @Override public String toString(Number epochSeconds) { Date date = new Date(epochSeconds.longValue() * 1000); return DisplayUtils.formatDateAxis(date, getDateFormatPatters()); } @Override public Number fromString(String string) { return 0; } }; } protected StringConverter getYAxisStringConverter() { return new StringConverter<>() { @Override public String toString(Number value) { return String.valueOf(value); } @Override public Number fromString(String string) { return null; } }; } String getTooltipDateConverter(Number date) { return getTimeAxisStringConverter().toString(date).replace("\n", " "); } protected String getTooltipValueConverter(Number value) { return getYAxisStringConverter().toString(value); } /////////////////////////////////////////////////////////////////////////////////////////// // Data /////////////////////////////////////////////////////////////////////////////////////////// protected void invalidateCache() { dataModel.invalidateCache(); } /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// protected List> toChartData(Map map) { return map.entrySet().stream() .map(entry -> new XYChart.Data(entry.getKey(), entry.getValue())) .collect(Collectors.toList()); } protected List> toChartDoubleData(Map map) { return map.entrySet().stream() .map(entry -> new XYChart.Data(entry.getKey(), entry.getValue())) .collect(Collectors.toList()); } protected List> toChartLongData(Map map) { return map.entrySet().stream() .map(entry -> new XYChart.Data(entry.getKey(), entry.getValue())) .collect(Collectors.toList()); } private Tuple2 getMinMax(List> chartData) { long min = Long.MAX_VALUE, max = 0; for (XYChart.Data data : chartData) { long value = data.getXValue().longValue(); min = Math.min(value, min); max = Math.max(value, max); } return new Tuple2<>((double) min, (double) max); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/chart/TemporalAdjusterModel.java ================================================ /* * 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 . */ package haveno.desktop.components.chart; import haveno.common.util.MathUtils; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.math.RoundingMode; import java.time.DayOfWeek; import java.time.Instant; import java.time.LocalDate; import java.time.ZoneId; import java.time.temporal.TemporalAdjuster; import java.time.temporal.TemporalAdjusters; import static java.time.temporal.ChronoField.DAY_OF_YEAR; @Slf4j public class TemporalAdjusterModel { private static final ZoneId ZONE_ID = ZoneId.systemDefault(); public enum Interval { YEAR(TemporalAdjusters.firstDayOfYear()), HALF_YEAR(temporal -> { long halfYear = temporal.range(DAY_OF_YEAR).getMaximum() / 2; int dayOfYear = 0; if (temporal instanceof LocalDate) { dayOfYear = ((LocalDate) temporal).getDayOfYear(); // getDayOfYear delivers 1-365 (366 in leap years) } if (dayOfYear <= halfYear) { return temporal.with(DAY_OF_YEAR, 1); } else { return temporal.with(DAY_OF_YEAR, halfYear + 1); } }), QUARTER(temporal -> { long quarter1 = temporal.range(DAY_OF_YEAR).getMaximum() / 4; long halfYear = temporal.range(DAY_OF_YEAR).getMaximum() / 2; long quarter3 = MathUtils.roundDoubleToLong(temporal.range(DAY_OF_YEAR).getMaximum() * 0.75, RoundingMode.FLOOR); int dayOfYear = 0; if (temporal instanceof LocalDate) { dayOfYear = ((LocalDate) temporal).getDayOfYear(); } if (dayOfYear <= quarter1) { return temporal.with(DAY_OF_YEAR, 1); } else if (dayOfYear <= halfYear) { return temporal.with(DAY_OF_YEAR, quarter1 + 1); } else if (dayOfYear <= quarter3) { return temporal.with(DAY_OF_YEAR, halfYear + 1); } else { return temporal.with(DAY_OF_YEAR, quarter3 + 1); } }), MONTH(TemporalAdjusters.firstDayOfMonth()), WEEK(TemporalAdjusters.next(DayOfWeek.MONDAY)), DAY(TemporalAdjusters.ofDateAdjuster(d -> d)); @Getter private final TemporalAdjuster adjuster; Interval(TemporalAdjuster adjuster) { this.adjuster = adjuster; } } protected TemporalAdjuster temporalAdjuster = Interval.DAY.getAdjuster(); public void setTemporalAdjuster(TemporalAdjuster temporalAdjuster) { this.temporalAdjuster = temporalAdjuster; } public TemporalAdjuster getTemporalAdjuster() { return temporalAdjuster; } public long toTimeInterval(Instant instant) { return toTimeInterval(instant, temporalAdjuster); } public long toTimeInterval(Instant instant, TemporalAdjuster temporalAdjuster) { return instant .atZone(ZONE_ID) .toLocalDate() .with(temporalAdjuster) .atStartOfDay(ZONE_ID) .toInstant() .getEpochSecond(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/controlsfx/README.md ================================================ This package is a very minimal subset of the external library `controlsfx`. Three files were embedded into the project to avoid having `controlsfx` as dependency. This is based on version `8.0.6_20` tagged in commit 6a52afec3ef16094cda281abc80b4daa3d3bf1fd: [https://github.com/controlsfx/controlsfx/commit/6a52afec3ef16094cda281abc80b4daa3d3bf1fd] These specific files got raw copied (with package name adjustment): [https://github.com/controlsfx/controlsfx/blob/6a52afec3ef16094cda281abc80b4daa3d3bf1fd/controlsfx/src/main/java/org/controlsfx/control/PopOver.java] [https://github.com/controlsfx/controlsfx/blob/6a52afec3ef16094cda281abc80b4daa3d3bf1fd/controlsfx/src/main/java/impl/org/controlsfx/skin/PopOverSkin.java] [https://github.com/controlsfx/controlsfx/blob/6a52afec3ef16094cda281abc80b4daa3d3bf1fd/controlsfx/src/main/resources/org/controlsfx/control/popover.css] ================================================ FILE: desktop/src/main/java/haveno/desktop/components/controlsfx/control/PopOver.java ================================================ /* * Copyright (c) 2013, 2016 ControlsFX * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of ControlsFX, any associated website, nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package haveno.desktop.components.controlsfx.control; import haveno.desktop.components.controlsfx.skin.PopOverSkin; import javafx.animation.FadeTransition; import javafx.beans.InvalidationListener; import javafx.beans.WeakInvalidationListener; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.WeakChangeListener; import javafx.event.EventHandler; import javafx.event.WeakEventHandler; import javafx.geometry.Bounds; import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.PopupControl; import javafx.scene.control.Skin; import javafx.scene.layout.StackPane; import javafx.stage.Window; import javafx.stage.WindowEvent; import javafx.util.Duration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static java.util.Objects.requireNonNull; import static javafx.scene.input.MouseEvent.MOUSE_CLICKED; /** * The PopOver control provides detailed information about an owning node in a * popup window. The popup window has a very lightweight appearance (no default * window decorations) and an arrow pointing at the owner. Due to the nature of * popup windows the PopOver will move around with the parent window when the * user drags it.
    *
    Screenshot of PopOver

    * The PopOver can be detached from the owning node by dragging it away from the * owner. It stops displaying an arrow and starts displaying a title and a close * icon.
    *
    *
    Screenshot of a detached PopOver

    * The following image shows a popover with an accordion content node. PopOver * controls are automatically resizing themselves when the content node changes * its size.
    *
    *
    Screenshot of PopOver containing an Accordion

    * For styling apply stylesheets to the root pane of the PopOver. * *

    Example:

    * *
     * PopOver popOver = new PopOver();
     * popOver.getRoot().getStylesheets().add(...);
     * 
    * */ public class PopOver extends PopupControl { private static final String DEFAULT_STYLE_CLASS = "popover"; //$NON-NLS-1$ private static final Duration DEFAULT_FADE_DURATION = Duration.seconds(.2); private double targetX; private double targetY; private final SimpleBooleanProperty animated = new SimpleBooleanProperty(true); private final ObjectProperty fadeInDuration = new SimpleObjectProperty<>(DEFAULT_FADE_DURATION); private final ObjectProperty fadeOutDuration = new SimpleObjectProperty<>(DEFAULT_FADE_DURATION); private final Logger log = LoggerFactory.getLogger(this.getClass()); /** * Creates a pop over with a label as the content node. */ public PopOver() { super(); getStyleClass().add(DEFAULT_STYLE_CLASS); getRoot().getStylesheets().add( requireNonNull(PopOver.class.getResource("popover.css")).toExternalForm()); //$NON-NLS-1$ setAnchorLocation(AnchorLocation.WINDOW_TOP_LEFT); setOnHiding(evt -> setDetached(false)); /* * Create some initial content. */ Label label = new Label("No content set"); //$NON-NLS-1$ label.setPrefSize(200, 200); label.setPadding(new Insets(4)); setContentNode(label); InvalidationListener repositionListener = observable -> { if (isShowing() && !isDetached()) { show(getOwnerNode(), targetX, targetY); adjustWindowLocation(); } }; arrowSize.addListener(repositionListener); cornerRadius.addListener(repositionListener); arrowLocation.addListener(repositionListener); arrowIndent.addListener(repositionListener); headerAlwaysVisible.addListener(repositionListener); /* * A detached popover should of course not automatically hide itself. */ detached.addListener(it -> setAutoHide(!isDetached())); setAutoHide(true); } /** * Creates a pop over with the given node as the content node. * * @param content * The content shown by the pop over */ public PopOver(Node content) { this(); setContentNode(content); } @Override protected Skin createDefaultSkin() { return new PopOverSkin(this); } private final StackPane root = new StackPane(); /** * The root pane stores the content node of the popover. It is accessible * via this method in order to support proper styling. * *

    Example:

    * *
         * PopOver popOver = new PopOver();
         * popOver.getRoot().getStylesheets().add(...);
         * 
    * * @return the root pane */ public final StackPane getRoot() { return root; } // Content support. private final ObjectProperty contentNode = new SimpleObjectProperty<>( this, "contentNode") { //$NON-NLS-1$ @Override public void setValue(Node node) { if (node == null) { throw new IllegalArgumentException( "content node can not be null"); //$NON-NLS-1$ } } }; /** * Returns the content shown by the pop over. * * @return the content node property */ public final ObjectProperty contentNodeProperty() { return contentNode; } /** * Returns the value of the content property * * @return the content node * * @see #contentNodeProperty() */ public final Node getContentNode() { return contentNodeProperty().get(); } /** * Sets the value of the content property. * * @param content * the new content node value * * @see #contentNodeProperty() */ public final void setContentNode(Node content) { contentNodeProperty().set(content); } private final InvalidationListener hideListener = observable -> { if (!isDetached()) { hide(Duration.ZERO); } }; private final WeakInvalidationListener weakHideListener = new WeakInvalidationListener( hideListener); private final ChangeListener xListener = (value, oldX, newX) -> { if (!isDetached()) { setAnchorX(getAnchorX() + (newX.doubleValue() - oldX.doubleValue())); } }; private final WeakChangeListener weakXListener = new WeakChangeListener<>( xListener); private final ChangeListener yListener = (value, oldY, newY) -> { if (!isDetached()) { setAnchorY(getAnchorY() + (newY.doubleValue() - oldY.doubleValue())); } }; private final WeakChangeListener weakYListener = new WeakChangeListener<>( yListener); private Window ownerWindow; private final EventHandler closePopOverOnOwnerWindowCloseLambda = event -> ownerWindowClosing(); private final WeakEventHandler closePopOverOnOwnerWindowClose = new WeakEventHandler<>(closePopOverOnOwnerWindowCloseLambda); /** * Shows the pop over in a position relative to the edges of the given owner * node. The position is dependent on the arrow location. If the arrow is * pointing to the right then the pop over will be placed to the left of the * given owner. If the arrow points up then the pop over will be placed * below the given owner node. The arrow will slightly overlap with the * owner node. * * @param owner * the owner of the pop over */ public final void show(Node owner) { show(owner, 4); } /** * Shows the pop over in a position relative to the edges of the given owner * node. The position is dependent on the arrow location. If the arrow is * pointing to the right then the pop over will be placed to the left of the * given owner. If the arrow points up then the pop over will be placed * below the given owner node. * * @param owner * the owner of the pop over * @param offset * if negative specifies the distance to the owner node or when * positive specifies the number of pixels that the arrow will * overlap with the owner node (positive values are recommended) */ public final void show(Node owner, double offset) { requireNonNull(owner); Bounds bounds = owner.localToScreen(owner.getBoundsInLocal()); switch (getArrowLocation()) { case BOTTOM_CENTER: case BOTTOM_LEFT: case BOTTOM_RIGHT: show(owner, bounds.getMinX() + bounds.getWidth() / 2, bounds.getMinY() + offset); break; case LEFT_BOTTOM: case LEFT_CENTER: case LEFT_TOP: show(owner, bounds.getMaxX() - offset, bounds.getMinY() + bounds.getHeight() / 2); break; case RIGHT_BOTTOM: case RIGHT_CENTER: case RIGHT_TOP: show(owner, bounds.getMinX() + offset, bounds.getMinY() + bounds.getHeight() / 2); break; case TOP_CENTER: case TOP_LEFT: case TOP_RIGHT: show(owner, bounds.getMinX() + bounds.getWidth() / 2, bounds.getMinY() + bounds.getHeight() - offset); break; default: break; } } /** {@inheritDoc} */ @Override public final void show(Window owner) { super.show(owner); ownerWindow = owner; if (isAnimated()) { showFadeInAnimation(getFadeInDuration()); } ownerWindow.addEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, closePopOverOnOwnerWindowClose); ownerWindow.addEventFilter(WindowEvent.WINDOW_HIDING, closePopOverOnOwnerWindowClose); } /** {@inheritDoc} */ @Override public final void show(Window ownerWindow, double anchorX, double anchorY) { super.show(ownerWindow, anchorX, anchorY); this.ownerWindow = ownerWindow; if (isAnimated()) { showFadeInAnimation(getFadeInDuration()); } ownerWindow.addEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, closePopOverOnOwnerWindowClose); ownerWindow.addEventFilter(WindowEvent.WINDOW_HIDING, closePopOverOnOwnerWindowClose); } /** * Makes the pop over visible at the give location and associates it with * the given owner node. The x and y coordinate will be the target location * of the arrow of the pop over and not the location of the window. * * @param owner * the owning node * @param x * the x coordinate for the pop over arrow tip * @param y * the y coordinate for the pop over arrow tip */ @Override public final void show(Node owner, double x, double y) { show(owner, x, y, getFadeInDuration()); } /** * Makes the pop over visible at the give location and associates it with * the given owner node. The x and y coordinate will be the target location * of the arrow of the pop over and not the location of the window. * * @param owner * the owning node * @param x * the x coordinate for the pop over arrow tip * @param y * the y coordinate for the pop over arrow tip * @param fadeInDuration * the time it takes for the pop over to be fully visible. This duration takes precedence over the fade-in property without setting. */ public final void show(Node owner, double x, double y, Duration fadeInDuration) { /* * Calling show() a second time without first closing the pop over * causes it to be placed at the wrong location. */ if (ownerWindow != null && isShowing()) { super.hide(); } targetX = x; targetY = y; if (owner == null) { throw new IllegalArgumentException("owner can not be null"); //$NON-NLS-1$ } if (fadeInDuration == null) { fadeInDuration = DEFAULT_FADE_DURATION; } /* * This is all needed because children windows do not get their x and y * coordinate updated when the owning window gets moved by the user. */ if (ownerWindow != null) { ownerWindow.xProperty().removeListener(weakXListener); ownerWindow.yProperty().removeListener(weakYListener); ownerWindow.widthProperty().removeListener(weakHideListener); ownerWindow.heightProperty().removeListener(weakHideListener); } ownerWindow = owner.getScene().getWindow(); ownerWindow.xProperty().addListener(weakXListener); ownerWindow.yProperty().addListener(weakYListener); ownerWindow.widthProperty().addListener(weakHideListener); ownerWindow.heightProperty().addListener(weakHideListener); setOnShown(evt -> { /* * The user clicked somewhere into the transparent background. If * this is the case then hide the window (when attached). */ getScene().addEventHandler(MOUSE_CLICKED, mouseEvent -> { if (mouseEvent.getTarget().equals(getScene().getRoot())) { if (!isDetached()) { hide(); } } }); /* * Move the window so that the arrow will end up pointing at the * target coordinates. */ adjustWindowLocation(); }); super.show(owner, x, y); if (isAnimated()) { showFadeInAnimation(fadeInDuration); } // Bug fix - close popup when owner window is closing ownerWindow.addEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, closePopOverOnOwnerWindowClose); ownerWindow.addEventFilter(WindowEvent.WINDOW_HIDING, closePopOverOnOwnerWindowClose); } private void showFadeInAnimation(Duration fadeInDuration) { // Fade In Node skinNode = getSkin().getNode(); skinNode.setOpacity(0); FadeTransition fadeIn = new FadeTransition(fadeInDuration, skinNode); fadeIn.setFromValue(0); fadeIn.setToValue(1); fadeIn.play(); } private void ownerWindowClosing() { hide(Duration.ZERO); } /** * Hides the pop over by quickly changing its opacity to 0. * * @see #hide(Duration) */ @Override public final void hide() { hide(getFadeOutDuration()); } /** * Hides the pop over by quickly changing its opacity to 0. * * @param fadeOutDuration * the duration of the fade transition that is being used to * change the opacity of the pop over * @since 1.0 */ public final void hide(Duration fadeOutDuration) { log.debug("hide:" + fadeOutDuration.toString()); //We must remove EventFilter in order to prevent memory leak. if (ownerWindow != null) { ownerWindow.removeEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, closePopOverOnOwnerWindowClose); ownerWindow.removeEventFilter(WindowEvent.WINDOW_HIDING, closePopOverOnOwnerWindowClose); } if (fadeOutDuration == null) { fadeOutDuration = DEFAULT_FADE_DURATION; } if (isShowing()) { super.hide(); // TODO: getting error "The window has already been closed" with animation which freezes application. // To recreate: create multiple payment methods, edit offer, go to payment method drop down, hover over info box, then quickly select another payment method // if (isAnimated()) { // // Fade Out // Node skinNode = getSkin().getNode(); // FadeTransition fadeOut = new FadeTransition(fadeOutDuration, // skinNode); // fadeOut.setFromValue(skinNode.getOpacity()); // fadeOut.setToValue(0); // fadeOut.setOnFinished(evt -> { // try { // super.hide(); // } catch (IllegalStateException e) { // log.warn("Error hiding PopOver: " + e.getMessage()); // e.printStackTrace(); // } // }); // fadeOut.play(); // } else { // super.hide(); // } } } private void adjustWindowLocation() { Bounds bounds = PopOver.this.getSkin().getNode().getBoundsInParent(); switch (getArrowLocation()) { case TOP_CENTER: case TOP_LEFT: case TOP_RIGHT: setAnchorX(getAnchorX() + bounds.getMinX() - computeXOffset()); setAnchorY(getAnchorY() + bounds.getMinY() + getArrowSize()); break; case LEFT_TOP: case LEFT_CENTER: case LEFT_BOTTOM: setAnchorX(getAnchorX() + bounds.getMinX() + getArrowSize()); setAnchorY(getAnchorY() + bounds.getMinY() - computeYOffset()); break; case BOTTOM_CENTER: case BOTTOM_LEFT: case BOTTOM_RIGHT: setAnchorX(getAnchorX() + bounds.getMinX() - computeXOffset()); setAnchorY(getAnchorY() - bounds.getMinY() - bounds.getMaxY() - 1); break; case RIGHT_TOP: case RIGHT_BOTTOM: case RIGHT_CENTER: setAnchorX(getAnchorX() - bounds.getMinX() - bounds.getMaxX() - 1); setAnchorY(getAnchorY() + bounds.getMinY() - computeYOffset()); break; } } private double computeXOffset() { switch (getArrowLocation()) { case TOP_LEFT: case BOTTOM_LEFT: return getCornerRadius() + getArrowIndent() + getArrowSize(); case TOP_CENTER: case BOTTOM_CENTER: return getContentNode().prefWidth(-1) / 2; case TOP_RIGHT: case BOTTOM_RIGHT: return getContentNode().prefWidth(-1) - getArrowIndent() - getCornerRadius() - getArrowSize(); default: return 0; } } private double computeYOffset() { double prefContentHeight = getContentNode().prefHeight(-1); switch (getArrowLocation()) { case LEFT_TOP: case RIGHT_TOP: return getCornerRadius() + getArrowIndent() + getArrowSize(); case LEFT_CENTER: case RIGHT_CENTER: return Math.max(prefContentHeight, 2 * (getCornerRadius() + getArrowIndent() + getArrowSize())) / 2; case LEFT_BOTTOM: case RIGHT_BOTTOM: return Math.max(prefContentHeight - getCornerRadius() - getArrowIndent() - getArrowSize(), getCornerRadius() + getArrowIndent() + getArrowSize()); default: return 0; } } /** * Detaches the pop over from the owning node. The pop over will no longer * display an arrow pointing at the owner node. */ public final void detach() { if (isDetachable()) { setDetached(true); } } // always show header private final BooleanProperty headerAlwaysVisible = new SimpleBooleanProperty(this, "headerAlwaysVisible"); //$NON-NLS-1$ /** * Determines whether or not the {@link PopOver} header should remain visible, even while attached. */ public final BooleanProperty headerAlwaysVisibleProperty() { return headerAlwaysVisible; } /** * Sets the value of the headerAlwaysVisible property. * * @param visible * if true, then the header is visible even while attached * * @see #headerAlwaysVisibleProperty() */ public final void setHeaderAlwaysVisible(boolean visible) { headerAlwaysVisible.setValue(visible); } /** * Returns the value of the detachable property. * * @return true if the header is visible even while attached * * @see #headerAlwaysVisibleProperty() */ public final boolean isHeaderAlwaysVisible() { return headerAlwaysVisible.getValue(); } // enable close button private final BooleanProperty closeButtonEnabled = new SimpleBooleanProperty(this, "closeButtonEnabled", true); //$NON-NLS-1$ /** * Determines whether or not the header's close button should be available. */ public final BooleanProperty closeButtonEnabledProperty() { return closeButtonEnabled; } /** * Sets the value of the closeButtonEnabled property. * * @param enabled * if false, the pop over will not be closeable by the header's close button * * @see #closeButtonEnabledProperty() */ public final void setCloseButtonEnabled(boolean enabled) { closeButtonEnabled.setValue(enabled); } /** * Returns the value of the closeButtonEnabled property. * * @return true if the header's close button is enabled * * @see #closeButtonEnabledProperty() */ public final boolean isCloseButtonEnabled() { return closeButtonEnabled.getValue(); } // detach support private final BooleanProperty detachable = new SimpleBooleanProperty(this, "detachable", true); //$NON-NLS-1$ /** * Determines if the pop over is detachable at all. */ public final BooleanProperty detachableProperty() { return detachable; } /** * Sets the value of the detachable property. * * @param detachable * if true then the user can detach / tear off the pop over * * @see #detachableProperty() */ public final void setDetachable(boolean detachable) { detachableProperty().set(detachable); } /** * Returns the value of the detachable property. * * @return true if the user is allowed to detach / tear off the pop over * * @see #detachableProperty() */ public final boolean isDetachable() { return detachableProperty().get(); } private final BooleanProperty detached = new SimpleBooleanProperty(this, "detached", false); //$NON-NLS-1$ /** * Determines whether the pop over is detached from the owning node or not. * A detached pop over no longer shows an arrow pointing at the owner and * features its own title bar. * * @return the detached property */ public final BooleanProperty detachedProperty() { return detached; } /** * Sets the value of the detached property. * * @param detached * if true the pop over will change its apperance to "detached" * mode * * @see #detachedProperty() */ public final void setDetached(boolean detached) { detachedProperty().set(detached); } /** * Returns the value of the detached property. * * @return true if the pop over is currently detached. * * @see #detachedProperty() */ public final boolean isDetached() { return detachedProperty().get(); } // arrow size support // TODO: make styleable private final DoubleProperty arrowSize = new SimpleDoubleProperty(this, "arrowSize", 12); //$NON-NLS-1$ /** * Controls the size of the arrow. Default value is 12. * * @return the arrow size property */ public final DoubleProperty arrowSizeProperty() { return arrowSize; } /** * Returns the value of the arrow size property. * * @return the arrow size property value * * @see #arrowSizeProperty() */ public final double getArrowSize() { return arrowSizeProperty().get(); } /** * Sets the value of the arrow size property. * * @param size * the new value of the arrow size property * * @see #arrowSizeProperty() */ public final void setArrowSize(double size) { arrowSizeProperty().set(size); } // arrow indent support // TODO: make styleable private final DoubleProperty arrowIndent = new SimpleDoubleProperty(this, "arrowIndent", 12); //$NON-NLS-1$ /** * Controls the distance between the arrow and the corners of the pop over. * The default value is 12. * * @return the arrow indent property */ public final DoubleProperty arrowIndentProperty() { return arrowIndent; } /** * Returns the value of the arrow indent property. * * @return the arrow indent value * * @see #arrowIndentProperty() */ public final double getArrowIndent() { return arrowIndentProperty().get(); } /** * Sets the value of the arrow indent property. * * @param size * the arrow indent value * * @see #arrowIndentProperty() */ public final void setArrowIndent(double size) { arrowIndentProperty().set(size); } // radius support // TODO: make styleable private final DoubleProperty cornerRadius = new SimpleDoubleProperty(this, "cornerRadius", 6); //$NON-NLS-1$ /** * Returns the corner radius property for the pop over. * * @return the corner radius property (default is 6) */ public final DoubleProperty cornerRadiusProperty() { return cornerRadius; } /** * Returns the value of the corner radius property. * * @return the corner radius * * @see #cornerRadiusProperty() */ public final double getCornerRadius() { return cornerRadiusProperty().get(); } /** * Sets the value of the corner radius property. * * @param radius * the corner radius * * @see #cornerRadiusProperty() */ public final void setCornerRadius(double radius) { cornerRadiusProperty().set(radius); } // Detached stage title private final StringProperty title = new SimpleStringProperty(this, "title", "No title set"); //$NON-NLS-1$ //$NON-NLS-2$ /** * Stores the title to display in the PopOver's header. * * @return the title property */ public final StringProperty titleProperty() { return title; } /** * Returns the value of the title property. * * @return the detached title * @see #titleProperty() */ public final String getTitle() { return titleProperty().get(); } /** * Sets the value of the title property. * * @param title the title to use when detached * @see #titleProperty() */ public final void setTitle(String title) { if (title == null) { throw new IllegalArgumentException("title can not be null"); //$NON-NLS-1$ } titleProperty().set(title); } private final ObjectProperty arrowLocation = new SimpleObjectProperty<>( this, "arrowLocation", ArrowLocation.LEFT_TOP); //$NON-NLS-1$ /** * Stores the preferred arrow location. This might not be the actual * location of the arrow if auto fix is enabled. * * @see #setAutoFix(boolean) * * @return the arrow location property */ public final ObjectProperty arrowLocationProperty() { return arrowLocation; } /** * Sets the value of the arrow location property. * * @see #arrowLocationProperty() * * @param location * the requested location */ public final void setArrowLocation(ArrowLocation location) { arrowLocationProperty().set(location); } /** * Returns the value of the arrow location property. * * @see #arrowLocationProperty() * * @return the preferred arrow location */ public final ArrowLocation getArrowLocation() { return arrowLocationProperty().get(); } /** * All possible arrow locations. */ public enum ArrowLocation { LEFT_TOP, LEFT_CENTER, LEFT_BOTTOM, RIGHT_TOP, RIGHT_CENTER, RIGHT_BOTTOM, TOP_LEFT, TOP_CENTER, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_CENTER, BOTTOM_RIGHT } /** * Stores the fade-in duration. This should be set before calling PopOver.show(..). * * @return the fade-in duration property */ public final ObjectProperty fadeInDurationProperty() { return fadeInDuration; } /** * Stores the fade-out duration. * * @return the fade-out duration property */ public final ObjectProperty fadeOutDurationProperty() { return fadeOutDuration; } /** * Returns the value of the fade-in duration property. * * @return the fade-in duration * @see #fadeInDurationProperty() */ public final Duration getFadeInDuration() { return fadeInDurationProperty().get(); } /** * Sets the value of the fade-in duration property. This should be set before calling PopOver.show(..). * * @param duration the requested fade-in duration * @see #fadeInDurationProperty() */ public final void setFadeInDuration(Duration duration) { fadeInDurationProperty().setValue(duration); } /** * Returns the value of the fade-out duration property. * * @return the fade-out duration * @see #fadeOutDurationProperty() */ public final Duration getFadeOutDuration() { return fadeOutDurationProperty().get(); } /** * Sets the value of the fade-out duration property. * * @param duration the requested fade-out duration * @see #fadeOutDurationProperty() */ public final void setFadeOutDuration(Duration duration) { fadeOutDurationProperty().setValue(duration); } /** * Stores the "animated" flag. If true then the PopOver will be shown / hidden with a short fade in / out animation. * * @return the "animated" property */ public final BooleanProperty animatedProperty() { return animated; } /** * Returns the value of the "animated" property. * * @return true if the PopOver will be shown and hidden with a short fade animation * @see #animatedProperty() */ public final boolean isAnimated() { return animatedProperty().get(); } /** * Sets the value of the "animated" property. * * @param animated if true the PopOver will be shown and hidden with a short fade animation * @see #animatedProperty() */ public final void setAnimated(boolean animated) { animatedProperty().set(animated); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/controlsfx/control/popover.css ================================================ .popover { -fx-background-color: transparent; } .popover > .border { -fx-stroke: linear-gradient(to bottom, rgba(0, 0, 0, .3), rgba(0, 0, 0, .7)); -fx-stroke-width: 1; -fx-fill: rgba(255.0, 255.0, 255.0, .95); -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, .2), 10.0, 0.5, 2.0, 2.0); } .popover > .content { } .popover > .detached { } .popover > .content > .title > .text { -fx-padding: 6.0 6.0 0.0 6.0; -fx-text-fill: rgba(120, 120, 120, .8); -fx-font-weight: bold; } .popover > .content > .title > .icon { -fx-padding: 6.0 0.0 0.0 10.0; } .popover > .content > .title > .icon > .graphics > .circle { -fx-fill: gray; -fx-effect: innershadow(gaussian, rgba(0, 0, 0, .2), 3, 0.5, 1.0, 1.0); } .popover > .content > .title > .icon > .graphics > .line { -fx-stroke: white; -fx-stroke-width: 2; } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/controlsfx/skin/PopOverSkin.java ================================================ /* * Copyright (c) 2013 - 2015, ControlsFX * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of ControlsFX, any associated website, nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package haveno.desktop.components.controlsfx.skin; import haveno.desktop.components.controlsfx.control.PopOver; import haveno.desktop.components.controlsfx.control.PopOver.ArrowLocation; import javafx.beans.InvalidationListener; import javafx.beans.binding.Bindings; import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.event.EventHandler; import javafx.geometry.Point2D; import javafx.geometry.Pos; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.Skin; import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.StackPane; import javafx.scene.shape.Circle; import javafx.scene.shape.HLineTo; import javafx.scene.shape.Line; import javafx.scene.shape.LineTo; import javafx.scene.shape.MoveTo; import javafx.scene.shape.Path; import javafx.scene.shape.PathElement; import javafx.scene.shape.QuadCurveTo; import javafx.scene.shape.VLineTo; import javafx.stage.Window; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; import static haveno.desktop.components.controlsfx.control.PopOver.ArrowLocation.BOTTOM_CENTER; import static haveno.desktop.components.controlsfx.control.PopOver.ArrowLocation.BOTTOM_LEFT; import static haveno.desktop.components.controlsfx.control.PopOver.ArrowLocation.BOTTOM_RIGHT; import static haveno.desktop.components.controlsfx.control.PopOver.ArrowLocation.LEFT_BOTTOM; import static haveno.desktop.components.controlsfx.control.PopOver.ArrowLocation.LEFT_CENTER; import static haveno.desktop.components.controlsfx.control.PopOver.ArrowLocation.LEFT_TOP; import static haveno.desktop.components.controlsfx.control.PopOver.ArrowLocation.RIGHT_BOTTOM; import static haveno.desktop.components.controlsfx.control.PopOver.ArrowLocation.RIGHT_CENTER; import static haveno.desktop.components.controlsfx.control.PopOver.ArrowLocation.RIGHT_TOP; import static haveno.desktop.components.controlsfx.control.PopOver.ArrowLocation.TOP_CENTER; import static haveno.desktop.components.controlsfx.control.PopOver.ArrowLocation.TOP_LEFT; import static haveno.desktop.components.controlsfx.control.PopOver.ArrowLocation.TOP_RIGHT; import static java.lang.Double.MAX_VALUE; import static javafx.geometry.Pos.CENTER_LEFT; import static javafx.scene.control.ContentDisplay.GRAPHIC_ONLY; import static javafx.scene.paint.Color.YELLOW; public class PopOverSkin implements Skin { private static final String DETACHED_STYLE_CLASS = "detached"; //$NON-NLS-1$ private double xOffset; private double yOffset; private boolean tornOff; private final Path path; private final Path clip; private final BorderPane content; private final StackPane titlePane; private final StackPane stackPane; private Point2D dragStartLocation; private final PopOver popOver; private final Logger log = LoggerFactory.getLogger(this.getClass()); public PopOverSkin(final PopOver popOver) { this.popOver = popOver; stackPane = popOver.getRoot(); stackPane.setPickOnBounds(false); Bindings.bindContent(stackPane.getStyleClass(), popOver.getStyleClass()); /* * The min width and height equal 2 * corner radius + 2 * arrow indent + * 2 * arrow size. */ stackPane.minWidthProperty().bind( Bindings.add(Bindings.multiply(2, popOver.arrowSizeProperty()), Bindings.add( Bindings.multiply(2, popOver.cornerRadiusProperty()), Bindings.multiply(2, popOver.arrowIndentProperty())))); stackPane.minHeightProperty().bind(stackPane.minWidthProperty()); Label title = new Label(); title.textProperty().bind(popOver.titleProperty()); title.setMaxSize(MAX_VALUE, MAX_VALUE); title.setAlignment(Pos.CENTER); title.getStyleClass().add("text"); //$NON-NLS-1$ Label closeIcon = new Label(); closeIcon.setGraphic(createCloseIcon()); closeIcon.setMaxSize(MAX_VALUE, MAX_VALUE); closeIcon.setContentDisplay(GRAPHIC_ONLY); closeIcon.visibleProperty().bind( popOver.closeButtonEnabledProperty().and( popOver.detachedProperty().or(popOver.headerAlwaysVisibleProperty()))); closeIcon.getStyleClass().add("icon"); //$NON-NLS-1$ closeIcon.setAlignment(CENTER_LEFT); closeIcon.getGraphic().setOnMouseClicked(evt -> popOver.hide()); titlePane = new StackPane(); titlePane.getChildren().add(title); titlePane.getChildren().add(closeIcon); titlePane.getStyleClass().add("title"); //$NON-NLS-1$ content = new BorderPane(); content.setCenter(popOver.getContentNode()); content.getStyleClass().add("content"); //$NON-NLS-1$ if (popOver.isDetached() || popOver.isHeaderAlwaysVisible()) { content.setTop(titlePane); } if (popOver.isDetached()) { popOver.getStyleClass().add(DETACHED_STYLE_CLASS); content.getStyleClass().add(DETACHED_STYLE_CLASS); } popOver.headerAlwaysVisibleProperty().addListener((o, oV, isVisible) -> { if (isVisible) { content.setTop(titlePane); } else if (!popOver.isDetached()) { content.setTop(null); } }); InvalidationListener updatePathListener = observable -> updatePath(); getPopupWindow().xProperty().addListener(updatePathListener); getPopupWindow().yProperty().addListener(updatePathListener); popOver.arrowLocationProperty().addListener(updatePathListener); popOver.contentNodeProperty().addListener( (value, oldContent, newContent) -> content .setCenter(newContent)); popOver.detachedProperty() .addListener((value, oldDetached, newDetached) -> { if (newDetached) { popOver.getStyleClass().add(DETACHED_STYLE_CLASS); content.getStyleClass().add(DETACHED_STYLE_CLASS); content.setTop(titlePane); switch (getSkinnable().getArrowLocation()) { case LEFT_TOP: case LEFT_CENTER: case LEFT_BOTTOM: popOver.setAnchorX( popOver.getAnchorX() + popOver.getArrowSize()); break; case TOP_LEFT: case TOP_CENTER: case TOP_RIGHT: popOver.setAnchorY( popOver.getAnchorY() + popOver.getArrowSize()); break; default: break; } } else { popOver.getStyleClass().remove(DETACHED_STYLE_CLASS); content.getStyleClass().remove(DETACHED_STYLE_CLASS); if (!popOver.isHeaderAlwaysVisible()) { content.setTop(null); } } popOver.sizeToScene(); updatePath(); }); path = new Path(); path.getStyleClass().add("border"); //$NON-NLS-1$ path.setManaged(false); clip = new Path(); /* * The clip is a path and the path has to be filled with a color. * Otherwise clipping will not work. */ clip.setFill(YELLOW); createPathElements(); updatePath(); final EventHandler mousePressedHandler = evt -> { log.info("mousePressed:" + popOver.isDetachable() + "," + popOver.isDetached()); if (popOver.isDetachable() || popOver.isDetached()) { tornOff = false; xOffset = evt.getScreenX(); yOffset = evt.getScreenY(); dragStartLocation = new Point2D(xOffset, yOffset); } }; final EventHandler mouseReleasedHandler = evt -> { log.info("mouseReleased:tornOff" + tornOff + ", " + !getSkinnable().isDetached()); if (tornOff && !getSkinnable().isDetached()) { tornOff = false; getSkinnable().detach(); } }; final EventHandler mouseDragHandler = evt -> { log.info("mouseDrag:" + popOver.isDetachable() + "," + popOver.isDetached()); if (popOver.isDetachable() || popOver.isDetached()) { double deltaX = evt.getScreenX() - xOffset; double deltaY = evt.getScreenY() - yOffset; Window window = getSkinnable().getScene().getWindow(); window.setX(window.getX() + deltaX); window.setY(window.getY() + deltaY); xOffset = evt.getScreenX(); yOffset = evt.getScreenY(); if (dragStartLocation.distance(xOffset, yOffset) > 20) { tornOff = true; updatePath(); } else if (tornOff) { tornOff = false; updatePath(); } } }; stackPane.setOnMousePressed(mousePressedHandler); stackPane.setOnMouseDragged(mouseDragHandler); stackPane.setOnMouseReleased(mouseReleasedHandler); stackPane.getChildren().add(path); stackPane.getChildren().add(content); content.setClip(clip); } @Override public Node getNode() { return stackPane; } @Override public PopOver getSkinnable() { return popOver; } @Override public void dispose() { } private Node createCloseIcon() { Group group = new Group(); group.getStyleClass().add("graphics"); //$NON-NLS-1$ Circle circle = new Circle(); circle.getStyleClass().add("circle"); //$NON-NLS-1$ circle.setRadius(6); circle.setCenterX(6); circle.setCenterY(6); group.getChildren().add(circle); Line line1 = new Line(); line1.getStyleClass().add("line"); //$NON-NLS-1$ line1.setStartX(4); line1.setStartY(4); line1.setEndX(8); line1.setEndY(8); group.getChildren().add(line1); Line line2 = new Line(); line2.getStyleClass().add("line"); //$NON-NLS-1$ line2.setStartX(8); line2.setStartY(4); line2.setEndX(4); line2.setEndY(8); group.getChildren().add(line2); return group; } private MoveTo moveTo; private QuadCurveTo topCurveTo, rightCurveTo, bottomCurveTo, leftCurveTo; private HLineTo lineBTop, lineETop, lineHTop, lineKTop; private LineTo lineCTop, lineDTop, lineFTop, lineGTop, lineITop, lineJTop; private VLineTo lineBRight, lineERight, lineHRight, lineKRight; private LineTo lineCRight, lineDRight, lineFRight, lineGRight, lineIRight, lineJRight; private HLineTo lineBBottom, lineEBottom, lineHBottom, lineKBottom; private LineTo lineCBottom, lineDBottom, lineFBottom, lineGBottom, lineIBottom, lineJBottom; private VLineTo lineBLeft, lineELeft, lineHLeft, lineKLeft; private LineTo lineCLeft, lineDLeft, lineFLeft, lineGLeft, lineILeft, lineJLeft; private void createPathElements() { DoubleProperty centerYProperty = new SimpleDoubleProperty(); DoubleProperty centerXProperty = new SimpleDoubleProperty(); DoubleProperty leftEdgeProperty = new SimpleDoubleProperty(); DoubleProperty leftEdgePlusRadiusProperty = new SimpleDoubleProperty(); DoubleProperty topEdgeProperty = new SimpleDoubleProperty(); DoubleProperty topEdgePlusRadiusProperty = new SimpleDoubleProperty(); DoubleProperty rightEdgeProperty = new SimpleDoubleProperty(); DoubleProperty rightEdgeMinusRadiusProperty = new SimpleDoubleProperty(); DoubleProperty bottomEdgeProperty = new SimpleDoubleProperty(); DoubleProperty bottomEdgeMinusRadiusProperty = new SimpleDoubleProperty(); DoubleProperty cornerProperty = getSkinnable().cornerRadiusProperty(); DoubleProperty arrowSizeProperty = getSkinnable().arrowSizeProperty(); DoubleProperty arrowIndentProperty = getSkinnable() .arrowIndentProperty(); centerYProperty.bind(Bindings.divide(stackPane.heightProperty(), 2)); centerXProperty.bind(Bindings.divide(stackPane.widthProperty(), 2)); leftEdgePlusRadiusProperty.bind(Bindings.add(leftEdgeProperty, getSkinnable().cornerRadiusProperty())); topEdgePlusRadiusProperty.bind(Bindings.add(topEdgeProperty, getSkinnable().cornerRadiusProperty())); rightEdgeProperty.bind(stackPane.widthProperty()); rightEdgeMinusRadiusProperty.bind(Bindings.subtract(rightEdgeProperty, getSkinnable().cornerRadiusProperty())); bottomEdgeProperty.bind(stackPane.heightProperty()); bottomEdgeMinusRadiusProperty.bind(Bindings.subtract( bottomEdgeProperty, getSkinnable().cornerRadiusProperty())); // INIT moveTo = new MoveTo(); moveTo.xProperty().bind(leftEdgePlusRadiusProperty); moveTo.yProperty().bind(topEdgeProperty); // // TOP EDGE // lineBTop = new HLineTo(); lineBTop.xProperty().bind( Bindings.add(leftEdgePlusRadiusProperty, arrowIndentProperty)); lineCTop = new LineTo(); lineCTop.xProperty().bind( Bindings.add(lineBTop.xProperty(), arrowSizeProperty)); lineCTop.yProperty().bind( Bindings.subtract(topEdgeProperty, arrowSizeProperty)); lineDTop = new LineTo(); lineDTop.xProperty().bind( Bindings.add(lineCTop.xProperty(), arrowSizeProperty)); lineDTop.yProperty().bind(topEdgeProperty); lineETop = new HLineTo(); lineETop.xProperty().bind( Bindings.subtract(centerXProperty, arrowSizeProperty)); lineFTop = new LineTo(); lineFTop.xProperty().bind(centerXProperty); lineFTop.yProperty().bind( Bindings.subtract(topEdgeProperty, arrowSizeProperty)); lineGTop = new LineTo(); lineGTop.xProperty().bind( Bindings.add(centerXProperty, arrowSizeProperty)); lineGTop.yProperty().bind(topEdgeProperty); lineHTop = new HLineTo(); lineHTop.xProperty().bind( Bindings.subtract(Bindings.subtract( rightEdgeMinusRadiusProperty, arrowIndentProperty), Bindings.multiply(arrowSizeProperty, 2))); lineITop = new LineTo(); lineITop.xProperty().bind( Bindings.subtract(Bindings.subtract( rightEdgeMinusRadiusProperty, arrowIndentProperty), arrowSizeProperty)); lineITop.yProperty().bind( Bindings.subtract(topEdgeProperty, arrowSizeProperty)); lineJTop = new LineTo(); lineJTop.xProperty().bind( Bindings.subtract(rightEdgeMinusRadiusProperty, arrowIndentProperty)); lineJTop.yProperty().bind(topEdgeProperty); lineKTop = new HLineTo(); lineKTop.xProperty().bind(rightEdgeMinusRadiusProperty); // // RIGHT EDGE // rightCurveTo = new QuadCurveTo(); rightCurveTo.xProperty().bind(rightEdgeProperty); rightCurveTo.yProperty().bind( Bindings.add(topEdgeProperty, cornerProperty)); rightCurveTo.controlXProperty().bind(rightEdgeProperty); rightCurveTo.controlYProperty().bind(topEdgeProperty); lineBRight = new VLineTo(); lineBRight.yProperty().bind( Bindings.add(topEdgePlusRadiusProperty, arrowIndentProperty)); lineCRight = new LineTo(); lineCRight.xProperty().bind( Bindings.add(rightEdgeProperty, arrowSizeProperty)); lineCRight.yProperty().bind( Bindings.add(lineBRight.yProperty(), arrowSizeProperty)); lineDRight = new LineTo(); lineDRight.xProperty().bind(rightEdgeProperty); lineDRight.yProperty().bind( Bindings.add(lineCRight.yProperty(), arrowSizeProperty)); lineERight = new VLineTo(); lineERight.yProperty().bind( Bindings.subtract(centerYProperty, arrowSizeProperty)); lineFRight = new LineTo(); lineFRight.xProperty().bind( Bindings.add(rightEdgeProperty, arrowSizeProperty)); lineFRight.yProperty().bind(centerYProperty); lineGRight = new LineTo(); lineGRight.xProperty().bind(rightEdgeProperty); lineGRight.yProperty().bind( Bindings.add(centerYProperty, arrowSizeProperty)); lineHRight = new VLineTo(); lineHRight.yProperty().bind( Bindings.subtract(Bindings.subtract( bottomEdgeMinusRadiusProperty, arrowIndentProperty), Bindings.multiply(arrowSizeProperty, 2))); lineIRight = new LineTo(); lineIRight.xProperty().bind( Bindings.add(rightEdgeProperty, arrowSizeProperty)); lineIRight.yProperty().bind( Bindings.subtract(Bindings.subtract( bottomEdgeMinusRadiusProperty, arrowIndentProperty), arrowSizeProperty)); lineJRight = new LineTo(); lineJRight.xProperty().bind(rightEdgeProperty); lineJRight.yProperty().bind( Bindings.subtract(bottomEdgeMinusRadiusProperty, arrowIndentProperty)); lineKRight = new VLineTo(); lineKRight.yProperty().bind(bottomEdgeMinusRadiusProperty); // // BOTTOM EDGE // bottomCurveTo = new QuadCurveTo(); bottomCurveTo.xProperty().bind(rightEdgeMinusRadiusProperty); bottomCurveTo.yProperty().bind(bottomEdgeProperty); bottomCurveTo.controlXProperty().bind(rightEdgeProperty); bottomCurveTo.controlYProperty().bind(bottomEdgeProperty); lineBBottom = new HLineTo(); lineBBottom.xProperty().bind( Bindings.subtract(rightEdgeMinusRadiusProperty, arrowIndentProperty)); lineCBottom = new LineTo(); lineCBottom.xProperty().bind( Bindings.subtract(lineBBottom.xProperty(), arrowSizeProperty)); lineCBottom.yProperty().bind( Bindings.add(bottomEdgeProperty, arrowSizeProperty)); lineDBottom = new LineTo(); lineDBottom.xProperty().bind( Bindings.subtract(lineCBottom.xProperty(), arrowSizeProperty)); lineDBottom.yProperty().bind(bottomEdgeProperty); lineEBottom = new HLineTo(); lineEBottom.xProperty().bind( Bindings.add(centerXProperty, arrowSizeProperty)); lineFBottom = new LineTo(); lineFBottom.xProperty().bind(centerXProperty); lineFBottom.yProperty().bind( Bindings.add(bottomEdgeProperty, arrowSizeProperty)); lineGBottom = new LineTo(); lineGBottom.xProperty().bind( Bindings.subtract(centerXProperty, arrowSizeProperty)); lineGBottom.yProperty().bind(bottomEdgeProperty); lineHBottom = new HLineTo(); lineHBottom.xProperty().bind( Bindings.add(Bindings.add(leftEdgePlusRadiusProperty, arrowIndentProperty), Bindings.multiply( arrowSizeProperty, 2))); lineIBottom = new LineTo(); lineIBottom.xProperty().bind( Bindings.add(Bindings.add(leftEdgePlusRadiusProperty, arrowIndentProperty), arrowSizeProperty)); lineIBottom.yProperty().bind( Bindings.add(bottomEdgeProperty, arrowSizeProperty)); lineJBottom = new LineTo(); lineJBottom.xProperty().bind( Bindings.add(leftEdgePlusRadiusProperty, arrowIndentProperty)); lineJBottom.yProperty().bind(bottomEdgeProperty); lineKBottom = new HLineTo(); lineKBottom.xProperty().bind(leftEdgePlusRadiusProperty); // // LEFT EDGE // leftCurveTo = new QuadCurveTo(); leftCurveTo.xProperty().bind(leftEdgeProperty); leftCurveTo.yProperty().bind( Bindings.subtract(bottomEdgeProperty, cornerProperty)); leftCurveTo.controlXProperty().bind(leftEdgeProperty); leftCurveTo.controlYProperty().bind(bottomEdgeProperty); lineBLeft = new VLineTo(); lineBLeft.yProperty().bind( Bindings.subtract(bottomEdgeMinusRadiusProperty, arrowIndentProperty)); lineCLeft = new LineTo(); lineCLeft.xProperty().bind( Bindings.subtract(leftEdgeProperty, arrowSizeProperty)); lineCLeft.yProperty().bind( Bindings.subtract(lineBLeft.yProperty(), arrowSizeProperty)); lineDLeft = new LineTo(); lineDLeft.xProperty().bind(leftEdgeProperty); lineDLeft.yProperty().bind( Bindings.subtract(lineCLeft.yProperty(), arrowSizeProperty)); lineELeft = new VLineTo(); lineELeft.yProperty().bind( Bindings.add(centerYProperty, arrowSizeProperty)); lineFLeft = new LineTo(); lineFLeft.xProperty().bind( Bindings.subtract(leftEdgeProperty, arrowSizeProperty)); lineFLeft.yProperty().bind(centerYProperty); lineGLeft = new LineTo(); lineGLeft.xProperty().bind(leftEdgeProperty); lineGLeft.yProperty().bind( Bindings.subtract(centerYProperty, arrowSizeProperty)); lineHLeft = new VLineTo(); lineHLeft.yProperty().bind( Bindings.add(Bindings.add(topEdgePlusRadiusProperty, arrowIndentProperty), Bindings.multiply( arrowSizeProperty, 2))); lineILeft = new LineTo(); lineILeft.xProperty().bind( Bindings.subtract(leftEdgeProperty, arrowSizeProperty)); lineILeft.yProperty().bind( Bindings.add(Bindings.add(topEdgePlusRadiusProperty, arrowIndentProperty), arrowSizeProperty)); lineJLeft = new LineTo(); lineJLeft.xProperty().bind(leftEdgeProperty); lineJLeft.yProperty().bind( Bindings.add(topEdgePlusRadiusProperty, arrowIndentProperty)); lineKLeft = new VLineTo(); lineKLeft.yProperty().bind(topEdgePlusRadiusProperty); topCurveTo = new QuadCurveTo(); topCurveTo.xProperty().bind(leftEdgePlusRadiusProperty); topCurveTo.yProperty().bind(topEdgeProperty); topCurveTo.controlXProperty().bind(leftEdgeProperty); topCurveTo.controlYProperty().bind(topEdgeProperty); } private Window getPopupWindow() { return getSkinnable().getScene().getWindow(); } private boolean showArrow(ArrowLocation location) { ArrowLocation arrowLocation = getSkinnable().getArrowLocation(); return location.equals(arrowLocation) && !getSkinnable().isDetached() && !tornOff; } private void updatePath() { List elements = new ArrayList<>(); elements.add(moveTo); if (showArrow(TOP_LEFT)) { elements.add(lineBTop); elements.add(lineCTop); elements.add(lineDTop); } if (showArrow(TOP_CENTER)) { elements.add(lineETop); elements.add(lineFTop); elements.add(lineGTop); } if (showArrow(TOP_RIGHT)) { elements.add(lineHTop); elements.add(lineITop); elements.add(lineJTop); } elements.add(lineKTop); elements.add(rightCurveTo); if (showArrow(RIGHT_TOP)) { elements.add(lineBRight); elements.add(lineCRight); elements.add(lineDRight); } if (showArrow(RIGHT_CENTER)) { elements.add(lineERight); elements.add(lineFRight); elements.add(lineGRight); } if (showArrow(RIGHT_BOTTOM)) { elements.add(lineHRight); elements.add(lineIRight); elements.add(lineJRight); } elements.add(lineKRight); elements.add(bottomCurveTo); if (showArrow(BOTTOM_RIGHT)) { elements.add(lineBBottom); elements.add(lineCBottom); elements.add(lineDBottom); } if (showArrow(BOTTOM_CENTER)) { elements.add(lineEBottom); elements.add(lineFBottom); elements.add(lineGBottom); } if (showArrow(BOTTOM_LEFT)) { elements.add(lineHBottom); elements.add(lineIBottom); elements.add(lineJBottom); } elements.add(lineKBottom); elements.add(leftCurveTo); if (showArrow(LEFT_BOTTOM)) { elements.add(lineBLeft); elements.add(lineCLeft); elements.add(lineDLeft); } if (showArrow(LEFT_CENTER)) { elements.add(lineELeft); elements.add(lineFLeft); elements.add(lineGLeft); } if (showArrow(LEFT_TOP)) { elements.add(lineHLeft); elements.add(lineILeft); elements.add(lineJLeft); } elements.add(lineKLeft); elements.add(topCurveTo); path.getElements().setAll(elements); clip.getElements().setAll(elements); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/indicator/TxConfidenceIndicator.java ================================================ /* * 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 . */ /* * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code 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 General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package haveno.desktop.components.indicator; import haveno.common.UserThread; import haveno.desktop.components.indicator.skin.StaticProgressIndicatorSkin; import javafx.beans.property.DoubleProperty; import javafx.beans.property.DoublePropertyBase; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.css.PseudoClass; import javafx.css.StyleableProperty; import javafx.scene.control.Control; import javafx.scene.control.Skin; // TODO Copied form OpenJFX, check license issues and way how we integrated it // We changed behaviour which was not exposed via APIs /** * A circular control which is used for indicating progress, either * infinite (aka indeterminate) or finite. Often used with the Task API for * representing progress of background Tasks. *

    * ProgressIndicator sets focusTraversable to false. *

    *

    *

    * This first example creates a ProgressIndicator with an indeterminate value : *

    
     * import javafx.scene.control.ProgressIndicator;
     * ProgressIndicator p1 = new ProgressIndicator();
     * 
    *

    *

    * This next example creates a ProgressIndicator which is 25% complete : *

    
     * import javafx.scene.control.ProgressIndicator;
     * ProgressIndicator p2 = new ProgressIndicator();
     * p2.setProgress(0.25F);
     * 
    *

    * Implementation of ProgressIndicator According to JavaFX UI Control API Specification * * @since JavaFX 2.0 */ @SuppressWarnings({"SameParameterValue", "WeakerAccess"}) public class TxConfidenceIndicator extends Control { /** * Value for progress indicating that the progress is indeterminate. * * @see #setProgress */ public static final double INDETERMINATE_PROGRESS = -1; /*************************************************************************** * * * Constructors * * * **************************************************************************/ /** * Initialize the style class to 'progress-indicator'. *

    * This is the selector class from which CSS can be used to style * this control. */ private static final String DEFAULT_STYLE_CLASS = "progress-indicator"; /** * Pseudoclass indicating this is a determinate (i.e., progress can be * determined) progress indicator. */ private static final PseudoClass PSEUDO_CLASS_DETERMINATE = PseudoClass.getPseudoClass("determinate"); /*************************************************************************** * * * Properties * * * **************************************************************************/ /** * Pseudoclass indicating this is an indeterminate (i.e., progress cannot * be determined) progress indicator. */ private static final PseudoClass PSEUDO_CLASS_INDETERMINATE = PseudoClass.getPseudoClass("indeterminate"); /** * A flag indicating whether it is possible to determine the progress * of the ProgressIndicator. Typically indeterminate progress bars are * rendered with some form of animation indicating potentially "infinite" * progress. */ private ReadOnlyBooleanWrapper indeterminate; /** * The actual progress of the ProgressIndicator. A negative value for * progress indicates that the progress is indeterminate. A positive value * between 0 and 1 indicates the percentage of progress where 0 is 0% and 1 * is 100%. Any value greater than 1 is interpreted as 100%. */ private DoubleProperty progress; /** * Creates a new indeterminate ProgressIndicator. */ public TxConfidenceIndicator() { this(INDETERMINATE_PROGRESS); } /** * Creates a new ProgressIndicator with the given progress value. */ @SuppressWarnings("unchecked") public TxConfidenceIndicator(double progress) { // focusTraversable is styleable through css. Calling setFocusTraversable // makes it look to css like the user set the value and css will not // override. Initializing focusTraversable by calling applyStyle with null // StyleOrigin ensures that css will be able to override the value. ((StyleableProperty) focusTraversableProperty()).applyStyle(null, Boolean.FALSE); setProgress(progress); getStyleClass().setAll(DEFAULT_STYLE_CLASS); // need to initialize pseudo-class state final int c = Double.compare(INDETERMINATE_PROGRESS, progress); pseudoClassStateChanged(PSEUDO_CLASS_INDETERMINATE, c == 0); pseudoClassStateChanged(PSEUDO_CLASS_DETERMINATE, c != 0); } public final boolean isIndeterminate() { return indeterminate == null || indeterminate.get(); } private void setIndeterminate(boolean value) { indeterminatePropertyImpl().set(value); } public final ReadOnlyBooleanProperty indeterminateProperty() { return indeterminatePropertyImpl().getReadOnlyProperty(); } private ReadOnlyBooleanWrapper indeterminatePropertyImpl() { if (indeterminate == null) { indeterminate = new ReadOnlyBooleanWrapper(true) { @Override protected void invalidated() { final boolean active = get(); pseudoClassStateChanged(PSEUDO_CLASS_INDETERMINATE, active); pseudoClassStateChanged(PSEUDO_CLASS_DETERMINATE, !active); } @Override public Object getBean() { return TxConfidenceIndicator.this; } @Override public String getName() { return "indeterminate"; } }; } return indeterminate; } /** * ************************************************************************ * * * Methods * * * * ************************************************************************ */ public final double getProgress() { return progress == null ? INDETERMINATE_PROGRESS : progress.get(); } /** * ************************************************************************ * * * Stylesheet Handling * * * * ************************************************************************ */ public final void setProgress(double value) { UserThread.execute(() -> progressProperty().set(value)); } public final DoubleProperty progressProperty() { if (progress == null) { progress = new DoublePropertyBase(-1.0) { @Override protected void invalidated() { setIndeterminate(getProgress() < 0.0); } @Override public Object getBean() { return TxConfidenceIndicator.this; } @Override public String getName() { return "progress"; } }; } return progress; } /** * {@inheritDoc} */ @Override protected Skin createDefaultSkin() { return new StaticProgressIndicatorSkin(this); } /** * Most Controls return true for focusTraversable, so Control overrides * this method to return true, but ProgressIndicator returns false for * focusTraversable's initial value; hence the override of the override. * This method is called from CSS code to get the correct initial value. */ @Deprecated @SuppressWarnings("deprecation") protected /*do not make final*/ Boolean impl_cssGetFocusTraversableInitialValue() { return Boolean.FALSE; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/indicator/skin/StaticProgressIndicatorSkin.java ================================================ /* * 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 . */ /* * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code 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 General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package haveno.desktop.components.indicator.skin; import haveno.desktop.components.indicator.TxConfidenceIndicator; import javafx.beans.InvalidationListener; import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.css.CssMetaData; import javafx.css.StyleOrigin; import javafx.css.Styleable; import javafx.css.StyleableBooleanProperty; import javafx.css.StyleableIntegerProperty; import javafx.css.StyleableObjectProperty; import javafx.css.StyleableProperty; import javafx.css.converter.BooleanConverter; import javafx.css.converter.PaintConverter; import javafx.css.converter.SizeConverter; import javafx.geometry.Insets; import javafx.geometry.NodeOrientation; import javafx.scene.Node; import javafx.scene.control.SkinBase; import javafx.scene.layout.Pane; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; import javafx.scene.shape.Arc; import javafx.scene.shape.ArcType; import javafx.scene.shape.Circle; import javafx.scene.transform.Scale; import java.util.ArrayList; import java.util.List; // TODO Copied form OpenJFX, check license issues and way how we integrated it // We changed behaviour which was not exposed via APIs public class StaticProgressIndicatorSkin extends SkinBase { /** * ************************************************************************ * * * UI SubComponents * * * * ************************************************************************ */ private IndeterminateSpinner spinner; /** * The number of segments in the spinner. */ private final IntegerProperty indeterminateSegmentCount = new StyleableIntegerProperty(8) { @Override protected void invalidated() { if (spinner != null) { spinner.rebuild(); } } @Override public Object getBean() { return StaticProgressIndicatorSkin.this; } @Override public String getName() { return "indeterminateSegmentCount"; } @Override public CssMetaData getCssMetaData() { return StyleableProperties.INDETERMINATE_SEGMENT_COUNT; } }; /** * True if the progress indicator should rotate as well as animate opacity. */ private final BooleanProperty spinEnabled = new StyleableBooleanProperty(false) { @Override protected void invalidated() { if (spinner != null) { spinner.setSpinEnabled(get()); } } @Override public CssMetaData getCssMetaData() { return StyleableProperties.SPIN_ENABLED; } @Override public Object getBean() { return StaticProgressIndicatorSkin.this; } @Override public String getName() { return "spinEnabled"; } }; private DeterminateIndicator determinateIndicator; /** * The colour of the progress segment. */ private final ObjectProperty progressColor = new StyleableObjectProperty<>(null) { @Override public void set(Paint newProgressColor) { final Paint color = (newProgressColor instanceof Color) ? newProgressColor : null; super.set(color); } @Override protected void invalidated() { if (spinner != null) { spinner.setFillOverride(get()); } if (determinateIndicator != null) { determinateIndicator.setFillOverride(get()); } } @Override public Object getBean() { return StaticProgressIndicatorSkin.this; } @Override public String getName() { return "progressColorProperty"; } @Override public CssMetaData getCssMetaData() { return StyleableProperties.PROGRESS_COLOR; } }; private boolean timeLineNulled = false; /** * ************************************************************************ * * * Constructors * * * * ************************************************************************ */ @SuppressWarnings("deprecation") public StaticProgressIndicatorSkin(TxConfidenceIndicator control) { super(control); InvalidationListener indeterminateListener = valueModel -> initialize(); control.indeterminateProperty().addListener(indeterminateListener); InvalidationListener visibilityListener = valueModel -> { if (getSkinnable().isIndeterminate() && timeLineNulled && spinner == null) { timeLineNulled = false; spinner = new IndeterminateSpinner( getSkinnable(), StaticProgressIndicatorSkin.this, spinEnabled.get(), progressColor.get()); getChildren().add(spinner); } if (spinner != null) { if (getSkinnable().getScene() != null) { getChildren().remove(spinner); spinner = null; timeLineNulled = true; } } }; control.visibleProperty().addListener(visibilityListener); control.parentProperty().addListener(visibilityListener); InvalidationListener sceneListener = valueModel -> { if (spinner != null) { if (getSkinnable().getScene() == null) { getChildren().remove(spinner); spinner = null; timeLineNulled = true; } } else { if (getSkinnable().getScene() != null && getSkinnable().isIndeterminate()) { timeLineNulled = false; spinner = new IndeterminateSpinner( getSkinnable(), StaticProgressIndicatorSkin.this, spinEnabled.get(), progressColor.get()); getChildren().add(spinner); getSkinnable().requestLayout(); } } }; control.sceneProperty().addListener(sceneListener); initialize(); getSkinnable().requestLayout(); } /** * @return The CssMetaData associated with this class, which may include the * CssMetaData of its super classes. */ @SuppressWarnings("SameReturnValue") public static ObservableList> getClassCssMetaData() { return StyleableProperties.STYLEABLES; } @SuppressWarnings("deprecation") private void initialize() { TxConfidenceIndicator control = getSkinnable(); boolean isIndeterminate = control.isIndeterminate(); if (isIndeterminate) { // clean up determinateIndicator determinateIndicator = null; // create spinner spinner = new IndeterminateSpinner(control, this, spinEnabled.get(), progressColor.get()); getChildren().clear(); getChildren().add(spinner); } else { // clean up after spinner if (spinner != null) { spinner = null; } // create determinateIndicator determinateIndicator = new StaticProgressIndicatorSkin.DeterminateIndicator(control, this, progressColor.get()); getChildren().clear(); getChildren().add(determinateIndicator); } } @Override public void dispose() { super.dispose(); if (spinner != null) { spinner = null; } } @Override protected void layoutChildren(final double x, final double y, final double w, final double h) { if (spinner != null && getSkinnable().isIndeterminate()) { spinner.layoutChildren(); spinner.resizeRelocate(0, 0, w, h); } else if (determinateIndicator != null) { determinateIndicator.layoutChildren(); determinateIndicator.resizeRelocate(0, 0, w, h); } } public Paint getProgressColor() { return progressColor.get(); } /** * {@inheritDoc} */ @Override public ObservableList> getCssMetaData() { return getClassCssMetaData(); } // *********** Stylesheet Handling ***************************************** /** * ************************************************************************ * * * DeterminateIndicator * * * * ************************************************************************ */ @SuppressWarnings({"SameReturnValue", "UnusedParameters"}) static class DeterminateIndicator extends Region { //private double textGap = 2.0F; private final TxConfidenceIndicator control; //private Text text; private final StackPane indicator; private final StackPane progress; private final StackPane tick; private final Arc arcShape; private final Circle indicatorCircle; // only update progress text on whole percentages private int intProgress; // only update pie arc to nearest degree private int degProgress; DeterminateIndicator(TxConfidenceIndicator control, StaticProgressIndicatorSkin s, Paint fillOverride) { this.control = control; getStyleClass().add("determinate-indicator"); intProgress = (int) Math.round(control.getProgress() * 100.0); degProgress = (int) (360 * control.getProgress()); InvalidationListener progressListener = valueModel -> updateProgress(); control.progressProperty().addListener(progressListener); getChildren().clear(); // The circular background for the progress pie piece indicator = new StackPane(); indicator.setScaleShape(false); indicator.setCenterShape(false); indicator.getStyleClass().setAll("indicator"); indicatorCircle = new Circle(); indicator.setShape(indicatorCircle); // The shape for our progress pie piece arcShape = new Arc(); arcShape.setType(ArcType.ROUND); arcShape.setStartAngle(90.0F); // Our progress pie piece progress = new StackPane(); progress.getStyleClass().setAll("progress"); progress.setScaleShape(false); progress.setCenterShape(false); progress.setShape(arcShape); progress.getChildren().clear(); setFillOverride(fillOverride); // The check mark that's drawn at 100% tick = new StackPane(); tick.getStyleClass().setAll("tick"); getChildren().setAll(indicator, progress, /*text,*/ tick); updateProgress(); } private void setFillOverride(Paint fillOverride) { if (fillOverride instanceof Color) { Color c = (Color) fillOverride; progress.setStyle("-fx-background-color: rgba(" + ((int) (255 * c.getRed())) + "," + "" + ((int) (255 * c.getGreen())) + "," + ((int) (255 * c.getBlue())) + "," + "" + c.getOpacity() + ");"); } else { progress.setStyle(null); } } //@Override public boolean isAutomaticallyMirrored() { // This is used instead of setting NodeOrientation, // allowing the Text node to inherit the current // orientation. return false; } private void updateProgress() { intProgress = (int) Math.round(control.getProgress() * 100.0); // text.setText((control.getProgress() >= 1) ? (DONE) : ("" + intProgress + "%")); degProgress = (int) (360 * control.getProgress()); arcShape.setLength(-degProgress); indicator.setOpacity(control.getProgress() == 0 ? 0 : 1); requestLayout(); } @Override protected void layoutChildren() { // Position and size the circular background //double doneTextHeight = doneText.getLayoutBounds().getHeight(); final Insets controlInsets = control.getInsets(); final double left = snapSizeX(controlInsets.getLeft()); final double right = snapSizeX(controlInsets.getRight()); final double top = snapSizeY(controlInsets.getTop()); final double bottom = snapSizeY(controlInsets.getBottom()); /* ** use the min of width, or height, keep it a circle */ final double areaW = control.getWidth() - left - right; final double areaH = control.getHeight() - top - bottom /*- textGap - doneTextHeight*/; final double radiusW = areaW / 2; final double radiusH = areaH / 2; final double radius = Math.round(Math.min(radiusW, radiusH)); // use round instead of floor final double centerX = snapPositionX(left + radiusW); final double centerY = snapPositionY(top + radius); // find radius that fits inside radius - insetsPadding final Insets indicatorInsets = indicator.getInsets(); final double iLeft = snapSizeX(indicatorInsets.getLeft()); final double iRight = snapSizeX(indicatorInsets.getRight()); final double iTop = snapSizeY(indicatorInsets.getTop()); final double iBottom = snapSizeY(indicatorInsets.getBottom()); final double progressRadius = snapSizeX(Math.min(Math.min(radius - iLeft, radius - iRight), Math.min(radius - iTop, radius - iBottom))); indicatorCircle.setRadius(radius); indicator.setLayoutX(centerX); indicator.setLayoutY(centerY); arcShape.setRadiusX(progressRadius); arcShape.setRadiusY(progressRadius); progress.setLayoutX(centerX); progress.setLayoutY(centerY); // find radius that fits inside progressRadius - progressInsets final Insets progressInsets = progress.getInsets(); final double pLeft = snapSizeX(progressInsets.getLeft()); final double pRight = snapSizeX(progressInsets.getRight()); final double pTop = snapSizeY(progressInsets.getTop()); final double pBottom = snapSizeY(progressInsets.getBottom()); final double indicatorRadius = snapSizeX(Math.min(Math.min(progressRadius - pLeft, progressRadius - pRight), Math.min(progressRadius - pTop, progressRadius - pBottom))); // find size of spare box that fits inside indicator radius double squareBoxHalfWidth = Math.ceil(Math.sqrt((indicatorRadius * indicatorRadius) / 2)); // double squareBoxHalfWidth2 = indicatorRadius * (Math.sqrt(2) / 2); tick.setLayoutX(centerX - squareBoxHalfWidth); tick.setLayoutY(centerY - squareBoxHalfWidth); tick.resize(squareBoxHalfWidth + squareBoxHalfWidth, squareBoxHalfWidth + squareBoxHalfWidth); tick.setVisible(control.getProgress() >= 1); } @Override protected double computePrefWidth(double height) { final Insets controlInsets = control.getInsets(); final double left = snapSizeX(controlInsets.getLeft()); final double right = snapSizeX(controlInsets.getRight()); final Insets indicatorInsets = indicator.getInsets(); final double iLeft = snapSizeX(indicatorInsets.getLeft()); final double iRight = snapSizeX(indicatorInsets.getRight()); final double iTop = snapSizeY(indicatorInsets.getTop()); final double iBottom = snapSizeY(indicatorInsets.getBottom()); final double indicatorMax = snapSizeX(Math.max(Math.max(iLeft, iRight), Math.max(iTop, iBottom))); final Insets progressInsets = progress.getInsets(); final double pLeft = snapSizeX(progressInsets.getLeft()); final double pRight = snapSizeX(progressInsets.getRight()); final double pTop = snapSizeY(progressInsets.getTop()); final double pBottom = snapSizeY(progressInsets.getBottom()); final double progressMax = snapSizeX(Math.max(Math.max(pLeft, pRight), Math.max(pTop, pBottom))); final Insets tickInsets = tick.getInsets(); final double tLeft = snapSizeX(tickInsets.getLeft()); final double tRight = snapSizeX(tickInsets.getRight()); final double indicatorWidth = indicatorMax + progressMax + tLeft + tRight + progressMax + indicatorMax; return left + indicatorWidth + /*Math.max(indicatorWidth, doneText.getLayoutBounds().getWidth()) + */right; } @Override protected double computePrefHeight(double width) { final Insets controlInsets = control.getInsets(); final double top = snapSizeY(controlInsets.getTop()); final double bottom = snapSizeY(controlInsets.getBottom()); final Insets indicatorInsets = indicator.getInsets(); final double iLeft = snapSizeX(indicatorInsets.getLeft()); final double iRight = snapSizeX(indicatorInsets.getRight()); final double iTop = snapSizeY(indicatorInsets.getTop()); final double iBottom = snapSizeY(indicatorInsets.getBottom()); final double indicatorMax = snapSizeX(Math.max(Math.max(iLeft, iRight), Math.max(iTop, iBottom))); final Insets progressInsets = progress.getInsets(); final double pLeft = snapSizeX(progressInsets.getLeft()); final double pRight = snapSizeX(progressInsets.getRight()); final double pTop = snapSizeY(progressInsets.getTop()); final double pBottom = snapSizeY(progressInsets.getBottom()); final double progressMax = snapSizeX(Math.max(Math.max(pLeft, pRight), Math.max(pTop, pBottom))); final Insets tickInsets = tick.getInsets(); final double tTop = snapSizeY(tickInsets.getTop()); final double tBottom = snapSizeY(tickInsets.getBottom()); final double indicatorHeight = indicatorMax + progressMax + tTop + tBottom + progressMax + indicatorMax; return top + indicatorHeight /*+ textGap + doneText.getLayoutBounds().getHeight()*/ + bottom; } @Override protected double computeMaxWidth(double height) { return computePrefWidth(height); } @Override protected double computeMaxHeight(double width) { return computePrefHeight(width); } } /** * ************************************************************************ * * * IndeterminateSpinner * * * * ************************************************************************ */ @SuppressWarnings({"ConstantConditions", "MismatchedQueryAndUpdateOfCollection"}) static class IndeterminateSpinner extends Region { private final TxConfidenceIndicator control; private final StaticProgressIndicatorSkin skin; private final IndicatorPaths pathsG; private final List opacities = new ArrayList<>(); private Paint fillOverride = null; IndeterminateSpinner(TxConfidenceIndicator control, StaticProgressIndicatorSkin s, boolean spinEnabled, Paint fillOverride) { this.control = control; this.skin = s; this.fillOverride = fillOverride; setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT); getStyleClass().setAll("spinner"); pathsG = new IndicatorPaths(this); getChildren().add(pathsG); rebuild(); } void setFillOverride(Paint fillOverride) { this.fillOverride = fillOverride; rebuild(); } void setSpinEnabled(boolean spinEnabled) { } @Override protected void layoutChildren() { Insets controlInsets = control.getInsets(); final double w = control.getWidth() - controlInsets.getLeft() - controlInsets.getRight(); final double h = control.getHeight() - controlInsets.getTop() - controlInsets.getBottom(); final double prefW = pathsG.prefWidth(-1); final double prefH = pathsG.prefHeight(-1); double scaleX = w / prefW; double scale = scaleX; if ((scaleX * prefH) > h) { scale = h / prefH; } double indicatorW = prefW * scale - 3; double indicatorH = prefH * scale - 3; pathsG.resizeRelocate((w - indicatorW) / 2, (h - indicatorH) / 2, indicatorW, indicatorH); } private void rebuild() { // update indeterminate indicator final int segments = skin.indeterminateSegmentCount.get(); opacities.clear(); pathsG.getChildren().clear(); final double step = 0.8 / (segments - 1); for (int i = 0; i < segments; i++) { Region region = new Region(); region.setScaleShape(false); region.setCenterShape(false); region.getStyleClass().addAll("segment", "segment" + i); if (fillOverride instanceof Color) { Color c = (Color) fillOverride; region.setStyle("-fx-background-color: rgba(" + ((int) (255 * c.getRed())) + "," + "" + ((int) (255 * c.getGreen())) + "," + ((int) (255 * c.getBlue())) + "," + "" + c.getOpacity() + ");"); } else { region.setStyle(null); } double opacity = Math.min(1, i * step); opacities.add(opacity); region.setOpacity(opacity); pathsG.getChildren().add(region); } } @SuppressWarnings("deprecation") private class IndicatorPaths extends Pane { final IndeterminateSpinner piSkin; IndicatorPaths(IndeterminateSpinner pi) { super(); piSkin = pi; } @Override protected double computePrefWidth(double height) { double w = 0; for (Node child : getChildren()) { if (child instanceof Region) { Region region = (Region) child; if (region.getShape() != null) { w = Math.max(w, region.getShape().getLayoutBounds().getMaxX()); } else { w = Math.max(w, region.prefWidth(height)); } } } return w; } @Override protected double computePrefHeight(double width) { double h = 0; for (Node child : getChildren()) { if (child instanceof Region) { Region region = (Region) child; if (region.getShape() != null) { h = Math.max(h, region.getShape().getLayoutBounds().getMaxY()); } else { h = Math.max(h, region.prefHeight(width)); } } } return h; } @Override protected void layoutChildren() { // calculate scale double scale = getWidth() / computePrefWidth(-1); getChildren().stream().filter(child -> child instanceof Region).forEach(child -> { Region region = (Region) child; if (region.getShape() != null) { region.resize(region.getShape().getLayoutBounds().getMaxX(), region.getShape().getLayoutBounds().getMaxY()); region.getTransforms().setAll(new Scale(scale, scale, 0, 0)); } else { region.autosize(); } }); } } } /** * Super-lazy instantiation pattern from Bill Pugh. */ @SuppressWarnings({"deprecation", "unchecked", "ConstantConditions"}) private static class StyleableProperties { static final ObservableList> STYLEABLES; private static final CssMetaData PROGRESS_COLOR = new CssMetaData<>( "-fx-progress-color", PaintConverter.getInstance(), null) { @Override public boolean isSettable(TxConfidenceIndicator n) { final StaticProgressIndicatorSkin skin = (StaticProgressIndicatorSkin) n.getSkin(); return skin.progressColor == null || !skin.progressColor.isBound(); } @Override public StyleableProperty getStyleableProperty(TxConfidenceIndicator n) { final StaticProgressIndicatorSkin skin = (StaticProgressIndicatorSkin) n.getSkin(); return (StyleableProperty) skin.progressColor; } }; private static final CssMetaData INDETERMINATE_SEGMENT_COUNT = new CssMetaData<>( "-fx-indeterminate-segment-count", SizeConverter.getInstance(), 8) { @Override public void set(TxConfidenceIndicator node, Number value, StyleOrigin origin) { super.set(node, value.intValue(), origin); } @Override public boolean isSettable(TxConfidenceIndicator n) { final StaticProgressIndicatorSkin skin = (StaticProgressIndicatorSkin) n.getSkin(); return skin.indeterminateSegmentCount == null || !skin.indeterminateSegmentCount.isBound(); } @Override public StyleableProperty getStyleableProperty(TxConfidenceIndicator n) { final StaticProgressIndicatorSkin skin = (StaticProgressIndicatorSkin) n.getSkin(); return (StyleableProperty) skin.indeterminateSegmentCount; } }; private static final CssMetaData SPIN_ENABLED = new CssMetaData<>("-fx-spin-enabled", BooleanConverter.getInstance(), Boolean.FALSE) { @Override public boolean isSettable(TxConfidenceIndicator node) { final StaticProgressIndicatorSkin skin = (StaticProgressIndicatorSkin) node.getSkin(); return skin.spinEnabled == null || !skin.spinEnabled.isBound(); } @Override public StyleableProperty getStyleableProperty(TxConfidenceIndicator node) { final StaticProgressIndicatorSkin skin = (StaticProgressIndicatorSkin) node.getSkin(); return (StyleableProperty) skin.spinEnabled; } }; static { final ObservableList> styleables = FXCollections.observableArrayList(SkinBase.getClassCssMetaData()); styleables.add(PROGRESS_COLOR); styleables.add(INDETERMINATE_SEGMENT_COUNT); styleables.add(SPIN_ENABLED); STYLEABLES = FXCollections.unmodifiableObservableList(styleables); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/list/FilterBox.java ================================================ /* * 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 . */ package haveno.desktop.components.list; import haveno.common.UserThread; import haveno.desktop.components.InputTextField; import haveno.desktop.util.filtering.FilterableListItem; import javafx.beans.value.ChangeListener; import javafx.collections.transformation.FilteredList; import javafx.scene.control.TableView; import javafx.scene.layout.HBox; public class FilterBox extends HBox { private final InputTextField textField; private FilteredList filteredList; private ChangeListener listener; public FilterBox() { super(); setSpacing(5.0); textField = new InputTextField(); textField.setMinWidth(500); getChildren().addAll(textField); } public void initialize(FilteredList filteredList, TableView tableView) { this.filteredList = filteredList; listener = (observable, oldValue, newValue) -> { UserThread.execute(() -> { applyFilter(tableView, null); }); }; } public void initializeWithCallback(FilteredList filteredList, TableView tableView, Runnable callback) { this.filteredList = filteredList; listener = (observable, oldValue, newValue) -> applyFilter(tableView, callback); applyFilter(tableView, callback); // first time init } public void activate() { textField.textProperty().addListener(listener); applyFilteredListPredicate(textField.getText()); } public void deactivate() { textField.textProperty().removeListener(listener); } private void applyFilter(TableView tableView, Runnable callback) { tableView.getSelectionModel().clearSelection(); applyFilteredListPredicate(textField.getText()); if (callback != null) { callback.run(); } } private void applyFilteredListPredicate(String filterString) { filteredList.setPredicate(item -> item.match(filterString)); } public void setPromptText(String promptText) { textField.setPromptText(promptText); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/AchTransferForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.BankUtil; import haveno.core.locale.Country; import haveno.core.locale.Res; import haveno.core.payment.AchTransferAccount; import haveno.core.payment.CountryBasedPaymentAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.AchTransferAccountPayload; import haveno.core.payment.payload.BankAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import javafx.collections.FXCollections; import javafx.scene.control.ComboBox; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addComboBox; public class AchTransferForm extends GeneralUsBankForm { public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { AchTransferAccountPayload achTransferAccountPayload = (AchTransferAccountPayload) paymentAccountPayload; return addFormForBuyer(gridPane, gridRow, paymentAccountPayload, achTransferAccountPayload.getAccountType(), achTransferAccountPayload.getHolderAddress()); } private final AchTransferAccount achTransferAccount; public AchTransferForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.achTransferAccount = (AchTransferAccount) paymentAccount; } @Override public void addFormForEditAccount() { addFormForEditAccount(achTransferAccount.getPayload(), achTransferAccount.getPayload().getHolderAddress()); } @Override public void addFormForAddAccount() { addFormForAddAccountInternal(achTransferAccount.getPayload(), achTransferAccount.getPayload().getHolderAddress()); } @Override protected void setHolderAddress(String holderAddress) { achTransferAccount.getPayload().setHolderAddress(holderAddress); } @Override protected void maybeAddAccountTypeCombo(BankAccountPayload bankAccountPayload, Country country) { ComboBox accountTypeComboBox = addComboBox(gridPane, ++gridRow, Res.get("payment.select.account")); accountTypeComboBox.setItems(FXCollections.observableArrayList(BankUtil.getAccountTypeValues(country.code))); accountTypeComboBox.setOnAction(e -> { if (BankUtil.isAccountTypeRequired(country.code)) { bankAccountPayload.setAccountType(accountTypeComboBox.getSelectionModel().getSelectedItem()); updateFromInputs(); } }); } @Override public void updateAllInputsValid() { AchTransferAccountPayload achTransferAccountPayload = achTransferAccount.getPayload(); boolean result = isAccountNameValid() && paymentAccount.getSingleTradeCurrency() != null && ((CountryBasedPaymentAccount) this.paymentAccount).getCountry() != null && inputValidator.validate(achTransferAccountPayload.getHolderName()).isValid && inputValidator.validate(achTransferAccountPayload.getHolderAddress()).isValid; result = getValidationResult(result, achTransferAccountPayload.getCountryCode(), achTransferAccountPayload.getBankName(), achTransferAccountPayload.getBankId(), achTransferAccountPayload.getBranchId(), achTransferAccountPayload.getAccountNr(), achTransferAccountPayload.getAccountType(), achTransferAccountPayload.getHolderTaxId(), achTransferAccountPayload.getNationalAccountId()); allInputsValid.set(result); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/AdvancedCashForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.common.util.Tuple2; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.payment.AdvancedCashAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.AdvancedCashAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.validation.AdvancedCashValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import javafx.scene.control.Label; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addTopLabelFlowPane; @Deprecated public class AdvancedCashForm extends PaymentMethodForm { private final AdvancedCashAccount advancedCashAccount; private final AdvancedCashValidator advancedCashValidator; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.wallet"), ((AdvancedCashAccountPayload) paymentAccountPayload).getAccountNr()); return gridRow; } public AdvancedCashForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, AdvancedCashValidator advancedCashValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.advancedCashAccount = (AdvancedCashAccount) paymentAccount; this.advancedCashValidator = advancedCashValidator; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField accountNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.wallet")); accountNrInputTextField.setValidator(advancedCashValidator); accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { advancedCashAccount.setAccountNr(newValue); updateFromInputs(); }); addCurrenciesGrid(true); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } private void addCurrenciesGrid(boolean isEditable) { final Tuple2 labelFlowPaneTuple2 = addTopLabelFlowPane(gridPane, ++gridRow, Res.get("payment.supportedCurrencies"), 0); FlowPane flowPane = labelFlowPaneTuple2.second; if (isEditable) flowPane.setId("flow-pane-checkboxes-bg"); else flowPane.setId("flow-pane-checkboxes-non-editable-bg"); paymentAccount.getSupportedCurrencies().forEach(e -> fillUpFlowPaneWithCurrencies(isEditable, flowPane, e, advancedCashAccount)); } @Override protected void autoFillNameTextField() { setAccountNameWithString(advancedCashAccount.getAccountNr()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(advancedCashAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.wallet"), advancedCashAccount.getAccountNr()); addLimitations(true); addCurrenciesGrid(false); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && advancedCashValidator.validate(advancedCashAccount.getAccountNr()).isValid && advancedCashAccount.getTradeCurrencies().size() > 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/AliPayForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.payment.AliPayAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.AliPayAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.validation.AliPayValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import javafx.collections.FXCollections; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; public class AliPayForm extends GeneralAccountNumberForm { private final AliPayAccount aliPayAccount; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.no"), ((AliPayAccountPayload) paymentAccountPayload).getAccountNr()); return gridRow; } // TODO: AliPay validator is not used public AliPayForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, AliPayValidator aliPayValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.aliPayAccount = (AliPayAccount) paymentAccount; } @Override public void addTradeCurrency() { addTradeCurrencyComboBox(); currencyComboBox.setItems(FXCollections.observableArrayList(aliPayAccount.getSupportedCurrencies())); } @Override void setAccountNumber(String newValue) { aliPayAccount.setAccountNr(newValue); } @Override String getAccountNr() { return aliPayAccount.getAccountNr(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/AmazonGiftCardForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.AmazonGiftCardAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.AmazonGiftCardAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.Layout; import javafx.collections.FXCollections; import javafx.scene.control.ComboBox; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import javafx.util.StringConverter; import lombok.extern.slf4j.Slf4j; import java.util.HashMap; import static haveno.desktop.util.FormBuilder.addComboBox; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addInputTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextFieldWithCopyIcon; @Slf4j public class AmazonGiftCardForm extends PaymentMethodForm { ComboBox countryCombo; private final AmazonGiftCardAccount amazonGiftCardAccount; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { AmazonGiftCardAccountPayload amazonGiftCardAccountPayload = (AmazonGiftCardAccountPayload) paymentAccountPayload; addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.amazon.site"), countryToAmazonSite(amazonGiftCardAccountPayload.getCountryCode()), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email.mobile"), amazonGiftCardAccountPayload.getEmailOrMobileNr()); String countryText = CountryUtil.getNameAndCode(amazonGiftCardAccountPayload.getCountryCode()); if (countryText.isEmpty()) { countryText = Res.get("payment.ask"); } addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("shared.country"), countryText); return gridRow; } public AmazonGiftCardForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.amazonGiftCardAccount = (AmazonGiftCardAccount) paymentAccount; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField accountNrInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.email.mobile")); accountNrInputTextField.setValidator(inputValidator); accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { amazonGiftCardAccount.setEmailOrMobileNr(newValue); updateFromInputs(); }); countryCombo = addComboBox(gridPane, ++gridRow, Res.get("shared.country")); countryCombo.setPromptText(Res.get("payment.select.country")); countryCombo.setItems(FXCollections.observableArrayList(CountryUtil.getAllAmazonGiftCardCountries())); TextField ccyField = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), "").second; countryCombo.setConverter(new StringConverter<>() { @Override public String toString(Country country) { return country.name + " (" + country.code + ")"; } @Override public Country fromString(String s) { return null; } }); countryCombo.setOnAction(e -> { Country countryCode = countryCombo.getValue(); amazonGiftCardAccount.setCountry(countryCode); TradeCurrency currency = CurrencyUtil.getCurrencyByCountryCode(countryCode.code); paymentAccount.setSingleTradeCurrency(currency); ccyField.setText(currency.getNameAndCode()); updateFromInputs(); }); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(amazonGiftCardAccount.getEmailOrMobileNr() == null ? "" : amazonGiftCardAccount.getEmailOrMobileNr()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(paymentAccount.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email.mobile"), amazonGiftCardAccount.getEmailOrMobileNr()).second; field.setMouseTransparent(false); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"), amazonGiftCardAccount.getCountry() != null ? amazonGiftCardAccount.getCountry().name : ""); String nameAndCode = paymentAccount.getSingleTradeCurrency() != null ? paymentAccount.getSingleTradeCurrency().getNameAndCode() : ""; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && inputValidator.validate(amazonGiftCardAccount.getEmailOrMobileNr()).isValid && paymentAccount.getTradeCurrencies().size() > 0); } private static String countryToAmazonSite(String countryCode) { HashMap mapCountryToSite = new HashMap<>() {{ put("AU", "https://www.amazon.au"); put("CA", "https://www.amazon.ca"); put("FR", "https://www.amazon.fr"); put("DE", "https://www.amazon.de"); put("IT", "https://www.amazon.it"); put("NL", "https://www.amazon.nl"); put("ES", "https://www.amazon.es"); put("UK", "https://www.amazon.co.uk"); put("IN", "https://www.amazon.in"); put("JP", "https://www.amazon.co.jp"); put("SA", "https://www.amazon.sa"); put("SE", "https://www.amazon.se"); put("SG", "https://www.amazon.sg"); put("TR", "https://www.amazon.tr"); put("US", "https://www.amazon.com"); put("", Res.get("payment.ask")); }}; return mapCountryToSite.get(countryCode); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/AssetsForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.common.UserThread; import haveno.common.util.Tuple3; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.filter.FilterManager; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.AssetAccount; import haveno.core.payment.InstantCryptoCurrencyAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.AssetAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.validation.CryptoAddressValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.AutocompleteComboBox; import haveno.desktop.components.InputTextField; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.Layout; import javafx.geometry.Insets; import javafx.scene.control.CheckBox; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import javafx.scene.layout.VBox; import javafx.util.StringConverter; import static haveno.desktop.util.DisplayUtils.createAssetsAccountName; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addLabelCheckBox; import static haveno.desktop.util.GUIUtil.getComboBoxButtonCell; public class AssetsForm extends PaymentMethodForm { public static final String INSTANT_TRADE_NEWS = "instantTradeNews0.9.5"; private final AssetAccount assetAccount; private final CryptoAddressValidator altCoinAddressValidator; private final FilterManager filterManager; private InputTextField addressInputTextField; private CheckBox tradeInstantCheckBox; private boolean tradeInstant; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload, String labelTitle) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, labelTitle, ((AssetAccountPayload) paymentAccountPayload).getAddress()); return gridRow; } public AssetsForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, CryptoAddressValidator altCoinAddressValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter, FilterManager filterManager) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.assetAccount = (AssetAccount) paymentAccount; this.altCoinAddressValidator = altCoinAddressValidator; this.filterManager = filterManager; tradeInstant = paymentAccount instanceof InstantCryptoCurrencyAccount; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; addTradeCurrencyComboBox(); currencyComboBox.setPrefWidth(250); tradeInstantCheckBox = addLabelCheckBox(gridPane, ++gridRow, Res.get("payment.crypto.tradeInstantCheckbox"), 10); tradeInstantCheckBox.setSelected(tradeInstant); tradeInstantCheckBox.setOnAction(e -> { tradeInstant = tradeInstantCheckBox.isSelected(); if (tradeInstant) new Popup().information(Res.get("payment.crypto.tradeInstant.popup")).show(); paymentLimitationsTextField.setText(getLimitationsText()); }); gridPane.getChildren().remove(tradeInstantCheckBox); tradeInstantCheckBox.setPadding(new Insets(0, 40, 0, 0)); gridPane.getChildren().add(tradeInstantCheckBox); addressInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.crypto.address")); addressInputTextField.setValidator(altCoinAddressValidator); addressInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { if (newValue.startsWith("monero:")) { UserThread.execute(() -> { String addressWithoutPrefix = newValue.replace("monero:", ""); addressInputTextField.setText(addressWithoutPrefix); }); return; } assetAccount.setAddress(newValue); updateFromInputs(); }); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override public PaymentAccount getPaymentAccount() { if (tradeInstant) { InstantCryptoCurrencyAccount instantCryptoCurrencyAccount = new InstantCryptoCurrencyAccount(); instantCryptoCurrencyAccount.init(); instantCryptoCurrencyAccount.setAccountName(paymentAccount.getAccountName()); instantCryptoCurrencyAccount.setSaltAsHex(paymentAccount.getSaltAsHex()); instantCryptoCurrencyAccount.setSalt(paymentAccount.getSalt()); instantCryptoCurrencyAccount.setSingleTradeCurrency(paymentAccount.getSingleTradeCurrency()); instantCryptoCurrencyAccount.setSelectedTradeCurrency(paymentAccount.getSelectedTradeCurrency()); instantCryptoCurrencyAccount.setAddress(assetAccount.getAddress()); return instantCryptoCurrencyAccount; } else { return paymentAccount; } } @Override public void updateFromInputs() { if (addressInputTextField != null && assetAccount.getSingleTradeCurrency() != null) addressInputTextField.setPromptText(Res.get("payment.crypto.address.dyn", assetAccount.getSingleTradeCurrency().getName())); super.updateFromInputs(); } @Override protected void autoFillNameTextField() { if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) { accountNameTextField.setText(createAssetsAccountName(paymentAccount, assetAccount.getAddress())); } } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(assetAccount.getPaymentMethod().getId())); Tuple3 tuple2 = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.crypto.address"), assetAccount.getAddress()); TextField field = tuple2.second; field.setMouseTransparent(false); final TradeCurrency singleTradeCurrency = assetAccount.getSingleTradeCurrency(); final String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.crypto"), nameAndCode); addLimitations(true); } @Override public void updateAllInputsValid() { TradeCurrency selectedTradeCurrency = assetAccount.getSelectedTradeCurrency(); if (selectedTradeCurrency != null) { altCoinAddressValidator.setCurrencyCode(selectedTradeCurrency.getCode()); allInputsValid.set(isAccountNameValid() && altCoinAddressValidator.validate(assetAccount.getAddress()).isValid && assetAccount.getSingleTradeCurrency() != null); } } @Override protected void addTradeCurrencyComboBox() { currencyComboBox = FormBuilder.addLabelAutocompleteComboBox(gridPane, ++gridRow, Res.get("payment.crypto"), Layout.GROUP_DISTANCE).second; currencyComboBox.setPromptText(Res.get("payment.select.crypto")); currencyComboBox.setButtonCell(getComboBoxButtonCell(Res.get("payment.select.crypto"), currencyComboBox)); currencyComboBox.getEditor().focusedProperty().addListener(observable -> currencyComboBox.setPromptText("")); ((AutocompleteComboBox) currencyComboBox).setAutocompleteItems( CurrencyUtil.getActiveSortedCryptoCurrencies(filterManager)); currencyComboBox.setVisibleRowCount(Math.min(currencyComboBox.getItems().size(), 10)); currencyComboBox.setCellFactory(GUIUtil.getTradeCurrencyCellFactoryNameAndCode()); currencyComboBox.setConverter(new StringConverter<>() { @Override public String toString(TradeCurrency tradeCurrency) { return tradeCurrency != null ? tradeCurrency.getNameAndCode() : ""; } @Override public TradeCurrency fromString(String s) { return currencyComboBox.getItems().stream(). filter(item -> item.getNameAndCode().equals(s)). findAny().orElse(null); } }); ((AutocompleteComboBox) currencyComboBox).setOnChangeConfirmed(e -> { addressInputTextField.resetValidation(); addressInputTextField.validate(); TradeCurrency tradeCurrency = currencyComboBox.getSelectionModel().getSelectedItem(); paymentAccount.setSingleTradeCurrency(tradeCurrency); updateFromInputs(); if (tradeCurrency != null && tradeCurrency.getCode().equals("BSQ")) { new Popup().information(Res.get("payment.select.crypto.bsq.warning")).show(); } }); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/AustraliaPayidForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.AustraliaPayidAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.AustraliaPayidAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.validation.AustraliaPayidValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextArea; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextArea; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; import com.jfoenix.controls.JFXTextArea; public class AustraliaPayidForm extends PaymentMethodForm { private final AustraliaPayidAccount australiaPayidAccount; private final AustraliaPayidValidator australiaPayidValidator; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), ((AustraliaPayidAccountPayload) paymentAccountPayload).getBankAccountName()); addCompactTopLabelTextField(gridPane, gridRow, 1, Res.get("payment.payid"), ((AustraliaPayidAccountPayload) paymentAccountPayload).getPayid()); AustraliaPayidAccountPayload payId = (AustraliaPayidAccountPayload) paymentAccountPayload; TextArea textExtraInfo = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), "").second; textExtraInfo.setMinHeight(70); textExtraInfo.setEditable(false); textExtraInfo.setText(payId.getExtraInfo()); return gridRow; } public AustraliaPayidForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, AustraliaPayidValidator australiaPayidValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.australiaPayidAccount = (AustraliaPayidAccount) paymentAccount; this.australiaPayidValidator = australiaPayidValidator; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname")); holderNameInputTextField.setValidator(inputValidator); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { australiaPayidAccount.setBankAccountName(newValue); updateFromInputs(); }); InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.payid")); mobileNrInputTextField.setValidator(australiaPayidValidator); mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { australiaPayidAccount.setPayid(newValue); updateFromInputs(); }); TextArea extraTextArea = addTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt.paymentAccount")).second; extraTextArea.setMinHeight(70); ((JFXTextArea) extraTextArea).setLabelFloat(false); extraTextArea.textProperty().addListener((ov, oldValue, newValue) -> { australiaPayidAccount.setExtraInfo(newValue); updateFromInputs(); }); TradeCurrency singleTradeCurrency = australiaPayidAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(australiaPayidAccount.getPayid()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(australiaPayidAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.payid"), australiaPayidAccount.getPayid()); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), australiaPayidAccount.getBankAccountName()).second; field.setMouseTransparent(false); TextArea textAreaExtra = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), "").second; textAreaExtra.setText(australiaPayidAccount.getExtraInfo()); textAreaExtra.setMinHeight(70); textAreaExtra.setEditable(false); TradeCurrency singleTradeCurrency = australiaPayidAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && australiaPayidValidator.validate(australiaPayidAccount.getPayid()).isValid && inputValidator.validate(australiaPayidAccount.getBankAccountName()).isValid && australiaPayidAccount.getTradeCurrencies().size() > 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/BankForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.common.util.Tuple2; import haveno.common.util.Tuple4; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.BankUtil; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.CountryBasedPaymentAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.BankAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.GUIUtil; import javafx.collections.FXCollections; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addComboBox; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addInputTextField; import static haveno.desktop.util.FormBuilder.addInputTextFieldInputTextField; abstract class BankForm extends GeneralBankForm { static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { BankAccountPayload data = (BankAccountPayload) paymentAccountPayload; String countryCode = ((BankAccountPayload) paymentAccountPayload).getCountryCode(); int colIndex = 0; if (data.getHolderTaxId() != null) { final String title = Res.get("payment.account.owner.fullname") + " / " + BankUtil.getHolderIdLabelShort(countryCode); final String value = data.getHolderName() + " / " + data.getHolderTaxId(); addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), title, value); } else { final String title = Res.get("payment.account.owner.fullname"); final String value = data.getHolderName(); addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), title, value); } addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), Res.get("payment.bank.country"), CountryUtil.getNameAndCode(countryCode)); // We don't want to display more than 6 rows to avoid scrolling, so if we get too many fields we combine them horizontally int nrRows = 0; if (BankUtil.isBankNameRequired(countryCode)) nrRows++; if (BankUtil.isBankIdRequired(countryCode)) nrRows++; if (BankUtil.isBranchIdRequired(countryCode)) nrRows++; if (BankUtil.isAccountNrRequired(countryCode)) nrRows++; if (BankUtil.isAccountTypeRequired(countryCode)) nrRows++; if (BankUtil.isNationalAccountIdRequired(countryCode)) nrRows++; String bankNameLabel = BankUtil.getBankNameLabel(countryCode); String bankIdLabel = BankUtil.getBankIdLabel(countryCode); String branchIdLabel = BankUtil.getBranchIdLabel(countryCode); String nationalAccountIdLabel = BankUtil.getNationalAccountIdLabel(countryCode); String accountNrLabel = BankUtil.getAccountNrLabel(countryCode); String accountTypeLabel = BankUtil.getAccountTypeLabel(countryCode); accountNrAccountTypeCombined = false; nationalAccountIdAccountNrCombined = false; bankNameBankIdCombined = false; bankIdBranchIdCombined = false; bankNameBranchIdCombined = false; branchIdAccountNrCombined = false; prepareFormLayoutFlags(countryCode, nrRows); if (bankNameBankIdCombined) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), bankNameLabel + " / " + bankIdLabel + ":", data.getBankName() + " / " + data.getBankId(), true); } if (bankNameBranchIdCombined) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), bankNameLabel + " / " + branchIdLabel + ":", data.getBankName() + " / " + data.getBranchId(), true); } if (!bankNameBankIdCombined && !bankNameBranchIdCombined && BankUtil.isBankNameRequired(countryCode)) addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), bankNameLabel, data.getBankName()); if (!bankNameBankIdCombined && !bankNameBranchIdCombined && !branchIdAccountNrCombined && bankIdBranchIdCombined) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), bankIdLabel + " / " + branchIdLabel + ":", data.getBankId() + " / " + data.getBranchId()); } if (!bankNameBankIdCombined && !bankIdBranchIdCombined && BankUtil.isBankIdRequired(countryCode)) addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), bankIdLabel, data.getBankId()); if (!bankNameBranchIdCombined && !bankIdBranchIdCombined && branchIdAccountNrCombined) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), branchIdLabel + " / " + accountNrLabel + ":", data.getBranchId() + " / " + data.getAccountNr()); } if (!bankNameBranchIdCombined && !bankIdBranchIdCombined && !branchIdAccountNrCombined && BankUtil.isBranchIdRequired(countryCode)) addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), branchIdLabel, data.getBranchId()); if (!branchIdAccountNrCombined && accountNrAccountTypeCombined) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), accountNrLabel + " / " + accountTypeLabel, data.getAccountNr() + " / " + data.getAccountType()); } if (!branchIdAccountNrCombined && !accountNrAccountTypeCombined && !nationalAccountIdAccountNrCombined && BankUtil.isAccountNrRequired(countryCode)) addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), accountNrLabel, data.getAccountNr()); if (!accountNrAccountTypeCombined && BankUtil.isAccountTypeRequired(countryCode)) addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), accountTypeLabel, data.getAccountType()); if (!branchIdAccountNrCombined && !accountNrAccountTypeCombined && nationalAccountIdAccountNrCombined) addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), nationalAccountIdLabel + " / " + accountNrLabel, data.getNationalAccountId() + " / " + data.getAccountNr()); return gridRow; } private final BankAccountPayload bankAccountPayload; private InputTextField holderNameInputTextField; private ComboBox accountTypeComboBox; private Country selectedCountry; BankForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.bankAccountPayload = (BankAccountPayload) paymentAccount.paymentAccountPayload; } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; String countryCode = bankAccountPayload.getCountryCode(); addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(paymentAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"), getCountryBasedPaymentAccount().getCountry() != null ? getCountryBasedPaymentAccount().getCountry().name : ""); TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addAcceptedBanksForDisplayAccount(); addHolderNameAndIdForDisplayAccount(); if (BankUtil.isBankNameRequired(countryCode)) addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.bank.name"), bankAccountPayload.getBankName()).second.setMouseTransparent(false); if (BankUtil.isBankIdRequired(countryCode)) addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getBankIdLabel(countryCode), bankAccountPayload.getBankId()).second.setMouseTransparent(false); if (BankUtil.isBranchIdRequired(countryCode)) addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getBranchIdLabel(countryCode), bankAccountPayload.getBranchId()).second.setMouseTransparent(false); if (BankUtil.isNationalAccountIdRequired(countryCode)) addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getNationalAccountIdLabel(countryCode), bankAccountPayload.getNationalAccountId()).second.setMouseTransparent(false); if (BankUtil.isAccountNrRequired(countryCode)) addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getAccountNrLabel(countryCode), bankAccountPayload.getAccountNr()).second.setMouseTransparent(false); if (BankUtil.isAccountTypeRequired(countryCode)) addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getAccountTypeLabel(countryCode), bankAccountPayload.getAccountType()).second.setMouseTransparent(false); addLimitations(true); } @Override public void addFormForAddAccount() { accountNrInputTextFieldEdited = false; gridRowFrom = gridRow + 1; Tuple2, Integer> tuple = GUIUtil.addRegionCountryTradeCurrencyComboBoxes(gridPane, gridRow, this::onCountrySelected, this::onTradeCurrencySelected); currencyComboBox = tuple.first; gridRow = tuple.second; addAcceptedBanksForAddAccount(); addHolderNameAndId(); nationalAccountIdInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getNationalAccountIdLabel("")); nationalAccountIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { bankAccountPayload.setNationalAccountId(newValue); updateFromInputs(); }); bankNameInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.bank.name")); bankNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { bankAccountPayload.setBankName(newValue.trim()); updateFromInputs(); }); bankIdInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getBankIdLabel("")); bankIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { bankAccountPayload.setBankId(newValue.trim()); updateFromInputs(); }); branchIdInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getBranchIdLabel("")); branchIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { bankAccountPayload.setBranchId(newValue.trim()); updateFromInputs(); }); accountNrInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getAccountNrLabel("")); accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { bankAccountPayload.setAccountNr(newValue.trim()); updateFromInputs(); }); accountTypeComboBox = addComboBox(gridPane, ++gridRow, ""); accountTypeComboBox.setPromptText(Res.get("payment.select.account")); accountTypeComboBox.setOnAction(e -> { if (BankUtil.isAccountTypeRequired(bankAccountPayload.getCountryCode())) { bankAccountPayload.setAccountType(accountTypeComboBox.getSelectionModel().getSelectedItem()); updateFromInputs(); } }); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); updateFromInputs(); } private void onCountrySelected(Country country) { selectedCountry = country; if (country != null) { getCountryBasedPaymentAccount().setCountry(country); String countryCode = country.code; TradeCurrency currency = CurrencyUtil.getCurrencyByCountryCode(countryCode); paymentAccount.setSingleTradeCurrency(currency); currencyComboBox.setDisable(false); currencyComboBox.getSelectionModel().select(currency); bankIdInputTextField.setPromptText(BankUtil.getBankIdLabel(countryCode)); branchIdInputTextField.setPromptText(BankUtil.getBranchIdLabel(countryCode)); nationalAccountIdInputTextField.setPromptText(BankUtil.getNationalAccountIdLabel(countryCode)); accountNrInputTextField.setPromptText(BankUtil.getAccountNrLabel(countryCode)); accountTypeComboBox.setPromptText(BankUtil.getAccountTypeLabel(countryCode)); bankNameInputTextField.setText(""); bankIdInputTextField.setText(""); branchIdInputTextField.setText(""); nationalAccountIdInputTextField.setText(""); accountNrInputTextField.setText(""); accountNrInputTextField.focusedProperty().addListener((observable, oldValue, newValue) -> { if (newValue) accountNrInputTextFieldEdited = true; }); accountTypeComboBox.getSelectionModel().clearSelection(); accountTypeComboBox.setItems(FXCollections.observableArrayList(BankUtil.getAccountTypeValues(countryCode))); validateInput(countryCode); holderNameInputTextField.resetValidation(); holderNameInputTextField.validate(); bankNameInputTextField.resetValidation(); bankNameInputTextField.validate(); bankIdInputTextField.resetValidation(); bankIdInputTextField.validate(); branchIdInputTextField.resetValidation(); branchIdInputTextField.validate(); accountNrInputTextField.resetValidation(); accountNrInputTextField.validate(); nationalAccountIdInputTextField.resetValidation(); nationalAccountIdInputTextField.validate(); boolean requiresHolderId = BankUtil.isHolderIdRequired(countryCode); if (requiresHolderId) { holderNameInputTextField.minWidthProperty().unbind(); holderNameInputTextField.setMinWidth(250); } else { holderNameInputTextField.minWidthProperty().bind(currencyComboBox.widthProperty()); } updateHolderIDInput(countryCode, requiresHolderId); boolean nationalAccountIdRequired = BankUtil.isNationalAccountIdRequired(countryCode); nationalAccountIdInputTextField.setVisible(nationalAccountIdRequired); nationalAccountIdInputTextField.setManaged(nationalAccountIdRequired); boolean bankNameRequired = BankUtil.isBankNameRequired(countryCode); bankNameInputTextField.setVisible(bankNameRequired); bankNameInputTextField.setManaged(bankNameRequired); boolean bankIdRequired = BankUtil.isBankIdRequired(countryCode); bankIdInputTextField.setVisible(bankIdRequired); bankIdInputTextField.setManaged(bankIdRequired); boolean branchIdRequired = BankUtil.isBranchIdRequired(countryCode); branchIdInputTextField.setVisible(branchIdRequired); branchIdInputTextField.setManaged(branchIdRequired); boolean accountNrRequired = BankUtil.isAccountNrRequired(countryCode); accountNrInputTextField.setVisible(accountNrRequired); accountNrInputTextField.setManaged(accountNrRequired); boolean accountTypeRequired = BankUtil.isAccountTypeRequired(countryCode); accountTypeComboBox.setVisible(accountTypeRequired); accountTypeComboBox.setManaged(accountTypeRequired); updateFromInputs(); onCountryChanged(); } } private void onTradeCurrencySelected(TradeCurrency tradeCurrency) { TraditionalCurrency defaultCurrency = CurrencyUtil.getCurrencyByCountryCode(selectedCountry.code); applyTradeCurrency(tradeCurrency, defaultCurrency); } private CountryBasedPaymentAccount getCountryBasedPaymentAccount() { return (CountryBasedPaymentAccount) this.paymentAccount; } protected void onCountryChanged() { } private void addHolderNameAndId() { Tuple2 tuple = addInputTextFieldInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), BankUtil.getHolderIdLabel("")); holderNameInputTextField = tuple.first; holderNameInputTextField.setMinWidth(250); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { bankAccountPayload.setHolderName(newValue.trim()); updateFromInputs(); }); holderNameInputTextField.minWidthProperty().bind(currencyComboBox.widthProperty()); holderNameInputTextField.setValidator(inputValidator); useHolderID = true; holderIdInputTextField = tuple.second; holderIdInputTextField.setVisible(false); holderIdInputTextField.setManaged(false); holderIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { bankAccountPayload.setHolderTaxId(newValue); updateFromInputs(); }); } @Override public void updateAllInputsValid() { boolean result = isAccountNameValid() && paymentAccount.getSingleTradeCurrency() != null && getCountryBasedPaymentAccount().getCountry() != null && inputValidator.validate(bankAccountPayload.getHolderName()).isValid; String countryCode = bankAccountPayload.getCountryCode(); result = getValidationResult(result, countryCode, bankAccountPayload.getBankName(), bankAccountPayload.getBankId(), bankAccountPayload.getBranchId(), bankAccountPayload.getAccountNr(), bankAccountPayload.getAccountType(), bankAccountPayload.getHolderTaxId(), bankAccountPayload.getNationalAccountId()); allInputsValid.set(result); } private void addHolderNameAndIdForDisplayAccount() { String countryCode = bankAccountPayload.getCountryCode(); if (BankUtil.isHolderIdRequired(countryCode)) { Tuple4 tuple = addCompactTopLabelTextFieldTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), BankUtil.getHolderIdLabel(countryCode)); TextField holderNameTextField = tuple.second; holderNameTextField.setText(bankAccountPayload.getHolderName()); holderNameTextField.setMinWidth(250); tuple.fourth.setText(bankAccountPayload.getHolderTaxId()); } else { addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), bankAccountPayload.getHolderName()); } } protected void addAcceptedBanksForAddAccount() { } public void addAcceptedBanksForDisplayAccount() { } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/BizumForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import haveno.core.payment.BizumAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.BizumAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.Layout; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextFieldWithCopyIcon; public class BizumForm extends PaymentMethodForm { private final BizumAccount account; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.mobile"), ((BizumAccountPayload) paymentAccountPayload).getMobileNr(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); return gridRow; } public BizumForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.account = (BizumAccount) paymentAccount; } @Override public void addFormForAddAccount() { // this payment method is only for Spain/EUR account.setSingleTradeCurrency(account.getSupportedCurrencies().get(0)); CountryUtil.findCountryByCode("ES").ifPresent(c -> account.setCountry(c)); gridRowFrom = gridRow + 1; InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.mobile")); mobileNrInputTextField.setValidator(inputValidator); mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setMobileNr(newValue.trim()); updateFromInputs(); }); addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(account.getMobileNr()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.mobile"), account.getMobileNr()).second; field.setMouseTransparent(false); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && inputValidator.validate(account.getMobileNr()).isValid); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/CapitualForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.common.util.Tuple2; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.payment.CapitualAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.CapitualAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.validation.CapitualValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import javafx.scene.control.Label; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addTopLabelFlowPane; public class CapitualForm extends PaymentMethodForm { private final CapitualAccount capitualAccount; private final CapitualValidator capitualValidator; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.capitual.cap"), ((CapitualAccountPayload) paymentAccountPayload).getAccountNr()); return gridRow; } public CapitualForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, CapitualValidator capitualValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.capitualAccount = (CapitualAccount) paymentAccount; this.capitualValidator = capitualValidator; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField accountNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.capitual.cap")); accountNrInputTextField.setValidator(capitualValidator); accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { capitualAccount.setAccountNr(newValue); updateFromInputs(); }); addCurrenciesGrid(true); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } private void addCurrenciesGrid(boolean isEditable) { final Tuple2 labelFlowPaneTuple2 = addTopLabelFlowPane(gridPane, ++gridRow, Res.get("payment.supportedCurrencies"), 0); FlowPane flowPane = labelFlowPaneTuple2.second; if (isEditable) flowPane.setId("flow-pane-checkboxes-bg"); else flowPane.setId("flow-pane-checkboxes-non-editable-bg"); paymentAccount.getSupportedCurrencies().forEach(e -> fillUpFlowPaneWithCurrencies(isEditable, flowPane, e, capitualAccount)); } @Override protected void autoFillNameTextField() { setAccountNameWithString(capitualAccount.getAccountNr()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(capitualAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.capitual.cap"), capitualAccount.getAccountNr()); addLimitations(true); addCurrenciesGrid(false); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && capitualValidator.validate(capitualAccount.getAccountNr()).isValid && !capitualAccount.getTradeCurrencies().isEmpty()); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.desktop.components.paymentmethods; import com.jfoenix.controls.JFXTextArea; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.payment.CashAppAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.CashAppAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.validation.EmailOrMobileNrOrCashtagValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.Layout; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addTopLabelFlowPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextArea; import static haveno.desktop.util.FormBuilder.addTopLabelTextArea; public class CashAppForm extends PaymentMethodForm { private final CashAppAccount cashAppAccount; private final EmailOrMobileNrOrCashtagValidator cashAppValidator; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email.mobile.cashtag"), ((CashAppAccountPayload) paymentAccountPayload).getEmailOrMobileNrOrCashtag()); CashAppAccountPayload payId = (CashAppAccountPayload) paymentAccountPayload; TextArea textExtraInfo = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), "").second; textExtraInfo.setMinHeight(70); textExtraInfo.setEditable(false); textExtraInfo.setText(payId.getExtraInfo()); return gridRow; } public CashAppForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, EmailOrMobileNrOrCashtagValidator cashAppValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.cashAppAccount = (CashAppAccount) paymentAccount; this.cashAppValidator = cashAppValidator; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email.mobile.cashtag")); mobileNrInputTextField.setValidator(cashAppValidator); mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { cashAppAccount.setEmailOrMobileNrOrCashtag(newValue.trim()); updateFromInputs(); }); TextArea extraTextArea = addTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt.paymentAccount")).second; extraTextArea.setMinHeight(70); ((JFXTextArea) extraTextArea).setLabelFloat(false); extraTextArea.textProperty().addListener((ov, oldValue, newValue) -> { cashAppAccount.setExtraInfo(newValue); updateFromInputs(); }); addCurrenciesGrid(true); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } private void addCurrenciesGrid(boolean isEditable) { FlowPane flowPane = addTopLabelFlowPane(gridPane, ++gridRow, Res.get("payment.supportedCurrencies"), Layout.FLOATING_LABEL_DISTANCE * 3, Layout.FLOATING_LABEL_DISTANCE * 3).second; if (isEditable) flowPane.setId("flow-pane-checkboxes-bg"); else flowPane.setId("flow-pane-checkboxes-non-editable-bg"); cashAppAccount.getSupportedCurrencies().forEach(e -> fillUpFlowPaneWithCurrencies(isEditable, flowPane, e, cashAppAccount)); } @Override protected void autoFillNameTextField() { setAccountNameWithString(cashAppAccount.getEmailOrMobileNrOrCashtag()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(cashAppAccount.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email.mobile.cashtag"), cashAppAccount.getEmailOrMobileNrOrCashtag()).second; TextArea textAreaExtra = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), "").second; textAreaExtra.setText(cashAppAccount.getExtraInfo()); textAreaExtra.setMinHeight(70); textAreaExtra.setEditable(false); field.setMouseTransparent(false); addLimitations(true); addCurrenciesGrid(false); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && cashAppValidator.validate(cashAppAccount.getEmailOrMobileNrOrCashtag()).isValid && cashAppAccount.getTradeCurrencies().size() > 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAtAtmForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import com.jfoenix.controls.JFXTextArea; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.CashAtAtmAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.CashAtAtmAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import javafx.collections.FXCollections; import javafx.scene.control.TextArea; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextArea; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextArea; public class CashAtAtmForm extends PaymentMethodForm { private final CashAtAtmAccount cashAtAtmAccount; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { CashAtAtmAccountPayload cashAtAtmPayload = (CashAtAtmAccountPayload) paymentAccountPayload; TextArea textExtraInfo = addCompactTopLabelTextArea(gridPane, ++gridRow, 0, Res.get("payment.shared.extraInfo"), "").second; textExtraInfo.setMinHeight(70); textExtraInfo.setEditable(false); textExtraInfo.setText(cashAtAtmPayload.getExtraInfo()); return gridRow; } public CashAtAtmForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.cashAtAtmAccount = (CashAtAtmAccount) paymentAccount; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; addTradeCurrencyComboBox(); currencyComboBox.setItems(FXCollections.observableArrayList(CurrencyUtil.getAllSortedFiatCurrencies())); TextArea extraTextArea = addTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.optionalExtra"), Res.get("payment.cashAtAtm.extraInfo.prompt")).second; extraTextArea.setMinHeight(70); ((JFXTextArea) extraTextArea).setLabelFloat(false); extraTextArea.textProperty().addListener((ov, oldValue, newValue) -> { cashAtAtmAccount.setExtraInfo(newValue); updateFromInputs(); }); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { if (cashAtAtmAccount.getExtraInfo() != null && !cashAtAtmAccount.getExtraInfo().isEmpty()) { setAccountNameWithString(cashAtAtmAccount.getExtraInfo().substring(0, Math.min(50, cashAtAtmAccount.getExtraInfo().length()))); } else { setAccountNameWithString(cashAtAtmAccount.getSelectedTradeCurrency().getCode()); } } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(cashAtAtmAccount.getPaymentMethod().getId())); TradeCurrency tradeCurrency = paymentAccount.getSingleTradeCurrency(); String nameAndCode = tradeCurrency != null ? tradeCurrency.getNameAndCode() : ""; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); TextArea textAreaExtra = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), "").second; textAreaExtra.setText(cashAtAtmAccount.getExtraInfo()); textAreaExtra.setMinHeight(70); textAreaExtra.setEditable(false); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && paymentAccount.getSingleTradeCurrency() != null); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/CashDepositForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.common.util.Tuple2; import haveno.common.util.Tuple4; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.BankUtil; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.CountryBasedPaymentAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.CashDepositAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.validation.EmailValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.GUIUtil; import javafx.collections.FXCollections; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addComboBox; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextArea; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addInputTextField; import static haveno.desktop.util.FormBuilder.addInputTextFieldInputTextField; import static haveno.desktop.util.FormBuilder.addTextArea; public class CashDepositForm extends GeneralBankForm { public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { CashDepositAccountPayload data = (CashDepositAccountPayload) paymentAccountPayload; String countryCode = data.getCountryCode(); String requirements = data.getRequirements(); boolean showRequirements = requirements != null && !requirements.isEmpty(); int colIndex = 0; if (data.getHolderTaxId() != null) addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), Res.get("payment.account.name.emailAndHolderId", BankUtil.getHolderIdLabel(countryCode)), data.getHolderName() + " / " + data.getHolderEmail() + " / " + data.getHolderTaxId()); else addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), Res.get("payment.account.name.email"), data.getHolderName() + " / " + data.getHolderEmail()); if (!showRequirements) addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), Res.getWithCol("payment.bank.country"), CountryUtil.getNameAndCode(countryCode)); else requirements += "\n" + Res.get("payment.bank.country") + " " + CountryUtil.getNameAndCode(countryCode); // We don't want to display more than 6 rows to avoid scrolling, so if we get too many fields we combine them horizontally int nrRows = 0; if (BankUtil.isBankNameRequired(countryCode)) nrRows++; if (BankUtil.isBankIdRequired(countryCode)) nrRows++; if (BankUtil.isBranchIdRequired(countryCode)) nrRows++; if (BankUtil.isAccountNrRequired(countryCode)) nrRows++; if (BankUtil.isAccountTypeRequired(countryCode)) nrRows++; if (BankUtil.isNationalAccountIdRequired(countryCode)) nrRows++; String bankNameLabel = BankUtil.getBankNameLabel(countryCode); String bankIdLabel = BankUtil.getBankIdLabel(countryCode); String branchIdLabel = BankUtil.getBranchIdLabel(countryCode); String nationalAccountIdLabel = BankUtil.getNationalAccountIdLabel(countryCode); String accountNrLabel = BankUtil.getAccountNrLabel(countryCode); String accountTypeLabel = BankUtil.getAccountTypeLabel(countryCode); accountNrAccountTypeCombined = false; nationalAccountIdAccountNrCombined = false; bankNameBankIdCombined = false; bankIdBranchIdCombined = false; bankNameBranchIdCombined = false; branchIdAccountNrCombined = false; prepareFormLayoutFlags(countryCode, nrRows); if (bankNameBankIdCombined) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), bankNameLabel + " / " + bankIdLabel, data.getBankName() + " / " + data.getBankId()); } if (bankNameBranchIdCombined) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), bankNameLabel + " / " + branchIdLabel, data.getBankName() + " / " + data.getBranchId()); } if (!bankNameBankIdCombined && !bankNameBranchIdCombined && BankUtil.isBankNameRequired(countryCode)) addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), bankNameLabel, data.getBankName()); if (!bankNameBankIdCombined && !bankNameBranchIdCombined && !branchIdAccountNrCombined && bankIdBranchIdCombined) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), bankIdLabel + " / " + branchIdLabel, data.getBankId() + " / " + data.getBranchId()); } if (!bankNameBankIdCombined && !bankIdBranchIdCombined && BankUtil.isBankIdRequired(countryCode)) addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), bankIdLabel, data.getBankId()); if (!bankNameBranchIdCombined && !bankIdBranchIdCombined && branchIdAccountNrCombined) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), branchIdLabel + " / " + accountNrLabel, data.getBranchId() + " / " + data.getAccountNr()); } if (!bankNameBranchIdCombined && !bankIdBranchIdCombined && !branchIdAccountNrCombined && BankUtil.isBranchIdRequired(countryCode)) addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), branchIdLabel, data.getBranchId()); if (!branchIdAccountNrCombined && accountNrAccountTypeCombined) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), accountNrLabel + " / " + accountTypeLabel, data.getAccountNr() + " / " + data.getAccountType()); } if (!branchIdAccountNrCombined && !accountNrAccountTypeCombined && !nationalAccountIdAccountNrCombined && BankUtil.isAccountNrRequired(countryCode)) addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), accountNrLabel, data.getAccountNr()); if (!accountNrAccountTypeCombined && BankUtil.isAccountTypeRequired(countryCode)) addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), accountTypeLabel, data.getAccountType()); if (!branchIdAccountNrCombined && !accountNrAccountTypeCombined && nationalAccountIdAccountNrCombined) addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), nationalAccountIdLabel + " / " + accountNrLabel, data.getNationalAccountId() + " / " + data.getAccountNr()); if (showRequirements) { TextArea textArea = addCompactTopLabelTextArea(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), Res.get("payment.extras"), "").second; textArea.setMinHeight(45); textArea.setMaxHeight(45); textArea.setEditable(false); textArea.setId("text-area-disabled"); textArea.setText(requirements); } return gridRow; } private final CashDepositAccountPayload cashDepositAccountPayload; private InputTextField holderNameInputTextField, emailInputTextField; private ComboBox accountTypeComboBox; private final EmailValidator emailValidator; private Country selectedCountry; public CashDepositForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.cashDepositAccountPayload = (CashDepositAccountPayload) paymentAccount.paymentAccountPayload; emailValidator = new EmailValidator(); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; String countryCode = cashDepositAccountPayload.getCountryCode(); addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(paymentAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"), getCountryBasedPaymentAccount().getCountry() != null ? getCountryBasedPaymentAccount().getCountry().name : ""); TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addHolderNameAndIdForDisplayAccount(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), cashDepositAccountPayload.getHolderEmail()); if (BankUtil.isBankNameRequired(countryCode)) addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.bank.name"), cashDepositAccountPayload.getBankName()).second.setMouseTransparent(false); if (BankUtil.isBankIdRequired(countryCode)) addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getBankIdLabel(countryCode), cashDepositAccountPayload.getBankId()).second.setMouseTransparent(false); if (BankUtil.isBranchIdRequired(countryCode)) addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getBranchIdLabel(countryCode), cashDepositAccountPayload.getBranchId()).second.setMouseTransparent(false); if (BankUtil.isNationalAccountIdRequired(countryCode)) addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getNationalAccountIdLabel(countryCode), cashDepositAccountPayload.getNationalAccountId()).second.setMouseTransparent(false); if (BankUtil.isAccountNrRequired(countryCode)) addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getAccountNrLabel(countryCode), cashDepositAccountPayload.getAccountNr()).second.setMouseTransparent(false); if (BankUtil.isAccountTypeRequired(countryCode)) addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getAccountTypeLabel(countryCode), cashDepositAccountPayload.getAccountType()).second.setMouseTransparent(false); String requirements = cashDepositAccountPayload.getRequirements(); boolean showRequirements = requirements != null && !requirements.isEmpty(); if (showRequirements) { TextArea textArea = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.extras"), "").second; textArea.setMinHeight(30); textArea.setMaxHeight(30); textArea.setEditable(false); textArea.setId("text-area-disabled"); textArea.setText(requirements); } addLimitations(true); } @Override public void addFormForAddAccount() { accountNrInputTextFieldEdited = false; gridRowFrom = gridRow + 1; Tuple2, Integer> tuple = GUIUtil.addRegionCountryTradeCurrencyComboBoxes(gridPane, gridRow, this::onCountrySelected, this::onTradeCurrencySelected); currencyComboBox = tuple.first; gridRow = tuple.second; addHolderNameAndId(); nationalAccountIdInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getNationalAccountIdLabel("")); nationalAccountIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { cashDepositAccountPayload.setNationalAccountId(newValue); updateFromInputs(); }); bankNameInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.bank.name")); bankNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { cashDepositAccountPayload.setBankName(newValue); updateFromInputs(); }); bankIdInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getBankIdLabel("")); bankIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { cashDepositAccountPayload.setBankId(newValue); updateFromInputs(); }); branchIdInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getBranchIdLabel("")); branchIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { cashDepositAccountPayload.setBranchId(newValue); updateFromInputs(); }); accountNrInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getAccountNrLabel("")); accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { cashDepositAccountPayload.setAccountNr(newValue); updateFromInputs(); }); accountTypeComboBox = addComboBox(gridPane, ++gridRow, Res.get("payment.select.account")); accountTypeComboBox.setOnAction(e -> { if (BankUtil.isAccountTypeRequired(cashDepositAccountPayload.getCountryCode())) { cashDepositAccountPayload.setAccountType(accountTypeComboBox.getSelectionModel().getSelectedItem()); updateFromInputs(); } }); TextArea requirementsTextArea = addTextArea(gridPane, ++gridRow, Res.get("payment.extras")); requirementsTextArea.setMinHeight(30); requirementsTextArea.setMaxHeight(90); requirementsTextArea.textProperty().addListener((ov, oldValue, newValue) -> { cashDepositAccountPayload.setRequirements(newValue); updateFromInputs(); }); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); updateFromInputs(); } private void onTradeCurrencySelected(TradeCurrency tradeCurrency) { TraditionalCurrency defaultCurrency = CurrencyUtil.getCurrencyByCountryCode(selectedCountry.code); applyTradeCurrency(tradeCurrency, defaultCurrency); } private void onCountrySelected(Country country) { selectedCountry = country; if (selectedCountry != null) { getCountryBasedPaymentAccount().setCountry(selectedCountry); String countryCode = selectedCountry.code; TradeCurrency currency = CurrencyUtil.getCurrencyByCountryCode(countryCode); paymentAccount.setSingleTradeCurrency(currency); currencyComboBox.setDisable(false); currencyComboBox.getSelectionModel().select(currency); bankIdInputTextField.setPromptText(BankUtil.getBankIdLabel(countryCode)); branchIdInputTextField.setPromptText(BankUtil.getBranchIdLabel(countryCode)); nationalAccountIdInputTextField.setPromptText(BankUtil.getNationalAccountIdLabel(countryCode)); accountNrInputTextField.setPromptText(BankUtil.getAccountNrLabel(countryCode)); accountTypeComboBox.setPromptText(BankUtil.getAccountTypeLabel(countryCode)); bankNameInputTextField.setText(""); bankIdInputTextField.setText(""); branchIdInputTextField.setText(""); nationalAccountIdInputTextField.setText(""); accountNrInputTextField.setText(""); accountNrInputTextField.focusedProperty().addListener((observable, oldValue, newValue) -> { if (newValue) accountNrInputTextFieldEdited = true; }); accountTypeComboBox.getSelectionModel().clearSelection(); accountTypeComboBox.setItems(FXCollections.observableArrayList(BankUtil.getAccountTypeValues(countryCode))); validateInput(countryCode); holderNameInputTextField.resetValidation(); emailInputTextField.resetValidation(); bankNameInputTextField.resetValidation(); bankIdInputTextField.resetValidation(); branchIdInputTextField.resetValidation(); accountNrInputTextField.resetValidation(); nationalAccountIdInputTextField.resetValidation(); holderNameInputTextField.validate(); emailInputTextField.validate(); bankNameInputTextField.validate(); bankIdInputTextField.validate(); branchIdInputTextField.validate(); accountNrInputTextField.validate(); nationalAccountIdInputTextField.validate(); boolean requiresHolderId = BankUtil.isHolderIdRequired(countryCode); if (requiresHolderId) { holderNameInputTextField.minWidthProperty().unbind(); holderNameInputTextField.setMinWidth(300); } else { holderNameInputTextField.minWidthProperty().bind(currencyComboBox.widthProperty()); } updateHolderIDInput(countryCode, requiresHolderId); boolean nationalAccountIdRequired = BankUtil.isNationalAccountIdRequired(countryCode); nationalAccountIdInputTextField.setVisible(nationalAccountIdRequired); nationalAccountIdInputTextField.setManaged(nationalAccountIdRequired); boolean bankNameRequired = BankUtil.isBankNameRequired(countryCode); bankNameInputTextField.setVisible(bankNameRequired); bankNameInputTextField.setManaged(bankNameRequired); boolean bankIdRequired = BankUtil.isBankIdRequired(countryCode); bankIdInputTextField.setVisible(bankIdRequired); bankIdInputTextField.setManaged(bankIdRequired); boolean branchIdRequired = BankUtil.isBranchIdRequired(countryCode); branchIdInputTextField.setVisible(branchIdRequired); branchIdInputTextField.setManaged(branchIdRequired); boolean accountNrRequired = BankUtil.isAccountNrRequired(countryCode); accountNrInputTextField.setVisible(accountNrRequired); accountNrInputTextField.setManaged(accountNrRequired); boolean accountTypeRequired = BankUtil.isAccountTypeRequired(countryCode); accountTypeComboBox.setVisible(accountTypeRequired); accountTypeComboBox.setManaged(accountTypeRequired); updateFromInputs(); onCountryChanged(); } } private CountryBasedPaymentAccount getCountryBasedPaymentAccount() { return (CountryBasedPaymentAccount) this.paymentAccount; } private void onCountryChanged() { } private void addHolderNameAndId() { Tuple2 tuple = addInputTextFieldInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), BankUtil.getHolderIdLabel("")); holderNameInputTextField = tuple.first; holderNameInputTextField.setMinWidth(300); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { cashDepositAccountPayload.setHolderName(newValue); updateFromInputs(); }); holderNameInputTextField.minWidthProperty().bind(currencyComboBox.widthProperty()); holderNameInputTextField.setValidator(inputValidator); emailInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.email")); emailInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { cashDepositAccountPayload.setHolderEmail(newValue); updateFromInputs(); }); emailInputTextField.minWidthProperty().bind(currencyComboBox.widthProperty()); emailInputTextField.setValidator(emailValidator); useHolderID = true; holderIdInputTextField = tuple.second; holderIdInputTextField.setMinWidth(250); holderIdInputTextField.setVisible(false); holderIdInputTextField.setManaged(false); holderIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { cashDepositAccountPayload.setHolderTaxId(newValue); updateFromInputs(); }); } @Override public void updateAllInputsValid() { boolean result = isAccountNameValid() && paymentAccount.getSingleTradeCurrency() != null && getCountryBasedPaymentAccount().getCountry() != null && inputValidator.validate(cashDepositAccountPayload.getHolderName()).isValid && emailValidator.validate(cashDepositAccountPayload.getHolderEmail()).isValid; String countryCode = cashDepositAccountPayload.getCountryCode(); result = getValidationResult(result, countryCode, cashDepositAccountPayload.getBankName(), cashDepositAccountPayload.getBankId(), cashDepositAccountPayload.getBranchId(), cashDepositAccountPayload.getAccountNr(), cashDepositAccountPayload.getAccountType(), cashDepositAccountPayload.getHolderTaxId(), cashDepositAccountPayload.getNationalAccountId()); allInputsValid.set(result); } private void addHolderNameAndIdForDisplayAccount() { String countryCode = cashDepositAccountPayload.getCountryCode(); if (BankUtil.isHolderIdRequired(countryCode)) { Tuple4 tuple = addCompactTopLabelTextFieldTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), BankUtil.getHolderIdLabel(countryCode)); TextField holderNameTextField = tuple.second; holderNameTextField.setText(cashDepositAccountPayload.getHolderName()); holderNameTextField.setMinWidth(300); tuple.fourth.setText(cashDepositAccountPayload.getHolderTaxId()); } else { addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), cashDepositAccountPayload.getHolderName()); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/CelPayForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.payment.CelPayAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.CelPayAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.validation.EmailValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import javafx.scene.control.TextField; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; public class CelPayForm extends PaymentMethodForm { private final CelPayAccount account; private final EmailValidator validator = new EmailValidator(); public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email"), ((CelPayAccountPayload) paymentAccountPayload).getEmail()); return gridRow; } public CelPayForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.account = (CelPayAccount) paymentAccount; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField emailInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email")); emailInputTextField.setValidator(validator); emailInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setEmail(newValue.trim()); updateFromInputs(); }); addCurrenciesGrid(true); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } private void addCurrenciesGrid(boolean isEditable) { FlowPane flowPane = FormBuilder.addTopLabelFlowPane(gridPane, ++gridRow, Res.get("payment.celpay.supportedCurrenciesForReceiver"), 20, 20).second; if (isEditable) { flowPane.setId("flow-pane-checkboxes-bg"); } else { flowPane.setId("flow-pane-checkboxes-non-editable-bg"); } account.getSupportedCurrencies().forEach(currency -> fillUpFlowPaneWithCurrencies(isEditable, flowPane, currency, account)); } @Override protected void autoFillNameTextField() { setAccountNameWithString(account.getEmail()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), account.getEmail()).second; field.setMouseTransparent(false); addLimitations(true); addCurrenciesGrid(false); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && account.getEmail() != null && validator.validate(account.getEmail()).isValid && account.getTradeCurrencies().size() > 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/ChaseQuickPayForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.ChaseQuickPayAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.ChaseQuickPayAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.validation.ChaseQuickPayValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; public class ChaseQuickPayForm extends PaymentMethodForm { private final ChaseQuickPayAccount chaseQuickPayAccount; private final ChaseQuickPayValidator chaseQuickPayValidator; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), ((ChaseQuickPayAccountPayload) paymentAccountPayload).getHolderName()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), ((ChaseQuickPayAccountPayload) paymentAccountPayload).getEmail()); return gridRow; } public ChaseQuickPayForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, ChaseQuickPayValidator chaseQuickPayValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.chaseQuickPayAccount = (ChaseQuickPayAccount) paymentAccount; this.chaseQuickPayValidator = chaseQuickPayValidator; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname")); holderNameInputTextField.setValidator(inputValidator); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { chaseQuickPayAccount.setHolderName(newValue); updateFromInputs(); }); InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email")); mobileNrInputTextField.setValidator(chaseQuickPayValidator); mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { chaseQuickPayAccount.setEmail(newValue); updateFromInputs(); }); TradeCurrency singleTradeCurrency = chaseQuickPayAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(chaseQuickPayAccount.getEmail()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(chaseQuickPayAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), chaseQuickPayAccount.getHolderName()); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), chaseQuickPayAccount.getEmail()).second; field.setMouseTransparent(false); TradeCurrency singleTradeCurrency = chaseQuickPayAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && chaseQuickPayValidator.validate(chaseQuickPayAccount.getEmail()).isValid && inputValidator.validate(chaseQuickPayAccount.getHolderName()).isValid && chaseQuickPayAccount.getTradeCurrencies().size() > 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/DomesticWireTransferForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Country; import haveno.core.payment.CountryBasedPaymentAccount; import haveno.core.payment.DomesticWireTransferAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.BankAccountPayload; import haveno.core.payment.payload.DomesticWireTransferAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import javafx.scene.layout.GridPane; public class DomesticWireTransferForm extends GeneralUsBankForm { public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { DomesticWireTransferAccountPayload domesticWireTransferAccountPayload = (DomesticWireTransferAccountPayload) paymentAccountPayload; return addFormForBuyer(gridPane, gridRow, paymentAccountPayload, null, domesticWireTransferAccountPayload.getHolderAddress()); } private final DomesticWireTransferAccount domesticWireTransferAccount; public DomesticWireTransferForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.domesticWireTransferAccount = (DomesticWireTransferAccount) paymentAccount; } @Override public void addFormForEditAccount() { addFormForEditAccount(domesticWireTransferAccount.getPayload(), domesticWireTransferAccount.getPayload().getHolderAddress()); } @Override public void addFormForAddAccount() { addFormForAddAccountInternal(domesticWireTransferAccount.getPayload(), domesticWireTransferAccount.getPayload().getHolderAddress()); } @Override protected void setHolderAddress(String holderAddress) { domesticWireTransferAccount.getPayload().setHolderAddress(holderAddress); } @Override protected void maybeAddAccountTypeCombo(BankAccountPayload bankAccountPayload, Country country) { // DomesticWireTransfer does not use the account type combo } @Override public void updateAllInputsValid() { DomesticWireTransferAccountPayload domesticWireTransferAccountPayload = domesticWireTransferAccount.getPayload(); boolean result = isAccountNameValid() && paymentAccount.getSingleTradeCurrency() != null && ((CountryBasedPaymentAccount) this.paymentAccount).getCountry() != null && inputValidator.validate(domesticWireTransferAccountPayload.getHolderName()).isValid && inputValidator.validate(domesticWireTransferAccountPayload.getHolderAddress()).isValid; result = getValidationResult(result, domesticWireTransferAccountPayload.getCountryCode(), domesticWireTransferAccountPayload.getBankName(), domesticWireTransferAccountPayload.getBankId(), domesticWireTransferAccountPayload.getBranchId(), domesticWireTransferAccountPayload.getAccountNr(), domesticWireTransferAccountPayload.getAccountNr(), domesticWireTransferAccountPayload.getHolderTaxId(), domesticWireTransferAccountPayload.getNationalAccountId()); allInputsValid.set(result); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/F2FForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import com.jfoenix.controls.JFXTextArea; import haveno.common.util.Tuple2; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.offer.Offer; import haveno.core.payment.CountryBasedPaymentAccount; import haveno.core.payment.F2FAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.F2FAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.validation.F2FValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.GUIUtil; import javafx.scene.control.ComboBox; import javafx.scene.control.TextArea; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextArea; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addInputTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextArea; public class F2FForm extends PaymentMethodForm { private final F2FAccount f2fAccount; private final F2FValidator f2fValidator; private Country selectedCountry; public static int addStep2Form(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload, Offer offer, double top, boolean isBuyer) { F2FAccountPayload f2fAccountPayload = (F2FAccountPayload) paymentAccountPayload; addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, 0, Res.get("shared.country"), CountryUtil.getNameAndCode(f2fAccountPayload.getCountryCode()), top); addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.f2f.city"), offer.getF2FCity(), top); addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.f2f.contact"), isBuyer ? f2fAccountPayload.getContact() : Res.get("shared.na")); TextArea textArea = addTopLabelTextArea(gridPane, gridRow, 1, Res.get("payment.shared.extraInfo"), "").second; textArea.setMinHeight(70); textArea.setEditable(false); textArea.setId("text-area-disabled"); textArea.setText(offer.getPaymentAccountExtraInfo()); return gridRow; } public F2FForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, F2FValidator f2fValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.f2fAccount = (F2FAccount) paymentAccount; this.f2fValidator = f2fValidator; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; Tuple2, Integer> tuple = GUIUtil.addRegionCountryTradeCurrencyComboBoxes(gridPane, gridRow, this::onCountrySelected, this::onTradeCurrencySelected); currencyComboBox = tuple.first; gridRow = tuple.second; InputTextField contactInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.f2f.contact")); contactInputTextField.setPromptText(Res.get("payment.f2f.contact.prompt")); contactInputTextField.setValidator(f2fValidator); contactInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { f2fAccount.setContact(newValue); updateFromInputs(); }); InputTextField cityInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.f2f.city")); cityInputTextField.setPromptText(Res.get("payment.f2f.city.prompt")); cityInputTextField.setValidator(f2fValidator); cityInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { f2fAccount.setCity(newValue); updateFromInputs(); }); TextArea extraTextArea = addTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt.paymentAccount")).second; extraTextArea.setMinHeight(70); ((JFXTextArea) extraTextArea).setLabelFloat(false); //extraTextArea.setValidator(f2fValidator); extraTextArea.textProperty().addListener((ov, oldValue, newValue) -> { f2fAccount.setExtraInfo(newValue); updateFromInputs(); }); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } private void onCountrySelected(Country country) { selectedCountry = country; if (selectedCountry != null) { getCountryBasedPaymentAccount().setCountry(selectedCountry); String countryCode = selectedCountry.code; TradeCurrency currency = CurrencyUtil.getCurrencyByCountryCode(countryCode); paymentAccount.setSingleTradeCurrency(currency); currencyComboBox.setDisable(false); currencyComboBox.getSelectionModel().select(currency); updateFromInputs(); } } private void onTradeCurrencySelected(TradeCurrency tradeCurrency) { TraditionalCurrency defaultCurrency = CurrencyUtil.getCurrencyByCountryCode(selectedCountry.code); applyTradeCurrency(tradeCurrency, defaultCurrency); } @Override protected void autoFillNameTextField() { setAccountNameWithString(f2fAccount.getCity()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(paymentAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"), getCountryBasedPaymentAccount().getCountry() != null ? getCountryBasedPaymentAccount().getCountry().name : ""); TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.f2f.contact", f2fAccount.getContact()), f2fAccount.getContact()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.f2f.city", f2fAccount.getCity()), f2fAccount.getCity()); TextArea textArea = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), "").second; textArea.setText(f2fAccount.getExtraInfo()); textArea.setMinHeight(70); textArea.setEditable(false); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && f2fValidator.validate(f2fAccount.getContact()).isValid && f2fValidator.validate(f2fAccount.getCity()).isValid && f2fAccount.getTradeCurrencies().size() > 0); } private CountryBasedPaymentAccount getCountryBasedPaymentAccount() { return (CountryBasedPaymentAccount) this.paymentAccount; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/FasterPaymentsForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.FasterPaymentsAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.FasterPaymentsAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.validation.AccountNrValidator; import haveno.core.payment.validation.BranchIdValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; public class FasterPaymentsForm extends PaymentMethodForm { private static final String UK_SORT_CODE = Res.get("payment.fasterPayments.ukSortCode"); public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { if (!((FasterPaymentsAccountPayload) paymentAccountPayload).getHolderName().isEmpty()) { addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), ((FasterPaymentsAccountPayload) paymentAccountPayload).getHolderName()); } // do not translate as it is used in English only addCompactTopLabelTextField(gridPane, ++gridRow, UK_SORT_CODE, ((FasterPaymentsAccountPayload) paymentAccountPayload).getSortCode()); addCompactTopLabelTextField(gridPane, gridRow, 1, Res.get("payment.accountNr"), ((FasterPaymentsAccountPayload) paymentAccountPayload).getAccountNr()); return gridRow; } private final FasterPaymentsAccount fasterPaymentsAccount; private InputTextField holderNameInputTextField; private InputTextField accountNrInputTextField; private InputTextField sortCodeInputTextField; private final BranchIdValidator branchIdValidator; private final AccountNrValidator accountNrValidator; public FasterPaymentsForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.fasterPaymentsAccount = (FasterPaymentsAccount) paymentAccount; this.branchIdValidator = new BranchIdValidator("GB"); this.accountNrValidator = new AccountNrValidator("GB"); } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname")); holderNameInputTextField.setValidator(inputValidator); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { fasterPaymentsAccount.setHolderName(newValue); updateFromInputs(); }); // do not translate as it is used in English only sortCodeInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, UK_SORT_CODE); sortCodeInputTextField.setValidator(inputValidator); sortCodeInputTextField.setValidator(branchIdValidator); sortCodeInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { fasterPaymentsAccount.setSortCode(newValue); updateFromInputs(); }); accountNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.accountNr")); accountNrInputTextField.setValidator(accountNrValidator); accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { fasterPaymentsAccount.setAccountNr(newValue); updateFromInputs(); }); TradeCurrency singleTradeCurrency = fasterPaymentsAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(fasterPaymentsAccount.getAccountNr()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(fasterPaymentsAccount.getPaymentMethod().getId())); if (!fasterPaymentsAccount.getHolderName().isEmpty()) { addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), fasterPaymentsAccount.getHolderName()); } // do not translate as it is used in English only addCompactTopLabelTextField(gridPane, ++gridRow, UK_SORT_CODE, fasterPaymentsAccount.getSortCode()); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.accountNr"), fasterPaymentsAccount.getAccountNr()).second; field.setMouseTransparent(false); TradeCurrency singleTradeCurrency = fasterPaymentsAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && inputValidator.validate(fasterPaymentsAccount.getHolderName()).isValid && branchIdValidator.validate(fasterPaymentsAccount.getSortCode()).isValid && accountNrValidator.validate(fasterPaymentsAccount.getAccountNr()).isValid && fasterPaymentsAccount.getTradeCurrencies().size() > 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/GeneralAccountNumberForm.java ================================================ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.PaymentAccount; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addInputTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; public abstract class GeneralAccountNumberForm extends PaymentMethodForm { GeneralAccountNumberForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField accountNrInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.account.no")); accountNrInputTextField.setValidator(inputValidator); accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { setAccountNumber(newValue); updateFromInputs(); }); addTradeCurrency(); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } public void addTradeCurrency() { final TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency(); final String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); } @Override protected void autoFillNameTextField() { setAccountNameWithString(getAccountNr()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(paymentAccount.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.no"), getAccountNr()).second; field.setMouseTransparent(false); final String nameAndCode = paymentAccount.getSingleTradeCurrency() != null ? paymentAccount.getSingleTradeCurrency().getNameAndCode() : ""; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && inputValidator.validate(getAccountNr()).isValid && paymentAccount.getTradeCurrencies().size() > 0); } abstract void setAccountNumber(String newValue); abstract String getAccountNr(); } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/GeneralBankForm.java ================================================ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.BankUtil; import haveno.core.locale.Res; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.BankAccountPayload; import haveno.core.payment.validation.AccountNrValidator; import haveno.core.payment.validation.BankIdValidator; import haveno.core.payment.validation.BranchIdValidator; import haveno.core.payment.validation.NationalAccountIdValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import javafx.scene.layout.GridPane; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public abstract class GeneralBankForm extends PaymentMethodForm { private static final Logger log = LoggerFactory.getLogger(GeneralBankForm.class); static boolean accountNrAccountTypeCombined = false; static boolean nationalAccountIdAccountNrCombined = false; static boolean bankNameBankIdCombined = false; static boolean bankIdBranchIdCombined = false; static boolean bankNameBranchIdCombined = false; static boolean branchIdAccountNrCombined = false; boolean validatorsApplied; boolean useHolderID; InputTextField bankNameInputTextField, bankIdInputTextField, branchIdInputTextField, accountNrInputTextField, holderIdInputTextField, nationalAccountIdInputTextField; boolean accountNrInputTextFieldEdited; public GeneralBankForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); } static int getIndexOfColumn(int colIndex) { return colIndex % 2; } static void prepareFormLayoutFlags(String countryCode, int currentNumberOfRows) { int nrRows = currentNumberOfRows; if (nrRows > 2) { // Try combine AccountNr + AccountType accountNrAccountTypeCombined = BankUtil.isAccountNrRequired(countryCode) && BankUtil.isAccountTypeRequired(countryCode); if (accountNrAccountTypeCombined) nrRows--; if (nrRows > 2) { nationalAccountIdAccountNrCombined = BankUtil.isAccountNrRequired(countryCode) && BankUtil.isNationalAccountIdRequired(countryCode); if (nationalAccountIdAccountNrCombined) nrRows--; if (nrRows > 2) { // Next we try BankName + BankId bankNameBankIdCombined = BankUtil.isBankNameRequired(countryCode) && BankUtil.isBankIdRequired(countryCode); if (bankNameBankIdCombined) nrRows--; if (nrRows > 2) { // Next we try BankId + BranchId bankIdBranchIdCombined = !bankNameBankIdCombined && BankUtil.isBankIdRequired(countryCode) && BankUtil.isBranchIdRequired(countryCode); if (bankIdBranchIdCombined) nrRows--; if (nrRows > 2) { // Next we try BankId + BranchId bankNameBranchIdCombined = !bankNameBankIdCombined && !bankIdBranchIdCombined && BankUtil.isBankNameRequired(countryCode) && BankUtil.isBranchIdRequired(countryCode); if (bankNameBranchIdCombined) nrRows--; if (nrRows > 2) { branchIdAccountNrCombined = !bankNameBranchIdCombined && !bankIdBranchIdCombined && !accountNrAccountTypeCombined && BankUtil.isBranchIdRequired(countryCode) && BankUtil.isAccountNrRequired(countryCode); if (branchIdAccountNrCombined) nrRows--; if (nrRows > 2) log.warn("We still have too many rows...."); } } } } } } } void validateInput(String countryCode) { if (BankUtil.useValidation(countryCode)) { validatorsApplied = true; if (useHolderID) holderIdInputTextField.setValidator(inputValidator); bankNameInputTextField.setValidator(inputValidator); bankIdInputTextField.setValidator(new BankIdValidator(countryCode)); branchIdInputTextField.setValidator(new BranchIdValidator(countryCode)); accountNrInputTextField.setValidator(new AccountNrValidator(countryCode)); nationalAccountIdInputTextField.setValidator(new NationalAccountIdValidator(countryCode)); } else { validatorsApplied = false; if (useHolderID) holderIdInputTextField.setValidator(null); bankNameInputTextField.setValidator(null); bankIdInputTextField.setValidator(null); branchIdInputTextField.setValidator(null); accountNrInputTextField.setValidator(inputValidator); nationalAccountIdInputTextField.setValidator(null); } } void updateHolderIDInput(String countryCode, boolean requiresHolderId) { if (useHolderID) { if (!requiresHolderId) holderIdInputTextField.setText(""); holderIdInputTextField.resetValidation(); holderIdInputTextField.setVisible(requiresHolderId); holderIdInputTextField.setManaged(requiresHolderId); holderIdInputTextField.setPromptText(BankUtil.getHolderIdLabel(countryCode)); } } @Override protected void autoFillNameTextField() { if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) { BankAccountPayload payload = (BankAccountPayload) paymentAccount.paymentAccountPayload; String bankId = null; String countryCode = payload.getCountryCode(); if (countryCode == null) countryCode = ""; if (BankUtil.isBankIdRequired(countryCode)) { bankId = payload.getBankId(); if (bankId.length() > 9) bankId = StringUtils.abbreviate(bankId, 9); } else if (BankUtil.isBranchIdRequired(countryCode)) { bankId = payload.getBranchId(); if (bankId.length() > 9) bankId = StringUtils.abbreviate(bankId, 9); } else if (BankUtil.isBankNameRequired(countryCode)) { bankId = payload.getBankName(); if (bankId.length() > 9) bankId = StringUtils.abbreviate(bankId, 9); } String accountNr = payload.getAccountNr(); if (accountNr.length() > 9) accountNr = StringUtils.abbreviate(accountNr, 9); String method = Res.get(paymentAccount.getPaymentMethod().getId()); if (bankId != null && !bankId.isEmpty()) accountNameTextField.setText(method.concat(": ").concat(bankId).concat(", ").concat(accountNr)); else accountNameTextField.setText(method.concat(": ").concat(accountNr)); if (BankUtil.isNationalAccountIdRequired(countryCode)) { String nationalAccountId = nationalAccountIdInputTextField.getText(); if (countryCode.equals("AR") && nationalAccountId.length() == 22 && !accountNrInputTextFieldEdited) { branchIdInputTextField.setText(nationalAccountId.substring(3, 7)); accountNrInputTextField.setText(nationalAccountId.substring(8, 21)); } } } } boolean getValidationResult(boolean result, String countryCode, String bankName, String bankId, String branchId, String accountNr, String accountType, String holderTaxId, String nationalAccountId) { if (validatorsApplied && BankUtil.useValidation(countryCode)) { if (BankUtil.isBankNameRequired(countryCode)) result = result && bankNameInputTextField.getValidator().validate(bankName).isValid; if (BankUtil.isBankIdRequired(countryCode)) result = result && bankIdInputTextField.getValidator().validate(bankId).isValid; if (BankUtil.isBranchIdRequired(countryCode)) result = result && branchIdInputTextField.getValidator().validate(branchId).isValid; if (BankUtil.isAccountNrRequired(countryCode)) result = result && accountNrInputTextField.getValidator().validate(accountNr).isValid; if (BankUtil.isAccountTypeRequired(countryCode)) result = result && accountType != null; if (useHolderID && BankUtil.isHolderIdRequired(countryCode)) result = result && holderIdInputTextField.getValidator().validate(holderTaxId).isValid; if (BankUtil.isNationalAccountIdRequired(countryCode)) result = result && nationalAccountIdInputTextField.getValidator().validate(nationalAccountId).isValid; } else { // only account number not empty validation result = result && (accountNrInputTextField == null || accountNrInputTextField.getValidator().validate(accountNr).isValid); } return result; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/GeneralSepaForm.java ================================================ package haveno.desktop.components.paymentmethods; import com.jfoenix.controls.JFXComboBox; import com.jfoenix.controls.JFXTextField; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Country; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.CountryBasedPaymentAccount; import haveno.core.payment.PaymentAccount; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.AutoTooltipCheckBox; import haveno.desktop.util.FormBuilder; import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.util.StringConverter; import org.apache.commons.lang3.StringUtils; import java.util.List; import java.util.Objects; import static haveno.desktop.util.FormBuilder.addTopLabelWithVBox; public abstract class GeneralSepaForm extends PaymentMethodForm { static final String BIC = "BIC"; static final String IBAN = "IBAN"; GeneralSepaForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); } @Override protected void autoFillNameTextField() { if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) { TradeCurrency singleTradeCurrency = this.paymentAccount.getSingleTradeCurrency(); String currency = singleTradeCurrency != null ? singleTradeCurrency.getCode() : null; if (currency != null) { String iban = getIban(); if (iban.length() > 9) iban = StringUtils.abbreviate(iban, 9); String method = Res.get(paymentAccount.getPaymentMethod().getId()); CountryBasedPaymentAccount countryBasedPaymentAccount = (CountryBasedPaymentAccount) this.paymentAccount; String country = countryBasedPaymentAccount.getCountry() != null ? countryBasedPaymentAccount.getCountry().code : null; if (country != null) accountNameTextField.setText(method.concat(" (").concat(currency).concat("/").concat(country) .concat("): ").concat(iban)); } } } void setCountryComboBoxAction(ComboBox countryComboBox, CountryBasedPaymentAccount paymentAccount) { countryComboBox.setOnAction(e -> { Country selectedItem = countryComboBox.getSelectionModel().getSelectedItem(); paymentAccount.setCountry(selectedItem); updateFromInputs(); }); } void addCountriesGrid(String title, List countries) { FlowPane flowPane = FormBuilder.addTopLabelFlowPane(gridPane, ++gridRow, title, 0).second; flowPane.setId("flow-pane-checkboxes-bg"); countries.forEach(country -> { CheckBox checkBox = new AutoTooltipCheckBox(country.code); checkBox.setUserData(country.code); checkBox.setSelected(isCountryAccepted(country.code)); checkBox.setMouseTransparent(false); checkBox.setMinWidth(45); checkBox.setMaxWidth(45); checkBox.setTooltip(new Tooltip(country.name)); checkBox.setOnAction(event -> { if (checkBox.isSelected()) { addAcceptedCountry(country.code); } else { removeAcceptedCountry(country.code); } updateAllInputsValid(); }); flowPane.getChildren().add(checkBox); }); } ComboBox addCountrySelection() { HBox hBox = new HBox(); hBox.setSpacing(10); ComboBox countryComboBox = new JFXComboBox<>(); TextField currencyTextField = new JFXTextField(""); currencyTextField.setEditable(false); currencyTextField.setMouseTransparent(true); currencyTextField.setFocusTraversable(false); currencyTextField.setMinWidth(300); currencyTextField.setVisible(true); currencyTextField.setManaged(true); currencyTextField.setText(Res.get("payment.currencyWithSymbol", Objects.requireNonNull(paymentAccount.getSingleTradeCurrency()).getNameAndCode())); hBox.getChildren().addAll(countryComboBox, currencyTextField); addTopLabelWithVBox(gridPane, ++gridRow, Res.get("payment.bank.country"), hBox, 0); countryComboBox.setPromptText(Res.get("payment.select.bank.country")); countryComboBox.setConverter(new StringConverter<>() { @Override public String toString(Country country) { return country.name + " (" + country.code + ")"; } @Override public Country fromString(String s) { return null; } }); return countryComboBox; } abstract boolean isCountryAccepted(String countryCode); protected abstract String getIban(); } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/GeneralUsBankForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.BankUtil; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.CountryBasedPaymentAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.BankAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.Layout; import javafx.scene.control.TextArea; import javafx.scene.layout.GridPane; import javax.annotation.Nullable; import static haveno.common.util.Utilities.cleanString; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextArea; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addInputTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextArea; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextFieldWithCopyIcon; public abstract class GeneralUsBankForm extends GeneralBankForm { protected static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload, @Nullable String accountType, String holderAddress) { BankAccountPayload bankAccountPayload = (BankAccountPayload) paymentAccountPayload; String countryCode = bankAccountPayload.getCountryCode(); int colIndex = 1; addTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), Res.get("payment.account.owner.fullname"), bankAccountPayload.getHolderName(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); String branchIdLabel = BankUtil.getBranchIdLabel(countryCode); String accountNrLabel = BankUtil.getAccountNrLabel(countryCode); addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), branchIdLabel + " / " + accountNrLabel, bankAccountPayload.getBranchId() + " / " + bankAccountPayload.getAccountNr()); String bankNameLabel = BankUtil.getBankNameLabel(countryCode); String accountTypeLabel = accountType == null ? "" : " / " + BankUtil.getAccountTypeLabel(countryCode); addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), bankNameLabel + accountTypeLabel, accountType == null ? bankAccountPayload.getBankName() : bankAccountPayload.getBankName() + " / " + accountType); if (holderAddress.length() > 0) { TextArea textAddress = addCompactTopLabelTextArea(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), Res.get("payment.account.address"), "").second; textAddress.setMinHeight(70); textAddress.setEditable(false); textAddress.setText(holderAddress); } return gridRow; } public GeneralUsBankForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); } protected void addFormForEditAccount(BankAccountPayload bankAccountPayload, String holderAddress) { Country country = ((CountryBasedPaymentAccount) paymentAccount).getCountry(); addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(paymentAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), bankAccountPayload.getHolderName()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.address"), cleanString(holderAddress)); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.bank.name"), bankAccountPayload.getBankName()); addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getBranchIdLabel(country.code), bankAccountPayload.getBranchId()); addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getAccountNrLabel(country.code), bankAccountPayload.getAccountNr()); if (bankAccountPayload.getAccountType() != null) { addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getAccountTypeLabel(country.code), bankAccountPayload.getAccountType()); } addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), paymentAccount.getSingleTradeCurrency().getNameAndCode()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), country.name); addLimitations(true); } protected void addFormForAddAccountInternal(BankAccountPayload bankAccountPayload, String holderAddress) { // this payment method is only for United States/USD CountryUtil.findCountryByCode("US").ifPresent(c -> onCountrySelected(c)); Country country = ((CountryBasedPaymentAccount) paymentAccount).getCountry(); InputTextField holderNameInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname")); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { bankAccountPayload.setHolderName(newValue); updateFromInputs(); }); holderNameInputTextField.setValidator(inputValidator); TextArea addressTextArea = addTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.account.owner.address"), Res.get("payment.account.owner.address")).second; addressTextArea.setMinHeight(70); addressTextArea.textProperty().addListener((ov, oldValue, newValue) -> { setHolderAddress(newValue.trim()); updateFromInputs(); }); bankNameInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.bank.name")); bankNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { bankAccountPayload.setBankName(newValue); updateFromInputs(); }); bankNameInputTextField.setValidator(inputValidator); branchIdInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getBranchIdLabel(country.code)); branchIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { bankAccountPayload.setBranchId(newValue); updateFromInputs(); }); branchIdInputTextField.setValidator(inputValidator); accountNrInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getAccountNrLabel(country.code)); accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { bankAccountPayload.setAccountNr(newValue); updateFromInputs(); }); accountNrInputTextField.setValidator(inputValidator); maybeAddAccountTypeCombo(bankAccountPayload, country); addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), paymentAccount.getSingleTradeCurrency().getNameAndCode()); addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), country.name); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); this.validatorsApplied = true; } abstract protected void setHolderAddress(String holderAddress); abstract protected void maybeAddAccountTypeCombo(BankAccountPayload bankAccountPayload, Country country); protected void onCountrySelected(Country country) { if (country != null) { ((CountryBasedPaymentAccount) this.paymentAccount).setCountry(country); String countryCode = country.code; TradeCurrency currency = CurrencyUtil.getCurrencyByCountryCode(countryCode); paymentAccount.setSingleTradeCurrency(currency); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/HalCashForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.HalCashAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.HalCashAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.validation.HalCashValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; public class HalCashForm extends PaymentMethodForm { private final HalCashAccount halCashAccount; private final HalCashValidator halCashValidator; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.mobile"), ((HalCashAccountPayload) paymentAccountPayload).getMobileNr()); return gridRow; } public HalCashForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, HalCashValidator halCashValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.halCashAccount = (HalCashAccount) paymentAccount; this.halCashValidator = halCashValidator; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.mobile")); mobileNrInputTextField.setValidator(halCashValidator); mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { halCashAccount.setMobileNr(newValue); updateFromInputs(); }); TradeCurrency singleTradeCurrency = halCashAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(halCashAccount.getMobileNr()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(halCashAccount.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.mobile"), halCashAccount.getMobileNr()).second; field.setMouseTransparent(false); TradeCurrency singleTradeCurrency = halCashAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && halCashValidator.validate(halCashAccount.getMobileNr()).isValid && halCashAccount.getTradeCurrencies().size() > 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/IfscBankForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.IfscBasedAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.core.util.validation.RegexValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.Layout; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextFieldWithCopyIcon; public class IfscBankForm extends PaymentMethodForm { private final IfscBasedAccountPayload ifscBasedAccountPayload; private final RegexValidator ifscValidator; // https://en.wikipedia.org/wiki/Indian_Financial_System_Code public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { IfscBasedAccountPayload ifscAccountPayload = (IfscBasedAccountPayload) paymentAccountPayload; addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.account.owner.fullname"), ifscAccountPayload.getHolderName(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.accountNr"), ifscAccountPayload.getAccountNr()); addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.ifsc"), ifscAccountPayload.getIfsc()); return gridRow; } public IfscBankForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.ifscBasedAccountPayload = (IfscBasedAccountPayload) paymentAccount.paymentAccountPayload; ifscValidator = new RegexValidator(); ifscValidator.setPattern("[A-Z]{4}0[0-9]{6}"); ifscValidator.setErrorMessage(Res.get("payment.ifsc.validation")); } @Override public void addFormForAddAccount() { // this payment method is only for India/INR paymentAccount.setSingleTradeCurrency(paymentAccount.getSupportedCurrencies().get(0)); CountryUtil.findCountryByCode("IN").ifPresent(c -> ifscBasedAccountPayload.setCountryCode(c.code)); gridRowFrom = gridRow + 1; InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname")); holderNameInputTextField.setValidator(inputValidator); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { ifscBasedAccountPayload.setHolderName(newValue.trim()); updateFromInputs(); }); InputTextField accountNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.accountNr")); accountNrInputTextField.setValidator(inputValidator); accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { ifscBasedAccountPayload.setAccountNr(newValue.trim()); updateFromInputs(); }); InputTextField ifscInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.ifsc")); ifscInputTextField.setText("XXXX0999999"); ifscInputTextField.setValidator(ifscValidator); ifscInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { ifscBasedAccountPayload.setIfsc(newValue.trim()); updateFromInputs(); }); addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), paymentAccount.getSingleTradeCurrency().getNameAndCode()); addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), CountryUtil.getNameByCode(ifscBasedAccountPayload.getCountryCode())); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(ifscBasedAccountPayload.getHolderName()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(paymentAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), ifscBasedAccountPayload.getHolderName()); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.accountNr"), ifscBasedAccountPayload.getAccountNr()).second; field.setMouseTransparent(false); TextField fieldIfsc = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.ifsc"), ifscBasedAccountPayload.getIfsc()).second; fieldIfsc.setMouseTransparent(false); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), paymentAccount.getSingleTradeCurrency().getNameAndCode()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), CountryUtil.getNameByCode(ifscBasedAccountPayload.getCountryCode())); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && inputValidator.validate(ifscBasedAccountPayload.getHolderName()).isValid && inputValidator.validate(ifscBasedAccountPayload.getAccountNr()).isValid && ifscValidator.validate(ifscBasedAccountPayload.getIfsc()).isValid); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/ImpsForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import javafx.scene.layout.GridPane; public class ImpsForm extends IfscBankForm { public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { return IfscBankForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); } public ImpsForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/InteracETransferForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.InteracETransferAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.InteracETransferAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.validation.InteracETransferValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; public class InteracETransferForm extends PaymentMethodForm { private final InteracETransferAccount interacETransferAccount; private final InteracETransferValidator interacETransferValidator; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), ((InteracETransferAccountPayload) paymentAccountPayload).getHolderName()); addCompactTopLabelTextField(gridPane, gridRow, 1, Res.get("payment.emailOrMobile"), ((InteracETransferAccountPayload) paymentAccountPayload).getEmailOrMobileNr()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.secret"), ((InteracETransferAccountPayload) paymentAccountPayload).getQuestion()); addCompactTopLabelTextField(gridPane, gridRow, 1, Res.get("payment.answer"), ((InteracETransferAccountPayload) paymentAccountPayload).getAnswer()); return gridRow; } public InteracETransferForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InteracETransferValidator interacETransferValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.interacETransferAccount = (InteracETransferAccount) paymentAccount; this.interacETransferValidator = interacETransferValidator; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname")); holderNameInputTextField.setValidator(inputValidator); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { interacETransferAccount.setHolderName(newValue); updateFromInputs(); }); InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.emailOrMobile")); mobileNrInputTextField.setValidator(interacETransferValidator); mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { interacETransferAccount.setEmail(newValue); updateFromInputs(); }); InputTextField questionInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.secret")); questionInputTextField.setValidator(interacETransferValidator.questionValidator); questionInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { interacETransferAccount.setQuestion(newValue); updateFromInputs(); }); InputTextField answerInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.answer")); answerInputTextField.setValidator(interacETransferValidator.answerValidator); answerInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { interacETransferAccount.setAnswer(newValue); updateFromInputs(); }); TradeCurrency singleTradeCurrency = interacETransferAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(interacETransferAccount.getEmail()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(interacETransferAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), interacETransferAccount.getHolderName()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), interacETransferAccount.getEmail()).second.setMouseTransparent(false); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.secret"), interacETransferAccount.getQuestion()).second.setMouseTransparent(false); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.answer"), interacETransferAccount.getAnswer()).second.setMouseTransparent(false); TradeCurrency singleTradeCurrency = interacETransferAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && interacETransferValidator.validate(interacETransferAccount.getEmail()).isValid && inputValidator.validate(interacETransferAccount.getHolderName()).isValid && interacETransferValidator.questionValidator.validate(interacETransferAccount.getQuestion()).isValid && interacETransferValidator.answerValidator.validate(interacETransferAccount.getAnswer()).isValid && interacETransferAccount.getTradeCurrencies().size() > 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/JapanBankTransferForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.common.util.Tuple2; import haveno.common.util.Tuple3; import haveno.common.util.Tuple4; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.JapanBankAccount; import haveno.core.payment.JapanBankData; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.JapanBankAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.validation.JapanBankAccountNameValidator; import haveno.core.payment.validation.JapanBankAccountNumberValidator; import haveno.core.payment.validation.JapanBankBranchCodeValidator; import haveno.core.payment.validation.JapanBankBranchNameValidator; import haveno.core.payment.validation.LengthValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.core.util.validation.RegexValidator; import haveno.desktop.components.AutocompleteComboBox; import haveno.desktop.components.InputTextField; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.RadioButton; import javafx.scene.control.TextField; import javafx.scene.control.ToggleGroup; import javafx.scene.layout.GridPane; import javafx.util.StringConverter; import org.apache.commons.lang3.StringUtils; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addInputTextFieldInputTextField; import static haveno.desktop.util.FormBuilder.addTopLabelRadioButtonRadioButton; import static haveno.desktop.util.FormBuilder.addTopLabelTextFieldAutocompleteComboBox; import static haveno.desktop.util.GUIUtil.getComboBoxButtonCell; public class JapanBankTransferForm extends PaymentMethodForm { private final JapanBankAccount japanBankAccount; protected ComboBox bankComboBox; private final JapanBankBranchNameValidator japanBankBranchNameValidator; private final JapanBankBranchCodeValidator japanBankBranchCodeValidator; private final JapanBankAccountNameValidator japanBankAccountNameValidator; private final JapanBankAccountNumberValidator japanBankAccountNumberValidator; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { JapanBankAccountPayload japanBankAccount = ((JapanBankAccountPayload) paymentAccountPayload); String bankText = japanBankAccount.getBankCode() + " " + japanBankAccount.getBankName(); addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.japan.bank"), bankText); String branchText = japanBankAccount.getBankBranchCode() + " " + japanBankAccount.getBankBranchName(); addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.japan.branch"), branchText); String accountText = japanBankAccount.getBankAccountType() + " " + japanBankAccount.getBankAccountNumber(); addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.japan.account"), accountText); String accountNameText = japanBankAccount.getBankAccountName(); addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.japan.recipient"), accountNameText); return gridRow; } public JapanBankTransferForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.japanBankAccount = (JapanBankAccount) paymentAccount; this.japanBankBranchCodeValidator = new JapanBankBranchCodeValidator(); this.japanBankAccountNumberValidator = new JapanBankAccountNumberValidator(); LengthValidator lengthValidator = new LengthValidator(); RegexValidator regexValidator = new RegexValidator(); this.japanBankBranchNameValidator = new JapanBankBranchNameValidator(lengthValidator, regexValidator); this.japanBankAccountNameValidator = new JapanBankAccountNameValidator(lengthValidator, regexValidator); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(japanBankAccount.getPaymentMethod().getId())); addBankDisplay(); addBankBranchDisplay(); addBankAccountDisplay(); addBankAccountTypeDisplay(); addLimitations(true); } private void addBankDisplay() { String bankText = japanBankAccount.getBankCode() + " " + japanBankAccount.getBankName(); TextField bankTextField = addCompactTopLabelTextField(gridPane, ++gridRow, JapanBankData.getString("bank"), bankText).second; bankTextField.setEditable(false); } private void addBankBranchDisplay() { String branchText = japanBankAccount.getBankBranchCode() + " " + japanBankAccount.getBankBranchName(); TextField branchTextField = addCompactTopLabelTextField(gridPane, ++gridRow, JapanBankData.getString("branch"), branchText).second; branchTextField.setEditable(false); } private void addBankAccountDisplay() { String accountText = japanBankAccount.getBankAccountNumber() + " " + japanBankAccount.getBankAccountName(); TextField accountTextField = addCompactTopLabelTextField(gridPane, ++gridRow, JapanBankData.getString("account"), accountText).second; accountTextField.setEditable(false); } private void addBankAccountTypeDisplay() { TradeCurrency singleTradeCurrency = japanBankAccount.getSingleTradeCurrency(); String currency = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; String accountTypeText = currency + " " + japanBankAccount.getBankAccountType(); TextField accountTypeTextField = addCompactTopLabelTextField(gridPane, ++gridRow, JapanBankData.getString("account.type"), accountTypeText).second; accountTypeTextField.setEditable(false); } @Override public void addFormForAddAccount() { gridRowFrom = gridRow; addBankInput(); addBankBranchInput(); addBankAccountInput(); addBankAccountTypeInput(); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } private void addBankInput() { gridRow++; Tuple4> tuple4 = addTopLabelTextFieldAutocompleteComboBox(gridPane, gridRow, JapanBankData.getString("bank.code"), JapanBankData.getString("bank.name"), 10); // Bank Code (readonly) TextField bankCodeField = tuple4.second; bankCodeField.setPrefWidth(200); bankCodeField.setMaxWidth(200); bankCodeField.setEditable(false); // Bank Selector bankComboBox = tuple4.fourth; bankComboBox.setPromptText(JapanBankData.getString("bank.select")); bankComboBox.setButtonCell(getComboBoxButtonCell(JapanBankData.getString("bank.name"), bankComboBox)); bankComboBox.getEditor().focusedProperty().addListener(observable -> bankComboBox.setPromptText("")); bankComboBox.setConverter(new StringConverter<>() { @Override public String toString(String bank) { return bank != null ? bank : ""; } public String fromString(String s) { return s != null ? s : ""; } }); ((AutocompleteComboBox) bankComboBox).setAutocompleteItems(JapanBankData.prettyPrintBankList()); bankComboBox.setPrefWidth(430); bankComboBox.setVisibleRowCount(430); ((AutocompleteComboBox) bankComboBox).setOnChangeConfirmed(e -> { // get selected value String bank = bankComboBox.getSelectionModel().getSelectedItem(); // parse first 4 characters as bank code String bankCode = StringUtils.substring(bank, 0, 4); if (bankCode != null) { // set bank code field to this value bankCodeField.setText(bankCode); // save to payload japanBankAccount.setBankCode(bankCode); // parse remainder as bank name String bankNameFull = StringUtils.substringAfter(bank, JapanBankData.SPACE); // parse beginning as Japanese bank name String bankNameJa = StringUtils.substringBefore(bankNameFull, JapanBankData.SPACE); // set bank name field to this value bankComboBox.getEditor().setText(bankNameJa); // save to payload japanBankAccount.setBankName(bankNameJa); } updateFromInputs(); }); } private void addBankBranchInput() { gridRow++; Tuple2 tuple2 = addInputTextFieldInputTextField(gridPane, gridRow, JapanBankData.getString("branch.code"), JapanBankData.getString("branch.name")); // branch code InputTextField bankBranchCodeInputTextField = tuple2.first; bankBranchCodeInputTextField.setValidator(japanBankBranchCodeValidator); bankBranchCodeInputTextField.setPrefWidth(200); bankBranchCodeInputTextField.setMaxWidth(200); bankBranchCodeInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { japanBankAccount.setBankBranchCode(newValue); updateFromInputs(); }); // branch name InputTextField bankBranchNameInputTextField = tuple2.second; bankBranchNameInputTextField.setValidator(japanBankBranchNameValidator); bankBranchNameInputTextField.setPrefWidth(430); bankBranchNameInputTextField.setMaxWidth(430); bankBranchNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { japanBankAccount.setBankBranchName(newValue); updateFromInputs(); }); } private void addBankAccountInput() { gridRow++; Tuple2 tuple2 = addInputTextFieldInputTextField(gridPane, gridRow, JapanBankData.getString("account.number"), JapanBankData.getString("account.name")); // account number InputTextField bankAccountNumberInputTextField = tuple2.first; bankAccountNumberInputTextField.setValidator(japanBankAccountNumberValidator); bankAccountNumberInputTextField.setPrefWidth(200); bankAccountNumberInputTextField.setMaxWidth(200); bankAccountNumberInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { japanBankAccount.setBankAccountNumber(newValue); updateFromInputs(); }); // account name InputTextField bankAccountNameInputTextField = tuple2.second; bankAccountNameInputTextField.setValidator(japanBankAccountNameValidator); bankAccountNameInputTextField.setPrefWidth(430); bankAccountNameInputTextField.setMaxWidth(430); bankAccountNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { japanBankAccount.setBankAccountName(newValue); updateFromInputs(); }); } private void addBankAccountTypeInput() { // account currency gridRow++; TradeCurrency singleTradeCurrency = japanBankAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addCompactTopLabelTextField(gridPane, gridRow, Res.get("shared.currency"), nameAndCode, 20); // account type gridRow++; ToggleGroup toggleGroup = new ToggleGroup(); Tuple3 tuple3 = addTopLabelRadioButtonRadioButton( gridPane, gridRow, toggleGroup, JapanBankData.getString("account.type.select"), JapanBankData.getString("account.type.futsu"), JapanBankData.getString("account.type.touza"), 0 ); toggleGroup.getToggles().get(0).setSelected(true); japanBankAccount.setBankAccountType(JapanBankData.getString("account.type.futsu.ja")); RadioButton futsu = tuple3.second; RadioButton touza = tuple3.third; toggleGroup.selectedToggleProperty().addListener ( (ov, oldValue, newValue) -> { if (futsu.isSelected()) japanBankAccount.setBankAccountType(JapanBankData.getString("account.type.futsu.ja")); if (touza.isSelected()) japanBankAccount.setBankAccountType(JapanBankData.getString("account.type.touza.ja")); } ); } @Override public void updateFromInputs() { System.out.println("JapanBankTransferForm: updateFromInputs()"); System.out.println("bankName: " + japanBankAccount.getBankName()); System.out.println("bankCode: " + japanBankAccount.getBankCode()); System.out.println("bankBranchName: " + japanBankAccount.getBankBranchName()); System.out.println("bankBranchCode: " + japanBankAccount.getBankBranchCode()); System.out.println("bankAccountType: " + japanBankAccount.getBankAccountType()); System.out.println("bankAccountName: " + japanBankAccount.getBankAccountName()); System.out.println("bankAccountNumber: " + japanBankAccount.getBankAccountNumber()); super.updateFromInputs(); } @Override protected void autoFillNameTextField() { if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) { accountNameTextField.setText( Res.get(paymentAccount.getPaymentMethod().getId()) .concat(": ") .concat(japanBankAccount.getBankName()) .concat(" ") .concat(japanBankAccount.getBankBranchName()) .concat(" ") .concat(japanBankAccount.getBankAccountNumber()) .concat(" ") .concat(japanBankAccount.getBankAccountName()) ); } } @Override public void updateAllInputsValid() { boolean result = ( isAccountNameValid() && inputValidator.validate(japanBankAccount.getBankCode()).isValid && inputValidator.validate(japanBankAccount.getBankName()).isValid && japanBankBranchCodeValidator.validate(japanBankAccount.getBankBranchCode()).isValid && japanBankBranchNameValidator.validate(japanBankAccount.getBankBranchName()).isValid && japanBankAccountNumberValidator.validate(japanBankAccount.getBankAccountNumber()).isValid && japanBankAccountNameValidator.validate(japanBankAccount.getBankAccountName()).isValid && inputValidator.validate(japanBankAccount.getBankAccountType()).isValid ); allInputsValid.set(result); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/MoneseForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.payment.MoneseAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.MoneseAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; public class MoneseForm extends PaymentMethodForm { private final MoneseAccount account; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, 0, Res.get("payment.account.owner.fullname"), ((MoneseAccountPayload) paymentAccountPayload).getHolderName()); addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.mobile"), ((MoneseAccountPayload) paymentAccountPayload).getMobileNr()); return gridRow; } public MoneseForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.account = (MoneseAccount) paymentAccount; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname")); holderNameInputTextField.setValidator(inputValidator); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setHolderName(newValue.trim()); updateFromInputs(); }); InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.mobile")); mobileNrInputTextField.setValidator(inputValidator); mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setMobileNr(newValue.trim()); updateFromInputs(); }); addCurrenciesGrid(true); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } private void addCurrenciesGrid(boolean isEditable) { FlowPane flowPane = FormBuilder.addTopLabelFlowPane(gridPane, ++gridRow, Res.get("payment.supportedCurrencies"), 20, 20).second; if (isEditable) { flowPane.setId("flow-pane-checkboxes-bg"); } else { flowPane.setId("flow-pane-checkboxes-non-editable-bg"); } account.getSupportedCurrencies().forEach(currency -> fillUpFlowPaneWithCurrencies(isEditable, flowPane, currency, account)); } @Override protected void autoFillNameTextField() { setAccountNameWithString(account.getMobileNr()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), account.getHolderName()) .second.setMouseTransparent(false); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.mobile"), account.getMobileNr()) .second.setMouseTransparent(false); addLimitations(true); addCurrenciesGrid(false); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && inputValidator.validate(account.getHolderName()).isValid && inputValidator.validate(account.getMobileNr()).isValid && account.getTradeCurrencies().size() > 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/MoneyBeamForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.MoneyBeamAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.MoneyBeamAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.validation.MoneyBeamValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; public class MoneyBeamForm extends PaymentMethodForm { private final MoneyBeamAccount account; private final MoneyBeamValidator validator; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.moneyBeam.accountId"), ((MoneyBeamAccountPayload) paymentAccountPayload).getAccountId()); addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.account.owner.fullname"), PaymentAccountPayload.getHolderNameOrPromptIfEmpty(((MoneyBeamAccountPayload) paymentAccountPayload).getHolderName())); return gridRow; } public MoneyBeamForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, MoneyBeamValidator moneyBeamValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.account = (MoneyBeamAccount) paymentAccount; this.validator = moneyBeamValidator; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField accountIdInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.moneyBeam.accountId")); accountIdInputTextField.setValidator(validator); accountIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setAccountId(newValue.trim()); updateFromInputs(); }); InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname")); holderNameInputTextField.setValidator(inputValidator); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setHolderName(newValue); updateFromInputs(); }); final TradeCurrency singleTradeCurrency = account.getSingleTradeCurrency(); final String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(account.getAccountId()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.moneyBeam.accountId"), account.getAccountId()).second; field.setMouseTransparent(false); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), account.getHolderName()); final TradeCurrency singleTradeCurrency = account.getSingleTradeCurrency(); final String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && validator.validate(account.getAccountId()).isValid && inputValidator.validate(account.getHolderName()).isValid && account.getTradeCurrencies().size() > 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/MoneyGramForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.common.util.Tuple2; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.BankUtil; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import haveno.core.payment.MoneyGramAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.MoneyGramAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.validation.EmailValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.Layout; import javafx.scene.control.Label; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; import lombok.extern.slf4j.Slf4j; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addInputTextField; import static haveno.desktop.util.FormBuilder.addTopLabelFlowPane; @Slf4j public class MoneyGramForm extends PaymentMethodForm { public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { final MoneyGramAccountPayload payload = (MoneyGramAccountPayload) paymentAccountPayload; addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.fullName"), payload.getHolderName()); addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.email"), payload.getEmail()); addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.bank.country"), CountryUtil.getNameAndCode(((MoneyGramAccountPayload) paymentAccountPayload).getCountryCode())); if (BankUtil.isStateRequired(payload.getCountryCode())) addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.account.state"), payload.getState()); return gridRow; } private final MoneyGramAccountPayload moneyGramAccountPayload; private InputTextField stateInputTextField; private final EmailValidator emailValidator; public MoneyGramForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.moneyGramAccountPayload = (MoneyGramAccountPayload) paymentAccount.paymentAccountPayload; emailValidator = new EmailValidator(); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; final Country country = getMoneyGramPaymentAccount().getCountry(); addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(paymentAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"), country != null ? country.name : ""); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.fullName"), moneyGramAccountPayload.getHolderName()); if (BankUtil.isStateRequired(moneyGramAccountPayload.getCountryCode())) addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.state"), moneyGramAccountPayload.getState()).second.setMouseTransparent(false); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), moneyGramAccountPayload.getEmail()); addLimitations(true); addCurrenciesGrid(false); } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; gridRow = GUIUtil.addRegionCountry(gridPane, gridRow, this::onCountrySelected); InputTextField holderNameInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.account.fullName")); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { moneyGramAccountPayload.setHolderName(newValue); updateFromInputs(); }); holderNameInputTextField.setValidator(inputValidator); stateInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.account.state")); stateInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { moneyGramAccountPayload.setState(newValue); updateFromInputs(); }); applyIsStateRequired(); InputTextField emailInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.email")); emailInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { moneyGramAccountPayload.setEmail(newValue); updateFromInputs(); }); emailInputTextField.setValidator(emailValidator); addCurrenciesGrid(true); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); updateFromInputs(); } private void onCountrySelected(Country country) { if (country != null) { getMoneyGramPaymentAccount().setCountry(country); updateFromInputs(); applyIsStateRequired(); stateInputTextField.setText(""); } } private void addCurrenciesGrid(boolean isEditable) { final Tuple2 labelFlowPaneTuple2 = addTopLabelFlowPane(gridPane, ++gridRow, Res.get("payment.supportedCurrencies"), Layout.FLOATING_LABEL_DISTANCE * 3, Layout.FLOATING_LABEL_DISTANCE * 3); FlowPane flowPane = labelFlowPaneTuple2.second; if (isEditable) flowPane.setId("flow-pane-checkboxes-bg"); else flowPane.setId("flow-pane-checkboxes-non-editable-bg"); paymentAccount.getSupportedCurrencies().forEach(e -> fillUpFlowPaneWithCurrencies(isEditable, flowPane, e, paymentAccount)); } private void applyIsStateRequired() { final boolean stateRequired = BankUtil.isStateRequired(moneyGramAccountPayload.getCountryCode()); stateInputTextField.setManaged(stateRequired); stateInputTextField.setVisible(stateRequired); } private MoneyGramAccount getMoneyGramPaymentAccount() { return (MoneyGramAccount) this.paymentAccount; } @Override protected void autoFillNameTextField() { setAccountNameWithString(moneyGramAccountPayload.getHolderName() == null ? "" : moneyGramAccountPayload.getHolderName()); } @Override public void updateAllInputsValid() { boolean result = isAccountNameValid() && getMoneyGramPaymentAccount().getCountry() != null && inputValidator.validate(moneyGramAccountPayload.getHolderName()).isValid && emailValidator.validate(moneyGramAccountPayload.getEmail()).isValid && paymentAccount.getTradeCurrencies().size() > 0; allInputsValid.set(result); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/NationalBankForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import javafx.scene.layout.GridPane; public class NationalBankForm extends BankForm { public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { return BankForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); } public NationalBankForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/NeftForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import javafx.scene.layout.GridPane; public class NeftForm extends IfscBankForm { public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { return IfscBankForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); } public NeftForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/NequiForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import haveno.core.payment.NequiAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.NequiAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.Layout; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextFieldWithCopyIcon; public class NequiForm extends PaymentMethodForm { private final NequiAccount account; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.mobile"), ((NequiAccountPayload) paymentAccountPayload).getMobileNr(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); return gridRow; } public NequiForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.account = (NequiAccount) paymentAccount; } @Override public void addFormForAddAccount() { // this payment method is only for Columbia/COP account.setSingleTradeCurrency(account.getSupportedCurrencies().get(0)); CountryUtil.findCountryByCode("CO").ifPresent(c -> account.setCountry(c)); gridRowFrom = gridRow + 1; InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.mobile")); mobileNrInputTextField.setValidator(inputValidator); mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setMobileNr(newValue.trim()); updateFromInputs(); }); addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(account.getMobileNr()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.mobile"), account.getMobileNr()).second; field.setMouseTransparent(false); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && inputValidator.validate(account.getMobileNr()).isValid); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/PaxumForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.payment.PaxumAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaxumAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.validation.EmailValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import javafx.scene.control.TextField; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; public class PaxumForm extends PaymentMethodForm { private final PaxumAccount account; private final EmailValidator validator = new EmailValidator(); public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email"), ((PaxumAccountPayload) paymentAccountPayload).getEmail()); return gridRow; } public PaxumForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.account = (PaxumAccount) paymentAccount; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField emailInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email")); emailInputTextField.setValidator(validator); emailInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setEmail(newValue.trim()); updateFromInputs(); }); addCurrenciesGrid(true); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } private void addCurrenciesGrid(boolean isEditable) { FlowPane flowPane = FormBuilder.addTopLabelFlowPane(gridPane, ++gridRow, Res.get("payment.supportedCurrenciesForReceiver"), 20, 20).second; if (isEditable) { flowPane.setId("flow-pane-checkboxes-bg"); } else { flowPane.setId("flow-pane-checkboxes-non-editable-bg"); } paymentAccount.getSupportedCurrencies().forEach(currency -> fillUpFlowPaneWithCurrencies(isEditable, flowPane, currency, account)); } @Override protected void autoFillNameTextField() { setAccountNameWithString(account.getEmail()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), account.getEmail()).second; field.setMouseTransparent(false); addLimitations(true); addCurrenciesGrid(false); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && account.getEmail() != null && validator.validate(account.getEmail()).isValid && account.getTradeCurrencies().size() > 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/PayByMailForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import com.jfoenix.controls.JFXTextArea; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.PayByMailAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PayByMailAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.Layout; import javafx.collections.FXCollections; import javafx.scene.control.TextArea; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextArea; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addInputTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextArea; import static haveno.desktop.util.FormBuilder.addTopLabelTextFieldWithCopyIcon; public class PayByMailForm extends PaymentMethodForm { private final PayByMailAccount payByMailAccount; private TextArea postalAddressTextArea; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { PayByMailAccountPayload cbm = (PayByMailAccountPayload) paymentAccountPayload; addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.account.owner.fullname"), cbm.getHolderName(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); TextArea textAddress = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.postal.address"), "").second; textAddress.setMinHeight(70); textAddress.setEditable(false); textAddress.setText(cbm.getPostalAddress()); TextArea textExtraInfo = addCompactTopLabelTextArea(gridPane, gridRow, 1, Res.get("payment.shared.extraInfo"), "").second; textExtraInfo.setMinHeight(70); textExtraInfo.setEditable(false); textExtraInfo.setText(cbm.getExtraInfo()); return gridRow; } public PayByMailForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.payByMailAccount = (PayByMailAccount) paymentAccount; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; addTradeCurrencyComboBox(); currencyComboBox.setItems(FXCollections.observableArrayList(CurrencyUtil.getAllSortedTraditionalCurrencies())); InputTextField contactField = addInputTextField(gridPane, ++gridRow, Res.get("payment.payByMail.contact")); contactField.setPromptText(Res.get("payment.payByMail.contact.prompt")); contactField.setValidator(inputValidator); contactField.textProperty().addListener((ov, oldValue, newValue) -> { payByMailAccount.setContact(newValue); updateFromInputs(); }); postalAddressTextArea = addTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.postal.address"), "").second; postalAddressTextArea.setMinHeight(70); postalAddressTextArea.textProperty().addListener((ov, oldValue, newValue) -> { payByMailAccount.setPostalAddress(newValue); updateFromInputs(); }); TextArea extraTextArea = addTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.optionalExtra"), Res.get("payment.payByMail.extraInfo.prompt")).second; extraTextArea.setMinHeight(70); ((JFXTextArea) extraTextArea).setLabelFloat(false); extraTextArea.textProperty().addListener((ov, oldValue, newValue) -> { payByMailAccount.setExtraInfo(newValue); updateFromInputs(); }); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(payByMailAccount.getContact()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(payByMailAccount.getPaymentMethod().getId())); TradeCurrency tradeCurrency = paymentAccount.getSingleTradeCurrency(); String nameAndCode = tradeCurrency != null ? tradeCurrency.getNameAndCode() : ""; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.f2f.contact"), payByMailAccount.getContact()); TextArea textArea = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.postal.address"), "").second; textArea.setText(payByMailAccount.getPostalAddress()); textArea.setMinHeight(70); textArea.setEditable(false); TextArea textAreaExtra = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), "").second; textAreaExtra.setText(payByMailAccount.getExtraInfo()); textAreaExtra.setMinHeight(70); textAreaExtra.setEditable(false); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && !payByMailAccount.getPostalAddress().isEmpty() && inputValidator.validate(payByMailAccount.getContact()).isValid && paymentAccount.getSingleTradeCurrency() != null); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/PayPalForm.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.desktop.components.paymentmethods; import com.jfoenix.controls.JFXTextArea; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.payment.PayPalAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PayPalAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.validation.EmailOrMobileNrOrUsernameValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.Layout; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextArea; import static haveno.desktop.util.FormBuilder.addTopLabelTextArea; import static haveno.desktop.util.FormBuilder.addTopLabelFlowPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; public class PayPalForm extends PaymentMethodForm { private final PayPalAccount paypalAccount; private final EmailOrMobileNrOrUsernameValidator paypalValidator; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email.mobile.username"), ((PayPalAccountPayload) paymentAccountPayload).getEmailOrMobileNrOrUsername()); PayPalAccountPayload payId = (PayPalAccountPayload) paymentAccountPayload; TextArea textExtraInfo = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), "").second; textExtraInfo.setMinHeight(70); textExtraInfo.setEditable(false); textExtraInfo.setText(payId.getExtraInfo()); return gridRow; } public PayPalForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, EmailOrMobileNrOrUsernameValidator paypalValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.paypalAccount = (PayPalAccount) paymentAccount; this.paypalValidator = paypalValidator; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email.mobile.username")); mobileNrInputTextField.setValidator(paypalValidator); mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { paypalAccount.setEmailOrMobileNrOrUsername(newValue.trim()); updateFromInputs(); }); TextArea extraTextArea = addTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt.paymentAccount")).second; extraTextArea.setMinHeight(70); ((JFXTextArea) extraTextArea).setLabelFloat(false); extraTextArea.textProperty().addListener((ov, oldValue, newValue) -> { paypalAccount.setExtraInfo(newValue); updateFromInputs(); }); addCurrenciesGrid(true); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } private void addCurrenciesGrid(boolean isEditable) { FlowPane flowPane = addTopLabelFlowPane(gridPane, ++gridRow, Res.get("payment.supportedCurrencies"), Layout.FLOATING_LABEL_DISTANCE * 3, Layout.FLOATING_LABEL_DISTANCE * 3).second; if (isEditable) flowPane.setId("flow-pane-checkboxes-bg"); else flowPane.setId("flow-pane-checkboxes-non-editable-bg"); paypalAccount.getSupportedCurrencies().forEach(e -> fillUpFlowPaneWithCurrencies(isEditable, flowPane, e, paypalAccount)); } @Override protected void autoFillNameTextField() { setAccountNameWithString(paypalAccount.getEmailOrMobileNrOrUsername()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(paypalAccount.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email.mobile.username"), paypalAccount.getEmailOrMobileNrOrUsername()).second; TextArea textAreaExtra = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), "").second; textAreaExtra.setText(paypalAccount.getExtraInfo()); textAreaExtra.setMinHeight(70); textAreaExtra.setEditable(false); field.setMouseTransparent(false); addLimitations(true); addCurrenciesGrid(false); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && paypalValidator.validate(paypalAccount.getEmailOrMobileNrOrUsername()).isValid && paypalAccount.getTradeCurrencies().size() > 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/PaymentMethodForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import haveno.common.util.Tuple3; import haveno.common.util.Utilities; import haveno.core.account.witness.AccountAgeWitness; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.payment.AssetAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentMethod; import haveno.core.trade.HavenoUtils; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.AutoTooltipCheckBox; import haveno.desktop.components.InfoTextField; import haveno.desktop.components.InputTextField; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.DisplayUtils; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.Layout; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.FXCollections; import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.control.ToggleButton; import javafx.scene.control.Tooltip; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.VBox; import javafx.util.StringConverter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import java.util.Date; import java.util.concurrent.TimeUnit; import static haveno.desktop.util.DisplayUtils.createAccountName; import static haveno.desktop.util.FormBuilder.addCompactTopLabelInfoTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addInputTextField; import static haveno.desktop.util.FormBuilder.addTopLabelInfoTextField; import static haveno.desktop.util.FormBuilder.addTopLabelInputTextFieldSlideToggleButton; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; @Slf4j public abstract class PaymentMethodForm { protected final PaymentAccount paymentAccount; private final AccountAgeWitnessService accountAgeWitnessService; protected final InputValidator inputValidator; protected final GridPane gridPane; protected int gridRow; private final CoinFormatter formatter; protected final BooleanProperty allInputsValid = new SimpleBooleanProperty(); protected int gridRowFrom; InputTextField accountNameTextField; protected TextField paymentLimitationsTextField; ToggleButton useCustomAccountNameToggleButton; protected ComboBox currencyComboBox; public PaymentMethodForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { this.paymentAccount = paymentAccount; this.accountAgeWitnessService = accountAgeWitnessService; this.inputValidator = inputValidator; this.gridPane = gridPane; this.gridRow = gridRow; this.formatter = formatter; } protected void addTradeCurrencyComboBox() { currencyComboBox = FormBuilder.addComboBox(gridPane, ++gridRow, Res.get("shared.currency")); currencyComboBox.setPromptText(Res.get("list.currency.select")); currencyComboBox.setItems(FXCollections.observableArrayList(CurrencyUtil.getMainTraditionalCurrencies())); currencyComboBox.setConverter(new StringConverter<>() { @Override public String toString(TradeCurrency tradeCurrency) { return tradeCurrency.getNameAndCode(); } @Override public TradeCurrency fromString(String s) { return null; } }); currencyComboBox.setOnAction(e -> { paymentAccount.setSingleTradeCurrency(currencyComboBox.getSelectionModel().getSelectedItem()); updateFromInputs(); }); } protected void addAccountNameTextFieldWithAutoFillToggleButton() { boolean isEditMode = paymentAccount.getPersistedAccountName() != null; Tuple3 tuple = addTopLabelInputTextFieldSlideToggleButton(gridPane, ++gridRow, Res.get("payment.account.name"), Res.get("payment.useCustomAccountName")); accountNameTextField = tuple.second; accountNameTextField.setPrefWidth(300); accountNameTextField.setEditable(isEditMode); accountNameTextField.setValidator(inputValidator); accountNameTextField.setFocusTraversable(false); accountNameTextField.setText(paymentAccount.getAccountName()); accountNameTextField.textProperty().addListener((ov, oldValue, newValue) -> { paymentAccount.setAccountName(newValue); updateAllInputsValid(); }); useCustomAccountNameToggleButton = tuple.third; useCustomAccountNameToggleButton.setSelected(isEditMode); useCustomAccountNameToggleButton.setOnAction(e -> { boolean selected = useCustomAccountNameToggleButton.isSelected(); accountNameTextField.setEditable(selected); accountNameTextField.setFocusTraversable(selected); autoFillNameTextField(); }); } public static InfoTextField addOpenTradeDuration(GridPane gridPane, int gridRow, Offer offer) { long hours = offer.getPaymentMethod().getMaxTradePeriod() / 3600_000; final Tuple3 labelInfoTextFieldVBoxTuple3 = addTopLabelInfoTextField(gridPane, gridRow, Res.get("payment.maxPeriod"), getTimeText(hours), -Layout.FLOATING_LABEL_DISTANCE); return labelInfoTextFieldVBoxTuple3.second; } private static String getTimeText(long hours) { String time = hours + " " + Res.get("time.hours"); if (hours == 1) time = Res.get("time.1hour"); else if (hours == 24) time = Res.get("time.1day"); else if (hours > 24) time = hours / 24 + " " + Res.get("time.days"); return time; } protected String getLimitationsText() { final PaymentAccount paymentAccount = getPaymentAccount(); long hours = paymentAccount.getMaxTradePeriod() / 3600_000; final TradeCurrency tradeCurrency; if (paymentAccount.getSingleTradeCurrency() != null) tradeCurrency = paymentAccount.getSingleTradeCurrency(); else if (paymentAccount.getSelectedTradeCurrency() != null) tradeCurrency = paymentAccount.getSelectedTradeCurrency(); else if (!paymentAccount.getTradeCurrencies().isEmpty() && paymentAccount.getTradeCurrencies().get(0) != null) tradeCurrency = paymentAccount.getTradeCurrencies().get(0); else tradeCurrency = paymentAccount instanceof AssetAccount ? CurrencyUtil.getAllSortedCryptoCurrencies().iterator().next() : CurrencyUtil.getDefaultTradeCurrency(); final boolean isAddAccountScreen = paymentAccount.getAccountName() == null; final long accountAge = !isAddAccountScreen ? accountAgeWitnessService.getMyAccountAge(paymentAccount.getPaymentAccountPayload()) : 0L; final String limitationsText = paymentAccount instanceof AssetAccount ? Res.get("payment.maxPeriodAndLimitCrypto", getTimeText(hours), HavenoUtils.formatXmr(accountAgeWitnessService.getMyTradeLimit( paymentAccount, tradeCurrency.getCode(), OfferDirection.BUY, false), true)) : Res.get("payment.maxPeriodAndLimit", getTimeText(hours), HavenoUtils.formatXmr(accountAgeWitnessService.getMyTradeLimit( paymentAccount, tradeCurrency.getCode(), OfferDirection.BUY, false), true), HavenoUtils.formatXmr(accountAgeWitnessService.getMyTradeLimit( paymentAccount, tradeCurrency.getCode(), OfferDirection.SELL, false), true), DisplayUtils.formatAccountAge(accountAge)); return limitationsText; } protected void addLimitations(boolean isDisplayForm) { final boolean isAddAccountScreen = paymentAccount.getAccountName() == null; if (isDisplayForm) { addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.limitations"), getLimitationsText()); String accountSigningStateText; MaterialDesignIcon icon; boolean needsSigning = PaymentMethod.hasChargebackRisk(paymentAccount.getPaymentMethod(), paymentAccount.getTradeCurrencies()); if (needsSigning) { AccountAgeWitness myWitness = accountAgeWitnessService.getMyWitness( paymentAccount.paymentAccountPayload); AccountAgeWitnessService.SignState signState = accountAgeWitnessService.getSignState(myWitness); accountSigningStateText = StringUtils.capitalize(signState.getDisplayString()); long daysSinceSigning = TimeUnit.MILLISECONDS.toDays( accountAgeWitnessService.getWitnessSignAge(myWitness, new Date())); String timeSinceSigning = Res.get("offerbook.timeSinceSigning.daysSinceSigning.long", Res.get("offerbook.timeSinceSigning.daysSinceSigning", daysSinceSigning)); if (!signState.equals(AccountAgeWitnessService.SignState.UNSIGNED)) { accountSigningStateText += " / " + timeSinceSigning; } icon = GUIUtil.getIconForSignState(signState); InfoTextField accountSigningField = addCompactTopLabelInfoTextField(gridPane, ++gridRow, Res.get("shared.accountSigningState"), accountSigningStateText).second; //TODO: add additional information regarding account signing accountSigningField.setContent(icon, accountSigningStateText, "", 0.4); } } else { paymentLimitationsTextField = addTopLabelTextField(gridPane, ++gridRow, Res.get("payment.limitations"), getLimitationsText()).second; } if (!(paymentAccount instanceof AssetAccount)) { if (isAddAccountScreen) { InputTextField inputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.salt"), 0); inputTextField.setText(Utilities.bytesAsHexString(paymentAccount.getPaymentAccountPayload().getSalt())); inputTextField.textProperty().addListener((observable, oldValue, newValue) -> { if (!newValue.isEmpty()) { try { // test if input is hex Utilities.decodeFromHex(newValue); paymentAccount.setSaltAsHex(newValue); } catch (Throwable t) { new Popup().warning(Res.get("payment.error.noHexSalt")).show(); inputTextField.setText(Utilities.bytesAsHexString(paymentAccount.getPaymentAccountPayload().getSalt())); log.warn(t.toString()); } } }); } else { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.salt", Utilities.bytesAsHexString(paymentAccount.getPaymentAccountPayload().getSalt())), Utilities.bytesAsHexString(paymentAccount.getPaymentAccountPayload().getSalt())); } } } void applyTradeCurrency(TradeCurrency tradeCurrency, TraditionalCurrency defaultCurrency) { if (!defaultCurrency.equals(tradeCurrency)) { new Popup().warning(Res.get("payment.foreign.currency")) .actionButtonText(Res.get("shared.yes")) .onAction(() -> { paymentAccount.setSingleTradeCurrency(tradeCurrency); autoFillNameTextField(); }) .closeButtonText(Res.get("payment.restore.default")) .onClose(() -> currencyComboBox.getSelectionModel().select(defaultCurrency)) .show(); } else { paymentAccount.setSingleTradeCurrency(tradeCurrency); autoFillNameTextField(); } } void setAccountNameWithString(String name) { if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) { String accountName = createAccountName(paymentAccount.getPaymentMethod().getId(), name); accountNameTextField.setText(accountName); } } void fillUpFlowPaneWithCurrencies(boolean isEditable, FlowPane flowPane, TradeCurrency e, PaymentAccount paymentAccount) { CheckBox checkBox = new AutoTooltipCheckBox(e.getCode()); checkBox.setMouseTransparent(!isEditable); checkBox.setSelected(paymentAccount.getTradeCurrencies().contains(e)); checkBox.setMinWidth(60); checkBox.setMaxWidth(checkBox.getMinWidth()); checkBox.setTooltip(new Tooltip(e.getName())); checkBox.setOnAction(event -> { if (checkBox.isSelected()) paymentAccount.addCurrency(e); else paymentAccount.removeCurrency(e); updateAllInputsValid(); }); flowPane.getChildren().add(checkBox); } protected abstract void autoFillNameTextField(); public abstract void addFormForAddAccount(); public abstract void addFormForEditAccount(); protected abstract void updateAllInputsValid(); public void updateFromInputs() { autoFillNameTextField(); updateAllInputsValid(); } public boolean isAccountNameValid() { return inputValidator.validate(paymentAccount.getAccountName()).isValid; } public int getGridRow() { return gridRow; } public int getRowSpan() { return gridRow - gridRowFrom + 2; } public PaymentAccount getPaymentAccount() { return paymentAccount; } public BooleanProperty allInputsValidProperty() { return allInputsValid; } void removeAcceptedCountry(String countryCode) { } void addAcceptedCountry(String countryCode) { } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/PaysafeForm.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaysafeAccount; import haveno.core.payment.payload.PaysafeAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.validation.EmailValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import javafx.scene.control.TextField; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; public class PaysafeForm extends PaymentMethodForm { private final PaysafeAccount account; private final EmailValidator validator = new EmailValidator(); public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email"), ((PaysafeAccountPayload) paymentAccountPayload).getEmail()); return gridRow; } public PaysafeForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.account = (PaysafeAccount) paymentAccount; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField emailInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email")); emailInputTextField.setValidator(validator); emailInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setEmail(newValue.trim()); updateFromInputs(); }); addCurrenciesGrid(true); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } private void addCurrenciesGrid(boolean isEditable) { FlowPane flowPane = FormBuilder.addTopLabelFlowPane(gridPane, ++gridRow, Res.get("payment.supportedCurrenciesForReceiver"), 20, 20).second; if (isEditable) { flowPane.setId("flow-pane-checkboxes-bg"); } else { flowPane.setId("flow-pane-checkboxes-non-editable-bg"); } paymentAccount.getSupportedCurrencies().forEach(currency -> fillUpFlowPaneWithCurrencies(isEditable, flowPane, currency, account)); } @Override protected void autoFillNameTextField() { setAccountNameWithString(account.getEmail()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), account.getEmail()).second; field.setMouseTransparent(false); addLimitations(true); addCurrenciesGrid(false); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && account.getEmail() != null && validator.validate(account.getEmail()).isValid && account.getTradeCurrencies().size() > 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/PayseraForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PayseraAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PayseraAccountPayload; import haveno.core.payment.validation.EmailValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import javafx.scene.control.TextField; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; public class PayseraForm extends PaymentMethodForm { private final PayseraAccount account; private final EmailValidator validator = new EmailValidator(); public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email"), ((PayseraAccountPayload) paymentAccountPayload).getEmail()); return gridRow; } public PayseraForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.account = (PayseraAccount) paymentAccount; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField emailInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email")); emailInputTextField.setValidator(validator); emailInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setEmail(newValue.trim()); updateFromInputs(); }); addCurrenciesGrid(true); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } private void addCurrenciesGrid(boolean isEditable) { FlowPane flowPane = FormBuilder.addTopLabelFlowPane(gridPane, ++gridRow, Res.get("payment.supportedCurrenciesForReceiver"), 20, 20).second; if (isEditable) { flowPane.setId("flow-pane-checkboxes-bg"); } else { flowPane.setId("flow-pane-checkboxes-non-editable-bg"); } paymentAccount.getSupportedCurrencies().forEach(currency -> fillUpFlowPaneWithCurrencies(isEditable, flowPane, currency, account)); } @Override protected void autoFillNameTextField() { setAccountNameWithString(account.getEmail()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), account.getEmail()).second; field.setMouseTransparent(false); addLimitations(true); addCurrenciesGrid(false); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && account.getEmail() != null && validator.validate(account.getEmail()).isValid && account.getTradeCurrencies().size() > 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/PaytmForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaytmAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaytmAccountPayload; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.Layout; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextFieldWithCopyIcon; public class PaytmForm extends PaymentMethodForm { private final PaytmAccount account; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.email.mobile"), ((PaytmAccountPayload) paymentAccountPayload).getEmailOrMobileNr(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); return gridRow; } public PaytmForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.account = (PaytmAccount) paymentAccount; } @Override public void addFormForAddAccount() { // this payment method is only for India/INR account.setSingleTradeCurrency(account.getSupportedCurrencies().get(0)); CountryUtil.findCountryByCode("IN").ifPresent(c -> account.setCountry(c)); gridRowFrom = gridRow + 1; InputTextField emailOrMobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email.mobile")); emailOrMobileNrInputTextField.setValidator(inputValidator); emailOrMobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setEmailOrMobileNr(newValue.trim()); updateFromInputs(); }); addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(account.getEmailOrMobileNr()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email.mobile"), account.getEmailOrMobileNr()).second; field.setMouseTransparent(false); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && inputValidator.validate(account.getEmailOrMobileNr()).isValid); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/PerfectMoneyForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.Res; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PerfectMoneyAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PerfectMoneyAccountPayload; import haveno.core.payment.validation.PerfectMoneyValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import javafx.collections.FXCollections; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; public class PerfectMoneyForm extends GeneralAccountNumberForm { private final PerfectMoneyAccount perfectMoneyAccount; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.no"), ((PerfectMoneyAccountPayload) paymentAccountPayload).getAccountNr()); return gridRow; } public PerfectMoneyForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, PerfectMoneyValidator perfectMoneyValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.perfectMoneyAccount = (PerfectMoneyAccount) paymentAccount; } @Override public void addTradeCurrency() { addTradeCurrencyComboBox(); currencyComboBox.setItems(FXCollections.observableArrayList(new TraditionalCurrency("USD"), new TraditionalCurrency("EUR"))); } @Override void setAccountNumber(String newValue) { perfectMoneyAccount.setAccountNr(newValue); } @Override String getAccountNr() { return perfectMoneyAccount.getAccountNr(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/PixForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PixAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PixAccountPayload; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.Layout; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextFieldWithCopyIcon; public class PixForm extends PaymentMethodForm { private final PixAccount account; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.pix.key"), ((PixAccountPayload) paymentAccountPayload).getPixKey(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), PaymentAccountPayload.getHolderNameOrPromptIfEmpty(((PixAccountPayload) paymentAccountPayload).getHolderName())); return gridRow; } public PixForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.account = (PixAccount) paymentAccount; } @Override public void addFormForAddAccount() { // this payment method is only for Brazil/BRL account.setSingleTradeCurrency(account.getSupportedCurrencies().get(0)); CountryUtil.findCountryByCode("BR").ifPresent(c -> account.setCountry(c)); gridRowFrom = gridRow + 1; InputTextField pixKeyInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.pix.key")); pixKeyInputTextField.setValidator(inputValidator); pixKeyInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setPixKey(newValue.trim()); updateFromInputs(); }); InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname")); holderNameInputTextField.setValidator(inputValidator); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setHolderName(newValue); updateFromInputs(); }); addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(account.getPixKey()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.pix.key"), account.getPixKey()).second; field.setMouseTransparent(false); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), account.getHolderName()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && inputValidator.validate(account.getHolderName()).isValid && inputValidator.validate(account.getPixKey()).isValid); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/PopmoneyForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PopmoneyAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PopmoneyAccountPayload; import haveno.core.payment.validation.PopmoneyValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; public class PopmoneyForm extends PaymentMethodForm { private final PopmoneyAccount account; private final PopmoneyValidator validator; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), ((PopmoneyAccountPayload) paymentAccountPayload).getHolderName()); addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.popmoney.accountId"), ((PopmoneyAccountPayload) paymentAccountPayload).getAccountId()); return gridRow; } public PopmoneyForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, PopmoneyValidator popmoneyValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.account = (PopmoneyAccount) paymentAccount; this.validator = popmoneyValidator; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname")); holderNameInputTextField.setValidator(inputValidator); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setHolderName(newValue.trim()); updateFromInputs(); }); InputTextField accountIdInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.popmoney.accountId")); accountIdInputTextField.setValidator(validator); accountIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setAccountId(newValue.trim()); updateFromInputs(); }); final TradeCurrency singleTradeCurrency = account.getSingleTradeCurrency(); final String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(account.getAccountId()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), account.getHolderName()); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.popmoney.accountId"), account.getAccountId()).second; field.setMouseTransparent(false); final TradeCurrency singleTradeCurrency = account.getSingleTradeCurrency(); final String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && inputValidator.validate(account.getHolderName()).isValid && validator.validate(account.getAccountId()).isValid && account.getTradeCurrencies().size() > 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/PromptPayForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PromptPayAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PromptPayAccountPayload; import haveno.core.payment.validation.PromptPayValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addInputTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; public class PromptPayForm extends PaymentMethodForm { private final PromptPayAccount promptPayAccount; private final PromptPayValidator promptPayValidator; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.promptPay.promptPayId"), ((PromptPayAccountPayload) paymentAccountPayload).getPromptPayId()); return gridRow; } public PromptPayForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, PromptPayValidator promptPayValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.promptPayAccount = (PromptPayAccount) paymentAccount; this.promptPayValidator = promptPayValidator; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField promptPayIdInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.promptPay.promptPayId")); promptPayIdInputTextField.setValidator(promptPayValidator); promptPayIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { promptPayAccount.setPromptPayId(newValue); updateFromInputs(); }); TradeCurrency singleTradeCurrency = promptPayAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(promptPayAccount.getPromptPayId()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(promptPayAccount.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.promptPay.promptPayId"), promptPayAccount.getPromptPayId()).second; field.setMouseTransparent(false); TradeCurrency singleTradeCurrency = promptPayAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && promptPayValidator.validate(promptPayAccount.getPromptPayId()).isValid && promptPayAccount.getTradeCurrencies().size() > 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/RevolutForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.common.util.Tuple2; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.payment.PaymentAccount; import haveno.core.payment.RevolutAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.RevolutAccountPayload; import haveno.core.payment.validation.RevolutValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.Layout; import javafx.scene.control.TextField; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; import lombok.extern.slf4j.Slf4j; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addTopLabelFlowPane; @Slf4j public class RevolutForm extends PaymentMethodForm { private final RevolutAccount account; private final RevolutValidator validator; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { Tuple2 tuple = ((RevolutAccountPayload) paymentAccountPayload).getRecipientsAccountData(); addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, tuple.first, tuple.second); return gridRow; } public RevolutForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, RevolutValidator revolutValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.account = (RevolutAccount) paymentAccount; this.validator = revolutValidator; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField userNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.username")); userNameInputTextField.setValidator(validator); userNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setUsername(newValue.trim()); updateFromInputs(); }); addCurrenciesGrid(true); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } private void addCurrenciesGrid(boolean isEditable) { FlowPane flowPane = addTopLabelFlowPane(gridPane, ++gridRow, Res.get("payment.supportedCurrencies"), Layout.FLOATING_LABEL_DISTANCE * 3, Layout.FLOATING_LABEL_DISTANCE * 3).second; if (isEditable) flowPane.setId("flow-pane-checkboxes-bg"); else flowPane.setId("flow-pane-checkboxes-non-editable-bg"); account.getSupportedCurrencies().forEach(e -> fillUpFlowPaneWithCurrencies(isEditable, flowPane, e, account)); } @Override protected void autoFillNameTextField() { setAccountNameWithString(account.getUsername()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); String userName = account.getUsername(); TextField userNameTf = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.username"), userName).second; userNameTf.setMouseTransparent(false); addLimitations(true); addCurrenciesGrid(false); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && validator.validate(account.getUsername()).isValid && account.getTradeCurrencies().size() > 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/RtgsForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import javafx.scene.layout.GridPane; public class RtgsForm extends IfscBankForm { public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { return IfscBankForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); } public RtgsForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/SameBankForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import javafx.scene.layout.GridPane; public class SameBankForm extends BankForm { public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { return BankForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); } public SameBankForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/SatispayForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import haveno.core.payment.PaymentAccount; import haveno.core.payment.SatispayAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.SatispayAccountPayload; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; public class SatispayForm extends PaymentMethodForm { private final SatispayAccount account; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, 0, Res.get("payment.account.owner.fullname"), ((SatispayAccountPayload) paymentAccountPayload).getHolderName()); addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.mobile"), ((SatispayAccountPayload) paymentAccountPayload).getMobileNr()); return gridRow; } public SatispayForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.account = (SatispayAccount) paymentAccount; } @Override public void addFormForAddAccount() { // this payment method is only for Italy/EUR account.setSingleTradeCurrency(account.getSupportedCurrencies().get(0)); CountryUtil.findCountryByCode("IT").ifPresent(c -> account.setCountry(c)); gridRowFrom = gridRow + 1; InputTextField holderNameField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname")); holderNameField.setValidator(inputValidator); holderNameField.textProperty().addListener((ov, oldValue, newValue) -> { account.setHolderName(newValue.trim()); updateFromInputs(); }); InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.mobile")); mobileNrInputTextField.setValidator(inputValidator); mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setMobileNr(newValue.trim()); updateFromInputs(); }); addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(account.getMobileNr()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), account.getHolderName()) .second.setMouseTransparent(false); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.mobile"), account.getMobileNr()) .second.setMouseTransparent(false); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && inputValidator.validate(account.getHolderName()).isValid && inputValidator.validate(account.getMobileNr()).isValid); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/SepaForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.PaymentAccount; import haveno.core.payment.SepaAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.SepaAccountPayload; import haveno.core.payment.validation.BICValidator; import haveno.core.payment.validation.SepaIBANValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.normalization.IBANNormalizer; import javafx.collections.FXCollections; import javafx.scene.control.ComboBox; import javafx.scene.control.TextFormatter; import javafx.scene.layout.GridPane; import java.util.List; import java.util.Optional; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; public class SepaForm extends GeneralSepaForm { public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { SepaAccountPayload sepaAccountPayload = (SepaAccountPayload) paymentAccountPayload; final String title = Res.get("payment.account.owner.fullname"); final String value = sepaAccountPayload.getHolderName(); addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, title, value); addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.bank.country"), CountryUtil.getNameAndCode(sepaAccountPayload.getCountryCode())); // IBAN, BIC will not be translated addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, IBAN, sepaAccountPayload.getIban()); addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, BIC, sepaAccountPayload.getBic()); return gridRow; } private final SepaAccount sepaAccount; private final SepaIBANValidator sepaIBANValidator; private final BICValidator bicValidator; public SepaForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, BICValidator bicValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.sepaAccount = (SepaAccount) paymentAccount; this.bicValidator = bicValidator; this.sepaIBANValidator = new SepaIBANValidator(); } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname")); holderNameInputTextField.setValidator(inputValidator); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { sepaAccount.setHolderName(newValue); updateFromInputs(); }); InputTextField ibanInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, IBAN); ibanInputTextField.setTextFormatter(new TextFormatter<>(new IBANNormalizer())); ibanInputTextField.setValidator(sepaIBANValidator); InputTextField bicInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, BIC); bicInputTextField.setValidator(bicValidator); bicInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { sepaAccount.setBic(newValue); updateFromInputs(); }); ComboBox countryComboBox = addCountrySelection(); setCountryComboBoxAction(countryComboBox, sepaAccount); addCountriesGrid(Res.get("payment.accept.euro"), CountryUtil.getAllSepaEuroCountries()); addCountriesGrid(Res.get("payment.accept.nonEuro"), CountryUtil.getAllSepaNonEuroCountries()); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); countryComboBox.setItems(FXCollections.observableArrayList(CountryUtil.getAllSepaCountries())); Country country = CountryUtil.getDefaultCountry(); if (CountryUtil.getAllSepaCountries().contains(country)) { countryComboBox.getSelectionModel().select(country); sepaAccount.setCountry(country); } ibanInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { sepaAccount.setIban(newValue); updateFromInputs(); if (ibanInputTextField.validate()) { List countries = CountryUtil.getAllSepaCountries(); String ibanCountryCode = newValue.substring(0, 2).toUpperCase(); Optional ibanCountry = countries .stream() .filter(c -> c.code.equals(ibanCountryCode)) .findFirst(); ibanCountry.ifPresent(countryComboBox::setValue); } }); countryComboBox.valueProperty().addListener((ov, oldValue, newValue) -> { sepaIBANValidator.setRestrictToCountry(newValue.code); ibanInputTextField.refreshValidation(); }); updateFromInputs(); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && bicValidator.validate(sepaAccount.getBic()).isValid && sepaIBANValidator.validate(sepaAccount.getIban()).isValid && inputValidator.validate(sepaAccount.getHolderName()).isValid && sepaAccount.getAcceptedCountryCodes().size() > 0 && sepaAccount.getSingleTradeCurrency() != null && sepaAccount.getCountry() != null); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(sepaAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), sepaAccount.getHolderName()); addCompactTopLabelTextField(gridPane, ++gridRow, IBAN, sepaAccount.getIban()).second.setMouseTransparent(false); addCompactTopLabelTextField(gridPane, ++gridRow, BIC, sepaAccount.getBic()).second.setMouseTransparent(false); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.bank.country"), sepaAccount.getCountry() != null ? sepaAccount.getCountry().name : ""); TradeCurrency singleTradeCurrency = sepaAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addCountriesGrid(Res.get("payment.accept.euro"), CountryUtil.getAllSepaEuroCountries()); addCountriesGrid(Res.get("payment.accept.nonEuro"), CountryUtil.getAllSepaNonEuroCountries()); addLimitations(true); } @Override void removeAcceptedCountry(String countryCode) { sepaAccount.removeAcceptedCountry(countryCode); } @Override void addAcceptedCountry(String countryCode) { sepaAccount.addAcceptedCountry(countryCode); } @Override boolean isCountryAccepted(String countryCode) { return sepaAccount.getAcceptedCountryCodes().contains(countryCode); } @Override protected String getIban() { return sepaAccount.getIban(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/SepaInstantForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.PaymentAccount; import haveno.core.payment.SepaInstantAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.SepaInstantAccountPayload; import haveno.core.payment.validation.BICValidator; import haveno.core.payment.validation.SepaIBANValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.normalization.IBANNormalizer; import javafx.collections.FXCollections; import javafx.scene.control.ComboBox; import javafx.scene.control.TextFormatter; import javafx.scene.layout.GridPane; import java.util.List; import java.util.Optional; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; public class SepaInstantForm extends GeneralSepaForm { public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { SepaInstantAccountPayload sepaInstantAccountPayload = (SepaInstantAccountPayload) paymentAccountPayload; final String title = Res.get("payment.account.owner.fullname"); final String value = sepaInstantAccountPayload.getHolderName(); addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, title, value); addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.bank.country"), CountryUtil.getNameAndCode(sepaInstantAccountPayload.getCountryCode())); // IBAN, BIC will not be translated addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, IBAN, sepaInstantAccountPayload.getIban()); addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, BIC, sepaInstantAccountPayload.getBic()); return gridRow; } private final SepaInstantAccount sepaInstantAccount; private final SepaIBANValidator sepaIBANValidator; private final BICValidator bicValidator; public SepaInstantForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, BICValidator bicValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.sepaInstantAccount = (SepaInstantAccount) paymentAccount; this.sepaIBANValidator = new SepaIBANValidator(); this.bicValidator = bicValidator; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname")); holderNameInputTextField.setValidator(inputValidator); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { sepaInstantAccount.setHolderName(newValue); updateFromInputs(); }); InputTextField ibanInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, IBAN); ibanInputTextField.setTextFormatter(new TextFormatter<>(new IBANNormalizer())); ibanInputTextField.setValidator(sepaIBANValidator); InputTextField bicInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, BIC); bicInputTextField.setValidator(bicValidator); bicInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { sepaInstantAccount.setBic(newValue); updateFromInputs(); }); ComboBox countryComboBox = addCountrySelection(); setCountryComboBoxAction(countryComboBox, sepaInstantAccount); addCountriesGrid(Res.get("payment.accept.euro"), CountryUtil.getAllSepaEuroCountries()); addCountriesGrid(Res.get("payment.accept.nonEuro"), CountryUtil.getAllSepaNonEuroCountries()); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); countryComboBox.setItems(FXCollections.observableArrayList(CountryUtil.getAllSepaInstantCountries())); Country country = CountryUtil.getDefaultCountry(); if (CountryUtil.getAllSepaInstantCountries().contains(country)) { countryComboBox.getSelectionModel().select(country); sepaInstantAccount.setCountry(country); } ibanInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { sepaInstantAccount.setIban(newValue); updateFromInputs(); if (ibanInputTextField.validate()) { List countries = CountryUtil.getAllSepaCountries(); String ibanCountryCode = newValue.substring(0, 2).toUpperCase(); Optional ibanCountry = countries .stream() .filter(c -> c.code.equals(ibanCountryCode)) .findFirst(); if (ibanCountry.isPresent()) { countryComboBox.setValue(ibanCountry.get()); } } }); countryComboBox.valueProperty().addListener((ov, oldValue, newValue) -> { sepaIBANValidator.setRestrictToCountry(newValue.code); ibanInputTextField.refreshValidation(); }); updateFromInputs(); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && bicValidator.validate(sepaInstantAccount.getBic()).isValid && sepaIBANValidator.validate(sepaInstantAccount.getIban()).isValid && inputValidator.validate(sepaInstantAccount.getHolderName()).isValid && sepaInstantAccount.getAcceptedCountryCodes().size() > 0 && sepaInstantAccount.getSingleTradeCurrency() != null && sepaInstantAccount.getCountry() != null); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(sepaInstantAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), sepaInstantAccount.getHolderName()); addCompactTopLabelTextField(gridPane, ++gridRow, IBAN, sepaInstantAccount.getIban()).second.setMouseTransparent(false); addCompactTopLabelTextField(gridPane, ++gridRow, BIC, sepaInstantAccount.getBic()).second.setMouseTransparent(false); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.bank.country"), sepaInstantAccount.getCountry() != null ? sepaInstantAccount.getCountry().name : ""); TradeCurrency singleTradeCurrency = sepaInstantAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addCountriesGrid(Res.get("payment.accept.euro"), CountryUtil.getAllSepaEuroCountries()); addCountriesGrid(Res.get("payment.accept.nonEuro"), CountryUtil.getAllSepaNonEuroCountries()); addLimitations(true); } @Override void removeAcceptedCountry(String countryCode) { sepaInstantAccount.removeAcceptedCountry(countryCode); } @Override void addAcceptedCountry(String countryCode) { sepaInstantAccount.addAcceptedCountry(countryCode); } @Override boolean isCountryAccepted(String countryCode) { return sepaInstantAccount.getAcceptedCountryCodes().contains(countryCode); } @Override protected String getIban() { return sepaInstantAccount.getIban(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/SpecificBankForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import com.google.common.base.Joiner; import haveno.common.util.Tuple3; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.SpecificBanksAccountPayload; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import javafx.beans.binding.Bindings; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addTopLabelInputTextFieldButton; import static haveno.desktop.util.FormBuilder.addTopLabelTextFieldButton; public class SpecificBankForm extends BankForm { private final SpecificBanksAccountPayload specificBanksAccountPayload; private TextField acceptedBanksTextField; private Tooltip acceptedBanksTooltip; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { return BankForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); } public SpecificBankForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.specificBanksAccountPayload = (SpecificBanksAccountPayload) paymentAccount.paymentAccountPayload; } @Override protected void addAcceptedBanksForAddAccount() { Tuple3 addBankTuple = addTopLabelInputTextFieldButton(gridPane, ++gridRow, Res.get("payment.nameOfAcceptedBank"), Res.get("payment.addAcceptedBank")); InputTextField addBankInputTextField = addBankTuple.second; Button addButton = addBankTuple.third; addButton.setMinWidth(200); addButton.disableProperty().bind(Bindings.createBooleanBinding(() -> addBankInputTextField.getText().isEmpty(), addBankInputTextField.textProperty())); Tuple3 acceptedBanksTuple = addTopLabelTextFieldButton(gridPane, ++gridRow, Res.get("payment.accepted.banks"), Res.get("payment.clearAcceptedBanks")); acceptedBanksTextField = acceptedBanksTuple.second; acceptedBanksTextField.setMouseTransparent(false); acceptedBanksTooltip = new Tooltip(); acceptedBanksTextField.setTooltip(acceptedBanksTooltip); Button clearButton = acceptedBanksTuple.third; clearButton.setMinWidth(200); clearButton.setDefaultButton(false); clearButton.disableProperty().bind(Bindings.createBooleanBinding(() -> acceptedBanksTextField.getText().isEmpty(), acceptedBanksTextField.textProperty())); addButton.setOnAction(e -> { specificBanksAccountPayload.addAcceptedBank(addBankInputTextField.getText()); addBankInputTextField.setText(""); String value = Joiner.on(", ").join(specificBanksAccountPayload.getAcceptedBanks()); acceptedBanksTextField.setText(value); acceptedBanksTooltip.setText(value); updateAllInputsValid(); }); clearButton.setOnAction(e -> resetAcceptedBanks()); } private void resetAcceptedBanks() { specificBanksAccountPayload.clearAcceptedBanks(); acceptedBanksTextField.setText(""); acceptedBanksTooltip.setText(""); updateAllInputsValid(); } @Override protected void onCountryChanged() { resetAcceptedBanks(); } @Override public void addAcceptedBanksForDisplayAccount() { addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.accepted.banks"), Joiner.on(", ").join(specificBanksAccountPayload.getAcceptedBanks())).second.setMouseTransparent(false); } @Override public void updateAllInputsValid() { super.updateAllInputsValid(); allInputsValid.set(allInputsValid.get() && inputValidator.validate(acceptedBanksTextField.getText()).isValid); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/StrikeForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import haveno.core.payment.PaymentAccount; import haveno.core.payment.StrikeAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.StrikeAccountPayload; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.Layout; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextFieldWithCopyIcon; public class StrikeForm extends PaymentMethodForm { private final StrikeAccount account; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.account.username"), ((StrikeAccountPayload) paymentAccountPayload).getHolderName(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); return gridRow; } public StrikeForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.account = (StrikeAccount) paymentAccount; } @Override public void addFormForAddAccount() { // this payment method is currently restricted to United States/USD CountryUtil.findCountryByCode("US").ifPresent(account::setCountry); gridRowFrom = gridRow + 1; InputTextField holderNameField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.username")); holderNameField.setValidator(inputValidator); holderNameField.textProperty().addListener((ov, oldValue, newValue) -> { account.setHolderName(newValue.trim()); updateFromInputs(); }); addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(account.getHolderName()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.username"), account.getHolderName()).second; field.setMouseTransparent(false); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && inputValidator.validate(account.getHolderName()).isValid); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/SwiftForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import com.jfoenix.controls.JFXTextArea; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import haveno.core.payment.PaymentAccount; import haveno.core.payment.SwiftAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.SwiftAccountPayload; import haveno.core.payment.validation.LengthValidator; import haveno.core.trade.Trade; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipCheckBox; import haveno.desktop.components.InputTextField; import haveno.desktop.main.overlays.windows.SwiftPaymentDetails; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.Layout; import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TextArea; import javafx.scene.control.TitledPane; import javafx.scene.layout.GridPane; import java.util.function.Consumer; import static haveno.common.util.Utilities.cleanString; import static haveno.core.payment.payload.SwiftAccountPayload.ADDRESS; import static haveno.core.payment.payload.SwiftAccountPayload.BANKPOSTFIX; import static haveno.core.payment.payload.SwiftAccountPayload.BENEFICIARYPOSTFIX; import static haveno.core.payment.payload.SwiftAccountPayload.BRANCH; import static haveno.core.payment.payload.SwiftAccountPayload.COUNTRY; import static haveno.core.payment.payload.SwiftAccountPayload.INTERMEDIARYPOSTFIX; import static haveno.core.payment.payload.SwiftAccountPayload.PHONE; import static haveno.core.payment.payload.SwiftAccountPayload.SNAME; import static haveno.core.payment.payload.SwiftAccountPayload.SWIFT_ACCOUNT; import static haveno.core.payment.payload.SwiftAccountPayload.SWIFT_CODE; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addInputTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextArea; public class SwiftForm extends PaymentMethodForm { private final SwiftAccountPayload formData; private final AutoTooltipCheckBox useIntermediaryCheck; private final LengthValidator defaultValidator = new LengthValidator(2, 34); private final LengthValidator swiftValidator = new LengthValidator(11, 11); private final LengthValidator accountNrValidator = new LengthValidator(2, 40); private final LengthValidator addressValidator = new LengthValidator(1, 100); public SwiftForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator defaultValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, defaultValidator, gridPane, gridRow, formatter); this.formData = ((SwiftAccount) paymentAccount).getPayload(); this.useIntermediaryCheck = new AutoTooltipCheckBox(Res.get("payment.swift.use.intermediary")); } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; addFieldsForBankEdit(true, this::setBankSwiftCode, this::setBankName, this::setBankBranch, this::setBankAddress); addFieldsForBankEdit(false, this::setIntermediarySwiftCode, this::setIntermediaryName, this::setIntermediaryBranch, this::setIntermediaryAddress); addFieldsForBeneficiaryEdit(); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(formData.getBeneficiaryName()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(paymentAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(SWIFT_CODE + BANKPOSTFIX), formData.getBankSwiftCode()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(COUNTRY + BANKPOSTFIX), CountryUtil.getNameAndCode(formData.getBankCountryCode())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(SNAME + BANKPOSTFIX), formData.getBankName()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(BRANCH + BANKPOSTFIX), formData.getBankBranch()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(ADDRESS + BANKPOSTFIX), cleanString(formData.getBankAddress())); if (formData.usesIntermediaryBank()) { addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(SWIFT_CODE + INTERMEDIARYPOSTFIX), formData.getIntermediarySwiftCode(), Layout.GROUP_DISTANCE_WITHOUT_SEPARATOR); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(COUNTRY + INTERMEDIARYPOSTFIX), CountryUtil.getNameAndCode(formData.getIntermediaryCountryCode())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(SNAME + INTERMEDIARYPOSTFIX), formData.getIntermediaryName()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(BRANCH + INTERMEDIARYPOSTFIX), formData.getIntermediaryBranch()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(ADDRESS + INTERMEDIARYPOSTFIX), cleanString(formData.getIntermediaryAddress())); } addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), formData.getBeneficiaryName(), Layout.GROUP_DISTANCE_WITHOUT_SEPARATOR); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(SWIFT_ACCOUNT), formData.getBeneficiaryAccountNr()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(ADDRESS + BENEFICIARYPOSTFIX), cleanString(formData.getBeneficiaryAddress())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get(PHONE + BENEFICIARYPOSTFIX), formData.getBeneficiaryPhone()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.city"), formData.getBeneficiaryCity()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"), CountryUtil.getNameAndCode(formData.getBankCountryCode())); // same as receiving bank country addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), cleanString(formData.getSpecialInstructions())); gridPane.add(new Label(""), 0, ++gridRow); // spacer addLimitations(true); } @Override public void updateAllInputsValid() { SwiftAccountPayload data = formData; // intermediary bank details are optional, but if specified must be valid boolean intermediaryValidIfSpecified = !useIntermediaryCheck.isSelected() && !data.usesIntermediaryBank() || data.usesIntermediaryBank() && (swiftValidator.validate(data.getIntermediarySwiftCode()).isValid && defaultValidator.validate(data.getIntermediaryCountryCode()).isValid && defaultValidator.validate(data.getIntermediaryName()).isValid && defaultValidator.validate(data.getIntermediaryBranch()).isValid && addressValidator.validate(data.getIntermediaryAddress()).isValid ); allInputsValid.set(isAccountNameValid() && swiftValidator.validate(data.getBankSwiftCode()).isValid && defaultValidator.validate(data.getBankCountryCode()).isValid && defaultValidator.validate(data.getBankName()).isValid && defaultValidator.validate(data.getBankBranch()).isValid && addressValidator.validate(data.getBankAddress()).isValid && defaultValidator.validate(data.getBeneficiaryName()).isValid && accountNrValidator.validate(data.getBeneficiaryAccountNr()).isValid && addressValidator.validate(data.getBeneficiaryAddress()).isValid && defaultValidator.validate(data.getBeneficiaryPhone()).isValid && defaultValidator.validate(data.getBeneficiaryCity()).isValid && paymentAccount.getTradeCurrencies().size() > 0 && intermediaryValidIfSpecified); } // Here we need to show information to buyer so they can make the fiat payment, however there is only enough space // on the trade screen for ~4 fields. // Since SWIFT has an unusually large number of fields, it will be better to offer a button which will show // the SWIFT information in a popup screen. public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload, Trade trade) { SwiftAccountPayload swiftAccountPayload = (SwiftAccountPayload) paymentAccountPayload; Button button = new AutoTooltipButton(Res.get("payment.swift.showPaymentInfo")); GridPane.setRowIndex(button, gridRow); GridPane.setColumnIndex(button, 1); gridPane.getChildren().add(button); GridPane.setMargin(button, new Insets(Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, Layout.FLOATING_LABEL_DISTANCE)); button.setOnAction((e) -> new SwiftPaymentDetails(swiftAccountPayload, trade).show()); return gridRow; } private void addFieldsForBankEdit(boolean isPrimary, Consumer onSwiftCodeSelected, Consumer onNameSelected, Consumer onBranchSelected, Consumer onAddressSelected) { GridPane gridPane2 = new GridPane(); gridPane2.getColumnConstraints().add(gridPane.getColumnConstraints().get(0)); TitledPane titledPane = new TitledPane(isPrimary ? Res.get("payment.swift.title" + BANKPOSTFIX) : Res.get("payment.swift.title" + INTERMEDIARYPOSTFIX), gridPane2); titledPane.setExpanded(isPrimary); gridPane.add(titledPane, 0, ++gridRow); int gridRow2 = 0; if (!isPrimary) { // secondary bank (optional) has a checkbox to specify if it is being used gridPane2.add(useIntermediaryCheck, 0, ++gridRow2); } String label = isPrimary ? Res.get(SWIFT_CODE + BANKPOSTFIX) : Res.get(SWIFT_CODE + INTERMEDIARYPOSTFIX); InputTextField bankSwiftCodeField = addInputTextField(gridPane2, ++gridRow2, label); bankSwiftCodeField.setPromptText(label); bankSwiftCodeField.setValidator(swiftValidator); bankSwiftCodeField.textProperty().addListener((ov, oldValue, newValue) -> onSwiftCodeSelected.accept(newValue)); if (isPrimary) { gridRow2 = GUIUtil.addRegionCountry(gridPane2, gridRow2, this::setBankCountry); } else { gridRow2 = GUIUtil.addRegionCountry(gridPane2, ++gridRow2, this::setIntermediaryCountry); } label = isPrimary ? Res.get(SNAME + BANKPOSTFIX) : Res.get(SNAME + INTERMEDIARYPOSTFIX); InputTextField bankNameField = addInputTextField(gridPane2, ++gridRow2, label); bankNameField.setPromptText(label); bankNameField.setValidator(defaultValidator); bankNameField.textProperty().addListener((ov, oldValue, newValue) -> onNameSelected.accept(newValue)); label = isPrimary ? Res.get(BRANCH + BANKPOSTFIX) : Res.get(BRANCH + INTERMEDIARYPOSTFIX); InputTextField bankBranchField = addInputTextField(gridPane2, ++gridRow2, label); bankBranchField.setPromptText(label); bankBranchField.setValidator(defaultValidator); bankBranchField.textProperty().addListener((ov, oldValue, newValue) -> onBranchSelected.accept(newValue)); label = isPrimary ? Res.get(ADDRESS + BANKPOSTFIX) : Res.get(ADDRESS + INTERMEDIARYPOSTFIX); TextArea bankAddressTextArea = addTopLabelTextArea(gridPane2, ++gridRow2, label, label).second; bankAddressTextArea.setMinHeight(70); bankAddressTextArea.textProperty().addListener((ov, oldValue, newValue) -> onAddressSelected.accept(newValue)); // intermediary bank can be enabled/disabled via checkbox if (!isPrimary) { useIntermediaryCheck.setOnAction((e) -> { for (Node x : gridPane2.getChildren()) { if (x == useIntermediaryCheck) continue; x.setDisable(!useIntermediaryCheck.isSelected()); } if (!useIntermediaryCheck.isSelected()) { bankSwiftCodeField.setText(""); bankNameField.setText(""); bankBranchField.setText(""); bankAddressTextArea.setText(""); } updateFromInputs(); }); // make the intermediary fields initially greyed out for (Node x : gridPane2.getChildren()) { if (x == useIntermediaryCheck) continue; x.setDisable(!useIntermediaryCheck.isSelected()); } } } private void addFieldsForBeneficiaryEdit() { String label = Res.get("payment.account.owner.fullname"); InputTextField beneficiaryNameField = addInputTextField(gridPane, ++gridRow, label); beneficiaryNameField.setPromptText(label); beneficiaryNameField.setValidator(defaultValidator); beneficiaryNameField.textProperty().addListener((ov, oldValue, newValue) -> { formData.setBeneficiaryName(newValue.trim()); updateFromInputs(); }); label = Res.get(SWIFT_ACCOUNT); InputTextField beneficiaryAccountNrField = addInputTextField(gridPane, ++gridRow, label); beneficiaryAccountNrField.setPromptText(label); beneficiaryAccountNrField.setValidator(defaultValidator); beneficiaryAccountNrField.setValidator(accountNrValidator); beneficiaryAccountNrField.textProperty().addListener((ov, oldValue, newValue) -> { formData.setBeneficiaryAccountNr(newValue.trim()); updateFromInputs(); }); label = Res.get(ADDRESS + BENEFICIARYPOSTFIX); TextArea beneficiaryAddressTextArea = addTopLabelTextArea(gridPane, ++gridRow, label, label).second; beneficiaryAddressTextArea.setMinHeight(70); beneficiaryAddressTextArea.textProperty().addListener((ov, oldValue, newValue) -> { formData.setBeneficiaryAddress(newValue.trim()); updateFromInputs(); }); label = Res.get("payment.account.city"); InputTextField beneficiaryCityField = addInputTextField(gridPane, ++gridRow, label); beneficiaryCityField.setPromptText(label); beneficiaryCityField.setValidator(defaultValidator); beneficiaryCityField.textProperty().addListener((ov, oldValue, newValue) -> { formData.setBeneficiaryCity(newValue.trim()); updateFromInputs(); }); label = Res.get(PHONE + BENEFICIARYPOSTFIX); InputTextField beneficiaryPhoneField = addInputTextField(gridPane, ++gridRow, label); beneficiaryPhoneField.setPromptText(label); beneficiaryPhoneField.setValidator(defaultValidator); beneficiaryPhoneField.textProperty().addListener((ov, oldValue, newValue) -> { formData.setBeneficiaryPhone(newValue.trim()); updateFromInputs(); }); label = Res.get("payment.shared.optionalExtra"); TextArea extraTextArea = addTopLabelTextArea(gridPane, ++gridRow, label, label).second; extraTextArea.setMinHeight(70); ((JFXTextArea) extraTextArea).setLabelFloat(false); extraTextArea.textProperty().addListener((ov, oldValue, newValue) -> { formData.setSpecialInstructions(newValue.trim()); updateFromInputs(); }); } private void setBankSwiftCode(String value) { formData.setBankSwiftCode(value.trim()); updateFromInputs(); } private void setBankName(String value) { formData.setBankName(value.trim()); updateFromInputs(); } private void setBankBranch(String value) { formData.setBankBranch(value.trim()); updateFromInputs(); } private void setBankAddress(String value) { formData.setBankAddress(value.trim()); updateFromInputs(); } private void setIntermediarySwiftCode(String value) { formData.setIntermediarySwiftCode(value.trim()); updateFromInputs(); } private void setIntermediaryName(String value) { formData.setIntermediaryName(value.trim()); updateFromInputs(); } private void setIntermediaryBranch(String value) { formData.setIntermediaryBranch(value.trim()); updateFromInputs(); } private void setIntermediaryAddress(String value) { formData.setIntermediaryAddress(value.trim()); updateFromInputs(); } private void setBankCountry(Country country) { if (country == null) return; formData.setBankCountryCode(country.code); updateFromInputs(); } private void setIntermediaryCountry(Country country) { if (country == null) return; formData.setIntermediaryCountryCode(country.code); updateFromInputs(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/SwishForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.PaymentAccount; import haveno.core.payment.SwishAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.SwishAccountPayload; import haveno.core.payment.validation.SwishValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import lombok.extern.slf4j.Slf4j; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; @Slf4j public class SwishForm extends PaymentMethodForm { private final SwishAccount swishAccount; private final SwishValidator swishValidator; public SwishForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, SwishValidator swishValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.swishAccount = (SwishAccount) paymentAccount; this.swishValidator = swishValidator; } public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), ((SwishAccountPayload) paymentAccountPayload).getHolderName()); addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.mobile"), ((SwishAccountPayload) paymentAccountPayload).getMobileNr()); return gridRow; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname")); holderNameInputTextField.setValidator(inputValidator); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { swishAccount.setHolderName(newValue); updateFromInputs(); }); InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.mobile")); mobileNrInputTextField.setValidator(swishValidator); mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { swishAccount.setMobileNr(newValue); updateFromInputs(); }); TradeCurrency singleTradeCurrency = swishAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(swishAccount.getMobileNr()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(swishAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), swishAccount.getHolderName()); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.mobile"), swishAccount.getMobileNr()).second; field.setMouseTransparent(false); TradeCurrency singleTradeCurrency = swishAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(true); } @Override public void updateAllInputsValid() { if (swishValidator.validate(swishAccount.getMobileNr()).isValid) { swishAccount.setMobileNr(swishValidator.getNormalizedPhoneNumber()); } allInputsValid.set(isAccountNameValid() && swishValidator.validate(swishAccount.getMobileNr()).isValid && inputValidator.validate(swishAccount.getHolderName()).isValid && swishAccount.getTradeCurrencies().size() > 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/TikkieForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import haveno.core.payment.PaymentAccount; import haveno.core.payment.TikkieAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.TikkieAccountPayload; import haveno.core.payment.validation.IBANValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; public class TikkieForm extends PaymentMethodForm { private final TikkieAccount account; private final IBANValidator ibanValidator = new IBANValidator("NL"); public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, 0, Res.get("payment.tikkie.iban"), ((TikkieAccountPayload) paymentAccountPayload).getIban()); return gridRow; } public TikkieForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.account = (TikkieAccount) paymentAccount; } @Override public void addFormForAddAccount() { // this payment method is only for Netherlands/EUR CountryUtil.findCountryByCode("NL").ifPresent(account::setCountry); gridRowFrom = gridRow + 1; InputTextField ibanField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.tikkie.iban")); ibanField.setValidator(ibanValidator); ibanField.textProperty().addListener((ov, oldValue, newValue) -> { account.setIban(newValue.trim()); updateFromInputs(); }); addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(account.getIban()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.tikkie.iban"), account.getIban()) .second.setMouseTransparent(false); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && ibanValidator.validate(account.getIban()).isValid); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/TransferwiseForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.payment.PaymentAccount; import haveno.core.payment.TransferwiseAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.TransferwiseAccountPayload; import haveno.core.payment.validation.TransferwiseValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import javafx.scene.control.TextField; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; public class TransferwiseForm extends PaymentMethodForm { private final TransferwiseAccount account; private final TransferwiseValidator validator; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email"), ((TransferwiseAccountPayload) paymentAccountPayload).getEmail()); addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), PaymentAccountPayload.getHolderNameOrPromptIfEmpty(((TransferwiseAccountPayload) paymentAccountPayload).getHolderName())); return gridRow; } public TransferwiseForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, TransferwiseValidator validator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.account = (TransferwiseAccount) paymentAccount; this.validator = validator; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField emailInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email")); emailInputTextField.setValidator(validator); emailInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setEmail(newValue.trim()); updateFromInputs(); }); InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname")); holderNameInputTextField.setValidator(inputValidator); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setHolderName(newValue); updateFromInputs(); }); addCurrenciesGrid(true); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } private void addCurrenciesGrid(boolean isEditable) { FlowPane flowPane = FormBuilder.addTopLabelFlowPane(gridPane, ++gridRow, Res.get("payment.supportedCurrenciesForReceiver"), 20, 20).second; if (isEditable) { flowPane.setId("flow-pane-checkboxes-bg"); } else { flowPane.setId("flow-pane-checkboxes-non-editable-bg"); } account.getSupportedCurrencies().forEach(currency -> fillUpFlowPaneWithCurrencies(isEditable, flowPane, currency, account)); } @Override protected void autoFillNameTextField() { setAccountNameWithString(account.getEmail()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), account.getEmail()).second; field.setMouseTransparent(false); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), account.getHolderName()); addLimitations(true); addCurrenciesGrid(false); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && validator.validate(account.getEmail()).isValid && inputValidator.validate(account.getHolderName()).isValid && account.getTradeCurrencies().size() > 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/TransferwiseUsdForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import haveno.core.payment.PaymentAccount; import haveno.core.payment.TransferwiseUsdAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.TransferwiseUsdAccountPayload; import haveno.core.payment.validation.EmailValidator; import haveno.core.payment.validation.LengthValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.Layout; import javafx.scene.control.TextArea; import javafx.scene.layout.GridPane; import static haveno.common.util.Utilities.cleanString; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextArea; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addInputTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextArea; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextFieldWithCopyIcon; public class TransferwiseUsdForm extends PaymentMethodForm { private final TransferwiseUsdAccount account; private final LengthValidator addressValidator = new LengthValidator(0, 100); private final EmailValidator emailValidator = new EmailValidator(); public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.account.owner.fullname"), ((TransferwiseUsdAccountPayload) paymentAccountPayload).getHolderName(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, 1, Res.get("payment.email"), ((TransferwiseUsdAccountPayload) paymentAccountPayload).getEmail()); String address = ((TransferwiseUsdAccountPayload) paymentAccountPayload).getHolderAddress(); if (address.length() > 0) { TextArea textAddress = addCompactTopLabelTextArea(gridPane, gridRow, 0, Res.get("payment.account.address"), "").second; textAddress.setMinHeight(70); textAddress.setEditable(false); textAddress.setText(address); } return gridRow; } public TransferwiseUsdForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.account = (TransferwiseUsdAccount) paymentAccount; } @Override public void addFormForAddAccount() { CountryUtil.findCountryByCode("US").ifPresent(account::setCountry); gridRowFrom = gridRow + 1; InputTextField emailField = addInputTextField(gridPane, ++gridRow, Res.get("payment.email")); emailField.setValidator(emailValidator); emailField.textProperty().addListener((ov, oldValue, newValue) -> { account.setEmail(newValue.trim()); updateFromInputs(); }); InputTextField holderNameField = addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname")); holderNameField.setValidator(inputValidator); holderNameField.textProperty().addListener((ov, oldValue, newValue) -> { account.setHolderName(newValue.trim()); updateFromInputs(); }); String addressLabel = Res.get("payment.account.owner.address") + " " + Res.get("payment.transferwiseUsd.address"); TextArea addressTextArea = addTopLabelTextArea(gridPane, ++gridRow, addressLabel, addressLabel).second; addressTextArea.setMinHeight(70); addressTextArea.textProperty().addListener((ov, oldValue, newValue) -> { account.setBeneficiaryAddress(newValue.trim()); updateFromInputs(); }); addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(account.getHolderName()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), account.getEmail()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), account.getHolderName()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.address"), cleanString(account.getBeneficiaryAddress())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && emailValidator.validate(account.getEmail()).isValid && inputValidator.validate(account.getHolderName()).isValid && addressValidator.validate(account.getBeneficiaryAddress()).isValid ); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/USPostalMoneyOrderForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.PaymentAccount; import haveno.core.payment.USPostalMoneyOrderAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.USPostalMoneyOrderAccountPayload; import haveno.core.payment.validation.USPostalMoneyOrderValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import javafx.scene.control.TextArea; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextArea; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addTopLabelTextArea; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; public class USPostalMoneyOrderForm extends PaymentMethodForm { private final USPostalMoneyOrderAccount usPostalMoneyOrderAccount; private final USPostalMoneyOrderValidator usPostalMoneyOrderValidator; private TextArea postalAddressTextArea; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), ((USPostalMoneyOrderAccountPayload) paymentAccountPayload).getHolderName()); TextArea textArea = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.postal.address"), "").second; textArea.setMinHeight(70); textArea.setEditable(false); textArea.setId("text-area-disabled"); textArea.setText(((USPostalMoneyOrderAccountPayload) paymentAccountPayload).getPostalAddress()); return gridRow; } public USPostalMoneyOrderForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, USPostalMoneyOrderValidator usPostalMoneyOrderValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.usPostalMoneyOrderAccount = (USPostalMoneyOrderAccount) paymentAccount; this.usPostalMoneyOrderValidator = usPostalMoneyOrderValidator; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname")); holderNameInputTextField.setValidator(inputValidator); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { usPostalMoneyOrderAccount.setHolderName(newValue); updateFromInputs(); }); postalAddressTextArea = addTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.postal.address"), "").second; postalAddressTextArea.setMinHeight(70); //postalAddressTextArea.setValidator(usPostalMoneyOrderValidator); postalAddressTextArea.textProperty().addListener((ov, oldValue, newValue) -> { usPostalMoneyOrderAccount.setPostalAddress(newValue); updateFromInputs(); }); TradeCurrency singleTradeCurrency = usPostalMoneyOrderAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(usPostalMoneyOrderAccount.getPostalAddress()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(usPostalMoneyOrderAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), usPostalMoneyOrderAccount.getHolderName()); TextArea textArea = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.postal.address"), "").second; textArea.setText(usPostalMoneyOrderAccount.getPostalAddress()); textArea.setMinHeight(70); textArea.setEditable(false); TradeCurrency singleTradeCurrency = usPostalMoneyOrderAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && usPostalMoneyOrderValidator.validate(usPostalMoneyOrderAccount.getPostalAddress()).isValid && !usPostalMoneyOrderAccount.getPostalAddress().isEmpty() && inputValidator.validate(usPostalMoneyOrderAccount.getHolderName()).isValid && usPostalMoneyOrderAccount.getTradeCurrencies().size() > 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/UpholdForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.payment.PaymentAccount; import haveno.core.payment.UpholdAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.UpholdAccountPayload; import haveno.core.payment.validation.UpholdValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import javafx.scene.control.TextField; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; public class UpholdForm extends PaymentMethodForm { private final UpholdAccount upholdAccount; private final UpholdValidator upholdValidator; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { String accountOwner = ((UpholdAccountPayload) paymentAccountPayload).getAccountOwner(); if (accountOwner.isEmpty()) { accountOwner = Res.get("payment.ask"); } addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), accountOwner); addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.uphold.accountId"), ((UpholdAccountPayload) paymentAccountPayload).getAccountId()); return gridRow; } public UpholdForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, UpholdValidator upholdValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.upholdAccount = (UpholdAccount) paymentAccount; this.upholdValidator = upholdValidator; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname")); holderNameInputTextField.setValidator(inputValidator); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { upholdAccount.setAccountOwner(newValue); updateFromInputs(); }); InputTextField accountIdInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.uphold.accountId")); accountIdInputTextField.setValidator(upholdValidator); accountIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { upholdAccount.setAccountId(newValue.trim()); updateFromInputs(); }); addCurrenciesGrid(true); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } private void addCurrenciesGrid(boolean isEditable) { FlowPane flowPane = FormBuilder.addTopLabelFlowPane(gridPane, ++gridRow, Res.get("payment.supportedCurrencies"), 0).second; if (isEditable) flowPane.setId("flow-pane-checkboxes-bg"); else flowPane.setId("flow-pane-checkboxes-non-editable-bg"); paymentAccount.getSupportedCurrencies().forEach(e -> fillUpFlowPaneWithCurrencies(isEditable, flowPane, e, upholdAccount)); } @Override protected void autoFillNameTextField() { setAccountNameWithString(upholdAccount.getAccountId()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(upholdAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), upholdAccount.getAccountOwner()); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.uphold.accountId"), upholdAccount.getAccountId()).second; field.setMouseTransparent(false); addLimitations(true); addCurrenciesGrid(false); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && upholdValidator.validate(upholdAccount.getAccountId()).isValid && upholdAccount.getTradeCurrencies().size() > 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/UpiForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import haveno.core.payment.PaymentAccount; import haveno.core.payment.UpiAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.UpiAccountPayload; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; public class UpiForm extends PaymentMethodForm { private final UpiAccount account; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.upi.virtualPaymentAddress"), ((UpiAccountPayload) paymentAccountPayload).getVirtualPaymentAddress()); return gridRow; } public UpiForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.account = (UpiAccount) paymentAccount; } @Override public void addFormForAddAccount() { // this payment method is only for India/INR account.setSingleTradeCurrency(account.getSupportedCurrencies().get(0)); CountryUtil.findCountryByCode("IN").ifPresent(c -> account.setCountry(c)); gridRowFrom = gridRow + 1; InputTextField virtualPaymentAddressInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.upi.virtualPaymentAddress")); virtualPaymentAddressInputTextField.setValidator(inputValidator); virtualPaymentAddressInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setVirtualPaymentAddress(newValue.trim()); updateFromInputs(); }); addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(account.getVirtualPaymentAddress()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.upi.virtualPaymentAddress"), account.getVirtualPaymentAddress()).second; field.setMouseTransparent(false); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && inputValidator.validate(account.getVirtualPaymentAddress()).isValid); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/VenmoForm.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.VenmoAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.VenmoAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.validation.EmailOrMobileNrOrUsernameValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; public class VenmoForm extends PaymentMethodForm { private final VenmoAccount venmoAccount; private final EmailOrMobileNrOrUsernameValidator venmoValidator; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email.mobile.username"), ((VenmoAccountPayload) paymentAccountPayload).getEmailOrMobileNrOrUsername()); return gridRow; } public VenmoForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, EmailOrMobileNrOrUsernameValidator venmoValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.venmoAccount = (VenmoAccount) paymentAccount; this.venmoValidator = venmoValidator; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email.mobile.username")); mobileNrInputTextField.setValidator(venmoValidator); mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { venmoAccount.setNameOrUsernameOrEmailOrMobileNr(newValue.trim()); updateFromInputs(); }); final TradeCurrency singleTradeCurrency = venmoAccount.getSingleTradeCurrency(); final String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(venmoAccount.getNameOrUsernameOrEmailOrMobileNr()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(venmoAccount.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email.mobile.username"), venmoAccount.getNameOrUsernameOrEmailOrMobileNr()).second; field.setMouseTransparent(false); final TradeCurrency singleTradeCurrency = venmoAccount.getSingleTradeCurrency(); final String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && venmoValidator.validate(venmoAccount.getNameOrUsernameOrEmailOrMobileNr()).isValid && venmoAccount.getTradeCurrencies().size() > 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/VerseForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.payment.PaymentAccount; import haveno.core.payment.VerseAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.VerseAccountPayload; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import javafx.scene.control.TextField; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; public class VerseForm extends PaymentMethodForm { private final VerseAccount account; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.username"), ((VerseAccountPayload) paymentAccountPayload).getHolderName()); return gridRow; } public VerseForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.account = (VerseAccount) paymentAccount; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.username")); holderNameInputTextField.setValidator(inputValidator); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { account.setHolderName(newValue.trim()); updateFromInputs(); }); addCurrenciesGrid(true); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } private void addCurrenciesGrid(boolean isEditable) { FlowPane flowPane = FormBuilder.addTopLabelFlowPane(gridPane, ++gridRow, Res.get("payment.supportedCurrencies"), 20, 20).second; if (isEditable) { flowPane.setId("flow-pane-checkboxes-bg"); } else { flowPane.setId("flow-pane-checkboxes-non-editable-bg"); } paymentAccount.getSupportedCurrencies().forEach(currency -> fillUpFlowPaneWithCurrencies(isEditable, flowPane, currency, account)); } @Override protected void autoFillNameTextField() { setAccountNameWithString(account.getHolderName()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.username"), account.getHolderName()).second; field.setMouseTransparent(false); addLimitations(true); addCurrenciesGrid(false); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && account.getHolderName() != null && inputValidator.validate(account.getHolderName()).isValid && account.getTradeCurrencies().size() > 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/WeChatPayForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.payment.PaymentAccount; import haveno.core.payment.WeChatPayAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.WeChatPayAccountPayload; import haveno.core.payment.validation.WeChatPayValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import javafx.collections.FXCollections; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; public class WeChatPayForm extends GeneralAccountNumberForm { private final WeChatPayAccount weChatPayAccount; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.no"), ((WeChatPayAccountPayload) paymentAccountPayload).getAccountNr()); return gridRow; } // TODO: WeChatPay validator is not used public WeChatPayForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, WeChatPayValidator weChatPayValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.weChatPayAccount = (WeChatPayAccount) paymentAccount; } @Override public void addTradeCurrency() { addTradeCurrencyComboBox(); currencyComboBox.setItems(FXCollections.observableArrayList(weChatPayAccount.getSupportedCurrencies())); } @Override void setAccountNumber(String newValue) { weChatPayAccount.setAccountNr(newValue); } @Override String getAccountNr() { return weChatPayAccount.getAccountNr(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/WesternUnionForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.common.util.Tuple2; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.BankUtil; import haveno.core.locale.Country; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.CountryBasedPaymentAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.WesternUnionAccountPayload; import haveno.core.payment.validation.EmailValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.GUIUtil; import javafx.scene.control.ComboBox; import javafx.scene.layout.GridPane; import lombok.extern.slf4j.Slf4j; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; @Slf4j public class WesternUnionForm extends PaymentMethodForm { public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { final WesternUnionAccountPayload payload = (WesternUnionAccountPayload) paymentAccountPayload; addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.fullName"), payload.getHolderName()); addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.email"), payload.getEmail()); addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.city"), payload.getCity()); if (BankUtil.isStateRequired(payload.getCountryCode())) addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.account.state"), payload.getState()); return gridRow; } private final WesternUnionAccountPayload westernUnionAccountPayload; private InputTextField cityInputTextField; private InputTextField stateInputTextField; private final EmailValidator emailValidator; private Country selectedCountry; public WesternUnionForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.westernUnionAccountPayload = (WesternUnionAccountPayload) paymentAccount.paymentAccountPayload; emailValidator = new EmailValidator(); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(paymentAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"), getCountryBasedPaymentAccount().getCountry() != null ? getCountryBasedPaymentAccount().getCountry().name : ""); TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency(); String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null"; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.fullName"), westernUnionAccountPayload.getHolderName()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.city"), westernUnionAccountPayload.getCity()).second.setMouseTransparent(false); if (BankUtil.isStateRequired(westernUnionAccountPayload.getCountryCode())) addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.state"), westernUnionAccountPayload.getState()).second.setMouseTransparent(false); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), westernUnionAccountPayload.getEmail()); addLimitations(true); } private void onTradeCurrencySelected(TradeCurrency tradeCurrency) { TraditionalCurrency defaultCurrency = CurrencyUtil.getCurrencyByCountryCode(selectedCountry.code); applyTradeCurrency(tradeCurrency, defaultCurrency); } private void onCountrySelected(Country country) { selectedCountry = country; if (country != null) { getCountryBasedPaymentAccount().setCountry(country); String countryCode = country.code; TradeCurrency currency = CurrencyUtil.getCurrencyByCountryCode(countryCode); paymentAccount.setSingleTradeCurrency(currency); currencyComboBox.setDisable(false); currencyComboBox.getSelectionModel().select(currency); updateFromInputs(); applyIsStateRequired(); cityInputTextField.setText(""); stateInputTextField.setText(""); } } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; Tuple2, Integer> tuple = GUIUtil.addRegionCountryTradeCurrencyComboBoxes(gridPane, gridRow, this::onCountrySelected, this::onTradeCurrencySelected); currencyComboBox = tuple.first; gridRow = tuple.second; InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.fullName")); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { westernUnionAccountPayload.setHolderName(newValue); updateFromInputs(); }); holderNameInputTextField.setValidator(inputValidator); cityInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.city")); cityInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { westernUnionAccountPayload.setCity(newValue); updateFromInputs(); }); stateInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.state")); stateInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { westernUnionAccountPayload.setState(newValue); updateFromInputs(); }); applyIsStateRequired(); InputTextField emailInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email")); emailInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { westernUnionAccountPayload.setEmail(newValue); updateFromInputs(); }); emailInputTextField.setValidator(emailValidator); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); updateFromInputs(); } private void applyIsStateRequired() { final boolean stateRequired = BankUtil.isStateRequired(westernUnionAccountPayload.getCountryCode()); stateInputTextField.setManaged(stateRequired); stateInputTextField.setVisible(stateRequired); } private CountryBasedPaymentAccount getCountryBasedPaymentAccount() { return (CountryBasedPaymentAccount) this.paymentAccount; } @Override protected void autoFillNameTextField() { setAccountNameWithString(westernUnionAccountPayload.getHolderName() == null ? "" : westernUnionAccountPayload.getHolderName()); } @Override public void updateAllInputsValid() { boolean result = isAccountNameValid() && paymentAccount.getSingleTradeCurrency() != null && getCountryBasedPaymentAccount().getCountry() != null && inputValidator.validate(westernUnionAccountPayload.getHolderName()).isValid && inputValidator.validate(westernUnionAccountPayload.getCity()).isValid && emailValidator.validate(westernUnionAccountPayload.getEmail()).isValid; allInputsValid.set(result); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/components/paymentmethods/ZelleForm.java ================================================ /* * 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 . */ package haveno.desktop.components.paymentmethods; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.ZelleAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.ZelleAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.validation.EmailOrMobileNrValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; public class ZelleForm extends PaymentMethodForm { private final ZelleAccount zelleAccount; private final EmailOrMobileNrValidator zelleValidator; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), ((ZelleAccountPayload) paymentAccountPayload).getHolderName()); addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.email.mobile"), ((ZelleAccountPayload) paymentAccountPayload).getEmailOrMobileNr()); return gridRow; } public ZelleForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, EmailOrMobileNrValidator zelleValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.zelleAccount = (ZelleAccount) paymentAccount; this.zelleValidator = zelleValidator; } @Override public void addFormForAddAccount() { gridRowFrom = gridRow + 1; InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname")); holderNameInputTextField.setValidator(inputValidator); holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { zelleAccount.setHolderName(newValue.trim()); updateFromInputs(); }); InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email.mobile")); mobileNrInputTextField.setValidator(zelleValidator); mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { zelleAccount.setEmailOrMobileNr(newValue.trim()); updateFromInputs(); }); final TradeCurrency singleTradeCurrency = zelleAccount.getSingleTradeCurrency(); final String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); } @Override protected void autoFillNameTextField() { setAccountNameWithString(zelleAccount.getEmailOrMobileNr()); } @Override public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(zelleAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner.fullname"), zelleAccount.getHolderName()); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email.mobile"), zelleAccount.getEmailOrMobileNr()).second; field.setMouseTransparent(false); final TradeCurrency singleTradeCurrency = zelleAccount.getSingleTradeCurrency(); final String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode); addLimitations(true); } @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() && zelleValidator.validate(zelleAccount.getEmailOrMobileNr()).isValid && inputValidator.validate(zelleAccount.getHolderName()).isValid && zelleAccount.getTradeCurrencies().size() > 0); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/haveno.css ================================================ @font-face { src: url("/fonts/IBMPlexSans-Regular.ttf"); } @font-face { src: url("/fonts/IBMPlexSans-Bold.ttf"); } @font-face { src: url("/fonts/IBMPlexSans-Medium.ttf"); } @font-face { src: url("/fonts/IBMPlexSans-Light.ttf"); } @font-face { src: url("/fonts/IBMPlexMono-Regular.ttf"); } .root { -fx-font-size: 13; } .root:dir(ltr){ -fx-font-family: "IBM Plex Sans"; } /******************************************************************************************************************** * * * General * * * ********************************************************************************************************************/ /* Text */ .error-text { -fx-text-fill: -bs-rd-error-red; } .error { -fx-accent: -bs-rd-error-red; } .success-text { -fx-text-fill: -bs-color-primary; } .highlight, .highlight-static, .highlight.label .glyph-icon { -fx-text-fill: -fx-accent; -fx-fill: -fx-accent; } .highlight:hover { -fx-text-fill: -bs-text-color; -fx-fill: -bs-text-color; } .info { -fx-text-fill: -bs-color-primary; -fx-fill: -bs-color-primary; } .info:hover { -fx-text-fill: -bs-color-gray-6; -fx-fill: -bs-color-gray-6; } .sub-info { -fx-text-fill: -bs-color-gray-4; -fx-fill: -bs-color-gray-4; } .headline-label { -fx-font-weight: bold; -fx-font-size: 1.692em; } .warning-box { -fx-background-color: -bs-yellow-light; -fx-spacing: 6; -fx-alignment: center; } .warning { -fx-text-fill: -bs-yellow; -fx-fill: -bs-yellow; } .warning:hover { -fx-text-fill: -bs-color-gray-6; -fx-fill: -bs-color-gray-6; } .zero-decimals { -fx-text-fill: -bs-color-gray-3; } .confirmation-label { -fx-font-size: 1.077em; -fx-text-fill: -bs-rd-font-confirmation-label; } .confirmation-value { -fx-font-size: 1.077em; -fx-font-family: "IBM Plex Mono"; -fx-text-fill: -bs-rd-font-dark-gray; } .confirmation-text-field-as-label:readonly { -fx-background-color: transparent !important; -fx-font-size: 1.077em; -fx-font-family: "IBM Plex Mono"; -fx-padding: 0 !important; -fx-border-width: 0; -fx-text-fill: -bs-rd-font-dark-gray !important; } .confirmation-text-field-as-label-icon { } /* Other UI Elements */ .separator *.line { -fx-border-style: solid; -fx-border-width: 0 0 1 0; -fx-border-color: -bs-rd-separator-dark; } .separator:vertical *.line { -fx-border-width: 0 1 0 0; } .jfx-progress-bar > .bar, .jfx-progress-bar:indeterminate > .bar { -fx-background-color: -bs-color-primary; } .jfx-progress-bar.error > .bar, .jfx-progress-bar:indeterminate.error > .bar { -fx-background-color: -bs-rd-error-red; } .jfx-progress-bar > .track { -fx-background-color: -bs-progress-bar-track; } .jfx-spinner { -jfx-radius: 10; } .jfx-spinner:indeterminate .arc, .jfx-spinner:determinate .arc { -fx-stroke: -bs-color-primary; } .busyanimation .text.percentage { -fx-fill: null; } .jfx-button, .action-button { -fx-background-color: -bs-color-gray-bbb; -fx-text-fill: -bs-rd-font-dark-gray; -fx-font-size: 0.923em; -fx-font-weight: normal; -fx-background-radius: 999; -fx-border-radius: 999; -fx-min-height: 32; -fx-padding: 0 40 0 40; -fx-effect: dropshadow(gaussian, -bs-text-color-transparent, 2, 0, 0, 0, 1); -fx-cursor: hand; } .jfx-button:hover, .jfx-button:focused { -fx-background-color: derive(-bs-color-gray-2, -10%); } .action-button:hover, .action-button:focused { -fx-background-color: derive(-bs-color-primary-dark, -10%); } .action-button { -fx-background-color: -bs-color-primary-dark; -fx-text-fill: -bs-background-color; } .action-button.compact-button, .compact-button { -fx-padding: 0 15 0 15; } .table-cell .jfx-button { -fx-padding: 0 7 0 7; } .tiny-button, .action-button.tiny-button { -fx-font-size: 0.769em; -fx-pref-height: 20; -fx-padding: 3 8 3 8; -fx-border-radius: 5; } .text-button { -fx-background-color: transparent; -fx-underline: true; -fx-padding: 0 10 0 10; -fx-pref-height: 28; -fx-min-height: -fx-pref-height; } .text-button:hover { -fx-text-fill: -bs-text-color; -fx-background-color: transparent; -fx-underline: false; } .jfx-checkbox { -jfx-checked-color: -bs-color-primary; -fx-font-size: 0.692em; } .jfx-check-box .box, .jfx-check-box:indeterminate .box, .jfx-check-box:indeterminate:selected .box { -fx-border-radius: 0; -fx-border-width: 1; -fx-pref-width: 12; -fx-pref-height: 12; } .jfx-check-box .mark, .jfx-check-box .indeterminate-mark { -fx-border-radius: 0; -fx-border-width: 1; } .jfx-combo-box, .jfx-text-field, .jfx-text-area, .jfx-password-field, .toggle-button-no-slider { -fx-padding: 7 14 7 14; -fx-background-radius: 999; -fx-border-radius: 999; -fx-border-color: transparent; } .jfx-combo-box { -fx-background-color: -bs-color-background-form-field; } .input-line, .input-focused-line { -fx-background-color: transparent; visibility: hidden; -fx-max-height: 0; } .jfx-text-field { -fx-background-radius: 999; -fx-border-radius: 999; -fx-background-color: -bs-color-background-form-field; } .jfx-text-field.label-float .prompt-container { -fx-translate-y: 0px; } .jfx-text-field.filled.label-float .prompt-container, .jfx-text-field.label-float:focused .prompt-container, .jfx-combo-box.filled.label-float .prompt-container, .jfx-combo-box.label-float:focused .prompt-container, .jfx-password-field.filled.label-float .prompt-container, .jfx-password-field.label-float:focused .prompt-container { -fx-translate-x: -14px; -fx-translate-y: -5.5px; } .jfx-combo-box .arrow-button { -fx-background-radius: 999; -fx-border-radius: 999; -fx-padding: 0 0 0 10; } .jfx-combo-box:hover { -fx-cursor: hand; } .jfx-combo-box:editable:hover { -fx-cursor: null; } .jfx-combo-box .arrow-button:hover { -fx-cursor: hand; } .jfx-combo-box > .list-cell { -fx-text-fill: -bs-text-color; -fx-font-family: "IBM Plex Sans Medium"; } /* TODO: otherwise combo box with "odd" class is opacity 0.4? */ .jfx-combo-box > .list-cell:odd, .jfx-combo-box > .list-cell:even { -fx-opacity: 1.0; } .jfx-combo-box > .list-cell, .jfx-combo-box > .text-field { -fx-padding: 0 !important; } .jfx-combo-box > .arrow-button > .arrow { -fx-background-color: null; -fx-border-color: -bs-color-gray-line; -fx-shape: "M 0 0 l 3.5 4 l 3.5 -4"; } .combo-box-popup { -fx-background-color: -bs-color-background-pane; -fx-background-radius: 15; -fx-border-radius: 15; -fx-padding: 5; } .combo-box-popup .scroll-pane { -fx-background-color: -bs-color-background-pane; -fx-background-radius: 15; -fx-border-radius: 15; -fx-padding: 5; } .combo-box-popup > .list-view { -fx-background-color: -bs-color-background-pane; -fx-border-color: -bs-color-border-form-field; -fx-translate-y: 4; -fx-background-radius: 15; -fx-border-radius: 15; -fx-padding: 5; } /* Rounds the first and last list cells to create full round illusion */ .combo-box-popup .list-cell:first-child { -fx-background-radius: 10 10 0 0; } .combo-box-popup .list-cell:last-child { -fx-background-radius: 0 0 10 10; } .combo-box-popup .list-cell:hover { -fx-background-radius: 8; } .combo-box-popup > .list-view:hover { -fx-cursor: hand; } .combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected { -fx-background: -fx-selection-bar; -fx-background-color: -fx-selection-bar; -fx-background-radius: 15; -fx-border-radius: 15; } .combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:hover, .combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected:hover { -fx-background: -fx-accent; -fx-background-color: -fx-selection-bar; } /* list view */ .list-view .list-cell { -fx-background-color: -bs-background-color; } .list-view .list-cell:odd { -fx-background-color: derive(-bs-background-color, -5%); } .list-view .list-cell:even { -fx-background-color: derive(-bs-background-color, 5%); } .list-view .list-cell:hover, .list-view .list-cell:selected, .table-view .table-cell:hover, .table-view .table-cell:selected { -fx-background: -fx-accent; -fx-background-color: -fx-selection-bar; } .number-column.table-cell { -fx-background-color: -bs-background-color; } .list-view:focused, .tree-view:focused, .table-view:focused, .tree-table-view:focused, .table-view:focused, tree-table-view:focused { -fx-background-insets: 0; } .list-view:focused { -fx-background-color: -bs-color-primary; -fx-background-insets: 0; } /* Selected rows */ .list-view:focused > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected, .tree-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-cell:filled:selected, .table-view:focused > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled:selected, .tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:selected, .table-view:focused > .virtual-flow > .clipped-container > .sheet > .table-row-cell .table-cell:selected, .tree-table-view:focused > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell .tree-table-cell:selected { -fx-background-insets: 0; } /* Selected when control is not focused */ .list-cell:filled:selected, .tree-cell:filled:selected, .table-row-cell:filled:selected, .tree-table-row-cell:filled:selected, .table-row-cell:filled > .table-cell:selected, .tree-table-row-cell:filled > .tree-table-cell:selected { -fx-background-insets: 0; } /* combo box list view */ .combo-box .list-view .list-cell:odd { -fx-background-color: -bs-color-background-pane; } .combo-box .list-view .list-cell:even { -fx-background-color: -bs-color-background-pane; } .jfx-text-field-top-label { -fx-text-fill: -bs-color-gray-dim; } .jfx-text-field:readonly, .hyperlink-with-icon { -fx-background-color: -bs-color-background-form-field-readonly; } .jfx-text-field:readonly > .input-line { -fx-background-color: transparent; } .jfx-text-field:readonly > .input-focused-line { -fx-background-color: transparent; } .jfx-text-field:disabled > .input-line { -fx-background-color: transparent; -fx-border-width: 0; } .jfx-text-field:disabled > .input-focused-line { -fx-background-color: transparent; -fx-border-width: 0; } #info-field { -fx-prompt-text-fill: -bs-text-color; } .jfx-password-field { -fx-background-color: -bs-color-background-form-field; } .jfx-password-field > .input-line { -fx-translate-x: -0.333333em; } .jfx-combo-box:error, .jfx-text-field:error { -fx-text-fill: -bs-rd-error-red; } .jfx-text-field .error-label, .jfx-password-field .error-label, .jfx-text-area .error-label { -fx-text-fill: -bs-rd-error-red; -fx-font-size: 0.692em; -fx-padding: -0.5em 0 0 0; } .jfx-text-field .error-icon, .jfx-password-field .error-icon, .jfx-text-area .error-icon { -fx-text-fill: -bs-rd-error-red; -fx-font-size: 1em; } .offer-input { -fx-background-color: -bs-color-background-form-field; -fx-border-color: -bs-background-gray; -fx-pref-height: 43; -fx-pref-width: 310; -fx-effect: innershadow(gaussian, -bs-text-color-transparent, 3, 0, 0, 0); -fx-background-radius: 999; -fx-border-radius: 999; -fx-alignment: center; } .offer-input .text-field { -fx-alignment: center-right; -fx-pref-height: 44; -fx-font-size: 1.385em; -fx-background-radius: 999 0 0 999; -fx-border-radius: 999 0 0 999; -fx-background-color: -bs-color-background-form-field; -fx-border-color: transparent; } .offer-input > .input-label { -fx-font-size: 0.692em; -fx-min-width: 45; -fx-padding: 8; -fx-alignment: center; -fx-background-radius: 999; -fx-border-radius: 999; -fx-background-color: derive(-bs-color-background-form-field, 15%); } .offer-input .icon { -fx-padding: 10; } .offer-input-readonly { -fx-background-color: -bs-color-gray-1; -fx-border-width: 0; -fx-pref-width: 300; -fx-background-radius: 999; -fx-border-radius: 999; } .offer-input-readonly .text-field { -fx-alignment: center-right; -fx-font-size: 1em; -fx-background-color: -bs-color-gray-1; -fx-border-width: 0; } .offer-input-readonly .text-field > .input-line { -fx-background-color: transparent; } .offer-input-readonly > .input-label { -fx-font-size: 0.692em; -fx-min-width: 30; -fx-padding: 8; -fx-alignment: center; } .offer-input-readonly .icon { -fx-padding: 3; } .jfx-badge .badge-pane { -fx-background-color: -bs-red; -fx-background-radius: 15; -fx-pref-width: 15; -fx-pref-height: 15; } .jfx-badge.new .badge-pane { -fx-pref-width: 30; } .jfx-badge.auto-conf .badge-pane { -fx-background-color: -xmr-orange; -fx-pref-width: -1; -fx-padding: -1 10 0 10; } .jfx-badge .badge-pane .label { -fx-font-weight: bold; -fx-font-size: 0.692em; -fx-text-fill: -bs-background-color; } .jfx-badge { -fx-padding: -2 0 0 0; -fx-border-insets: 0 0 0 0; } .jfx-toggle-button, .jfx-toggle-button:armed, .jfx-toggle-button:hover, .jfx-toggle-button:focused, .jfx-toggle-button:selected, .jfx-toggle-button:focused:selected { -jfx-toggle-color: -bs-color-primary-dark; -jfx-size: 8; } .jfx-toggle-button:hover { -fx-cursor: hand; } .jfx-text-area { -fx-background-color: -bs-color-background-form-field; -fx-padding: 9 9 9 9; -fx-background-radius: 15; -fx-border-radius: 15; } .jfx-text-area:readonly { -fx-background-color: transparent; } .jfx-text-area > .input-line { -fx-translate-x: -0.333333em; } .text-area .viewport { -fx-background-color: transparent; -fx-background-radius: 15; -fx-border-radius: 15; } .wallet-seed-words { -fx-font-family: "IBM Plex Mono"; } .wallet-seed-words .content, .wallet-seed-words:focused .content { -fx-padding: 12 12 0 12; } .jfx-date-picker { -jfx-default-color: -bs-color-primary; } .jfx-date-picker { -fx-padding: 0.333333em 0em 0.333333em 0em; } .jfx-date-picker .jfx-text-field .jfx-text-area > .input-line { -fx-translate-x: 0em; -fx-background-color: transparent; } .jfx-date-picker .jfx-text-field .jfx-text-area > .input-focused-line { -fx-translate-x: 0em; -fx-background-color: transparent; } .jfx-date-picker > .arrow-button > .arrow { -fx-shape: "M320 384h128v128h-128zM512 384h128v128h-128zM704 384h128v128h-128zM128 768h128v128h-128zM320 768h128v128h-128zM512 768h128v128h-128zM320 576h128v128h-128zM512 576h128v128h-128zM704 576h128v128h-128zM128 576h128v128h-128zM832 0v64h-128v-64h-448v64h-128v-64h-128v1024h960v-1024h-128zM896 960h-832v-704h832v704z"; -fx-background-color: -jfx-default-color; -fx-background-insets: 0; -fx-padding: 10; } .jfx-date-picker > .arrow-button > .jfx-svg-glyph { -fx-background-color: -jfx-default-color; } .date-picker-popup .month-year-pane { -fx-background-color: -bs-color-primary-dark; } .scroll-bar { -fx-background-color: -bs-background-color; -fx-background-radius: 0; } .scroll-bar:horizontal .track, .scroll-bar:vertical .track { -fx-background-color: -bs-color-background-pane; -fx-border-color: -bs-color-background-pane; -fx-background-radius: 0; } .scroll-bar:vertical .track-background, .scroll-bar:horizontal .track-background { -fx-background-color: -bs-color-background-pane; -fx-background-insets: 0; -fx-background-radius: 0; } .scroll-bar:horizontal .thumb { -fx-background-color: -bs-color-gray-2; -fx-background-insets: 2 0 2 0; -fx-background-radius: 3; } .scroll-bar:vertical .thumb { -fx-background-color: -bs-color-gray-2; -fx-background-insets: 0 2 0 2; -fx-background-radius: 3; } .scroll-bar:horizontal .thumb:hover, .scroll-bar:vertical .thumb:hover { -fx-background-color: -bs-color-gray-ccc; } .scroll-bar:horizontal .thumb:pressed, .scroll-bar:vertical .thumb:pressed { -fx-background-color: -bs-color-gray-ddd; } .scroll-bar:vertical .increment-button, .scroll-bar:vertical .decrement-button, .scroll-bar:horizontal .increment-button, .scroll-bar:horizontal .decrement-button { -fx-background-color: -bs-color-background-pane; -fx-padding: 1; } .scroll-bar:horizontal .increment-arrow, .scroll-bar:vertical .increment-arrow, .scroll-bar:horizontal .decrement-arrow, .scroll-bar:vertical .decrement-arrow { -fx-shape: null; -fx-background-color: -bs-color-background-pane; } .scroll-bar:vertical:focused, .scroll-bar:horizontal:focused { -fx-background-color: -bs-color-background-pane; } /* Behavior */ .show-hand { -fx-cursor: hand; } .hide-focus { -fx-focus-color: transparent; } /* Font */ .very-small-text { -fx-font-size: 0.692em; } .small-text { -fx-font-size: 0.769em; } .medium-text { -fx-font-size: 0.846em; } .normal-text { -fx-font-size: 0.923em; } .default-text { -fx-font-size: 13; } .bold-text, .bold-text .text { -fx-font-weight: bold; } /* Splash */ #splash { -fx-background-color: -bs-background-color; } /* Main UI */ #base-content-container { -fx-background-color: -bs-color-gray-background; } .content-pane { -fx-background-color: linear-gradient(-bs-content-pane-bg-top 0%, -bs-content-pane-bg-bottom 100%); } #footer-pane { -fx-background-color: -bs-footer-pane-background; -fx-font-size: 0.923em; -fx-text-fill: -bs-footer-pane-text; } #footer-pane-line { -fx-background: -bs-footer-pane-line; } #headline-label { -fx-font-weight: bold; -fx-font-size: 1.385em; } /* Main navigation */ .top-navigation { -fx-background-color: -bs-rd-nav-background; -fx-border-width: 0 0 0 0; -fx-border-color: -bs-rd-nav-primary-border; -fx-background-radius: 15; -fx-border-radius: 15; } .top-navigation .separator:vertical .line { -fx-border-color: transparent transparent transparent transparent; -fx-border-width: 1; -fx-border-insets: 0 0 0 1; } .nav-logo { -fx-max-width: 190; -fx-min-width: 155; } .nav-primary { -fx-background-color: -bs-rd-nav-primary-background; -fx-border-width: 0 0 0 0; -fx-border-color: -bs-rd-nav-primary-border; -fx-background-radius: 999; -fx-border-radius: 999; -fx-padding: 9 0 9 20; } .nav-secondary { -fx-padding: 0 14 0 0; } .nav-separator { -fx-max-width: 1; -fx-min-width: 1; -fx-border-color: transparent transparent transparent -bs-rd-separator-dark; -fx-border-width: 1; -fx-border-insets: 0 0 0 1; } .nav-spacer { -fx-max-width: 10; -fx-min-width: 10; } .jfx-badge > .nav-button, .jfx-badge > .nav-secondary-button { -fx-translate-y: 1; } .nav-button { -fx-cursor: hand; -fx-background-color: transparent; -fx-padding: 9 15; -fx-background-radius: 999; -fx-border-radius: 999; } .nav-button .text { -fx-font-size: 0.95em; -fx-font-weight: 500; -fx-fill: -bs-rd-nav-deselected; } .nav-button-japanese .text { -fx-font-size: 1em; } .nav-button:selected { -fx-background-color: -bs-background-color; -fx-effect: dropshadow(gaussian, -bs-text-color-transparent, 4, 0, 0, 0, 2); } .top-navigation .nav-button:hover { -fx-background-color: -bs-rd-nav-button-hover; } .nav-primary .nav-button:hover { -fx-background-color: -bs-rd-nav-primary-button-hover; } .nav-button:selected .text { -fx-fill: -bs-rd-nav-selected; } .nav-secondary-button { -fx-cursor: hand; -fx-padding: 9 2 9 2; -fx-border-insets: 0 12 1 12; -fx-border-color: transparent; -fx-border-width: 0 0 1px 0; } .nav-secondary-button .text { -fx-font-size: 0.95em; -fx-font-weight: 500; -fx-fill: -bs-rd-nav-secondary-deselected; } .nav-secondary-button-japanese .text { -fx-font-size: 1em; } .nav-secondary-button:selected { -fx-border-color: transparent transparent -bs-rd-nav-secondary-selected transparent; -fx-border-width: 0 0 1px 0; } .nav-secondary-button:hover { } .nav-secondary-button:selected .text { -fx-fill: -bs-rd-nav-secondary-selected; } .nav-balance-display { -fx-alignment: center-left; -fx-text-fill: -bs-rd-font-balance; } .nav-price-balance { -fx-background-color: -bs-rd-nav-background; -fx-background-radius: 999; -fx-border-radius: 999; -fx-padding: 0 20 0 20; } .nav-price-balance .separator:vertical .line { -fx-border-color: transparent transparent transparent -bs-rd-separator-dark; -fx-border-width: 1; -fx-border-insets: 0 0 0 1; } .nav-price-balance .jfx-combo-box { -fx-border-color: transparent; -fx-padding: 0; -fx-pref-width: 180; } .nav-price-balance .jfx-combo-box > .input-line { -fx-pref-height: 0px; } .nav-balance-label { -fx-font-size: 0.769em; -fx-alignment: center-left; -fx-text-fill: -bs-rd-font-balance-label; -fx-padding: 0; } #nav-alert-label { -fx-font-weight: bold; -fx-alignment: center; -fx-font-size: 0.846em; -fx-text-fill: -bs-background-color; } .text-field { -fx-prompt-text-fill: derive(-bs-prompt-text, -30%); } .text-area { -fx-prompt-text-fill: derive(-bs-prompt-text, -30%); } #label-url { -fx-cursor: hand; -fx-text-fill: -bs-color-blue-0; -fx-underline: true; } /** Confirmation Indicator */ .progress-indicator > .determinate-indicator > .indicator { -fx-background-color: -fx-control-inner-background; -fx-border-color: -fx-box-border; -fx-border-width: 1; -fx-padding: 0.166667em; /* 2px */ } /******************************************************************************* * * * Icons * * * ******************************************************************************/ .icon, .icon:hover { -fx-cursor: hand; } .hidden-icon-button { -fx-background-color: transparent; -fx-padding: 0; -fx-cursor: hand; } #icon-button { -fx-cursor: hand; -fx-background-color: transparent; } .copy-icon-disputes { -fx-text-fill: -bs-background-color; } .copy-icon-disputes.label .glyph-icon { -fx-fill: -bs-background-color; } .copy-icon:hover { -fx-text-fill: -bs-text-color; } .received-funds-icon { -fx-text-fill: -bs-green-soft; } .sent-funds-icon { -fx-text-fill: -bs-red-soft; } .version { -fx-text-fill: -bs-text-color; -fx-underline: false; -fx-cursor: null; } .version-new { -fx-text-fill: -bs-rd-error-red; -fx-underline: true; -fx-cursor: hand; } .alert { -fx-text-fill: -bs-rd-error-red; } .icon { -fx-fill: -bs-text-color; } .opaque-icon { -fx-fill: -bs-color-gray-bbb; -fx-opacity: 1; } .opaque-icon-character { -fx-font-size: 3em; -fx-text-fill: -bs-color-gray-bbb; -fx-padding: 24 2 0 2; } .opaque-icon-character.small { -fx-font-size: 1em; -fx-padding: 27 2 0 2; } .alert-icon { -fx-fill: -bs-rd-error-red; -fx-cursor: hand; } .close-icon { -fx-fill: -bs-text-color; } .close-icon:hover { -fx-fill: -fx-accent; } .tooltip-icon { -fx-fill: -bs-text-color; } .link-icon { -fx-fill: -bs-color-gray-ccc; } .link-icon:hover { -fx-fill: -fx-accent; } /******************************************************************************* * * * Tooltip * * * ******************************************************************************/ .tooltip { -fx-background: -bs-background-color; -fx-text-fill: -bs-text-color; -fx-background-color: -bs-background-color; -fx-background-radius: 6px; -fx-background-insets: 0; -fx-padding: 0.667em 0.75em 0.667em 0.75em; /* 10px */ -fx-effect: dropshadow(three-pass-box, -bs-text-color-transparent, 10, 0, 0, 3); -fx-font-size: 0.85em; } /* Same style like non editable textfield. But textfield spans a whole column in a grid, so we use generally textfield */ #label-with-background { -fx-background-color: -bs-color-gray-fafa; -fx-border-radius: 4; -fx-padding: 4 4 4 4; } #funds-confidence { -fx-progress-color: -bs-color-gray-dim; -fx-max-width: 20; -fx-max-height: 20; } #xmr-confidence { -fx-progress-color: -xmr-orange; -fx-max-width: 20; -fx-max-height: 20; } .hyperlink, .hyperlink.force-underline .text, .hyperlink:hover, .hyperlink:visited, .hyperlink:hover:visited, .hyperlink:focused { -fx-border-style: none; -fx-border-width: 0px; -fx-underline: true; -fx-text-fill: -bs-rd-font-dark; -fx-fill: -bs-rd-font-dark; } .hyperlink.no-underline { -fx-underline: false; } .hyperlink:hover { -fx-text-fill: -bs-text-color; -fx-fill: -bs-text-color; } .hyperlink:hover, .hyperlink:visited, .hyperlink:hover:visited { -fx-underline: false; } .hyperlink.highlight, .hyperlink.highlight .text.hyperlink.no-underline { -fx-text-fill: -fx-accent; -fx-fill: -fx-accent; } .hyperlink.error { -fx-text-fill: -bs-rd-error-red; -fx-fill: -bs-rd-error-red; } /******************************************************************************* * * * Table * * * ******************************************************************************/ .table-view .table-row-cell:even .table-cell { -fx-background-color: -bs-color-background-row-even; -fx-border-color: -bs-color-background-row-even; } .table-view .table-row-cell:odd .table-cell { -fx-background-color: -bs-color-background-row-odd; -fx-border-color: -bs-color-background-row-odd; } .table-view .table-row-cell.row-faded .table-cell .text { -fx-fill: -bs-color-table-cell-dim; } .cell-faded { -fx-opacity: 0.4; } .table-view .table-row-cell:hover .table-cell, .table-view .table-row-cell:selected .table-cell { -fx-background: -fx-accent; -fx-background-color: -fx-selection-bar; -fx-border-color: -fx-selection-bar; } .table-row-cell { -fx-border-color: -bs-background-color; } .table-row-cell:empty, .table-row-cell:empty:even, .table-row-cell:empty:odd { -fx-background-color: -bs-background-color; -fx-min-height: 36; } .offer-table .table-row-cell { -fx-background: -fx-accent; -fx-background-color: -bs-color-gray-6; } .table-view .table-cell { -fx-alignment: center-left; -fx-padding: 6 0 4 0; -fx-text-fill: -bs-text-color; /*-fx-padding: 3 0 2 0;*/ } .table-view .table-cell.last-column { -fx-padding: 6 0 4 0; } .table-view .table-cell.avatar-column { -fx-alignment: center; -fx-padding: 6 0 4 0; } .table-view .table-cell.first-column { -fx-padding: 6 0 4 0; } .table-view .column-header.last-column .label { } .table-view .column-header.first-column { -fx-padding: 0 0 0 0; } .table-view .column-header.last-column { -fx-padding: 0 0 0 0; } .table-view .column-header.avatar-column { -fx-padding: 0; } .table-view .column-header.avatar-column .label { -fx-alignment: center; } .number-column.table-cell { -fx-font-size: 1em; -fx-padding: 0 0 0 0; } .table-view .filler { -fx-background-color: transparent; } .table-view { -fx-control-inner-background-alt: -fx-control-inner-background; -fx-padding: 0; } .table-view .column-header-background { -fx-background-color: -bs-color-background-pane; -fx-border-color: -bs-color-border-form-field; -fx-border-width: 0 0 1 0; } .table-view .column-header .label { -fx-alignment: center-left; -fx-font-weight: normal; -fx-font-size: 0.923em; -fx-padding: 6 0 6 0; -fx-text-fill: -bs-text-color; } .table-view .column-header { -fx-border-color: transparent; -fx-background-color: -bs-color-background-pane; } /* horizontal scrollbars are never needed and are flickering at scaling so lets turn them off */ .table-view > .scroll-bar:horizontal { -fx-opacity: 0; } .offer-table .table-row-cell { -fx-border-color: -bs-background-color; -fx-table-cell-border-color: -bs-background-color; } .table-row-cell { -fx-border-width: 0 0 1 0; -fx-border-color: -bs-color-gray-0; -fx-table-cell-border-color: -bs-background-color; } .table-row-cell:selected { -fx-border-width: 0 0 1 0; -fx-table-cell-border-color: transparent; } .table-row-cell:empty { -fx-border-width: 0; -fx-background-color: -bs-background-color; -fx-table-cell-border-color: transparent; } .table-view .table-row-cell:selected .table-row-cell:row-selection .table-row-cell:cell-selection .text { -fx-fill: -bs-text-color; } .table-view .table-row-cell:selected .button .text { -fx-fill: -bs-text-color; } .table-view .table-row-cell .copy-icon .text, .table-view .table-row-cell .copy-icon .text:hover { -fx-fill: -fx-accent; } .table-view .table-row-cell:selected .copy-icon .text { -fx-fill: -bs-text-color; } .table-view .table-row-cell:selected .copy-icon .text:hover { -fx-fill: -bs-text-color; } .table-view .table-row-cell:selected .hyperlink .text { -fx-fill: -bs-text-color; -fx-border-style: none; -fx-border-width: 0px; } .table-view .table-row-cell .hyperlink .text { -fx-fill: -bs-rd-font-dark; -fx-border-style: none; -fx-border-width: 0px; } .table-view .table-row-cell .hyperlink .text:hover, .table-view .table-row-cell:selected .hyperlink .text:hover { -fx-fill: -bs-text-color; -fx-border-style: none; -fx-border-width: 0px; } .table-view .table-row-cell .hyperlink:hover, .table-view .table-row-cell .hyperlink:visited, .table-view .table-row-cell .hyperlink:hover:visited { -fx-underline: false; -fx-border-style: none; -fx-border-width: 0px; } .table-view .table-row-cell .hyperlink:focused { -fx-border-style: none; -fx-border-width: 0px; } .table-view.large-rows .table-row-cell { -fx-cell-size: 47px; } .table-view.offer-table { -fx-background-radius: 0; -fx-border-radius: 0; } .table-view.offer-table .column-header.first-column { -fx-background-radius: 0; -fx-border-radius: 0; } .table-view.offer-table .column-header.last-column { -fx-background-radius: 0; -fx-border-radius: 0; } .table-view.offer-table .table-row-cell { -fx-background: -fx-accent; -fx-background-color: -bs-color-gray-6; } .offer-table-top { -fx-background-color: -bs-color-background-pane; -fx-padding: 15 15 5 15; -fx-background-radius: 15 15 0 0; -fx-border-radius: 15 15 0 0; -fx-border-width: 0 0 0 0; } .offer-table-top .label { -fx-text-fill: -bs-text-color; -fx-font-size: 1.1em; -fx-font-weight: bold; } .offer-table-top .jfx-button { -fx-pref-width: 300px; -fx-min-height: 35px; -fx-padding: 5 25 5 25; } /******************************************************************************* * * * Icons * * * ******************************************************************************/ #non-clickable-icon { -fx-text-fill: -bs-color-gray-4; } .delete-icon { -fx-fill: -bs-red; } .delete { -fx-text-fill: -bs-rd-error-red; -fx-fill: -bs-rd-error-red; } .delete:hover { -fx-text-fill: -bs-text-color; -fx-fill: -bs-text-color; } .warn-icon { -fx-text-fill: -bs-yellow; -fx-fill: -bs-yellow; } .warn-icon:hover { -fx-text-fill: -bs-yellow; -fx-fill: -bs-yellow; } .error-icon { -fx-text-fill: -bs-rd-error-red; -fx-fill: -bs-rd-error-red; } .error-icon:hover { -fx-text-fill: -bs-rd-error-red; -fx-fill: -bs-rd-error-red; } /******************************************************************************* * * * Images * * * ******************************************************************************/ .qr-code { -fx-cursor: hand; } /******************************************************************************* * * * Textarea * * * ******************************************************************************/ .text-area { -fx-border-color: -bs-background-gray; } .text-area-popup { -fx-border-color: -bs-color-background-popup-blur; } .text-area-popup .content { -fx-background-color: -bs-color-background-popup-blur; } .text-area-popup:focused { -fx-faint-focus-color: -bs-color-background-popup-blur; } .notification-popup-bg .text-area-popup, .peer-info-popup-bg .text-area-popup { -fx-border-color: -bs-color-background-popup; } .notification-popup-bg .text-area-popup .content, .peer-info-popup-bg .text-area-popup .content{ -fx-background-color: -bs-color-background-popup; } .notification-popup-bg .text-area-popup:focused, .peer-info-popup-bg .text-area-popup:focused { -fx-faint-focus-color: -bs-color-background-popup; } /******************************************************************************* * * * Tab pane * * * ******************************************************************************/ .jfx-tab-pane { -fx-padding: 0; -jfx-disable-animation: true; } .jfx-tab-pane .headers-region .tab .tab-container .tab-close-button { -fx-background-color: transparent; -fx-pref-width: 20; -fx-pref-height: 20; -fx-min-width: -fx-pref-width; -fx-max-width: -fx-pref-width; -fx-min-height: -fx-pref-height; -fx-max-height: -fx-pref-height; } .jfx-tab-pane .headers-region .tab .tab-container .tab-close-button .jfx-rippler { -jfx-rippler-fill: none; } .tab:disabled .jfx-rippler { -jfx-rippler-fill: none !important; } .tab:disabled .tab-label { -fx-cursor: default !important; } .jfx-tab-pane .headers-region .tab .tab-container .tab-close-button > .jfx-svg-glyph { -fx-shape: "M810 274l-238 238 238 238-60 60-238-238-238 238-60-60 238-238-238-238 60-60 238 238 238-238z"; -jfx-size: 9; -fx-background-color: -bs-rd-font-light; } .jfx-tab-pane .headers-region .tab .tab-container .tab-close-button { -fx-padding: 0 0 0 0; } .jfx-tab-pane .headers-region .tab:selected .tab-container .tab-close-button > .jfx-svg-glyph { -fx-background-color: -fx-accent; } .jfx-tab-pane .tab-header-background { -fx-background-color: -bs-color-gray-background; -fx-border-width: 0 0 1 0; -fx-border-color: -bs-rd-tab-border; } .jfx-tab-pane .headers-region .tab-selected-line { -fx-background-color: -fx-accent; -fx-pref-height: 1; } .jfx-tab-pane .headers-region .tab .tab-container .tab-label { -fx-text-fill: -bs-rd-font-light; -fx-padding: 9 14; -fx-font-size: .95em; -fx-font-weight: normal; -fx-cursor: hand; } .jfx-tab-pane .depth-container { -fx-effect: none; } .jfx-tab-pane .headers-region .tab:selected .tab-container .tab-label { -fx-text-fill: -fx-accent; } .jfx-tab-pane .headers-region > .tab > .jfx-rippler { -jfx-rippler-fill: none; } .jfx-tab-pane .headers-region .tab:closable { -fx-border-color: transparent; -fx-border-width: 0; -fx-border-style: none; -fx-border-insets: 0; -fx-padding: 9; } .jfx-tab-pane .headers-region .tab:closable .tab-container .tab-label { -fx-padding: 5; } #form-header-text { -fx-font-weight: bold; -fx-font-size: 1.077em; } #form-title { -fx-font-weight: bold; } /* scroll-pane */ .scroll-pane { -fx-background-insets: 0; -fx-padding: 0; } .scroll-pane:focused { -fx-background-insets: 0; } .scroll-pane .corner { -fx-background-insets: 0; } /* validation */ .validation-error { -fx-text-fill: -bs-red; } /* Account */ #content-pane-top { -fx-background-color: -bs-color-gray-2, linear-gradient(-bs-color-gray-2 0%, -bs-color-gray-3 100%), linear-gradient(-bs-color-gray-2 0%, -bs-background-gray 100%); -fx-background-insets: 0 0 0 0, 0, 1; } #info-icon-label { -fx-font-size: 1.231em; -fx-text-fill: -bs-color-gray-13; } /* OfferPayload book */ #num-offers { -fx-font-size: 0.923em; } /* Create offer */ #direction-icon-label { -fx-font-weight: bold; -fx-font-size: 1.231em; -fx-text-fill: -bs-color-gray-6; } #input-description-label { -fx-font-size: 0.846em; -fx-alignment: center-left; } #create-offer-calc-label { -fx-font-weight: bold; -fx-font-size: 1.538em; -fx-padding: 15 5 0 5; } #toggle-price-left { -fx-border-radius: 4 0 0 4; -fx-padding: 4 4 4 4; -fx-border-color: -bs-color-gray-4; -fx-border-style: solid none solid solid; -fx-border-insets: 0 -2 0 0; -fx-background-insets: 0 -2 0 0; -fx-background-radius: 4 0 0 4; } #toggle-price-right { -fx-border-radius: 0 4 4 0; -fx-padding: 4 4 4 4; -fx-border-color: -bs-color-gray-4; -fx-border-style: solid solid solid none; -fx-border-insets: 0 0 0 -2; -fx-background-insets: 0 0 0 -2; -fx-background-radius: 0 4 4 0; } #totals-separator { -fx-background: -bs-color-gray-4; } #payment-info { -fx-background-color: -bs-color-gray-fafa; } .toggle-button-active { -fx-background-color: -bs-blue-transparent; } .toggle-button-inactive { -fx-background-color: -bs-color-gray-1; } .toggle-button-no-slider { -fx-border-width: 1px; -fx-border-color: -bs-color-border-form-field; -fx-background-insets: 0; -fx-pref-height: 36px; -fx-focus-color: transparent; -fx-faint-focus-color: transparent; } .toggle-button-no-slider:hover { -fx-cursor: hand; } #trade-fee-textfield { -fx-font-size: 0.9em; -fx-alignment: center-right; } /* Open Offer */ .offer-disabled .label { -fx-text-fill: -bs-color-gray-3; } /* OfferBook */ .table-title { -fx-font-size: 1.077em; -fx-font-family: "IBM Plex Sans Medium"; -fx-alignment: center-left; } .combo-box-editor-bold { -fx-font-weight: bold; -fx-padding: 0 !important; -fx-text-fill: -bs-text-color; -fx-font-family: "IBM Plex Sans Medium"; } .currency-label-small { -fx-font-size: 0.692em; -fx-text-fill: -bs-rd-font-lighter; -fx-alignment: center; -fx-pref-height: 35px; -fx-pref-width: 45px; } .offer-label-small { -fx-font-size: 0.692em; -fx-alignment: center-right; -fx-text-fill: -bs-text-color; } .currency-label-selected { -fx-text-fill: -bs-text-color; -fx-font-family: "IBM Plex Sans Medium"; } .currency-label { -fx-font-size: 1.077em; -fx-text-fill: -bs-rd-font-dark-gray; -fx-alignment: center-left; -fx-pref-height: 35px; } .offer-label { -fx-background-color: rgb(50, 95, 182); -fx-text-fill: white; -fx-font-weight: normal; -fx-background-radius: 999; -fx-border-radius: 999; -fx-padding: 0 6 0 6; } /* Offer */ .percentage-label { -fx-alignment: center; } .offer-separator { -fx-background: -bs-color-gray-6; } #address-text-field { -fx-cursor: hand; -fx-text-fill: -fx-accent; -fx-prompt-text-fill: -bs-text-color; } #address-text-field:hover { -fx-text-fill: -bs-text-color; } #address-text-field-error { -fx-cursor: hand; -fx-text-fill: -bs-rd-error-red; -fx-prompt-text-fill: -bs-text-color; } /* Account setup */ #wizard-item-background-deactivated { -fx-body-color: linear-gradient(to bottom, -bs-content-background-gray, -bs-color-gray-aaa); -fx-outer-border: linear-gradient(to bottom, -bs-background-gray, -bs-color-gray-3); -fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color; -fx-background-insets: 0 0 -1 0, 0, 1, 2; -fx-background-radius: 3px, 3px, 2px, 1px; } #wizard-item-background-active { -fx-body-color: linear-gradient(to bottom, -bs-bg-gray-5, -bs-color-gray-6); -fx-outer-border: linear-gradient(to bottom, -bs-color-blue-1, -bs-color-blue-2); -fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color; -fx-background-insets: 0 0 -1 0, 0, 1, 2; -fx-background-radius: 3px, 3px, 2px, 1px; } #wizard-item-background-completed { -fx-body-color: linear-gradient(to bottom, -bs-content-background-gray, -bs-color-gray-aaa); -fx-outer-border: linear-gradient(to bottom, -bs-bg-green, -bs-color-green-2); -fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color; -fx-background-insets: 0 0 -1 0, 0, 1, 2; -fx-background-radius: 3px, 3px, 2px, 1px; } /* Account settings */ #account-settings-item-background-disabled { -fx-body-color: linear-gradient(to bottom, -bs-content-background-gray, -bs-color-gray-1); -fx-outer-border: linear-gradient(to bottom, -bs-background-gray, -bs-color-gray-3); -fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color; -fx-background-insets: 0 0 -1 0, 0, 1, 2; -fx-background-radius: 3px, 3px, 2px, 1px; } #account-settings-item-background-active { -fx-body-color: linear-gradient(to bottom, -bs-content-background-gray, -bs-color-gray-1); -fx-outer-border: linear-gradient(to bottom, -bs-background-gray, -bs-color-gray-3); -fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color; -fx-background-insets: 0 0 -1 0, 0, 1, 2; -fx-background-radius: 3px, 3px, 2px, 1px; } #account-settings-item-background-selected { -fx-body-color: linear-gradient(to bottom, -bs-color-gray-5, -bs-color-gray-1); -fx-outer-border: linear-gradient(to bottom, -bs-color-blue-1, -bs-color-blue-2); -fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color; -fx-background-insets: 0 0 -1 0, 0, 1, 2; -fx-background-radius: 3px, 3px, 2px, 1px; } /* Pending trades */ #trade-wizard-item-background-disabled { -fx-text-fill: -bs-rd-font-light; } #trade-wizard-item-background-active { -fx-text-fill: -bs-text-color; -fx-font-family: "IBM Plex Sans Medium"; } .trade-step-label { -fx-text-fill: -bs-background-color; } .trade-step-disabled-bg { -fx-fill: -bs-color-gray-ccc; } .trade-step-active-bg { -fx-fill: -bs-color-primary-dark; } .trade-msg-state-undefined { -fx-text-fill: -bs-yellow; } .trade-msg-state-sent { -fx-text-fill: -bs-yellow-light; } .trade-msg-state-arrived { -fx-text-fill: -bs-turquoise; } .trade-msg-state-stored { -fx-text-fill: -bs-color-blue-4; } .trade-msg-state-acknowledged { -fx-text-fill: -bs-color-primary; } .trade-msg-state-failed { -fx-text-fill: -bs-rd-error-red; } #open-support-button { -fx-font-weight: bold; -fx-font-size: 1.077em; -fx-background-color: -bs-warning; } #open-dispute-button { -fx-font-weight: bold; -fx-text-fill: -bs-background-color; -fx-font-size: 1.077em; -fx-background-color: -bs-rd-error-red; } /* TitledGroupBg */ .titled-group-bg-label, .titled-group-bg-label-active { -fx-font-size: 1.077em; -fx-font-family: "IBM Plex Sans Medium"; -fx-text-fill: -bs-text-color; -fx-background-color: transparent; } .titled-group-bg, .titled-group-bg-active { -fx-body-color: -bs-color-gray-background; -fx-border-color: -bs-rd-separator; -fx-border-width: 0 0 0 0; -fx-background-color: transparent; -fx-background-insets: 0; } .titled-group-bg.last, .titled-group-bg-active.last { -fx-border-width: 0; } /* TableGroupHeadline */ #table-group-headline { -fx-background-color: -bs-content-background-gray; -fx-background-insets: 10 0 -1 0, 0, 1, 2; -fx-background-radius: 3px, 3px, 2px, 1px; } /* copied form modena.css text-input */ #flow-pane-checkboxes-bg { -fx-text-fill: -fx-text-inner-color; -fx-highlight-fill: derive(-fx-control-inner-background, -20%); -fx-highlight-text-fill: -fx-text-inner-color; -fx-prompt-text-fill: derive(-bs-prompt-text, -30%); -fx-background-color: linear-gradient(to bottom, derive(-fx-text-box-border, -10%), -fx-text-box-border), linear-gradient(from 0px 0px to 0px 5px, derive(-fx-control-inner-background, -9%), -fx-control-inner-background); -fx-background-insets: 0, 1; -fx-background-radius: 3, 2; -fx-padding: 0.333333em 0.583em 0.333333em 0.583em; /* 4 7 4 7 */ } #flow-pane-checkboxes-non-editable-bg { -fx-text-fill: -fx-text-inner-color; -fx-highlight-fill: derive(-fx-control-inner-background, -20%); -fx-highlight-text-fill: -fx-text-inner-color; -fx-prompt-text-fill: derive(-bs-prompt-text, -30%); -fx-background-color: linear-gradient(to bottom, derive(-fx-text-box-border, -10%), -fx-text-box-border), linear-gradient(from 0px 0px to 0px 5px, derive(-fx-control-inner-background, -9%), -bs-color-gray-1); -fx-background-insets: 0, 1; -fx-background-radius: 3, 2; -fx-padding: 0.333333em 0.583em 0.333333em 0.583em; /* 4 7 4 7 */ } /* message-list-view*/ #message-list-view.list-view:focused > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected { -fx-background-color: transparent; } #message-list-view.list-view > .virtual-flow > .clipped-container > .sheet > .list-cell { -fx-background-color: transparent; } #message-list-view.list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled { -fx-background-color: transparent; } #message-list-view.list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected { -fx-background-color: transparent; } #message-list-view.list-view:focused > .virtual-flow > .clipped-container > .sheet > .list-cell { -fx-background-color: transparent; } #message-list-view.list-cell { -fx-padding: 0.25em 0.583em 0.25em 0.583em; } #message-list-view.list-view { -fx-background-color: transparent; -fx-background-insets: 0, 1; -fx-padding: 1; -fx-background-image: url("../../images/chatbg_light.png"); -fx-background-repeat: repeat; -fx-background-position: 0 0; -fx-background-size: 700px; } #message-list-view.list-view:focused { -fx-background-color: transparent; -fx-background-insets: 0, 1; -fx-padding: 1; } /* bubble */ #message-bubble-green { -fx-background-color: -bs-color-primary; -fx-background-radius: 10 10 10 10; } #message-bubble-blue { -fx-background-color: -bs-rd-message-bubble; -fx-background-radius: 10 10 10 10; } #message-bubble-grey { -fx-background-color: -bs-color-gray-3; -fx-background-radius: 10 10 10 10; } .attachment-icon { -fx-text-fill: -bs-background-color; -fx-cursor: hand; } .attachment-icon-black { -fx-text-fill: -bs-text-color; -fx-cursor: hand; } /******************************************************************************* * * * Grid pane * * * ******************************************************************************/ .grid-pane { -fx-background-color: -bs-color-background-popup-blur; -fx-background-insets: 10; -fx-background-radius: 15; -fx-border-radius: 15; -fx-padding: 35, 40, 30, 40; } .grid-pane-separator { -fx-border-color: -bs-rd-separator; -fx-border-width: 0 0 1 0; -fx-translate-y: -2; } .grid-pane .text-area { -fx-border-width: 1; -fx-border-color: -bs-color-border-form-field; -fx-text-fill: -bs-text-color; } /******************************************************************************************************************** * * * Market overview * * * ********************************************************************************************************************/ .chart-pane { -fx-background-color: -bs-background-color; } #charts .chart-legend, #charts-dao .chart-legend { -fx-font-size: 1.077em; -fx-alignment: center; } #charts .axis, #price-chart .axis, #volume-chart .axis, #charts-dao .axis { -fx-tick-label-fill: -bs-rd-font-lighter; -fx-tick-label-font-size: 0.769em; -fx-font-size: 0.880em; } #price-chart .axis-tick-mark-text-node, #volume-chart .axis-tick-mark-text-node, #charts-dao .axis-tick-mark-text-node { -fx-text-alignment: center; } .chart-pane, .chart-plot-background, #charts .chart-plot-background { -fx-background-color: transparent; } #charts .default-color0.chart-area-symbol { -fx-background-color: -bs-buy, -bs-background-color; } #charts .default-color1.chart-area-symbol, #charts-dao .default-color0.chart-area-symbol { -fx-background-color: -bs-sell, -bs-background-color; } #charts .default-color0.chart-series-area-line { -fx-stroke: -bs-buy; -fx-stroke-width: 2px; } #charts .default-color1.chart-series-area-line, #charts-dao .default-color0.chart-series-area-line { -fx-stroke: -bs-sell; -fx-stroke-width: 2px; } /* The .chart-line-symbol rules change the color of the legend symbol */ #charts-dao .default-color0.chart-series-line { -fx-stroke: -bs-chart-dao-line1; } #charts-dao .default-color0.chart-line-symbol { -fx-background-color: -bs-chart-dao-line1, -bs-chart-dao-line1; } #charts-dao .default-color1.chart-series-line { -fx-stroke: -bs-chart-dao-line2; } #charts-dao .default-color1.chart-line-symbol { -fx-background-color: -bs-chart-dao-line2, -bs-chart-dao-line2; } #charts-dao .default-color2.chart-series-line { -fx-stroke: -bs-chart-dao-line3; } #charts-dao .default-color2.chart-line-symbol { -fx-background-color: -bs-chart-dao-line3, -bs-chart-dao-line3; } #charts-dao .default-color3.chart-series-line { -fx-stroke: -bs-chart-dao-line4; } #charts-dao .default-color3.chart-line-symbol { -fx-background-color: -bs-chart-dao-line4, -bs-chart-dao-line4; } #charts-dao .default-color4.chart-series-line { -fx-stroke: -bs-chart-dao-line5; } #charts-dao .default-color4.chart-line-symbol { -fx-background-color: -bs-chart-dao-line5, -bs-chart-dao-line5; } #charts-dao .default-color5.chart-series-line { -fx-stroke: -bs-chart-dao-line6; } #charts-dao .default-color5.chart-line-symbol { -fx-background-color: -bs-chart-dao-line6, -bs-chart-dao-line6; } #charts-dao .default-color6.chart-series-line { -fx-stroke: -bs-chart-dao-line7; } #charts-dao .default-color6.chart-line-symbol { -fx-background-color: -bs-chart-dao-line7, -bs-chart-dao-line7; } #charts-dao .default-color7.chart-series-line { -fx-stroke: -bs-chart-dao-line8; } #charts-dao .default-color7.chart-line-symbol { -fx-background-color: -bs-chart-dao-line8, -bs-chart-dao-line8; } #charts-dao .default-color8.chart-series-line { -fx-stroke: -bs-chart-dao-line9; } #charts-dao .default-color8.chart-line-symbol { -fx-background-color: -bs-chart-dao-line9, -bs-chart-dao-line9; } #charts-dao .default-color9.chart-series-line { -fx-stroke: -bs-chart-dao-line10; } #charts-dao .default-color9.chart-line-symbol { -fx-background-color: -bs-chart-dao-line10, -bs-chart-dao-line10; } #charts-dao .default-color10.chart-series-line { -fx-stroke: -bs-chart-dao-line11; } #charts-dao .default-color10.chart-line-symbol { -fx-background-color: -bs-chart-dao-line11, -bs-chart-dao-line11; } #charts-legend-toggle0 { -jfx-toggle-color: -bs-chart-dao-line1 } #charts-legend-toggle1 { -jfx-toggle-color: -bs-chart-dao-line2; } #charts-legend-toggle2 { -jfx-toggle-color: -bs-chart-dao-line3; } #charts-legend-toggle3 { -jfx-toggle-color: -bs-chart-dao-line4; } #charts-legend-toggle4 { -jfx-toggle-color: -bs-chart-dao-line5; } #charts-legend-toggle5 { -jfx-toggle-color: -bs-chart-dao-line6; } #charts-legend-toggle6 { -jfx-toggle-color: -bs-chart-dao-line7; } #charts-legend-toggle7 { -jfx-toggle-color: -bs-chart-dao-line8; } #charts-legend-toggle8 { -jfx-toggle-color: -bs-chart-dao-line9; } #charts-legend-toggle9 { -jfx-toggle-color: -bs-chart-dao-line10; } #charts-legend-toggle10 { -jfx-toggle-color: -bs-chart-dao-line11; } #charts-dao .chart-series-line { -fx-stroke-width: 2px; } .chart-vertical-grid-lines { -fx-stroke: transparent; } #charts .axis-label { -fx-font-size: 0.769em; -fx-alignment: center-left; } #charts .axisy .axis-label { -fx-alignment: center; } #chart-navigation-label { -fx-text-fill: -bs-rd-font-lighter; -fx-font-size: 0.769em; -fx-alignment: center; } #chart-navigation-center-pane { -fx-background-color: -bs-progress-bar-track; } /******************************************************************************************************************** * * * Highlight buttons * * * ********************************************************************************************************************/ #buy-button-big { -fx-font-size: 1em; -fx-background-color: -bs-buy; -fx-text-fill: -bs-white; } #buy-button { -fx-background-color: -bs-buy; -fx-text-fill: -bs-white; } #buy-button-big:hover, #buy-button:hover, #buy-button-big:focused, #buy-button:focused { -fx-background-color: derive(-bs-buy, -10%); } #copy-button-thin { -fx-background-color: -bs-buy; -fx-text-fill: -bs-white; -fx-min-height: 30px; -fx-max-height: 30px; -fx-pref-height: 30px; } #sell-button-big { -fx-background-color: -bs-sell; -fx-text-fill: -bs-white; -fx-font-size: 1em; } #sell-button { -fx-background-color: -bs-sell; -fx-text-fill: -bs-white; } #sell-button-big:hover, #sell-button:hover, #sell-button-big:focused, #sell-button:focused { -fx-background-color: derive(-bs-sell, -10%); } #sell-button-big.grey-style, #buy-button-big.grey-style, #sell-button.grey-style, #buy-button.grey-style { -fx-background-color: -bs-color-gray-bbb; -fx-text-fill: -bs-rd-font-dark-gray; } .action-button:disabled, #sell-button:disabled, #buy-button:disabled { -fx-background-color: -bs-color-gray-0; -fx-text-fill: -bs-rd-font-dark-gray; } /******************************************************************************************************************** * * * Popups * * * ********************************************************************************************************************/ .popup-headline { -fx-font-size: 1.538em; -fx-text-fill: -bs-rd-font-dark; } .popup-headline-information { -fx-font-size: 1.538em; -fx-text-fill: -bs-color-primary; } .popup-headline-warning { -fx-font-size: 1.538em; -fx-text-fill: -bs-rd-error-red; } .popup-icon-information { -fx-text-fill: -bs-color-primary; } .popup-icon-warning { -fx-text-fill: -bs-rd-error-red; } .popup-headline-information.label .glyph-icon, .popup-headline-warning.label .glyph-icon, .popup-icon-information.label .glyph-icon, .popup-icon-warning.label .glyph-icon { -fx-fill: -bs-color-primary; } .popup-bg { -fx-font-size: 1.077em; -fx-background-color: -bs-color-background-popup-blur; -fx-background-insets: 44; -fx-background-radius: 15; -fx-border-radius: 15; -fx-effect: dropshadow(gaussian, -bs-text-color-dropshadow-light-mode, 44, 0, 0, 0); } .notification-popup-bg, .peer-info-popup-bg { -fx-font-size: 0.846em; -fx-text-fill: -bs-rd-font-dark; -fx-background-color: -bs-color-background-popup; -fx-background-insets: 44; -fx-effect: dropshadow(gaussian, -bs-text-color-dropshadow-light-mode, 44, 0, 0, 0); -fx-background-radius: 15; -fx-border-radius: 15; } .popup-bg-top { -fx-font-size: 1.077em; -fx-text-fill: -bs-rd-font-dark; -fx-background-color: -bs-color-background-popup-blur; -fx-background-insets: 44; -fx-background-radius: 0 0 15px 15px; } .popup-dropshadow { -fx-effect: dropshadow(gaussian, -bs-text-color-dropshadow, 20, 0, 0, 0); } .notification-popup-headline, peer-info-popup-headline { -fx-font-size: 1.077em; /*-fx-font-weight: bold;*/ -fx-text-fill: -bs-color-primary; } .account-status-title { -fx-font-size: 0.769em; -fx-font-family: "IBM Plex Sans Medium"; } .account-status-inactive-info-item { -fx-text-fill: -bs-color-gray-dim; -fx-fill: -bs-color-gray-dim; } .account-status-active-info-item { -fx-text-fill: -fx-accent; -fx-fill: -fx-accent; } #price-feed-combo { -fx-background-color: none; } #price-feed-combo > .list-cell { -fx-text-fill: -bs-rd-font-balance; -fx-font-family: "IBM Plex Sans"; } #invert-market-price { -fx-text-fill: -bs-color-gray-11; } #popup-qr-code-info { -fx-font-size: 0.846em; } #ident-num-label { -fx-font-weight: bold; -fx-alignment: center; -fx-font-size: 0.769em; -fx-text-fill: -bs-background-color; } #toggle-left { -fx-border-color: -bs-rd-separator-dark; -fx-border-radius: 4 0 0 4; -fx-border-style: solid; -fx-border-width: 1 1 1 1; -fx-background-radius: 4 0 0 4; -fx-border-insets: 0; -fx-background-insets: 1 1 1 1; -fx-background-color: -bs-background-color; -fx-effect: dropshadow(gaussian, -bs-text-color-transparent, 4, 0, 0, 0, 2); } #toggle-center { -fx-border-color: -bs-rd-separator-dark; -fx-border-radius: 0; -fx-border-style: solid; -fx-border-width: 1 1 1 0; -fx-border-insets: 0; -fx-background-insets: 1 1 1 0; -fx-background-radius: 0; -fx-background-color: -bs-background-color; -fx-effect: dropshadow(gaussian, -bs-text-color-transparent, 4, 0, 0, 0, 2); } #toggle-right { -fx-border-color: -bs-rd-separator-dark; -fx-border-radius: 0 4 4 0; -fx-border-width: 1 1 1 0; -fx-border-insets: 0; -fx-background-insets: 1 1 1 0; -fx-background-radius: 0 4 4 0; -fx-background-color: -bs-background-color; -fx-effect: dropshadow(gaussian, -bs-text-color-transparent, 4, 0, 0, 0, 2); } #toggle-center:selected, #toggle-left:selected, #toggle-right:selected { -fx-text-fill: white; -fx-background-color: -bs-toggle-selected; } #toggle-left:hover, #toggle-right:hover, #toggle-center:hover { -fx-background-color: -bs-toggle-selected; -fx-cursor: hand; } /******************************************************************************************************************** * * * Arbitration * * * ********************************************************************************************************************/ .message { -fx-text-fill: -bs-text-color; } .message.label .glyph-icon { -fx-fill: -bs-text-color; } .my-message { -fx-text-fill: -bs-background-color; } .my-message.label .glyph-icon { -fx-fill: -bs-background-color; } .message-header { -fx-text-fill: -bs-color-gray-6; -fx-font-size: 0.846em; } .my-message-header { -fx-text-fill: -bs-rd-message-bubble; -fx-fill: -bs-rd-message-bubble; -fx-font-size: 0.846em; } .dispute-chat-border { -fx-background-color: -bs-support-chat-background; } /******************************************************************************************************************** * * * DAO * * * ********************************************************************************************************************/ .dao-tx-type-trade-fee-icon, .dao-tx-type-trade-fee-icon:hover { -fx-text-fill: -bs-color-green-2; } .dao-tx-type-unverified-icon, .dao-tx-type-unverified-icon:hover { -fx-text-fill: -bs-yellow; } .dao-tx-type-invalid-icon, .dao-tx-type-invalid-icon:hover { -fx-text-fill: -bs-red-soft; } .dao-tx-type-self-icon, .dao-tx-type-self-icon:hover { -fx-text-fill: -bs-color-gray-2; } .dao-tx-type-proposal-fee-icon, .dao-tx-type-proposal-fee-icon:hover { -fx-text-fill: -bs-color-green-4; } .dao-tx-type-genesis-icon, .dao-tx-type-genesis-icon:hover { -fx-text-fill: -fx-accent; } .dao-tx-type-received-funds-icon, .dao-tx-type-received-funds-icon:hover { -fx-text-fill: -bs-green-soft; } .dao-tx-type-sent-funds-icon, .dao-tx-type-sent-funds-icon:hover { -fx-text-fill: -bs-red-soft; } .dao-tx-type-vote-icon, .dao-tx-type-vote-icon:hover { -fx-text-fill: -bs-color-blue-5; } .dao-tx-type-vote-reveal-icon, .dao-tx-type-vote-reveal-icon:hover { -fx-text-fill: -bs-color-blue-4; } .dao-tx-type-issuance-icon, .dao-tx-type-issuance-icon:hover { -fx-text-fill: -bs-color-green-3; } .dao-tx-type-lockup-icon, .dao-tx-type-lockup-icon:hover { -fx-text-fill: -bs-color-blue-5; } .dao-tx-type-unlock-icon, .dao-tx-type-unlock-icon:hover { -fx-text-fill: -bs-color-green-3; } .dao-tx-type-bsq-swap-icon, .dao-tx-type-bsq-swap-icon:hover { -fx-text-fill: -bs-color-blue-4; } .dao-accepted-icon { -fx-text-fill: -bs-color-primary; } .dao-rejected-icon { -fx-text-fill: -bs-rd-error-red; } .dao-ignored-icon { -fx-text-fill: -bs-color-gray-4; } .compensation-root { -fx-background-insets: 0, 0 0 0 0; } .info-icon { -fx-text-fill: -fx-accent; } .info-icon-button { -fx-cursor: hand; -fx-background-color: transparent; } .price-trend-up { -fx-text-fill: -bs-color-primary; -fx-padding: 2 0 0 0; } .price-trend-down { -fx-text-fill: -bs-red; -fx-padding: 2 0 0 0; } /******************************************************************************************************************** * * * News * * * ********************************************************************************************************************/ .news-version { -fx-alignment: center-left; -fx-font-size: 1em; } .news-feature-headline { -fx-font-size: 1.077em; -fx-text-fill: -bs-rd-font-dark-gray; -fx-font-family: "IBM Plex Sans Medium"; } .news-feature-description { -fx-font-size: 1em; -fx-text-fill: -bs-rd-font-dark-gray; } .news-feature-image { -fx-border-style: solid; -fx-border-width: 1; -fx-border-color: -bs-rd-separator-dark; } /******************************************************************************************************************** * * * Notifications * * * ********************************************************************************************************************/ #notification-erase-button { -fx-background-color: -bs-red-soft; -fx-text-fill: -bs-background-color; } .status-icon { -fx-text-fill: -fx-faint-focus-color; } /******************************************************************************************************************** * * * Popover * * * ********************************************************************************************************************/ .popover > .content { -fx-padding: 10; -fx-background-color: -bs-color-background-popup; -fx-border-radius: 3; -fx-background-radius: 3; -fx-background-insets: 1; } .popover > .content .default-text { -fx-text-fill: -bs-text-color; } .popover > .content .text-field { -fx-background-color: -bs-color-background-form-field-readonly !important; -fx-border-radius: 4; -fx-background-radius: 4; } .popover > .border { -fx-stroke: linear-gradient(to bottom, -bs-text-color-transparent, -bs-text-color-dropshadow) !important; -fx-fill: -bs-color-background-popup !important; } /******************************************************************************************************************** * * * Other * * * ********************************************************************************************************************/ .input-with-border { -fx-border-width: 1; -fx-border-color: -bs-color-border-form-field; -fx-border-insets: 1 0 1 0; -fx-background-insets: 1 0 1 0; } .table-view.non-interactive-table .column-header .label { -fx-text-fill: -bs-text-color-dim2; } .highlight-text { -fx-text-fill: -fx-dark-text-color !important; } .grid-pane .text-area, .flat-text-area-with-border { -fx-background-radius: 8; -fx-border-radius: 8; -fx-font-size: 1.077em; -fx-font-family: "IBM Plex Sans"; -fx-font-weight: normal; -fx-text-fill: -bs-rd-font-dark-gray !important; -fx-border-width: 1; -fx-border-color: -bs-color-border-form-field !important; } .grid-pane .text-area:readonly, .flat-text-area-with-border { -fx-background-color: transparent !important; } .grid-pane .text-area { -fx-max-height: 150 !important; } .passphrase-copy-box { -fx-border-width: 1; -fx-border-color: -bs-color-border-form-field; -fx-background-radius: 8; -fx-border-radius: 8; -fx-padding: 13; -fx-background-insets: 0; } .passphrase-copy-box > .jfx-text-field { -fx-padding: 0; -fx-background-color: transparent; -fx-border-width: 0; } .passphrase-copy-box .label { -fx-text-fill: white; -fx-padding: 0; } .passphrase-copy-box .jfx-button { -fx-padding: 5 15 5 15; -fx-background-radius: 999; -fx-border-radius: 999; -fx-min-height: 0; -fx-font-size: 1.077em; -fx-font-family: "IBM Plex Sans"; -fx-font-weight: normal; } .popup-with-input { -fx-background-color: -bs-color-background-popup-input; } ================================================ FILE: desktop/src/main/java/haveno/desktop/images.css ================================================ #image-info { -fx-image: url("../../images/info.png"); } #light_close { -fx-image: url("../../images/light_close.png"); } #image-alert-round { -fx-image: url("../../images/alert_round.png"); } #image-red_circle_solid { -fx-image: url("../../images/red_circle_solid.png"); } #image-green_circle { -fx-image: url("../../images/green_circle.png"); } #image-green_circle_solid { -fx-image: url("../../images/green_circle_solid.png"); } #image-yellow_circle { -fx-image: url("../../images/yellow_circle.png"); } #image-yellow_circle_solid { -fx-image: url("../../images/yellow_circle_solid.png"); } #image-blue_circle_solid { -fx-image: url("../../images/blue_circle_solid.png"); } #image-remove { -fx-image: url("../../images/remove.png"); } #image-buy-white { -fx-image: url("../../images/buy_white.png"); } #image-buy-green { -fx-image: url("../../images/buy_green.png"); } #image-sell-white { -fx-image: url("../../images/sell_white.png"); } #image-sell-red { -fx-image: url("../../images/sell_red.png"); } #image-lock2x { -fx-image: url("../../images/lock@2x.png"); } #image-expand { -fx-image: url("../../images/expand.png"); } #image-collapse { -fx-image: url("../../images/collapse.png"); } /* navigation buttons */ #image-nav-market { -fx-image: url("../../images/nav/market.png"); } #image-nav-market-active { -fx-image: url("../../images/nav/market_active.png"); } #image-nav-buyoffer { -fx-image: url("../../images/nav/buyoffer.png"); } #image-nav-buyoffer-active { -fx-image: url("../../images/nav/buyoffer_active.png"); } #image-nav-selloffer { -fx-image: url("../../images/nav/selloffer.png"); } #image-nav-selloffer-active { -fx-image: url("../../images/nav/selloffer_active.png"); } #image-nav-portfolio { -fx-image: url("../../images/nav/portfolio.png"); } #image-nav-portfolio-active { -fx-image: url("../../images/nav/portfolio_active.png"); } #image-nav-funds { -fx-image: url("../../images/nav/funds.png"); } #image-nav-funds-active { -fx-image: url("../../images/nav/funds_active.png"); } #image-nav-disputes { -fx-image: url("../../images/nav/disputes.png"); } #image-nav-disputes-active { -fx-image: url("../../images/nav/disputes_active.png"); } #image-nav-settings { -fx-image: url("../../images/nav/settings.png"); } #image-nav-settings-active { -fx-image: url("../../images/nav/settings_active.png"); } #image-nav-account { -fx-image: url("../../images/nav/account.png"); } #image-nav-account-active { -fx-image: url("../../images/nav/account_active.png"); } #image-nav-dao { -fx-image: url("../../images/nav/dao.png"); } #image-nav-dao-active { -fx-image: url("../../images/nav/dao_active.png"); } /* account*/ #image-tick { -fx-image: url("../../images/tick.png"); } #image-arrow-blue { -fx-image: url("../../images/arrow_blue.png"); } #image-arrow-grey { -fx-image: url("../../images/arrow_grey.png"); } /* connection state*/ #image-connection-tor { -fx-image: url("../../images/connection/tor.png"); } #image-connection-direct { -fx-image: url("../../images/connection/direct.png"); } #image-connection-nat { -fx-image: url("../../images/connection/nat.png"); } #image-connection-relay { -fx-image: url("../../images/connection/relay.png"); } #image-connection-synced { -fx-image: url("../../images/connection/synced.png"); } /* software update*/ #image-update-in-progress { -fx-image: url("../../images/update/update_in_progress.png"); } #image-update-up-to-date { -fx-image: url("../../images/update/update_up_to_date.png"); } #image-update-available { -fx-image: url("../../images/update/update_available.png"); } #image-update-failed { -fx-image: url("../../images/update/update_failed.png"); } /* offer state */ #image-offer_state_unknown { -fx-image: url("../../images/offer/offer_state_unknown.png"); } #image-offer_state_available { -fx-image: url("../../images/offer/offer_state_available.png"); } #image-offer_state_not_available { -fx-image: url("../../images/offer/offer_state_not_available.png"); } #image-attachment { -fx-image: url("../../images/attachment.png"); } #bubble_arrow_grey_left { -fx-image: url("../../images/bubble_arrow_grey_left.png"); } #bubble_arrow_blue_left { -fx-image: url("../../images/bubble_arrow_blue_left.png"); } #bubble_arrow_grey_right { -fx-image: url("../../images/bubble_arrow_grey_right.png"); } #bubble_arrow_blue_right { -fx-image: url("../../images/bubble_arrow_blue_right.png"); } #link { -fx-image: url("../../images/link.png"); } #invert { -fx-image: url("../../images/invert.png"); } #avatar_1 { -fx-image: url("../../images/avatars/avatar_1.png"); } #avatar_2 { -fx-image: url("../../images/avatars/avatar_2.png"); } #avatar_3 { -fx-image: url("../../images/avatars/avatar_3.png"); } #avatar_4 { -fx-image: url("../../images/avatars/avatar_4.png"); } #avatar_5 { -fx-image: url("../../images/avatars/avatar_5.png"); } #avatar_6 { -fx-image: url("../../images/avatars/avatar_6.png"); } #avatar_7 { -fx-image: url("../../images/avatars/avatar_7.png"); } #avatar_8 { -fx-image: url("../../images/avatars/avatar_8.png"); } #avatar_9 { -fx-image: url("../../images/avatars/avatar_9.png"); } #avatar_10 { -fx-image: url("../../images/avatars/avatar_10.png"); } #avatar_11 { -fx-image: url("../../images/avatars/avatar_11.png"); } #avatar_12 { -fx-image: url("../../images/avatars/avatar_12.png"); } #avatar_13 { -fx-image: url("../../images/avatars/avatar_13.png"); } #avatar_14 { -fx-image: url("../../images/avatars/avatar_14.png"); } #avatar_15 { -fx-image: url("../../images/avatars/avatar_15.png"); } #image-account-signing-screenshot { -fx-image: url("../../images/account_signing_screenshot.png"); } #image-new-trade-protocol-screenshot { -fx-image: url("../../images/new_trade_protocol_screenshot.png"); } #image-support { -fx-image: url("../../images/support.png"); } #image-account { -fx-image: url("../../images/account.png"); } #image-settings { -fx-image: url("../../images/settings.png"); } #image-fiat-logo { -fx-image: url("../../images/fiat_logo_light_mode.png"); } #image-btc-logo { -fx-image: url("../../images/btc_logo.png"); } #image-bch-logo { -fx-image: url("../../images/bch_logo.png"); } #image-dai-erc20-logo { -fx-image: url("../../images/dai-erc20_logo.png"); } #image-eth-logo { -fx-image: url("../../images/eth_logo.png"); } #image-ltc-logo { -fx-image: url("../../images/ltc_logo.png"); } #image-usdc-erc20-logo { -fx-image: url("../../images/usdc-erc20_logo.png"); } #image-usdt-erc20-logo { -fx-image: url("../../images/usdt-erc20_logo.png"); } #image-usdt-trc20-logo { -fx-image: url("../../images/usdt-trc20_logo.png"); } #image-xmr-logo { -fx-image: url("../../images/xmr_logo.png"); } #image-xrp-logo { -fx-image: url("../../images/xrp_logo.png"); } #image-ada-logo { -fx-image: url("../../images/ada_logo.png"); } #image-sol-logo { -fx-image: url("../../images/sol_logo.png"); } #image-trx-logo { -fx-image: url("../../images/trx_logo.png"); } #image-doge-logo { -fx-image: url("../../images/doge_logo.png"); } #image-dark-mode-toggle { -fx-image: url("../../images/dark_mode_toggle.png"); } #image-light-mode-toggle { -fx-image: url("../../images/light_mode_toggle.png"); } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/MainView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/MainView.java ================================================ /* * 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 . */ package haveno.desktop.main; import com.google.inject.Inject; import com.jfoenix.controls.JFXBadge; import com.jfoenix.controls.JFXComboBox; import haveno.common.HavenoException; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; import haveno.common.util.Tuple2; import haveno.common.util.Utilities; import haveno.core.locale.GlobalSettings; import haveno.core.locale.LanguageUtil; import haveno.core.locale.Res; import haveno.core.provider.price.MarketPrice; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.desktop.Navigation; import haveno.desktop.common.view.CachingViewLoader; import haveno.desktop.common.view.FxmlView; import haveno.desktop.common.view.InitializableView; import haveno.desktop.common.view.View; import haveno.desktop.common.view.ViewLoader; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.AutoTooltipToggleButton; import haveno.desktop.components.BusyAnimation; import haveno.desktop.main.account.AccountView; import haveno.desktop.main.funds.FundsView; import haveno.desktop.main.market.MarketView; import haveno.desktop.main.market.offerbook.OfferBookChartView; import haveno.desktop.main.offer.BuyOfferView; import haveno.desktop.main.offer.SellOfferView; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.TorNetworkSettingsWindow; import haveno.desktop.main.portfolio.PortfolioView; import haveno.desktop.main.settings.SettingsView; import haveno.desktop.main.shared.PriceFeedComboBoxItem; import haveno.desktop.main.support.SupportView; import haveno.desktop.util.DisplayUtils; import haveno.desktop.util.Transitions; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.Date; import java.util.Locale; import javafx.animation.Animation; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.beans.binding.ObjectBinding; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import javafx.geometry.Insets; import javafx.geometry.NodeOrientation; import javafx.geometry.Orientation; import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ProgressBar; import javafx.scene.control.Separator; import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleGroup; import javafx.scene.control.Tooltip; import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.AnchorPane; import static javafx.scene.layout.AnchorPane.setBottomAnchor; import static javafx.scene.layout.AnchorPane.setLeftAnchor; import static javafx.scene.layout.AnchorPane.setRightAnchor; import static javafx.scene.layout.AnchorPane.setTopAnchor; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.scene.text.TextAlignment; import javafx.util.Duration; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; @FxmlView @Slf4j public class MainView extends InitializableView { // If after 30 sec we have not got connected we show "open network settings" button private final static int SHOW_TOR_SETTINGS_DELAY_SEC = 90; @Setter private Runnable onApplicationStartedHandler; private static Transitions transitions; private static StackPane rootContainer; private final ViewLoader viewLoader; private final Navigation navigation; private final ToggleGroup navButtons = new ToggleGroup(); private ChangeListener walletServiceErrorMsgListener; private ChangeListener xmrSyncIconIdListener; private ChangeListener splashP2PNetworkErrorMsgListener; private ChangeListener splashP2PNetworkIconIdListener; private ChangeListener splashP2PNetworkVisibleListener; private BusyAnimation splashP2PNetworkBusyAnimation; private Label splashP2PNetworkLabel; private ProgressBar xmrSyncIndicator; private Label xmrSplashInfo; private Popup p2PNetworkWarnMsgPopup, xmrNetworkWarnMsgPopup; private final TorNetworkSettingsWindow torNetworkSettingsWindow; private final Preferences preferences; private static final int networkIconSize = 20; public static StackPane getRootContainer() { return MainView.rootContainer; } public static void blurLight() { transitions.blur(MainView.rootContainer, Transitions.DEFAULT_DURATION, -0.6, false, 15); } public static void blurUltraLight() { transitions.blur(MainView.rootContainer, Transitions.DEFAULT_DURATION, -0.6, false, 15); } public static void darken() { transitions.darken(MainView.rootContainer, Transitions.DEFAULT_DURATION, false); } public static void removeEffect() { transitions.removeEffect(MainView.rootContainer); } @Inject public MainView(MainViewModel model, CachingViewLoader viewLoader, Navigation navigation, Transitions transitions, TorNetworkSettingsWindow torNetworkSettingsWindow, Preferences preferences) { super(model); this.viewLoader = viewLoader; this.navigation = navigation; MainView.transitions = transitions; this.torNetworkSettingsWindow = torNetworkSettingsWindow; this.preferences = preferences; } @Override protected void initialize() { MainView.rootContainer = root; if (LanguageUtil.isDefaultLanguageRTL()) MainView.rootContainer.setNodeOrientation(NodeOrientation.RIGHT_TO_LEFT); ToggleButton marketButton = new NavButton(MarketView.class, Res.get("mainView.menu.market")); ToggleButton buyButton = new NavButton(BuyOfferView.class, Res.get("mainView.menu.buyXmr")); ToggleButton sellButton = new NavButton(SellOfferView.class, Res.get("mainView.menu.sellXmr")); ToggleButton portfolioButton = new NavButton(PortfolioView.class, Res.get("mainView.menu.portfolio")); ToggleButton fundsButton = new NavButton(FundsView.class, Res.get("mainView.menu.funds")); ToggleButton supportButton = new SecondaryNavButton(SupportView.class, Res.get("mainView.menu.support"), "image-support"); ToggleButton accountButton = new SecondaryNavButton(AccountView.class, Res.get("mainView.menu.account"), "image-account"); ToggleButton settingsButton = new SecondaryNavButton(SettingsView.class, Res.get("mainView.menu.settings"), "image-settings"); JFXBadge portfolioButtonWithBadge = new JFXBadge(portfolioButton); JFXBadge supportButtonWithBadge = new JFXBadge(supportButton); JFXBadge settingsButtonWithBadge = new JFXBadge(settingsButton); Locale locale = GlobalSettings.getLocale(); DecimalFormat currencyFormat = (DecimalFormat) NumberFormat.getNumberInstance(locale); currencyFormat.setMinimumFractionDigits(2); currencyFormat.setMaximumFractionDigits(2); root.sceneProperty().addListener((observable, oldValue, newValue) -> { if (newValue != null) { newValue.addEventHandler(KeyEvent.KEY_RELEASED, keyEvent -> { if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT1, keyEvent)) { marketButton.fire(); } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT2, keyEvent)) { buyButton.fire(); } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT3, keyEvent)) { sellButton.fire(); } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT4, keyEvent)) { portfolioButton.fire(); } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT5, keyEvent)) { fundsButton.fire(); } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT6, keyEvent)) { supportButton.fire(); } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT8, keyEvent)) { accountButton.fire(); } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT7, keyEvent)) { settingsButton.fire(); } }); } }); Tuple2, VBox> marketPriceBox = getMarketPriceBox(); ComboBox priceComboBox = marketPriceBox.first; priceComboBox.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> model.setPriceFeedComboBoxItem(newValue)); ChangeListener selectedPriceFeedItemListener = (observable, oldValue, newValue) -> { if (newValue != null) priceComboBox.getSelectionModel().select(newValue); }; model.getSelectedPriceFeedComboBoxItemProperty().addListener(selectedPriceFeedItemListener); priceComboBox.setItems(model.getPriceFeedComboBoxItems()); Tuple2 availableBalanceBox = getBalanceBox(Res.get("mainView.balance.available")); availableBalanceBox.first.textProperty().bind(model.getAvailableBalance()); availableBalanceBox.first.setPrefWidth(112); availableBalanceBox.first.tooltipProperty().bind(new ObjectBinding<>() { { bind(model.getAvailableBalance()); bind(model.getMarketPrice()); } @Override protected Tooltip computeValue() { String tooltipText = Res.get("mainView.balance.available"); try { String preferredTradeCurrency = model.getPreferences().getPreferredTradeCurrency().getCode(); double availableBalance = Double.parseDouble( model.getAvailableBalance().getValue().replace("XMR", "")); double marketPrice = Double.parseDouble(model.getMarketPrice(preferredTradeCurrency).getValue()); tooltipText += "\n" + currencyFormat.format(availableBalance * marketPrice) + " " + preferredTradeCurrency; } catch (NullPointerException | NumberFormatException e) { // Either the balance or market price is not available yet } return new Tooltip(tooltipText); } }); Tuple2 reservedBalanceBox = getBalanceBox(Res.get("mainView.balance.reserved.short")); reservedBalanceBox.first.textProperty().bind(model.getReservedBalance()); reservedBalanceBox.first.tooltipProperty().bind(new ObjectBinding<>() { { bind(model.getReservedBalance()); bind(model.getMarketPrice()); } @Override protected Tooltip computeValue() { String tooltipText = Res.get("mainView.balance.reserved"); try { String preferredTradeCurrency = model.getPreferences().getPreferredTradeCurrency().getCode(); double reservedBalance = Double.parseDouble( model.getReservedBalance().getValue().replace("XMR", "")); double marketPrice = Double.parseDouble(model.getMarketPrice(preferredTradeCurrency).getValue()); tooltipText += "\n" + currencyFormat.format(reservedBalance * marketPrice) + " " + preferredTradeCurrency; } catch (NullPointerException | NumberFormatException e) { // Either the balance or market price is not available yet } return new Tooltip(tooltipText); } }); Tuple2 pendingBalanceBox = getBalanceBox(Res.get("mainView.balance.pending.short")); pendingBalanceBox.first.textProperty().bind(model.getPendingBalance()); pendingBalanceBox.first.tooltipProperty().bind(new ObjectBinding<>() { { bind(model.getPendingBalance()); bind(model.getMarketPrice()); } @Override protected Tooltip computeValue() { String tooltipText = Res.get("mainView.balance.pending"); try { String preferredTradeCurrency = model.getPreferences().getPreferredTradeCurrency().getCode(); double lockedBalance = Double.parseDouble( model.getPendingBalance().getValue().replace("XMR", "")); double marketPrice = Double.parseDouble(model.getMarketPrice(preferredTradeCurrency).getValue()); tooltipText += "\n" + currencyFormat.format(lockedBalance * marketPrice) + " " + preferredTradeCurrency; } catch (NullPointerException | NumberFormatException e) { // Either the balance or market price is not available yet } return new Tooltip(tooltipText); } }); // add spacer to center the nav buttons when window is small Region rightSpacer = new Region(); HBox.setHgrow(rightSpacer, Priority.ALWAYS); HBox primaryNav = new HBox(getLogoPane(), marketButton, getNavigationSpacer(), buyButton, getNavigationSpacer(), sellButton, getNavigationSpacer(), portfolioButtonWithBadge, getNavigationSpacer(), fundsButton, rightSpacer); primaryNav.setAlignment(Pos.CENTER_LEFT); primaryNav.getStyleClass().add("nav-primary"); HBox.setHgrow(primaryNav, Priority.SOMETIMES); HBox priceAndBalance = new HBox(marketPriceBox.second, getNavigationSeparator(), availableBalanceBox.second, getNavigationSeparator(), pendingBalanceBox.second, getNavigationSeparator(), reservedBalanceBox.second); priceAndBalance.setAlignment(Pos.CENTER); priceAndBalance.setSpacing(12); priceAndBalance.getStyleClass().add("nav-price-balance"); HBox navPane = new HBox(primaryNav, priceAndBalance) {{ setLeftAnchor(this, 25d); setRightAnchor(this, 25d); setTopAnchor(this, 20d); setPadding(new Insets(0, 0, 0, 0)); getStyleClass().add("top-navigation"); }}; navPane.setAlignment(Pos.CENTER); HBox secondaryNav = new HBox(supportButtonWithBadge, accountButton, settingsButtonWithBadge); secondaryNav.getStyleClass().add("nav-secondary"); secondaryNav.setAlignment(Pos.CENTER_RIGHT); secondaryNav.setPickOnBounds(false); HBox.setHgrow(secondaryNav, Priority.ALWAYS); AnchorPane.setLeftAnchor(secondaryNav, 0.0); AnchorPane.setRightAnchor(secondaryNav, 0.0); AnchorPane.setTopAnchor(secondaryNav, 0.0); AnchorPane secondaryNavContainer = new AnchorPane() {{ setId("nav-secondary-container"); setLeftAnchor(this, 0d); setRightAnchor(this, 0d); setTopAnchor(this, 94d); }}; secondaryNavContainer.setPickOnBounds(false); secondaryNavContainer.getChildren().add(secondaryNav); AnchorPane contentContainer = new AnchorPane() {{ getStyleClass().add("content-pane"); setLeftAnchor(this, 0d); setRightAnchor(this, 0d); setTopAnchor(this, 95d); setBottomAnchor(this, 0d); }}; AnchorPane applicationContainer = new AnchorPane(navPane, contentContainer, secondaryNavContainer) {{ setId("application-container"); }}; BorderPane baseApplicationContainer = new BorderPane(applicationContainer) {{ setId("base-content-container"); }}; baseApplicationContainer.setBottom(createFooter()); setupBadge(portfolioButtonWithBadge, model.getNumPendingTrades(), model.getShowPendingTradesNotification()); setupBadge(supportButtonWithBadge, model.getNumOpenSupportTickets(), model.getShowOpenSupportTicketsNotification()); setupBadge(settingsButtonWithBadge, new SimpleStringProperty(Res.get("shared.new")), new SimpleBooleanProperty(false)); settingsButtonWithBadge.getStyleClass().add("new"); navigation.addListener((viewPath, data) -> { UserThread.await(() -> { // TODO: this uses `await` to fix nagivation link from market view to offer book, but await can cause hanging, so execute should be used if (viewPath.size() != 2 || viewPath.indexOf(MainView.class) != 0) return; Class viewClass = viewPath.tip(); View view = viewLoader.load(viewClass); contentContainer.getChildren().setAll(view.getRoot()); try { navButtons.getToggles().stream() .filter(toggle -> toggle instanceof NavButton) .filter(button -> viewClass == ((NavButton) button).viewClass) .findFirst() .orElseThrow(() -> new HavenoException("No button matching %s found", viewClass)) .setSelected(true); } catch (HavenoException e) { navigation.navigateTo(MainView.class, MarketView.class, OfferBookChartView.class); } }); }); VBox splashScreen = createSplashScreen(); root.getChildren().addAll(baseApplicationContainer, splashScreen); model.getShowAppScreen().addListener((ov, oldValue, newValue) -> { if (newValue) { navigation.navigateToPreviousVisitedView(); transitions.fadeOutAndRemove(splashScreen, 1500, actionEvent -> disposeSplashScreen()); } }); // Delay a bit to give time for rendering the splash screen UserThread.execute(() -> onApplicationStartedHandler.run()); } /////////////////////////////////////////////////////////////////////////////////////////// // Helpers /////////////////////////////////////////////////////////////////////////////////////////// @NotNull private Separator getNavigationSeparator() { final Separator separator = new Separator(Orientation.VERTICAL); HBox.setHgrow(separator, Priority.ALWAYS); separator.setMaxWidth(Double.MAX_VALUE); separator.getStyleClass().add("nav-separator"); return separator; } @NotNull private Pane getLogoPane() { ImageView logo = new ImageView(); logo.setId("image-logo-landscape"); logo.setPreserveRatio(true); logo.setFitHeight(40); logo.setSmooth(true); logo.setCache(true); final Pane pane = new Pane(); HBox.setHgrow(pane, Priority.ALWAYS); pane.getStyleClass().add("nav-logo"); pane.getChildren().add(logo); return pane; } @NotNull private Region getNavigationSpacer() { final Region spacer = new Region(); HBox.setHgrow(spacer, Priority.ALWAYS); spacer.getStyleClass().add("nav-spacer"); return spacer; } private Tuple2 getBalanceBox(String text) { Label balanceDisplay = new Label(); balanceDisplay.getStyleClass().add("nav-balance-display"); Label label = new Label(text); label.getStyleClass().add("nav-balance-label"); label.maxWidthProperty().bind(balanceDisplay.widthProperty()); label.setPadding(new Insets(0, 0, 0, 0)); VBox vBox = new VBox(); vBox.setAlignment(Pos.CENTER_LEFT); vBox.getChildren().addAll(balanceDisplay, label); return new Tuple2<>(balanceDisplay, vBox); } private ListCell getPriceFeedComboBoxListCell() { return new ListCell<>() { @Override protected void updateItem(PriceFeedComboBoxItem item, boolean empty) { super.updateItem(item, empty); if (!empty && item != null) { textProperty().bind(item.displayStringProperty); } else { textProperty().unbind(); } } }; } private Tuple2, VBox> getMarketPriceBox() { VBox marketPriceBox = new VBox(); marketPriceBox.setAlignment(Pos.CENTER_LEFT); ComboBox priceComboBox = new JFXComboBox<>(); priceComboBox.setVisibleRowCount(12); priceComboBox.setFocusTraversable(false); priceComboBox.setId("price-feed-combo"); priceComboBox.setCellFactory(p -> getPriceFeedComboBoxListCell()); ListCell buttonCell = getPriceFeedComboBoxListCell(); buttonCell.setId("price-feed-combo"); priceComboBox.setButtonCell(buttonCell); Label marketPriceLabel = new Label(); updateMarketPriceLabel(marketPriceLabel); marketPriceLabel.getStyleClass().add("nav-balance-label"); marketPriceBox.getChildren().addAll(priceComboBox, marketPriceLabel); model.getMarketPriceUpdated().addListener((observable, oldValue, newValue) -> updateMarketPriceLabel(marketPriceLabel)); return new Tuple2<>(priceComboBox, marketPriceBox); } @NotNull private String getPriceProvider() { return model.getIsFiatCurrencyPriceFeedSelected().get() ? "BitcoinAverage" : "Poloniex"; } private void updateMarketPriceLabel(Label label) { if (model.getIsPriceAvailable().get()) { if (model.getIsExternallyProvidedPrice().get()) { label.setText(Res.get("mainView.marketPriceWithProvider.label", "Haveno Price Index")); label.setTooltip(new Tooltip(getPriceProviderTooltipString())); } else { label.setText(Res.get("mainView.marketPrice.havenoInternalPrice")); final Tooltip tooltip = new Tooltip(Res.get("mainView.marketPrice.tooltip.havenoInternalPrice")); label.setTooltip(tooltip); } } else { label.setText(""); label.setTooltip(null); } } @NotNull private String getPriceProviderTooltipString() { String selectedCurrencyCode = model.getPriceFeedService().getCurrencyCode(); MarketPrice selectedMarketPrice = model.getPriceFeedService().getMarketPrice(selectedCurrencyCode); return Res.get("mainView.marketPrice.tooltip", "Haveno Price Index for " + selectedCurrencyCode, "", selectedMarketPrice != null ? DisplayUtils.formatTime(new Date(selectedMarketPrice.getTimestampSec())) : Res.get("shared.na"), model.getPriceFeedService().getProviderNodeAddress()); } private VBox createSplashScreen() { VBox vBox = new VBox(); vBox.setAlignment(Pos.CENTER); vBox.setSpacing(10); vBox.setId("splash"); ImageView logo = new ImageView(); logo.setId(Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_MAINNET ? "image-logo-splash" : "image-logo-splash-testnet"); logo.setFitWidth(400); logo.setPreserveRatio(true); logo.setSmooth(true); // createBitcoinInfoBox xmrSplashInfo = new AutoTooltipLabel(); xmrSplashInfo.textProperty().bind(model.getXmrInfo()); walletServiceErrorMsgListener = (ov, oldValue, newValue) -> { UserThread.execute(() -> { if (newValue != null && !newValue.isEmpty()) { xmrSplashInfo.setId("splash-error-state-msg"); if (!xmrSplashInfo.getStyleClass().contains("error-text")) xmrSplashInfo.getStyleClass().add("error-text"); } else { xmrSplashInfo.setId(""); xmrSplashInfo.getStyleClass().remove("error-text"); } }); }; model.getConnectionServiceErrorMsg().addListener(walletServiceErrorMsgListener); xmrSyncIndicator = new ProgressBar(); xmrSyncIndicator.setPrefWidth(305); xmrSyncIndicator.progressProperty().bind(model.getCombinedSyncProgress()); ImageView xmrSyncIcon = new ImageView(); xmrSyncIcon.setVisible(false); xmrSyncIcon.setManaged(false); xmrSyncIconIdListener = (ov, oldValue, newValue) -> { xmrSyncIcon.setId(newValue); xmrSyncIcon.setVisible(true); xmrSyncIcon.setManaged(true); // show progress bar until we have checkmark id boolean inProgress = "".equals(newValue); xmrSyncIndicator.setVisible(inProgress); xmrSyncIndicator.setManaged(inProgress); }; model.getXmrSplashSyncIconId().addListener(xmrSyncIconIdListener); HBox blockchainSyncBox = new HBox(); blockchainSyncBox.setSpacing(10); blockchainSyncBox.setAlignment(Pos.CENTER); blockchainSyncBox.setPadding(new Insets(40, 0, 0, 0)); blockchainSyncBox.setPrefHeight(50); blockchainSyncBox.getChildren().addAll(xmrSplashInfo, xmrSyncIcon); // create P2PNetworkBox splashP2PNetworkLabel = new AutoTooltipLabel(); splashP2PNetworkLabel.setWrapText(true); splashP2PNetworkLabel.setMaxWidth(700); splashP2PNetworkLabel.setTextAlignment(TextAlignment.CENTER); splashP2PNetworkLabel.getStyleClass().add("sub-info"); splashP2PNetworkLabel.textProperty().bind(model.getP2PNetworkInfo()); Button showTorNetworkSettingsButton = new AutoTooltipButton(Res.get("settings.net.openTorSettingsButton")); showTorNetworkSettingsButton.setVisible(false); showTorNetworkSettingsButton.setManaged(false); showTorNetworkSettingsButton.setOnAction(e -> model.getTorNetworkSettingsWindow().show()); splashP2PNetworkBusyAnimation = new BusyAnimation(false); splashP2PNetworkErrorMsgListener = (ov, oldValue, newValue) -> { if (newValue != null) { splashP2PNetworkLabel.setId("splash-error-state-msg"); splashP2PNetworkLabel.getStyleClass().remove("sub-info"); splashP2PNetworkLabel.getStyleClass().add("error-text"); splashP2PNetworkBusyAnimation.setDisable(true); splashP2PNetworkBusyAnimation.stop(); showTorNetworkSettingsButton.setVisible(true); showTorNetworkSettingsButton.setManaged(true); if (model.getUseTorForXmr().get().isUseTorForXmr()) { // If using tor for XMR, hide the XMR status since tor is not working xmrSyncIndicator.setVisible(false); xmrSplashInfo.setVisible(false); } } else if (model.getSplashP2PNetworkAnimationVisible().get()) { splashP2PNetworkBusyAnimation.setDisable(false); splashP2PNetworkBusyAnimation.play(); } }; model.getP2pNetworkWarnMsg().addListener(splashP2PNetworkErrorMsgListener); ImageView splashP2PNetworkIcon = new ImageView(); splashP2PNetworkIcon.setId("image-connection-tor"); splashP2PNetworkIcon.setFitWidth(networkIconSize); splashP2PNetworkIcon.setFitHeight(networkIconSize); splashP2PNetworkIcon.setVisible(false); splashP2PNetworkIcon.setManaged(false); HBox.setMargin(splashP2PNetworkIcon, new Insets(0, 0, 0, 0)); splashP2PNetworkIcon.setOnMouseClicked(e -> { torNetworkSettingsWindow.show(); }); Timer showTorNetworkSettingsTimer = UserThread.runAfter(() -> { showTorNetworkSettingsButton.setVisible(true); showTorNetworkSettingsButton.setManaged(true); }, SHOW_TOR_SETTINGS_DELAY_SEC); splashP2PNetworkIconIdListener = (ov, oldValue, newValue) -> { splashP2PNetworkIcon.setId(newValue); splashP2PNetworkIcon.setVisible(true); splashP2PNetworkIcon.setManaged(true); splashP2PNetworkIcon.setFitWidth(networkIconSize); splashP2PNetworkIcon.setFitHeight(networkIconSize); // if we can connect in 10 sec. we know that tor is working showTorNetworkSettingsTimer.stop(); }; model.getP2PNetworkIconId().addListener(splashP2PNetworkIconIdListener); splashP2PNetworkVisibleListener = (ov, oldValue, newValue) -> { splashP2PNetworkBusyAnimation.setDisable(!newValue); if (newValue) splashP2PNetworkBusyAnimation.play(); }; model.getSplashP2PNetworkAnimationVisible().addListener(splashP2PNetworkVisibleListener); HBox splashP2PNetworkBox = new HBox(); splashP2PNetworkBox.setSpacing(10); splashP2PNetworkBox.setAlignment(Pos.CENTER); splashP2PNetworkBox.setPrefHeight(40); splashP2PNetworkBox.getChildren().addAll(splashP2PNetworkLabel, splashP2PNetworkBusyAnimation, splashP2PNetworkIcon, showTorNetworkSettingsButton); Label versionLabel = new Label(FormattingUtils.formatVersion()); vBox.getChildren().addAll(logo, blockchainSyncBox, xmrSyncIndicator, splashP2PNetworkBox, versionLabel); return vBox; } private void disposeSplashScreen() { model.getConnectionServiceErrorMsg().removeListener(walletServiceErrorMsgListener); model.getXmrSplashSyncIconId().removeListener(xmrSyncIconIdListener); model.getP2pNetworkWarnMsg().removeListener(splashP2PNetworkErrorMsgListener); model.getP2PNetworkIconId().removeListener(splashP2PNetworkIconIdListener); model.getSplashP2PNetworkAnimationVisible().removeListener(splashP2PNetworkVisibleListener); xmrSplashInfo.textProperty().unbind(); xmrSyncIndicator.progressProperty().unbind(); splashP2PNetworkLabel.textProperty().unbind(); model.onSplashScreenRemoved(); } private AnchorPane createFooter() { // line Separator separator = new Separator(); separator.setId("footer-pane-line"); separator.setPrefHeight(1); setLeftAnchor(separator, 0d); setRightAnchor(separator, 0d); setTopAnchor(separator, 0d); // XMR Label xmrInfoLabel = new AutoTooltipLabel(); xmrInfoLabel.setId("footer-pane"); xmrInfoLabel.textProperty().bind(model.getXmrInfo()); setLeftAnchor(xmrInfoLabel, 10d); setBottomAnchor(xmrInfoLabel, 7d); // temporarily disabled due to high CPU usage (per issue #4649) //ProgressBar blockchainSyncIndicator = new JFXProgressBar(-1); //blockchainSyncIndicator.setPrefWidth(80); //blockchainSyncIndicator.setMaxHeight(10); //blockchainSyncIndicator.progressProperty().bind(model.getCombinedSyncProgress()); model.getConnectionServiceErrorMsg().addListener((ov, oldValue, newValue) -> { UserThread.execute(() -> { if (newValue != null && !newValue.isEmpty()) { xmrInfoLabel.setId("splash-error-state-msg"); if (!xmrInfoLabel.getStyleClass().contains("error-text")) xmrInfoLabel.getStyleClass().add("error-text"); xmrNetworkWarnMsgPopup = new Popup().warning(newValue); xmrNetworkWarnMsgPopup.show(); } else { xmrInfoLabel.setId("footer-pane"); xmrInfoLabel.getStyleClass().remove("error-text"); if (xmrNetworkWarnMsgPopup != null) xmrNetworkWarnMsgPopup.hide(); } }); }); model.getTopErrorMsg().addListener((ov, oldValue, newValue) -> { log.warn("Top level warning: " + newValue); if (newValue != null) { new Popup().warning(newValue).show(); } }); // temporarily disabled due to high CPU usage (per issue #4649) //model.getCombinedSyncProgress().addListener((ov, oldValue, newValue) -> { // if ((double) newValue >= 1) { // blockchainSyncIndicator.setVisible(false); // blockchainSyncIndicator.setManaged(false); // } //}); // version Label versionLabel = new AutoTooltipLabel(); versionLabel.setId("footer-pane"); versionLabel.setTextAlignment(TextAlignment.CENTER); versionLabel.setAlignment(Pos.BASELINE_CENTER); versionLabel.textProperty().bind(model.getCombinedFooterInfo()); root.widthProperty().addListener((ov, oldValue, newValue) -> versionLabel.setLayoutX(((double) newValue - versionLabel.getWidth()) / 2)); model.getNewVersionAvailableProperty().addListener((observable, oldValue, newValue) -> { versionLabel.getStyleClass().removeAll("version-new", "version"); if (newValue) { versionLabel.getStyleClass().add("version-new"); versionLabel.setOnMouseClicked(e -> model.onOpenDownloadWindow()); } else { versionLabel.getStyleClass().add("version"); versionLabel.setOnMouseClicked(null); } }); HBox versionBox = new HBox(); versionBox.setSpacing(10); versionBox.setAlignment(Pos.CENTER); versionBox.setAlignment(Pos.BASELINE_CENTER); versionBox.getChildren().addAll(versionLabel); //blockchainSyncIndicator removed per issue #4649 setLeftAnchor(versionBox, 10d); setRightAnchor(versionBox, 10d); setBottomAnchor(versionBox, 7d); // Dark mode toggle ImageView useDarkModeIcon = new ImageView(); useDarkModeIcon.setId(preferences.getCssTheme() == 1 ? "image-dark-mode-toggle" : "image-light-mode-toggle"); useDarkModeIcon.setFitHeight(networkIconSize); useDarkModeIcon.setPreserveRatio(true); useDarkModeIcon.setPickOnBounds(true); useDarkModeIcon.setCursor(Cursor.HAND); setRightAnchor(useDarkModeIcon, 8d); setBottomAnchor(useDarkModeIcon, 6d); Tooltip modeToolTip = new Tooltip(); Tooltip.install(useDarkModeIcon, modeToolTip); useDarkModeIcon.setOnMouseEntered(e -> modeToolTip.setText(Res.get(preferences.getCssTheme() == 1 ? "setting.preferences.useLightMode" : "setting.preferences.useDarkMode"))); useDarkModeIcon.setOnMouseClicked(e -> { preferences.setCssTheme(preferences.getCssTheme() != 1); }); preferences.getCssThemeProperty().addListener((observable, oldValue, newValue) -> { useDarkModeIcon.setId(preferences.getCssTheme() == 1 ? "image-dark-mode-toggle" : "image-light-mode-toggle"); }); // P2P Network Label p2PNetworkLabel = new AutoTooltipLabel(); p2PNetworkLabel.setId("footer-pane"); p2PNetworkLabel.textProperty().bind(model.getP2PNetworkInfo()); double networkIconRightAnchor = 54d; ImageView p2PNetworkIcon = new ImageView(); setRightAnchor(p2PNetworkIcon, networkIconRightAnchor); setBottomAnchor(p2PNetworkIcon, 6d); p2PNetworkIcon.setPickOnBounds(true); p2PNetworkIcon.setCursor(Cursor.HAND); p2PNetworkIcon.setOpacity(0.4); p2PNetworkIcon.setFitWidth(networkIconSize); p2PNetworkIcon.setFitHeight(networkIconSize); p2PNetworkIcon.idProperty().bind(model.getP2PNetworkIconId()); p2PNetworkLabel.idProperty().bind(model.getP2pNetworkLabelId()); model.getP2pNetworkWarnMsg().addListener((ov, oldValue, newValue) -> { if (newValue != null) { p2PNetworkWarnMsgPopup = new Popup().warning(newValue); p2PNetworkWarnMsgPopup.show(); } else if (p2PNetworkWarnMsgPopup != null) { p2PNetworkWarnMsgPopup.hide(); } }); p2PNetworkIcon.setOnMouseClicked(e -> { torNetworkSettingsWindow.show(); }); ImageView p2PNetworkStatusIcon = new ImageView(); p2PNetworkStatusIcon.setPickOnBounds(true); p2PNetworkStatusIcon.setCursor(Cursor.HAND); p2PNetworkStatusIcon.setFitWidth(networkIconSize); p2PNetworkStatusIcon.setFitHeight(networkIconSize); p2PNetworkStatusIcon.setId(model.getP2PNetworkStatusIconId().get()); setRightAnchor(p2PNetworkStatusIcon, networkIconRightAnchor + 22); setBottomAnchor(p2PNetworkStatusIcon, 6d); Tooltip p2pNetworkStatusToolTip = new Tooltip(); Tooltip.install(p2PNetworkStatusIcon, p2pNetworkStatusToolTip); p2PNetworkStatusIcon.setOnMouseEntered(e -> p2pNetworkStatusToolTip.setText(model.getP2pConnectionSummary())); Timeline flasher = new Timeline( new KeyFrame(Duration.seconds(0.5), e -> p2PNetworkStatusIcon.setOpacity(0.2)), new KeyFrame(Duration.seconds(1.0), e -> p2PNetworkStatusIcon.setOpacity(1)) ); flasher.setCycleCount(Animation.INDEFINITE); model.getP2PNetworkStatusIconId().addListener((ov, oldValue, newValue) -> { if (newValue.equalsIgnoreCase("flashing:image-yellow_circle")) { p2PNetworkStatusIcon.setId("image-yellow_circle"); flasher.play(); } else { p2PNetworkStatusIcon.setId(newValue); flasher.stop(); p2PNetworkStatusIcon.setOpacity(1); } }); p2PNetworkStatusIcon.setOnMouseClicked(e -> { if (p2PNetworkStatusIcon.getId().equalsIgnoreCase("image-alert-round")) { new Popup().warning(Res.get("popup.info.p2pStatusIndicator.red", model.getP2pConnectionSummary())).show(); } else if (p2PNetworkStatusIcon.getId().equalsIgnoreCase("image-yellow_circle")) { new Popup().information(Res.get("popup.info.p2pStatusIndicator.yellow", model.getP2pConnectionSummary())).show(); } else { new Popup().information(Res.get("popup.info.p2pStatusIndicator.green", model.getP2pConnectionSummary())).show(); } }); model.getUpdatedDataReceived().addListener((observable, oldValue, newValue) -> UserThread.execute(() -> { p2PNetworkIcon.setOpacity(1); })); VBox vBox = new VBox(); vBox.setAlignment(Pos.CENTER_RIGHT); vBox.getChildren().addAll(p2PNetworkLabel); setRightAnchor(vBox, networkIconRightAnchor + 45); setBottomAnchor(vBox, 7d); return new AnchorPane(separator, xmrInfoLabel, versionBox, vBox, p2PNetworkStatusIcon, p2PNetworkIcon, useDarkModeIcon) {{ setId("footer-pane"); setMinHeight(30); setMaxHeight(30); }}; } private void setupBadge(JFXBadge buttonWithBadge, StringProperty badgeNumber, BooleanProperty badgeEnabled) { buttonWithBadge.textProperty().bind(badgeNumber); buttonWithBadge.setEnabled(badgeEnabled.get()); badgeEnabled.addListener((observable, oldValue, newValue) -> UserThread.execute(() -> { buttonWithBadge.setEnabled(newValue); buttonWithBadge.refreshBadge(); })); buttonWithBadge.setPosition(Pos.TOP_RIGHT); buttonWithBadge.setMinHeight(34); buttonWithBadge.setMaxHeight(34); } private class NavButton extends AutoTooltipToggleButton { private final Class viewClass; NavButton(Class viewClass, String title) { super(title); this.viewClass = viewClass; this.setToggleGroup(navButtons); this.getStyleClass().add("nav-button"); this.setMinWidth(Region.USE_PREF_SIZE); // prevent squashing content this.setPrefWidth(Region.USE_COMPUTED_SIZE); // Japanese fonts are dense, increase top nav button text size if (model.getPreferences() != null && "ja".equals(model.getPreferences().getUserLanguage())) { this.getStyleClass().add("nav-button-japanese"); } this.selectedProperty().addListener((ov, oldValue, newValue) -> this.setMouseTransparent(newValue)); this.setOnAction(e -> navigation.navigateTo(MainView.class, viewClass)); } } private class SecondaryNavButton extends NavButton { SecondaryNavButton(Class viewClass, String title, String iconId) { super(viewClass, title); this.getStyleClass().setAll("nav-secondary-button"); // Japanese fonts are dense, increase top nav button text size if (model.getPreferences() != null && "ja".equals(model.getPreferences().getUserLanguage())) { this.getStyleClass().setAll("nav-secondary-button-japanese"); } // add icon ImageView imageView = new ImageView(); imageView.setId(iconId); imageView.setFitWidth(15); imageView.setPreserveRatio(true); setGraphicTextGap(10); setGraphic(imageView); // show cursor hand on any hover this.setPickOnBounds(true); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/MainViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main; import com.google.inject.Inject; import haveno.common.ThreadUtils; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.app.Version; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; import haveno.common.file.CorruptedStorageFileHandler; import haveno.common.util.Tuple2; import haveno.core.account.sign.SignedWitnessService; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.alert.PrivateNotificationManager; import haveno.core.api.XmrConnectionService; import haveno.core.app.HavenoSetup; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.AliPayAccount; import haveno.core.payment.AmazonGiftCardAccount; import haveno.core.payment.CryptoCurrencyAccount; import haveno.core.payment.RevolutAccount; import haveno.core.presentation.BalancePresentation; import haveno.core.presentation.SupportTicketsPresentation; import haveno.core.presentation.TradePresentation; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.HavenoUtils; import haveno.core.trade.TradeManager; import haveno.core.user.DontShowAgainLookup; import haveno.core.user.Preferences; import haveno.core.user.Preferences.UseTorForXmr; import haveno.core.user.User; import haveno.core.util.FormattingUtils; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.Navigation; import haveno.desktop.app.HavenoApp; import haveno.desktop.common.model.ViewModel; import haveno.desktop.components.TxIdTextField; import haveno.desktop.main.account.AccountView; import haveno.desktop.main.account.content.backup.BackupView; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.main.overlays.notifications.NotificationCenter; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.DisplayAlertMessageWindow; import haveno.desktop.main.overlays.windows.TacWindow; import haveno.desktop.main.overlays.windows.TorNetworkSettingsWindow; import haveno.desktop.main.overlays.windows.UpdateAmazonGiftCardAccountWindow; import haveno.desktop.main.overlays.windows.UpdateRevolutAccountWindow; import haveno.desktop.main.overlays.windows.WalletPasswordWindow; import haveno.desktop.main.overlays.windows.downloadupdate.DisplayUpdateDownloadWindow; import haveno.desktop.main.presentation.AccountPresentation; import haveno.desktop.main.presentation.MarketPricePresentation; import haveno.desktop.main.presentation.SettingsPresentation; import haveno.desktop.main.shared.PriceFeedComboBoxItem; import haveno.desktop.util.DisplayUtils; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.BootstrapListener; import haveno.network.p2p.P2PService; import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.monadic.MonadicBinding; import java.util.ArrayList; import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Optional; import java.util.PriorityQueue; import java.util.Queue; import java.util.Random; import java.util.concurrent.TimeUnit; @Slf4j public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener { private final HavenoSetup havenoSetup; private final XmrConnectionService xmrConnectionService; private final User user; private final BalancePresentation balancePresentation; private final TradePresentation tradePresentation; private final SupportTicketsPresentation supportTicketsPresentation; private final MarketPricePresentation marketPricePresentation; private final AccountPresentation accountPresentation; private final SettingsPresentation settingsPresentation; private final P2PService p2PService; private final TradeManager tradeManager; private final OpenOfferManager openOfferManager; @Getter private final Preferences preferences; private final PrivateNotificationManager privateNotificationManager; private final WalletPasswordWindow walletPasswordWindow; private final NotificationCenter notificationCenter; private final TacWindow tacWindow; @Getter private final PriceFeedService priceFeedService; private final Config config; private final AccountAgeWitnessService accountAgeWitnessService; @Getter private final TorNetworkSettingsWindow torNetworkSettingsWindow; private final CorruptedStorageFileHandler corruptedStorageFileHandler; private final Navigation navigation; @Getter private final BooleanProperty showAppScreen = new SimpleBooleanProperty(); private final DoubleProperty combinedSyncProgress = new SimpleDoubleProperty(-1); private final BooleanProperty isSplashScreenRemoved = new SimpleBooleanProperty(); private final StringProperty footerVersionInfo = new SimpleStringProperty(); private Timer checkNumberOfXmrPeersTimer; private Timer checkNumberOfP2pNetworkPeersTimer; @SuppressWarnings("FieldCanBeLocal") private MonadicBinding tradesAndUIReady; private final Queue> popupQueue = new PriorityQueue<>(Comparator.comparing(Overlay::getDisplayOrderPriority)); private Popup moneroConnectionErrorPopup; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public MainViewModel(HavenoSetup havenoSetup, XmrConnectionService xmrConnectionService, XmrWalletService xmrWalletService, User user, BalancePresentation balancePresentation, TradePresentation tradePresentation, SupportTicketsPresentation supportTicketsPresentation, MarketPricePresentation marketPricePresentation, AccountPresentation accountPresentation, SettingsPresentation settingsPresentation, P2PService p2PService, TradeManager tradeManager, OpenOfferManager openOfferManager, Preferences preferences, PrivateNotificationManager privateNotificationManager, WalletPasswordWindow walletPasswordWindow, NotificationCenter notificationCenter, TacWindow tacWindow, PriceFeedService priceFeedService, Config config, AccountAgeWitnessService accountAgeWitnessService, TorNetworkSettingsWindow torNetworkSettingsWindow, CorruptedStorageFileHandler corruptedStorageFileHandler, Navigation navigation) { this.havenoSetup = havenoSetup; this.xmrConnectionService = xmrConnectionService; this.user = user; this.balancePresentation = balancePresentation; this.tradePresentation = tradePresentation; this.supportTicketsPresentation = supportTicketsPresentation; this.marketPricePresentation = marketPricePresentation; this.accountPresentation = accountPresentation; this.settingsPresentation = settingsPresentation; this.p2PService = p2PService; this.tradeManager = tradeManager; this.openOfferManager = openOfferManager; this.preferences = preferences; this.privateNotificationManager = privateNotificationManager; this.walletPasswordWindow = walletPasswordWindow; this.notificationCenter = notificationCenter; this.tacWindow = tacWindow; this.priceFeedService = priceFeedService; this.config = config; this.accountAgeWitnessService = accountAgeWitnessService; this.torNetworkSettingsWindow = torNetworkSettingsWindow; this.corruptedStorageFileHandler = corruptedStorageFileHandler; this.navigation = navigation; TxIdTextField.setPreferences(preferences); TxIdTextField.setXmrWalletService(xmrWalletService); GUIUtil.setPreferences(preferences); setupHandlers(); havenoSetup.addHavenoSetupListener(this); } /////////////////////////////////////////////////////////////////////////////////////////// // HavenoSetupListener /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onSetupComplete() { // We handle the trade period here as we display a global popup if we reached dispute time tradesAndUIReady = EasyBind.combine(isSplashScreenRemoved, tradeManager.tradesInitializedProperty(), (a, b) -> a && b); tradesAndUIReady.subscribe((observable, oldValue, newValue) -> { if (newValue) { ThreadUtils.submitToPool(() -> { tradeManager.applyTradePeriodState(); tradeManager.getOpenTrades().forEach(trade -> { // check initialization error if (trade.getInitError() != null) { new Popup().warning("Error initializing trade" + " " + trade.getShortId() + "\n\n" + trade.getInitError().getMessage()) .show(); return; } // check trade period Date maxTradePeriodDate = trade.getMaxTradePeriodDate(); String key; switch (trade.getPeriodState()) { case FIRST_HALF: break; case SECOND_HALF: key = "displayHalfTradePeriodOver" + trade.getId(); if (DontShowAgainLookup.showAgain(key)) { DontShowAgainLookup.dontShowAgain(key, true); if (trade instanceof ArbitratorTrade) break; // skip popup if arbitrator trade new Popup().warning(Res.get("popup.warning.tradePeriod.halfReached", trade.getShortId(), DisplayUtils.formatDateTime(maxTradePeriodDate))) .show(); } break; case TRADE_PERIOD_OVER: key = "displayTradePeriodOver" + trade.getId(); if (DontShowAgainLookup.showAgain(key)) { DontShowAgainLookup.dontShowAgain(key, true); if (trade instanceof ArbitratorTrade) break; // skip popup if arbitrator trade new Popup().warning(Res.get("popup.warning.tradePeriod.ended", trade.getShortId(), DisplayUtils.formatDateTime(maxTradePeriodDate))) .show(); } break; } }); }); } }); setupP2PNumPeersWatcher(); marketPricePresentation.setup(); accountPresentation.setup(); settingsPresentation.setup(); if (DevEnv.isDevMode()) { preferences.setShowOwnOffersInOfferBook(true); setupDevDummyPaymentAccounts(); } UserThread.execute(() -> getShowAppScreen().set(true)); // show welcome message if (Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_STAGENET) { String key = "welcome.stagenet"; if (DontShowAgainLookup.showAgain(key)) { UserThread.runAfter(() -> { new Popup().attention(Res.get("popup.attention.welcome.stagenet")). dontShowAgainId(key) .closeButtonText(Res.get("shared.iUnderstand")) .show(); }, 1); } } else if (Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_MAINNET) { String key = "welcome.mainnet"; boolean isReleaseLimited = HavenoUtils.isReleasedWithinDays(HavenoUtils.RELEASE_LIMIT_DAYS); if (DontShowAgainLookup.showAgain(key)) { UserThread.runAfter(() -> { new Popup().attention(Res.get(isReleaseLimited ? "popup.attention.welcome.mainnet.test" : "popup.attention.welcome.mainnet")). dontShowAgainId(key) .closeButtonText(Res.get("shared.iUnderstand")) .show(); }, 1); } } } /////////////////////////////////////////////////////////////////////////////////////////// // UI handlers /////////////////////////////////////////////////////////////////////////////////////////// // After showAppScreen is set and splash screen is faded out void onSplashScreenRemoved() { isSplashScreenRemoved.set(true); // Delay that as we want to know what is the current path of the navigation which is set // in MainView showAppScreen handler notificationCenter.onAllServicesAndViewsInitialized(); maybeShowPopupsFromQueue(); } void onOpenDownloadWindow() { havenoSetup.displayAlertIfPresent(user.getDisplayedAlert(), true); } void setPriceFeedComboBoxItem(PriceFeedComboBoxItem item) { marketPricePresentation.setPriceFeedComboBoxItem(item); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void setupHandlers() { havenoSetup.setDisplayTacHandler(acceptedHandler -> UserThread.runAfter(() -> { //noinspection FunctionalExpressionCanBeFolded tacWindow.onAction(acceptedHandler::run).show(); }, 1)); havenoSetup.setDisplayMoneroConnectionFallbackHandler(connectionError -> { if (connectionError == null) { if (moneroConnectionErrorPopup != null) moneroConnectionErrorPopup.hide(); } else { switch (connectionError) { case LOCAL: moneroConnectionErrorPopup = new Popup() .headLine(Res.get("xmrConnectionError.headline")) .warning(Res.get("xmrConnectionError.localNode")) .actionButtonText(Res.get("xmrConnectionError.localNode.start")) .onAction(() -> { log.warn("User has chosen to start local node."); new Thread(() -> { try { HavenoUtils.xmrConnectionService.startLocalNode(); } catch (Exception e) { log.error("Error starting local node: {}", e.getMessage(), e); new Popup() .headLine(Res.get("xmrConnectionError.localNode.start.error")) .warning(e.getMessage()) .closeButtonText(Res.get("shared.close")) .onClose(() -> havenoSetup.getConnectionServiceFallbackType().set(null)) .show(); } finally { havenoSetup.getConnectionServiceFallbackType().set(null); } }).start(); }) .secondaryActionButtonText(Res.get("xmrConnectionError.localNode.fallback")) .onSecondaryAction(() -> { log.warn("User has chosen to fallback to the next best available Monero node."); new Thread(() -> { HavenoUtils.xmrConnectionService.fallbackToBestConnection(); havenoSetup.getConnectionServiceFallbackType().set(null); }).start(); }) .closeButtonText(Res.get("shared.shutDown")) .onClose(HavenoApp.getShutDownHandler()); break; case CUSTOM: moneroConnectionErrorPopup = new Popup() .headLine(Res.get("xmrConnectionError.headline")) .warning(Res.get("xmrConnectionError.customNodes")) .actionButtonText(Res.get("shared.yes")) .onAction(() -> { new Thread(() -> { HavenoUtils.xmrConnectionService.fallbackToBestConnection(); havenoSetup.getConnectionServiceFallbackType().set(null); }).start(); }) .closeButtonText(Res.get("shared.no")) .onClose(() -> { log.warn("User has declined to fallback to the next best available Monero node."); havenoSetup.getConnectionServiceFallbackType().set(null); }); break; case PROVIDED: moneroConnectionErrorPopup = new Popup() .headLine(Res.get("xmrConnectionError.headline")) .warning(Res.get("xmrConnectionError.providedNodes")) .actionButtonText(Res.get("shared.yes")) .onAction(() -> { new Thread(() -> { HavenoUtils.xmrConnectionService.fallbackToBestConnection(); havenoSetup.getConnectionServiceFallbackType().set(null); }).start(); }) .closeButtonText(Res.get("shared.no")) .onClose(() -> { log.warn("User has declined to fallback to the next best available Monero node."); havenoSetup.getConnectionServiceFallbackType().set(null); }); break; } moneroConnectionErrorPopup.show(); } }); havenoSetup.setDisplayTorNetworkSettingsHandler(show -> { if (show) { torNetworkSettingsWindow.show(); } else if (torNetworkSettingsWindow.isDisplayed()) { torNetworkSettingsWindow.hide(); } }); havenoSetup.setChainFileLockedExceptionHandler(msg -> new Popup().warning(msg) .useShutDownButton() .show()); tradeManager.setLockedUpFundsHandler(msg -> new Popup().width(850).warning(msg).show()); havenoSetup.setDisplayUpdateHandler((alert, key) -> new DisplayUpdateDownloadWindow(alert, config) .actionButtonText(Res.get("displayUpdateDownloadWindow.button.downloadLater")) .onAction(() -> { preferences.dontShowAgain(key, false); // update later }) .closeButtonText(Res.get("shared.cancel")) .onClose(() -> { preferences.dontShowAgain(key, true); // ignore update }) .show()); havenoSetup.setDisplayAlertHandler(alert -> new DisplayAlertMessageWindow() .alertMessage(alert) .closeButtonText(Res.get("shared.close")) .onClose(() -> user.setDisplayedAlert(alert)) .show()); havenoSetup.setDisplayPrivateNotificationHandler(privateNotification -> new Popup().headLine(Res.get("popup.privateNotification.headline")) .attention(privateNotification.getMessage()) .onClose(privateNotificationManager::removePrivateNotification) .useIUnderstandButton() .show()); havenoSetup.setDisplaySecurityRecommendationHandler(key -> {}); havenoSetup.setDisplayLocalhostHandler(key -> { if (!DevEnv.isDevMode()) { Popup popup = new Popup().backgroundInfo(Res.get("popup.xmrLocalNode.msg")) .dontShowAgainId(key); popup.setDisplayOrderPriority(5); popupQueue.add(popup); } }); havenoSetup.setDisplaySignedByArbitratorHandler(key -> accountPresentation.showOneTimeAccountSigningPopup( key, "popup.accountSigning.signedByArbitrator")); havenoSetup.setDisplaySignedByPeerHandler(key -> accountPresentation.showOneTimeAccountSigningPopup( key, "popup.accountSigning.signedByPeer", String.valueOf(SignedWitnessService.SIGNER_AGE_DAYS))); havenoSetup.setDisplayPeerLimitLiftedHandler(key -> accountPresentation.showOneTimeAccountSigningPopup( key, "popup.accountSigning.peerLimitLifted")); havenoSetup.setDisplayPeerSignerHandler(key -> accountPresentation.showOneTimeAccountSigningPopup( key, "popup.accountSigning.peerSigner")); havenoSetup.setWrongOSArchitectureHandler(msg -> new Popup().warning(msg).show()); havenoSetup.setRejectedTxErrorMessageHandler(msg -> new Popup().width(850).warning(msg).show()); havenoSetup.setShowPopupIfInvalidXmrConfigHandler(this::showPopupIfInvalidXmrConfig); havenoSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> { // We copy the array as we will mutate it later showRevolutAccountUpdateWindow(new ArrayList<>(revolutAccountList)); }); havenoSetup.setAmazonGiftCardAccountsUpdateHandler(amazonGiftCardAccountList -> { // We copy the array as we will mutate it later showAmazonGiftCardAccountUpdateWindow(new ArrayList<>(amazonGiftCardAccountList)); }); havenoSetup.setOsxKeyLoggerWarningHandler(() -> { }); havenoSetup.setQubesOSInfoHandler(() -> { String key = "qubesOSSetupInfo"; if (preferences.showAgain(key)) { new Popup().information(Res.get("popup.info.qubesOSSetupInfo")) .closeButtonText(Res.get("shared.iUnderstand")) .dontShowAgainId(key) .show(); } }); havenoSetup.setDownGradePreventionHandler(lastVersion -> { new Popup().warning(Res.get("popup.warn.downGradePrevention", lastVersion, Version.VERSION)) .useShutDownButton() .hideCloseButton() .show(); }); havenoSetup.setTorAddressUpgradeHandler(() -> new Popup().information(Res.get("popup.info.torMigration.msg")) .actionButtonTextWithGoTo("navigation.account.backup") .onAction(() -> { navigation.setReturnPath(navigation.getCurrentPath()); navigation.navigateTo(MainView.class, AccountView.class, BackupView.class); }).show()); corruptedStorageFileHandler.getFiles().ifPresent(files -> new Popup() .warning(Res.get("popup.warning.incompatibleDB", files.toString(), config.appDataDir)) .useShutDownButton() .show()); havenoSetup.getXmrDaemonSyncProgress().addListener((observable, oldValue, newValue) -> updateXmrDaemonSyncProgress()); havenoSetup.getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> updateXmrWalletSyncProgress()); havenoSetup.setFilterWarningHandler(warning -> new Popup().warning(warning).show()); this.footerVersionInfo.setValue(FormattingUtils.formatVersion()); this.getNewVersionAvailableProperty().addListener((observable, oldValue, newValue) -> { if (newValue) { this.footerVersionInfo.setValue(FormattingUtils.formatVersion() + " " + Res.get("mainView.version.update")); } else { this.footerVersionInfo.setValue(FormattingUtils.formatVersion()); } }); if (p2PService.isBootstrapped()) { setupInvalidOpenOffersHandler(); } else { p2PService.addP2PServiceListener(new BootstrapListener() { @Override public void onDataReceived() { setupInvalidOpenOffersHandler(); } }); } } private void showRevolutAccountUpdateWindow(List revolutAccountList) { if (!revolutAccountList.isEmpty()) { RevolutAccount revolutAccount = revolutAccountList.get(0); revolutAccountList.remove(0); new UpdateRevolutAccountWindow(revolutAccount, user).onClose(() -> { // We delay a bit in case we have multiple account for better UX UserThread.runAfter(() -> showRevolutAccountUpdateWindow(revolutAccountList), 300, TimeUnit.MILLISECONDS); }).show(); } } private void showAmazonGiftCardAccountUpdateWindow(List amazonGiftCardAccountList) { if (!amazonGiftCardAccountList.isEmpty()) { AmazonGiftCardAccount amazonGiftCardAccount = amazonGiftCardAccountList.get(0); amazonGiftCardAccountList.remove(0); new UpdateAmazonGiftCardAccountWindow(amazonGiftCardAccount, user).onClose(() -> { // We delay a bit in case we have multiple account for better UX UserThread.runAfter(() -> showAmazonGiftCardAccountUpdateWindow(amazonGiftCardAccountList), 300, TimeUnit.MILLISECONDS); }).show(); } } private void setupP2PNumPeersWatcher() { p2PService.getNumConnectedPeers().addListener((observable, oldValue, newValue) -> { int numPeers = (int) newValue; if ((int) oldValue > 0 && numPeers == 0) { // give a bit of tolerance if (checkNumberOfP2pNetworkPeersTimer != null) checkNumberOfP2pNetworkPeersTimer.stop(); checkNumberOfP2pNetworkPeersTimer = UserThread.runAfter(() -> { // check again numPeers if (p2PService.getNumConnectedPeers().get() == 0) { getP2pNetworkWarnMsg().set(Res.get("mainView.networkWarning.allConnectionsLost", Res.get("shared.P2P"))); getP2pNetworkLabelId().set("splash-error-state-msg"); } else { getP2pNetworkWarnMsg().set(null); getP2pNetworkLabelId().set("footer-pane"); } }, 5); } else if ((int) oldValue == 0 && numPeers > 0) { if (checkNumberOfP2pNetworkPeersTimer != null) checkNumberOfP2pNetworkPeersTimer.stop(); getP2pNetworkWarnMsg().set(null); getP2pNetworkLabelId().set("footer-pane"); } }); } private void showPopupIfInvalidXmrConfig() { preferences.setMoneroNodesOptionOrdinal(0); new Popup().warning(Res.get("settings.net.warn.invalidXmrConfig")) .hideCloseButton() .useShutDownButton() .show(); } private void setupDevDummyPaymentAccounts() { if (user.getPaymentAccounts() != null && user.getPaymentAccounts().isEmpty()) { AliPayAccount aliPayAccount = new AliPayAccount(); aliPayAccount.init(); aliPayAccount.setAccountNr("dummy_" + new Random().nextInt(100)); aliPayAccount.setAccountName("AliPayAccount dummy");// Don't translate only for dev user.addPaymentAccount(aliPayAccount); if (p2PService.isBootstrapped()) { accountAgeWitnessService.publishMyAccountAgeWitness(aliPayAccount.getPaymentAccountPayload()); } else { p2PService.addP2PServiceListener(new BootstrapListener() { @Override public void onDataReceived() { accountAgeWitnessService.publishMyAccountAgeWitness(aliPayAccount.getPaymentAccountPayload()); } }); } CryptoCurrencyAccount cryptoCurrencyAccount = new CryptoCurrencyAccount(); cryptoCurrencyAccount.init(); cryptoCurrencyAccount.setAccountName("ETH dummy");// Don't translate only for dev cryptoCurrencyAccount.setAddress("0x" + new Random().nextInt(1000000)); Optional eth = CurrencyUtil.getCryptoCurrency("ETH"); eth.ifPresent(cryptoCurrencyAccount::setSingleTradeCurrency); user.addPaymentAccount(cryptoCurrencyAccount); } } private void updateXmrDaemonSyncProgress() { final DoubleProperty xmrDaemonSyncProgress = havenoSetup.getXmrDaemonSyncProgress(); UserThread.execute(() -> { combinedSyncProgress.set(xmrDaemonSyncProgress.doubleValue()); }); } private void updateXmrWalletSyncProgress() { final DoubleProperty xmrWalletSyncProgress = havenoSetup.getXmrWalletSyncProgress(); UserThread.execute(() -> { combinedSyncProgress.set(xmrWalletSyncProgress.doubleValue()); }); } private void setupInvalidOpenOffersHandler() { openOfferManager.getInvalidOffers().addListener((ListChangeListener>) c -> { c.next(); if (c.wasAdded()) { handleInvalidOpenOffers(c.getAddedSubList()); } }); handleInvalidOpenOffers(openOfferManager.getInvalidOffers()); } private void handleInvalidOpenOffers(List> list) { list.forEach(tuple2 -> { String errorMsg = tuple2.second; OpenOffer openOffer = tuple2.first; new Popup().warning(errorMsg) .width(1000) .actionButtonText(Res.get("shared.removeOffer")) .onAction(() -> { openOfferManager.cancelOpenOffer(openOffer, () -> { log.info("Invalid open offer with ID {} was successfully removed.", openOffer.getId()); }, log::error); }) .hideCloseButton() .show(); }); } /////////////////////////////////////////////////////////////////////////////////////////// // MainView delegate getters /////////////////////////////////////////////////////////////////////////////////////////// BooleanProperty getNewVersionAvailableProperty() { return havenoSetup.getNewVersionAvailableProperty(); } StringProperty getNumOpenSupportTickets() { return supportTicketsPresentation.getNumOpenSupportTickets(); } BooleanProperty getShowOpenSupportTicketsNotification() { return supportTicketsPresentation.getShowOpenSupportTicketsNotification(); } BooleanProperty getShowPendingTradesNotification() { return tradePresentation.getShowPendingTradesNotification(); } StringProperty getNumPendingTrades() { return tradePresentation.getNumPendingTrades(); } StringProperty getAvailableBalance() { return balancePresentation.getAvailableBalance(); } StringProperty getReservedBalance() { return balancePresentation.getReservedBalance(); } StringProperty getPendingBalance() { return balancePresentation.getPendingBalance(); } // Wallet StringProperty getXmrInfo() { final StringProperty combinedInfo = new SimpleStringProperty(); combinedInfo.bind(havenoSetup.getXmrInfo()); return combinedInfo; } StringProperty getCombinedFooterInfo() { final StringProperty combinedInfo = new SimpleStringProperty(); combinedInfo.bind(Bindings.concat(this.footerVersionInfo, " ")); return combinedInfo; } DoubleProperty getCombinedSyncProgress() { return combinedSyncProgress; } StringProperty getConnectionServiceErrorMsg() { return havenoSetup.getConnectionServiceErrorMsg(); } StringProperty getTopErrorMsg() { return havenoSetup.getTopErrorMsg(); } StringProperty getXmrSplashSyncIconId() { return havenoSetup.getXmrSplashSyncIconId(); } ObjectProperty getUseTorForXmr() { return havenoSetup.getUseTorForXmr(); } // P2P StringProperty getP2PNetworkInfo() { return havenoSetup.getP2PNetworkInfo(); } BooleanProperty getSplashP2PNetworkAnimationVisible() { return havenoSetup.getSplashP2PNetworkAnimationVisible(); } StringProperty getP2pNetworkWarnMsg() { return havenoSetup.getP2pNetworkWarnMsg(); } StringProperty getP2PNetworkIconId() { return havenoSetup.getP2PNetworkIconId(); } StringProperty getP2PNetworkStatusIconId() { return havenoSetup.getP2PNetworkStatusIconId(); } BooleanProperty getUpdatedDataReceived() { return havenoSetup.getUpdatedDataReceived(); } StringProperty getP2pNetworkLabelId() { return havenoSetup.getP2pNetworkLabelId(); } // marketPricePresentation ObjectProperty getSelectedPriceFeedComboBoxItemProperty() { return marketPricePresentation.getSelectedPriceFeedComboBoxItemProperty(); } BooleanProperty getIsFiatCurrencyPriceFeedSelected() { return marketPricePresentation.getIsFiatCurrencyPriceFeedSelected(); } BooleanProperty getIsExternallyProvidedPrice() { return marketPricePresentation.getIsExternallyProvidedPrice(); } BooleanProperty getIsPriceAvailable() { return marketPricePresentation.getIsPriceAvailable(); } IntegerProperty getMarketPriceUpdated() { return marketPricePresentation.getMarketPriceUpdated(); } StringProperty getMarketPrice() { return marketPricePresentation.getMarketPrice(); } StringProperty getMarketPrice(String currencyCode) { return marketPricePresentation.getMarketPrice(currencyCode); } public ObservableList getPriceFeedComboBoxItems() { return marketPricePresentation.getPriceFeedComboBoxItems(); } // We keep accountPresentation support even it is not used atm. But if we add a new feature and // add a badge again it will be needed. @SuppressWarnings("unused") public BooleanProperty getShowAccountUpdatesNotification() { return accountPresentation.getShowAccountUpdatesNotification(); } public BooleanProperty getShowSettingsUpdatesNotification() { return settingsPresentation.getShowSettingsUpdatesNotification(); } private void maybeShowPopupsFromQueue() { if (!popupQueue.isEmpty()) { Overlay overlay = popupQueue.poll(); overlay.getIsHiddenProperty().addListener((observable, oldValue, newValue) -> { if (newValue) { UserThread.runAfter(this::maybeShowPopupsFromQueue, 2); } }); overlay.show(); } } public String getP2pConnectionSummary() { return Res.get("mainView.status.connections", p2PService.getNetworkNode().getInboundConnectionCount(), p2PService.getNetworkNode().getOutboundConnectionCount()); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/SharedPresentation.java ================================================ /* * 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 . */ package haveno.desktop.main; import haveno.common.UserThread; import haveno.common.file.FileUtil; import haveno.core.locale.Res; import haveno.core.offer.OpenOfferManager; import haveno.core.xmr.wallet.WalletsManager; import haveno.desktop.app.HavenoApp; import haveno.desktop.main.overlays.popups.Popup; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.wallet.DeterministicSeed; import java.io.File; import java.util.concurrent.TimeUnit; /** * This serves as shared space for static methods used from different views where no common parent view would fit as * owner of that code. We keep it strictly static. It should replace GUIUtil for those methods which are not utility * methods. */ @Slf4j public class SharedPresentation { public static void restoreSeedWords(WalletsManager walletsManager, OpenOfferManager openOfferManager, DeterministicSeed seed, File storageDir) { if (!openOfferManager.getObservableList().isEmpty()) { UserThread.runAfter(() -> new Popup().warning(Res.get("seed.restore.openOffers.warn")) .actionButtonText(Res.get("shared.yes")) .onAction(() -> { openOfferManager.removeAllOpenOffers(() -> { doRestoreSeedWords(walletsManager, seed, storageDir); }); }) .show(), 100, TimeUnit.MILLISECONDS); } else { doRestoreSeedWords(walletsManager, seed, storageDir); } } private static void doRestoreSeedWords(WalletsManager walletsManager, DeterministicSeed seed, File storageDir) { try { File backup = new File(storageDir, "AddressEntryList_backup_pre_wallet_restore_" + System.currentTimeMillis()); FileUtil.copyFile(new File(storageDir, "AddressEntryList"), backup); } catch (Throwable t) { new Popup().error(Res.get("error.deleteAddressEntryListFailed", t)).show(); } walletsManager.restoreSeedWords( seed, () -> UserThread.execute(() -> { log.info("Wallets restored with seed words"); new Popup().feedback(Res.get("seed.restore.success")).hideCloseButton().show(); HavenoApp.getShutDownHandler().run(); }), throwable -> UserThread.execute(() -> { log.error(throwable.toString()); new Popup().error(Res.get("seed.restore.error", Res.get("shared.errorMessageInline", throwable))) .show(); })); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/AccountView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/AccountView.java ================================================ /* * 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 . */ package haveno.desktop.main.account; import com.google.inject.Inject; import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.core.user.DontShowAgainLookup; import haveno.desktop.Navigation; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.common.view.CachingViewLoader; import haveno.desktop.common.view.FxmlView; import haveno.desktop.common.view.View; import haveno.desktop.common.view.ViewLoader; import haveno.desktop.main.MainView; import haveno.desktop.main.account.content.backup.BackupView; import haveno.desktop.main.account.content.cryptoaccounts.CryptoAccountsView; import haveno.desktop.main.account.content.password.PasswordView; import haveno.desktop.main.account.content.seedwords.SeedWordsView; import haveno.desktop.main.account.content.traditionalaccounts.TraditionalAccountsView; import haveno.desktop.main.account.content.walletinfo.WalletInfoView; import haveno.desktop.main.account.register.arbitrator.ArbitratorRegistrationView; import haveno.desktop.main.account.register.mediator.MediatorRegistrationView; import haveno.desktop.main.account.register.refundagent.RefundAgentRegistrationView; import haveno.desktop.main.account.register.signing.SigningView; import haveno.desktop.main.presentation.AccountPresentation; import java.util.List; import javafx.beans.value.ChangeListener; import javafx.collections.ListChangeListener; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.scene.Scene; import javafx.scene.control.ScrollPane; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; @FxmlView public class AccountView extends ActivatableView { @FXML Tab traditionalAccountsTab, cryptoAccountsTab, passwordTab, seedWordsTab, walletInfoTab, backupTab; private Navigation.Listener navigationListener; private ChangeListener tabChangeListener; private final ViewLoader viewLoader; private final Navigation navigation; private Tab selectedTab; private Tab arbitratorRegistrationTab; private Tab mediatorRegistrationTab; private Tab refundAgentRegistrationTab; private Tab signingTab; private ArbitratorRegistrationView arbitratorRegistrationView; private MediatorRegistrationView mediatorRegistrationView; private RefundAgentRegistrationView refundAgentRegistrationView; private Scene scene; private EventHandler keyEventEventHandler; private ListChangeListener tabListChangeListener; @Inject private AccountView(CachingViewLoader viewLoader, Navigation navigation) { this.viewLoader = viewLoader; this.navigation = navigation; } @Override public void initialize() { root.setTabClosingPolicy(TabPane.TabClosingPolicy.ALL_TABS); traditionalAccountsTab.setText(Res.get("account.menu.paymentAccount")); cryptoAccountsTab.setText(Res.get("account.menu.altCoinsAccountView")); passwordTab.setText(Res.get("account.menu.password")); seedWordsTab.setText(Res.get("account.menu.seedWords")); //walletInfoTab.setText(Res.get("account.menu.walletInfo")); backupTab.setText(Res.get("account.menu.backup")); navigationListener = (viewPath, data) -> { if (viewPath.size() == 3 && viewPath.indexOf(AccountView.class) == 1) { if (arbitratorRegistrationTab == null && viewPath.get(2).equals(ArbitratorRegistrationView.class)) { navigation.navigateTo(MainView.class, AccountView.class, TraditionalAccountsView.class); } else if (mediatorRegistrationTab == null && viewPath.get(2).equals(MediatorRegistrationView.class)) { navigation.navigateTo(MainView.class, AccountView.class, TraditionalAccountsView.class); } else if (refundAgentRegistrationTab == null && viewPath.get(2).equals(RefundAgentRegistrationView.class)) { navigation.navigateTo(MainView.class, AccountView.class, TraditionalAccountsView.class); } else if (signingTab == null && viewPath.get(2).equals(SigningView.class)) { navigation.navigateTo(MainView.class, AccountView.class, TraditionalAccountsView.class); } else { loadView(viewPath.tip()); } } else { resetSelectedTab(); } }; keyEventEventHandler = event -> { if (Utilities.isAltOrCtrlPressed(KeyCode.R, event) && arbitratorRegistrationTab == null) { closeOtherExtraTabs(arbitratorRegistrationTab); arbitratorRegistrationTab = new Tab(Res.get("account.tab.arbitratorRegistration").toUpperCase()); arbitratorRegistrationTab.setClosable(true); root.getTabs().add(arbitratorRegistrationTab); navigation.navigateTo(MainView.class, AccountView.class, ArbitratorRegistrationView.class); } else if (Utilities.isAltOrCtrlPressed(KeyCode.D, event) && mediatorRegistrationTab == null) { closeOtherExtraTabs(mediatorRegistrationTab); mediatorRegistrationTab = new Tab(Res.get("account.tab.mediatorRegistration").toUpperCase()); mediatorRegistrationTab.setClosable(true); root.getTabs().add(mediatorRegistrationTab); navigation.navigateTo(MainView.class, AccountView.class, MediatorRegistrationView.class); } else if (Utilities.isAltOrCtrlPressed(KeyCode.N, event) && refundAgentRegistrationTab == null) { closeOtherExtraTabs(refundAgentRegistrationTab); refundAgentRegistrationTab = new Tab(Res.get("account.tab.refundAgentRegistration").toUpperCase()); refundAgentRegistrationTab.setClosable(true); root.getTabs().add(refundAgentRegistrationTab); navigation.navigateTo(MainView.class, AccountView.class, RefundAgentRegistrationView.class); } else if (Utilities.isAltOrCtrlPressed(KeyCode.I, event) && signingTab == null) { closeOtherExtraTabs(signingTab); signingTab = new Tab(Res.get("account.tab.signing").toUpperCase()); signingTab.setClosable(true); root.getTabs().add(signingTab); navigation.navigateTo(MainView.class, AccountView.class, SigningView.class); } }; tabChangeListener = (ov, oldValue, newValue) -> { if (arbitratorRegistrationTab != null && selectedTab != arbitratorRegistrationTab) { navigation.navigateTo(MainView.class, AccountView.class, ArbitratorRegistrationView.class); } else if (mediatorRegistrationTab != null && selectedTab != mediatorRegistrationTab) { navigation.navigateTo(MainView.class, AccountView.class, MediatorRegistrationView.class); } else if (refundAgentRegistrationTab != null && selectedTab != refundAgentRegistrationTab) { navigation.navigateTo(MainView.class, AccountView.class, RefundAgentRegistrationView.class); } else if (signingTab != null && !selectedTab.equals(signingTab)) { navigation.navigateTo(MainView.class, AccountView.class, SigningView.class); } else if (newValue == traditionalAccountsTab && selectedTab != traditionalAccountsTab) { navigation.navigateTo(MainView.class, AccountView.class, TraditionalAccountsView.class); } else if (newValue == cryptoAccountsTab && selectedTab != cryptoAccountsTab) { navigation.navigateTo(MainView.class, AccountView.class, CryptoAccountsView.class); } else if (newValue == passwordTab && selectedTab != passwordTab) { navigation.navigateTo(MainView.class, AccountView.class, PasswordView.class); } else if (newValue == seedWordsTab && selectedTab != seedWordsTab) { navigation.navigateTo(MainView.class, AccountView.class, SeedWordsView.class); } else if (newValue == walletInfoTab && selectedTab != walletInfoTab) { navigation.navigateTo(MainView.class, AccountView.class, WalletInfoView.class); } else if (newValue == backupTab && selectedTab != backupTab) { navigation.navigateTo(MainView.class, AccountView.class, BackupView.class); } }; tabListChangeListener = change -> { change.next(); List removedTabs = change.getRemoved(); if (removedTabs.size() == 1 && removedTabs.get(0).equals(arbitratorRegistrationTab)) onArbitratorRegistrationTabRemoved(); if (removedTabs.size() == 1 && removedTabs.get(0).equals(mediatorRegistrationTab)) onMediatorRegistrationTabRemoved(); if (removedTabs.size() == 1 && removedTabs.get(0).equals(refundAgentRegistrationTab)) onRefundAgentRegistrationTabRemoved(); if (removedTabs.size() == 1 && removedTabs.get(0).equals(signingTab)) onSigningTabRemoved(); }; } private void closeOtherExtraTabs(Tab newTab) { if (arbitratorRegistrationTab != null && !arbitratorRegistrationTab.equals(newTab)) { root.getTabs().remove(arbitratorRegistrationTab); } if (mediatorRegistrationTab != null && !mediatorRegistrationTab.equals(newTab)) { root.getTabs().remove(mediatorRegistrationTab); } if (refundAgentRegistrationTab != null && !refundAgentRegistrationTab.equals(newTab)) { root.getTabs().remove(refundAgentRegistrationTab); } if (signingTab != null && !signingTab.equals(newTab)) { root.getTabs().remove(signingTab); } } private void onArbitratorRegistrationTabRemoved() { arbitratorRegistrationTab = null; navigation.navigateTo(MainView.class, AccountView.class, TraditionalAccountsView.class); } private void onMediatorRegistrationTabRemoved() { mediatorRegistrationTab = null; navigation.navigateTo(MainView.class, AccountView.class, TraditionalAccountsView.class); } private void onRefundAgentRegistrationTabRemoved() { refundAgentRegistrationTab = null; navigation.navigateTo(MainView.class, AccountView.class, TraditionalAccountsView.class); } private void onSigningTabRemoved() { signingTab = null; navigation.navigateTo(MainView.class, AccountView.class, TraditionalAccountsView.class); } @Override protected void activate() { // Hide account new badge if user saw this section DontShowAgainLookup.dontShowAgain(AccountPresentation.ACCOUNT_NEWS, true); navigation.addListener(navigationListener); root.getSelectionModel().selectedItemProperty().addListener(tabChangeListener); root.getTabs().addListener(tabListChangeListener); scene = root.getScene(); if (scene != null) scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); if (navigation.getCurrentPath().size() == 2 && navigation.getCurrentPath().get(1) == AccountView.class) { if (arbitratorRegistrationTab != null) navigation.navigateTo(MainView.class, AccountView.class, ArbitratorRegistrationView.class); else if (mediatorRegistrationTab != null) navigation.navigateTo(MainView.class, AccountView.class, MediatorRegistrationView.class); else if (refundAgentRegistrationTab != null) navigation.navigateTo(MainView.class, AccountView.class, RefundAgentRegistrationView.class); else if (signingTab != null) navigation.navigateTo(MainView.class, AccountView.class, SigningView.class); else if (root.getSelectionModel().getSelectedItem() == traditionalAccountsTab) navigation.navigateTo(MainView.class, AccountView.class, TraditionalAccountsView.class); else if (root.getSelectionModel().getSelectedItem() == cryptoAccountsTab) navigation.navigateTo(MainView.class, AccountView.class, CryptoAccountsView.class); else if (root.getSelectionModel().getSelectedItem() == passwordTab) navigation.navigateTo(MainView.class, AccountView.class, PasswordView.class); else if (root.getSelectionModel().getSelectedItem() == seedWordsTab) navigation.navigateTo(MainView.class, AccountView.class, SeedWordsView.class); else if (root.getSelectionModel().getSelectedItem() == walletInfoTab) navigation.navigateTo(MainView.class, AccountView.class, WalletInfoView.class); else if (root.getSelectionModel().getSelectedItem() == backupTab) navigation.navigateTo(MainView.class, AccountView.class, BackupView.class); else navigation.navigateTo(MainView.class, AccountView.class, TraditionalAccountsView.class); } } @Override protected void deactivate() { navigation.removeListener(navigationListener); root.getSelectionModel().selectedItemProperty().removeListener(tabChangeListener); root.getTabs().removeListener(tabListChangeListener); if (scene != null) scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); } private void loadView(Class viewClass) { View view = viewLoader.load(viewClass); resetSelectedTab(); if (view instanceof ArbitratorRegistrationView) { if (arbitratorRegistrationTab != null) { selectedTab = arbitratorRegistrationTab; arbitratorRegistrationView = (ArbitratorRegistrationView) view; arbitratorRegistrationView.onTabSelection(true); } } else if (view instanceof MediatorRegistrationView) { if (mediatorRegistrationTab != null) { selectedTab = mediatorRegistrationTab; mediatorRegistrationView = (MediatorRegistrationView) view; mediatorRegistrationView.onTabSelection(true); } } else if (view instanceof RefundAgentRegistrationView) { if (refundAgentRegistrationTab != null) { selectedTab = refundAgentRegistrationTab; refundAgentRegistrationView = (RefundAgentRegistrationView) view; refundAgentRegistrationView.onTabSelection(true); } } else if (view instanceof SigningView) { if (signingTab != null) { selectedTab = signingTab; } } else if (view instanceof TraditionalAccountsView) { selectedTab = traditionalAccountsTab; } else if (view instanceof CryptoAccountsView) { selectedTab = cryptoAccountsTab; } else if (view instanceof PasswordView) { selectedTab = passwordTab; } else if (view instanceof SeedWordsView) { selectedTab = seedWordsTab; } else if (view instanceof WalletInfoView) { selectedTab = walletInfoTab; } else if (view instanceof BackupView) { selectedTab = backupTab; } else { throw new IllegalArgumentException("View not supported: " + view); } if (selectedTab.getContent() != null && selectedTab.getContent() instanceof ScrollPane) { ((ScrollPane) selectedTab.getContent()).setContent(view.getRoot()); } else { selectedTab.setContent(view.getRoot()); } root.getSelectionModel().select(selectedTab); } private void resetSelectedTab() { if (selectedTab != null && selectedTab.getContent() != null) { if (selectedTab.getContent() instanceof ScrollPane) { ((ScrollPane) selectedTab.getContent()).setContent(null); } else { selectedTab.setContent(null); } } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/content/PaymentAccountsView.java ================================================ package haveno.desktop.main.account.content; import haveno.common.UserThread; import haveno.common.util.Utilities; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentMethod; import haveno.desktop.common.model.ActivatableWithDataModel; import haveno.desktop.common.view.ActivatableViewAndModel; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.InfoAutoTooltipLabel; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.ImageUtil; import haveno.desktop.util.Layout; import javafx.beans.value.ChangeListener; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.event.EventHandler; import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.ContentDisplay; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.AnchorPane; import javafx.util.Callback; import org.apache.commons.lang3.StringUtils; import java.util.concurrent.TimeUnit; public abstract class PaymentAccountsView extends ActivatableViewAndModel { protected ListView paymentAccountsListView; private ChangeListener paymentAccountChangeListener; protected Button addAccountButton, exportButton, importButton; protected AccountAgeWitnessService accountAgeWitnessService; private EventHandler keyEventEventHandler; public PaymentAccountsView(M model, AccountAgeWitnessService accountAgeWitnessService) { super(model); this.accountAgeWitnessService = accountAgeWitnessService; } @Override public void initialize() { keyEventEventHandler = event -> { if (Utilities.isCtrlShiftPressed(KeyCode.L, event)) { accountAgeWitnessService.getAccountAgeWitnessUtils().logSignedWitnesses(); } else if (Utilities.isCtrlShiftPressed(KeyCode.S, event)) { accountAgeWitnessService.getAccountAgeWitnessUtils().logSigners(); } else if (Utilities.isCtrlShiftPressed(KeyCode.U, event)) { accountAgeWitnessService.getAccountAgeWitnessUtils().logUnsignedSignerPubKeys(); } else if (Utilities.isCtrlShiftPressed(KeyCode.C, event)) { copyAccount(); } }; buildForm(); paymentAccountChangeListener = (observable, oldValue, newValue) -> { if (newValue != null) onSelectAccount(oldValue, newValue); }; Label placeholder = new AutoTooltipLabel(Res.get("shared.noAccountsSetupYet")); placeholder.setWrapText(true); paymentAccountsListView.setPlaceholder(placeholder); getPaymentAccounts().addListener((ListChangeListener) change -> { setPaymentAccountsListHeight(); }); } @Override protected void activate() { paymentAccountsListView.setItems(getPaymentAccounts()); paymentAccountsListView.getSelectionModel().selectedItemProperty().addListener(paymentAccountChangeListener); addAccountButton.setOnAction(event -> addNewAccount()); exportButton.setOnAction(event -> exportAccounts()); importButton.setOnAction(event -> importAccounts()); if (root.getScene() != null) root.getScene().addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); } @Override protected void deactivate() { paymentAccountsListView.getSelectionModel().selectedItemProperty().removeListener(paymentAccountChangeListener); addAccountButton.setOnAction(null); exportButton.setOnAction(null); importButton.setOnAction(null); if (root.getScene() != null) root.getScene().removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); } protected void onDeleteAccount(PaymentAccount paymentAccount) { new Popup().warning(Res.get("shared.askConfirmDeleteAccount")) .actionButtonText(Res.get("shared.yes")) .onAction(() -> { boolean isPaymentAccountUsed = deleteAccountFromModel(paymentAccount); if (!isPaymentAccountUsed) removeSelectAccountForm(); else UserThread.runAfter(() -> new Popup().warning( Res.get("shared.cannotDeleteAccount")) .show(), 100, TimeUnit.MILLISECONDS); }) .closeButtonText(Res.get("shared.cancel")) .show(); } protected void setPaymentAccountsCellFactory() { paymentAccountsListView.setCellFactory(new Callback<>() { @Override public ListCell call(ListView list) { return new ListCell<>() { final InfoAutoTooltipLabel label = new InfoAutoTooltipLabel("", ContentDisplay.RIGHT); final ImageView icon = ImageUtil.getImageViewById(ImageUtil.REMOVE_ICON); final Button removeButton = new AutoTooltipButton("", icon); final AnchorPane pane = new AnchorPane(label, removeButton); { label.setLayoutY(5); removeButton.setId("icon-button"); AnchorPane.setRightAnchor(removeButton, 0d); } @Override public void updateItem(final PaymentAccount item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { label.setText(item.getAccountName()); boolean needsSigning = PaymentMethod.hasChargebackRisk(item.getPaymentMethod(), item.getTradeCurrencies()); if (needsSigning) { AccountAgeWitnessService.SignState signState = accountAgeWitnessService.getSignState(accountAgeWitnessService.getMyWitness( item.paymentAccountPayload)); String info = StringUtils.capitalize(signState.getDisplayString()); label.setIcon(GUIUtil.getIconForSignState(signState), info); } else { label.hideIcon(); } removeButton.setOnAction(e -> onDeleteAccount(item)); setGraphic(pane); } else { setGraphic(null); } } }; } }); } protected void setPaymentAccountsListHeight() { int prefNumRows = Math.min(5, Math.max(2, getPaymentAccounts().size())); double prefHeight = prefNumRows * (Layout.LIST_ROW_HEIGHT + 6); paymentAccountsListView.setMinHeight(prefHeight); paymentAccountsListView.setMaxHeight(prefHeight); } protected abstract void removeSelectAccountForm(); protected abstract boolean deleteAccountFromModel(PaymentAccount paymentAccount); protected abstract void importAccounts(); protected abstract void exportAccounts(); protected abstract void addNewAccount(); protected abstract ObservableList getPaymentAccounts(); protected abstract void buildForm(); protected abstract void onSelectAccount(PaymentAccount previous, PaymentAccount current); protected void copyAccount() { } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/content/backup/BackupView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/content/backup/BackupView.java ================================================ /* * 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 . */ package haveno.desktop.main.account.content.backup; import com.google.inject.Inject; import haveno.common.UserThread; import haveno.common.config.Config; import haveno.common.file.FileUtil; import haveno.common.persistence.PersistenceManager; import haveno.common.util.Tuple2; import haveno.common.util.Utilities; import haveno.core.api.XmrLocalNode; import haveno.core.locale.Res; import haveno.core.user.Preferences; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.app.HavenoApp; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.common.view.FxmlView; import haveno.desktop.main.overlays.popups.Popup; import static haveno.desktop.util.FormBuilder.add2Buttons; import static haveno.desktop.util.FormBuilder.add2ButtonsAfterGroup; import static haveno.desktop.util.FormBuilder.addInputTextField; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import haveno.desktop.util.Layout; import java.io.File; import java.io.IOException; import java.nio.file.Paths; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.TimeUnit; import javafx.beans.value.ChangeListener; import javafx.scene.control.Button; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import javafx.stage.DirectoryChooser; import javax.annotation.Nullable; @FxmlView public class BackupView extends ActivatableView { private final File dataDir, logFile; private int gridRow = 0; private final Preferences preferences; private Button selectBackupDir, backupNow; private TextField backUpLocationTextField; private Button openDataDirButton, openLogsButton; private ChangeListener backUpLocationTextFieldFocusListener; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject private BackupView(Preferences preferences, Config config) { super(); this.preferences = preferences; dataDir = new File(config.appDataDir.getPath()); logFile = new File(Paths.get(dataDir.getPath(), "haveno.log").toString()); } @Override public void initialize() { addTitledGroupBg(root, gridRow, 2, Res.get("account.backup.title")); backUpLocationTextField = addInputTextField(root, gridRow, Res.get("account.backup.location"), Layout.FIRST_ROW_DISTANCE); String backupDirectory = preferences.getBackupDirectory(); if (backupDirectory != null) backUpLocationTextField.setText(backupDirectory); backUpLocationTextFieldFocusListener = (observable, oldValue, newValue) -> { if (oldValue && !newValue) applyBackupDirectory(backUpLocationTextField.getText()); }; Tuple2 tuple2 = add2ButtonsAfterGroup(root, ++gridRow, Res.get("account.backup.selectLocation"), Res.get("account.backup.backupNow")); selectBackupDir = tuple2.first; selectBackupDir.setId("buy-button-big"); backupNow = tuple2.second; updateButtons(); addTitledGroupBg(root, ++gridRow, 2, Res.get("account.backup.appDir"), Layout.GROUP_DISTANCE); final Tuple2 applicationDataDirTuple2 = add2Buttons(root, gridRow, Res.get("account.backup.openDirectory"), Res.get("account.backup.openLogFile"), Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE, false); openDataDirButton = applicationDataDirTuple2.first; openLogsButton = applicationDataDirTuple2.second; } @Override protected void activate() { backUpLocationTextField.focusedProperty().addListener(backUpLocationTextFieldFocusListener); selectBackupDir.setOnAction(e -> { String path = preferences.getDirectoryChooserPath(); if (!Utilities.isDirectory(path)) { path = Utilities.getSystemHomeDirectory(); backUpLocationTextField.setText(path); } DirectoryChooser directoryChooser = new DirectoryChooser(); directoryChooser.setInitialDirectory(new File(path)); directoryChooser.setTitle(Res.get("account.backup.selectLocation")); try { File dir = directoryChooser.showDialog(root.getScene().getWindow()); if (dir != null) { applyBackupDirectory(dir.getAbsolutePath()); } } catch (Throwable t) { showWrongPathWarningAndReset(t); } }); openFileOrShowWarning(openDataDirButton, dataDir); openFileOrShowWarning(openLogsButton, logFile); backupNow.setOnAction(event -> { // windows requires closing wallets for read access if (Utilities.isWindows()) { new Popup().information(Res.get("settings.net.needRestart")) .actionButtonText(Res.get("shared.applyAndShutDown")) .onAction(() -> { UserThread.runAfter(() -> { HavenoApp.setOnGracefulShutDownHandler(() -> doBackup()); HavenoApp.getShutDownHandler().run(); }, 500, TimeUnit.MILLISECONDS); }) .closeButtonText(Res.get("shared.cancel")) .onClose(() -> { // nothing to do }) .show(); } else { doBackup(); } }); } private void doBackup() { log.info("Backing up data directory"); String backupDirectory = preferences.getBackupDirectory(); if (backupDirectory != null && backupDirectory.length() > 0) { // We need to flush data to disk PersistenceManager.flushAllDataToDiskAtBackup(() -> { try { // copy data directory to backup directory String dateString = new SimpleDateFormat("yyyy-MM-dd-HHmmss").format(new Date()); String destination = Paths.get(backupDirectory, "haveno_backup_" + dateString).toString(); File destinationFile = new File(destination); FileUtil.copyDirectory(dataDir, new File(destination)); // delete monerod and monero-wallet-rpc binaries from backup so they're reinstalled with permissions File monerod = new File(destinationFile, XmrLocalNode.MONEROD_NAME); if (monerod.exists()) monerod.delete(); File moneroWalletRpc = new File(destinationFile, XmrWalletService.MONERO_WALLET_RPC_NAME); if (moneroWalletRpc.exists()) moneroWalletRpc.delete(); new Popup().feedback(Res.get("account.backup.success", destination)).show(); } catch (IOException e) { e.printStackTrace(); log.error(e.getMessage()); showWrongPathWarningAndReset(e); } }); } } private void openFileOrShowWarning(Button button, File dataDir) { button.setOnAction(event -> { try { Utilities.openFile(dataDir); } catch (IOException e) { e.printStackTrace(); log.error(e.getMessage()); showWrongPathWarningAndReset(e); } }); } @Override protected void deactivate() { backUpLocationTextField.focusedProperty().removeListener(backUpLocationTextFieldFocusListener); selectBackupDir.setOnAction(null); openDataDirButton.setOnAction(null); openLogsButton.setOnAction(null); backupNow.setOnAction(null); } private void updateButtons() { boolean noBackupSet = backUpLocationTextField.getText() == null || backUpLocationTextField.getText().length() == 0; selectBackupDir.setDefaultButton(noBackupSet); backupNow.setDefaultButton(!noBackupSet); backupNow.setDisable(noBackupSet); } private void showWrongPathWarningAndReset(@Nullable Throwable t) { String error = t != null ? Res.get("shared.errorMessageInline", t.getMessage()) : ""; new Popup().warning(Res.get("account.backup.directoryNotAccessible", error)).show(); applyBackupDirectory(Utilities.getSystemHomeDirectory()); } private void applyBackupDirectory(String path) { if (isPathValid(path)) { preferences.setDirectoryChooserPath(path); backUpLocationTextField.setText(path); preferences.setBackupDirectory(path); updateButtons(); } else { showWrongPathWarningAndReset(null); } } private boolean isPathValid(String path) { return path == null || path.isEmpty() || Utilities.isDirectory(path); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/content/cryptoaccounts/CryptoAccountsDataModel.java ================================================ /* * 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 . */ package haveno.desktop.main.account.content.cryptoaccounts; import com.google.inject.Inject; import haveno.common.file.CorruptedStorageFileHandler; import haveno.common.proto.persistable.PersistenceProtoResolver; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.AssetAccount; import haveno.core.payment.PaymentAccount; import haveno.core.trade.TradeManager; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.desktop.common.model.ActivatableDataModel; import haveno.desktop.util.GUIUtil; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.stage.Stage; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; class CryptoAccountsDataModel extends ActivatableDataModel { private final User user; private final Preferences preferences; private final OpenOfferManager openOfferManager; private final TradeManager tradeManager; private final AccountAgeWitnessService accountAgeWitnessService; final ObservableList paymentAccounts = FXCollections.observableArrayList(); private final ListChangeListener listChangeListener; private final String accountsFileName = "CryptoPaymentAccounts"; private final PersistenceProtoResolver persistenceProtoResolver; private final CorruptedStorageFileHandler corruptedStorageFileHandler; @Inject public CryptoAccountsDataModel(User user, Preferences preferences, OpenOfferManager openOfferManager, TradeManager tradeManager, AccountAgeWitnessService accountAgeWitnessService, PersistenceProtoResolver persistenceProtoResolver, CorruptedStorageFileHandler corruptedStorageFileHandler) { this.user = user; this.preferences = preferences; this.openOfferManager = openOfferManager; this.tradeManager = tradeManager; this.accountAgeWitnessService = accountAgeWitnessService; this.persistenceProtoResolver = persistenceProtoResolver; this.corruptedStorageFileHandler = corruptedStorageFileHandler; listChangeListener = change -> fillAndSortPaymentAccounts(); } @Override protected void activate() { user.getPaymentAccountsAsObservable().addListener(listChangeListener); fillAndSortPaymentAccounts(); } private void fillAndSortPaymentAccounts() { if (user.getPaymentAccounts() != null) { paymentAccounts.setAll(user.getPaymentAccounts().stream() .filter(paymentAccount -> paymentAccount.getPaymentMethod().isBlockchain()) .collect(Collectors.toList())); paymentAccounts.sort(Comparator.comparing(PaymentAccount::getAccountName)); } } @Override protected void deactivate() { user.getPaymentAccountsAsObservable().removeListener(listChangeListener); } /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// public void onSaveNewAccount(PaymentAccount paymentAccount) { TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency(); List tradeCurrencies = paymentAccount.getTradeCurrencies(); if (singleTradeCurrency != null) { if (singleTradeCurrency instanceof TraditionalCurrency) preferences.addTraditionalCurrency((TraditionalCurrency) singleTradeCurrency); else preferences.addCryptoCurrency((CryptoCurrency) singleTradeCurrency); } else if (tradeCurrencies != null && !tradeCurrencies.isEmpty()) { tradeCurrencies.forEach(tradeCurrency -> { if (tradeCurrency instanceof TraditionalCurrency) preferences.addTraditionalCurrency((TraditionalCurrency) tradeCurrency); else preferences.addCryptoCurrency((CryptoCurrency) tradeCurrency); }); } if (paymentAccount.getAccountName() == null) throw new IllegalStateException("Account name cannot be null"); user.addPaymentAccount(paymentAccount); paymentAccount.onPersistChanges(); if (!(paymentAccount instanceof AssetAccount)) accountAgeWitnessService.publishMyAccountAgeWitness(paymentAccount.getPaymentAccountPayload()); } public void onUpdateAccount(PaymentAccount paymentAccount) { if (paymentAccount.getAccountName() == null) throw new IllegalStateException("Account name cannot be null"); paymentAccount.onPersistChanges(); user.requestPersistence(); } public boolean onDeleteAccount(PaymentAccount paymentAccount) { boolean isPaymentAccountUsed = openOfferManager.getObservableList().stream() .filter(o -> o.getOffer().getMakerPaymentAccountId().equals(paymentAccount.getId())) .findAny() .isPresent(); isPaymentAccountUsed = isPaymentAccountUsed || tradeManager.getObservableList().stream() .filter(t -> t.getOffer().getMakerPaymentAccountId().equals(paymentAccount.getId()) || paymentAccount.getId().equals(t.getTaker().getPaymentAccountId())) .findAny() .isPresent(); if (!isPaymentAccountUsed) user.removePaymentAccount(paymentAccount); return isPaymentAccountUsed; } public void onSelectAccount(PaymentAccount paymentAccount) { user.setCurrentPaymentAccount(paymentAccount); } public void exportAccounts(Stage stage) { if (user.getPaymentAccounts() != null) { ArrayList accounts = new ArrayList<>(user.getPaymentAccounts().stream() .filter(paymentAccount -> paymentAccount instanceof AssetAccount) .collect(Collectors.toList())); GUIUtil.exportAccounts(accounts, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler); } } public void importAccounts(Stage stage) { GUIUtil.importAccounts(user, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler); } public int getNumPaymentAccounts() { return user.getPaymentAccounts() != null ? user.getPaymentAccounts().size() : 0; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/content/cryptoaccounts/CryptoAccountsView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/content/cryptoaccounts/CryptoAccountsView.java ================================================ /* * 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 . */ package haveno.desktop.main.account.content.cryptoaccounts; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.asset.Asset; import haveno.asset.CryptoAccountDisclaimer; import haveno.asset.coins.Monero; import haveno.common.util.Tuple2; import haveno.common.util.Tuple3; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.filter.FilterManager; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccountFactory; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.validation.CryptoAddressValidator; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.TitledGroupBg; import haveno.desktop.components.paymentmethods.AssetsForm; import static haveno.desktop.components.paymentmethods.AssetsForm.INSTANT_TRADE_NEWS; import haveno.desktop.components.paymentmethods.PaymentMethodForm; import haveno.desktop.main.account.content.PaymentAccountsView; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.FormBuilder; import static haveno.desktop.util.FormBuilder.add2ButtonsAfterGroup; import static haveno.desktop.util.FormBuilder.add3ButtonsAfterGroup; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import static haveno.desktop.util.FormBuilder.addTopLabelListView; import haveno.desktop.util.Layout; import java.util.Optional; import javafx.collections.ObservableList; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ListView; import javafx.scene.layout.GridPane; import javafx.scene.layout.VBox; import javafx.stage.Stage; @FxmlView public class CryptoAccountsView extends PaymentAccountsView { private final InputValidator inputValidator; private final CryptoAddressValidator altCoinAddressValidator; private final FilterManager filterManager; private final CoinFormatter formatter; private final Preferences preferences; private PaymentMethodForm paymentMethodForm; private TitledGroupBg accountTitledGroupBg; private Button saveNewAccountButton; private int gridRow = 0; @Inject public CryptoAccountsView(CryptoAccountsViewModel model, InputValidator inputValidator, CryptoAddressValidator altCoinAddressValidator, AccountAgeWitnessService accountAgeWitnessService, FilterManager filterManager, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, Preferences preferences) { super(model, accountAgeWitnessService); this.inputValidator = inputValidator; this.altCoinAddressValidator = altCoinAddressValidator; this.filterManager = filterManager; this.formatter = formatter; this.preferences = preferences; } @Override protected ObservableList getPaymentAccounts() { return model.getPaymentAccounts(); } @Override protected void importAccounts() { model.dataModel.importAccounts((Stage) root.getScene().getWindow()); } @Override protected void exportAccounts() { model.dataModel.exportAccounts((Stage) root.getScene().getWindow()); } /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// private void onSaveNewAccount(PaymentAccount paymentAccount) { TradeCurrency selectedTradeCurrency = paymentAccount.getSelectedTradeCurrency(); if (selectedTradeCurrency != null) { if (selectedTradeCurrency instanceof CryptoCurrency && ((CryptoCurrency) selectedTradeCurrency).isAsset()) { String name = selectedTradeCurrency.getName(); new Popup().information(Res.get("account.crypto.popup.wallet.msg", selectedTradeCurrency.getCodeAndName(), name, name)) .closeButtonText(Res.get("account.crypto.popup.wallet.confirm")) .show(); } final Optional asset = CurrencyUtil.findAsset(selectedTradeCurrency.getCode()); if (asset.isPresent()) { final CryptoAccountDisclaimer disclaimerAnnotation = asset.get().getClass().getAnnotation(CryptoAccountDisclaimer.class); if (disclaimerAnnotation != null) { new Popup() .width(asset.get() instanceof Monero ? 1000 : 669) .maxMessageLength(2500) .information(Res.get(disclaimerAnnotation.value())) .useIUnderstandButton() .show(); } } if (model.getPaymentAccounts().stream().noneMatch(e -> e.getAccountName() != null && e.getAccountName().equals(paymentAccount.getAccountName()))) { model.onSaveNewAccount(paymentAccount); removeNewAccountForm(); } else { new Popup().warning(Res.get("shared.accountNameAlreadyUsed")).show(); } preferences.dontShowAgain(INSTANT_TRADE_NEWS, true); } } private void onCancelNewAccount() { removeNewAccountForm(); preferences.dontShowAgain(INSTANT_TRADE_NEWS, true); } private void onUpdateAccount(PaymentAccount paymentAccount) { model.onUpdateAccount(paymentAccount); removeSelectAccountForm(); } private void onCancelSelectedAccount(PaymentAccount paymentAccount) { paymentAccount.revertChanges(); removeSelectAccountForm(); } /////////////////////////////////////////////////////////////////////////////////////////// // Base form /////////////////////////////////////////////////////////////////////////////////////////// protected void buildForm() { addTitledGroupBg(root, gridRow, 2, Res.get("shared.manageAccounts")); Tuple3, VBox> tuple = addTopLabelListView(root, gridRow, Res.get("account.crypto.yourCryptoAccounts"), Layout.FIRST_ROW_DISTANCE); paymentAccountsListView = tuple.second; setPaymentAccountsListHeight(); setPaymentAccountsCellFactory(); Tuple3 tuple3 = add3ButtonsAfterGroup(root, ++gridRow, Res.get("shared.addNewAccount"), Res.get("shared.ExportAccounts"), Res.get("shared.importAccounts")); addAccountButton = tuple3.first; addAccountButton.setId("buy-button-big"); exportButton = tuple3.second; importButton = tuple3.third; } // Add new account form protected void addNewAccount() { paymentAccountsListView.getSelectionModel().clearSelection(); removeAccountRows(); addAccountButton.setDisable(true); accountTitledGroupBg = addTitledGroupBg(root, ++gridRow, 1, Res.get("shared.createNewAccount"), Layout.COMPACT_GROUP_DISTANCE); if (paymentMethodForm != null) { FormBuilder.removeRowsFromGridPane(root, 3, paymentMethodForm.getGridRow() + 1); GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan() + 1); } gridRow = 2; paymentMethodForm = getPaymentMethodForm(PaymentMethod.BLOCK_CHAINS); paymentMethodForm.addFormForAddAccount(); gridRow = paymentMethodForm.getGridRow(); Tuple2 tuple2 = add2ButtonsAfterGroup(root, ++gridRow, Res.get("shared.saveNewAccount"), Res.get("shared.cancel")); saveNewAccountButton = tuple2.first; saveNewAccountButton.setOnAction(event -> onSaveNewAccount(paymentMethodForm.getPaymentAccount())); saveNewAccountButton.disableProperty().bind(paymentMethodForm.allInputsValidProperty().not()); Button cancelButton = tuple2.second; cancelButton.setOnAction(event -> onCancelNewAccount()); GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan() + 1); } // Select account form protected void onSelectAccount(PaymentAccount previous, PaymentAccount current) { if (previous != null) { previous.revertChanges(); } removeAccountRows(); addAccountButton.setDisable(false); accountTitledGroupBg = addTitledGroupBg(root, ++gridRow, 2, "", Layout.COMPACT_GROUP_DISTANCE); paymentMethodForm = getPaymentMethodForm(current); paymentMethodForm.addFormForEditAccount(); gridRow = paymentMethodForm.getGridRow(); Tuple3 tuple = add3ButtonsAfterGroup( root, ++gridRow, Res.get("shared.save"), Res.get("shared.deleteAccount"), Res.get("shared.cancel") ); Button saveAccountButton = tuple.first; saveAccountButton.setOnAction(event -> onUpdateAccount(current)); Button deleteAccountButton = tuple.second; deleteAccountButton.setOnAction(event -> onDeleteAccount(current)); Button cancelButton = tuple.third; cancelButton.setOnAction(event -> onCancelSelectedAccount(current)); GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan()); model.onSelectAccount(current); } /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// private PaymentMethodForm getPaymentMethodForm(PaymentMethod paymentMethod) { PaymentAccount paymentAccount = PaymentAccountFactory.getPaymentAccount(paymentMethod); paymentAccount.init(); return getPaymentMethodForm(paymentAccount); } private PaymentMethodForm getPaymentMethodForm(PaymentAccount paymentAccount) { return new AssetsForm(paymentAccount, accountAgeWitnessService, altCoinAddressValidator, inputValidator, root, gridRow, formatter, filterManager); } private void removeNewAccountForm() { saveNewAccountButton.disableProperty().unbind(); removeAccountRows(); addAccountButton.setDisable(false); } @Override protected void removeSelectAccountForm() { FormBuilder.removeRowsFromGridPane(root, 2, gridRow); gridRow = 1; addAccountButton.setDisable(false); paymentAccountsListView.getSelectionModel().clearSelection(); } @Override protected boolean deleteAccountFromModel(PaymentAccount paymentAccount) { return model.onDeleteAccount(paymentAccount); } private void removeAccountRows() { FormBuilder.removeRowsFromGridPane(root, 2, gridRow); gridRow = 1; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/content/cryptoaccounts/CryptoAccountsViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.account.content.cryptoaccounts; import com.google.inject.Inject; import haveno.core.payment.PaymentAccount; import haveno.desktop.common.model.ActivatableWithDataModel; import haveno.desktop.common.model.ViewModel; import javafx.collections.ObservableList; class CryptoAccountsViewModel extends ActivatableWithDataModel implements ViewModel { @Inject public CryptoAccountsViewModel(CryptoAccountsDataModel dataModel) { super(dataModel); } @Override protected void activate() { } @Override protected void deactivate() { } /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// public void onSaveNewAccount(PaymentAccount paymentAccount) { dataModel.onSaveNewAccount(paymentAccount); } public void onUpdateAccount(PaymentAccount paymentAccount) { dataModel.onUpdateAccount(paymentAccount); } public boolean onDeleteAccount(PaymentAccount paymentAccount) { return dataModel.onDeleteAccount(paymentAccount); } public void onSelectAccount(PaymentAccount paymentAccount) { dataModel.onSelectAccount(paymentAccount); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// ObservableList getPaymentAccounts() { return dataModel.paymentAccounts; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/content/notifications/ManageMarketAlertsWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.account.content.notifications; import haveno.common.UserThread; import haveno.core.locale.Res; import haveno.core.notifications.alerts.market.MarketAlertFilter; import haveno.core.notifications.alerts.market.MarketAlerts; import haveno.core.util.FormattingUtils; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.AutoTooltipTableColumn; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.ImageUtil; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.collections.FXCollections; import javafx.geometry.Insets; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; import javafx.scene.image.ImageView; import javafx.scene.layout.GridPane; import javafx.util.Callback; import lombok.extern.slf4j.Slf4j; @Slf4j public class ManageMarketAlertsWindow extends Overlay { private final MarketAlerts marketAlerts; ManageMarketAlertsWindow(MarketAlerts marketAlerts) { this.marketAlerts = marketAlerts; type = Type.Attention; } @Override public void show() { if (headLine == null) headLine = Res.get("account.notifications.marketAlert.manageAlerts.title"); width = 968; createGridPane(); addHeadLine(); addContent(); addButtons(); applyStyles(); display(); } @Override protected void applyStyles() { super.applyStyles(); gridPane.setId("popup-grid-pane-bg"); } private void addContent() { TableView tableView = new TableView<>(); GUIUtil.applyTableStyle(tableView); GridPane.setRowIndex(tableView, ++rowIndex); GridPane.setColumnSpan(tableView, 2); GridPane.setMargin(tableView, new Insets(10, 0, 0, 0)); gridPane.getChildren().add(tableView); Label placeholder = new AutoTooltipLabel(Res.get("table.placeholder.noData")); placeholder.setWrapText(true); tableView.setPlaceholder(placeholder); tableView.setPrefHeight(300); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); setColumns(tableView); tableView.setItems(FXCollections.observableArrayList(marketAlerts.getMarketAlertFilters())); } private void removeMarketAlertFilter(MarketAlertFilter marketAlertFilter, TableView tableView) { marketAlerts.removeMarketAlertFilter(marketAlertFilter); UserThread.execute(() -> tableView.setItems(FXCollections.observableArrayList(marketAlerts.getMarketAlertFilters()))); } private void setColumns(TableView tableView) { TableColumn column; column = new AutoTooltipTableColumn<>(Res.get("account.notifications.marketAlert.manageAlerts.header.paymentAccount")); column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final MarketAlertFilter item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setText(item.getPaymentAccount().getAccountName()); } else { setText(""); } } }; } }); tableView.getColumns().add(column); column = new AutoTooltipTableColumn<>(Res.get("account.notifications.marketAlert.manageAlerts.header.trigger")); column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final MarketAlertFilter item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setText(FormattingUtils.formatPercentagePrice(item.getTriggerValue() / 10000d)); } else { setText(""); } } }; } }); tableView.getColumns().add(column); column = new AutoTooltipTableColumn<>(Res.get("account.notifications.marketAlert.manageAlerts.header.offerType")); column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final MarketAlertFilter item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setText(item.isBuyOffer() ? Res.get("shared.buyBitcoin") : Res.get("shared.sellBitcoin")); } else { setText(""); } } }; } }); tableView.getColumns().add(column); column = new TableColumn<>(); column.setMinWidth(40); column.setMaxWidth(column.getMinWidth()); column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { final ImageView icon = ImageUtil.getImageViewById(ImageUtil.REMOVE_ICON); final Button removeButton = new AutoTooltipButton("", icon); { removeButton.setId("icon-button"); removeButton.setTooltip(new Tooltip(Res.get("shared.remove"))); } @Override public void updateItem(final MarketAlertFilter item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { removeButton.setOnAction(e -> removeMarketAlertFilter(item, tableView)); setGraphic(removeButton); } else { setGraphic(null); removeButton.setOnAction(null); } } }; } }); tableView.getColumns().add(column); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/content/notifications/MobileNotificationsView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/content/notifications/MobileNotificationsView.java ================================================ /* * 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 . */ package haveno.desktop.main.account.content.notifications; import com.google.inject.Inject; import haveno.common.UserThread; import haveno.common.util.Tuple2; import haveno.common.util.Tuple3; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.notifications.MobileMessage; import haveno.core.notifications.MobileNotificationService; import haveno.core.notifications.alerts.DisputeMsgEvents; import haveno.core.notifications.alerts.MyOfferTakenEvents; import haveno.core.notifications.alerts.TradeEvents; import haveno.core.notifications.alerts.market.MarketAlertFilter; import haveno.core.notifications.alerts.market.MarketAlerts; import haveno.core.notifications.alerts.price.PriceAlert; import haveno.core.notifications.alerts.price.PriceAlertFilter; import haveno.core.payment.PaymentAccount; import haveno.core.payment.validation.PercentageNumberValidator; import haveno.core.provider.price.PriceFeedService; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.util.FormattingUtils; import haveno.core.util.ParsingUtils; import haveno.core.util.PriceUtil; import haveno.core.util.validation.InputValidator; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.InfoInputTextField; import haveno.desktop.components.InputTextField; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.FormBuilder; import static haveno.desktop.util.FormBuilder.addButton; import static haveno.desktop.util.FormBuilder.addInputTextField; import static haveno.desktop.util.FormBuilder.addSlideToggleButton; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import static haveno.desktop.util.FormBuilder.addTopLabelButton; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.Layout; import java.util.ArrayList; import java.util.List; import java.util.Optional; import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.geometry.Insets; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.RadioButton; import javafx.scene.control.TextField; import javafx.scene.control.Toggle; import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleGroup; import javafx.scene.layout.GridPane; import javafx.util.StringConverter; @FxmlView public class MobileNotificationsView extends ActivatableView { private final Preferences preferences; private final User user; private final PriceFeedService priceFeedService; private final MarketAlerts marketAlerts; private final MobileNotificationService mobileNotificationService; private TextField tokenInputTextField; private InputTextField priceAlertHighInputTextField, priceAlertLowInputTextField, marketAlertTriggerInputTextField; private ToggleButton useSoundToggleButton, tradeToggleButton, marketToggleButton, priceToggleButton; private ComboBox currencyComboBox; private ComboBox paymentAccountsComboBox; private Button downloadButton, eraseButton, setPriceAlertButton, removePriceAlertButton, addMarketAlertButton, manageAlertsButton /*,testMsgButton*/; private ChangeListener useSoundCheckBoxListener, tradeCheckBoxListener, marketCheckBoxListener, priceCheckBoxListener, priceAlertHighFocusListener, priceAlertLowFocusListener, marketAlertTriggerFocusListener; private ChangeListener tokenInputTextFieldListener, priceAlertHighListener, priceAlertLowListener, marketAlertTriggerListener; private ChangeListener priceFeedServiceListener; private ListChangeListener paymentAccountsChangeListener; private int gridRow = 0; private int testMsgCounter = 0; private RadioButton buyOffersRadioButton, sellOffersRadioButton; private ToggleGroup offerTypeRadioButtonsToggleGroup; private ChangeListener offerTypeListener; private String selectedPriceAlertTradeCurrency; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject private MobileNotificationsView(Preferences preferences, User user, PriceFeedService priceFeedService, MarketAlerts marketAlerts, MobileNotificationService mobileNotificationService) { super(); this.preferences = preferences; this.user = user; this.priceFeedService = priceFeedService; this.marketAlerts = marketAlerts; this.mobileNotificationService = mobileNotificationService; } @Override public void initialize() { createListeners(); createSetupFields(); createSettingsFields(); createMarketAlertFields(); createPriceAlertFields(); } @Override protected void activate() { addListeners(); // setup tokenInputTextField.textProperty().addListener(tokenInputTextFieldListener); downloadButton.setOnAction(e -> onDownload()); // testMsgButton.setOnAction(e -> onSendTestMsg()); eraseButton.setOnAction(e -> onErase()); // settings useSoundToggleButton.selectedProperty().addListener(useSoundCheckBoxListener); tradeToggleButton.selectedProperty().addListener(tradeCheckBoxListener); marketToggleButton.selectedProperty().addListener(marketCheckBoxListener); priceToggleButton.selectedProperty().addListener(priceCheckBoxListener); // market alert marketAlertTriggerInputTextField.textProperty().addListener(marketAlertTriggerListener); marketAlertTriggerInputTextField.focusedProperty().addListener(marketAlertTriggerFocusListener); offerTypeRadioButtonsToggleGroup.selectedToggleProperty().addListener(offerTypeListener); paymentAccountsComboBox.setOnAction(e -> onPaymentAccountSelected()); addMarketAlertButton.setOnAction(e -> onAddMarketAlert()); manageAlertsButton.setOnAction(e -> onManageMarketAlerts()); fillPaymentAccounts(); // price alert priceAlertHighInputTextField.textProperty().addListener(priceAlertHighListener); priceAlertLowInputTextField.textProperty().addListener(priceAlertLowListener); priceAlertHighInputTextField.focusedProperty().addListener(priceAlertHighFocusListener); priceAlertLowInputTextField.focusedProperty().addListener(priceAlertLowFocusListener); priceFeedService.updateCounterProperty().addListener(priceFeedServiceListener); currencyComboBox.setOnAction(e -> onSelectedTradeCurrency()); setPriceAlertButton.setOnAction(e -> onSetPriceAlert()); removePriceAlertButton.setOnAction(e -> onRemovePriceAlert()); currencyComboBox.setItems(preferences.getTradeCurrenciesAsObservable()); if (preferences.getPhoneKeyAndToken() != null) { tokenInputTextField.setText(preferences.getPhoneKeyAndToken()); setPairingTokenFieldsVisible(); } else { eraseButton.setDisable(true); //testMsgButton.setDisable(true); } setDisableForSetupFields(!mobileNotificationService.isSetupConfirmationSent()); updateMarketAlertFields(); fillPriceAlertFields(); updatePriceAlertFields(); } @Override protected void deactivate() { removeListeners(); // setup tokenInputTextField.textProperty().removeListener(tokenInputTextFieldListener); downloadButton.setOnAction(null); //testMsgButton.setOnAction(null); eraseButton.setOnAction(null); // settings useSoundToggleButton.selectedProperty().removeListener(useSoundCheckBoxListener); tradeToggleButton.selectedProperty().removeListener(tradeCheckBoxListener); marketToggleButton.selectedProperty().removeListener(marketCheckBoxListener); priceToggleButton.selectedProperty().removeListener(priceCheckBoxListener); // market alert marketAlertTriggerInputTextField.textProperty().removeListener(marketAlertTriggerListener); marketAlertTriggerInputTextField.focusedProperty().removeListener(marketAlertTriggerFocusListener); offerTypeRadioButtonsToggleGroup.selectedToggleProperty().removeListener(offerTypeListener); paymentAccountsComboBox.setOnAction(null); addMarketAlertButton.setOnAction(null); manageAlertsButton.setOnAction(null); // price alert priceAlertHighInputTextField.textProperty().removeListener(priceAlertHighListener); priceAlertLowInputTextField.textProperty().removeListener(priceAlertLowListener); priceAlertHighInputTextField.focusedProperty().removeListener(priceAlertHighFocusListener); priceAlertLowInputTextField.focusedProperty().removeListener(priceAlertLowFocusListener); priceFeedService.updateCounterProperty().removeListener(priceFeedServiceListener); currencyComboBox.setOnAction(null); setPriceAlertButton.setOnAction(null); removePriceAlertButton.setOnAction(null); } /////////////////////////////////////////////////////////////////////////////////////////// // UI events /////////////////////////////////////////////////////////////////////////////////////////// // Setup private void onDownload() { GUIUtil.openWebPage("https://haveno.exchange/downloads"); } private void onErase() { try { mobileNotificationService.sendEraseMessage(); reset(); } catch (Exception e) { new Popup().error(e.toString()).show(); } } //TODO: never used --> Do we really want to keep it here if we need it? private void onSendTestMsg() { MobileMessage message = null; List messages = null; switch (testMsgCounter) { case 0: message = MyOfferTakenEvents.getTestMsg(); break; case 1: messages = TradeEvents.getTestMessages(); break; case 2: message = DisputeMsgEvents.getTestMsg(); break; case 3: message = PriceAlert.getTestMsg(); break; case 4: default: message = MarketAlerts.getTestMsg(); break; } testMsgCounter++; if (testMsgCounter > 4) testMsgCounter = 0; try { if (message != null) { mobileNotificationService.sendMessage(message, useSoundToggleButton.isSelected()); } else { messages.forEach(msg -> { try { mobileNotificationService.sendMessage(msg, useSoundToggleButton.isSelected()); } catch (Exception e) { e.printStackTrace(); } }); } } catch (Exception e) { new Popup().error(e.toString()).show(); } } // Market alerts private void onPaymentAccountSelected() { marketAlertTriggerInputTextField.clear(); marketAlertTriggerInputTextField.resetValidation(); offerTypeRadioButtonsToggleGroup.selectToggle(null); updateMarketAlertFields(); } private void onAddMarketAlert() { PaymentAccount paymentAccount = paymentAccountsComboBox.getSelectionModel().getSelectedItem(); double percentAsDouble = ParsingUtils.parsePercentStringToDouble(marketAlertTriggerInputTextField.getText()); int triggerValue = (int) Math.round(percentAsDouble * 10000); boolean isBuyOffer = offerTypeRadioButtonsToggleGroup.getSelectedToggle() == buyOffersRadioButton; MarketAlertFilter marketAlertFilter = new MarketAlertFilter(paymentAccount, triggerValue, isBuyOffer); marketAlerts.addMarketAlertFilter(marketAlertFilter); paymentAccountsComboBox.getSelectionModel().clearSelection(); } private void onManageMarketAlerts() { new ManageMarketAlertsWindow(marketAlerts) .onClose(this::updateMarketAlertFields) .show(); } // Price alerts private void onSelectedTradeCurrency() { TradeCurrency selectedItem = currencyComboBox.getSelectionModel().getSelectedItem(); if (selectedItem != null) { selectedPriceAlertTradeCurrency = selectedItem.getCode(); priceAlertHighInputTextField.setValidator(PriceUtil.getPriceValidator(selectedPriceAlertTradeCurrency)); priceAlertLowInputTextField.setValidator(PriceUtil.getPriceValidator(selectedPriceAlertTradeCurrency)); } else { selectedPriceAlertTradeCurrency = null; } updatePriceAlertFields(); } private void onSetPriceAlert() { if (arePriceAlertInputsValid()) { String code = selectedPriceAlertTradeCurrency; long high = getPriceAsLong(priceAlertHighInputTextField); long low = getPriceAsLong(priceAlertLowInputTextField); if (high > 0 && low > 0) user.setPriceAlertFilter(new PriceAlertFilter(code, high, low)); updatePriceAlertFields(); } } private void onRemovePriceAlert() { user.removePriceAlertFilter(); fillPriceAlertFields(); updatePriceAlertFields(); } /////////////////////////////////////////////////////////////////////////////////////////// // Create views /////////////////////////////////////////////////////////////////////////////////////////// private void createSetupFields() { addTitledGroupBg(root, gridRow, 4, Res.get("account.notifications.setup.title")); downloadButton = addButton(root, gridRow, Res.get("account.notifications.download.label"), Layout.TWICE_FIRST_ROW_DISTANCE); tokenInputTextField = addInputTextField(root, ++gridRow, Res.get("account.notifications.email.label")); tokenInputTextField.setPromptText(Res.get("account.notifications.email.prompt")); tokenInputTextFieldListener = (observable, oldValue, newValue) -> applyKeyAndToken(newValue); /*testMsgButton = FormBuilder.addTopLabelButton(root, ++gridRow, Res.get("account.notifications.testMsg.label"), Res.get("account.notifications.testMsg.title")).second; testMsgButton.setDefaultButton(false);*/ eraseButton = addTopLabelButton(root, ++gridRow, Res.get("account.notifications.erase.label"), Res.get("account.notifications.erase.title")).second; eraseButton.setId("notification-erase-button"); } private void createSettingsFields() { addTitledGroupBg(root, ++gridRow, 4, Res.get("account.notifications.settings.title"), Layout.GROUP_DISTANCE); useSoundToggleButton = addSlideToggleButton(root, gridRow, Res.get("account.notifications.useSound.label"), Layout.FIRST_ROW_AND_GROUP_DISTANCE); useSoundToggleButton.setSelected(preferences.isUseSoundForMobileNotifications()); useSoundCheckBoxListener = (observable, oldValue, newValue) -> { mobileNotificationService.getUseSoundProperty().set(newValue); preferences.setUseSoundForMobileNotifications(newValue); }; tradeToggleButton = addSlideToggleButton(root, ++gridRow, Res.get("account.notifications.trade.label")); tradeToggleButton.setSelected(preferences.isUseTradeNotifications()); tradeCheckBoxListener = (observable, oldValue, newValue) -> { mobileNotificationService.getUseTradeNotificationsProperty().set(newValue); preferences.setUseTradeNotifications(newValue); }; marketToggleButton = addSlideToggleButton(root, ++gridRow, Res.get("account.notifications.market.label")); marketToggleButton.setSelected(preferences.isUseMarketNotifications()); marketCheckBoxListener = (observable, oldValue, newValue) -> { mobileNotificationService.getUseMarketNotificationsProperty().set(newValue); preferences.setUseMarketNotifications(newValue); updateMarketAlertFields(); }; priceToggleButton = addSlideToggleButton(root, ++gridRow, Res.get("account.notifications.price.label")); priceToggleButton.setSelected(preferences.isUsePriceNotifications()); priceCheckBoxListener = (observable, oldValue, newValue) -> { mobileNotificationService.getUsePriceNotificationsProperty().set(newValue); preferences.setUsePriceNotifications(newValue); updatePriceAlertFields(); }; } private void createMarketAlertFields() { addTitledGroupBg(root, ++gridRow, 4, Res.get("account.notifications.marketAlert.title"), Layout.GROUP_DISTANCE); paymentAccountsComboBox = FormBuilder.addComboBox(root, gridRow, Res.get("account.notifications.marketAlert.selectPaymentAccount"), Layout.FIRST_ROW_AND_GROUP_DISTANCE); paymentAccountsComboBox.setConverter(new StringConverter<>() { @Override public String toString(PaymentAccount paymentAccount) { return paymentAccount.getAccountName(); } @Override public PaymentAccount fromString(String string) { return null; } }); offerTypeRadioButtonsToggleGroup = new ToggleGroup(); Tuple3 tuple = FormBuilder.addTopLabelRadioButtonRadioButton(root, ++gridRow, offerTypeRadioButtonsToggleGroup, Res.get("account.notifications.marketAlert.offerType.label"), Res.get("account.notifications.marketAlert.offerType.buy"), Res.get("account.notifications.marketAlert.offerType.sell"), 10); buyOffersRadioButton = tuple.second; sellOffersRadioButton = tuple.third; offerTypeListener = (observable, oldValue, newValue) -> { marketAlertTriggerInputTextField.clear(); marketAlertTriggerInputTextField.resetValidation(); updateMarketAlertFields(); }; InfoInputTextField infoInputTextField = FormBuilder.addTopLabelInfoInputTextField(root, ++gridRow, Res.get("account.notifications.marketAlert.trigger"), 10).second; marketAlertTriggerInputTextField = infoInputTextField.getInputTextField(); marketAlertTriggerInputTextField.setPromptText(Res.get("account.notifications.marketAlert.trigger.prompt")); PercentageNumberValidator validator = new PercentageNumberValidator(); validator.setMaxValue(50D); marketAlertTriggerInputTextField.setValidator(validator); infoInputTextField.setContentForInfoPopOver(createMarketAlertPriceInfoPopupLabel(Res.get("account.notifications.marketAlert.trigger.info"))); infoInputTextField.setIconsRightAligned(); marketAlertTriggerListener = (observable, oldValue, newValue) -> updateMarketAlertFields(); marketAlertTriggerFocusListener = (observable, oldValue, newValue) -> { if (oldValue && !newValue) { try { double percentAsDouble = ParsingUtils.parsePercentStringToDouble(marketAlertTriggerInputTextField.getText()) * 100; marketAlertTriggerInputTextField.setText(FormattingUtils.formatRoundedDoubleWithPrecision(percentAsDouble, 2) + "%"); } catch (Throwable ignore) { } updateMarketAlertFields(); } }; Tuple2 buttonTuple = FormBuilder.add2ButtonsAfterGroup(root, ++gridRow, Res.get("account.notifications.marketAlert.addButton"), Res.get("account.notifications.marketAlert.manageAlertsButton")); addMarketAlertButton = buttonTuple.first; manageAlertsButton = buttonTuple.second; } private void createPriceAlertFields() { addTitledGroupBg(root, ++gridRow, 4, Res.get("account.notifications.priceAlert.title"), 20); currencyComboBox = FormBuilder.addComboBox(root, gridRow, Res.get("list.currency.select"), 40); currencyComboBox.setPromptText(Res.get("list.currency.select")); currencyComboBox.setConverter(new StringConverter<>() { @Override public String toString(TradeCurrency currency) { return currency.getNameAndCode(); } @Override public TradeCurrency fromString(String string) { return null; } }); priceAlertHighInputTextField = addInputTextField(root, ++gridRow, Res.get("account.notifications.priceAlert.high.label")); priceAlertHighListener = (observable, oldValue, newValue) -> { long priceAlertHighTextFieldValue = getPriceAsLong(priceAlertHighInputTextField); long priceAlertLowTextFieldValue = getPriceAsLong(priceAlertLowInputTextField); if (priceAlertLowTextFieldValue != 0 && priceAlertHighTextFieldValue != 0) { if (priceAlertHighTextFieldValue > priceAlertLowTextFieldValue) updatePriceAlertFields(); } }; priceAlertHighFocusListener = (observable, oldValue, newValue) -> { if (oldValue && !newValue) { applyPriceFormatting(priceAlertHighInputTextField); long priceAlertHighTextFieldValue = getPriceAsLong(priceAlertHighInputTextField); long priceAlertLowTextFieldValue = getPriceAsLong(priceAlertLowInputTextField); if (priceAlertLowTextFieldValue != 0 && priceAlertHighTextFieldValue != 0) { if (priceAlertHighTextFieldValue <= priceAlertLowTextFieldValue) { new Popup().warning(Res.get("account.notifications.priceAlert.warning.highPriceTooLow")).show(); UserThread.execute(() -> { priceAlertHighInputTextField.clear(); updatePriceAlertFields(); }); } } } }; priceAlertLowInputTextField = addInputTextField(root, ++gridRow, Res.get("account.notifications.priceAlert.low.label")); priceAlertLowListener = (observable, oldValue, newValue) -> { long priceAlertHighTextFieldValue = getPriceAsLong(priceAlertHighInputTextField); long priceAlertLowTextFieldValue = getPriceAsLong(priceAlertLowInputTextField); if (priceAlertLowTextFieldValue != 0 && priceAlertHighTextFieldValue != 0) { if (priceAlertLowTextFieldValue < priceAlertHighTextFieldValue) updatePriceAlertFields(); } }; priceAlertLowFocusListener = (observable, oldValue, newValue) -> { applyPriceFormatting(priceAlertLowInputTextField); long priceAlertHighTextFieldValue = getPriceAsLong(priceAlertHighInputTextField); long priceAlertLowTextFieldValue = getPriceAsLong(priceAlertLowInputTextField); if (priceAlertLowTextFieldValue != 0 && priceAlertHighTextFieldValue != 0) { if (priceAlertLowTextFieldValue >= priceAlertHighTextFieldValue) { new Popup().warning(Res.get("account.notifications.priceAlert.warning.lowerPriceTooHigh")).show(); UserThread.execute(() -> { priceAlertLowInputTextField.clear(); updatePriceAlertFields(); }); } } }; Tuple2 tuple = FormBuilder.add2ButtonsAfterGroup(root, ++gridRow, Res.get("account.notifications.priceAlert.setButton"), Res.get("account.notifications.priceAlert.removeButton")); setPriceAlertButton = tuple.first; removePriceAlertButton = tuple.second; // When we get a price update an existing price alert might get removed. // We get updated the view at each price update so we get aware of the removed PriceAlertFilter in the // fillPriceAlertFields method. To be sure that we called after the PriceAlertFilter has been removed we delay // to the next frame. The priceFeedServiceListener in the mobileNotificationService might get called before // our listener here. priceFeedServiceListener = (observable, oldValue, newValue) -> UserThread.execute(() -> { fillPriceAlertFields(); updatePriceAlertFields(); }); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// // Setup/Settings private void applyKeyAndToken(String keyAndToken) { if (keyAndToken != null && !keyAndToken.isEmpty()) { boolean isValid = mobileNotificationService.applyKeyAndToken(keyAndToken); if (isValid) { setDisableForSetupFields(false); setPairingTokenFieldsVisible(); updateMarketAlertFields(); updatePriceAlertFields(); } } } private void setDisableForSetupFields(boolean disable) { // testMsgButton.setDisable(disable); eraseButton.setDisable(disable); useSoundToggleButton.setDisable(disable); tradeToggleButton.setDisable(disable); marketToggleButton.setDisable(disable); priceToggleButton.setDisable(disable); } private void setPairingTokenFieldsVisible() { tokenInputTextField.setManaged(true); tokenInputTextField.setVisible(true); } private void reset() { mobileNotificationService.reset(); tokenInputTextField.clear(); setDisableForSetupFields(true); eraseButton.setDisable(true); //testMsgButton.setDisable(true); onRemovePriceAlert(); new ArrayList<>(marketAlerts.getMarketAlertFilters()).forEach(marketAlerts::removeMarketAlertFilter); } // Market alerts private Label createMarketAlertPriceInfoPopupLabel(String text) { final Label label = new Label(text); label.setPrefWidth(300); label.setWrapText(true); label.setPadding(new Insets(10)); return label; } private void updateMarketAlertFields() { boolean setupConfirmationSent = mobileNotificationService.isSetupConfirmationSent(); boolean selected = marketToggleButton.isSelected(); boolean disabled = !selected || !setupConfirmationSent; boolean isPaymentAccountSelected = paymentAccountsComboBox.getSelectionModel().getSelectedItem() != null; boolean isOfferTypeSelected = offerTypeRadioButtonsToggleGroup.getSelectedToggle() != null; boolean isTriggerValueValid = marketAlertTriggerInputTextField.getValidator() != null && marketAlertTriggerInputTextField.getValidator().validate(marketAlertTriggerInputTextField.getText()).isValid; boolean allInputsValid = isPaymentAccountSelected && isOfferTypeSelected && isTriggerValueValid; paymentAccountsComboBox.setDisable(disabled); buyOffersRadioButton.setDisable(disabled); sellOffersRadioButton.setDisable(disabled); marketAlertTriggerInputTextField.setDisable(disabled); addMarketAlertButton.setDisable(disabled || !allInputsValid); manageAlertsButton.setDisable(disabled || marketAlerts.getMarketAlertFilters().isEmpty()); } // PriceAlert private void fillPriceAlertFields() { PriceAlertFilter priceAlertFilter = user.getPriceAlertFilter(); if (priceAlertFilter != null) { String currencyCode = priceAlertFilter.getCurrencyCode(); Optional optionalTradeCurrency = CurrencyUtil.getTradeCurrency(currencyCode); if (optionalTradeCurrency.isPresent()) { currencyComboBox.getSelectionModel().select(optionalTradeCurrency.get()); onSelectedTradeCurrency(); priceAlertHighInputTextField.setText(PriceUtil.formatMarketPrice(priceAlertFilter.getHigh(), currencyCode)); priceAlertHighInputTextField.setText(FormattingUtils.formatMarketPrice(priceAlertFilter.getHigh() / 10000d, currencyCode)); priceAlertLowInputTextField.setText(FormattingUtils.formatMarketPrice(priceAlertFilter.getLow() / 10000d, currencyCode)); } else { currencyComboBox.getSelectionModel().clearSelection(); } } else { priceAlertHighInputTextField.clear(); priceAlertLowInputTextField.clear(); priceAlertHighInputTextField.resetValidation(); priceAlertLowInputTextField.resetValidation(); currencyComboBox.getSelectionModel().clearSelection(); } } private void updatePriceAlertFields() { boolean setupConfirmationSent = mobileNotificationService.isSetupConfirmationSent(); boolean selected = priceToggleButton.isSelected(); boolean disable = !setupConfirmationSent || !selected; priceAlertHighInputTextField.setDisable(selectedPriceAlertTradeCurrency == null || disable); priceAlertLowInputTextField.setDisable(selectedPriceAlertTradeCurrency == null || disable); PriceAlertFilter priceAlertFilter = user.getPriceAlertFilter(); boolean valueSameAsFilter = false; if (priceAlertFilter != null && selectedPriceAlertTradeCurrency != null) { valueSameAsFilter = priceAlertFilter.getHigh() == getPriceAsLong(priceAlertHighInputTextField) && priceAlertFilter.getLow() == getPriceAsLong(priceAlertLowInputTextField) && priceAlertFilter.getCurrencyCode().equals(selectedPriceAlertTradeCurrency); } setPriceAlertButton.setDisable(disable || !arePriceAlertInputsValid() || valueSameAsFilter); removePriceAlertButton.setDisable(disable || priceAlertFilter == null); currencyComboBox.setDisable(disable); } private boolean arePriceAlertInputsValid() { return selectedPriceAlertTradeCurrency != null && isPriceInputValid(priceAlertHighInputTextField).isValid && isPriceInputValid(priceAlertLowInputTextField).isValid; } private InputValidator.ValidationResult isPriceInputValid(InputTextField inputTextField) { InputValidator validator = inputTextField.getValidator(); if (validator != null) return validator.validate(inputTextField.getText()); else return new InputValidator.ValidationResult(false); } private long getPriceAsLong(InputTextField inputTextField) { return PriceUtil.getMarketPriceAsLong(inputTextField.getText(), selectedPriceAlertTradeCurrency); } private void applyPriceFormatting(InputTextField inputTextField) { try { String reformattedPrice = PriceUtil.reformatMarketPrice(inputTextField.getText(), selectedPriceAlertTradeCurrency); inputTextField.setText(reformattedPrice); } catch (Throwable ignore) { updatePriceAlertFields(); } } private void createListeners() { paymentAccountsChangeListener = change -> fillPaymentAccounts(); } private void addListeners() { user.getPaymentAccountsAsObservable().addListener(paymentAccountsChangeListener); } private void removeListeners() { user.getPaymentAccountsAsObservable().removeListener(paymentAccountsChangeListener); } private void fillPaymentAccounts() { paymentAccountsComboBox.setItems(FXCollections.observableArrayList(user.getPaymentAccounts())); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/content/notifications/NoWebCamFoundException.java ================================================ /* * 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 . */ package haveno.desktop.main.account.content.notifications; public class NoWebCamFoundException extends Throwable { public NoWebCamFoundException(String msg) { super(msg); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/content/password/PasswordView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/content/password/PasswordView.java ================================================ /* * 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 . */ package haveno.desktop.main.account.content.password; import static com.google.common.base.Preconditions.checkArgument; import com.google.inject.Inject; import com.jfoenix.validation.RequiredFieldValidator; import haveno.common.util.Tuple4; import haveno.core.api.CoreAccountService; import haveno.core.locale.Res; import haveno.core.xmr.wallet.WalletsManager; import haveno.desktop.Navigation; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.BusyAnimation; import haveno.desktop.components.PasswordTextField; import haveno.desktop.components.TitledGroupBg; import haveno.desktop.main.MainView; import haveno.desktop.main.account.AccountView; import haveno.desktop.main.account.content.backup.BackupView; import haveno.desktop.main.overlays.popups.Popup; import static haveno.desktop.util.FormBuilder.addButtonBusyAnimationLabel; import static haveno.desktop.util.FormBuilder.addMultilineLabel; import static haveno.desktop.util.FormBuilder.addPasswordTextField; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import haveno.desktop.util.Layout; import haveno.desktop.util.validation.PasswordValidator; import javafx.beans.value.ChangeListener; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; @FxmlView public class PasswordView extends ActivatableView { private final WalletsManager walletsManager; private final PasswordValidator passwordValidator; private final Navigation navigation; private final CoreAccountService accountService; private PasswordTextField passwordField; private PasswordTextField repeatedPasswordField; private AutoTooltipButton pwButton; private TitledGroupBg headline; private int gridRow = 0; private ChangeListener passwordFieldFocusChangeListener; private ChangeListener passwordFieldTextChangeListener; private ChangeListener repeatedPasswordFieldChangeListener; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject private PasswordView(CoreAccountService accountService, WalletsManager walletsManager, PasswordValidator passwordValidator, Navigation navigation) { this.walletsManager = walletsManager; this.passwordValidator = passwordValidator; this.navigation = navigation; this.accountService = accountService; } @Override public void initialize() { headline = addTitledGroupBg(root, gridRow, 3, ""); passwordField = addPasswordTextField(root, gridRow, Res.get("password.enterPassword"), Layout.TWICE_FIRST_ROW_DISTANCE); final RequiredFieldValidator requiredFieldValidator = new RequiredFieldValidator(); passwordField.getValidators().addAll(requiredFieldValidator, passwordValidator); passwordFieldFocusChangeListener = (observable, oldValue, newValue) -> { if (!newValue) validatePasswords(); }; passwordFieldTextChangeListener = (observable, oldvalue, newValue) -> { if (oldvalue != newValue) validatePasswords(); }; repeatedPasswordField = addPasswordTextField(root, ++gridRow, Res.get("password.confirmPassword")); requiredFieldValidator.setMessage(Res.get("validation.empty")); repeatedPasswordField.getValidators().addAll(requiredFieldValidator, passwordValidator); repeatedPasswordFieldChangeListener = (observable, oldValue, newValue) -> { if (oldValue != newValue) validatePasswords(); }; Tuple4 tuple = addButtonBusyAnimationLabel(root, ++gridRow, 0, "", 10); pwButton = (AutoTooltipButton) tuple.first; BusyAnimation busyAnimation = tuple.second; Label deriveStatusLabel = tuple.third; pwButton.setDisable(true); setText(); pwButton.setOnAction(e -> { if (!walletsManager.areWalletsEncrypted()) { new Popup().backgroundInfo(Res.get("password.backupReminder")) .actionButtonText(Res.get("password.setPassword")) .onAction(() -> onApplyPassword(busyAnimation, deriveStatusLabel)) .secondaryActionButtonText(Res.get("password.makeBackup")) .onSecondaryAction(() -> { navigation.setReturnPath(navigation.getCurrentPath()); navigation.navigateTo(MainView.class, AccountView.class, BackupView.class); }) .width(800) .show(); } else { onApplyPassword(busyAnimation, deriveStatusLabel); } }); addTitledGroupBg(root, ++gridRow, 1, Res.get("shared.information"), Layout.GROUP_DISTANCE); addMultilineLabel(root, gridRow, Res.get("account.password.info"), Layout.FIRST_ROW_AND_GROUP_DISTANCE); } private void onApplyPassword(BusyAnimation busyAnimation, Label deriveStatusLabel) { String password = passwordField.getText(); checkArgument(password.length() < 500, Res.get("password.tooLong")); pwButton.setDisable(true); deriveStatusLabel.setText(Res.get("password.deriveKey")); busyAnimation.play(); if (walletsManager.areWalletsEncrypted()) { try { accountService.changePassword(password, null); new Popup() .feedback(Res.get("password.walletDecrypted")) .show(); backupWalletAndResetFields(); } catch (Throwable t) { pwButton.setDisable(false); new Popup() .warning(Res.get("password.wrongPw")) .show(); } } else { try { accountService.changePassword(accountService.getPassword(), password); new Popup() .feedback(Res.get("password.walletEncrypted")) .show(); backupWalletAndResetFields(); walletsManager.clearBackup(); } catch (Throwable t) { log.error("Error applying password: {}\n", t.getMessage(), t); new Popup() .warning(Res.get("password.walletEncryptionFailed") + "\n\n" + t.getMessage()) .show(); } } setText(); updatePasswordListeners(); deriveStatusLabel.setText(""); busyAnimation.stop(); } private void backupWalletAndResetFields() { passwordField.clear(); repeatedPasswordField.clear(); walletsManager.backupWallets(); } private void setText() { if (walletsManager.areWalletsEncrypted()) { pwButton.updateText(Res.get("account.password.removePw.button")); headline.setText(Res.get("account.password.removePw.headline")); repeatedPasswordField.setVisible(false); repeatedPasswordField.setManaged(false); } else { pwButton.updateText(Res.get("account.password.setPw.button")); headline.setText(Res.get("account.password.setPw.headline")); repeatedPasswordField.setVisible(true); repeatedPasswordField.setManaged(true); } } @Override protected void activate() { updatePasswordListeners(); repeatedPasswordField.textProperty().addListener(repeatedPasswordFieldChangeListener); } private void updatePasswordListeners() { passwordField.focusedProperty().removeListener(passwordFieldFocusChangeListener); passwordField.textProperty().removeListener(passwordFieldTextChangeListener); if (walletsManager.areWalletsEncrypted()) { passwordField.textProperty().addListener(passwordFieldTextChangeListener); } else { passwordField.focusedProperty().addListener(passwordFieldFocusChangeListener); } } @Override protected void deactivate() { passwordField.focusedProperty().removeListener(passwordFieldFocusChangeListener); passwordField.textProperty().removeListener(passwordFieldTextChangeListener); repeatedPasswordField.textProperty().removeListener(repeatedPasswordFieldChangeListener); } private void validatePasswords() { passwordValidator.setPasswordsMatch(true); if (passwordField.validate()) { if (walletsManager.areWalletsEncrypted()) { pwButton.setDisable(false); return; } else { if (repeatedPasswordField.validate()) { if (passwordField.getText().equals(repeatedPasswordField.getText())) { pwButton.setDisable(false); return; } else { passwordValidator.setPasswordsMatch(false); repeatedPasswordField.validate(); } } } } pwButton.setDisable(true); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/content/seedwords/SeedWordsView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/content/seedwords/SeedWordsView.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.desktop.main.account.content.seedwords; import com.google.common.base.Splitter; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.core.locale.Res; import haveno.core.offer.OpenOfferManager; import haveno.core.user.DontShowAgainLookup; import haveno.core.xmr.wallet.WalletsManager; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.common.view.FxmlView; import haveno.desktop.main.SharedPresentation; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.WalletPasswordWindow; import static haveno.desktop.util.FormBuilder.addMultilineLabel; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import static haveno.desktop.util.FormBuilder.addTopLabelDatePicker; import static haveno.desktop.util.FormBuilder.addTopLabelTextArea; import haveno.desktop.util.Layout; import java.io.File; import java.io.IOException; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZoneOffset; import java.util.TimeZone; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ChangeListener; import javafx.scene.control.Button; import javafx.scene.control.DatePicker; import javafx.scene.control.TextArea; import javafx.scene.layout.GridPane; import org.bitcoinj.crypto.MnemonicCode; import org.bitcoinj.crypto.MnemonicException; import org.bitcoinj.wallet.DeterministicSeed; //import static javafx.beans.binding.Bindings.createBooleanBinding; @FxmlView public class SeedWordsView extends ActivatableView { private final WalletsManager walletsManager; private final OpenOfferManager openOfferManager; private final XmrWalletService xmrWalletService; private final WalletPasswordWindow walletPasswordWindow; private final File storageDir; private Button restoreButton; private TextArea displaySeedWordsTextArea, seedWordsTextArea; private DatePicker datePicker, restoreDatePicker; private int gridRow = 0; private ChangeListener seedWordsValidChangeListener; private final SimpleBooleanProperty seedWordsValid = new SimpleBooleanProperty(false); private ChangeListener seedWordsTextAreaChangeListener; private final BooleanProperty seedWordsEdited = new SimpleBooleanProperty(); private String seedWordText; private LocalDate walletCreationDate; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject private SeedWordsView(WalletsManager walletsManager, OpenOfferManager openOfferManager, XmrWalletService xmrWalletService, WalletPasswordWindow walletPasswordWindow, @Named(Config.STORAGE_DIR) File storageDir) { this.walletsManager = walletsManager; this.openOfferManager = openOfferManager; this.xmrWalletService = xmrWalletService; this.walletPasswordWindow = walletPasswordWindow; this.storageDir = storageDir; } @Override protected void initialize() { addTitledGroupBg(root, gridRow, 2, Res.get("account.seed.backup.title")); displaySeedWordsTextArea = addTopLabelTextArea(root, gridRow, Res.get("seed.seedWords"), "", Layout.FIRST_ROW_DISTANCE).second; displaySeedWordsTextArea.getStyleClass().add("wallet-seed-words"); displaySeedWordsTextArea.setPrefHeight(70); displaySeedWordsTextArea.setMaxHeight(70); displaySeedWordsTextArea.setEditable(false); datePicker = addTopLabelDatePicker(root, ++gridRow, Res.get("seed.date"), 10).second; datePicker.setMouseTransparent(true); // TODO: to re-enable restore functionality: // - uncomment code throughout this file // - support getting wallet's restore height // - support translating between date and restore height // - clear XmrAddressEntries which are incompatible with new wallet and other tests // - update mnemonic validation and restore calls // addTitledGroupBg(root, ++gridRow, 3, Res.get("seed.restore.title"), Layout.GROUP_DISTANCE); // seedWordsTextArea = addTopLabelTextArea(root, gridRow, Res.get("seed.seedWords"), "", Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; // seedWordsTextArea.getStyleClass().add("wallet-seed-words"); // seedWordsTextArea.setPrefHeight(40); // seedWordsTextArea.setMaxHeight(40); // restoreDatePicker = addTopLabelDatePicker(root, ++gridRow, Res.get("seed.date"), 10).second; // restoreButton = addPrimaryActionButtonAFterGroup(root, ++gridRow, Res.get("seed.restore")); addTitledGroupBg(root, ++gridRow, 1, Res.get("shared.information"), Layout.GROUP_DISTANCE); addMultilineLabel(root, gridRow, Res.get("account.seed.info"), Layout.FIRST_ROW_AND_GROUP_DISTANCE); seedWordsValidChangeListener = (observable, oldValue, newValue) -> { if (newValue) { seedWordsTextArea.getStyleClass().remove("validation-error"); } else { seedWordsTextArea.getStyleClass().add("validation-error"); } }; seedWordsTextAreaChangeListener = (observable, oldValue, newValue) -> { seedWordsEdited.set(true); try { MnemonicCode codec = new MnemonicCode(); codec.check(Splitter.on(" ").splitToList(newValue)); seedWordsValid.set(true); } catch (IOException | MnemonicException e) { seedWordsValid.set(false); } }; } @Override public void activate() { // seedWordsValid.addListener(seedWordsValidChangeListener); // seedWordsTextArea.textProperty().addListener(seedWordsTextAreaChangeListener); // restoreButton.disableProperty().bind(createBooleanBinding(() -> !seedWordsValid.get() || !seedWordsEdited.get(), // seedWordsValid, seedWordsEdited)); // restoreButton.setOnAction(e -> { // new Popup().information(Res.get("account.seed.restore.info")) // .closeButtonText(Res.get("shared.cancel")) // .actionButtonText(Res.get("account.seed.restore.ok")) // .onAction(this::onRestore) // .show(); // }); // seedWordsTextArea.getStyleClass().remove("validation-error"); // restoreDatePicker.getStyleClass().remove("validation-error"); String key = "showBackupWarningAtSeedPhrase"; if (DontShowAgainLookup.showAgain(key)) { new Popup().warning(Res.get("account.seed.backup.warning")) .onAction(this::showSeedPhrase) .actionButtonText(Res.get("shared.iUnderstand")) .useIUnderstandButton() .dontShowAgainId(key) .hideCloseButton() .show(); } else { showSeedPhrase(); } } private void showSeedPhrase() { if (xmrWalletService.isWalletEncrypted()) { askForPassword(); } else { String key = "showSeedWordsWarning"; if (DontShowAgainLookup.showAgain(key)) { new Popup().warning(Res.get("account.seed.warn.noPw.msg")) .actionButtonText(Res.get("account.seed.warn.noPw.yes")) .onAction(() -> { DontShowAgainLookup.dontShowAgain(key, true); initSeedWords(xmrWalletService.getWallet().getSeed()); showSeedScreen(); }) .closeButtonText(Res.get("shared.no")) .show(); } else { initSeedWords(xmrWalletService.getWallet().getSeed()); showSeedScreen(); } } } @Override protected void deactivate() { displaySeedWordsTextArea.setText(""); datePicker.setValue(null); // seedWordsValid.removeListener(seedWordsValidChangeListener); // seedWordsTextArea.textProperty().removeListener(seedWordsTextAreaChangeListener); // restoreButton.disableProperty().unbind(); // restoreButton.setOnAction(null); // seedWordsTextArea.setText(""); // restoreDatePicker.setValue(null); // seedWordsTextArea.getStyleClass().remove("validation-error"); // restoreDatePicker.getStyleClass().remove("validation-error"); } private void askForPassword() { walletPasswordWindow.headLine(Res.get("account.seed.enterPw")).onSuccess(() -> { initSeedWords(xmrWalletService.getWallet().getSeed()); showSeedScreen(); }).hideForgotPasswordButton().show(); } private void initSeedWords(String seed) { seedWordText = seed; } private void showSeedScreen() { displaySeedWordsTextArea.setText(seedWordText); walletCreationDate = Instant.ofEpochSecond(xmrWalletService.getWalletCreationDate()).atZone(ZoneId.systemDefault()).toLocalDate(); datePicker.setValue(walletCreationDate); } private void onRestore() { if (walletsManager.hasPositiveBalance()) { new Popup().warning(Res.get("seed.warn.walletNotEmpty.msg")) .actionButtonText(Res.get("seed.warn.walletNotEmpty.restore")) .onAction(this::checkIfEncrypted) .closeButtonText(Res.get("seed.warn.walletNotEmpty.emptyWallet")) .show(); } else { checkIfEncrypted(); } } private void checkIfEncrypted() { if (walletsManager.areWalletsEncrypted()) { new Popup().information(Res.get("seed.warn.notEncryptedAnymore")) .closeButtonText(Res.get("shared.no")) .actionButtonText(Res.get("shared.yes")) .onAction(this::doRestoreDateCheck) .show(); } else { doRestoreDateCheck(); } } private void doRestoreDateCheck() { if (restoreDatePicker.getValue() == null) { // Provide feedback when attempting to restore a wallet from seed words without specifying a date new Popup().information(Res.get("seed.warn.walletDateEmpty")) .closeButtonText(Res.get("shared.no")) .actionButtonText(Res.get("shared.yes")) .onAction(this::doRestore) .show(); } else { doRestore(); } } private LocalDate getWalletDate() { LocalDate walletDate = restoreDatePicker.getValue(); // Even though no current Haveno wallet could have been created before the v0.5 release date (2017.06.28), // the user may want to import from a seed generated by another wallet. // So use when the BIP39 standard was finalised (2013.10.09) as the oldest possible wallet date. LocalDate oldestWalletDate = LocalDate.ofInstant( Instant.ofEpochMilli(MnemonicCode.BIP39_STANDARDISATION_TIME_SECS * 1000), TimeZone.getDefault().toZoneId()); if (walletDate == null) { // No date was specified, perhaps the user doesn't know the wallet date walletDate = oldestWalletDate; } else if (walletDate.isBefore(oldestWalletDate)) { walletDate = oldestWalletDate; } else if (walletDate.isAfter(LocalDate.now())) { walletDate = LocalDate.now(); } return walletDate; } private void doRestore() { LocalDate walletDate = getWalletDate(); // We subtract 1 day to be sure to not have any issues with timezones. Even if we can be sure that the timezone // is handled correctly it could be that the user created the wallet in one timezone and make a restore at // a different timezone which could lead in the worst case that he miss the first day of the wallet transactions. LocalDateTime localDateTime = walletDate.atStartOfDay().minusDays(1); long date = localDateTime.toEpochSecond(ZoneOffset.UTC); DeterministicSeed seed = new DeterministicSeed(Splitter.on(" ").splitToList(seedWordsTextArea.getText()), null, "", date); SharedPresentation.restoreSeedWords(walletsManager, openOfferManager, seed, storageDir); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsDataModel.java ================================================ /* * 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 . */ package haveno.desktop.main.account.content.traditionalaccounts; import com.google.inject.Inject; import haveno.common.file.CorruptedStorageFileHandler; import haveno.common.proto.persistable.PersistenceProtoResolver; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.AssetAccount; import haveno.core.payment.PaymentAccount; import haveno.core.trade.TradeManager; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.desktop.common.model.ActivatableDataModel; import haveno.desktop.util.GUIUtil; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.stage.Stage; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; class TraditionalAccountsDataModel extends ActivatableDataModel { private final User user; private final Preferences preferences; private final OpenOfferManager openOfferManager; private final TradeManager tradeManager; private final AccountAgeWitnessService accountAgeWitnessService; final ObservableList paymentAccounts = FXCollections.observableArrayList(); private final ListChangeListener listChangeListener; private final String accountsFileName = "FiatPaymentAccounts"; private final PersistenceProtoResolver persistenceProtoResolver; private final CorruptedStorageFileHandler corruptedStorageFileHandler; @Inject public TraditionalAccountsDataModel(User user, Preferences preferences, OpenOfferManager openOfferManager, TradeManager tradeManager, AccountAgeWitnessService accountAgeWitnessService, PersistenceProtoResolver persistenceProtoResolver, CorruptedStorageFileHandler corruptedStorageFileHandler) { this.user = user; this.preferences = preferences; this.openOfferManager = openOfferManager; this.tradeManager = tradeManager; this.accountAgeWitnessService = accountAgeWitnessService; this.persistenceProtoResolver = persistenceProtoResolver; this.corruptedStorageFileHandler = corruptedStorageFileHandler; listChangeListener = change -> fillAndSortPaymentAccounts(); } @Override protected void activate() { user.getPaymentAccountsAsObservable().addListener(listChangeListener); fillAndSortPaymentAccounts(); } private void fillAndSortPaymentAccounts() { if (user.getPaymentAccounts() != null) { List list = user.getPaymentAccounts().stream() .filter(paymentAccount -> !paymentAccount.getPaymentMethod().isBlockchain()) .collect(Collectors.toList()); paymentAccounts.setAll(list); paymentAccounts.sort(Comparator.comparing(PaymentAccount::getAccountName)); } } @Override protected void deactivate() { user.getPaymentAccountsAsObservable().removeListener(listChangeListener); } /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// public void onSaveNewAccount(PaymentAccount paymentAccount) { TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency(); List tradeCurrencies = paymentAccount.getTradeCurrencies(); if (singleTradeCurrency != null) { paymentAccount.setSelectedTradeCurrency(singleTradeCurrency); if (singleTradeCurrency instanceof TraditionalCurrency) preferences.addTraditionalCurrency((TraditionalCurrency) singleTradeCurrency); else preferences.addCryptoCurrency((CryptoCurrency) singleTradeCurrency); } else if (tradeCurrencies != null && !tradeCurrencies.isEmpty()) { if (tradeCurrencies.contains(CurrencyUtil.getDefaultTradeCurrency())) paymentAccount.setSelectedTradeCurrency(CurrencyUtil.getDefaultTradeCurrency()); else paymentAccount.setSelectedTradeCurrency(tradeCurrencies.get(0)); tradeCurrencies.forEach(tradeCurrency -> { if (tradeCurrency instanceof TraditionalCurrency) preferences.addTraditionalCurrency((TraditionalCurrency) tradeCurrency); else preferences.addCryptoCurrency((CryptoCurrency) tradeCurrency); }); } user.addPaymentAccount(paymentAccount); paymentAccount.onPersistChanges(); accountAgeWitnessService.publishMyAccountAgeWitness(paymentAccount.getPaymentAccountPayload()); accountAgeWitnessService.signAndPublishSameNameAccounts(); } public void onUpdateAccount(PaymentAccount paymentAccount) { paymentAccount.onPersistChanges(); user.requestPersistence(); } public boolean onDeleteAccount(PaymentAccount paymentAccount) { boolean isPaymentAccountUsed = openOfferManager.getObservableList().stream() .anyMatch(o -> o.getOffer().getMakerPaymentAccountId().equals(paymentAccount.getId())); isPaymentAccountUsed = isPaymentAccountUsed || tradeManager.getObservableList().stream() .anyMatch(t -> t.getOffer().getMakerPaymentAccountId().equals(paymentAccount.getId()) || paymentAccount.getId().equals(t.getTaker().getPaymentAccountId())); if (!isPaymentAccountUsed) user.removePaymentAccount(paymentAccount); return isPaymentAccountUsed; } public void onSelectAccount(PaymentAccount paymentAccount) { user.setCurrentPaymentAccount(paymentAccount); } public void exportAccounts(Stage stage) { if (user.getPaymentAccounts() != null) { ArrayList accounts = new ArrayList<>(user.getPaymentAccounts().stream() .filter(paymentAccount -> !(paymentAccount instanceof AssetAccount)) .collect(Collectors.toList())); GUIUtil.exportAccounts(accounts, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler); } } public void importAccounts(Stage stage) { GUIUtil.importAccounts(user, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler); } public int getNumPaymentAccounts() { return user.getPaymentAccounts() != null ? user.getPaymentAccounts().size() : 0; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java ================================================ /* * 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 . */ package haveno.desktop.main.account.content.traditionalaccounts; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.common.util.Tuple2; import haveno.common.util.Tuple3; import haveno.common.util.Utilities; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.offer.OfferRestrictions; import haveno.core.payment.AmazonGiftCardAccount; import haveno.core.payment.AustraliaPayidAccount; import haveno.core.payment.CashAppAccount; import haveno.core.payment.CashAtAtmAccount; import haveno.core.payment.CashDepositAccount; import haveno.core.payment.F2FAccount; import haveno.core.payment.HalCashAccount; import haveno.core.payment.MoneyGramAccount; import haveno.core.payment.PayByMailAccount; import haveno.core.payment.PayPalAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccountFactory; import haveno.core.payment.PaysafeAccount; import haveno.core.payment.RevolutAccount; import haveno.core.payment.USPostalMoneyOrderAccount; import haveno.core.payment.VenmoAccount; import haveno.core.payment.WesternUnionAccount; import haveno.core.payment.ZelleAccount; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.validation.AdvancedCashValidator; import haveno.core.payment.validation.AliPayValidator; import haveno.core.payment.validation.AustraliaPayidValidator; import haveno.core.payment.validation.BICValidator; import haveno.core.payment.validation.CapitualValidator; import haveno.core.payment.validation.ChaseQuickPayValidator; import haveno.core.payment.validation.EmailOrMobileNrValidator; import haveno.core.payment.validation.EmailOrMobileNrOrCashtagValidator; import haveno.core.payment.validation.EmailOrMobileNrOrUsernameValidator; import haveno.core.payment.validation.F2FValidator; import haveno.core.payment.validation.HalCashValidator; import haveno.core.payment.validation.InteracETransferValidator; import haveno.core.payment.validation.JapanBankTransferValidator; import haveno.core.payment.validation.LengthValidator; import haveno.core.payment.validation.MoneyBeamValidator; import haveno.core.payment.validation.PerfectMoneyValidator; import haveno.core.payment.validation.PopmoneyValidator; import haveno.core.payment.validation.PromptPayValidator; import haveno.core.payment.validation.RevolutValidator; import haveno.core.payment.validation.SwishValidator; import haveno.core.payment.validation.TransferwiseValidator; import haveno.core.payment.validation.USPostalMoneyOrderValidator; import haveno.core.payment.validation.UpholdValidator; import haveno.core.payment.validation.WeChatPayValidator; import haveno.core.trade.HavenoUtils; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.AutocompleteComboBox; import haveno.desktop.components.TitledGroupBg; import haveno.desktop.components.paymentmethods.AchTransferForm; import haveno.desktop.components.paymentmethods.AdvancedCashForm; import haveno.desktop.components.paymentmethods.AliPayForm; import haveno.desktop.components.paymentmethods.AmazonGiftCardForm; import haveno.desktop.components.paymentmethods.AustraliaPayidForm; import haveno.desktop.components.paymentmethods.BizumForm; import haveno.desktop.components.paymentmethods.CapitualForm; import haveno.desktop.components.paymentmethods.CashAppForm; import haveno.desktop.components.paymentmethods.CashAtAtmForm; import haveno.desktop.components.paymentmethods.CashDepositForm; import haveno.desktop.components.paymentmethods.CelPayForm; import haveno.desktop.components.paymentmethods.ChaseQuickPayForm; import haveno.desktop.components.paymentmethods.DomesticWireTransferForm; import haveno.desktop.components.paymentmethods.F2FForm; import haveno.desktop.components.paymentmethods.FasterPaymentsForm; import haveno.desktop.components.paymentmethods.HalCashForm; import haveno.desktop.components.paymentmethods.ImpsForm; import haveno.desktop.components.paymentmethods.InteracETransferForm; import haveno.desktop.components.paymentmethods.JapanBankTransferForm; import haveno.desktop.components.paymentmethods.MoneseForm; import haveno.desktop.components.paymentmethods.MoneyBeamForm; import haveno.desktop.components.paymentmethods.MoneyGramForm; import haveno.desktop.components.paymentmethods.NationalBankForm; import haveno.desktop.components.paymentmethods.NeftForm; import haveno.desktop.components.paymentmethods.NequiForm; import haveno.desktop.components.paymentmethods.PaxumForm; import haveno.desktop.components.paymentmethods.PayByMailForm; import haveno.desktop.components.paymentmethods.PayPalForm; import haveno.desktop.components.paymentmethods.PaymentMethodForm; import haveno.desktop.components.paymentmethods.PaysafeForm; import haveno.desktop.components.paymentmethods.PayseraForm; import haveno.desktop.components.paymentmethods.PaytmForm; import haveno.desktop.components.paymentmethods.PerfectMoneyForm; import haveno.desktop.components.paymentmethods.PixForm; import haveno.desktop.components.paymentmethods.PopmoneyForm; import haveno.desktop.components.paymentmethods.PromptPayForm; import haveno.desktop.components.paymentmethods.RevolutForm; import haveno.desktop.components.paymentmethods.RtgsForm; import haveno.desktop.components.paymentmethods.SameBankForm; import haveno.desktop.components.paymentmethods.SatispayForm; import haveno.desktop.components.paymentmethods.SepaForm; import haveno.desktop.components.paymentmethods.SepaInstantForm; import haveno.desktop.components.paymentmethods.SpecificBankForm; import haveno.desktop.components.paymentmethods.StrikeForm; import haveno.desktop.components.paymentmethods.SwiftForm; import haveno.desktop.components.paymentmethods.SwishForm; import haveno.desktop.components.paymentmethods.TikkieForm; import haveno.desktop.components.paymentmethods.TransferwiseForm; import haveno.desktop.components.paymentmethods.TransferwiseUsdForm; import haveno.desktop.components.paymentmethods.USPostalMoneyOrderForm; import haveno.desktop.components.paymentmethods.UpholdForm; import haveno.desktop.components.paymentmethods.UpiForm; import haveno.desktop.components.paymentmethods.VenmoForm; import haveno.desktop.components.paymentmethods.VerseForm; import haveno.desktop.components.paymentmethods.WeChatPayForm; import haveno.desktop.components.paymentmethods.WesternUnionForm; import haveno.desktop.components.paymentmethods.ZelleForm; import haveno.desktop.main.account.content.PaymentAccountsView; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.FormBuilder; import static haveno.desktop.util.FormBuilder.add2ButtonsAfterGroup; import static haveno.desktop.util.FormBuilder.add3ButtonsAfterGroup; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import static haveno.desktop.util.FormBuilder.addLabel; import static haveno.desktop.util.FormBuilder.addTopLabelListView; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.Layout; import java.math.BigInteger; import java.util.List; import java.util.stream.Collectors; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ListView; import javafx.scene.layout.GridPane; import javafx.scene.layout.VBox; import javafx.stage.Stage; import javafx.util.StringConverter; @FxmlView public class TraditionalAccountsView extends PaymentAccountsView { private final BICValidator bicValidator; private final CapitualValidator capitualValidator; private final LengthValidator inputValidator; private final UpholdValidator upholdValidator; private final MoneyBeamValidator moneyBeamValidator; private final PopmoneyValidator popmoneyValidator; private final RevolutValidator revolutValidator; private final AliPayValidator aliPayValidator; private final PerfectMoneyValidator perfectMoneyValidator; private final SwishValidator swishValidator; private final EmailOrMobileNrValidator zelleValidator; private final EmailOrMobileNrOrUsernameValidator paypalValidator; private final EmailOrMobileNrOrUsernameValidator venmoValidator; private final EmailOrMobileNrOrCashtagValidator cashAppValidator; private final ChaseQuickPayValidator chaseQuickPayValidator; private final InteracETransferValidator interacETransferValidator; private final JapanBankTransferValidator japanBankTransferValidator; private final AustraliaPayidValidator australiapayidValidator; private final USPostalMoneyOrderValidator usPostalMoneyOrderValidator; private final WeChatPayValidator weChatPayValidator; private final HalCashValidator halCashValidator; private final F2FValidator f2FValidator; private final PromptPayValidator promptPayValidator; private final AdvancedCashValidator advancedCashValidator; private final TransferwiseValidator transferwiseValidator; private final CoinFormatter formatter; private AutocompleteComboBox paymentMethodComboBox; private PaymentMethodForm paymentMethodForm; private TitledGroupBg accountTitledGroupBg; private Button saveNewAccountButton; private int gridRow = 0; @Inject public TraditionalAccountsView(TraditionalAccountsViewModel model, BICValidator bicValidator, CapitualValidator capitualValidator, LengthValidator inputValidator, UpholdValidator upholdValidator, MoneyBeamValidator moneyBeamValidator, PopmoneyValidator popmoneyValidator, RevolutValidator revolutValidator, AliPayValidator aliPayValidator, PerfectMoneyValidator perfectMoneyValidator, SwishValidator swishValidator, EmailOrMobileNrValidator zelleValidator, EmailOrMobileNrOrCashtagValidator cashAppValidator, EmailOrMobileNrOrUsernameValidator emailMobileUsernameValidator, ChaseQuickPayValidator chaseQuickPayValidator, InteracETransferValidator interacETransferValidator, JapanBankTransferValidator japanBankTransferValidator, AustraliaPayidValidator australiaPayIDValidator, USPostalMoneyOrderValidator usPostalMoneyOrderValidator, WeChatPayValidator weChatPayValidator, HalCashValidator halCashValidator, F2FValidator f2FValidator, PromptPayValidator promptPayValidator, AdvancedCashValidator advancedCashValidator, TransferwiseValidator transferwiseValidator, AccountAgeWitnessService accountAgeWitnessService, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter) { super(model, accountAgeWitnessService); this.bicValidator = bicValidator; this.capitualValidator = capitualValidator; this.inputValidator = inputValidator; this.inputValidator.setMaxLength(100); // restrict general field entry length this.inputValidator.setMinLength(2); this.upholdValidator = upholdValidator; this.moneyBeamValidator = moneyBeamValidator; this.popmoneyValidator = popmoneyValidator; this.revolutValidator = revolutValidator; this.aliPayValidator = aliPayValidator; this.perfectMoneyValidator = perfectMoneyValidator; this.swishValidator = swishValidator; this.zelleValidator = zelleValidator; this.paypalValidator = emailMobileUsernameValidator; this.venmoValidator = emailMobileUsernameValidator; this.cashAppValidator = cashAppValidator; this.chaseQuickPayValidator = chaseQuickPayValidator; this.interacETransferValidator = interacETransferValidator; this.japanBankTransferValidator = japanBankTransferValidator; this.australiapayidValidator = australiaPayIDValidator; this.usPostalMoneyOrderValidator = usPostalMoneyOrderValidator; this.weChatPayValidator = weChatPayValidator; this.halCashValidator = halCashValidator; this.f2FValidator = f2FValidator; this.promptPayValidator = promptPayValidator; this.advancedCashValidator = advancedCashValidator; this.transferwiseValidator = transferwiseValidator; this.formatter = formatter; } @Override protected ObservableList getPaymentAccounts() { return model.getPaymentAccounts(); } @Override protected void importAccounts() { model.dataModel.importAccounts((Stage) root.getScene().getWindow()); } @Override protected void exportAccounts() { model.dataModel.exportAccounts((Stage) root.getScene().getWindow()); } /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// private void onSaveNewAccount(PaymentAccount paymentAccount) { BigInteger maxTradeLimit = paymentAccount.getPaymentMethod().getMaxTradeLimit("USD"); BigInteger maxTradeLimitSecondMonth = maxTradeLimit.divide(BigInteger.valueOf(2L)); BigInteger maxTradeLimitFirstMonth = maxTradeLimit.divide(BigInteger.valueOf(4L)); if (paymentAccount instanceof F2FAccount) { new Popup().information(Res.get("payment.f2f.info")) .width(700) .closeButtonText(Res.get("payment.f2f.info.openURL")) .onClose(() -> GUIUtil.openWebPage("https://docs.haveno.exchange/overview/payment_methods/F2F")) .actionButtonText(Res.get("shared.iUnderstand")) .onAction(() -> doSaveNewAccount(paymentAccount)) .show(); } else if (paymentAccount instanceof PayByMailAccount) { // PayByMail has no chargeback risk so we don't show the text from payment.limits.info. new Popup().information(Res.get("payment.payByMail.info")) .width(850) .closeButtonText(Res.get("shared.cancel")) .actionButtonText(Res.get("shared.iUnderstand")) .onAction(() -> doSaveNewAccount(paymentAccount)) .showScrollPane() .show(); } else if (paymentAccount instanceof CashAtAtmAccount) { // CashAtAtm has no chargeback risk so we don't show the text from payment.limits.info. new Popup().information(Res.get("payment.cashAtAtm.info")) .width(850) .closeButtonText(Res.get("shared.cancel")) .actionButtonText(Res.get("shared.iUnderstand")) .onAction(() -> doSaveNewAccount(paymentAccount)) .show(); } else if (paymentAccount instanceof HalCashAccount) { // HalCash has no chargeback risk so we don't show the text from payment.limits.info. new Popup().information(Res.get("payment.halCash.info")) .width(700) .closeButtonText(Res.get("shared.cancel")) .actionButtonText(Res.get("shared.iUnderstand")) .onAction(() -> doSaveNewAccount(paymentAccount)) .show(); } else { String limitsInfoKey = "payment.limits.info"; String initialLimit = HavenoUtils.formatXmr(maxTradeLimitFirstMonth, true); if (PaymentMethod.hasChargebackRisk(paymentAccount.getPaymentMethod(), paymentAccount.getTradeCurrencies())) { limitsInfoKey = "payment.limits.info.withSigning"; initialLimit = HavenoUtils.formatXmr(OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT, true); } new Popup().information(Res.get(limitsInfoKey, initialLimit, HavenoUtils.formatXmr(maxTradeLimitSecondMonth, true), HavenoUtils.formatXmr(maxTradeLimit, true))) .width(700) .closeButtonText(Res.get("shared.cancel")) .actionButtonText(Res.get("shared.iUnderstand")) .onAction(() -> { final String currencyName = Config.baseCurrencyNetwork().getCurrencyName(); if (paymentAccount instanceof ZelleAccount) { new Popup().information(Res.get("payment.zelle.info", currencyName, currencyName)) .width(900) .closeButtonText(Res.get("shared.cancel")) .actionButtonText(Res.get("shared.iConfirm")) .onAction(() -> doSaveNewAccount(paymentAccount)) .show(); } else if (paymentAccount instanceof WesternUnionAccount) { new Popup().information(Res.get("payment.westernUnion.info")) .width(700) .closeButtonText(Res.get("shared.cancel")) .actionButtonText(Res.get("shared.iUnderstand")) .onAction(() -> doSaveNewAccount(paymentAccount)) .show(); } else if (paymentAccount instanceof MoneyGramAccount) { new Popup().information(Res.get("payment.moneyGram.info")) .width(700) .closeButtonText(Res.get("shared.cancel")) .actionButtonText(Res.get("shared.iUnderstand")) .onAction(() -> doSaveNewAccount(paymentAccount)) .show(); } else if (paymentAccount instanceof CashDepositAccount) { new Popup().information(Res.get("payment.cashDeposit.info")) .width(700) .closeButtonText(Res.get("shared.cancel")) .actionButtonText(Res.get("shared.iConfirm")) .onAction(() -> doSaveNewAccount(paymentAccount)) .show(); } else if (paymentAccount instanceof RevolutAccount) { new Popup().information(Res.get("payment.revolut.info")) .width(700) .closeButtonText(Res.get("shared.cancel")) .actionButtonText(Res.get("shared.iConfirm")) .onAction(() -> doSaveNewAccount(paymentAccount)) .show(); } else if (paymentAccount instanceof USPostalMoneyOrderAccount) { new Popup().information(Res.get("payment.usPostalMoneyOrder.info")) .width(700) .closeButtonText(Res.get("shared.cancel")) .actionButtonText(Res.get("shared.iUnderstand")) .onAction(() -> doSaveNewAccount(paymentAccount)) .show(); } else if (paymentAccount instanceof AustraliaPayidAccount) { new Popup().information(Res.get("payment.payid.info", currencyName, currencyName)) .width(900) .closeButtonText(Res.get("shared.cancel")) .actionButtonText(Res.get("shared.iConfirm")) .onAction(() -> doSaveNewAccount(paymentAccount)) .show(); } else if (paymentAccount instanceof AmazonGiftCardAccount) { new Popup().information(Res.get("payment.amazonGiftCard.info", currencyName, currencyName)) .width(900) .closeButtonText(Res.get("shared.cancel")) .actionButtonText(Res.get("shared.iUnderstand")) .onAction(() -> doSaveNewAccount(paymentAccount)) .show(); } else if (paymentAccount instanceof CashAppAccount) { new Popup().warning(Res.get("payment.cashapp.info")) .width(700) .closeButtonText(Res.get("shared.cancel")) .actionButtonText(Res.get("shared.iUnderstand")) .onAction(() -> doSaveNewAccount(paymentAccount)) .show(); } else if (paymentAccount instanceof VenmoAccount) { new Popup().warning(Res.get("payment.venmo.info")) .width(700) .closeButtonText(Res.get("shared.cancel")) .actionButtonText(Res.get("shared.iUnderstand")) .onAction(() -> doSaveNewAccount(paymentAccount)) .show(); } else if (paymentAccount instanceof PayPalAccount) { new Popup().warning(Res.get("payment.paypal.info")) .width(700) .closeButtonText(Res.get("shared.cancel")) .actionButtonText(Res.get("shared.iUnderstand")) .onAction(() -> doSaveNewAccount(paymentAccount)) .show(); } else if (paymentAccount instanceof PaysafeAccount) { new Popup().warning(Res.get("payment.paysafe.info")) .width(700) .closeButtonText(Res.get("shared.cancel")) .actionButtonText(Res.get("shared.iUnderstand")) .onAction(() -> doSaveNewAccount(paymentAccount)) .show(); } else { doSaveNewAccount(paymentAccount); } }) .show(); } } private void doSaveNewAccount(PaymentAccount paymentAccount) { if (getPaymentAccounts().stream().noneMatch(e -> e.getAccountName() != null && e.getAccountName().equals(paymentAccount.getAccountName()))) { model.onSaveNewAccount(paymentAccount); removeNewAccountForm(); } else { new Popup().warning(Res.get("shared.accountNameAlreadyUsed")).show(); } } private void onCancelNewAccount() { removeNewAccountForm(); } private void onUpdateAccount(PaymentAccount paymentAccount) { model.onUpdateAccount(paymentAccount); removeSelectAccountForm(); } private void onCancelSelectedAccount(PaymentAccount paymentAccount) { paymentAccount.revertChanges(); removeSelectAccountForm(); } protected boolean deleteAccountFromModel(PaymentAccount paymentAccount) { return model.onDeleteAccount(paymentAccount); } /////////////////////////////////////////////////////////////////////////////////////////// // Base form /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void buildForm() { addTitledGroupBg(root, gridRow, 2, Res.get("shared.manageAccounts")); Tuple3, VBox> tuple = addTopLabelListView(root, gridRow, Res.get("account.traditional.yourTraditionalAccounts"), Layout.FIRST_ROW_DISTANCE); paymentAccountsListView = tuple.second; setPaymentAccountsListHeight(); setPaymentAccountsCellFactory(); Tuple3 tuple3 = add3ButtonsAfterGroup(root, ++gridRow, Res.get("shared.addNewAccount"), Res.get("shared.ExportAccounts"), Res.get("shared.importAccounts")); addAccountButton = tuple3.first; addAccountButton.setId("buy-button-big"); exportButton = tuple3.second; importButton = tuple3.third; } // Add new account form @Override protected void addNewAccount() { paymentAccountsListView.getSelectionModel().clearSelection(); removeAccountRows(); addAccountButton.setDisable(true); accountTitledGroupBg = addTitledGroupBg(root, ++gridRow, 2, Res.get("shared.createNewAccount"), Layout.COMPACT_GROUP_DISTANCE); addLabel(root, gridRow, Res.get("shared.createNewAccountDescription")); paymentMethodComboBox = FormBuilder.addAutocompleteComboBox( root, gridRow, Res.get("shared.selectPaymentMethod"), Layout.TWICE_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE + Layout.PADDING ); paymentMethodComboBox.setVisibleRowCount(Math.min(paymentMethodComboBox.getItems().size(), 10)); paymentMethodComboBox.setPrefWidth(250); List list = PaymentMethod.paymentMethods.stream() .filter(PaymentMethod::isTraditional) .sorted() .collect(Collectors.toList()); paymentMethodComboBox.setAutocompleteItems(FXCollections.observableArrayList(list)); paymentMethodComboBox.setConverter(new StringConverter<>() { @Override public String toString(PaymentMethod paymentMethod) { return paymentMethod != null ? Res.get(paymentMethod.getId()) : ""; } @Override public PaymentMethod fromString(String s) { if (s.isEmpty()) return null; return paymentMethodComboBox.getItems().stream() .filter(item -> Res.get(item.getId()).equals(s)) .findAny().orElse(null); } }); paymentMethodComboBox.setOnChangeConfirmed(e -> { if (paymentMethodComboBox.getEditor().getText().isEmpty()) return; if (paymentMethodForm != null) { FormBuilder.removeRowsFromGridPane(root, 3, paymentMethodForm.getGridRow() + 1); GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan() + 1); } gridRow = 2; paymentMethodForm = getPaymentMethodForm(paymentMethodComboBox.getSelectionModel().getSelectedItem()); if (paymentMethodForm != null) { if (paymentMethodForm.getPaymentAccount().getMessageForAccountCreation() != null) { new Popup().information(Res.get(paymentMethodForm.getPaymentAccount().getMessageForAccountCreation())) .width(900) .closeButtonText(Res.get("shared.iUnderstand")) .show(); } paymentMethodForm.addFormForAddAccount(); gridRow = paymentMethodForm.getGridRow(); Tuple2 tuple2 = add2ButtonsAfterGroup(root, ++gridRow, Res.get("shared.saveNewAccount"), Res.get("shared.cancel")); saveNewAccountButton = tuple2.first; saveNewAccountButton.setOnAction(event -> onSaveNewAccount(paymentMethodForm.getPaymentAccount())); saveNewAccountButton.disableProperty().bind(paymentMethodForm.allInputsValidProperty().not()); Button cancelButton = tuple2.second; cancelButton.setOnAction(event -> onCancelNewAccount()); GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan() + 1); } }); } // Select account form @Override protected void onSelectAccount(PaymentAccount previous, PaymentAccount current) { if (previous != null) { previous.revertChanges(); } removeAccountRows(); addAccountButton.setDisable(false); accountTitledGroupBg = addTitledGroupBg(root, ++gridRow, 2, "", Layout.COMPACT_GROUP_DISTANCE); paymentMethodForm = getPaymentMethodForm(current); if (paymentMethodForm != null) { paymentMethodForm.addFormForEditAccount(); gridRow = paymentMethodForm.getGridRow(); Tuple3 tuple = add3ButtonsAfterGroup( root, ++gridRow, Res.get("shared.save"), Res.get("shared.deleteAccount"), Res.get("shared.cancel") ); Button updateButton = tuple.first; updateButton.setOnAction(event -> onUpdateAccount(paymentMethodForm.getPaymentAccount())); Button deleteAccountButton = tuple.second; deleteAccountButton.setOnAction(event -> onDeleteAccount(paymentMethodForm.getPaymentAccount())); Button cancelButton = tuple.third; cancelButton.setOnAction(event -> onCancelSelectedAccount(paymentMethodForm.getPaymentAccount())); GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan()); model.onSelectAccount(current); } } /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// private PaymentMethodForm getPaymentMethodForm(PaymentAccount paymentAccount) { return getPaymentMethodForm(paymentAccount.getPaymentMethod(), paymentAccount); } private PaymentMethodForm getPaymentMethodForm(PaymentMethod paymentMethod) { if (paymentMethod == null) return null; final PaymentAccount paymentAccount = PaymentAccountFactory.getPaymentAccount(paymentMethod); paymentAccount.init(); return getPaymentMethodForm(paymentMethod, paymentAccount); } private PaymentMethodForm getPaymentMethodForm(PaymentMethod paymentMethod, PaymentAccount paymentAccount) { switch (paymentMethod.getId()) { case PaymentMethod.UPHOLD_ID: return new UpholdForm(paymentAccount, accountAgeWitnessService, upholdValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.MONEY_BEAM_ID: return new MoneyBeamForm(paymentAccount, accountAgeWitnessService, moneyBeamValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.POPMONEY_ID: return new PopmoneyForm(paymentAccount, accountAgeWitnessService, popmoneyValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.REVOLUT_ID: return new RevolutForm(paymentAccount, accountAgeWitnessService, revolutValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.PERFECT_MONEY_ID: return new PerfectMoneyForm(paymentAccount, accountAgeWitnessService, perfectMoneyValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.SEPA_ID: return new SepaForm(paymentAccount, accountAgeWitnessService, bicValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.SEPA_INSTANT_ID: return new SepaInstantForm(paymentAccount, accountAgeWitnessService, bicValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.FASTER_PAYMENTS_ID: return new FasterPaymentsForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.NATIONAL_BANK_ID: return new NationalBankForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.SAME_BANK_ID: return new SameBankForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.SPECIFIC_BANKS_ID: return new SpecificBankForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.JAPAN_BANK_ID: return new JapanBankTransferForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.AUSTRALIA_PAYID_ID: return new AustraliaPayidForm(paymentAccount, accountAgeWitnessService, australiapayidValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.ALI_PAY_ID: return new AliPayForm(paymentAccount, accountAgeWitnessService, aliPayValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.WECHAT_PAY_ID: return new WeChatPayForm(paymentAccount, accountAgeWitnessService, weChatPayValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.SWISH_ID: return new SwishForm(paymentAccount, accountAgeWitnessService, swishValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.ZELLE_ID: return new ZelleForm(paymentAccount, accountAgeWitnessService, zelleValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.CHASE_QUICK_PAY_ID: return new ChaseQuickPayForm(paymentAccount, accountAgeWitnessService, chaseQuickPayValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.INTERAC_E_TRANSFER_ID: return new InteracETransferForm(paymentAccount, accountAgeWitnessService, interacETransferValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.US_POSTAL_MONEY_ORDER_ID: return new USPostalMoneyOrderForm(paymentAccount, accountAgeWitnessService, usPostalMoneyOrderValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.MONEY_GRAM_ID: return new MoneyGramForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.WESTERN_UNION_ID: return new WesternUnionForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.CASH_DEPOSIT_ID: return new CashDepositForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.PAY_BY_MAIL_ID: return new PayByMailForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.CASH_AT_ATM_ID: return new CashAtAtmForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.HAL_CASH_ID: return new HalCashForm(paymentAccount, accountAgeWitnessService, halCashValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.F2F_ID: return new F2FForm(paymentAccount, accountAgeWitnessService, f2FValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.PROMPT_PAY_ID: return new PromptPayForm(paymentAccount, accountAgeWitnessService, promptPayValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.ADVANCED_CASH_ID: return new AdvancedCashForm(paymentAccount, accountAgeWitnessService, advancedCashValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.TRANSFERWISE_ID: return new TransferwiseForm(paymentAccount, accountAgeWitnessService, transferwiseValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.TRANSFERWISE_USD_ID: return new TransferwiseUsdForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.PAYSERA_ID: return new PayseraForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.PAXUM_ID: return new PaxumForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.NEFT_ID: return new NeftForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.RTGS_ID: return new RtgsForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.IMPS_ID: return new ImpsForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.UPI_ID: return new UpiForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.PAYTM_ID: return new PaytmForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.NEQUI_ID: return new NequiForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.BIZUM_ID: return new BizumForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.PIX_ID: return new PixForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.AMAZON_GIFT_CARD_ID: return new AmazonGiftCardForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.CAPITUAL_ID: return new CapitualForm(paymentAccount, accountAgeWitnessService, capitualValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.CELPAY_ID: return new CelPayForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.MONESE_ID: return new MoneseForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.SATISPAY_ID: return new SatispayForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.TIKKIE_ID: return new TikkieForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.VERSE_ID: return new VerseForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.STRIKE_ID: return new StrikeForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.SWIFT_ID: return new SwiftForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.ACH_TRANSFER_ID: return new AchTransferForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.DOMESTIC_WIRE_TRANSFER_ID: return new DomesticWireTransferForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); case PaymentMethod.PAYPAL_ID: return new PayPalForm(paymentAccount, accountAgeWitnessService, paypalValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.VENMO_ID: return new VenmoForm(paymentAccount, accountAgeWitnessService, venmoValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.CASH_APP_ID: return new CashAppForm(paymentAccount, accountAgeWitnessService, cashAppValidator, inputValidator, root, gridRow, formatter); case PaymentMethod.PAYSAFE_ID: return new PaysafeForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter); default: log.error("Not supported PaymentMethod: " + paymentMethod); return null; } } private void removeNewAccountForm() { saveNewAccountButton.disableProperty().unbind(); removeAccountRows(); addAccountButton.setDisable(false); } @Override protected void removeSelectAccountForm() { FormBuilder.removeRowsFromGridPane(root, 2, gridRow); gridRow = 1; addAccountButton.setDisable(false); paymentAccountsListView.getSelectionModel().clearSelection(); } private void removeAccountRows() { FormBuilder.removeRowsFromGridPane(root, 2, gridRow); gridRow = 1; } @Override protected void copyAccount() { var selectedAccount = paymentAccountsListView.getSelectionModel().getSelectedItem(); if (selectedAccount == null) { return; } Utilities.copyToClipboard(accountAgeWitnessService.getSignInfoFromAccount(selectedAccount)); } @Override protected void deactivate() { super.deactivate(); var selectedAccount = paymentAccountsListView.getSelectionModel().getSelectedItem(); if (selectedAccount != null) { onCancelSelectedAccount(selectedAccount); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.account.content.traditionalaccounts; import com.google.inject.Inject; import haveno.core.payment.PaymentAccount; import haveno.desktop.common.model.ActivatableWithDataModel; import haveno.desktop.common.model.ViewModel; import javafx.collections.ObservableList; class TraditionalAccountsViewModel extends ActivatableWithDataModel implements ViewModel { @Inject public TraditionalAccountsViewModel(TraditionalAccountsDataModel dataModel) { super(dataModel); } @Override protected void activate() { } @Override protected void deactivate() { } /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// public void onSaveNewAccount(PaymentAccount paymentAccount) { dataModel.onSaveNewAccount(paymentAccount); } public void onUpdateAccount(PaymentAccount paymentAccount) { dataModel.onUpdateAccount(paymentAccount); } public boolean onDeleteAccount(PaymentAccount paymentAccount) { return dataModel.onDeleteAccount(paymentAccount); } public void onSelectAccount(PaymentAccount paymentAccount) { dataModel.onSelectAccount(paymentAccount); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// ObservableList getPaymentAccounts() { return dataModel.paymentAccounts; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/content/walletinfo/WalletInfoView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/content/walletinfo/WalletInfoView.java ================================================ /* * 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 . */ package haveno.desktop.main.account.content.walletinfo; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.core.locale.Res; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.core.xmr.listeners.BalanceListener; import haveno.core.xmr.wallet.BtcWalletService; import haveno.core.xmr.wallet.WalletService; import haveno.core.xmr.wallet.WalletsManager; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.common.view.FxmlView; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.ShowWalletDataWindow; import static haveno.desktop.util.FormBuilder.addButtonAfterGroup; import static haveno.desktop.util.FormBuilder.addMultilineLabel; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; import haveno.desktop.util.Layout; import javafx.scene.control.Button; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; import org.bitcoinj.script.Script; import org.bitcoinj.wallet.DeterministicKeyChain; import static org.bitcoinj.wallet.Wallet.BalanceType.ESTIMATED_SPENDABLE; @FxmlView public class WalletInfoView extends ActivatableView { private final WalletsManager walletsManager; private final BtcWalletService btcWalletService; private final CoinFormatter btcFormatter; private int gridRow = 0; private Button openDetailsButton; private TextField btcTextField; private BalanceListener btcWalletBalanceListener; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject private WalletInfoView(WalletsManager walletsManager, BtcWalletService btcWalletService, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter) { this.walletsManager = walletsManager; this.btcWalletService = btcWalletService; this.btcFormatter = btcFormatter; } @Override public void initialize() { addTitledGroupBg(root, gridRow, 3, Res.get("account.menu.walletInfo.balance.headLine")); addMultilineLabel(root, gridRow, Res.get("account.menu.walletInfo.balance.info"), Layout.FIRST_ROW_DISTANCE, Double.MAX_VALUE); btcTextField = addTopLabelTextField(root, ++gridRow, "BTC", -Layout.FLOATING_LABEL_DISTANCE).second; addTitledGroupBg(root, ++gridRow, 3, Res.get("account.menu.walletInfo.xpub.headLine"), Layout.GROUP_DISTANCE); addXpubKeys(btcWalletService, "BTC", gridRow, Layout.FIRST_ROW_AND_GROUP_DISTANCE); ++gridRow; // update gridRow addTitledGroupBg(root, ++gridRow, 4, Res.get("account.menu.walletInfo.path.headLine"), Layout.GROUP_DISTANCE); addMultilineLabel(root, gridRow, Res.get("account.menu.walletInfo.path.info"), Layout.FIRST_ROW_AND_GROUP_DISTANCE, Double.MAX_VALUE); addTopLabelTextField(root, ++gridRow, Res.get("account.menu.walletInfo.walletSelector", "BTC", "legacy"), "44'/0'/0'", -Layout.FLOATING_LABEL_DISTANCE); addTopLabelTextField(root, ++gridRow, Res.get("account.menu.walletInfo.walletSelector", "BTC", "segwit"), "44'/0'/1'", -Layout.FLOATING_LABEL_DISTANCE); openDetailsButton = addButtonAfterGroup(root, ++gridRow, Res.get("account.menu.walletInfo.openDetails")); btcWalletBalanceListener = new BalanceListener() { @Override public void onBalanceChanged(Coin balanceAsCoin, Transaction tx) { updateBalances(btcWalletService); } }; } @Override protected void activate() { btcWalletService.addBalanceListener(btcWalletBalanceListener); updateBalances(btcWalletService); openDetailsButton.setOnAction(e -> { if (walletsManager.areWalletsAvailable()) { new ShowWalletDataWindow(walletsManager).width(root.getWidth()).show(); } else { new Popup().warning(Res.get("popup.warning.walletNotInitialized")).show(); } }); } @Override protected void deactivate() { btcWalletService.removeBalanceListener(btcWalletBalanceListener); openDetailsButton.setOnAction(null); } private void addXpubKeys(WalletService walletService, String currency, int gridRow, double top) { int row = gridRow; double topDist = top; for (DeterministicKeyChain chain : walletService.getWallet().getActiveKeyChains()) { Script.ScriptType outputScriptType = chain.getOutputScriptType(); String type = outputScriptType == Script.ScriptType.P2WPKH ? "segwit" : "legacy"; String key = chain.getWatchingKey().serializePubB58(Config.baseCurrencyNetworkParameters(), outputScriptType); addTopLabelTextField(root, row, Res.get("account.menu.walletInfo.walletSelector", currency, type), key, topDist); row++; topDist = -Layout.FLOATING_LABEL_DISTANCE; } } private void updateBalances(WalletService walletService) { if (walletService instanceof BtcWalletService) { btcTextField.setText(btcFormatter.formatCoinWithCode(walletService.getBalance(ESTIMATED_SPENDABLE))); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/register/AgentRegistrationView.java ================================================ /* * 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 . */ package haveno.desktop.main.account.register; import haveno.common.UserThread; import haveno.common.util.Tuple2; import haveno.common.util.Tuple3; import haveno.core.locale.LanguageUtil; import haveno.core.locale.Res; import haveno.core.support.dispute.agent.DisputeAgent; import haveno.desktop.common.view.ActivatableViewAndModel; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.TitledGroupBg; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.UnlockDisputeAgentRegistrationWindow; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.ImageUtil; import haveno.desktop.util.Layout; import javafx.beans.value.ChangeListener; import javafx.collections.ListChangeListener; import javafx.geometry.Insets; import javafx.geometry.VPos; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.TextField; import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.util.Callback; import javafx.util.StringConverter; import static haveno.desktop.util.FormBuilder.add2ButtonsAfterGroup; import static haveno.desktop.util.FormBuilder.addMultilineLabel; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; public abstract class AgentRegistrationView> extends ActivatableViewAndModel { private final boolean useDevPrivilegeKeys; private ListView languagesListView; private ComboBox languageComboBox; private int gridRow = 0; private ChangeListener changeListener; private UnlockDisputeAgentRegistrationWindow unlockDisputeAgentRegistrationWindow; private ListChangeListener listChangeListener; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// public AgentRegistrationView(T model, boolean useDevPrivilegeKeys) { super(model); this.useDevPrivilegeKeys = useDevPrivilegeKeys; } @Override public void initialize() { buildUI(); languageComboBox.setItems(model.allLanguageCodes); changeListener = (observable, oldValue, newValue) -> updateLanguageList(); } @Override protected void activate() { } @Override protected void deactivate() { model.myDisputeAgentProperty.removeListener(changeListener); languagesListView.getItems().removeListener(listChangeListener); } public void onTabSelection(boolean isSelectedTab) { if (isSelectedTab) { model.myDisputeAgentProperty.addListener(changeListener); updateLanguageList(); if (model.registrationPubKeyAsHex.get() == null && unlockDisputeAgentRegistrationWindow == null) { unlockDisputeAgentRegistrationWindow = new UnlockDisputeAgentRegistrationWindow(useDevPrivilegeKeys); unlockDisputeAgentRegistrationWindow.onClose(() -> unlockDisputeAgentRegistrationWindow = null) .onKey(model::setPrivKeyAndCheckPubKey) .width(700) .show(); } } else { model.myDisputeAgentProperty.removeListener(changeListener); } } private void updateLanguageList() { languagesListView.setItems(model.languageCodes); languagesListView.setPrefHeight(languagesListView.getItems().size() * Layout.LIST_ROW_HEIGHT + 2); listChangeListener = c -> languagesListView.setPrefHeight(languagesListView.getItems().size() * Layout.LIST_ROW_HEIGHT + 2); languagesListView.getItems().addListener(listChangeListener); } private void buildUI() { GridPane gridPane = new GridPane(); gridPane.setPadding(new Insets(30, 25, -1, 25)); gridPane.setHgap(5); gridPane.setVgap(5); ColumnConstraints columnConstraints1 = new ColumnConstraints(); columnConstraints1.setHgrow(Priority.SOMETIMES); columnConstraints1.setMinWidth(200); columnConstraints1.setMaxWidth(500); gridPane.getColumnConstraints().addAll(columnConstraints1); root.getChildren().add(gridPane); addTitledGroupBg(gridPane, gridRow, 4, Res.get("account.arbitratorRegistration.registration", getRole())); TextField pubKeyTextField = addTopLabelTextField(gridPane, gridRow, Res.get("account.arbitratorRegistration.pubKey"), model.registrationPubKeyAsHex.get(), Layout.FIRST_ROW_DISTANCE).second; pubKeyTextField.textProperty().bind(model.registrationPubKeyAsHex); Tuple3, VBox> tuple = FormBuilder.addTopLabelListView(gridPane, ++gridRow, Res.get("shared.yourLanguage")); GridPane.setValignment(tuple.first, VPos.TOP); languagesListView = tuple.second; languagesListView.disableProperty().bind(model.registrationEditDisabled); languagesListView.setMinHeight(3 * Layout.LIST_ROW_HEIGHT + 2); languagesListView.setMaxHeight(6 * Layout.LIST_ROW_HEIGHT + 2); languagesListView.setCellFactory(new Callback<>() { @Override public ListCell call(ListView list) { return new ListCell<>() { final Label label = new AutoTooltipLabel(); final ImageView icon = ImageUtil.getImageViewById(ImageUtil.REMOVE_ICON); final Button removeButton = new AutoTooltipButton("", icon); final AnchorPane pane = new AnchorPane(label, removeButton); { label.setLayoutY(5); removeButton.setId("icon-button"); AnchorPane.setRightAnchor(removeButton, 0d); } @Override public void updateItem(final String item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { label.setText(LanguageUtil.getDisplayName(item)); removeButton.setOnAction(e -> onRemoveLanguage(item)); setGraphic(pane); } else { setGraphic(null); } } }; } }); languageComboBox = FormBuilder.addComboBox(gridPane, ++gridRow); languageComboBox.disableProperty().bind(model.registrationEditDisabled); languageComboBox.setPromptText(Res.get("shared.addLanguage")); languageComboBox.setConverter(new StringConverter<>() { @Override public String toString(String code) { return LanguageUtil.getDisplayName(code); } @Override public String fromString(String s) { return null; } }); languageComboBox.setOnAction(e -> onAddLanguage()); Tuple2 buttonButtonTuple2 = add2ButtonsAfterGroup(gridPane, ++gridRow, Res.get("account.arbitratorRegistration.register"), Res.get("account.arbitratorRegistration.revoke")); Button registerButton = buttonButtonTuple2.first; registerButton.disableProperty().bind(model.registrationEditDisabled); registerButton.setOnAction(e -> onRegister()); Button revokeButton = buttonButtonTuple2.second; revokeButton.setDefaultButton(false); revokeButton.disableProperty().bind(model.revokeButtonDisabled); revokeButton.setOnAction(e -> onRevoke()); final TitledGroupBg titledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 2, Res.get("shared.information"), Layout.GROUP_DISTANCE); titledGroupBg.getStyleClass().add("last"); Label infoLabel = addMultilineLabel(gridPane, gridRow); GridPane.setMargin(infoLabel, new Insets(Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0)); infoLabel.setText(Res.get("account.arbitratorRegistration.info.msg", getRole().toLowerCase())); } protected abstract String getRole(); /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// private void onAddLanguage() { model.onAddLanguage(languageComboBox.getSelectionModel().getSelectedItem()); UserThread.execute(() -> languageComboBox.getSelectionModel().clearSelection()); } private void onRemoveLanguage(String locale) { model.onRemoveLanguage(locale); if (languagesListView.getItems().size() == 0) { new Popup().warning(Res.get("account.arbitratorRegistration.warn.min1Language")).show(); model.onAddLanguage(LanguageUtil.getDefaultLanguageLocaleAsCode()); } } private void onRevoke() { if (model.isBootstrappedOrShowPopup()) { model.onRevoke( () -> new Popup().feedback(Res.get("account.arbitratorRegistration.removedSuccess")).show(), (errorMessage) -> new Popup().error(Res.get("account.arbitratorRegistration.removedFailed", Res.get("shared.errorMessageInline", errorMessage))).show()); } } private void onRegister() { if (model.isBootstrappedOrShowPopup()) { model.onRegister( () -> new Popup().feedback(Res.get("account.arbitratorRegistration.registerSuccess")).show(), (errorMessage) -> new Popup().error(Res.get("account.arbitratorRegistration.registerFailed", Res.get("shared.errorMessageInline", errorMessage))).show()); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/register/AgentRegistrationViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.account.register; import haveno.common.crypto.KeyRing; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.core.locale.LanguageUtil; import haveno.core.support.dispute.agent.DisputeAgent; import haveno.core.support.dispute.agent.DisputeAgentManager; import haveno.core.user.User; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.common.model.ActivatableViewModel; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.MapChangeListener; import javafx.collections.ObservableList; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.Utils; public abstract class AgentRegistrationViewModel> extends ActivatableViewModel { private final T disputeAgentManager; protected final User user; protected final P2PService p2PService; protected final XmrWalletService xmrWalletService; protected final KeyRing keyRing; final BooleanProperty registrationEditDisabled = new SimpleBooleanProperty(true); final BooleanProperty revokeButtonDisabled = new SimpleBooleanProperty(true); final ObjectProperty myDisputeAgentProperty = new SimpleObjectProperty<>(); protected final ObservableList languageCodes = FXCollections.observableArrayList(LanguageUtil.getDefaultLanguageLocaleAsCode()); final ObservableList allLanguageCodes = FXCollections.observableArrayList(LanguageUtil.getAllLanguageCodes()); private boolean allDataValid; private final MapChangeListener mapChangeListener; protected ECKey registrationKey; final StringProperty registrationPubKeyAsHex = new SimpleStringProperty(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// public AgentRegistrationViewModel(T disputeAgentManager, User user, P2PService p2PService, XmrWalletService xmrWalletService, KeyRing keyRing) { this.disputeAgentManager = disputeAgentManager; this.user = user; this.p2PService = p2PService; this.xmrWalletService = xmrWalletService; this.keyRing = keyRing; mapChangeListener = change -> { R registeredDisputeAgentFromUser = getRegisteredDisputeAgentFromUser(); myDisputeAgentProperty.set(registeredDisputeAgentFromUser); // We don't reset the languages in case of revocation, as its likely that the disputeAgent will use the // same again when he re-activate registration later if (registeredDisputeAgentFromUser != null) languageCodes.setAll(registeredDisputeAgentFromUser.getLanguageCodes()); updateDisableStates(); }; } @Override protected void activate() { disputeAgentManager.getObservableMap().addListener(mapChangeListener); myDisputeAgentProperty.set(getRegisteredDisputeAgentFromUser()); updateDisableStates(); } protected abstract R getRegisteredDisputeAgentFromUser(); @Override protected void deactivate() { disputeAgentManager.getObservableMap().removeListener(mapChangeListener); } /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// void onAddLanguage(String code) { if (code != null && !languageCodes.contains(code)) languageCodes.add(code); updateDisableStates(); } void onRemoveLanguage(String code) { if (code != null && languageCodes.contains(code)) languageCodes.remove(code); updateDisableStates(); } boolean setPrivKeyAndCheckPubKey(String privKeyString) { ECKey registrationKey = disputeAgentManager.getRegistrationKey(privKeyString); if (registrationKey != null) { String _registrationPubKeyAsHex = Utils.HEX.encode(registrationKey.getPubKey()); boolean isKeyValid = disputeAgentManager.isPublicKeyInList(_registrationPubKeyAsHex); if (isKeyValid) { this.registrationKey = registrationKey; registrationPubKeyAsHex.set(_registrationPubKeyAsHex); } updateDisableStates(); return isKeyValid; } else { updateDisableStates(); return false; } } protected abstract R getDisputeAgent(String registrationSignature, String emailAddress); void onRegister(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { updateDisableStates(); if (allDataValid) { String registrationSignature = disputeAgentManager.signStorageSignaturePubKey(registrationKey); // TODO not impl in UI String emailAddress = null; @SuppressWarnings("ConstantConditions") R disputeAgent = getDisputeAgent(registrationSignature, emailAddress); disputeAgentManager.addDisputeAgent(disputeAgent, () -> { updateDisableStates(); resultHandler.handleResult(); }, (errorMessage) -> { updateDisableStates(); errorMessageHandler.handleErrorMessage(errorMessage); }); } } void onRevoke(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { disputeAgentManager.removeDisputeAgent( () -> { updateDisableStates(); resultHandler.handleResult(); }, (errorMessage) -> { updateDisableStates(); errorMessageHandler.handleErrorMessage(errorMessage); }); } private void updateDisableStates() { allDataValid = languageCodes.size() > 0 && registrationKey != null && registrationPubKeyAsHex.get() != null; registrationEditDisabled.set(!allDataValid || myDisputeAgentProperty.get() != null); revokeButtonDisabled.set(!allDataValid || myDisputeAgentProperty.get() == null); } boolean isBootstrappedOrShowPopup() { return GUIUtil.isBootstrappedOrShowPopup(p2PService); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/register/arbitrator/ArbitratorRegistrationView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/register/arbitrator/ArbitratorRegistrationView.java ================================================ /* * 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 . */ package haveno.desktop.main.account.register.arbitrator; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.core.locale.Res; import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; import haveno.desktop.common.view.FxmlView; import haveno.desktop.main.account.register.AgentRegistrationView; @FxmlView public class ArbitratorRegistrationView extends AgentRegistrationView { @Inject public ArbitratorRegistrationView(ArbitratorRegistrationViewModel model, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { super(model, useDevPrivilegeKeys); } @Override protected String getRole() { return Res.get("shared.arbitrator"); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/register/arbitrator/ArbitratorRegistrationViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.account.register.arbitrator; import com.google.inject.Inject; import haveno.common.crypto.KeyRing; import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.user.User; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.main.account.register.AgentRegistrationViewModel; import haveno.network.p2p.P2PService; import java.util.ArrayList; import java.util.Date; public class ArbitratorRegistrationViewModel extends AgentRegistrationViewModel { @Inject public ArbitratorRegistrationViewModel(ArbitratorManager arbitratorManager, User user, P2PService p2PService, XmrWalletService xmrWalletService, KeyRing keyRing) { super(arbitratorManager, user, p2PService, xmrWalletService, keyRing); } @Override protected Arbitrator getDisputeAgent(String registrationSignature, String emailAddress) { return new Arbitrator( p2PService.getAddress(), keyRing.getPubKeyRing(), new ArrayList<>(languageCodes), new Date().getTime(), registrationKey.getPubKey(), registrationSignature, emailAddress, null, null ); } @Override protected Arbitrator getRegisteredDisputeAgentFromUser() { return user.getRegisteredArbitrator(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/register/mediator/MediatorRegistrationView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/register/mediator/MediatorRegistrationView.java ================================================ /* * 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 . */ package haveno.desktop.main.account.register.mediator; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.core.locale.Res; import haveno.core.support.dispute.mediation.mediator.Mediator; import haveno.desktop.common.view.FxmlView; import haveno.desktop.main.account.register.AgentRegistrationView; @FxmlView public class MediatorRegistrationView extends AgentRegistrationView { @Inject public MediatorRegistrationView(MediatorRegistrationViewModel model, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { super(model, useDevPrivilegeKeys); } @Override protected String getRole() { return Res.get("shared.mediator"); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/register/mediator/MediatorRegistrationViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.account.register.mediator; import com.google.inject.Inject; import haveno.common.crypto.KeyRing; import haveno.core.support.dispute.mediation.mediator.Mediator; import haveno.core.support.dispute.mediation.mediator.MediatorManager; import haveno.core.user.User; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.main.account.register.AgentRegistrationViewModel; import haveno.network.p2p.P2PService; import java.util.ArrayList; import java.util.Date; class MediatorRegistrationViewModel extends AgentRegistrationViewModel { @Inject public MediatorRegistrationViewModel(MediatorManager mediatorManager, User user, P2PService p2PService, XmrWalletService xmrWalletService, KeyRing keyRing) { super(mediatorManager, user, p2PService, xmrWalletService, keyRing); } @Override protected Mediator getDisputeAgent(String registrationSignature, String emailAddress) { return new Mediator( p2PService.getAddress(), keyRing.getPubKeyRing(), new ArrayList<>(languageCodes), new Date().getTime(), registrationKey.getPubKey(), registrationSignature, emailAddress, null, null ); } @Override protected Mediator getRegisteredDisputeAgentFromUser() { return user.getRegisteredMediator(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/register/refundagent/RefundAgentRegistrationView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/register/refundagent/RefundAgentRegistrationView.java ================================================ /* * 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 . */ package haveno.desktop.main.account.register.refundagent; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.core.locale.Res; import haveno.core.support.dispute.refund.refundagent.RefundAgent; import haveno.desktop.common.view.FxmlView; import haveno.desktop.main.account.register.AgentRegistrationView; @FxmlView public class RefundAgentRegistrationView extends AgentRegistrationView { @Inject public RefundAgentRegistrationView(RefundAgentRegistrationViewModel model, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { super(model, useDevPrivilegeKeys); } @Override protected String getRole() { return Res.get("shared.refundAgentForSupportStaff"); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/register/refundagent/RefundAgentRegistrationViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.account.register.refundagent; import com.google.inject.Inject; import haveno.common.crypto.KeyRing; import haveno.core.support.dispute.refund.refundagent.RefundAgent; import haveno.core.support.dispute.refund.refundagent.RefundAgentManager; import haveno.core.user.User; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.main.account.register.AgentRegistrationViewModel; import haveno.network.p2p.P2PService; import java.util.ArrayList; import java.util.Date; public class RefundAgentRegistrationViewModel extends AgentRegistrationViewModel { @Inject public RefundAgentRegistrationViewModel(RefundAgentManager arbitratorManager, User user, P2PService p2PService, XmrWalletService xmrWalletService, KeyRing keyRing) { super(arbitratorManager, user, p2PService, xmrWalletService, keyRing); } @Override protected RefundAgent getDisputeAgent(String registrationSignature, String emailAddress) { return new RefundAgent( p2PService.getAddress(), keyRing.getPubKeyRing(), new ArrayList<>(languageCodes), new Date().getTime(), registrationKey.getPubKey(), registrationSignature, emailAddress, null, null ); } @Override protected RefundAgent getRegisteredDisputeAgentFromUser() { return user.getRegisteredRefundAgent(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/register/signing/SigningView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/account/register/signing/SigningView.java ================================================ /* * 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 . */ package haveno.desktop.main.account.register.signing; import com.google.inject.Inject; import haveno.common.util.Utilities; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.common.view.FxmlView; import haveno.desktop.main.overlays.windows.SignPaymentAccountsWindow; import haveno.desktop.main.overlays.windows.SignSpecificWitnessWindow; import haveno.desktop.main.overlays.windows.SignUnsignedPubKeysWindow; import javafx.event.EventHandler; import javafx.scene.Scene; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.AnchorPane; @FxmlView public class SigningView extends ActivatableView { private final SignPaymentAccountsWindow signPaymentAccountsWindow; private final SignSpecificWitnessWindow signSpecificWitnessWindow; private final SignUnsignedPubKeysWindow signUnsignedPubKeysWindow; private EventHandler keyEventEventHandler; private Scene scene; @Inject public SigningView(SignPaymentAccountsWindow signPaymentAccountsWindow, SignSpecificWitnessWindow signSpecificWitnessWindow, SignUnsignedPubKeysWindow signUnsignedPubKeysWindow) { this.signPaymentAccountsWindow = signPaymentAccountsWindow; this.signSpecificWitnessWindow = signSpecificWitnessWindow; this.signUnsignedPubKeysWindow = signUnsignedPubKeysWindow; keyEventEventHandler = this::handleKeyPressed; } @Override protected void activate() { scene = root.getScene(); if (scene != null) scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); } @Override protected void deactivate() { if (scene != null) scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); } protected void handleKeyPressed(KeyEvent event) { if (Utilities.isAltOrCtrlPressed(KeyCode.S, event)) { signPaymentAccountsWindow.show(); } else if (Utilities.isAltOrCtrlPressed(KeyCode.P, event)) { signSpecificWitnessWindow.show(); } else if (Utilities.isAltOrCtrlPressed(KeyCode.O, event)) { signUnsignedPubKeysWindow.show(); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/debug/DebugView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/debug/DebugView.java ================================================ /* * 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 . */ package haveno.desktop.main.debug; import com.google.inject.Inject; import haveno.common.taskrunner.Task; import haveno.common.util.Tuple2; import haveno.core.offer.availability.tasks.ProcessOfferAvailabilityResponse; import haveno.core.offer.availability.tasks.SendOfferAvailabilityRequest; import haveno.core.offer.placeoffer.tasks.MaybeAddToOfferBook; import haveno.core.offer.placeoffer.tasks.MakerReserveOfferFunds; import haveno.core.offer.placeoffer.tasks.ValidateOffer; import haveno.core.trade.protocol.tasks.ApplyFilter; import haveno.core.trade.protocol.tasks.BuyerPreparePaymentSentMessage; import haveno.core.trade.protocol.tasks.BuyerSendPaymentSentMessage; import haveno.core.trade.protocol.tasks.MakerSetLockTime; import haveno.core.trade.protocol.tasks.ProcessPaymentReceivedMessage; import haveno.core.trade.protocol.tasks.ProcessPaymentSentMessage; import haveno.core.trade.protocol.tasks.SellerPreparePaymentReceivedMessage; import haveno.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToBuyer; import haveno.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness; import haveno.desktop.common.view.FxmlView; import haveno.desktop.common.view.InitializableView; import haveno.desktop.components.TitledGroupBg; import static haveno.desktop.util.FormBuilder.addTopLabelComboBox; import java.util.Arrays; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.layout.GridPane; import javafx.util.StringConverter; // Not maintained anymore with new trade protocol, but leave it...If used needs to be adopted to current protocol. @FxmlView public class DebugView extends InitializableView { @FXML TitledGroupBg titledGroupBg; private int rowIndex = 0; @Inject public DebugView() { } @Override public void initialize() { addGroup("OfferAvailabilityProtocol", FXCollections.observableArrayList(Arrays.asList( SendOfferAvailabilityRequest.class, ProcessOfferAvailabilityResponse.class) )); addGroup("PlaceOfferProtocol", FXCollections.observableArrayList(Arrays.asList( ValidateOffer.class, MakerReserveOfferFunds.class, MaybeAddToOfferBook.class) )); addGroup("SellerAsTakerProtocol", FXCollections.observableArrayList(Arrays.asList( ApplyFilter.class, ApplyFilter.class, VerifyPeersAccountAgeWitness.class, ProcessPaymentSentMessage.class, ApplyFilter.class, ApplyFilter.class, SellerPreparePaymentReceivedMessage.class, //SellerBroadcastPayoutTx.class, // TODO (woodser): removed from main pipeline; debug view? SellerSendPaymentReceivedMessageToBuyer.class ) )); addGroup("BuyerAsMakerProtocol", FXCollections.observableArrayList(Arrays.asList( ApplyFilter.class, VerifyPeersAccountAgeWitness.class, MakerSetLockTime.class, ApplyFilter.class, BuyerPreparePaymentSentMessage.class, BuyerSendPaymentSentMessage.class, ProcessPaymentReceivedMessage.class ) )); addGroup("BuyerAsTakerProtocol", FXCollections.observableArrayList(Arrays.asList( ApplyFilter.class, ApplyFilter.class, VerifyPeersAccountAgeWitness.class, ApplyFilter.class, BuyerPreparePaymentSentMessage.class, BuyerSendPaymentSentMessage.class, ProcessPaymentReceivedMessage.class) )); addGroup("SellerAsMakerProtocol", FXCollections.observableArrayList(Arrays.asList( ApplyFilter.class, VerifyPeersAccountAgeWitness.class, MakerSetLockTime.class, ProcessPaymentSentMessage.class, ApplyFilter.class, ApplyFilter.class, SellerPreparePaymentReceivedMessage.class, SellerSendPaymentReceivedMessageToBuyer.class ) )); } private void addGroup(String title, ObservableList> list) { final Tuple2>> selectTaskToIntercept = addTopLabelComboBox(root, ++rowIndex, title, "Select task to intercept", 15); ComboBox> comboBox = selectTaskToIntercept.second; comboBox.setVisibleRowCount(list.size()); comboBox.setItems(list); comboBox.setConverter(new StringConverter<>() { @Override public String toString(Class item) { return item.getSimpleName(); } @Override public Class fromString(String s) { return null; } }); comboBox.setOnAction(event -> Task.taskToIntercept = comboBox.getSelectionModel().getSelectedItem()); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/FundsView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/FundsView.java ================================================ /* * 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 . */ package haveno.desktop.main.funds; import com.google.inject.Inject; import haveno.core.locale.Res; import haveno.desktop.Navigation; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.common.view.CachingViewLoader; import haveno.desktop.common.view.FxmlView; import haveno.desktop.common.view.View; import haveno.desktop.common.view.ViewLoader; import haveno.desktop.main.MainView; import haveno.desktop.main.funds.deposit.DepositView; import haveno.desktop.main.funds.transactions.TransactionsView; import haveno.desktop.main.funds.withdrawal.WithdrawalView; import javafx.beans.value.ChangeListener; import javafx.fxml.FXML; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; @FxmlView public class FundsView extends ActivatableView { @FXML Tab depositTab, withdrawalTab, transactionsTab; private Navigation.Listener navigationListener; private ChangeListener tabChangeListener; private Tab currentTab; private final ViewLoader viewLoader; private final Navigation navigation; @Inject public FundsView(CachingViewLoader viewLoader, Navigation navigation) { this.viewLoader = viewLoader; this.navigation = navigation; } @Override public void initialize() { depositTab.setText(Res.get("funds.tab.deposit")); withdrawalTab.setText(Res.get("funds.tab.withdrawal")); transactionsTab.setText(Res.get("funds.tab.transactions")); navigationListener = (viewPath, data) -> { if (viewPath.size() == 3 && viewPath.indexOf(FundsView.class) == 1) loadView(viewPath.tip()); }; tabChangeListener = (ov, oldValue, newValue) -> { if (newValue == depositTab) navigation.navigateTo(MainView.class, FundsView.class, DepositView.class); else if (newValue == withdrawalTab) navigation.navigateTo(MainView.class, FundsView.class, WithdrawalView.class); else if (newValue == transactionsTab) navigation.navigateTo(MainView.class, FundsView.class, TransactionsView.class); }; } @Override protected void activate() { root.getSelectionModel().selectedItemProperty().addListener(tabChangeListener); navigation.addListener(navigationListener); if (root.getSelectionModel().getSelectedItem() == depositTab) navigation.navigateTo(MainView.class, FundsView.class, DepositView.class); else if (root.getSelectionModel().getSelectedItem() == withdrawalTab) navigation.navigateTo(MainView.class, FundsView.class, WithdrawalView.class); else if (root.getSelectionModel().getSelectedItem() == transactionsTab) navigation.navigateTo(MainView.class, FundsView.class, TransactionsView.class); } @Override protected void deactivate() { root.getSelectionModel().selectedItemProperty().removeListener(tabChangeListener); navigation.removeListener(navigationListener); currentTab = null; } private void loadView(Class viewClass) { // we want to get activate/deactivate called, so we remove the old view on tab change if (currentTab != null) currentTab.setContent(null); View view = viewLoader.load(viewClass); if (view instanceof DepositView) currentTab = depositTab; else if (view instanceof WithdrawalView) currentTab = withdrawalTab; else if (view instanceof TransactionsView) currentTab = transactionsTab; currentTab.setContent(view.getRoot()); root.getSelectionModel().select(currentTab); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositListItem.java ================================================ /* * 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 . */ package haveno.desktop.main.funds.deposit; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; import haveno.core.util.coin.CoinFormatter; import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.components.indicator.TxConfidenceIndicator; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.filtering.FilterableListItem; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.scene.control.Tooltip; import lombok.extern.slf4j.Slf4j; import monero.daemon.model.MoneroTx; import monero.wallet.model.MoneroTxWallet; import java.math.BigInteger; import java.util.List; import org.apache.commons.lang3.StringUtils; @Slf4j class DepositListItem implements FilterableListItem { private final StringProperty balance = new SimpleStringProperty(); private final XmrAddressEntry addressEntry; private final XmrWalletService xmrWalletService; private BigInteger balanceAsBI; private String usage = "-"; private int numTxsWithOutputs = 0; private final Supplier lazyFieldsSupplier; private static class LazyFields { TxConfidenceIndicator txConfidenceIndicator; Tooltip tooltip; } private LazyFields lazy() { return lazyFieldsSupplier.get(); } DepositListItem(XmrAddressEntry addressEntry, XmrWalletService xmrWalletService, CoinFormatter formatter) { this.xmrWalletService = xmrWalletService; this.addressEntry = addressEntry; balanceAsBI = xmrWalletService.getBalanceForSubaddress(addressEntry.getSubaddressIndex(), true); balance.set(HavenoUtils.formatXmr(balanceAsBI)); updateUsage(addressEntry.getSubaddressIndex()); // confidence lazyFieldsSupplier = Suppliers.memoize(() -> new LazyFields() {{ txConfidenceIndicator = new TxConfidenceIndicator(); txConfidenceIndicator.setId("funds-confidence"); tooltip = new Tooltip(Res.get("shared.notUsedYet")); txConfidenceIndicator.setProgress(0); txConfidenceIndicator.setTooltip(tooltip); MoneroTx tx = getTxWithFewestConfirmations(); if (tx == null) { txConfidenceIndicator.setVisible(false); } else { GUIUtil.updateConfidence(tx, tooltip, txConfidenceIndicator); txConfidenceIndicator.setVisible(true); } }}); } private void updateUsage(int subaddressIndex) { numTxsWithOutputs = xmrWalletService.getNumTxsWithIncomingOutputs(addressEntry.getSubaddressIndex()); switch (addressEntry.getContext()) { case BASE_ADDRESS: usage = Res.get("funds.deposit.baseAddress"); break; case AVAILABLE: usage = numTxsWithOutputs == 0 ? Res.get("funds.deposit.unused") : Res.get("funds.deposit.usedInTx", numTxsWithOutputs); break; case OFFER_FUNDING: usage = Res.get("funds.deposit.offerFunding", addressEntry.getShortOfferId()); break; case TRADE_PAYOUT: usage = Res.get("funds.deposit.tradePayout", addressEntry.getShortOfferId()); break; default: usage = addressEntry.getContext().toString(); } } public void cleanup() { } public TxConfidenceIndicator getTxConfidenceIndicator() { return lazy().txConfidenceIndicator; } public String getAddressString() { return addressEntry.getAddressString(); } public int getSubaddressIndex() { return addressEntry.getSubaddressIndex(); } public String getUsage() { return usage; } public final StringProperty balanceProperty() { return this.balance; } public String getBalance() { return balance.get(); } public BigInteger getBalanceAsBI() { return balanceAsBI; } public int getNumTxsWithOutputs() { return numTxsWithOutputs; } public long getNumConfirmationsSinceFirstUsed() { MoneroTx tx = getTxWithFewestConfirmations(); return tx == null ? 0 : tx.getNumConfirmations(); } private MoneroTxWallet getTxWithFewestConfirmations() { // get txs with incoming outputs to subaddress index List txs = xmrWalletService.getTxsWithIncomingOutputs(addressEntry.getSubaddressIndex()); // get tx with fewest confirmations MoneroTxWallet highestTx = null; for (MoneroTxWallet tx : txs) { if (highestTx == null || tx.getHeight() == null || (highestTx.getHeight() != null && tx.getHeight() > highestTx.getHeight())) { highestTx = tx; } } return highestTx; } @Override public boolean match(String filterString) { if (filterString.isEmpty()) { return true; } if (StringUtils.containsIgnoreCase(getAddressString(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getUsage(), filterString)) { return true; } return getBalance().contains(filterString); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.desktop.main.funds.deposit; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.ThreadUtils; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.util.Tuple2; import haveno.common.util.Tuple3; import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.ParsingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.core.xmr.listeners.XmrBalanceListener; import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.AddressTextField; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.HyperlinkWithIcon; import haveno.desktop.components.InputTextField; import haveno.desktop.components.TitledGroupBg; import haveno.desktop.components.list.FilterBox; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.QRCodeWindow; import static haveno.desktop.util.FormBuilder.addAddressTextField; import static haveno.desktop.util.FormBuilder.addButtonCheckBoxWithBox; import static haveno.desktop.util.FormBuilder.addInputTextField; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.Layout; import java.io.ByteArrayInputStream; import java.math.BigInteger; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.concurrent.TimeUnit; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; import javafx.fxml.FXML; import javafx.geometry.Insets; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.Label; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.util.Callback; import monero.wallet.model.MoneroWalletListener; import net.glxn.qrgen.QRCode; import net.glxn.qrgen.image.ImageType; import org.bitcoinj.core.Coin; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; import org.jetbrains.annotations.NotNull; @FxmlView public class DepositView extends ActivatableView { @FXML GridPane gridPane; @FXML FilterBox filterBox; @FXML TableView tableView; @FXML TableColumn addressColumn, balanceColumn, confirmationsColumn, usageColumn; private ImageView qrCodeImageView; private StackPane qrCodePane; private AddressTextField addressTextField; private Button generateNewAddressButton; private TitledGroupBg titledGroupBg; private InputTextField amountTextField; private static final String THREAD_ID = DepositView.class.getName(); private final XmrWalletService xmrWalletService; private final Preferences preferences; private final CoinFormatter formatter; private String paymentLabelString; private final ObservableList observableList = FXCollections.observableArrayList(); private final FilteredList filteredList = new FilteredList<>(observableList); private final SortedList sortedList = new SortedList<>(filteredList); private XmrBalanceListener balanceListener; private MoneroWalletListener walletListener; private Subscription amountTextFieldSubscription; private ChangeListener tableViewSelectionListener; private int gridRow = 0; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject private DepositView(XmrWalletService xmrWalletService, Preferences preferences, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter) { this.xmrWalletService = xmrWalletService; this.preferences = preferences; this.formatter = formatter; } @Override public void initialize() { GUIUtil.applyTableStyle(tableView); filterBox.initialize(filteredList, tableView); filterBox.setPromptText(Res.get("shared.filter")); paymentLabelString = Res.get("funds.deposit.fundHavenoWallet"); addressColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.address"))); balanceColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.balanceWithCur", Res.getBaseCurrencyCode()))); confirmationsColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.confirmations"))); usageColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.usage"))); // set loading placeholder Label placeholderLabel = new Label("Loading..."); tableView.setPlaceholder(placeholderLabel); tableView.getStyleClass().add("non-interactive-table"); ThreadUtils.execute(() -> { // trigger creation of at least 1 address try { xmrWalletService.getFreshAddressEntry(); } catch (Exception e) { log.warn("Failed to create fresh address entry to initialize DepositView"); e.printStackTrace(); } UserThread.execute(() -> { tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); tableView.setPlaceholder(new AutoTooltipLabel(Res.get("funds.deposit.noAddresses"))); tableViewSelectionListener = (observableValue, oldValue, newValue) -> { if (newValue != null) { fillForm(newValue.getAddressString()); GUIUtil.requestFocus(amountTextField); } }; setAddressColumnCellFactory(); setBalanceColumnCellFactory(); setUsageColumnCellFactory(); setConfidenceColumnCellFactory(); addressColumn.setComparator(Comparator.comparing(DepositListItem::getAddressString)); balanceColumn.setComparator(Comparator.comparing(DepositListItem::getBalanceAsBI)); confirmationsColumn.setComparator(Comparator.comparingLong(o -> o.getNumConfirmationsSinceFirstUsed())); usageColumn.setComparator(Comparator.comparing(DepositListItem::getUsage)); tableView.getSortOrder().add(usageColumn); tableView.setItems(sortedList); titledGroupBg = addTitledGroupBg(gridPane, gridRow, 4, Res.get("funds.deposit.fundWallet")); titledGroupBg.getStyleClass().add("last"); Tuple2 qrCodeTuple = GUIUtil.getSmallXmrQrCodePane(); qrCodePane = qrCodeTuple.first; qrCodeImageView = qrCodeTuple.second; Tooltip.install(qrCodePane, new Tooltip(Res.get("shared.openLargeQRWindow"))); qrCodePane.setOnMouseClicked(e -> UserThread.runAfter( () -> new QRCodeWindow(getPaymentUri()).show(), 200, TimeUnit.MILLISECONDS)); GridPane.setRowIndex(qrCodePane, gridRow); GridPane.setRowSpan(qrCodePane, 4); GridPane.setColumnIndex(qrCodePane, 1); GridPane.setMargin(qrCodePane, new Insets(Layout.FIRST_ROW_DISTANCE, 0, 0, 10)); gridPane.getChildren().add(qrCodePane); addressTextField = addAddressTextField(gridPane, ++gridRow, Res.get("shared.address"), Layout.FIRST_ROW_DISTANCE); addressTextField.setPaymentLabel(paymentLabelString); amountTextField = addInputTextField(gridPane, ++gridRow, Res.get("funds.deposit.amount")); amountTextField.setMaxWidth(380); if (DevEnv.isDevMode()) amountTextField.setText("10"); titledGroupBg.setVisible(false); titledGroupBg.setManaged(false); qrCodePane.setVisible(false); qrCodePane.setManaged(false); addressTextField.setVisible(false); addressTextField.setManaged(false); amountTextField.setManaged(false); Tuple3 buttonCheckBoxHBox = addButtonCheckBoxWithBox(gridPane, ++gridRow, Res.get("funds.deposit.generateAddress"), null, 15); buttonCheckBoxHBox.third.setSpacing(25); generateNewAddressButton = buttonCheckBoxHBox.first; generateNewAddressButton.setOnAction(event -> { boolean hasUnusedAddress = !xmrWalletService.getUnusedAddressEntries().isEmpty(); if (hasUnusedAddress) { new Popup().warning(Res.get("funds.deposit.selectUnused")).show(); } else { XmrAddressEntry newSavingsAddressEntry = xmrWalletService.getNewAddressEntry(); updateList(); UserThread.execute(() -> { observableList.stream() .filter(depositListItem -> depositListItem.getAddressString().equals(newSavingsAddressEntry.getAddressString())) .findAny() .ifPresent(depositListItem -> tableView.getSelectionModel().select(depositListItem)); }); } }); balanceListener = new XmrBalanceListener() { @Override public void onBalanceChanged(BigInteger balance) { updateList(); } }; walletListener = new MoneroWalletListener() { @Override public void onNewBlock(long height) { updateList(); } }; GUIUtil.focusWhenAddedToScene(amountTextField); }); }, THREAD_ID); } @Override protected void activate() { ThreadUtils.execute(() -> { UserThread.execute(() -> { filterBox.activate(); tableView.getSelectionModel().selectedItemProperty().addListener(tableViewSelectionListener); sortedList.comparatorProperty().bind(tableView.comparatorProperty()); // try to update deposits list try { updateList(); } catch (Exception e) { log.warn("Could not update deposits list"); e.printStackTrace(); } xmrWalletService.addBalanceListener(balanceListener); xmrWalletService.addWalletListener(walletListener); amountTextFieldSubscription = EasyBind.subscribe(amountTextField.textProperty(), t -> { addressTextField.setAmount(HavenoUtils.parseXmr(t)); updateQRCode(); }); if (tableView.getSelectionModel().getSelectedItem() == null && !sortedList.isEmpty()) tableView.getSelectionModel().select(0); }); }, THREAD_ID); } @Override protected void deactivate() { ThreadUtils.execute(() -> { filterBox.deactivate(); tableView.getSelectionModel().selectedItemProperty().removeListener(tableViewSelectionListener); sortedList.comparatorProperty().unbind(); observableList.forEach(DepositListItem::cleanup); xmrWalletService.removeBalanceListener(balanceListener); xmrWalletService.removeWalletListener(walletListener); amountTextFieldSubscription.unsubscribe(); }, THREAD_ID); } /////////////////////////////////////////////////////////////////////////////////////////// // UI handlers /////////////////////////////////////////////////////////////////////////////////////////// private void fillForm(String address) { titledGroupBg.setVisible(true); titledGroupBg.setManaged(true); qrCodePane.setVisible(true); qrCodePane.setManaged(true); addressTextField.setVisible(true); addressTextField.setManaged(true); amountTextField.setManaged(true); GridPane.setMargin(generateNewAddressButton, new Insets(15, 0, 0, 0)); addressTextField.setAddress(address); updateQRCode(); } private void updateQRCode() { if (addressTextField.getAddress() != null && !addressTextField.getAddress().isEmpty()) { final byte[] imageBytes = QRCode .from(getPaymentUri()) .withSize(300, 300) .to(ImageType.PNG) .stream() .toByteArray(); Image qrImage = new Image(new ByteArrayInputStream(imageBytes)); qrCodeImageView.setImage(qrImage); } } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void updateList() { // create deposit list items List addressEntries = xmrWalletService.getAddressEntries(); List items = new ArrayList<>(); for (XmrAddressEntry addressEntry : addressEntries) { if (addressEntry.isTradePayout()) continue; // do not show trade payout addresses items.add(new DepositListItem(addressEntry, xmrWalletService, formatter)); } // update list UserThread.execute(() -> { observableList.forEach(DepositListItem::cleanup); observableList.clear(); for (DepositListItem item : items) { observableList.add(item); } }); } private Coin getAmount() { return ParsingUtils.parseToCoin(amountTextField.getText(), formatter); } @NotNull private String getPaymentUri() { return GUIUtil.getMoneroURI(addressTextField.getAddress(), HavenoUtils.coinToAtomicUnits(getAmount()), paymentLabelString); } /////////////////////////////////////////////////////////////////////////////////////////// // ColumnCellFactories /////////////////////////////////////////////////////////////////////////////////////////// private void setUsageColumnCellFactory() { usageColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); usageColumn.setCellFactory(new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final DepositListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { Label usageLabel = new AutoTooltipLabel(item.getUsage()); usageLabel.getStyleClass().add("highlight-text"); setGraphic(usageLabel); } else { setGraphic(null); } } }; } }); } private void setAddressColumnCellFactory() { addressColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); addressColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { private HyperlinkWithIcon field; @Override public void updateItem(final DepositListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { String address = item.getAddressString(); setGraphic(new AutoTooltipLabel(address)); } else { setGraphic(null); if (field != null) field.setOnAction(null); } } }; } }); } private void setBalanceColumnCellFactory() { balanceColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); balanceColumn.getStyleClass().add("highlight-text"); balanceColumn.setCellFactory(new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final DepositListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { if (textProperty().isBound()) textProperty().unbind(); textProperty().bind(item.balanceProperty()); } else { textProperty().unbind(); setText(""); } } }; } }); } private void setConfidenceColumnCellFactory() { confirmationsColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); confirmationsColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final DepositListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setGraphic(item.getTxConfidenceIndicator()); } else { setGraphic(null); } } }; } }); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/locked/LockedListItem.java ================================================ /* * 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 . */ package haveno.desktop.main.funds.locked; import haveno.core.locale.Res; import haveno.core.trade.Trade; import haveno.core.util.coin.CoinFormatter; import haveno.core.xmr.listeners.BalanceListener; import haveno.core.xmr.model.AddressEntry; import haveno.core.xmr.wallet.BtcWalletService; import haveno.desktop.util.DisplayUtils; import javafx.scene.control.Label; import lombok.Getter; import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; import javax.annotation.Nullable; class LockedListItem { private final BalanceListener balanceListener; private final BtcWalletService btcWalletService; private final CoinFormatter formatter; @Getter private final Label balanceLabel; @Getter private final Trade trade; @Getter private final AddressEntry addressEntry; @Getter private final String addressString; @Nullable private final Address address; @Getter private Coin balance; @Getter private String balanceString; public LockedListItem(Trade trade, AddressEntry addressEntry, BtcWalletService btcWalletService, CoinFormatter formatter) { this.trade = trade; this.addressEntry = addressEntry; this.btcWalletService = btcWalletService; this.formatter = formatter; throw new RuntimeException("Cannot listen to multisig deposits in xmr without exchanging multisig info"); // if (trade.getDepositTx() != null && !trade.getDepositTx().getOutputs().isEmpty()) { // address = WalletService.getAddressFromOutput(trade.getDepositTx().getOutput(0)); // addressString = address != null ? address.toString() : ""; // } else { // address = null; // addressString = ""; // } // balanceLabel = new AutoTooltipLabel(); // balanceListener = new BalanceListener(address) { // @Override // public void onBalanceChanged(Coin balance, Transaction tx) { // updateBalance(); // } // }; // btcWalletService.addBalanceListener(balanceListener); // updateBalance(); } LockedListItem() { this.trade = null; this.addressEntry = null; this.btcWalletService = null; this.formatter = null; addressString = null; address = null; balanceLabel = null; balanceListener = null; } public void cleanup() { btcWalletService.removeBalanceListener(balanceListener); } private void updateBalance() { balance = addressEntry.getCoinLockedInMultiSigAsCoin(); balanceString = formatter.formatCoin(this.balance); balanceLabel.setText(balanceString); } public String getDetails() { return trade != null ? Res.get("funds.locked.locked", trade.getShortId()) : Res.get("shared.noDetailsAvailable"); } public String getDateString() { return trade != null ? DisplayUtils.formatDateTime(trade.getDate()) : Res.get("shared.noDateAvailable"); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/locked/LockedView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/locked/LockedView.java ================================================ /* * 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 . */ package haveno.desktop.main.funds.locked; import com.google.inject.Inject; import com.google.inject.name.Named; import com.googlecode.jcsv.writer.CSVEntryConverter; import de.jensd.fx.fontawesome.AwesomeIcon; import haveno.core.locale.Res; import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; import haveno.core.trade.Tradable; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.core.xmr.listeners.BalanceListener; import haveno.core.xmr.model.AddressEntry; import haveno.core.xmr.wallet.BtcWalletService; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.HyperlinkWithIcon; import haveno.desktop.main.overlays.windows.OfferDetailsWindow; import haveno.desktop.main.overlays.windows.TradeDetailsWindow; import haveno.desktop.util.GUIUtil; import java.util.Comparator; import java.util.Date; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.SortedList; import javafx.fxml.FXML; import javafx.geometry.Insets; import javafx.scene.control.Label; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.stage.Stage; import javafx.util.Callback; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; @FxmlView public class LockedView extends ActivatableView { @FXML TableView tableView; @FXML TableColumn dateColumn, detailsColumn, addressColumn, balanceColumn; @FXML Label numItems; @FXML Region spacer; @FXML AutoTooltipButton exportButton; private final BtcWalletService btcWalletService; private final TradeManager tradeManager; private final OpenOfferManager openOfferManager; private final Preferences preferences; private final CoinFormatter formatter; private final OfferDetailsWindow offerDetailsWindow; private final TradeDetailsWindow tradeDetailsWindow; private final ObservableList observableList = FXCollections.observableArrayList(); private final SortedList sortedList = new SortedList<>(observableList); private BalanceListener balanceListener; private ListChangeListener openOfferListChangeListener; private ListChangeListener tradeListChangeListener; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject private LockedView(BtcWalletService btcWalletService, TradeManager tradeManager, OpenOfferManager openOfferManager, Preferences preferences, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, OfferDetailsWindow offerDetailsWindow, TradeDetailsWindow tradeDetailsWindow) { this.btcWalletService = btcWalletService; this.tradeManager = tradeManager; this.openOfferManager = openOfferManager; this.preferences = preferences; this.formatter = formatter; this.offerDetailsWindow = offerDetailsWindow; this.tradeDetailsWindow = tradeDetailsWindow; } @Override public void initialize() { dateColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.dateTime"))); detailsColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.details"))); addressColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.address"))); balanceColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.balanceWithCur", Res.getBaseCurrencyCode()))); GUIUtil.applyTableStyle(tableView); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); tableView.setPlaceholder(new AutoTooltipLabel(Res.get("funds.locked.noFunds"))); setDateColumnCellFactory(); setDetailsColumnCellFactory(); setAddressColumnCellFactory(); setBalanceColumnCellFactory(); addressColumn.setComparator(Comparator.comparing(LockedListItem::getAddressString)); detailsColumn.setComparator(Comparator.comparing(o -> o.getTrade().getId())); balanceColumn.setComparator(Comparator.comparing(LockedListItem::getBalance)); dateColumn.setComparator(Comparator.comparing(o -> getTradable(o).map(Tradable::getDate).orElse(new Date(0)))); tableView.getSortOrder().add(dateColumn); dateColumn.setSortType(TableColumn.SortType.DESCENDING); balanceListener = new BalanceListener() { @Override public void onBalanceChanged(Coin balance, Transaction tx) { updateList(); } }; openOfferListChangeListener = c -> updateList(); tradeListChangeListener = c -> updateList(); HBox.setHgrow(spacer, Priority.ALWAYS); numItems.setId("num-offers"); numItems.setPadding(new Insets(-5, 0, 0, 10)); exportButton.updateText(Res.get("shared.exportCSV")); } @Override protected void activate() { openOfferManager.getObservableList().addListener(openOfferListChangeListener); tradeManager.getObservableList().addListener(tradeListChangeListener); sortedList.comparatorProperty().bind(tableView.comparatorProperty()); tableView.setItems(sortedList); updateList(); btcWalletService.addBalanceListener(balanceListener); numItems.setText(Res.get("shared.numItemsLabel", sortedList.size())); exportButton.setOnAction(event -> { ObservableList> tableColumns = GUIUtil.getContentColumns(tableView); int reportColumns = tableColumns.size(); CSVEntryConverter headerConverter = item -> { String[] columns = new String[reportColumns]; for (int i = 0; i < columns.length; i++) columns[i] = ((AutoTooltipLabel) tableColumns.get(i).getGraphic()).getText(); return columns; }; CSVEntryConverter contentConverter = item -> { String[] columns = new String[reportColumns]; columns[0] = item.getDateString(); columns[1] = item.getDetails(); columns[2] = item.getAddressString(); columns[3] = item.getBalanceString(); return columns; }; GUIUtil.exportCSV("lockedInTradesFunds.csv", headerConverter, contentConverter, new LockedListItem(), sortedList, (Stage) root.getScene().getWindow()); }); } @Override protected void deactivate() { openOfferManager.getObservableList().removeListener(openOfferListChangeListener); tradeManager.getObservableList().removeListener(tradeListChangeListener); sortedList.comparatorProperty().unbind(); observableList.forEach(LockedListItem::cleanup); btcWalletService.removeBalanceListener(balanceListener); exportButton.setOnAction(null); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void updateList() { observableList.forEach(LockedListItem::cleanup); observableList.setAll(tradeManager.getTradesStreamWithFundsLockedIn() .map(trade -> { Optional addressEntryOptional = btcWalletService.getAddressEntry(trade.getId(), AddressEntry.Context.MULTI_SIG); return addressEntryOptional.map(addressEntry -> new LockedListItem(trade, addressEntry, btcWalletService, formatter)).orElse(null); }) .filter(Objects::nonNull) .collect(Collectors.toList())); } private Optional getTradable(LockedListItem item) { String offerId = item.getAddressEntry().getOfferId(); Optional tradeOptional = tradeManager.getOpenTrade(offerId); if (tradeOptional.isPresent()) { return Optional.of(tradeOptional.get()); } else if (openOfferManager.getOpenOffer(offerId).isPresent()) { return Optional.of(openOfferManager.getOpenOffer(offerId).get()); } else { return Optional.empty(); } } private void openDetailPopup(LockedListItem item) { Optional tradableOptional = getTradable(item); if (tradableOptional.isPresent()) { Tradable tradable = tradableOptional.get(); if (tradable instanceof Trade) { tradeDetailsWindow.show((Trade) tradable); } else if (tradable instanceof OpenOffer) { offerDetailsWindow.show(tradable.getOffer()); } } } /////////////////////////////////////////////////////////////////////////////////////////// // ColumnCellFactories /////////////////////////////////////////////////////////////////////////////////////////// private void setDateColumnCellFactory() { dateColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); dateColumn.setCellFactory(new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final LockedListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { if (getTradable(item).isPresent()) setGraphic(new AutoTooltipLabel(item.getDateString())); else setGraphic(new AutoTooltipLabel(item.getDateString())); } else { setGraphic(null); } } }; } }); } private void setDetailsColumnCellFactory() { detailsColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); detailsColumn.setCellFactory(new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { private HyperlinkWithIcon field; @Override public void updateItem(final LockedListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { Optional tradableOptional = getTradable(item); if (tradableOptional.isPresent()) { field = new HyperlinkWithIcon(item.getDetails(), AwesomeIcon.INFO_SIGN); field.setOnAction(event -> openDetailPopup(item)); field.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails"))); setGraphic(field); } else { setGraphic(new AutoTooltipLabel(item.getDetails())); } } else { setGraphic(null); if (field != null) field.setOnAction(null); } } }; } }); } private void setAddressColumnCellFactory() { addressColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); addressColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final LockedListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { String address = item.getAddressString(); setGraphic(new AutoTooltipLabel(address)); } else { setGraphic(null); } } }; } }); } private void setBalanceColumnCellFactory() { balanceColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); balanceColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final LockedListItem item, boolean empty) { super.updateItem(item, empty); setGraphic((item != null && !empty) ? item.getBalanceLabel() : null); } }; } }); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/reserved/ReservedListItem.java ================================================ /* * 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 . */ package haveno.desktop.main.funds.reserved; import haveno.core.locale.Res; import haveno.core.offer.OpenOffer; import haveno.core.util.coin.CoinFormatter; import haveno.core.xmr.listeners.BalanceListener; import haveno.core.xmr.model.AddressEntry; import haveno.core.xmr.wallet.BtcWalletService; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.util.DisplayUtils; import javafx.scene.control.Label; import lombok.Getter; import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; import java.util.Optional; class ReservedListItem { private final BalanceListener balanceListener; private final BtcWalletService btcWalletService; private final CoinFormatter formatter; @Getter private final Label balanceLabel; @Getter private final OpenOffer openOffer; @Getter private final AddressEntry addressEntry; @Getter private final String addressString; @Getter private final Address address; @Getter private Coin balance; @Getter private String balanceString; public ReservedListItem(OpenOffer openOffer, AddressEntry addressEntry, BtcWalletService btcWalletService, CoinFormatter formatter) { this.openOffer = openOffer; this.addressEntry = addressEntry; this.btcWalletService = btcWalletService; this.formatter = formatter; addressString = addressEntry.getAddressString(); address = addressEntry.getAddress(); balanceLabel = new AutoTooltipLabel(); balanceListener = new BalanceListener(address) { @Override public void onBalanceChanged(Coin balance, Transaction tx) { updateBalance(); } }; btcWalletService.addBalanceListener(balanceListener); updateBalance(); } ReservedListItem() { this.openOffer = null; this.addressEntry = null; this.btcWalletService = null; this.formatter = null; addressString = null; address = null; balanceLabel = null; balanceListener = null; } public void cleanup() { btcWalletService.removeBalanceListener(balanceListener); } private void updateBalance() { Optional addressEntryOptional = btcWalletService.getAddressEntry(openOffer.getId(), AddressEntry.Context.RESERVED_FOR_TRADE); addressEntryOptional.ifPresent(addressEntry -> { balance = btcWalletService.getBalanceForAddress(addressEntry.getAddress()); if (balance != null) { balanceString = formatter.formatCoin(balance); balanceLabel.setText(balanceString); } }); } public String getDateAsString() { return DisplayUtils.formatDateTime(openOffer.getDate()); } public String getDetails() { return openOffer != null ? Res.get("funds.reserved.reserved", openOffer.getShortId()) : Res.get("shared.noDetailsAvailable"); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/reserved/ReservedView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/reserved/ReservedView.java ================================================ /* * 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 . */ package haveno.desktop.main.funds.reserved; import com.google.inject.Inject; import com.google.inject.name.Named; import com.googlecode.jcsv.writer.CSVEntryConverter; import de.jensd.fx.fontawesome.AwesomeIcon; import haveno.core.locale.Res; import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; import haveno.core.trade.Tradable; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.core.xmr.listeners.BalanceListener; import haveno.core.xmr.model.AddressEntry; import haveno.core.xmr.wallet.BtcWalletService; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.HyperlinkWithIcon; import haveno.desktop.main.overlays.windows.OfferDetailsWindow; import haveno.desktop.main.overlays.windows.TradeDetailsWindow; import haveno.desktop.util.GUIUtil; import java.util.Comparator; import java.util.Date; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.SortedList; import javafx.fxml.FXML; import javafx.geometry.Insets; import javafx.scene.control.Label; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.stage.Stage; import javafx.util.Callback; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; @FxmlView public class ReservedView extends ActivatableView { @FXML TableView tableView; @FXML TableColumn dateColumn, detailsColumn, addressColumn, balanceColumn; @FXML Label numItems; @FXML Region spacer; @FXML AutoTooltipButton exportButton; private final BtcWalletService btcWalletService; private final TradeManager tradeManager; private final OpenOfferManager openOfferManager; private final Preferences preferences; private final CoinFormatter formatter; private final OfferDetailsWindow offerDetailsWindow; private final TradeDetailsWindow tradeDetailsWindow; private final ObservableList observableList = FXCollections.observableArrayList(); private final SortedList sortedList = new SortedList<>(observableList); private BalanceListener balanceListener; private ListChangeListener openOfferListChangeListener; private ListChangeListener tradeListChangeListener; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject private ReservedView(BtcWalletService btcWalletService, TradeManager tradeManager, OpenOfferManager openOfferManager, Preferences preferences, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, OfferDetailsWindow offerDetailsWindow, TradeDetailsWindow tradeDetailsWindow) { this.btcWalletService = btcWalletService; this.tradeManager = tradeManager; this.openOfferManager = openOfferManager; this.preferences = preferences; this.formatter = formatter; this.offerDetailsWindow = offerDetailsWindow; this.tradeDetailsWindow = tradeDetailsWindow; } @Override public void initialize() { dateColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.dateTime"))); detailsColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.details"))); addressColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.address"))); balanceColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.balanceWithCur", Res.getBaseCurrencyCode()))); GUIUtil.applyTableStyle(tableView); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); tableView.setPlaceholder(new AutoTooltipLabel(Res.get("funds.reserved.noFunds"))); setDateColumnCellFactory(); setDetailsColumnCellFactory(); setAddressColumnCellFactory(); setBalanceColumnCellFactory(); addressColumn.setComparator(Comparator.comparing(ReservedListItem::getAddressString)); detailsColumn.setComparator(Comparator.comparing(o -> o.getOpenOffer().getId())); balanceColumn.setComparator(Comparator.comparing(ReservedListItem::getBalance)); dateColumn.setComparator(Comparator.comparing(o -> getTradable(o).map(Tradable::getDate).orElse(new Date(0)))); tableView.getSortOrder().add(dateColumn); dateColumn.setSortType(TableColumn.SortType.DESCENDING); balanceListener = new BalanceListener() { @Override public void onBalanceChanged(Coin balance, Transaction tx) { updateList(); } }; openOfferListChangeListener = c -> updateList(); tradeListChangeListener = c -> updateList(); HBox.setHgrow(spacer, Priority.ALWAYS); numItems.setId("num-offers"); numItems.setPadding(new Insets(-5, 0, 0, 10)); exportButton.updateText(Res.get("shared.exportCSV")); } @Override protected void activate() { openOfferManager.getObservableList().addListener(openOfferListChangeListener); tradeManager.getObservableList().addListener(tradeListChangeListener); sortedList.comparatorProperty().bind(tableView.comparatorProperty()); tableView.setItems(sortedList); updateList(); btcWalletService.addBalanceListener(balanceListener); numItems.setText(Res.get("shared.numItemsLabel", sortedList.size())); exportButton.setOnAction(event -> { ObservableList> tableColumns = GUIUtil.getContentColumns(tableView); int reportColumns = tableColumns.size(); CSVEntryConverter headerConverter = item -> { String[] columns = new String[reportColumns]; for (int i = 0; i < columns.length; i++) columns[i] = ((AutoTooltipLabel) tableColumns.get(i).getGraphic()).getText(); return columns; }; CSVEntryConverter contentConverter = item -> { String[] columns = new String[reportColumns]; columns[0] = item.getDateAsString(); columns[1] = item.getDetails(); columns[2] = item.getAddressString(); columns[3] = item.getBalanceString(); return columns; }; GUIUtil.exportCSV("reservedInOffersFunds.csv", headerConverter, contentConverter, new ReservedListItem(), sortedList, (Stage) root.getScene().getWindow()); }); } @Override protected void deactivate() { openOfferManager.getObservableList().removeListener(openOfferListChangeListener); tradeManager.getObservableList().removeListener(tradeListChangeListener); sortedList.comparatorProperty().unbind(); observableList.forEach(ReservedListItem::cleanup); btcWalletService.removeBalanceListener(balanceListener); exportButton.setOnAction(null); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void updateList() { observableList.forEach(ReservedListItem::cleanup); observableList.setAll(openOfferManager.getObservableList().stream() .map(openOffer -> { Optional addressEntryOptional = btcWalletService.getAddressEntry(openOffer.getId(), AddressEntry.Context.RESERVED_FOR_TRADE); return addressEntryOptional.map(addressEntry -> new ReservedListItem(openOffer, addressEntry, btcWalletService, formatter)).orElse(null); }) .filter(Objects::nonNull) .collect(Collectors.toList())); } private Optional getTradable(ReservedListItem item) { String offerId = item.getAddressEntry().getOfferId(); Optional tradeOptional = tradeManager.getOpenTrade(offerId); if (tradeOptional.isPresent()) { return Optional.of(tradeOptional.get()); } else if (openOfferManager.getOpenOffer(offerId).isPresent()) { return Optional.of(openOfferManager.getOpenOffer(offerId).get()); } else { return Optional.empty(); } } private void openDetailPopup(ReservedListItem item) { Optional tradableOptional = getTradable(item); if (tradableOptional.isPresent()) { Tradable tradable = tradableOptional.get(); if (tradable instanceof Trade) { tradeDetailsWindow.show((Trade) tradable); } else if (tradable instanceof OpenOffer) { offerDetailsWindow.show(tradable.getOffer()); } } } /////////////////////////////////////////////////////////////////////////////////////////// // ColumnCellFactories /////////////////////////////////////////////////////////////////////////////////////////// private void setDateColumnCellFactory() { dateColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); dateColumn.setCellFactory(new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final ReservedListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { if (getTradable(item).isPresent()) { setGraphic(new AutoTooltipLabel(item.getDateAsString())); } else setGraphic(new AutoTooltipLabel(Res.get("shared.noDateAvailable"))); } else { setGraphic(null); } } }; } }); } private void setDetailsColumnCellFactory() { detailsColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); detailsColumn.setCellFactory(new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { private HyperlinkWithIcon field; @Override public void updateItem(final ReservedListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { Optional tradableOptional = getTradable(item); if (tradableOptional.isPresent()) { field = new HyperlinkWithIcon(item.getDetails(), AwesomeIcon.INFO_SIGN); field.setOnAction(event -> openDetailPopup(item)); field.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails"))); setGraphic(field); } else { setGraphic(new AutoTooltipLabel(item.getDetails())); } } else { setGraphic(null); if (field != null) field.setOnAction(null); } } }; } }); } private void setAddressColumnCellFactory() { addressColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); addressColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final ReservedListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { String address = item.getAddressString(); setGraphic(new AutoTooltipLabel(address)); } else { setGraphic(null); } } }; } }); } private void setBalanceColumnCellFactory() { balanceColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); balanceColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final ReservedListItem item, boolean empty) { super.updateItem(item, empty); setGraphic((item != null && !empty) ? item.getBalanceLabel() : null); } }; } }); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/transactions/DisplayedTransactions.java ================================================ /* * 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 . */ package haveno.desktop.main.funds.transactions; import haveno.core.trade.Tradable; import haveno.core.xmr.wallet.XmrWalletService; import monero.wallet.model.MoneroTxWallet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; class DisplayedTransactions extends ObservableListDecorator { private final XmrWalletService xmrWalletService; private final TradableRepository tradableRepository; private final TransactionListItemFactory transactionListItemFactory; private final TransactionAwareTradableFactory transactionAwareTradableFactory; DisplayedTransactions(XmrWalletService xmrWalletService, TradableRepository tradableRepository, TransactionListItemFactory transactionListItemFactory, TransactionAwareTradableFactory transactionAwareTradableFactory) { this.xmrWalletService = xmrWalletService; this.tradableRepository = tradableRepository; this.transactionListItemFactory = transactionListItemFactory; this.transactionAwareTradableFactory = transactionAwareTradableFactory; } void update() { List transactionsListItems = getTransactionListItems(); forEach(TransactionsListItem::cleanup); setAll(transactionsListItems); } private List getTransactionListItems() { List transactions = xmrWalletService.getTxs(false); return transactions.stream() .map(this::convertTransactionToListItem) .collect(Collectors.toList()); } private TransactionsListItem convertTransactionToListItem(MoneroTxWallet transaction) { Set tradables = tradableRepository.getAll(); TransactionAwareTradable maybeTradable = tradables.stream() .map(transactionAwareTradableFactory::create) .filter(tradable -> tradable.isRelatedToTransaction(transaction)) .findAny() .orElse(null); return transactionListItemFactory.create(transaction, maybeTradable); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/transactions/DisplayedTransactionsFactory.java ================================================ /* * 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 . */ package haveno.desktop.main.funds.transactions; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.core.xmr.wallet.XmrWalletService; @Singleton public class DisplayedTransactionsFactory { private final XmrWalletService xmrWalletService; private final TradableRepository tradableRepository; private final TransactionListItemFactory transactionListItemFactory; private final TransactionAwareTradableFactory transactionAwareTradableFactory; @Inject DisplayedTransactionsFactory(XmrWalletService xmrWalletService, TradableRepository tradableRepository, TransactionListItemFactory transactionListItemFactory, TransactionAwareTradableFactory transactionAwareTradableFactory) { this.xmrWalletService = xmrWalletService; this.tradableRepository = tradableRepository; this.transactionListItemFactory = transactionListItemFactory; this.transactionAwareTradableFactory = transactionAwareTradableFactory; } DisplayedTransactions create() { return new DisplayedTransactions(xmrWalletService, tradableRepository, transactionListItemFactory, transactionAwareTradableFactory); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/transactions/DummyTransactionAwareTradable.java ================================================ /* * 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 . */ package haveno.desktop.main.funds.transactions; import haveno.core.trade.Tradable; import monero.wallet.model.MoneroTxWallet; class DummyTransactionAwareTradable implements TransactionAwareTradable { private final Tradable delegate; DummyTransactionAwareTradable(Tradable delegate) { this.delegate = delegate; } @Override public boolean isRelatedToTransaction(MoneroTxWallet transaction) { return false; } @Override public Tradable asTradable() { return delegate; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/transactions/ObservableListDecorator.java ================================================ /* * 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 . */ package haveno.desktop.main.funds.transactions; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.transformation.SortedList; import java.util.AbstractList; import java.util.Collection; class ObservableListDecorator extends AbstractList { private final ObservableList delegate = FXCollections.observableArrayList(); SortedList asSortedList() { return new SortedList<>(delegate); } void setAll(Collection elements) { delegate.setAll(elements); } @Override public T get(int index) { return delegate.get(index); } @Override public int size() { return delegate.size(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/transactions/TradableRepository.java ================================================ /* * 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 . */ package haveno.desktop.main.funds.transactions; import com.google.common.collect.ImmutableSet; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.core.offer.OpenOfferManager; import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.Tradable; import haveno.core.trade.TradeManager; import haveno.core.trade.failed.FailedTradesManager; import java.util.Set; @Singleton public class TradableRepository { private final OpenOfferManager openOfferManager; private final TradeManager tradeManager; private final ClosedTradableManager closedTradableManager; private final FailedTradesManager failedTradesManager; @Inject TradableRepository(OpenOfferManager openOfferManager, TradeManager tradeManager, ClosedTradableManager closedTradableManager, FailedTradesManager failedTradesManager) { this.openOfferManager = openOfferManager; this.tradeManager = tradeManager; this.closedTradableManager = closedTradableManager; this.failedTradesManager = failedTradesManager; } Set getAll() { return ImmutableSet.builder() .addAll(openOfferManager.getObservableList()) .addAll(tradeManager.getObservableList()) .addAll(closedTradableManager.getObservableList()) .addAll(failedTradesManager.getObservableList()) .build(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionAwareOpenOffer.java ================================================ /* * 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 . */ package haveno.desktop.main.funds.transactions; import haveno.core.offer.OpenOffer; import haveno.core.trade.Tradable; import monero.wallet.model.MoneroTxWallet; class TransactionAwareOpenOffer implements TransactionAwareTradable { private final OpenOffer openOffer; TransactionAwareOpenOffer(OpenOffer delegate) { this.openOffer = delegate; } @Override public boolean isRelatedToTransaction(MoneroTxWallet transaction) { String txId = transaction.getHash(); return txId.equals(openOffer.getReserveTxHash()); } @Override public Tradable asTradable() { return openOffer; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionAwareTradable.java ================================================ /* * 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 . */ package haveno.desktop.main.funds.transactions; import haveno.core.trade.Tradable; import monero.wallet.model.MoneroTxWallet; interface TransactionAwareTradable { boolean isRelatedToTransaction(MoneroTxWallet transaction); Tradable asTradable(); } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionAwareTradableFactory.java ================================================ /* * 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 . */ package haveno.desktop.main.funds.transactions; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.crypto.PubKeyRingProvider; import haveno.core.offer.OpenOffer; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.refund.RefundManager; import haveno.core.trade.Tradable; import haveno.core.trade.Trade; import haveno.core.xmr.wallet.XmrWalletService; @Singleton public class TransactionAwareTradableFactory { private final ArbitrationManager arbitrationManager; private final RefundManager refundManager; private final XmrWalletService xmrWalletService; private final PubKeyRingProvider pubKeyRingProvider; @Inject TransactionAwareTradableFactory(ArbitrationManager arbitrationManager, RefundManager refundManager, XmrWalletService xmrWalletService, PubKeyRingProvider pubKeyRingProvider) { this.arbitrationManager = arbitrationManager; this.refundManager = refundManager; this.xmrWalletService = xmrWalletService; this.pubKeyRingProvider = pubKeyRingProvider; } TransactionAwareTradable create(Tradable delegate) { if (delegate instanceof OpenOffer) { return new TransactionAwareOpenOffer((OpenOffer) delegate); } else if (delegate instanceof Trade) { return new TransactionAwareTrade((Trade) delegate, arbitrationManager, refundManager, xmrWalletService, pubKeyRingProvider.get()); } else { return new DummyTransactionAwareTradable(delegate); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionAwareTrade.java ================================================ /* * 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 . */ package haveno.desktop.main.funds.transactions; import haveno.common.crypto.PubKeyRing; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.refund.RefundManager; import haveno.core.trade.Tradable; import haveno.core.trade.Trade; import haveno.core.xmr.wallet.XmrWalletService; import javafx.collections.ObservableList; import lombok.extern.slf4j.Slf4j; import monero.wallet.model.MoneroTxWallet; @Slf4j class TransactionAwareTrade implements TransactionAwareTradable { private final Trade trade; private final ArbitrationManager arbitrationManager; private final RefundManager refundManager; private final XmrWalletService xmrWalletService; private final PubKeyRing pubKeyRing; TransactionAwareTrade(Trade trade, ArbitrationManager arbitrationManager, RefundManager refundManager, XmrWalletService xmrWalletService, PubKeyRing pubKeyRing) { this.trade = trade; this.arbitrationManager = arbitrationManager; this.refundManager = refundManager; this.xmrWalletService = xmrWalletService; this.pubKeyRing = pubKeyRing; } @Override public boolean isRelatedToTransaction(MoneroTxWallet transaction) { String txId = transaction.getHash(); boolean isMakerDepositTx = isMakerDepositTx(txId); boolean isTakerDepositTx = isTakerDepositTx(txId); boolean isPayoutTx = isPayoutTx(txId); boolean isDisputedPayoutTx = isDisputedPayoutTx(txId); return isMakerDepositTx || isTakerDepositTx || isPayoutTx || isDisputedPayoutTx; } private boolean isPayoutTx(String txId) { return txId.equals(trade.getPayoutTxId()); } private boolean isMakerDepositTx(String txId) { return txId.equals(trade.getMaker().getDepositTxHash()); } private boolean isTakerDepositTx(String txId) { return txId.equals(trade.getTaker().getDepositTxHash()); } private boolean isDisputedPayoutTx(String txId) { String delegateId = trade.getId(); ObservableList disputes = arbitrationManager.getDisputesAsObservableList(); boolean isAnyDisputeRelatedToThis = arbitrationManager.getDisputedTradeIds().contains(trade.getId()); return isAnyDisputeRelatedToThis && disputes.stream() .anyMatch(dispute -> { String disputePayoutTxId = dispute.getDisputePayoutTxId(); boolean isDisputePayoutTx = txId.equals(disputePayoutTxId); String disputeTradeId = dispute.getTradeId(); boolean isDisputeRelatedToThis = delegateId.equals(disputeTradeId); return isDisputePayoutTx && isDisputeRelatedToThis; }); } // boolean isDelayedPayoutTx(String txId) { // Transaction transaction = btcWalletService.getTransaction(txId); // if (transaction == null) // return false; // // if (transaction.getLockTime() == 0) // return false; // // if (transaction.getInputs() == null) // return false; // // return transaction.getInputs().stream() // .anyMatch(input -> { // TransactionOutput connectedOutput = input.getConnectedOutput(); // if (connectedOutput == null) { // return false; // } // Transaction parentTransaction = connectedOutput.getParentTransaction(); // if (parentTransaction == null) { // return false; // } // return isDepositTx(parentTransaction.getTxId()); // }); // } // // private boolean isRefundPayoutTx(String txId) { // String tradeId = trade.getId(); // ObservableList disputes = refundManager.getDisputesAsObservableList(); // // boolean isAnyDisputeRelatedToThis = refundManager.getDisputedTradeIds().contains(tradeId); // // if (isAnyDisputeRelatedToThis) { // Transaction tx = btcWalletService.getTransaction(txId); // if (tx != null) { // for (TransactionOutput txo : tx.getOutputs()) { // if (btcWalletService.isTransactionOutputMine(txo)) { // try { // Address receiverAddress = txo.getScriptPubKey().getToAddress(btcWalletService.getParams()); // Contract contract = checkNotNull(trade.getContract()); // String myPayoutAddressString = contract.isMyRoleBuyer(pubKeyRing) ? // contract.getBuyerPayoutAddressString() : // contract.getSellerPayoutAddressString(); // if (receiverAddress != null && myPayoutAddressString.equals(receiverAddress.toString())) { // return true; // } // } catch (RuntimeException ignore) { // } // } // } // } // } // return false; // } @Override public Tradable asTradable() { return trade; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionListItemFactory.java ================================================ /* * 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 . */ package haveno.desktop.main.funds.transactions; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.inject.name.Named; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.core.xmr.wallet.XmrWalletService; import javax.annotation.Nullable; import monero.wallet.model.MoneroTxWallet; @Singleton public class TransactionListItemFactory { private final XmrWalletService xmrWalletService; private final CoinFormatter formatter; private final Preferences preferences; @Inject TransactionListItemFactory(XmrWalletService xmrWalletService, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, Preferences preferences) { this.xmrWalletService = xmrWalletService; this.formatter = formatter; this.preferences = preferences; } TransactionsListItem create(MoneroTxWallet transaction, @Nullable TransactionAwareTradable tradable) { return new TransactionsListItem(transaction, xmrWalletService, tradable); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsListItem.java ================================================ /* * 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 . */ package haveno.desktop.main.funds.transactions; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import haveno.core.locale.Res; import haveno.core.offer.Offer; import haveno.core.offer.OpenOffer; import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Tradable; import haveno.core.trade.Trade; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.components.indicator.TxConfidenceIndicator; import haveno.desktop.util.DisplayUtils; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.filtering.FilterableListItem; import javafx.scene.control.Tooltip; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import monero.wallet.model.MoneroIncomingTransfer; import monero.wallet.model.MoneroOutgoingTransfer; import monero.wallet.model.MoneroTxWallet; import javax.annotation.Nullable; import org.apache.commons.lang3.StringUtils; import java.math.BigInteger; import java.util.Date; import java.util.Optional; @Slf4j public class TransactionsListItem implements FilterableListItem { private String dateString; private final Date date; private final String txId; @Nullable private Tradable tradable; private String details = ""; private String addressString = ""; private String direction = ""; private boolean received; private boolean detailsAvailable; private BigInteger amount = BigInteger.ZERO; private BigInteger txFee = BigInteger.ZERO; private String memo = ""; private long confirmations = 0; @Getter private boolean initialTxConfidenceVisibility = true; private final Supplier lazyFieldsSupplier; private XmrWalletService xmrWalletService; @Getter private MoneroTxWallet tx; private static class LazyFields { TxConfidenceIndicator txConfidenceIndicator; Tooltip tooltip; } private LazyFields lazy() { return lazyFieldsSupplier.get(); } // used at exportCSV TransactionsListItem() { date = null; txId = null; lazyFieldsSupplier = null; } TransactionsListItem(MoneroTxWallet tx, XmrWalletService xmrWalletService, TransactionAwareTradable transactionAwareTradable) { this.tx = tx; this.memo = tx.getNote(); this.txId = tx.getHash(); this.xmrWalletService = xmrWalletService; this.confirmations = tx.getNumConfirmations() == null ? 0 : tx.getNumConfirmations(); Optional optionalTradable = Optional.ofNullable(transactionAwareTradable) .map(TransactionAwareTradable::asTradable); BigInteger valueSentToMe = tx.getIncomingAmount() == null ? BigInteger.ZERO : tx.getIncomingAmount(); BigInteger valueSentFromMe = tx.getOutgoingAmount() == null ? BigInteger.ZERO : tx.getOutgoingAmount(); if (tx.getTransfers().get(0).isIncoming()) { addressString = ((MoneroIncomingTransfer) tx.getTransfers().get(0)).getAddress(); } else { MoneroOutgoingTransfer transfer = (MoneroOutgoingTransfer) tx.getTransfers().get(0); if (transfer.getDestinations() != null) addressString = transfer.getDestinations().get(0).getAddress(); else addressString = "unavailable"; } if (valueSentFromMe.compareTo(BigInteger.ZERO) == 0) { amount = valueSentToMe; direction = Res.get("funds.tx.direction.receivedWith"); received = true; } else { amount = valueSentFromMe.multiply(BigInteger.valueOf(-1)); received = false; direction = Res.get("funds.tx.direction.sentTo"); txFee = tx.getFee().multiply(BigInteger.valueOf(-1)); } if (optionalTradable.isPresent()) { tradable = optionalTradable.get(); detailsAvailable = true; String tradeId = tradable.getShortId(); if (tradable instanceof OpenOffer) { details = Res.get("funds.tx.createOfferFee", tradeId); } else if (tradable instanceof Trade) { Trade trade = (Trade) tradable; Offer offer = trade.getOffer(); if (trade.getSelf().getDepositTxHash() != null && trade.getSelf().getDepositTxHash().equals(txId)) { details = Res.get("funds.tx.multiSigDeposit", tradeId); addressString = trade.getProcessModel().getMultisigAddress(); } else if (trade.getPayoutTxId() != null && trade.getPayoutTxId().equals(txId)) { details = Res.get("funds.tx.multiSigPayout", tradeId); if (amount.compareTo(BigInteger.ZERO) == 0) { initialTxConfidenceVisibility = false; } } else { Trade.DisputeState disputeState = trade.getDisputeState(); if (disputeState == Trade.DisputeState.DISPUTE_CLOSED) { if (valueSentToMe.compareTo(BigInteger.ZERO) > 0) { details = Res.get("funds.tx.disputePayout", tradeId); } else { details = Res.get("funds.tx.disputeLost", tradeId); } } else if (disputeState == Trade.DisputeState.REFUND_REQUEST_CLOSED || disputeState == Trade.DisputeState.REFUND_REQUESTED || disputeState == Trade.DisputeState.REFUND_REQUEST_STARTED_BY_PEER) { if (valueSentToMe.compareTo(BigInteger.ZERO) > 0) { details = Res.get("funds.tx.refund", tradeId); } else { // We have spent the deposit tx outputs to the Haveno donation address to enable // the refund process (refund agent -> reimbursement). As the funds have left our wallet // already when funding the deposit tx we show 0 BTC as amount. // Confirmation is not known from the BitcoinJ side (not 100% clear why) as no funds // left our wallet nor we received funds. So we set indicator invisible. amount = BigInteger.ZERO; details = Res.get("funds.tx.collateralForRefund", tradeId); initialTxConfidenceVisibility = false; } } else { details = Res.get("funds.tx.unknown", tradeId); if (trade instanceof ArbitratorTrade) { if (txId.equals(trade.getMaker().getDepositTxHash())) { details = Res.get("funds.tx.makerTradeFee", tradeId); } else if (txId.equals(trade.getTaker().getDepositTxHash())) { details = Res.get("funds.tx.takerTradeFee", tradeId); } } } } } } // get tx date/time Long timestamp = tx.getBlock() == null ? System.currentTimeMillis() : tx.getBlock().getTimestamp() * 1000l; this.date = new Date(timestamp); dateString = DisplayUtils.formatDateTime(date); // confidence lazyFieldsSupplier = Suppliers.memoize(() -> new LazyFields() {{ txConfidenceIndicator = new TxConfidenceIndicator(); txConfidenceIndicator.setId("funds-confidence"); tooltip = new Tooltip(Res.get("shared.notUsedYet")); txConfidenceIndicator.setProgress(0); txConfidenceIndicator.setTooltip(tooltip); txConfidenceIndicator.setVisible(initialTxConfidenceVisibility); GUIUtil.updateConfidence(tx, tooltip, txConfidenceIndicator); confirmations = tx.getNumConfirmations(); }}); } public void cleanup() { } public TxConfidenceIndicator getTxConfidenceIndicator() { return lazy().txConfidenceIndicator; } public final String getDateString() { return dateString; } public String getAmountStr() { return HavenoUtils.formatXmr(amount); } public BigInteger getAmount() { return amount; } public BigInteger getTxFee() { return txFee; } public String getTxFeeStr() { return txFee.equals(BigInteger.ZERO) ? "" : HavenoUtils.formatXmr(txFee); } public String getAddressString() { return addressString; } public String getDirection() { return direction; } public String getTxId() { return txId; } public boolean getReceived() { return received; } public String getDetails() { return details; } public boolean getDetailsAvailable() { return detailsAvailable; } public Date getDate() { return date; } @Nullable public Tradable getTradable() { return tradable; } public long getNumConfirmations() { return confirmations; } public String getMemo() { return memo; } @Override public boolean match(String filterString) { if (filterString.isEmpty()) { return true; } if (StringUtils.containsIgnoreCase(getTxId(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getDetails(), filterString)) { return true; } if (getMemo() != null && StringUtils.containsIgnoreCase(getMemo(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getDirection(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getDateString(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getAmountStr(), filterString)) { return true; } return StringUtils.containsIgnoreCase(getAddressString(), filterString); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java ================================================ /* * 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 . */ package haveno.desktop.main.funds.transactions; import com.google.inject.Inject; import com.googlecode.jcsv.writer.CSVEntryConverter; import de.jensd.fx.fontawesome.AwesomeIcon; import haveno.common.UserThread; import haveno.core.api.XmrConnectionService; import haveno.core.locale.Res; import haveno.core.offer.OpenOffer; import haveno.core.trade.Trade; import haveno.core.user.Preferences; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.AddressWithIconAndDirection; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.HyperlinkWithIcon; import haveno.desktop.components.list.FilterBox; import haveno.desktop.main.overlays.windows.OfferDetailsWindow; import haveno.desktop.main.overlays.windows.TradeDetailsWindow; import haveno.desktop.main.overlays.windows.TxDetailsWindow; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.P2PService; import java.math.BigInteger; import java.util.Comparator; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.geometry.Insets; import javafx.scene.Scene; import javafx.scene.control.Label; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; import javafx.scene.input.KeyEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.stage.Stage; import javafx.util.Callback; import monero.wallet.model.MoneroWalletListener; @FxmlView public class TransactionsView extends ActivatableView { @FXML FilterBox filterBox; @FXML TableView tableView; @FXML TableColumn dateColumn, detailsColumn, addressColumn, transactionColumn, amountColumn, txFeeColumn, confidenceColumn, memoColumn; @FXML Label numItems; @FXML Region spacer; @FXML AutoTooltipButton exportButton; private final DisplayedTransactions displayedTransactions; private final ObservableList observableList = FXCollections.observableArrayList(); private final FilteredList filteredList = new FilteredList<>(observableList); private final SortedList sortedList = new SortedList<>(filteredList); private final XmrWalletService xmrWalletService; private final Preferences preferences; private final TradeDetailsWindow tradeDetailsWindow; private final OfferDetailsWindow offerDetailsWindow; private final TxDetailsWindow txDetailsWindow; private EventHandler keyEventEventHandler; private Scene scene; private final TransactionsUpdater transactionsUpdater = new TransactionsUpdater(); private class TransactionsUpdater extends MoneroWalletListener { @Override public void onNewBlock(long height) { updateList(); } @Override public void onBalancesChanged(BigInteger newBalance, BigInteger newUnlockedBalance) { updateList(); } } /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject private TransactionsView(XmrWalletService xmrWalletService, P2PService p2PService, XmrConnectionService xmrConnectionService, Preferences preferences, TradeDetailsWindow tradeDetailsWindow, OfferDetailsWindow offerDetailsWindow, TxDetailsWindow txDetailsWindow, DisplayedTransactionsFactory displayedTransactionsFactory) { this.xmrWalletService = xmrWalletService; this.preferences = preferences; this.tradeDetailsWindow = tradeDetailsWindow; this.offerDetailsWindow = offerDetailsWindow; this.txDetailsWindow = txDetailsWindow; this.displayedTransactions = displayedTransactionsFactory.create(); updateList(); } @Override public void initialize() { GUIUtil.applyTableStyle(tableView); filterBox.initialize(filteredList, tableView); filterBox.setPromptText(Res.get("shared.filter")); dateColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.dateTime"))); detailsColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.details"))); addressColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.address"))); transactionColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.txId", Res.getBaseCurrencyCode()))); amountColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amountWithCur", Res.getBaseCurrencyCode()))); txFeeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.txFee", Res.getBaseCurrencyCode()))); confidenceColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.confirmations", Res.getBaseCurrencyCode()))); memoColumn.setGraphic(new AutoTooltipLabel(Res.get("funds.tx.memo"))); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN); tableView.setPlaceholder(new AutoTooltipLabel(Res.get("funds.tx.noTxAvailable"))); tableView.getStyleClass().add("non-interactive-table"); setDateColumnCellFactory(); setDetailsColumnCellFactory(); setAddressColumnCellFactory(); setTransactionColumnCellFactory(); setAmountColumnCellFactory(); setTxFeeColumnCellFactory(); setConfidenceColumnCellFactory(); setMemoColumnCellFactory(); dateColumn.setComparator(Comparator.comparing(TransactionsListItem::getDate)); detailsColumn.setComparator((o1, o2) -> { String id1 = !o1.getDetails().isEmpty() ? o1.getDetails() : o1.getTradable() != null ? o1.getTradable().getId() : o1.getTxId(); String id2 = !o2.getDetails().isEmpty() ? o2.getDetails() : o2.getTradable() != null ? o2.getTradable().getId() : o2.getTxId(); return id1.compareTo(id2); }); addressColumn.setComparator(Comparator.comparing(item -> item.getDirection() + item.getAddressString())); transactionColumn.setComparator(Comparator.comparing(TransactionsListItem::getTxId)); amountColumn.setComparator(Comparator.comparing(TransactionsListItem::getAmount)); confidenceColumn.setComparator(Comparator.comparingLong(TransactionsListItem::getNumConfirmations)); memoColumn.setComparator(Comparator.comparing(TransactionsListItem::getMemo, Comparator.nullsLast(Comparator.naturalOrder()))); dateColumn.setSortType(TableColumn.SortType.DESCENDING); tableView.getSortOrder().add(dateColumn); keyEventEventHandler = event -> { // unused }; HBox.setHgrow(spacer, Priority.ALWAYS); numItems.setId("num-offers"); numItems.setPadding(new Insets(-5, 0, 0, 10)); exportButton.updateText(Res.get("shared.exportCSV")); } @Override protected void activate() { sortedList.comparatorProperty().bind(tableView.comparatorProperty()); tableView.setItems(sortedList); updateList(); filterBox.initializeWithCallback(filteredList, tableView, () -> numItems.setText(Res.get("shared.numItemsLabel", sortedList.size()))); filterBox.activate(); xmrWalletService.addWalletListener(transactionsUpdater); scene = root.getScene(); if (scene != null) scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); exportButton.setOnAction(event -> { final ObservableList> tableColumns = GUIUtil.getContentColumns(tableView); final int reportColumns = tableColumns.size(); CSVEntryConverter headerConverter = item -> { String[] columns = new String[reportColumns]; for (int i = 0; i < columns.length; i++) columns[i] = ((AutoTooltipLabel) tableColumns.get(i).getGraphic()).getText(); return columns; }; CSVEntryConverter contentConverter = item -> { String[] columns = new String[reportColumns]; columns[0] = item.getDateString(); columns[1] = item.getDetails(); columns[2] = item.getDirection() + " " + item.getAddressString(); columns[3] = item.getTxId(); columns[4] = item.getAmountStr(); columns[5] = item.getTxFeeStr(); columns[6] = String.valueOf(item.getNumConfirmations()); columns[7] = item.getMemo() == null ? "" : item.getMemo(); return columns; }; GUIUtil.exportCSV("transactions.csv", headerConverter, contentConverter, new TransactionsListItem(), sortedList, (Stage) root.getScene().getWindow()); }); } @Override protected void deactivate() { filterBox.deactivate(); sortedList.comparatorProperty().unbind(); xmrWalletService.removeWalletListener(transactionsUpdater); if (scene != null) scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); exportButton.setOnAction(null); } private void updateList() { displayedTransactions.update(); UserThread.execute((() -> { observableList.setAll(displayedTransactions); })); } private void openTxInBlockExplorer(TransactionsListItem item) { if (item.getTxId() != null) GUIUtil.openWebPage(preferences.getBlockChainExplorer().txUrl + item.getTxId(), false); } private void openDetailPopup(TransactionsListItem item) { if (item.getTradable() instanceof OpenOffer) offerDetailsWindow.show(item.getTradable().getOffer()); else if (item.getTradable() instanceof Trade) tradeDetailsWindow.show((Trade) item.getTradable()); } private void openTxDetailPopup(TransactionsListItem item) { txDetailsWindow.show(item); } /////////////////////////////////////////////////////////////////////////////////////////// // ColumnCellFactories /////////////////////////////////////////////////////////////////////////////////////////// private void setDateColumnCellFactory() { dateColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); dateColumn.setMaxWidth(200); dateColumn.setMinWidth(dateColumn.getMaxWidth()); dateColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final TransactionsListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setGraphic(new AutoTooltipLabel(item.getDateString())); } else { setGraphic(null); } } }; } }); } private void setDetailsColumnCellFactory() { detailsColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); detailsColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { private HyperlinkWithIcon hyperlinkWithIcon; @Override public void updateItem(final TransactionsListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { if (item.getDetailsAvailable()) { hyperlinkWithIcon = new HyperlinkWithIcon(item.getDetails(), AwesomeIcon.INFO_SIGN); hyperlinkWithIcon.setOnAction(event -> openDetailPopup(item)); hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails"))); setGraphic(hyperlinkWithIcon); // If details are available its a trade tx and we don't expect any dust attack tx } else { setGraphic(new AutoTooltipLabel(item.getDetails())); } } else { setGraphic(null); if (hyperlinkWithIcon != null) hyperlinkWithIcon.setOnAction(null); } } }; } }); } private void setAddressColumnCellFactory() { addressColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); addressColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { private AddressWithIconAndDirection field; @Override public void updateItem(final TransactionsListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { String addressString = item.getAddressString(); field = new AddressWithIconAndDirection(item.getDirection(), addressString, item.getReceived()); field.setTooltip(new Tooltip(Res.get("tooltip.openBlockchainForAddress", addressString))); setGraphic(field); } else { setGraphic(null); if (field != null) field.setOnAction(null); } } }; } }); } private void setTransactionColumnCellFactory() { transactionColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); transactionColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { private HyperlinkWithIcon hyperlinkWithIcon; @Override public void updateItem(final TransactionsListItem item, boolean empty) { super.updateItem(item, empty); //noinspection Duplicates if (item != null && !empty) { String transactionId = item.getTxId(); hyperlinkWithIcon = new HyperlinkWithIcon(transactionId, AwesomeIcon.INFO_SIGN); hyperlinkWithIcon.setOnAction(event -> openTxDetailPopup(item)); hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("txDetailsWindow.headline"))); setGraphic(hyperlinkWithIcon); } else { setGraphic(null); if (hyperlinkWithIcon != null) hyperlinkWithIcon.setOnAction(null); } } }; } }); } private void setAmountColumnCellFactory() { amountColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); amountColumn.getStyleClass().add("highlight-text"); amountColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final TransactionsListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setGraphic(new AutoTooltipLabel(item.getAmountStr())); } else { setGraphic(null); } } }; } }); } private void setTxFeeColumnCellFactory() { txFeeColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); txFeeColumn.getStyleClass().add("highlight-text"); txFeeColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final TransactionsListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setGraphic(new AutoTooltipLabel(item.getTxFeeStr())); } else { setGraphic(null); } } }; } }); } private void setMemoColumnCellFactory() { memoColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); memoColumn.getStyleClass().add("highlight-text"); memoColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final TransactionsListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setGraphic(new AutoTooltipLabel(item.getMemo())); } else { setGraphic(null); } } }; } }); } private void setConfidenceColumnCellFactory() { confidenceColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); confidenceColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final TransactionsListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setGraphic(item.getTxConfidenceIndicator()); } else { setGraphic(null); } } }; } }); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalListItem.java ================================================ /* * 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 . */ package haveno.desktop.main.funds.withdrawal; import haveno.common.UserThread; import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; import haveno.core.util.coin.CoinFormatter; import haveno.core.xmr.listeners.XmrBalanceListener; import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.components.AutoTooltipLabel; import javafx.scene.control.Label; import lombok.Getter; import lombok.Setter; import java.math.BigInteger; class WithdrawalListItem { private final XmrBalanceListener balanceListener; private final Label balanceLabel; private final XmrAddressEntry addressEntry; private final XmrWalletService walletService; private final CoinFormatter formatter; private BigInteger balance; private final String addressString; @Setter @Getter private boolean isSelected; public WithdrawalListItem(XmrAddressEntry addressEntry, XmrWalletService walletService, CoinFormatter formatter) { this.addressEntry = addressEntry; this.walletService = walletService; this.formatter = formatter; addressString = addressEntry.getAddressString(); // balance balanceLabel = new AutoTooltipLabel(); balanceListener = new XmrBalanceListener(addressEntry.getSubaddressIndex()) { @Override public void onBalanceChanged(BigInteger balance) { updateBalance(); } }; walletService.addBalanceListener(balanceListener); updateBalance(); } public void cleanup() { walletService.removeBalanceListener(balanceListener); } private void updateBalance() { balance = walletService.getBalanceForSubaddress(addressEntry.getSubaddressIndex()); if (balance != null) { UserThread.execute(() -> balanceLabel.setText(HavenoUtils.formatXmr(this.balance))); } } public final String getLabel() { if (addressEntry.isOpenOffer()) return Res.getWithCol("shared.offerId") + " " + addressEntry.getShortOfferId(); else if (addressEntry.isTradePayout()) return Res.getWithCol("shared.tradeId") + " " + addressEntry.getShortOfferId(); else if (addressEntry.getContext() == XmrAddressEntry.Context.ARBITRATOR) return Res.get("funds.withdrawal.arbitrationFee"); else return "-"; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof WithdrawalListItem)) return false; WithdrawalListItem that = (WithdrawalListItem) o; return addressEntry.equals(that.addressEntry); } @Override public int hashCode() { return addressEntry.hashCode(); } public XmrAddressEntry getAddressEntry() { return addressEntry; } public Label getBalanceLabel() { return balanceLabel; } public BigInteger getBalance() { return balance; } public String getAddressString() { return addressString; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.desktop.main.funds.withdrawal; import com.google.inject.Inject; import haveno.common.util.Tuple3; import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; import haveno.core.trade.TradeManager; import haveno.core.trade.protocol.TradeProtocol; import haveno.core.util.validation.BtcAddressValidator; import haveno.core.xmr.listeners.XmrBalanceListener; import haveno.core.xmr.setup.WalletsSetup; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.BusyAnimation; import haveno.desktop.components.HyperlinkWithIcon; import haveno.desktop.components.TitledGroupBg; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.TxWithdrawWindow; import haveno.desktop.main.overlays.windows.WalletPasswordWindow; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.P2PService; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.control.Button; import javafx.scene.layout.GridPane; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import monero.common.MoneroRpcConnection; import monero.common.MoneroUtils; import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxWallet; import java.math.BigInteger; import java.util.Arrays; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import static haveno.desktop.util.FormBuilder.addTopLabelInputTextField; import static haveno.desktop.util.FormBuilder.addButton; @FxmlView public class WithdrawalView extends ActivatableView { @FXML private GridPane gridPane; private BusyAnimation spinningWheel; private StackPane overlayPane; private Label amountLabel; private TextField amountTextField, withdrawToTextField, withdrawMemoTextField; private final XmrWalletService xmrWalletService; private final TradeManager tradeManager; private final P2PService p2PService; private final WalletPasswordWindow walletPasswordWindow; private XmrBalanceListener balanceListener; private BigInteger amount = BigInteger.ZERO; private ChangeListener amountListener; private ChangeListener amountFocusListener; private int rowIndex = 0; boolean sendMax = false; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject private WithdrawalView(XmrWalletService xmrWalletService, TradeManager tradeManager, P2PService p2PService, WalletsSetup walletsSetup, BtcAddressValidator btcAddressValidator, WalletPasswordWindow walletPasswordWindow) { this.xmrWalletService = xmrWalletService; this.tradeManager = tradeManager; this.p2PService = p2PService; this.walletPasswordWindow = walletPasswordWindow; } @Override public void initialize() { spinningWheel = new BusyAnimation(); overlayPane = new StackPane(); overlayPane.setStyle("-fx-background-color: transparent;"); // Adjust opacity as needed overlayPane.setVisible(false); overlayPane.getChildren().add(spinningWheel); // Add overlay pane to root VBox root.getChildren().add(overlayPane); final TitledGroupBg titledGroupBg = addTitledGroupBg(gridPane, rowIndex, 4, Res.get("funds.deposit.withdrawFromWallet")); titledGroupBg.getStyleClass().add("last"); withdrawToTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("funds.withdrawal.toLabel", Res.getBaseCurrencyCode())).second; final Tuple3 feeTuple3 = FormBuilder.addTopLabelTextFieldHyperLink(gridPane, ++rowIndex, "", Res.get("funds.withdrawal.receiverAmount", Res.getBaseCurrencyCode()), Res.get("funds.withdrawal.sendMax"), 0); amountLabel = feeTuple3.first; amountTextField = feeTuple3.second; amountTextField.setMinWidth(225); HyperlinkWithIcon sendMaxLink = feeTuple3.third; withdrawMemoTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("funds.withdrawal.memoLabel", Res.getBaseCurrencyCode())).second; final Button withdrawButton = addButton(gridPane, ++rowIndex, Res.get("funds.withdrawal.withdrawButton"), 15); withdrawButton.setOnAction(event -> { // Show the spinning wheel (progress indicator) showLoadingIndicator(); // Execute onWithdraw() method on a separate thread new Thread(() -> { // Call the method that performs the withdrawal onWithdraw(); // Hide the spinning wheel (progress indicator) after withdrawal is complete Platform.runLater(() -> hideLoadingIndicator()); }).start(); }); sendMaxLink.setOnAction(event -> { sendMax = true; amount = null; // set amount when tx created amountTextField.setText(Res.get("funds.withdrawal.maximum")); }); balanceListener = new XmrBalanceListener() { @Override public void onBalanceChanged(BigInteger balance) { } }; amountListener = (observable, oldValue, newValue) -> { if (amountTextField.focusedProperty().get()) { sendMax = false; // disable max if amount changed while focused try { amount = HavenoUtils.parseXmr(amountTextField.getText()); } catch (Throwable t) { log.error("Error at amountTextField input. " + t.toString()); } } }; amountFocusListener = (observable, oldValue, newValue) -> { // parse amount on focus out unless sending max if (oldValue && !newValue && !sendMax) { if (amount.compareTo(BigInteger.ZERO) > 0) amountTextField.setText(HavenoUtils.formatXmr(amount)); else amountTextField.setText(""); } }; amountLabel.setText(Res.get("funds.withdrawal.receiverAmount")); } private void showLoadingIndicator() { overlayPane.setVisible(true); spinningWheel.play(); root.setDisable(true); } private void hideLoadingIndicator() { overlayPane.setVisible(false); spinningWheel.stop(); root.setDisable(false); } @Override protected void activate() { reset(); amountTextField.textProperty().addListener(amountListener); amountTextField.focusedProperty().addListener(amountFocusListener); xmrWalletService.addBalanceListener(balanceListener); GUIUtil.requestFocus(withdrawToTextField); } @Override protected void deactivate() { spinningWheel.stop(); xmrWalletService.removeBalanceListener(balanceListener); amountTextField.textProperty().removeListener(amountListener); amountTextField.focusedProperty().removeListener(amountFocusListener); } /////////////////////////////////////////////////////////////////////////////////////////// // UI handlers /////////////////////////////////////////////////////////////////////////////////////////// private void onWithdraw() { if (GUIUtil.isReadyForTxBroadcastOrShowPopup(xmrWalletService)) { try { // collect tx fields to local variables String withdrawToAddress = withdrawToTextField.getText(); boolean sendMax = this.sendMax; BigInteger amount = this.amount; // validate address if (!MoneroUtils.isValidAddress(withdrawToAddress, XmrWalletService.getMoneroNetworkType())) { throw new IllegalArgumentException(Res.get("validation.xmr.invalidAddress")); } // set max amount if requested if (sendMax) amount = xmrWalletService.getAvailableBalance(); // check sufficient available balance if (amount.compareTo(BigInteger.ZERO) <= 0) throw new RuntimeException(Res.get("portfolio.pending.step5_buyer.amountTooLow")); // create tx MoneroTxWallet tx = null; for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { MoneroRpcConnection sourceConnection = xmrWalletService.getXmrConnectionService().getConnection(); try { log.info("Creating withdraw tx"); long startTime = System.currentTimeMillis(); tx = xmrWalletService.createTx(new MoneroTxConfig() .setAccountIndex(0) .setAmount(amount) .setAddress(withdrawToAddress) .setSubtractFeeFrom(sendMax ? Arrays.asList(0) : null)); log.info("Done creating withdraw tx in {} ms", System.currentTimeMillis() - startTime); break; } catch (Exception e) { if (isNotEnoughMoney(e.getMessage())) throw e; log.warn("Error creating creating withdraw tx, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (xmrWalletService.getXmrConnectionService().isConnected()) xmrWalletService.requestSwitchToNextBestConnection(sourceConnection); HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } } // popup confirmation message popupConfirmationMessage(tx); } catch (Throwable e) { e.printStackTrace(); if (isNotEnoughMoney(e.getMessage())) new Popup().warning(Res.get("funds.withdrawal.notEnoughFunds")).show(); else new Popup().warning(e.getMessage()).show(); } } } private static boolean isNotEnoughMoney(String errorMsg) { return errorMsg.contains("not enough"); } private void popupConfirmationMessage(MoneroTxWallet tx) { // create confirmation message String withdrawToAddress = tx.getOutgoingTransfer().getDestinations().get(0).getAddress(); BigInteger receiverAmount = tx.getOutgoingTransfer().getDestinations().get(0).getAmount(); BigInteger fee = tx.getFee(); String messageText = Res.get("shared.sendFundsDetailsWithFee", HavenoUtils.formatXmr(receiverAmount, true), withdrawToAddress, HavenoUtils.formatXmr(fee, true)); // popup confirmation message Popup popup = new Popup(); popup.headLine(Res.get("funds.withdrawal.confirmWithdrawalRequest")) .confirmation(messageText) .actionButtonText(Res.get("shared.yes")) .onAction(() -> { if (xmrWalletService.isWalletEncrypted()) { walletPasswordWindow.headLine(Res.get("walletPasswordWindow.headline")).onSuccess(() -> { relayTx(tx, withdrawToAddress, receiverAmount, fee); }).onClose(() -> { popup.hide(); }).hideForgotPasswordButton().show(); } else { relayTx(tx, withdrawToAddress, receiverAmount, fee); } }) .closeButtonText(Res.get("shared.cancel")) .show(); } private void relayTx(MoneroTxWallet tx, String withdrawToAddress, BigInteger receiverAmount, BigInteger fee) { try { xmrWalletService.getWallet().relayTx(tx); xmrWalletService.getWallet().setTxNote(tx.getHash(), withdrawMemoTextField.getText()); // TODO (monero-java): tx note does not persist when tx created then relayed new TxWithdrawWindow(tx.getHash(), withdrawToAddress, HavenoUtils.formatXmr(receiverAmount, true), HavenoUtils.formatXmr(fee, true), xmrWalletService.getWallet().getTxNote(tx.getHash())) .show(); log.debug("onWithdraw onSuccess tx ID:{}", tx.getHash()); } catch (Exception e) { e.printStackTrace(); new Popup().warning(e.getMessage()).show(); } } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void reset() { sendMax = false; amount = BigInteger.ZERO; amountTextField.setText(""); amountTextField.setPromptText(Res.get("funds.withdrawal.setAmount")); withdrawToTextField.setText(""); withdrawToTextField.setPromptText(Res.get("funds.withdrawal.fillDestAddress")); withdrawMemoTextField.setText(""); withdrawMemoTextField.setPromptText(Res.get("funds.withdrawal.memo")); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/market/MarketView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/market/MarketView.java ================================================ /* * 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 . */ package haveno.desktop.main.market; import com.google.common.base.Joiner; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.util.Utilities; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.offer.OfferPayload; import haveno.core.trade.HavenoUtils; import haveno.core.trade.statistics.TradeStatistics3; import haveno.core.trade.statistics.TradeStatistics3StorageService; import haveno.core.util.FormattingUtils; import haveno.core.util.VolumeUtil; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.Navigation; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.common.view.CachingViewLoader; import haveno.desktop.common.view.FxmlView; import haveno.desktop.common.view.View; import haveno.desktop.common.view.ViewLoader; import haveno.desktop.main.MainView; import haveno.desktop.main.market.offerbook.OfferBookChartView; import haveno.desktop.main.market.spread.SpreadView; import haveno.desktop.main.market.spread.SpreadViewPaymentMethod; import haveno.desktop.main.market.trades.TradesChartsView; import haveno.desktop.main.offer.offerbook.OfferBook; import haveno.desktop.main.offer.offerbook.OfferBookListItem; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.DisplayUtils; import java.util.List; import java.util.stream.Collectors; import javafx.beans.value.ChangeListener; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.scene.Scene; import javafx.scene.control.ScrollPane; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import org.apache.commons.lang3.StringUtils; @FxmlView public class MarketView extends ActivatableView { @FXML Tab offerBookTab, tradesTab, spreadTab, spreadTabPaymentMethod; private final ViewLoader viewLoader; private final TradeStatistics3StorageService tradeStatistics3StorageService; private final OfferBook offerBook; private final CoinFormatter formatter; private final Navigation navigation; private Navigation.Listener navigationListener; private ChangeListener tabChangeListener; private EventHandler keyEventEventHandler; private Scene scene; @Inject public MarketView(CachingViewLoader viewLoader, TradeStatistics3StorageService tradeStatistics3StorageService, OfferBook offerBook, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, Navigation navigation) { this.viewLoader = viewLoader; this.tradeStatistics3StorageService = tradeStatistics3StorageService; this.offerBook = offerBook; this.formatter = formatter; this.navigation = navigation; } @Override public void initialize() { offerBookTab.setText(Res.get("market.tabs.offerBook")); spreadTab.setText(Res.get("market.tabs.spreadCurrency")); spreadTabPaymentMethod.setText(Res.get("market.tabs.spreadPayment")); tradesTab.setText(Res.get("market.tabs.trades")); navigationListener = (viewPath, data) -> { if (viewPath.size() == 3 && viewPath.indexOf(MarketView.class) == 1) loadView(viewPath.tip()); }; tabChangeListener = (ov, oldValue, newValue) -> { if (newValue == offerBookTab) navigation.navigateTo(MainView.class, MarketView.class, OfferBookChartView.class); else if (newValue == tradesTab) navigation.navigateTo(MainView.class, MarketView.class, TradesChartsView.class); else if (newValue == spreadTab) navigation.navigateTo(MainView.class, MarketView.class, SpreadView.class); else if (newValue == spreadTabPaymentMethod) navigation.navigateTo(MainView.class, MarketView.class, SpreadViewPaymentMethod.class); }; keyEventEventHandler = keyEvent -> { if (Utilities.isCtrlPressed(KeyCode.T, keyEvent)) { String allTradesWithReferralId = getAllTradesWithReferralId(); new Popup().message(StringUtils.abbreviate(allTradesWithReferralId, 600)) .actionButtonText(Res.get("shared.copyToClipboard")) .onAction(() -> Utilities.copyToClipboard(allTradesWithReferralId)) .show(); } else if (Utilities.isCtrlPressed(KeyCode.O, keyEvent)) { String allOffersWithReferralId = getAllOffersWithReferralId(); new Popup().message(StringUtils.abbreviate(allOffersWithReferralId, 600)) .actionButtonText(Res.get("shared.copyToClipboard")) .onAction(() -> Utilities.copyToClipboard(allOffersWithReferralId)) .show(); } }; } @Override protected void activate() { root.getSelectionModel().selectedItemProperty().addListener(tabChangeListener); navigation.addListener(navigationListener); if (root.getSelectionModel().getSelectedItem() == offerBookTab) navigation.navigateTo(MainView.class, MarketView.class, OfferBookChartView.class); else if (root.getSelectionModel().getSelectedItem() == tradesTab) navigation.navigateTo(MainView.class, MarketView.class, TradesChartsView.class); else if (root.getSelectionModel().getSelectedItem() == spreadTab) navigation.navigateTo(MainView.class, MarketView.class, SpreadView.class); else navigation.navigateTo(MainView.class, MarketView.class, SpreadViewPaymentMethod.class); if (root.getScene() != null) { scene = root.getScene(); scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); } } @Override protected void deactivate() { root.getSelectionModel().selectedItemProperty().removeListener(tabChangeListener); navigation.removeListener(navigationListener); // root.getScene() is null already so we used a field property if (scene != null) scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); } private void loadView(Class viewClass) { final Tab tab; View view = viewLoader.load(viewClass); if (view instanceof OfferBookChartView) tab = offerBookTab; else if (view instanceof TradesChartsView) tab = tradesTab; else if (view instanceof SpreadViewPaymentMethod) tab = spreadTabPaymentMethod; else if (view instanceof SpreadView) tab = spreadTab; else throw new IllegalArgumentException("Navigation to " + viewClass + " is not supported"); if (tab.getContent() != null && tab.getContent() instanceof ScrollPane) { ((ScrollPane) tab.getContent()).setContent(view.getRoot()); } else { tab.setContent(view.getRoot()); } root.getSelectionModel().select(tab); } private String getAllTradesWithReferralId() { // We don't use the list from the tradeStatisticsManager as that has filtered the duplicates but we want to get // all items of both traders in case the referral ID was only set by one trader. // If both traders had set it the tradeStatistics is only delivered once. // If both traders used a different referral ID then we would get 2 objects. List list = tradeStatistics3StorageService.getMapOfAllData().values().stream() .filter(e -> e instanceof TradeStatistics3) .map(e -> (TradeStatistics3) e) .filter(tradeStatistics3 -> tradeStatistics3.getExtraDataMap() != null) .filter(tradeStatistics3 -> tradeStatistics3.getExtraDataMap().get(OfferPayload.REFERRAL_ID) != null) .map(tradeStatistics3 -> { StringBuilder sb = new StringBuilder(); sb.append("Date: ").append(DisplayUtils.formatDateTime(tradeStatistics3.getDate())).append("\n") .append("Market: ").append(CurrencyUtil.getCurrencyPair(tradeStatistics3.getCurrency())).append("\n") .append("Price: ").append(FormattingUtils.formatPrice(tradeStatistics3.getTradePrice())).append("\n") .append("Amount: ").append(HavenoUtils.formatXmr(tradeStatistics3.getTradeAmount())).append("\n") .append("Volume: ").append(VolumeUtil.formatVolume(tradeStatistics3.getTradeVolume())).append("\n") .append("Payment method: ").append(Res.get(tradeStatistics3.getPaymentMethodId())).append("\n") .append("ReferralID: ").append(tradeStatistics3.getExtraDataMap().get(OfferPayload.REFERRAL_ID)); return sb.toString(); }) .collect(Collectors.toList()); return Joiner.on("\n\n").join(list); } private String getAllOffersWithReferralId() { synchronized (offerBook.getOfferBookListItems()) { List list = offerBook.getOfferBookListItems().stream() .map(OfferBookListItem::getOffer) .filter(offer -> offer.getExtraDataMap() != null) .filter(offer -> offer.getExtraDataMap().get(OfferPayload.REFERRAL_ID) != null) .map(offer -> { StringBuilder sb = new StringBuilder(); sb.append("Offer ID: ").append(offer.getId()).append("\n") .append("Type: ").append(offer.getDirection().name()).append("\n") .append("Market: ").append(CurrencyUtil.getCurrencyPair(offer.getCounterCurrencyCode())).append("\n") .append("Price: ").append(FormattingUtils.formatPrice(offer.getPrice())).append("\n") .append("Amount: ").append(DisplayUtils.formatAmount(offer, formatter)).append(" BTC\n") .append("Payment method: ").append(Res.get(offer.getPaymentMethod().getId())).append("\n") .append("ReferralID: ").append(offer.getExtraDataMap().get(OfferPayload.REFERRAL_ID)); return sb.toString(); }) .collect(Collectors.toList()); return Joiner.on("\n\n").join(list); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartView.java ================================================ /* * 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 . */ package haveno.desktop.main.market.offerbook; import com.google.inject.Inject; import com.google.inject.name.Named; import com.jfoenix.controls.JFXTabPane; import haveno.common.UserThread; import haveno.common.config.Config; import haveno.common.util.Tuple3; import haveno.common.util.Tuple4; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.monetary.Volume; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.util.FormattingUtils; import haveno.core.util.VolumeUtil; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.common.view.ActivatableViewAndModel; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.AutoTooltipTableColumn; import haveno.desktop.components.AutocompleteComboBox; import haveno.desktop.components.ColoredDecimalPlacesWithZerosText; import haveno.desktop.components.PeerInfoIconSmall; import haveno.desktop.main.offer.offerbook.OfferBookListItem; import haveno.desktop.util.CurrencyListItem; import haveno.desktop.util.DisplayUtils; import static haveno.desktop.util.FormBuilder.addTopLabelAutocompleteComboBox; import haveno.desktop.util.GUIUtil; import static haveno.desktop.util.Layout.INITIAL_WINDOW_HEIGHT; import haveno.network.p2p.NodeAddress; import java.text.DecimalFormat; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.Collectors; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.geometry.Side; import javafx.scene.chart.AreaChart; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.ContentDisplay; import javafx.scene.control.Label; import javafx.scene.control.SingleSelectionModel; import javafx.scene.control.Tab; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.util.Callback; import javafx.util.StringConverter; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; @FxmlView public class OfferBookChartView extends ActivatableViewAndModel { private final boolean useDevPrivilegeKeys; private NumberAxis xAxis; private XYChart.Series seriesBuy, seriesSell; private final CoinFormatter formatter; private TableView buyOfferTableView; private TableView sellOfferTableView; private AreaChart areaChart; private AnchorPane chartPane; private AutocompleteComboBox currencyComboBox; private Subscription tradeCurrencySubscriber; private final StringProperty volumeSellColumnLabel = new SimpleStringProperty(); private final StringProperty volumeBuyColumnLabel = new SimpleStringProperty(); private final StringProperty amountSellColumnLabel = new SimpleStringProperty(); private final StringProperty amountBuyColumnLabel = new SimpleStringProperty(); private final StringProperty priceColumnLabel = new SimpleStringProperty(); private AutoTooltipButton sellButton; private AutoTooltipButton buyButton; private ChangeListener selectedTabIndexListener; private SingleSelectionModel tabPaneSelectionModel; private Label sellHeaderLabel, buyHeaderLabel; private ChangeListener sellTableRowSelectionListener, buyTableRowSelectionListener; private ListChangeListener changeListener; private ListChangeListener currencyListItemsListener; private final double dataLimitFactor = 3; private final double initialOfferTableViewHeight = 78; // decrease as MainView's content-pane's top anchor increases private final double offerTableExtraMarginBottom = 0; private final Function offerTableViewHeight = (screenSize) -> { // initial visible row count=5, header height=30 double pixelsPerOfferTableRow = (initialOfferTableViewHeight - offerTableExtraMarginBottom) / 5.0; int extraRows = screenSize <= INITIAL_WINDOW_HEIGHT ? 0 : (int) ((screenSize - INITIAL_WINDOW_HEIGHT) / pixelsPerOfferTableRow); return extraRows == 0 ? initialOfferTableViewHeight : Math.ceil(initialOfferTableViewHeight + ((extraRows + 1) * pixelsPerOfferTableRow)); }; private ChangeListener havenoWindowVerticalSizeListener; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject public OfferBookChartView(OfferBookChartViewModel model, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { super(model); this.formatter = formatter; this.useDevPrivilegeKeys = useDevPrivilegeKeys; } @Override public void initialize() { createListener(); final Tuple3> currencyComboBoxTuple = addTopLabelAutocompleteComboBox(Res.get("shared.currency"), 0); this.currencyComboBox = currencyComboBoxTuple.third; this.currencyComboBox.setCellFactory(GUIUtil.getCurrencyListItemCellFactory(Res.get("shared.oneOffer"), Res.get("shared.multipleOffers"), model.preferences)); this.currencyComboBox.getStyleClass().add("input-with-border"); createChart(); VBox.setMargin(chartPane, new Insets(0, 0, 5, 0)); Tuple4, VBox, Button, Label> tupleBuy = getOfferTable(OfferDirection.BUY); Tuple4, VBox, Button, Label> tupleSell = getOfferTable(OfferDirection.SELL); buyOfferTableView = tupleBuy.first; sellOfferTableView = tupleSell.first; sellButton = (AutoTooltipButton) tupleBuy.third; buyButton = (AutoTooltipButton) tupleSell.third; sellHeaderLabel = tupleBuy.fourth; buyHeaderLabel = tupleSell.fourth; HBox bottomHBox = new HBox(); bottomHBox.setSpacing(20); //30 bottomHBox.setAlignment(Pos.CENTER); VBox.setMargin(bottomHBox, new Insets(-5, 0, 0, 0)); HBox.setHgrow(tupleBuy.second, Priority.ALWAYS); HBox.setHgrow(tupleSell.second, Priority.ALWAYS); tupleBuy.second.setUserData(OfferDirection.BUY.name()); tupleSell.second.setUserData(OfferDirection.SELL.name()); bottomHBox.getChildren().addAll(tupleSell.second, tupleBuy.second); root.getChildren().addAll(currencyComboBoxTuple.first, chartPane, bottomHBox); } @Override protected void activate() { // root.getParent() is null at initialize tabPaneSelectionModel = GUIUtil.getParentOfType(root, JFXTabPane.class).getSelectionModel(); selectedTabIndexListener = (observable, oldValue, newValue) -> model.setSelectedTabIndex((int) newValue); model.setSelectedTabIndex(tabPaneSelectionModel.getSelectedIndex()); tabPaneSelectionModel.selectedIndexProperty().addListener(selectedTabIndexListener); currencyComboBox.setConverter(new CurrencyListItemStringConverter(currencyComboBox)); currencyComboBox.getEditor().getStyleClass().add("combo-box-editor-bold"); currencyComboBox.setAutocompleteItems(model.getCurrencyListItems()); currencyComboBox.setVisibleRowCount(10); if (model.getSelectedCurrencyListItem().isPresent()) { CurrencyListItem selectedItem = model.getSelectedCurrencyListItem().get(); currencyComboBox.getSelectionModel().select(model.getSelectedCurrencyListItem().get()); currencyComboBox.getEditor().setText(new CurrencyListItemStringConverter(currencyComboBox).toString(selectedItem)); } currencyComboBox.setOnChangeConfirmed(e -> { CurrencyListItem selectedItem = currencyComboBox.getSelectionModel().getSelectedItem(); if (selectedItem != null) { model.onSetTradeCurrency(selectedItem.tradeCurrency); UserThread.execute(() -> updateChartData()); } }); model.currencyListItems.getObservableList().addListener(currencyListItemsListener); model.getOfferBookListItems().addListener(changeListener); tradeCurrencySubscriber = EasyBind.subscribe(model.selectedTradeCurrencyProperty, tradeCurrency -> { String code = tradeCurrency.getCode(); xAxis.setTickLabelFormatter(new StringConverter<>() { final int cryptoPrecision = 3; final DecimalFormat df = new DecimalFormat(",###"); @Override public String toString(Number object) { try { final double doubleValue = (double) object; if (CurrencyUtil.isCryptoCurrency(model.getCurrencyCode())) { final String withCryptoPrecision = FormattingUtils.formatRoundedDoubleWithPrecision(doubleValue, cryptoPrecision); if (withCryptoPrecision.startsWith("0.0")) { return FormattingUtils.formatRoundedDoubleWithPrecision(doubleValue, 8).replaceFirst("0+$", ""); } else { return withCryptoPrecision.replaceFirst("0+$", ""); } } else { return df.format(Double.parseDouble(FormattingUtils.formatRoundedDoubleWithPrecision(doubleValue, 0))); } } catch (IllegalArgumentException e) { log.error("Error converting number to string, tradeCurrency={}, number={}\n", code, object, e); return "NaN"; // TODO: occasionally getting invalid number } } @Override public Number fromString(String string) { return null; } }); String viewBaseCurrencyCode = Res.getBaseCurrencyCode(); String viewPriceCurrencyCode = code; sellHeaderLabel.setText(Res.get("market.offerBook.sellOffersHeaderLabel", viewBaseCurrencyCode)); sellButton.updateText(Res.get("shared.sellCurrency", viewBaseCurrencyCode)); sellButton.setGraphic(GUIUtil.getCurrencyIconWithBorder(viewBaseCurrencyCode)); sellButton.setOnAction(e -> model.goToOfferView(OfferDirection.BUY)); sellButton.setId("sell-button-big"); buyHeaderLabel.setText(Res.get("market.offerBook.buyOffersHeaderLabel", viewBaseCurrencyCode)); buyButton.updateText(Res.get( "shared.buyCurrency", viewBaseCurrencyCode)); buyButton.setGraphic(GUIUtil.getCurrencyIconWithBorder(viewBaseCurrencyCode)); buyButton.setOnAction(e -> model.goToOfferView(OfferDirection.SELL)); buyButton.setId("buy-button-big"); priceColumnLabel.set(Res.get("shared.priceWithCur", viewPriceCurrencyCode)); xAxis.setLabel(CurrencyUtil.getPriceWithCurrencyCode(code)); seriesBuy.setName(sellHeaderLabel.getText() + " "); seriesSell.setName(buyHeaderLabel.getText()); }); buyOfferTableView.setItems(model.getTopBuyOfferList()); sellOfferTableView.setItems(model.getTopSellOfferList()); buyOfferTableView.getSelectionModel().selectedItemProperty().addListener(buyTableRowSelectionListener); sellOfferTableView.getSelectionModel().selectedItemProperty().addListener(sellTableRowSelectionListener); root.getScene().heightProperty().addListener(havenoWindowVerticalSizeListener); layout(); updateChartData(); } static class CurrencyListItemStringConverter extends StringConverter { private final ComboBox comboBox; CurrencyListItemStringConverter(ComboBox comboBox) { this.comboBox = comboBox; } @Override public String toString(CurrencyListItem currencyItem) { return currencyItem != null ? currencyItem.codeDashNameString() : ""; } @Override public CurrencyListItem fromString(String s) { return comboBox.getItems().stream(). filter(currencyItem -> currencyItem.codeDashNameString().equals(s)). findAny().orElse(null); } } private void createListener() { changeListener = c -> UserThread.execute(() -> updateChartData()); currencyListItemsListener = c -> { if (model.getSelectedCurrencyListItem().isPresent()) currencyComboBox.getSelectionModel().select(model.getSelectedCurrencyListItem().get()); }; sellTableRowSelectionListener = (observable, oldValue, newValue) -> model.goToOfferView(OfferDirection.SELL); buyTableRowSelectionListener = (observable, oldValue, newValue) -> model.goToOfferView(OfferDirection.BUY); havenoWindowVerticalSizeListener = (observable, oldValue, newValue) -> layout(); } @Override protected void deactivate() { model.getOfferBookListItems().removeListener(changeListener); tabPaneSelectionModel.selectedIndexProperty().removeListener(selectedTabIndexListener); model.currencyListItems.getObservableList().removeListener(currencyListItemsListener); tradeCurrencySubscriber.unsubscribe(); buyOfferTableView.getSelectionModel().selectedItemProperty().removeListener(buyTableRowSelectionListener); sellOfferTableView.getSelectionModel().selectedItemProperty().removeListener(sellTableRowSelectionListener); } private void createChart() { xAxis = new NumberAxis(); xAxis.setForceZeroInRange(false); xAxis.setAutoRanging(true); xAxis.setTickMarkVisible(true); xAxis.setMinorTickVisible(false); NumberAxis yAxis = new NumberAxis(); yAxis.setForceZeroInRange(false); yAxis.setSide(Side.RIGHT); yAxis.setAutoRanging(true); yAxis.setTickMarkVisible(true); yAxis.setMinorTickVisible(false); yAxis.getStyleClass().add("axisy"); yAxis.setLabel(CurrencyUtil.getOfferVolumeCode(Res.getBaseCurrencyCode())); seriesBuy = new XYChart.Series<>(); seriesSell = new XYChart.Series<>(); areaChart = new AreaChart<>(xAxis, yAxis); areaChart.setLegendVisible(false); areaChart.setAnimated(false); areaChart.setId("charts"); areaChart.setMinHeight(270); areaChart.setPrefHeight(270); areaChart.setCreateSymbols(true); areaChart.setPadding(new Insets(0, 10, 0, 10)); areaChart.getData().addAll(List.of(seriesBuy, seriesSell)); chartPane = new AnchorPane(); chartPane.getStyleClass().add("chart-pane"); AnchorPane.setTopAnchor(areaChart, 15d); AnchorPane.setBottomAnchor(areaChart, 10d); AnchorPane.setLeftAnchor(areaChart, 10d); AnchorPane.setRightAnchor(areaChart, 0d); chartPane.getChildren().add(areaChart); } private synchronized void updateChartData() { // update volume headers Volume volumeSell = model.getTotalVolume(OfferDirection.SELL); Volume volumeBuy = model.getTotalVolume(OfferDirection.BUY); String formattedVolumeSell = volumeSell == null ? null : VolumeUtil.formatVolume(volumeSell); String formattedVolumeBuy = volumeBuy == null ? null : VolumeUtil.formatVolume(volumeBuy); if (model.getSellData().isEmpty()) formattedVolumeSell = "0.0"; if (model.getBuyData().isEmpty()) formattedVolumeBuy = "0.0"; volumeSellColumnLabel.set(Res.get("offerbook.volumeTotal", model.getCurrencyCode(), formattedVolumeSell == null ? "" : "(" + formattedVolumeSell + ")")); volumeBuyColumnLabel.set(Res.get("offerbook.volumeTotal", model.getCurrencyCode(), formattedVolumeBuy == null ? "" : "(" + formattedVolumeBuy + ")")); // update amount headers amountSellColumnLabel.set(Res.get("offerbook.XMRTotal", "" + model.getTotalAmount(OfferDirection.SELL))); amountBuyColumnLabel.set(Res.get("offerbook.XMRTotal", "" + model.getTotalAmount(OfferDirection.BUY))); seriesSell.getData().clear(); seriesBuy.getData().clear(); areaChart.getData().clear(); seriesSell.getData().addAll(filterOutliersSell(model.getSellData())); seriesBuy.getData().addAll(filterOutliersBuy(model.getBuyData())); areaChart.getData().addAll(List.of(seriesBuy, seriesSell)); } List> filterOutliersBuy(List> buy) { List mnmx = minMaxFilterLeft(buy); if (mnmx.get(0) == Double.MAX_VALUE || mnmx.get(1) == Double.MIN_VALUE) { // no filtering return buy; } // apply filtering return filterLeft(buy, mnmx.get(1)); } List> filterOutliersSell(List> sell) { List mnmx = minMaxFilterRight(sell); if (mnmx.get(0) == Double.MAX_VALUE || mnmx.get(1) == Double.MIN_VALUE) { // no filtering return sell; } // apply filtering return filterRight(sell, mnmx.get(0)); } private List minMaxFilterLeft(List> data) { synchronized (data) { double maxValue = data.stream() .mapToDouble(o -> o.getXValue().doubleValue()) .max() .orElse(Double.MIN_VALUE); // Hide offers less than a div-factor of dataLimitFactor lower than the highest offer. double minValue = data.stream() .mapToDouble(o -> o.getXValue().doubleValue()) .filter(o -> o > maxValue / dataLimitFactor) .min() .orElse(Double.MAX_VALUE); return List.of(minValue, maxValue); } } private List minMaxFilterRight(List> data) { synchronized (data) { double minValue = data.stream() .mapToDouble(o -> o.getXValue().doubleValue()) .min() .orElse(Double.MAX_VALUE); // Hide offers a dataLimitFactor factor higher than the lowest offer double maxValue = data.stream() .mapToDouble(o -> o.getXValue().doubleValue()) .filter(o -> o < minValue * dataLimitFactor) .max() .orElse(Double.MIN_VALUE); return List.of(minValue, maxValue); } } private List> filterLeft(List> data, double maxValue) { synchronized (data) { return data.stream() .filter(o -> o.getXValue().doubleValue() > maxValue / dataLimitFactor) .collect(Collectors.toList()); } } private List> filterRight(List> data, double minValue) { synchronized (data) { return data.stream() .filter(o -> o.getXValue().doubleValue() < minValue * dataLimitFactor) .collect(Collectors.toList()); } } private Tuple4, VBox, Button, Label> getOfferTable(OfferDirection direction) { TableView tableView = new TableView<>(); GUIUtil.applyTableStyle(tableView, false); tableView.setMinHeight(initialOfferTableViewHeight); tableView.setPrefHeight(initialOfferTableViewHeight); tableView.setMinWidth(480); tableView.getStyleClass().addAll("offer-table", "non-interactive-table"); // price TableColumn priceColumn = new TableColumn<>(); priceColumn.textProperty().bind(priceColumnLabel); priceColumn.setMinWidth(115); priceColumn.setMaxWidth(115); priceColumn.setSortable(false); priceColumn.getStyleClass().add("number-column"); priceColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); priceColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { private Offer offer; final ChangeListener listener = new ChangeListener<>() { @Override public void changed(ObservableValue observable, Number oldValue, Number newValue) { if (offer != null && offer.getPrice() != null) { setText(""); setGraphic(new ColoredDecimalPlacesWithZerosText(model.getPrice(offer), model.getZeroDecimalsForPrice(offer))); model.priceFeedService.updateCounterProperty().removeListener(listener); } } }; @Override public void updateItem(final OfferListItem offerListItem, boolean empty) { super.updateItem(offerListItem, empty); if (offerListItem != null && !empty) { final Offer offer = offerListItem.offer; if (offer.getPrice() == null) { this.offer = offer; model.priceFeedService.updateCounterProperty().addListener(listener); setText(Res.get("shared.na")); } else { setGraphic(new ColoredDecimalPlacesWithZerosText(model.getPrice(offer), model.getZeroDecimalsForPrice(offer))); } } else { model.priceFeedService.updateCounterProperty().removeListener(listener); this.offer = null; setText(""); setGraphic(null); } } }; } }); boolean isSellTable = model.isSellOffer(direction); // volume TableColumn volumeColumn = new TableColumn<>(); volumeColumn.setMinWidth(115); volumeColumn.setSortable(false); volumeColumn.textProperty().bind(isSellTable ? volumeSellColumnLabel : volumeBuyColumnLabel); volumeColumn.getStyleClass().addAll("number-column"); volumeColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); volumeColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { private Offer offer; final ChangeListener listener = new ChangeListener<>() { @Override public void changed(ObservableValue observable, Number oldValue, Number newValue) { if (offer != null && offer.getPrice() != null) { renderCellContentRange(); model.priceFeedService.updateCounterProperty().removeListener(listener); } } }; @Override public void updateItem(final OfferListItem offerListItem, boolean empty) { super.updateItem(offerListItem, empty); if (offerListItem != null && !empty) { this.offer = offerListItem.offer; if (offer.getPrice() == null) { this.offer = offerListItem.offer; model.priceFeedService.updateCounterProperty().addListener(listener); setText(Res.get("shared.na")); } else { renderCellContentRange(); } } else { model.priceFeedService.updateCounterProperty().removeListener(listener); this.offer = null; setText(""); setGraphic(null); } } /** * Renders cell content, if it has a single value or a range. * Should not be called for empty cells */ private void renderCellContentRange() { String volumeRange = VolumeUtil.formatVolume(offer, true, 2); setText(""); setGraphic(new ColoredDecimalPlacesWithZerosText(volumeRange, model.getMaxNumberOfPriceZeroDecimalsToColorize(offer))); } }; } }); // amount TableColumn amountColumn = new TableColumn<>(); amountColumn.textProperty().bind(isSellTable ? amountSellColumnLabel : amountBuyColumnLabel); amountColumn.setMinWidth(115); amountColumn.setSortable(false); amountColumn.getStyleClass().add("number-column"); amountColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); amountColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final OfferListItem offerListItem, boolean empty) { super.updateItem(offerListItem, empty); if (offerListItem != null && !empty) { String amountRange = DisplayUtils.formatAmount(offerListItem.offer, formatter); setGraphic(new ColoredDecimalPlacesWithZerosText(amountRange, GUIUtil.AMOUNT_DECIMALS_WITH_ZEROS)); } else { setGraphic(null); } } }; } }); // trader avatar TableColumn avatarColumn = new AutoTooltipTableColumn<>(isSellTable ? Res.get("shared.sellerUpperCase") : Res.get("shared.buyerUpperCase")) { { setMinWidth(80); setMaxWidth(80); setSortable(true); } }; avatarColumn.getStyleClass().addAll("avatar-column"); avatarColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); avatarColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final OfferListItem newItem, boolean empty) { super.updateItem(newItem, empty); if (newItem != null && !empty) { final Offer offer = newItem.offer; boolean myOffer = model.isMyOffer(offer); if (!myOffer) { final NodeAddress makersNodeAddress = offer.getOwnerNodeAddress(); String role = Res.get("peerInfoIcon.tooltip.maker"); PeerInfoIconSmall peerInfoIcon = new PeerInfoIconSmall(makersNodeAddress, role, offer, model.preferences, model.accountAgeWitnessService, useDevPrivilegeKeys); // setAlignment(Pos.CENTER); setGraphic(peerInfoIcon); } else { setGraphic(new Label(Res.get("shared.me"))); } } else { setGraphic(null); } } }; } }); tableView.getColumns().add(volumeColumn); tableView.getColumns().add(amountColumn); tableView.getColumns().add(priceColumn); tableView.getColumns().add(avatarColumn); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); Label placeholder = new AutoTooltipLabel(Res.get("table.placeholder.noItems", Res.get("shared.multipleOffers"))); placeholder.setWrapText(true); tableView.setPlaceholder(placeholder); HBox titleButtonBox = new HBox(); titleButtonBox.getStyleClass().add("offer-table-top"); titleButtonBox.setAlignment(Pos.CENTER); Label titleLabel = new AutoTooltipLabel(); titleLabel.getStyleClass().add("table-title"); AutoTooltipButton button = new AutoTooltipButton(); button.setContentDisplay(ContentDisplay.RIGHT); button.setGraphicTextGap(10); button.setMinHeight(32); Region spacer = new Region(); HBox.setHgrow(spacer, Priority.ALWAYS); titleButtonBox.getChildren().addAll(titleLabel, spacer, button); VBox vBox = new VBox(); VBox.setVgrow(tableView, Priority.ALWAYS); vBox.setPadding(new Insets(0, 0, 0, 0)); vBox.setSpacing(0); vBox.setFillWidth(true); //vBox.setMinHeight(190); vBox.getChildren().addAll(titleButtonBox, tableView); return new Tuple4<>(tableView, vBox, button, titleLabel); } private void layout() { UserThread.runAfter(() -> { if (root.getScene() != null) { double newTableViewHeight = offerTableViewHeight.apply(root.getScene().getHeight()); if (buyOfferTableView.getHeight() != newTableViewHeight) { buyOfferTableView.setMinHeight(newTableViewHeight); sellOfferTableView.setMinHeight(newTableViewHeight); } } }, 100, TimeUnit.MILLISECONDS); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.market.offerbook; import com.google.common.math.LongMath; import com.google.inject.Inject; import haveno.common.UserThread; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.GlobalSettings; import haveno.core.locale.TradeCurrency; import haveno.core.monetary.Price; import haveno.core.monetary.Volume; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OpenOfferManager; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.HavenoUtils; import haveno.core.user.Preferences; import haveno.core.util.VolumeUtil; import haveno.desktop.Navigation; import haveno.desktop.common.model.ActivatableViewModel; import haveno.desktop.main.MainView; import haveno.desktop.main.offer.BuyOfferView; import haveno.desktop.main.offer.OfferView; import haveno.desktop.main.offer.OfferViewUtil; import haveno.desktop.main.offer.SellOfferView; import haveno.desktop.main.offer.offerbook.OfferBook; import haveno.desktop.main.offer.offerbook.OfferBookListItem; import haveno.desktop.main.settings.SettingsView; import haveno.desktop.main.settings.preferences.PreferencesView; import haveno.desktop.util.CurrencyList; import haveno.desktop.util.CurrencyListItem; import haveno.desktop.util.DisplayUtils; import haveno.desktop.util.GUIUtil; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.scene.chart.XYChart; import java.math.BigInteger; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; class OfferBookChartViewModel extends ActivatableViewModel { private static final int TAB_INDEX = 0; private final OfferBook offerBook; private final OpenOfferManager openOfferManager; final Preferences preferences; final PriceFeedService priceFeedService; final AccountAgeWitnessService accountAgeWitnessService; private final Navigation navigation; final ObjectProperty selectedTradeCurrencyProperty = new SimpleObjectProperty<>(); private final List> buyData = new ArrayList<>(); private final List> sellData = new ArrayList<>(); private final ObservableList offerBookListItems; private final ListChangeListener offerBookListItemsListener; final CurrencyList currencyListItems; private final ObservableList topBuyOfferList = FXCollections.observableArrayList(); private final ObservableList topSellOfferList = FXCollections.observableArrayList(); private final ChangeListener currenciesUpdatedListener; private int selectedTabIndex; public final IntegerProperty maxPlacesForBuyPrice = new SimpleIntegerProperty(); public final IntegerProperty maxPlacesForBuyVolume = new SimpleIntegerProperty(); public final IntegerProperty maxPlacesForSellPrice = new SimpleIntegerProperty(); public final IntegerProperty maxPlacesForSellVolume = new SimpleIntegerProperty(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject OfferBookChartViewModel(OfferBook offerBook, OpenOfferManager openOfferManager, Preferences preferences, PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation) { this.offerBook = offerBook; this.openOfferManager = openOfferManager; this.preferences = preferences; this.priceFeedService = priceFeedService; this.navigation = navigation; this.accountAgeWitnessService = accountAgeWitnessService; String code = preferences.getOfferBookChartScreenCurrencyCode(); if (code != null) { Optional tradeCurrencyOptional = CurrencyUtil.getTradeCurrency(code); if (tradeCurrencyOptional.isPresent()) selectedTradeCurrencyProperty.set(tradeCurrencyOptional.get()); else { selectedTradeCurrencyProperty.set(GlobalSettings.getDefaultTradeCurrency()); } } else { selectedTradeCurrencyProperty.set(GlobalSettings.getDefaultTradeCurrency()); } offerBookListItems = offerBook.getOfferBookListItems(); offerBookListItemsListener = c -> { c.next(); if (c.wasAdded() || c.wasRemoved()) { ArrayList list = new ArrayList<>(c.getRemoved()); list.addAll(c.getAddedSubList()); if (list.stream() .map(OfferBookListItem::getOffer) .anyMatch(e -> e.getCounterCurrencyCode().equals(selectedTradeCurrencyProperty.get().getCode()))) updateChartData(); } fillTradeCurrencies(); }; currenciesUpdatedListener = (observable, oldValue, newValue) -> { if (!isAnyPriceAbsent()) { UserThread.execute(() -> { offerBook.fillOfferBookListItems(); updateChartData(); var self = this; priceFeedService.updateCounterProperty().removeListener(self.currenciesUpdatedListener); }); } }; this.currencyListItems = new CurrencyList(preferences); } private void fillTradeCurrencies() { // Don't use a set as we need all entries synchronized (offerBookListItems) { List tradeCurrencyList = offerBookListItems.stream() .map(e -> { String currencyCode = e.getOffer().getCounterCurrencyCode(); Optional tradeCurrencyOptional = CurrencyUtil.getTradeCurrency(currencyCode); return tradeCurrencyOptional.orElse(null); }) .filter(Objects::nonNull) .collect(Collectors.toList()); currencyListItems.updateWithCurrencies(tradeCurrencyList, null); } } @Override protected void activate() { offerBookListItems.addListener(offerBookListItemsListener); offerBook.fillOfferBookListItems(); fillTradeCurrencies(); updateChartData(); if (isAnyPriceAbsent()) priceFeedService.updateCounterProperty().addListener(currenciesUpdatedListener); syncPriceFeedCurrency(); } @Override protected void deactivate() { offerBookListItems.removeListener(offerBookListItemsListener); } /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// public void onSetTradeCurrency(TradeCurrency tradeCurrency) { if (tradeCurrency != null) { final String code = tradeCurrency.getCode(); if (isEditEntry(code)) { navigation.navigateTo(MainView.class, SettingsView.class, PreferencesView.class); } else { selectedTradeCurrencyProperty.set(tradeCurrency); preferences.setOfferBookChartScreenCurrencyCode(code); updateChartData(); priceFeedService.setCurrencyCode(code); } } } public void setSelectedTabIndex(int selectedTabIndex) { this.selectedTabIndex = selectedTabIndex; syncPriceFeedCurrency(); } public boolean isSellOffer(OfferDirection direction) { return direction == OfferDirection.SELL; } public double getTotalAmount(OfferDirection direction) { synchronized (offerBookListItems) { List offerList = offerBookListItems.stream() .map(OfferBookListItem::getOffer) .filter(e -> e.getCounterCurrencyCode().equals(selectedTradeCurrencyProperty.get().getCode()) && e.getDirection().equals(direction)) .collect(Collectors.toList()); BigInteger sum = BigInteger.ZERO; for (Offer offer : offerList) sum = sum.add(offer.getAmount()); return HavenoUtils.atomicUnitsToXmr(sum); } } public Volume getTotalVolume(OfferDirection direction) { synchronized (offerBookListItems) { List volumes = offerBookListItems.stream() .map(OfferBookListItem::getOffer) .filter(e -> e.getCounterCurrencyCode().equals(selectedTradeCurrencyProperty.get().getCode()) && e.getDirection().equals(direction)) .map(Offer::getVolume) .collect(Collectors.toList()); try { return VolumeUtil.sum(volumes); } catch (Exception e) { // log.error("Cannot compute total volume because prices are unavailable, currency={}, direction={}", // selectedTradeCurrencyProperty.get().getCode(), direction); return null; // expected before prices are available } } } public boolean isCrypto() { return CurrencyUtil.isCryptoCurrency(getCurrencyCode()); } public boolean isMyOffer(Offer offer) { return openOfferManager.isMyOffer(offer); } public void goToOfferView(OfferDirection direction) { updateScreenCurrencyInPreferences(direction); Class offerView = isSellOffer(direction) ? BuyOfferView.class : SellOfferView.class; navigation.navigateTo(MainView.class, offerView, OfferViewUtil.getOfferBookViewClass(getCurrencyCode())); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// public List> getBuyData() { return buyData; } public List> getSellData() { return sellData; } public String getCurrencyCode() { return selectedTradeCurrencyProperty.get().getCode(); } public ObservableList getOfferBookListItems() { return offerBookListItems; } public ObservableList getTopBuyOfferList() { return topBuyOfferList; } public ObservableList getTopSellOfferList() { return topSellOfferList; } public ObservableList getCurrencyListItems() { return currencyListItems.getObservableList(); } public Optional getSelectedCurrencyListItem() { return currencyListItems.getObservableList().stream() .filter(e -> e.tradeCurrency.equals(selectedTradeCurrencyProperty.get())).findAny(); } public int getMaxNumberOfPriceZeroDecimalsToColorize(Offer offer) { return CurrencyUtil.isVolumeRoundedToNearestUnit(offer.getCounterCurrencyCode()) ? GUIUtil.NUM_DECIMALS_UNIT : GUIUtil.NUM_DECIMALS_PRECISE; } public int getZeroDecimalsForPrice(Offer offer) { return CurrencyUtil.isPricePrecise(offer.getCounterCurrencyCode()) ? GUIUtil.NUM_DECIMALS_PRECISE : GUIUtil.NUM_DECIMALS_PRICE_LESS_PRECISE; } public String getPrice(Offer offer) { return formatPrice(offer, true); } private String formatPrice(Offer offer, boolean decimalAligned) { return DisplayUtils.formatPrice(offer.getPrice(), decimalAligned, offer.isBuyOffer() ? maxPlacesForBuyPrice.get() : maxPlacesForSellPrice.get()); } public String getVolume(Offer offer) { return formatVolume(offer, true); } private String formatVolume(Offer offer, boolean decimalAligned) { return VolumeUtil.formatVolume(offer, decimalAligned, offer.isBuyOffer() ? maxPlacesForBuyVolume.get() : maxPlacesForSellVolume.get(), false); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void syncPriceFeedCurrency() { if (selectedTabIndex == TAB_INDEX) priceFeedService.setCurrencyCode(getCurrencyCode()); } private boolean isAnyPriceAbsent() { return offerBookListItems.stream().anyMatch(item -> item.getOffer().getPrice() == null); } private void updateChartData() { // Offer price can be null (if price feed unavailable), thus a null-tolerant comparator is used. Comparator offerPriceComparator = Comparator.comparing(Offer::getPrice, Comparator.nullsLast(Comparator.naturalOrder())); // Offer amounts are used for the secondary sort. They are sorted from high to low. Comparator offerAmountComparator = Comparator.comparing(Offer::getAmount).reversed(); var buyOfferSortComparator = offerPriceComparator.reversed() // Buy offers, as opposed to sell offers, are primarily sorted from high price to low. .thenComparing(offerAmountComparator); var sellOfferSortComparator = offerPriceComparator .thenComparing(offerAmountComparator); OfferDirection buyOfferDirection = OfferDirection.BUY; List allBuyOffers = offerBookListItems.stream() .map(OfferBookListItem::getOffer) .filter(e -> e.getCounterCurrencyCode().equals(selectedTradeCurrencyProperty.get().getCode()) && e.getDirection().equals(buyOfferDirection)) .sorted(buyOfferSortComparator) .collect(Collectors.toList()); final Optional highestBuyPriceOffer = allBuyOffers.stream() .filter(o -> o.getPrice() != null) .max(Comparator.comparingLong(o -> o.getPrice().getValue())); if (highestBuyPriceOffer.isPresent()) { final Offer offer = highestBuyPriceOffer.get(); maxPlacesForBuyPrice.set(formatPrice(offer, false).length()); } else { log.debug("highestBuyPriceOffer not present"); } final Optional highestBuyVolumeOffer = allBuyOffers.stream() .filter(o -> o.getVolume() != null) .max(Comparator.comparingLong(o -> o.getVolume().getValue())); if (highestBuyVolumeOffer.isPresent()) { final Offer offer = highestBuyVolumeOffer.get(); maxPlacesForBuyVolume.set(formatVolume(offer, false).length()); } buildChartAndTableEntries(allBuyOffers, OfferDirection.BUY, buyData, topBuyOfferList); OfferDirection sellOfferDirection = OfferDirection.SELL; List allSellOffers = offerBookListItems.stream() .map(OfferBookListItem::getOffer) .filter(e -> e.getCounterCurrencyCode().equals(selectedTradeCurrencyProperty.get().getCode()) && e.getDirection().equals(sellOfferDirection)) .sorted(sellOfferSortComparator) .collect(Collectors.toList()); final Optional highestSellPriceOffer = allSellOffers.stream() .filter(o -> o.getPrice() != null) .max(Comparator.comparingLong(o -> o.getPrice().getValue())); if (highestSellPriceOffer.isPresent()) { final Offer offer = highestSellPriceOffer.get(); maxPlacesForSellPrice.set(formatPrice(offer, false).length()); } final Optional highestSellVolumeOffer = allSellOffers.stream() .filter(o -> o.getVolume() != null) .max(Comparator.comparingLong(o -> o.getVolume().getValue())); if (highestSellVolumeOffer.isPresent()) { final Offer offer = highestSellVolumeOffer.get(); maxPlacesForSellVolume.set(formatVolume(offer, false).length()); } buildChartAndTableEntries(allSellOffers, OfferDirection.SELL, sellData, topSellOfferList); } private void buildChartAndTableEntries(List sortedList, OfferDirection direction, List> data, ObservableList offerTableList) { synchronized (data) { data.clear(); double accumulatedAmount = 0; List offerTableListTemp = new ArrayList<>(); for (Offer offer : sortedList) { Price price = offer.getPrice(); if (price != null) { double amount = (double) offer.getAmount().longValueExact() / LongMath.pow(10, HavenoUtils.XMR_SMALLEST_UNIT_EXPONENT); accumulatedAmount += amount; offerTableListTemp.add(new OfferListItem(offer, accumulatedAmount)); double priceAsDouble = (double) price.getValue() / LongMath.pow(10, price.smallestUnitExponent()); if (direction.equals(OfferDirection.BUY)) data.add(0, new XYChart.Data<>(priceAsDouble, accumulatedAmount)); else data.add(new XYChart.Data<>(priceAsDouble, accumulatedAmount)); } } offerTableList.setAll(offerTableListTemp); } } private boolean isEditEntry(String id) { return id.equals(GUIUtil.EDIT_FLAG); } private void updateScreenCurrencyInPreferences(OfferDirection direction) { if (isSellOffer(direction)) { if (CurrencyUtil.isFiatCurrency(getCurrencyCode())) { preferences.setBuyScreenCurrencyCode(getCurrencyCode()); } else if (CurrencyUtil.isCryptoCurrency(getCurrencyCode())) { preferences.setBuyScreenCryptoCurrencyCode(getCurrencyCode()); } else if (CurrencyUtil.isTraditionalCurrency(getCurrencyCode())) { preferences.setBuyScreenOtherCurrencyCode(getCurrencyCode()); } } else { if (CurrencyUtil.isFiatCurrency(getCurrencyCode())) { preferences.setSellScreenCurrencyCode(getCurrencyCode()); } else if (CurrencyUtil.isCryptoCurrency(getCurrencyCode())) { preferences.setSellScreenCryptoCurrencyCode(getCurrencyCode()); } else if (CurrencyUtil.isTraditionalCurrency(getCurrencyCode())) { preferences.setSellScreenOtherCurrencyCode(getCurrencyCode()); } } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferListItem.java ================================================ /* * 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 . */ package haveno.desktop.main.market.offerbook; import haveno.core.offer.Offer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class OfferListItem { private static final Logger log = LoggerFactory.getLogger(OfferListItem.class); public final Offer offer; public final double accumulated; public OfferListItem(Offer offer, double accumulated) { this.offer = offer; this.accumulated = accumulated; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; OfferListItem that = (OfferListItem) o; //noinspection SimplifiableIfStatement if (Double.compare(that.accumulated, accumulated) != 0) return false; return !(offer != null ? !offer.equals(that.offer) : that.offer != null); } @Override public int hashCode() { int result; long temp; result = offer != null ? offer.hashCode() : 0; temp = Double.doubleToLongBits(accumulated); result = 31 * result + (int) (temp ^ (temp >>> 32)); return result; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/market/spread/SpreadItem.java ================================================ /* * 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 . */ package haveno.desktop.main.market.spread; import haveno.core.monetary.Price; import javax.annotation.Nullable; import java.math.BigInteger; public class SpreadItem { public final String currencyCode; public final int numberOfBuyOffers; public final int numberOfSellOffers; public final int numberOfOffers; @Nullable public final Price priceSpread; public final String percentage; public final double percentageValue; public final BigInteger totalAmount; public SpreadItem(String currencyCode, int numberOfBuyOffers, int numberOfSellOffers, int numberOfOffers, @Nullable Price priceSpread, String percentage, double percentageValue, BigInteger totalAmount) { this.currencyCode = currencyCode; this.numberOfBuyOffers = numberOfBuyOffers; this.numberOfSellOffers = numberOfSellOffers; this.numberOfOffers = numberOfOffers; this.priceSpread = priceSpread; this.percentage = percentage; this.percentageValue = percentageValue; this.totalAmount = totalAmount; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/market/spread/SpreadView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/market/spread/SpreadView.java ================================================ /* * 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 . */ package haveno.desktop.main.market.spread; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.UserThread; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.common.view.ActivatableViewAndModel; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.AutoTooltipTableColumn; import haveno.desktop.components.ColoredDecimalPlacesWithZerosText; import haveno.desktop.util.GUIUtil; import java.math.BigInteger; import java.util.Comparator; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.collections.ListChangeListener; import javafx.collections.transformation.SortedList; import javafx.scene.control.Label; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; import javafx.util.Callback; @FxmlView public class SpreadView extends ActivatableViewAndModel { private final CoinFormatter formatter; private TableView tableView; private SortedList sortedList; private ListChangeListener itemListChangeListener; private TableColumn totalAmountColumn, numberOfOffersColumn, numberOfBuyOffersColumn, numberOfSellOffersColumn; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject public SpreadView(SpreadViewModel model, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter) { super(model); this.formatter = formatter; } @Override public void initialize() { tableView = new TableView<>(); GUIUtil.applyTableStyle(tableView); tableView.getStyleClass().add("non-interactive-table"); int gridRow = 0; GridPane.setRowIndex(tableView, gridRow); GridPane.setVgrow(tableView, Priority.ALWAYS); GridPane.setHgrow(tableView, Priority.ALWAYS); root.getChildren().add(tableView); Label placeholder = new AutoTooltipLabel(Res.get("table.placeholder.noData")); placeholder.setWrapText(true); tableView.setPlaceholder(placeholder); TableColumn currencyColumn = getCurrencyColumn(); tableView.getColumns().add(currencyColumn); numberOfOffersColumn = getNumberOfOffersColumn(); tableView.getColumns().add(numberOfOffersColumn); numberOfBuyOffersColumn = getNumberOfBuyOffersColumn(); tableView.getColumns().add(numberOfBuyOffersColumn); numberOfSellOffersColumn = getNumberOfSellOffersColumn(); tableView.getColumns().add(numberOfSellOffersColumn); totalAmountColumn = getTotalAmountColumn(); tableView.getColumns().add(totalAmountColumn); TableColumn spreadColumn = getSpreadColumn(); tableView.getColumns().add(spreadColumn); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); currencyColumn.setComparator(Comparator.comparing(o -> model.isIncludePaymentMethod() ? o.currencyCode : CurrencyUtil.getNameByCode(o.currencyCode))); numberOfOffersColumn.setComparator(Comparator.comparingInt(o3 -> o3.numberOfOffers)); numberOfBuyOffersColumn.setComparator(Comparator.comparingInt(o3 -> o3.numberOfBuyOffers)); numberOfSellOffersColumn.setComparator(Comparator.comparingInt(o2 -> o2.numberOfSellOffers)); totalAmountColumn.setComparator(Comparator.comparing(o -> o.totalAmount)); spreadColumn.setComparator(Comparator.comparingDouble(o -> o.percentageValue)); numberOfOffersColumn.setSortType(TableColumn.SortType.DESCENDING); tableView.getSortOrder().add(numberOfOffersColumn); itemListChangeListener = c -> updateHeaders(); } @Override protected void activate() { sortedList = new SortedList<>(model.spreadItems); sortedList.comparatorProperty().bind(tableView.comparatorProperty()); tableView.setItems(sortedList); sortedList.addListener(itemListChangeListener); updateHeaders(); } @Override protected void deactivate() { sortedList.comparatorProperty().unbind(); sortedList.removeListener(itemListChangeListener); } private void updateHeaders() { int numberOfOffers = sortedList.stream().mapToInt(item -> item.numberOfOffers).sum(); int numberOfBuyOffers = sortedList.stream().mapToInt(item -> item.numberOfBuyOffers).sum(); int numberOfSellOffers = sortedList.stream().mapToInt(item -> item.numberOfSellOffers).sum(); BigInteger totalAmount = BigInteger.ZERO; for (SpreadItem item : sortedList) totalAmount = totalAmount.add(item.totalAmount); String total = HavenoUtils.formatXmr(totalAmount); UserThread.execute(() -> { numberOfOffersColumn.setGraphic(new AutoTooltipLabel(Res.get("market.spread.numberOfOffersColumn", numberOfOffers))); numberOfBuyOffersColumn.setGraphic(new AutoTooltipLabel(Res.get("market.spread.numberOfBuyOffersColumn", numberOfBuyOffers))); numberOfSellOffersColumn.setGraphic(new AutoTooltipLabel((Res.get("market.spread.numberOfSellOffersColumn", numberOfSellOffers)))); totalAmountColumn.setGraphic(new AutoTooltipLabel(Res.get("market.spread.totalAmountColumn", total))); }); } /////////////////////////////////////////////////////////////////////////////////////////// // Columns /////////////////////////////////////////////////////////////////////////////////////////// private TableColumn getCurrencyColumn() { TableColumn column = new AutoTooltipTableColumn<>(model.getKeyColumnName()) { { setMinWidth(160); } }; column.getStyleClass().addAll("number-column"); column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final SpreadItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) if (model.isIncludePaymentMethod()) setText(item.currencyCode); else setText(CurrencyUtil.getNameAndCode(item.currencyCode)); else setText(""); } }; } }); return column; } private TableColumn getNumberOfOffersColumn() { TableColumn column = new TableColumn<>() { { setMinWidth(100); } }; column.getStyleClass().add("number-column"); column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final SpreadItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) setText(String.valueOf(item.numberOfOffers)); else setText(""); } }; } }); return column; } private TableColumn getNumberOfBuyOffersColumn() { TableColumn column = new TableColumn<>() { { setMinWidth(100); } }; column.getStyleClass().add("number-column"); column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final SpreadItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) setText(String.valueOf(item.numberOfBuyOffers)); else setText(""); } }; } }); return column; } private TableColumn getNumberOfSellOffersColumn() { TableColumn column = new TableColumn<>() { { setMinWidth(100); } }; column.getStyleClass().add("number-column"); column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final SpreadItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) setText(String.valueOf(item.numberOfSellOffers)); else setText(""); } }; } }); return column; } private TableColumn getTotalAmountColumn() { TableColumn column = new TableColumn<>() { { setMinWidth(140); } }; column.getStyleClass().addAll("number-column", "highlight-text"); column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final SpreadItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) setGraphic(new ColoredDecimalPlacesWithZerosText(model.getAmount(item.totalAmount), GUIUtil.AMOUNT_DECIMALS_WITH_ZEROS)); else { setText(""); setGraphic(null); } } }; } }); return column; } private TableColumn getSpreadColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("market.spread.spreadColumn")) { { setMinWidth(110); } }; column.getStyleClass().addAll("number-column", "highlight-text"); column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final SpreadItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { // TODO maybe show extra columns with item.priceSpread and use real amount diff // not % based if (item.priceSpread != null) setText(item.percentage); /*setText(item.percentage + " (" + formatter.formatPriceWithCode(item.priceSpread) + ")");*/ else setText("-"); } else { setText(""); } } }; } }); return column; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/market/spread/SpreadViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.market.spread; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.UserThread; import haveno.core.locale.Res; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.Price; import haveno.core.monetary.TraditionalMoney; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.provider.price.MarketPrice; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.HavenoUtils; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.common.model.ActivatableViewModel; import haveno.desktop.main.offer.offerbook.OfferBook; import haveno.desktop.main.offer.offerbook.OfferBookListItem; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.GUIUtil; import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import lombok.Getter; import lombok.Setter; class SpreadViewModel extends ActivatableViewModel { private final OfferBook offerBook; private final PriceFeedService priceFeedService; private final CoinFormatter formatter; private final ObservableList offerBookListItems; private final ListChangeListener listChangeListener; final ObservableList spreadItems = FXCollections.observableArrayList(); final IntegerProperty maxPlacesForAmount = new SimpleIntegerProperty(); @Setter @Getter private boolean includePaymentMethod; @Getter private boolean expandedView; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject public SpreadViewModel(OfferBook offerBook, PriceFeedService priceFeedService, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter) { this.offerBook = offerBook; this.priceFeedService = priceFeedService; this.formatter = formatter; includePaymentMethod = false; offerBookListItems = offerBook.getOfferBookListItems(); listChangeListener = c -> UserThread.execute(() -> update(offerBookListItems)); } public String getKeyColumnName() { return includePaymentMethod ? Res.get("shared.paymentMethod") : Res.get("shared.currency"); } public void setExpandedView(boolean expandedView) { this.expandedView = expandedView; update(offerBookListItems); } @Override protected void activate() { offerBookListItems.addListener(listChangeListener); offerBook.fillOfferBookListItems(); update(offerBookListItems); } @Override protected void deactivate() { offerBookListItems.removeListener(listChangeListener); } private static Predicate distinctByKey(Function keyExtractor) { Set seen = ConcurrentHashMap.newKeySet(); return t -> seen.add(keyExtractor.apply(t)); } private void update(ObservableList offerBookListItems) { Map> offersByCurrencyMap = new HashMap<>(); synchronized (offerBookListItems) { for (OfferBookListItem offerBookListItem : offerBookListItems) { Offer offer = offerBookListItem.getOffer(); String key = offer.getCounterCurrencyCode(); if (includePaymentMethod) { key = offer.getPaymentMethod().getShortName(); if (expandedView) { key += ":" + offer.getCounterCurrencyCode(); } } if (!offersByCurrencyMap.containsKey(key)) offersByCurrencyMap.put(key, new ArrayList<>()); offersByCurrencyMap.get(key).add(offer); } } spreadItems.clear(); BigInteger totalAmount = BigInteger.ZERO; for (String key : offersByCurrencyMap.keySet()) { List offers = offersByCurrencyMap.get(key); List uniqueOffers = offers.stream().filter(distinctByKey(Offer::getId)).collect(Collectors.toList()); List buyOffers = uniqueOffers .stream() .filter(e -> e.getDirection().equals(OfferDirection.BUY)) .sorted((o1, o2) -> { long a = o1.getPrice() != null ? o1.getPrice().getValue() : 0; long b = o2.getPrice() != null ? o2.getPrice().getValue() : 0; if (a != b) { return a < b ? 1 : -1; } return 0; }) .collect(Collectors.toList()); List sellOffers = uniqueOffers .stream() .filter(e -> e.getDirection().equals(OfferDirection.SELL)) .sorted((o1, o2) -> { long a = o1.getPrice() != null ? o1.getPrice().getValue() : 0; long b = o2.getPrice() != null ? o2.getPrice().getValue() : 0; if (a != b) { return a > b ? 1 : -1; } return 0; }) .collect(Collectors.toList()); Price spread = null; String percentage = ""; double percentageValue = 0; Price bestSellOfferPrice = sellOffers.isEmpty() ? null : sellOffers.get(0).getPrice(); Price bestBuyOfferPrice = buyOffers.isEmpty() ? null : buyOffers.get(0).getPrice(); if (bestBuyOfferPrice != null && bestSellOfferPrice != null && sellOffers.get(0).getCounterCurrencyCode().equals(buyOffers.get(0).getCounterCurrencyCode())) { MarketPrice marketPrice = priceFeedService.getMarketPrice(sellOffers.get(0).getCounterCurrencyCode()); // There have been some bug reports that an offer caused an overflow exception. // We never found out which offer it was. So add here a try/catch to get better info if it // happens again try { spread = bestSellOfferPrice.subtract(bestBuyOfferPrice); // TODO maybe show extra columns with spread and use real amount diff // not % based. e.g. diff between best buy and sell offer (of small amounts its a smaller gain) if (spread != null && marketPrice != null && marketPrice.isPriceAvailable()) { double marketPriceAsDouble = marketPrice.getPrice(); boolean isTraditionalCurrency = (offers.size() > 0 && offers.get(0).getPaymentMethod().isTraditional()); final double precision = isTraditionalCurrency ? Math.pow(10, TraditionalMoney.SMALLEST_UNIT_EXPONENT) : Math.pow(10, CryptoMoney.SMALLEST_UNIT_EXPONENT); BigDecimal marketPriceAsBigDecimal = BigDecimal.valueOf(marketPriceAsDouble) .multiply(BigDecimal.valueOf(precision)); // We multiply with 10000 because we use precision of 2 at % (100.00%) percentageValue = BigDecimal.valueOf(spread.getValue()) .multiply(BigDecimal.valueOf(10000)) .divide(marketPriceAsBigDecimal, RoundingMode.HALF_UP) .doubleValue() / 10000; percentage = FormattingUtils.formatPercentagePrice(percentageValue); } } catch (Throwable t) { try { // Don't translate msg. It is just for rare error cases and can be removed probably later if // that error never gets reported again. String msg = "An error occurred at the spread calculation.\n" + "Error msg: " + t.toString() + "\n" + "Details of offer data: \n" + "bestSellOfferPrice: " + bestSellOfferPrice.getValue() + "\n" + "bestBuyOfferPrice: " + bestBuyOfferPrice.getValue() + "\n" + "sellOffer getCurrencyCode: " + sellOffers.get(0).getCounterCurrencyCode() + "\n" + "buyOffer getCurrencyCode: " + buyOffers.get(0).getCounterCurrencyCode() + "\n\n" + "Please copy and paste this data and send it to the developers so they can investigate the issue."; new Popup().error(msg).show(); log.error(t.toString()); t.printStackTrace(); } catch (Throwable t2) { log.error(t2.toString()); t2.printStackTrace(); } } } BigInteger totalAmountForCurrency = BigInteger.ZERO; for (Offer offer : offers) { totalAmount = totalAmount.add(offer.getAmount()); totalAmountForCurrency = totalAmountForCurrency.add(offer.getAmount()); } spreadItems.add(new SpreadItem(key, buyOffers.size(), sellOffers.size(), uniqueOffers.size(), spread, percentage, percentageValue, totalAmountForCurrency)); } maxPlacesForAmount.set(formatAmount(totalAmount, false).length()); } public String getAmount(BigInteger amount) { return formatAmount(amount, true); } private String formatAmount(BigInteger amount, boolean decimalAligned) { return formatter.formatCoin(HavenoUtils.atomicUnitsToCoin(amount), GUIUtil.AMOUNT_DECIMALS, decimalAligned, maxPlacesForAmount.get()); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/market/spread/SpreadViewPaymentMethod.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/market/spread/SpreadViewPaymentMethod.java ================================================ /* * 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 . */ package haveno.desktop.main.market.spread; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.core.locale.Res; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.common.view.FxmlView; import static haveno.desktop.util.FormBuilder.addSlideToggleButton; import javafx.scene.control.ToggleButton; @FxmlView public class SpreadViewPaymentMethod extends SpreadView { private ToggleButton expandedMode; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject public SpreadViewPaymentMethod(SpreadViewModel model, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter) { super(model, formatter); model.setIncludePaymentMethod(true); } @Override public void initialize() { super.initialize(); int gridRow = 0; expandedMode = addSlideToggleButton(root, ++gridRow, Res.get("market.spread.expanded")); } @Override protected void activate() { super.activate(); expandedMode.setSelected(model.isExpandedView()); expandedMode.setOnAction(e -> model.setExpandedView(expandedMode.isSelected())); } @Override protected void deactivate() { expandedMode.setOnAction(null); super.deactivate(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/market/trades/ChartCalculations.java ================================================ /* * 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 . */ package haveno.desktop.main.market.trades; import com.google.common.annotations.VisibleForTesting; import haveno.common.util.MathUtils; import haveno.core.locale.CurrencyUtil; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.TraditionalMoney; import haveno.core.trade.HavenoUtils; import haveno.core.trade.statistics.TradeStatistics3; import haveno.desktop.main.market.trades.charts.CandleData; import haveno.desktop.util.DisplayUtils; import javafx.scene.chart.XYChart; import javafx.util.Pair; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.math.BigInteger; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; import static haveno.desktop.main.market.trades.TradesChartsViewModel.MAX_TICKS; @Slf4j public class ChartCalculations { static final ZoneId ZONE_ID = ZoneId.systemDefault(); /////////////////////////////////////////////////////////////////////////////////////////// // Async /////////////////////////////////////////////////////////////////////////////////////////// static CompletableFuture>> getUsdAveragePriceMapsPerTickUnit(List tradeStatisticsList) { return CompletableFuture.supplyAsync(() -> { Map> usdAveragePriceMapsPerTickUnit = new HashMap<>(); Map>> dateMapsPerTickUnit = new HashMap<>(); for (TradesChartsViewModel.TickUnit tick : TradesChartsViewModel.TickUnit.values()) { dateMapsPerTickUnit.put(tick, new HashMap<>()); } synchronized (tradeStatisticsList) { tradeStatisticsList.stream() .filter(e -> e.getCurrency().equals("USD")) .forEach(tradeStatistics -> { for (TradesChartsViewModel.TickUnit tick : TradesChartsViewModel.TickUnit.values()) { long time = roundToTick(tradeStatistics.getLocalDateTime(), tick).getTime(); Map> map = dateMapsPerTickUnit.get(tick); map.putIfAbsent(time, new ArrayList<>()); map.get(time).add(tradeStatistics); } }); } dateMapsPerTickUnit.forEach((tick, map) -> { HashMap priceMap = new HashMap<>(); map.forEach((date, tradeStatistics) -> priceMap.put(date, getAverageTraditionalPrice(tradeStatistics))); usdAveragePriceMapsPerTickUnit.put(tick, priceMap); }); return usdAveragePriceMapsPerTickUnit; }); } static CompletableFuture> getTradeStatisticsForCurrency(List tradeStatisticsList, String currencyCode, boolean showAllTradeCurrencies) { return CompletableFuture.supplyAsync(() -> { synchronized (tradeStatisticsList) { return tradeStatisticsList.stream() .filter(e -> showAllTradeCurrencies || e.getCurrency().equals(currencyCode)) .collect(Collectors.toList()); } }); } static CompletableFuture getUpdateChartResult(List tradeStatisticsByCurrency, TradesChartsViewModel.TickUnit tickUnit, Map> usdAveragePriceMapsPerTickUnit, String currencyCode) { return CompletableFuture.supplyAsync(() -> { // Generate date range and create sets for all ticks Map>> itemsPerInterval = getItemsPerInterval(tradeStatisticsByCurrency, tickUnit); Map usdAveragePriceMap = usdAveragePriceMapsPerTickUnit.get(tickUnit); AtomicLong averageUsdPrice = new AtomicLong(0); // create CandleData for defined time interval List candleDataList = itemsPerInterval.entrySet().stream() .filter(entry -> entry.getKey() >= 0 && !entry.getValue().getValue().isEmpty()) .map(entry -> { long tickStartDate = entry.getValue().getKey().getTime(); // If we don't have a price we take the previous one if (usdAveragePriceMap.containsKey(tickStartDate)) { averageUsdPrice.set(usdAveragePriceMap.get(tickStartDate)); } return getCandleData(entry.getKey(), entry.getValue().getValue(), averageUsdPrice.get(), tickUnit, currencyCode, itemsPerInterval); }) .sorted(Comparator.comparingLong(o -> o.tick)) .collect(Collectors.toList()); List> priceItems = candleDataList.stream() .map(e -> new XYChart.Data(e.tick, e.open, e)) .collect(Collectors.toList()); List> volumeItems = candleDataList.stream() .map(candleData -> new XYChart.Data(candleData.tick, candleData.accumulatedAmount, candleData)) .collect(Collectors.toList()); List> volumeInUsdItems = candleDataList.stream() .map(candleData -> new XYChart.Data(candleData.tick, candleData.volumeInUsd, candleData)) .collect(Collectors.toList()); return new UpdateChartResult(itemsPerInterval, priceItems, volumeItems, volumeInUsdItems); }); } @Getter static class UpdateChartResult { private final Map>> itemsPerInterval; private final List> priceItems; private final List> volumeItems; private final List> volumeInUsdItems; public UpdateChartResult(Map>> itemsPerInterval, List> priceItems, List> volumeItems, List> volumeInUsdItems) { this.itemsPerInterval = itemsPerInterval; this.priceItems = priceItems; this.volumeItems = volumeItems; this.volumeInUsdItems = volumeInUsdItems; } } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// static Map>> getItemsPerInterval(List tradeStatisticsByCurrency, TradesChartsViewModel.TickUnit tickUnit) { // Generate date range and create sets for all ticks Map>> itemsPerInterval = new HashMap<>(); Date time = new Date(); for (long i = MAX_TICKS + 1; i >= 0; --i) { Pair> pair = new Pair<>((Date) time.clone(), new HashSet<>()); itemsPerInterval.put(i, pair); // We adjust the time for the next iteration time.setTime(time.getTime() - 1); time = roundToTick(time, tickUnit); } // Get all entries for the defined time interval tradeStatisticsByCurrency.forEach(tradeStatistics -> { for (long i = MAX_TICKS; i > 0; --i) { Pair> pair = itemsPerInterval.get(i); if (tradeStatistics.getDate().after(pair.getKey())) { pair.getValue().add(tradeStatistics); break; } } }); return itemsPerInterval; } static Date roundToTick(LocalDateTime localDate, TradesChartsViewModel.TickUnit tickUnit) { switch (tickUnit) { case YEAR: return Date.from(localDate.withMonth(1).withDayOfYear(1).withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); case MONTH: return Date.from(localDate.withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); case WEEK: int dayOfWeek = localDate.getDayOfWeek().getValue(); LocalDateTime firstDayOfWeek = ChronoUnit.DAYS.addTo(localDate, 1 - dayOfWeek); return Date.from(firstDayOfWeek.withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); case DAY: return Date.from(localDate.withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); case HOUR: return Date.from(localDate.withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); case MINUTE_10: return Date.from(localDate.withMinute(localDate.getMinute() - localDate.getMinute() % 10).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); default: return Date.from(localDate.atZone(ZONE_ID).toInstant()); } } static Date roundToTick(Date time, TradesChartsViewModel.TickUnit tickUnit) { return roundToTick(time.toInstant().atZone(ChartCalculations.ZONE_ID).toLocalDateTime(), tickUnit); } private static long getAverageTraditionalPrice(List tradeStatisticsList) { BigInteger accumulatedAmount = BigInteger.ZERO; BigInteger accumulatedVolume = BigInteger.ZERO; for (TradeStatistics3 tradeStatistics : tradeStatisticsList) { accumulatedAmount = accumulatedAmount.add(BigInteger.valueOf(tradeStatistics.getAmount())); accumulatedVolume = accumulatedVolume.add(BigInteger.valueOf(tradeStatistics.getTradeVolume().getValue())); } BigInteger accumulatedVolumeAsBI = MathUtils.scaleUpByPowerOf10(accumulatedVolume, TraditionalMoney.SMALLEST_UNIT_EXPONENT + 4); return MathUtils.roundDoubleToLong(HavenoUtils.divide(accumulatedVolumeAsBI, accumulatedAmount)); } @VisibleForTesting static CandleData getCandleData(long tick, Set set, long averageUsdPrice, TradesChartsViewModel.TickUnit tickUnit, String currencyCode, Map>> itemsPerInterval) { long open = 0; long close = 0; long high = 0; long low = 0; BigInteger accumulatedVolume = BigInteger.ZERO; BigInteger accumulatedAmount = BigInteger.ZERO; long numTrades = set.size(); List tradePrices = new ArrayList<>(); for (TradeStatistics3 item : set) { long tradePriceAsLong = item.getTradePrice().getValue(); // Previously a check was done which inverted the low and high for cryptocurrencies. low = (low != 0) ? Math.min(low, tradePriceAsLong) : tradePriceAsLong; high = (high != 0) ? Math.max(high, tradePriceAsLong) : tradePriceAsLong; accumulatedVolume = accumulatedVolume.add(BigInteger.valueOf(item.getTradeVolume().getValue())); accumulatedAmount = accumulatedAmount.add(item.getTradeAmount()); tradePrices.add(tradePriceAsLong); } Collections.sort(tradePrices); List list = new ArrayList<>(set); list.sort(Comparator.comparingLong(TradeStatistics3::getDateAsLong)); if (list.size() > 0) { open = list.get(0).getTradePrice().getValue(); close = list.get(list.size() - 1).getTradePrice().getValue(); } long averagePrice; Long[] prices = new Long[tradePrices.size()]; tradePrices.toArray(prices); long medianPrice = MathUtils.getMedian(prices); boolean isBullish = close > open; int smallestUnitExponent = CurrencyUtil.isCryptoCurrency(currencyCode) ? CryptoMoney.SMALLEST_UNIT_EXPONENT : TraditionalMoney.SMALLEST_UNIT_EXPONENT; BigInteger accumulatedVolumeAsBI = MathUtils.scaleUpByPowerOf10(accumulatedVolume, smallestUnitExponent + 4); averagePrice = MathUtils.roundDoubleToLong(HavenoUtils.divide(accumulatedVolumeAsBI, accumulatedAmount)); Date dateFrom = new Date(getTimeFromTickIndex(tick, itemsPerInterval)); Date dateTo = new Date(getTimeFromTickIndex(tick + 1, itemsPerInterval)); String dateString = tickUnit.ordinal() > TradesChartsViewModel.TickUnit.DAY.ordinal() ? DisplayUtils.formatDateTimeSpan(dateFrom, dateTo) : DisplayUtils.formatDate(dateFrom) + " - " + DisplayUtils.formatDate(dateTo); // We do not need precision, so we scale down before multiplication otherwise we could get an overflow. averageUsdPrice = (long) MathUtils.scaleDownByPowerOf10((double) averageUsdPrice, smallestUnitExponent); long volumeInUsd = averageUsdPrice * MathUtils.scaleDownByPowerOf10(accumulatedAmount, 4).longValue(); // We store USD value without decimals as its only total volume, no precision is needed. volumeInUsd = (long) MathUtils.scaleDownByPowerOf10((double) volumeInUsd, smallestUnitExponent); return new CandleData(tick, open, close, high, low, averagePrice, medianPrice, accumulatedAmount.longValueExact(), accumulatedVolume.longValueExact(), numTrades, isBullish, dateString, volumeInUsd); } static long getTimeFromTickIndex(long tick, Map>> itemsPerInterval) { if (tick > MAX_TICKS + 1 || itemsPerInterval.get(tick) == null) { return 0; } return itemsPerInterval.get(tick).getKey().getTime(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/market/trades/TradeStatistics3ListItem.java ================================================ /* * 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 . */ package haveno.desktop.main.market.trades; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; import haveno.core.trade.statistics.TradeStatistics3; import haveno.core.util.FormattingUtils; import haveno.core.util.VolumeUtil; import haveno.desktop.util.DisplayUtils; import lombok.experimental.Delegate; import org.jetbrains.annotations.Nullable; public class TradeStatistics3ListItem { @Delegate private final TradeStatistics3 tradeStatistics3; private final boolean showAllTradeCurrencies; private String dateString; private String market; private String priceString; private String volumeString; private String paymentMethodString; private String amountString; public TradeStatistics3ListItem(@Nullable TradeStatistics3 tradeStatistics3, boolean showAllTradeCurrencies) { this.tradeStatistics3 = tradeStatistics3; this.showAllTradeCurrencies = showAllTradeCurrencies; } public String getDateString() { if (dateString == null) { dateString = tradeStatistics3 != null ? DisplayUtils.formatDateTime(tradeStatistics3.getDate()) : ""; } return dateString; } public String getMarket() { if (market == null) { market = tradeStatistics3 != null ? CurrencyUtil.getCurrencyPair(tradeStatistics3.getCurrency()) : ""; } return market; } public String getPriceString() { if (priceString == null) { priceString = tradeStatistics3 != null ? FormattingUtils.formatPrice(tradeStatistics3.getTradePrice()) : ""; } return priceString; } public String getVolumeString() { if (volumeString == null) { volumeString = tradeStatistics3 != null ? showAllTradeCurrencies ? VolumeUtil.formatVolumeWithCode(tradeStatistics3.getTradeVolume()) : VolumeUtil.formatVolume(tradeStatistics3.getTradeVolume()) : ""; } return volumeString; } public String getPaymentMethodString() { if (paymentMethodString == null) { paymentMethodString = tradeStatistics3 != null ? Res.get(tradeStatistics3.getPaymentMethodId()) : ""; } return paymentMethodString; } public String getAmountString() { if (amountString == null) { amountString = tradeStatistics3 != null ? HavenoUtils.formatXmr(getAmount(), false, 4) : ""; } return amountString; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java ================================================ /* * 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 . */ package haveno.desktop.main.market.trades; import com.google.inject.Inject; import com.google.inject.name.Named; import com.googlecode.jcsv.writer.CSVEntryConverter; import com.jfoenix.controls.JFXTabPane; import haveno.common.UserThread; import haveno.common.util.MathUtils; import haveno.common.util.Tuple2; import haveno.common.util.Tuple3; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.monetary.Price; import haveno.core.trade.HavenoUtils; import haveno.core.trade.statistics.TradeStatistics3; import haveno.core.user.CookieKey; import haveno.core.user.User; import haveno.core.util.FormattingUtils; import haveno.core.util.VolumeUtil; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.common.view.ActivatableViewAndModel; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.AutoTooltipSlideToggleButton; import haveno.desktop.components.AutoTooltipTableColumn; import haveno.desktop.components.AutoTooltipToggleButton; import haveno.desktop.components.AutocompleteComboBox; import haveno.desktop.components.ColoredDecimalPlacesWithZerosText; import static haveno.desktop.main.market.trades.TradesChartsViewModel.MAX_TICKS; import haveno.desktop.main.market.trades.charts.price.CandleStickChart; import haveno.desktop.main.market.trades.charts.volume.VolumeChart; import haveno.desktop.util.CurrencyListItem; import haveno.desktop.util.DisplayUtils; import static haveno.desktop.util.FormBuilder.addTopLabelAutocompleteComboBox; import static haveno.desktop.util.FormBuilder.getTopLabelWithVBox; import haveno.desktop.util.GUIUtil; import java.text.DecimalFormat; import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.SortedList; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; import javafx.scene.control.ComboBox; import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.control.SingleSelectionModel; import javafx.scene.control.Tab; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.Toggle; import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleGroup; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.stage.Stage; import javafx.util.Callback; import javafx.util.StringConverter; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; import org.fxmisc.easybind.monadic.MonadicBinding; import org.jetbrains.annotations.NotNull; @FxmlView public class TradesChartsView extends ActivatableViewAndModel { private static final int SHOW_ALL = 0; static class CurrencyStringConverter extends StringConverter { private final ComboBox comboBox; CurrencyStringConverter(ComboBox comboBox) { this.comboBox = comboBox; } @Override public String toString(CurrencyListItem currencyItem) { return currencyItem != null ? currencyItem.codeDashNameString() : ""; } @Override public CurrencyListItem fromString(String query) { if (comboBox.getItems().isEmpty()) return null; if (query.isEmpty()) return null; return comboBox.getItems().stream(). filter(currencyItem -> currencyItem.codeDashNameString().equals(query)). findAny().orElse(null); } } private final User user; private final CoinFormatter coinFormatter; private VolumeChart volumeChart, volumeInUsdChart; private CandleStickChart priceChart; private AutocompleteComboBox currencyComboBox; private TableView tableView; private Hyperlink exportLink; private HBox toolBox; private Pane rootParent; private AnchorPane priceChartPane, volumeChartPane; private HBox footer; private AutoTooltipSlideToggleButton showVolumeAsUsdToggleButton; private Label nrOfTradeStatisticsLabel; private ToggleGroup toggleGroup; private SingleSelectionModel tabPaneSelectionModel; private TableColumn priceColumn, volumeColumn, marketColumn; private SortedList sortedList = new SortedList<>(FXCollections.observableArrayList()); private ChangeListener timeUnitChangeListener; private ChangeListener priceAxisYWidthListener; private ChangeListener volumeAxisYWidthListener; private ChangeListener selectedTabIndexListener; private ChangeListener parentHeightListener; private ChangeListener priceColumnLabelListener; private ListChangeListener> itemsChangeListener; private ListChangeListener tradeStatisticsByCurrencyListener; @SuppressWarnings("FieldCanBeLocal") private MonadicBinding currencySelectionBinding; private Subscription currencySelectionSubscriber; private final StringProperty priceColumnLabel = new SimpleStringProperty(); private NumberAxis priceAxisX, priceAxisY, volumeAxisY, volumeAxisX, volumeInUsdAxisX; private XYChart.Series priceSeries; private final XYChart.Series volumeSeries = new XYChart.Series<>(); private final XYChart.Series volumeInUsdSeries = new XYChart.Series<>(); private double priceAxisYWidth; private double volumeAxisYWidth; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @SuppressWarnings("WeakerAccess") @Inject public TradesChartsView(TradesChartsViewModel model, User user, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter coinFormatter) { super(model); this.user = user; this.coinFormatter = coinFormatter; } @Override public void initialize() { root.setAlignment(Pos.CENTER_LEFT); toolBox = getToolBox(); createCharts(); createTable(); footer = new HBox(); VBox.setVgrow(footer, Priority.ALWAYS); Pane spacer = new Pane(); HBox.setHgrow(spacer, Priority.ALWAYS); nrOfTradeStatisticsLabel = new AutoTooltipLabel(" "); // set empty string for layout nrOfTradeStatisticsLabel.setPadding(new Insets(-2, 0, -10, 12)); exportLink = new Hyperlink(Res.get("shared.exportCSV")); exportLink.setPadding(new Insets(-2, 12, -10, 0)); footer.getChildren().addAll(nrOfTradeStatisticsLabel, spacer, exportLink); root.getChildren().addAll(toolBox, priceChartPane, volumeChartPane, tableView, footer); timeUnitChangeListener = (observable, oldValue, newValue) -> UserThread.execute(() -> { if (newValue != null) { model.setTickUnit((TradesChartsViewModel.TickUnit) newValue.getUserData()); priceAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); volumeAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); volumeInUsdAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); } }); priceAxisYWidthListener = (observable, oldValue, newValue) -> UserThread.execute(() -> { priceAxisYWidth = (double) newValue; layoutChart(); }); volumeAxisYWidthListener = (observable, oldValue, newValue) -> UserThread.execute(() -> { volumeAxisYWidth = (double) newValue; layoutChart(); }); tradeStatisticsByCurrencyListener = c -> UserThread.execute(() -> { nrOfTradeStatisticsLabel.setText(Res.get("market.trades.nrOfTrades", model.tradeStatisticsByCurrency.size())); fillList(); }); parentHeightListener = (observable, oldValue, newValue) -> UserThread.execute(this::layout); priceColumnLabelListener = (o, oldVal, newVal) -> UserThread.execute(() -> priceColumn.setGraphic(new AutoTooltipLabel(newVal))); // Need to render on next frame as otherwise there are issues in the chart rendering itemsChangeListener = c -> UserThread.execute(this::updateChartData); currencySelectionBinding = EasyBind.combine( model.showAllTradeCurrenciesProperty, model.selectedTradeCurrencyProperty, (showAll, selectedTradeCurrency) -> { UserThread.execute(() -> { priceChart.setVisible(!showAll); priceChart.setManaged(!showAll); priceColumn.setSortable(!showAll); if (showAll) { volumeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amount"))); priceColumnLabel.set(Res.get("shared.price")); if (!tableView.getColumns().contains(marketColumn)) tableView.getColumns().add(1, marketColumn); volumeChart.setPrefHeight(volumeChart.getMaxHeight()); volumeInUsdChart.setPrefHeight(volumeInUsdChart.getMaxHeight()); } else { volumeChart.setPrefHeight(volumeChart.getMinHeight()); volumeInUsdChart.setPrefHeight(volumeInUsdChart.getMinHeight()); priceSeries.setName(selectedTradeCurrency.getName()); String code = selectedTradeCurrency.getCode(); volumeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amountWithCur", code))); priceColumnLabel.set(CurrencyUtil.getPriceWithCurrencyCode(code)); tableView.getColumns().remove(marketColumn); } layout(); }); return null; }); } @Override protected void activate() { tabPaneSelectionModel = GUIUtil.getParentOfType(root, JFXTabPane.class).getSelectionModel(); selectedTabIndexListener = (observable, oldValue, newValue) -> model.setSelectedTabIndex((int) newValue); model.setSelectedTabIndex(tabPaneSelectionModel.getSelectedIndex()); tabPaneSelectionModel.selectedIndexProperty().addListener(selectedTabIndexListener); currencyComboBox.setConverter(new CurrencyStringConverter(currencyComboBox)); currencyComboBox.getEditor().getStyleClass().add("combo-box-editor-bold"); currencyComboBox.setAutocompleteItems(model.getCurrencyListItems()); currencyComboBox.setVisibleRowCount(10); if (model.showAllTradeCurrenciesProperty.get()) currencyComboBox.getSelectionModel().select(SHOW_ALL); else if (model.getSelectedCurrencyListItem().isPresent()) currencyComboBox.getSelectionModel().select(model.getSelectedCurrencyListItem().get()); currencyComboBox.getEditor().setText(new CurrencyStringConverter(currencyComboBox).toString(currencyComboBox.getSelectionModel().getSelectedItem())); currencyComboBox.setOnChangeConfirmed(e -> UserThread.execute(() -> { if (currencyComboBox.getEditor().getText().isEmpty()) return; CurrencyListItem selectedItem = currencyComboBox.getSelectionModel().getSelectedItem(); if (selectedItem != null) { model.onSetTradeCurrency(selectedItem.tradeCurrency); } })); toggleGroup.getToggles().get(model.tickUnit.ordinal()).setSelected(true); model.priceItems.addListener(itemsChangeListener); toggleGroup.selectedToggleProperty().addListener(timeUnitChangeListener); priceAxisY.widthProperty().addListener(priceAxisYWidthListener); volumeAxisY.widthProperty().addListener(volumeAxisYWidthListener); model.tradeStatisticsByCurrency.addListener(tradeStatisticsByCurrencyListener); priceAxisY.labelProperty().bind(priceColumnLabel); priceColumnLabel.addListener(priceColumnLabelListener); currencySelectionSubscriber = currencySelectionBinding.subscribe((observable, oldValue, newValue) -> { }); boolean useAnimations = model.preferences.isUseAnimations(); priceChart.setAnimated(useAnimations); volumeChart.setAnimated(useAnimations); volumeInUsdChart.setAnimated(useAnimations); nrOfTradeStatisticsLabel.setText(Res.get("market.trades.nrOfTrades", model.tradeStatisticsByCurrency.size())); exportLink.setOnAction(e -> exportToCsv()); if (root.getParent() instanceof Pane) { rootParent = (Pane) root.getParent(); rootParent.heightProperty().addListener(parentHeightListener); } user.getCookie().getAsOptionalBoolean(CookieKey.TRADE_STAT_CHART_USE_USD).ifPresent(showUsd -> { showVolumeAsUsdToggleButton.setSelected(showUsd); showVolumeAsUsd(showUsd); }); showVolumeAsUsdToggleButton.setOnAction(e -> { boolean selected = showVolumeAsUsdToggleButton.isSelected(); showVolumeAsUsd(selected); user.getCookie().putAsBoolean(CookieKey.TRADE_STAT_CHART_USE_USD, selected); user.requestPersistence(); }); layout(); } @Override protected void deactivate() { tabPaneSelectionModel.selectedIndexProperty().removeListener(selectedTabIndexListener); model.priceItems.removeListener(itemsChangeListener); toggleGroup.selectedToggleProperty().removeListener(timeUnitChangeListener); priceAxisY.widthProperty().removeListener(priceAxisYWidthListener); volumeAxisY.widthProperty().removeListener(volumeAxisYWidthListener); model.tradeStatisticsByCurrency.removeListener(tradeStatisticsByCurrencyListener); priceAxisY.labelProperty().unbind(); priceColumnLabel.removeListener(priceColumnLabelListener); currencySelectionSubscriber.unsubscribe(); sortedList.comparatorProperty().unbind(); priceSeries.getData().clear(); priceChart.getData().clear(); exportLink.setOnAction(null); showVolumeAsUsdToggleButton.setOnAction(null); if (rootParent != null) { rootParent.heightProperty().removeListener(parentHeightListener); } } private void showVolumeAsUsd(Boolean showUsd) { volumeChart.setVisible(!showUsd); volumeChart.setManaged(!showUsd); volumeInUsdChart.setVisible(showUsd); volumeInUsdChart.setManaged(showUsd); } private void fillList() { long ts = System.currentTimeMillis(); CompletableFuture.supplyAsync(() -> { return model.tradeStatisticsByCurrency.stream() .map(tradeStatistics -> new TradeStatistics3ListItem(tradeStatistics, model.showAllTradeCurrenciesProperty.get())) .collect(Collectors.toCollection(FXCollections::observableArrayList)); }).whenComplete((listItems, throwable) -> { log.debug("Creating listItems took {} ms", System.currentTimeMillis() - ts); long ts2 = System.currentTimeMillis(); sortedList.comparatorProperty().unbind(); // Sorting is slow as we have > 100k items. So we prefer to do it on the non UI thread. sortedList = new SortedList<>(listItems); sortedList.comparatorProperty().bind(tableView.comparatorProperty()); log.debug("Created sorted list took {} ms", System.currentTimeMillis() - ts2); UserThread.execute(() -> { // When we attach the list to the table we need to be on the UI thread. tableView.setItems(sortedList); }); }); } private void exportToCsv() { ObservableList> tableColumns = GUIUtil.getContentColumns(tableView); int reportColumns = tableColumns.size() + 1; boolean showAllTradeCurrencies = model.showAllTradeCurrenciesProperty.get(); CSVEntryConverter headerConverter = item -> { String[] columns = new String[reportColumns]; columns[0] = "Epoch time in ms"; for (int i = 0; i < tableColumns.size(); i++) { columns[(i + 1)] = ((AutoTooltipLabel) tableColumns.get(i).getGraphic()).getText(); } return columns; }; CSVEntryConverter contentConverter; if (showAllTradeCurrencies) { contentConverter = item -> { String[] columns = new String[reportColumns]; columns[0] = String.valueOf(item.getDateAsLong()); columns[1] = item.getDateString(); columns[2] = item.getMarket(); columns[3] = item.getPriceString(); columns[4] = item.getAmountString(); columns[5] = item.getVolumeString(); columns[6] = item.getPaymentMethodString(); return columns; }; } else { contentConverter = item -> { String[] columns = new String[reportColumns]; columns[0] = String.valueOf(item.getDateAsLong()); columns[1] = item.getDateString(); columns[2] = item.getPriceString(); columns[3] = item.getAmountString(); columns[4] = item.getVolumeString(); columns[5] = item.getPaymentMethodString(); return columns; }; } String details = showAllTradeCurrencies ? "all-markets" : model.getCurrencyCode(); GUIUtil.exportCSV("trade-statistics-" + details + ".csv", headerConverter, contentConverter, new TradeStatistics3ListItem(null, showAllTradeCurrencies), sortedList, (Stage) root.getScene().getWindow()); } /////////////////////////////////////////////////////////////////////////////////////////// // Chart /////////////////////////////////////////////////////////////////////////////////////////// private void createCharts() { priceSeries = new XYChart.Series<>(); priceAxisX = new NumberAxis(0, MAX_TICKS + 1, 1); priceAxisX.setTickUnit(4); priceAxisX.setMinorTickCount(4); priceAxisX.setMinorTickVisible(true); priceAxisX.setForceZeroInRange(false); addTickMarkLabelCssClass(priceAxisX, "axis-tick-mark-text-node"); priceAxisY = new NumberAxis(); priceAxisY.setForceZeroInRange(false); priceAxisY.setAutoRanging(true); priceAxisY.setTickLabelFormatter(new StringConverter<>() { @Override public String toString(Number object) { String currencyCode = model.getCurrencyCode(); double doubleValue = (double) object; if (CurrencyUtil.isCryptoCurrency(currencyCode)) { final double value = MathUtils.scaleDownByPowerOf10(doubleValue, 8); return FormattingUtils.formatRoundedDoubleWithPrecision(value, 8).replaceFirst("0{3}$", ""); } else { DecimalFormat df = new DecimalFormat(",###"); return df.format(Double.parseDouble(FormattingUtils.formatPrice(Price.valueOf(currencyCode, MathUtils.doubleToLong(doubleValue))))); } } @Override public Number fromString(String string) { return null; } }); priceChart = new CandleStickChart(priceAxisX, priceAxisY, new StringConverter<>() { @Override public String toString(Number object) { if (CurrencyUtil.isCryptoCurrency(model.getCurrencyCode())) { final double value = MathUtils.scaleDownByPowerOf10((long) object, 8); return FormattingUtils.formatRoundedDoubleWithPrecision(value, 8); } else { return FormattingUtils.formatPrice(Price.valueOf(model.getCurrencyCode(), (long) object)); } } @Override public Number fromString(String string) { return null; } }); priceChart.setId("price-chart"); priceChart.setMinHeight(188); priceChart.setPrefHeight(188); priceChart.setMaxHeight(300); priceChart.setLegendVisible(false); priceChart.setPadding(new Insets(0)); priceChart.setData(FXCollections.observableArrayList(List.of(priceSeries))); priceChartPane = new AnchorPane(); priceChartPane.getStyleClass().add("chart-pane"); AnchorPane.setTopAnchor(priceChart, 15d); AnchorPane.setBottomAnchor(priceChart, 10d); AnchorPane.setLeftAnchor(priceChart, 0d); AnchorPane.setRightAnchor(priceChart, 10d); priceChartPane.getChildren().add(priceChart); volumeAxisX = new NumberAxis(0, MAX_TICKS + 1, 1); volumeAxisY = new NumberAxis(); volumeChart = getVolumeChart(volumeAxisX, volumeAxisY, volumeSeries, "XMR"); volumeInUsdAxisX = new NumberAxis(0, MAX_TICKS + 1, 1); NumberAxis volumeInUsdAxisY = new NumberAxis(); volumeInUsdChart = getVolumeChart(volumeInUsdAxisX, volumeInUsdAxisY, volumeInUsdSeries, "USD"); volumeInUsdChart.setVisible(false); volumeInUsdChart.setManaged(false); showVolumeAsUsdToggleButton = new AutoTooltipSlideToggleButton(); showVolumeAsUsdToggleButton.setText(Res.get("market.trades.showVolumeInUSD")); showVolumeAsUsdToggleButton.setPadding(new Insets(-15, 0, 0, 10)); VBox vBox = new VBox(); AnchorPane.setTopAnchor(vBox, 15d); AnchorPane.setBottomAnchor(vBox, 10d); AnchorPane.setLeftAnchor(vBox, 0d); AnchorPane.setRightAnchor(vBox, 10d); vBox.getChildren().addAll(showVolumeAsUsdToggleButton, volumeChart, volumeInUsdChart); volumeChartPane = new AnchorPane(); volumeChartPane.getStyleClass().add("chart-pane"); volumeChartPane.getChildren().add(vBox); } private VolumeChart getVolumeChart(NumberAxis axisX, NumberAxis axisY, XYChart.Series series, String currency) { axisX.setTickUnit(4); axisX.setMinorTickCount(4); axisX.setMinorTickVisible(true); axisX.setForceZeroInRange(false); addTickMarkLabelCssClass(axisX, "axis-tick-mark-text-node"); axisY.setForceZeroInRange(true); axisY.setAutoRanging(true); axisY.setLabel(Res.get("shared.volumeWithCur", currency)); axisY.setTickLabelFormatter(new StringConverter<>() { @Override public String toString(Number volume) { return currency.equals("XMR") ? HavenoUtils.formatXmr(MathUtils.doubleToLong((double) volume)) : VolumeUtil.formatLargeFiatWithUnitPostFix((double) volume, "USD"); } @Override public Number fromString(String string) { return null; } }); StringConverter xmrStringConverter = new StringConverter<>() { @Override public String toString(Number volume) { return HavenoUtils.formatXmr((long) volume, true); } @Override public Number fromString(String string) { return null; } }; VolumeChart volumeChart = new VolumeChart(axisX, axisY, xmrStringConverter); volumeChart.setId("volume-chart"); volumeChart.setData(FXCollections.observableArrayList(List.of(series))); volumeChart.setMinHeight(138); volumeChart.setPrefHeight(138); volumeChart.setMaxHeight(200); volumeChart.setLegendVisible(false); volumeChart.setPadding(new Insets(0)); return volumeChart; } private void updateChartData() { volumeSeries.getData().setAll(model.volumeItems); volumeInUsdSeries.getData().setAll(model.volumeInUsdItems); // At price chart we need to set the priceSeries new otherwise the lines are not rendered correctly // TODO should be fixed in candle chart priceSeries.getData().clear(); priceSeries = new XYChart.Series<>(); priceSeries.getData().setAll(model.priceItems); priceChart.getData().clear(); priceChart.setData(FXCollections.observableArrayList(List.of(priceSeries))); priceAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); volumeAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); volumeInUsdAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); } private void layoutChart() { UserThread.execute(() -> { if (volumeAxisYWidth > priceAxisYWidth) { priceChart.setPadding(new Insets(0, 0, 0, volumeAxisYWidth - priceAxisYWidth)); volumeChart.setPadding(new Insets(0, 0, 0, 0)); volumeInUsdChart.setPadding(new Insets(0, 0, 0, 0)); } else if (volumeAxisYWidth < priceAxisYWidth) { priceChart.setPadding(new Insets(0, 0, 0, 0)); volumeChart.setPadding(new Insets(0, 0, 0, priceAxisYWidth - volumeAxisYWidth)); volumeInUsdChart.setPadding(new Insets(0, 0, 0, priceAxisYWidth - volumeAxisYWidth)); } }); } @NotNull private StringConverter getTimeAxisStringConverter() { return new StringConverter<>() { @Override public String toString(Number object) { long index = MathUtils.doubleToLong((double) object); // The last tick is on the chart edge, it is not well spaced with // the previous tick and interferes with its label. if (MAX_TICKS + 1 == index) return ""; long time = model.getTimeFromTickIndex(index); String fmt = ""; switch (model.tickUnit) { case YEAR: fmt = "yyyy"; break; case MONTH: fmt = "MMMyy"; break; case WEEK: case DAY: fmt = "dd/MMM\nyyyy"; break; case HOUR: case MINUTE_10: fmt = "HH:mm\ndd/MMM"; break; default: // nothing here } return DisplayUtils.formatDateAxis(new Date(time), fmt); } @Override public Number fromString(String string) { return null; } }; } private void addTickMarkLabelCssClass(NumberAxis axis, String cssClass) { // grab the axis tick mark label (text object) and add a CSS class. axis.getChildrenUnmodifiable().addListener((ListChangeListener) c -> { while (c.next()) { if (c.wasAdded()) { for (Node mark : c.getAddedSubList()) { if (mark instanceof Text) { mark.getStyleClass().add(cssClass); } } } } }); } /////////////////////////////////////////////////////////////////////////////////////////// // CurrencyComboBox /////////////////////////////////////////////////////////////////////////////////////////// private HBox getToolBox() { final Tuple3> currencyComboBoxTuple = addTopLabelAutocompleteComboBox( Res.get("shared.currency")); currencyComboBox = currencyComboBoxTuple.third; currencyComboBox.setCellFactory(GUIUtil.getCurrencyListItemCellFactory(Res.get("shared.trade"), Res.get("shared.trades"), model.preferences)); currencyComboBox.getStyleClass().add("input-with-border"); Pane spacer = new Pane(); HBox.setHgrow(spacer, Priority.ALWAYS); toggleGroup = new ToggleGroup(); ToggleButton year = getToggleButton(Res.get("time.year"), TradesChartsViewModel.TickUnit.YEAR, toggleGroup, "toggle-left"); ToggleButton month = getToggleButton(Res.get("time.month"), TradesChartsViewModel.TickUnit.MONTH, toggleGroup, "toggle-center"); ToggleButton week = getToggleButton(Res.get("time.week"), TradesChartsViewModel.TickUnit.WEEK, toggleGroup, "toggle-center"); ToggleButton day = getToggleButton(Res.get("time.day"), TradesChartsViewModel.TickUnit.DAY, toggleGroup, "toggle-center"); ToggleButton hour = getToggleButton(Res.get("time.hour"), TradesChartsViewModel.TickUnit.HOUR, toggleGroup, "toggle-center"); ToggleButton minute10 = getToggleButton(Res.get("time.minute10"), TradesChartsViewModel.TickUnit.MINUTE_10, toggleGroup, "toggle-right"); HBox toggleBox = new HBox(); toggleBox.setSpacing(0); toggleBox.setAlignment(Pos.CENTER_LEFT); toggleBox.getChildren().addAll(year, month, week, day, hour, minute10); final Tuple2 topLabelWithVBox = getTopLabelWithVBox(Res.get("shared.interval"), toggleBox); HBox hBox = new HBox(); hBox.setSpacing(0); hBox.setAlignment(Pos.CENTER_LEFT); hBox.getChildren().addAll(currencyComboBoxTuple.first, spacer, topLabelWithVBox.second); return hBox; } private ToggleButton getToggleButton(String label, TradesChartsViewModel.TickUnit tickUnit, ToggleGroup toggleGroup, String style) { ToggleButton toggleButton = new AutoTooltipToggleButton(label); toggleButton.setUserData(tickUnit); toggleButton.setToggleGroup(toggleGroup); toggleButton.setId(style); return toggleButton; } /////////////////////////////////////////////////////////////////////////////////////////// // Table /////////////////////////////////////////////////////////////////////////////////////////// private void createTable() { tableView = new TableView<>(); GUIUtil.applyTableStyle(tableView); VBox.setVgrow(tableView, Priority.ALWAYS); tableView.getStyleClass().add("non-interactive-table"); // date TableColumn dateColumn = new AutoTooltipTableColumn<>(Res.get("shared.dateTime")) { { setMinWidth(240); setMaxWidth(240); } }; dateColumn.getStyleClass().addAll("number-column"); dateColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue())); dateColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final TradeStatistics3ListItem item, boolean empty) { super.updateItem(item, empty); if (item != null) { setText(item.getDateString()); } else setText(""); } }; } }); dateColumn.setComparator(Comparator.comparing(TradeStatistics3ListItem::getDate)); tableView.getColumns().add(dateColumn); // market marketColumn = new AutoTooltipTableColumn<>(Res.get("shared.market")) { { setMinWidth(130); setMaxWidth(130); } }; marketColumn.getStyleClass().add("number-column"); marketColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue())); marketColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final TradeStatistics3ListItem item, boolean empty) { super.updateItem(item, empty); if (item != null) setText(item.getMarket()); else setText(""); } }; } }); marketColumn.setComparator(Comparator.comparing(TradeStatistics3ListItem::getMarket)); tableView.getColumns().add(marketColumn); // price priceColumn = new TableColumn<>(); priceColumn.getStyleClass().add("number-column"); priceColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue())); priceColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final TradeStatistics3ListItem item, boolean empty) { super.updateItem(item, empty); if (item != null) setText(item.getPriceString()); else setText(""); } }; } }); priceColumn.setComparator(Comparator.comparing(TradeStatistics3ListItem::getTradePrice)); tableView.getColumns().add(priceColumn); // amount TableColumn amountColumn = new AutoTooltipTableColumn<>(Res.get("shared.amountWithCur", Res.getBaseCurrencyCode())); amountColumn.getStyleClass().add("number-column"); amountColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue())); amountColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final TradeStatistics3ListItem item, boolean empty) { super.updateItem(item, empty); if (item != null) { setGraphic(new ColoredDecimalPlacesWithZerosText(item.getAmountString(), GUIUtil.AMOUNT_DECIMALS_WITH_ZEROS)); } else setGraphic(null); } }; } }); amountColumn.setComparator(Comparator.comparing(TradeStatistics3ListItem::getTradeAmount)); tableView.getColumns().add(amountColumn); // volume volumeColumn = new TableColumn<>(); volumeColumn.getStyleClass().add("number-column"); volumeColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue())); volumeColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final TradeStatistics3ListItem item, boolean empty) { super.updateItem(item, empty); if (item != null) setText(item.getVolumeString()); else setText(""); } }; } }); volumeColumn.setComparator(Comparator.comparing(TradeStatistics3ListItem::getTradeVolume)); tableView.getColumns().add(volumeColumn); // paymentMethod TableColumn paymentMethodColumn = new AutoTooltipTableColumn<>(Res.get("shared.paymentMethod")); paymentMethodColumn.getStyleClass().addAll("number-column"); paymentMethodColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue())); paymentMethodColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final TradeStatistics3ListItem item, boolean empty) { super.updateItem(item, empty); if (item != null) setText(item.getPaymentMethodString()); else setText(""); } }; } }); paymentMethodColumn.setComparator(Comparator.comparing(TradeStatistics3ListItem::getPaymentMethodString)); tableView.getColumns().add(paymentMethodColumn); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); Label placeholder = new AutoTooltipLabel(Res.get("table.placeholder.noData")); placeholder.setWrapText(true); tableView.setPlaceholder(placeholder); dateColumn.setSortType(TableColumn.SortType.DESCENDING); tableView.getSortOrder().add(dateColumn); } private void layout() { double available; if (root.getParent() instanceof Pane) { available = ((Pane) root.getParent()).getHeight(); } else { available = root.getHeight(); } if (available == 0) { UserThread.execute(this::layout); return; } available = available - volumeChartPane.getHeight() - toolBox.getHeight() - footer.getHeight() - 60; if (!model.showAllTradeCurrenciesProperty.get()) { double priceChartPaneHeight = priceChartPane.getHeight(); if (priceChartPaneHeight == 0) { UserThread.execute(this::layout); return; } available -= priceChartPaneHeight; } else { // If rendering is not done we get the height which is smaller than the volumeChart max Height so we // delay to next render frame. // Using runAfter does not work well as filling the table list and creating the chart can be a bit slow and // its hard to estimate correct delay. if (volumeChartPane.getHeight() < volumeChart.getMaxHeight()) { UserThread.execute(this::layout); return; } } tableView.setPrefHeight(available); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.market.trades; import com.google.inject.Inject; import haveno.common.UserThread; import haveno.common.util.CompletableFutureUtils; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.GlobalSettings; import haveno.core.locale.TradeCurrency; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.statistics.TradeStatistics3; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.Preferences; import haveno.desktop.Navigation; import haveno.desktop.common.model.ActivatableViewModel; import haveno.desktop.main.MainView; import haveno.desktop.main.settings.SettingsView; import haveno.desktop.main.settings.preferences.PreferencesView; import haveno.desktop.util.CurrencyList; import haveno.desktop.util.CurrencyListItem; import haveno.desktop.util.GUIUtil; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.scene.chart.XYChart; import javafx.util.Pair; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; class TradesChartsViewModel extends ActivatableViewModel { static final int MAX_TICKS = 90; private static final int TAB_INDEX = 2; /////////////////////////////////////////////////////////////////////////////////////////// // Enum /////////////////////////////////////////////////////////////////////////////////////////// public enum TickUnit { YEAR, MONTH, WEEK, DAY, HOUR, MINUTE_10 } private final TradeStatisticsManager tradeStatisticsManager; final Preferences preferences; private final PriceFeedService priceFeedService; private final Navigation navigation; private final ListChangeListener listChangeListener; final ObjectProperty selectedTradeCurrencyProperty = new SimpleObjectProperty<>(); final BooleanProperty showAllTradeCurrenciesProperty = new SimpleBooleanProperty(false); private final CurrencyList currencyListItems; private final CurrencyListItem showAllCurrencyListItem = new CurrencyListItem(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, ""), -1); final ObservableList tradeStatisticsByCurrency = FXCollections.observableArrayList(); final ObservableList> priceItems = FXCollections.observableArrayList(); final ObservableList> volumeItems = FXCollections.observableArrayList(); final ObservableList> volumeInUsdItems = FXCollections.observableArrayList(); private final Map>> itemsPerInterval = new HashMap<>(); TickUnit tickUnit; private int selectedTabIndex; final Map> usdAveragePriceMapsPerTickUnit = new HashMap<>(); private boolean fillTradeCurrenciesOnActivateCalled; private volatile boolean deactivateCalled; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject TradesChartsViewModel(TradeStatisticsManager tradeStatisticsManager, Preferences preferences, PriceFeedService priceFeedService, Navigation navigation) { this.tradeStatisticsManager = tradeStatisticsManager; this.preferences = preferences; this.priceFeedService = priceFeedService; this.navigation = navigation; listChangeListener = change -> { applyAsyncTradeStatisticsForCurrency(getCurrencyCode()) .whenComplete((result, throwable) -> { if (deactivateCalled) { return; } if (throwable != null) { log.error("Error at setChangeListener/applyAsyncTradeStatisticsForCurrency. {}", throwable.toString()); return; } applyAsyncChartData(); }); fillTradeCurrencies(); }; String tradeChartsScreenCurrencyCode = preferences.getTradeChartsScreenCurrencyCode(); showAllTradeCurrenciesProperty.set(isShowAllEntry(tradeChartsScreenCurrencyCode)); TradeCurrency tradeCurrency = CurrencyUtil.getTradeCurrency(tradeChartsScreenCurrencyCode) .orElse(GlobalSettings.getDefaultTradeCurrency()); selectedTradeCurrencyProperty.set(tradeCurrency); tickUnit = TickUnit.values()[preferences.getTradeStatisticsTickUnitIndex()]; currencyListItems = new CurrencyList(this.preferences); } @Override protected void activate() { long ts = System.currentTimeMillis(); deactivateCalled = false; synchronized (tradeStatisticsManager.getObservableTradeStatisticsList()) { tradeStatisticsManager.getObservableTradeStatisticsList().addListener(listChangeListener); } if (!fillTradeCurrenciesOnActivateCalled) { fillTradeCurrencies(); fillTradeCurrenciesOnActivateCalled = true; } syncPriceFeedCurrency(); setMarketPriceFeedCurrency(); List> allFutures = new ArrayList<>(); CompletableFuture task1Done = new CompletableFuture<>(); allFutures.add(task1Done); CompletableFuture task2Done = new CompletableFuture<>(); allFutures.add(task2Done); CompletableFutureUtils.allOf(allFutures) .whenComplete((res, throwable) -> { if (deactivateCalled) { return; } if (throwable != null) { log.error(throwable.toString()); return; } //Once applyAsyncUsdAveragePriceMapsPerTickUnit and applyAsyncTradeStatisticsForCurrency are // both completed we call applyAsyncChartData UserThread.execute(this::applyAsyncChartData); }); // We call applyAsyncUsdAveragePriceMapsPerTickUnit and applyAsyncTradeStatisticsForCurrency // in parallel for better performance applyAsyncUsdAveragePriceMapsPerTickUnit(task1Done); applyAsyncTradeStatisticsForCurrency(getCurrencyCode(), task2Done); log.debug("activate took {}", System.currentTimeMillis() - ts); } @Override protected void deactivate() { deactivateCalled = true; synchronized (tradeStatisticsManager.getObservableTradeStatisticsList()) { tradeStatisticsManager.getObservableTradeStatisticsList().removeListener(listChangeListener); } // We want to avoid to trigger listeners in the view so we delay a bit. Deactivate on model is called before // deactivate on view. UserThread.execute(() -> { usdAveragePriceMapsPerTickUnit.clear(); tradeStatisticsByCurrency.clear(); priceItems.clear(); volumeItems.clear(); volumeInUsdItems.clear(); itemsPerInterval.clear(); }); } /////////////////////////////////////////////////////////////////////////////////////////// // Async calls /////////////////////////////////////////////////////////////////////////////////////////// private void applyAsyncUsdAveragePriceMapsPerTickUnit(CompletableFuture completeFuture) { long ts = System.currentTimeMillis(); ChartCalculations.getUsdAveragePriceMapsPerTickUnit(tradeStatisticsManager.getObservableTradeStatisticsList()) .whenComplete((usdAveragePriceMapsPerTickUnit, throwable) -> { if (deactivateCalled) { return; } if (throwable != null) { log.error("Error at applyAsyncUsdAveragePriceMapsPerTickUnit. {}", throwable.toString()); completeFuture.completeExceptionally(throwable); return; } UserThread.execute(() -> { this.usdAveragePriceMapsPerTickUnit.clear(); this.usdAveragePriceMapsPerTickUnit.putAll(usdAveragePriceMapsPerTickUnit); log.debug("applyAsyncUsdAveragePriceMapsPerTickUnit took {}", System.currentTimeMillis() - ts); completeFuture.complete(true); }); }); } private CompletableFuture applyAsyncTradeStatisticsForCurrency(String currencyCode) { return applyAsyncTradeStatisticsForCurrency(currencyCode, null); } private CompletableFuture applyAsyncTradeStatisticsForCurrency(String currencyCode, @Nullable CompletableFuture completeFuture) { CompletableFuture future = new CompletableFuture<>(); long ts = System.currentTimeMillis(); ChartCalculations.getTradeStatisticsForCurrency(tradeStatisticsManager.getObservableTradeStatisticsList(), currencyCode, showAllTradeCurrenciesProperty.get()) .whenComplete((list, throwable) -> { if (deactivateCalled) { return; } if (throwable != null) { log.error("Error at applyAsyncTradeStatisticsForCurrency. {}", throwable.toString()); if (completeFuture != null) { completeFuture.completeExceptionally(throwable); } return; } UserThread.execute(() -> { tradeStatisticsByCurrency.setAll(list); log.debug("applyAsyncTradeStatisticsForCurrency took {}", System.currentTimeMillis() - ts); if (completeFuture != null) { completeFuture.complete(true); } future.complete(true); }); }); return future; } private void applyAsyncChartData() { long ts = System.currentTimeMillis(); ChartCalculations.getUpdateChartResult(new ArrayList<>(tradeStatisticsByCurrency), tickUnit, usdAveragePriceMapsPerTickUnit, getCurrencyCode()) .whenComplete((updateChartResult, throwable) -> { if (deactivateCalled) { return; } if (throwable != null) { log.error("Error at applyAsyncChartData. {}", throwable); return; } UserThread.execute(() -> { itemsPerInterval.clear(); itemsPerInterval.putAll(updateChartResult.getItemsPerInterval()); priceItems.setAll(updateChartResult.getPriceItems()); volumeItems.setAll(updateChartResult.getVolumeItems()); volumeInUsdItems.setAll(updateChartResult.getVolumeInUsdItems()); log.debug("applyAsyncChartData took {}", System.currentTimeMillis() - ts); }); }); } /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// void onSetTradeCurrency(TradeCurrency tradeCurrency) { if (tradeCurrency != null) { String code = tradeCurrency.getCode(); if (isEditEntry(code)) { navigation.navigateTo(MainView.class, SettingsView.class, PreferencesView.class); return; } boolean showAllEntry = isShowAllEntry(code); showAllTradeCurrenciesProperty.set(showAllEntry); if (showAllEntry) { priceFeedService.setCurrencyCode(GlobalSettings.getDefaultTradeCurrency().getCode()); } else { selectedTradeCurrencyProperty.set(tradeCurrency); priceFeedService.setCurrencyCode(code); } preferences.setTradeChartsScreenCurrencyCode(code); applyAsyncTradeStatisticsForCurrency(getCurrencyCode()) .whenComplete((result, throwable) -> { if (deactivateCalled) { return; } if (throwable != null) { log.error("Error at onSetTradeCurrency/applyAsyncTradeStatisticsForCurrency. {}", throwable.toString()); return; } applyAsyncChartData(); }); } } void setTickUnit(TickUnit tickUnit) { this.tickUnit = tickUnit; preferences.setTradeStatisticsTickUnitIndex(tickUnit.ordinal()); applyAsyncChartData(); } void setSelectedTabIndex(int selectedTabIndex) { this.selectedTabIndex = selectedTabIndex; syncPriceFeedCurrency(); setMarketPriceFeedCurrency(); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// public String getCurrencyCode() { return selectedTradeCurrencyProperty.get().getCode(); } public ObservableList getCurrencyListItems() { return currencyListItems.getObservableList(); } public Optional getSelectedCurrencyListItem() { return currencyListItems.getObservableList().stream().filter(e -> e.tradeCurrency.equals(selectedTradeCurrencyProperty.get())).findAny(); } long getTimeFromTickIndex(long tick) { return ChartCalculations.getTimeFromTickIndex(tick, itemsPerInterval); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void fillTradeCurrencies() { // Don't use a set as we need all entries List tradeCurrencyList; synchronized (tradeStatisticsManager.getObservableTradeStatisticsList()) { tradeCurrencyList = tradeStatisticsManager.getObservableTradeStatisticsList().stream() .flatMap(e -> CurrencyUtil.getTradeCurrency(e.getCurrency()).stream()) .collect(Collectors.toList()); } currencyListItems.updateWithCurrencies(tradeCurrencyList, showAllCurrencyListItem); } private void setMarketPriceFeedCurrency() { if (selectedTabIndex == TAB_INDEX) { if (showAllTradeCurrenciesProperty.get()) priceFeedService.setCurrencyCode(GlobalSettings.getDefaultTradeCurrency().getCode()); else priceFeedService.setCurrencyCode(getCurrencyCode()); } } private void syncPriceFeedCurrency() { if (selectedTabIndex == TAB_INDEX) priceFeedService.setCurrencyCode(selectedTradeCurrencyProperty.get().getCode()); } private boolean isShowAllEntry(@Nullable String id) { return id != null && id.equals(GUIUtil.SHOW_ALL_FLAG); } private boolean isEditEntry(@Nullable String id) { return id != null && id.equals(GUIUtil.EDIT_FLAG); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/market/trades/charts/CandleData.java ================================================ /* * 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 . */ package haveno.desktop.main.market.trades.charts; public class CandleData { public final long tick; // Is the time tick in the chosen time interval public final long open; public final long close; public final long high; public final long low; public final long average; public final long median; public final long accumulatedAmount; public final long accumulatedVolume; public final long numTrades; public final boolean isBullish; public final String date; public final long volumeInUsd; public CandleData(long tick, long open, long close, long high, long low, long average, long median, long accumulatedAmount, long accumulatedVolume, long numTrades, boolean isBullish, String date, long volumeInUsd) { this.tick = tick; this.open = open; this.close = close; this.high = high; this.low = low; this.average = average; this.median = median; this.accumulatedAmount = accumulatedAmount; this.accumulatedVolume = accumulatedVolume; this.numTrades = numTrades; this.isBullish = isBullish; this.date = date; this.volumeInUsd = volumeInUsd; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/market/trades/charts/price/Candle.java ================================================ /* * 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 . */ /* * Copyright (c) 2008, 2014, Oracle and/or its affiliates. * All rights reserved. Use is subject to license terms. * * This file is available and licensed under the following license: * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the distribution. * - Neither the name of Oracle Corporation nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package haveno.desktop.main.market.trades.charts.price; import haveno.desktop.main.market.trades.charts.CandleData; import javafx.scene.Group; import javafx.scene.control.Tooltip; import javafx.scene.layout.Region; import javafx.scene.shape.Line; import javafx.util.StringConverter; /** * Candle node used for drawing a candle */ public class Candle extends Group { private String seriesStyleClass; private String dataStyleClass; private final CandleTooltip candleTooltip; private final Line highLowLine = new Line(); private final Region bar = new Region(); private boolean openAboveClose = true; private double closeOffset; Candle(String seriesStyleClass, String dataStyleClass, StringConverter priceStringConverter) { this.seriesStyleClass = seriesStyleClass; this.dataStyleClass = dataStyleClass; setAutoSizeChildren(false); getChildren().addAll(highLowLine, bar); getStyleClass().setAll("candlestick-candle", seriesStyleClass, dataStyleClass); updateStyleClasses(); candleTooltip = new CandleTooltip(priceStringConverter); Tooltip tooltip = new Tooltip(); tooltip.setGraphic(candleTooltip); Tooltip.install(this, tooltip); } public void setSeriesAndDataStyleClasses(String seriesStyleClass, String dataStyleClass) { this.seriesStyleClass = seriesStyleClass; this.dataStyleClass = dataStyleClass; getStyleClass().setAll("candlestick-candle", seriesStyleClass, dataStyleClass); updateStyleClasses(); } public void update(double closeOffset, double highOffset, double lowOffset, double candleWidth) { this.closeOffset = closeOffset; openAboveClose = closeOffset > 0; updateStyleClasses(); highLowLine.setStartY(highOffset); highLowLine.setEndY(lowOffset); if (openAboveClose) { bar.resizeRelocate(-candleWidth / 2, 0, candleWidth, Math.max(5, closeOffset)); } else { bar.resizeRelocate(-candleWidth / 2, closeOffset, candleWidth, Math.max(5, closeOffset * -1)); } } public void updateTooltip(CandleData candleData) { candleTooltip.update(candleData); } private void updateStyleClasses() { String style = openAboveClose ? "open-above-close" : "close-above-open"; if (closeOffset == 0) style = "empty"; highLowLine.getStyleClass().setAll("candlestick-line", seriesStyleClass, dataStyleClass, style); bar.getStyleClass().setAll("candlestick-bar", seriesStyleClass, dataStyleClass, style); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/market/trades/charts/price/CandleStickChart.java ================================================ /* * 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 . */ /* * Copyright (c) 2008, 2014, Oracle and/or its affiliates. * All rights reserved. Use is subject to license terms. * * This file is available and licensed under the following license: * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the distribution. * - Neither the name of Oracle Corporation nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package haveno.desktop.main.market.trades.charts.price; import haveno.desktop.main.market.trades.charts.CandleData; import javafx.animation.FadeTransition; import javafx.event.ActionEvent; import javafx.scene.Node; import javafx.scene.chart.Axis; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; import javafx.scene.shape.LineTo; import javafx.scene.shape.MoveTo; import javafx.scene.shape.Path; import javafx.util.Duration; import javafx.util.StringConverter; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * A candlestick chart is a style of bar-chart used primarily to describe price movements of a security, derivative, * or currency over time. *

    * The Data Y value is used for the opening price and then the close, high and low values are stored in the Data's * extra value property using a CandleStickExtraValues object. */ public class CandleStickChart extends XYChart { private final StringConverter priceStringConverter; // -------------- CONSTRUCTORS ---------------------------------------------- /** * Construct a new CandleStickChart with the given axis. * * @param xAxis The x axis to use * @param yAxis The y axis to use */ public CandleStickChart(Axis xAxis, Axis yAxis, StringConverter priceStringConverter) { super(xAxis, yAxis); this.priceStringConverter = priceStringConverter; } // -------------- METHODS ------------------------------------------------------------------------------------------ /** * Called to update and layout the content for the plot */ @Override protected void layoutPlotChildren() { // we have nothing to layout if no data is present if (getData() == null) { return; } // update candle positions for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) { XYChart.Series series = getData().get(seriesIndex); Iterator> iterator = getDisplayedDataIterator(series); Path seriesPath = null; if (series.getNode() instanceof Path) { seriesPath = (Path) series.getNode(); seriesPath.getElements().clear(); } while (iterator.hasNext()) { XYChart.Data item = iterator.next(); double x = getXAxis().getDisplayPosition(getCurrentDisplayedXValue(item)); double y = getYAxis().getDisplayPosition(getCurrentDisplayedYValue(item)); Node itemNode = item.getNode(); CandleData candleData = (CandleData) item.getExtraValue(); if (itemNode instanceof Candle && candleData != null) { Candle candle = (Candle) itemNode; double close = getYAxis().getDisplayPosition(candleData.close); double high = getYAxis().getDisplayPosition(candleData.high); double low = getYAxis().getDisplayPosition(candleData.low); // calculate candle width double candleWidth = -1; if (getXAxis() instanceof NumberAxis) { NumberAxis xa = (NumberAxis) getXAxis(); candleWidth = xa.getDisplayPosition(1) * 0.60; // use 60% width between units } // update candle candle.update(close - y, high - y, low - y, candleWidth); candle.updateTooltip(candleData); // position the candle candle.setLayoutX(x); candle.setLayoutY(y); } if (seriesPath != null && candleData != null) { final double displayPosition = getYAxis().getDisplayPosition(candleData.average); if (seriesPath.getElements().isEmpty()) seriesPath.getElements().add(new MoveTo(x, displayPosition)); else seriesPath.getElements().add(new LineTo(x, displayPosition)); } } } } @Override protected void dataItemChanged(XYChart.Data item) { } @Override protected void dataItemAdded(XYChart.Series series, int itemIndex, XYChart.Data item) { Node candle = createCandle(getData().indexOf(series), item, itemIndex); getPlotChildren().remove(candle); if (shouldAnimate()) { candle.setOpacity(0); getPlotChildren().add(candle); // fade in new candle FadeTransition ft = new FadeTransition(Duration.millis(500), candle); ft.setToValue(1); ft.play(); } else { getPlotChildren().add(candle); } // always draw average line on top if (series.getNode() instanceof Path) { Path seriesPath = (Path) series.getNode(); seriesPath.toFront(); } } @Override protected void dataItemRemoved(XYChart.Data item, XYChart.Series series) { if (series.getNode() instanceof Path) { Path seriesPath = (Path) series.getNode(); seriesPath.getElements().clear(); } final Node node = item.getNode(); if (shouldAnimate()) { // fade out old candle FadeTransition ft = new FadeTransition(Duration.millis(500), node); ft.setToValue(0); ft.setOnFinished((ActionEvent actionEvent) -> { getPlotChildren().remove(node); removeDataItemFromDisplay(series, item); }); ft.play(); } else { getPlotChildren().remove(node); removeDataItemFromDisplay(series, item); } } @Override protected void seriesAdded(XYChart.Series series, int seriesIndex) { // handle any data already in series for (int j = 0; j < series.getData().size(); j++) { XYChart.Data item = series.getData().get(j); Node candle = createCandle(seriesIndex, item, j); if (!getPlotChildren().contains(candle)) { getPlotChildren().add(candle); if (shouldAnimate()) { candle.setOpacity(0); FadeTransition ft = new FadeTransition(Duration.millis(500), candle); ft.setToValue(1); ft.play(); } } } Path seriesPath = new Path(); seriesPath.getStyleClass().setAll("candlestick-average-line", "series" + seriesIndex); series.setNode(seriesPath); if (!getPlotChildren().contains(seriesPath)) { getPlotChildren().add(seriesPath); if (shouldAnimate()) { seriesPath.setOpacity(0); FadeTransition ft = new FadeTransition(Duration.millis(500), seriesPath); ft.setToValue(1); ft.play(); } } } @Override protected void seriesRemoved(XYChart.Series series) { // remove all candle nodes for (XYChart.Data d : series.getData()) { final Node candle = d.getNode(); if (shouldAnimate()) { FadeTransition ft = new FadeTransition(Duration.millis(500), candle); ft.setToValue(0); ft.setOnFinished((ActionEvent actionEvent) -> getPlotChildren().remove(candle)); ft.play(); } else { getPlotChildren().remove(candle); } } if (series.getNode() instanceof Path) { Path seriesPath = (Path) series.getNode(); if (shouldAnimate()) { FadeTransition ft = new FadeTransition(Duration.millis(500), seriesPath); ft.setToValue(0); ft.setOnFinished((ActionEvent actionEvent) -> { getPlotChildren().remove(seriesPath); seriesPath.getElements().clear(); removeSeriesFromDisplay(series); }); ft.play(); } else { getPlotChildren().remove(seriesPath); seriesPath.getElements().clear(); removeSeriesFromDisplay(series); } } else { removeSeriesFromDisplay(series); } } /** * Create a new Candle node to represent a single data item * * @param seriesIndex The index of the series the data item is in * @param item The data item to create node for * @param itemIndex The index of the data item in the series * @return New candle node to represent the give data item */ private Node createCandle(int seriesIndex, final XYChart.Data item, int itemIndex) { Node candle = item.getNode(); // check if candle has already been created if (candle instanceof Candle) { ((Candle) candle).setSeriesAndDataStyleClasses("series" + seriesIndex, "data" + itemIndex); } else { candle = new Candle("series" + seriesIndex, "data" + itemIndex, priceStringConverter); item.setNode(candle); } return candle; } /** * This is called when the range has been invalidated and we need to update it. If the axis are auto * ranging then we compile a list of all data that the given axis has to plot and call invalidateRange() on the * axis passing it that data. */ @Override protected void updateAxisRange() { // For candle stick chart we need to override this method as we need to let the axis know that they need to be able // to cover the whole area occupied by the high to low range not just its center data value final Axis xa = getXAxis(); final Axis ya = getYAxis(); List xData = null; List yData = null; if (xa.isAutoRanging()) { xData = new ArrayList<>(); } if (ya.isAutoRanging()) { yData = new ArrayList<>(); } if (xData != null || yData != null) { for (XYChart.Series series : getData()) { for (XYChart.Data data : series.getData()) { if (xData != null) { xData.add(data.getXValue()); } if (yData != null) { if (data.getExtraValue() instanceof CandleData) { CandleData candleData = (CandleData) data.getExtraValue(); yData.add(candleData.high); yData.add(candleData.low); } else { yData.add(data.getYValue()); } } } } if (xData != null) { xa.invalidateRange(xData); } if (yData != null) { ya.invalidateRange(yData); } } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/market/trades/charts/price/CandleTooltip.java ================================================ /* * 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 . */ /* * Copyright (c) 2008, 2014, Oracle and/or its affiliates. * All rights reserved. Use is subject to license terms. * * This file is available and licensed under the following license: * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the distribution. * - Neither the name of Oracle Corporation nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package haveno.desktop.main.market.trades.charts.price; import haveno.core.locale.Res; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.main.market.trades.charts.CandleData; import haveno.desktop.util.Layout; import javafx.geometry.HPos; import javafx.scene.control.Label; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; import javafx.util.StringConverter; /** * The content for Candle tool tips */ public class CandleTooltip extends GridPane { private final StringConverter priceStringConverter; private final Label openValue = new AutoTooltipLabel(); private final Label closeValue = new AutoTooltipLabel(); private final Label highValue = new AutoTooltipLabel(); private final Label lowValue = new AutoTooltipLabel(); private final Label averageValue = new AutoTooltipLabel(); private final Label medianValue = new AutoTooltipLabel(); private final Label dateValue = new AutoTooltipLabel(); CandleTooltip(StringConverter priceStringConverter) { this.priceStringConverter = priceStringConverter; setHgap(Layout.GRID_GAP); setVgap(2); Label open = new AutoTooltipLabel(Res.get("market.trades.tooltip.candle.open")); Label close = new AutoTooltipLabel(Res.get("market.trades.tooltip.candle.close")); Label high = new AutoTooltipLabel(Res.get("market.trades.tooltip.candle.high")); Label low = new AutoTooltipLabel(Res.get("market.trades.tooltip.candle.low")); Label average = new AutoTooltipLabel(Res.get("market.trades.tooltip.candle.average")); Label median = new AutoTooltipLabel(Res.get("market.trades.tooltip.candle.median")); Label date = new AutoTooltipLabel(Res.get("market.trades.tooltip.candle.date")); setConstraints(open, 0, 0); setConstraints(openValue, 1, 0); setConstraints(close, 0, 1); setConstraints(closeValue, 1, 1); setConstraints(high, 0, 2); setConstraints(highValue, 1, 2); setConstraints(low, 0, 3); setConstraints(lowValue, 1, 3); setConstraints(average, 0, 4); setConstraints(averageValue, 1, 4); setConstraints(median, 0, 5); setConstraints(medianValue, 1, 5); setConstraints(date, 0, 6); setConstraints(dateValue, 1, 6); ColumnConstraints columnConstraints1 = new ColumnConstraints(); columnConstraints1.setHalignment(HPos.RIGHT); columnConstraints1.setHgrow(Priority.NEVER); ColumnConstraints columnConstraints2 = new ColumnConstraints(); columnConstraints2.setHgrow(Priority.ALWAYS); getColumnConstraints().addAll(columnConstraints1, columnConstraints2); getChildren().addAll(open, openValue, close, closeValue, high, highValue, low, lowValue, average, averageValue, median, medianValue, date, dateValue); } public void update(CandleData candleData) { openValue.setText(priceStringConverter.toString(candleData.open)); closeValue.setText(priceStringConverter.toString(candleData.close)); highValue.setText(priceStringConverter.toString(candleData.high)); lowValue.setText(priceStringConverter.toString(candleData.low)); averageValue.setText(priceStringConverter.toString(candleData.average)); medianValue.setText(priceStringConverter.toString(candleData.median)); dateValue.setText(candleData.date); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/market/trades/charts/volume/VolumeBar.java ================================================ /* * 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 . */ package haveno.desktop.main.market.trades.charts.volume; import haveno.core.locale.Res; import haveno.core.util.VolumeUtil; import haveno.desktop.main.market.trades.charts.CandleData; import javafx.scene.Group; import javafx.scene.control.Tooltip; import javafx.scene.layout.Region; import javafx.util.StringConverter; public class VolumeBar extends Group { private String seriesStyleClass; private String dataStyleClass; private final StringConverter volumeStringConverter; private final Region bar = new Region(); private final Tooltip tooltip; VolumeBar(String seriesStyleClass, String dataStyleClass, StringConverter volumeStringConverter) { this.seriesStyleClass = seriesStyleClass; this.dataStyleClass = dataStyleClass; this.volumeStringConverter = volumeStringConverter; setAutoSizeChildren(false); getChildren().add(bar); updateStyleClasses(); tooltip = new Tooltip(); Tooltip.install(this, tooltip); } public void setSeriesAndDataStyleClasses(String seriesStyleClass, String dataStyleClass) { this.seriesStyleClass = seriesStyleClass; this.dataStyleClass = dataStyleClass; updateStyleClasses(); } public void update(double height, double candleWidth, CandleData candleData) { bar.resizeRelocate(-candleWidth / 2, 0, candleWidth, height); String volumeInXmr = volumeStringConverter.toString(candleData.accumulatedAmount); String volumeInUsd = VolumeUtil.formatLargeFiat(candleData.volumeInUsd, "USD"); tooltip.setText(Res.get("market.trades.tooltip.volumeBar", volumeInXmr, volumeInUsd, candleData.numTrades, candleData.date)); } private void updateStyleClasses() { bar.getStyleClass().setAll("volume-bar", seriesStyleClass, dataStyleClass, "bg"); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/market/trades/charts/volume/VolumeChart.java ================================================ /* * 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 . */ package haveno.desktop.main.market.trades.charts.volume; import haveno.desktop.main.market.trades.charts.CandleData; import javafx.animation.FadeTransition; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.event.ActionEvent; import javafx.scene.Node; import javafx.scene.chart.Axis; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; import javafx.util.Duration; import javafx.util.StringConverter; import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class VolumeChart extends XYChart { private final StringConverter toolTipStringConverter; public VolumeChart(Axis xAxis, Axis yAxis, StringConverter toolTipStringConverter) { super(xAxis, yAxis); this.toolTipStringConverter = toolTipStringConverter; } @Override protected void layoutPlotChildren() { if (getData() == null) { return; } for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) { XYChart.Series series = getData().get(seriesIndex); Iterator> iterator = getDisplayedDataIterator(series); while (iterator.hasNext()) { XYChart.Data item = iterator.next(); double x = getXAxis().getDisplayPosition(getCurrentDisplayedXValue(item)); double y = getYAxis().getDisplayPosition(getCurrentDisplayedYValue(item)); Node itemNode = item.getNode(); CandleData candleData = (CandleData) item.getExtraValue(); if (itemNode instanceof VolumeBar && candleData != null) { VolumeBar volumeBar = (VolumeBar) itemNode; double candleWidth = -1; if (getXAxis() instanceof NumberAxis) { NumberAxis xa = (NumberAxis) getXAxis(); candleWidth = xa.getDisplayPosition(1) * 0.60; // use 60% width between units } // 97 is visible chart data height if chart height is 140. // So we subtract 43 form the height to get the height for the bar to the bottom. // Did not find a way how to request the chart data height final double height = getHeight() - 43; double upperYPos = Math.min(height - 5, y); // We want min 5px height to allow tooltips volumeBar.update(height - upperYPos, candleWidth, candleData); volumeBar.setLayoutX(x); volumeBar.setLayoutY(upperYPos); } } } } @Override protected void dataItemChanged(XYChart.Data item) { } @Override protected void dataItemAdded(XYChart.Series series, int itemIndex, XYChart.Data item) { Node volumeBar = createCandle(getData().indexOf(series), item, itemIndex); getPlotChildren().remove(volumeBar); if (shouldAnimate()) { volumeBar.setOpacity(0); getPlotChildren().add(volumeBar); FadeTransition ft = new FadeTransition(Duration.millis(500), volumeBar); ft.setToValue(1); ft.play(); } else { getPlotChildren().add(volumeBar); } } @Override protected void dataItemRemoved(XYChart.Data item, XYChart.Series series) { final Node node = item.getNode(); if (shouldAnimate()) { FadeTransition ft = new FadeTransition(Duration.millis(500), node); ft.setToValue(0); ft.setOnFinished((ActionEvent actionEvent) -> { getPlotChildren().remove(node); removeDataItemFromDisplay(series, item); }); ft.play(); } else { getPlotChildren().remove(node); removeDataItemFromDisplay(series, item); } } @Override protected void seriesAdded(XYChart.Series series, int seriesIndex) { for (int j = 0; j < series.getData().size(); j++) { XYChart.Data item = series.getData().get(j); Node volumeBar = createCandle(seriesIndex, item, j); if (shouldAnimate()) { volumeBar.setOpacity(0); getPlotChildren().add(volumeBar); FadeTransition ft = new FadeTransition(Duration.millis(500), volumeBar); ft.setToValue(1); ft.play(); } else { getPlotChildren().add(volumeBar); } } } @Override protected void seriesRemoved(XYChart.Series series) { for (XYChart.Data d : series.getData()) { final Node volumeBar = d.getNode(); if (shouldAnimate()) { FadeTransition ft = new FadeTransition(Duration.millis(500), volumeBar); ft.setToValue(0); ft.setOnFinished((ActionEvent actionEvent) -> getPlotChildren().remove(volumeBar)); ft.play(); } else { getPlotChildren().remove(volumeBar); } } if (shouldAnimate()) { new Timeline(new KeyFrame(Duration.millis(500), event -> removeSeriesFromDisplay(series))).play(); } else { removeSeriesFromDisplay(series); } } private Node createCandle(int seriesIndex, final XYChart.Data item, int itemIndex) { Node volumeBar = item.getNode(); if (volumeBar instanceof VolumeBar) { ((VolumeBar) volumeBar).setSeriesAndDataStyleClasses("series" + seriesIndex, "data" + itemIndex); } else { volumeBar = new VolumeBar("series" + seriesIndex, "data" + itemIndex, toolTipStringConverter); item.setNode(volumeBar); } return volumeBar; } @Override protected void updateAxisRange() { final Axis xa = getXAxis(); final Axis ya = getYAxis(); List xData = null; List yData = null; if (xa.isAutoRanging()) { xData = new ArrayList<>(); } if (ya.isAutoRanging()) yData = new ArrayList<>(); if (xData != null || yData != null) { for (XYChart.Series series : getData()) { for (XYChart.Data data : series.getData()) { if (xData != null) { xData.add(data.getXValue()); } if (yData != null) yData.add(data.getYValue()); } } if (xData != null) { xa.invalidateRange(xData); } if (yData != null) { ya.invalidateRange(yData); } } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/BuyOfferView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/BuyOfferView.java ================================================ /* * 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 . */ package haveno.desktop.main.offer; import com.google.inject.Inject; import haveno.core.locale.Res; import haveno.core.offer.OfferDirection; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.desktop.Navigation; import haveno.desktop.common.view.FxmlView; import haveno.desktop.common.view.ViewLoader; import haveno.network.p2p.P2PService; @FxmlView public class BuyOfferView extends OfferView { @Inject public BuyOfferView(ViewLoader viewLoader, Navigation navigation, Preferences preferences, User user, P2PService p2PService) { super(viewLoader, navigation, preferences, user, p2PService, OfferDirection.BUY); } @Override protected String getOfferLabel() { return Res.get("offerbook.buyXmrWith"); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/ClosableView.java ================================================ /* * 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 . */ package haveno.desktop.main.offer; public interface ClosableView { public void setCloseHandler(OfferView.CloseHandler closeHandler); } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/InitializableViewWithTakeOfferData.java ================================================ /* * 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 . */ package haveno.desktop.main.offer; import haveno.core.offer.Offer; public interface InitializableViewWithTakeOfferData { public void initWithData(Offer offer); } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java ================================================ /* * 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 . */ package haveno.desktop.main.offer; import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.UserThread; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.util.MathUtils; import haveno.common.util.Utilities; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TradeCurrency; import haveno.core.monetary.Price; import haveno.core.monetary.Volume; import haveno.core.offer.CreateOfferService; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OfferUtil; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.PaymentAccount; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.HavenoUtils; import haveno.core.trade.handlers.TransactionResultHandler; import haveno.core.trade.statistics.TradeStatistics3; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.util.FormattingUtils; import haveno.core.util.VolumeUtil; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.coin.CoinUtil; import haveno.core.xmr.listeners.XmrBalanceListener; import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.wallet.Restrictions; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.Navigation; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.P2PService; import java.math.BigInteger; import java.util.Comparator; import static java.util.Comparator.comparing; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyStringProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import lombok.Getter; import org.jetbrains.annotations.NotNull; public abstract class MutableOfferDataModel extends OfferDataModel { protected final CreateOfferService createOfferService; protected final OpenOfferManager openOfferManager; private final XmrWalletService xmrWalletService; private final Preferences preferences; protected final User user; private final P2PService p2PService; protected final PriceFeedService priceFeedService; final String shortOfferId; private final AccountAgeWitnessService accountAgeWitnessService; private final CoinFormatter btcFormatter; private final Navigation navigation; private final String offerId; private final ListChangeListener paymentAccountsChangeListener; protected OfferDirection direction; protected TradeCurrency tradeCurrency; protected final StringProperty tradeCurrencyCode = new SimpleStringProperty(); protected final BooleanProperty useMarketBasedPrice = new SimpleBooleanProperty(); protected final ObjectProperty amount = new SimpleObjectProperty<>(); protected final ObjectProperty minAmount = new SimpleObjectProperty<>(); protected final ObjectProperty price = new SimpleObjectProperty<>(); protected final ObjectProperty volume = new SimpleObjectProperty<>(); protected final ObjectProperty minVolume = new SimpleObjectProperty<>(); protected final ObjectProperty extraInfo = new SimpleObjectProperty<>(); // Percentage value of buyer security deposit. E.g. 0.01 means 1% of trade amount protected final DoubleProperty securityDepositPct = new SimpleDoubleProperty(); protected final BooleanProperty buyerAsTakerWithoutDeposit = new SimpleBooleanProperty(); protected final ObservableList paymentAccounts = FXCollections.observableArrayList(); protected PaymentAccount paymentAccount; boolean isTabSelected; protected double marketPriceMarginPct = 0; @Getter private boolean marketPriceAvailable; protected boolean allowAmountUpdate = true; private final TradeStatisticsManager tradeStatisticsManager; private final Predicate> isNonZeroAmount = (c) -> c.get() != null && c.get().compareTo(BigInteger.ZERO) != 0; private final Predicate> isNonZeroPrice = (p) -> p.get() != null && !p.get().isZero(); private final Predicate> isNonZeroVolume = (v) -> v.get() != null && !v.get().isZero(); @Getter protected long triggerPrice; @Getter protected boolean reserveExactAmount; private XmrBalanceListener xmrBalanceListener; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject public MutableOfferDataModel(CreateOfferService createOfferService, OpenOfferManager openOfferManager, OfferUtil offerUtil, XmrWalletService xmrWalletService, Preferences preferences, User user, P2PService p2PService, PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, TradeStatisticsManager tradeStatisticsManager, Navigation navigation) { super(xmrWalletService, openOfferManager, offerUtil); this.xmrWalletService = xmrWalletService; this.createOfferService = createOfferService; this.openOfferManager = openOfferManager; this.preferences = preferences; this.user = user; this.p2PService = p2PService; this.priceFeedService = priceFeedService; this.accountAgeWitnessService = accountAgeWitnessService; this.btcFormatter = btcFormatter; this.navigation = navigation; this.tradeStatisticsManager = tradeStatisticsManager; offerId = OfferUtil.getRandomOfferId(); shortOfferId = Utilities.getShortId(offerId); reserveExactAmount = preferences.getSplitOfferOutput(); useMarketBasedPrice.set(preferences.isUsePercentageBasedPrice()); securityDepositPct.set(Restrictions.getMinSecurityDepositPct()); paymentAccountsChangeListener = change -> fillPaymentAccounts(); } @Override public void activate() { addListeners(); if (isTabSelected) priceFeedService.setCurrencyCode(tradeCurrencyCode.get()); updateBalances(); } @Override protected void deactivate() { removeListeners(); } private void addListeners() { if (xmrBalanceListener != null) xmrWalletService.addBalanceListener(xmrBalanceListener); user.getPaymentAccountsAsObservable().addListener(paymentAccountsChangeListener); } private void removeListeners() { if (xmrBalanceListener != null) xmrWalletService.removeBalanceListener(xmrBalanceListener); user.getPaymentAccountsAsObservable().removeListener(paymentAccountsChangeListener); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// // called before activate() public boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency, boolean initAddressEntry) { if (initAddressEntry) { addressEntry = xmrWalletService.getOrCreateAddressEntry(offerId, XmrAddressEntry.Context.OFFER_FUNDING); xmrBalanceListener = new XmrBalanceListener(getAddressEntry().getSubaddressIndex()) { @Override public void onBalanceChanged(BigInteger balance) { updateBalances(); } }; } this.direction = direction; this.tradeCurrency = tradeCurrency; fillPaymentAccounts(); PaymentAccount account; PaymentAccount lastSelectedPaymentAccount = getPreselectedPaymentAccount(); if (lastSelectedPaymentAccount != null && lastSelectedPaymentAccount.getTradeCurrencies().contains(tradeCurrency) && user.getPaymentAccounts() != null && user.getPaymentAccounts().stream().anyMatch(paymentAccount -> paymentAccount.getId().equals(lastSelectedPaymentAccount.getId()))) { account = lastSelectedPaymentAccount; } else { account = user.findFirstPaymentAccountWithCurrency(tradeCurrency); } if (account != null) { this.paymentAccount = account; } else { Optional paymentAccountOptional = getAnyPaymentAccount(); if (paymentAccountOptional.isPresent()) { this.paymentAccount = paymentAccountOptional.get(); } else { log.warn("PaymentAccount not available. Should never get called as in offer view you should not be able to open a create offer view"); return false; } } setTradeCurrencyFromPaymentAccount(paymentAccount); tradeCurrencyCode.set(this.tradeCurrency.getCode()); priceFeedService.setCurrencyCode(tradeCurrencyCode.get()); calculateVolume(); calculateTotalToPay(); updateBalances(); setSuggestedSecurityDeposit(getPaymentAccount()); return true; } @NotNull private Optional getAnyPaymentAccount() { if (CurrencyUtil.isFiatCurrency(tradeCurrency.getCode())) { return paymentAccounts.stream().filter(paymentAccount1 -> paymentAccount1.isFiat()).findAny(); } else if (CurrencyUtil.isCryptoCurrency(tradeCurrency.getCode())) { return paymentAccounts.stream().filter(paymentAccount1 -> paymentAccount1.isCryptoCurrency()).findAny(); } else { return paymentAccounts.stream().filter(paymentAccount1 -> paymentAccount1.getTradeCurrency().isPresent()).findAny(); } } protected PaymentAccount getPreselectedPaymentAccount() { return preferences.getSelectedPaymentAccountForCreateOffer(); } void onTabSelected(boolean isSelected) { this.isTabSelected = isSelected; if (isTabSelected) priceFeedService.setCurrencyCode(tradeCurrencyCode.get()); } protected void updateBalances() { if (addressEntry == null) return; super.updateBalances(); // update remaining balance UserThread.await(() -> { missingCoin.set(offerUtil.getBalanceShortage(totalToPay.get(), unallocatedBalance.get())); isXmrWalletFunded.set(offerUtil.isBalanceSufficient(totalToPay.get(), unallocatedBalance.get())); if (totalToPay.get() != null && isXmrWalletFunded.get() && !showWalletFundedNotification.get()) { showWalletFundedNotification.set(true); } }); } /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// protected Offer createAndGetOffer() { return createOfferService.createAndGetOffer(offerId, direction, tradeCurrencyCode.get(), amount.get(), minAmount.get(), useMarketBasedPrice.get() ? null : price.get(), useMarketBasedPrice.get(), useMarketBasedPrice.get() ? marketPriceMarginPct : 0, securityDepositPct.get(), paymentAccount, buyerAsTakerWithoutDeposit.get(), // private offer if buyer as taker without deposit buyerAsTakerWithoutDeposit.get(), extraInfo.get()); } void onPlaceOffer(Offer offer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { openOfferManager.placeOffer(offer, useSavingsWallet, triggerPrice, reserveExactAmount, false, // desktop ui resets address entries on cancel null, resultHandler, errorMessageHandler); } void onPaymentAccountSelected(PaymentAccount paymentAccount) { if (paymentAccount != null && !this.paymentAccount.equals(paymentAccount)) { preferences.setSelectedPaymentAccountForCreateOffer(paymentAccount); this.paymentAccount = paymentAccount; setTradeCurrencyFromPaymentAccount(paymentAccount); setSuggestedSecurityDeposit(getPaymentAccount()); if (amount.get() != null && this.allowAmountUpdate) this.amount.set(amount.get().min(getMaxTradeLimit())); } } private void setSuggestedSecurityDeposit(PaymentAccount paymentAccount) { var minSecurityDeposit = Restrictions.getMinSecurityDepositPct(); try { if (getTradeCurrency() == null) { setSecurityDepositPct(minSecurityDeposit); return; } // Get average historic prices over for the prior trade period equaling the lock time var blocksRange = Restrictions.getLockTime(paymentAccount.getPaymentMethod().isBlockchain()); var startDate = new Date(System.currentTimeMillis() - blocksRange * 10L * 60000); List sortedRangeData; synchronized (tradeStatisticsManager.getObservableTradeStatisticsList()) { sortedRangeData = tradeStatisticsManager.getObservableTradeStatisticsList().stream() .filter(e -> e.getCurrency().equals(getTradeCurrency().getCode())) .filter(e -> e.getDate().compareTo(startDate) >= 0) .sorted(Comparator.comparing(TradeStatistics3::getDate)) .collect(Collectors.toList()); } var movingAverage = new MathUtils.MovingAverage(10, 0.2); double[] extremes = {Double.MAX_VALUE, Double.MIN_VALUE}; sortedRangeData.forEach(e -> { var price = e.getTradePrice().getValue(); movingAverage.next(price).ifPresent(val -> { if (val < extremes[0]) extremes[0] = val; if (val > extremes[1]) extremes[1] = val; }); }); var min = extremes[0]; var max = extremes[1]; if (min == 0d || max == 0d) { setSecurityDepositPct(minSecurityDeposit); return; } // Suggested deposit is double the trade range over the previous lock time period, bounded by min/max deposit var suggestedSecurityDeposit = Math.min(2 * (max - min) / max, Restrictions.getMaxSecurityDepositPct()); securityDepositPct.set(Math.max(suggestedSecurityDeposit, minSecurityDeposit)); } catch (Throwable t) { log.error(t.toString()); securityDepositPct.set(minSecurityDeposit); } } private void setTradeCurrencyFromPaymentAccount(PaymentAccount paymentAccount) { if (!paymentAccount.getTradeCurrencies().contains(tradeCurrency)) tradeCurrency = paymentAccount.getTradeCurrency().orElse(tradeCurrency); checkNotNull(tradeCurrency, "tradeCurrency must not be null"); tradeCurrencyCode.set(tradeCurrency.getCode()); } void onCurrencySelected(TradeCurrency tradeCurrency) { if (tradeCurrency != null) { if (!this.tradeCurrency.equals(tradeCurrency)) { volume.set(null); minVolume.set(null); price.set(null); marketPriceMarginPct = 0; } this.tradeCurrency = tradeCurrency; final String code = this.tradeCurrency.getCode(); tradeCurrencyCode.set(code); if (paymentAccount != null) paymentAccount.setSelectedTradeCurrency(tradeCurrency); priceFeedService.setCurrencyCode(code); Optional tradeCurrencyOptional = preferences.getTradeCurrenciesAsObservable() .stream().filter(e -> e.getCode().equals(code)).findAny(); if (tradeCurrencyOptional.isEmpty()) { if (CurrencyUtil.isCryptoCurrency(code)) { CurrencyUtil.getCryptoCurrency(code).ifPresent(preferences::addCryptoCurrency); } else { CurrencyUtil.getTraditionalCurrency(code).ifPresent(preferences::addTraditionalCurrency); } } } } void fundFromSavingsWallet() { this.useSavingsWallet = true; updateBalances(); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// boolean isMinAmountLessOrEqualAmount() { //noinspection SimplifiableIfStatement if (minAmount.get() != null && amount.get() != null) return minAmount.get().compareTo(amount.get()) <= 0; return true; } public OfferDirection getDirection() { return direction; } boolean isSellOffer() { return direction == OfferDirection.SELL; } boolean isBuyOffer() { return direction == OfferDirection.BUY; } XmrAddressEntry getAddressEntry() { return addressEntry; } protected TradeCurrency getTradeCurrency() { return tradeCurrency; } protected PaymentAccount getPaymentAccount() { return paymentAccount; } protected void setUseMarketBasedPrice(boolean useMarketBasedPrice) { this.useMarketBasedPrice.set(useMarketBasedPrice); preferences.setUsePercentageBasedPrice(useMarketBasedPrice); } protected void setBuyerAsTakerWithoutDeposit(boolean buyerAsTakerWithoutDeposit) { this.buyerAsTakerWithoutDeposit.set(buyerAsTakerWithoutDeposit); } public ObservableList getPaymentAccounts() { return paymentAccounts; } public double getMarketPriceMarginPct() { return marketPriceMarginPct; } BigInteger getMinTradeLimit() { return Restrictions.getMinTradeAmount(); } BigInteger getMaxTradeLimit() { return offerUtil.getMaxTradeLimitForRelease(paymentAccount, tradeCurrencyCode.get(), direction, buyerAsTakerWithoutDeposit.get()); } /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// double calculateMarketPriceManual(double marketPrice, double volumeAsDouble, double amountAsDouble) { double manualPriceAsDouble = offerUtil.calculateManualPrice(volumeAsDouble, amountAsDouble); double percentage = offerUtil.calculateMarketPriceMarginPct(manualPriceAsDouble, marketPrice); setMarketPriceMarginPct(percentage); return manualPriceAsDouble; } void calculateVolume() { if (isNonZeroPrice.test(price) && isNonZeroAmount.test(amount)) { try { Volume volumeByAmount = calculateVolumeForAmount(amount); volume.set(volumeByAmount); calculateMinVolume(); } catch (Throwable t) { log.error(t.toString()); } } updateBalances(); } void calculateMinVolume() { if (isNonZeroPrice.test(price) && isNonZeroAmount.test(minAmount)) { try { Volume volumeByAmount = calculateVolumeForAmount(minAmount); minVolume.set(volumeByAmount); } catch (Throwable t) { log.error(t.toString()); } } } private Volume calculateVolumeForAmount(ObjectProperty minAmount) { Volume volumeByAmount = price.get().getVolumeByAmount(minAmount.get()); volumeByAmount = VolumeUtil.getAdjustedVolume(volumeByAmount, paymentAccount.getPaymentMethod().getId()); return volumeByAmount; } void calculateAmount() { if (isNonZeroPrice.test(price) && isNonZeroVolume.test(volume) && allowAmountUpdate) { try { Volume volumeBefore = volume.get(); calculateVolume(); // if the volume != amount * price, we need to adjust the amount if (amount.get() == null || !volumeBefore.equals(price.get().getVolumeByAmount(amount.get()))) { BigInteger value = price.get().getAmountByVolume(volumeBefore); BigInteger minAmount = getMinTradeLimit(); BigInteger maxAmount = getMaxTradeLimit(); value = value.max(minAmount); // adjust if below minimum value = value.min(maxAmount); // adjust if above maximum value = CoinUtil.getRoundedAmount(value, price.get(), minAmount, maxAmount, tradeCurrencyCode.get(), paymentAccount.getPaymentMethod().getId()); amount.set(value); } calculateTotalToPay(); } catch (Throwable t) { log.error(t.toString()); } } } void calculateTotalToPay() { // Maker does not pay the mining fee for the trade txs because the mining fee might be different when maker // created the offer and reserved his funds, so that would not work well with dynamic fees. // The mining fee for the createOfferFee tx is deducted from the createOfferFee and not visible to the trader final BigInteger makerFee = getMaxMakerFee(); if (direction != null && amount.get() != null && makerFee != null) { BigInteger feeAndSecDeposit = getSecurityDeposit().add(makerFee); BigInteger total = isBuyOffer() ? feeAndSecDeposit : feeAndSecDeposit.add(amount.get()); totalToPay.set(total); updateBalances(); } } void swapTradeToSavings() { xmrWalletService.resetAddressEntriesForOpenOffer(offerId); } private void fillPaymentAccounts() { if (user.getPaymentAccounts() != null) paymentAccounts.setAll(new HashSet<>(user.getPaymentAccounts())); paymentAccounts.sort(comparing(PaymentAccount::getAccountName)); } protected abstract Set getUserPaymentAccounts(); protected void setAmount(BigInteger amount) { this.amount.set(amount); } protected void setMinAmount(BigInteger minAmount) { this.minAmount.set(minAmount); } protected void setPrice(Price price) { this.price.set(price); } protected void setVolume(Volume volume) { this.volume.set(volume); } protected void setSecurityDepositPct(double value) { this.securityDepositPct.set(value); } public void setMarketPriceAvailable(boolean marketPriceAvailable) { this.marketPriceAvailable = marketPriceAvailable; } public void setTriggerPrice(long triggerPrice) { this.triggerPrice = triggerPrice; } public void setMarketPriceMarginPct(double marketPriceMarginPct) { this.marketPriceMarginPct = marketPriceMarginPct; } public void setReserveExactAmount(boolean reserveExactAmount) { this.reserveExactAmount = reserveExactAmount; } protected void setExtraInfo(String extraInfo) { this.extraInfo.set(extraInfo); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// public BigInteger getMaxUnsignedBuyLimit() { return BigInteger.valueOf(accountAgeWitnessService.getUnsignedTradeLimit(paymentAccount.getPaymentMethod(), tradeCurrencyCode.get(), OfferDirection.BUY)); } protected ReadOnlyObjectProperty getAmount() { return amount; } protected ReadOnlyObjectProperty getMinAmount() { return minAmount; } public ReadOnlyObjectProperty getPrice() { return price; } ReadOnlyObjectProperty getVolume() { return volume; } ReadOnlyObjectProperty getMinVolume() { return minVolume; } public ReadOnlyBooleanProperty getBuyerAsTakerWithoutDeposit() { return buyerAsTakerWithoutDeposit; } public ReadOnlyStringProperty getTradeCurrencyCode() { return tradeCurrencyCode; } public String getCurrencyCode() { return tradeCurrencyCode.get(); } boolean isCryptoCurrency() { return CurrencyUtil.isCryptoCurrency(tradeCurrencyCode.get()); } boolean isTraditionalCurrency() { return CurrencyUtil.isTraditionalCurrency(tradeCurrencyCode.get()); } ReadOnlyBooleanProperty getUseMarketBasedPrice() { return useMarketBasedPrice; } ReadOnlyDoubleProperty getSecurityDepositPct() { return securityDepositPct; } protected BigInteger getSecurityDeposit() { BigInteger amount = this.amount.get(); if (amount == null) amount = BigInteger.ZERO; BigInteger percentOfAmount = CoinUtil.getPercentOfAmount(securityDepositPct.get(), amount); return getBoundedSecurityDeposit(percentOfAmount); } protected BigInteger getBoundedSecurityDeposit(BigInteger value) { return Restrictions.getMinSecurityDeposit().max(value); } protected double getSecurityAsPercent(Offer offer) { BigInteger offerSellerSecurityDeposit = getBoundedSecurityDeposit(offer.getMaxSellerSecurityDeposit()); double offerSellerSecurityDepositAsPercent = CoinUtil.getAsPercentPerXmr(offerSellerSecurityDeposit, offer.getAmount()); return Math.min(offerSellerSecurityDepositAsPercent, Restrictions.getMaxSecurityDepositPct()); } ReadOnlyObjectProperty totalToPayAsProperty() { return totalToPay; } public BigInteger getMaxMakerFee() { return HavenoUtils.multiply(amount.get(), HavenoUtils.getMakerFeePct(tradeCurrencyCode.get(), buyerAsTakerWithoutDeposit.get())); } boolean canPlaceOffer() { return GUIUtil.isBootstrappedOrShowPopup(p2PService) && GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation); } public boolean isMinSecurityDeposit() { return getSecurityDeposit().compareTo(Restrictions.getMinSecurityDeposit()) <= 0; } public ReadOnlyObjectProperty getExtraInfo() { return extraInfo; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java ================================================ /* * 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 . */ package haveno.desktop.main.offer; import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.util.Tuple2; import haveno.common.util.Tuple3; import haveno.common.util.Utilities; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.payment.FasterPaymentsAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentMethod; import haveno.core.trade.HavenoUtils; import haveno.core.user.DontShowAgainLookup; import haveno.core.user.Preferences; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.Navigation; import haveno.desktop.common.view.ActivatableViewAndModel; import haveno.desktop.components.AddressTextField; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.BalanceTextField; import haveno.desktop.components.BusyAnimation; import haveno.desktop.components.FundsTextField; import haveno.desktop.components.InfoInputTextField; import haveno.desktop.components.InputTextArea; import haveno.desktop.components.InputTextField; import haveno.desktop.components.TitledGroupBg; import haveno.desktop.main.MainView; import haveno.desktop.main.account.AccountView; import haveno.desktop.main.account.content.traditionalaccounts.TraditionalAccountsView; import haveno.desktop.main.overlays.notifications.Notification; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.OfferDetailsWindow; import haveno.desktop.main.overlays.windows.QRCodeWindow; import haveno.desktop.main.portfolio.PortfolioView; import haveno.desktop.main.portfolio.openoffer.OpenOffersView; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.Layout; import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Orientation; import javafx.geometry.Pos; import javafx.geometry.VPos; import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.control.Separator; import javafx.scene.control.TextField; import javafx.scene.control.ToggleButton; import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.util.StringConverter; import lombok.Setter; import net.glxn.qrgen.QRCode; import net.glxn.qrgen.image.ImageType; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; import org.jetbrains.annotations.NotNull; import java.io.ByteArrayInputStream; import java.math.BigInteger; import java.net.URI; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.concurrent.TimeUnit; import static haveno.desktop.main.offer.OfferViewUtil.addPayInfoEntry; import static haveno.desktop.util.FormBuilder.add2ButtonsAfterGroup; import static haveno.desktop.util.FormBuilder.addAddressTextField; import static haveno.desktop.util.FormBuilder.addBalanceTextField; import static haveno.desktop.util.FormBuilder.addFundsTextfield; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import static haveno.desktop.util.FormBuilder.addTopLabelComboBox; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; import static haveno.desktop.util.FormBuilder.getEditableValueBox; import static haveno.desktop.util.FormBuilder.getEditableValueBoxWithInfo; import static haveno.desktop.util.FormBuilder.getIconButton; import static haveno.desktop.util.FormBuilder.getIconForLabel; import static haveno.desktop.util.FormBuilder.getSmallIconForLabel; import static haveno.desktop.util.FormBuilder.getTradeInputBox; import static javafx.beans.binding.Bindings.createStringBinding; public abstract class MutableOfferView> extends ActivatableViewAndModel implements ClosableView, SelectableView { protected final Navigation navigation; private final Preferences preferences; private final OfferDetailsWindow offerDetailsWindow; private final CoinFormatter xmrFormatter; private ScrollPane scrollPane; protected GridPane gridPane; private TitledGroupBg payFundsTitledGroupBg, setDepositTitledGroupBg, extraInfoTitledGroupBg, paymentTitledGroupBg; protected TitledGroupBg amountTitledGroupBg; private BusyAnimation waitingForFundsSpinner; private AutoTooltipButton nextButton, cancelButton1, cancelButton2, placeOfferButton, fundFromSavingsWalletButton; private Button priceTypeToggleButton; private InputTextField fixedPriceTextField, marketBasedPriceTextField, triggerPriceInputTextField; protected InputTextField amountTextField, minAmountTextField, volumeTextField, securityDepositInputTextField; private TextField currencyTextField; private AddressTextField addressTextField; private BalanceTextField balanceTextField; private ToggleButton reserveExactAmountSlider; private ToggleButton buyerAsTakerWithoutDepositSlider; protected InputTextArea extraInfoTextArea; private FundsTextField totalToPayTextField; private Label amountDescriptionLabel, priceCurrencyLabel, priceDescriptionLabel, volumeDescriptionLabel, waitingForFundsLabel, marketBasedPriceLabel, percentagePriceDescriptionLabel, tradeFeeDescriptionLabel, resultLabel, tradeFeeInXmrLabel, xLabel, fakeXLabel, securityDepositLabel, securityDepositPercentageLabel, triggerPriceCurrencyLabel, triggerPriceDescriptionLabel; protected Label amountBtcLabel, volumeCurrencyLabel, minAmountBtcLabel; private ComboBox paymentAccountsComboBox; private ComboBox currencyComboBox; private ImageView qrCodeImageView; private StackPane qrCodePane; private VBox paymentAccountsSelection, currencySelection, fixedPriceBox, percentagePriceBox, currencyTextFieldBox, triggerPriceVBox; private HBox fundingHBox, firstRowHBox, secondRowHBox, placeOfferBox, amountValueCurrencyBox, priceAsPercentageValueCurrencyBox, volumeValueCurrencyBox, priceValueCurrencyBox, minAmountValueCurrencyBox, securityDepositAndFeeBox, triggerPriceHBox; private Subscription isWaitingForFundsSubscription, balanceSubscription; private ChangeListener amountFocusedListener, minAmountFocusedListener, volumeFocusedListener, securityDepositFocusedListener, priceFocusedListener, placeOfferCompletedListener, priceAsPercentageFocusedListener, getShowWalletFundedNotificationListener, isMinSecurityDepositListener, buyerAsTakerWithoutDepositListener, triggerPriceFocusedListener, extraInfoFocusedListener; private ChangeListener missingCoinListener; private ChangeListener tradeCurrencyCodeListener, errorMessageListener, marketPriceMarginListener, volumeListener, securityDepositInXMRListener, extraInfoListener; private ChangeListener marketPriceAvailableListener; private EventHandler currencyComboBoxSelectionHandler, paymentAccountsComboBoxSelectionHandler; private OfferView.CloseHandler closeHandler; protected int gridRow = 0; private final List editOfferElements = new ArrayList<>(); private final HashMap paymentAccountWarningDisplayed = new HashMap<>(); private boolean zelleWarningDisplayed, fasterPaymentsWarningDisplayed, isActivated; private InfoInputTextField marketBasedPriceInfoInputTextField, volumeInfoInputTextField, securityDepositInfoInputTextField, triggerPriceInfoInputTextField; private Text xIcon, fakeXIcon; @Setter private OfferView.OfferActionHandler offerActionHandler; private int heightAdjustment = -5; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// public MutableOfferView(M model, Navigation navigation, Preferences preferences, OfferDetailsWindow offerDetailsWindow, CoinFormatter btcFormatter) { super(model); this.navigation = navigation; this.preferences = preferences; this.offerDetailsWindow = offerDetailsWindow; this.xmrFormatter = btcFormatter; } @Override protected void initialize() { addScrollPane(); addGridPane(); addPaymentGroup(); addAmountPriceGroup(); addOptionsGroup(); addExtraInfoGroup(); addNextButtons(); addFundingGroup(); createListeners(); balanceTextField.setFormatter(model.getXmrFormatter()); paymentAccountsComboBox.setConverter(GUIUtil.getPaymentAccountsComboBoxStringConverter()); paymentAccountsComboBox.setButtonCell(GUIUtil.getComboBoxButtonCell(Res.get("shared.chooseTradingAccount"), paymentAccountsComboBox, false)); paymentAccountsComboBox.setCellFactory(model.getPaymentAccountListCellFactory(paymentAccountsComboBox)); doSetFocus(); } protected void doSetFocus() { GUIUtil.focusWhenAddedToScene(amountTextField); } @Override protected void activate() { if (model.getDataModel().isTabSelected) doActivate(); } protected void doActivate() { if (!isActivated) { isActivated = true; currencyComboBox.setPrefWidth(250); paymentAccountsComboBox.setPrefWidth(250); addBindings(); addListeners(); addSubscriptions(); // temporarily disabled due to high CPU usage (per issue #4649) // if (waitingForFundsSpinner != null) // waitingForFundsSpinner.play(); amountDescriptionLabel.setText(model.getAmountDescription()); addressTextField.setAddress(model.getAddressAsString()); addressTextField.setPaymentLabel(model.getPaymentLabel()); currencyComboBox.getSelectionModel().select(model.getTradeCurrency()); paymentAccountsComboBox.setItems(getPaymentAccounts()); paymentAccountsComboBox.getSelectionModel().select(model.getPaymentAccount()); onPaymentAccountsComboBoxSelected(); balanceTextField.setTargetAmount(model.getDataModel().totalToPayAsProperty().get()); updatePriceToggle(); Label popOverLabel = OfferViewUtil.createPopOverLabel(Res.get("createOffer.triggerPrice.tooltip")); triggerPriceInfoInputTextField.setContentForPopOver(popOverLabel, AwesomeIcon.SHIELD); buyerAsTakerWithoutDepositSlider.setSelected(model.dataModel.getBuyerAsTakerWithoutDeposit().get()); triggerPriceInputTextField.setText(model.triggerPrice.get()); extraInfoTextArea.setText(model.dataModel.extraInfo.get()); // show or hide elements based on current screen if (model.showPayFundsScreenDisplayed.get()) { onShowPayFundsScreen(); } } } @Override protected void deactivate() { if (isActivated) { isActivated = false; removeBindings(); removeListeners(); removeSubscriptions(); // temporarily disabled due to high CPU usage (per issue #4649) //if (waitingForFundsSpinner != null) // waitingForFundsSpinner.stop(); } } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onTabSelected(boolean isSelected) { if (isSelected) { doActivate(); } else { deactivate(); } isActivated = isSelected; model.getDataModel().onTabSelected(isSelected); } public void initWithData(OfferDirection direction, TradeCurrency tradeCurrency, boolean initAddressEntry, OfferView.OfferActionHandler offerActionHandler) { this.offerActionHandler = offerActionHandler; boolean result = model.initWithData(direction, tradeCurrency, initAddressEntry); if (!result) { new Popup().headLine(Res.get("popup.warning.noTradingAccountSetup.headline")) .instruction(Res.get("popup.warning.noTradingAccountSetup.msg")) .actionButtonTextWithGoTo("mainView.menu.account") .onAction(() -> { navigation.setReturnPath(navigation.getCurrentPath()); navigation.navigateTo(MainView.class, AccountView.class, TraditionalAccountsView.class); }).show(); } String placeOfferButtonLabel; if (OfferViewUtil.isShownAsBuyOffer(direction, tradeCurrency)) { placeOfferButton.setId("buy-button-big"); placeOfferButtonLabel = Res.get("createOffer.placeOfferButton.buy", tradeCurrency.getCode()); nextButton.setId("buy-button"); fundFromSavingsWalletButton.setId("buy-button"); } else { placeOfferButton.setId("sell-button-big"); placeOfferButtonLabel = Res.get("createOffer.placeOfferButton.sell", tradeCurrency.getCode()); nextButton.setId("sell-button"); fundFromSavingsWalletButton.setId("sell-button"); } buyerAsTakerWithoutDepositSlider.setVisible(model.isSellOffer()); buyerAsTakerWithoutDepositSlider.setManaged(model.isSellOffer()); placeOfferButton.updateText(placeOfferButtonLabel); updatePriceToggle(); } // called from parent as the view does not get notified when the tab is closed public void onClose() { // we use model.placeOfferCompleted to not react on close which was triggered by a successful placeOffer if (model.getDataModel().getUnallocatedBalance().get().compareTo(BigInteger.ZERO) > 0 && !model.placeOfferCompleted.get()) { model.getDataModel().swapTradeToSavings(); } } @Override public void setCloseHandler(OfferView.CloseHandler closeHandler) { this.closeHandler = closeHandler; } /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// private void onPlaceOffer() { if (model.getDataModel().canPlaceOffer()) { Offer offer = model.createAndGetOffer(); if (!DevEnv.isDevMode()) { offerDetailsWindow.onPlaceOffer(() -> { model.onPlaceOffer(offer, offerDetailsWindow::hide); }).show(offer); offerDetailsWindow.onClose(() -> { model.onCancelOffer(null, null); }); } else { balanceSubscription.unsubscribe(); model.onPlaceOffer(offer, () -> { }); } } } private void onShowPayFundsScreen() { nextButton.setVisible(false); nextButton.setManaged(false); nextButton.setOnAction(null); cancelButton1.setVisible(false); cancelButton1.setManaged(false); cancelButton1.setOnAction(null); setDepositTitledGroupBg.setVisible(false); setDepositTitledGroupBg.setManaged(false); securityDepositAndFeeBox.setVisible(false); securityDepositAndFeeBox.setManaged(false); buyerAsTakerWithoutDepositSlider.setVisible(false); buyerAsTakerWithoutDepositSlider.setManaged(false); extraInfoTitledGroupBg.setVisible(false); extraInfoTitledGroupBg.setManaged(false); extraInfoTextArea.setVisible(false); extraInfoTextArea.setManaged(false); updateQrCode(); model.onShowPayFundsScreen(() -> { if (!DevEnv.isDevMode()) { String key = "createOfferFundWalletInfo"; String tradeAmountText = model.isSellOffer() ? Res.get("createOffer.createOfferFundWalletInfo.tradeAmount", model.getTradeAmount()) : ""; String message = Res.get("createOffer.createOfferFundWalletInfo.msg", model.getTotalToPayInfo(), tradeAmountText, model.getSecurityDepositInfo(), model.getTradeFee() ); new Popup().headLine(Res.get("createOffer.createOfferFundWalletInfo.headline")) .instruction(message) .dontShowAgainId(key) .show(); } totalToPayTextField.setFundsStructure(model.getFundsStructure()); totalToPayTextField.setContentForInfoPopOver(createInfoPopover()); }); paymentAccountsSelection.setDisable(true); currencySelection.setDisable(true); editOfferElements.forEach(node -> { node.setMouseTransparent(true); node.setFocusTraversable(false); }); updateOfferElementsStyle(); if (triggerPriceInputTextField.getText().isEmpty()) { triggerPriceVBox.setVisible(false); } balanceTextField.setTargetAmount(model.getDataModel().totalToPayAsProperty().get()); // temporarily disabled due to high CPU usage (per issue #4649) // waitingForFundsSpinner.play(); showFundingGroup(); } private void updateOfferElementsStyle() { GridPane.setColumnSpan(firstRowHBox, 2); String activeInputStyle = "offer-input"; String readOnlyInputStyle = "offer-input-readonly"; amountValueCurrencyBox.getStyleClass().remove(activeInputStyle); amountValueCurrencyBox.getStyleClass().add(readOnlyInputStyle); priceAsPercentageValueCurrencyBox.getStyleClass().remove(activeInputStyle); priceAsPercentageValueCurrencyBox.getStyleClass().add(readOnlyInputStyle); volumeValueCurrencyBox.getStyleClass().remove(activeInputStyle); volumeValueCurrencyBox.getStyleClass().add(readOnlyInputStyle); priceValueCurrencyBox.getStyleClass().remove(activeInputStyle); priceValueCurrencyBox.getStyleClass().add(readOnlyInputStyle); minAmountValueCurrencyBox.getStyleClass().remove(activeInputStyle); minAmountValueCurrencyBox.getStyleClass().add(readOnlyInputStyle); triggerPriceHBox.getStyleClass().remove(activeInputStyle); triggerPriceHBox.getStyleClass().add(readOnlyInputStyle); GridPane.setColumnSpan(secondRowHBox, 1); priceTypeToggleButton.setVisible(false); HBox.setMargin(priceTypeToggleButton, new Insets(16, -14, 0, 0)); resultLabel.getStyleClass().add("small"); xLabel.getStyleClass().add("small"); xIcon.setStyle(String.format("-fx-font-family: %s; -fx-font-size: %s;", MaterialDesignIcon.CLOSE.fontFamily(), "1em")); fakeXIcon.setStyle(String.format("-fx-font-family: %s; -fx-font-size: %s;", MaterialDesignIcon.CLOSE.fontFamily(), "1em")); fakeXLabel.getStyleClass().add("small"); } private void maybeShowZelleWarning(PaymentAccount paymentAccount) { if (paymentAccount.getPaymentMethod().getId().equals(PaymentMethod.ZELLE_ID) && !zelleWarningDisplayed) { zelleWarningDisplayed = true; UserThread.runAfter(GUIUtil::showZelleWarning, 500, TimeUnit.MILLISECONDS); } } private void maybeShowFasterPaymentsWarning(PaymentAccount paymentAccount) { if (paymentAccount.getPaymentMethod().getId().equals(PaymentMethod.FASTER_PAYMENTS_ID) && ((FasterPaymentsAccount) paymentAccount).getHolderName().isEmpty() && !fasterPaymentsWarningDisplayed) { fasterPaymentsWarningDisplayed = true; UserThread.runAfter(() -> GUIUtil.showFasterPaymentsWarning(navigation), 500, TimeUnit.MILLISECONDS); } } private void maybeShowAccountWarning(PaymentAccount paymentAccount, boolean isBuyer) { String msgKey = paymentAccount.getPreTradeMessage(isBuyer); OfferViewUtil.showPaymentAccountWarning(msgKey, paymentAccountWarningDisplayed); } protected void onPaymentAccountsComboBoxSelected() { // Temporary deactivate handler as the payment account change can populate a new currency list and causes // unwanted selection events (item 0) currencyComboBox.setOnAction(null); PaymentAccount paymentAccount = paymentAccountsComboBox.getSelectionModel().getSelectedItem(); if (paymentAccount != null) { maybeShowZelleWarning(paymentAccount); maybeShowFasterPaymentsWarning(paymentAccount); maybeShowAccountWarning(paymentAccount, model.getDataModel().isBuyOffer()); currencySelection.setVisible(paymentAccount.hasMultipleCurrencies()); currencySelection.setManaged(paymentAccount.hasMultipleCurrencies()); currencyTextFieldBox.setVisible(!paymentAccount.hasMultipleCurrencies()); model.onPaymentAccountSelected(paymentAccount); model.onCurrencySelected(model.getTradeCurrency()); updatePlaceOfferButton(); if (paymentAccount.hasMultipleCurrencies()) { final List tradeCurrencies = paymentAccount.getTradeCurrencies(); currencyComboBox.setItems(FXCollections.observableArrayList(tradeCurrencies)); currencyComboBox.getSelectionModel().select(model.getTradeCurrency()); } else { TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency(); if (singleTradeCurrency != null) currencyTextField.setText(singleTradeCurrency.getNameAndCode()); } } else { currencySelection.setVisible(false); currencySelection.setManaged(false); currencyTextFieldBox.setVisible(true); currencyTextField.setText(""); } currencyComboBox.setOnAction(currencyComboBoxSelectionHandler); updatePriceToggle(); } private void onCurrencyComboBoxSelected() { TradeCurrency currency = currencyComboBox.getSelectionModel().getSelectedItem(); model.onCurrencySelected(currency); updatePlaceOfferButton(); } // TODO: this should be called automatically on model.onCurrencySelected() ? private void updatePlaceOfferButton() { TradeCurrency currency = model.getTradeCurrency(); if (OfferViewUtil.isShownAsBuyOffer(model.getDataModel().getDirection(), currency)) { placeOfferButton.updateText(Res.get("createOffer.placeOfferButton.buy", currency.getCode())); } else { placeOfferButton.updateText(Res.get("createOffer.placeOfferButton.sell", currency.getCode())); } } /////////////////////////////////////////////////////////////////////////////////////////// // Navigation /////////////////////////////////////////////////////////////////////////////////////////// protected void close() { if (closeHandler != null) closeHandler.close(); } /////////////////////////////////////////////////////////////////////////////////////////// // Bindings, Listeners /////////////////////////////////////////////////////////////////////////////////////////// private void addBindings() { priceCurrencyLabel.textProperty().bind(createStringBinding(() -> CurrencyUtil.getCounterCurrency(model.tradeCurrencyCode.get()), model.tradeCurrencyCode)); triggerPriceCurrencyLabel.textProperty().bind(createStringBinding(() -> CurrencyUtil.getCounterCurrency(model.tradeCurrencyCode.get()), model.tradeCurrencyCode)); triggerPriceDescriptionLabel.textProperty().bind(model.triggerPriceDescription); percentagePriceDescriptionLabel.textProperty().bind(model.percentagePriceDescription); marketBasedPriceLabel.prefWidthProperty().bind(priceCurrencyLabel.widthProperty()); volumeCurrencyLabel.textProperty().bind(model.tradeCurrencyCode); priceDescriptionLabel.textProperty().bind(createStringBinding(() -> CurrencyUtil.getPriceWithCurrencyCode(model.tradeCurrencyCode.get(), "shared.fixedPriceInCurForCur"), model.tradeCurrencyCode)); volumeDescriptionLabel.textProperty().bind(createStringBinding(model.volumeDescriptionLabel::get, model.tradeCurrencyCode, model.volumeDescriptionLabel)); amountTextField.textProperty().bindBidirectional(model.amount); minAmountTextField.textProperty().bindBidirectional(model.minAmount); fixedPriceTextField.textProperty().bindBidirectional(model.price); triggerPriceInputTextField.textProperty().bindBidirectional(model.triggerPrice); marketBasedPriceTextField.textProperty().bindBidirectional(model.marketPriceMargin); volumeTextField.textProperty().bindBidirectional(model.volume); volumeTextField.promptTextProperty().bind(model.volumePromptLabel); totalToPayTextField.textProperty().bind(model.totalToPay); addressTextField.amountAsProperty().bind(model.getDataModel().getMissingCoin()); securityDepositInputTextField.textProperty().bindBidirectional(model.securityDeposit); securityDepositLabel.textProperty().bind(model.securityDepositLabel); tradeFeeInXmrLabel.textProperty().bind(model.tradeFeeInXmrWithFiat); tradeFeeDescriptionLabel.textProperty().bind(model.tradeFeeDescription); extraInfoTextArea.textProperty().bindBidirectional(model.extraInfo); // Validation amountTextField.validationResultProperty().bind(model.amountValidationResult); minAmountTextField.validationResultProperty().bind(model.minAmountValidationResult); fixedPriceTextField.validationResultProperty().bind(model.priceValidationResult); triggerPriceInputTextField.validationResultProperty().bind(model.triggerPriceValidationResult); volumeTextField.validationResultProperty().bind(model.volumeValidationResult); securityDepositInputTextField.validationResultProperty().bind(model.securityDepositValidationResult); extraInfoTextArea.validationResultProperty().bind(model.extraInfoValidationResult); // funding fundingHBox.visibleProperty().bind(model.getDataModel().getIsXmrWalletFunded().not().and(model.showPayFundsScreenDisplayed)); fundingHBox.managedProperty().bind(model.getDataModel().getIsXmrWalletFunded().not().and(model.showPayFundsScreenDisplayed)); waitingForFundsLabel.textProperty().bind(model.waitingForFundsText); placeOfferBox.visibleProperty().bind(model.getDataModel().getIsXmrWalletFunded().and(model.showPayFundsScreenDisplayed)); placeOfferBox.managedProperty().bind(model.getDataModel().getIsXmrWalletFunded().and(model.showPayFundsScreenDisplayed)); placeOfferButton.disableProperty().bind(model.isPlaceOfferButtonDisabled); cancelButton2.disableProperty().bind(model.cancelButtonDisabled); // trading account paymentAccountsComboBox.managedProperty().bind(paymentAccountsComboBox.visibleProperty()); paymentTitledGroupBg.managedProperty().bind(paymentTitledGroupBg.visibleProperty()); currencyComboBox.prefWidthProperty().bind(paymentAccountsComboBox.widthProperty()); currencyComboBox.managedProperty().bind(currencyComboBox.visibleProperty()); currencyTextFieldBox.prefWidthProperty().bind(paymentAccountsComboBox.widthProperty()); currencyTextFieldBox.managedProperty().bind(currencyTextFieldBox.visibleProperty()); } private void removeBindings() { priceCurrencyLabel.textProperty().unbind(); triggerPriceCurrencyLabel.textProperty().unbind(); triggerPriceDescriptionLabel.textProperty().unbind(); percentagePriceDescriptionLabel.textProperty().unbind(); volumeCurrencyLabel.textProperty().unbind(); priceDescriptionLabel.textProperty().unbind(); volumeDescriptionLabel.textProperty().unbind(); amountTextField.textProperty().unbindBidirectional(model.amount); minAmountTextField.textProperty().unbindBidirectional(model.minAmount); fixedPriceTextField.textProperty().unbindBidirectional(model.price); triggerPriceInputTextField.textProperty().unbindBidirectional(model.triggerPrice); marketBasedPriceTextField.textProperty().unbindBidirectional(model.marketPriceMargin); marketBasedPriceLabel.prefWidthProperty().unbind(); volumeTextField.textProperty().unbindBidirectional(model.volume); volumeTextField.promptTextProperty().unbindBidirectional(model.volume); totalToPayTextField.textProperty().unbind(); addressTextField.amountAsProperty().unbind(); securityDepositInputTextField.textProperty().unbindBidirectional(model.securityDeposit); securityDepositLabel.textProperty().unbind(); tradeFeeInXmrLabel.textProperty().unbind(); tradeFeeDescriptionLabel.textProperty().unbind(); tradeFeeInXmrLabel.visibleProperty().unbind(); tradeFeeDescriptionLabel.visibleProperty().unbind(); extraInfoTextArea.textProperty().unbindBidirectional(model.extraInfo); // Validation amountTextField.validationResultProperty().unbind(); minAmountTextField.validationResultProperty().unbind(); fixedPriceTextField.validationResultProperty().unbind(); triggerPriceInputTextField.validationResultProperty().unbind(); volumeTextField.validationResultProperty().unbind(); securityDepositInputTextField.validationResultProperty().unbind(); // funding fundingHBox.visibleProperty().unbind(); fundingHBox.managedProperty().unbind(); waitingForFundsLabel.textProperty().unbind(); placeOfferBox.visibleProperty().unbind(); placeOfferBox.managedProperty().unbind(); placeOfferButton.disableProperty().unbind(); cancelButton2.disableProperty().unbind(); // trading account paymentTitledGroupBg.managedProperty().unbind(); paymentAccountsComboBox.managedProperty().unbind(); currencyComboBox.managedProperty().unbind(); currencyComboBox.prefWidthProperty().unbind(); currencyTextFieldBox.managedProperty().unbind(); } private void addSubscriptions() { isWaitingForFundsSubscription = EasyBind.subscribe(model.isWaitingForFunds, isWaitingForFunds -> { // temporarily disabled due to high CPU usage (per issue #4649) //if (isWaitingForFunds) { // waitingForFundsSpinner.play(); //} else { // waitingForFundsSpinner.stop(); //} waitingForFundsLabel.setVisible(isWaitingForFunds); waitingForFundsLabel.setManaged(isWaitingForFunds); }); balanceSubscription = EasyBind.subscribe(model.getDataModel().getUnallocatedBalance(), balanceTextField::setBalance); } private void removeSubscriptions() { isWaitingForFundsSubscription.unsubscribe(); balanceSubscription.unsubscribe(); } private void createListeners() { amountFocusedListener = (o, oldValue, newValue) -> { model.onFocusOutAmountTextField(oldValue, newValue); amountTextField.setText(model.amount.get()); }; minAmountFocusedListener = (o, oldValue, newValue) -> { model.onFocusOutMinAmountTextField(oldValue, newValue); minAmountTextField.setText(model.minAmount.get()); }; priceFocusedListener = (o, oldValue, newValue) -> { model.onFocusOutPriceTextField(oldValue, newValue); fixedPriceTextField.setText(model.price.get()); }; priceAsPercentageFocusedListener = (o, oldValue, newValue) -> { model.onFocusOutPriceAsPercentageTextField(oldValue, newValue); marketBasedPriceTextField.setText(model.marketPriceMargin.get()); }; volumeFocusedListener = (o, oldValue, newValue) -> { model.onFocusOutVolumeTextField(oldValue, newValue); volumeTextField.setText(model.volume.get()); }; securityDepositFocusedListener = (o, oldValue, newValue) -> { model.onFocusOutSecurityDepositTextField(oldValue, newValue); securityDepositInputTextField.setText(model.securityDeposit.get()); }; triggerPriceFocusedListener = (o, oldValue, newValue) -> { model.onFocusOutTriggerPriceTextField(oldValue, newValue); triggerPriceInputTextField.setText(model.triggerPrice.get()); }; extraInfoFocusedListener = (observable, oldValue, newValue) -> { model.onFocusOutExtraInfoTextArea(oldValue, newValue); // avoid setting text area to empty text because blinking caret does not appear if (model.extraInfo.get() != null && !model.extraInfo.get().isEmpty()) { extraInfoTextArea.setText(model.extraInfo.get()); } }; errorMessageListener = (o, oldValue, newValue) -> { if (model.createOfferCanceled) return; if (newValue != null) { UserThread.runAfter(() -> new Popup().error(Res.get("createOffer.amountPriceBox.error.message", model.errorMessage.get())) .show(), 100, TimeUnit.MILLISECONDS); } }; paymentAccountsComboBoxSelectionHandler = e -> onPaymentAccountsComboBoxSelected(); currencyComboBoxSelectionHandler = e -> onCurrencyComboBoxSelected(); tradeCurrencyCodeListener = (observable, oldValue, newValue) -> { fixedPriceTextField.clear(); marketBasedPriceTextField.clear(); volumeTextField.clear(); triggerPriceInputTextField.clear(); }; placeOfferCompletedListener = (o, oldValue, newValue) -> { if (DevEnv.isDevMode()) { close(); } else if (newValue) { // We need a bit of delay to avoid issues with fade out/fade in of 2 popups String key = "createOfferSuccessInfo"; if (DontShowAgainLookup.showAgain(key)) { UserThread.runAfter(() -> new Popup().headLine(Res.get("createOffer.success.headline")) .feedback(Res.get("createOffer.success.info")) .dontShowAgainId(key) .actionButtonTextWithGoTo("portfolio.tab.openOffers") .onAction(this::closeAndGoToOpenOffers) .onClose(this::close) .show(), 1); } else { closeAndGoToOpenOffers(); } } }; marketPriceAvailableListener = (observable, oldValue, newValue) -> updatePriceToggle(); getShowWalletFundedNotificationListener = (observable, oldValue, newValue) -> { if (newValue) { Notification walletFundedNotification = new Notification() .headLine(Res.get("notification.walletUpdate.headline")) .notification(Res.get("notification.walletUpdate.msg", HavenoUtils.formatXmr(model.getDataModel().getTotalToPay().get(), true))) .autoClose(); walletFundedNotification.show(); } }; securityDepositInXMRListener = (observable, oldValue, newValue) -> { if (!newValue.equals("")) { updateSecurityDepositLabels(); } else { securityDepositInfoInputTextField.setContentForInfoPopOver(null); } }; volumeListener = (observable, oldValue, newValue) -> { if (!newValue.equals("") && CurrencyUtil.isFiatCurrency(model.tradeCurrencyCode.get())) { Label popOverLabel = OfferViewUtil.createPopOverLabel(Res.get("offerbook.info.roundedFiatVolume")); volumeInfoInputTextField.setContentForPrivacyPopOver(popOverLabel); } else { volumeInfoInputTextField.hideIcon(); } }; missingCoinListener = (observable, oldValue, newValue) -> { if (!newValue.toString().equals("")) { updateQrCode(); } }; marketPriceMarginListener = (observable, oldValue, newValue) -> { if (marketBasedPriceInfoInputTextField != null) { String tooltip; if (newValue.equals("0.00")) { if (model.isSellOffer()) { tooltip = Res.get("createOffer.info.sellAtMarketPrice"); } else { tooltip = Res.get("createOffer.info.buyAtMarketPrice"); } final Label atMarketPriceLabel = OfferViewUtil.createPopOverLabel(tooltip); marketBasedPriceInfoInputTextField.setContentForInfoPopOver(atMarketPriceLabel); } else if (newValue.contains("-")) { if (model.isSellOffer()) { tooltip = Res.get("createOffer.warning.sellBelowMarketPrice", newValue.substring(1)); } else { tooltip = Res.get("createOffer.warning.buyAboveMarketPrice", newValue.substring(1)); } final Label negativePercentageLabel = OfferViewUtil.createPopOverLabel(tooltip); marketBasedPriceInfoInputTextField.setContentForWarningPopOver(negativePercentageLabel); } else if (!newValue.equals("")) { if (model.isSellOffer()) { tooltip = Res.get("createOffer.info.sellAboveMarketPrice", newValue); } else { tooltip = Res.get("createOffer.info.buyBelowMarketPrice", newValue); } Label positivePercentageLabel = OfferViewUtil.createPopOverLabel(tooltip); marketBasedPriceInfoInputTextField.setContentForInfoPopOver(positivePercentageLabel); } } }; isMinSecurityDepositListener = ((observable, oldValue, newValue) -> { updateSecurityDepositLabels(); }); buyerAsTakerWithoutDepositListener = ((observable, oldValue, newValue) -> { updateSecurityDepositLabels(); }); extraInfoListener = (observable, oldValue, newValue) -> { if (newValue != null && !newValue.equals("")) { // no action } }; } private void updateSecurityDepositLabels() { if (model.isMinSecurityDeposit.get()) { // show XMR securityDepositPercentageLabel.setText(Res.getBaseCurrencyCode()); securityDepositInputTextField.setDisable(true); } else { // show % securityDepositPercentageLabel.setText("%"); securityDepositInputTextField.setDisable(model.getDataModel().buyerAsTakerWithoutDeposit.get()); } if (model.securityDepositInXMR.get() != null && !model.securityDepositInXMR.get().equals("")) { Label depositInBTCInfo = OfferViewUtil.createPopOverLabel(model.getSecurityDepositPopOverLabel(model.securityDepositInXMR.get())); securityDepositInfoInputTextField.setContentForInfoPopOver(depositInBTCInfo); } } private void updateQrCode() { final byte[] imageBytes = QRCode .from(getMoneroURI()) .withSize(300, 300) .to(ImageType.PNG) .stream() .toByteArray(); Image qrImage = new Image(new ByteArrayInputStream(imageBytes)); qrCodeImageView.setImage(qrImage); } private void closeAndGoToOpenOffers() { //go to open offers UserThread.runAfter(() -> navigation.navigateTo(MainView.class, PortfolioView.class, OpenOffersView.class), 1, TimeUnit.SECONDS); close(); } protected void updatePriceToggle() { int marketPriceAvailableValue = model.marketPriceAvailableProperty.get(); if (marketPriceAvailableValue > -1) { boolean showPriceToggle = marketPriceAvailableValue == 1 && !PaymentMethod.isFixedPriceOnly(model.getDataModel().paymentAccount.getPaymentMethod().getId()); percentagePriceBox.setVisible(showPriceToggle); priceTypeToggleButton.setVisible(showPriceToggle); boolean fixedPriceSelected = !model.getDataModel().getUseMarketBasedPrice().get() || !showPriceToggle; updatePriceToggleButtons(fixedPriceSelected); } } private void addListeners() { model.tradeCurrencyCode.addListener(tradeCurrencyCodeListener); model.marketPriceAvailableProperty.addListener(marketPriceAvailableListener); model.marketPriceMargin.addListener(marketPriceMarginListener); model.volume.addListener(volumeListener); model.getDataModel().missingCoin.addListener(missingCoinListener); model.securityDepositInXMR.addListener(securityDepositInXMRListener); model.isMinSecurityDeposit.addListener(isMinSecurityDepositListener); model.getDataModel().buyerAsTakerWithoutDeposit.addListener(buyerAsTakerWithoutDepositListener); model.getDataModel().extraInfo.addListener(extraInfoListener); // focus out amountTextField.focusedProperty().addListener(amountFocusedListener); minAmountTextField.focusedProperty().addListener(minAmountFocusedListener); fixedPriceTextField.focusedProperty().addListener(priceFocusedListener); triggerPriceInputTextField.focusedProperty().addListener(triggerPriceFocusedListener); marketBasedPriceTextField.focusedProperty().addListener(priceAsPercentageFocusedListener); volumeTextField.focusedProperty().addListener(volumeFocusedListener); securityDepositInputTextField.focusedProperty().addListener(securityDepositFocusedListener); extraInfoTextArea.focusedProperty().addListener(extraInfoFocusedListener); // notifications model.getDataModel().getShowWalletFundedNotification().addListener(getShowWalletFundedNotificationListener); // warnings model.errorMessage.addListener(errorMessageListener); // model.getDataModel().feeFromFundingTxProperty.addListener(feeFromFundingTxListener); model.placeOfferCompleted.addListener(placeOfferCompletedListener); // UI actions paymentAccountsComboBox.setOnAction(paymentAccountsComboBoxSelectionHandler); currencyComboBox.setOnAction(currencyComboBoxSelectionHandler); } private void removeListeners() { model.tradeCurrencyCode.removeListener(tradeCurrencyCodeListener); model.marketPriceAvailableProperty.removeListener(marketPriceAvailableListener); model.marketPriceMargin.removeListener(marketPriceMarginListener); model.volume.removeListener(volumeListener); model.getDataModel().missingCoin.removeListener(missingCoinListener); model.securityDepositInXMR.removeListener(securityDepositInXMRListener); model.isMinSecurityDeposit.removeListener(isMinSecurityDepositListener); model.getDataModel().buyerAsTakerWithoutDeposit.removeListener(buyerAsTakerWithoutDepositListener); model.getDataModel().extraInfo.removeListener(extraInfoListener); // focus out amountTextField.focusedProperty().removeListener(amountFocusedListener); minAmountTextField.focusedProperty().removeListener(minAmountFocusedListener); fixedPriceTextField.focusedProperty().removeListener(priceFocusedListener); triggerPriceInputTextField.focusedProperty().removeListener(triggerPriceFocusedListener); marketBasedPriceTextField.focusedProperty().removeListener(priceAsPercentageFocusedListener); volumeTextField.focusedProperty().removeListener(volumeFocusedListener); securityDepositInputTextField.focusedProperty().removeListener(securityDepositFocusedListener); extraInfoTextArea.focusedProperty().removeListener(extraInfoFocusedListener); // notifications model.getDataModel().getShowWalletFundedNotification().removeListener(getShowWalletFundedNotificationListener); // warnings model.errorMessage.removeListener(errorMessageListener); // model.getDataModel().feeFromFundingTxProperty.removeListener(feeFromFundingTxListener); model.placeOfferCompleted.removeListener(placeOfferCompletedListener); // UI actions paymentAccountsComboBox.setOnAction(null); currencyComboBox.setOnAction(null); } /////////////////////////////////////////////////////////////////////////////////////////// // Build UI elements /////////////////////////////////////////////////////////////////////////////////////////// private void addScrollPane() { scrollPane = GUIUtil.createScrollPane(); root.getChildren().add(scrollPane); } private void addGridPane() { gridPane = new GridPane(); gridPane.getStyleClass().add("content-pane"); gridPane.setPadding(new Insets(25, 25, 25, 25)); gridPane.setHgap(5); gridPane.setVgap(5); GUIUtil.setDefaultTwoColumnConstraintsForGridPane(gridPane); scrollPane.setContent(gridPane); scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); } private void addPaymentGroup() { paymentTitledGroupBg = addTitledGroupBg(gridPane, gridRow, 1, Res.get("offerbook.createOffer"), heightAdjustment); GridPane.setColumnSpan(paymentTitledGroupBg, 2); HBox paymentGroupBox = new HBox(); paymentGroupBox.setAlignment(Pos.CENTER_LEFT); paymentGroupBox.setSpacing(12); paymentGroupBox.setPadding(new Insets(10, 0, 18, 0)); final Tuple3> tradingAccountBoxTuple = addTopLabelComboBox( Res.get("shared.chooseTradingAccount"), Res.get("shared.chooseTradingAccount")); final Tuple3> currencyBoxTuple = addTopLabelComboBox( Res.get("shared.currency"), Res.get("list.currency.select")); paymentAccountsSelection = tradingAccountBoxTuple.first; currencySelection = currencyBoxTuple.first; paymentGroupBox.getChildren().addAll(paymentAccountsSelection, currencySelection); GridPane.setRowIndex(paymentGroupBox, gridRow); GridPane.setColumnSpan(paymentGroupBox, 2); GridPane.setMargin(paymentGroupBox, new Insets(Layout.FIRST_ROW_DISTANCE + heightAdjustment, 0, 0, 0)); gridPane.getChildren().add(paymentGroupBox); tradingAccountBoxTuple.first.setMinWidth(800); paymentAccountsComboBox = tradingAccountBoxTuple.third; paymentAccountsComboBox.setMinWidth(tradingAccountBoxTuple.first.getMinWidth()); paymentAccountsComboBox.setPrefWidth(tradingAccountBoxTuple.first.getMinWidth()); paymentAccountsComboBox.getStyleClass().add("input-with-border"); editOfferElements.add(paymentAccountsSelection); // we display either currencyComboBox (multi currency account) or currencyTextField (single) currencyComboBox = currencyBoxTuple.third; currencyComboBox.setMaxWidth(tradingAccountBoxTuple.first.getMinWidth() / 2); currencyComboBox.getStyleClass().add("input-with-border"); editOfferElements.add(currencySelection); currencyComboBox.setConverter(new StringConverter<>() { @Override public String toString(TradeCurrency tradeCurrency) { return tradeCurrency.getNameAndCode(); } @Override public TradeCurrency fromString(String s) { return null; } }); final Tuple3 currencyTextFieldTuple = addTopLabelTextField(gridPane, gridRow, Res.get("shared.currency"), "", 5d); currencyTextField = currencyTextFieldTuple.second; currencyTextFieldBox = currencyTextFieldTuple.third; currencyTextFieldBox.setMaxWidth(tradingAccountBoxTuple.first.getMinWidth() / 2); currencyTextFieldBox.setVisible(false); editOfferElements.add(currencyTextFieldBox); paymentGroupBox.getChildren().add(currencyTextFieldBox); } private void addAmountPriceGroup() { amountTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 2, Res.get("createOffer.setAmountPrice"), 25 + heightAdjustment); GridPane.setColumnSpan(amountTitledGroupBg, 2); addAmountPriceFields(); addSecondRow(); } private void addOptionsGroup() { setDepositTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 2, Res.get("shared.advancedOptions"), 25 + heightAdjustment); securityDepositAndFeeBox = new HBox(); securityDepositAndFeeBox.setSpacing(40); GridPane.setRowIndex(securityDepositAndFeeBox, gridRow); GridPane.setColumnSpan(securityDepositAndFeeBox, GridPane.REMAINING); GridPane.setColumnIndex(securityDepositAndFeeBox, 0); GridPane.setHalignment(securityDepositAndFeeBox, HPos.LEFT); GridPane.setMargin(securityDepositAndFeeBox, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0)); gridPane.getChildren().add(securityDepositAndFeeBox); VBox tradeFeeFieldsBox = getTradeFeeFieldsBox(); tradeFeeFieldsBox.setMinWidth(240); securityDepositAndFeeBox.getChildren().addAll(getSecurityDepositBox(), tradeFeeFieldsBox); buyerAsTakerWithoutDepositSlider = FormBuilder.addSlideToggleButton(gridPane, ++gridRow, Res.get("createOffer.buyerAsTakerWithoutDeposit")); buyerAsTakerWithoutDepositSlider.setPadding(new Insets(0, 0, 0, 0)); buyerAsTakerWithoutDepositSlider.setOnAction(event -> { // popup info box String key = "popup.info.buyerAsTakerWithoutDeposit"; if (buyerAsTakerWithoutDepositSlider.isSelected() && DontShowAgainLookup.showAgain(key)) { new Popup().headLine(Res.get(key + ".headline")) .information(Res.get(key)) .closeButtonText(Res.get("shared.cancel")) .actionButtonText(Res.get("shared.ok")) .onAction(() -> model.dataModel.setBuyerAsTakerWithoutDeposit(true)) .onClose(() -> { buyerAsTakerWithoutDepositSlider.setSelected(false); model.dataModel.setBuyerAsTakerWithoutDeposit(false); }) .dontShowAgainId(key) .show(); } else { model.dataModel.setBuyerAsTakerWithoutDeposit(buyerAsTakerWithoutDepositSlider.isSelected()); } }); GridPane.setHalignment(buyerAsTakerWithoutDepositSlider, HPos.LEFT); GridPane.setMargin(buyerAsTakerWithoutDepositSlider, new Insets(0, 0, 0, 0)); } private void addExtraInfoGroup() { extraInfoTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 1, Res.get("payment.shared.optionalExtra"), 25 + heightAdjustment); GridPane.setColumnSpan(extraInfoTitledGroupBg, 3); extraInfoTextArea = new InputTextArea(); extraInfoTextArea.setText(""); extraInfoTextArea.setPromptText(Res.get("payment.shared.extraInfo.prompt.offer")); extraInfoTextArea.getStyleClass().add("text-area"); extraInfoTextArea.setWrapText(true); extraInfoTextArea.setPrefHeight(75); extraInfoTextArea.setMinHeight(75); extraInfoTextArea.setMaxHeight(75); extraInfoTextArea.setFocusTraversable(false); GridPane.setRowIndex(extraInfoTextArea, gridRow); GridPane.setColumnSpan(extraInfoTextArea, GridPane.REMAINING); GridPane.setColumnIndex(extraInfoTextArea, 0); GridPane.setHalignment(extraInfoTextArea, HPos.LEFT); GridPane.setMargin(extraInfoTextArea, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, 0, 10, 0)); gridPane.getChildren().add(extraInfoTextArea); } private void addNextButtons() { Tuple2 tuple = add2ButtonsAfterGroup(gridPane, ++gridRow, Res.get("shared.nextStep"), Res.get("shared.cancel")); nextButton = (AutoTooltipButton) tuple.first; nextButton.setMaxWidth(200); editOfferElements.add(nextButton); nextButton.disableProperty().bind(model.isNextButtonDisabled); cancelButton1 = (AutoTooltipButton) tuple.second; cancelButton1.setMaxWidth(200); editOfferElements.add(cancelButton1); cancelButton1.setDefaultButton(false); cancelButton1.setOnAction(e -> { close(); model.getDataModel().swapTradeToSavings(); }); nextButton.setOnAction(e -> { if (model.isPriceInRange()) { // warn if sell offer exceeds unsigned buy limit within release window boolean isSellOffer = model.getDataModel().isSellOffer(); boolean exceedsUnsignedBuyLimit = model.getDataModel().getAmount().get().compareTo(model.getDataModel().getMaxUnsignedBuyLimit()) > 0; String key = "popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit"; if (isSellOffer && exceedsUnsignedBuyLimit && DontShowAgainLookup.showAgain(key) && HavenoUtils.isReleasedWithinDays(HavenoUtils.WARN_ON_OFFER_EXCEEDS_UNSIGNED_BUY_LIMIT_DAYS)) { new Popup().information(Res.get(key, HavenoUtils.formatXmr(model.getDataModel().getMaxUnsignedBuyLimit(), true), Res.get("offerbook.warning.newVersionAnnouncement"))) .closeButtonText(Res.get("shared.cancel")) .actionButtonText(Res.get("shared.ok")) .onAction(this::onShowPayFundsScreen) .width(900) .dontShowAgainId(key) .show(); } else { onShowPayFundsScreen(); } } }); } protected void hideOptionsGroup() { setDepositTitledGroupBg.setVisible(false); setDepositTitledGroupBg.setManaged(false); securityDepositAndFeeBox.setVisible(false); securityDepositAndFeeBox.setManaged(false); buyerAsTakerWithoutDepositSlider.setVisible(false); buyerAsTakerWithoutDepositSlider.setManaged(false); } protected void hideExtraInfoGroup() { extraInfoTitledGroupBg.setVisible(false); extraInfoTitledGroupBg.setManaged(false); extraInfoTextArea.setVisible(false); extraInfoTextArea.setManaged(false); } protected void hideNextButtons() { nextButton.setVisible(false); nextButton.setManaged(false); cancelButton1.setVisible(false); cancelButton1.setManaged(false); } protected void hideFundingGroup() { payFundsTitledGroupBg.setVisible(false); payFundsTitledGroupBg.setManaged(false); totalToPayTextField.setVisible(false); totalToPayTextField.setManaged(false); addressTextField.setVisible(false); addressTextField.setManaged(false); qrCodePane.setVisible(false); qrCodePane.setManaged(false); balanceTextField.setVisible(false); balanceTextField.setManaged(false); cancelButton2.setVisible(false); cancelButton2.setManaged(false); reserveExactAmountSlider.setVisible(false); reserveExactAmountSlider.setManaged(false); } protected void showFundingGroup() { payFundsTitledGroupBg.setVisible(true); payFundsTitledGroupBg.setManaged(true); totalToPayTextField.setVisible(true); totalToPayTextField.setManaged(true); addressTextField.setVisible(true); addressTextField.setManaged(true); qrCodePane.setVisible(true); qrCodePane.setManaged(true); balanceTextField.setVisible(true); balanceTextField.setManaged(true); cancelButton2.setVisible(true); cancelButton2.setManaged(true); reserveExactAmountSlider.setVisible(true); reserveExactAmountSlider.setManaged(true); } private VBox getSecurityDepositBox() { Tuple3 tuple = getEditableValueBoxWithInfo( Res.get("createOffer.securityDeposit.prompt")); securityDepositInfoInputTextField = tuple.second; securityDepositInputTextField = securityDepositInfoInputTextField.getInputTextField(); securityDepositPercentageLabel = tuple.third; // getEditableValueBox delivers BTC, so we overwrite it with % securityDepositPercentageLabel.setText("%"); Tuple2 tradeInputBoxTuple = getTradeInputBox(tuple.first, model.getSecurityDepositLabel()); VBox depositBox = tradeInputBoxTuple.second; securityDepositLabel = tradeInputBoxTuple.first; depositBox.setMaxWidth(310); editOfferElements.add(securityDepositInputTextField); editOfferElements.add(securityDepositPercentageLabel); return depositBox; } private void addFundingGroup() { // don't increase gridRow as we removed button when this gets visible payFundsTitledGroupBg = addTitledGroupBg(gridPane, gridRow, 3, Res.get("createOffer.fundsBox.title"), 20 + heightAdjustment); payFundsTitledGroupBg.getStyleClass().add("last"); GridPane.setColumnSpan(payFundsTitledGroupBg, 2); payFundsTitledGroupBg.setVisible(false); totalToPayTextField = addFundsTextfield(gridPane, gridRow, Res.get("shared.totalsNeeded"), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); totalToPayTextField.setVisible(false); GridPane.setMargin(totalToPayTextField, new Insets(60 + heightAdjustment, 10, 0, 0)); Tuple2 qrCodeTuple = GUIUtil.getSmallXmrQrCodePane(); qrCodePane = qrCodeTuple.first; qrCodeImageView = qrCodeTuple.second; Tooltip.install(qrCodePane, new Tooltip(Res.get("shared.openLargeQRWindow"))); qrCodePane.setOnMouseClicked(e -> UserThread.runAfter( () -> new QRCodeWindow(getMoneroURI()).show(), 200, TimeUnit.MILLISECONDS)); GridPane.setRowIndex(qrCodePane, gridRow); GridPane.setColumnIndex(qrCodePane, 1); GridPane.setRowSpan(qrCodePane, 3); GridPane.setValignment(qrCodePane, VPos.BOTTOM); GridPane.setMargin(qrCodePane, new Insets(Layout.FIRST_ROW_DISTANCE - 9, 0, 0, 10)); gridPane.getChildren().add(qrCodePane); qrCodePane.setVisible(false); qrCodePane.setManaged(false); addressTextField = addAddressTextField(gridPane, ++gridRow, Res.get("shared.tradeWalletAddress")); addressTextField.setVisible(false); balanceTextField = addBalanceTextField(gridPane, ++gridRow, Res.get("shared.tradeWalletBalance")); balanceTextField.setVisible(false); reserveExactAmountSlider = FormBuilder.addSlideToggleButton(gridPane, ++gridRow, Res.get("shared.reserveExactAmount"), heightAdjustment); GridPane.setHalignment(reserveExactAmountSlider, HPos.LEFT); GridPane.setMargin(reserveExactAmountSlider, new Insets(-5, 0, -5, 0)); reserveExactAmountSlider.setPadding(new Insets(0, 0, 0, 0)); reserveExactAmountSlider.setVisible(false); reserveExactAmountSlider.setSelected(preferences.getSplitOfferOutput()); reserveExactAmountSlider.setOnAction(event -> { boolean selected = reserveExactAmountSlider.isSelected(); if (selected != preferences.getSplitOfferOutput()) { preferences.setSplitOfferOutput(selected); model.dataModel.setReserveExactAmount(selected); } }); fundingHBox = new HBox(); fundingHBox.setVisible(false); fundingHBox.setManaged(false); fundingHBox.setSpacing(10); fundFromSavingsWalletButton = new AutoTooltipButton(Res.get("shared.fundFromSavingsWalletButton")); fundFromSavingsWalletButton.setDefaultButton(true); fundFromSavingsWalletButton.getStyleClass().add("action-button"); fundFromSavingsWalletButton.setOnAction(e -> model.fundFromSavingsWallet()); Label label = new AutoTooltipLabel(Res.get("shared.OR")); label.setPadding(new Insets(5, 0, 0, 0)); Button fundFromExternalWalletButton = new AutoTooltipButton(Res.get("shared.fundFromExternalWalletButton")); fundFromExternalWalletButton.setDefaultButton(false); fundFromExternalWalletButton.setOnAction(e -> openWallet()); waitingForFundsSpinner = new BusyAnimation(false); waitingForFundsLabel = new AutoTooltipLabel(); waitingForFundsLabel.setPadding(new Insets(5, 0, 0, 0)); fundingHBox.getChildren().addAll(fundFromSavingsWalletButton, label, fundFromExternalWalletButton, waitingForFundsSpinner, waitingForFundsLabel); GridPane.setRowIndex(fundingHBox, ++gridRow); GridPane.setColumnSpan(fundingHBox, 2); GridPane.setMargin(fundingHBox, new Insets(5, 0, 0, 0)); gridPane.getChildren().add(fundingHBox); placeOfferBox = new HBox(); placeOfferBox.setSpacing(10); GridPane.setRowIndex(placeOfferBox, gridRow); GridPane.setColumnSpan(placeOfferBox, 2); GridPane.setMargin(placeOfferBox, new Insets(5, 20, 0, 0)); gridPane.getChildren().add(placeOfferBox); placeOfferButton = new AutoTooltipButton(); placeOfferButton.setOnAction(e -> onPlaceOffer()); placeOfferButton.setMinHeight(40); placeOfferButton.setPadding(new Insets(0, 20, 0, 20)); placeOfferBox.getChildren().add(placeOfferButton); placeOfferBox.visibleProperty().addListener((observable, oldValue, newValue) -> { if (newValue) { fundingHBox.getChildren().remove(cancelButton2); placeOfferBox.getChildren().add(cancelButton2); } else if (!fundingHBox.getChildren().contains(cancelButton2)) { placeOfferBox.getChildren().remove(cancelButton2); fundingHBox.getChildren().add(cancelButton2); } }); cancelButton2 = new AutoTooltipButton(Res.get("shared.cancel")); fundingHBox.getChildren().add(cancelButton2); cancelButton2.setOnAction(e -> { String key = "CreateOfferCancelAndFunded"; if (model.getDataModel().getIsXmrWalletFunded().get() && preferences.showAgain(key)) { new Popup().backgroundInfo(Res.get("createOffer.warnCancelOffer")) .closeButtonText(Res.get("shared.no")) .actionButtonText(Res.get("shared.yesCancel")) .onAction(() -> { close(); model.getDataModel().swapTradeToSavings(); }) .dontShowAgainId(key) .show(); } else { close(); model.getDataModel().swapTradeToSavings(); } }); cancelButton2.setDefaultButton(false); cancelButton2.setVisible(false); hideFundingGroup(); } private void openWallet() { try { Utilities.openURI(URI.create(getMoneroURI())); } catch (Exception ex) { log.warn(ex.getMessage()); new Popup().warning(Res.get("shared.openDefaultWalletFailed")).show(); } } @NotNull private String getMoneroURI() { return GUIUtil.getMoneroURI( addressTextField.getAddress(), model.getDataModel().getMissingCoin().get(), model.getPaymentLabel()); } private void addAmountPriceFields() { // amountBox Tuple3 amountValueCurrencyBoxTuple = getEditableValueBox(Res.get("createOffer.amount.prompt")); amountValueCurrencyBox = amountValueCurrencyBoxTuple.first; amountTextField = amountValueCurrencyBoxTuple.second; editOfferElements.add(amountTextField); amountBtcLabel = amountValueCurrencyBoxTuple.third; editOfferElements.add(amountBtcLabel); Tuple2 amountInputBoxTuple = getTradeInputBox(amountValueCurrencyBox, model.getAmountDescription()); amountDescriptionLabel = amountInputBoxTuple.first; editOfferElements.add(amountDescriptionLabel); VBox amountBox = amountInputBoxTuple.second; // x xLabel = new Label(); xIcon = getIconForLabel(MaterialDesignIcon.CLOSE, "2em", xLabel); xIcon.getStyleClass().add("opaque-icon"); xLabel.getStyleClass().add("opaque-icon-character"); // price as percent Tuple3 priceAsPercentageTuple = getEditableValueBoxWithInfo(Res.get("createOffer.price.prompt")); priceAsPercentageValueCurrencyBox = priceAsPercentageTuple.first; marketBasedPriceInfoInputTextField = priceAsPercentageTuple.second; marketBasedPriceTextField = marketBasedPriceInfoInputTextField.getInputTextField(); editOfferElements.add(marketBasedPriceTextField); marketBasedPriceLabel = priceAsPercentageTuple.third; editOfferElements.add(marketBasedPriceLabel); Tuple2 priceAsPercentageInputBoxTuple = getTradeInputBox(priceAsPercentageValueCurrencyBox, model.getPercentagePriceDescription()); percentagePriceDescriptionLabel = priceAsPercentageInputBoxTuple.first; getSmallIconForLabel(MaterialDesignIcon.CHART_LINE, percentagePriceDescriptionLabel, "small-icon-label"); percentagePriceBox = priceAsPercentageInputBoxTuple.second; // = resultLabel = new AutoTooltipLabel("="); resultLabel.getStyleClass().add("opaque-icon-character"); // volume Tuple3 volumeValueCurrencyBoxTuple = getEditableValueBoxWithInfo(Res.get("createOffer.volume.prompt")); volumeValueCurrencyBox = volumeValueCurrencyBoxTuple.first; volumeInfoInputTextField = volumeValueCurrencyBoxTuple.second; volumeTextField = volumeInfoInputTextField.getInputTextField(); editOfferElements.add(volumeTextField); volumeCurrencyLabel = volumeValueCurrencyBoxTuple.third; editOfferElements.add(volumeCurrencyLabel); Tuple2 volumeInputBoxTuple = getTradeInputBox(volumeValueCurrencyBox, model.volumeDescriptionLabel.get()); volumeDescriptionLabel = volumeInputBoxTuple.first; editOfferElements.add(volumeDescriptionLabel); VBox volumeBox = volumeInputBoxTuple.second; firstRowHBox = new HBox(); firstRowHBox.setSpacing(5); firstRowHBox.setAlignment(Pos.CENTER_LEFT); firstRowHBox.getChildren().addAll(amountBox, xLabel, percentagePriceBox, resultLabel, volumeBox); GridPane.setColumnSpan(firstRowHBox, 2); GridPane.setRowIndex(firstRowHBox, gridRow); GridPane.setMargin(firstRowHBox, new Insets(40 + heightAdjustment, 10, 0, 0)); gridPane.getChildren().add(firstRowHBox); } private void updatePriceToggleButtons(boolean fixedPriceSelected) { int marketPriceAvailable = model.marketPriceAvailableProperty.get(); fixedPriceSelected = fixedPriceSelected || (marketPriceAvailable == 0); model.getDataModel().setUseMarketBasedPrice(marketPriceAvailable == 1 && !fixedPriceSelected); percentagePriceBox.setDisable(fixedPriceSelected); fixedPriceBox.setDisable(!fixedPriceSelected); if (fixedPriceSelected) { firstRowHBox.getChildren().remove(percentagePriceBox); secondRowHBox.getChildren().remove(fixedPriceBox); if (!firstRowHBox.getChildren().contains(fixedPriceBox)) firstRowHBox.getChildren().add(2, fixedPriceBox); if (!secondRowHBox.getChildren().contains(percentagePriceBox)) secondRowHBox.getChildren().add(2, percentagePriceBox); model.triggerPrice.set(""); model.onTriggerPriceTextFieldChanged(); } else { firstRowHBox.getChildren().remove(fixedPriceBox); secondRowHBox.getChildren().remove(percentagePriceBox); if (!firstRowHBox.getChildren().contains(percentagePriceBox)) firstRowHBox.getChildren().add(2, percentagePriceBox); if (!secondRowHBox.getChildren().contains(fixedPriceBox)) secondRowHBox.getChildren().add(2, fixedPriceBox); } triggerPriceVBox.setVisible(!fixedPriceSelected); model.onFixPriceToggleChange(fixedPriceSelected); } private void addSecondRow() { // price as traditional currency Tuple3 priceValueCurrencyBoxTuple = getEditableValueBox( Res.get("createOffer.price.prompt")); priceValueCurrencyBox = priceValueCurrencyBoxTuple.first; fixedPriceTextField = priceValueCurrencyBoxTuple.second; editOfferElements.add(fixedPriceTextField); priceCurrencyLabel = priceValueCurrencyBoxTuple.third; editOfferElements.add(priceCurrencyLabel); Tuple2 priceInputBoxTuple = getTradeInputBox(priceValueCurrencyBox, ""); priceDescriptionLabel = priceInputBoxTuple.first; getSmallIconForLabel(MaterialDesignIcon.LOCK, priceDescriptionLabel, "small-icon-label"); editOfferElements.add(priceDescriptionLabel); fixedPriceBox = priceInputBoxTuple.second; marketBasedPriceTextField.setPromptText(Res.get("shared.enterPercentageValue")); marketBasedPriceLabel.setText("%"); Tuple3 amountValueCurrencyBoxTuple = getEditableValueBox(Res.get("createOffer.amount.prompt")); minAmountValueCurrencyBox = amountValueCurrencyBoxTuple.first; minAmountTextField = amountValueCurrencyBoxTuple.second; editOfferElements.add(minAmountTextField); minAmountBtcLabel = amountValueCurrencyBoxTuple.third; editOfferElements.add(minAmountBtcLabel); Tuple2 amountInputBoxTuple = getTradeInputBox(minAmountValueCurrencyBox, Res.get("createOffer.amountPriceBox.minAmountDescription")); fakeXLabel = new Label(); fakeXIcon = getIconForLabel(MaterialDesignIcon.CLOSE, "2em", fakeXLabel); fakeXLabel.getStyleClass().add("opaque-icon-character"); fakeXLabel.setVisible(false); // we just use it to get the same layout as the upper row // Fixed/Percentage toggle priceTypeToggleButton = getIconButton(MaterialDesignIcon.SWAP_VERTICAL); editOfferElements.add(priceTypeToggleButton); HBox.setMargin(priceTypeToggleButton, new Insets(25, 1.5, 0, 0)); priceTypeToggleButton.setOnAction((actionEvent) -> updatePriceToggleButtons(model.getDataModel().getUseMarketBasedPrice().getValue())); // triggerPrice Tuple3 triggerPriceTuple3 = getEditableValueBoxWithInfo(Res.get("createOffer.triggerPrice.prompt")); triggerPriceHBox = triggerPriceTuple3.first; triggerPriceInfoInputTextField = triggerPriceTuple3.second; editOfferElements.add(triggerPriceInfoInputTextField); triggerPriceInputTextField = triggerPriceInfoInputTextField.getInputTextField(); triggerPriceCurrencyLabel = triggerPriceTuple3.third; editOfferElements.add(triggerPriceCurrencyLabel); Tuple2 triggerPriceTuple2 = getTradeInputBox(triggerPriceHBox, model.getTriggerPriceDescriptionLabel()); triggerPriceDescriptionLabel = triggerPriceTuple2.first; triggerPriceDescriptionLabel.setPrefWidth(290); triggerPriceVBox = triggerPriceTuple2.second; secondRowHBox = new HBox(); secondRowHBox.setSpacing(5); secondRowHBox.setAlignment(Pos.CENTER_LEFT); secondRowHBox.getChildren().addAll(amountInputBoxTuple.second, fakeXLabel, fixedPriceBox, priceTypeToggleButton, triggerPriceVBox); GridPane.setColumnSpan(secondRowHBox, 2); GridPane.setRowIndex(secondRowHBox, ++gridRow); GridPane.setColumnIndex(secondRowHBox, 0); GridPane.setMargin(secondRowHBox, new Insets(0, 10, 10, 0)); gridPane.getChildren().add(secondRowHBox); } private VBox getTradeFeeFieldsBox() { tradeFeeInXmrLabel = new Label(); tradeFeeInXmrLabel.setMouseTransparent(true); tradeFeeInXmrLabel.setId("trade-fee-textfield"); VBox vBox = new VBox(); vBox.setSpacing(6); vBox.setMaxWidth(300); vBox.setAlignment(Pos.CENTER_LEFT); vBox.getChildren().addAll(tradeFeeInXmrLabel); HBox hBox = new HBox(); hBox.getChildren().addAll(vBox); hBox.setMinHeight(47); hBox.setMaxHeight(hBox.getMinHeight()); HBox.setHgrow(vBox, Priority.ALWAYS); final Tuple2 tradeInputBox = getTradeInputBox(hBox, Res.get("createOffer.tradeFee.description")); tradeFeeDescriptionLabel = tradeInputBox.first; return tradeInputBox.second; } /////////////////////////////////////////////////////////////////////////////////////////// // PayInfo /////////////////////////////////////////////////////////////////////////////////////////// private GridPane createInfoPopover() { GridPane infoGridPane = new GridPane(); infoGridPane.setHgap(5); infoGridPane.setVgap(5); infoGridPane.setPadding(new Insets(10, 10, 10, 10)); int i = 0; if (model.isSellOffer()) { addPayInfoEntry(infoGridPane, i++, Res.getWithCol("shared.tradeAmount"), model.getTradeAmount()); } addPayInfoEntry(infoGridPane, i++, Res.getWithCol("shared.yourSecurityDeposit"), model.getSecurityDepositInfo()); addPayInfoEntry(infoGridPane, i++, Res.getWithCol("createOffer.fundsBox.offerFee"), model.getTradeFee()); Separator separator = new Separator(); separator.setOrientation(Orientation.HORIZONTAL); separator.getStyleClass().add("offer-separator"); GridPane.setConstraints(separator, 1, i++); infoGridPane.getChildren().add(separator); addPayInfoEntry(infoGridPane, i, Res.getWithCol("shared.total"), model.getTotalToPayInfo()); return infoGridPane; } /////////////////////////////////////////////////////////////////////////////////////////// // Helpers /////////////////////////////////////////////////////////////////////////////////////////// private ObservableList getPaymentAccounts() { return filterPaymentAccounts(model.getDataModel().getPaymentAccounts()); } /////////////////////////////////////////////////////////////////////////////////////////// // Abstract Methods /////////////////////////////////////////////////////////////////////////////////////////// protected abstract ObservableList filterPaymentAccounts(ObservableList paymentAccounts); } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.offer; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.ThreadUtils; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.common.util.MathUtils; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.Price; import haveno.core.monetary.TraditionalMoney; import haveno.core.monetary.Volume; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OfferRestrictions; import haveno.core.offer.OfferUtil; import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.PaymentAccount; import haveno.core.payment.validation.FiatVolumeValidator; import haveno.core.payment.validation.SecurityDepositValidator; import haveno.core.payment.validation.XmrValidator; import haveno.core.provider.price.MarketPrice; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.HavenoUtils; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.ParsingUtils; import haveno.core.util.PriceUtil; import haveno.core.util.VolumeUtil; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.coin.CoinUtil; import haveno.core.util.validation.AmountValidator4Decimals; import haveno.core.util.validation.AmountValidator8Decimals; import haveno.core.util.validation.InputValidator; import haveno.core.util.validation.InputValidator.ValidationResult; import haveno.core.util.validation.MonetaryValidator; import haveno.core.xmr.wallet.Restrictions; import haveno.desktop.Navigation; import haveno.desktop.common.model.ActivatableWithDataModel; import haveno.desktop.main.MainView; import haveno.desktop.main.funds.FundsView; import haveno.desktop.main.funds.deposit.DepositView; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.settings.SettingsView; import haveno.desktop.main.settings.preferences.PreferencesView; import haveno.desktop.util.DisplayUtils; import haveno.desktop.util.GUIUtil; import java.math.BigInteger; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.Optional; import java.util.concurrent.TimeUnit; import static javafx.beans.binding.Bindings.createStringBinding; import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import javafx.scene.control.ComboBox; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.util.Callback; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.Coin; @Slf4j public abstract class MutableOfferViewModel extends ActivatableWithDataModel { private final XmrValidator xmrValidator; protected final SecurityDepositValidator securityDepositValidator; protected final PriceFeedService priceFeedService; private final AccountAgeWitnessService accountAgeWitnessService; private final Navigation navigation; private final Preferences preferences; protected final CoinFormatter xmrFormatter; private final FiatVolumeValidator fiatVolumeValidator; private final AmountValidator4Decimals amountValidator4Decimals; private final AmountValidator8Decimals amountValidator8Decimals; protected final OfferUtil offerUtil; private String amountDescription; private String addressAsString; private final String paymentLabel; private boolean createOfferInProgress; public boolean createOfferCanceled; public final StringProperty amount = new SimpleStringProperty(); public final StringProperty minAmount = new SimpleStringProperty(); protected final StringProperty securityDeposit = new SimpleStringProperty(); final StringProperty securityDepositInXMR = new SimpleStringProperty(); final StringProperty securityDepositLabel = new SimpleStringProperty(); // Price in the viewModel is always dependent on fiat/crypto: Fiat Fiat/BTC, for cryptos we use inverted price. // The domain (dataModel) uses always the same price model (otherCurrencyBTC) // If we would change the price representation in the domain we would not be backward compatible public final StringProperty price = new SimpleStringProperty(); public final StringProperty triggerPrice = new SimpleStringProperty(""); public final BooleanProperty reserveExactAmount = new SimpleBooleanProperty(true); final StringProperty tradeFee = new SimpleStringProperty(); final StringProperty tradeFeeInXmrWithFiat = new SimpleStringProperty(); final StringProperty tradeFeeCurrencyCode = new SimpleStringProperty(); final StringProperty tradeFeeDescription = new SimpleStringProperty(); final BooleanProperty isTradeFeeVisible = new SimpleBooleanProperty(false); // Positive % value means always a better price form the maker's perspective: // Buyer (with fiat): lower price as market // Buyer (with crypto): higher (display) price as market (display price is inverted) public final StringProperty marketPriceMargin = new SimpleStringProperty(); public final StringProperty volume = new SimpleStringProperty(); final StringProperty volumeDescriptionLabel = new SimpleStringProperty(); final StringProperty volumePromptLabel = new SimpleStringProperty(); final StringProperty tradeAmount = new SimpleStringProperty(); final StringProperty totalToPay = new SimpleStringProperty(); final StringProperty errorMessage = new SimpleStringProperty(); final StringProperty tradeCurrencyCode = new SimpleStringProperty(); final StringProperty waitingForFundsText = new SimpleStringProperty(""); final StringProperty triggerPriceDescription = new SimpleStringProperty(""); final StringProperty percentagePriceDescription = new SimpleStringProperty(""); final StringProperty extraInfo = new SimpleStringProperty(""); final BooleanProperty isPlaceOfferButtonDisabled = new SimpleBooleanProperty(true); final BooleanProperty cancelButtonDisabled = new SimpleBooleanProperty(); public final BooleanProperty isNextButtonDisabled = new SimpleBooleanProperty(true); final BooleanProperty placeOfferCompleted = new SimpleBooleanProperty(); final BooleanProperty showPayFundsScreenDisplayed = new SimpleBooleanProperty(); private final BooleanProperty showTransactionPublishedScreen = new SimpleBooleanProperty(); final BooleanProperty isWaitingForFunds = new SimpleBooleanProperty(); final BooleanProperty isMinSecurityDeposit = new SimpleBooleanProperty(); final ObjectProperty amountValidationResult = new SimpleObjectProperty<>(); final ObjectProperty minAmountValidationResult = new SimpleObjectProperty<>(); final ObjectProperty priceValidationResult = new SimpleObjectProperty<>(); final ObjectProperty triggerPriceValidationResult = new SimpleObjectProperty<>(new InputValidator.ValidationResult(true)); final ObjectProperty volumeValidationResult = new SimpleObjectProperty<>(); final ObjectProperty securityDepositValidationResult = new SimpleObjectProperty<>(); final ObjectProperty extraInfoValidationResult = new SimpleObjectProperty<>(); private ChangeListener amountStringListener; private ChangeListener minAmountStringListener; private ChangeListener priceStringListener, marketPriceMarginStringListener; private ChangeListener volumeStringListener; private ChangeListener securityDepositStringListener; private ChangeListener extraInfoStringListener; private ChangeListener amountListener; private ChangeListener minAmountListener; private ChangeListener priceListener; private ChangeListener volumeListener; private ChangeListener securityDepositAsDoubleListener; private ChangeListener buyerAsTakerWithoutDepositListener; private ChangeListener isWalletFundedListener; private ChangeListener errorMessageListener; protected Offer offer; private boolean inputIsMarketBasedPrice; private ChangeListener useMarketBasedPriceListener; private boolean ignorePriceStringListener, ignoreVolumeStringListener, ignoreAmountStringListener, ignoreSecurityDepositStringListener; private MarketPrice marketPrice; final IntegerProperty marketPriceAvailableProperty = new SimpleIntegerProperty(-1); private ChangeListener currenciesUpdateListener; protected boolean syncMinAmountWithAmount = true; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject public MutableOfferViewModel(M dataModel, FiatVolumeValidator fiatVolumeValidator, AmountValidator4Decimals amountValidator4Decimals, AmountValidator8Decimals amountValidator8Decimals, XmrValidator xmrValidator, SecurityDepositValidator securityDepositValidator, PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation, Preferences preferences, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter xmrFormatter, OfferUtil offerUtil) { super(dataModel); this.fiatVolumeValidator = fiatVolumeValidator; this.amountValidator4Decimals = amountValidator4Decimals; this.amountValidator8Decimals = amountValidator8Decimals; this.xmrValidator = xmrValidator; this.securityDepositValidator = securityDepositValidator; this.priceFeedService = priceFeedService; this.accountAgeWitnessService = accountAgeWitnessService; this.navigation = navigation; this.preferences = preferences; this.xmrFormatter = xmrFormatter; this.offerUtil = offerUtil; paymentLabel = Res.get("createOffer.fundsBox.paymentLabel", dataModel.shortOfferId); createListeners(); } @Override public void activate() { if (DevEnv.isDevMode()) { UserThread.runAfter(() -> { amount.set("0.001"); price.set("210000"); minAmount.set(amount.get()); onFocusOutPriceAsPercentageTextField(true, false); applyMakerFee(); setAmountToModel(); setMinAmountToModel(); setPriceToModel(); dataModel.calculateVolume(); dataModel.calculateTotalToPay(); updateButtonDisableState(); updateSpinnerInfo(); setExtraInfoToModel(); }, 100, TimeUnit.MILLISECONDS); } addBindings(); addListeners(); updateButtonDisableState(); updateMarketPriceAvailable(); } @Override protected void deactivate() { removeBindings(); removeListeners(); } private void addBindings() { if (dataModel.getDirection() == OfferDirection.BUY) { volumeDescriptionLabel.bind(createStringBinding( () -> Res.get("createOffer.amountPriceBox.buy.volumeDescription", dataModel.getTradeCurrencyCode().get()), dataModel.getTradeCurrencyCode())); } else { volumeDescriptionLabel.bind(createStringBinding( () -> Res.get("createOffer.amountPriceBox.sell.volumeDescription", dataModel.getTradeCurrencyCode().get()), dataModel.getTradeCurrencyCode())); } volumePromptLabel.bind(createStringBinding( () -> Res.get("createOffer.volume.prompt", CurrencyUtil.getCurrencyCodeBase(dataModel.getTradeCurrencyCode().get())), dataModel.getTradeCurrencyCode())); totalToPay.bind(createStringBinding(() -> HavenoUtils.formatXmr(dataModel.totalToPayAsProperty().get(), true), dataModel.totalToPayAsProperty())); tradeAmount.bind(createStringBinding(() -> HavenoUtils.formatXmr(dataModel.getAmount().get(), true), dataModel.getAmount())); tradeCurrencyCode.bind(dataModel.getTradeCurrencyCode()); triggerPriceDescription.bind(createStringBinding(this::getTriggerPriceDescriptionLabel, dataModel.getTradeCurrencyCode())); percentagePriceDescription.bind(createStringBinding(this::getPercentagePriceDescription, dataModel.getTradeCurrencyCode())); } private void removeBindings() { totalToPay.unbind(); tradeAmount.unbind(); tradeCurrencyCode.unbind(); volumeDescriptionLabel.unbind(); volumePromptLabel.unbind(); triggerPriceDescription.unbind(); percentagePriceDescription.unbind(); } private void createListeners() { amountStringListener = (ov, oldValue, newValue) -> { if (!ignoreAmountStringListener) { if (isXmrInputValid(newValue).isValid) { setAmountToModel(); dataModel.calculateVolume(); dataModel.calculateTotalToPay(); } updateSecurityDeposit(); updateButtonDisableState(); } }; minAmountStringListener = (ov, oldValue, newValue) -> { if (isXmrInputValid(newValue).isValid) setMinAmountToModel(); updateButtonDisableState(); }; priceStringListener = (ov, oldValue, newValue) -> { updateMarketPriceAvailable(); if (!ignorePriceStringListener) { if (isPriceInputValid(newValue).isValid) { setPriceToModel(); dataModel.calculateVolume(); dataModel.calculateTotalToPay(); if (!inputIsMarketBasedPrice) { if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) { double marketPriceAsDouble = marketPrice.getPrice(); try { double priceAsDouble = ParsingUtils.parseNumberStringToDouble(price.get()); double relation = priceAsDouble / marketPriceAsDouble; final OfferDirection compareDirection = OfferDirection.BUY; double percentage = dataModel.getDirection() == compareDirection ? 1 - relation : relation - 1; percentage = MathUtils.roundDouble(percentage, 4); dataModel.setMarketPriceMarginPct(percentage); marketPriceMargin.set(FormattingUtils.formatToPercent(percentage)); applyMakerFee(); } catch (NumberFormatException t) { marketPriceMargin.set(""); new Popup().warning(Res.get("validation.NaN")).show(); } } else { log.debug("We don't have a market price. We use the static price instead."); } } } } updateButtonDisableState(); }; marketPriceMarginStringListener = (ov, oldValue, newValue) -> { if (inputIsMarketBasedPrice) { try { if (!newValue.isEmpty() && !newValue.equals("-")) { double percentage = ParsingUtils.parsePercentStringToDouble(newValue); if (percentage >= 1 || percentage <= -1) { new Popup().warning(Res.get("popup.warning.tooLargePercentageValue") + "\n" + Res.get("popup.warning.examplePercentageValue")) .show(); } else { final String currencyCode = dataModel.getTradeCurrencyCode().get(); MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) { percentage = MathUtils.roundDouble(percentage, 4); double marketPriceAsDouble = marketPrice.getPrice(); final OfferDirection compareDirection = OfferDirection.BUY; double factor = dataModel.getDirection() == compareDirection ? 1 - percentage : 1 + percentage; double targetPrice = marketPriceAsDouble * factor; int precision = CurrencyUtil.isTraditionalCurrency(currencyCode) ? TraditionalMoney.SMALLEST_UNIT_EXPONENT : CryptoMoney.SMALLEST_UNIT_EXPONENT; // protect from triggering unwanted updates ignorePriceStringListener = true; price.set(FormattingUtils.formatRoundedDoubleWithPrecision(targetPrice, precision)); ignorePriceStringListener = false; setPriceToModel(); dataModel.setMarketPriceMarginPct(percentage); dataModel.calculateVolume(); dataModel.calculateTotalToPay(); updateButtonDisableState(); applyMakerFee(); } else { marketPriceMargin.set(""); String id = "showNoPriceFeedAvailablePopup"; if (preferences.showAgain(id)) { new Popup().warning(Res.get("popup.warning.noPriceFeedAvailable")) .dontShowAgainId(id) .show(); } } } } } catch (NumberFormatException t) { log.error(t.toString()); t.printStackTrace(); new Popup().warning(Res.get("validation.NaN")).show(); } catch (Throwable t) { log.error(t.toString()); t.printStackTrace(); new Popup().warning(Res.get("validation.inputError", t.toString())).show(); } } }; useMarketBasedPriceListener = (observable, oldValue, newValue) -> { if (newValue) priceValidationResult.set(new InputValidator.ValidationResult(true)); }; volumeStringListener = (ov, oldValue, newValue) -> { if (!ignoreVolumeStringListener) { if (isVolumeInputValid(newValue).isValid) { setVolumeToModel(); setPriceToModel(); dataModel.calculateAmount(); dataModel.calculateTotalToPay(); } updateButtonDisableState(); } }; securityDepositStringListener = (ov, oldValue, newValue) -> { if (!ignoreSecurityDepositStringListener) { if (securityDepositValidator.validate(newValue).isValid) { setSecurityDepositToModel(); dataModel.calculateTotalToPay(); } updateButtonDisableState(); } }; amountListener = (ov, oldValue, newValue) -> { if (newValue != null) { amount.set(HavenoUtils.formatXmr(newValue)); securityDepositInXMR.set(HavenoUtils.formatXmr(dataModel.getSecurityDeposit(), true)); } else { amount.set(""); securityDepositInXMR.set(""); } applyMakerFee(); }; minAmountListener = (ov, oldValue, newValue) -> { if (newValue != null) minAmount.set(HavenoUtils.formatXmr(newValue)); else minAmount.set(""); }; priceListener = (ov, oldValue, newValue) -> { ignorePriceStringListener = true; if (newValue != null) price.set(FormattingUtils.formatPrice(newValue)); else price.set(""); ignorePriceStringListener = false; applyMakerFee(); }; volumeListener = (ov, oldValue, newValue) -> { ignoreVolumeStringListener = true; if (newValue != null) volume.set(VolumeUtil.formatVolume(newValue)); else volume.set(""); ignoreVolumeStringListener = false; applyMakerFee(); }; securityDepositAsDoubleListener = (ov, oldValue, newValue) -> { if (newValue != null) { securityDeposit.set(FormattingUtils.formatToPercent((double) newValue)); if (dataModel.getAmount().get() != null) { securityDepositInXMR.set(HavenoUtils.formatXmr(dataModel.getSecurityDeposit(), true)); } updateSecurityDeposit(); } else { securityDeposit.set(""); securityDepositInXMR.set(""); } }; buyerAsTakerWithoutDepositListener = (ov, oldValue, newValue) -> { if (dataModel.paymentAccount != null) xmrValidator.setMaxValue(dataModel.paymentAccount.getPaymentMethod().getMaxTradeLimit(dataModel.getTradeCurrencyCode().get())); xmrValidator.setMaxTradeLimit(dataModel.getMaxTradeLimit()); xmrValidator.setMinValue(dataModel.getMinTradeLimit()); if (amount.get() != null) amountValidationResult.set(isXmrInputValid(amount.get())); updateSecurityDeposit(); setSecurityDepositToModel(); onFocusOutSecurityDepositTextField(true, false); // refresh security deposit field applyMakerFee(); dataModel.calculateTotalToPay(); updateButtonDisableState(); }; extraInfoStringListener = (ov, oldValue, newValue) -> { if (newValue != null) { extraInfo.set(newValue.trim()); UserThread.execute(() -> onExtraInfoTextAreaChanged()); } }; isWalletFundedListener = (ov, oldValue, newValue) -> updateButtonDisableState(); /* feeFromFundingTxListener = (ov, oldValue, newValue) -> { updateButtonDisableState(); };*/ currenciesUpdateListener = (observable, oldValue, newValue) -> { updateMarketPriceAvailable(); updateButtonDisableState(); }; } private void applyMakerFee() { tradeFeeCurrencyCode.set(Res.getBaseCurrencyCode()); tradeFeeDescription.set(Res.get("createOffer.tradeFee.descriptionXMROnly")); BigInteger makerFee = dataModel.getMaxMakerFee(); if (makerFee == null) { return; } isTradeFeeVisible.setValue(true); tradeFee.set(HavenoUtils.formatXmr(makerFee)); tradeFeeInXmrWithFiat.set(OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, dataModel.getMaxMakerFee(), xmrFormatter)); } private void updateMarketPriceAvailable() { marketPrice = priceFeedService.getMarketPrice(dataModel.getTradeCurrencyCode().get()); marketPriceAvailableProperty.set(marketPrice == null || !marketPrice.isExternallyProvidedPrice() ? 0 : 1); dataModel.setMarketPriceAvailable(marketPrice != null && marketPrice.isExternallyProvidedPrice()); } private void addListeners() { // Bidirectional bindings are used for all input fields: amount, price, volume and minAmount // We do volume/amount calculation during input, so user has immediate feedback amount.addListener(amountStringListener); minAmount.addListener(minAmountStringListener); price.addListener(priceStringListener); marketPriceMargin.addListener(marketPriceMarginStringListener); dataModel.getUseMarketBasedPrice().addListener(useMarketBasedPriceListener); volume.addListener(volumeStringListener); securityDeposit.addListener(securityDepositStringListener); extraInfo.addListener(extraInfoStringListener); // Binding with Bindings.createObjectBinding does not work because of bi-directional binding dataModel.getAmount().addListener(amountListener); dataModel.getMinAmount().addListener(minAmountListener); dataModel.getPrice().addListener(priceListener); dataModel.getVolume().addListener(volumeListener); dataModel.getSecurityDepositPct().addListener(securityDepositAsDoubleListener); dataModel.getBuyerAsTakerWithoutDeposit().addListener(buyerAsTakerWithoutDepositListener); dataModel.getExtraInfo().addListener(extraInfoStringListener); // dataModel.feeFromFundingTxProperty.addListener(feeFromFundingTxListener); dataModel.getIsXmrWalletFunded().addListener(isWalletFundedListener); priceFeedService.updateCounterProperty().addListener(currenciesUpdateListener); } private void removeListeners() { amount.removeListener(amountStringListener); minAmount.removeListener(minAmountStringListener); price.removeListener(priceStringListener); marketPriceMargin.removeListener(marketPriceMarginStringListener); dataModel.getUseMarketBasedPrice().removeListener(useMarketBasedPriceListener); volume.removeListener(volumeStringListener); securityDeposit.removeListener(securityDepositStringListener); extraInfo.removeListener(extraInfoStringListener); // Binding with Bindings.createObjectBinding does not work because of bi-directional binding dataModel.getAmount().removeListener(amountListener); dataModel.getMinAmount().removeListener(minAmountListener); dataModel.getPrice().removeListener(priceListener); dataModel.getVolume().removeListener(volumeListener); dataModel.getSecurityDepositPct().removeListener(securityDepositAsDoubleListener); dataModel.getBuyerAsTakerWithoutDeposit().removeListener(buyerAsTakerWithoutDepositListener); dataModel.getExtraInfo().removeListener(extraInfoStringListener); //dataModel.feeFromFundingTxProperty.removeListener(feeFromFundingTxListener); dataModel.getIsXmrWalletFunded().removeListener(isWalletFundedListener); if (offer != null && errorMessageListener != null) offer.getErrorMessageProperty().removeListener(errorMessageListener); priceFeedService.updateCounterProperty().removeListener(currenciesUpdateListener); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency, boolean initAddressEntry) { boolean result = dataModel.initWithData(direction, tradeCurrency, initAddressEntry); if (dataModel.getAddressEntry() != null) { addressAsString = dataModel.getAddressEntry().getAddressString(); } if (dataModel.paymentAccount != null) xmrValidator.setMaxValue(dataModel.paymentAccount.getPaymentMethod().getMaxTradeLimit(dataModel.getTradeCurrencyCode().get())); xmrValidator.setMaxTradeLimit(dataModel.getMaxTradeLimit()); xmrValidator.setMinValue(dataModel.getMinTradeLimit()); final boolean isBuy = dataModel.getDirection() == OfferDirection.BUY; amountDescription = Res.get("createOffer.amountPriceBox.amountDescription", isBuy ? Res.get("shared.buy") : Res.get("shared.sell")); securityDepositValidator.setPaymentAccount(dataModel.paymentAccount); validateAndSetSecurityDepositToModel(); securityDeposit.set(FormattingUtils.formatToPercent(dataModel.getSecurityDepositPct().get())); securityDepositLabel.set(getSecurityDepositLabel()); applyMakerFee(); return result; } /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// void onPlaceOffer(Offer offer, Runnable resultHandler) { ThreadUtils.execute(() -> { errorMessage.set(null); createOfferInProgress = true; createOfferCanceled = false; dataModel.onPlaceOffer(offer, transaction -> { createOfferInProgress = false; resultHandler.run(); if (!createOfferCanceled) placeOfferCompleted.set(true); errorMessage.set(null); }, errMessage -> { createOfferInProgress = false; if (offer.getState() == Offer.State.OFFER_FEE_RESERVED) errorMessage.set(errMessage + Res.get("createOffer.errorInfo")); else errorMessage.set(errMessage); UserThread.execute(() -> { updateButtonDisableState(); updateSpinnerInfo(); resultHandler.run(); }); }); UserThread.execute(() -> { updateButtonDisableState(); updateSpinnerInfo(); }); }, getClass().getSimpleName()); } public void onCancelOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { log.info("Canceling posting offer {}", offer.getId()); createOfferCanceled = true; OpenOfferManager openOfferManager = HavenoUtils.openOfferManager; Optional openOffer = openOfferManager.getOpenOffer(offer.getId()); if (openOffer.isPresent()) { openOfferManager.cancelOpenOffer(openOffer.get(), () -> { UserThread.execute(() -> { updateButtonDisableState(); updateSpinnerInfo(); }); if (resultHandler != null) resultHandler.handleResult(); }, errorMessage -> { UserThread.execute(() -> { updateButtonDisableState(); updateSpinnerInfo(); if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage(errorMessage); }); }); } else { if (resultHandler != null) resultHandler.handleResult(); return; } } public void onPaymentAccountSelected(PaymentAccount paymentAccount) { dataModel.onPaymentAccountSelected(paymentAccount); if (amount.get() != null) amountValidationResult.set(isXmrInputValid(amount.get())); xmrValidator.setMaxValue(dataModel.paymentAccount.getPaymentMethod().getMaxTradeLimit(dataModel.getTradeCurrencyCode().get())); xmrValidator.setMaxTradeLimit(dataModel.getMaxTradeLimit()); securityDepositValidator.setPaymentAccount(paymentAccount); } public void onCurrencySelected(TradeCurrency tradeCurrency) { dataModel.onCurrencySelected(tradeCurrency); marketPrice = priceFeedService.getMarketPrice(dataModel.getTradeCurrencyCode().get()); marketPriceAvailableProperty.set(marketPrice == null || !marketPrice.isExternallyProvidedPrice() ? 0 : 1); updateButtonDisableState(); } void onShowPayFundsScreen(Runnable actionHandler) { actionHandler.run(); showPayFundsScreenDisplayed.set(true); updateSpinnerInfo(); } void fundFromSavingsWallet() { dataModel.fundFromSavingsWallet(); if (dataModel.getIsXmrWalletFunded().get()) { updateButtonDisableState(); } else { new Popup().warning(Res.get("shared.notEnoughFunds", HavenoUtils.formatXmr(dataModel.totalToPayAsProperty().get(), true), HavenoUtils.formatXmr(dataModel.getTotalUnallocatedBalance(), true))) .actionButtonTextWithGoTo("funds.tab.deposit") .onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, DepositView.class)) .show(); } } /////////////////////////////////////////////////////////////////////////////////////////// // Handle focus /////////////////////////////////////////////////////////////////////////////////////////// // On focus out we do validation and apply the data to the model void onFocusOutAmountTextField(boolean oldValue, boolean newValue) { if (oldValue && !newValue) { InputValidator.ValidationResult result = isXmrInputValid(amount.get()); amountValidationResult.set(result); if (result.isValid) { setAmountToModel(); ignoreAmountStringListener = true; amount.set(HavenoUtils.formatXmr(dataModel.getAmount().get())); ignoreAmountStringListener = false; dataModel.calculateVolume(); if (!dataModel.isMinAmountLessOrEqualAmount()) minAmount.set(amount.get()); else amountValidationResult.set(result); if (minAmount.get() != null) minAmountValidationResult.set(isXmrInputValid(minAmount.get())); } else if (amount.get() != null && !amount.get().isEmpty() && xmrValidator.getMaxTradeLimit() != null && xmrValidator.getMaxTradeLimit().longValueExact() == OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT.longValueExact()) { // TODO: tolerated small amount will only equal max trade limit for riskiest payment methods, so that logic is not relevant? try { if (ParsingUtils.parseNumberStringToDouble(amount.get()) < HavenoUtils.atomicUnitsToXmr(dataModel.getMinTradeLimit())) { amountValidationResult.set(result); } else { amount.set(HavenoUtils.formatXmr(xmrValidator.getMaxTradeLimit())); boolean isBuy = dataModel.getDirection() == OfferDirection.BUY; boolean isSellerWithinReleaseWindow = !isBuy && HavenoUtils.isReleasedWithinDays(HavenoUtils.RELEASE_LIMIT_DAYS); if (isSellerWithinReleaseWindow) { // format release date plus days Date releaseDate = HavenoUtils.getReleaseDate(); Calendar c = Calendar.getInstance(); c.setTime(releaseDate); c.add(Calendar.DATE, HavenoUtils.RELEASE_LIMIT_DAYS); Date releaseDatePlusDays = c.getTime(); SimpleDateFormat formatter = new SimpleDateFormat("MMMM d, yyyy"); String releaseDatePlusDaysAsString = formatter.format(releaseDatePlusDays); // popup temporary restriction new Popup().information(Res.get("popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit", HavenoUtils.formatXmr(xmrValidator.getMaxTradeLimit(), true), releaseDatePlusDaysAsString, Res.get("offerbook.warning.newVersionAnnouncement"))) .width(900) .show(); } else { new Popup().information(Res.get(isBuy ? "popup.warning.tradeLimitDueAccountAgeRestriction.buyer" : "popup.warning.tradeLimitDueAccountAgeRestriction.seller", HavenoUtils.formatXmr(xmrValidator.getMaxTradeLimit(), true), Res.get("offerbook.warning.newVersionAnnouncement"))) .width(900) .show(); } } } catch (Exception e) { log.warn("Error while parsing amount on focus out: ", e); } } // trigger recalculation of the volume UserThread.execute(() -> { onFocusOutVolumeTextField(true, false); onFocusOutMinAmountTextField(true, false); }); if (marketPriceMargin.get() == null && amount.get() != null && volume.get() != null) { updateMarketPriceToManual(); } } } public void onFocusOutMinAmountTextField(boolean oldValue, boolean newValue) { if (oldValue && !newValue) { InputValidator.ValidationResult result = isXmrInputValid(minAmount.get()); minAmountValidationResult.set(result); if (result.isValid) { BigInteger minAmount = dataModel.getMinAmount().get(); syncMinAmountWithAmount = minAmount != null && minAmount.equals(dataModel.getAmount().get()); setMinAmountToModel(); dataModel.calculateMinVolume(); if (dataModel.getMinVolume().get() != null) { InputValidator.ValidationResult minVolumeResult = isVolumeInputValid( VolumeUtil.formatVolume(dataModel.getMinVolume().get())); volumeValidationResult.set(minVolumeResult); updateButtonDisableState(); } this.minAmount.set(HavenoUtils.formatXmr(minAmount)); if (!dataModel.isMinAmountLessOrEqualAmount()) { this.amount.set(this.minAmount.get()); } else { minAmountValidationResult.set(result); if (this.amount.get() != null) amountValidationResult.set(isXmrInputValid(this.amount.get())); } } else { syncMinAmountWithAmount = true; } // trigger recalculation of the security deposit UserThread.execute(() -> { onFocusOutSecurityDepositTextField(true, false); }); } } public void onFocusOutExtraInfoTextArea(boolean oldValue, boolean newValue) { if (oldValue && !newValue) { onExtraInfoTextAreaChanged(); } } public void onExtraInfoTextAreaChanged() { extraInfoValidationResult.set(getExtraInfoValidationResult()); updateButtonDisableState(); if (extraInfoValidationResult.get().isValid) { setExtraInfoToModel(); } } void onFocusOutTriggerPriceTextField(boolean oldValue, boolean newValue) { if (oldValue && !newValue) { onTriggerPriceTextFieldChanged(); } } public void onTriggerPriceTextFieldChanged() { String triggerPriceAsString = triggerPrice.get(); // Error field does not update if there was an error and then another different error // if not reset here. Not clear why... triggerPriceValidationResult.set(new InputValidator.ValidationResult(true)); String currencyCode = dataModel.getTradeCurrencyCode().get(); MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); InputValidator.ValidationResult result = PriceUtil.isTriggerPriceValid(triggerPriceAsString, marketPrice, dataModel.isSellOffer(), dataModel.getCurrencyCode() ); triggerPriceValidationResult.set(result); updateButtonDisableState(); if (result.isValid) { // In case of 0 or empty string we set the string to empty string and data value to 0 long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, dataModel.getCurrencyCode()); dataModel.setTriggerPrice(triggerPriceAsLong); if (dataModel.getTriggerPrice() == 0) { triggerPrice.set(""); } else { triggerPrice.set(PriceUtil.formatMarketPrice(dataModel.getTriggerPrice(), dataModel.getCurrencyCode())); } } } public void onReserveExactAmountCheckboxChanged() { dataModel.setReserveExactAmount(reserveExactAmount.get()); } void onFixPriceToggleChange(boolean fixedPriceSelected) { inputIsMarketBasedPrice = !fixedPriceSelected; updateButtonDisableState(); if (!fixedPriceSelected) { onTriggerPriceTextFieldChanged(); } } void onFocusOutPriceTextField(boolean oldValue, boolean newValue) { if (oldValue && !newValue) { InputValidator.ValidationResult result = isPriceInputValid(price.get()); priceValidationResult.set(result); if (result.isValid) { setPriceToModel(); ignorePriceStringListener = true; if (dataModel.getPrice().get() != null) price.set(FormattingUtils.formatPrice(dataModel.getPrice().get())); ignorePriceStringListener = false; dataModel.calculateVolume(); dataModel.calculateAmount(); applyMakerFee(); } // We want to trigger a recalculation of the volume and minAmount UserThread.execute(() -> { onFocusOutVolumeTextField(true, false); triggerFocusOutOnAmountFields(); }); } } public void triggerFocusOutOnAmountFields() { onFocusOutAmountTextField(true, false); onFocusOutMinAmountTextField(true, false); } public void onFocusOutPriceAsPercentageTextField(boolean oldValue, boolean newValue) { inputIsMarketBasedPrice = !oldValue && newValue; if (oldValue && !newValue) { if (marketPriceMargin.get() == null) { // field wasn't set manually inputIsMarketBasedPrice = true; } marketPriceMargin.set(FormattingUtils.formatRoundedDoubleWithPrecision(dataModel.getMarketPriceMarginPct() * 100, 2)); } // We want to trigger a recalculation of the volume, as well as update trigger price validation UserThread.execute(() -> { onFocusOutVolumeTextField(true, false); onTriggerPriceTextFieldChanged(); }); } void onFocusOutVolumeTextField(boolean oldValue, boolean newValue) { if (oldValue && !newValue) { InputValidator.ValidationResult result = isVolumeInputValid(volume.get()); volumeValidationResult.set(result); if (result.isValid) { setVolumeToModel(); ignoreVolumeStringListener = true; Volume volume = dataModel.getVolume().get(); if (volume != null) { volume = VolumeUtil.getAdjustedVolume(volume, dataModel.getPaymentAccount().getPaymentMethod().getId()); this.volume.set(VolumeUtil.formatVolume(volume)); } ignoreVolumeStringListener = false; dataModel.calculateAmount(); if (!dataModel.isMinAmountLessOrEqualAmount()) { minAmount.set(amount.getValue()); } else { if (amount.get() != null) amountValidationResult.set(isXmrInputValid(amount.get())); // We only check minAmountValidationResult if amountValidationResult is valid, otherwise we would get // triggered a close of the popup when the minAmountValidationResult is applied if (amountValidationResult.getValue() != null && amountValidationResult.getValue().isValid && minAmount.get() != null) minAmountValidationResult.set(isXmrInputValid(minAmount.get())); } } if (marketPriceMargin.get() == null && amount.get() != null && volume.get() != null) { updateMarketPriceToManual(); } // trigger recalculation of security deposit UserThread.execute(() -> { onFocusOutSecurityDepositTextField(true, false); }); } } void onFocusOutSecurityDepositTextField(boolean oldValue, boolean newValue) { if (oldValue && !newValue && !isMinSecurityDeposit.get()) { InputValidator.ValidationResult result = securityDepositValidator.validate(securityDeposit.get()); securityDepositValidationResult.set(result); if (result.isValid) { double defaultSecurityDeposit = Restrictions.getDefaultSecurityDepositPct(); String key = "buyerSecurityDepositIsLowerAsDefault"; double depositAsDouble = ParsingUtils.parsePercentStringToDouble(securityDeposit.get()); if (preferences.showAgain(key) && depositAsDouble < defaultSecurityDeposit) { String postfix = dataModel.isBuyOffer() ? Res.get("createOffer.tooLowSecDeposit.makerIsBuyer") : Res.get("createOffer.tooLowSecDeposit.makerIsSeller"); new Popup() .warning(Res.get("createOffer.tooLowSecDeposit.warning", FormattingUtils.formatToPercentWithSymbol(defaultSecurityDeposit)) + "\n\n" + postfix) .width(800) .actionButtonText(Res.get("createOffer.resetToDefault")) .onAction(() -> { dataModel.setSecurityDepositPct(defaultSecurityDeposit); ignoreSecurityDepositStringListener = true; securityDeposit.set(FormattingUtils.formatToPercent(dataModel.getSecurityDepositPct().get())); ignoreSecurityDepositStringListener = false; }) .closeButtonText(Res.get("createOffer.useLowerValue")) .onClose(this::applySecurityDepositOnFocusOut) .dontShowAgainId(key) .show(); } else { applySecurityDepositOnFocusOut(); } } } } private void applySecurityDepositOnFocusOut() { setSecurityDepositToModel(); ignoreSecurityDepositStringListener = true; securityDeposit.set(FormattingUtils.formatToPercent(dataModel.getSecurityDepositPct().get())); ignoreSecurityDepositStringListener = false; } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// public boolean isPriceInRange() { if (marketPriceMargin.get() != null && !marketPriceMargin.get().isEmpty()) { if (Math.abs(ParsingUtils.parsePercentStringToDouble(marketPriceMargin.get())) > preferences.getMaxPriceDistanceInPercent()) { displayPriceOutOfRangePopup(); return false; } else { return true; } } else { return true; } } private void displayPriceOutOfRangePopup() { Popup popup = new Popup(); popup.warning(Res.get("createOffer.priceOutSideOfDeviation", FormattingUtils.formatToPercentWithSymbol(preferences.getMaxPriceDistanceInPercent()))) .actionButtonText(Res.get("createOffer.changePrice")) .onAction(popup::hide) .closeButtonTextWithGoTo("settings.tab.preferences") .onClose(() -> navigation.navigateTo(MainView.class, SettingsView.class, PreferencesView.class)) .show(); } CoinFormatter getXmrFormatter() { return xmrFormatter; } public boolean isShownAsBuyOffer() { return OfferViewUtil.isShownAsBuyOffer(dataModel.getDirection(), dataModel.getTradeCurrency()); } public boolean isSellOffer() { return dataModel.getDirection() == OfferDirection.SELL; } public TradeCurrency getTradeCurrency() { return dataModel.getTradeCurrency(); } public String getTradeAmount() { return OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, dataModel.getAmount().get(), xmrFormatter); } public String getSecurityDepositLabel() { return dataModel.buyerAsTakerWithoutDeposit.get() && dataModel.isSellOffer() ? Res.get("createOffer.myDeposit") : dataModel.isMinSecurityDeposit() ? Res.get("createOffer.minSecurityDepositUsed") : Preferences.USE_SYMMETRIC_SECURITY_DEPOSIT ? Res.get("createOffer.setDepositForBothTraders") : dataModel.isBuyOffer() ? Res.get("createOffer.setDepositAsBuyer") : Res.get("createOffer.setDeposit"); } public String getSecurityDepositPopOverLabel(String depositInXMR) { return dataModel.buyerAsTakerWithoutDeposit.get() && dataModel.isSellOffer() ? Res.get("createOffer.myDepositInfo", depositInXMR) : dataModel.isBuyOffer() ? Res.get("createOffer.securityDepositInfoAsBuyer", depositInXMR) : Res.get("createOffer.securityDepositInfo", depositInXMR); } public String getSecurityDepositInfo() { return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, dataModel.getSecurityDeposit(), dataModel.getAmount().get(), xmrFormatter ); } public String getSecurityDepositWithCode() { return HavenoUtils.formatXmr(dataModel.getSecurityDeposit(), true); } public String getTradeFee() { return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, dataModel.getMaxMakerFee(), dataModel.getAmount().get(), xmrFormatter); } public String getMakerFeePercentage() { final BigInteger makerFee = dataModel.getMaxMakerFee(); return GUIUtil.getPercentage(makerFee, dataModel.getAmount().get()); } public String getTotalToPayInfo() { return OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, dataModel.totalToPay.get(), xmrFormatter); } public String getFundsStructure() { String fundsStructure; fundsStructure = Res.get("createOffer.fundsBox.fundsStructure", getSecurityDepositWithCode(), getMakerFeePercentage()); return fundsStructure; } public PaymentAccount getPaymentAccount() { return dataModel.getPaymentAccount(); } public String getAmountDescription() { return amountDescription; } public String getAddressAsString() { return addressAsString; } public String getPaymentLabel() { return paymentLabel; } public Offer createAndGetOffer() { offer = dataModel.createAndGetOffer(); return offer; } public Callback, ListCell> getPaymentAccountListCellFactory( ComboBox paymentAccountsComboBox) { return GUIUtil.getPaymentAccountListCellFactory(paymentAccountsComboBox, accountAgeWitnessService); } public M getDataModel() { return dataModel; } String getTriggerPriceDescriptionLabel() { String details = dataModel.isBuyOffer() ? Res.get("account.notifications.marketAlert.message.msg.above") : Res.get("account.notifications.marketAlert.message.msg.below"); return Res.get("createOffer.triggerPrice.label", details); } String getPercentagePriceDescription() { return dataModel.isBuyOffer() ? Res.get("shared.belowInPercent") : Res.get("shared.aboveInPercent"); } /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// private void setAmountToModel() { if (amount.get() != null && !amount.get().isEmpty()) { BigInteger amount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.amount.get(), xmrFormatter)); Price price = dataModel.getPrice().get(); if (price != null && price.isPositive()) { amount = CoinUtil.getRoundedAmount(amount, price, dataModel.getMinTradeLimit(), dataModel.getMaxTradeLimit(), tradeCurrencyCode.get(), dataModel.getPaymentAccount().getPaymentMethod().getId()); } dataModel.setAmount(amount); if (syncMinAmountWithAmount || dataModel.getMinAmount().get() == null || dataModel.getMinAmount().get().equals(Coin.ZERO)) { minAmount.set(this.amount.get()); setMinAmountToModel(); } } else { dataModel.setAmount(null); } } private void setMinAmountToModel() { if (minAmount.get() != null && !minAmount.get().isEmpty()) { BigInteger minAmount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.minAmount.get(), xmrFormatter)); Price price = dataModel.getPrice().get(); if (price != null && price.isPositive()) { minAmount = CoinUtil.getRoundedAmount(minAmount, price, dataModel.getMinTradeLimit(), dataModel.getMaxTradeLimit(), tradeCurrencyCode.get(), dataModel.getPaymentAccount().getPaymentMethod().getId()); } dataModel.setMinAmount(minAmount); } else { dataModel.setMinAmount(null); } } private void setPriceToModel() { if (price.get() != null && !price.get().isEmpty()) { try { dataModel.setPrice(Price.parse(dataModel.getTradeCurrencyCode().get(), this.price.get())); } catch (Throwable t) { log.debug(t.getMessage()); } } else { dataModel.setPrice(null); } } private void setVolumeToModel() { if (volume.get() != null && !volume.get().isEmpty()) { try { dataModel.setVolume(Volume.parse(volume.get(), dataModel.getTradeCurrencyCode().get())); } catch (Throwable t) { log.debug(t.getMessage()); } } else { dataModel.setVolume(null); } } private void setSecurityDepositToModel() { if (securityDeposit.get() != null && !securityDeposit.get().isEmpty() && !isMinSecurityDeposit.get()) { dataModel.setSecurityDepositPct(ParsingUtils.parsePercentStringToDouble(securityDeposit.get())); } else { dataModel.setSecurityDepositPct(Restrictions.getDefaultSecurityDepositPct()); } } private void setExtraInfoToModel() { if (extraInfo.get() != null && !extraInfo.get().isEmpty()) { dataModel.setExtraInfo(extraInfo.get()); } else { dataModel.setExtraInfo(null); } } private void validateAndSetSecurityDepositToModel() { // If the security deposit in the model is not valid percent String value = FormattingUtils.formatToPercent(dataModel.getSecurityDepositPct().get()); if (!securityDepositValidator.validate(value).isValid) { dataModel.setSecurityDepositPct(Restrictions.getDefaultSecurityDepositPct()); } } private InputValidator.ValidationResult isXmrInputValid(String input) { return xmrValidator.validate(input); } private InputValidator.ValidationResult isPriceInputValid(String input) { return getPriceValidator().validate(input); } private InputValidator.ValidationResult isVolumeInputValid(String input) { return getVolumeValidator().validate(input); } // TODO: replace with PriceUtils and VolumeUtils? private MonetaryValidator getPriceValidator() { return CurrencyUtil.isPricePrecise(getTradeCurrency().getCode()) ? amountValidator8Decimals : amountValidator4Decimals; } private MonetaryValidator getVolumeValidator() { final String code = getTradeCurrency().getCode(); if (CurrencyUtil.isFiatCurrency(code)) { return fiatVolumeValidator; } else if (CurrencyUtil.isVolumeRoundedToNearestUnit(code)) { return amountValidator4Decimals; } else { return amountValidator8Decimals; } } private void updateSpinnerInfo() { if (!showPayFundsScreenDisplayed.get() || errorMessage.get() != null || showTransactionPublishedScreen.get()) { waitingForFundsText.set(""); } else if (dataModel.getIsXmrWalletFunded().get()) { waitingForFundsText.set(""); } else { waitingForFundsText.set(Res.get("shared.waitingForFunds")); } isWaitingForFunds.set(!waitingForFundsText.get().isEmpty()); } private void updateSecurityDeposit() { isMinSecurityDeposit.set(dataModel.isMinSecurityDeposit()); securityDepositLabel.set(getSecurityDepositLabel()); if (dataModel.isMinSecurityDeposit()) { securityDeposit.set(HavenoUtils.formatXmr(Restrictions.getMinSecurityDeposit())); securityDepositValidationResult.set(new ValidationResult(true)); } else { boolean hasBuyerAsTakerWithoutDeposit = dataModel.buyerAsTakerWithoutDeposit.get() && dataModel.isSellOffer(); securityDeposit.set(FormattingUtils.formatToPercent(hasBuyerAsTakerWithoutDeposit ? Restrictions.getDefaultSecurityDepositPct() : // use default percent if no deposit from buyer dataModel.getSecurityDepositPct().get())); } } void updateButtonDisableState() { dataModel.calculateVolume(); dataModel.calculateTotalToPay(); boolean inputDataValid = isXmrInputValid(amount.get()).isValid && isXmrInputValid(minAmount.get()).isValid && isPriceInputValid(price.get()).isValid && dataModel.getPrice().get() != null && dataModel.getPrice().get().getValue() != 0 && isVolumeInputValid(volume.get()).isValid && isVolumeInputValid(VolumeUtil.formatVolume(dataModel.getMinVolume().get())).isValid && dataModel.isMinAmountLessOrEqualAmount(); if (dataModel.useMarketBasedPrice.get() && dataModel.isMarketPriceAvailable()) { inputDataValid = inputDataValid && triggerPriceValidationResult.get().isValid; } // validating the percentage deposit value only makes sense if it is actually used if (!dataModel.isMinSecurityDeposit()) { inputDataValid = inputDataValid && securityDepositValidator.validate(securityDeposit.get()).isValid; } inputDataValid = inputDataValid && getExtraInfoValidationResult().isValid; isNextButtonDisabled.set(!inputDataValid); isPlaceOfferButtonDisabled.set(createOfferInProgress || !inputDataValid || !dataModel.getIsXmrWalletFunded().get()); } private ValidationResult getExtraInfoValidationResult() { if (extraInfo.get() != null && !extraInfo.get().isEmpty() && extraInfo.get().length() > Restrictions.getMaxExtraInfoLength()) { return new InputValidator.ValidationResult(false, Res.get("createOffer.extraInfo.invalid.tooLong", Restrictions.getMaxExtraInfoLength())); } else { return new InputValidator.ValidationResult(true); } } private void updateMarketPriceToManual() { final String currencyCode = dataModel.getTradeCurrencyCode().get(); MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) { double marketPriceAsDouble = marketPrice.getPrice(); double amountAsDouble = ParsingUtils.parseNumberStringToDouble(amount.get()); double volumeAsDouble = ParsingUtils.parseNumberStringToDouble(volume.get()); double manualPriceAsDouble = dataModel.calculateMarketPriceManual(marketPriceAsDouble, volumeAsDouble, amountAsDouble); int precision = CurrencyUtil.isTraditionalCurrency(currencyCode) ? TraditionalMoney.SMALLEST_UNIT_EXPONENT : CryptoMoney.SMALLEST_UNIT_EXPONENT; price.set(FormattingUtils.formatRoundedDoubleWithPrecision(manualPriceAsDouble, precision)); setPriceToModel(); dataModel.calculateTotalToPay(); updateButtonDisableState(); applyMakerFee(); } else { marketPriceMargin.set(""); String id = "showNoPriceFeedAvailablePopup"; if (preferences.showAgain(id)) { new Popup().warning(Res.get("popup.warning.noPriceFeedAvailable")) .dontShowAgainId(id) .show(); } } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/OfferDataModel.java ================================================ /* * 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 . */ package haveno.desktop.main.offer; import haveno.common.UserThread; import haveno.core.offer.OfferUtil; import haveno.core.offer.OpenOfferManager; import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.common.model.ActivatableDataModel; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import lombok.Getter; import java.math.BigInteger; /** * Domain for that UI element. * Note that the create offer domain has a deeper scope in the application domain * (TradeManager). That model is just responsible for the domain specific parts displayed * needed in that UI element. */ public abstract class OfferDataModel extends ActivatableDataModel { @Getter protected final XmrWalletService xmrWalletService; protected final OpenOfferManager openOfferManager; protected final OfferUtil offerUtil; @Getter protected final BooleanProperty isXmrWalletFunded = new SimpleBooleanProperty(); @Getter protected final ObjectProperty totalToPay = new SimpleObjectProperty<>(); @Getter protected final ObjectProperty unallocatedBalance = new SimpleObjectProperty<>(); @Getter protected final ObjectProperty availableBalance = new SimpleObjectProperty<>(); @Getter protected final ObjectProperty missingCoin = new SimpleObjectProperty<>(BigInteger.ZERO); @Getter protected final BooleanProperty showWalletFundedNotification = new SimpleBooleanProperty(); @Getter protected BigInteger totalUnallocatedBalance; @Getter protected BigInteger totalAvailableBalance; protected XmrAddressEntry addressEntry; protected boolean useSavingsWallet; public OfferDataModel(XmrWalletService xmrWalletService, OpenOfferManager openOfferManager, OfferUtil offerUtil) { this.xmrWalletService = xmrWalletService; this.openOfferManager = openOfferManager; this.offerUtil = offerUtil; } protected void updateBalances() { BigInteger tradeWalletBalance = xmrWalletService.getBalanceForSubaddress(addressEntry.getSubaddressIndex()); BigInteger tradeWalletAvailableBalance = xmrWalletService.getAvailableBalanceForSubaddress(addressEntry.getSubaddressIndex()); BigInteger walletUnallocatedBalance = openOfferManager.getUnallocatedBalance(); BigInteger walletAvailableBalance = xmrWalletService.getAvailableBalance(); UserThread.await(() -> { if (useSavingsWallet) { totalUnallocatedBalance = walletUnallocatedBalance; totalAvailableBalance = walletAvailableBalance; if (totalToPay.get() != null) { unallocatedBalance.set(totalToPay.get().min(totalUnallocatedBalance)); availableBalance.set(totalToPay.get().min(totalAvailableBalance)); } } else { totalUnallocatedBalance = tradeWalletBalance; totalAvailableBalance = tradeWalletAvailableBalance; unallocatedBalance.set(tradeWalletBalance); availableBalance.set(tradeWalletAvailableBalance); } }); } public boolean hasTotalToPay() { return totalToPay.get() != null && totalToPay.get().compareTo(BigInteger.ZERO) > 0; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/OfferView.java ================================================ /* * 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 . */ package haveno.desktop.main.offer; import haveno.common.UserThread; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.GlobalSettings; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.payment.payload.PaymentMethod; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.desktop.Navigation; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.common.view.View; import haveno.desktop.common.view.ViewLoader; import haveno.desktop.main.MainView; import haveno.desktop.main.offer.createoffer.CreateOfferView; import haveno.desktop.main.offer.offerbook.FiatOfferBookView; import haveno.desktop.main.offer.offerbook.OfferBookView; import haveno.desktop.main.offer.offerbook.CryptoOfferBookView; import haveno.desktop.main.offer.offerbook.OtherOfferBookView; import haveno.desktop.main.offer.takeoffer.TakeOfferView; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.P2PService; import javafx.beans.value.ChangeListener; import javafx.scene.control.Label; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; import java.util.Optional; public abstract class OfferView extends ActivatableView { private OfferBookView fiatOfferBookView, cryptoOfferBookView, otherOfferBookView; private Tab labelTab, fiatOfferBookTab, cryptoOfferBookTab, otherOfferBookTab; private final ViewLoader viewLoader; private final Navigation navigation; private final Preferences preferences; private final User user; private final P2PService p2PService; private final OfferDirection direction; private Offer offer; private TradeCurrency tradeCurrency; private Navigation.Listener navigationListener; private ChangeListener tabChangeListener; private OfferView.OfferActionHandler offerActionHandler; private boolean creatingOrTakingOffer; protected OfferView(ViewLoader viewLoader, Navigation navigation, Preferences preferences, User user, P2PService p2PService, OfferDirection direction) { this.viewLoader = viewLoader; this.navigation = navigation; this.preferences = preferences; this.user = user; this.p2PService = p2PService; this.direction = direction; } @Override protected void initialize() { navigationListener = (viewPath, data) -> { UserThread.execute(() -> { if (creatingOrTakingOffer) return; if (viewPath.size() == 3 && viewPath.indexOf(this.getClass()) == 1) { loadView(viewPath.tip(), null, data); } else if (viewPath.size() == 4 && viewPath.indexOf(this.getClass()) == 1) { loadView(viewPath.get(2), viewPath.tip(), data); } }); }; tabChangeListener = (observableValue, oldValue, newValue) -> { UserThread.execute(() -> { if (newValue != null) { if (newValue.equals(fiatOfferBookTab)) { if (fiatOfferBookView != null) { fiatOfferBookView.onTabSelected(true); } else { loadView(FiatOfferBookView.class, null, null); } } else if (newValue.equals(cryptoOfferBookTab)) { if (cryptoOfferBookView != null) { cryptoOfferBookView.onTabSelected(true); } else { loadView(CryptoOfferBookView.class, null, null); } } else if (newValue.equals(otherOfferBookTab)) { if (otherOfferBookView != null) { otherOfferBookView.onTabSelected(true); } else { loadView(OtherOfferBookView.class, null, null); } } } if (oldValue != null) { if (oldValue.equals(fiatOfferBookTab) && fiatOfferBookView != null) { fiatOfferBookView.onTabSelected(false); } else if (oldValue.equals(cryptoOfferBookTab) && cryptoOfferBookView != null) { cryptoOfferBookView.onTabSelected(false); } else if (oldValue.equals(otherOfferBookTab) && otherOfferBookView != null) { otherOfferBookView.onTabSelected(false); } } }); }; offerActionHandler = new OfferActionHandler() { @Override public void onCreateOffer(TradeCurrency tradeCurrency, PaymentMethod paymentMethod) { if (canCreateOrTakeOffer(tradeCurrency)) { showCreateOffer(tradeCurrency, paymentMethod); } } @Override public void onTakeOffer(Offer offer) { Optional optionalTradeCurrency = CurrencyUtil.getTradeCurrency(offer.getCounterCurrencyCode()); if (optionalTradeCurrency.isPresent() && canCreateOrTakeOffer(optionalTradeCurrency.get())) { showTakeOffer(offer); } } }; } @Override protected void activate() { Optional tradeCurrencyOptional = (this.direction == OfferDirection.SELL) ? CurrencyUtil.getTradeCurrency(preferences.getSellScreenCurrencyCode()) : CurrencyUtil.getTradeCurrency(preferences.getBuyScreenCurrencyCode()); tradeCurrency = tradeCurrencyOptional.orElseGet(GlobalSettings::getDefaultTradeCurrency); root.getSelectionModel().selectedItemProperty().addListener(tabChangeListener); navigation.addListener(navigationListener); if (fiatOfferBookView == null) { navigation.navigateTo(MainView.class, this.getClass(), FiatOfferBookView.class); } } @Override protected void deactivate() { navigation.removeListener(navigationListener); root.getSelectionModel().selectedItemProperty().removeListener(tabChangeListener); } protected abstract String getOfferLabel(); private void loadView(Class viewClass, Class childViewClass, @Nullable Object data) { TabPane tabPane = root; tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.ALL_TABS); if (OfferBookView.class.isAssignableFrom(viewClass)) { if (viewClass == FiatOfferBookView.class && fiatOfferBookTab != null && fiatOfferBookView != null) { if (childViewClass == null) { fiatOfferBookTab.setContent(fiatOfferBookView.getRoot()); } else if (childViewClass == TakeOfferView.class) { loadTakeViewClass(viewClass, childViewClass, fiatOfferBookTab); } else { loadCreateViewClass(fiatOfferBookView, viewClass, childViewClass, fiatOfferBookTab, (PaymentMethod) data); } tabPane.getSelectionModel().select(fiatOfferBookTab); } else if (viewClass == CryptoOfferBookView.class && cryptoOfferBookTab != null && cryptoOfferBookView != null) { if (childViewClass == null) { cryptoOfferBookTab.setContent(cryptoOfferBookView.getRoot()); } else if (childViewClass == TakeOfferView.class) { loadTakeViewClass(viewClass, childViewClass, cryptoOfferBookTab); } else { // add sanity check in case of app restart Optional tradeCurrencyOptional = (this.direction == OfferDirection.SELL) ? CurrencyUtil.getTradeCurrency(preferences.getSellScreenCryptoCurrencyCode()) : CurrencyUtil.getTradeCurrency(preferences.getBuyScreenCryptoCurrencyCode()); tradeCurrency = tradeCurrencyOptional.isEmpty() ? OfferViewUtil.getAnyOfMainCryptoCurrencies() : tradeCurrencyOptional.get(); loadCreateViewClass(cryptoOfferBookView, viewClass, childViewClass, cryptoOfferBookTab, (PaymentMethod) data); } tabPane.getSelectionModel().select(cryptoOfferBookTab); } else if (viewClass == OtherOfferBookView.class && otherOfferBookTab != null && otherOfferBookView != null) { if (childViewClass == null) { otherOfferBookTab.setContent(otherOfferBookView.getRoot()); } else if (childViewClass == TakeOfferView.class) { loadTakeViewClass(viewClass, childViewClass, otherOfferBookTab); } else { loadCreateViewClass(otherOfferBookView, viewClass, childViewClass, otherOfferBookTab, (PaymentMethod) data); } tabPane.getSelectionModel().select(otherOfferBookTab); } else { if (fiatOfferBookTab == null) { // add preceding label tab labelTab = new Tab(); labelTab.setDisable(true); labelTab.setContent(new Label()); labelTab.setClosable(false); Label offerLabel = new Label(getOfferLabel()); // use overlay for label for custom formatting offerLabel.getStyleClass().add("titled-group-bg-label"); offerLabel.setStyle("-fx-font-size: 1.3em;"); labelTab.setGraphic(offerLabel); fiatOfferBookTab = new Tab(Res.get("shared.fiat")); fiatOfferBookTab.setClosable(false); cryptoOfferBookTab = new Tab(Res.get("shared.crypto")); cryptoOfferBookTab.setClosable(false); otherOfferBookTab = new Tab(Res.get("shared.other")); otherOfferBookTab.setClosable(false); tabPane.getTabs().addAll(labelTab, fiatOfferBookTab, cryptoOfferBookTab, otherOfferBookTab); } if (viewClass == FiatOfferBookView.class) { fiatOfferBookView = (FiatOfferBookView) viewLoader.load(FiatOfferBookView.class); fiatOfferBookView.setOfferActionHandler(offerActionHandler); fiatOfferBookView.setDirection(direction); fiatOfferBookView.onTabSelected(true); tabPane.getSelectionModel().select(fiatOfferBookTab); fiatOfferBookTab.setContent(fiatOfferBookView.getRoot()); } else if (viewClass == CryptoOfferBookView.class) { cryptoOfferBookView = (CryptoOfferBookView) viewLoader.load(CryptoOfferBookView.class); cryptoOfferBookView.setOfferActionHandler(offerActionHandler); cryptoOfferBookView.setDirection(direction); cryptoOfferBookView.onTabSelected(true); tabPane.getSelectionModel().select(cryptoOfferBookTab); cryptoOfferBookTab.setContent(cryptoOfferBookView.getRoot()); } else if (viewClass == OtherOfferBookView.class) { otherOfferBookView = (OtherOfferBookView) viewLoader.load(OtherOfferBookView.class); otherOfferBookView.setOfferActionHandler(offerActionHandler); otherOfferBookView.setDirection(direction); otherOfferBookView.onTabSelected(true); tabPane.getSelectionModel().select(otherOfferBookTab); otherOfferBookTab.setContent(otherOfferBookView.getRoot()); } } } } private void loadCreateViewClass(OfferBookView offerBookView, Class viewClass, Class childViewClass, Tab marketOfferBookTab, @Nullable PaymentMethod paymentMethod) { if (tradeCurrency == null) { return; } View view; // CreateOffer and TakeOffer must not be cached by ViewLoader as we cannot use a view multiple times // in different graphs view = viewLoader.load(childViewClass); ((CreateOfferView) view).initWithData(direction, tradeCurrency, offerActionHandler); ((SelectableView) view).onTabSelected(true); creatingOrTakingOffer = true; ((ClosableView) view).setCloseHandler(() -> { creatingOrTakingOffer = false; offerBookView.enableCreateOfferButton(); ((SelectableView) view).onTabSelected(false); //reset tab navigation.navigateTo(MainView.class, this.getClass(), viewClass); }); // close handler from close on create offer action marketOfferBookTab.setContent(view.getRoot()); } private void loadTakeViewClass(Class viewClass, Class childViewClass, Tab marketOfferBookTab) { if (offer == null) { return; } View view = viewLoader.load(childViewClass); // CreateOffer and TakeOffer must not be cached by ViewLoader as we cannot use a view multiple times // in different graphs ((InitializableViewWithTakeOfferData) view).initWithData(offer); ((SelectableView) view).onTabSelected(true); // close handler from close on take offer action creatingOrTakingOffer = true; ((ClosableView) view).setCloseHandler(() -> { creatingOrTakingOffer = false; ((SelectableView) view).onTabSelected(false); navigation.navigateTo(MainView.class, this.getClass(), viewClass); }); marketOfferBookTab.setContent(view.getRoot()); } protected boolean canCreateOrTakeOffer(TradeCurrency tradeCurrency) { return GUIUtil.isBootstrappedOrShowPopup(p2PService) && GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation); } private void showTakeOffer(Offer offer) { this.offer = offer; Class> offerBookViewClass = getOfferBookViewClassFor(offer.getCounterCurrencyCode()); navigation.navigateTo(MainView.class, this.getClass(), offerBookViewClass, TakeOfferView.class); } private void showCreateOffer(TradeCurrency tradeCurrency, PaymentMethod paymentMethod) { this.tradeCurrency = tradeCurrency; Class> offerBookViewClass = getOfferBookViewClassFor(tradeCurrency.getCode()); navigation.navigateToWithData(paymentMethod, MainView.class, this.getClass(), offerBookViewClass, CreateOfferView.class); } @NotNull private Class> getOfferBookViewClassFor(String currencyCode) { Class> offerBookViewClass; if (CurrencyUtil.isFiatCurrency(currencyCode)) { offerBookViewClass = FiatOfferBookView.class; } else if (CurrencyUtil.isCryptoCurrency(currencyCode)) { offerBookViewClass = CryptoOfferBookView.class; } else { offerBookViewClass = OtherOfferBookView.class; } return offerBookViewClass; } public interface OfferActionHandler { void onCreateOffer(TradeCurrency tradeCurrency, PaymentMethod paymentMethod); void onTakeOffer(Offer offer); } public interface CloseHandler { void close(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/OfferViewModelUtil.java ================================================ /* * 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 . */ package haveno.desktop.main.offer; import haveno.core.locale.Res; import haveno.core.monetary.Volume; import haveno.core.offer.OfferUtil; import haveno.core.trade.HavenoUtils; import haveno.core.util.VolumeUtil; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.util.DisplayUtils; import haveno.desktop.util.GUIUtil; import java.math.BigInteger; import java.util.Optional; // Shared utils for ViewModels public class OfferViewModelUtil { public static String getTradeFeeWithFiatEquivalent(OfferUtil offerUtil, BigInteger tradeFee, CoinFormatter formatter) { Optional optionalBtcFeeInFiat = offerUtil.getFeeInUserFiatCurrency(tradeFee, formatter); return DisplayUtils.getFeeWithFiatAmount(tradeFee, optionalBtcFeeInFiat, formatter); } public static String getTradeFeeWithFiatEquivalentAndPercentage(OfferUtil offerUtil, BigInteger tradeFee, BigInteger tradeAmount, CoinFormatter formatter) { String feeAsXmr = HavenoUtils.formatXmr(tradeFee, true); String percentage; percentage = GUIUtil.getPercentage(tradeFee, tradeAmount) + " " + Res.get("guiUtil.ofTradeAmount"); return offerUtil.getFeeInUserFiatCurrency(tradeFee, formatter) .map(VolumeUtil::formatAverageVolumeWithCode) .map(feeInFiat -> Res.get("feeOptionWindow.xmrFeeWithFiatAndPercentage", feeAsXmr, feeInFiat, percentage)) .orElseGet(() -> Res.get("feeOptionWindow.xmrFeeWithPercentage", feeAsXmr, percentage)); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/OfferViewUtil.java ================================================ /* * 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 . */ package haveno.desktop.main.offer; import haveno.common.UserThread; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.locale.TraditionalCurrency; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.main.offer.offerbook.FiatOfferBookView; import haveno.desktop.main.offer.offerbook.OfferBookView; import haveno.desktop.main.offer.offerbook.CryptoOfferBookView; import haveno.desktop.main.offer.offerbook.OtherOfferBookView; import haveno.desktop.main.overlays.popups.Popup; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.VPos; import javafx.scene.control.Label; import javafx.scene.control.TableView; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import lombok.extern.slf4j.Slf4j; import monero.daemon.model.MoneroSubmitTxResult; import org.jetbrains.annotations.NotNull; import java.util.HashMap; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; // Shared utils for Views @Slf4j public class OfferViewUtil { public static Label createPopOverLabel(String text) { final Label label = new Label(text); label.setPrefWidth(300); label.setWrapText(true); label.setLineSpacing(1); label.setPadding(new Insets(10)); return label; } public static void showPaymentAccountWarning(String msgKey, HashMap paymentAccountWarningDisplayed) { if (msgKey == null || paymentAccountWarningDisplayed.getOrDefault(msgKey, false)) { return; } paymentAccountWarningDisplayed.put(msgKey, true); UserThread.runAfter(() -> { new Popup().information(Res.get(msgKey)) .width(900) .closeButtonText(Res.get("shared.iConfirm")) .dontShowAgainId(msgKey) .show(); }, 500, TimeUnit.MILLISECONDS); } public static void addPayInfoEntry(GridPane infoGridPane, int row, String labelText, String value) { Label label = new AutoTooltipLabel(labelText); TextField textField = new TextField(value); textField.setMinWidth(500); textField.setEditable(false); textField.setFocusTraversable(false); textField.setId("payment-info"); GridPane.setConstraints(label, 0, row, 1, 1, HPos.RIGHT, VPos.CENTER); GridPane.setConstraints(textField, 1, row); infoGridPane.getChildren().addAll(label, textField); } public static Class> getOfferBookViewClass(String currencyCode) { Class> offerBookViewClazz; if (CurrencyUtil.isFiatCurrency(currencyCode)) { offerBookViewClazz = FiatOfferBookView.class; } else if (CurrencyUtil.isCryptoCurrency(currencyCode)) { offerBookViewClazz = CryptoOfferBookView.class; } else { offerBookViewClazz = OtherOfferBookView.class; } return offerBookViewClazz; } public static boolean isShownAsSellOffer(Offer offer) { return isShownAsSellOffer(offer.getCounterCurrencyCode(), offer.getDirection()); } public static boolean isShownAsSellOffer(TradeCurrency tradeCurrency, OfferDirection direction) { return isShownAsSellOffer(tradeCurrency.getCode(), direction); } public static boolean isShownAsSellOffer(String currencyCode, OfferDirection direction) { return direction == OfferDirection.SELL; } public static boolean isShownAsBuyOffer(Offer offer) { return !isShownAsSellOffer(offer); } public static boolean isShownAsBuyOffer(OfferDirection direction, TradeCurrency tradeCurrency) { return !isShownAsSellOffer(tradeCurrency.getCode(), direction); } public static TradeCurrency getAnyOfMainCryptoCurrencies() { return getMainCryptoCurrencies().findAny().get(); } public static TradeCurrency getAnyOfOtherCurrencies() { return getOtherCurrencies().findAny().get(); } @NotNull public static Stream getMainCryptoCurrencies() { return CurrencyUtil.getMainCryptoCurrencies().stream(); } @NotNull public static Stream getOtherCurrencies() { return CurrencyUtil.getTraditionalNonFiatCurrencies().stream(); } public static void submitTransactionHex(XmrWalletService xmrWalletService, TableView tableView, String reserveTxHex) { MoneroSubmitTxResult result = xmrWalletService.getMonerod().submitTxHex(reserveTxHex); log.info("submitTransactionHex: reserveTxHex={} result={}", result); tableView.refresh(); if(result.isGood()) { new Popup().information(Res.get("support.result.success")).show(); } else { new Popup().attention(result.toString()).show(); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/SelectableView.java ================================================ /* * 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 . */ package haveno.desktop.main.offer; public interface SelectableView { public void onTabSelected(boolean isSelected); } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/SellOfferView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/SellOfferView.java ================================================ /* * 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 . */ package haveno.desktop.main.offer; import com.google.inject.Inject; import haveno.core.locale.Res; import haveno.core.offer.OfferDirection; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.desktop.Navigation; import haveno.desktop.common.view.FxmlView; import haveno.desktop.common.view.ViewLoader; import haveno.network.p2p.P2PService; @FxmlView public class SellOfferView extends OfferView { @Inject public SellOfferView(ViewLoader viewLoader, Navigation navigation, Preferences preferences, User user, P2PService p2PService) { super(viewLoader, navigation, preferences, user, p2PService, OfferDirection.SELL); } @Override protected String getOfferLabel() { return Res.get("offerbook.sellXmrFor"); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/createoffer/CreateOfferDataModel.java ================================================ /* * 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 . */ package haveno.desktop.main.offer.createoffer; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.offer.CreateOfferService; import haveno.core.offer.OfferUtil; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.PaymentAccount; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.Navigation; import haveno.desktop.main.offer.MutableOfferDataModel; import haveno.network.p2p.P2PService; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; /** * Domain for that UI element. * Note that the create offer domain has a deeper scope in the application domain (TradeManager). * That model is just responsible for the domain specific parts displayed needed in that UI element. */ class CreateOfferDataModel extends MutableOfferDataModel { @Inject public CreateOfferDataModel(CreateOfferService createOfferService, OpenOfferManager openOfferManager, OfferUtil offerUtil, XmrWalletService xmrWalletService, Preferences preferences, User user, P2PService p2PService, PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, TradeStatisticsManager tradeStatisticsManager, Navigation navigation) { super(createOfferService, openOfferManager, offerUtil, xmrWalletService, preferences, user, p2PService, priceFeedService, accountAgeWitnessService, btcFormatter, tradeStatisticsManager, navigation); } @Override protected Set getUserPaymentAccounts() { return Objects.requireNonNull(user.getPaymentAccounts()).stream() .collect(Collectors.toSet()); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/createoffer/CreateOfferView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/createoffer/CreateOfferView.java ================================================ /* * 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 . */ package haveno.desktop.main.offer.createoffer; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TradeCurrency; import haveno.core.offer.OfferDirection; import haveno.core.payment.PaymentAccount; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.Navigation; import haveno.desktop.common.view.FxmlView; import haveno.desktop.main.offer.MutableOfferView; import haveno.desktop.main.offer.OfferView; import haveno.desktop.main.overlays.windows.OfferDetailsWindow; import java.util.stream.Collectors; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @FxmlView public class CreateOfferView extends MutableOfferView { @Inject private CreateOfferView(CreateOfferViewModel model, Navigation navigation, Preferences preferences, OfferDetailsWindow offerDetailsWindow, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter) { super(model, navigation, preferences, offerDetailsWindow, btcFormatter); } public void initWithData(OfferDirection direction, TradeCurrency tradeCurrency, OfferView.OfferActionHandler offerActionHandler) { super.initWithData(direction, tradeCurrency, true, offerActionHandler); } @Override protected ObservableList filterPaymentAccounts(ObservableList paymentAccounts) { return FXCollections.observableArrayList( paymentAccounts.stream().filter(paymentAccount -> { if (CurrencyUtil.isFiatCurrency(model.getTradeCurrency().getCode())) { return paymentAccount.isFiat(); } else if (CurrencyUtil.isCryptoCurrency(model.getTradeCurrency().getCode())) { return paymentAccount.isCryptoCurrency(); } else { return !paymentAccount.isFiat() && !paymentAccount.isCryptoCurrency(); } }).collect(Collectors.toList())); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/createoffer/CreateOfferViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.offer.createoffer; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.offer.OfferUtil; import haveno.core.payment.validation.FiatVolumeValidator; import haveno.core.payment.validation.SecurityDepositValidator; import haveno.core.payment.validation.XmrValidator; import haveno.core.provider.price.PriceFeedService; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.AmountValidator4Decimals; import haveno.core.util.validation.AmountValidator8Decimals; import haveno.desktop.Navigation; import haveno.desktop.common.model.ViewModel; import haveno.desktop.main.offer.MutableOfferViewModel; class CreateOfferViewModel extends MutableOfferViewModel implements ViewModel { @Inject public CreateOfferViewModel(CreateOfferDataModel dataModel, FiatVolumeValidator fiatVolumeValidator, AmountValidator4Decimals priceValidator4Decimals, AmountValidator8Decimals priceValidator8Decimals, XmrValidator btcValidator, SecurityDepositValidator securityDepositValidator, PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation, Preferences preferences, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, OfferUtil offerUtil) { super(dataModel, fiatVolumeValidator, priceValidator4Decimals, priceValidator8Decimals, btcValidator, securityDepositValidator, priceFeedService, accountAgeWitnessService, navigation, preferences, btcFormatter, offerUtil); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/offerbook/CryptoOfferBookView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/offerbook/CryptoOfferBookView.java ================================================ /* * 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 . */ package haveno.desktop.main.offer.offerbook; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.core.account.sign.SignedWitnessService; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.alert.PrivateNotificationManager; import haveno.core.locale.Res; import haveno.core.offer.OfferDirection; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.Navigation; import haveno.desktop.common.view.FxmlView; import haveno.desktop.main.overlays.windows.OfferDetailsWindow; import javafx.scene.layout.GridPane; @FxmlView public class CryptoOfferBookView extends OfferBookView { @Inject CryptoOfferBookView(CryptoOfferBookViewModel model, Navigation navigation, OfferDetailsWindow offerDetailsWindow, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, PrivateNotificationManager privateNotificationManager, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys, AccountAgeWitnessService accountAgeWitnessService, SignedWitnessService signedWitnessService) { super(model, navigation, offerDetailsWindow, formatter, privateNotificationManager, useDevPrivilegeKeys, accountAgeWitnessService, signedWitnessService); } @Override protected String getMarketTitle() { return model.getDirection().equals(OfferDirection.BUY) ? Res.get("offerbook.availableOffersToBuy", Res.getBaseCurrencyCode(), Res.get("shared.crypto")) : Res.get("offerbook.availableOffersToSell", Res.getBaseCurrencyCode(), Res.get("shared.crypto")); } @Override String getTradeCurrencyCode() { return Res.getBaseCurrencyCode(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/offerbook/CryptoOfferBookViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.offer.offerbook; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.api.CoreApi; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.GlobalSettings; import haveno.core.locale.TradeCurrency; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OfferFilterService; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.payload.PaymentMethod; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.ClosedTradableManager; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.util.FormattingUtils; import haveno.core.util.PriceUtil; import haveno.core.util.coin.CoinFormatter; import haveno.core.xmr.setup.WalletsSetup; import haveno.desktop.Navigation; import haveno.desktop.main.offer.OfferViewUtil; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.P2PService; import java.util.List; import java.util.Map; import java.util.function.Predicate; import java.util.stream.Collectors; import javafx.collections.FXCollections; import javafx.collections.ObservableList; public class CryptoOfferBookViewModel extends OfferBookViewModel { @Inject public CryptoOfferBookViewModel(User user, OpenOfferManager openOfferManager, OfferBook offerBook, Preferences preferences, WalletsSetup walletsSetup, P2PService p2PService, PriceFeedService priceFeedService, ClosedTradableManager closedTradableManager, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation, PriceUtil priceUtil, OfferFilterService offerFilterService, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, CoreApi coreApi) { super(user, openOfferManager, offerBook, preferences, walletsSetup, p2PService, priceFeedService, closedTradableManager, accountAgeWitnessService, navigation, priceUtil, offerFilterService, btcFormatter, coreApi); } @Override void saveSelectedCurrencyCodeInPreferences(OfferDirection direction, String code) { if (direction == OfferDirection.BUY) { preferences.setBuyScreenCryptoCurrencyCode(code); } else { preferences.setSellScreenCryptoCurrencyCode(code); } } @Override protected ObservableList filterPaymentMethods(ObservableList list, TradeCurrency selectedTradeCurrency) { return FXCollections.observableArrayList(list.stream().filter(paymentMethod -> { return paymentMethod.isBlockchain(); }).collect(Collectors.toList())); } @Override void fillCurrencies(ObservableList tradeCurrencies, ObservableList allCurrencies) { tradeCurrencies.add(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, "")); // get and sort currencies List cryptoCurrencies = preferences.getCryptoCurrenciesAsObservable().stream() .collect(Collectors.toList()); if (OfferBookViewModel.SORT_CURRENCIES_BY_OFFER_COUNT) { Map offerCounts = getOfferCounts(); cryptoCurrencies.sort((o1, o2) -> { Integer count1 = offerCounts.getOrDefault(o1.getCode(), 0); Integer count2 = offerCounts.getOrDefault(o2.getCode(), 0); return Integer.compare(count2, count1); }); } tradeCurrencies.addAll(cryptoCurrencies); tradeCurrencies.add(new CryptoCurrency(GUIUtil.EDIT_FLAG, "")); allCurrencies.add(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, "")); allCurrencies.addAll(CurrencyUtil.getAllSortedCryptoCurrencies().stream() .collect(Collectors.toList())); allCurrencies.add(new CryptoCurrency(GUIUtil.EDIT_FLAG, "")); } @Override Predicate getCurrencyAndMethodPredicate(OfferDirection direction, TradeCurrency selectedTradeCurrency) { return offerBookListItem -> { Offer offer = offerBookListItem.getOffer(); boolean directionResult = offer.getDirection() != direction; // offer to buy xmr appears as offer to sell in peer's offer book and vice versa boolean currencyResult = CurrencyUtil.isCryptoCurrency(offer.getCounterCurrencyCode()) && (showAllTradeCurrenciesProperty.get() || offer.getCounterCurrencyCode().equals(selectedTradeCurrency.getCode())); boolean paymentMethodResult = showAllPaymentMethods || offer.getPaymentMethod().equals(selectedPaymentMethod); boolean notMyOfferOrShowMyOffersActivated = !isMyOffer(offerBookListItem.getOffer()) || preferences.isShowOwnOffersInOfferBook(); return directionResult && currencyResult && paymentMethodResult && notMyOfferOrShowMyOffersActivated; }; } @Override TradeCurrency getDefaultTradeCurrency() { TradeCurrency defaultTradeCurrency = GlobalSettings.getDefaultTradeCurrency(); if (CurrencyUtil.isCryptoCurrency(defaultTradeCurrency.getCode()) && hasPaymentAccountForCurrency(defaultTradeCurrency)) { return defaultTradeCurrency; } ObservableList tradeCurrencies = FXCollections.observableArrayList(getTradeCurrencies()); if (!tradeCurrencies.isEmpty()) { // drop show all entry and select first currency with payment account available tradeCurrencies.remove(0); List sortedList = tradeCurrencies.stream().sorted((o1, o2) -> Boolean.compare(!hasPaymentAccountForCurrency(o1), !hasPaymentAccountForCurrency(o2))).collect(Collectors.toList()); return sortedList.get(0); } else { return OfferViewUtil.getMainCryptoCurrencies().sorted((o1, o2) -> Boolean.compare(!hasPaymentAccountForCurrency(o1), !hasPaymentAccountForCurrency(o2))).collect(Collectors.toList()).get(0); } } @Override String getCurrencyCodeFromPreferences(OfferDirection direction) { return direction == OfferDirection.BUY ? preferences.getBuyScreenCryptoCurrencyCode() : preferences.getSellScreenCryptoCurrencyCode(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/offerbook/FiatOfferBookView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/offerbook/FiatOfferBookView.java ================================================ /* * 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 . */ package haveno.desktop.main.offer.offerbook; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.core.account.sign.SignedWitnessService; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.alert.PrivateNotificationManager; import haveno.core.locale.Res; import haveno.core.offer.OfferDirection; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.Navigation; import haveno.desktop.common.view.FxmlView; import haveno.desktop.main.overlays.windows.OfferDetailsWindow; import javafx.scene.layout.GridPane; @FxmlView public class FiatOfferBookView extends OfferBookView { @Inject FiatOfferBookView(FiatOfferBookViewModel model, Navigation navigation, OfferDetailsWindow offerDetailsWindow, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, PrivateNotificationManager privateNotificationManager, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys, AccountAgeWitnessService accountAgeWitnessService, SignedWitnessService signedWitnessService) { super(model, navigation, offerDetailsWindow, formatter, privateNotificationManager, useDevPrivilegeKeys, accountAgeWitnessService, signedWitnessService); } @Override protected String getMarketTitle() { return model.getDirection().equals(OfferDirection.BUY) ? Res.get("offerbook.availableOffersToBuy", Res.getBaseCurrencyCode(), Res.get("shared.fiat")) : Res.get("offerbook.availableOffersToSell", Res.getBaseCurrencyCode(), Res.get("shared.fiat")); } @Override String getTradeCurrencyCode() { return Res.getBaseCurrencyCode(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/offerbook/FiatOfferBookViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.offer.offerbook; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.api.CoreApi; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.GlobalSettings; import haveno.core.locale.TradeCurrency; import haveno.core.locale.TraditionalCurrency; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OfferFilterService; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.PaymentAccountUtil; import haveno.core.payment.payload.PaymentMethod; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.ClosedTradableManager; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.util.FormattingUtils; import haveno.core.util.PriceUtil; import haveno.core.util.coin.CoinFormatter; import haveno.core.xmr.setup.WalletsSetup; import haveno.desktop.Navigation; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.P2PService; import java.util.List; import java.util.Map; import java.util.function.Predicate; import java.util.stream.Collectors; import javafx.collections.FXCollections; import javafx.collections.ObservableList; public class FiatOfferBookViewModel extends OfferBookViewModel { @Inject public FiatOfferBookViewModel(User user, OpenOfferManager openOfferManager, OfferBook offerBook, Preferences preferences, WalletsSetup walletsSetup, P2PService p2PService, PriceFeedService priceFeedService, ClosedTradableManager closedTradableManager, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation, PriceUtil priceUtil, OfferFilterService offerFilterService, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, CoreApi coreApi) { super(user, openOfferManager, offerBook, preferences, walletsSetup, p2PService, priceFeedService, closedTradableManager, accountAgeWitnessService, navigation, priceUtil, offerFilterService, btcFormatter, coreApi); } @Override void saveSelectedCurrencyCodeInPreferences(OfferDirection direction, String code) { if (direction == OfferDirection.BUY) { preferences.setBuyScreenCurrencyCode(code); } else { preferences.setSellScreenCurrencyCode(code); } } @Override protected ObservableList filterPaymentMethods(ObservableList list, TradeCurrency selectedTradeCurrency) { return FXCollections.observableArrayList(list.stream() .filter(paymentMethod -> { if (showAllTradeCurrenciesProperty.get()) { return paymentMethod.isTraditional(); } return paymentMethod.isTraditional() && PaymentAccountUtil.supportsCurrency(paymentMethod, selectedTradeCurrency); }) .collect(Collectors.toList())); } @Override void fillCurrencies(ObservableList tradeCurrencies, ObservableList allCurrencies) { // Used for ignoring filter (show all) tradeCurrencies.add(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, "")); // get and sort currencies List traditionalCurrencies = preferences.getTraditionalCurrenciesAsObservable().stream() .filter(withFiatCurrency()) .collect(Collectors.toList()); if (OfferBookViewModel.SORT_CURRENCIES_BY_OFFER_COUNT) { Map offerCounts = getOfferCounts(); traditionalCurrencies.sort((o1, o2) -> { Integer count1 = offerCounts.getOrDefault(o1.getCode(), 0); Integer count2 = offerCounts.getOrDefault(o2.getCode(), 0); return Integer.compare(count2, count1); }); } tradeCurrencies.addAll(traditionalCurrencies); tradeCurrencies.add(new CryptoCurrency(GUIUtil.EDIT_FLAG, "")); allCurrencies.add(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, "")); allCurrencies.addAll(CurrencyUtil.getAllSortedTraditionalCurrencies().stream() .filter(withFiatCurrency()) .collect(Collectors.toList())); allCurrencies.add(new CryptoCurrency(GUIUtil.EDIT_FLAG, "")); } @Override Predicate getCurrencyAndMethodPredicate(OfferDirection direction, TradeCurrency selectedTradeCurrency) { return offerBookListItem -> { Offer offer = offerBookListItem.getOffer(); boolean directionResult = offer.getDirection() != direction; boolean currencyResult = (showAllTradeCurrenciesProperty.get() && offer.isFiatOffer()) || offer.getCounterCurrencyCode().equals(selectedTradeCurrency.getCode()); boolean paymentMethodResult = showAllPaymentMethods || offer.getPaymentMethod().equals(selectedPaymentMethod); boolean notMyOfferOrShowMyOffersActivated = !isMyOffer(offerBookListItem.getOffer()) || preferences.isShowOwnOffersInOfferBook(); return directionResult && currencyResult && paymentMethodResult && notMyOfferOrShowMyOffersActivated; }; } @Override TradeCurrency getDefaultTradeCurrency() { TradeCurrency defaultTradeCurrency = GlobalSettings.getDefaultTradeCurrency(); if (CurrencyUtil.isTraditionalCurrency(defaultTradeCurrency.getCode()) && hasPaymentAccountForCurrency(defaultTradeCurrency)) { return defaultTradeCurrency; } ObservableList tradeCurrencies = FXCollections.observableArrayList(getTradeCurrencies()); if (!tradeCurrencies.isEmpty()) { // drop show all entry and select first currency with payment account available tradeCurrencies.remove(0); List sortedList = tradeCurrencies.stream().sorted((o1, o2) -> Boolean.compare(!hasPaymentAccountForCurrency(o1), !hasPaymentAccountForCurrency(o2))).collect(Collectors.toList()); return sortedList.get(0); } else { return CurrencyUtil.getMainTraditionalCurrencies().stream() .filter(withFiatCurrency()) .sorted((o1, o2) -> Boolean.compare(!hasPaymentAccountForCurrency(o1), !hasPaymentAccountForCurrency(o2))) .collect(Collectors.toList()).get(0); } } @Override String getCurrencyCodeFromPreferences(OfferDirection direction) { // validate if previous stored currencies are Traditional ones String currencyCode = direction == OfferDirection.BUY ? preferences.getBuyScreenCurrencyCode() : preferences.getSellScreenCurrencyCode(); return CurrencyUtil.isTraditionalCurrency(currencyCode) ? currencyCode : null; } private Predicate withFiatCurrency() { return fiatCurrency -> CurrencyUtil.isFiatCurrency(fiatCurrency.getCode()); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBook.java ================================================ /* * 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 . */ package haveno.desktop.main.offer.offerbook; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.UserThread; import haveno.core.offer.Offer; import haveno.core.offer.OfferBookService; import static haveno.core.offer.OfferDirection.BUY; import haveno.network.p2p.storage.P2PDataStorage; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import lombok.extern.slf4j.Slf4j; /** * Holds and manages the unsorted and unfiltered offerbook list (except for banned offers) of both buy and sell offers. * It is handled as singleton by Guice and is used by 2 instances of OfferBookDataModel (one for Buy one for Sell). * As it is used only by the Buy and Sell UIs we treat it as local UI model. * It also use OfferRepository.Listener as the lists items class and we don't want to get any dependency out of the * package for that. */ @Singleton @Slf4j public class OfferBook { private final OfferBookService offerBookService; private final ObservableList offerBookListItems = FXCollections.observableArrayList(); private final Map buyOfferCountMap = new HashMap<>(); private final Map sellOfferCountMap = new HashMap<>(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject OfferBook(OfferBookService offerBookService) { this.offerBookService = offerBookService; offerBookService.addOfferBookChangedListener(new OfferBookService.OfferBookChangedListener() { @Override public void onAdded(Offer offer) { UserThread.execute(() -> { printOfferBookListItems("Before onAdded"); // Use offer.equals(offer) to see if the OfferBook list contains an exact // match -- offer.equals(offer) includes comparisons of payload, state // and errorMessage. synchronized (offerBookListItems) { boolean hasSameOffer = offerBookListItems.stream().anyMatch(item -> item.getOffer().equals(offer)); if (!hasSameOffer) { OfferBookListItem newOfferBookListItem = new OfferBookListItem(offer); removeDuplicateItem(newOfferBookListItem); offerBookListItems.add(newOfferBookListItem); // Add replacement. if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. log.debug("onAdded: Added new offer {}\n" + "\twith newItem.payloadHash: {}", offer.getId(), newOfferBookListItem.hashOfPayload.getHex()); } } else { log.debug("We have the exact same offer already in our list and ignore the onAdded call. ID={}", offer.getId()); } printOfferBookListItems("After onAdded"); } }); } @Override public void onRemoved(Offer offer) { UserThread.execute(() -> { synchronized (offerBookListItems) { printOfferBookListItems("Before onRemoved"); removeOffer(offer); printOfferBookListItems("After onRemoved"); } }); } }); } private void removeDuplicateItem(OfferBookListItem newOfferBookListItem) { synchronized (offerBookListItems) { String offerId = newOfferBookListItem.getOffer().getId(); // We need to remove any view items with a matching offerId before // a newOfferBookListItem is added to the view. List duplicateItems = offerBookListItems.stream() .filter(item -> item.getOffer().getId().equals(offerId)) .collect(Collectors.toList()); duplicateItems.forEach(oldOfferItem -> { offerBookListItems.remove(oldOfferItem); if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. log.debug("onAdded: Removed old offer {}\n" + "\twith payload hash {} from list.\n" + "\tThis may make a subsequent onRemoved( {} ) call redundant.", offerId, oldOfferItem.getHashOfPayload().getHex(), oldOfferItem.getOffer().getId()); } }); } } public void removeOffer(Offer offer) { synchronized (offerBookListItems) { // Update state in case that that offer is used in the take offer screen, so it gets updated correctly offer.setState(Offer.State.REMOVED); offer.cancelAvailabilityRequest(); P2PDataStorage.ByteArray hashOfPayload = new P2PDataStorage.ByteArray(offer.getOfferPayload().getHash()); if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. log.debug("onRemoved: id = {}\n" + "\twith payload-hash = {}", offer.getId(), hashOfPayload.getHex()); } // Find the removal candidate in the OfferBook list with matching offerId and payload-hash. Optional candidateWithMatchingPayloadHash = offerBookListItems.stream() .filter(item -> item.getOffer().getId().equals(offer.getId()) && item.hashOfPayload.equals(hashOfPayload)) .findAny(); if (!candidateWithMatchingPayloadHash.isPresent()) { if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. log.debug("UI view list does not contain offer with id {} and payload-hash {}", offer.getId(), hashOfPayload.getHex()); } return; } OfferBookListItem candidate = candidateWithMatchingPayloadHash.get(); // Remove the candidate only if the candidate's offer payload the hash matches the // onRemoved hashOfPayload parameter. We may receive add/remove messages out of // order from the API's 'editoffer' method, and use the offer payload hash to // ensure we do not remove an edited offer immediately after it was added. if (candidate.getHashOfPayload().equals(hashOfPayload)) { // The payload-hash test passed, remove the candidate and print reason. offerBookListItems.remove(candidate); if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. log.debug("Candidate.payload-hash: {} == onRemoved.payload-hash: {} ?" + " Yes, removed old offer", candidate.hashOfPayload.getHex(), hashOfPayload.getHex()); } } else { if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. // Candidate's payload-hash test failed: payload-hash != onRemoved.payload-hash. // Print reason for not removing candidate. log.debug("Candidate.payload-hash: {} == onRemoved.payload-hash: {} ?" + " No, old offer not removed", candidate.hashOfPayload.getHex(), hashOfPayload.getHex()); } } } } public ObservableList getOfferBookListItems() { return offerBookListItems; } public void fillOfferBookListItems() { synchronized (offerBookListItems) { try { // setAll causes sometimes an UnsupportedOperationException // Investigate why.... offerBookListItems.clear(); offerBookListItems.addAll(offerBookService.getOffers().stream() .map(OfferBookListItem::new) .collect(Collectors.toList())); log.debug("offerBookListItems.size {}", offerBookListItems.size()); fillOfferCountMaps(); } catch (Throwable t) { log.error("Error at fillOfferBookListItems: " + t); } } } public void printOfferBookListItems(String msg) { synchronized (offerBookListItems) { if (log.isDebugEnabled()) { if (offerBookListItems.size() == 0) { log.debug("{} -> OfferBookListItems: none", msg); return; } StringBuilder stringBuilder = new StringBuilder(msg + " -> ").append("OfferBookListItems:").append("\n"); offerBookListItems.forEach(i -> stringBuilder.append("\t").append(i.toString()).append("\n")); stringBuilder.deleteCharAt(stringBuilder.length() - 1); log.debug(stringBuilder.toString()); } } } public Map getBuyOfferCountMap() { return buyOfferCountMap; } public Map getSellOfferCountMap() { return sellOfferCountMap; } public void fillOfferCountMaps() { buyOfferCountMap.clear(); sellOfferCountMap.clear(); final String[] ccyCode = new String[1]; final int[] offerCount = new int[1]; offerBookListItems.forEach(o -> { ccyCode[0] = o.getOffer().getCounterCurrencyCode(); if (o.getOffer().getDirection() == BUY) { offerCount[0] = buyOfferCountMap.getOrDefault(ccyCode[0], 0) + 1; buyOfferCountMap.put(ccyCode[0], offerCount[0]); } else { offerCount[0] = sellOfferCountMap.getOrDefault(ccyCode[0], 0) + 1; sellOfferCountMap.put(ccyCode[0], offerCount[0]); } }); log.debug("buyOfferCountMap.size {} sellOfferCountMap.size {}", buyOfferCountMap.size(), sellOfferCountMap.size()); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookListItem.java ================================================ /* * 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 . */ package haveno.desktop.main.offer.offerbook; import de.jensd.fx.glyphs.GlyphIcons; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import haveno.core.account.sign.SignedWitnessService; import haveno.core.account.witness.AccountAgeWitness; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.offer.Offer; import haveno.core.payment.payload.PaymentMethod; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.storage.P2PDataStorage; import lombok.Getter; import lombok.Value; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import java.util.Date; import java.util.Optional; import java.util.concurrent.TimeUnit; @Slf4j public class OfferBookListItem { @Getter private final Offer offer; /** * The protected storage (offer) payload hash helps prevent edited offers from being * mistakenly removed from a UI user's OfferBook list if the API's 'editoffer' * command results in onRemoved(offer) being called after onAdded(offer) on peers. * (Checking the offer-id is not enough.) This msg order problem does not happen * when the UI edits an offer because the remove/add msgs are always sent in separate * envelope bundles. It can happen when the API is used to edit an offer because * the remove/add msgs are received in the same envelope bundle, then processed in * unpredictable order. */ @Getter P2PDataStorage.ByteArray hashOfPayload; // We cache the data once created for performance reasons. AccountAgeWitnessService calls can // be a bit expensive. private WitnessAgeData witnessAgeData; public OfferBookListItem(Offer offer) { this.offer = offer; this.hashOfPayload = new P2PDataStorage.ByteArray(offer.getOfferPayload().getHash()); } public WitnessAgeData getWitnessAgeData(AccountAgeWitnessService accountAgeWitnessService, SignedWitnessService signedWitnessService) { if (witnessAgeData == null) { if (CurrencyUtil.isCryptoCurrency(offer.getCounterCurrencyCode())) { witnessAgeData = new WitnessAgeData(WitnessAgeData.TYPE_CRYPTOS); } else if (PaymentMethod.hasChargebackRisk(offer.getPaymentMethod(), offer.getCounterCurrencyCode())) { // Fiat and signed witness required Optional optionalWitness = accountAgeWitnessService.findWitness(offer); AccountAgeWitnessService.SignState signState = optionalWitness .map(accountAgeWitnessService::getSignState) .orElse(AccountAgeWitnessService.SignState.UNSIGNED); boolean isSignedAccountAgeWitness = optionalWitness .map(signedWitnessService::isSignedAccountAgeWitness) .orElse(false); if (isSignedAccountAgeWitness || !signState.equals(AccountAgeWitnessService.SignState.UNSIGNED)) { // either signed & limits lifted, or waiting for limits to be lifted // Or banned witnessAgeData = new WitnessAgeData( signState.isLimitLifted() ? WitnessAgeData.TYPE_SIGNED_AND_LIMIT_LIFTED : WitnessAgeData.TYPE_SIGNED_OR_BANNED, optionalWitness.map(witness -> accountAgeWitnessService.getWitnessSignAge(witness, new Date())).orElse(0L), signState); } else { witnessAgeData = new WitnessAgeData( WitnessAgeData.TYPE_NOT_SIGNED, optionalWitness.map(e -> accountAgeWitnessService.getAccountAge(e, new Date())).orElse(0L), signState ); } } else { // Fiat, no signed witness required, we show account age witnessAgeData = new WitnessAgeData( WitnessAgeData.TYPE_NOT_SIGNING_REQUIRED, accountAgeWitnessService.getAccountAge(offer) ); } } return witnessAgeData; } @Override public String toString() { return "OfferBookListItem{" + "offerId=" + offer.getId() + ", hashOfPayload=" + hashOfPayload.getHex() + ", witnessAgeData=" + (witnessAgeData == null ? "null" : witnessAgeData.displayString) + '}'; } @Value public static class WitnessAgeData implements Comparable { String displayString; String info; GlyphIcons icon; // Used for sorting Long type; // Used for sorting Long days; public static final long TYPE_SIGNED_AND_LIMIT_LIFTED = 4L; public static final long TYPE_SIGNED_OR_BANNED = 3L; public static final long TYPE_NOT_SIGNED = 2L; public static final long TYPE_NOT_SIGNING_REQUIRED = 1L; public static final long TYPE_CRYPTOS = 0L; public WitnessAgeData(long type) { this(type, 0, null); } public WitnessAgeData(long type, long days) { this(type, days, null); } public WitnessAgeData(long type, long age, AccountAgeWitnessService.SignState signState) { this.type = type; long days = age > -1 ? TimeUnit.MILLISECONDS.toDays(age) : 0; this.days = days; if (type == TYPE_SIGNED_AND_LIMIT_LIFTED) { this.displayString = Res.get("offerbook.timeSinceSigning.daysSinceSigning", days); this.info = Res.get("offerbook.timeSinceSigning.tooltip.info.signedAndLifted"); this.icon = GUIUtil.getIconForSignState(signState); } else if (type == TYPE_SIGNED_OR_BANNED) { this.displayString = Res.get("offerbook.timeSinceSigning.daysSinceSigning", days); this.info = Res.get("offerbook.timeSinceSigning.tooltip.info.signed"); this.icon = GUIUtil.getIconForSignState(signState); } else if (type == TYPE_NOT_SIGNED) { this.displayString = Res.get("offerbook.timeSinceSigning.notSigned"); this.info = Res.get("offerbook.timeSinceSigning.tooltip.info.unsigned"); this.icon = GUIUtil.getIconForSignState(signState); } else if (type == TYPE_NOT_SIGNING_REQUIRED) { this.displayString = Res.get("offerbook.timeSinceSigning.notSigned.ageDays", days); this.info = Res.get("shared.notSigned.noNeedDays", days); this.icon = MaterialDesignIcon.CHECKBOX_MARKED_OUTLINE; } else { this.displayString = Res.get("offerbook.timeSinceSigning.notSigned.noNeed"); this.info = Res.get("shared.notSigned.noNeedAlts"); this.icon = MaterialDesignIcon.INFORMATION_OUTLINE; } } public boolean isAccountSigned() { return this.type == TYPE_SIGNED_AND_LIMIT_LIFTED || this.type == TYPE_SIGNED_OR_BANNED; } public boolean isLimitLifted() { return this.type == TYPE_SIGNED_AND_LIMIT_LIFTED; } public boolean isSigningRequired() { return this.type != TYPE_NOT_SIGNING_REQUIRED && this.type != TYPE_CRYPTOS; } @Override public int compareTo(@NotNull WitnessAgeData o) { return (int) (this.type.equals(o.getType()) ? this.days - o.getDays() : this.type - o.getType()); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java ================================================ /* * 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 . */ package haveno.desktop.main.offer.offerbook; import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIconView; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.util.Tuple3; import haveno.core.account.sign.SignedWitnessService; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.alert.PrivateNotificationManager; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.monetary.Price; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OfferFilterService; import haveno.core.offer.OfferRestrictions; import haveno.core.offer.OpenOffer; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentMethod; import haveno.core.trade.HavenoUtils; import haveno.core.user.DontShowAgainLookup; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.Navigation; import haveno.desktop.common.view.ActivatableViewAndModel; import haveno.desktop.components.AccountStatusTooltipLabel; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.AutoTooltipTableColumn; import haveno.desktop.components.AutoTooltipTextField; import haveno.desktop.components.AutocompleteComboBox; import haveno.desktop.components.ColoredDecimalPlacesWithZerosText; import haveno.desktop.components.HyperlinkWithIcon; import haveno.desktop.components.InfoAutoTooltipLabel; import haveno.desktop.components.PeerInfoIconTrading; import haveno.desktop.main.MainView; import haveno.desktop.main.account.AccountView; import haveno.desktop.main.account.content.cryptoaccounts.CryptoAccountsView; import haveno.desktop.main.account.content.traditionalaccounts.TraditionalAccountsView; import haveno.desktop.main.funds.FundsView; import haveno.desktop.main.funds.withdrawal.WithdrawalView; import haveno.desktop.main.offer.OfferView; import haveno.desktop.main.offer.OfferViewUtil; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.OfferDetailsWindow; import haveno.desktop.main.portfolio.PortfolioView; import haveno.desktop.main.portfolio.editoffer.EditOfferView; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.NodeAddress; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.geometry.VPos; import javafx.scene.canvas.Canvas; import javafx.scene.control.ComboBox; import javafx.scene.control.ContentDisplay; import javafx.scene.control.Label; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableRow; import javafx.scene.control.TableView; import javafx.scene.control.ToggleButton; import javafx.scene.control.Tooltip; import javafx.scene.image.ImageView; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.scene.text.TextAlignment; import javafx.util.Callback; import javafx.util.StringConverter; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; import org.fxmisc.easybind.monadic.MonadicBinding; import org.jetbrains.annotations.NotNull; import java.math.BigInteger; import java.util.Comparator; import java.util.Map; import java.util.Optional; import static haveno.desktop.util.FormBuilder.addTopLabelAutoToolTipTextField; abstract public class OfferBookView extends ActivatableViewAndModel { private final Navigation navigation; private final OfferDetailsWindow offerDetailsWindow; private final CoinFormatter formatter; private final PrivateNotificationManager privateNotificationManager; private final boolean useDevPrivilegeKeys; private final AccountAgeWitnessService accountAgeWitnessService; private final SignedWitnessService signedWitnessService; protected AutocompleteComboBox currencyComboBox; private AutocompleteComboBox paymentMethodComboBox; private AutoTooltipButton createOfferButton; private AutoTooltipTextField filterInputField; private ToggleButton matchingOffersToggleButton; private ToggleButton noDepositOffersToggleButton; private AutoTooltipTableColumn amountColumn; private AutoTooltipTableColumn volumeColumn; private AutoTooltipTableColumn marketColumn; private AutoTooltipTableColumn priceColumn; private AutoTooltipTableColumn depositColumn; private AutoTooltipTableColumn signingStateColumn; private AutoTooltipTableColumn avatarColumn; private TableView tableView; private int gridRow = 0; private Label nrOfOffersLabel; private ListChangeListener offerListListener; private ChangeListener priceFeedUpdateCounterListener; private Subscription currencySelectionSubscriber; private static final int SHOW_ALL = 0; private Label disabledCreateOfferButtonTooltip; protected VBox currencyComboBoxContainer; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// OfferBookView(M model, Navigation navigation, OfferDetailsWindow offerDetailsWindow, CoinFormatter formatter, PrivateNotificationManager privateNotificationManager, boolean useDevPrivilegeKeys, AccountAgeWitnessService accountAgeWitnessService, SignedWitnessService signedWitnessService) { super(model); this.navigation = navigation; this.offerDetailsWindow = offerDetailsWindow; this.formatter = formatter; this.privateNotificationManager = privateNotificationManager; this.useDevPrivilegeKeys = useDevPrivilegeKeys; this.accountAgeWitnessService = accountAgeWitnessService; this.signedWitnessService = signedWitnessService; } @Override public void initialize() { root.setPadding(new Insets(15, 15, 5, 15)); HBox offerToolsBox = new HBox(); offerToolsBox.setAlignment(Pos.BOTTOM_LEFT); offerToolsBox.setSpacing(10); offerToolsBox.setPadding(new Insets(0, 0, 0, 0)); Tuple3> currencyBoxTuple = FormBuilder.addTopLabelAutocompleteComboBox( Res.get("offerbook.filterByCurrency")); currencyComboBoxContainer = currencyBoxTuple.first; currencyComboBox = currencyBoxTuple.third; currencyComboBox.setPrefWidth(250); currencyComboBox.getStyleClass().add("input-with-border"); Tuple3> paymentBoxTuple = FormBuilder.addTopLabelAutocompleteComboBox( Res.get("offerbook.filterByPaymentMethod")); paymentMethodComboBox = paymentBoxTuple.third; paymentMethodComboBox.setCellFactory(GUIUtil.getPaymentMethodCellFactory()); paymentMethodComboBox.setPrefWidth(250); paymentMethodComboBox.getStyleClass().add("input-with-border"); noDepositOffersToggleButton = new ToggleButton(Res.get("offerbook.filterNoDeposit")); noDepositOffersToggleButton.getStyleClass().add("toggle-button-no-slider"); Tooltip noDepositOffersTooltip = new Tooltip(Res.get("offerbook.noDepositOffers")); Tooltip.install(noDepositOffersToggleButton, noDepositOffersTooltip); matchingOffersToggleButton = AwesomeDude.createIconToggleButton(AwesomeIcon.USER, null, "1.5em", null); matchingOffersToggleButton.getStyleClass().add("toggle-button-no-slider"); Tooltip matchingOffersTooltip = new Tooltip(Res.get("offerbook.matchingOffers")); Tooltip.install(matchingOffersToggleButton, matchingOffersTooltip); createOfferButton = new AutoTooltipButton(""); createOfferButton.setMinHeight(40); createOfferButton.setGraphicTextGap(10); createOfferButton.setStyle("-fx-padding: 7 25 7 25;"); disabledCreateOfferButtonTooltip = new Label(""); disabledCreateOfferButtonTooltip.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); disabledCreateOfferButtonTooltip.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); disabledCreateOfferButtonTooltip.prefWidthProperty().bind(createOfferButton.widthProperty()); disabledCreateOfferButtonTooltip.prefHeightProperty().bind(createOfferButton.heightProperty()); disabledCreateOfferButtonTooltip.setTooltip(new Tooltip(Res.get("offerbook.createOfferDisabled.tooltip"))); disabledCreateOfferButtonTooltip.setManaged(false); disabledCreateOfferButtonTooltip.setVisible(false); var createOfferVBox = new VBox(createOfferButton, disabledCreateOfferButtonTooltip); createOfferVBox.setAlignment(Pos.BOTTOM_RIGHT); Tuple3 autoToolTipTextField = addTopLabelAutoToolTipTextField(""); VBox filterBox = autoToolTipTextField.first; filterInputField = autoToolTipTextField.third; filterInputField.setPromptText(Res.get("shared.filter")); filterInputField.getStyleClass().add("input-with-border"); offerToolsBox.getChildren().addAll(currencyBoxTuple.first, paymentBoxTuple.first, filterBox, noDepositOffersToggleButton, matchingOffersToggleButton, getSpacer(), createOfferVBox); GridPane.setHgrow(offerToolsBox, Priority.ALWAYS); GridPane.setRowIndex(offerToolsBox, gridRow); GridPane.setColumnSpan(offerToolsBox, 2); GridPane.setMargin(offerToolsBox, new Insets(0, 0, 0, 0)); root.getChildren().add(offerToolsBox); tableView = new TableView<>(); GUIUtil.applyTableStyle(tableView); GridPane.setRowIndex(tableView, ++gridRow); GridPane.setColumnIndex(tableView, 0); GridPane.setColumnSpan(tableView, 2); GridPane.setMargin(tableView, new Insets(10, 0, -10, 0)); GridPane.setVgrow(tableView, Priority.ALWAYS); root.getChildren().add(tableView); marketColumn = getMarketColumn(); priceColumn = getPriceColumn(); tableView.getColumns().add(priceColumn); amountColumn = getAmountColumn(); tableView.getColumns().add(amountColumn); volumeColumn = getVolumeColumn(); tableView.getColumns().add(volumeColumn); AutoTooltipTableColumn paymentMethodColumn = getPaymentMethodColumn(); tableView.getColumns().add(paymentMethodColumn); depositColumn = getDepositColumn(); tableView.getColumns().add(depositColumn); signingStateColumn = getSigningStateColumn(); tableView.getColumns().add(signingStateColumn); avatarColumn = getAvatarColumn(); tableView.getColumns().add(getActionColumn()); tableView.getColumns().add(avatarColumn); tableView.getSortOrder().add(priceColumn); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); Label placeholder = new AutoTooltipLabel(Res.get("table.placeholder.noItems", Res.get("shared.multipleOffers"))); placeholder.setWrapText(true); tableView.setPlaceholder(placeholder); marketColumn.setComparator(Comparator.comparing( o -> CurrencyUtil.getCurrencyPair(o.getOffer().getCounterCurrencyCode()), Comparator.nullsFirst(Comparator.naturalOrder()) )); // We sort by % so we can also sort if SHOW ALL is selected Comparator marketBasedPriceComparator = (o1, o2) -> { Optional marketBasedPrice1 = model.getMarketBasedPrice(o1.getOffer()); Optional marketBasedPrice2 = model.getMarketBasedPrice(o2.getOffer()); if (marketBasedPrice1.isPresent() && marketBasedPrice2.isPresent()) { return Double.compare(marketBasedPrice1.get(), marketBasedPrice2.get()); } else { return 0; } }; // If we do not have a % price we use only fix price and sort by that priceColumn.setComparator(marketBasedPriceComparator.thenComparing((o1, o2) -> { Price price2 = o2.getOffer().getPrice(); Price price1 = o1.getOffer().getPrice(); if (price2 == null || price1 == null) { return 0; } if (OfferViewUtil.isShownAsSellOffer(model.getSelectedTradeCurrency().getCode(), model.getDirection())) { return price1.compareTo(price2); } else { return price2.compareTo(price1); } })); amountColumn.setComparator(Comparator.comparing(o -> o.getOffer().getMinAmount())); volumeColumn.setComparator(Comparator.comparing(o -> o.getOffer().getMinVolume(), Comparator.nullsFirst(Comparator.naturalOrder()))); paymentMethodColumn.setComparator(Comparator.comparing(o -> Res.get(o.getOffer().getPaymentMethod().getId()))); avatarColumn.setComparator(Comparator.comparing(o -> model.getNumTrades(o.getOffer()))); depositColumn.setComparator(Comparator.comparing(item -> { boolean isSellOffer = item.getOffer().getDirection() == OfferDirection.SELL; BigInteger deposit = isSellOffer ? item.getOffer().getMaxBuyerSecurityDeposit() : item.getOffer().getMaxSellerSecurityDeposit(); long amountValue = item.getOffer().getAmount().longValueExact(); if ((deposit == null || amountValue == 0)) { return 0d; } else { return HavenoUtils.divide(deposit, BigInteger.valueOf(amountValue)); } }, Comparator.nullsFirst(Comparator.naturalOrder()))); signingStateColumn.setComparator(Comparator.comparing(e -> e.getWitnessAgeData(accountAgeWitnessService, signedWitnessService), Comparator.nullsFirst(Comparator.naturalOrder()))); nrOfOffersLabel = new AutoTooltipLabel(""); nrOfOffersLabel.setId("num-offers"); GridPane.setHalignment(nrOfOffersLabel, HPos.LEFT); GridPane.setVgrow(nrOfOffersLabel, Priority.NEVER); GridPane.setValignment(nrOfOffersLabel, VPos.TOP); GridPane.setRowIndex(nrOfOffersLabel, ++gridRow); GridPane.setColumnIndex(nrOfOffersLabel, 0); GridPane.setMargin(nrOfOffersLabel, new Insets(10, 0, 0, 0)); root.getChildren().add(nrOfOffersLabel); offerListListener = c -> UserThread.execute(() -> nrOfOffersLabel.setText(Res.get("offerbook.nrOffers", model.getOfferList().size()))); // Fixes incorrect ordering of Available offers: // https://github.com/bisq-network/bisq-desktop/issues/588 priceFeedUpdateCounterListener = (observable, oldValue, newValue) -> tableView.sort(); } abstract protected String getMarketTitle(); @Override protected void activate() { Map offerCounts = model.getOfferCounts(); currencyComboBox.setCellFactory(GUIUtil.getTradeCurrencyCellFactory(Res.get("shared.oneOffer"), Res.get("shared.multipleOffers"), offerCounts)); currencyComboBox.setConverter(new CurrencyStringConverter(currencyComboBox)); currencyComboBox.getEditor().getStyleClass().add("combo-box-editor-bold"); currencyComboBox.setAutocompleteItems(model.getTradeCurrencies(), model.getAllCurrencies()); currencyComboBox.setVisibleRowCount(Math.min(currencyComboBox.getItems().size(), 10)); currencyComboBox.setOnChangeConfirmed(e -> { if (currencyComboBox.getEditor().getText().isEmpty()) return; model.onSetTradeCurrency(currencyComboBox.getSelectionModel().getSelectedItem()); paymentMethodComboBox.setAutocompleteItems(model.getPaymentMethods()); model.updateSelectedPaymentMethod(); updatePaymentMethodComboBoxEditor(); model.onSetPaymentMethod(paymentMethodComboBox.getSelectionModel().getSelectedItem()); updateCreateOfferButton(); }); updateCurrencyComboBoxFromModel(); currencyComboBox.getEditor().setText(new CurrencyStringConverter(currencyComboBox).toString(currencyComboBox.getSelectionModel().getSelectedItem())); matchingOffersToggleButton.setSelected(model.useOffersMatchingMyAccountsFilter); matchingOffersToggleButton.disableProperty().bind(model.disableMatchToggle); matchingOffersToggleButton.setOnAction(e -> model.onShowOffersMatchingMyAccounts(matchingOffersToggleButton.isSelected())); noDepositOffersToggleButton.setSelected(model.showPrivateOffers); noDepositOffersToggleButton.setOnAction(e -> model.onShowPrivateOffers(noDepositOffersToggleButton.isSelected())); model.getOfferList().comparatorProperty().bind(tableView.comparatorProperty()); amountColumn.sortTypeProperty().addListener((observable, oldValue, newValue) -> { if (newValue == TableColumn.SortType.DESCENDING) { amountColumn.setComparator(Comparator.comparing(o -> o.getOffer().getAmount(), Comparator.nullsFirst(Comparator.naturalOrder()))); } else { amountColumn.setComparator(Comparator.comparing(o -> o.getOffer().getMinAmount(), Comparator.nullsFirst(Comparator.naturalOrder()))); } }); volumeColumn.sortTypeProperty().addListener((observable, oldValue, newValue) -> { if (newValue == TableColumn.SortType.DESCENDING) { volumeColumn.setComparator(Comparator.comparing(o -> o.getOffer().getVolume(), Comparator.nullsFirst(Comparator.naturalOrder()))); } else { volumeColumn.setComparator(Comparator.comparing(o -> o.getOffer().getMinVolume(), Comparator.nullsFirst(Comparator.naturalOrder()))); } }); paymentMethodComboBox.setConverter(new PaymentMethodStringConverter(paymentMethodComboBox)); paymentMethodComboBox.getEditor().getStyleClass().add("combo-box-editor-bold"); paymentMethodComboBox.setAutocompleteItems(model.getPaymentMethods()); paymentMethodComboBox.setVisibleRowCount(Math.min(paymentMethodComboBox.getItems().size(), 10)); paymentMethodComboBox.setOnChangeConfirmed(e -> { if (paymentMethodComboBox.getEditor().getText().isEmpty()) paymentMethodComboBox.getSelectionModel().select(SHOW_ALL); model.onSetPaymentMethod(paymentMethodComboBox.getSelectionModel().getSelectedItem()); updateCurrencyComboBoxFromModel(); updateSigningStateColumn(); }); updatePaymentMethodComboBoxEditor(); createOfferButton.setOnAction(e -> onCreateOffer()); MonadicBinding currencySelectionBinding = EasyBind.combine( model.showAllTradeCurrenciesProperty, model.tradeCurrencyCode, (showAll, code) -> { if (showAll) { volumeColumn.setTitleWithHelpText(Res.get("shared.amountMinMax"), Res.get("shared.amountHelp")); priceColumn.setTitle(Res.get("shared.price")); if (!tableView.getColumns().contains(marketColumn)) tableView.getColumns().add(0, marketColumn); } else { volumeColumn.setTitleWithHelpText(Res.get("offerbook.volume", code), Res.get("shared.amountHelp")); priceColumn.setTitle(CurrencyUtil.getPriceWithCurrencyCode(code)); tableView.getColumns().remove(marketColumn); } updateSigningStateColumn(); return null; }); currencySelectionSubscriber = currencySelectionBinding.subscribe((observable, oldValue, newValue) -> { }); UserThread.execute(() -> tableView.setItems(model.getOfferList())); model.getOfferList().addListener(offerListListener); nrOfOffersLabel.setText(Res.get("offerbook.nrOffers", model.getOfferList().size())); model.priceFeedService.updateCounterProperty().addListener(priceFeedUpdateCounterListener); filterInputField.setOnKeyTyped(event -> { model.onFilterKeyTyped(filterInputField.getText()); }); } private void updatePaymentMethodComboBoxEditor() { if (model.showAllPaymentMethods) paymentMethodComboBox.getSelectionModel().select(SHOW_ALL); else paymentMethodComboBox.getSelectionModel().select(model.selectedPaymentMethod); paymentMethodComboBox.getEditor().setText(new PaymentMethodStringConverter(paymentMethodComboBox).toString(paymentMethodComboBox.getSelectionModel().getSelectedItem())); } protected void updateCurrencyComboBoxFromModel() { if (model.showAllTradeCurrenciesProperty.get()) { currencyComboBox.getSelectionModel().select(SHOW_ALL); } else { currencyComboBox.getSelectionModel().select(model.getSelectedTradeCurrency()); } } private void updateSigningStateColumn() { if (model.hasSelectionAccountSigning()) { if (!tableView.getColumns().contains(signingStateColumn)) { tableView.getColumns().add(tableView.getColumns().indexOf(depositColumn) + 1, signingStateColumn); } } else { tableView.getColumns().remove(signingStateColumn); } } @Override protected void deactivate() { createOfferButton.setOnAction(null); matchingOffersToggleButton.setOnAction(null); matchingOffersToggleButton.disableProperty().unbind(); noDepositOffersToggleButton.setOnAction(null); noDepositOffersToggleButton.disableProperty().unbind(); model.getOfferList().comparatorProperty().unbind(); volumeColumn.sortableProperty().unbind(); priceColumn.sortableProperty().unbind(); amountColumn.sortableProperty().unbind(); model.getOfferList().comparatorProperty().unbind(); model.getOfferList().removeListener(offerListListener); model.priceFeedService.updateCounterProperty().removeListener(priceFeedUpdateCounterListener); currencySelectionSubscriber.unsubscribe(); } static class CurrencyStringConverter extends StringConverter { private final ComboBox comboBox; CurrencyStringConverter(ComboBox comboBox) { this.comboBox = comboBox; } @Override public String toString(TradeCurrency item) { return item != null ? asString(item) : ""; } @Override public TradeCurrency fromString(String query) { if (comboBox.getItems().isEmpty()) return null; if (query.isEmpty()) return null; return comboBox.getItems().stream(). filter(item -> asString(item).equals(query)). findAny().orElse(null); } private String asString(TradeCurrency item) { if (isSpecialShowAllItem(item)) return Res.get(GUIUtil.SHOW_ALL_FLAG); if (isSpecialEditItem(item)) return Res.get(GUIUtil.EDIT_FLAG); return item.getName() + " (" + item.getCode() + ")"; } private boolean isSpecialShowAllItem(TradeCurrency item) { return item.getCode().equals(GUIUtil.SHOW_ALL_FLAG); } private boolean isSpecialEditItem(TradeCurrency item) { return item.getCode().equals(GUIUtil.EDIT_FLAG); } private TradeCurrency specialShowAllItem() { return comboBox.getItems().get(SHOW_ALL); } } static class PaymentMethodStringConverter extends StringConverter { private final ComboBox comboBox; PaymentMethodStringConverter(ComboBox comboBox) { this.comboBox = comboBox; } @Override public String toString(PaymentMethod item) { return item != null ? asString(item) : ""; } @Override public PaymentMethod fromString(String query) { if (comboBox.getItems().isEmpty()) return null; if (query.isEmpty()) return specialShowAllItem(); return comboBox.getItems().stream(). filter(item -> asString(item).equals(query)). findAny().orElse(null); } private String asString(PaymentMethod item) { if (isSpecialShowAllItem(item)) return Res.get(GUIUtil.SHOW_ALL_FLAG); return Res.get(item.getId()); } private boolean isSpecialShowAllItem(PaymentMethod item) { return item.getId().equals(GUIUtil.SHOW_ALL_FLAG); } private PaymentMethod specialShowAllItem() { return comboBox.getItems().get(SHOW_ALL); } } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void enableCreateOfferButton() { createOfferButton.setDisable(false); disabledCreateOfferButtonTooltip.setManaged(false); disabledCreateOfferButtonTooltip.setVisible(false); } private void disableCreateOfferButton() { createOfferButton.setDisable(true); disabledCreateOfferButtonTooltip.setManaged(true); disabledCreateOfferButtonTooltip.setVisible(true); model.onCreateOffer(); } public void setDirection(OfferDirection direction) { model.initWithDirection(direction); createOfferButton.setGraphic(GUIUtil.getCurrencyIconWithBorder(Res.getBaseCurrencyCode())); createOfferButton.setContentDisplay(ContentDisplay.RIGHT); createOfferButton.setId(direction == OfferDirection.SELL ? "sell-button-big" : "buy-button-big"); avatarColumn.setTitle(direction == OfferDirection.SELL ? Res.get("shared.buyerUpperCase") : Res.get("shared.sellerUpperCase")); if (direction == OfferDirection.SELL) { noDepositOffersToggleButton.setVisible(false); noDepositOffersToggleButton.setManaged(false); } } public void setOfferActionHandler(OfferView.OfferActionHandler offerActionHandler) { model.setOfferActionHandler(offerActionHandler); } public void onTabSelected(boolean isSelected) { if (model.isTabSelected == isSelected) { return; } model.onTabSelected(isSelected); if (isSelected) { updateCurrencyComboBoxFromModel(); root.requestFocus(); updateCreateOfferButton(); } updateCreateOfferButton(); } /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// private void onCreateOffer() { if (model.canCreateOrTakeOffer()) { if (!model.hasPaymentAccountForCurrency()) { new Popup().headLine(Res.get("offerbook.warning.noTradingAccountForCurrency.headline")) .instruction(Res.get("offerbook.warning.noTradingAccountForCurrency.msg")) .actionButtonText(Res.get("offerbook.setupNewAccount")) .onAction(() -> { navigation.setReturnPath(navigation.getCurrentPath()); if (CurrencyUtil.isTraditionalCurrency(model.getSelectedTradeCurrency().getCode())) { navigation.navigateTo(MainView.class, AccountView.class, TraditionalAccountsView.class); } else { navigation.navigateTo(MainView.class, AccountView.class, CryptoAccountsView.class); } }) .width(725) .show(); return; } disableCreateOfferButton(); } } private void onShowInfo(Offer offer, OfferFilterService.Result result) { switch (result) { case API_DISABLED: DevEnv.logErrorAndThrowIfDevMode("We are in desktop and in the taker position " + "viewing offers, so it cannot be that we got that result as we are not an API user."); break; case HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER: openPopupForMissingAccountSetup(offer); break; case HAS_NOT_SAME_PROTOCOL_VERSION: new Popup().warning(Res.get("offerbook.warning.wrongTradeProtocol")).show(); break; case IS_IGNORED: new Popup().warning(Res.get("offerbook.warning.userIgnored")).show(); break; case IS_OFFER_BANNED: new Popup().warning(Res.get("offerbook.warning.offerBlocked")).show(); break; case IS_CURRENCY_BANNED: new Popup().warning(Res.get("offerbook.warning.currencyBanned")).show(); break; case IS_PAYMENT_METHOD_BANNED: new Popup().warning(Res.get("offerbook.warning.paymentMethodBanned")).show(); break; case IS_NODE_ADDRESS_BANNED: new Popup().warning(Res.get("offerbook.warning.nodeBlocked")).show(); break; case REQUIRE_UPDATE_TO_NEW_VERSION: new Popup().warning(Res.get("offerbook.warning.requireUpdateToNewVersion")).show(); break; case IS_INSUFFICIENT_COUNTERPARTY_TRADE_LIMIT: new Popup().warning(Res.get("offerbook.warning.counterpartyTradeRestrictions")).show(); break; case IS_MY_INSUFFICIENT_TRADE_LIMIT: Optional account = model.getMostMaturePaymentAccountForOffer(offer); if (account.isPresent()) { long tradeLimit = model.accountAgeWitnessService.getMyTradeLimit(account.get(), offer.getCounterCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()); new Popup() .warning(Res.get("popup.warning.tradeLimitDueAccountAgeRestriction.buyer", HavenoUtils.formatXmr(tradeLimit, true), Res.get("offerbook.warning.newVersionAnnouncement"))) .show(); } else { DevEnv.logErrorAndThrowIfDevMode("We don't found a payment account but got called the " + "isInsufficientTradeLimit case."); } break; case ARBITRATOR_NOT_VALIDATED: new Popup().warning(Res.get("offerbook.warning.arbitratorNotValidated")).show(); break; case SIGNATURE_NOT_VALIDATED: new Popup().warning(Res.get("offerbook.warning.signatureNotValidated")).show(); break; case RESERVE_FUNDS_SPENT: new Popup().warning(Res.get("offerbook.warning.reserveFundsSpent")).show(); break; case VALID: break; default: log.warn("Unhandled offer filter service result: " + result); break; } } private void onTakeOffer(Offer offer) { if (model.canCreateOrTakeOffer()) { if (offer.getDirection() == OfferDirection.SELL && offer.getPaymentMethod().getId().equals(PaymentMethod.CASH_DEPOSIT.getId())) { new Popup().confirmation(Res.get("popup.info.cashDepositInfo", offer.getBankId())) .actionButtonText(Res.get("popup.info.cashDepositInfo.confirm")) .onAction(() -> model.onTakeOffer(offer)) .show(); } else { model.onTakeOffer(offer); } } } private void onRemoveOpenOffer(Offer offer) { if (model.isBootstrappedOrShowPopup()) { String key = "RemoveOfferWarning"; if (DontShowAgainLookup.showAgain(key)) { String message = Res.get("popup.warning.removeOffer"); new Popup().warning(message) .actionButtonText(Res.get("shared.removeOffer")) .onAction(() -> doRemoveOffer(offer)) .closeButtonText(Res.get("shared.dontRemoveOffer")) .dontShowAgainId(key) .show(); } else { doRemoveOffer(offer); } } } private void onEditOpenOffer(Offer offer) { OpenOffer openOffer = model.getOpenOffer(offer); if (openOffer != null) { navigation.navigateToWithData(openOffer, MainView.class, PortfolioView.class, EditOfferView.class); } } private void doRemoveOffer(Offer offer) { String key = "WithdrawFundsAfterRemoveOfferInfo"; model.onRemoveOpenOffer(offer, () -> { log.debug(Res.get("offerbook.removeOffer.success")); if (DontShowAgainLookup.showAgain(key)) new Popup().instruction(Res.get("offerbook.withdrawFundsHint", Res.get("funds.tab.withdrawal"))) .actionButtonTextWithGoTo("funds.tab.withdrawal") .onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, WithdrawalView.class)) .dontShowAgainId(key) .show(); }, (message) -> { log.error(message); new Popup().warning(Res.get("offerbook.removeOffer.failed", message)).show(); }); } private void openPopupForMissingAccountSetup(Offer offer) { String headline = Res.get("offerbook.warning.noMatchingAccount.headline"); var accountViewClass = offer.isTraditionalOffer() ? TraditionalAccountsView.class : CryptoAccountsView.class; new Popup().headLine(headline) .instruction(Res.get("offerbook.warning.noMatchingAccount.msg")) .actionButtonTextWithGoTo("mainView.menu.account") .onAction(() -> { navigation.setReturnPath(navigation.getCurrentPath()); navigation.navigateTo(MainView.class, AccountView.class, accountViewClass); }).show(); } /////////////////////////////////////////////////////////////////////////////////////////// // Table /////////////////////////////////////////////////////////////////////////////////////////// private AutoTooltipTableColumn getAmountColumn() { AutoTooltipTableColumn column = new AutoTooltipTableColumn<>(Res.get("shared.XMRMinMax"), Res.get("shared.amountHelp")); column.setMinWidth(100); column.setSortable(true); column.getStyleClass().add("number-column"); column.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final OfferBookListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) setGraphic(new ColoredDecimalPlacesWithZerosText(model.getAmount(item), GUIUtil.AMOUNT_DECIMALS_WITH_ZEROS)); else setGraphic(null); } }; } }); return column; } private AutoTooltipTableColumn getMarketColumn() { AutoTooltipTableColumn column = new AutoTooltipTableColumn<>(Res.get("shared.market")) { { setMinWidth(40); } }; column.getStyleClass().addAll("number-column"); column.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final OfferBookListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) setText(CurrencyUtil.getCurrencyPair(item.getOffer().getCounterCurrencyCode())); else setText(""); } }; } }); return column; } private ObservableValue asPriceDependentObservable(OfferBookListItem item) { return item.getOffer().isUseMarketBasedPrice() ? EasyBind.map(model.priceFeedService.updateCounterProperty(), n -> item) : new ReadOnlyObjectWrapper<>(item); } private AutoTooltipTableColumn getPriceColumn() { AutoTooltipTableColumn column = new AutoTooltipTableColumn<>("") { { setMinWidth(130); } }; column.getStyleClass().add("number-column"); column.setCellValueFactory(offer -> asPriceDependentObservable(offer.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final OfferBookListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setGraphic(getPriceAndPercentage(item)); } else { setGraphic(null); } } private HBox getPriceAndPercentage(OfferBookListItem item) { Offer offer = item.getOffer(); boolean useMarketBasedPrice = offer.isUseMarketBasedPrice(); boolean isShownAsBuyOffer = OfferViewUtil.isShownAsBuyOffer(offer); MaterialDesignIcon icon = useMarketBasedPrice ? MaterialDesignIcon.CHART_LINE : MaterialDesignIcon.LOCK; String info; if (useMarketBasedPrice) { double marketPriceMargin = offer.getMarketPriceMarginPct(); if (marketPriceMargin == 0) { if (isShownAsBuyOffer) { info = Res.get("offerbook.info.sellAtMarketPrice"); } else { info = Res.get("offerbook.info.buyAtMarketPrice"); } } else { String absolutePriceMargin = model.getAbsolutePriceMargin(offer); if (marketPriceMargin > 0) { if (isShownAsBuyOffer) { info = Res.get("offerbook.info.sellBelowMarketPrice", absolutePriceMargin); } else { info = Res.get("offerbook.info.buyAboveMarketPrice", absolutePriceMargin); } } else { if (isShownAsBuyOffer) { info = Res.get("offerbook.info.sellAboveMarketPrice", absolutePriceMargin); } else { info = Res.get("offerbook.info.buyBelowMarketPrice", absolutePriceMargin); } } } } else { if (isShownAsBuyOffer) { info = Res.get("offerbook.info.sellAtFixedPrice"); } else { info = Res.get("offerbook.info.buyAtFixedPrice"); } } InfoAutoTooltipLabel priceLabel = new InfoAutoTooltipLabel(model.getPrice(item), icon, ContentDisplay.RIGHT, info); priceLabel.setTextAlignment(TextAlignment.RIGHT); AutoTooltipLabel percentageLabel = new AutoTooltipLabel(model.getPriceAsPercentage(item)); percentageLabel.setOpacity(useMarketBasedPrice ? 1 : 0.4); HBox hBox = new HBox(); hBox.setSpacing(5); hBox.getChildren().addAll(priceLabel, percentageLabel); hBox.setAlignment(Pos.CENTER_LEFT); return hBox; } }; } }); return column; } private AutoTooltipTableColumn getVolumeColumn() { AutoTooltipTableColumn column = new AutoTooltipTableColumn<>("") { { setMinWidth(125); setSortable(true); } }; column.getStyleClass().add("number-column"); column.setCellValueFactory(offer -> asPriceDependentObservable(offer.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final OfferBookListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { if (item.getOffer().getPrice() == null) { setText(Res.get("shared.na")); setGraphic(null); } else { setText(""); ColoredDecimalPlacesWithZerosText volumeBox = new ColoredDecimalPlacesWithZerosText(model.getVolumeAmount(item), model.getNumberOfDecimalsForVolume(item)); if (model.showAllTradeCurrenciesProperty.get()) volumeBox.getChildren().add(new Label(" " + item.getOffer().getCounterCurrencyCode())); setGraphic(volumeBox); } } else { setText(""); setGraphic(null); } } }; } }); return column; } private AutoTooltipTableColumn getPaymentMethodColumn() { AutoTooltipTableColumn column = new AutoTooltipTableColumn<>(Res.get("shared.paymentMethod")) { { setMinWidth(80); } }; column.getStyleClass().add("number-column"); column.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { private HyperlinkWithIcon field; @Override public void updateItem(final OfferBookListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { Offer offer = item.getOffer(); if (model.isOfferBanned(offer)) { setGraphic(new AutoTooltipLabel(model.getPaymentMethod(item))); } else { field = new HyperlinkWithIcon(model.getPaymentMethod(item)); field.setOnAction(event -> { offerDetailsWindow.show(offer); }); field.setTooltip(new Tooltip(model.getPaymentMethodToolTip(item))); setGraphic(field); } } else { setGraphic(null); if (field != null) field.setOnAction(null); } } }; } }); return column; } private AutoTooltipTableColumn getDepositColumn() { AutoTooltipTableColumn column = new AutoTooltipTableColumn<>( Res.get("offerbook.deposit"), Res.get("offerbook.deposit.help")) { { setMinWidth(70); setSortable(true); } }; column.getStyleClass().add("number-column"); column.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final OfferBookListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { var isSellOffer = item.getOffer().getDirection() == OfferDirection.SELL; var deposit = isSellOffer ? item.getOffer().getMaxBuyerSecurityDeposit() : item.getOffer().getMaxSellerSecurityDeposit(); if (deposit == null) { setText(Res.get("shared.na")); setGraphic(null); } else { setText(""); String rangePrefix = item.getOffer().isRange() ? "<= " : ""; setGraphic(new ColoredDecimalPlacesWithZerosText(rangePrefix + model.formatDepositString( deposit, item.getOffer().getAmount().longValueExact()), GUIUtil.AMOUNT_DECIMALS_WITH_ZEROS)); } } else { setText(""); setGraphic(null); } } }; } }); return column; } private TableColumn getActionColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("shared.actions")) { { setMinWidth(180); setSortable(false); } }; column.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { OfferFilterService.Result canTakeOfferResult = null; @Override public void updateItem(final OfferBookListItem item, boolean empty) { super.updateItem(item, empty); final ImageView iconView = new ImageView(); final AutoTooltipButton button = new AutoTooltipButton(); { button.setGraphic(iconView); button.setGraphicTextGap(10); button.setPrefWidth(10000); } MaterialDesignIconView iconView2 = new MaterialDesignIconView(MaterialDesignIcon.PENCIL); final AutoTooltipButton button2 = new AutoTooltipButton(); { button2.setGraphic(iconView2); button2.setGraphicTextGap(10); button2.setPrefWidth(10000); } final HBox hbox = new HBox(); { hbox.setSpacing(8); hbox.setAlignment(Pos.CENTER); hbox.getChildren().add(button); hbox.getChildren().add(button2); HBox.setHgrow(button, Priority.ALWAYS); HBox.setHgrow(button2, Priority.ALWAYS); } TableRow tableRow = getTableRow(); if (item != null && !empty) { Offer offer = item.getOffer(); boolean myOffer = model.isMyOffer(offer); // https://github.com/bisq-network/bisq/issues/4986 if (tableRow != null) { canTakeOfferResult = model.offerFilterService.canTakeOffer(offer, false); if (canTakeOfferResult.isValid() || myOffer) { tableRow.getStyleClass().remove("row-faded"); } else { if (!tableRow.getStyleClass().contains("row-faded")) tableRow.getStyleClass().add("row-faded"); hbox.getStyleClass().add("cell-faded"); } if (myOffer) { button.setDefaultButton(false); tableRow.setOnMousePressed(null); } else if (canTakeOfferResult.isValid()) { // set first row button as default button.setDefaultButton(getIndex() == 0); tableRow.setOnMousePressed(null); } else { button.setDefaultButton(false); tableRow.setOnMousePressed(e -> { // ugly hack to get the icon clickable when deactivated if (!(e.getTarget() instanceof ImageView || e.getTarget() instanceof Canvas)) onShowInfo(offer, canTakeOfferResult); }); } } String title; if (myOffer) { iconView.setId("image-remove"); title = Res.get("shared.remove"); button.setOnAction(e -> onRemoveOpenOffer(offer)); iconView2.setSize("16px"); button2.updateText(Res.get("shared.edit")); button2.setOnAction(e -> onEditOpenOffer(offer)); button2.setManaged(true); button2.setVisible(true); } else { boolean isSellOffer = OfferViewUtil.isShownAsSellOffer(offer); boolean isPrivateOffer = offer.isPrivateOffer(); if (isPrivateOffer) { button.setGraphic(GUIUtil.getLockLabel()); } else { iconView.setId(isSellOffer ? "image-buy-white" : "image-sell-white"); iconView.setFitHeight(16); iconView.setFitWidth(16); } button.setId(isSellOffer ? "buy-button" : "sell-button"); button.setStyle("-fx-text-fill: white"); title = Res.get(isSellOffer ? "mainView.menu.buyXmr" : "mainView.menu.sellXmr"); button.setTooltip(new Tooltip(Res.get("offerbook.takeOfferButton.tooltip", model.getDirectionLabelTooltip(offer)))); button.setOnAction(e -> onTakeOffer(offer)); button2.setManaged(false); button2.setVisible(false); } if (!myOffer) { if (canTakeOfferResult == null) { canTakeOfferResult = model.offerFilterService.canTakeOffer(offer, false); } if (!canTakeOfferResult.isValid()) { button.setOnAction(e -> onShowInfo(offer, canTakeOfferResult)); } } button.updateText(title); setPadding(new Insets(0, 15, 0, 0)); setGraphic(hbox); } else { setGraphic(null); button.setOnAction(null); button2.setOnAction(null); if (tableRow != null) { tableRow.setOnMousePressed(null); tableRow.getStyleClass().remove("row-faded"); } } } }; } }); return column; } private AutoTooltipTableColumn getSigningStateColumn() { AutoTooltipTableColumn column = new AutoTooltipTableColumn<>( Res.get("offerbook.timeSinceSigning"), Res.get("offerbook.timeSinceSigning.help", SignedWitnessService.SIGNER_AGE_DAYS, HavenoUtils.formatXmr(OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT, true))) { { setMinWidth(60); setSortable(true); } }; column.getStyleClass().add("number-column"); column.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); column.setCellFactory(new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final OfferBookListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { var witnessAgeData = item.getWitnessAgeData(accountAgeWitnessService, signedWitnessService); var label = witnessAgeData.isSigningRequired() ? new AccountStatusTooltipLabel(witnessAgeData) : new InfoAutoTooltipLabel(witnessAgeData.getDisplayString(), witnessAgeData.getIcon(), ContentDisplay.RIGHT, witnessAgeData.getInfo()); setGraphic(label); } else { setGraphic(null); } } }; } }); return column; } private AutoTooltipTableColumn getAvatarColumn() { AutoTooltipTableColumn column = new AutoTooltipTableColumn<>(Res.get("offerbook.trader")) { { setMinWidth(60); setMaxWidth(60); setSortable(true); } }; column.getStyleClass().addAll("avatar-column"); column.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final OfferBookListItem newItem, boolean empty) { super.updateItem(newItem, empty); if (newItem != null && !empty) { final Offer offer = newItem.getOffer(); final NodeAddress makersNodeAddress = offer.getOwnerNodeAddress(); String role = Res.get("peerInfoIcon.tooltip.maker"); int numTrades = model.getNumTrades(offer); PeerInfoIconTrading peerInfoIcon = new PeerInfoIconTrading(makersNodeAddress, role, numTrades, privateNotificationManager, offer, model.preferences, model.accountAgeWitnessService, useDevPrivilegeKeys); setGraphic(peerInfoIcon); } else { setGraphic(null); } } }; } }); return column; } @NotNull private Region getSpacer() { final Region spacer = new Region(); HBox.setHgrow(spacer, Priority.ALWAYS); return spacer; } private void updateCreateOfferButton() { createOfferButton.setText(Res.get("offerbook.createNewOffer", model.getDirection() == OfferDirection.BUY ? Res.get("shared.buy").toUpperCase() : Res.get("shared.sell").toUpperCase(), getTradeCurrencyCode())); } abstract String getTradeCurrencyCode(); } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.offer.offerbook; import com.google.common.base.Joiner; import haveno.common.ThreadUtils; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.api.CoreApi; import haveno.core.locale.BankUtil; import haveno.core.locale.CountryUtil; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.monetary.Price; import haveno.core.monetary.Volume; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OfferFilterService; import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccountUtil; import haveno.core.payment.payload.PaymentMethod; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.util.FormattingUtils; import haveno.core.util.PriceUtil; import haveno.core.util.VolumeUtil; import haveno.core.util.coin.CoinFormatter; import haveno.core.xmr.setup.WalletsSetup; import haveno.desktop.Navigation; import haveno.desktop.common.model.ActivatableViewModel; import haveno.desktop.main.MainView; import haveno.desktop.main.offer.OfferView; import haveno.desktop.main.offer.OfferViewUtil; import haveno.desktop.main.settings.SettingsView; import haveno.desktop.main.settings.preferences.PreferencesView; import haveno.desktop.util.DisplayUtils; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.math.BigInteger; import java.text.DecimalFormat; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import java.util.stream.Collectors; import javax.annotation.Nullable; @Slf4j abstract class OfferBookViewModel extends ActivatableViewModel { private final OpenOfferManager openOfferManager; private final User user; final OfferBook offerBook; final Preferences preferences; private final WalletsSetup walletsSetup; private final P2PService p2PService; final PriceFeedService priceFeedService; private final ClosedTradableManager closedTradableManager; final AccountAgeWitnessService accountAgeWitnessService; private final Navigation navigation; private final PriceUtil priceUtil; final OfferFilterService offerFilterService; private final CoinFormatter btcFormatter; private final FilteredList filteredItems; private final CoreApi coreApi; private final SortedList sortedItems; private final ListChangeListener tradeCurrencyListChangeListener; private final ListChangeListener filterItemsListener; private TradeCurrency selectedTradeCurrency; @Getter private final ObservableList tradeCurrencies = FXCollections.observableArrayList(); @Getter private final ObservableList allCurrencies = FXCollections.observableArrayList(); private OfferDirection direction; final StringProperty tradeCurrencyCode = new SimpleStringProperty(); private static final Object processOfferBookListItemsLock = new Object(); @Nullable private static Timer processOfferBookListItemsTimer; private final String THREAD_ID = OfferBookViewModel.class.getSimpleName(); private OfferView.OfferActionHandler offerActionHandler; // If id is empty string we ignore filter (display all methods) PaymentMethod selectedPaymentMethod = getShowAllEntryForPaymentMethod(); boolean isTabSelected; String filterText = ""; final BooleanProperty showAllTradeCurrenciesProperty = new SimpleBooleanProperty(true); final BooleanProperty disableMatchToggle = new SimpleBooleanProperty(); final IntegerProperty maxPlacesForAmount = new SimpleIntegerProperty(); final IntegerProperty maxPlacesForVolume = new SimpleIntegerProperty(); final IntegerProperty maxPlacesForPrice = new SimpleIntegerProperty(); final IntegerProperty maxPlacesForMarketPriceMargin = new SimpleIntegerProperty(); boolean showAllPaymentMethods = true; boolean useOffersMatchingMyAccountsFilter; boolean showPrivateOffers; protected static final boolean SORT_CURRENCIES_BY_OFFER_COUNT = true; // TODO: make configurable via preferences? /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// public OfferBookViewModel(User user, OpenOfferManager openOfferManager, OfferBook offerBook, Preferences preferences, WalletsSetup walletsSetup, P2PService p2PService, PriceFeedService priceFeedService, ClosedTradableManager closedTradableManager, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation, PriceUtil priceUtil, OfferFilterService offerFilterService, CoinFormatter btcFormatter, CoreApi coreApi) { super(); this.openOfferManager = openOfferManager; this.user = user; this.offerBook = offerBook; this.preferences = preferences; this.walletsSetup = walletsSetup; this.p2PService = p2PService; this.priceFeedService = priceFeedService; this.closedTradableManager = closedTradableManager; this.accountAgeWitnessService = accountAgeWitnessService; this.navigation = navigation; this.priceUtil = priceUtil; this.offerFilterService = offerFilterService; this.btcFormatter = btcFormatter; this.filteredItems = new FilteredList<>(offerBook.getOfferBookListItems()); this.coreApi = coreApi; this.sortedItems = new SortedList<>(filteredItems); tradeCurrencyListChangeListener = c -> fillCurrencies(); // refresh on offer changes offerBook.getOfferBookListItems().addListener((ListChangeListener) c -> { synchronized (processOfferBookListItemsLock) { if (processOfferBookListItemsTimer == null) { processOfferBookListItemsTimer = UserThread.runAfter(() -> { ThreadUtils.execute(() -> { offerBook.fillOfferCountMaps(); fillCurrencies(); synchronized (processOfferBookListItemsLock) { processOfferBookListItemsTimer = null; } }, THREAD_ID); }, 100, TimeUnit.MILLISECONDS); } } // TODO: This is removed because it's expensive to re-filter offers for every change (high cpu for many offers). // This was used to ensure offer list is fully refreshed, but is unnecessary after refactoring OfferBookService to clone offers? //filterOffers(); }); filterItemsListener = c -> { final Optional highestAmountOffer = filteredItems.stream() .max(Comparator.comparingLong(o -> o.getOffer().getAmount().longValueExact())); final boolean containsRangeAmount = filteredItems.stream().anyMatch(o -> o.getOffer().isRange()); if (highestAmountOffer.isPresent()) { final OfferBookListItem item = highestAmountOffer.get(); if (!item.getOffer().isRange() && containsRangeAmount) { maxPlacesForAmount.set(formatAmount(item.getOffer(), false) .length() * 2 + FormattingUtils.RANGE_SEPARATOR.length()); maxPlacesForVolume.set(formatVolume(item.getOffer(), false) .length() * 2 + FormattingUtils.RANGE_SEPARATOR.length()); } else { maxPlacesForAmount.set(formatAmount(item.getOffer(), false).length()); maxPlacesForVolume.set(formatVolume(item.getOffer(), false).length()); } } final Optional highestPriceOffer = filteredItems.stream() .filter(o -> o.getOffer().getPrice() != null) .max(Comparator.comparingLong(o -> o.getOffer().getPrice() != null ? o.getOffer().getPrice().getValue() : 0)); highestPriceOffer.ifPresent(offerBookListItem -> maxPlacesForPrice.set(formatPrice(offerBookListItem.getOffer(), false).length())); final Optional highestMarketPriceMarginOffer = filteredItems.stream() .filter(o -> o.getOffer().isUseMarketBasedPrice()) .max(Comparator.comparing(o -> new DecimalFormat("#0.00").format(o.getOffer().getMarketPriceMarginPct() * 100).length())); highestMarketPriceMarginOffer.ifPresent(offerBookListItem -> maxPlacesForMarketPriceMargin.set(formatMarketPriceMarginPct(offerBookListItem.getOffer()).length())); }; } @Override protected void activate() { filteredItems.addListener(filterItemsListener); if (user != null) { disableMatchToggle.set(user.getPaymentAccounts() == null || user.getPaymentAccounts().isEmpty()); } useOffersMatchingMyAccountsFilter = !disableMatchToggle.get() && isShowOffersMatchingMyAccounts(); showPrivateOffers = preferences.isShowPrivateOffers(); updateSelectedTradeCurrency(); fillCurrencies(); preferences.getTradeCurrenciesAsObservable().addListener(tradeCurrencyListChangeListener); offerBook.fillOfferBookListItems(); filterOffers(); setMarketPriceFeedCurrency(); } @Override protected void deactivate() { filteredItems.removeListener(filterItemsListener); preferences.getTradeCurrenciesAsObservable().removeListener(tradeCurrencyListChangeListener); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// void initWithDirection(OfferDirection direction) { this.direction = direction; } void onTabSelected(boolean isSelected) { this.isTabSelected = isSelected; setMarketPriceFeedCurrency(); if (isTabSelected) { updateSelectedTradeCurrency(); filterOffers(); } } /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// void onSetTradeCurrency(TradeCurrency tradeCurrency) { if (tradeCurrency != null) { String code = tradeCurrency.getCode(); boolean showAllEntry = isShowAllEntry(code); showAllTradeCurrenciesProperty.set(showAllEntry); if (isEditEntry(code)) navigation.navigateTo(MainView.class, SettingsView.class, PreferencesView.class); else if (!showAllEntry) { this.selectedTradeCurrency = tradeCurrency; tradeCurrencyCode.set(code); } setMarketPriceFeedCurrency(); filterOffers(); saveSelectedCurrencyCodeInPreferences(direction, code); } } void onFilterKeyTyped(String filterText) { this.filterText = filterText; filterOffers(); } abstract void saveSelectedCurrencyCodeInPreferences(OfferDirection direction, String code); protected void onSetPaymentMethod(PaymentMethod paymentMethod) { if (paymentMethod == null) return; showAllPaymentMethods = isShowAllEntry(paymentMethod.getId()); if (!showAllPaymentMethods) { this.selectedPaymentMethod = paymentMethod; // If we select Wise we switch to show all currencies as Wise supports // sending to most currencies. if (paymentMethod.getId().equals(PaymentMethod.TRANSFERWISE_ID)) { onSetTradeCurrency(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, "")); } } else { this.selectedPaymentMethod = getShowAllEntryForPaymentMethod(); } filterOffers(); } void onRemoveOpenOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { openOfferManager.removeOffer(offer, resultHandler, errorMessageHandler); } void onShowOffersMatchingMyAccounts(boolean isSelected) { useOffersMatchingMyAccountsFilter = isSelected; preferences.setShowOffersMatchingMyAccounts(useOffersMatchingMyAccountsFilter); filterOffers(); } void onShowPrivateOffers(boolean isSelected) { showPrivateOffers = isSelected; preferences.setShowPrivateOffers(isSelected); filterOffers(); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// boolean isShowOffersMatchingMyAccounts() { return preferences.isShowOffersMatchingMyAccounts(); } SortedList getOfferList() { return sortedItems; } Map getBuyOfferCounts() { return offerBook.getBuyOfferCountMap(); } Map getSellOfferCounts() { return offerBook.getSellOfferCountMap(); } boolean isMyOffer(Offer offer) { return openOfferManager.isMyOffer(offer); } OfferDirection getDirection() { return direction; } boolean isBootstrappedOrShowPopup() { return GUIUtil.isBootstrappedOrShowPopup(p2PService); } TradeCurrency getSelectedTradeCurrency() { return selectedTradeCurrency; } Map getOfferCounts() { return OfferViewUtil.isShownAsBuyOffer(getDirection(), getSelectedTradeCurrency()) ? getSellOfferCounts() : getBuyOfferCounts(); } ObservableList getPaymentMethods() { ObservableList list = FXCollections.observableArrayList(PaymentMethod.paymentMethods); if (preferences.isHideNonAccountPaymentMethods() && user.getPaymentAccounts() != null) { Set supportedPaymentMethods = user.getPaymentAccounts().stream() .map(PaymentAccount::getPaymentMethod).collect(Collectors.toSet()); if (!supportedPaymentMethods.isEmpty()) { list = FXCollections.observableArrayList(supportedPaymentMethods); } } list = filterPaymentMethods(list, selectedTradeCurrency); list.sort(Comparator.naturalOrder()); list.add(0, getShowAllEntryForPaymentMethod()); return list; } protected abstract ObservableList filterPaymentMethods(ObservableList list, TradeCurrency selectedTradeCurrency); String getAmount(OfferBookListItem item) { return formatAmount(item.getOffer(), true); } private String formatAmount(Offer offer, boolean decimalAligned) { return DisplayUtils.formatAmount(offer, GUIUtil.AMOUNT_DECIMALS, decimalAligned, maxPlacesForAmount.get(), btcFormatter); } String getPrice(OfferBookListItem item) { if ((item == null)) { return ""; } Offer offer = item.getOffer(); Price price = offer.getPrice(); if (price != null) { return formatPrice(offer, true); } else { return Res.get("shared.na"); } } String getAbsolutePriceMargin(Offer offer) { return FormattingUtils.formatPercentagePrice(Math.abs(offer.getMarketPriceMarginPct())); } private String formatPrice(Offer offer, boolean decimalAligned) { return DisplayUtils.formatPrice(offer.getPrice(), decimalAligned, maxPlacesForPrice.get()); } String getPriceAsPercentage(OfferBookListItem item) { return getMarketBasedPrice(item.getOffer()) .map(price -> "(" + FormattingUtils.formatPercentagePrice(price) + ")") .orElse(""); } public Optional getMarketBasedPrice(Offer offer) { return priceUtil.getMarketBasedPrice(offer, direction); } String formatMarketPriceMarginPct(Offer offer) { String postFix = ""; if (offer.isUseMarketBasedPrice()) { postFix = " (" + FormattingUtils.formatPercentagePrice(offer.getMarketPriceMarginPct()) + ")"; } return postFix; } String getVolume(OfferBookListItem item) { return formatVolume(item.getOffer(), true); } String getVolumeAmount(OfferBookListItem item) { return formatVolume(item.getOffer(), true, false); } private String formatVolume(Offer offer, boolean decimalAligned) { return formatVolume(offer, decimalAligned, showAllTradeCurrenciesProperty.get()); } private String formatVolume(Offer offer, boolean decimalAligned, boolean appendCurrencyCode) { Volume offerVolume = offer.getVolume(); Volume minOfferVolume = offer.getMinVolume(); if (offerVolume != null && minOfferVolume != null) { String postFix = appendCurrencyCode ? " " + offer.getCounterCurrencyCode() : ""; decimalAligned = decimalAligned && !showAllTradeCurrenciesProperty.get(); return VolumeUtil.formatVolume(offer, decimalAligned, maxPlacesForVolume.get()) + postFix; } else { return Res.get("shared.na"); } } int getNumberOfDecimalsForVolume(OfferBookListItem item) { return CurrencyUtil.isVolumeRoundedToNearestUnit(item.getOffer().getCounterCurrencyCode()) ? GUIUtil.NUM_DECIMALS_UNIT : GUIUtil.NUM_DECIMALS_PRECISE; } String getPaymentMethod(OfferBookListItem item) { String result = ""; if (item != null) { Offer offer = item.getOffer(); String method = Res.get(offer.getPaymentMethod().getId() + "_SHORT"); String methodCountryCode = offer.getCountryCode(); if (isF2F(offer)) { result = method + " (" + methodCountryCode + ", " + offer.getF2FCity() + ")"; } else { if (methodCountryCode != null) result = method + " (" + methodCountryCode + ")"; else result = method; } } return result; } String getPaymentMethodToolTip(OfferBookListItem item) { String result = ""; if (item != null) { Offer offer = item.getOffer(); result = Res.getWithCol("shared.paymentMethod") + " " + Res.get(offer.getPaymentMethod().getId()); result += "\n" + Res.getWithCol("shared.currency") + " " + CurrencyUtil.getNameAndCode(offer.getCounterCurrencyCode()); String countryCode = offer.getCountryCode(); if (isF2F(offer)) { if (countryCode != null) { result += "\n" + Res.get("payment.f2f.offerbook.tooltip.countryAndCity", CountryUtil.getNameByCode(countryCode), offer.getF2FCity()); } } else { if (countryCode != null) { String bankId = offer.getBankId(); if (bankId != null && !bankId.equals("null")) { if (BankUtil.isBankIdRequired(countryCode)) result += "\n" + Res.get("offerbook.offerersBankId", bankId); else if (BankUtil.isBankNameRequired(countryCode)) result += "\n" + Res.get("offerbook.offerersBankName", bankId); } } if (countryCode != null) result += "\n" + Res.get("offerbook.offerersBankSeat", CountryUtil.getNameByCode(countryCode)); List acceptedCountryCodes = offer.getAcceptedCountryCodes(); List acceptedBanks = offer.getAcceptedBankIds(); if (acceptedCountryCodes != null && !acceptedCountryCodes.isEmpty()) { if (CountryUtil.containsAllSepaEuroCountries(acceptedCountryCodes)) result += "\n" + Res.get("offerbook.offerersAcceptedBankSeatsEuro"); else result += "\n" + Res.get("offerbook.offerersAcceptedBankSeats", CountryUtil.getNamesByCodesString(acceptedCountryCodes)); } else if (acceptedBanks != null && !acceptedBanks.isEmpty()) { if (offer.getPaymentMethod().equals(PaymentMethod.SAME_BANK)) result += "\n" + Res.getWithCol("shared.bankName") + " " + acceptedBanks.get(0); else if (offer.getPaymentMethod().equals(PaymentMethod.SPECIFIC_BANKS)) result += "\n" + Res.getWithCol("shared.acceptedBanks") + " " + Joiner.on(", ").join(acceptedBanks); } } if (offer.getCombinedExtraInfo() != null && !offer.getCombinedExtraInfo().isEmpty()) result += "\n" + Res.get("payment.shared.extraInfo.tooltip", offer.getCombinedExtraInfo()); } return result; } private boolean isF2F(Offer offer) { return offer.getPaymentMethod().equals(PaymentMethod.F2F); } String getDirectionLabelTooltip(Offer offer) { return getDirectionWithCodeDetailed(offer.getMirroredDirection(), offer.getCounterCurrencyCode()); } Optional getMostMaturePaymentAccountForOffer(Offer offer) { return PaymentAccountUtil.getMostMaturePaymentAccountForOffer(offer, user.getPaymentAccounts(), accountAgeWitnessService); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void setMarketPriceFeedCurrency() { if (isTabSelected) { if (showAllTradeCurrenciesProperty.get()) priceFeedService.setCurrencyCode(getDefaultTradeCurrency().getCode()); else priceFeedService.setCurrencyCode(tradeCurrencyCode.get()); } } private void fillCurrencies() { tradeCurrencies.clear(); allCurrencies.clear(); fillCurrencies(tradeCurrencies, allCurrencies); } abstract void fillCurrencies(ObservableList tradeCurrencies, ObservableList allCurrencies); /////////////////////////////////////////////////////////////////////////////////////////// // Checks /////////////////////////////////////////////////////////////////////////////////////////// boolean hasPaymentAccountForCurrency() { if (showAllTradeCurrenciesProperty.get()) { if (this instanceof CryptoOfferBookViewModel) { return user.hasCryptoPaymentAccount(); } else if (this instanceof FiatOfferBookViewModel) { return user.hasFiatPaymentAccount(); } else if (this instanceof OtherOfferBookViewModel) { return user.hasTraditionalNonFiatAccount(); } } return hasPaymentAccountForCurrency(selectedTradeCurrency); } boolean hasPaymentAccountForCurrency(TradeCurrency currency) { return user.hasPaymentAccountForCurrency(currency); } boolean canCreateOrTakeOffer() { return GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation) && GUIUtil.isWalletSyncedWithinToleranceOrShowPopup(openOfferManager.getXmrWalletService()) && GUIUtil.isBootstrappedOrShowPopup(p2PService); } /////////////////////////////////////////////////////////////////////////////////////////// // Filters /////////////////////////////////////////////////////////////////////////////////////////// private void filterOffers() { Predicate predicate = useOffersMatchingMyAccountsFilter ? getCurrencyAndMethodPredicate(direction, selectedTradeCurrency).and(getOffersMatchingMyAccountsPredicate()) : getCurrencyAndMethodPredicate(direction, selectedTradeCurrency); // filter private offers if (direction == OfferDirection.BUY) { predicate = predicate.and(offerBookListItem -> offerBookListItem.getOffer().isPrivateOffer() == showPrivateOffers); } if (!filterText.isEmpty()) { // filter node address Predicate nextPredicate = offerBookListItem -> offerBookListItem.getOffer().getOfferPayload().getOwnerNodeAddress().getFullAddress().toLowerCase().contains(filterText.toLowerCase()); // filter offer id nextPredicate = nextPredicate.or(offerBookListItem -> offerBookListItem.getOffer().getId().toLowerCase().contains(filterText.toLowerCase())); // filter full payment method nextPredicate = nextPredicate.or(offerBookListItem -> Res.get(offerBookListItem.getOffer().getPaymentMethod().getId()).toLowerCase().contains(filterText.toLowerCase())); // filter short payment method nextPredicate = nextPredicate.or(offerBookListItem -> { return getPaymentMethod(offerBookListItem).toLowerCase().contains(filterText.toLowerCase()); }); // filter currencies nextPredicate = nextPredicate.or(offerBookListItem -> { return offerBookListItem.getOffer().getCounterCurrencyCode().toLowerCase().contains(filterText.toLowerCase()) || offerBookListItem.getOffer().getBaseCurrencyCode().toLowerCase().contains(filterText.toLowerCase()) || CurrencyUtil.getNameAndCode(offerBookListItem.getOffer().getCounterCurrencyCode()).toLowerCase().contains(filterText.toLowerCase()) || CurrencyUtil.getNameAndCode(offerBookListItem.getOffer().getBaseCurrencyCode()).toLowerCase().contains(filterText.toLowerCase()); }); // filter extra info nextPredicate = nextPredicate.or(offerBookListItem -> { return offerBookListItem.getOffer().getCombinedExtraInfo() != null && offerBookListItem.getOffer().getCombinedExtraInfo().toLowerCase().contains(filterText.toLowerCase()); }); filteredItems.setPredicate(predicate.and(nextPredicate)); } else { filteredItems.setPredicate(predicate); } } abstract Predicate getCurrencyAndMethodPredicate(OfferDirection direction, TradeCurrency selectedTradeCurrency); private Predicate getOffersMatchingMyAccountsPredicate() { // This code duplicates code in the view at the button column. We need there the different results for // display in popups so we cannot replace that with the predicate. Any change need to be applied in both // places. return offerBookListItem -> offerFilterService.canTakeOffer(offerBookListItem.getOffer(), false).isValid(); } boolean isOfferBanned(Offer offer) { return offerFilterService.isOfferBanned(offer); } private boolean isShowAllEntry(String id) { return id.equals(GUIUtil.SHOW_ALL_FLAG); } private boolean isEditEntry(String id) { return id.equals(GUIUtil.EDIT_FLAG); } public int getNumTrades(Offer offer) { return closedTradableManager.getTradableList().stream() .filter(e -> e instanceof Trade) // weed out canceled offers .filter(e -> { final Optional tradePeerNodeAddress = e.getOptionalTradePeerNodeAddress(); return tradePeerNodeAddress.isPresent() && tradePeerNodeAddress.get().getFullAddress().equals(offer.getMakerNodeAddress().getFullAddress()); }) .collect(Collectors.toSet()) .size(); } public boolean hasSelectionAccountSigning() { if (showAllTradeCurrenciesProperty.get()) { if (isShowAllEntry(selectedPaymentMethod.getId())) return !(this instanceof CryptoOfferBookViewModel); else return PaymentMethod.hasChargebackRisk(selectedPaymentMethod); } else { if (isShowAllEntry(selectedPaymentMethod.getId())) return CurrencyUtil.getMatureMarketCurrencies().stream() .anyMatch(c -> c.getCode().equals(selectedTradeCurrency.getCode())); else return PaymentMethod.hasChargebackRisk(selectedPaymentMethod, tradeCurrencyCode.get()); } } private static String getDirectionWithCodeDetailed(OfferDirection direction, String currencyCode) { return (direction == OfferDirection.BUY) ? Res.get("shared.buyingXMRWith", currencyCode) : Res.get("shared.sellingXMRFor", currencyCode); } public String formatDepositString(BigInteger deposit, long amount) { var percentage = FormattingUtils.formatToRoundedPercentWithSymbol(HavenoUtils.divide(deposit, BigInteger.valueOf(amount))); return HavenoUtils.formatXmr(deposit) + " (" + percentage + ")"; } PaymentMethod getShowAllEntryForPaymentMethod() { return PaymentMethod.getDummyPaymentMethod(GUIUtil.SHOW_ALL_FLAG); } public boolean isInstantPaymentMethod(Offer offer) { return offer.getPaymentMethod().equals(PaymentMethod.BLOCK_CHAINS_INSTANT); } public void setOfferActionHandler(OfferView.OfferActionHandler offerActionHandler) { this.offerActionHandler = offerActionHandler; } public void onCreateOffer() { offerActionHandler.onCreateOffer(getSelectedTradeCurrency(), selectedPaymentMethod); } public void onTakeOffer(Offer offer) { offerActionHandler.onTakeOffer(offer); } private void updateSelectedTradeCurrency() { String code = getCurrencyCodeFromPreferences(direction); if (code != null && !code.isEmpty() && !isShowAllEntry(code) && CurrencyUtil.getTradeCurrency(code).isPresent()) { showAllTradeCurrenciesProperty.set(false); selectedTradeCurrency = CurrencyUtil.getTradeCurrency(code).get(); } else { showAllTradeCurrenciesProperty.set(true); selectedTradeCurrency = getDefaultTradeCurrency(); } tradeCurrencyCode.set(selectedTradeCurrency.getCode()); } abstract TradeCurrency getDefaultTradeCurrency(); public void updateSelectedPaymentMethod() { showAllPaymentMethods = getPaymentMethods().stream().noneMatch(paymentMethod -> paymentMethod.equals(selectedPaymentMethod)); } abstract String getCurrencyCodeFromPreferences(OfferDirection direction); public OpenOffer getOpenOffer(Offer offer) { return openOfferManager.getOpenOffer(offer.getId()).orElse(null); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/offerbook/OtherOfferBookView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/offerbook/OtherOfferBookView.java ================================================ /* * 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 . */ package haveno.desktop.main.offer.offerbook; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.core.account.sign.SignedWitnessService; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.alert.PrivateNotificationManager; import haveno.core.locale.Res; import haveno.core.offer.OfferDirection; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.Navigation; import haveno.desktop.common.view.FxmlView; import haveno.desktop.main.overlays.windows.OfferDetailsWindow; import javafx.scene.layout.GridPane; @FxmlView public class OtherOfferBookView extends OfferBookView { @Inject OtherOfferBookView(OtherOfferBookViewModel model, Navigation navigation, OfferDetailsWindow offerDetailsWindow, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, PrivateNotificationManager privateNotificationManager, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys, AccountAgeWitnessService accountAgeWitnessService, SignedWitnessService signedWitnessService) { super(model, navigation, offerDetailsWindow, formatter, privateNotificationManager, useDevPrivilegeKeys, accountAgeWitnessService, signedWitnessService); } @Override protected String getMarketTitle() { return model.getDirection().equals(OfferDirection.BUY) ? Res.get("offerbook.availableOffersToBuy", Res.getBaseCurrencyCode(), Res.get("shared.otherAssets")) : Res.get("offerbook.availableOffersToSell", Res.getBaseCurrencyCode(), Res.get("shared.otherAssets")); } @Override String getTradeCurrencyCode() { return Res.getBaseCurrencyCode(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/offerbook/OtherOfferBookViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.offer.offerbook; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.api.CoreApi; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.GlobalSettings; import haveno.core.locale.TradeCurrency; import haveno.core.locale.TraditionalCurrency; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OfferFilterService; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.payload.PaymentMethod; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.ClosedTradableManager; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.util.FormattingUtils; import haveno.core.util.PriceUtil; import haveno.core.util.coin.CoinFormatter; import haveno.core.xmr.setup.WalletsSetup; import haveno.desktop.Navigation; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.P2PService; import java.util.List; import java.util.function.Predicate; import java.util.stream.Collectors; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import org.jetbrains.annotations.NotNull; public class OtherOfferBookViewModel extends OfferBookViewModel { @Inject public OtherOfferBookViewModel(User user, OpenOfferManager openOfferManager, OfferBook offerBook, Preferences preferences, WalletsSetup walletsSetup, P2PService p2PService, PriceFeedService priceFeedService, ClosedTradableManager closedTradableManager, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation, PriceUtil priceUtil, OfferFilterService offerFilterService, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, CoreApi coreApi) { super(user, openOfferManager, offerBook, preferences, walletsSetup, p2PService, priceFeedService, closedTradableManager, accountAgeWitnessService, navigation, priceUtil, offerFilterService, btcFormatter, coreApi); } @Override void saveSelectedCurrencyCodeInPreferences(OfferDirection direction, String code) { if (direction == OfferDirection.BUY) { preferences.setBuyScreenOtherCurrencyCode(code); } else { preferences.setBuyScreenOtherCurrencyCode(code); } } @Override protected ObservableList filterPaymentMethods(ObservableList list, TradeCurrency selectedTradeCurrency) { return FXCollections.observableArrayList(list.stream().filter(paymentMethod -> { if (paymentMethod.getSupportedAssetCodes() == null) return true; for (String assetCode : paymentMethod.getSupportedAssetCodes()) { if (!CurrencyUtil.isFiatCurrency(assetCode)) return true; } return false; }).collect(Collectors.toList())); } @Override void fillCurrencies(ObservableList tradeCurrencies, ObservableList allCurrencies) { tradeCurrencies.add(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, "")); tradeCurrencies.addAll(CurrencyUtil.getMainTraditionalCurrencies().stream() .filter(withoutFiatCurrency()) .collect(Collectors.toList())); tradeCurrencies.add(new CryptoCurrency(GUIUtil.EDIT_FLAG, "")); allCurrencies.add(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, "")); allCurrencies.addAll(CurrencyUtil.getMainTraditionalCurrencies().stream() .filter(withoutFiatCurrency()) .collect(Collectors.toList())); allCurrencies.add(new CryptoCurrency(GUIUtil.EDIT_FLAG, "")); } @Override Predicate getCurrencyAndMethodPredicate(OfferDirection direction, TradeCurrency selectedTradeCurrency) { return offerBookListItem -> { Offer offer = offerBookListItem.getOffer(); boolean directionResult = offer.getDirection() != direction; boolean currencyResult = CurrencyUtil.isTraditionalCurrency(offer.getCounterCurrencyCode()) && !CurrencyUtil.isFiatCurrency(offer.getCounterCurrencyCode()) && (showAllTradeCurrenciesProperty.get() || offer.getCounterCurrencyCode().equals(selectedTradeCurrency.getCode())); boolean paymentMethodResult = showAllPaymentMethods || offer.getPaymentMethod().equals(selectedPaymentMethod); boolean notMyOfferOrShowMyOffersActivated = !isMyOffer(offerBookListItem.getOffer()) || preferences.isShowOwnOffersInOfferBook(); return directionResult && currencyResult && paymentMethodResult && notMyOfferOrShowMyOffersActivated; }; } @Override TradeCurrency getDefaultTradeCurrency() { TradeCurrency defaultTradeCurrency = GlobalSettings.getDefaultTradeCurrency(); if (CurrencyUtil.isTraditionalCurrency(defaultTradeCurrency.getCode()) && !CurrencyUtil.isFiatCurrency(defaultTradeCurrency.getCode()) && hasPaymentAccountForCurrency(defaultTradeCurrency)) { return defaultTradeCurrency; } ObservableList tradeCurrencies = FXCollections.observableArrayList(getTradeCurrencies()); if (!tradeCurrencies.isEmpty()) { // drop show all entry and select first currency with payment account available tradeCurrencies.remove(0); List sortedList = tradeCurrencies.stream().sorted((o1, o2) -> Boolean.compare(!hasPaymentAccountForCurrency(o1), !hasPaymentAccountForCurrency(o2))).collect(Collectors.toList()); return sortedList.get(0); } else { return CurrencyUtil.getMainTraditionalCurrencies().stream() .filter(withoutFiatCurrency()) .sorted((o1, o2) -> Boolean.compare(!hasPaymentAccountForCurrency(o1), !hasPaymentAccountForCurrency(o2))) .collect(Collectors.toList()).get(0); } } @Override String getCurrencyCodeFromPreferences(OfferDirection direction) { // validate if previous stored currencies are Traditional ones String currencyCode = direction == OfferDirection.BUY ? preferences.getBuyScreenOtherCurrencyCode() : preferences.getSellScreenOtherCurrencyCode(); return CurrencyUtil.isTraditionalCurrency(currencyCode) ? currencyCode : null; } @NotNull private Predicate withoutFiatCurrency() { return fiatCurrency -> !CurrencyUtil.isFiatCurrency(fiatCurrency.getCode()); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/signedoffer/SignedOfferListItem.java ================================================ /* * 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 . */ package haveno.desktop.main.offer.signedoffer; import org.apache.commons.lang3.StringUtils; import lombok.Getter; import haveno.core.offer.SignedOffer; import haveno.desktop.util.filtering.FilterableListItem; class SignedOfferListItem implements FilterableListItem { @Getter private final SignedOffer signedOffer; SignedOfferListItem(SignedOffer signedOffer) { this.signedOffer = signedOffer; } @Override public boolean match(String filterString) { if (filterString.isEmpty()) { return true; } if (StringUtils.containsIgnoreCase(String.valueOf(signedOffer.getTraderId()), filterString)) { return true; } if (StringUtils.containsIgnoreCase(String.valueOf(signedOffer.getOfferId()), filterString)) { return true; } return StringUtils.containsIgnoreCase(String.valueOf(signedOffer.getReserveTxKeyImages()), filterString); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/signedoffer/SignedOfferView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/signedoffer/SignedOfferView.java ================================================ /* * 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 . */ package haveno.desktop.main.offer.signedoffer; import com.google.inject.Inject; import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.core.offer.SignedOffer; import haveno.core.trade.HavenoUtils; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.common.view.ActivatableViewAndModel; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.AutoTooltipTableColumn; import haveno.desktop.components.HyperlinkWithIcon; import haveno.desktop.components.InputTextField; import haveno.desktop.components.list.FilterBox; import haveno.desktop.main.offer.OfferViewUtil; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.DisplayUtils; import haveno.desktop.util.GUIUtil; import java.util.Comparator; import java.util.Date; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; import javafx.fxml.FXML; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.MenuItem; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableRow; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.util.Callback; import javafx.util.Duration; @FxmlView public class SignedOfferView extends ActivatableViewAndModel { @FXML FilterBox filterBox; @FXML protected TableView tableView; @FXML TableColumn dateColumn; @FXML TableColumn traderIdColumn; @FXML TableColumn offerIdColumn; @FXML TableColumn reserveTxKeyImages; @FXML TableColumn makerPenaltyFeeColumn; @FXML InputTextField filterTextField; @FXML Label numItems; @FXML Region footerSpacer; private SignedOfferListItem selectedSignedOffer; private final XmrWalletService xmrWalletService; private ContextMenu contextMenu; @Inject public SignedOfferView(SignedOffersViewModel model, XmrWalletService xmrWalletService) { super(model); this.xmrWalletService = xmrWalletService; } /////////////////////////////////////////////////////////////////////////////////////////// // Life cycle /////////////////////////////////////////////////////////////////////////////////////////// @Override public void initialize() { filterTextField = new InputTextField(); Tooltip tooltip = new Tooltip(); tooltip.setShowDelay(Duration.millis(100)); tooltip.setShowDuration(Duration.seconds(10)); filterTextField.setTooltip(tooltip); HBox.setHgrow(filterTextField, Priority.NEVER); filterTextField.setText("open"); setupTable(); } @Override protected void activate() { FilteredList filteredList = new FilteredList<>(model.getList()); SortedList sortedList = new SortedList<>(filteredList); sortedList.comparatorProperty().bind(tableView.comparatorProperty()); tableView.setItems(sortedList); filterBox.initialize(filteredList, tableView); filterBox.setPromptText(Res.get("shared.filter")); filterBox.activate(); contextMenu = new ContextMenu(); MenuItem makerPenalization = new MenuItem( Res.get("support.contextmenu.penalize.msg", Res.get("shared.maker").toLowerCase()) ); MenuItem copyToClipboard = new MenuItem(Res.get("shared.copyToClipboard")); contextMenu.getItems().addAll(makerPenalization, copyToClipboard); tableView.setRowFactory(tv -> { TableRow row = new TableRow<>(); row.setOnContextMenuRequested(event -> contextMenu.show(row, event.getScreenX(), event.getScreenY())); return row; }); copyToClipboard.setOnAction(event -> { selectedSignedOffer = tableView.getSelectionModel().getSelectedItem(); Utilities.copyToClipboard(selectedSignedOffer.getSignedOffer().toJson()); }); makerPenalization.setOnAction(event -> { selectedSignedOffer = tableView.getSelectionModel().getSelectedItem(); if(selectedSignedOffer != null) { SignedOffer signedOffer = selectedSignedOffer.getSignedOffer(); new Popup().warning(Res.get("support.prompt.signedOffer.penalty.msg", signedOffer.getOfferId(), HavenoUtils.formatXmr(signedOffer.getPenaltyAmount(), true), HavenoUtils.formatXmr(signedOffer.getReserveTxMinerFee(), true), signedOffer.getReserveTxHash(), signedOffer.getReserveTxKeyImages()) ).onAction(() -> OfferViewUtil.submitTransactionHex(xmrWalletService, tableView, signedOffer.getReserveTxHex())).show(); } else { new Popup().error(Res.get("support.prompt.signedOffer.error.msg")).show(); } }); GUIUtil.requestFocus(tableView); } @Override protected void deactivate() { filterBox.deactivate(); } /////////////////////////////////////////////////////////////////////////////////////////// // SignedOfferView /////////////////////////////////////////////////////////////////////////////////////////// protected void setupTable() { tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); Label placeholder = new AutoTooltipLabel(Res.get("support.noTickets")); placeholder.setWrapText(true); tableView.setPlaceholder(placeholder); tableView.getSelectionModel().clearSelection(); dateColumn = getDateColumn(); tableView.getColumns().add(dateColumn); traderIdColumn = getTraderIdColumn(); tableView.getColumns().add(traderIdColumn); offerIdColumn = getOfferIdColumn(); tableView.getColumns().add(offerIdColumn); makerPenaltyFeeColumn = getMakerPenaltyFeeColumn(); tableView.getColumns().add(makerPenaltyFeeColumn); reserveTxKeyImages = getReserveTxKeyImagesColumn(); tableView.getColumns().add(reserveTxKeyImages); traderIdColumn.setComparator(Comparator.comparing(o -> o.getSignedOffer().getTraderId())); offerIdColumn.setComparator(Comparator.comparing(o -> o.getSignedOffer().getOfferId())); dateColumn.setComparator(Comparator.comparing(o -> o.getSignedOffer().getTimeStamp())); dateColumn.setSortType(TableColumn.SortType.DESCENDING); tableView.getSortOrder().add(dateColumn); } private TableColumn getDateColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("shared.date")) { { setMinWidth(180); } }; column.setCellValueFactory((signedOffer) -> new ReadOnlyObjectWrapper<>(signedOffer.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final SignedOfferListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) setText(DisplayUtils.formatDateTime(new Date(item.getSignedOffer().getTimeStamp()))); else setText(""); } }; } }); return column; } private TableColumn getTraderIdColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("shared.traderId")) { { setMinWidth(110); } }; column.setCellValueFactory(signedOffer -> new ReadOnlyObjectWrapper<>(signedOffer.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { private HyperlinkWithIcon field; @Override public void updateItem(final SignedOfferListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setText(String.valueOf(item.getSignedOffer().getTraderId())); setGraphic(field); } else { setGraphic(null); setText(""); if (field != null) field.setOnAction(null); } } }; } }); return column; } private TableColumn getOfferIdColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("shared.offerId")) { { setMinWidth(110); } }; column.setCellValueFactory((signedOffer) -> new ReadOnlyObjectWrapper<>(signedOffer.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { private HyperlinkWithIcon field; @Override public void updateItem(final SignedOfferListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setText(String.valueOf(item.getSignedOffer().getOfferId())); setGraphic(field); } else { setGraphic(null); setText(""); if (field != null) field.setOnAction(null); } } }; } }); return column; } private TableColumn getMakerPenaltyFeeColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("support.maker.penalty.fee")) { { setMinWidth(160); } }; column.setCellValueFactory((signedOffer) -> new ReadOnlyObjectWrapper<>(signedOffer.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final SignedOfferListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) setText(HavenoUtils.formatXmr(item.getSignedOffer().getPenaltyAmount(), true)); else setText(""); } }; } }); return column; } private TableColumn getReserveTxKeyImagesColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("support.txKeyImages")) { { setMinWidth(160); } }; column.setCellValueFactory((signedOffer) -> new ReadOnlyObjectWrapper<>(signedOffer.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final SignedOfferListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) setText(item.getSignedOffer().getReserveTxKeyImages().toString()); else setText(""); } }; } }); return column; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/signedoffer/SignedOffersDataModel.java ================================================ /* * 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 . */ package haveno.desktop.main.offer.signedoffer; import com.google.inject.Inject; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import java.util.stream.Collectors; import haveno.core.offer.OpenOfferManager; import haveno.core.offer.SignedOffer; import haveno.desktop.common.model.ActivatableDataModel; import java.sql.Date; class SignedOffersDataModel extends ActivatableDataModel { private final OpenOfferManager openOfferManager; private final ObservableList list = FXCollections.observableArrayList(); private final ListChangeListener tradesListChangeListener; @Inject public SignedOffersDataModel(OpenOfferManager openOfferManager) { this.openOfferManager = openOfferManager; tradesListChangeListener = change -> applyList(); } @Override protected void activate() { openOfferManager.getObservableSignedOffersList().addListener(tradesListChangeListener); applyList(); } @Override protected void deactivate() { openOfferManager.getObservableSignedOffersList().removeListener(tradesListChangeListener); } public ObservableList getList() { return list; } private void applyList() { list.clear(); synchronized (openOfferManager.getObservableSignedOffersList()) { list.addAll(openOfferManager.getObservableSignedOffersList().stream().map(SignedOfferListItem::new).collect(Collectors.toList())); } // we sort by date, the earliest first list.sort((o1, o2) -> new Date(o2.getSignedOffer().getTimeStamp()).compareTo(new Date(o1.getSignedOffer().getTimeStamp()))); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/signedoffer/SignedOffersViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.offer.signedoffer; import com.google.inject.Inject; import javafx.collections.ObservableList; import haveno.desktop.common.model.ActivatableWithDataModel; import haveno.desktop.common.model.ViewModel; class SignedOffersViewModel extends ActivatableWithDataModel implements ViewModel { @Inject public SignedOffersViewModel(SignedOffersDataModel model) { super(model); } public ObservableList getList() { return dataModel.getList(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java ================================================ /* * 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 . */ package haveno.desktop.main.offer.takeoffer; import com.google.inject.Inject; import haveno.common.ThreadUtils; import haveno.common.UserThread; import haveno.common.handlers.ErrorMessageHandler; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.filter.FilterManager; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.monetary.Price; import haveno.core.monetary.Volume; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OfferUtil; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccountUtil; import haveno.core.payment.payload.PaymentMethod; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.HavenoUtils; import haveno.core.trade.TradeManager; import haveno.core.trade.handlers.TradeResultHandler; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.util.VolumeUtil; import haveno.core.util.coin.CoinUtil; import haveno.core.xmr.listeners.XmrBalanceListener; import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.Navigation; import haveno.desktop.main.offer.OfferDataModel; import haveno.desktop.main.offer.offerbook.OfferBook; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.P2PService; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.ObservableList; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; import java.math.BigInteger; import java.util.Set; import java.util.function.Predicate; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; /** * Domain for that UI element. * Note that the create offer domain has a deeper scope in the application domain (TradeManager). * That model is just responsible for the domain specific parts displayed needed in that UI element. */ class TakeOfferDataModel extends OfferDataModel { private final TradeManager tradeManager; private final OfferBook offerBook; private final User user; private final FilterManager filterManager; final Preferences preferences; private final PriceFeedService priceFeedService; private final AccountAgeWitnessService accountAgeWitnessService; private final Navigation navigation; private final P2PService p2PService; private BigInteger securityDeposit; private Offer offer; // final BooleanProperty isFeeFromFundingTxSufficient = new SimpleBooleanProperty(); // final BooleanProperty isMainNet = new SimpleBooleanProperty(); private final ObjectProperty amount = new SimpleObjectProperty<>(); final ObjectProperty volume = new SimpleObjectProperty<>(); private XmrBalanceListener balanceListener; private PaymentAccount paymentAccount; private boolean isTabSelected; protected boolean allowAmountUpdate = true; Price tradePrice; private final Predicate isNonZeroPrice = (p) -> p != null && !p.isZero(); private final Predicate> isNonZeroVolume = (v) -> v.get() != null && !v.get().isZero(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject TakeOfferDataModel(TradeManager tradeManager, OfferBook offerBook, OfferUtil offerUtil, XmrWalletService xmrWalletService, OpenOfferManager openOfferManager, User user, FilterManager filterManager, Preferences preferences, PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation, P2PService p2PService ) { super(xmrWalletService, openOfferManager, offerUtil); this.tradeManager = tradeManager; this.offerBook = offerBook; this.user = user; this.filterManager = filterManager; this.preferences = preferences; this.priceFeedService = priceFeedService; this.accountAgeWitnessService = accountAgeWitnessService; this.navigation = navigation; this.p2PService = p2PService; } @Override protected void activate() { // when leaving screen we reset state offer.setState(Offer.State.UNKNOWN); addListeners(); updateBalances(); // TODO In case that we have funded but restarted, or canceled but took again the offer we would need to // store locally the result when we received the funding tx(s). // For now we just ignore that rare case and bypass the check by setting a sufficient value // if (isWalletFunded.get()) // feeFromFundingTxProperty.set(FeePolicy.getMinRequiredFeeForFundingTx()); if (isTabSelected) priceFeedService.setCurrencyCode(offer.getCounterCurrencyCode()); if (canTakeOffer()) { tradeManager.checkOfferAvailability(offer, false, paymentAccount.getId(), this.amount.get(), () -> { }, errorMessage -> { log.warn(errorMessage); if (offer.getState() != Offer.State.NOT_AVAILABLE && offer.getState() != Offer.State.INVALID) { // handled elsewhere in UI new Popup().warning(errorMessage).show(); } }); } } @Override protected void deactivate() { removeListeners(); if (offer != null) { offer.cancelAvailabilityRequest(); } } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// // called before activate void initWithData(Offer offer) { this.offer = offer; tradePrice = offer.getPrice(); addressEntry = xmrWalletService.getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); checkNotNull(addressEntry, "addressEntry must not be null"); ObservableList possiblePaymentAccounts = getPossiblePaymentAccounts(); checkArgument(!possiblePaymentAccounts.isEmpty(), "possiblePaymentAccounts.isEmpty()"); paymentAccount = getLastSelectedPaymentAccount(); this.amount.set(getMaxTradeLimit()); updateSecurityDeposit(); calculateVolume(); calculateTotalToPay(); balanceListener = new XmrBalanceListener(addressEntry.getSubaddressIndex()) { @Override public void onBalanceChanged(BigInteger balance) { updateBalances(); } }; offer.resetState(); priceFeedService.setCurrencyCode(offer.getCounterCurrencyCode()); } // We don't want that the fee gets updated anymore after we show the funding screen. void onShowPayFundsScreen() { calculateTotalToPay(); } void onTabSelected(boolean isSelected) { this.isTabSelected = isSelected; if (isTabSelected) priceFeedService.setCurrencyCode(offer.getCounterCurrencyCode()); } public void onClose(boolean removeOffer) { // We do not wait until the offer got removed by a network remove message but remove it // directly from the offer book. The broadcast gets now bundled and has 2 sec. delay so the // removal from the network is a bit slower as it has been before. To avoid that the taker gets // confused to see the same offer still in the offerbook we remove it manually. This removal has // only local effect. Other trader might see the offer for a few seconds // still (but cannot take it). if (removeOffer) { offerBook.removeOffer(checkNotNull(offer)); } // reset address entries off thread ThreadUtils.submitToPool(() -> xmrWalletService.resetAddressEntriesForOpenOffer(offer.getId())); } protected void updateBalances() { super.updateBalances(); // update remaining balance UserThread.await(() -> { missingCoin.set(offerUtil.getBalanceShortage(totalToPay.get(), unallocatedBalance.get())); isXmrWalletFunded.set(offerUtil.isBalanceSufficient(totalToPay.get(), availableBalance.get())); if (totalToPay.get() != null && isXmrWalletFunded.get() && !showWalletFundedNotification.get()) { showWalletFundedNotification.set(true); } }); } /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// void onTakeOffer(TradeResultHandler tradeResultHandler, ErrorMessageHandler errorMessageHandler) { checkNotNull(getTakerFee(), "takerFee must not be null"); BigInteger fundsNeededForTrade = getFundsNeededForTrade(); if (isBuyOffer()) fundsNeededForTrade = fundsNeededForTrade.add(amount.get()); String errorMsg = null; if (filterManager.isCurrencyBanned(offer.getCounterCurrencyCode())) { errorMsg = Res.get("offerbook.warning.currencyBanned"); } else if (filterManager.isPaymentMethodBanned(offer.getPaymentMethod())) { errorMsg = Res.get("offerbook.warning.paymentMethodBanned"); } else if (filterManager.isOfferIdBanned(offer.getId())) { errorMsg = Res.get("offerbook.warning.offerBlocked"); } else if (filterManager.isNodeAddressBanned(offer.getMakerNodeAddress())) { errorMsg = Res.get("offerbook.warning.nodeBlocked"); } else if (filterManager.requireUpdateToNewVersionForTrading()) { errorMsg = Res.get("offerbook.warning.requireUpdateToNewVersion"); } else if (tradeManager.wasOfferAlreadyUsedInTrade(offer.getId())) { errorMsg = Res.get("offerbook.warning.offerWasAlreadyUsedInTrade"); } else { tradeManager.onTakeOffer(amount.get(), fundsNeededForTrade, offer, paymentAccount.getId(), useSavingsWallet, false, tradeResultHandler, errorMessage -> { log.warn(errorMessage); errorMessageHandler.handleErrorMessage(errorMessage); } ); } // handle error if (errorMsg != null) { new Popup().warning(errorMsg).show(); log.warn("Error taking offer " + offer.getId() + ": " + errorMsg); errorMessageHandler.handleErrorMessage(errorMsg); } } public void onPaymentAccountSelected(PaymentAccount paymentAccount) { if (paymentAccount != null) { this.paymentAccount = paymentAccount; this.amount.set(getMaxTradeLimit()); preferences.setTakeOfferSelectedPaymentAccountId(paymentAccount.getId()); } } void fundFromSavingsWallet() { useSavingsWallet = true; updateBalances(); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// OfferDirection getDirection() { return offer.getDirection(); } public Offer getOffer() { return offer; } ReadOnlyObjectProperty getVolume() { return volume; } ObservableList getPossiblePaymentAccounts() { Set paymentAccounts = user.getPaymentAccounts(); checkNotNull(paymentAccounts, "paymentAccounts must not be null"); return PaymentAccountUtil.getPossiblePaymentAccounts(offer, paymentAccounts, accountAgeWitnessService); } public PaymentAccount getLastSelectedPaymentAccount() { ObservableList possiblePaymentAccounts = getPossiblePaymentAccounts(); checkArgument(!possiblePaymentAccounts.isEmpty(), "possiblePaymentAccounts must not be empty"); PaymentAccount firstItem = possiblePaymentAccounts.get(0); String id = preferences.getTakeOfferSelectedPaymentAccountId(); if (id == null) return firstItem; return possiblePaymentAccounts.stream() .filter(e -> e.getId().equals(id)) .findAny() .orElse(firstItem); } BigInteger getMyMaxTradeLimit() { if (paymentAccount != null) { return BigInteger.valueOf(accountAgeWitnessService.getMyTradeLimit(paymentAccount, getCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit())); } else { return BigInteger.ZERO; } } BigInteger getMaxTradeLimit() { return offer.getAmount().min(getMyMaxTradeLimit()); } boolean canTakeOffer() { return GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation) && GUIUtil.isBootstrappedOrShowPopup(p2PService); } /////////////////////////////////////////////////////////////////////////////////////////// // Bindings, listeners /////////////////////////////////////////////////////////////////////////////////////////// private void addListeners() { xmrWalletService.addBalanceListener(balanceListener); } private void removeListeners() { xmrWalletService.removeBalanceListener(balanceListener); } /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// void calculateVolume() { if (tradePrice != null && offer != null && amount.get() != null && amount.get().compareTo(BigInteger.ZERO) != 0) { Volume volumeByAmount = tradePrice.getVolumeByAmount(amount.get()); volumeByAmount = VolumeUtil.getAdjustedVolume(volumeByAmount, offer.getPaymentMethod().getId()); volume.set(volumeByAmount); updateBalances(); } } void calculateAmount() { if (isNonZeroPrice.test(tradePrice) && isNonZeroVolume.test(volume) && allowAmountUpdate) { try { Volume volumeBefore = volume.get(); calculateVolume(); // if the volume != amount * price, we need to adjust the amount if (amount.get() == null || !volumeBefore.equals(tradePrice.getVolumeByAmount(amount.get()))) { BigInteger value = tradePrice.getAmountByVolume(volumeBefore); value = value.min(offer.getAmount()); // adjust if above maximum value = value.max(offer.getMinAmount()); // adjust if below minimum value = CoinUtil.getRoundedAmount(value, tradePrice, offer.getMinAmount(), getMaxTradeLimit(), offer.getCounterCurrencyCode(), paymentAccount.getPaymentMethod().getId()); amount.set(value); } calculateTotalToPay(); } catch (Throwable t) { log.error(t.toString()); } } } protected void setVolume(Volume volume) { this.volume.set(volume); } void maybeApplyAmount(BigInteger amount) { if (amount.compareTo(offer.getMinAmount()) >= 0 && amount.compareTo(getMaxTradeLimit()) <= 0) { this.amount.set(amount); } calculateTotalToPay(); } void calculateTotalToPay() { updateSecurityDeposit(); // Taker pays 2 times the tx fee because the mining fee might be different when maker created the offer // and reserved his funds, so that would not work well with dynamic fees. // The mining fee for the takeOfferFee tx is deducted from the createOfferFee and not visible to the trader final BigInteger takerFee = getTakerFee(); if (offer != null && amount.get() != null && takerFee != null) { BigInteger feeAndSecDeposit = securityDeposit.add(takerFee); if (isBuyOffer()) totalToPay.set(feeAndSecDeposit.add(amount.get())); else totalToPay.set(feeAndSecDeposit); updateBalances(); log.debug("totalToPay {}", totalToPay.get()); } } boolean isBuyOffer() { return getDirection() == OfferDirection.BUY; } boolean isSellOffer() { return getDirection() == OfferDirection.SELL; } boolean isCryptoCurrency() { return CurrencyUtil.isCryptoCurrency(getCurrencyCode()); } @Nullable BigInteger getTakerFee() { return HavenoUtils.multiply(this.amount.get(), offer.getTakerFeePct()); } public void swapTradeToSavings() { log.debug("swapTradeToSavings, offerId={}", offer.getId()); xmrWalletService.resetAddressEntriesForOpenOffer(offer.getId()); } /* private void setFeeFromFundingTx(Coin fee) { feeFromFundingTx = fee; isFeeFromFundingTxSufficient.set(feeFromFundingTx.compareTo(FeePolicy.getMinRequiredFeeForFundingTx()) >= 0); }*/ boolean isMinAmountLessOrEqualAmount() { //noinspection SimplifiableIfStatement if (offer != null && amount.get() != null) return offer.getMinAmount().compareTo(amount.get()) <= 0; return true; } boolean isAmountLargerThanOfferAmount() { //noinspection SimplifiableIfStatement if (amount.get() != null && offer != null) return amount.get().compareTo(offer.getAmount()) > 0; return true; } boolean wouldCreateDustForMaker() { return false; // TODO: update for XMR? } ReadOnlyObjectProperty getAmount() { return amount; } public PaymentMethod getPaymentMethod() { return offer.getPaymentMethod(); } public String getCurrencyCode() { return offer.getCounterCurrencyCode(); } public String getCurrencyNameAndCode() { return CurrencyUtil.getNameByCode(offer.getCounterCurrencyCode()); } @NotNull private BigInteger getFundsNeededForTrade() { return getSecurityDeposit(); } public XmrAddressEntry getAddressEntry() { return addressEntry; } public BigInteger getSecurityDeposit() { return securityDeposit; } private void updateSecurityDeposit() { securityDeposit = offer.getDirection() == OfferDirection.SELL ? getBuyerSecurityDeposit() : getSellerSecurityDeposit(); } private BigInteger getBuyerSecurityDeposit() { return offer.getOfferPayload().getBuyerSecurityDepositForTradeAmount(amount.get()); } private BigInteger getSellerSecurityDeposit() { return offer.getOfferPayload().getSellerSecurityDepositForTradeAmount(amount.get()); } public boolean isRoundedForAtmCash() { return PaymentMethod.isRoundedForAtmCash(paymentAccount.getPaymentMethod().getId()); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java ================================================ /* * 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 . */ package haveno.desktop.main.offer.takeoffer; import com.google.inject.Inject; import com.google.inject.name.Named; import com.jfoenix.controls.JFXTextField; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.util.Tuple2; import haveno.common.util.Tuple3; import haveno.common.util.Tuple4; import haveno.common.util.Utilities; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.offer.Offer; import haveno.core.payment.FasterPaymentsAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentMethod; import haveno.core.trade.HavenoUtils; import haveno.core.user.DontShowAgainLookup; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.Navigation; import haveno.desktop.common.view.ActivatableViewAndModel; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.AddressTextField; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.BalanceTextField; import haveno.desktop.components.BusyAnimation; import haveno.desktop.components.FundsTextField; import haveno.desktop.components.InfoInputTextField; import haveno.desktop.components.InputTextField; import haveno.desktop.components.TitledGroupBg; import haveno.desktop.main.MainView; import haveno.desktop.main.funds.FundsView; import haveno.desktop.main.funds.withdrawal.WithdrawalView; import haveno.desktop.main.offer.ClosableView; import haveno.desktop.main.offer.InitializableViewWithTakeOfferData; import haveno.desktop.main.offer.OfferView; import haveno.desktop.main.offer.OfferViewUtil; import static haveno.desktop.main.offer.OfferViewUtil.addPayInfoEntry; import haveno.desktop.main.offer.SelectableView; import haveno.desktop.main.overlays.notifications.Notification; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.GenericMessageWindow; import haveno.desktop.main.overlays.windows.OfferDetailsWindow; import haveno.desktop.main.overlays.windows.QRCodeWindow; import haveno.desktop.main.portfolio.PortfolioView; import haveno.desktop.main.portfolio.pendingtrades.PendingTradesView; import static haveno.desktop.util.FormBuilder.add2ButtonsWithBox; import static haveno.desktop.util.FormBuilder.addAddressTextField; import static haveno.desktop.util.FormBuilder.addBalanceTextField; import static haveno.desktop.util.FormBuilder.addComboBoxTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextArea; import static haveno.desktop.util.FormBuilder.addFundsTextfield; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import static haveno.desktop.util.FormBuilder.getEditableValueBox; import static haveno.desktop.util.FormBuilder.getIconForLabel; import static haveno.desktop.util.FormBuilder.getNonEditableValueBox; import static haveno.desktop.util.FormBuilder.getNonEditableValueBoxWithInfo; import static haveno.desktop.util.FormBuilder.getSmallIconForLabel; import static haveno.desktop.util.FormBuilder.getTopLabelWithVBox; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.Layout; import haveno.desktop.util.Transitions; import java.io.ByteArrayInputStream; import java.math.BigInteger; import java.net.URI; import java.util.HashMap; import java.util.concurrent.TimeUnit; import static javafx.beans.binding.Bindings.createStringBinding; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ChangeListener; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Orientation; import javafx.geometry.Pos; import javafx.geometry.VPos; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.control.Separator; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import net.glxn.qrgen.QRCode; import net.glxn.qrgen.image.ImageType; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; import org.jetbrains.annotations.NotNull; @FxmlView public class TakeOfferView extends ActivatableViewAndModel implements ClosableView, InitializableViewWithTakeOfferData, SelectableView { private final Navigation navigation; private final CoinFormatter formatter; private final OfferDetailsWindow offerDetailsWindow; private final Transitions transitions; private ScrollPane scrollPane; private GridPane gridPane; private TitledGroupBg noFundingRequiredTitledGroupBg; private Label noFundingRequiredLabel; private int gridRowNoFundingRequired; private TitledGroupBg payFundsTitledGroupBg; private TitledGroupBg advancedOptionsGroup; private VBox priceAsPercentageInputBox, amountRangeBox; private HBox fundingHBox, amountValueCurrencyBox, priceValueCurrencyBox, volumeValueCurrencyBox, priceAsPercentageValueCurrencyBox, minAmountValueCurrencyBox, advancedOptionsBox, takeOfferBox, nextButtonBox, firstRowHBox; private ComboBox paymentAccountsComboBox; private TextArea extraInfoTextArea; private Label amountDescriptionLabel, paymentMethodLabel, priceCurrencyLabel, priceAsPercentageLabel, volumeCurrencyLabel, priceDescriptionLabel, volumeDescriptionLabel, waitingForFundsLabel, offerAvailabilityLabel, priceAsPercentageDescription, tradeFeeDescriptionLabel, resultLabel, tradeFeeInXmrLabel, xLabel, fakeXLabel, extraInfoLabel; private InputTextField amountTextField, volumeTextField; private TextField paymentMethodTextField, currencyTextField, priceTextField, priceAsPercentageTextField, amountRangeTextField; private FundsTextField totalToPayTextField; private AddressTextField addressTextField; private BalanceTextField balanceTextField; private Text xIcon, fakeXIcon; private Button nextButton, cancelButton1, cancelButton2; private AutoTooltipButton takeOfferButton, fundFromSavingsWalletButton; private ImageView qrCodeImageView; private StackPane qrCodePane; private BusyAnimation waitingForFundsBusyAnimation, offerAvailabilityBusyAnimation; private Notification walletFundedNotification; private OfferView.CloseHandler closeHandler; private Subscription balanceSubscription, showTransactionPublishedScreenSubscription, showWarningInvalidXmrDecimalPlacesSubscription, isWaitingForFundsSubscription, offerWarningSubscription, errorMessageSubscription, isOfferAvailableSubscription; private ChangeListener missingCoinListener; private int gridRow = 0; private final HashMap paymentAccountWarningDisplayed = new HashMap<>(); private boolean offerDetailsWindowDisplayed, extraInfoPopupDisplayed, zelleWarningDisplayed, fasterPaymentsWarningDisplayed, takeOfferFromUnsignedAccountWarningDisplayed, payByMailWarningDisplayed, cashAtAtmWarningDisplayed, australiaPayidWarningDisplayed, paypalWarningDisplayed, cashAppWarningDisplayed, F2FWarningDisplayed; private SimpleBooleanProperty errorPopupDisplayed; private ChangeListener amountFocusedListener, volumeFocusedListener, getShowWalletFundedNotificationListener; private InfoInputTextField volumeInfoTextField; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject private TakeOfferView(TakeOfferViewModel model, Navigation navigation, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, OfferDetailsWindow offerDetailsWindow, Transitions transitions) { super(model); this.navigation = navigation; this.formatter = formatter; this.offerDetailsWindow = offerDetailsWindow; this.transitions = transitions; } @Override protected void initialize() { addScrollPane(); addGridPane(); addPaymentGroup(); addAmountPriceGroup(); addOptionsGroup(); createListeners(); addNextButtons(); addOfferAvailabilityLabel(); addFundingGroup(); balanceTextField.setFormatter(model.getXmrFormatter()); GUIUtil.focusWhenAddedToScene(amountTextField); } @Override protected void activate() { addBindings(); addSubscriptions(); addListeners(); if (offerAvailabilityBusyAnimation != null && !model.showPayFundsScreenDisplayed.get()) { // temporarily disabled due to high CPU usage (per issue #4649) // offerAvailabilityBusyAnimation.play(); offerAvailabilityLabel.setVisible(true); offerAvailabilityLabel.setManaged(true); } else { offerAvailabilityLabel.setVisible(false); offerAvailabilityLabel.setManaged(false); } if (waitingForFundsBusyAnimation != null && model.isWaitingForFunds.get()) { // temporarily disabled due to high CPU usage (per issue #4649) // waitingForFundsBusyAnimation.play(); waitingForFundsLabel.setVisible(true); waitingForFundsLabel.setManaged(true); } else { waitingForFundsLabel.setVisible(false); waitingForFundsLabel.setManaged(false); } String currencyCode = model.dataModel.getCurrencyCode(); volumeCurrencyLabel.setText(currencyCode); priceDescriptionLabel.setText(CurrencyUtil.getPriceWithCurrencyCode(currencyCode)); volumeDescriptionLabel.setText(model.volumeDescriptionLabel.get()); PaymentAccount lastPaymentAccount = model.getLastSelectedPaymentAccount(); if (model.getPossiblePaymentAccounts().size() > 1) { new Popup().headLine(Res.get("popup.info.multiplePaymentAccounts.headline")) .information(Res.get("popup.info.multiplePaymentAccounts.msg")) .dontShowAgainId("MultiplePaymentAccountsAvailableWarning") .show(); paymentAccountsComboBox.setItems(model.getPossiblePaymentAccounts()); paymentAccountsComboBox.getSelectionModel().select(lastPaymentAccount); model.onPaymentAccountSelected(lastPaymentAccount); } balanceTextField.setTargetAmount(model.dataModel.getTotalToPay().get()); maybeShowExtraInfoPopup(model.dataModel.getOffer()); maybeShowTakeOfferFromUnsignedAccountWarning(model.dataModel.getOffer()); maybeShowZelleWarning(lastPaymentAccount); maybeShowFasterPaymentsWarning(lastPaymentAccount); maybeShowAccountWarning(lastPaymentAccount, model.dataModel.isBuyOffer()); if (!model.isRange()) { nextButton.setVisible(false); cancelButton1.setVisible(false); if (model.isOfferAvailable.get()) showNextStepAfterAmountIsSet(); } } @Override protected void deactivate() { removeBindings(); removeSubscriptions(); removeListeners(); if (offerAvailabilityBusyAnimation != null) offerAvailabilityBusyAnimation.stop(); if (waitingForFundsBusyAnimation != null) waitingForFundsBusyAnimation.stop(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public void initWithData(Offer offer) { model.initWithData(offer); priceAsPercentageInputBox.setVisible(offer.isUseMarketBasedPrice()); if (OfferViewUtil.isShownAsSellOffer(model.getOffer())) { takeOfferButton.setId("buy-button-big"); nextButton.setId("buy-button"); fundFromSavingsWalletButton.setId("buy-button"); takeOfferButton.updateText(getTakeOfferLabel(offer, false)); } else { takeOfferButton.setId("sell-button-big"); nextButton.setId("sell-button"); fundFromSavingsWalletButton.setId("sell-button"); takeOfferButton.updateText(getTakeOfferLabel(offer, true)); } priceAsPercentageDescription.setText(model.getPercentagePriceDescription()); boolean showComboBox = model.getPossiblePaymentAccounts().size() > 1; paymentAccountsComboBox.setVisible(showComboBox); paymentAccountsComboBox.setManaged(showComboBox); paymentAccountsComboBox.setMouseTransparent(!showComboBox); paymentMethodTextField.setVisible(!showComboBox); paymentMethodTextField.setManaged(!showComboBox); paymentMethodLabel.setVisible(!showComboBox); paymentMethodLabel.setManaged(!showComboBox); if (!showComboBox) { paymentMethodTextField.setText(model.getPossiblePaymentAccounts().get(0).getAccountName()); } currencyTextField.setText(model.dataModel.getCurrencyNameAndCode()); amountDescriptionLabel.setText(model.getAmountDescription()); if (model.isRange()) { amountRangeTextField.setText(model.getAmountRange()); amountRangeBox.setVisible(true); amountRangeBox.setManaged(true); volumeTextField.setDisable(false); } else { amountTextField.setDisable(true); amountTextField.setManaged(true); } priceTextField.setText(model.getPrice()); priceAsPercentageTextField.setText(model.marketPriceMargin); addressTextField.setPaymentLabel(model.getPaymentLabel()); addressTextField.setAddress(model.dataModel.getAddressEntry().getAddressString()); if (offer.isFiatOffer()) { Label popOverLabel = OfferViewUtil.createPopOverLabel(Res.get("offerbook.info.roundedFiatVolume")); volumeInfoTextField.setContentForPrivacyPopOver(popOverLabel); } if (offer.getPrice() == null) new Popup().warning(Res.get("takeOffer.noPriceFeedAvailable")) .onClose(() -> close(false)) .show(); if (offer.hasBuyerAsTakerWithoutDeposit() && offer.getCombinedExtraInfo() != null && !offer.getCombinedExtraInfo().isEmpty()) { // attach extra info text area //updateOfferElementsStyle(); Tuple2 extraInfoTuple = addCompactTopLabelTextArea(gridPane, ++gridRowNoFundingRequired, Res.get("payment.shared.extraInfo.noDeposit"), ""); extraInfoLabel = extraInfoTuple.first; extraInfoLabel.setVisible(false); extraInfoLabel.setManaged(false); extraInfoTextArea = extraInfoTuple.second; extraInfoTextArea.setVisible(false); extraInfoTextArea.setManaged(false); extraInfoTextArea.setText(offer.getCombinedExtraInfo().trim()); extraInfoTextArea.getStyleClass().addAll("text-area", "flat-text-area-with-border"); extraInfoTextArea.setWrapText(true); extraInfoTextArea.setMaxHeight(300); extraInfoTextArea.setEditable(false); GUIUtil.adjustHeightAutomatically(extraInfoTextArea); GridPane.setRowIndex(extraInfoTextArea, gridRowNoFundingRequired); GridPane.setColumnSpan(extraInfoTextArea, GridPane.REMAINING); GridPane.setColumnIndex(extraInfoTextArea, 0); // move up take offer buttons GridPane.setRowIndex(takeOfferBox, gridRowNoFundingRequired + 1); GridPane.setMargin(takeOfferBox, new Insets(15, 0, 0, 0)); } } @Override public void setCloseHandler(OfferView.CloseHandler closeHandler) { this.closeHandler = closeHandler; } // Called from parent as the view does not get notified when the tab is closed public void onClose() { BigInteger availableBalance = model.dataModel.getAvailableBalance().get(); if (availableBalance != null && availableBalance.compareTo(BigInteger.ZERO) > 0 && !model.takeOfferCompleted.get() && !DevEnv.isDevMode()) { model.dataModel.swapTradeToSavings(); } } @Override public void onTabSelected(boolean isSelected) { model.dataModel.onTabSelected(isSelected); } /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// private void onTakeOffer() { if (!model.dataModel.canTakeOffer()) { return; } if (DevEnv.isDevMode()) { balanceSubscription.unsubscribe(); model.onTakeOffer(() -> { }); return; } offerDetailsWindow.onTakeOffer(() -> model.onTakeOffer(() -> { offerDetailsWindow.hide(); offerDetailsWindowDisplayed = false; }) ).show(model.getOffer(), model.dataModel.getAmount().get(), model.dataModel.tradePrice); offerDetailsWindowDisplayed = true; } private void onShowPayFundsScreen() { scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); nextButton.setVisible(false); nextButton.setManaged(false); nextButton.setOnAction(null); cancelButton1.setVisible(false); cancelButton1.setManaged(false); cancelButton1.setOnAction(null); offerAvailabilityBusyAnimation.stop(); offerAvailabilityBusyAnimation.setVisible(false); offerAvailabilityBusyAnimation.setManaged(false); offerAvailabilityLabel.setVisible(false); offerAvailabilityLabel.setManaged(false); int delay = 500; int diff = 100; transitions.fadeOutAndRemove(advancedOptionsGroup, delay, (event) -> { }); delay -= diff; transitions.fadeOutAndRemove(advancedOptionsBox, delay); model.onShowPayFundsScreen(); amountTextField.setMouseTransparent(true); amountTextField.setDisable(false); amountTextField.setFocusTraversable(false); amountRangeTextField.setMouseTransparent(true); amountRangeTextField.setDisable(false); amountRangeTextField.setFocusTraversable(false); priceTextField.setMouseTransparent(true); priceTextField.setDisable(false); priceTextField.setFocusTraversable(false); priceAsPercentageTextField.setMouseTransparent(true); priceAsPercentageTextField.setDisable(false); priceAsPercentageTextField.setFocusTraversable(false); volumeTextField.setMouseTransparent(true); volumeTextField.setDisable(false); volumeTextField.setFocusTraversable(false); updateOfferElementsStyle(); balanceTextField.setTargetAmount(model.dataModel.getTotalToPay().get()); if (!DevEnv.isDevMode() && model.dataModel.hasTotalToPay()) { String tradeAmountText = model.isSeller() ? Res.get("takeOffer.takeOfferFundWalletInfo.tradeAmount", model.getTradeAmount()) : ""; String message = Res.get("takeOffer.takeOfferFundWalletInfo.msg", model.getTotalToPayInfo(), tradeAmountText, model.getSecurityDepositInfo(), model.getTradeFee() ); String key = "takeOfferFundWalletInfo"; new Popup().headLine(Res.get("takeOffer.takeOfferFundWalletInfo.headline")) .instruction(message) .dontShowAgainId(key) .show(); } cancelButton2.setVisible(true); cancelButton2.setManaged(true); // temporarily disabled due to high CPU usage (per issue #4649) //waitingForFundsBusyAnimation.play(); if (model.getOffer().hasBuyerAsTakerWithoutDeposit()) { noFundingRequiredTitledGroupBg.setVisible(true); noFundingRequiredTitledGroupBg.setManaged(true); noFundingRequiredLabel.setVisible(true); noFundingRequiredLabel.setManaged(true); if (model.getOffer().getCombinedExtraInfo() != null && !model.getOffer().getCombinedExtraInfo().isEmpty()) { extraInfoLabel.setVisible(true); extraInfoLabel.setManaged(true); extraInfoTextArea.setVisible(true); extraInfoTextArea.setManaged(true); } } else { payFundsTitledGroupBg.setVisible(true); payFundsTitledGroupBg.setManaged(true); totalToPayTextField.setVisible(true); totalToPayTextField.setManaged(true); addressTextField.setVisible(true); addressTextField.setManaged(true); qrCodePane.setVisible(true); qrCodePane.setManaged(true); balanceTextField.setVisible(true); balanceTextField.setManaged(true); } totalToPayTextField.setFundsStructure(Res.get("takeOffer.fundsBox.fundsStructure", model.getSecurityDepositWithCode(), model.getTakerFeePercentage())); totalToPayTextField.setContentForInfoPopOver(createInfoPopover()); if (model.dataModel.getIsXmrWalletFunded().get() && model.dataModel.hasTotalToPay()) { if (walletFundedNotification == null) { walletFundedNotification = new Notification() .headLine(Res.get("notification.walletUpdate.headline")) .notification(Res.get("notification.takeOffer.walletUpdate.msg", HavenoUtils.formatXmr(model.dataModel.getTotalToPay().get(), true))) .autoClose(); walletFundedNotification.show(); } } updateQrCode(); } private void updateQrCode() { final byte[] imageBytes = QRCode .from(getMoneroURI()) .withSize(300, 300) .to(ImageType.PNG) .stream() .toByteArray(); Image qrImage = new Image(new ByteArrayInputStream(imageBytes)); qrCodeImageView.setImage(qrImage); } private void updateOfferElementsStyle() { GridPane.setColumnSpan(firstRowHBox, 1); final String activeInputStyle = "offer-input"; final String readOnlyInputStyle = "offer-input-readonly"; amountValueCurrencyBox.getStyleClass().remove(activeInputStyle); amountValueCurrencyBox.getStyleClass().add(readOnlyInputStyle); priceAsPercentageValueCurrencyBox.getStyleClass().remove(activeInputStyle); priceAsPercentageValueCurrencyBox.getStyleClass().add(readOnlyInputStyle); volumeValueCurrencyBox.getStyleClass().remove(activeInputStyle); volumeValueCurrencyBox.getStyleClass().add(readOnlyInputStyle); priceValueCurrencyBox.getStyleClass().remove(activeInputStyle); priceValueCurrencyBox.getStyleClass().add(readOnlyInputStyle); minAmountValueCurrencyBox.getStyleClass().remove(activeInputStyle); minAmountValueCurrencyBox.getStyleClass().add(readOnlyInputStyle); resultLabel.getStyleClass().add("small"); xLabel.getStyleClass().add("small"); xIcon.setStyle(String.format("-fx-font-family: %s; -fx-font-size: %s;", MaterialDesignIcon.CLOSE.fontFamily(), "1em")); fakeXIcon.setStyle(String.format("-fx-font-family: %s; -fx-font-size: %s;", MaterialDesignIcon.CLOSE.fontFamily(), "1em")); fakeXLabel.getStyleClass().add("small"); } /////////////////////////////////////////////////////////////////////////////////////////// // Navigation /////////////////////////////////////////////////////////////////////////////////////////// private void close() { close(true); } private void close(boolean removeOffer) { model.dataModel.onClose(removeOffer); if (closeHandler != null) closeHandler.close(); } /////////////////////////////////////////////////////////////////////////////////////////// // Bindings, Listeners /////////////////////////////////////////////////////////////////////////////////////////// private void addBindings() { amountTextField.textProperty().bindBidirectional(model.amount); volumeTextField.textProperty().bindBidirectional(model.volume); totalToPayTextField.textProperty().bind(model.totalToPay); addressTextField.amountAsProperty().bind(model.dataModel.getMissingCoin()); amountTextField.validationResultProperty().bind(model.amountValidationResult); volumeTextField.validationResultProperty().bind(model.volumeValidationResult); priceCurrencyLabel.textProperty().bind(createStringBinding(() -> CurrencyUtil.getCounterCurrency(model.dataModel.getCurrencyCode()))); priceAsPercentageLabel.prefWidthProperty().bind(priceCurrencyLabel.widthProperty()); nextButton.disableProperty().bind(model.isNextButtonDisabled); tradeFeeInXmrLabel.textProperty().bind(model.tradeFeeInXmrWithFiat); tradeFeeDescriptionLabel.textProperty().bind(model.tradeFeeDescription); tradeFeeInXmrLabel.visibleProperty().bind(model.isTradeFeeVisible); tradeFeeDescriptionLabel.visibleProperty().bind(model.isTradeFeeVisible); tradeFeeDescriptionLabel.managedProperty().bind(tradeFeeDescriptionLabel.visibleProperty()); // funding fundingHBox.visibleProperty().bind(model.dataModel.getIsXmrWalletFunded().not().and(model.showPayFundsScreenDisplayed)); fundingHBox.managedProperty().bind(model.dataModel.getIsXmrWalletFunded().not().and(model.showPayFundsScreenDisplayed)); waitingForFundsLabel.textProperty().bind(model.spinnerInfoText); takeOfferBox.visibleProperty().bind(model.dataModel.getIsXmrWalletFunded().and(model.showPayFundsScreenDisplayed)); takeOfferBox.managedProperty().bind(model.dataModel.getIsXmrWalletFunded().and(model.showPayFundsScreenDisplayed)); takeOfferButton.disableProperty().bind(model.isTakeOfferButtonDisabled); } private void removeBindings() { amountTextField.textProperty().unbindBidirectional(model.amount); volumeTextField.textProperty().unbindBidirectional(model.volume); totalToPayTextField.textProperty().unbind(); addressTextField.amountAsProperty().unbind(); amountTextField.validationResultProperty().unbind(); priceCurrencyLabel.textProperty().unbind(); priceAsPercentageLabel.prefWidthProperty().unbind(); nextButton.disableProperty().unbind(); tradeFeeInXmrLabel.textProperty().unbind(); tradeFeeDescriptionLabel.textProperty().unbind(); tradeFeeInXmrLabel.visibleProperty().unbind(); tradeFeeDescriptionLabel.visibleProperty().unbind(); tradeFeeDescriptionLabel.managedProperty().unbind(); // funding fundingHBox.visibleProperty().unbind(); fundingHBox.managedProperty().unbind(); waitingForFundsLabel.textProperty().unbind(); takeOfferBox.visibleProperty().unbind(); takeOfferBox.managedProperty().unbind(); takeOfferButton.disableProperty().unbind(); } private void addSubscriptions() { errorPopupDisplayed = new SimpleBooleanProperty(); offerWarningSubscription = EasyBind.subscribe(model.offerWarning, newValue -> { if (newValue != null) { if (offerDetailsWindowDisplayed) offerDetailsWindow.hide(); UserThread.runAfter(() -> new Popup().warning(newValue + "\n\n" + Res.get("takeOffer.alreadyPaidInFunds")) .actionButtonTextWithGoTo("funds.tab.withdrawal") .onAction(() -> { errorPopupDisplayed.set(true); model.resetOfferWarning(); close(); navigation.navigateTo(MainView.class, FundsView.class, WithdrawalView.class); }) .onClose(() -> { errorPopupDisplayed.set(true); model.resetOfferWarning(); close(); }) .show(), 100, TimeUnit.MILLISECONDS); } }); errorMessageSubscription = EasyBind.subscribe(model.errorMessage, newValue -> { if (newValue != null) { new Popup().error(Res.get("takeOffer.error.message", model.errorMessage.get())) .onClose(() -> { errorPopupDisplayed.set(true); model.resetErrorMessage(); close(); }) .show(); } }); isOfferAvailableSubscription = EasyBind.subscribe(model.isOfferAvailable, isOfferAvailable -> { if (isOfferAvailable) { offerAvailabilityBusyAnimation.stop(); offerAvailabilityBusyAnimation.setVisible(false); offerAvailabilityBusyAnimation.setManaged(false); if (!model.isRange() && !model.showPayFundsScreenDisplayed.get()) showNextStepAfterAmountIsSet(); } offerAvailabilityLabel.setVisible(!isOfferAvailable); offerAvailabilityLabel.setManaged(!isOfferAvailable); }); isWaitingForFundsSubscription = EasyBind.subscribe(model.isWaitingForFunds, isWaitingForFunds -> { // temporarily disabled due to high CPU usage (per issue #4649) // waitingForFundsBusyAnimation.play(); waitingForFundsLabel.setVisible(isWaitingForFunds); waitingForFundsLabel.setManaged(isWaitingForFunds); }); showWarningInvalidXmrDecimalPlacesSubscription = EasyBind.subscribe(model.showWarningInvalidXmrDecimalPlaces, newValue -> { if (newValue) { new Popup().warning(Res.get("takeOffer.amountPriceBox.warning.invalidXmrDecimalPlaces")).show(); model.showWarningInvalidXmrDecimalPlaces.set(false); } }); showTransactionPublishedScreenSubscription = EasyBind.subscribe(model.showTransactionPublishedScreen, newValue -> { if (newValue && DevEnv.isDevMode()) { close(); } else if (newValue && model.getTrade() != null && !model.getTrade().hasFailed()) { String key = "takeOfferSuccessInfo"; if (DontShowAgainLookup.showAgain(key)) { UserThread.runAfter(() -> new Popup().headLine(Res.get("takeOffer.success.headline")) .feedback(Res.get("takeOffer.success.info")) .actionButtonTextWithGoTo("portfolio.tab.pendingTrades") .dontShowAgainId(key) .onAction(() -> { UserThread.runAfter( () -> navigation.navigateTo(MainView.class, PortfolioView.class, PendingTradesView.class) , 100, TimeUnit.MILLISECONDS); close(); }) .onClose(this::close) .show(), 1); } else { close(); } } }); balanceSubscription = EasyBind.subscribe(model.dataModel.getAvailableBalance(), balanceTextField::setBalance); } private void removeSubscriptions() { offerWarningSubscription.unsubscribe(); errorMessageSubscription.unsubscribe(); isOfferAvailableSubscription.unsubscribe(); isWaitingForFundsSubscription.unsubscribe(); showWarningInvalidXmrDecimalPlacesSubscription.unsubscribe(); showTransactionPublishedScreenSubscription.unsubscribe(); // noSufficientFeeSubscription.unsubscribe(); balanceSubscription.unsubscribe(); } private void createListeners() { amountFocusedListener = (o, oldValue, newValue) -> { model.onFocusOutAmountTextField(oldValue, newValue, amountTextField.getText()); amountTextField.setText(model.amount.get()); }; getShowWalletFundedNotificationListener = (observable, oldValue, newValue) -> { if (newValue) { Notification walletFundedNotification = new Notification() .headLine(Res.get("notification.walletUpdate.headline")) .notification(Res.get("notification.walletUpdate.msg", HavenoUtils.formatXmr(model.dataModel.getTotalToPay().get(), true))) .autoClose(); walletFundedNotification.show(); } }; volumeFocusedListener = (o, oldValue, newValue) -> { model.onFocusOutVolumeTextField(oldValue, newValue); volumeTextField.setText(model.volume.get()); }; missingCoinListener = (observable, oldValue, newValue) -> { if (!newValue.toString().equals("")) { updateQrCode(); } }; } private void addListeners() { amountTextField.focusedProperty().addListener(amountFocusedListener); volumeTextField.focusedProperty().addListener(volumeFocusedListener); model.dataModel.getShowWalletFundedNotification().addListener(getShowWalletFundedNotificationListener); model.dataModel.getMissingCoin().addListener(missingCoinListener); } private void removeListeners() { amountTextField.focusedProperty().removeListener(amountFocusedListener); volumeTextField.focusedProperty().removeListener(volumeFocusedListener); model.dataModel.getShowWalletFundedNotification().removeListener(getShowWalletFundedNotificationListener); model.dataModel.getMissingCoin().removeListener(missingCoinListener); } /////////////////////////////////////////////////////////////////////////////////////////// // Build UI elements /////////////////////////////////////////////////////////////////////////////////////////// private void addScrollPane() { scrollPane = GUIUtil.createScrollPane(); root.getChildren().add(scrollPane); } private void addGridPane() { gridPane = new GridPane(); gridPane.getStyleClass().add("content-pane"); gridPane.setPadding(new Insets(25, 25, 25, 25)); gridPane.setHgap(5); gridPane.setVgap(5); GUIUtil.setDefaultTwoColumnConstraintsForGridPane(gridPane); scrollPane.setContent(gridPane); } private void addPaymentGroup() { TitledGroupBg paymentAccountTitledGroupBg = addTitledGroupBg(gridPane, gridRow, 1, Res.get("offerbook.takeOffer")); GridPane.setColumnSpan(paymentAccountTitledGroupBg, 2); final Tuple4, Label, TextField, HBox> paymentAccountTuple = addComboBoxTopLabelTextField(gridPane, gridRow, Res.get("shared.chooseTradingAccount"), Res.get("shared.chooseTradingAccount"), Layout.FIRST_ROW_DISTANCE); paymentAccountsComboBox = paymentAccountTuple.first; HBox.setMargin(paymentAccountsComboBox, new Insets(Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); paymentAccountsComboBox.setConverter(GUIUtil.getPaymentAccountsComboBoxStringConverter()); paymentAccountsComboBox.setCellFactory(model.getPaymentAccountListCellFactory(paymentAccountsComboBox)); paymentAccountsComboBox.setVisible(false); paymentAccountsComboBox.setManaged(false); paymentAccountsComboBox.setOnAction(e -> { PaymentAccount paymentAccount = paymentAccountsComboBox.getSelectionModel().getSelectedItem(); if (paymentAccount != null) { maybeShowZelleWarning(paymentAccount); maybeShowFasterPaymentsWarning(paymentAccount); maybeShowAccountWarning(paymentAccount, model.dataModel.isBuyOffer()); } model.onPaymentAccountSelected(paymentAccount); }); paymentMethodLabel = paymentAccountTuple.second; paymentMethodTextField = paymentAccountTuple.third; paymentMethodTextField.setMinWidth(250); paymentMethodTextField.setEditable(false); paymentMethodTextField.setMouseTransparent(true); paymentMethodTextField.setFocusTraversable(false); currencyTextField = new JFXTextField(); currencyTextField.setMinWidth(250); currencyTextField.setEditable(false); currencyTextField.setMouseTransparent(true); currencyTextField.setFocusTraversable(false); final Tuple2 tradeCurrencyTuple = getTopLabelWithVBox(Res.get("shared.tradeCurrency"), currencyTextField); HBox.setMargin(tradeCurrencyTuple.second, new Insets(5, 0, 0, 0)); final HBox hBox = paymentAccountTuple.fourth; hBox.setSpacing(30); hBox.setAlignment(Pos.CENTER_LEFT); hBox.setPadding(new Insets(10, 0, 18, 0)); hBox.getChildren().add(tradeCurrencyTuple.second); } private void addAmountPriceGroup() { TitledGroupBg titledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 2, Res.get("takeOffer.setAmountPrice"), Layout.COMPACT_GROUP_DISTANCE); GridPane.setColumnSpan(titledGroupBg, 2); addAmountPriceFields(); addSecondRow(); } private void addOptionsGroup() { advancedOptionsGroup = addTitledGroupBg(gridPane, ++gridRow, 1, Res.get("shared.advancedOptions"), Layout.COMPACT_GROUP_DISTANCE); advancedOptionsBox = new HBox(); advancedOptionsBox.setSpacing(40); GridPane.setRowIndex(advancedOptionsBox, gridRow); GridPane.setColumnSpan(advancedOptionsBox, GridPane.REMAINING); GridPane.setColumnIndex(advancedOptionsBox, 0); GridPane.setHalignment(advancedOptionsBox, HPos.LEFT); GridPane.setMargin(advancedOptionsBox, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0)); gridPane.getChildren().add(advancedOptionsBox); VBox tradeFeeFieldsBox = getTradeFeeFieldsBox(); tradeFeeFieldsBox.setMinWidth(240); advancedOptionsBox.getChildren().addAll(tradeFeeFieldsBox); } private void addNextButtons() { Tuple3 tuple = add2ButtonsWithBox(gridPane, ++gridRow, Res.get("shared.nextStep"), Res.get("shared.cancel"), 15, true); nextButtonBox = tuple.third; nextButton = tuple.first; nextButton.setMaxWidth(200); nextButton.setDefaultButton(true); nextButton.setOnAction(e -> nextStepCheckMakerTx()); cancelButton1 = tuple.second; cancelButton1.setMaxWidth(200); cancelButton1.setDefaultButton(false); cancelButton1.setOnAction(e -> { model.dataModel.swapTradeToSavings(); close(false); }); } private void nextStepCheckMakerTx() { // TODO: pre-check if open offer's reserve tx is failed or double spend seen? showNextStepAfterAmountIsSet(); } private void showNextStepAfterAmountIsSet() { onShowPayFundsScreen(); } private void addOfferAvailabilityLabel() { offerAvailabilityBusyAnimation = new BusyAnimation(false); offerAvailabilityLabel = new AutoTooltipLabel(Res.get("takeOffer.fundsBox.isOfferAvailable")); HBox.setMargin(offerAvailabilityLabel, new Insets(6, 0, 0, 0)); nextButtonBox.getChildren().addAll(offerAvailabilityBusyAnimation, offerAvailabilityLabel); } private void addFundingGroup() { // no funding required title noFundingRequiredTitledGroupBg = addTitledGroupBg(gridPane, gridRow, 3, Res.get("takeOffer.fundsBox.noFundingRequiredTitle"), Layout.COMPACT_GROUP_DISTANCE); noFundingRequiredTitledGroupBg.getStyleClass().add("last"); GridPane.setColumnSpan(noFundingRequiredTitledGroupBg, 2); noFundingRequiredTitledGroupBg.setVisible(false); noFundingRequiredTitledGroupBg.setManaged(false); // no funding required description noFundingRequiredLabel = new AutoTooltipLabel(Res.get("takeOffer.fundsBox.noFundingRequiredDescription")); noFundingRequiredLabel.setVisible(false); noFundingRequiredLabel.setManaged(false); //GridPane.setRowSpan(noFundingRequiredLabel, 1); GridPane.setRowIndex(noFundingRequiredLabel, gridRow); noFundingRequiredLabel.setPadding(new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0)); GridPane.setHalignment(noFundingRequiredLabel, HPos.LEFT); gridPane.getChildren().add(noFundingRequiredLabel); gridRowNoFundingRequired = gridRow; // funding title payFundsTitledGroupBg = addTitledGroupBg(gridPane, gridRow, 3, Res.get("takeOffer.fundsBox.title"), Layout.COMPACT_GROUP_DISTANCE); payFundsTitledGroupBg.getStyleClass().add("last"); GridPane.setColumnSpan(payFundsTitledGroupBg, 2); payFundsTitledGroupBg.setVisible(false); payFundsTitledGroupBg.setManaged(false); totalToPayTextField = addFundsTextfield(gridPane, gridRow, Res.get("shared.totalsNeeded"), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); totalToPayTextField.setVisible(false); totalToPayTextField.setManaged(false); Tuple2 qrCodeTuple = GUIUtil.getSmallXmrQrCodePane(); qrCodePane = qrCodeTuple.first; qrCodeImageView = qrCodeTuple.second; Tooltip.install(qrCodePane, new Tooltip(Res.get("shared.openLargeQRWindow"))); qrCodePane.setOnMouseClicked(e -> UserThread.runAfter( () -> new QRCodeWindow(getMoneroURI()).show(), 200, TimeUnit.MILLISECONDS)); GridPane.setRowIndex(qrCodePane, gridRow); GridPane.setColumnIndex(qrCodePane, 1); GridPane.setRowSpan(qrCodePane, 3); GridPane.setValignment(qrCodePane, VPos.BOTTOM); GridPane.setMargin(qrCodePane, new Insets(Layout.FIRST_ROW_DISTANCE - 9, 0, 0, 10)); gridPane.getChildren().add(qrCodePane); qrCodePane.setVisible(false); qrCodePane.setManaged(false); addressTextField = addAddressTextField(gridPane, ++gridRow, Res.get("shared.tradeWalletAddress")); addressTextField.setVisible(false); addressTextField.setManaged(false); balanceTextField = addBalanceTextField(gridPane, ++gridRow, Res.get("shared.tradeWalletBalance")); balanceTextField.setVisible(false); balanceTextField.setManaged(false); fundingHBox = new HBox(); fundingHBox.setVisible(false); fundingHBox.setManaged(false); fundingHBox.setSpacing(10); fundFromSavingsWalletButton = new AutoTooltipButton(Res.get("shared.fundFromSavingsWalletButton")); fundFromSavingsWalletButton.setDefaultButton(true); fundFromSavingsWalletButton.getStyleClass().add("action-button"); fundFromSavingsWalletButton.setOnAction(e -> model.fundFromSavingsWallet()); Label label = new AutoTooltipLabel(Res.get("shared.OR")); label.setPadding(new Insets(5, 0, 0, 0)); Button fundFromExternalWalletButton = new AutoTooltipButton(Res.get("shared.fundFromExternalWalletButton")); fundFromExternalWalletButton.setDefaultButton(false); fundFromExternalWalletButton.setOnAction(e -> openWallet()); waitingForFundsBusyAnimation = new BusyAnimation(false); waitingForFundsLabel = new AutoTooltipLabel(); waitingForFundsLabel.setPadding(new Insets(5, 0, 0, 0)); fundingHBox.getChildren().addAll(fundFromSavingsWalletButton, label, fundFromExternalWalletButton, waitingForFundsBusyAnimation, waitingForFundsLabel); GridPane.setRowIndex(fundingHBox, ++gridRow); GridPane.setMargin(fundingHBox, new Insets(5, 0, 0, 0)); gridPane.getChildren().add(fundingHBox); takeOfferBox = new HBox(); takeOfferBox.setSpacing(10); GridPane.setRowIndex(takeOfferBox, gridRow); GridPane.setColumnSpan(takeOfferBox, 2); GridPane.setMargin(takeOfferBox, new Insets(5, 20, 0, 0)); gridPane.getChildren().add(takeOfferBox); takeOfferButton = new AutoTooltipButton(); takeOfferButton.setOnAction(e -> onTakeOffer()); takeOfferButton.setMinHeight(40); takeOfferButton.setPadding(new Insets(0, 20, 0, 20)); takeOfferBox.getChildren().add(takeOfferButton); takeOfferBox.visibleProperty().addListener((observable, oldValue, newValue) -> { UserThread.execute(() -> { if (newValue) { fundingHBox.getChildren().remove(cancelButton2); takeOfferBox.getChildren().add(cancelButton2); } else if (!fundingHBox.getChildren().contains(cancelButton2)) { takeOfferBox.getChildren().remove(cancelButton2); fundingHBox.getChildren().add(cancelButton2); } }); }); cancelButton2 = new AutoTooltipButton(Res.get("shared.cancel")); fundingHBox.getChildren().add(cancelButton2); cancelButton2.setOnAction(e -> { String key = "CreateOfferCancelAndFunded"; if (model.dataModel.getIsXmrWalletFunded().get() && model.dataModel.hasTotalToPay() && model.dataModel.preferences.showAgain(key)) { new Popup().backgroundInfo(Res.get("takeOffer.alreadyFunded.askCancel")) .closeButtonText(Res.get("shared.no")) .actionButtonText(Res.get("shared.yesCancel")) .onAction(() -> { model.dataModel.swapTradeToSavings(); close(false); }) .dontShowAgainId(key) .show(); } else { close(false); model.dataModel.swapTradeToSavings(); } }); cancelButton2.setDefaultButton(false); cancelButton2.setVisible(false); cancelButton2.setManaged(false); } private void openWallet() { try { Utilities.openURI(URI.create(getMoneroURI())); } catch (Exception ex) { log.warn(ex.getMessage()); new Popup().warning(Res.get("shared.openDefaultWalletFailed")).show(); } } @NotNull private String getMoneroURI() { return GUIUtil.getMoneroURI( model.dataModel.getAddressEntry().getAddressString(), model.dataModel.getMissingCoin().get(), model.getPaymentLabel()); } private void addAmountPriceFields() { // amountBox Tuple3 amountValueCurrencyBoxTuple = getEditableValueBox(Res.get("takeOffer.amount.prompt")); amountValueCurrencyBox = amountValueCurrencyBoxTuple.first; amountTextField = amountValueCurrencyBoxTuple.second; Tuple2 amountInputBoxTuple = getTradeInputBox(amountValueCurrencyBox, model.getAmountDescription()); amountDescriptionLabel = amountInputBoxTuple.first; VBox amountBox = amountInputBoxTuple.second; // x xLabel = new Label(); xIcon = getIconForLabel(MaterialDesignIcon.CLOSE, "2em", xLabel); xIcon.getStyleClass().add("opaque-icon"); xLabel.getStyleClass().addAll("opaque-icon-character"); // price Tuple3 priceValueCurrencyBoxTuple = getNonEditableValueBox(); priceValueCurrencyBox = priceValueCurrencyBoxTuple.first; priceTextField = priceValueCurrencyBoxTuple.second; priceCurrencyLabel = priceValueCurrencyBoxTuple.third; Tuple2 priceInputBoxTuple = getTradeInputBox(priceValueCurrencyBox, Res.get("takeOffer.amountPriceBox.priceDescription")); priceDescriptionLabel = priceInputBoxTuple.first; getSmallIconForLabel(MaterialDesignIcon.LOCK, priceDescriptionLabel, "small-icon-label"); VBox priceBox = priceInputBoxTuple.second; // = resultLabel = new AutoTooltipLabel("="); resultLabel.getStyleClass().addAll("opaque-icon-character"); // volume Tuple3 volumeValueCurrencyBoxTuple = getNonEditableValueBoxWithInfo(); volumeValueCurrencyBox = volumeValueCurrencyBoxTuple.first; volumeInfoTextField = volumeValueCurrencyBoxTuple.second; volumeTextField = volumeInfoTextField.getInputTextField(); volumeCurrencyLabel = volumeValueCurrencyBoxTuple.third; Tuple2 volumeInputBoxTuple = getTradeInputBox(volumeValueCurrencyBox, model.volumeDescriptionLabel.get()); volumeDescriptionLabel = volumeInputBoxTuple.first; VBox volumeBox = volumeInputBoxTuple.second; firstRowHBox = new HBox(); firstRowHBox.setSpacing(5); firstRowHBox.setAlignment(Pos.CENTER_LEFT); firstRowHBox.getChildren().addAll(amountBox, xLabel, priceBox, resultLabel, volumeBox); GridPane.setColumnSpan(firstRowHBox, 2); GridPane.setRowIndex(firstRowHBox, gridRow); GridPane.setMargin(firstRowHBox, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, 10, 0, 0)); gridPane.getChildren().add(firstRowHBox); } private void addSecondRow() { Tuple3 priceAsPercentageTuple = getNonEditableValueBox(); priceAsPercentageValueCurrencyBox = priceAsPercentageTuple.first; priceAsPercentageTextField = priceAsPercentageTuple.second; priceAsPercentageLabel = priceAsPercentageTuple.third; Tuple2 priceAsPercentageInputBoxTuple = getTradeInputBox(priceAsPercentageValueCurrencyBox, ""); priceAsPercentageDescription = priceAsPercentageInputBoxTuple.first; getSmallIconForLabel(MaterialDesignIcon.CHART_LINE, priceAsPercentageDescription, "small-icon-label"); priceAsPercentageInputBox = priceAsPercentageInputBoxTuple.second; priceAsPercentageLabel.setText("%"); Tuple3 amountValueCurrencyBoxTuple = getNonEditableValueBox(); amountRangeTextField = amountValueCurrencyBoxTuple.second; minAmountValueCurrencyBox = amountValueCurrencyBoxTuple.first; Tuple2 amountInputBoxTuple = getTradeInputBox(minAmountValueCurrencyBox, Res.get("takeOffer.amountPriceBox.amountRangeDescription")); amountRangeBox = amountInputBoxTuple.second; amountRangeBox.setVisible(false); fakeXLabel = new Label(); fakeXIcon = getIconForLabel(MaterialDesignIcon.CLOSE, "2em", fakeXLabel); fakeXLabel.setVisible(false); // we just use it to get the same layout as the upper row fakeXLabel.getStyleClass().add("opaque-icon-character"); HBox hBox = new HBox(); hBox.setSpacing(5); hBox.setAlignment(Pos.CENTER_LEFT); hBox.getChildren().addAll(amountRangeBox, fakeXLabel, priceAsPercentageInputBox); GridPane.setRowIndex(hBox, ++gridRow); GridPane.setMargin(hBox, new Insets(0, 10, 10, 0)); gridPane.getChildren().add(hBox); } private VBox getTradeFeeFieldsBox() { tradeFeeInXmrLabel = new Label(); tradeFeeInXmrLabel.setMouseTransparent(true); tradeFeeInXmrLabel.setId("trade-fee-textfield"); VBox vBox = new VBox(); vBox.setSpacing(6); vBox.setMaxWidth(300); vBox.setAlignment(Pos.CENTER_LEFT); vBox.getChildren().addAll(tradeFeeInXmrLabel); HBox hBox = new HBox(); hBox.getChildren().addAll(vBox); hBox.setMinHeight(47); hBox.setMaxHeight(hBox.getMinHeight()); HBox.setHgrow(vBox, Priority.ALWAYS); final Tuple2 tradeInputBox = getTradeInputBox(hBox, Res.get("createOffer.tradeFee.description")); tradeFeeDescriptionLabel = tradeInputBox.first; return tradeInputBox.second; } /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// private void maybeShowExtraInfoPopup(Offer offer) { if (offer.getCombinedExtraInfo() != null && !offer.getCombinedExtraInfo().isEmpty() && !extraInfoPopupDisplayed) { extraInfoPopupDisplayed = true; UserThread.runAfter(() -> { new GenericMessageWindow() .preamble(Res.get("payment.tradingRestrictions")) .instruction(offer.getCombinedExtraInfo().trim()) .actionButtonText(Res.get("shared.iConfirm")) .closeButtonText(Res.get("shared.close")) .width(Layout.INITIAL_WINDOW_WIDTH) .onClose(() -> close(false)) .show(); }, 500, TimeUnit.MILLISECONDS); } } private void maybeShowTakeOfferFromUnsignedAccountWarning(Offer offer) { // warn if you are selling BTC to unsigned account (#5343) if (model.isSellingToAnUnsignedAccount(offer) && !takeOfferFromUnsignedAccountWarningDisplayed) { takeOfferFromUnsignedAccountWarningDisplayed = true; } } private void maybeShowZelleWarning(PaymentAccount paymentAccount) { if (paymentAccount.getPaymentMethod().getId().equals(PaymentMethod.ZELLE_ID) && !zelleWarningDisplayed) { zelleWarningDisplayed = true; UserThread.runAfter(GUIUtil::showZelleWarning, 500, TimeUnit.MILLISECONDS); } } private void maybeShowFasterPaymentsWarning(PaymentAccount paymentAccount) { if (paymentAccount.getPaymentMethod().getId().equals(PaymentMethod.FASTER_PAYMENTS_ID) && ((FasterPaymentsAccount) paymentAccount).getHolderName().isEmpty() && !fasterPaymentsWarningDisplayed) { fasterPaymentsWarningDisplayed = true; UserThread.runAfter(() -> GUIUtil.showFasterPaymentsWarning(navigation), 500, TimeUnit.MILLISECONDS); } } private void maybeShowAccountWarning(PaymentAccount paymentAccount, boolean isBuyer) { String msgKey = paymentAccount.getPreTradeMessage(!isBuyer); OfferViewUtil.showPaymentAccountWarning(msgKey, paymentAccountWarningDisplayed); } private Tuple2 getTradeInputBox(HBox amountValueBox, String promptText) { Label descriptionLabel = new AutoTooltipLabel(promptText); descriptionLabel.setId("input-description-label"); descriptionLabel.setPrefWidth(170); VBox box = new VBox(); box.setPadding(new Insets(10, 0, 0, 0)); box.setSpacing(2); box.getChildren().addAll(descriptionLabel, amountValueBox); return new Tuple2<>(descriptionLabel, box); } // As we don't use binding here we need to recreate it on mouse over to reflect the current state private GridPane createInfoPopover() { GridPane infoGridPane = new GridPane(); infoGridPane.setHgap(5); infoGridPane.setVgap(5); infoGridPane.setPadding(new Insets(10, 10, 10, 10)); int i = 0; if (model.isSeller()) { addPayInfoEntry(infoGridPane, i++, Res.get("takeOffer.fundsBox.tradeAmount"), model.getTradeAmount()); } addPayInfoEntry(infoGridPane, i++, Res.getWithCol("shared.yourSecurityDeposit"), model.getSecurityDepositInfo()); addPayInfoEntry(infoGridPane, i++, Res.get("takeOffer.fundsBox.offerFee"), model.getTradeFee()); Separator separator = new Separator(); separator.setOrientation(Orientation.HORIZONTAL); separator.getStyleClass().add("offer-separator"); GridPane.setConstraints(separator, 1, i++); infoGridPane.getChildren().add(separator); addPayInfoEntry(infoGridPane, i, Res.getWithCol("shared.total"), model.getTotalToPayInfo()); return infoGridPane; } @NotNull private String getTakeOfferLabel(Offer offer, boolean isBuyOffer) { return Res.get(isBuyOffer ? "takeOffer.takeOfferButton.sell" : "takeOffer.takeOfferButton.buy", offer.getCounterCurrencyCode()); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.offer.takeoffer; import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.UserThread; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.monetary.Price; import haveno.core.monetary.Volume; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OfferRestrictions; import haveno.core.offer.OfferUtil; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.validation.FiatVolumeValidator; import haveno.core.payment.validation.XmrValidator; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.util.FormattingUtils; import haveno.core.util.VolumeUtil; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.coin.CoinUtil; import haveno.core.util.validation.AmountValidator4Decimals; import haveno.core.util.validation.AmountValidator8Decimals; import haveno.core.util.validation.InputValidator; import haveno.core.util.validation.MonetaryValidator; import haveno.desktop.Navigation; import haveno.desktop.common.model.ActivatableWithDataModel; import haveno.desktop.common.model.ViewModel; import haveno.desktop.main.MainView; import haveno.desktop.main.funds.FundsView; import haveno.desktop.main.funds.deposit.DepositView; import haveno.desktop.main.offer.OfferViewModelUtil; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.DisplayUtils; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.P2PService; import haveno.network.p2p.network.CloseConnectionReason; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.ConnectionListener; import java.math.BigInteger; import static javafx.beans.binding.Bindings.createStringBinding; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import javafx.collections.ObservableList; import javafx.scene.control.ComboBox; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.util.Callback; import javax.annotation.Nullable; class TakeOfferViewModel extends ActivatableWithDataModel implements ViewModel { final TakeOfferDataModel dataModel; private final OfferUtil offerUtil; private final XmrValidator xmrValidator; private final P2PService p2PService; private final AccountAgeWitnessService accountAgeWitnessService; private final Navigation navigation; private final CoinFormatter xmrFormatter; private final FiatVolumeValidator fiatVolumeValidator; private final AmountValidator4Decimals amountValidator4Decimals; private final AmountValidator8Decimals amountValidator8Decimals; private String amountRange; private String paymentLabel; private boolean takeOfferRequested, ignoreVolumeStringListener; private Trade trade; protected Offer offer; private String price; private String amountDescription; final StringProperty amount = new SimpleStringProperty(); final StringProperty volume = new SimpleStringProperty(); final StringProperty volumeDescriptionLabel = new SimpleStringProperty(); final StringProperty totalToPay = new SimpleStringProperty(); final StringProperty errorMessage = new SimpleStringProperty(); final StringProperty offerWarning = new SimpleStringProperty(); final StringProperty spinnerInfoText = new SimpleStringProperty(""); final StringProperty tradeFee = new SimpleStringProperty(); final StringProperty tradeFeeInXmrWithFiat = new SimpleStringProperty(); final StringProperty tradeFeeDescription = new SimpleStringProperty(); final BooleanProperty isTradeFeeVisible = new SimpleBooleanProperty(false); final BooleanProperty isOfferAvailable = new SimpleBooleanProperty(); final BooleanProperty isTakeOfferButtonDisabled = new SimpleBooleanProperty(true); final BooleanProperty isNextButtonDisabled = new SimpleBooleanProperty(true); final BooleanProperty isWaitingForFunds = new SimpleBooleanProperty(); final BooleanProperty showWarningInvalidXmrDecimalPlaces = new SimpleBooleanProperty(); final BooleanProperty showTransactionPublishedScreen = new SimpleBooleanProperty(); final BooleanProperty takeOfferCompleted = new SimpleBooleanProperty(); final BooleanProperty showPayFundsScreenDisplayed = new SimpleBooleanProperty(); final ObjectProperty amountValidationResult = new SimpleObjectProperty<>(); final ObjectProperty volumeValidationResult = new SimpleObjectProperty<>(); private ChangeListener amountStrListener; private ChangeListener amountListener; private ChangeListener volumeStringListener; private ChangeListener volumeListener; private ChangeListener isWalletFundedListener; private ChangeListener tradeStateListener; private ChangeListener offerStateListener; private ConnectionListener connectionListener; // private Subscription isFeeSufficientSubscription; private Runnable takeOfferResultHandler; String marketPriceMargin; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject public TakeOfferViewModel(TakeOfferDataModel dataModel, FiatVolumeValidator fiatVolumeValidator, AmountValidator4Decimals amountValidator4Decimals, AmountValidator8Decimals amountValidator8Decimals, OfferUtil offerUtil, XmrValidator btcValidator, P2PService p2PService, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter) { super(dataModel); this.dataModel = dataModel; this.offerUtil = offerUtil; this.xmrValidator = btcValidator; this.p2PService = p2PService; this.accountAgeWitnessService = accountAgeWitnessService; this.navigation = navigation; this.xmrFormatter = btcFormatter; this.fiatVolumeValidator = fiatVolumeValidator; this.amountValidator4Decimals = amountValidator4Decimals; this.amountValidator8Decimals = amountValidator8Decimals; createListeners(); } @Override protected void activate() { addBindings(); addListeners(); String buyVolumeDescriptionKey = "createOffer.amountPriceBox.buy.volumeDescription"; String sellVolumeDescriptionKey = "createOffer.amountPriceBox.sell.volumeDescription"; if (dataModel.getDirection() == OfferDirection.SELL) { volumeDescriptionLabel.set(Res.get(buyVolumeDescriptionKey, dataModel.getCurrencyCode())); } else { volumeDescriptionLabel.set(Res.get(sellVolumeDescriptionKey, dataModel.getCurrencyCode())); } amount.set(HavenoUtils.formatXmr(dataModel.getAmount().get())); showTransactionPublishedScreen.set(false); // when getting back to an open screen we want to re-check again isOfferAvailable.set(false); checkNotNull(offer, "offer must not be null"); offer.stateProperty().addListener(offerStateListener); applyOfferState(offer.stateProperty().get()); updateButtonDisableState(); updateSpinnerInfo(); isTradeFeeVisible.setValue(false); } @Override protected void deactivate() { removeBindings(); removeListeners(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// // called before doActivate void initWithData(Offer offer) { dataModel.initWithData(offer); this.offer = offer; String buyAmountDescriptionKey = offer.isTraditionalOffer() ? "takeOffer.amountPriceBox.buy.amountDescription" : "takeOffer.amountPriceBox.buy.amountDescriptionCrypto"; String sellAmountDescriptionKey = offer.isTraditionalOffer() ? "takeOffer.amountPriceBox.sell.amountDescription" : "takeOffer.amountPriceBox.sell.amountDescriptionCrypto"; amountDescription = offer.isBuyOffer() ? Res.get(buyAmountDescriptionKey) : Res.get(sellAmountDescriptionKey); amountRange = HavenoUtils.formatXmr(offer.getMinAmount()) + " - " + HavenoUtils.formatXmr(offer.getAmount()); price = FormattingUtils.formatPrice(dataModel.tradePrice); marketPriceMargin = FormattingUtils.formatToPercent(offer.getMarketPriceMarginPct()); paymentLabel = Res.get("takeOffer.fundsBox.paymentLabel", offer.getShortId()); checkNotNull(dataModel.getAddressEntry(), "dataModel.getAddressEntry() must not be null"); errorMessage.set(offer.getErrorMessage()); xmrValidator.setMaxValue(offer.getAmount()); xmrValidator.setMaxTradeLimit(dataModel.getMaxTradeLimit()); xmrValidator.setMinValue(offer.getMinAmount()); setVolumeToModel(); } /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// void onTakeOffer(Runnable resultHandler) { takeOfferResultHandler = resultHandler; takeOfferRequested = true; showTransactionPublishedScreen.set(false); dataModel.onTakeOffer(trade -> { this.trade = trade; takeOfferCompleted.set(true); trade.stateProperty().addListener(tradeStateListener); applyTradeState(); applyTradeErrorMessage(trade.getErrorMessage()); takeOfferCompleted.set(true); }, errMessage -> { applyTradeErrorMessage(errMessage); }); updateButtonDisableState(); updateSpinnerInfo(); } public void onPaymentAccountSelected(PaymentAccount paymentAccount) { dataModel.onPaymentAccountSelected(paymentAccount); xmrValidator.setMaxTradeLimit(dataModel.getMaxTradeLimit()); updateButtonDisableState(); } public void onShowPayFundsScreen() { dataModel.onShowPayFundsScreen(); showPayFundsScreenDisplayed.set(true); updateSpinnerInfo(); } boolean fundFromSavingsWallet() { dataModel.fundFromSavingsWallet(); if (dataModel.getIsXmrWalletFunded().get()) { updateButtonDisableState(); return true; } else { new Popup().warning(Res.get("shared.notEnoughFunds", HavenoUtils.formatXmr(dataModel.getTotalToPay().get(), true), HavenoUtils.formatXmr(dataModel.getTotalAvailableBalance(), true))) .actionButtonTextWithGoTo("funds.tab.deposit") .onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, DepositView.class)) .show(); return false; } } private void applyTakerFee() { tradeFeeDescription.set(Res.get("createOffer.tradeFee.descriptionXMROnly")); BigInteger takerFee = dataModel.getTakerFee(); if (takerFee == null) { return; } isTradeFeeVisible.setValue(true); tradeFee.set(HavenoUtils.formatXmr(takerFee)); tradeFeeInXmrWithFiat.set(OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, dataModel.getTakerFee(), xmrFormatter)); } /////////////////////////////////////////////////////////////////////////////////////////// // Handle focus /////////////////////////////////////////////////////////////////////////////////////////// // On focus out we do validation and apply the data to the model void onFocusOutAmountTextField(boolean oldValue, boolean newValue, String userInput) { if (oldValue && !newValue) { InputValidator.ValidationResult result = isXmrInputValid(amount.get()); amountValidationResult.set(result); if (result.isValid) { if (userInput != null) showWarningInvalidXmrDecimalPlaces.set(!DisplayUtils.hasBtcValidDecimals(userInput, xmrFormatter)); // only allow max 4 decimal places for xmr values setAmountToModel(); // reformat input amount.set(HavenoUtils.formatXmr(dataModel.getAmount().get())); calculateVolume(); Price tradePrice = dataModel.tradePrice; BigInteger minAmount = dataModel.getOffer().getMinAmount(); BigInteger maxAmount = dataModel.getMaxTradeLimit(); if (PaymentMethod.isRoundedForAtmCash(dataModel.getPaymentMethod().getId())) { BigInteger adjustedAmountForAtm = CoinUtil.getRoundedAtmCashAmount(dataModel.getAmount().get(), tradePrice, minAmount, maxAmount); dataModel.maybeApplyAmount(adjustedAmountForAtm); } else if (dataModel.getOffer().isTraditionalOffer() && dataModel.getOffer().isRange()) { BigInteger roundedAmount = CoinUtil.getRoundedAmount(dataModel.getAmount().get(), tradePrice, minAmount, maxAmount, dataModel.getOffer().getCounterCurrencyCode(), dataModel.getOffer().getPaymentMethodId()); dataModel.maybeApplyAmount(roundedAmount); } amount.set(HavenoUtils.formatXmr(dataModel.getAmount().get())); if (!dataModel.isMinAmountLessOrEqualAmount()) amountValidationResult.set(new InputValidator.ValidationResult(false, Res.get("takeOffer.validation.amountSmallerThanMinAmount"))); if (dataModel.isAmountLargerThanOfferAmount()) amountValidationResult.set(new InputValidator.ValidationResult(false, Res.get("takeOffer.validation.amountLargerThanOfferAmount"))); if (dataModel.wouldCreateDustForMaker()) amountValidationResult.set(new InputValidator.ValidationResult(false, Res.get("takeOffer.validation.amountLargerThanOfferAmountMinusFee"))); } else if (xmrValidator.getMaxTradeLimit() != null && xmrValidator.getMaxTradeLimit().equals(OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT)) { if (dataModel.getDirection() == OfferDirection.BUY) { new Popup().information(Res.get("popup.warning.tradeLimitDueAccountAgeRestriction.seller", HavenoUtils.formatXmr(OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT, true), Res.get("offerbook.warning.newVersionAnnouncement"))) .width(900) .show(); } else { new Popup().information(Res.get("popup.warning.tradeLimitDueAccountAgeRestriction.buyer", HavenoUtils.formatXmr(OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT, true), Res.get("offerbook.warning.newVersionAnnouncement"))) .width(900) .show(); } } } } void onFocusOutVolumeTextField(boolean oldValue, boolean newValue) { if (oldValue && !newValue) { InputValidator.ValidationResult result = isVolumeInputValid(volume.get()); volumeValidationResult.set(result); if (result.isValid) { setVolumeToModel(); ignoreVolumeStringListener = true; Volume volume = dataModel.getVolume().get(); if (volume != null) { volume = VolumeUtil.getAdjustedVolume(volume, offer.getPaymentMethod().getId()); this.volume.set(VolumeUtil.formatVolume(volume)); } ignoreVolumeStringListener = false; dataModel.calculateAmount(); if (amount.get() != null) amountValidationResult.set(isXmrInputValid(amount.get())); } } } /////////////////////////////////////////////////////////////////////////////////////////// // States /////////////////////////////////////////////////////////////////////////////////////////// private void applyOfferState(Offer.State state) { UserThread.execute(() -> { offerWarning.set(null); // We have 2 situations handled here: // 1. when clicking take offer in the offerbook screen, we do the availability check // 2. Before actually taking the offer in the take offer screen, we check again the availability as some time might have passed in the meantime // So we use the takeOfferRequested flag to display different network_messages depending on the context. switch (state) { case UNKNOWN: break; case OFFER_FEE_RESERVED: // irrelevant for taker break; case AVAILABLE: isOfferAvailable.set(true); updateButtonDisableState(); break; case NOT_AVAILABLE: if (takeOfferRequested) offerWarning.set(Res.get("takeOffer.failed.offerNotAvailable")); else offerWarning.set(Res.get("takeOffer.failed.offerTaken")); takeOfferRequested = false; break; case INVALID: offerWarning.set(Res.get("takeOffer.failed.offerInvalid")); takeOfferRequested = false; break; case REMOVED: // if (takeOfferRequested) // TODO: show any warning or removed is expected? // offerWarning.set(Res.get("takeOffer.failed.offerRemoved")); takeOfferRequested = false; break; case MAKER_OFFLINE: if (takeOfferRequested) offerWarning.set(Res.get("takeOffer.failed.offererNotOnline")); else offerWarning.set(Res.get("takeOffer.failed.offererOffline")); takeOfferRequested = false; break; default: log.error("Unhandled offer state: " + state); break; } updateSpinnerInfo(); updateButtonDisableState(); }); } private void applyTradeErrorMessage(@Nullable String errorMessage) { if (errorMessage != null) { String appendMsg = ""; if (trade != null) { if (trade.isPayoutPublished()) appendMsg = Res.get("takeOffer.error.payoutPublished"); else { switch (trade.getState().getPhase()) { case INIT: appendMsg = Res.get("takeOffer.error.noFundsLost"); break; case DEPOSIT_REQUESTED: appendMsg = Res.get("takeOffer.error.feePaid"); break; case DEPOSITS_PUBLISHED: case PAYMENT_SENT: case PAYMENT_RECEIVED: appendMsg = Res.get("takeOffer.error.depositPublished"); break; default: break; } } } this.errorMessage.set(errorMessage + appendMsg); updateSpinnerInfo(); if (takeOfferResultHandler != null) takeOfferResultHandler.run(); } else { this.errorMessage.set(null); } } private void applyTradeState() { if (trade.isDepositRequested()) { if (takeOfferResultHandler != null) takeOfferResultHandler.run(); showTransactionPublishedScreen.set(true); updateSpinnerInfo(); } } private void updateButtonDisableState() { boolean inputDataValid = isXmrInputValid(amount.get()).isValid && dataModel.isMinAmountLessOrEqualAmount() && !dataModel.isAmountLargerThanOfferAmount() && isOfferAvailable.get() && !dataModel.wouldCreateDustForMaker(); isNextButtonDisabled.set(!inputDataValid); isTakeOfferButtonDisabled.set(takeOfferRequested || !inputDataValid || !dataModel.getIsXmrWalletFunded().get()); } /////////////////////////////////////////////////////////////////////////////////////////// // Bindings, listeners /////////////////////////////////////////////////////////////////////////////////////////// private void addBindings() { totalToPay.bind(createStringBinding(() -> HavenoUtils.formatXmr(dataModel.getTotalToPay().get(), true), dataModel.getTotalToPay())); } private void removeBindings() { volumeDescriptionLabel.unbind(); totalToPay.unbind(); } private void createListeners() { amountStrListener = (ov, oldValue, newValue) -> { if (isXmrInputValid(newValue).isValid) { setAmountToModel(); calculateVolume(); dataModel.calculateTotalToPay(); applyTakerFee(); } updateButtonDisableState(); }; amountListener = (ov, oldValue, newValue) -> { amount.set(HavenoUtils.formatXmr(newValue)); applyTakerFee(); }; volumeStringListener = (ov, oldValue, newValue) -> { if (!ignoreVolumeStringListener) { if (isVolumeInputValid(newValue).isValid) { setVolumeToModel(); dataModel.calculateAmount(); dataModel.calculateTotalToPay(); } updateButtonDisableState(); } }; volumeListener = (ov, oldValue, newValue) -> { ignoreVolumeStringListener = true; if (newValue != null) volume.set(VolumeUtil.formatVolume(newValue)); else volume.set(""); ignoreVolumeStringListener = false; }; isWalletFundedListener = (ov, oldValue, newValue) -> updateButtonDisableState(); tradeStateListener = (ov, oldValue, newValue) -> applyTradeState(); offerStateListener = (ov, oldValue, newValue) -> applyOfferState(newValue); connectionListener = new ConnectionListener() { @Override public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { if (trade == null) return; // ignore if trade initializing if (connection.getPeersNodeAddressOptional().isPresent() && connection.getPeersNodeAddressOptional().get().equals(offer.getMakerNodeAddress())) { offerWarning.set(Res.get("takeOffer.warning.connectionToPeerLost")); updateSpinnerInfo(); } } @Override public void onConnection(Connection connection) { } }; } private void updateSpinnerInfo() { UserThread.execute(() -> { if (!showPayFundsScreenDisplayed.get() || offerWarning.get() != null || errorMessage.get() != null || showTransactionPublishedScreen.get()) { spinnerInfoText.set(""); } else if (dataModel.getIsXmrWalletFunded().get()) { spinnerInfoText.set(""); /* if (dataModel.isFeeFromFundingTxSufficient.get()) { spinnerInfoText.set(""); } else { spinnerInfoText.set("Check if funding tx miner fee is sufficient..."); }*/ } else { spinnerInfoText.set(Res.get("shared.waitingForFunds")); } isWaitingForFunds.set(!spinnerInfoText.get().isEmpty()); }); } private void addListeners() { // Bidirectional bindings are used for all input fields: amount, price, volume and minAmount // We do volume/amount calculation during input, so user has immediate feedback amount.addListener(amountStrListener); volume.addListener(volumeStringListener); // Binding with Bindings.createObjectBinding does not work because of bi-directional binding dataModel.getAmount().addListener(amountListener); dataModel.getVolume().addListener(volumeListener); dataModel.getIsXmrWalletFunded().addListener(isWalletFundedListener); p2PService.getNetworkNode().addConnectionListener(connectionListener); /* isFeeSufficientSubscription = EasyBind.subscribe(dataModel.isFeeFromFundingTxSufficient, newValue -> { updateButtonDisableState(); updateSpinnerInfo(); });*/ } private void removeListeners() { amount.removeListener(amountStrListener); volume.removeListener(volumeStringListener); // Binding with Bindings.createObjectBinding does not work because of bi-directional binding dataModel.getAmount().removeListener(amountListener); dataModel.getVolume().removeListener(volumeListener); dataModel.getIsXmrWalletFunded().removeListener(isWalletFundedListener); if (offer != null) { offer.stateProperty().removeListener(offerStateListener); } if (trade != null) { trade.stateProperty().removeListener(tradeStateListener); } p2PService.getNetworkNode().removeConnectionListener(connectionListener); //isFeeSufficientSubscription.unsubscribe(); } /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// private void calculateVolume() { setAmountToModel(); dataModel.calculateVolume(); } private void setAmountToModel() { if (amount.get() != null && !amount.get().isEmpty()) { BigInteger amount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.amount.get(), xmrFormatter)); BigInteger minAmount = dataModel.getOffer().getMinAmount(); BigInteger maxAmount = dataModel.getMaxTradeLimit(); Price price = dataModel.tradePrice; if (price != null) { if (dataModel.isRoundedForAtmCash()) { amount = CoinUtil.getRoundedAtmCashAmount(amount, price, minAmount, maxAmount); } else if (dataModel.getOffer().isTraditionalOffer() && dataModel.getOffer().isRange()) { amount = CoinUtil.getRoundedAmount(amount, price, minAmount, maxAmount, dataModel.getOffer().getCounterCurrencyCode(), dataModel.getOffer().getPaymentMethodId()); } } dataModel.maybeApplyAmount(amount); } } private void setVolumeToModel() { if (volume.get() != null && !volume.get().isEmpty()) { try { dataModel.setVolume(Volume.parse(volume.get(), offer.getCounterCurrencyCode())); } catch (Throwable t) { log.debug(t.getMessage()); } } else { dataModel.setVolume(null); } } private InputValidator.ValidationResult isVolumeInputValid(String input) { return getVolumeValidator().validate(input); } // TODO: replace with VolumeUtils? private MonetaryValidator getVolumeValidator() { final String code = offer.getCounterCurrencyCode(); if (CurrencyUtil.isFiatCurrency(code)) { return fiatVolumeValidator; } else if (CurrencyUtil.isVolumeRoundedToNearestUnit(code)) { return amountValidator4Decimals; } else { return amountValidator8Decimals; } } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// CoinFormatter getXmrFormatter() { return xmrFormatter; } boolean isSeller() { return dataModel.getDirection() == OfferDirection.BUY; } public boolean isSellingToAnUnsignedAccount(Offer offer) { if (offer.getDirection() == OfferDirection.BUY && PaymentMethod.hasChargebackRisk(offer.getPaymentMethod(), offer.getCounterCurrencyCode())) { // considered risky when either UNSIGNED, PEER_INITIAL, or BANNED (see #5343) return accountAgeWitnessService.getSignState(offer) == AccountAgeWitnessService.SignState.UNSIGNED || accountAgeWitnessService.getSignState(offer) == AccountAgeWitnessService.SignState.PEER_INITIAL || accountAgeWitnessService.getSignState(offer) == AccountAgeWitnessService.SignState.BANNED; } return false; } private InputValidator.ValidationResult isXmrInputValid(String input) { return xmrValidator.validate(input); } public Offer getOffer() { return dataModel.getOffer(); } public boolean isRange() { return dataModel.getOffer().isRange(); } public String getAmountRange() { return amountRange; } public String getPaymentLabel() { return paymentLabel; } public String getPrice() { return price; } public String getAmountDescription() { return amountDescription; } String getTradeAmount() { return OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, dataModel.getAmount().get(), xmrFormatter); } public String getSecurityDepositInfo() { return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, dataModel.getSecurityDeposit(), dataModel.getAmount().get(), xmrFormatter); } public String getSecurityDepositWithCode() { return HavenoUtils.formatXmr(dataModel.getSecurityDeposit(), true); } public String getTradeFee() { return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, dataModel.getTakerFee(), dataModel.getAmount().get(), xmrFormatter); } public String getTakerFeePercentage() { final BigInteger takerFee = dataModel.getTakerFee(); return takerFee != null ? GUIUtil.getPercentage(takerFee, dataModel.getAmount().get()) : Res.get("shared.na"); } public String getTotalToPayInfo() { final String totalToPay = this.totalToPay.get(); return totalToPay; } ObservableList getPossiblePaymentAccounts() { return dataModel.getPossiblePaymentAccounts(); } public PaymentAccount getLastSelectedPaymentAccount() { return dataModel.getLastSelectedPaymentAccount(); } public void resetOfferWarning() { offerWarning.set(null); } public Trade getTrade() { return trade; } public void resetErrorMessage() { offer.setErrorMessage(null); } public Callback, ListCell> getPaymentAccountListCellFactory( ComboBox paymentAccountsComboBox) { return GUIUtil.getPaymentAccountListCellFactory(paymentAccountsComboBox, accountAgeWitnessService); } String getPercentagePriceDescription() { return dataModel.isBuyOffer() ? Res.get("shared.belowInPercent") : Res.get("shared.aboveInPercent"); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/Overlay.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays; import com.google.common.reflect.TypeToken; import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIconView; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.config.Config; import haveno.common.util.Utilities; import haveno.core.locale.GlobalSettings; import haveno.core.locale.LanguageUtil; import haveno.core.locale.Res; import haveno.core.user.DontShowAgainLookup; import haveno.desktop.app.HavenoApp; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipCheckBox; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.BusyAnimation; import haveno.desktop.main.MainView; import haveno.desktop.util.CssTheme; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.Layout; import haveno.desktop.util.Transitions; import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ChangeListener; import javafx.collections.ObservableList; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.NodeOrientation; import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.Node; import javafx.scene.PerspectiveCamera; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.control.TextArea; import javafx.scene.control.Tooltip; import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane.ScrollBarPolicy; import javafx.scene.input.KeyCode; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.scene.transform.Rotate; import javafx.stage.Modality; import javafx.stage.Stage; import javafx.stage.StageStyle; import javafx.stage.Window; import javafx.util.Duration; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Objects; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import static javafx.scene.input.MouseEvent.MOUSE_CLICKED; @Slf4j public abstract class Overlay> { /////////////////////////////////////////////////////////////////////////////////////////// // Enum /////////////////////////////////////////////////////////////////////////////////////////// private enum AnimationType { FadeInAtCenter, SlideDownFromCenterTop, SlideFromRightTop, ScaleDownToCenter, ScaleFromCenter, ScaleYFromCenter } private enum ChangeBackgroundType { BlurLight, BlurUltraLight, Darken } protected enum Type { Undefined(AnimationType.ScaleFromCenter, ChangeBackgroundType.BlurLight), Notification(AnimationType.SlideFromRightTop, ChangeBackgroundType.BlurLight), BackgroundInfo(AnimationType.SlideDownFromCenterTop, ChangeBackgroundType.BlurUltraLight), Feedback(AnimationType.SlideDownFromCenterTop, ChangeBackgroundType.BlurLight), Information(AnimationType.FadeInAtCenter, ChangeBackgroundType.BlurLight), Instruction(AnimationType.ScaleFromCenter, ChangeBackgroundType.BlurLight), Attention(AnimationType.ScaleFromCenter, ChangeBackgroundType.BlurLight), Confirmation(AnimationType.ScaleYFromCenter, ChangeBackgroundType.BlurLight), Warning(AnimationType.ScaleDownToCenter, ChangeBackgroundType.BlurLight), Error(AnimationType.ScaleDownToCenter, ChangeBackgroundType.BlurLight); public final AnimationType animationType; public final ChangeBackgroundType changeBackgroundType; Type(AnimationType animationType, ChangeBackgroundType changeBackgroundType) { this.animationType = animationType; this.changeBackgroundType = changeBackgroundType; } } private static int numCenterOverlays = 0; private static int numBlurEffects = 0; protected final static double DEFAULT_WIDTH = 800; protected Stage stage; protected GridPane gridPane; protected Pane owner; protected int rowIndex = -1; protected double width = DEFAULT_WIDTH; protected double buttonDistance = 20; protected boolean showReportErrorButtons; private boolean showBusyAnimation; protected boolean hideCloseButton; protected boolean isDisplayed; protected boolean disableActionButton; @Getter protected BooleanProperty isHiddenProperty = new SimpleBooleanProperty(); // Used when a priority queue is used for displaying order of popups. Higher numbers mean lower priority @Setter @Getter protected Integer displayOrderPriority = Integer.MAX_VALUE; protected boolean useAnimation = true; protected boolean showScrollPane = false; protected TextArea messageTextArea; protected Label headlineIcon, copyLabel, headLineLabel; protected String headLine, message, closeButtonText, actionButtonText, secondaryActionButtonText, dontShowAgainId, dontShowAgainText, truncatedMessage; private ArrayList messageHyperlinks; private String headlineStyle; protected Button actionButton, secondaryActionButton; private HBox buttonBox; protected AutoTooltipButton closeButton; protected ScrollPane scrollPane; private HPos buttonAlignment = HPos.RIGHT; protected Optional closeHandlerOptional = Optional.empty(); protected Optional actionHandlerOptional = Optional.empty(); protected Optional secondaryActionHandlerOptional = Optional.empty(); protected ChangeListener positionListener; protected Timer centerTime; protected Type type = Type.Undefined; protected int maxChar = 2200; private T cast() { //noinspection unchecked return (T) this; } /////////////////////////////////////////////////////////////////////////////////////////// // Public API /////////////////////////////////////////////////////////////////////////////////////////// public Overlay() { //noinspection UnstableApiUsage TypeToken typeToken = new TypeToken<>(getClass()) { }; if (!typeToken.isSupertypeOf(getClass())) { throw new RuntimeException("Subclass of Overlay should be castable to T"); } } public void show(boolean showAgainChecked) { if (dontShowAgainId == null || DontShowAgainLookup.showAgain(dontShowAgainId)) { createGridPane(); if (LanguageUtil.isDefaultLanguageRTL()) getRootContainer().setNodeOrientation(NodeOrientation.RIGHT_TO_LEFT); addHeadLine(); if (showBusyAnimation) addBusyAnimation(); addMessage(); if (showReportErrorButtons) addReportErrorButtons(); addButtons(); addDontShowAgainCheckBox(showAgainChecked); applyStyles(); onShow(); } } public void show() { this.show(false); } protected void onShow() { } public void hide() { if (gridPane != null) { animateHide(); } isDisplayed = false; isHiddenProperty.set(true); } protected void animateHide() { animateHide(() -> { if (isCentered()) numCenterOverlays--; removeEffectFromBackground(); if (stage != null) stage.hide(); else log.warn("Stage is null"); cleanup(); onHidden(); }); } protected void onHidden() { } protected void cleanup() { if (centerTime != null) centerTime.stop(); if (owner == null) owner = MainView.getRootContainer(); Scene rootScene = owner.getScene(); if (rootScene != null) { Window window = rootScene.getWindow(); if (window != null && positionListener != null) { window.xProperty().removeListener(positionListener); window.yProperty().removeListener(positionListener); window.widthProperty().removeListener(positionListener); } } } public T onClose(Runnable closeHandler) { this.closeHandlerOptional = Optional.of(closeHandler); return cast(); } public T onAction(Runnable actionHandler) { this.actionHandlerOptional = Optional.of(actionHandler); return cast(); } public T onSecondaryAction(Runnable secondaryActionHandlerOptional) { this.secondaryActionHandlerOptional = Optional.of(secondaryActionHandlerOptional); return cast(); } public T headLine(String headLine) { this.headLine = headLine; return cast(); } public T notification(String message) { type = Type.Notification; if (headLine == null) this.headLine = Res.get("popup.headline.notification"); preProcessMessage(message); return cast(); } public T instruction(String message) { type = Type.Instruction; if (headLine == null) this.headLine = Res.get("popup.headline.instruction"); preProcessMessage(message); return cast(); } public T attention(String message) { type = Type.Attention; if (headLine == null) this.headLine = Res.get("popup.headline.attention"); preProcessMessage(message); return cast(); } public T backgroundInfo(String message) { type = Type.BackgroundInfo; if (headLine == null) this.headLine = Res.get("popup.headline.backgroundInfo"); preProcessMessage(message); return cast(); } public T feedback(String message) { type = Type.Feedback; if (headLine == null) this.headLine = Res.get("popup.headline.feedback"); preProcessMessage(message); return cast(); } public T confirmation(String message) { type = Type.Confirmation; if (headLine == null) this.headLine = Res.get("popup.headline.confirmation"); preProcessMessage(message); return cast(); } public T information(String message) { type = Type.Information; if (headLine == null) this.headLine = Res.get("popup.headline.information"); preProcessMessage(message); return cast(); } public T warning(String message) { type = Type.Warning; if (headLine == null) this.headLine = Res.get("popup.headline.warning"); preProcessMessage(message); return cast(); } public T error(String message) { type = Type.Error; showReportErrorButtons(); width = 1100; if (headLine == null) this.headLine = Res.get("popup.headline.error"); preProcessMessage(message); return cast(); } @SuppressWarnings("UnusedReturnValue") public T showReportErrorButtons() { this.showReportErrorButtons = true; return cast(); } public T message(String message) { preProcessMessage(message); return cast(); } public T closeButtonText(String closeButtonText) { this.closeButtonText = closeButtonText; return cast(); } public T useReportBugButton() { this.closeButtonText = Res.get("shared.reportBug"); this.closeHandlerOptional = Optional.of(() -> GUIUtil.openWebPage("https://github.com/haveno-dex/haveno/issues")); return cast(); } public T useIUnderstandButton() { this.closeButtonText = Res.get("shared.iUnderstand"); return cast(); } public T actionButtonTextWithGoTo(String target) { this.actionButtonText = Res.get("shared.goTo", Res.get(target)); return cast(); } public T secondaryActionButtonTextWithGoTo(String target) { this.secondaryActionButtonText = Res.get("shared.goTo", Res.get(target)); return cast(); } public T closeButtonTextWithGoTo(String target) { this.closeButtonText = Res.get("shared.goTo", Res.get(target)); return cast(); } public T actionButtonText(String actionButtonText) { this.actionButtonText = actionButtonText; return cast(); } public T secondaryActionButtonText(String secondaryActionButtonText) { this.secondaryActionButtonText = secondaryActionButtonText; return cast(); } public T useShutDownButton() { this.actionButtonText = Res.get("shared.shutDown"); this.actionHandlerOptional = Optional.ofNullable(HavenoApp.getShutDownHandler()); return cast(); } public T buttonAlignment(HPos pos) { this.buttonAlignment = pos; return cast(); } public T width(double width) { this.width = width; return cast(); } public T maxMessageLength(int maxChar) { this.maxChar = maxChar; return cast(); } public T showBusyAnimation() { this.showBusyAnimation = true; return cast(); } public T dontShowAgainId(String key) { this.dontShowAgainId = key; return cast(); } public T dontShowAgainText(String dontShowAgainText) { this.dontShowAgainText = dontShowAgainText; return cast(); } public T hideCloseButton() { this.hideCloseButton = true; return cast(); } public T useAnimation(boolean useAnimation) { this.useAnimation = useAnimation; return cast(); } public T setHeadlineStyle(String headlineStyle) { this.headlineStyle = headlineStyle; return cast(); } public T disableActionButton() { this.disableActionButton = true; return cast(); } public T showScrollPane() { this.showScrollPane = true; return cast(); } /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// protected void createGridPane() { gridPane = new GridPane(); gridPane.setHgap(5); gridPane.setVgap(5); gridPane.setPadding(new Insets(64, 64, 64, 64)); gridPane.setPrefWidth(width); gridPane.setMaxHeight(Layout.MAX_POPUP_HEIGHT); ColumnConstraints columnConstraints1 = new ColumnConstraints(); columnConstraints1.setHalignment(HPos.RIGHT); columnConstraints1.setHgrow(Priority.SOMETIMES); ColumnConstraints columnConstraints2 = new ColumnConstraints(); columnConstraints2.setHgrow(Priority.ALWAYS); gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2); } protected void blurAgain() { UserThread.runAfter(MainView::blurLight, Transitions.DEFAULT_DURATION, TimeUnit.MILLISECONDS); } public void display() { if (owner == null) owner = MainView.getRootContainer(); if (owner != null) { Scene rootScene = owner.getScene(); if (rootScene != null) { UserThread.execute(() -> { Scene scene = new Scene(getRootContainer()); scene.getStylesheets().setAll(rootScene.getStylesheets()); scene.setFill(Color.TRANSPARENT); setupKeyHandler(scene); stage = new Stage(); stage.setScene(scene); Window window = rootScene.getWindow(); setModality(); stage.initStyle(StageStyle.TRANSPARENT); stage.setOnCloseRequest(event -> { event.consume(); doClose(); }); stage.sizeToScene(); stage.show(); layout(); // add dropshadow if light mode or multiple centered overlays if (isCentered()) { numCenterOverlays++; } if (!CssTheme.isDarkTheme() || numCenterOverlays > 1) { getRootContainer().getStyleClass().add("popup-dropshadow"); } addEffectToBackground(); // On Linux the owner stage does not move the child stage as it does on Mac // So we need to apply centerPopup. Further with fast movements the handler loses // the latest position, with a delay it fixes that. // Also on Mac sometimes the popups are positioned outside of the main app, so keep it for all OS positionListener = (observable, oldValue, newValue) -> { if (stage != null) { layout(); if (centerTime != null) centerTime.stop(); centerTime = UserThread.runAfter(this::layout, 3); } }; window.xProperty().addListener(positionListener); window.yProperty().addListener(positionListener); window.widthProperty().addListener(positionListener); animateDisplay(); isDisplayed = true; }); } } } protected Region getRootContainer() { return gridPane; } protected void setupKeyHandler(Scene scene) { if (!hideCloseButton) { scene.setOnKeyPressed(e -> { if (e.getCode() == KeyCode.ESCAPE || e.getCode() == KeyCode.ENTER) { e.consume(); doClose(); } }); } } protected void animateDisplay() { Region rootContainer = this.getRootContainer(); rootContainer.setOpacity(0); Interpolator interpolator = Interpolator.SPLINE(0.25, 0.1, 0.25, 1); double duration = getDuration(400); Timeline timeline = new Timeline(); ObservableList keyFrames = timeline.getKeyFrames(); if (type.animationType == AnimationType.SlideDownFromCenterTop) { double startY = -rootContainer.getHeight(); keyFrames.add(new KeyFrame(Duration.millis(0), new KeyValue(rootContainer.opacityProperty(), 0, interpolator), new KeyValue(rootContainer.translateYProperty(), startY, interpolator) )); keyFrames.add(new KeyFrame(Duration.millis(duration), new KeyValue(rootContainer.opacityProperty(), 1, interpolator), new KeyValue(rootContainer.translateYProperty(), -50, interpolator) )); } else if (type.animationType == AnimationType.ScaleFromCenter) { double startScale = 0.25; keyFrames.add(new KeyFrame(Duration.millis(0), new KeyValue(rootContainer.opacityProperty(), 0, interpolator), new KeyValue(rootContainer.scaleXProperty(), startScale, interpolator), new KeyValue(rootContainer.scaleYProperty(), startScale, interpolator) )); keyFrames.add(new KeyFrame(Duration.millis(duration), new KeyValue(rootContainer.opacityProperty(), 1, interpolator), new KeyValue(rootContainer.scaleXProperty(), 1, interpolator), new KeyValue(rootContainer.scaleYProperty(), 1, interpolator) )); } else if (type.animationType == AnimationType.ScaleYFromCenter) { double startYScale = 0.25; keyFrames.add(new KeyFrame(Duration.millis(0), new KeyValue(rootContainer.opacityProperty(), 0, interpolator), new KeyValue(rootContainer.scaleYProperty(), startYScale, interpolator) )); keyFrames.add(new KeyFrame(Duration.millis(duration), new KeyValue(rootContainer.opacityProperty(), 1, interpolator), new KeyValue(rootContainer.scaleYProperty(), 1, interpolator) )); } else if (type.animationType == AnimationType.ScaleDownToCenter) { double startScale = 1.1; keyFrames.add(new KeyFrame(Duration.millis(0), new KeyValue(rootContainer.opacityProperty(), 0, interpolator), new KeyValue(rootContainer.scaleXProperty(), startScale, interpolator), new KeyValue(rootContainer.scaleYProperty(), startScale, interpolator) )); keyFrames.add(new KeyFrame(Duration.millis(duration), new KeyValue(rootContainer.opacityProperty(), 1, interpolator), new KeyValue(rootContainer.scaleXProperty(), 1, interpolator), new KeyValue(rootContainer.scaleYProperty(), 1, interpolator) )); } else if (type.animationType == AnimationType.FadeInAtCenter) { keyFrames.add(new KeyFrame(Duration.millis(0), new KeyValue(rootContainer.opacityProperty(), 0, interpolator) )); keyFrames.add(new KeyFrame(Duration.millis(duration), new KeyValue(rootContainer.opacityProperty(), 1, interpolator) )); } timeline.play(); } protected void animateHide(Runnable onFinishedHandler) { Interpolator interpolator = Interpolator.SPLINE(0.25, 0.1, 0.25, 1); double duration = getDuration(200); Timeline timeline = new Timeline(); ObservableList keyFrames = timeline.getKeyFrames(); Region rootContainer = getRootContainer(); if (type.animationType == AnimationType.SlideDownFromCenterTop) { double endY = -rootContainer.getHeight(); keyFrames.add(new KeyFrame(Duration.millis(0), new KeyValue(rootContainer.opacityProperty(), 1, interpolator), new KeyValue(rootContainer.translateYProperty(), -10, interpolator) )); keyFrames.add(new KeyFrame(Duration.millis(duration), new KeyValue(rootContainer.opacityProperty(), 0, interpolator), new KeyValue(rootContainer.translateYProperty(), endY, interpolator) )); timeline.setOnFinished(e -> onFinishedHandler.run()); timeline.play(); } else if (type.animationType == AnimationType.ScaleFromCenter) { double endScale = 0.25; keyFrames.add(new KeyFrame(Duration.millis(0), new KeyValue(rootContainer.opacityProperty(), 1, interpolator), new KeyValue(rootContainer.scaleXProperty(), 1, interpolator), new KeyValue(rootContainer.scaleYProperty(), 1, interpolator) )); keyFrames.add(new KeyFrame(Duration.millis(duration), new KeyValue(rootContainer.opacityProperty(), 0, interpolator), new KeyValue(rootContainer.scaleXProperty(), endScale, interpolator), new KeyValue(rootContainer.scaleYProperty(), endScale, interpolator) )); } else if (type.animationType == AnimationType.ScaleYFromCenter) { rootContainer.setRotationAxis(Rotate.X_AXIS); rootContainer.getScene().setCamera(new PerspectiveCamera()); keyFrames.add(new KeyFrame(Duration.millis(0), new KeyValue(rootContainer.rotateProperty(), 0, interpolator), new KeyValue(rootContainer.opacityProperty(), 1, interpolator) )); keyFrames.add(new KeyFrame(Duration.millis(duration), new KeyValue(rootContainer.rotateProperty(), -90, interpolator), new KeyValue(rootContainer.opacityProperty(), 0, interpolator) )); } else if (type.animationType == AnimationType.ScaleDownToCenter) { double endScale = 0.1; keyFrames.add(new KeyFrame(Duration.millis(0), new KeyValue(rootContainer.opacityProperty(), 1, interpolator), new KeyValue(rootContainer.scaleXProperty(), 1, interpolator), new KeyValue(rootContainer.scaleYProperty(), 1, interpolator) )); keyFrames.add(new KeyFrame(Duration.millis(duration), new KeyValue(rootContainer.opacityProperty(), 0, interpolator), new KeyValue(rootContainer.scaleXProperty(), endScale, interpolator), new KeyValue(rootContainer.scaleYProperty(), endScale, interpolator) )); } else if (type.animationType == AnimationType.FadeInAtCenter) { keyFrames.add(new KeyFrame(Duration.millis(0), new KeyValue(rootContainer.opacityProperty(), 1, interpolator) )); keyFrames.add(new KeyFrame(Duration.millis(duration), new KeyValue(rootContainer.opacityProperty(), 0, interpolator) )); } timeline.setOnFinished(e -> onFinishedHandler.run()); timeline.play(); } protected void layout() { if (owner == null) owner = MainView.getRootContainer(); Scene rootScene = owner.getScene(); if (rootScene != null) { Window window = rootScene.getWindow(); double titleBarHeight = window.getHeight() - rootScene.getHeight(); if (Utilities.isWindows()) titleBarHeight -= 9; stage.setX(Math.round(window.getX() + (owner.getWidth() - stage.getWidth()) / 2)); if (type.animationType == AnimationType.SlideDownFromCenterTop) stage.setY(Math.round(window.getY() + titleBarHeight)); else stage.setY(Math.round(window.getY() + titleBarHeight + (owner.getHeight() - stage.getHeight()) / 2)); } } protected void addEffectToBackground() { numBlurEffects++; if (numBlurEffects > 1) return; if (type.changeBackgroundType == ChangeBackgroundType.BlurUltraLight) MainView.blurUltraLight(); else if (type.changeBackgroundType == ChangeBackgroundType.BlurLight) MainView.blurLight(); else MainView.darken(); } protected void applyStyles() { Region rootContainer = getRootContainer(); if (type.animationType == AnimationType.SlideDownFromCenterTop) { rootContainer.getStyleClass().add("popup-bg-top"); } else { rootContainer.getStyleClass().add("popup-bg"); } if (headLineLabel != null) { if (copyLabel != null) { copyLabel.getStyleClass().add("popup-icon-information"); copyLabel.setManaged(true); copyLabel.setVisible(true); MaterialDesignIconView copyIcon = new MaterialDesignIconView(MaterialDesignIcon.CONTENT_COPY, "1.2em"); copyLabel.setGraphic(copyIcon); copyLabel.setCursor(Cursor.HAND); copyLabel.addEventHandler(MOUSE_CLICKED, mouseEvent -> { if (message != null) { Utilities.copyToClipboard(getClipboardText()); Tooltip tp = new Tooltip(Res.get("shared.copiedToClipboard")); Node node = (Node) mouseEvent.getSource(); UserThread.runAfter(() -> tp.hide(), 1); tp.show(node, mouseEvent.getScreenX() + Layout.PADDING, mouseEvent.getScreenY() + Layout.PADDING); } }); } switch (type) { case Information: case BackgroundInfo: case Instruction: case Confirmation: case Feedback: case Notification: case Attention: headLineLabel.getStyleClass().add("popup-headline-information"); headlineIcon.getStyleClass().add("popup-icon-information"); headlineIcon.setManaged(true); headlineIcon.setVisible(true); FormBuilder.getIconForLabel(AwesomeIcon.INFO_SIGN, headlineIcon, "1.5em"); break; case Warning: case Error: headLineLabel.getStyleClass().add("popup-headline-warning"); headlineIcon.getStyleClass().add("popup-icon-warning"); headlineIcon.setManaged(true); headlineIcon.setVisible(true); FormBuilder.getIconForLabel(AwesomeIcon.EXCLAMATION_SIGN, headlineIcon, "1.5em"); break; default: headLineLabel.getStyleClass().add("popup-headline"); } } } protected void setModality() { stage.initOwner(owner.getScene().getWindow()); stage.initModality(Modality.WINDOW_MODAL); } protected void removeEffectFromBackground() { numBlurEffects--; if (numBlurEffects > 0) return; MainView.removeEffect(); } protected void addHeadLine() { if (headLine != null) { ++rowIndex; HBox hBox = new HBox(); hBox.setSpacing(7); headLineLabel = new AutoTooltipLabel(headLine); headlineIcon = new Label(); headlineIcon.setManaged(false); headlineIcon.setVisible(false); headlineIcon.setPadding(new Insets(3)); headLineLabel.setMouseTransparent(true); if (headlineStyle != null) headLineLabel.setStyle(headlineStyle); if (message != null) { copyLabel = new Label(); copyLabel.setManaged(false); copyLabel.setVisible(false); copyLabel.setPadding(new Insets(3)); copyLabel.setTooltip(new Tooltip(Res.get("shared.copyToClipboard"))); final Pane spacer = new Pane(); HBox.setHgrow(spacer, Priority.ALWAYS); spacer.setMinSize(Layout.PADDING, 1); hBox.getChildren().addAll(headlineIcon, headLineLabel, spacer, copyLabel); } else { hBox.getChildren().addAll(headlineIcon, headLineLabel); } GridPane.setHalignment(hBox, HPos.LEFT); GridPane.setRowIndex(hBox, rowIndex); GridPane.setColumnSpan(hBox, 2); gridPane.getChildren().addAll(hBox); } } protected void addMessage() { if (message != null) { messageTextArea = new TextArea(truncatedMessage); messageTextArea.setEditable(false); messageTextArea.getStyleClass().add("text-area-popup"); GUIUtil.adjustHeightAutomatically(messageTextArea); messageTextArea.setWrapText(true); Region messageRegion; if (showScrollPane) { scrollPane = new ScrollPane(messageTextArea); scrollPane.setHbarPolicy(ScrollBarPolicy.NEVER); scrollPane.setVbarPolicy(ScrollBarPolicy.AS_NEEDED); scrollPane.setFitToWidth(true); messageRegion = scrollPane; } else messageRegion = messageTextArea; GridPane.setHalignment(messageRegion, HPos.LEFT); GridPane.setHgrow(messageRegion, Priority.ALWAYS); GridPane.setMargin(messageRegion, new Insets(3, 0, 0, 0)); GridPane.setRowIndex(messageRegion, ++rowIndex); GridPane.setColumnIndex(messageRegion, 0); GridPane.setColumnSpan(messageRegion, 2); gridPane.getChildren().add(messageRegion); addFooter(); } } // footer contains optional hyperlinks extracted from the message private void addFooter() { if (messageHyperlinks != null && messageHyperlinks.size() > 0) { VBox footerBox = new VBox(); GridPane.setRowIndex(footerBox, ++rowIndex); GridPane.setColumnSpan(footerBox, 2); GridPane.setMargin(footerBox, new Insets(buttonDistance, 0, 0, 0)); gridPane.getChildren().add(footerBox); for (int i = 0; i < messageHyperlinks.size(); i++) { Label label = new Label(String.format("[%d]", i + 1)); Hyperlink link = new Hyperlink(messageHyperlinks.get(i)); link.setOnAction(event -> GUIUtil.openWebPageNoPopup(link.getText())); footerBox.getChildren().addAll(new HBox(label, link)); } } } private void addReportErrorButtons() { messageTextArea.setText(Res.get("popup.reportError", truncatedMessage)); Button logButton = new AutoTooltipButton(Res.get("popup.reportError.log")); GridPane.setMargin(logButton, new Insets(20, 0, 0, 0)); GridPane.setHalignment(logButton, HPos.LEFT); GridPane.setRowIndex(logButton, ++rowIndex); gridPane.getChildren().add(logButton); logButton.setOnAction(event -> { try { File dataDir = Config.appDataDir(); File logFile = new File(dataDir, "haveno.log"); Utilities.openFile(logFile); } catch (IOException e) { e.printStackTrace(); log.error(e.getMessage()); } }); Button gitHubButton = new AutoTooltipButton(Res.get("popup.reportError.gitHub")); GridPane.setHalignment(gitHubButton, HPos.RIGHT); GridPane.setRowIndex(gitHubButton, ++rowIndex); gridPane.getChildren().add(gitHubButton); gitHubButton.setOnAction(event -> { if (message != null) Utilities.copyToClipboard(message); GUIUtil.openWebPage("https://github.com/haveno-dex/haveno/issues"); hide(); }); } protected void addBusyAnimation() { BusyAnimation busyAnimation = new BusyAnimation(); GridPane.setHalignment(busyAnimation, HPos.CENTER); GridPane.setRowIndex(busyAnimation, ++rowIndex); GridPane.setColumnSpan(busyAnimation, 2); gridPane.getChildren().add(busyAnimation); } protected void addDontShowAgainCheckBox(boolean isChecked) { if (dontShowAgainId != null) { // We might have set it and overridden the default, so we check if it is not set if (dontShowAgainText == null) dontShowAgainText = Res.get("popup.doNotShowAgain"); CheckBox dontShowAgainCheckBox = new AutoTooltipCheckBox(dontShowAgainText); HBox.setHgrow(dontShowAgainCheckBox, Priority.NEVER); buttonBox.getChildren().add(0, dontShowAgainCheckBox); dontShowAgainCheckBox.setSelected(isChecked); DontShowAgainLookup.dontShowAgain(dontShowAgainId, isChecked); dontShowAgainCheckBox.setOnAction(e -> DontShowAgainLookup.dontShowAgain(dontShowAgainId, dontShowAgainCheckBox.isSelected())); } } protected void addDontShowAgainCheckBox() { this.addDontShowAgainCheckBox(false); } protected void addButtons() { if (!hideCloseButton) { closeButton = new AutoTooltipButton(closeButtonText == null ? Res.get("shared.close") : closeButtonText); closeButton.getStyleClass().add("compact-button"); closeButton.setOnAction(event -> doClose()); closeButton.setMinWidth(70); HBox.setHgrow(closeButton, Priority.SOMETIMES); } Pane spacer = new Pane(); if (buttonAlignment == HPos.RIGHT) { HBox.setHgrow(spacer, Priority.ALWAYS); spacer.setMaxWidth(Double.MAX_VALUE); } buttonBox = new HBox(); GridPane.setHalignment(buttonBox, buttonAlignment); GridPane.setRowIndex(buttonBox, ++rowIndex); GridPane.setColumnSpan(buttonBox, 2); GridPane.setMargin(buttonBox, new Insets(buttonDistance, 0, 0, 0)); gridPane.getChildren().add(buttonBox); if (actionHandlerOptional.isPresent() || actionButtonText != null) { actionButton = new AutoTooltipButton(actionButtonText == null ? Res.get("shared.ok") : actionButtonText); if (!disableActionButton) actionButton.setDefaultButton(true); else actionButton.setDisable(true); HBox.setHgrow(actionButton, Priority.SOMETIMES); actionButton.getStyleClass().add("action-button"); //TODO app wide focus //actionButton.requestFocus(); if (!disableActionButton) { actionButton.setOnAction(event -> { hide(); actionHandlerOptional.ifPresent(Runnable::run); }); } buttonBox.setSpacing(10); buttonBox.setAlignment(Pos.CENTER); if (buttonAlignment == HPos.RIGHT) buttonBox.getChildren().add(spacer); buttonBox.getChildren().addAll(actionButton); if (secondaryActionButtonText != null && secondaryActionHandlerOptional.isPresent()) { secondaryActionButton = new AutoTooltipButton(secondaryActionButtonText); secondaryActionButton.setOnAction(event -> { hide(); secondaryActionHandlerOptional.ifPresent(Runnable::run); }); buttonBox.getChildren().add(secondaryActionButton); } if (!hideCloseButton) buttonBox.getChildren().add(closeButton); } else if (!hideCloseButton) { closeButton.setDefaultButton(true); buttonBox.getChildren().addAll(spacer, closeButton); } } protected void doClose() { hide(); closeHandlerOptional.ifPresent(Runnable::run); } protected void setTruncatedMessage() { if (message != null && message.length() > maxChar) truncatedMessage = StringUtils.abbreviate(message, maxChar); else truncatedMessage = Objects.requireNonNullElse(message, ""); } // separate a popup message from optional hyperlinks. [bisq-network/bisq/pull/4637] // hyperlinks are distinguished by [HYPERLINK:] tag // referenced in order from within the message via [1], [2] etc. // e.g. [HYPERLINK:https://haveno.exchange/wiki] private void preProcessMessage(String message) { Pattern pattern = Pattern.compile("\\[HYPERLINK:(.*?)\\]"); Matcher matcher = pattern.matcher(message); String work = message; while (matcher.find()) { // extract hyperlinks & store in array if (messageHyperlinks == null) { messageHyperlinks = new ArrayList<>(); } messageHyperlinks.add(matcher.group(1)); // replace hyperlink in message with [n] reference work = work.replaceFirst(pattern.toString(), String.format("[%d]", messageHyperlinks.size())); } this.message = work; setTruncatedMessage(); } protected double getDuration(double duration) { return useAnimation && GlobalSettings.getUseAnimations() ? duration : 1; } public boolean isDisplayed() { return isDisplayed; } public String getClipboardText() { return headLineLabel.getText() + System.lineSeparator() + message + System.lineSeparator() + (messageHyperlinks == null ? "" : messageHyperlinks.toString()); } @Override public String toString() { return "Popup{" + "headLine='" + headLine + '\'' + ", message='" + message + '\'' + '}'; } private boolean isCentered() { if (type.animationType == AnimationType.SlideDownFromCenterTop) return false; if (type.animationType == AnimationType.SlideFromRightTop) return false; return true; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/TabbedOverlay.java ================================================ package haveno.desktop.main.overlays; import com.jfoenix.controls.JFXTabPane; import javafx.scene.layout.Region; public abstract class TabbedOverlay> extends Overlay { protected JFXTabPane tabPane; protected void createTabPane() { this.tabPane = new JFXTabPane(); tabPane.setMinWidth(width); } @Override protected Region getRootContainer() { return tabPane; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/editor/PasswordPopup.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.desktop.main.overlays.editor; import haveno.common.util.Utilities; import haveno.core.locale.GlobalSettings; import haveno.desktop.components.InputTextField; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.util.FormBuilder; import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.beans.value.ChangeListener; import javafx.collections.ObservableList; import javafx.event.EventHandler; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.scene.Camera; import javafx.scene.PerspectiveCamera; import javafx.scene.Scene; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.GridPane; import javafx.scene.transform.Rotate; import javafx.stage.Modality; import javafx.util.Duration; import lombok.extern.slf4j.Slf4j; import java.util.function.Consumer; import de.jensd.fx.fontawesome.AwesomeIcon; import static haveno.desktop.util.FormBuilder.addInputTextField; @Slf4j public class PasswordPopup extends Overlay { private InputTextField inputTextField; private static PasswordPopup INSTANCE; private Consumer actionHandler; private ChangeListener focusListener; private EventHandler keyEventEventHandler; public PasswordPopup() { width = 600; type = Type.Confirmation; if (INSTANCE != null) INSTANCE.hide(); INSTANCE = this; } public PasswordPopup onAction(Consumer confirmHandler) { this.actionHandler = confirmHandler; return this; } @Override public void show() { actionButtonText("Confirm"); createGridPane(); addHeadLine(); addContent(); addButtons(); applyStyles(); onShow(); } @Override protected void onShow() { super.display(); if (stage != null) { focusListener = (observable, oldValue, newValue) -> { if (!newValue) hide(); }; stage.focusedProperty().addListener(focusListener); Scene scene = stage.getScene(); if (scene != null) scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); } } @Override public void hide() { animateHide(); } @Override protected void onHidden() { INSTANCE = null; if (stage != null) { if (focusListener != null) stage.focusedProperty().removeListener(focusListener); Scene scene = stage.getScene(); if (scene != null) scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); } } private void addContent() { gridPane.setPadding(new Insets(64)); inputTextField = addInputTextField(gridPane, ++rowIndex, null, -10d); GridPane.setColumnSpan(inputTextField, 2); inputTextField.requestFocus(); keyEventEventHandler = event -> { if (Utilities.isAltOrCtrlPressed(KeyCode.R, event)) { doClose(); } }; } @Override protected void addHeadLine() { super.addHeadLine(); GridPane.setHalignment(headLineLabel, HPos.CENTER); } protected void setupKeyHandler(Scene scene) { scene.setOnKeyPressed(e -> { if (e.getCode() == KeyCode.ESCAPE) { e.consume(); doClose(); } if (e.getCode() == KeyCode.ENTER) { e.consume(); apply(); } }); } @Override protected void animateHide(Runnable onFinishedHandler) { if (GlobalSettings.getUseAnimations()) { double duration = getDuration(300); Interpolator interpolator = Interpolator.SPLINE(0.25, 0.1, 0.25, 1); gridPane.setRotationAxis(Rotate.X_AXIS); Camera camera = gridPane.getScene().getCamera(); gridPane.getScene().setCamera(new PerspectiveCamera()); Timeline timeline = new Timeline(); ObservableList keyFrames = timeline.getKeyFrames(); keyFrames.add(new KeyFrame(Duration.millis(0), new KeyValue(gridPane.rotateProperty(), 0, interpolator), new KeyValue(gridPane.opacityProperty(), 1, interpolator) )); keyFrames.add(new KeyFrame(Duration.millis(duration), new KeyValue(gridPane.rotateProperty(), -90, interpolator), new KeyValue(gridPane.opacityProperty(), 0, interpolator) )); timeline.setOnFinished(event -> { gridPane.setRotate(0); gridPane.setRotationAxis(Rotate.Z_AXIS); gridPane.getScene().setCamera(camera); onFinishedHandler.run(); }); timeline.play(); } else { onFinishedHandler.run(); } } @Override protected void animateDisplay() { if (GlobalSettings.getUseAnimations()) { double startY = -160; double duration = getDuration(400); Interpolator interpolator = Interpolator.SPLINE(0.25, 0.1, 0.25, 1); Timeline timeline = new Timeline(); ObservableList keyFrames = timeline.getKeyFrames(); keyFrames.add(new KeyFrame(Duration.millis(0), new KeyValue(gridPane.opacityProperty(), 0, interpolator), new KeyValue(gridPane.translateYProperty(), startY, interpolator) )); keyFrames.add(new KeyFrame(Duration.millis(duration), new KeyValue(gridPane.opacityProperty(), 1, interpolator), new KeyValue(gridPane.translateYProperty(), 0, interpolator) )); timeline.play(); } } @Override protected void createGridPane() { super.createGridPane(); gridPane.setPadding(new Insets(15, 15, 30, 30)); } @Override protected void addButtons() { buttonDistance = 10; super.addButtons(); actionButton.setOnAction(event -> apply()); } private void apply() { hide(); if (actionHandler != null && inputTextField != null) actionHandler.accept(inputTextField.getText()); } @Override protected void applyStyles() { super.applyStyles(); FormBuilder.getIconForLabel(AwesomeIcon.LOCK, headlineIcon, "1.5em"); } @Override protected void setModality() { stage.initOwner(owner.getScene().getWindow()); stage.initModality(Modality.NONE); } @Override protected void addEffectToBackground() { } @Override protected void removeEffectFromBackground() { } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/editor/PeerInfoWithTagEditor.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.editor; import haveno.common.UserThread; import haveno.common.crypto.PubKeyRing; import haveno.common.util.Tuple3; import haveno.common.util.Utilities; import haveno.core.alert.PrivateNotificationManager; import haveno.core.locale.GlobalSettings; import haveno.core.locale.Res; import haveno.core.offer.Offer; import haveno.core.trade.Trade; import haveno.core.user.Preferences; import haveno.desktop.components.InputTextField; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.main.overlays.windows.SendPrivateNotificationWindow; import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.beans.value.ChangeListener; import javafx.collections.ObservableList; import javafx.event.EventHandler; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Point2D; import javafx.scene.Camera; import javafx.scene.PerspectiveCamera; import javafx.scene.Scene; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.GridPane; import javafx.scene.layout.VBox; import javafx.scene.transform.Rotate; import javafx.stage.Modality; import javafx.stage.Window; import javafx.util.Duration; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addInputTextField; @Slf4j public class PeerInfoWithTagEditor extends Overlay { private final boolean useDevPrivilegeKeys; private InputTextField inputTextField; private Point2D position; private static PeerInfoWithTagEditor INSTANCE; private Consumer saveHandler; private String hostName; private int numTrades; private ChangeListener focusListener; private final PrivateNotificationManager privateNotificationManager; @Nullable private final Trade trade; private final Offer offer; private final Preferences preferences; private EventHandler keyEventEventHandler; @Nullable private String accountAge; private String accountAgeInfo; @Nullable private String accountSigningState; @Nullable private String signAge; @Nullable private String signAgeInfo; public PeerInfoWithTagEditor(PrivateNotificationManager privateNotificationManager, @Nullable Trade trade, Offer offer, Preferences preferences, boolean useDevPrivilegeKeys) { this.privateNotificationManager = privateNotificationManager; this.trade = trade; this.offer = offer; this.preferences = preferences; this.useDevPrivilegeKeys = useDevPrivilegeKeys; width = 468; type = Type.Undefined; if (INSTANCE != null) INSTANCE.hide(); INSTANCE = this; } public PeerInfoWithTagEditor onSave(Consumer saveHandler) { this.saveHandler = saveHandler; return this; } public PeerInfoWithTagEditor position(Point2D position) { this.position = position; return this; } public PeerInfoWithTagEditor fullAddress(String hostName) { this.hostName = hostName; return this; } public PeerInfoWithTagEditor accountAge(@Nullable String accountAge) { this.accountAge = accountAge; return this; } public PeerInfoWithTagEditor accountAgeInfo(String accountAgeInfo) { this.accountAgeInfo = accountAgeInfo; return this; } public PeerInfoWithTagEditor signAge(@Nullable String signAge) { this.signAge = signAge; return this; } public PeerInfoWithTagEditor signAgeInfo(String signAgeInfo) { this.signAgeInfo = signAgeInfo; return this; } public PeerInfoWithTagEditor accountSigningState(@Nullable String accountSigningState) { this.accountSigningState = accountSigningState; return this; } public PeerInfoWithTagEditor numTrades(int numTrades) { this.numTrades = numTrades; if (numTrades == 0) width = 568; return this; } @Override public void show() { headLine(Res.get("peerInfo.title")); actionButtonText(Res.get("shared.save")); createGridPane(); addHeadLine(); addContent(); addButtons(); applyStyles(); onShow(); } @Override protected void onShow() { super.display(); if (stage != null) { focusListener = (observable, oldValue, newValue) -> { if (!newValue) hide(); }; stage.focusedProperty().addListener(focusListener); Scene scene = stage.getScene(); if (scene != null) scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); } } @Override public void hide() { animateHide(); } @Override protected void onHidden() { INSTANCE = null; if (stage != null) { if (focusListener != null) stage.focusedProperty().removeListener(focusListener); Scene scene = stage.getScene(); if (scene != null) scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); } } private void addContent() { gridPane.setPadding(new Insets(64)); final Tuple3 onionTuple = addCompactTopLabelTextField(gridPane, ++rowIndex, Res.get("shared.onionAddress"), hostName); GridPane.setColumnSpan(onionTuple.third, 2); onionTuple.second.setMouseTransparent(false); GridPane.setColumnSpan(addCompactTopLabelTextField(gridPane, ++rowIndex, Res.get("peerInfo.nrOfTrades"), numTrades > 0 ? String.valueOf(numTrades) : Res.get("peerInfo.notTradedYet")).third, 2); if (accountAge != null) { GridPane.setColumnSpan(addCompactTopLabelTextField(gridPane, ++rowIndex, accountAgeInfo, accountAge).third, 2); } if (accountSigningState != null) { GridPane.setColumnSpan(addCompactTopLabelTextField(gridPane, ++rowIndex, Res.get("shared.accountSigningState"), accountSigningState).third, 2); } if (signAge != null) { GridPane.setColumnSpan(addCompactTopLabelTextField(gridPane, ++rowIndex, signAgeInfo, signAge).third, 2); } inputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("peerInfo.setTag")); GridPane.setColumnSpan(inputTextField, 2); Map peerTagMap = preferences.getPeerTagMap(); String tag = peerTagMap.getOrDefault(hostName, ""); inputTextField.setText(tag); keyEventEventHandler = event -> { if (Utilities.isAltOrCtrlPressed(KeyCode.R, event)) { // We need to close first our current popup and the open delayed the new one, // otherwise the text input handler does not work. doClose(); UserThread.runAfter(() -> { PubKeyRing peersPubKeyRing = null; if (trade != null) { peersPubKeyRing = trade.getTradePeer().getPubKeyRing(); } else if (offer != null) { peersPubKeyRing = offer.getPubKeyRing(); } if (peersPubKeyRing != null) { new SendPrivateNotificationWindow( privateNotificationManager, peersPubKeyRing, offer.getMakerNodeAddress(), useDevPrivilegeKeys ).show(); } }, 100, TimeUnit.MILLISECONDS); } }; } @Override protected void addHeadLine() { super.addHeadLine(); GridPane.setHalignment(headLineLabel, HPos.CENTER); } protected void setupKeyHandler(Scene scene) { scene.setOnKeyPressed(e -> { if (e.getCode() == KeyCode.ESCAPE) { e.consume(); doClose(); } if (e.getCode() == KeyCode.ENTER) { e.consume(); save(); } }); } @Override protected void animateHide(Runnable onFinishedHandler) { if (GlobalSettings.getUseAnimations()) { double duration = getDuration(300); Interpolator interpolator = Interpolator.SPLINE(0.25, 0.1, 0.25, 1); gridPane.setRotationAxis(Rotate.X_AXIS); Camera camera = gridPane.getScene().getCamera(); gridPane.getScene().setCamera(new PerspectiveCamera()); Timeline timeline = new Timeline(); ObservableList keyFrames = timeline.getKeyFrames(); keyFrames.add(new KeyFrame(Duration.millis(0), new KeyValue(gridPane.rotateProperty(), 0, interpolator), new KeyValue(gridPane.opacityProperty(), 1, interpolator) )); keyFrames.add(new KeyFrame(Duration.millis(duration), new KeyValue(gridPane.rotateProperty(), -90, interpolator), new KeyValue(gridPane.opacityProperty(), 0, interpolator) )); timeline.setOnFinished(event -> { gridPane.setRotate(0); gridPane.setRotationAxis(Rotate.Z_AXIS); gridPane.getScene().setCamera(camera); onFinishedHandler.run(); }); timeline.play(); } else { onFinishedHandler.run(); } } @Override protected void animateDisplay() { if (GlobalSettings.getUseAnimations()) { double startY = -160; double duration = getDuration(400); Interpolator interpolator = Interpolator.SPLINE(0.25, 0.1, 0.25, 1); Timeline timeline = new Timeline(); ObservableList keyFrames = timeline.getKeyFrames(); keyFrames.add(new KeyFrame(Duration.millis(0), new KeyValue(gridPane.opacityProperty(), 0, interpolator), new KeyValue(gridPane.translateYProperty(), startY, interpolator) )); keyFrames.add(new KeyFrame(Duration.millis(duration), new KeyValue(gridPane.opacityProperty(), 1, interpolator), new KeyValue(gridPane.translateYProperty(), 0, interpolator) )); timeline.play(); } } @Override protected void createGridPane() { super.createGridPane(); gridPane.setPadding(new Insets(15, 15, 30, 30)); } @Override protected void addButtons() { buttonDistance = 10; super.addButtons(); actionButton.setOnAction(event -> save()); } private void save() { hide(); if (saveHandler != null && inputTextField != null) saveHandler.accept(inputTextField.getText()); } @Override protected void applyStyles() { gridPane.getStyleClass().add("peer-info-popup-bg"); if (headLineLabel != null) headLineLabel.getStyleClass().add("peer-info-popup-headline"); } @Override protected void setModality() { stage.initOwner(owner.getScene().getWindow()); stage.initModality(Modality.NONE); } @Override protected void layout() { Window window = owner.getScene().getWindow(); stage.setX(Math.round(window.getX() + position.getX() - width)); stage.setY(Math.round(window.getY() + position.getY())); } @Override protected void addEffectToBackground() { } @Override protected void removeEffectFromBackground() { } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/notifications/Notification.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.notifications; import de.jensd.fx.fontawesome.AwesomeIcon; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.core.locale.Res; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.util.FormBuilder; import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.collections.ObservableList; import javafx.geometry.Insets; import javafx.scene.Camera; import javafx.scene.PerspectiveCamera; import javafx.scene.input.MouseEvent; import javafx.scene.transform.Rotate; import javafx.stage.Modality; import javafx.stage.Window; import javafx.util.Duration; public class Notification extends Overlay { private boolean hasBeenDisplayed; private boolean autoClose; private Timer autoCloseTimer; private static final int BORDER_PADDING = 10; public Notification() { width = 413; // 320 visible bg because of insets NotificationCenter.add(this); type = Type.Notification; } void onReadyForDisplay() { super.display(); if (autoClose && autoCloseTimer == null) autoCloseTimer = UserThread.runAfter(this::doClose, 6); UserThread.execute(() -> { stage.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> doClose()); }); } @Override public void hide() { animateHide(); } @Override protected void onShow() { NotificationManager.queueForDisplay(this); } @Override protected void onHidden() { NotificationManager.onHidden(this); } public Notification tradeHeadLine(String tradeId) { return headLine(Res.get("notification.trade.headline", tradeId)); } public Notification disputeHeadLine(String tradeId) { return headLine(Res.get("notification.ticket.headline", tradeId)); } @Override public void show() { if (DevEnv.isDevMode()) { return; } super.show(); hasBeenDisplayed = true; } public Notification autoClose() { autoClose = true; return this; } @Override protected void animateHide(Runnable onFinishedHandler) { if (autoCloseTimer != null) { autoCloseTimer.stop(); autoCloseTimer = null; } if (NotificationCenter.useAnimations) { double duration = getDuration(400); Interpolator interpolator = Interpolator.SPLINE(0.25, 0.1, 0.25, 1); gridPane.setRotationAxis(Rotate.X_AXIS); Camera camera = gridPane.getScene().getCamera(); gridPane.getScene().setCamera(new PerspectiveCamera()); Timeline timeline = new Timeline(); ObservableList keyFrames = timeline.getKeyFrames(); keyFrames.add(new KeyFrame(Duration.millis(0), new KeyValue(gridPane.rotateProperty(), 0, interpolator), new KeyValue(gridPane.opacityProperty(), 1, interpolator) )); keyFrames.add(new KeyFrame(Duration.millis(duration), new KeyValue(gridPane.rotateProperty(), -90, interpolator), new KeyValue(gridPane.opacityProperty(), 0, interpolator) )); timeline.setOnFinished(event -> { gridPane.setRotate(0); gridPane.setRotationAxis(Rotate.Z_AXIS); gridPane.getScene().setCamera(camera); onFinishedHandler.run(); }); timeline.play(); } else { onFinishedHandler.run(); } } @Override protected void animateDisplay() { if (NotificationCenter.useAnimations) { double startX = 320; double duration = getDuration(600); Interpolator interpolator = Interpolator.SPLINE(0.25, 0.1, 0.25, 1); Timeline timeline = new Timeline(); ObservableList keyFrames = timeline.getKeyFrames(); keyFrames.add(new KeyFrame(Duration.millis(0), new KeyValue(gridPane.opacityProperty(), 0, interpolator), new KeyValue(gridPane.translateXProperty(), startX, interpolator) )); //bouncing /* keyFrames.add(new KeyFrame(Duration.millis(duration * 0.6), new KeyValue(gridPane.opacityProperty(), 1, interpolator), new KeyValue(gridPane.translateXProperty(), -12, interpolator) )); keyFrames.add(new KeyFrame(Duration.millis(duration * 0.8), new KeyValue(gridPane.opacityProperty(), 1, interpolator), new KeyValue(gridPane.translateXProperty(), 4, interpolator) ));*/ keyFrames.add(new KeyFrame(Duration.millis(duration), new KeyValue(gridPane.opacityProperty(), 1, interpolator), new KeyValue(gridPane.translateXProperty(), 0, interpolator) )); timeline.play(); } } @Override protected void createGridPane() { super.createGridPane(); gridPane.setPadding(new Insets(62, 62, 62, 62)); } @Override protected void addButtons() { buttonDistance = 10; super.addButtons(); } @Override protected void applyStyles() { gridPane.getStyleClass().add("notification-popup-bg"); if (headLineLabel != null) headLineLabel.getStyleClass().add("notification-popup-headline"); headlineIcon.getStyleClass().add("popup-icon-information"); headlineIcon.setManaged(true); headlineIcon.setVisible(true); headlineIcon.setPadding(new Insets(1)); FormBuilder.getIconForLabel(AwesomeIcon.INFO_SIGN, headlineIcon, "1em"); if (actionButton != null) actionButton.getStyleClass().add("compact-button"); } @Override protected void setModality() { stage.initOwner(owner.getScene().getWindow()); stage.initModality(Modality.NONE); } @Override protected void layout() { Window window = owner.getScene().getWindow(); double titleBarHeight = window.getHeight() - owner.getScene().getHeight(); double shadowInset = 44; stage.setX(Math.round(window.getX() + window.getWidth() + shadowInset - stage.getWidth() - BORDER_PADDING)); stage.setY(Math.round(window.getY() + titleBarHeight - shadowInset + BORDER_PADDING)); } @Override protected void addEffectToBackground() { } @Override protected void removeEffectFromBackground() { } public boolean isHasBeenDisplayed() { return hasBeenDisplayed; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/notifications/NotificationCenter.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.notifications; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.ThreadUtils; import haveno.common.UserThread; import haveno.core.api.NotificationListener; import haveno.core.locale.Res; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.mediation.MediationManager; import haveno.core.support.dispute.refund.RefundManager; import haveno.core.trade.BuyerTrade; import haveno.core.trade.MakerTrade; import haveno.core.trade.SellerTrade; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.user.DontShowAgainLookup; import haveno.core.user.Preferences; import haveno.desktop.Navigation; import haveno.desktop.main.MainView; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.portfolio.PortfolioView; import haveno.desktop.main.portfolio.pendingtrades.PendingTradesView; import haveno.desktop.main.support.SupportView; import haveno.desktop.main.support.dispute.DisputeView; import haveno.desktop.main.support.dispute.agent.arbitration.ArbitratorView; import haveno.desktop.main.support.dispute.client.arbitration.ArbitrationClientView; import haveno.desktop.main.support.dispute.client.mediation.MediationClientView; import haveno.desktop.main.support.dispute.client.refund.RefundClientView; import haveno.proto.grpc.NotificationMessage; import haveno.proto.grpc.NotificationMessage.NotificationType; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; import javafx.collections.ListChangeListener; import javax.annotation.Nullable; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; @Slf4j @Singleton public class NotificationCenter { /////////////////////////////////////////////////////////////////////////////////////////// // Static /////////////////////////////////////////////////////////////////////////////////////////// @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") private final static List notifications = new ArrayList<>(); private Consumer selectItemByTradeIdConsumer; static void add(Notification notification) { notifications.add(notification); } static boolean useAnimations; /////////////////////////////////////////////////////////////////////////////////////////// // Instance fields /////////////////////////////////////////////////////////////////////////////////////////// private final TradeManager tradeManager; private final ArbitrationManager arbitrationManager; private final MediationManager mediationManager; private final RefundManager refundManager; private final Navigation navigation; private final Map disputeStateSubscriptionsMap = new HashMap<>(); private final Map tradePhaseSubscriptionsMap = new HashMap<>(); @Nullable private String selectedTradeId; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, initialisation /////////////////////////////////////////////////////////////////////////////////////////// @Inject public NotificationCenter(TradeManager tradeManager, ArbitrationManager arbitrationManager, MediationManager mediationManager, RefundManager refundManager, Preferences preferences, Navigation navigation) { this.tradeManager = tradeManager; this.arbitrationManager = arbitrationManager; this.mediationManager = mediationManager; this.refundManager = refundManager; this.navigation = navigation; EasyBind.subscribe(preferences.getUseAnimationsProperty(), useAnimations -> NotificationCenter.useAnimations = useAnimations); } public void onAllServicesAndViewsInitialized() { tradeManager.getObservableList().addListener((ListChangeListener) change -> { change.next(); if (change.wasRemoved()) { change.getRemoved().forEach(trade -> { String tradeId = trade.getId(); if (disputeStateSubscriptionsMap.containsKey(tradeId)) { disputeStateSubscriptionsMap.get(tradeId).unsubscribe(); disputeStateSubscriptionsMap.remove(tradeId); } if (tradePhaseSubscriptionsMap.containsKey(tradeId)) { tradePhaseSubscriptionsMap.get(tradeId).unsubscribe(); tradePhaseSubscriptionsMap.remove(tradeId); } }); } if (change.wasAdded()) { change.getAddedSubList().forEach(trade -> { String tradeId = trade.getId(); if (disputeStateSubscriptionsMap.containsKey(tradeId)) { log.debug("We have already an entry in disputeStateSubscriptionsMap."); } else { Subscription disputeStateSubscription = EasyBind.subscribe(trade.disputeStateProperty(), disputeState -> ThreadUtils.submitToPool(() -> onDisputeStateChanged(trade, disputeState))); disputeStateSubscriptionsMap.put(tradeId, disputeStateSubscription); } if (tradePhaseSubscriptionsMap.containsKey(tradeId)) { log.debug("We have already an entry in tradePhaseSubscriptionsMap."); } else { Subscription tradePhaseSubscription = EasyBind.subscribe(trade.statePhaseProperty(), phase -> onTradePhaseChanged(trade, phase)); tradePhaseSubscriptionsMap.put(tradeId, tradePhaseSubscription); } }); } }); tradeManager.getObservableList().forEach(trade -> { String tradeId = trade.getId(); Subscription disputeStateSubscription = EasyBind.subscribe(trade.disputeStateProperty(), disputeState -> ThreadUtils.submitToPool(() -> onDisputeStateChanged(trade, disputeState))); disputeStateSubscriptionsMap.put(tradeId, disputeStateSubscription); Subscription tradePhaseSubscription = EasyBind.subscribe(trade.statePhaseProperty(), phase -> onTradePhaseChanged(trade, phase)); tradePhaseSubscriptionsMap.put(tradeId, tradePhaseSubscription); } ); // show popup for error notifications tradeManager.getNotificationService().addListener(new NotificationListener() { @Override public void onMessage(@NonNull NotificationMessage message) { if (message.getType() == NotificationType.ERROR) { new Popup().warning(message.getMessage()).show(); } } }); } /////////////////////////////////////////////////////////////////////////////////////////// // Setter/Getter /////////////////////////////////////////////////////////////////////////////////////////// @Nullable public String getSelectedTradeId() { return selectedTradeId; } public void setSelectedTradeId(@Nullable String selectedTradeId) { this.selectedTradeId = selectedTradeId; } public void setSelectItemByTradeIdConsumer(Consumer selectItemByTradeIdConsumer) { this.selectItemByTradeIdConsumer = selectItemByTradeIdConsumer; } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void onTradePhaseChanged(Trade trade, Trade.Phase phase) { String message = null; if (trade.isPayoutPublished() && !trade.isCompleted()) { message = Res.get("notification.trade.completed"); } else { if (trade instanceof MakerTrade && phase.ordinal() == Trade.Phase.DEPOSITS_PUBLISHED.ordinal()) { final String role = trade instanceof BuyerTrade ? Res.get("shared.seller") : Res.get("shared.buyer"); message = Res.get("notification.trade.accepted", role); } if (trade instanceof BuyerTrade) { if (phase.ordinal() == Trade.Phase.DEPOSITS_UNLOCKED.ordinal()) message = Res.get("notification.trade.unlocked"); else if (phase.ordinal() == Trade.Phase.DEPOSITS_FINALIZED.ordinal()) message = Res.get("notification.trade.finalized", Trade.NUM_BLOCKS_DEPOSITS_FINALIZED); } else if (trade instanceof SellerTrade && phase.ordinal() == Trade.Phase.PAYMENT_SENT.ordinal()) message = Res.get("notification.trade.paymentSent"); } if (message != null) { String key = "NotificationCenter_" + phase.name() + trade.getId(); if (DontShowAgainLookup.showAgain(key)) { Notification notification = new Notification().tradeHeadLine(trade.getShortId()).message(message); if (navigation.getCurrentPath() != null && !navigation.getCurrentPath().contains(PendingTradesView.class)) { notification.actionButtonTextWithGoTo("portfolio.tab.pendingTrades") .onAction(() -> { DontShowAgainLookup.dontShowAgain(key, true); navigation.navigateTo(MainView.class, PortfolioView.class, PendingTradesView.class); if (selectItemByTradeIdConsumer != null) UserThread.runAfter(() -> selectItemByTradeIdConsumer.accept(trade.getId()), 1); }) .onClose(() -> DontShowAgainLookup.dontShowAgain(key, true)) .show(); } else if (selectedTradeId != null && !trade.getId().equals(selectedTradeId)) { notification.actionButtonText(Res.get("notification.trade.selectTrade")) .onAction(() -> { DontShowAgainLookup.dontShowAgain(key, true); if (selectItemByTradeIdConsumer != null) selectItemByTradeIdConsumer.accept(trade.getId()); }) .onClose(() -> DontShowAgainLookup.dontShowAgain(key, true)) .show(); } } } } private void onDisputeStateChanged(Trade trade, Trade.DisputeState disputeState) { String message = null; if (arbitrationManager.findDispute(trade.getId()).isPresent()) { Dispute dispute = arbitrationManager.findDispute(trade.getId()).get(); String disputeOrTicket = dispute.isSupportTicket() ? Res.get("shared.supportTicket") : Res.get("shared.dispute"); switch (disputeState) { case NO_DISPUTE: break; case DISPUTE_OPENED: // notify if arbitrator or dispute opener (arbitrator's disputes are in context of each trader, so isOpener() doesn't apply) if (trade.isArbitrator() || !dispute.isOpener()) message = Res.get("notification.trade.peerOpenedDispute", disputeOrTicket); break; case DISPUTE_CLOSED: // skip notifying arbitrator if (!trade.isArbitrator()) message = Res.get("notification.trade.disputeClosed", disputeOrTicket); break; default: break; } if (message != null) { goToSupport(trade, message, trade.isArbitrator() ? ArbitratorView.class : ArbitrationClientView.class); } } else if (refundManager.findDispute(trade.getId()).isPresent()) { String disputeOrTicket = refundManager.findDispute(trade.getId()).get().isSupportTicket() ? Res.get("shared.supportTicket") : Res.get("shared.dispute"); switch (disputeState) { case NO_DISPUTE: break; case REFUND_REQUESTED: break; case REFUND_REQUEST_STARTED_BY_PEER: message = Res.get("notification.trade.peerOpenedDispute", disputeOrTicket); break; case REFUND_REQUEST_CLOSED: message = Res.get("notification.trade.disputeClosed", disputeOrTicket); break; default: // if (DevEnv.isDevMode()) { // log.error("refundManager must not contain mediation or arbitration disputes. disputeState={}", disputeState); // throw new RuntimeException("arbitrationDisputeManager must not contain mediation disputes"); // } break; } if (message != null) { goToSupport(trade, message, RefundClientView.class); } } else if (mediationManager.findDispute(trade.getId()).isPresent()) { String disputeOrTicket = mediationManager.findDispute(trade.getId()).get().isSupportTicket() ? Res.get("shared.supportTicket") : Res.get("shared.mediationCase"); switch (disputeState) { // TODO case MEDIATION_REQUESTED: break; case MEDIATION_STARTED_BY_PEER: message = Res.get("notification.trade.peerOpenedDispute", disputeOrTicket); break; case MEDIATION_CLOSED: message = Res.get("notification.trade.disputeClosed", disputeOrTicket); break; default: // if (DevEnv.isDevMode()) { // log.error("mediationDisputeManager must not contain arbitration or refund disputes. disputeState={}", disputeState); // throw new RuntimeException("mediationDisputeManager must not contain arbitration disputes"); // } break; } if (message != null) { goToSupport(trade, message, MediationClientView.class); } } } private void goToSupport(Trade trade, String message, Class viewClass) { Notification notification = new Notification().disputeHeadLine(trade.getShortId()).message(message); if (navigation.getCurrentPath() != null && !navigation.getCurrentPath().contains(viewClass)) { notification.actionButtonTextWithGoTo("mainView.menu.support") .onAction(() -> navigation.navigateTo(MainView.class, SupportView.class, viewClass)) .show(); } else { notification.show(); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/notifications/NotificationManager.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.notifications; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; public class NotificationManager { private static final Logger log = LoggerFactory.getLogger(NotificationManager.class); private static final Queue popups = new LinkedBlockingQueue<>(5); private static Notification displayedPopup; public static void queueForDisplay(Notification popup) { boolean result = popups.offer(popup); if (!result) log.warn("The capacity is full with popups in the queue.\n\t" + "Not added new popup=" + popup); displayNext(); } public static void onHidden(Notification popup) { if (displayedPopup == null || displayedPopup == popup) { displayedPopup = null; displayNext(); } else { log.warn("We got a isHidden called with a wrong popup.\n\t" + "popup (argument)=" + popup + "\n\tdisplayedPopup=" + displayedPopup); } } private static void displayNext() { if (displayedPopup == null) { if (!popups.isEmpty()) { displayedPopup = popups.poll(); displayedPopup.onReadyForDisplay(); } } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/popups/Popup.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.popups; import haveno.desktop.main.overlays.Overlay; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Popup extends Overlay { protected final Logger log = LoggerFactory.getLogger(this.getClass()); public Popup() { } public void onReadyForDisplay() { super.display(); } @Override protected void onShow() { PopupManager.queueForDisplay(this); } @Override protected void onHidden() { PopupManager.onHidden(this); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/popups/PopupManager.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.popups; import haveno.common.UserThread; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; public class PopupManager { private static final Logger log = LoggerFactory.getLogger(PopupManager.class); private static final Queue popups = new LinkedBlockingQueue<>(5); private static Popup displayedPopup; public static void queueForDisplay(Popup popup) { if (hasDuplicatePopup(popup)) { log.warn("The popup is already in the queue or displayed.\n\t" + "New popup not added=" + popup); return; } boolean result = popups.offer(popup); if (!result) log.warn("The capacity is full with popups in the queue.\n\t" + "New popup not added=" + popup); displayNext(); } public static void onHidden(Popup popup) { if (displayedPopup == null || displayedPopup == popup) { displayedPopup = null; UserThread.runAfter(() -> { displayNext(); }, 100, TimeUnit.MILLISECONDS); } else { boolean removed = popups.remove(popup); if (removed) { log.warn("We got a isHidden called with a wrong popup.\n\t" + "popup (argument)=" + popup + "\n\tdisplayedPopup=" + displayedPopup); } } } private static void displayNext() { if (displayedPopup == null) { if (!popups.isEmpty()) { displayedPopup = popups.poll(); displayedPopup.onReadyForDisplay(); } } } private static boolean hasDuplicatePopup(Popup popup) { if (displayedPopup != null && displayedPopup.toString().equals(popup.toString())) { return true; } for (Popup p : popups) { if (p.toString().equals(popup.toString())) { return true; } } return false; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/ClosedTradesSummaryWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import com.google.inject.Inject; import haveno.core.locale.Res; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.main.portfolio.closedtrades.ClosedTradesViewModel; import static haveno.desktop.util.FormBuilder.addConfirmationLabelLabel; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import haveno.desktop.util.Layout; import java.math.BigInteger; import java.util.Map; import javafx.geometry.Insets; public class ClosedTradesSummaryWindow extends Overlay { private final ClosedTradesViewModel model; @Inject public ClosedTradesSummaryWindow(ClosedTradesViewModel model) { this.model = model; type = Type.Information; } public void show() { rowIndex = 0; width = 900; createGridPane(); addContent(); addButtons(); display(); } /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void createGridPane() { super.createGridPane(); gridPane.setPadding(new Insets(35, 40, 30, 40)); gridPane.getStyleClass().add("grid-pane"); } private void addContent() { Map totalVolumeByCurrency = model.getTotalVolumeByCurrency(); int rowSpan = totalVolumeByCurrency.size() + 4; addTitledGroupBg(gridPane, rowIndex, rowSpan, Res.get("closedTradesSummaryWindow.headline")); BigInteger totalTradeAmount = model.getTotalTradeAmount(); addConfirmationLabelLabel(gridPane, rowIndex, Res.get("closedTradesSummaryWindow.totalAmount.title"), model.getTotalAmountWithVolume(totalTradeAmount), Layout.TWICE_FIRST_ROW_DISTANCE); totalVolumeByCurrency.entrySet().forEach(entry -> { addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("closedTradesSummaryWindow.totalVolume.title", entry.getKey()), entry.getValue()); }); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("closedTradesSummaryWindow.totalMinerFee.title"), model.getTotalTxFee(totalTradeAmount)); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("closedTradesSummaryWindow.totalTradeFeeInXmr.title"), model.getTotalTradeFee(totalTradeAmount)); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/ContractWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import com.google.common.base.Joiner; import com.google.inject.Inject; import haveno.common.UserThread; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import haveno.core.offer.Offer; import haveno.core.payment.payload.PaymentMethod; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeList; import haveno.core.support.dispute.DisputeManager; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.mediation.MediationManager; import haveno.core.support.dispute.refund.RefundManager; import haveno.core.trade.Contract; import haveno.core.trade.HavenoUtils; import haveno.core.util.FormattingUtils; import haveno.core.util.VolumeUtil; import haveno.desktop.components.HavenoTextArea; import haveno.desktop.main.MainView; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.util.DisplayUtils; import static haveno.desktop.util.DisplayUtils.getAccountWitnessDescription; import static haveno.desktop.util.FormBuilder.addButtonAfterGroup; import static haveno.desktop.util.FormBuilder.addConfirmationLabelButton; import static haveno.desktop.util.FormBuilder.addConfirmationLabelTextField; import static haveno.desktop.util.FormBuilder.addConfirmationLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addLabelExplorerAddressTextField; import static haveno.desktop.util.FormBuilder.addLabelTxIdTextField; import static haveno.desktop.util.FormBuilder.addSeparator; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import haveno.desktop.util.Layout; import haveno.network.p2p.NodeAddress; import java.util.List; import javafx.geometry.Insets; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.TextArea; import javafx.scene.control.Tooltip; import javafx.scene.input.KeyCode; import javafx.scene.layout.GridPane; import javafx.stage.Modality; import javafx.stage.Stage; import javafx.stage.StageStyle; import javafx.stage.Window; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.Utils; @Slf4j public class ContractWindow extends Overlay { private final ArbitrationManager arbitrationManager; private final MediationManager mediationManager; private final RefundManager refundManager; private final AccountAgeWitnessService accountAgeWitnessService; private Dispute dispute; /////////////////////////////////////////////////////////////////////////////////////////// // Public API /////////////////////////////////////////////////////////////////////////////////////////// @Inject public ContractWindow(ArbitrationManager arbitrationManager, MediationManager mediationManager, RefundManager refundManager, AccountAgeWitnessService accountAgeWitnessService) { this.arbitrationManager = arbitrationManager; this.mediationManager = mediationManager; this.refundManager = refundManager; this.accountAgeWitnessService = accountAgeWitnessService; type = Type.Confirmation; } public void show(Dispute dispute) { this.dispute = dispute; rowIndex = -1; width = 1168; createGridPane(); addContent(); display(); } /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void createGridPane() { super.createGridPane(); gridPane.getColumnConstraints().get(0).setMinWidth(250d); gridPane.setPadding(new Insets(35, 40, 30, 40)); gridPane.getStyleClass().add("grid-pane"); } private void addContent() { Contract contract = dispute.getContract(); Offer offer = new Offer(contract.getOfferPayload()); List acceptedBanks = offer.getAcceptedBankIds(); boolean showAcceptedBanks = acceptedBanks != null && !acceptedBanks.isEmpty(); List acceptedCountryCodes = offer.getAcceptedCountryCodes(); boolean showAcceptedCountryCodes = acceptedCountryCodes != null && !acceptedCountryCodes.isEmpty(); int rows = 18; if (dispute.getPayoutTxSerialized() != null) rows++; if (dispute.getDelayedPayoutTxId() != null) rows++; if (dispute.getDonationAddressOfDelayedPayoutTx() != null) rows++; if (showAcceptedCountryCodes) rows++; if (showAcceptedBanks) rows++; addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("contractWindow.title")); addConfirmationLabelTextField(gridPane, rowIndex, Res.get("shared.offerId"), offer.getId(), Layout.TWICE_FIRST_ROW_DISTANCE); addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("contractWindow.dates"), DisplayUtils.formatDateTime(offer.getDate()) + " / " + DisplayUtils.formatDateTime(dispute.getTradeDate())); String currencyCode = offer.getCounterCurrencyCode(); addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.offerType"), DisplayUtils.getDirectionBothSides(offer.getDirection(), offer.isPrivateOffer())); addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.tradePrice"), FormattingUtils.formatPrice(contract.getPrice())); addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.tradeAmount"), HavenoUtils.formatXmr(contract.getTradeAmount(), true)); addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, VolumeUtil.formatVolumeLabel(currencyCode, ":"), VolumeUtil.formatVolumeWithCode(contract.getTradeVolume())); String securityDeposit = Res.getWithColAndCap("shared.buyer") + " " + HavenoUtils.formatXmr(offer.getOfferPayload().getBuyerSecurityDepositForTradeAmount(contract.getTradeAmount()), true) + " / " + Res.getWithColAndCap("shared.seller") + " " + HavenoUtils.formatXmr(offer.getOfferPayload().getSellerSecurityDepositForTradeAmount(contract.getTradeAmount()), true); addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), securityDeposit); addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("contractWindow.xmrAddresses"), contract.getBuyerPayoutAddressString() + " / " + contract.getSellerPayoutAddressString()); addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("contractWindow.onions"), contract.getBuyerNodeAddress().getFullAddress() + " / " + contract.getSellerNodeAddress().getFullAddress()); addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("contractWindow.accountAge"), getAccountWitnessDescription(accountAgeWitnessService, offer.getPaymentMethod(), dispute.getBuyerPaymentAccountPayload(), contract.getBuyerPubKeyRing()) + " / " + getAccountWitnessDescription(accountAgeWitnessService, offer.getPaymentMethod(), dispute.getSellerPaymentAccountPayload(), contract.getSellerPubKeyRing())); DisputeManager> disputeManager = getDisputeManager(dispute); String nrOfDisputesAsBuyer = disputeManager != null ? disputeManager.getNrOfDisputes(true, contract) : ""; String nrOfDisputesAsSeller = disputeManager != null ? disputeManager.getNrOfDisputes(false, contract) : ""; addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("contractWindow.numDisputes"), nrOfDisputesAsBuyer + " / " + nrOfDisputesAsSeller); addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.paymentDetails", Res.get("shared.buyer")), dispute.getBuyerPaymentAccountPayload() != null ? dispute.getBuyerPaymentAccountPayload().getPaymentDetails() : "NA"); addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.paymentDetails", Res.get("shared.seller")), dispute.getSellerPaymentAccountPayload() != null ? dispute.getSellerPaymentAccountPayload().getPaymentDetails() : "NA"); String title = ""; String agentMatrixUserName = ""; if (dispute.getSupportType() != null) { switch (dispute.getSupportType()) { case ARBITRATION: title = Res.get("shared.selectedArbitrator"); break; case MEDIATION: throw new RuntimeException("Mediation type not adapted to XMR"); // agentMatrixUserName = DisputeAgentLookupMap.getMatrixUserName(contract.getMediatorNodeAddress().getFullAddress()); // title = Res.get("shared.selectedMediator"); // break; case TRADE: break; case REFUND: throw new RuntimeException("Refund type not adapted to XMR"); // agentMatrixUserName = DisputeAgentLookupMap.getMatrixUserName(contract.getRefundAgentNodeAddress().getFullAddress()); // title = Res.get("shared.selectedRefundAgent"); // break; } } if (disputeManager != null) { NodeAddress agentNodeAddress = disputeManager.getAgentNodeAddress(dispute); if (agentNodeAddress != null) { String value = agentMatrixUserName + " (" + agentNodeAddress.getFullAddress() + ")"; addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, title, value); } } if (showAcceptedCountryCodes) { String countries; Tooltip tooltip = null; if (CountryUtil.containsAllSepaEuroCountries(acceptedCountryCodes)) { countries = Res.get("shared.allEuroCountries"); } else { countries = CountryUtil.getCodesString(acceptedCountryCodes); tooltip = new Tooltip(CountryUtil.getNamesByCodesString(acceptedCountryCodes)); } addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.acceptedTakerCountries"), countries) .second.setTooltip(tooltip); } if (showAcceptedBanks) { if (offer.getPaymentMethod().equals(PaymentMethod.SAME_BANK)) { addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.bankName"), acceptedBanks.get(0)); } else if (offer.getPaymentMethod().equals(PaymentMethod.SPECIFIC_BANKS)) { String value = Joiner.on(", ").join(acceptedBanks); Tooltip tooltip = new Tooltip(Res.get("shared.acceptedBanks") + value); addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.acceptedBanks"), value) .second.setTooltip(tooltip); } } addSeparator(gridPane, ++rowIndex); addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.makerDepositTransactionId"), contract.getMakerDepositTxHash()); if (contract.getTakerDepositTxHash() != null) { addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.takerDepositTransactionId"), contract.getTakerDepositTxHash()); } if (dispute.getDelayedPayoutTxId() != null) { addSeparator(gridPane, ++rowIndex); addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.delayedPayoutTxId"), dispute.getDelayedPayoutTxId()); } if (dispute.getDonationAddressOfDelayedPayoutTx() != null) { addSeparator(gridPane, ++rowIndex); addLabelExplorerAddressTextField(gridPane, ++rowIndex, Res.get("shared.delayedPayoutTxReceiverAddress"), dispute.getDonationAddressOfDelayedPayoutTx()); } if (dispute.getPayoutTxSerialized() != null) { addSeparator(gridPane, ++rowIndex); addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.payoutTxId"), dispute.getPayoutTxId()); } if (dispute.getContractHash() != null) { addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("contractWindow.contractHash"), Utils.HEX.encode(dispute.getContractHash())).second.setMouseTransparent(false); } addSeparator(gridPane, ++rowIndex); Button viewContractButton = addConfirmationLabelButton(gridPane, ++rowIndex, Res.get("shared.contractAsJson"), Res.get("shared.viewContractAsJson"), 0).second; viewContractButton.setDefaultButton(false); viewContractButton.setOnAction(e -> { TextArea textArea = new HavenoTextArea(); String contractAsJson = dispute.getContractAsJson(); textArea.setText(contractAsJson); textArea.setPrefHeight(50); textArea.setEditable(false); textArea.setWrapText(true); textArea.setPrefSize(800, 600); Scene viewContractScene = new Scene(textArea); Stage viewContractStage = new Stage(); viewContractStage.setTitle(Res.get("shared.contract.title", dispute.getShortTradeId())); viewContractStage.setScene(viewContractScene); if (owner == null) owner = MainView.getRootContainer(); Scene rootScene = owner.getScene(); viewContractStage.initOwner(rootScene.getWindow()); viewContractStage.initModality(Modality.NONE); viewContractStage.initStyle(StageStyle.UTILITY); viewContractStage.setOpacity(0); viewContractStage.show(); Window window = rootScene.getWindow(); double titleBarHeight = window.getHeight() - rootScene.getHeight(); viewContractStage.setX(Math.round(window.getX() + (owner.getWidth() - viewContractStage.getWidth()) / 2) + 200); viewContractStage.setY(Math.round(window.getY() + titleBarHeight + (owner.getHeight() - viewContractStage.getHeight()) / 2) + 50); // Delay display to next render frame to avoid that the popup is first quickly displayed in default position // and after a short moment in the correct position UserThread.execute(() -> viewContractStage.setOpacity(1)); viewContractScene.setOnKeyPressed(ev -> { if (ev.getCode() == KeyCode.ESCAPE) { ev.consume(); viewContractStage.hide(); } }); }); Button closeButton = addButtonAfterGroup(gridPane, ++rowIndex, Res.get("shared.close")); GridPane.setColumnSpan(closeButton, 2); closeButton.setOnAction(e -> { closeHandlerOptional.ifPresent(Runnable::run); hide(); }); } private DisputeManager> getDisputeManager(Dispute dispute) { if (dispute.getSupportType() != null) { switch (dispute.getSupportType()) { case ARBITRATION: return arbitrationManager; case MEDIATION: return mediationManager; case TRADE: break; case REFUND: return refundManager; } } return null; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/DisplayAlertMessageWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import haveno.core.alert.Alert; import haveno.core.locale.Res; import haveno.desktop.components.HyperlinkWithIcon; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.util.FormBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static com.google.common.base.Preconditions.checkNotNull; public class DisplayAlertMessageWindow extends Overlay { private static final Logger log = LoggerFactory.getLogger(DisplayAlertMessageWindow.class); private Alert alert; /////////////////////////////////////////////////////////////////////////////////////////// // Public API /////////////////////////////////////////////////////////////////////////////////////////// public DisplayAlertMessageWindow() { type = Type.Attention; } @Override public void show() { width = 768; // need to set headLine, otherwise the fields will not be created in addHeadLine createGridPane(); checkNotNull(alert, "alertMessage must not be null"); if (alert.isSoftwareUpdateNotification()) { information(""); headLine = Res.get("displayAlertMessageWindow.update.headline"); } else { error(""); headLine = Res.get("displayAlertMessageWindow.headline"); } headLine = Res.get("displayAlertMessageWindow.headline"); addHeadLine(); addContent(); addButtons(); applyStyles(); display(); } public DisplayAlertMessageWindow alertMessage(Alert alert) { this.alert = alert; return this; } /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// private void addContent() { checkNotNull(alert, "alertMessage must not be null"); message(alert.getMessage()); addMessage(); if (alert.isSoftwareUpdateNotification()) { String url = "https://haveno.exchange/downloads"; HyperlinkWithIcon hyperlinkWithIcon = FormBuilder.addLabelHyperlinkWithIcon(gridPane, ++rowIndex, Res.get("displayAlertMessageWindow.update.download"), url, url).second; hyperlinkWithIcon.setMaxWidth(550); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.handlers.ResultHandler; import haveno.common.util.Tuple2; import haveno.common.util.Tuple3; import haveno.core.api.CoreDisputesService; import haveno.core.api.CoreDisputesService.PayoutSuggestion; import haveno.core.locale.Res; import haveno.core.support.SupportType; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeList; import haveno.core.support.dispute.DisputeManager; import haveno.core.support.dispute.DisputeResult; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.mediation.MediationManager; import haveno.core.trade.Contract; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.util.FormattingUtils; import haveno.core.util.VolumeUtil; import haveno.core.util.coin.CoinFormatter; import haveno.core.xmr.wallet.TradeWalletService; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.components.AutoTooltipRadioButton; import haveno.desktop.components.HavenoTextArea; import haveno.desktop.components.InputTextField; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.DisplayUtils; import haveno.desktop.util.Layout; import javafx.beans.binding.Bindings; import javafx.beans.value.ChangeListener; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.RadioButton; import javafx.scene.control.TextArea; import javafx.scene.control.Toggle; import javafx.scene.control.ToggleGroup; import javafx.scene.input.KeyCode; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import lombok.extern.slf4j.Slf4j; import monero.wallet.model.MoneroDestination; import monero.wallet.model.MoneroTxWallet; import java.math.BigInteger; import java.util.Date; import java.util.List; import java.util.Optional; import static com.google.common.base.Preconditions.checkNotNull; import static haveno.desktop.util.FormBuilder.add2ButtonsWithBox; import static haveno.desktop.util.FormBuilder.addConfirmationLabelLabel; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import static haveno.desktop.util.FormBuilder.addTopLabelWithVBox; @Slf4j public class DisputeSummaryWindow extends Overlay { private final CoinFormatter formatter; private final TradeManager tradeManager; private final ArbitrationManager arbitrationManager; private final MediationManager mediationManager; private final CoreDisputesService disputesService; private Dispute dispute; private Trade trade; private ToggleGroup tradeAmountToggleGroup, reasonToggleGroup; private DisputeResult disputeResult; private RadioButton buyerGetsTradeAmountRadioButton, sellerGetsTradeAmountRadioButton, buyerGetsAllRadioButton, sellerGetsAllRadioButton, customRadioButton; private RadioButton reasonWasBugRadioButton, reasonWasUsabilityIssueRadioButton, reasonProtocolViolationRadioButton, reasonNoReplyRadioButton, reasonWasScamRadioButton, reasonWasOtherRadioButton, reasonWasBankRadioButton, reasonWasOptionTradeRadioButton, reasonWasSellerNotRespondingRadioButton, reasonWasWrongSenderAccountRadioButton, reasonWasPeerWasLateRadioButton, reasonWasTradeAlreadySettledRadioButton; private CoreDisputesService.PayoutSuggestion payoutSuggestion; // Dispute object of other trade peer. The dispute field is the one from which we opened the close dispute window. private Optional peersDisputeOptional; private String role; private TextArea summaryNotesTextArea; private ChangeListener customRadioButtonSelectedListener; private ChangeListener reasonToggleSelectionListener; private InputTextField buyerPayoutAmountInputTextField, sellerPayoutAmountInputTextField; private ChangeListener buyerPayoutAmountListener, sellerPayoutAmountListener; private ChangeListener tradeAmountToggleGroupListener; /////////////////////////////////////////////////////////////////////////////////////////// // Public API /////////////////////////////////////////////////////////////////////////////////////////// @Inject public DisputeSummaryWindow(@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, TradeManager tradeManager, ArbitrationManager arbitrationManager, MediationManager mediationManager, XmrWalletService walletService, TradeWalletService tradeWalletService, CoreDisputesService disputesService) { this.formatter = formatter; this.tradeManager = tradeManager; this.arbitrationManager = arbitrationManager; this.mediationManager = mediationManager; this.disputesService = disputesService; type = Type.Confirmation; } public void show(Dispute dispute) { this.dispute = dispute; this.trade = tradeManager.getTrade(dispute.getTradeId()); this.payoutSuggestion = null; rowIndex = -1; width = 1150; createGridPane(); addContent(); display(); if (DevEnv.isDevMode()) { UserThread.execute(() -> { summaryNotesTextArea.setText("dummy result...."); }); } } /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void cleanup() { if (reasonToggleGroup != null) reasonToggleGroup.selectedToggleProperty().removeListener(reasonToggleSelectionListener); if (customRadioButton != null) customRadioButton.selectedProperty().removeListener(customRadioButtonSelectedListener); if (tradeAmountToggleGroup != null) tradeAmountToggleGroup.selectedToggleProperty().removeListener(tradeAmountToggleGroupListener); removePayoutAmountListeners(); } @Override protected void setupKeyHandler(Scene scene) { if (!hideCloseButton) { scene.setOnKeyPressed(e -> { if (e.getCode() == KeyCode.ESCAPE) { e.consume(); doClose(); } }); } } @Override protected void createGridPane() { super.createGridPane(); gridPane.setPadding(new Insets(35, 40, 30, 40)); gridPane.getStyleClass().addAll("grid-pane", "popup-with-input"); gridPane.getColumnConstraints().get(0).setHalignment(HPos.LEFT); gridPane.setPrefWidth(width); } private void addContent() { Contract contract = dispute.getContract(); if (dispute.getDisputeResultProperty().get() == null) disputeResult = new DisputeResult(dispute.getTradeId(), dispute.getTraderId()); else disputeResult = dispute.getDisputeResultProperty().get(); peersDisputeOptional = checkNotNull(getDisputeManager(dispute)).getDisputesAsObservableList().stream() .filter(d -> dispute.getTradeId().equals(d.getTradeId()) && dispute.getTraderId() != d.getTraderId()) .findFirst(); addInfoPane(); addTradeAmountPayoutControls(); addPayoutAmountTextFields(); addReasonControls(); boolean applyPeersDisputeResult = peersDisputeOptional.isPresent() && peersDisputeOptional.get().isClosed() && peersDisputeOptional.get().getDisputeResultProperty().get() != null; if (applyPeersDisputeResult) { // If the other peers dispute has been closed we apply the result to ourselves DisputeResult peersDisputeResult = peersDisputeOptional.get().getDisputeResultProperty().get(); disputeResult.setBuyerPayoutAmountBeforeCost(peersDisputeResult.getBuyerPayoutAmountBeforeCost()); disputeResult.setSellerPayoutAmountBeforeCost(peersDisputeResult.getSellerPayoutAmountBeforeCost()); disputeResult.setWinner(peersDisputeResult.getWinner()); disputeResult.setReason(peersDisputeResult.getReason()); disputeResult.setSummaryNotes(peersDisputeResult.summaryNotesProperty().get()); disputeResult.setSubtractFeeFrom(peersDisputeResult.getSubtractFeeFrom()); disableTradeAmountPayoutControls(); applyTradeAmountRadioButtonStates(); } else if (trade.isPayoutPublished()) { log.info("Payout is already published for {} {}, disabling payout controls", trade.getClass().getSimpleName(), trade.getId()); disableTradeAmountPayoutControls(); } else if (trade.isDepositTxMissing()) { log.warn("Missing deposit tx for {} {}, disabling some payout controls", trade.getClass().getSimpleName(), trade.getId()); disableTradeAmountPayoutControlsWhenDepositMissing(); } setReasonRadioButtonState(); addSummaryNotes(); addButtons(contract); } private void disableTradeAmountPayoutControls() { buyerGetsTradeAmountRadioButton.setDisable(true); buyerGetsAllRadioButton.setDisable(true); sellerGetsTradeAmountRadioButton.setDisable(true); sellerGetsAllRadioButton.setDisable(true); customRadioButton.setDisable(true); buyerPayoutAmountInputTextField.setDisable(true); sellerPayoutAmountInputTextField.setDisable(true); buyerPayoutAmountInputTextField.setEditable(false); sellerPayoutAmountInputTextField.setEditable(false); reasonWasBugRadioButton.setDisable(true); reasonWasUsabilityIssueRadioButton.setDisable(true); reasonProtocolViolationRadioButton.setDisable(true); reasonNoReplyRadioButton.setDisable(true); reasonWasScamRadioButton.setDisable(true); reasonWasOtherRadioButton.setDisable(true); reasonWasBankRadioButton.setDisable(true); reasonWasOptionTradeRadioButton.setDisable(true); reasonWasSellerNotRespondingRadioButton.setDisable(true); reasonWasWrongSenderAccountRadioButton.setDisable(true); reasonWasPeerWasLateRadioButton.setDisable(true); reasonWasTradeAlreadySettledRadioButton.setDisable(true); } private void disableTradeAmountPayoutControlsWhenDepositMissing() { buyerGetsTradeAmountRadioButton.setDisable(true); sellerGetsTradeAmountRadioButton.setDisable(true); } private void addInfoPane() { Contract contract = dispute.getContract(); addTitledGroupBg(gridPane, ++rowIndex, 17, Res.get("disputeSummaryWindow.title")).getStyleClass().add("last"); addConfirmationLabelLabel(gridPane, rowIndex, Res.get("shared.tradeId"), dispute.getShortTradeId(), Layout.TWICE_FIRST_ROW_DISTANCE); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.openDate"), DisplayUtils.formatDateTime(dispute.getOpeningDate())); if (dispute.isDisputeOpenerIsMaker()) { if (dispute.isDisputeOpenerIsBuyer()) role = Res.get("support.buyerMaker"); else role = Res.get("support.sellerMaker"); } else { if (dispute.isDisputeOpenerIsBuyer()) role = Res.get("support.buyerTaker"); else role = Res.get("support.sellerTaker"); } addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.role"), role); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.tradeAmount"), HavenoUtils.formatXmr(contract.getTradeAmount(), true)); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.tradePrice"), FormattingUtils.formatPrice(contract.getPrice())); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.tradeVolume"), VolumeUtil.formatVolumeWithCode(contract.getTradeVolume())); String tradeFee = Res.getWithColAndCap("shared.buyer") + " " + HavenoUtils.formatXmr(trade.getBuyer() == trade.getMaker() ? trade.getMakerFee() : trade.getTakerFee(), true) + " / " + Res.getWithColAndCap("shared.seller") + " " + HavenoUtils.formatXmr(trade.getSeller() == trade.getMaker() ? trade.getMakerFee() : trade.getTakerFee(), true); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.tradeFee"), tradeFee); String securityDeposit = Res.getWithColAndCap("shared.buyer") + " " + HavenoUtils.formatXmr(trade.getBuyer().getSecurityDeposit(), true) + " / " + Res.getWithColAndCap("shared.seller") + " " + HavenoUtils.formatXmr(trade.getSeller().getSecurityDeposit(), true); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), securityDeposit); } private void addTradeAmountPayoutControls() { buyerGetsTradeAmountRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.payout.getsTradeAmount", Res.get("shared.buyer"))); buyerGetsAllRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.payout.getsAll", Res.get("shared.buyer"))); sellerGetsTradeAmountRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.payout.getsTradeAmount", Res.get("shared.seller"))); sellerGetsAllRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.payout.getsAll", Res.get("shared.seller"))); customRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.payout.custom")); VBox radioButtonPane = new VBox(); radioButtonPane.setSpacing(10); radioButtonPane.getChildren().addAll(buyerGetsTradeAmountRadioButton, buyerGetsAllRadioButton, sellerGetsTradeAmountRadioButton, sellerGetsAllRadioButton, customRadioButton); addTopLabelWithVBox(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.payout"), radioButtonPane, 0); tradeAmountToggleGroup = new ToggleGroup(); buyerGetsTradeAmountRadioButton.setToggleGroup(tradeAmountToggleGroup); buyerGetsAllRadioButton.setToggleGroup(tradeAmountToggleGroup); sellerGetsTradeAmountRadioButton.setToggleGroup(tradeAmountToggleGroup); sellerGetsAllRadioButton.setToggleGroup(tradeAmountToggleGroup); customRadioButton.setToggleGroup(tradeAmountToggleGroup); tradeAmountToggleGroupListener = (observable, oldValue, newValue) -> applyPayoutAmounts(newValue); tradeAmountToggleGroup.selectedToggleProperty().addListener(tradeAmountToggleGroupListener); buyerPayoutAmountListener = (observable, oldValue, newValue) -> applyCustomAmounts(buyerPayoutAmountInputTextField, oldValue, newValue); sellerPayoutAmountListener = (observable, oldValue, newValue) -> applyCustomAmounts(sellerPayoutAmountInputTextField, oldValue, newValue); customRadioButtonSelectedListener = (observable, oldValue, newValue) -> { buyerPayoutAmountInputTextField.setEditable(newValue); sellerPayoutAmountInputTextField.setEditable(newValue); if (newValue) { buyerPayoutAmountInputTextField.focusedProperty().addListener(buyerPayoutAmountListener); sellerPayoutAmountInputTextField.focusedProperty().addListener(sellerPayoutAmountListener); } else { removePayoutAmountListeners(); } }; customRadioButton.selectedProperty().addListener(customRadioButtonSelectedListener); } private void removePayoutAmountListeners() { if (buyerPayoutAmountInputTextField != null && buyerPayoutAmountListener != null) buyerPayoutAmountInputTextField.focusedProperty().removeListener(buyerPayoutAmountListener); if (sellerPayoutAmountInputTextField != null && sellerPayoutAmountListener != null) sellerPayoutAmountInputTextField.focusedProperty().removeListener(sellerPayoutAmountListener); } private boolean isPayoutAmountValid() { BigInteger buyerAmount = HavenoUtils.parseXmr(buyerPayoutAmountInputTextField.getText()); BigInteger sellerAmount = HavenoUtils.parseXmr(sellerPayoutAmountInputTextField.getText()); Contract contract = dispute.getContract(); BigInteger tradeAmount = contract.getTradeAmount(); BigInteger expected = tradeAmount .add(trade.getBuyer().getSecurityDeposit()) .add(trade.getSeller().getSecurityDeposit()); BigInteger totalAmount = buyerAmount.add(sellerAmount); return totalAmount.compareTo(expected) == 0 || totalAmount.compareTo(trade.getWalletBalance()) == 0; // allow spending the expected amount or full wallet balance in case a deposit transaction was dropped } private void applyCustomAmounts(InputTextField inputTextField, boolean oldFocusValue, boolean newFocusValue) { // We only apply adjustments at focus out, otherwise we cannot enter certain values if we update at each // keystroke. if (!oldFocusValue || newFocusValue) { return; } BigInteger available = trade.getWalletBalance(); BigInteger enteredAmount = HavenoUtils.parseXmr(inputTextField.getText()); if (enteredAmount.compareTo(available) > 0) { enteredAmount = available; BigInteger finalEnteredAmount = enteredAmount; inputTextField.setText(HavenoUtils.formatXmr(finalEnteredAmount)); } BigInteger counterPart = available.subtract(enteredAmount); String formattedCounterPartAmount = HavenoUtils.formatXmr(counterPart); BigInteger buyerAmount; BigInteger sellerAmount; if (inputTextField == buyerPayoutAmountInputTextField) { buyerAmount = enteredAmount; sellerAmount = counterPart; sellerPayoutAmountInputTextField.setText(formattedCounterPartAmount); } else { sellerAmount = enteredAmount; buyerAmount = counterPart; buyerPayoutAmountInputTextField.setText(formattedCounterPartAmount); } disputeResult.setBuyerPayoutAmountBeforeCost(buyerAmount); disputeResult.setSellerPayoutAmountBeforeCost(sellerAmount); disputeResult.setWinner(buyerAmount.compareTo(sellerAmount) > 0 ? DisputeResult.Winner.BUYER : DisputeResult.Winner.SELLER); // TODO: UI should allow selection of receiver of exact custom amount, otherwise defaulting to bigger receiver. could extend API to specify who pays payout tx fee: buyer, seller, or both disputeResult.setSubtractFeeFrom(buyerAmount.compareTo(sellerAmount) > 0 ? DisputeResult.SubtractFeeFrom.SELLER_ONLY : DisputeResult.SubtractFeeFrom.BUYER_ONLY); } private void addPayoutAmountTextFields() { buyerPayoutAmountInputTextField = new InputTextField(); buyerPayoutAmountInputTextField.setLabelFloat(true); buyerPayoutAmountInputTextField.getStyleClass().add("label-float"); buyerPayoutAmountInputTextField.setEditable(false); buyerPayoutAmountInputTextField.setPromptText(Res.get("disputeSummaryWindow.payoutAmount.buyer")); sellerPayoutAmountInputTextField = new InputTextField(); sellerPayoutAmountInputTextField.setLabelFloat(true); sellerPayoutAmountInputTextField.getStyleClass().add("label-float"); sellerPayoutAmountInputTextField.setPromptText(Res.get("disputeSummaryWindow.payoutAmount.seller")); sellerPayoutAmountInputTextField.setEditable(false); VBox vBox = new VBox(); vBox.setSpacing(15); vBox.getChildren().addAll(buyerPayoutAmountInputTextField, sellerPayoutAmountInputTextField); GridPane.setMargin(vBox, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0)); GridPane.setRowIndex(vBox, rowIndex); GridPane.setColumnIndex(vBox, 1); gridPane.getChildren().add(vBox); } private void addReasonControls() { reasonWasBugRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.BUG.name())); reasonWasUsabilityIssueRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.USABILITY.name())); reasonProtocolViolationRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.PROTOCOL_VIOLATION.name())); reasonNoReplyRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.NO_REPLY.name())); reasonWasScamRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.SCAM.name())); reasonWasBankRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.BANK_PROBLEMS.name())); reasonWasOtherRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.OTHER.name())); reasonWasOptionTradeRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.OPTION_TRADE.name())); reasonWasSellerNotRespondingRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.SELLER_NOT_RESPONDING.name())); reasonWasWrongSenderAccountRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.WRONG_SENDER_ACCOUNT.name())); reasonWasPeerWasLateRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.PEER_WAS_LATE.name())); reasonWasTradeAlreadySettledRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.TRADE_ALREADY_SETTLED.name())); HBox feeRadioButtonPane = new HBox(); feeRadioButtonPane.setSpacing(20); // We don't show no reply and protocol violation as those should be covered by more specific ones. We still leave // the code to enable it if it turns out it is still requested by mediators. feeRadioButtonPane.getChildren().addAll( reasonWasTradeAlreadySettledRadioButton, reasonWasPeerWasLateRadioButton, reasonWasOptionTradeRadioButton, reasonWasSellerNotRespondingRadioButton, reasonWasWrongSenderAccountRadioButton, reasonWasBugRadioButton, reasonWasUsabilityIssueRadioButton, reasonWasBankRadioButton, reasonWasOtherRadioButton ); VBox vBox = addTopLabelWithVBox(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.reason"), feeRadioButtonPane, 10).second; GridPane.setColumnSpan(vBox, 2); reasonToggleGroup = new ToggleGroup(); reasonWasBugRadioButton.setToggleGroup(reasonToggleGroup); reasonWasUsabilityIssueRadioButton.setToggleGroup(reasonToggleGroup); reasonProtocolViolationRadioButton.setToggleGroup(reasonToggleGroup); reasonNoReplyRadioButton.setToggleGroup(reasonToggleGroup); reasonWasScamRadioButton.setToggleGroup(reasonToggleGroup); reasonWasOtherRadioButton.setToggleGroup(reasonToggleGroup); reasonWasBankRadioButton.setToggleGroup(reasonToggleGroup); reasonWasOptionTradeRadioButton.setToggleGroup(reasonToggleGroup); reasonWasSellerNotRespondingRadioButton.setToggleGroup(reasonToggleGroup); reasonWasWrongSenderAccountRadioButton.setToggleGroup(reasonToggleGroup); reasonWasPeerWasLateRadioButton.setToggleGroup(reasonToggleGroup); reasonWasTradeAlreadySettledRadioButton.setToggleGroup(reasonToggleGroup); reasonToggleSelectionListener = (observable, oldValue, newValue) -> { if (newValue == reasonWasBugRadioButton) { disputeResult.setReason(DisputeResult.Reason.BUG); } else if (newValue == reasonWasUsabilityIssueRadioButton) { disputeResult.setReason(DisputeResult.Reason.USABILITY); } else if (newValue == reasonProtocolViolationRadioButton) { disputeResult.setReason(DisputeResult.Reason.PROTOCOL_VIOLATION); } else if (newValue == reasonNoReplyRadioButton) { disputeResult.setReason(DisputeResult.Reason.NO_REPLY); } else if (newValue == reasonWasScamRadioButton) { disputeResult.setReason(DisputeResult.Reason.SCAM); } else if (newValue == reasonWasBankRadioButton) { disputeResult.setReason(DisputeResult.Reason.BANK_PROBLEMS); } else if (newValue == reasonWasOtherRadioButton) { disputeResult.setReason(DisputeResult.Reason.OTHER); } else if (newValue == reasonWasOptionTradeRadioButton) { disputeResult.setReason(DisputeResult.Reason.OPTION_TRADE); } else if (newValue == reasonWasSellerNotRespondingRadioButton) { disputeResult.setReason(DisputeResult.Reason.SELLER_NOT_RESPONDING); } else if (newValue == reasonWasWrongSenderAccountRadioButton) { disputeResult.setReason(DisputeResult.Reason.WRONG_SENDER_ACCOUNT); } else if (newValue == reasonWasTradeAlreadySettledRadioButton) { disputeResult.setReason(DisputeResult.Reason.TRADE_ALREADY_SETTLED); } else if (newValue == reasonWasPeerWasLateRadioButton) { disputeResult.setReason(DisputeResult.Reason.PEER_WAS_LATE); } }; reasonToggleGroup.selectedToggleProperty().addListener(reasonToggleSelectionListener); } private void setReasonRadioButtonState() { if (disputeResult.getReason() != null) { switch (disputeResult.getReason()) { case BUG: reasonToggleGroup.selectToggle(reasonWasBugRadioButton); break; case USABILITY: reasonToggleGroup.selectToggle(reasonWasUsabilityIssueRadioButton); break; case PROTOCOL_VIOLATION: reasonToggleGroup.selectToggle(reasonProtocolViolationRadioButton); break; case NO_REPLY: reasonToggleGroup.selectToggle(reasonNoReplyRadioButton); break; case SCAM: reasonToggleGroup.selectToggle(reasonWasScamRadioButton); break; case BANK_PROBLEMS: reasonToggleGroup.selectToggle(reasonWasBankRadioButton); break; case OTHER: reasonToggleGroup.selectToggle(reasonWasOtherRadioButton); break; case OPTION_TRADE: reasonToggleGroup.selectToggle(reasonWasOptionTradeRadioButton); break; case SELLER_NOT_RESPONDING: reasonToggleGroup.selectToggle(reasonWasSellerNotRespondingRadioButton); break; case WRONG_SENDER_ACCOUNT: reasonToggleGroup.selectToggle(reasonWasWrongSenderAccountRadioButton); break; case PEER_WAS_LATE: reasonToggleGroup.selectToggle(reasonWasPeerWasLateRadioButton); break; case TRADE_ALREADY_SETTLED: reasonToggleGroup.selectToggle(reasonWasTradeAlreadySettledRadioButton); break; } } } private void addSummaryNotes() { summaryNotesTextArea = new HavenoTextArea(); summaryNotesTextArea.setPromptText(Res.get("disputeSummaryWindow.addSummaryNotes")); summaryNotesTextArea.setWrapText(true); Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.summaryNotes"), summaryNotesTextArea, 0); GridPane.setColumnSpan(topLabelWithVBox.second, 2); summaryNotesTextArea.setPrefHeight(50); summaryNotesTextArea.textProperty().bindBidirectional(disputeResult.summaryNotesProperty()); if (isClosedAndPublished()) { summaryNotesTextArea.setEditable(false); summaryNotesTextArea.setDisable(true); } } private void addButtons(Contract contract) { Tuple3 tuple = add2ButtonsWithBox(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.close.button"), Res.get("shared.cancel"), 15, true); Button closeTicketButton = tuple.first; closeTicketButton.disableProperty().bind(Bindings.createBooleanBinding( () -> tradeAmountToggleGroup.getSelectedToggle() == null || summaryNotesTextArea.getText() == null || summaryNotesTextArea.getText().length() == 0 || isClosedAndPublished() || !isPayoutAmountValid(), tradeAmountToggleGroup.selectedToggleProperty(), summaryNotesTextArea.textProperty(), buyerPayoutAmountInputTextField.textProperty(), sellerPayoutAmountInputTextField.textProperty())); Button cancelButton = tuple.second; closeTicketButton.setOnAction(e -> { closeTicketButton.disableProperty().unbind(); closeTicketButton.setDisable(true); if (dispute.getSupportType() == SupportType.ARBITRATION && peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !trade.isPayoutPublished()) { // create payout tx try { MoneroTxWallet payoutTx = trade.createDisputePayoutTx(dispute.getContract(), disputeResult, true); // show confirmation showPayoutTxConfirmation(contract, payoutTx, () -> doClose(closeTicketButton, cancelButton), () -> { closeTicketButton.setDisable(false); cancelButton.setDisable(false); }); } catch (Exception ex) { if (trade.isPayoutPublished()) { doClose(closeTicketButton, cancelButton); } else { log.error("Error creating dispute payout tx for dispute: " + ex.getMessage(), ex); new Popup().error(ex.getMessage()).show(); closeTicketButton.setDisable(false); cancelButton.setDisable(false); } } } else { doClose(closeTicketButton, cancelButton); } }); cancelButton.setOnAction(e -> { hide(); }); } private boolean isClosedAndPublished() { return dispute.isClosed() && trade.isPayoutPublished(); } private void showPayoutTxConfirmation(Contract contract, MoneroTxWallet payoutTx, ResultHandler resultHandler, ResultHandler cancelHandler) { // get buyer and seller destinations (order not preserved) String buyerPayoutAddressString = contract.getBuyerPayoutAddressString(); String sellerPayoutAddressString = contract.getSellerPayoutAddressString(); List destinations = payoutTx.getOutgoingTransfer().getDestinations(); boolean buyerFirst = destinations.get(0).getAddress().equals(buyerPayoutAddressString); BigInteger buyerPayoutAmount = buyerFirst ? destinations.get(0).getAmount() : destinations.size() == 2 ? destinations.get(1).getAmount() : BigInteger.ZERO; BigInteger sellerPayoutAmount = buyerFirst ? (destinations.size() == 2 ? destinations.get(1).getAmount() : BigInteger.ZERO) : destinations.get(0).getAmount(); String buyerDetails = ""; if (buyerPayoutAmount.compareTo(BigInteger.ZERO) > 0) { buyerDetails = Res.get("disputeSummaryWindow.close.txDetails.buyer", HavenoUtils.formatXmr(buyerPayoutAmount, true), buyerPayoutAddressString); } String sellerDetails = ""; if (sellerPayoutAmount.compareTo(BigInteger.ZERO) > 0) { sellerDetails = Res.get("disputeSummaryWindow.close.txDetails.seller", HavenoUtils.formatXmr(sellerPayoutAmount, true), sellerPayoutAddressString); } BigInteger outputAmount = buyerPayoutAmount.add(sellerPayoutAmount).add(payoutTx.getFee()); if (outputAmount.compareTo(BigInteger.ZERO) > 0) { new Popup().width(900) .headLine(Res.get("disputeSummaryWindow.close.txDetails.headline")) .confirmation(Res.get("disputeSummaryWindow.close.txDetails", HavenoUtils.formatXmr(outputAmount, true), buyerDetails, sellerDetails, formatter.formatCoinWithCode(HavenoUtils.atomicUnitsToCoin(payoutTx.getFee())))) .actionButtonText(Res.get("shared.yes")) .onAction(() -> resultHandler.handleResult()) .closeButtonText(Res.get("shared.cancel")) .onClose(() -> cancelHandler.handleResult()) .show(); } else { // No payout will be made new Popup().headLine(Res.get("disputeSummaryWindow.close.noPayout.headline")) .confirmation(Res.get("disputeSummaryWindow.close.noPayout.text")) .actionButtonText(Res.get("shared.yes")) .onAction(resultHandler::handleResult) .closeButtonText(Res.get("shared.cancel")) .onClose(() -> cancelHandler.handleResult()) .show(); } } private void doClose(Button closeTicketButton, Button cancelButton) { cancelButton.setDisable(true); DisputeManager> disputeManager = getDisputeManager(dispute); if (disputeManager == null) { return; } summaryNotesTextArea.textProperty().unbindBidirectional(disputeResult.summaryNotesProperty()); disputeResult.setCloseDate(new Date()); disputesService.closeDisputeTicket(disputeManager, dispute, disputeResult, () -> { if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) { new Popup().attention(Res.get("disputeSummaryWindow.close.closePeer")).show(); } disputeManager.requestPersistence(); closeTicketButton.disableProperty().unbind(); hide(); }, (errMessage, err) -> { log.error("Error closing dispute ticket: " + errMessage + "\n", err); new Popup().error(err.toString()).show(); }); } private DisputeManager> getDisputeManager(Dispute dispute) { return dispute.isMediationDispute() ? mediationManager : arbitrationManager; } /////////////////////////////////////////////////////////////////////////////////////////// // Controller /////////////////////////////////////////////////////////////////////////////////////////// private void applyPayoutAmounts(Toggle selectedTradeAmountToggle) { if (selectedTradeAmountToggle != customRadioButton && selectedTradeAmountToggle != null) { applyPayoutAmountsToDisputeResult(selectedTradeAmountToggle); applyTradeAmountRadioButtonStates(); } } private void applyPayoutAmountsToDisputeResult(Toggle selectedTradeAmountToggle) { if (selectedTradeAmountToggle == buyerGetsTradeAmountRadioButton) { payoutSuggestion = CoreDisputesService.PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT; disputeResult.setWinner(DisputeResult.Winner.BUYER); } else if (selectedTradeAmountToggle == buyerGetsAllRadioButton) { payoutSuggestion = CoreDisputesService.PayoutSuggestion.BUYER_GETS_ALL; disputeResult.setWinner(DisputeResult.Winner.BUYER); } else if (selectedTradeAmountToggle == sellerGetsTradeAmountRadioButton) { payoutSuggestion = CoreDisputesService.PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT; disputeResult.setWinner(DisputeResult.Winner.SELLER); } else if (selectedTradeAmountToggle == sellerGetsAllRadioButton) { payoutSuggestion = CoreDisputesService.PayoutSuggestion.SELLER_GETS_ALL; disputeResult.setWinner(DisputeResult.Winner.SELLER); } else { // should not happen throw new IllegalStateException("Unknown radio button"); } disputesService.applyPayoutAmountsToDisputeResult(payoutSuggestion, dispute, disputeResult, -1); buyerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmountBeforeCost())); sellerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmountBeforeCost())); } private void applyTradeAmountRadioButtonStates() { if (payoutSuggestion == null) { payoutSuggestion = getPayoutSuggestionFromDisputeResult(); } BigInteger buyerPayoutAmount = disputeResult.getBuyerPayoutAmountBeforeCost(); BigInteger sellerPayoutAmount = disputeResult.getSellerPayoutAmountBeforeCost(); buyerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(buyerPayoutAmount)); sellerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(sellerPayoutAmount)); switch (payoutSuggestion) { case BUYER_GETS_TRADE_AMOUNT: buyerGetsTradeAmountRadioButton.setSelected(true); break; case BUYER_GETS_ALL: buyerGetsAllRadioButton.setSelected(true); break; case SELLER_GETS_TRADE_AMOUNT: sellerGetsTradeAmountRadioButton.setSelected(true); break; case SELLER_GETS_ALL: sellerGetsAllRadioButton.setSelected(true); break; case CUSTOM: customRadioButton.setSelected(true); break; } } // TODO: Persist the payout suggestion to DisputeResult like Bisq upstream? // That would be a better design, but it's not currently needed. private PayoutSuggestion getPayoutSuggestionFromDisputeResult() { if (disputeResult.getBuyerPayoutAmountBeforeCost().equals(BigInteger.ZERO)) { return PayoutSuggestion.SELLER_GETS_ALL; } else if (disputeResult.getSellerPayoutAmountBeforeCost().equals(BigInteger.ZERO)) { return PayoutSuggestion.BUYER_GETS_ALL; } else if (disputeResult.getBuyerPayoutAmountBeforeCost().equals(trade.getAmount().add(trade.getBuyer().getSecurityDeposit()))) { return PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT; } else if (disputeResult.getSellerPayoutAmountBeforeCost().equals(trade.getAmount().add(trade.getSeller().getSecurityDeposit()))) { return PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT; } else { return PayoutSuggestion.CUSTOM; } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/EditCustomExplorerWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import haveno.common.util.Tuple2; import haveno.core.locale.Res; import haveno.core.payment.validation.LengthValidator; import haveno.core.user.BlockChainExplorer; import haveno.core.util.validation.UrlInputValidator; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.InputTextField; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.util.Layout; import javafx.collections.FXCollections; import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.geometry.VPos; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.input.MouseEvent; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.util.Callback; import java.util.ArrayList; import static haveno.desktop.util.FormBuilder.addInputTextField; import static haveno.desktop.util.FormBuilder.addMultilineLabel; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import static haveno.desktop.util.FormBuilder.getTopLabelWithVBox; import static javafx.beans.binding.Bindings.createBooleanBinding; public class EditCustomExplorerWindow extends Overlay { private InputTextField nameInputTextField, txUrlInputTextField; private UrlInputValidator urlInputValidator; private BlockChainExplorer currentExplorer; private ListView listView; public EditCustomExplorerWindow(String coin, BlockChainExplorer currentExplorer, ArrayList availableExplorers) { this.currentExplorer = currentExplorer; listView = new ListView<>(); listView.setItems(FXCollections.observableArrayList(availableExplorers)); headLine = coin + " " + Res.get("settings.preferences.editCustomExplorer.headline"); } public BlockChainExplorer getEditedBlockChainExplorer() { return new BlockChainExplorer(nameInputTextField.getText(), txUrlInputTextField.getText()); } @Override public void show() { width = 1000; createGridPane(); addHeadLine(); addContent(); addButtons(); urlInputValidator = new UrlInputValidator(); txUrlInputTextField.setValidator(urlInputValidator); nameInputTextField.setValidator(new LengthValidator(1, 50)); actionButton.disableProperty().bind(createBooleanBinding(() -> { String name = nameInputTextField.getText(); String txUrl = txUrlInputTextField.getText(); // Otherwise we require that input is valid return !nameInputTextField.getValidator().validate(name).isValid || !txUrlInputTextField.getValidator().validate(txUrl).isValid; }, nameInputTextField.textProperty(), txUrlInputTextField.textProperty())); applyStyles(); display(); } @Override protected void createGridPane() { gridPane = new GridPane(); gridPane.setHgap(15); gridPane.setVgap(15); gridPane.setPadding(new Insets(64, 64, 64, 64)); gridPane.setPrefWidth(width); ColumnConstraints columnConstraints1 = new ColumnConstraints(); ColumnConstraints columnConstraints2 = new ColumnConstraints(); columnConstraints1.setPercentWidth(45); columnConstraints2.setPercentWidth(55); gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2); } private void addContent() { Label mlm = addMultilineLabel(gridPane, rowIndex++, Res.get("settings.preferences.editCustomExplorer.description"), 0); GridPane.setColumnSpan(mlm, 2); GridPane.setMargin(mlm, new Insets(40, 0, 0, 0)); Button button = new AutoTooltipButton(">>"); button.setOnAction(e -> { BlockChainExplorer blockChainExplorer = listView.getSelectionModel().getSelectedItem(); if (blockChainExplorer != null) { nameInputTextField.setText(blockChainExplorer.name); txUrlInputTextField.setText(blockChainExplorer.txUrl); } }); button.setStyle("-fx-pref-width: 50px; -fx-pref-height: 30; -fx-padding: 3 3 3 3;"); VBox vBox = new VBox(button); vBox.setAlignment(Pos.CENTER); final Tuple2 topLabelWithVBox = getTopLabelWithVBox(Res.get("settings.preferences.editCustomExplorer.available"), listView); listView.setPrefWidth(300); HBox hBox = new HBox(topLabelWithVBox.second, vBox); hBox.setAlignment(Pos.CENTER_LEFT); hBox.setSpacing(20); hBox.setMaxHeight(200); gridPane.add(hBox, 0, rowIndex); GridPane.setColumnIndex(hBox, 0); GridPane.setValignment(hBox, VPos.TOP); GridPane.setMargin(hBox, new Insets(10, 0, 0, 0)); listView.setCellFactory(new Callback<>() { @Override public ListCell call(ListView list) { ListCell cell = new ListCell<>() { final Label label = new AutoTooltipLabel(); final AnchorPane pane = new AnchorPane(label); @Override public void updateItem(final BlockChainExplorer item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { label.setText(item.name); setGraphic(pane); } else { setGraphic(null); } } }; cell.addEventFilter(MouseEvent.MOUSE_CLICKED, new EventHandler() { @Override public void handle(MouseEvent event) { if (event.getClickCount() == 2) { BlockChainExplorer blockChainExplorer = listView.getSelectionModel().getSelectedItem(); nameInputTextField.setText(blockChainExplorer.name); txUrlInputTextField.setText(blockChainExplorer.txUrl); } } }); return cell; } }); GridPane autoConfirmGridPane = new GridPane(); autoConfirmGridPane.setPrefHeight(150); GridPane.setMargin(autoConfirmGridPane, new Insets(10, 0, 0, 0)); gridPane.add(autoConfirmGridPane, 1, rowIndex); addTitledGroupBg(autoConfirmGridPane, 0, 6, Res.get("settings.preferences.editCustomExplorer.chosen"), 0); int localRowIndex = 0; nameInputTextField = addInputTextField(autoConfirmGridPane, ++localRowIndex, Res.get("settings.preferences.editCustomExplorer.name"), Layout.FIRST_ROW_DISTANCE); nameInputTextField.setPrefWidth(Layout.INITIAL_WINDOW_WIDTH); txUrlInputTextField = addInputTextField(autoConfirmGridPane, ++localRowIndex, Res.get("settings.preferences.editCustomExplorer.txUrl")); nameInputTextField.setText(currentExplorer.name); txUrlInputTextField.setText(currentExplorer.txUrl); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/FilterWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.config.Config; import haveno.core.filter.Filter; import haveno.core.filter.FilterManager; import haveno.core.filter.PaymentAccountFilter; import haveno.core.locale.Res; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.InputTextField; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.main.overlays.popups.Popup; import static haveno.desktop.util.FormBuilder.addInputTextField; import static haveno.desktop.util.FormBuilder.addLabelCheckBox; import static haveno.desktop.util.FormBuilder.addTopLabelInputTextField; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.stream.Collectors; import javafx.collections.FXCollections; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.ScrollPane; import javafx.scene.input.KeyCode; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; import org.apache.commons.lang3.StringUtils; public class FilterWindow extends Overlay { private final FilterManager filterManager; private final boolean useDevPrivilegeKeys; private ScrollPane scrollPane; @Inject public FilterWindow(FilterManager filterManager, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { this.filterManager = filterManager; this.useDevPrivilegeKeys = useDevPrivilegeKeys; type = Type.Attention; } @Override protected Region getRootContainer() { return scrollPane; } public void show() { if (headLine == null) headLine = Res.get("filterWindow.headline"); width = 1000; createGridPane(); scrollPane = new ScrollPane(); scrollPane.setContent(gridPane); scrollPane.setFitToWidth(true); scrollPane.setFitToHeight(true); scrollPane.setMaxHeight(700); scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); addHeadLine(); addContent(); applyStyles(); display(); } @Override protected void setupKeyHandler(Scene scene) { if (!hideCloseButton) { scene.setOnKeyPressed(e -> { if (e.getCode() == KeyCode.ESCAPE) { e.consume(); doClose(); } }); } } private void addContent() { gridPane.getColumnConstraints().remove(1); gridPane.getColumnConstraints().get(0).setHalignment(HPos.LEFT); InputTextField keyTF = addInputTextField(gridPane, ++rowIndex, Res.get("shared.unlock"), 10); if (useDevPrivilegeKeys) { keyTF.setText(DevEnv.DEV_PRIVILEGE_PRIV_KEY); } InputTextField offerIdsTF = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.offers")); InputTextField bannedFromTradingTF = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.onions")).second; InputTextField bannedFromNetworkTF = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.bannedFromNetwork")).second; bannedFromTradingTF.setPromptText("E.g. zqnzx6o3nifef5df.onion:9999"); // Do not translate InputTextField paymentAccountFilterTF = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.accounts")).second; GridPane.setHalignment(paymentAccountFilterTF, HPos.RIGHT); paymentAccountFilterTF.setPromptText("E.g. PERFECT_MONEY|getAccountNr|12345"); // Do not translate InputTextField bannedCurrenciesTF = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.bannedCurrencies")); InputTextField bannedPaymentMethodsTF = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.bannedPaymentMethods")).second; bannedPaymentMethodsTF.setPromptText("E.g. PERFECT_MONEY"); // Do not translate InputTextField bannedAccountWitnessSignerPubKeysTF = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.bannedAccountWitnessSignerPubKeys")).second; bannedAccountWitnessSignerPubKeysTF.setPromptText("E.g. 7f66117aa084e5a2c54fe17d29dd1fee2b241257"); // Do not translate InputTextField arbitratorsTF = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.arbitrators")); InputTextField mediatorsTF = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.mediators")); InputTextField refundAgentsTF = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.refundAgents")); InputTextField xmrFeeReceiverAddressesTF = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.xmrFeeReceiverAddresses")); InputTextField seedNodesTF = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.seedNode")); InputTextField priceRelayNodesTF = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.priceRelayNode")); InputTextField xmrNodesTF = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.xmrNode")); CheckBox preventPublicXmrNetworkCheckBox = addLabelCheckBox(gridPane, ++rowIndex, Res.get("filterWindow.preventPublicXmrNetwork")); CheckBox disableAutoConfCheckBox = addLabelCheckBox(gridPane, ++rowIndex, Res.get("filterWindow.disableAutoConf")); InputTextField disableTradeBelowVersionTF = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.disableTradeBelowVersion")); InputTextField bannedPrivilegedDevPubKeysTF = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.bannedPrivilegedDevPubKeys")).second; InputTextField autoConfExplorersTF = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.autoConfExplorers")).second; CheckBox disableMempoolValidationCheckBox = addLabelCheckBox(gridPane, ++rowIndex, Res.get("filterWindow.disableMempoolValidation")); CheckBox disableApiCheckBox = addLabelCheckBox(gridPane, ++rowIndex, Res.get("filterWindow.disableApi")); Filter filter = filterManager.getDevFilter(); if (filter != null) { setupFieldFromList(offerIdsTF, filter.getBannedOfferIds()); setupFieldFromList(bannedFromTradingTF, filter.getNodeAddressesBannedFromTrading()); setupFieldFromList(bannedFromNetworkTF, filter.getNodeAddressesBannedFromNetwork()); setupFieldFromPaymentAccountFiltersList(paymentAccountFilterTF, filter.getBannedPaymentAccounts()); setupFieldFromList(bannedCurrenciesTF, filter.getBannedCurrencies()); setupFieldFromList(bannedPaymentMethodsTF, filter.getBannedPaymentMethods()); setupFieldFromList(bannedAccountWitnessSignerPubKeysTF, filter.getBannedAccountWitnessSignerPubKeys()); setupFieldFromList(arbitratorsTF, filter.getArbitrators()); setupFieldFromList(mediatorsTF, filter.getMediators()); setupFieldFromList(refundAgentsTF, filter.getRefundAgents()); setupFieldFromList(xmrFeeReceiverAddressesTF, filter.getXmrFeeReceiverAddresses()); setupFieldFromList(seedNodesTF, filter.getSeedNodes()); setupFieldFromList(priceRelayNodesTF, filter.getPriceRelayNodes()); setupFieldFromList(xmrNodesTF, filter.getXmrNodes()); setupFieldFromList(bannedPrivilegedDevPubKeysTF, filter.getBannedPrivilegedDevPubKeys()); setupFieldFromList(autoConfExplorersTF, filter.getBannedAutoConfExplorers()); preventPublicXmrNetworkCheckBox.setSelected(filter.isPreventPublicXmrNetwork()); disableAutoConfCheckBox.setSelected(filter.isDisableAutoConf()); disableTradeBelowVersionTF.setText(filter.getDisableTradeBelowVersion()); disableMempoolValidationCheckBox.setSelected(filter.isDisableMempoolValidation()); disableApiCheckBox.setSelected(filter.isDisableApi()); } Button removeFilterMessageButton = new AutoTooltipButton(Res.get("filterWindow.remove")); removeFilterMessageButton.setDisable(filterManager.getDevFilter() == null); Button sendButton = new AutoTooltipButton(Res.get("filterWindow.add")); sendButton.setOnAction(e -> { String privKeyString = keyTF.getText(); if (filterManager.canAddDevFilter(privKeyString)) { String signerPubKeyAsHex = filterManager.getSignerPubKeyAsHex(privKeyString); Filter newFilter = new Filter( readAsList(offerIdsTF), readAsList(bannedFromTradingTF), readAsPaymentAccountFiltersList(paymentAccountFilterTF), readAsList(bannedCurrenciesTF), readAsList(bannedPaymentMethodsTF), readAsList(arbitratorsTF), readAsList(seedNodesTF), readAsList(priceRelayNodesTF), preventPublicXmrNetworkCheckBox.isSelected(), readAsList(xmrNodesTF), disableTradeBelowVersionTF.getText(), readAsList(mediatorsTF), readAsList(refundAgentsTF), readAsList(bannedAccountWitnessSignerPubKeysTF), readAsList(xmrFeeReceiverAddressesTF), filterManager.getOwnerPubKey(), signerPubKeyAsHex, readAsList(bannedPrivilegedDevPubKeysTF), disableAutoConfCheckBox.isSelected(), readAsList(autoConfExplorersTF), new HashSet<>(readAsList(bannedFromNetworkTF)), disableMempoolValidationCheckBox.isSelected(), disableApiCheckBox.isSelected() ); // We remove first the old filter // We delay a bit with adding as it seems that the instant add/remove calls lead to issues that the // remove msg was rejected (P2P storage should handle it but seems there are edge cases where its not // working as expected) if (filterManager.canRemoveDevFilter(privKeyString)) { filterManager.removeDevFilter(privKeyString); UserThread.runAfter(() -> addDevFilter(removeFilterMessageButton, privKeyString, newFilter), 5); } else { addDevFilter(removeFilterMessageButton, privKeyString, newFilter); } } else { new Popup().warning(Res.get("shared.invalidKey")).onClose(this::blurAgain).show(); } }); removeFilterMessageButton.setOnAction(e -> { String privKeyString = keyTF.getText(); if (filterManager.canRemoveDevFilter(privKeyString)) { filterManager.removeDevFilter(privKeyString); hide(); } else { new Popup().warning(Res.get("shared.invalidKey")).onClose(this::blurAgain).show(); } }); closeButton = new AutoTooltipButton(Res.get("shared.close")); closeButton.setOnAction(e -> { hide(); closeHandlerOptional.ifPresent(Runnable::run); }); HBox hBox = new HBox(); hBox.setSpacing(10); GridPane.setRowIndex(hBox, ++rowIndex); hBox.getChildren().addAll(sendButton, removeFilterMessageButton, closeButton); gridPane.getChildren().add(hBox); GridPane.setMargin(hBox, new Insets(10, 0, 0, 0)); } private void addDevFilter(Button removeFilterMessageButton, String privKeyString, Filter newFilter) { filterManager.addDevFilter(newFilter, privKeyString); removeFilterMessageButton.setDisable(filterManager.getDevFilter() == null); hide(); } private void setupFieldFromList(InputTextField field, Collection values) { if (values != null) field.setText(String.join(", ", values)); } private void setupFieldFromPaymentAccountFiltersList(InputTextField field, List values) { if (values != null) { StringBuilder sb = new StringBuilder(); values.forEach(e -> { if (e != null && e.getPaymentMethodId() != null) { sb .append(e.getPaymentMethodId()) .append("|") .append(e.getGetMethodName()) .append("|") .append(e.getValue()) .append(", "); } }); field.setText(sb.toString()); } } private List readAsList(InputTextField field) { if (field.getText().isEmpty()) { return FXCollections.emptyObservableList(); } else { return Arrays.asList(StringUtils.deleteWhitespace(field.getText()).split(",")); } } private List readAsPaymentAccountFiltersList(InputTextField field) { return readAsList(field) .stream().map(item -> { String[] list = item.split("\\|"); if (list.length == 3) return new PaymentAccountFilter(list[0], list[1], list[2]); else return new PaymentAccountFilter("", "", ""); }) .collect(Collectors.toList()); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/GenericMessageWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.Layout; import javafx.scene.control.Label; import javafx.scene.control.TextArea; import static com.google.common.base.Preconditions.checkNotNull; import static haveno.desktop.util.FormBuilder.addMultilineLabel; import static haveno.desktop.util.FormBuilder.addTextArea; public class GenericMessageWindow extends Overlay { private String preamble; private static final double MAX_TEXT_AREA_HEIGHT = 250; public GenericMessageWindow() { super(); } public void show() { createGridPane(); addHeadLine(); addContent(); addButtons(); applyStyles(); display(); } public GenericMessageWindow preamble(String preamble) { this.preamble = preamble; return this; } private void addContent() { if (preamble != null) { Label label = addMultilineLabel(gridPane, ++rowIndex, preamble, 10); label.setPrefSize(Layout.INITIAL_WINDOW_WIDTH, Layout.INITIAL_WINDOW_HEIGHT * 0.1); } checkNotNull(message, "message must not be null"); TextArea textArea = addTextArea(gridPane, ++rowIndex, "", 10); textArea.getStyleClass().add("flat-text-area-with-border"); textArea.setText(message); textArea.setEditable(false); textArea.setWrapText(true); textArea.setPrefWidth(Layout.INITIAL_WINDOW_WIDTH); GUIUtil.adjustHeightAutomatically(textArea, MAX_TEXT_AREA_HEIGHT); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import com.google.common.base.Joiner; import com.google.inject.Inject; import com.google.inject.name.Named; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXTextField; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIconView; import haveno.common.UserThread; import haveno.common.crypto.KeyRing; import haveno.common.util.Tuple2; import haveno.common.util.Tuple4; import haveno.common.util.Utilities; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import haveno.core.monetary.Price; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OpenOffer; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentMethod; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.user.User; import haveno.core.util.FormattingUtils; import haveno.core.util.VolumeUtil; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.Navigation; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.BusyAnimation; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.main.overlays.editor.PasswordPopup; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.DisplayUtils; import static haveno.desktop.util.FormBuilder.addButtonAfterGroup; import static haveno.desktop.util.FormBuilder.addButtonBusyAnimationLabelAfterGroup; import static haveno.desktop.util.FormBuilder.addConfirmationLabelLabel; import static haveno.desktop.util.FormBuilder.addConfirmationLabelTextArea; import static haveno.desktop.util.FormBuilder.addConfirmationLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addLabel; import static haveno.desktop.util.FormBuilder.addSeparator; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.Layout; import java.math.BigInteger; import java.util.List; import java.util.Optional; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.geometry.VPos; import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.ContentDisplay; import javafx.scene.control.Label; import javafx.scene.control.TextArea; import javafx.scene.control.Tooltip; import javafx.scene.image.ImageView; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class OfferDetailsWindow extends Overlay { protected static final Logger log = LoggerFactory.getLogger(OfferDetailsWindow.class); private final CoinFormatter formatter; private final User user; private final KeyRing keyRing; private final Navigation navigation; private Offer offer; private BigInteger tradeAmount; private Price tradePrice; private Optional placeOfferHandlerOptional = Optional.empty(); private Optional takeOfferHandlerOptional = Optional.empty(); private BusyAnimation busyAnimation; private TradeManager tradeManager; private Subscription numTradesSubscription; private Subscription initProgressSubscription; /////////////////////////////////////////////////////////////////////////////////////////// // Public API /////////////////////////////////////////////////////////////////////////////////////////// @Inject public OfferDetailsWindow(@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, User user, KeyRing keyRing, Navigation navigation, TradeManager tradeManager) { this.formatter = formatter; this.user = user; this.keyRing = keyRing; this.navigation = navigation; this.tradeManager = tradeManager; type = Type.Confirmation; } public void show(Offer offer, BigInteger tradeAmount, Price tradePrice) { this.offer = offer; this.tradeAmount = tradeAmount; this.tradePrice = tradePrice; rowIndex = -1; width = 1050; createGridPane(); addContent(); display(); } public void show(Offer offer) { this.offer = offer; rowIndex = -1; width = 1050; createGridPane(); addContent(); display(); } public OfferDetailsWindow onPlaceOffer(Runnable placeOfferHandler) { this.placeOfferHandlerOptional = Optional.of(placeOfferHandler); return this; } public OfferDetailsWindow onTakeOffer(Runnable takeOfferHandler) { this.takeOfferHandlerOptional = Optional.of(takeOfferHandler); return this; } /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void onHidden() { if (busyAnimation != null) busyAnimation.stop(); if (numTradesSubscription != null) { numTradesSubscription.unsubscribe(); numTradesSubscription = null; } if (initProgressSubscription != null) { initProgressSubscription.unsubscribe(); initProgressSubscription = null; } } @Override protected void createGridPane() { super.createGridPane(); gridPane.setPadding(new Insets(35, 40, 30, 40)); gridPane.getStyleClass().add("grid-pane"); } private void addContent() { gridPane.getColumnConstraints().get(0).setMinWidth(224); int rows = 5; List acceptedBanks = offer.getAcceptedBankIds(); boolean showAcceptedBanks = acceptedBanks != null && !acceptedBanks.isEmpty(); List acceptedCountryCodes = offer.getAcceptedCountryCodes(); boolean showAcceptedCountryCodes = acceptedCountryCodes != null && !acceptedCountryCodes.isEmpty(); boolean isF2F = offer.getPaymentMethod().equals(PaymentMethod.F2F); boolean showOfferExtraInfo = offer.getCombinedExtraInfo() != null && !offer.getCombinedExtraInfo().isEmpty(); if (!takeOfferHandlerOptional.isPresent()) rows++; if (showAcceptedBanks) rows++; if (showAcceptedCountryCodes) rows++; if (showOfferExtraInfo) rows++; if (isF2F) rows++; addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.Offer")); String counterCurrencyDirectionInfo = ""; String xmrDirectionInfo = ""; OfferDirection direction = offer.getDirection(); String currencyCode = offer.getCounterCurrencyCode(); String offerTypeLabel = Res.get("shared.offerType"); String toReceive = " " + Res.get("shared.toReceive"); String toSpend = " " + Res.get("shared.toSpend"); double firstRowDistance = Layout.TWICE_FIRST_ROW_DISTANCE; if (takeOfferHandlerOptional.isPresent()) { addConfirmationLabelLabel(gridPane, rowIndex, offerTypeLabel, DisplayUtils.getDirectionForTakeOffer(direction, currencyCode), firstRowDistance); counterCurrencyDirectionInfo = direction == OfferDirection.BUY ? toReceive : toSpend; xmrDirectionInfo = direction == OfferDirection.SELL ? toReceive : toSpend; } else if (placeOfferHandlerOptional.isPresent()) { addConfirmationLabelLabel(gridPane, rowIndex, offerTypeLabel, DisplayUtils.getOfferDirectionForCreateOffer(direction, currencyCode, offer.isPrivateOffer()), firstRowDistance); counterCurrencyDirectionInfo = direction == OfferDirection.SELL ? toReceive : toSpend; xmrDirectionInfo = direction == OfferDirection.BUY ? toReceive : toSpend; } else { addConfirmationLabelLabel(gridPane, rowIndex, offerTypeLabel, DisplayUtils.getDirectionBothSides(direction, offer.isPrivateOffer()), firstRowDistance); } String amount = Res.get("shared.xmrAmount"); addSeparator(gridPane, ++rowIndex); if (takeOfferHandlerOptional.isPresent()) { addConfirmationLabelLabel(gridPane, ++rowIndex, amount + xmrDirectionInfo, HavenoUtils.formatXmr(tradeAmount, true)); addSeparator(gridPane, ++rowIndex); addConfirmationLabelLabel(gridPane, ++rowIndex, VolumeUtil.formatVolumeLabel(currencyCode) + counterCurrencyDirectionInfo, VolumeUtil.formatVolumeWithCode(offer.getVolumeByAmount(tradeAmount, offer.getMinAmount(), tradeAmount))); } else { addConfirmationLabelLabel(gridPane, ++rowIndex, amount + xmrDirectionInfo, HavenoUtils.formatXmr(offer.getAmount(), true)); addSeparator(gridPane, ++rowIndex); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.minXmrAmount"), HavenoUtils.formatXmr(offer.getMinAmount(), true)); addSeparator(gridPane, ++rowIndex); String volume = VolumeUtil.formatVolumeWithCode(offer.getVolume()); String minVolume = ""; if (offer.getVolume() != null && offer.getMinVolume() != null && !offer.getVolume().equals(offer.getMinVolume())) minVolume = " " + Res.get("offerDetailsWindow.min", VolumeUtil.formatVolumeWithCode(offer.getMinVolume())); addConfirmationLabelLabel(gridPane, ++rowIndex, VolumeUtil.formatVolumeLabel(currencyCode) + counterCurrencyDirectionInfo, volume + minVolume); } String priceLabel = Res.get("shared.price"); addSeparator(gridPane, ++rowIndex); if (takeOfferHandlerOptional.isPresent()) { addConfirmationLabelLabel(gridPane, ++rowIndex, priceLabel, FormattingUtils.formatPrice(tradePrice)); } else { Price price = offer.getPrice(); if (offer.isUseMarketBasedPrice()) { addConfirmationLabelLabel(gridPane, ++rowIndex, priceLabel, FormattingUtils.formatPrice(price) + " " + Res.get("offerDetailsWindow.distance", FormattingUtils.formatPercentagePrice(offer.getMarketPriceMarginPct()))); } else { addConfirmationLabelLabel(gridPane, ++rowIndex, priceLabel, FormattingUtils.formatPrice(price)); } } final PaymentMethod paymentMethod = offer.getPaymentMethod(); String bankId = offer.getBankId(); if (bankId == null || bankId.equals("null")) bankId = ""; else bankId = " (" + bankId + ")"; final boolean isSpecificBanks = paymentMethod.equals(PaymentMethod.SPECIFIC_BANKS); final boolean isNationalBanks = paymentMethod.equals(PaymentMethod.NATIONAL_BANK); final boolean isSepa = paymentMethod.equals(PaymentMethod.SEPA); final String makerPaymentAccountId = offer.getMakerPaymentAccountId(); final PaymentAccount myPaymentAccount = user.getPaymentAccount(makerPaymentAccountId); String countryCode = offer.getCountryCode(); boolean isMyOffer = offer.isMyOffer(keyRing); addSeparator(gridPane, ++rowIndex); if (isMyOffer && makerPaymentAccountId != null && myPaymentAccount != null) { addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.myTradingAccount"), myPaymentAccount.getAccountName()); } else { final String method = Res.get(paymentMethod.getId()); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.paymentMethod"), method); } if (showAcceptedBanks) { if (paymentMethod.equals(PaymentMethod.SAME_BANK)) { addSeparator(gridPane, ++rowIndex); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.bankId"), acceptedBanks.get(0)); } else if (isSpecificBanks) { addSeparator(gridPane, ++rowIndex); String value = Joiner.on(", ").join(acceptedBanks); String acceptedBanksLabel = Res.get("shared.acceptedBanks"); Tooltip tooltip = new Tooltip(acceptedBanksLabel + " " + value); Label acceptedBanksTextField = addConfirmationLabelLabel(gridPane, ++rowIndex, acceptedBanksLabel, value).second; acceptedBanksTextField.setMouseTransparent(false); acceptedBanksTextField.setTooltip(tooltip); } } if (showAcceptedCountryCodes) { addSeparator(gridPane, ++rowIndex); String countries = CountryUtil.getCountriesString(acceptedCountryCodes); Tooltip tooltip = null; if (!CountryUtil.containsAllSepaEuroCountries(acceptedCountryCodes)) { if (acceptedCountryCodes.size() == 1) { tooltip = new Tooltip(countries); } else { tooltip = new Tooltip(CountryUtil.getNamesByCodesString(acceptedCountryCodes)); } } Label acceptedCountries = addConfirmationLabelLabel(gridPane, true, ++rowIndex, Res.get("shared.acceptedTakerCountries"), countries).second; if (tooltip != null) { acceptedCountries.setMouseTransparent(false); acceptedCountries.setTooltip(tooltip); } } if (isF2F) { addSeparator(gridPane, ++rowIndex); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("payment.f2f.city"), offer.getF2FCity()); } if (showOfferExtraInfo) { addSeparator(gridPane, ++rowIndex); TextArea textArea = addConfirmationLabelTextArea(gridPane, ++rowIndex, Res.get("payment.shared.extraInfo"), "", 0).second; textArea.setText(offer.getCombinedExtraInfo().trim()); textArea.setMaxHeight(Layout.DETAILS_WINDOW_EXTRA_INFO_MAX_HEIGHT); textArea.setEditable(false); GUIUtil.adjustHeightAutomatically(textArea, Layout.DETAILS_WINDOW_EXTRA_INFO_MAX_HEIGHT); GridPane.setVgrow(textArea, Priority.SOMETIMES); } // get amount reserved for the offer BigInteger reservedAmount = isMyOffer ? offer.getReservedAmount() : null; // get offer challenge OpenOffer myOpenOffer = HavenoUtils.openOfferManager.getOpenOffer(offer.getId()).orElse(null); String offerChallenge = myOpenOffer == null ? null : myOpenOffer.getChallenge(); rows = 3; if (countryCode != null) rows++; if (!isF2F) rows++; if (reservedAmount != null) rows++; if (offerChallenge != null) rows++; addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.details"), Layout.COMPACT_GROUP_DISTANCE); addConfirmationLabelTextFieldWithCopyIcon(gridPane, rowIndex, Res.get("shared.offerId"), offer.getId(), Layout.TWICE_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE); addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("offerDetailsWindow.makersOnion"), offer.getMakerNodeAddress().getFullAddress()); addSeparator(gridPane, ++rowIndex); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.creationDate"), DisplayUtils.formatDateTime(offer.getDate())); addSeparator(gridPane, ++rowIndex); String value = Res.getWithColAndCap("shared.buyer") + " " + HavenoUtils.formatXmr(takeOfferHandlerOptional.isPresent() ? offer.getOfferPayload().getBuyerSecurityDepositForTradeAmount(tradeAmount) : offer.getOfferPayload().getMaxBuyerSecurityDeposit(), true) + " / " + Res.getWithColAndCap("shared.seller") + " " + HavenoUtils.formatXmr(takeOfferHandlerOptional.isPresent() ? offer.getOfferPayload().getSellerSecurityDepositForTradeAmount(tradeAmount) : offer.getOfferPayload().getMaxSellerSecurityDeposit(), true); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), value); if (reservedAmount != null) { addSeparator(gridPane, ++rowIndex); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.reservedAmount"), HavenoUtils.formatXmr(reservedAmount, true)); } if (countryCode != null && !isF2F) { addSeparator(gridPane, ++rowIndex); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.countryBank"), CountryUtil.getNameAndCode(countryCode)); } if (offerChallenge != null) { addSeparator(gridPane, ++rowIndex); // add label Label label = addLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.challenge"), 0); label.getStyleClass().addAll("confirmation-label", "regular-text-color"); GridPane.setHalignment(label, HPos.LEFT); GridPane.setValignment(label, VPos.TOP); // add vbox with passphrase and copy button VBox vbox = new VBox(13); vbox.setAlignment(Pos.TOP_CENTER); VBox.setVgrow(vbox, Priority.ALWAYS); vbox.getStyleClass().addAll("passphrase-copy-box"); // add passphrase JFXTextField centerLabel = new JFXTextField(offerChallenge); centerLabel.getStyleClass().add("confirmation-value"); centerLabel.setAlignment(Pos.CENTER); centerLabel.setFocusTraversable(false); // add copy button Label copyLabel = new Label(); copyLabel.getStyleClass().addAll("icon"); copyLabel.setTooltip(new Tooltip(Res.get("shared.copyToClipboard"))); MaterialDesignIconView copyIcon = new MaterialDesignIconView(MaterialDesignIcon.CONTENT_COPY, "1.2em"); copyIcon.setFill(Color.WHITE); copyLabel.setGraphic(copyIcon); JFXButton copyButton = new JFXButton(Res.get("offerDetailsWindow.challenge.copy"), copyLabel); copyButton.setContentDisplay(ContentDisplay.LEFT); copyButton.setGraphicTextGap(8); copyButton.setOnMouseClicked(e -> { Utilities.copyToClipboard(offerChallenge); Tooltip tp = new Tooltip(Res.get("shared.copiedToClipboard")); Node node = (Node) e.getSource(); UserThread.runAfter(() -> tp.hide(), 1); tp.show(node, e.getScreenX() + Layout.PADDING, e.getScreenY() + Layout.PADDING); }); copyButton.setId("copy-button-thin"); copyButton.setFocusTraversable(false); vbox.getChildren().addAll(centerLabel, copyButton); // add vbox to grid pane in next column GridPane.setRowIndex(vbox, rowIndex); GridPane.setColumnIndex(vbox, 1); gridPane.getChildren().add(vbox); } if (placeOfferHandlerOptional.isPresent()) { addTitledGroupBg(gridPane, ++rowIndex, 1, Res.get("offerDetailsWindow.commitment"), Layout.COMPACT_GROUP_DISTANCE); final Tuple2 labelLabelTuple2 = addConfirmationLabelLabel(gridPane, rowIndex, Res.get("offerDetailsWindow.agree"), Res.get("createOffer.tac"), Layout.TWICE_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE); labelLabelTuple2.second.setWrapText(true); labelLabelTuple2.second.setMinHeight(Region.USE_PREF_SIZE); GridPane.setVgrow(labelLabelTuple2.second, Priority.ALWAYS); addConfirmAndCancelButtons(true); } else if (takeOfferHandlerOptional.isPresent()) { addTitledGroupBg(gridPane, ++rowIndex, 1, Res.get("shared.contract"), Layout.COMPACT_GROUP_DISTANCE); final Tuple2 labelLabelTuple2 = addConfirmationLabelLabel(gridPane, rowIndex, Res.get("offerDetailsWindow.tac"), Res.get("takeOffer.tac"), Layout.TWICE_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE); labelLabelTuple2.second.setWrapText(true); addConfirmAndCancelButtons(false); } else { Button closeButton = addButtonAfterGroup(gridPane, ++rowIndex, Res.get("shared.close")); GridPane.setColumnIndex(closeButton, 1); GridPane.setHalignment(closeButton, HPos.RIGHT); closeButton.setOnAction(e -> { closeHandlerOptional.ifPresent(Runnable::run); hide(); }); } } private void addConfirmAndCancelButtons(boolean isPlaceOffer) { boolean isBuyOffer = offer.isBuyOffer(); boolean isBuyerRole = isPlaceOffer == isBuyOffer; String placeOfferButtonText = isBuyerRole ? Res.get("offerDetailsWindow.confirm.maker.buy", offer.getCounterCurrencyCode()) : Res.get("offerDetailsWindow.confirm.maker.sell", offer.getCounterCurrencyCode()); String takeOfferButtonText = isBuyerRole ? Res.get("offerDetailsWindow.confirm.taker.buy", offer.getCounterCurrencyCode()) : Res.get("offerDetailsWindow.confirm.taker.sell", offer.getCounterCurrencyCode()); Tuple4 placeOfferTuple = addButtonBusyAnimationLabelAfterGroup(gridPane, ++rowIndex, 1, isPlaceOffer ? placeOfferButtonText : takeOfferButtonText); AutoTooltipButton confirmButton = (AutoTooltipButton) placeOfferTuple.first; confirmButton.setMinHeight(40); confirmButton.setPadding(new Insets(0, 20, 0, 20)); confirmButton.setGraphicTextGap(10); confirmButton.setId(isBuyerRole ? "buy-button-big" : "sell-button-big"); confirmButton.updateText(isPlaceOffer ? placeOfferButtonText : takeOfferButtonText); if (offer.hasBuyerAsTakerWithoutDeposit()) { confirmButton.setGraphic(GUIUtil.getLockLabel()); } else { ImageView iconView = new ImageView(); iconView.setId(isBuyerRole ? "image-buy-white" : "image-sell-white"); confirmButton.setGraphic(iconView); } busyAnimation = placeOfferTuple.second; Label spinnerInfoLabel = placeOfferTuple.third; Button cancelButton = new AutoTooltipButton(Res.get("shared.cancel")); cancelButton.setDefaultButton(false); cancelButton.setOnAction(e -> { closeHandlerOptional.ifPresent(Runnable::run); hide(); }); placeOfferTuple.fourth.getChildren().add(cancelButton); confirmButton.setOnAction(e -> { if (GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation)) { if (!isPlaceOffer && offer.isPrivateOffer()) { new PasswordPopup() .headLine(Res.get("offerbook.takeOffer.enterChallenge")) .onAction(password -> { if (offer.getChallengeHash().equals(HavenoUtils.getChallengeHash(password))) { offer.setChallenge(password); confirmTakeOfferAux(confirmButton, cancelButton, spinnerInfoLabel, isPlaceOffer); } else { new Popup().warning(Res.get("password.wrongPw")).show(); } }) .closeButtonText(Res.get("shared.cancel")) .show(); } else { confirmTakeOfferAux(confirmButton, cancelButton, spinnerInfoLabel, isPlaceOffer); } } }); } private void confirmTakeOfferAux(Button button, Button cancelButton, Label spinnerInfoLabel, boolean isPlaceOffer) { button.setDisable(true); cancelButton.setDisable(isPlaceOffer ? false : true); // TODO: enable cancel button for taking an offer until messages sent // temporarily disabled due to high CPU usage (per issue #4649) // busyAnimation.play(); if (isPlaceOffer) { spinnerInfoLabel.setText(Res.get("createOffer.fundsBox.placeOfferSpinnerInfo")); placeOfferHandlerOptional.ifPresent(Runnable::run); } else { // subscribe to trade progress spinnerInfoLabel.setText(Res.get("takeOffer.fundsBox.takeOfferSpinnerInfo", "0%")); numTradesSubscription = EasyBind.subscribe(tradeManager.getNumPendingTrades(), newNum -> { subscribeToProgress(spinnerInfoLabel); }); takeOfferHandlerOptional.ifPresent(Runnable::run); } } private void subscribeToProgress(Label spinnerInfoLabel) { Trade trade = tradeManager.getTrade(offer.getId()); if (trade == null || initProgressSubscription != null) return; initProgressSubscription = EasyBind.subscribe(trade.initProgressProperty(), newProgress -> { String progress = (int) (newProgress.doubleValue() * 100.0) + "%"; UserThread.execute(() -> spinnerInfoLabel.setText(Res.get("takeOffer.fundsBox.takeOfferSpinnerInfo", progress))); }); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/QRCodeWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import haveno.common.util.Tuple2; import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.util.GUIUtil; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.scene.control.Label; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; import net.glxn.qrgen.QRCode; import net.glxn.qrgen.image.ImageType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.net.URI; public class QRCodeWindow extends Overlay { private static final Logger log = LoggerFactory.getLogger(QRCodeWindow.class); private final StackPane qrCodePane; private final String moneroUri; public QRCodeWindow(String moneroUri) { this.moneroUri = moneroUri; Tuple2 qrCodeTuple = GUIUtil.getBigXmrQrCodePane(); qrCodePane = qrCodeTuple.first; ImageView qrCodeImageView = qrCodeTuple.second; final byte[] imageBytes = QRCode .from(moneroUri) .withSize(300, 300) .to(ImageType.PNG) .stream() .toByteArray(); Image qrImage = new Image(new ByteArrayInputStream(imageBytes)); qrCodeImageView.setImage(qrImage); type = Type.Information; width = 468; headLine(Res.get("qRCodeWindow.headline")); message(Res.get("qRCodeWindow.msg")); } @Override public void show() { createGridPane(); addHeadLine(); addMessage(); qrCodePane.setOnMouseClicked(event -> openWallet()); GridPane.setRowIndex(qrCodePane, ++rowIndex); GridPane.setColumnSpan(qrCodePane, 2); GridPane.setHalignment(qrCodePane, HPos.CENTER); gridPane.getChildren().add(qrCodePane); String request = moneroUri.replace("%20", " ").replace("?", "\n?").replace("&", "\n&"); Label infoLabel = new AutoTooltipLabel(Res.get("qRCodeWindow.request", request)); infoLabel.setMouseTransparent(true); infoLabel.setWrapText(true); infoLabel.setId("popup-qr-code-info"); GridPane.setHalignment(infoLabel, HPos.CENTER); GridPane.setHgrow(infoLabel, Priority.ALWAYS); GridPane.setMargin(infoLabel, new Insets(3, 0, 0, 0)); GridPane.setRowIndex(infoLabel, ++rowIndex); GridPane.setColumnIndex(infoLabel, 0); GridPane.setColumnSpan(infoLabel, 2); gridPane.getChildren().add(infoLabel); addButtons(); applyStyles(); display(); } public String getClipboardText() { return moneroUri; } private void openWallet() { try { Utilities.openURI(URI.create(moneroUri)); } catch (Exception e) { log.warn(e.getMessage()); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/SelectDepositTxWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import com.google.inject.Inject; import haveno.core.locale.Res; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.util.FormBuilder; import static haveno.desktop.util.FormBuilder.addMultilineLabel; import java.util.List; import java.util.Optional; import java.util.function.Consumer; import javafx.collections.FXCollections; import javafx.geometry.Insets; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.layout.GridPane; import javafx.util.StringConverter; import org.bitcoinj.core.Transaction; //TODO might be removed, but leave it for now until sure we will not use it anymore. public class SelectDepositTxWindow extends Overlay { private ComboBox transactionsComboBox; private List transactions; private Optional> selectHandlerOptional; /////////////////////////////////////////////////////////////////////////////////////////// // Public API /////////////////////////////////////////////////////////////////////////////////////////// @Inject public SelectDepositTxWindow() { type = Type.Attention; } public void show() { if (headLine == null) headLine = Res.get("selectDepositTxWindow.headline"); width = 768; createGridPane(); addHeadLine(); addContent(); addButtons(); applyStyles(); display(); } public SelectDepositTxWindow onSelect(Consumer selectHandler) { this.selectHandlerOptional = Optional.of(selectHandler); return this; } public SelectDepositTxWindow transactions(List transactions) { this.transactions = transactions; return this; } /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// private void addContent() { Label label = addMultilineLabel(gridPane, ++rowIndex, Res.get("selectDepositTxWindow.msg"), 10); GridPane.setMargin(label, new Insets(0, 0, 10, 0)); transactionsComboBox = FormBuilder.addComboBox(gridPane, ++rowIndex, Res.get("selectDepositTxWindow.select")); ; transactionsComboBox.setConverter(new StringConverter<>() { @Override public String toString(Transaction transaction) { return transaction.getTxId().toString(); } @Override public Transaction fromString(String string) { return null; } }); transactionsComboBox.setItems(FXCollections.observableArrayList(transactions)); transactionsComboBox.setOnAction(event -> { if (selectHandlerOptional.isPresent()) { selectHandlerOptional.get().accept(transactionsComboBox.getSelectionModel().getSelectedItem()); } hide(); }); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/SendAlertMessageWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.app.DevEnv; import haveno.common.config.Config; import haveno.common.util.Tuple2; import haveno.core.alert.Alert; import haveno.core.alert.AlertManager; import haveno.core.locale.Res; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.InputTextField; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.FormBuilder; import static haveno.desktop.util.FormBuilder.addInputTextField; import static haveno.desktop.util.FormBuilder.addLabelCheckBox; import static haveno.desktop.util.FormBuilder.addRadioButton; import static haveno.desktop.util.FormBuilder.addTopLabelTextArea; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.Label; import javafx.scene.control.RadioButton; import javafx.scene.control.TextArea; import javafx.scene.control.ToggleGroup; import javafx.scene.input.KeyCode; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; public class SendAlertMessageWindow extends Overlay { private final AlertManager alertManager; private final boolean useDevPrivilegeKeys; @Inject public SendAlertMessageWindow(AlertManager alertManager, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { this.alertManager = alertManager; this.useDevPrivilegeKeys = useDevPrivilegeKeys; type = Type.Attention; } public void show() { if (headLine == null) headLine = Res.get("sendAlertMessageWindow.headline"); width = 968; createGridPane(); addHeadLine(); addContent(); applyStyles(); display(); } /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void setupKeyHandler(Scene scene) { if (!hideCloseButton) { scene.setOnKeyPressed(e -> { if (e.getCode() == KeyCode.ESCAPE) { e.consume(); doClose(); } }); } } private void addContent() { gridPane.getColumnConstraints().get(0).setHalignment(HPos.LEFT); gridPane.getColumnConstraints().remove(1); InputTextField keyInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("shared.unlock"), 10); if (useDevPrivilegeKeys) keyInputTextField.setText(DevEnv.DEV_PRIVILEGE_PRIV_KEY); Tuple2 labelTextAreaTuple2 = addTopLabelTextArea(gridPane, ++rowIndex, Res.get("sendAlertMessageWindow.alertMsg"), Res.get("sendAlertMessageWindow.enterMsg")); TextArea alertMessageTextArea = labelTextAreaTuple2.second; Label first = labelTextAreaTuple2.first; first.setMinWidth(150); CheckBox isSoftwareUpdateCheckBox = addLabelCheckBox(gridPane, ++rowIndex, Res.get("sendAlertMessageWindow.isSoftwareUpdate")); HBox hBoxRelease = new HBox(); hBoxRelease.setSpacing(10); GridPane.setRowIndex(hBoxRelease, ++rowIndex); ToggleGroup toggleGroup = new ToggleGroup(); RadioButton isUpdateCheckBox = addRadioButton(gridPane, rowIndex, toggleGroup, Res.get("sendAlertMessageWindow.isUpdate")); RadioButton isPreReleaseCheckBox = addRadioButton(gridPane, rowIndex, toggleGroup, Res.get("sendAlertMessageWindow.isPreRelease")); hBoxRelease.getChildren().addAll(new Label(""), isUpdateCheckBox, isPreReleaseCheckBox); gridPane.getChildren().add(hBoxRelease); isSoftwareUpdateCheckBox.setSelected(true); isUpdateCheckBox.setSelected(true); InputTextField versionInputTextField = FormBuilder.addInputTextField(gridPane, ++rowIndex, Res.get("sendAlertMessageWindow.version")); versionInputTextField.disableProperty().bind(isSoftwareUpdateCheckBox.selectedProperty().not()); isUpdateCheckBox.disableProperty().bind(isSoftwareUpdateCheckBox.selectedProperty().not()); isPreReleaseCheckBox.disableProperty().bind(isSoftwareUpdateCheckBox.selectedProperty().not()); Button sendButton = new AutoTooltipButton(Res.get("sendAlertMessageWindow.send")); sendButton.getStyleClass().add("action-button"); sendButton.setDefaultButton(true); sendButton.setOnAction(e -> { final String version = versionInputTextField.getText(); boolean versionOK = false; final boolean isUpdate = (isSoftwareUpdateCheckBox.isSelected() && isUpdateCheckBox.isSelected()); final boolean isPreRelease = (isSoftwareUpdateCheckBox.isSelected() && isPreReleaseCheckBox.isSelected()); if (isUpdate || isPreRelease) { final String[] split = version.split("\\."); versionOK = split.length == 3; if (!versionOK) // Do not translate as only used by devs new Popup().warning("Version number must be in semantic version format (contain 2 '.'). version=" + version) .onClose(this::blurAgain) .show(); } if (!isSoftwareUpdateCheckBox.isSelected() || versionOK) { if (alertMessageTextArea.getText().length() > 0 && keyInputTextField.getText().length() > 0) { if (alertManager.addAlertMessageIfKeyIsValid( new Alert(alertMessageTextArea.getText(), isUpdate, isPreRelease, version), keyInputTextField.getText()) ) hide(); else new Popup().warning(Res.get("shared.invalidKey")).width(300).onClose(this::blurAgain).show(); } } }); Button removeAlertMessageButton = new AutoTooltipButton(Res.get("sendAlertMessageWindow.remove")); removeAlertMessageButton.setOnAction(e -> { if (keyInputTextField.getText().length() > 0) { if (alertManager.removeAlertMessageIfKeyIsValid(keyInputTextField.getText())) hide(); else new Popup().warning(Res.get("shared.invalidKey")).width(300).onClose(this::blurAgain).show(); } }); closeButton = new AutoTooltipButton(Res.get("shared.close")); closeButton.setOnAction(e -> { hide(); closeHandlerOptional.ifPresent(Runnable::run); }); HBox hBox = new HBox(); hBox.setSpacing(10); GridPane.setRowIndex(hBox, ++rowIndex); hBox.getChildren().addAll(sendButton, removeAlertMessageButton, closeButton); gridPane.getChildren().add(hBox); GridPane.setMargin(hBox, new Insets(10, 0, 0, 0)); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/SendLogFilesWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.main.portfolio.pendingtrades.steps.TradeWizardItem; import haveno.desktop.main.portfolio.pendingtrades.steps.buyer.BuyerStep1View; import haveno.desktop.main.portfolio.pendingtrades.steps.buyer.BuyerStep2View; import haveno.desktop.main.portfolio.pendingtrades.steps.buyer.BuyerStep3View; import haveno.desktop.util.Layout; import haveno.core.locale.Res; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.mediation.FileTransferSender; import haveno.core.support.dispute.mediation.FileTransferSession; import haveno.common.UserThread; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ProgressBar; import javafx.scene.control.Separator; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Orientation; import javafx.geometry.Pos; import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; import java.io.IOException; import lombok.extern.slf4j.Slf4j; import static haveno.desktop.util.FormBuilder.addMultilineLabel; @Slf4j public class SendLogFilesWindow extends Overlay implements FileTransferSession.FtpCallback { private final String tradeId; private final int traderId; private final ArbitrationManager arbitrationManager; private Label statusLabel; private Button sendButton, stopButton; private final DoubleProperty ftpProgress = new SimpleDoubleProperty(-1); TradeWizardItem step1, step2, step3; private FileTransferSender fileTransferSender; public SendLogFilesWindow(String tradeId, int traderId, ArbitrationManager arbitrationManager) { this.tradeId = tradeId; this.traderId = traderId; this.arbitrationManager = arbitrationManager; type = Type.Attention; } public void show() { headLine = Res.get("support.sendLogs.title"); width = 668; createGridPane(); addHeadLine(); addContent(); addButtons(); applyStyles(); display(); } @Override protected void createGridPane() { gridPane = new GridPane(); gridPane.setHgap(5); gridPane.setVgap(5); gridPane.setPadding(new Insets(64, 64, 64, 64)); gridPane.setPrefWidth(width); } void addWizardsToGridPane(TradeWizardItem tradeWizardItem) { GridPane.setRowIndex(tradeWizardItem, rowIndex++); GridPane.setColumnIndex(tradeWizardItem, 0); GridPane.setHalignment(tradeWizardItem, HPos.LEFT); gridPane.getChildren().add(tradeWizardItem); } void addLineSeparatorToGridPane() { final Separator separator = new Separator(Orientation.VERTICAL); separator.setMinHeight(22); GridPane.setMargin(separator, new Insets(0, 0, 0, 13)); GridPane.setHalignment(separator, HPos.LEFT); GridPane.setRowIndex(separator, rowIndex++); gridPane.getChildren().add(separator); } void addRegionToGridPane() { final Region region = new Region(); region.setMinHeight(22); GridPane.setMargin(region, new Insets(0, 0, 0, 13)); GridPane.setRowIndex(region, rowIndex++); gridPane.getChildren().add(region); } private void addContent() { this.hideCloseButton = true; addMultilineLabel(gridPane, ++rowIndex, Res.get("support.sendLogs.backgroundInfo"), 0); addRegionToGridPane(); step1 = new TradeWizardItem(BuyerStep1View.class, Res.get("support.sendLogs.step1"), "1"); step2 = new TradeWizardItem(BuyerStep2View.class, Res.get("support.sendLogs.step2"), "2"); step3 = new TradeWizardItem(BuyerStep3View.class, Res.get("support.sendLogs.step3"), "3"); addRegionToGridPane(); addRegionToGridPane(); addWizardsToGridPane(step1); addLineSeparatorToGridPane(); addWizardsToGridPane(step2); addLineSeparatorToGridPane(); addWizardsToGridPane(step3); addRegionToGridPane(); ProgressBar progressBar = new ProgressBar(); progressBar.setMinHeight(19); progressBar.setMaxHeight(19); progressBar.setPrefWidth(9305); progressBar.setVisible(false); progressBar.progressProperty().bind(ftpProgress); gridPane.add(progressBar, 0, ++rowIndex); statusLabel = addMultilineLabel(gridPane, ++rowIndex, "", -Layout.FLOATING_LABEL_DISTANCE); statusLabel.getStyleClass().add("sub-info"); addRegionToGridPane(); sendButton = new AutoTooltipButton(Res.get("support.sendLogs.send")); stopButton = new AutoTooltipButton(Res.get("support.sendLogs.cancel")); stopButton.setDisable(true); closeButton = new AutoTooltipButton(Res.get("shared.close")); sendButton.setOnAction(e -> { try { progressBar.setVisible(true); if (fileTransferSender == null) { setActiveStep(1); statusLabel.setText(Res.get("support.sendLogs.init")); fileTransferSender = arbitrationManager.initLogUpload(this, tradeId, traderId); UserThread.runAfter(() -> { fileTransferSender.createZipFileToSend(); setActiveStep(2); UserThread.runAfter(() -> { setActiveStep(3); try { fileTransferSender.initSend(); } catch (IOException ioe) { log.error(ioe.toString()); statusLabel.setText(ioe.toString()); ioe.printStackTrace(); } }, 1); }, 1); sendButton.setDisable(true); stopButton.setDisable(false); } else { // resend the latest block in the event of a timeout statusLabel.setText(Res.get("support.sendLogs.retry")); fileTransferSender.retrySend(); sendButton.setDisable(true); } } catch (IOException ex) { log.error(ex.toString()); statusLabel.setText(ex.toString()); ex.printStackTrace(); } }); stopButton.setOnAction(e -> { if (fileTransferSender != null) { fileTransferSender.resetSession(); statusLabel.setText(Res.get("support.sendLogs.stopped")); stopButton.setDisable(true); } }); closeButton.setOnAction(e -> { hide(); closeHandlerOptional.ifPresent(Runnable::run); }); HBox hBox = new HBox(); hBox.setSpacing(10); hBox.setAlignment(Pos.CENTER_RIGHT); GridPane.setRowIndex(hBox, ++rowIndex); GridPane.setColumnSpan(hBox, 2); GridPane.setColumnIndex(hBox, 0); hBox.getChildren().addAll(sendButton, stopButton, closeButton); gridPane.getChildren().add(hBox); GridPane.setMargin(hBox, new Insets(10, 0, 0, 0)); } void setActiveStep(int step) { if (step < 1) { step1.setDisabled(); step2.setDisabled(); step3.setDisabled(); } else if (step == 1) { step1.setActive(); } else if (step == 2) { step1.setCompleted(); step2.setActive(); } else if (step == 3) { step2.setCompleted(); step3.setActive(); } else { step3.setCompleted(); } } @Override public void onFtpProgress(double progressPct) { UserThread.execute(() -> { if (progressPct > 0.0) { statusLabel.setText(String.format(Res.get("support.sendLogs.progress"), progressPct * 100)); sendButton.setDisable(true); } ftpProgress.set(progressPct); }); } @Override public void onFtpComplete(FileTransferSession session) { UserThread.execute(() -> { setActiveStep(4); // all finished statusLabel.setText(Res.get("support.sendLogs.finished")); stopButton.setDisable(true); }); } @Override public void onFtpTimeout(String statusMsg, FileTransferSession session) { UserThread.execute(() -> { statusLabel.setText(statusMsg + "\r\n" + Res.get("support.sendLogs.command")); sendButton.setDisable(false); }); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/SendPrivateNotificationWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.crypto.PubKeyRing; import haveno.common.util.Tuple2; import haveno.core.alert.PrivateNotificationManager; import haveno.core.alert.PrivateNotificationPayload; import haveno.core.locale.Res; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.InputTextField; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.main.overlays.popups.Popup; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.SendMailboxMessageListener; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TextArea; import javafx.scene.input.KeyCode; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.TimeUnit; import static haveno.desktop.util.FormBuilder.addInputTextField; import static haveno.desktop.util.FormBuilder.addTopLabelTextArea; public class SendPrivateNotificationWindow extends Overlay { private static final Logger log = LoggerFactory.getLogger(SendPrivateNotificationWindow.class); private final PrivateNotificationManager privateNotificationManager; private final PubKeyRing pubKeyRing; private final NodeAddress nodeAddress; private final boolean useDevPrivilegeKeys; public SendPrivateNotificationWindow(PrivateNotificationManager privateNotificationManager, PubKeyRing pubKeyRing, NodeAddress nodeAddress, boolean useDevPrivilegeKeys) { this.privateNotificationManager = privateNotificationManager; this.pubKeyRing = pubKeyRing; this.nodeAddress = nodeAddress; this.useDevPrivilegeKeys = useDevPrivilegeKeys; type = Type.Attention; } public void show() { if (headLine == null) headLine = Res.get("sendPrivateNotificationWindow.headline"); width = 868; createGridPane(); addHeadLine(); addContent(); applyStyles(); display(); } @Override protected void setupKeyHandler(Scene scene) { if (!hideCloseButton) { scene.setOnKeyPressed(e -> { if (e.getCode() == KeyCode.ESCAPE) { e.consume(); doClose(); } }); } } private void addContent() { InputTextField keyInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("shared.unlock"), 10); if (useDevPrivilegeKeys) keyInputTextField.setText(DevEnv.DEV_PRIVILEGE_PRIV_KEY); Tuple2 labelTextAreaTuple2 = addTopLabelTextArea(gridPane, ++rowIndex, Res.get("sendPrivateNotificationWindow.privateNotification"), Res.get("sendPrivateNotificationWindow.enterNotification")); TextArea alertMessageTextArea = labelTextAreaTuple2.second; Label first = labelTextAreaTuple2.first; first.setMinWidth(200); Button sendButton = new AutoTooltipButton(Res.get("sendPrivateNotificationWindow.send")); sendButton.setOnAction(e -> { if (alertMessageTextArea.getText().length() > 0 && keyInputTextField.getText().length() > 0) { PrivateNotificationPayload privateNotification = new PrivateNotificationPayload(alertMessageTextArea.getText()); boolean wasKeyValid = privateNotificationManager.sendPrivateNotificationMessageIfKeyIsValid( privateNotification, pubKeyRing, nodeAddress, keyInputTextField.getText(), new SendMailboxMessageListener() { @Override public void onArrived() { log.info("PrivateNotificationPayload arrived at peer {}.", nodeAddress); UserThread.runAfter(() -> new Popup().feedback(Res.get("shared.messageArrived")) .onClose(SendPrivateNotificationWindow.this::hide) .show(), 100, TimeUnit.MILLISECONDS); } @Override public void onStoredInMailbox() { log.info("PrivateNotificationPayload stored in mailbox for peer {}.", nodeAddress); UserThread.runAfter(() -> new Popup().feedback(Res.get("shared.messageStoredInMailbox")) .onClose(SendPrivateNotificationWindow.this::hide) .show(), 100, TimeUnit.MILLISECONDS); } @Override public void onFault(String errorMessage) { log.error("PrivateNotificationPayload failed: Peer {}, errorMessage={}", nodeAddress, errorMessage); UserThread.runAfter(() -> new Popup().feedback(Res.get("shared.messageSendingFailed", errorMessage)) .onClose(SendPrivateNotificationWindow.this::hide) .show(), 100, TimeUnit.MILLISECONDS); } }); if (wasKeyValid) { doClose(); } else { UserThread.runAfter(() -> new Popup().warning(Res.get("shared.invalidKey")) .width(300) .onClose(this::blurAgain) .show(), 100, TimeUnit.MILLISECONDS); } } }); closeButton = new AutoTooltipButton(Res.get("shared.close")); closeButton.setOnAction(e -> { hide(); closeHandlerOptional.ifPresent(Runnable::run); }); HBox hBox = new HBox(); hBox.setSpacing(10); hBox.setAlignment(Pos.CENTER_RIGHT); GridPane.setRowIndex(hBox, ++rowIndex); GridPane.setColumnSpan(hBox, 2); GridPane.setColumnIndex(hBox, 0); hBox.getChildren().addAll(sendButton, closeButton); gridPane.getChildren().add(hBox); GridPane.setMargin(hBox, new Insets(10, 0, 0, 0)); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/ShowWalletDataWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import haveno.common.util.Tuple2; import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.core.xmr.wallet.WalletsManager; import haveno.desktop.main.overlays.Overlay; import javafx.geometry.HPos; import javafx.scene.Scene; import javafx.scene.control.CheckBox; import javafx.scene.control.Label; import javafx.scene.control.TextArea; import javafx.scene.input.KeyCode; import javafx.scene.layout.Priority; import static haveno.desktop.util.FormBuilder.addLabelCheckBox; import static haveno.desktop.util.FormBuilder.addTopLabelTextArea; public class ShowWalletDataWindow extends Overlay { private final WalletsManager walletsManager; /////////////////////////////////////////////////////////////////////////////////////////// // Public API /////////////////////////////////////////////////////////////////////////////////////////// public ShowWalletDataWindow(WalletsManager walletsManager) { this.walletsManager = walletsManager; type = Type.Attention; } public void show() { if (headLine == null) headLine = Res.get("showWalletDataWindow.walletData"); width = 1000; createGridPane(); addHeadLine(); addContent(); addButtons(); applyStyles(); display(); } /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void setupKeyHandler(Scene scene) { if (!hideCloseButton) { scene.setOnKeyPressed(e -> { if (e.getCode() == KeyCode.ESCAPE) { e.consume(); doClose(); } }); } } private void addContent() { gridPane.getColumnConstraints().get(0).setHalignment(HPos.LEFT); gridPane.getColumnConstraints().get(0).setHgrow(Priority.ALWAYS); gridPane.getColumnConstraints().get(1).setHgrow(Priority.SOMETIMES); Tuple2 labelTextAreaTuple2 = addTopLabelTextArea(gridPane, ++rowIndex, Res.get("showWalletDataWindow.walletData"), ""); TextArea textArea = labelTextAreaTuple2.second; Label label = labelTextAreaTuple2.first; label.setMinWidth(150); textArea.setPrefHeight(500); textArea.getStyleClass().add("small-text"); CheckBox isUpdateCheckBox = addLabelCheckBox(gridPane, ++rowIndex, Res.get("showWalletDataWindow.includePrivKeys")); isUpdateCheckBox.setSelected(false); isUpdateCheckBox.selectedProperty().addListener((observable, oldValue, newValue) -> { showWallet(textArea, isUpdateCheckBox); }); showWallet(textArea, isUpdateCheckBox); actionButtonText(Res.get("shared.copyToClipboard")); onAction(() -> Utilities.copyToClipboard(textArea.getText())); } private void showWallet(TextArea textArea, CheckBox includePrivKeysCheckBox) { textArea.setText(walletsManager.getWalletsAsString(includePrivKeysCheckBox.isSelected())); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/SignPaymentAccountsWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.common.util.Tuple2; import haveno.common.util.Tuple3; import haveno.common.util.Utilities; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.payment.payload.PaymentMethod; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.arbitration.TraderDataItem; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.support.dispute.mediation.MediationManager; import haveno.core.util.FormattingUtils; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.InputTextField; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.main.overlays.popups.Popup; import static haveno.desktop.util.FormBuilder.add2ButtonsAfterGroup; import static haveno.desktop.util.FormBuilder.addComboBox; import static haveno.desktop.util.FormBuilder.addInputTextField; import static haveno.desktop.util.FormBuilder.addLabelCheckBox; import static haveno.desktop.util.FormBuilder.addMultilineLabel; import static haveno.desktop.util.FormBuilder.addTopLabelDatePicker; import static haveno.desktop.util.FormBuilder.addTopLabelListView; import static haveno.desktop.util.FormBuilder.removeRowsFromGridPane; import java.time.Instant; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.stream.Collectors; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.geometry.HPos; import javafx.geometry.VPos; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.DatePicker; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.util.Callback; import javafx.util.StringConverter; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.Utils; @Slf4j public class SignPaymentAccountsWindow extends Overlay { private Label descriptionLabel; private ComboBox paymentMethodComboBox; private CheckBox signAllCheckbox; private DatePicker datePicker; private InputTextField privateKey; private ListView selectedPaymentAccountsList = new ListView<>(); private final AccountAgeWitnessService accountAgeWitnessService; private final ArbitratorManager arbitratorManager; private final ArbitrationManager arbitrationManager; private final MediationManager mediationManager; private final String appName; private final boolean useDevPrivilegeKeys; @Inject public SignPaymentAccountsWindow(AccountAgeWitnessService accountAgeWitnessService, ArbitratorManager arbitratorManager, ArbitrationManager arbitrationManager, MediationManager mediationManager, @Named(Config.APP_NAME) String appName, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { this.accountAgeWitnessService = accountAgeWitnessService; this.arbitratorManager = arbitratorManager; this.arbitrationManager = arbitrationManager; this.mediationManager = mediationManager; this.appName = appName; this.useDevPrivilegeKeys = useDevPrivilegeKeys; } @Override public void show() { width = 1000; rowIndex = -1; createGridPane(); // We want to have more space to read list entries... initial screen does not look so nice now, but // dynamically updating height of window is a bit tricky.... @christoph feel free to improve if you like... gridPane.setPrefHeight(600); gridPane.getStyleClass().add("popup-with-input"); gridPane.getColumnConstraints().get(1).setHgrow(Priority.NEVER); headLine(Res.get("popup.accountSigning.selectAccounts.headline")); type = Type.Attention; addHeadLine(); addSelectAccountsContent(); addButtons(); applyStyles(); display(); } private void addSelectAccountsContent() { descriptionLabel = addMultilineLabel(gridPane, ++rowIndex, Res.get("popup.accountSigning.selectAccounts.description")); paymentMethodComboBox = addComboBox(gridPane, ++rowIndex, Res.get("shared.selectPaymentMethod")); paymentMethodComboBox.setVisibleRowCount(11); paymentMethodComboBox.setConverter(new StringConverter<>() { @Override public String toString(PaymentMethod paymentMethod) { return paymentMethod != null ? Res.get(paymentMethod.getId()) : ""; } @Override public PaymentMethod fromString(String s) { return null; } }); paymentMethodComboBox.setItems(FXCollections.observableArrayList(getPaymentMethods())); paymentMethodComboBox.setOnAction(e -> updateAccountSelectionState()); signAllCheckbox = addLabelCheckBox(gridPane, ++rowIndex, Res.get("popup.accountSigning.selectAccounts.signAll")); GridPane.setHalignment(signAllCheckbox, HPos.LEFT); signAllCheckbox.selectedProperty().addListener((observable, oldValue, newValue) -> { paymentMethodComboBox.setDisable(newValue); updateAccountSelectionState(); }); datePicker = addTopLabelDatePicker(gridPane, ++rowIndex, Res.get("popup.accountSigning.selectAccounts.datePicker"), 0).second; datePicker.setOnAction(e -> updateAccountSelectionState()); datePicker.setValue(Instant.ofEpochMilli(new Date().getTime()).minus(60, ChronoUnit.DAYS) .atZone(ZoneId.systemDefault()).toLocalDate()); } private List getPaymentMethods() { return PaymentMethod.paymentMethods.stream() .filter(PaymentMethod::isTraditional) .filter(PaymentMethod::hasChargebackRisk) .collect(Collectors.toList()); } private void addECKeyField() { privateKey = addInputTextField(gridPane, ++rowIndex, Res.get("popup.accountSigning.signAccounts.ECKey")); GridPane.setVgrow(privateKey, Priority.ALWAYS); GridPane.setValignment(privateKey, VPos.TOP); } private void updateAccountSelectionState() { actionButton.setDisable((!signAllCheckbox.isSelected() && paymentMethodComboBox.getSelectionModel().isEmpty()) || datePicker.getValue() == null ); } private void removeContent() { removeRowsFromGridPane(gridPane, 2, 3); rowIndex = 1; } private void addSelectedAccountsContent() { removeContent(); Tuple3, VBox> selectedPaymentAccountsTuple = addTopLabelListView(gridPane, ++rowIndex, Res.get("popup.accountSigning.confirmSelectedAccounts.headline")); GridPane.setRowSpan(selectedPaymentAccountsTuple.third, 2); selectedPaymentAccountsList = selectedPaymentAccountsTuple.second; ObservableList disputesAsObservableList = arbitrationManager.getDisputesAsObservableList(); long safeDate = datePicker.getValue().atStartOfDay().toEpochSecond(ZoneOffset.UTC) * 1000; List traderDataItemList; StringBuilder sb = new StringBuilder("Summary for ").append(appName).append("\n"); if (signAllCheckbox.isSelected()) { traderDataItemList = new ArrayList<>(); getPaymentMethods().forEach(paymentMethod -> { List list = accountAgeWitnessService.getTraderPaymentAccounts( safeDate, paymentMethod, disputesAsObservableList); traderDataItemList.addAll(list); sb.append("\nPayment method: ").append(Res.get(paymentMethod.getId())) .append(" (No. of signed accounts: ").append(list.size()).append(")\n"); list.forEach(traderDataItem -> { sb.append("Account created: ") .append(FormattingUtils.formatDateTime(new Date(traderDataItem.getAccountAgeWitness().getDate()), true)) .append(" Account: ") .append(traderDataItem.getPaymentAccountPayload().getPaymentDetails()).append("\n"); }); }); sb.append("\nTotal accounts signed: ").append(traderDataItemList.size()); } else { PaymentMethod paymentMethod = paymentMethodComboBox.getSelectionModel().getSelectedItem(); traderDataItemList = accountAgeWitnessService.getTraderPaymentAccounts( safeDate, paymentMethod, disputesAsObservableList); sb.append("\nPayment method: ").append(Res.get(paymentMethod.getId())) .append(" (No. of signed accounts: ").append(traderDataItemList.size()).append(")\n"); traderDataItemList.forEach(traderDataItem -> { sb.append("Account created: ") .append(FormattingUtils.formatDateTime(new Date(traderDataItem.getAccountAgeWitness().getDate()), true)) .append(" Account: ") .append(traderDataItem.getPaymentAccountPayload().getPaymentDetails()).append("\n"); }); } log.info(sb.toString()); Utilities.copyToClipboard(sb.toString()); selectedPaymentAccountsList.setItems(FXCollections.observableArrayList(traderDataItemList)); headLineLabel.setText(Res.get("popup.accountSigning.confirmSelectedAccounts.headline")); descriptionLabel.setText(Res.get("popup.accountSigning.confirmSelectedAccounts.description", selectedPaymentAccountsList.getItems().size())); ((AutoTooltipButton) actionButton).updateText(Res.get("popup.accountSigning.confirmSelectedAccounts.button")); updateAccountSelectionState(); actionButton.setOnAction(e -> addAccountsToSignContent()); selectedPaymentAccountsList.setCellFactory(new Callback<>() { @Override public ListCell call( ListView param) { return new ListCell<>() { @Override protected void updateItem(TraderDataItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setText(item.getPaymentAccountPayload().getPaymentDetails()); } else { setText(null); } } }; } }); } private void addAccountsToSignContent() { removeContent(); addECKeyField(); headLineLabel.setText(Res.get("popup.accountSigning.signAccounts.headline")); descriptionLabel.setText(Res.get("popup.accountSigning.signAccounts.description", selectedPaymentAccountsList.getItems().size())); ((AutoTooltipButton) actionButton).updateText(Res.get("popup.accountSigning.signAccounts.button")); actionButton.setOnAction(a -> { ECKey arbitratorKey = arbitratorManager.getRegistrationKey(privateKey.getText()); if (arbitratorKey != null) { String arbitratorPubKeyAsHex = Utils.HEX.encode(arbitratorKey.getPubKey()); boolean isKeyValid = arbitratorManager.isPublicKeyInList(arbitratorPubKeyAsHex); if (isKeyValid) { selectedPaymentAccountsList.getItems().forEach(item -> accountAgeWitnessService.arbitratorSignAccountAgeWitness(item.getTradeAmount(), item.getAccountAgeWitness(), arbitratorKey, item.getPeersPubKey())); addSuccessContent(); } } else { new Popup().error(Res.get("popup.accountSigning.signAccounts.ECKey.error")).onClose(this::hide).show(); } }); } private void addSuccessContent() { removeContent(); GridPane.setVgrow(descriptionLabel, Priority.ALWAYS); GridPane.setValignment(descriptionLabel, VPos.TOP); closeButton.setVisible(false); closeButton.setManaged(false); headLineLabel.setText(Res.get("popup.accountSigning.success.headline")); descriptionLabel.setText(Res.get("popup.accountSigning.success.description", selectedPaymentAccountsList.getItems().size())); ((AutoTooltipButton) actionButton).updateText(Res.get("shared.ok")); actionButton.setOnAction(a -> hide()); } @Override protected void addButtons() { Tuple2 buttonTuple = add2ButtonsAfterGroup(gridPane, ++rowIndex, Res.get("popup.accountSigning.selectAccounts.headline"), Res.get("shared.cancel")); actionButton = buttonTuple.first; actionButton.setDisable(true); actionButton.setOnAction(e -> addSelectedAccountsContent()); closeButton = (AutoTooltipButton) buttonTuple.second; closeButton.setOnAction(e -> hide()); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/SignSpecificWitnessWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.app.DevEnv; import haveno.common.config.Config; import haveno.common.util.Tuple2; import haveno.common.util.Utilities; import haveno.core.account.witness.AccountAgeWitness; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.HavenoTextArea; import haveno.desktop.components.InputTextField; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.main.overlays.popups.Popup; import static haveno.desktop.util.FormBuilder.add2ButtonsAfterGroup; import static haveno.desktop.util.FormBuilder.addInputTextField; import static haveno.desktop.util.FormBuilder.addMultilineLabel; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; import static haveno.desktop.util.FormBuilder.removeRowsFromGridPane; import java.util.Date; import javafx.geometry.VPos; import javafx.scene.control.TextArea; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.Utils; @Slf4j public class SignSpecificWitnessWindow extends Overlay { private Tuple2 signInfo; private InputTextField privateKey; private final AccountAgeWitnessService accountAgeWitnessService; private final ArbitratorManager arbitratorManager; private final boolean useDevPrivilegeKeys; @Inject public SignSpecificWitnessWindow(AccountAgeWitnessService accountAgeWitnessService, ArbitratorManager arbitratorManager, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { this.accountAgeWitnessService = accountAgeWitnessService; this.arbitratorManager = arbitratorManager; this.useDevPrivilegeKeys = useDevPrivilegeKeys; } @Override public void show() { width = 1000; rowIndex = -1; createGridPane(); gridPane.setPrefHeight(600); gridPane.getColumnConstraints().get(1).setHgrow(Priority.NEVER); gridPane.getStyleClass().add("popup-with-input"); headLine(Res.get("popup.accountSigning.singleAccountSelect.headline")); type = Type.Attention; addHeadLine(); addSelectWitnessContent(); addButtons(); applyStyles(); display(); } private void addSelectWitnessContent() { TextArea accountInfoText = new HavenoTextArea(); accountInfoText.setPrefHeight(270); accountInfoText.setWrapText(true); GridPane.setRowIndex(accountInfoText, ++rowIndex); gridPane.getChildren().add(accountInfoText); accountInfoText.textProperty().addListener((observable, oldValue, newValue) -> { if (newValue == null || newValue.isEmpty()) { return; } signInfo = accountAgeWitnessService.getSignInfoFromString(newValue); if (signInfo == null) { actionButton.setDisable(true); return; } actionButton.setDisable(false); }); } private void addECKeyField() { privateKey = addInputTextField(gridPane, ++rowIndex, Res.get("popup.accountSigning.signAccounts.ECKey")); actionButton.setDisable(true); GridPane.setVgrow(privateKey, Priority.ALWAYS); GridPane.setValignment(privateKey, VPos.TOP); privateKey.textProperty().addListener((observable, oldValue, newValue) -> { if (checkedArbitratorKey() == null) { actionButton.setDisable(true); return; } actionButton.setDisable(false); }); if (useDevPrivilegeKeys) privateKey.setText(DevEnv.DEV_PRIVILEGE_PRIV_KEY); } private void removeContent() { removeRowsFromGridPane(gridPane, 1, 3); rowIndex = 1; } private void importAccountAgeWitness() { removeContent(); headLineLabel.setText(Res.get("popup.accountSigning.confirmSingleAccount.headline")); var selectedWitnessTextField = addTopLabelTextField(gridPane, ++rowIndex, Res.get("popup.accountSigning.confirmSingleAccount.selectedHash")).second; selectedWitnessTextField.setText(Utilities.bytesAsHexString(signInfo.first.getHash())); addECKeyField(); ((AutoTooltipButton) actionButton).updateText(Res.get("popup.accountSigning.confirmSingleAccount.button")); actionButton.setOnAction(a -> { var arbitratorKey = checkedArbitratorKey(); if (arbitratorKey != null) { accountAgeWitnessService.arbitratorSignAccountAgeWitness(signInfo.first, arbitratorKey, signInfo.second, new Date().getTime()); addSuccessContent(); } else { new Popup().error(Res.get("popup.accountSigning.signAccounts.ECKey.error")).onClose(this::hide).show(); } }); } private void addSuccessContent() { removeContent(); closeButton.setVisible(false); closeButton.setManaged(false); headLineLabel.setText(Res.get("popup.accountSigning.successSingleAccount.success.headline")); var descriptionLabel = addMultilineLabel(gridPane, ++rowIndex, Res.get("popup.accountSigning.successSingleAccount.description", Utilities.bytesAsHexString(signInfo.first.getHash()))); GridPane.setVgrow(descriptionLabel, Priority.ALWAYS); GridPane.setValignment(descriptionLabel, VPos.TOP); ((AutoTooltipButton) actionButton).updateText(Res.get("shared.ok")); actionButton.setOnAction(a -> hide()); } @Override protected void addButtons() { var buttonTuple = add2ButtonsAfterGroup(gridPane, ++rowIndex + 2, Res.get("popup.accountSigning.singleAccountSelect.headline"), Res.get("shared.cancel")); actionButton = buttonTuple.first; actionButton.setDisable(true); actionButton.setOnAction(e -> importAccountAgeWitness()); closeButton = (AutoTooltipButton) buttonTuple.second; closeButton.setOnAction(e -> hide()); } private ECKey checkedArbitratorKey() { var arbitratorKey = arbitratorManager.getRegistrationKey(privateKey.getText()); if (arbitratorKey == null) { return null; } var arbitratorPubKeyAsHex = Utils.HEX.encode(arbitratorKey.getPubKey()); var isKeyValid = arbitratorManager.isPublicKeyInList(arbitratorPubKeyAsHex); return isKeyValid ? arbitratorKey : null; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/SignUnsignedPubKeysWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import com.google.inject.Inject; import haveno.common.crypto.Hash; import haveno.common.util.Tuple3; import haveno.common.util.Utilities; import haveno.core.account.sign.SignedWitness; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.InputTextField; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.main.overlays.popups.Popup; import static haveno.desktop.util.FormBuilder.add2ButtonsAfterGroup; import static haveno.desktop.util.FormBuilder.addInputTextField; import static haveno.desktop.util.FormBuilder.addTopLabelListView; import static haveno.desktop.util.FormBuilder.removeRowsFromGridPane; import java.util.ArrayList; import java.util.List; import javafx.collections.FXCollections; import javafx.geometry.VPos; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.util.Callback; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.Utils; @Slf4j public class SignUnsignedPubKeysWindow extends Overlay { private ListView unsignedPubKeys = new ListView<>(); private InputTextField privateKey; private final AccountAgeWitnessService accountAgeWitnessService; private final ArbitratorManager arbitratorManager; private List signedWitnessList = new ArrayList<>(); private List failed = new ArrayList<>(); private Callback, ListCell> signedWitnessCellFactory; @Inject public SignUnsignedPubKeysWindow(AccountAgeWitnessService accountAgeWitnessService, ArbitratorManager arbitratorManager) { this.accountAgeWitnessService = accountAgeWitnessService; this.arbitratorManager = arbitratorManager; signedWitnessCellFactory = new Callback<>() { @Override public ListCell call( ListView param) { return new ListCell<>() { @Override protected void updateItem(SignedWitness item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setText(Utilities.bytesAsHexString(Hash.getRipemd160hash(item.getSignerPubKey()))); } else { setText(null); } } }; } }; } @Override public void show() { width = 1000; rowIndex = -1; createGridPane(); gridPane.setPrefHeight(600); gridPane.getColumnConstraints().get(1).setHgrow(Priority.NEVER); gridPane.getStyleClass().add("popup-with-input"); headLine(Res.get("popup.accountSigning.singleAccountSelect.headline")); type = Type.Attention; addHeadLine(); addUnsignedPubKeysContent(); addECKeyField(); addButtons(); applyStyles(); display(); } private void addUnsignedPubKeysContent() { Tuple3, VBox> unsignedPubKeysTuple = addTopLabelListView(gridPane, ++rowIndex, Res.get("popup.accountSigning.unsignedPubKeys.headline")); unsignedPubKeys = unsignedPubKeysTuple.second; unsignedPubKeys.setCellFactory(signedWitnessCellFactory); unsignedPubKeys.setItems(FXCollections.observableArrayList( accountAgeWitnessService.getUnsignedSignerPubKeys())); } private void addECKeyField() { privateKey = addInputTextField(gridPane, ++rowIndex, Res.get("popup.accountSigning.signAccounts.ECKey")); GridPane.setVgrow(privateKey, Priority.ALWAYS); GridPane.setValignment(privateKey, VPos.TOP); } private void removeContent() { removeRowsFromGridPane(gridPane, 1, 3); rowIndex = 1; } private void signPubKeys() { removeContent(); headLineLabel.setText(Res.get("popup.accountSigning.unsignedPubKeys.signed")); var arbitratorKey = arbitratorManager.getRegistrationKey(privateKey.getText()); if (arbitratorKey != null) { var arbitratorPubKeyAsHex = Utils.HEX.encode(arbitratorKey.getPubKey()); var isKeyValid = arbitratorManager.isPublicKeyInList(arbitratorPubKeyAsHex); failed.clear(); if (isKeyValid) { unsignedPubKeys.getItems().forEach(signedWitness -> { var result = accountAgeWitnessService.arbitratorSignOrphanPubKey(arbitratorKey, signedWitness.getSignerPubKey(), signedWitness.getDate()); if (result.isEmpty()) { signedWitnessList.add(signedWitness); } else { failed.add("Signing pubkey " + Utilities.bytesAsHexString(Hash.getRipemd160hash( signedWitness.getSignerPubKey())) + " failed with error " + result); } }); showResult(); } } else { new Popup().error(Res.get("popup.accountSigning.signAccounts.ECKey.error")).onClose(this::hide).show(); } } private void showResult() { removeContent(); closeButton.setVisible(false); closeButton.setManaged(false); Tuple3, VBox> signedTuple = addTopLabelListView(gridPane, ++rowIndex, Res.get("popup.accountSigning.unsignedPubKeys.result.signed")); ListView signedWitnessListView = signedTuple.second; signedWitnessListView.setCellFactory(signedWitnessCellFactory); signedWitnessListView.setItems(FXCollections.observableArrayList(signedWitnessList)); Tuple3, VBox> failedTuple = addTopLabelListView(gridPane, ++rowIndex, Res.get("popup.accountSigning.unsignedPubKeys.result.failed")); ListView failedView = failedTuple.second; failedView.setItems(FXCollections.observableArrayList(failed)); ((AutoTooltipButton) actionButton).updateText(Res.get("shared.ok")); actionButton.setOnAction(a -> hide()); } @Override protected void addButtons() { var buttonTuple = add2ButtonsAfterGroup(gridPane, ++rowIndex + 1, Res.get("popup.accountSigning.unsignedPubKeys.sign"), Res.get("shared.cancel")); actionButton = buttonTuple.first; actionButton.setDisable(unsignedPubKeys.getItems().size() == 0); actionButton.setOnAction(e -> signPubKeys()); closeButton = (AutoTooltipButton) buttonTuple.second; closeButton.setOnAction(e -> hide()); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/SwiftPaymentDetails.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import haveno.core.payment.payload.SwiftAccountPayload; import haveno.core.trade.Trade; import haveno.core.util.VolumeUtil; import haveno.desktop.main.overlays.Overlay; import javafx.geometry.Insets; import javafx.scene.control.Label; import java.util.ArrayList; import java.util.List; import static haveno.common.util.Utilities.cleanString; import static haveno.common.util.Utilities.copyToClipboard; import static haveno.core.payment.payload.SwiftAccountPayload.ADDRESS; import static haveno.core.payment.payload.SwiftAccountPayload.BANKPOSTFIX; import static haveno.core.payment.payload.SwiftAccountPayload.BENEFICIARYPOSTFIX; import static haveno.core.payment.payload.SwiftAccountPayload.BRANCH; import static haveno.core.payment.payload.SwiftAccountPayload.COUNTRY; import static haveno.core.payment.payload.SwiftAccountPayload.INTERMEDIARYPOSTFIX; import static haveno.core.payment.payload.SwiftAccountPayload.PHONE; import static haveno.core.payment.payload.SwiftAccountPayload.SNAME; import static haveno.core.payment.payload.SwiftAccountPayload.SWIFT_ACCOUNT; import static haveno.core.payment.payload.SwiftAccountPayload.SWIFT_CODE; import static haveno.desktop.util.FormBuilder.addConfirmationLabelLabel; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; public class SwiftPaymentDetails extends Overlay { private final SwiftAccountPayload payload; private final Trade trade; private final List copyToClipboardData = new ArrayList<>(); public SwiftPaymentDetails(SwiftAccountPayload swiftAccountPayload, Trade trade) { this.payload = swiftAccountPayload; this.trade = trade; } @Override public void show() { rowIndex = -1; width = 918; createGridPane(); addContent(); addButtons(); display(); } @Override protected void cleanup() { } @Override protected void createGridPane() { super.createGridPane(); gridPane.setPadding(new Insets(35, 40, 30, 40)); gridPane.getStyleClass().add("grid-pane"); } private void addContent() { int rows = payload.usesIntermediaryBank() ? 22 : 16; addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("payment.swift.headline")); gridPane.add(new Label(""), 0, ++rowIndex); // spacer addLabelsAndCopy(Res.get("portfolio.pending.step2_buyer.amountToTransfer"), VolumeUtil.formatVolumeWithCode(trade.getVolume())); addLabelsAndCopy(Res.get(SWIFT_CODE + BANKPOSTFIX), payload.getBankSwiftCode()); addLabelsAndCopy(Res.get(SNAME + BANKPOSTFIX), payload.getBankName()); addLabelsAndCopy(Res.get(BRANCH + BANKPOSTFIX), payload.getBankBranch()); addLabelsAndCopy(Res.get(ADDRESS + BANKPOSTFIX), cleanString(payload.getBankAddress())); addLabelsAndCopy(Res.get(COUNTRY + BANKPOSTFIX), CountryUtil.getNameAndCode(payload.getBankCountryCode())); if (payload.usesIntermediaryBank()) { gridPane.add(new Label(""), 0, ++rowIndex); // spacer addLabelsAndCopy(Res.get(SWIFT_CODE + INTERMEDIARYPOSTFIX), payload.getIntermediarySwiftCode()); addLabelsAndCopy(Res.get(SNAME + INTERMEDIARYPOSTFIX), payload.getIntermediaryName()); addLabelsAndCopy(Res.get(BRANCH + INTERMEDIARYPOSTFIX), payload.getIntermediaryBranch()); addLabelsAndCopy(Res.get(ADDRESS + INTERMEDIARYPOSTFIX), cleanString(payload.getIntermediaryAddress())); addLabelsAndCopy(Res.get(COUNTRY + INTERMEDIARYPOSTFIX), CountryUtil.getNameAndCode(payload.getIntermediaryCountryCode())); } gridPane.add(new Label(""), 0, ++rowIndex); // spacer addLabelsAndCopy(Res.get("payment.account.owner.fullname"), payload.getBeneficiaryName()); addLabelsAndCopy(Res.get(SWIFT_ACCOUNT), payload.getBeneficiaryAccountNr()); addLabelsAndCopy(Res.get(ADDRESS + BENEFICIARYPOSTFIX), cleanString(payload.getBeneficiaryAddress())); addLabelsAndCopy(Res.get(PHONE + BENEFICIARYPOSTFIX), payload.getBeneficiaryPhone()); addLabelsAndCopy(Res.get("payment.account.city"), payload.getBeneficiaryCity()); addLabelsAndCopy(Res.get("payment.country"), CountryUtil.getNameAndCode(payload.getBankCountryCode())); addLabelsAndCopy(Res.get("payment.shared.extraInfo"), cleanString(payload.getSpecialInstructions())); actionButtonText(Res.get("shared.copyToClipboard")); onAction(() -> { StringBuilder work = new StringBuilder(); for (String s : copyToClipboardData) { work.append(s).append(System.lineSeparator()); } copyToClipboard(work.toString()); }); } private void addLabelsAndCopy(String title, String value) { addConfirmationLabelLabel(gridPane, ++rowIndex, title, value); copyToClipboardData.add(title + " : " + value); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/TacWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import com.google.inject.Inject; import haveno.core.locale.Res; import haveno.desktop.app.HavenoApp; import haveno.desktop.components.HyperlinkWithIcon; import haveno.desktop.main.overlays.Overlay; import javafx.geometry.Insets; import javafx.geometry.Rectangle2D; import javafx.scene.layout.GridPane; import javafx.stage.Screen; import lombok.extern.slf4j.Slf4j; import static haveno.desktop.util.FormBuilder.addHyperlinkWithIcon; @Slf4j public class TacWindow extends Overlay { private final boolean smallScreen; @Inject public TacWindow() { type = Type.Attention; Rectangle2D primaryScreenBounds = Screen.getPrimary().getVisualBounds(); final double primaryScreenBoundsWidth = primaryScreenBounds.getWidth(); smallScreen = primaryScreenBoundsWidth < 1024; if (smallScreen) { this.width = primaryScreenBoundsWidth * 0.8; log.warn("Very small screen: primaryScreenBounds=" + primaryScreenBounds.toString()); } else { width = 1250; } } @Override public void show() { headLine(Res.get("tacWindow.headline")); // We do not translate the tacs because of the legal nature. We would need translations checked by lawyers // in each language which is too expensive atm. String text = "1. In no event, unless for damages caused by acts of intent and gross negligence, damages resulting from personal injury, " + "or damages ensuing from other instances where liability is required by applicable law or agreed to in writing, will any " + "developer, copyright holder and/or any other party who modifies and/or conveys the software as permitted above or " + "facilitates its operation, be liable for damages, including any general, special, incidental or consequential damages " + "arising out of the use or inability to use the software (including but not limited to loss of data or data being " + "rendered inaccurate or losses sustained by you or third parties or a failure of the software to operate with any " + "other software), even if such developer, copyright holder and/or other party has been advised of the possibility of such damages.\n\n" + "2. The user is responsible for using the software in compliance with local laws. Don't use the software if using it is not legal in your jurisdiction.\n\n" + "3. Any " + Res.getBaseCurrencyName() + " market prices, network fee estimates, or other data obtained from servers operated by Haveno is provided on an 'as is, as available' basis without representation or warranty of any kind. It is your responsibility to verify any data provided in regards to inaccuracies or omissions.\n\n" + "4. Any Fiat payment method carries a potential risk for bank chargeback. By accepting the \"User Agreement\" the user confirms " + "to be aware of those risks and in no case will claim legal responsibility to the authors or copyright holders of the software.\n\n" + "5. Any dispute, controversy or claim arising out of or relating to the use of the software shall be settled by arbitration in " + "accordance with the Haveno arbitration rules as at present in force. The arbitration is conducted online. " + "The language to be used in the arbitration proceedings shall be English if not otherwise stated.\n\n" + "6. The user confirms that they have read and agreed to the rules regarding the trade and dispute processes:\n" + " - You must complete trades within the maximum duration specified for each payment method.\n" + " - Leave the \"reason for payment\" field empty. DO NOT put the trade ID or any other text like 'monero', 'XMR', or 'Haveno'.\n" + " - If the bank of the fiat sender charges fees, the fiat sender (" + Res.getBaseCurrencyCode() + " buyer) has to cover the fees.\n" + " - If either trader opens a dispute, the arbitrator can settle the dispute and pay out trade funds accordingly.\n" + " - In case of arbitration, you must cooperate with the arbitrator and respond to each message within 48 hours.\n" + " - The arbitrator may penalize offer makers and traders for breaching Haveno rules and the principle of acting in good faith within the network, up to the value of the security deposit.\n"; message(text); actionButtonText(Res.get("tacWindow.agree")); closeButtonText(Res.get("tacWindow.disagree")); onClose(HavenoApp.getShutDownHandler()); super.show(); } @Override protected void addMessage() { super.addMessage(); String fontStyleClass = smallScreen ? "small-text" : "normal-text"; messageTextArea.getStyleClass().add(fontStyleClass); HyperlinkWithIcon hyperlinkWithIcon = addHyperlinkWithIcon(gridPane, ++rowIndex, Res.get("tacWindow.arbitrationSystem"), "https://docs.haveno.exchange/overview/dispute-resolution"); hyperlinkWithIcon.getStyleClass().add(fontStyleClass); GridPane.setMargin(hyperlinkWithIcon, new Insets(-6, 0, -20, -4)); } @Override protected void setTruncatedMessage() { truncatedMessage = message; } @Override protected void onShow() { display(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/TorNetworkSettingsWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.UserThread; import haveno.common.util.Tuple2; import haveno.common.util.Tuple4; import haveno.common.util.Utilities; import haveno.core.app.TorSetup; import haveno.core.locale.Res; import haveno.core.user.Preferences; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.BusyAnimation; import haveno.desktop.components.TitledGroupBg; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.main.overlays.popups.Popup; import static haveno.desktop.util.FormBuilder.addButtonBusyAnimationLabelAfterGroup; import static haveno.desktop.util.FormBuilder.addComboBox; import static haveno.desktop.util.FormBuilder.addLabel; import static haveno.desktop.util.FormBuilder.addRadioButton; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import static haveno.desktop.util.FormBuilder.addTopLabelTextArea; import haveno.desktop.util.Layout; import haveno.network.p2p.network.DefaultPluggableTransports; import haveno.network.p2p.network.NetworkNode; import java.io.IOException; import java.net.URI; import java.util.Arrays; import java.util.concurrent.TimeUnit; import javafx.collections.FXCollections; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.VPos; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.RadioButton; import javafx.scene.control.TextArea; import javafx.scene.control.ToggleGroup; import javafx.scene.input.KeyCode; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.util.StringConverter; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton public class TorNetworkSettingsWindow extends Overlay { public enum BridgeOption { NONE, PROVIDED, CUSTOM } public enum Transport { OBFS_4, OBFS_3, MEEK_AMAZON, MEEK_AZURE } private final Preferences preferences; private final NetworkNode networkNode; private final TorSetup torSetup; private Label enterBridgeLabel; private ComboBox transportTypeComboBox; private TextArea bridgeEntriesTextArea; private BridgeOption selectedBridgeOption = BridgeOption.NONE; private Transport selectedTorTransportOrdinal = Transport.OBFS_4; private String customBridges = ""; @Inject public TorNetworkSettingsWindow(Preferences preferences, NetworkNode networkNode, TorSetup torSetup) { this.preferences = preferences; this.networkNode = networkNode; this.torSetup = torSetup; type = Type.Attention; useShutDownButton(); } /////////////////////////////////////////////////////////////////////////////////////////// // Public API /////////////////////////////////////////////////////////////////////////////////////////// public void show() { if (!isDisplayed) { if (headLine == null) headLine = Res.get("torNetworkSettingWindow.header"); width = 1068; rowIndex = 0; createGridPane(); gridPane.getColumnConstraints().get(0).setHalignment(HPos.LEFT); addContent(); addButtons(); applyStyles(); display(); } } protected void addButtons() { closeButton = new AutoTooltipButton(closeButtonText == null ? Res.get("shared.close") : closeButtonText); closeButton.setOnAction(event -> doClose()); if (actionHandlerOptional.isPresent()) { actionButton = new AutoTooltipButton(Res.get("shared.shutDown")); actionButton.setDefaultButton(true); //TODO app wide focus //actionButton.requestFocus(); actionButton.setOnAction(event -> saveAndShutDown()); Button urlButton = new AutoTooltipButton(Res.get("torNetworkSettingWindow.openTorWebPage")); urlButton.setOnAction(event -> { try { Utilities.openURI(URI.create("https://bridges.torproject.org")); } catch (IOException e) { e.printStackTrace(); } }); Pane spacer = new Pane(); HBox hBox = new HBox(); hBox.setSpacing(10); hBox.getChildren().addAll(spacer, urlButton, closeButton, actionButton); HBox.setHgrow(spacer, Priority.ALWAYS); GridPane.setHalignment(hBox, HPos.RIGHT); GridPane.setRowIndex(hBox, ++rowIndex); GridPane.setColumnSpan(hBox, 2); GridPane.setMargin(hBox, new Insets(buttonDistance, 0, 0, 0)); gridPane.getChildren().add(hBox); } else if (!hideCloseButton) { closeButton.setDefaultButton(true); GridPane.setHalignment(closeButton, HPos.RIGHT); GridPane.setMargin(closeButton, new Insets(buttonDistance, 0, 0, 0)); GridPane.setRowIndex(closeButton, rowIndex); GridPane.setColumnIndex(closeButton, 1); gridPane.getChildren().add(closeButton); } } /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void setupKeyHandler(Scene scene) { if (!hideCloseButton) { scene.setOnKeyPressed(e -> { if (e.getCode() == KeyCode.ESCAPE) { e.consume(); doClose(); } else if (e.getCode() == KeyCode.ENTER) { e.consume(); saveAndShutDown(); } }); } } @Override protected void applyStyles() { super.applyStyles(); gridPane.setId("popup-grid-pane-bg"); } private void addContent() { addTitledGroupBg(gridPane, ++rowIndex, 2, Res.get("torNetworkSettingWindow.deleteFiles.header")); Label deleteFilesLabel = addLabel(gridPane, rowIndex, Res.get("torNetworkSettingWindow.deleteFiles.info"), Layout.TWICE_FIRST_ROW_DISTANCE); deleteFilesLabel.setWrapText(true); GridPane.setColumnIndex(deleteFilesLabel, 0); GridPane.setColumnSpan(deleteFilesLabel, 2); GridPane.setHalignment(deleteFilesLabel, HPos.LEFT); GridPane.setValignment(deleteFilesLabel, VPos.TOP); Tuple4 tuple = addButtonBusyAnimationLabelAfterGroup(gridPane, ++rowIndex, Res.get("torNetworkSettingWindow.deleteFiles.button")); Button deleteFilesButton = tuple.first; deleteFilesButton.getStyleClass().remove("action-button"); deleteFilesButton.setOnAction(e -> { tuple.second.play(); tuple.third.setText(Res.get("torNetworkSettingWindow.deleteFiles.progress")); gridPane.setMouseTransparent(true); deleteFilesButton.setDisable(true); cleanTorDir(() -> { tuple.second.stop(); tuple.third.setText(""); new Popup().feedback(Res.get("torNetworkSettingWindow.deleteFiles.success")) .useShutDownButton() .hideCloseButton() .show(); }); }); final TitledGroupBg titledGroupBg = addTitledGroupBg(gridPane, ++rowIndex, 8, Res.get("torNetworkSettingWindow.bridges.header"), Layout.GROUP_DISTANCE); titledGroupBg.getStyleClass().add("last"); Label bridgesLabel = addLabel(gridPane, rowIndex, Res.get("torNetworkSettingWindow.bridges.info"), Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE); bridgesLabel.setWrapText(true); GridPane.setColumnIndex(bridgesLabel, 0); GridPane.setColumnSpan(bridgesLabel, 2); GridPane.setHalignment(bridgesLabel, HPos.LEFT); GridPane.setValignment(bridgesLabel, VPos.TOP); ToggleGroup toggleGroup = new ToggleGroup(); // noBridges RadioButton noBridgesRadioButton = addRadioButton(gridPane, ++rowIndex, toggleGroup, Res.get("torNetworkSettingWindow.noBridges")); noBridgesRadioButton.setUserData(BridgeOption.NONE); GridPane.setMargin(noBridgesRadioButton, new Insets(20, 0, 0, 0)); // providedBridges RadioButton providedBridgesRadioButton = addRadioButton(gridPane, ++rowIndex, toggleGroup, Res.get("torNetworkSettingWindow.providedBridges")); providedBridgesRadioButton.setUserData(BridgeOption.PROVIDED); transportTypeComboBox = addComboBox(gridPane, ++rowIndex, Res.get("torNetworkSettingWindow.transportType")); transportTypeComboBox.setItems(FXCollections.observableArrayList(Arrays.asList( Transport.OBFS_4, Transport.OBFS_3, Transport.MEEK_AMAZON, Transport.MEEK_AZURE))); transportTypeComboBox.setConverter(new StringConverter<>() { @Override public String toString(Transport transport) { switch (transport) { case OBFS_3: return Res.get("torNetworkSettingWindow.obfs3"); case MEEK_AMAZON: return Res.get("torNetworkSettingWindow.meekAmazon"); case MEEK_AZURE: return Res.get("torNetworkSettingWindow.meekAzure"); default: case OBFS_4: return Res.get("torNetworkSettingWindow.obfs4"); } } @Override public Transport fromString(String string) { return null; } }); // customBridges RadioButton customBridgesRadioButton = addRadioButton(gridPane, ++rowIndex, toggleGroup, Res.get("torNetworkSettingWindow.customBridges")); customBridgesRadioButton.setUserData(BridgeOption.CUSTOM); final Tuple2 labelTextAreaTuple2 = addTopLabelTextArea(gridPane, ++rowIndex, Res.get("torNetworkSettingWindow.enterBridge"), Res.get("torNetworkSettingWindow.enterBridgePrompt")); enterBridgeLabel = labelTextAreaTuple2.first; bridgeEntriesTextArea = labelTextAreaTuple2.second; bridgeEntriesTextArea.setPrefHeight(60); Label label2 = addLabel(gridPane, ++rowIndex, Res.get("torNetworkSettingWindow.restartInfo")); label2.setWrapText(true); GridPane.setColumnSpan(label2, 2); GridPane.setHalignment(label2, HPos.LEFT); GridPane.setValignment(label2, VPos.TOP); GridPane.setMargin(label2, new Insets(10, 10, 20, 0)); // init persisted values selectedBridgeOption = BridgeOption.values()[preferences.getBridgeOptionOrdinal()]; switch (selectedBridgeOption) { case PROVIDED: toggleGroup.selectToggle(providedBridgesRadioButton); break; case CUSTOM: toggleGroup.selectToggle(customBridgesRadioButton); break; default: case NONE: toggleGroup.selectToggle(noBridgesRadioButton); break; } applyToggleSelection(); selectedTorTransportOrdinal = Transport.values()[preferences.getTorTransportOrdinal()]; transportTypeComboBox.getSelectionModel().select(selectedTorTransportOrdinal); customBridges = preferences.getCustomBridges(); bridgeEntriesTextArea.setText(customBridges); toggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> { selectedBridgeOption = (BridgeOption) newValue.getUserData(); preferences.setBridgeOptionOrdinal(selectedBridgeOption.ordinal()); applyToggleSelection(); }); transportTypeComboBox.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { selectedTorTransportOrdinal = newValue; preferences.setTorTransportOrdinal(selectedTorTransportOrdinal.ordinal()); setBridgeAddressesByTransport(); }); bridgeEntriesTextArea.textProperty().addListener((observable, oldValue, newValue) -> { customBridges = newValue; preferences.setCustomBridges(customBridges); setBridgeAddressesByCustomBridges(); } ); } private void cleanTorDir(Runnable resultHandler) { // We shut down Tor to be able to delete locked files (Windows locks files used by a process) networkNode.shutDown(() -> { // We give it a bit extra time to be sure that OS locks are removed UserThread.runAfter(() -> { torSetup.cleanupTorFiles(resultHandler, errorMessage -> new Popup().error(errorMessage).show()); }, 3); }); } private void applyToggleSelection() { switch (selectedBridgeOption) { case PROVIDED: transportTypeComboBox.setDisable(false); enterBridgeLabel.setDisable(true); bridgeEntriesTextArea.setDisable(true); setBridgeAddressesByTransport(); break; case CUSTOM: enterBridgeLabel.setDisable(false); bridgeEntriesTextArea.setDisable(false); transportTypeComboBox.setDisable(true); setBridgeAddressesByCustomBridges(); break; default: case NONE: transportTypeComboBox.setDisable(true); enterBridgeLabel.setDisable(true); bridgeEntriesTextArea.setDisable(true); preferences.setBridgeAddresses(null); break; } } private void setBridgeAddressesByTransport() { switch (selectedTorTransportOrdinal) { case OBFS_3: preferences.setBridgeAddresses(DefaultPluggableTransports.OBFS_3); break; case MEEK_AMAZON: preferences.setBridgeAddresses(DefaultPluggableTransports.MEEK_AMAZON); break; case MEEK_AZURE: preferences.setBridgeAddresses(DefaultPluggableTransports.MEEK_AZURE); break; default: case OBFS_4: preferences.setBridgeAddresses(DefaultPluggableTransports.OBFS_4); break; } } private void setBridgeAddressesByCustomBridges() { preferences.setBridgeAddresses(customBridges != null ? Arrays.asList(customBridges.split("\\n")) : null); } private void saveAndShutDown() { UserThread.runAfter(() -> actionHandlerOptional.ifPresent(Runnable::run), 500, TimeUnit.MILLISECONDS); hide(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeDetailsWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.UserThread; import haveno.common.util.Tuple3; import haveno.common.util.Utilities; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.offer.Offer; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.trade.Contract; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.util.FormattingUtils; import haveno.core.util.VolumeUtil; import haveno.core.util.coin.CoinFormatter; import haveno.core.xmr.wallet.BtcWalletService; import haveno.desktop.components.HavenoTextArea; import haveno.desktop.main.MainView; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.util.DisplayUtils; import haveno.desktop.util.GUIUtil; import static haveno.desktop.util.DisplayUtils.getAccountWitnessDescription; import static haveno.desktop.util.FormBuilder.add2ButtonsWithBox; import static haveno.desktop.util.FormBuilder.addConfirmationLabelTextArea; import static haveno.desktop.util.FormBuilder.addConfirmationLabelTextField; import static haveno.desktop.util.FormBuilder.addLabelTxIdTextField; import static haveno.desktop.util.FormBuilder.addSeparator; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import haveno.desktop.util.Layout; import haveno.network.p2p.NodeAddress; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.value.ChangeListener; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.TextArea; import javafx.scene.control.Tooltip; import javafx.scene.input.KeyCode; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.stage.Modality; import javafx.stage.Stage; import javafx.stage.StageStyle; import javafx.stage.Window; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class TradeDetailsWindow extends Overlay { protected static final Logger log = LoggerFactory.getLogger(TradeDetailsWindow.class); private final CoinFormatter formatter; private final ArbitrationManager arbitrationManager; private final TradeManager tradeManager; private final BtcWalletService btcWalletService; private final AccountAgeWitnessService accountAgeWitnessService; private Trade trade; private ChangeListener changeListener; private TextArea textArea; private String buyersAccountAge; private String sellersAccountAge; /////////////////////////////////////////////////////////////////////////////////////////// // Public API /////////////////////////////////////////////////////////////////////////////////////////// @Inject public TradeDetailsWindow(@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, ArbitrationManager arbitrationManager, TradeManager tradeManager, BtcWalletService btcWalletService, AccountAgeWitnessService accountAgeWitnessService) { this.formatter = formatter; this.arbitrationManager = arbitrationManager; this.tradeManager = tradeManager; this.btcWalletService = btcWalletService; this.accountAgeWitnessService = accountAgeWitnessService; type = Type.Confirmation; } public void show(Trade trade) { this.trade = trade; rowIndex = -1; width = Layout.DETAILS_WINDOW_WIDTH; createGridPane(); addContent(); display(); } /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void cleanup() { if (textArea != null) textArea.scrollTopProperty().addListener(changeListener); } @Override protected void createGridPane() { super.createGridPane(); gridPane.getStyleClass().add("grid-pane"); } private void addContent() { Offer offer = trade.getOffer(); Contract contract = trade.getContract(); int rows = 9; addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("tradeDetailsWindow.headline")); boolean myOffer = tradeManager.isMyOffer(offer); String counterCurrencyDirectionInfo; String xmrDirectionInfo; String toReceive = " " + Res.get("shared.toReceive"); String toSpend = " " + Res.get("shared.toSpend"); String offerType = Res.get("shared.offerType"); if (tradeManager.isBuyer(offer)) { addConfirmationLabelTextField(gridPane, rowIndex, offerType, DisplayUtils.getDirectionForBuyer(myOffer, offer.getCounterCurrencyCode()), Layout.TWICE_FIRST_ROW_DISTANCE); counterCurrencyDirectionInfo = toSpend; xmrDirectionInfo = toReceive; } else { addConfirmationLabelTextField(gridPane, rowIndex, offerType, DisplayUtils.getDirectionForSeller(myOffer, offer.getCounterCurrencyCode()), Layout.TWICE_FIRST_ROW_DISTANCE); counterCurrencyDirectionInfo = toReceive; xmrDirectionInfo = toSpend; } addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.xmrAmount") + xmrDirectionInfo, HavenoUtils.formatXmr(trade.getAmount(), true)); addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, VolumeUtil.formatVolumeLabel(offer.getCounterCurrencyCode()) + counterCurrencyDirectionInfo, VolumeUtil.formatVolumeWithCode(trade.getVolume())); addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.tradePrice"), FormattingUtils.formatPrice(trade.getPrice())); String paymentMethodText = Res.get(offer.getPaymentMethod().getId()); addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.paymentMethod"), paymentMethodText); // second group rows = 5; if (offer.getCombinedExtraInfo() != null && !offer.getCombinedExtraInfo().isEmpty()) rows++; PaymentAccountPayload buyerPaymentAccountPayload = null; PaymentAccountPayload sellerPaymentAccountPayload = null; if (contract != null) { rows++; buyerPaymentAccountPayload = trade.getBuyer().getPaymentAccountPayload(); sellerPaymentAccountPayload = trade.getSeller().getPaymentAccountPayload(); if (buyerPaymentAccountPayload != null) rows++; if (sellerPaymentAccountPayload != null) rows++; if (buyerPaymentAccountPayload == null && sellerPaymentAccountPayload == null) rows++; } boolean showDisputedTx = arbitrationManager.findOwnDispute(trade.getId()).isPresent() && arbitrationManager.findOwnDispute(trade.getId()).get().getDisputePayoutTxId() != null; if (showDisputedTx) rows++; else if (trade.getPayoutTxId() != null) rows++; if (trade.hasFailed()) rows += 2; if (trade.getTradePeerNodeAddress() != null) rows++; addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.details"), Layout.COMPACT_GROUP_DISTANCE); addConfirmationLabelTextField(gridPane, rowIndex, Res.get("shared.tradeId"), trade.getId(), Layout.TWICE_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE); addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.tradeDate"), DisplayUtils.formatDateTime(trade.getDate())); String securityDeposit = Res.getWithColAndCap("shared.buyer") + " " + HavenoUtils.formatXmr(trade.getBuyerSecurityDepositBeforeMiningFee(), true) + " / " + Res.getWithColAndCap("shared.seller") + " " + HavenoUtils.formatXmr(trade.getSellerSecurityDepositBeforeMiningFee(), true); addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), securityDeposit); NodeAddress arbitratorNodeAddress = trade.getArbitratorNodeAddress(); if (arbitratorNodeAddress != null) { addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.agentAddresses"), arbitratorNodeAddress.getFullAddress()); } if (trade.getTradePeerNodeAddress() != null) { addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.tradePeersOnion"), trade.getTradePeerNodeAddress().getFullAddress()); } if (offer.getCombinedExtraInfo() != null && !offer.getCombinedExtraInfo().isEmpty()) { addSeparator(gridPane, ++rowIndex); TextArea textArea = addConfirmationLabelTextArea(gridPane, ++rowIndex, Res.get("payment.shared.extraInfo.offer"), "", 0).second; textArea.setText(offer.getCombinedExtraInfo().trim()); textArea.setMaxHeight(Layout.DETAILS_WINDOW_EXTRA_INFO_MAX_HEIGHT); textArea.setEditable(false); GUIUtil.adjustHeightAutomatically(textArea, Layout.DETAILS_WINDOW_EXTRA_INFO_MAX_HEIGHT); } if (contract != null) { buyersAccountAge = getAccountWitnessDescription(accountAgeWitnessService, offer.getPaymentMethod(), buyerPaymentAccountPayload, contract.getBuyerPubKeyRing()); sellersAccountAge = getAccountWitnessDescription(accountAgeWitnessService, offer.getPaymentMethod(), sellerPaymentAccountPayload, contract.getSellerPubKeyRing()); if (buyerPaymentAccountPayload != null) { String paymentDetails = buyerPaymentAccountPayload.getPaymentDetails(); String postFix = " / " + buyersAccountAge; addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.paymentDetails", Res.get("shared.buyer")), paymentDetails + postFix).second.setTooltip(new Tooltip(paymentDetails + postFix)); } if (sellerPaymentAccountPayload != null) { String paymentDetails = sellerPaymentAccountPayload.getPaymentDetails(); String postFix = " / " + sellersAccountAge; addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.paymentDetails", Res.get("shared.seller")), paymentDetails + postFix).second.setTooltip(new Tooltip(paymentDetails + postFix)); } if (buyerPaymentAccountPayload == null && sellerPaymentAccountPayload == null) { addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.paymentMethod"), Res.get(contract.getPaymentMethodId())); } } if (trade.getMaker().getDepositTxHash() != null) { addSeparator(gridPane, ++rowIndex); addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.makerDepositTransactionId"), trade.getMaker().getDepositTxHash()); } if (trade.getTaker().getDepositTxHash() != null) { addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.takerDepositTransactionId"), trade.getTaker().getDepositTxHash()); } if (showDisputedTx) { addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.disputedPayoutTxId"), arbitrationManager.findOwnDispute(trade.getId()).get().getDisputePayoutTxId()); } else if (trade.getPayoutTxId() != null && !trade.getPayoutTxId().isBlank()) { addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.payoutTxId"), trade.getPayoutTxId()); } if (trade.hasFailed()) { addSeparator(gridPane, ++rowIndex); textArea = addConfirmationLabelTextArea(gridPane, ++rowIndex, Res.get("shared.errorMessage"), "", 0).second; textArea.setText(trade.getErrorMessage()); textArea.setEditable(false); //TODO paint red IntegerProperty count = new SimpleIntegerProperty(20); int rowHeight = 10; textArea.prefHeightProperty().bindBidirectional(count); changeListener = (ov, old, newVal) -> { if (newVal.intValue() > rowHeight) count.setValue(count.get() + newVal.intValue() + 10); }; textArea.scrollTopProperty().addListener(changeListener); textArea.setScrollTop(30); addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.tradePhase"), trade.getPhase().name()); } Tuple3 tuple = add2ButtonsWithBox(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.detailData"), Res.get("shared.close"), 15, false); Button viewContractButton = tuple.first; viewContractButton.setMaxWidth(Region.USE_COMPUTED_SIZE); Button closeButton = tuple.second; closeButton.setMaxWidth(Region.USE_COMPUTED_SIZE); HBox hBox = tuple.third; GridPane.setColumnSpan(hBox, 2); Region spacer = new Region(); HBox.setHgrow(spacer, Priority.ALWAYS); hBox.getChildren().add(0, spacer); String buyerWitnessHash = trade.getBuyer().getAccountAgeWitness() == null ? "null" : Utilities.bytesAsHexString(trade.getBuyer().getAccountAgeWitness().getHash()); String buyerPubKeyRingHash = Utilities.bytesAsHexString(trade.getBuyer().getPubKeyRing().getSignaturePubKeyBytes()); String sellerWitnessHash = trade.getSeller().getAccountAgeWitness() == null ? "null" : Utilities.bytesAsHexString(trade.getSeller().getAccountAgeWitness().getHash()); String sellerPubKeyRingHash = Utilities.bytesAsHexString(trade.getSeller().getPubKeyRing().getSignaturePubKeyBytes()); viewContractButton.setOnAction(e -> { TextArea textArea = new HavenoTextArea(); textArea.setText(trade.getContractAsJson()); String data = "Trade state: " + trade.getState(); data += "\nTrade payout state: " + trade.getPayoutState(); data += "\nTrade dispute state: " + trade.getDisputeState(); data += "\n\nContract as json:\n"; data += trade.getContractAsJson(); data += "\n\nOther detail data:"; if (!trade.isDepositsPublished()) { data += "\n\n" + (trade.getMaker() == trade.getBuyer() ? "Buyer" : "Seller") + " as maker reserve tx hex: " + trade.getMaker().getReserveTxHex(); data += "\n\n" + (trade.getTaker() == trade.getBuyer() ? "Buyer" : "Seller") + " as taker reserve tx hex: " + trade.getTaker().getReserveTxHex(); } if (offer.isTraditionalOffer()) { data += "\n\nBuyers witness hash,pub key ring hash: " + buyerWitnessHash + "," + buyerPubKeyRingHash; data += "\nBuyers account age: " + buyersAccountAge; data += "\nSellers witness hash,pub key ring hash: " + sellerWitnessHash + "," + sellerPubKeyRingHash; data += "\nSellers account age: " + sellersAccountAge; } // TODO (woodser): include maker and taker deposit tx hex in contract? // if (depositTx != null) { // String depositTxAsHex = Utils.HEX.encode(depositTx.bitcoinSerialize(true)); // data += "\n\nRaw deposit transaction as hex:\n" + depositTxAsHex; // } data += "\n\nSelected arbitrator: " + trade.getArbitrator().getNodeAddress(); textArea.setText(data); textArea.setPrefHeight(50); textArea.setEditable(false); textArea.setWrapText(true); textArea.setPrefSize(800, 600); Scene viewContractScene = new Scene(textArea); Stage viewContractStage = new Stage(); viewContractStage.setTitle(Res.get("shared.contract.title", trade.getShortId())); viewContractStage.setScene(viewContractScene); if (owner == null) owner = MainView.getRootContainer(); Scene rootScene = owner.getScene(); viewContractStage.initOwner(rootScene.getWindow()); viewContractStage.initModality(Modality.NONE); viewContractStage.initStyle(StageStyle.UTILITY); viewContractStage.setOpacity(0); viewContractStage.show(); Window window = rootScene.getWindow(); double titleBarHeight = window.getHeight() - rootScene.getHeight(); viewContractStage.setX(Math.round(window.getX() + (owner.getWidth() - viewContractStage.getWidth()) / 2) + 200); viewContractStage.setY(Math.round(window.getY() + titleBarHeight + (owner.getHeight() - viewContractStage.getHeight()) / 2) + 50); // Delay display to next render frame to avoid that the popup is first quickly displayed in default position // and after a short moment in the correct position UserThread.execute(() -> viewContractStage.setOpacity(1)); viewContractScene.setOnKeyPressed(ev -> { if (ev.getCode() == KeyCode.ESCAPE) { ev.consume(); viewContractStage.hide(); } }); }); closeButton.setOnAction(e -> { closeHandlerOptional.ifPresent(Runnable::run); hide(); }); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeFeedbackWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import com.google.inject.Inject; import haveno.core.locale.Res; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.HyperlinkWithIcon; import haveno.desktop.main.overlays.Overlay; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; import lombok.extern.slf4j.Slf4j; import static haveno.desktop.util.FormBuilder.addHyperlinkWithIcon; @Slf4j public class TradeFeedbackWindow extends Overlay { @Inject public TradeFeedbackWindow() { type = Type.Confirmation; } @Override public void show() { headLine(Res.get("tradeFeedbackWindow.title")); //message(Res.get("tradeFeedbackWindow.msg.part1")); // TODO: this message part has padding which remaining message does not have hideCloseButton(); actionButtonText(Res.get("shared.close")); super.show(); } @Override protected void addMessage() { super.addMessage(); AutoTooltipLabel messageLabel1 = new AutoTooltipLabel(Res.get("tradeFeedbackWindow.msg.part1")); messageLabel1.setMouseTransparent(true); messageLabel1.setWrapText(true); GridPane.setHalignment(messageLabel1, HPos.LEFT); GridPane.setHgrow(messageLabel1, Priority.ALWAYS); GridPane.setRowIndex(messageLabel1, ++rowIndex); GridPane.setColumnIndex(messageLabel1, 0); GridPane.setColumnSpan(messageLabel1, 2); gridPane.getChildren().add(messageLabel1); GridPane.setMargin(messageLabel1, new Insets(10, 0, 10, 0)); AutoTooltipLabel messageLabel2 = new AutoTooltipLabel(Res.get("tradeFeedbackWindow.msg.part2")); messageLabel2.setMouseTransparent(true); messageLabel2.setWrapText(true); GridPane.setHalignment(messageLabel2, HPos.LEFT); GridPane.setHgrow(messageLabel2, Priority.ALWAYS); GridPane.setRowIndex(messageLabel2, ++rowIndex); GridPane.setColumnIndex(messageLabel2, 0); GridPane.setColumnSpan(messageLabel2, 2); gridPane.getChildren().add(messageLabel2); HyperlinkWithIcon matrix = addHyperlinkWithIcon(gridPane, ++rowIndex, "https://matrix.to/#/#haveno:monero.social", "https://matrix.to/#/%23haveno:monero.social", 40); GridPane.setMargin(matrix, new Insets(-6, 0, 10, 0)); AutoTooltipLabel messageLabel3 = new AutoTooltipLabel(Res.get("tradeFeedbackWindow.msg.part3")); messageLabel3.setMouseTransparent(true); messageLabel3.setWrapText(true); GridPane.setHalignment(messageLabel3, HPos.LEFT); GridPane.setHgrow(messageLabel3, Priority.ALWAYS); GridPane.setRowIndex(messageLabel3, ++rowIndex); GridPane.setColumnIndex(messageLabel3, 0); GridPane.setColumnSpan(messageLabel3, 2); gridPane.getChildren().add(messageLabel3); } @Override protected void onShow() { display(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/TxDetailsWindow.java ================================================ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.desktop.main.overlays.windows; import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.main.funds.transactions.TransactionsListItem; import haveno.desktop.main.overlays.Overlay; import javafx.scene.layout.GridPane; import javafx.scene.layout.Region; import monero.wallet.model.MoneroTxWallet; import static haveno.desktop.util.FormBuilder.addConfirmationLabelLabel; import static haveno.desktop.util.FormBuilder.addConfirmationLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addLabelTxIdTextField; import static haveno.desktop.util.FormBuilder.addMultilineLabel; import java.math.BigInteger; import com.google.inject.Inject; public class TxDetailsWindow extends Overlay { private XmrWalletService xmrWalletService; private TransactionsListItem item; @Inject public TxDetailsWindow(XmrWalletService xmrWalletService) { this.xmrWalletService = xmrWalletService; } public void show(TransactionsListItem item) { this.item = item; rowIndex = -1; width = 918; if (headLine == null) headLine = Res.get("txDetailsWindow.headline"); createGridPane(); gridPane.setHgap(15); addHeadLine(); addContent(); addButtons(); applyStyles(); display(); } protected void addContent() { MoneroTxWallet tx = item.getTx(); String memo = tx.getNote(); String txKey = null; boolean isOutgoing = tx.getOutgoingTransfer() != null; if (isOutgoing) { try { txKey = xmrWalletService.getWallet().getTxKey(tx.getHash()); } catch (Exception e) { // TODO (monero-java): wallet.getTxKey() should return null if key does not exist instead of throwing exception } } // add sent or received note String resKey = isOutgoing ? "txDetailsWindow.xmr.noteSent" : "txDetailsWindow.xmr.noteReceived"; GridPane.setColumnSpan(addMultilineLabel(gridPane, ++rowIndex, Res.get(resKey), 0), 2); Region spacer = new Region(); spacer.setMinHeight(15); gridPane.add(spacer, 0, ++rowIndex); // add tx fields addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.dateTime"), item.getDateString()); BigInteger amount; if (isOutgoing) { addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("txDetailsWindow.sentTo"), item.getAddressString()); amount = tx.getOutgoingAmount(); } else { addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("txDetailsWindow.receivedWith"), item.getAddressString()); amount = tx.getIncomingAmount(); } addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.amount"), HavenoUtils.formatXmr(amount)); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.txFee"), HavenoUtils.formatXmr(tx.getFee())); if (memo != null && !"".equals(memo)) addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("funds.withdrawal.memoLabel"), memo); if (txKey != null && !"".equals(txKey)) addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("txDetailsWindow.txKey"), txKey); addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("txDetailsWindow.txId"), tx.getHash()); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/TxWithdrawWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import haveno.core.locale.Res; import haveno.desktop.components.TxIdTextField; import haveno.desktop.main.overlays.Overlay; import javafx.scene.layout.GridPane; import javafx.scene.layout.Region; import static haveno.desktop.util.FormBuilder.addConfirmationLabelLabel; import static haveno.desktop.util.FormBuilder.addConfirmationLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addLabelTxIdTextField; import static haveno.desktop.util.FormBuilder.addMultilineLabel; public class TxWithdrawWindow extends Overlay { protected String txId, address, amount, fee, memo; protected TxIdTextField txIdTextField; public TxWithdrawWindow(String txId, String address, String amount, String fee, String memo) { type = Type.Attention; this.txId = txId; this.address = address; this.amount = amount; this.fee = fee; this.memo = memo; } public void show() { rowIndex = -1; width = 918; if (headLine == null) headLine = Res.get("txDetailsWindow.headline"); createGridPane(); gridPane.setHgap(15); addHeadLine(); addContent(); addButtons(); applyStyles(); display(); } protected void addContent() { GridPane.setColumnSpan( addMultilineLabel(gridPane, ++rowIndex, Res.get("txDetailsWindow.xmr.noteSent"), 0), 2); Region spacer = new Region(); spacer.setMinHeight(20); gridPane.add(spacer, 0, ++rowIndex); addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("txDetailsWindow.sentTo"), address); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.amount"), amount); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.txFee"), fee); if (memo != null && !"".equals(memo)) addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("funds.withdrawal.memoLabel"), memo); txIdTextField = addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("txDetailsWindow.txId"), txId).second; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/UnlockDisputeAgentRegistrationWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import haveno.common.app.DevEnv; import haveno.common.util.Tuple2; import haveno.common.util.Tuple3; import haveno.core.locale.Res; import haveno.desktop.components.InputTextField; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.main.overlays.popups.Popup; import javafx.beans.value.ChangeListener; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.input.KeyCode; import javafx.scene.layout.GridPane; import javafx.scene.layout.VBox; import static haveno.desktop.util.FormBuilder.add2ButtonsAfterGroup; import static haveno.desktop.util.FormBuilder.addTopLabelInputTextFieldWithVBox; public class UnlockDisputeAgentRegistrationWindow extends Overlay { private final boolean useDevPrivilegeKeys; private Button unlockButton; private InputTextField keyInputTextField; private PrivKeyHandler privKeyHandler; private ChangeListener changeListener; /////////////////////////////////////////////////////////////////////////////////////////// // Interface /////////////////////////////////////////////////////////////////////////////////////////// public interface PrivKeyHandler { boolean checkKey(String privKey); } /////////////////////////////////////////////////////////////////////////////////////////// // Public API /////////////////////////////////////////////////////////////////////////////////////////// public UnlockDisputeAgentRegistrationWindow(boolean useDevPrivilegeKeys) { this.useDevPrivilegeKeys = useDevPrivilegeKeys; if (keyInputTextField != null) keyInputTextField.textProperty().addListener(changeListener); type = Type.Attention; } public void show() { if (gridPane != null) { rowIndex = -1; gridPane.getChildren().clear(); } if (headLine == null) headLine = Res.get("enterPrivKeyWindow.headline"); createGridPane(); addHeadLine(); addInputFields(); addButtons(); applyStyles(); display(); } public UnlockDisputeAgentRegistrationWindow onKey(PrivKeyHandler privKeyHandler) { this.privKeyHandler = privKeyHandler; return this; } /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void cleanup() { } @Override protected void setupKeyHandler(Scene scene) { if (!hideCloseButton) { scene.setOnKeyPressed(e -> { if (e.getCode() == KeyCode.ESCAPE) { e.consume(); doClose(); } }); } } private void addInputFields() { final Tuple3 labelInputTextFieldTuple2 = addTopLabelInputTextFieldWithVBox(gridPane, ++rowIndex, Res.get("shared.enterPrivKey"), 3); GridPane.setColumnSpan(labelInputTextFieldTuple2.third, 2); Label label = labelInputTextFieldTuple2.first; label.setWrapText(true); keyInputTextField = labelInputTextFieldTuple2.second; if (useDevPrivilegeKeys) keyInputTextField.setText(DevEnv.DEV_PRIVILEGE_PRIV_KEY); changeListener = (observable, oldValue, newValue) -> unlockButton.setDisable(newValue.length() == 0); keyInputTextField.textProperty().addListener(changeListener); } @Override protected void addButtons() { final Tuple2 buttonButtonTuple2 = add2ButtonsAfterGroup(gridPane, ++rowIndex, Res.get("shared.unlock"), Res.get("shared.close")); unlockButton = buttonButtonTuple2.first; unlockButton.setDisable(keyInputTextField.getText().length() == 0); unlockButton.setOnAction(e -> { if (privKeyHandler.checkKey(keyInputTextField.getText())) hide(); else new Popup().warning(Res.get("shared.invalidKey")).width(300).onClose(this::blurAgain).show(); }); Button closeButton = buttonButtonTuple2.second; closeButton.setOnAction(event -> { hide(); closeHandlerOptional.ifPresent(Runnable::run); }); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/UpdateAmazonGiftCardAccountWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import haveno.common.UserThread; import haveno.core.locale.Country; import haveno.core.locale.Res; import haveno.core.payment.AmazonGiftCardAccount; import haveno.core.user.User; import haveno.desktop.main.overlays.Overlay; import javafx.collections.FXCollections; import javafx.scene.Scene; import javafx.scene.control.ComboBox; import javafx.util.StringConverter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static haveno.core.locale.CountryUtil.findCountryByCode; import static haveno.core.locale.CountryUtil.getAllAmazonGiftCardCountries; import static haveno.desktop.util.FormBuilder.addComboBox; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addLabel; public class UpdateAmazonGiftCardAccountWindow extends Overlay { private final AmazonGiftCardAccount amazonGiftCardAccount; private final User user; private ComboBox countryCombo; public UpdateAmazonGiftCardAccountWindow(AmazonGiftCardAccount amazonGiftCardAccount, User user) { super(); this.amazonGiftCardAccount = amazonGiftCardAccount; this.user = user; type = Type.Attention; hideCloseButton = true; actionButtonText = Res.get("shared.save"); } @Override protected void setupKeyHandler(Scene scene) { // We do not support enter or escape here } @Override public void show() { if (headLine == null) headLine = Res.get("payment.amazonGiftCard.upgrade.headLine"); width = 868; createGridPane(); addHeadLine(); addContent(); addButtons(); applyStyles(); display(); // when there is only one possible country to choose from just go ahead and choose it. e.g. UK, US, JP etc. if (countryCombo.getItems().size() == 1) { countryCombo.setValue(countryCombo.getItems().get(0)); UserThread.runAfter(() -> actionButton.fire(), 300, TimeUnit.MILLISECONDS); } } private void addContent() { addLabel(gridPane, ++rowIndex, Res.get("payment.account.amazonGiftCard.addCountryInfo", Res.get("payment.amazonGiftCard.upgrade"), amazonGiftCardAccount.getAccountName())); addCompactTopLabelTextField(gridPane, ++rowIndex, Res.get("shared.currency"), amazonGiftCardAccount.getSingleTradeCurrency().getNameAndCode()); countryCombo = addComboBox(gridPane, ++rowIndex, Res.get("shared.country")); countryCombo.setPromptText(Res.get("payment.select.country")); countryCombo.setItems(FXCollections.observableArrayList(getAppropriateCountries(amazonGiftCardAccount.getSingleTradeCurrency().getCode()))); countryCombo.setConverter(new StringConverter<>() { @Override public String toString(Country country) { return country.name + " (" + country.code + ")"; } @Override public Country fromString(String s) { return null; } }); countryCombo.setOnAction(e -> { Country countryCode = countryCombo.getValue(); actionButton.setDisable(countryCode == null || countryCode.code == null || countryCode.code.length() < 1); }); } @Override protected void addButtons() { super.addButtons(); Country countryCode = countryCombo.getValue(); if (countryCode == null || countryCode.code == null || countryCode.code.isEmpty()) actionButton.setDisable(true); // We do not allow close in case the field is not correctly added actionButton.setOnAction(event -> { Country chosenCountryCode = countryCombo.getValue(); if (chosenCountryCode != null && chosenCountryCode.code != null && !chosenCountryCode.code.isEmpty()) { amazonGiftCardAccount.setCountry(chosenCountryCode); user.requestPersistence(); closeHandlerOptional.ifPresent(Runnable::run); hide(); } }); } public static List getAppropriateCountries(String currency) { List list = new ArrayList<>(); if (currency.equalsIgnoreCase("EUR")) { // Eurozone countries using EUR list = getAllAmazonGiftCardCountries(); list = list.stream().filter(e -> e.code.matches("FR|DE|IT|NL|ES")).collect(Collectors.toList()); } else { // non-Eurozone with own ccy HashMap mapCcyToCountry = new HashMap<>(Map.of( "AUD", "AU", "CAD", "CA", "GBP", "GB", "INR", "IN", "JPY", "JP", "SAR", "SA", "SEK", "SE", "SGD", "SG", "TRY", "TR", "USD", "US" )); Optional found = findCountryByCode(mapCcyToCountry.get(currency)); if (found.isPresent()) list.add(found.get()); } return list; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/UpdateRevolutAccountWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import haveno.core.locale.Res; import haveno.core.payment.RevolutAccount; import haveno.core.payment.validation.RevolutValidator; import haveno.core.user.User; import haveno.desktop.components.InputTextField; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.util.Layout; import javafx.scene.Scene; import static haveno.desktop.util.FormBuilder.addInputTextField; import static haveno.desktop.util.FormBuilder.addLabel; public class UpdateRevolutAccountWindow extends Overlay { private final RevolutValidator revolutValidator; private final RevolutAccount revolutAccount; private final User user; private InputTextField userNameInputTextField; public UpdateRevolutAccountWindow(RevolutAccount revolutAccount, User user) { super(); this.revolutAccount = revolutAccount; this.user = user; type = Type.Attention; hideCloseButton = true; revolutValidator = new RevolutValidator(); actionButtonText = Res.get("shared.save"); } @Override protected void setupKeyHandler(Scene scene) { // We do not support enter or escape here } @Override public void show() { if (headLine == null) headLine = Res.get("payment.revolut.addUserNameInfo.headLine"); width = 868; createGridPane(); addHeadLine(); addContent(); addButtons(); applyStyles(); display(); } private void addContent() { addLabel(gridPane, ++rowIndex, Res.get("payment.account.revolut.addUserNameInfo", Res.get("payment.revolut.info"), revolutAccount.getAccountName())); userNameInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("payment.account.username"), Layout.COMPACT_FIRST_ROW_DISTANCE); userNameInputTextField.setValidator(revolutValidator); userNameInputTextField.textProperty().addListener((observable, oldValue, newValue) -> actionButton.setDisable(!revolutValidator.validate(newValue).isValid)); } @Override protected void addButtons() { super.addButtons(); // We do not allow close in case the userName is not correctly added so we // overwrote the default handler actionButton.setOnAction(event -> { String userName = userNameInputTextField.getText(); if (revolutValidator.validate(userName).isValid) { revolutAccount.setUsername(userName); user.requestPersistence(); closeHandlerOptional.ifPresent(Runnable::run); hide(); } }); actionButton.setDisable(true); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/VerifyDisputeResultSignatureWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import haveno.core.locale.Res; import haveno.core.support.dispute.DisputeSummaryVerification; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.desktop.main.overlays.Overlay; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; import lombok.extern.slf4j.Slf4j; import static haveno.desktop.util.FormBuilder.addMultilineLabel; import static haveno.desktop.util.FormBuilder.addTopLabelTextArea; import static haveno.desktop.util.FormBuilder.addTopLabelTextField; @Slf4j public class VerifyDisputeResultSignatureWindow extends Overlay { private TextArea textArea; private TextField resultTextField; private final ArbitratorManager arbitratorManager; public VerifyDisputeResultSignatureWindow(ArbitratorManager arbitratorManager) { this.arbitratorManager = arbitratorManager; type = Type.Attention; } @Override public void show() { if (headLine == null) headLine = Res.get("support.sigCheck.popup.header"); width = 1050; createGridPane(); addHeadLine(); addContent(); addButtons(); applyStyles(); display(); textArea.textProperty().addListener((observable, oldValue, newValue) -> { try { DisputeSummaryVerification.verifySignature(newValue, arbitratorManager); resultTextField.setText(Res.get("support.sigCheck.popup.success")); } catch (Exception e) { resultTextField.setText(e.getMessage()); } }); } @Override protected void createGridPane() { gridPane = new GridPane(); gridPane.setHgap(5); gridPane.setVgap(5); gridPane.setPadding(new Insets(64, 64, 64, 64)); gridPane.setPrefWidth(width); gridPane.getStyleClass().add("popup-with-input"); ColumnConstraints columnConstraints1 = new ColumnConstraints(); columnConstraints1.setHalignment(HPos.RIGHT); columnConstraints1.setHgrow(Priority.SOMETIMES); gridPane.getColumnConstraints().addAll(columnConstraints1); } private void addContent() { addMultilineLabel(gridPane, ++rowIndex, Res.get("support.sigCheck.popup.info"), 0, width); textArea = addTopLabelTextArea(gridPane, ++rowIndex, Res.get("support.sigCheck.popup.msg.label"), Res.get("support.sigCheck.popup.msg.prompt")).second; resultTextField = addTopLabelTextField(gridPane, ++rowIndex, Res.get("support.sigCheck.popup.result")).second; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/WalletPasswordWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import static com.google.common.base.Preconditions.checkArgument; import com.google.common.base.Splitter; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.common.crypto.IncorrectPasswordException; import haveno.common.util.Tuple2; import haveno.core.api.CoreAccountService; import haveno.core.locale.Res; import haveno.core.offer.OpenOfferManager; import haveno.core.xmr.wallet.WalletsManager; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.BusyAnimation; import haveno.desktop.components.PasswordTextField; import haveno.desktop.main.SharedPresentation; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.main.overlays.popups.Popup; import static haveno.desktop.util.FormBuilder.addPasswordTextField; import static haveno.desktop.util.FormBuilder.addPrimaryActionButton; import static haveno.desktop.util.FormBuilder.addTextArea; import static haveno.desktop.util.FormBuilder.addTopLabelDatePicker; import haveno.desktop.util.Layout; import java.io.File; import java.io.IOException; import java.time.Instant; import java.time.LocalDate; import java.time.ZoneId; import java.time.ZoneOffset; import static javafx.beans.binding.Bindings.createBooleanBinding; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ChangeListener; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.DatePicker; import javafx.scene.control.Label; import javafx.scene.control.TextArea; import javafx.scene.input.KeyCode; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.crypto.MnemonicCode; import org.bitcoinj.crypto.MnemonicException; import org.bitcoinj.wallet.DeterministicSeed; @Slf4j public class WalletPasswordWindow extends Overlay { private final CoreAccountService accountService; private final WalletsManager walletsManager; private final OpenOfferManager openOfferManager; private File storageDir; private Button unlockButton; private WalletPasswordHandler passwordHandler; private PasswordTextField passwordTextField; private Button forgotPasswordButton; private Button restoreButton; private TextArea seedWordsTextArea; private DatePicker datePicker; private final SimpleBooleanProperty seedWordsValid = new SimpleBooleanProperty(false); private final BooleanProperty seedWordsEdited = new SimpleBooleanProperty(); private ChangeListener changeListener; private ChangeListener wordsTextAreaChangeListener; private ChangeListener seedWordsValidChangeListener; private boolean hideForgotPasswordButton = false; /////////////////////////////////////////////////////////////////////////////////////////// // Interface /////////////////////////////////////////////////////////////////////////////////////////// public interface WalletPasswordHandler { void onSuccess(); } @Inject private WalletPasswordWindow(CoreAccountService accountService, WalletsManager walletsManager, OpenOfferManager openOfferManager, @Named(Config.STORAGE_DIR) File storageDir) { this.accountService = accountService; this.walletsManager = walletsManager; this.openOfferManager = openOfferManager; this.storageDir = storageDir; type = Type.Attention; width = 900; } /////////////////////////////////////////////////////////////////////////////////////////// // Public API /////////////////////////////////////////////////////////////////////////////////////////// @Override public void show() { if (gridPane != null) { rowIndex = -1; gridPane.getChildren().clear(); } if (headLine == null) headLine = Res.get("walletPasswordWindow.headline"); createGridPane(); addHeadLine(); addInputFields(); addButtons(); applyStyles(); display(); } public WalletPasswordWindow onSuccess(WalletPasswordHandler passwordHandler) { this.passwordHandler = passwordHandler; return this; } public WalletPasswordWindow hideForgotPasswordButton() { this.hideForgotPasswordButton = true; return this; } @Override protected void cleanup() { if (passwordTextField != null) passwordTextField.textProperty().removeListener(changeListener); if (seedWordsValidChangeListener != null) { seedWordsValid.removeListener(seedWordsValidChangeListener); seedWordsTextArea.textProperty().removeListener(wordsTextAreaChangeListener); restoreButton.disableProperty().unbind(); restoreButton.setOnAction(null); seedWordsTextArea.setText(""); datePicker.setValue(null); seedWordsTextArea.getStyleClass().remove("validation-error"); datePicker.getStyleClass().remove("validation-error"); } } /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void setupKeyHandler(Scene scene) { if (!hideCloseButton) { scene.setOnKeyPressed(e -> { if (e.getCode() == KeyCode.ESCAPE) { e.consume(); doClose(); } }); } } private void addInputFields() { passwordTextField = addPasswordTextField(gridPane, ++rowIndex, Res.get("password.enterPassword"), Layout.FLOATING_LABEL_DISTANCE); GridPane.setColumnSpan(passwordTextField, 1); GridPane.setHalignment(passwordTextField, HPos.LEFT); changeListener = (observable, oldValue, newValue) -> unlockButton.setDisable(!passwordTextField.validate()); passwordTextField.textProperty().addListener(changeListener); } @Override protected void addButtons() { BusyAnimation busyAnimation = new BusyAnimation(false); Label deriveStatusLabel = new AutoTooltipLabel(); unlockButton = new AutoTooltipButton(Res.get("shared.unlock")); unlockButton.setDefaultButton(true); unlockButton.getStyleClass().add("action-button"); unlockButton.setDisable(true); unlockButton.setOnAction(e -> { String password = passwordTextField.getText(); checkArgument(password.length() < 500, Res.get("password.tooLong")); try { accountService.verifyPassword(password); if (passwordHandler != null) passwordHandler.onSuccess(); hide(); } catch (IncorrectPasswordException e2) { busyAnimation.stop(); deriveStatusLabel.setText(""); new Popup() .warning(Res.get("password.wrongPw")) .onClose(this::blurAgain).show(); } }); forgotPasswordButton = new AutoTooltipButton(Res.get("password.forgotPassword")); forgotPasswordButton.setOnAction(e -> { forgotPasswordButton.setDisable(true); unlockButton.setDefaultButton(false); showRestoreScreen(); }); Button cancelButton = new AutoTooltipButton(Res.get("shared.cancel")); cancelButton.setOnAction(event -> { hide(); closeHandlerOptional.ifPresent(Runnable::run); }); HBox hBox = new HBox(); hBox.setMinWidth(560); hBox.setPadding(new Insets(0, 0, 0, 0)); hBox.setSpacing(10); GridPane.setRowIndex(hBox, ++rowIndex); hBox.setAlignment(Pos.CENTER_LEFT); hBox.getChildren().add(unlockButton); if (!hideForgotPasswordButton) hBox.getChildren().add(forgotPasswordButton); if (!hideCloseButton) hBox.getChildren().add(cancelButton); hBox.getChildren().addAll(busyAnimation, deriveStatusLabel); gridPane.getChildren().add(hBox); ColumnConstraints columnConstraints1 = new ColumnConstraints(); columnConstraints1.setHalignment(HPos.LEFT); columnConstraints1.setHgrow(Priority.ALWAYS); gridPane.getColumnConstraints().addAll(columnConstraints1); } private void showRestoreScreen() { Label headLine2Label = new AutoTooltipLabel(Res.get("seed.restore.title")); headLine2Label.getStyleClass().add("popup-headline"); headLine2Label.setMouseTransparent(true); GridPane.setHalignment(headLine2Label, HPos.LEFT); GridPane.setRowIndex(headLine2Label, ++rowIndex); GridPane.setMargin(headLine2Label, new Insets(30, 0, 0, 0)); gridPane.getChildren().add(headLine2Label); seedWordsTextArea = addTextArea(gridPane, ++rowIndex, Res.get("seed.enterSeedWords"), 5); seedWordsTextArea.setPrefHeight(60); Tuple2 labelDatePickerTuple2 = addTopLabelDatePicker(gridPane, ++rowIndex, Res.get("seed.creationDate"), 10); datePicker = labelDatePickerTuple2.second; restoreButton = addPrimaryActionButton(gridPane, ++rowIndex, Res.get("seed.restore"), 0); restoreButton.setDefaultButton(true); stage.setHeight(570); // wallet creation date is not encrypted LocalDate walletCreationDate = Instant.ofEpochSecond(walletsManager.getChainSeedCreationTimeSeconds()).atZone(ZoneId.systemDefault()).toLocalDate(); log.info("walletCreationDate " + walletCreationDate); datePicker.setValue(walletCreationDate); restoreButton.disableProperty().bind(createBooleanBinding(() -> !seedWordsValid.get() || !seedWordsEdited.get(), seedWordsValid, seedWordsEdited)); seedWordsValidChangeListener = (observable, oldValue, newValue) -> { if (newValue) { seedWordsTextArea.getStyleClass().remove("validation-error"); } else { seedWordsTextArea.getStyleClass().add("validation-error"); } }; wordsTextAreaChangeListener = (observable, oldValue, newValue) -> { seedWordsEdited.set(true); try { MnemonicCode codec = new MnemonicCode(); codec.check(Splitter.on(" ").splitToList(newValue)); seedWordsValid.set(true); } catch (IOException | MnemonicException e) { seedWordsValid.set(false); } }; seedWordsValid.addListener(seedWordsValidChangeListener); seedWordsTextArea.textProperty().addListener(wordsTextAreaChangeListener); restoreButton.disableProperty().bind(createBooleanBinding(() -> !seedWordsValid.get() || !seedWordsEdited.get(), seedWordsValid, seedWordsEdited)); restoreButton.setOnAction(e -> onRestore()); seedWordsTextArea.getStyleClass().remove("validation-error"); datePicker.getStyleClass().remove("validation-error"); layout(); } private void onRestore() { if (walletsManager.hasPositiveBalance()) { new Popup().warning(Res.get("seed.warn.walletNotEmpty.msg")) .actionButtonText(Res.get("seed.warn.walletNotEmpty.restore")) .onAction(this::checkIfEncrypted) .closeButtonText(Res.get("seed.warn.walletNotEmpty.emptyWallet")) .show(); } else { checkIfEncrypted(); } } private void checkIfEncrypted() { if (walletsManager.areWalletsEncrypted()) { new Popup().information(Res.get("seed.warn.notEncryptedAnymore")) .closeButtonText(Res.get("shared.no")) .actionButtonText(Res.get("shared.yes")) .onAction(this::doRestore) .show(); } else { doRestore(); } } private void doRestore() { final LocalDate value = datePicker.getValue(); //TODO Is ZoneOffset correct? long date = value != null ? value.atStartOfDay().toEpochSecond(ZoneOffset.UTC) : 0; DeterministicSeed seed = new DeterministicSeed(Splitter.on(" ").splitToList(seedWordsTextArea.getText()), null, "", date); SharedPresentation.restoreSeedWords(walletsManager, openOfferManager, seed, storageDir); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/WebCamWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows; import haveno.common.UserThread; import haveno.core.locale.Res; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.util.FormBuilder; import javafx.beans.value.ChangeListener; import javafx.geometry.HPos; import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.GridPane; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @Slf4j public class WebCamWindow extends Overlay { @Getter private ImageView imageView = new ImageView(); private ChangeListener listener; public WebCamWindow(double width, double height) { type = Type.Feedback; imageView.setFitWidth(width); imageView.setFitHeight(height); } public void show() { headLine = Res.get("account.notifications.webCamWindow.headline"); createGridPane(); addHeadLine(); addContent(); addButtons(); applyStyles(); display(); } private void addContent() { GridPane.setHalignment(headLineLabel, HPos.CENTER); Label label = FormBuilder.addLabel(gridPane, ++rowIndex, Res.get("account.notifications.waitingForWebCam")); label.setAlignment(Pos.CENTER); GridPane.setColumnSpan(label, 2); GridPane.setHalignment(label, HPos.CENTER); GridPane.setRowIndex(imageView, rowIndex); GridPane.setColumnSpan(imageView, 2); gridPane.getChildren().add(imageView); } @Override protected void addButtons() { super.addButtons(); closeButton.setText(Res.get("shared.cancel")); listener = (observable, oldValue, newValue) -> { if (newValue != null) UserThread.execute(() -> closeButton.setText(Res.get("shared.close"))); }; imageView.imageProperty().addListener(listener); } @Override public void hide() { super.hide(); if (listener != null) imageView.imageProperty().removeListener(listener); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/downloadupdate/DisplayUpdateDownloadWindow.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows.downloadupdate; import com.google.common.base.Joiner; import static com.google.common.base.Preconditions.checkNotNull; import haveno.common.config.Config; import haveno.common.util.Utilities; import haveno.core.alert.Alert; import haveno.core.locale.Res; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.BusyAnimation; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.main.overlays.popups.Popup; import static haveno.desktop.util.FormBuilder.addLabel; import static haveno.desktop.util.FormBuilder.addMultilineLabel; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.PrintWriter; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.Scanner; import javafx.beans.value.ChangeListener; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Orientation; import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ProgressBar; import javafx.scene.control.Separator; import javafx.scene.input.KeyCode; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import lombok.extern.slf4j.Slf4j; @Slf4j public class DisplayUpdateDownloadWindow extends Overlay { private final Alert alert; private final Config config; private Optional downloadTaskOptional; private VerifyTask verifyTask; private ProgressBar progressBar; private BusyAnimation busyAnimation; /////////////////////////////////////////////////////////////////////////////////////////// // Public API /////////////////////////////////////////////////////////////////////////////////////////// public DisplayUpdateDownloadWindow(Alert alert, Config config) { this.alert = alert; this.config = config; this.type = Type.Attention; } public void show() { width = 968; // need to set headLine, otherwise the fields will not be created in addHeadLine createGridPane(); information(""); // to set regular information styling headLine = Res.get("displayUpdateDownloadWindow.headline"); addHeadLine(); addContent(); addButtons(); applyStyles(); display(); } /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// private void addContent() { checkNotNull(alert, "alertMessage must not be null"); addMultilineLabel(gridPane, ++rowIndex, alert.getMessage(), 10); Separator separator = new Separator(); separator.setMouseTransparent(true); separator.setOrientation(Orientation.HORIZONTAL); separator.getStyleClass().add("separator"); GridPane.setHalignment(separator, HPos.CENTER); GridPane.setRowIndex(separator, ++rowIndex); GridPane.setColumnSpan(separator, 2); GridPane.setMargin(separator, new Insets(20, 0, 20, 0)); gridPane.getChildren().add(separator); Button downloadButton = new AutoTooltipButton(Res.get("displayUpdateDownloadWindow.button.label")); downloadButton.getStyleClass().add("action-button"); downloadButton.setDefaultButton(true); busyAnimation = new BusyAnimation(false); Label statusLabel = new AutoTooltipLabel(); statusLabel.managedProperty().bind(statusLabel.visibleProperty()); HBox hBox = new HBox(); hBox.setSpacing(10); hBox.setAlignment(Pos.CENTER_LEFT); hBox.getChildren().addAll(downloadButton, busyAnimation, statusLabel); GridPane.setRowIndex(hBox, ++rowIndex); GridPane.setColumnIndex(hBox, 0); GridPane.setColumnSpan(hBox, 2); gridPane.getChildren().add(hBox); Label downloadingFileLabel = addLabel(gridPane, ++rowIndex, Res.get("displayUpdateDownloadWindow.downloadingFile", "")); downloadingFileLabel.setOpacity(0.2); GridPane.setHalignment(downloadingFileLabel, HPos.LEFT); progressBar = new ProgressBar(0L); progressBar.setMaxHeight(4); progressBar.managedProperty().bind(progressBar.visibleProperty()); GridPane.setRowIndex(progressBar, ++rowIndex); GridPane.setHalignment(progressBar, HPos.LEFT); GridPane.setFillWidth(progressBar, true); gridPane.getChildren().add(progressBar); final String downloadedFilesLabelTitle = Res.get("displayUpdateDownloadWindow.downloadedFiles"); Label downloadedFilesLabel = addLabel(gridPane, ++rowIndex, downloadedFilesLabelTitle); GridPane.setColumnIndex(downloadedFilesLabel, 0); GridPane.setHalignment(downloadedFilesLabel, HPos.LEFT); GridPane.setColumnSpan(downloadedFilesLabel, 2); downloadedFilesLabel.setOpacity(0.2); final String verifiedSigLabelTitle = Res.get("displayUpdateDownloadWindow.verifiedSigs"); Label verifiedSigLabel = addLabel(gridPane, ++rowIndex, verifiedSigLabelTitle); GridPane.setColumnIndex(verifiedSigLabel, 0); GridPane.setColumnSpan(verifiedSigLabel, 2); GridPane.setHalignment(verifiedSigLabel, HPos.LEFT); verifiedSigLabel.setOpacity(0.2); Separator separator2 = new Separator(); separator2.setMouseTransparent(true); separator2.setOrientation(Orientation.HORIZONTAL); separator2.getStyleClass().add("separator"); GridPane.setHalignment(separator2, HPos.CENTER); GridPane.setRowIndex(separator2, ++rowIndex); GridPane.setColumnSpan(separator2, 2); GridPane.setMargin(separator2, new Insets(20, 0, 20, 0)); gridPane.getChildren().add(separator2); HavenoInstaller installer = new HavenoInstaller(); String downloadFailedString = Res.get("displayUpdateDownloadWindow.download.failed"); downloadButton.setOnAction(e -> { if (installer.isSupportedOS()) { List downloadedFiles = new ArrayList<>(); List verifiedSigs = new ArrayList<>(); downloadButton.setDisable(true); progressBar.setVisible(true); downloadedFilesLabel.setOpacity(1); downloadingFileLabel.setOpacity(1); busyAnimation.play(); statusLabel.setText(Res.get("displayUpdateDownloadWindow.status.downloading")); // download installer downloadTaskOptional = installer.download(alert.getVersion()); if (downloadTaskOptional.isPresent()) { final DownloadTask downloadTask = downloadTaskOptional.get(); final ChangeListener downloadedFilesListener = (observable, oldValue, newValue) -> { if (!newValue.endsWith("-local")) { downloadingFileLabel.setText(Res.get("displayUpdateDownloadWindow.downloadingFile", newValue)); downloadedFilesLabel.setText(downloadedFilesLabelTitle + " " + Joiner.on(", ").join(downloadedFiles)); downloadedFiles.add(newValue); } }; downloadTask.messageProperty().addListener(downloadedFilesListener); progressBar.progressProperty().unbind(); progressBar.progressProperty().bind(downloadTask.progressProperty()); downloadTask.setOnSucceeded(workerStateEvent -> { downloadedFilesLabel.setText(downloadedFilesLabelTitle + " " + Joiner.on(", ").join(downloadedFiles)); downloadTask.messageProperty().removeListener(downloadedFilesListener); progressBar.setVisible(false); downloadingFileLabel.setText(""); downloadingFileLabel.setOpacity(0.2); statusLabel.setText(Res.get("displayUpdateDownloadWindow.status.verifying")); List downloadResults = downloadTask.getValue(); Optional downloadFailed = downloadResults.stream() .filter(fileDescriptor -> !HavenoInstaller.DownloadStatusEnum.OK.equals(fileDescriptor.getDownloadStatus())) .findFirst(); downloadedFilesLabel.getStyleClass().removeAll("error-text", "success-text"); if (downloadResults == null || downloadResults.isEmpty() || downloadFailed.isPresent()) { showErrorMessage(downloadButton, statusLabel, downloadFailedString); downloadedFilesLabel.getStyleClass().add("error-text"); } else { log.debug("Download completed successfully."); downloadedFilesLabel.getStyleClass().add("success-text"); downloadTask.getFileDescriptors().stream() .filter(fileDescriptor -> fileDescriptor.getType() == HavenoInstaller.DownloadType.JAR_HASH) .findFirst() .ifPresent(this::copyJarHashToDataDir); verifyTask = installer.verify(downloadResults); verifiedSigLabel.setOpacity(1); final ChangeListener verifiedSigLabelListener = (observable, oldValue, newValue) -> { verifiedSigs.add(newValue); verifiedSigLabel.setText(verifiedSigLabelTitle + " " + Joiner.on(", ").join(verifiedSigs)); }; verifyTask.messageProperty().addListener(verifiedSigLabelListener); verifyTask.setOnSucceeded(event -> { verifyTask.messageProperty().removeListener(verifiedSigLabelListener); statusLabel.setText(""); stopAnimations(); List verifyResults = verifyTask.getValue(); // check that there are no failed verifications Optional verifyFailed = verifyResults.stream() .filter(verifyDescriptor -> !HavenoInstaller.VerifyStatusEnum.OK.equals(verifyDescriptor.getVerifyStatusEnum())).findFirst(); if (verifyResults == null || verifyResults.isEmpty() || verifyFailed.isPresent()) { showErrorMessage(downloadButton, statusLabel, Res.get("displayUpdateDownloadWindow.verify.failed")); } else { verifiedSigLabel.getStyleClass().add("success-text"); new Popup().feedback(Res.get("displayUpdateDownloadWindow.success")) .actionButtonText(Res.get("displayUpdateDownloadWindow.download.openDir")) .onAction(() -> { try { Utilities.openFile(new File(Utilities.getDownloadOfHomeDir())); doClose(); } catch (IOException e2) { log.error(e2.getMessage()); e2.printStackTrace(); } }) .onClose(this::doClose) .show(); log.info("Download & verification succeeded."); } }); } }); } else { showErrorMessage(downloadButton, statusLabel, downloadFailedString); } } else { showErrorMessage(downloadButton, statusLabel, (Res.get("displayUpdateDownloadWindow.installer.failed"))); } }); } private void copyJarHashToDataDir(HavenoInstaller.FileDescriptor fileDescriptor) { StringBuilder sb = new StringBuilder(); final File sourceFile = fileDescriptor.getSaveFile(); try (Scanner scanner = new Scanner(new FileReader(sourceFile))) { while (scanner.hasNext()) { sb.append(scanner.next()); } scanner.close(); final String hashOfJar = sb.toString(); Path path = Paths.get(config.appDataDir.getPath(), fileDescriptor.getFileName()); final String target = path.toString(); try (PrintWriter writer = new PrintWriter(target, "UTF-8")) { writer.println(hashOfJar); writer.close(); log.info("Copied hash of jar from {} to {}", sourceFile.getAbsolutePath(), target); } catch (Exception e) { log.error(e.toString()); e.printStackTrace(); } } catch (Exception e) { log.error(e.toString()); e.printStackTrace(); } } @Override protected void addButtons() { closeButton = new AutoTooltipButton(Res.get("displayUpdateDownloadWindow.button.ignoreDownload")); closeButton.setOnAction(event -> doClose()); actionButton = new AutoTooltipButton(Res.get("displayUpdateDownloadWindow.button.downloadLater")); actionButton.setDefaultButton(false); actionButton.setOnAction(event -> { cleanup(); hide(); actionHandlerOptional.ifPresent(Runnable::run); }); HBox hBox = new HBox(); hBox.setSpacing(10); hBox.getChildren().addAll(closeButton, actionButton); GridPane.setHalignment(hBox, HPos.LEFT); GridPane.setRowIndex(hBox, ++rowIndex); GridPane.setColumnSpan(hBox, 2); GridPane.setMargin(hBox, new Insets(buttonDistance, 0, 0, 0)); gridPane.getChildren().add(hBox); } @Override protected void setupKeyHandler(Scene scene) { if (!hideCloseButton) { scene.setOnKeyPressed(e -> { if (e.getCode() == KeyCode.ESCAPE || e.getCode() == KeyCode.ENTER) { e.consume(); cleanup(); hide(); actionHandlerOptional.ifPresent(Runnable::run); } }); } } @Override protected void doClose() { super.doClose(); cleanup(); stopAnimations(); hide(); closeHandlerOptional.ifPresent(Runnable::run); } @Override protected void cleanup() { super.cleanup(); if (verifyTask != null && verifyTask.isRunning()) verifyTask.cancel(); if (downloadTaskOptional != null && downloadTaskOptional.isPresent() && downloadTaskOptional.get().isRunning()) downloadTaskOptional.get().cancel(); } private void showErrorMessage(Button downloadButton, Label statusLabel, String errorMsg) { statusLabel.setText(""); stopAnimations(); downloadButton.setDisable(false); new Popup() .headLine(Res.get("displayUpdateDownloadWindow.download.failed.headline")) .feedback(errorMsg) .onClose(this::doClose) .show(); } private void stopAnimations() { if (progressBar != null) { progressBar.progressProperty().unbind(); progressBar.setProgress(0); } if (busyAnimation != null) busyAnimation.stop(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/downloadupdate/DownloadTask.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows.downloadupdate; import com.google.common.collect.Lists; import haveno.common.file.FileUtil; import javafx.concurrent.Task; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLConnection; import java.util.List; import java.util.stream.Collectors; @Slf4j @Getter public class DownloadTask extends Task> { private static final int EOF = -1; private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; private String fileName = null; private final List fileDescriptors; private final String saveDir; /** * Prepares a task to download a file from {@code fileDescriptors} to the system's download dir. */ public DownloadTask(final HavenoInstaller.FileDescriptor fileDescriptor) { this(Lists.newArrayList(fileDescriptor)); } public DownloadTask(final HavenoInstaller.FileDescriptor fileDescriptor, final String saveDir) { this(Lists.newArrayList(fileDescriptor), saveDir); } public DownloadTask(final List fileDescriptors) { this(Lists.newArrayList(fileDescriptors), System.getProperty("java.io.tmpdir")); } /** * Prepares a task to download a file from {@code fileDescriptors} to {@code saveDir}. * * @param fileDescriptors HTTP URL of the file to be downloaded * @param saveDir path of the directory to save the file */ public DownloadTask(final List fileDescriptors, final String saveDir) { super(); this.fileDescriptors = fileDescriptors; this.saveDir = saveDir; log.info("Starting DownloadTask with file:{}, saveDir:{}, nr of files: {}", fileDescriptors, saveDir, fileDescriptors.size()); } /** * Starts the task and therefore the actual download. * * @return A reference to the created file or {@code null} if no file could be found at the provided URL * @throws IOException Forwarded exceotions from HttpURLConnection and file handling methods */ @Override protected List call() throws IOException { log.debug("DownloadTask started..."); String partialSaveFilePath = saveDir + (saveDir.endsWith(File.separator) ? "" : File.separator); // go twice over the fileDescriptors: first fill in the saveFile, then download the file and fill in the status return fileDescriptors.stream() .map(fileDescriptor -> { fileDescriptor.setSaveFile(new File(partialSaveFilePath + fileDescriptor.getFileName())); log.info("Downloading {}", fileDescriptor.getLoadUrl()); try { updateMessage(fileDescriptor.getFileName()); download(new URL(fileDescriptor.getLoadUrl()), fileDescriptor.getSaveFile()); log.info("Download for {} done", fileDescriptor.getLoadUrl()); fileDescriptor.setDownloadStatus(HavenoInstaller.DownloadStatusEnum.OK); } catch (Exception e) { fileDescriptor.setDownloadStatus(HavenoInstaller.DownloadStatusEnum.FAIL); log.error("Error downloading file:" + fileDescriptor.toString(), e); e.printStackTrace(); } return fileDescriptor; }) .collect(Collectors.toList()); } private void download(URL url, File outputFile) throws IOException { if (outputFile.exists()) { log.info("We found an existing file and rename it as *.backup."); FileUtil.renameFile(outputFile, new File(outputFile.getAbsolutePath() + ".backup")); } URLConnection urlConnection = url.openConnection(); urlConnection.connect(); int fileSize = urlConnection.getContentLength(); copyInputStreamToFileNew(urlConnection.getInputStream(), outputFile, fileSize); } public void copyInputStreamToFileNew(final InputStream source, final File destination, int fileSize) throws IOException { try { final FileOutputStream output = FileUtils.openOutputStream(destination); try { final byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; long count = 0; int n; while (EOF != (n = source.read(buffer))) { output.write(buffer, 0, n); count += n; log.trace("Progress: {}/{}", count, fileSize); updateProgress(count, fileSize); } output.close(); // don't swallow close Exception if copy completes normally } finally { IOUtils.closeQuietly(output); } } finally { IOUtils.closeQuietly(source); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/downloadupdate/HavenoInstaller.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows.downloadupdate; import com.google.common.collect.Lists; import haveno.common.util.Utilities; import lombok.Builder; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; import org.jetbrains.annotations.NotNull; import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.security.SignatureException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Optional; import static com.google.common.base.Preconditions.checkArgument; @Slf4j public class HavenoInstaller { private static final String FINGER_PRINT_MANFRED_KARRER = "F379A1C6"; private static final String FINGER_PRINT_CHRIS_BEAMS = "5BC5ED73"; private static final String FINGER_PRINT_CHRISTOPH_ATTENEDER = "29CDFD3B"; private static final String PUB_KEY_HOSTING_URL = "https://haveno.exchange/pubkey/"; private static final String DOWNLOAD_HOST_URL = "https://haveno.exchange/downloads/"; public boolean isSupportedOS() { return Utilities.isOSX() || Utilities.isWindows() || Utilities.isDebianLinux() || Utilities.isRedHatLinux(); } public Optional download(String version) { String partialUrl = DOWNLOAD_HOST_URL + "v" + version + "/"; // Get installer filename on all platforms FileDescriptor installerFileDescriptor = getInstallerDescriptor(version, partialUrl); // tells us which key was used for signing FileDescriptor signingKeyDescriptor = getSigningKeyDescriptor(partialUrl); // Hash of jar file inside of the binary FileDescriptor jarHashDescriptor = getJarHashDescriptor(version, partialUrl); List keyFileDescriptors = getKeyFileDescriptors(); List sigFileDescriptors = getSigFileDescriptors(installerFileDescriptor, keyFileDescriptors); List allFiles = Lists.newArrayList(); allFiles.add(installerFileDescriptor); allFiles.add(signingKeyDescriptor); allFiles.add(jarHashDescriptor); allFiles.addAll(keyFileDescriptors); allFiles.addAll(sigFileDescriptors); // Download keys, sigs and Installer return getDownloadTask(allFiles); } public VerifyTask verify(List fileDescriptors) { VerifyTask verifyTask = new VerifyTask(fileDescriptors); new Thread(verifyTask, "HavenoInstaller VerifyTask").start(); // TODO: check for problems when creating task return verifyTask; } private Optional getDownloadTask(List fileDescriptors) { try { return Optional.of(downloadFiles(fileDescriptors, Utilities.getDownloadOfHomeDir())); } catch (IOException exception) { return Optional.empty(); } } /** * Creates and starts a Task for background downloading * * @param fileDescriptors * @param saveDir Directory to save file to * @return The task handling the download * @throws IOException */ private static DownloadTask downloadFiles(List fileDescriptors, String saveDir) throws IOException { if (saveDir == null) saveDir = Utilities.getDownloadOfHomeDir(); DownloadTask task = new DownloadTask(fileDescriptors, saveDir); new Thread(task, "HavenoInstaller DownloadTask").start(); // TODO: check for problems when creating task return task; } /** * Verifies detached PGP signatures against GPG/openPGP RSA public keys. Does currently not work with openssl or JCA/JCE keys. * * @param pubKeyFile Path to file providing the public key to use * @param sigFile Path to detached signature file * @param dataFile Path to signed data file * @return {@code true} if signature is valid, {@code false} if signature is not valid * @throws Exception throws various exceptions in case something went wrong. Main reason should be that key or * signature could be extracted from the provided files due to a "bad" format.
    * FileNotFoundException, IOException, SignatureException, PGPException */ public static VerifyStatusEnum verifySignature(File pubKeyFile, File sigFile, File dataFile) throws Exception { InputStream inputStream; int bytesRead; PGPPublicKey publicKey; PGPSignature pgpSignature; boolean result; // Read keys from file inputStream = PGPUtil.getDecoderStream(new FileInputStream(pubKeyFile)); PGPPublicKeyRingCollection publicKeyRingCollection = new PGPPublicKeyRingCollection(inputStream, new JcaKeyFingerprintCalculator()); inputStream.close(); Iterator iterator = publicKeyRingCollection.getKeyRings(); PGPPublicKeyRing pgpPublicKeyRing; if (iterator.hasNext()) { pgpPublicKeyRing = iterator.next(); } else { throw new PGPException("Could not find public keyring in provided key file"); } // Would be the solution for multiple keys in one file // Iterator kIt; // kIt = pgpPublicKeyRing.getPublicKeys(); // publicKey = pgpPublicKeyRing.getPublicKey(0xF5B84436F379A1C6L); // Read signature from file inputStream = PGPUtil.getDecoderStream(new FileInputStream(sigFile)); PGPObjectFactory pgpObjectFactory = new PGPObjectFactory(inputStream, new JcaKeyFingerprintCalculator()); Object o = pgpObjectFactory.nextObject(); if (o instanceof PGPSignatureList) { PGPSignatureList signatureList = (PGPSignatureList) o; checkArgument(!signatureList.isEmpty(), "signatureList must not be empty"); pgpSignature = signatureList.get(0); } else if (o instanceof PGPSignature) { pgpSignature = (PGPSignature) o; } else { throw new SignatureException("Could not find signature in provided signature file"); } inputStream.close(); log.debug("KeyID used in signature: %X\n", pgpSignature.getKeyID()); publicKey = pgpPublicKeyRing.getPublicKey(pgpSignature.getKeyID()); // If signature is not matching the key used for signing we fail if (publicKey == null) return VerifyStatusEnum.FAIL; log.debug("The ID of the selected key is %X\n", publicKey.getKeyID()); pgpSignature.init(new BcPGPContentVerifierBuilderProvider(), publicKey); // Read file to verify byte[] data = new byte[1024]; inputStream = new DataInputStream(new BufferedInputStream(new FileInputStream(dataFile))); while (true) { bytesRead = inputStream.read(data, 0, 1024); if (bytesRead == -1) break; pgpSignature.update(data, 0, bytesRead); } inputStream.close(); // Verify the signature result = pgpSignature.verify(); return result ? VerifyStatusEnum.OK : VerifyStatusEnum.FAIL; } @NotNull private FileDescriptor getInstallerDescriptor(String version, String partialUrl) { String fileName; String prefix = "Haveno-"; // https://github.com/bisq-network/exchange/releases/download/v0.5.1/Haveno-0.5.1.dmg if (Utilities.isOSX()) fileName = prefix + version + ".dmg"; else if (Utilities.isWindows()) fileName = prefix + Utilities.getOSArchitecture() + "bit-" + version + ".exe"; else if (Utilities.isDebianLinux()) fileName = prefix + Utilities.getOSArchitecture() + "bit-" + version + ".deb"; else if (Utilities.isRedHatLinux()) fileName = prefix + Utilities.getOSArchitecture() + "bit-" + version + ".rpm"; else throw new RuntimeException("No suitable install package available for your OS."); return FileDescriptor.builder() .type(DownloadType.INSTALLER) .fileName(fileName) .id(fileName) .loadUrl(partialUrl.concat(fileName)) .build(); } @NotNull private FileDescriptor getSigningKeyDescriptor(String url) { String fileName = "signingkey.asc"; return FileDescriptor.builder() .type(DownloadType.SIGNING_KEY) .fileName(fileName) .id(fileName) .loadUrl(url.concat(fileName)) .build(); } @NotNull private FileDescriptor getJarHashDescriptor(String version, String partialUrl) { String fileName = "Haveno-" + version + ".jar.txt"; return FileDescriptor.builder() .type(DownloadType.JAR_HASH) .fileName(fileName) .id(fileName) .loadUrl(partialUrl.concat(fileName)) .build(); } /** * The files containing the gpg keys of the haveno signers. * Currently these are 2 hard-coded keys, one included with haveno and the same key online for maximum security. * * @return list of keys to check agains corresponding sigs. */ private List getKeyFileDescriptors() { List list = new ArrayList<>(); list.add(getKeyFileDescriptor(FINGER_PRINT_MANFRED_KARRER)); list.add(getLocalKeyFileDescriptor(FINGER_PRINT_MANFRED_KARRER)); list.add(getKeyFileDescriptor(FINGER_PRINT_CHRIS_BEAMS)); list.add(getLocalKeyFileDescriptor(FINGER_PRINT_CHRIS_BEAMS)); list.add(getKeyFileDescriptor(FINGER_PRINT_CHRISTOPH_ATTENEDER)); list.add(getLocalKeyFileDescriptor(FINGER_PRINT_CHRISTOPH_ATTENEDER)); return list; } private FileDescriptor getKeyFileDescriptor(String fingerPrint) { final String fileName = fingerPrint + ".asc"; return FileDescriptor.builder() .type(DownloadType.KEY) .fileName(fileName) .id(fingerPrint) .loadUrl(PUB_KEY_HOSTING_URL + fileName) .build(); } private FileDescriptor getLocalKeyFileDescriptor(String fingerPrint) { return FileDescriptor.builder() .type(DownloadType.KEY) .fileName(fingerPrint + ".asc-local") .id(fingerPrint) .loadUrl(getClass().getResource("/keys/" + fingerPrint + ".asc").toExternalForm()) .build(); } /** * There is one installer file, X keys and X sigs. The id links the sig to its key. * If we switch to multiple keys, the filename should also be key-dependent (filename.F1234.asc). * * @param installerFileDescriptor which installer file should this signatures be linked to? * @return */ public List getSigFileDescriptors(FileDescriptor installerFileDescriptor, List keys) { String suffix = ".asc"; List result = Lists.newArrayList(); for (FileDescriptor key : keys) { if (result.stream().noneMatch(e -> e.getId().equals(key.getId()))) { result.add(FileDescriptor.builder() .type(DownloadType.SIG) .fileName(installerFileDescriptor.getFileName().concat(suffix)) .id(key.getId()) .loadUrl(installerFileDescriptor.getLoadUrl().concat(suffix)) .build()); } else { log.debug("We have already a file with the key: {}", key.getId()); } } return result; } @Data @Builder public static class FileDescriptor { private String id; private DownloadType type; private String loadUrl; private String fileName; private File saveFile; @Builder.Default private DownloadStatusEnum downloadStatus = DownloadStatusEnum.UNKNOWN; } @Data @Builder public static class VerifyDescriptor { private File keyFile; private File sigFile; @Builder.Default private VerifyStatusEnum verifyStatusEnum = VerifyStatusEnum.UNKNOWN; } public enum DownloadStatusEnum { OK, FAIL, TIMEOUT, UNKNOWN } public enum VerifyStatusEnum { OK, FAIL, UNKNOWN } public enum DownloadType { INSTALLER, KEY, SIG, SIGNING_KEY, MISC, JAR_HASH } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/overlays/windows/downloadupdate/VerifyTask.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows.downloadupdate; /** * A Task to verify the downloaded haveno installer against the available keys/signatures. */ import com.google.common.collect.Lists; import javafx.concurrent.Task; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.io.FileReader; import java.io.IOException; import java.util.List; import java.util.Optional; import java.util.Scanner; import java.util.stream.Collectors; @Slf4j @Getter public class VerifyTask extends Task> { private final List fileDescriptors; /** * Prepares a task to download a file from {@code fileDescriptors} to {@code saveDir}. * * @param fileDescriptors HTTP URL of the file to be downloaded */ public VerifyTask(final List fileDescriptors) { super(); this.fileDescriptors = fileDescriptors; log.info("Starting VerifyTask with files:{}", fileDescriptors); } /** * Starts the task and therefore the actual download. * * @return A reference to the created file or {@code null} if no file could be found at the provided URL * @throws IOException Forwarded exceotions from HttpURLConnection and file handling methods */ @Override protected List call() { log.debug("VerifyTask started..."); Optional installer = fileDescriptors.stream() .filter(fileDescriptor -> HavenoInstaller.DownloadType.INSTALLER.equals(fileDescriptor.getType())) .findFirst(); if (!installer.isPresent()) { log.error("No installer file found."); return Lists.newArrayList(); } Optional signingKeyOptional = fileDescriptors.stream() .filter(fileDescriptor -> HavenoInstaller.DownloadType.SIGNING_KEY.equals(fileDescriptor.getType())) .findAny(); List verifyDescriptors = Lists.newArrayList(); if (signingKeyOptional.isPresent()) { final HavenoInstaller.FileDescriptor signingKeyFD = signingKeyOptional.get(); StringBuilder sb = new StringBuilder(); try { Scanner scanner = new Scanner(new FileReader(signingKeyFD.getSaveFile())); while (scanner.hasNext()) { sb.append(scanner.next()); } scanner.close(); } catch (Exception e) { log.error(e.toString()); e.printStackTrace(); HavenoInstaller.VerifyDescriptor.VerifyDescriptorBuilder verifyDescriptorBuilder = HavenoInstaller.VerifyDescriptor.builder(); verifyDescriptorBuilder.verifyStatusEnum(HavenoInstaller.VerifyStatusEnum.FAIL); verifyDescriptors.add(verifyDescriptorBuilder.build()); return verifyDescriptors; } String signingKey = sb.toString(); List sigs = fileDescriptors.stream() .filter(fileDescriptor -> HavenoInstaller.DownloadType.SIG.equals(fileDescriptor.getType())) .collect(Collectors.toList()); // iterate all signatures available to us for (HavenoInstaller.FileDescriptor sig : sigs) { HavenoInstaller.VerifyDescriptor.VerifyDescriptorBuilder verifyDescriptorBuilder = HavenoInstaller.VerifyDescriptor.builder().sigFile(sig.getSaveFile()); // Sigs are linked to keys, extract all keys which have the same id List keys = fileDescriptors.stream() .filter(keyDescriptor -> HavenoInstaller.DownloadType.KEY.equals(keyDescriptor.getType())) .filter(keyDescriptor -> sig.getId().equals(keyDescriptor.getId())) .collect(Collectors.toList()); // iterate all keys which have the same id for (HavenoInstaller.FileDescriptor key : keys) { if (signingKey.equals(key.getId())) { verifyDescriptorBuilder.keyFile(key.getSaveFile()); try { verifyDescriptorBuilder.verifyStatusEnum(HavenoInstaller.verifySignature(key.getSaveFile(), sig.getSaveFile(), installer.get().getSaveFile())); updateMessage(key.getFileName()); } catch (Exception e) { verifyDescriptorBuilder.verifyStatusEnum(HavenoInstaller.VerifyStatusEnum.FAIL); log.error(e.toString()); e.printStackTrace(); } verifyDescriptors.add(verifyDescriptorBuilder.build()); } else { log.trace("key not matching the defined in signingKey. We try the next."); } } } } else { log.error("signingKey is not found"); HavenoInstaller.VerifyDescriptor.VerifyDescriptorBuilder verifyDescriptorBuilder = HavenoInstaller.VerifyDescriptor.builder(); verifyDescriptorBuilder.verifyStatusEnum(HavenoInstaller.VerifyStatusEnum.FAIL); verifyDescriptors.add(verifyDescriptorBuilder.build()); } return verifyDescriptors; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/PortfolioView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/PortfolioView.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio; import com.google.inject.Inject; import haveno.common.UserThread; import haveno.core.locale.Res; import haveno.core.offer.OfferPayload; import haveno.core.offer.OpenOffer; import haveno.core.trade.Trade; import haveno.core.trade.failed.FailedTradesManager; import haveno.desktop.Navigation; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.common.view.CachingViewLoader; import haveno.desktop.common.view.FxmlView; import haveno.desktop.common.view.View; import haveno.desktop.main.MainView; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.portfolio.cloneoffer.CloneOfferView; import haveno.desktop.main.portfolio.closedtrades.ClosedTradesView; import haveno.desktop.main.portfolio.duplicateoffer.DuplicateOfferView; import haveno.desktop.main.portfolio.editoffer.EditOfferView; import haveno.desktop.main.portfolio.failedtrades.FailedTradesView; import haveno.desktop.main.portfolio.openoffer.OpenOffersView; import haveno.desktop.main.portfolio.pendingtrades.PendingTradesView; import java.util.List; import javafx.beans.value.ChangeListener; import javafx.collections.ListChangeListener; import javafx.fxml.FXML; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javax.annotation.Nullable; @FxmlView public class PortfolioView extends ActivatableView { @FXML Tab openOffersTab, pendingTradesTab, closedTradesTab; private Tab editOpenOfferTab, duplicateOfferTab, cloneOpenOfferTab; private final Tab failedTradesTab = new Tab(Res.get("portfolio.tab.failed")); private Tab currentTab; private Navigation.Listener navigationListener; private ChangeListener tabChangeListener; private ListChangeListener tabListChangeListener; private final CachingViewLoader viewLoader; private final Navigation navigation; private final FailedTradesManager failedTradesManager; private EditOfferView editOfferView; private DuplicateOfferView duplicateOfferView; private CloneOfferView cloneOfferView; private boolean editOpenOfferViewOpen, cloneOpenOfferViewOpen; private OpenOffer openOffer; private OpenOffersView openOffersView; private boolean tabListChangeListenerAdded = false; @Inject public PortfolioView(CachingViewLoader viewLoader, Navigation navigation, FailedTradesManager failedTradesManager) { this.viewLoader = viewLoader; this.navigation = navigation; this.failedTradesManager = failedTradesManager; } @Override public void initialize() { root.setTabClosingPolicy(TabPane.TabClosingPolicy.ALL_TABS); failedTradesTab.setClosable(false); openOffersTab.setText(Res.get("portfolio.tab.openOffers")); pendingTradesTab.setText(Res.get("portfolio.tab.pendingTrades")); closedTradesTab.setText(Res.get("portfolio.tab.history")); navigationListener = (viewPath, data) -> { if (viewPath.size() == 3 && viewPath.indexOf(PortfolioView.class) == 1) loadView(viewPath.tip(), data); }; tabChangeListener = (ov, oldValue, newValue) -> { if (newValue == openOffersTab) navigation.navigateTo(MainView.class, PortfolioView.class, OpenOffersView.class); else if (newValue == pendingTradesTab) navigation.navigateTo(MainView.class, PortfolioView.class, PendingTradesView.class); else if (newValue == closedTradesTab) navigation.navigateTo(MainView.class, PortfolioView.class, ClosedTradesView.class); else if (newValue == failedTradesTab) navigation.navigateTo(MainView.class, PortfolioView.class, FailedTradesView.class); else if (newValue == editOpenOfferTab) navigation.navigateTo(MainView.class, PortfolioView.class, EditOfferView.class); else if (newValue == duplicateOfferTab) { navigation.navigateTo(MainView.class, PortfolioView.class, DuplicateOfferView.class); } else if (newValue == cloneOpenOfferTab) { navigation.navigateTo(MainView.class, PortfolioView.class, CloneOfferView.class); } if (oldValue != null && oldValue == editOpenOfferTab) editOfferView.onTabSelected(false); if (oldValue != null && oldValue == duplicateOfferTab) duplicateOfferView.onTabSelected(false); if (oldValue != null && oldValue == cloneOpenOfferTab) cloneOfferView.onTabSelected(false); }; tabListChangeListener = change -> { change.next(); List removedTabs = change.getRemoved(); if (removedTabs.size() == 1 && removedTabs.get(0).equals(editOpenOfferTab)) onEditOpenOfferRemoved(); if (removedTabs.size() == 1 && removedTabs.get(0).equals(duplicateOfferTab)) onDuplicateOfferRemoved(); if (removedTabs.size() == 1 && removedTabs.get(0).equals(cloneOpenOfferTab)) onCloneOpenOfferRemoved(); }; } private void onEditOpenOfferRemoved() { editOpenOfferViewOpen = false; if (editOfferView != null) { editOfferView.onClose(); editOfferView = null; } navigation.navigateTo(MainView.class, this.getClass(), OpenOffersView.class); } private void onDuplicateOfferRemoved() { if (duplicateOfferView != null) { duplicateOfferView.onClose(); duplicateOfferView = null; } navigation.navigateTo(MainView.class, this.getClass(), OpenOffersView.class); } private void onCloneOpenOfferRemoved() { cloneOpenOfferViewOpen = false; if (cloneOfferView != null) { cloneOfferView.onClose(); cloneOfferView = null; } navigation.navigateTo(MainView.class, this.getClass(), OpenOffersView.class); } @Override protected void activate() { failedTradesManager.getObservableList().addListener((ListChangeListener) c -> { UserThread.execute(() -> { if (failedTradesManager.getObservableList().size() > 0 && root.getTabs().size() == 3) root.getTabs().add(failedTradesTab); }); }); if (failedTradesManager.getObservableList().size() > 0 && root.getTabs().size() == 3) root.getTabs().add(failedTradesTab); root.getSelectionModel().selectedItemProperty().addListener(tabChangeListener); if (!tabListChangeListenerAdded) { root.getTabs().addListener(tabListChangeListener); tabListChangeListenerAdded = true; // add listener only once } navigation.addListener(navigationListener); if (root.getSelectionModel().getSelectedItem() == openOffersTab) navigation.navigateTo(MainView.class, PortfolioView.class, OpenOffersView.class); else if (root.getSelectionModel().getSelectedItem() == pendingTradesTab) navigation.navigateTo(MainView.class, PortfolioView.class, PendingTradesView.class); else if (root.getSelectionModel().getSelectedItem() == closedTradesTab) navigation.navigateTo(MainView.class, PortfolioView.class, ClosedTradesView.class); else if (root.getSelectionModel().getSelectedItem() == failedTradesTab) navigation.navigateTo(MainView.class, PortfolioView.class, FailedTradesView.class); else if (root.getSelectionModel().getSelectedItem() == editOpenOfferTab) { navigation.navigateTo(MainView.class, PortfolioView.class, EditOfferView.class); if (editOfferView != null) editOfferView.onTabSelected(true); } else if (root.getSelectionModel().getSelectedItem() == duplicateOfferTab) { navigation.navigateTo(MainView.class, PortfolioView.class, DuplicateOfferView.class); if (duplicateOfferView != null) duplicateOfferView.onTabSelected(true); } else if (root.getSelectionModel().getSelectedItem() == cloneOpenOfferTab) { navigation.navigateTo(MainView.class, PortfolioView.class, CloneOfferView.class); if (cloneOfferView != null) cloneOfferView.onTabSelected(true); } } @Override protected void deactivate() { root.getSelectionModel().selectedItemProperty().removeListener(tabChangeListener); navigation.removeListener(navigationListener); currentTab = null; } private void loadView(Class viewClass, @Nullable Object data) { // nullify current tab to trigger activate/deactivate if (currentTab != null) currentTab.setContent(null); View view = viewLoader.load(viewClass); if (view instanceof OpenOffersView) { selectOpenOffersView((OpenOffersView) view); } else if (view instanceof PendingTradesView) { currentTab = pendingTradesTab; } else if (view instanceof ClosedTradesView) { currentTab = closedTradesTab; } else if (view instanceof FailedTradesView) { currentTab = failedTradesTab; } else if (view instanceof EditOfferView) { if (data instanceof OpenOffer) { openOffer = (OpenOffer) data; } if (openOffer != null) { if (editOfferView == null) { editOfferView = (EditOfferView) view; editOfferView.applyOpenOffer(openOffer); editOpenOfferTab = new Tab(Res.get("portfolio.tab.editOpenOffer").toUpperCase()); editOfferView.setCloseHandler(() -> { UserThread.execute(() -> root.getTabs().remove(editOpenOfferTab)); }); root.getTabs().add(editOpenOfferTab); } if (currentTab != editOpenOfferTab) editOfferView.onTabSelected(true); currentTab = editOpenOfferTab; } else { view = viewLoader.load(OpenOffersView.class); selectOpenOffersView((OpenOffersView) view); } } else if (view instanceof DuplicateOfferView) { if (duplicateOfferView == null && data instanceof OfferPayload && data != null) { viewLoader.removeFromCache(viewClass); // remove cached dialog view = viewLoader.load(viewClass); // and load a fresh one duplicateOfferView = (DuplicateOfferView) view; duplicateOfferView.initWithData((OfferPayload) data); duplicateOfferTab = new Tab(Res.get("portfolio.tab.duplicateOffer").toUpperCase()); duplicateOfferView.setCloseHandler(() -> { UserThread.execute(() -> root.getTabs().remove(duplicateOfferTab)); }); root.getTabs().add(duplicateOfferTab); } if (duplicateOfferView != null) { if (currentTab != duplicateOfferTab) duplicateOfferView.onTabSelected(true); currentTab = duplicateOfferTab; } else { view = viewLoader.load(OpenOffersView.class); selectOpenOffersView((OpenOffersView) view); } } else if (view instanceof CloneOfferView) { if (data instanceof OpenOffer) { openOffer = (OpenOffer) data; } if (openOffer != null) { if (cloneOfferView == null) { cloneOfferView = (CloneOfferView) view; cloneOfferView.applyOpenOffer(openOffer); cloneOpenOfferTab = new Tab(Res.get("portfolio.tab.cloneOpenOffer").toUpperCase()); cloneOfferView.setCloseHandler(() -> { root.getTabs().remove(cloneOpenOfferTab); }); root.getTabs().add(cloneOpenOfferTab); } if (currentTab != cloneOpenOfferTab) cloneOfferView.onTabSelected(true); currentTab = cloneOpenOfferTab; } else { view = viewLoader.load(OpenOffersView.class); selectOpenOffersView((OpenOffersView) view); } } currentTab.setContent(view.getRoot()); root.getSelectionModel().select(currentTab); } private void selectOpenOffersView(OpenOffersView view) { openOffersView = view; currentTab = openOffersTab; EditOpenOfferHandler editOpenOfferHandler = openOffer -> { if (!editOpenOfferViewOpen) { editOpenOfferViewOpen = true; PortfolioView.this.openOffer = openOffer; navigation.navigateTo(MainView.class, PortfolioView.this.getClass(), EditOfferView.class); } else { new Popup().warning(Res.get("editOffer.openTabWarning")).show(); } }; openOffersView.setEditOpenOfferHandler(editOpenOfferHandler); CloneOpenOfferHandler cloneOpenOfferHandler = openOffer -> { if (!cloneOpenOfferViewOpen) { cloneOpenOfferViewOpen = true; PortfolioView.this.openOffer = openOffer; navigation.navigateTo(MainView.class, PortfolioView.this.getClass(), CloneOfferView.class); } else { new Popup().warning(Res.get("cloneOffer.openTabWarning")).show(); } }; openOffersView.setCloneOpenOfferHandler(cloneOpenOfferHandler); } public interface EditOpenOfferHandler { void onEditOpenOffer(OpenOffer openOffer); } public interface CloneOpenOfferHandler { void onCloneOpenOffer(OpenOffer openOffer); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.cloneoffer; import haveno.desktop.Navigation; import haveno.desktop.main.offer.MutableOfferDataModel; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TradeCurrency; import haveno.core.offer.CreateOfferService; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OfferUtil; import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.PaymentAccount; import haveno.core.proto.persistable.CorePersistenceProtoResolver; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.P2PService; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import com.google.inject.Inject; import com.google.inject.name.Named; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; class CloneOfferDataModel extends MutableOfferDataModel { private final CorePersistenceProtoResolver corePersistenceProtoResolver; private OpenOffer sourceOpenOffer; @Inject CloneOfferDataModel(CreateOfferService createOfferService, OpenOfferManager openOfferManager, OfferUtil offerUtil, XmrWalletService xmrWalletService, Preferences preferences, User user, P2PService p2PService, PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter xmrFormatter, CorePersistenceProtoResolver corePersistenceProtoResolver, TradeStatisticsManager tradeStatisticsManager, Navigation navigation) { super(createOfferService, openOfferManager, offerUtil, xmrWalletService, preferences, user, p2PService, priceFeedService, accountAgeWitnessService, xmrFormatter, tradeStatisticsManager, navigation); this.corePersistenceProtoResolver = corePersistenceProtoResolver; } public void reset() { direction = null; tradeCurrency = null; tradeCurrencyCode.set(null); useMarketBasedPrice.set(false); amount.set(null); minAmount.set(null); price.set(null); volume.set(null); minVolume.set(null); securityDepositPct.set(0); paymentAccounts.clear(); paymentAccount = null; marketPriceMarginPct = 0; sourceOpenOffer = null; } public void applyOpenOffer(OpenOffer openOffer) { this.sourceOpenOffer = openOffer; Offer offer = openOffer.getOffer(); direction = offer.getDirection(); CurrencyUtil.getTradeCurrency(offer.getCounterCurrencyCode()) .ifPresent(c -> this.tradeCurrency = c); tradeCurrencyCode.set(offer.getCounterCurrencyCode()); PaymentAccount tmpPaymentAccount = user.getPaymentAccount(openOffer.getOffer().getMakerPaymentAccountId()); Optional optionalTradeCurrency = CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCounterCurrencyCode()); if (optionalTradeCurrency.isPresent() && tmpPaymentAccount != null) { TradeCurrency selectedTradeCurrency = optionalTradeCurrency.get(); this.paymentAccount = PaymentAccount.fromProto(tmpPaymentAccount.toProtoMessage(), corePersistenceProtoResolver); if (paymentAccount.getSingleTradeCurrency() != null) paymentAccount.setSingleTradeCurrency(selectedTradeCurrency); else paymentAccount.setSelectedTradeCurrency(selectedTradeCurrency); } allowAmountUpdate = false; } public boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency) { try { return super.initWithData(direction, tradeCurrency, false); } catch (NullPointerException e) { if (e.getMessage().contains("tradeCurrency")) { throw new IllegalArgumentException("Offers of removed assets cannot be edited. You can only cancel it.", e); } return false; } } @Override protected Set getUserPaymentAccounts() { return Objects.requireNonNull(user.getPaymentAccounts()).stream() .filter(account -> !account.getPaymentMethod().isBsqSwap()) .collect(Collectors.toSet()); } @Override protected PaymentAccount getPreselectedPaymentAccount() { return paymentAccount; } public void populateData() { Offer offer = sourceOpenOffer.getOffer(); // Min amount need to be set before amount as if minAmount is null it would be set by amount setMinAmount(offer.getMinAmount()); setAmount(offer.getAmount()); setPrice(offer.getPrice()); setVolume(offer.getVolume()); setUseMarketBasedPrice(offer.isUseMarketBasedPrice()); setTriggerPrice(sourceOpenOffer.getTriggerPrice()); if (offer.isUseMarketBasedPrice()) { setMarketPriceMarginPct(offer.getMarketPriceMarginPct()); } setBuyerAsTakerWithoutDeposit(offer.hasBuyerAsTakerWithoutDeposit()); setSecurityDepositPct(getSecurityAsPercent(offer)); setExtraInfo(offer.getOfferExtraInfo()); } public void onCloneOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { Offer clonedOffer = createClonedOffer(); openOfferManager.placeOffer(clonedOffer, false, triggerPrice, false, true, sourceOpenOffer.getId(), transaction -> resultHandler.handleResult(), errorMessageHandler); } private Offer createClonedOffer() { return createOfferService.createClonedOffer(sourceOpenOffer.getOffer(), tradeCurrencyCode.get(), useMarketBasedPrice.get() ? null : price.get(), useMarketBasedPrice.get(), useMarketBasedPrice.get() ? marketPriceMarginPct : 0, paymentAccount, extraInfo.get()); } public boolean hasConflictingClone() { Offer clonedOffer = createClonedOffer(); return openOfferManager.hasConflictingClone(clonedOffer, sourceOpenOffer); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferView.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.cloneoffer; import haveno.desktop.Navigation; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.BusyAnimation; import haveno.desktop.main.offer.MutableOfferView; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.OfferDetailsWindow; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.offer.OpenOffer; import haveno.core.payment.PaymentAccount; import haveno.core.user.DontShowAgainLookup; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.common.UserThread; import haveno.common.util.Tuple4; import com.google.inject.Inject; import com.google.inject.name.Named; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.image.ImageView; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.collections.ObservableList; import static haveno.desktop.util.FormBuilder.addButtonBusyAnimationLabelAfterGroup; @FxmlView public class CloneOfferView extends MutableOfferView { private BusyAnimation busyAnimation; private Button cloneButton; private Button cancelButton; private Label spinnerInfoLabel; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject private CloneOfferView(CloneOfferViewModel model, Navigation navigation, Preferences preferences, OfferDetailsWindow offerDetailsWindow, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter) { super(model, navigation, preferences, offerDetailsWindow, btcFormatter); } @Override protected void initialize() { super.initialize(); addCloneGroup(); renameAmountGroup(); } private void renameAmountGroup() { amountTitledGroupBg.setText(Res.get("editOffer.setPrice")); } @Override protected void doSetFocus() { // Don't focus in any field before data was set } @Override protected void doActivate() { super.doActivate(); addBindings(); hideOptionsGroup(); hideNextButtons(); // Lock amount field as it would require bigger changes to support increased amount values. amountTextField.setDisable(true); amountBtcLabel.setDisable(true); minAmountTextField.setDisable(true); minAmountBtcLabel.setDisable(true); volumeTextField.setDisable(true); volumeCurrencyLabel.setDisable(true); // Workaround to fix margin on top of amount group gridPane.setPadding(new Insets(-20, 25, 25, 25)); updatePriceToggle(); updateElementsWithDirection(); model.isNextButtonDisabled.setValue(false); cancelButton.setDisable(false); model.onInvalidateMarketPriceMargin(); model.onInvalidatePrice(); // To force re-validation of payment account validation onPaymentAccountsComboBoxSelected(); } @Override protected void deactivate() { super.deactivate(); removeBindings(); } @Override public void onClose() { } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void applyOpenOffer(OpenOffer openOffer) { model.applyOpenOffer(openOffer); initWithData(openOffer.getOffer().getDirection(), CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCounterCurrencyCode()).get(), false, null); if (!model.isSecurityDepositValid()) { new Popup().warning(Res.get("editOffer.invalidDeposit")) .onClose(this::close) .show(); } } /////////////////////////////////////////////////////////////////////////////////////////// // Bindings, Listeners /////////////////////////////////////////////////////////////////////////////////////////// private void addBindings() { cloneButton.disableProperty().bind(model.isNextButtonDisabled); } private void removeBindings() { cloneButton.disableProperty().unbind(); } @Override protected ObservableList filterPaymentAccounts(ObservableList paymentAccounts) { return paymentAccounts; } /////////////////////////////////////////////////////////////////////////////////////////// // Build UI elements /////////////////////////////////////////////////////////////////////////////////////////// private void addCloneGroup() { Tuple4 tuple4 = addButtonBusyAnimationLabelAfterGroup(gridPane, 6, Res.get("cloneOffer.clone")); HBox hBox = tuple4.fourth; hBox.setAlignment(Pos.CENTER_LEFT); GridPane.setHalignment(hBox, HPos.LEFT); cloneButton = tuple4.first; cloneButton.setMinHeight(40); cloneButton.setPadding(new Insets(0, 20, 0, 20)); cloneButton.setGraphicTextGap(10); busyAnimation = tuple4.second; spinnerInfoLabel = tuple4.third; cancelButton = new AutoTooltipButton(Res.get("shared.cancel")); cancelButton.setDefaultButton(false); cancelButton.setOnAction(event -> close()); hBox.getChildren().add(cancelButton); cloneButton.setOnAction(e -> { cloneButton.requestFocus(); // fix issue #5460 (when enter key used, focus is wrong) onClone(); }); } private void onClone() { if (model.dataModel.hasConflictingClone()) { new Popup().warning(Res.get("cloneOffer.hasConflictingClone")) .actionButtonText(Res.get("shared.yes")) .onAction(this::doClone) .closeButtonText(Res.get("shared.no")) .show(); } else { doClone(); } } private void doClone() { if (model.isPriceInRange()) { model.isNextButtonDisabled.setValue(true); cancelButton.setDisable(true); busyAnimation.play(); spinnerInfoLabel.setText(Res.get("cloneOffer.publishOffer")); model.onCloneOffer(() -> { UserThread.execute(() -> { String key = "cloneOfferSuccess"; if (DontShowAgainLookup.showAgain(key)) { new Popup() .feedback(Res.get("cloneOffer.success")) .dontShowAgainId(key) .show(); } spinnerInfoLabel.setText(""); busyAnimation.stop(); close(); }); }, errorMessage -> { UserThread.execute(() -> { log.error(errorMessage); spinnerInfoLabel.setText(""); busyAnimation.stop(); model.isNextButtonDisabled.setValue(false); cancelButton.setDisable(false); new Popup().warning(errorMessage).show(); }); }); } } /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// private void updateElementsWithDirection() { ImageView iconView = new ImageView(); iconView.setId(model.isShownAsSellOffer() ? "image-sell-white" : "image-buy-white"); cloneButton.setGraphic(iconView); cloneButton.setId(model.isShownAsSellOffer() ? "sell-button-big" : "buy-button-big"); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.cloneoffer; import haveno.desktop.Navigation; import haveno.desktop.main.offer.MutableOfferViewModel; import haveno.desktop.main.offer.OfferViewUtil; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.offer.OfferUtil; import haveno.core.offer.OpenOffer; import haveno.core.payment.validation.FiatVolumeValidator; import haveno.core.payment.validation.SecurityDepositValidator; import haveno.core.payment.validation.XmrValidator; import haveno.core.provider.price.PriceFeedService; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.PriceUtil; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.AmountValidator4Decimals; import haveno.core.util.validation.AmountValidator8Decimals; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import com.google.inject.Inject; import com.google.inject.name.Named; class CloneOfferViewModel extends MutableOfferViewModel { @Inject public CloneOfferViewModel(CloneOfferDataModel dataModel, FiatVolumeValidator fiatVolumeValidator, AmountValidator4Decimals priceValidator4Decimals, AmountValidator8Decimals priceValidator8Decimals, XmrValidator xmrValidator, SecurityDepositValidator securityDepositValidator, PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation, Preferences preferences, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, OfferUtil offerUtil) { super(dataModel, fiatVolumeValidator, priceValidator4Decimals, priceValidator8Decimals, xmrValidator, securityDepositValidator, priceFeedService, accountAgeWitnessService, navigation, preferences, btcFormatter, offerUtil); syncMinAmountWithAmount = false; } @Override public void activate() { super.activate(); dataModel.populateData(); long triggerPriceAsLong = dataModel.getTriggerPrice(); dataModel.setTriggerPrice(triggerPriceAsLong); if (triggerPriceAsLong > 0) { triggerPrice.set(PriceUtil.formatMarketPrice(triggerPriceAsLong, dataModel.getCurrencyCode())); } else { triggerPrice.set(""); } onTriggerPriceTextFieldChanged(); } public void applyOpenOffer(OpenOffer openOffer) { dataModel.reset(); dataModel.applyOpenOffer(openOffer); } public void onCloneOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { dataModel.onCloneOffer(resultHandler, errorMessageHandler); } public void onInvalidateMarketPriceMargin() { marketPriceMargin.set(FormattingUtils.formatToPercent(dataModel.getMarketPriceMarginPct())); } public void onInvalidatePrice() { price.set(FormattingUtils.formatPrice(null)); price.set(FormattingUtils.formatPrice(dataModel.getPrice().get())); } public boolean isSecurityDepositValid() { return securityDepositValidator.validate(securityDeposit.get()).isValid; } @Override public void triggerFocusOutOnAmountFields() { // do not update BTC Amount or minAmount here // issue 2798: "after a few edits of offer the BTC amount has increased" } public boolean isShownAsSellOffer() { return OfferViewUtil.isShownAsSellOffer(getTradeCurrency(), dataModel.getDirection()); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradableListItem.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.closedtrades; import haveno.core.trade.Tradable; /** * We could remove that wrapper if it is not needed for additional UI only fields. */ class ClosedTradableListItem { private final Tradable tradable; ClosedTradableListItem(Tradable tradable) { this.tradable = tradable; } Tradable getTradable() { return tradable; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesDataModel.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.closedtrades; import com.google.inject.Inject; import haveno.common.UserThread; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.monetary.Price; import haveno.core.monetary.Volume; import haveno.core.provider.price.MarketPrice; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.ClosedTradableFormatter; import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.ClosedTradableUtil; import haveno.core.trade.Tradable; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.user.Preferences; import haveno.core.util.PriceUtil; import haveno.core.util.VolumeUtil; import haveno.desktop.common.model.ActivatableDataModel; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import java.math.BigInteger; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; class ClosedTradesDataModel extends ActivatableDataModel { final ClosedTradableManager closedTradableManager; final ClosedTradableFormatter closedTradableFormatter; private final Preferences preferences; private final PriceFeedService priceFeedService; final AccountAgeWitnessService accountAgeWitnessService; private final ObservableList list = FXCollections.observableArrayList(); private final ListChangeListener tradesListChangeListener; private final TradeManager tradeManager; @Inject public ClosedTradesDataModel(ClosedTradableManager closedTradableManager, ClosedTradableFormatter closedTradableFormatter, Preferences preferences, PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService, TradeManager tradeManager) { this.closedTradableManager = closedTradableManager; this.closedTradableFormatter = closedTradableFormatter; this.preferences = preferences; this.priceFeedService = priceFeedService; this.accountAgeWitnessService = accountAgeWitnessService; this.tradeManager = tradeManager; tradesListChangeListener = change -> applyList(); } @Override protected void activate() { applyList(); closedTradableManager.getObservableList().addListener(tradesListChangeListener); } @Override protected void deactivate() { closedTradableManager.getObservableList().removeListener(tradesListChangeListener); } ObservableList getList() { return list; } List getListAsTradables() { synchronized (list) { return list.stream().map(ClosedTradesListItem::getTradable).collect(Collectors.toList()); } } BigInteger getTotalAmount() { return ClosedTradableUtil.getTotalAmount(getListAsTradables()); } Optional getVolumeInUserFiatCurrency(BigInteger amount) { return getVolume(amount, preferences.getPreferredTradeCurrency().getCode()); } Optional getVolume(BigInteger amount, String currencyCode) { MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); if (marketPrice == null) { return Optional.empty(); } Price price = PriceUtil.marketPriceToPrice(marketPrice); return Optional.of(VolumeUtil.getVolume(amount, price)); } BigInteger getTotalTxFee() { return ClosedTradableUtil.getTotalTxFee(getListAsTradables()); } BigInteger getTotalTradeFee() { return closedTradableManager.getTotalTradeFee(getListAsTradables()); } boolean isCurrencyForTradeFeeBtc(Tradable item) { return item != null; } private void applyList() { UserThread.execute(() -> { synchronized (list) { list.clear(); list.addAll( closedTradableManager.getObservableList().stream() .map(tradable -> new ClosedTradesListItem(tradable, closedTradableFormatter, closedTradableManager)) .collect(Collectors.toList()) ); // We sort by date, the earliest first list.sort((o1, o2) -> o2.getTradable().getDate().compareTo(o1.getTradable().getDate())); } }); } public void onMoveTradeToPendingTrades(Trade trade) { tradeManager.onMoveClosedTradeToPendingTrades(trade); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesListItem.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.closedtrades; import haveno.core.locale.CurrencyUtil; import haveno.core.monetary.Price; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.ClosedTradableFormatter; import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.Tradable; import haveno.core.trade.Trade; import haveno.desktop.util.DisplayUtils; import haveno.desktop.util.filtering.FilterableListItem; import haveno.desktop.util.filtering.FilteringUtils; import lombok.Getter; import org.apache.commons.lang3.StringUtils; import java.math.BigInteger; import java.util.Date; public class ClosedTradesListItem implements FilterableListItem { @Getter private final Tradable tradable; private final ClosedTradableFormatter closedTradableFormatter; private final ClosedTradableManager closedTradableManager; public ClosedTradesListItem( Tradable tradable, ClosedTradableFormatter closedTradableFormatter, ClosedTradableManager closedTradableManager) { this.tradable = tradable; this.closedTradableFormatter = closedTradableFormatter; this.closedTradableManager = closedTradableManager; } public String getTradeId() { return tradable.getShortId(); } public BigInteger getAmount() { return tradable.getOptionalAmount().orElse(null); } public String getAmountAsString() { return closedTradableFormatter.getAmountAsString(tradable); } public Price getPrice() { return tradable.getOptionalPrice().orElse(null); } public String getPriceAsString() { return closedTradableFormatter.getPriceAsString(tradable); } public String getPriceDeviationAsString() { return closedTradableFormatter.getPriceDeviationAsString(tradable); } public String getVolumeAsString(boolean appendCode) { return closedTradableFormatter.getVolumeAsString(tradable, appendCode); } public String getVolumeCurrencyAsString() { return closedTradableFormatter.getVolumeCurrencyAsString(tradable); } public String getTxFeeAsString() { return closedTradableFormatter.getTotalTxFeeAsString(tradable); } public String getTradeFeeAsString(boolean appendCode) { return closedTradableFormatter.getTradeFeeAsString(tradable, appendCode); } public String getBuyerSecurityDepositAsString() { return closedTradableFormatter.getBuyerSecurityDepositAsString(tradable); } public String getSellerSecurityDepositAsString() { return closedTradableFormatter.getSellerSecurityDepositAsString(tradable); } public String getDirectionLabel() { Offer offer = tradable.getOffer(); OfferDirection direction = closedTradableManager.wasMyOffer(offer) || tradable instanceof ArbitratorTrade ? offer.getDirection() : offer.getMirroredDirection(); String currencyCode = tradable.getOffer().getCounterCurrencyCode(); return DisplayUtils.getDirectionWithCode(direction, currencyCode, offer.isPrivateOffer()); } public Date getDate() { return tradable.getDate(); } public String getDateAsString() { return DisplayUtils.formatDateTime(tradable.getDate()); } public String getMarketLabel() { return CurrencyUtil.getCurrencyPair(tradable.getOffer().getCounterCurrencyCode()); } public String getState() { return closedTradableFormatter.getStateAsString(tradable); } public int getNumPastTrades() { return closedTradableManager.getNumPastTrades(tradable); } @Override public boolean match(String filterString) { if (filterString.isEmpty()) { return true; } if (StringUtils.containsIgnoreCase(getDateAsString(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getMarketLabel(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getPriceAsString(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getPriceDeviationAsString(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getVolumeAsString(true), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getAmountAsString(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getTradeFeeAsString(true), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getTxFeeAsString(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getBuyerSecurityDepositAsString(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getSellerSecurityDepositAsString(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getState(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getDirectionLabel(), filterString)) { return true; } if (FilteringUtils.match(getTradable().getOffer(), filterString)) { return true; } return getTradable() instanceof Trade && FilteringUtils.match((Trade) getTradable(), filterString); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.closedtrades; import com.google.inject.Inject; import com.google.inject.name.Named; import com.googlecode.jcsv.writer.CSVEntryConverter; import com.jfoenix.controls.JFXButton; import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import haveno.common.config.Config; import haveno.common.crypto.KeyRing; import haveno.core.alert.PrivateNotificationManager; import haveno.core.locale.Res; import haveno.core.offer.Offer; import haveno.core.offer.OfferPayload; import haveno.core.offer.OpenOffer; import haveno.core.trade.Tradable; import haveno.core.trade.Trade; import haveno.core.user.Preferences; import haveno.desktop.Navigation; import haveno.desktop.common.view.ActivatableViewAndModel; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.AutoTooltipTableColumn; import haveno.desktop.components.HyperlinkWithIcon; import haveno.desktop.components.PeerInfoIconTrading; import haveno.desktop.components.list.FilterBox; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.ClosedTradesSummaryWindow; import haveno.desktop.main.overlays.windows.OfferDetailsWindow; import haveno.desktop.main.overlays.windows.TradeDetailsWindow; import haveno.desktop.main.portfolio.presentation.PortfolioUtil; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.NodeAddress; import java.util.Comparator; import java.util.function.Function; import javafx.beans.binding.Bindings; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.value.ChangeListener; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.MenuItem; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableRow; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.stage.Stage; import javafx.util.Callback; @FxmlView public class ClosedTradesView extends ActivatableViewAndModel { private final boolean useDevPrivilegeKeys; private enum ColumnNames { TRADE_ID(Res.get("shared.tradeId")), DATE(Res.get("shared.dateTime")), MARKET(Res.get("shared.market")), PRICE(Res.get("shared.price")), DEVIATION(Res.get("shared.deviation")), AMOUNT(Res.get("shared.amountWithCur", Res.getBaseCurrencyCode())), VOLUME(Res.get("shared.amount")), VOLUME_CURRENCY(Res.get("shared.currency")), TX_FEE(Res.get("shared.txFee")), TRADE_FEE(Res.get("shared.tradeFee")), BUYER_SEC(Res.get("shared.buyerSecurityDeposit")), SELLER_SEC(Res.get("shared.sellerSecurityDeposit")), OFFER_TYPE(Res.get("shared.offerType")), STATUS(Res.get("shared.state")); private final String text; ColumnNames(String text) { this.text = text; } @Override public String toString() { return text; } } @FXML TableView tableView; @FXML TableColumn priceColumn, deviationColumn, amountColumn, volumeColumn, tradeFeeColumn, buyerSecurityDepositColumn, sellerSecurityDepositColumn, marketColumn, directionColumn, dateColumn, tradeIdColumn, stateColumn, removeTradeColumn, duplicateColumn, avatarColumn; @FXML FilterBox filterBox; @FXML AutoTooltipButton exportButton, summaryButton; @FXML Label numItems; @FXML Region footerSpacer; private final OfferDetailsWindow offerDetailsWindow; private final Navigation navigation; private final KeyRing keyRing; private final Preferences preferences; private final TradeDetailsWindow tradeDetailsWindow; private final PrivateNotificationManager privateNotificationManager; private SortedList sortedList; private FilteredList filteredList; private ChangeListener widthListener; @Inject public ClosedTradesView(ClosedTradesViewModel model, OfferDetailsWindow offerDetailsWindow, Navigation navigation, KeyRing keyRing, Preferences preferences, TradeDetailsWindow tradeDetailsWindow, PrivateNotificationManager privateNotificationManager, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { super(model); this.offerDetailsWindow = offerDetailsWindow; this.navigation = navigation; this.keyRing = keyRing; this.preferences = preferences; this.tradeDetailsWindow = tradeDetailsWindow; this.privateNotificationManager = privateNotificationManager; this.useDevPrivilegeKeys = useDevPrivilegeKeys; } @Override public void initialize() { GUIUtil.applyTableStyle(tableView); widthListener = (observable, oldValue, newValue) -> onWidthChange((double) newValue); tradeFeeColumn.setGraphic(new AutoTooltipLabel(ColumnNames.TRADE_FEE.toString().replace(" BTC", ""))); buyerSecurityDepositColumn.setGraphic(new AutoTooltipLabel(ColumnNames.BUYER_SEC.toString())); sellerSecurityDepositColumn.setGraphic(new AutoTooltipLabel(ColumnNames.SELLER_SEC.toString())); priceColumn.setGraphic(new AutoTooltipLabel(ColumnNames.PRICE.toString())); deviationColumn.setGraphic(new AutoTooltipTableColumn<>(ColumnNames.DEVIATION.toString(), Res.get("portfolio.closedTrades.deviation.help")).getGraphic()); amountColumn.setGraphic(new AutoTooltipLabel(ColumnNames.AMOUNT.toString())); volumeColumn.setGraphic(new AutoTooltipLabel(ColumnNames.VOLUME.toString())); marketColumn.setGraphic(new AutoTooltipLabel(ColumnNames.MARKET.toString())); directionColumn.setGraphic(new AutoTooltipLabel(ColumnNames.OFFER_TYPE.toString())); dateColumn.setGraphic(new AutoTooltipLabel(ColumnNames.DATE.toString())); tradeIdColumn.setGraphic(new AutoTooltipLabel(ColumnNames.TRADE_ID.toString())); stateColumn.setGraphic(new AutoTooltipLabel(ColumnNames.STATUS.toString())); duplicateColumn.setGraphic(new AutoTooltipLabel("")); avatarColumn.setText(""); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); tableView.setPlaceholder(new AutoTooltipLabel(Res.get("table.placeholder.noItems", Res.get("shared.trades")))); setTradeIdColumnCellFactory(); setDirectionColumnCellFactory(); setAmountColumnCellFactory(); setTradeFeeColumnCellFactory(); setBuyerSecurityDepositColumnCellFactory(); setSellerSecurityDepositColumnCellFactory(); setPriceColumnCellFactory(); setDeviationColumnCellFactory(); setVolumeColumnCellFactory(); setDateColumnCellFactory(); setMarketColumnCellFactory(); setStateColumnCellFactory(); setRemoveTradeColumnCellFactory(); setDuplicateColumnCellFactory(); setAvatarColumnCellFactory(); tradeIdColumn.setComparator(Comparator.comparing(ClosedTradesListItem::getTradeId)); dateColumn.setComparator(Comparator.comparing(ClosedTradesListItem::getDate)); directionColumn.setComparator(Comparator.comparing(o -> o.getTradable().getOffer().getDirection())); marketColumn.setComparator(Comparator.comparing(ClosedTradesListItem::getMarketLabel)); priceColumn.setComparator(Comparator.comparing(ClosedTradesListItem::getPrice, Comparator.nullsFirst(Comparator.naturalOrder()))); deviationColumn.setComparator(Comparator.comparing(o -> o.getTradable().getOffer().isUseMarketBasedPrice() ? o.getTradable().getOffer().getMarketPriceMarginPct() : 1, Comparator.nullsFirst(Comparator.naturalOrder()))); volumeColumn.setComparator(nullsFirstComparingAsTrade(Trade::getVolume)); amountColumn.setComparator(Comparator.comparing(ClosedTradesListItem::getAmount, Comparator.nullsFirst(Comparator.naturalOrder()))); avatarColumn.setComparator(Comparator.comparing(ClosedTradesListItem::getNumPastTrades, Comparator.nullsFirst(Comparator.naturalOrder()))); // tradeFeeColumn.setComparator(Comparator.comparing(item -> { String tradeFee = item.getTradeFeeAsString(true); return "BTC" + tradeFee; }, Comparator.nullsFirst(Comparator.naturalOrder()))); buyerSecurityDepositColumn.setComparator(nullsFirstComparing(o -> o.getTradable().getOffer() != null ? o.getTradable().getOffer().getMaxBuyerSecurityDeposit() : null )); sellerSecurityDepositColumn.setComparator(nullsFirstComparing(o -> o.getTradable().getOffer() != null ? o.getTradable().getOffer().getMaxSellerSecurityDeposit() : null )); stateColumn.setComparator(Comparator.comparing(ClosedTradesListItem::getState)); dateColumn.setSortType(TableColumn.SortType.DESCENDING); tableView.getSortOrder().add(dateColumn); tableView.setRowFactory( tableView -> { TableRow row = new TableRow<>(); ContextMenu rowMenu = new ContextMenu(); MenuItem duplicateItem = new MenuItem(Res.get("portfolio.context.offerLikeThis")); duplicateItem.setOnAction((ActionEvent event) -> onDuplicateOffer(row.getItem().getTradable().getOffer())); rowMenu.getItems().add(duplicateItem); row.contextMenuProperty().bind( Bindings.when(Bindings.isNotNull(row.itemProperty())) .then(rowMenu) .otherwise((ContextMenu) null)); return row; }); numItems.setId("num-offers"); numItems.setPadding(new Insets(-5, 0, 0, 10)); HBox.setHgrow(footerSpacer, Priority.ALWAYS); HBox.setMargin(exportButton, new Insets(0, 10, 0, 0)); exportButton.updateText(Res.get("shared.exportCSV")); summaryButton.updateText(Res.get("shared.summary")); } @Override protected void activate() { filteredList = new FilteredList<>(model.dataModel.getList()); sortedList = new SortedList<>(filteredList); sortedList.comparatorProperty().bind(tableView.comparatorProperty()); tableView.setItems(sortedList); filterBox.initialize(filteredList, tableView); // here because filteredList is instantiated here filterBox.setPromptText(Res.get("shared.filter")); filterBox.activate(); numItems.setText(Res.get("shared.numItemsLabel", sortedList.size())); exportButton.setOnAction(event -> { CSVEntryConverter headerConverter = item -> { String[] columns = new String[ColumnNames.values().length]; for (ColumnNames m : ColumnNames.values()) { columns[m.ordinal()] = m.toString(); } return columns; }; CSVEntryConverter contentConverter = item -> { String[] columns = new String[ColumnNames.values().length]; columns[ColumnNames.TRADE_ID.ordinal()] = item.getTradeId(); columns[ColumnNames.DATE.ordinal()] = item.getDateAsString(); columns[ColumnNames.MARKET.ordinal()] = item.getMarketLabel(); columns[ColumnNames.PRICE.ordinal()] = item.getPriceAsString(); columns[ColumnNames.DEVIATION.ordinal()] = item.getPriceDeviationAsString(); columns[ColumnNames.AMOUNT.ordinal()] = item.getAmountAsString(); columns[ColumnNames.VOLUME.ordinal()] = item.getVolumeAsString(false); columns[ColumnNames.VOLUME_CURRENCY.ordinal()] = item.getVolumeCurrencyAsString(); columns[ColumnNames.TX_FEE.ordinal()] = item.getTxFeeAsString(); if (model.dataModel.isCurrencyForTradeFeeBtc(item.getTradable())) { columns[ColumnNames.TRADE_FEE.ordinal()] = item.getTradeFeeAsString(false); } else { columns[ColumnNames.TRADE_FEE.ordinal()] = ""; } columns[ColumnNames.BUYER_SEC.ordinal()] = item.getBuyerSecurityDepositAsString(); columns[ColumnNames.SELLER_SEC.ordinal()] = item.getSellerSecurityDepositAsString(); columns[ColumnNames.OFFER_TYPE.ordinal()] = item.getDirectionLabel(); columns[ColumnNames.STATUS.ordinal()] = item.getState(); return columns; }; GUIUtil.exportCSV("tradeHistory.csv", headerConverter, contentConverter, null, sortedList, (Stage) root.getScene().getWindow()); }); summaryButton.setOnAction(event -> new ClosedTradesSummaryWindow(model).show()); root.widthProperty().addListener(widthListener); onWidthChange(root.getWidth()); } @Override protected void deactivate() { sortedList.comparatorProperty().unbind(); exportButton.setOnAction(null); summaryButton.setOnAction(null); filterBox.deactivate(); root.widthProperty().removeListener(widthListener); } private static > Comparator nullsFirstComparing(Function keyExtractor) { return Comparator.comparing( o -> o != null ? keyExtractor.apply(o) : null, Comparator.nullsFirst(Comparator.naturalOrder()) ); } private static > Comparator nullsFirstComparingAsTrade(Function keyExtractor) { return Comparator.comparing( o -> o.getTradable() instanceof Trade ? keyExtractor.apply((Trade) o.getTradable()) : null, Comparator.nullsFirst(Comparator.naturalOrder()) ); } private void onWidthChange(double width) { buyerSecurityDepositColumn.setVisible(width > 1400); sellerSecurityDepositColumn.setVisible(width > 1500); } private void setTradeIdColumnCellFactory() { tradeIdColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); tradeIdColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { private HyperlinkWithIcon field; @Override public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { field = new HyperlinkWithIcon(item.getTradeId()); field.setOnAction(event -> { Tradable tradable = item.getTradable(); if (tradable instanceof Trade) { tradeDetailsWindow.show((Trade) tradable); } else if (tradable instanceof OpenOffer) { offerDetailsWindow.show(tradable.getOffer()); } }); field.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails"))); setGraphic(field); } else { setGraphic(null); if (field != null) field.setOnAction(null); } } }; } }); } private void setDateColumnCellFactory() { dateColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); dateColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null) setGraphic(new AutoTooltipLabel(item.getDateAsString())); else setGraphic(null); } }; } }); } private void setMarketColumnCellFactory() { marketColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); marketColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setGraphic(new AutoTooltipLabel(item.getMarketLabel())); } else { setGraphic(null); } } }; } }); } private void setStateColumnCellFactory() { stateColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); stateColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null) setGraphic(new AutoTooltipLabel(item.getState())); else setGraphic(null); } }; } }); } private void setDuplicateColumnCellFactory() { duplicateColumn.getStyleClass().add("avatar-column"); duplicateColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); duplicateColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { Button button; @Override public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty && isMyOfferAsMaker(item.getTradable().getOffer().getOfferPayload())) { if (button == null) { button = FormBuilder.getRegularIconButton(MaterialDesignIcon.CONTENT_COPY); button.setTooltip(new Tooltip(Res.get("portfolio.context.offerLikeThis"))); setGraphic(button); } button.setOnAction(event -> onDuplicateOffer(item.getTradable().getOffer())); } else { setGraphic(null); if (button != null) { button.setOnAction(null); button = null; } } } }; } }); } @SuppressWarnings("UnusedReturnValue") private TableColumn setAvatarColumnCellFactory() { avatarColumn.getStyleClass().add("avatar-column"); avatarColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); avatarColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (!empty && item != null && item.getTradable() instanceof Trade) { Trade tradeModel = (Trade) item.getTradable(); int numPastTrades = item.getNumPastTrades(); NodeAddress tradePeerNodeAddress = tradeModel.getTradePeerNodeAddress(); String role = Res.get("peerInfoIcon.tooltip.tradePeer"); Node peerInfoIcon = new PeerInfoIconTrading(tradePeerNodeAddress, role, numPastTrades, privateNotificationManager, tradeModel, preferences, model.dataModel.accountAgeWitnessService, useDevPrivilegeKeys); setPadding(new Insets(1, 15, 0, 0)); setGraphic(peerInfoIcon); } else { setGraphic(null); } } }; } }); return avatarColumn; } private void setAmountColumnCellFactory() { amountColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); amountColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setGraphic(new AutoTooltipLabel(item.getAmountAsString())); } else { setGraphic(null); } } }; } }); } private void setPriceColumnCellFactory() { priceColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); priceColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setGraphic(new AutoTooltipLabel(item.getPriceAsString())); } else { setGraphic(null); } } }; } }); } private void setDeviationColumnCellFactory() { deviationColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); deviationColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setGraphic(new AutoTooltipLabel(item.getPriceDeviationAsString())); } else { setGraphic(null); } } }; } }); } private void setVolumeColumnCellFactory() { volumeColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); volumeColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null) setGraphic(new AutoTooltipLabel(item.getVolumeAsString(true))); else setGraphic(null); } }; } }); } private void setDirectionColumnCellFactory() { directionColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); directionColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setGraphic(new AutoTooltipLabel(item.getDirectionLabel())); } else { setGraphic(null); } } }; } }); } private void setTradeFeeColumnCellFactory() { tradeFeeColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); tradeFeeColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setGraphic(new AutoTooltipLabel(item.getTradeFeeAsString(true))); } else { setGraphic(null); } } }; } }); } private void setBuyerSecurityDepositColumnCellFactory() { buyerSecurityDepositColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); buyerSecurityDepositColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setGraphic(new AutoTooltipLabel(item.getBuyerSecurityDepositAsString())); } else { setGraphic(null); } } }; } }); } private void setSellerSecurityDepositColumnCellFactory() { sellerSecurityDepositColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); sellerSecurityDepositColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final ClosedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setGraphic(new AutoTooltipLabel(item.getSellerSecurityDepositAsString())); } else { setGraphic(null); } } }; } }); } private TableColumn setRemoveTradeColumnCellFactory() { removeTradeColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue())); removeTradeColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(ClosedTradesListItem newItem, boolean empty) { if (newItem == null || !(newItem.getTradable() instanceof Trade)) { setGraphic(null); return; } Trade trade = (Trade) newItem.getTradable(); super.updateItem(newItem, empty); if (!empty && newItem != null && !trade.isPayoutConfirmed()) { Label icon = FormBuilder.getIcon(AwesomeIcon.UNDO); JFXButton iconButton = new JFXButton("", icon); iconButton.setStyle("-fx-cursor: hand; -fx-padding: 0 10 0 10;"); iconButton.getStyleClass().add("hidden-icon-button"); iconButton.setTooltip(new Tooltip(Res.get("portfolio.failed.revertToPending"))); iconButton.setOnAction(e -> onRevertTrade(trade)); setGraphic(iconButton); } else { setGraphic(null); } } }; } }); return removeTradeColumn; } private void onRevertTrade(Trade trade) { new Popup().attention(Res.get("portfolio.failed.revertToPending.popup")) .onAction(() -> onMoveTradeToPendingTrades(trade)) .actionButtonText(Res.get("shared.yes")) .closeButtonText(Res.get("shared.no")) .show(); } private void onMoveTradeToPendingTrades(Trade trade) { model.dataModel.onMoveTradeToPendingTrades(trade); } private void onDuplicateOffer(Offer offer) { try { OfferPayload offerPayload = offer.getOfferPayload(); if (isMyOfferAsMaker(offerPayload)) { PortfolioUtil.duplicateOffer(navigation, offerPayload); } else { new Popup().warning(Res.get("portfolio.context.notYourOffer")).show(); } } catch (NullPointerException e) { log.warn("Unable to get offerPayload - {}", e.toString()); } } private boolean isMyOfferAsMaker(OfferPayload offerPayload) { return offerPayload.getPubKeyRing().equals(keyRing.getPubKeyRing()); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.closedtrades; import com.google.inject.Inject; import haveno.core.trade.ClosedTradableFormatter; import haveno.desktop.common.model.ActivatableWithDataModel; import haveno.desktop.common.model.ViewModel; import java.math.BigInteger; import java.util.Map; public class ClosedTradesViewModel extends ActivatableWithDataModel implements ViewModel { private final ClosedTradableFormatter closedTradableFormatter; @Inject public ClosedTradesViewModel(ClosedTradesDataModel dataModel, ClosedTradableFormatter closedTradableFormatter) { super(dataModel); this.closedTradableFormatter = closedTradableFormatter; } /////////////////////////////////////////////////////////////////////////////////////////// // Used in ClosedTradesSummaryWindow /////////////////////////////////////////////////////////////////////////////////////////// public BigInteger getTotalTradeAmount() { return dataModel.getTotalAmount(); } public String getTotalAmountWithVolume(BigInteger totalTradeAmount) { return dataModel.getVolumeInUserFiatCurrency(totalTradeAmount) .map(volume -> closedTradableFormatter.getTotalAmountWithVolumeAsString(totalTradeAmount, volume)) .orElse(""); } public Map getTotalVolumeByCurrency() { return closedTradableFormatter.getTotalVolumeByCurrencyAsString(dataModel.getListAsTradables()); } public String getTotalTxFee(BigInteger totalTradeAmount) { BigInteger totalTxFee = dataModel.getTotalTxFee(); return closedTradableFormatter.getTotalTxFeeAsString(totalTradeAmount, totalTxFee); } public String getTotalTradeFee(BigInteger totalTradeAmount) { BigInteger totalTradeFee = dataModel.getTotalTradeFee(); return closedTradableFormatter.getTotalTradeFeeAsString(totalTradeAmount, totalTradeFee); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.duplicateoffer; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TradeCurrency; import haveno.core.offer.CreateOfferService; import haveno.core.offer.Offer; import haveno.core.offer.OfferUtil; import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.PaymentAccount; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.Navigation; import haveno.desktop.main.offer.MutableOfferDataModel; import haveno.network.p2p.P2PService; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; class DuplicateOfferDataModel extends MutableOfferDataModel { @Inject DuplicateOfferDataModel(CreateOfferService createOfferService, OpenOfferManager openOfferManager, OfferUtil offerUtil, XmrWalletService xmrWalletService, Preferences preferences, User user, P2PService p2PService, PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, TradeStatisticsManager tradeStatisticsManager, Navigation navigation) { super(createOfferService, openOfferManager, offerUtil, xmrWalletService, preferences, user, p2PService, priceFeedService, accountAgeWitnessService, btcFormatter, tradeStatisticsManager, navigation); } public void populateData(Offer offer) { if (offer == null) return; PaymentAccount account = user.getPaymentAccount(offer.getMakerPaymentAccountId()); if (account != null) { this.paymentAccount = account; } setMinAmount(offer.getMinAmount()); setAmount(offer.getAmount()); setPrice(offer.getPrice()); setVolume(offer.getVolume()); setUseMarketBasedPrice(offer.isUseMarketBasedPrice()); if (offer.isUseMarketBasedPrice()) { setMarketPriceMarginPct(offer.getMarketPriceMarginPct()); } setBuyerAsTakerWithoutDeposit(offer.hasBuyerAsTakerWithoutDeposit()); setSecurityDepositPct(getSecurityAsPercent(offer)); setExtraInfo(offer.getOfferExtraInfo()); OpenOffer openOffer = openOfferManager.getOpenOffer(offer.getId()).orElse(null); if (openOffer != null) setTriggerPrice(openOffer.getTriggerPrice()); } @Override protected Set getUserPaymentAccounts() { return Objects.requireNonNull(user.getPaymentAccounts()).stream() .filter(account -> !account.getPaymentMethod().isBsqSwap()) .collect(Collectors.toSet()); } @Override protected PaymentAccount getPreselectedPaymentAccount() { // If trade currency is BSQ don't use the BSQ swap payment account as it will automatically // close the duplicate offer view Optional bsqOptional = CurrencyUtil.getTradeCurrency("BSQ"); if (bsqOptional.isPresent() && tradeCurrency.equals(bsqOptional.get()) && user.getPaymentAccounts() != null) { Optional firstBsqPaymentAccount = user.getPaymentAccounts().stream().filter(paymentAccount1 -> { Optional tradeCurrency = paymentAccount1.getTradeCurrency(); return tradeCurrency.isPresent() && tradeCurrency.get().equals(bsqOptional.get()); }).findFirst(); if (firstBsqPaymentAccount.isPresent()) { return firstBsqPaymentAccount.get(); } } return super.getPreselectedPaymentAccount(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferView.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.duplicateoffer; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.core.locale.CurrencyUtil; import haveno.core.offer.OfferPayload; import haveno.core.payment.PaymentAccount; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.Navigation; import haveno.desktop.common.view.FxmlView; import haveno.desktop.main.offer.MutableOfferView; import haveno.desktop.main.overlays.windows.OfferDetailsWindow; import javafx.collections.ObservableList; import javafx.geometry.Insets; @FxmlView public class DuplicateOfferView extends MutableOfferView { @Inject private DuplicateOfferView(DuplicateOfferViewModel model, Navigation navigation, Preferences preferences, OfferDetailsWindow offerDetailsWindow, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter) { super(model, navigation, preferences, offerDetailsWindow, btcFormatter); } @Override protected void initialize() { super.initialize(); } @Override protected void doActivate() { super.doActivate(); // Workaround to fix margin on top of amount group gridPane.setPadding(new Insets(-20, 25, 25, 25)); updatePriceToggle(); // To force re-validation of payment account validation onPaymentAccountsComboBoxSelected(); } @Override protected ObservableList filterPaymentAccounts(ObservableList paymentAccounts) { return paymentAccounts; } public void initWithData(OfferPayload offerPayload) { initWithData(offerPayload.getDirection(), CurrencyUtil.getTradeCurrency(offerPayload.getCurrencyCode()).get(), true, null); model.initWithData(offerPayload); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.duplicateoffer; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.offer.Offer; import haveno.core.offer.OfferPayload; import haveno.core.offer.OfferUtil; import haveno.core.payment.validation.FiatVolumeValidator; import haveno.core.payment.validation.SecurityDepositValidator; import haveno.core.payment.validation.XmrValidator; import haveno.core.provider.price.PriceFeedService; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.PriceUtil; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.AmountValidator4Decimals; import haveno.core.util.validation.AmountValidator8Decimals; import haveno.desktop.Navigation; import haveno.desktop.main.offer.MutableOfferViewModel; import lombok.extern.slf4j.Slf4j; @Slf4j class DuplicateOfferViewModel extends MutableOfferViewModel { @Inject public DuplicateOfferViewModel(DuplicateOfferDataModel dataModel, FiatVolumeValidator fiatVolumeValidator, AmountValidator4Decimals priceValidator4Decimals, AmountValidator8Decimals priceValidator8Decimals, XmrValidator btcValidator, SecurityDepositValidator securityDepositValidator, PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation, Preferences preferences, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, OfferUtil offerUtil) { super(dataModel, fiatVolumeValidator, priceValidator4Decimals, priceValidator8Decimals, btcValidator, securityDepositValidator, priceFeedService, accountAgeWitnessService, navigation, preferences, btcFormatter, offerUtil); syncMinAmountWithAmount = false; } public void initWithData(OfferPayload offerPayload) { this.offer = new Offer(offerPayload); offer.setPriceFeedService(priceFeedService); } @Override public void activate() { super.activate(); dataModel.populateData(offer); long triggerPriceAsLong = dataModel.getTriggerPrice(); dataModel.setTriggerPrice(triggerPriceAsLong); if (triggerPriceAsLong > 0) { triggerPrice.set(PriceUtil.formatMarketPrice(triggerPriceAsLong, dataModel.getCurrencyCode())); } else { triggerPrice.set(""); } onTriggerPriceTextFieldChanged(); triggerFocusOutOnAmountFields(); onFocusOutPriceAsPercentageTextField(true, false); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.editoffer; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.api.CoreOffersService; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TradeCurrency; import haveno.core.offer.CreateOfferService; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OfferUtil; import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.PaymentAccount; import haveno.core.proto.persistable.CorePersistenceProtoResolver; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.coin.CoinUtil; import haveno.core.xmr.wallet.Restrictions; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.Navigation; import haveno.desktop.main.offer.MutableOfferDataModel; import haveno.network.p2p.P2PService; import java.util.Optional; import java.util.Set; class EditOfferDataModel extends MutableOfferDataModel { private final CorePersistenceProtoResolver corePersistenceProtoResolver; private OpenOffer openOffer; private OpenOffer.State initialState; private Offer editedOffer; private final CoreOffersService coreOffersService; @Inject EditOfferDataModel(CreateOfferService createOfferService, OpenOfferManager openOfferManager, OfferUtil offerUtil, XmrWalletService xmrWalletService, Preferences preferences, User user, P2PService p2PService, PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, CorePersistenceProtoResolver corePersistenceProtoResolver, TradeStatisticsManager tradeStatisticsManager, Navigation navigation, CoreOffersService coreOffersService) { super(createOfferService, openOfferManager, offerUtil, xmrWalletService, preferences, user, p2PService, priceFeedService, accountAgeWitnessService, btcFormatter, tradeStatisticsManager, navigation); this.corePersistenceProtoResolver = corePersistenceProtoResolver; this.coreOffersService = coreOffersService; } public void reset() { direction = null; tradeCurrency = null; tradeCurrencyCode.set(null); useMarketBasedPrice.set(false); amount.set(null); minAmount.set(null); price.set(null); volume.set(null); minVolume.set(null); securityDepositPct.set(0); paymentAccounts.clear(); paymentAccount = null; marketPriceMarginPct = 0; } public void applyOpenOffer(OpenOffer openOffer) { this.openOffer = openOffer; Offer offer = openOffer.getOffer(); direction = offer.getDirection(); CurrencyUtil.getTradeCurrency(offer.getCounterCurrencyCode()) .ifPresent(c -> this.tradeCurrency = c); tradeCurrencyCode.set(offer.getCounterCurrencyCode()); this.initialState = openOffer.getState(); PaymentAccount tmpPaymentAccount = user.getPaymentAccount(openOffer.getOffer().getMakerPaymentAccountId()); Optional optionalTradeCurrency = CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCounterCurrencyCode()); if (optionalTradeCurrency.isPresent() && tmpPaymentAccount != null) { TradeCurrency selectedTradeCurrency = optionalTradeCurrency.get(); this.paymentAccount = PaymentAccount.fromProto(tmpPaymentAccount.toProtoMessage(), corePersistenceProtoResolver); if (paymentAccount.getSingleTradeCurrency() != null) paymentAccount.setSingleTradeCurrency(selectedTradeCurrency); else paymentAccount.setSelectedTradeCurrency(selectedTradeCurrency); } // TODO: update for XMR to use percent as double? // If the security deposit got bounded because it was below the coin amount limit, it can be bigger // by percentage than the restriction. We can't determine the percentage originally entered at offer // creation, so just use the default value as it doesn't matter anyway. double securityDepositPercent = CoinUtil.getAsPercentPerXmr(offer.getMaxSellerSecurityDeposit(), offer.getAmount()); if (securityDepositPercent > Restrictions.getMaxSecurityDepositPct() && offer.getMaxSellerSecurityDeposit().equals(Restrictions.getMinSecurityDeposit())) securityDepositPct.set(Restrictions.getDefaultSecurityDepositPct()); else securityDepositPct.set(securityDepositPercent); allowAmountUpdate = false; triggerPrice = openOffer.getTriggerPrice(); extraInfo.set(offer.getOfferExtraInfo()); } public boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency) { try { return super.initWithData(direction, tradeCurrency, false); } catch (NullPointerException e) { if (e.getMessage().contains("tradeCurrency")) { throw new IllegalArgumentException("Offers of removed assets cannot be edited. You can only cancel it.", e); } return false; } } @Override protected PaymentAccount getPreselectedPaymentAccount() { return paymentAccount; } public void populateData() { Offer offer = openOffer.getOffer(); // Min amount need to be set before amount as if minAmount is null it would be set by amount setMinAmount(offer.getMinAmount()); setAmount(offer.getAmount()); setPrice(offer.getPrice()); setVolume(offer.getVolume()); setUseMarketBasedPrice(offer.isUseMarketBasedPrice()); if (offer.isUseMarketBasedPrice()) { setMarketPriceMarginPct(offer.getMarketPriceMarginPct()); } setTriggerPrice(openOffer.getTriggerPrice()); setExtraInfo(offer.getOfferExtraInfo()); } public void onStartEditOffer(ErrorMessageHandler errorMessageHandler) { openOfferManager.editOpenOfferStart(openOffer, () -> { }, errorMessageHandler); } public void onPublishOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { // get edited offer editedOffer = coreOffersService.getEditedOffer(openOffer, createAndGetOffer().getOfferPayload()); // publish edited offer openOfferManager.editOpenOfferPublish(editedOffer, triggerPrice, initialState, () -> { resultHandler.handleResult(); // process result before nullifying state openOffer = null; editedOffer = null; }, (errorMsg) -> { errorMessageHandler.handleErrorMessage(errorMsg); }); } public void onCancelEditOffer(ErrorMessageHandler errorMessageHandler) { if (openOffer != null) openOfferManager.editOpenOfferCancel(openOffer, initialState, () -> { }, errorMessageHandler); } public boolean hasConflictingClone() { Optional editedOpenOffer = openOfferManager.getOpenOffer(openOffer.getId()); if (!editedOpenOffer.isPresent()) { log.warn("Edited open offer is no longer present"); return false; } return openOfferManager.hasConflictingClone(editedOpenOffer.get()); } @Override protected Set getUserPaymentAccounts() { throw new RuntimeException("Edit offer not supported with XMR"); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.editoffer; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.UserThread; import haveno.common.util.Tuple4; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.offer.OpenOffer; import haveno.core.payment.PaymentAccount; import haveno.core.user.DontShowAgainLookup; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.Navigation; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.BusyAnimation; import haveno.desktop.main.offer.MutableOfferView; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.OfferDetailsWindow; import static haveno.desktop.util.FormBuilder.addButtonBusyAnimationLabelAfterGroup; import javafx.collections.ObservableList; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.image.ImageView; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; @FxmlView public class EditOfferView extends MutableOfferView { private BusyAnimation busyAnimation; private Button confirmButton; private Button cancelButton; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject private EditOfferView(EditOfferViewModel model, Navigation navigation, Preferences preferences, OfferDetailsWindow offerDetailsWindow, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter) { super(model, navigation, preferences, offerDetailsWindow, btcFormatter); } @Override protected void initialize() { super.initialize(); addConfirmEditGroup(); renameAmountGroup(); } private void renameAmountGroup() { amountTitledGroupBg.setText(Res.get("editOffer.setPrice")); } @Override protected void doSetFocus() { // Don't focus in any field before data was set } @Override protected void doActivate() { super.doActivate(); addBindings(); hideOptionsGroup(); hideNextButtons(); // Lock amount field as it would require bigger changes to support increased amount values. amountTextField.setDisable(true); amountBtcLabel.setDisable(true); minAmountTextField.setDisable(true); minAmountBtcLabel.setDisable(true); volumeTextField.setDisable(true); volumeCurrencyLabel.setDisable(true); // Workaround to fix margin on top of amount group gridPane.setPadding(new Insets(-20, 25, 25, 25)); updatePriceToggle(); updateElementsWithDirection(); model.isNextButtonDisabled.setValue(false); cancelButton.setDisable(false); model.onInvalidateMarketPriceMarginPct(); model.onInvalidatePrice(); // To force re-validation of payment account validation onPaymentAccountsComboBoxSelected(); } @Override public void onClose() { model.onCancelEditOffer(errorMessage -> { log.error(errorMessage); new Popup().warning(Res.get("editOffer.failed", errorMessage)).show(); }); } @Override protected void deactivate() { super.deactivate(); removeBindings(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void applyOpenOffer(OpenOffer openOffer) { model.applyOpenOffer(openOffer); initWithData(openOffer.getOffer().getDirection(), CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCounterCurrencyCode()).get(), false, null); model.onStartEditOffer(errorMessage -> { log.error(errorMessage); new Popup().warning(Res.get("editOffer.failed", errorMessage)) .onClose(this::close) .show(); }); if (!model.isSecurityDepositValid()) { new Popup().warning(Res.get("editOffer.invalidDeposit")) .onClose(this::close) .show(); } } /////////////////////////////////////////////////////////////////////////////////////////// // Bindings, Listeners /////////////////////////////////////////////////////////////////////////////////////////// private void addBindings() { confirmButton.disableProperty().bind(model.isNextButtonDisabled); } private void removeBindings() { confirmButton.disableProperty().unbind(); } @Override protected ObservableList filterPaymentAccounts(ObservableList paymentAccounts) { return paymentAccounts; } /////////////////////////////////////////////////////////////////////////////////////////// // Build UI elements /////////////////////////////////////////////////////////////////////////////////////////// private void addConfirmEditGroup() { int tmpGridRow = 6; final Tuple4 editOfferTuple = addButtonBusyAnimationLabelAfterGroup(gridPane, tmpGridRow++, Res.get("editOffer.confirmEdit")); final HBox editOfferConfirmationBox = editOfferTuple.fourth; editOfferConfirmationBox.setAlignment(Pos.CENTER_LEFT); GridPane.setHalignment(editOfferConfirmationBox, HPos.LEFT); confirmButton = editOfferTuple.first; confirmButton.setMinHeight(40); confirmButton.setPadding(new Insets(0, 20, 0, 20)); confirmButton.setGraphicTextGap(10); busyAnimation = editOfferTuple.second; Label spinnerInfoLabel = editOfferTuple.third; cancelButton = new AutoTooltipButton(Res.get("shared.cancel")); cancelButton.setDefaultButton(false); cancelButton.setOnAction(event -> close()); editOfferConfirmationBox.getChildren().add(cancelButton); confirmButton.setOnAction(e -> { if (model.isPriceInRange()) { model.isNextButtonDisabled.setValue(true); cancelButton.setDisable(true); busyAnimation.play(); spinnerInfoLabel.setText(Res.get("editOffer.publishOffer")); // edit offer model.onPublishOffer(() -> { if (model.dataModel.hasConflictingClone()) { new Popup().warning(Res.get("editOffer.hasConflictingClone")).show(); } else { String key = "editOfferSuccess"; if (DontShowAgainLookup.showAgain(key)) { new Popup() .feedback(Res.get("editOffer.success")) .dontShowAgainId(key) .show(); } } UserThread.execute(() -> { spinnerInfoLabel.setText(""); busyAnimation.stop(); close(); }); }, (message) -> { UserThread.execute(() -> { log.error(message); spinnerInfoLabel.setText(""); busyAnimation.stop(); model.isNextButtonDisabled.setValue(false); cancelButton.setDisable(false); new Popup().warning(Res.get("editOffer.failed", message)).show(); }); }); } }); } /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// private void updateElementsWithDirection() { ImageView iconView = new ImageView(); iconView.setId(model.isSellOffer() ? "image-sell-white" : "image-buy-white"); confirmButton.setGraphic(iconView); confirmButton.setId(model.isSellOffer() ? "sell-button-big" : "buy-button-big"); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.editoffer; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.offer.OfferUtil; import haveno.core.offer.OpenOffer; import haveno.core.payment.validation.FiatVolumeValidator; import haveno.core.payment.validation.SecurityDepositValidator; import haveno.core.payment.validation.XmrValidator; import haveno.core.provider.price.PriceFeedService; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.PriceUtil; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.AmountValidator4Decimals; import haveno.core.util.validation.AmountValidator8Decimals; import haveno.desktop.Navigation; import haveno.desktop.main.offer.MutableOfferViewModel; class EditOfferViewModel extends MutableOfferViewModel { @Inject public EditOfferViewModel(EditOfferDataModel dataModel, FiatVolumeValidator fiatVolumeValidator, AmountValidator4Decimals priceValidator4Decimals, AmountValidator8Decimals priceValidator8Decimals, XmrValidator xmrValidator, SecurityDepositValidator securityDepositValidator, PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation, Preferences preferences, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, OfferUtil offerUtil) { super(dataModel, fiatVolumeValidator, priceValidator4Decimals, priceValidator8Decimals, xmrValidator, securityDepositValidator, priceFeedService, accountAgeWitnessService, navigation, preferences, btcFormatter, offerUtil); syncMinAmountWithAmount = false; } @Override public void activate() { super.activate(); dataModel.populateData(); long triggerPriceAsLong = dataModel.getTriggerPrice(); dataModel.setTriggerPrice(triggerPriceAsLong); if (triggerPriceAsLong > 0) { triggerPrice.set(PriceUtil.formatMarketPrice(triggerPriceAsLong, dataModel.getCurrencyCode())); } else { triggerPrice.set(""); } onTriggerPriceTextFieldChanged(); onReserveExactAmountCheckboxChanged(); } public void applyOpenOffer(OpenOffer openOffer) { dataModel.reset(); dataModel.applyOpenOffer(openOffer); } public void onStartEditOffer(ErrorMessageHandler errorMessageHandler) { dataModel.onStartEditOffer(errorMessageHandler); } public void onPublishOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { dataModel.onPublishOffer(resultHandler, errorMessageHandler); } public void onCancelEditOffer(ErrorMessageHandler errorMessageHandler) { dataModel.onCancelEditOffer(errorMessageHandler); } public void onInvalidateMarketPriceMarginPct() { marketPriceMargin.set(FormattingUtils.formatToPercent(dataModel.getMarketPriceMarginPct())); } public void onInvalidatePrice() { price.set(FormattingUtils.formatPrice(null)); price.set(FormattingUtils.formatPrice(dataModel.getPrice().get())); } public boolean isSecurityDepositValid() { return securityDepositValidator.validate(securityDeposit.get()).isValid; } @Override public void triggerFocusOutOnAmountFields() { // do not update BTC Amount or minAmount here // issue 2798: "after a few edits of offer the BTC amount has increased" } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesDataModel.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.failedtrades; import com.google.inject.Inject; import haveno.common.UserThread; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.trade.failed.FailedTradesManager; import haveno.desktop.common.model.ActivatableDataModel; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import java.util.stream.Collectors; class FailedTradesDataModel extends ActivatableDataModel { private final FailedTradesManager failedTradesManager; private final TradeManager tradeManager; private final ObservableList list = FXCollections.observableArrayList(); private final ListChangeListener tradesListChangeListener; @Inject public FailedTradesDataModel(FailedTradesManager failedTradesManager, TradeManager tradeManager) { this.failedTradesManager = failedTradesManager; this.tradeManager = tradeManager; tradesListChangeListener = change -> applyList(); } @Override protected void activate() { applyList(); failedTradesManager.getObservableList().addListener(tradesListChangeListener); } @Override protected void deactivate() { failedTradesManager.getObservableList().removeListener(tradesListChangeListener); } public ObservableList getList() { return list; } public OfferDirection getDirection(Offer offer) { return failedTradesManager.wasMyOffer(offer) ? offer.getDirection() : offer.getMirroredDirection(); } private void applyList() { UserThread.execute(() -> { synchronized (list) { list.clear(); list.addAll( failedTradesManager.getObservableList().stream() .map(trade -> new FailedTradesListItem(trade, failedTradesManager)) .collect(Collectors.toList()) ); // we sort by date, earliest first list.sort((o1, o2) -> o2.getTrade().getDate().compareTo(o1.getTrade().getDate())); } }); } public void onMoveTradeToPendingTrades(Trade trade) { tradeManager.onMoveFailedTradeToPendingTrades(trade); } public void unfailTrade(Trade trade) { failedTradesManager.unFailTrade(trade); } public String checkUnfail(Trade trade) { return failedTradesManager.checkUnFail(trade); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesListItem.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.failedtrades; import org.apache.commons.lang3.StringUtils; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.failed.FailedTradesManager; import haveno.core.util.FormattingUtils; import haveno.core.util.VolumeUtil; import haveno.desktop.util.DisplayUtils; import haveno.desktop.util.filtering.FilterableListItem; import haveno.desktop.util.filtering.FilteringUtils; import lombok.Getter; class FailedTradesListItem implements FilterableListItem { @Getter private final Trade trade; private final FailedTradesManager failedTradesManager; FailedTradesListItem(Trade trade, FailedTradesManager failedTradesManager) { this.trade = trade; this.failedTradesManager = failedTradesManager; } FailedTradesListItem() { this.trade = null; this.failedTradesManager = null; } public String getDateAsString() { return DisplayUtils.formatDateTime(trade.getDate()); } public String getPriceAsString() { return FormattingUtils.formatPrice(trade.getPrice()); } public String getAmountAsString() { return HavenoUtils.formatXmr(trade.getAmount()); } public String getPaymentMethod() { return trade.getOffer().getPaymentMethodNameWithCountryCode(); } public String getMarketDescription() { return CurrencyUtil.getCurrencyPair(trade.getOffer().getCounterCurrencyCode()); } public String getDirectionLabel() { Offer offer = trade.getOffer(); OfferDirection direction = failedTradesManager.wasMyOffer(offer) || trade instanceof ArbitratorTrade ? offer.getDirection() : offer.getMirroredDirection(); String currencyCode = trade.getOffer().getCounterCurrencyCode(); return DisplayUtils.getDirectionWithCode(direction, currencyCode, offer.isPrivateOffer()); } public String getVolumeAsString() { return VolumeUtil.formatVolumeWithCode(trade.getVolume()); } public String getState() { return Res.get("portfolio.failed.Failed"); } @Override public boolean match(String filterString) { if (filterString.isEmpty()) { return true; } if (StringUtils.containsIgnoreCase(getDateAsString(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getMarketDescription(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getPriceAsString(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getPaymentMethod(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getAmountAsString(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getDirectionLabel(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getVolumeAsString(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getState(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getTrade().getOffer().getCombinedExtraInfo(), filterString)) { return true; } if (trade.getBuyer().getPaymentAccountPayload() != null && StringUtils.containsIgnoreCase(getTrade().getBuyer().getPaymentAccountPayload().getPaymentDetails(), filterString)) { return true; } if (trade.getSeller().getPaymentAccountPayload() != null && StringUtils.containsIgnoreCase(getTrade().getSeller().getPaymentAccountPayload().getPaymentDetails(), filterString)) { return true; } if (FilteringUtils.match(trade.getOffer(), filterString)) { return true; } return FilteringUtils.match(trade, filterString); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesView.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.failedtrades; import com.google.inject.Inject; import com.googlecode.jcsv.writer.CSVEntryConverter; import com.jfoenix.controls.JFXButton; import de.jensd.fx.fontawesome.AwesomeIcon; import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.user.User; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.common.view.ActivatableViewAndModel; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.HyperlinkWithIcon; import haveno.desktop.components.list.FilterBox; import haveno.desktop.main.offer.OfferViewUtil; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.TradeDetailsWindow; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.GUIUtil; import java.math.BigInteger; import java.util.Comparator; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.geometry.Insets; import javafx.scene.Scene; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.MenuItem; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableRow; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.stage.Stage; import javafx.util.Callback; @FxmlView public class FailedTradesView extends ActivatableViewAndModel { @FXML TableView tableView; @FXML TableColumn priceColumn, amountColumn, volumeColumn, marketColumn, directionColumn, dateColumn, tradeIdColumn, stateColumn, removeTradeColumn; @FXML FilterBox filterBox; @FXML Label numItems; @FXML Region footerSpacer; @FXML AutoTooltipButton exportButton; private final TradeDetailsWindow tradeDetailsWindow; private SortedList sortedList; private FilteredList filteredList; private EventHandler keyEventEventHandler; private Scene scene; private XmrWalletService xmrWalletService; private User user; private ContextMenu contextMenu; @Inject public FailedTradesView(FailedTradesViewModel model, TradeDetailsWindow tradeDetailsWindow, XmrWalletService xmrWalletService, User user) { super(model); this.tradeDetailsWindow = tradeDetailsWindow; this.xmrWalletService = xmrWalletService; this.user = user; } @Override public void initialize() { GUIUtil.applyTableStyle(tableView); priceColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.price"))); amountColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amountWithCur", Res.getBaseCurrencyCode()))); volumeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amount"))); marketColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.market"))); directionColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.offerType"))); dateColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.dateTime"))); tradeIdColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.tradeId"))); stateColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.state"))); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); tableView.setPlaceholder(new AutoTooltipLabel(Res.get("table.placeholder.noItems", Res.get("shared.trades")))); setTradeIdColumnCellFactory(); setDirectionColumnCellFactory(); setAmountColumnCellFactory(); setPriceColumnCellFactory(); setVolumeColumnCellFactory(); setDateColumnCellFactory(); setMarketColumnCellFactory(); setStateColumnCellFactory(); setRemoveTradeColumnCellFactory(); tradeIdColumn.setComparator(Comparator.comparing(o -> o.getTrade().getId())); dateColumn.setComparator(Comparator.comparing(o -> o.getTrade().getDate())); priceColumn.setComparator(Comparator.comparing(o -> o.getTrade().getPrice())); volumeColumn.setComparator(Comparator.comparing(o -> o.getTrade().getVolume(), Comparator.nullsFirst(Comparator.naturalOrder()))); amountColumn.setComparator(Comparator.comparing(o -> o.getTrade().getAmount(), Comparator.nullsFirst(Comparator.naturalOrder()))); stateColumn.setComparator(Comparator.comparing(o -> o.getState())); marketColumn.setComparator(Comparator.comparing(o -> o.getMarketDescription())); dateColumn.setSortType(TableColumn.SortType.DESCENDING); tableView.getSortOrder().add(dateColumn); keyEventEventHandler = keyEvent -> { if (Utilities.isAltOrCtrlPressed(KeyCode.Y, keyEvent)) { var checkTxs = checkTxs(); var checkUnfailString = checkUnfail(); if (!checkTxs.isEmpty()) { log.warn("Cannot unfail, error {}", checkTxs); new Popup().warning(checkTxs) .show(); } else if (!checkUnfailString.isEmpty()) { log.warn("Cannot unfail, error {}", checkUnfailString); new Popup().warning(Res.get("portfolio.failed.cantUnfail", checkUnfailString)) .show(); } else { new Popup().warning(Res.get("portfolio.failed.unfail")) .onAction(this::onUnfail) .show(); } } }; numItems.setId("num-offers"); numItems.setPadding(new Insets(-5, 0, 0, 10)); HBox.setHgrow(footerSpacer, Priority.ALWAYS); HBox.setMargin(exportButton, new Insets(0, 10, 0, 0)); exportButton.updateText(Res.get("shared.exportCSV")); } @Override protected void activate() { scene = root.getScene(); if (scene != null) { scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); } filteredList = new FilteredList<>(model.getList()); sortedList = new SortedList<>(filteredList); sortedList.comparatorProperty().bind(tableView.comparatorProperty()); tableView.setItems(sortedList); filterBox.initialize(filteredList, tableView); // here because filteredList is instantiated here filterBox.setPromptText(Res.get("shared.filter")); filterBox.activate(); contextMenu = new ContextMenu(); boolean isArbitrator = user.getRegisteredArbitrator() != null; if (isArbitrator) { MenuItem item1 = new MenuItem(Res.get("support.contextmenu.penalize.msg", Res.get("shared.maker"))); MenuItem item2 = new MenuItem(Res.get("support.contextmenu.penalize.msg", Res.get("shared.taker"))); item1.setOnAction(event -> { Trade selectedFailedTrade = tableView.getSelectionModel().getSelectedItem().getTrade(); handleContextMenu("portfolio.failed.penalty.msg", Res.get(selectedFailedTrade.getMaker() == selectedFailedTrade.getBuyer() ? "shared.buyer" : "shared.seller"), Res.get("shared.maker"), selectedFailedTrade.getMaker().getSecurityDeposit(), selectedFailedTrade.getMaker().getReserveTxHash(), selectedFailedTrade.getMaker().getReserveTxHex()); }); item2.setOnAction(event -> { Trade selectedFailedTrade = tableView.getSelectionModel().getSelectedItem().getTrade(); handleContextMenu("portfolio.failed.penalty.msg", Res.get(selectedFailedTrade.getTaker() == selectedFailedTrade.getBuyer() ? "shared.buyer" : "shared.seller"), Res.get("shared.taker"), selectedFailedTrade.getTaker().getSecurityDeposit(), selectedFailedTrade.getTaker().getReserveTxHash(), selectedFailedTrade.getTaker().getReserveTxHex()); }); contextMenu.getItems().addAll(item1, item2); } tableView.setRowFactory(tv -> { TableRow row = new TableRow<>(); row.setOnContextMenuRequested(event -> { contextMenu.show(row, event.getScreenX(), event.getScreenY()); }); return row; }); numItems.setText(Res.get("shared.numItemsLabel", sortedList.size())); exportButton.setOnAction(event -> { ObservableList> tableColumns = GUIUtil.getContentColumns(tableView); int reportColumns = tableColumns.size() - 1; // CSV report excludes the last column (an icon) CSVEntryConverter headerConverter = item -> { String[] columns = new String[reportColumns]; for (int i = 0; i < columns.length; i++) columns[i] = ((AutoTooltipLabel) tableColumns.get(i).getGraphic()).getText(); return columns; }; CSVEntryConverter contentConverter = item -> { String[] columns = new String[reportColumns]; columns[0] = item.getTrade().getId(); columns[1] = item.getDateAsString(); columns[2] = item.getMarketDescription(); columns[3] = item.getPriceAsString(); columns[4] = item.getAmountAsString(); columns[5] = item.getVolumeAsString(); columns[6] = item.getDirectionLabel(); columns[7] = item.getState(); return columns; }; GUIUtil.exportCSV("failedTrades.csv", headerConverter, contentConverter, new FailedTradesListItem(), sortedList, (Stage) root.getScene().getWindow()); }); } private void handleContextMenu(String msgKey, String buyerOrSeller, String makerOrTaker, BigInteger fee, String reserveTxHash, String reserveTxHex) { final Trade failedTrade = tableView.getSelectionModel().getSelectedItem().getTrade(); log.debug("Found {} matching trade.", (failedTrade != null ? failedTrade.getId() : null)); if(failedTrade != null) { new Popup().warning(Res.get(msgKey, buyerOrSeller, makerOrTaker, HavenoUtils.formatXmr(fee, true), "todo", // TODO: set reserve tx miner fee when verified reserveTxHash ) ).onAction(() -> OfferViewUtil.submitTransactionHex(xmrWalletService, tableView, reserveTxHex)).show(); } else { new Popup().error(Res.get("portfolio.failed.error.msg")).show(); } } @Override protected void deactivate() { if (scene != null) { scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); } sortedList.comparatorProperty().unbind(); exportButton.setOnAction(null); filterBox.deactivate(); } private void onUnfail() { Trade trade = sortedList.get(tableView.getSelectionModel().getFocusedIndex()).getTrade(); model.dataModel.unfailTrade(trade); } private String checkUnfail() { Trade trade = sortedList.get(tableView.getSelectionModel().getFocusedIndex()).getTrade(); return model.dataModel.checkUnfail(trade); } private String checkTxs() { Trade trade = sortedList.get(tableView.getSelectionModel().getFocusedIndex()).getTrade(); log.info("Initiated unfail of trade {}", trade.getId()); if (trade.getMakerDepositTx() == null || (trade.getTakerDepositTx() == null && !trade.hasBuyerAsTakerWithoutDeposit())) { log.info("Check unfail found no deposit tx(s) for trade {}", trade.getId()); return Res.get("portfolio.failed.depositTxNull"); } return ""; } private void onRevertTrade(Trade trade) { new Popup().attention(Res.get("portfolio.failed.revertToPending.popup")) .onAction(() -> onMoveTradeToPendingTrades(trade)) .actionButtonText(Res.get("shared.yes")) .closeButtonText(Res.get("shared.no")) .show(); } private void onMoveTradeToPendingTrades(Trade trade) { model.dataModel.onMoveTradeToPendingTrades(trade); } private void setTradeIdColumnCellFactory() { tradeIdColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); tradeIdColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { private HyperlinkWithIcon field; @Override public void updateItem(final FailedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { field = new HyperlinkWithIcon(item.getTrade().getShortId()); field.setOnAction(event -> tradeDetailsWindow.show(item.getTrade())); field.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails"))); setGraphic(field); } else { setGraphic(null); if (field != null) field.setOnAction(null); } } }; } }); } private void setDateColumnCellFactory() { dateColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue())); dateColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final FailedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null) setGraphic(new AutoTooltipLabel(item.getDateAsString())); else setGraphic(null); } }; } }); } private void setMarketColumnCellFactory() { marketColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue())); marketColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final FailedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null) setGraphic(new AutoTooltipLabel(item.getMarketDescription())); else setGraphic(null); } }; } }); } private void setStateColumnCellFactory() { stateColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue())); stateColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final FailedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null) setGraphic(new AutoTooltipLabel(item.getState())); else setGraphic(null); } }; } }); } private void setAmountColumnCellFactory() { amountColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue())); amountColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final FailedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null) setGraphic(new AutoTooltipLabel(item.getAmountAsString())); else setGraphic(null); } }; } }); } private void setPriceColumnCellFactory() { priceColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue())); priceColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final FailedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null) setGraphic(new AutoTooltipLabel(item.getPriceAsString())); else setGraphic(null); } }; } }); } private void setVolumeColumnCellFactory() { volumeColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue())); volumeColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final FailedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null) setGraphic(new AutoTooltipLabel(item.getVolumeAsString())); else setGraphic(null); } }; } }); } private void setDirectionColumnCellFactory() { directionColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue())); directionColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final FailedTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null) setGraphic(new AutoTooltipLabel(item.getDirectionLabel())); else setGraphic(null); } }; } }); } private TableColumn setRemoveTradeColumnCellFactory() { removeTradeColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue())); removeTradeColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(FailedTradesListItem newItem, boolean empty) { super.updateItem(newItem, empty); if (!empty && newItem != null && newItem.getTrade().isDepositsPublished()) { Label icon = FormBuilder.getIcon(AwesomeIcon.UNDO); JFXButton iconButton = new JFXButton("", icon); iconButton.setStyle("-fx-cursor: hand;"); iconButton.getStyleClass().add("hidden-icon-button"); iconButton.setTooltip(new Tooltip(Res.get("portfolio.failed.revertToPending"))); iconButton.setOnAction(e -> onRevertTrade(newItem.getTrade())); setGraphic(iconButton); } else { setGraphic(null); } } }; } }); return removeTradeColumn; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.failedtrades; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.common.model.ActivatableWithDataModel; import haveno.desktop.common.model.ViewModel; import javafx.collections.ObservableList; class FailedTradesViewModel extends ActivatableWithDataModel implements ViewModel { private final CoinFormatter formatter; @Inject public FailedTradesViewModel(FailedTradesDataModel dataModel, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter) { super(dataModel); this.formatter = formatter; } public ObservableList getList() { return dataModel.getList(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOfferListItem.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.openoffer; import haveno.core.offer.Offer; import haveno.core.offer.OpenOffer; import lombok.Getter; /** * We could remove that wrapper if it is not needed for additional UI only fields. */ class OpenOfferListItem { @Getter private final OpenOffer openOffer; OpenOfferListItem(OpenOffer openOffer) { this.openOffer = openOffer; } OpenOfferListItem() { openOffer = null; } public Offer getOffer() { return openOffer.getOffer(); } public String getGroupId() { return openOffer.getGroupId(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersDataModel.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.openoffer; import com.google.inject.Inject; import haveno.common.UserThread; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; import haveno.core.offer.TriggerPriceService; import haveno.core.provider.price.PriceFeedService; import haveno.desktop.common.model.ActivatableDataModel; import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import java.util.stream.Collectors; class OpenOffersDataModel extends ActivatableDataModel { private final OpenOfferManager openOfferManager; private final PriceFeedService priceFeedService; private final ObservableList list = FXCollections.observableArrayList(); private final ListChangeListener tradesListChangeListener; private final ChangeListener currenciesUpdateFlagPropertyListener; @Inject public OpenOffersDataModel(OpenOfferManager openOfferManager, PriceFeedService priceFeedService) { this.openOfferManager = openOfferManager; this.priceFeedService = priceFeedService; tradesListChangeListener = change -> UserThread.execute(() -> applyList()); currenciesUpdateFlagPropertyListener = (observable, oldValue, newValue) -> UserThread.execute(() -> applyList()); } @Override protected void activate() { openOfferManager.getObservableList().addListener(tradesListChangeListener); priceFeedService.updateCounterProperty().addListener(currenciesUpdateFlagPropertyListener); applyList(); } @Override protected void deactivate() { openOfferManager.getObservableList().removeListener(tradesListChangeListener); priceFeedService.updateCounterProperty().removeListener(currenciesUpdateFlagPropertyListener); } void onActivateOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { openOfferManager.activateOpenOffer(openOffer, resultHandler, errorMessageHandler); } void onDeactivateOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { openOfferManager.deactivateOpenOffer(openOffer, false, resultHandler, errorMessageHandler); } void onRemoveOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { openOfferManager.cancelOpenOffer(openOffer, resultHandler, errorMessageHandler); } public ObservableList getList() { return list; } public OfferDirection getDirection(Offer offer) { return openOfferManager.isMyOffer(offer) ? offer.getDirection() : offer.getMirroredDirection(); } private synchronized void applyList() { list.clear(); list.addAll(openOfferManager.getOpenOffers().stream().map(OpenOfferListItem::new).collect(Collectors.toList())); // we sort by date, earliest first list.sort((o1, o2) -> o2.getOffer().getDate().compareTo(o1.getOffer().getDate())); } boolean isTriggered(OpenOffer openOffer) { return TriggerPriceService.isTriggered(priceFeedService.getMarketPrice(openOffer.getOffer().getCounterCurrencyCode()), openOffer); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.openoffer; import com.google.inject.Inject; import com.googlecode.jcsv.writer.CSVEntryConverter; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import haveno.core.locale.Res; import haveno.core.offer.Offer; import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; import haveno.core.user.DontShowAgainLookup; import haveno.desktop.Navigation; import haveno.desktop.common.view.ActivatableViewAndModel; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.AutoTooltipSlideToggleButton; import haveno.desktop.components.AutoTooltipTableColumn; import haveno.desktop.components.HyperlinkWithIcon; import haveno.desktop.components.InputTextField; import haveno.desktop.main.MainView; import haveno.desktop.main.funds.FundsView; import haveno.desktop.main.funds.withdrawal.WithdrawalView; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.OfferDetailsWindow; import haveno.desktop.main.portfolio.PortfolioView; import haveno.desktop.main.portfolio.presentation.PortfolioUtil; import static haveno.desktop.util.FormBuilder.getRegularIconButton; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.GUIUtil; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import javafx.beans.binding.Bindings; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.value.ChangeListener; import javafx.collections.ListChangeListener; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; import javafx.fxml.FXML; import javafx.geometry.Insets; import javafx.scene.control.Button; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.MenuItem; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableRow; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; import javafx.scene.image.ImageView; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.stage.Stage; import javafx.util.Callback; import org.jetbrains.annotations.NotNull; @FxmlView public class OpenOffersView extends ActivatableViewAndModel { private enum ColumnNames { OFFER_ID(Res.get("shared.offerId")), GROUP_ID(Res.get("openOffer.header.groupId")), DATE(Res.get("shared.dateTime")), MARKET(Res.get("shared.market")), PRICE(Res.get("shared.price")), DEVIATION(Res.get("shared.deviation")), TRIGGER_PRICE(Res.get("openOffer.header.triggerPrice")), AMOUNT(Res.get("shared.XMRMinMax")), VOLUME(Res.get("shared.amountMinMax")), PAYMENT_METHOD(Res.get("shared.paymentMethod")), DIRECTION(Res.get("shared.offerType")), STATUS(Res.get("shared.state")); private final String text; ColumnNames(String text) { this.text = text; } @Override public String toString() { return text; } } @FXML TableView tableView; @FXML TableColumn priceColumn, deviationColumn, amountColumn, volumeColumn, marketColumn, directionColumn, dateColumn, offerIdColumn, deactivateItemColumn, groupIdColumn, removeItemColumn, editItemColumn, triggerPriceColumn, triggerIconColumn, paymentMethodColumn, duplicateItemColumn, cloneItemColumn; @FXML HBox searchBox; @FXML InputTextField filterTextField; @FXML Pane searchBoxSpacer; @FXML Label numItems; @FXML Region footerSpacer; @FXML AutoTooltipButton exportButton; @FXML AutoTooltipSlideToggleButton selectToggleButton; private final Navigation navigation; private final OfferDetailsWindow offerDetailsWindow; private SortedList sortedList; private FilteredList filteredList; private ChangeListener filterTextFieldListener; private final OpenOfferManager openOfferManager; private PortfolioView.EditOpenOfferHandler editOpenOfferHandler; private PortfolioView.CloneOpenOfferHandler cloneOpenOfferHandler; private ChangeListener widthListener; private ListChangeListener sortedListChangedListener; private Map> offerStateChangeListeners = new HashMap>(); @Inject public OpenOffersView(OpenOffersViewModel model, OpenOfferManager openOfferManager, Navigation navigation, OfferDetailsWindow offerDetailsWindow) { super(model); this.navigation = navigation; this.offerDetailsWindow = offerDetailsWindow; this.openOfferManager = openOfferManager; } @Override public void initialize() { GUIUtil.applyTableStyle(tableView); widthListener = (observable, oldValue, newValue) -> onWidthChange((double) newValue); groupIdColumn.setGraphic(new AutoTooltipLabel(ColumnNames.GROUP_ID.toString())); paymentMethodColumn.setGraphic(new AutoTooltipLabel(ColumnNames.PAYMENT_METHOD.toString())); priceColumn.setGraphic(new AutoTooltipLabel(ColumnNames.PRICE.toString())); deviationColumn.setGraphic(new AutoTooltipTableColumn<>(ColumnNames.DEVIATION.toString(), Res.get("portfolio.closedTrades.deviation.help")).getGraphic()); triggerPriceColumn.setGraphic(new AutoTooltipLabel(ColumnNames.TRIGGER_PRICE.toString())); amountColumn.setGraphic(new AutoTooltipLabel(ColumnNames.AMOUNT.toString())); volumeColumn.setGraphic(new AutoTooltipLabel(ColumnNames.VOLUME.toString())); marketColumn.setGraphic(new AutoTooltipLabel(ColumnNames.MARKET.toString())); directionColumn.setGraphic(new AutoTooltipLabel(ColumnNames.DIRECTION.toString())); dateColumn.setGraphic(new AutoTooltipLabel(ColumnNames.DATE.toString())); offerIdColumn.setGraphic(new AutoTooltipLabel(ColumnNames.OFFER_ID.toString())); deactivateItemColumn.setGraphic(new AutoTooltipLabel(ColumnNames.STATUS.toString())); editItemColumn.setGraphic(new AutoTooltipLabel("")); duplicateItemColumn.setText(""); cloneItemColumn.setText(""); removeItemColumn.setGraphic(new AutoTooltipLabel("")); setOfferIdColumnCellFactory(); setGroupIdCellFactory(); setDirectionColumnCellFactory(); setMarketColumnCellFactory(); setPriceColumnCellFactory(); setDeviationColumnCellFactory(); setAmountColumnCellFactory(); setVolumeColumnCellFactory(); setPaymentMethodColumnCellFactory(); setDateColumnCellFactory(); setDeactivateColumnCellFactory(); setEditColumnCellFactory(); setTriggerIconColumnCellFactory(); setTriggerPriceColumnCellFactory(); setDuplicateColumnCellFactory(); setCloneColumnCellFactory(); setRemoveColumnCellFactory(); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); tableView.setPlaceholder(new AutoTooltipLabel(Res.get("table.placeholder.noItems", Res.get("shared.openOffers")))); offerIdColumn.setComparator(Comparator.comparing(o -> o.getOffer().getId())); groupIdColumn.setComparator(Comparator.comparing(o -> o.getOpenOffer().getReserveTxHash() == null ? "" : o.getOpenOffer().getReserveTxHash())); directionColumn.setComparator(Comparator.comparing(o -> o.getOffer().getDirection())); marketColumn.setComparator(Comparator.comparing(model::getMarketLabel)); amountColumn.setComparator(Comparator.comparing(o -> o.getOffer().getAmount())); priceColumn.setComparator(Comparator.comparing(o -> o.getOffer().getPrice(), Comparator.nullsFirst(Comparator.naturalOrder()))); deviationColumn.setComparator(Comparator.comparing(model::getPriceDeviationAsDouble, Comparator.nullsFirst(Comparator.naturalOrder()))); triggerPriceColumn.setComparator(Comparator.comparing(o -> o.getOpenOffer().getTriggerPrice(), Comparator.nullsFirst(Comparator.naturalOrder()))); volumeColumn.setComparator(Comparator.comparing(o -> o.getOffer().getVolume(), Comparator.nullsFirst(Comparator.naturalOrder()))); dateColumn.setComparator(Comparator.comparing(o -> o.getOffer().getDate())); paymentMethodColumn.setComparator(Comparator.comparing(o -> Res.get(o.getOffer().getPaymentMethod().getId()))); dateColumn.setSortType(TableColumn.SortType.ASCENDING); tableView.getSortOrder().add(dateColumn); tableView.setRowFactory( tableView -> { final TableRow row = new TableRow<>(); final ContextMenu rowMenu = new ContextMenu(); MenuItem duplicateOfferMenuItem = new MenuItem(Res.get("portfolio.context.offerLikeThis")); duplicateOfferMenuItem.setOnAction((event) -> onDuplicateOffer(row.getItem())); rowMenu.getItems().add(duplicateOfferMenuItem); MenuItem cloneOfferMenuItem = new MenuItem(Res.get("offerbook.cloneOffer")); cloneOfferMenuItem.setOnAction((event) -> onCloneOffer(row.getItem())); rowMenu.getItems().add(cloneOfferMenuItem); row.contextMenuProperty().bind( Bindings.when(Bindings.isNotNull(row.itemProperty())) .then(rowMenu) .otherwise((ContextMenu) null)); return row; }); filterTextField.setPromptText(Res.get("shared.filter")); filterTextFieldListener = (observable, oldValue, newValue) -> applyFilteredListPredicate(filterTextField.getText()); searchBox.setSpacing(5); HBox.setHgrow(searchBoxSpacer, Priority.ALWAYS); selectToggleButton.setPadding(new Insets(0, 60, -20, 0)); selectToggleButton.setText(Res.get("shared.enabled")); selectToggleButton.setDisable(true); numItems.setId("num-offers"); numItems.setPadding(new Insets(-5, 0, 0, 10)); HBox.setHgrow(footerSpacer, Priority.ALWAYS); HBox.setMargin(exportButton, new Insets(0, 10, 0, 0)); exportButton.updateText(Res.get("shared.exportCSV")); sortedListChangedListener = c -> { c.next(); if (c.wasAdded() || c.wasRemoved()) { updateNumberOfOffers(); updateGroupIdColumnVisibility(); updateTriggerColumnVisibility(); } }; } @Override protected void activate() { filteredList = new FilteredList<>(model.getList()); sortedList = new SortedList<>(filteredList); sortedList.comparatorProperty().bind(tableView.comparatorProperty()); sortedList.addListener(sortedListChangedListener); tableView.setItems(sortedList); updateGroupIdColumnVisibility(); updateTriggerColumnVisibility(); updateSelectToggleButtonState(); selectToggleButton.setOnAction(event -> { if (model.isBootstrappedOrShowPopup()) { if (selectToggleButton.isSelected()) { sortedList.forEach(openOfferListItem -> onActivateOpenOffer(openOfferListItem.getOpenOffer())); } else { sortedList.forEach(openOfferListItem -> onDeactivateOpenOffer(openOfferListItem.getOpenOffer())); } } tableView.refresh(); }); numItems.setText(Res.get("shared.numItemsLabel", sortedList.size())); exportButton.setOnAction(event -> { CSVEntryConverter headerConverter = item -> { String[] columns = new String[ColumnNames.values().length]; for (ColumnNames m : ColumnNames.values()) { columns[m.ordinal()] = m.toString(); } return columns; }; CSVEntryConverter contentConverter = item -> { String[] columns = new String[ColumnNames.values().length]; columns[ColumnNames.OFFER_ID.ordinal()] = model.getOfferId(item); columns[ColumnNames.GROUP_ID.ordinal()] = openOfferManager.hasClonedOffer(item.getOffer().getId()) ? getShortenedGroupId(item.getGroupId()) : ""; columns[ColumnNames.DATE.ordinal()] = model.getDate(item); columns[ColumnNames.MARKET.ordinal()] = model.getMarketLabel(item); columns[ColumnNames.PRICE.ordinal()] = model.getPrice(item); columns[ColumnNames.DEVIATION.ordinal()] = model.getPriceDeviation(item); columns[ColumnNames.TRIGGER_PRICE.ordinal()] = model.getTriggerPrice(item); columns[ColumnNames.AMOUNT.ordinal()] = model.getAmount(item); columns[ColumnNames.VOLUME.ordinal()] = model.getVolume(item); columns[ColumnNames.PAYMENT_METHOD.ordinal()] = model.getPaymentMethod(item); columns[ColumnNames.DIRECTION.ordinal()] = model.getDirectionLabel(item); columns[ColumnNames.STATUS.ordinal()] = String.valueOf(!item.getOpenOffer().isDeactivated()); return columns; }; GUIUtil.exportCSV("openOffers.csv", headerConverter, contentConverter, new OpenOfferListItem(), sortedList, (Stage) root.getScene().getWindow()); }); filterTextField.textProperty().addListener(filterTextFieldListener); applyFilteredListPredicate(filterTextField.getText()); root.widthProperty().addListener(widthListener); onWidthChange(root.getWidth()); } private void updateNumberOfOffers() { numItems.setText(Res.get("shared.numItemsLabel", sortedList.size())); } private void updateGroupIdColumnVisibility() { groupIdColumn.setVisible(openOfferManager.hasClonedOffers()); } private void updateTriggerColumnVisibility() { triggerIconColumn.setVisible(model.dataModel.getList().stream() .mapToLong(item -> item.getOpenOffer().getTriggerPrice()) .sum() > 0); } @Override protected void deactivate() { sortedList.comparatorProperty().unbind(); sortedList.removeListener(sortedListChangedListener); exportButton.setOnAction(null); filterTextField.textProperty().removeListener(filterTextFieldListener); root.widthProperty().removeListener(widthListener); } private void refresh() { tableView.refresh(); updateSelectToggleButtonState(); } private void updateSelectToggleButtonState() { List availableItems = sortedList.stream() .filter(openOfferListItem -> !openOfferListItem.getOpenOffer().isPending()) .collect(Collectors.toList()); if (availableItems.size() == 0) { selectToggleButton.setDisable(true); selectToggleButton.setSelected(false); } else { selectToggleButton.setDisable(false); long numDeactivated = availableItems.stream() .filter(openOfferListItem -> openOfferListItem.getOpenOffer().isDeactivated()) .count(); if (numDeactivated == availableItems.size()) { selectToggleButton.setSelected(false); } else if (numDeactivated == 0) { selectToggleButton.setSelected(true); } } } private void applyFilteredListPredicate(String filterString) { filteredList.setPredicate(item -> { if (filterString.isEmpty()) return true; Offer offer = item.getOpenOffer().getOffer(); if (offer.getId().toLowerCase().contains(filterString.toLowerCase())) { return true; } if (model.getDate(item).toLowerCase().contains(filterString.toLowerCase())) { return true; } if (model.getMarketLabel(item).toLowerCase().contains(filterString.toLowerCase())) { return true; } if (model.getPrice(item).toLowerCase().contains(filterString.toLowerCase())) { return true; } if (model.getPriceDeviation(item).toLowerCase().contains(filterString.toLowerCase())) { return true; } if (model.getPaymentMethod(item).toLowerCase().contains(filterString.toLowerCase())) { return true; } if (model.getVolume(item).toLowerCase().contains(filterString.toLowerCase())) { return true; } if (model.getAmount(item).toLowerCase().contains(filterString.toLowerCase())) { return true; } if (model.getDirectionLabel(item).toLowerCase().contains(filterString.toLowerCase())) { return true; } if (item.getOffer().getCombinedExtraInfo() != null && item.getOffer().getCombinedExtraInfo().toLowerCase().contains(filterString.toLowerCase())) { return true; } return false; }); } private void onWidthChange(double width) { triggerPriceColumn.setVisible(width > 1300); } private void onDeactivateOpenOffer(OpenOffer openOffer) { if (model.isBootstrappedOrShowPopup()) { model.onDeactivateOpenOffer(openOffer, () -> log.debug("Deactivate offer was successful"), (message) -> { log.error(message); new Popup().warning(message).show(); }); updateSelectToggleButtonState(); } } private void onActivateOpenOffer(OpenOffer openOffer) { if (model.isBootstrappedOrShowPopup() && !model.dataModel.isTriggered(openOffer)) { model.onActivateOpenOffer(openOffer, () -> log.debug("Activate offer was successful"), (message) -> { log.error(message); new Popup().warning(Res.get("offerbook.activateOffer.failed", message)).show(); }); updateSelectToggleButtonState(); } } private void onRemoveOpenOffer(OpenOffer openOffer) { if (model.isBootstrappedOrShowPopup()) { String key = "RemoveOfferWarning"; if (DontShowAgainLookup.showAgain(key)) { new Popup().warning(Res.get("popup.warning.removeOffer")) .actionButtonText(Res.get("shared.removeOffer")) .onAction(() -> doRemoveOpenOffer(openOffer)) .closeButtonText(Res.get("shared.dontRemoveOffer")) .dontShowAgainId(key) .show(); } else { doRemoveOpenOffer(openOffer); } updateSelectToggleButtonState(); } } private void doRemoveOpenOffer(OpenOffer openOffer) { boolean hasClonedOffer = openOfferManager.hasClonedOffer(openOffer.getId()); model.onRemoveOpenOffer(openOffer, () -> { log.debug("Remove offer was successful"); tableView.refresh(); // We do not show the popup if it's a cloned offer with shared maker reserve tx if (hasClonedOffer) { return; } String key = "WithdrawFundsAfterRemoveOfferInfo"; if (DontShowAgainLookup.showAgain(key)) { new Popup().instruction(Res.get("offerbook.withdrawFundsHint", Res.get("funds.tab.withdrawal"))) .actionButtonTextWithGoTo("funds.tab.withdrawal") .onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, WithdrawalView.class)) .dontShowAgainId(key) .show(); } }, (message) -> { log.error(message); new Popup().warning(Res.get("offerbook.removeOffer.failed", message)).show(); }); } private void onEditOpenOffer(OpenOffer openOffer) { if (model.isBootstrappedOrShowPopup()) { editOpenOfferHandler.onEditOpenOffer(openOffer); } } private void onDuplicateOffer(OpenOfferListItem item) { if (item == null || item.getOffer().getOfferPayload() == null) { return; } if (model.isBootstrappedOrShowPopup()) { PortfolioUtil.duplicateOffer(navigation, item.getOffer().getOfferPayload()); } } private void onCloneOffer(OpenOfferListItem item) { if (item == null) { return; } if (model.isBootstrappedOrShowPopup()) { String key = "clonedOfferInfo"; if (DontShowAgainLookup.showAgain(key)) { new Popup().headLine(Res.get("offerbook.clonedOffer.headline")) .instruction(Res.get("offerbook.clonedOffer.info")) .useIUnderstandButton() .dontShowAgainId(key) .onClose(() -> doCloneOffer(item)) .show(); } else { doCloneOffer(item); } } } private void doCloneOffer(OpenOfferListItem item) { OpenOffer openOffer = item.getOpenOffer(); if (openOffer == null || openOffer.getOffer() == null || openOffer.getOffer().getOfferPayload() == null) { return; } cloneOpenOfferHandler.onCloneOpenOffer(openOffer); } private void setOfferIdColumnCellFactory() { offerIdColumn.setCellValueFactory((openOfferListItem) -> new ReadOnlyObjectWrapper<>(openOfferListItem.getValue())); offerIdColumn.getStyleClass().addAll("number-column"); offerIdColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { private HyperlinkWithIcon hyperlinkWithIcon; @Override public void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { hyperlinkWithIcon = new HyperlinkWithIcon(item.getOffer().getShortId()); if (model.isDeactivated(item)) { // getStyleClass().add("offer-disabled"); does not work with hyperlinkWithIcon;-( hyperlinkWithIcon.setStyle("-fx-text-fill: -bs-color-gray-3;"); hyperlinkWithIcon.getIcon().setOpacity(0.2); } hyperlinkWithIcon.setOnAction(event -> { offerDetailsWindow.show(item.getOffer()); }); hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails"))); setGraphic(hyperlinkWithIcon); } else { setGraphic(null); if (hyperlinkWithIcon != null) hyperlinkWithIcon.setOnAction(null); } } }; } }); } private void setGroupIdCellFactory() { groupIdColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); groupIdColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); getStyleClass().removeAll("offer-disabled"); if (item != null) { Label label; Text icon; if (openOfferManager.hasClonedOffer(item.getOpenOffer().getId())) { label = new Label(getShortenedGroupId(item.getOpenOffer().getGroupId())); icon = FormBuilder.getRegularIconForLabel(MaterialDesignIcon.LINK, label, "icon"); icon.setVisible(true); setTooltip(new Tooltip(Res.get("offerbook.clonedOffer.tooltip", item.getOpenOffer().getReserveTxHash()))); } else { label = new Label(""); icon = FormBuilder.getRegularIconForLabel(MaterialDesignIcon.LINK_OFF, label, "icon"); icon.setVisible(false); setTooltip(new Tooltip(Res.get("offerbook.nonClonedOffer.tooltip", item.getOpenOffer().getReserveTxHash()))); } if (model.isDeactivated(item)) { getStyleClass().add("offer-disabled"); icon.setOpacity(0.2); } setGraphic(label); } else { setGraphic(null); } } }; } }); } private String getShortenedGroupId(String groupId) { if (groupId.length() > 5) { return groupId.substring(0, 5); } return groupId; } private void setDateColumnCellFactory() { dateColumn.setCellValueFactory((openOfferListItem) -> new ReadOnlyObjectWrapper<>(openOfferListItem.getValue())); dateColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); getStyleClass().removeAll("offer-disabled"); if (item != null) { if (model.isDeactivated(item)) getStyleClass().add("offer-disabled"); setGraphic(new AutoTooltipLabel(model.getDate(item))); } else { setGraphic(null); } } }; } }); } private void setAmountColumnCellFactory() { amountColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); amountColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); getStyleClass().removeAll("offer-disabled"); if (item != null) { if (model.isDeactivated(item)) getStyleClass().add("offer-disabled"); setGraphic(new AutoTooltipLabel(model.getAmount(item))); } else { setGraphic(null); } } }; } }); } private void setPriceColumnCellFactory() { priceColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); priceColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); getStyleClass().removeAll("offer-disabled"); if (item != null) { if (model.isDeactivated(item)) getStyleClass().add("offer-disabled"); setGraphic(new AutoTooltipLabel(model.getPrice(item))); } else { setGraphic(null); } } }; } }); } private void setDeviationColumnCellFactory() { deviationColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); deviationColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); getStyleClass().removeAll("offer-disabled"); if (item != null) { if (model.isDeactivated(item)) getStyleClass().add("offer-disabled"); AutoTooltipLabel autoTooltipLabel = new AutoTooltipLabel(model.getPriceDeviation(item)); autoTooltipLabel.setOpacity(item.getOffer().isUseMarketBasedPrice() ? 1 : 0.4); setGraphic(autoTooltipLabel); } else { setGraphic(null); } } }; } }); } private void setTriggerPriceColumnCellFactory() { triggerPriceColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); triggerPriceColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); getStyleClass().removeAll("offer-disabled"); if (item != null) { if (model.isDeactivated(item)) getStyleClass().add("offer-disabled"); setGraphic(new AutoTooltipLabel(model.getTriggerPrice(item))); } else { setGraphic(null); } } }; } }); } private void setVolumeColumnCellFactory() { volumeColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); volumeColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); getStyleClass().removeAll("offer-disabled"); if (item != null) { if (model.isDeactivated(item)) getStyleClass().add("offer-disabled"); setGraphic(new AutoTooltipLabel(model.getVolume(item))); } else { setGraphic(null); } } }; } }); } private void setPaymentMethodColumnCellFactory() { paymentMethodColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); paymentMethodColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); getStyleClass().removeAll("offer-disabled"); if (item != null) { if (model.isDeactivated(item)) getStyleClass().add("offer-disabled"); setGraphic(new AutoTooltipLabel(model.getPaymentMethod(item))); } else { setGraphic(null); } } }; } }); } private void setDirectionColumnCellFactory() { directionColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); directionColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); getStyleClass().removeAll("offer-disabled"); if (item != null) { if (model.isDeactivated(item)) getStyleClass().add("offer-disabled"); setGraphic(new AutoTooltipLabel(model.getDirectionLabel(item))); } else { setGraphic(null); } } }; } }); } private void setMarketColumnCellFactory() { marketColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); marketColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); getStyleClass().removeAll("offer-disabled"); if (item != null) { if (model.isDeactivated(item)) getStyleClass().add("offer-disabled"); setGraphic(new AutoTooltipLabel(model.getMarketLabel(item))); } else { setGraphic(null); } } }; } }); } private void setDeactivateColumnCellFactory() { deactivateItemColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); deactivateItemColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { final ImageView iconView = new ImageView(); AutoTooltipSlideToggleButton checkBox; private void updateState(@NotNull OpenOffer openOffer) { if (checkBox != null) checkBox.setSelected(openOffer.getState() == OpenOffer.State.AVAILABLE); } @Override public void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { OpenOffer openOffer = item.getOpenOffer(); // refresh on state change if (offerStateChangeListeners.containsKey(openOffer.getId())) { openOffer.stateProperty().removeListener(offerStateChangeListeners.get(openOffer.getId())); offerStateChangeListeners.remove(openOffer.getId()); } ChangeListener listener = (observable, oldValue, newValue) -> { if (oldValue != newValue) refresh(); }; offerStateChangeListeners.put(openOffer.getId(), listener); openOffer.stateProperty().addListener(listener); if (openOffer.getState() == OpenOffer.State.PENDING) { setGraphic(new AutoTooltipLabel(Res.get("shared.pending"))); return; } if (checkBox == null) { checkBox = new AutoTooltipSlideToggleButton(); checkBox.setPadding(new Insets(-7, 0, -7, 0)); checkBox.setGraphic(iconView); } checkBox.setDisable(model.dataModel.isTriggered(openOffer)); checkBox.setOnAction(event -> { if (openOffer.isDeactivated()) { onActivateOpenOffer(openOffer); } else { onDeactivateOpenOffer(openOffer); } updateState(openOffer); tableView.refresh(); }); updateState(openOffer); setGraphic(checkBox); } else { setGraphic(null); if (checkBox != null) { checkBox.setOnAction(null); checkBox = null; } } } }; } }); } private void setRemoveColumnCellFactory() { removeItemColumn.getStyleClass().addAll("avatar-column"); removeItemColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); removeItemColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { Button button; @Override public void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { if (button == null) { button = getRegularIconButton(MaterialDesignIcon.DELETE_FOREVER, "delete"); button.setTooltip(new Tooltip(Res.get("shared.removeOffer"))); setGraphic(button); } button.setOnAction(event -> onRemoveOpenOffer(item.getOpenOffer())); } else { setGraphic(null); if (button != null) { button.setOnAction(null); button = null; } } } }; } }); } private void setDuplicateColumnCellFactory() { duplicateItemColumn.getStyleClass().add("avatar-column"); duplicateItemColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); duplicateItemColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { Button button; @Override public void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { if (button == null) { button = getRegularIconButton(MaterialDesignIcon.CONTENT_COPY); button.setTooltip(new Tooltip(Res.get("portfolio.context.offerLikeThis"))); setGraphic(button); } button.setOnAction(event -> onDuplicateOffer(item)); } else { setGraphic(null); if (button != null) { button.setOnAction(null); button = null; } } } }; } }); } private void setCloneColumnCellFactory() { cloneItemColumn.getStyleClass().add("avatar-column"); cloneItemColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); cloneItemColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { Button button; @Override public void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { if (button == null) { button = getRegularIconButton(MaterialDesignIcon.BOX_SHADOW); button.setTooltip(new Tooltip(Res.get("offerbook.cloneOffer"))); setGraphic(button); } button.setOnAction(event -> onCloneOffer(item)); } else { setGraphic(null); if (button != null) { button.setOnAction(null); button = null; } } } }; } }); } private void setTriggerIconColumnCellFactory() { triggerIconColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); triggerIconColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { private final Button button = getRegularIconButton(MaterialDesignIcon.SHIELD_HALF_FULL); @Override protected void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); if (item == null || empty) { setGraphic(null); button.setOnAction(null); return; } boolean triggerPriceSet = item.getOpenOffer().getTriggerPrice() > 0; button.setVisible(triggerPriceSet); if (model.dataModel.isTriggered(item.getOpenOffer())) { button.getGraphic().getStyleClass().add("warning"); button.setTooltip(new Tooltip(Res.get("openOffer.triggered"))); } else { button.getGraphic().getStyleClass().remove("warning"); button.setTooltip(new Tooltip(Res.get("openOffer.triggerPrice", model.getTriggerPrice(item)))); } button.setOnAction(e -> onEditOpenOffer(item.getOpenOffer())); setGraphic(button); } }; } }); } private void setEditColumnCellFactory() { editItemColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); editItemColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { Button button; @Override public void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { if (button == null) { button = getRegularIconButton(MaterialDesignIcon.PENCIL); button.setTooltip(new Tooltip(Res.get("shared.editOffer"))); setGraphic(button); } button.setOnAction(event -> onEditOpenOffer(item.getOpenOffer())); } else { setGraphic(null); if (button != null) { button.setOnAction(null); button = null; } } } }; } }); } public void setEditOpenOfferHandler(PortfolioView.EditOpenOfferHandler editOpenOfferHandler) { this.editOpenOfferHandler = editOpenOfferHandler; } public void setCloneOpenOfferHandler(PortfolioView.CloneOpenOfferHandler cloneOpenOfferHandler) { this.cloneOpenOfferHandler = cloneOpenOfferHandler; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.openoffer; import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.monetary.Price; import haveno.core.offer.Offer; import haveno.core.offer.OpenOffer; import haveno.core.util.FormattingUtils; import haveno.core.util.PriceUtil; import haveno.core.util.VolumeUtil; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.common.model.ActivatableWithDataModel; import haveno.desktop.common.model.ViewModel; import haveno.desktop.util.DisplayUtils; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.P2PService; import javafx.collections.ObservableList; class OpenOffersViewModel extends ActivatableWithDataModel implements ViewModel { private final P2PService p2PService; private final PriceUtil priceUtil; private final CoinFormatter btcFormatter; @Inject public OpenOffersViewModel(OpenOffersDataModel dataModel, P2PService p2PService, PriceUtil priceUtil, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter) { super(dataModel); this.p2PService = p2PService; this.priceUtil = priceUtil; this.btcFormatter = btcFormatter; } @Override protected void activate() { } void onActivateOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { dataModel.onActivateOpenOffer(openOffer, resultHandler, errorMessageHandler); } void onDeactivateOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { dataModel.onDeactivateOpenOffer(openOffer, resultHandler, errorMessageHandler); } void onRemoveOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { dataModel.onRemoveOpenOffer(openOffer, resultHandler, errorMessageHandler); } public ObservableList getList() { return dataModel.getList(); } String getOfferId(OpenOfferListItem item) { return item.getOffer().getShortId(); } String getGroupId(OpenOfferListItem item) { return item.getGroupId(); } String getAmount(OpenOfferListItem item) { return (item != null) ? DisplayUtils.formatAmount(item.getOffer(), btcFormatter) : ""; } String getPrice(OpenOfferListItem item) { if ((item == null)) return ""; Offer offer = item.getOffer(); Price price = offer.getPrice(); if (price != null) { return FormattingUtils.formatPrice(price); } else { return Res.get("shared.na"); } } String getPriceDeviation(OpenOfferListItem item) { Offer offer = item.getOffer(); return priceUtil.getMarketBasedPrice(offer, offer.getMirroredDirection()) .map(FormattingUtils::formatPercentagePrice) .orElse(""); } Double getPriceDeviationAsDouble(OpenOfferListItem item) { Offer offer = item.getOffer(); return priceUtil.getMarketBasedPrice(offer, offer.getMirroredDirection()).orElse(0d); } String getVolume(OpenOfferListItem item) { return (item != null) ? VolumeUtil.formatVolume(item.getOffer(), false, 0) + " " + item.getOffer().getCounterCurrencyCode() : ""; } String getDirectionLabel(OpenOfferListItem item) { if ((item == null)) return ""; return DisplayUtils.getDirectionWithCode(dataModel.getDirection(item.getOffer()), item.getOffer().getCounterCurrencyCode(), item.getOffer().isPrivateOffer()); } String getMarketLabel(OpenOfferListItem item) { if ((item == null)) return ""; return CurrencyUtil.getCurrencyPair(item.getOffer().getCounterCurrencyCode()); } String getPaymentMethod(OpenOfferListItem item) { String result = ""; if (item != null) { Offer offer = item.getOffer(); checkNotNull(offer); checkNotNull(offer.getPaymentMethod()); result = offer.getPaymentMethodNameWithCountryCode(); } return result; } String getDate(OpenOfferListItem item) { return DisplayUtils.formatDateTime(item.getOffer().getDate()); } boolean isDeactivated(OpenOfferListItem item) { return item != null && item.getOpenOffer() != null && item.getOpenOffer().isDeactivated(); } boolean isBootstrappedOrShowPopup() { return GUIUtil.isBootstrappedOrShowPopup(p2PService); } String getTriggerPrice(OpenOfferListItem item) { if ((item == null)) { return ""; } Offer offer = item.getOffer(); long triggerPrice = item.getOpenOffer().getTriggerPrice(); if (!offer.isUseMarketBasedPrice() || triggerPrice <= 0) { return Res.get("shared.na"); } else { return PriceUtil.formatMarketPrice(triggerPrice, offer.getCounterCurrencyCode()); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/BuyerSubView.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.pendingtrades; import haveno.core.locale.Res; import haveno.desktop.main.portfolio.pendingtrades.steps.TradeWizardItem; import haveno.desktop.main.portfolio.pendingtrades.steps.buyer.BuyerStep1View; import haveno.desktop.main.portfolio.pendingtrades.steps.buyer.BuyerStep2View; import haveno.desktop.main.portfolio.pendingtrades.steps.buyer.BuyerStep3View; import haveno.desktop.main.portfolio.pendingtrades.steps.buyer.BuyerStep4View; import lombok.extern.slf4j.Slf4j; import org.fxmisc.easybind.EasyBind; @Slf4j public class BuyerSubView extends TradeSubView { private TradeWizardItem step1; private TradeWizardItem step2; private TradeWizardItem step3; private TradeWizardItem step4; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialisation /////////////////////////////////////////////////////////////////////////////////////////// public BuyerSubView(PendingTradesViewModel model) { super(model); } @Override protected void activate() { viewStateSubscription = EasyBind.subscribe(model.getBuyerState(), this::onViewStateChanged); super.activate(); } @Override protected void addWizards() { step1 = new TradeWizardItem(BuyerStep1View.class, Res.get("portfolio.pending.step1.waitForConf"), "1"); step2 = new TradeWizardItem(BuyerStep2View.class, Res.get("portfolio.pending.step2_buyer.startPayment"), "2"); step3 = new TradeWizardItem(BuyerStep3View.class, Res.get("portfolio.pending.step3_buyer.waitPaymentArrived"), "3"); step4 = new TradeWizardItem(BuyerStep4View.class, Res.get("portfolio.pending.step5.completed"), "4"); addWizardsToGridPane(step1); addLineSeparatorToGridPane(); addWizardsToGridPane(step2); addLineSeparatorToGridPane(); addWizardsToGridPane(step3); addLineSeparatorToGridPane(); addWizardsToGridPane(step4); } /////////////////////////////////////////////////////////////////////////////////////////// // State /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void onViewStateChanged(PendingTradesViewModel.State viewState) { super.onViewStateChanged(viewState); if (viewState != null && model.getTrade() != null) { PendingTradesViewModel.BuyerState buyerState = (PendingTradesViewModel.BuyerState) viewState; step1.setDisabled(); step2.setDisabled(); step3.setDisabled(); step4.setDisabled(); switch (buyerState) { case UNDEFINED: break; case STEP1: showItem(step1); break; case STEP2: step1.setCompleted(); showItem(step2); break; case STEP3: step1.setCompleted(); step2.setCompleted(); showItem(step3); break; case STEP4: step1.setCompleted(); step2.setCompleted(); step3.setCompleted(); showItem(step4); break; default: log.warn("unhandled buyerState " + buyerState); break; } } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.pendingtrades; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.UserThread; import haveno.common.crypto.PubKeyRing; import haveno.common.crypto.PubKeyRingProvider; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.FaultHandler; import haveno.common.handlers.ResultHandler; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.api.CoreDisputesService; import haveno.core.api.XmrConnectionService; import haveno.core.locale.Res; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OfferUtil; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.support.SupportType; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeList; import haveno.core.support.dispute.DisputeManager; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.mediation.MediationManager; import haveno.core.support.traderchat.TraderChatManager; import haveno.core.trade.BuyerTrade; import haveno.core.trade.SellerTrade; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.trade.protocol.BuyerProtocol; import haveno.core.trade.protocol.SellerProtocol; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.Navigation; import haveno.desktop.common.model.ActivatableDataModel; import haveno.desktop.main.MainView; import haveno.desktop.main.overlays.notifications.NotificationCenter; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.WalletPasswordWindow; import haveno.desktop.main.support.SupportView; import haveno.desktop.main.support.dispute.client.arbitration.ArbitrationClientView; import haveno.desktop.main.support.dispute.client.mediation.MediationClientView; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.P2PService; import java.math.BigInteger; import java.util.Date; import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javax.annotation.Nullable; import lombok.Getter; import org.bitcoinj.core.Coin; import org.bouncycastle.crypto.params.KeyParameter; public class PendingTradesDataModel extends ActivatableDataModel { @Getter public final TradeManager tradeManager; public final XmrWalletService xmrWalletService; public final ArbitrationManager arbitrationManager; public final MediationManager mediationManager; private final P2PService p2PService; private final XmrConnectionService xmrConnectionService; @Getter private final AccountAgeWitnessService accountAgeWitnessService; public final Navigation navigation; public final WalletPasswordWindow walletPasswordWindow; private final NotificationCenter notificationCenter; private final OfferUtil offerUtil; private final CoinFormatter btcFormatter; final ObservableList list = FXCollections.observableArrayList(); private final ListChangeListener tradesListChangeListener; private boolean isMaker; final ObjectProperty selectedItemProperty = new SimpleObjectProperty<>(); public final StringProperty makerTxId = new SimpleStringProperty(); public final StringProperty takerTxId = new SimpleStringProperty(); @Getter private final TraderChatManager traderChatManager; public final Preferences preferences; private boolean activated; private ChangeListener tradeStateChangeListener; private Trade selectedTrade; @Getter private final PubKeyRingProvider pubKeyRingProvider; private final CoreDisputesService disputesService; private final Set hiddenTrades = new HashSet(); private final ChangeListener hiddenStateChangeListener = (observable, oldValue, newValue) -> { onListChanged(); }; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, initialization /////////////////////////////////////////////////////////////////////////////////////////// @Inject public PendingTradesDataModel(TradeManager tradeManager, XmrWalletService xmrWalletService, PubKeyRingProvider pubKeyRingProvider, ArbitrationManager arbitrationManager, MediationManager mediationManager, TraderChatManager traderChatManager, Preferences preferences, P2PService p2PService, XmrConnectionService xmrConnectionService, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation, WalletPasswordWindow walletPasswordWindow, NotificationCenter notificationCenter, OfferUtil offerUtil, CoreDisputesService disputesService, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter) { this.tradeManager = tradeManager; this.xmrWalletService = xmrWalletService; this.pubKeyRingProvider = pubKeyRingProvider; this.arbitrationManager = arbitrationManager; this.mediationManager = mediationManager; this.traderChatManager = traderChatManager; this.preferences = preferences; this.p2PService = p2PService; this.xmrConnectionService = xmrConnectionService; this.accountAgeWitnessService = accountAgeWitnessService; this.navigation = navigation; this.walletPasswordWindow = walletPasswordWindow; this.notificationCenter = notificationCenter; this.offerUtil = offerUtil; this.disputesService = disputesService; this.btcFormatter = formatter; tradesListChangeListener = change -> onListChanged(); notificationCenter.setSelectItemByTradeIdConsumer(this::selectItemByTradeId); } @Override protected void activate() { tradeManager.getObservableList().addListener(tradesListChangeListener); onListChanged(); if (selectedItemProperty.get() != null) notificationCenter.setSelectedTradeId(selectedItemProperty.get().getTrade().getId()); activated = true; } @Override protected void deactivate() { for (Trade trade : hiddenTrades) trade.stateProperty().removeListener(hiddenStateChangeListener); tradeManager.getObservableList().removeListener(tradesListChangeListener); notificationCenter.setSelectedTradeId(null); activated = false; } /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// void onSelectItem(PendingTradesListItem item) { doSelectItem(item); } public void onPaymentSent(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { Trade trade = getTrade(); checkNotNull(trade, "trade must not be null"); checkArgument(trade instanceof BuyerTrade, "Check failed: trade instanceof BuyerTrade. Was: " + trade.getClass().getSimpleName()); ((BuyerProtocol) tradeManager.getTradeProtocol(trade)).onPaymentSent(resultHandler, errorMessageHandler); } public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { Trade trade = getTrade(); checkNotNull(trade, "trade must not be null"); checkArgument(trade instanceof SellerTrade, "Trade must be instance of SellerTrade"); ((SellerProtocol) tradeManager.getTradeProtocol(trade)).onPaymentReceived(resultHandler, errorMessageHandler); } public void onWithdrawRequest(String toAddress, Coin amount, Coin fee, KeyParameter aesKey, @Nullable String memo, ResultHandler resultHandler, FaultHandler faultHandler) { checkNotNull(getTrade(), "trade must not be null"); if (toAddress != null && toAddress.length() > 0) { tradeManager.onWithdrawRequest( toAddress, amount, fee, aesKey, getTrade(), memo, () -> { resultHandler.handleResult(); selectBestItem(); }, (errorMessage, throwable) -> { log.error(errorMessage); faultHandler.handleFault(errorMessage, throwable); }); } else { faultHandler.handleFault(Res.get("portfolio.pending.noReceiverAddressDefined"), null); } } public void onOpenDispute() { tryOpenDispute(false); } public void onOpenSupportTicket() { tryOpenDispute(true); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// @Nullable public Trade getTrade() { return selectedItemProperty.get() != null ? selectedItemProperty.get().getTrade() : null; } @Nullable Offer getOffer() { return getTrade() != null ? getTrade().getOffer() : null; } private boolean isBuyOffer() { return getOffer() != null && offerUtil.isBuyOffer(getOffer().getDirection()); } boolean isBuyer() { return (isMaker(getOffer()) && isBuyOffer()) || (!isMaker(getOffer()) && !isBuyOffer()); } boolean isMaker(Offer offer) { return tradeManager.isMyOffer(offer); } public boolean isMaker() { return isMaker; } BigInteger getTradeFee() { Trade trade = getTrade(); if (trade != null) { Offer offer = trade.getOffer(); if (isMaker()) { if (offer != null) { return trade.getMakerFee(); } else { log.error("offer is null"); return BigInteger.ZERO; } } else { return trade.getTakerFee(); } } else { log.error("Trade is null at getTotalFees"); return BigInteger.ZERO; } } @Nullable public PaymentAccountPayload getSellersPaymentAccountPayload() { if (getTrade() == null) return null; return getTrade().getSeller().getPaymentAccountPayload(); } @Nullable public PaymentAccountPayload getBuyersPaymentAccountPayload() { if (getTrade() == null) return null; return getTrade().getBuyer().getPaymentAccountPayload(); } public String getReference() { return getOffer() != null ? getOffer().getShortId() : ""; } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void onListChanged() { UserThread.execute(() -> { synchronized (tradeManager.getObservableList()) { // add or remove listener for hidden trades for (Trade trade : tradeManager.getObservableList()) { if (isTradeShown(trade)) { if (hiddenTrades.contains(trade)) { UserThread.execute(() -> trade.stateProperty().removeListener(hiddenStateChangeListener)); hiddenTrades.remove(trade); } } else { if (!hiddenTrades.contains(trade)) { UserThread.execute(() -> trade.stateProperty().addListener(hiddenStateChangeListener)); hiddenTrades.add(trade); } } } // add shown trades to list synchronized (list) { list.clear(); list.addAll(tradeManager.getObservableList().stream() .filter(trade -> isTradeShown(trade)) .map(trade -> new PendingTradesListItem(trade, btcFormatter)) .collect(Collectors.toList())); // we sort by date, earliest first list.sort((o1, o2) -> o2.getTrade().getDate().compareTo(o1.getTrade().getDate())); } } selectBestItem(); }); } private boolean isTradeShown(Trade trade) { return trade.isDepositsPublished(); } private void selectBestItem() { synchronized (list) { if (list.size() == 1) doSelectItem(list.get(0)); else if (list.size() > 1 && (selectedItemProperty.get() == null || !list.contains(selectedItemProperty.get()))) doSelectItem(list.get(0)); else if (list.size() == 0) doSelectItem(null); } } private void selectItemByTradeId(String tradeId) { if (activated) { synchronized (list) { list.stream().filter(e -> e.getTrade().getId().equals(tradeId)).findAny().ifPresent(this::doSelectItem); } } } private void doSelectItem(@Nullable PendingTradesListItem item) { UserThread.execute(() -> { if (selectedTrade != null) selectedTrade.stateProperty().removeListener(tradeStateChangeListener); if (item != null) { selectedTrade = item.getTrade(); if (selectedTrade == null) { log.error("selectedTrade is null"); return; } String tradeId = selectedTrade.getId(); tradeStateChangeListener = (observable, oldValue, newValue) -> { String makerDepositTxHash = selectedTrade.getMaker().getDepositTxHash(); String takerDepositTxHash = selectedTrade.getTaker().getDepositTxHash(); makerTxId.set(nullToEmptyString(makerDepositTxHash)); takerTxId.set(nullToEmptyString(takerDepositTxHash)); if (makerDepositTxHash != null || takerDepositTxHash != null) { notificationCenter.setSelectedTradeId(tradeId); UserThread.execute(() -> selectedTrade.stateProperty().removeListener(tradeStateChangeListener)); } }; selectedTrade.stateProperty().addListener(tradeStateChangeListener); Offer offer = selectedTrade.getOffer(); if (offer == null) { log.error("offer is null"); return; } isMaker = tradeManager.isMyOffer(offer); String makerDepositTxHash = selectedTrade.getMaker().getDepositTxHash(); String takerDepositTxHash = selectedTrade.getTaker().getDepositTxHash(); makerTxId.set(nullToEmptyString(makerDepositTxHash)); takerTxId.set(nullToEmptyString(takerDepositTxHash)); notificationCenter.setSelectedTradeId(tradeId); } else { selectedTrade = null; makerTxId.set(""); takerTxId.set(""); notificationCenter.setSelectedTradeId(null); } selectedItemProperty.set(item); }); } private String nullToEmptyString(String str) { return str == null ? "" : str; } private void tryOpenDispute(boolean isSupportTicket) { Trade trade = getTrade(); if (trade == null) { log.error("Trade is null"); return; } doOpenDispute(isSupportTicket, trade); } private void doOpenDispute(boolean isSupportTicket, Trade trade) { if (trade == null) { log.warn("trade is null at doOpenDispute"); return; } Offer offer = trade.getOffer(); if (offer == null) { log.warn("offer is null at doOpenDispute"); return; } if (!GUIUtil.isBootstrappedOrShowPopup(p2PService)) { return; } byte[] payoutTxSerialized = null; String payoutTxHashAsString = null; if (trade.getPayoutTxId() != null) { // payoutTxSerialized = payoutTx.bitcoinSerialize(); // TODO (woodser): no need to pass serialized txs for xmr // payoutTxHashAsString = payoutTx.getHashAsString(); } Trade.DisputeState disputeState = trade.getDisputeState(); DisputeManager> disputeManager; boolean useMediation; boolean useArbitration; // If mediation is not activated we use arbitration if (false) { // TODO (woodser): use mediation for xmr? if (MediationManager.isMediationActivated()) { // In case we re-open a dispute we allow Trade.DisputeState.MEDIATION_REQUESTED or useMediation = disputeState == Trade.DisputeState.NO_DISPUTE || disputeState == Trade.DisputeState.MEDIATION_REQUESTED || disputeState == Trade.DisputeState.DISPUTE_OPENED; // in case of arbitration disputeState == Trade.DisputeState.ARBITRATION_REQUESTED useArbitration = disputeState == Trade.DisputeState.MEDIATION_CLOSED || disputeState == Trade.DisputeState.DISPUTE_REQUESTED || disputeState == Trade.DisputeState.DISPUTE_OPENED; } else { useMediation = false; useArbitration = true; } // if (useMediation) { // // If no dispute state set we start with mediation // disputeManager = mediationManager; // PubKeyRing mediatorPubKeyRing = trade.getMediatorPubKeyRing(); // checkNotNull(mediatorPubKeyRing, "mediatorPubKeyRing must not be null"); // byte[] depositTxSerialized = null; // depositTx.bitcoinSerialize(); // TODO (woodser): no serialized txs in xmr // String depositTxHashAsString = null; // depositTx.getHashAsString(); // TODO (woodser): two deposit txs for dispute // Dispute dispute = new Dispute(new Date().getTime(), // trade.getId(), // pubKeyRing.hashCode(), // traderId // true, // (offer.getDirection() == OfferDirection.BUY) == isMaker, // isMaker, // pubKeyRing, // trade.getDate().getTime(), // trade.getMaxTradePeriodDate().getTime(), // trade.getContract(), // trade.getContractHash(), // payoutTxSerialized, // payoutTxHashAsString, // trade.getContractAsJson(), // trade.getMakerContractSignature(), // trade.getTakerContractSignature(), // mediatorPubKeyRing, // isSupportTicket, // SupportType.MEDIATION); ResultHandler resultHandler; if (useMediation) { // If no dispute state set we start with mediation resultHandler = () -> navigation.navigateTo(MainView.class, SupportView.class, MediationClientView.class); disputeManager = mediationManager; PubKeyRing arbitratorPubKeyRing = trade.getArbitrator().getPubKeyRing(); checkNotNull(arbitratorPubKeyRing, "arbitratorPubKeyRing must not be null"); byte[] depositTxSerialized = null; // depositTx.bitcoinSerialize(); // TODO (woodser): no serialized txs in xmr Dispute dispute = new Dispute(new Date().getTime(), trade.getId(), pubKeyRingProvider.get().hashCode(), // trader id true, (offer.getDirection() == OfferDirection.BUY) == isMaker, isMaker, pubKeyRingProvider.get(), trade.getDate().getTime(), trade.getMaxTradePeriodDate().getTime(), trade.getContract(), trade.getContractHash(), payoutTxSerialized, payoutTxHashAsString, trade.getContractAsJson(), trade.getMaker().getContractSignature(), trade.getTaker().getContractSignature(), trade.getMaker().getPaymentAccountPayload(), trade.getTaker().getPaymentAccountPayload(), arbitratorPubKeyRing, isSupportTicket, SupportType.MEDIATION); dispute.setExtraData("counterCurrencyTxId", trade.getCounterCurrencyTxId()); dispute.setExtraData("counterCurrencyExtraData", trade.getCounterCurrencyExtraData()); trade.setDisputeState(Trade.DisputeState.MEDIATION_REQUESTED); sendDisputeOpenedMessage(dispute, disputeManager); tradeManager.requestPersistence(); } else if (useArbitration) { disputeManager = arbitrationManager; Dispute dispute = disputesService.createDisputeForTrade(trade, offer, pubKeyRingProvider.get(), isMaker, isSupportTicket); // send dispute opened message sendDisputeOpenedMessage(dispute, disputeManager); tradeManager.requestPersistence(); } else { log.warn("Invalid dispute state {}", disputeState.name()); } } private void sendDisputeOpenedMessage(Dispute dispute, DisputeManager> disputeManager) { Optional optionalDispute = disputeManager.findDispute(dispute); boolean disputeClosed = optionalDispute.isPresent() && optionalDispute.get().isClosed(); if (disputeClosed) { String msg = "We got a dispute already open for that trade and trading peer.\n" + "TradeId = " + dispute.getTradeId(); new Popup().warning(msg + "\n\n" + Res.get("portfolio.pending.openAgainDispute.msg")) .actionButtonText(Res.get("portfolio.pending.openAgainDispute.button")) .onAction(() -> doSendDisputeOpenedMessage(dispute, disputeManager)) .closeButtonText(Res.get("shared.cancel")).show(); } else { doSendDisputeOpenedMessage(dispute, disputeManager); } } private void doSendDisputeOpenedMessage(Dispute dispute, DisputeManager> disputeManager) { navigation.navigateTo(MainView.class, SupportView.class, ArbitrationClientView.class); disputeManager.sendDisputeOpenedMessage(dispute, null, (errorMessage, throwable) -> new Popup().warning(errorMessage).show()); } public boolean isReadyForTxBroadcast() { return GUIUtil.isBootstrappedOrShowPopup(p2PService) && GUIUtil.isReadyForTxBroadcastOrShowPopup(xmrWalletService); } public boolean isBootstrappedOrShowPopup() { return GUIUtil.isBootstrappedOrShowPopup(p2PService); } public void onMoveInvalidTradeToFailedTrades(Trade trade) { tradeManager.onMoveInvalidTradeToFailedTrades(trade); } public boolean isSignWitnessTrade() { return accountAgeWitnessService.isSignWitnessTrade(selectedTrade); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesListItem.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.pendingtrades; import haveno.core.locale.CurrencyUtil; import haveno.core.monetary.Price; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.util.filtering.FilterableListItem; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * We could remove that wrapper if it is not needed for additional UI only fields. */ public class PendingTradesListItem implements FilterableListItem { public static final Logger log = LoggerFactory.getLogger(PendingTradesListItem.class); private final CoinFormatter btcFormatter; private final Trade trade; public PendingTradesListItem(Trade trade, CoinFormatter btcFormatter) { this.trade = trade; this.btcFormatter = btcFormatter; } public Trade getTrade() { return trade; } public Price getPrice() { return trade.getPrice(); } public String getPriceAsString() { return FormattingUtils.formatPrice(trade.getPrice()); } public String getAmountAsString() { return HavenoUtils.formatXmr(trade.getAmount()); } public String getPaymentMethod() { return trade.getOffer().getPaymentMethodNameWithCountryCode(); } public String getMarketDescription() { return CurrencyUtil.getCurrencyPair(trade.getOffer().getCounterCurrencyCode()); } @Override public boolean match(String filterString) { if (filterString.isEmpty()) { return true; } if (StringUtils.containsIgnoreCase(getTrade().getId(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getAmountAsString(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getPaymentMethod(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getMarketDescription(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(getTrade().getOffer().getCombinedExtraInfo(), filterString)) { return true; } if (getTrade().getBuyer().getPaymentAccountPayload() != null && StringUtils.containsIgnoreCase(getTrade().getBuyer().getPaymentAccountPayload().getPaymentDetails(), filterString)) { return true; } if (getTrade().getSeller().getPaymentAccountPayload() != null && StringUtils.containsIgnoreCase(getTrade().getSeller().getPaymentAccountPayload().getPaymentDetails(), filterString)) { return true; } return StringUtils.containsIgnoreCase(getPriceAsString(), filterString); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.pendingtrades; import com.google.inject.Inject; import com.google.inject.name.Named; import com.jfoenix.controls.JFXBadge; import com.jfoenix.controls.JFXButton; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import haveno.common.UserThread; import haveno.common.config.Config; import haveno.common.crypto.KeyRing; import haveno.common.util.Utilities; import haveno.core.alert.PrivateNotificationManager; import haveno.core.locale.Res; import haveno.core.offer.OfferPayload; import haveno.core.support.dispute.mediation.MediationResultState; import haveno.core.support.messages.ChatMessage; import haveno.core.support.traderchat.TradeChatSession; import haveno.core.support.traderchat.TraderChatManager; import haveno.core.trade.Contract; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.VolumeUtil; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.Navigation; import haveno.desktop.common.view.ActivatableViewAndModel; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.HyperlinkWithIcon; import haveno.desktop.components.PeerInfoIconTrading; import haveno.desktop.components.list.FilterBox; import haveno.desktop.main.MainView; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.TradeDetailsWindow; import haveno.desktop.main.portfolio.presentation.PortfolioUtil; import haveno.desktop.main.shared.ChatView; import haveno.desktop.util.CssTheme; import haveno.desktop.util.DisplayUtils; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.NodeAddress; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import javafx.beans.binding.Bindings; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.value.ChangeListener; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.ContextMenu; import javafx.scene.control.MenuItem; import javafx.scene.control.ScrollPane; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableRow; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.stage.Modality; import javafx.stage.Stage; import javafx.stage.StageStyle; import javafx.stage.Window; import javafx.util.Callback; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; @FxmlView public class PendingTradesView extends ActivatableViewAndModel { public interface ChatCallback { void onOpenChat(Trade trade); } private final TradeDetailsWindow tradeDetailsWindow; private final Navigation navigation; private final KeyRing keyRing; private final CoinFormatter formatter; private final PrivateNotificationManager privateNotificationManager; private final boolean useDevPrivilegeKeys; private final boolean useDevModeHeader; private final Preferences preferences; @FXML FilterBox filterBox; @FXML TableView tableView; @FXML TableColumn priceColumn, volumeColumn, amountColumn, avatarColumn, marketColumn, roleColumn, paymentMethodColumn, tradeIdColumn, dateColumn, chatColumn, moveTradeToFailedColumn; @FXML ScrollPane scrollView; private FilteredList filteredList; private SortedList sortedList; private TradeSubView selectedSubView; private EventHandler keyEventEventHandler; private Scene scene; private Subscription selectedTableItemSubscription; private Subscription selectedItemSubscription; private Stage chatPopupStage; private ListChangeListener tradesListChangeListener; private final Map newChatMessagesByTradeMap = new HashMap<>(); private String tradeIdOfOpenChat; private double chatPopupStageXPosition = -1; private double chatPopupStageYPosition = -1; private ChangeListener xPositionListener; private ChangeListener yPositionListener; private final Map buttonByTrade = new HashMap<>(); private final Map badgeByTrade = new HashMap<>(); private final Map> listenerByTrade = new HashMap<>(); private ChangeListener tradeStateListener; private ChangeListener disputeStateListener; private ChangeListener mediationResultStateListener; private ChangeListener getMempoolStatusListener; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialisation /////////////////////////////////////////////////////////////////////////////////////////// @Inject public PendingTradesView(PendingTradesViewModel model, TradeDetailsWindow tradeDetailsWindow, Navigation navigation, KeyRing keyRing, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, PrivateNotificationManager privateNotificationManager, Preferences preferences, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys, @Named(Config.USE_DEV_MODE_HEADER) boolean useDevModeHeader) { super(model); this.tradeDetailsWindow = tradeDetailsWindow; this.navigation = navigation; this.keyRing = keyRing; this.formatter = formatter; this.privateNotificationManager = privateNotificationManager; this.preferences = preferences; this.useDevPrivilegeKeys = useDevPrivilegeKeys; this.useDevModeHeader = useDevModeHeader; } @Override public void initialize() { GUIUtil.applyTableStyle(tableView); priceColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.price"))); amountColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amountWithCur", Res.getBaseCurrencyCode()))); volumeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amount"))); marketColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.market"))); roleColumn.setGraphic(new AutoTooltipLabel(Res.get("portfolio.pending.role"))); dateColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.dateTime"))); tradeIdColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.tradeId"))); paymentMethodColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.paymentMethod"))); avatarColumn.setText(""); chatColumn.setText(""); moveTradeToFailedColumn.setText(""); setTradeIdColumnCellFactory(); setDateColumnCellFactory(); setAmountColumnCellFactory(); setPriceColumnCellFactory(); setVolumeColumnCellFactory(); setPaymentMethodColumnCellFactory(); setMarketColumnCellFactory(); setRoleColumnCellFactory(); setAvatarColumnCellFactory(); setChatColumnCellFactory(); setRemoveTradeColumnCellFactory(); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); tableView.setPlaceholder(new AutoTooltipLabel(Res.get("table.placeholder.noItems", Res.get("shared.openTrades")))); tableView.setMinHeight(100); tradeIdColumn.setComparator(Comparator.comparing(o -> o.getTrade().getId())); dateColumn.setComparator(Comparator.comparing(o -> o.getTrade().getDate())); volumeColumn.setComparator(Comparator.comparing(o -> o.getTrade().getVolume(), Comparator.nullsFirst(Comparator.naturalOrder()))); amountColumn.setComparator(Comparator.comparing(o -> o.getTrade().getAmount(), Comparator.nullsFirst(Comparator.naturalOrder()))); priceColumn.setComparator(Comparator.comparing(PendingTradesListItem::getPrice)); paymentMethodColumn.setComparator(Comparator.comparing( item -> item.getTrade().getOffer() != null ? Res.get(item.getTrade().getOffer().getPaymentMethod().getId()) : null, Comparator.nullsFirst(Comparator.naturalOrder()))); marketColumn.setComparator(Comparator.comparing(PendingTradesListItem::getMarketDescription)); roleColumn.setComparator(Comparator.comparing(model::getMyRole)); avatarColumn.setComparator(Comparator.comparing( o -> model.getNumPastTrades(o.getTrade()), Comparator.nullsFirst(Comparator.naturalOrder()) )); dateColumn.setSortType(TableColumn.SortType.DESCENDING); tableView.getSortOrder().add(dateColumn); tableView.setRowFactory( tableView -> { final TableRow row = new TableRow<>(); final ContextMenu rowMenu = new ContextMenu(); MenuItem duplicateItem = new MenuItem(Res.get("portfolio.context.offerLikeThis")); duplicateItem.setOnAction((event) -> { try { OfferPayload offerPayload = row.getItem().getTrade().getOffer().getOfferPayload(); if (offerPayload.getPubKeyRing().equals(keyRing.getPubKeyRing())) { PortfolioUtil.duplicateOffer(navigation, offerPayload); } else { new Popup().warning(Res.get("portfolio.context.notYourOffer")).show(); } } catch (NullPointerException e) { log.warn("Unable to get offerPayload - {}", e.toString()); } }); MenuItem moveToFailedItem = new MenuItem(Res.get("portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip")); moveToFailedItem.setOnAction((event) -> { if (isMaybeInvalidTrade(row.getItem().getTrade())) { onMoveInvalidTradeToFailedTrades(row.getItem().getTrade()); } else { model.dataModel.tradeManager.onMoveInvalidTradeToFailedTrades(row.getItem().getTrade()); } }); rowMenu.getItems().addAll(duplicateItem, moveToFailedItem); row.contextMenuProperty().bind( Bindings.when(Bindings.isNotNull(row.itemProperty())) .then(rowMenu) .otherwise((ContextMenu) null)); return row; }); // we use a hidden emergency shortcut to open support ticket keyEventEventHandler = keyEvent -> { if (Utilities.isAltOrCtrlPressed(KeyCode.O, keyEvent)) { Popup popup = new Popup(); popup.headLine(Res.get("portfolio.pending.openSupportTicket.headline")) .message(Res.get("portfolio.pending.openSupportTicket.msg")) .actionButtonText(Res.get("portfolio.pending.openSupportTicket.headline")) .onAction(model.dataModel::onOpenSupportTicket) .closeButtonText(Res.get("shared.cancel")) .onClose(popup::hide) .show(); } }; tradesListChangeListener = c -> onListChanged(); getMempoolStatusListener = (observable, oldValue, newValue) -> { // -1 status is unknown // 0 status is FAIL // 1 status is PASS if (newValue.longValue() >= 0) { log.info("Taker fee validation returned {}", newValue.longValue()); } }; } @Override protected void activate() { ObservableList list = model.dataModel.list; filteredList = new FilteredList<>(list); sortedList = new SortedList<>(filteredList); sortedList.comparatorProperty().bind(tableView.comparatorProperty()); tableView.setItems(sortedList); tableView.setPrefHeight(100); tableView.setMinHeight(getMinTableViewHeight()); tableView.setMaxHeight(200); filterBox.initialize(filteredList, tableView); // here because filteredList is instantiated here filterBox.setPromptText(Res.get("shared.filter")); filterBox.activate(); updateMoveTradeToFailedColumnState(); scene = root.getScene(); if (scene != null) { scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); } sortedList.addListener((ListChangeListener) change -> { tableView.setMinHeight(getMinTableViewHeight()); }); selectedItemSubscription = EasyBind.subscribe(model.dataModel.selectedItemProperty, selectedItem -> { if (selectedItem != null) { if (selectedSubView != null) selectedSubView.deactivate(); if (selectedItem.getTrade() != null) { selectedSubView = model.dataModel.tradeManager.isBuyer(model.dataModel.getOffer()) ? new BuyerSubView(model) : new SellerSubView(model); //selectedSubView.setMinHeight(460); //VBox.setVgrow(selectedSubView, Priority.SOMETIMES); if (root.getChildren().size() == 2) root.getChildren().add(scrollView); else if (root.getChildren().size() == 3) root.getChildren().set(2, scrollView); scrollView.setContent(selectedSubView); // create and register a callback so we can be notified when the subview // wants to open the chat window ChatCallback chatCallback = this::openChat; selectedSubView.setChatCallback(chatCallback); } updateTableSelection(); } else { removeSelectedSubView(); } model.onSelectedItemChanged(selectedItem); if (selectedSubView != null && selectedItem != null) selectedSubView.activate(); }); selectedTableItemSubscription = EasyBind.subscribe(tableView.getSelectionModel().selectedItemProperty(), selectedItem -> { if (selectedItem != null && !selectedItem.equals(model.dataModel.selectedItemProperty.get())) { if (selectedSubView != null) selectedSubView.deactivate(); model.dataModel.onSelectItem(selectedItem); } }); updateTableSelection(); list.addListener(tradesListChangeListener); updateNewChatMessagesByTradeMap(); model.getMempoolStatus().addListener(getMempoolStatusListener); } private int getMinTableViewHeight() { return sortedList.size() <= 1 ? 100 : 130; } @Override protected void deactivate() { filterBox.deactivate(); sortedList.comparatorProperty().unbind(); selectedItemSubscription.unsubscribe(); selectedTableItemSubscription.unsubscribe(); removeSelectedSubView(); model.dataModel.list.removeListener(tradesListChangeListener); model.getMempoolStatus().removeListener(getMempoolStatusListener); if (scene != null) scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); } private void removeSelectedSubView() { if (selectedSubView != null) { selectedSubView.deactivate(); root.getChildren().remove(selectedSubView); selectedSubView = null; } } private void updateMoveTradeToFailedColumnState() { UserThread.execute(() -> { synchronized (model.dataModel.list) { moveTradeToFailedColumn.setVisible(model.dataModel.list.stream().anyMatch(item -> isMaybeInvalidTrade(item.getTrade()))); } }); } private boolean isMaybeInvalidTrade(Trade trade) { return trade.hasErrorMessage() || (Trade.Phase.DEPOSITS_PUBLISHED.ordinal() <= trade.getPhase().ordinal() && trade.isTxChainInvalid()); } private void onMoveInvalidTradeToFailedTrades(Trade trade) { String msg = trade.isTxChainInvalid() ? Res.get("portfolio.pending.failedTrade.txChainInvalid.moveToFailed", getInvalidTradeDetails(trade)) : Res.get("portfolio.pending.failedTrade.txChainValid.moveToFailed", getInvalidTradeDetails(trade)); new Popup().width(900).attention(msg) .onAction(() -> { model.dataModel.onMoveInvalidTradeToFailedTrades(trade); updateMoveTradeToFailedColumnState(); }) .actionButtonText(Res.get("shared.yes")) .closeButtonText(Res.get("shared.no")) .show(); } private void onShowInfoForInvalidTrade(Trade trade) { new Popup().width(900).attention(Res.get("portfolio.pending.failedTrade.info.popup", getInvalidTradeDetails(trade))) .show(); } private String getInvalidTradeDetails(Trade trade) { Contract contract = trade.getContract(); if (contract == null) { return Res.get("portfolio.pending.failedTrade.missingContract"); } if (trade.getMakerDepositTx() == null) { return Res.get("portfolio.pending.failedTrade.missingDepositTx"); } if (trade.getTakerDepositTx() == null && !trade.hasBuyerAsTakerWithoutDeposit()) { return Res.get("portfolio.pending.failedTrade.missingDepositTx"); // TODO (woodser): use .missingTakerDepositTx, .missingMakerDepositTx, update translation to 2-of-3 multisig } if (trade.hasErrorMessage()) { return Res.get("portfolio.pending.failedTrade.errorMsgSet", trade.getErrorMessage()); } return Res.get("shared.na"); } /////////////////////////////////////////////////////////////////////////////////////////// // Chat /////////////////////////////////////////////////////////////////////////////////////////// private void updateNewChatMessagesByTradeMap() { synchronized (model.dataModel.list) { model.dataModel.list.forEach(t -> { Trade trade = t.getTrade(); synchronized (trade.getChatMessages()) { newChatMessagesByTradeMap.put(trade.getId(), trade.getChatMessages().stream() .filter(m -> !m.isWasDisplayed()) .filter(m -> !m.isSystemMessage()) .count()); } }); } } private void openChat(Trade trade) { if (chatPopupStage != null) chatPopupStage.close(); TraderChatManager traderChatManager = model.dataModel.getTraderChatManager(); if (trade.getChatMessages().isEmpty()) { traderChatManager.addSystemMsg(trade); } trade.getChatMessages().forEach(m -> m.setWasDisplayed(true)); model.dataModel.getTradeManager().requestPersistence(); tradeIdOfOpenChat = trade.getId(); ChatView chatView = new ChatView(traderChatManager, Res.get("offerbook.trader")); chatView.setAllowAttachments(false); chatView.setDisplayHeader(false); chatView.initialize(); AnchorPane pane = new AnchorPane(chatView); pane.setPrefSize(760, 500); AnchorPane.setLeftAnchor(chatView, 10d); AnchorPane.setRightAnchor(chatView, 10d); AnchorPane.setTopAnchor(chatView, -20d); AnchorPane.setBottomAnchor(chatView, 10d); boolean isTaker = !model.dataModel.isMaker(trade.getOffer()); TradeChatSession tradeChatSession = new TradeChatSession(trade, isTaker); tradeStateListener = (observable, oldValue, newValue) -> { UserThread.execute(() -> { if (trade.isPayoutPublished()) { if (chatPopupStage.isShowing()) { chatPopupStage.hide(); } } }); }; trade.stateProperty().addListener(tradeStateListener); disputeStateListener = (observable, oldValue, newValue) -> { UserThread.execute(() -> { if (newValue == Trade.DisputeState.DISPUTE_CLOSED || newValue == Trade.DisputeState.REFUND_REQUEST_CLOSED) { chatPopupStage.hide(); } }); }; trade.disputeStateProperty().addListener(disputeStateListener); mediationResultStateListener = (observable, oldValue, newValue) -> { UserThread.execute(() -> { if (newValue == MediationResultState.PAYOUT_TX_PUBLISHED || newValue == MediationResultState.RECEIVED_PAYOUT_TX_PUBLISHED_MSG || newValue == MediationResultState.PAYOUT_TX_SEEN_IN_NETWORK) { chatPopupStage.hide(); } }); }; trade.mediationResultStateProperty().addListener(mediationResultStateListener); chatView.display(tradeChatSession, pane.widthProperty()); chatView.activate(); chatView.scrollToBottom(); chatPopupStage = new Stage(); chatPopupStage.setTitle(Res.get("tradeChat.chatWindowTitle", trade.getShortId())); StackPane owner = MainView.getRootContainer(); Scene rootScene = owner.getScene(); chatPopupStage.initOwner(rootScene.getWindow()); chatPopupStage.initModality(Modality.NONE); chatPopupStage.initStyle(StageStyle.DECORATED); chatPopupStage.setOnHiding(event -> { chatView.deactivate(); // at close we set all as displayed. While open we ignore updates of the numNewMsg in the list icon. trade.getChatMessages().forEach(m -> m.setWasDisplayed(true)); model.dataModel.getTradeManager().requestPersistence(); tradeIdOfOpenChat = null; if (xPositionListener != null) { chatPopupStage.xProperty().removeListener(xPositionListener); } if (yPositionListener != null) { chatPopupStage.xProperty().removeListener(yPositionListener); } UserThread.execute(() -> { trade.stateProperty().removeListener(tradeStateListener); trade.disputeStateProperty().addListener(disputeStateListener); trade.mediationResultStateProperty().addListener(mediationResultStateListener); }); traderChatManager.requestPersistence(); }); Scene scene = new Scene(pane); CssTheme.loadSceneStyles(scene, preferences.getCssTheme(), useDevModeHeader); scene.addEventHandler(KeyEvent.KEY_RELEASED, ev -> { if (ev.getCode() == KeyCode.ESCAPE) { ev.consume(); chatPopupStage.hide(); } }); chatPopupStage.setScene(scene); chatPopupStage.setOpacity(0); chatPopupStage.show(); xPositionListener = (observable, oldValue, newValue) -> chatPopupStageXPosition = (double) newValue; chatPopupStage.xProperty().addListener(xPositionListener); yPositionListener = (observable, oldValue, newValue) -> chatPopupStageYPosition = (double) newValue; chatPopupStage.yProperty().addListener(yPositionListener); if (chatPopupStageXPosition == -1) { Window rootSceneWindow = rootScene.getWindow(); double titleBarHeight = rootSceneWindow.getHeight() - rootScene.getHeight(); chatPopupStage.setX(Math.round(rootSceneWindow.getX() + (owner.getWidth() - chatPopupStage.getWidth() / 4 * 3))); chatPopupStage.setY(Math.round(rootSceneWindow.getY() + titleBarHeight + (owner.getHeight() - chatPopupStage.getHeight() / 4 * 3))); } else { chatPopupStage.setX(chatPopupStageXPosition); chatPopupStage.setY(chatPopupStageYPosition); } // Delay display to next render frame to avoid that the popup is first quickly displayed in default position // and after a short moment in the correct position UserThread.execute(() -> chatPopupStage.setOpacity(1)); updateChatMessageCount(trade, badgeByTrade.get(trade.getId())); } private void updateChatMessageCount(Trade trade, JFXBadge badge) { UserThread.execute(() -> { if (!trade.getId().equals(tradeIdOfOpenChat)) { updateNewChatMessagesByTradeMap(); long num = newChatMessagesByTradeMap.get(trade.getId()); if (num > 0) { badge.setText(String.valueOf(num)); badge.setEnabled(true); } else { badge.setText(""); badge.setEnabled(false); } } else { badge.setText(""); badge.setEnabled(false); } badge.refreshBadge(); }); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void updateTableSelection() { PendingTradesListItem selectedItemFromModel = model.dataModel.selectedItemProperty.get(); if (selectedItemFromModel != null) { // Select and focus selectedItem from model int index = tableView.getItems().indexOf(selectedItemFromModel); UserThread.execute(() -> tableView.getSelectionModel().select(index)); } } private void onListChanged() { updateNewChatMessagesByTradeMap(); updateMoveTradeToFailedColumnState(); } /////////////////////////////////////////////////////////////////////////////////////////// // CellFactories /////////////////////////////////////////////////////////////////////////////////////////// private void setTradeIdColumnCellFactory() { tradeIdColumn.setCellValueFactory((pendingTradesListItem) -> new ReadOnlyObjectWrapper<>(pendingTradesListItem.getValue())); tradeIdColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { private Trade trade; private ChangeListener listener; @Override public void updateItem(final PendingTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { trade = item.getTrade(); listener = (observable, oldValue, newValue) -> UserThread.execute(() -> update()); trade.stateProperty().addListener(listener); update(); } else { setGraphic(null); if (trade != null && listener != null) { trade.stateProperty().removeListener(listener); trade = null; listener = null; } } } private void update() { HyperlinkWithIcon field; if (trade == null) return; if (isMaybeInvalidTrade(trade)) { field = new HyperlinkWithIcon(trade.getShortId()); field.setIcon(FormBuilder.getMediumSizeIcon(MaterialDesignIcon.ALERT_CIRCLE_OUTLINE)); field.setOnAction(event -> tradeDetailsWindow.show(trade)); field.setTooltip(new Tooltip(Res.get("tooltip.invalidTradeState.warning"))); if (trade.isTxChainInvalid()) { field.getIcon().getStyleClass().addAll("icon", "error-icon"); } else { field.getIcon().getStyleClass().addAll("icon", "warn-icon"); } } else { field = new HyperlinkWithIcon(trade.getShortId()); field.setOnAction(event -> tradeDetailsWindow.show(trade)); field.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails"))); } setGraphic(field); } }; } }); } private void setDateColumnCellFactory() { dateColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue())); dateColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final PendingTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setGraphic(new AutoTooltipLabel(DisplayUtils.formatDateTime(item.getTrade().getDate()))); } else { setGraphic(null); } } }; } }); } private void setAmountColumnCellFactory() { amountColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue())); amountColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final PendingTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) setGraphic(new AutoTooltipLabel(HavenoUtils.formatXmr(item.getTrade().getAmount()))); else setGraphic(null); } }; } }); } private void setPriceColumnCellFactory() { priceColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue())); priceColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final PendingTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) setGraphic(new AutoTooltipLabel(FormattingUtils.formatPrice(item.getPrice()))); else setGraphic(null); } }; } }); } private void setVolumeColumnCellFactory() { volumeColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue())); volumeColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final PendingTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { try { String volume = VolumeUtil.formatVolumeWithCode(item.getTrade().getVolume()); setGraphic(new AutoTooltipLabel(volume)); } catch (Throwable ignore) { log.debug(ignore.toString()); // Stupidity to make Codacy happy } } else setGraphic(null); } }; } }); } private void setPaymentMethodColumnCellFactory() { paymentMethodColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue())); paymentMethodColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final PendingTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) setGraphic(new AutoTooltipLabel(item.getPaymentMethod())); else setGraphic(null); } }; } }); } private void setMarketColumnCellFactory() { marketColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue())); marketColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final PendingTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setGraphic(new AutoTooltipLabel(item.getMarketDescription())); } else { setGraphic(null); } } }; } }); } private void setRoleColumnCellFactory() { roleColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue())); roleColumn.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { @Override public void updateItem(final PendingTradesListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) setGraphic(new AutoTooltipLabel(model.getMyRole(item))); else setGraphic(null); } }; } }); } @SuppressWarnings("UnusedReturnValue") private TableColumn setAvatarColumnCellFactory() { avatarColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue())); avatarColumn.getStyleClass().add("avatar-column"); avatarColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(PendingTradesListItem newItem, boolean empty) { super.updateItem(newItem, empty); if (!empty && newItem != null) { final Trade trade = newItem.getTrade(); final NodeAddress tradePeerNodeAddress = trade.getTradePeerNodeAddress(); int numPastTrades = model.getNumPastTrades(trade); String role = Res.get("peerInfoIcon.tooltip.tradePeer"); Node peerInfoIcon = new PeerInfoIconTrading(tradePeerNodeAddress, // TODO: display maker and taker node addresses for arbitrator role, numPastTrades, privateNotificationManager, trade, preferences, model.accountAgeWitnessService, useDevPrivilegeKeys); setPadding(new Insets(1, 0, 0, 0)); setGraphic(peerInfoIcon); } else { setGraphic(null); } } }; } }); return avatarColumn; } @SuppressWarnings("UnusedReturnValue") private TableColumn setChatColumnCellFactory() { chatColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue())); chatColumn.getStyleClass().addAll("avatar-column"); chatColumn.setSortable(false); chatColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(PendingTradesListItem newItem, boolean empty) { super.updateItem(newItem, empty); if (!empty && newItem != null) { Trade trade = newItem.getTrade(); String id = trade.getId(); // We use maps for each trade to avoid multiple listener registrations when // switching views. With current implementation we avoid that but we do not // remove listeners when a trade is removed (completed) but that has no consequences // as we will not receive any message anyway from a closed trade. Supporting it // more correctly would require more effort and managing listener deactivation at // screen switches (currently we get the update called if we have selected another // view. Button button; if (!buttonByTrade.containsKey(id)) { button = FormBuilder.getIconButton(MaterialDesignIcon.COMMENT_MULTIPLE_OUTLINE); buttonByTrade.put(id, button); button.setTooltip(new Tooltip(Res.get("tradeChat.openChat"))); } else { button = buttonByTrade.get(id); } JFXBadge badge; if (!badgeByTrade.containsKey(id)) { badge = new JFXBadge(button); badgeByTrade.put(id, badge); badge.setPosition(Pos.TOP_RIGHT); } else { badge = badgeByTrade.get(id); } button.setOnAction(e -> { tableView.getSelectionModel().select(this.getIndex()); openChat(trade); }); if (!listenerByTrade.containsKey(id)) { ListChangeListener listener = c -> updateChatMessageCount(trade, badge); listenerByTrade.put(id, listener); trade.getChatMessages().addListener(listener); } updateChatMessageCount(trade, badge); setGraphic(badge); } else { setGraphic(null); } } }; } }); return chatColumn; } private void setRemoveTradeColumnCellFactory() { moveTradeToFailedColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue())); moveTradeToFailedColumn.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { private Trade trade; private JFXButton warnIconButton, trashIconButton; private ChangeListener listener; @Override public void updateItem(PendingTradesListItem newItem, boolean empty) { super.updateItem(newItem, empty); if (!empty && newItem != null) { trade = newItem.getTrade(); listener = (observable, oldValue, newValue) -> UserThread.execute(() -> update()); trade.stateProperty().addListener(listener); update(); } else { cleanup(); } } private void update() { if (isMaybeInvalidTrade(trade)) { Text warnIcon = FormBuilder.getMediumSizeIcon(MaterialDesignIcon.ALERT_CIRCLE_OUTLINE); Text trashIcon = FormBuilder.getMediumSizeIcon(MaterialDesignIcon.ARROW_RIGHT_BOLD_BOX_OUTLINE); if (trade.isTxChainInvalid()) { trashIcon.getStyleClass().addAll("icon", "error-icon"); warnIcon.getStyleClass().addAll("icon", "error-icon"); } else { trashIcon.getStyleClass().addAll("icon", "warn-icon"); warnIcon.getStyleClass().addAll("icon", "warn-icon"); } warnIconButton = new JFXButton("", warnIcon); warnIconButton.getStyleClass().add("hidden-icon-button"); warnIconButton.setTooltip(new Tooltip(Res.get("portfolio.pending.failedTrade.warningIcon.tooltip"))); warnIconButton.setOnAction(e -> onShowInfoForInvalidTrade(trade)); trashIconButton = new JFXButton("", trashIcon); trashIconButton.getStyleClass().add("hidden-icon-button"); trashIconButton.setTooltip(new Tooltip(Res.get("portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip"))); trashIconButton.setOnAction(e -> onMoveInvalidTradeToFailedTrades(trade)); HBox hBox = new HBox(); hBox.setSpacing(0); hBox.getChildren().addAll(warnIconButton, trashIconButton); setGraphic(hBox); } else { cleanup(); } updateMoveTradeToFailedColumnState(); } private void cleanup() { if (warnIconButton != null) { warnIconButton.setOnAction(null); } if (trashIconButton != null) { trashIconButton.setOnAction(null); } if (listener != null && trade != null) { trade.stateProperty().removeListener(listener); } setGraphic(null); } }; } }); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.pendingtrades; import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.ClockWatcher; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.network.MessageState; import haveno.core.offer.Offer; import haveno.core.offer.OfferUtil; import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.TradeUtil; import haveno.core.user.User; import haveno.core.util.FormattingUtils; import haveno.core.util.VolumeUtil; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.BtcAddressValidator; import haveno.desktop.Navigation; import haveno.desktop.common.model.ActivatableWithDataModel; import haveno.desktop.common.model.ViewModel; import static haveno.desktop.main.portfolio.pendingtrades.PendingTradesViewModel.SellerState.UNDEFINED; import haveno.desktop.util.DisplayUtils; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.P2PService; import java.math.BigInteger; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; import javax.annotation.Nullable; import lombok.Getter; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; public class PendingTradesViewModel extends ActivatableWithDataModel implements ViewModel { @Getter @Nullable private Trade trade; interface State { } enum BuyerState implements State { UNDEFINED, STEP1, STEP2, STEP3, STEP4 } enum SellerState implements State { UNDEFINED, STEP1, STEP2, STEP3, STEP4 } public final CoinFormatter btcFormatter; public final BtcAddressValidator btcAddressValidator; final AccountAgeWitnessService accountAgeWitnessService; public final P2PService p2PService; private final ClosedTradableManager closedTradableManager; private final OfferUtil offerUtil; private final TradeUtil tradeUtil; public final ClockWatcher clockWatcher; @Getter private final Navigation navigation; @Getter private final User user; private final ObjectProperty buyerState = new SimpleObjectProperty<>(); private final ObjectProperty sellerState = new SimpleObjectProperty<>(); @Getter private final ObjectProperty paymentSentMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); private Subscription tradeStateSubscription; private Subscription paymentAccountDecryptedSubscription; private Subscription payoutStateSubscription; private Subscription messageStateSubscription; @Getter protected final IntegerProperty mempoolStatus = new SimpleIntegerProperty(); private transient Map showPaymentDetailsEarly = new HashMap(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, initialization /////////////////////////////////////////////////////////////////////////////////////////// @Inject public PendingTradesViewModel(PendingTradesDataModel dataModel, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, BtcAddressValidator btcAddressValidator, P2PService p2PService, ClosedTradableManager closedTradableManager, OfferUtil offerUtil, TradeUtil tradeUtil, AccountAgeWitnessService accountAgeWitnessService, ClockWatcher clockWatcher, Navigation navigation, User user) { super(dataModel); this.btcFormatter = btcFormatter; this.btcAddressValidator = btcAddressValidator; this.p2PService = p2PService; this.closedTradableManager = closedTradableManager; this.offerUtil = offerUtil; this.tradeUtil = tradeUtil; this.accountAgeWitnessService = accountAgeWitnessService; this.clockWatcher = clockWatcher; this.navigation = navigation; this.user = user; } @Override protected void deactivate() { synchronized (this) { if (tradeStateSubscription != null) { tradeStateSubscription.unsubscribe(); tradeStateSubscription = null; } if (paymentAccountDecryptedSubscription != null) { paymentAccountDecryptedSubscription.unsubscribe(); paymentAccountDecryptedSubscription = null; } if (payoutStateSubscription != null) { payoutStateSubscription.unsubscribe(); payoutStateSubscription = null; } if (messageStateSubscription != null) { messageStateSubscription.unsubscribe(); messageStateSubscription = null; } } } // Don't set own listener as we need to control the order of the calls public void onSelectedItemChanged(PendingTradesListItem selectedItem) { synchronized (this) { if (tradeStateSubscription != null) { tradeStateSubscription.unsubscribe(); sellerState.set(SellerState.UNDEFINED); buyerState.set(BuyerState.UNDEFINED); } if (paymentAccountDecryptedSubscription != null) { paymentAccountDecryptedSubscription.unsubscribe(); } if (payoutStateSubscription != null) { payoutStateSubscription.unsubscribe(); sellerState.set(SellerState.UNDEFINED); buyerState.set(BuyerState.UNDEFINED); } if (messageStateSubscription != null) { messageStateSubscription.unsubscribe(); paymentSentMessageStateProperty.set(MessageState.UNDEFINED); } if (selectedItem != null) { this.trade = selectedItem.getTrade(); tradeStateSubscription = EasyBind.subscribe(trade.stateProperty(), state -> { onTradeStateChanged(state); }); paymentAccountDecryptedSubscription = EasyBind.subscribe(trade.getProcessModel().getPaymentAccountDecryptedProperty(), decrypted -> { refresh(); }); payoutStateSubscription = EasyBind.subscribe(trade.payoutStateProperty(), state -> { onPayoutStateChanged(state); }); messageStateSubscription = EasyBind.subscribe(trade.getSeller().getPaymentSentMessageStateProperty(), this::onPaymentSentMessageStateChanged); } } } private void refresh() { UserThread.execute(() -> { sellerState.set(UNDEFINED); buyerState.set(BuyerState.UNDEFINED); onTradeStateChanged(trade.getState()); if (trade.isPayoutPublished()) onPayoutStateChanged(trade.getPayoutState()); // TODO: payout state takes precedence in case PaymentReceivedMessage not processed else onTradeStateChanged(trade.getState()); }); } private void onPaymentSentMessageStateChanged(MessageState messageState) { paymentSentMessageStateProperty.set(messageState); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// ReadOnlyObjectProperty getBuyerState() { return buyerState; } ReadOnlyObjectProperty getSellerState() { return sellerState; } public String getPayoutAmountBeforeCost() { return dataModel.getTrade() != null ? HavenoUtils.formatXmr(dataModel.getTrade().getPayoutAmountBeforeCost(), true) : ""; } String getMarketLabel(PendingTradesListItem item) { return item == null ? "" : tradeUtil.getMarketDescription(item.getTrade()); } public String getRemainingTradeDurationAsWords() { checkNotNull(dataModel.getTrade(), "model's trade must not be null"); return tradeUtil.getRemainingTradeDurationAsWords(dataModel.getTrade()); } public double getRemainingTradeDurationAsPercentage() { checkNotNull(dataModel.getTrade(), "model's trade must not be null"); return tradeUtil.getRemainingTradeDurationAsPercentage(dataModel.getTrade()); } public String getDateForOpenDispute() { checkNotNull(dataModel.getTrade(), "model's trade must not be null"); return DisplayUtils.formatDateTime(tradeUtil.getDateForOpenDispute(dataModel.getTrade())); } public boolean showWarning() { checkNotNull(dataModel.getTrade(), "model's trade must not be null"); Date halfTradePeriodDate = tradeUtil.getHalfTradePeriodDate(dataModel.getTrade()); return halfTradePeriodDate != null && new Date().after(halfTradePeriodDate); } public boolean showDispute() { return getMaxTradePeriodDate() != null && new Date().after(getMaxTradePeriodDate()); } public boolean getShowPaymentDetailsEarly() { return !HavenoUtils.RECOMMEND_CONFIRMATIONS_BEFORE_SENDING_PAYMENT || showPaymentDetailsEarly.getOrDefault(dataModel.getTrade().getId(), false); } public void setShowPaymentDetailsEarly(boolean show) { showPaymentDetailsEarly.put(dataModel.getTrade().getId(), show); } String getMyRole(PendingTradesListItem item) { return TradeUtil.getRole(item.getTrade()); } String getPaymentMethod(PendingTradesListItem item) { return item == null ? "" : tradeUtil.getPaymentMethodNameWithCountryCode(item.getTrade()); } // summary public String getTradeVolume() { return dataModel.getTrade() != null ? HavenoUtils.formatXmr(dataModel.getTrade().getAmount(), true) : ""; } public String getFiatVolume() { return dataModel.getTrade() != null ? VolumeUtil.formatVolumeWithCode(dataModel.getTrade().getVolume()) : ""; } public String getTradeFee() { if (trade != null && dataModel.getOffer() != null && trade.getAmount() != null) { checkNotNull(dataModel.getTrade()); BigInteger tradeFeeInXmr = dataModel.getTradeFee(); String percentage = GUIUtil.getPercentageOfTradeAmount(tradeFeeInXmr, trade.getAmount()); return HavenoUtils.formatXmr(tradeFeeInXmr, true) + percentage; } else { return ""; } } public String getSecurityDeposit() { Offer offer = dataModel.getOffer(); Trade trade = dataModel.getTrade(); if (offer != null && trade != null && trade.getAmount() != null) { BigInteger securityDeposit = dataModel.isBuyer() ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit(); String percentage = GUIUtil.getPercentageOfTradeAmount(securityDeposit, trade.getAmount()); return HavenoUtils.formatXmr(securityDeposit, true) + percentage; } else { return ""; } } public boolean isBlockChainMethod() { return offerUtil.isBlockChainPaymentMethod(dataModel.getOffer()); } public int getNumPastTrades(Trade trade) { return closedTradableManager.getObservableList().stream() .filter(e -> { if (e instanceof Trade) { Trade t = (Trade) e; return t.getTradePeerNodeAddress() != null && trade.getTradePeerNodeAddress() != null && t.getTradePeerNodeAddress().getFullAddress().equals(trade.getTradePeerNodeAddress().getFullAddress()); } else return false; }) .collect(Collectors.toSet()) .size(); } @Nullable private Date getMaxTradePeriodDate() { return dataModel.getTrade() != null ? dataModel.getTrade().getMaxTradePeriodDate() : null; } /////////////////////////////////////////////////////////////////////////////////////////// // States /////////////////////////////////////////////////////////////////////////////////////////// private void onTradeStateChanged(Trade.State tradeState) { log.debug("UI tradeState={}, id={}", tradeState, trade != null ? trade.getShortId() : "trade is null"); // arbitrator trade view only shows tx status if (trade.isArbitrator()) { buyerState.set(BuyerState.STEP1); sellerState.set(SellerState.STEP1); return; } if (trade.isCompleted()) { sellerState.set(UNDEFINED); buyerState.set(BuyerState.UNDEFINED); return; } switch (tradeState) { // initialization case PREPARATION: case MULTISIG_PREPARED: case MULTISIG_MADE: case MULTISIG_EXCHANGED: case MULTISIG_COMPLETED: case CONTRACT_SIGNATURE_REQUESTED: case CONTRACT_SIGNED: case SENT_PUBLISH_DEPOSIT_TX_REQUEST: case SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST: case SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST: buyerState.set(BuyerState.UNDEFINED); sellerState.set(UNDEFINED); // TODO: show view while trade initializes? break; case ARBITRATOR_PUBLISHED_DEPOSIT_TXS: case DEPOSIT_TXS_SEEN_IN_NETWORK: case DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN: // TODO: separate step to wait for first confirmation buyerState.set(BuyerState.STEP1); sellerState.set(SellerState.STEP1); break; // buyer and seller step 2 // deposits unlocked or finalized case DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN: case DEPOSIT_TXS_FINALIZED_IN_BLOCKCHAIN: buyerState.set(BuyerState.STEP2); sellerState.set(SellerState.STEP2); break; // buyer step 3 case BUYER_CONFIRMED_PAYMENT_SENT: // UI action case BUYER_SENT_PAYMENT_SENT_MSG: // PAYMENT_SENT_MSG sent case BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG: // PAYMENT_SENT_MSG arrived // We don't switch the UI before we got the feedback of the msg delivery buyerState.set(BuyerState.STEP2); sellerState.set(trade.isPayoutPublished() ? SellerState.STEP4 : SellerState.STEP3); break; case BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG: // PAYMENT_SENT_MSG in mailbox case SELLER_RECEIVED_PAYMENT_SENT_MSG: // PAYMENT_SENT_MSG acked buyerState.set(BuyerState.STEP3); break; case BUYER_SEND_FAILED_PAYMENT_SENT_MSG: // PAYMENT_SENT_MSG failed // if failed we need to repeat sending so back to step 2 buyerState.set(BuyerState.STEP2); break; // payment marked as received case SELLER_CONFIRMED_PAYMENT_RECEIPT: if (trade.isBuyer()) { buyerState.set(BuyerState.STEP3); } else if (trade.isSeller()) { sellerState.set(trade.isPayoutPublished() ? SellerState.STEP4 : SellerState.STEP3); } break; // payment received case SELLER_SENT_PAYMENT_RECEIVED_MSG: if (trade.isBuyer()) { buyerState.set(BuyerState.UNDEFINED); // TODO: resetting screen to populate summary information which can be missing before payout message processed buyerState.set(BuyerState.STEP4); } else if (trade.isSeller()) { sellerState.set(trade.isPayoutPublished() ? SellerState.STEP4 : SellerState.STEP3); } break; // seller step 3 or 4 if published case SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG: case SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG: case SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG: case BUYER_RECEIVED_PAYMENT_RECEIVED_MSG: sellerState.set(trade.isPayoutPublished() ? SellerState.STEP4 : SellerState.STEP3); break; default: sellerState.set(UNDEFINED); buyerState.set(BuyerState.UNDEFINED); log.warn("unhandled processState " + tradeState); DevEnv.logErrorAndThrowIfDevMode("unhandled processState " + tradeState); break; } } private void onPayoutStateChanged(Trade.PayoutState payoutState) { log.debug("UI payoutState={}, id={}", payoutState, trade != null ? trade.getShortId() : "trade is null"); if (trade.isArbitrator()) return; switch (payoutState) { case PAYOUT_PUBLISHED: case PAYOUT_CONFIRMED: case PAYOUT_UNLOCKED: case PAYOUT_FINALIZED: sellerState.set(SellerState.STEP4); buyerState.set(BuyerState.STEP4); break; default: break; } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/SellerSubView.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.pendingtrades; import haveno.core.locale.Res; import haveno.desktop.main.portfolio.pendingtrades.steps.TradeWizardItem; import haveno.desktop.main.portfolio.pendingtrades.steps.seller.SellerStep1View; import haveno.desktop.main.portfolio.pendingtrades.steps.seller.SellerStep2View; import haveno.desktop.main.portfolio.pendingtrades.steps.seller.SellerStep3View; import haveno.desktop.main.portfolio.pendingtrades.steps.seller.SellerStep4View; import lombok.extern.slf4j.Slf4j; import org.fxmisc.easybind.EasyBind; @Slf4j public class SellerSubView extends TradeSubView { private TradeWizardItem step1; private TradeWizardItem step2; private TradeWizardItem step3; private TradeWizardItem step4; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialisation /////////////////////////////////////////////////////////////////////////////////////////// SellerSubView(PendingTradesViewModel model) { super(model); } @Override protected void activate() { viewStateSubscription = EasyBind.subscribe(model.getSellerState(), this::onViewStateChanged); super.activate(); } @Override protected void addWizards() { step1 = new TradeWizardItem(SellerStep1View.class, Res.get("portfolio.pending.step1.waitForConf"), "1"); step2 = new TradeWizardItem(SellerStep2View.class, Res.get("portfolio.pending.step2_seller.waitPaymentSent"), "2"); step3 = new TradeWizardItem(SellerStep3View.class, Res.get("portfolio.pending.step3_seller.confirmPaymentReceived"), "3"); step4 = new TradeWizardItem(SellerStep4View.class, Res.get("portfolio.pending.step5.completed"), "4"); addWizardsToGridPane(step1); addLineSeparatorToGridPane(); addWizardsToGridPane(step2); addLineSeparatorToGridPane(); addWizardsToGridPane(step3); addLineSeparatorToGridPane(); addWizardsToGridPane(step4); } /////////////////////////////////////////////////////////////////////////////////////////// // State /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void onViewStateChanged(PendingTradesViewModel.State viewState) { super.onViewStateChanged(viewState); if (viewState != null) { PendingTradesViewModel.SellerState sellerState = (PendingTradesViewModel.SellerState) viewState; step1.setDisabled(); step2.setDisabled(); step3.setDisabled(); step4.setDisabled(); switch (sellerState) { case UNDEFINED: break; case STEP1: showItem(step1); break; case STEP2: step1.setCompleted(); showItem(step2); break; case STEP3: step1.setCompleted(); step2.setCompleted(); showItem(step3); break; case STEP4: step1.setCompleted(); step2.setCompleted(); step3.setCompleted(); showItem(step4); break; default: log.warn("unhandled viewState " + sellerState); break; } } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/TradeStepInfo.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.pendingtrades; import haveno.core.locale.Res; import haveno.core.trade.Trade; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.SimpleMarkdownLabel; import haveno.desktop.components.TitledGroupBg; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.layout.GridPane; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.util.function.Supplier; @Slf4j public class TradeStepInfo { public enum State { UNDEFINED, SHOW_GET_HELP_BUTTON, IN_MEDIATION_SELF_REQUESTED, IN_MEDIATION_PEER_REQUESTED, MEDIATION_RESULT, MEDIATION_RESULT_SELF_ACCEPTED, MEDIATION_RESULT_PEER_ACCEPTED, IN_ARBITRATION_SELF_REQUESTED, IN_ARBITRATION_PEER_REQUESTED, IN_REFUND_REQUEST_SELF_REQUESTED, IN_REFUND_REQUEST_PEER_REQUESTED, WARN_HALF_PERIOD, WARN_PERIOD_OVER, DEPOSIT_MISSING, TRADE_COMPLETED } private final TitledGroupBg titledGroupBg; private final SimpleMarkdownLabel label; private final SimpleMarkdownLabel footerLabel; private final AutoTooltipButton button; @Nullable @Setter private Trade trade; @Getter private State state = State.UNDEFINED; private Supplier firstHalfOverWarnTextSupplier = () -> ""; private Supplier periodOverWarnTextSupplier = () -> ""; private Supplier depositTxMissingWarnTextSupplier = () -> ""; TradeStepInfo(TitledGroupBg titledGroupBg, SimpleMarkdownLabel label, AutoTooltipButton button, SimpleMarkdownLabel footerLabel) { this.titledGroupBg = titledGroupBg; this.label = label; this.button = button; this.footerLabel = footerLabel; GridPane.setColumnIndex(button, 0); setState(State.SHOW_GET_HELP_BUTTON); } void removeItselfFrom(GridPane leftGridPane) { leftGridPane.getChildren().remove(titledGroupBg); leftGridPane.getChildren().remove(label); leftGridPane.getChildren().remove(button); } public void setOnAction(EventHandler e) { button.setOnAction(e); } public void setFirstHalfOverWarnTextSupplier(Supplier firstHalfOverWarnTextSupplier) { this.firstHalfOverWarnTextSupplier = firstHalfOverWarnTextSupplier; } public void setPeriodOverWarnTextSupplier(Supplier periodOverWarnTextSupplier) { this.periodOverWarnTextSupplier = periodOverWarnTextSupplier; } public void setDepositTxMissingWarnTextSupplier(Supplier depositTxMissingWarnTextSupplier) { this.depositTxMissingWarnTextSupplier = depositTxMissingWarnTextSupplier; } public void setState(State state) { this.state = state; switch (state) { case UNDEFINED: break; case SHOW_GET_HELP_BUTTON: // grey button titledGroupBg.setText(Res.get("portfolio.pending.support.headline.getHelp")); label.updateContent(""); button.setText(Res.get("portfolio.pending.support.button.getHelp").toUpperCase()); button.setId(null); button.getStyleClass().remove("action-button"); button.setDisable(false); break; case IN_ARBITRATION_SELF_REQUESTED: // red button String text = trade.getDisputeState().isOpen() ? Res.get("portfolio.pending.supportTicketOpened") : Res.get("portfolio.pending.arbitrationRequested"); titledGroupBg.setText(text); label.updateContent(Res.get("portfolio.pending.disputeOpenedByUser", Res.get("portfolio.pending.communicateWithArbitrator"))); button.setText(text.toUpperCase()); button.setId("open-dispute-button"); button.getStyleClass().remove("action-button"); button.setDisable(true); break; case IN_ARBITRATION_PEER_REQUESTED: // red button text = trade.getDisputeState().isOpen() ? Res.get("portfolio.pending.supportTicketOpened") : Res.get("portfolio.pending.arbitrationRequested"); titledGroupBg.setText(text); label.updateContent(Res.get("portfolio.pending.disputeOpenedByPeer", Res.get("portfolio.pending.communicateWithArbitrator"))); button.setText(text.toUpperCase()); button.setId("open-dispute-button"); button.getStyleClass().remove("action-button"); button.setDisable(true); break; case MEDIATION_RESULT: // green button titledGroupBg.setText(Res.get("portfolio.pending.mediationResult.headline")); label.updateContent(Res.get("portfolio.pending.mediationResult.info.noneAccepted")); button.setText(Res.get("portfolio.pending.mediationResult.button").toUpperCase()); button.setId(null); button.getStyleClass().add("action-button"); button.setDisable(false); break; case MEDIATION_RESULT_SELF_ACCEPTED: // green button deactivated titledGroupBg.setText(Res.get("portfolio.pending.mediationResult.headline")); label.updateContent(Res.get("portfolio.pending.mediationResult.info.selfAccepted")); button.setText(Res.get("portfolio.pending.mediationResult.button").toUpperCase()); button.setId(null); button.getStyleClass().add("action-button"); button.setDisable(false); break; case MEDIATION_RESULT_PEER_ACCEPTED: // green button titledGroupBg.setText(Res.get("portfolio.pending.mediationResult.headline")); label.updateContent(Res.get("portfolio.pending.mediationResult.info.peerAccepted")); button.setText(Res.get("portfolio.pending.mediationResult.button").toUpperCase()); button.setId(null); button.getStyleClass().add("action-button"); button.setDisable(false); break; case IN_REFUND_REQUEST_SELF_REQUESTED: // red button titledGroupBg.setText(Res.get("portfolio.pending.refundRequested")); label.updateContent(Res.get("portfolio.pending.disputeOpenedByUser", Res.get("portfolio.pending.communicateWithArbitrator"))); button.setText(Res.get("portfolio.pending.refundRequested").toUpperCase()); button.setId("open-dispute-button"); button.getStyleClass().remove("action-button"); button.setDisable(true); break; case IN_REFUND_REQUEST_PEER_REQUESTED: // red button titledGroupBg.setText(Res.get("portfolio.pending.refundRequested")); label.updateContent(Res.get("portfolio.pending.disputeOpenedByPeer", Res.get("portfolio.pending.communicateWithArbitrator"))); button.setText(Res.get("portfolio.pending.refundRequested").toUpperCase()); button.setId("open-dispute-button"); button.getStyleClass().remove("action-button"); button.setDisable(true); break; case WARN_HALF_PERIOD: // orange button titledGroupBg.setText(Res.get("portfolio.pending.support.headline.halfPeriodOver")); label.updateContent(firstHalfOverWarnTextSupplier.get()); button.setText(Res.get("portfolio.pending.support.button.getHelp").toUpperCase()); button.setId(null); button.getStyleClass().remove("action-button"); button.setDisable(false); break; case WARN_PERIOD_OVER: // red button titledGroupBg.setText(Res.get("portfolio.pending.support.headline.periodOver")); label.updateContent(periodOverWarnTextSupplier.get()); button.setText(Res.get("portfolio.pending.openSupport").toUpperCase()); button.setId("open-dispute-button"); button.getStyleClass().remove("action-button"); button.setDisable(false); break; case DEPOSIT_MISSING: // red button titledGroupBg.setText(Res.get("portfolio.pending.support.headline.depositTxMissing")); label.updateContent(depositTxMissingWarnTextSupplier.get()); button.setText(Res.get("portfolio.pending.openSupport").toUpperCase()); button.setId("open-dispute-button"); button.getStyleClass().remove("action-button"); button.setDisable(false); break; case TRADE_COMPLETED: // hide group titledGroupBg.setVisible(false); label.setVisible(false); button.setVisible(false); footerLabel.setVisible(false); default: break; } if (trade != null && trade.getPayoutTxId() != null) { button.setDisable(true); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/TradeSubView.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.pendingtrades; import haveno.core.locale.Res; import haveno.core.trade.Trade; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.SimpleMarkdownLabel; import haveno.desktop.components.TitledGroupBg; import haveno.desktop.main.portfolio.pendingtrades.steps.TradeStepView; import haveno.desktop.main.portfolio.pendingtrades.steps.TradeWizardItem; import haveno.desktop.util.Layout; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Orientation; import javafx.scene.control.Separator; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import lombok.extern.slf4j.Slf4j; import org.fxmisc.easybind.Subscription; import static haveno.desktop.util.FormBuilder.addButtonAfterGroup; import static haveno.desktop.util.FormBuilder.addSimpleMarkdownLabel; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; @Slf4j public abstract class TradeSubView extends HBox { protected final PendingTradesViewModel model; protected VBox leftVBox; private AnchorPane contentPane; private TradeStepView tradeStepView; protected TradeStepInfo tradeStepInfo; private GridPane leftGridPane; private TitledGroupBg tradeProcessTitledGroupBg; private int leftGridPaneRowIndex = 0; Subscription viewStateSubscription; private PendingTradesView.ChatCallback chatCallback; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialisation /////////////////////////////////////////////////////////////////////////////////////////// public TradeSubView(PendingTradesViewModel model) { this.model = model; HBox.setHgrow(this, Priority.ALWAYS); setSpacing(Layout.PADDING_WINDOW); buildViews(); } protected void activate() { } protected void deactivate() { if (viewStateSubscription != null) viewStateSubscription.unsubscribe(); if (tradeStepView != null) tradeStepView.deactivate(); if (tradeStepInfo != null) tradeStepInfo.removeItselfFrom(leftGridPane); } private void buildViews() { addLeftBox(); addContentPane(); leftGridPane = new GridPane(); leftGridPane.setPrefWidth(340); leftGridPane.setHgap(Layout.GRID_GAP); leftGridPane.setVgap(Layout.GRID_GAP); VBox.setMargin(leftGridPane, new Insets(0, 10, 10, 10)); leftVBox.getChildren().add(leftGridPane); leftGridPaneRowIndex = 0; tradeProcessTitledGroupBg = addTitledGroupBg(leftGridPane, leftGridPaneRowIndex, 1, Res.get("portfolio.pending.tradeProcess")); tradeProcessTitledGroupBg.getStyleClass().add("last"); addWizards(); TitledGroupBg titledGroupBg = addTitledGroupBg(leftGridPane, ++leftGridPaneRowIndex, 1, "", 10); titledGroupBg.getStyleClass().add("last"); SimpleMarkdownLabel label = addSimpleMarkdownLabel(leftGridPane, ++leftGridPaneRowIndex); AutoTooltipButton button = (AutoTooltipButton) addButtonAfterGroup(leftGridPane, ++leftGridPaneRowIndex, ""); SimpleMarkdownLabel footerLabel = addSimpleMarkdownLabel(leftGridPane, ++leftGridPaneRowIndex, Res.get("portfolio.pending.stillNotResolved"), 10); footerLabel.getStyleClass().add("medium-text"); tradeStepInfo = new TradeStepInfo(titledGroupBg, label, button, footerLabel); } void showItem(TradeWizardItem item) { item.setActive(); createAndAddTradeStepView(item.getViewClass()); } protected abstract void addWizards(); protected void onViewStateChanged(PendingTradesViewModel.State viewState) { tradeStepInfo.setTrade(model.getTrade()); } void addWizardsToGridPane(TradeWizardItem tradeWizardItem) { if (leftGridPaneRowIndex == 0) GridPane.setMargin(tradeWizardItem, new Insets(Layout.FIRST_ROW_DISTANCE + Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); GridPane.setRowIndex(tradeWizardItem, leftGridPaneRowIndex++); leftGridPane.getChildren().add(tradeWizardItem); GridPane.setRowSpan(tradeProcessTitledGroupBg, leftGridPaneRowIndex); GridPane.setFillWidth(tradeWizardItem, true); } void addLineSeparatorToGridPane() { final Separator separator = new Separator(Orientation.VERTICAL); separator.setMinHeight(10); GridPane.setMargin(separator, new Insets(0, 0, 0, 13)); GridPane.setHalignment(separator, HPos.LEFT); GridPane.setRowIndex(separator, leftGridPaneRowIndex++); leftGridPane.getChildren().add(separator); } private void createAndAddTradeStepView(Class viewClass) { if (tradeStepView != null) tradeStepView.deactivate(); try { tradeStepView = viewClass.getDeclaredConstructor(PendingTradesViewModel.class).newInstance(model); contentPane.getChildren().setAll(tradeStepView); tradeStepView.setTradeStepInfo(tradeStepInfo); ChatCallback chatCallback = trade -> { // call up the chain to open chat if (this.chatCallback != null) { this.chatCallback.onOpenChat(trade); } }; tradeStepView.setChatCallback(chatCallback); tradeStepView.activate(); } catch (Exception e) { log.error("Creating viewClass {} caused an error {}\n", viewClass, e.getMessage(), e); } } private void addLeftBox() { leftVBox = new VBox(); leftVBox.setSpacing(Layout.SPACING_V_BOX); leftVBox.setMinWidth(290); getChildren().add(leftVBox); } private void addContentPane() { contentPane = new AnchorPane(); HBox.setHgrow(contentPane, Priority.SOMETIMES); getChildren().add(contentPane); } public interface ChatCallback { void onOpenChat(Trade trade); } public void setChatCallback(PendingTradesView.ChatCallback chatCallback) { this.chatCallback = chatCallback; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.pendingtrades.steps; import static com.google.common.base.Preconditions.checkNotNull; import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; import haveno.common.ClockWatcher; import haveno.common.UserThread; import haveno.common.util.Tuple3; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeResult; import haveno.core.support.dispute.mediation.MediationResultState; import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.Contract; import haveno.core.trade.HavenoUtils; import haveno.core.trade.MakerTrade; import haveno.core.trade.TakerTrade; import haveno.core.trade.Trade; import haveno.core.user.Preferences; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.components.InfoTextField; import haveno.desktop.components.TitledGroupBg; import haveno.desktop.components.TxIdTextField; import static haveno.desktop.components.paymentmethods.PaymentMethodForm.addOpenTradeDuration; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.portfolio.pendingtrades.PendingTradesViewModel; import haveno.desktop.main.portfolio.pendingtrades.TradeStepInfo; import haveno.desktop.main.portfolio.pendingtrades.TradeSubView; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addMultilineLabel; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import static haveno.desktop.util.FormBuilder.addTopLabelTxIdTextField; import haveno.desktop.util.Layout; import haveno.network.p2p.BootstrapListener; import java.util.Optional; import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.value.ChangeListener; import javafx.geometry.Insets; import javafx.scene.control.Label; import javafx.scene.control.ProgressBar; import javafx.scene.control.ScrollPane; import javafx.scene.control.TextField; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public abstract class TradeStepView extends AnchorPane { protected final Logger log = LoggerFactory.getLogger(this.getClass()); protected final PendingTradesViewModel model; protected final Trade trade; protected final Preferences preferences; protected final GridPane gridPane; private Subscription tradePeriodStateSubscription, tradeStateSubscription, disputeStateSubscription, mediationResultStateSubscription, syncUpdateSubscription; protected int gridRow = 0; private TextField timeLeftTextField; private ProgressBar timeLeftProgressBar; private TxIdTextField selfTxIdTextField; private TxIdTextField peerTxIdTextField; private TradeStepInfo tradeStepInfo; private Subscription selfTxIdSubscription; private Subscription peerTxIdSubscription; private ClockWatcher.Listener clockListener; private final ChangeListener errorMessageListener; protected Label infoLabel; private Popup acceptMediationResultPopup; private BootstrapListener bootstrapListener; private TradeSubView.ChatCallback chatCallback; private ChangeListener pendingTradesInitializedListener; protected Label statusLabel; protected String syncStatus; protected String tradeStatus; private ChangeListener depositsListener; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialisation /////////////////////////////////////////////////////////////////////////////////////////// protected TradeStepView(PendingTradesViewModel model) { this.model = model; preferences = model.dataModel.preferences; trade = model.dataModel.getTrade(); checkNotNull(trade, "Trade must not be null at TradeStepView"); ScrollPane scrollPane = new ScrollPane(); scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); scrollPane.setFitToHeight(true); scrollPane.setFitToWidth(true); AnchorPane.setLeftAnchor(scrollPane, 10d); AnchorPane.setRightAnchor(scrollPane, 10d); AnchorPane.setTopAnchor(scrollPane, 10d); AnchorPane.setBottomAnchor(scrollPane, 0d); getChildren().add(scrollPane); gridPane = new GridPane(); gridPane.setHgap(Layout.GRID_GAP); gridPane.setVgap(Layout.GRID_GAP); gridPane.setPadding(new Insets(0, 0, 25, 0)); ColumnConstraints columnConstraints1 = new ColumnConstraints(); columnConstraints1.setHgrow(Priority.ALWAYS); ColumnConstraints columnConstraints2 = new ColumnConstraints(); columnConstraints2.setHgrow(Priority.ALWAYS); gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2); scrollPane.setContent(gridPane); AnchorPane.setLeftAnchor(this, 0d); AnchorPane.setRightAnchor(this, 0d); AnchorPane.setTopAnchor(this, -10d); AnchorPane.setBottomAnchor(this, 0d); addContent(); errorMessageListener = (observable, oldValue, newValue) -> { if (newValue != null) { log.warn("Showing popup for trade error {} {}", trade.getClass().getSimpleName(), trade.getId(), new RuntimeException(newValue)); new Popup().error(newValue).show(); } }; clockListener = new ClockWatcher.Listener() { @Override public void onSecondTick() { } @Override public void onMinuteTick() { updateTimeLeft(); } }; } public void activate() { if (selfTxIdTextField != null) { if (selfTxIdSubscription != null) selfTxIdSubscription.unsubscribe(); selfTxIdSubscription = EasyBind.subscribe(model.dataModel.isMaker() ? model.dataModel.makerTxId : model.dataModel.takerTxId, id -> { if (!id.isEmpty()) { selfTxIdTextField.setup(id, trade); } else { selfTxIdTextField.cleanup(); } }); } if (peerTxIdTextField != null) { if (peerTxIdSubscription != null) peerTxIdSubscription.unsubscribe(); peerTxIdSubscription = EasyBind.subscribe(model.dataModel.isMaker() ? model.dataModel.takerTxId : model.dataModel.makerTxId, id -> { if (!id.isEmpty()) { peerTxIdTextField.setup(id, trade); } else { peerTxIdTextField.cleanup(); } }); } trade.errorMessageProperty().addListener(errorMessageListener); tradeStepInfo.setOnAction(e -> { if (!isArbitrationOpenedState() && (this.isTradePeriodOver() || trade.isDepositTxMissing())) { openSupportTicket(); } else { openChat(); } }); // We get mailbox messages processed after we have bootstrapped. This will influence the states we // handle in our disputeStateSubscription and mediationResultStateSubscriptions. To avoid that we show // popups from incorrect states we wait until we have bootstrapped and the mailbox messages processed. if (model.p2PService.isBootstrapped()) { registerSubscriptions(); } else { bootstrapListener = new BootstrapListener() { @Override public void onDataReceived() { registerSubscriptions(); } }; model.p2PService.addP2PServiceListener(bootstrapListener); } tradePeriodStateSubscription = EasyBind.subscribe(trade.tradePeriodStateProperty(), newValue -> { if (newValue != null) { UserThread.execute(() -> updateTradePeriodState(newValue)); } }); model.clockWatcher.addListener(clockListener); if (infoLabel != null) { infoLabel.setText(getInfoText()); } BooleanProperty initialized = model.dataModel.tradeManager.getTradesInitialized(); if (initialized.get()) { onPendingTradesInitialized(); } else { pendingTradesInitializedListener = (observable, oldValue, newValue) -> { if (newValue) { onPendingTradesInitialized(); UserThread.execute(() -> initialized.removeListener(pendingTradesInitializedListener)); } }; initialized.addListener(pendingTradesInitializedListener); } depositsListener = (observable, oldValue, newValue) -> { if (newValue != null) { UserThread.execute(() -> onDepositTxsUpdate()); } }; trade.getDepositTxsUpdateCounter().addListener(depositsListener); } protected void onPendingTradesInitialized() { // model.dataModel.xmrWalletService.addNewBestBlockListener(newBestBlockListener); // TODO (woodser): different listener? // checkIfLockTimeIsOver(); } private void registerSubscriptions() { disputeStateSubscription = EasyBind.subscribe(trade.disputeStateProperty(), newValue -> { if (newValue != null) { updateDisputeState(newValue); } }); mediationResultStateSubscription = EasyBind.subscribe(trade.mediationResultStateProperty(), newValue -> { if (newValue != null) { updateMediationResultState(true); } }); if (trade.wasWalletSyncedAndPolledProperty.get()) addTradeStateSubscription(); else trade.wasWalletSyncedAndPolledProperty.addListener((observable, oldValue, newValue) -> { if (newValue) addTradeStateSubscription(); }); syncUpdateSubscription = EasyBind.subscribe(trade.getSyncProgressListener().numUpdates(), newValue -> { if (newValue != null) updateSyncProgress(); }); UserThread.execute(() -> model.p2PService.removeP2PServiceListener(bootstrapListener)); } protected void updateSyncProgress() { // TODO: don't set sync status when 2 blocks behind or less after synced? long blocksRemaining = trade.blocksRemainingProperty().get(); // if (trade.wasWalletSynced() && blocksRemaining <= 2) { // setSyncStatus(""); // return; // } // set status with percentage and blocks remaining if available double percent = trade.downloadPercentageProperty().get(); if (percent < 0.0 || percent >= 1.0) setSyncStatus(""); else { if (trade.blocksRemainingProperty().get() < 0) { setSyncStatus(Res.get("portfolio.pending.syncing")); } else { if (trade.blocksRemainingProperty().get() == 1) { setSyncStatus(Res.get("portfolio.pending.syncing.blockRemaining")); } else { setSyncStatus(Res.get("portfolio.pending.syncing.blocksRemaining", blocksRemaining)); } } } } private void addTradeStateSubscription() { tradeStateSubscription = EasyBind.subscribe(trade.stateProperty(), newValue -> { if (newValue != null) { UserThread.execute(() -> updateTradeState(newValue)); } }); } private void openSupportTicket() { applyOnDisputeOpened(); model.dataModel.onOpenDispute(); } private void openChat() { // call up the chain to open chat if (this.chatCallback != null) { this.chatCallback.onOpenChat(this.trade); } } public void deactivate() { if (selfTxIdSubscription != null) selfTxIdSubscription.unsubscribe(); if (peerTxIdSubscription != null) peerTxIdSubscription.unsubscribe(); if (selfTxIdTextField != null) selfTxIdTextField.cleanup(); if (peerTxIdTextField != null) peerTxIdTextField.cleanup(); if (errorMessageListener != null) trade.errorMessageProperty().removeListener(errorMessageListener); if (disputeStateSubscription != null) disputeStateSubscription.unsubscribe(); if (mediationResultStateSubscription != null) mediationResultStateSubscription.unsubscribe(); if (syncUpdateSubscription != null) syncUpdateSubscription.unsubscribe(); if (tradePeriodStateSubscription != null) tradePeriodStateSubscription.unsubscribe(); if (tradeStateSubscription != null) tradeStateSubscription.unsubscribe(); if (clockListener != null) model.clockWatcher.removeListener(clockListener); if (tradeStepInfo != null) tradeStepInfo.setOnAction(null); if (acceptMediationResultPopup != null) { acceptMediationResultPopup.hide(); acceptMediationResultPopup = null; } if (depositsListener != null) { trade.getDepositTxsUpdateCounter().removeListener(depositsListener); depositsListener = null; } } protected void onDepositTxsUpdate() { // no default action } protected long getNumMinutesToUnlock() { long numDepositConfirmations = trade.getNumDepositConfirmations() == null ? 0 : trade.getNumDepositConfirmations(); long numMinutesToUnlock = (Math.max(0, XmrWalletService.NUM_BLOCKS_UNLOCK - numDepositConfirmations)) * 2; return numMinutesToUnlock; } /////////////////////////////////////////////////////////////////////////////////////////// // Content /////////////////////////////////////////////////////////////////////////////////////////// protected void addContent() { addTradeInfoBlock(); addInfoBlock(); } protected void addTradeInfoBlock() { TitledGroupBg tradeInfoTitledGroupBg = addTitledGroupBg(gridPane, gridRow, 3, Res.get("portfolio.pending.tradeInformation")); GridPane.setColumnSpan(tradeInfoTitledGroupBg, 2); // self's deposit tx id boolean showSelfTxId = model.dataModel.isMaker() || !trade.hasBuyerAsTakerWithoutDeposit(); if (showSelfTxId) { final Tuple3 labelSelfTxIdTextFieldVBoxTuple3 = addTopLabelTxIdTextField(gridPane, gridRow, Res.get("shared.yourDepositTransactionId"), Layout.COMPACT_FIRST_ROW_DISTANCE); GridPane.setColumnSpan(labelSelfTxIdTextFieldVBoxTuple3.third, 2); selfTxIdTextField = labelSelfTxIdTextFieldVBoxTuple3.second; String selfTxId = model.dataModel.isMaker() ? model.dataModel.makerTxId.get() : model.dataModel.takerTxId.get(); if (!selfTxId.isEmpty()) selfTxIdTextField.setup(selfTxId, trade); else selfTxIdTextField.cleanup(); } // peer's deposit tx id boolean showPeerTxId = !model.dataModel.isMaker() || !trade.hasBuyerAsTakerWithoutDeposit(); if (showPeerTxId) { final Tuple3 labelPeerTxIdTextFieldVBoxTuple3 = addTopLabelTxIdTextField(gridPane, showSelfTxId ? ++gridRow : gridRow, Res.get("shared.peerDepositTransactionId"), showSelfTxId ? -Layout.GROUP_DISTANCE_WITHOUT_SEPARATOR : Layout.COMPACT_FIRST_ROW_DISTANCE); GridPane.setColumnSpan(labelPeerTxIdTextFieldVBoxTuple3.third, 2); peerTxIdTextField = labelPeerTxIdTextFieldVBoxTuple3.second; String peerTxId = model.dataModel.isMaker() ? model.dataModel.takerTxId.get() : model.dataModel.makerTxId.get(); if (!peerTxId.isEmpty()) peerTxIdTextField.setup(peerTxId, trade); else peerTxIdTextField.cleanup(); } if (model.dataModel.getTrade() != null) { checkNotNull(model.dataModel.getTrade().getOffer(), "Offer must not be null in TradeStepView"); InfoTextField infoTextField = addOpenTradeDuration(gridPane, ++gridRow, model.dataModel.getTrade().getOffer()); infoTextField.setContentForInfoPopOver(createInfoPopover()); } final Tuple3 labelTextFieldVBoxTuple3 = addCompactTopLabelTextField(gridPane, gridRow, 1, Res.get("portfolio.pending.remainingTime"), ""); timeLeftTextField = labelTextFieldVBoxTuple3.second; timeLeftTextField.setMinWidth(400); timeLeftProgressBar = new ProgressBar(0); timeLeftProgressBar.setOpacity(0.7); timeLeftProgressBar.setMinHeight(9); timeLeftProgressBar.setMaxHeight(9); timeLeftProgressBar.setMaxWidth(Double.MAX_VALUE); GridPane.setRowIndex(timeLeftProgressBar, ++gridRow); GridPane.setColumnSpan(timeLeftProgressBar, 2); GridPane.setFillWidth(timeLeftProgressBar, true); gridPane.getChildren().add(timeLeftProgressBar); updateTimeLeft(); } protected void addInfoBlock() { final TitledGroupBg titledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 1, getInfoBlockTitle(), Layout.COMPACT_GROUP_DISTANCE); titledGroupBg.getStyleClass().add("last"); GridPane.setColumnSpan(titledGroupBg, 2); infoLabel = addMultilineLabel(gridPane, gridRow, "", Layout.COMPACT_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE); GridPane.setColumnSpan(infoLabel, 2); statusLabel = new Label(); gridPane.add(statusLabel, 0, ++gridRow, 2, 1); } protected String getInfoText() { return ""; } protected String getInfoBlockTitle() { return ""; } private void updateTimeLeft() { if (!trade.isInitialized()) return; if (timeLeftTextField != null) { // TODO (woodser): extra TradeStepView created but not deactivated on trade.setState(), so deactivate when model's trade is null if (model.dataModel.getTrade() == null) { log.warn("deactivating TradeStepView because model's trade is null"); // schedule deactivation to avoid concurrent modification of clock listeners Platform.runLater(() -> deactivate()); return; } String remainingTime = model.getRemainingTradeDurationAsWords(); timeLeftProgressBar.setProgress(model.getRemainingTradeDurationAsPercentage()); if (!remainingTime.isEmpty()) { boolean isDepositsFinalized = trade.isDepositsFinalized(); timeLeftTextField.setText(isDepositsFinalized ? Res.get("portfolio.pending.remainingTimeDetail", remainingTime, model.getDateForOpenDispute()) : Res.get("portfolio.pending.remainingTimeDetail.startsAfter", Trade.NUM_BLOCKS_DEPOSITS_FINALIZED)); if (model.showWarning() || model.showDispute()) { timeLeftTextField.getStyleClass().add("error-text"); timeLeftProgressBar.getStyleClass().add("error"); } } else { timeLeftTextField.setText(Res.get("portfolio.pending.tradeNotCompleted", model.getDateForOpenDispute())); timeLeftTextField.getStyleClass().add("error-text"); timeLeftProgressBar.getStyleClass().add("error"); } } } /////////////////////////////////////////////////////////////////////////////////////////// // Dispute/warning label and button /////////////////////////////////////////////////////////////////////////////////////////// // We have the dispute button and text field on the left side, but we handle the content here as it // is trade state specific public void setTradeStepInfo(TradeStepInfo tradeStepInfo) { this.tradeStepInfo = tradeStepInfo; tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText); tradeStepInfo.setPeriodOverWarnTextSupplier(this::getPeriodOverWarnText); tradeStepInfo.setDepositTxMissingWarnTextSupplier(this::getDepositTxMissingWarnText); } protected void hideTradeStepInfo() { tradeStepInfo.setState(TradeStepInfo.State.TRADE_COMPLETED); } protected String getFirstHalfOverWarnText() { return ""; } /////////////////////////////////////////////////////////////////////////////////////////// // Dispute /////////////////////////////////////////////////////////////////////////////////////////// protected String getPeriodOverWarnText() { return ""; } protected String getDepositTxMissingWarnText() { return Res.get("portfolio.pending.support.depositTxMissing"); } protected void applyOnDisputeOpened() { } protected void updateDisputeState(Trade.DisputeState disputeState) { Optional ownDispute; switch (disputeState) { case NO_DISPUTE: break; case DISPUTE_PREPARING: case DISPUTE_REQUESTED: case DISPUTE_OPENED: if (tradeStepInfo != null) { tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText); } applyOnDisputeOpened(); // update trade view unless arbitrator if (trade instanceof ArbitratorTrade) break; ownDispute = model.dataModel.arbitrationManager.findDispute(trade.getId()); ownDispute.ifPresent(dispute -> { if (tradeStepInfo != null) { boolean isOpener = dispute.isDisputeOpenerIsBuyer() ? trade.isBuyer() : trade.isSeller(); tradeStepInfo.setState(isOpener ? TradeStepInfo.State.IN_ARBITRATION_SELF_REQUESTED : TradeStepInfo.State.IN_ARBITRATION_PEER_REQUESTED); } }); break; case DISPUTE_CLOSED: break; case MEDIATION_REQUESTED: if (tradeStepInfo != null) { tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText); } applyOnDisputeOpened(); ownDispute = model.dataModel.mediationManager.findDispute(trade.getId()); ownDispute.ifPresent(dispute -> { if (tradeStepInfo != null) tradeStepInfo.setState(TradeStepInfo.State.IN_MEDIATION_SELF_REQUESTED); }); break; case MEDIATION_STARTED_BY_PEER: if (tradeStepInfo != null) { tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText); } applyOnDisputeOpened(); ownDispute = model.dataModel.mediationManager.findDispute(trade.getId()); ownDispute.ifPresent(dispute -> { if (tradeStepInfo != null) { tradeStepInfo.setState(TradeStepInfo.State.IN_MEDIATION_PEER_REQUESTED); } }); break; case MEDIATION_CLOSED: if (tradeStepInfo != null) { tradeStepInfo.setOnAction(e -> { updateMediationResultState(false); }); } if (tradeStepInfo != null) { tradeStepInfo.setState(TradeStepInfo.State.MEDIATION_RESULT); } updateMediationResultState(true); break; case REFUND_REQUESTED: throw new RuntimeException("Unhandled case: " + Trade.DisputeState.REFUND_REQUESTED); // if (tradeStepInfo != null) { // tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText); // } // applyOnDisputeOpened(); // // ownDispute = model.dataModel.refundManager.findOwnDispute(trade.getId()); // ownDispute.ifPresent(dispute -> { // if (tradeStepInfo != null) // tradeStepInfo.setState(TradeStepInfo.State.IN_REFUND_REQUEST_SELF_REQUESTED); // }); // // if (acceptMediationResultPopup != null) { // acceptMediationResultPopup.hide(); // acceptMediationResultPopup = null; // } // // break; case REFUND_REQUEST_STARTED_BY_PEER: throw new RuntimeException("Unhandled case: " + Trade.DisputeState.REFUND_REQUEST_STARTED_BY_PEER); // if (tradeStepInfo != null) { // tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText); // } // applyOnDisputeOpened(); // // ownDispute = model.dataModel.refundManager.findOwnDispute(trade.getId()); // ownDispute.ifPresent(dispute -> { // if (tradeStepInfo != null) // tradeStepInfo.setState(TradeStepInfo.State.IN_REFUND_REQUEST_PEER_REQUESTED); // }); // // if (acceptMediationResultPopup != null) { // acceptMediationResultPopup.hide(); // acceptMediationResultPopup = null; // } // break; case REFUND_REQUEST_CLOSED: break; default: break; } } private void updateMediationResultState(boolean blockOpeningOfResultAcceptedPopup) { if (isInMediation()) { if (isRefundRequestStartedByPeer()) { tradeStepInfo.setState(TradeStepInfo.State.IN_REFUND_REQUEST_PEER_REQUESTED); } else if (isRefundRequestSelfStarted()) { tradeStepInfo.setState(TradeStepInfo.State.IN_REFUND_REQUEST_SELF_REQUESTED); } } else if (isMediationClosedState()) { // We do not use the state itself as it is not guaranteed the last state reflects relevant information // (e.g. we might receive a RECEIVED_SIG_MSG but then later a SIG_MSG_IN_MAILBOX). if (hasSelfAccepted()) { tradeStepInfo.setState(TradeStepInfo.State.MEDIATION_RESULT_SELF_ACCEPTED); if (!blockOpeningOfResultAcceptedPopup) openMediationResultPopup(Res.get("portfolio.pending.mediationResult.popup.headline", trade.getShortId())); } else if (peerAccepted()) { tradeStepInfo.setState(TradeStepInfo.State.MEDIATION_RESULT_PEER_ACCEPTED); if (acceptMediationResultPopup == null) { openMediationResultPopup(Res.get("portfolio.pending.mediationResult.popup.headline.peerAccepted", trade.getShortId())); } } else { tradeStepInfo.setState(TradeStepInfo.State.MEDIATION_RESULT); openMediationResultPopup(Res.get("portfolio.pending.mediationResult.popup.headline", trade.getShortId())); } } } private boolean isInMediation() { return isRefundRequestStartedByPeer() || isRefundRequestSelfStarted(); } private boolean isRefundRequestStartedByPeer() { return trade.getDisputeState() == Trade.DisputeState.REFUND_REQUEST_STARTED_BY_PEER; } private boolean isRefundRequestSelfStarted() { return trade.getDisputeState() == Trade.DisputeState.REFUND_REQUESTED; } private boolean isMediationClosedState() { return trade.getDisputeState() == Trade.DisputeState.MEDIATION_CLOSED; } private boolean isArbitrationOpenedState() { return trade.getDisputeState().isOpen(); } private boolean isTradePeriodOver() { return Trade.TradePeriodState.TRADE_PERIOD_OVER == trade.tradePeriodStateProperty().get(); } private boolean hasSelfAccepted() { return trade.getProcessModel().getMediatedPayoutTxSignature() != null; } private boolean peerAccepted() { return trade.getTradePeer().getMediatedPayoutTxSignature() != null; } private void openMediationResultPopup(String headLine) { if (acceptMediationResultPopup != null) { return; } Optional optionalDispute = model.dataModel.mediationManager.findDispute(trade.getId()); if (optionalDispute.isEmpty()) { return; } if (trade.getPayoutTxId() != null) { return; } if (trade instanceof MakerTrade && trade.getMakerDepositTx() == null) { log.error("trade.getMakerDepositTx() was null at openMediationResultPopup. " + "We add the trade to failed trades. TradeId={}", trade.getId()); //model.dataModel.addTradeToFailedTrades(); // TODO (woodser): new way to move trade to failed trades? model.dataModel.onMoveInvalidTradeToFailedTrades(trade); new Popup().warning(Res.get("portfolio.pending.mediationResult.error.depositTxNull")).show(); // TODO (woodser): separate error messages for maker/taker return; } else if (trade instanceof TakerTrade && trade.getTakerDepositTx() == null && !trade.hasBuyerAsTakerWithoutDeposit()) { log.error("trade.getTakerDepositTx() was null at openMediationResultPopup. " + "We add the trade to failed trades. TradeId={}", trade.getId()); //model.dataModel.addTradeToFailedTrades(); model.dataModel.onMoveInvalidTradeToFailedTrades(trade); new Popup().warning(Res.get("portfolio.pending.mediationResult.error.depositTxNull")).show(); return; } DisputeResult disputeResult = optionalDispute.get().getDisputeResultProperty().get(); Contract contract = checkNotNull(trade.getContract(), "contract must not be null"); boolean isMyRoleBuyer = contract.isMyRoleBuyer(model.dataModel.getPubKeyRingProvider().get()); String buyerPayoutAmount = HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmountBeforeCost(), true); String sellerPayoutAmount = HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmountBeforeCost(), true); String myPayoutAmount = isMyRoleBuyer ? buyerPayoutAmount : sellerPayoutAmount; String peersPayoutAmount = isMyRoleBuyer ? sellerPayoutAmount : buyerPayoutAmount; String actionButtonText = hasSelfAccepted() ? Res.get("portfolio.pending.mediationResult.popup.alreadyAccepted") : Res.get("shared.accept"); String message; MediationResultState mediationResultState = checkNotNull(trade).getMediationResultState(); if (mediationResultState == null) { return; } switch (mediationResultState) { case MEDIATION_RESULT_ACCEPTED: case SIG_MSG_SENT: case SIG_MSG_ARRIVED: case SIG_MSG_IN_MAILBOX: case SIG_MSG_SEND_FAILED: message = Res.get("portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver", "N/A", // TODO (woodser): no timelocked tx in xmr, so part of popup message is n/a -1); break; default: message = Res.get("portfolio.pending.mediationResult.popup.info", myPayoutAmount, peersPayoutAmount, "N/A", // TODO (woodser): no timelocked tx in xmr, so part of popup message is n/a -1); break; } acceptMediationResultPopup = new Popup().width(900) .headLine(headLine) .instruction(message) .actionButtonText(actionButtonText) .onAction(() -> { model.dataModel.mediationManager.onAcceptMediationResult(trade, () -> { log.info("onAcceptMediationResult completed"); acceptMediationResultPopup = null; }, errorMessage -> { UserThread.execute(() -> { new Popup().error(errorMessage).show(); if (acceptMediationResultPopup != null) { acceptMediationResultPopup.hide(); acceptMediationResultPopup = null; } }); }); }) .secondaryActionButtonText(Res.get("portfolio.pending.mediationResult.popup.openArbitration")) .onSecondaryAction(() -> { model.dataModel.mediationManager.rejectMediationResult(trade); model.dataModel.onOpenDispute(); acceptMediationResultPopup = null; }) .onClose(() -> { acceptMediationResultPopup = null; }); if (hasSelfAccepted()) { acceptMediationResultPopup.disableActionButton(); } acceptMediationResultPopup.show(); } protected String getCurrencyName(Trade trade) { return CurrencyUtil.getNameByCode(getCurrencyCode(trade)); } protected String getCurrencyCode(Trade trade) { return checkNotNull(trade.getOffer()).getCounterCurrencyCode(); } protected boolean isXmrTrade() { return getCurrencyCode(trade).equals("XMR"); } private void updateTradePeriodState(Trade.TradePeriodState tradePeriodState) { if (!trade.getDisputeState().isDisputed()) { switch (tradePeriodState) { case FIRST_HALF: // just for dev testing. not possible to go back in time ;-) if (tradeStepInfo.getState() == TradeStepInfo.State.WARN_PERIOD_OVER) { tradeStepInfo.setState(TradeStepInfo.State.WARN_HALF_PERIOD); } else if (tradeStepInfo.getState() == TradeStepInfo.State.WARN_HALF_PERIOD) { tradeStepInfo.setState(TradeStepInfo.State.SHOW_GET_HELP_BUTTON); tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText); } break; case SECOND_HALF: if (!trade.isPaymentReceived()) { if (tradeStepInfo != null) { tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText); tradeStepInfo.setState(TradeStepInfo.State.WARN_HALF_PERIOD); } } else { tradeStepInfo.setState(TradeStepInfo.State.SHOW_GET_HELP_BUTTON); } break; case TRADE_PERIOD_OVER: if (tradeStepInfo != null) { tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getPeriodOverWarnText); tradeStepInfo.setState(TradeStepInfo.State.WARN_PERIOD_OVER); } break; } } } private void updateTradeState(Trade.State tradeState) { updateTimeLeft(); if (!trade.getDisputeState().isOpen() && trade.isDepositTxMissing()) { tradeStepInfo.setState(TradeStepInfo.State.DEPOSIT_MISSING); } } // private void checkIfLockTimeIsOver() { // if (trade.getDisputeState() == Trade.DisputeState.MEDIATION_CLOSED) { // Transaction delayedPayoutTx = trade.getDelayedPayoutTx(); // if (delayedPayoutTx != null) { // long lockTime = delayedPayoutTx.getLockTime(); // int bestChainHeight = model.dataModel.btcWalletService.getBestChainHeight(); // long remaining = lockTime - bestChainHeight; // if (remaining <= 0) { // openMediationResultPopup(Res.get("portfolio.pending.mediationResult.popup.headline", trade.getShortId())); // } // } // } // } // protected void checkForUnconfirmedTimeout() { // if (trade.isDepositsConfirmed()) return; // long unconfirmedHours = Duration.between(trade.getDate().toInstant(), Instant.now()).toHours(); // if (unconfirmedHours >= 3 && !trade.hasFailed()) { // String key = "tradeUnconfirmedTooLong_" + trade.getShortId(); // if (DontShowAgainLookup.showAgain(key)) { // new Popup().warning(Res.get("portfolio.pending.unconfirmedTooLong", trade.getShortId(), unconfirmedHours)) // .dontShowAgainId(key) // .closeButtonText(Res.get("shared.ok")) // .show(); // } // } // } /////////////////////////////////////////////////////////////////////////////////////////// // TradeDurationLimitInfo /////////////////////////////////////////////////////////////////////////////////////////// private GridPane createInfoPopover() { GridPane infoGridPane = new GridPane(); int rowIndex = 0; infoGridPane.setHgap(5); infoGridPane.setVgap(10); infoGridPane.setPadding(new Insets(10, 10, 10, 10)); Label label = addMultilineLabel(infoGridPane, rowIndex++, Res.get("portfolio.pending.tradePeriodInfo", Trade.NUM_BLOCKS_DEPOSITS_FINALIZED)); label.setMaxWidth(450); HBox warningBox = new HBox(); warningBox.setMinHeight(30); warningBox.setPadding(new Insets(5)); warningBox.getStyleClass().add("warning-box"); GridPane.setRowIndex(warningBox, rowIndex); GridPane.setColumnSpan(warningBox, 2); Label warningIcon = new Label(); AwesomeDude.setIcon(warningIcon, AwesomeIcon.WARNING_SIGN); warningIcon.getStyleClass().add("warning"); Label warning = new Label(Res.get("portfolio.pending.tradePeriodWarning")); warning.setWrapText(true); warning.setMaxWidth(410); warningBox.getChildren().addAll(warningIcon, warning); infoGridPane.getChildren().add(warningBox); return infoGridPane; } public void setChatCallback(TradeSubView.ChatCallback chatCallback) { this.chatCallback = chatCallback; } protected void setSyncStatus(String text) { syncStatus = text; updateStatus(); } protected void setTradeStatus(String text) { tradeStatus = text; updateStatus(); } protected void updateStatus() { String text = getStatusText(); if (statusLabel != null) statusLabel.setText(text == null ? "" : text); } private String getStatusText() { if (syncStatus == null || syncStatus.isEmpty()) { return tradeStatus; } else { return syncStatus; } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/TradeWizardItem.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.pendingtrades.steps; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import haveno.common.UserThread; import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.layout.StackPane; import javafx.scene.text.Text; import org.jetbrains.annotations.NotNull; import static haveno.desktop.util.FormBuilder.getBigIcon; public class TradeWizardItem extends Label { private final String iconLabel; public Class getViewClass() { return viewClass; } private final Class viewClass; public TradeWizardItem(Class viewClass, String title, String iconLabel) { this.viewClass = viewClass; this.iconLabel = iconLabel; setMouseTransparent(true); setText(title); // setPrefHeight(40); setPrefWidth(360); setAlignment(Pos.CENTER_LEFT); setDisabled(); } public void setDisabled() { setId("trade-wizard-item-background-disabled"); UserThread.execute(() -> setGraphic(getStackPane("trade-step-disabled-bg"))); } public void setActive() { setId("trade-wizard-item-background-active"); UserThread.execute(() -> setGraphic(getStackPane("trade-step-active-bg"))); } public void setCompleted() { setId("trade-wizard-item-background-active"); final Text icon = getBigIcon(MaterialDesignIcon.CHECK_CIRCLE); icon.getStyleClass().add("trade-step-active-bg"); UserThread.execute(() -> setGraphic(icon)); } @NotNull private StackPane getStackPane(String styleClass) { StackPane stackPane = new StackPane(); final Label label = new Label(iconLabel); label.getStyleClass().add("trade-step-label"); final Text icon = getBigIcon(MaterialDesignIcon.CIRCLE); icon.getStyleClass().add(styleClass); stackPane.getChildren().addAll(icon, label); return stackPane; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.pendingtrades.steps.buyer; import haveno.core.locale.Res; import haveno.desktop.main.portfolio.pendingtrades.PendingTradesViewModel; import haveno.desktop.main.portfolio.pendingtrades.steps.TradeStepView; public class BuyerStep1View extends TradeStepView { /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialisation /////////////////////////////////////////////////////////////////////////////////////////// public BuyerStep1View(PendingTradesViewModel model) { super(model); } @Override protected void onPendingTradesInitialized() { super.onPendingTradesInitialized(); //validatePayoutTx(); // TODO (woodser): no payout tx in xmr integration, do something else? //validateDepositInputs(); //checkForUnconfirmedTimeout(); } /////////////////////////////////////////////////////////////////////////////////////////// // Info /////////////////////////////////////////////////////////////////////////////////////////// @Override protected String getInfoBlockTitle() { return Res.get("portfolio.pending.step1.waitForConf"); } @Override protected String getInfoText() { return Res.get("portfolio.pending.step1.info.you", getNumMinutesToUnlock()); } @Override public void onDepositTxsUpdate() { infoLabel.setText(getInfoText()); } /////////////////////////////////////////////////////////////////////////////////////////// // Warning /////////////////////////////////////////////////////////////////////////////////////////// @Override protected String getFirstHalfOverWarnText() { return Res.get("portfolio.pending.step1.warn"); } /////////////////////////////////////////////////////////////////////////////////////////// // Dispute /////////////////////////////////////////////////////////////////////////////////////////// @Override protected String getPeriodOverWarnText() { return Res.get("portfolio.pending.step1.openForDispute"); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.pendingtrades.steps.buyer; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.util.Tuple4; import haveno.core.locale.Res; import haveno.core.offer.Offer; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccountUtil; import haveno.core.payment.payload.AssetAccountPayload; import haveno.core.payment.payload.CashDepositAccountPayload; import haveno.core.payment.payload.F2FAccountPayload; import haveno.core.payment.payload.FasterPaymentsAccountPayload; import haveno.core.payment.payload.HalCashAccountPayload; import haveno.core.payment.payload.MoneyGramAccountPayload; import haveno.core.payment.payload.PayByMailAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.SwiftAccountPayload; import haveno.core.payment.payload.USPostalMoneyOrderAccountPayload; import haveno.core.payment.payload.WesternUnionAccountPayload; import haveno.core.trade.Trade; import haveno.core.user.DontShowAgainLookup; import haveno.core.util.VolumeUtil; import haveno.desktop.components.BusyAnimation; import haveno.desktop.components.TextFieldWithCopyIcon; import haveno.desktop.components.TitledGroupBg; import haveno.desktop.components.paymentmethods.AchTransferForm; import haveno.desktop.components.paymentmethods.AdvancedCashForm; import haveno.desktop.components.paymentmethods.AliPayForm; import haveno.desktop.components.paymentmethods.AmazonGiftCardForm; import haveno.desktop.components.paymentmethods.AssetsForm; import haveno.desktop.components.paymentmethods.AustraliaPayidForm; import haveno.desktop.components.paymentmethods.BizumForm; import haveno.desktop.components.paymentmethods.CapitualForm; import haveno.desktop.components.paymentmethods.CashAppForm; import haveno.desktop.components.paymentmethods.CashAtAtmForm; import haveno.desktop.components.paymentmethods.PayByMailForm; import haveno.desktop.components.paymentmethods.PayPalForm; import haveno.desktop.components.paymentmethods.PaysafeForm; import haveno.desktop.components.paymentmethods.CashDepositForm; import haveno.desktop.components.paymentmethods.CelPayForm; import haveno.desktop.components.paymentmethods.ChaseQuickPayForm; import haveno.desktop.components.paymentmethods.ZelleForm; import haveno.desktop.components.paymentmethods.DomesticWireTransferForm; import haveno.desktop.components.paymentmethods.F2FForm; import haveno.desktop.components.paymentmethods.FasterPaymentsForm; import haveno.desktop.components.paymentmethods.HalCashForm; import haveno.desktop.components.paymentmethods.ImpsForm; import haveno.desktop.components.paymentmethods.InteracETransferForm; import haveno.desktop.components.paymentmethods.JapanBankTransferForm; import haveno.desktop.components.paymentmethods.MoneseForm; import haveno.desktop.components.paymentmethods.MoneyBeamForm; import haveno.desktop.components.paymentmethods.MoneyGramForm; import haveno.desktop.components.paymentmethods.NationalBankForm; import haveno.desktop.components.paymentmethods.NeftForm; import haveno.desktop.components.paymentmethods.NequiForm; import haveno.desktop.components.paymentmethods.PaxumForm; import haveno.desktop.components.paymentmethods.PayseraForm; import haveno.desktop.components.paymentmethods.PaytmForm; import haveno.desktop.components.paymentmethods.PerfectMoneyForm; import haveno.desktop.components.paymentmethods.PixForm; import haveno.desktop.components.paymentmethods.PopmoneyForm; import haveno.desktop.components.paymentmethods.PromptPayForm; import haveno.desktop.components.paymentmethods.RevolutForm; import haveno.desktop.components.paymentmethods.RtgsForm; import haveno.desktop.components.paymentmethods.SameBankForm; import haveno.desktop.components.paymentmethods.SatispayForm; import haveno.desktop.components.paymentmethods.SepaForm; import haveno.desktop.components.paymentmethods.SepaInstantForm; import haveno.desktop.components.paymentmethods.SpecificBankForm; import haveno.desktop.components.paymentmethods.StrikeForm; import haveno.desktop.components.paymentmethods.SwiftForm; import haveno.desktop.components.paymentmethods.SwishForm; import haveno.desktop.components.paymentmethods.TikkieForm; import haveno.desktop.components.paymentmethods.TransferwiseForm; import haveno.desktop.components.paymentmethods.TransferwiseUsdForm; import haveno.desktop.components.paymentmethods.USPostalMoneyOrderForm; import haveno.desktop.components.paymentmethods.UpholdForm; import haveno.desktop.components.paymentmethods.UpiForm; import haveno.desktop.components.paymentmethods.VenmoForm; import haveno.desktop.components.paymentmethods.VerseForm; import haveno.desktop.components.paymentmethods.WeChatPayForm; import haveno.desktop.components.paymentmethods.WesternUnionForm; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.portfolio.pendingtrades.PendingTradesViewModel; import haveno.desktop.main.portfolio.pendingtrades.steps.TradeStepView; import haveno.desktop.util.Layout; import javafx.geometry.Insets; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TextArea; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.text.Font; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; import java.util.List; import static com.google.common.base.Preconditions.checkNotNull; import static haveno.desktop.util.FormBuilder.addButtonBusyAnimationLabel; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import static haveno.desktop.util.FormBuilder.addTopLabelTextFieldWithCopyIcon; public class BuyerStep2View extends TradeStepView { private Button confirmButton; private BusyAnimation busyAnimation; private Subscription tradeStatePropertySubscription; private Timer timeoutTimer; private int paymentAccountGridRow = 0; private GridPane paymentAccountGridPane; private GridPane moreConfirmationsGridPane; private Label paymentDetailsLabel; private Label moreConfirmationsLabel; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialisation /////////////////////////////////////////////////////////////////////////////////////////// public BuyerStep2View(PendingTradesViewModel model) { super(model); } @Override public void activate() { super.activate(); if (timeoutTimer != null) timeoutTimer.stop(); //TODO we get called twice, check why if (tradeStatePropertySubscription == null) { tradeStatePropertySubscription = EasyBind.subscribe(trade.stateProperty(), state -> { if (timeoutTimer != null) timeoutTimer.stop(); if (trade.isDepositsUnlocked() && !trade.isPaymentSent()) { busyAnimation.stop(); setTradeStatus(""); showPopup(); } else if (state.ordinal() <= Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG.ordinal()) { switch (state) { case BUYER_CONFIRMED_PAYMENT_SENT: busyAnimation.play(); setTradeStatus(Res.get("shared.preparingConfirmation")); break; case BUYER_SENT_PAYMENT_SENT_MSG: busyAnimation.play(); setTradeStatus(Res.get("shared.sendingConfirmation")); timeoutTimer = UserThread.runAfter(() -> { busyAnimation.stop(); setTradeStatus(Res.get("shared.sendingConfirmationAgain")); }, 30); break; case BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG: busyAnimation.stop(); setTradeStatus(Res.get("shared.messageStoredInMailbox")); break; case BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG: case SELLER_RECEIVED_PAYMENT_SENT_MSG: busyAnimation.stop(); setTradeStatus(Res.get("shared.messageArrived")); break; case BUYER_SEND_FAILED_PAYMENT_SENT_MSG: // We get a popup and the trade closed, so we dont need to show anything here busyAnimation.stop(); setTradeStatus(""); break; default: log.warn("Unexpected case: State={}, tradeId={} ", state.name(), trade.getId()); busyAnimation.stop(); setTradeStatus(Res.get("shared.sendingConfirmationAgain")); break; } } }); } } @Override public void deactivate() { super.deactivate(); busyAnimation.stop(); if (timeoutTimer != null) timeoutTimer.stop(); if (tradeStatePropertySubscription != null) { tradeStatePropertySubscription.unsubscribe(); tradeStatePropertySubscription = null; } } @Override protected void onPendingTradesInitialized() { super.onPendingTradesInitialized(); } /////////////////////////////////////////////////////////////////////////////////////////// // Content /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void addContent() { gridPane.getColumnConstraints().get(1).setHgrow(Priority.ALWAYS); addTradeInfoBlock(); createPaymentDetailsGridPane(); createRecommendationGridPane(); // attach grid pane based on current state EasyBind.subscribe(trade.statePhaseProperty(), newValue -> { if (trade.isPaymentSent() || model.getShowPaymentDetailsEarly() || trade.isDepositsFinalized()) { attachPaymentDetailsGrid(); } else { attachRecommendationGrid(); } }); } private void createPaymentDetailsGridPane() { PaymentAccountPayload paymentAccountPayload = model.dataModel.getSellersPaymentAccountPayload(); String paymentMethodId = paymentAccountPayload != null ? paymentAccountPayload.getPaymentMethodId() : ""; paymentAccountGridPane = createGridPane(); TitledGroupBg accountTitledGroupBg = addTitledGroupBg(paymentAccountGridPane, paymentAccountGridRow, 4, Res.get("portfolio.pending.step2_buyer.startPaymentUsing", Res.get(paymentMethodId)), Layout.COMPACT_GROUP_DISTANCE); TextFieldWithCopyIcon field = addTopLabelTextFieldWithCopyIcon(paymentAccountGridPane, paymentAccountGridRow, 0, Res.get("portfolio.pending.step2_buyer.amountToTransfer"), model.getFiatVolume(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE).second; field.setCopyWithoutCurrencyPostFix(true); //preland: this fixes a textarea layout glitch // TODO: can this be removed now? TextArea uiHack = new TextArea(); uiHack.setMaxHeight(1); GridPane.setRowIndex(uiHack, 1); GridPane.setMargin(uiHack, new Insets(0, 0, 0, 0)); uiHack.setVisible(false); paymentAccountGridPane.getChildren().add(uiHack); switch (paymentMethodId) { case PaymentMethod.UPHOLD_ID: paymentAccountGridRow = UpholdForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.MONEY_BEAM_ID: paymentAccountGridRow = MoneyBeamForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.POPMONEY_ID: paymentAccountGridRow = PopmoneyForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.REVOLUT_ID: paymentAccountGridRow = RevolutForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.PERFECT_MONEY_ID: paymentAccountGridRow = PerfectMoneyForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.SEPA_ID: paymentAccountGridRow = SepaForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.SEPA_INSTANT_ID: paymentAccountGridRow = SepaInstantForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.FASTER_PAYMENTS_ID: paymentAccountGridRow = FasterPaymentsForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.NATIONAL_BANK_ID: paymentAccountGridRow = NationalBankForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.AUSTRALIA_PAYID_ID: paymentAccountGridRow = AustraliaPayidForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.SAME_BANK_ID: paymentAccountGridRow = SameBankForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.SPECIFIC_BANKS_ID: paymentAccountGridRow = SpecificBankForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.SWISH_ID: paymentAccountGridRow = SwishForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.ALI_PAY_ID: paymentAccountGridRow = AliPayForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.WECHAT_PAY_ID: paymentAccountGridRow = WeChatPayForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.ZELLE_ID: paymentAccountGridRow = ZelleForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.CHASE_QUICK_PAY_ID: paymentAccountGridRow = ChaseQuickPayForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.INTERAC_E_TRANSFER_ID: paymentAccountGridRow = InteracETransferForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.JAPAN_BANK_ID: paymentAccountGridRow = JapanBankTransferForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.US_POSTAL_MONEY_ORDER_ID: paymentAccountGridRow = USPostalMoneyOrderForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.CASH_DEPOSIT_ID: paymentAccountGridRow = CashDepositForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.PAY_BY_MAIL_ID: paymentAccountGridRow = PayByMailForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.CASH_AT_ATM_ID: paymentAccountGridRow = CashAtAtmForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.MONEY_GRAM_ID: paymentAccountGridRow = MoneyGramForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.WESTERN_UNION_ID: paymentAccountGridRow = WesternUnionForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.HAL_CASH_ID: paymentAccountGridRow = HalCashForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.F2F_ID: checkNotNull(model.dataModel.getTrade(), "model.dataModel.getTrade() must not be null"); checkNotNull(model.dataModel.getTrade().getOffer(), "model.dataModel.getTrade().getOffer() must not be null"); paymentAccountGridRow = F2FForm.addStep2Form(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload, model.dataModel.getTrade().getOffer(), 0, true); break; case PaymentMethod.BLOCK_CHAINS_ID: case PaymentMethod.BLOCK_CHAINS_INSTANT_ID: String labelTitle = Res.get("portfolio.pending.step2_buyer.sellersAddress", getCurrencyName(trade)); paymentAccountGridRow = AssetsForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload, labelTitle); break; case PaymentMethod.PROMPT_PAY_ID: paymentAccountGridRow = PromptPayForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.ADVANCED_CASH_ID: paymentAccountGridRow = AdvancedCashForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.TRANSFERWISE_ID: paymentAccountGridRow = TransferwiseForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.TRANSFERWISE_USD_ID: paymentAccountGridRow = TransferwiseUsdForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.PAYSERA_ID: paymentAccountGridRow = PayseraForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.PAXUM_ID: paymentAccountGridRow = PaxumForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.NEFT_ID: paymentAccountGridRow = NeftForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.RTGS_ID: paymentAccountGridRow = RtgsForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.IMPS_ID: paymentAccountGridRow = ImpsForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.UPI_ID: paymentAccountGridRow = UpiForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.PAYTM_ID: paymentAccountGridRow = PaytmForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.NEQUI_ID: paymentAccountGridRow = NequiForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.BIZUM_ID: paymentAccountGridRow = BizumForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.PIX_ID: paymentAccountGridRow = PixForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.AMAZON_GIFT_CARD_ID: paymentAccountGridRow = AmazonGiftCardForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.CAPITUAL_ID: paymentAccountGridRow = CapitualForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.CELPAY_ID: paymentAccountGridRow = CelPayForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.MONESE_ID: paymentAccountGridRow = MoneseForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.SATISPAY_ID: paymentAccountGridRow = SatispayForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.TIKKIE_ID: paymentAccountGridRow = TikkieForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.VERSE_ID: paymentAccountGridRow = VerseForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.STRIKE_ID: paymentAccountGridRow = StrikeForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.SWIFT_ID: paymentAccountGridRow = SwiftForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload, trade); break; case PaymentMethod.ACH_TRANSFER_ID: paymentAccountGridRow = AchTransferForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.DOMESTIC_WIRE_TRANSFER_ID: paymentAccountGridRow = DomesticWireTransferForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.CASH_APP_ID: paymentAccountGridRow = CashAppForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.PAYPAL_ID: paymentAccountGridRow = PayPalForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.VENMO_ID: paymentAccountGridRow = VenmoForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.PAYSAFE_ID: paymentAccountGridRow = PaysafeForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; default: log.error("Not supported PaymentMethod: " + paymentMethodId); } Trade trade = model.getTrade(); if (trade != null && model.getUser().getPaymentAccounts() != null) { Offer offer = trade.getOffer(); List possiblePaymentAccounts = PaymentAccountUtil.getPossiblePaymentAccounts(offer, model.getUser().getPaymentAccounts(), model.dataModel.getAccountAgeWitnessService()); PaymentAccountPayload buyersPaymentAccountPayload = model.dataModel.getBuyersPaymentAccountPayload(); if (buyersPaymentAccountPayload != null && possiblePaymentAccounts.size() > 1) { String id = buyersPaymentAccountPayload.getId(); possiblePaymentAccounts.stream() .filter(paymentAccount -> paymentAccount.getId().equals(id)) .findFirst() .ifPresent(paymentAccount -> { String accountName = paymentAccount.getAccountName(); addCompactTopLabelTextFieldWithCopyIcon(paymentAccountGridPane, ++paymentAccountGridRow, 0, Res.get("portfolio.pending.step2_buyer.buyerAccount"), accountName); }); } } GridPane.setRowSpan(accountTitledGroupBg, gridRow + paymentAccountGridRow - 1); Tuple4 tuple3 = addButtonBusyAnimationLabel(paymentAccountGridPane, ++paymentAccountGridRow, 0, Res.get("portfolio.pending.step2_buyer.paymentSent"), 10); HBox confirmButtonHBox = tuple3.fourth; GridPane.setColumnSpan(confirmButtonHBox, 2); confirmButton = tuple3.first; confirmButton.setDisable(!confirmPaymentSentPermitted()); confirmButton.setOnAction(e -> onPaymentSent()); busyAnimation = tuple3.second; paymentDetailsLabel = tuple3.third; } private void createRecommendationGridPane() { // create grid pane to show recommendation for more blocks moreConfirmationsGridPane = new GridPane(); moreConfirmationsGridPane.setStyle("-fx-background-color: -bs-content-background-gray;"); moreConfirmationsGridPane.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); // add title addTitledGroupBg(moreConfirmationsGridPane, 0, 1, Res.get("portfolio.pending.step1.waitForConf"), Layout.COMPACT_GROUP_DISTANCE); // add text Label label = new Label(Res.get("portfolio.pending.step2_buyer.additionalConf", Trade.NUM_BLOCKS_DEPOSITS_FINALIZED)); label.setFont(new Font(15)); GridPane.setMargin(label, new Insets(20, 0, 0, 0)); moreConfirmationsGridPane.add(label, 0, 1, 2, 1); Tuple4 tuple3 = addButtonBusyAnimationLabel(moreConfirmationsGridPane, gridRow, 0, Res.get("portfolio.pending.step2_buyer.showEarly"), 10); // add button to show payment details Button showPaymentDetailsButton = tuple3.first; showPaymentDetailsButton.setOnAction(e -> { model.setShowPaymentDetailsEarly(true); gridPane.getChildren().remove(moreConfirmationsGridPane); gridPane.getChildren().add(paymentAccountGridPane); GridPane.setRowIndex(paymentAccountGridPane, gridRow + 1); GridPane.setColumnSpan(paymentAccountGridPane, 2); statusLabel = paymentDetailsLabel; updateStatus(); }); moreConfirmationsLabel = tuple3.third; } private GridPane createGridPane() { GridPane gridPane = new GridPane(); gridPane.setHgap(Layout.GRID_GAP); gridPane.setVgap(Layout.GRID_GAP); ColumnConstraints columnConstraints1 = new ColumnConstraints(); columnConstraints1.setHgrow(Priority.ALWAYS); ColumnConstraints columnConstraints2 = new ColumnConstraints(); columnConstraints2.setHgrow(Priority.ALWAYS); gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2); return gridPane; } private void attachRecommendationGrid() { if (gridPane.getChildren().contains(moreConfirmationsGridPane)) return; if (gridPane.getChildren().contains(paymentAccountGridPane)) gridPane.getChildren().remove(paymentAccountGridPane); gridPane.getChildren().add(moreConfirmationsGridPane); GridPane.setRowIndex(moreConfirmationsGridPane, gridRow + 1); GridPane.setColumnSpan(moreConfirmationsGridPane, 2); statusLabel = moreConfirmationsLabel; updateStatus(); } private void attachPaymentDetailsGrid() { if (gridPane.getChildren().contains(paymentAccountGridPane)) return; if (gridPane.getChildren().contains(moreConfirmationsGridPane)) gridPane.getChildren().remove(moreConfirmationsGridPane); gridPane.getChildren().add(paymentAccountGridPane); GridPane.setRowIndex(paymentAccountGridPane, gridRow + 1); GridPane.setColumnSpan(paymentAccountGridPane, 2); statusLabel = paymentDetailsLabel; updateStatus(); } private boolean confirmPaymentSentPermitted() { if (!trade.confirmPermitted()) return false; if (trade.getState() == Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG) return true; return trade.isDepositsUnlocked() && trade.getState().ordinal() < Trade.State.BUYER_CONFIRMED_PAYMENT_SENT.ordinal(); } /////////////////////////////////////////////////////////////////////////////////////////// // Warning /////////////////////////////////////////////////////////////////////////////////////////// @Override protected String getFirstHalfOverWarnText() { return Res.get("portfolio.pending.step2_buyer.warn", getCurrencyCode(trade), model.getDateForOpenDispute()); } /////////////////////////////////////////////////////////////////////////////////////////// // Dispute /////////////////////////////////////////////////////////////////////////////////////////// @Override protected String getPeriodOverWarnText() { return Res.get("portfolio.pending.step2_buyer.openForDispute"); } @Override protected void applyOnDisputeOpened() { } @Override protected void updateDisputeState(Trade.DisputeState disputeState) { super.updateDisputeState(disputeState); confirmButton.setDisable(!confirmPaymentSentPermitted()); } /////////////////////////////////////////////////////////////////////////////////////////// // UI Handlers /////////////////////////////////////////////////////////////////////////////////////////// private void onPaymentSent() { if (!model.dataModel.isBootstrappedOrShowPopup()) { return; } if (!model.dataModel.isReadyForTxBroadcast()) { return; } PaymentAccountPayload sellersPaymentAccountPayload = model.dataModel.getSellersPaymentAccountPayload(); Trade trade = checkNotNull(model.dataModel.getTrade(), "trade must not be null"); if (sellersPaymentAccountPayload instanceof CashDepositAccountPayload) { String key = "confirmPaperReceiptSent"; if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) { Popup popup = new Popup(); popup.headLine(Res.get("portfolio.pending.step2_buyer.paperReceipt.headline")) .feedback(Res.get("portfolio.pending.step2_buyer.paperReceipt.msg")) .onAction(this::showConfirmPaymentSentPopup) .closeButtonText(Res.get("shared.no")) .onClose(popup::hide) .dontShowAgainId(key) .show(); } else { showConfirmPaymentSentPopup(); } } else if (sellersPaymentAccountPayload instanceof WesternUnionAccountPayload) { String key = "westernUnionMTCNSent"; if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) { String email = ((WesternUnionAccountPayload) sellersPaymentAccountPayload).getEmail(); Popup popup = new Popup(); popup.headLine(Res.get("portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline")) .feedback(Res.get("portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg", email)) .onAction(this::showConfirmPaymentSentPopup) .actionButtonText(Res.get("shared.yes")) .closeButtonText(Res.get("shared.no")) .onClose(popup::hide) .dontShowAgainId(key) .show(); } else { showConfirmPaymentSentPopup(); } } else if (sellersPaymentAccountPayload instanceof MoneyGramAccountPayload) { String key = "moneyGramMTCNSent"; if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) { String email = ((MoneyGramAccountPayload) sellersPaymentAccountPayload).getEmail(); Popup popup = new Popup(); popup.headLine(Res.get("portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline")) .feedback(Res.get("portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg", email)) .onAction(this::showConfirmPaymentSentPopup) .actionButtonText(Res.get("shared.yes")) .closeButtonText(Res.get("shared.no")) .onClose(popup::hide) .dontShowAgainId(key) .show(); } else { showConfirmPaymentSentPopup(); } } else if (sellersPaymentAccountPayload instanceof HalCashAccountPayload) { String key = "halCashCodeInfo"; if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) { String mobileNr = ((HalCashAccountPayload) sellersPaymentAccountPayload).getMobileNr(); Popup popup = new Popup(); popup.headLine(Res.get("portfolio.pending.step2_buyer.halCashInfo.headline")) .feedback(Res.get("portfolio.pending.step2_buyer.halCashInfo.msg", trade.getShortId(), mobileNr)) .onAction(this::showConfirmPaymentSentPopup) .actionButtonText(Res.get("shared.yes")) .closeButtonText(Res.get("shared.no")) .onClose(popup::hide) .dontShowAgainId(key) .show(); } else { showConfirmPaymentSentPopup(); } } else { showConfirmPaymentSentPopup(); } } private void showConfirmPaymentSentPopup() { String key = "confirmPaymentSent"; if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) { Popup popup = new Popup(); popup.headLine(Res.get("portfolio.pending.step2_buyer.confirmStart.headline")) .confirmation(Res.get("portfolio.pending.step2_buyer.confirmStart.msg", getCurrencyName(trade))) .width(700) .actionButtonText(Res.get("portfolio.pending.step2_buyer.confirmStart.yes")) .onAction(this::confirmPaymentSent) .closeButtonText(Res.get("shared.no")) .onClose(popup::hide) .dontShowAgainId(key) .show(); } else { confirmPaymentSent(); } } private void confirmPaymentSent() { busyAnimation.play(); setTradeStatus(Res.get("shared.preparingConfirmation")); confirmButton.setDisable(true); model.dataModel.onPaymentSent(() -> { }, errorMessage -> { busyAnimation.stop(); new Popup().warning(Res.get("popup.warning.sendMsgFailed") + "\n\n" + errorMessage).show(); confirmButton.setDisable(!confirmPaymentSentPermitted()); UserThread.execute(() -> setTradeStatus("Error confirming payment sent.")); }); } private void showPopup() { PaymentAccountPayload paymentAccountPayload = model.dataModel.getSellersPaymentAccountPayload(); if (paymentAccountPayload != null && !trade.isPayoutPublished()) { String message = Res.get("portfolio.pending.step2.confReached"); String refTextWarn = Res.get("portfolio.pending.step2_buyer.refTextWarn"); String fees = Res.get("portfolio.pending.step2_buyer.fees"); String id = trade.getShortId(); String amount = VolumeUtil.formatVolumeWithCode(trade.getVolume()); if (paymentAccountPayload instanceof AssetAccountPayload) { message += Res.get("portfolio.pending.step2_buyer.crypto", getCurrencyName(trade), amount); } else if (paymentAccountPayload instanceof CashDepositAccountPayload) { message += Res.get("portfolio.pending.step2_buyer.cash", amount) + refTextWarn + "\n\n" + fees + "\n\n" + Res.get("portfolio.pending.step2_buyer.cash.extra"); } else if (paymentAccountPayload instanceof WesternUnionAccountPayload) { final String email = ((WesternUnionAccountPayload) paymentAccountPayload).getEmail(); final String extra = Res.get("portfolio.pending.step2_buyer.westernUnion.extra", email); message += Res.get("portfolio.pending.step2_buyer.westernUnion", amount) + extra; } else if (paymentAccountPayload instanceof MoneyGramAccountPayload) { final String email = ((MoneyGramAccountPayload) paymentAccountPayload).getEmail(); final String extra = Res.get("portfolio.pending.step2_buyer.moneyGram.extra", email); message += Res.get("portfolio.pending.step2_buyer.moneyGram", amount) + extra; } else if (paymentAccountPayload instanceof USPostalMoneyOrderAccountPayload) { message += Res.get("portfolio.pending.step2_buyer.postal", amount) + refTextWarn; } else if (paymentAccountPayload instanceof F2FAccountPayload) { message += Res.get("portfolio.pending.step2_buyer.f2f", amount); } else if (paymentAccountPayload instanceof FasterPaymentsAccountPayload) { message += Res.get("portfolio.pending.step2_buyer.pay", amount) + Res.get("portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo") + "\n\n" + refTextWarn + "\n\n" + fees; } else if (paymentAccountPayload instanceof PayByMailAccountPayload || paymentAccountPayload instanceof HalCashAccountPayload) { message += Res.get("portfolio.pending.step2_buyer.pay", amount); } else if (paymentAccountPayload instanceof SwiftAccountPayload) { message += Res.get("portfolio.pending.step2_buyer.pay", amount) + refTextWarn + "\n\n" + Res.get("portfolio.pending.step2_buyer.fees.swift"); } else { message += Res.get("portfolio.pending.step2_buyer.pay", amount) + refTextWarn + "\n\n" + fees; } String key = "startPayment" + trade.getId(); if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) { DontShowAgainLookup.dontShowAgain(key, true); new Popup().headLine(Res.get("popup.attention.forTradeWithId", id)) .attention(message) .show(); } } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep3View.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.pendingtrades.steps.buyer; import de.jensd.fx.fontawesome.AwesomeIcon; import haveno.common.util.Tuple4; import haveno.core.locale.Res; import haveno.core.network.MessageState; import haveno.desktop.components.TextFieldWithIcon; import haveno.desktop.main.portfolio.pendingtrades.PendingTradesViewModel; import haveno.desktop.main.portfolio.pendingtrades.steps.TradeStepView; import haveno.desktop.util.Layout; import javafx.beans.value.ChangeListener; import javafx.scene.control.Label; import javafx.scene.layout.GridPane; import javafx.scene.layout.VBox; import static haveno.desktop.util.FormBuilder.addMultilineLabel; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import static haveno.desktop.util.FormBuilder.addTopLabelTextFieldWithIconLabel; public class BuyerStep3View extends TradeStepView { private final ChangeListener messageStateChangeListener; private TextFieldWithIcon textFieldWithIcon; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialisation /////////////////////////////////////////////////////////////////////////////////////////// public BuyerStep3View(PendingTradesViewModel model) { super(model); messageStateChangeListener = (observable, oldValue, newValue) -> { updateMessageStateInfo(); }; } @Override public void activate() { super.activate(); model.getPaymentSentMessageStateProperty().addListener(messageStateChangeListener); updateMessageStateInfo(); } public void deactivate() { super.deactivate(); model.getPaymentSentMessageStateProperty().removeListener(messageStateChangeListener); } /////////////////////////////////////////////////////////////////////////////////////////// // Info /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void addInfoBlock() { addTitledGroupBg(gridPane, ++gridRow, 2, getInfoBlockTitle(), Layout.GROUP_DISTANCE); infoLabel = addMultilineLabel(gridPane, gridRow, "", Layout.FIRST_ROW_AND_GROUP_DISTANCE); GridPane.setColumnSpan(infoLabel, 2); Tuple4 tuple = addTopLabelTextFieldWithIconLabel(gridPane, ++gridRow, Res.get("portfolio.pending.step3_buyer.wait.msgStateInfo.label"), 0); GridPane.setColumnSpan(tuple.first, 2); textFieldWithIcon = tuple.third; statusLabel = tuple.fourth; } @Override protected String getInfoBlockTitle() { return Res.get("portfolio.pending.step3_buyer.wait.headline"); } @Override protected String getInfoText() { return Res.get("portfolio.pending.step3_buyer.wait.info", getCurrencyCode(trade)); } private void updateMessageStateInfo() { MessageState messageState = model.getPaymentSentMessageStateProperty().get(); textFieldWithIcon.setText(Res.get("message.state." + messageState.name())); Label iconLabel = textFieldWithIcon.getIconLabel(); switch (messageState) { case UNDEFINED: textFieldWithIcon.setIcon(AwesomeIcon.QUESTION); iconLabel.getStyleClass().add("trade-msg-state-undefined"); break; case SENT: textFieldWithIcon.setIcon(AwesomeIcon.ARROW_RIGHT); iconLabel.getStyleClass().add("trade-msg-state-sent"); break; case ARRIVED: textFieldWithIcon.setIcon(AwesomeIcon.OK); iconLabel.getStyleClass().add("trade-msg-state-arrived"); break; case STORED_IN_MAILBOX: textFieldWithIcon.setIcon(AwesomeIcon.ENVELOPE_ALT); iconLabel.getStyleClass().add("trade-msg-state-stored"); break; case ACKNOWLEDGED: textFieldWithIcon.setIcon(AwesomeIcon.OK_SIGN); iconLabel.getStyleClass().add("trade-msg-state-stored"); break; case FAILED: case NACKED: textFieldWithIcon.setIcon(AwesomeIcon.EXCLAMATION_SIGN); iconLabel.getStyleClass().add("trade-msg-state-acknowledged"); break; } } /////////////////////////////////////////////////////////////////////////////////////////// // Warning /////////////////////////////////////////////////////////////////////////////////////////// @Override protected String getFirstHalfOverWarnText() { String substitute = model.isBlockChainMethod() ? Res.get("portfolio.pending.step3_buyer.warn.part1a", getCurrencyCode(trade)) : Res.get("portfolio.pending.step3_buyer.warn.part1b"); return Res.get("portfolio.pending.step3_buyer.warn.part2", substitute); } /////////////////////////////////////////////////////////////////////////////////////////// // Dispute /////////////////////////////////////////////////////////////////////////////////////////// @Override protected String getPeriodOverWarnText() { return Res.get("portfolio.pending.step3_buyer.openForDispute"); } @Override protected void applyOnDisputeOpened() { } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.pendingtrades.steps.buyer; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.core.locale.Res; import haveno.core.user.DontShowAgainLookup; import haveno.core.xmr.model.XmrAddressEntry; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.TitledGroupBg; import haveno.desktop.main.MainView; import haveno.desktop.main.overlays.notifications.Notification; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.TradeFeedbackWindow; import haveno.desktop.main.portfolio.PortfolioView; import haveno.desktop.main.portfolio.closedtrades.ClosedTradesView; import haveno.desktop.main.portfolio.pendingtrades.PendingTradesViewModel; import haveno.desktop.main.portfolio.pendingtrades.steps.TradeStepView; import haveno.desktop.util.Layout; import javafx.geometry.Insets; import javafx.scene.control.Button; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import java.util.concurrent.TimeUnit; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; public class BuyerStep4View extends TradeStepView { private Button closeButton; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialisation /////////////////////////////////////////////////////////////////////////////////////////// public BuyerStep4View(PendingTradesViewModel model) { super(model); } @Override public void activate() { super.activate(); // Don't display any trade step info when trade is complete hideTradeStepInfo(); } @Override public void deactivate() { super.deactivate(); } /////////////////////////////////////////////////////////////////////////////////////////// // Content /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void addContent() { gridPane.getColumnConstraints().get(1).setHgrow(Priority.SOMETIMES); TitledGroupBg completedTradeLabel = new TitledGroupBg(); if (trade.getDisputeState().isMediated()) { completedTradeLabel.setText(Res.get("portfolio.pending.step5_buyer.groupTitle.mediated")); } else if (trade.getDisputeState().isDisputed() && trade.getDisputeResult() != null) { completedTradeLabel.setText(Res.get("portfolio.pending.step5_buyer.groupTitle.arbitrated")); } else { completedTradeLabel.setText(Res.get("portfolio.pending.step5_buyer.groupTitle")); } HBox hBox2 = new HBox(1, completedTradeLabel); GridPane.setMargin(hBox2, new Insets(18, -10, -12, -10)); gridPane.getChildren().add(hBox2); GridPane.setRowSpan(hBox2, 5); if (trade.isPaymentReceived()) { addCompactTopLabelTextField(gridPane, gridRow, getXmrTradeAmountLabel(), model.getTradeVolume(), Layout.TWICE_FIRST_ROW_DISTANCE); addCompactTopLabelTextField(gridPane, ++gridRow, getTraditionalTradeAmountLabel(), model.getFiatVolume()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("portfolio.pending.step5_buyer.refunded"), model.getSecurityDeposit()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("portfolio.pending.step5_buyer.tradeFee"), model.getTradeFee()); } closeButton = new AutoTooltipButton(Res.get("shared.close")); closeButton.setDefaultButton(true); closeButton.getStyleClass().add("action-button"); GridPane.setRowIndex(closeButton, ++gridRow); GridPane.setMargin(closeButton, new Insets(Layout.GROUP_DISTANCE, 10, 0, 0)); gridPane.getChildren().add(closeButton); closeButton.setOnAction(e -> { handleTradeCompleted(); model.dataModel.tradeManager.onTradeCompleted(trade); }); String key = "tradeCompleted" + trade.getId(); if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) { DontShowAgainLookup.dontShowAgain(key, true); new Notification().headLine(Res.get("notification.tradeCompleted.headline")) .notification(Res.get("notification.tradeCompleted.msg")) .autoClose() .show(); } } private void handleTradeCompleted() { closeButton.setDisable(true); model.dataModel.xmrWalletService.swapAddressEntryToAvailable(trade.getId(), XmrAddressEntry.Context.TRADE_PAYOUT); openTradeFeedbackWindow(); } private void openTradeFeedbackWindow() { String key = "feedbackPopupAfterTrade"; if (!DevEnv.isDevMode() && preferences.showAgain(key)) { UserThread.runAfter(() -> new TradeFeedbackWindow() .dontShowAgainId(key) .onAction(this::showNavigateToClosedTradesViewPopup) .show(), 500, TimeUnit.MILLISECONDS); } else { showNavigateToClosedTradesViewPopup(); } } private void showNavigateToClosedTradesViewPopup() { if (!DevEnv.isDevMode()) { UserThread.runAfter(() -> new Popup().headLine(Res.get("portfolio.pending.step5_buyer.tradeCompleted.headline")) .feedback(Res.get("portfolio.pending.step5_buyer.tradeCompleted.msg")) .actionButtonTextWithGoTo("portfolio.tab.history") .onAction(() -> model.dataModel.navigation.navigateTo(MainView.class, PortfolioView.class, ClosedTradesView.class)) .dontShowAgainId("tradeCompleteWithdrawCompletedInfo") .show(), 500, TimeUnit.MILLISECONDS); } } protected String getXmrTradeAmountLabel() { return Res.get("portfolio.pending.step5_buyer.bought"); } protected String getTraditionalTradeAmountLabel() { return Res.get("portfolio.pending.step5_buyer.paid"); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep1View.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.pendingtrades.steps.seller; import haveno.core.locale.Res; import haveno.desktop.main.portfolio.pendingtrades.PendingTradesViewModel; import haveno.desktop.main.portfolio.pendingtrades.steps.TradeStepView; public class SellerStep1View extends TradeStepView { /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialisation /////////////////////////////////////////////////////////////////////////////////////////// public SellerStep1View(PendingTradesViewModel model) { super(model); } @Override protected void onPendingTradesInitialized() { super.onPendingTradesInitialized(); //checkForUnconfirmedTimeout(); } /////////////////////////////////////////////////////////////////////////////////////////// // Info /////////////////////////////////////////////////////////////////////////////////////////// @Override protected String getInfoBlockTitle() { return Res.get("portfolio.pending.step1.waitForConf"); } @Override protected String getInfoText() { return Res.get("portfolio.pending.step1.info.buyer", getNumMinutesToUnlock()); } @Override public void onDepositTxsUpdate() { infoLabel.setText(getInfoText()); } /////////////////////////////////////////////////////////////////////////////////////////// // Warning /////////////////////////////////////////////////////////////////////////////////////////// @Override protected String getFirstHalfOverWarnText() { return Res.get("portfolio.pending.step1.warn"); } /////////////////////////////////////////////////////////////////////////////////////////// // Dispute /////////////////////////////////////////////////////////////////////////////////////////// @Override protected String getPeriodOverWarnText() { return Res.get("portfolio.pending.step1.openForDispute"); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep2View.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.pendingtrades.steps.seller; import haveno.core.locale.Res; import haveno.core.payment.payload.F2FAccountPayload; import haveno.desktop.components.paymentmethods.F2FForm; import haveno.desktop.main.portfolio.pendingtrades.PendingTradesViewModel; import haveno.desktop.main.portfolio.pendingtrades.steps.TradeStepView; import haveno.desktop.util.Layout; import static com.google.common.base.Preconditions.checkNotNull; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; public class SellerStep2View extends TradeStepView { /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialisation /////////////////////////////////////////////////////////////////////////////////////////// public SellerStep2View(PendingTradesViewModel model) { super(model); } @Override protected void addContent() { addTradeInfoBlock(); addInfoBlock(); checkNotNull(model.dataModel.getTrade(), "No trade found"); checkNotNull(model.dataModel.getTrade().getOffer(), "No offer found"); if (model.dataModel.getSellersPaymentAccountPayload() instanceof F2FAccountPayload) { addTitledGroupBg(gridPane, ++gridRow, 4, Res.get("portfolio.pending.step2_seller.f2fInfo.headline"), Layout.COMPACT_GROUP_DISTANCE); gridRow = F2FForm.addStep2Form(gridPane, --gridRow, model.dataModel.getSellersPaymentAccountPayload(), model.dataModel.getTrade().getOffer(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, false); } } @Override public void activate() { super.activate(); } @Override public void deactivate() { super.deactivate(); } @Override protected void onPendingTradesInitialized() { super.onPendingTradesInitialized(); } /////////////////////////////////////////////////////////////////////////////////////////// // Info /////////////////////////////////////////////////////////////////////////////////////////// @Override protected String getInfoBlockTitle() { return Res.get("portfolio.pending.step2_seller.waitPayment.headline"); } @Override protected String getInfoText() { return Res.get("portfolio.pending.step2_seller.waitPayment.msg", getCurrencyCode(trade)); } /////////////////////////////////////////////////////////////////////////////////////////// // Warning /////////////////////////////////////////////////////////////////////////////////////////// @Override protected String getFirstHalfOverWarnText() { return Res.get("portfolio.pending.step2_seller.warn", getCurrencyCode(trade), model.getDateForOpenDispute()); } /////////////////////////////////////////////////////////////////////////////////////////// // Dispute /////////////////////////////////////////////////////////////////////////////////////////// @Override protected String getPeriodOverWarnText() { return Res.get("portfolio.pending.step2_seller.openForDispute"); } @Override protected void applyOnDisputeOpened() { } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.pendingtrades.steps.seller; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.util.Tuple2; import haveno.common.util.Tuple4; import haveno.core.locale.Res; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccountUtil; import haveno.core.payment.payload.AmazonGiftCardAccountPayload; import haveno.core.payment.payload.AssetAccountPayload; import haveno.core.payment.payload.BankAccountPayload; import haveno.core.payment.payload.PayByMailAccountPayload; import haveno.core.payment.payload.CashDepositAccountPayload; import haveno.core.payment.payload.F2FAccountPayload; import haveno.core.payment.payload.HalCashAccountPayload; import haveno.core.payment.payload.MoneyGramAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.SepaAccountPayload; import haveno.core.payment.payload.SepaInstantAccountPayload; import haveno.core.payment.payload.USPostalMoneyOrderAccountPayload; import haveno.core.payment.payload.WesternUnionAccountPayload; import haveno.core.trade.Contract; import haveno.core.trade.Trade; import haveno.core.user.DontShowAgainLookup; import haveno.core.util.VolumeUtil; import haveno.desktop.components.BusyAnimation; import haveno.desktop.components.InfoTextField; import haveno.desktop.components.TextFieldWithCopyIcon; import haveno.desktop.components.indicator.TxConfidenceIndicator; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.portfolio.pendingtrades.PendingTradesViewModel; import haveno.desktop.main.portfolio.pendingtrades.steps.TradeStepView; import haveno.desktop.util.Layout; import javafx.beans.value.ChangeListener; import javafx.geometry.Insets; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.Tooltip; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; import javax.annotation.Nullable; import java.util.Optional; import static haveno.desktop.util.FormBuilder.addButtonBusyAnimationLabelAfterGroup; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import static haveno.desktop.util.FormBuilder.addTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.getTopLabelWithVBox; import static haveno.desktop.util.Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE; import static haveno.desktop.util.Layout.COMPACT_GROUP_DISTANCE; import static haveno.desktop.util.Layout.FLOATING_LABEL_DISTANCE; public class SellerStep3View extends TradeStepView { private Button confirmButton; private BusyAnimation busyAnimation; private Subscription tradeStatePropertySubscription; private Timer timeoutTimer; @Nullable private InfoTextField assetTxProofResultField; @Nullable private TxConfidenceIndicator assetTxConfidenceIndicator; @Nullable private ChangeListener proofResultListener; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialisation /////////////////////////////////////////////////////////////////////////////////////////// public SellerStep3View(PendingTradesViewModel model) { super(model); } @Override public void activate() { super.activate(); if (timeoutTimer != null) timeoutTimer.stop(); tradeStatePropertySubscription = EasyBind.subscribe(trade.stateProperty(), state -> { if (timeoutTimer != null) timeoutTimer.stop(); if (trade.isPaymentSent() && !trade.isPaymentReceived()) { busyAnimation.stop(); setTradeStatus(""); showPopup(); } else if (trade.isPaymentReceived()) { if (trade.isCompleted()) { if (!trade.isPayoutPublished()) log.warn("Payout is expected to be published for {} {} state {}", trade.getClass().getSimpleName(), trade.getId(), trade.getState()); busyAnimation.stop(); setTradeStatus(""); } else switch (state) { case SELLER_CONFIRMED_PAYMENT_RECEIPT: busyAnimation.play(); setTradeStatus(Res.get("shared.preparingConfirmation")); break; case SELLER_SENT_PAYMENT_RECEIVED_MSG: busyAnimation.play(); setTradeStatus(Res.get("shared.sendingConfirmation")); timeoutTimer = UserThread.runAfter(() -> { busyAnimation.stop(); setTradeStatus(Res.get("shared.sendingConfirmationAgain")); }, 30); break; case SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG: busyAnimation.stop(); setTradeStatus(Res.get("shared.messageStoredInMailbox")); break; case SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG: case BUYER_RECEIVED_PAYMENT_RECEIVED_MSG: busyAnimation.stop(); setTradeStatus(Res.get("shared.messageArrived")); break; case SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG: // We get a popup and the trade closed, so we dont need to show anything here busyAnimation.stop(); setTradeStatus(""); break; default: log.warn("Unexpected case: State={}, tradeId={} " + state.name(), trade.getId()); busyAnimation.stop(); setTradeStatus(Res.get("shared.sendingConfirmationAgain")); break; } } // update confirm button state confirmButton.setDisable(!confirmPaymentReceivedPermitted()); }); } @Override public void deactivate() { super.deactivate(); if (tradeStatePropertySubscription != null) { tradeStatePropertySubscription.unsubscribe(); tradeStatePropertySubscription = null; } busyAnimation.stop(); if (timeoutTimer != null) { timeoutTimer.stop(); } } /////////////////////////////////////////////////////////////////////////////////////////// // Content /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void addContent() { gridPane.getColumnConstraints().get(1).setHgrow(Priority.ALWAYS); addTradeInfoBlock(); addTitledGroupBg(gridPane, ++gridRow, 3, Res.get("portfolio.pending.step3_seller.confirmPaymentReceipt"), COMPACT_GROUP_DISTANCE); TextFieldWithCopyIcon field = addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, Res.get("portfolio.pending.step3_seller.amountToReceive"), model.getFiatVolume(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE).second; field.setCopyWithoutCurrencyPostFix(true); String myPaymentDetails = ""; String peersPaymentDetails = ""; String myTitle = ""; String peersTitle = ""; String currencyName = getCurrencyName(trade); Contract contract = trade.getContract(); if (contract != null) { PaymentAccountPayload myPaymentAccountPayload = trade.getSeller().getPaymentAccountPayload(); PaymentAccountPayload peersPaymentAccountPayload = trade.getBuyer().getPaymentAccountPayload(); myPaymentDetails = PaymentAccountUtil.findPaymentAccount(myPaymentAccountPayload, model.getUser()) .map(PaymentAccount::getAccountName) .orElse(""); if (myPaymentAccountPayload instanceof AssetAccountPayload) { // for crypto always display the receiving address myPaymentDetails = ((AssetAccountPayload) myPaymentAccountPayload).getAddress(); peersPaymentDetails = peersPaymentAccountPayload != null ? ((AssetAccountPayload) peersPaymentAccountPayload).getAddress() : "NA"; myTitle = Res.get("portfolio.pending.step3_seller.yourAddress", currencyName); peersTitle = Res.get("portfolio.pending.step3_seller.buyersAddress", currencyName); } else { if (myPaymentDetails.isEmpty()) { // Not expected myPaymentDetails = myPaymentAccountPayload != null ? myPaymentAccountPayload.getPaymentDetails() : "NA"; } peersPaymentDetails = peersPaymentAccountPayload != null ? peersPaymentAccountPayload.getPaymentDetails() : "NA"; myTitle = Res.get("portfolio.pending.step3_seller.yourAccount"); peersTitle = Res.get("portfolio.pending.step3_seller.buyersAccount"); } } if (isXmrTrade()) { assetTxProofResultField = new InfoTextField(); Tuple2 topLabelWithVBox = getTopLabelWithVBox(Res.get("portfolio.pending.step3_seller.autoConf.status.label"), assetTxProofResultField); VBox vBox = topLabelWithVBox.second; assetTxConfidenceIndicator = new TxConfidenceIndicator(); assetTxConfidenceIndicator.setId("xmr-confidence"); assetTxConfidenceIndicator.setProgress(0); assetTxConfidenceIndicator.setTooltip(new Tooltip()); assetTxProofResultField.setContentForInfoPopOver(createPopoverLabel(Res.get("setting.info.msg"))); HBox.setMargin(assetTxConfidenceIndicator, new Insets(FLOATING_LABEL_DISTANCE, 0, 0, 0)); HBox hBox = new HBox(); HBox.setHgrow(vBox, Priority.ALWAYS); hBox.setSpacing(10); hBox.getChildren().addAll(vBox, assetTxConfidenceIndicator); GridPane.setRowIndex(hBox, gridRow); GridPane.setColumnIndex(hBox, 1); GridPane.setMargin(hBox, new Insets(COMPACT_FIRST_ROW_AND_GROUP_DISTANCE + FLOATING_LABEL_DISTANCE, 0, 0, 0)); gridPane.getChildren().add(hBox); } TextFieldWithCopyIcon myPaymentDetailsTextField = addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, 0, myTitle, myPaymentDetails).second; myPaymentDetailsTextField.setMouseTransparent(false); myPaymentDetailsTextField.setTooltip(new Tooltip(myPaymentDetails)); TextFieldWithCopyIcon peersPaymentDetailsTextField = addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, peersTitle, peersPaymentDetails).second; peersPaymentDetailsTextField.setMouseTransparent(false); peersPaymentDetailsTextField.setTooltip(new Tooltip(peersPaymentDetails)); String counterCurrencyTxId = trade.getCounterCurrencyTxId(); String counterCurrencyExtraData = trade.getCounterCurrencyExtraData(); if (counterCurrencyTxId != null && !counterCurrencyTxId.isEmpty() && counterCurrencyExtraData != null && !counterCurrencyExtraData.isEmpty()) { TextFieldWithCopyIcon txHashTextField = addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, 0, Res.get("portfolio.pending.step3_seller.xmrTxHash"), counterCurrencyTxId).second; txHashTextField.setMouseTransparent(false); txHashTextField.setTooltip(new Tooltip(myPaymentDetails)); TextFieldWithCopyIcon txKeyDetailsTextField = addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("portfolio.pending.step3_seller.xmrTxKey"), counterCurrencyExtraData).second; txKeyDetailsTextField.setMouseTransparent(false); txKeyDetailsTextField.setTooltip(new Tooltip(peersPaymentDetails)); } Tuple4 tuple = addButtonBusyAnimationLabelAfterGroup(gridPane, ++gridRow, Res.get("portfolio.pending.step3_seller.confirmReceipt")); HBox hBox = tuple.fourth; GridPane.setColumnSpan(tuple.fourth, 2); confirmButton = tuple.first; confirmButton.setDisable(!confirmPaymentReceivedPermitted()); confirmButton.setOnAction(e -> onPaymentReceived()); busyAnimation = tuple.second; statusLabel = tuple.third; } private boolean confirmPaymentReceivedPermitted() { if (!trade.confirmPermitted()) return false; if (trade.getState() == Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG) return true; return trade.getState().ordinal() >= Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal() && trade.getState().ordinal() < Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT.ordinal(); } /////////////////////////////////////////////////////////////////////////////////////////// // Info /////////////////////////////////////////////////////////////////////////////////////////// @Override protected String getInfoText() { String currencyName = getCurrencyName(trade); if (model.isBlockChainMethod()) { return Res.get("portfolio.pending.step3_seller.buyerStartedPayment", Res.get("portfolio.pending.step3_seller.buyerStartedPayment.crypto", currencyName)); } else { return Res.get("portfolio.pending.step3_seller.buyerStartedPayment", Res.get("portfolio.pending.step3_seller.buyerStartedPayment.traditional", currencyName)); } } /////////////////////////////////////////////////////////////////////////////////////////// // Warning /////////////////////////////////////////////////////////////////////////////////////////// @Override protected String getFirstHalfOverWarnText() { String substitute = model.isBlockChainMethod() ? Res.get("portfolio.pending.step3_seller.warn.part1a", getCurrencyName(trade)) : Res.get("portfolio.pending.step3_seller.warn.part1b"); return Res.get("portfolio.pending.step3_seller.warn.part2", substitute); } /////////////////////////////////////////////////////////////////////////////////////////// // Dispute /////////////////////////////////////////////////////////////////////////////////////////// @Override protected String getPeriodOverWarnText() { return Res.get("portfolio.pending.step3_seller.openForDispute"); } @Override protected void applyOnDisputeOpened() { } @Override protected void updateDisputeState(Trade.DisputeState disputeState) { super.updateDisputeState(disputeState); confirmButton.setDisable(!confirmPaymentReceivedPermitted()); } //////////////////////////////////////////////////////////////////////////////////////// // UI Handlers /////////////////////////////////////////////////////////////////////////////////////////// private void onPaymentReceived() { // The confirmPaymentReceived call will trigger the trade protocol to do the payout tx. We want to be sure that we // are well connected to the Bitcoin network before triggering the broadcast. if (model.dataModel.isReadyForTxBroadcast()) { String key = "confirmPaymentReceived"; if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) { PaymentAccountPayload paymentAccountPayload = model.dataModel.getSellersPaymentAccountPayload(); String message = Res.get("portfolio.pending.step3_seller.onPaymentReceived.part1", getCurrencyName(trade)); if (!(paymentAccountPayload instanceof AssetAccountPayload)) { Optional optionalHolderName = getOptionalHolderName(); if (optionalHolderName.isPresent()) { message += Res.get("portfolio.pending.step3_seller.onPaymentReceived.name", optionalHolderName.get()); } } message += Res.get("portfolio.pending.step3_seller.onPaymentReceived.note"); if (model.dataModel.isSignWitnessTrade()) { message += Res.get("portfolio.pending.step3_seller.onPaymentReceived.signer"); } new Popup() .headLine(Res.get("portfolio.pending.step3_seller.onPaymentReceived.confirm.headline")) .confirmation(message) .width(700) .actionButtonText(Res.get("portfolio.pending.step3_seller.onPaymentReceived.confirm.yes")) .onAction(this::confirmPaymentReceived) .closeButtonText(Res.get("shared.cancel")) .show(); } else { confirmPaymentReceived(); } } } private void showPopup() { PaymentAccountPayload paymentAccountPayload = model.dataModel.getSellersPaymentAccountPayload(); String key = "confirmPayment" + trade.getId(); String message = ""; String tradeVolumeWithCode = VolumeUtil.formatVolumeWithCode(trade.getVolume()); String currencyName = getCurrencyName(trade); String part1 = Res.get("portfolio.pending.step3_seller.part", currencyName); if (paymentAccountPayload instanceof AssetAccountPayload) { String address = ((AssetAccountPayload) paymentAccountPayload).getAddress(); String explorerOrWalletString = isXmrTrade() ? Res.get("portfolio.pending.step3_seller.crypto.wallet", currencyName) : Res.get("portfolio.pending.step3_seller.crypto.explorer", currencyName); message = Res.get("portfolio.pending.step3_seller.crypto", part1, explorerOrWalletString, address, tradeVolumeWithCode, currencyName); } else { if (paymentAccountPayload instanceof USPostalMoneyOrderAccountPayload) { message = Res.get("portfolio.pending.step3_seller.postal", part1, tradeVolumeWithCode); } else if (paymentAccountPayload instanceof PayByMailAccountPayload) { message = Res.get("portfolio.pending.step3_seller.payByMail", part1, tradeVolumeWithCode); } else if (!(paymentAccountPayload instanceof WesternUnionAccountPayload) && !(paymentAccountPayload instanceof HalCashAccountPayload) && !(paymentAccountPayload instanceof F2FAccountPayload) && !(paymentAccountPayload instanceof AmazonGiftCardAccountPayload)) { message = Res.get("portfolio.pending.step3_seller.bank", currencyName, tradeVolumeWithCode); } String part = Res.get("portfolio.pending.step3_seller.openDispute"); if (paymentAccountPayload instanceof CashDepositAccountPayload) message = message + Res.get("portfolio.pending.step3_seller.cash", part); else if (paymentAccountPayload instanceof WesternUnionAccountPayload) message = message + Res.get("portfolio.pending.step3_seller.westernUnion"); else if (paymentAccountPayload instanceof MoneyGramAccountPayload) message = message + Res.get("portfolio.pending.step3_seller.moneyGram"); else if (paymentAccountPayload instanceof HalCashAccountPayload) message = message + Res.get("portfolio.pending.step3_seller.halCash"); else if (paymentAccountPayload instanceof F2FAccountPayload) message = part1; else if (paymentAccountPayload instanceof AmazonGiftCardAccountPayload) message = Res.get("portfolio.pending.step3_seller.amazonGiftCard"); Optional optionalHolderName = getOptionalHolderName(); if (optionalHolderName.isPresent()) { message += Res.get("portfolio.pending.step3_seller.bankCheck", optionalHolderName.get(), part); } if (model.dataModel.isSignWitnessTrade()) { message += "\n\n" + Res.get("portfolio.pending.step3_seller.onPaymentReceived.signer"); } } if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) { DontShowAgainLookup.dontShowAgain(key, true); new Popup().headLine(Res.get("popup.attention.forTradeWithId", trade.getShortId())) .attention(message) .show(); } } private void confirmPaymentReceived() { log.info("User pressed the [Confirm payment receipt] button for Trade {}", trade.getShortId()); busyAnimation.play(); setTradeStatus(Res.get("shared.preparingConfirmation")); confirmButton.setDisable(true); model.dataModel.onPaymentReceived(() -> { }, errorMessage -> { busyAnimation.stop(); new Popup().warning(Res.get("popup.warning.sendMsgFailed") + "\n\n" + errorMessage).show(); confirmButton.setDisable(!confirmPaymentReceivedPermitted()); UserThread.execute(() -> setTradeStatus("Error confirming payment received.")); }); } private Optional getOptionalHolderName() { Contract contract = trade.getContract(); if (contract != null) { PaymentAccountPayload paymentAccountPayload = trade.getBuyer().getPaymentAccountPayload(); if (paymentAccountPayload instanceof BankAccountPayload) return Optional.of(((BankAccountPayload) paymentAccountPayload).getHolderName()); else if (paymentAccountPayload instanceof SepaAccountPayload) return Optional.of(((SepaAccountPayload) paymentAccountPayload).getHolderName()); else if (paymentAccountPayload instanceof SepaInstantAccountPayload) return Optional.of(((SepaInstantAccountPayload) paymentAccountPayload).getHolderName()); else return Optional.empty(); } else { return Optional.empty(); } } private Label createPopoverLabel(String text) { Label label = new Label(text); label.setPrefWidth(600); label.setWrapText(true); label.setPadding(new Insets(10)); return label; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep4View.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.pendingtrades.steps.seller; import haveno.core.locale.Res; import haveno.desktop.main.portfolio.pendingtrades.PendingTradesViewModel; import haveno.desktop.main.portfolio.pendingtrades.steps.buyer.BuyerStep4View; public class SellerStep4View extends BuyerStep4View { /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialisation /////////////////////////////////////////////////////////////////////////////////////////// public SellerStep4View(PendingTradesViewModel model) { super(model); } @Override protected String getXmrTradeAmountLabel() { return Res.get("portfolio.pending.step5_seller.sold"); } @Override protected String getTraditionalTradeAmountLabel() { return Res.get("portfolio.pending.step5_seller.received"); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/portfolio/presentation/PortfolioUtil.java ================================================ /* * 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 . */ package haveno.desktop.main.portfolio.presentation; import haveno.core.offer.OfferPayload; import haveno.desktop.Navigation; import haveno.desktop.main.MainView; import haveno.desktop.main.portfolio.PortfolioView; import haveno.desktop.main.portfolio.duplicateoffer.DuplicateOfferView; public class PortfolioUtil { public static void duplicateOffer(Navigation navigation, OfferPayload offerPayload) { navigation.navigateToWithData(offerPayload, MainView.class, PortfolioView.class, DuplicateOfferView.class); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/presentation/AccountPresentation.java ================================================ /* * 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 . */ package haveno.desktop.main.presentation; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.app.DevEnv; import haveno.core.locale.Res; import haveno.core.user.DontShowAgainLookup; import haveno.core.user.Preferences; import haveno.desktop.main.overlays.popups.Popup; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.MapChangeListener; @Singleton public class AccountPresentation { public static final String ACCOUNT_NEWS = "accountNews"; private Preferences preferences; private final SimpleBooleanProperty showNotification = new SimpleBooleanProperty(false); @Inject public AccountPresentation(Preferences preferences) { this.preferences = preferences; preferences.getDontShowAgainMapAsObservable().addListener((MapChangeListener) change -> { if (change.getKey().equals(ACCOUNT_NEWS)) { showNotification.set(!change.wasAdded()); } }); } /////////////////////////////////////////////////////////////////////////////////////////// // Public /////////////////////////////////////////////////////////////////////////////////////////// public BooleanProperty getShowAccountUpdatesNotification() { return showNotification; } public void setup() { showNotification.set(preferences.showAgain(ACCOUNT_NEWS)); } public void showOneTimeAccountSigningPopup(String key, String s) { showOneTimeAccountSigningPopup(key, s, null); } public void showOneTimeAccountSigningPopup(String key, String s, String optionalParam) { if (!DevEnv.isDevMode()) { DontShowAgainLookup.dontShowAgain(ACCOUNT_NEWS, false); showNotification.set(true); DontShowAgainLookup.dontShowAgain(key, true); String message = optionalParam != null ? Res.get(s, optionalParam, Res.get("popup.accountSigning.generalInformation")) : Res.get(s, Res.get("popup.accountSigning.generalInformation")); new Popup().information(message) .show(); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/presentation/MarketPricePresentation.java ================================================ /* * 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 . */ package haveno.desktop.main.presentation; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.UserThread; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.provider.price.MarketPrice; import haveno.core.provider.price.PriceFeedService; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.components.TxIdTextField; import haveno.desktop.main.shared.PriceFeedComboBoxItem; import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import lombok.Getter; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; import org.fxmisc.easybind.monadic.MonadicBinding; @Singleton public class MarketPricePresentation { private final Preferences preferences; private final PriceFeedService priceFeedService; @Getter private final ObservableList priceFeedComboBoxItems = FXCollections.observableArrayList(); @SuppressWarnings("FieldCanBeLocal") private MonadicBinding marketPriceBinding; @SuppressWarnings({"FieldCanBeLocal", "unused"}) private Subscription priceFeedAllLoadedSubscription; private final StringProperty marketPriceCurrencyCode = new SimpleStringProperty(""); private final ObjectProperty selectedPriceFeedComboBoxItemProperty = new SimpleObjectProperty<>(); private final BooleanProperty isFiatCurrencyPriceFeedSelected = new SimpleBooleanProperty(true); private final BooleanProperty isCryptoCurrencyPriceFeedSelected = new SimpleBooleanProperty(false); private final BooleanProperty isExternallyProvidedPrice = new SimpleBooleanProperty(true); private final BooleanProperty isPriceAvailable = new SimpleBooleanProperty(false); private final IntegerProperty marketPriceUpdated = new SimpleIntegerProperty(0); private final StringProperty marketPrice = new SimpleStringProperty(Res.get("shared.na")); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public MarketPricePresentation(XmrWalletService xmrWalletService, PriceFeedService priceFeedService, Preferences preferences) { this.priceFeedService = priceFeedService; this.preferences = preferences; TxIdTextField.setPreferences(preferences); TxIdTextField.setXmrWalletService(xmrWalletService); } public void setup() { fillPriceFeedComboBoxItems(); setupMarketPriceFeed(); } public void setPriceFeedComboBoxItem(PriceFeedComboBoxItem item) { if (item != null) { Optional itemOptional = findPriceFeedComboBoxItem(priceFeedService.currencyCodeProperty().get()); if (itemOptional.isPresent()) selectedPriceFeedComboBoxItemProperty.set(itemOptional.get()); else findPriceFeedComboBoxItem(preferences.getPreferredTradeCurrency().getCode()) .ifPresent(selectedPriceFeedComboBoxItemProperty::set); priceFeedService.setCurrencyCode(item.currencyCode); } else { findPriceFeedComboBoxItem(preferences.getPreferredTradeCurrency().getCode()) .ifPresent(selectedPriceFeedComboBoxItemProperty::set); } } private void fillPriceFeedComboBoxItems() { // collect unique currency code bases List uniqueCurrencyCodeBases = preferences.getTradeCurrenciesAsObservable() .stream() .map(TradeCurrency::getCode) .map(CurrencyUtil::getCurrencyCodeBase) .distinct() .collect(Collectors.toList()); // create price feed items List currencyItems = uniqueCurrencyCodeBases .stream() .map(currencyCodeBase -> new PriceFeedComboBoxItem(currencyCodeBase)) .collect(Collectors.toList()); priceFeedComboBoxItems.setAll(currencyItems); } private void setupMarketPriceFeed() { priceFeedService.startRequestingPrices(price -> marketPrice.set(FormattingUtils.formatMarketPrice(price, priceFeedService.getCurrencyCode())), (errorMessage, throwable) -> marketPrice.set(Res.get("shared.na"))); marketPriceBinding = EasyBind.combine( marketPriceCurrencyCode, marketPrice, (currencyCode, price) -> { MarketPrice currentPrice = priceFeedService.getMarketPrice(currencyCode); String currentPriceStr = currentPrice == null ? Res.get("shared.na") : FormattingUtils.formatMarketPrice(currentPrice.getPrice(), currencyCode); return CurrencyUtil.getCurrencyPair(currencyCode) + ": " + currentPriceStr; }); marketPriceBinding.subscribe((observable, oldValue, newValue) -> { UserThread.execute(() -> { if (newValue != null && !newValue.equals(oldValue)) { setMarketPriceInItems(); String code = priceFeedService.currencyCodeProperty().get(); Optional itemOptional = findPriceFeedComboBoxItem(code); if (itemOptional.isPresent()) { itemOptional.get().setDisplayString(newValue); selectedPriceFeedComboBoxItemProperty.set(itemOptional.get()); } else { if (CurrencyUtil.isCryptoCurrency(code)) { CurrencyUtil.getCryptoCurrency(code).ifPresent(cryptoCurrency -> { preferences.addCryptoCurrency(cryptoCurrency); fillPriceFeedComboBoxItems(); }); } else { CurrencyUtil.getTraditionalCurrency(code).ifPresent(traditionalCurrency -> { preferences.addTraditionalCurrency(traditionalCurrency); fillPriceFeedComboBoxItems(); }); } } if (selectedPriceFeedComboBoxItemProperty.get() != null) selectedPriceFeedComboBoxItemProperty.get().setDisplayString(newValue); } }); }); marketPriceCurrencyCode.bind(priceFeedService.currencyCodeProperty()); priceFeedAllLoadedSubscription = EasyBind.subscribe(priceFeedService.updateCounterProperty(), updateCounter -> UserThread.execute(() -> setMarketPriceInItems())); preferences.getTradeCurrenciesAsObservable().addListener((ListChangeListener) c -> UserThread.runAfter(() -> { fillPriceFeedComboBoxItems(); setMarketPriceInItems(); }, 100, TimeUnit.MILLISECONDS)); } private Optional findPriceFeedComboBoxItem(String currencyCode) { return priceFeedComboBoxItems.stream() .filter(item -> CurrencyUtil.getCurrencyCodeBase(item.currencyCode).equals(CurrencyUtil.getCurrencyCodeBase(currencyCode))) .findAny(); } private void setMarketPriceInItems() { priceFeedComboBoxItems.forEach(item -> { String currencyCode = item.currencyCode; MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); String priceString; if (marketPrice != null && marketPrice.isPriceAvailable()) { priceString = FormattingUtils.formatMarketPrice(marketPrice.getPrice(), currencyCode); item.setPriceAvailable(true); item.setExternallyProvidedPrice(marketPrice.isExternallyProvidedPrice()); } else { priceString = Res.get("shared.na"); item.setPriceAvailable(false); } item.setDisplayString(CurrencyUtil.getCurrencyPair(currencyCode) + ": " + priceString); final String code = item.currencyCode; if (selectedPriceFeedComboBoxItemProperty.get() != null && selectedPriceFeedComboBoxItemProperty.get().currencyCode.equals(code)) { isFiatCurrencyPriceFeedSelected.set(CurrencyUtil.isTraditionalCurrency(code) && CurrencyUtil.getTraditionalCurrency(code).isPresent() && item.isPriceAvailable() && item.isExternallyProvidedPrice()); isCryptoCurrencyPriceFeedSelected.set(CurrencyUtil.isCryptoCurrency(code) && CurrencyUtil.getCryptoCurrency(code).isPresent() && item.isPriceAvailable() && item.isExternallyProvidedPrice()); isExternallyProvidedPrice.set(item.isExternallyProvidedPrice()); isPriceAvailable.set(item.isPriceAvailable()); marketPriceUpdated.set(marketPriceUpdated.get() + 1); } }); } public ObjectProperty getSelectedPriceFeedComboBoxItemProperty() { return selectedPriceFeedComboBoxItemProperty; } public BooleanProperty getIsFiatCurrencyPriceFeedSelected() { return isFiatCurrencyPriceFeedSelected; } public BooleanProperty getIsCryptoCurrencyPriceFeedSelected() { return isCryptoCurrencyPriceFeedSelected; } public BooleanProperty getIsExternallyProvidedPrice() { return isExternallyProvidedPrice; } public BooleanProperty getIsPriceAvailable() { return isPriceAvailable; } public IntegerProperty getMarketPriceUpdated() { return marketPriceUpdated; } public StringProperty getMarketPrice() { return marketPrice; } public StringProperty getMarketPrice(String currencyCode) { SimpleStringProperty marketPrice = new SimpleStringProperty(Res.get("shared.na")); MarketPrice marketPriceValue = priceFeedService.getMarketPrice(currencyCode); // Market price might not be available yet: if (marketPriceValue != null) { marketPrice.set(String.valueOf(marketPriceValue.getPrice())); } return marketPrice; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/presentation/SettingsPresentation.java ================================================ /* * 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 . */ package haveno.desktop.main.presentation; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.core.user.Preferences; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.MapChangeListener; @Singleton public class SettingsPresentation { public static final String SETTINGS_NEWS = "settingsNews"; private Preferences preferences; private final SimpleBooleanProperty showNotification = new SimpleBooleanProperty(false); @Inject public SettingsPresentation(Preferences preferences) { this.preferences = preferences; preferences.getDontShowAgainMapAsObservable().addListener((MapChangeListener) change -> { if (change.getKey().equals(SETTINGS_NEWS)) { showNotification.set(!change.wasAdded()); } }); } /////////////////////////////////////////////////////////////////////////////////////////// // Public /////////////////////////////////////////////////////////////////////////////////////////// public BooleanProperty getShowSettingsUpdatesNotification() { return showNotification; } public void setup() { showNotification.set(preferences.showAgain(SETTINGS_NEWS)); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/settings/SettingsView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/settings/SettingsView.java ================================================ /* * 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 . */ package haveno.desktop.main.settings; import com.google.inject.Inject; import haveno.core.locale.Res; import haveno.core.user.Preferences; import haveno.desktop.Navigation; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.common.view.CachingViewLoader; import haveno.desktop.common.view.FxmlView; import haveno.desktop.common.view.View; import haveno.desktop.common.view.ViewLoader; import haveno.desktop.main.MainView; import haveno.desktop.main.presentation.SettingsPresentation; import haveno.desktop.main.settings.about.AboutView; import haveno.desktop.main.settings.network.NetworkSettingsView; import haveno.desktop.main.settings.preferences.PreferencesView; import javafx.beans.value.ChangeListener; import javafx.fxml.FXML; import javafx.scene.control.ScrollPane; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; @FxmlView public class SettingsView extends ActivatableView { @FXML Tab preferencesTab, networkTab, aboutTab; private final ViewLoader viewLoader; private final Navigation navigation; private Preferences preferences; private Navigation.Listener navigationListener; private ChangeListener tabChangeListener; @Inject public SettingsView(CachingViewLoader viewLoader, Navigation navigation, Preferences preferences) { this.viewLoader = viewLoader; this.navigation = navigation; this.preferences = preferences; } @Override public void initialize() { preferencesTab.setText(Res.get("settings.tab.preferences")); networkTab.setText(Res.get("settings.tab.network")); aboutTab.setText(Res.get("settings.tab.about")); navigationListener = (viewPath, data) -> { if (viewPath.size() == 3 && viewPath.indexOf(SettingsView.class) == 1) loadView(viewPath.tip()); }; tabChangeListener = (ov, oldValue, newValue) -> { navigationToTabContent(newValue); }; } private void navigationToTabContent(Tab newValue) { if (newValue == preferencesTab) navigation.navigateTo(MainView.class, SettingsView.class, PreferencesView.class); else if (newValue == networkTab) navigation.navigateTo(MainView.class, SettingsView.class, NetworkSettingsView.class); else if (newValue == aboutTab) navigation.navigateTo(MainView.class, SettingsView.class, AboutView.class); } @Override protected void activate() { // Hide new badge if user saw this section preferences.dontShowAgain(SettingsPresentation.SETTINGS_NEWS, true); root.getSelectionModel().selectedItemProperty().addListener(tabChangeListener); navigation.addListener(navigationListener); Tab selectedItem = root.getSelectionModel().getSelectedItem(); navigationToTabContent(selectedItem); } @Override protected void deactivate() { root.getSelectionModel().selectedItemProperty().removeListener(tabChangeListener); navigation.removeListener(navigationListener); } private void loadView(Class viewClass) { final Tab tab; View view = viewLoader.load(viewClass); if (view instanceof PreferencesView) tab = preferencesTab; else if (view instanceof NetworkSettingsView) tab = networkTab; else if (view instanceof AboutView) tab = aboutTab; else throw new IllegalArgumentException("Navigation to " + viewClass + " is not supported"); if (tab.getContent() != null && tab.getContent() instanceof ScrollPane) { ((ScrollPane) tab.getContent()).setContent(view.getRoot()); } else { tab.setContent(view.getRoot()); } root.getSelectionModel().select(tab); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/settings/about/AboutView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/settings/about/AboutView.java ================================================ /* * 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 . */ package haveno.desktop.main.settings.about; import com.google.inject.Inject; import haveno.common.app.Version; import haveno.core.filter.FilterManager; import haveno.core.locale.Res; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.HyperlinkWithIcon; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addHyperlinkWithIcon; import static haveno.desktop.util.FormBuilder.addLabel; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import haveno.desktop.util.Layout; import javafx.geometry.HPos; import javafx.scene.control.Label; import javafx.scene.layout.GridPane; @FxmlView public class AboutView extends ActivatableView { private final FilterManager filterManager; private int gridRow = 0; @Inject public AboutView(FilterManager filterManager) { super(); this.filterManager = filterManager; } @Override public void initialize() { addTitledGroupBg(root, gridRow, 5, Res.get("setting.about.aboutHaveno")); Label label = addLabel(root, gridRow, Res.get("setting.about.about"), Layout.TWICE_FIRST_ROW_DISTANCE); label.setWrapText(true); GridPane.setColumnSpan(label, 2); GridPane.setHalignment(label, HPos.LEFT); HyperlinkWithIcon hyperlinkWithIcon = addHyperlinkWithIcon(root, ++gridRow, Res.get("setting.about.web"), "https://haveno.exchange"); GridPane.setColumnSpan(hyperlinkWithIcon, 2); hyperlinkWithIcon = addHyperlinkWithIcon(root, ++gridRow, Res.get("setting.about.code"), "https://github.com/haveno-dex/haveno"); GridPane.setColumnSpan(hyperlinkWithIcon, 2); hyperlinkWithIcon = addHyperlinkWithIcon(root, ++gridRow, Res.get("setting.about.agpl"), "https://github.com/haveno-dex/haveno/blob/master/LICENSE"); GridPane.setColumnSpan(hyperlinkWithIcon, 2); addTitledGroupBg(root, ++gridRow, 2, Res.get("setting.about.support"), Layout.GROUP_DISTANCE); label = addLabel(root, gridRow, Res.get("setting.about.def"), Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE); label.setWrapText(true); GridPane.setColumnSpan(label, 2); GridPane.setHalignment(label, HPos.LEFT); hyperlinkWithIcon = addHyperlinkWithIcon(root, ++gridRow, Res.get("setting.about.contribute"), "https://github.com/haveno-dex/haveno#support"); GridPane.setColumnSpan(hyperlinkWithIcon, 2); boolean isXmr = Res.getBaseCurrencyCode().equals("XMR"); addTitledGroupBg(root, ++gridRow, isXmr ? 3 : 2, Res.get("setting.about.providers"), Layout.GROUP_DISTANCE); label = addLabel(root, gridRow, Res.get(isXmr ? "setting.about.apisWithFee" : "setting.about.apis"), Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE); label.setWrapText(true); GridPane.setHalignment(label, HPos.LEFT); addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.pricesProvided"), "Haveno's pricenode (https://price.haveno.network)"); if (isXmr) addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.feeEstimation.label"), "Monero node"); String minVersion = filterManager.getDisableTradeBelowVersion() == null ? Res.get("shared.none") : filterManager.getDisableTradeBelowVersion(); addTitledGroupBg(root, ++gridRow, 3, Res.get("setting.about.versionDetails"), Layout.GROUP_DISTANCE); addCompactTopLabelTextField(root, gridRow, Res.get("setting.about.version"), Version.VERSION, Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE); addCompactTopLabelTextField(root, ++gridRow, Res.get("filterWindow.disableTradeBelowVersion"), minVersion); addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.subsystems.label"), Res.get("setting.about.subsystems.val", Version.P2P_NETWORK_VERSION, Version.getP2PMessageVersion(), Version.LOCAL_DB_VERSION, Version.TRADE_PROTOCOL_VERSION)); addTitledGroupBg(root, ++gridRow, 18, Res.get("setting.about.shortcuts"), Layout.GROUP_DISTANCE); // basics addCompactTopLabelTextField(root, gridRow, Res.get("setting.about.shortcuts.menuNav"), Res.get("setting.about.shortcuts.menuNav.value"), Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE); addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.shortcuts.close"), Res.get("setting.about.shortcuts.close.value", "q", "w")); addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.shortcuts.closePopup"), Res.get("setting.about.shortcuts.closePopup.value")); addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.shortcuts.chatSendMsg"), Res.get("setting.about.shortcuts.chatSendMsg.value")); addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.shortcuts.openDispute"), Res.get("setting.about.shortcuts.openDispute.value", Res.get("setting.about.shortcuts.ctrlOrAltOrCmd", "o"))); addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.shortcuts.walletDetails"), Res.get("setting.about.shortcuts.ctrlOrAltOrCmd", "j")); addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.shortcuts.openEmergencyXmrWalletTool"), Res.get("setting.about.shortcuts.ctrlOrAltOrCmd", "e")); addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.shortcuts.showTorLogs"), Res.get("setting.about.shortcuts.ctrlOrAltOrCmd", "t")); // special addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.shortcuts.removeStuckTrade"), Res.get("setting.about.shortcuts.removeStuckTrade.value", Res.get("setting.about.shortcuts.ctrlOrAltOrCmd", "y"))); addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.shortcuts.manualPayoutTxWindow"), Res.get("setting.about.shortcuts.ctrlOrAltOrCmd", "g")); // for arbitrators addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.shortcuts.registerArbitrator"), Res.get("setting.about.shortcuts.registerArbitrator.value", Res.get("setting.about.shortcuts.ctrlOrAltOrCmd", "n"))); addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.shortcuts.registerMediator"), Res.get("setting.about.shortcuts.registerMediator.value", Res.get("setting.about.shortcuts.ctrlOrAltOrCmd", "d"))); addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.shortcuts.openSignPaymentAccountsWindow"), Res.get("setting.about.shortcuts.openSignPaymentAccountsWindow.value", Res.get("setting.about.shortcuts.ctrlOrAltOrCmd", "s"))); // only for maintainers addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.shortcuts.sendAlertMsg"), Res.get("setting.about.shortcuts.ctrlOrAltOrCmd", "m")); addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.shortcuts.sendFilter"), Res.get("setting.about.shortcuts.ctrlOrAltOrCmd", "f")); addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.shortcuts.sendPrivateNotification"), Res.get("setting.about.shortcuts.sendPrivateNotification.value", Res.get("setting.about.shortcuts.ctrlOrAltOrCmd", "r"))); // Not added: // allTradesWithReferralId, allOffersWithReferralId -> ReferralId is not used yet // revert tx -> not tested well, high risk // debug window -> not maintained, only for devs working on trade protocol relevant } @Override public void activate() { } @Override public void deactivate() { } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/settings/network/MoneroNetworkListItem.java ================================================ /* * 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 . */ package haveno.desktop.main.settings.network; import haveno.core.locale.Res; import monero.common.MoneroRpcConnection; public class MoneroNetworkListItem { private final MoneroRpcConnection connection; private final boolean connected; public MoneroNetworkListItem(MoneroRpcConnection connection, boolean connected) { this.connection = connection; this.connected = connected; } public String getAddress() { return connection.getUri(); } public String getConnected() { return connected ? Res.get("settings.net.connected") : ""; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java ================================================ /* * 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 . */ package haveno.desktop.main.settings.network; import com.google.inject.Inject; import haveno.common.ClockWatcher; import haveno.common.UserThread; import haveno.core.api.XmrConnectionService; import haveno.core.api.XmrLocalNode; import haveno.core.filter.Filter; import haveno.core.filter.FilterManager; import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.validation.RegexValidator; import haveno.core.util.validation.RegexValidatorFactory; import haveno.core.xmr.nodes.XmrNodes; import haveno.core.xmr.setup.WalletsSetup; import haveno.desktop.app.HavenoApp; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.InputTextField; import haveno.desktop.components.TitledGroupBg; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.TorNetworkSettingsWindow; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.P2PService; import haveno.network.p2p.network.Statistic; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static javafx.beans.binding.Bindings.createStringBinding; import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.transformation.SortedList; import javafx.fxml.FXML; import javafx.geometry.Insets; import javafx.geometry.VPos; import javafx.scene.control.Label; import javafx.scene.control.RadioButton; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.TextField; import javafx.scene.control.Toggle; import javafx.scene.control.ToggleGroup; import javafx.scene.layout.GridPane; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; @FxmlView public class NetworkSettingsView extends ActivatableView { @FXML TitledGroupBg p2pHeader, btcHeader; @FXML Label useTorForXmrLabel, xmrNodesLabel, moneroNodesLabel, localhostXmrNodeInfoLabel; @FXML InputTextField xmrNodesInputTextField; @FXML TextField onionAddress, sentDataTextField, receivedDataTextField, chainHeightTextField; @FXML Label p2PPeersLabel, moneroConnectionsLabel; @FXML RadioButton useTorForXmrAfterSyncRadio, useTorForXmrOffRadio, useTorForXmrOnRadio; @FXML RadioButton useProvidedNodesRadio, useCustomNodesRadio, usePublicNodesRadio; @FXML TableView p2pPeersTableView; @FXML TableView moneroConnectionsTableView; @FXML TableColumn onionAddressColumn, connectionTypeColumn, creationDateColumn, roundTripTimeColumn, sentBytesColumn, receivedBytesColumn, peerTypeColumn; @FXML TableColumn moneroConnectionAddressColumn, moneroConnectionConnectedColumn; @FXML Label rescanOutputsLabel; @FXML AutoTooltipButton rescanOutputsButton, openTorSettingsButton; private final Preferences preferences; private final XmrNodes xmrNodes; private final FilterManager filterManager; private final XmrLocalNode xmrLocalNode; private final TorNetworkSettingsWindow torNetworkSettingsWindow; private final ClockWatcher clockWatcher; private final WalletsSetup walletsSetup; private final P2PService p2PService; private final XmrConnectionService connectionService; private final ObservableList p2pNetworkListItems = FXCollections.observableArrayList(); private final SortedList p2pSortedList = new SortedList<>(p2pNetworkListItems); private final ObservableList moneroNetworkListItems = FXCollections.observableArrayList(); private final SortedList moneroSortedList = new SortedList<>(moneroNetworkListItems); private Subscription numP2PPeersSubscription; private Subscription moneroConnectionsSubscription; private Subscription moneroBlockHeightSubscription; private Subscription nodeAddressSubscription; private ChangeListener xmrNodesInputTextFieldFocusListener; private ToggleGroup useTorForXmrToggleGroup; private ToggleGroup moneroPeersToggleGroup; private Preferences.UseTorForXmr selectedUseTorForXmr; private XmrNodes.MoneroNodesOption selectedMoneroNodesOption; private ChangeListener useTorForXmrToggleGroupListener; private ChangeListener moneroPeersToggleGroupListener; private ChangeListener filterPropertyListener; @Inject public NetworkSettingsView(WalletsSetup walletsSetup, P2PService p2PService, XmrConnectionService connectionService, Preferences preferences, XmrNodes xmrNodes, FilterManager filterManager, XmrLocalNode xmrLocalNode, TorNetworkSettingsWindow torNetworkSettingsWindow, ClockWatcher clockWatcher) { super(); this.walletsSetup = walletsSetup; this.p2PService = p2PService; this.connectionService = connectionService; this.preferences = preferences; this.xmrNodes = xmrNodes; this.filterManager = filterManager; this.xmrLocalNode = xmrLocalNode; this.torNetworkSettingsWindow = torNetworkSettingsWindow; this.clockWatcher = clockWatcher; } @Override public void initialize() { GUIUtil.applyTableStyle(p2pPeersTableView); GUIUtil.applyTableStyle(moneroConnectionsTableView); onionAddress.getStyleClass().add("label-float"); sentDataTextField.getStyleClass().add("label-float"); receivedDataTextField.getStyleClass().add("label-float"); chainHeightTextField.getStyleClass().add("label-float"); btcHeader.setText(Res.get("settings.net.xmrHeader")); p2pHeader.setText(Res.get("settings.net.p2pHeader")); onionAddress.setPromptText(Res.get("settings.net.onionAddressLabel")); xmrNodesLabel.setText(Res.get("settings.net.xmrNodesLabel")); moneroConnectionsLabel.setText(Res.get("settings.net.moneroPeersLabel")); useTorForXmrLabel.setText(Res.get("settings.net.useTorForXmrJLabel")); useTorForXmrAfterSyncRadio.setText(Res.get("settings.net.useTorForXmrAfterSyncRadio")); useTorForXmrOffRadio.setText(Res.get("settings.net.useTorForXmrOffRadio")); useTorForXmrOnRadio.setText(Res.get("settings.net.useTorForXmrOnRadio")); moneroNodesLabel.setText(Res.get("settings.net.moneroNodesLabel")); moneroConnectionAddressColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.address"))); moneroConnectionConnectedColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.connection"))); localhostXmrNodeInfoLabel.setText(Res.get("settings.net.localhostXmrNodeInfo")); useProvidedNodesRadio.setText(Res.get("settings.net.useProvidedNodesRadio")); useCustomNodesRadio.setText(Res.get("settings.net.useCustomNodesRadio")); usePublicNodesRadio.setText(Res.get("settings.net.usePublicNodesRadio")); rescanOutputsLabel.setText(Res.get("settings.net.rescanOutputsLabel")); rescanOutputsButton.updateText(Res.get("settings.net.rescanOutputsButton")); p2PPeersLabel.setText(Res.get("settings.net.p2PPeersLabel")); onionAddressColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.onionAddressColumn"))); creationDateColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.creationDateColumn"))); connectionTypeColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.connectionTypeColumn"))); sentDataTextField.setPromptText(Res.get("settings.net.sentDataLabel")); receivedDataTextField.setPromptText(Res.get("settings.net.receivedDataLabel")); chainHeightTextField.setPromptText(Res.get("settings.net.chainHeightLabel")); roundTripTimeColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.roundTripTimeColumn"))); sentBytesColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.sentBytesColumn"))); receivedBytesColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.receivedBytesColumn"))); peerTypeColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.peerTypeColumn"))); openTorSettingsButton.updateText(Res.get("settings.net.openTorSettingsButton")); // TODO: hiding button to rescan outputs until supported rescanOutputsLabel.setVisible(false); rescanOutputsButton.setVisible(false); GridPane.setMargin(moneroConnectionsLabel, new Insets(4, 0, 0, 0)); GridPane.setValignment(moneroConnectionsLabel, VPos.TOP); GridPane.setMargin(p2PPeersLabel, new Insets(4, 0, 0, 0)); GridPane.setValignment(p2PPeersLabel, VPos.TOP); moneroConnectionAddressColumn.setSortType(TableColumn.SortType.ASCENDING); moneroConnectionConnectedColumn.setSortType(TableColumn.SortType.DESCENDING); moneroConnectionsTableView.setMinHeight(180); moneroConnectionsTableView.setPrefHeight(180); moneroConnectionsTableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); moneroConnectionsTableView.setPlaceholder(new AutoTooltipLabel(Res.get("table.placeholder.noData"))); moneroConnectionsTableView.getSortOrder().add(moneroConnectionConnectedColumn); p2pPeersTableView.setMinHeight(180); p2pPeersTableView.setPrefHeight(180); p2pPeersTableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); p2pPeersTableView.setPlaceholder(new AutoTooltipLabel(Res.get("table.placeholder.noData"))); p2pPeersTableView.getSortOrder().add(creationDateColumn); creationDateColumn.setSortType(TableColumn.SortType.ASCENDING); // use tor for xmr radio buttons useTorForXmrToggleGroup = new ToggleGroup(); useTorForXmrAfterSyncRadio.setToggleGroup(useTorForXmrToggleGroup); useTorForXmrOffRadio.setToggleGroup(useTorForXmrToggleGroup); useTorForXmrOnRadio.setToggleGroup(useTorForXmrToggleGroup); useTorForXmrAfterSyncRadio.setUserData(Preferences.UseTorForXmr.AFTER_SYNC); useTorForXmrOffRadio.setUserData(Preferences.UseTorForXmr.OFF); useTorForXmrOnRadio.setUserData(Preferences.UseTorForXmr.ON); selectedUseTorForXmr = Preferences.UseTorForXmr.values()[preferences.getUseTorForXmrOrdinal()]; selectUseTorForXmrToggle(); onUseTorForXmrToggleSelected(false); useTorForXmrToggleGroupListener = (observable, oldValue, newValue) -> { if (newValue != null) { selectedUseTorForXmr = (Preferences.UseTorForXmr) newValue.getUserData(); onUseTorForXmrToggleSelected(true); } }; // monero nodes radio buttons moneroPeersToggleGroup = new ToggleGroup(); useProvidedNodesRadio.setToggleGroup(moneroPeersToggleGroup); useCustomNodesRadio.setToggleGroup(moneroPeersToggleGroup); usePublicNodesRadio.setToggleGroup(moneroPeersToggleGroup); useProvidedNodesRadio.setUserData(XmrNodes.MoneroNodesOption.PROVIDED); useCustomNodesRadio.setUserData(XmrNodes.MoneroNodesOption.CUSTOM); usePublicNodesRadio.setUserData(XmrNodes.MoneroNodesOption.PUBLIC); selectedMoneroNodesOption = XmrNodes.MoneroNodesOption.values()[preferences.getMoneroNodesOptionOrdinal()]; // In case CUSTOM is selected but no custom nodes are set or // in case PUBLIC is selected but we blocked it (B2X risk) we revert to provided nodes if ((selectedMoneroNodesOption == XmrNodes.MoneroNodesOption.CUSTOM && (preferences.getMoneroNodes() == null || preferences.getMoneroNodes().isEmpty())) || (selectedMoneroNodesOption == XmrNodes.MoneroNodesOption.PUBLIC && isPreventPublicXmrNetwork())) { selectedMoneroNodesOption = XmrNodes.MoneroNodesOption.PROVIDED; preferences.setMoneroNodesOptionOrdinal(selectedMoneroNodesOption.ordinal()); } selectMoneroPeersToggle(); onMoneroPeersToggleSelected(false); moneroPeersToggleGroupListener = (observable, oldValue, newValue) -> { if (newValue != null) { selectedMoneroNodesOption = (XmrNodes.MoneroNodesOption) newValue.getUserData(); onMoneroPeersToggleSelected(true); } }; xmrNodesInputTextField.setPromptText(Res.get("settings.net.ips", "" + HavenoUtils.getDefaultMoneroPort())); RegexValidator regexValidator = RegexValidatorFactory.addressRegexValidator(); xmrNodesInputTextField.setValidator(regexValidator); xmrNodesInputTextField.setErrorMessage(Res.get("validation.invalidAddressList")); xmrNodesInputTextFieldFocusListener = (observable, oldValue, newValue) -> { if (oldValue && !newValue && !xmrNodesInputTextField.getText().equals(preferences.getMoneroNodes()) && xmrNodesInputTextField.validate()) { preferences.setMoneroNodes(xmrNodesInputTextField.getText()); preferences.setMoneroNodesOptionOrdinal(selectedMoneroNodesOption.ordinal()); showShutDownPopup(); } }; filterPropertyListener = (observable, oldValue, newValue) -> applyFilter(); // disable radio buttons if no nodes available if (xmrNodes.getProvidedXmrNodes().isEmpty()) { useProvidedNodesRadio.setDisable(true); } usePublicNodesRadio.setDisable(isPublicNodesDisabled()); //TODO sorting needs other NetworkStatisticListItem as columns type /* creationDateColumn.setComparator((o1, o2) -> o1.statistic.getCreationDate().compareTo(o2.statistic.getCreationDate())); sentBytesColumn.setComparator((o1, o2) -> ((Integer) o1.statistic.getSentBytes()).compareTo(((Integer) o2.statistic.getSentBytes()))); receivedBytesColumn.setComparator((o1, o2) -> ((Integer) o1.statistic.getReceivedBytes()).compareTo(((Integer) o2.statistic.getReceivedBytes())));*/ } @Override public void activate() { useTorForXmrToggleGroup.selectedToggleProperty().addListener(useTorForXmrToggleGroupListener); moneroPeersToggleGroup.selectedToggleProperty().addListener(moneroPeersToggleGroupListener); if (filterManager.getFilter() != null) applyFilter(); filterManager.filterProperty().addListener(filterPropertyListener); rescanOutputsButton.setOnAction(event -> GUIUtil.rescanOutputs(preferences)); moneroConnectionsSubscription = EasyBind.subscribe(connectionService.connectionsProperty(), connections -> updateMoneroConnectionsTable()); moneroBlockHeightSubscription = EasyBind.subscribe(connectionService.chainHeightProperty(), height -> updateMoneroConnectionsTable()); nodeAddressSubscription = EasyBind.subscribe(p2PService.getNetworkNode().nodeAddressProperty(), nodeAddress -> onionAddress.setText(nodeAddress == null ? Res.get("settings.net.notKnownYet") : nodeAddress.getFullAddress())); numP2PPeersSubscription = EasyBind.subscribe(p2PService.getNumConnectedPeers(), numPeers -> updateP2PTable()); sentDataTextField.textProperty().bind(createStringBinding(() -> Res.get("settings.net.sentData", FormattingUtils.formatBytes(Statistic.totalSentBytesProperty().get()), Statistic.numTotalSentMessagesProperty().get(), Statistic.numTotalSentMessagesPerSecProperty().get()), Statistic.numTotalSentMessagesPerSecProperty())); receivedDataTextField.textProperty().bind(createStringBinding(() -> Res.get("settings.net.receivedData", FormattingUtils.formatBytes(Statistic.totalReceivedBytesProperty().get()), Statistic.numTotalReceivedMessagesProperty().get(), Statistic.numTotalReceivedMessagesPerSecProperty().get()), Statistic.numTotalReceivedMessagesPerSecProperty())); moneroSortedList.comparatorProperty().bind(moneroConnectionsTableView.comparatorProperty()); moneroConnectionsTableView.setItems(moneroSortedList); p2pSortedList.comparatorProperty().bind(p2pPeersTableView.comparatorProperty()); p2pPeersTableView.setItems(p2pSortedList); xmrNodesInputTextField.setText(preferences.getMoneroNodes()); xmrNodesInputTextField.focusedProperty().addListener(xmrNodesInputTextFieldFocusListener); openTorSettingsButton.setOnAction(e -> torNetworkSettingsWindow.show()); } @Override public void deactivate() { useTorForXmrToggleGroup.selectedToggleProperty().removeListener(useTorForXmrToggleGroupListener); moneroPeersToggleGroup.selectedToggleProperty().removeListener(moneroPeersToggleGroupListener); filterManager.filterProperty().removeListener(filterPropertyListener); if (nodeAddressSubscription != null) nodeAddressSubscription.unsubscribe(); if (moneroConnectionsSubscription != null) moneroConnectionsSubscription.unsubscribe(); if (moneroBlockHeightSubscription != null) moneroBlockHeightSubscription.unsubscribe(); if (numP2PPeersSubscription != null) numP2PPeersSubscription.unsubscribe(); sentDataTextField.textProperty().unbind(); receivedDataTextField.textProperty().unbind(); moneroSortedList.comparatorProperty().unbind(); p2pSortedList.comparatorProperty().unbind(); p2pPeersTableView.getItems().forEach(P2pNetworkListItem::cleanup); xmrNodesInputTextField.focusedProperty().removeListener(xmrNodesInputTextFieldFocusListener); openTorSettingsButton.setOnAction(null); } private boolean isPreventPublicXmrNetwork() { return filterManager.getFilter() != null && filterManager.getFilter().isPreventPublicXmrNetwork(); } private void selectUseTorForXmrToggle() { switch (selectedUseTorForXmr) { case OFF: useTorForXmrToggleGroup.selectToggle(useTorForXmrOffRadio); break; case ON: useTorForXmrToggleGroup.selectToggle(useTorForXmrOnRadio); break; default: case AFTER_SYNC: useTorForXmrToggleGroup.selectToggle(useTorForXmrAfterSyncRadio); break; } } private void selectMoneroPeersToggle() { switch (selectedMoneroNodesOption) { case CUSTOM: moneroPeersToggleGroup.selectToggle(useCustomNodesRadio); break; case PUBLIC: moneroPeersToggleGroup.selectToggle(usePublicNodesRadio); break; default: case PROVIDED: moneroPeersToggleGroup.selectToggle(useProvidedNodesRadio); break; } } private void showShutDownPopup() { new Popup() .information(Res.get("settings.net.needRestart")) .closeButtonText(Res.get("shared.cancel")) .useShutDownButton() .show(); } private void onUseTorForXmrToggleSelected(boolean calledFromUser) { Preferences.UseTorForXmr currentUseTorForXmr = Preferences.UseTorForXmr.values()[preferences.getUseTorForXmrOrdinal()]; if (currentUseTorForXmr != selectedUseTorForXmr) { if (calledFromUser) { new Popup().information(Res.get("settings.net.needRestart")) .actionButtonText(Res.get("shared.applyAndShutDown")) .onAction(() -> { preferences.setUseTorForXmrOrdinal(selectedUseTorForXmr.ordinal()); UserThread.runAfter(HavenoApp.getShutDownHandler(), 500, TimeUnit.MILLISECONDS); }) .closeButtonText(Res.get("shared.cancel")) .onClose(() -> { selectedUseTorForXmr = currentUseTorForXmr; selectUseTorForXmrToggle(); }) .show(); } } } private void onMoneroPeersToggleSelected(boolean calledFromUser) { usePublicNodesRadio.setDisable(isPublicNodesDisabled()); XmrNodes.MoneroNodesOption currentMoneroNodesOption = XmrNodes.MoneroNodesOption.values()[preferences.getMoneroNodesOptionOrdinal()]; switch (selectedMoneroNodesOption) { case CUSTOM: xmrNodesInputTextField.setDisable(false); xmrNodesLabel.setDisable(false); if (!xmrNodesInputTextField.getText().isEmpty() && xmrNodesInputTextField.validate() && currentMoneroNodesOption != XmrNodes.MoneroNodesOption.CUSTOM) { preferences.setMoneroNodesOptionOrdinal(selectedMoneroNodesOption.ordinal()); if (calledFromUser) { if (isPreventPublicXmrNetwork()) { new Popup().warning(Res.get("settings.net.warn.useCustomNodes.B2XWarning")) .onAction(() -> UserThread.runAfter(this::showShutDownPopup, 300, TimeUnit.MILLISECONDS)).show(); } else { showShutDownPopup(); } } } break; case PUBLIC: xmrNodesInputTextField.setDisable(true); xmrNodesLabel.setDisable(true); if (currentMoneroNodesOption != XmrNodes.MoneroNodesOption.PUBLIC) { preferences.setMoneroNodesOptionOrdinal(selectedMoneroNodesOption.ordinal()); if (calledFromUser) { new Popup() .warning(Res.get("settings.net.warn.usePublicNodes")) .actionButtonText(Res.get("settings.net.warn.usePublicNodes.useProvided")) .onAction(() -> UserThread.runAfter(() -> { selectedMoneroNodesOption = XmrNodes.MoneroNodesOption.PROVIDED; preferences.setMoneroNodesOptionOrdinal(selectedMoneroNodesOption.ordinal()); selectMoneroPeersToggle(); onMoneroPeersToggleSelected(false); }, 300, TimeUnit.MILLISECONDS)) .closeButtonText(Res.get("settings.net.warn.usePublicNodes.usePublic")) .onClose(() -> UserThread.runAfter(this::showShutDownPopup, 300, TimeUnit.MILLISECONDS)) .show(); } } break; default: case PROVIDED: xmrNodesInputTextField.setDisable(true); xmrNodesLabel.setDisable(true); if (currentMoneroNodesOption != XmrNodes.MoneroNodesOption.PROVIDED) { preferences.setMoneroNodesOptionOrdinal(selectedMoneroNodesOption.ordinal()); if (calledFromUser) { showShutDownPopup(); } } break; } } private void applyFilter() { // prevent public xmr network final boolean preventPublicXmrNetwork = isPreventPublicXmrNetwork(); usePublicNodesRadio.setDisable(isPublicNodesDisabled()); if (preventPublicXmrNetwork && selectedMoneroNodesOption == XmrNodes.MoneroNodesOption.PUBLIC) { selectedMoneroNodesOption = XmrNodes.MoneroNodesOption.PROVIDED; preferences.setMoneroNodesOptionOrdinal(selectedMoneroNodesOption.ordinal()); selectMoneroPeersToggle(); onMoneroPeersToggleSelected(false); } } private boolean isPublicNodesDisabled() { return xmrNodes.getPublicXmrNodes().isEmpty() || isPreventPublicXmrNetwork(); } private void updateP2PTable() { UserThread.execute(() -> { if (connectionService.isShutDownStarted()) return; // ignore if shutting down p2pPeersTableView.getItems().forEach(P2pNetworkListItem::cleanup); p2pNetworkListItems.clear(); p2pNetworkListItems.setAll(p2PService.getNetworkNode().getAllConnections().stream() .map(connection -> new P2pNetworkListItem(connection, clockWatcher)) .collect(Collectors.toList())); }); } private void updateMoneroConnectionsTable() { UserThread.execute(() -> { if (connectionService.isShutDownStarted()) return; // ignore if shutting down moneroNetworkListItems.clear(); moneroNetworkListItems.setAll(connectionService.getConnections().stream() .map(connection -> new MoneroNetworkListItem(connection, connection == connectionService.getConnection() && Boolean.TRUE.equals(connectionService.isConnected()))) .collect(Collectors.toList())); updateChainHeightTextField(connectionService.chainHeightProperty().get()); }); } private void updateChainHeightTextField(Number chainHeight) { chainHeightTextField.textProperty().setValue(Res.get("settings.net.chainHeight", chainHeight)); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/settings/network/P2pNetworkListItem.java ================================================ /* * 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 . */ package haveno.desktop.main.settings.network; import haveno.common.ClockWatcher; import haveno.core.locale.Res; import haveno.core.util.FormattingUtils; import haveno.desktop.util.DisplayUtils; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.ConnectionState; import haveno.network.p2p.network.OutboundConnection; import haveno.network.p2p.network.PeerType; import haveno.network.p2p.network.Statistic; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import org.apache.commons.lang3.time.DurationFormatUtils; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class P2pNetworkListItem { private static final Logger log = LoggerFactory.getLogger(P2pNetworkListItem.class); private final Statistic statistic; private final Connection connection; private final Subscription sentBytesSubscription, receivedBytesSubscription, onionAddressSubscription, roundTripTimeSubscription; private final ClockWatcher clockWatcher; private final StringProperty lastActivity = new SimpleStringProperty(); private final StringProperty sentBytes = new SimpleStringProperty(); private final StringProperty receivedBytes = new SimpleStringProperty(); private final StringProperty peerType = new SimpleStringProperty(); private final StringProperty connectionType = new SimpleStringProperty(); private final StringProperty roundTripTime = new SimpleStringProperty(); private final StringProperty onionAddress = new SimpleStringProperty(); private final ClockWatcher.Listener listener; P2pNetworkListItem(Connection connection, ClockWatcher clockWatcher) { this.connection = connection; this.clockWatcher = clockWatcher; this.statistic = connection.getStatistic(); sentBytesSubscription = EasyBind.subscribe(statistic.sentBytesProperty(), e -> sentBytes.set(FormattingUtils.formatBytes((long) e))); receivedBytesSubscription = EasyBind.subscribe(statistic.receivedBytesProperty(), e -> receivedBytes.set(FormattingUtils.formatBytes((long) e))); onionAddressSubscription = EasyBind.subscribe(connection.getPeersNodeAddressProperty(), nodeAddress -> onionAddress.set(nodeAddress != null ? nodeAddress.getFullAddress() : Res.get("settings.net.notKnownYet"))); roundTripTimeSubscription = EasyBind.subscribe(statistic.roundTripTimeProperty(), roundTripTime -> this.roundTripTime.set((int) roundTripTime == 0 ? "-" : roundTripTime + " ms")); listener = new ClockWatcher.Listener() { @Override public void onSecondTick() { onLastActivityChanged(statistic.getLastActivityTimestamp()); updatePeerType(); updateConnectionType(); } @Override public void onMinuteTick() { } }; clockWatcher.addListener(listener); onLastActivityChanged(statistic.getLastActivityTimestamp()); updatePeerType(); updateConnectionType(); } private void onLastActivityChanged(long timeStamp) { // TODO // Got one case where System.currentTimeMillis() - timeStamp resulted in a negative value, // probably caused by a threading issue. Protect it with Math.abs for a quick fix... lastActivity.set(DurationFormatUtils.formatDuration(Math.abs(System.currentTimeMillis() - timeStamp), "mm:ss.SSS")); } public void cleanup() { sentBytesSubscription.unsubscribe(); receivedBytesSubscription.unsubscribe(); onionAddressSubscription.unsubscribe(); roundTripTimeSubscription.unsubscribe(); clockWatcher.removeListener(listener); } public void updateConnectionType() { connectionType.set(connection instanceof OutboundConnection ? Res.get("settings.net.outbound") : Res.get("settings.net.inbound")); } public void updatePeerType() { ConnectionState connectionState = connection.getConnectionState(); if (connectionState.getPeerType() == PeerType.DIRECT_MSG_PEER) { peerType.set(Res.get("settings.net.directPeer")); } else { String peerOrSeed = connectionState.isSeedNode() ? Res.get("settings.net.seedNode") : Res.get("settings.net.peer"); if (connectionState.getPeerType() == PeerType.INITIAL_DATA_EXCHANGE) { peerType.set(Res.get("settings.net.initialDataExchange", peerOrSeed)); } else { peerType.set(peerOrSeed); } } } public String getCreationDate() { return DisplayUtils.formatDateTime(statistic.getCreationDate()); } public String getOnionAddress() { return onionAddress.get(); } public StringProperty onionAddressProperty() { return onionAddress; } public String getConnectionType() { return connectionType.get(); } public StringProperty connectionTypeProperty() { return connectionType; } public String getPeerType() { return peerType.get(); } public StringProperty peerTypeProperty() { return peerType; } public String getLastActivity() { return lastActivity.get(); } public StringProperty lastActivityProperty() { return lastActivity; } public String getSentBytes() { return sentBytes.get(); } public StringProperty sentBytesProperty() { return sentBytes; } public String getReceivedBytes() { return receivedBytes.get(); } public StringProperty receivedBytesProperty() { return receivedBytes; } public String getRoundTripTime() { return roundTripTime.get(); } public StringProperty roundTripTimeProperty() { return roundTripTime; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/settings/preferences/PreferencesView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/settings/preferences/PreferencesView.java ================================================ /* * 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 . */ package haveno.desktop.main.settings.preferences; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.config.Config; import haveno.common.util.Tuple2; import haveno.common.util.Tuple3; import haveno.common.util.Utilities; import haveno.core.filter.Filter; import haveno.core.filter.FilterManager; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.LanguageUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.locale.TraditionalCurrency; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.validation.XmrValidator; import haveno.core.trade.HavenoUtils; import haveno.core.user.DontShowAgainLookup; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.util.FormattingUtils; import haveno.core.util.ParsingUtils; import haveno.core.util.validation.IntegerValidator; import haveno.core.util.validation.RegexValidator; import haveno.core.util.validation.RegexValidatorFactory; import haveno.desktop.common.view.ActivatableViewAndModel; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.InputTextField; import haveno.desktop.components.PasswordTextField; import haveno.desktop.components.TitledGroupBg; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.EditCustomExplorerWindow; import static haveno.desktop.util.FormBuilder.addButton; import static haveno.desktop.util.FormBuilder.addComboBox; import static haveno.desktop.util.FormBuilder.addInputTextField; import static haveno.desktop.util.FormBuilder.addSlideToggleButton; import static haveno.desktop.util.FormBuilder.addTextFieldWithEditButton; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import static haveno.desktop.util.FormBuilder.addTopLabelListView; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.ImageUtil; import haveno.desktop.util.Layout; import java.io.File; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Orientation; import javafx.geometry.VPos; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.Separator; import javafx.scene.control.TextField; import javafx.scene.control.ToggleButton; import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.util.Callback; import javafx.util.StringConverter; import org.apache.commons.lang3.StringUtils; @FxmlView public class PreferencesView extends ActivatableViewAndModel { private final User user; private TextField xmrExplorerTextField; private ComboBox userLanguageComboBox; private ComboBox userCountryComboBox; private ComboBox preferredTradeCurrencyComboBox; private ToggleButton showOwnOffersInOfferBook, useAnimations, useDarkMode, sortMarketCurrenciesNumerically, avoidStandbyMode, useSoundForNotifications, useCustomFee, autoConfirmXmrToggle, hideNonAccountPaymentMethodsToggle, denyApiTakerToggle, notifyOnPreReleaseToggle; private int gridRow = 0; private int displayCurrenciesGridRowIndex = 0; private InputTextField ignoreTradersListInputTextField, autoConfRequiredConfirmationsTf, autoConfServiceAddressTf, autoConfTradeLimitTf, clearDataAfterDaysInputTextField, rpcUserTextField, blockNotifyPortTextField; private PasswordTextField rpcPwTextField; private ChangeListener autoConfServiceAddressFocusOutListener, autoConfRequiredConfirmationsFocusOutListener; private final Preferences preferences; //private final ReferralIdService referralIdService; private final FilterManager filterManager; private final File storageDir; private ListView traditionalCurrenciesListView; private ComboBox traditionalCurrenciesComboBox; private ListView cryptoCurrenciesListView; private ComboBox cryptoCurrenciesComboBox; private Button resetDontShowAgainButton, editCustomBtcExplorer; private ObservableList languageCodes; private ObservableList countries; private ObservableList traditionalCurrencies; private ObservableList allTraditionalCurrencies; private ObservableList cryptoCurrencies; private ObservableList allCryptoCurrencies; private ObservableList tradeCurrencies; private InputTextField deviationInputTextField; private ChangeListener deviationListener, ignoreTradersListListener, rpcUserListener, rpcPwListener, blockNotifyPortListener, clearDataAfterDaysListener, autoConfTradeLimitListener, autoConfServiceAddressListener; private ChangeListener deviationFocusedListener; private final boolean displayStandbyModeFeature; private ChangeListener filterChangeListener; private boolean hideXmrAutoConf = true; // TODO: remove xmr auto conf or use as a model for other blockchains /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, initialisation /////////////////////////////////////////////////////////////////////////////////////////// @Inject public PreferencesView(PreferencesViewModel model, Preferences preferences, FilterManager filterManager, Config config, User user, @Named(Config.STORAGE_DIR) File storageDir) { super(model); this.user = user; this.preferences = preferences; this.filterManager = filterManager; this.storageDir = storageDir; this.displayStandbyModeFeature = Utilities.isLinux() || Utilities.isOSX() || Utilities.isWindows(); } @Override public void initialize() { languageCodes = FXCollections.observableArrayList(LanguageUtil.getUserLanguageCodes()); countries = FXCollections.observableArrayList(CountryUtil.getAllCountries()); traditionalCurrencies = preferences.getTraditionalCurrenciesAsObservable(); cryptoCurrencies = preferences.getCryptoCurrenciesAsObservable(); tradeCurrencies = preferences.getTradeCurrenciesAsObservable(); allTraditionalCurrencies = FXCollections.observableArrayList(CurrencyUtil.getAllSortedTraditionalCurrencies()); allTraditionalCurrencies.removeAll(traditionalCurrencies); initializeGeneralOptions(); initializeDisplayOptions(); initializeSeparator(); initializeAutoConfirmOptions(); initializeDisplayCurrencies(); } @Override protected void activate() { String key = "sensitiveDataRemovalInfo"; if (DontShowAgainLookup.showAgain(key) && (preferences.getClearDataAfterDays() == 0 || preferences.getClearDataAfterDays() == Preferences.CLEAR_DATA_AFTER_DAYS_DISABLED)) { // existing users must agree to new feature new Popup() .headLine(Res.get("setting.info.headline")) .backgroundInfo(Res.get("settings.preferences.sensitiveDataRemoval.msg")) .actionButtonText(Res.get("shared.iUnderstand")) .onAction(() -> { DontShowAgainLookup.dontShowAgain(key, true); // user has acknowledged, enable the feature with a reasonable default value preferences.setClearDataAfterDays(Preferences.CLEAR_DATA_AFTER_DAYS_DEFAULT); clearDataAfterDaysInputTextField.setText(String.valueOf(preferences.getClearDataAfterDays())); }) .closeButtonText(Res.get("shared.cancel")) .show(); } // We want to have it updated in case an asset got removed allCryptoCurrencies = FXCollections.observableArrayList(CurrencyUtil.getActiveSortedCryptoCurrencies(filterManager)); allCryptoCurrencies.removeAll(cryptoCurrencies); activateGeneralOptions(); activateDisplayCurrencies(); activateDisplayPreferences(); activateAutoConfirmPreferences(); } @Override protected void deactivate() { deactivateGeneralOptions(); deactivateDisplayCurrencies(); deactivateDisplayPreferences(); deactivateAutoConfirmPreferences(); } /////////////////////////////////////////////////////////////////////////////////////////// // Initialize /////////////////////////////////////////////////////////////////////////////////////////// private void initializeGeneralOptions() { int titledGroupBgRowSpan = displayStandbyModeFeature ? 8 : 7; TitledGroupBg titledGroupBg = addTitledGroupBg(root, gridRow, titledGroupBgRowSpan, Res.get("setting.preferences.general")); GridPane.setColumnSpan(titledGroupBg, 1); userLanguageComboBox = addComboBox(root, gridRow, Res.get("shared.language"), Layout.FIRST_ROW_DISTANCE); userCountryComboBox = addComboBox(root, ++gridRow, Res.get("shared.country")); userCountryComboBox.setButtonCell(GUIUtil.getComboBoxButtonCell(Res.get("shared.country"), userCountryComboBox, false)); Tuple2 xmrExp = addTextFieldWithEditButton(root, ++gridRow, Res.get("setting.preferences.explorer")); xmrExplorerTextField = xmrExp.first; editCustomBtcExplorer = xmrExp.second; // deviation deviationInputTextField = addInputTextField(root, ++gridRow, Res.get("setting.preferences.deviation")); deviationListener = (observable, oldValue, newValue) -> { try { double value = ParsingUtils.parsePercentStringToDouble(newValue); final double maxDeviation = 0.5; if (value <= maxDeviation) { preferences.setMaxPriceDistanceInPercent(value); } else { new Popup().warning(Res.get("setting.preferences.deviationToLarge", maxDeviation * 100)).show(); UserThread.runAfter(() -> deviationInputTextField.setText(FormattingUtils.formatToPercentWithSymbol(preferences.getMaxPriceDistanceInPercent())), 100, TimeUnit.MILLISECONDS); } } catch (NumberFormatException t) { log.error("Exception at parseDouble deviation: " + t.toString()); UserThread.runAfter(() -> deviationInputTextField.setText(FormattingUtils.formatToPercentWithSymbol(preferences.getMaxPriceDistanceInPercent())), 100, TimeUnit.MILLISECONDS); } }; deviationFocusedListener = (observable1, oldValue1, newValue1) -> { if (oldValue1 && !newValue1) UserThread.runAfter(() -> deviationInputTextField.setText(FormattingUtils.formatToPercentWithSymbol(preferences.getMaxPriceDistanceInPercent())), 100, TimeUnit.MILLISECONDS); }; // ignoreTraders ignoreTradersListInputTextField = addInputTextField(root, ++gridRow, Res.get("setting.preferences.ignorePeers")); RegexValidator regexValidator = RegexValidatorFactory.addressRegexValidator(); ignoreTradersListInputTextField.setValidator(regexValidator); ignoreTradersListInputTextField.setErrorMessage(Res.get("validation.invalidAddressList")); ignoreTradersListListener = (observable, oldValue, newValue) -> { if (regexValidator.validate(newValue).isValid && !newValue.equals(oldValue)) { preferences.setIgnoreTradersList(Arrays.asList(StringUtils.deleteWhitespace(newValue).split(","))); } }; // clearDataAfterDays clearDataAfterDaysInputTextField = addInputTextField(root, ++gridRow, Res.get("setting.preferences.clearDataAfterDays")); IntegerValidator clearDataAfterDaysValidator = new IntegerValidator(); clearDataAfterDaysValidator.setMinValue(1); clearDataAfterDaysValidator.setMaxValue(Preferences.CLEAR_DATA_AFTER_DAYS_DISABLED); clearDataAfterDaysInputTextField.setValidator(clearDataAfterDaysValidator); clearDataAfterDaysListener = (observable, oldValue, newValue) -> { try { int value = Integer.parseInt(newValue); if (!newValue.equals(oldValue)) { preferences.setClearDataAfterDays(value); } } catch (Throwable ignore) { } }; if (displayStandbyModeFeature) { // AvoidStandbyModeService feature works only on OSX & Windows avoidStandbyMode = addSlideToggleButton(root, ++gridRow, Res.get("setting.preferences.avoidStandbyMode")); } useSoundForNotifications = addSlideToggleButton(root, ++gridRow, Res.get("setting.preferences.useSoundForNotifications"), -5); // TODO: why must negative value be used to place toggle consistently? } private void initializeSeparator() { final Separator separator = new Separator(Orientation.VERTICAL); separator.setPadding(new Insets(0, 10, 0, 10)); GridPane.setColumnIndex(separator, 1); GridPane.setHalignment(separator, HPos.CENTER); GridPane.setRowIndex(separator, 0); GridPane.setRowSpan(separator, GridPane.REMAINING); root.getChildren().add(separator); } private void initializeDisplayCurrencies() { TitledGroupBg titledGroupBg = addTitledGroupBg(root, displayCurrenciesGridRowIndex, 8, Res.get("setting.preferences.currenciesInList"), hideXmrAutoConf ? 0.0 :Layout.GROUP_DISTANCE); GridPane.setColumnIndex(titledGroupBg, 2); GridPane.setColumnSpan(titledGroupBg, 2); preferredTradeCurrencyComboBox = addComboBox(root, displayCurrenciesGridRowIndex++, Res.get("setting.preferences.prefCurrency"), Layout.FIRST_ROW_DISTANCE); GridPane.setColumnIndex(preferredTradeCurrencyComboBox, 2); preferredTradeCurrencyComboBox.setConverter(new StringConverter<>() { @Override public String toString(TradeCurrency object) { return object.getName() + " (" + object.getCode() + ")"; } @Override public TradeCurrency fromString(String string) { return null; } }); preferredTradeCurrencyComboBox.setButtonCell(GUIUtil.getTradeCurrencyButtonCell("", "", FXCollections.emptyObservableMap())); preferredTradeCurrencyComboBox.setCellFactory(GUIUtil.getTradeCurrencyCellFactory("", "", FXCollections.emptyObservableMap())); Tuple3, VBox> traditionalTuple = addTopLabelListView(root, displayCurrenciesGridRowIndex, Res.get("setting.preferences.displayTraditional")); int listRowSpan = 6; GridPane.setColumnIndex(traditionalTuple.third, 2); GridPane.setRowSpan(traditionalTuple.third, listRowSpan); GridPane.setValignment(traditionalTuple.third, VPos.TOP); GridPane.setMargin(traditionalTuple.third, new Insets(10, 0, 0, 0)); traditionalCurrenciesListView = traditionalTuple.second; traditionalCurrenciesListView.setMinHeight(9 * Layout.LIST_ROW_HEIGHT + 2); traditionalCurrenciesListView.setPrefHeight(10 * Layout.LIST_ROW_HEIGHT + 2); Label placeholder = new AutoTooltipLabel(Res.get("setting.preferences.noTraditional")); placeholder.setWrapText(true); traditionalCurrenciesListView.setPlaceholder(placeholder); traditionalCurrenciesListView.setCellFactory(new Callback<>() { @Override public ListCell call(ListView list) { return new ListCell<>() { final Label label = new AutoTooltipLabel(); final ImageView icon = ImageUtil.getImageViewById(ImageUtil.REMOVE_ICON); final Button removeButton = new AutoTooltipButton("", icon); final AnchorPane pane = new AnchorPane(label, removeButton); { label.setLayoutY(5); removeButton.setId("icon-button"); AnchorPane.setRightAnchor(removeButton, -30d); } @Override public void updateItem(final TraditionalCurrency item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { label.setText(item.getNameAndCode()); removeButton.setOnAction(e -> { if (item.equals(preferences.getPreferredTradeCurrency())) { new Popup().warning(Res.get("setting.preferences.cannotRemovePrefCurrency")).show(); } else { preferences.removeTraditionalCurrency(item); if (!allTraditionalCurrencies.contains(item)) { allTraditionalCurrencies.add(item); allTraditionalCurrencies.sort(TradeCurrency::compareTo); } } }); setGraphic(pane); } else { setGraphic(null); removeButton.setOnAction(null); } } }; } }); Tuple3, VBox> cryptoCurrenciesTuple = addTopLabelListView(root, displayCurrenciesGridRowIndex, Res.get("setting.preferences.displayCryptos")); GridPane.setColumnIndex(cryptoCurrenciesTuple.third, 3); GridPane.setRowSpan(cryptoCurrenciesTuple.third, listRowSpan); GridPane.setValignment(cryptoCurrenciesTuple.third, VPos.TOP); GridPane.setMargin(cryptoCurrenciesTuple.third, new Insets(0, 0, 0, 20)); cryptoCurrenciesListView = cryptoCurrenciesTuple.second; cryptoCurrenciesListView.setMinHeight(9 * Layout.LIST_ROW_HEIGHT + 2); cryptoCurrenciesListView.setPrefHeight(10 * Layout.LIST_ROW_HEIGHT + 2); placeholder = new AutoTooltipLabel(Res.get("setting.preferences.noCryptos")); placeholder.setWrapText(true); cryptoCurrenciesListView.setPlaceholder(placeholder); cryptoCurrenciesListView.setCellFactory(new Callback<>() { @Override public ListCell call(ListView list) { return new ListCell<>() { final Label label = new AutoTooltipLabel(); final ImageView icon = ImageUtil.getImageViewById(ImageUtil.REMOVE_ICON); final Button removeButton = new AutoTooltipButton("", icon); final AnchorPane pane = new AnchorPane(label, removeButton); { label.setLayoutY(5); removeButton.setId("icon-button"); AnchorPane.setRightAnchor(removeButton, -30d); } @Override public void updateItem(final CryptoCurrency item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { label.setText(item.getNameAndCode()); removeButton.setOnAction(e -> { if (item.equals(preferences.getPreferredTradeCurrency())) { new Popup().warning(Res.get("setting.preferences.cannotRemovePrefCurrency")).show(); } else { preferences.removeCryptoCurrency(item); if (!allCryptoCurrencies.contains(item)) { allCryptoCurrencies.add(item); allCryptoCurrencies.sort(TradeCurrency::compareTo); } } }); setGraphic(pane); } else { setGraphic(null); removeButton.setOnAction(null); } } }; } }); traditionalCurrenciesComboBox = addComboBox(root, displayCurrenciesGridRowIndex + listRowSpan); GridPane.setColumnIndex(traditionalCurrenciesComboBox, 2); GridPane.setValignment(traditionalCurrenciesComboBox, VPos.TOP); traditionalCurrenciesComboBox.setPromptText(Res.get("setting.preferences.addTraditional")); traditionalCurrenciesComboBox.setButtonCell(new ListCell<>() { @Override protected void updateItem(final TraditionalCurrency item, boolean empty) { super.updateItem(item, empty); this.setVisible(item != null || !empty); if (empty || item == null) { setText(Res.get("setting.preferences.addTraditional")); } else { setText(item.getNameAndCode()); } } }); traditionalCurrenciesComboBox.setConverter(new StringConverter<>() { @Override public String toString(TraditionalCurrency tradeCurrency) { return tradeCurrency.getNameAndCode(); } @Override public TraditionalCurrency fromString(String s) { return null; } }); cryptoCurrenciesComboBox = addComboBox(root, displayCurrenciesGridRowIndex + listRowSpan); GridPane.setColumnIndex(cryptoCurrenciesComboBox, 3); GridPane.setValignment(cryptoCurrenciesComboBox, VPos.TOP); GridPane.setMargin(cryptoCurrenciesComboBox, new Insets(Layout.FLOATING_LABEL_DISTANCE, 0, 0, 20)); cryptoCurrenciesComboBox.setPromptText(Res.get("setting.preferences.addCrypto")); cryptoCurrenciesComboBox.setButtonCell(new ListCell<>() { @Override protected void updateItem(final CryptoCurrency item, boolean empty) { super.updateItem(item, empty); this.setVisible(item != null || !empty); if (empty || item == null) { setText(Res.get("setting.preferences.addCrypto")); } else { setText(item.getNameAndCode()); } } }); cryptoCurrenciesComboBox.setConverter(new StringConverter<>() { @Override public String toString(CryptoCurrency tradeCurrency) { return tradeCurrency.getNameAndCode(); } @Override public CryptoCurrency fromString(String s) { return null; } }); displayCurrenciesGridRowIndex += listRowSpan; } private void initializeDisplayOptions() { TitledGroupBg titledGroupBg = addTitledGroupBg(root, ++gridRow, 7, Res.get("setting.preferences.displayOptions"), Layout.GROUP_DISTANCE); GridPane.setColumnSpan(titledGroupBg, 1); showOwnOffersInOfferBook = addSlideToggleButton(root, gridRow, Res.get("setting.preferences.showOwnOffers"), Layout.FIRST_ROW_AND_GROUP_DISTANCE); useAnimations = addSlideToggleButton(root, ++gridRow, Res.get("setting.preferences.useAnimations")); useDarkMode = addSlideToggleButton(root, ++gridRow, Res.get("setting.preferences.useDarkMode")); sortMarketCurrenciesNumerically = addSlideToggleButton(root, ++gridRow, Res.get("setting.preferences.sortWithNumOffers")); hideNonAccountPaymentMethodsToggle = addSlideToggleButton(root, ++gridRow, Res.get("setting.preferences.onlyShowPaymentMethodsFromAccount")); //denyApiTakerToggle = addSlideToggleButton(root, ++gridRow, Res.get("setting.preferences.denyApiTaker")); // TODO: re-enable? //notifyOnPreReleaseToggle = addSlideToggleButton(root, ++gridRow, Res.get("setting.preferences.notifyOnPreRelease")); resetDontShowAgainButton = addButton(root, ++gridRow, Res.get("setting.preferences.resetAllFlags"), 0); resetDontShowAgainButton.getStyleClass().add("compact-button"); resetDontShowAgainButton.setMaxWidth(Double.MAX_VALUE); GridPane.setHgrow(resetDontShowAgainButton, Priority.ALWAYS); GridPane.setColumnIndex(resetDontShowAgainButton, 0); } private void initializeAutoConfirmOptions() { GridPane autoConfirmGridPane = new GridPane(); GridPane.setHgrow(autoConfirmGridPane, Priority.ALWAYS); if (!hideXmrAutoConf) root.add(autoConfirmGridPane, 2, displayCurrenciesGridRowIndex, 2, 10); addTitledGroupBg(autoConfirmGridPane, 0, 4, Res.get("setting.preferences.autoConfirmXMR"), 0); int localRowIndex = 0; autoConfirmXmrToggle = addSlideToggleButton(autoConfirmGridPane, localRowIndex, Res.get("setting.preferences.autoConfirmEnabled"), Layout.FIRST_ROW_DISTANCE); autoConfRequiredConfirmationsTf = addInputTextField(autoConfirmGridPane, ++localRowIndex, Res.get("setting.preferences.autoConfirmRequiredConfirmations")); autoConfRequiredConfirmationsTf.setValidator(new IntegerValidator(1, DevEnv.isDevMode() ? 100000000 : 1000)); autoConfTradeLimitTf = addInputTextField(autoConfirmGridPane, ++localRowIndex, Res.get("setting.preferences.autoConfirmMaxTradeSize")); autoConfTradeLimitTf.setValidator(new XmrValidator()); autoConfServiceAddressTf = addInputTextField(autoConfirmGridPane, ++localRowIndex, Res.get("setting.preferences.autoConfirmServiceAddresses")); GridPane.setHgrow(autoConfServiceAddressTf, Priority.ALWAYS); if (!hideXmrAutoConf) displayCurrenciesGridRowIndex += 4; autoConfServiceAddressListener = (observable, oldValue, newValue) -> { if (!newValue.equals(oldValue)) { RegexValidator onionRegex = RegexValidatorFactory.onionAddressRegexValidator(); RegexValidator localhostRegex = RegexValidatorFactory.localhostAddressRegexValidator(); RegexValidator localnetRegex = RegexValidatorFactory.localnetAddressRegexValidator(); List serviceAddressesRaw = Arrays.asList(StringUtils.deleteWhitespace(newValue).split(",")); // revert to default service providers when user empties the list if (serviceAddressesRaw.size() == 1 && serviceAddressesRaw.get(0).isEmpty()) { serviceAddressesRaw = preferences.getDefaultXmrTxProofServices(); } // we must always communicate with XMR explorer API securely // if *.onion hostname, we use Tor normally // if localhost, LAN address, or *.local FQDN we use HTTP without Tor // otherwise we enforce https:// for any clearnet FQDN hostname List serviceAddressesParsed = new ArrayList(); serviceAddressesRaw.forEach((addr) -> { addr = addr.replaceAll("http://", "").replaceAll("https://", ""); if (onionRegex.validate(addr).isValid) { log.info("Using Tor for onion hostname: {}", addr); serviceAddressesParsed.add(addr); } else if (localhostRegex.validate(addr).isValid) { log.info("Using HTTP without Tor for Loopback address: {}", addr); serviceAddressesParsed.add("http://" + addr); } else if (localnetRegex.validate(addr).isValid) { log.info("Using HTTP without Tor for LAN address: {}", addr); serviceAddressesParsed.add("http://" + addr); } else { log.info("Using HTTPS with Tor for Clearnet address: {}", addr); serviceAddressesParsed.add("https://" + addr); } }); preferences.setAutoConfServiceAddresses("XMR", serviceAddressesParsed); } }; autoConfTradeLimitListener = (observable, oldValue, newValue) -> { if (!newValue.equals(oldValue) && autoConfTradeLimitTf.getValidator().validate(newValue).isValid) { BigInteger amount = HavenoUtils.parseXmr(newValue); preferences.setAutoConfTradeLimit("XMR", amount.longValueExact()); } }; autoConfServiceAddressFocusOutListener = (observable, oldValue, newValue) -> { if (oldValue && !newValue) { log.info("Service address focus out, check and re-display default option"); if (autoConfServiceAddressTf.getText().isEmpty()) { preferences.findAutoConfirmSettings("XMR").ifPresent(autoConfirmSettings -> { List serviceAddresses = autoConfirmSettings.getServiceAddresses(); autoConfServiceAddressTf.setText(String.join(", ", serviceAddresses)); }); } } }; // We use a focus out handler to not update the data during entering text as that might lead to lower than // intended numbers which could be lead in the worst case to auto completion as number of confirmations is // reached. E.g. user had value 10 and wants to change it to 15 and deletes the 0, so current value would be 1. // If the service result just comes in at that moment the service might be considered complete as 1 is at that // moment used. We read the data just in time to make changes more flexible, otherwise user would need to // restart to apply changes from the number of confirmations settings. // Other fields like service addresses and limits are not affected and are taken at service start and cannot be // changed for already started services. autoConfRequiredConfirmationsFocusOutListener = (observable, oldValue, newValue) -> { if (oldValue && !newValue) { String txt = autoConfRequiredConfirmationsTf.getText(); if (autoConfRequiredConfirmationsTf.getValidator().validate(txt).isValid) { int requiredConfirmations = Integer.parseInt(txt); preferences.setAutoConfRequiredConfirmations("XMR", requiredConfirmations); } else { preferences.findAutoConfirmSettings("XMR") .ifPresent(e -> autoConfRequiredConfirmationsTf .setText(String.valueOf(e.getRequiredConfirmations()))); } } }; filterChangeListener = (observable, oldValue, newValue) -> { autoConfirmGridPane.setDisable(newValue != null && newValue.isDisableAutoConf()); }; autoConfirmGridPane.setDisable(filterManager.getFilter() != null && filterManager.getFilter().isDisableAutoConf()); } /////////////////////////////////////////////////////////////////////////////////////////// // Activate /////////////////////////////////////////////////////////////////////////////////////////// private void activateGeneralOptions() { ignoreTradersListInputTextField.setText(String.join(", ", preferences.getIgnoreTradersList())); /* referralIdService.getOptionalReferralId().ifPresent(referralId -> referralIdInputTextField.setText(referralId)); referralIdInputTextField.setPromptText(Res.get("setting.preferences.refererId.prompt"));*/ clearDataAfterDaysInputTextField.setText(String.valueOf(preferences.getClearDataAfterDays())); userLanguageComboBox.setItems(languageCodes); userLanguageComboBox.getSelectionModel().select(preferences.getUserLanguage()); userLanguageComboBox.setConverter(new StringConverter<>() { @Override public String toString(String code) { return LanguageUtil.getDisplayName(code); } @Override public String fromString(String string) { return null; } }); userLanguageComboBox.setOnAction(e -> { String selectedItem = userLanguageComboBox.getSelectionModel().getSelectedItem(); if (selectedItem != null) { preferences.setUserLanguage(selectedItem); new Popup().information(Res.get("settings.preferences.languageChange")) .closeButtonText(Res.get("shared.ok")) .show(); if (model.needsSupportLanguageWarning()) { new Popup().warning(Res.get("settings.preferences.supportLanguageWarning", model.getArbitrationLanguages())) .closeButtonText(Res.get("shared.ok")) .show(); } } }); userCountryComboBox.setItems(countries); userCountryComboBox.getSelectionModel().select(preferences.getUserCountry()); userCountryComboBox.setConverter(new StringConverter<>() { @Override public String toString(Country country) { return CountryUtil.getNameByCode(country.code); } @Override public Country fromString(String string) { return null; } }); userCountryComboBox.setOnAction(e -> { Country country = userCountryComboBox.getSelectionModel().getSelectedItem(); if (country != null) { preferences.setUserCountry(country); } }); xmrExplorerTextField.setText(preferences.getBlockChainExplorer().name); deviationInputTextField.setText(FormattingUtils.formatToPercentWithSymbol(preferences.getMaxPriceDistanceInPercent())); deviationInputTextField.textProperty().addListener(deviationListener); deviationInputTextField.focusedProperty().addListener(deviationFocusedListener); ignoreTradersListInputTextField.textProperty().addListener(ignoreTradersListListener); //referralIdInputTextField.textProperty().addListener(referralIdListener); clearDataAfterDaysInputTextField.textProperty().addListener(clearDataAfterDaysListener); } private void activateDisplayCurrencies() { preferredTradeCurrencyComboBox.setItems(tradeCurrencies); preferredTradeCurrencyComboBox.getSelectionModel().select(preferences.getPreferredTradeCurrency()); preferredTradeCurrencyComboBox.setVisibleRowCount(12); preferredTradeCurrencyComboBox.setOnAction(e -> { TradeCurrency selectedItem = preferredTradeCurrencyComboBox.getSelectionModel().getSelectedItem(); if (selectedItem != null) preferences.setPreferredTradeCurrency(selectedItem); }); traditionalCurrenciesComboBox.setItems(allTraditionalCurrencies); traditionalCurrenciesListView.setItems(traditionalCurrencies); traditionalCurrenciesComboBox.setOnHiding(e -> { TraditionalCurrency selectedItem = traditionalCurrenciesComboBox.getSelectionModel().getSelectedItem(); if (selectedItem != null) { preferences.addTraditionalCurrency(selectedItem); if (allTraditionalCurrencies.contains(selectedItem)) { UserThread.execute(() -> { traditionalCurrenciesComboBox.getSelectionModel().clearSelection(); allTraditionalCurrencies.remove(selectedItem); }); } } }); cryptoCurrenciesComboBox.setItems(allCryptoCurrencies); cryptoCurrenciesListView.setItems(cryptoCurrencies); cryptoCurrenciesComboBox.setOnHiding(e -> { CryptoCurrency selectedItem = cryptoCurrenciesComboBox.getSelectionModel().getSelectedItem(); if (selectedItem != null) { preferences.addCryptoCurrency(selectedItem); if (allCryptoCurrencies.contains(selectedItem)) { UserThread.execute(() -> { cryptoCurrenciesComboBox.getSelectionModel().clearSelection(); allCryptoCurrencies.remove(selectedItem); }); } } }); } private void activateDisplayPreferences() { showOwnOffersInOfferBook.setSelected(preferences.isShowOwnOffersInOfferBook()); showOwnOffersInOfferBook.setOnAction(e -> preferences.setShowOwnOffersInOfferBook(showOwnOffersInOfferBook.isSelected())); useAnimations.setSelected(preferences.isUseAnimations()); useAnimations.setOnAction(e -> preferences.setUseAnimations(useAnimations.isSelected())); useDarkMode.setSelected(preferences.getCssTheme() == 1); useDarkMode.setOnAction(e -> preferences.setCssTheme(useDarkMode.isSelected())); sortMarketCurrenciesNumerically.setSelected(preferences.isSortMarketCurrenciesNumerically()); sortMarketCurrenciesNumerically.setOnAction(e -> preferences.setSortMarketCurrenciesNumerically(sortMarketCurrenciesNumerically.isSelected())); boolean disableToggle = false; if (user.getPaymentAccounts() != null) { Set supportedPaymentMethods = user.getPaymentAccounts().stream() .map(PaymentAccount::getPaymentMethod).collect(Collectors.toSet()); disableToggle = supportedPaymentMethods.isEmpty(); } hideNonAccountPaymentMethodsToggle.setSelected(preferences.isHideNonAccountPaymentMethods() && !disableToggle); hideNonAccountPaymentMethodsToggle.setOnAction(e -> preferences.setHideNonAccountPaymentMethods(hideNonAccountPaymentMethodsToggle.isSelected())); hideNonAccountPaymentMethodsToggle.setDisable(disableToggle); //denyApiTakerToggle.setSelected(preferences.isDenyApiTaker()); //denyApiTakerToggle.setOnAction(e -> preferences.setDenyApiTaker(denyApiTakerToggle.isSelected())); //notifyOnPreReleaseToggle.setSelected(preferences.isNotifyOnPreRelease()); //notifyOnPreReleaseToggle.setOnAction(e -> preferences.setNotifyOnPreRelease(notifyOnPreReleaseToggle.isSelected())); resetDontShowAgainButton.setOnAction(e -> preferences.resetDontShowAgain()); editCustomBtcExplorer.setOnAction(e -> { EditCustomExplorerWindow urlWindow = new EditCustomExplorerWindow("XMR", preferences.getBlockChainExplorer(), preferences.getBlockChainExplorers()); urlWindow .actionButtonText(Res.get("shared.save")) .onAction(() -> { preferences.setBlockChainExplorer(urlWindow.getEditedBlockChainExplorer()); xmrExplorerTextField.setText(preferences.getBlockChainExplorer().name); }) .closeButtonText(Res.get("shared.cancel")) .onClose(urlWindow::hide) .show(); }); // We use opposite property (useStandbyMode) in preferences to have the default value (false) set as we want it, // so users who update gets set avoidStandbyMode=true (useStandbyMode=false) if (displayStandbyModeFeature) { avoidStandbyMode.setSelected(!preferences.isUseStandbyMode()); avoidStandbyMode.setOnAction(e -> preferences.setUseStandbyMode(!avoidStandbyMode.isSelected())); } else { preferences.setUseStandbyMode(false); } useSoundForNotifications.setSelected(preferences.isUseSoundForNotifications()); useSoundForNotifications.setOnAction(e -> preferences.setUseSoundForNotifications(useSoundForNotifications.isSelected())); } private void activateAutoConfirmPreferences() { preferences.findAutoConfirmSettings("XMR").ifPresent(autoConfirmSettings -> { autoConfirmXmrToggle.setSelected(autoConfirmSettings.isEnabled()); autoConfRequiredConfirmationsTf.setText(String.valueOf(autoConfirmSettings.getRequiredConfirmations())); autoConfTradeLimitTf.setText(HavenoUtils.formatXmr(autoConfirmSettings.getTradeLimit())); autoConfServiceAddressTf.setText(String.join(", ", autoConfirmSettings.getServiceAddresses())); autoConfRequiredConfirmationsTf.focusedProperty().addListener(autoConfRequiredConfirmationsFocusOutListener); autoConfTradeLimitTf.textProperty().addListener(autoConfTradeLimitListener); autoConfServiceAddressTf.textProperty().addListener(autoConfServiceAddressListener); autoConfServiceAddressTf.focusedProperty().addListener(autoConfServiceAddressFocusOutListener); autoConfirmXmrToggle.setOnAction(e -> { preferences.setAutoConfEnabled(autoConfirmSettings.getCurrencyCode(), autoConfirmXmrToggle.isSelected()); }); filterManager.filterProperty().addListener(filterChangeListener); }); } /////////////////////////////////////////////////////////////////////////////////////////// // Deactivate /////////////////////////////////////////////////////////////////////////////////////////// private void deactivateGeneralOptions() { //selectBaseCurrencyNetworkComboBox.setOnAction(null); userLanguageComboBox.setOnAction(null); userCountryComboBox.setOnAction(null); editCustomBtcExplorer.setOnAction(null); deviationInputTextField.textProperty().removeListener(deviationListener); deviationInputTextField.focusedProperty().removeListener(deviationFocusedListener); ignoreTradersListInputTextField.textProperty().removeListener(ignoreTradersListListener); //referralIdInputTextField.textProperty().removeListener(referralIdListener); clearDataAfterDaysInputTextField.textProperty().removeListener(clearDataAfterDaysListener); } private void deactivateDisplayCurrencies() { preferredTradeCurrencyComboBox.setOnAction(null); } private void deactivateDisplayPreferences() { useAnimations.setOnAction(null); useDarkMode.setOnAction(null); sortMarketCurrenciesNumerically.setOnAction(null); hideNonAccountPaymentMethodsToggle.setOnAction(null); //denyApiTakerToggle.setOnAction(null); //notifyOnPreReleaseToggle.setOnAction(null); showOwnOffersInOfferBook.setOnAction(null); resetDontShowAgainButton.setOnAction(null); if (displayStandbyModeFeature) { avoidStandbyMode.setOnAction(null); } } private void deactivateAutoConfirmPreferences() { preferences.findAutoConfirmSettings("XMR").ifPresent(autoConfirmSettings -> { autoConfirmXmrToggle.setOnAction(null); autoConfTradeLimitTf.textProperty().removeListener(autoConfTradeLimitListener); autoConfServiceAddressTf.textProperty().removeListener(autoConfServiceAddressListener); autoConfServiceAddressTf.focusedProperty().removeListener(autoConfServiceAddressFocusOutListener); autoConfRequiredConfirmationsTf.focusedProperty().removeListener(autoConfRequiredConfirmationsFocusOutListener); filterManager.filterProperty().removeListener(filterChangeListener); }); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/settings/preferences/PreferencesViewModel.java ================================================ /* * 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 . */ package haveno.desktop.main.settings.preferences; import com.google.inject.Inject; import haveno.core.locale.LanguageUtil; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.support.dispute.mediation.mediator.MediatorManager; import haveno.core.user.Preferences; import haveno.desktop.common.model.ActivatableViewModel; import java.util.stream.Collectors; public class PreferencesViewModel extends ActivatableViewModel { private final ArbitratorManager arbitratorManager; private final MediatorManager mediationManager; private final Preferences preferences; @Inject public PreferencesViewModel(Preferences preferences, ArbitratorManager arbitratorManager, MediatorManager mediationManager) { this.preferences = preferences; this.arbitratorManager = arbitratorManager; this.mediationManager = mediationManager; } boolean needsSupportLanguageWarning() { return !arbitratorManager.isAgentAvailableForLanguage(preferences.getUserLanguage()) || !mediationManager.isAgentAvailableForLanguage(preferences.getUserLanguage()); } String getArbitrationLanguages() { return arbitratorManager.getObservableMap().values().stream() .flatMap(arbitrator -> arbitrator.getLanguageCodes().stream()) .distinct() .map(LanguageUtil::getDisplayName) .collect(Collectors.joining(", ")); } public String getMediationLanguages() { return mediationManager.getObservableMap().values().stream() .flatMap(mediator -> mediator.getLanguageCodes().stream()) .distinct() .map(LanguageUtil::getDisplayName) .collect(Collectors.joining(", ")); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/shared/ChatView.java ================================================ /* * 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 . */ package haveno.desktop.main.shared; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.HavenoTextArea; import haveno.desktop.components.BusyAnimation; import haveno.desktop.components.TableGroupHeadline; import haveno.desktop.main.overlays.notifications.Notification; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.DisplayUtils; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.Layout; import haveno.core.locale.Res; import haveno.core.support.SupportManager; import haveno.core.support.SupportSession; import haveno.core.support.dispute.Attachment; import haveno.core.support.messages.ChatMessage; import haveno.network.p2p.network.Connection; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.util.Utilities; import com.google.common.io.ByteStreams; import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIconView; import javafx.stage.FileChooser; import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.TextArea; import javafx.scene.control.Tooltip; import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.scene.text.TextAlignment; import javafx.geometry.Insets; import org.apache.commons.lang3.exception.ExceptionUtils; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.value.ChangeListener; import javafx.event.EventHandler; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.SortedList; import javafx.util.Callback; import java.net.MalformedURLException; import java.net.URL; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; @Slf4j public class ChatView extends AnchorPane { // UI private TextArea inputTextArea; private Button sendButton; private ListView messageListView; private Label sendMsgInfoLabel; private BusyAnimation sendMsgBusyAnimation; private TableGroupHeadline tableGroupHeadline; private VBox messagesInputBox; // Options @Getter Node extraButton; @Getter private ReadOnlyDoubleProperty widthProperty; @Setter boolean allowAttachments; @Setter boolean displayHeader; // Communication stuff, to be renamed to something more generic private ChatMessage chatMessage; private ObservableList chatMessages; private ListChangeListener disputeDirectMessageListListener; private Subscription inputTextAreaTextSubscription; private final List tempAttachments = new ArrayList<>(); private ChangeListener storedInMailboxPropertyListener, acknowledgedPropertyListener; private ChangeListener sendMessageErrorPropertyListener; private EventHandler keyEventEventHandler; private SupportManager supportManager; private Optional optionalSupportSession = Optional.empty(); private String counterpartyName; public ChatView(SupportManager supportManager, String counterpartyName) { this.supportManager = supportManager; this.counterpartyName = counterpartyName; allowAttachments = true; displayHeader = true; } public void initialize() { disputeDirectMessageListListener = c -> scrollToBottom(); keyEventEventHandler = event -> { if (Utilities.isAltOrCtrlPressed(KeyCode.ENTER, event)) { optionalSupportSession.ifPresent(supportSession -> { if (supportSession.chatIsOpen() && inputTextArea.isFocused()) { onTrySendMessage(); } }); } }; } public void activate() { addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); } public void deactivate() { removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); removeListenersOnSessionChange(); } public void display(SupportSession supportSession, ReadOnlyDoubleProperty widthProperty) { display(supportSession, null, widthProperty); } public void display(SupportSession supportSession, @Nullable Node extraButton, ReadOnlyDoubleProperty widthProperty) { optionalSupportSession = Optional.of(supportSession); removeListenersOnSessionChange(); this.getChildren().clear(); this.extraButton = extraButton; this.widthProperty = widthProperty; tableGroupHeadline = new TableGroupHeadline(); tableGroupHeadline.setText(Res.get("support.messages")); AnchorPane.setTopAnchor(tableGroupHeadline, 10d); AnchorPane.setRightAnchor(tableGroupHeadline, 0d); AnchorPane.setBottomAnchor(tableGroupHeadline, 0d); AnchorPane.setLeftAnchor(tableGroupHeadline, 0d); chatMessages = supportSession.getObservableChatMessageList(); SortedList sortedList = new SortedList<>(chatMessages); sortedList.setComparator(Comparator.comparing(o -> new Date(o.getDate()))); messageListView = new ListView<>(sortedList); messageListView.setId("message-list-view"); messageListView.setMinHeight(150); AnchorPane.setTopAnchor(messageListView, 30d); AnchorPane.setRightAnchor(messageListView, 0d); AnchorPane.setLeftAnchor(messageListView, 0d); VBox.setVgrow(this, Priority.ALWAYS); inputTextArea = new HavenoTextArea(); inputTextArea.setPrefHeight(70); inputTextArea.setWrapText(true); inputTextArea.getStyleClass().add("input-with-border"); if (!supportSession.isDisputeAgent()) { inputTextArea.setPromptText(Res.get("support.input.prompt")); } sendButton = new AutoTooltipButton(Res.get("support.send")); sendButton.setDefaultButton(true); sendButton.setOnAction(e -> onTrySendMessage()); sendButton.setStyle("-fx-pref-width: 125; -fx-min-width: 110; -fx-padding: 3 3 3 3;"); inputTextAreaTextSubscription = EasyBind.subscribe(inputTextArea.textProperty(), t -> sendButton.setDisable(t.isEmpty())); Button uploadButton = new AutoTooltipButton(Res.get("support.addAttachments")); uploadButton.setOnAction(e -> onRequestUpload()); Button clipboardButton = new AutoTooltipButton(Res.get("shared.copyToClipboard")); clipboardButton.setOnAction(e -> copyChatMessagesToClipboard(clipboardButton)); uploadButton.setStyle("-fx-pref-width: 125; -fx-min-width: 110; -fx-padding: 3 3 3 3;"); clipboardButton.setStyle("-fx-pref-width: 125; -fx-min-width: 110; -fx-padding: 3 3 3 3;"); sendMsgInfoLabel = new AutoTooltipLabel(); sendMsgInfoLabel.setVisible(false); sendMsgInfoLabel.setManaged(false); sendMsgInfoLabel.setPadding(new Insets(5, 0, 0, 0)); sendMsgBusyAnimation = new BusyAnimation(false); if (displayHeader) this.getChildren().add(tableGroupHeadline); if (supportSession.chatIsOpen()) { HBox buttonBox = new HBox(); buttonBox.setSpacing(10); if (allowAttachments) buttonBox.getChildren().addAll(sendButton, uploadButton, clipboardButton, sendMsgBusyAnimation, sendMsgInfoLabel); else buttonBox.getChildren().addAll(sendButton, sendMsgBusyAnimation, sendMsgInfoLabel); if (extraButton != null) { Pane spacer = new Pane(); HBox.setHgrow(spacer, Priority.ALWAYS); buttonBox.getChildren().addAll(spacer, extraButton); } messagesInputBox = new VBox(); messagesInputBox.setSpacing(10); messagesInputBox.getChildren().addAll(inputTextArea, buttonBox); VBox.setVgrow(buttonBox, Priority.ALWAYS); AnchorPane.setRightAnchor(messagesInputBox, 0d); AnchorPane.setBottomAnchor(messagesInputBox, 5d); AnchorPane.setLeftAnchor(messagesInputBox, 0d); AnchorPane.setBottomAnchor(messageListView, 120d); this.getChildren().addAll(messageListView, messagesInputBox); } else { AnchorPane.setBottomAnchor(messageListView, 0d); this.getChildren().add(messageListView); } messageListView.setCellFactory(new Callback<>() { @Override public ListCell call(ListView list) { return new ListCell<>() { ChangeListener sendMsgBusyAnimationListener; Pane bg = new Pane(); ImageView arrow = new ImageView(); Label headerLabel = new AutoTooltipLabel(); Label messageLabel = new AutoTooltipLabel(); Label copyLabel = new Label(); HBox attachmentsBox = new HBox(); AnchorPane messageAnchorPane = new AnchorPane(); Label statusIcon = new Label(); Label statusInfoLabel = new Label(); HBox statusHBox = new HBox(); double arrowWidth = 15d; double attachmentsBoxHeight = 20d; double border = 10d; double bottomBorder = 25d; double padding = border + 10d; double msgLabelPaddingRight = padding + 20d; { bg.setMinHeight(30); messageLabel.setWrapText(true); headerLabel.setTextAlignment(TextAlignment.CENTER); attachmentsBox.setSpacing(5); statusIcon.getStyleClass().add("small-text"); statusInfoLabel.getStyleClass().add("small-text"); statusInfoLabel.setPadding(new Insets(3, 0, 0, 0)); copyLabel.setTooltip(new Tooltip(Res.get("shared.copyToClipboard"))); statusHBox.setSpacing(5); statusHBox.getChildren().addAll(statusIcon, statusInfoLabel); messageAnchorPane.getChildren().addAll(bg, arrow, headerLabel, messageLabel, copyLabel, attachmentsBox, statusHBox); } @Override protected void updateItem(ChatMessage message, boolean empty) { UserThread.execute(() -> { super.updateItem(message, empty); if (message != null && !empty) { copyLabel.setOnMouseClicked(e -> { Utilities.copyToClipboard(messageLabel.getText()); Tooltip tp = new Tooltip(Res.get("shared.copiedToClipboard")); Node node = (Node) e.getSource(); UserThread.runAfter(() -> tp.hide(), 1); tp.show(node, e.getScreenX() + Layout.PADDING, e.getScreenY() + Layout.PADDING); }); messageLabel.setOnMouseClicked(event -> { if (2 > event.getClickCount()) { return; } GUIUtil.showSelectableTextModal(headerLabel.getText(), messageLabel.getText()); }); if (!messageAnchorPane.prefWidthProperty().isBound()) messageAnchorPane.prefWidthProperty() .bind(messageListView.widthProperty().subtract(padding + GUIUtil.getScrollbarWidth(messageListView))); AnchorPane.clearConstraints(bg); AnchorPane.clearConstraints(headerLabel); AnchorPane.clearConstraints(arrow); AnchorPane.clearConstraints(messageLabel); AnchorPane.clearConstraints(copyLabel); AnchorPane.clearConstraints(statusHBox); AnchorPane.clearConstraints(attachmentsBox); AnchorPane.setTopAnchor(bg, 15d); AnchorPane.setBottomAnchor(bg, bottomBorder); AnchorPane.setTopAnchor(headerLabel, 0d); AnchorPane.setBottomAnchor(arrow, bottomBorder + 5d); AnchorPane.setTopAnchor(messageLabel, 25d); AnchorPane.setTopAnchor(copyLabel, 25d); AnchorPane.setBottomAnchor(attachmentsBox, bottomBorder + 10); boolean senderIsTrader = message.isSenderIsTrader(); boolean isMyMsg = supportSession.isClient() == senderIsTrader; arrow.setVisible(!message.isSystemMessage()); arrow.setManaged(!message.isSystemMessage()); statusHBox.setVisible(false); headerLabel.getStyleClass().removeAll("message-header", "my-message-header", "success-text", "highlight-static"); messageLabel.getStyleClass().removeAll("my-message", "message"); copyLabel.getStyleClass().removeAll("my-message", "message"); if (message.isSystemMessage()) { headerLabel.getStyleClass().addAll("message-header", "success-text"); bg.setId("message-bubble-green"); messageLabel.getStyleClass().add("my-message"); copyLabel.getStyleClass().add("my-message"); message.addWeakMessageStateListener(() -> UserThread.execute(() -> updateMsgState(message))); updateMsgState(message); } else if (isMyMsg) { headerLabel.getStyleClass().add("my-message-header"); bg.setId("message-bubble-blue"); messageLabel.getStyleClass().add("my-message"); copyLabel.getStyleClass().add("my-message"); if (supportSession.isClient()) arrow.setId("bubble_arrow_blue_left"); else arrow.setId("bubble_arrow_blue_right"); if (sendMsgBusyAnimationListener != null) sendMsgBusyAnimation.isRunningProperty().removeListener(sendMsgBusyAnimationListener); sendMsgBusyAnimationListener = (observable, oldValue, newValue) -> { if (!newValue) UserThread.execute(() -> updateMsgState(message)); }; sendMsgBusyAnimation.isRunningProperty().addListener(sendMsgBusyAnimationListener); message.addWeakMessageStateListener(() -> UserThread.execute(() -> updateMsgState(message))); updateMsgState(message); } else { headerLabel.getStyleClass().add("message-header"); bg.setId("message-bubble-grey"); messageLabel.getStyleClass().add("message"); copyLabel.getStyleClass().add("message"); if (supportSession.isClient()) arrow.setId("bubble_arrow_grey_right"); else arrow.setId("bubble_arrow_grey_left"); } if (message.isSystemMessage()) { AnchorPane.setLeftAnchor(headerLabel, padding); AnchorPane.setRightAnchor(headerLabel, padding); AnchorPane.setLeftAnchor(bg, border); AnchorPane.setRightAnchor(bg, border); AnchorPane.setLeftAnchor(messageLabel, padding); AnchorPane.setRightAnchor(messageLabel, msgLabelPaddingRight); AnchorPane.setRightAnchor(copyLabel, padding); AnchorPane.setLeftAnchor(attachmentsBox, padding); AnchorPane.setRightAnchor(attachmentsBox, padding); AnchorPane.setLeftAnchor(statusHBox, padding); } else if (senderIsTrader) { AnchorPane.setLeftAnchor(headerLabel, padding + arrowWidth); AnchorPane.setLeftAnchor(bg, border + arrowWidth); AnchorPane.setRightAnchor(bg, border); AnchorPane.setLeftAnchor(arrow, border); AnchorPane.setLeftAnchor(messageLabel, padding + arrowWidth); AnchorPane.setRightAnchor(messageLabel, msgLabelPaddingRight); AnchorPane.setRightAnchor(copyLabel, padding); AnchorPane.setLeftAnchor(attachmentsBox, padding + arrowWidth); AnchorPane.setRightAnchor(attachmentsBox, padding); AnchorPane.setRightAnchor(statusHBox, padding); } else { AnchorPane.setRightAnchor(headerLabel, padding + arrowWidth); AnchorPane.setRightAnchor(bg, border + arrowWidth); AnchorPane.setLeftAnchor(bg, border); AnchorPane.setRightAnchor(arrow, border); AnchorPane.setLeftAnchor(messageLabel, padding); AnchorPane.setRightAnchor(messageLabel, msgLabelPaddingRight + arrowWidth); AnchorPane.setRightAnchor(copyLabel, padding + arrowWidth); AnchorPane.setLeftAnchor(attachmentsBox, padding); AnchorPane.setRightAnchor(attachmentsBox, padding + arrowWidth); AnchorPane.setLeftAnchor(statusHBox, padding); } AnchorPane.setBottomAnchor(statusHBox, 7d); String metaData = DisplayUtils.formatDateTime(new Date(message.getDate())); if (!message.isSystemMessage()) metaData = (isMyMsg ? "Sent " : "Received ") + metaData + (isMyMsg ? "" : " from " + counterpartyName); headerLabel.setText(metaData); messageLabel.setText(message.getMessage()); attachmentsBox.getChildren().clear(); if (allowAttachments && message.getAttachments() != null && message.getAttachments().size() > 0) { AnchorPane.setBottomAnchor(messageLabel, bottomBorder + attachmentsBoxHeight + 10); attachmentsBox.getChildren().add(new AutoTooltipLabel(Res.get("support.attachments") + " ") {{ setPadding(new Insets(0, 0, 3, 0)); if (isMyMsg) getStyleClass().add("my-message"); else getStyleClass().add("message"); }}); message.getAttachments().forEach(attachment -> { Label icon = new Label(); setPadding(new Insets(0, 0, 3, 0)); if (isMyMsg) icon.getStyleClass().add("attachment-icon"); else icon.getStyleClass().add("attachment-icon-black"); AwesomeDude.setIcon(icon, AwesomeIcon.FILE_TEXT); icon.setPadding(new Insets(-2, 0, 0, 0)); icon.setTooltip(new Tooltip(attachment.getFileName())); icon.setOnMouseClicked(event -> onOpenAttachment(attachment)); attachmentsBox.getChildren().add(icon); }); } else { AnchorPane.setBottomAnchor(messageLabel, bottomBorder + 10); } // Need to set it here otherwise style is not correct copyLabel.getStyleClass().addAll("icon", "copy-icon-disputes"); MaterialDesignIconView copyIcon = new MaterialDesignIconView(MaterialDesignIcon.CONTENT_COPY, "16.0"); copyLabel.setGraphic(copyIcon); // TODO There are still some cell rendering issues on updates setGraphic(messageAnchorPane); } else { if (sendMsgBusyAnimation != null && sendMsgBusyAnimationListener != null) sendMsgBusyAnimation.isRunningProperty().removeListener(sendMsgBusyAnimationListener); messageAnchorPane.prefWidthProperty().unbind(); copyLabel.setOnMouseClicked(null); messageLabel.setOnMouseClicked(null); setGraphic(null); } }); } private void updateMsgState(ChatMessage message) { boolean visible; AwesomeIcon icon = null; String text = null; statusIcon.getStyleClass().add("status-icon"); statusInfoLabel.getStyleClass().add("status-icon"); statusHBox.setOpacity(1); log.debug("updateMsgState msg-{}, ack={}, arrived={}", message.getMessage(), message.acknowledgedProperty().get(), message.arrivedProperty().get()); if (message.acknowledgedProperty().get()) { visible = true; icon = AwesomeIcon.OK_SIGN; text = Res.get("support.acknowledged"); } else if (message.storedInMailboxProperty().get()) { visible = true; icon = AwesomeIcon.ENVELOPE; text = Res.get("support.savedInMailbox"); } else if (message.ackErrorProperty().get() != null) { visible = true; icon = AwesomeIcon.EXCLAMATION_SIGN; text = Res.get("support.error", message.ackErrorProperty().get()); statusIcon.getStyleClass().add("error-text"); statusInfoLabel.getStyleClass().add("error-text"); } else if (message.arrivedProperty().get()) { visible = true; icon = AwesomeIcon.MAIL_REPLY; text = Res.get("support.transient"); } else { visible = false; log.debug("updateMsgState called but no msg state available. message={}", message); } statusHBox.setVisible(visible); if (visible) { AwesomeDude.setIcon(statusIcon, icon, "14"); statusIcon.setTooltip(new Tooltip(text)); statusInfoLabel.setText(text); } } }; } }); addListenersOnSessionChange(widthProperty); scrollToBottom(); } /////////////////////////////////////////////////////////////////////////////////////////// // Actions /////////////////////////////////////////////////////////////////////////////////////////// private void onTrySendMessage() { if (supportManager.isBootstrapped()) { String text = inputTextArea.getText(); if (!text.isEmpty()) { if (text.length() < 5_000) { onSendMessage(text); } else { new Popup().information(Res.get("popup.warning.messageTooLong")).show(); } } } else { new Popup().information(Res.get("popup.warning.notFullyConnected")).show(); } } private void onRequestUpload() { if (!allowAttachments) return; int totalSize = tempAttachments.stream().mapToInt(a -> a.getBytes().length).sum(); if (tempAttachments.size() < 3) { FileChooser fileChooser = new FileChooser(); int maxMsgSize = Connection.getPermittedMessageSize(); int maxSizeInKB = maxMsgSize / 1024; fileChooser.setTitle(Res.get("support.openFile", maxSizeInKB)); /* if (Utilities.isUnix()) fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));*/ File result = fileChooser.showOpenDialog(getScene().getWindow()); if (result != null) { try { URL url = result.toURI().toURL(); try (InputStream inputStream = url.openStream()) { byte[] filesAsBytes = ByteStreams.toByteArray(inputStream); int size = filesAsBytes.length; int newSize = totalSize + size; if (newSize > maxMsgSize) { new Popup().warning(Res.get("support.attachmentTooLarge", (newSize / 1024), maxSizeInKB)).show(); } else if (size > maxMsgSize) { new Popup().warning(Res.get("support.maxSize", maxSizeInKB)).show(); } else { tempAttachments.add(new Attachment(result.getName(), filesAsBytes)); inputTextArea.setText(inputTextArea.getText() + "\n[" + Res.get("support.attachment") + " " + result.getName() + "]"); } } catch (java.io.IOException e) { log.error(ExceptionUtils.getStackTrace(e)); } } catch (MalformedURLException e2) { log.error(ExceptionUtils.getStackTrace(e2)); } } } else { new Popup().warning(Res.get("support.tooManyAttachments")).show(); } } public void onAttachText(String textAttachment, String name) { if (!allowAttachments) return; try { byte[] filesAsBytes = textAttachment.getBytes("UTF8"); int size = filesAsBytes.length; int maxMsgSize = Connection.getPermittedMessageSize(); int maxSizeInKB = maxMsgSize / 1024; if (size > maxMsgSize) { new Popup().warning(Res.get("support.attachmentTooLarge", (size / 1024), maxSizeInKB)).show(); } else { tempAttachments.add(new Attachment(name, filesAsBytes)); inputTextArea.setText(inputTextArea.getText() + "\n[" + Res.get("support.attachment") + " " + name + "]"); } } catch (Exception e) { log.error(ExceptionUtils.getStackTrace(e)); } } private void copyChatMessagesToClipboard(Button sourceBtn) { optionalSupportSession.ifPresent(session -> { StringBuilder stringBuilder = new StringBuilder(); chatMessages.forEach(i -> { String metaData = DisplayUtils.formatDateTime(new Date(i.getDate())); metaData = metaData + (i.isSystemMessage() ? " (System message)" : (i.isSenderIsTrader() ? " (from Trader)" : " (from Agent)")); stringBuilder.append(metaData).append("\n").append(i.getMessage()).append("\n\n"); }); Utilities.copyToClipboard(stringBuilder.toString()); new Notification() .notification(Res.get("shared.copiedToClipboard")) .hideCloseButton() .autoClose() .show(); }); } private void onOpenAttachment(Attachment attachment) { if (!allowAttachments) return; FileChooser fileChooser = new FileChooser(); fileChooser.setTitle(Res.get("support.save")); fileChooser.setInitialFileName(attachment.getFileName()); /* if (Utilities.isUnix()) fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));*/ File file = fileChooser.showSaveDialog(getScene().getWindow()); if (file != null) { try (FileOutputStream fileOutputStream = new FileOutputStream(file.getAbsolutePath())) { fileOutputStream.write(attachment.getBytes()); } catch (IOException e) { log.error("Error opening attachment: {}\n", e.getMessage(), e); } } } private void onSendMessage(String inputText) { if (chatMessage != null) { chatMessage.acknowledgedProperty().removeListener(acknowledgedPropertyListener); chatMessage.storedInMailboxProperty().removeListener(storedInMailboxPropertyListener); chatMessage.sendMessageErrorProperty().removeListener(sendMessageErrorPropertyListener); } chatMessage = sendDisputeDirectMessage(inputText, new ArrayList<>(tempAttachments)); tempAttachments.clear(); scrollToBottom(); inputTextArea.setDisable(true); inputTextArea.clear(); chatMessage.startAckTimer(); Timer timer = UserThread.runAfter(() -> { sendMsgInfoLabel.setVisible(true); sendMsgInfoLabel.setManaged(true); sendMsgInfoLabel.setText(Res.get("support.sendingMessage")); sendMsgBusyAnimation.play(); }, 500, TimeUnit.MILLISECONDS); acknowledgedPropertyListener = (observable, oldValue, newValue) -> { if (newValue) { sendMsgInfoLabel.setVisible(false); hideSendMsgInfo(timer); } }; storedInMailboxPropertyListener = (observable, oldValue, newValue) -> { if (newValue) { sendMsgInfoLabel.setVisible(true); sendMsgInfoLabel.setManaged(true); sendMsgInfoLabel.setText(Res.get("support.receiverNotOnline")); hideSendMsgInfo(timer); } }; sendMessageErrorPropertyListener = (observable, oldValue, newValue) -> { if (newValue != null) { sendMsgInfoLabel.setVisible(true); sendMsgInfoLabel.setManaged(true); sendMsgInfoLabel.setText(Res.get("support.sendMessageError", newValue)); hideSendMsgInfo(timer); } }; if (chatMessage != null) { chatMessage.acknowledgedProperty().addListener(acknowledgedPropertyListener); chatMessage.storedInMailboxProperty().addListener(storedInMailboxPropertyListener); chatMessage.sendMessageErrorProperty().addListener(sendMessageErrorPropertyListener); } } private ChatMessage sendDisputeDirectMessage(String text, ArrayList attachments) { return optionalSupportSession.map(supportSession -> { ChatMessage message = new ChatMessage( supportManager.getSupportType(), supportSession.getTradeId(), supportSession.getClientId(), supportSession.isClient(), text, supportManager.getMyAddress(), attachments ); supportManager.addAndPersistChatMessage(message); return supportManager.sendChatMessage(message); }).orElse(null); } /////////////////////////////////////////////////////////////////////////////////////////// // Helpers /////////////////////////////////////////////////////////////////////////////////////////// private void hideSendMsgInfo(Timer timer) { timer.stop(); inputTextArea.setDisable(false); UserThread.runAfter(() -> { sendMsgInfoLabel.setVisible(false); sendMsgInfoLabel.setManaged(false); }, 5); sendMsgBusyAnimation.stop(); } public void scrollToBottom() { UserThread.execute(() -> { if (messageListView != null && !messageListView.getItems().isEmpty()) { int lastIndex = messageListView.getItems().size(); messageListView.scrollTo(lastIndex); } }); } public void setInputBoxVisible(boolean visible) { if (messagesInputBox != null) { messagesInputBox.setVisible(visible); messagesInputBox.setManaged(visible); AnchorPane.setBottomAnchor(messageListView, visible ? 120d : 0d); } } public void removeInputBox() { this.getChildren().remove(messagesInputBox); } /////////////////////////////////////////////////////////////////////////////////////////// // Bindings /////////////////////////////////////////////////////////////////////////////////////////// private void addListenersOnSessionChange(ReadOnlyDoubleProperty widthProperty) { if (tableGroupHeadline != null) { tableGroupHeadline.prefWidthProperty().bind(widthProperty); messageListView.prefWidthProperty().bind(widthProperty); this.prefWidthProperty().bind(widthProperty); chatMessages.addListener(disputeDirectMessageListListener); inputTextAreaTextSubscription = EasyBind.subscribe(inputTextArea.textProperty(), t -> sendButton.setDisable(t.isEmpty())); } } private void removeListenersOnSessionChange() { if (chatMessages != null && disputeDirectMessageListListener != null) chatMessages.removeListener(disputeDirectMessageListListener); if (chatMessage != null) { if (acknowledgedPropertyListener != null) chatMessage.arrivedProperty().removeListener(acknowledgedPropertyListener); if (storedInMailboxPropertyListener != null) chatMessage.storedInMailboxProperty().removeListener(storedInMailboxPropertyListener); } if (messageListView != null) messageListView.prefWidthProperty().unbind(); if (tableGroupHeadline != null) tableGroupHeadline.prefWidthProperty().unbind(); this.prefWidthProperty().unbind(); if (inputTextAreaTextSubscription != null) inputTextAreaTextSubscription.unsubscribe(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/shared/PriceFeedComboBoxItem.java ================================================ /* * 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 . */ package haveno.desktop.main.shared; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import lombok.Getter; import lombok.Setter; public class PriceFeedComboBoxItem { public final String currencyCode; public final StringProperty displayStringProperty = new SimpleStringProperty(); @Setter @Getter private boolean isPriceAvailable; @Setter @Getter private boolean isExternallyProvidedPrice; public PriceFeedComboBoxItem(String currencyCode) { this.currencyCode = currencyCode; } public void setDisplayString(String displayString) { this.displayStringProperty.set(displayString); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/support/SupportView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/support/SupportView.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.desktop.main.support; import com.google.inject.Inject; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.crypto.KeyRing; import haveno.common.crypto.PubKeyRing; import haveno.core.locale.Res; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.support.dispute.mediation.MediationManager; import haveno.core.support.dispute.mediation.mediator.Mediator; import haveno.core.support.dispute.mediation.mediator.MediatorManager; import haveno.core.support.dispute.refund.RefundManager; import haveno.core.support.dispute.refund.refundagent.RefundAgent; import haveno.core.support.dispute.refund.refundagent.RefundAgentManager; import haveno.desktop.Navigation; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.common.view.CachingViewLoader; import haveno.desktop.common.view.FxmlView; import haveno.desktop.common.view.View; import haveno.desktop.common.view.ViewLoader; import haveno.desktop.main.MainView; import haveno.desktop.main.offer.signedoffer.SignedOfferView; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.support.dispute.agent.arbitration.ArbitratorView; import haveno.desktop.main.support.dispute.agent.mediation.MediatorView; import haveno.desktop.main.support.dispute.agent.refund.RefundAgentView; import haveno.desktop.main.support.dispute.client.arbitration.ArbitrationClientView; import haveno.desktop.main.support.dispute.client.mediation.MediationClientView; import haveno.desktop.main.support.dispute.client.refund.RefundClientView; import haveno.network.p2p.NodeAddress; import javafx.beans.value.ChangeListener; import javafx.collections.MapChangeListener; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javax.annotation.Nullable; @FxmlView public class SupportView extends ActivatableView { private Tab tradersMediationDisputesTab, tradersRefundDisputesTab; @Nullable private Tab tradersArbitrationDisputesTab; private Tab mediatorTab, refundAgentTab; @Nullable private Tab arbitratorTab; @Nullable private Tab signedOfferTab; private final Navigation navigation; private final ArbitratorManager arbitratorManager; private final MediatorManager mediatorManager; private final RefundAgentManager refundAgentManager; private final ArbitrationManager arbitrationManager; private final MediationManager mediationManager; private final RefundManager refundManager; private final KeyRing keyRing; private Navigation.Listener navigationListener; private ChangeListener tabChangeListener; private Tab currentTab; private final ViewLoader viewLoader; private MapChangeListener arbitratorMapChangeListener; private MapChangeListener mediatorMapChangeListener; private MapChangeListener refundAgentMapChangeListener; @Inject public SupportView(CachingViewLoader viewLoader, Navigation navigation, ArbitratorManager arbitratorManager, MediatorManager mediatorManager, RefundAgentManager refundAgentManager, ArbitrationManager arbitrationManager, MediationManager mediationManager, RefundManager refundManager, KeyRing keyRing) { this.viewLoader = viewLoader; this.navigation = navigation; this.arbitratorManager = arbitratorManager; this.mediatorManager = mediatorManager; this.refundAgentManager = refundAgentManager; this.arbitrationManager = arbitrationManager; this.mediationManager = mediationManager; this.refundManager = refundManager; this.keyRing = keyRing; } @Override public void initialize() { tradersMediationDisputesTab = new Tab(); tradersMediationDisputesTab.setClosable(false); //root.getTabs().add(tradersMediationDisputesTab); // hidden since mediation and refunds are not used in haveno tradersRefundDisputesTab = new Tab(); tradersRefundDisputesTab.setClosable(false); //root.getTabs().add(tradersRefundDisputesTab); tradersArbitrationDisputesTab = new Tab(); tradersArbitrationDisputesTab.setClosable(false); root.getTabs().add(tradersArbitrationDisputesTab); // Has to be called before loadView updateAgentTabs(); tradersMediationDisputesTab.setText(Res.get("support.tab.mediation.support")); tradersRefundDisputesTab.setText(Res.get("support.tab.refund.support")); tradersArbitrationDisputesTab.setText(Res.get("support.tab.arbitration.support")); navigationListener = (viewPath, data) -> { if (viewPath.size() == 3 && viewPath.indexOf(SupportView.class) == 1) UserThread.execute(() -> loadView(viewPath.tip())); }; tabChangeListener = (ov, oldValue, newValue) -> { if (newValue == tradersArbitrationDisputesTab) navigation.navigateTo(MainView.class, SupportView.class, ArbitrationClientView.class); else if (newValue == tradersMediationDisputesTab) navigation.navigateTo(MainView.class, SupportView.class, MediationClientView.class); else if (newValue == tradersRefundDisputesTab) navigation.navigateTo(MainView.class, SupportView.class, RefundClientView.class); else if (newValue == arbitratorTab) navigation.navigateTo(MainView.class, SupportView.class, ArbitratorView.class); else if (newValue == signedOfferTab) navigation.navigateTo(MainView.class, SupportView.class, SignedOfferView.class); else if (newValue == mediatorTab) navigation.navigateTo(MainView.class, SupportView.class, MediatorView.class); else if (newValue == refundAgentTab) navigation.navigateTo(MainView.class, SupportView.class, RefundAgentView.class); }; arbitratorMapChangeListener = change -> updateAgentTabs(); mediatorMapChangeListener = change -> updateAgentTabs(); refundAgentMapChangeListener = change -> updateAgentTabs(); } private void updateAgentTabs() { PubKeyRing myPubKeyRing = keyRing.getPubKeyRing(); boolean isActiveArbitrator = arbitratorManager.getObservableMap().values().stream() .anyMatch(e -> e.getPubKeyRing() != null && e.getPubKeyRing().equals(myPubKeyRing)); // In case a arbitrator has become inactive he still might get disputes from pending trades boolean hasDisputesAsArbitrator = arbitrationManager.getDisputesAsObservableList().stream() .anyMatch(d -> d.getAgentPubKeyRing().equals(myPubKeyRing)); if (isActiveArbitrator || hasDisputesAsArbitrator) { if (arbitratorTab == null) { arbitratorTab = new Tab(); arbitratorTab.setClosable(false); root.getTabs().add(arbitratorTab); } if (signedOfferTab == null) { signedOfferTab = new Tab(); signedOfferTab.setClosable(false); root.getTabs().add(signedOfferTab); } } boolean isActiveMediator = mediatorManager.getObservableMap().values().stream() .anyMatch(e -> e.getPubKeyRing() != null && e.getPubKeyRing().equals(myPubKeyRing)); if (mediatorTab == null) { // In case a mediator has become inactive he still might get disputes from pending trades boolean hasDisputesAsMediator = mediationManager.getDisputesAsObservableList().stream() .anyMatch(d -> d.getAgentPubKeyRing().equals(myPubKeyRing)); if (isActiveMediator || hasDisputesAsMediator) { mediatorTab = new Tab(); mediatorTab.setClosable(false); root.getTabs().add(mediatorTab); } } boolean isActiveRefundAgent = refundAgentManager.getObservableMap().values().stream() .anyMatch(e -> e.getPubKeyRing() != null && e.getPubKeyRing().equals(myPubKeyRing)); if (refundAgentTab == null) { // In case a refundAgent has become inactive he still might get disputes from pending trades boolean hasDisputesAsRefundAgent = refundManager.getDisputesAsObservableList().stream() .anyMatch(d -> d.getAgentPubKeyRing().equals(myPubKeyRing)); if (isActiveRefundAgent || hasDisputesAsRefundAgent) { refundAgentTab = new Tab(); refundAgentTab.setClosable(false); root.getTabs().add(refundAgentTab); } } // We might get that method called before we have the map is filled in the arbitratorManager if (arbitratorTab != null) { arbitratorTab.setText(Res.get("support.tab.ArbitratorsSupportTickets", Res.get("shared.arbitrator"))); } if (signedOfferTab != null) { signedOfferTab.setText(Res.get("support.tab.SignedOffers")); } if (mediatorTab != null) { mediatorTab.setText(Res.get("support.tab.ArbitratorsSupportTickets", Res.get("shared.mediator"))); } if (refundAgentTab != null) { refundAgentTab.setText(Res.get("support.tab.ArbitratorsSupportTickets", Res.get("shared.refundAgentForSupportStaff"))); } } @Override protected void activate() { arbitratorManager.updateMap(); arbitratorManager.getObservableMap().addListener(arbitratorMapChangeListener); mediatorManager.updateMap(); mediatorManager.getObservableMap().addListener(mediatorMapChangeListener); refundAgentManager.updateMap(); refundAgentManager.getObservableMap().addListener(refundAgentMapChangeListener); updateAgentTabs(); root.getSelectionModel().selectedItemProperty().addListener(tabChangeListener); navigation.addListener(navigationListener); if (root.getSelectionModel().getSelectedItem() == tradersMediationDisputesTab) { navigation.navigateTo(MainView.class, SupportView.class, MediationClientView.class); } else if (root.getSelectionModel().getSelectedItem() == tradersArbitrationDisputesTab) { navigation.navigateTo(MainView.class, SupportView.class, ArbitrationClientView.class); } else if (root.getSelectionModel().getSelectedItem() == tradersRefundDisputesTab) { navigation.navigateTo(MainView.class, SupportView.class, RefundClientView.class); } else if (arbitratorTab != null) { navigation.navigateTo(MainView.class, SupportView.class, ArbitratorView.class); } else if (signedOfferTab != null) { navigation.navigateTo(MainView.class, SupportView.class, SignedOfferView.class); } else if (mediatorTab != null) { navigation.navigateTo(MainView.class, SupportView.class, MediatorView.class); } else if (refundAgentTab != null) { navigation.navigateTo(MainView.class, SupportView.class, RefundAgentView.class); } String key = "supportInfo"; if (!DevEnv.isDevMode()) { new Popup().backgroundInfo(Res.get("support.backgroundInfo")) .width(900) .dontShowAgainId(key) .show(); } } @Override protected void deactivate() { arbitratorManager.getObservableMap().removeListener(arbitratorMapChangeListener); mediatorManager.getObservableMap().removeListener(mediatorMapChangeListener); refundAgentManager.getObservableMap().removeListener(refundAgentMapChangeListener); root.getSelectionModel().selectedItemProperty().removeListener(tabChangeListener); navigation.removeListener(navigationListener); currentTab = null; } private void loadView(Class viewClass) { // we want to get activate/deactivate called, so we remove the old view on tab change if (currentTab != null) currentTab.setContent(null); View view = viewLoader.load(viewClass); if (view instanceof MediationClientView) { currentTab = tradersMediationDisputesTab; } else if (view instanceof ArbitrationClientView) { currentTab = tradersArbitrationDisputesTab; } else if (view instanceof RefundClientView) { currentTab = tradersRefundDisputesTab; } else if (view instanceof ArbitratorView) { currentTab = arbitratorTab; } else if (view instanceof SignedOfferView) { currentTab = signedOfferTab; } else if (view instanceof MediatorView) { currentTab = mediatorTab; } else if (view instanceof RefundAgentView) { currentTab = refundAgentTab; } else { currentTab = null; } if (currentTab != null) { currentTab.setContent(view.getRoot()); root.getSelectionModel().select(currentTab); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/support/dispute/DisputeChatPopup.java ================================================ /* * This file is part of haveno. * * 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 haveno. If not, see . */ package haveno.desktop.main.support.dispute; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.main.MainView; import haveno.desktop.main.shared.ChatView; import haveno.desktop.util.CssTheme; import haveno.desktop.util.DisplayUtils; import haveno.core.locale.Res; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeList; import haveno.core.support.dispute.DisputeManager; import haveno.core.support.dispute.DisputeSession; import haveno.core.support.messages.ChatMessage; import haveno.core.user.Preferences; import haveno.core.util.coin.CoinFormatter; import haveno.common.UserThread; import javafx.stage.Modality; import javafx.stage.Stage; import javafx.stage.StageStyle; import javafx.stage.Window; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.MenuButton; import javafx.scene.control.MenuItem; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.StackPane; import javafx.beans.value.ChangeListener; import java.util.Date; import java.util.List; import lombok.Getter; public class DisputeChatPopup { public interface ChatCallback { void onCloseDisputeFromChatWindow(Dispute dispute); void onSendLogsFromChatWindow(Dispute dispute); } private Stage chatPopupStage; protected final DisputeManager> disputeManager; protected final CoinFormatter formatter; protected final Preferences preferences; private final ChatCallback chatCallback; private double chatPopupStageXPosition = -1; private double chatPopupStageYPosition = -1; @Getter private Dispute selectedDispute; DisputeChatPopup(DisputeManager> disputeManager, CoinFormatter formatter, Preferences preferences, ChatCallback chatCallback) { this.disputeManager = disputeManager; this.formatter = formatter; this.preferences = preferences; this.chatCallback = chatCallback; } public boolean isChatShown() { return chatPopupStage != null; } public void closeChat() { if (chatPopupStage != null) chatPopupStage.close(); selectedDispute = null; } public void openChat(Dispute selectedDispute, DisputeSession concreteDisputeSession, String counterpartyName) { closeChat(); this.selectedDispute = selectedDispute; selectedDispute.getChatMessages().forEach(m -> m.setWasDisplayed(true)); disputeManager.requestPersistence(); ChatView chatView = new ChatView(disputeManager, counterpartyName); chatView.setAllowAttachments(true); chatView.setDisplayHeader(false); chatView.initialize(); AnchorPane pane = new AnchorPane(chatView); pane.setPrefSize(760, 500); AnchorPane.setLeftAnchor(chatView, 10d); AnchorPane.setRightAnchor(chatView, 10d); AnchorPane.setTopAnchor(chatView, -20d); AnchorPane.setBottomAnchor(chatView, 10d); pane.getStyleClass().add("dispute-chat-border"); if (selectedDispute.isClosed()) { chatView.display(concreteDisputeSession, null, pane.widthProperty()); } else { if (disputeManager.isAgent(selectedDispute)) { Button closeDisputeButton = new AutoTooltipButton(Res.get("support.closeTicket")); closeDisputeButton.setDefaultButton(true); closeDisputeButton.setOnAction(e -> chatCallback.onCloseDisputeFromChatWindow(selectedDispute)); chatView.display(concreteDisputeSession, closeDisputeButton, pane.widthProperty()); } else { MenuButton menuButton = new MenuButton(Res.get("support.moreButton")); MenuItem menuItem1 = new MenuItem(Res.get("support.uploadTraderChat")); MenuItem menuItem2 = new MenuItem(Res.get("support.sendLogFiles")); menuItem1.setOnAction(e -> doTextAttachment(chatView)); setChatUploadEnabledState(menuItem1); menuItem2.setOnAction(e -> chatCallback.onSendLogsFromChatWindow(selectedDispute)); menuButton.getItems().addAll(menuItem1, menuItem2); menuButton.getStyleClass().add("jfx-button"); menuButton.setStyle("-fx-min-width: 95; -fx-padding: 0 10 0 10;"); chatView.display(concreteDisputeSession, menuButton, pane.widthProperty()); } } chatView.activate(); chatView.scrollToBottom(); chatPopupStage = new Stage(); chatPopupStage.setTitle(Res.get("disputeChat.chatWindowTitle", selectedDispute.getShortTradeId()) + " " + selectedDispute.getRoleString()); StackPane owner = MainView.getRootContainer(); Scene rootScene = owner.getScene(); chatPopupStage.initOwner(rootScene.getWindow()); chatPopupStage.initModality(Modality.NONE); chatPopupStage.initStyle(StageStyle.DECORATED); chatPopupStage.setOnHiding(event -> { chatView.deactivate(); // at close we set all as displayed. While open we ignore updates of the numNewMsg in the list icon. selectedDispute.getChatMessages().forEach(m -> m.setWasDisplayed(true)); disputeManager.requestPersistence(); chatPopupStage = null; }); Scene scene = new Scene(pane); CssTheme.loadSceneStyles(scene, preferences.getCssTheme(), false); scene.addEventHandler(KeyEvent.KEY_RELEASED, ev -> { if (ev.getCode() == KeyCode.ESCAPE) { ev.consume(); chatPopupStage.hide(); } }); chatPopupStage.setScene(scene); chatPopupStage.setOpacity(0); chatPopupStage.show(); ChangeListener xPositionListener = (observable, oldValue, newValue) -> chatPopupStageXPosition = (double) newValue; chatPopupStage.xProperty().addListener(xPositionListener); ChangeListener yPositionListener = (observable, oldValue, newValue) -> chatPopupStageYPosition = (double) newValue; chatPopupStage.yProperty().addListener(yPositionListener); if (chatPopupStageXPosition == -1) { Window rootSceneWindow = rootScene.getWindow(); double titleBarHeight = rootSceneWindow.getHeight() - rootScene.getHeight(); chatPopupStage.setX(Math.round(rootSceneWindow.getX() + (owner.getWidth() - chatPopupStage.getWidth() / 4 * 3))); chatPopupStage.setY(Math.round(rootSceneWindow.getY() + titleBarHeight + (owner.getHeight() - chatPopupStage.getHeight() / 4 * 3))); } else { chatPopupStage.setX(chatPopupStageXPosition); chatPopupStage.setY(chatPopupStageYPosition); } // Delay display to next render frame to avoid that the popup is first quickly displayed in default position // and after a short moment in the correct position UserThread.execute(() -> chatPopupStage.setOpacity(1)); } private void doTextAttachment(ChatView chatView) { disputeManager.findTrade(selectedDispute).ifPresent(t -> { List chatMessages = t.getChatMessages(); if (chatMessages.size() > 0) { StringBuilder stringBuilder = new StringBuilder(); chatMessages.forEach(i -> { boolean isMyMsg = i.isSenderIsTrader(); String metaData = DisplayUtils.formatDateTime(new Date(i.getDate())); if (!i.isSystemMessage()) metaData = (isMyMsg ? "Sent " : "Received ") + metaData + (isMyMsg ? "" : " from Trader"); stringBuilder.append(metaData).append("\n").append(i.getMessage()).append("\n\n"); }); String fileName = selectedDispute.getShortTradeId() + "_" + selectedDispute.getRoleStringForLogFile() + "_TraderChat.txt"; chatView.onAttachText(stringBuilder.toString(), fileName); } }); } private void setChatUploadEnabledState(MenuItem menuItem) { disputeManager.findTrade(selectedDispute).ifPresentOrElse(t -> { menuItem.setDisable(t.getChatMessages().size() == 0); }, () -> { menuItem.setDisable(true); }); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/support/dispute/DisputeView.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.desktop.main.support.dispute; import com.jfoenix.controls.JFXBadge; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import haveno.common.UserThread; import haveno.common.crypto.KeyRing; import haveno.common.crypto.PubKeyRing; import haveno.common.util.Utilities; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.alert.PrivateNotificationManager; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.support.SupportType; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeList; import haveno.core.support.dispute.DisputeManager; import haveno.core.support.dispute.DisputeResult; import haveno.core.support.dispute.DisputeSession; import haveno.core.support.dispute.agent.DisputeAgentLookupMap; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.support.dispute.mediation.MediationManager; import haveno.core.support.messages.ChatMessage; import haveno.core.trade.Contract; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.AutoTooltipTableColumn; import haveno.desktop.components.HyperlinkWithIcon; import haveno.desktop.components.InputTextField; import haveno.desktop.components.PeerInfoIconDispute; import haveno.desktop.components.PeerInfoIconMap; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.ContractWindow; import haveno.desktop.main.overlays.windows.DisputeSummaryWindow; import haveno.desktop.main.overlays.windows.SendLogFilesWindow; import haveno.desktop.main.overlays.windows.SendPrivateNotificationWindow; import haveno.desktop.main.overlays.windows.TradeDetailsWindow; import haveno.desktop.main.overlays.windows.VerifyDisputeResultSignatureWindow; import haveno.desktop.util.DisplayUtils; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.NodeAddress; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.value.ChangeListener; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableRow; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.util.Callback; import javafx.util.Duration; import lombok.Getter; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; import javax.annotation.Nullable; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import static haveno.desktop.util.FormBuilder.getIconForLabel; import static haveno.desktop.util.FormBuilder.getRegularIconButton; public abstract class DisputeView extends ActivatableView implements DisputeChatPopup.ChatCallback { public enum FilterResult { NO_MATCH("No Match"), NO_FILTER("No filter text"), OPEN_DISPUTES("Open disputes"), TRADE_ID("Trade ID"), OPENING_DATE("Opening date"), BUYER_NODE_ADDRESS("Buyer node address"), SELLER_NODE_ADDRESS("Seller node address"), BUYER_ACCOUNT_DETAILS("Buyer account details"), SELLER_ACCOUNT_DETAILS("Seller account details"), DEPOSIT_TX("Deposit tx ID"), PAYOUT_TX("Payout tx ID"), DEL_PAYOUT_TX("Delayed payout tx ID"), RESULT_MESSAGE("Result message"), REASON("Reason"), JSON("Contract as json"); // Used in tooltip at search string to show where the match was found @Getter private final String displayString; FilterResult(String displayString) { this.displayString = displayString; } } protected final DisputeManager> disputeManager; protected final KeyRing keyRing; private final TradeManager tradeManager; protected final CoinFormatter formatter; protected final Preferences preferences; protected final DisputeSummaryWindow disputeSummaryWindow; private final PrivateNotificationManager privateNotificationManager; private final ContractWindow contractWindow; private final TradeDetailsWindow tradeDetailsWindow; private final AccountAgeWitnessService accountAgeWitnessService; private final ArbitratorManager arbitratorManager; private final boolean useDevPrivilegeKeys; protected TableView tableView; private SortedList sortedList; @Getter protected Dispute selectedDispute; private Subscription selectedDisputeSubscription; protected FilteredList filteredList; protected InputTextField filterTextField; private ChangeListener filterTextFieldListener; protected AutoTooltipButton sigCheckButton, reOpenButton, closeButton, sendPrivateNotificationButton, reportButton, fullReportButton; private final Map> disputeChatMessagesListeners = new HashMap<>(); @Nullable private ListChangeListener disputesListener; // Only set in mediation cases protected Label alertIconLabel; protected TableColumn stateColumn; private Map> listenerByDispute = new HashMap<>(); private Map chatButtonByDispute = new HashMap<>(); private Map chatBadgeByDispute = new HashMap<>(); private Map newBadgeByDispute = new HashMap<>(); @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") private final PeerInfoIconMap avatarMap = new PeerInfoIconMap(); protected DisputeChatPopup chatPopup; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// public DisputeView(DisputeManager> disputeManager, KeyRing keyRing, TradeManager tradeManager, CoinFormatter formatter, Preferences preferences, DisputeSummaryWindow disputeSummaryWindow, PrivateNotificationManager privateNotificationManager, ContractWindow contractWindow, TradeDetailsWindow tradeDetailsWindow, AccountAgeWitnessService accountAgeWitnessService, ArbitratorManager arbitratorManager, boolean useDevPrivilegeKeys) { this.disputeManager = disputeManager; this.keyRing = keyRing; this.tradeManager = tradeManager; this.formatter = formatter; this.preferences = preferences; this.disputeSummaryWindow = disputeSummaryWindow; this.privateNotificationManager = privateNotificationManager; this.contractWindow = contractWindow; this.tradeDetailsWindow = tradeDetailsWindow; this.accountAgeWitnessService = accountAgeWitnessService; this.arbitratorManager = arbitratorManager; this.useDevPrivilegeKeys = useDevPrivilegeKeys; chatPopup = new DisputeChatPopup(disputeManager, formatter, preferences, this); } @Override public void initialize() { filterTextField = new InputTextField(); filterTextField.setPromptText(Res.get("shared.filter")); Tooltip tooltip = new Tooltip(); tooltip.setShowDelay(Duration.millis(100)); tooltip.setShowDuration(Duration.seconds(10)); filterTextField.setTooltip(tooltip); filterTextFieldListener = (observable, oldValue, newValue) -> applyFilteredListPredicate(filterTextField.getText()); HBox.setHgrow(filterTextField, Priority.ALWAYS); alertIconLabel = new Label(); Text icon = getIconForLabel(MaterialDesignIcon.ALERT_CIRCLE_OUTLINE, "2em", alertIconLabel); icon.getStyleClass().add("alert-icon"); HBox.setMargin(alertIconLabel, new Insets(4, 0, 0, 10)); alertIconLabel.setMouseTransparent(false); alertIconLabel.setVisible(false); alertIconLabel.setManaged(false); reOpenButton = new AutoTooltipButton(Res.get("support.reOpenButton.label")); reOpenButton.setDisable(true); reOpenButton.setVisible(false); reOpenButton.setManaged(false); HBox.setHgrow(reOpenButton, Priority.NEVER); reOpenButton.setOnAction(e -> { reOpenDisputeFromButton(); }); closeButton = new AutoTooltipButton(Res.get("support.closeTicket")); closeButton.setDisable(true); closeButton.setVisible(false); closeButton.setManaged(false); HBox.setHgrow(closeButton, Priority.NEVER); closeButton.setOnAction(e -> { closeDisputeFromButton(); }); sendPrivateNotificationButton = new AutoTooltipButton(Res.get("support.sendNotificationButton.label")); sendPrivateNotificationButton.setDisable(true); sendPrivateNotificationButton.setVisible(false); sendPrivateNotificationButton.setManaged(false); HBox.setHgrow(sendPrivateNotificationButton, Priority.NEVER); sendPrivateNotificationButton.setOnAction(e -> { sendPrivateNotification(); }); reportButton = new AutoTooltipButton(Res.get("support.reportButton.label")); reportButton.setVisible(false); reportButton.setManaged(false); HBox.setHgrow(reportButton, Priority.NEVER); reportButton.setOnAction(e -> { showCompactReport(); }); fullReportButton = new AutoTooltipButton(Res.get("support.fullReportButton.label")); fullReportButton.setVisible(false); fullReportButton.setManaged(false); HBox.setHgrow(fullReportButton, Priority.NEVER); fullReportButton.setOnAction(e -> { showFullReport(); }); sigCheckButton = new AutoTooltipButton(Res.get("support.sigCheck.button")); HBox.setHgrow(sigCheckButton, Priority.NEVER); sigCheckButton.setOnAction(e -> { new VerifyDisputeResultSignatureWindow(arbitratorManager).show(); }); Pane spacer = new Pane(); HBox.setHgrow(spacer, Priority.ALWAYS); HBox filterBox = new HBox(); filterBox.setSpacing(5); filterBox.getChildren().addAll(filterTextField, alertIconLabel, spacer, reOpenButton, closeButton, sendPrivateNotificationButton, reportButton, fullReportButton, sigCheckButton); VBox.setVgrow(filterBox, Priority.NEVER); tableView = new TableView<>(); GUIUtil.applyTableStyle(tableView); VBox.setVgrow(tableView, Priority.SOMETIMES); tableView.setMinHeight(150); root.getChildren().addAll(filterBox, tableView); setupTable(); } @Override protected void activate() { filterTextField.textProperty().addListener(filterTextFieldListener); ObservableList disputesAsObservableList = disputeManager.getDisputesAsObservableList(); filteredList = new FilteredList<>(disputesAsObservableList); applyFilteredListPredicate(filterTextField.getText()); sortedList = new SortedList<>(filteredList); sortedList.comparatorProperty().bind(tableView.comparatorProperty()); tableView.setItems(sortedList); // double-click on a row opens chat window tableView.setRowFactory( tv -> { TableRow row = new TableRow<>(); row.setOnMouseClicked(event -> { if (event.getClickCount() == 2 && (!row.isEmpty())) { if (!canViewChatMessages(row.getItem())) return; openChat(row.getItem()); } }); return row; }); selectedDisputeSubscription = EasyBind.subscribe(tableView.getSelectionModel().selectedItemProperty(), this::onSelectDispute); Dispute selectedItem = tableView.getSelectionModel().getSelectedItem(); if (selectedItem != null) tableView.getSelectionModel().select(selectedItem); else if (sortedList.size() > 0) tableView.getSelectionModel().select(0); GUIUtil.requestFocus(tableView); } @Override protected void deactivate() { filterTextField.textProperty().removeListener(filterTextFieldListener); sortedList.comparatorProperty().unbind(); selectedDisputeSubscription.unsubscribe(); } /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// // Reopen feature is only use in mediation from both mediator and traders protected void setupReOpenDisputeListener() { disputesListener = c -> { c.next(); if (c.wasAdded()) { onDisputesAdded(c.getAddedSubList()); } else if (c.wasRemoved()) { onDisputesRemoved(c.getRemoved()); } }; } // Reopen feature is only use in mediation from both mediator and traders protected void activateReOpenDisputeListener() { // Register listeners on all disputes for potential re-opening onDisputesAdded(disputeManager.getDisputesAsObservableList()); disputeManager.getDisputesAsObservableList().addListener(disputesListener); disputeManager.getDisputesAsObservableList().forEach(dispute -> { if (dispute.isClosed()) { ObservableList chatMessages = dispute.getChatMessages(); // If last message is not a result message we re-open as we might have received a new message from the // trader/mediator/arbitrator who has reopened the case if (!chatMessages.isEmpty() && !chatMessages.get(chatMessages.size() - 1).isResultMessage(dispute) && dispute.unreadMessageCount(senderFlag()) > 0) { onSelectDispute(dispute); reOpenDispute(); } } }); } // Reopen feature is only use in mediation from both mediator and traders protected void deactivateReOpenDisputeListener() { onDisputesRemoved(disputeManager.getDisputesAsObservableList()); disputeManager.getDisputesAsObservableList().removeListener(disputesListener); } protected abstract SupportType getType(); protected abstract DisputeSession getConcreteDisputeChatSession(Dispute dispute); protected abstract boolean senderFlag(); // implemented in the agent / client views protected void applyFilteredListPredicate(String filterString) { AtomicReference filterResult = new AtomicReference<>(FilterResult.NO_FILTER); filteredList.setPredicate(dispute -> { filterResult.set(getFilterResult(dispute, filterString)); return filterResult.get() != FilterResult.NO_MATCH; }); if (filterResult.get() == FilterResult.NO_MATCH) { filterTextField.getTooltip().setText("No matches found"); } else if (filterResult.get() == FilterResult.NO_FILTER) { filterTextField.getTooltip().setText("No filter applied"); } else if (filterResult.get() == FilterResult.OPEN_DISPUTES) { filterTextField.getTooltip().setText("Show all open disputes"); } else { filterTextField.getTooltip().setText("Data matching filter string: " + filterResult.get().getDisplayString()); } } protected FilterResult getFilterResult(Dispute dispute, String filterTerm) { String filter = filterTerm.toLowerCase(); if (filter.isEmpty()) { return FilterResult.NO_FILTER; } // For open filter we do not want to continue further as json data would cause a match if (filter.equalsIgnoreCase("open")) { return !dispute.isClosed() || dispute.unreadMessageCount(senderFlag()) > 0 ? FilterResult.OPEN_DISPUTES : FilterResult.NO_MATCH; } if (dispute.getTradeId().toLowerCase().contains(filter)) { return FilterResult.TRADE_ID; } if (DisplayUtils.formatDate(dispute.getOpeningDate()).toLowerCase().contains(filter)) { return FilterResult.OPENING_DATE; } if (dispute.getContract().getBuyerNodeAddress().getFullAddress().contains(filter)) { return FilterResult.BUYER_NODE_ADDRESS; } if (dispute.getContract().getSellerNodeAddress().getFullAddress().contains(filter)) { return FilterResult.SELLER_NODE_ADDRESS; } if (dispute.getBuyerPaymentAccountPayload() != null && dispute.getBuyerPaymentAccountPayload().getPaymentDetails().toLowerCase().contains(filter)) { return FilterResult.BUYER_ACCOUNT_DETAILS; } if (dispute.getSellerPaymentAccountPayload() != null && dispute.getSellerPaymentAccountPayload().getPaymentDetails().toLowerCase().contains(filter)) { return FilterResult.SELLER_ACCOUNT_DETAILS; } if (dispute.getPayoutTxId() != null && dispute.getPayoutTxId().contains(filter)) { return FilterResult.PAYOUT_TX; } if (dispute.getDelayedPayoutTxId() != null && dispute.getDelayedPayoutTxId().contains(filter)) { return FilterResult.DEL_PAYOUT_TX; } DisputeResult disputeResult = dispute.getDisputeResultProperty().get(); if (disputeResult != null) { ChatMessage chatMessage = disputeResult.getChatMessage(); if (chatMessage != null && chatMessage.getMessage().toLowerCase().contains(filter)) { return FilterResult.RESULT_MESSAGE; } if (disputeResult.getReason().name().toLowerCase().contains(filter)) { return FilterResult.REASON; } } if (dispute.getContractAsJson().toLowerCase().contains(filter)) { return FilterResult.JSON; } return FilterResult.NO_MATCH; } // a derived version in the ClientView for users pops up an "Are you sure" box first. // this version includes the sending of an automatic message to the user, see addMediationReOpenedMessage protected void reOpenDisputeFromButton() { reOpenDispute(); disputeManager.addMediationReOpenedMessage(selectedDispute, false); } // only applicable to traders // only allow them to close the dispute if the trade is paid out // the reason for having this is that sometimes traders end up with closed disputes that are not "closed" @pazza protected void closeDisputeFromButton() { Optional tradeOptional = disputeManager.findTrade(selectedDispute); if (tradeOptional.isPresent() && tradeOptional.get().getPayoutTxId() != null && tradeOptional.get().getPayoutTxId().length() > 0) { selectedDispute.setIsClosed(); disputeManager.requestPersistence(); onSelectDispute(selectedDispute); } else { new Popup().warning(Res.get("support.warning.traderCloseOwnDisputeWarning")).show(); } } protected void handleOnProcessDispute(Dispute dispute) { // overridden by clients that use it (dispute agents) } protected void reOpenDispute() { if (selectedDispute != null && selectedDispute.isClosed()) { selectedDispute.reOpen(); handleOnProcessDispute(selectedDispute); disputeManager.requestPersistence(); onSelectDispute(selectedDispute); } } /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// protected void onOpenContract(Dispute dispute) { dispute.setDisputeSeen(senderFlag()); contractWindow.show(dispute); } private void onSelectDispute(Dispute dispute) { if (dispute == null) { selectedDispute = null; } else if (selectedDispute != dispute) { selectedDispute = dispute; } reOpenButton.setDisable(selectedDispute == null || !selectedDispute.isClosed()); closeButton.setDisable(selectedDispute == null || selectedDispute.isClosed()); sendPrivateNotificationButton.setDisable(selectedDispute == null); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// // Reopen feature is only use in mediation from both mediator and traders private void onDisputesAdded(List addedDisputes) { addedDisputes.forEach(dispute -> { ListChangeListener listener = c -> { c.next(); if (c.wasAdded()) { c.getAddedSubList().forEach(chatMessage -> { if (dispute.isClosed()) { if (chatMessage.isResultMessage(dispute)) { onSelectDispute(null); } else { onSelectDispute(dispute); reOpenDispute(); } } }); } // We never remove chat messages so no remove listener }; dispute.getChatMessages().addListener(listener); disputeChatMessagesListeners.put(dispute.getId(), listener); }); } // Reopen feature is only use in mediation from both mediator and traders private void onDisputesRemoved(List removedDisputes) { removedDisputes.forEach(dispute -> { String id = dispute.getId(); if (disputeChatMessagesListeners.containsKey(id)) { ListChangeListener listener = disputeChatMessagesListeners.get(id); dispute.getChatMessages().removeListener(listener); disputeChatMessagesListeners.remove(id); } }); } private void sendPrivateNotification() { if (selectedDispute != null) { PubKeyRing pubKeyRing = selectedDispute.getTraderPubKeyRing(); NodeAddress nodeAddress; Contract contract = selectedDispute.getContract(); if (pubKeyRing.equals(contract.getBuyerPubKeyRing())) { nodeAddress = contract.getBuyerNodeAddress(); } else { nodeAddress = contract.getSellerNodeAddress(); } new SendPrivateNotificationWindow( privateNotificationManager, pubKeyRing, nodeAddress, useDevPrivilegeKeys ).show(); } } private void showCompactReport() { Map> map = new HashMap<>(); Map> disputesByReason = new HashMap<>(); disputeManager.getDisputesAsObservableList().forEach(dispute -> { String tradeId = dispute.getTradeId(); List list; if (!map.containsKey(tradeId)) map.put(tradeId, new ArrayList<>()); list = map.get(tradeId); list.add(dispute); }); List> allDisputes = new ArrayList<>(); map.forEach((key, value) -> allDisputes.add(value)); allDisputes.sort(Comparator.comparing(o -> !o.isEmpty() ? o.get(0).getOpeningDate() : new Date(0))); StringBuilder stringBuilder = new StringBuilder(); StringBuilder csvStringBuilder = new StringBuilder(); csvStringBuilder.append("Dispute nr").append(";") .append("Closed during cycle").append(";") .append("Status").append(";") .append("Trade date").append(";") .append("Trade ID").append(";") .append("Offer version").append(";") .append("Opening date").append(";") .append("Close date").append(";") .append("Duration").append(";") .append("Currency").append(";") .append("Trade amount").append(";") .append("Payment method").append(";") .append("Buyer account details").append(";") .append("Seller account details").append(";") .append("Buyer address").append(";") .append("Seller address").append(";") .append("Buyer security deposit").append(";") .append("Seller security deposit").append(";") .append("Dispute opened by").append(";") .append("Payout to buyer").append(";") .append("Payout to seller").append(";") .append("Winner").append(";") .append("Reason").append(";") .append("Summary notes").append(";") .append("Summary notes (other trader)"); SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy MM dd HH:mm:ss"); AtomicInteger disputeIndex = new AtomicInteger(); allDisputes.forEach(disputesPerTrade -> { if (disputesPerTrade.size() > 0) { Dispute firstDispute = disputesPerTrade.get(0); Date openingDate = firstDispute.getOpeningDate(); Contract contract = firstDispute.getContract(); String buyersRole = contract.isBuyerMakerAndSellerTaker() ? "Buyer as maker" : "Buyer as taker"; String sellersRole = contract.isBuyerMakerAndSellerTaker() ? "Seller as taker" : "Seller as maker"; String opener = firstDispute.isDisputeOpenerIsBuyer() ? buyersRole : sellersRole; DisputeResult disputeResult = firstDispute.getDisputeResultProperty().get(); String winner = disputeResult != null && disputeResult.getWinner() == DisputeResult.Winner.BUYER ? "Buyer" : "Seller"; String buyerPayoutAmount = disputeResult != null ? HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmountBeforeCost(), true) : ""; String sellerPayoutAmount = disputeResult != null ? HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmountBeforeCost(), true) : ""; int index = disputeIndex.incrementAndGet(); String tradeDateString = dateFormatter.format(firstDispute.getTradeDate()); String openingDateString = dateFormatter.format(openingDate); // Index we display starts with 1 not with 0 int cycleIndex = 0; stringBuilder.append("\n").append("Dispute nr.: ").append(index).append("\n"); if (cycleIndex > 0) { stringBuilder.append("Closed during cycle: ").append(cycleIndex).append("\n"); } stringBuilder.append("Trade date: ").append(tradeDateString) .append("\n") .append("Opening date: ").append(openingDateString) .append("\n"); String tradeId = firstDispute.getTradeId(); csvStringBuilder.append("\n").append(index).append(";"); if (cycleIndex > 0) { csvStringBuilder.append(cycleIndex).append(";"); } else { csvStringBuilder.append(";"); } csvStringBuilder.append(firstDispute.isClosed() ? "Closed" : "Open").append(";") .append(tradeDateString).append(";") .append(firstDispute.getShortTradeId()).append(";") .append(tradeId, tradeId.length() - 3, tradeId.length()).append(";") .append(openingDateString).append(";"); String summaryNotes = ""; if (disputeResult != null) { Date closeDate = disputeResult.getCloseDate(); long duration = closeDate.getTime() - openingDate.getTime(); String closeDateString = dateFormatter.format(closeDate); String durationAsWords = FormattingUtils.formatDurationAsWords(duration); stringBuilder.append("Close date: ").append(closeDateString).append("\n") .append("Dispute duration: ").append(durationAsWords).append("\n"); csvStringBuilder.append(closeDateString).append(";") .append(durationAsWords).append(";"); } else { csvStringBuilder.append(";").append(";"); } String paymentMethod = Res.get(contract.getPaymentMethodId()); String currency = CurrencyUtil.getNameAndCode(contract.getOfferPayload().getCurrencyCode()); String tradeAmount = HavenoUtils.formatXmr(contract.getTradeAmount(), true); String buyerDeposit = HavenoUtils.formatXmr(contract.getOfferPayload().getBuyerSecurityDepositForTradeAmount(contract.getTradeAmount()), true); String sellerDeposit = HavenoUtils.formatXmr(contract.getOfferPayload().getSellerSecurityDepositForTradeAmount(contract.getTradeAmount()), true); stringBuilder.append("Payment method: ") .append(paymentMethod) .append("\n") .append("Currency: ") .append(currency) .append("\n") .append("Trade amount: ") .append(tradeAmount) .append("\n") .append("Buyer/seller security deposit %: ") .append(buyerDeposit) .append("/") .append(sellerDeposit) .append("\n") .append("Dispute opened by: ") .append(opener) .append("\n") .append("Payout to buyer/seller (winner): ") .append(buyerPayoutAmount).append("/") .append(sellerPayoutAmount).append(" (") .append(winner) .append(")\n"); String buyerPaymentAccountPayload = firstDispute.getBuyerPaymentAccountPayload() == null ? null : Utilities.toTruncatedString( firstDispute.getBuyerPaymentAccountPayload().getPaymentDetails(). replace("\n", " ").replace(";", "."), 100); String sellerPaymentAccountPayload = firstDispute.getSellerPaymentAccountPayload() == null ? null : Utilities.toTruncatedString( firstDispute.getSellerPaymentAccountPayload().getPaymentDetails() .replace("\n", " ").replace(";", "."), 100); String buyerNodeAddress = contract.getBuyerNodeAddress().getFullAddress(); String sellerNodeAddress = contract.getSellerNodeAddress().getFullAddress(); csvStringBuilder.append(currency).append(";") .append(tradeAmount.replace(" BTC", "")).append(";") .append(paymentMethod).append(";") .append(buyerPaymentAccountPayload).append(";") .append(sellerPaymentAccountPayload).append(";") .append(buyerNodeAddress.replace(".onion:9999", "")).append(";") .append(sellerNodeAddress.replace(".onion:9999", "")).append(";") .append(buyerDeposit.replace(" BTC", "")).append(";") .append(sellerDeposit.replace(" BTC", "")).append(";") .append(opener).append(";") .append(buyerPayoutAmount.replace(" BTC", "")).append(";") .append(sellerPayoutAmount.replace(" BTC", "")).append(";") .append(winner).append(";"); if (disputeResult != null) { DisputeResult.Reason reason = disputeResult.getReason(); if (firstDispute.disputeResultProperty().get().getReason() != null) { disputesByReason.putIfAbsent(reason.name(), new ArrayList<>()); disputesByReason.get(reason.name()).add(firstDispute); stringBuilder.append("Reason: ") .append(reason.name()) .append("\n"); csvStringBuilder.append(reason.name()).append(";"); } else { csvStringBuilder.append(";"); } summaryNotes = disputeResult.getSummaryNotesProperty().get(); stringBuilder.append("Summary notes: ").append(summaryNotes).append("\n"); csvStringBuilder.append(summaryNotes).append(";"); } else { csvStringBuilder.append(";"); } // We might have a different summary notes at second trader. Only if it // is different we show it. if (disputesPerTrade.size() > 1) { Dispute dispute1 = disputesPerTrade.get(1); DisputeResult disputeResult1 = dispute1.getDisputeResultProperty().get(); if (disputeResult1 != null) { String summaryNotes1 = disputeResult1.getSummaryNotesProperty().get(); if (!summaryNotes1.equals(summaryNotes)) { stringBuilder.append("Summary notes (different message to other trader was used): ").append(summaryNotes1).append("\n"); csvStringBuilder.append(summaryNotes1).append(";"); } else { csvStringBuilder.append(";"); } } } } }); stringBuilder.append("\n").append("Summary of reasons for disputes: ").append("\n"); disputesByReason.forEach((k, v) -> { stringBuilder.append(k).append(": ").append(v.size()).append("\n"); }); String message = stringBuilder.toString(); new Popup().headLine("Report for " + allDisputes.size() + " disputes") .maxMessageLength(500) .information(message) .width(1200) .actionButtonText("Copy to clipboard") .onAction(() -> Utilities.copyToClipboard(message)) .secondaryActionButtonText("Copy as csv data") .onSecondaryAction(() -> Utilities.copyToClipboard(csvStringBuilder.toString())) .show(); } private void showFullReport() { Map> map = new HashMap<>(); disputeManager.getDisputesAsObservableList().forEach(dispute -> { String tradeId = dispute.getTradeId(); List list; if (!map.containsKey(tradeId)) map.put(tradeId, new ArrayList<>()); list = map.get(tradeId); list.add(dispute); }); List> disputeGroups = new ArrayList<>(); map.forEach((key, value) -> disputeGroups.add(value)); disputeGroups.sort(Comparator.comparing(o -> !o.isEmpty() ? o.get(0).getOpeningDate() : new Date(0))); StringBuilder stringBuilder = new StringBuilder(); // We don't translate that as it is not intended for the public disputeGroups.forEach(disputeGroup -> { Dispute dispute0 = disputeGroup.get(0); stringBuilder.append("##########################################################################################/\n") .append("## Trade ID: ") .append(dispute0.getTradeId()) .append("\n") .append("## Date: ") .append(DisplayUtils.formatDateTime(dispute0.getOpeningDate())) .append("\n") .append("## Is support ticket: ") .append(dispute0.isSupportTicket()) .append("\n"); if (dispute0.disputeResultProperty().get() != null && dispute0.disputeResultProperty().get().getReason() != null) { stringBuilder.append("## Reason: ") .append(dispute0.disputeResultProperty().get().getReason()) .append("\n"); } stringBuilder.append("##########################################################################################/\n") .append("\n"); disputeGroup.forEach(dispute -> { stringBuilder .append("*******************************************************************************************\n") .append("** Trader's ID: ") .append(dispute.getTraderId()) .append("\n*******************************************************************************************\n") .append("\n"); dispute.getChatMessages().forEach(m -> { String role = m.isSenderIsTrader() ? ">> Trader's msg: " : "<< Arbitrator's msg: "; stringBuilder.append(role) .append(m.getMessage()) .append("\n"); }); stringBuilder.append("\n"); }); stringBuilder.append("\n"); }); String message = stringBuilder.toString(); // We don't translate that as it is not intended for the public new Popup().headLine("Detailed text dump for " + disputeGroups.size() + " disputes") .maxMessageLength(1000) .information(message) .width(1200) .actionButtonText("Copy to clipboard") .onAction(() -> Utilities.copyToClipboard(message)) .show(); } /////////////////////////////////////////////////////////////////////////////////////////// // Table /////////////////////////////////////////////////////////////////////////////////////////// protected void setupTable() { tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); Label placeholder = new AutoTooltipLabel(Res.get("support.noTickets")); placeholder.setWrapText(true); tableView.setPlaceholder(placeholder); tableView.getSelectionModel().clearSelection(); tableView.getColumns().add(getContractColumn()); maybeAddProcessColumnsForAgent(); // agent view prefers action buttons on the left TableColumn dateColumn = getDateColumn(); tableView.getColumns().add(dateColumn); TableColumn tradeIdColumn = getTradeIdColumn(); tableView.getColumns().add(tradeIdColumn); TableColumn buyerOnionAddressColumn = getBuyerOnionAddressColumn(); tableView.getColumns().add(buyerOnionAddressColumn); TableColumn sellerOnionAddressColumn = getSellerOnionAddressColumn(); tableView.getColumns().add(sellerOnionAddressColumn); TableColumn marketColumn = getMarketColumn(); tableView.getColumns().add(marketColumn); tableView.getColumns().add(getRoleColumn()); maybeAddAgentColumn(); stateColumn = getStateColumn(); tableView.getColumns().add(stateColumn); // client view has the chat button to the right maybeAddChatColumnForClient(); tradeIdColumn.setComparator(Comparator.comparing(Dispute::getTradeId)); dateColumn.setComparator(Comparator.comparing(Dispute::getOpeningDate)); buyerOnionAddressColumn.setComparator(Comparator.comparing(this::getBuyerOnionAddressColumnLabel)); sellerOnionAddressColumn.setComparator(Comparator.comparing(this::getSellerOnionAddressColumnLabel)); marketColumn.setComparator((o1, o2) -> CurrencyUtil.getCurrencyPair(o1.getContract().getOfferPayload().getCurrencyCode()).compareTo(o2.getContract().getOfferPayload().getCurrencyCode())); stateColumn.setComparator(Comparator.comparing(this::getDisputeStateText)); dateColumn.setSortType(TableColumn.SortType.DESCENDING); tableView.getSortOrder().add(dateColumn); } protected void maybeAddProcessColumnsForAgent() { // Only relevant client views will impl it } protected void maybeAddChatColumnForClient() { // Only relevant client views will impl it } protected void maybeAddAgentColumn() { // Only relevant client views will impl it } // Relevant client views will override that protected NodeAddress getAgentNodeAddress(Contract contract) { return null; } private TableColumn getContractColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("shared.details")) { { setMaxWidth(80); setMinWidth(65); setSortable(false); } }; column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final Dispute item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { Button button = getRegularIconButton(MaterialDesignIcon.INFORMATION_OUTLINE); JFXBadge badge = new JFXBadge(new Label(""), Pos.BASELINE_RIGHT); badge.setPosition(Pos.TOP_RIGHT); badge.setVisible(item.isNew()); badge.setText("New"); badge.getStyleClass().add("new"); newBadgeByDispute.put(item.getId(), badge); HBox hBox = new HBox(button, badge); setGraphic(hBox); button.setOnAction(e -> { tableView.getSelectionModel().select(this.getIndex()); onOpenContract(item); item.setDisputeSeen(senderFlag()); badge.setVisible(item.isNew()); }); } else { setGraphic(null); } } }; } }); return column; } protected TableColumn getProcessColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("support.process")) { { setMaxWidth(50); setMinWidth(50); getStyleClass().addAll("avatar-column"); setSortable(false); } }; column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final Dispute item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { Button button = getRegularIconButton(MaterialDesignIcon.GAVEL); button.setOnAction(e -> { tableView.getSelectionModel().select(this.getIndex()); handleOnProcessDispute(item); item.setDisputeSeen(senderFlag()); newBadgeByDispute.get(item.getId()).setVisible(item.isNew()); }); HBox hBox = new HBox(button); hBox.setAlignment(Pos.CENTER); setGraphic(hBox); } else { setGraphic(null); } } }; } }); return column; } protected TableColumn getChatColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("support.chat")) { { setMaxWidth(40); setMinWidth(40); getStyleClass().addAll("avatar-column"); setSortable(false); } }; column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { Subscription subscription; @Override public void updateItem(final Dispute item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { if (subscription != null) { subscription.unsubscribe(); subscription = null; } String id = item.getId(); Button button; if (!chatButtonByDispute.containsKey(id)) { button = FormBuilder.getIconButton(MaterialDesignIcon.COMMENT_MULTIPLE_OUTLINE); chatButtonByDispute.put(id, button); button.setTooltip(new Tooltip(Res.get("tradeChat.openChat"))); } else { button = chatButtonByDispute.get(id); } JFXBadge chatBadge; if (!chatBadgeByDispute.containsKey(id)) { chatBadge = new JFXBadge(button); chatBadgeByDispute.put(id, chatBadge); chatBadge.setPosition(Pos.TOP_RIGHT); } else { chatBadge = chatBadgeByDispute.get(id); } button.setOnAction(e -> { tableView.getSelectionModel().select(this.getIndex()); openChat(item); }); if (!listenerByDispute.containsKey(id)) { ListChangeListener listener = c -> updateChatMessageCount(item, chatBadge); listenerByDispute.put(id, listener); item.getChatMessages().addListener(listener); } // subscribe to trade's dispute state Trade trade = tradeManager.getTrade(item.getTradeId()); if (trade == null) log.warn("Dispute's trade is null for trade {}", item.getTradeId()); else subscription = EasyBind.subscribe(trade.disputeStateProperty(), disputeState -> { chatBadge.setDisable(!canViewChatMessages(item)); updateChatMessageCount(item, chatBadge); }); updateChatMessageCount(item, chatBadge); setGraphic(chatBadge); } else { setGraphic(null); if (subscription != null) { subscription.unsubscribe(); subscription = null; } } } }; } }); return column; } private TableColumn getDateColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("shared.date")) { { setMinWidth(100); setPrefWidth(150); } }; column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final Dispute item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) setText(DisplayUtils.formatDateTime(item.getOpeningDate())); else setText(""); } }; } }); return column; } private TableColumn getTradeIdColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("shared.tradeId")) { { setMinWidth(50); setPrefWidth(100); } }; column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { private HyperlinkWithIcon field; @Override public void updateItem(final Dispute item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { Optional tradeOptional = tradeManager.getOpenTrade(item.getTradeId()); if (tradeOptional.isPresent()) { field = new HyperlinkWithIcon(item.getShortTradeId()); field.setMouseTransparent(false); field.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails"))); field.setOnAction(event -> tradeDetailsWindow.show(tradeOptional.get())); setGraphic(field); setText(""); } else { setText(item.getShortTradeId()); setGraphic(null); if (field != null) field.setOnAction(null); } } else { setGraphic(null); setText(""); if (field != null) field.setOnAction(null); } } }; } }); return column; } private TableColumn getBuyerOnionAddressColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("support.buyerAddress")) { { setMinWidth(160); } }; column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final Dispute item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setText(getBuyerOnionAddressColumnLabel(item)); PeerInfoIconDispute peerInfoIconDispute = createAvatar(tableRowProperty().get().getIndex(), item, true); setGraphic(peerInfoIconDispute); } else { setText(""); setGraphic(null); } } }; } }); return column; } private TableColumn getSellerOnionAddressColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("support.sellerAddress")) { { setMinWidth(160); } }; column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final Dispute item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setText(getSellerOnionAddressColumnLabel(item)); PeerInfoIconDispute peerInfoIconDispute = createAvatar(tableRowProperty().get().getIndex(), item, false); setGraphic(peerInfoIconDispute); } else { setText(""); setGraphic(null); } } }; } }); return column; } private String getBuyerOnionAddressColumnLabel(Dispute item) { Contract contract = item.getContract(); if (contract != null) { NodeAddress buyerNodeAddress = contract.getBuyerNodeAddress(); if (buyerNodeAddress != null) { String nrOfDisputes = disputeManager.getNrOfDisputes(true, contract); long accountAge = accountAgeWitnessService.getAccountAge(item.getBuyerPaymentAccountPayload(), contract.getBuyerPubKeyRing()); String age = DisplayUtils.formatAccountAge(accountAge); String postFix = CurrencyUtil.isTraditionalCurrency(item.getContract().getOfferPayload().getCurrencyCode()) ? " / " + age : ""; return buyerNodeAddress.getAddressForDisplay() + " (" + nrOfDisputes + postFix + ")"; } else return Res.get("shared.na"); } else { return Res.get("shared.na"); } } private String getSellerOnionAddressColumnLabel(Dispute item) { Contract contract = item.getContract(); if (contract != null) { NodeAddress sellerNodeAddress = contract.getSellerNodeAddress(); if (sellerNodeAddress != null) { String nrOfDisputes = disputeManager.getNrOfDisputes(false, contract); long accountAge = accountAgeWitnessService.getAccountAge(item.getSellerPaymentAccountPayload(), contract.getSellerPubKeyRing()); String age = DisplayUtils.formatAccountAge(accountAge); String postFix = CurrencyUtil.isTraditionalCurrency(item.getContract().getOfferPayload().getCurrencyCode()) ? " / " + age : ""; return sellerNodeAddress.getAddressForDisplay() + " (" + nrOfDisputes + postFix + ")"; } else return Res.get("shared.na"); } else { return Res.get("shared.na"); } } private TableColumn getMarketColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("shared.market")) { { setMinWidth(80); } }; column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final Dispute item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) setText(CurrencyUtil.getCurrencyPair(item.getContract().getOfferPayload().getCurrencyCode())); else setText(""); } }; } }); return column; } private TableColumn getRoleColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("support.role")) { { setMinWidth(130); } }; column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final Dispute item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setText(item.getRoleString()); } else { setText(""); } } }; } }); return column; } protected TableColumn getAgentColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("support.agent")) { { setMinWidth(70); } }; column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { @Override public void updateItem(final Dispute item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { NodeAddress agentNodeAddress = getAgentNodeAddress(item.getContract()); if (agentNodeAddress == null) { setText(Res.get("shared.na")); return; } String MatrixUserName = DisputeAgentLookupMap.getMatrixUserName(agentNodeAddress.getFullAddress()); setText(MatrixUserName); } else { setText(""); } } }; } }); return column; } private TableColumn getStateColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("support.state")) { { setMinWidth(50); } }; column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call(TableColumn column) { return new TableCell<>() { ReadOnlyBooleanProperty closedProperty; ChangeListener listener; Subscription subscription; @Override public void updateItem(final Dispute item, boolean empty) { super.updateItem(item, empty); UserThread.execute(() -> { if (item != null && !empty) { if (closedProperty != null) closedProperty.removeListener(listener); if (subscription != null) { subscription.unsubscribe(); subscription = null; } listener = (observable, oldValue, newValue) -> { setText(getDisputeStateText(item)); if (getTableRow() != null) getTableRow().setOpacity(newValue && item.getBadgeCountProperty().get() == 0 ? 0.4 : 1); if (item.isClosed() && item == chatPopup.getSelectedDispute()) chatPopup.closeChat(); // close the chat popup when the associated ticket is closed }; closedProperty = item.isClosedProperty(); closedProperty.addListener(listener); boolean isClosed = item.isClosed(); setText(getDisputeStateText(item)); if (getTableRow() != null) getTableRow().setOpacity(isClosed && item.getBadgeCountProperty().get() == 0 ? 0.4 : 1); // subscribe to trade's dispute state Trade trade = tradeManager.getTrade(item.getTradeId()); if (trade == null) log.warn("Dispute's trade is null for trade {}", item.getTradeId()); else subscription = EasyBind.subscribe(trade.disputeStateProperty(), disputeState -> setText(getDisputeStateText(item))); } else { if (closedProperty != null) { closedProperty.removeListener(listener); closedProperty = null; } if (subscription != null) { subscription.unsubscribe(); subscription = null; } setText(""); } }); } }; } }); return column; } private String getDisputeStateText(Dispute dispute) { Trade trade = tradeManager.getTrade(dispute.getTradeId()); if (trade == null) { log.warn("Dispute's trade is null for trade {}, defaulting to dispute state text 'closed'", dispute.getTradeId()); return Res.get("support.closed"); } if (dispute.isClosed()) return Res.get("support.closed"); switch (trade.getDisputeState()) { case NO_DISPUTE: return Res.get("shared.pending"); case DISPUTE_PREPARING: return Res.get("support.preparing"); case DISPUTE_REQUESTED: return Res.get("support.requested"); default: return Res.get("support.open"); } } private void openChat(Dispute dispute) { chatPopup.openChat(dispute, getConcreteDisputeChatSession(dispute), getCounterpartyName()); dispute.setDisputeSeen(senderFlag()); newBadgeByDispute.get(dispute.getId()).setVisible(dispute.isNew()); updateChatMessageCount(dispute, chatBadgeByDispute.get(dispute.getId())); } private void updateChatMessageCount(Dispute dispute, JFXBadge chatBadge) { UserThread.execute(() -> { if (chatBadge == null) return; // when the chat popup is active, we do not display new message count indicator for that item if (chatPopup.isChatShown() && selectedDispute != null && dispute.getId().equals(selectedDispute.getId())) { chatBadge.setText(""); chatBadge.setEnabled(false); chatBadge.refreshBadge(); // have to UserThread.execute or the new message will be sent to peer as "read" UserThread.execute(() -> dispute.setChatMessagesSeen(senderFlag())); return; } if (canViewChatMessages(dispute) && dispute.unreadMessageCount(senderFlag()) > 0) { chatBadge.setText(String.valueOf(dispute.unreadMessageCount(senderFlag()))); chatBadge.setEnabled(true); } else { chatBadge.setText(""); chatBadge.setEnabled(false); } chatBadge.refreshBadge(); dispute.refreshAlertLevel(senderFlag()); }); } private String getCounterpartyName() { if (senderFlag()) { return Res.get("offerbook.trader"); } else { return (disputeManager instanceof MediationManager) ? Res.get("shared.mediator") : Res.get("shared.refundAgent"); } } private PeerInfoIconDispute createAvatar(Integer tableRowId, Dispute dispute, boolean isBuyer) { NodeAddress nodeAddress = isBuyer ? dispute.getContract().getBuyerNodeAddress() : dispute.getContract().getSellerNodeAddress(); String key = tableRowId + nodeAddress.getAddressForDisplay() + (isBuyer ? "BUYER" : "SELLER"); Long accountAge = isBuyer ? accountAgeWitnessService.getAccountAge(dispute.getBuyerPaymentAccountPayload(), dispute.getContract().getBuyerPubKeyRing()) : accountAgeWitnessService.getAccountAge(dispute.getSellerPaymentAccountPayload(), dispute.getContract().getSellerPubKeyRing()); PeerInfoIconDispute peerInfoIcon = new PeerInfoIconDispute( nodeAddress, disputeManager.getNrOfDisputes(isBuyer, dispute.getContract()), accountAge, preferences); avatarMap.put(key, peerInfoIcon); // TODO return peerInfoIcon; } @Override public void onCloseDisputeFromChatWindow(Dispute dispute) { if (dispute.getDisputeState() == Dispute.State.NEW || dispute.getDisputeState().isOpen()) { handleOnProcessDispute(dispute); } else { closeDisputeFromButton(); } } @Override public void onSendLogsFromChatWindow(Dispute dispute) { if (!(disputeManager instanceof ArbitrationManager)) return; ArbitrationManager arbitrationManager = (ArbitrationManager) disputeManager; new SendLogFilesWindow(dispute.getTradeId(), dispute.getTraderId(), arbitrationManager).show(); } private boolean canViewChatMessages(Dispute dispute) { return disputeManager.canSendChatMessages(dispute) || dispute.isClosed(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/support/dispute/agent/DisputeAgentView.java ================================================ /* * 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 . */ package haveno.desktop.main.support.dispute.agent; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import haveno.common.crypto.KeyRing; import haveno.common.util.Utilities; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.alert.PrivateNotificationManager; import haveno.core.locale.Res; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeList; import haveno.core.support.dispute.DisputeManager; import haveno.core.support.dispute.DisputeValidation; import haveno.core.support.dispute.agent.MultipleHolderNameDetection; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.trade.TradeManager; import haveno.core.user.DontShowAgainLookup; import haveno.core.user.Preferences; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.components.AutoTooltipTableColumn; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.ContractWindow; import haveno.desktop.main.overlays.windows.DisputeSummaryWindow; import haveno.desktop.main.overlays.windows.TradeDetailsWindow; import haveno.desktop.main.support.dispute.DisputeView; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.collections.ListChangeListener; import javafx.geometry.Insets; import javafx.scene.control.Label; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.Tooltip; import javafx.scene.layout.HBox; import javafx.scene.text.Text; import org.jetbrains.annotations.NotNull; import java.util.List; import static haveno.desktop.util.FormBuilder.getIconForLabel; public abstract class DisputeAgentView extends DisputeView implements MultipleHolderNameDetection.Listener { private final MultipleHolderNameDetection multipleHolderNameDetection; private ListChangeListener validationExceptionListener; public DisputeAgentView(DisputeManager> disputeManager, KeyRing keyRing, TradeManager tradeManager, CoinFormatter formatter, Preferences preferences, DisputeSummaryWindow disputeSummaryWindow, PrivateNotificationManager privateNotificationManager, ContractWindow contractWindow, TradeDetailsWindow tradeDetailsWindow, AccountAgeWitnessService accountAgeWitnessService, ArbitratorManager arbitratorManager, boolean useDevPrivilegeKeys) { super(disputeManager, keyRing, tradeManager, formatter, preferences, disputeSummaryWindow, privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService, arbitratorManager, useDevPrivilegeKeys); multipleHolderNameDetection = new MultipleHolderNameDetection(disputeManager); } /////////////////////////////////////////////////////////////////////////////////////////// // Life cycle /////////////////////////////////////////////////////////////////////////////////////////// @Override public void initialize() { super.initialize(); filterTextField.setText("open"); sendPrivateNotificationButton.setVisible(true); sendPrivateNotificationButton.setManaged(true); reportButton.setVisible(true); reportButton.setManaged(true); fullReportButton.setVisible(true); fullReportButton.setManaged(true); multipleHolderNameDetection.detectMultipleHolderNames(); validationExceptionListener = c -> { c.next(); if (c.wasAdded()) { showWarningForValidationExceptions(c.getAddedSubList()); } }; } protected void showWarningForValidationExceptions(List exceptions) { exceptions.stream() .filter(ex -> ex.getDispute() != null) .filter(ex -> !ex.getDispute().isClosed()) // we show warnings only for open cases .filter(ex -> DontShowAgainLookup.showAgain(getKey(ex))) .forEach(ex -> new Popup().width(900).warning(getValidationExceptionMessage(ex)).dontShowAgainId(getKey(ex)).show()); } private String getKey(DisputeValidation.ValidationException exception) { Dispute dispute = exception.getDispute(); if (dispute != null) { return "ValExcPopup-" + dispute.getTradeId() + "-" + dispute.getTraderId(); } return "ValExcPopup-" + exception.toString(); } private String getValidationExceptionMessage(DisputeValidation.ValidationException exception) { Dispute dispute = exception.getDispute(); if (dispute != null && exception instanceof DisputeValidation.AddressException) { return getAddressExceptionMessage(dispute); } else if (exception.getMessage() != null && !exception.getMessage().isEmpty()) { return exception.getMessage(); } else { return exception.toString(); } } @NotNull private String getAddressExceptionMessage(Dispute dispute) { return Res.get("support.warning.disputesWithInvalidDonationAddress", dispute.getDonationAddressOfDelayedPayoutTx(), dispute.getTradeId(), ""); } @Override protected void activate() { super.activate(); multipleHolderNameDetection.addListener(this); if (multipleHolderNameDetection.hasSuspiciousDisputesDetected()) { suspiciousDisputeDetected(); } disputeManager.getValidationExceptions().addListener(validationExceptionListener); showWarningForValidationExceptions(disputeManager.getValidationExceptions()); } @Override protected void deactivate() { super.deactivate(); multipleHolderNameDetection.removeListener(this); disputeManager.getValidationExceptions().removeListener(validationExceptionListener); } /////////////////////////////////////////////////////////////////////////////////////////// // MultipleHolderNamesDetection.Listener /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onSuspiciousDisputeDetected() { suspiciousDisputeDetected(); } /////////////////////////////////////////////////////////////////////////////////////////// // DisputeView /////////////////////////////////////////////////////////////////////////////////////////// @Override protected DisputeView.FilterResult getFilterResult(Dispute dispute, String filterString) { // If in arbitrator view we must only display disputes where we are selected as arbitrator (must not receive others anyway) if (!dispute.getAgentPubKeyRing().equals(keyRing.getPubKeyRing())) { return FilterResult.NO_MATCH; } return super.getFilterResult(dispute, filterString); } @Override protected void handleOnProcessDispute(Dispute dispute) { onCloseDispute(dispute); } @Override protected void setupTable() { super.setupTable(); tableView.getColumns().add(getAlertColumn()); } protected abstract void onCloseDispute(Dispute dispute); /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void suspiciousDisputeDetected() { alertIconLabel.setVisible(true); alertIconLabel.setManaged(true); alertIconLabel.setTooltip(new Tooltip("You have suspicious disputes where the same trader used different " + "account holder names.\nClick for more information.")); // Text below is for arbitrators only so no need to translate it alertIconLabel.setOnMouseClicked(e -> { String reportForAllDisputes = multipleHolderNameDetection.getReportForAllDisputes(); new Popup() .width(1100) .warning(getReportMessage(reportForAllDisputes, "traders")) .actionButtonText(Res.get("shared.copyToClipboard")) .onAction(() -> Utilities.copyToClipboard(reportForAllDisputes)) .show(); }); } private TableColumn getAlertColumn() { TableColumn column = new AutoTooltipTableColumn<>("Alert") { { setMinWidth(50); } }; column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); column.setCellFactory( c -> new TableCell<>() { Label alertIconLabel; @Override public void updateItem(Dispute dispute, boolean empty) { if (dispute != null && !empty) { if (!showAlertAtDispute(dispute)) { setGraphic(null); if (alertIconLabel != null) { alertIconLabel.setOnMouseClicked(null); } return; } if (alertIconLabel != null) { alertIconLabel.setOnMouseClicked(null); } alertIconLabel = new Label(); Text icon = getIconForLabel(MaterialDesignIcon.ALERT_CIRCLE_OUTLINE, "1.5em", alertIconLabel); icon.getStyleClass().add("alert-icon"); HBox.setMargin(alertIconLabel, new Insets(4, 0, 0, 10)); alertIconLabel.setMouseTransparent(false); setGraphic(alertIconLabel); alertIconLabel.setOnMouseClicked(e -> { List realNameAccountInfoList = multipleHolderNameDetection.getDisputesForTrader(dispute); String reportForDisputeOfTrader = multipleHolderNameDetection.getReportForDisputeOfTrader(realNameAccountInfoList); String key = MultipleHolderNameDetection.getAckKey(dispute); new Popup() .width(1100) .warning(getReportMessage(reportForDisputeOfTrader, "this trader")) .actionButtonText(Res.get("shared.copyToClipboard")) .onAction(() -> { Utilities.copyToClipboard(reportForDisputeOfTrader); if (!DontShowAgainLookup.showAgain(key)) { setGraphic(null); } }) .dontShowAgainId(key) .dontShowAgainText("Is not suspicious") .onClose(() -> { if (!DontShowAgainLookup.showAgain(key)) { setGraphic(null); } }) .show(); }); } else { setGraphic(null); if (alertIconLabel != null) { alertIconLabel.setOnMouseClicked(null); } } } }); column.setComparator((o1, o2) -> Boolean.compare(showAlertAtDispute(o1), showAlertAtDispute(o2))); column.setSortable(true); return column; } private boolean showAlertAtDispute(Dispute dispute) { return DontShowAgainLookup.showAgain(MultipleHolderNameDetection.getAckKey(dispute)) && !multipleHolderNameDetection.getDisputesForTrader(dispute).isEmpty(); } private String getReportMessage(String report, String subString) { return "You have dispute cases where " + subString + " used different account holder names.\n\n" + "This might be not critical in case of small variations of the same name " + "(e.g. first name and last name are swapped), " + "but if the name is completely different you should request information from the trader why they " + "used a different name and request proof that the person with the real name is aware " + "of the trade. " + "It can be that the trader uses the account of their wife/husband, but it also could " + "be a case of a stolen bank account or money laundering.\n\n" + "Please check below the list of the names which have been detected. " + "Search with the trade ID for the dispute case or check out the alert icon at each dispute in " + "the list (you might need to remove the 'open' filter) and evaluate " + "if it might be a fraudulent account (buyer role is more likely to be fraudulent). " + "If you find suspicious disputes, please notify the developers and provide the contract json data " + "to them so they can ban those traders.\n\n" + Utilities.toTruncatedString(report, 700, false); } @Override protected void maybeAddProcessColumnsForAgent() { tableView.getColumns().add(getProcessColumn()); tableView.getColumns().add(getChatColumn()); } @Override protected boolean senderFlag() { return true; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/support/dispute/agent/arbitration/ArbitratorView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/support/dispute/agent/arbitration/ArbitratorView.java ================================================ /* * 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 . */ package haveno.desktop.main.support.dispute.agent.arbitration; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.common.crypto.KeyRing; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.alert.PrivateNotificationManager; import haveno.core.support.SupportType; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeSession; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.arbitration.ArbitrationSession; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.trade.TradeManager; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.common.view.FxmlView; import haveno.desktop.main.overlays.windows.ContractWindow; import haveno.desktop.main.overlays.windows.DisputeSummaryWindow; import haveno.desktop.main.overlays.windows.TradeDetailsWindow; import haveno.desktop.main.support.dispute.agent.DisputeAgentView; @FxmlView public class ArbitratorView extends DisputeAgentView { @Inject public ArbitratorView(ArbitrationManager arbitrationManager, KeyRing keyRing, TradeManager tradeManager, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, Preferences preferences, DisputeSummaryWindow disputeSummaryWindow, PrivateNotificationManager privateNotificationManager, ContractWindow contractWindow, TradeDetailsWindow tradeDetailsWindow, AccountAgeWitnessService accountAgeWitnessService, ArbitratorManager arbitratorManager, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { super(arbitrationManager, keyRing, tradeManager, formatter, preferences, disputeSummaryWindow, privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService, arbitratorManager, useDevPrivilegeKeys); } @Override protected SupportType getType() { return SupportType.ARBITRATION; } @Override protected DisputeSession getConcreteDisputeChatSession(Dispute dispute) { return new ArbitrationSession(dispute, disputeManager.isTrader(dispute)); } @Override protected void onCloseDispute(Dispute dispute) { long protocolVersion = dispute.getContract().getOfferPayload().getProtocolVersion(); // Only cases with protocolVersion 1 are candidates for legacy arbitration. // This code path is not tested and it is not assumed that it is still be used as old arbitrators would use // their old Haveno version if still cases are pending. // if (protocolVersion == 1) { chatPopup.closeChat(); disputeSummaryWindow.show(dispute); // } else { // new Popup().warning(Res.get("support.wrongVersion", protocolVersion)).show(); // } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/support/dispute/agent/mediation/MediatorView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/support/dispute/agent/mediation/MediatorView.java ================================================ /* * 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 . */ package haveno.desktop.main.support.dispute.agent.mediation; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.common.crypto.KeyRing; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.alert.PrivateNotificationManager; import haveno.core.support.SupportType; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeSession; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.support.dispute.mediation.MediationManager; import haveno.core.support.dispute.mediation.MediationSession; import haveno.core.trade.TradeManager; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.common.view.FxmlView; import haveno.desktop.main.overlays.windows.ContractWindow; import haveno.desktop.main.overlays.windows.DisputeSummaryWindow; import haveno.desktop.main.overlays.windows.TradeDetailsWindow; import haveno.desktop.main.support.dispute.agent.DisputeAgentView; @FxmlView public class MediatorView extends DisputeAgentView { @Inject public MediatorView(MediationManager mediationManager, KeyRing keyRing, TradeManager tradeManager, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, Preferences preferences, DisputeSummaryWindow disputeSummaryWindow, PrivateNotificationManager privateNotificationManager, ContractWindow contractWindow, TradeDetailsWindow tradeDetailsWindow, AccountAgeWitnessService accountAgeWitnessService, ArbitratorManager arbitratorManager, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { super(mediationManager, keyRing, tradeManager, formatter, preferences, disputeSummaryWindow, privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService, arbitratorManager, useDevPrivilegeKeys); } @Override public void initialize() { super.initialize(); reOpenButton.setVisible(true); reOpenButton.setManaged(true); setupReOpenDisputeListener(); } @Override protected void activate() { super.activate(); activateReOpenDisputeListener(); // We need to call applyFilteredListPredicate after we called activateReOpenDisputeListener as we use the // "open" string by default and it was applied in the super call but the disputes got set in // activateReOpenDisputeListener applyFilteredListPredicate(filterTextField.getText()); } @Override protected void deactivate() { super.deactivate(); deactivateReOpenDisputeListener(); } @Override protected SupportType getType() { return SupportType.MEDIATION; } @Override protected DisputeSession getConcreteDisputeChatSession(Dispute dispute) { return new MediationSession(dispute, disputeManager.isTrader(dispute)); } @Override protected void onCloseDispute(Dispute dispute) { chatPopup.closeChat(); disputeSummaryWindow.show(dispute); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/support/dispute/agent/refund/RefundAgentView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/support/dispute/agent/refund/RefundAgentView.java ================================================ /* * 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 . */ package haveno.desktop.main.support.dispute.agent.refund; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.common.crypto.KeyRing; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.alert.PrivateNotificationManager; import haveno.core.locale.Res; import haveno.core.support.SupportType; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeSession; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.support.dispute.refund.RefundManager; import haveno.core.support.dispute.refund.RefundSession; import haveno.core.trade.TradeManager; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.common.view.FxmlView; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.ContractWindow; import haveno.desktop.main.overlays.windows.DisputeSummaryWindow; import haveno.desktop.main.overlays.windows.TradeDetailsWindow; import haveno.desktop.main.support.dispute.agent.DisputeAgentView; @FxmlView public class RefundAgentView extends DisputeAgentView { @Inject public RefundAgentView(RefundManager refundManager, KeyRing keyRing, TradeManager tradeManager, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, Preferences preferences, DisputeSummaryWindow disputeSummaryWindow, PrivateNotificationManager privateNotificationManager, ContractWindow contractWindow, TradeDetailsWindow tradeDetailsWindow, AccountAgeWitnessService accountAgeWitnessService, ArbitratorManager arbitratorService, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { super(refundManager, keyRing, tradeManager, formatter, preferences, disputeSummaryWindow, privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService, arbitratorService, useDevPrivilegeKeys); } @Override protected SupportType getType() { return SupportType.REFUND; } @Override protected DisputeSession getConcreteDisputeChatSession(Dispute dispute) { return new RefundSession(dispute, disputeManager.isTrader(dispute)); } @Override protected void onCloseDispute(Dispute dispute) { long protocolVersion = dispute.getContract().getOfferPayload().getProtocolVersion(); // Refund agent was introduced with protocolVersion version 2. We do not support old trade protocol cases. if (protocolVersion >= 2) { chatPopup.closeChat(); disputeSummaryWindow.show(dispute); } else { new Popup().warning(Res.get("support.wrongVersion", protocolVersion)).show(); } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/support/dispute/client/DisputeClientView.java ================================================ /* * 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 . */ package haveno.desktop.main.support.dispute.client; import haveno.common.crypto.KeyRing; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.alert.PrivateNotificationManager; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeList; import haveno.core.support.dispute.DisputeManager; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.trade.TradeManager; import haveno.core.user.Preferences; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.main.overlays.windows.ContractWindow; import haveno.desktop.main.overlays.windows.DisputeSummaryWindow; import haveno.desktop.main.overlays.windows.TradeDetailsWindow; import haveno.desktop.main.support.dispute.DisputeView; public abstract class DisputeClientView extends DisputeView { public DisputeClientView(DisputeManager> DisputeManager, KeyRing keyRing, TradeManager tradeManager, CoinFormatter formatter, Preferences preferences, DisputeSummaryWindow disputeSummaryWindow, PrivateNotificationManager privateNotificationManager, ContractWindow contractWindow, TradeDetailsWindow tradeDetailsWindow, AccountAgeWitnessService accountAgeWitnessService, ArbitratorManager arbitratorManager, boolean useDevPrivilegeKeys) { super(DisputeManager, keyRing, tradeManager, formatter, preferences, disputeSummaryWindow, privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService, arbitratorManager, useDevPrivilegeKeys); } @Override protected DisputeView.FilterResult getFilterResult(Dispute dispute, String filterString) { // As we are in the client view we hide disputes where we are the agent if (dispute.getAgentPubKeyRing().equals(keyRing.getPubKeyRing())) { return FilterResult.NO_MATCH; } return super.getFilterResult(dispute, filterString); } @Override protected void maybeAddChatColumnForClient() { tableView.getColumns().add(getChatColumn()); } @Override protected boolean senderFlag() { return false; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/support/dispute/client/arbitration/ArbitrationClientView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/support/dispute/client/arbitration/ArbitrationClientView.java ================================================ /* * 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 . */ package haveno.desktop.main.support.dispute.client.arbitration; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.common.crypto.KeyRing; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.alert.PrivateNotificationManager; import haveno.core.support.SupportType; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeSession; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.arbitration.ArbitrationSession; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.trade.TradeManager; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.common.view.FxmlView; import haveno.desktop.main.overlays.windows.ContractWindow; import haveno.desktop.main.overlays.windows.DisputeSummaryWindow; import haveno.desktop.main.overlays.windows.TradeDetailsWindow; import haveno.desktop.main.support.dispute.client.DisputeClientView; @FxmlView public class ArbitrationClientView extends DisputeClientView { @Inject public ArbitrationClientView(ArbitrationManager arbitrationManager, KeyRing keyRing, TradeManager tradeManager, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, Preferences preferences, DisputeSummaryWindow disputeSummaryWindow, PrivateNotificationManager privateNotificationManager, ContractWindow contractWindow, TradeDetailsWindow tradeDetailsWindow, AccountAgeWitnessService accountAgeWitnessService, ArbitratorManager arbitratorManager, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { super(arbitrationManager, keyRing, tradeManager, formatter, preferences, disputeSummaryWindow, privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService, arbitratorManager, useDevPrivilegeKeys); } @Override protected SupportType getType() { return SupportType.ARBITRATION; } @Override protected DisputeSession getConcreteDisputeChatSession(Dispute dispute) { return new ArbitrationSession(dispute, disputeManager.isTrader(dispute)); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/support/dispute/client/mediation/MediationClientView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/support/dispute/client/mediation/MediationClientView.java ================================================ /* * 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 . */ package haveno.desktop.main.support.dispute.client.mediation; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.common.crypto.KeyRing; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.alert.PrivateNotificationManager; import haveno.core.locale.Res; import haveno.core.support.SupportType; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeSession; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.support.dispute.mediation.MediationManager; import haveno.core.support.dispute.mediation.MediationSession; import haveno.core.trade.Contract; import haveno.core.trade.TradeManager; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.common.view.FxmlView; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.ContractWindow; import haveno.desktop.main.overlays.windows.DisputeSummaryWindow; import haveno.desktop.main.overlays.windows.TradeDetailsWindow; import haveno.desktop.main.support.dispute.client.DisputeClientView; import haveno.network.p2p.NodeAddress; @FxmlView public class MediationClientView extends DisputeClientView { @Inject public MediationClientView(MediationManager mediationManager, KeyRing keyRing, TradeManager tradeManager, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, Preferences preferences, DisputeSummaryWindow disputeSummaryWindow, PrivateNotificationManager privateNotificationManager, ContractWindow contractWindow, TradeDetailsWindow tradeDetailsWindow, AccountAgeWitnessService accountAgeWitnessService, ArbitratorManager arbitratorManager, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { super(mediationManager, keyRing, tradeManager, formatter, preferences, disputeSummaryWindow, privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService, arbitratorManager, useDevPrivilegeKeys); } @Override public void initialize() { super.initialize(); reOpenButton.setVisible(true); reOpenButton.setManaged(true); closeButton.setVisible(true); closeButton.setManaged(true); setupReOpenDisputeListener(); } @Override protected void activate() { super.activate(); activateReOpenDisputeListener(); } @Override protected void deactivate() { super.deactivate(); deactivateReOpenDisputeListener(); } @Override protected SupportType getType() { return SupportType.MEDIATION; } @Override protected DisputeSession getConcreteDisputeChatSession(Dispute dispute) { return new MediationSession(dispute, disputeManager.isTrader(dispute)); } @Override protected void reOpenDisputeFromButton() { new Popup().attention(Res.get("support.reOpenByTrader.prompt")) .actionButtonText(Res.get("shared.yes")) .onAction(() -> reOpenDispute()) .show(); } @Override protected NodeAddress getAgentNodeAddress(Contract contract) { throw new RuntimeException("MediationClientView.getAgentNodeAddress() not implementd for XMR"); //return contract.getMediatorNodeAddress(); } @Override protected void maybeAddAgentColumn() { tableView.getColumns().add(getAgentColumn()); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/main/support/dispute/client/refund/RefundClientView.fxml ================================================ ================================================ FILE: desktop/src/main/java/haveno/desktop/main/support/dispute/client/refund/RefundClientView.java ================================================ /* * 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 . */ package haveno.desktop.main.support.dispute.client.refund; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.common.crypto.KeyRing; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.alert.PrivateNotificationManager; import haveno.core.support.SupportType; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.DisputeSession; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.support.dispute.refund.RefundManager; import haveno.core.support.dispute.refund.RefundSession; import haveno.core.trade.Contract; import haveno.core.trade.TradeManager; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.desktop.common.view.FxmlView; import haveno.desktop.main.overlays.windows.ContractWindow; import haveno.desktop.main.overlays.windows.DisputeSummaryWindow; import haveno.desktop.main.overlays.windows.TradeDetailsWindow; import haveno.desktop.main.support.dispute.client.DisputeClientView; import haveno.network.p2p.NodeAddress; @FxmlView public class RefundClientView extends DisputeClientView { @Inject public RefundClientView(RefundManager refundManager, KeyRing keyRing, TradeManager tradeManager, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, Preferences preferences, DisputeSummaryWindow disputeSummaryWindow, PrivateNotificationManager privateNotificationManager, ContractWindow contractWindow, TradeDetailsWindow tradeDetailsWindow, AccountAgeWitnessService accountAgeWitnessService, ArbitratorManager arbitratorManager, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { super(refundManager, keyRing, tradeManager, formatter, preferences, disputeSummaryWindow, privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService, arbitratorManager, useDevPrivilegeKeys); } @Override protected SupportType getType() { return SupportType.REFUND; } @Override protected DisputeSession getConcreteDisputeChatSession(Dispute dispute) { return new RefundSession(dispute, disputeManager.isTrader(dispute)); } @Override protected NodeAddress getAgentNodeAddress(Contract contract) { throw new RuntimeException("RefundClientView.getAgentNodeAddress() not implementd for XMR"); //return contract.getRefundAgentNodeAddress(); } @Override protected void maybeAddAgentColumn() { tableView.getColumns().add(getAgentColumn()); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/setup/DesktopPersistedDataHost.java ================================================ /* * 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 . */ package haveno.desktop.setup; import com.google.inject.Injector; import haveno.common.proto.persistable.PersistedDataHost; import haveno.desktop.Navigation; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; @Slf4j public class DesktopPersistedDataHost { // All classes which are persisting objects need to be added here public static List getPersistedDataHosts(Injector injector) { List persistedDataHosts = new ArrayList<>(); persistedDataHosts.add(injector.getInstance(Navigation.class)); return persistedDataHosts; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/theme-dark.css ================================================ .root { -bs-color-primary: rgb(28, 96, 220); /* javafx main color palette */ -fx-base: #29292a; -fx-background: #29292a; -fx-control-inner-background: #2B2B2B; -fx-dark-text-color: #fcfcfc; -fx-mid-text-color: #dadada; -fx-light-text-color: #cacaca; -fx-text-fill: #dadada; /* javafx elements */ -fx-accent: -bs-color-primary; -fx-box-border: transparent; -fx-focus-color: #0c59bd; -fx-faint-focus-color: #0c59bd; -fx-selection-bar: #0f87c3; -fx-selection-bar-non-focused: #4181d4; -fx-default-button: derive(-fx-accent, 95%); /* haveno main colors */ -bs-color-primary-dark: #0c59bd; -bs-text-color: white; -bs-background-color: black; -bs-background-gray: transparent; -bs-content-background-gray: black; /* fifty shades of gray */ -bs-color-gray-13: #bbb; -bs-color-gray-12: #eaeaea; -bs-color-gray-11: #dadada; -bs-color-gray-10: #cfcecf; -bs-color-gray-6: #afaeb0; -bs-color-gray-5: #424242; -bs-color-gray-4: #929293; -bs-color-gray-3: #656565; -bs-color-gray-2: #504f52; -bs-color-gray-1: #29292a; -bs-color-gray-0: #2B2B2B; -bs-color-gray-dim: #aaaaaa; -bs-color-gray-ddd: #444444; -bs-color-gray-ccc: #7a7a7a; -bs-color-gray-bbb: #5a5a5a; -bs-color-gray-aaa: #29292a; -bs-color-gray-fafa: #0a0a0a; -bs-color-gray-background: black; -bs-color-background-popup: rgb(38, 38, 38); -bs-color-background-popup-blur: rgb(9, 9, 9); -bs-color-background-popup-input: rgb(9, 9, 9); -bs-color-background-form-field: rgb(26, 26, 26); -bs-color-background-form-field-readonly: rgb(18, 18, 18); -bs-color-border-form-field: rgb(65, 65, 65); -bs-color-background-pane: rgb(15, 15, 15); -bs-color-background-row-even: rgb(19, 19, 19); -bs-color-background-row-odd: rgb(9, 9, 9); -bs-color-table-cell-dim: -bs-color-gray-ccc; -bs-text-color-dim1: rgb(87, 87, 87); -bs-text-color-dim2: rgb(130, 130, 130); /* lesser used colors */ -bs-color-blue-5: #0a4576; -bs-color-blue-4: #57acc9; -bs-color-blue-3: #9bbdc9; -bs-color-blue-2: #6aa4b6; -bs-color-blue-1: #b5e1ef; -bs-color-blue-0: blue; -bs-color-green-1: #c4d9c5; -bs-color-green-2: #98b999; -bs-color-green-3: #619864; -bs-color-green-4: #15bc1d; -bs-color-green-5: #2a7e2e; -bs-red: #D73030; -bs-turquoise: #2cacaf; -bs-yellow: #ffb60f; -bs-yellow-light: derive(-bs-yellow, 81%); -bs-blue-transparent: #0f87c344; -bs-bg-green: #99ba9c; -bs-rd-green: #25B135; -bs-rd-green-dark: #3EA34A; /* other element styling */ -bs-rd-nav-border: #535353; -bs-rd-nav-primary-border: rgba(0, 0, 0, 0); -bs-rd-nav-border-color: rgba(255, 255, 255, 0.1); -bs-rd-nav-background: rgb(15, 15, 15); -bs-rd-nav-primary-background: rgb(15, 15, 15); -bs-rd-nav-selected: black; -bs-rd-nav-deselected: rgba(255, 255, 255, 1); -bs-rd-nav-secondary-selected: -fx-accent; -bs-rd-nav-secondary-deselected: -bs-rd-font-light; -bs-rd-nav-button-hover: derive(-bs-rd-nav-background, 10%); -bs-rd-nav-primary-button-hover: derive(-bs-rd-nav-primary-background, 10%); -bs-rd-nav-hover-text: black; -bs-content-pane-bg-top: #212121; -bs-rd-tab-border: rgba(255, 255, 255, 0.00); -bs-tab-content-label-hover: rgba(0, 0, 0, 0.03); -bs-combobox-hover: rgba(255, 255, 255, 0.1); -bs-combobox-selected: rgba(21, 188, 29, 0.1); -bs-content-pane-bg-bottom: #29292a; -bs-scroll-pane-background: transparent; -bs-tab-content-area: transparent; -bs-viewport-background: transparent; -bs-footer-pane-background: #29292a; -bs-footer-pane-text: #cfcecf; -bs-footer-pane-line: #29292a; -bs-rd-font-balance: white; -bs-rd-font-dark-gray: #d4d4d4; -bs-rd-font-dark: #cccccc; -bs-rd-font-light: #b4b4b4; -bs-rd-font-lighter: #a0a0a0; -bs-rd-font-confirmation-label: #504f52; -bs-rd-font-balance-label: #999999; -bs-text-color-dropshadow: rgba(45, 45, 49, .75); -bs-text-color-dropshadow-light-mode: transparent; -bs-text-color-transparent: rgba(29, 29, 33, 0.2); -bs-color-gray-line: #504f52; -bs-rd-separator: #1F1F1F; -bs-rd-separator-dark: rgb(255, 255, 255, 0.1); -bs-rd-error-red: #d83431; -bs-rd-error-field: #521C1C; -bs-rd-message-bubble: #0086c6; -bs-rd-tooltip-truncated: #afaeb0; /*-bs-toggle-selected: rgb(12, 89, 189);*/ -bs-toggle-selected: rgb(12, 89, 190); -bs-warning: #db6300; -bs-buy: rgb(80, 180, 90); -bs-buy-focus: derive(-bs-buy, -50%); -bs-buy-hover: derive(-bs-buy, -10%); -bs-sell: rgb(213, 63, 46); -bs-sell-focus: derive(-bs-sell, -50%); -bs-sell-hover: derive(-bs-sell, -10%); -bs-volume-transparent: -bs-buy; -bs-candle-stick-average-line: rgba(21, 188, 29, 0.8); -bs-candle-stick-loss: #ee6563; -bs-candle-stick-won: #15bc1d; -bs-cancel: #1d1d21; -bs-cancel-focus: black; -bs-cancel-hover: #050506; -bs-green-soft: derive(-bs-rd-green, 60%); -bs-red-soft: derive(-bs-rd-error-red, 60%); -bs-progress-bar-track: #272728; -bs-chart-tick: rgba(255, 255, 255, 0.7); -bs-chart-lines: -bs-color-gray-2; -bs-white: white; -bs-prompt-text: -bs-color-gray-6; -bs-decimals: #db6300; -bs-soft-red: #aa4c3b; -bs-turquoise-light: #11eeee; /* dao chart colors */ -bs-chart-dao-line1: -bs-color-blue-5; -bs-chart-dao-line2: -bs-color-green-3; -bs-chart-dao-line3: #0195fe; -bs-chart-dao-line4: -bs-soft-red; -bs-chart-dao-line5: -bs-yellow; -bs-chart-dao-line6: -bs-turquoise-light; -bs-chart-dao-line7: #ff6c00; -bs-chart-dao-line8: -bs-turquoise; -bs-chart-dao-line9: #7fad01; -bs-chart-dao-line10: #420080; -bs-chart-dao-line11: #ff3939; /* Monero orange color code */ -xmr-orange: #f26822; -bs-support-chat-background: rgb(125, 125, 125); } /* table view */ .table-view, .table-cell:focused, .table-row-cell { -fx-background: transparent; -fx-border-width: 0; } .table-view .column-header { -fx-background-color: -bs-color-background-pane; -fx-border-width: 0; } .table-view { -fx-background-color: derive(-bs-background-color,-30%); -fx-border-width: 0; } /** These must be set to override default styles */ .table-view .table-row-cell:even .table-cell { -fx-background-color: -bs-color-background-row-even; -fx-border-color: -bs-color-background-row-even; } .table-view .table-row-cell:odd .table-cell { -fx-background-color: -bs-color-background-row-odd; -fx-border-color: -bs-color-background-row-odd; } .table-view .table-row-cell:selected .table-cell { -fx-background: -fx-accent; -fx-background-color: -fx-selection-bar; -fx-border-color: -fx-selection-bar; } .table-view .table-row-cell:selected .table-cell, .table-view .table-row-cell:selected .table-cell .label, .table-view .table-row-cell:selected .table-cell .text { -fx-text-fill: -fx-dark-text-color; } .table-view .table-row-cell:selected .table-cell .hyperlink, .table-view .table-row-cell:selected .table-cell .hyperlink .text, .table-view .table-row-cell:selected .table-cell .hyperlink-with-icon, .table-view .table-row-cell:selected .table-cell .hyperlink-with-icon .text { -fx-fill: -fx-dark-text-color; } .table-row-cell { -fx-border-color: -bs-background-color; } .table-row-cell:empty, .table-row-cell:empty:even, .table-row-cell:empty:odd { -fx-background-color: -bs-background-color; -fx-min-height: 36; } .offer-table .table-row-cell { -fx-background: -fx-accent; -fx-background-color: -bs-color-gray-6; } /* tab pane */ .jfx-tab-pane .tab-content-area { -fx-background-color: -bs-tab-content-area; } .jfx-tab-pane .headers-region .tab:selected .tab-container .tab-label { -fx-text-fill: white; } .nav-secondary-button:selected .text { -fx-fill: white; } .jfx-tab-pane .headers-region > .tab > .jfx-rippler { -jfx-rippler-fill: none; } .jfx-tab-pane .viewport { -fx-background-color: -bs-viewport-background; } /* text field */ .jfx-text-field, .jfx-text-area, .jfx-combo-box, .jfx-combo-box > .list-cell { -fx-prompt-text-fill: -bs-color-gray-6; -fx-text-fill: -bs-color-gray-12; } .jfx-text-area:readonly, .jfx-text-field:readonly, .hyperlink-with-icon { -fx-background: -bs-background-color; -fx-background-color: -bs-color-background-form-field-readonly; -fx-prompt-text-fill: -bs-color-gray-2; -fx-text-fill: -bs-color-gray-4; } .popover > .content .text-field { -fx-background-color: -bs-color-background-form-field !important; } .jfx-combo-box > .text, .jfx-text-field-top-label, .jfx-text-area-top-label { -fx-text-fill: -bs-color-gray-11; } .offer-input { -fx-border-color: -bs-color-gray-2; -fx-border-width: 0 0 10 0; } #address-text-field.jfx-text-field:readonly { -fx-background-color: derive(-bs-background-color, 15%); } .wallet-seed-words { -fx-text-fill: -bs-color-gray-6; } .action-button { -fx-background-color: -bs-color-primary-dark; -fx-text-fill: -fx-dark-text-color; } .axis:top, .axis:right, .axis:bottom, .axis:left { -fx-border-color: transparent transparent transparent transparent; } #charts .axis, #price-chart .axis, #volume-chart .axis, #charts-dao .axis { -fx-tick-label-fill: -bs-chart-tick; } .chart-horizontal-grid-lines, .chart-horizontal-zero-line, .chart-vertical-zero-line, .axis-tick-mark, .axis-minor-tick-mark { -fx-stroke: -bs-chart-lines; } /* scrollbars */ .scroll-pane { -fx-background-color: -bs-scroll-pane-background; } .scroll-bar { -fx-background-color: transparent; } .scroll-bar:horizontal .thumb:hover, .scroll-bar:vertical .thumb:hover { -fx-background-color: -bs-color-gray-5; } .scroll-bar:horizontal .thumb:pressed, .scroll-bar:vertical .thumb:pressed { -fx-background-color: -bs-color-gray-4; } .scroll-bar:vertical:focused, .scroll-bar:horizontal:focused { -fx-background-color: transparent, -bs-color-gray-4, -bs-color-gray-4; } .small-icon-label { -fx-fill: -bs-color-gray-11; } #bubble_arrow_grey_left { -fx-image: url("../../images/bubble_arrow_grey_left_dark.png"); } #bubble_arrow_grey_right { -fx-image: url("../../images/bubble_arrow_grey_right_dark.png"); } .headline-label { -fx-font-weight: normal; -fx-font-size: 1.892em; } .zero-decimals { -bs-text-color: -bs-color-gray-3; } .confirmation-label { -fx-effect: null; } .jfx-check-box .box, .jfx-check-box:indeterminate .box, .jfx-check-box:indeterminate:selected .box { -fx-border-radius: 1; -fx-pref-width: 15; -fx-pref-height: 15; } .jfx-check-box .mark, .jfx-check-box .indeterminate-mark { -fx-border-radius: 1; } .combo-box-popup > .list-view{ -fx-background-color: -bs-color-background-pane; } .jfx-combo-box > .arrow-button > .arrow { -fx-border-color: -bs-color-gray-13; } .combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected { -fx-background: -bs-combobox-selected; -fx-background-color: -bs-combobox-selected; } .combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:hover, .combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected:hover { -fx-background:-bs-combobox-hover; -fx-background-color: -bs-combobox-hover; -fx-text-fill: -bs-text-color; } .list-view .list-cell:odd, .list-view .list-cell:even { -fx-border-width: 0; } .list-view .list-cell:selected, .table-view .table-cell:selected { -fx-border-color: transparent; -fx-border-width: 0; } .list-view, .list-view:focused { -fx-border-color: -bs-content-background-gray; -fx-border-width: 1; } .list-view .list-cell, .list-view .list-cell:focused { -fx-border-color: transparent; -fx-border-width: 0; } .jfx-text-field-top-label { -fx-text-fill: -bs-color-gray-dim; } .jfx-combo-box:focused, .jfx-combo-box:focused > .arrow-button, .jfx-combo-box:focused > .text-input, .jfx-text-field:focused{ -fx-background-color: derive(-bs-background-color, 15%); } .jfx-combo-box:error, .jfx-text-field:error { -fx-text-fill: -bs-rd-error-red; -fx-background-color: -bs-rd-error-field; } .jfx-combo-box:error:focused, .jfx-text-field:error:focused{ -fx-background-color: derive(-bs-rd-error-field, -5%); } /* Hide bottom line in all combo boxes and text fields*/ .jfx-combo-box > .input-line, .jfx-text-field > .input-line, .jfx-combo-box > .input-focused-line, .jfx-text-field > .input-focused-line, .jfx-password-field > .input-line, .jfx-password-field > .input-focused-line { -fx-translate-x: -0.333333em; -fx-background-color: transparent; -jfx-disable-animation: true; } .offer-input { -fx-border-width: 0; -fx-border-color: -bs-background-color; } .jfx-toggle-button, .jfx-toggle-button:armed, .jfx-toggle-button:hover, .jfx-toggle-button:focused, .jfx-toggle-button:selected, .jfx-toggle-button:focused:selected { -jfx-disable-visual-focus: true; } .jfx-text-area > .input-line { -fx-background-color: transparent; } .jfx-date-picker .jfx-text-field > .input-line { -fx-background-color: transparent; } .content-pane { -fx-background-color: -bs-content-background-gray; -jfx-disable-animation: true; } .nav-price-balance { -fx-effect: null; } .nav-price-balance .jfx-combo-box > .input-line { -fx-background-color: transparent; } .nav-button:selected { -fx-background-color: white; -fx-effect: null; } .nav-primary .nav-button:selected { -fx-background-color: derive(white, -5%); } .table-view { -fx-border-color: transparent; } .jfx-tab-pane .headers-region .tab .tab-container .tab-label { -fx-cursor: hand; -jfx-disable-animation: true; } .jfx-tab-pane .headers-region .tab .tab-container .tab-label:hover { -fx-background-color: -bs-tab-content-label-hover; -jfx-disable-animation: true; } .jfx-tab-pane { -jfx-disable-animation: true; } #form-header-text { -fx-font-weight: normal; -fx-font-size: 2.077em; } #form-title { -fx-font-weight: normal; } #content-pane-top { -fx-background-color: transparent; } .combo-box-editor-bold { -fx-font-weight: normal; } .titled-group-bg, .titled-group-bg-active { -fx-border-color: -bs-rd-separator-dark; } .action-button:disabled, #sell-button:disabled, #buy-button:disabled { -fx-background-color: derive(-bs-color-gray-0, -20%); } .warning-box { -fx-background-color: -bs-color-primary-dark; } .jfx-date-picker .date-picker-popup { -fx-background-color: -bs-color-gray-background; } .jfx-date-picker .left-button, .jfx-date-picker .right-button { -fx-background-color: derive(-bs-color-gray-0, -10%); } .progress-bar > .secondary-bar { -fx-background-color: -bs-color-gray-0; } .offer-disabled .label { -bs-text-color: -bs-color-gray-bbb; } .markdown-label, .markdown-label * { -fx-text-fill: -bs-text-color; -fx-fill: -bs-text-color; } .toggle-button-no-slider { -fx-background-color: -bs-color-background-form-field; } .toggle-button-no-slider:selected { -fx-text-fill: white; -fx-background-color: -bs-color-gray-ccc; -fx-border-color: -bs-color-gray-ccc; -fx-border-width: 1px; } .toggle-button-no-slider:hover { -fx-cursor: hand; -fx-background-color: -bs-color-gray-ddd; -fx-border-color: -bs-color-gray-ddd; } .toggle-button-no-slider:selected:hover { -fx-cursor: hand; -fx-background-color: -bs-color-gray-3; -fx-border-color: -bs-color-gray-3; } .toggle-button-no-slider:pressed, .toggle-button-no-slider:selected:hover:pressed { -fx-background-color: -bs-color-gray-bbb; } #image-logo-splash { -fx-image: url("../../images/logo_splash_dark_mode.png"); } #image-logo-splash-testnet { -fx-image: url("../../images/logo_splash_testnet_dark_mode.png"); } #image-logo-landscape { -fx-image: url("../../images/logo_landscape_dark_mode.png"); } .table-view .placeholder { -fx-background-color: -bs-color-background-pane; } #charts .default-color0.chart-series-area-fill { -fx-fill: linear-gradient(to bottom, rgba(80, 181, 90, 0.45) 0%, rgba(80, 181, 90, 0.0) 100% ); } #charts .default-color1.chart-series-area-fill { -fx-fill: linear-gradient(to bottom, rgba(213, 63, 46, 0.45) 0%, rgba(213, 63, 46, 0.0) 100% ); } .table-view .table-row-cell .label { -fx-text-fill: -bs-text-color; } .table-view.non-interactive-table .table-cell, .table-view.non-interactive-table .table-cell .label, .table-view.non-interactive-table .label, .table-view.non-interactive-table .text, .table-view.non-interactive-table .hyperlink, .table-view.non-interactive-table .hyperlink-with-icon, .table-view.non-interactive-table .table-row-cell .hyperlink .text { -fx-text-fill: -bs-color-gray-dim; } .table-view.non-interactive-table .hyperlink, .table-view.non-interactive-table .hyperlink-with-icon, .table-view.non-interactive-table .table-row-cell .hyperlink .text { -fx-fill: -bs-color-gray-dim; } .table-view.non-interactive-table .table-cell.highlight-text, .table-view.non-interactive-table .table-cell.highlight-text .label, .table-view.non-interactive-table .table-cell.highlight-text .text, .table-view.non-interactive-table .table-cell.highlight-text .hyperlink, .table-view.non-interactive-table .table-cell.highlight-text .hyperlink .text { -fx-text-fill: -fx-dark-text-color; } /* Match specificity to override. */ .table-view.non-interactive-table .table-cell.highlight-text .zero-decimals { -fx-text-fill: -bs-color-gray-3; } .regular-text-color { -fx-text-fill: -bs-text-color; } #image-fiat-logo { -fx-image: url("../../images/fiat_logo_dark_mode.png"); } #message-list-view.list-view { -fx-background-image: url("../../images/chatbg_dark.png"); } ================================================ FILE: desktop/src/main/java/haveno/desktop/theme-dev.css ================================================ .root { -bs-rd-nav-background: #841413; -bs-rd-nav-primary-background: #841413; } ================================================ FILE: desktop/src/main/java/haveno/desktop/theme-light.css ================================================ .root { -bs-color-primary: rgb(28, 96, 220); -bs-color-primary-dark: #0c59bd; -bs-text-color: #000000; -bs-background-color: #ffffff; -bs-background-gray: #dddddd; -bs-content-background-gray: #f4f4f4; -bs-color-gray-13: #333; -bs-color-gray-11: #111; -bs-color-gray-10: #555; -bs-color-gray-6: #666666; -bs-color-gray-5: #f1f6f7; -bs-color-gray-4: #aaaaaa; -bs-color-gray-3: #cccccc; -bs-color-gray-2: #d0d0d0; -bs-color-gray-1: #e9e9e9; -bs-color-gray-0: #f8f8f8; -bs-color-gray-dim: dimgray; -bs-color-gray-ddd: #444444; -bs-color-gray-ccc: #9a9a9a; -bs-color-gray-bbb: #d8d8d8; -bs-color-gray-aaa: #f0f0f0; -bs-color-gray-fafa: #fafafa; -bs-color-blue-5: #0a4576; -bs-color-blue-4: #57acc9; -bs-color-blue-3: #9bbdc9; -bs-color-blue-2: #6aa4b6; -bs-color-blue-1: #b5e1ef; -bs-color-blue-0: blue; -bs-color-green-1: #c4d9c5; -bs-color-green-2: #98b999; -bs-color-green-3: #619864; -bs-color-green-4: #15bc1d; -bs-color-green-5: #2a7e2e; -bs-red: #D73030; -bs-turquoise: #2cacaf; -bs-yellow: #ffb60f; -bs-yellow-light: derive(-bs-yellow, 81%); -bs-blue-transparent: #0f87c344; -bs-bg-green: #99ba9c; -bs-rd-green: -bs-color-primary; -bs-rd-green-dark: #3EA34A; -bs-rd-nav-selected: -bs-color-primary; -bs-rd-nav-deselected: rgba(255, 255, 255, 1); -bs-rd-nav-secondary-selected: -fx-accent; -bs-rd-nav-secondary-deselected: -bs-rd-font-light; -bs-rd-nav-background: -bs-color-primary; -bs-rd-nav-primary-background: -bs-color-primary; -bs-rd-nav-button-hover: derive(-bs-rd-nav-background, 10%); -bs-rd-nav-primary-button-hover: derive(-bs-rd-nav-primary-background, 10%); -bs-rd-nav-primary-border: -bs-color-primary; -bs-rd-nav-border: #535353; -bs-rd-nav-border-color: rgba(255, 255, 255, 0.31); -bs-rd-nav-hover-text: white; -bs-rd-tab-border: #e2e0e0; -bs-tab-content-area: #ffffff; -bs-color-gray-background: #f2f2f2; -bs-content-pane-bg-top: #f2f2f2; -bs-content-pane-bg-bottom: #f6f6f6; -bs-scroll-pane-background: transparent; -bs-tab-content-area: transparent; -bs-viewport-background: transparent; -bs-footer-pane-background: #dddddd; -bs-footer-pane-text: #4b4b4b; -bs-footer-pane-line: #bbb; -bs-rd-font-balance: white; -bs-rd-font-dark-gray: #3c3c3c; -bs-rd-font-dark: #4b4b4b; -bs-rd-font-light: #8d8d8d; -bs-rd-font-lighter: #a7a7a7; -bs-rd-font-confirmation-label: #504f52; -bs-rd-font-balance-label: rgb(215, 215, 215, 1); -bs-text-color-dropshadow: rgba(0, 0, 0, 0.54); -bs-text-color-dropshadow-light-mode: rgba(0, 0, 0, 0.54); -bs-text-color-transparent: rgba(0, 0, 0, 0.2); -bs-color-gray-line: #979797; -bs-rd-separator: #dbdbdb; -bs-rd-separator-dark: rgb(255, 255, 255, 0.1); -bs-rd-error-red: #dd0000; -bs-rd-message-bubble: #0086c6; -bs-toggle-selected: #7b7b7b; -bs-rd-tooltip-truncated: #0a0a0a; -bs-warning: #ff8a2b; -bs-buy: rgb(80, 180, 90); -bs-buy-focus: derive(-bs-buy, -50%); -bs-buy-hover: derive(-bs-buy, -10%); -bs-sell: rgb(213, 63, 46); -bs-sell-focus: derive(-bs-sell, -50%); -bs-sell-hover: derive(-bs-sell, -10%); -bs-volume-transparent: -bs-buy; -bs-candle-stick-average-line: -bs-rd-green; -bs-candle-stick-loss: #fe3001; -bs-candle-stick-won: #20b221; -bs-cancel: #dddddd; -bs-cancel-focus: derive(-bs-cancel, -50%); -bs-cancel-hover: derive(-bs-cancel, -10%); -fx-accent: -bs-color-primary; -fx-box-border: #e9e9e9; -bs-green-soft: derive(-bs-rd-green, 60%); -bs-red-soft: derive(-bs-rd-error-red, 60%); -fx-focus-color: -fx-accent; -fx-faint-focus-color: #0f87c3; -fx-selection-bar: #4181d4; -fx-selection-bar-non-focused: -fx-selection-bar; -fx-default-button: derive(-fx-accent, 95%); -bs-progress-bar-track: #e0e0e0; -bs-white: white; -bs-prompt-text: -fx-control-inner-background; -bs-soft-red: #aa4c3b; -bs-turquoise-light: #11eeee; -bs-color-border-form-field: -bs-background-gray; -bs-color-background-form-field-readonly: -bs-color-gray-1; -bs-color-background-pane: -bs-background-color; -bs-color-background-row-even: -bs-color-background-pane; -bs-color-background-row-odd: derive(-bs-color-background-pane, -6%); -bs-color-table-cell-dim: -bs-color-gray-ccc; -bs-color-background-popup: white; -bs-color-background-popup-blur: white; -bs-color-background-popup-input: -bs-color-gray-background; -bs-color-background-form-field: white; -bs-text-color-dim1: black; -bs-text-color-dim2: black; /* Monero orange color code */ -xmr-orange: #f26822; -bs-support-chat-background: #4b4b4b; } /* set a javafx.scene.paint.Color of black for text in the table view */ .table-view { -fx-dark-text-color: black; -fx-mid-text-color: black; -fx-light-text-color: black; } .warning-box { -fx-background-color: -bs-yellow-light; } .progress-bar > .secondary-bar { -fx-background-color: -bs-color-gray-3; } #image-logo-splash { -fx-image: url("../../images/logo_splash_light_mode.png"); } #image-logo-splash-testnet { -fx-image: url("../../images/logo_splash_testnet_light_mode.png"); } #image-logo-landscape { -fx-image: url("../../images/logo_landscape_light_mode.png"); } #charts .default-color0.chart-series-area-fill { -fx-fill: linear-gradient(to bottom, rgba(62, 163, 74, 0.45) 0%, rgba(62, 163, 74, 0.0) 100% ); } #charts .default-color1.chart-series-area-fill { -fx-fill: linear-gradient(to bottom, rgba(215, 48, 48, 0.45) 0%, rgba(215, 48, 48, 0.0) 100% ); } /* All inputs have border in light mode. */ .jfx-combo-box, .jfx-text-field, .jfx-text-area, .jfx-password-field { -fx-border-color: -bs-color-border-form-field; } ================================================ FILE: desktop/src/main/java/haveno/desktop/util/AxisInlierUtils.java ================================================ /* * 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 . */ package haveno.desktop.util; import haveno.common.util.Tuple2; import haveno.core.util.InlierUtil; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; import java.util.List; import java.util.stream.Collectors; public class AxisInlierUtils { /* Returns a ListChangeListener that is meant to be attached to an * ObservableList. On event, it triggers a recalculation of a provided * axis' range so as to zoom in on inliers. */ public static ListChangeListener> getListenerThatZoomsToInliers( NumberAxis axis, int maxNumberOfTicks, double percentToTrim, double howManyStdDevsConstituteOutlier ) { return change -> { boolean axisHasBeenInitialized = axis != null; if (axisHasBeenInitialized) { zoomToInliers( axis, change.getList(), maxNumberOfTicks, percentToTrim, howManyStdDevsConstituteOutlier ); } }; } /* Applies the inlier range to the axis bounds and sets an appropriate tick-unit. * The methods describing the arguments passed here are `computeReferenceTickUnit`, * `trim`, and `computeInlierThreshold`. */ public static void zoomToInliers( NumberAxis yAxis, ObservableList> xyValues, int maxNumberOfTicks, double percentToTrim, double howManyStdDevsConstituteOutlier ) { List yValues = extractYValues(xyValues); if (yValues.size() < 3) { // with less than 3 elements, there is no meaningful inlier analysis return; } Tuple2 inlierRange = InlierUtil.findInlierRange(yValues, percentToTrim, howManyStdDevsConstituteOutlier); applyRange(yAxis, maxNumberOfTicks, inlierRange); } private static List extractYValues(ObservableList> xyValues) { return xyValues .stream() .map(xyData -> (double) xyData.getYValue()) .collect(Collectors.toList()); } /* On the given axis, sets the provided lower and upper bounds, and * computes an appropriate major tick unit (distance between major ticks in data-space). * External computation of tick unit is necessary, because JavaFX doesn't support automatic * tick unit computation when axis bounds are set manually. */ private static void applyRange(NumberAxis axis, int maxNumberOfTicks, Tuple2 bounds) { var boundsWidth = getBoundsWidth(bounds); if (boundsWidth < 0) { throw new IllegalArgumentException( "The lower bound must be a smaller number than the upper bound"); } if (boundsWidth == 0 || Double.isNaN(boundsWidth)) { // less than 2 unique data-points: recalculating axis range doesn't make sense return; } axis.setAutoRanging(false); var lowerBound = bounds.first; var upperBound = bounds.second; // If one of the ends of the range weren't zero, // additional logic would be needed to make ticks "round". // Of course, many, if not most, charts benefit from having 0 on the axis. if (lowerBound > 0) { lowerBound = 0d; } else if (upperBound < 0) { upperBound = 0d; } axis.setLowerBound(lowerBound); axis.setUpperBound(upperBound); var referenceTickUnit = computeReferenceTickUnit(maxNumberOfTicks, bounds); var tickUnit = computeTickUnit(referenceTickUnit); axis.setTickUnit(tickUnit); } /* Uses bounds and maximum number of major ticks to find a reference tick unit * for the `computeTickUnit` method. The reference tick unit is later used as a * starting point for tick unit's search. * The rationale behind dividing the range/domain/width of an axis by maximum number * of ticks is that it yields a good number of ticks, but they are not "well rounded", * hence the next step of computing the actual tick unit. * `maxNumberOfTicks` specifies how many subdivisions (major tick units) an axis * should have at most. The final number of subdivisions, after `computeTickUnit`, * usually will be lower, but never higher. */ private static double computeReferenceTickUnit(int maxNumberOfTicks, Tuple2 bounds) { if (maxNumberOfTicks <= 0) { throw new IllegalArgumentException("maxNumberOfTicks must be a positive number"); } var width = getBoundsWidth(bounds); return width / maxNumberOfTicks; } /* Extracted from cern.extjfx.chart.DefaultTickUnitSupplier (licensed Apache 2.0). * Original description below; note that the `multipliers` vector is hardcoded in the method to the default value * used in the source class: * * Computes tick unit using the following formula: tickUnit = M*10^E, where M is one of the multipliers specified in * the constructor and E is an exponent of 10. Both M and E are selected so that the calculated unit is the smallest * (closest to the zero) value that is greater than or equal to the reference tick unit. * * For example with multipliers [1, 2, 5], the method will give the following results: * * computeTickUnit(0.01) returns 0.01 * computeTickUnit(0.42) returns 0.5 * computeTickUnit(1.73) returns 2 * computeTickUnit(5) returns 5 * computeTickUnit(27) returns 50 * * @param referenceTickUnit the reference tick unit, must be a positive number */ private static double computeTickUnit(double referenceTickUnit) { if (referenceTickUnit <= 0) { throw new IllegalArgumentException("The reference tick unit must be a positive number"); } // Default multipliers vector extracted from the source class. double[] multipliers = {1d, 2.5, 5d}; int BASE = 10; int exp = (int) Math.floor(Math.log10(referenceTickUnit)); double factor = referenceTickUnit / Math.pow(BASE, exp); double multiplier = 0; int lastIndex = multipliers.length - 1; if (factor > multipliers[lastIndex]) { exp++; multiplier = multipliers[0]; } else { for (int i = lastIndex; i >= 0; i--) { if (factor <= multipliers[i]) { multiplier = multipliers[i]; } else { break; } } } return multiplier * Math.pow(BASE, exp); } private static double getBoundsWidth(Tuple2 bounds) { var lowerBound = bounds.first; var upperBound = bounds.second; return Math.abs(upperBound - lowerBound); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/util/Colors.java ================================================ /* * 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 . */ package haveno.desktop.util; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; public class Colors { public static final Paint BLUE = Color.valueOf("#0f87c3"); public static final Paint LIGHT_GREY = Color.valueOf("#CCCCCC"); public static final Paint GREEN = Color.valueOf("#00aa33"); public static final Color AVATAR_RED = Color.rgb(255, 0, 0); public static final Color AVATAR_ORANGE = Color.rgb(255, 140, 0); public static final Color AVATAR_BLUE = Color.rgb(0, 139, 205); public static final Color AVATAR_GREEN = Color.rgb(0, 225, 0); public static final Color AVATAR_GREY = Color.rgb(128, 128, 128); } ================================================ FILE: desktop/src/main/java/haveno/desktop/util/CssTheme.java ================================================ /* * 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 . */ package haveno.desktop.util; import javafx.scene.Scene; public class CssTheme { public static final int CSS_THEME_LIGHT = 0; public static final int CSS_THEME_DARK = 1; private static int currentCSSTheme; private static boolean useDevHeader; public static void loadSceneStyles(Scene scene, int cssTheme, boolean devHeader) { String cssThemeFolder = "/haveno/desktop/"; String cssThemeFile = ""; currentCSSTheme = cssTheme; useDevHeader = devHeader; switch (cssTheme) { case CSS_THEME_DARK: cssThemeFile = "theme-dark.css"; break; case CSS_THEME_LIGHT: default: cssThemeFile = "theme-light.css"; break; } scene.getStylesheets().setAll( // load base styles first cssThemeFolder + "haveno.css", cssThemeFolder + "images.css", cssThemeFolder + "CandleStickChart.css", // load theme last to allow override cssThemeFolder + cssThemeFile ); if (useDevHeader) scene.getStylesheets().add(cssThemeFolder + "theme-dev.css"); } public static int getCurrentTheme() { return currentCSSTheme; } public static boolean isDarkTheme() { return currentCSSTheme == CSS_THEME_DARK; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/util/CurrencyList.java ================================================ /* * 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 . */ package haveno.desktop.util; import com.google.common.collect.Lists; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TradeCurrency; import haveno.core.user.Preferences; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.BiFunction; public class CurrencyList { private final CurrencyPredicates predicates; private final Preferences preferences; private final List delegate; public CurrencyList(Preferences preferences) { this(new ArrayList<>(), preferences, new CurrencyPredicates()); } public CurrencyList(List delegate, Preferences preferences, CurrencyPredicates predicates) { this.delegate = delegate; this.predicates = predicates; this.preferences = preferences; } public ObservableList getObservableList() { return FXCollections.observableList(delegate); } public void updateWithCurrencies(List currencies, @Nullable CurrencyListItem first) { List result = Lists.newLinkedList(); Optional.ofNullable(first).ifPresent(result::add); result.addAll(getPartitionedSortedItems(currencies)); delegate.clear(); delegate.addAll(result); } private List getPartitionedSortedItems(List currencies) { Map tradesPerCurrency = countTrades(currencies); List fiatCurrencies = new ArrayList<>(); List traditionalCurrencies = new ArrayList<>(); List cryptoCurrencies = new ArrayList<>(); for (Map.Entry entry : tradesPerCurrency.entrySet()) { TradeCurrency currency = entry.getKey(); Integer count = entry.getValue(); CurrencyListItem item = new CurrencyListItem(currency, count); if (predicates.isFiatCurrency(currency)) { fiatCurrencies.add(item); } else if (predicates.isTraditionalCurrency(currency)) { traditionalCurrencies.add(item); } if (predicates.isCryptoCurrency(currency)) { cryptoCurrencies.add(item); } } Comparator comparator = getComparator(); fiatCurrencies.sort(comparator); traditionalCurrencies.sort(comparator); cryptoCurrencies.sort(comparator); List result = new ArrayList<>(); result.addAll(fiatCurrencies); result.addAll(traditionalCurrencies); result.addAll(cryptoCurrencies); return result; } private Comparator getComparator() { if (preferences.isSortMarketCurrenciesNumerically()) { return Comparator .comparingInt((CurrencyListItem item) -> item.numTrades).reversed() .thenComparing(item -> CurrencyUtil.isCryptoCurrency(item.tradeCurrency.getCode()) ? item.tradeCurrency.getName() : item.tradeCurrency.getCode()); } else { return Comparator.comparing(item -> CurrencyUtil.isCryptoCurrency(item.tradeCurrency.getCode()) ? item.tradeCurrency.getName() : item.tradeCurrency.getCode()); } } private Map countTrades(List currencies) { Map result = new HashMap<>(); BiFunction incrementCurrentOrOne = (key, value) -> value == null ? 1 : value + 1; currencies.forEach(currency -> result.compute(currency, incrementCurrentOrOne)); Set preferred = new HashSet<>(); preferred.addAll(preferences.getTraditionalCurrencies()); preferred.addAll(preferences.getCryptoCurrencies()); preferred.forEach(currency -> result.putIfAbsent(currency, 0)); return result; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/util/CurrencyListItem.java ================================================ /* * 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 . */ package haveno.desktop.util; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; public class CurrencyListItem { public final TradeCurrency tradeCurrency; public final int numTrades; public CurrencyListItem(TradeCurrency tradeCurrency, int numTrades) { this.tradeCurrency = tradeCurrency; this.numTrades = numTrades; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; CurrencyListItem that = (CurrencyListItem) o; //noinspection SimplifiableIfStatement if (numTrades != that.numTrades) return false; return !(tradeCurrency != null ? !tradeCurrency.equals(that.tradeCurrency) : that.tradeCurrency != null); } @Override public int hashCode() { int result = tradeCurrency != null ? tradeCurrency.hashCode() : 0; result = 31 * result + numTrades; return result; } @Override public String toString() { return "CurrencyListItem{" + "tradeCurrency=" + tradeCurrency + ", numTrades=" + numTrades + '}'; } public String codeDashNameString() { if (isSpecialShowAllItem()) return Res.get(GUIUtil.SHOW_ALL_FLAG); else return tradeCurrency.getName() + " (" + tradeCurrency.getCode() + ")"; } private boolean isSpecialShowAllItem() { return tradeCurrency.getCode().equals(GUIUtil.SHOW_ALL_FLAG); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/util/CurrencyPredicates.java ================================================ /* * 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 . */ package haveno.desktop.util; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TradeCurrency; class CurrencyPredicates { boolean isCryptoCurrency(TradeCurrency currency) { return CurrencyUtil.isCryptoCurrency(currency.getCode()); } boolean isFiatCurrency(TradeCurrency currency) { return CurrencyUtil.isFiatCurrency(currency.getCode()); } boolean isTraditionalCurrency(TradeCurrency currency) { return CurrencyUtil.isTraditionalCurrency(currency.getCode()); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/util/DisplayUtils.java ================================================ package haveno.desktop.util; import haveno.common.crypto.PubKeyRing; import haveno.core.account.witness.AccountAgeWitness; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.GlobalSettings; import haveno.core.locale.Res; import haveno.core.monetary.Price; import haveno.core.monetary.Volume; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.trade.HavenoUtils; import haveno.core.util.FormattingUtils; import haveno.core.util.ParsingUtils; import haveno.core.util.VolumeUtil; import haveno.core.util.coin.CoinFormatter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DurationFormatUtils; import org.bitcoinj.core.Coin; import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Optional; @Slf4j public class DisplayUtils { private static final int SCALE = 3; private static final String LOCKED = ".locked"; public static String formatDateTime(Date date) { return FormattingUtils.formatDateTime(date, true); } public static String formatDateTimeSpan(Date dateFrom, Date dateTo) { if (dateFrom != null && dateTo != null) { DateFormat dateFormatter = DateFormat.getDateInstance(DateFormat.DEFAULT, GlobalSettings.getLocale()); DateFormat timeFormatter = DateFormat.getTimeInstance(DateFormat.DEFAULT, GlobalSettings.getLocale()); return dateFormatter.format(dateFrom) + " " + timeFormatter.format(dateFrom) + FormattingUtils.RANGE_SEPARATOR + timeFormatter.format(dateTo); } else { return ""; } } public static String formatTime(Date date) { if (date != null) { DateFormat timeFormatter = DateFormat.getTimeInstance(DateFormat.DEFAULT, GlobalSettings.getLocale()); return timeFormatter.format(date); } else { return ""; } } public static String formatDate(Date date) { if (date != null) { DateFormat dateFormatter = DateFormat.getDateInstance(DateFormat.DEFAULT, GlobalSettings.getLocale()); return dateFormatter.format(date); } else { return ""; } } public static String formatDateAxis(Date date, String format) { if (date != null) { SimpleDateFormat dateFormatter = new SimpleDateFormat(format, GlobalSettings.getLocale()); return dateFormatter.format(date); } else { return ""; } } public static String getAccountWitnessDescription(AccountAgeWitnessService accountAgeWitnessService, PaymentMethod paymentMethod, PaymentAccountPayload paymentAccountPayload, PubKeyRing pubKeyRing) { String description = Res.get("peerInfoIcon.tooltip.unknownAge"); Optional aaw = accountAgeWitnessService.findWitness(paymentAccountPayload, pubKeyRing); if (aaw.isPresent()) { long accountAge = accountAgeWitnessService.getAccountAge(aaw.get(), new Date()); long signAge = -1L; if (PaymentMethod.hasChargebackRisk(paymentMethod)) { signAge = accountAgeWitnessService.getWitnessSignAge(aaw.get(), new Date()); } if (signAge > -1) { description = Res.get("peerInfo.age.chargeBackRisk") + ": " + formatAccountAge(signAge); } else if (accountAge > -1) { description = Res.get("peerInfoIcon.tooltip.age", formatAccountAge(accountAge)); if (PaymentMethod.hasChargebackRisk(paymentMethod)) { description += ", " + Res.get("offerbook.timeSinceSigning.notSigned"); } } } return description; } public static String formatAccountAge(long durationMillis) { durationMillis = Math.max(0, durationMillis); String day = Res.get("time.day").toLowerCase(); String days = Res.get("time.days"); String format = " d' " + days + "'"; return StringUtils.strip(StringUtils.replaceOnce(DurationFormatUtils.formatDuration(durationMillis, format), " 1 " + days, " 1 " + day)); } public static String booleanToYesNo(boolean value) { return value ? Res.get("shared.yes") : Res.get("shared.no"); } /////////////////////////////////////////////////////////////////////////////////////////// // Offer direction /////////////////////////////////////////////////////////////////////////////////////////// public static String getDirectionWithCode(OfferDirection direction, String currencyCode, boolean isPrivate) { return (direction == OfferDirection.BUY) ? Res.get("shared.buyCurrency" + (isPrivate ? LOCKED : ""), Res.getBaseCurrencyCode()) : Res.get("shared.sellCurrency" + (isPrivate ? LOCKED : ""), Res.getBaseCurrencyCode()); } public static String getDirectionBothSides(OfferDirection direction, boolean isPrivate) { String currencyCode = Res.getBaseCurrencyCode(); return direction == OfferDirection.BUY ? Res.get("formatter.makerTaker" + (isPrivate ? LOCKED : ""), currencyCode, Res.get("shared.buyer"), currencyCode, Res.get("shared.seller")) : Res.get("formatter.makerTaker" + (isPrivate ? LOCKED : ""), currencyCode, Res.get("shared.seller"), currencyCode, Res.get("shared.buyer")); } public static String getDirectionForBuyer(boolean isMyOffer, String currencyCode) { String code = Res.getBaseCurrencyCode(); return isMyOffer ? Res.get("formatter.youAreAsMaker", Res.get("shared.buyer"), code, Res.get("shared.seller"), code) : Res.get("formatter.youAreAsTaker", Res.get("shared.buyer"), code, Res.get("shared.seller"), code); } public static String getDirectionForSeller(boolean isMyOffer, String currencyCode) { String code = Res.getBaseCurrencyCode(); return isMyOffer ? Res.get("formatter.youAreAsMaker", Res.get("shared.seller"), code, Res.get("shared.buyer"), code) : Res.get("formatter.youAreAsTaker", Res.get("shared.seller"), code, Res.get("shared.buyer"), code); } public static String getDirectionForTakeOffer(OfferDirection direction, String currencyCode) { String baseCurrencyCode = Res.getBaseCurrencyCode(); return direction == OfferDirection.BUY ? Res.get("formatter.youAre", Res.get("shared.selling"), baseCurrencyCode, Res.get("shared.buying"), currencyCode) : Res.get("formatter.youAre", Res.get("shared.buying"), baseCurrencyCode, Res.get("shared.selling"), currencyCode); } public static String getOfferDirectionForCreateOffer(OfferDirection direction, String currencyCode, boolean isPrivate) { String baseCurrencyCode = Res.getBaseCurrencyCode(); return direction == OfferDirection.BUY ? Res.get("formatter.youAreCreatingAnOffer.traditional" + (isPrivate ? LOCKED : ""), Res.get("shared.buy"), baseCurrencyCode) : Res.get("formatter.youAreCreatingAnOffer.traditional" + (isPrivate ? LOCKED : ""), Res.get("shared.sell"), baseCurrencyCode); } /////////////////////////////////////////////////////////////////////////////////////////// // Amount /////////////////////////////////////////////////////////////////////////////////////////// public static String formatAmount(Offer offer, CoinFormatter coinFormatter) { return offer.isRange() ? HavenoUtils.formatXmr(offer.getMinAmount()) + FormattingUtils.RANGE_SEPARATOR + HavenoUtils.formatXmr(offer.getAmount()) : HavenoUtils.formatXmr(offer.getAmount()); } public static String formatAmount(Offer offer, int decimalPlaces, boolean decimalAligned, int maxPlaces, CoinFormatter coinFormatter) { String formattedAmount = offer.isRange() ? HavenoUtils.formatXmr(offer.getMinAmount(), decimalPlaces) + FormattingUtils.RANGE_SEPARATOR + HavenoUtils.formatXmr(offer.getAmount(), decimalPlaces) : HavenoUtils.formatXmr(offer.getAmount(), decimalPlaces); if (decimalAligned) { formattedAmount = FormattingUtils.fillUpPlacesWithEmptyStrings(formattedAmount, maxPlaces); } return formattedAmount; } /////////////////////////////////////////////////////////////////////////////////////////// // Other /////////////////////////////////////////////////////////////////////////////////////////// public static String formatPrice(Price price, Boolean decimalAligned, int maxPlaces) { String formattedPrice = FormattingUtils.formatPrice(price); if (decimalAligned) { formattedPrice = FormattingUtils.fillUpPlacesWithEmptyStrings(formattedPrice, maxPlaces); } return formattedPrice; } public static String getFeeWithFiatAmount(BigInteger makerFee, Optional optionalFeeInFiat, CoinFormatter formatter) { String feeInXmr = makerFee != null ? HavenoUtils.formatXmr(makerFee, true) : Res.get("shared.na"); if (optionalFeeInFiat != null && optionalFeeInFiat.isPresent()) { String feeInFiat = VolumeUtil.formatAverageVolumeWithCode(optionalFeeInFiat.get()); return Res.get("feeOptionWindow.fee", feeInXmr, feeInFiat); } else { return feeInXmr; } } /** * Converts to a coin with max. 4 decimal places. Last place gets rounded. *

    0.01234 -> 0.0123 *

    0.01235 -> 0.0124 * * @param input the decimal coin value to parse and round * @param coinFormatter the coin formatter instance * @return the converted coin */ public static Coin parseToCoinWith4Decimals(String input, CoinFormatter coinFormatter) { try { return Coin.valueOf( new BigDecimal(ParsingUtils.parseToCoin(ParsingUtils.cleanDoubleInput(input), coinFormatter).value) .setScale(-SCALE - 1, RoundingMode.HALF_UP) .setScale(SCALE + 1, RoundingMode.HALF_UP) .toBigInteger().longValue() ); } catch (Throwable t) { if (input != null && input.length() > 0) log.warn("Exception at parseToCoinWith4Decimals: " + t.toString()); return Coin.ZERO; } } public static boolean hasBtcValidDecimals(String input, CoinFormatter coinFormatter) { return ParsingUtils.parseToCoin(input, coinFormatter).equals(parseToCoinWith4Decimals(input, coinFormatter)); } /** * Transform a coin with the properties defined in the format (used to reduce decimal places) * * @param coin the coin which should be transformed * @param coinFormatter the coin formatter instance * @return the transformed coin */ public static Coin reduceTo4Decimals(Coin coin, CoinFormatter coinFormatter) { return ParsingUtils.parseToCoin(coinFormatter.formatCoin(coin), coinFormatter); } public static String createAccountName(String paymentMethodId, String name) { name = name.trim(); name = StringUtils.abbreviate(name, 9); String method = Res.get(paymentMethodId); return method.concat(": ").concat(name); } public static String createAssetsAccountName(PaymentAccount paymentAccount, String address) { String currency = paymentAccount.getSingleTradeCurrency() != null ? paymentAccount.getSingleTradeCurrency().getCode() : ""; return createAssetsAccountName(currency, address); } public static String createAssetsAccountName(String currency, String address) { address = StringUtils.abbreviate(address, 9); return currency.concat(": ").concat(address); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/util/FormBuilder.java ================================================ /* * 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 . */ package haveno.desktop.util; import com.jfoenix.controls.JFXComboBox; //import com.jfoenix.controls.JFXDatePicker; import com.jfoenix.controls.JFXTextArea; import com.jfoenix.controls.JFXToggleButton; import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.glyphs.GlyphIcons; import de.jensd.fx.glyphs.materialdesignicons.utils.MaterialDesignIconFactory; import haveno.common.util.Tuple2; import haveno.common.util.Tuple3; import haveno.common.util.Tuple4; import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.desktop.components.AddressTextField; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipCheckBox; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.AutoTooltipRadioButton; import haveno.desktop.components.AutoTooltipSlideToggleButton; import haveno.desktop.components.AutoTooltipTextField; import haveno.desktop.components.AutocompleteComboBox; import haveno.desktop.components.BalanceTextField; import haveno.desktop.components.BusyAnimation; import haveno.desktop.components.ExplorerAddressTextField; import haveno.desktop.components.ExternalHyperlink; import haveno.desktop.components.FundsTextField; import haveno.desktop.components.HavenoTextArea; import haveno.desktop.components.HavenoTextField; import haveno.desktop.components.HyperlinkWithIcon; import haveno.desktop.components.InfoInputTextField; import haveno.desktop.components.InfoTextField; import haveno.desktop.components.InputTextField; import haveno.desktop.components.PasswordTextField; import haveno.desktop.components.SimpleMarkdownLabel; import haveno.desktop.components.TextFieldWithCopyIcon; import haveno.desktop.components.TextFieldWithIcon; import haveno.desktop.components.TitledGroupBg; import haveno.desktop.components.TxIdTextField; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.geometry.VPos; import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.ContentDisplay; import javafx.scene.control.DatePicker; import javafx.scene.control.Label; import javafx.scene.control.ListView; import javafx.scene.control.RadioButton; import javafx.scene.control.TableView; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleGroup; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import org.jetbrains.annotations.NotNull; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import static haveno.desktop.util.GUIUtil.getComboBoxButtonCell; public class FormBuilder { private static final String MATERIAL_DESIGN_ICONS = "'Material Design Icons'"; /////////////////////////////////////////////////////////////////////////////////////////// // TitledGroupBg /////////////////////////////////////////////////////////////////////////////////////////// public static TitledGroupBg addTitledGroupBg(GridPane gridPane, int rowIndex, int rowSpan, String title) { return addTitledGroupBg(gridPane, rowIndex, rowSpan, title, 0); } public static TitledGroupBg addTitledGroupBg(GridPane gridPane, int rowIndex, int columnIndex, int rowSpan, String title) { TitledGroupBg titledGroupBg = addTitledGroupBg(gridPane, rowIndex, rowSpan, title, 0); GridPane.setColumnIndex(titledGroupBg, columnIndex); return titledGroupBg; } public static TitledGroupBg addTitledGroupBg(GridPane gridPane, int rowIndex, int columnIndex, int rowSpan, String title, double top) { TitledGroupBg titledGroupBg = addTitledGroupBg(gridPane, rowIndex, rowSpan, title, top); GridPane.setColumnIndex(titledGroupBg, columnIndex); return titledGroupBg; } public static TitledGroupBg addTitledGroupBg(GridPane gridPane, int rowIndex, int rowSpan, String title, double top) { TitledGroupBg titledGroupBg = new TitledGroupBg(); titledGroupBg.setText(title); titledGroupBg.prefWidthProperty().bind(gridPane.widthProperty()); GridPane.setRowIndex(titledGroupBg, rowIndex); GridPane.setRowSpan(titledGroupBg, rowSpan); GridPane.setMargin(titledGroupBg, new Insets(top + 8, -10, -12, -10)); gridPane.getChildren().add(titledGroupBg); return titledGroupBg; } /////////////////////////////////////////////////////////////////////////////////////////// // Divider /////////////////////////////////////////////////////////////////////////////////////////// public static Region addSeparator(GridPane gridPane, int rowIndex) { Region separator = new Region(); separator.getStyleClass().add("grid-pane-separator"); separator.setPrefHeight(1); separator.setMinHeight(1); separator.setMaxHeight(1); GridPane.setRowIndex(separator, rowIndex); GridPane.setColumnIndex(separator, 0); GridPane.setColumnSpan(separator, 2); gridPane.getChildren().add(separator); separator.setPrefHeight(1); GridPane.setMargin(separator, new Insets(0, 0, 3, 0)); return separator; } /////////////////////////////////////////////////////////////////////////////////////////// // Label /////////////////////////////////////////////////////////////////////////////////////////// public static Label addLabel(GridPane gridPane, int rowIndex, String title) { return addLabel(gridPane, rowIndex, title, 0); } public static Label addLabel(GridPane gridPane, int rowIndex, String title, double top) { Label label = new AutoTooltipLabel(title); GridPane.setRowIndex(label, rowIndex); GridPane.setMargin(label, new Insets(top, 0, 0, 0)); gridPane.getChildren().add(label); return label; } /////////////////////////////////////////////////////////////////////////////////////////// // Label + Subtext /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple3 addLabelWithSubText(GridPane gridPane, int rowIndex, String title, String description) { return addLabelWithSubText(gridPane, rowIndex, title, description, 0); } public static Tuple3 addLabelWithSubText(GridPane gridPane, int rowIndex, String title, String description, double top) { Label label = new AutoTooltipLabel(title); Label subText = new AutoTooltipLabel(description); VBox vBox = new VBox(); vBox.getChildren().setAll(label, subText); GridPane.setRowIndex(vBox, rowIndex); GridPane.setMargin(vBox, new Insets(top, 0, 0, 0)); gridPane.getChildren().add(vBox); return new Tuple3<>(label, subText, vBox); } /////////////////////////////////////////////////////////////////////////////////////////// // Simple Markdown Label /////////////////////////////////////////////////////////////////////////////////////////// public static SimpleMarkdownLabel addSimpleMarkdownLabel(GridPane gridPane, int rowIndex) { return addSimpleMarkdownLabel(gridPane, rowIndex, null, 0); } public static SimpleMarkdownLabel addSimpleMarkdownLabel(GridPane gridPane, int rowIndex, String markdown, double top) { SimpleMarkdownLabel label = new SimpleMarkdownLabel(markdown); GridPane.setRowIndex(label, rowIndex); GridPane.setMargin(label, new Insets(top, 0, 0, 0)); gridPane.getChildren().add(label); return label; } /////////////////////////////////////////////////////////////////////////////////////////// // Multiline Label /////////////////////////////////////////////////////////////////////////////////////////// public static Label addMultilineLabel(GridPane gridPane, int rowIndex) { return addMultilineLabel(gridPane, rowIndex, 0); } public static Label addMultilineLabel(GridPane gridPane, int rowIndex, String text) { return addMultilineLabel(gridPane, rowIndex, text, 0); } public static Label addMultilineLabel(GridPane gridPane, int rowIndex, double top) { return addMultilineLabel(gridPane, rowIndex, "", top); } public static Label addMultilineLabel(GridPane gridPane, int rowIndex, String text, double top) { return addMultilineLabel(gridPane, rowIndex, text, top, 600); } public static Label addMultilineLabel(GridPane gridPane, int rowIndex, String text, double top, double maxWidth) { Label label = new AutoTooltipLabel(text); label.setWrapText(true); label.setMaxWidth(maxWidth); GridPane.setHalignment(label, HPos.LEFT); GridPane.setHgrow(label, Priority.ALWAYS); GridPane.setRowIndex(label, rowIndex); GridPane.setMargin(label, new Insets(top + Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); gridPane.getChildren().add(label); return label; } /////////////////////////////////////////////////////////////////////////////////////////// // Label + TextField /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple3 addTopLabelReadOnlyTextField(GridPane gridPane, int rowIndex, String title) { return addTopLabelTextField(gridPane, rowIndex, title, "", -15); } public static Tuple3 addTopLabelReadOnlyTextField(GridPane gridPane, int rowIndex, int columnIndex, String title) { Tuple3 tuple = addTopLabelTextField(gridPane, rowIndex, title, "", -15); GridPane.setColumnIndex(tuple.third, columnIndex); return tuple; } public static Tuple3 addTopLabelReadOnlyTextField(GridPane gridPane, int rowIndex, String title, double top) { return addTopLabelTextField(gridPane, rowIndex, title, "", top - 15); } public static Tuple3 addTopLabelReadOnlyTextField(GridPane gridPane, int rowIndex, String title, String value) { return addTopLabelReadOnlyTextField(gridPane, rowIndex, title, value, 0); } public static Tuple3 addTopLabelReadOnlyTextField(GridPane gridPane, int rowIndex, int columnIndex, String title, String value, double top) { Tuple3 tuple = addTopLabelTextField(gridPane, rowIndex, title, value, top - 15); GridPane.setColumnIndex(tuple.third, columnIndex); return tuple; } public static Tuple3 addTopLabelReadOnlyTextField(GridPane gridPane, int rowIndex, int columnIndex, String title, double top) { Tuple3 tuple = addTopLabelTextField(gridPane, rowIndex, title, "", top - 15); GridPane.setColumnIndex(tuple.third, columnIndex); return tuple; } public static Tuple3 addTopLabelReadOnlyTextField(GridPane gridPane, int rowIndex, String title, String value, double top) { return addTopLabelTextField(gridPane, rowIndex, title, value, top - 15); } public static Tuple3 addTopLabelTextField(GridPane gridPane, int rowIndex, String title) { return addTopLabelTextField(gridPane, rowIndex, title, "", 0); } public static Tuple3 addCompactTopLabelTextField(GridPane gridPane, int rowIndex, String title, String value) { return addTopLabelTextField(gridPane, rowIndex, title, value, -Layout.FLOATING_LABEL_DISTANCE); } public static Tuple3 addCompactTopLabelTextField(GridPane gridPane, int rowIndex, int colIndex, String title, String value) { final Tuple3 labelTextFieldVBoxTuple3 = addTopLabelTextField(gridPane, rowIndex, title, value, -Layout.FLOATING_LABEL_DISTANCE); GridPane.setColumnIndex(labelTextFieldVBoxTuple3.third, colIndex); return labelTextFieldVBoxTuple3; } public static Tuple3 addCompactTopLabelTextField(GridPane gridPane, int rowIndex, String title, String value, double top) { return addTopLabelTextField(gridPane, rowIndex, title, value, top - Layout.FLOATING_LABEL_DISTANCE); } public static Tuple3 addTopLabelTextField(GridPane gridPane, int rowIndex, String title, String value) { return addTopLabelTextField(gridPane, rowIndex, title, value, 0); } public static Tuple3 addTopLabelTextField(GridPane gridPane, int rowIndex, String title, double top) { return addTopLabelTextField(gridPane, rowIndex, title, "", top); } public static Tuple3 addTopLabelTextField(GridPane gridPane, int rowIndex, String title, String value, double top) { TextField textField = new HavenoTextField(value); textField.setEditable(false); textField.setFocusTraversable(false); final Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, rowIndex, title, textField, top); // TODO not 100% sure if that is a good idea.... //topLabelWithVBox.first.getStyleClass().add("jfx-text-field-top-label"); return new Tuple3<>(topLabelWithVBox.first, textField, topLabelWithVBox.second); } public static Tuple2 addTextFieldWithEditButton(GridPane gridPane, int rowIndex, String title) { TextField textField = new HavenoTextField(); textField.setPromptText(title); textField.setEditable(false); textField.setFocusTraversable(false); textField.setPrefWidth(Layout.INITIAL_WINDOW_WIDTH); Button button = new AutoTooltipButton("..."); button.setStyle("-fx-min-width: 32; -fx-padding: 0 0 10 0; -fx-background-color: -fx-background;"); button.managedProperty().bind(button.visibleProperty()); HBox hbox = new HBox(textField, button); hbox.setAlignment(Pos.CENTER_LEFT); hbox.setSpacing(8); VBox vbox = getTopLabelVBox(0); vbox.setSpacing(2); vbox.getChildren().addAll(getTopLabel(title), hbox); gridPane.getChildren().add(vbox); GridPane.setRowIndex(vbox, rowIndex); GridPane.setMargin(vbox, new Insets(Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); return new Tuple2<>(textField, button); } /////////////////////////////////////////////////////////////////////////////////////////// // Confirmation Fields /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple2 addConfirmationLabelLabel(GridPane gridPane, int rowIndex, String title1, String title2, double top) { return addConfirmationLabelLabel(gridPane, false, rowIndex, title1, title2, top); } public static Tuple2 addConfirmationLabelLabel(GridPane gridPane, int rowIndex, String title1, String title2) { return addConfirmationLabelLabel(gridPane, false, rowIndex, title1, title2, 0); } public static Tuple2 addConfirmationLabelLabel(GridPane gridPane, boolean isWrapped, int rowIndex, String title1, String title2) { return addConfirmationLabelLabel(gridPane, isWrapped, rowIndex, title1, title2, 0); } public static Tuple2 addConfirmationLabelLabel(GridPane gridPane, boolean isWrapped, int rowIndex, String title1, String title2, double top) { Label label1 = addLabel(gridPane, rowIndex, title1); label1.getStyleClass().add("confirmation-label"); Label label2 = addLabel(gridPane, rowIndex, title2); label2.getStyleClass().add("confirmation-value"); label2.setWrapText(isWrapped); GridPane.setColumnIndex(label2, 1); GridPane.setMargin(label1, new Insets(top, 0, 0, 0)); GridPane.setHalignment(label1, HPos.LEFT); GridPane.setValignment(label1, VPos.TOP); GridPane.setMargin(label2, new Insets(top, 0, 0, 0)); GridPane.setHalignment(label2, HPos.LEFT); GridPane.setValignment(label2, VPos.TOP); return new Tuple2<>(label1, label2); } public static Tuple2 addConfirmationLabelTextField(GridPane gridPane, int rowIndex, String title1, String title2) { return addConfirmationLabelTextField(gridPane, rowIndex, title1, title2, 0); } public static Tuple2 addConfirmationLabelTextField(GridPane gridPane, int rowIndex, String title1, String title2, double top) { Label label1 = addLabel(gridPane, rowIndex, title1); label1.getStyleClass().add("confirmation-label"); TextField label2 = new HavenoTextField(title2); gridPane.getChildren().add(label2); label2.getStyleClass().add("confirmation-text-field-as-label"); label2.setEditable(false); label2.setFocusTraversable(false); GridPane.setRowIndex(label2, rowIndex); GridPane.setColumnIndex(label2, 1); GridPane.setMargin(label1, new Insets(top, 0, 0, 0)); GridPane.setHalignment(label1, HPos.LEFT); GridPane.setMargin(label2, new Insets(top, 0, 0, 0)); return new Tuple2<>(label1, label2); } public static Tuple2 addConfirmationLabelLabelWithCopyIcon(GridPane gridPane, int rowIndex, String title1, String title2) { Label label1 = addLabel(gridPane, rowIndex, title1); label1.getStyleClass().add("confirmation-label"); TextFieldWithCopyIcon label2 = new TextFieldWithCopyIcon("confirmation-value"); label2.setText(title2); GridPane.setRowIndex(label2, rowIndex); gridPane.getChildren().add(label2); GridPane.setColumnIndex(label2, 1); GridPane.setHalignment(label1, HPos.LEFT); return new Tuple2<>(label1, label2); } public static Tuple2 addConfirmationLabelTextArea(GridPane gridPane, int rowIndex, String title1, String title2, double top) { return addConfirmationLabelTextArea(gridPane, false, rowIndex, title1, title2, top); } public static Tuple2 addConfirmationLabelTextArea(GridPane gridPane, boolean isWrapped, int rowIndex, String title1, String title2, double top) { Label label = addLabel(gridPane, rowIndex, title1); label.getStyleClass().add("confirmation-label"); TextArea textArea = addTextArea(gridPane, rowIndex, title2); ((JFXTextArea) textArea).setLabelFloat(false); textArea.setWrapText(isWrapped); GridPane.setColumnIndex(textArea, 1); GridPane.setMargin(label, new Insets(top, 0, 0, 0)); GridPane.setHalignment(label, HPos.LEFT); GridPane.setValignment(label, VPos.TOP); GridPane.setMargin(textArea, new Insets(top, 0, 0, 0)); return new Tuple2<>(label, textArea); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + TextFieldWithIcon /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple2 addTopLabelTextFieldWithIcon(GridPane gridPane, int rowIndex, String title, double top) { return addTopLabelTextFieldWithIcon(gridPane, rowIndex, 0, title, top); } public static Tuple2 addTopLabelTextFieldWithIcon(GridPane gridPane, int rowIndex, int columnIndex, String title, double top) { TextFieldWithIcon textFieldWithIcon = new TextFieldWithIcon(); textFieldWithIcon.setFocusTraversable(false); return new Tuple2<>(addTopLabelWithVBox(gridPane, rowIndex, columnIndex, title, textFieldWithIcon, top).first, textFieldWithIcon); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + TextFieldWithIcon + Label /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple4 addTopLabelTextFieldWithIconLabel(GridPane gridPane, int rowIndex, String title, double top) { return addTopLabelTextFieldWithIconLabel(gridPane, rowIndex, 0, title, top); } public static Tuple4 addTopLabelTextFieldWithIconLabel(GridPane gridPane, int rowIndex, int columnIndex, String title, double top) { HBox hBox = new HBox(); hBox.setSpacing(10); TextFieldWithIcon textFieldWithIcon = new TextFieldWithIcon(); textFieldWithIcon.setFocusTraversable(false); Label label = new AutoTooltipLabel(); hBox.setAlignment(Pos.CENTER_LEFT); hBox.getChildren().addAll(textFieldWithIcon, label); GridPane.setRowIndex(hBox, rowIndex); GridPane.setColumnIndex(hBox, columnIndex); GridPane.setMargin(hBox, new Insets(top, 0, 0, 0)); gridPane.getChildren().add(hBox); Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, rowIndex, columnIndex, title, hBox, top); return new Tuple4<>(topLabelWithVBox.second, topLabelWithVBox.first, textFieldWithIcon, label); } /////////////////////////////////////////////////////////////////////////////////////////// // HyperlinkWithIcon /////////////////////////////////////////////////////////////////////////////////////////// public static HyperlinkWithIcon addHyperlinkWithIcon(GridPane gridPane, int rowIndex, String title, String url) { return addHyperlinkWithIcon(gridPane, rowIndex, title, url, 0); } public static HyperlinkWithIcon addHyperlinkWithIcon(GridPane gridPane, int rowIndex, String title, String url, double top) { HyperlinkWithIcon hyperlinkWithIcon = new ExternalHyperlink(title); hyperlinkWithIcon.setOnAction(e -> GUIUtil.openWebPage(url)); GridPane.setRowIndex(hyperlinkWithIcon, rowIndex); GridPane.setColumnIndex(hyperlinkWithIcon, 0); GridPane.setMargin(hyperlinkWithIcon, new Insets(top, 0, 0, 0)); GridPane.setHalignment(hyperlinkWithIcon, HPos.LEFT); gridPane.getChildren().add(hyperlinkWithIcon); return hyperlinkWithIcon; } /////////////////////////////////////////////////////////////////////////////////////////// // Label + HyperlinkWithIcon /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple2 addLabelHyperlinkWithIcon(GridPane gridPane, int rowIndex, String labelTitle, String title, String url) { return addLabelHyperlinkWithIcon(gridPane, rowIndex, labelTitle, title, url, 0); } public static Tuple2 addLabelHyperlinkWithIcon(GridPane gridPane, int rowIndex, String labelTitle, String title, String url, double top) { Label label = addLabel(gridPane, rowIndex, labelTitle, top); HyperlinkWithIcon hyperlinkWithIcon = new ExternalHyperlink(title); hyperlinkWithIcon.setOnAction(e -> GUIUtil.openWebPage(url)); GridPane.setRowIndex(hyperlinkWithIcon, rowIndex); GridPane.setMargin(hyperlinkWithIcon, new Insets(top, 0, 0, -4)); gridPane.getChildren().add(hyperlinkWithIcon); return new Tuple2<>(label, hyperlinkWithIcon); } public static Tuple3 addTopLabelHyperlinkWithIcon(GridPane gridPane, int rowIndex, int columnIndex, String title, String value, String url, double top) { Tuple3 tuple = addTopLabelHyperlinkWithIcon(gridPane, rowIndex, title, value, url, top); GridPane.setColumnIndex(tuple.third, columnIndex); return tuple; } public static Tuple3 addTopLabelHyperlinkWithIcon(GridPane gridPane, int rowIndex, String title, String value, String url, double top) { HyperlinkWithIcon hyperlinkWithIcon = new ExternalHyperlink(value); hyperlinkWithIcon.setOnAction(e -> GUIUtil.openWebPage(url)); hyperlinkWithIcon.getStyleClass().add("hyperlink-with-icon"); GridPane.setRowIndex(hyperlinkWithIcon, rowIndex); Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, rowIndex, title, hyperlinkWithIcon, top - 15); return new Tuple3<>(topLabelWithVBox.first, hyperlinkWithIcon, topLabelWithVBox.second); } /////////////////////////////////////////////////////////////////////////////////////////// // TextArea /////////////////////////////////////////////////////////////////////////////////////////// public static TextArea addTextArea(GridPane gridPane, int rowIndex, String prompt) { return addTextArea(gridPane, rowIndex, prompt, 0); } public static TextArea addTextArea(GridPane gridPane, int rowIndex, String prompt, double top) { JFXTextArea textArea = new HavenoTextArea(); textArea.setPromptText(prompt); textArea.setLabelFloat(true); textArea.getStyleClass().add("label-float"); textArea.setWrapText(true); GridPane.setRowIndex(textArea, rowIndex); GridPane.setColumnIndex(textArea, 0); GridPane.setMargin(textArea, new Insets(top + Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); gridPane.getChildren().add(textArea); return textArea; } /////////////////////////////////////////////////////////////////////////////////////////// // Label + TextArea /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple2 addCompactTopLabelTextArea(GridPane gridPane, int rowIndex, String title, String prompt) { return addTopLabelTextArea(gridPane, rowIndex, title, prompt, -Layout.FLOATING_LABEL_DISTANCE); } public static Tuple2 addCompactTopLabelTextArea(GridPane gridPane, int rowIndex, int colIndex, String title, String prompt) { return addTopLabelTextArea(gridPane, rowIndex, colIndex, title, prompt, -Layout.FLOATING_LABEL_DISTANCE); } public static Tuple2 addTopLabelTextArea(GridPane gridPane, int rowIndex, String title, String prompt) { return addTopLabelTextArea(gridPane, rowIndex, title, prompt, 0); } public static Tuple2 addTopLabelTextArea(GridPane gridPane, int rowIndex, int colIndex, String title, String prompt) { return addTopLabelTextArea(gridPane, rowIndex, colIndex, title, prompt, 0); } public static Tuple2 addTopLabelTextArea(GridPane gridPane, int rowIndex, String title, String prompt, double top) { return addTopLabelTextArea(gridPane, rowIndex, 0, title, prompt, top); } public static Tuple2 addTopLabelTextArea(GridPane gridPane, int rowIndex, int colIndex, String title, String prompt, double top) { TextArea textArea = new HavenoTextArea(); textArea.setPromptText(prompt); textArea.setWrapText(true); textArea.setPrefHeight(100); final Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, rowIndex, title, textArea, top); GridPane.setColumnIndex(topLabelWithVBox.second, colIndex); return new Tuple2<>(topLabelWithVBox.first, textArea); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + DatePicker /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple2 addTopLabelDatePicker(GridPane gridPane, int rowIndex, String title, double top) { return addTopLabelDatePicker(gridPane, rowIndex, 0, title, top); } public static Tuple2 addTopLabelDatePicker(GridPane gridPane, int rowIndex, int columnIndex, String title, double top) { //DatePicker datePicker = new JFXDatePicker(); // //Temporary solution to fix issue 527; a more //permanant solution would require this issue to be solved: //(https://github.com/sshahine/JFoenix/issues/1245) DatePicker datePicker = new DatePicker(); Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, rowIndex, columnIndex, title, datePicker, top); return new Tuple2<>(topLabelWithVBox.first, datePicker); } /////////////////////////////////////////////////////////////////////////////////////////// // 2 DatePickers /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple2 add2TopLabelDatePicker(GridPane gridPane, int rowIndex, int columnIndex, String title1, String title2, double top) { //DatePicker datePicker1 = new JFXDatePicker(); DatePicker datePicker1 = new DatePicker(); Tuple2 topLabelWithVBox1 = getTopLabelWithVBox(title1, datePicker1); VBox vBox1 = topLabelWithVBox1.second; //DatePicker datePicker2 = new JFXDatePicker(); DatePicker datePicker2 = new DatePicker(); Tuple2 topLabelWithVBox2 = getTopLabelWithVBox(title2, datePicker2); VBox vBox2 = topLabelWithVBox2.second; Region spacer = new Region(); HBox.setHgrow(spacer, Priority.ALWAYS); HBox hBox = new HBox(); hBox.setSpacing(10); hBox.getChildren().addAll(spacer, vBox1, vBox2); GridPane.setRowIndex(hBox, rowIndex); GridPane.setColumnIndex(hBox, columnIndex); GridPane.setMargin(hBox, new Insets(top, 0, 0, 0)); gridPane.getChildren().add(hBox); return new Tuple2<>(datePicker1, datePicker2); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + TxIdTextField /////////////////////////////////////////////////////////////////////////////////////////// @SuppressWarnings("UnusedReturnValue") public static Tuple2 addLabelTxIdTextField(GridPane gridPane, int rowIndex, String title, String value) { return addLabelTxIdTextField(gridPane, rowIndex, title, value, 0); } public static Tuple2 addLabelTxIdTextField(GridPane gridPane, int rowIndex, String title, String value, double top) { Label label = addLabel(gridPane, rowIndex, title, top); label.getStyleClass().add("confirmation-label"); GridPane.setHalignment(label, HPos.LEFT); TxIdTextField txTextField = new TxIdTextField(); txTextField.setup(value); GridPane.setRowIndex(txTextField, rowIndex); GridPane.setColumnIndex(txTextField, 1); gridPane.getChildren().add(txTextField); return new Tuple2<>(label, txTextField); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + ExplorerAddressTextField /////////////////////////////////////////////////////////////////////////////////////////// public static void addLabelExplorerAddressTextField(GridPane gridPane, int rowIndex, String title, String address) { Label label = addLabel(gridPane, rowIndex, title, 0); label.getStyleClass().add("confirmation-label"); GridPane.setHalignment(label, HPos.LEFT); ExplorerAddressTextField addressTextField = new ExplorerAddressTextField(); addressTextField.setup(address); GridPane.setRowIndex(addressTextField, rowIndex); GridPane.setColumnIndex(addressTextField, 1); gridPane.getChildren().add(addressTextField); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + InputTextField /////////////////////////////////////////////////////////////////////////////////////////// public static InputTextField addInputTextField(GridPane gridPane, int rowIndex, String title) { return addInputTextField(gridPane, rowIndex, title, 0); } public static InputTextField addInputTextField(GridPane gridPane, int rowIndex, String title, double top) { InputTextField inputTextField = new InputTextField(); inputTextField.setLabelFloat(true); inputTextField.getStyleClass().add("label-float"); inputTextField.setPromptText(title); GridPane.setRowIndex(inputTextField, rowIndex); GridPane.setColumnIndex(inputTextField, 0); GridPane.setMargin(inputTextField, new Insets(top + Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); gridPane.getChildren().add(inputTextField); return inputTextField; } /////////////////////////////////////////////////////////////////////////////////////////// // Label + InputTextField /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple2 addTopLabelInputTextField(GridPane gridPane, int rowIndex, String title) { return addTopLabelInputTextField(gridPane, rowIndex, title, 0); } public static Tuple2 addTopLabelInputTextField(GridPane gridPane, int rowIndex, String title, double top) { final Tuple3 topLabelWithVBox = addTopLabelInputTextFieldWithVBox(gridPane, rowIndex, title, top); return new Tuple2<>(topLabelWithVBox.first, topLabelWithVBox.second); } public static Tuple3 addTopLabelInputTextFieldWithVBox(GridPane gridPane, int rowIndex, String title, double top) { InputTextField inputTextField = new InputTextField(); final Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, rowIndex, title, inputTextField, top); return new Tuple3<>(topLabelWithVBox.first, inputTextField, topLabelWithVBox.second); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + InfoInputTextField /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple2 addTopLabelInfoInputTextField(GridPane gridPane, int rowIndex, String title) { return addTopLabelInfoInputTextField(gridPane, rowIndex, title, 0); } public static Tuple2 addTopLabelInfoInputTextField(GridPane gridPane, int rowIndex, String title, double top) { InfoInputTextField inputTextField = new InfoInputTextField(); final Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, rowIndex, title, inputTextField, top); return new Tuple2<>(topLabelWithVBox.first, inputTextField); } /////////////////////////////////////////////////////////////////////////////////////////// // PasswordField /////////////////////////////////////////////////////////////////////////////////////////// public static PasswordTextField addPasswordTextField(GridPane gridPane, int rowIndex, String title) { return addPasswordTextField(gridPane, rowIndex, title, 0); } public static PasswordTextField addPasswordTextField(GridPane gridPane, int rowIndex, String title, double top) { PasswordTextField passwordField = new PasswordTextField(); passwordField.getStyleClass().addAll("label-float"); GUIUtil.applyFilledStyle(passwordField); passwordField.setPromptText(title); GridPane.setRowIndex(passwordField, rowIndex); GridPane.setColumnIndex(passwordField, 0); GridPane.setColumnSpan(passwordField, 2); GridPane.setMargin(passwordField, new Insets(top + 10, 0, 20, 0)); gridPane.getChildren().add(passwordField); return passwordField; } /////////////////////////////////////////////////////////////////////////////////////////// // Label + InputTextField + CheckBox /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple3 addTopLabelInputTextFieldSlideToggleButton(GridPane gridPane, int rowIndex, String title, String toggleButtonTitle) { InputTextField inputTextField = new InputTextField(); ToggleButton toggleButton = new JFXToggleButton(); toggleButton.setText(toggleButtonTitle); VBox.setMargin(toggleButton, new Insets(4, 0, 0, 0)); final Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, rowIndex, title, inputTextField, 0); topLabelWithVBox.second.getChildren().add(toggleButton); return new Tuple3<>(topLabelWithVBox.first, inputTextField, toggleButton); } public static Tuple3 addTopLabelInputTextFieldSlideToggleButtonRight(GridPane gridPane, int rowIndex, String title, String toggleButtonTitle) { InputTextField inputTextField = new InputTextField(); Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, rowIndex, title, inputTextField, 0); ToggleButton toggleButton = new JFXToggleButton(); toggleButton.setText(toggleButtonTitle); HBox hBox = new HBox(); hBox.getChildren().addAll(topLabelWithVBox.second, toggleButton); HBox.setMargin(toggleButton, new Insets(9, 0, 0, 0)); gridPane.add(hBox, 0, rowIndex); GridPane.setMargin(hBox, new Insets(Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); return new Tuple3<>(topLabelWithVBox.first, inputTextField, toggleButton); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + InputTextField + Button /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple3 addTopLabelInputTextFieldButton(GridPane gridPane, int rowIndex, String title, String buttonTitle) { InputTextField inputTextField = new InputTextField(); Button button = new AutoTooltipButton(buttonTitle); button.setDefaultButton(true); HBox hBox = new HBox(); hBox.setSpacing(10); hBox.getChildren().addAll(inputTextField, button); HBox.setHgrow(inputTextField, Priority.ALWAYS); final Tuple2 labelVBoxTuple2 = addTopLabelWithVBox(gridPane, rowIndex, title, hBox, 0); return new Tuple3<>(labelVBoxTuple2.first, inputTextField, button); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + TextField + Button /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple3 addTopLabelTextFieldButton(GridPane gridPane, int rowIndex, String title, String buttonTitle) { return addTopLabelTextFieldButton(gridPane, rowIndex, title, buttonTitle, 0); } public static Tuple3 addTopLabelTextFieldButton(GridPane gridPane, int rowIndex, String title, String buttonTitle, double top) { TextField textField = new HavenoTextField(); textField.setEditable(false); textField.setMouseTransparent(true); textField.setFocusTraversable(false); Button button = new AutoTooltipButton(buttonTitle); button.setDefaultButton(true); HBox hBox = new HBox(); hBox.setSpacing(10); hBox.getChildren().addAll(textField, button); HBox.setHgrow(textField, Priority.ALWAYS); final Tuple2 labelVBoxTuple2 = addTopLabelWithVBox(gridPane, rowIndex, title, hBox, top); return new Tuple3<>(labelVBoxTuple2.first, textField, button); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + InputTextField + Label + InputTextField /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple2 addInputTextFieldInputTextField(GridPane gridPane, int rowIndex, String title1, String title2) { InputTextField inputTextField1 = new InputTextField(); inputTextField1.setPromptText(title1); inputTextField1.setLabelFloat(true); inputTextField1.getStyleClass().add("label-float"); InputTextField inputTextField2 = new InputTextField(); inputTextField2.setLabelFloat(true); inputTextField2.getStyleClass().add("label-float"); inputTextField2.setPromptText(title2); HBox hBox = new HBox(); hBox.setSpacing(10); hBox.getChildren().addAll(inputTextField1, inputTextField2); GridPane.setRowIndex(hBox, rowIndex); GridPane.setColumnIndex(hBox, 0); GridPane.setMargin(hBox, new Insets(Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); gridPane.getChildren().add(hBox); return new Tuple2<>(inputTextField1, inputTextField2); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + TextField + Label + TextField /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple4 addCompactTopLabelTextFieldTopLabelTextField(GridPane gridPane, int rowIndex, String title1, String title2) { TextField textField1 = new HavenoTextField(); textField1.setEditable(false); textField1.setMouseTransparent(true); textField1.setFocusTraversable(false); final Tuple2 topLabelWithVBox1 = getTopLabelWithVBox(title1, textField1); TextField textField2 = new HavenoTextField(); textField2.setEditable(false); textField2.setMouseTransparent(true); textField2.setFocusTraversable(false); final Tuple2 topLabelWithVBox2 = getTopLabelWithVBox(title2, textField2); HBox hBox = new HBox(); hBox.setSpacing(10); hBox.getChildren().addAll(topLabelWithVBox1.second, topLabelWithVBox2.second); GridPane.setRowIndex(hBox, rowIndex); gridPane.getChildren().add(hBox); return new Tuple4<>(topLabelWithVBox1.first, textField1, topLabelWithVBox2.first, textField2); } /////////////////////////////////////////////////////////////////////////////////////////// // Button + CheckBox /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple2 addButtonCheckBox(GridPane gridPane, int rowIndex, String buttonTitle, String checkBoxTitle) { return addButtonCheckBox(gridPane, rowIndex, buttonTitle, checkBoxTitle, 0); } public static Tuple2 addButtonCheckBox(GridPane gridPane, int rowIndex, String buttonTitle, String checkBoxTitle, double top) { final Tuple3 tuple = addButtonCheckBoxWithBox(gridPane, rowIndex, buttonTitle, checkBoxTitle, top); return new Tuple2<>(tuple.first, tuple.second); } public static Tuple3 addButtonCheckBoxWithBox(GridPane gridPane, int rowIndex, String buttonTitle, String checkBoxTitle, double top) { Button button = new AutoTooltipButton(buttonTitle); CheckBox checkBox = checkBoxTitle == null ? null : new AutoTooltipCheckBox(checkBoxTitle); HBox hBox = new HBox(20); hBox.setAlignment(Pos.CENTER_LEFT); hBox.getChildren().add(button); if (checkBox != null) hBox.getChildren().add(button); GridPane.setRowIndex(hBox, rowIndex); hBox.setPadding(new Insets(top, 0, 0, 0)); gridPane.getChildren().add(hBox); return new Tuple3<>(button, checkBox, hBox); } /////////////////////////////////////////////////////////////////////////////////////////// // CheckBox /////////////////////////////////////////////////////////////////////////////////////////// public static CheckBox addCheckBox(GridPane gridPane, int rowIndex, String checkBoxTitle) { return addCheckBox(gridPane, rowIndex, checkBoxTitle, 0); } public static CheckBox addCheckBox(GridPane gridPane, int rowIndex, String checkBoxTitle, double top) { return addCheckBox(gridPane, rowIndex, 0, checkBoxTitle, top); } public static CheckBox addCheckBox(GridPane gridPane, int rowIndex, int colIndex, String checkBoxTitle, double top) { CheckBox checkBox = new AutoTooltipCheckBox(checkBoxTitle); GridPane.setMargin(checkBox, new Insets(top, 0, 0, 0)); GridPane.setRowIndex(checkBox, rowIndex); GridPane.setColumnIndex(checkBox, colIndex); gridPane.getChildren().add(checkBox); return checkBox; } /////////////////////////////////////////////////////////////////////////////////////////// // RadioButton /////////////////////////////////////////////////////////////////////////////////////////// public static RadioButton addRadioButton(GridPane gridPane, int rowIndex, ToggleGroup toggleGroup, String title) { RadioButton radioButton = new AutoTooltipRadioButton(title); radioButton.setToggleGroup(toggleGroup); GridPane.setRowIndex(radioButton, rowIndex); gridPane.getChildren().add(radioButton); return radioButton; } /////////////////////////////////////////////////////////////////////////////////////////// // Label + RadioButton + RadioButton /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple3 addTopLabelRadioButtonRadioButton(GridPane gridPane, int rowIndex, ToggleGroup toggleGroup, String title, String radioButtonTitle1, String radioButtonTitle2, double top) { RadioButton radioButton1 = new AutoTooltipRadioButton(radioButtonTitle1); radioButton1.setToggleGroup(toggleGroup); radioButton1.setPadding(new Insets(6, 0, 0, 0)); RadioButton radioButton2 = new AutoTooltipRadioButton(radioButtonTitle2); radioButton2.setToggleGroup(toggleGroup); radioButton2.setPadding(new Insets(6, 0, 0, 0)); HBox hBox = new HBox(); hBox.setSpacing(10); hBox.getChildren().addAll(radioButton1, radioButton2); final Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, rowIndex, title, hBox, top); return new Tuple3<>(topLabelWithVBox.first, radioButton1, radioButton2); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + TextField + HyperlinkWithIcon /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple3 addTopLabelTextFieldHyperLink(GridPane gridPane, int rowIndex, String title, String textFieldTitle, String maxButtonTitle, double top) { TextField textField = new HavenoTextField(); textField.setPromptText(textFieldTitle); HyperlinkWithIcon maxLink = new ExternalHyperlink(maxButtonTitle); HBox hBox = new HBox(); hBox.setSpacing(10); hBox.getChildren().addAll(textField, maxLink); hBox.setAlignment(Pos.CENTER_LEFT); final Tuple2 labelVBoxTuple2 = addTopLabelWithVBox(gridPane, rowIndex, title, hBox, top); return new Tuple3<>(labelVBoxTuple2.first, textField, maxLink); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + CheckBox /////////////////////////////////////////////////////////////////////////////////////////// public static CheckBox addLabelCheckBox(GridPane gridPane, int rowIndex, String title) { return addLabelCheckBox(gridPane, rowIndex, title, 0); } public static CheckBox addLabelCheckBox(GridPane gridPane, int rowIndex, String title, double top) { CheckBox checkBox = new AutoTooltipCheckBox(title); GridPane.setRowIndex(checkBox, rowIndex); GridPane.setColumnIndex(checkBox, 0); GridPane.setMargin(checkBox, new Insets(top, 0, 0, 0)); gridPane.getChildren().add(checkBox); return checkBox; } /////////////////////////////////////////////////////////////////////////////////////////// // SlideToggleButton /////////////////////////////////////////////////////////////////////////////////////////// public static ToggleButton addSlideToggleButton(GridPane gridPane, int rowIndex, String title) { return addSlideToggleButton(gridPane, rowIndex, title, 0); } public static ToggleButton addSlideToggleButton(GridPane gridPane, int rowIndex, String title, double top) { ToggleButton toggleButton = new AutoTooltipSlideToggleButton(); toggleButton.setText(title); GridPane.setRowIndex(toggleButton, rowIndex); GridPane.setColumnIndex(toggleButton, 0); GridPane.setMargin(toggleButton, new Insets(top, 0, 0, 0)); gridPane.getChildren().add(toggleButton); return toggleButton; } /////////////////////////////////////////////////////////////////////////////////////////// // ComboBox /////////////////////////////////////////////////////////////////////////////////////////// public static ComboBox addComboBox(GridPane gridPane, int rowIndex, int top) { final JFXComboBox comboBox = new JFXComboBox<>(); GUIUtil.applyFilledStyle(comboBox); GridPane.setRowIndex(comboBox, rowIndex); GridPane.setMargin(comboBox, new Insets(top, 0, 0, 0)); gridPane.getChildren().add(comboBox); return comboBox; } /////////////////////////////////////////////////////////////////////////////////////////// // Label + ComboBox /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple2> addTopLabelComboBox(GridPane gridPane, int rowIndex, String title, String prompt, int top) { final Tuple3> tuple3 = addTopLabelComboBox(title, prompt, 0); final VBox vBox = tuple3.first; GridPane.setRowIndex(vBox, rowIndex); GridPane.setMargin(vBox, new Insets(top, 0, 0, 0)); gridPane.getChildren().add(vBox); return new Tuple2<>(tuple3.second, tuple3.third); } public static Tuple3> addTopLabelComboBox(String title, String prompt) { return addTopLabelComboBox(title, prompt, 0); } public static Tuple3> addTopLabelComboBox(String title, String prompt, int top) { Label label = getTopLabel(title); VBox vBox = getTopLabelVBox(top); final JFXComboBox comboBox = new JFXComboBox<>(); GUIUtil.applyFilledStyle(comboBox); comboBox.setPromptText(prompt); comboBox.setPadding(new Insets(top, 0, 0, 12)); vBox.getChildren().addAll(label, comboBox); return new Tuple3<>(vBox, label, comboBox); } public static Tuple3> addTopLabelAutocompleteComboBox(String title) { return addTopLabelAutocompleteComboBox(title, 0); } public static Tuple3> addTopLabelAutocompleteComboBox(String title, int top) { Label label = getTopLabel(title); VBox vBox = getTopLabelVBox(top); final AutocompleteComboBox comboBox = new AutocompleteComboBox<>(); vBox.getChildren().addAll(label, comboBox); return new Tuple3<>(vBox, label, comboBox); } public static Tuple3 addTopLabelAutoToolTipTextField(String title) { return addTopLabelAutoToolTipTextField(title, 0); } public static Tuple3 addTopLabelAutoToolTipTextField(String title, int top) { Label label = getTopLabel(title); VBox vBox = getTopLabelVBox(top); final AutoTooltipTextField textField = new AutoTooltipTextField(); vBox.getChildren().addAll(label, textField); return new Tuple3<>(vBox, label, textField); } @NotNull private static VBox getTopLabelVBox(int top) { VBox vBox = new VBox(); vBox.setSpacing(0); vBox.setPadding(new Insets(top, 0, 0, 0)); vBox.setAlignment(Pos.CENTER_LEFT); return vBox; } @NotNull private static Label getTopLabel(String title) { Label label = new AutoTooltipLabel(title); label.getStyleClass().add("small-text"); return label; } public static Tuple2 addTopLabelWithVBox(GridPane gridPane, int rowIndex, String title, Node node, double top) { return addTopLabelWithVBox(gridPane, rowIndex, 0, title, node, top); } @NotNull public static Tuple2 addTopLabelWithVBox(GridPane gridPane, int rowIndex, int columnIndex, String title, Node node, double top) { final Tuple2 topLabelWithVBox = getTopLabelWithVBox(title, node); VBox vBox = topLabelWithVBox.second; GridPane.setRowIndex(vBox, rowIndex); GridPane.setColumnIndex(vBox, columnIndex); GridPane.setMargin(vBox, new Insets(top + Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); gridPane.getChildren().add(vBox); return new Tuple2<>(topLabelWithVBox.first, vBox); } @NotNull public static Tuple2 getTopLabelWithVBox(String title, Node node) { Label label = getTopLabel(title); VBox vBox = getTopLabelVBox(0); vBox.getChildren().addAll(label, node); return new Tuple2<>(label, vBox); } public static Tuple3 addTopLabelTextFieldWithHbox(GridPane gridPane, int rowIndex, String titleTextfield, double top) { HBox hBox = new HBox(); hBox.setSpacing(10); TextField textField = new HavenoTextField(); final VBox topLabelVBox = getTopLabelVBox(5); final Label topLabel = getTopLabel(titleTextfield); topLabelVBox.getChildren().addAll(topLabel, textField); hBox.getChildren().addAll(topLabelVBox); GridPane.setRowIndex(hBox, rowIndex); GridPane.setMargin(hBox, new Insets(top, 0, 0, 0)); gridPane.getChildren().add(hBox); return new Tuple3<>(topLabel, textField, hBox); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + ComboBox /////////////////////////////////////////////////////////////////////////////////////////// public static ComboBox addComboBox(GridPane gridPane, int rowIndex) { return addComboBox(gridPane, rowIndex, null, 0); } public static ComboBox addComboBox(GridPane gridPane, int rowIndex, String title) { return addComboBox(gridPane, rowIndex, title, 0); } public static ComboBox addComboBox(GridPane gridPane, int rowIndex, String title, double top) { JFXComboBox comboBox = new JFXComboBox<>(); GUIUtil.applyFilledStyle(comboBox); comboBox.setLabelFloat(true); comboBox.getStyleClass().add("label-float"); comboBox.setPromptText(title); comboBox.setMaxWidth(Double.MAX_VALUE); // Default ComboBox does not show promptText after clear selection. // https://stackoverflow.com/questions/50569330/how-to-reset-combobox-and-display-prompttext?noredirect=1&lq=1 comboBox.setButtonCell(getComboBoxButtonCell(title, comboBox)); GridPane.setRowIndex(comboBox, rowIndex); GridPane.setColumnIndex(comboBox, 0); comboBox.setPadding(new Insets(0, 0, 0, 12)); GridPane.setMargin(comboBox, new Insets(top + Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); gridPane.getChildren().add(comboBox); return comboBox; } public static AutocompleteComboBox addAutocompleteComboBox(GridPane gridPane, int rowIndex, String title, double top) { var comboBox = new AutocompleteComboBox(); GUIUtil.applyFilledStyle(comboBox); comboBox.setLabelFloat(true); comboBox.getStyleClass().add("label-float"); comboBox.setPromptText(title); comboBox.setMaxWidth(Double.MAX_VALUE); // Default ComboBox does not show promptText after clear selection. // https://stackoverflow.com/questions/50569330/how-to-reset-combobox-and-display-prompttext?noredirect=1&lq=1 comboBox.setButtonCell(getComboBoxButtonCell(title, comboBox)); GridPane.setRowIndex(comboBox, rowIndex); GridPane.setColumnIndex(comboBox, 0); GridPane.setMargin(comboBox, new Insets(top + Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); gridPane.getChildren().add(comboBox); return comboBox; } /////////////////////////////////////////////////////////////////////////////////////////// // Label + AutocompleteComboBox /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple2> addLabelAutocompleteComboBox(GridPane gridPane, int rowIndex, String title, double top) { AutocompleteComboBox comboBox = new AutocompleteComboBox<>(); final Tuple2 labelVBoxTuple2 = addTopLabelWithVBox(gridPane, rowIndex, title, comboBox, top); return new Tuple2<>(labelVBoxTuple2.first, comboBox); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + TextField + AutocompleteComboBox /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple4> addTopLabelTextFieldAutocompleteComboBox( GridPane gridPane, int rowIndex, String titleTextfield, String titleCombobox ) { return addTopLabelTextFieldAutocompleteComboBox(gridPane, rowIndex, titleTextfield, titleCombobox, 0); } public static Tuple4> addTopLabelTextFieldAutocompleteComboBox( GridPane gridPane, int rowIndex, String titleTextfield, String titleCombobox, double top ) { HBox hBox = new HBox(); hBox.setSpacing(10); final VBox topLabelVBox1 = getTopLabelVBox(5); final Label topLabel1 = getTopLabel(titleTextfield); final TextField textField = new HavenoTextField(); topLabelVBox1.getChildren().addAll(topLabel1, textField); final VBox topLabelVBox2 = getTopLabelVBox(5); final Label topLabel2 = getTopLabel(titleCombobox); AutocompleteComboBox comboBox = new AutocompleteComboBox<>(); comboBox.setPromptText(titleCombobox); comboBox.setLabelFloat(true); comboBox.getStyleClass().add("label-float"); topLabelVBox2.getChildren().addAll(topLabel2, comboBox); hBox.getChildren().addAll(topLabelVBox1, topLabelVBox2); GridPane.setRowIndex(hBox, rowIndex); GridPane.setMargin(hBox, new Insets(top, 0, 0, 0)); gridPane.getChildren().add(hBox); return new Tuple4<>(topLabel1, textField, topLabel2, comboBox); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + ComboBox + ComboBox /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple3, ComboBox> addTopLabelComboBoxComboBox(GridPane gridPane, int rowIndex, String title) { return addTopLabelComboBoxComboBox(gridPane, rowIndex, title, 0); } public static Tuple3, ComboBox> addTopLabelComboBoxComboBox(GridPane gridPane, int rowIndex, String title, double top) { HBox hBox = new HBox(); hBox.setSpacing(10); ComboBox comboBox1 = new JFXComboBox<>(); GUIUtil.applyFilledStyle(comboBox1); ComboBox comboBox2 = new JFXComboBox<>(); GUIUtil.applyFilledStyle(comboBox2); hBox.getChildren().addAll(comboBox1, comboBox2); final Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, rowIndex, title, hBox, top); return new Tuple3<>(topLabelWithVBox.first, comboBox1, comboBox2); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + ComboBox + TextField /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple4, Label, TextField, HBox> addComboBoxTopLabelTextField(GridPane gridPane, int rowIndex, String titleCombobox, String titleTextfield) { return addComboBoxTopLabelTextField(gridPane, rowIndex, titleCombobox, titleTextfield, 0); } public static Tuple4, Label, TextField, HBox> addComboBoxTopLabelTextField(GridPane gridPane, int rowIndex, String titleCombobox, String titleTextfield, double top) { HBox hBox = new HBox(); hBox.setSpacing(10); JFXComboBox comboBox = new JFXComboBox<>(); GUIUtil.applyFilledStyle(comboBox); comboBox.setPromptText(titleCombobox); comboBox.setLabelFloat(true); comboBox.getStyleClass().add("label-float"); TextField textField = new HavenoTextField(); final VBox topLabelVBox = getTopLabelVBox(5); final Label topLabel = getTopLabel(titleTextfield); topLabelVBox.getChildren().addAll(topLabel, textField); hBox.getChildren().addAll(comboBox, topLabelVBox); GridPane.setRowIndex(hBox, rowIndex); GridPane.setMargin(hBox, new Insets(top, 0, 0, 0)); gridPane.getChildren().add(hBox); return new Tuple4<>(comboBox, topLabel, textField, hBox); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + ComboBox + Button /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple3, Button> addLabelComboBoxButton(GridPane gridPane, int rowIndex, String title, String buttonTitle) { return addLabelComboBoxButton(gridPane, rowIndex, title, buttonTitle, 0); } public static Tuple3, Button> addLabelComboBoxButton(GridPane gridPane, int rowIndex, String title, String buttonTitle, double top) { Label label = addLabel(gridPane, rowIndex, title, top); HBox hBox = new HBox(); hBox.setSpacing(10); Button button = new AutoTooltipButton(buttonTitle); button.setDefaultButton(true); ComboBox comboBox = new JFXComboBox<>(); GUIUtil.applyFilledStyle(comboBox); hBox.getChildren().addAll(comboBox, button); GridPane.setRowIndex(hBox, rowIndex); GridPane.setColumnIndex(hBox, 1); GridPane.setMargin(hBox, new Insets(top, 0, 0, 0)); gridPane.getChildren().add(hBox); return new Tuple3<>(label, comboBox, button); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + ComboBox + Label /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple3, TextField> addLabelComboBoxLabel(GridPane gridPane, int rowIndex, String title, String textFieldText) { return addLabelComboBoxLabel(gridPane, rowIndex, title, textFieldText, 0); } public static Tuple3, TextField> addLabelComboBoxLabel(GridPane gridPane, int rowIndex, String title, String textFieldText, double top) { Label label = addLabel(gridPane, rowIndex, title, top); HBox hBox = new HBox(); hBox.setSpacing(10); ComboBox comboBox = new JFXComboBox<>(); GUIUtil.applyFilledStyle(comboBox); TextField textField = new TextField(textFieldText); textField.setEditable(false); textField.setMouseTransparent(true); textField.setFocusTraversable(false); hBox.getChildren().addAll(comboBox, textField); GridPane.setRowIndex(hBox, rowIndex); GridPane.setColumnIndex(hBox, 1); GridPane.setMargin(hBox, new Insets(top, 0, 0, 0)); gridPane.getChildren().add(hBox); return new Tuple3<>(label, comboBox, textField); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + TxIdTextField /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple2 addLabelTxIdTextField(GridPane gridPane, int rowIndex, int columnIndex, String title) { return addLabelTxIdTextField(gridPane, rowIndex, columnIndex, title, 0); } public static Tuple2 addLabelTxIdTextField(GridPane gridPane, int rowIndex, int columnIndex, String title, double top) { Label label = addLabel(gridPane, rowIndex, title, top); TxIdTextField txIdTextField = new TxIdTextField(); GridPane.setRowIndex(txIdTextField, rowIndex); GridPane.setColumnIndex(txIdTextField, columnIndex); GridPane.setMargin(txIdTextField, new Insets(top, 0, 0, 0)); gridPane.getChildren().add(txIdTextField); return new Tuple2<>(label, txIdTextField); } public static Tuple3 addTopLabelTxIdTextField(GridPane gridPane, int rowIndex, String title, double top) { TxIdTextField textField = new TxIdTextField(); textField.setFocusTraversable(false); final Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, rowIndex, title, textField, top); // TODO not 100% sure if that is a good idea.... //topLabelWithVBox.first.getStyleClass().add("jfx-text-field-top-label"); return new Tuple3<>(topLabelWithVBox.first, textField, topLabelWithVBox.second); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + TextFieldWithCopyIcon /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple2 addCompactTopLabelTextFieldWithCopyIcon(GridPane gridPane, int rowIndex, String title, String value) { return addTopLabelTextFieldWithCopyIcon(gridPane, rowIndex, title, value, -Layout.FLOATING_LABEL_DISTANCE); } public static Tuple2 addCompactTopLabelTextFieldWithCopyIcon(GridPane gridPane, int rowIndex, int colIndex, String title, String value, double top) { return addTopLabelTextFieldWithCopyIcon(gridPane, rowIndex, colIndex, title, value, top - Layout.FLOATING_LABEL_DISTANCE); } public static Tuple2 addCompactTopLabelTextFieldWithCopyIcon(GridPane gridPane, int rowIndex, int colIndex, String title) { return addTopLabelTextFieldWithCopyIcon(gridPane, rowIndex, colIndex, title, "", -Layout.FLOATING_LABEL_DISTANCE); } public static Tuple2 addCompactTopLabelTextFieldWithCopyIcon(GridPane gridPane, int rowIndex, int colIndex, String title, String value) { return addTopLabelTextFieldWithCopyIcon(gridPane, rowIndex, colIndex, title, value, -Layout.FLOATING_LABEL_DISTANCE); } public static Tuple2 addCompactTopLabelTextFieldWithCopyIcon(GridPane gridPane, int rowIndex, int colIndex, String title, String value, boolean onlyCopyTextAfterDelimiter) { return addTopLabelTextFieldWithCopyIcon(gridPane, rowIndex, colIndex, title, value, -Layout.FLOATING_LABEL_DISTANCE, onlyCopyTextAfterDelimiter); } public static Tuple2 addTopLabelTextFieldWithCopyIcon(GridPane gridPane, int rowIndex, String title, String value) { return addTopLabelTextFieldWithCopyIcon(gridPane, rowIndex, title, value, 0); } public static Tuple2 addTopLabelTextFieldWithCopyIcon(GridPane gridPane, int rowIndex, String title, String value, double top) { return addTopLabelTextFieldWithCopyIcon(gridPane, rowIndex, title, value, top, null); } public static Tuple2 addTopLabelTextFieldWithCopyIcon(GridPane gridPane, int rowIndex, String title, String value, double top, String styleClass) { TextFieldWithCopyIcon textFieldWithCopyIcon = new TextFieldWithCopyIcon(styleClass); textFieldWithCopyIcon.setText(value); final Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, rowIndex, title, textFieldWithCopyIcon, top); return new Tuple2<>(topLabelWithVBox.first, textFieldWithCopyIcon); } public static Tuple2 addTopLabelTextFieldWithCopyIcon(GridPane gridPane, int rowIndex, int colIndex, String title, String value, double top, boolean onlyCopyTextAfterDelimiter) { TextFieldWithCopyIcon textFieldWithCopyIcon = new TextFieldWithCopyIcon(); textFieldWithCopyIcon.setText(value); textFieldWithCopyIcon.setCopyTextAfterDelimiter(true); final Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, rowIndex, title, textFieldWithCopyIcon, top); topLabelWithVBox.second.setAlignment(Pos.TOP_LEFT); GridPane.setColumnIndex(topLabelWithVBox.second, colIndex); return new Tuple2<>(topLabelWithVBox.first, textFieldWithCopyIcon); } public static Tuple2 addTopLabelTextFieldWithCopyIcon(GridPane gridPane, int rowIndex, int colIndex, String title, String value, double top) { TextFieldWithCopyIcon textFieldWithCopyIcon = new TextFieldWithCopyIcon(); textFieldWithCopyIcon.setText(value); final Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, rowIndex, title, textFieldWithCopyIcon, top); topLabelWithVBox.second.setAlignment(Pos.TOP_LEFT); GridPane.setColumnIndex(topLabelWithVBox.second, colIndex); return new Tuple2<>(topLabelWithVBox.first, textFieldWithCopyIcon); } public static Tuple2 addConfirmationLabelTextFieldWithCopyIcon(GridPane gridPane, int rowIndex, String title, String value) { return addConfirmationLabelTextFieldWithCopyIcon(gridPane, rowIndex, title, value, 0); } public static Tuple2 addConfirmationLabelTextFieldWithCopyIcon(GridPane gridPane, int rowIndex, String title, String value, double top) { Label label = addLabel(gridPane, rowIndex, title, top); label.getStyleClass().add("confirmation-label"); GridPane.setHalignment(label, HPos.LEFT); TextFieldWithCopyIcon textFieldWithCopyIcon = new TextFieldWithCopyIcon("confirmation-text-field-as-label"); textFieldWithCopyIcon.setText(value); GridPane.setRowIndex(textFieldWithCopyIcon, rowIndex); GridPane.setColumnIndex(textFieldWithCopyIcon, 1); GridPane.setMargin(textFieldWithCopyIcon, new Insets(top, 0, 0, 0)); gridPane.getChildren().add(textFieldWithCopyIcon); return new Tuple2<>(label, textFieldWithCopyIcon); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + AddressTextField /////////////////////////////////////////////////////////////////////////////////////////// public static AddressTextField addAddressTextField(GridPane gridPane, int rowIndex, String title) { return addAddressTextField(gridPane, rowIndex, title, 0); } public static AddressTextField addAddressTextField(GridPane gridPane, int rowIndex, String title, double top) { AddressTextField addressTextField = new AddressTextField(title); GridPane.setRowIndex(addressTextField, rowIndex); GridPane.setColumnIndex(addressTextField, 0); GridPane.setMargin(addressTextField, new Insets(top + 20, 0, 0, 0)); gridPane.getChildren().add(addressTextField); return addressTextField; } /////////////////////////////////////////////////////////////////////////////////////////// // Label + FundsTextField /////////////////////////////////////////////////////////////////////////////////////////// public static FundsTextField addFundsTextfield(GridPane gridPane, int rowIndex, String text) { return addFundsTextfield(gridPane, rowIndex, text, 0); } public static FundsTextField addFundsTextfield(GridPane gridPane, int rowIndex, String text, double top) { FundsTextField fundsTextField = new FundsTextField(); fundsTextField.getTextField().setPromptText(text); GridPane.setRowIndex(fundsTextField, rowIndex); GridPane.setColumnIndex(fundsTextField, 0); GridPane.setMargin(fundsTextField, new Insets(top + 20, 0, 0, 0)); gridPane.getChildren().add(fundsTextField); return fundsTextField; } /////////////////////////////////////////////////////////////////////////////////////////// // Label + InfoTextField /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple3 addCompactTopLabelInfoTextField(GridPane gridPane, int rowIndex, String labelText, String fieldText) { return addTopLabelInfoTextField(gridPane, rowIndex, labelText, fieldText, -Layout.FLOATING_LABEL_DISTANCE); } public static Tuple3 addTopLabelInfoTextField(GridPane gridPane, int rowIndex, String labelText, String fieldText, double top) { InfoTextField infoTextField = new InfoTextField(); infoTextField.setText(fieldText); final Tuple2 labelVBoxTuple2 = addTopLabelWithVBox(gridPane, rowIndex, labelText, infoTextField, top); return new Tuple3<>(labelVBoxTuple2.first, infoTextField, labelVBoxTuple2.second); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + BalanceTextField /////////////////////////////////////////////////////////////////////////////////////////// public static BalanceTextField addBalanceTextField(GridPane gridPane, int rowIndex, String title) { return addBalanceTextField(gridPane, rowIndex, title, 20); } public static BalanceTextField addBalanceTextField(GridPane gridPane, int rowIndex, String title, double top) { BalanceTextField balanceTextField = new BalanceTextField(title); GridPane.setRowIndex(balanceTextField, rowIndex); GridPane.setColumnIndex(balanceTextField, 0); GridPane.setMargin(balanceTextField, new Insets(top, 0, 0, 0)); gridPane.getChildren().add(balanceTextField); return balanceTextField; } /////////////////////////////////////////////////////////////////////////////////////////// // Label + Button /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple2 addTopLabelButton(GridPane gridPane, int rowIndex, String labelText, String buttonTitle) { return addTopLabelButton(gridPane, rowIndex, labelText, buttonTitle, 0); } public static Tuple2 addTopLabelButton(GridPane gridPane, int rowIndex, String labelText, String buttonTitle, double top) { Button button = new AutoTooltipButton(buttonTitle); button.setDefaultButton(true); final Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, rowIndex, labelText, button, top); return new Tuple2<>(topLabelWithVBox.first, button); } public static Tuple2 addConfirmationLabelButton(GridPane gridPane, int rowIndex, String labelText, String buttonTitle, double top) { Label label = addLabel(gridPane, rowIndex, labelText); label.getStyleClass().add("confirmation-label"); Button button = new AutoTooltipButton(buttonTitle); button.getStyleClass().add("confirmation-value"); button.setDefaultButton(true); GridPane.setColumnIndex(button, 1); GridPane.setRowIndex(button, rowIndex); GridPane.setMargin(label, new Insets(top, 0, 0, 0)); GridPane.setHalignment(label, HPos.LEFT); GridPane.setMargin(button, new Insets(top, 0, 0, 0)); gridPane.getChildren().add(button); return new Tuple2<>(label, button); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + Button + Button /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple3 addTopLabel2Buttons(GridPane gridPane, int rowIndex, String labelText, String title1, String title2, double top) { HBox hBox = new HBox(); hBox.setSpacing(10); Button button1 = new AutoTooltipButton(title1); button1.setDefaultButton(true); button1.getStyleClass().add("action-button"); button1.setDefaultButton(true); button1.setMaxWidth(Double.MAX_VALUE); HBox.setHgrow(button1, Priority.ALWAYS); Button button2 = new AutoTooltipButton(title2); button2.setMaxWidth(Double.MAX_VALUE); HBox.setHgrow(button2, Priority.ALWAYS); hBox.getChildren().addAll(button1, button2); final Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, rowIndex, labelText, hBox, top); return new Tuple3<>(topLabelWithVBox.first, button1, button2); } /////////////////////////////////////////////////////////////////////////////////////////// // Button /////////////////////////////////////////////////////////////////////////////////////////// public static Button addButton(GridPane gridPane, int rowIndex, String title) { return addButton(gridPane, rowIndex, title, 0); } public static Button addButtonAfterGroup(GridPane gridPane, int rowIndex, String title) { return addButton(gridPane, rowIndex, title, 15); } public static Button addPrimaryActionButton(GridPane gridPane, int rowIndex, String title, double top) { return addButton(gridPane, rowIndex, title, top, true); } public static Button addPrimaryActionButtonAFterGroup(GridPane gridPane, int rowIndex, String title) { return addPrimaryActionButton(gridPane, rowIndex, title, 15); } public static Button addButton(GridPane gridPane, int rowIndex, String title, double top) { return addButton(gridPane, rowIndex, title, top, false); } public static Button addButton(GridPane gridPane, int rowIndex, String title, double top, boolean isPrimaryAction) { Button button = new AutoTooltipButton(title); if (isPrimaryAction) { button.setDefaultButton(true); button.getStyleClass().add("action-button"); } GridPane.setRowIndex(button, rowIndex); GridPane.setColumnIndex(button, 0); gridPane.getChildren().add(button); GridPane.setMargin(button, new Insets(top, 0, 0, 0)); return button; } public static Button addCloseButton(GridPane gridPane, int rowIndex, Runnable closeHandler) { Button closeButton = addButtonAfterGroup(gridPane, rowIndex, Res.get("shared.close")); GridPane.setColumnIndex(closeButton, 1); GridPane.setHalignment(closeButton, HPos.RIGHT); closeButton.setOnAction(e -> closeHandler.run()); return closeButton; } /////////////////////////////////////////////////////////////////////////////////////////// // Button + Button /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple2 add2Buttons(GridPane gridPane, int rowIndex, String title1, String title2) { return add2Buttons(gridPane, rowIndex, title1, title2, 0); } public static Tuple2 add2ButtonsAfterGroup(GridPane gridPane, int rowIndex, String title1, String title2) { return add2ButtonsAfterGroup(gridPane, rowIndex, title1, title2, true); } public static Tuple2 add2ButtonsAfterGroup(GridPane gridPane, int rowIndex, String title1, String title2, boolean hasPrimaryButton) { return add2Buttons(gridPane, rowIndex, title1, title2, 15, hasPrimaryButton); } public static Tuple2 add2Buttons(GridPane gridPane, int rowIndex, String title1, String title2, double top) { return add2Buttons(gridPane, rowIndex, title1, title2, top, true); } public static Tuple2 add2Buttons(GridPane gridPane, int rowIndex, String title1, String title2, double top, boolean hasPrimaryButton) { final Tuple3 buttonButtonHBoxTuple3 = add2ButtonsWithBox(gridPane, rowIndex, title1, title2, top, hasPrimaryButton); return new Tuple2<>(buttonButtonHBoxTuple3.first, buttonButtonHBoxTuple3.second); } public static Tuple3 add2ButtonsWithBox(GridPane gridPane, int rowIndex, String title1, String title2, double top, boolean hasPrimaryButton) { HBox hBox = new HBox(); hBox.setSpacing(10); Button button1 = new AutoTooltipButton(title1); if (hasPrimaryButton) { button1.getStyleClass().add("action-button"); button1.setDefaultButton(true); } button1.setMaxWidth(Double.MAX_VALUE); HBox.setHgrow(button1, Priority.ALWAYS); Button button2 = new AutoTooltipButton(title2); button2.setMaxWidth(Double.MAX_VALUE); HBox.setHgrow(button2, Priority.ALWAYS); hBox.getChildren().addAll(button1, button2); GridPane.setRowIndex(hBox, rowIndex); GridPane.setColumnIndex(hBox, 0); GridPane.setMargin(hBox, new Insets(top, 10, 0, 0)); gridPane.getChildren().add(hBox); return new Tuple3<>(button1, button2, hBox); } /////////////////////////////////////////////////////////////////////////////////////////// // Button + Button + Button /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple3 add3Buttons(GridPane gridPane, int rowIndex, String title1, String title2, String title3) { return add3Buttons(gridPane, rowIndex, title1, title2, title3, 0); } public static Tuple3 add3ButtonsAfterGroup(GridPane gridPane, int rowIndex, String title1, String title2, String title3) { return add3Buttons(gridPane, rowIndex, title1, title2, title3, 15); } public static Tuple3 add3Buttons(GridPane gridPane, int rowIndex, String title1, String title2, String title3, double top) { HBox hBox = new HBox(); hBox.setSpacing(10); Button button1 = new AutoTooltipButton(title1); button1.getStyleClass().add("action-button"); button1.setDefaultButton(true); button1.setMaxWidth(Double.MAX_VALUE); HBox.setHgrow(button1, Priority.ALWAYS); Button button2 = new AutoTooltipButton(title2); button2.setMaxWidth(Double.MAX_VALUE); HBox.setHgrow(button2, Priority.ALWAYS); Button button3 = new AutoTooltipButton(title3); button3.setMaxWidth(Double.MAX_VALUE); HBox.setHgrow(button3, Priority.ALWAYS); hBox.getChildren().addAll(button1, button2, button3); GridPane.setRowIndex(hBox, rowIndex); GridPane.setColumnIndex(hBox, 0); GridPane.setMargin(hBox, new Insets(top, 10, 0, 0)); gridPane.getChildren().add(hBox); return new Tuple3<>(button1, button2, button3); } /////////////////////////////////////////////////////////////////////////////////////////// // Button + ProgressIndicator + Label /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple4 addButtonBusyAnimationLabelAfterGroup(GridPane gridPane, int rowIndex, int colIndex, String buttonTitle) { return addButtonBusyAnimationLabel(gridPane, rowIndex, colIndex, buttonTitle, 15); } public static Tuple4 addButtonBusyAnimationLabelAfterGroup(GridPane gridPane, int rowIndex, String buttonTitle) { return addButtonBusyAnimationLabelAfterGroup(gridPane, rowIndex, 0, buttonTitle); } public static Tuple4 addButtonBusyAnimationLabel(GridPane gridPane, int rowIndex, int colIndex, String buttonTitle, double top) { HBox hBox = new HBox(); hBox.setSpacing(10); Button button = new AutoTooltipButton(buttonTitle); button.setDefaultButton(true); button.getStyleClass().add("action-button"); BusyAnimation busyAnimation = new BusyAnimation(false); Label label = new AutoTooltipLabel(); hBox.setAlignment(Pos.CENTER_LEFT); hBox.getChildren().addAll(button, busyAnimation, label); GridPane.setRowIndex(hBox, rowIndex); GridPane.setHalignment(hBox, HPos.LEFT); GridPane.setColumnIndex(hBox, colIndex); GridPane.setMargin(hBox, new Insets(top, 0, 0, 0)); gridPane.getChildren().add(hBox); return new Tuple4<>(button, busyAnimation, label, hBox); } /////////////////////////////////////////////////////////////////////////////////////////// // Trade: HBox, InputTextField, Label /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple3 getEditableValueBox(String promptText) { InputTextField input = new InputTextField(60); input.setPromptText(promptText); Label label = new AutoTooltipLabel(Res.getBaseCurrencyCode()); label.getStyleClass().add("input-label"); HBox.setMargin(label, new Insets(0, 8, 0, 0)); HBox box = new HBox(); HBox.setHgrow(input, Priority.ALWAYS); input.setMaxWidth(Double.MAX_VALUE); box.setAlignment(Pos.CENTER_LEFT); box.getStyleClass().add("offer-input"); box.getChildren().addAll(input, label); return new Tuple3<>(box, input, label); } public static Tuple3 getEditableValueBoxWithInfo(String promptText) { InfoInputTextField infoInputTextField = new InfoInputTextField(60); InputTextField input = infoInputTextField.getInputTextField(); input.setPromptText(Utilities.toTruncatedString(promptText, 28)); Label label = new AutoTooltipLabel(Res.getBaseCurrencyCode()); label.getStyleClass().add("input-label"); HBox.setMargin(label, new Insets(0, 8, 0, 0)); HBox box = new HBox(); HBox.setHgrow(infoInputTextField, Priority.ALWAYS); infoInputTextField.setMaxWidth(Double.MAX_VALUE); box.setAlignment(Pos.CENTER_LEFT); box.getStyleClass().add("offer-input"); box.getChildren().addAll(infoInputTextField, label); return new Tuple3<>(box, infoInputTextField, label); } public static Tuple3 getNonEditableValueBox() { final Tuple3 editableValueBox = getEditableValueBox(""); final TextField textField = editableValueBox.second; textField.setDisable(true); return new Tuple3<>(editableValueBox.first, editableValueBox.second, editableValueBox.third); } public static Tuple3 getNonEditableValueBoxWithInfo() { final Tuple3 editableValueBoxWithInfo = getEditableValueBoxWithInfo(""); TextField textField = editableValueBoxWithInfo.second.getInputTextField(); textField.setDisable(true); return editableValueBoxWithInfo; } /////////////////////////////////////////////////////////////////////////////////////////// // Trade: Label, VBox /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple2 getTradeInputBox(Pane amountValueBox, String descriptionText) { Label descriptionLabel = new AutoTooltipLabel(descriptionText); descriptionLabel.setId("input-description-label"); descriptionLabel.setPrefWidth(190); VBox box = new VBox(); box.setPadding(new Insets(10, 0, 0, 0)); box.setSpacing(2); box.getChildren().addAll(descriptionLabel, amountValueBox); return new Tuple2<>(descriptionLabel, box); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + List /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple3, VBox> addTopLabelListView(GridPane gridPane, int rowIndex, String title) { return addTopLabelListView(gridPane, rowIndex, title, 0); } public static Tuple3, VBox> addTopLabelListView(GridPane gridPane, int rowIndex, String title, double top) { ListView listView = new ListView<>(); final Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, rowIndex, title, listView, top); return new Tuple3<>(topLabelWithVBox.first, listView, topLabelWithVBox.second); } /////////////////////////////////////////////////////////////////////////////////////////// // Label + FlowPane /////////////////////////////////////////////////////////////////////////////////////////// public static Tuple2 addTopLabelFlowPane(GridPane gridPane, int rowIndex, String title, double top) { return addTopLabelFlowPane(gridPane, rowIndex, title, top, 0); } public static Tuple2 addTopLabelFlowPane(GridPane gridPane, int rowIndex, String title, double top, double bottom) { FlowPane flowPane = new FlowPane(); flowPane.setPadding(new Insets(10, 10, 10, 10)); flowPane.setVgap(10); flowPane.setHgap(10); final Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, rowIndex, title, flowPane, top); GridPane.setMargin(topLabelWithVBox.second, new Insets(top + Layout.FLOATING_LABEL_DISTANCE, 0, bottom, 0)); return new Tuple2<>(topLabelWithVBox.first, flowPane); } /////////////////////////////////////////////////////////////////////////////////////////// // Remove /////////////////////////////////////////////////////////////////////////////////////////// public static void removeRowFromGridPane(GridPane gridPane, int gridRow) { removeRowsFromGridPane(gridPane, gridRow, gridRow); } public static void removeRowsFromGridPane(GridPane gridPane, int fromGridRow, int toGridRow) { Set nodes = new CopyOnWriteArraySet<>(gridPane.getChildren()); nodes.stream() .filter(e -> GridPane.getRowIndex(e) != null && GridPane.getRowIndex(e) >= fromGridRow && GridPane.getRowIndex(e) <= toGridRow) .forEach(e -> gridPane.getChildren().remove(e)); } /////////////////////////////////////////////////////////////////////////////////////////// // Icons /////////////////////////////////////////////////////////////////////////////////////////// public static Text getIconForLabel(GlyphIcons icon, String iconSize, Label label, String style) { if (icon.fontFamily().equals(MATERIAL_DESIGN_ICONS)) { final Text textIcon = MaterialDesignIconFactory.get().createIcon(icon, iconSize); textIcon.setOpacity(0.7); if (style != null) { textIcon.getStyleClass().add(style); } label.setContentDisplay(ContentDisplay.LEFT); label.setGraphic(textIcon); return textIcon; } else { throw new IllegalArgumentException("Not supported icon type"); } } public static Text getIconForLabel(GlyphIcons icon, String iconSize, Label label) { return getIconForLabel(icon, iconSize, label, null); } public static Text getSmallIconForLabel(GlyphIcons icon, Label label, String style) { return getIconForLabel(icon, "0.769em", label, style); } public static Text getSmallIconForLabel(GlyphIcons icon, Label label) { return getIconForLabel(icon, "0.769em", label); } public static Text getRegularIconForLabel(GlyphIcons icon, Label label) { return getRegularIconForLabel(icon, label, null); } public static Text getRegularIconForLabel(GlyphIcons icon, Label label, String styleClass) { return getIconForLabel(icon, "1.231em", label, styleClass); } public static Text getIcon(GlyphIcons icon) { return getIcon(icon, "1.231em"); } public static Text getBigIcon(GlyphIcons icon) { return getIcon(icon, "2em"); } public static Text getMediumSizeIcon(GlyphIcons icon) { return getIcon(icon, "1.5em"); } public static Text getIcon(GlyphIcons icon, String iconSize) { Text textIcon; if (icon.fontFamily().equals(MATERIAL_DESIGN_ICONS)) { textIcon = MaterialDesignIconFactory.get().createIcon(icon, iconSize); } else { throw new IllegalArgumentException("Not supported icon type"); } return textIcon; } public static Label getIcon(AwesomeIcon icon) { final Label label = new Label(); AwesomeDude.setIcon(label, icon); return label; } public static Label getIcon(AwesomeIcon icon, String fontSize) { return getIconForLabel(icon, new Label(), fontSize); } public static Label getSmallIcon(AwesomeIcon icon) { return getIcon(icon, "1em"); } public static Label getIconForLabel(AwesomeIcon icon, Label label, String fontSize) { AwesomeDude.setIcon(label, icon, fontSize); return label; } public static Button getIconButton(GlyphIcons icon) { return getIconButton(icon, "highlight"); } public static Button getIconButton(GlyphIcons icon, String styleClass) { return getIconButton(icon, styleClass, "2em"); } public static Button getRegularIconButton(GlyphIcons icon) { return getIconButton(icon, "highlight", "1.6em"); } public static Button getRegularIconButton(GlyphIcons icon, String styleClass) { return getIconButton(icon, styleClass, "1.6em"); } public static Button getIconButton(GlyphIcons icon, String styleClass, String iconSize) { if (icon.fontFamily().equals(MATERIAL_DESIGN_ICONS)) { Button iconButton = MaterialDesignIconFactory.get().createIconButton(icon, "", iconSize, null, ContentDisplay.CENTER); iconButton.setId("icon-button"); iconButton.getGraphic().getStyleClass().add(styleClass); iconButton.setPrefWidth(20); iconButton.setPrefHeight(20); iconButton.setPadding(new Insets(0)); return iconButton; } else { throw new IllegalArgumentException("Not supported icon type"); } } public static TableView addTableViewWithHeader(GridPane gridPane, int rowIndex, String headerText) { return addTableViewWithHeader(gridPane, rowIndex, headerText, 0, null); } public static TableView addTableViewWithHeader(GridPane gridPane, int rowIndex, String headerText, String groupStyle) { return addTableViewWithHeader(gridPane, rowIndex, headerText, 0, groupStyle); } public static TableView addTableViewWithHeader(GridPane gridPane, int rowIndex, String headerText, int top) { return addTableViewWithHeader(gridPane, rowIndex, headerText, top, null); } public static TableView addTableViewWithHeader(GridPane gridPane, int rowIndex, String headerText, int top, String groupStyle) { TitledGroupBg titledGroupBg = addTitledGroupBg(gridPane, rowIndex, 1, headerText, top); if (groupStyle != null) titledGroupBg.getStyleClass().add(groupStyle); TableView tableView = new TableView<>(); GUIUtil.applyTableStyle(tableView); GridPane.setRowIndex(tableView, rowIndex); GridPane.setMargin(tableView, new Insets(top + 30, -10, 5, -10)); gridPane.getChildren().add(tableView); tableView.setPlaceholder(new AutoTooltipLabel(Res.get("table.placeholder.noData"))); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); return tableView; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/util/GUIProfiler.java ================================================ /* * 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 . */ package haveno.desktop.util; import com.google.common.base.Stopwatch; import javafx.animation.AnimationTimer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.TimeUnit; public class GUIProfiler { private static final Logger log = LoggerFactory.getLogger(GUIProfiler.class); private static final Stopwatch globalStopwatch = Stopwatch.createStarted(); private static final ThreadLocal threadStopwatch = ThreadLocal.withInitial(Stopwatch::createStarted); private static final ThreadLocal last = ThreadLocal.withInitial(() -> 0L); private static long lastFPSTime = System.currentTimeMillis(); public static void printMsgWithTime(String msg) { final long elapsed = threadStopwatch.get().elapsed(TimeUnit.MILLISECONDS); log.trace("\n\nCalled by: {} \nElapsed time: {}ms \nTotal time: {}ms\n\n", msg, elapsed - last.get(), globalStopwatch.elapsed(TimeUnit.MILLISECONDS)); last.set(elapsed); } public static void init() { AnimationTimer fpsTimer = new AnimationTimer() { @Override public void handle(long l) { long elapsed = (System.currentTimeMillis() - lastFPSTime); if (elapsed > 50) log.trace("Profiler: last frame used {}ms", elapsed); lastFPSTime = System.currentTimeMillis(); } }; fpsTimer.start(); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/util/GUIUtil.java ================================================ /* * 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 . */ package haveno.desktop.util; import com.google.common.base.Charsets; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.googlecode.jcsv.CSVStrategy; import com.googlecode.jcsv.writer.CSVEntryConverter; import com.googlecode.jcsv.writer.CSVWriter; import com.googlecode.jcsv.writer.internal.CSVWriterBuilder; import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIconView; import haveno.common.UserThread; import haveno.common.config.Config; import haveno.common.file.CorruptedStorageFileHandler; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.persistable.PersistableEnvelope; import haveno.common.proto.persistable.PersistenceProtoResolver; import haveno.common.util.Tuple2; import haveno.common.util.Tuple3; import haveno.common.util.Utilities; import haveno.core.account.witness.AccountAgeWitness; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.api.XmrConnectionService; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccountList; import haveno.core.payment.payload.PaymentMethod; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.user.DontShowAgainLookup; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.Navigation; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.HavenoTextArea; import haveno.desktop.components.InfoAutoTooltipLabel; import haveno.desktop.components.indicator.TxConfidenceIndicator; import haveno.desktop.main.MainView; import haveno.desktop.main.account.AccountView; import haveno.desktop.main.account.content.traditionalaccounts.TraditionalAccountsView; import haveno.desktop.main.overlays.popups.Popup; import haveno.network.p2p.P2PService; import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Orientation; import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.ComboBox; import javafx.scene.control.ContentDisplay; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.ScrollBar; import javafx.scene.control.ScrollPane; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.scene.shape.Rectangle; import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; import javafx.stage.Modality; import javafx.stage.Stage; import javafx.stage.StageStyle; import javafx.util.Callback; import javafx.util.StringConverter; import lombok.extern.slf4j.Slf4j; import monero.common.MoneroUtils; import monero.daemon.model.MoneroTx; import monero.wallet.model.MoneroTxConfig; import org.apache.commons.lang3.StringUtils; import org.bitcoinj.core.Coin; import org.jetbrains.annotations.NotNull; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.math.BigInteger; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; import static com.google.common.base.Preconditions.checkArgument; import static haveno.desktop.util.FormBuilder.addTopLabelComboBoxComboBox; @Slf4j public class GUIUtil { public final static String SHOW_ALL_FLAG = "list.currency.showAll"; // Used for accessing the i18n resource public final static String EDIT_FLAG = "list.currency.editList"; // Used for accessing the i18n resource public final static String OPEN_WEB_PAGE_KEY = "warnOpenURLWhenTorEnabled"; public final static int NUM_DECIMALS_UNIT = 0; public final static int NUM_DECIMALS_PRICE_LESS_PRECISE = 3; public final static int NUM_DECIMALS_PRECISE = 7; public final static int AMOUNT_DECIMALS_WITH_ZEROS = 3; public final static int AMOUNT_DECIMALS = 4; public static final double NUM_OFFERS_TRANSLATE_X = -13.0; public static final boolean disablePaymentUriLabel = true; // universally disable payment uri labels, allowing bigger xmr logo overlays private static Preferences preferences; public static void setPreferences(Preferences preferences) { GUIUtil.preferences = preferences; } public static String getUserLanguage() { return preferences.getUserLanguage(); } public static double getScrollbarWidth(Node scrollablePane) { Node node = scrollablePane.lookup(".scroll-bar"); if (node instanceof ScrollBar) { final ScrollBar bar = (ScrollBar) node; if (bar.getOrientation().equals(Orientation.VERTICAL)) return bar.getWidth(); } return 0; } public static void focusWhenAddedToScene(Node node) { node.sceneProperty().addListener((observable, oldValue, newValue) -> { if (null != newValue) { node.requestFocus(); } }); } public static void exportAccounts(ArrayList accounts, String fileName, Preferences preferences, Stage stage, PersistenceProtoResolver persistenceProtoResolver, CorruptedStorageFileHandler corruptedStorageFileHandler) { if (!accounts.isEmpty()) { String directory = getDirectoryFromChooser(preferences, stage); if (!directory.isEmpty()) { PersistenceManager persistenceManager = new PersistenceManager<>(new File(directory), persistenceProtoResolver, corruptedStorageFileHandler, null); PaymentAccountList paymentAccounts = new PaymentAccountList(accounts); persistenceManager.initialize(paymentAccounts, fileName, PersistenceManager.Source.PRIVATE_LOW_PRIO); persistenceManager.persistNow(() -> { persistenceManager.shutdown(); new Popup().feedback(Res.get("guiUtil.accountExport.savedToPath", Paths.get(directory, fileName).toAbsolutePath())) .show(); }); } } else { new Popup().warning(Res.get("guiUtil.accountExport.noAccountSetup")).show(); } } public static void importAccounts(User user, String fileName, Preferences preferences, Stage stage, PersistenceProtoResolver persistenceProtoResolver, CorruptedStorageFileHandler corruptedStorageFileHandler) { FileChooser fileChooser = new FileChooser(); File initDir = new File(preferences.getDirectoryChooserPath()); if (initDir.isDirectory()) { fileChooser.setInitialDirectory(initDir); } fileChooser.setTitle(Res.get("guiUtil.accountExport.selectPath", fileName)); File file = fileChooser.showOpenDialog(stage.getOwner()); if (file != null) { String path = file.getAbsolutePath(); if (Paths.get(path).getFileName().toString().equals(fileName)) { String directory = Paths.get(path).getParent().toString(); preferences.setDirectoryChooserPath(directory); PersistenceManager persistenceManager = new PersistenceManager<>(new File(directory), persistenceProtoResolver, corruptedStorageFileHandler, null); persistenceManager.readPersisted(fileName, persisted -> { StringBuilder msg = new StringBuilder(); HashSet paymentAccounts = new HashSet<>(); synchronized (persisted.getList()) { persisted.getList().forEach(paymentAccount -> { String id = paymentAccount.getId(); if (user.getPaymentAccount(id) == null) { paymentAccounts.add(paymentAccount); msg.append(Res.get("guiUtil.accountExport.tradingAccount", id)); } else { msg.append(Res.get("guiUtil.accountImport.noImport", id)); } }); } user.addImportedPaymentAccounts(paymentAccounts); new Popup().feedback(Res.get("guiUtil.accountImport.imported", path, msg)).show(); }, () -> { new Popup().warning(Res.get("guiUtil.accountImport.noAccountsFound", path, fileName)).show(); }); } else { log.error("The selected file is not the expected file for import. The expected file name is: " + fileName + "."); } } } public static void exportCSV(String fileName, CSVEntryConverter headerConverter, CSVEntryConverter contentConverter, T emptyItem, List list, Stage stage) { FileChooser fileChooser = new FileChooser(); fileChooser.setInitialFileName(fileName); File file = fileChooser.showSaveDialog(stage); if (file != null) { try (OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(file, false), Charsets.UTF_8)) { CSVWriter headerWriter = new CSVWriterBuilder(outputStreamWriter) .strategy(CSVStrategy.UK_DEFAULT) .entryConverter(headerConverter) .build(); headerWriter.write(emptyItem); CSVWriter contentWriter = new CSVWriterBuilder(outputStreamWriter) .strategy(CSVStrategy.UK_DEFAULT) .entryConverter(contentConverter) .build(); contentWriter.writeAll(list); } catch (RuntimeException | IOException e) { e.printStackTrace(); log.error(e.getMessage()); new Popup().error(Res.get("guiUtil.accountExport.exportFailed", e.getMessage())).show(); } } } public static void exportJSON(String fileName, JsonElement data, Stage stage) { FileChooser fileChooser = new FileChooser(); fileChooser.setInitialFileName(fileName); File file = fileChooser.showSaveDialog(stage); if (file != null) { try (OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(file, false), Charsets.UTF_8)) { Gson gson = new GsonBuilder().setPrettyPrinting().create(); outputStreamWriter.write(gson.toJson(data)); } catch (RuntimeException | IOException e) { e.printStackTrace(); log.error(e.getMessage()); new Popup().error(Res.get("guiUtil.accountExport.exportFailed", e.getMessage())); } } } private static String getDirectoryFromChooser(Preferences preferences, Stage stage) { DirectoryChooser directoryChooser = new DirectoryChooser(); File initDir = new File(preferences.getDirectoryChooserPath()); if (initDir.isDirectory()) { directoryChooser.setInitialDirectory(initDir); } directoryChooser.setTitle(Res.get("guiUtil.accountExport.selectExportPath")); File dir = directoryChooser.showDialog(stage); if (dir != null) { String directory = dir.getAbsolutePath(); preferences.setDirectoryChooserPath(directory); return directory; } else { return ""; } } public static Callback, ListCell> getCurrencyListItemCellFactory(String postFixSingle, String postFixMulti, Preferences preferences) { return p -> new ListCell<>() { @Override protected void updateItem(CurrencyListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { String code = item.tradeCurrency.getCode(); HBox box = new HBox(); box.setSpacing(20); box.setAlignment(Pos.CENTER_LEFT); Label label1 = new AutoTooltipLabel(getCurrencyType(code)); label1.getStyleClass().add("currency-label-small"); Label label2 = new AutoTooltipLabel(CurrencyUtil.isCryptoCurrency(code) ? item.tradeCurrency.getNameAndCode() : code); label2.getStyleClass().add("currency-label"); Label label3 = new AutoTooltipLabel(CurrencyUtil.isCryptoCurrency(code) ? "" : item.tradeCurrency.getName()); if (!CurrencyUtil.isCryptoCurrency(code)) label3.getStyleClass().add("currency-label"); Label label4 = new AutoTooltipLabel(); box.getChildren().addAll(label1, label2, label3); if (!CurrencyUtil.isCryptoCurrency(code)) box.getChildren().add(label4); switch (code) { case GUIUtil.SHOW_ALL_FLAG: label1.setText(Res.get("shared.all")); label2.setText(Res.get("list.currency.showAll")); break; case GUIUtil.EDIT_FLAG: label1.setText(Res.get("shared.edit")); label2.setText(Res.get("list.currency.editList")); break; default: // use icon if available StackPane currencyIcon = getCurrencyIcon(code); if (currencyIcon != null) { label1.setText(""); label1.setGraphic(currencyIcon); } if (preferences.isSortMarketCurrenciesNumerically() && item.numTrades > 0) { boolean isCrypto = CurrencyUtil.isCryptoCurrency(code); Label offersTarget = isCrypto ? label3 : label4; HBox.setMargin(offersTarget, new Insets(0, 0, 0, NUM_OFFERS_TRANSLATE_X)); offersTarget.getStyleClass().add("offer-label"); offersTarget.setText(item.numTrades + " " + (item.numTrades == 1 ? postFixSingle : postFixMulti)); } } setGraphic(box); } else { setGraphic(null); } } }; } public static ListCell getTradeCurrencyButtonCell(String postFixSingle, String postFixMulti, Map offerCounts) { return new ListCell<>() { @Override protected void updateItem(TradeCurrency item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { String code = item.getCode(); AnchorPane pane = new AnchorPane(); Label currency = new AutoTooltipLabel(item.getName() + " (" + item.getCode() + ")"); currency.getStyleClass().add("currency-label-selected"); AnchorPane.setLeftAnchor(currency, 0.0); pane.getChildren().add(currency); Optional offerCountOptional = Optional.ofNullable(offerCounts.get(code)); switch (code) { case GUIUtil.SHOW_ALL_FLAG: currency.setText(Res.get("list.currency.showAll")); break; case GUIUtil.EDIT_FLAG: currency.setText(Res.get("list.currency.editList")); break; default: if (offerCountOptional.isPresent()) { Label numberOfOffers = new AutoTooltipLabel(offerCountOptional.get() + " " + (offerCountOptional.get() == 1 ? postFixSingle : postFixMulti)); numberOfOffers.getStyleClass().add("offer-label-small"); AnchorPane.setRightAnchor(numberOfOffers, 0.0); AnchorPane.setBottomAnchor(numberOfOffers, 2.0); pane.getChildren().add(numberOfOffers); } } setGraphic(pane); setText(""); } else { setGraphic(null); setText(""); } } }; } public static Callback, ListCell> getTradeCurrencyCellFactory(String postFixSingle, String postFixMulti, Map offerCounts) { return p -> new ListCell<>() { @Override protected void updateItem(TradeCurrency item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { String code = item.getCode(); HBox box = new HBox(); box.setSpacing(20); box.setAlignment(Pos.CENTER_LEFT); Label label1 = new AutoTooltipLabel(getCurrencyType(item.getCode())); label1.getStyleClass().add("currency-label-small"); Label label2 = new AutoTooltipLabel(CurrencyUtil.isCryptoCurrency(code) ? item.getNameAndCode() : code); label2.getStyleClass().add("currency-label"); Label label3 = new AutoTooltipLabel(CurrencyUtil.isCryptoCurrency(code) ? "" : item.getName()); if (!CurrencyUtil.isCryptoCurrency(code)) label3.getStyleClass().add("currency-label"); Label label4 = new AutoTooltipLabel(); Optional offerCountOptional = Optional.ofNullable(offerCounts.get(code)); switch (code) { case GUIUtil.SHOW_ALL_FLAG: label1.setText(Res.get("shared.all")); label2.setText(Res.get("list.currency.showAll")); break; case GUIUtil.EDIT_FLAG: label1.setText(Res.get("shared.edit")); label2.setText(Res.get("list.currency.editList")); break; default: // use icon if available StackPane currencyIcon = getCurrencyIcon(code); if (currencyIcon != null) { label1.setText(""); label1.setGraphic(currencyIcon); } boolean isCrypto = CurrencyUtil.isCryptoCurrency(code); Label offersTarget = isCrypto ? label3 : label4; offerCountOptional.ifPresent(numOffers -> { HBox.setMargin(offersTarget, new Insets(0, 0, 0, NUM_OFFERS_TRANSLATE_X)); offersTarget.getStyleClass().add("offer-label"); offersTarget.setText(numOffers + " " + (numOffers == 1 ? postFixSingle : postFixMulti)); }); } box.getChildren().addAll(label1, label2, label3); if (!CurrencyUtil.isCryptoCurrency(code)) box.getChildren().add(label4); setGraphic(box); } else { setGraphic(null); } } }; } public static Callback, ListCell> getTradeCurrencyCellFactoryNameAndCode() { return p -> new ListCell<>() { @Override protected void updateItem(TradeCurrency item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { HBox box = new HBox(); box.setSpacing(10); Label label1 = new AutoTooltipLabel(getCurrencyType(item.getCode())); label1.getStyleClass().add("currency-label-small"); Label label2 = new AutoTooltipLabel(item.getNameAndCode()); label2.getStyleClass().add("currency-label"); // use icon if available StackPane currencyIcon = getCurrencyIcon(item.getCode()); if (currencyIcon != null) { label1.setText(""); label1.setGraphic(currencyIcon); } box.getChildren().addAll(label1, label2); setGraphic(box); } else { setGraphic(null); } } }; } private static String getCurrencyType(String code) { if (CurrencyUtil.isFiatCurrency(code)) { return Res.get("shared.fiat"); } else if (CurrencyUtil.isTraditionalCurrency(code)) { return Res.get("shared.traditional"); } else if (CurrencyUtil.isCryptoCurrency(code)) { return Res.get("shared.crypto"); } else { return ""; } } private static String getCurrencyType(PaymentMethod method) { return method.isTraditional() ? Res.get("shared.traditional") : Res.get("shared.crypto"); } public static ListCell getPaymentMethodButtonCell() { return new ListCell<>() { @Override protected void updateItem(PaymentMethod item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { String id = item.getId(); this.getStyleClass().add("currency-label-selected"); if (id.equals(GUIUtil.SHOW_ALL_FLAG)) { setText(Res.get("list.currency.showAll")); } else { setText(Res.get(id)); } } else { setText(""); } } }; } public static Callback, ListCell> getPaymentMethodCellFactory() { return p -> new ListCell<>() { @Override protected void updateItem(PaymentMethod method, boolean empty) { super.updateItem(method, empty); if (method != null && !empty) { String id = method.getId(); HBox box = new HBox(); box.setSpacing(20); Label paymentType = new AutoTooltipLabel(getCurrencyType(method)); paymentType.getStyleClass().add("currency-label-small"); Label paymentMethod = new AutoTooltipLabel(Res.get(id)); paymentMethod.getStyleClass().add("currency-label"); box.getChildren().addAll(paymentType, paymentMethod); if (id.equals(GUIUtil.SHOW_ALL_FLAG)) { paymentType.setText(Res.get("shared.all")); paymentMethod.setText(Res.get("list.currency.showAll")); } setGraphic(box); } else { setGraphic(null); } } }; } public static void updateConfidence(MoneroTx tx, Tooltip tooltip, TxConfidenceIndicator txConfidenceIndicator) { updateConfidence(tx, null, tooltip, txConfidenceIndicator); } public static void updateConfidence(MoneroTx tx, Trade trade, Tooltip tooltip, TxConfidenceIndicator txConfidenceIndicator) { if (tx == null || tx.getNumConfirmations() == null || !tx.isRelayed()) { if (trade != null && trade.isDepositsUnlocked()) { tooltip.setText(Res.get("confidence.confirmed", ">=10")); txConfidenceIndicator.setProgress(1.0); } else { tooltip.setText(Res.get("confidence.unknown")); txConfidenceIndicator.setProgress(-1); } } else { if (tx.isFailed()) { tooltip.setText(Res.get("confidence.invalid")); txConfidenceIndicator.setProgress(0); } else if (tx.isConfirmed()) { tooltip.setText(Res.get("confidence.confirmed", tx.getNumConfirmations())); txConfidenceIndicator.setProgress((double) tx.getNumConfirmations() / (double) XmrWalletService.NUM_BLOCKS_UNLOCK); } else { tooltip.setText(Res.get("confidence.confirmed", 0)); txConfidenceIndicator.setProgress(-1); } } txConfidenceIndicator.setPrefSize(24, 24); } public static void openWebPage(String target) { openWebPage(target, true, null); } public static void openWebPage(String target, boolean useReferrer) { openWebPage(target, useReferrer, null); } public static void openWebPageNoPopup(String target) { doOpenWebPage(target); } public static void openWebPage(String target, boolean useReferrer, Runnable closeHandler) { if (useReferrer && target.contains("haveno.network")) { // add utm parameters target = appendURI(target, "utm_source=desktop-client&utm_medium=in-app-link&utm_campaign=language_" + preferences.getUserLanguage()); } if (DontShowAgainLookup.showAgain(OPEN_WEB_PAGE_KEY)) { final String finalTarget = target; new Popup().information(Res.get("guiUtil.openWebBrowser.warning", target)) .actionButtonText(Res.get("guiUtil.openWebBrowser.doOpen")) .onAction(() -> { DontShowAgainLookup.dontShowAgain(OPEN_WEB_PAGE_KEY, true); doOpenWebPage(finalTarget); }) .closeButtonText(Res.get("guiUtil.openWebBrowser.copyUrl")) .onClose(() -> { Utilities.copyToClipboard(finalTarget); if (closeHandler != null) { closeHandler.run(); } }) .show(); } else { if (closeHandler != null) { closeHandler.run(); } doOpenWebPage(target); } } private static String appendURI(String uri, String appendQuery) { try { final URI oldURI = new URI(uri); String newQuery = oldURI.getQuery(); if (newQuery == null) { newQuery = appendQuery; } else { newQuery += "&" + appendQuery; } URI newURI = new URI(oldURI.getScheme(), oldURI.getAuthority(), oldURI.getPath(), newQuery, oldURI.getFragment()); return newURI.toString(); } catch (URISyntaxException e) { e.printStackTrace(); log.error(e.getMessage()); return uri; } } private static void doOpenWebPage(String target) { try { Utilities.openURI(safeParse(target)); } catch (Exception e) { e.printStackTrace(); log.error(e.getMessage()); } } private static URI safeParse(String url) throws URISyntaxException { int hashIndex = url.indexOf('#'); if (hashIndex >= 0 && hashIndex < url.length() - 1) { String base = url.substring(0, hashIndex); String fragment = url.substring(hashIndex + 1); String encodedFragment = URLEncoder.encode(fragment, StandardCharsets.UTF_8); return new URI(base + "#" + encodedFragment); } return new URI(url); // no fragment } public static String getPercentageOfTradeAmount(BigInteger fee, BigInteger tradeAmount) { String result = " (" + getPercentage(fee, tradeAmount) + " " + Res.get("guiUtil.ofTradeAmount") + ")"; return result; } public static String getPercentage(BigInteger part, BigInteger total) { return FormattingUtils.formatToPercentWithSymbol(HavenoUtils.divide(part, total)); } public static T getParentOfType(Node node, Class t) { Node parent = node.getParent(); while (parent != null) { if (parent.getClass().isAssignableFrom(t)) { break; } else { parent = parent.getParent(); } } return t.cast(parent); } public static void showZelleWarning() { String key = "confirmZelleRequirements"; final String currencyName = Config.baseCurrencyNetwork().getCurrencyName(); new Popup().information(Res.get("payment.zelle.info", currencyName, currencyName)) .width(900) .closeButtonText(Res.get("shared.iConfirm")) .dontShowAgainId(key) .show(); } public static void showFasterPaymentsWarning(Navigation navigation) { String key = "recreateFasterPaymentsAccount"; String currencyName = Config.baseCurrencyNetwork().getCurrencyName(); new Popup().information(Res.get("payment.fasterPayments.newRequirements.info", currencyName)) .width(900) .actionButtonTextWithGoTo("mainView.menu.account") .onAction(() -> { navigation.setReturnPath(navigation.getCurrentPath()); navigation.navigateTo(MainView.class, AccountView.class, TraditionalAccountsView.class); }) .dontShowAgainId(key) .show(); } public static String getMoneroURI(String address, BigInteger amount, String label) { MoneroTxConfig txConfig = new MoneroTxConfig().setAddress(address); if (amount != null) txConfig.setAmount(amount); if (label != null && !label.isEmpty() && !disablePaymentUriLabel) txConfig.setNote(label); return MoneroUtils.getPaymentUri(txConfig); } public static boolean isBootstrappedOrShowPopup(P2PService p2PService) { if (p2PService.isBootstrapped() && p2PService.getNumConnectedPeers().get() > 0) { return true; } new Popup().information(Res.get("popup.warning.notFullyConnected")).show(); return false; } public static boolean isReadyForTxBroadcastOrShowPopup(XmrWalletService xmrWalletService) { XmrConnectionService xmrConnectionService = xmrWalletService.getXmrConnectionService(); if (!xmrConnectionService.hasSufficientPeersForBroadcast()) { new Popup().information(Res.get("popup.warning.notSufficientConnectionsToXmrNetwork", xmrConnectionService.getMinBroadcastConnections())).show(); return false; } if (!xmrConnectionService.isDownloadComplete()) { new Popup().information(Res.get("popup.warning.downloadNotComplete")).show(); return false; } if (!isWalletSyncedWithinToleranceOrShowPopup(xmrWalletService)) { return false; } try { xmrConnectionService.verifyConnection(); } catch (Exception e) { new Popup().information(e.getMessage()).show(); return false; } return true; } public static boolean isWalletSyncedWithinToleranceOrShowPopup(XmrWalletService xmrWalletService) { if (!xmrWalletService.isSyncedWithinTolerance()) { new Popup().information(Res.get("popup.warning.walletNotSynced")).show(); return false; } return true; } public static boolean canCreateOrTakeOfferOrShowPopup(User user, Navigation navigation) { if (!user.hasAcceptedArbitrators()) { log.warn("There are no arbitrators available"); new Popup().warning(Res.get("popup.warning.noArbitratorsAvailable")).show(); return false; } if (user.currentPaymentAccountProperty().get() == null) { new Popup().headLine(Res.get("popup.warning.noTradingAccountSetup.headline")) .instruction(Res.get("popup.warning.noTradingAccountSetup.msg")) .actionButtonTextWithGoTo("mainView.menu.account") .onAction(() -> { navigation.setReturnPath(navigation.getCurrentPath()); navigation.navigateTo(MainView.class, AccountView.class, TraditionalAccountsView.class); }).show(); return false; } return true; } public static void showWantToBurnBTCPopup(Coin miningFee, Coin amount, CoinFormatter btcFormatter) { new Popup().warning(Res.get("popup.warning.burnXMR", btcFormatter.formatCoinWithCode(miningFee), btcFormatter.formatCoinWithCode(amount))).show(); } public static void requestFocus(Node node) { UserThread.execute(node::requestFocus); } public static void rescanOutputs(Preferences preferences) { try { new Popup().information(Res.get("settings.net.rescanOutputsSuccess")) .actionButtonText(Res.get("shared.yes")) .onAction(() -> { throw new RuntimeException("Rescanning wallet outputs not yet implemented"); //UserThread.runAfter(HavenoApp.getShutDownHandler(), 100, TimeUnit.MILLISECONDS); }) .closeButtonText(Res.get("shared.cancel")) .show(); } catch (Throwable t) { new Popup().error(Res.get("settings.net.rescanOutputsFailed", t)).show(); } } public static void showSelectableTextModal(String title, String text) { TextArea textArea = new HavenoTextArea(); textArea.setText(text); textArea.setEditable(false); textArea.setWrapText(true); textArea.setPrefSize(800, 600); Scene scene = new Scene(textArea); Stage stage = new Stage(); if (null != title) { stage.setTitle(title); } stage.setScene(scene); stage.initModality(Modality.NONE); stage.initStyle(StageStyle.UTILITY); stage.show(); } public static StringConverter getPaymentAccountsComboBoxStringConverter() { return new StringConverter<>() { @Override public String toString(PaymentAccount paymentAccount) { if (paymentAccount.hasMultipleCurrencies()) { return paymentAccount.getAccountName() + " (" + Res.get(paymentAccount.getPaymentMethod().getId()) + ")"; } else { TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency(); String prefix = singleTradeCurrency != null ? singleTradeCurrency.getCode() + ", " : ""; return paymentAccount.getAccountName() + " (" + prefix + Res.get(paymentAccount.getPaymentMethod().getId()) + ")"; } } @Override public PaymentAccount fromString(String s) { return null; } }; } public static Callback, ListCell> getPaymentAccountListCellFactory( ComboBox paymentAccountsComboBox, AccountAgeWitnessService accountAgeWitnessService) { return p -> new ListCell<>() { @Override protected void updateItem(PaymentAccount item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { boolean needsSigning = PaymentMethod.hasChargebackRisk(item.getPaymentMethod(), item.getTradeCurrencies()); InfoAutoTooltipLabel label = new InfoAutoTooltipLabel( paymentAccountsComboBox.getConverter().toString(item), ContentDisplay.RIGHT); if (needsSigning) { AccountAgeWitness myWitness = accountAgeWitnessService.getMyWitness( item.paymentAccountPayload); AccountAgeWitnessService.SignState signState = accountAgeWitnessService.getSignState(myWitness); String info = StringUtils.capitalize(signState.getDisplayString()); MaterialDesignIcon icon = getIconForSignState(signState); label.setIcon(icon, info); } setGraphic(label); } else { setGraphic(null); } } }; } public static void removeChildrenFromGridPaneRows(GridPane gridPane, int start, int end) { Map> childByRowMap = new HashMap<>(); gridPane.getChildren().forEach(child -> { final Integer rowIndex = GridPane.getRowIndex(child); childByRowMap.computeIfAbsent(rowIndex, key -> new ArrayList<>()); childByRowMap.get(rowIndex).add(child); }); for (int i = Math.min(start, childByRowMap.size()); i < Math.min(end + 1, childByRowMap.size()); i++) { List nodes = childByRowMap.get(i); if (nodes != null) { nodes.stream() .filter(Objects::nonNull) .filter(node -> gridPane.getChildren().contains(node)) .forEach(node -> gridPane.getChildren().remove(node)); } } } public static void setFitToRowsForTableView(TableView tableView, int rowHeight, int headerHeight, int minNumRows, int maxNumRows) { int size = tableView.getItems().size(); int minHeight = rowHeight * minNumRows + headerHeight; int maxHeight = rowHeight * maxNumRows + headerHeight; checkArgument(maxHeight >= minHeight, "maxHeight cannot be smaller as minHeight"); int height = Math.min(maxHeight, Math.max(minHeight, size * rowHeight + headerHeight)); tableView.setPrefHeight(-1); tableView.setVisible(false); // We need to delay the setter to the next render frame as otherwise views don' get updated in some cases // Not 100% clear what causes that issue, but seems the requestLayout method is not called otherwise. // We still need to set the height immediately, otherwise some views render an incorrect layout. tableView.setPrefHeight(height); UserThread.execute(() -> { tableView.setPrefHeight(height); tableView.setVisible(true); }); } public static Tuple2, Integer> addRegionCountryTradeCurrencyComboBoxes(GridPane gridPane, int gridRow, Consumer onCountrySelectedHandler, Consumer onTradeCurrencySelectedHandler) { gridRow = addRegionCountry(gridPane, gridRow, onCountrySelectedHandler); ComboBox currencyComboBox = FormBuilder.addComboBox(gridPane, ++gridRow, Res.get("shared.currency")); currencyComboBox.setPromptText(Res.get("list.currency.select")); currencyComboBox.setItems(FXCollections.observableArrayList(CurrencyUtil.getAllSortedTraditionalCurrencies())); currencyComboBox.setConverter(new StringConverter<>() { @Override public String toString(TradeCurrency currency) { return currency.getNameAndCode(); } @Override public TradeCurrency fromString(String string) { return null; } }); currencyComboBox.setDisable(true); currencyComboBox.setOnAction(e -> onTradeCurrencySelectedHandler.accept(currencyComboBox.getSelectionModel().getSelectedItem())); return new Tuple2<>(currencyComboBox, gridRow); } public static int addRegionCountry(GridPane gridPane, int gridRow, Consumer onCountrySelectedHandler) { Tuple3, ComboBox> tuple3 = addTopLabelComboBoxComboBox(gridPane, ++gridRow, Res.get("payment.country")); ComboBox regionComboBox = tuple3.second; regionComboBox.setPromptText(Res.get("payment.select.region")); regionComboBox.setConverter(new StringConverter<>() { @Override public String toString(haveno.core.locale.Region region) { return region.name; } @Override public haveno.core.locale.Region fromString(String s) { return null; } }); regionComboBox.setItems(FXCollections.observableArrayList(CountryUtil.getAllRegions())); ComboBox countryComboBox = tuple3.third; countryComboBox.setVisibleRowCount(15); countryComboBox.setDisable(true); countryComboBox.setPromptText(Res.get("payment.select.country")); countryComboBox.setConverter(new StringConverter<>() { @Override public String toString(Country country) { return country.name + " (" + country.code + ")"; } @Override public Country fromString(String s) { return null; } }); regionComboBox.setOnAction(e -> { haveno.core.locale.Region selectedItem = regionComboBox.getSelectionModel().getSelectedItem(); if (selectedItem != null) { countryComboBox.setDisable(false); countryComboBox.setItems(FXCollections.observableArrayList(CountryUtil.getAllCountriesForRegion(selectedItem))); } }); countryComboBox.setOnAction(e -> onCountrySelectedHandler.accept(countryComboBox.getSelectionModel().getSelectedItem())); return gridRow; } @NotNull public static ListCell getComboBoxButtonCell(String title, ComboBox comboBox) { return getComboBoxButtonCell(title, comboBox, true); } @NotNull public static ListCell getComboBoxButtonCell(String title, ComboBox comboBox, Boolean hideOriginalPrompt) { return new ListCell<>() { @Override protected void updateItem(T item, boolean empty) { super.updateItem(item, empty); // See https://github.com/jfoenixadmin/JFoenix/issues/610 if (hideOriginalPrompt) this.setVisible(item != null || !empty); if (empty || item == null) { setText(title); } else { setText(comboBox.getConverter().toString(item)); } } }; } public static MaterialDesignIcon getIconForSignState(AccountAgeWitnessService.SignState state) { if (state.equals(AccountAgeWitnessService.SignState.PEER_INITIAL)) { return MaterialDesignIcon.CLOCK; } return (state.equals(AccountAgeWitnessService.SignState.ARBITRATOR) || state.equals(AccountAgeWitnessService.SignState.PEER_SIGNER)) ? MaterialDesignIcon.APPROVAL : MaterialDesignIcon.ALERT_CIRCLE_OUTLINE; } public static ScrollPane createScrollPane() { ScrollPane scrollPane = new ScrollPane(); scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); scrollPane.setFitToWidth(true); scrollPane.setFitToHeight(true); AnchorPane.setLeftAnchor(scrollPane, 0d); AnchorPane.setTopAnchor(scrollPane, 0d); AnchorPane.setRightAnchor(scrollPane, 0d); AnchorPane.setBottomAnchor(scrollPane, 0d); return scrollPane; } public static void setDefaultTwoColumnConstraintsForGridPane(GridPane gridPane) { ColumnConstraints columnConstraints1 = new ColumnConstraints(); columnConstraints1.setHalignment(HPos.RIGHT); columnConstraints1.setHgrow(Priority.NEVER); columnConstraints1.setMinWidth(200); ColumnConstraints columnConstraints2 = new ColumnConstraints(); columnConstraints2.setHgrow(Priority.ALWAYS); gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2); } public static void applyFilledStyle(TextField textField) { textField.textProperty().addListener((observable, oldValue, newValue) -> { updateFilledStyle(textField); }); } private static void updateFilledStyle(TextField textField) { if (textField.getText() != null && !textField.getText().isEmpty()) { if (!textField.getStyleClass().contains("filled")) { textField.getStyleClass().add("filled"); } } else { textField.getStyleClass().remove("filled"); } } public static void applyFilledStyle(ComboBox comboBox) { comboBox.valueProperty().addListener((observable, oldValue, newValue) -> { updateFilledStyle(comboBox); }); } private static void updateFilledStyle(ComboBox comboBox) { if (comboBox.getValue() != null) { if (!comboBox.getStyleClass().contains("filled")) { comboBox.getStyleClass().add("filled"); } } else { comboBox.getStyleClass().remove("filled"); } } public static void applyTableStyle(TableView tableView) { applyTableStyle(tableView, true); } public static void applyTableStyle(TableView tableView, boolean applyRoundedArc) { if (applyRoundedArc) applyRoundedArc(tableView); addSpacerColumns(tableView); applyEdgeColumnStyleClasses(tableView); } private static void applyRoundedArc(TableView tableView) { Rectangle clip = new Rectangle(); clip.setArcWidth(Layout.ROUNDED_ARC); clip.setArcHeight(Layout.ROUNDED_ARC); tableView.setClip(clip); tableView.layoutBoundsProperty().addListener((obs, oldVal, newVal) -> { clip.setWidth(newVal.getWidth()); clip.setHeight(newVal.getHeight()); }); } private static void addSpacerColumns(TableView tableView) { TableColumn leftSpacer = new TableColumn<>(); TableColumn rightSpacer = new TableColumn<>(); configureSpacerColumn(leftSpacer); configureSpacerColumn(rightSpacer); tableView.getColumns().add(0, leftSpacer); tableView.getColumns().add(rightSpacer); } private static void configureSpacerColumn(TableColumn column) { column.setPrefWidth(15); column.setMaxWidth(15); column.setMinWidth(15); column.setReorderable(false); column.setResizable(false); column.setSortable(false); column.setCellFactory(col -> new TableCell<>()); // empty cell } private static void applyEdgeColumnStyleClasses(TableView tableView) { ListChangeListener> columnListener = change -> { UserThread.execute(() -> { updateEdgeColumnStyleClasses(tableView); }); }; tableView.getColumns().addListener(columnListener); tableView.skinProperty().addListener((obs, oldSkin, newSkin) -> { if (newSkin != null) { UserThread.execute(() -> { updateEdgeColumnStyleClasses(tableView); }); } }); // react to size changes ChangeListener sizeListener = (obs, oldVal, newVal) -> updateEdgeColumnStyleClasses(tableView); tableView.heightProperty().addListener(sizeListener); tableView.widthProperty().addListener(sizeListener); updateEdgeColumnStyleClasses(tableView); } private static void updateEdgeColumnStyleClasses(TableView tableView) { ObservableList> columns = tableView.getColumns(); // find columns with "first-column" and "last-column" classes TableColumn firstCol = null; TableColumn lastCol = null; for (TableColumn col : columns) { if (col.getStyleClass().contains("first-column")) { firstCol = col; } else if (col.getStyleClass().contains("last-column")) { lastCol = col; } } // handle if columns do not exist if (firstCol == null || lastCol == null) { if (firstCol != null) throw new IllegalStateException("Missing column with 'last-column'"); if (lastCol != null) throw new IllegalStateException("Missing column with 'first-column'"); // remove all classes for (TableColumn col : columns) { col.getStyleClass().removeAll("first-column", "last-column"); } // apply first and last classes if (!columns.isEmpty()) { TableColumn first = columns.get(0); TableColumn last = columns.get(columns.size() - 1); if (!first.getStyleClass().contains("first-column")) { first.getStyleClass().add("first-column"); } if (!last.getStyleClass().contains("last-column")) { last.getStyleClass().add("last-column"); } } } else { // done if correct order if (columns.get(0) == firstCol && columns.get(columns.size() - 1) == lastCol) { return; } // set first and last columns if (columns.get(0) != firstCol) { columns.remove(firstCol); columns.add(0, firstCol); } if (columns.get(columns.size() - 1) != lastCol) { columns.remove(lastCol); columns.add(firstCol == lastCol ? columns.size() - 1 : columns.size(), lastCol); } } } public static ObservableList> getContentColumns(TableView tableView) { ObservableList> contentColumns = FXCollections.observableArrayList(); for (TableColumn column : tableView.getColumns()) { if (!column.getStyleClass().contains("first-column") && !column.getStyleClass().contains("last-column")) { contentColumns.add(column); } } return contentColumns; } private static ImageView getCurrencyImageView(String currencyCode) { return getCurrencyImageView(currencyCode, 24); } private static ImageView getCurrencyImageView(String currencyCode, double size) { if (currencyCode == null) return null; String imageId = getImageId(currencyCode); if (imageId == null) return null; ImageView icon = new ImageView(); icon.setFitWidth(size); icon.setPreserveRatio(true); icon.setSmooth(true); icon.setCache(true); icon.setId(imageId); return icon; } public static StackPane getCurrencyIcon(String currencyCode) { ImageView icon = getCurrencyImageView(currencyCode); return icon == null ? null : new StackPane(icon); } public static StackPane getCurrencyIcon(String currencyCode, double size) { ImageView icon = getCurrencyImageView(currencyCode, size); return icon == null ? null : new StackPane(icon); } public static StackPane getCurrencyIconWithBorder(String currencyCode) { return getCurrencyIconWithBorder(currencyCode, 25, 1); } public static StackPane getCurrencyIconWithBorder(String currencyCode, double size, double borderWidth) { if (currencyCode == null) return null; ImageView icon = getCurrencyImageView(currencyCode, size); icon.setFitWidth(size - 2 * borderWidth); icon.setFitHeight(size - 2 * borderWidth); StackPane circleWrapper = new StackPane(icon); circleWrapper.setPrefSize(size, size); circleWrapper.setMaxSize(size, size); circleWrapper.setMinSize(size, size); circleWrapper.setStyle( "-fx-background-color: white;" + "-fx-background-radius: 50%;" + "-fx-border-radius: 50%;" + "-fx-border-color: white;" + "-fx-border-width: " + borderWidth + "px;" ); StackPane.setAlignment(icon, Pos.CENTER); return circleWrapper; } private static String getImageId(String currencyCode) { if (currencyCode == null) return null; if (CurrencyUtil.isCryptoCurrency(currencyCode)) return "image-" + currencyCode.toLowerCase() + "-logo"; if (CurrencyUtil.isFiatCurrency(currencyCode)) return "image-fiat-logo"; return null; } public static void adjustHeightAutomatically(TextArea textArea) { adjustHeightAutomatically(textArea, null); } public static void adjustHeightAutomatically(TextArea textArea, Double maxHeight) { textArea.sceneProperty().addListener((o, oldScene, newScene) -> { if (newScene != null) { // avoid javafx css warning CssTheme.loadSceneStyles(newScene, CssTheme.getCurrentTheme(), false); textArea.applyCss(); var text = textArea.lookup(".text"); textArea.prefHeightProperty().bind(Bindings.createDoubleBinding(() -> { Insets padding = textArea.getInsets(); double topBottomPadding = padding.getTop() + padding.getBottom(); double prefHeight = textArea.getFont().getSize() + text.getBoundsInLocal().getHeight() + topBottomPadding; return maxHeight == null ? prefHeight : Math.min(prefHeight, maxHeight); }, text.boundsInLocalProperty())); text.boundsInLocalProperty().addListener((observableBoundsAfter, boundsBefore, boundsAfter) -> { Platform.runLater(() -> textArea.requestLayout()); }); } }); } public static Label getLockLabel() { Label lockLabel = FormBuilder.getIcon(AwesomeIcon.LOCK, "16px"); lockLabel.setStyle(lockLabel.getStyle() + " -fx-text-fill: white;"); return lockLabel; } public static MaterialDesignIconView getCopyIcon() { return new MaterialDesignIconView(MaterialDesignIcon.CONTENT_COPY, "1.35em"); } public static Tuple2 getSmallXmrQrCodePane() { return getXmrQrCodePane(150, disablePaymentUriLabel ? 32 : 28, 2); } public static Tuple2 getBigXmrQrCodePane() { return getXmrQrCodePane(250, disablePaymentUriLabel ? 47 : 45, 3); } private static Tuple2 getXmrQrCodePane(int qrCodeSize, int logoSize, int logoBorderWidth) { ImageView qrCodeImageView = new ImageView(); qrCodeImageView.setFitHeight(qrCodeSize); qrCodeImageView.setFitWidth(qrCodeSize); qrCodeImageView.getStyleClass().add("qr-code"); StackPane xmrLogo = GUIUtil.getCurrencyIconWithBorder(Res.getBaseCurrencyCode(), logoSize, logoBorderWidth); StackPane qrCodePane = new StackPane(qrCodeImageView, xmrLogo); qrCodePane.setCursor(Cursor.HAND); qrCodePane.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); return new Tuple2<>(qrCodePane, qrCodeImageView); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/util/ImageUtil.java ================================================ /* * 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 . */ package haveno.desktop.util; import haveno.common.util.Utilities; import haveno.core.locale.Country; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.stage.Screen; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ImageUtil { private static final Logger log = LoggerFactory.getLogger(ImageUtil.class); public static final String REMOVE_ICON = "image-remove"; public static ImageView getImageViewById(String id) { ImageView imageView = new ImageView(); imageView.setId(id); return imageView; } public static Image getApplicationIconImage () { String iconPath; if (Utilities.isOSX()) iconPath = ImageUtil.isRetina() ? "/images/window_icon@2x.png" : "/images/window_icon.png"; else if (Utilities.isWindows()) iconPath = "/images/task_bar_icon_windows.png"; else iconPath = "/images/task_bar_icon_linux.png"; return getImageByUrl(iconPath); } private static Image getImageByUrl(String url) { return new Image(ImageUtil.class.getResourceAsStream(url)); } private static ImageView getImageViewByUrl(String url) { return new ImageView(getImageByUrl(url)); } public static ImageView getCountryIconImageView(Country country) { try { return ImageUtil.getImageViewByUrl("/images/countries/" + country.code.toLowerCase() + ".png"); } catch (Exception e) { log.error("Country icon not found URL = /images/countries/" + country.code.toLowerCase() + ".png / country name = " + country.name); return null; } } public static Image getImageByPath(String imagePath) { return getImageByUrl("/images/" + imagePath); } // determine if this is a MacOS retina display // https://stackoverflow.com/questions/20767708/how-do-you-detect-a-retina-display-in-java#20767802 public static boolean isRetina() { return Screen.getPrimary().getOutputScaleX() > 1.5; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/util/Layout.java ================================================ /* * 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 . */ package haveno.desktop.util; public class Layout { public static final double INITIAL_WINDOW_WIDTH = 1200; public static final double INITIAL_WINDOW_HEIGHT = 710; //740 public static final double MIN_WINDOW_WIDTH = 1020; public static final double MIN_WINDOW_HEIGHT = 620; public static final double MAX_POPUP_HEIGHT = 850; public static final double FIRST_ROW_DISTANCE = 20d; public static final double COMPACT_FIRST_ROW_DISTANCE = 10d; public static final double TWICE_FIRST_ROW_DISTANCE = 20d * 2; public static final double FLOATING_LABEL_DISTANCE = 23d; public static final double GROUP_DISTANCE = 40d; public static final double COMPACT_GROUP_DISTANCE = 30d; public static final double GROUP_DISTANCE_WITHOUT_SEPARATOR = 20d; public static final double FIRST_ROW_AND_GROUP_DISTANCE = GROUP_DISTANCE + FIRST_ROW_DISTANCE; public static final double COMPACT_FIRST_ROW_AND_GROUP_DISTANCE = COMPACT_GROUP_DISTANCE + FIRST_ROW_DISTANCE; public static final double COMPACT_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE = COMPACT_GROUP_DISTANCE + COMPACT_FIRST_ROW_DISTANCE; public static final double COMPACT_FIRST_ROW_AND_GROUP_DISTANCE_WITHOUT_SEPARATOR = GROUP_DISTANCE_WITHOUT_SEPARATOR + COMPACT_FIRST_ROW_DISTANCE; public static final double TWICE_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE = COMPACT_GROUP_DISTANCE + TWICE_FIRST_ROW_DISTANCE; public static final double TWICE_FIRST_ROW_AND_GROUP_DISTANCE = GROUP_DISTANCE + TWICE_FIRST_ROW_DISTANCE; public static final double PADDING_WINDOW = 20d; public static double PADDING = 10d; public static double SPACING_H_BOX = 10d; public static final double SPACING_V_BOX = 5d; public static final double GRID_GAP = 5d; public static final double LIST_ROW_HEIGHT = 34; public static final double ROUNDED_ARC = 20; public static final double FLOATING_ICON_Y = 9; // adjust when .jfx-text-field padding is changed for right icons public static final double DETAILS_WINDOW_WIDTH = 950; public static final double DETAILS_WINDOW_EXTRA_INFO_MAX_HEIGHT = 150; } ================================================ FILE: desktop/src/main/java/haveno/desktop/util/MovingAverageUtils.java ================================================ /* * 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 . */ package haveno.desktop.util; import java.util.ArrayDeque; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.Objects; import java.util.Queue; import java.util.Spliterator; import java.util.function.Consumer; import java.util.stream.Stream; import java.util.stream.StreamSupport; public class MovingAverageUtils { /* With period 2, on an input of [1,2,3,4], should return [Double.NaN, 1.5, 2.5, 3.5]. * * In case of the source stream having too few elements to compute a moving average * (as a function of the provided period), the returned stream will (only) contain * a sequence of (period - 1) NaNs. Otherwise, the resulting stream is prepadded with * these NaNs. See `prependLagCompensation` for details. */ public static Stream simpleMovingAverage(Stream source, int period) { if (period < 1) { throw new IllegalArgumentException("Simple moving average period must be a positive number."); } var windows = SlidingWindowSpliterator.windowed(source, period); Stream averages = windows.map(window -> window .mapToDouble(Number::doubleValue) .summaryStatistics() .getAverage() ); return prependLagCompensation(averages, period); } /* Given a period of for example 3, prepends a sequence of 2 NaNs. * In this way the returned stream has the same length as the input stream, * and the index of a given average matches the index of the last element * of a sequence of data points from which the average was computed, * Provided there were enough data points in the input stream to compute * the moving average (see next paragraph). * * Unfortunately, if there are too little data points to calculate the * moving average, this will return a stream with more elements, that are * all NaNs, than the input stream contained. This is due to the inherent * laziness of streams: we cannot check the relevant streams' sizes * without destroying them, so we cannot make the prepadding adaptive. * The exact number of NaNs returned in this case is `period - 1`. */ private static Stream prependLagCompensation(Stream averages, int period) { var lag = period - 1; var lagCompensation = Collections.nCopies(lag, Double.NaN).stream(); return Stream.concat(lagCompensation, averages); } static class SlidingWindowSpliterator implements Spliterator> { static Stream> windowed(Stream source, int windowSize) { return StreamSupport.stream(new SlidingWindowSpliterator<>(source, windowSize), false); } private final Queue buffer; private final Iterator sourceIterator; private final int windowSize; private SlidingWindowSpliterator(Stream source, int windowSize) { this.buffer = new ArrayDeque<>(windowSize); this.sourceIterator = Objects.requireNonNull(source).iterator(); this.windowSize = windowSize; } @Override public boolean tryAdvance(Consumer> action) { if (windowSize < 1) { return false; } while (sourceIterator.hasNext()) { buffer.add(sourceIterator.next()); if (buffer.size() == windowSize) { action.accept(Arrays.stream((T[]) buffer.toArray(new Object[0]))); buffer.poll(); return true; } } return false; } @Override public Spliterator> trySplit() { return null; } @Override public long estimateSize() { return Long.MAX_VALUE; } @Override public int characteristics() { return ORDERED | NONNULL; } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/util/Transitions.java ================================================ /* * 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 . */ package haveno.desktop.util; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.UserThread; import haveno.core.user.Preferences; import javafx.animation.FadeTransition; import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.Node; import javafx.scene.effect.ColorAdjust; import javafx.scene.effect.GaussianBlur; import javafx.scene.layout.Pane; import javafx.util.Duration; @Singleton public class Transitions { public final static int DEFAULT_DURATION = 400; private final Preferences preferences; private Timeline removeEffectTimeLine; @Inject public Transitions(Preferences preferences) { this.preferences = preferences; } private int getDuration(int duration) { return preferences.isUseAnimations() ? duration : 1; } // Fade public void fadeIn(Node node) { fadeIn(node, DEFAULT_DURATION); } public void fadeIn(Node node, int duration) { FadeTransition fade = new FadeTransition(Duration.millis(getDuration(duration)), node); fade.setFromValue(node.getOpacity()); fade.setToValue(1.0); fade.play(); } public FadeTransition fadeOut(Node node) { return fadeOut(node, DEFAULT_DURATION); } private FadeTransition fadeOut(Node node, int duration) { FadeTransition fade = new FadeTransition(Duration.millis(getDuration(duration)), node); fade.setFromValue(node.getOpacity()); fade.setToValue(0.0); fade.play(); return fade; } public void fadeOutAndRemove(Node node) { fadeOutAndRemove(node, DEFAULT_DURATION); } public void fadeOutAndRemove(Node node, int duration) { fadeOutAndRemove(node, duration, null); } public void fadeOutAndRemove(Node node, int duration, EventHandler handler) { FadeTransition fade = fadeOut(node, getDuration(duration)); fade.setInterpolator(Interpolator.EASE_IN); fade.setOnFinished(actionEvent -> { ((Pane) (node.getParent())).getChildren().remove(node); //Profiler.printMsgWithTime("fadeOutAndRemove"); if (handler != null) handler.handle(actionEvent); }); } // Blur public void blur(Node node) { blur(node, DEFAULT_DURATION, -0.1, false, 45); } public void blur(Node node, int duration, double brightness, boolean removeNode, double blurRadius) { if (removeEffectTimeLine != null) removeEffectTimeLine.stop(); node.setMouseTransparent(true); GaussianBlur blur = new GaussianBlur(0.0); Timeline timeline = new Timeline(); KeyValue kv1 = new KeyValue(blur.radiusProperty(), blurRadius); KeyFrame kf1 = new KeyFrame(Duration.millis(getDuration(duration)), kv1); ColorAdjust darken = new ColorAdjust(); darken.setBrightness(0.0); blur.setInput(darken); KeyValue kv2 = new KeyValue(darken.brightnessProperty(), CssTheme.isDarkTheme() ? brightness * -0.13 : brightness); KeyFrame kf2 = new KeyFrame(Duration.millis(getDuration(duration)), kv2); timeline.getKeyFrames().addAll(kf1, kf2); node.setEffect(blur); if (removeNode) timeline.setOnFinished(actionEvent -> UserThread.execute(() -> ((Pane) (node.getParent())) .getChildren().remove(node))); timeline.play(); } // Darken public void darken(Node node, int duration, boolean removeNode) { blur(node, duration, -0.2, removeNode, 0); } public void removeEffect(Node node) { removeEffect(node, DEFAULT_DURATION); } private void removeEffect(Node node, int duration) { if (node != null) { node.setMouseTransparent(false); removeEffectTimeLine = new Timeline(); GaussianBlur blur = (GaussianBlur) node.getEffect(); if (blur != null) { KeyValue kv1 = new KeyValue(blur.radiusProperty(), 0.0); KeyFrame kf1 = new KeyFrame(Duration.millis(getDuration(duration)), kv1); removeEffectTimeLine.getKeyFrames().add(kf1); ColorAdjust darken = (ColorAdjust) blur.getInput(); KeyValue kv2 = new KeyValue(darken.brightnessProperty(), 0.0); KeyFrame kf2 = new KeyFrame(Duration.millis(getDuration(duration)), kv2); removeEffectTimeLine.getKeyFrames().add(kf2); removeEffectTimeLine.setOnFinished(actionEvent -> { node.setEffect(null); removeEffectTimeLine = null; }); removeEffectTimeLine.play(); } else { node.setEffect(null); removeEffectTimeLine = null; } } } } ================================================ FILE: desktop/src/main/java/haveno/desktop/util/filtering/FilterableListItem.java ================================================ /* * 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 . */ package haveno.desktop.util.filtering; public interface FilterableListItem { boolean match(String filterString); } ================================================ FILE: desktop/src/main/java/haveno/desktop/util/filtering/FilteringUtils.java ================================================ /* * 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 . */ package haveno.desktop.util.filtering; import haveno.core.offer.Offer; import haveno.core.trade.Trade; import org.apache.commons.lang3.StringUtils; public class FilteringUtils { public static boolean match(Offer offer, String filterString) { if (StringUtils.containsIgnoreCase(offer.getId(), filterString)) { return true; } if (StringUtils.containsIgnoreCase(offer.getPaymentMethod().getDisplayString(), filterString)) { return true; } return false; } public static boolean match(Trade trade, String filterString) { if (trade == null) { return false; } if (trade.getMaker().getDepositTxHash() != null && StringUtils.containsIgnoreCase(trade.getMaker().getDepositTxHash(), filterString)) { return true; } if (trade.getTaker().getDepositTxHash() != null && StringUtils.containsIgnoreCase(trade.getTaker().getDepositTxHash(), filterString)) { return true; } if (trade.getPayoutTxId() != null && StringUtils.containsIgnoreCase(trade.getPayoutTxId(), filterString)) { return true; } // match contract boolean isBuyerOnion = false; boolean isSellerOnion = false; boolean matchesBuyersPaymentAccountData = false; boolean matchesSellersPaymentAccountData = false; if (trade.getContract() != null) { isBuyerOnion = StringUtils.containsIgnoreCase(trade.getContract().getBuyerNodeAddress().getFullAddress(), filterString); isSellerOnion = StringUtils.containsIgnoreCase(trade.getContract().getSellerNodeAddress().getFullAddress(), filterString); matchesBuyersPaymentAccountData = trade.getBuyer().getPaymentAccountPayload() != null && StringUtils.containsIgnoreCase(trade.getBuyer().getPaymentAccountPayload().getPaymentDetails(), filterString); matchesSellersPaymentAccountData = trade.getSeller().getPaymentAccountPayload() != null && StringUtils.containsIgnoreCase(trade.getSeller().getPaymentAccountPayload().getPaymentDetails(), filterString); } return isBuyerOnion || isSellerOnion || matchesBuyersPaymentAccountData || matchesSellersPaymentAccountData; } } ================================================ FILE: desktop/src/main/java/haveno/desktop/util/normalization/IBANNormalizer.java ================================================ package haveno.desktop.util.normalization; import javafx.util.StringConverter; public class IBANNormalizer extends StringConverter { @Override public String toString(String s) { return s; } @Override public String fromString(String s) { return s.replaceAll("\\s+", ""); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/util/validation/JFXInputValidator.java ================================================ package haveno.desktop.util.validation; import com.jfoenix.validation.base.ValidatorBase; import haveno.core.util.validation.InputValidator; public class JFXInputValidator extends ValidatorBase { public JFXInputValidator() { super(); } @Override protected void eval() { //Do nothing as validation is handled by current validation logic } public void resetValidation() { message.set(null); hasErrors.set(false); } public void applyErrorMessage(InputValidator.ValidationResult newValue) { applyErrorMessage(newValue.errorMessage); } public void applyErrorMessage(String errorMessage) { message.set(errorMessage); hasErrors.set(true); } } ================================================ FILE: desktop/src/main/java/haveno/desktop/util/validation/PasswordValidator.java ================================================ /* * 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 . */ package haveno.desktop.util.validation; import com.jfoenix.validation.base.ValidatorBase; import haveno.core.locale.Res; import javafx.scene.control.TextInputControl; public final class PasswordValidator extends ValidatorBase { private boolean passwordsMatch = true; @Override protected void eval() { if (srcControl.get() instanceof TextInputControl) { evalTextInputField(); } } private void evalTextInputField() { TextInputControl textField = (TextInputControl) srcControl.get(); String text = textField.getText(); hasErrors.set(false); if (!passwordsMatch) { hasErrors.set(true); message.set(Res.get("password.passwordsDoNotMatch")); } else if (text.length() < 8) { hasErrors.set(true); message.set(Res.get("validation.passwordTooShort")); } else if (text.length() > 50) { hasErrors.set(true); message.set(Res.get("validation.passwordTooLong")); } } public void setPasswordsMatch(boolean isMatch) { this.passwordsMatch = isMatch; } } ================================================ FILE: desktop/src/main/resources/fonts/OFL.txt ================================================ Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL ----------------------------------------------------------- SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ----------------------------------------------------------- PREAMBLE The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. DEFINITIONS "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. "Reserved Font Name" refers to any names specified as such after the copyright statement(s). "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). "Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. PERMISSION & CONDITIONS Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. TERMINATION This license becomes null and void if any of the above conditions are not met. DISCLAIMER THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. ================================================ FILE: desktop/src/main/resources/keys/29CDFD3B.asc ================================================ -----BEGIN PGP PUBLIC KEY BLOCK----- mQINBFl5pBEBEACmse+BgUYi+WLTHR4xDwFE5LyEIT3a5t+lGolO3cVkfw5RI+7g FEpxXzWontiLxDdDi34nr1zXOIEjSgQ7HzdtnFiTRN4tIENCBul4YiCOiyBi5ofN ejAHqmeiO0KsDBQZBdyiK1iWi6yNbpG/rARwHu/Rx5ouT1YX1hV92Qh1bnU+4j4O FcePQRNl+4q/SrtKdm047Ikr/LBvy/WYBYe9BcQGhbHI4DrUOSnIuI/Zq7xLF8QS U/an/d0ftbSBZNX3anDiZjzSmR16seRQtvRO6mehWFNlgLMOGgFeJzPkByTd1TlV K/KaHKQ71FNkRiP87pwkHZI5zJPAQfve+KmYPwOyETUaX43XOuixqutUV6Lrd0ng bKe6q4nZDOWi5a4I3+hkrfzaGOKm9TlZoEmpJHh6pa5ULoDnEpKCg5Dgn3NGwokw 57sDAC2mtf41/uSkR20ALN1q4iOLXiHn+T6Z+Uq7aL3OcKGcBu4xC6Jfofmmdfdd QxEEaeHvAI9ETlKy3tsMhEs5XD6m90rCKLnb97Y8eT/xJL4/oDsxI0o7qICz1nFS 2IhV8xULZ2533vNQPMEbSLoTzgz1OEPYwI1b+YJDFlp1y0XRiEtDZiAFfgsJY7UE DizfuUFsK5LOkw2+NVmLphDVrDW1MXbhX1xspZDmBG9giE08sPtHj/EZHwARAQAB tDNDaHJpc3RvcGggQXR0ZW5lZGVyIDxjaHJpc3RvcGguYXR0ZW5lZGVyQGdtYWls LmNvbT6JAj0EEwEKACcFAll5pBECGwMFCQeGH4AFCwkIBwMFFQoJCAsFFgIDAQAC HgECF4AACgkQzV3BxSnN/Ts46g/+KJL3M6Bfr9K9muNjEeA6yBS5HmZ2tBJI3PiF pJC5MBO5H+3ukI8nyzYil3JhEqCcTUspBGbbkqbwbSQuS19VYBHHxhSAn7B/MHFC FnlbKEzS3cHyp95lGPLJ/y5FXXnSxdlC1EXFcuSjHWR27cCUGuH+1diuyh2eZoT5 fN27g5B5ej0ExXoCp8J4MtMhcHXjGy7Kiv0CbcY8vYEYbqd5GsMvk7UZIa+vWTMz JE1fp6msFfUFzHXYRhO/TKi8iRtVaUUcaOHz7kb226ckVnzIO3CjsGg7y19BYaWf C6Rw0XqPfCf7PoJjhRxbC/9ZWujy/pkaOtOBoq+IZECkiHsKUcZgNdU7xMyCE0a5 jOvJrzKna6MELPczTyeWqZvL0dKNhllw5WJIhzf5mcFqOb1OlNjWxC1BnOeNk51f +FDtjxOyp6P7uL0dPy7j4TA7aHgQNKy2Uvx3+Eu9EHKL2T35xXPvma1ZVybQlMBK z7rbjTIiKTf5LqTtFyE4Kx6IS29rygyJPxz81r4pbjoGUIxLnhxL+6LwxCPwmbkI fFRD+gk8ODmhgY947D6VBPPrrH4U9YiUJZ718b3tCJoubLPrGUfbFlKaGBloK+Ld 0ulJGZrQWxiK3y1KO1AF8k1ge9utJowLAq8rZOUdSPb/cjo3OsspqJR9OQQXNO0n 6WL3Y/a5Ag0EWXmkEQEQAMt06beoYe/vmAWR91y5BUIu1zNmQP2NNAZ1Jh1K3q7a AVEamyVmdF4i2JVF7fTnRGWDiKgjF2f9KJA2mC9v6EK6l7KK/7oQfFgympku8hSL jtp/TWIZZ1D9z16GdqmWaRGdMkqmjf7Wpy26A5TCsUbGvn1tm9P8PxqNfgCv3Cap FhPciK4o/e4gXY7tUbYMC65Dmq3OoJWWzAGqeDmbH4U5BcoZBk+SFyknF/5NWGuz E0yl6TRkgEhzneyBcaV1bmSVcWBpNozoyZC49JggrwFJExd5QQE06iWbx+OkWHYt ObJSKQd3liC1EcAFzI0BoZQ5ZE8VoTXpVQXQcsYtbWKj5BReiEIovi3/+CmjxUFS M7fjeelRwVWeh0/FnD7KxF5LshUDlrc/JIRxI9RYZcbhoXB1UMc/5SX5AT0+a86p Gay7yE0JQGtap1Hi5yf1yDMJr1i89u1LfKXbHb2jMOzyiDYR2kaPO0IDpDJ6kjPc fFAcNt/FpJw5U3mBKy8tHlIMoFd/5hTFBf9Pnrj3bmXx2dSd1Y3l6sQjhceSIALQ I95QfXY57a04mHURO/CCxwzLlKeI1Qp7zT9TiV7oBx85uY2VtrxPdPmPHF0y9Fnh K1Pq2VAN53WHGK9MEuyIV/VxebN7w2tDhVi9SI2UmdGuDdrLlCBhT0UeCYt2jFxF ABEBAAGJAiUEGAEKAA8FAll5pBECGwwFCQeGH4AACgkQzV3BxSnN/TsbkQ//dsg1 fvzYZDv989U/dcvZHWdQHhjRz1+Y2oSmRzsab+lbCMd9nbtHa4CNjc5UxFrZst83 7fBvUPrldNFCA94UOEEORRUJntLdcHhNnPK+pBkLzLcQbtww9nD94B6hqdLND5iW hnKuI7BXFg8uzH3fRrEhxNByfXv1Uyq9aolsbvRjfFsL7n/+02aKuBzIO5VbFedN 0aZ52mA1aooDKD69kppBWXs+sxPkHkpCexJUkr3ekjsH8jk10Over8DNj8QN4ii2 I3/xsRCCvrvcKNfm4LR49KJ+5YUUkOo1xWSwOzWHV9lpn2abMEqqIqnubvENclNi qIbE6vkAaILyubilgxTVbc6SntUarUO/59j2a0c+pDnHgLB799bnh0mAmXXCVO3Z 14GpaH15iaUCgRgxx9uP+lQIj6LtrPOsc5b5J6VLgdxQlDXejKe9PaM6+agtIBmb I24t36ljmRrha2QH90MhyDPrJ/U6ch/ilgTTNRWbfTsALRxzNmnHvj0Y55IsdYg3 bm71QT99x9kNuozo7I4MrGElS+9Pwy31lcY17OSy/K1wqpLCW1exc4SwJRsAImNW QLNcwMx1fIBhPiyuhRVsjoCEda5rO+NYF8U8u/UrXixNXsHGBgaynWO/rI9KFg0f NYeOG8Xnm4CxuWqUu0FDMv6BhkMCTz2X4xcnbtI= =9LRS -----END PGP PUBLIC KEY BLOCK----- ================================================ FILE: desktop/src/main/resources/keys/5BC5ED73.asc ================================================ -----BEGIN PGP PUBLIC KEY BLOCK----- mQINBFLubUkBEAC9dIbgokeCmvyELlpIW56AIgRPsqm5WqxXQyaoKGc2jwWsuHY2 10ekprWficlPS2AC/lV0Mj5rtEgintRYh0Do0gKVaiCL31/L2lPh9WVuLeYQ2Oyv 4p5u7BFHLOu+j3VynLI9MKlr7rT1gDuFLGp8eTfaYnIgFmZ1uTB48YoYw9AAnOpT qtxIYZ81jS7lPkQeeViGEqdJdTDZZUKeKaTnJL+yaq6kSFhUW9I4HPxS/oZGRuFn qefqmDyWypc5bl4CsxLHhhNGI4QrCEHZcQEGwx4Fn8qFXW+47e4KVBZrh0QxIjNJ Rg41DF/oBBsTMXJogVawKIlyQalE+WcKVQtKcUcCBw3cLaHzn/QMYrfQTMhB/3Sk kuN4TCx7HOyM9rFt7y+lz5buPdHlocqbISk6QtbiMCKyb5XwXVcE/MAas/LGE2il zxf7el9Sfey8Yd0t71SAJXrItdygz+iAxoTtnXbjIB/3YzkfSPD4nCAbbHmzx+C6 oV1Xw07usdXLBLQf5jPvKKzjO+xAMHyS7Sf6JJod2ACdJXBEuA2YhK9GNqojfJjI /w0GpV96tAHq3tb30QXZe5NxxIdiw4h5q+VGgIHwpRtNeqx2ngpxY8qHBm5UBYk0 KKX8msoDIwjnVtfcBFkuPiJlxQ48JRmh80vW4ZEZ3Rm2zRv1lsWpx/QhRwARAQAB tBxDaHJpcyBCZWFtcyA8Y2hyaXNAYmVhbXMuaW8+iQI3BBMBAgAiBQJS7m1JAhsD BgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRA9IU+PW8XtcxXHD/dAAY9mw9AT 5LeDFvtkZlK2hec0MPWnoUMBjxTCsaoH+fBCT2Bt53hwkHw4tm0mkxpq3E+zoy2B 1tOHSI7a6AxBREBXEh2OFDU2gDj/H8nR65gw+OAJSCzAH2s1+v30YcajsRZUEbhu wxXWg+e01wKsJZjjcef47Q3N6H6h/H5HrtCz2+s/E3JmDNlD8zR1ueqfG0LjvmD9 MJfjI8sHhRUBoQeLxUirn4oD0++jf3M4JIClZeq1ZHJBxvk+fyty4CTn7ekhqJvl p9z+AF3MgpmHfbvzDUeqSKFVuLUd3KijI4I9vgbv5/EZuXP+punbXKIOFqjCyLpP zToDrjupNIkddwvhNTaapHyxlR5fIRRRgGB2fVDkfsfNz9rIoEM6k7saAxgJrccz Ry193nic4IuyX/LVFVxpX8rSYvVNmbaLPSOre6o4pc+26Etj5uddsLQAxPOdk4m3 rd8lVNtKEHbQ/6IFC2wdH52v4kIc5NNIa3YnmjXzaQ3W0dPaS9VDruQm20+zHubs LIU0kh1O9gSiTsPK3IHAu0Y/usdYES/IwxdyUR+Lue0XTS/NaKvt3BqZ5wnIQRKo X1ka5iUwpmJ6OlI4eqc+3noHQfgNfYrhCR8g9A0FypHctE0pO2UTqCnaCmHuX4Gw +I3Q7IWvpF/mqeRp6eerByt6H3iwvA93uQINBFLubUkBEADRMq7zeNj6dXGCY7JC Uy2YqRvL5N5+AMF2iC4WZ/Lci8iALGcPcsSI8CwdTqGl9SOV/5qqBR3osz50tDoK H+NUjd0sN86kefTVhk9a2TlTKTUmFocqc4sJi2uLl8gBySoyBwucMD1JULvxmdOp i40n/YcIZ/NsUr5MZsLAxNRNbc9SiNhG6Ccq8mURbuwVx+S+qQEqgKAjMAeKeWDa +kFAzfBRi+CoN0yvOF1hDmcXe0zQuShPZU1/KbbSWc0nUcO78b05xK1da5+/iTaU 4GepVYO8o11YiYEV4DgVTTBilFST27vaAe8Re1VBlKlQdSM6tuJAc8IG7FbGyu33 mCzMNfj0niIErZIcFAsrwAeT3ea/d9ckp/xBK51hgRctaNl4Tw9GVudfrVspREGf oUBwOICUhpv51gbuvNWdyUvThYdIGWPGO7NMMCfWFkiJi/UKd5PDcnif1DXnsw4M FnV67AqWDr0neIxz46RjGvPBOERu7uFSrey70V5HA50rTETofr59dblnICDyS7Jn yVM1pLzrKgm+R1LXilrH9+1dmEU/oJlmbY6ikX3IQTUZLnLsP3I/u0V8YbAa3Q4p EqifZscPzw0A65FB1ihAjfj9Ar10LbPIOSbj8rLB2/hCA3TtkXvYxaq7jwOf68Gm 6M8Uh6h0EbVg/MkrAQhlPhtb1QARAQABiQIfBBgBAgAJBQJS7m1JAhsMAAoJED0h T49bxe1zZdoP/0bMLMiOQFg1/64QeI0n8OcNbcVsWh+1NWi7LtTFX3pKuiWhTOiS UJslD9Kwtbe9tqiOXxXoXO/XOPOZfa2hv6D7q9xyv5aGClFY5NXc7pNP3I6CqCh0 6VOy99X2m9H2rYE9RCg4CRt1rIT1Uzespx+kdQgJNBSmwFFT/DvpbPQ+LZBu3izp MK2qZXd2yoe4xv1Oo0dodU/OVgjkgQk38flphDUxOkkOy1meU42Oh6iY4BvuhelD a9eJgtXovWqCGoZErbfQZMgzpZVeHjvLEsOUye0nZlo/hpTjiHYhUJrjZN3Muik5 7BhHLm0MRu1o0kgAhE2Vd3qjKgMjQDnZGmn7bi3pSwdE6qob6B4A6dsN8R589tEN haxPnmjjyM+F4dw/O//Hb2dwOv0386Kv8lNINdY/1S6HRNeh+c4eh6MAd7nf+vWU JZjF6aPmr6Sa0VXVrMdsLo/7RBZxHtRBc8glQPM13hSYeU86a5Qn9AyHwS3fVgcc pKOk2kLJ9XMRuzD70qWItebghB5Yrtp1sL0LMhNYBkAMv73QxoW11fI/6T3fBqAS 1xGI0yMF/tFTIP1TRwJ0uEgK9vOYlS01OM4ajLGfcV/ZWelQDCM2cJXshq/extL1 C3Ba3TvZjzPPWR//c0wkF/4gg/V2A/9Jjam7BVS4JWd/bFRwZ5aR3qux =AWz+ -----END PGP PUBLIC KEY BLOCK----- ================================================ FILE: desktop/src/main/resources/keys/F379A1C6.asc ================================================ -----BEGIN PGP PUBLIC KEY BLOCK----- mQINBFWK4uEBEADjSnRHU294auU1BPH+50OvsWnIvMb6kzqRdY3xlxecRAMsC/Dh XyKVvY+wtC2a/1R+Cj5VO/geEDt0WBbwqj/zAi+x8ttrzZDn5CxmWvU6ulFCFKAr cmB/eZmBMQSJ/JSZw1DeD090/tafuYUDjfhcqE1ajh7WxSIbMudaAm5yd/AuHB3c +mlr5fjBwtBN1nyjfi9N3f7XJS8GrdJFC43/1FWHS3Z+GHydLkIcLS1keT5fYJbe VZGC/RzUJBxqN6UFxIRJhPIplyBFfQBpWIFFxZNr6VZWeQlGnFjX1v3//hmD7mnT 3aGqqkUFcI5q7De3nNm2wfVnV50bzqj+FiSZWUUpWvgD01uzxWxzCVERn8s1jana jLt3hfS8ly5kx311oZTyhXDR5z5LsrOjJv7U+hwhtDHAI0yyD7LPWCYFK2jwljYV Tli8KHchMOlV0Yxm62ebmO/orju4Rq+T4id2nfwJGimRY/DX+k7/1qSHdyjnoYn1 qqpVWD0UhjNLf337PThr20nA/FD3hjwnmIT5becHzrPGbRnr3Y2s77LFUe+nfGE3 wvQmmpSNccFIz/146lynxJHWMfSqOJMgJZWpSPFKd39BhxxP9g5Sou6wEnM+YWYT eOI1dGPejA4EHZec7s3j7hcx33rejydmsjW8yJjkRaFxYJk4jaoT7LgGiQARAQAB tCVNYW5mcmVkIEthcnJlciA8bWFuZnJlZEBiaXRzcXVhcmUuaW8+iQI9BBMBCgAn BQJViuLhAhsDBQkHhh+ABQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJEPW4RDbz eaHGLFIQAM/w8BBoZ7+K4hxZmjkt/lXDXddHg9jvfU3jgIR+CkThcHy7n1e+QvsG k0FozYsFyCaLwOiGR0/eUcUu+aegwRnx+eh/scElcAN40RYIr2nCU5vNGqmKBrP2 ShQu0z5CcqFoccIHZ7VSe42SYb3GJ8g2mtC1Um+ryytZtF0g7nJxGWe//4YmqavC TV4KU5akJGfFVPDW84qJJo34gwg80oshxnOQnXfoa+xlmSDQMaOTYH07cR9N64JW TUad4aTl7niFZPizzg9r4ltRwUzvyXD5CHQoKqGWvZO0pHvRRq5SHp5nDoKh5hZs lb/QJu6X8bTlLwhpOLsXPBPqoXQibRiICfAdVPBYnHMtvJ7RcuZyazvpYYjlgYsK kol+jUude4zAIEky1A/77wl1pBXURw8vk8CRPIqAAOniaTySk6b24fseYsMcmP1Z VLt4HxV0njBRAe51DV8AiT/gscTdg8/GsJRjzKedCs0SZIjBg5/1iULXRtQZrUd7 vkOZZCSRzMAf4iHGK0qFuUoEkoZacv+bfOfwse62F89ngM1iB9RdyPPZIuh70i8O Ebzzs6TBptq9WtV5LEXtkkHyfCugoIKegdKZmZBxnNT+XZMQQ68E8jeTiUA8MaPR hYJp2FL+DLsTcLHt+ffHmcYJ1+6/v8UMIx6wC2k862+h4Y1aBme9iQEiBBABCgAM BQJVwoK3BYMHhh+AAAoJEAxMpdRX1mvaFmYH/jx1ayv+6zNlYsFaL3idIBCmWQb7 Lu4qE8bhSGN557jc7HoYT1DAYubm4zV65KxMVs+AsrNoy9Q4mXxpzIsi3X/J/3qF L7hF+ZiZf8ms0FNScFW4rrWJpWaZ03zf97hx7D08oBxMtn++hA0/Ur7YN6fLtOe4 sv19D8U6UvjT1LsDIpDXDUuLNTAcpq5liOGL9PHa3przvekuVIROgosGbdfY34KO v9PfyL2H7Q6np7awjBE3GsbIMcEp+JsFgE8M3GJzke/absuqeNHpCgpIWPMoWCXb guKOsVipIBuRNhaRG4hRFAQAUYRe2UL9ZH6BBKfAZoOkgYP+Kv01XGhADXGJAiIE EAEKAAwFAlXCgr8FgweGH4AACgkQQBJQlmprLEaYSw//ccfvWZGxvi4R0EHM61pD +Jp0iTdMb+8L1lK6+pzVaQvPf06UWD9qjN79cWDI0HmVFVgFPE0qRbsIi33s7ltF Gc3Dw7ql2R7q1XS06pkT8ihesdYgauNA0802js5/RJK3joZEujNAQkz33O4daECf MWEVia0JrFZktUlwVTjKOzKyoBlUiV/Rg/ivnTRVXyfDIp7qCUHcIhz19U4zK0kh NKkgVxddKIeyivmUghzQbYEkAZlvBfLRXvnnK1TdouOgLOvHetf7LQDKpgHxJmre o8XjHrF08/mDfKRvqh8Vi/j5zj8Kyy7LIjF3rHzCJDjwDp4tgSDEekMgEzYLoiyn /y6lCxS78m+/EkCdE2Hncb81n6fgldQTSCChpfUbvqQAuewb9wonQp3gtqIdEwg1 WS4T78m1qNfFP9I3UYKWRdSplifJAhr2NAyaf9fSNVSGRZk5sDcaXRrraPj5DHR6 kgDITkv1ph6sB+4cu/6XmoZ8ZWAmmQFz4/EejlBu4khZHVPCojtGmbyOAAmV8M+a 4zWPXxdtOlKUCZpa7jOPkto4V804K5FsOn3qascdLdd/VYtjPWE/qoWs96/J81w5 SJIXZ0s3j/EWwRKtcxZO22x0IXDIA5oPY7gQC1JDT/dt1blbGQ9nCLIPL5QoxxAF noxMOtoVHlC98rjnPgCtuACJASIEEAEKAAwFAlXCgsYFgweGH4AACgkQeiIM+js9 3FtGugf+JPu6S1RNVbgv8n1cX9Krt3JnXi0ybzhlCILxe8lRqzk9OXsVzY1Zvnor 0L35jYa2Y8GSEgivKEZXcdJroCXmBJRWs3ck3SSevxmqDm1+nQ96TBFtno6m8hf4 3UoT0YnuryGffV0XEyO/m7ujIj5iF8UvWC6d+ve/oQw815IJROZNBjgRn6bhpgnq sWVWioSQg9Jzqs/h8rjFWrscbln/mBCxyn6PsjIO6N1ArBcB9s95iCxiz6MXiMrl vGbd6KIsaaG6H9IXfCFcOXkN+1pfr6439LRZMxC1hqHEqLWPV8iCwPyFJkHJUeyg 8hYuIFeEIvG4Z9ukEKrd27sh7eCgN4kCHAQQAQgABgUCVulMPQAKCRA97MEF9d0j ghURD/4p+kGPeqQZq4sq4v7wxYPLnihTIdD1rZXOtWa3wnVOf5o03MGpXaaQIyez LRgF1FSgkAV0v1kOJcUVOwZNXfivFAz5b5dV5cX8X/8AFc798gOQ2BpKDs8Gh4Vh V+aV9Zslac0QLKA8LKmJOlVCb+GpQKmwPZ8IFfr+NtMhRW/h1WSualLHYpmmfH0A GCnDM00w1pgGavtcTjIrvihA0uw4ySFT6QzI1z+1zmZlPsdpZEbpAeTyrGecDIRj FAOTsmbe2YOk6kzj3xhwL8hMjtfX4EZl9KW1bGR6/fy8fVaM8lHi0Pa4BgIeca+M ir+kHw7G1FgHrjiqOUuvCuK9uln1We0DttIi7RB1lYmX+Ds7XfSKj/OcrHWwxtJk phKofIyGt9b12tqjJKdyS+81FBjgsiUSGQJdThm2vefVKKMqc91OlGPg8q3804x2 3BlOHg98pz3TjOmrARSzhpGLz1KfS8o3YQYQ1HqymS1zyjuim5V+pf1s7bFg6RE9 d0ipnTwEXmXIuU8fu09DAm6Z4o9XP2RP49cCieDOQ0dp2YKIzae6RkZjCjUPujiI pZeN8rCageX6G0iuwKMOLfNn/g5ItHOm72U28aV6hMzTmHvdB+eaKl304N1fzjya +FSncOcO9SazYGEKjs46ec48k389lXZ7nMuZDMwPZgsDyzZWn4kCHAQSAQoABgUC WDhcJAAKCRDAwHYTL/p2lSfUD/sGJhNJiIrGYf9qw8qBQJyaQDoNBFHLvl2tUpOf +TVojDywJ+51askcL3L0hldbUg4wziPi2FV8AtRyerDKNcMUJYn6SQ3Rhx/7eFP/ vnUqJ7f8ZJEk7LzGYDZGQpnSe/eyXNARVjoPUFhjl6mTLtKPZWfaprs2e+yvQimy 2hgsiWOvc19ifsRg6KVSSTBqUS+FCSw0VRR1wt5cmrFRkuRfGoCHHd8mXkI1qSit xfFQxyURxHWxLkWnwN9y0G8cYvSOI/hgmfY/MYY6NRWKbmzXIve5n7qFKNFBR3n4 NA9oJwI2Mzop8GuwSU54QlmiG6N0Elqt7c+aU1bOGt3dWJS8e5J7VBZqoFrIzPAC DObdSkU3Y04ZQ5LcnAn0n6dpZRTX2Fv0Tcxv7MCEfQCDCeBs9xDrXIcEZLNyrBQE kcXbL4EUBjsq80fLV5/a5iyhS67pJc10mS5T8pkFd7hA6eTesRRbP2Do1ndiZCPw E2gugDmz6hjTAUwG6iLu2rwJ2aOOm3V3PmYZ/JM45zGTjKFb2sEzkuOG1YdHIt30 FXqEswItLMWQl5xTwuHId9mPvgKLz9h5ZYt8ML5G+QXFEVnKiU0pFWabDgpb81cS 0aAYQcSOUG5ObyBjHsZXwQKNpe9oEFN6xrbE7dp4FxtZpZXExLE+PUffxjOyGw0W Lj+WYokCPQQTAQoAJwIbAwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAUCWU795AUJ BaVOgwAKCRD1uEQ283mhxrjVD/41bwb1y38w1K3dlOavw6t2RwwvmBgNDlhLFrM1 ZK3Kjk+p9s2/8GoeGdPiVgrqv3okI+Ztme+R+jtWRPSozczfZyIdgR2/jdhS8P0L IUIbQlkn7cvvDb4Wf8lAUhnGF/a+Gmnpn+Ju65KcTxFBGSSt5q2iQVbsW+krhyoy nuD6C/2QLDKH+YPOahihmrTpLQkJ4IwdK+0LfoWqcgNB5JiRKd4fcgXEYTxxBMSc 5QwlRkU638PTkjGaBbb7I+RxWrk3Y75SyyFbD/svJm4JxQGFQCvPOiesSTQrQuCV opoZj0YKfZzUpgiVYQFm1MCLLhWs9nDxJ0d2lxropUTm+8BYuuy/pSki60GGbKv6 MnWUhExmde01U3wjxkHeXX9u2qswL/spORVtqtxDvWQeUyZyhIs1Slled+7RLOww bJNamKGVdBcN3XZwaxNeuBX1nppjKKeleS56C0BFuTVEptEsdRj62FVJIli1MH43 IxAN0iJUsO2XSljhmixQu37jfkLW4HlCLiZwYLCJDoXFtHZZ5nwURGeBGSeGyWzC tx7DJvXDEx/GWMJzU500X4iTc5gcvLLsTm5bxKthOITITHgvXXAMmc0YpLmmueZA UNMShQsxkzm3QOBCVgjW532OQHM66Plsact4hCYJ+p0GSQGcUgKmNGcfQKmLNhvR oT2NlYkCIgQTAQoADAUCWF7kZAWDB4YfgAAKCRBAElCWamssRtBxD/0RK9Rk9eCg jj61Vk8rA/Uvmz7ZEwHlunL6pucvy2RZL+ztMNSlLPYcvtByvSvUo8Q9G/YnjR6l EGMi5DERN5Euc2nMIlg82EWQyd3MEAcBxcqriLuKrybizce1o8pUExV84DJYchr/ A9ei93GiHbNodMxv5zt+4pu/e540DxATf9ME6EpJtbzJcUwsGUrPOtC9Xp13t1sL 4sHL1z3TVPOzOQ8HSlfMOUdYNoJg89MjTnX//rOpfSIq3FcEVURipOgLKDhiQEmQ tpeknv7uZDZuRLxK/J7IebmnbnmV3wq3l5LVCLRTzpCXtucTwnKoRYBcbT676F70 GbM7QPwdiyiq17lx3+YHElHm0KGfxn6iUSPGtEJv6RcO8glU/VIN6/N8HxH5Y9HE 28+eC094GnKD3xQtMzSrzTkp7q5NGZPBS4wT3mGwem4pfjuYbkWea3+71jnr6SHW ciGWo4kXuVp9Va1ZFuNxk8o0G8YdHV30oEXJwTyyxXFMdePraz95B4fjtJdpgsnq JLsFIIgdpECBeiyqy83pOCv5aoRiqz8xAYrkZWYKtw1xdMZ25LoyI/OgdmCIjrr1 q+VGPXi2CcB2Y4XmJCUCYUh1W7eGoXlOEWA3upONa+RkrOZ0bEQ/VuHqvWeVol7i uWqdxi9IrjPK2hhrMPMKeNphpjLLitfWM4kBIgQTAQoADAUCWF7kdQWDB4YfgAAK CRAMTKXUV9Zr2pLpB/9K4hu0ELE9D5ZvsFlD6lQUNvonuiZGX/xBsejYL/rEPWSv 2dJpb815rPkJtoqUZ5cem2sJhOKc+ZIKHZy0hiobqbePZXArT7dh5aIfMYfFfvYE YOZYUD9dbMjzjqHrpfUvht6IxekYfkm+XKYL1PGdxGZ4AiK9ZoRVnM/eK2p6+qcG VEPkHJXJKNRMvPJTniKsW3vryqM0J0Z6wDqH9IEWIhus1GeEcm/j7Hw0OO5gku2/ uI8SndS6dtCjruGcVLpG0quMdCFCsp/jtH1y1opFDT3cW7g7q3RQxw0dNzflraaz vQm+caO8QtZGVS6vegaOf2a4hfACKtt9EzVVLq93iQIiBBMBCgAMBQJYXuR9BYMH hh+AAAoJEMM+Vu/rhndFAXwP/2bAkgdM2CZ/WRXRAecBCCIz483+bS4yXaraSeJz bWl8Cp3aVMqRyLGcDo0UhYRTzDfgcX2YLlK0pBwAnvNd2hi8cqQC1RWOdYrgUMzN /StI9NOImov0n8kWqK+kqdxJIwz2Rs3crlwpDR5bosTzG/HwwxtNGB1h1T72RrGf ISOrqtRgHnAod9DBluONJaUv1QN95zHCVQqh0deAZLYtOlPhUbXYgfZQGlufUBpt SJAsA2W5yuN+Qiav7TetqQDN/zapPIxGSkfuN+t22ek65OyAbEHQP7Z1ltI/+PEM DaqU+Pzb9UJtZEpcHDCTB4YbI9+H6k/WAp6w2Cm3nNEJiaYAr8XocxxTMnjlVtnr OE/4++wuQE8EvcuvnxXkhyLbZnhAdpv1lUgzll3pAhe9w4RCpX4tz5JHB5EoQXAP LHIQRzWc8TzE4H+aUdSwrewQL3qxrgadJDST2Em/DFIj8EftwbJCLpm8jtp9fw7U gP4iv8hYhUBCRH24n5h8YwbwCOOGadLNfoUh6DHHa7ZdktOADQfcmOhA840FoL+v t85+sVCgGaBsoPkgS/ZvE5KRbtugiMuCO5OS3lIpzDjaw+2nH1Wq2uuuBya3owyJ GIxKtLNxg/pZhwnSsVwi3sDmmXLq6WDTCAfeR+8+PKgC6IgDUAYalpoxEq7grVi9 e/yiiQEiBBMBCgAMBQJYXuSFBYMHhh+AAAoJEHoiDPo7PdxbfaEH/AshsMxZBRt3 f5f4JrTQtk8veAqwKzHNUMbl9sQzBjEvg9V7SaoGp1cKPb1e9Sy1VDD496dBYgxc rUOG7Wp3XD/Gccht2a/EL+CJaJHyVzfskLTtFtnrMItLCR7uuqQEvK/Q2IxJpaMt wNKEpzDCgKzFXUjet/gB2j90dMWqPVDh/GRiktrcDVXaX3roMBenWmzeBpT8oUA2 Tw8gUr696Y7d2RgDHncTlm/qS5w1QyLT5CIXd1Os6+eA8MIjFUSKaDxNM9zCzGvv hkj7bBfi1xxmPonogGKd56tgBFZ7EGeFD/TGLjCtJLYz8pPP/F2az557xFJ41aVt Cq0wTtHSrsGJAiIEEwEKAAwFAlhe/hQFgweGH4AACgkQNmQEJH0guzLtoRAAvA1V ZPyE/RoTjTkZ0468R7txGSNiQBMHeKQzSj5vrFvFjuQOx1pKvPbBa//pfddmXsyN 4+fXkm+i3jhiww85VmfP+jaPZE7ha3gI1sLIXywBUEQXGtN+JrGdIfx4fm9Xj+Km a41o77XxnYxg/puqtoxXuFQfF+KcX3SgCzaGnhn+p2YfUtIgqaVkQl6H6vhKley8 pZcB58O9Eu0RbpGg/FWovOWY/Jg8DdbOpQmrp4tXD116rt8m0jEJcWk/DPexehHn Znt4Xi/oogBiccRDd/ebUeyjUkkrPk+IQjdYYOuN0i0nMUL9KsWLJwUHNa2IWv1e xgVg9dWuPk13K2hJFzdvGa19IVsBOEEXgfIyC2ZSqz0zFhAQQ/2saRDvITgQS10W duL55lv78YevjqeETEHW2DeXkzUiRwe64BUuu/9LFsSLuwCwLrvz3Yyh0T21MAAA /5sHsai4hRhxAhVoWfelKShzmZdh7bdqrxDrivutdcOn9Evdw3IQ9rsDtgyDrvmm Mok1eSYvZF61yhHvdVU6wQOET7u3T7eSFoAW7EknuAd4rSIZ2AqBchARGEbz3m7w aidB1KmedzlGNk0DEWcXiqpdgQdvalzxfJJSIOsJic7FH+p2xBnFYBVdS/ftgrC2 kuPY4dpfVviNxGLrRDd8fYfdVDolMW1pOWo7oQq5Ag0EVYri4QEQAOtygi1rXfDl /H18Evad7dz96ZFDGSQNoD9eC4UCGD5F2AqEil7pTNapIDqcGaz1MZl5k4B9CjH7 mutQukLXcHtdrc5eXYjMQZ/jVFjjv5j3fPgwWrz6LfxYD/jxw7uTgDHlgEo/Dv8D WMeE3wcycKhlG9KT/qdx+1b36ds7ecYeooYIxHSCAbQl+4mKjn4HNIhAGTcNe7i9 79rGApBNJgpSYnaqK7i3CFvIeMRWLQKk41s4sBrwZI+hEFnlZoJ3Le7Mh/0emcfs ZCk4YNwdfGiZWoic8ZMudx0JUkso/ELRxzx/bgNls+vpQb3SQ1zuFZ8xnOunEmaf DYbg/hJguAT3fnvGqqeO0305+OVflxcoUyxXDxLtY+4t6SEj2v3L9t+ZpbQg12+d lr3Eel+NltXibv2yVhwP0NpQq+CJ+nPDQWsCcK/FelP2ik1EZqasQPZZFBORKXNV JCmWXm+8GNbwN9wvVR9rmwh0h8v9RAbh7Q4inYnxiVVKIH14ZGQp8i3NW/k5sOuk RqM203tEV4LGCP+bwswcwPCmvfid3L8oQmPA7ezL6rmlehe7ctP4iHEX/xxGbRzD ZWNdNZrTdq0h1WR6ce7Ya52VNN1dBoqkbZmzQxD+NC/3dv/yl8MfnEeJDdvQCGQ2 0zCbGfXWc2T9ov4BK/a05cDBlXaIpH/fABEBAAGJAiUEGAEKAA8FAlWK4uECGwwF CQeGH4AACgkQ9bhENvN5ocZGxA/+I+GLTTFaHRy6ZNmAr6uEPQ59yXOE5k2ZrML7 F2nnIR0FJFydhnLSsxCt89zXxmxk4kA4h+M5jmyB4HiIGp0u0lC/zpklJwJ8+EKj KpSaL9zdo1hwojybGar78mF4qsQ2EZP0TIq41gOZ/qx7dVaDSu75cuQvgGakEQcx 89B5RGaZRKLlE68Mo2QXktNENnPFkkOPBoil8KX34DHIWJafncwu0vObcE31ifIZ j9j3FoeupnIW4HXEbsZBWkM0k/Fzx3wdvvYuEwR0JvihSJ4YEncB33weZ+u1+XTa cAWt98oubYMoR+M2d4+EAmOJVjz0oGXNvs/BBwSCem3c/oSt43R3lc7zMU8shZf8 bKS+TGYnV/kRWcNc2l0BTiRRUwFZ0/XvAcNXJsB1CyrvbWvrZiDIm6tA3xOJzFGY wLNTM1BqfNfrPbzov67vkkbxxRlTRx1x6LTFPV0H1FTZ5CSQgahjm9SwANb0jyU7 xR9hL3zBvKr7quR7mM1zzjnoGkNMdVsM02fBrmqfhABychMFMVVOWhyLLQO47YZB ghu/JigFHreRBbTOPLcCSfkH24EL91nDnfLp6KHLcz2DfU2W1lajwRfDm2rpbKx+ 6iAnmNBJV49ZaM7lFqPaJz942mVySd+4rygkuF1olWxN1EbzK0/bKRuzljIj5U+r vUTpzlk= =ZIr3 -----END PGP PUBLIC KEY BLOCK----- ================================================ FILE: desktop/src/main/resources/logback.xml ================================================ %hl2(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{40}: %msg %xEx%n) ================================================ FILE: desktop/src/test/java/haveno/desktop/AwesomeFontDemo.java ================================================ /* * 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 . */ package haveno.desktop; import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.layout.FlowPane; import javafx.stage.Stage; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class AwesomeFontDemo extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) { FlowPane flowPane = new FlowPane(); flowPane.setStyle("-fx-background-color: #ddd;"); flowPane.setHgap(2); flowPane.setVgap(2); List values = new ArrayList<>(Arrays.asList(AwesomeIcon.values())); values.sort((o1, o2) -> o1.name().compareTo(o2.name())); for (AwesomeIcon icon : values) { Label label = new Label(); Button button = new Button(icon.name(), label); button.setStyle("-fx-background-color: #fff;"); AwesomeDude.setIcon(label, icon, "12"); flowPane.getChildren().add(button); } primaryStage.setScene(new Scene(flowPane, 1200, 950)); primaryStage.show(); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/AwesomeFontDemoLauncher.java ================================================ package haveno.desktop; import javafx.application.Application; public class AwesomeFontDemoLauncher { public static void main(String[] args) { Application.launch(AwesomeFontDemo.class); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/BindingTest.java ================================================ /* * 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 . */ package haveno.desktop; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import javafx.application.Application; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.layout.VBox; import javafx.stage.Stage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class BindingTest extends Application { private static final Logger log = LoggerFactory.getLogger(BindingTest.class); private static int counter = 0; public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) { VBox root = new VBox(); root.setSpacing(20); Label label = new AutoTooltipLabel(); StringProperty txt = new SimpleStringProperty(); txt.set("-"); label.textProperty().bind(txt); Button button = new AutoTooltipButton("count up"); button.setOnAction(e -> txt.set("counter " + counter++)); root.getChildren().addAll(label, button); primaryStage.setScene(new Scene(root, 400, 400)); primaryStage.show(); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/ComponentsDemo.java ================================================ package haveno.desktop; import com.jfoenix.controls.JFXBadge; import com.jfoenix.controls.JFXSnackbar; import haveno.common.util.Tuple3; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.GlobalSettings; import haveno.core.locale.Res; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.FundsTextField; import haveno.desktop.components.InfoInputTextField; import haveno.desktop.components.InputTextField; import haveno.desktop.components.TitledGroupBg; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.Layout; import javafx.application.Application; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ScrollPane; import javafx.scene.control.ToggleButton; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; import javafx.stage.Stage; import java.util.Locale; import static haveno.desktop.util.FormBuilder.addFundsTextfield; import static haveno.desktop.util.FormBuilder.addTopLabelInputTextFieldSlideToggleButton; public class ComponentsDemo extends Application { private JFXSnackbar bar; public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) throws Exception { final CryptoCurrency xmr = new CryptoCurrency("XMR", "monero"); GlobalSettings.setDefaultTradeCurrency(xmr); GlobalSettings.setLocale(Locale.US); Res.setup(); StackPane stackPane = new StackPane(); //MainView.rootContainer = stackPane; bar = new JFXSnackbar(stackPane); bar.setPrefWidth(450); GridPane gridPane = new GridPane(); gridPane.getStyleClass().add("content-pane"); gridPane.setVgap(20); int rowIndex = 0; final TitledGroupBg helloGroup = FormBuilder.addTitledGroupBg(gridPane, rowIndex++, 4, "Hello Group", 20); final Button button = FormBuilder.addButton(gridPane, rowIndex++, "Hello World"); button.setDisable(true); button.getStyleClass().add("action-button"); Label label = new Label("PORTFOLIO"); label.setStyle("-fx-background-color: green"); final JFXBadge jfxBadge = new JFXBadge(label); jfxBadge.setPosition(Pos.TOP_RIGHT); jfxBadge.setPrefHeight(100); jfxBadge.setMaxWidth(110); final Button buttonEnabled = FormBuilder.addButton(gridPane, rowIndex++, "Hello World"); buttonEnabled.setOnMouseClicked((click) -> { //bar.enqueue(new JFXSnackbar.SnackbarEvent(Res.get("notification.walletUpdate.msg", "0.345 XMR"), "CLOSE", 3000, true, b -> bar.close())); // new Popup<>().headLine(Res.get("popup.roundedFiatValues.headline")) // .message(Res.get("popup.roundedFiatValues.msg", "XMR")) // .show(); // new Notification().headLine(Res.get("notification.tradeCompleted.headline")) // .notification(Res.get("notification.tradeCompleted.msg")) // .autoClose() // .show(); jfxBadge.refreshBadge(); }); buttonEnabled.getStyleClass().add("action-button"); InputTextField inputTextField = FormBuilder.addInputTextField(gridPane, rowIndex++, "Enter something title"); inputTextField.setLabelFloat(true); inputTextField.getStyleClass().add("label-float"); inputTextField.setText("Hello"); inputTextField.setPromptText("Enter something"); final Tuple3 editableValueBox = FormBuilder.getEditableValueBoxWithInfo("Please Enter!"); final HBox box = editableValueBox.first; // box.setMaxWidth(243); //box.setMaxWidth(200); editableValueBox.third.setText("BTC"); editableValueBox.second.setContentForInfoPopOver(new Label("Hello World!")); GridPane.setRowIndex(box, rowIndex++); GridPane.setColumnIndex(box, 1); gridPane.getChildren().add(box); final FundsTextField fundsTextField = addFundsTextfield(gridPane, rowIndex++, "Total Needed", Layout.FIRST_ROW_AND_GROUP_DISTANCE); //fundsTextField.setText("Hello World"); final ComboBox comboBox = FormBuilder.addTopLabelComboBox(gridPane, rowIndex++, "Numbers", "Select currency", 0).second; ObservableList list = FXCollections.observableArrayList(); list.addAll("EUR", "USD", "GBP"); comboBox.setItems(list); /*comboBox.setButtonCell(new ListCell<>() { @Override protected void updateItem(String item, boolean empty) { super.updateItem(item, empty); this.setVisible(item != null || !empty); if (item != null && !empty) { AnchorPane pane = new AnchorPane(); Label currency = new AutoTooltipLabel(item + " - US Dollar"); currency.getStyleClass().add("currency-label-selected"); AnchorPane.setLeftAnchor(currency, 0.0); Label numberOfOffers = new AutoTooltipLabel("21 offers"); numberOfOffers.getStyleClass().add("offer-label-small"); AnchorPane.setRightAnchor(numberOfOffers, 0.0); AnchorPane.setBottomAnchor(numberOfOffers, 0.0); pane.getChildren().addAll(currency, numberOfOffers); setGraphic(pane); setText(""); } else { setGraphic(null); setText(""); } } });*/ comboBox.setCellFactory(p -> new ListCell<>() { @Override protected void updateItem(String item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { HBox box = new HBox(); box.setSpacing(20); Label currencyType = new AutoTooltipLabel("Crypto"); currencyType.getStyleClass().add("currency-label-small"); Label currency = new AutoTooltipLabel(item); currency.getStyleClass().add("currency-label"); Label offers = new AutoTooltipLabel("Euro (21 offers)"); offers.getStyleClass().add("currency-label"); box.getChildren().addAll(currencyType, currency, offers); setGraphic(box); } else { setGraphic(null); } } }); Label xLabel = new Label(); // xLabel.getStyleClass().add("opaque-icon"); //final Text icon = getIconForLabel(MaterialDesignIcon.CLOSE, "2em", xLabel); //final Text icon = getIconForLabel(MaterialDesignIcon.CLOSE, "2em", xLabel); /*icon.getStyleClass().add("opaque-icon"); GridPane.setRowIndex(xLabel, rowIndex++); GridPane.setColumnIndex(xLabel, 0); gridPane.getChildren().add(xLabel);*/ // FlowPane iconsPane = new FlowPane(3,3); // for (MaterialDesignIcon ic : MaterialDesignIcon.values()) { // iconsPane.getChildren().add(new MaterialDesignIconView(ic, "3em")); // } // // GridPane.setRowIndex(iconsPane, rowIndex++); // gridPane.getChildren().add(iconsPane); jfxBadge.setText("2"); // jfxBadge.setEnabled(false); GridPane.setRowIndex(jfxBadge, rowIndex++); GridPane.setColumnIndex(jfxBadge, 0); gridPane.getChildren().add(jfxBadge); Tuple3 tuple = addTopLabelInputTextFieldSlideToggleButton(gridPane, ++rowIndex, Res.get("setting.preferences.txFee"), Res.get("setting.preferences.useCustomValue")); tuple.second.setDisable(true); FormBuilder.addInputTextField(gridPane, ++rowIndex, Res.get("setting.preferences.deviation")); final ScrollPane scrollPane = new ScrollPane(); scrollPane.setContent(gridPane); stackPane.getChildren().add(scrollPane); Scene scene = new Scene(stackPane, 1000, 650); scene.getStylesheets().setAll( "/haveno/desktop/haveno.css", "/haveno/desktop/images.css"); primaryStage.setScene(scene); primaryStage.show(); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/ComponentsDemoLauncher.java ================================================ package haveno.desktop; import javafx.application.Application; public class ComponentsDemoLauncher { public static void main(String[] args) { Application.launch(ComponentsDemo.class); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/GuiceSetupTest.java ================================================ package haveno.desktop; import com.google.inject.Guice; import com.google.inject.Injector; import haveno.common.ClockWatcher; import haveno.common.config.Config; import haveno.common.crypto.KeyRing; import haveno.common.crypto.KeyStorage; import haveno.common.file.CorruptedStorageFileHandler; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.network.NetworkProtoResolver; import haveno.common.proto.persistable.PersistenceProtoResolver; import haveno.core.app.AvoidStandbyModeService; import haveno.core.app.P2PNetworkSetup; import haveno.core.app.TorSetup; import haveno.core.app.WalletAppSetup; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.network.p2p.seed.DefaultSeedNodeRepository; import haveno.core.notifications.MobileMessageEncryption; import haveno.core.notifications.MobileModel; import haveno.core.notifications.MobileNotificationService; import haveno.core.notifications.MobileNotificationValidator; import haveno.core.notifications.alerts.MyOfferTakenEvents; import haveno.core.notifications.alerts.TradeEvents; import haveno.core.notifications.alerts.market.MarketAlerts; import haveno.core.notifications.alerts.price.PriceAlert; import haveno.core.payment.ChargeBackRisk; import haveno.core.payment.TradeLimits; import haveno.core.proto.network.CoreNetworkProtoResolver; import haveno.core.proto.persistable.CorePersistenceProtoResolver; import haveno.core.support.dispute.arbitration.ArbitrationDisputeListService; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorService; import haveno.core.support.dispute.mediation.MediationDisputeListService; import haveno.core.support.dispute.mediation.MediationManager; import haveno.core.support.dispute.mediation.mediator.MediatorManager; import haveno.core.support.dispute.mediation.mediator.MediatorService; import haveno.core.support.traderchat.TraderChatManager; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.desktop.app.HavenoAppModule; import haveno.desktop.common.view.CachingViewLoader; import haveno.desktop.common.view.ViewLoader; import haveno.desktop.common.view.guice.InjectorViewFactory; import haveno.desktop.main.funds.transactions.DisplayedTransactionsFactory; import haveno.desktop.main.funds.transactions.TradableRepository; import haveno.desktop.main.funds.transactions.TransactionAwareTradableFactory; import haveno.desktop.main.funds.transactions.TransactionListItemFactory; import haveno.desktop.main.offer.offerbook.OfferBook; import haveno.desktop.main.overlays.notifications.NotificationCenter; import haveno.desktop.main.overlays.windows.TorNetworkSettingsWindow; import haveno.desktop.main.presentation.MarketPricePresentation; import haveno.desktop.util.Transitions; import haveno.network.p2p.network.BridgeAddressProvider; import haveno.network.p2p.seed.SeedNodeRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; public class GuiceSetupTest { private Injector injector; @BeforeEach public void setUp() { Res.setup(); CurrencyUtil.setup(); injector = Guice.createInjector(new HavenoAppModule(new Config())); } @Test public void testGuiceSetup() { injector.getInstance(AvoidStandbyModeService.class); // desktop module assertSingleton(OfferBook.class); assertSingleton(CachingViewLoader.class); assertSingleton(Navigation.class); assertSingleton(InjectorViewFactory.class); assertSingleton(NotificationCenter.class); assertSingleton(TorNetworkSettingsWindow.class); assertSingleton(MarketPricePresentation.class); assertSingleton(ViewLoader.class); assertSingleton(Transitions.class); assertSingleton(TradableRepository.class); assertSingleton(TransactionListItemFactory.class); assertSingleton(TransactionAwareTradableFactory.class); assertSingleton(DisplayedTransactionsFactory.class); // core module // assertSingleton(HavenoSetup.class); // this is a can of worms // assertSingleton(DisputeMsgEvents.class); assertSingleton(TorSetup.class); assertSingleton(P2PNetworkSetup.class); assertSingleton(WalletAppSetup.class); assertSingleton(TradeLimits.class); assertSingleton(KeyStorage.class); assertSingleton(KeyRing.class); assertSingleton(User.class); assertSingleton(ClockWatcher.class); assertSingleton(Preferences.class); assertSingleton(BridgeAddressProvider.class); assertSingleton(CorruptedStorageFileHandler.class); assertSingleton(AvoidStandbyModeService.class); assertSingleton(DefaultSeedNodeRepository.class); assertSingleton(SeedNodeRepository.class); assertTrue(injector.getInstance(SeedNodeRepository.class) instanceof DefaultSeedNodeRepository); assertSingleton(CoreNetworkProtoResolver.class); assertSingleton(NetworkProtoResolver.class); assertTrue(injector.getInstance(NetworkProtoResolver.class) instanceof CoreNetworkProtoResolver); assertSingleton(PersistenceProtoResolver.class); assertSingleton(CorePersistenceProtoResolver.class); assertTrue(injector.getInstance(PersistenceProtoResolver.class) instanceof CorePersistenceProtoResolver); assertSingleton(MobileMessageEncryption.class); assertSingleton(MobileNotificationService.class); assertSingleton(MobileNotificationValidator.class); assertSingleton(MobileModel.class); assertSingleton(MyOfferTakenEvents.class); assertSingleton(TradeEvents.class); assertSingleton(PriceAlert.class); assertSingleton(MarketAlerts.class); assertSingleton(ChargeBackRisk.class); assertSingleton(ArbitratorService.class); assertSingleton(ArbitratorManager.class); assertSingleton(ArbitrationManager.class); assertSingleton(ArbitrationDisputeListService.class); assertSingleton(MediatorService.class); assertSingleton(MediatorManager.class); assertSingleton(MediationManager.class); assertSingleton(MediationDisputeListService.class); assertSingleton(TraderChatManager.class); assertNotSingleton(PersistenceManager.class); } private void assertSingleton(Class type) { assertSame(injector.getInstance(type), injector.getInstance(type)); } private void assertNotSingleton(Class type) { assertNotSame(injector.getInstance(type), injector.getInstance(type)); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/MarketsPrintTool.java ================================================ /* * 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 . */ package haveno.desktop; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TraditionalCurrency; import java.util.Collection; import java.util.Comparator; import java.util.Locale; import java.util.stream.Stream; public class MarketsPrintTool { public static void main(String[] args) { // Prints out all coins in the format used in the market_currency_selector.html. // Run that and copy paste the result to the market_currency_selector.html at new releases. StringBuilder sb = new StringBuilder(); Locale.setDefault(new Locale("en", "US")); // // final Collection allSortedTraditionalCurrencies = CurrencyUtil.getAllSortedTraditionalCurrencies(); final Stream traditionalStream = allSortedTraditionalCurrencies.stream() .filter(e -> !e.getCode().equals("XMR")) .map(e -> new MarketCurrency("xmr_" + e.getCode().toLowerCase(), e.getName(), e.getCode())) .distinct(); final Collection allSortedCryptoCurrencies = CurrencyUtil.getAllSortedCryptoCurrencies(); final Stream cryptoStream = allSortedCryptoCurrencies.stream() .filter(e -> !e.getCode().equals("XMR")) .map(e -> new MarketCurrency(e.getCode().toLowerCase() + "_xmr", e.getName(), e.getCode())) .distinct(); Stream.concat(traditionalStream, cryptoStream) .sorted(Comparator.comparing(o -> o.currencyName.toLowerCase())) .distinct() .forEach(e -> sb.append("") .append("\n")); System.out.println(sb.toString()); } private static class MarketCurrency { final String marketSelector; final String currencyName; final String currencyCode; MarketCurrency(String marketSelector, String currencyName, String currencyCode) { this.marketSelector = marketSelector; this.currencyName = currencyName; this.currencyCode = currencyCode; } } } ================================================ FILE: desktop/src/test/java/haveno/desktop/MaterialDesignIconDemo.java ================================================ /* * 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 . */ package haveno.desktop; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import de.jensd.fx.glyphs.materialdesignicons.utils.MaterialDesignIconFactory; import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.ScrollPane; import javafx.scene.layout.FlowPane; import javafx.stage.Stage; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; public class MaterialDesignIconDemo extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) { ScrollPane scrollPane = new ScrollPane(); scrollPane.setFitToWidth(true); FlowPane flowPane = new FlowPane(); flowPane.setStyle("-fx-background-color: #ddd;"); flowPane.setHgap(2); flowPane.setVgap(2); List values = new ArrayList<>(Arrays.asList(MaterialDesignIcon.values())); values.sort(Comparator.comparing(Enum::name)); for (MaterialDesignIcon icon : values) { Button button = MaterialDesignIconFactory.get().createIconButton(icon, icon.name()); flowPane.getChildren().add(button); } scrollPane.setContent(flowPane); primaryStage.setScene(new Scene(scrollPane, 1200, 950)); primaryStage.show(); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/MaterialDesignIconDemoLauncher.java ================================================ /* * 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 . */ package haveno.desktop; import javafx.application.Application; public class MaterialDesignIconDemoLauncher { public static void main(String[] args) { Application.launch(MaterialDesignIconDemo.class); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/common/fxml/FxmlViewLoaderTests$Malformed.fxml ================================================ ================================================ FILE: desktop/src/test/java/haveno/desktop/common/fxml/FxmlViewLoaderTests$MissingFxController.fxml ================================================ ================================================ FILE: desktop/src/test/java/haveno/desktop/common/fxml/FxmlViewLoaderTests$MissingFxmlViewAnnotation.fxml ================================================ ================================================ FILE: desktop/src/test/java/haveno/desktop/common/fxml/FxmlViewLoaderTests$WellFormed.fxml ================================================ ================================================ FILE: desktop/src/test/java/haveno/desktop/common/fxml/FxmlViewLoaderTests.java ================================================ /* * 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 . */ package haveno.desktop.common.fxml; import haveno.desktop.common.ViewfxException; import haveno.desktop.common.view.AbstractView; import haveno.desktop.common.view.FxmlView; import haveno.desktop.common.view.View; import haveno.desktop.common.view.ViewFactory; import haveno.desktop.common.view.ViewLoader; import javafx.fxml.LoadException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.util.ResourceBundle; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; // TODO Some refactorings seem to have broken those tests. Investigate and remove @Disabled as soon its fixed. @Disabled public class FxmlViewLoaderTests { private ViewLoader viewLoader; private ViewFactory viewFactory; @BeforeEach public void setUp() { viewFactory = mock(ViewFactory.class); ResourceBundle resourceBundle = mock(ResourceBundle.class); viewLoader = new FxmlViewLoader(viewFactory, resourceBundle); } @FxmlView public static class WellFormed extends AbstractView { } @Test public void wellFormedFxmlFileShouldSucceed() { given(viewFactory.call(WellFormed.class)).willReturn(new WellFormed()); View view = viewLoader.load(WellFormed.class); assertThat(view, instanceOf(WellFormed.class)); } @FxmlView public static class MissingFxController extends AbstractView { } @Test public void fxmlFileMissingFxControllerAttributeShouldThrow() { Throwable exception = assertThrows(ViewfxException.class, () -> viewLoader.load(MissingFxController.class)); assertEquals("Does it declare an fx:controller attribute?", exception.getMessage()); } public static class MissingFxmlViewAnnotation extends AbstractView { } @Test public void fxmlViewAnnotationShouldBeOptional() { given(viewFactory.call(MissingFxmlViewAnnotation.class)).willReturn(new MissingFxmlViewAnnotation()); View view = viewLoader.load(MissingFxmlViewAnnotation.class); assertThat(view, instanceOf(MissingFxmlViewAnnotation.class)); } @FxmlView public static class Malformed extends AbstractView { } @Test public void malformedFxmlFileShouldThrow() { Throwable exception = assertThrows(ViewfxException.class, () -> viewLoader.load(Malformed.class)); assertTrue(exception.getCause() instanceof LoadException); assertEquals("Failed to load view from FXML file", exception.getMessage()); } @FxmlView public static class MissingFxmlFile extends AbstractView { } @Test public void missingFxmlFileShouldThrow() { Throwable exception = assertThrows(ViewfxException.class, () -> viewLoader.load(MissingFxmlFile.class)); assertEquals("Does it exist?", exception.getMessage()); } @FxmlView(location = "unconventionally/located.fxml") public static class CustomLocation extends AbstractView { } @Test public void customFxmlFileLocationShouldOverrideDefaultConvention() { Throwable exception = assertThrows(ViewfxException.class, () -> viewLoader.load(CustomLocation.class)); assertTrue(exception.getMessage().contains("Failed to load view class")); assertTrue(exception.getMessage().contains("CustomLocation")); assertTrue(exception.getMessage().contains("[unconventionally/located.fxml] could not be loaded")); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/common/support/CachingViewLoaderTests.java ================================================ /* * 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 . */ package haveno.desktop.common.support; import haveno.desktop.common.view.AbstractView; import haveno.desktop.common.view.CachingViewLoader; import haveno.desktop.common.view.ViewLoader; import org.junit.jupiter.api.Test; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; public class CachingViewLoaderTests { @Test public void test() { ViewLoader delegateViewLoader = mock(ViewLoader.class); ViewLoader cachingViewLoader = new CachingViewLoader(delegateViewLoader); cachingViewLoader.load(TestView1.class); cachingViewLoader.load(TestView1.class); cachingViewLoader.load(TestView2.class); then(delegateViewLoader).should(times(1)).load(TestView1.class); then(delegateViewLoader).should(times(1)).load(TestView2.class); then(delegateViewLoader).should(times(0)).load(TestView3.class); } static class TestView1 extends AbstractView { } static class TestView2 extends AbstractView { } static class TestView3 extends AbstractView { } } ================================================ FILE: desktop/src/test/java/haveno/desktop/components/ColoredDecimalPlacesWithZerosTextTest.java ================================================ /* * 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 . */ package haveno.desktop.components; import javafx.scene.control.Label; import javafx.scene.text.Text; import org.junit.jupiter.api.Disabled; import static org.junit.jupiter.api.Assertions.assertEquals; public class ColoredDecimalPlacesWithZerosTextTest { @Disabled public void testOnlyZeroDecimals() { ColoredDecimalPlacesWithZerosText text = new ColoredDecimalPlacesWithZerosText("1.0000", 3); Label beforeZeros = (Label) text.getChildren().get(0); Label zeroDecimals = (Label) text.getChildren().get(1); assertEquals("1.0", beforeZeros.getText()); assertEquals("000", zeroDecimals.getText()); } @Disabled public void testOneZeroDecimal() { ColoredDecimalPlacesWithZerosText text = new ColoredDecimalPlacesWithZerosText("1.2570", 3); Text beforeZeros = (Text) text.getChildren().get(0); Text zeroDecimals = (Text) text.getChildren().get(1); assertEquals("1.257", beforeZeros.getText()); assertEquals("0", zeroDecimals.getText()); } @Disabled public void testMultipleZeroDecimal() { ColoredDecimalPlacesWithZerosText text = new ColoredDecimalPlacesWithZerosText("1.2000", 3); Text beforeZeros = (Text) text.getChildren().get(0); Text zeroDecimals = (Text) text.getChildren().get(1); assertEquals("1.2", beforeZeros.getText()); assertEquals("000", zeroDecimals.getText()); } @Disabled public void testZeroDecimalsWithRange() { ColoredDecimalPlacesWithZerosText text = new ColoredDecimalPlacesWithZerosText("0.1000 - 0.1250", 3); assertEquals(5, text.getChildren().size()); Text beforeZeros = (Text) text.getChildren().get(0); Text zeroDecimals = (Text) text.getChildren().get(1); Text separator = (Text) text.getChildren().get(2); Text beforeZeros2 = (Text) text.getChildren().get(3); Text zeroDecimals2 = (Text) text.getChildren().get(4); assertEquals("0.1", beforeZeros.getText()); assertEquals("000", zeroDecimals.getText()); assertEquals(" - ", separator.getText()); assertEquals("0.125", beforeZeros2.getText()); assertEquals("0", zeroDecimals2.getText()); } @Disabled public void testNoColorizing() { ColoredDecimalPlacesWithZerosText text = new ColoredDecimalPlacesWithZerosText("1.2570", 0); Text beforeZeros = (Text) text.getChildren().get(0); assertEquals("1.2570", beforeZeros.getText()); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/main/funds/transactions/DisplayedTransactionsTest.java ================================================ /* * 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 . */ package haveno.desktop.main.funds.transactions; import com.google.common.collect.Lists; import haveno.core.xmr.wallet.XmrWalletService; import javafx.collections.FXCollections; import monero.wallet.model.MoneroTxWallet; import org.junit.jupiter.api.Test; import java.util.Collections; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class DisplayedTransactionsTest { @Test public void testUpdate() { List transactions = Lists.newArrayList(mock(MoneroTxWallet.class), mock(MoneroTxWallet.class)); XmrWalletService walletService = mock(XmrWalletService.class); when(walletService.getTxs(false)).thenReturn(transactions); TransactionListItemFactory transactionListItemFactory = mock(TransactionListItemFactory.class, RETURNS_DEEP_STUBS); @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") DisplayedTransactions testedEntity = new DisplayedTransactions( walletService, mock(TradableRepository.class), transactionListItemFactory, mock(TransactionAwareTradableFactory.class)); testedEntity.update(); assertEquals(transactions.size(), testedEntity.size()); } @Test public void testUpdateWhenRepositoryIsEmpty() { XmrWalletService walletService = mock(XmrWalletService.class); when(walletService.getTxs(false)) .thenReturn(Collections.singletonList(mock(MoneroTxWallet.class))); TradableRepository tradableRepository = mock(TradableRepository.class); when(tradableRepository.getAll()).thenReturn(FXCollections.emptyObservableSet()); TransactionListItemFactory transactionListItemFactory = mock(TransactionListItemFactory.class); @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") DisplayedTransactions testedEntity = new DisplayedTransactions( walletService, tradableRepository, transactionListItemFactory, mock(TransactionAwareTradableFactory.class)); testedEntity.update(); assertEquals(1, testedEntity.size()); verify(transactionListItemFactory).create(any(), nullable(TransactionAwareTradable.class)); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/main/funds/transactions/ObservableListDecoratorTest.java ================================================ /* * 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 . */ package haveno.desktop.main.funds.transactions; import com.google.common.collect.Lists; import org.junit.jupiter.api.Test; import java.util.Collection; import java.util.function.Supplier; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; public class ObservableListDecoratorTest { @Test public void testSetAll() { ObservableListDecorator list = new ObservableListDecorator<>(); Collection state = Lists.newArrayList(3, 2, 1); list.setAll(state); assertEquals(state, list); state = Lists.newArrayList(0, 0, 0, 0); list.setAll(state); assertEquals(state, list); } @Test public void testForEach() { ObservableListDecorator list = new ObservableListDecorator<>(); Collection state = Lists.newArrayList(mock(Supplier.class), mock(Supplier.class)); list.setAll(state); assertEquals(state, list); list.forEach(Supplier::get); state.forEach(supplier -> verify(supplier).get()); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/main/funds/transactions/TransactionAwareTradableFactoryTest.java ================================================ /* * 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 . */ package haveno.desktop.main.funds.transactions; import haveno.core.offer.OpenOffer; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.trade.Tradable; import haveno.core.trade.Trade; import monero.wallet.model.MoneroTxWallet; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.mockito.Mockito.mock; public class TransactionAwareTradableFactoryTest { @Test public void testCreateWhenNotOpenOfferOrTrade() { ArbitrationManager arbitrationManager = mock(ArbitrationManager.class); TransactionAwareTradableFactory factory = new TransactionAwareTradableFactory(arbitrationManager, null, null, null); Tradable delegate = mock(Tradable.class); assertFalse(delegate instanceof OpenOffer); assertFalse(delegate instanceof Trade); TransactionAwareTradable tradable = factory.create(delegate); assertFalse(tradable.isRelatedToTransaction(mock(MoneroTxWallet.class))); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/main/funds/transactions/TransactionAwareTradeTest.java ================================================ /* * 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 . */ package haveno.desktop.main.funds.transactions; import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.refund.RefundManager; import haveno.core.trade.Trade; import haveno.core.xmr.wallet.XmrWalletService; import javafx.collections.FXCollections; import monero.wallet.model.MoneroTxWallet; import org.bitcoinj.core.Sha256Hash; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class TransactionAwareTradeTest { private static final Sha256Hash XID = Sha256Hash.wrap("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); private MoneroTxWallet transaction; private ArbitrationManager arbitrationManager; private Trade delegate; private TransactionAwareTradable trade; private RefundManager refundManager; private XmrWalletService xmrWalletService; @BeforeEach public void setUp() { this.transaction = mock(MoneroTxWallet.class); when(transaction.getHash()).thenReturn(XID.toString()); delegate = mock(Trade.class, RETURNS_DEEP_STUBS); arbitrationManager = mock(ArbitrationManager.class, RETURNS_DEEP_STUBS); refundManager = mock(RefundManager.class, RETURNS_DEEP_STUBS); xmrWalletService = mock(XmrWalletService.class, RETURNS_DEEP_STUBS); trade = new TransactionAwareTrade(delegate, arbitrationManager, refundManager, xmrWalletService, null); } @Test public void testIsRelatedToTransactionWhenPayoutTx() { when(delegate.getPayoutTxId()).thenReturn(XID.toString()); assertTrue(trade.isRelatedToTransaction(transaction)); } @Test public void testIsRelatedToTransactionWhenMakerDepositTx() { when(delegate.getMaker().getDepositTxHash()).thenReturn(XID.toString()); assertTrue(trade.isRelatedToTransaction(transaction)); } @Test public void testIsRelatedToTransactionWhenTakerDepositTx() { when(delegate.getTaker().getDepositTxHash()).thenReturn(XID.toString()); assertTrue(trade.isRelatedToTransaction(transaction)); } @Test public void testIsRelatedToTransactionWhenDisputedPayoutTx() { final String tradeId = "7"; Dispute dispute = mock(Dispute.class); when(dispute.getDisputePayoutTxId()).thenReturn(XID.toString()); when(dispute.getTradeId()).thenReturn(tradeId); when(arbitrationManager.getDisputesAsObservableList()) .thenReturn(FXCollections.observableArrayList(Set.of(dispute))); when(arbitrationManager.getDisputedTradeIds()) .thenReturn(Set.of(tradeId)); when(delegate.getId()).thenReturn(tradeId); assertTrue(trade.isRelatedToTransaction(transaction)); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModelTest.java ================================================ /* * 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 . */ package haveno.desktop.main.market.offerbook; import haveno.core.locale.GlobalSettings; import haveno.core.provider.price.PriceFeedService; import haveno.desktop.main.offer.offerbook.OfferBook; import haveno.desktop.main.offer.offerbook.OfferBookListItem; import haveno.desktop.main.offer.offerbook.OfferBookListItemMaker; import javafx.beans.property.SimpleIntegerProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static com.natpryce.makeiteasy.MakeItEasy.make; import static com.natpryce.makeiteasy.MakeItEasy.with; import static haveno.desktop.main.offer.offerbook.OfferBookListItemMaker.xmrBuyItem; import static haveno.desktop.main.offer.offerbook.OfferBookListItemMaker.xmrSellItem; import static haveno.desktop.maker.PreferenceMakers.empty; import static haveno.desktop.maker.TradeCurrencyMakers.usd; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class OfferBookChartViewModelTest { @BeforeEach public void setUp() { GlobalSettings.setDefaultTradeCurrency(usd); } @Test public void testMaxCharactersForBuyPriceWithNoOffers() { OfferBook offerBook = mock(OfferBook.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, null, null, null); assertEquals(0, model.maxPlacesForBuyPrice.intValue()); } @Test public void testMaxCharactersForBuyPriceWithOfflinePriceFeedService() { OfferBook offerBook = mock(OfferBook.class); PriceFeedService priceFeedService = mock(PriceFeedService.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); final OfferBookListItem item = make(OfferBookListItemMaker.xmrBuyItem.but(with(OfferBookListItemMaker.useMarketBasedPrice, true))); item.getOffer().setPriceFeedService(priceFeedService); offerBookListItems.addAll(item); when(priceFeedService.getMarketPrice(anyString())).thenReturn(null); when(priceFeedService.updateCounterProperty()).thenReturn(new SimpleIntegerProperty()); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, priceFeedService, null, null); model.activate(); assertEquals(0, model.maxPlacesForBuyPrice.intValue()); } @Test public void testMaxCharactersForFiatBuyPrice() { OfferBook offerBook = mock(OfferBook.class); PriceFeedService service = mock(PriceFeedService.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); offerBookListItems.addAll(make(OfferBookListItemMaker.xmrBuyItem)); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, service, null, null); model.activate(); assertEquals(9, model.maxPlacesForBuyPrice.intValue()); offerBookListItems.addAll(make(xmrBuyItem.but(with(OfferBookListItemMaker.price, 940164750000L)))); assertEquals(9, model.maxPlacesForBuyPrice.intValue()); // 9401.6475 offerBookListItems.addAll(make(xmrBuyItem.but(with(OfferBookListItemMaker.price, 1010164750000L)))); assertEquals(10, model.maxPlacesForBuyPrice.intValue()); //10101.6475 } @Test public void testMaxCharactersForBuyVolumeWithNoOffers() { OfferBook offerBook = mock(OfferBook.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, null, null, null); assertEquals(0, model.maxPlacesForBuyVolume.intValue()); } @Test public void testMaxCharactersForFiatBuyVolume() { OfferBook offerBook = mock(OfferBook.class); PriceFeedService service = mock(PriceFeedService.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); offerBookListItems.addAll(make(OfferBookListItemMaker.xmrBuyItem)); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, service, null, null); model.activate(); assertEquals(3, model.maxPlacesForBuyVolume.intValue()); //0 offerBookListItems.addAll(make(xmrBuyItem.but(with(OfferBookListItemMaker.amount, 1000000000000L)))); assertEquals(4, model.maxPlacesForBuyVolume.intValue()); //10 offerBookListItems.addAll(make(xmrBuyItem.but(with(OfferBookListItemMaker.amount, 221286000000000L)))); assertEquals(6, model.maxPlacesForBuyVolume.intValue()); //2213 } @Test public void testMaxCharactersForSellPriceWithNoOffers() { OfferBook offerBook = mock(OfferBook.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, null, null, null); assertEquals(0, model.maxPlacesForSellPrice.intValue()); } @Test public void testMaxCharactersForSellPriceWithOfflinePriceFeedService() { OfferBook offerBook = mock(OfferBook.class); PriceFeedService priceFeedService = mock(PriceFeedService.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); final OfferBookListItem item = make(OfferBookListItemMaker.xmrSellItem.but(with(OfferBookListItemMaker.useMarketBasedPrice, true))); item.getOffer().setPriceFeedService(priceFeedService); offerBookListItems.addAll(item); when(priceFeedService.getMarketPrice(anyString())).thenReturn(null); when(priceFeedService.updateCounterProperty()).thenReturn(new SimpleIntegerProperty()); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, priceFeedService, null, null); model.activate(); assertEquals(0, model.maxPlacesForSellPrice.intValue()); } @Test public void testMaxCharactersForFiatSellPrice() { OfferBook offerBook = mock(OfferBook.class); PriceFeedService service = mock(PriceFeedService.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); offerBookListItems.addAll(make(OfferBookListItemMaker.xmrSellItem)); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, service, null, null); model.activate(); assertEquals(9, model.maxPlacesForSellPrice.intValue()); // 10.0000 default price offerBookListItems.addAll(make(xmrSellItem.but(with(OfferBookListItemMaker.price, 940164750000L)))); assertEquals(9, model.maxPlacesForSellPrice.intValue()); // 9401.6475 offerBookListItems.addAll(make(xmrSellItem.but(with(OfferBookListItemMaker.price, 1010164750000L)))); assertEquals(10, model.maxPlacesForSellPrice.intValue()); // 10101.6475 } @Test public void testMaxCharactersForSellVolumeWithNoOffers() { OfferBook offerBook = mock(OfferBook.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, null, null, null); assertEquals(0, model.maxPlacesForSellVolume.intValue()); } @Test public void testMaxCharactersForFiatSellVolume() { OfferBook offerBook = mock(OfferBook.class); PriceFeedService service = mock(PriceFeedService.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); offerBookListItems.addAll(make(OfferBookListItemMaker.xmrSellItem)); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, service, null, null); model.activate(); assertEquals(3, model.maxPlacesForSellVolume.intValue()); //0 offerBookListItems.addAll(make(xmrSellItem.but(with(OfferBookListItemMaker.amount, 1000000000000L)))); assertEquals(4, model.maxPlacesForSellVolume.intValue()); //10 offerBookListItems.addAll(make(xmrSellItem.but(with(OfferBookListItemMaker.amount, 221286000000000L)))); assertEquals(6, model.maxPlacesForSellVolume.intValue()); //2213 } } ================================================ FILE: desktop/src/test/java/haveno/desktop/main/market/spread/SpreadViewModelTest.java ================================================ /* * 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 . */ package haveno.desktop.main.market.spread; import haveno.common.config.Config; import haveno.core.provider.price.PriceFeedService; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.coin.ImmutableCoinFormatter; import haveno.desktop.main.offer.offerbook.OfferBook; import haveno.desktop.main.offer.offerbook.OfferBookListItem; import haveno.desktop.main.offer.offerbook.OfferBookListItemMaker; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import org.junit.jupiter.api.Test; import static com.natpryce.makeiteasy.MakeItEasy.make; import static com.natpryce.makeiteasy.MakeItEasy.with; import static haveno.desktop.main.offer.offerbook.OfferBookListItemMaker.id; import static haveno.desktop.main.offer.offerbook.OfferBookListItemMaker.xmrBuyItem; import static haveno.desktop.main.offer.offerbook.OfferBookListItemMaker.xmrSellItem; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class SpreadViewModelTest { private final CoinFormatter coinFormatter = new ImmutableCoinFormatter(Config.baseCurrencyNetworkParameters().getMonetaryFormat()); @Test public void testMaxCharactersForAmountWithNoOffers() { OfferBook offerBook = mock(OfferBook.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); SpreadViewModel model = new SpreadViewModel(offerBook, null, coinFormatter); assertEquals(0, model.maxPlacesForAmount.intValue()); } @Test public void testMaxCharactersForAmount() { OfferBook offerBook = mock(OfferBook.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); offerBookListItems.addAll(make(xmrBuyItem)); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); SpreadViewModel model = new SpreadViewModel(offerBook, null, coinFormatter); model.activate(); assertEquals(6, model.maxPlacesForAmount.intValue()); // 0.001 offerBookListItems.addAll(make(xmrBuyItem.but(with(OfferBookListItemMaker.amount, 14030000000000L)))); assertEquals(7, model.maxPlacesForAmount.intValue()); //14.0300 } @Test public void testFilterSpreadItemsForUniqueOffers() { OfferBook offerBook = mock(OfferBook.class); PriceFeedService priceFeedService = mock(PriceFeedService.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); offerBookListItems.addAll(make(xmrBuyItem)); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); SpreadViewModel model = new SpreadViewModel(offerBook, priceFeedService, coinFormatter); model.activate(); assertEquals(1, model.spreadItems.get(0).numberOfOffers); offerBookListItems.addAll(make(xmrBuyItem.but(with(id, "2345"))), make(xmrBuyItem.but(with(id, "2345"))), make(xmrSellItem.but(with(id, "3456"))), make(xmrSellItem.but(with(id, "3456")))); assertEquals(2, model.spreadItems.get(0).numberOfBuyOffers); assertEquals(1, model.spreadItems.get(0).numberOfSellOffers); assertEquals(3, model.spreadItems.get(0).numberOfOffers); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/main/market/trades/TradesChartsViewModelTest.java ================================================ /* * 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 . */ package haveno.desktop.main.market.trades; import haveno.core.locale.TraditionalCurrency; import haveno.core.monetary.Price; import haveno.core.monetary.TraditionalMoney; import haveno.core.offer.OfferPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.HavenoUtils; import haveno.core.trade.statistics.TradeStatistics3; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.Preferences; import haveno.desktop.Navigation; import haveno.desktop.main.market.trades.charts.CandleData; import javafx.collections.FXCollections; import javafx.collections.ObservableSet; import javafx.util.Pair; import org.bitcoinj.core.Coin; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.io.File; import java.io.IOException; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import java.util.Map; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; public class TradesChartsViewModelTest { TradesChartsViewModel model; TradeStatisticsManager tradeStatisticsManager; DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); private File dir; OfferPayload offer = new OfferPayload(null, 0, null, null, null, 0, 0, false, 0, 0, 0, 0, 0, 0, 0, "XMR", "EUR", null, null, null, null, null, null, null, 0, 0, 0, false, false, 0, 0, false, null, null, 0, null, null, null, null); @BeforeEach public void setup() throws IOException { tradeStatisticsManager = mock(TradeStatisticsManager.class); model = new TradesChartsViewModel(tradeStatisticsManager, mock(Preferences.class), mock(PriceFeedService.class), mock(Navigation.class)); dir = File.createTempFile("temp_tests1", ""); //noinspection ResultOfMethodCallIgnored dir.delete(); //noinspection ResultOfMethodCallIgnored dir.mkdir(); } @SuppressWarnings("ConstantConditions") @Test public void testGetCandleData() { String currencyCode = "EUR"; model.selectedTradeCurrencyProperty.setValue(new TraditionalCurrency(currencyCode)); long low = TraditionalMoney.parseTraditionalMoney("EUR", "500").value; long open = TraditionalMoney.parseTraditionalMoney("EUR", "520").value; long close = TraditionalMoney.parseTraditionalMoney("EUR", "580").value; long high = TraditionalMoney.parseTraditionalMoney("EUR", "600").value; long average = TraditionalMoney.parseTraditionalMoney("EUR", "550").value; long median = TraditionalMoney.parseTraditionalMoney("EUR", "550").value; long amount = HavenoUtils.xmrToAtomicUnits(4).longValue(); long volume = TraditionalMoney.parseTraditionalMoney("EUR", "2200").value; boolean isBullish = true; Set set = new HashSet<>(); final Date now = new Date(); set.add(new TradeStatistics3(offer.getCurrencyCode(), Price.parse("EUR", "520").getValue(), HavenoUtils.xmrToAtomicUnits(1).longValue(), PaymentMethod.BLOCK_CHAINS_ID, now.getTime(), null, null, null)); set.add(new TradeStatistics3(offer.getCurrencyCode(), Price.parse("EUR", "500").getValue(), HavenoUtils.xmrToAtomicUnits(1).longValue(), PaymentMethod.BLOCK_CHAINS_ID, now.getTime() + 100, null, null, null)); set.add(new TradeStatistics3(offer.getCurrencyCode(), Price.parse("EUR", "600").getValue(), HavenoUtils.xmrToAtomicUnits(1).longValue(), PaymentMethod.BLOCK_CHAINS_ID, now.getTime() + 200, null, null, null)); set.add(new TradeStatistics3(offer.getCurrencyCode(), Price.parse("EUR", "580").getValue(), HavenoUtils.xmrToAtomicUnits(1).longValue(), PaymentMethod.BLOCK_CHAINS_ID, now.getTime() + 300, null, null, null)); Map>> itemsPerInterval = null; long tick = ChartCalculations.roundToTick(now, TradesChartsViewModel.TickUnit.DAY).getTime(); CandleData candleData = ChartCalculations.getCandleData(tick, set, 0, TradesChartsViewModel.TickUnit.DAY, currencyCode, itemsPerInterval); assertEquals(open, candleData.open); assertEquals(close, candleData.close); assertEquals(high, candleData.high); assertEquals(low, candleData.low); assertEquals(average, candleData.average); assertEquals(median, candleData.median); assertEquals(amount, candleData.accumulatedAmount); assertEquals(volume, candleData.accumulatedVolume); assertEquals(isBullish, candleData.isBullish); } // TODO JMOCKIT @Disabled @Test public void testItemLists() throws ParseException { // Helper class to add historic trades class Trade { Trade(String date, String size, String price, String cc) { try { this.date = dateFormat.parse(date); } catch (ParseException p) { this.date = new Date(); } this.size = size; this.price = price; this.cc = cc; } Date date; String size; String price; String cc; } // Trade EUR model.selectedTradeCurrencyProperty.setValue(new TraditionalCurrency("EUR")); ArrayList trades = new ArrayList<>(); // Set predetermined time to use as "now" during test /* new MockUp() { @Mock long currentTimeMillis() { return test_time.getTime(); } };*/ // Two trades 10 seconds apart, different YEAR, MONTH, WEEK, DAY, HOUR, MINUTE_10 trades.add(new Trade("2017-12-31T23:59:52", "1", "100", "EUR")); trades.add(new Trade("2018-01-01T00:00:02", "1", "110", "EUR")); Set set = new HashSet<>(); trades.forEach(t -> set.add(new TradeStatistics3(offer.getCurrencyCode(), Price.parse(t.cc, t.price).getValue(), Coin.parseCoin(t.size).getValue(), PaymentMethod.BLOCK_CHAINS_ID, t.date.getTime(), null, null, null)) ); ObservableSet tradeStats = FXCollections.observableSet(set); // Run test for each tick type for (TradesChartsViewModel.TickUnit tick : TradesChartsViewModel.TickUnit.values()) { /* new Expectations() {{ tradeStatisticsManager.getObservableTradeStatisticsSet(); result = tradeStats; }};*/ // Trigger chart update model.setTickUnit(tick); assertEquals(model.selectedTradeCurrencyProperty.get().getCode(), tradeStats.iterator().next().getCurrency()); assertEquals(2, model.priceItems.size()); assertEquals(2, model.volumeItems.size()); } } } ================================================ FILE: desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferDataModelTest.java ================================================ package haveno.desktop.main.offer.createoffer; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.GlobalSettings; import haveno.core.locale.Res; import haveno.core.offer.CreateOfferService; import haveno.core.offer.OfferDirection; import haveno.core.offer.OfferUtil; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.ZelleAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.RevolutAccount; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.wallet.XmrWalletService; import javafx.collections.FXCollections; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.HashSet; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class CreateOfferDataModelTest { private CreateOfferDataModel model; private User user; private Preferences preferences; private OfferUtil offerUtil; @BeforeEach public void setUp() { final CryptoCurrency xmr = new CryptoCurrency("XMR", "monero"); GlobalSettings.setDefaultTradeCurrency(xmr); Res.setup(); OpenOfferManager openOfferManager = mock(OpenOfferManager.class); XmrAddressEntry addressEntry = mock(XmrAddressEntry.class); XmrWalletService xmrWalletService = mock(XmrWalletService.class); PriceFeedService priceFeedService = mock(PriceFeedService.class); CreateOfferService createOfferService = mock(CreateOfferService.class); preferences = mock(Preferences.class); offerUtil = mock(OfferUtil.class); user = mock(User.class); var tradeStats = mock(TradeStatisticsManager.class); when(xmrWalletService.getOrCreateAddressEntry(anyString(), any())).thenReturn(addressEntry); when(preferences.isUsePercentageBasedPrice()).thenReturn(true); when(preferences.getSecurityDepositAsPercent(null)).thenReturn(0.01); when(createOfferService.getRandomOfferId()).thenReturn(UUID.randomUUID().toString()); when(tradeStats.getObservableTradeStatisticsList()).thenReturn(FXCollections.observableArrayList()); model = new CreateOfferDataModel(createOfferService, openOfferManager, offerUtil, xmrWalletService, preferences, user, null, priceFeedService, null, null, tradeStats, null); } @Test public void testUseTradeCurrencySetInOfferViewWhenInPaymentAccountAvailable() { final HashSet paymentAccounts = new HashSet<>(); final ZelleAccount zelleAccount = new ZelleAccount(); zelleAccount.setId("234"); zelleAccount.setAccountName("zelleAccount"); paymentAccounts.add(zelleAccount); final RevolutAccount revolutAccount = new RevolutAccount(); revolutAccount.setId("123"); revolutAccount.setAccountName("revolutAccount"); revolutAccount.setSingleTradeCurrency(new TraditionalCurrency("EUR")); revolutAccount.addCurrency(new TraditionalCurrency("USD")); paymentAccounts.add(revolutAccount); when(user.getPaymentAccounts()).thenReturn(paymentAccounts); when(preferences.getSelectedPaymentAccountForCreateOffer()).thenReturn(revolutAccount); model.initWithData(OfferDirection.BUY, new TraditionalCurrency("USD"), true); assertEquals("USD", model.getTradeCurrencyCode().get()); } @Test public void testUseTradeAccountThatMatchesTradeCurrencySetInOffer() { final HashSet paymentAccounts = new HashSet<>(); final ZelleAccount zelleAccount = new ZelleAccount(); zelleAccount.setId("234"); zelleAccount.setAccountName("zelleAccount"); paymentAccounts.add(zelleAccount); final RevolutAccount revolutAccount = new RevolutAccount(); revolutAccount.setId("123"); revolutAccount.setAccountName("revolutAccount"); revolutAccount.setSingleTradeCurrency(new TraditionalCurrency("EUR")); paymentAccounts.add(revolutAccount); when(user.getPaymentAccounts()).thenReturn(paymentAccounts); when(user.findFirstPaymentAccountWithCurrency(new TraditionalCurrency("USD"))).thenReturn(zelleAccount); when(preferences.getSelectedPaymentAccountForCreateOffer()).thenReturn(revolutAccount); model.initWithData(OfferDirection.BUY, new TraditionalCurrency("USD"), true); assertEquals("USD", model.getTradeCurrencyCode().get()); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferViewModelTest.java ================================================ /* * 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 . */ package haveno.desktop.main.offer.createoffer; import haveno.common.config.Config; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Country; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.GlobalSettings; import haveno.core.locale.Res; import haveno.core.offer.CreateOfferService; import haveno.core.offer.OfferDirection; import haveno.core.offer.OfferUtil; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.validation.SecurityDepositValidator; import haveno.core.payment.validation.XmrValidator; import haveno.core.provider.price.MarketPrice; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.coin.ImmutableCoinFormatter; import haveno.core.util.validation.AmountValidator8Decimals; import haveno.core.util.validation.AmountValidator4Decimals; import haveno.core.util.validation.InputValidator; import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.wallet.XmrWalletService; import javafx.beans.property.SimpleIntegerProperty; import javafx.collections.FXCollections; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.math.BigInteger; import java.time.Instant; import java.util.UUID; import static haveno.desktop.maker.PreferenceMakers.empty; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class CreateOfferViewModelTest { private CreateOfferViewModel model; private final CoinFormatter coinFormatter = new ImmutableCoinFormatter( Config.baseCurrencyNetworkParameters().getMonetaryFormat()); @BeforeEach public void setUp() { final CryptoCurrency xmr = new CryptoCurrency("XMR", "monero"); GlobalSettings.setDefaultTradeCurrency(xmr); Res.setup(); final XmrValidator btcValidator = new XmrValidator(); final AmountValidator8Decimals priceValidator8Decimals = new AmountValidator8Decimals(); final AmountValidator4Decimals priceValidator4Decimals = new AmountValidator4Decimals(); XmrAddressEntry addressEntry = mock(XmrAddressEntry.class); OpenOfferManager openOfferManager = mock(OpenOfferManager.class); XmrWalletService xmrWalletService = mock(XmrWalletService.class); PriceFeedService priceFeedService = mock(PriceFeedService.class); User user = mock(User.class); PaymentAccount paymentAccount = mock(PaymentAccount.class); Preferences preferences = mock(Preferences.class); SecurityDepositValidator securityDepositValidator = mock(SecurityDepositValidator.class); AccountAgeWitnessService accountAgeWitnessService = mock(AccountAgeWitnessService.class); CreateOfferService createOfferService = mock(CreateOfferService.class); OfferUtil offerUtil = mock(OfferUtil.class); var tradeStats = mock(TradeStatisticsManager.class); when(xmrWalletService.getOrCreateAddressEntry(anyString(), any())).thenReturn(addressEntry); when(xmrWalletService.getBalanceForSubaddress(any(Integer.class))).thenReturn(BigInteger.valueOf(10000000L)); when(priceFeedService.updateCounterProperty()).thenReturn(new SimpleIntegerProperty()); when(priceFeedService.getMarketPrice(anyString())).thenReturn( new MarketPrice("USD", 12684.0450, Instant.now().getEpochSecond(), true)); when(user.findFirstPaymentAccountWithCurrency(any())).thenReturn(paymentAccount); when(paymentAccount.getPaymentMethod()).thenReturn(PaymentMethod.ZELLE); when(user.getPaymentAccountsAsObservable()).thenReturn(FXCollections.observableArrayList()); when(securityDepositValidator.validate(any())).thenReturn(new InputValidator.ValidationResult(false)); when(accountAgeWitnessService.getMyTradeLimit(any(), any(), any(), anyBoolean())).thenReturn(100000000L); when(preferences.getUserCountry()).thenReturn(new Country("ES", "Spain", null)); when(createOfferService.getRandomOfferId()).thenReturn(UUID.randomUUID().toString()); when(tradeStats.getObservableTradeStatisticsList()).thenReturn(FXCollections.observableArrayList()); CreateOfferDataModel dataModel = new CreateOfferDataModel(createOfferService, openOfferManager, offerUtil, xmrWalletService, empty, user, null, priceFeedService, accountAgeWitnessService, coinFormatter, tradeStats, null); dataModel.initWithData(OfferDirection.BUY, new CryptoCurrency("XMR", "monero"), true); dataModel.activate(); model = new CreateOfferViewModel(dataModel, null, priceValidator4Decimals, priceValidator8Decimals, btcValidator, securityDepositValidator, priceFeedService, null, null, preferences, coinFormatter, offerUtil); model.activate(); } @Test public void testSyncMinAmountWithAmountUntilChanged() { assertNull(model.amount.get()); assertNull(model.minAmount.get()); model.amount.set("0.0"); assertEquals("0.0", model.amount.get()); assertNull(model.minAmount.get()); model.amount.set("0.03"); assertEquals("0.03", model.amount.get()); assertEquals("0.03", model.minAmount.get()); model.amount.set("0.0312"); assertEquals("0.0312", model.amount.get()); assertEquals("0.0312", model.minAmount.get()); model.minAmount.set("0.01"); model.onFocusOutMinAmountTextField(true, false); assertEquals("0.01", model.minAmount.get()); model.amount.set("0.0301"); assertEquals("0.0301", model.amount.get()); assertEquals("0.01", model.minAmount.get()); } @Test public void testSyncMinAmountWithAmountWhenZeroCoinIsSet() { model.amount.set("0.03"); assertEquals("0.03", model.amount.get()); assertEquals("0.03", model.minAmount.get()); model.minAmount.set("0.00"); model.onFocusOutMinAmountTextField(true, false); model.amount.set("0.04"); assertEquals("0.04", model.amount.get()); assertEquals("0.04", model.minAmount.get()); } @Test public void testSyncMinAmountWithAmountWhenSameValueIsSet() { model.amount.set("0.03"); assertEquals("0.03", model.amount.get()); assertEquals("0.03", model.minAmount.get()); model.minAmount.set("0.03"); model.onFocusOutMinAmountTextField(true, false); model.amount.set("0.04"); assertEquals("0.04", model.amount.get()); assertEquals("0.04", model.minAmount.get()); } @Test public void testSyncMinAmountWithAmountWhenHigherMinAmountValueIsSet() { model.amount.set("0.03"); assertEquals("0.03", model.amount.get()); assertEquals("0.03", model.minAmount.get()); model.minAmount.set("0.05"); model.onFocusOutMinAmountTextField(true, false); assertEquals("0.05", model.amount.get()); assertEquals("0.05", model.minAmount.get()); } @Test public void testSyncPriceMarginWithVolumeAndFixedPrice() { model.amount.set("0.01"); model.onFocusOutPriceAsPercentageTextField(true, false); //leave focus without changing assertEquals("0.00", model.marketPriceMargin.get()); assertEquals("126.84045000", model.volume.get()); assertEquals("12684.04500000", model.price.get()); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookListItemMaker.java ================================================ /* * 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 . */ package haveno.desktop.main.offer.offerbook; import com.natpryce.makeiteasy.Instantiator; import com.natpryce.makeiteasy.MakeItEasy; import static com.natpryce.makeiteasy.MakeItEasy.a; import static com.natpryce.makeiteasy.MakeItEasy.make; import static com.natpryce.makeiteasy.MakeItEasy.with; import com.natpryce.makeiteasy.Maker; import com.natpryce.makeiteasy.Property; import haveno.core.offer.OfferDirection; import haveno.desktop.maker.OfferMaker; import static haveno.desktop.maker.OfferMaker.xmrUsdOffer; public class OfferBookListItemMaker { public static final Property id = new Property<>(); public static final Property price = new Property<>(); public static final Property amount = new Property<>(); public static final Property minAmount = new Property<>(); public static final Property direction = new Property<>(); public static final Property useMarketBasedPrice = new Property<>(); public static final Property marketPriceMargin = new Property<>(); public static final Property baseCurrencyCode = new Property<>(); public static final Property counterCurrencyCode = new Property<>(); public static final Instantiator OfferBookListItem = lookup -> new OfferBookListItem(make(xmrUsdOffer.but( with(OfferMaker.price, lookup.valueOf(price, 100000000000L)), with(OfferMaker.amount, lookup.valueOf(amount, 100000000000L)), with(OfferMaker.minAmount, lookup.valueOf(amount, 100000000000L)), with(OfferMaker.direction, lookup.valueOf(direction, OfferDirection.BUY)), with(OfferMaker.useMarketBasedPrice, lookup.valueOf(useMarketBasedPrice, false)), with(OfferMaker.marketPriceMargin, lookup.valueOf(marketPriceMargin, 0.0)), with(OfferMaker.baseCurrencyCode, lookup.valueOf(baseCurrencyCode, "XMR")), with(OfferMaker.counterCurrencyCode, lookup.valueOf(counterCurrencyCode, "USD")), with(OfferMaker.id, lookup.valueOf(id, "1234")) ))); public static final Instantiator OfferBookListItemWithRange = lookup -> new OfferBookListItem(make(xmrUsdOffer.but( MakeItEasy.with(OfferMaker.price, lookup.valueOf(price, 100000L)), with(OfferMaker.minAmount, lookup.valueOf(minAmount, 100000000000L)), with(OfferMaker.amount, lookup.valueOf(amount, 200000000000L))))); public static final Maker xmrBuyItem = a(OfferBookListItem); public static final Maker xmrSellItem = a(OfferBookListItem, with(direction, OfferDirection.SELL)); public static final Maker xmrItemWithRange = a(OfferBookListItemWithRange); } ================================================ FILE: desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookViewModelTest.java ================================================ /* * 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 . */ package haveno.desktop.main.offer.offerbook; import com.natpryce.makeiteasy.Maker; import haveno.common.config.Config; import haveno.core.locale.Country; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.GlobalSettings; import haveno.core.locale.Res; import haveno.core.offer.Offer; import haveno.core.offer.OfferPayload; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.AliPayAccount; import haveno.core.payment.CountryBasedPaymentAccount; import haveno.core.payment.CryptoCurrencyAccount; import haveno.core.payment.NationalBankAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccountUtil; import haveno.core.payment.SameBankAccount; import haveno.core.payment.SepaAccount; import haveno.core.payment.SpecificBanksAccount; import haveno.core.payment.payload.NationalBankAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.SameBankAccountPayload; import haveno.core.payment.payload.SepaAccountPayload; import haveno.core.payment.payload.SpecificBanksAccountPayload; import haveno.core.provider.price.MarketPrice; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.User; import haveno.core.util.PriceUtil; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.coin.ImmutableCoinFormatter; import javafx.beans.property.SimpleIntegerProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import static com.natpryce.makeiteasy.MakeItEasy.make; import static com.natpryce.makeiteasy.MakeItEasy.with; import static haveno.desktop.main.offer.offerbook.OfferBookListItemMaker.amount; import static haveno.desktop.main.offer.offerbook.OfferBookListItemMaker.marketPriceMargin; import static haveno.desktop.main.offer.offerbook.OfferBookListItemMaker.minAmount; import static haveno.desktop.main.offer.offerbook.OfferBookListItemMaker.price; import static haveno.desktop.main.offer.offerbook.OfferBookListItemMaker.useMarketBasedPrice; import static haveno.desktop.main.offer.offerbook.OfferBookListItemMaker.xmrBuyItem; import static haveno.desktop.main.offer.offerbook.OfferBookListItemMaker.xmrItemWithRange; import static haveno.desktop.maker.PreferenceMakers.empty; import static haveno.desktop.maker.TradeCurrencyMakers.usd; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class OfferBookViewModelTest { private final CoinFormatter coinFormatter = new ImmutableCoinFormatter(Config.baseCurrencyNetworkParameters().getMonetaryFormat()); private static final Logger log = LoggerFactory.getLogger(OfferBookViewModelTest.class); private User user; @BeforeEach public void setUp() { GlobalSettings.setDefaultTradeCurrency(usd); Res.setBaseCurrencyCode(usd.getCode()); Res.setBaseCurrencyName(usd.getName()); user = mock(User.class); when(user.hasPaymentAccountForCurrency(any())).thenReturn(true); } private PriceUtil getPriceUtil() { PriceFeedService priceFeedService = mock(PriceFeedService.class); TradeStatisticsManager tradeStatisticsManager = mock(TradeStatisticsManager.class); when(tradeStatisticsManager.getObservableTradeStatisticsList()).thenReturn(FXCollections.observableArrayList()); return new PriceUtil(priceFeedService, tradeStatisticsManager, empty); } @Disabled("PaymentAccountPayload needs to be set (has been changed with PB changes)") public void testIsAnyPaymentAccountValidForOffer() { Collection paymentAccounts; paymentAccounts = new ArrayList<>(FXCollections.singletonObservableList(getSepaAccount("EUR", "DE", "1212324", new ArrayList<>(Arrays.asList("AT", "DE"))))); assertTrue(PaymentAccountUtil.isAnyPaymentAccountValidForOffer( getSEPAPaymentMethod("EUR", "AT", new ArrayList<>(Arrays.asList("AT", "DE")), "PSK"), paymentAccounts)); // empty paymentAccounts paymentAccounts = new ArrayList<>(); assertFalse(PaymentAccountUtil.isAnyPaymentAccountValidForOffer(getSEPAPaymentMethod("EUR", "AT", new ArrayList<>(Arrays.asList("AT", "DE")), "PSK"), paymentAccounts)); // simple cases: same payment methods // offer: alipay paymentAccount: alipay - same country, same currency paymentAccounts = new ArrayList<>(FXCollections.singletonObservableList(getAliPayAccount("CNY"))); assertTrue(PaymentAccountUtil.isAnyPaymentAccountValidForOffer( getAliPayPaymentMethod("EUR"), paymentAccounts)); // offer: ether paymentAccount: ether - same country, same currency paymentAccounts = new ArrayList<>(FXCollections.singletonObservableList(getCryptoAccount("ETH"))); assertTrue(PaymentAccountUtil.isAnyPaymentAccountValidForOffer( getBlockChainsPaymentMethod("ETH"), paymentAccounts)); // offer: sepa paymentAccount: sepa - same country, same currency paymentAccounts = new ArrayList<>(FXCollections.singletonObservableList(getSepaAccount("EUR", "AT", "1212324", new ArrayList<>(Arrays.asList("AT", "DE"))))); assertTrue(PaymentAccountUtil.isAnyPaymentAccountValidForOffer( getSEPAPaymentMethod("EUR", "AT", new ArrayList<>(Arrays.asList("AT", "DE")), "PSK"), paymentAccounts)); // offer: nationalBank paymentAccount: nationalBank - same country, same currency paymentAccounts = new ArrayList<>(FXCollections.singletonObservableList(getNationalBankAccount("EUR", "AT", "PSK"))); assertTrue(PaymentAccountUtil.isAnyPaymentAccountValidForOffer( getNationalBankPaymentMethod("EUR", "AT", "PSK"), paymentAccounts)); // offer: SameBank paymentAccount: SameBank - same country, same currency paymentAccounts = new ArrayList<>(FXCollections.singletonObservableList(getSameBankAccount("EUR", "AT", "PSK"))); assertTrue(PaymentAccountUtil.isAnyPaymentAccountValidForOffer( getSameBankPaymentMethod("EUR", "AT", "PSK"), paymentAccounts)); // offer: sepa paymentAccount: sepa - diff. country, same currency paymentAccounts = new ArrayList<>(FXCollections.singletonObservableList(getSepaAccount("EUR", "DE", "1212324", new ArrayList<>(Arrays.asList("AT", "DE"))))); assertTrue(PaymentAccountUtil.isAnyPaymentAccountValidForOffer( getSEPAPaymentMethod("EUR", "AT", new ArrayList<>(Arrays.asList("AT", "DE")), "PSK"), paymentAccounts)); ////// // offer: sepa paymentAccount: sepa - same country, same currency paymentAccounts = new ArrayList<>(FXCollections.singletonObservableList(getSepaAccount("EUR", "AT", "1212324", new ArrayList<>(Arrays.asList("AT", "DE"))))); assertTrue(PaymentAccountUtil.isAnyPaymentAccountValidForOffer( getSEPAPaymentMethod("EUR", "AT", new ArrayList<>(Arrays.asList("AT", "DE")), "PSK"), paymentAccounts)); // offer: sepa paymentAccount: nationalBank - same country, same currency // wrong method paymentAccounts = new ArrayList<>(FXCollections.singletonObservableList(getNationalBankAccount("EUR", "AT", "PSK"))); assertFalse(PaymentAccountUtil.isAnyPaymentAccountValidForOffer( getSEPAPaymentMethod("EUR", "AT", new ArrayList<>(Arrays.asList("AT", "DE")), "PSK"), paymentAccounts)); // wrong currency paymentAccounts = new ArrayList<>(FXCollections.singletonObservableList(getNationalBankAccount("USD", "US", "XXX"))); assertFalse(PaymentAccountUtil.isAnyPaymentAccountValidForOffer( getNationalBankPaymentMethod("EUR", "AT", "PSK"), paymentAccounts)); // wrong country paymentAccounts = new ArrayList<>(FXCollections.singletonObservableList(getNationalBankAccount("EUR", "FR", "PSK"))); assertFalse(PaymentAccountUtil.isAnyPaymentAccountValidForOffer( getNationalBankPaymentMethod("EUR", "AT", "PSK"), paymentAccounts)); // sepa wrong country paymentAccounts = new ArrayList<>(FXCollections.singletonObservableList(getNationalBankAccount("EUR", "CH", "PSK"))); assertFalse(PaymentAccountUtil.isAnyPaymentAccountValidForOffer( getSEPAPaymentMethod("EUR", "AT", new ArrayList<>(Arrays.asList("AT", "DE")), "PSK"), paymentAccounts)); // sepa wrong currency paymentAccounts = new ArrayList<>(FXCollections.singletonObservableList(getNationalBankAccount("CHF", "DE", "PSK"))); assertFalse(PaymentAccountUtil.isAnyPaymentAccountValidForOffer( getSEPAPaymentMethod("EUR", "AT", new ArrayList<>(Arrays.asList("AT", "DE")), "PSK"), paymentAccounts)); // same bank paymentAccounts = new ArrayList<>(FXCollections.singletonObservableList(getSameBankAccount("EUR", "AT", "PSK"))); assertTrue(PaymentAccountUtil.isAnyPaymentAccountValidForOffer( getNationalBankPaymentMethod("EUR", "AT", "PSK"), paymentAccounts)); // not same bank paymentAccounts = new ArrayList<>(FXCollections.singletonObservableList(getSameBankAccount("EUR", "AT", "Raika"))); assertFalse(PaymentAccountUtil.isAnyPaymentAccountValidForOffer( getNationalBankPaymentMethod("EUR", "AT", "PSK"), paymentAccounts)); // same bank, wrong country paymentAccounts = new ArrayList<>(FXCollections.singletonObservableList(getSameBankAccount("EUR", "DE", "PSK"))); assertFalse(PaymentAccountUtil.isAnyPaymentAccountValidForOffer( getNationalBankPaymentMethod("EUR", "AT", "PSK"), paymentAccounts)); // same bank, wrong currency paymentAccounts = new ArrayList<>(FXCollections.singletonObservableList(getSameBankAccount("USD", "AT", "PSK"))); assertFalse(PaymentAccountUtil.isAnyPaymentAccountValidForOffer( getNationalBankPaymentMethod("EUR", "AT", "PSK"), paymentAccounts)); // spec. bank paymentAccounts = new ArrayList<>(FXCollections.singletonObservableList(getSpecificBanksAccount("EUR", "AT", "PSK", new ArrayList<>(Arrays.asList("PSK", "Raika"))))); assertTrue(PaymentAccountUtil.isAnyPaymentAccountValidForOffer( getNationalBankPaymentMethod("EUR", "AT", "PSK"), paymentAccounts)); // spec. bank, missing bank paymentAccounts = new ArrayList<>(FXCollections.singletonObservableList(getSpecificBanksAccount("EUR", "AT", "PSK", new ArrayList<>(FXCollections.singletonObservableList("Raika"))))); assertFalse(PaymentAccountUtil.isAnyPaymentAccountValidForOffer( getNationalBankPaymentMethod("EUR", "AT", "PSK"), paymentAccounts)); // spec. bank, wrong country paymentAccounts = new ArrayList<>(FXCollections.singletonObservableList(getSpecificBanksAccount("EUR", "FR", "PSK", new ArrayList<>(Arrays.asList("PSK", "Raika"))))); assertFalse(PaymentAccountUtil.isAnyPaymentAccountValidForOffer( getNationalBankPaymentMethod("EUR", "AT", "PSK"), paymentAccounts)); // spec. bank, wrong currency paymentAccounts = new ArrayList<>(FXCollections.singletonObservableList(getSpecificBanksAccount("USD", "AT", "PSK", new ArrayList<>(Arrays.asList("PSK", "Raika"))))); assertFalse(PaymentAccountUtil.isAnyPaymentAccountValidForOffer( getNationalBankPaymentMethod("EUR", "AT", "PSK"), paymentAccounts)); //TODO add more tests } @Test public void testMaxCharactersForAmountWithNoOffers() { OfferBook offerBook = mock(OfferBook.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookViewModel model = new FiatOfferBookViewModel(null, null, offerBook, empty, null, null, null, null, null, null, getPriceUtil(), null, coinFormatter, null); assertEquals(0, model.maxPlacesForAmount.intValue()); } @Test public void testMaxCharactersForAmount() { OfferBook offerBook = mock(OfferBook.class); OpenOfferManager openOfferManager = mock(OpenOfferManager.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); offerBookListItems.addAll(make(OfferBookListItemMaker.xmrBuyItem)); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookViewModel model = new FiatOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null, null, null, null, getPriceUtil(), null, coinFormatter, null); model.activate(); assertEquals(6, model.maxPlacesForAmount.intValue()); offerBookListItems.addAll(make(xmrBuyItem.but(with(amount, 20000000000000L)))); assertEquals(7, model.maxPlacesForAmount.intValue()); } @Test public void testMaxCharactersForAmountRange() { OfferBook offerBook = mock(OfferBook.class); OpenOfferManager openOfferManager = mock(OpenOfferManager.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); offerBookListItems.addAll(make(OfferBookListItemMaker.xmrItemWithRange)); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookViewModel model = new FiatOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null, null, null, null, getPriceUtil(), null, coinFormatter, null); model.activate(); assertEquals(15, model.maxPlacesForAmount.intValue()); offerBookListItems.addAll(make(xmrItemWithRange.but(with(amount, 20000000000000L)))); assertEquals(16, model.maxPlacesForAmount.intValue()); offerBookListItems.addAll(make(xmrItemWithRange.but(with(minAmount, 300000000000000L), with(amount, 300000000000000L)))); assertEquals(19, model.maxPlacesForAmount.intValue()); } @Test public void testMaxCharactersForVolumeWithNoOffers() { OfferBook offerBook = mock(OfferBook.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookViewModel model = new FiatOfferBookViewModel(null, null, offerBook, empty, null, null, null, null, null, null, getPriceUtil(), null, coinFormatter, null); assertEquals(0, model.maxPlacesForVolume.intValue()); } @Test public void testMaxCharactersForVolume() { OfferBook offerBook = mock(OfferBook.class); OpenOfferManager openOfferManager = mock(OpenOfferManager.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); offerBookListItems.addAll(make(OfferBookListItemMaker.xmrBuyItem)); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookViewModel model = new FiatOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null, null, null, null, getPriceUtil(), null, coinFormatter, null); model.activate(); assertEquals(7, model.maxPlacesForVolume.intValue()); offerBookListItems.addAll(make(xmrBuyItem.but(with(amount, 20000000000000L)))); assertEquals(9, model.maxPlacesForVolume.intValue()); } @Test public void testMaxCharactersForVolumeRange() { OfferBook offerBook = mock(OfferBook.class); OpenOfferManager openOfferManager = mock(OpenOfferManager.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); offerBookListItems.addAll(make(OfferBookListItemMaker.xmrItemWithRange)); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookViewModel model = new FiatOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null, null, null, null, getPriceUtil(), null, coinFormatter, null); model.activate(); assertEquals(9, model.maxPlacesForVolume.intValue()); offerBookListItems.addAll(make(xmrItemWithRange.but(with(amount, 200000000000000000L)))); assertEquals(11, model.maxPlacesForVolume.intValue()); offerBookListItems.addAll(make(xmrItemWithRange.but(with(minAmount, 3000000000000000000L), with(amount, 3000000000000000000L)))); assertEquals(19, model.maxPlacesForVolume.intValue()); } @Test public void testMaxCharactersForPriceWithNoOffers() { OfferBook offerBook = mock(OfferBook.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookViewModel model = new FiatOfferBookViewModel(null, null, offerBook, empty, null, null, null, null, null, null, getPriceUtil(), null, coinFormatter, null); assertEquals(0, model.maxPlacesForPrice.intValue()); } @Test public void testMaxCharactersForPrice() { OfferBook offerBook = mock(OfferBook.class); OpenOfferManager openOfferManager = mock(OpenOfferManager.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); offerBookListItems.addAll(make(OfferBookListItemMaker.xmrBuyItem)); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookViewModel model = new FiatOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null, null, null, null, getPriceUtil(), null, coinFormatter, null); model.activate(); assertEquals(9, model.maxPlacesForPrice.intValue()); offerBookListItems.addAll(make(xmrBuyItem.but(with(price, 1495582400000L)))); //149558240 assertEquals(10, model.maxPlacesForPrice.intValue()); offerBookListItems.addAll(make(xmrBuyItem.but(with(price, 149558240000L)))); //149558240 assertEquals(10, model.maxPlacesForPrice.intValue()); } @Test public void testMaxCharactersForPriceDistanceWithNoOffers() { OfferBook offerBook = mock(OfferBook.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookViewModel model = new FiatOfferBookViewModel(null, null, offerBook, empty, null, null, null, null, null, null, getPriceUtil(), null, coinFormatter, null); assertEquals(0, model.maxPlacesForMarketPriceMargin.intValue()); } @Test public void testMaxCharactersForPriceDistance() { OfferBook offerBook = mock(OfferBook.class); OpenOfferManager openOfferManager = mock(OpenOfferManager.class); PriceFeedService priceFeedService = mock(PriceFeedService.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); final Maker item = xmrBuyItem.but(with(useMarketBasedPrice, true)); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); when(priceFeedService.getMarketPrice(anyString())).thenReturn(null); when(priceFeedService.updateCounterProperty()).thenReturn(new SimpleIntegerProperty()); final OfferBookListItem item1 = make(item); assertNotNull(item1.getHashOfPayload()); item1.getOffer().setPriceFeedService(priceFeedService); final OfferBookListItem item2 = make(item.but(with(marketPriceMargin, 0.0197))); assertNotNull(item2.getHashOfPayload()); item2.getOffer().setPriceFeedService(priceFeedService); final OfferBookListItem item3 = make(item.but(with(marketPriceMargin, 0.1))); assertNotNull(item3.getHashOfPayload()); item3.getOffer().setPriceFeedService(priceFeedService); final OfferBookListItem item4 = make(item.but(with(marketPriceMargin, -0.1))); assertNotNull(item4.getHashOfPayload()); item4.getOffer().setPriceFeedService(priceFeedService); offerBookListItems.addAll(item1, item2); final OfferBookViewModel model = new FiatOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, priceFeedService, null, null, null, getPriceUtil(), null, coinFormatter, null); model.activate(); assertEquals(8, model.maxPlacesForMarketPriceMargin.intValue()); //" (1.97%)" offerBookListItems.addAll(item3); assertEquals(9, model.maxPlacesForMarketPriceMargin.intValue()); //" (10.00%)" offerBookListItems.addAll(item4); assertEquals(10, model.maxPlacesForMarketPriceMargin.intValue()); //" (-10.00%)" } @Test public void testGetPrice() { OfferBook offerBook = mock(OfferBook.class); OpenOfferManager openOfferManager = mock(OpenOfferManager.class); PriceFeedService priceFeedService = mock(PriceFeedService.class); final ObservableList offerBookListItems = FXCollections.observableArrayList(); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); when(priceFeedService.getMarketPrice(anyString())).thenReturn(new MarketPrice("USD", 12684.0450, Instant.now().getEpochSecond(), true)); final OfferBookViewModel model = new FiatOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null, null, null, null, getPriceUtil(), null, coinFormatter, null); final OfferBookListItem item = make(xmrBuyItem.but( with(useMarketBasedPrice, true), with(marketPriceMargin, -0.12))); assertNotNull(item.getHashOfPayload()); final OfferBookListItem lowItem = make(xmrBuyItem.but( with(useMarketBasedPrice, true), with(marketPriceMargin, 0.01))); assertNotNull(lowItem.getHashOfPayload()); final OfferBookListItem fixedItem = make(xmrBuyItem); assertNotNull(fixedItem.getHashOfPayload()); item.getOffer().setPriceFeedService(priceFeedService); lowItem.getOffer().setPriceFeedService(priceFeedService); offerBookListItems.addAll(lowItem, fixedItem); model.activate(); assertEquals("12557.2046", model.getPrice(lowItem)); assertEquals("(1.00%)", model.getPriceAsPercentage(lowItem)); assertEquals("1000.0000", model.getPrice(fixedItem)); offerBookListItems.addAll(item); assertEquals("14206.1304", model.getPrice(item)); assertEquals("(-12.00%)", model.getPriceAsPercentage(item)); assertEquals("12557.2046", model.getPrice(lowItem)); assertEquals("(1.00%)", model.getPriceAsPercentage(lowItem)); } private PaymentAccount getAliPayAccount(String currencyCode) { PaymentAccount paymentAccount = new AliPayAccount(); paymentAccount.setSelectedTradeCurrency(new TraditionalCurrency(currencyCode)); return paymentAccount; } private PaymentAccount getCryptoAccount(String currencyCode) { PaymentAccount paymentAccount = new CryptoCurrencyAccount(); paymentAccount.addCurrency(new CryptoCurrency(currencyCode, null)); return paymentAccount; } private PaymentAccount getSepaAccount(String currencyCode, String countryCode, String bic, ArrayList countryCodes) { CountryBasedPaymentAccount paymentAccount = new SepaAccount(); paymentAccount.setSingleTradeCurrency(new TraditionalCurrency(currencyCode)); paymentAccount.setCountry(new Country(countryCode, null, null)); ((SepaAccountPayload) paymentAccount.getPaymentAccountPayload()).setBic(bic); countryCodes.forEach(((SepaAccountPayload) paymentAccount.getPaymentAccountPayload())::addAcceptedCountry); return paymentAccount; } private PaymentAccount getNationalBankAccount(String currencyCode, String countryCode, String bankId) { CountryBasedPaymentAccount paymentAccount = new NationalBankAccount(); paymentAccount.setSingleTradeCurrency(new TraditionalCurrency(currencyCode)); paymentAccount.setCountry(new Country(countryCode, null, null)); ((NationalBankAccountPayload) paymentAccount.getPaymentAccountPayload()).setBankId(bankId); return paymentAccount; } private PaymentAccount getSameBankAccount(String currencyCode, String countryCode, String bankId) { SameBankAccount paymentAccount = new SameBankAccount(); paymentAccount.setSingleTradeCurrency(new TraditionalCurrency(currencyCode)); paymentAccount.setCountry(new Country(countryCode, null, null)); ((SameBankAccountPayload) paymentAccount.getPaymentAccountPayload()).setBankId(bankId); return paymentAccount; } private PaymentAccount getSpecificBanksAccount(String currencyCode, String countryCode, String bankId, ArrayList bankIds) { SpecificBanksAccount paymentAccount = new SpecificBanksAccount(); paymentAccount.setSingleTradeCurrency(new TraditionalCurrency(currencyCode)); paymentAccount.setCountry(new Country(countryCode, null, null)); ((SpecificBanksAccountPayload) paymentAccount.getPaymentAccountPayload()).setBankId(bankId); bankIds.forEach(((SpecificBanksAccountPayload) paymentAccount.getPaymentAccountPayload())::addAcceptedBank); return paymentAccount; } private Offer getBlockChainsPaymentMethod(String currencyCode) { return getOffer(currencyCode, PaymentMethod.BLOCK_CHAINS_ID, null, null, null, null); } private Offer getAliPayPaymentMethod(String currencyCode) { return getOffer(currencyCode, PaymentMethod.ALI_PAY_ID, null, null, null, null); } private Offer getSEPAPaymentMethod(String currencyCode, String countryCode, ArrayList countryCodes, String bankId) { return getPaymentMethod(currencyCode, PaymentMethod.SEPA_ID, countryCode, countryCodes, bankId, null); } private Offer getNationalBankPaymentMethod(String currencyCode, String countryCode, String bankId) { return getPaymentMethod(currencyCode, PaymentMethod.NATIONAL_BANK_ID, countryCode, new ArrayList<>(FXCollections.singletonObservableList(countryCode)), bankId, null); } private Offer getSameBankPaymentMethod(String currencyCode, String countryCode, String bankId) { return getPaymentMethod(currencyCode, PaymentMethod.SAME_BANK_ID, countryCode, new ArrayList<>(FXCollections.singletonObservableList(countryCode)), bankId, new ArrayList<>(FXCollections.singletonObservableList(bankId))); } private Offer getSpecificBanksPaymentMethod(String currencyCode, String countryCode, String bankId, ArrayList bankIds) { return getPaymentMethod(currencyCode, PaymentMethod.SPECIFIC_BANKS_ID, countryCode, new ArrayList<>(FXCollections.singletonObservableList(countryCode)), bankId, bankIds); } private Offer getPaymentMethod(String currencyCode, String paymentMethodId, String countryCode, ArrayList countryCodes, String bankId, ArrayList bankIds) { return getOffer(currencyCode, paymentMethodId, countryCode, countryCodes, bankId, bankIds); } private Offer getOffer(String tradeCurrencyCode, String paymentMethodId, String countryCode, ArrayList acceptedCountryCodes, String bankId, ArrayList acceptedBanks) { return new Offer(new OfferPayload(null, 0, null, null, null, 0, 0, false, 0, 0, 0, 0, 0, 0, 0, "BTC", tradeCurrencyCode, paymentMethodId, null, countryCode, acceptedCountryCodes, bankId, acceptedBanks, null, 0, 0, 0, false, false, 0, 0, false, null, null, 0, null, null, null, null)); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/main/overlays/OverlayTest.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertThrows; public class OverlayTest { @Test public void typeSafeCreation() { new A(); new C(); new D<>(); } @Test public void typeUnsafeCreation() { assertThrows(RuntimeException.class, () -> new B()); } private static class A extends Overlay { } private static class B extends Overlay { } private static class C extends TabbedOverlay { } private static class D extends Overlay> { } } ================================================ FILE: desktop/src/test/java/haveno/desktop/main/overlays/windows/downloadupdate/HavenoInstallerTest.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows.downloadupdate; import com.google.common.collect.Lists; import haveno.desktop.main.overlays.windows.downloadupdate.HavenoInstaller.FileDescriptor; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import java.io.File; import java.net.URL; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; @Slf4j public class HavenoInstallerTest { @Test public void call() throws Exception { } @Test public void verifySignature() throws Exception { URL url = this.getClass().getResource("/downloadUpdate/test.txt"); File dataFile = new File(url.toURI().getPath()); url = this.getClass().getResource("/downloadUpdate/test.txt.asc"); File sigFile = new File(url.toURI().getPath()); url = this.getClass().getResource("/downloadUpdate/F379A1C6.asc"); File pubKeyFile = new File(url.toURI().getPath()); assertEquals(HavenoInstaller.VerifyStatusEnum.OK, HavenoInstaller.verifySignature(pubKeyFile, sigFile, dataFile)); url = this.getClass().getResource("/downloadUpdate/test_bad.txt"); dataFile = new File(url.toURI().getPath()); url = this.getClass().getResource("/downloadUpdate/test_bad.txt.asc"); sigFile = new File(url.toURI().getPath()); url = this.getClass().getResource("/downloadUpdate/F379A1C6.asc"); pubKeyFile = new File(url.toURI().getPath()); HavenoInstaller.verifySignature(pubKeyFile, sigFile, dataFile); assertEquals(HavenoInstaller.VerifyStatusEnum.FAIL, HavenoInstaller.verifySignature(pubKeyFile, sigFile, dataFile)); } @Test public void getFileName() throws Exception { } @Test public void getDownloadType() throws Exception { } @Test public void getIndex() throws Exception { } @Test public void getSigFileDescriptors() throws Exception { HavenoInstaller havenoInstaller = new HavenoInstaller(); FileDescriptor installerFileDescriptor = FileDescriptor.builder().fileName("filename.txt").id("filename").loadUrl("url://filename.txt").build(); FileDescriptor key1 = FileDescriptor.builder().fileName("key1").id("key1").loadUrl("").build(); FileDescriptor key2 = FileDescriptor.builder().fileName("key2").id("key2").loadUrl("").build(); List sigFileDescriptors = havenoInstaller.getSigFileDescriptors(installerFileDescriptor, Lists.newArrayList(key1)); assertEquals(1, sigFileDescriptors.size()); sigFileDescriptors = havenoInstaller.getSigFileDescriptors(installerFileDescriptor, Lists.newArrayList(key1, key2)); assertEquals(2, sigFileDescriptors.size()); log.info("test"); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/main/overlays/windows/downloadupdate/VerifyTaskTest.java ================================================ /* * 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 . */ package haveno.desktop.main.overlays.windows.downloadupdate; import org.junit.jupiter.api.Test; public class VerifyTaskTest { @Test public void call() throws Exception { } } ================================================ FILE: desktop/src/test/java/haveno/desktop/main/settings/preferences/PreferencesViewModelTest.java ================================================ /* * 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 . */ package haveno.desktop.main.settings.preferences; import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.support.dispute.mediation.mediator.Mediator; import haveno.core.support.dispute.mediation.mediator.MediatorManager; import haveno.core.user.Preferences; import haveno.desktop.maker.PreferenceMakers; import haveno.network.p2p.NodeAddress; import javafx.collections.FXCollections; import javafx.collections.ObservableMap; import org.junit.jupiter.api.Test; import java.util.ArrayList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class PreferencesViewModelTest { @Test public void getArbitrationLanguages() { ArbitratorManager arbitratorAgentManager = mock(ArbitratorManager.class); final ObservableMap arbitrators = FXCollections.observableHashMap(); ArrayList languagesOne = new ArrayList<>() {{ add("en"); add("de"); }}; ArrayList languagesTwo = new ArrayList<>() {{ add("en"); add("es"); }}; Arbitrator one = new Arbitrator(new NodeAddress("refundAgent:1"), null, languagesOne, 0L, null, null, null, null, null); Arbitrator two = new Arbitrator(new NodeAddress("refundAgent:2"), null, languagesTwo, 0L, null, null, null, null, null); arbitrators.put(one.getNodeAddress(), one); arbitrators.put(two.getNodeAddress(), two); Preferences preferences = PreferenceMakers.empty; when(arbitratorAgentManager.getObservableMap()).thenReturn(arbitrators); PreferencesViewModel model = new PreferencesViewModel(preferences, arbitratorAgentManager, null); assertEquals("English, Deutsch, español", model.getArbitrationLanguages()); } @Test public void getMediationLanguages() { MediatorManager mediationManager = mock(MediatorManager.class); final ObservableMap mnediators = FXCollections.observableHashMap(); ArrayList languagesOne = new ArrayList<>() {{ add("en"); add("de"); }}; ArrayList languagesTwo = new ArrayList<>() {{ add("en"); add("es"); }}; Mediator one = new Mediator(new NodeAddress("refundAgent:1"), null, languagesOne, 0L, null, null, null, null, null); Mediator two = new Mediator(new NodeAddress("refundAgent:2"), null, languagesTwo, 0L, null, null, null, null, null); mnediators.put(one.getNodeAddress(), one); mnediators.put(two.getNodeAddress(), two); Preferences preferences = PreferenceMakers.empty; when(mediationManager.getObservableMap()).thenReturn(mnediators); PreferencesViewModel model = new PreferencesViewModel(preferences, null, mediationManager); assertEquals("English, Deutsch, español", model.getMediationLanguages()); } @Test public void needsSupportLanguageWarning_forNotSupportedLanguageInArbitration() { MediatorManager mediationManager = mock(MediatorManager.class); ArbitratorManager arbitratorManager = mock(ArbitratorManager.class); Preferences preferences = PreferenceMakers.empty; when(arbitratorManager.isAgentAvailableForLanguage(preferences.getUserLanguage())).thenReturn(false); PreferencesViewModel model = new PreferencesViewModel(preferences, arbitratorManager, mediationManager); assertTrue(model.needsSupportLanguageWarning()); } @Test public void needsSupportLanguageWarning_forNotSupportedLanguageInMediation() { MediatorManager mediationManager = mock(MediatorManager.class); ArbitratorManager arbitratorManager = mock(ArbitratorManager.class); Preferences preferences = PreferenceMakers.empty; when(arbitratorManager.isAgentAvailableForLanguage(preferences.getUserLanguage())).thenReturn(true); when(mediationManager.isAgentAvailableForLanguage(preferences.getUserLanguage())).thenReturn(false); PreferencesViewModel model = new PreferencesViewModel(preferences, arbitratorManager, mediationManager); assertTrue(model.needsSupportLanguageWarning()); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/maker/CurrencyListItemMakers.java ================================================ /* * 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 . */ package haveno.desktop.maker; import com.natpryce.makeiteasy.Instantiator; import com.natpryce.makeiteasy.Maker; import com.natpryce.makeiteasy.Property; import haveno.core.locale.TradeCurrency; import haveno.desktop.util.CurrencyListItem; import static com.natpryce.makeiteasy.MakeItEasy.a; import static com.natpryce.makeiteasy.MakeItEasy.with; import static haveno.desktop.maker.TradeCurrencyMakers.euro; import static haveno.desktop.maker.TradeCurrencyMakers.monero; public class CurrencyListItemMakers { public static final Property tradeCurrency = new Property<>(); public static final Property numberOfTrades = new Property<>(); public static final Instantiator CurrencyListItem = lookup -> new CurrencyListItem(lookup.valueOf(tradeCurrency, monero), lookup.valueOf(numberOfTrades, 0)); public static final Maker bitcoinItem = a(CurrencyListItem); public static final Maker euroItem = a(CurrencyListItem, with(tradeCurrency, euro)); } ================================================ FILE: desktop/src/test/java/haveno/desktop/maker/OfferMaker.java ================================================ /* * 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 . */ package haveno.desktop.maker; import com.natpryce.makeiteasy.Instantiator; import com.natpryce.makeiteasy.Maker; import com.natpryce.makeiteasy.Property; import haveno.common.crypto.Encryption; import haveno.common.crypto.PubKeyRing; import haveno.common.crypto.Sig; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OfferPayload; import haveno.network.p2p.NodeAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import static com.natpryce.makeiteasy.MakeItEasy.a; import static com.natpryce.makeiteasy.MakeItEasy.with; import static com.natpryce.makeiteasy.Property.newProperty; import static java.lang.System.currentTimeMillis; import static java.net.InetAddress.getLocalHost; @SuppressWarnings("InstantiationOfUtilityClass") public class OfferMaker { public static final Property id = newProperty(); public static final Property paymentMethodId = newProperty(); public static final Property paymentAccountId = newProperty(); public static final Property countryCode = newProperty(); public static final Property> countryCodes = newProperty(); public static final Property date = newProperty(); public static final Property price = newProperty(); public static final Property minAmount = newProperty(); public static final Property amount = newProperty(); public static final Property baseCurrencyCode = newProperty(); public static final Property counterCurrencyCode = newProperty(); public static final Property direction = newProperty(); public static final Property useMarketBasedPrice = newProperty(); public static final Property marketPriceMargin = newProperty(); public static final Property nodeAddress = newProperty(); public static final Property> nodeAddresses = newProperty(); public static final Property pubKeyRing = newProperty(); public static final Property blockHeight = newProperty(); public static final Property txFee = newProperty(); public static final Property makerFeePct = newProperty(); public static final Property takerFeePct = newProperty(); public static final Property penaltyFeePct = newProperty(); public static final Property buyerSecurityDepositPct = newProperty(); public static final Property sellerSecurityDepositPct = newProperty(); public static final Property tradeLimit = newProperty(); public static final Property maxTradePeriod = newProperty(); public static final Property lowerClosePrice = newProperty(); public static final Property upperClosePrice = newProperty(); public static final Property protocolVersion = newProperty(); public static final Instantiator Offer = lookup -> new Offer( new OfferPayload(lookup.valueOf(id, "1234"), lookup.valueOf(date, currentTimeMillis()), lookup.valueOf(nodeAddress, getLocalHostNodeWithPort(10000)), lookup.valueOf(pubKeyRing, genPubKeyRing()), lookup.valueOf(direction, OfferDirection.BUY), lookup.valueOf(price, 100000L), lookup.valueOf(marketPriceMargin, 0.0), lookup.valueOf(useMarketBasedPrice, false), lookup.valueOf(amount, 100000000000L), lookup.valueOf(minAmount, 100000000000L), lookup.valueOf(makerFeePct, .0015), lookup.valueOf(takerFeePct, .0075), lookup.valueOf(penaltyFeePct, 0.03), lookup.valueOf(buyerSecurityDepositPct, .15), lookup.valueOf(sellerSecurityDepositPct, .15), lookup.valueOf(baseCurrencyCode, "XMR"), lookup.valueOf(counterCurrencyCode, "USD"), lookup.valueOf(paymentMethodId, "SEPA"), lookup.valueOf(paymentAccountId, "00002c4d-1ffc-4208-8ff3-e669817b0000"), lookup.valueOf(countryCode, "FR"), lookup.valueOf(countryCodes, new ArrayList<>() {{ add("FR"); }}), null, null, "3", lookup.valueOf(blockHeight, 700000L), lookup.valueOf(tradeLimit, 0L), lookup.valueOf(maxTradePeriod, 0L), false, false, lookup.valueOf(lowerClosePrice, 0L), lookup.valueOf(upperClosePrice, 0L), false, null, null, lookup.valueOf(protocolVersion, 0), getLocalHostNodeWithPort(99999), null, null, null)); public static final Maker xmrUsdOffer = a(Offer); public static final Maker btcBCHCOffer = a(Offer).but(with(counterCurrencyCode, "BCHC")); static NodeAddress getLocalHostNodeWithPort(int port) { try { return new NodeAddress(getLocalHost().getHostAddress(), port); } catch (UnknownHostException ex) { throw new IllegalStateException(ex); } } static PubKeyRing genPubKeyRing() { return new PubKeyRing(Sig.generateKeyPair().getPublic(), Encryption.generateKeyPair().getPublic()); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/maker/PreferenceMakers.java ================================================ /* * 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 . */ package haveno.desktop.maker; import com.natpryce.makeiteasy.Instantiator; import com.natpryce.makeiteasy.Property; import com.natpryce.makeiteasy.SameValueDonor; import haveno.common.config.Config; import haveno.common.persistence.PersistenceManager; import haveno.core.api.XmrLocalNode; import haveno.core.user.Preferences; import static com.natpryce.makeiteasy.MakeItEasy.a; import static com.natpryce.makeiteasy.MakeItEasy.make; public class PreferenceMakers { public static final Property storage = new Property<>(); public static final Property config = new Property<>(); public static final Property xmrLocalNode = new Property<>(); public static final Property useTorFlagFromOptions = new Property<>(); public static final Property referralID = new Property<>(); public static final Instantiator Preferences = lookup -> new Preferences( lookup.valueOf(storage, new SameValueDonor(null)), lookup.valueOf(config, new SameValueDonor(null)), lookup.valueOf(useTorFlagFromOptions, new SameValueDonor(null)), null ); public static final Preferences empty = make(a(Preferences)); } ================================================ FILE: desktop/src/test/java/haveno/desktop/maker/PriceMaker.java ================================================ /* * 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 . */ package haveno.desktop.maker; import com.natpryce.makeiteasy.Instantiator; import com.natpryce.makeiteasy.Maker; import com.natpryce.makeiteasy.Property; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.Price; import haveno.core.monetary.TraditionalMoney; import static com.natpryce.makeiteasy.MakeItEasy.a; public class PriceMaker { public static final Property currencyCode = new Property<>(); public static final Property priceString = new Property<>(); public static final Instantiator TraditionalMoneyPrice = lookup -> new Price(TraditionalMoney.parseTraditionalMoney(lookup.valueOf(currencyCode, "USD"), lookup.valueOf(priceString, "100"))); public static final Instantiator CryptoPrice = lookup -> new Price(CryptoMoney.parseCrypto(lookup.valueOf(currencyCode, "LTC"), lookup.valueOf(priceString, "100"))); public static final Maker usdPrice = a(TraditionalMoneyPrice); public static final Maker ltcPrice = a(CryptoPrice); } ================================================ FILE: desktop/src/test/java/haveno/desktop/maker/TradeCurrencyMakers.java ================================================ /* * 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 . */ package haveno.desktop.maker; import com.natpryce.makeiteasy.Instantiator; import com.natpryce.makeiteasy.Property; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import static com.natpryce.makeiteasy.MakeItEasy.a; import static com.natpryce.makeiteasy.MakeItEasy.make; import static com.natpryce.makeiteasy.MakeItEasy.with; public class TradeCurrencyMakers { public static final Property currencyCode = new Property<>(); public static final Property currencyName = new Property<>(); public static final Instantiator CryptoCurrency = lookup -> new CryptoCurrency(lookup.valueOf(currencyCode, "XMR"), lookup.valueOf(currencyName, "Monero")); public static final Instantiator TraditionalCurrency = lookup -> new TraditionalCurrency(lookup.valueOf(currencyCode, "EUR")); public static final CryptoCurrency monero = make(a(CryptoCurrency)); public static final TraditionalCurrency euro = make(a(TraditionalCurrency)); public static final TraditionalCurrency usd = make(a(TraditionalCurrency).but(with(currencyCode, "USD"))); } ================================================ FILE: desktop/src/test/java/haveno/desktop/maker/VolumeMaker.java ================================================ /* * 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 . */ package haveno.desktop.maker; import com.natpryce.makeiteasy.Instantiator; import com.natpryce.makeiteasy.Maker; import com.natpryce.makeiteasy.Property; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.TraditionalMoney; import haveno.core.monetary.Volume; import static com.natpryce.makeiteasy.MakeItEasy.a; public class VolumeMaker { public static final Property currencyCode = new Property<>(); public static final Property volumeString = new Property<>(); public static final Instantiator TraditionalMoneyVolume = lookup -> new Volume(TraditionalMoney.parseTraditionalMoney(lookup.valueOf(currencyCode, "USD"), lookup.valueOf(volumeString, "100"))); public static final Instantiator CryptoVolume = lookup -> new Volume(CryptoMoney.parseCrypto(lookup.valueOf(currencyCode, "LTC"), lookup.valueOf(volumeString, "100"))); public static final Maker usdVolume = a(TraditionalMoneyVolume); public static final Maker ltcVolume = a(CryptoVolume); } ================================================ FILE: desktop/src/test/java/haveno/desktop/util/CurrencyListTest.java ================================================ /* * 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 . */ package haveno.desktop.util; import com.google.common.collect.Lists; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.user.Preferences; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.Currency; import java.util.List; import java.util.Locale; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class CurrencyListTest { private static final Locale locale = new Locale("en", "US"); private static final TradeCurrency USD = new TraditionalCurrency(Currency.getInstance("USD"), locale); private static final TradeCurrency RUR = new TraditionalCurrency(Currency.getInstance("RUR"), locale); private static final TradeCurrency BTC = new CryptoCurrency("BTC", "Bitcoin"); private static final TradeCurrency ETH = new CryptoCurrency("ETH", "Ether"); private Preferences preferences; private List delegate; private CurrencyList testedEntity; @BeforeEach public void setUp() { Locale.setDefault(locale); CurrencyPredicates predicates = mock(CurrencyPredicates.class); when(predicates.isCryptoCurrency(USD)).thenReturn(false); when(predicates.isCryptoCurrency(RUR)).thenReturn(false); when(predicates.isCryptoCurrency(BTC)).thenReturn(true); when(predicates.isCryptoCurrency(ETH)).thenReturn(true); when(predicates.isTraditionalCurrency(USD)).thenReturn(true); when(predicates.isTraditionalCurrency(RUR)).thenReturn(true); when(predicates.isTraditionalCurrency(BTC)).thenReturn(false); when(predicates.isTraditionalCurrency(ETH)).thenReturn(false); this.preferences = mock(Preferences.class); this.delegate = new ArrayList<>(); this.testedEntity = new CurrencyList(delegate, preferences, predicates); } @Test public void testUpdateWhenSortNumerically() { when(preferences.isSortMarketCurrenciesNumerically()).thenReturn(true); List currencies = Lists.newArrayList(USD, RUR, USD, ETH, ETH, BTC); testedEntity.updateWithCurrencies(currencies, null); List expected = Lists.newArrayList( new CurrencyListItem(USD, 2), new CurrencyListItem(RUR, 1), new CurrencyListItem(ETH, 2), new CurrencyListItem(BTC, 1)); assertEquals(expected, delegate); } @Test public void testUpdateWhenNotSortNumerically() { when(preferences.isSortMarketCurrenciesNumerically()).thenReturn(false); List currencies = Lists.newArrayList(USD, RUR, USD, ETH, ETH, BTC); testedEntity.updateWithCurrencies(currencies, null); List expected = Lists.newArrayList( new CurrencyListItem(RUR, 1), new CurrencyListItem(USD, 2), new CurrencyListItem(BTC, 1), new CurrencyListItem(ETH, 2)); assertEquals(expected, delegate); } @Test public void testUpdateWhenSortNumericallyAndFirstSpecified() { when(preferences.isSortMarketCurrenciesNumerically()).thenReturn(true); List currencies = Lists.newArrayList(USD, RUR, USD, ETH, ETH, BTC); CurrencyListItem first = new CurrencyListItem(BTC, 5); testedEntity.updateWithCurrencies(currencies, first); List expected = Lists.newArrayList( first, new CurrencyListItem(USD, 2), new CurrencyListItem(RUR, 1), new CurrencyListItem(ETH, 2), new CurrencyListItem(BTC, 1)); assertEquals(expected, delegate); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/util/DisplayUtilsTest.java ================================================ package haveno.desktop.util; import haveno.common.config.Config; import haveno.core.locale.GlobalSettings; import haveno.core.locale.Res; import haveno.core.monetary.Volume; import haveno.core.offer.Offer; import haveno.core.offer.OfferPayload; import haveno.core.util.VolumeUtil; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.coin.ImmutableCoinFormatter; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.math.BigInteger; import java.util.Locale; import java.util.concurrent.TimeUnit; import static com.natpryce.makeiteasy.MakeItEasy.make; import static com.natpryce.makeiteasy.MakeItEasy.with; import static haveno.desktop.maker.OfferMaker.xmrUsdOffer; import static haveno.desktop.maker.VolumeMaker.usdVolume; import static haveno.desktop.maker.VolumeMaker.volumeString; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class DisplayUtilsTest { private final CoinFormatter formatter = new ImmutableCoinFormatter(Config.baseCurrencyNetworkParameters().getMonetaryFormat()); @BeforeEach public void setUp() { Locale.setDefault(Locale.US); GlobalSettings.setLocale(Locale.US); Res.setBaseCurrencyCode("XMR"); Res.setBaseCurrencyName("Monero"); } @Test public void testFormatAccountAge() { assertEquals("0 days", DisplayUtils.formatAccountAge(TimeUnit.HOURS.toMillis(23))); assertEquals("0 days", DisplayUtils.formatAccountAge(0)); assertEquals("0 days", DisplayUtils.formatAccountAge(-1)); assertEquals("1 day", DisplayUtils.formatAccountAge(TimeUnit.DAYS.toMillis(1))); assertEquals("2 days", DisplayUtils.formatAccountAge(TimeUnit.DAYS.toMillis(2))); assertEquals("30 days", DisplayUtils.formatAccountAge(TimeUnit.DAYS.toMillis(30))); assertEquals("60 days", DisplayUtils.formatAccountAge(TimeUnit.DAYS.toMillis(60))); } @Test public void testFormatVolume() { assertEquals("1", VolumeUtil.formatVolume(make(xmrUsdOffer), true, 4)); assertEquals("100", VolumeUtil.formatVolume(make(usdVolume))); assertEquals("1775", VolumeUtil.formatVolume(make(usdVolume.but(with(volumeString, "1774.62"))))); } @Test public void testFormatSameVolume() { Offer offer = mock(Offer.class); Volume xmr = Volume.parse("0.10", "XMR"); when(offer.getMinVolume()).thenReturn(xmr); when(offer.getVolume()).thenReturn(xmr); assertEquals("0.10000000", VolumeUtil.formatVolume(offer.getVolume())); } @Test public void testFormatDifferentVolume() { Offer offer = mock(Offer.class); Volume xmrMin = Volume.parse("0.10", "XMR"); Volume xmrMax = Volume.parse("0.25", "XMR"); when(offer.isRange()).thenReturn(true); when(offer.getMinVolume()).thenReturn(xmrMin); when(offer.getVolume()).thenReturn(xmrMax); assertEquals("0.10000000 - 0.25000000", VolumeUtil.formatVolume(offer, false, 0)); } @Test public void testFormatNullVolume() { Offer offer = mock(Offer.class); when(offer.getMinVolume()).thenReturn(null); when(offer.getVolume()).thenReturn(null); assertEquals("", VolumeUtil.formatVolume(offer.getVolume())); } @Test public void testFormatSameAmount() { Offer offer = mock(Offer.class); when(offer.getMinAmount()).thenReturn(BigInteger.valueOf(100000000000L)); when(offer.getAmount()).thenReturn(BigInteger.valueOf(100000000000L)); assertEquals("0.10", DisplayUtils.formatAmount(offer, formatter)); } @Test public void testFormatDifferentAmount() { OfferPayload offerPayload = mock(OfferPayload.class); Offer offer = new Offer(offerPayload); when(offerPayload.getMinAmount()).thenReturn(100000000000L); when(offerPayload.getAmount()).thenReturn(200000000000L); assertEquals("0.10 - 0.20", DisplayUtils.formatAmount(offer, formatter)); } @Test public void testFormatAmountWithAlignmenWithDecimals() { OfferPayload offerPayload = mock(OfferPayload.class); Offer offer = new Offer(offerPayload); when(offerPayload.getMinAmount()).thenReturn(100000000000L); when(offerPayload.getAmount()).thenReturn(200000000000L); assertEquals("0.1000 - 0.2000", DisplayUtils.formatAmount(offer, 4, true, 15, formatter)); } @Test public void testFormatAmountWithAlignmenWithDecimalsNoRange() { OfferPayload offerPayload = mock(OfferPayload.class); Offer offer = new Offer(offerPayload); when(offerPayload.getMinAmount()).thenReturn(100000000000L); when(offerPayload.getAmount()).thenReturn(100000000000L); assertEquals("0.1000", DisplayUtils.formatAmount(offer, 4, true, 15, formatter)); } @Test public void testFormatNullAmount() { Offer offer = mock(Offer.class); when(offer.getMinAmount()).thenReturn(null); when(offer.getAmount()).thenReturn(null); assertEquals("", DisplayUtils.formatAmount(offer, formatter)); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/util/GUIUtilTest.java ================================================ /* * 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 . */ package haveno.desktop.util; import haveno.core.locale.GlobalSettings; import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; import haveno.core.user.DontShowAgainLookup; import haveno.core.user.Preferences; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.math.BigInteger; import java.util.Locale; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class GUIUtilTest { @BeforeEach public void setup() { Locale.setDefault(new Locale("en", "US")); GlobalSettings.setLocale(new Locale("en", "US")); Res.setBaseCurrencyCode("BTC"); Res.setBaseCurrencyName("Bitcoin"); } @Test public void testOpenURLWithCampaignParameters() { Preferences preferences = mock(Preferences.class); DontShowAgainLookup.setPreferences(preferences); GUIUtil.setPreferences(preferences); when(preferences.showAgain("warnOpenURLWhenTorEnabled")).thenReturn(false); when(preferences.getUserLanguage()).thenReturn("en"); /* PowerMockito.mockStatic(Utilities.class); ArgumentCaptor captor = ArgumentCaptor.forClass(URI.class); PowerMockito.doNothing().when(Utilities.class, "openURI", captor.capture()); GUIUtil.openWebPage("https://haveno.exchange"); assertEquals("https://haveno.exchange?utm_source=desktop-client&utm_medium=in-app-link&utm_campaign=language_en", captor.getValue().toString()); GUIUtil.openWebPage("https://docs.haveno.exchange/trading-rules.html#f2f-trading"); assertEquals("https://docs.haveno.exchange/trading-rules.html?utm_source=desktop-client&utm_medium=in-app-link&utm_campaign=language_en#f2f-trading", captor.getValue().toString()); */ } @Test public void testOpenURLWithoutCampaignParameters() { Preferences preferences = mock(Preferences.class); DontShowAgainLookup.setPreferences(preferences); GUIUtil.setPreferences(preferences); when(preferences.showAgain("warnOpenURLWhenTorEnabled")).thenReturn(false); /* PowerMockito.mockStatic(Utilities.class); ArgumentCaptor captor = ArgumentCaptor.forClass(URI.class); PowerMockito.doNothing().when(Utilities.class, "openURI", captor.capture()); GUIUtil.openWebPage("https://www.github.com"); assertEquals("https://www.github.com", captor.getValue().toString()); */ } @Test public void percentageOfTradeAmount1() { BigInteger fee = BigInteger.valueOf(200000000L); assertEquals(" (0.02% of trade amount)", GUIUtil.getPercentageOfTradeAmount(fee, HavenoUtils.xmrToAtomicUnits(1.0))); } @Test public void percentageOfTradeAmount2() { BigInteger fee = BigInteger.valueOf(100000000L); assertEquals(" (0.01% of trade amount)", GUIUtil.getPercentageOfTradeAmount(fee, HavenoUtils.xmrToAtomicUnits(1.0))); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/util/ImmutableCoinFormatterTest.java ================================================ /* * 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 . */ package haveno.desktop.util; import haveno.common.config.Config; import haveno.core.locale.Res; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.coin.ImmutableCoinFormatter; import org.bitcoinj.core.CoinMaker; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.Locale; import static com.natpryce.makeiteasy.MakeItEasy.a; import static com.natpryce.makeiteasy.MakeItEasy.make; import static com.natpryce.makeiteasy.MakeItEasy.with; import static org.bitcoinj.core.CoinMaker.oneBitcoin; import static org.bitcoinj.core.CoinMaker.satoshis; import static org.junit.jupiter.api.Assertions.assertEquals; public class ImmutableCoinFormatterTest { private final CoinFormatter formatter = new ImmutableCoinFormatter(Config.baseCurrencyNetworkParameters().getMonetaryFormat()); @BeforeEach public void setUp() { Locale.setDefault(new Locale("en", "US")); Res.setBaseCurrencyCode("XMR"); Res.setBaseCurrencyName("Monero"); } @Test public void testFormatCoin() { assertEquals("1.00", formatter.formatCoin(oneBitcoin)); assertEquals("1.0000", formatter.formatCoin(oneBitcoin, 4)); assertEquals("1.00", formatter.formatCoin(oneBitcoin, 5)); assertEquals("0.000001", formatter.formatCoin(make(a(CoinMaker.Coin).but(with(satoshis, 100L))))); assertEquals("0.00000001", formatter.formatCoin(make(a(CoinMaker.Coin).but(with(satoshis, 1L))))); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/util/MovingAverageUtilsTest.java ================================================ /* * 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 . */ package haveno.desktop.util; import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertThrows; public class MovingAverageUtilsTest { private static final int NAN = -99; private static int[] calcMA(int period, int[] input) { System.out.println("Input:"); System.out.println(Arrays.toString(input)); Stream streamInput = Arrays .stream(input) .boxed() .map(x -> x == NAN ? Double.NaN : x); int[] output = MovingAverageUtils .simpleMovingAverage(streamInput, period) .mapToInt(x -> Double.isFinite(x) ? (int) Math.round(x) : NAN) .toArray(); System.out.println("Output:"); System.out.println(Arrays.toString(output)); return output; } private static void testMA(int period, int[] input, int[] expected) { var output = calcMA(period, input); assertArrayEquals(output, expected); } @Test public void normalConditions() { testMA( 2, new int[]{10, 20, 30, 40}, new int[]{NAN, 15, 25, 35} ); } @Test public void inputContainsNaNs0() { testMA( 2, new int[]{NAN, 20, 30, 40}, new int[]{NAN, NAN, 25, 35} ); } @Test public void inputContainsNaNs1() { testMA( 2, new int[]{10, NAN, 30, 40}, new int[]{NAN, NAN, NAN, 35} ); } @Test public void inputContainsNaNs2() { testMA( 2, new int[]{10, NAN, NAN, 40}, new int[]{NAN, NAN, NAN, NAN} ); } @Test public void inputContainsNaNs3() { testMA( 2, new int[]{10, NAN, 30, NAN, 40}, new int[]{NAN, NAN, NAN, NAN, NAN} ); } @Test public void nonsensicalPeriod0() { testMA( 1, new int[]{10, 20}, new int[]{10, 20} ); } @Test public void nonsensicalPeriod1() { assertThrows(IllegalArgumentException.class, () -> { var impossible = new int[]{}; testMA( 0, new int[]{10, 20}, impossible ); }); } @Test public void nonsensicalPeriod2() { assertThrows(IllegalArgumentException.class, () -> { var impossible = new int[]{}; testMA( -1, new int[]{10, 20}, impossible ); }); } @Test public void tooLittleData0() { testMA( 3, new int[]{}, new int[]{NAN, NAN} ); } @Test public void tooLittleData1() { testMA( 3, new int[]{10}, new int[]{NAN, NAN} ); } @Test public void tooLittleData2() { testMA( 3, new int[]{10, 20}, new int[]{NAN, NAN} ); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/util/validation/AccountNrValidatorTest.java ================================================ package haveno.desktop.util.validation; import haveno.core.locale.Res; import haveno.core.payment.validation.AccountNrValidator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.Locale; import static org.junit.jupiter.api.Assertions.assertTrue; public class AccountNrValidatorTest { @BeforeEach public void setup() { Locale.setDefault(new Locale("en", "US")); Res.setBaseCurrencyCode("XMR"); Res.setBaseCurrencyName("Monero"); } @Test public void testValidationForArgentina() { AccountNrValidator validator = new AccountNrValidator("AR"); assertTrue(validator.validate("4009041813520").isValid); assertTrue(validator.validate("035-005198/5").isValid); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/util/validation/AdvancedCashValidatorTest.java ================================================ package haveno.desktop.util.validation; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.payment.validation.AdvancedCashValidator; import haveno.core.payment.validation.EmailValidator; import haveno.core.util.validation.RegexValidator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class AdvancedCashValidatorTest { @BeforeEach public void setup() { final BaseCurrencyNetwork baseCurrencyNetwork = Config.baseCurrencyNetwork(); final String currencyCode = baseCurrencyNetwork.getCurrencyCode(); Res.setBaseCurrencyCode(currencyCode); Res.setBaseCurrencyName(baseCurrencyNetwork.getCurrencyName()); CurrencyUtil.setBaseCurrencyCode(currencyCode); } @Test public void validate(){ AdvancedCashValidator validator = new AdvancedCashValidator( new EmailValidator(), new RegexValidator() ); assertTrue(validator.validate("U123456789012").isValid); assertTrue(validator.validate("test@user.com").isValid); assertFalse(validator.validate("").isValid); assertFalse(validator.validate(null).isValid); assertFalse(validator.validate("123456789012").isValid); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/util/validation/BranchIdValidatorTest.java ================================================ package haveno.desktop.util.validation; import haveno.core.locale.Res; import haveno.core.payment.validation.BranchIdValidator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.Locale; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class BranchIdValidatorTest { @BeforeEach public void setup() { Locale.setDefault(new Locale("en", "US")); Res.setBaseCurrencyCode("XMR"); Res.setBaseCurrencyName("Monero"); } @Test public void testValidationForArgentina() { BranchIdValidator validator = new BranchIdValidator("AR"); assertTrue(validator.validate("0590").isValid); assertFalse(validator.validate("05901").isValid); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/util/validation/CapitualValidatorTest.java ================================================ package haveno.desktop.util.validation; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.payment.validation.CapitualValidator; import haveno.core.util.validation.RegexValidator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class CapitualValidatorTest { @BeforeEach public void setup() { final BaseCurrencyNetwork baseCurrencyNetwork = Config.baseCurrencyNetwork(); final String currencyCode = baseCurrencyNetwork.getCurrencyCode(); Res.setBaseCurrencyCode(currencyCode); Res.setBaseCurrencyName(baseCurrencyNetwork.getCurrencyName()); CurrencyUtil.setBaseCurrencyCode(currencyCode); } @Test public void validate() { CapitualValidator validator = new CapitualValidator( new RegexValidator() ); assertTrue(validator.validate("CAP-123456").isValid); assertTrue(validator.validate("CAP-XXXXXX").isValid); assertTrue(validator.validate("CAP-123XXX").isValid); assertFalse(validator.validate("").isValid); assertFalse(validator.validate(null).isValid); assertFalse(validator.validate("123456").isValid); assertFalse(validator.validate("XXXXXX").isValid); assertFalse(validator.validate("123XXX").isValid); assertFalse(validator.validate("12XXX").isValid); assertFalse(validator.validate("CAP-12XXX").isValid); assertFalse(validator.validate("CA-12XXXx").isValid); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/util/validation/FiatVolumeValidatorTest.java ================================================ /* * 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 . */ package haveno.desktop.util.validation; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.payment.validation.FiatVolumeValidator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class FiatVolumeValidatorTest { @BeforeEach public void setup() { final BaseCurrencyNetwork baseCurrencyNetwork = Config.baseCurrencyNetwork(); final String currencyCode = baseCurrencyNetwork.getCurrencyCode(); Res.setBaseCurrencyCode(currencyCode); Res.setBaseCurrencyName(baseCurrencyNetwork.getCurrencyName()); CurrencyUtil.setBaseCurrencyCode(currencyCode); } @Test public void testValidate() { FiatVolumeValidator validator = new FiatVolumeValidator(); assertTrue(validator.validate("1").isValid); assertTrue(validator.validate("1,1").isValid); assertTrue(validator.validate("1.1").isValid); assertTrue(validator.validate(",1").isValid); assertTrue(validator.validate(".1").isValid); assertTrue(validator.validate("0.01").isValid); assertTrue(validator.validate("1000000.00").isValid); assertTrue(validator.validate(String.valueOf(validator.getMinValue())).isValid); assertTrue(validator.validate(String.valueOf(validator.getMaxValue())).isValid); assertFalse(validator.validate(null).isValid); assertFalse(validator.validate("").isValid); assertFalse(validator.validate("a").isValid); assertFalse(validator.validate("2a").isValid); assertFalse(validator.validate("a2").isValid); assertFalse(validator.validate("0").isValid); assertFalse(validator.validate("-1").isValid); assertFalse(validator.validate("0.0").isValid); assertFalse(validator.validate("0,1,1").isValid); assertFalse(validator.validate("0.1.1").isValid); assertFalse(validator.validate("1,000.1").isValid); assertFalse(validator.validate("1.000,1").isValid); assertFalse(validator.validate("0.009").isValid); assertFalse(validator.validate(String.valueOf(validator.getMinValue() - 1)).isValid); assertFalse(validator.validate(String.valueOf(Double.MIN_VALUE)).isValid); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/util/validation/InteracETransferAnswerValidatorTest.java ================================================ /* * 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 . */ package haveno.desktop.util.validation; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.payment.validation.InteracETransferAnswerValidator; import haveno.core.payment.validation.LengthValidator; import haveno.core.util.validation.RegexValidator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class InteracETransferAnswerValidatorTest { @BeforeEach public void setup() { final BaseCurrencyNetwork baseCurrencyNetwork = Config.baseCurrencyNetwork(); final String currencyCode = baseCurrencyNetwork.getCurrencyCode(); Res.setBaseCurrencyCode(currencyCode); Res.setBaseCurrencyName(baseCurrencyNetwork.getCurrencyName()); CurrencyUtil.setBaseCurrencyCode(currencyCode); } @Test public void validate() throws Exception { InteracETransferAnswerValidator validator = new InteracETransferAnswerValidator(new LengthValidator(), new RegexValidator()); assertTrue(validator.validate("abcdefghijklmnopqrstuvwxy").isValid); assertTrue(validator.validate("ABCDEFGHIJKLMNOPQRSTUVWXY").isValid); assertTrue(validator.validate("1234567890").isValid); assertTrue(validator.validate("zZ-").isValid); assertFalse(validator.validate(null).isValid); // null assertFalse(validator.validate("").isValid); // empty assertFalse(validator.validate("two words").isValid); // two words assertFalse(validator.validate("ab").isValid); // too short assertFalse(validator.validate("abcdefghijklmnopqrstuvwxyz").isValid); // too long assertFalse(validator.validate("abc !@#").isValid); // invalid characters } } ================================================ FILE: desktop/src/test/java/haveno/desktop/util/validation/InteracETransferQuestionValidatorTest.java ================================================ /* * 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 . */ package haveno.desktop.util.validation; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.payment.validation.InteracETransferQuestionValidator; import haveno.core.payment.validation.LengthValidator; import haveno.core.util.validation.RegexValidator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class InteracETransferQuestionValidatorTest { @BeforeEach public void setup() { final BaseCurrencyNetwork baseCurrencyNetwork = Config.baseCurrencyNetwork(); final String currencyCode = baseCurrencyNetwork.getCurrencyCode(); Res.setBaseCurrencyCode(currencyCode); Res.setBaseCurrencyName(baseCurrencyNetwork.getCurrencyName()); CurrencyUtil.setBaseCurrencyCode(currencyCode); } @Test public void validate() throws Exception { InteracETransferQuestionValidator validator = new InteracETransferQuestionValidator(new LengthValidator(), new RegexValidator()); assertTrue(validator.validate("abcdefghijklmnopqrstuvwxyz").isValid); assertTrue(validator.validate("ABCDEFGHIJKLMNOPQRSTUVWXYZ").isValid); assertTrue(validator.validate("1234567890").isValid); assertTrue(validator.validate("' _ , . ? -").isValid); assertTrue(validator.validate("what is 2-1?").isValid); assertFalse(validator.validate(null).isValid); // null assertFalse(validator.validate("").isValid); // empty assertFalse(validator.validate("abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ").isValid); // too long assertFalse(validator.validate("abc !@#").isValid); // invalid characters } } ================================================ FILE: desktop/src/test/java/haveno/desktop/util/validation/InteracETransferValidatorTest.java ================================================ /* * 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 . */ package haveno.desktop.util.validation; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.payment.validation.EmailValidator; import haveno.core.payment.validation.InteracETransferAnswerValidator; import haveno.core.payment.validation.InteracETransferQuestionValidator; import haveno.core.payment.validation.InteracETransferValidator; import haveno.core.payment.validation.LengthValidator; import haveno.core.util.validation.RegexValidator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class InteracETransferValidatorTest { @BeforeEach public void setup() { final BaseCurrencyNetwork baseCurrencyNetwork = Config.baseCurrencyNetwork(); final String currencyCode = baseCurrencyNetwork.getCurrencyCode(); Res.setBaseCurrencyCode(currencyCode); Res.setBaseCurrencyName(baseCurrencyNetwork.getCurrencyName()); CurrencyUtil.setBaseCurrencyCode(currencyCode); } @Test public void validate() throws Exception { InteracETransferValidator validator = new InteracETransferValidator( new EmailValidator(), new InteracETransferQuestionValidator(new LengthValidator(), new RegexValidator()), new InteracETransferAnswerValidator(new LengthValidator(), new RegexValidator()) ); assertTrue(validator.validate("name@domain.tld").isValid); assertTrue(validator.validate("n1.n2@c.dd").isValid); assertTrue(validator.validate("+1 236 123-4567").isValid); assertTrue(validator.validate("15061234567").isValid); assertTrue(validator.validate("1 289 784 2134").isValid); assertTrue(validator.validate("+1-514-654-7412").isValid); assertFalse(validator.validate("abc@.de").isValid); // Domain name missing assertFalse(validator.validate("abc@d.e").isValid); // TLD too short assertFalse(validator.validate("2361234567").isValid); // Prefix for North America missing (often required for local calls as well) assertFalse(validator.validate("+150612345678").isValid); // Too long assertFalse(validator.validate("1289784213").isValid); // Too short assertFalse(validator.validate("+1 555 123-4567").isValid); // Non-Canadian area code assertFalse(validator.validate("+1 236 1234-567").isValid); // Wrong grouping } } ================================================ FILE: desktop/src/test/java/haveno/desktop/util/validation/LengthValidatorTest.java ================================================ /* * 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 . */ package haveno.desktop.util.validation; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.payment.validation.LengthValidator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class LengthValidatorTest { @BeforeEach public void setup() { final BaseCurrencyNetwork baseCurrencyNetwork = Config.baseCurrencyNetwork(); final String currencyCode = baseCurrencyNetwork.getCurrencyCode(); Res.setBaseCurrencyCode(currencyCode); Res.setBaseCurrencyName(baseCurrencyNetwork.getCurrencyName()); CurrencyUtil.setBaseCurrencyCode(currencyCode); } @Test public void validate() throws Exception { LengthValidator validator = new LengthValidator(); assertTrue(validator.validate("").isValid); assertTrue(validator.validate(null).isValid); assertTrue(validator.validate("123456789").isValid); validator.setMinLength(2); validator.setMaxLength(5); assertTrue(validator.validate("12").isValid); assertTrue(validator.validate("12345").isValid); assertFalse(validator.validate("1").isValid); // too short assertFalse(validator.validate("").isValid); // too short assertFalse(validator.validate(null).isValid); // too short assertFalse(validator.validate("123456789").isValid); // too long LengthValidator validator2 = new LengthValidator(2, 5); assertTrue(validator2.validate("12").isValid); assertTrue(validator2.validate("12345").isValid); assertFalse(validator2.validate("1").isValid); // too short assertFalse(validator2.validate("").isValid); // too short assertFalse(validator2.validate(null).isValid); // too short assertFalse(validator2.validate("123456789").isValid); // too long } } ================================================ FILE: desktop/src/test/java/haveno/desktop/util/validation/NationalAccountIdValidatorTest.java ================================================ package haveno.desktop.util.validation; import haveno.core.locale.Res; import haveno.core.payment.validation.NationalAccountIdValidator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.Locale; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class NationalAccountIdValidatorTest { @BeforeEach public void setup() { Locale.setDefault(new Locale("en", "US")); Res.setBaseCurrencyCode("XMR"); Res.setBaseCurrencyName("Monero"); } @Test public void testValidationForArgentina(){ NationalAccountIdValidator validator = new NationalAccountIdValidator("AR"); assertTrue(validator.validate("2850590940090418135201").isValid); final String wrongNationalAccountId = "285059094009041813520"; assertFalse(validator.validate(wrongNationalAccountId).isValid); assertEquals("CBU number must consist of 22 numbers.", validator.validate(wrongNationalAccountId).errorMessage); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/util/validation/PhoneNumberValidatorTest.java ================================================ package haveno.desktop.util.validation; import haveno.core.locale.Res; import haveno.core.payment.validation.PhoneNumberValidator; import haveno.core.util.validation.InputValidator.ValidationResult; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; public class PhoneNumberValidatorTest { private PhoneNumberValidator validator; private ValidationResult validationResult; @BeforeEach public void setup() { Res.setup(); } @Test public void testMissingCountryCode() { validator = new PhoneNumberValidator(); validationResult = validator.validate("+12124567890"); assertFalse(validationResult.isValid, "Should not be valid if validator's country code is missing"); assertEquals(Res.get("validation.phone.missingCountryCode"), validationResult.errorMessage); assertNull(validator.getNormalizedPhoneNumber()); } @Test public void testNoInput() { validator = new PhoneNumberValidator("AT"); validationResult = validator.validate(""); assertFalse(validationResult.isValid, "'' should not be a valid number in AT"); assertEquals(Res.get("validation.empty"), validationResult.errorMessage); assertNull(validator.getNormalizedPhoneNumber()); validationResult = validator.validate(null); assertFalse(validationResult.isValid, "'' should not be a valid number in AT"); assertEquals(Res.get("validation.empty"), validationResult.errorMessage); assertNull(validator.getNormalizedPhoneNumber()); } @Test public void testAustriaNumbers() { validator = new PhoneNumberValidator("AT"); assertEquals(validator.getCallingCode(), "43"); validationResult = validator.validate("(0316) 214 4366"); assertTrue(validationResult.isValid); assertEquals("+4303162144366", validator.getNormalizedPhoneNumber()); // 1 Vienna validationResult = validator.validate("+43 1 214-3512"); assertTrue(validationResult.isValid); assertEquals("+4312143512", validator.getNormalizedPhoneNumber()); // 1 Vienna cell validationResult = validator.validate("+43 1 650 454 0987"); assertTrue(validationResult.isValid); assertEquals("+4316504540987", validator.getNormalizedPhoneNumber()); // 676 T-Mobile validationResult = validator.validate("0676 2241647"); assertTrue(validationResult.isValid); assertEquals("+4306762241647", validator.getNormalizedPhoneNumber()); } @Test public void testInvalidAustriaNumbers() { validator = new PhoneNumberValidator("AT"); // AT country code is +43 validationResult = validator.validate("+43 1 214"); assertFalse(validationResult.isValid, "+43 1 214 should not be a valid number in AT"); assertEquals(Res.get("validation.phone.insufficientDigits", "+43 1 214"), validationResult.errorMessage); assertNull(validator.getNormalizedPhoneNumber()); validationResult = validator.validate("+42 1 650 454 0987"); assertFalse(validationResult.isValid, "+42 1 650 454 0987 should not be a valid number in AT"); assertEquals(Res.get("validation.phone.invalidDialingCode", "+42 1 650 454 0987", "AT", validator.getCallingCode()), validationResult.errorMessage); assertNull(validator.getNormalizedPhoneNumber()); } @Test public void testDominicanRepublicNumbers() { validator = new PhoneNumberValidator("DO"); assertEquals(validator.getCallingCode(), "1"); validationResult = validator.validate("1829-123 4567"); assertTrue(validationResult.isValid); assertEquals("+18291234567", validator.getNormalizedPhoneNumber()); validationResult = validator.validate("+18091234567"); assertTrue(validationResult.isValid); assertEquals("+18091234567", validator.getNormalizedPhoneNumber()); validationResult = validator.validate("+1 (849) 543-0098"); assertTrue(validationResult.isValid); assertEquals("+18495430098", validator.getNormalizedPhoneNumber()); } @Test public void testBrazilNumbers() { validator = new PhoneNumberValidator("BR"); assertEquals(validator.getCallingCode(), "55"); validationResult = validator.validate("11 3333 5555"); assertTrue(validationResult.isValid); assertEquals("+551133335555", validator.getNormalizedPhoneNumber()); // Sao Paulo cell validationResult = validator.validate("55 11 9 3444 2567"); assertTrue(validationResult.isValid); assertEquals("+5511934442567", validator.getNormalizedPhoneNumber()); // 85 Fortaleza landline using long-distance carrier OI (31) validationResult = validator.validate("031 85 4433 8432"); assertTrue(validationResult.isValid); assertEquals("+550318544338432", validator.getNormalizedPhoneNumber()); } @Test public void testCanadaNumbers() { validator = new PhoneNumberValidator("CA"); assertEquals(validator.getCallingCode(), "1"); validationResult = validator.validate("867 374-8299"); assertTrue(validationResult.isValid); assertEquals("+18673748299", validator.getNormalizedPhoneNumber()); validationResult = validator.validate("+1 306-374-8299 "); assertTrue(validationResult.isValid); assertEquals("+13063748299", validator.getNormalizedPhoneNumber()); validationResult = validator.validate(" (709) 374-8299"); assertTrue(validationResult.isValid); assertEquals("+17093748299", validator.getNormalizedPhoneNumber()); } @Test public void testInvalidCanadaNumber() { validator = new PhoneNumberValidator("CA"); validationResult = validator.validate("+2 1 650 454 0987"); assertFalse(validationResult.isValid, "+2 1 650 454 0987 should not be a valid number in CA"); assertEquals(Res.get("validation.phone.invalidDialingCode", "+2 1 650 454 0987", "CA", validator.getCallingCode()), validationResult.errorMessage); assertNull(validator.getNormalizedPhoneNumber()); } @Test public void testChinaNumbers() { validator = new PhoneNumberValidator("CN"); assertEquals(validator.getCallingCode(), "86"); // In major cities, landline-numbers consist of a two-digit area code followed // by an eight-digit inner-number. In other places, landline-numbers consist of // a three-digit area code followed by a seven- or eight-digit inner-number. // The numbers of mobile phones consist of eleven digits. Hard to validate. // 10 Beijing validationResult = validator.validate(" 10 4534 8214"); assertTrue(validationResult.isValid); assertEquals("+861045348214", validator.getNormalizedPhoneNumber()); // 21 Shanghai validationResult = validator.validate("+86 (21) 3422-5814"); assertTrue(validationResult.isValid); assertEquals("+862134225814", validator.getNormalizedPhoneNumber()); // 18x Mobile phone numbers have 11 digits in the format 1xx-xxxx-xxxx, // in which the first three digits (e.g. 13x, 14x,15x,17x and 18x) designate // the mobile phone service provider. validationResult = validator.validate("180-4353-7877"); // no country code prefix assertTrue(validationResult.isValid); assertEquals("+8618043537877", validator.getNormalizedPhoneNumber()); } @Test public void testJapanNumbers() { validator = new PhoneNumberValidator("JP"); assertEquals(validator.getCallingCode(), "81"); // 11 Sapporo 011-XXX-XXXX validationResult = validator.validate("11 367-2345"); assertTrue(validationResult.isValid); assertEquals("+81113672345", validator.getNormalizedPhoneNumber()); // 0476 Narita, 0476-XX-XXXX validationResult = validator.validate("0476 87 2055"); assertTrue(validationResult.isValid); assertEquals("+810476872055", validator.getNormalizedPhoneNumber()); // 03 Tokyo 03-XXXX-XXXX validationResult = validator.validate("03-3129-5367"); assertTrue(validationResult.isValid); assertEquals("+810331295367", validator.getNormalizedPhoneNumber()); // 090 Cell number (area code) validationResult = validator.validate("(090) 3129-5367"); assertTrue(validationResult.isValid); assertEquals("+8109031295367", validator.getNormalizedPhoneNumber()); } @Test public void testRussiaNumbers() { validator = new PhoneNumberValidator("RU"); assertEquals(validator.getCallingCode(), "7"); // Moscow has four area codes: 495, 496, 498 and 499 validationResult = validator.validate("499 345-99-36"); assertTrue(validationResult.isValid); assertEquals("+74993459936", validator.getNormalizedPhoneNumber()); // 812 St. Peter validationResult = validator.validate("+7 812 567-22-11"); assertTrue(validationResult.isValid); assertEquals("+78125672211", validator.getNormalizedPhoneNumber()); // 395 Irkutsk Oblast Call from outside RU validationResult = validator.validate("+7 395 232-88-35"); assertTrue(validationResult.isValid); assertEquals("+73952328835", validator.getNormalizedPhoneNumber()); } @Test public void testUKNumber() { validator = new PhoneNumberValidator("GB"); assertEquals(validator.getCallingCode(), "44"); validationResult = validator.validate("020 7946 0230"); assertTrue(validationResult.isValid); assertEquals("+4402079460230", validator.getNormalizedPhoneNumber()); validationResult = validator.validate("+ 44 20 79 46 00 93"); assertTrue(validationResult.isValid); assertEquals("+442079460093", validator.getNormalizedPhoneNumber()); // (0114) Sheffield validationResult = validator.validate("(0114) 436 8888"); assertTrue(validationResult.isValid); assertEquals("+4401144368888", validator.getNormalizedPhoneNumber()); validationResult = validator.validate("+44 11 44368888"); assertTrue(validationResult.isValid); assertEquals("+441144368888", validator.getNormalizedPhoneNumber()); // (0131) xxx xxxx Edinburgh validationResult = validator.validate("(0131) 267 1111"); assertTrue(validationResult.isValid); assertEquals("+4401312671111", validator.getNormalizedPhoneNumber()); validationResult = validator.validate("131 267-1111"); assertTrue(validationResult.isValid); assertEquals("+441312671111", validator.getNormalizedPhoneNumber()); } @Test public void testUSNumber() { validator = new PhoneNumberValidator("US"); assertEquals(validator.getCallingCode(), "1"); validationResult = validator.validate("(800) 253 0000"); assertTrue(validationResult.isValid); assertEquals("+18002530000", validator.getNormalizedPhoneNumber()); validationResult = validator.validate("+ 1 800 253-0000"); assertTrue(validationResult.isValid); assertEquals("+18002530000", validator.getNormalizedPhoneNumber()); validationResult = validator.validate("8002530000"); assertTrue(validationResult.isValid); assertEquals("+18002530000", validator.getNormalizedPhoneNumber()); } @Test public void testInvalidUSNumbers() { validator = new PhoneNumberValidator("US"); validationResult = validator.validate("+1 512 GR8 0150"); assertFalse(validationResult.isValid, "+1 512 GR8 0150 should not be a valid number in US"); assertEquals(Res.get("validation.phone.invalidCharacters", "+1 512 GR8 0150", "US", validator.getCallingCode()), validationResult.errorMessage); assertNull(validator.getNormalizedPhoneNumber()); validationResult = validator.validate("+1 212-3456-0150-9832"); assertFalse(validationResult.isValid, "+1 212-3456-0150-9832 should not be a valid number in US"); assertEquals(Res.get("validation.phone.tooManyDigits", "+1 212-3456-0150-9832", "US", validator.getCallingCode()), validationResult.errorMessage); assertNull(validator.getNormalizedPhoneNumber()); } @Test public void testUSAreaCodeMatchesCallingCode() { // These are not valid US numbers because these area codes // do not exist, but validating all area codes on the globe // is probably too expensive. We have to trust end users // to input correct area/region codes. validator = new PhoneNumberValidator("US"); assertEquals(validator.getCallingCode(), "1"); validationResult = validator.validate("1 (1) 253 0000"); assertTrue(validationResult.isValid); assertEquals("+112530000", validator.getNormalizedPhoneNumber()); validationResult = validator.validate("1-120-253 0000"); assertTrue(validationResult.isValid); assertEquals("+11202530000", validator.getNormalizedPhoneNumber()); validationResult = validator.validate("(120) 253 0000"); assertTrue(validationResult.isValid); // TODO validator incorrectly treats input as if it were +1 (202) 53-0000 /// assertEquals("+1202530000", validator.getNormalizedPhoneNumber()); } } ================================================ FILE: desktop/src/test/java/haveno/desktop/util/validation/RegexValidatorTest.java ================================================ /* * 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 . */ package haveno.desktop.util.validation; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.util.validation.RegexValidator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class RegexValidatorTest { @BeforeEach public void setup() { final BaseCurrencyNetwork baseCurrencyNetwork = Config.baseCurrencyNetwork(); final String currencyCode = baseCurrencyNetwork.getCurrencyCode(); Res.setBaseCurrencyCode(currencyCode); Res.setBaseCurrencyName(baseCurrencyNetwork.getCurrencyName()); CurrencyUtil.setBaseCurrencyCode(currencyCode); } @Test public void validate() throws Exception { RegexValidator validator = new RegexValidator(); assertTrue(validator.validate("").isValid); assertTrue(validator.validate(null).isValid); assertTrue(validator.validate("123456789").isValid); validator.setPattern("[a-z]*"); assertTrue(validator.validate("abcdefghijklmnopqrstuvwxyz").isValid); assertTrue(validator.validate("").isValid); assertTrue(validator.validate(null).isValid); assertFalse(validator.validate("123").isValid); // invalid assertFalse(validator.validate("ABC").isValid); // invalid validator.setPattern("[a-z]+"); assertTrue(validator.validate("abcdefghijklmnopqrstuvwxyz").isValid); assertFalse(validator.validate("123").isValid); // invalid assertFalse(validator.validate("ABC").isValid); // invalid assertFalse(validator.validate("").isValid); // invalid assertFalse(validator.validate(null).isValid); // invalid } } ================================================ FILE: desktop/src/test/java/haveno/desktop/util/validation/XmrValidatorTest.java ================================================ /* * 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 . */ package haveno.desktop.util.validation; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.payment.validation.XmrValidator; import haveno.core.trade.HavenoUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class XmrValidatorTest { @BeforeEach public void setup() { final BaseCurrencyNetwork baseCurrencyNetwork = Config.baseCurrencyNetwork(); final String currencyCode = baseCurrencyNetwork.getCurrencyCode(); Res.setBaseCurrencyCode(currencyCode); Res.setBaseCurrencyName(baseCurrencyNetwork.getCurrencyName()); CurrencyUtil.setBaseCurrencyCode(currencyCode); } @Test public void testIsValid() { XmrValidator validator = new XmrValidator(); assertTrue(validator.validate("1").isValid); assertTrue(validator.validate("0,1").isValid); assertTrue(validator.validate("0.1").isValid); assertTrue(validator.validate(",1").isValid); assertTrue(validator.validate(".1").isValid); assertTrue(validator.validate("0.12345678").isValid); assertTrue(validator.validate("0.0000001").isValid); validator.setMinValue(HavenoUtils.xmrToAtomicUnits(0.0001)); assertFalse(validator.validate("0.0000001").isValid); // below minimum assertFalse(validator.validate(null).isValid); assertFalse(validator.validate("").isValid); assertFalse(validator.validate("0").isValid); assertFalse(validator.validate("0.0").isValid); assertFalse(validator.validate("0,1,1").isValid); assertFalse(validator.validate("0.1.1").isValid); assertFalse(validator.validate("0,000.1").isValid); assertFalse(validator.validate("0.000,1").isValid); assertFalse(validator.validate("0.123456789123456").isValid); assertFalse(validator.validate("-1").isValid); // assertFalse(validator.validate(NetworkParameters.MAX_MONEY.toPlainString()).isValid); } } ================================================ FILE: desktop/src/test/java/org/bitcoinj/core/CoinMaker.java ================================================ /* * 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 . */ package org.bitcoinj.core; import com.natpryce.makeiteasy.Instantiator; import com.natpryce.makeiteasy.Property; import static com.natpryce.makeiteasy.MakeItEasy.a; import static com.natpryce.makeiteasy.MakeItEasy.make; import static org.bitcoinj.core.Coin.valueOf; public class CoinMaker { public static final Property satoshis = new Property<>(); public static final Instantiator Coin = lookup -> valueOf(lookup.valueOf(satoshis, 100000000L)); public static final Coin oneBitcoin = make(a(Coin)); } ================================================ FILE: desktop/src/test/resources/downloadUpdate/F379A1C6.asc ================================================ -----BEGIN PGP PUBLIC KEY BLOCK----- mQINBFWK4uEBEADjSnRHU294auU1BPH+50OvsWnIvMb6kzqRdY3xlxecRAMsC/Dh XyKVvY+wtC2a/1R+Cj5VO/geEDt0WBbwqj/zAi+x8ttrzZDn5CxmWvU6ulFCFKAr cmB/eZmBMQSJ/JSZw1DeD090/tafuYUDjfhcqE1ajh7WxSIbMudaAm5yd/AuHB3c +mlr5fjBwtBN1nyjfi9N3f7XJS8GrdJFC43/1FWHS3Z+GHydLkIcLS1keT5fYJbe VZGC/RzUJBxqN6UFxIRJhPIplyBFfQBpWIFFxZNr6VZWeQlGnFjX1v3//hmD7mnT 3aGqqkUFcI5q7De3nNm2wfVnV50bzqj+FiSZWUUpWvgD01uzxWxzCVERn8s1jana jLt3hfS8ly5kx311oZTyhXDR5z5LsrOjJv7U+hwhtDHAI0yyD7LPWCYFK2jwljYV Tli8KHchMOlV0Yxm62ebmO/orju4Rq+T4id2nfwJGimRY/DX+k7/1qSHdyjnoYn1 qqpVWD0UhjNLf337PThr20nA/FD3hjwnmIT5becHzrPGbRnr3Y2s77LFUe+nfGE3 wvQmmpSNccFIz/146lynxJHWMfSqOJMgJZWpSPFKd39BhxxP9g5Sou6wEnM+YWYT eOI1dGPejA4EHZec7s3j7hcx33rejydmsjW8yJjkRaFxYJk4jaoT7LgGiQARAQAB tCVNYW5mcmVkIEthcnJlciA8bWFuZnJlZEBiaXRzcXVhcmUuaW8+iQI9BBMBCgAn BQJViuLhAhsDBQkHhh+ABQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJEPW4RDbz eaHGLFIQAM/w8BBoZ7+K4hxZmjkt/lXDXddHg9jvfU3jgIR+CkThcHy7n1e+QvsG k0FozYsFyCaLwOiGR0/eUcUu+aegwRnx+eh/scElcAN40RYIr2nCU5vNGqmKBrP2 ShQu0z5CcqFoccIHZ7VSe42SYb3GJ8g2mtC1Um+ryytZtF0g7nJxGWe//4YmqavC TV4KU5akJGfFVPDW84qJJo34gwg80oshxnOQnXfoa+xlmSDQMaOTYH07cR9N64JW TUad4aTl7niFZPizzg9r4ltRwUzvyXD5CHQoKqGWvZO0pHvRRq5SHp5nDoKh5hZs lb/QJu6X8bTlLwhpOLsXPBPqoXQibRiICfAdVPBYnHMtvJ7RcuZyazvpYYjlgYsK kol+jUude4zAIEky1A/77wl1pBXURw8vk8CRPIqAAOniaTySk6b24fseYsMcmP1Z VLt4HxV0njBRAe51DV8AiT/gscTdg8/GsJRjzKedCs0SZIjBg5/1iULXRtQZrUd7 vkOZZCSRzMAf4iHGK0qFuUoEkoZacv+bfOfwse62F89ngM1iB9RdyPPZIuh70i8O Ebzzs6TBptq9WtV5LEXtkkHyfCugoIKegdKZmZBxnNT+XZMQQ68E8jeTiUA8MaPR hYJp2FL+DLsTcLHt+ffHmcYJ1+6/v8UMIx6wC2k862+h4Y1aBme9iQEiBBABCgAM BQJVwoK3BYMHhh+AAAoJEAxMpdRX1mvaFmYH/jx1ayv+6zNlYsFaL3idIBCmWQb7 Lu4qE8bhSGN557jc7HoYT1DAYubm4zV65KxMVs+AsrNoy9Q4mXxpzIsi3X/J/3qF L7hF+ZiZf8ms0FNScFW4rrWJpWaZ03zf97hx7D08oBxMtn++hA0/Ur7YN6fLtOe4 sv19D8U6UvjT1LsDIpDXDUuLNTAcpq5liOGL9PHa3przvekuVIROgosGbdfY34KO v9PfyL2H7Q6np7awjBE3GsbIMcEp+JsFgE8M3GJzke/absuqeNHpCgpIWPMoWCXb guKOsVipIBuRNhaRG4hRFAQAUYRe2UL9ZH6BBKfAZoOkgYP+Kv01XGhADXGJAiIE EAEKAAwFAlXCgr8FgweGH4AACgkQQBJQlmprLEaYSw//ccfvWZGxvi4R0EHM61pD +Jp0iTdMb+8L1lK6+pzVaQvPf06UWD9qjN79cWDI0HmVFVgFPE0qRbsIi33s7ltF Gc3Dw7ql2R7q1XS06pkT8ihesdYgauNA0802js5/RJK3joZEujNAQkz33O4daECf MWEVia0JrFZktUlwVTjKOzKyoBlUiV/Rg/ivnTRVXyfDIp7qCUHcIhz19U4zK0kh NKkgVxddKIeyivmUghzQbYEkAZlvBfLRXvnnK1TdouOgLOvHetf7LQDKpgHxJmre o8XjHrF08/mDfKRvqh8Vi/j5zj8Kyy7LIjF3rHzCJDjwDp4tgSDEekMgEzYLoiyn /y6lCxS78m+/EkCdE2Hncb81n6fgldQTSCChpfUbvqQAuewb9wonQp3gtqIdEwg1 WS4T78m1qNfFP9I3UYKWRdSplifJAhr2NAyaf9fSNVSGRZk5sDcaXRrraPj5DHR6 kgDITkv1ph6sB+4cu/6XmoZ8ZWAmmQFz4/EejlBu4khZHVPCojtGmbyOAAmV8M+a 4zWPXxdtOlKUCZpa7jOPkto4V804K5FsOn3qascdLdd/VYtjPWE/qoWs96/J81w5 SJIXZ0s3j/EWwRKtcxZO22x0IXDIA5oPY7gQC1JDT/dt1blbGQ9nCLIPL5QoxxAF noxMOtoVHlC98rjnPgCtuACJASIEEAEKAAwFAlXCgsYFgweGH4AACgkQeiIM+js9 3FtGugf+JPu6S1RNVbgv8n1cX9Krt3JnXi0ybzhlCILxe8lRqzk9OXsVzY1Zvnor 0L35jYa2Y8GSEgivKEZXcdJroCXmBJRWs3ck3SSevxmqDm1+nQ96TBFtno6m8hf4 3UoT0YnuryGffV0XEyO/m7ujIj5iF8UvWC6d+ve/oQw815IJROZNBjgRn6bhpgnq sWVWioSQg9Jzqs/h8rjFWrscbln/mBCxyn6PsjIO6N1ArBcB9s95iCxiz6MXiMrl vGbd6KIsaaG6H9IXfCFcOXkN+1pfr6439LRZMxC1hqHEqLWPV8iCwPyFJkHJUeyg 8hYuIFeEIvG4Z9ukEKrd27sh7eCgN4kCHAQQAQgABgUCVulMPQAKCRA97MEF9d0j ghURD/4p+kGPeqQZq4sq4v7wxYPLnihTIdD1rZXOtWa3wnVOf5o03MGpXaaQIyez LRgF1FSgkAV0v1kOJcUVOwZNXfivFAz5b5dV5cX8X/8AFc798gOQ2BpKDs8Gh4Vh V+aV9Zslac0QLKA8LKmJOlVCb+GpQKmwPZ8IFfr+NtMhRW/h1WSualLHYpmmfH0A GCnDM00w1pgGavtcTjIrvihA0uw4ySFT6QzI1z+1zmZlPsdpZEbpAeTyrGecDIRj FAOTsmbe2YOk6kzj3xhwL8hMjtfX4EZl9KW1bGR6/fy8fVaM8lHi0Pa4BgIeca+M ir+kHw7G1FgHrjiqOUuvCuK9uln1We0DttIi7RB1lYmX+Ds7XfSKj/OcrHWwxtJk phKofIyGt9b12tqjJKdyS+81FBjgsiUSGQJdThm2vefVKKMqc91OlGPg8q3804x2 3BlOHg98pz3TjOmrARSzhpGLz1KfS8o3YQYQ1HqymS1zyjuim5V+pf1s7bFg6RE9 d0ipnTwEXmXIuU8fu09DAm6Z4o9XP2RP49cCieDOQ0dp2YKIzae6RkZjCjUPujiI pZeN8rCageX6G0iuwKMOLfNn/g5ItHOm72U28aV6hMzTmHvdB+eaKl304N1fzjya +FSncOcO9SazYGEKjs46ec48k389lXZ7nMuZDMwPZgsDyzZWn4kCHAQSAQoABgUC WDhcJAAKCRDAwHYTL/p2lSfUD/sGJhNJiIrGYf9qw8qBQJyaQDoNBFHLvl2tUpOf +TVojDywJ+51askcL3L0hldbUg4wziPi2FV8AtRyerDKNcMUJYn6SQ3Rhx/7eFP/ vnUqJ7f8ZJEk7LzGYDZGQpnSe/eyXNARVjoPUFhjl6mTLtKPZWfaprs2e+yvQimy 2hgsiWOvc19ifsRg6KVSSTBqUS+FCSw0VRR1wt5cmrFRkuRfGoCHHd8mXkI1qSit xfFQxyURxHWxLkWnwN9y0G8cYvSOI/hgmfY/MYY6NRWKbmzXIve5n7qFKNFBR3n4 NA9oJwI2Mzop8GuwSU54QlmiG6N0Elqt7c+aU1bOGt3dWJS8e5J7VBZqoFrIzPAC DObdSkU3Y04ZQ5LcnAn0n6dpZRTX2Fv0Tcxv7MCEfQCDCeBs9xDrXIcEZLNyrBQE kcXbL4EUBjsq80fLV5/a5iyhS67pJc10mS5T8pkFd7hA6eTesRRbP2Do1ndiZCPw E2gugDmz6hjTAUwG6iLu2rwJ2aOOm3V3PmYZ/JM45zGTjKFb2sEzkuOG1YdHIt30 FXqEswItLMWQl5xTwuHId9mPvgKLz9h5ZYt8ML5G+QXFEVnKiU0pFWabDgpb81cS 0aAYQcSOUG5ObyBjHsZXwQKNpe9oEFN6xrbE7dp4FxtZpZXExLE+PUffxjOyGw0W Lj+WYokCPQQTAQoAJwIbAwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAUCWU795AUJ BaVOgwAKCRD1uEQ283mhxrjVD/41bwb1y38w1K3dlOavw6t2RwwvmBgNDlhLFrM1 ZK3Kjk+p9s2/8GoeGdPiVgrqv3okI+Ztme+R+jtWRPSozczfZyIdgR2/jdhS8P0L IUIbQlkn7cvvDb4Wf8lAUhnGF/a+Gmnpn+Ju65KcTxFBGSSt5q2iQVbsW+krhyoy nuD6C/2QLDKH+YPOahihmrTpLQkJ4IwdK+0LfoWqcgNB5JiRKd4fcgXEYTxxBMSc 5QwlRkU638PTkjGaBbb7I+RxWrk3Y75SyyFbD/svJm4JxQGFQCvPOiesSTQrQuCV opoZj0YKfZzUpgiVYQFm1MCLLhWs9nDxJ0d2lxropUTm+8BYuuy/pSki60GGbKv6 MnWUhExmde01U3wjxkHeXX9u2qswL/spORVtqtxDvWQeUyZyhIs1Slled+7RLOww bJNamKGVdBcN3XZwaxNeuBX1nppjKKeleS56C0BFuTVEptEsdRj62FVJIli1MH43 IxAN0iJUsO2XSljhmixQu37jfkLW4HlCLiZwYLCJDoXFtHZZ5nwURGeBGSeGyWzC tx7DJvXDEx/GWMJzU500X4iTc5gcvLLsTm5bxKthOITITHgvXXAMmc0YpLmmueZA UNMShQsxkzm3QOBCVgjW532OQHM66Plsact4hCYJ+p0GSQGcUgKmNGcfQKmLNhvR oT2NlYkCIgQTAQoADAUCWF7kZAWDB4YfgAAKCRBAElCWamssRtBxD/0RK9Rk9eCg jj61Vk8rA/Uvmz7ZEwHlunL6pucvy2RZL+ztMNSlLPYcvtByvSvUo8Q9G/YnjR6l EGMi5DERN5Euc2nMIlg82EWQyd3MEAcBxcqriLuKrybizce1o8pUExV84DJYchr/ A9ei93GiHbNodMxv5zt+4pu/e540DxATf9ME6EpJtbzJcUwsGUrPOtC9Xp13t1sL 4sHL1z3TVPOzOQ8HSlfMOUdYNoJg89MjTnX//rOpfSIq3FcEVURipOgLKDhiQEmQ tpeknv7uZDZuRLxK/J7IebmnbnmV3wq3l5LVCLRTzpCXtucTwnKoRYBcbT676F70 GbM7QPwdiyiq17lx3+YHElHm0KGfxn6iUSPGtEJv6RcO8glU/VIN6/N8HxH5Y9HE 28+eC094GnKD3xQtMzSrzTkp7q5NGZPBS4wT3mGwem4pfjuYbkWea3+71jnr6SHW ciGWo4kXuVp9Va1ZFuNxk8o0G8YdHV30oEXJwTyyxXFMdePraz95B4fjtJdpgsnq JLsFIIgdpECBeiyqy83pOCv5aoRiqz8xAYrkZWYKtw1xdMZ25LoyI/OgdmCIjrr1 q+VGPXi2CcB2Y4XmJCUCYUh1W7eGoXlOEWA3upONa+RkrOZ0bEQ/VuHqvWeVol7i uWqdxi9IrjPK2hhrMPMKeNphpjLLitfWM4kBIgQTAQoADAUCWF7kdQWDB4YfgAAK CRAMTKXUV9Zr2pLpB/9K4hu0ELE9D5ZvsFlD6lQUNvonuiZGX/xBsejYL/rEPWSv 2dJpb815rPkJtoqUZ5cem2sJhOKc+ZIKHZy0hiobqbePZXArT7dh5aIfMYfFfvYE YOZYUD9dbMjzjqHrpfUvht6IxekYfkm+XKYL1PGdxGZ4AiK9ZoRVnM/eK2p6+qcG VEPkHJXJKNRMvPJTniKsW3vryqM0J0Z6wDqH9IEWIhus1GeEcm/j7Hw0OO5gku2/ uI8SndS6dtCjruGcVLpG0quMdCFCsp/jtH1y1opFDT3cW7g7q3RQxw0dNzflraaz vQm+caO8QtZGVS6vegaOf2a4hfACKtt9EzVVLq93iQIiBBMBCgAMBQJYXuR9BYMH hh+AAAoJEMM+Vu/rhndFAXwP/2bAkgdM2CZ/WRXRAecBCCIz483+bS4yXaraSeJz bWl8Cp3aVMqRyLGcDo0UhYRTzDfgcX2YLlK0pBwAnvNd2hi8cqQC1RWOdYrgUMzN /StI9NOImov0n8kWqK+kqdxJIwz2Rs3crlwpDR5bosTzG/HwwxtNGB1h1T72RrGf ISOrqtRgHnAod9DBluONJaUv1QN95zHCVQqh0deAZLYtOlPhUbXYgfZQGlufUBpt SJAsA2W5yuN+Qiav7TetqQDN/zapPIxGSkfuN+t22ek65OyAbEHQP7Z1ltI/+PEM DaqU+Pzb9UJtZEpcHDCTB4YbI9+H6k/WAp6w2Cm3nNEJiaYAr8XocxxTMnjlVtnr OE/4++wuQE8EvcuvnxXkhyLbZnhAdpv1lUgzll3pAhe9w4RCpX4tz5JHB5EoQXAP LHIQRzWc8TzE4H+aUdSwrewQL3qxrgadJDST2Em/DFIj8EftwbJCLpm8jtp9fw7U gP4iv8hYhUBCRH24n5h8YwbwCOOGadLNfoUh6DHHa7ZdktOADQfcmOhA840FoL+v t85+sVCgGaBsoPkgS/ZvE5KRbtugiMuCO5OS3lIpzDjaw+2nH1Wq2uuuBya3owyJ GIxKtLNxg/pZhwnSsVwi3sDmmXLq6WDTCAfeR+8+PKgC6IgDUAYalpoxEq7grVi9 e/yiiQEiBBMBCgAMBQJYXuSFBYMHhh+AAAoJEHoiDPo7PdxbfaEH/AshsMxZBRt3 f5f4JrTQtk8veAqwKzHNUMbl9sQzBjEvg9V7SaoGp1cKPb1e9Sy1VDD496dBYgxc rUOG7Wp3XD/Gccht2a/EL+CJaJHyVzfskLTtFtnrMItLCR7uuqQEvK/Q2IxJpaMt wNKEpzDCgKzFXUjet/gB2j90dMWqPVDh/GRiktrcDVXaX3roMBenWmzeBpT8oUA2 Tw8gUr696Y7d2RgDHncTlm/qS5w1QyLT5CIXd1Os6+eA8MIjFUSKaDxNM9zCzGvv hkj7bBfi1xxmPonogGKd56tgBFZ7EGeFD/TGLjCtJLYz8pPP/F2az557xFJ41aVt Cq0wTtHSrsGJAiIEEwEKAAwFAlhe/hQFgweGH4AACgkQNmQEJH0guzLtoRAAvA1V ZPyE/RoTjTkZ0468R7txGSNiQBMHeKQzSj5vrFvFjuQOx1pKvPbBa//pfddmXsyN 4+fXkm+i3jhiww85VmfP+jaPZE7ha3gI1sLIXywBUEQXGtN+JrGdIfx4fm9Xj+Km a41o77XxnYxg/puqtoxXuFQfF+KcX3SgCzaGnhn+p2YfUtIgqaVkQl6H6vhKley8 pZcB58O9Eu0RbpGg/FWovOWY/Jg8DdbOpQmrp4tXD116rt8m0jEJcWk/DPexehHn Znt4Xi/oogBiccRDd/ebUeyjUkkrPk+IQjdYYOuN0i0nMUL9KsWLJwUHNa2IWv1e xgVg9dWuPk13K2hJFzdvGa19IVsBOEEXgfIyC2ZSqz0zFhAQQ/2saRDvITgQS10W duL55lv78YevjqeETEHW2DeXkzUiRwe64BUuu/9LFsSLuwCwLrvz3Yyh0T21MAAA /5sHsai4hRhxAhVoWfelKShzmZdh7bdqrxDrivutdcOn9Evdw3IQ9rsDtgyDrvmm Mok1eSYvZF61yhHvdVU6wQOET7u3T7eSFoAW7EknuAd4rSIZ2AqBchARGEbz3m7w aidB1KmedzlGNk0DEWcXiqpdgQdvalzxfJJSIOsJic7FH+p2xBnFYBVdS/ftgrC2 kuPY4dpfVviNxGLrRDd8fYfdVDolMW1pOWo7oQq5Ag0EVYri4QEQAOtygi1rXfDl /H18Evad7dz96ZFDGSQNoD9eC4UCGD5F2AqEil7pTNapIDqcGaz1MZl5k4B9CjH7 mutQukLXcHtdrc5eXYjMQZ/jVFjjv5j3fPgwWrz6LfxYD/jxw7uTgDHlgEo/Dv8D WMeE3wcycKhlG9KT/qdx+1b36ds7ecYeooYIxHSCAbQl+4mKjn4HNIhAGTcNe7i9 79rGApBNJgpSYnaqK7i3CFvIeMRWLQKk41s4sBrwZI+hEFnlZoJ3Le7Mh/0emcfs ZCk4YNwdfGiZWoic8ZMudx0JUkso/ELRxzx/bgNls+vpQb3SQ1zuFZ8xnOunEmaf DYbg/hJguAT3fnvGqqeO0305+OVflxcoUyxXDxLtY+4t6SEj2v3L9t+ZpbQg12+d lr3Eel+NltXibv2yVhwP0NpQq+CJ+nPDQWsCcK/FelP2ik1EZqasQPZZFBORKXNV JCmWXm+8GNbwN9wvVR9rmwh0h8v9RAbh7Q4inYnxiVVKIH14ZGQp8i3NW/k5sOuk RqM203tEV4LGCP+bwswcwPCmvfid3L8oQmPA7ezL6rmlehe7ctP4iHEX/xxGbRzD ZWNdNZrTdq0h1WR6ce7Ya52VNN1dBoqkbZmzQxD+NC/3dv/yl8MfnEeJDdvQCGQ2 0zCbGfXWc2T9ov4BK/a05cDBlXaIpH/fABEBAAGJAiUEGAEKAA8FAlWK4uECGwwF CQeGH4AACgkQ9bhENvN5ocZGxA/+I+GLTTFaHRy6ZNmAr6uEPQ59yXOE5k2ZrML7 F2nnIR0FJFydhnLSsxCt89zXxmxk4kA4h+M5jmyB4HiIGp0u0lC/zpklJwJ8+EKj KpSaL9zdo1hwojybGar78mF4qsQ2EZP0TIq41gOZ/qx7dVaDSu75cuQvgGakEQcx 89B5RGaZRKLlE68Mo2QXktNENnPFkkOPBoil8KX34DHIWJafncwu0vObcE31ifIZ j9j3FoeupnIW4HXEbsZBWkM0k/Fzx3wdvvYuEwR0JvihSJ4YEncB33weZ+u1+XTa cAWt98oubYMoR+M2d4+EAmOJVjz0oGXNvs/BBwSCem3c/oSt43R3lc7zMU8shZf8 bKS+TGYnV/kRWcNc2l0BTiRRUwFZ0/XvAcNXJsB1CyrvbWvrZiDIm6tA3xOJzFGY wLNTM1BqfNfrPbzov67vkkbxxRlTRx1x6LTFPV0H1FTZ5CSQgahjm9SwANb0jyU7 xR9hL3zBvKr7quR7mM1zzjnoGkNMdVsM02fBrmqfhABychMFMVVOWhyLLQO47YZB ghu/JigFHreRBbTOPLcCSfkH24EL91nDnfLp6KHLcz2DfU2W1lajwRfDm2rpbKx+ 6iAnmNBJV49ZaM7lFqPaJz942mVySd+4rygkuF1olWxN1EbzK0/bKRuzljIj5U+r vUTpzlk= =ZIr3 -----END PGP PUBLIC KEY BLOCK----- ================================================ FILE: desktop/src/test/resources/downloadUpdate/test.txt ================================================ test ================================================ FILE: desktop/src/test/resources/downloadUpdate/test.txt.asc ================================================ -----BEGIN PGP SIGNATURE----- iQIcBAABCAAGBQJZWlbrAAoJEPW4RDbzeaHGNbgP/RHqnJ718ukagmjeZnvIHwq7 UWJsaQH/7lmDZ5Di4oFSTSWB8B6nA/7aOmuqMXP9A4jLb4uCF88uCLtEm3dEcYra K07ZNBc/P3nNziVOU354mHQlo4/vFGhMxivfE6OeSbpxR4meQvLGZTFRNfPX5LjB AGxufkcE/MLgi33OKqBQn8o9wCh5H5THndCLkwB3srplr4h4xqu/ptRYV/rle8+1 XTsmv83IVtmoOhdpKwTwHAFX85zNELY+dC6TcU7mjU+6NQUMUfzuS8vhvjAzeRAM wXfqeKELZ8v+o/fCsVgnyCC6HMeuepPC4f4BtHy0J6kAUIZoKsFKa/Zcaf2HxOD1 y9UehVZgGUAIyU5PW2md4y14nd4HfJ8X/J/cOn+L3VmHYgBXVk7UAfaCWEhYug7L 7KI71s3E63mdS/Hq1WToiK/TpU0WrxFnDW1HmJy0JvE6NeqHtyeH5F+5EwFbJHz4 bRKC+uhVgtnhHSftbz3qMupNhAd9rIx8ZBbVJ2f3Oj/cl6Mly6wBxoF3Tza00Z8K OOa93XJGJobmNT9E2wjgDqueaQGcfwSmejY8z6u8MMUwOqSTJVuq9mjdBg+TDIXE rW2SUzUypGnogGoxFOeowWoYR9TLBWFF4mZGEj0UPZWJQQ7mN/n/2qPxuEajwnfJ FtOkmm/NQ628rR/gKiXk =gg4v -----END PGP SIGNATURE----- ================================================ FILE: desktop/src/test/resources/downloadUpdate/test_bad.txt ================================================ tesp ================================================ FILE: desktop/src/test/resources/downloadUpdate/test_bad.txt.asc ================================================ -----BEGIN PGP SIGNATURE----- iQIcBAABCAAGBQJZWlbrAAoJEPW4RDbzeaHGNbgP/RHqnJ718ukagmjeZnvIHwq7 UWJsaQH/7lmDZ5Di4oFSTSWB8B6nA/7aOmuqMXP9A4jLb4uCF88uCLtEm3dEcYra K07ZNBc/P3nNziVOU354mHQlo4/vFGhMxivfE6OeSbpxR4meQvLGZTFRNfPX5LjB AGxufkcE/MLgi33OKqBQn8o9wCh5H5THndCLkwB3srplr4h4xqu/ptRYV/rle8+1 XTsmv83IVtmoOhdpKwTwHAFX85zNELY+dC6TcU7mjU+6NQUMUfzuS8vhvjAzeRAM wXfqeKELZ8v+o/fCsVgnyCC6HMeuepPC4f4BtHy0J6kAUIZoKsFKa/Zcaf2HxOD1 y9UehVZgGUAIyU5PW2md4y14nd4HfJ8X/J/cOn+L3VmHYgBXVk7UAfaCWEhYug7L 7KI71s3E63mdS/Hq1WToiK/TpU0WrxFnDW1HmJy0JvE6NeqHtyeH5F+5EwFbJHz4 bRKC+uhVgtnhHSftbz3qMupNhAd9rIx8ZBbVJ2f3Oj/cl6Mly6wBxoF3Tza00Z8K OOa93XJGJobmNT9E2wjgDqueaQGcfwSmejY8z6u8MMUwOqSTJVuq9mjdBg+TDIXE rW2SUzUypGnogGoxFOeowWoYR9TLBWFF4mZGEj0UPZWJQQ7mN/n/2qPxuEajwnfJ FtOkmm/NQ628rR/gKiXk =gg4v -----END PGP SIGNATURE----- ================================================ FILE: desktop/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker ================================================ mock-maker-inline # enable mocking final classes in mockito ================================================ FILE: docs/CONTRIBUTING.md ================================================ # Contributing to Haveno Thanks for wishing to help! Here there are some guidelines and information about the development process. We suggest you to join the [matrix](https://app.element.io/#/room/#haveno-development:monero.social) room `#haveno-development` (relayed on [IRC/Libera](irc://irc.libera.chat/#haveno-development)) and have a chat with the devs, so that we can help get you started. Issues are tracked on GitHub. We use [a label system](https://github.com/haveno-dex/haveno/issues/50) and GitHub's [project boards](https://github.com/haveno-dex/haveno/projects) to simplify development. Make sure to take a look at those and to follow the priorities suggested. ## General guidelines - Be verbose. Remember this is collaborative development and we need to make life as easy as possible for future developers and for the current maintainers. - All formatting needs to be consistent with the current code. - Changes must be done 'the right way'. No hacks to make things work, if you are having issues, let the other devs know, so that we can help you out. ## Development process When you have something new built for Haveno, submit a pull request for review to be merged. - Pull requests should contain as many details as possible about what you are going to change and why. Avoid "title only" PRs, unless they are self-explanatory. - Pull requests should contain one single commit, unless it makes sense to have more. Please become familiar with git if you are not. - Pull requests won't be merged before 24 hours has passed. This timeframe will be extended when Haveno will have more active developers. ## Developer guide See the [developer guide](developer-guide.md) to get started developing Haveno. ## Translation Existing translation files are in [core/src/main/resources/i18n/](/core/src/main/resources/i18n/), feel free to update or improve them if needed. To add a new locale translations, follow these steps: - Add your [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) standard country language code to [core/src/main/java/haveno/core/locale/LanguageUtil.java](/core/src/main/java/haveno/core/locale/LanguageUtil.java) and remove it from "// not translated yet" if it is there. - Copy [displayStrings.properties](/core/src/main/resources/i18n/displayStrings.properties), create new file in [core/src/main/resources/i18n/](/core/src/main/resources/i18n/) in this format: `displayStrings_[insertLocaleName].properties` and then add translations. ================================================ FILE: docs/README.md ================================================ # Haveno Documentation - [installing.md](installing.md ) - Instructions for running a local test network - [CONTRIBUTING.md](CONTRIBUTING.md) - Guidelines for contributors - [developer-guide.md](developer-guide.md) - Guide for developers - [bounties.md](bounties.md) - Rules and description of the bounty program - [import-haveno.md](import-haveno.md) - Instructions for importing Haveno into Eclipse IDE or IntelliJ IDEA (from Haveno) - [trade-protocol.md](trade_protocol/trade-protocol.md) - Haveno's trade protocol - [data-stores.md](data-stores.md) - Instructions for updating data stores (from Haveno) - [tor-upgrade.md](tor-upgrade.md) - Instructions for upgrading the Tor dependencies used by Haveno (from Haveno) - [README.md](README.md) - This file Documents outside of this folder: - [FAQ on haveno.exchange](https://haveno.exchange/faq/) - Common questions and answers ================================================ FILE: docs/bounties.md ================================================ ## Bounties We use bounties to incentivize development and reward contributors. All issues available for a bounty are listed [here](https://github.com/haveno-dex/haveno/issues?q=is%3Aissue%20state%3Aopen%20label%3A%F0%9F%92%B0bounty). It's possible to list on each repository the issues with a bounty on them, by searching issues with the '💰bounty' label. To receive a bounty, you agree to these conditions: - Your changes must follow the [styling guidelines](CONTRIBUTING.md). - Bounties will be set and awarded in XMR at discretion of the Haveno core team. - The issues eligible for a bounty are labelled '💰bounty' and have the amount of the bounty specified in the title in this form: `[$200]` if in dollars or `[1 XMR]` if in Monero. - An issue is considered resolved when the patch(es) proposed by the contributor is/are merged in the appropriate repository according to terms of the issue. Pull requests to monero-project must be merged into the release branch to be considered final. - The first person who resolves an issue in its entirety will receive the entire amount of the bounty. - If the issue is resolved collaboratively by more than one person, the reward will be distributed among the contributors at discretion of the Haveno core team. - Let the Maintainers know if you intend to work on a bounty, so that the issue can be assigned to you. Being assigned to an issue doesn't make that issue resolvable only by the assignee. It's meant to avoid duplication of efforts and not to discourage collective works. - After the issue is resolved, contact the maintainers and claim your bounty (remember to provide them with a Monero address). - If a big number of bounties is claimed at the same time, the maintainers might opt for sending all the rewards on a specific day of the month instead of right after resolution. - If the bounty is in dollars, the contributor will receive the amount in Monero according to the value of XMR at the moment the issue was practically resolved, so in the moment the maintainers accept the last patch resolving the issue. We want to keep the system simple and flexible, but we will add new rules or edit existing ones if necessary. ================================================ FILE: docs/create-mainnet.md ================================================ # Create Haveno network quick start guide These instructions describe how to quickly start a public Haveno network running on Monero's main network from your local machine, which is useful for demonstration and testing. For a more robust and decentralized deployment to VPS for reliable uptime, see the [deployment guide](./deployment-guide.md). ## Clone and build Haveno ``` git clone https://github.com/haveno-dex/haveno.git cd haveno git checkout master make clean && make ``` ## Start a Monero node In a new terminal window, run `make monerod` to start and sync a Monero node on mainnet. Seed nodes and arbitrators require a local, unrestricted Monero node for performance and functionality. ## Start and register seed nodes In a new terminal window, run: `make seednode`. The seed node's onion address will print to the screen (denoted by `Hidden service`). Record the seed node's URL to xmr_mainnet.seednodes with port 1002. For example, `4op7nzb65z4xg2taqmt2uhih7uwi3ya25yx5bvskbkjisnq7rwepzvad.onion:1002`. In a new terminal window, run: `make seednode2`. The seed node's onion address will print to the screen (denoted by `Hidden service`). Record the seed node's URL to xmr_mainnet.seednodes with port 1003. For example, `abwyc7ccjq4oyiej5z3dpwupzql34nnedaft5jc5l2dbocko7naosrjqd.onion:1003`. Stop both seed nodes. ## Register public key(s) for various roles Run `./gradlew generateKeypairs`. A list of public/private keypairs will print to the screen which can be used for different roles like arbitration, sending private notifications, etc. For demonstration, we can use the first generated public/private keypair for all roles, but you can customize as desired. Hardcode the public key(s) in these files: - [AlertManager.java](https://github.com/haveno-dex/haveno/blob/2ff149b1ebcfd1a4c40d77d05d4ee9981353a8a6/core/src/main/java/haveno/core/alert/AlertManager.java#L112) - [ArbitratorManager.java](https://github.com/haveno-dex/haveno/blob/2ff149b1ebcfd1a4c40d77d05d4ee9981353a8a6/core/src/main/java/haveno/core/support/dispute/arbitration/arbitrator/ArbitratorManager.java#L81) - [FilterManager.java](https://github.com/haveno-dex/haveno/blob/2ff149b1ebcfd1a4c40d77d05d4ee9981353a8a6/core/src/main/java/haveno/core/filter/FilterManager.java#L135) - [PrivateNotificationManager.java](https://github.com/haveno-dex/haveno/blob/2ff149b1ebcfd1a4c40d77d05d4ee9981353a8a6/core/src/main/java/haveno/core/alert/PrivateNotificationManager.java#L111) ## Change the default folder name for Haveno application data To avoid user data corruption when using multiple Haveno networks, change the default folder name for Haveno's application data on your network: - Change `DEFAULT_APP_NAME` in [HavenoExecutable.java](https://github.com/haveno-dex/haveno/blob/64acf86fbea069b0ae9f9bce086f8ecce1e91b87/core/src/main/java/haveno/core/app/HavenoExecutable.java#L85). - Change `appName` throughout the [Makefile](https://github.com/haveno-dex/haveno/blob/64acf86fbea069b0ae9f9bce086f8ecce1e91b87/Makefile#L479) accordingly. For example, change "Haveno" to "HavenoX", which will use this application folder: - Linux: ~/.local/share/HavenoX/ - macOS: ~/Library/Application Support/HavenoX/ - Windows: ~\AppData\Roaming\HavenoX\ ## Change the P2P network version To avoid interference with other networks, change `P2P_NETWORK_VERSION` in [Version.java](https://github.com/haveno-dex/haveno/blob/a7e90395d24ec3d33262dd5d09c5faec61651a51/common/src/main/java/haveno/common/app/Version.java#L83). For example, change it to `"B"`. ## Start the seed nodes Rebuild for the previous changes to the source code to take effect: `make skip-tests`. In a new terminal window, run: `make seednode`. In a new terminal window, run: `make seednode2`. > **Notes** > * At least 2 seed nodes should be run because the seed nodes restart once per day. > * Avoid all seed nodes going offline at the same time. If all seed nodes go offline at the same time, network information like registered arbitrators and the network filter object will be reset. In that case, re-apply the network filter object (ctrl+f) and restart the arbitrators in order to re-register them with the seed nodes. ## Start and register the arbitrator In a new terminal window, run: `make arbitrator-desktop-mainnet`. Ignore the error about not receiving a filter object. Go to the `Account` tab and then press `ctrl + r`. A prompt will open asking to enter the key to register the arbitrator. Enter your private key. ## Set a network filter on mainnet On mainnet, the p2p network is expected to have a filter object for offers, onions, currencies, payment methods, etc. To set the network's filter object: 1. Enter `ctrl + f` in the arbitrator or other Haveno instance to open the Filter window. 2. Enter a developer private key from the previous steps and click "Add Filter" to register. ## Other configuration ### Set the network's release date Set the network's approximate release date by setting `RELEASE_DATE` in HavenoUtils.java. This will prevent posting sell offers which no buyers can take before any buyer accounts are signed and aged, while the network bootstraps. After a period (default 60 days), the limit is lifted and sellers can post offers exceeding unsigned buy limits, but they will receive an informational warning for an additional period (default 6 months after release). The defaults can be adjusted with the related constants in HavenoUtils.java. ### Optionally configure trade fees Trade fees can be configured in HavenoUtils.java. The maker and taker fee percents can be adjusted. Set `ARBITRATOR_ASSIGNS_TRADE_FEE_ADDRESS` to `true` for the arbitrator to assign the trade fee address, which defaults to their own wallet. Otherwise set `ARBITRATOR_ASSIGNS_TRADE_FEE_ADDRESS` to `false` and set the XMR address in `getGlobalTradeFeeAddress()` to collect all trade fees to a single address (e.g. a multisig wallet shared among network administrators). ### Optionally start a price node The price node is separated from Haveno and is run as a standalone service. To deploy a pricenode on both TOR and clearnet, see the instructions on the repository: https://github.com/haveno-dex/haveno-pricenode. After the price node is built and deployed, add the price node to `DEFAULT_NODES` in [ProvidersRepository.java](https://github.com/haveno-dex/haveno/blob/3cdd88b56915c7f8afd4f1a39e6c1197c2665d63/core/src/main/java/haveno/core/provider/ProvidersRepository.java#L50). ### Update the download URL Change every instance of `https://haveno.exchange/downloads` to your download URL. For example, `https://havenoexample.com/downloads`. ## Review all local changes For comparison, placeholders to run on mainnet are marked [here on this branch](https://github.com/haveno-dex/haveno/tree/mainnet_placeholders). ## Start users for testing Optionally set `--ignoreLocalXmrNode` to `true` in Makefile for the user applications to use public nodes and ignore the locally running Monero node, in order test real network conditions. Start user1: `make user1-desktop-mainnet`. Start user2: `make user2-desktop-mainnet`. Test trades among the users and arbitrator over Monero's mainnet. ## Share your git repository for others to test To share your network for others to use, commit your local changes and share your *git repository's URL*. It is not sufficient to share only your seed node addresses, because their application must be built with the same public keys and other configuration to work properly. After sharing your git repository, others can build and start their application with: ``` git clone cd haveno make skip-tests make user1-desktop-mainnet ``` However a [more robust VPS setup](./deployment-guide.md) should be used for actual trades. ## Build the installers for distribution To build the installers for distribution, first change `XMR_STAGENET` to `XMR_MAINNET` in [package.gradle](https://github.com/haveno-dex/haveno/blob/1bf83ecb8baa06b6bfcc30720f165f20b8f77025/desktop/package/package.gradle#L278). Then [follow instructions](https://github.com/haveno-dex/haveno/blob/master/desktop/package/README.md) to build the installers for distribution. Alternatively, the installers are built automatically by GitHub. ================================================ FILE: docs/data-stores.md ================================================ # Data stores ### Update stores With every release we include the latest snapshot of Mainnet and Testnet data from the P2P network within the client. * Start your Haveno client on Mainnet and Testnet and let it run until it is fully synced. * Run [copy_dbs.sh](https://github.com/bisq-network/bisq/blob/master/desktop/package/macosx/copy_dbs.sh) to copy the required files into the [p2p resources directory](https://github.com/bisq-network/bisq/blob/master/p2p/src/main/resources). * To add a new trade statistic snapshot just add it to the list of trade statistic snapshots in https://github.com/bisq-network/bisq/blob/0345c795e2c227d827a1f239a323dda1250f4e69/common/src/main/java/haveno/common/app/Version.java#L40 ================================================ FILE: docs/deployment-guide.md ================================================ # Deployment Guide This guide describes how to deploy a Haveno network: - Manage services on a VPS - Fork and build Haveno - Start a Monero node - Add seed nodes - Add arbitrators - Configure trade fees and other configuration - Build and start price nodes - Set a network filter - Build Haveno installers for distribution - Send alerts to update the application and other maintenance ## Manage services on a VPS Haveno's services should be run on a VPS for reliable uptime. The seed node, price node, and Monero node can be run as system services. Scripts are available for reference in [scripts/deployment](scripts/deployment) to customize and run system services. Arbitrators can be started in a Screen session and then detached to run in the background. Some good hints about how to secure a VPS are in [Monero's meta repository](https://github.com/monero-project/meta/blob/master/SERVER_SETUP_HARDENING.md). ## Install dependencies On Linux and macOS, install Java JDK 21: ``` curl -s "https://get.sdkman.io" | bash sdk install java 21.0.9.fx-librca ``` Alternatively, on Ubuntu 22.04: `sudo apt-get install openjdk-21-jdk` On Windows, install MSYS2 and Java JDK 21: 1. Install [MSYS2](https://www.msys2.org/). 2. Start MSYS2 MINGW64 or MSYS MINGW32 depending on your system. Use MSYS2 for all commands throughout this document. 4. Update pacman: `pacman -Syy` 5. Install dependencies. During installation, use default=all by leaving the input blank and pressing enter. 64-bit: `pacman -S mingw-w64-x86_64-toolchain make mingw-w64-x86_64-cmake git` 32-bit: `pacman -S mingw-w64-i686-toolchain make mingw-w64-i686-cmake git` 6. `curl -s "https://get.sdkman.io" | bash` 7. `sdk install java 21.0.9.fx-librca` ## Fork and build Haveno Fork Haveno to a public repository. Then build Haveno: ``` git clone cd haveno git checkout make clean && make ``` ## Start a Monero node Seed nodes and arbitrators must use a local, unrestricted Monero node for performance and functionality. To run a private Monero node as a system service, customize and deploy private-stagenet.service and private-stagenet.conf. Optionally customize and deploy monero-stagenet.service and monero-stagenet.conf to run a public Monero node as a system service for Haveno clients to use. You can also start the Monero node in your current terminal session by running `make monerod` for mainnet or `make monerod-stagenet` for stagenet. ## Add seed nodes ### Seed nodes without Proof of Work (PoW) > [!note] > Using PoW is suggested. See next section for PoW setup. For each seed node: 1. [Build the Haveno repository](#fork-and-build-haveno). 2. [Start a local Monero node](#start-a-local-monero-node). 3. Modify `./scripts/deployment/haveno-seednode.service` and `./scripts/deployment/haveno-seednode2.service` as needed. 4. Copy `./scripts/deployment/haveno-seednode.service` to `/etc/systemd/system` (if you are the very first seed in a new network also copy `./scripts/deployment/haveno-seednode2.service` to `/etc/systemd/system`). 5. Run `sudo systemctl start haveno-seednode.service` to start the seednode and also run `sudo systemctl start haveno-seednode2.service` if you are the very first seed in a new network and copied haveno-seednode2.service to your systemd folder. 6. Run `journalctl -u haveno-seednode.service -b -f` which will print the log and show the `.onion` address of the seed node. Press `Ctrl+C` to stop printing the log and record the `.onion` address given. 7. Add the `.onion` address to `core/src/main/resources/xmr_.seednodes` along with the port specified in the haveno-seednode.service file(s) `(ex: example.onion:1002)`. Be careful to record full addresses correctly. 8. Update all seed nodes, arbitrators, and user applications for the change to take effect. ### Seed nodes with Proof of Work (PoW) > [!note] > These instructions were written for Ubuntu with an Intel/AMD 64-bit CPU so changes may be needed for your distribution. ### Install Tor Source: [Tor Project Support](https://support.torproject.org/apt/) 1. Verify architecture `sudo dpkg --print-architecture`. 2. Create sources.list file `sudo nano /etc/apt/sources.list.d/tor.list`. 3. Paste `deb [signed-by=/usr/share/keyrings/deb.torproject.org-keyring.gpg] https://deb.torproject.org/torproject.org main`. 4. Paste `deb-src [signed-by=/usr/share/keyrings/deb.torproject.org-keyring.gpg] https://deb.torproject.org/torproject.org main`. > [!note] > Replace `` with your system codename such as "jammy" for Ubuntu 22.04. 5. Press Ctrl+X, then "y", then the enter key. 6. Add the gpg key used to sign the packages `sudo wget -qO- https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --dearmor | tee /usr/share/keyrings/deb.torproject.org-keyring.gpg >/dev/null`. 7. Update repositories `sudo apt update`. 8. Install tor and tor debian keyring `sudo apt install tor deb.torproject.org-keyring`. 9. Replace torrc `sudo mv /etc/tor/torrc /etc/tor/torrc.default` then `sudo cp seednode/torrc /etc/tor/torrc`. 10. Stop tor `sudo systemctl stop tor`. For each seed node: 1. [Build the Haveno repository](#fork-and-build-haveno). 2. [Start a local Monero node](#start-a-local-monero-node). 3. Run `sudo cat /var/lib/tor/haveno_seednode/hostname` and note down the .onion for the next step & step 10. 4. Modify `./scripts/deployment/haveno-seednode.service` and `./scripts/deployment/haveno-seednode2.service` as needed. 5. Copy `./scripts/deployment/haveno-seednode.service` to `/etc/systemd/system` (if you are the very first seed in a new network also copy `./scripts/deployment/haveno-seednode2.service` to `/etc/systemd/system`). 6. Add user to tor group `sudo usermod -aG debian-tor `. > [!note] > Replace `` above with the user that will be running the seed node (step 6 above & step 4) 7. Disconnect and reconnect SSH session or logout and back in. 8. Run `sudo systemctl start tor`. 9. Run `sudo systemctl start haveno-seednode` to start the seednode and also run `sudo systemctl start haveno-seednode2` if you are the very first seed in a new network and copied haveno-seednode2.service to your systemd folder. 10. Add the `.onion` address from step 3 to `core/src/main/resources/xmr_.seednodes` along with the port specified in the haveno-seednode.service file(s) `(ex: example.onion:2002)`. Be careful to record full addresses correctly. 11. Update all seed nodes, arbitrators, and user applications for the change to take effect. Customize and deploy haveno-seednode.service to run a seed node as a system service. Each seed node requires a locally running Monero node. You can use the default port or configure it manually with `--xmrNode`, `--xmrNodeUsername`, and `--xmrNodePassword`. Rebuild all seed nodes any time the list of registered seed nodes changes. > [!note] > * At least 2 seed nodes should be run because the seed nodes restart once per day. > * Avoid all seed nodes going offline at the same time. If all seed nodes go offline at the same time, network information like registered arbitrators and the network filter object will be reset. In that case, re-apply the network filter object (ctrl+f) and restart the arbitrators in order to re-register them with the seed nodes. ## Register keypairs with privileges ### Register keypair(s) with developer privileges 1. [Build the Haveno repository](#fork-and-build-haveno). 2. Generate public/private keypairs for developers: `./gradlew generateKeypairs` 3. Add the public key to `getPubKeyList()` in [FilterManager.java](https://github.com/haveno-dex/haveno/blob/2ff149b1ebcfd1a4c40d77d05d4ee9981353a8a6/core/src/main/java/haveno/core/filter/FilterManager.java#L135). 4. Update all seed nodes, arbitrators, and user applications for the change to take effect. ### Register keypair(s) with alert privileges 1. [Build the Haveno repository](#fork-and-build-haveno). 2. Generate public/private keypairs for alerts: `./gradlew generateKeypairs` 3. Add the public key to `getPubKeyList()` in [AlertManager.java](https://github.com/haveno-dex/haveno/blob/2ff149b1ebcfd1a4c40d77d05d4ee9981353a8a6/core/src/main/java/haveno/core/alert/AlertManager.java#L112). 4. Update all seed nodes, arbitrators, and user applications for the change to take effect. ### Register keypair(s) with private notification privileges 1. [Build the Haveno repository](#fork-and-build-haveno). 2. Generate public/private keypairs for private notifications: `./gradlew generateKeypairs` 3. Add the public key to `getPubKeyList()` in [PrivateNotificationManager.java](https://github.com/haveno-dex/haveno/blob/2ff149b1ebcfd1a4c40d77d05d4ee9981353a8a6/core/src/main/java/haveno/core/alert/PrivateNotificationManager.java#L111). 4. Update all seed nodes, arbitrators, and user applications for the change to take effect. ## Add arbitrators For each arbitrator: 1. [Build the Haveno repository](#fork-and-build-haveno). 2. Generate a public/private keypair for the arbitrator: `./gradlew generateKeypairs` 3. Add the public key to `getPubKeyList()` in [ArbitratorManager.java](https://github.com/haveno-dex/haveno/blob/2ff149b1ebcfd1a4c40d77d05d4ee9981353a8a6/core/src/main/java/haveno/core/support/dispute/arbitration/arbitrator/ArbitratorManager.java#L81). 4. Update all seed nodes, arbitrators, and user applications for the change to take effect. 5. [Start a local Monero node](#start-a-local-monero-node). 6. Start the Haveno desktop application using the application launcher or e.g. `make arbitrator-desktop-mainnet` 7. Go to the `Account` tab and then press `ctrl + r`. A prompt will open asking to enter the key to register the arbitrator. Enter your private key. The arbitrator is now registered and ready to accept requests for dispute resolution. > [!note] > * Arbitrators must use a local Monero node with unrestricted RPC in order to submit and flush transactions from the pool. > * Arbitrators should remain online as much as possible in order to balance trades and avoid clients spending time trying to contact offline arbitrators. A VPS or dedicated machine running 24/7 is highly recommended. > * Remember that for the network to run correctly and people to be able to open and accept trades, at least one arbitrator must be registered on the network. > * IMPORTANT: Do not reuse keypairs on multiple arbitrator instances. ## Remove an arbitrator > [!warning] > * Ensure the arbitrator's trades are completed before retiring the instance. > * To preserve signed accounts, the arbitrator public key must remain in the repository, even after revoking. 1. Start the arbitrator's desktop application using the application launcher or e.g. `make arbitrator-desktop-mainnet` from the root of the repository. 2. Go to the `Account` tab and click the button to unregister the arbitrator. ## Change the default folder name for Haveno application data To avoid user data corruption when using multiple Haveno networks, change the default folder name for Haveno's application data on your network: - Change `DEFAULT_APP_NAME` in [HavenoExecutable.java](https://.com/haveno-dex/haveno/blob/1aa62863f49a15e8322a8d96e58dc0ed37dec4eb/core/src/main/java/haveno/core/app/HavenoExecutable.java#L85). - Change `appName` throughout the [Makefile](https://github.com/haveno-dex/haveno/blob/64acf86fbea069b0ae9f9bce086f8ecce1e91b87/Makefile#L479) accordingly. For example, change "Haveno" to "HavenoX", which will use this application folder: - Linux: ~/.local/share/HavenoX/ - macOS: ~/Library/Application Support/HavenoX/ - Windows: ~\AppData\Roaming\HavenoX\ ## Change the P2P network version To avoid interference with other networks, change `P2P_NETWORK_VERSION` in [Version.java](https://github.com/haveno-dex/haveno/blob/a7e90395d24ec3d33262dd5d09c5faec61651a51/common/src/main/java/haveno/common/app/Version.java#L83). For example, change it to `"B"`. ## Set your fork's version Optionally add a fourth digit to `Version.VERSION` to represent your fork’s build version. For example, upstream Haveno may use version `1.2.3`, while your fork may use `1.2.3.0` and increment the last version digit as needed. ## Set the network's release date Optionally set the network's approximate release date by setting `RELEASE_DATE` in HavenoUtils.java. This will prevent posting sell offers which no buyers can take before any buyer accounts are signed and aged, while the network bootstraps. After a period (default 60 days), the limit is lifted and sellers can post offers exceeding unsigned buy limits, but they will receive an informational warning for an additional period (default 6 months after release). The defaults can be adjusted with the related constants in HavenoUtils.java. ## Configure trade fees Trade fees can be configured in HavenoUtils.java. The maker and taker fee percents can be adjusted. Set `ARBITRATOR_ASSIGNS_TRADE_FEE_ADDRESS` to `true` for the arbitrator to assign the trade fee address, which defaults to their own wallet. Otherwise set `ARBITRATOR_ASSIGNS_TRADE_FEE_ADDRESS` to `false` and set the XMR address in `getGlobalTradeFeeAddress()` to collect all trade fees to a single address (e.g. a multisig wallet shared among network administrators). ## Build and start price nodes The price node is separated from Haveno and is run as a standalone service. To deploy a pricenode on both TOR and clearnet, see the instructions on the repository: https://github.com/haveno-dex/haveno-pricenode. After the price node is built and deployed, add the price node to `DEFAULT_NODES` in [ProvidersRepository.java](https://github.com/haveno-dex/haveno/blob/3cdd88b56915c7f8afd4f1a39e6c1197c2665d63/core/src/main/java/haveno/core/provider/ProvidersRepository.java#L50). Customize and deploy haveno-pricenode.env and haveno-pricenode.service to run as a system service. ## Update the download URL Change every instance of `https://haveno.exchange/downloads` to your download URL. For example, `https://havenoexample.com/downloads`. ## Set a network filter on mainnet On mainnet, the p2p network is expected to have a filter object for offers, onions, currencies, payment methods, etc. To set the network's filter object: 1. Enter `ctrl + f` in the arbitrator or other Haveno instance to open the Filter window. 2. Enter a developer private key from the previous steps and click "Add Filter" to register. > [!note] > If all seed nodes are restarted at the same time, arbitrators and the filter object will become unregistered and will need to be re-registered. ## Start users for testing Start user1 on Monero's mainnet using `make user1-desktop-mainnet` or Monero's stagenet using `make user1-desktop-stagenet`. Similarly, start user2 on Monero's mainnet using `make user2-desktop-mainnet` or Monero's stagenet using `make user2-desktop-stagenet`. Test trades among the users and arbitrator. ## Build Haveno installers for distribution For mainnet, first modify [package.gradle](https://github.com/haveno-dex/haveno/blob/aeb0822f9fc72bd5a0e23d0c42c2a8f5f87625bb/desktop/package/package.gradle#L252) to `--arguments --baseCurrencyNetwork=XMR_MAINNET`. Then follow these instructions: https://github.com/haveno-dex/haveno/blob/master/desktop/package/README.md. ## Send alerts to update the application Upload updated installers for download * In https:///downloads//, upload the installer files: Haveno-.jar.txt, signingkey.asc, Haveno-.dmg, Haveno-.dmg.asc, and files for Linux and Windows. * In https:///pubkey/, upload pub key files, e.g. F379A1C6.asc. Set the mandatory minimum version for trading (optional) If applicable, update the mandatory minimum version for trading, by entering `ctrl + f` to open the Filter window, enter a private key with developer privileges, and enter the minimum version (e.g. 1.0.19) in the field labeled "Min. version required for trading". Send update alert Enter `ctrl + m` to open the window to send an update alert. Enter a private key which is registered to send alerts. Enter the alert message and new version number, then click the button to send the notification. ## Manually sign payment accounts as the arbitrator Arbitrators can manually sign payment accounts. First open the legacy UI. ### Sign payment account after trade is completed 1. Go to Portfolio > History > open trade details > click 'DETAIL DATA' button. 2. Copy the `,` string for the buyer or seller. 3. Go to Account > `ctrl + i` > `ctrl + p`. 5. Paste the buyer or seller's `,` string. 6. Click the "Import unsigned account age witness" button to confirm. ### Sign payment account from dispute 1. Go to Account > `ctrl + i` > `ctrl + s`. 2. Select payment accounts to sign from disputes. ### Sign unsigned witness pub keys 1. Go to Account > `ctrl + i` > `ctrl + o`. ## Other tips * If a dispute does not open properly, try manually reopening the dispute with a keyboard shortcut: `ctrl + o`. * To send a private notification to a peer: click the user icon and enter `alt + r`. Enter a private key which is registered to send private notifications. ================================================ FILE: docs/developer-guide.md ================================================ # Developer Guide This document is a guide for Haveno development. ## Install and test Haveno [Build Haveno and join the test network or test locally](installing.md). ## Run the UI proof of concept Follow [instructions](https://github.com/haveno-dex/haveno-ui-poc) to run Haveno's UI proof of concept in a browser. This proof of concept demonstrates using Haveno's gRPC server with a web frontend (react and typescript) instead of Haveno's JFX application. ## Import Haveno into development environment VSCode is recommended. Otherwise follow [instructions](import-haveno.md) to import Haveno into a Eclipse or IntelliJ IDEA. ## Run end-to-end API tests Follow [instructions](https://github.com/haveno-dex/haveno-ts#run-tests) to run end-to-end API tests in the UI project. ## Add new API functions and tests 1. Follow [instructions](https://github.com/haveno-dex/haveno-ts#run-tests) to run Haveno's existing API tests successfully. 2. Define the new service or message in Haveno's [protobuf definition](../proto/src/main/proto/grpc.proto). 3. Clean and build Haveno after modifying the protobuf definition: `make clean && make` 4. Implement the new service in Haveno's backend, following existing patterns.
    For example, the gRPC function to get offers is implemented by [`GrpcServer`](https://github.com/haveno-dex/haveno/blob/master/daemon/src/main/java/haveno/daemon/grpc/GrpcServer.java) > [`GrpcOffersService.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/060d9fa4f138ca07f596386972265782e5ec7b7a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java#L102) > [`CoreApi.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/060d9fa4f138ca07f596386972265782e5ec7b7a/core/src/main/java/haveno/core/api/CoreApi.java#L403) > [`CoreOffersService.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/060d9fa4f138ca07f596386972265782e5ec7b7a/core/src/main/java/haveno/core/api/CoreOffersService.java#L131) > [`OfferBookService.getOffers()`](https://github.com/haveno-dex/haveno/blob/060d9fa4f138ca07f596386972265782e5ec7b7a/core/src/main/java/haveno/core/offer/OfferBookService.java#L248). 5. Build Haveno: `make` 6. Update the gRPC client in haveno-ts: `npm install` 7. Add the corresponding typescript method(s) to [HavenoClient.ts](https://github.com/haveno-dex/haveno-ts/blob/master/src/HavenoClient.ts) with clear and concise documentation. 8. Add clean and comprehensive tests to [HavenoClient.test.ts](https://github.com/haveno-dex/haveno-ts/blob/master/src/HavenoClient.test.ts), following existing patterns. 9. Run the tests with `npm run test -- -t 'my test'` to run tests by name and `npm test` to run all tests together. Ensure all tests pass and there are no exception stacktraces in the terminals of Alice, Bob, or the arbitrator. 10. Open pull requests to the haveno and haveno-ts projects for the backend and frontend implementations. ## Release portable Monero binaries for each platform 1. Update the release-v0.18 branch on Haveno's [monero repo](https://github.com/haveno-dex/monero) to the latest release from upstream + any customizations (e.g. a commit to speed up testnet hardforks for local development). 2. `git tag && git push origin ` 3. Follow instructions to [build portable binaries for each platform](#build-portable-monero-binaries-for-each-platform). 4. Publish a new release at https://github.com/haveno-dex/monero/releases with the updated binaries and hashes. 5. Update the paths and hashes in build.gradle and PR. ### Build portable Monero binaries for each platform Based on these instructions: https://github.com/monero-project/monero#cross-compiling > Note: > If during building you get the prompt "Reversed (or previously applied) patch detected! Assume -R? [n]" then confirm 'y'. **Prepare Linux x86_64** 1. Install Ubuntu 20.04 on x86_64. 2. `sudo apt-get update && sudo apt-get upgrade` 3. Install monero dependencies: `sudo apt update && sudo apt install build-essential cmake pkg-config libssl-dev libzmq3-dev libsodium-dev libunwind8-dev liblzma-dev libreadline6-dev libpgm-dev qttools5-dev-tools libhidapi-dev libusb-1.0-0-dev libprotobuf-dev protobuf-compiler libudev-dev libboost-chrono-dev libboost-date-time-dev libboost-filesystem-dev libboost-locale-dev libboost-program-options-dev libboost-regex-dev libboost-serialization-dev libboost-system-dev libboost-thread-dev python3 ccache` 4. `sudo apt install cmake imagemagick libcap-dev librsvg2-bin libz-dev libbz2-dev libtiff-tools python-dev libtinfo5 autoconf libtool libtool-bin gperf git curl` 5. `git clone https://github.com/haveno-dex/monero.git` 6. `cd ./monero` (or rename to haveno-monero: `mv monero/ haveno-monero && cd ./haveno-monero`) 7. `git fetch origin && git reset --hard origin/release-v0.18` 8. `git submodule update --init --force` **Build for Linux x86_64** 1. `make depends target=x86_64-linux-gnu -j` 2. `cd build/x86_64-linux-gnu/release/bin/` 3. `tar -zcvf monero-bins-haveno-linux-x86_64.tar.gz monerod monero-wallet-rpc` 4. Save monero-bins-haveno-linux-x86_64.tar.gz for release. **Build for Mac** 1. `make depends target=x86_64-apple-darwin11 -j` 2. `cd build/x86_64-apple-darwin11/release/bin/` 3. `tar -zcvf monero-bins-haveno-mac.tar.gz monerod monero-wallet-rpc` 4. Save monero-bins-haveno-mac.tar.gz for release. **Build for Windows** 1. `sudo apt install python3 g++-mingw-w64-x86-64 bc` 2. `sudo update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix && sudo update-alternatives --set x86_64-w64-mingw32-gcc /usr/bin/x86_64-w64-mingw32-gcc-posix` 3. `make depends target=x86_64-w64-mingw32 -j` 4. `cd build/x86_64-w64-mingw32/release/bin/` 5. `zip monero-bins-haveno-windows.zip monerod.exe monero-wallet-rpc.exe` 6. Save monero-bins-haveno-windows.zip for release. **Prepare Linux aarch64** 1. Install Ubuntu 20.04 on aarch64. 2. `sudo apt-get update && sudo apt-get upgrade` 3. Install monero dependencies: `sudo apt update && sudo apt install build-essential cmake pkg-config libssl-dev libzmq3-dev libsodium-dev libunwind8-dev liblzma-dev libreadline6-dev libpgm-dev qttools5-dev-tools libhidapi-dev libusb-1.0-0-dev libprotobuf-dev protobuf-compiler libudev-dev libboost-chrono-dev libboost-date-time-dev libboost-filesystem-dev libboost-locale-dev libboost-program-options-dev libboost-regex-dev libboost-serialization-dev libboost-system-dev libboost-thread-dev python3 ccache` 4. `sudo apt install cmake imagemagick libcap-dev librsvg2-bin libz-dev libbz2-dev libtiff-tools python-dev libtinfo5 autoconf libtool libtool-bin gperf git curl` 5. `git clone https://github.com/haveno-dex/monero.git` 6. `cd ./monero` (or rename to haveno-monero: `mv monero/ haveno-monero && cd ./haveno-monero`) 7. `git fetch origin && git reset --hard origin/release-v0.18` 8. `git submodule update --init --force` **Build for Linux aarch64** 1. `make depends target=aarch64-linux-gnu -j` 2. `cd build/aarch64-linux-gnu/release/bin/` 3. `tar -zcvf monero-bins-haveno-linux-aarch64.tar.gz monerod monero-wallet-rpc` 4. Save monero-bins-haveno-linux-aarch64.tar.gz for release. ## Build executable installers for each platform See [instructions](/desktop/package/README.md). ## Rebase and squash your commits When submitting a pull request for review, please first rebase and squash your commits. 1. Checkout the latest version from master, e.g.: `git checkout master && git pull upstream master` 2. Checkout your feature branch, e.g.: `git checkout your_branch` 3. Optionally make a backup branch just in case something goes wrong, e.g.: `git checkout -b your_branch_bkp && git checkout your_branch` 4. Rebase on master: `git rebase master` 5. Squash your commits: `git reset --soft ` 6. Commit your changes to a single commit: `git commit` 7. Push your local branch to your remote repository: `git push --force` If you have a PR open on that branch, it'll be updated automatically. ## Trade Protocol For documentation of the trade protocol, see [trade protocol](trade_protocol/trade-protocol.pdf). ================================================ FILE: docs/external-tor-usage.md ================================================ # **Using External `tor` with Haveno** ## [How to Install little-t-`tor` for Your Platform](https://support.torproject.org/little-t-tor/#little-t-tor_install-little-t-tor) The following `tor` installation instructions are presented here for convenience. * **For the most complete, up-to-date & authoritative steps, readers are encouraged to refer to the [Tor Project's Official Homepage](https://www.torproject.org) linked in the header** * **Notes:** For optimum compatibility with Haveno the running `tor` version should match that of the internal Haveno `tor` version For best results, use a version of `tor` which supports the [Onion Service Proof of Work](https://onionservices.torproject.org/technology/security/pow) (`PoW`) mechanism * (IE: `GNU` build of `tor`) --- * **Note Regarding Admin Access:** To install `tor` you need root privileges. Below all commands that need to be run as `root` user like `apt` and `dpkg` are prepended with `#`, while commands to be run as user with `$` resembling the standard prompt in a terminal. ### macOS #### Install a Package Manager Two of the most popular package managers for `macOS` are: [`Homebrew`](https://brew.sh) and [`Macports`](https://www.macports.org) (You can use the package manager of your choice) + Install [`Homebrew`](https://brew.sh) Follow the instructions on [brew.sh](https://brew.sh) + Install [`Macports`](https://www.macports.org) Follow the instructions on [macports.org](https://www.macports.org) #### Package Installation ##### [`Homebrew`](https://brew.sh) ```shell # brew update && brew install tor ``` ##### [`Macports`](https://www.macports.org) ```shell # port sync && port install tor ``` ### Debian / Ubuntu * *Do **not** use the packages in Ubuntu's universe. In the past they have not reliably been updated. That means you could be missing stability and security fixes.* * Configure the [Official Tor Package Repository](https://deb.torproject.org/torproject.org) Enable the [Official Tor Package Repository](https://deb.torproject.org/torproject.org) following these [instructions](https://support.torproject.org/apt/tor-deb-repo/) #### Package Installation ```shell # apt update && apt install tor ``` ### Fedora * Configure the [Official Tor Package Repository](https://rpm.torproject.org/fedora) Enable the [Official Tor Package Repository](https://rpm.torproject.org/fedora) by following these [instructions](https://support.torproject.org/rpm/tor-rpm-install) #### Package Installation ``` # dnf update && dnf install tor ``` ### Arch Linux #### Package Installation ```shell # pacman -Fy && pacman -Syu tor ``` ### Installing `tor` from source #### Download Latest Release & Dependencies The latest release of `tor` can be found on the [download](https://www.torproject.org/download/tor) page * When building from source: *First* install `libevent`,`openssl` & `zlib` *(Including the -devel packages when applicable)* #### Install `tor` ```shell $ tar -xzf tor-.tar.gz; cd tor- ``` * Replace \ with the latest version of `tor` > For example, `tor-0.4.8.14` ```shell $ ./configure && make ``` * Now you can run `tor` (0.4.3.x and Later) locally like this: ```shell $ ./src/app/tor ``` Or, you can run `make install` (as `root` if necessary) to install it globally into `/usr/local/` * Now you can run `tor` directly without absolute path like this: ```shell $ tor ``` ### Windows #### Download * Download the `Windows Expert Bundle` from the [Official Tor Project's Download page](https://www.torproject.org/download/tor) #### Extract * Extract Archive to Disk #### Open Terminal * Open PowerShell with Admin Privileges #### Change to Location of Extracted Archive * Navigate to `Tor` Directory #### Package Installation * v10 ```powershell PS C:\Tor\> tor.exe –-service install ``` * v11 ```powershell PS C:\Tor\> tor.exe –-service install ``` #### Create Service ```powershell PS C:\Tor\> sc create tor start=auto binPath="\Tor\tor.exe -nt-service" ``` #### Start Service ```powershell PS C:\Tor\> sc start tor ``` ## Configuring `tor` via `torrc` #### [I'm supposed to "edit my torrc". What does that mean?](https://support.torproject.org/tbb/tbb-editing-torrc/) * Per the [Official Tor Project's support page](https://support.torproject.org/tbb/tbb-editing-torrc/): * **WARNING:** Do **NOT** follow random advice instructing you to edit your torrc! Doing so can allow an attacker to compromise your security and anonymity through malicious configuration of your torrc. **Note:** The `torrc` location will ***not*** match those stated in the documentation linked above and will vary across each platform. #### [Sample `torrc`](https://gitlab.torproject.org/tpo/core/tor/-/blob/HEAD/src/config/torrc.sample.in) Users are ***strongly*** encouraged to review both the [Official Tor Project's support page](https://support.torproject.org/tbb/tbb-editing-torrc/) as well as the [sample `torrc`](https://gitlab.torproject.org/tpo/core/tor/-/blob/HEAD/src/config/torrc.sample.in) before proceeding. #### Enable `torControlPort` in `torrc` In order for Haveno to use the `--torControlPort` option, it must be enabled and accessible. The most common way to do so is to edit the `torrc` fiel with a text editor to ensure that an entry for `ControlPort` followed by port number to listen on is present in the `torrc` file. #### [Authentication](https://spec.torproject.org/control-spec/implementation-notes.html#authentication) Per the [Tor Control Protocol - Implementation Notes](https://spec.torproject.org/control-spec/implementation-notes.html): * ***"If the control port is open and no authentication operation is enabled, `tor` trusts any local user that connects to the control port. This is generally a poor idea."*** ##### `CookieAuthentication` If the `CookieAuthentication` option is true, `tor` writes a *"magic cookie"* file named `control_auth_cookie` into its data directory (or to another file specified in the `CookieAuthFile` option). ##### Example: ```shell ControlPort 9051 CookieAuthentication 1 ``` ##### `HashedControlPassword` If the `HashedControlPassword` option is set, it must contain the salted hash of a secret password. The salted hash is computed according to the S2K algorithm in `RFC 2440` of `OpenPGP`, and prefixed with the s2k specifier. This is then encoded in hexadecimal, prefixed by the indicator sequence "16:". * `HashedControlPassword` can be generated like so: ```shell $ tor --hash-password ``` ###### Example: ```shell ControlPort 9051 HashedControlPassword 16:C01147DC5F4DA2346056668DD23522558D0E0C8B5CC88FE72EEBC51967 ``` ##### Restart `tor` `tor` must be restarted for changes to `torrc` to be applied. ### \* ***Optional*** \* #### [Set Up Your Onion Service](https://community.torproject.org/onion-services/setup) While not a *strict* requirement for use with Haveno, some users may wish to configure an [Onion Service](https://community.torproject.org/onion-services) * ***Only Required When Using The Haveno `--hiddenServiceAddress` Option*** Please see the [Official Tor Project's Documentation](https://community.torproject.org/onion-services/setup) for more information about configuration and usage of these services --- ## Haveno's `tor` Aware Options Haveno is a natively `tor` aware application and offers **many** flexible configuration options for use by privacy conscious users. While some are mutually exclusive, many are cross-applicable. Users are encouraged to experiment with options before use to determine which options best fit their personal threat profile. ### Options #### `--hiddenServiceAddress` * Function: This option configures a *static* Hidden Service Address to listen on * Expected Input Format: `` (`ed25519`) * Acceptable Values `` * Default value: `null` #### `--socks5ProxyXmrAddress` * Function: A proxy address to be used for `monero` network * Expected Input Format: `` * Acceptable Values `` * Default value: `null` #### `--torrcFile` * Function: An existing `torrc`-file to be sourced for `tor` **Note:** `torrc`-entries which are critical to Haveno's flawless operation (`torrc` options line, `torrc` option, ...) **can not** be overwritten * Expected Input Format: `` * Acceptable Values `` * Default value: `null` #### `--torrcOptions` * Function: A list of `torrc`-entries to amend to Haveno's `torrc` **Note:** *`torrc`-entries which are critical to Haveno's flawless operation (`torrc` options line, `torrc` option, ...) can **not** be overwritten* * Expected Input Format: `` * Acceptable Values `<^([^\s,]+\s[^,]+,?\s*)+$>` * Default value: `null` #### `--torControlHost` + Function The control `hostname` or `IP` of an already running `tor` service to be used by Haveno * Expected Input Format `` (`hostname`, `IPv4` or `IPv6`) * Acceptable Values `` * Default Value `null` #### `--torControlPort` + Function The control port of an already running `tor` service to be used by Haveno * Expected Input Format `` * Acceptable Values `` * Default Value `-1` #### `--torControlPassword` + Function The password for controlling the already running `tor` service * Expected Input Format `` * Acceptable Values `` * Default Value `null` #### `--torControlCookieFile` + Function The cookie file for authenticating against the already running `tor` service * Used in conjunction with `--torControlUseSafeCookieAuth` option * Expected Input Format `` * Acceptable Values `` * Default Value `null` #### `--torControlUseSafeCookieAuth` + Function Use the `SafeCookie` method when authenticating to the already running `tor` service * Expected Input Format `null` * Acceptable Values `none` * Default Value `off` #### `--torStreamIsolation` + Function Use stream isolation for Tor * This option is currently considered ***experimental*** * Expected Input Format `` * Acceptable Values `` * Default Value `off` #### `--useTorForXmr` + Function Configure `tor` for `monero` connections with ***either***: * after_sync **or** * off **or** * on * Expected Input Format `` * Acceptable Values `` * Default Value `AFTER_SYNC` #### `--socks5DiscoverMode` + Function Specify discovery mode for `monero` nodes * Expected Input Format `` * Acceptable Values `ADDR, DNS, ONION, ALL` One or more comma separated. *(Will be **OR**'d together)* * Default Value `ALL` --- ## Starting Haveno Using Externally Available `tor` ### Dynamic Onion Assignment via `--torControlPort` ```shell $ /opt/haveno/bin/Haveno --torControlPort='9051' --torControlCookieFile='/var/run/tor/control.authcookie' --torControlUseSafeCookieAuth --useTorForXmr='on' --socks5ProxyXmrAddress='127.0.0.1:9050' ``` ### Static Onion Assignment via `--hiddenServiceAddress` ```shell $ /opt/haveno/bin/Haveno --socks5ProxyXmrAddress='127.0.0.1:9050' --useTorForXmr='on' --hiddenServiceAddress='2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion' ``` ================================================ FILE: docs/flatpak.md ================================================ # Flatpak distribution The `.flatpak` binary files (known as "bundles") that `./gradlew packageInstallers` creates can be used to download and install Haveno, but there are several security issues that arise in Flatpak when only using the bundle files: - There is no [digital signature](https://en.wikipedia.org/wiki/Digital_signature), if a bad actor were to upload a malicious `.flatpak` the users would have no way to tell when upgrading. - Upgrading isn't as easy, your users need to find the new Flatpak bundle file, and you cannot update multiple apps easily. - This also makes an accidental downgrade much more likely. Flatpak has a solution for these issues, a [Flatpak repository](https://docs.flatpak.org/en/latest/repositories.html). Flatpak repos store the data of their apps within an OSTree (almost like git) repository, and the commits can be signed with a GPG key. The nature of OSTree also allows for easy updates, as the Flatpak client can download deltas of the changes instead of the entire file. If you plan on distributing Haveno as a Flatpak, it's recommended to create a Flatpak repository as well. This guide will show you how to create a Flatpak repository for Haveno. The official documentation states that [it's possible to use GitHub/Lab Pages](https://docs.flatpak.org/en/latest/hosting-a-repository.html#hosting-a-repository-on-gitlab-github-pages) to host the repository, but this hasn't been tested. The more common way is to use a web server, or something like [flat-manager](https://github.com/flatpak/flat-manager). An example Haveno flat-manager solution using `docker-compose` has been created and documented at if you want a quick way to get started. Note that this does require an always-on server. ================================================ FILE: docs/import-haveno.md ================================================ # Importing Haveno dev environment This document describes how to import Haveno into an integrated development environment (IDE). First [install and run a Haveno test network](installing.md), then use the following instructions to import Haveno into an IDE. ## Visual Studio Code (recommended) 1. Download and open Visual Studio Code: https://code.visualstudio.com/. 2. File > Add folder to Workspace... 3. Browse to the `haveno` git project. ## Eclipse IDE > Note: Use default values unless specified otherwise. 1. If you haven't already, first [install and run a Haveno test network](installing.md). 2. Download and run the [Eclipse](https://www.eclipse.org/downloads/) installer. 3. Select "Eclipse IDE for Enterprise Java and Web Developers" to install. 4. Launch an eclipse workspace and close out of the welcome screen. 5. [Download](https://search.maven.org/search?q=g:org.projectlombok%20AND%20a:lombok&core=gav) the latest version of the lombok jar. 6. Run lombok jar, e.g.: `java -jar ~/Downloads/lombok-1.18.22.jar`. 7. Follow prompts to install lombok to your Eclipse installation. 8. Restart Eclipse. 9. File > Import... > Existing Gradle Project. 10. Select the location of "haveno" project, e.g. ~/git/haveno. 11. Advance to finish importing haveno project. 12. Right click haveno project > Gradle > Refresh Gradle Project. 13. File > Import... > Existing Projects into Workspace. 14. Select the location of "haveno-ts" project, e.g. ~/git/haveno-ts. 15. Advance to finish importing haveno-ts project. You are now ready to make, run, and test changes to the Haveno project! ## IntelliJ IDEA > Note: These instructions are outdated and for Haveno. Most Haveno contributors use IDEA for development. The following instructions have been tested on IDEA 2021.1. 1. Follow the instructions in [build.md](build.md) to clone and build Haveno at the command line. 1. Open IDEA 1. Go to `File -> Settings -> Build, Execution, Deployment -> Compiler -> Annotation Processors` and check the `Enable annotation processing` option to enable processing of Lombok annotations (Lombok plugin installed by default since v2020.3) 1. Go to `File -> New -> Project from Existing Sources...` and then select the main Haveno folder to load automatically the related Gradle project 1. If you did not yet setup JDK11 in IntelliJ, go to `File-> Project Structure -> Project` and under the `Project SDK` option locate your JDK11 folder 1. Select JDK 11 for Gradle as well. Go to `File -> Settings -> Build, Execution, Deployment -> Build Tools -> Gradle` and select the JDK11 location for the Gradle JVM value 1. Go to `Build -> Build Project`. Everything should build cleanly 1. Go to `Run > Edit Configurations... -> Plus (+) icon on the top left -> Application` anf then fill the requested fields as shown below, while using as CLI arguments one of those listed in [dev-setup.md](dev-setup.md): ![edit_configurations.png](edit_configurations.png) 9. Now you should be able to run Haveno by clicking on the _Play_ button or via `Run -> Run 'Haveno Desktop'` 10. If you want to debug the application and execute breakpoints, use `Run -> Debug 'Haveno Desktop'` > TIP: If you encounter compilation errors in IDEA related to the `protobuf.*` classes, it is probably because you didn't build Haveno at the command line as instructed above. You need to run the `generateProto` task in the `other` project. You can do this via the Gradle tool window in IDEA, or you can do it the command line with `./gradlew :other:generateProto`. Once you've done that, run `Build -> Build Project` again and you should have no errors. > > If this does not solve the issue, try to execute `./gradlew clean` and then rebuild the project again. ================================================ FILE: docs/installing.md ================================================ # Build and run Haveno These are the steps to build and run Haveno using the *official test network*. > [!warning] > The official Haveno repository does not support making real trades directly. > > To make real trades with Haveno, first find a third party network, and then use their installer or build their repository. We do not endorse any networks at this time. > > Alternatively, you can [create your own mainnet network](create-mainnet.md). ## Install dependencies manually On Ubuntu: `sudo apt install make wget git` On Windows, first install MSYS2: 1. Install [MSYS2](https://www.msys2.org/). 2. Start MSYS2 MINGW64 or MSYS MINGW32 depending on your system. Use MSYS2 for all commands throughout this document. 4. Update pacman: `pacman -Syy` 5. Install dependencies. During installation, use default=all by leaving the input blank and pressing enter. 64-bit: `pacman -S mingw-w64-x86_64-toolchain make mingw-w64-x86_64-cmake git zip unzip` 32-bit: `pacman -S mingw-w64-i686-toolchain make mingw-w64-i686-cmake git zip unzip` On all platforms, install Java JDK 21: ``` curl -s "https://get.sdkman.io" | bash sdk install java 21.0.9.fx-librca ``` Restart the terminal for the changes to take effect. ## Install dependencies using Nix Alternatively, dependencies can be installed automatically using [NixOS](https://nixos.org) or [Nix](https://nixos.org/download) on any platform: ``` git clone https://github.com/haveno-dex/haveno.git cd haveno nix-shell ``` *Using Nix, there is no need to install Java dependencies, because they are managed by the shell.nix file. Build and run haveno in nix-shell terminal.* ## Build Haveno If it's the first time you are building Haveno, run the following commands to download the repository, the needed dependencies, and build the latest release. If using a third party network, replace the repository URL with theirs: ``` git clone https://github.com/haveno-dex/haveno.git cd haveno git checkout v1.2.3 make ``` *If you only want to quickly build the binaries, use `make skip-tests` instead of `make`. It will skip the tests and increase the build speed drastically.* If you are updating from a previous version, run from the root of the repository: ``` git checkout v1.2.3 git pull make clean && make ``` ## Run Haveno > [!note] > When you run Haveno, your application folder will be installed to: > * Linux: `~/.local/share/Haveno/` > * macOS: `~/Library/Application\ Support/Haveno/` > * Windows: `~\AppData\Roaming\Haveno\` ### Mainnet If you are building a third party repository which supports mainnet, you can start Haveno with: ``` make haveno-desktop-mainnet ``` ### Join the public test network If you want to try Haveno in a live setup, launch a Haveno instance that will connect to other peers on our public test environment, which runs on Monero's stagenet (you won't need to download the blockchain locally). You'll be able to make test trades with other users and have a preview of Haveno's trade protocol in action. Note that development is very much ongoing. Things are slow and might break. Steps: 1. Run `make user1-desktop-stagenet` to start the application. 2. Click on the "Funds" tab in the top menu and copy the generated XMR address. 3. Go to the [stagenet faucet](https://stagenet-faucet.xmr-tw.org) and paste the address above in the "Get XMR" field. Submit and see the stagenet coins being sent to your Haveno instance. 4. While you wait the 10 confirmations (20 minutes) needed for your funds to be spendable, create a fiat account by clicking on "Account" in the top menu, select the "National currency accounts" tab, then add a new account. For simplicity, we suggest to test using a Revolut account with a random ID. 5. Now pick up an existing offer or open a new one. Fund your trade and wait 10 blocks for your deposit to be unlocked. 6. Now if you are taking a trade you'll be asked to confirm you have sent the payment outside Haveno. Confirm in the app and wait for the confirmation of received payment from the other trader. 7. Once the other trader confirms, deposits are sent back to the owners and the trade is complete. ### Run a local test network If you are a developer who wants to test Haveno in a more controlled way, follow the next steps to build a local test environment. #### Run a local XMR testnet 1. In a new terminal window run `make monerod1-local` 1. In a new terminal window run `make monerod2-local` 3. Now mine the first 150 blocks to a random address before using, so wallets only use the latest output type. Run in one of the terminal windows opened above: `start_mining 9tsUiG9bwcU7oTbAdBwBk2PzxFtysge5qcEsHEpetmEKgerHQa1fDqH7a4FiquZmms7yM22jdifVAD7jAb2e63GSJMuhY75 1` #### Deploy If you are a *screen* user, simply run `make deploy-screen`. This command will open all needed Haveno instances (seednode, user1, user2, arbitrator) using *screen*. If you are a *tmux* user, simply run `make deploy-tmux`. This command will open all needed Haveno instances (seednode, user1, user2, arbitrator) using *tmux* and attach them to splitted view. If you don't use *screen* or *tmux*, open 4 terminal windows and run in each one of them: 1. `make seednode-local` 2. `make user1-desktop-local` or if you want to run user1 as a daemon: `make user1-daemon-local` 3. `make user2-desktop-local` or if you want to run user2 as a daemon: `make user2-daemon-local` 4. `make arbitrator-desktop-local` or if you want to run arbitrator as a daemon: `make arbitrator-daemon-local` 5. Optionally run a [local price node](https://github.com/haveno-dex/haveno-pricenode/blob/main/README.md) If this is the first time launching the arbitrator desktop application, register the arbitrator after the interface opens. Go to the *Account* tab and press `cmd+r`. Confirm the registration of the arbitrator. #### Fund your wallets When running user1 and user2, you'll see a Monero address prompted in the terminal. Send test XMR to the addresses of both user1 and user2 to be able to initiate a trade. You can fund the two wallets by mining some test XMR coins to those addresses. To do so, open a terminal where you ran monerod and run: `start_mining ADDRESS 1`. monerod will start mining local testnet coins on your device using one thread. Replace `ADDRESS` with the address of user1 first, and then user2's. Run `stop_mining` to stop mining. #### Start testing You are all set. Now that everything is running and your wallets are funded, you can create test trades between user1 and user2. Remember to mine a few blocks after opening and accepting the test trade so the transaction will be confirmed. ================================================ FILE: docs/tor-upgrade.md ================================================ # Tor upgrade in Haveno This guide describes the steps necessary to upgrade the tor dependencies used by Haveno. ## Background Haveno uses two libraries for tor: [netlayer][1] and [tor-binary][2]. As per the project's authors, `netlayer` is _"essentially a wrapper around the official Tor releases, pre-packaged for easy use and convenient integration into Kotlin/Java projects"_. Similarly, `tor-binary` is _"[the] Tor binary packaged in a way that can be used for java projects"_. The project unpacks the Tor Browser binaries to extract and repackage the tor binaries themselves. Therefore, upgrading tor in Haveno comes down to upgrading these two artefacts. ## Upgrade steps ### 1. Decide if upgrade necessary - Find out which tor version Haveno currently uses - Find out the current `netlayer` version (see `netlayerVersion` in `haveno/build.gradle`) - Find that tag on the project's [Tags page][3] - The tag description says which tor version it includes - Find out the latest available tor release - See the [official tor changelog][4] ### 2. Update `tor-binary` During this update, you will need to keep track of: - the new Tor Browser version - the new tor binary version Create a PR for the `master` branch of [tor-binary][2] with the following changes: - Decide which Tor Browser version contains the desired tor binary version - The latest official Tor Browser releases are here: https://dist.torproject.org/torbrowser/ - All official Tor Browser releases are here: https://archive.torproject.org/tor-package-archive/torbrowser/ - For the chosen Tor Browser version, get the list of SHA256 checksums and its signature - For example, for Tor Browser 14.0.7: - https://dist.torproject.org/torbrowser/14.0.7/sha256sums-signed-build.txt - https://dist.torproject.org/torbrowser/14.0.7/sha256sums-signed-build.txt.asc - Verify the signature of the checksums list (see [instructions][5]) - Update the `tor-binary` checksums - For each file present in `tor-binary/tor-binary-resources/checksums`: - Rename the file such that it reflects the new Tor Browser version, but preserves the naming scheme - Update the contents of the file with the corresponding SHA256 checksum from the list - Update `torbrowser.version` to the new Tor Browser version in: - `tor-binary/build.xml` - `tor-binary/pom.xml` - Update `version` to the new tor binary version in: - `tor-binary/pom.xml` - `tor-binary-geoip/pom.xml` - `tor-binary-linux32/pom.xml` - `tor-binary-linux64/pom.xml` - `tor-binary-macos/pom.xml` - `tor-binary-windows/pom.xml` - `tor-binary-resources/pom.xml` - Run `mvn install` - If it completes successfully, then the artefact is correctly configured Only the files listed above should be part of the PR. The last step will generate a few extra artefacts (for example in `tor-binary-resources/src/main/resources`), but these should NOT be committed. Once the PR is merged, make a note of the commit ID in the `master` branch (for example `a4b868a`), as it will be needed next. ### 3. Update `netlayer` Create a PR for the `master` branch of [netlayer][1] with the following changes: - In `netlayer/pom.xml`: - Update `tor-binary.version` to the `tor-binary` commit ID from above (e.g. `a4b868a`) - Bump `version`, representing the `netlayer` artefact version, in: - `netlayer/pom.xml` - `netlayer/tor/pom.xml` - `netlayer/tor.external/pom.xml` - `netlayer/tor.native/pom.xml` Once the PR is merged, make a note of the commit ID in the `master` branch (for example `32779ac`), as it will be needed next. Create a tag for the new artefact version, having the new tor binary version as description, for example: ``` # Create tag locally for new netlayer release, on the master branch git tag -s 0.7.0 -m"tor 0.4.5.6" # Push it to netlayer repo git push origin 0.7.0 ``` ### 4. Update dependency in Haveno Create a Haveno PR with the following changes: - In `haveno/build.gradle` update `netlayerVersion` to the `netlayer` commit ID from above - Update the gradle dependency checksums - See instructions in `haveno/gradle/witness/gradle-witness.gradle` ## Credits Thanks to freimair, JesusMcCloud, mrosseel, sschuberth and cedricwalter for their work on the original [tor-binary](https://github.com/JesusMcCloud/tor-binary) and [netlayer](https://github.com/JesusMcCloud/netlayer) repos. [1]: https://github.com/haveno-dex/netlayer "netlayer" [2]: https://github.com/haveno-dex/tor-binary "tor-binary" [3]: https://github.com/haveno-dex/netlayer/tags "netlayer Tags" [4]: https://gitweb.torproject.org/tor.git/plain/ChangeLog "tor changelog" [5]: https://support.torproject.org/tbb/how-to-verify-signature/ "verify tor signature" ================================================ FILE: docs/trade_protocol/trade-protocol.drawio ================================================ 7V1Zl5tIlv41OlP1IA5EsD460+WuqrG7s21Pl6tf5iAJpThGoAaUSz3Mb5+IYCcCARLB4gyXjysTSSG494u7xV1W8P748rfQPh0+BTvHWwF597KC71cAKAYAK/xX3r0mVwBU0iuPobtL31Vc+OL+5aQX5fTq2d05UeWNcRB4sXuqXtwGvu9s48o1OwyD5+rb9oFX/daT/ehQF75sbY+++oe7iw/JVRMYxfVfHffxkH2zolvJK0c7e3P6JNHB3gXPpUvwlxW8D4MgTn46vtw7HqZeRpc/fnv9w/v4Xf/b7/+M/mP/z91/f/37v9bJYh/6fCR/hNDx46uX/mZu3x8/riPDir6to3jz7V57WatmsvaT7Z1TgqUPG79mFHwMg/NpBe88e+N4d/b2O77g7+4DLwjJO+Ce/EFv6Xij6QM9OWHsvLBQYG+yLy8IjSDqBEcnDl/R+7JPqZqkpx9NAarAlF3PBbd1Nb12KHHaTK/ZKcAe89ULIqIfUjr2oCnUOdP0MidvJmpGrJSgawZBx6Wn2k5PtDdP+Ed7G2MC3j0f3Nj5crK3+OIzEm3o2iE+ou99r6AfozgMvufyAF/ZB36cCi8kueCd7bmPPvrFc/bxoHzK0VHnE82PFoLr3AiuMQiue3FKpgrl9f+cg+yFdUQI+A69QQGnl+JF9NMj/v8n+7sTZkuhW0tWS16jWIqoE7O4ltHeD3wHf7HrebVLGe+2iBMORgOmtYtUwrv0haO72+GvYcKEMNjB9JEZuJgECK9VhrfgAnDDhbFswaaphqRqMxNuHRTwgoSb0cCrGQk3i5Nwexdu3Di0MYuEhOuLhvEl3Bf/8/++/O7sHv8pv77/Gj28V7bRWukg4Rx/9w77JQU3SgxEBAlfv2HKSlr265/l196/pGRPfntNfxteXMZ2+Ohc5E66ERAJnIu8KTFDYzAjuxY6nh27T1W3i8Wh9BseApdst0w465RwttQan6PgHG6d9INlV6e2lm6A+lq5P5qtldCHWovAJn/468VMB5v1IpAGd7ZmwmYgV/miyMaVTAayXl2prqM5c1hjKZJWFu/s6EBEr1LlN77+YMdImvvkCpAhpcrBUNJ6AThRkDQwKvzV6ju4K1DMDmvxxgpoNDqik+1fb3Rkq2yyC79J6L2/+W7sIhsBfRbI/9jvK07Xpv5ZdC25iQYzpWouDGZ85rZL2b5JcAu2W8oYQq/s9I2u6c2gT7dYP7x3ME4aIZkrldKeUBTGpoC8DBZazXxB1CUs/+z85+xEMcXO6Nk9enZiWWLWpK9g1mwPrrf7aL8GZ3zfUYwonP12dwhC9y/0frvgtB1mjAV65R1f8CfTNUMHY/gh44ZSu/TJfqm88aMdxdndBJ5nnyI38e7wB49ol7r+XRDHwTF9EwUSmfwZJVzZjBRgSumNJH/UKmwMjYaNeUGWDg4bOsBz9k/njecS5YSFd+SET1h6xC8UgFK3hemr1DzLulsSB6dE5G1d//Ejec97tbjyOX10fClAn917RHUekDPj+MRxie3M28csPmF5TWij3aG/iFr32O7W0F3eo9+V4nf0F789jO8DH9247RKGOwhsz3iPsIRQApGrUHRxq3Z2hoDeDSVA5YQSpUMgu2ThbD07itxtzSF6cWPsD61lSZaN9MKf6QUVpBcKrwj/8lr65cEJXfQ0WFFc7yhd2tWJFXGJCvKsDCNoaJJWtaGNzNfpaxkhGVVZCFrczCI2aTsEv7oCTClhK3W+G4E1PojUWWEI0BjKodDb164dF1EL8cZQczzv7DENa8/1nXVG1dy0xiyXWTY3MZQbzG7P7Wy6s5Y5Bdg+k4PEQpfjAP2zCYLvUck0x19B2ev4yVqtdeVWax10DhUzIpV4M2n4P5aRppM/6deVrid/prPMIMsy001JVem9WA9ODaZ2MyE4hFSUZy0VwbxUq66oUnYekwWkVVXSZHClelWgJJuNYNPrapuzpASsmORCJCXa4e4eI8H2sX8SYaGE+Z26uDOVlk3y8LIU7S8tq378NKJTBwzRaeiSBcYUnbRjS2Ih755sFxHd9eLXz050Qj6gQ0HhrcRFWCCfb6xEtxghNoOhA+rHCMNhivaCjziP5b+iVBDZ8Tl0iEgkRlxo75yybHrL0ZPBsHZ5t98UUmGBSeUVUgH9DpiZtl0HAhbH0KvSIXTpTLrhGLrfJi8fMbOf1pyVfVcPdxj1PILOhp3RshBnSw6yPISlWHJIyO3s2GGJyjduxvFSsTpSsVnWfoZYllplBZe5nVyhJ65jGB9bfsWYEEdXk5ljukphxTQYWNEviOnhsUJ7rnFuggmDi8txVb4/bzKuWDDhdl7FzFv/IQNnGXPmYlhZpqSVnThYFSDWLUE0INcWGzdqxqzmucLWAnKzuTWErdUcMutjZt3yhaFzDEjOQHaesQ+Rrpv7icbSjbuW+AlTeZvssNwQOffsPUR7vcLWm97Wa4GOpQMGdNiHYUPUx7APzmnxK1KVpgi29S/mYOYvseHDzSQEjOjtT4Hv4duMHGJnuPuSysLxXPIDuoD1+M8CUnOClMZSZmxI8QtcLDjhpBR8G9gavHTTvVLcWQtF9pNDifqG5PU0Hz4c5M6Ftco/FMk2MziVhLJtDPn2uMGPWocDAZSUak6MIl/ry+s1X55eibMzr7E4PUTxryK9waLfYUSCIddzUQHjcGLUMmBN4QQTIGAyHEygOTVMWIf5fKr66LK+r0lUT5T1DV/WpzHaEHAr62PWi+YrCyej1TdIUkGDWpXr8N9z0QeZl5ew9Cx9fAitVoORgBXH1ixJN0d0FGjPv1Rt+8bTSqfMWdAppxLqjGxklnHALWdBoSV4OciYJo2+5eDicIFEqzNiLoSmmaYjt9LaLrW1c268CTWp3n8lN7TH6LzZQFXeHepamDlY780R2tE1PMpS+9G1AGIWDeka7pFXR7q32m7zZijMpRsdoHNklyXONM2cWJ4xix6XK89A96zMyeQZowhvGHn2pjts3oyH2Qi1fl1l3k5rRMWibOoigbrvURteTFWt/I9ZDXEqo7fp6NLFfT6tVW8qe8sl4EyAZeA+BEMBy1RMyvNT64txB9NASdmTtjLYuyFp/5IHyWUX17OkWdM2S9kNHtXehk4SqT+FDsb/Dv14PHuxG7mPBIzzDXIvPRWmJftWURVWl0DITHbTtGaZcJu2Zna5FUfh08a7a1k0OqSRMq5Np9NRI3xY/SmVJCLHfzqs1JO3VFbPWkvOD1AqPTW4hb8tWqxc0EBsgfA2Tkn4pmAXO/ems5MG/HDsTErjhwiBKniIdeMGPrFvgq0TRQJLs8ISK59/dCwZtGcqlNcslFebiawxMgCb8MMv3mnQ8U6hy6aQP/lG5qHLID/502+QyNU9obLOBpJlwVXR3UCRZHC5vwGrD3dDh6mWWFw/QdHemjvbeu0hOWNmraiARUXR9GsHnFBlFfrofbzHqaBh9fpWLkN3HjDVFwpTDQnD4WAKa2cQer1nI3eYss7TFxI2ziO1x6Sbh4jSziJKq7Pykg0oafSe5Nb1VjEHHNSBzYPynA5kHsg3tz+6Tgyz7I7pLAhzXqfFEDIsiGsbWiKZTi82uhExQG/UHGhKX6DhD5EhNXBw8I8BY6MzjOfVOoxpYVw99xPQixmjzyMBjUbGbSdmUJyYXX8KIkGliguTMepj1EMzIItDs4XGHU1GpUNT3IhbgRGQ6bhRg28gYo7cWtDLizw/A0qHzHJ+feg7mmPdwzwDm1M5V9s73Gdsm4k5BUFtvLaZHcr17ruq07aUWRdmvCcWdannmg6m+DcqLr4k7M4rTRUqtTC2WS8L6YpdzaqtZNU7NXAH7gCTLfsA9yp3V54MoxmnO2BUnxVGNasuX7Vr5Wsd7dbIQ0Ryz6ZLadT2HHqvdyECI2ZXW5S4GlJuNvFrNtsvOv6v6jmC7u0txvAMLEPSqs3N2UkIgJmmq3BrSqmzeMmpFRHdi+hT4W/8YXueU274IvoSXQk1WJMPkFEbjsRPbqSN0vwUmB3KeJbVt6CY5jRZ3wJgdphcMXyhb8HMxfctAF2OwJZT51sAYr51voB5XiP6Fgxc4tsHCjMp8QUWq3/mksTZ5H0LAPMUabnyzOpu/kwmz6zm7CDRt2BgodYDD7MRarcWsP/AfQtWZYMaqtee31NNC0BlYaCOHX+2+k2Uezs8V3VTkrWCU4NBAC+sIBzoGk6jy8TEdAC4tVnJcrpW5PpvJhAzFMpVvx5WJp0ZBKhEZu5g4lVLL6YPXB1l00wpO2XPcQEmzgyCMq12cKPhe0S2EBn7IjNorplB7NbxDdkd3JxHKNNaa+ecgsjFMgb3LcefbxANIitogMFmcu+YzRyygiCj2cvJfj0mc/Ls7RaRJSbP/+oF9g7flh0dBJBmBaQeJfX8RuQx08uWVmhl+xji6XyJbap7RaHVRJqVXWhlSVlYZpRKK6j0CwKNm5A2sEMI55bbWGtlBK6vE6HLncDYxahQ6Rdbur3sX5bLdX2yZJn6qm/Zfz+ItSaCwcxcngvE9DrE4EDpXRPga8E9Mv0gdvcu6Z8S4PHMqfcSCe07jfZFYGI0oxy5zBkys8GHiJ+pIn52dfyMSohEbDKkqSNojPzr94kMEYO65tOMEio9wmXcCukgcwCLCGpwC2ooi6yZg4xRIg9JdOxdEhp7SOJiIjw/1/A8BIxcfSBf8Cc4oIjRLfBSjFXIIW5yKN/Qt8ghJnz4CSEIluvVbc6vyfxLx99FCcZT3D8fHBxk3Z+TF86+FyA5Jpy9aaQkZJ0hjO3sQVGh3kV2tUdxs8YncwmxqXINbfVjqB5R3NpKY4fYes404tCximpYdRGE82hWBUHXZlW5tpsJdhnNqiAcsFkVWmtsBPcomxI1wMmrli6ZVY3JTvtpqAHm51rA5vzCoYuA/4VrgO+D4zFJKQpW5SJgUfs7cO0vUC0Gvsau/YVqs+8hZkHNJv4OTHXq6LtK2+7V/FURgp9phAwYLEkz9kgNqNKG9TEpCi4lYpHULDs+hzSQRJRsuCiZ2nvE8BymakCV1YHpikAZkJtjZUMEyprng7KQLjIguArGmiJlpRyaimTQOOYXB1NHmg5zWxxMkuUsmSxNLrOmTy5TO8fI1JSlM4kzwLr9b14bI9Msur7JHLmxex5OHmI8AWnRrlahZrTMgGFAjVtvvcEx3HV0TK7wZoJhzaTlaT300TmZUm3bD9wh3KGjiYiTtcbJmN7F2L3yoNYcx5j9GepVAb2bylyqAbs0EBgO8hDCiB06wQ4YUm3KFrtwxpQlY9TjXG2k49zSFKOyldA+yGUMgzSrr5yJiqbMzBsmEDaBbjQFPVJhVsnhAVV8AXVyfM2smkart2/R6p7HgvDV1pJBhLTnklU+kxi2Rh+t26VGcCKQPXYgW+PZlIFfIFsbeQYGx3DH4gJx9cnK14cwdMgIxNU7wHBXYkuYSkEifJmt3p7TNordvtggnFXH77XZalCeAX715rYityU4VUYrXsxQEgkorQ3UoAxp+2vcBBSd9gbz8k9RnDXP1BP8Umdzi1/vNJ129Gq908RIRb52er55l1UeqtP+nshZWgyEZtE2LQtqvemDqTz/iZn0NPZp1FyosgkDe7fFZkDeDYfoozbaiCO2oaONUJV0ULNcFIb0YBdM8qv+MDqcsIkUhbYUBbYVyk5R4GeEGqzecENUWmii0mLATkdw8k7hzHF8CYMPgAmTTfCCUYLMwgQnmyDcOeEaXV5lqc6JQ4kuJR4lQZN+ol4nVid+EailF2mLRMJUSl/Nv620NPlwFHi4J3X6tpO926E7rLxPlqBzbNawXfcF0Fn74nPgVcyM6tbIL2OaVi8elNuonD4boUHyb4XUU1GzwdzkAB659Fp2o4liINzCN9mQgV9elbz2XKITqaTtaepeerrehl0HUlE3rbNu+i7pmtFk5dlHLDX9TYT/t8ZEdMIowPe0Ob/ibwfyt0+fr7FxuzO7YCZC5yVY8afWFyQ1ryNXhD759uhVn7XYmVxH+3tCrWC/r6zwY9Pr67X0it8mvZiz79qIhiw3N8amF/L9A+8poRvyeZOoN3KA3eh0jvsVQyUXT1OotdsV1weSMeLY2wNGUmjvHByKQwZl9it+2Y3SK1mfJfKhKBWIaZ6vnWzd4vUY/1L2Ck4su0eh7J63ZV1isqWWePaWc4S+Ckd2EVrXpzCIg23glVeQyfl55TO2vz2QnVC86RA6+8p7DnF8ishNfUB/H934cN5IW3Sf4MNzEOwizLoPB/vJ8QPcYsELNth1sKOYvLALtggGHwgo8ruSjujhIfMer9v6XmCn9CemWjN36lwtLLs1YFl+YWkjXCqwRI9oD+sBPBTUeWtOwCUx1cSA4FYHYRQHoFnkyFeU7/bxD1K7qtS5PO9uR7ql/JpuYPnZRvI5zuXzs528+f9wzEA+uj7Wc7jZyk/BKXYD3/Z+nth2yJ7M9d3YtYkWTmwafJe/7Uv6ZReQF/0APx6WWPgpn2zXsxM1TvTPyfF3qWF0jpFWzym0xUdVhbrLyIfVHpJkx1PeeGbvhvWweBuFGQSeBU2TohxMgs05rlMQ5wJho6j4ODKPnJCQFRHJj+wtRkhFvD8fXGIznOzXqExLp6j8CZ34HPr41WPewadES/uYdiQlRD2m1oKc3fE+dJy/yE26/onccwbztK7ovDkmiE7t3mSdSiYsuYJPe6ZmQ9lYlck5lEseLQN0A71LJVTNm2GDWPs9SsCbKpr7OiG2xBo/nX0Xj9UqPvqacT/ZKE23Qeh4IhipMvqns+85UflJolPagzNda5cxMCqBxvW35zBKdknyzWi5n6dm0j9SHBFzF6E0JjefWrcl6UKosSE/oYfG9HGPR2eHRZb3OvVDfP1h1UP2ZKXtQzbEf0V1tJdSIdKnK6uUqr+cb6dY6JYrGTJ73ZJ5ovLX3rolzLL6ipVWjRpnaoY0aZn4kpahNsAV+iQeXp/Ey9cnqSVzXzzNfY6wKjWziml8aAermE7kwEweJb35uLLxS/v+wq4vybkCClFl27NRUdqVDvlffZfnsjejGkZ0erZRVoPZmrlsWAESp3AQdpKgYLHy88FBzxatSlGuwPdeqwvSn5wnm6gKfPnZjUvxPjl2wmNBZneXqNYKQdBbM57N9CGJyE6flkiJ0vPaNeBUIbgUMd7jgZKu92QXRq1Pvs26rVYMxdJumpQa2U5OjYF8P/7kSGSnZz3/f/n6688rYo9F7s7JEJzbU/Y+TlCDH9Rzknw3m4iY54S2yCI72rNA+B8J+xAXVyz7JrF363bpT9kTVmzEn3OwFDMRknD+9rsfPHu4nGiXKKbjJeFBIIHwhAhXMmuRIYXDx+V7zIR4QFSCvdshvR05qQkcJILUyWQP+RUp+Ox8IT1+KUkoQoKZbs3kDIlszeLGXYw6+3Ty3C1mzOUbRhcD6iRp5z7VL+E8VOqTjPcxLl3OD209H/p7EKdBUdtLArPOKbGbn/LzszqTCnie4wBvqS362XnZOqd4VT15y47bmoukLj1t7fioMdkU87glEbGcSKiuuiYSNmeqMTPa2Mn1vLLKao0N1oqSpZyXcsoMnEsK6bwyRan3ix8ws6zDeFpM59ONlCFC3Xe2WQHCKs2Wu1BGJNdJxkjez1uZjJKwySys7XcqBZn5mrliLLqry19Te7D1xGruGZ1QW1Wr7S7UFzc2XG9Bb/f0zuooBkZGt5bV+I/UA7lD18Qsn9s92o9l2fURE/EBWyHYk4fvs5OvLvmzbPlnIw9/i595775gXt6Rr3yXXZWzK+jn6gE2OSYnL0VScpiNr6Tn5um5NlQMDeq48u6DoqiaZShoW28dY2/Lpry2trqONrmzWZuqAtc7R9GhvLfs/WYrnbC1wAMVJb6bnCRJoNz9CZ93z/+6/3vw26/3zubb+y9rQDH9N9+Ns90vqldn02VGMQEtInjOLmXCha75OCbKoWTri3rVoYoNL+7YuVarMm96EU2lVxMNV8uVf2u/D5AxbSb9PiDUJMWw8j+14iaF6tjRuf2HoUuaVpTug+q6Vr3CZbhOIEz4Kh1soyG7SXZG4thNoy9JpA7o1WeFXlWvenaKWe/y1xmutdGAilXvbMkboB36RU/eaakCa0lWjMvQHrTR0iVrpgNwtVkBV6sD17q6TdiIwL2s0d50D4K96yMr+K+aIZ0le4jW2D9G3X6xuSYcdJwr4SVuuI77RCB5SCS39NGaBaoVY6DxW4tWI3nmTUNQRiiSBW4/liJhzljgt+XYLkfz9N/beoYoomfItVgxqta8Dg0pk9YjtAxhBweaO4aIIb7zgImhyNPDhNV1fAiYQAGToWBiwclhAlmB2CFgogqYDAQT05pemsDmxt2in9k8YGJZYEyYsL3GAfKohA07tL9Ti0ircOKmdyA7SxGdLnt0uqS6FyqqQjPSkizGdOshktyi30++/GH3bxj8uXn4/X6t3H3897pHiptgZCMjmZlIHKeqfjO3748f15FhRd/WUbz5dq+9rFVefmW945aQ3n3gomrI+qvhRafhwkuAs6HSo1OxvcWlCjdGIeVZ7WAWSxhNh1ks4ZZtnCfVt6f7j0G73kUBqqYhmhZHItXBkwpgSEgWfbkVCCjZcy0O8y1w6ZVJPTKkWWai0EjXa6SbofBaZflkDoTZYQ7erIWdfEHYAcg4jBpV2AGzQ3rxcoRdAZf5CjtgsfS3EHYDC7s+UJiJsOuXiJpyo18Waj/5N5OcTVW3aCv82nxjvJgKLU3X8LG/JVu1YsKRB2UyjHnSNe1d0ojA9dz4VZSSTeYAQquqv2sw1FnhHP3CTuCQk0jhx/W33pkkMeF6RFKC3ISgN1VbNqBu6W1HswrJWDjhNvMQLDjJL20ls8rbsxCpKLLmeMk805BMuVSZVhF5Bmvolq4yQ9j8MrFpr6VV1gltObW2NMyO2tLgNvmVtrZIkybS8VTMex1aTVqdATQbNcnIuTz7p/PGc6MDaYJX6sD5xmv1pwQKawbwqEBRmOl0wltHFDc0STYL88Gq6ICCTb0d92TdzHFXZK2yMGLIuJ67YtDVYKIBzDzsEEOVLKPxiFFj2SENbT34nXkZdNr2KXQwMbCaEU1hxppAX+xjHm1hIDcFZLLCxUvx6LtW2wnPflC5aEIJmI3+2di1cZc7+gi1ujC1OnaXtQb40GJR9FkbWaUWe3iundYaokO0SfYFcfg+bRMvRM9MRU/R1acDfLjl0kCZPmMpzQl4ST4vxA43sVPs32WJHZ22eIrZAvZ2m05XQde8wMbO4cHGQ4IEkGYEJFZAsskl5CaAltwQLHcJiwmNq/KEFuEScnIJFQmoahGtrUZVAdslNCSNUXvGzSeEgA61PiQS8l0iHh8S2SgstOksNM2SZFltCPtDwIi5AlmTLMasDG7OIWS0yrioaYWK5aZiwRBR1yYE8bPV4IKjrpt08Fc24KuA/nMyHysbQnX2vQAJM6FzJwnDQsgyJkcOw0KVHipUjYVEJyRQ6FwZoWqnDoYY1gyON6FKm2z5lNNi7KLIuRpD16rdh+nM6IQTqs2NzXrp2mwmYaciqit0LbUGGTH62oD0SOhVbnKxqkiByXJeLVWS9TE1qU7D+H0SFhbu6kx1KL7FzgKQ34GC3nqgIA4y+WpOfaEnCnR9hDC+FgOhOZwl5HNblhjoELMO5hfVmMWsA2b0rsblAWY99SNxa95+NroImpJSDasrhiUZtMK5lN3fY1RSrXmdmWXc95+UZF5eaLhM/bX67+fP+tMf5sdf/vm3P//xcPAedmvW1JbZM32cYg1qiJV67TgszWhbiXs9RodedYLPGXfqMrb7vD5TUkHLYrybZoDmJtWzN07y4NDeDbHvj1ibRgFkFx++7MOAlLqHGxdZsbjj0iWLZaisiwulJ8Je4mQvyZKsNZXZM1v+YnVvMersNa1ZHnS1l5iakxWAHV6iNswelrTVkLNYW0dV5s3YZiKztcxzzINS8FoDDLStxNkC69DEb3sOn3JZ0QYq1txp5CBb2qrTxN8qzlpQNuww1YYYgbpQjMJay3mo1KtwO5f31iwUKHe0HxFQ7NfS29KIUfMtW9UvAlYawG20bNXLH0A/JPcw6JYZZ+z7D2HMQsW61mmh4FtfibNgHGk6+g/C5vpJy7UzxumVODssMGPrEh0WP4jdvUtcg2CP/kmPoMQ5dr9d1cczUCXTKFJpzVp+mKKzXANZyqKbQ4dS2ZKrR5d0MRAkedXSJVPtwEv2QBB+rGQJJ8HKvqxkpbg3zXZR+vMS/RoGWDAXignR+/Ap2Dn4Hf8P ================================================ FILE: docs/trade_protocol/trade-protocol.md ================================================ ## Overview Haveno is a decentralized network where people meet to exchange XMR for fiat or other cryptocurrencies. There are no central entities involved and trades happen directly between traders, which are both required to deposit and lock some XMR until the trade is completed. In case of disagreement between traders, both of them can request the involvement of an arbitrator, which will resolve the dispute. This document provides a simplified overview of Haveno's trade protocol, but a PDF with the technical details is available: [trade-protocol.pdf](trade-protocol.pdf). ## Protocol ### Roles - **Maker** - person making offer - **Taker** - person taking offer - **Arbitrator** - entity resolving possible disputes ### Overview of a trade We assume the maker is selling XMR and the taker is buying them in exchange for fiat: 1. (optional) Maker **deposits funds** to Haveno wallet and waits for them to be unlocked (~20 minutes). 2. Maker **creates an offer** which reserves funds for the security deposit + amount to be sent to the taker. Also creates a penalty transaction which penalizes the maker if they break protocol. 3. Taker **accepts offer** which reserves funds for the security deposit + amount to be sent to the maker. As for the maker in the step above, a penalty transaction is created. 4. A **2/3 multisignature wallet is created** between the maker, taker and arbitrator. The arbitrator holds the third key, so that they can be summoned in case of disputes. If there is no dispute, the traders will complete the transaction without involving an arbitrator. 5. Both users have their deposit locked in multisig. Now they wait until the funds are spendable (~20 minutes). In the meantime, the XMR buyer can send payment outside Haveno (e.g. send bank transfer to the other trader). 6. When the taker has received the agreed amount with the agreed payment method, they **signal to the maker** that they have received the payment. 7. The maker checks they have received the agreed amount and if everything is ok, they **confirm the completed payment**. 8. When the XMR seller confirms they received the payment outside Haveno, the agreed amount in XMR is sent to the buyer, while the security deposits of both traders are returned to them. **Trade completed**. This protocol ensures trades on Haveno are non-custodial (Haveno never has access to your funds), peer-to-peer (there is no central entity, people trade among themselves) and safe (thanks to the security deposit and opt-in arbitration). ================================================ FILE: docs/user-guide.md ================================================ # User Guide This document is a guide for Haveno users. ## Running a Local Monero Node For the best experience using Haveno, it is highly recommended to run your own local Monero node to improve security and responsiveness. By default, Haveno will automatically connect to a local node if it is detected. Additionally, Haveno will automatically start and connect to your local Monero node if it was last used and is currently offline. Otherwise, Haveno will connect to a pre-configured remote node, unless manually configured otherwise. ## UI Scaling For High DPI Displays If the UI is too small on your display, you can force UI scaling to a value of your choice using one of the following approaches. The examples below scale the UI to 200%, you can replace the '2' with a value of your choice, e.g. '1.5' for 150%. ### Edit The Application Shortcut (KDE Plasma) 1) Open the properties of your shortcut to haveno 2) Click on Program 3) Add `JAVA_TOOL_OPTIONS=-Dglass.gtk.uiScale=2` to the environment variables ### Launching From The Command Line Prepend `JAVA_TOOL_OPTIONS=-Dglass.gtk.uiScale=2` to the command you use to launch haveno (e.g. `JAVA_TOOL_OPTIONS=-Dglass.gtk.uiScale=2 haveno-desktop`). ================================================ FILE: gpg_keys/reto_public.asc ================================================ -----BEGIN PGP PUBLIC KEY BLOCK----- mDMEZmhlIhYJKwYBBAHaRw8BAQdAlZx+3Fdi66/YBIHyCbOovxh7luW9r4G13UxX FOSQZSu0BHJldG+ImQQTFgoAQRYhBNqiTYeLjTbJASCol8oC2sEtri0PBQJmaGUi AhsDBQkFo1V+BQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJEMoC2sEtri0P n3gA/0f8+oU+dO9xsCdRynkBCdM2QWfQ3LkyhRf11mhIxGAAAP9cA5/eetIwwhTO AaIC6q4KBATTAN1cEhkeIMKSLDURDrg4BGZoZSISCisGAQQBl1UBBQEBB0A4FBiE cTUkbx33xmIVPv+WwbWLZeL3PBIUUhzirqDqZQMBCAeIfgQYFgoAJhYhBNqiTYeL jTbJASCol8oC2sEtri0PBQJmaGUiAhsMBQkFo1V+AAoJEMoC2sEtri0PWk4A/3UU X4JoX3+FZonPJfWc+HzCnuTEcDZKJzlVrtPFeMNnAP9HYF32KiRtjTgKORyCzBeY lFen4bY4fUNtKz5RjWnVAg== =QJTO -----END PGP PUBLIC KEY BLOCK----- ================================================ FILE: gpg_keys/woodser.asc ================================================ -----BEGIN PGP PUBLIC KEY BLOCK----- mQINBFpYwMsBEACpSn/AxDOGCELE9lmYPfvBzgw2+1xS3TX7kYdlvVDQf+8eCgGz 8ZpBY3lXdga/yMZZBoDknGzjlyaiG/vi7NljMQmWd5eGyhyfkWpeDXYLbiB5HlKe nHvJO2sHc+2DxULQ/f7VytvpM+eQdkQnZnDZbvqeeOaj66IGnmtRse0zMhkx0OsB 0YAx+zbwZstldiUqUyt9IBckiYLc/jtQ88rJ9OjsIc/gFM0849nSx1bGMGvYi5eE rHOvo67awqX7cNoZM9X1njHbYvUKL5+fAoT3TBjLyL7eUYNKFSwyGCczKL04pcqk eoCtuDoj8O7f6bkhBv8IW5WW03TZWlCYVrwiAlfdcnuKCWB9BcKElAMhwbhT5uRS ofYh3J/RJ4CCmjvyNp9NBH9PNdXt1ybJ4724rrTvTethaLhJgYBP0cBsZQiOObis QSdBguyy0IOV7F1f5Rnf5klea6HciNhxdeHSDGBUwmzEqiohV2oe1g8qogMwsOkL EOYJ3+qyiwF8bcCgklKj4/c8bgN0KuZ1QGnrRQfDsXkE2VMJghK+yorNcrLipM5x JXZ9x/ku+GCLvELoxI2oHknHUK7ySsnY7Wn4ZcRciJbA/CVfIgphJ49J5mMeDNmu kpp4CVBrttqDzOhgkcaAuBGY227VwOn/DjxpAXJ8ZHeXAYkbwXVU70nFBwARAQAB tCp3b29kc2VyIDx3b29kc2VyQHVzZXJzLm5vcmVwbHkuZ2l0aHViLmNvbT6JAk4E EwEKADgWIQRS/XwBh3ypaMlxGNBVoQ3Uit7l7wUCWljAywIbAwULCQgHAwUVCgkI CwUWAgMBAAIeAQIXgAAKCRBVoQ3Uit7l7+d4D/98eNSfd97rTNNaNq4CZqo3KJrC qPVrUGbbuTK7dNAQK/iMTthatiFUj9MSUWBpiNWaKHrYAJ+20r+XA9SezHV1Llnj mX/0JfIuJ6NeSYSWPKw2kLorPaIBrDcJw2bsRlSOYhodcrK63d7XqNTGLvK0Ja6o q4Vtdo6/4AAZx1ceGWzrBjP0dAQ/i/1rnowtIBU/Qi/1K6FDlVKcsgkbJQsCEnCH +ILy2l5Ol7BoRO7JaqUBsYLntMttBrauETG3vs8rpLcsPaShMSHT50PSgBtS1e41 0KYQQyl3YjqZz0fkM4aKNlqzqsYUI+gyC+s7LyJwACMDYCYk7O8lM39hkRFDm/AU Ke4EDHdl2Sk7HD3/GhJZhTcaxFcKGBK+AF7uiAyz98Ny0tJRZ1ziJSpSdMTvm4j9 zA6zmydMyNeUOYKjqnimQUuHBhxuUl5FlokoWaXnUavJvOjVfsoTcNxCcvMHnhFN R5TmNLOLPXrXwdU0V86nDmHstXl+E02SWFTgZ8Vxg318ZLpIw3rb65zUALTfZwpl 32XhIUhBBnN0zRl3scGW+oj6ks8WgErQ7o6dYdTu17AIggNdpHXO3XXVnW0mS6tz IeCvDkEQxegoL/B83B+9LI//U9sc5iSCQOEZQ1YLUdEkNSFgr7HU9GPllop52HUB GffqGoz4F7MXl3g2ZrQzd29vZHNlciA8MTMwNjg4NTkrd29vZHNlckB1c2Vycy5u b3JlcGx5LmdpdGh1Yi5jb20+iQJRBBMBCAA7FiEEUv18AYd8qWjJcRjQVaEN1Ire 5e8FAmfBv40CGwMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQVaEN1Ire 5e8bDBAAgET7qqMAhymtofo0NSemxLck1xEcZfco3inX4HVAV3J8HBnZPP2q19IP F+Lj2GTRJZstRWNLwD2+7N3LgPOTGt0X+f6BsHLP0NMR87I7NpBoU+QEJv6fY1Ld kZbgqfX0MPgHWHVN2qOsgZXQE4WKJECVpb8hJVNicfXb3Em+g5AtbI7ff4ycpRqz ajSTTnvcn6meoN/LgGHjnFmYkV8CXVfgpcvUQJNqNHsrk6/iFPiWly9zb7G/4Vh7 MqdjEZwEfGwgjA8Tzeh4Cks1fLM5KcZdMgRUmTSXZJxVdrq7ODwT9uRwCLJyncRx wA1VrZHqEtiv+k3U9ef7ZngVlRdwogam5WJzyCioNCxBBzs4Z3dm/ZWwR/80YSa1 DIGq//ybOaZqJ15wNAPzqdM1CwLg17w1sY//eKFFUQPZ7KmhG42/wWYG6ka9wgai x4iPzO73weQQU/kxa4hjnU07zw+NJUxHfsNmqgJW+fRKmi50h6uz5WxRDigjkdGR oe0HLipZ3cQjgLHaqR4Uw86yyWXQUYxZ+gmStUkrN3hgAX+JuXBxvKKlQQYUS3/j JwAepRhi3mkFyoJveGUyfYXvTgYddIiCXBpdRIZSlWOabSYfdxFq+CBuAi16IhII ulgsAXwKqUuX464zEFb+Ept5ESnApm8qDDXAzCBHlM6tJcOi3ey5Ag0EWljAywEQ AMQmYwEE9m898Kss9LwzM8G7T0bR6Nw2Pq9Z+gi8Vw17vLug1hr0V9zNme462yXu Hv3GA0g3zVY/RNmCFcg/KVG7/QFGIeVQaoUFOQvt2nkXjtY7NoktV5OiACetGqqf ybK50cjkH6QJxkGmZb6qJnW2682WgGjl73YGx8gUY9nh2bUn2JIZ3X7LaZBNvHra GYTWT9odHuQ1S5n54sIDJjLmaIiTcxABhnvZAMXZLyoEafRw64+phpB5m8Om2pvO w1a73jUz6euth+4C6SHFFCcc1ey7bWQyWpNfycQkFWz6MtBa5qd08V4ZkiZVdbxl V/EhbnGsa0kvAEYQkga3/oRsqWwxpvk4OfqGtYHVEsNuBTg5O7reizcBICeJ/bm7 P/BD+//a0XolS4ybFuOmbnktRIxU53mTNVcngKgSqm8I9tR/SsU4IaIAsaTx3uLa k69yfHhY1WKDWQ6BTt29NJI4V1jlonGwrWk+EmFLEWT+VrgcIELvFuABoEKQhb+p Cd+3LWH3knbg3xrDCtBCcacbrWg2QtveNFX1AN/CzkRW8Mz2jQBlfHu5lYqhBJgx OIOn09L8hqHQrADORCGz/0OLun7TXxOzH5pqdgi56h7H1S1Lxmx/BmzC6qEtW2o8 vLeBxFNDmezddiENYZ35yh6yyqXxil35eNY5Ky9oJ7slABEBAAGJAjYEGAEKACAW IQRS/XwBh3ypaMlxGNBVoQ3Uit7l7wUCWljAywIbDAAKCRBVoQ3Uit7l7398EACc l4rVvJfg9gGmrMyppuFV2JKn/ms61ZkS6Z6lDLyGsYSU2OCdh+W0+iQABFN/w6ev 4IWL6hm89ua/eD1JAzymf9tzLwTBWm/G4iP/6U/oycEBVyq1xCFobgTRb6ioS/Ds TItZKsrNxOOeqrrnqIUM2Wyss0wGKxAUF/P3zX/6mhrliM3K3VqgDtTsZzvywU2S IeCa59bKQYd51v1OpNyy0rF7D5Ab/RCB8UevNEfHLLU/XC2sVM6gYV1/oij6vDKl lw+YWSigQspVsm3X4RnOBRfjrM3blgz+J1WoTRg+6RV/YQjIuiubiEY/GzLVWoTe 2wYsGTKd50EQBgjubqaqMDGhib+wPc0NeJTyhBDn/t5EFI9l1MOI7kZlYlPdYaSE cuB3/+Fq8twIaiAn/ZjIJJv7SNXs/pqFkEYaKWGeFkzWyIUK0lLWhDkBwXCRYpyr keEl8cZqxR6Wd+yLd0mWecycmR6y8qU/a8tMa5uyVhfvsEFaH6r805lbSIsG2AA7 i/3w78qJaPOa0nqA0nWVRp72NrYqTIa9PqgQ6zot7Umhc1PqZndP4QZnjcUtXImJ 8QOhIvsNL+/Zm4CssJ4DE6vsEA8p16yB3jadaF6hrxVPW0OgYQwykueKTx5tGZRI PNPr3mpTV6VyFbY0jNj5UabE5ZqrN2HCGpqinWg6Gg== =4SFl -----END PGP PUBLIC KEY BLOCK----- ================================================ FILE: gradle/README.md ================================================ # How to upgrade the Gradle version Visit the [Gradle website](https://gradle.org/releases/) and decide the: - desired version - desired distribution type - what is the sha256 for the version and type chosen above Adjust the following command with tha arguments above and execute it twice: ./gradlew wrapper --gradle-version 8.2.1 \ --distribution-type all \ --gradle-distribution-sha256-sum 7c3ad722e9b0ce8205b91560fd6ce8296ac3eadf065672242fd73c06b8eeb6ee The first execution should automatically update: - `haveno/gradle/wrapper/gradle-wrapper.properties` The second execution should then update: - `haveno/gradle/wrapper/gradle-wrapper.jar` - `haveno/gradlew` - `haveno/gradlew.bat` The four updated files are ready to be committed. ================================================ FILE: gradle/verification-metadata.xml ================================================ false false ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ systemProp.org.gradle.internal.http.connectionTimeout=120000 systemProp.org.gradle.internal.http.socketTimeout=120000 ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in # double quotes to make sure that they get re-expanded; and # * put everything else in single quotes, so that it's not re-expanded. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ org.gradle.wrapper.GradleWrapperMain \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: inventory/src/main/resources/inv_btc_mainnet.seednodes ================================================ # nodeaddress.onion:port [(@owner,@backup)] wizseedscybbttk4bmb2lzvbuk2jtect37lcpva4l3twktmkzemwbead.onion:8000 (@wiz) wizseed3d376esppbmbjxk2fhk2jg5fpucddrzj2kxtbxbx4vrnwclad.onion:8000 (@wiz) wizseed7ab2gi3x267xahrp2pkndyrovczezzb46jk6quvguciuyqrid.onion:8000 (@wiz) sn3emzy56u3mxzsr4geysc52feoq5qt7ja56km6gygwnszkshunn2sid.onion:8000 (@emzy) sn4emzywye3dhjouv7jig677qepg7fnusjidw74fbwneieruhmi7fuyd.onion:8000 (@emzy) sn5emzyvxuildv34n6jewfp2zeota4aq63fsl5yyilnvksezr3htveqd.onion:8000 (@emzy) sn2havenoad7ncazupgbd3dcedqh5ptirgwofw63djwpdtftwhddo75oid.onion:8000 (@miker) sn3bsq3evqkpshdmc3sbdxafkhfnk7ctop44jsxbxyys5ridsaw5abyd.onion:8000 (@miker) sn4bsqpc7eb2ntvpsycxbzqt6fre72l4krp2fl5svphfh2eusrqtq3qd.onion:8000 (@miker) devinv3rhon24gqf5v6ondoqgyrbzyqihzyouzv7ptltsewhfmox2zqd.onion:8000 (@devinbileck) devinsn2teu33efff62bnvwbxmfgbfjlgqsu3ad4b4fudx3a725eqnyd.onion:8000 (@devinbileck) devinsn3xuzxhj6pmammrxpydhwwmwp75qkksedo5dn2tlmu7jggo7id.onion:8000 (@devinbileck) 5quyxpxheyvzmb2d.onion:8000 (@miker) rm7b56wbrcczpjvl.onion:8000 (@miker) s67qglwhkgkyvr74.onion:8000 (@emzy) fl3mmribyxgrv63c.onion:8000 (@devinbileck) ef5qnzx6znifo3df.onion:8000 (@chimp1984-lite-node) 3ksikse2xuxyqj7cioceewaqdjymovhacdtcvw4tzcjyjk5m67cytmyd.onion:8000 (@chimp1984-lite-node) ================================================ FILE: inventory/src/main/resources/logback.xml ================================================ %highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n) ================================================ FILE: monitor/README.md ================================================ # Haveno Network Monitor Node The Haveno monitor node collects a set of metrics which are of interest to developers and users alike. These metrics are then made available through reporters. The *Settled* release features these metrics: - Tor Startup Time: The time it takes to start Tor starting at a clean system, unpacking the shipped Tor binaries, firing up Tor until Tor is connected to the Tor network and ready to use. - Tor Roundtrip Time: Given a bootstrapped Tor, the roundtrip time of connecting to a hidden service is measured. - Tor Hidden Service Startup Time: Given a bootstrapped Tor, the time it takes to create and announce a freshly created hidden service. - P2P Round Trip Time: A metric hitchhiking the Ping/Pong messages of the Keep-Alive-Mechanism to determine the Round Trip Time when issuing a Ping to a seed node. - P2P Seed Node Message Snapshot: Get absolute number and constellation of messages a fresh Haveno client will get on startup. Also reports diffs between seed nodes on a per-message-type basis. - P2P Network Load: listens to the P2P network and its broadcast messages. Reports every X seconds. - P2P Market Statistics: a demonstration metric which extracts market information from broadcast messages. This demo implementation reports the number of open offers per market. The *Settled* release features these reporters: - A reporter that simply writes the findings to `System.err` - A reporter that reports the findings to a Graphite/Carbon instance using the [plaintext protocol](https://graphite.readthedocs.io/en/latest/feeding-carbon.html#the-plaintext-protocol) ## Configuration The *Haveno Network Monitor Node* is to be configured via a Java properties file. There is a default configuration file shipped with the monitor which reports to the one monitoring service currently up and running. If you want to tweak the configuration, you can pass the location of the file as command line parameter: ``` ./haveno-monitor /path/to/your/config.properties ``` A sample configuration file looks like follows: ``` ## System configuration # true overwrites the reporters picked by the developers (for debugging for example) (defaults to false) System.useConsoleReporter=true # 0 -> XMR_MAINNET, 1 -> XMR_LOCAL (default) System.baseCurrencyNetwork=0 ## Each Metric is configured via a set of properties. ## ## The minimal set of properties required to run a Metric is: ## ## YourMetricName.enabled=true|false ## YourMetricName.run.interval=10 [seconds] #Edit and uncomment the lines below to your liking #TorStartupTime Metric TorStartupTime.enabled=true TorStartupTime.run.interval=100 TorStartupTime.run.socksPort=90500 # so that there is no interference with a system Tor #TorRoundTripTime Metric TorRoundTripTime.enabled=true TorRoundTripTime.run.interval=100 TorRoundTripTime.run.sampleSize=5 TorRoundTripTime.run.hosts=http://expyuzz4wqqyqhjn.onion:80 # torproject.org hidden service #TorHiddenServiceStartupTime Metric TorHiddenServiceStartupTime.enabled=true TorHiddenServiceStartupTime.run.interval=100 TorHiddenServiceStartupTime.run.localPort=90501 # so that there is no interference with a system Tor TorHiddenServiceStartupTime.run.servicePort=90511 # so that there is no interference with a system Tor #P2PRoundTripTime Metric P2PRoundTripTime.enabled=true P2PRoundTripTime.run.interval=100 P2PRoundTripTime.run.sampleSize=5 P2PRoundTripTime.run.hosts=723ljisnynbtdohi.onion:8000, fl3mmribyxgrv63c.onion:8000 P2PRoundTripTime.run.torProxyPort=9060 #P2PNetworkLoad Metric P2PNetworkLoad.enabled=true P2PNetworkLoad.run.interval=100 P2PNetworkLoad.run.torProxyPort=9061 P2PNetworkLoad.run.historySize=500 #P2PNetworkMessageSnapshot Metric P2PSeedNodeSnapshot.enabled=true P2PSeedNodeSnapshot.run.interval=24 P2PSeedNodeSnapshot.run.hosts=3f3cu2yw7u457ztq.onion:8000, 723ljisnynbtdohi.onion:8000, fl3mmribyxgrv63c.onion:8000 P2PSeedNodeSnapshot.run.torProxyPort=9062 #P2PMarketStats Metric P2PMarketStats.enabled=true P2PMarketStats.run.interval=37 P2PMarketStats.run.hosts=ef5qnzx6znifo3df.onion:8000 P2PMarketStats.run.torProxyPort=9063 #PriceNodeStats Metric PriceNodeStats.enabled=true PriceNodeStats.run.interval=42 PriceNodeStats.run.hosts=http://5bmpx76qllutpcyp.onion, http://xc3nh4juf2hshy7e.onion, http://44mgyoe2b6oqiytt.onion, http://62nvujg5iou3vu3i.onion, http://ceaanhbvluug4we6.onion #MarketStats Metric MarketStats.enabled=true MarketStats.run.interval=191 ## Reporters are configured via a set of properties as well. ## ## In contrast to Metrics, Reporters do not have a minimal set of properties. #GraphiteReporter GraphiteReporter.serviceUrl=k6evlhg44acpchtc.onion:2003 ``` ## Run The distribution ships with a systemd .service file. Validate/change the executable/config paths within the shipped `haveno-monitor.service` file and copy/move the file to your systemd directory (something along `/usr/lib/systemd/system/`). Now you can control your *Monitor Node* via the usual systemd start/stop commands ``` systemctl start haveno-monitor.service systemctl stop haveno-monitor.service ``` and ``` systemctl enable haveno-monitor.service ``` You can reload the configuration without restarting the service by using ``` systemctl reload haveno-monitor.service ``` Follow the logs created by the service by inspecting ``` journalctl --unit haveno-monitor --follow ``` # Monitoring Service A typical monitoring service consists of a [Graphite](https://graphiteapp.org/) and a [Grafana](https://grafana.com/) instance. Both are available via Docker-containers. ## Setting up Graphite ### Install For a docker setup, use ``` docker run -d --name graphite --restart=always -p 2003:2003 -p 8080:8080 graphiteapp/graphite-statsd ``` - Port 2003 is used for the [plaintext protocol](https://graphite.readthedocs.io/en/latest/feeding-carbon.html#the-plaintext-protocol) mentioned above - Port 8080 offers an API for user interfaces. more information can be found [here](https://graphite.readthedocs.io/en/latest/install.html) ### Configuration For configuration, you must adapt the whisper database schema to suit your needs. First, stop your docker container by running ``` docker stop graphite ``` Find your config files within the `Source` directory stated in ``` docker inspect graphite | grep -C 2 graphite/conf\", ``` Edit `storage-schemas.conf` so that the frequency of your incoming data (configured in the monitor configs `interval`) is matched. For example, insert ``` [haveno] pattern = ^haveno.* retentions = 10s:1h,5m:31d,30m:2y,1h:5y ``` before the `[default...` blocks of the file. This basically says, that every incoming set of data reflects 5 minutes of the time series. Furthermore, every 30 minutes, the data is compressed and thus, takes less memory as it is kept for 2 years. Further, edit `storage-aggregation.conf` to configure how your data is compressed. For example, insert ``` [haveno] pattern=^haveno.* xFilesFactor = 0 aggregationMethod = average ``` before the `[default...` blocks of the file. With this configuration, whenever data is aggregated, the `average` data is made available given that at least `0%` of the data points (i.e. floor(30 / 5 * 40%) = 2 data points) exist. Otherwise, the aggregated data is dropped. Since we start the first hour with a frequency of 10s but only supply data every 4 to 6 minutes, our aggregated values would get dropped. *Please note, that I have not been able to get the whole thing to work without the 10s:1h part yet* Finally, update the database. For doing that, go to the storage directory of graphite, the `Source` directory stated in ``` docker inspect graphite | grep -C 2 graphite/conf\", ``` Once there, you have two options: - delete the whisper directory ``` rm -r whisper ``` - update the database by doing ``` find ./ -type f -name '*.wsp' -exec whisper-resize.py --nobackup {} 10s:1h 5m:31d 30m:2y 1h:5y \; ``` and finally, restart your graphite container: ``` docker start graphite ``` Other than that, there is no further configuration necessary. However, you might change your iptables/firewalls to not let anyone access your Graphite instance from the outside. ### Backup your data The metric data is kept in the `Source` directory stated in ``` docker inspect graphite | grep -C 2 graphite/conf\", ``` ready to be backed up regularly. ## Setting up Grafana ### Install For a docker setup, use ``` docker run -d --name=grafana -p 3000:3000 grafana/grafana ``` - Port 3000 offers the web interface more information can be found [here](https://grafana.com/grafana/download?platform=docker) ### Configuration - Once you have Grafana up and running, go to the *Data Source* configuration tab. - Once there click *Add data source* and select *Graphite*. - In the HTTP section enter the IP address of your graphite docker container and the port `8080` (as we have configured before). E.g. `http://172.170.1:8080` - Select `Server (default)` as an *Access* method and hit *Save & Test*. You should be all set. You can now proceed to add Dashboards, Panels and finally display the prettiest Graphs you can think of. A working connection to Graphite should let you add your data series in a *Graph*s *Metrics* tab in a pretty intuitive way. - Optional: hide your Grafana instance behind a reverse proxy like nginx and add some TLS. - Optional: make your Grafana instance accessible via a Tor hidden service. ### Backup your data Grafana stores every dashboard as a JSON model. This model can be accessed (copied/restored) within the dashboard's settings and its *JSON Model* tab. Do with the data whatever you want. ================================================ FILE: monitor/collectd.conf ================================================ Hostname "__ONION_ADDRESS__" Interval 30 LoadPlugin syslog LogLevel info LoadPlugin cpu LoadPlugin df LoadPlugin disk LoadPlugin fhcount LoadPlugin interface LoadPlugin java LoadPlugin load LoadPlugin memory LoadPlugin processes LoadPlugin swap LoadPlugin write_graphite ReportByCpu true ValuesPercentage true MountPoint "/" Disk "/[hs]da/" ValuesAbsolute false ValuesPercentage true Interface "eth0" JVMArg "-verbose:jni" JVMArg "-Djava.class.path=/usr/share/collectd/java/collectd-api.jar:/usr/share/collectd/java/generic-jmx.jar" LoadPlugin "org.collectd.java.GenericJMX" # Generic heap/nonheap memory usage. ObjectName "java.lang:type=Memory" #InstanceFrom "" InstancePrefix "memory" # Creates four values: committed, init, max, used Type "memory" #InstancePrefix "" #InstanceFrom "" Table true Attribute "HeapMemoryUsage" InstancePrefix "heap-" # Creates four values: committed, init, max, used Type "memory" #InstancePrefix "" #InstanceFrom "" Table true Attribute "NonHeapMemoryUsage" InstancePrefix "nonheap-" # Memory usage by memory pool. ObjectName "java.lang:type=MemoryPool,*" InstancePrefix "memory_pool-" InstanceFrom "name" Type "memory" #InstancePrefix "" #InstanceFrom "" Table true Attribute "Usage" ServiceURL "service:jmx:rmi:///jndi/rmi://localhost:6969/jmxrmi" Collect "memory_pool" Collect "memory" # See /usr/share/doc/collectd/examples/GenericJMX.conf # for an example config. # # ReportRelative true # # # ValuesAbsolute true # ValuesPercentage false # # # Process "name" # ProcessMatch "foobar" "/usr/bin/perl foobar\\.pl.*" # # # ReportByDevice false # ReportBytes true # Host "127.0.0.1" Port "2003" Protocol "tcp" ReconnectInterval 0 LogSendErrors false Prefix "servers." StoreRates true AlwaysAppendDS false EscapeCharacter "_" SeparateInstances false PreserveSeparator false DropDuplicateFields false ================================================ FILE: monitor/haveno-monitor.service ================================================ [Unit] Description=Haveno network monitor After=network.target [Service] WorkingDirectory=~ Environment="JAVA_OPTS='-Xmx500M'" ExecStart=/home/haveno/haveno/haveno-monitor /home/haveno/monitor.properties ExecReload=/bin/kill -USR1 $MAINPID Restart=on-failure User=haveno Group=haveno [Install] WantedBy=multi-user.target ================================================ FILE: monitor/install_collectd_debian.sh ================================================ #!/bin/bash set -e echo "[*] Haveno Server Monitoring installation script" ##### change paths if necessary for your system HAVENO_REPO_URL=https://raw.githubusercontent.com/bisq-network/bisq HAVENO_REPO_TAG=master ROOT_USER=root ROOT_GROUP=root ROOT_HOME=~root ROOT_PKG=(nginx collectd openssl) SYSTEMD_ENV_HOME=/etc/default ##### echo "[*] Gathering information" read -p "Please provide the onion address of your service (eg. 3f3cu2yw7u457ztq): " onionaddress echo "[*] Updating apt repo sources" sudo -H -i -u "${ROOT_USER}" DEBIAN_FRONTEND=noninteractive apt-get update -q echo "[*] Upgrading OS packages" sudo -H -i -u "${ROOT_USER}" DEBIAN_FRONTEND=noninteractive apt-get upgrade -qq -y echo "[*] Installing base packages" sudo -H -i -u "${ROOT_USER}" DEBIAN_FRONTEND=noninteractive apt-get install -qq -y ${ROOT_PKG[@]} echo "[*] Preparing Haveno init script for monitoring" # remove stuff it it is there already for file in "${SYSTEMD_ENV_HOME}/haveno.env" "${SYSTEMD_ENV_HOME}/haveno-pricenode.env" do if [ -f "$file" ];then sudo -H -i -u "${ROOT_USER}" sed -i -e 's/-Dcom.sun.management.jmxremote //g' -e 's/-Dcom.sun.management.jmxremote.local.only=true//g' -e 's/ -Dcom.sun.management.jmxremote.host=127.0.0.1//g' -e 's/ -Dcom.sun.management.jmxremote.port=6969//g' -e 's/ -Dcom.sun.management.jmxremote.rmi.port=6969//g' -e 's/ -Dcom.sun.management.jmxremote.ssl=false//g' -e 's/ -Dcom.sun.management.jmxremote.authenticate=false//g' "${file}" sudo -H -i -u "${ROOT_USER}" sed -i -e '/JAVA_OPTS/ s/"$/ -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.local.only=true -Dcom.sun.management.jmxremote.port=6969 -Dcom.sun.management.jmxremote.rmi.port=6969 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false"/' "${file}" fi done echo "[*] Seeding entropy from /dev/urandom" sudo -H -i -u "${ROOT_USER}" /bin/sh -c "head -1500 /dev/urandom > ${ROOT_HOME}/.rnd" echo "[*] Installing Nginx config" sudo -H -i -u "${ROOT_USER}" openssl req -x509 -nodes -newkey rsa:2048 -days 3000 -keyout /etc/nginx/cert.key -out /etc/nginx/cert.crt -subj="/O=Haveno/OU=Haveno Infrastructure/CN=$onionaddress" curl -s "${HAVENO_REPO_URL}/${HAVENO_REPO_TAG}/monitor/nginx.conf" > /tmp/nginx.conf sudo -H -i -u "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 /tmp/nginx.conf /etc/nginx/nginx.conf echo "[*] Installing collectd config" curl -s "${HAVENO_REPO_URL}/${HAVENO_REPO_TAG}/monitor/collectd.conf" > /tmp/collectd.conf sudo -H -i -u "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 /tmp/collectd.conf /etc/collectd/collectd.conf sudo -H -i -u "${ROOT_USER}" sed -i -e "s/__ONION_ADDRESS__/$onionaddress/" /etc/collectd/collectd.conf echo "[*] Updating systemd daemon configuration" sudo -H -i -u "${ROOT_USER}" systemctl daemon-reload sudo -H -i -u "${ROOT_USER}" systemctl enable nginx.service sudo -H -i -u "${ROOT_USER}" systemctl enable collectd.service echo "[*] Restarting services" set +e service haveno status >/dev/null 2>&1 [ $? != 4 ] && sudo -H -i -u "${ROOT_USER}" systemctl restart haveno.service service haveno-pricenode status >/dev/null 2>&1 [ $? != 4 ] && sudo -H -i -u "${ROOT_USER}" systemctl restart haveno-pricenode.service sudo -H -i -u "${ROOT_USER}" systemctl restart nginx.service sudo -H -i -u "${ROOT_USER}" systemctl restart collectd.service echo '[*] Done!' echo ' ' echo '[*] Report this certificate to the monitoring team!' echo '----------------------------------------------------------------' echo "Server: $onionaddress" echo ' ' cat /etc/nginx/cert.crt echo '----------------------------------------------------------------' echo ' ' ================================================ FILE: monitor/nginx.conf ================================================ load_module /usr/lib/nginx/modules/ngx_stream_module.so; worker_processes 1; events { worker_connections 1024; } stream { log_format basic '$remote_addr [$time_local] ' '$protocol Status $status Sent $bytes_sent Received $bytes_received ' 'Time $session_time'; error_log syslog:server=unix:/dev/log; access_log syslog:server=unix:/dev/log basic; server { listen 127.0.0.1:2003; proxy_pass monitor.haveno.network:2002; proxy_ssl on; proxy_ssl_certificate /etc/nginx/cert.crt; proxy_ssl_certificate_key /etc/nginx/cert.key; proxy_ssl_session_reuse on; } } ================================================ FILE: monitor/src/main/resources/logback.xml ================================================ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n ================================================ FILE: monitor/src/main/resources/metrics.properties ================================================ ## System configuration # true overwrites the reporters picked by the developers (for debugging for example) (defaults to false) System.useConsoleReporter=true # 0 -> XMR_MAINNET, 1 -> XMR_LOCAL (default) System.baseCurrencyNetwork=0 ## Each Metric is configured via a set of properties. ## ## The minimal set of properties required to run a Metric is: ## ## YourMetricName.enabled=true|false ## YourMetricName.run.interval=10 [seconds] #Edit and uncomment the lines below to your liking #TorStartupTime Metric TorStartupTime.enabled=false TorStartupTime.run.interval=100 TorStartupTime.run.socksPort=90500 TorRoundTripTime.enabled=false TorRoundTripTime.run.interval=100 TorRoundTripTime.run.sampleSize=3 # torproject.org hidden service TorRoundTripTime.run.hosts=http://2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion:80 #TorHiddenServiceStartupTime Metric TorHiddenServiceStartupTime.enabled=false TorHiddenServiceStartupTime.run.interval=100 TorHiddenServiceStartupTime.run.localPort=90501 TorHiddenServiceStartupTime.run.servicePort=90511 #P2PRoundTripTime Metric P2PRoundTripTime.enabled=false P2PRoundTripTime.run.interval=100 P2PRoundTripTime.run.sampleSize=5 P2PRoundTripTime.run.hosts=723ljisnynbtdohi.onion:8000, fl3mmribyxgrv63c.onion:8000 P2PRoundTripTime.run.torProxyPort=9060 #P2PNetworkLoad Metric P2PNetworkLoad.enabled=false P2PNetworkLoad.run.interval=100 P2PNetworkLoad.run.torProxyPort=9061 P2PNetworkLoad.run.historySize=200 #P2PSeedNodeSnapshotBase Metric P2PSeedNodeSnapshot.enabled=true P2PSeedNodeSnapshot.run.dbDir=haveno/p2p/build/resources/main/ P2PSeedNodeSnapshot.run.interval=24 P2PSeedNodeSnapshot.run.hosts=3f3cu2yw7u457ztq.onion:8000, 723ljisnynbtdohi.onion:8000, fl3mmribyxgrv63c.onion:8000 P2PSeedNodeSnapshot.run.torProxyPort=9062 #P2PMarketStats Metric P2PMarketStats.enabled=false P2PMarketStats.run.interval=37 P2PMarketStats.run.dbDir=haveno/p2p/build/resources/main/ P2PMarketStats.run.hosts=ef5qnzx6znifo3df.onion:8000 P2PMarketStats.run.torProxyPort=9063 #PriceNodeStats Metric PriceNodeStats.enabled=false PriceNodeStats.run.interval=42 PriceNodeStats.run.hosts=http://xc3nh4juf2hshy7e.onion, http://44mgyoe2b6oqiytt.onion, http://62nvujg5iou3vu3i.onion, http://ceaanhbvluug4we6.onion, http://gztmprecgqjq64zh.onion/ #MarketStats Metric MarketStats.enabled=false MarketStats.run.interval=191 #Another Metric Another.run.interval=5 ## Reporters are configured via a set of properties as well. ## ## In contrast to Metrics, Reporters do not have a minimal set of properties. #GraphiteReporter GraphiteReporter.serviceUrl=k6evlhg44acpchtc.onion:2003 ================================================ FILE: monitor/uninstall_collectd_debian.sh ================================================ #!/bin/sh echo "[*] Stopping Haveno Server monitoring utensils" echo ' ' echo 'This script will not remove any configuration or binaries from the system. It just stops the services.' sleep 10 sudo systemctl stop nginx sudo systemctl stop collectd sudo systemctl disable nginx sudo systemctl disable collectd echo "[*] Done!" ================================================ FILE: p2p/src/main/java/haveno/network/DnsLookupException.java ================================================ /* * 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 . */ package haveno.network; public class DnsLookupException extends Exception { public DnsLookupException(String message) { super(message); } public DnsLookupException(Exception e) { super(e); } } ================================================ FILE: p2p/src/main/java/haveno/network/DnsLookupTor.java ================================================ /* * 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 . */ package haveno.network; import com.google.common.base.Charsets; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; import java.util.HashMap; import java.util.Map; /** * Performs DNS lookup over Socks5 proxy that implements the RESOLVE extension. * At this time, Tor is only known Socks5 proxy that supports it. *

    * Adapted from https://github.com/btcsuite/btcd/blob/master/connmgr/tor.go */ public class DnsLookupTor { private static final Logger log = LoggerFactory.getLogger(DnsLookupTor.class); private static final Map torStatusErrors = DnsLookupTor.createMap(); private static Map createMap() { HashMap map = new HashMap<>(); map.put(b('\u0000'), "tor succeeded"); map.put(b('\u0001'), "tor general error"); map.put(b('\u0002'), "tor not allowed"); map.put(b('\u0003'), "tor network is unreachable"); map.put(b('\u0004'), "tor host is unreachable"); map.put(b('\u0005'), "tor connection refused"); map.put(b('\u0006'), "tor TTL expired"); map.put(b('\u0007'), "tor command not supported"); map.put(b('\u0008'), "tor address type not supported"); return map; } /** * Performs DNS lookup and returns a single InetAddress */ public static InetAddress lookup(Socks5Proxy proxy, String host) throws DnsLookupException { try { // note: This is creating a new connection to our proxy, without any authentication. // This works fine when connecting to haveno's internal Tor proxy, but // would fail if user has configured an external proxy that requires auth. // It would be much better to use the already connected proxy socket, but when I // tried that I get weird errors and the lookup fails. // // So this is an area for future improvement. Socket proxySocket = new Socket(proxy.getInetAddress(), proxy.getPort()); proxySocket.getOutputStream().write(new byte[]{b('\u0005'), b('\u0001'), b('\u0000')}); byte[] buf = new byte[2]; //noinspection ResultOfMethodCallIgnored proxySocket.getInputStream().read(buf); if (buf[0] != b('\u0005')) { throw new DnsLookupException("Invalid Proxy Response"); } if (buf[1] != b('\u0000')) { throw new DnsLookupException("Unrecognized Tor Auth Method"); } byte[] hostBytes = host.getBytes(Charsets.UTF_8); buf = new byte[7 + hostBytes.length]; buf[0] = b('\u0005'); // version SOCKS5 buf[1] = b('\u00f0'); // CMD_RESOLVE buf[2] = b('\u0000'); // (reserved) buf[3] = b('\u0003'); // SOCKS5_ATYPE_HOSTNAME buf[4] = (byte) hostBytes.length; System.arraycopy(hostBytes, 0, buf, 5, hostBytes.length); buf[5 + hostBytes.length] = 0; proxySocket.getOutputStream().write(buf); buf = new byte[4]; //noinspection UnusedAssignment int bytesRead = proxySocket.getInputStream().read(buf); // TODO: Should not be a length check here as well? /* if (bytesRead != 4) throw new DnsLookupException("Invalid Tor Address Response");*/ if (buf[0] != b('\u0005')) throw new DnsLookupException("Invalid Tor Proxy Response"); if (buf[1] != b('\u0000')) { if (!torStatusErrors.containsKey(buf[1])) { throw new DnsLookupException("Invalid Tor Proxy Response"); } throw new DnsLookupException(torStatusErrors.get(buf[1]) + "(host=" + host + ")"); } final char SOCKS5_ATYPE_IPV4 = '\u0001'; final char SOCKS5_ATYPE_IPV6 = '\u0004'; final byte atype = buf[3]; if (atype != b(SOCKS5_ATYPE_IPV4) && atype != b(SOCKS5_ATYPE_IPV6)) throw new DnsLookupException(torStatusErrors.get(b('\u0001')) + "(host=" + host + ")"); final int octets = (atype == SOCKS5_ATYPE_IPV4 ? 4 : 16); buf = new byte[octets]; bytesRead = proxySocket.getInputStream().read(buf); if (bytesRead != octets) throw new DnsLookupException("Invalid Tor Address Response"); return InetAddress.getByAddress(buf); } catch (IOException | DnsLookupException e) { log.warn("Error resolving " + host + ". Exception:\n" + e.toString()); throw new DnsLookupException(e); } } /** * so we can have prettier code without a bunch of casts. */ private static byte b(char c) { return (byte) c; } } ================================================ FILE: p2p/src/main/java/haveno/network/Socks5DnsDiscovery.java ================================================ /* * 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 . */ package haveno.network; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import haveno.common.util.SingleThreadExecutorUtils; import haveno.common.util.Utilities; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.net.discovery.DnsDiscovery; import org.bitcoinj.net.discovery.MultiplexingDiscovery; import org.bitcoinj.net.discovery.PeerDiscovery; import org.bitcoinj.net.discovery.PeerDiscoveryException; import org.bitcoinj.utils.ContextPropagatingThreadFactory; import org.bitcoinj.utils.DaemonThreadFactory; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; /** *

    Supports peer discovery through DNS over Socks5 proxy with RESOLVE DNS extension.

    *

    * (As of this writing, only Tor is known to support the RESOLVE DNS extension.) *

    *

    Failure to resolve individual host names will not cause an Exception to be thrown. * However, if all hosts passed fail to resolve a PeerDiscoveryException will be thrown during getPeers(). *

    *

    *

    DNS seeds do not attempt to enumerate every peer on the network. {@link DnsDiscovery#getPeers(long, java.util.concurrent.TimeUnit)} * will return up to 30 random peers from the set of those returned within the timeout period. If you want more peers * to connect to, you need to discover them via other means (like addr broadcasts).

    */ @Slf4j public class Socks5DnsDiscovery extends MultiplexingDiscovery { /** * Supports finding peers through DNS A records. Community run DNS entry points will be used. * * @param netParams Network parameters to be used for port information. */ public Socks5DnsDiscovery(Socks5Proxy proxy, NetworkParameters netParams) { this(proxy, netParams.getDnsSeeds(), netParams); } /** * Supports finding peers through DNS A records. * * @param dnsSeeds Host names to be examined for seed addresses. * @param params Network parameters to be used for port information. */ public Socks5DnsDiscovery(Socks5Proxy proxy, String[] dnsSeeds, NetworkParameters params) { super(params, buildDiscoveries(proxy, params, dnsSeeds)); } private static List buildDiscoveries(Socks5Proxy proxy, NetworkParameters params, String[] seeds) { List discoveries = new ArrayList<>(seeds.length); for (String seed : seeds) discoveries.add(new Socks5DnsSeedDiscovery(proxy, params, seed)); return discoveries; } @Override protected ExecutorService createExecutor() { // Attempted workaround for reported bugs on Linux in which gethostbyname does not appear to be properly // thread safe and can cause segfaults on some libc versions. if (Utilities.isLinux()) return SingleThreadExecutorUtils.getSingleThreadExecutor(new ContextPropagatingThreadFactory("DNS seed lookups")); else return Utilities.getFixedThreadPoolExecutor(seeds.size(), new DaemonThreadFactory("DNS seed lookups")); } /** * Implements discovery from a single DNS host over Socks5 proxy with RESOLVE DNS extension. * With our DnsLookupTor (used to not leak at DNS lookup) version we only get one address instead a list of addresses like in DnsDiscovery. * We get repeated the call until we have received enough addresses. */ public static class Socks5DnsSeedDiscovery implements PeerDiscovery { private final String hostname; private final NetworkParameters params; private final Socks5Proxy proxy; public Socks5DnsSeedDiscovery(Socks5Proxy proxy, NetworkParameters params, String hostname) { this.hostname = hostname; this.params = params; this.proxy = proxy; } /** * Returns peer addresses. The actual DNS lookup is performed here. */ @Override public InetSocketAddress[] getPeers(long services, long timeoutValue, TimeUnit timeoutUnit) throws PeerDiscoveryException { if (services != 0) throw new PeerDiscoveryException("DNS seeds cannot filter by services: " + services); try { InetSocketAddress addr = new InetSocketAddress(DnsLookupTor.lookup(proxy, hostname), params.getPort()); return new InetSocketAddress[]{addr}; } catch (Exception e) { throw new PeerDiscoveryException(e); } } @Override public void shutdown() { } @Override public String toString() { return hostname; } } } ================================================ FILE: p2p/src/main/java/haveno/network/Socks5MultiDiscovery.java ================================================ /* * 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 . */ package haveno.network; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.net.discovery.PeerDiscovery; import org.bitcoinj.net.discovery.PeerDiscoveryException; import org.bitcoinj.net.discovery.SeedPeers; import org.bitcoinj.params.MainNetParams; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.concurrent.TimeUnit; /** * This class implements various types of discovery over Socks5, * which can be enabled/disabled via constructor flag. */ @Slf4j public class Socks5MultiDiscovery implements PeerDiscovery { public static final int SOCKS5_DISCOVER_ADDR = 0x0001; public static final int SOCKS5_DISCOVER_DNS = 0x0010; public static final int SOCKS5_DISCOVER_ONION = 0x0100; public static final int SOCKS5_DISCOVER_ALL = 0x1111; private final ArrayList discoveryList = new ArrayList<>(); /** * Supports finding peers by hostname over a socks5 proxy. * * @param proxy proxy the socks5 proxy to connect over. * @param params param to be used for seed and port information. * @param mode specify discovery mode, OR'd together. one or more of: * SOCKS5_DISCOVER_ADDR * SOCKS5_DISCOVER_DNS * SOCKS5_DISCOVER_ONION * SOCKS5_DISCOVER_ALL */ public Socks5MultiDiscovery(Socks5Proxy proxy, NetworkParameters params, int mode) { if ((mode & SOCKS5_DISCOVER_ONION) != 0) discoveryList.add(new Socks5SeedOnionDiscovery(proxy, params)); // Testnet has no addrSeeds so SeedPeers is not supported (would throw a nullPointer) if ((mode & SOCKS5_DISCOVER_ADDR) != 0 && params == MainNetParams.get()) // note: SeedPeers does not perform any network operations, so does not use proxy. discoveryList.add(new SeedPeers(params)); if ((mode & SOCKS5_DISCOVER_DNS) != 0) discoveryList.add(new Socks5DnsDiscovery(proxy, params)); } /** * Returns an array containing all the Bitcoin nodes that have been discovered. */ @Override public InetSocketAddress[] getPeers(long services, long timeoutValue, TimeUnit timeoutUnit) throws PeerDiscoveryException { ArrayList list = new ArrayList<>(); for (PeerDiscovery discovery : discoveryList) { list.addAll(Arrays.asList(discovery.getPeers(services, timeoutValue, timeoutUnit))); } return list.toArray(new InetSocketAddress[list.size()]); } @Override public void shutdown() { //TODO should we add a DnsLookupTor.shutdown() ? } } ================================================ FILE: p2p/src/main/java/haveno/network/Socks5ProxyProvider.java ================================================ /* * 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 . */ package haveno.network; import com.google.inject.Inject; import com.google.inject.name.Named; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import haveno.common.config.Config; import haveno.network.p2p.network.NetworkNode; import java.net.UnknownHostException; import javax.annotation.Nullable; import org.apache.commons.lang3.exception.ExceptionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Provides Socks5Proxies for the monero network and http requests *

    * By default there is only used the haveno internal Tor proxy, which is used for the P2P network, xmr network * (if Tor for xmr is enabled) and http requests (if Tor for http requests is enabled). * If the user provides a socks5ProxyHttpAddress it will be used for http requests. * If the user provides a socks5ProxyXmrAddress, this will be used for the xmr network. * If socks5ProxyXmrAddress is present but no socks5ProxyHttpAddress the socks5ProxyXmrAddress will be used for http * requests. * If no socks5ProxyXmrAddress and no socks5ProxyHttpAddress is defined (default) we use socks5ProxyInternal. */ public class Socks5ProxyProvider { private static final Logger log = LoggerFactory.getLogger(Socks5ProxyProvider.class); @Nullable private NetworkNode socks5ProxyInternalFactory; // proxy used for btc network @Nullable private final Socks5Proxy socks5ProxyXmr; // if defined proxy used for http requests @Nullable private final Socks5Proxy socks5ProxyHttp; @Inject public Socks5ProxyProvider(@Named(Config.SOCKS_5_PROXY_XMR_ADDRESS) String socks5ProxyXmrAddress, @Named(Config.SOCKS_5_PROXY_HTTP_ADDRESS) String socks5ProxyHttpAddress) { socks5ProxyXmr = getProxyFromAddress(socks5ProxyXmrAddress); socks5ProxyHttp = getProxyFromAddress(socks5ProxyHttpAddress); } @Nullable public Socks5Proxy getSocks5Proxy() { if (socks5ProxyXmr != null) return socks5ProxyXmr; else if (socks5ProxyInternalFactory != null) return getSocks5ProxyInternal(); else return null; } @Nullable public Socks5Proxy getSocks5ProxyXmr() { return socks5ProxyXmr; } @Nullable public Socks5Proxy getSocks5ProxyHttp() { return socks5ProxyHttp; } @Nullable public Socks5Proxy getSocks5ProxyInternal() { return socks5ProxyInternalFactory.getSocksProxy(); } public void setSocks5ProxyInternal(@Nullable NetworkNode havenoSocks5ProxyFactory) { this.socks5ProxyInternalFactory = havenoSocks5ProxyFactory; } @Nullable private Socks5Proxy getProxyFromAddress(String socks5ProxyAddress) { if (!socks5ProxyAddress.isEmpty()) { String[] tokens = socks5ProxyAddress.split(":"); if (tokens.length == 2) { try { Socks5Proxy proxy = new Socks5Proxy(tokens[0], Integer.valueOf(tokens[1])); proxy.resolveAddrLocally(false); return proxy; } catch (UnknownHostException e) { log.error(ExceptionUtils.getStackTrace(e)); } } else { log.error("Incorrect format for socks5ProxyAddress. Should be: host:port.\n" + "socks5ProxyAddress=" + socks5ProxyAddress); } } return null; } } ================================================ FILE: p2p/src/main/java/haveno/network/Socks5SeedOnionDiscovery.java ================================================ /* * 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 . */ package haveno.network; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.net.discovery.PeerDiscovery; import org.bitcoinj.net.discovery.PeerDiscoveryException; import java.net.InetSocketAddress; import java.util.concurrent.TimeUnit; /** * Socks5SeedOnionDiscovery provides a list of known Bitcoin .onion seeds. * These are nodes running as hidden services on the Tor network. */ public class Socks5SeedOnionDiscovery implements PeerDiscovery { private InetSocketAddress[] seedAddrs; /** * Supports finding peers by hostname over a socks5 proxy. * * @param proxy proxy the socks5 proxy to connect over. * @param params param to be used for seed and port information. */ public Socks5SeedOnionDiscovery(@SuppressWarnings("UnusedParameters") Socks5Proxy proxy, NetworkParameters params) { // We do this because NetworkParameters does not contain any .onion // seeds. Perhaps someday... String[] seedAddresses = {}; switch (params.getId()) { case NetworkParameters.ID_MAINNET: seedAddresses = mainNetSeeds(); break; case NetworkParameters.ID_TESTNET: seedAddresses = testNet3Seeds(); break; } this.seedAddrs = convertAddrsString(seedAddresses, params.getPort()); } /** * returns .onion nodes available on mainnet */ private String[] mainNetSeeds() { // List copied from bitcoin-core on 2017-11-03 // https://raw.githubusercontent.com/bitcoin/bitcoin/master/contrib/seeds/nodes_main.txt return new String[]{ "2g5qfdkn2vvcbqhzcyvyiitg4ceukybxklraxjnu7atlhd22gdwywaid.onion", "2jmtxvyup3ijr7u6uvu7ijtnojx4g5wodvaedivbv74w4vzntxbrhvad.onion", "37m62wn7dz3uqpathpc4qfmgrbupachj52nt3jbtbjugpbu54kbud7yd.onion", "5g72ppm3krkorsfopcm2bi7wlv4ohhs4u4mlseymasn7g7zhdcyjpfid.onion", "7cgwjuwi5ehvcay4tazy7ya6463bndjk6xzrttw5t3xbpq4p22q6fyid.onion", "7pyrpvqdhmayxggpcyqn5l3m5vqkw3qubnmgwlpya2mdo6x7pih7r7id.onion", "b64xcbleqmwgq2u46bh4hegnlrzzvxntyzbmucn3zt7cssm7y4ubv3id.onion", "ejxefzf5fpst4mg2rib7grksvscl7p6fvjp6agzgfc2yglxnjtxc3aid.onion", "fjdyxicpm4o42xmedlwl3uvk5gmqdfs5j37wir52327vncjzvtpfv7yd.onion", "fpz6r5ppsakkwypjcglz6gcnwt7ytfhxskkfhzu62tnylcknh3eq6pad.onion", "fzhn4uoxfbfss7h7d6ffbn266ca432ekbbzvqtsdd55ylgxn4jucm5qd.onion", "gxo5anvfnffnftfy5frkgvplq3rpga2ie3tcblo2vl754fvnhgorn5yd.onion", "ifdu5qvbofrt4ekui2iyb3kbcyzcsglazhx2hn4wfskkrx2v24qxriid.onion", "itz3oxsihs62muvknc237xabl5f6w6rfznfhbpayrslv2j2ubels47yd.onion", "lrjh6fywjqttmlifuemq3puhvmshxzzyhoqx7uoufali57eypuenzzid.onion", "m7cbpjolo662uel7rpaid46as2otcj44vvwg3gccodnvaeuwbm3anbyd.onion", "opnyfyeiibe5qo5a3wbxzbb4xdiagc32bbce46owmertdknta5mi7uyd.onion", "owjsdxmzla6d7lrwkbmetywqym5cyswpihciesfl5qdv2vrmwsgy4uqd.onion", "q7kgmd7n7h27ds4fg7wocgniuqb3oe2zxp4nfe4skd5da6wyipibqzqd.onion", "rp7k2go3s5lyj3fnj6zn62ktarlrsft2ohlsxkyd7v3e3idqyptvread.onion", "sys54sv4xv3hn3sdiv3oadmzqpgyhd4u4xphv4xqk64ckvaxzm57a7yd.onion", "tddeij4qigtjr6jfnrmq6btnirmq5msgwcsdpcdjr7atftm7cxlqztid.onion", "vi5bnbxkleeqi6hfccjochnn65lcxlfqs4uwgmhudph554zibiusqnad.onion", "xqt25cobm5zqucac3634zfght72he6u3eagfyej5ellbhcdgos7t2had.onion" }; } /** * returns .onion nodes available on testnet3 */ private String[] testNet3Seeds() { // this list copied from bitcoin-core on 2017-01-19 // https://github.com/bitcoin/bitcoin/blob/57b34599b2deb179ff1bd97ffeab91ec9f904d85/contrib/seeds/nodes_test.txt return new String[]{ "thfsmmn2jbitcoin.onion", "it2pj4f7657g3rhi.onion", "nkf5e6b7pl4jfd4a.onion", "4zhkir2ofl7orfom.onion", "t6xj6wilh4ytvcs7.onion", "i6y6ivorwakd7nw3.onion", "ubqj4rsu3nqtxmtp.onion" }; } /** * Returns an array containing all the Bitcoin nodes within the list. */ @Override public InetSocketAddress[] getPeers(long services, long timeoutValue, TimeUnit timeoutUnit) throws PeerDiscoveryException { if (services != 0) throw new PeerDiscoveryException("DNS seeds cannot filter by services: " + services); return seedAddrs; } /** * Converts an array of hostnames to array of unresolved InetSocketAddress */ private InetSocketAddress[] convertAddrsString(String[] addrs, int port) { InetSocketAddress[] list = new InetSocketAddress[addrs.length]; for (int i = 0; i < addrs.length; i++) { list[i] = InetSocketAddress.createUnresolved(addrs[i], port); } return list; } @Override public void shutdown() { //TODO should we add a DnsLookupTor.shutdown() ? } } ================================================ FILE: p2p/src/main/java/haveno/network/crypto/DecryptedDataTuple.java ================================================ /* * 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 . */ package haveno.network.crypto; import haveno.common.proto.network.NetworkEnvelope; import lombok.EqualsAndHashCode; import lombok.Value; import java.security.PublicKey; @EqualsAndHashCode @Value public final class DecryptedDataTuple { private final NetworkEnvelope networkEnvelope; private final PublicKey sigPublicKey; public DecryptedDataTuple(NetworkEnvelope networkEnvelope, PublicKey sigPublicKey) { this.networkEnvelope = networkEnvelope; this.sigPublicKey = sigPublicKey; } } ================================================ FILE: p2p/src/main/java/haveno/network/crypto/EncryptionService.java ================================================ /* * 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 . */ package haveno.network.crypto; import com.google.inject.Inject; import com.google.protobuf.InvalidProtocolBufferException; import haveno.common.crypto.CryptoException; import haveno.common.crypto.Encryption; import static haveno.common.crypto.Encryption.decryptSecretKey; import haveno.common.crypto.Hash; import haveno.common.crypto.KeyRing; import haveno.common.crypto.PubKeyRing; import haveno.common.crypto.SealedAndSigned; import haveno.common.crypto.Sig; import haveno.common.proto.ProtobufferException; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.proto.network.NetworkProtoResolver; import haveno.network.p2p.DecryptedMessageWithPubKey; import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; import javax.crypto.SecretKey; import lombok.extern.slf4j.Slf4j; @Slf4j public class EncryptionService { private final KeyRing keyRing; private final NetworkProtoResolver networkProtoResolver; @Inject public EncryptionService(KeyRing keyRing, NetworkProtoResolver networkProtoResolver) { this.keyRing = keyRing; this.networkProtoResolver = networkProtoResolver; } public SealedAndSigned encryptAndSign(PubKeyRing pubKeyRing, NetworkEnvelope networkEnvelope) throws CryptoException { return encryptHybridWithSignature(networkEnvelope, keyRing.getSignatureKeyPair(), pubKeyRing.getEncryptionPubKey()); } /** * @param sealedAndSigned The sealedAndSigned object. * @param privateKey The private key for decryption * @return A DecryptedPayloadWithPubKey object. * @throws CryptoException */ public DecryptedDataTuple decryptHybridWithSignature(SealedAndSigned sealedAndSigned, PrivateKey privateKey) throws CryptoException, ProtobufferException { SecretKey secretKey = decryptSecretKey(sealedAndSigned.getEncryptedSecretKey(), privateKey); boolean isValid = Sig.verify(sealedAndSigned.getSigPublicKey(), Hash.getSha256Hash(sealedAndSigned.getEncryptedPayloadWithHmac()), sealedAndSigned.getSignature()); if (!isValid) throw new CryptoException("Signature verification failed."); try { final byte[] bytes = Encryption.decryptPayloadWithHmac(sealedAndSigned.getEncryptedPayloadWithHmac(), secretKey); final protobuf.NetworkEnvelope envelope = protobuf.NetworkEnvelope.parseFrom(bytes); NetworkEnvelope decryptedPayload = networkProtoResolver.fromProto(envelope); return new DecryptedDataTuple(decryptedPayload, sealedAndSigned.getSigPublicKey()); } catch (InvalidProtocolBufferException e) { throw new ProtobufferException("Unable to parse protobuffer message.", e); } } public DecryptedMessageWithPubKey decryptAndVerify(SealedAndSigned sealedAndSigned) throws CryptoException, ProtobufferException { DecryptedDataTuple decryptedDataTuple = decryptHybridWithSignature(sealedAndSigned, keyRing.getEncryptionKeyPair().getPrivate()); return new DecryptedMessageWithPubKey(decryptedDataTuple.getNetworkEnvelope(), decryptedDataTuple.getSigPublicKey()); } private static byte[] encryptPayloadWithHmac(NetworkEnvelope networkEnvelope, SecretKey secretKey) throws CryptoException { return Encryption.encryptPayloadWithHmac(networkEnvelope.toProtoNetworkEnvelope().toByteArray(), secretKey); } /** * @param payload The data to encrypt. * @param signatureKeyPair The key pair for signing. * @param encryptionPublicKey The public key used for encryption. * @return A SealedAndSigned object. * @throws CryptoException */ public static SealedAndSigned encryptHybridWithSignature(NetworkEnvelope payload, KeyPair signatureKeyPair, PublicKey encryptionPublicKey) throws CryptoException { // Create a symmetric key SecretKey secretKey = Encryption.generateSecretKey(256); // Encrypt secretKey with receiver's publicKey byte[] encryptedSecretKey = Encryption.encryptSecretKey(secretKey, encryptionPublicKey); // Encrypt with sym key payload with appended hmac byte[] encryptedPayloadWithHmac = encryptPayloadWithHmac(payload, secretKey); // sign hash of encryptedPayloadWithHmac byte[] hash = Hash.getSha256Hash(encryptedPayloadWithHmac); byte[] signature = Sig.sign(signatureKeyPair.getPrivate(), hash); // Pack all together return new SealedAndSigned(encryptedSecretKey, encryptedPayloadWithHmac, signature, signatureKeyPair.getPublic()); } } ================================================ FILE: p2p/src/main/java/haveno/network/crypto/EncryptionServiceModule.java ================================================ /* * 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 . */ package haveno.network.crypto; import com.google.inject.Singleton; import haveno.common.app.AppModule; import haveno.common.config.Config; public class EncryptionServiceModule extends AppModule { public EncryptionServiceModule(Config config) { super(config); } @Override protected void configure() { bind(EncryptionService.class).in(Singleton.class); } } ================================================ FILE: p2p/src/main/java/haveno/network/http/FakeDnsResolver.java ================================================ /* * 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 . */ package haveno.network.http; import org.apache.http.conn.DnsResolver; import java.net.InetAddress; import java.net.UnknownHostException; // This class is adapted from // http://stackoverflow.com/a/25203021/5616248 class FakeDnsResolver implements DnsResolver { @Override public InetAddress[] resolve(String host) throws UnknownHostException { // Return some fake DNS record for every request, we won't be using it return new InetAddress[]{InetAddress.getByAddress(new byte[]{1, 1, 1, 1})}; } } ================================================ FILE: p2p/src/main/java/haveno/network/http/HttpClient.java ================================================ /* * 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 . */ package haveno.network.http; import javax.annotation.Nullable; import java.io.IOException; public interface HttpClient { void setBaseUrl(String baseUrl); void setIgnoreSocks5Proxy(boolean ignoreSocks5Proxy); String get(String param, @Nullable String headerKey, @Nullable String headerValue) throws IOException; String post(String param, @Nullable String headerKey, @Nullable String headerValue) throws IOException; String getUid(); String getBaseUrl(); boolean hasPendingRequest(); void cancelPendingRequest(); void shutDown(); } ================================================ FILE: p2p/src/main/java/haveno/network/http/HttpClientImpl.java ================================================ /* * 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 . */ package haveno.network.http; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.Inject; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import haveno.common.ThreadUtils; import haveno.common.app.Version; import haveno.common.util.Utilities; import haveno.network.Socks5ProxyProvider; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.UUID; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.http.HttpEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.ssl.SSLContexts; // TODO close connection if failing @Slf4j public class HttpClientImpl implements HttpClient { @Nullable private Socks5ProxyProvider socks5ProxyProvider; @Nullable private HttpURLConnection connection; @Nullable private CloseableHttpClient closeableHttpClient; private static final long SHUTDOWN_TIMEOUT_MS = 5000l; @Getter @Setter private String baseUrl; @Setter private boolean ignoreSocks5Proxy; @Getter private final String uid; private boolean hasPendingRequest; @Inject public HttpClientImpl(@Nullable Socks5ProxyProvider socks5ProxyProvider) { this.socks5ProxyProvider = socks5ProxyProvider; uid = UUID.randomUUID().toString(); } public HttpClientImpl(String baseUrl) { this.baseUrl = baseUrl; uid = UUID.randomUUID().toString(); } @Override public void shutDown() { try { ThreadUtils.awaitTask(() -> { doShutDown(connection, closeableHttpClient); connection = null; closeableHttpClient = null; }, SHUTDOWN_TIMEOUT_MS); } catch (Exception e) { // ignore } } private void doShutDown(HttpURLConnection connection, CloseableHttpClient closeableHttpClient) { try { if (connection != null) { connection.getInputStream().close(); connection.disconnect(); } if (closeableHttpClient != null) { closeableHttpClient.close(); } } catch (IOException ignore) { } } @Override public boolean hasPendingRequest() { return hasPendingRequest; } @Override public String get(String param, @Nullable String headerKey, @Nullable String headerValue) throws IOException { return doRequest(param, HttpMethod.GET, headerKey, headerValue); } @Override public String post(String param, @Nullable String headerKey, @Nullable String headerValue) throws IOException { return doRequest(param, HttpMethod.POST, headerKey, headerValue); } private String doRequest(String param, HttpMethod httpMethod, @Nullable String headerKey, @Nullable String headerValue) throws IOException { checkNotNull(baseUrl, "baseUrl must be set before calling doRequest"); checkArgument(!hasPendingRequest, "We got called on the same HttpClient again while a request is still open."); hasPendingRequest = true; Socks5Proxy socks5Proxy = getSocks5Proxy(socks5ProxyProvider); if (ignoreSocks5Proxy || socks5Proxy == null || baseUrl.contains("localhost")) { return requestWithoutProxy(baseUrl, param, httpMethod, headerKey, headerValue); } else { return doRequestWithProxy(baseUrl, param, httpMethod, socks5Proxy, headerKey, headerValue); } } public void cancelPendingRequest() { if (!hasPendingRequest) return; shutDown(); hasPendingRequest = false; } private String requestWithoutProxy(String baseUrl, String param, HttpMethod httpMethod, @Nullable String headerKey, @Nullable String headerValue) throws IOException { long ts = System.currentTimeMillis(); log.debug("requestWithoutProxy: URL={}, param={}, httpMethod={}", baseUrl, param, httpMethod); try { String spec = httpMethod == HttpMethod.GET ? baseUrl + param : baseUrl; URL url = new URL(spec); connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod(httpMethod.name()); connection.setConnectTimeout((int) TimeUnit.SECONDS.toMillis(120)); connection.setReadTimeout((int) TimeUnit.SECONDS.toMillis(120)); connection.setRequestProperty("User-Agent", "haveno/" + Version.VERSION); if (headerKey != null && headerValue != null) { connection.setRequestProperty(headerKey, headerValue); } if (httpMethod == HttpMethod.POST) { connection.setDoOutput(true); connection.getOutputStream().write(param.getBytes(StandardCharsets.UTF_8)); } int responseCode = connection.getResponseCode(); if (responseCode == 200) { String response = convertInputStreamToString(connection.getInputStream()); log.debug("Response from {} with param {} took {} ms. Data size:{}, response: {}", baseUrl, param, System.currentTimeMillis() - ts, Utilities.readableFileSize(response.getBytes().length), Utilities.toTruncatedString(response)); return response; } else { InputStream errorStream = connection.getErrorStream(); if (errorStream != null) { String error = convertInputStreamToString(errorStream); errorStream.close(); log.info("Received errorMsg '{}' with responseCode {} from {}. Response took: {} ms. param: {}", error, responseCode, baseUrl, System.currentTimeMillis() - ts, param); throw new HttpException(error, responseCode); } else { log.info("Response with responseCode {} from {}. Response took: {} ms. param: {}", responseCode, baseUrl, System.currentTimeMillis() - ts, param); throw new HttpException("Request failed", responseCode); } } } catch (Throwable t) { String message = "Error at requestWithoutProxy with url " + baseUrl + " and param " + param + ". Throwable=" + t.getMessage(); throw new IOException(message, t); } finally { try { if (connection != null) { connection.getInputStream().close(); connection.disconnect(); connection = null; } } catch (Throwable ignore) { } hasPendingRequest = false; } } private String doRequestWithProxy(String baseUrl, String param, HttpMethod httpMethod, Socks5Proxy socks5Proxy, @Nullable String headerKey, @Nullable String headerValue) throws IOException { long ts = System.currentTimeMillis(); log.debug("doRequestWithProxy: baseUrl={}, param={}, httpMethod={}", baseUrl, param, httpMethod); // This code is adapted from: // http://stackoverflow.com/a/25203021/5616248 // Register our own SocketFactories to override createSocket() and connectSocket(). // connectSocket does NOT resolve hostname before passing it to proxy. Registry reg = RegistryBuilder.create() .register("http", new SocksConnectionSocketFactory()) .register("https", new SocksSSLConnectionSocketFactory(SSLContexts.createSystemDefault())).build(); // Use FakeDNSResolver if not resolving DNS locally. // This prevents a local DNS lookup (which would be ignored anyway) PoolingHttpClientConnectionManager cm = socks5Proxy.resolveAddrLocally() ? new PoolingHttpClientConnectionManager(reg) : new PoolingHttpClientConnectionManager(reg, new FakeDnsResolver()); try { closeableHttpClient = checkNotNull(HttpClients.custom().setConnectionManager(cm).build()); InetSocketAddress socksAddress = new InetSocketAddress(socks5Proxy.getInetAddress(), socks5Proxy.getPort()); // remove me: Use this to test with system-wide Tor proxy, or change port for another proxy. // InetSocketAddress socksAddress = new InetSocketAddress("127.0.0.1", 9050); HttpClientContext context = HttpClientContext.create(); context.setAttribute("socks.address", socksAddress); HttpUriRequest request = getHttpUriRequest(httpMethod, baseUrl, param); if (headerKey != null && headerValue != null) { request.setHeader(headerKey, headerValue); } try (CloseableHttpResponse httpResponse = closeableHttpClient.execute(request, context)) { String response = convertInputStreamToString(httpResponse.getEntity().getContent()); int statusCode = httpResponse.getStatusLine().getStatusCode(); if (statusCode == 200) { log.debug("Response from {} took {} ms. Data size:{}, response: {}, param: {}", baseUrl, System.currentTimeMillis() - ts, Utilities.readableFileSize(response.getBytes().length), Utilities.toTruncatedString(response), param); return response; } else { log.info("Received errorMsg '{}' with statusCode {} from {}. Response took: {} ms. param: {}", response, statusCode, baseUrl, System.currentTimeMillis() - ts, param); throw new HttpException(response, statusCode); } } } catch (Throwable t) { String message = "Error at doRequestWithProxy with url " + baseUrl + " and param " + param + ". Throwable=" + t.getMessage(); throw new IOException(message, t); } finally { if (closeableHttpClient != null) { closeableHttpClient.close(); closeableHttpClient = null; } hasPendingRequest = false; } } private HttpUriRequest getHttpUriRequest(HttpMethod httpMethod, String baseUrl, String param) throws UnsupportedEncodingException { switch (httpMethod) { case GET: return new HttpGet(baseUrl + param); case POST: HttpPost httpPost = new HttpPost(baseUrl); HttpEntity httpEntity = new StringEntity(param); httpPost.setEntity(httpEntity); return httpPost; default: throw new IllegalArgumentException("HttpMethod not supported: " + httpMethod); } } @Nullable private Socks5Proxy getSocks5Proxy(Socks5ProxyProvider socks5ProxyProvider) { if (socks5ProxyProvider == null) { return null; } // We use the custom socks5ProxyHttp. Socks5Proxy socks5Proxy = socks5ProxyProvider.getSocks5ProxyHttp(); if (socks5Proxy != null) { return socks5Proxy; } // If not set we request socks5ProxyProvider.getSocks5Proxy() // which delivers the btc proxy if set, otherwise the internal proxy. return socks5ProxyProvider.getSocks5Proxy(); } private String convertInputStreamToString(InputStream inputStream) throws IOException { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); StringBuilder stringBuilder = new StringBuilder(); String line; while ((line = bufferedReader.readLine()) != null) { stringBuilder.append(line); } return stringBuilder.toString(); } @Override public String toString() { return "HttpClientImpl{" + "\n socks5ProxyProvider=" + socks5ProxyProvider + ",\n baseUrl='" + baseUrl + '\'' + ",\n ignoreSocks5Proxy=" + ignoreSocks5Proxy + ",\n uid='" + uid + '\'' + ",\n connection=" + connection + ",\n httpclient=" + closeableHttpClient + "\n}"; } } ================================================ FILE: p2p/src/main/java/haveno/network/http/HttpException.java ================================================ /* * 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 . */ package haveno.network.http; import lombok.Getter; public class HttpException extends Exception { @Getter private final int responseCode; public HttpException(String message, int responseCode) { super(message); this.responseCode = responseCode; } } ================================================ FILE: p2p/src/main/java/haveno/network/http/HttpMethod.java ================================================ /* * 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 . */ package haveno.network.http; public enum HttpMethod { GET, POST } ================================================ FILE: p2p/src/main/java/haveno/network/http/SocksConnectionSocketFactory.java ================================================ /* * 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 . */ package haveno.network.http; import org.apache.http.HttpHost; import org.apache.http.conn.socket.PlainConnectionSocketFactory; import org.apache.http.protocol.HttpContext; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.Socket; // This class is adapted from // http://stackoverflow.com/a/25203021/5616248 // // This class routes connections over Socks, and avoids resolving hostnames locally. class SocksConnectionSocketFactory extends PlainConnectionSocketFactory { /** * creates an unconnected Socks Proxy socket */ @Override public Socket createSocket(final HttpContext context) throws IOException { InetSocketAddress socksaddr = (InetSocketAddress) context.getAttribute("socks.address"); Proxy proxy = new Proxy(Proxy.Type.SOCKS, socksaddr); return new Socket(proxy); } /** * connects a Socks Proxy socket and passes hostname to proxy without resolving it locally. */ @Override public Socket connectSocket(int connectTimeout, Socket socket, HttpHost host, InetSocketAddress remoteAddress, InetSocketAddress localAddress, HttpContext context) throws IOException { // Convert address to unresolved InetSocketAddress unresolvedRemote = InetSocketAddress .createUnresolved(host.getHostName(), remoteAddress.getPort()); return super.connectSocket(connectTimeout, socket, host, unresolvedRemote, localAddress, context); } } ================================================ FILE: p2p/src/main/java/haveno/network/http/SocksSSLConnectionSocketFactory.java ================================================ /* * 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 . */ package haveno.network.http; import org.apache.http.HttpHost; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.protocol.HttpContext; import javax.net.ssl.SSLContext; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.Socket; // This class is adapted from // http://stackoverflow.com/a/25203021/5616248 // // This class routes connections over Socks, and avoids resolving hostnames locally. class SocksSSLConnectionSocketFactory extends SSLConnectionSocketFactory { public SocksSSLConnectionSocketFactory(final SSLContext sslContext) { // TODO check alternative to deprecated call // Only allow connection's to site's with valid certs. super(sslContext, STRICT_HOSTNAME_VERIFIER); // Or to allow "insecure" (eg self-signed certs) // super(sslContext, ALLOW_ALL_HOSTNAME_VERIFIER); } /** * creates an unconnected Socks Proxy socket */ @Override public Socket createSocket(final HttpContext context) throws IOException { InetSocketAddress socksaddr = (InetSocketAddress) context.getAttribute("socks.address"); Proxy proxy = new Proxy(Proxy.Type.SOCKS, socksaddr); return new Socket(proxy); } /** * connects a Socks Proxy socket and passes hostname to proxy without resolving it locally. */ @Override public Socket connectSocket(int connectTimeout, Socket socket, HttpHost host, InetSocketAddress remoteAddress, InetSocketAddress localAddress, HttpContext context) throws IOException { // Convert address to unresolved InetSocketAddress unresolvedRemote = InetSocketAddress .createUnresolved(host.getHostName(), remoteAddress.getPort()); return super.connectSocket(connectTimeout, socket, host, unresolvedRemote, localAddress, context); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/AckMessage.java ================================================ /* * 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 . */ package haveno.network.p2p; import haveno.common.app.Version; import haveno.common.proto.ProtoUtil; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.proto.persistable.PersistablePayload; import haveno.network.p2p.mailbox.MailboxMessage; import haveno.network.p2p.storage.payload.ExpirablePayload; import lombok.EqualsAndHashCode; import lombok.Value; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.util.Optional; import java.util.UUID; import java.util.concurrent.TimeUnit; // TODO ExpirablePayload has no effect here as it is either a direct msg or packed into MailboxStoragePayload // We could extend the TTL by setting the TTL in MailboxStoragePayload from the type of msg which gets into the // SealedAndSigned data. // We exclude uid from hashcode and equals to detect duplicate entries of the same AckMessage @EqualsAndHashCode(callSuper = true, exclude = {"uid"}) @Value @Slf4j public final class AckMessage extends NetworkEnvelope implements MailboxMessage, PersistablePayload, ExpirablePayload { public static final long TTL = TimeUnit.DAYS.toMillis(7); private final String uid; private final NodeAddress senderNodeAddress; private final AckMessageSourceType sourceType; private final String sourceMsgClassName; @Nullable private final String sourceUid; private final String sourceId; private final boolean success; @Nullable private final String errorMessage; @Nullable private final String updatedMultisigHex; /** * * @param senderNodeAddress Address of sender * @param sourceType Type of source e.g. TradeMessage, DisputeMessage,... * @param sourceMsgClassName Class name of source msg * @param sourceUid Optional Uid of source (TradeMessage). Can be null if we receive trades/offers from old clients * @param sourceId Id of source (tradeId, disputeId) * @param success True if source message was processed successfully * @param errorMessage Optional error message if source message processing failed */ public AckMessage(NodeAddress senderNodeAddress, AckMessageSourceType sourceType, String sourceMsgClassName, String sourceUid, String sourceId, boolean success, String errorMessage) { this(UUID.randomUUID().toString(), senderNodeAddress, sourceType, sourceMsgClassName, sourceUid, sourceId, success, errorMessage, null, Version.getP2PMessageVersion()); } public AckMessage(NodeAddress senderNodeAddress, AckMessageSourceType sourceType, String sourceMsgClassName, String sourceUid, String sourceId, boolean success, String errorMessage, String updatedMultisigHex) { this(UUID.randomUUID().toString(), senderNodeAddress, sourceType, sourceMsgClassName, sourceUid, sourceId, success, errorMessage, updatedMultisigHex, Version.getP2PMessageVersion()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private AckMessage(String uid, NodeAddress senderNodeAddress, AckMessageSourceType sourceType, String sourceMsgClassName, @Nullable String sourceUid, String sourceId, boolean success, @Nullable String errorMessage, String updatedMultisigInfo, String messageVersion) { super(messageVersion); this.uid = uid; this.senderNodeAddress = senderNodeAddress; this.sourceType = sourceType; this.sourceMsgClassName = sourceMsgClassName; this.sourceUid = sourceUid; this.sourceId = sourceId; this.success = success; this.errorMessage = errorMessage; this.updatedMultisigHex = updatedMultisigInfo; } public protobuf.AckMessage toProtoMessage() { return getBuilder().build(); } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { return getNetworkEnvelopeBuilder().setAckMessage(getBuilder()).build(); } public protobuf.AckMessage.Builder getBuilder() { protobuf.AckMessage.Builder builder = protobuf.AckMessage.newBuilder() .setUid(uid) .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setSourceType(sourceType.name()) .setSourceMsgClassName(sourceMsgClassName) .setSourceId(sourceId) .setSuccess(success); Optional.ofNullable(sourceUid).ifPresent(builder::setSourceUid); Optional.ofNullable(errorMessage).ifPresent(builder::setErrorMessage); Optional.ofNullable(updatedMultisigHex).ifPresent(builder::setUpdatedMultisigHex); return builder; } public static AckMessage fromProto(protobuf.AckMessage proto, String messageVersion) { AckMessageSourceType sourceType = ProtoUtil.enumFromProto(AckMessageSourceType.class, proto.getSourceType()); return new AckMessage(proto.getUid(), NodeAddress.fromProto(proto.getSenderNodeAddress()), sourceType, proto.getSourceMsgClassName(), proto.getSourceUid().isEmpty() ? null : proto.getSourceUid(), proto.getSourceId(), proto.getSuccess(), proto.getErrorMessage().isEmpty() ? null : proto.getErrorMessage(), proto.getUpdatedMultisigHex().isEmpty() ? null : proto.getUpdatedMultisigHex(), messageVersion); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public long getTTL() { return TTL; } @Override public String toString() { return "AckMessage{" + "\n uid='" + uid + '\'' + ",\n senderNodeAddress=" + senderNodeAddress + ",\n sourceType=" + sourceType + ",\n sourceMsgClassName='" + sourceMsgClassName + '\'' + ",\n sourceUid='" + sourceUid + '\'' + ",\n sourceId='" + sourceId + '\'' + ",\n success=" + success + ",\n errorMessage='" + errorMessage + '\'' + ",\n updatedMultisigInfo='" + updatedMultisigHex + '\'' + "\n} " + super.toString(); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/AckMessageSourceType.java ================================================ /* * 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 . */ package haveno.network.p2p; public enum AckMessageSourceType { UNDEFINED, OFFER_MESSAGE, TRADE_MESSAGE, ARBITRATION_MESSAGE, MEDIATION_MESSAGE, TRADE_CHAT_MESSAGE, REFUND_MESSAGE, LOG_TRANSFER } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/AnonymousMessage.java ================================================ /* * 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 . */ package haveno.network.p2p; public interface AnonymousMessage { } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/BootstrapListener.java ================================================ /* * 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 . */ package haveno.network.p2p; public abstract class BootstrapListener implements P2PServiceListener { @Override public void onTorNodeReady() { } @Override public void onHiddenServicePublished() { } @Override public void onNoSeedNodeAvailable() { } @Override public void onNoPeersAvailable() { } @Override public void onSetupFailed(Throwable throwable) { } @Override public void onUpdatedDataReceived() { } @Override public abstract void onDataReceived(); @Override public void onRequestCustomBridges() { } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/BundleOfEnvelopes.java ================================================ /* * 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 . */ package haveno.network.p2p; import haveno.common.app.Capabilities; import haveno.common.app.Capability; import haveno.common.app.Version; import haveno.common.proto.ProtobufferException; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.proto.network.NetworkProtoResolver; import haveno.network.p2p.storage.messages.BroadcastMessage; import haveno.network.p2p.storage.payload.CapabilityRequiringPayload; import lombok.EqualsAndHashCode; import lombok.Value; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @EqualsAndHashCode(callSuper = true) @Value public final class BundleOfEnvelopes extends BroadcastMessage implements ExtendedDataSizePermission, CapabilityRequiringPayload { private final List envelopes; public BundleOfEnvelopes() { this(new ArrayList<>(), Version.getP2PMessageVersion()); } public BundleOfEnvelopes(List envelopes) { this(envelopes, Version.getP2PMessageVersion()); } public void add(NetworkEnvelope networkEnvelope) { envelopes.add(networkEnvelope); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private BundleOfEnvelopes(List envelopes, String messageVersion) { super(messageVersion); this.envelopes = envelopes; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { return getNetworkEnvelopeBuilder() .setBundleOfEnvelopes(protobuf.BundleOfEnvelopes.newBuilder().addAllEnvelopes(envelopes.stream() .map(NetworkEnvelope::toProtoNetworkEnvelope) .collect(Collectors.toList()))) .build(); } public static BundleOfEnvelopes fromProto(protobuf.BundleOfEnvelopes proto, NetworkProtoResolver resolver, String messageVersion) { List envelopes = proto.getEnvelopesList() .stream() .map(envelope -> { try { return resolver.fromProto(envelope); } catch (ProtobufferException e) { return null; } }) .filter(Objects::nonNull) .collect(Collectors.toList()); return new BundleOfEnvelopes(envelopes, messageVersion); } /////////////////////////////////////////////////////////////////////////////////////////// // CapabilityRequiringPayload /////////////////////////////////////////////////////////////////////////////////////////// @Override public Capabilities getRequiredCapabilities() { return new Capabilities(Capability.BUNDLE_OF_ENVELOPES); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/CloseConnectionMessage.java ================================================ /* * 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 . */ package haveno.network.p2p; import haveno.common.app.Version; import haveno.common.proto.network.NetworkEnvelope; import lombok.EqualsAndHashCode; import lombok.Value; @EqualsAndHashCode(callSuper = true) @Value public final class CloseConnectionMessage extends NetworkEnvelope { private final String reason; public CloseConnectionMessage(String reason) { this(reason, Version.getP2PMessageVersion()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private CloseConnectionMessage(String reason, String messageVersion) { super(messageVersion); this.reason = reason; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { return getNetworkEnvelopeBuilder() .setCloseConnectionMessage(protobuf.CloseConnectionMessage.newBuilder() .setReason(reason)) .build(); } public static CloseConnectionMessage fromProto(protobuf.CloseConnectionMessage proto, String messageVersion) { return new CloseConnectionMessage(proto.getReason(), messageVersion); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/DecryptedDirectMessageListener.java ================================================ /* * 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 . */ package haveno.network.p2p; public interface DecryptedDirectMessageListener { void onDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress peerNodeAddress); } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/DecryptedMessageWithPubKey.java ================================================ /* * 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 . */ package haveno.network.p2p; import com.google.protobuf.ByteString; import haveno.common.crypto.Sig; import haveno.common.proto.ProtobufferException; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.proto.network.NetworkProtoResolver; import haveno.common.proto.persistable.PersistablePayload; import lombok.Value; import java.security.PublicKey; @Value public final class DecryptedMessageWithPubKey implements PersistablePayload { private final NetworkEnvelope networkEnvelope; private final byte[] signaturePubKeyBytes; transient private final PublicKey signaturePubKey; public DecryptedMessageWithPubKey(NetworkEnvelope networkEnvelope, PublicKey signaturePubKey) { this.networkEnvelope = networkEnvelope; this.signaturePubKey = signaturePubKey; this.signaturePubKeyBytes = Sig.getPublicKeyBytes(signaturePubKey); } private DecryptedMessageWithPubKey(NetworkEnvelope networkEnvelope, byte[] signaturePubKeyBytes) { this.networkEnvelope = networkEnvelope; this.signaturePubKeyBytes = signaturePubKeyBytes; this.signaturePubKey = Sig.getPublicKeyFromBytes(signaturePubKeyBytes); } @Override public protobuf.DecryptedMessageWithPubKey toProtoMessage() { return protobuf.DecryptedMessageWithPubKey.newBuilder() .setNetworkEnvelope(networkEnvelope.toProtoNetworkEnvelope()) .setSignaturePubKeyBytes(ByteString.copyFrom(signaturePubKeyBytes)) .build(); } public static DecryptedMessageWithPubKey fromProto(protobuf.DecryptedMessageWithPubKey proto, NetworkProtoResolver networkProtoResolver) throws ProtobufferException { return new DecryptedMessageWithPubKey(networkProtoResolver.fromProto(proto.getNetworkEnvelope()), proto.getSignaturePubKeyBytes().toByteArray()); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/DirectMessage.java ================================================ /* * 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 . */ package haveno.network.p2p; public interface DirectMessage { } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/ExtendedDataSizePermission.java ================================================ /* * 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 . */ package haveno.network.p2p; // Marker interface for messages with higher allowed data size public interface ExtendedDataSizePermission { } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/FileTransferPart.java ================================================ /* * 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 . */ package haveno.network.p2p; import haveno.common.app.Version; import haveno.common.proto.network.NetworkEnvelope; import com.google.protobuf.ByteString; import lombok.EqualsAndHashCode; import lombok.Value; @EqualsAndHashCode(callSuper = true) @Value public class FileTransferPart extends NetworkEnvelope implements ExtendedDataSizePermission, SendersNodeAddressMessage { NodeAddress senderNodeAddress; public String uid; public String tradeId; public int traderId; public long seqNumOrFileLength; public ByteString messageData; // if message_data is empty it is the first message, requesting file upload permission public FileTransferPart(NodeAddress senderNodeAddress, String tradeId, int traderId, String uid, long seqNumOrFileLength, ByteString messageData) { this(senderNodeAddress, tradeId, traderId, uid, seqNumOrFileLength, messageData, Version.getP2PMessageVersion()); } public boolean isInitialRequest() { return messageData.size() == 0; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private FileTransferPart(NodeAddress senderNodeAddress, String tradeId, int traderId, String uid, long seqNumOrFileLength, ByteString messageData, String messageVersion) { super(messageVersion); this.senderNodeAddress = senderNodeAddress; this.tradeId = tradeId; this.traderId = traderId; this.uid = uid; this.seqNumOrFileLength = seqNumOrFileLength; this.messageData = messageData; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { return getNetworkEnvelopeBuilder() .setFileTransferPart(protobuf.FileTransferPart.newBuilder() .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setTradeId(tradeId) .setTraderId(traderId) .setUid(uid) .setSeqNumOrFileLength(seqNumOrFileLength) .setMessageData(messageData) .build()) .build(); } public static FileTransferPart fromProto(protobuf.FileTransferPart proto, String messageVersion) { return new FileTransferPart( NodeAddress.fromProto(proto.getSenderNodeAddress()), proto.getTradeId(), proto.getTraderId(), proto.getUid(), proto.getSeqNumOrFileLength(), proto.getMessageData(), messageVersion); } @Override public String toString() { return "FileTransferPart{" + "\n senderNodeAddress='" + senderNodeAddress.getAddressForDisplay() + '\'' + ",\n uid='" + uid + '\'' + ",\n tradeId='" + tradeId + '\'' + ",\n traderId='" + traderId + '\'' + ",\n seqNumOrFileLength=" + seqNumOrFileLength + "\n} " + super.toString(); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/InitialDataRequest.java ================================================ /* * 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 . */ package haveno.network.p2p; // Marker interface for initial data request public interface InitialDataRequest { } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/InitialDataResponse.java ================================================ /* * 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 . */ package haveno.network.p2p; // Marker interface for initial data response public interface InitialDataResponse { Class associatedRequest(); } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/NetworkNodeProvider.java ================================================ /* * 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 . */ package haveno.network.p2p; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.name.Named; import haveno.common.config.Config; import haveno.common.proto.network.NetworkProtoResolver; import haveno.network.p2p.network.BanFilter; import haveno.network.p2p.network.BridgeAddressProvider; import haveno.network.p2p.network.LocalhostNetworkNode; import haveno.network.p2p.network.NetworkNode; import haveno.network.p2p.network.NewTor; import haveno.network.p2p.network.RunningTor; import haveno.network.p2p.network.DirectBindTor; import haveno.network.p2p.network.TorMode; import haveno.network.p2p.network.TorNetworkNodeDirectBind; import haveno.network.p2p.network.TorNetworkNodeNetlayer; import java.io.File; import javax.annotation.Nullable; public class NetworkNodeProvider implements Provider { private final NetworkNode networkNode; @Inject public NetworkNodeProvider(NetworkProtoResolver networkProtoResolver, BridgeAddressProvider bridgeAddressProvider, @Nullable BanFilter banFilter, @Named(Config.MAX_CONNECTIONS) int maxConnections, @Named(Config.USE_LOCALHOST_FOR_P2P) boolean useLocalhostForP2P, @Named(Config.NODE_PORT) int port, @Named(Config.HIDDEN_SERVICE_ADDRESS) String hiddenServiceAddress, @Named(Config.TOR_DIR) File torDir, @Nullable @Named(Config.TORRC_FILE) File torrcFile, @Named(Config.TORRC_OPTIONS) String torrcOptions, @Named(Config.TOR_CONTROL_HOST) String controlHost, @Named(Config.TOR_CONTROL_PORT) int controlPort, @Named(Config.TOR_CONTROL_PASSWORD) String password, @Nullable @Named(Config.TOR_CONTROL_COOKIE_FILE) File cookieFile, @Named(Config.TOR_STREAM_ISOLATION) boolean streamIsolation, @Named(Config.TOR_CONTROL_USE_SAFE_COOKIE_AUTH) boolean useSafeCookieAuthentication) { if (useLocalhostForP2P) { networkNode = new LocalhostNetworkNode(port, networkProtoResolver, banFilter, maxConnections); } else { TorMode torMode = getTorMode(bridgeAddressProvider, torDir, torrcFile, torrcOptions, controlHost, controlPort, hiddenServiceAddress, password, cookieFile, useSafeCookieAuthentication); if (torMode instanceof NewTor || torMode instanceof RunningTor) { networkNode = new TorNetworkNodeNetlayer(port, networkProtoResolver, torMode, banFilter, maxConnections, streamIsolation, controlHost); } else { networkNode = new TorNetworkNodeDirectBind(port, networkProtoResolver, banFilter, maxConnections, hiddenServiceAddress); } } } private TorMode getTorMode(BridgeAddressProvider bridgeAddressProvider, File torDir, @Nullable File torrcFile, String torrcOptions, String controlHost, int controlPort, String hiddenServiceAddress, String password, @Nullable File cookieFile, boolean useSafeCookieAuthentication) { if (!hiddenServiceAddress.equals("")) { return new DirectBindTor(); } else if (controlPort != Config.UNSPECIFIED_PORT) { return new RunningTor(torDir, controlHost, controlPort, password, cookieFile, useSafeCookieAuthentication); } else { return new NewTor(torDir, torrcFile, torrcOptions, bridgeAddressProvider); } } @Override public NetworkNode get() { return networkNode; } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/NetworkNotReadyException.java ================================================ /* * 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 . */ package haveno.network.p2p; public class NetworkNotReadyException extends RuntimeException { public NetworkNotReadyException() { super("You must have bootstrapped before adding data to the P2P network."); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/NodeAddress.java ================================================ /* * 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 . */ package haveno.network.p2p; import haveno.common.consensus.UsedForTradeContractJson; import haveno.common.crypto.Hash; import haveno.common.proto.network.NetworkPayload; import haveno.common.proto.persistable.PersistablePayload; import haveno.common.util.JsonExclude; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.util.regex.Pattern; import static com.google.common.base.Preconditions.checkArgument; @Getter @EqualsAndHashCode @Slf4j public final class NodeAddress implements PersistablePayload, NetworkPayload, UsedForTradeContractJson { private final String hostName; private final int port; @JsonExclude private byte[] addressPrefixHash; public NodeAddress(String hostName, int port) { this.hostName = hostName; this.port = port; } public NodeAddress(String fullAddress) { final String[] split = fullAddress.split(Pattern.quote(":")); checkArgument(split.length == 2, "fullAddress must contain ':'"); this.hostName = split[0]; this.port = Integer.parseInt(split[1]); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// public protobuf.NodeAddress toProtoMessage() { return protobuf.NodeAddress.newBuilder().setHostName(hostName).setPort(port).build(); } public static NodeAddress fromProto(protobuf.NodeAddress proto) { return new NodeAddress(proto.getHostName(), proto.getPort()); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public String getFullAddress() { return hostName + ":" + port; } public String getAddressForDisplay() { if (hostName.endsWith(".onion")) return getHostNameForDisplay(); else return shortenAddressForDisplay(getFullAddress()); } private String getHostNameWithoutPostFix() { return hostName.replace(".onion", ""); } // tor v3 onions are too long to display for example in a table grid, so this convenience method // produces a display-friendly format which includes [first 7]..[last 7] characters. // tor v2 and localhost will be displayed in full, as they are 16 chars or fewer. private String getHostNameForDisplay() { return shortenAddressForDisplay(getHostNameWithoutPostFix()); } private String shortenAddressForDisplay(String address) { if (address.length() > 16) { return address.substring(0, 7) + ".." + address.substring(address.length() - 7); } return address; } // We use just a few chars from the full address to blur the potential receiver for sent network_messages public byte[] getAddressPrefixHash() { if (addressPrefixHash == null) addressPrefixHash = Hash.getSha256Hash(getFullAddress().substring(0, Math.min(2, getFullAddress().length()))); return addressPrefixHash; } @Override public String toString() { return getFullAddress(); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/P2PModule.java ================================================ /* * 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 . */ package haveno.network.p2p; import com.google.inject.Singleton; import com.google.inject.TypeLiteral; import static com.google.inject.name.Names.named; import static com.google.inject.util.Providers.of; import haveno.common.app.AppModule; import haveno.common.config.Config; import static haveno.common.config.Config.BAN_LIST; import static haveno.common.config.Config.MAX_CONNECTIONS; import static haveno.common.config.Config.NODE_PORT; import static haveno.common.config.Config.HIDDEN_SERVICE_ADDRESS; import static haveno.common.config.Config.REPUBLISH_MAILBOX_ENTRIES; import static haveno.common.config.Config.SOCKS_5_PROXY_HTTP_ADDRESS; import static haveno.common.config.Config.SOCKS_5_PROXY_XMR_ADDRESS; import static haveno.common.config.Config.TORRC_FILE; import static haveno.common.config.Config.TORRC_OPTIONS; import static haveno.common.config.Config.TOR_CONTROL_COOKIE_FILE; import static haveno.common.config.Config.TOR_CONTROL_HOST; import static haveno.common.config.Config.TOR_CONTROL_PASSWORD; import static haveno.common.config.Config.TOR_CONTROL_PORT; import static haveno.common.config.Config.TOR_CONTROL_USE_SAFE_COOKIE_AUTH; import static haveno.common.config.Config.TOR_DIR; import static haveno.common.config.Config.TOR_STREAM_ISOLATION; import static haveno.common.config.Config.USE_LOCALHOST_FOR_P2P; import haveno.network.Socks5ProxyProvider; import haveno.network.http.HttpClient; import haveno.network.http.HttpClientImpl; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.NetworkNode; import haveno.network.p2p.peers.Broadcaster; import haveno.network.p2p.peers.PeerManager; import haveno.network.p2p.peers.getdata.RequestDataManager; import haveno.network.p2p.peers.keepalive.KeepAliveManager; import haveno.network.p2p.peers.peerexchange.PeerExchangeManager; import haveno.network.p2p.storage.P2PDataStorage; import haveno.network.p2p.storage.persistence.AppendOnlyDataStoreService; import haveno.network.p2p.storage.persistence.ProtectedDataStoreService; import haveno.network.p2p.storage.persistence.ResourceDataStoreService; import java.io.File; import java.time.Clock; import java.util.List; public class P2PModule extends AppModule { public P2PModule(Config config) { super(config); } @Override protected void configure() { bind(Clock.class).toInstance(Clock.systemDefaultZone()); bind(P2PService.class).in(Singleton.class); bind(PeerManager.class).in(Singleton.class); bind(P2PDataStorage.class).in(Singleton.class); bind(AppendOnlyDataStoreService.class).in(Singleton.class); bind(ProtectedDataStoreService.class).in(Singleton.class); bind(ResourceDataStoreService.class).in(Singleton.class); bind(RequestDataManager.class).in(Singleton.class); bind(PeerExchangeManager.class).in(Singleton.class); bind(KeepAliveManager.class).in(Singleton.class); bind(Broadcaster.class).in(Singleton.class); bind(NetworkNode.class).toProvider(NetworkNodeProvider.class).in(Singleton.class); bind(Socks5ProxyProvider.class).in(Singleton.class); bind(HttpClient.class).to(HttpClientImpl.class); requestStaticInjection(Connection.class); bindConstant().annotatedWith(named(USE_LOCALHOST_FOR_P2P)).to(config.useLocalhostForP2P); bind(File.class).annotatedWith(named(TOR_DIR)).toInstance(config.torDir); bind(int.class).annotatedWith(named(NODE_PORT)).toInstance(config.nodePort); bind(String.class).annotatedWith(named(HIDDEN_SERVICE_ADDRESS)).toInstance(config.hiddenServiceAddress); bindConstant().annotatedWith(named(MAX_CONNECTIONS)).to(config.maxConnections); bind(new TypeLiteral>(){}).annotatedWith(named(BAN_LIST)).toInstance(config.banList); bindConstant().annotatedWith(named(SOCKS_5_PROXY_XMR_ADDRESS)).to(config.socks5ProxyXmrAddress); bindConstant().annotatedWith(named(SOCKS_5_PROXY_HTTP_ADDRESS)).to(config.socks5ProxyHttpAddress); bind(File.class).annotatedWith(named(TORRC_FILE)).toProvider(of(config.torrcFile)); // allow null value bindConstant().annotatedWith(named(TORRC_OPTIONS)).to(config.torrcOptions); bindConstant().annotatedWith(named(TOR_CONTROL_HOST)).to(config.torControlHost); bindConstant().annotatedWith(named(TOR_CONTROL_PORT)).to(config.torControlPort); bindConstant().annotatedWith(named(TOR_CONTROL_PASSWORD)).to(config.torControlPassword); bind(File.class).annotatedWith(named(TOR_CONTROL_COOKIE_FILE)).toProvider(of(config.torControlCookieFile)); bindConstant().annotatedWith(named(TOR_CONTROL_USE_SAFE_COOKIE_AUTH)).to(config.useTorControlSafeCookieAuth); bindConstant().annotatedWith(named(TOR_STREAM_ISOLATION)).to(config.torStreamIsolation); bindConstant().annotatedWith(named("MAX_SEQUENCE_NUMBER_MAP_SIZE_BEFORE_PURGE")).to(1000); bind(Boolean.class).annotatedWith(named(REPUBLISH_MAILBOX_ENTRIES)).toInstance(config.republishMailboxEntries); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/P2PService.java ================================================ /* * 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 . */ package haveno.network.p2p; import com.google.common.annotations.VisibleForTesting; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import com.google.inject.Inject; import haveno.common.UserThread; import haveno.common.app.Capabilities; import haveno.common.crypto.CryptoException; import haveno.common.crypto.KeyRing; import haveno.common.crypto.PubKeyRing; import haveno.common.proto.ProtobufferException; import haveno.common.proto.network.NetworkEnvelope; import haveno.network.Socks5ProxyProvider; import haveno.network.crypto.EncryptionService; import haveno.network.p2p.mailbox.MailboxMessageService; import haveno.network.p2p.network.CloseConnectionReason; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.ConnectionListener; import haveno.network.p2p.network.MessageListener; import haveno.network.p2p.network.NetworkNode; import haveno.network.p2p.network.SetupListener; import haveno.network.p2p.peers.Broadcaster; import haveno.network.p2p.peers.PeerManager; import haveno.network.p2p.peers.getdata.RequestDataManager; import haveno.network.p2p.peers.keepalive.KeepAliveManager; import haveno.network.p2p.peers.peerexchange.PeerExchangeManager; import haveno.network.p2p.storage.HashMapChangedListener; import haveno.network.p2p.storage.P2PDataStorage; import haveno.network.p2p.storage.messages.RefreshOfferMessage; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import haveno.network.p2p.storage.payload.ProtectedStoragePayload; import haveno.network.utils.CapabilityUtils; import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ReadOnlyIntegerProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import lombok.Getter; import org.apache.commons.lang3.exception.ExceptionUtils; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; import org.fxmisc.easybind.monadic.MonadicBinding; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; public class P2PService implements SetupListener, MessageListener, ConnectionListener, RequestDataManager.Listener { private static final Logger log = LoggerFactory.getLogger(P2PService.class); private final EncryptionService encryptionService; private final KeyRing keyRing; @Getter private final MailboxMessageService mailboxMessageService; private final NetworkNode networkNode; private final PeerManager peerManager; @Getter private final Broadcaster broadcaster; private final P2PDataStorage p2PDataStorage; private final RequestDataManager requestDataManager; private final PeerExchangeManager peerExchangeManager; @SuppressWarnings("FieldCanBeLocal") private final MonadicBinding networkReadyBinding; private final Set decryptedDirectMessageListeners = new CopyOnWriteArraySet<>(); private final Set p2pServiceListeners = new CopyOnWriteArraySet<>(); private final Set shutDownResultHandlers = new CopyOnWriteArraySet<>(); private final BooleanProperty hiddenServicePublished = new SimpleBooleanProperty(); private final BooleanProperty preliminaryDataReceived = new SimpleBooleanProperty(); private final IntegerProperty numConnectedPeers = new SimpleIntegerProperty(0); private final Subscription networkReadySubscription; private boolean isBootstrapped; private final KeepAliveManager keepAliveManager; private final Socks5ProxyProvider socks5ProxyProvider; @Getter private static NodeAddress myNodeAddress; @Getter private boolean isShutDownStarted = false; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// // Called also from SeedNodeP2PService @Inject public P2PService(NetworkNode networkNode, PeerManager peerManager, P2PDataStorage p2PDataStorage, RequestDataManager requestDataManager, PeerExchangeManager peerExchangeManager, KeepAliveManager keepAliveManager, Broadcaster broadcaster, Socks5ProxyProvider socks5ProxyProvider, EncryptionService encryptionService, KeyRing keyRing, MailboxMessageService mailboxMessageService) { this.networkNode = networkNode; this.peerManager = peerManager; this.p2PDataStorage = p2PDataStorage; this.requestDataManager = requestDataManager; this.peerExchangeManager = peerExchangeManager; this.keepAliveManager = keepAliveManager; this.broadcaster = broadcaster; this.socks5ProxyProvider = socks5ProxyProvider; this.encryptionService = encryptionService; this.keyRing = keyRing; this.mailboxMessageService = mailboxMessageService; this.networkNode.addConnectionListener(this); this.networkNode.addMessageListener(this); this.requestDataManager.setListener(this); // We need to have both the initial data delivered and the hidden service published networkReadyBinding = EasyBind.combine(hiddenServicePublished, preliminaryDataReceived, (hiddenServicePublished, preliminaryDataReceived) -> hiddenServicePublished && preliminaryDataReceived); networkReadySubscription = networkReadyBinding.subscribe((observable, oldValue, newValue) -> { if (newValue) onNetworkReady(); }); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void start(@Nullable P2PServiceListener listener) { if (listener != null) addP2PServiceListener(listener); networkNode.start(this); } public void onAllServicesInitialized() { if (networkNode.getNodeAddress() != null) { myNodeAddress = networkNode.getNodeAddress(); } else { // If our HS is still not published networkNode.nodeAddressProperty().addListener((observable, oldValue, newValue) -> { if (newValue != null) { myNodeAddress = networkNode.getNodeAddress(); } }); } } public void shutDown(Runnable shutDownCompleteHandler) { log.info("P2PService shutdown started"); shutDownResultHandlers.add(shutDownCompleteHandler); // We need to make sure queued up messages are flushed out before we continue shut down other network // services if (broadcaster != null) { broadcaster.shutDown(this::doShutDown); } else { doShutDown(); } } private void doShutDown() { log.info("P2PService doShutDown started"); isShutDownStarted = true; if (p2PDataStorage != null) { p2PDataStorage.shutDown(); } if (peerManager != null) { peerManager.shutDown(); } if (requestDataManager != null) { requestDataManager.shutDown(); } if (peerExchangeManager != null) { peerExchangeManager.shutDown(); } if (keepAliveManager != null) { keepAliveManager.shutDown(); } if (networkReadySubscription != null) { networkReadySubscription.unsubscribe(); } if (networkNode != null) { networkNode.shutDown(() -> shutDownResultHandlers.forEach(Runnable::run)); } else { shutDownResultHandlers.forEach(Runnable::run); } } /** * Startup sequence: *

    * Variant 1 (normal expected mode): * onTorNodeReady -> requestDataManager.firstDataRequestFromAnySeedNode() * RequestDataManager.Listener.onDataReceived && onHiddenServicePublished -> onNetworkReady() *

    * Variant 2 (no seed node available): * onTorNodeReady -> requestDataManager.firstDataRequestFromAnySeedNode * retry after 20-30 sec until we get at least one seed node connected * RequestDataManager.Listener.onDataReceived && onHiddenServicePublished -> onNetworkReady() */ /////////////////////////////////////////////////////////////////////////////////////////// // SetupListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onTorNodeReady() { socks5ProxyProvider.setSocks5ProxyInternal(networkNode); requestDataManager.requestPreliminaryData(); keepAliveManager.start(); synchronized (p2pServiceListeners) { p2pServiceListeners.forEach(SetupListener::onTorNodeReady); } } @Override public void onHiddenServicePublished() { checkArgument(networkNode.getNodeAddress() != null, "Address must be set when we have the hidden service ready"); hiddenServicePublished.set(true); synchronized (p2pServiceListeners) { p2pServiceListeners.forEach(SetupListener::onHiddenServicePublished); } } @Override public void onSetupFailed(Throwable throwable) { synchronized (p2pServiceListeners) { p2pServiceListeners.forEach(e -> e.onSetupFailed(throwable)); } } @Override public void onRequestCustomBridges() { synchronized (p2pServiceListeners) { p2pServiceListeners.forEach(SetupListener::onRequestCustomBridges); } } // Called from networkReadyBinding private void onNetworkReady() { networkReadySubscription.unsubscribe(); Optional seedNodeOfPreliminaryDataRequest = requestDataManager.getNodeAddressOfPreliminaryDataRequest(); checkArgument(seedNodeOfPreliminaryDataRequest.isPresent(), "seedNodeOfPreliminaryDataRequest must be present"); requestDataManager.requestUpdateData(); // If we start up first time we don't have any peers so we need to request from seed node. // As well it can be that the persisted peer list is outdated with dead peers. UserThread.runAfter(() -> { peerExchangeManager.requestReportedPeersFromSeedNodes(seedNodeOfPreliminaryDataRequest.get()); }, 100, TimeUnit.MILLISECONDS); // If we have reported or persisted peers we try to connect to those UserThread.runAfter(peerExchangeManager::initialRequestPeersFromReportedOrPersistedPeers, 300, TimeUnit.MILLISECONDS); } /////////////////////////////////////////////////////////////////////////////////////////// // RequestDataManager.Listener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onPreliminaryDataReceived() { checkArgument(!preliminaryDataReceived.get(), "preliminaryDataReceived was already set before."); preliminaryDataReceived.set(true); } @Override public void onUpdatedDataReceived() { synchronized (p2pServiceListeners) { p2pServiceListeners.forEach(P2PServiceListener::onUpdatedDataReceived); } } @Override public void onNoSeedNodeAvailable() { applyIsBootstrapped(P2PServiceListener::onNoSeedNodeAvailable); } @Override public void onNoPeersAvailable() { synchronized (p2pServiceListeners) { p2pServiceListeners.forEach(P2PServiceListener::onNoPeersAvailable); } } @Override public void onDataReceived() { applyIsBootstrapped(P2PServiceListener::onDataReceived); } private void applyIsBootstrapped(Consumer listenerHandler) { if (!isBootstrapped) { isBootstrapped = true; p2PDataStorage.onBootstrapped(); // We don't use a listener at mailboxMessageService as we require the correct // order of execution. The mailboxMessageService must be called before. mailboxMessageService.onBootstrapped(); // Once we have applied the state in the P2P domain we notify our listeners synchronized (p2pServiceListeners) { p2pServiceListeners.forEach(listenerHandler); } mailboxMessageService.initAfterBootstrapped(); } } /////////////////////////////////////////////////////////////////////////////////////////// // ConnectionListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onConnection(Connection connection) { numConnectedPeers.set(networkNode.getAllConnections().size()); //TODO check if still needed and why UserThread.runAfter(() -> numConnectedPeers.set(networkNode.getAllConnections().size()), 3); } @Override public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { numConnectedPeers.set(networkNode.getAllConnections().size()); //TODO check if still needed and why UserThread.runAfter(() -> numConnectedPeers.set(networkNode.getAllConnections().size()), 3); } /////////////////////////////////////////////////////////////////////////////////////////// // MessageListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { if (networkEnvelope instanceof PrefixedSealedAndSignedMessage) { PrefixedSealedAndSignedMessage sealedMsg = (PrefixedSealedAndSignedMessage) networkEnvelope; try { DecryptedMessageWithPubKey decryptedMsg = encryptionService.decryptAndVerify(sealedMsg.getSealedAndSigned()); connection.maybeHandleSupportedCapabilitiesMessage(decryptedMsg.getNetworkEnvelope()); connection.getPeersNodeAddressOptional().ifPresentOrElse(nodeAddress -> { synchronized (decryptedDirectMessageListeners) { decryptedDirectMessageListeners.forEach(e -> e.onDirectMessage(decryptedMsg, nodeAddress)); } }, () -> { log.error("peersNodeAddress is expected to be available at onMessage for " + "processing PrefixedSealedAndSignedMessage."); }); } catch (CryptoException e) { log.warn("Decryption of a direct message failed. This is not expected as the " + "direct message was sent to our node."); } catch (ProtobufferException e) { log.error("ProtobufferException at decryptAndVerify: {}", e.toString()); e.getStackTrace(); } } } /////////////////////////////////////////////////////////////////////////////////////////// // DirectMessages /////////////////////////////////////////////////////////////////////////////////////////// public void sendEncryptedDirectMessage(NodeAddress peerNodeAddress, PubKeyRing pubKeyRing, NetworkEnvelope message, SendDirectMessageListener sendDirectMessageListener) { sendEncryptedDirectMessage(peerNodeAddress, pubKeyRing, message, sendDirectMessageListener, null); } public void sendEncryptedDirectMessage(NodeAddress peerNodeAddress, PubKeyRing pubKeyRing, NetworkEnvelope message, SendDirectMessageListener sendDirectMessageListener, Integer timeoutSeconds) { checkNotNull(peerNodeAddress, "PeerAddress must not be null (sendEncryptedDirectMessage)"); if (isBootstrapped()) { doSendEncryptedDirectMessage(peerNodeAddress, pubKeyRing, message, sendDirectMessageListener, timeoutSeconds); } else { throw new NetworkNotReadyException(); } } private void doSendEncryptedDirectMessage(@NotNull NodeAddress peersNodeAddress, PubKeyRing pubKeyRing, NetworkEnvelope message, SendDirectMessageListener sendDirectMessageListener, Integer timeoutSeconds) { log.debug("Send encrypted direct message {} to peer {}", message.getClass().getSimpleName(), peersNodeAddress); checkNotNull(peersNodeAddress, "Peer node address must not be null at doSendEncryptedDirectMessage"); checkNotNull(networkNode.getNodeAddress(), "My node address must not be null at doSendEncryptedDirectMessage"); if (CapabilityUtils.capabilityRequiredAndCapabilityNotSupported(peersNodeAddress, message, peerManager)) { sendDirectMessageListener.onFault("We did not send the EncryptedMessage " + "because the peer does not support the capability."); return; } try { // Prefix is not needed for direct messages but as old code is doing the verification we still need to // send it if peer has not updated. PrefixedSealedAndSignedMessage sealedMsg = new PrefixedSealedAndSignedMessage( networkNode.getNodeAddress(), encryptionService.encryptAndSign(pubKeyRing, message)); SettableFuture future = networkNode.sendMessage(peersNodeAddress, sealedMsg, timeoutSeconds); Futures.addCallback(future, new FutureCallback<>() { @Override public void onSuccess(@Nullable Connection connection) { sendDirectMessageListener.onArrived(); } @Override public void onFailure(@NotNull Throwable throwable) { log.error(ExceptionUtils.getStackTrace(throwable)); sendDirectMessageListener.onFault(throwable.toString()); } }, MoreExecutors.directExecutor()); } catch (CryptoException e) { log.error("Error sending encrypted direct message, message={}, error={}\n", message.toString(), e.getMessage(), e); sendDirectMessageListener.onFault(e.toString()); } } /////////////////////////////////////////////////////////////////////////////////////////// // Data storage /////////////////////////////////////////////////////////////////////////////////////////// public boolean addPersistableNetworkPayload(PersistableNetworkPayload payload, boolean reBroadcast) { return p2PDataStorage.addPersistableNetworkPayload(payload, networkNode.getNodeAddress(), reBroadcast); } public boolean addProtectedStorageEntry(ProtectedStoragePayload protectedStoragePayload) { if (isBootstrapped()) { try { ProtectedStorageEntry protectedStorageEntry = p2PDataStorage.getProtectedStorageEntry(protectedStoragePayload, keyRing.getSignatureKeyPair()); return p2PDataStorage.addProtectedStorageEntry(protectedStorageEntry, networkNode.getNodeAddress(), null); } catch (CryptoException e) { log.error("Signing at getDataWithSignedSeqNr failed. That should never happen."); return false; } } else { throw new NetworkNotReadyException(); } } public boolean refreshTTL(ProtectedStoragePayload protectedStoragePayload) { if (isBootstrapped()) { try { RefreshOfferMessage refreshTTLMessage = p2PDataStorage.getRefreshTTLMessage(protectedStoragePayload, keyRing.getSignatureKeyPair()); return p2PDataStorage.refreshTTL(refreshTTLMessage, networkNode.getNodeAddress()); } catch (CryptoException e) { log.error("Signing at getDataWithSignedSeqNr failed. That should never happen."); return false; } } else { throw new NetworkNotReadyException(); } } public boolean removeData(ProtectedStoragePayload protectedStoragePayload) { if (isBootstrapped()) { try { ProtectedStorageEntry protectedStorageEntry = p2PDataStorage.getProtectedStorageEntry(protectedStoragePayload, keyRing.getSignatureKeyPair()); return p2PDataStorage.remove(protectedStorageEntry, networkNode.getNodeAddress()); } catch (CryptoException e) { log.error("Signing at getDataWithSignedSeqNr failed. That should never happen."); return false; } } else { throw new NetworkNotReadyException(); } } /////////////////////////////////////////////////////////////////////////////////////////// // Listeners /////////////////////////////////////////////////////////////////////////////////////////// public void addDecryptedDirectMessageListener(DecryptedDirectMessageListener listener) { synchronized (decryptedDirectMessageListeners) { decryptedDirectMessageListeners.add(listener); } } public void removeDecryptedDirectMessageListener(DecryptedDirectMessageListener listener) { synchronized (decryptedDirectMessageListeners) { decryptedDirectMessageListeners.remove(listener); } } public void addP2PServiceListener(P2PServiceListener listener) { synchronized (p2pServiceListeners) { p2pServiceListeners.add(listener); } } public void removeP2PServiceListener(P2PServiceListener listener) { synchronized (p2pServiceListeners) { p2pServiceListeners.remove(listener); } } public void addHashSetChangedListener(HashMapChangedListener hashMapChangedListener) { p2PDataStorage.addHashMapChangedListener(hashMapChangedListener); } public void removeHashMapChangedListener(HashMapChangedListener hashMapChangedListener) { p2PDataStorage.removeHashMapChangedListener(hashMapChangedListener); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// public boolean isBootstrapped() { return isBootstrapped; } public NetworkNode getNetworkNode() { return networkNode; } @Nullable public NodeAddress getAddress() { return networkNode.getNodeAddress(); } public ReadOnlyIntegerProperty getNumConnectedPeers() { return numConnectedPeers; } public Map getDataMap() { return p2PDataStorage.getMap(); } @VisibleForTesting public P2PDataStorage getP2PDataStorage() { return p2PDataStorage; } @VisibleForTesting public PeerManager getPeerManager() { return peerManager; } @VisibleForTesting public KeyRing getKeyRing() { return keyRing; } // TODO: this is unreliable and unused, because peer sometimes reports no TRADE_STATISTICS_3 capability, causing valid trades to be unpublished public Optional findPeersCapabilities(NodeAddress peer) { return networkNode.getConfirmedConnections().stream() .filter(e -> e.getPeersNodeAddressOptional().isPresent()) .filter(e -> e.getPeersNodeAddressOptional().get().equals(peer)) .map(Connection::getCapabilities) .findAny(); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/P2PServiceListener.java ================================================ /* * 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 . */ package haveno.network.p2p; import haveno.network.p2p.network.SetupListener; public interface P2PServiceListener extends SetupListener { void onDataReceived(); void onNoSeedNodeAvailable(); void onNoPeersAvailable(); void onUpdatedDataReceived(); } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/PrefixedSealedAndSignedMessage.java ================================================ /* * 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 . */ package haveno.network.p2p; import com.google.protobuf.ByteString; import haveno.common.app.Version; import haveno.common.crypto.SealedAndSigned; import haveno.common.proto.network.NetworkEnvelope; import haveno.network.p2p.mailbox.MailboxMessage; import lombok.EqualsAndHashCode; import lombok.Value; import java.util.UUID; import java.util.concurrent.TimeUnit; import static com.google.common.base.Preconditions.checkNotNull; @EqualsAndHashCode(callSuper = true) @Value public final class PrefixedSealedAndSignedMessage extends NetworkEnvelope implements MailboxMessage, SendersNodeAddressMessage { public static final long TTL = TimeUnit.DAYS.toMillis(15); private final NodeAddress senderNodeAddress; private final SealedAndSigned sealedAndSigned; // From v1.4.0 on addressPrefixHash can be an empty byte array. // We cannot make it nullable as not updated nodes would get a nullPointer exception at protobuf serialisation. private final byte[] addressPrefixHash; private final String uid; public PrefixedSealedAndSignedMessage(NodeAddress senderNodeAddress, SealedAndSigned sealedAndSigned) { this(senderNodeAddress, sealedAndSigned, new byte[0], UUID.randomUUID().toString(), Version.getP2PMessageVersion()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private PrefixedSealedAndSignedMessage(NodeAddress senderNodeAddress, SealedAndSigned sealedAndSigned, byte[] addressPrefixHash, String uid, String messageVersion) { super(messageVersion); this.senderNodeAddress = checkNotNull(senderNodeAddress, "senderNodeAddress must not be null"); this.sealedAndSigned = sealedAndSigned; this.addressPrefixHash = addressPrefixHash; this.uid = uid; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { return getNetworkEnvelopeBuilder() .setPrefixedSealedAndSignedMessage(protobuf.PrefixedSealedAndSignedMessage.newBuilder() .setNodeAddress(senderNodeAddress.toProtoMessage()) .setSealedAndSigned(sealedAndSigned.toProtoMessage()) .setAddressPrefixHash(ByteString.copyFrom(addressPrefixHash)) .setUid(uid)) .build(); } public static PrefixedSealedAndSignedMessage fromProto(protobuf.PrefixedSealedAndSignedMessage proto, String messageVersion) { return new PrefixedSealedAndSignedMessage(NodeAddress.fromProto(proto.getNodeAddress()), SealedAndSigned.fromProto(proto.getSealedAndSigned()), proto.getAddressPrefixHash().toByteArray(), proto.getUid(), messageVersion); } public static PrefixedSealedAndSignedMessage fromPayloadProto(protobuf.PrefixedSealedAndSignedMessage proto) { // We have the case that an envelope got wrapped into a payload. // We don't check the message version here as it was checked in the carrier envelope already (in connection class) // Payloads dont have a message version and are also used for persistence // We set the value to -1 to indicate it is set but irrelevant return new PrefixedSealedAndSignedMessage(NodeAddress.fromProto(proto.getNodeAddress()), SealedAndSigned.fromProto(proto.getSealedAndSigned()), proto.getAddressPrefixHash().toByteArray(), proto.getUid(), "-1"); } @Override public long getTTL() { return TTL; } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/SendDirectMessageListener.java ================================================ /* * 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 . */ package haveno.network.p2p; public interface SendDirectMessageListener { void onArrived(); void onFault(String ErrorMessage); } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/SendMailboxMessageListener.java ================================================ /* * 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 . */ package haveno.network.p2p; public interface SendMailboxMessageListener { void onArrived(); void onStoredInMailbox(); void onFault(@SuppressWarnings("UnusedParameters") String errorMessage); } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/SendersNodeAddressMessage.java ================================================ /* * 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 . */ package haveno.network.p2p; public interface SendersNodeAddressMessage { NodeAddress getSenderNodeAddress(); } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/SupportedCapabilitiesMessage.java ================================================ /* * 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 . */ package haveno.network.p2p; import haveno.common.app.Capabilities; import javax.annotation.Nullable; public interface SupportedCapabilitiesMessage { @Nullable Capabilities getSupportedCapabilities(); } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/UidMessage.java ================================================ /* * 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 . */ package haveno.network.p2p; public interface UidMessage { String getUid(); } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/mailbox/IgnoredMailboxMap.java ================================================ /* * 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 . */ package haveno.network.p2p.mailbox; import haveno.common.proto.persistable.PersistableEnvelope; import haveno.common.util.CollectionUtils; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.util.HashMap; import java.util.Map; @Slf4j @EqualsAndHashCode public class IgnoredMailboxMap implements PersistableEnvelope { @Getter private final Map dataMap; public IgnoredMailboxMap() { this.dataMap = new HashMap<>(); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// public IgnoredMailboxMap(Map ignored) { this.dataMap = ignored; } @Override public protobuf.PersistableEnvelope toProtoMessage() { return protobuf.PersistableEnvelope.newBuilder() .setIgnoredMailboxMap(protobuf.IgnoredMailboxMap.newBuilder().putAllData(dataMap)) .build(); } public static IgnoredMailboxMap fromProto(protobuf.IgnoredMailboxMap proto) { return new IgnoredMailboxMap(CollectionUtils.isEmpty(proto.getDataMap()) ? new HashMap<>() : proto.getDataMap()); } public void putAll(Map map) { dataMap.putAll(map); } public boolean containsKey(String uid) { return dataMap.containsKey(uid); } public void put(String uid, long creationTimeStamp) { dataMap.put(uid, creationTimeStamp); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/mailbox/IgnoredMailboxService.java ================================================ /* * 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 . */ package haveno.network.p2p.mailbox; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.persistable.PersistedDataHost; import haveno.network.p2p.storage.payload.MailboxStoragePayload; /** * We persist failed attempts to decrypt mailbox messages (expected if mailbox message was not addressed to us). * This improves performance at processing mailbox messages. * On a fast 4 core machine 1000 mailbox messages take about 1.5 second. At second start-up using the persisted data * it only takes about 30 ms. */ @Singleton public class IgnoredMailboxService implements PersistedDataHost { private final PersistenceManager persistenceManager; private final IgnoredMailboxMap ignoredMailboxMap = new IgnoredMailboxMap(); @Inject public IgnoredMailboxService(PersistenceManager persistenceManager) { this.persistenceManager = persistenceManager; this.persistenceManager.initialize(ignoredMailboxMap, PersistenceManager.Source.PRIVATE_LOW_PRIO); } /////////////////////////////////////////////////////////////////////////////////////////// // PersistedDataHost /////////////////////////////////////////////////////////////////////////////////////////// @Override public void readPersisted(Runnable completeHandler) { persistenceManager.readPersisted(persisted -> { // At each load we cleanup outdated entries long expiredDate = System.currentTimeMillis() - MailboxStoragePayload.TTL; persisted.getDataMap().entrySet().stream() .filter(e -> e.getValue() > expiredDate) .forEach(e -> ignoredMailboxMap.put(e.getKey(), e.getValue())); persistenceManager.requestPersistence(); completeHandler.run(); }, completeHandler); } public boolean isIgnored(String uid) { return ignoredMailboxMap.containsKey(uid); } public void ignore(String uid, long creationTimeStamp) { ignoredMailboxMap.put(uid, creationTimeStamp); persistenceManager.requestPersistence(); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/mailbox/MailboxItem.java ================================================ /* * 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 . */ package haveno.network.p2p.mailbox; import haveno.common.proto.ProtobufferException; import haveno.common.proto.network.NetworkProtoResolver; import haveno.common.proto.persistable.PersistablePayload; import haveno.network.p2p.DecryptedMessageWithPubKey; import haveno.network.p2p.storage.payload.ProtectedMailboxStorageEntry; import lombok.Value; import javax.annotation.Nullable; import java.time.Clock; import java.util.Optional; @Value public class MailboxItem implements PersistablePayload { private final ProtectedMailboxStorageEntry protectedMailboxStorageEntry; @Nullable private final DecryptedMessageWithPubKey decryptedMessageWithPubKey; public MailboxItem(ProtectedMailboxStorageEntry protectedMailboxStorageEntry, @Nullable DecryptedMessageWithPubKey decryptedMessageWithPubKey) { this.protectedMailboxStorageEntry = protectedMailboxStorageEntry; this.decryptedMessageWithPubKey = decryptedMessageWithPubKey; } @Override public protobuf.MailboxItem toProtoMessage() { protobuf.MailboxItem.Builder builder = protobuf.MailboxItem.newBuilder() .setProtectedMailboxStorageEntry(protectedMailboxStorageEntry.toProtoMessage()); Optional.ofNullable(decryptedMessageWithPubKey).ifPresent(decryptedMessageWithPubKey -> builder.setDecryptedMessageWithPubKey(decryptedMessageWithPubKey.toProtoMessage())); return builder .build(); } public static MailboxItem fromProto(protobuf.MailboxItem proto, NetworkProtoResolver networkProtoResolver) throws ProtobufferException { DecryptedMessageWithPubKey decryptedMessageWithPubKey = proto.hasDecryptedMessageWithPubKey() ? DecryptedMessageWithPubKey.fromProto(proto.getDecryptedMessageWithPubKey(), networkProtoResolver) : null; return new MailboxItem(ProtectedMailboxStorageEntry.fromProto(proto.getProtectedMailboxStorageEntry(), networkProtoResolver), decryptedMessageWithPubKey); } public boolean isMine() { return decryptedMessageWithPubKey != null; } public String getUid() { if (decryptedMessageWithPubKey != null) { // We use uid from mailboxMessage in case its ours as we have the at removeMailboxMsg only the // decryptedMessageWithPubKey available which contains the mailboxMessage. MailboxMessage mailboxMessage = (MailboxMessage) decryptedMessageWithPubKey.getNetworkEnvelope(); return mailboxMessage.getUid(); } else { // If its not our mailbox msg we take the uid from the prefixedSealedAndSignedMessage instead. // Those will never be removed via removeMailboxMsg but we clean up expired entries at startup. return protectedMailboxStorageEntry.getMailboxStoragePayload().getPrefixedSealedAndSignedMessage().getUid(); } } public boolean isExpired(Clock clock) { return protectedMailboxStorageEntry.isExpired(clock); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/mailbox/MailboxMessage.java ================================================ /* * 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 . */ package haveno.network.p2p.mailbox; import haveno.network.p2p.DirectMessage; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.UidMessage; import haveno.network.p2p.storage.payload.ExpirablePayload; public interface MailboxMessage extends DirectMessage, UidMessage, ExpirablePayload { NodeAddress getSenderNodeAddress(); } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/mailbox/MailboxMessageList.java ================================================ /* * 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 . */ package haveno.network.p2p.mailbox; import com.google.protobuf.Message; import haveno.common.proto.ProtobufferException; import haveno.common.proto.network.NetworkProtoResolver; import haveno.common.proto.persistable.PersistableList; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @Slf4j @EqualsAndHashCode(callSuper = true) public class MailboxMessageList extends PersistableList { public MailboxMessageList() { super(); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// public MailboxMessageList(List list) { super(list); } @Override public Message toProtoMessage() { synchronized (getList()) { return protobuf.PersistableEnvelope.newBuilder() .setMailboxMessageList(protobuf.MailboxMessageList.newBuilder() .addAllMailboxItem(getList().stream() .map(MailboxItem::toProtoMessage) .collect(Collectors.toList()))) .build(); } } public static MailboxMessageList fromProto(protobuf.MailboxMessageList proto, NetworkProtoResolver networkProtoResolver) { return new MailboxMessageList(new ArrayList<>(proto.getMailboxItemList().stream() .map(e -> { try { return MailboxItem.fromProto(e, networkProtoResolver); } catch (ProtobufferException protobufferException) { log.error("Error at MailboxItem.fromProto: {}", protobufferException.toString(), protobufferException); return null; } }) .filter(Objects::nonNull) .collect(Collectors.toList()))); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/mailbox/MailboxMessageService.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.network.p2p.mailbox; import com.google.common.base.Joiner; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.inject.name.Named; import haveno.common.ThreadUtils; import haveno.common.UserThread; import haveno.common.config.Config; import haveno.common.crypto.CryptoException; import haveno.common.crypto.KeyRing; import haveno.common.crypto.PubKeyRing; import haveno.common.crypto.SealedAndSigned; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.ProtobufferException; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.proto.persistable.PersistedDataHost; import haveno.common.util.Tuple2; import haveno.common.util.Utilities; import haveno.network.crypto.EncryptionService; import haveno.network.p2p.DecryptedMessageWithPubKey; import haveno.network.p2p.NetworkNotReadyException; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.PrefixedSealedAndSignedMessage; import haveno.network.p2p.SendMailboxMessageListener; import haveno.network.p2p.messaging.DecryptedMailboxListener; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.NetworkNode; import haveno.network.p2p.peers.BroadcastHandler; import haveno.network.p2p.peers.Broadcaster; import haveno.network.p2p.peers.PeerManager; import haveno.network.p2p.storage.HashMapChangedListener; import haveno.network.p2p.storage.P2PDataStorage; import haveno.network.p2p.storage.messages.AddDataMessage; import haveno.network.p2p.storage.payload.MailboxStoragePayload; import haveno.network.p2p.storage.payload.ProtectedMailboxStorageEntry; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import haveno.network.utils.CapabilityUtils; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import java.security.PublicKey; import java.time.Clock; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Random; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; import javax.annotation.Nullable; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; /** * Responsible for handling of mailbox messages. * We store mailbox messages locally to cause less load at initial data requests (using excluded keys) and to get better * resilience in case processing failed. In such cases it would be re-applied after restart. * * We use a map with the uid of the decrypted mailboxMessage if it was our own mailbox message. Otherwise we use the uid of the * prefixedSealedAndSignedMessage if it was a mailboxMessage not addressed to us. It would be better to use the hash of the payload but that * would require a large refactoring in the trade protocol. We call the remove method after a message got processed and pass * the tradeMessage to the remove method. We do not have the outer envelope which would be needed for the hash and * we do not want to pass around that to all trade methods just for that use case. So we use the uid as lookup to get * the mailboxItem containing the data we need for removal. * * If a node was not online and the remove mailbox message was sent during that time, the persisted mailbox message * does not get removed. So we need to take care that the persisted data is not growing too much and we apply some * filtering and limiting at reading the persisted data. * Any message gets removed once the expiry data (max 15 days) is reached. Missing messages would be delivered from * initial data requests. */ @Singleton @Slf4j public class MailboxMessageService implements HashMapChangedListener, PersistedDataHost { private static final long REPUBLISH_DELAY_SEC = TimeUnit.MINUTES.toSeconds(2); private static final long MAX_SERIALIZED_SIZE = 50000; private static final String THREAD_ID = MailboxMessageService.class.getSimpleName(); private final NetworkNode networkNode; private final PeerManager peerManager; private final P2PDataStorage p2PDataStorage; private final EncryptionService encryptionService; private final IgnoredMailboxService ignoredMailboxService; private final PersistenceManager persistenceManager; private final KeyRing keyRing; private final Clock clock; private final boolean republishMailboxEntries; private final Set decryptedMailboxListeners = new CopyOnWriteArraySet<>(); private final MailboxMessageList mailboxMessageList = new MailboxMessageList(); private final Map mailboxItemsByUid = new HashMap<>(); private boolean isBootstrapped; private boolean allServicesInitialized; private boolean initAfterBootstrapped; private BooleanProperty isInitializedProperty = new SimpleBooleanProperty(false); private static Comparator mailboxMessageComparator; @Inject public MailboxMessageService(NetworkNode networkNode, PeerManager peerManager, P2PDataStorage p2PDataStorage, EncryptionService encryptionService, IgnoredMailboxService ignoredMailboxService, PersistenceManager persistenceManager, KeyRing keyRing, Clock clock, @Named(Config.REPUBLISH_MAILBOX_ENTRIES) boolean republishMailboxEntries) { this.networkNode = networkNode; this.peerManager = peerManager; this.p2PDataStorage = p2PDataStorage; this.encryptionService = encryptionService; this.ignoredMailboxService = ignoredMailboxService; this.persistenceManager = persistenceManager; this.keyRing = keyRing; this.clock = clock; this.republishMailboxEntries = republishMailboxEntries; this.persistenceManager.initialize(mailboxMessageList, PersistenceManager.Source.PRIVATE_LOW_PRIO); } /////////////////////////////////////////////////////////////////////////////////////////// // PersistedDataHost /////////////////////////////////////////////////////////////////////////////////////////// @Override public void readPersisted(Runnable completeHandler) { persistenceManager.readPersisted(persisted -> { Map>> numItemsPerDay = new HashMap<>(); AtomicLong totalSize = new AtomicLong(); // We sort by creation date and limit to max 3000 entries, so the oldest items get skipped even if TTL // is not reached. 3000 items is about 60 MB with max size of 20kb supported for storage. persisted.stream() .sorted(Comparator.comparingLong(o -> ((MailboxItem) o).getProtectedMailboxStorageEntry().getCreationTimeStamp()).reversed()) .filter(e -> !e.isExpired(clock)) .filter(e -> !mailboxItemsByUid.containsKey(e.getUid())) .limit(3000) .forEach(mailboxItem -> { ProtectedMailboxStorageEntry protectedMailboxStorageEntry = mailboxItem.getProtectedMailboxStorageEntry(); int serializedSize = protectedMailboxStorageEntry.toProtoMessage().getSerializedSize(); // Usual size is 3-4kb. A few are about 15kb and very few are larger and about 100kb or // more (probably attachments in disputes) String date = new Date(protectedMailboxStorageEntry.getCreationTimeStamp()).toString(); String day = date.substring(4, 10); numItemsPerDay.putIfAbsent(day, new Tuple2<>(new AtomicLong(0), new ArrayList<>())); Tuple2> tuple = numItemsPerDay.get(day); tuple.first.getAndIncrement(); tuple.second.add(serializedSize); // We only keep small items, to reduce the potential impact of missed remove messages. // E.g. if a seed at a longer restart period missed the remove messages, then when loading from // persisted data the messages, they would add those again and distribute then later at requests to peers. // Those outdated messages would then stay in the network until TTL triggers removal. // By not applying large messages we reduce the impact of such cases at costs of extra loading costs if the message is still alive. if (serializedSize < MAX_SERIALIZED_SIZE) { synchronized (mailboxMessageList) { mailboxItemsByUid.put(mailboxItem.getUid(), mailboxItem); mailboxMessageList.add(mailboxItem); totalSize.getAndAdd(serializedSize); } // We add it to our map so that it get added to the excluded key set we send for // the initial data requests. So that helps to lower the load for mailbox messages at // initial data requests. p2PDataStorage.addProtectedMailboxStorageEntryToMap(protectedMailboxStorageEntry); } else { log.info("We ignore this large persisted mailboxItem. If still valid we will reload it from seed nodes at getData requests.\n" + "Size={}; date={}; sender={}", Utilities.readableFileSize(serializedSize), date, mailboxItem.getProtectedMailboxStorageEntry().getMailboxStoragePayload().getPrefixedSealedAndSignedMessage().getSenderNodeAddress()); } }); List perDay = numItemsPerDay.entrySet().stream() .sorted(Map.Entry.comparingByKey()) .map(entry -> { Tuple2> tuple = entry.getValue(); List sizes = tuple.second; long sum = sizes.stream().mapToLong(s -> s).sum(); List largeItems = sizes.stream() .filter(s -> s > 20000) .map(Utilities::readableFileSize) .collect(Collectors.toList()); String largeMsgInfo = largeItems.isEmpty() ? "" : "; Large messages: " + largeItems; return entry.getKey() + ": Num messages: " + tuple.first + "; Total size: " + Utilities.readableFileSize(sum) + largeMsgInfo; }) .collect(Collectors.toList()); log.info("We loaded {} persisted mailbox messages with {}.\nPer day distribution:\n{}", mailboxMessageList.size(), Utilities.readableFileSize(totalSize.get()), Joiner.on("\n").join(perDay)); requestPersistence(); completeHandler.run(); }, completeHandler); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// // We wait until all services are ready to avoid some edge cases as in https://github.com/bisq-network/bisq/issues/6367 public void onAllServicesInitialized() { allServicesInitialized = true; init(); } // We don't listen on requestDataManager directly as we require the correct // order of execution. The p2pService is handling the correct order of execution and we get called // directly from there. public void onBootstrapped() { if (!isBootstrapped) { isBootstrapped = true; } } // second stage starup for MailboxMessageService ... apply existing messages to their modules public void initAfterBootstrapped() { initAfterBootstrapped = true; init(); } private void init() { if (allServicesInitialized && initAfterBootstrapped) { // Only now we start listening and processing. The p2PDataStorage is our cache for data we have received // after the hidden service was ready. addHashMapChangedListener(); Collection entries = p2PDataStorage.getMap().values(); onAdded(entries); maybeRepublishMailBoxMessages(); if (entries.isEmpty()) UserThread.execute(() -> isInitializedProperty.set(true)); } } public BooleanProperty getIsInitializedProperty() { return isInitializedProperty; } public void sendEncryptedMailboxMessage(NodeAddress peer, PubKeyRing peersPubKeyRing, MailboxMessage mailboxMessage, SendMailboxMessageListener sendMailboxMessageListener) { if (peersPubKeyRing == null) { log.debug("sendEncryptedMailboxMessage: peersPubKeyRing is null. We ignore the call."); return; } checkNotNull(peer, "PeerAddress must not be null (sendEncryptedMailboxMessage)"); checkNotNull(networkNode.getNodeAddress(), "My node address must not be null at sendEncryptedMailboxMessage"); checkArgument(!keyRing.getPubKeyRing().equals(peersPubKeyRing), "We got own keyring instead of that from peer"); if (!isBootstrapped) { throw new NetworkNotReadyException(); } if (networkNode.getAllConnections().isEmpty()) { sendMailboxMessageListener.onFault("There are no P2P network nodes connected. " + "Please check your internet connection."); return; } NetworkEnvelope networkEnvelope = (NetworkEnvelope) mailboxMessage; if (CapabilityUtils.capabilityRequiredAndCapabilityNotSupported(peer, networkEnvelope, peerManager)) { sendMailboxMessageListener.onFault("We did not send the EncryptedMailboxMessage " + "because the peer does not support the capability."); return; } try { PrefixedSealedAndSignedMessage prefixedSealedAndSignedMessage = new PrefixedSealedAndSignedMessage( networkNode.getNodeAddress(), encryptionService.encryptAndSign(peersPubKeyRing, networkEnvelope)); SettableFuture future = networkNode.sendMessage(peer, prefixedSealedAndSignedMessage); Futures.addCallback(future, new FutureCallback<>() { @Override public void onSuccess(@Nullable Connection connection) { sendMailboxMessageListener.onArrived(); } @Override public void onFailure(@NotNull Throwable throwable) { PublicKey receiverStoragePublicKey = peersPubKeyRing.getSignaturePubKey(); long ttl = mailboxMessage.getTTL(); log.trace("## We take TTL from {}. ttl={}", mailboxMessage.getClass().getSimpleName(), ttl); addMailboxData(new MailboxStoragePayload(prefixedSealedAndSignedMessage, keyRing.getSignatureKeyPair().getPublic(), receiverStoragePublicKey, ttl), receiverStoragePublicKey, sendMailboxMessageListener); } }, MoreExecutors.directExecutor()); } catch (CryptoException e) { log.error("sendEncryptedMessage failed: {}\n", e.getMessage(), e); sendMailboxMessageListener.onFault("sendEncryptedMailboxMessage failed " + e); } } /** * The mailboxMessage has been applied and we remove it from our local storage and from the network. * * @param mailboxMessage The MailboxMessage to be removed */ public void removeMailboxMsg(MailboxMessage mailboxMessage) { if (isBootstrapped) { synchronized (mailboxMessageList) { String uid = mailboxMessage.getUid(); if (!mailboxItemsByUid.containsKey(uid)) { return; } // We called removeMailboxEntryFromNetwork at processMyMailboxItem, // but in case we have not been bootstrapped at that moment it did not get removed from the network. // So to be sure it gets removed we try to remove it now again. // In case it was removed earlier it will return early anyway inside the p2pDataStorage. removeMailboxEntryFromNetwork(mailboxItemsByUid.get(uid).getProtectedMailboxStorageEntry()); // We will get called the onRemoved handler which triggers removeMailboxItemFromMap as well. // But as we use the uid from the decrypted data which is not available at onRemoved we need to // call removeMailboxItemFromMap here. The onRemoved only removes foreign mailBoxMessages. log.trace("## removeMailboxMsg uid={}", uid); removeMailboxItemFromLocalStore(uid); } } else { // In case the network was not ready yet we try again later UserThread.runAfter(() -> removeMailboxMsg(mailboxMessage), 30); } } public Set getMyDecryptedMailboxMessages() { return mailboxItemsByUid.values().stream() .filter(MailboxItem::isMine) .map(MailboxItem::getDecryptedMessageWithPubKey) .collect(Collectors.toSet()); } public void addDecryptedMailboxListener(DecryptedMailboxListener listener) { decryptedMailboxListeners.add(listener); } /////////////////////////////////////////////////////////////////////////////////////////// // HashMapChangedListener implementation for ProtectedStorageEntry items /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onAdded(Collection protectedStorageEntries) { log.trace("## onAdded"); Collection entries = protectedStorageEntries.stream() .filter(e -> e instanceof ProtectedMailboxStorageEntry) .map(e -> (ProtectedMailboxStorageEntry) e) .filter(e -> networkNode.getNodeAddress() != null) .collect(Collectors.toSet()); threadedBatchProcessMailboxEntries(entries); } @Override public void onRemoved(Collection protectedStorageEntries) { log.trace("## onRemoved"); // We can only remove the foreign mailbox messages as for our own we use the uid from the decrypted // payload which is not available here. But own mailbox messages get removed anyway after processing // at the removeMailboxMsg method. protectedStorageEntries.stream() .filter(protectedStorageEntry -> protectedStorageEntry instanceof ProtectedMailboxStorageEntry) .map(protectedStorageEntry -> (ProtectedMailboxStorageEntry) protectedStorageEntry) .map(e -> e.getMailboxStoragePayload().getPrefixedSealedAndSignedMessage().getUid()) .filter(mailboxItemsByUid::containsKey) .forEach(this::removeMailboxItemFromLocalStore); } public static void setMailboxMessageComparator(Comparator comparator) { mailboxMessageComparator = comparator; } public static class DecryptedMessageWithPubKeyComparator implements Comparator { @Override public int compare(DecryptedMessageWithPubKey m1, DecryptedMessageWithPubKey m2) { if (m1.getNetworkEnvelope() instanceof MailboxMessage) { if (m2.getNetworkEnvelope() instanceof MailboxMessage) return mailboxMessageComparator.compare((MailboxMessage) m1.getNetworkEnvelope(), (MailboxMessage) m2.getNetworkEnvelope()); else return 1; } else { return m2.getNetworkEnvelope() instanceof MailboxMessage ? -1 : 0; } } } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void addHashMapChangedListener() { p2PDataStorage.addHashMapChangedListener(this); } // We run the batch processing of all mailbox messages we have received at startup in a thread to not block the UI. // For about 1000 messages decryption takes about 1 sec. private void threadedBatchProcessMailboxEntries(Collection protectedMailboxStorageEntries) { long ts = System.currentTimeMillis(); SettableFuture> future = SettableFuture.create(); new Thread(() -> { try { var mailboxItems = getMailboxItems(protectedMailboxStorageEntries); if (!protectedMailboxStorageEntries.isEmpty()) log.info("Batch processing of {} mailbox entries took {} ms", protectedMailboxStorageEntries.size(), System.currentTimeMillis() - ts); future.set(mailboxItems); } catch (Throwable throwable) { future.setException(throwable); } }, "processMailboxEntry-" + new Random().nextInt(1000)).start(); Futures.addCallback(future, new FutureCallback<>() { public void onSuccess(Set decryptedMailboxMessageWithEntries) { ThreadUtils.execute(() -> { handleMailboxItems(decryptedMailboxMessageWithEntries); UserThread.execute(() -> isInitializedProperty.set(true)); }, THREAD_ID); } public void onFailure(@NotNull Throwable throwable) { log.error(throwable.toString()); } }, MoreExecutors.directExecutor()); } private Set getMailboxItems(Collection protectedMailboxStorageEntries) { Set mailboxItems = new HashSet<>(); protectedMailboxStorageEntries.stream() .map(this::tryDecryptProtectedMailboxStorageEntry) .forEach(mailboxItems::add); return mailboxItems; } private MailboxItem tryDecryptProtectedMailboxStorageEntry(ProtectedMailboxStorageEntry protectedMailboxStorageEntry) { PrefixedSealedAndSignedMessage prefixedSealedAndSignedMessage = protectedMailboxStorageEntry .getMailboxStoragePayload() .getPrefixedSealedAndSignedMessage(); SealedAndSigned sealedAndSigned = prefixedSealedAndSignedMessage.getSealedAndSigned(); String uid = prefixedSealedAndSignedMessage.getUid(); if (ignoredMailboxService.isIgnored(uid)) { // We had persisted a past failed decryption attempt on that message so we don't try again and return early return new MailboxItem(protectedMailboxStorageEntry, null); } try { DecryptedMessageWithPubKey decryptedMessageWithPubKey = encryptionService.decryptAndVerify(sealedAndSigned); checkArgument(decryptedMessageWithPubKey.getNetworkEnvelope() instanceof MailboxMessage); return new MailboxItem(protectedMailboxStorageEntry, decryptedMessageWithPubKey); } catch (CryptoException ignore) { // Expected if message was not intended for us // We persist those entries so at the next startup we do not need to try to decrypt it anymore ignoredMailboxService.ignore(uid, protectedMailboxStorageEntry.getCreationTimeStamp()); } catch (ProtobufferException e) { log.error(e.toString()); e.getStackTrace(); } return new MailboxItem(protectedMailboxStorageEntry, null); } private void handleMailboxItems(Set mailboxItems) { // sort mailbox items List mailboxItemsSorted = mailboxItems.stream() .filter(e -> !e.isMine()) .collect(Collectors.toList()); mailboxItemsSorted.addAll(mailboxItems.stream() .filter(e -> e.isMine()) .sorted(new MailboxItemComparator()) .collect(Collectors.toList())); // handle mailbox items mailboxItemsSorted.forEach(e -> handleMailboxItem(e)); } private static class MailboxItemComparator implements Comparator { private DecryptedMessageWithPubKeyComparator comparator = new DecryptedMessageWithPubKeyComparator(); @Override public int compare(MailboxItem m1, MailboxItem m2) { return comparator.compare(m1.getDecryptedMessageWithPubKey(), m2.getDecryptedMessageWithPubKey()); } } private void handleMailboxItem(MailboxItem mailboxItem) { String uid = mailboxItem.getUid(); synchronized (mailboxMessageList) { if (!mailboxItemsByUid.containsKey(uid)) { mailboxItemsByUid.put(uid, mailboxItem); mailboxMessageList.add(mailboxItem); log.trace("## handleMailboxItem uid={}\nhash={}", uid, P2PDataStorage.get32ByteHashAsByteArray(mailboxItem.getProtectedMailboxStorageEntry().getProtectedStoragePayload())); requestPersistence(); } } // In case we had the item already stored we still prefer to apply it again to the domain. // Clients need to deal with the case that they get called multiple times with the same mailbox message. // This happens also because peer republish certain trade messages for higher resilience. Those messages // will be different mailbox messages instances but have the same internal content. if (mailboxItem.isMine()) { processMyMailboxItem(mailboxItem, uid); } } private void processMyMailboxItem(MailboxItem mailboxItem, String uid) { DecryptedMessageWithPubKey decryptedMessageWithPubKey = checkNotNull(mailboxItem.getDecryptedMessageWithPubKey()); MailboxMessage mailboxMessage = (MailboxMessage) decryptedMessageWithPubKey.getNetworkEnvelope(); NodeAddress sender = mailboxMessage.getSenderNodeAddress(); log.info("Received a {} mailbox message with uid {} and senderAddress {}", mailboxMessage.getClass().getSimpleName(), uid, sender); decryptedMailboxListeners.forEach(e -> e.onMailboxMessageAdded(decryptedMessageWithPubKey, sender)); if (allServicesInitialized && isBootstrapped) { // After we notified our listeners we remove the data immediately from the network. // In case the client has not been ready it need to take it via getMailBoxMessages. // We do not remove the data from our local map at that moment. This has to be called explicitely from the // client after processing. In case processing fails for some reason we still have the local data which can // be applied after restart, but the network got cleaned from pending mailbox messages. removeMailboxEntryFromNetwork(mailboxItem.getProtectedMailboxStorageEntry()); } else { log.info("We are not bootstrapped yet, so we remove later once the mailBoxMessage got processed."); } } private void addMailboxData(MailboxStoragePayload expirableMailboxStoragePayload, PublicKey receiversPublicKey, SendMailboxMessageListener sendMailboxMessageListener) { if (!isBootstrapped) { throw new NetworkNotReadyException(); } if (networkNode.getAllConnections().isEmpty()) { sendMailboxMessageListener.onFault("There are no P2P network nodes connected. " + "Please check your internet connection."); return; } try { ProtectedMailboxStorageEntry protectedMailboxStorageEntry = p2PDataStorage.getMailboxDataWithSignedSeqNr( expirableMailboxStoragePayload, keyRing.getSignatureKeyPair(), receiversPublicKey); BroadcastHandler.Listener listener = new BroadcastHandler.Listener() { @Override public void onSufficientlyBroadcast(List broadcastRequests) { broadcastRequests.stream() .filter(broadcastRequest -> broadcastRequest.getMessage() instanceof AddDataMessage) .filter(broadcastRequest -> { AddDataMessage addDataMessage = (AddDataMessage) broadcastRequest.getMessage(); return addDataMessage.getProtectedStorageEntry().equals(protectedMailboxStorageEntry); }) .forEach(e -> sendMailboxMessageListener.onStoredInMailbox()); } @Override public void onNotSufficientlyBroadcast(int numOfCompletedBroadcasts, int numOfFailedBroadcast) { sendMailboxMessageListener.onFault("Message was not sufficiently broadcast.\n" + "numOfCompletedBroadcasts: " + numOfCompletedBroadcasts + ".\n" + "numOfFailedBroadcast=" + numOfFailedBroadcast); } }; boolean result = p2PDataStorage.addProtectedStorageEntry(protectedMailboxStorageEntry, networkNode.getNodeAddress(), listener); if (!result) { sendMailboxMessageListener.onFault("Data already exists in our local database"); // This should only fail if there are concurrent calls to addProtectedStorageEntry with the // same ProtectedMailboxStorageEntry. This is an unexpected use case so if it happens we // want to see it, but it is not worth throwing an exception. log.error("Unexpected state: adding mailbox message that already exists."); } } catch (CryptoException e) { log.error("Signing at getMailboxDataWithSignedSeqNr failed."); } } private void removeMailboxEntryFromNetwork(ProtectedMailboxStorageEntry protectedMailboxStorageEntry) { MailboxStoragePayload mailboxStoragePayload = (MailboxStoragePayload) protectedMailboxStorageEntry.getProtectedStoragePayload(); PublicKey receiversPubKey = protectedMailboxStorageEntry.getReceiversPubKey(); try { ProtectedMailboxStorageEntry updatedEntry = p2PDataStorage.getMailboxDataWithSignedSeqNr( mailboxStoragePayload, keyRing.getSignatureKeyPair(), receiversPubKey); P2PDataStorage.ByteArray hashOfPayload = P2PDataStorage.get32ByteHashAsByteArray(mailboxStoragePayload); if (p2PDataStorage.getMap().containsKey(hashOfPayload)) { boolean result = p2PDataStorage.remove(updatedEntry, networkNode.getNodeAddress()); if (result) { log.info("Removed mailboxEntry from network"); } else { log.warn("Removing mailboxEntry from network failed"); } } else { log.info("The mailboxEntry was already removed earlier."); } } catch (CryptoException e) { log.error("Could not remove ProtectedMailboxStorageEntry from network. Error: {}\n", e.toString(), e); } } private void maybeRepublishMailBoxMessages() { // We only do the republishing if option is set (default is false) to avoid that the network gets too much traffic. // 1000 mailbox messages are about 3 MB, so that would cause quite some load if all nodes would do that. // We enable it on one v2 and one v3 seed node so we gain some resilience without causing much load. In // emergency case we can enable it on demand at any node. if (!republishMailboxEntries) { return; } log.info("We will republish our persisted mailbox messages after a delay of {} sec.", REPUBLISH_DELAY_SEC); log.trace("## republishMailBoxMessages mailboxItemsByUid={}", mailboxItemsByUid.keySet()); UserThread.runAfter(() -> { // In addProtectedStorageEntry we break early if we have already received a remove message for that entry. republishInChunks(mailboxItemsByUid.values().stream() .filter(e -> !e.isExpired(clock)) .map(MailboxItem::getProtectedMailboxStorageEntry) .collect(Collectors.toCollection(ArrayDeque::new))); }, REPUBLISH_DELAY_SEC); } // We republish buckets of 50 items which is about 200 kb. With 20 connections at a seed node that results in // 4 MB in total. For 1000 messages it takes 40 min with a 2 min delay. We do that republishing just for // additional resilience and as a backup in case all seed nodes would fail to prevent that mailbox messages would // get lost. A long delay for republishing is preferred over too much network load. private void republishInChunks(Queue queue) { int chunkSize = 50; log.info("Republish a bucket of {} persisted mailbox messages out of {}.", chunkSize, queue.size()); int i = 0; while (!queue.isEmpty() && i < chunkSize) { ProtectedMailboxStorageEntry protectedMailboxStorageEntry = queue.poll(); i++; // Broadcaster will accumulate messages in a BundleOfEnvelopes p2PDataStorage.republishExistingProtectedMailboxStorageEntry(protectedMailboxStorageEntry, networkNode.getNodeAddress(), null); } if (!queue.isEmpty()) { // We delay 2 minutes to not overload the network UserThread.runAfter(() -> republishInChunks(queue), REPUBLISH_DELAY_SEC); } } private void removeMailboxItemFromLocalStore(String uid) { MailboxItem mailboxItem = mailboxItemsByUid.get(uid); mailboxItemsByUid.remove(uid); mailboxMessageList.remove(mailboxItem); log.trace("## removeMailboxItemFromMap uid={}\nhash={}\nmailboxItemsByUid={}", uid, P2PDataStorage.get32ByteHashAsByteArray(mailboxItem.getProtectedMailboxStorageEntry().getProtectedStoragePayload()), mailboxItemsByUid.keySet() ); requestPersistence(); } private void requestPersistence() { persistenceManager.requestPersistence(); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/messaging/DecryptedMailboxListener.java ================================================ /* * 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 . */ package haveno.network.p2p.messaging; import haveno.network.p2p.DecryptedMessageWithPubKey; import haveno.network.p2p.NodeAddress; public interface DecryptedMailboxListener { void onMailboxMessageAdded(DecryptedMessageWithPubKey decryptedMessageWithPubKey, @SuppressWarnings("UnusedParameters") NodeAddress senderNodeAddress); } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/BanFilter.java ================================================ /* * 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 . */ package haveno.network.p2p.network; import haveno.network.p2p.NodeAddress; import java.util.function.Predicate; public interface BanFilter { boolean isPeerBanned(NodeAddress nodeAddress); void setBannedNodePredicate(Predicate isNodeAddressBanned); } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/BridgeAddressProvider.java ================================================ /* * 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 . */ package haveno.network.p2p.network; import org.jetbrains.annotations.Nullable; import java.util.List; public interface BridgeAddressProvider { @Nullable List getBridgeAddresses(); } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/CloseConnectionReason.java ================================================ /* * 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 . */ package haveno.network.p2p.network; public enum CloseConnectionReason { // First block are from different exceptions SOCKET_CLOSED(false, false), RESET(false, false), SOCKET_TIMEOUT(false, false), TERMINATED(false, false), // EOFException CORRUPTED_DATA(false, false), NO_PROTO_BUFFER_DATA(false, false), NO_PROTO_BUFFER_ENV(false, false), UNKNOWN_EXCEPTION(false, false), // Planned APP_SHUT_DOWN(true, true), CLOSE_REQUESTED_BY_PEER(false, true), // send msg SEND_MSG_FAILURE(false, false), SEND_MSG_TIMEOUT(false, false), // maintenance TOO_MANY_CONNECTIONS_OPEN(true, true), TOO_MANY_SEED_NODES_CONNECTED(true, true), UNKNOWN_PEER_ADDRESS(true, true), // illegal requests RULE_VIOLATION(true, false), PEER_BANNED(false, false), INVALID_CLASS_RECEIVED(false, false), MANDATORY_CAPABILITIES_NOT_SUPPORTED(false, false); public final boolean sendCloseMessage; public final boolean isIntended; CloseConnectionReason(boolean sendCloseMessage, boolean isIntended) { this.sendCloseMessage = sendCloseMessage; this.isIntended = isIntended; } @Override public String toString() { return "CloseConnectionReason{" + "sendCloseMessage=" + sendCloseMessage + ", isIntended=" + isIntended + "} " + super.toString(); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/Connection.java ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ package haveno.network.p2p.network; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.util.concurrent.Uninterruptibles; import com.google.inject.Inject; import com.google.protobuf.InvalidProtocolBufferException; import haveno.common.Proto; import haveno.common.ThreadUtils; import haveno.common.app.Capabilities; import haveno.common.app.HasCapabilities; import haveno.common.app.Version; import haveno.common.config.Config; import haveno.common.proto.ProtobufferException; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.proto.network.NetworkProtoResolver; import haveno.common.util.SingleThreadExecutorUtils; import haveno.common.util.Utilities; import haveno.network.p2p.BundleOfEnvelopes; import haveno.network.p2p.CloseConnectionMessage; import haveno.network.p2p.ExtendedDataSizePermission; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.SendersNodeAddressMessage; import haveno.network.p2p.SupportedCapabilitiesMessage; import haveno.network.p2p.peers.keepalive.messages.KeepAliveMessage; import haveno.network.p2p.storage.P2PDataStorage; import haveno.network.p2p.storage.messages.AddDataMessage; import haveno.network.p2p.storage.messages.AddPersistableNetworkPayloadMessage; import haveno.network.p2p.storage.messages.RemoveDataMessage; import haveno.network.p2p.storage.payload.CapabilityRequiringPayload; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.InvalidClassException; import java.io.OptionalDataException; import java.io.StreamCorruptedException; import java.lang.ref.WeakReference; import java.net.Socket; import java.net.SocketException; import java.net.SocketTimeoutException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; import org.jetbrains.annotations.Nullable; /** * Connection is created by the server thread or by sendMessage from NetworkNode. * All handlers are called on User thread. */ @Slf4j public class Connection implements HasCapabilities, Runnable, MessageListener { /////////////////////////////////////////////////////////////////////////////////////////// // Static /////////////////////////////////////////////////////////////////////////////////////////// @Inject @Nullable private static Config config; // Leaving some constants package-private for tests to know limits. private static final int PERMITTED_MESSAGE_SIZE = 200 * 1024; // 200 kb private static final int MAX_PERMITTED_MESSAGE_SIZE = 10 * 1024 * 1024; // 10 MB (425 offers resulted in about 660 kb, mailbox msg will add more to it) offer has usually 2 kb, mailbox 3kb. //TODO decrease limits again after testing private static final int SOCKET_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(240); private static final int SHUTDOWN_TIMEOUT = 100; private static final String THREAD_ID = Connection.class.getSimpleName(); public static final int POSSIBLE_DOS_THRESHOLD = 5; public static int getPermittedMessageSize() { return PERMITTED_MESSAGE_SIZE; } public static int getMaxPermittedMessageSize() { return MAX_PERMITTED_MESSAGE_SIZE; } public static int getShutdownTimeout() { return SHUTDOWN_TIMEOUT; } /////////////////////////////////////////////////////////////////////////////////////////// // Class fields /////////////////////////////////////////////////////////////////////////////////////////// private final Socket socket; private final ConnectionListener connectionListener; @Nullable private final BanFilter banFilter; @Getter private final String uid; private final ExecutorService executorService; @Getter private final Statistic statistic; @Getter private final ConnectionState connectionState; @Getter private final ConnectionStatistics connectionStatistics; // set in init private ProtoOutputStream protoOutputStream; // mutable data, set from other threads but not changed internally. @Getter private Optional peersNodeAddressOptional = Optional.empty(); @Getter private volatile boolean stopped; @Getter private final ObjectProperty peersNodeAddressProperty = new SimpleObjectProperty<>(); private final List messageTimeStamps = new ArrayList<>(); private final CopyOnWriteArraySet messageListeners = new CopyOnWriteArraySet<>(); private volatile long lastSendTimeStamp = 0; // We use a weak reference here to ensure that no connection causes a memory leak in case it get closed without // the shutDown being called. private final CopyOnWriteArraySet> capabilitiesListeners = new CopyOnWriteArraySet<>(); @Getter private RuleViolation ruleViolation; private final ConcurrentHashMap ruleViolations = new ConcurrentHashMap<>(); private final Capabilities capabilities = new Capabilities(); // throttle logs of reported invalid requests private static final long LOG_THROTTLE_INTERVAL_MS = 30000; // throttle logging rule violations and warnings to once every 30 seconds private static long lastLoggedInvalidRequestReportTs = 0; private static int numThrottledInvalidRequestReports = 0; private static long lastLoggedWarningTs = 0; private static int numThrottledWarnings = 0; private static long lastLoggedInfoTs = 0; private static int numThrottledInfos = 0; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// Connection(Socket socket, MessageListener messageListener, ConnectionListener connectionListener, @Nullable NodeAddress peersNodeAddress, NetworkProtoResolver networkProtoResolver, @Nullable BanFilter banFilter) { this.socket = socket; this.connectionListener = connectionListener; this.banFilter = banFilter; this.uid = UUID.randomUUID().toString(); this.executorService = SingleThreadExecutorUtils.getSingleThreadExecutor("Executor service for connection with uid " + uid); statistic = new Statistic(); addMessageListener(messageListener); this.networkProtoResolver = networkProtoResolver; connectionState = new ConnectionState(this); connectionStatistics = new ConnectionStatistics(this, connectionState); init(peersNodeAddress); } private void init(@Nullable NodeAddress peersNodeAddress) { try { socket.setSoTimeout(SOCKET_TIMEOUT); // Need to access first the ObjectOutputStream otherwise the ObjectInputStream would block // See: https://stackoverflow.com/questions/5658089/java-creating-a-new-objectinputstream-blocks/5658109#5658109 // When you construct an ObjectInputStream, in the constructor the class attempts to read a header that // the associated ObjectOutputStream on the other end of the connection has written. // It will not return until that header has been read. protoOutputStream = new ProtoOutputStream(socket.getOutputStream(), statistic); protoInputStream = socket.getInputStream(); // We create a thread for handling inputStream data executorService.submit(this); if (peersNodeAddress != null) { setPeersNodeAddress(peersNodeAddress); if (banFilter != null && banFilter.isPeerBanned(peersNodeAddress)) { reportInvalidRequest(RuleViolation.PEER_BANNED, "We created an outbound connection with a banned peer"); } } ThreadUtils.execute(() -> connectionListener.onConnection(this), THREAD_ID); } catch (Throwable e) { handleException(e); } } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public Capabilities getCapabilities() { return capabilities; } void sendMessage(NetworkEnvelope networkEnvelope) { long ts = System.currentTimeMillis(); log.debug(">> Send networkEnvelope of type: {}", networkEnvelope.getClass().getSimpleName()); if (stopped) { log.debug("called sendMessage but was already stopped"); return; } if (banFilter != null && peersNodeAddressOptional.isPresent() && banFilter.isPeerBanned(peersNodeAddressOptional.get())) { String errorMessage = "We tried to send a message to a banned peer. message=" + networkEnvelope.getClass().getSimpleName(); reportInvalidRequest(RuleViolation.PEER_BANNED, errorMessage); return; } if (!testCapability(networkEnvelope)) { log.debug("Capability for networkEnvelope is required but not supported"); return; } int networkEnvelopeSize = networkEnvelope.toProtoNetworkEnvelope().getSerializedSize(); try { // Throttle outbound network_messages long now = System.currentTimeMillis(); long elapsed = now - lastSendTimeStamp; if (elapsed < getSendMsgThrottleTrigger()) { log.debug("We got 2 sendMessage requests in less than {} ms. We set the thread to sleep " + "for {} ms to avoid flooding our peer. lastSendTimeStamp={}, now={}, elapsed={}, networkEnvelope={}", getSendMsgThrottleTrigger(), getSendMsgThrottleSleep(), lastSendTimeStamp, now, elapsed, networkEnvelope.getClass().getSimpleName()); Thread.sleep(getSendMsgThrottleSleep()); } lastSendTimeStamp = now; if (!stopped) { protoOutputStream.writeEnvelope(networkEnvelope); ThreadUtils.execute(() -> messageListeners.forEach(e -> e.onMessageSent(networkEnvelope, this)), THREAD_ID); ThreadUtils.execute(() -> connectionStatistics.addSendMsgMetrics(System.currentTimeMillis() - ts, networkEnvelopeSize), THREAD_ID); } } catch (Throwable t) { handleException(t); throw new RuntimeException(t); } } public boolean testCapability(NetworkEnvelope networkEnvelope) { if (networkEnvelope instanceof BundleOfEnvelopes) { // We remove elements in the list which fail the capability test BundleOfEnvelopes bundleOfEnvelopes = (BundleOfEnvelopes) networkEnvelope; updateBundleOfEnvelopes(bundleOfEnvelopes); // If the bundle is empty we dont send the networkEnvelope return !bundleOfEnvelopes.getEnvelopes().isEmpty(); } return extractCapabilityRequiringPayload(networkEnvelope) .map(this::testCapability) .orElse(true); } private boolean testCapability(CapabilityRequiringPayload capabilityRequiringPayload) { boolean result = capabilities.containsAll(capabilityRequiringPayload.getRequiredCapabilities()); if (!result) { log.debug("We did not send {} because capabilities are not supported.", capabilityRequiringPayload.getClass().getSimpleName()); } return result; } private void updateBundleOfEnvelopes(BundleOfEnvelopes bundleOfEnvelopes) { List toRemove = bundleOfEnvelopes.getEnvelopes().stream() .filter(networkEnvelope -> !testCapability(networkEnvelope)) .collect(Collectors.toList()); bundleOfEnvelopes.getEnvelopes().removeAll(toRemove); } private Optional extractCapabilityRequiringPayload(Proto proto) { Proto candidate = proto; // Lets check if our networkEnvelope is a wrapped data structure if (proto instanceof AddDataMessage) { candidate = (((AddDataMessage) proto).getProtectedStorageEntry()).getProtectedStoragePayload(); } else if (proto instanceof RemoveDataMessage) { candidate = (((RemoveDataMessage) proto).getProtectedStorageEntry()).getProtectedStoragePayload(); } else if (proto instanceof AddPersistableNetworkPayloadMessage) { candidate = (((AddPersistableNetworkPayloadMessage) proto).getPersistableNetworkPayload()); } if (candidate instanceof CapabilityRequiringPayload) { return Optional.of((CapabilityRequiringPayload) candidate); } return Optional.empty(); } public void addMessageListener(MessageListener messageListener) { boolean isNewEntry = messageListeners.add(messageListener); if (!isNewEntry) log.warn("Try to add a messageListener which was already added."); } public void removeMessageListener(MessageListener messageListener) { boolean contained = messageListeners.remove(messageListener); if (!contained) log.debug("Try to remove a messageListener which was never added.\n\t" + "That might happen because of async behaviour of CopyOnWriteArraySet"); } public void addWeakCapabilitiesListener(SupportedCapabilitiesListener listener) { capabilitiesListeners.add(new WeakReference<>(listener)); } private boolean violatesThrottleLimit() { long now = System.currentTimeMillis(); messageTimeStamps.add(now); // clean list while (messageTimeStamps.size() > getMsgThrottlePer10Sec()) messageTimeStamps.remove(0); return violatesThrottleLimit(now, 1, getMsgThrottlePerSec()) || violatesThrottleLimit(now, 10, getMsgThrottlePer10Sec()); } private int getMsgThrottlePerSec() { return config != null ? config.msgThrottlePerSec : 200; } private int getMsgThrottlePer10Sec() { return config != null ? config.msgThrottlePer10Sec : 1000; } private int getSendMsgThrottleSleep() { return config != null ? config.sendMsgThrottleSleep : 50; } private int getSendMsgThrottleTrigger() { return config != null ? config.sendMsgThrottleTrigger : 20; } private boolean violatesThrottleLimit(long now, int seconds, int messageCountLimit) { if (messageTimeStamps.size() >= messageCountLimit) { // find the entry in the message timestamp history which determines whether we overshot the limit or not long compareValue = messageTimeStamps.get(messageTimeStamps.size() - messageCountLimit); // if duration < seconds sec we received too much network_messages if (now - compareValue < TimeUnit.SECONDS.toMillis(seconds)) { log.error("violatesThrottleLimit {}/{} second(s)", messageCountLimit, seconds); return true; } } return false; } /////////////////////////////////////////////////////////////////////////////////////////// // MessageListener implementation /////////////////////////////////////////////////////////////////////////////////////////// // Only receive non - CloseConnectionMessage network_messages @Override public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { checkArgument(connection.equals(this)); if (networkEnvelope instanceof BundleOfEnvelopes) { onBundleOfEnvelopes((BundleOfEnvelopes) networkEnvelope, connection); } else { ThreadUtils.execute(() -> messageListeners.forEach(e -> e.onMessage(networkEnvelope, connection)), THREAD_ID); } } private void onBundleOfEnvelopes(BundleOfEnvelopes bundleOfEnvelopes, Connection connection) { Map> itemsByHash = new HashMap<>(); Set envelopesToProcess = new HashSet<>(); List networkEnvelopes = bundleOfEnvelopes.getEnvelopes(); for (NetworkEnvelope networkEnvelope : networkEnvelopes) { // If SendersNodeAddressMessage we do some verifications and apply if successful, otherwise we return false. if (networkEnvelope instanceof SendersNodeAddressMessage) { boolean isValid = processSendersNodeAddressMessage((SendersNodeAddressMessage) networkEnvelope); if (!isValid) { throttleWarn("Received an invalid " + networkEnvelope.getClass().getSimpleName() + " at processing BundleOfEnvelopes"); continue; } } if (networkEnvelope instanceof AddPersistableNetworkPayloadMessage) { PersistableNetworkPayload persistableNetworkPayload = ((AddPersistableNetworkPayloadMessage) networkEnvelope).getPersistableNetworkPayload(); byte[] hash = persistableNetworkPayload.getHash(); String itemName = persistableNetworkPayload.getClass().getSimpleName(); P2PDataStorage.ByteArray byteArray = new P2PDataStorage.ByteArray(hash); itemsByHash.putIfAbsent(byteArray, new HashSet<>()); Set envelopesByHash = itemsByHash.get(byteArray); if (!envelopesByHash.contains(networkEnvelope)) { envelopesByHash.add(networkEnvelope); envelopesToProcess.add(networkEnvelope); } else { log.debug("We got duplicated items for {}. We ignore the duplicates. Hash: {}", itemName, Utilities.encodeToHex(hash)); } } else { envelopesToProcess.add(networkEnvelope); } } envelopesToProcess.forEach(envelope -> ThreadUtils.execute(() -> { messageListeners.forEach(listener -> listener.onMessage(envelope, connection)); }, THREAD_ID)); } /////////////////////////////////////////////////////////////////////////////////////////// // Setters /////////////////////////////////////////////////////////////////////////////////////////// private void setPeersNodeAddress(NodeAddress peerNodeAddress) { checkNotNull(peerNodeAddress, "peerAddress must not be null"); peersNodeAddressOptional = Optional.of(peerNodeAddress); if (this instanceof InboundConnection) { log.debug("\n\n############################################################\n" + "We got the peers node address set.\n" + "peersNodeAddress= " + peerNodeAddress.getFullAddress() + "\nconnection.uid=" + getUid() + "\n############################################################\n"); } peersNodeAddressProperty.set(peerNodeAddress); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// public boolean hasPeersNodeAddress() { return peersNodeAddressOptional.isPresent(); } /////////////////////////////////////////////////////////////////////////////////////////// // ShutDown /////////////////////////////////////////////////////////////////////////////////////////// public void shutDown(CloseConnectionReason closeConnectionReason) { shutDown(closeConnectionReason, null); } public void shutDown(CloseConnectionReason closeConnectionReason, @Nullable Runnable shutDownCompleteHandler) { log.debug("shutDown: peersNodeAddressOptional={}, closeConnectionReason={}", peersNodeAddressOptional, closeConnectionReason); connectionState.shutDown(); if (!stopped) { String peersNodeAddress = peersNodeAddressOptional.map(NodeAddress::toString).orElse("null"); log.debug("\n\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n" + "ShutDown connection:" + "\npeersNodeAddress=" + peersNodeAddress + "\ncloseConnectionReason=" + closeConnectionReason + "\nuid=" + uid + "\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n"); if (closeConnectionReason.sendCloseMessage) { new Thread(() -> { try { String reason = closeConnectionReason == CloseConnectionReason.RULE_VIOLATION ? getRuleViolation().name() : closeConnectionReason.name(); sendMessage(new CloseConnectionMessage(reason)); stopped = true; Uninterruptibles.sleepUninterruptibly(200, TimeUnit.MILLISECONDS); } catch (Throwable t) { log.error(ExceptionUtils.getStackTrace(t)); } finally { stopped = true; ThreadUtils.execute(() -> doShutDown(closeConnectionReason, shutDownCompleteHandler), THREAD_ID); } }, "Connection:SendCloseConnectionMessage-" + this.uid).start(); } else { stopped = true; doShutDown(closeConnectionReason, shutDownCompleteHandler); } } else { //TODO find out why we get called that log.debug("stopped was already at shutDown call"); ThreadUtils.execute(() -> doShutDown(closeConnectionReason, shutDownCompleteHandler), THREAD_ID); } } private void doShutDown(CloseConnectionReason closeConnectionReason, @Nullable Runnable shutDownCompleteHandler) { ThreadUtils.execute(() -> connectionListener.onDisconnect(closeConnectionReason, this), THREAD_ID); try { protoOutputStream.onConnectionShutdown(); socket.close(); } catch (SocketException e) { log.trace("SocketException at shutdown might be expected {}", e.getMessage()); } catch (IOException e) { log.error("Exception at shutdown. {}\n", e.getMessage(), e); } finally { capabilitiesListeners.clear(); try { protoInputStream.close(); } catch (IOException e) { log.error(ExceptionUtils.getStackTrace(e)); } Utilities.shutdownAndAwaitTermination(executorService, SHUTDOWN_TIMEOUT, TimeUnit.MILLISECONDS); log.debug("Connection shutdown complete {}", this); if (shutDownCompleteHandler != null) ThreadUtils.execute(shutDownCompleteHandler, THREAD_ID); } } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Connection)) return false; Connection that = (Connection) o; return uid.equals(that.uid); } @Override public int hashCode() { return uid.hashCode(); } @Override public String toString() { return "Connection{" + "peerAddress=" + peersNodeAddressOptional + ", connectionState=" + connectionState + ", connectionType=" + (this instanceof InboundConnection ? "InboundConnection" : "OutboundConnection") + ", uid='" + uid + '\'' + '}'; } public String printDetails() { String portInfo; if (socket.getLocalPort() == 0) portInfo = "port=" + socket.getPort(); else portInfo = "localPort=" + socket.getLocalPort() + "/port=" + socket.getPort(); return "Connection{" + "peerAddress=" + peersNodeAddressOptional + ", connectionState=" + connectionState + ", portInfo=" + portInfo + ", uid='" + uid + '\'' + ", ruleViolation=" + ruleViolation + ", ruleViolations=" + ruleViolations + ", supportedCapabilities=" + capabilities + ", stopped=" + stopped + '}'; } /////////////////////////////////////////////////////////////////////////////////////////// // SharedSpace /////////////////////////////////////////////////////////////////////////////////////////// /** * Holds all shared data between Connection and InputHandler * Runs in same thread as Connection */ public boolean reportInvalidRequest(RuleViolation ruleViolation, String errorMessage) { return Connection.reportInvalidRequest(this, ruleViolation, errorMessage); } private static synchronized boolean reportInvalidRequest(Connection connection, RuleViolation ruleViolation, String errorMessage) { // determine if report should be logged to avoid spamming the logs boolean logReport = System.currentTimeMillis() - lastLoggedInvalidRequestReportTs > LOG_THROTTLE_INTERVAL_MS; // count the number of unlogged reports since last log entry if (!logReport) numThrottledInvalidRequestReports++; // handle report if (logReport) log.warn("We got reported the ruleViolation {} at connection with address={}, uid={}, errorMessage={}", ruleViolation, connection.getPeersNodeAddressProperty(), connection.getUid(), errorMessage); int numRuleViolations; numRuleViolations = connection.ruleViolations.getOrDefault(ruleViolation, 0); numRuleViolations++; connection.ruleViolations.put(ruleViolation, numRuleViolations); if (numRuleViolations >= ruleViolation.maxTolerance) { if (logReport) log.warn("We close connection as we received too many corrupt requests. " + "ruleViolations={} " + "connection with address {} and uid {}", connection.ruleViolations, connection.peersNodeAddressProperty, connection.uid); connection.ruleViolation = ruleViolation; if (ruleViolation == RuleViolation.PEER_BANNED) { if (logReport) log.debug("We close connection due RuleViolation.PEER_BANNED. peersNodeAddress={}", connection.getPeersNodeAddressOptional()); connection.shutDown(CloseConnectionReason.PEER_BANNED); } else if (ruleViolation == RuleViolation.INVALID_CLASS) { if (logReport) log.warn("We close connection due RuleViolation.INVALID_CLASS"); connection.shutDown(CloseConnectionReason.INVALID_CLASS_RECEIVED); } else { if (logReport) log.warn("We close connection due RuleViolation.RULE_VIOLATION"); connection.shutDown(CloseConnectionReason.RULE_VIOLATION); } resetReportedInvalidRequestsThrottle(logReport); return true; } else { resetReportedInvalidRequestsThrottle(logReport); return false; } } private static synchronized void resetReportedInvalidRequestsThrottle(boolean logReport) { if (logReport) { if (numThrottledInvalidRequestReports > 0) log.warn("We received {} throttled reports of invalid requests since the last log entry" + (numThrottledInvalidRequestReports >= POSSIBLE_DOS_THRESHOLD ? ". Possible DoS attack detected" : ""), numThrottledInvalidRequestReports); numThrottledInvalidRequestReports = 0; lastLoggedInvalidRequestReportTs = System.currentTimeMillis(); } } private void handleException(Throwable e) { CloseConnectionReason closeConnectionReason; // silent fail if we are shutdown if (stopped) return; if (e instanceof SocketException) { if (socket.isClosed()) closeConnectionReason = CloseConnectionReason.SOCKET_CLOSED; else closeConnectionReason = CloseConnectionReason.RESET; throttleWarn("SocketException (expected if connection lost). closeConnectionReason=" + closeConnectionReason + "; connection=" + this); } else if (e instanceof SocketTimeoutException || e instanceof TimeoutException) { closeConnectionReason = CloseConnectionReason.SOCKET_TIMEOUT; throttleInfo("Shut down caused by exception " + e.getMessage() + " on connection=" + this); } else if (e instanceof EOFException) { closeConnectionReason = CloseConnectionReason.TERMINATED; throttleWarn("Shut down caused by exception " + e.getMessage() + " on connection=" + this); } else if (e instanceof OptionalDataException || e instanceof StreamCorruptedException) { closeConnectionReason = CloseConnectionReason.CORRUPTED_DATA; throttleWarn("Shut down caused by exception " + e.getMessage() + " on connection=" + this); } else { // TODO sometimes we get StreamCorruptedException, OptionalDataException, IllegalStateException closeConnectionReason = CloseConnectionReason.UNKNOWN_EXCEPTION; throttleWarn("Unknown reason for exception at socket: " + socket.toString() + "\n\t" + "peer=" + this.peersNodeAddressOptional + "\n\t" + "Exception=" + e.toString()); } shutDown(closeConnectionReason); } private boolean processSendersNodeAddressMessage(SendersNodeAddressMessage sendersNodeAddressMessage) { NodeAddress senderNodeAddress = sendersNodeAddressMessage.getSenderNodeAddress(); checkNotNull(senderNodeAddress, "senderNodeAddress must not be null at SendersNodeAddressMessage " + sendersNodeAddressMessage.getClass().getSimpleName()); Optional existingAddressOptional = getPeersNodeAddressOptional(); if (existingAddressOptional.isPresent()) { // If we have already the peers address we check again if it matches our stored one checkArgument(existingAddressOptional.get().equals(senderNodeAddress), "senderNodeAddress not matching connections peer address.\n\t" + "message=" + sendersNodeAddressMessage); } else { setPeersNodeAddress(senderNodeAddress); } if (banFilter != null && banFilter.isPeerBanned(senderNodeAddress)) { String errorMessage = "We got a message from a banned peer. message=" + sendersNodeAddressMessage.getClass().getSimpleName(); reportInvalidRequest(RuleViolation.PEER_BANNED, errorMessage); return false; } return true; } /////////////////////////////////////////////////////////////////////////////////////////// // InputHandler /////////////////////////////////////////////////////////////////////////////////////////// // Runs in same thread as Connection, receives a message, performs several checks on it // (including throttling limits, validity and statistics) // and delivers it to the message listener given in the constructor. private InputStream protoInputStream; private final NetworkProtoResolver networkProtoResolver; private long lastReadTimeStamp; private boolean threadNameSet; @Override public void run() { try { Thread.currentThread().setName("InputHandler-" + Utilities.toTruncatedString(uid, 15)); while (!stopped && !Thread.currentThread().isInterrupted()) { if (!threadNameSet && getPeersNodeAddressOptional().isPresent()) { Thread.currentThread().setName("InputHandler-" + Utilities.toTruncatedString(getPeersNodeAddressOptional().get().getFullAddress(), 15)); threadNameSet = true; } try { if (socket != null && socket.isClosed()) { throttleWarn("Socket is null or closed socket=" + socket); shutDown(CloseConnectionReason.SOCKET_CLOSED); return; } // Blocking read from the inputStream protobuf.NetworkEnvelope proto = protobuf.NetworkEnvelope.parseDelimitedFrom(protoInputStream); long ts = System.currentTimeMillis(); if (socket != null && socket.isClosed()) { throttleWarn("Socket is null or closed socket=" + socket); shutDown(CloseConnectionReason.SOCKET_CLOSED); return; } if (proto == null) { if (stopped) { return; } if (protoInputStream.read() == -1) { throttleWarn("proto is null because protoInputStream.read()=-1 (EOF). That is expected if client got stopped without proper shutdown."); } else { throttleWarn("proto is null. protoInputStream.read()=" + protoInputStream.read()); } shutDown(CloseConnectionReason.NO_PROTO_BUFFER_ENV); return; } if (banFilter != null && peersNodeAddressOptional.isPresent() && banFilter.isPeerBanned(peersNodeAddressOptional.get())) { String errorMessage = "We got a message from a banned peer. proto=" + Utilities.toTruncatedString(proto); reportInvalidRequest(RuleViolation.PEER_BANNED, errorMessage); return; } // Throttle inbound network messages long now = System.currentTimeMillis(); long elapsed = now - lastReadTimeStamp; if (elapsed < 10) { log.debug("We got 2 network messages received in less than 10 ms. We set the thread to sleep " + "for 20 ms to avoid getting flooded by our peer. lastReadTimeStamp={}, now={}, elapsed={}", lastReadTimeStamp, now, elapsed); Thread.sleep(20); } NetworkEnvelope networkEnvelope = networkProtoResolver.fromProto(proto); lastReadTimeStamp = now; log.debug("<< Received networkEnvelope of type: {}", networkEnvelope.getClass().getSimpleName()); int size = proto.getSerializedSize(); // We want to track the size of each object even if it is invalid data statistic.addReceivedBytes(size); // We want to track the network_messages also before the checks, so do it early... statistic.addReceivedMessage(networkEnvelope); // First we check the size boolean exceeds; if (networkEnvelope instanceof ExtendedDataSizePermission) { exceeds = size > MAX_PERMITTED_MESSAGE_SIZE; } else { exceeds = size > PERMITTED_MESSAGE_SIZE; } if (networkEnvelope instanceof AddPersistableNetworkPayloadMessage && !((AddPersistableNetworkPayloadMessage) networkEnvelope).getPersistableNetworkPayload().verifyHashSize()) { String errorMessage = "PersistableNetworkPayload.verifyHashSize failed. hashSize=" + ((AddPersistableNetworkPayloadMessage) networkEnvelope).getPersistableNetworkPayload().getHash().length + "; object=" + Utilities.toTruncatedString(proto); if (reportInvalidRequest(RuleViolation.MAX_MSG_SIZE_EXCEEDED, errorMessage)) return; } if (exceeds) { String errorMessage = "size > MAX_MSG_SIZE. size=" + size + "; object=" + Utilities.toTruncatedString(proto); if (reportInvalidRequest(RuleViolation.MAX_MSG_SIZE_EXCEEDED, errorMessage)) return; } if (violatesThrottleLimit() && reportInvalidRequest(RuleViolation.THROTTLE_LIMIT_EXCEEDED, "Violates throttle limit")) return; // Check P2P network ID String errorMessage = "RuleViolation.WRONG_NETWORK_ID. version of message=" + proto.getMessageVersion() + ", app version=" + Version.getP2PMessageVersion() + ", proto.toTruncatedString=" + Utilities.toTruncatedString(proto.toString()); if (!proto.getMessageVersion().equals(Version.getP2PMessageVersion()) && reportInvalidRequest(RuleViolation.WRONG_NETWORK_ID, errorMessage)) { return; } boolean causedShutDown = maybeHandleSupportedCapabilitiesMessage(networkEnvelope); if (causedShutDown) { return; } if (networkEnvelope instanceof CloseConnectionMessage) { // If we get a CloseConnectionMessage we shut down log.debug("CloseConnectionMessage received. Reason={}\n\t" + "connection={}", proto.getCloseConnectionMessage().getReason(), this); if (CloseConnectionReason.PEER_BANNED.name().equals(proto.getCloseConnectionMessage().getReason())) { log.warn("We got shut down because we are banned by the other peer. " + "(InputHandler.run CloseConnectionMessage). Peer: {}", getPeersNodeAddressOptional()); } shutDown(CloseConnectionReason.CLOSE_REQUESTED_BY_PEER); return; } else if (!stopped) { // We don't want to get the activity ts updated by ping/pong msg if (!(networkEnvelope instanceof KeepAliveMessage)) statistic.updateLastActivityTimestamp(); // If SendersNodeAddressMessage we do some verifications and apply if successful, // otherwise we return false. if (networkEnvelope instanceof SendersNodeAddressMessage) { boolean isValid = processSendersNodeAddressMessage((SendersNodeAddressMessage) networkEnvelope); if (!isValid) { return; } } if (!(networkEnvelope instanceof SendersNodeAddressMessage) && peersNodeAddressOptional.isEmpty()) { log.info("We got a {} from a peer with yet unknown address on connection with uid={}", networkEnvelope.getClass().getSimpleName(), uid); } onMessage(networkEnvelope, this); ThreadUtils.execute(() -> connectionStatistics.addReceivedMsgMetrics(System.currentTimeMillis() - ts, size), THREAD_ID); } } catch (InvalidClassException e) { reportInvalidRequest(RuleViolation.INVALID_CLASS, e.getMessage()); } catch (ProtobufferException | NoClassDefFoundError | InvalidProtocolBufferException e) { reportInvalidRequest(RuleViolation.INVALID_DATA_TYPE, e.getMessage()); } catch (Throwable t) { handleException(t); } } } catch (Throwable t) { handleException(t); } } public boolean maybeHandleSupportedCapabilitiesMessage(NetworkEnvelope networkEnvelope) { if (!(networkEnvelope instanceof SupportedCapabilitiesMessage)) { return false; } Capabilities supportedCapabilities = ((SupportedCapabilitiesMessage) networkEnvelope).getSupportedCapabilities(); if (supportedCapabilities == null || supportedCapabilities.isEmpty()) { return false; } if (this.capabilities.equals(supportedCapabilities)) { return false; } if (!Capabilities.hasMandatoryCapability(supportedCapabilities)) { log.info("We close a connection because of " + "CloseConnectionReason.MANDATORY_CAPABILITIES_NOT_SUPPORTED " + "to node {}. Capabilities of old node: {}, " + "networkEnvelope class name={}", getSenderNodeAddressAsString(networkEnvelope), supportedCapabilities.prettyPrint(), networkEnvelope.getClass().getSimpleName()); shutDown(CloseConnectionReason.MANDATORY_CAPABILITIES_NOT_SUPPORTED); return true; } this.capabilities.set(supportedCapabilities); capabilitiesListeners.forEach(weakListener -> { SupportedCapabilitiesListener supportedCapabilitiesListener = weakListener.get(); if (supportedCapabilitiesListener != null) { ThreadUtils.execute(() -> supportedCapabilitiesListener.onChanged(supportedCapabilities), THREAD_ID); } }); return false; } @Nullable private NodeAddress getSenderNodeAddress(NetworkEnvelope networkEnvelope) { return getPeersNodeAddressOptional().orElse( networkEnvelope instanceof SendersNodeAddressMessage ? ((SendersNodeAddressMessage) networkEnvelope).getSenderNodeAddress() : null); } private String getSenderNodeAddressAsString(NetworkEnvelope networkEnvelope) { NodeAddress nodeAddress = getSenderNodeAddress(networkEnvelope); return nodeAddress == null ? "null" : nodeAddress.getFullAddress(); } private synchronized void throttleWarn(String msg) { boolean doLog = System.currentTimeMillis() - lastLoggedWarningTs > LOG_THROTTLE_INTERVAL_MS; if (doLog) { log.warn(msg); if (numThrottledWarnings > 0) log.warn("We received {} throttled warnings since the last log entry" + (numThrottledWarnings >= POSSIBLE_DOS_THRESHOLD ? ". Possible DoS attack detected" : ""), numThrottledWarnings); numThrottledWarnings = 0; lastLoggedWarningTs = System.currentTimeMillis(); } else { numThrottledWarnings++; } } private synchronized void throttleInfo(String msg) { boolean doLog = System.currentTimeMillis() - lastLoggedInfoTs > LOG_THROTTLE_INTERVAL_MS; if (doLog) { log.info(msg); if (numThrottledInfos > 0) log.warn("We received {} throttled info logs since the last log entry" + (numThrottledInfos >= POSSIBLE_DOS_THRESHOLD ? ". Possible DoS attack detected" : ""), numThrottledInfos); numThrottledInfos = 0; lastLoggedInfoTs = System.currentTimeMillis(); } else { numThrottledInfos++; } } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/ConnectionListener.java ================================================ /* * 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 . */ package haveno.network.p2p.network; public interface ConnectionListener { void onConnection(Connection connection); void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection); } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/ConnectionState.java ================================================ /* * 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 . */ package haveno.network.p2p.network; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.proto.network.NetworkEnvelope; import haveno.network.p2p.BundleOfEnvelopes; import haveno.network.p2p.InitialDataRequest; import haveno.network.p2p.InitialDataResponse; import haveno.network.p2p.PrefixedSealedAndSignedMessage; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.TimeUnit; /** * Holds state of connection. Data is applied from message handlers which are called on UserThread, so that class * is in a single threaded context. */ @Slf4j public class ConnectionState implements MessageListener { // We protect the INITIAL_DATA_EXCHANGE PeerType for max. 4 minutes in case not all expected initialDataRequests // and initialDataResponses have not been all sent/received. In case the PeerManager need to close connections // if it exceeds its limits the connectionCreationTimeStamp and lastInitialDataExchangeMessageTimeStamp can be // used to set priorities for closing connections. private static final long PEER_RESET_TIMER_DELAY_SEC = TimeUnit.MINUTES.toSeconds(4); private static final long COMPLETED_TIMER_DELAY_SEC = 10; // Number of expected requests in standard case. Can be different according to network conditions. @Setter private static int expectedRequests = 6; // We have 2 GetDataResponses and 3 GetHashResponses. If node is a lite node it also has a GetBlocksResponse if // blocks are missing. private static final int MIN_EXPECTED_RESPONSES = 5; private static int expectedInitialDataResponses = MIN_EXPECTED_RESPONSES; // If app runs in LiteNode mode there is one more expected request for the getBlocks request, so we increment standard value. public static void incrementExpectedInitialDataResponses() { expectedInitialDataResponses += 1; } private final Connection connection; @Getter private PeerType peerType = PeerType.PEER; @Getter private int numInitialDataRequests = 0; @Getter private int numInitialDataResponses = 0; @Getter private long lastInitialDataMsgTimeStamp; @Setter @Getter private boolean isSeedNode; private Timer peerTypeResetDueTimeoutTimer, initialDataExchangeCompletedTimer; public ConnectionState(Connection connection) { this.connection = connection; connection.addMessageListener(this); } public void shutDown() { connection.removeMessageListener(this); stopTimer(); } @Override public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { if (networkEnvelope instanceof BundleOfEnvelopes) { ((BundleOfEnvelopes) networkEnvelope).getEnvelopes().forEach(this::onMessageSentOrReceived); } else { onMessageSentOrReceived(networkEnvelope); } } @Override public void onMessageSent(NetworkEnvelope networkEnvelope, Connection connection) { if (networkEnvelope instanceof BundleOfEnvelopes) { ((BundleOfEnvelopes) networkEnvelope).getEnvelopes().forEach(this::onMessageSentOrReceived); } else { onMessageSentOrReceived(networkEnvelope); } } private void onMessageSentOrReceived(NetworkEnvelope networkEnvelope) { if (networkEnvelope instanceof InitialDataRequest) { numInitialDataRequests++; onInitialDataExchange(); } else if (networkEnvelope instanceof InitialDataResponse) { numInitialDataResponses++; onInitialDataExchange(); } else if (networkEnvelope instanceof PrefixedSealedAndSignedMessage && connection.getPeersNodeAddressOptional().isPresent()) { peerType = PeerType.DIRECT_MSG_PEER; } } private void onInitialDataExchange() { // If we have a higher prio type we do not handle it if (peerType == PeerType.DIRECT_MSG_PEER) { stopTimer(); return; } peerType = PeerType.INITIAL_DATA_EXCHANGE; lastInitialDataMsgTimeStamp = System.currentTimeMillis(); maybeResetInitialDataExchangeType(); if (peerTypeResetDueTimeoutTimer == null) { peerTypeResetDueTimeoutTimer = UserThread.runAfter(this::resetInitialDataExchangeType, PEER_RESET_TIMER_DELAY_SEC); } } private void maybeResetInitialDataExchangeType() { if (numInitialDataResponses >= expectedInitialDataResponses) { // We have received the expected messages from initial data requests. We delay a bit the reset // to give time for processing the response and more tolerance to edge cases where we expect more responses. // Reset to PEER does not mean disconnection as well, but just that this connection has lower priority and // runs higher risk for getting disconnected. if (initialDataExchangeCompletedTimer == null) { initialDataExchangeCompletedTimer = UserThread.runAfter(this::resetInitialDataExchangeType, COMPLETED_TIMER_DELAY_SEC); } } } private void resetInitialDataExchangeType() { // If we have a higher prio type we do not handle it if (peerType == PeerType.DIRECT_MSG_PEER) { stopTimer(); return; } stopTimer(); peerType = PeerType.PEER; log.info("We have changed the peerType from INITIAL_DATA_EXCHANGE to PEER as we have received all " + "expected initial data responses at connection with peer {}/{}.", connection.getPeersNodeAddressOptional(), connection.getUid()); } private void stopTimer() { if (peerTypeResetDueTimeoutTimer != null) { peerTypeResetDueTimeoutTimer.stop(); peerTypeResetDueTimeoutTimer = null; } if (initialDataExchangeCompletedTimer != null) { initialDataExchangeCompletedTimer.stop(); initialDataExchangeCompletedTimer = null; } } @Override public String toString() { return "ConnectionState{" + ",\n peerType=" + peerType + ",\n numInitialDataRequests=" + numInitialDataRequests + ",\n numInitialDataResponses=" + numInitialDataResponses + ",\n lastInitialDataMsgTimeStamp=" + lastInitialDataMsgTimeStamp + ",\n isSeedNode=" + isSeedNode + ",\n expectedInitialDataResponses=" + expectedInitialDataResponses + "\n}"; } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/ConnectionStatistics.java ================================================ /* * 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 . */ package haveno.network.p2p.network; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.util.Utilities; import haveno.network.p2p.BundleOfEnvelopes; import haveno.network.p2p.InitialDataRequest; import haveno.network.p2p.InitialDataResponse; import haveno.network.p2p.NodeAddress; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; @Slf4j public class ConnectionStatistics implements MessageListener { private final Connection connection; private final ConnectionState connectionState; private final Map sentDataMap = new HashMap<>(); private final Map receivedDataMap = new HashMap<>(); private final Map rrtMap = new HashMap<>(); @Getter private final long connectionCreationTimeStamp; @Getter private long lastMessageTimestamp; @Getter private long timeOnSendMsg = 0; @Getter private long timeOnReceivedMsg = 0; @Getter private int sentBytes = 0; @Getter private int receivedBytes = 0; public ConnectionStatistics(Connection connection, ConnectionState connectionState) { this.connection = connection; this.connectionState = connectionState; connection.addMessageListener(this); connectionCreationTimeStamp = System.currentTimeMillis(); } public void shutDown() { connection.removeMessageListener(this); } public String getInfo() { String ls = System.lineSeparator(); long now = System.currentTimeMillis(); String conInstance = connection instanceof InboundConnection ? "Inbound" : "Outbound"; String age = Utilities.formatDurationAsWords(now - connectionCreationTimeStamp); String lastMsg = Utilities.formatDurationAsWords(now - lastMessageTimestamp); String peer = connection.getPeersNodeAddressOptional() .map(NodeAddress::getFullAddress) .orElse("[address not known yet]"); // For seeds its processing time, for peers rrt String rrt = rrtMap.entrySet().stream() .map(e -> { long value = e.getValue(); // Value is current milli as long we don't have the response if (value < connectionCreationTimeStamp) { String key = e.getKey().replace("Request", "Request/Response"); return key + ": " + Utilities.formatDurationAsWords(value); } else { // we don't want to show pending requests return e.getKey() + " awaiting response... "; } }) .collect(Collectors.toList()) .toString(); if (rrt.equals("[]")) { rrt = ""; } else { rrt = "Time for response: " + rrt + ls; } boolean seedNode = connectionState.isSeedNode(); return String.format( "Age: %s" + ls + "Peer: %s%s " + ls + "Type: %s " + ls + "Direction: %s" + ls + "UID: %s" + ls + "Time since last message: %s" + ls + "%s" + "Sent data: %s; %s" + ls + "Received data: %s; %s" + ls + "CPU time spent on sending messages: %s" + ls + "CPU time spent on receiving messages: %s", age, seedNode ? "[Seed node] " : "", peer, connectionState.getPeerType().name(), conInstance, connection.getUid(), lastMsg, rrt, Utilities.readableFileSize(sentBytes), sentDataMap.toString(), Utilities.readableFileSize(receivedBytes), receivedDataMap.toString(), Utilities.formatDurationAsWords(timeOnSendMsg), Utilities.formatDurationAsWords(timeOnReceivedMsg)); } @Override public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { lastMessageTimestamp = System.currentTimeMillis(); if (networkEnvelope instanceof BundleOfEnvelopes) { ((BundleOfEnvelopes) networkEnvelope).getEnvelopes().forEach(e -> addToMap(e, receivedDataMap)); // We want to track also number of BundleOfEnvelopes addToMap(networkEnvelope, receivedDataMap); } else { addToMap(networkEnvelope, receivedDataMap); } } @Override public void onMessageSent(NetworkEnvelope networkEnvelope, Connection connection) { lastMessageTimestamp = System.currentTimeMillis(); if (networkEnvelope instanceof BundleOfEnvelopes) { ((BundleOfEnvelopes) networkEnvelope).getEnvelopes().forEach(e -> addToMap(e, sentDataMap)); // We want to track also number of BundleOfEnvelopes addToMap(networkEnvelope, sentDataMap); } else { addToMap(networkEnvelope, sentDataMap); } } private void addToMap(NetworkEnvelope networkEnvelope, Map map) { String key = networkEnvelope.getClass().getSimpleName(); map.putIfAbsent(key, 0); map.put(key, map.get(key) + 1); if (networkEnvelope instanceof InitialDataRequest) { rrtMap.putIfAbsent(key, System.currentTimeMillis()); } else if (networkEnvelope instanceof InitialDataResponse) { String associatedRequest = ((InitialDataResponse) networkEnvelope).associatedRequest().getSimpleName(); if (rrtMap.containsKey(associatedRequest)) { rrtMap.put(associatedRequest, System.currentTimeMillis() - rrtMap.get(associatedRequest)); } } } public void addSendMsgMetrics(long timeSpent, int bytes) { this.timeOnSendMsg += timeSpent; this.sentBytes += bytes; } public void addReceivedMsgMetrics(long timeSpent, int bytes) { this.timeOnReceivedMsg += timeSpent; this.receivedBytes += bytes; } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/DefaultPluggableTransports.java ================================================ /* * 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 . */ package haveno.network.p2p.network; import java.util.Arrays; import java.util.List; // Taken from Tor Browser's bundled default entries: Browser/TorBrowser/Data/Browser/profile.default/preferences/extension-overrides.js public class DefaultPluggableTransports { public static final List OBFS_3 = Arrays.asList( "obfs3 83.212.101.3:80 A09D536DD1752D542E1FBB3C9CE4449D51298239", "obfs3 169.229.59.74:31493 AF9F66B7B04F8FF6F32D455F05135250A16543C9", "obfs3 169.229.59.75:46328 AF9F66B7B04F8FF6F32D455F05135250A16543C9", "obfs3 109.105.109.163:38980 1E05F577A0EC0213F971D81BF4D86A9E4E8229ED", "obfs3 109.105.109.163:47779 4C331FA9B3D1D6D8FB0D8FBBF0C259C360D97E6A"); public static final List OBFS_4 = Arrays.asList( "obfs4 154.35.22.10:15937 8FB9F4319E89E5C6223052AA525A192AFBC85D55 cert=GGGS1TX4R81m3r0HBl79wKy1OtPPNR2CZUIrHjkRg65Vc2VR8fOyo64f9kmT1UAFG7j0HQ iat-mode=0", "obfs4 192.99.11.54:443 7B126FAB960E5AC6A629C729434FF84FB5074EC2 cert=VW5f8+IBUWpPFxF+rsiVy2wXkyTQG7vEd+rHeN2jV5LIDNu8wMNEOqZXPwHdwMVEBdqXEw iat-mode=0", "obfs4 109.105.109.165:10527 8DFCD8FB3285E855F5A55EDDA35696C743ABFC4E cert=Bvg/itxeL4TWKLP6N1MaQzSOC6tcRIBv6q57DYAZc3b2AzuM+/TfB7mqTFEfXILCjEwzVA iat-mode=1", "obfs4 83.212.101.3:50002 A09D536DD1752D542E1FBB3C9CE4449D51298239 cert=lPRQ/MXdD1t5SRZ9MquYQNT9m5DV757jtdXdlePmRCudUU9CFUOX1Tm7/meFSyPOsud7Cw iat-mode=0", "obfs4 109.105.109.147:13764 BBB28DF0F201E706BE564EFE690FE9577DD8386D cert=KfMQN/tNMFdda61hMgpiMI7pbwU1T+wxjTulYnfw+4sgvG0zSH7N7fwT10BI8MUdAD7iJA iat-mode=2", "obfs4 154.35.22.11:16488 A832D176ECD5C7C6B58825AE22FC4C90FA249637 cert=YPbQqXPiqTUBfjGFLpm9JYEFTBvnzEJDKJxXG5Sxzrr/v2qrhGU4Jls9lHjLAhqpXaEfZw iat-mode=0", "obfs4 154.35.22.12:80 00DC6C4FA49A65BD1472993CF6730D54F11E0DBB cert=N86E9hKXXXVz6G7w2z8wFfhIDztDAzZ/3poxVePHEYjbKDWzjkRDccFMAnhK75fc65pYSg iat-mode=0", "obfs4 154.35.22.13:443 FE7840FE1E21FE0A0639ED176EDA00A3ECA1E34D cert=fKnzxr+m+jWXXQGCaXe4f2gGoPXMzbL+bTBbXMYXuK0tMotd+nXyS33y2mONZWU29l81CA iat-mode=0", "obfs4 154.35.22.10:80 8FB9F4319E89E5C6223052AA525A192AFBC85D55 cert=GGGS1TX4R81m3r0HBl79wKy1OtPPNR2CZUIrHjkRg65Vc2VR8fOyo64f9kmT1UAFG7j0HQ iat-mode=0", "obfs4 154.35.22.10:443 8FB9F4319E89E5C6223052AA525A192AFBC85D55 cert=GGGS1TX4R81m3r0HBl79wKy1OtPPNR2CZUIrHjkRg65Vc2VR8fOyo64f9kmT1UAFG7j0HQ iat-mode=0", "obfs4 154.35.22.11:443 A832D176ECD5C7C6B58825AE22FC4C90FA249637 cert=YPbQqXPiqTUBfjGFLpm9JYEFTBvnzEJDKJxXG5Sxzrr/v2qrhGU4Jls9lHjLAhqpXaEfZw iat-mode=0", "obfs4 154.35.22.11:80 A832D176ECD5C7C6B58825AE22FC4C90FA249637 cert=YPbQqXPiqTUBfjGFLpm9JYEFTBvnzEJDKJxXG5Sxzrr/v2qrhGU4Jls9lHjLAhqpXaEfZw iat-mode=0", "obfs4 154.35.22.9:12166 C73ADBAC8ADFDBF0FC0F3F4E8091C0107D093716 cert=gEGKc5WN/bSjFa6UkG9hOcft1tuK+cV8hbZ0H6cqXiMPLqSbCh2Q3PHe5OOr6oMVORhoJA iat-mode=0", "obfs4 154.35.22.9:80 C73ADBAC8ADFDBF0FC0F3F4E8091C0107D093716 cert=gEGKc5WN/bSjFa6UkG9hOcft1tuK+cV8hbZ0H6cqXiMPLqSbCh2Q3PHe5OOr6oMVORhoJA iat-mode=0", "obfs4 154.35.22.9:443 C73ADBAC8ADFDBF0FC0F3F4E8091C0107D093716 cert=gEGKc5WN/bSjFa6UkG9hOcft1tuK+cV8hbZ0H6cqXiMPLqSbCh2Q3PHe5OOr6oMVORhoJA iat-mode=0", "obfs4 154.35.22.12:4304 00DC6C4FA49A65BD1472993CF6730D54F11E0DBB cert=N86E9hKXXXVz6G7w2z8wFfhIDztDAzZ/3poxVePHEYjbKDWzjkRDccFMAnhK75fc65pYSg iat-mode=0", "obfs4 154.35.22.13:16815 FE7840FE1E21FE0A0639ED176EDA00A3ECA1E34D cert=fKnzxr+m+jWXXQGCaXe4f2gGoPXMzbL+bTBbXMYXuK0tMotd+nXyS33y2mONZWU29l81CA iat-mode=0", "obfs4 192.95.36.142:443 CDF2E852BF539B82BD10E27E9115A31734E378C2 cert=qUVQ0srL1JI/vO6V6m/24anYXiJD3QP2HgzUKQtQ7GRqqUvs7P+tG43RtAqdhLOALP7DJQ iat-mode=1", "obfs4 85.17.30.79:443 FC259A04A328A07FED1413E9FC6526530D9FD87A cert=RutxZlu8BtyP+y0NX7bAVD41+J/qXNhHUrKjFkRSdiBAhIHIQLhKQ2HxESAKZprn/lR3KA iat-mode=0", "obfs4 38.229.1.78:80 C8CBDB2464FC9804A69531437BCF2BE31FDD2EE4 cert=Hmyfd2ev46gGY7NoVxA9ngrPF2zCZtzskRTzoWXbxNkzeVnGFPWmrTtILRyqCTjHR+s9dg iat-mode=1", "obfs4 38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 cert=VwEFpk9F/UN9JED7XpG1XOjm/O8ZCXK80oPecgWnNDZDv5pdkhq1OpbAH0wNqOT6H6BmRQ iat-mode=1", "obfs4 [2001:470:b381:bfff:216:3eff:fe23:d6c3]:443 CDF2E852BF539B82BD10E27E9115A31734E378C2 cert=qUVQ0srL1JI/vO6V6m/24anYXiJD3QP2HgzUKQtQ7GRqqUvs7P+tG43RtAqdhLOALP7DJQ iat-mode=1", "obfs4 37.218.240.34:40035 88CD36D45A35271963EF82E511C8827A24730913 cert=eGXYfWODcgqIdPJ+rRupg4GGvVGfh25FWaIXZkit206OSngsp7GAIiGIXOJJROMxEqFKJg iat-mode=1", "obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D cert=bjRaMrr1BRiAW8IE9U5z27fQaYgOhX1UCmOpg2pFpoMvo6ZgQMzLsaTzzQNTlm7hNcb+Sg iat-mode=0", "obfs4 85.31.186.98:443 011F2599C0E9B27EE74B353155E244813763C3E5 cert=ayq0XzCwhpdysn5o0EyDUbmSOx3X/oTEbzDMvczHOdBJKlvIdHHLJGkZARtT4dcBFArPPg iat-mode=0", "obfs4 85.31.186.26:443 91A6354697E6B02A386312F68D82CF86824D3606 cert=PBwr+S8JTVZo6MPdHnkTwXJPILWADLqfMGoVvhZClMq/Urndyd42BwX9YFJHZnBB3H0XCw iat-mode=0"); public static final List MEEK_AMAZON = Arrays.asList("meek 0.0.2.0:2 B9E7141C594AF25699E0079C1F0146F409495296 url=https://d2cly7j4zqgua7.cloudfront.net/ front=a0.awsstatic.com"); public static final List MEEK_AZURE = Arrays.asList("meek 0.0.2.0:3 97700DFE9F483596DDA6264C4D7DF7641E1E39CE url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com"); } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/DirectBindTor.java ================================================ package haveno.network.p2p.network; import lombok.extern.slf4j.Slf4j; import org.berndpruenster.netlayer.tor.Tor; @Slf4j public class DirectBindTor extends TorMode { public DirectBindTor() { super(null); } @Override public Tor getTor() { return null; } @Override public String getHiddenServiceDirectory() { return null; } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/HavenoRuntimeException.java ================================================ /* * 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 . */ package haveno.network.p2p.network; class HavenoRuntimeException extends RuntimeException { HavenoRuntimeException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/InboundConnection.java ================================================ /* * 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 . */ package haveno.network.p2p.network; import haveno.common.proto.network.NetworkProtoResolver; import java.net.Socket; import org.jetbrains.annotations.Nullable; public class InboundConnection extends Connection { public InboundConnection(Socket socket, MessageListener messageListener, ConnectionListener connectionListener, NetworkProtoResolver networkProtoResolver, @Nullable BanFilter banFilter) { super(socket, messageListener, connectionListener, null, networkProtoResolver, banFilter); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/LocalhostNetworkNode.java ================================================ /* * 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 . */ package haveno.network.p2p.network; import haveno.network.p2p.NodeAddress; import haveno.common.UserThread; import haveno.common.proto.network.NetworkProtoResolver; import java.net.ServerSocket; import java.net.Socket; import java.io.IOException; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.jetbrains.annotations.Nullable; // Run in UserThread public class LocalhostNetworkNode extends NetworkNode { private static final Logger log = LoggerFactory.getLogger(LocalhostNetworkNode.class); private static int simulateTorDelayTorNode = 500; private static int simulateTorDelayHiddenService = 500; public static void setSimulateTorDelayTorNode(int simulateTorDelayTorNode) { LocalhostNetworkNode.simulateTorDelayTorNode = simulateTorDelayTorNode; } public static void setSimulateTorDelayHiddenService(int simulateTorDelayHiddenService) { LocalhostNetworkNode.simulateTorDelayHiddenService = simulateTorDelayHiddenService; } /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public LocalhostNetworkNode(int port, NetworkProtoResolver networkProtoResolver, @Nullable BanFilter banFilter, int maxConnections) { super(port, networkProtoResolver, banFilter, maxConnections); } @Override public void start(@Nullable SetupListener setupListener) { if (setupListener != null) addSetupListener(setupListener); // simulate tor connection delay UserThread.runAfter(() -> { nodeAddressProperty.set(new NodeAddress("localhost", servicePort)); setupListeners.stream().forEach(SetupListener::onTorNodeReady); // simulate tor HS publishing delay UserThread.runAfter(() -> { try { startServer(new ServerSocket(servicePort)); } catch (IOException e) { log.error("Exception at startServer: {}\n", e.getMessage(), e); } setupListeners.stream().forEach(SetupListener::onHiddenServicePublished); }, simulateTorDelayTorNode, TimeUnit.MILLISECONDS); }, simulateTorDelayHiddenService, TimeUnit.MILLISECONDS); } // Called from NetworkNode thread @Override protected Socket createSocket(NodeAddress peerNodeAddress) throws IOException { return new Socket(peerNodeAddress.getHostName(), peerNodeAddress.getPort()); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/MessageListener.java ================================================ /* * 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 . */ package haveno.network.p2p.network; import haveno.common.proto.network.NetworkEnvelope; public interface MessageListener { void onMessage(NetworkEnvelope networkEnvelope, Connection connection); default void onMessageSent(NetworkEnvelope networkEnvelope, Connection connection) { } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/NetworkNode.java ================================================ /* * 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 . */ package haveno.network.p2p.network; import haveno.network.p2p.NodeAddress; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.app.Capabilities; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.proto.network.NetworkProtoResolver; import haveno.common.util.Utilities; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleObjectProperty; import lombok.Getter; import java.net.ServerSocket; import java.net.Socket; import java.io.IOException; import java.util.Date; import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import static com.google.common.base.Preconditions.checkNotNull; // Run in UserThread public abstract class NetworkNode implements MessageListener { private static final Logger log = LoggerFactory.getLogger(NetworkNode.class); private static final int CREATE_SOCKET_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(120); final int servicePort; private final NetworkProtoResolver networkProtoResolver; @Nullable private final BanFilter banFilter; private final CopyOnWriteArraySet inBoundConnections = new CopyOnWriteArraySet<>(); private final CopyOnWriteArraySet messageListeners = new CopyOnWriteArraySet<>(); private final CopyOnWriteArraySet connectionListeners = new CopyOnWriteArraySet<>(); final CopyOnWriteArraySet setupListeners = new CopyOnWriteArraySet<>(); private final ListeningExecutorService connectionExecutor; private final ListeningExecutorService sendMessageExecutor; private Server server; @Getter private volatile boolean isShutDownStarted; // accessed from different threads private final CopyOnWriteArraySet outBoundConnections = new CopyOnWriteArraySet<>(); protected final ObjectProperty nodeAddressProperty = new SimpleObjectProperty<>(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// NetworkNode(int servicePort, NetworkProtoResolver networkProtoResolver, @Nullable BanFilter banFilter, int maxConnections) { this.servicePort = servicePort; this.networkProtoResolver = networkProtoResolver; this.banFilter = banFilter; connectionExecutor = Utilities.getListeningExecutorService("NetworkNode.connection", maxConnections * 2, maxConnections * 3, 30, 30); sendMessageExecutor = Utilities.getListeningExecutorService("NetworkNode.sendMessage", maxConnections * 2, maxConnections * 3, 30, 30); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// // Calls this (and other registered) setup listener's ``onTorNodeReady()`` and ``onHiddenServicePublished`` // when the events happen. public abstract void start(@Nullable SetupListener setupListener); public SettableFuture sendMessage(@NotNull NodeAddress peersNodeAddress, NetworkEnvelope networkEnvelope) { return sendMessage(peersNodeAddress, networkEnvelope, null); } public SettableFuture sendMessage(@NotNull NodeAddress peersNodeAddress, NetworkEnvelope networkEnvelope, Integer timeoutSeconds) { log.debug("Send {} to {}. Message details: {}", networkEnvelope.getClass().getSimpleName(), peersNodeAddress, Utilities.toTruncatedString(networkEnvelope)); checkNotNull(peersNodeAddress, "peerAddress must not be null"); Connection connection = getOutboundConnection(peersNodeAddress); if (connection == null) connection = getInboundConnection(peersNodeAddress); if (connection != null) { return sendMessage(connection, networkEnvelope); } else { log.debug("We have not found any connection for peerAddress {}.\n\t" + "We will create a new outbound connection.", peersNodeAddress); SettableFuture resultFuture = SettableFuture.create(); CompletableFuture future = CompletableFuture.supplyAsync(() -> { try { Thread.currentThread().setName("NetworkNode.connectionExecutor:SendMessage-to-" + Utilities.toTruncatedString(peersNodeAddress.getFullAddress(), 15)); if (peersNodeAddress.equals(getNodeAddress())) { log.warn("We are sending a message to ourselves"); } OutboundConnection outboundConnection; // can take a while when using tor long startTs = System.currentTimeMillis(); log.debug("Start create socket to peersNodeAddress {}", peersNodeAddress.getFullAddress()); Socket socket = createSocket(peersNodeAddress); long duration = System.currentTimeMillis() - startTs; log.info("Socket creation to peersNodeAddress {} took {} ms", peersNodeAddress.getFullAddress(), duration); if (duration > CREATE_SOCKET_TIMEOUT) throw new TimeoutException("A timeout occurred when creating a socket."); // Tor needs sometimes quite long to create a connection. To avoid that we get too many // connections with the same peer we check again if we still don't have any connection for that node address. Connection existingConnection = getInboundConnection(peersNodeAddress); if (existingConnection == null) existingConnection = getOutboundConnection(peersNodeAddress); if (existingConnection != null) { log.debug("We found in the meantime a connection for peersNodeAddress {}, " + "so we use that for sending the message.\n" + "That can happen if Tor needs long for creating a new outbound connection.\n" + "We might have got a new inbound or outbound connection.", peersNodeAddress.getFullAddress()); try { socket.close(); } catch (Throwable throwable) { if (!isShutDownStarted) { log.error("Error at closing socket " + throwable); } } existingConnection.sendMessage(networkEnvelope); return existingConnection; } else { ConnectionListener connectionListener = new ConnectionListener() { @Override public void onConnection(Connection connection) { if (!connection.isStopped()) { outBoundConnections.add((OutboundConnection) connection); printOutBoundConnections(); connectionListeners.forEach(e -> e.onConnection(connection)); } } @Override public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { // noinspection SuspiciousMethodCalls outBoundConnections.remove(connection); printOutBoundConnections(); connectionListeners.forEach(e -> e.onDisconnect(closeConnectionReason, connection)); } }; outboundConnection = new OutboundConnection(socket, NetworkNode.this, connectionListener, peersNodeAddress, networkProtoResolver, banFilter); if (log.isDebugEnabled()) { log.debug("\n\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n" + "NetworkNode created new outbound connection:" + "\nmyNodeAddress=" + getNodeAddress() + "\npeersNodeAddress=" + peersNodeAddress + "\nuid=" + outboundConnection.getUid() + "\nmessage=" + networkEnvelope + "\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n"); } // can take a while when using tor outboundConnection.sendMessage(networkEnvelope); return outboundConnection; } } catch (Exception e) { throw new RuntimeException(e); } }, connectionExecutor); // handle future with timeout if (timeoutSeconds != null) future.orTimeout(timeoutSeconds, TimeUnit.SECONDS); future.exceptionally(throwable -> { log.debug("onFailure at sendMessage: peersNodeAddress={}\n\tmessage={}\n\tthrowable={}", peersNodeAddress, networkEnvelope.getClass().getSimpleName(), throwable.toString()); UserThread.execute(() -> { if (!resultFuture.setException(throwable)) { // In case the setException returns false we need to cancel the future. resultFuture.cancel(true); } }); return null; }); future.thenAccept(resultFuture::set); return resultFuture; } } @Nullable private InboundConnection getInboundConnection(@NotNull NodeAddress peersNodeAddress) { Optional inboundConnectionOptional = lookupInBoundConnection(peersNodeAddress); if (inboundConnectionOptional.isPresent()) { InboundConnection connection = inboundConnectionOptional.get(); log.trace("We have found a connection in inBoundConnections. Connection.uid={}", connection.getUid()); if (connection.isStopped()) { log.warn("We have a connection which is already stopped in inBoundConnections. Connection.uid=" + connection.getUid()); inBoundConnections.remove(connection); return null; } else { return connection; } } else { return null; } } @Nullable private OutboundConnection getOutboundConnection(@NotNull NodeAddress peersNodeAddress) { Optional outboundConnectionOptional = lookupOutBoundConnection(peersNodeAddress); if (outboundConnectionOptional.isPresent()) { OutboundConnection connection = outboundConnectionOptional.get(); log.trace("We have found a connection in outBoundConnections. Connection.uid={}", connection.getUid()); if (connection.isStopped()) { log.warn("We have a connection which is already stopped in outBoundConnections. Connection.uid=" + connection.getUid()); outBoundConnections.remove(connection); return null; } else { return connection; } } else { return null; } } @Nullable public Socks5Proxy getSocksProxy() { return null; } public SettableFuture sendMessage(Connection connection, NetworkEnvelope networkEnvelope) { return sendMessage(connection, networkEnvelope, sendMessageExecutor); } public SettableFuture sendMessage(Connection connection, NetworkEnvelope networkEnvelope, ListeningExecutorService executor) { SettableFuture resultFuture = SettableFuture.create(); try { ListenableFuture future = executor.submit(() -> { String id = connection.getPeersNodeAddressOptional().isPresent() ? connection.getPeersNodeAddressOptional().get().getFullAddress() : connection.getUid(); Thread.currentThread().setName("NetworkNode:SendMessage-to-" + Utilities.toTruncatedString(id, 15)); connection.sendMessage(networkEnvelope); return connection; }); Futures.addCallback(future, new FutureCallback<>() { public void onSuccess(Connection connection) { UserThread.execute(() -> resultFuture.set(connection)); } public void onFailure(@NotNull Throwable throwable) { UserThread.execute(() -> resolveWithException(resultFuture, throwable)); } }, MoreExecutors.directExecutor()); } catch (RejectedExecutionException exception) { if (!executor.isShutdown()) { log.error("RejectedExecutionException at sendMessage: ", exception); UserThread.execute(() -> resolveWithException(resultFuture, exception)); } } return resultFuture; } private void resolveWithException(SettableFuture future, Throwable exception) { if (!future.setException(exception)) { future.cancel(true); // In case the setException returns false we need to cancel the future. } } public ReadOnlyObjectProperty nodeAddressProperty() { return nodeAddressProperty; } public Set getAllConnections() { // Can contain inbound and outbound connections with the same peer node address, // as connection hashcode is using uid and port info Set set = new HashSet<>(inBoundConnections); set.addAll(outBoundConnections); return set; } public Set getConfirmedConnections() { // Can contain inbound and outbound connections with the same peer node address, // as connection hashcode is using uid and port info return getAllConnections().stream() .filter(Connection::hasPeersNodeAddress) .collect(Collectors.toSet()); } public Set getNodeAddressesOfConfirmedConnections() { // Does not contain inbound and outbound connection with the same peer node address return getConfirmedConnections().stream() .map(e -> e.getPeersNodeAddressOptional().get()) .collect(Collectors.toSet()); } public void shutDown(Runnable shutDownCompleteHandler) { log.info("NetworkNode shutdown started"); if (!isShutDownStarted) { isShutDownStarted = true; if (server != null) { server.shutDown(); server = null; } Set allConnections = getAllConnections(); int numConnections = allConnections.size(); if (numConnections == 0) { log.info("Shutdown immediately because no connections are open."); if (shutDownCompleteHandler != null) { shutDownCompleteHandler.run(); } return; } log.info("Shutdown {} connections", numConnections); AtomicInteger shutdownCompleted = new AtomicInteger(); Timer timeoutHandler = UserThread.runAfter(() -> { if (shutDownCompleteHandler != null) { log.info("Shutdown completed due timeout"); shutDownCompleteHandler.run(); } }, 1500, TimeUnit.MILLISECONDS); allConnections.forEach(c -> c.shutDown(CloseConnectionReason.APP_SHUT_DOWN, () -> { shutdownCompleted.getAndIncrement(); log.info("Shutdown of node {} completed", c.getPeersNodeAddressOptional()); if (shutdownCompleted.get() == numConnections) { log.info("Shutdown completed with all connections closed"); timeoutHandler.stop(); connectionExecutor.shutdownNow(); sendMessageExecutor.shutdownNow(); if (shutDownCompleteHandler != null) { shutDownCompleteHandler.run(); } } })); } } /////////////////////////////////////////////////////////////////////////////////////////// // SetupListener /////////////////////////////////////////////////////////////////////////////////////////// public void addSetupListener(SetupListener setupListener) { boolean isNewEntry = setupListeners.add(setupListener); if (!isNewEntry) log.warn("Try to add a setupListener which was already added."); } /////////////////////////////////////////////////////////////////////////////////////////// // MessageListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { messageListeners.stream().forEach(e -> e.onMessage(networkEnvelope, connection)); } /////////////////////////////////////////////////////////////////////////////////////////// // Listeners /////////////////////////////////////////////////////////////////////////////////////////// public void addConnectionListener(ConnectionListener connectionListener) { boolean isNewEntry = connectionListeners.add(connectionListener); if (!isNewEntry) log.warn("Try to add a connectionListener which was already added.\n\tconnectionListener={}\n\tconnectionListeners={}", connectionListener, connectionListeners); } public void removeConnectionListener(ConnectionListener connectionListener) { boolean contained = connectionListeners.remove(connectionListener); if (!contained) log.debug("Try to remove a connectionListener which was never added.\n\t" + "That might happen because of async behaviour of CopyOnWriteArraySet"); } public void addMessageListener(MessageListener messageListener) { boolean isNewEntry = messageListeners.add(messageListener); if (!isNewEntry) log.warn("Try to add a messageListener which was already added."); } public void removeMessageListener(MessageListener messageListener) { boolean contained = messageListeners.remove(messageListener); if (!contained) log.debug("Try to remove a messageListener which was never added.\n\t" + "That might happen because of async behaviour of CopyOnWriteArraySet"); } /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// void startServer(ServerSocket serverSocket) { ConnectionListener connectionListener = new ConnectionListener() { @Override public void onConnection(Connection connection) { if (!connection.isStopped()) { inBoundConnections.add((InboundConnection) connection); printInboundConnections(); connectionListeners.stream().forEach(e -> e.onConnection(connection)); } } @Override public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { log.trace("onDisconnect at server socket connectionListener\n\tconnection={}", connection); // noinspection SuspiciousMethodCalls inBoundConnections.remove(connection); printInboundConnections(); connectionListeners.stream().forEach(e -> e.onDisconnect(closeConnectionReason, connection)); } }; server = new Server(serverSocket, NetworkNode.this, connectionListener, networkProtoResolver, banFilter); server.start(); } private Optional lookupOutBoundConnection(NodeAddress peersNodeAddress) { log.trace("lookupOutboundConnection for peersNodeAddress={}", peersNodeAddress.getFullAddress()); printOutBoundConnections(); return outBoundConnections.stream() .filter(connection -> connection.hasPeersNodeAddress() && peersNodeAddress.equals(connection.getPeersNodeAddressOptional().get())) .findAny(); } private void printOutBoundConnections() { StringBuilder sb = new StringBuilder("outBoundConnections size()=") .append(outBoundConnections.size()).append("\n\toutBoundConnections="); outBoundConnections.stream().forEach(e -> sb.append(e).append("\n\t")); log.debug(sb.toString()); } private Optional lookupInBoundConnection(NodeAddress peersNodeAddress) { log.trace("lookupInboundConnection for peersNodeAddress={}", peersNodeAddress.getFullAddress()); printInboundConnections(); return inBoundConnections.stream() .filter(connection -> connection.hasPeersNodeAddress() && peersNodeAddress.equals(connection.getPeersNodeAddressOptional().get())) .findAny(); } private void printInboundConnections() { StringBuilder sb = new StringBuilder("inBoundConnections size()=") .append(inBoundConnections.size()).append("\n\tinBoundConnections="); inBoundConnections.stream().forEach(e -> sb.append(e).append("\n\t")); log.debug(sb.toString()); } protected abstract Socket createSocket(NodeAddress peersNodeAddress) throws IOException; @Nullable public NodeAddress getNodeAddress() { return nodeAddressProperty.get(); } public Optional findPeersCapabilities(NodeAddress nodeAddress) { return getConfirmedConnections().stream() .filter(c -> c.getPeersNodeAddressProperty().get() != null) .filter(c -> c.getPeersNodeAddressProperty().get().equals(nodeAddress)) .map(Connection::getCapabilities) .findAny(); } public long upTime() { // how long Haveno has been running with at least one connection // uptime is relative to last all connections lost event long earliestConnection = new Date().getTime(); for (Connection connection : outBoundConnections) { earliestConnection = Math.min(earliestConnection, connection.getStatistic().getCreationDate().getTime()); } return new Date().getTime() - earliestConnection; } public int getInboundConnectionCount() { return inBoundConnections.size(); } public int getOutboundConnectionCount() { return outBoundConnections.size(); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/NewTor.java ================================================ /* * 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 . */ package haveno.network.p2p.network; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.LinkedHashMap; import java.util.stream.Collectors; import org.berndpruenster.netlayer.tor.NativeTor; import org.berndpruenster.netlayer.tor.Tor; import org.berndpruenster.netlayer.tor.TorCtlException; import org.berndpruenster.netlayer.tor.Torrc; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; /** * This class creates a brand new instance of the Tor onion router. * * When asked, the class checks, whether command line parameters such as * --torrcFile and --torrcOptions are set and if so, takes these settings into * account. Then, a fresh set of Tor binaries is installed and Tor is launched. * Finally, a {@link Tor} instance is returned for further use. * * @author Florian Reimair * */ @Slf4j public class NewTor extends TorMode { private final File torrcFile; private final String torrcOptions; private final BridgeAddressProvider bridgeAddressProvider; public NewTor(File torWorkingDirectory, @Nullable File torrcFile, String torrcOptions, BridgeAddressProvider bridgeAddressProvider) { super(torWorkingDirectory); this.torrcFile = torrcFile; this.torrcOptions = torrcOptions; this.bridgeAddressProvider = bridgeAddressProvider; } @Override public Tor getTor() throws IOException, TorCtlException { long ts1 = new Date().getTime(); Collection bridgeEntries = bridgeAddressProvider.getBridgeAddresses(); if (bridgeEntries != null) log.info("Using bridges: {}", bridgeEntries.stream().collect(Collectors.joining(","))); Torrc override = null; // check if the user wants to provide his own torrc file if (torrcFile != null) { try { override = new Torrc(new FileInputStream(torrcFile)); } catch (IOException e) { log.error("custom torrc file not found ('{}'). Proceeding with defaults.", torrcFile); } } // check if the user wants to temporarily add to the default torrc file LinkedHashMap torrcOptionsMap = new LinkedHashMap<>(); if (!"".equals(torrcOptions)) { Arrays.asList(torrcOptions.split(",")).forEach(line -> { line = line.trim(); if (line.matches("^[^\\s]+\\s.+")) { String[] tmp = line.split("\\s", 2); torrcOptionsMap.put(tmp[0].trim(), tmp[1].trim()); } else { log.error("custom torrc override parse error ('{}'). Proceeding without custom overrides.", line); torrcOptionsMap.clear(); } }); } // assemble final override options if (!torrcOptionsMap.isEmpty()) // check for custom torrcFile if (override != null) // and merge the contents override = new Torrc(override.getInputStream$tor_native(), torrcOptionsMap); else override = new Torrc(torrcOptionsMap); log.info("Starting tor"); NativeTor result = new NativeTor(torDir, bridgeEntries, override); log.info( "\n################################################################\n" + "Tor started after {} ms. Start publishing hidden service.\n" + "################################################################", (new Date().getTime() - ts1)); // takes usually a few seconds return result; } @Override public String getHiddenServiceDirectory() { return ""; } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/OutboundConnection.java ================================================ /* * 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 . */ package haveno.network.p2p.network; import haveno.common.proto.network.NetworkProtoResolver; import haveno.network.p2p.NodeAddress; import org.jetbrains.annotations.Nullable; import java.net.Socket; public class OutboundConnection extends Connection { public OutboundConnection(Socket socket, MessageListener messageListener, ConnectionListener connectionListener, NodeAddress peersNodeAddress, NetworkProtoResolver networkProtoResolver, @Nullable BanFilter banFilter) { super(socket, messageListener, connectionListener, peersNodeAddress, networkProtoResolver, banFilter); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/PeerType.java ================================================ /* * 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 . */ package haveno.network.p2p.network; public enum PeerType { // PEER is default type PEER, // If connection was used for initial data request/response. Those are marked with the InitialDataExchangeMessage interface INITIAL_DATA_EXCHANGE, // If a PrefixedSealedAndSignedMessage was sent (usually a trade message). Expects that node address is known. DIRECT_MSG_PEER } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/ProtoOutputStream.java ================================================ /* * 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 . */ package haveno.network.p2p.network; import haveno.network.p2p.peers.keepalive.messages.KeepAliveMessage; import haveno.common.proto.network.NetworkEnvelope; import java.io.IOException; import java.io.OutputStream; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.concurrent.ThreadSafe; @ThreadSafe class ProtoOutputStream { private static final Logger log = LoggerFactory.getLogger(ProtoOutputStream.class); private final OutputStream outputStream; private final Statistic statistic; private final AtomicBoolean isConnectionActive = new AtomicBoolean(true); private final Lock lock = new ReentrantLock(); ProtoOutputStream(OutputStream outputStream, Statistic statistic) { this.outputStream = outputStream; this.statistic = statistic; } void writeEnvelope(NetworkEnvelope envelope) { lock.lock(); try { writeEnvelopeOrThrow(envelope); } catch (IOException e) { if (!isConnectionActive.get()) { // Connection was closed by us. return; } log.error("Failed to write envelope", e); throw new HavenoRuntimeException("Failed to write envelope", e); } finally { lock.unlock(); } } void onConnectionShutdown() { isConnectionActive.set(false); boolean acquiredLock = tryToAcquireLock(); if (!acquiredLock) { return; } try { outputStream.close(); } catch (Throwable t) { log.error("Failed to close connection", t); } finally { lock.unlock(); } } private void writeEnvelopeOrThrow(NetworkEnvelope envelope) throws IOException { long ts = System.currentTimeMillis(); protobuf.NetworkEnvelope proto = envelope.toProtoNetworkEnvelope(); proto.writeDelimitedTo(outputStream); outputStream.flush(); long duration = System.currentTimeMillis() - ts; if (duration > 10000) { log.info("Sending {} to peer took {} sec.", envelope.getClass().getSimpleName(), duration / 1000d); } statistic.addSentBytes(proto.getSerializedSize()); statistic.addSentMessage(envelope); if (!(envelope instanceof KeepAliveMessage)) { statistic.updateLastActivityTimestamp(); } } private boolean tryToAcquireLock() { long shutdownTimeout = Connection.getShutdownTimeout(); try { return lock.tryLock(shutdownTimeout, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { return false; } } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/RuleViolation.java ================================================ /* * 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 . */ package haveno.network.p2p.network; public enum RuleViolation { INVALID_DATA_TYPE(2), WRONG_NETWORK_ID(0), MAX_MSG_SIZE_EXCEEDED(2), THROTTLE_LIMIT_EXCEEDED(2), TOO_MANY_REPORTED_PEERS_SENT(2), PEER_BANNED(0), INVALID_CLASS(0); public final int maxTolerance; RuleViolation(int maxTolerance) { this.maxTolerance = maxTolerance; } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/RunningTor.java ================================================ /* * 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 . */ package haveno.network.p2p.network; import java.io.File; import java.util.Date; import lombok.extern.slf4j.Slf4j; import org.berndpruenster.netlayer.tor.ExternalTor; import org.berndpruenster.netlayer.tor.Tor; import org.berndpruenster.netlayer.tor.TorCtlException; import java.net.ConnectException; import java.net.UnknownHostException; /** * This class creates a brand new instance of the Tor onion router. * * When asked, the class checks for the authentication method selected and * connects to the given control port. Finally, a {@link Tor} instance is * returned for further use. * * @author Florian Reimair * */ @Slf4j public class RunningTor extends TorMode { private final String controlHost; private final int controlPort; private final String password; private final File cookieFile; private final boolean useSafeCookieAuthentication; public RunningTor(final File torDir, final String controlHost, final int controlPort, final String password, final File cookieFile, final boolean useSafeCookieAuthentication) { super(torDir); this.controlHost = controlHost; this.controlPort = controlPort; this.password = password; this.cookieFile = cookieFile; this.useSafeCookieAuthentication = useSafeCookieAuthentication; } @Override public Tor getTor() throws TorCtlException { long ts1 = new Date().getTime(); boolean retry = true; long twoMinutesInMilli = 1000 * 60 * 2; while (retry && ((new Date().getTime() - ts1) <= twoMinutesInMilli)) { retry = false; try { log.info("Connecting to running tor"); Tor result; if (!password.isEmpty()) result = new ExternalTor(controlHost, controlPort, password); else if (cookieFile != null && cookieFile.exists()) result = new ExternalTor(controlHost, controlPort, cookieFile, useSafeCookieAuthentication); else result = new ExternalTor(controlHost, controlPort); boolean isTorBootstrapped = result.control.waitUntilBootstrapped(); if (!isTorBootstrapped) { log.error("Couldn't bootstrap Tor."); } log.info( "\n################################################################\n" + "Connecting to Tor successful after {} ms. Start publishing hidden service.\n" + "################################################################", (new Date().getTime() - ts1)); // takes usually a few seconds return result; } catch (Exception e) { // netlayer throws UnknownHostException when tor docker container is not ready yet. // netlayer throws ConnectException before tor container bind to control port. if (e instanceof UnknownHostException || e instanceof ConnectException) { log.warn("Couldn't connect to Tor control port. Retrying...", e); retry = true; } log.error("Couldn't connect to Tor.", e); } } return null; } @Override public String getHiddenServiceDirectory() { return new File(torDir, HIDDEN_SERVICE_DIRECTORY).getAbsolutePath(); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/Server.java ================================================ /* * 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 . */ package haveno.network.p2p.network; import haveno.common.proto.network.NetworkProtoResolver; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.io.IOException; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.jetbrains.annotations.Nullable; class Server implements Runnable { private static final Logger log = LoggerFactory.getLogger(Server.class); private final MessageListener messageListener; private final ConnectionListener connectionListener; @Nullable private final BanFilter banFilter; private final ServerSocket serverSocket; private final int localPort; private final Set connections = new CopyOnWriteArraySet<>(); private final NetworkProtoResolver networkProtoResolver; private final Thread serverThread = new Thread(this); public Server(ServerSocket serverSocket, MessageListener messageListener, ConnectionListener connectionListener, NetworkProtoResolver networkProtoResolver, @Nullable BanFilter banFilter) { this.networkProtoResolver = networkProtoResolver; this.serverSocket = serverSocket; this.localPort = serverSocket.getLocalPort(); this.messageListener = messageListener; this.connectionListener = connectionListener; this.banFilter = banFilter; } public void start() { serverThread.setName("Server-" + localPort); serverThread.start(); } @Override public void run() { try { try { while (isServerActive()) { log.debug("Ready to accept new clients on port " + localPort); final Socket socket = serverSocket.accept(); if (isServerActive()) { log.debug("Accepted new client on localPort/port " + socket.getLocalPort() + "/" + socket.getPort()); InboundConnection connection = new InboundConnection(socket, messageListener, connectionListener, networkProtoResolver, banFilter); log.debug("\n\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n" + "Server created new inbound connection:" + "\nlocalPort/port={}/{}" + "\nconnection.uid={}", serverSocket.getLocalPort(), socket.getPort(), connection.getUid() + "\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n"); if (isServerActive()) connections.add(connection); else connection.shutDown(CloseConnectionReason.APP_SHUT_DOWN); } } } catch (IOException e) { if (isServerActive()) log.error("Error executing server loop: {}\n", e.getMessage(), e); } } catch (Throwable t) { log.error("Executing task failed: {}\n", t.getMessage(), t); } } public void shutDown() { log.info("Server shutdown started"); if (isServerActive()) { serverThread.interrupt(); connections.forEach(connection -> connection.shutDown(CloseConnectionReason.APP_SHUT_DOWN)); try { if (!serverSocket.isClosed()) { serverSocket.close(); } } catch (SocketException e) { log.debug("SocketException at shutdown might be expected " + e.getMessage()); } catch (IOException e) { log.debug("Exception at shutdown. " + e.getMessage()); } finally { log.debug("Server shutdown complete"); } } else { log.warn("stopped already called ast shutdown"); } } private boolean isServerActive() { return !serverThread.isInterrupted(); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/SetupListener.java ================================================ /* * 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 . */ package haveno.network.p2p.network; public interface SetupListener { void onTorNodeReady(); void onHiddenServicePublished(); default void onSetupFailed(Throwable throwable) { } default void onRequestCustomBridges() { } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/Statistic.java ================================================ /* * 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 . */ package haveno.network.p2p.network; import haveno.common.UserThread; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.util.Utilities; import javafx.beans.property.DoubleProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.LongProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleLongProperty; import lombok.extern.slf4j.Slf4j; import java.util.Date; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; /** * Network statistics per connection. As we are also interested in total network statistics * we use static properties to get traffic of all connections combined. */ @Slf4j public class Statistic { /////////////////////////////////////////////////////////////////////////////////////////// // Static /////////////////////////////////////////////////////////////////////////////////////////// private final static long startTime = System.currentTimeMillis(); private final static LongProperty totalSentBytes = new SimpleLongProperty(0); private final static DoubleProperty totalSentBytesPerSec = new SimpleDoubleProperty(0); private final static LongProperty totalReceivedBytes = new SimpleLongProperty(0); private final static DoubleProperty totalReceivedBytesPerSec = new SimpleDoubleProperty(0); private final static Map totalReceivedMessages = new ConcurrentHashMap<>(); private final static Map totalSentMessages = new ConcurrentHashMap<>(); private final static LongProperty numTotalSentMessages = new SimpleLongProperty(0); private final static DoubleProperty numTotalSentMessagesPerSec = new SimpleDoubleProperty(0); private final static LongProperty numTotalReceivedMessages = new SimpleLongProperty(0); private final static DoubleProperty numTotalReceivedMessagesPerSec = new SimpleDoubleProperty(0); static { UserThread.runPeriodically(() -> { numTotalSentMessages.set(totalSentMessages.values().stream().mapToInt(Integer::intValue).sum()); numTotalReceivedMessages.set(totalReceivedMessages.values().stream().mapToInt(Integer::intValue).sum()); long passed = (System.currentTimeMillis() - startTime) / 1000; numTotalSentMessagesPerSec.set(((double) numTotalSentMessages.get()) / passed); numTotalReceivedMessagesPerSec.set(((double) numTotalReceivedMessages.get()) / passed); totalSentBytesPerSec.set(((double) totalSentBytes.get()) / passed); totalReceivedBytesPerSec.set(((double) totalReceivedBytes.get()) / passed); }, 1); // We log statistics every 60 minutes UserThread.runPeriodically(() -> { String ls = System.lineSeparator(); log.info("Accumulated network statistics:" + ls + "Bytes sent: {};" + ls + "Number of sent messages/Sent messages: {} / {};" + ls + "Number of sent messages per sec: {};" + ls + "Bytes received: {}" + ls + "Number of received messages/Received messages: {} / {};" + ls + "Number of received messages per sec: {}" + ls, Utilities.readableFileSize(totalSentBytes.get()), numTotalSentMessages.get(), totalSentMessages, numTotalSentMessagesPerSec.get(), Utilities.readableFileSize(totalReceivedBytes.get()), numTotalReceivedMessages.get(), totalReceivedMessages, numTotalReceivedMessagesPerSec.get()); }, TimeUnit.MINUTES.toSeconds(60)); } public static LongProperty totalSentBytesProperty() { return totalSentBytes; } public static DoubleProperty totalSentBytesPerSecProperty() { return totalSentBytesPerSec; } public static LongProperty totalReceivedBytesProperty() { return totalReceivedBytes; } public static DoubleProperty totalReceivedBytesPerSecProperty() { return totalReceivedBytesPerSec; } public static LongProperty numTotalSentMessagesProperty() { return numTotalSentMessages; } public static DoubleProperty numTotalSentMessagesPerSecProperty() { return numTotalSentMessagesPerSec; } public static LongProperty numTotalReceivedMessagesProperty() { return numTotalReceivedMessages; } public static DoubleProperty numTotalReceivedMessagesPerSecProperty() { return numTotalReceivedMessagesPerSec; } /////////////////////////////////////////////////////////////////////////////////////////// // Instance fields /////////////////////////////////////////////////////////////////////////////////////////// private final Date creationDate; private long lastActivityTimestamp = System.currentTimeMillis(); private final LongProperty sentBytes = new SimpleLongProperty(0); private final LongProperty receivedBytes = new SimpleLongProperty(0); private final Map receivedMessages = new ConcurrentHashMap<>(); private final Map sentMessages = new ConcurrentHashMap<>(); private final IntegerProperty roundTripTime = new SimpleIntegerProperty(0); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// Statistic() { creationDate = new Date(); } /////////////////////////////////////////////////////////////////////////////////////////// // Update, increment /////////////////////////////////////////////////////////////////////////////////////////// void updateLastActivityTimestamp() { UserThread.execute(() -> lastActivityTimestamp = System.currentTimeMillis()); } void addSentBytes(int value) { UserThread.execute(() -> { sentBytes.set(sentBytes.get() + value); totalSentBytes.set(totalSentBytes.get() + value); }); } void addReceivedBytes(int value) { UserThread.execute(() -> { receivedBytes.set(receivedBytes.get() + value); totalReceivedBytes.set(totalReceivedBytes.get() + value); }); } // TODO would need msg inspection to get useful information... void addReceivedMessage(NetworkEnvelope networkEnvelope) { String messageClassName = networkEnvelope.getClass().getSimpleName(); int counter = 1; if (receivedMessages.containsKey(messageClassName)) { counter = receivedMessages.get(messageClassName) + 1; } receivedMessages.put(messageClassName, counter); counter = 1; if (totalReceivedMessages.containsKey(messageClassName)) { counter = totalReceivedMessages.get(messageClassName) + 1; } totalReceivedMessages.put(messageClassName, counter); } void addSentMessage(NetworkEnvelope networkEnvelope) { String messageClassName = networkEnvelope.getClass().getSimpleName(); int counter = 1; if (sentMessages.containsKey(messageClassName)) { counter = sentMessages.get(messageClassName) + 1; } sentMessages.put(messageClassName, counter); counter = 1; if (totalSentMessages.containsKey(messageClassName)) { counter = totalSentMessages.get(messageClassName) + 1; } totalSentMessages.put(messageClassName, counter); } public void setRoundTripTime(int roundTripTime) { this.roundTripTime.set(roundTripTime); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// public long getLastActivityTimestamp() { return lastActivityTimestamp; } public long getLastActivityAge() { return System.currentTimeMillis() - lastActivityTimestamp; } public long getSentBytes() { return sentBytes.get(); } public LongProperty sentBytesProperty() { return sentBytes; } public long getReceivedBytes() { return receivedBytes.get(); } public LongProperty receivedBytesProperty() { return receivedBytes; } public Date getCreationDate() { return creationDate; } public IntegerProperty roundTripTimeProperty() { return roundTripTime; } public static long getTotalSentBytes() { return totalSentBytes.get(); } public static double getTotalSentBytesPerSec() { return totalSentBytesPerSec.get(); } public static long getTotalReceivedBytes() { return totalReceivedBytes.get(); } public static double getTotalReceivedBytesPerSec() { return totalReceivedBytesPerSec.get(); } public static double numTotalReceivedMessagesPerSec() { return numTotalReceivedMessagesPerSec.get(); } public static double getNumTotalSentMessagesPerSec() { return numTotalSentMessagesPerSec.get(); } @Override public String toString() { return "Statistic{" + "\n creationDate=" + creationDate + ",\n lastActivityTimestamp=" + lastActivityTimestamp + ",\n sentBytes=" + sentBytes + ",\n receivedBytes=" + receivedBytes + ",\n receivedMessages=" + receivedMessages + ",\n sentMessages=" + sentMessages + ",\n roundTripTime=" + roundTripTime + "\n}"; } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/SupportedCapabilitiesListener.java ================================================ /* * 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 . */ package haveno.network.p2p.network; import haveno.common.app.Capabilities; public interface SupportedCapabilitiesListener { void onChanged(Capabilities supportedCapabilities); } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/TorMode.java ================================================ /* * 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 . */ package haveno.network.p2p.network; import haveno.common.file.FileUtil; import org.berndpruenster.netlayer.tor.Tor; import org.berndpruenster.netlayer.tor.TorCtlException; import java.io.File; import java.io.IOException; /** * Holds information on how tor should be created and delivers a respective * {@link Tor} object when asked. * * @author Florian Reimair * */ public abstract class TorMode { /** * The sub-directory where the private_key file sits in. Kept * private, because it only concerns implementations of {@link TorMode}. */ protected static final String HIDDEN_SERVICE_DIRECTORY = "hiddenservice"; protected final File torDir; /** * @param torDir points to the place, where we will persist private * key and address data */ public TorMode(File torDir) { this.torDir = torDir; } /** * Returns a fresh {@link Tor} object. * * @return a fresh instance of {@link Tor} * @throws IOException * @throws TorCtlException */ public abstract Tor getTor() throws IOException, TorCtlException; /** * {@link NativeTor}'s inner workings prepend its Tor installation path and some * other stuff to the hiddenServiceDir, thus, selecting nothing (i.e. * "") as a hidden service directory is fine. {@link ExternalTor}, * however, does not have a Tor installation path and thus, takes the hidden * service path literally. Hence, we set "torDir/hiddenservice" as * the hidden service directory. By doing so, we use the same * private_key file as in {@link NewTor} mode. * * @return "" in {@link NewTor} Mode, * "torDir/externalTorHiddenService" in {@link RunningTor} * mode */ public abstract String getHiddenServiceDirectory(); /** * Do a rolling backup of the "private_key" file. */ protected void doRollingBackup() { FileUtil.rollingBackup(new File(torDir, HIDDEN_SERVICE_DIRECTORY), "private_key", 20); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/TorNetworkNode.java ================================================ /* * 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 . */ package haveno.network.p2p.network; import haveno.network.p2p.NodeAddress; import haveno.common.proto.network.NetworkProtoResolver; import haveno.common.util.SingleThreadExecutorUtils; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import java.net.Socket; import java.io.IOException; import java.util.concurrent.ExecutorService; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.Nullable; @Slf4j public abstract class TorNetworkNode extends NetworkNode { protected final ExecutorService executor; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public TorNetworkNode(int servicePort, NetworkProtoResolver networkProtoResolver, @Nullable BanFilter banFilter, int maxConnections) { super(servicePort, networkProtoResolver, banFilter, maxConnections); executor = SingleThreadExecutorUtils.getSingleThreadExecutor("StartTor"); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public void start(@Nullable SetupListener setupListener) { if (setupListener != null) addSetupListener(setupListener); createTorAndHiddenService(); } public void shutDown(@Nullable Runnable shutDownCompleteHandler) { super.shutDown(shutDownCompleteHandler); } public abstract Socks5Proxy getSocksProxy(); protected abstract Socket createSocket(NodeAddress peerNodeAddress) throws IOException; protected abstract void createTorAndHiddenService(); } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/TorNetworkNodeDirectBind.java ================================================ package haveno.network.p2p.network; import haveno.common.util.Hex; import haveno.network.p2p.NodeAddress; import haveno.common.UserThread; import haveno.common.proto.network.NetworkProtoResolver; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import java.net.Socket; import java.net.InetAddress; import java.net.ServerSocket; import java.io.IOException; import java.nio.charset.StandardCharsets; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.Nullable; import static com.google.common.base.Preconditions.checkArgument; @Slf4j public class TorNetworkNodeDirectBind extends TorNetworkNode { private static final int TOR_DATA_PORT = 9050; // TODO: config option? private final String serviceAddress; public TorNetworkNodeDirectBind(int servicePort, NetworkProtoResolver networkProtoResolver, @Nullable BanFilter banFilter, int maxConnections, String hiddenServiceAddress) { super(servicePort, networkProtoResolver, banFilter, maxConnections); this.serviceAddress = hiddenServiceAddress; } @Override public void shutDown(@Nullable Runnable shutDownCompleteHandler) { super.shutDown(() -> { log.info("TorNetworkNodeDirectBind shutdown completed"); if (shutDownCompleteHandler != null) shutDownCompleteHandler.run(); }); } @Override public Socks5Proxy getSocksProxy() { Socks5Proxy proxy = new Socks5Proxy(InetAddress.getLoopbackAddress(), TOR_DATA_PORT); proxy.resolveAddrLocally(false); return proxy; } @Override protected Socket createSocket(NodeAddress peerNodeAddress) throws IOException { // https://datatracker.ietf.org/doc/html/rfc1928 SOCKS5 Protocol try { checkArgument(peerNodeAddress.getHostName().endsWith(".onion"), "PeerAddress is not an onion address"); Socket sock = new Socket(InetAddress.getLoopbackAddress(), TOR_DATA_PORT); sock.getOutputStream().write(Hex.decode("050100")); String response = Hex.encode(sock.getInputStream().readNBytes(2)); if (!response.equalsIgnoreCase("0500")) { return null; } String connect_details = "050100033E" + Hex.encode(peerNodeAddress.getHostName().getBytes(StandardCharsets.UTF_8)); StringBuilder connect_port = new StringBuilder(Integer.toHexString(peerNodeAddress.getPort())); while (connect_port.length() < 4) connect_port.insert(0, "0"); connect_details = connect_details + connect_port; sock.getOutputStream().write(Hex.decode(connect_details)); response = Hex.encode(sock.getInputStream().readNBytes(10)); if (response.substring(0, 2).equalsIgnoreCase("05") && response.substring(2, 4).equalsIgnoreCase("00")) { return sock; // success } if (response.substring(2, 4).equalsIgnoreCase("04")) { log.warn("Host unreachable: {}", peerNodeAddress); } else { log.warn("SOCKS error code received {} expected 00", response.substring(2, 4)); } if (!response.substring(0, 2).equalsIgnoreCase("05")) { log.warn("unexpected response, this isn't a SOCKS5 proxy?: {} {}", response, response.substring(0, 2)); } } catch (Exception e) { log.warn(e.toString()); } throw new IOException("createSocket failed"); } @Override protected void createTorAndHiddenService() { executor.submit(() -> { try { // listener for incoming messages at the hidden service ServerSocket socket = new ServerSocket(servicePort); nodeAddressProperty.set(new NodeAddress(serviceAddress + ":" + servicePort)); log.info("\n################################################################\n" + "Bound to Tor hidden service: {} Port: {}\n" + "################################################################", serviceAddress, servicePort); UserThread.execute(() -> setupListeners.forEach(SetupListener::onTorNodeReady)); UserThread.runAfter(() -> { nodeAddressProperty.set(new NodeAddress(serviceAddress + ":" + servicePort)); startServer(socket); setupListeners.forEach(SetupListener::onHiddenServicePublished); }, 3); return null; } catch (IOException e) { log.error("Could not connect to external Tor", e); UserThread.execute(() -> setupListeners.forEach(s -> s.onSetupFailed(new RuntimeException(e.getMessage())))); } return null; }); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/network/TorNetworkNodeNetlayer.java ================================================ package haveno.network.p2p.network; import haveno.common.Timer; import haveno.network.p2p.NodeAddress; import haveno.common.UserThread; import haveno.common.proto.network.NetworkProtoResolver; import haveno.network.utils.Utils; import org.berndpruenster.netlayer.tor.HiddenServiceSocket; import org.berndpruenster.netlayer.tor.Tor; import org.berndpruenster.netlayer.tor.TorCtlException; import org.berndpruenster.netlayer.tor.TorSocket; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import java.security.SecureRandom; import java.net.Socket; import java.io.IOException; import java.util.Base64; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.Nullable; import static com.google.common.base.Preconditions.checkArgument; @Slf4j public class TorNetworkNodeNetlayer extends TorNetworkNode { private static final long SHUT_DOWN_TIMEOUT = 2; private HiddenServiceSocket hiddenServiceSocket; private boolean streamIsolation; private Socks5Proxy socksProxy; protected TorMode torMode; private Tor tor; private final String torControlHost; private Timer shutDownTimeoutTimer; private boolean isShutDownStarted; private boolean isShutDownComplete; public TorNetworkNodeNetlayer(int servicePort, NetworkProtoResolver networkProtoResolver, TorMode torMode, @Nullable BanFilter banFilter, int maxConnections, boolean useStreamIsolation, String torControlHost) { super(servicePort, networkProtoResolver, banFilter, maxConnections); this.torControlHost = torControlHost; this.streamIsolation = useStreamIsolation; this.torMode = torMode; } @Override public void start(@Nullable SetupListener setupListener) { torMode.doRollingBackup(); super.start(setupListener); } @Override public void shutDown(@Nullable Runnable shutDownCompleteHandler) { log.info("TorNetworkNodeNetlayer shutdown started"); if (isShutDownComplete) { log.info("TorNetworkNodeNetlayer shutdown already completed"); if (shutDownCompleteHandler != null) shutDownCompleteHandler.run(); return; } if (isShutDownStarted) { log.warn("Ignoring request to shut down because shut down already started"); return; } isShutDownStarted = true; shutDownTimeoutTimer = UserThread.runAfter(() -> { log.error("A timeout occurred at shutDown"); isShutDownComplete = true; if (shutDownCompleteHandler != null) shutDownCompleteHandler.run(); executor.shutdownNow(); }, SHUT_DOWN_TIMEOUT); super.shutDown(() -> { try { tor = Tor.getDefault(); if (tor != null) { tor.shutdown(); tor = null; log.info("Tor shutdown completed"); } executor.shutdownNow(); } catch (Throwable e) { log.error("Shutdown TorNetworkNodeNetlayer failed with exception", e); } finally { shutDownTimeoutTimer.stop(); isShutDownComplete = true; if (shutDownCompleteHandler != null) shutDownCompleteHandler.run(); } }); } @Override protected Socket createSocket(NodeAddress peerNodeAddress) throws IOException { checkArgument(peerNodeAddress.getHostName().endsWith(".onion"), "PeerAddress is not an onion address"); // If streamId is null stream isolation gets deactivated. // Hidden services use stream isolation by default, so we pass null. return new TorSocket(peerNodeAddress.getHostName(), peerNodeAddress.getPort(), torControlHost, null); } @Override public Socks5Proxy getSocksProxy() { try { String stream = null; if (streamIsolation) { byte[] bytes = new byte[512]; // tor.getProxy creates a Sha256 hash new SecureRandom().nextBytes(bytes); stream = Base64.getEncoder().encodeToString(bytes); } if (socksProxy == null || streamIsolation) { tor = Tor.getDefault(); socksProxy = tor != null ? tor.getProxy(torControlHost, stream) : null; } return socksProxy; } catch (Throwable t) { log.error("Error at getSocksProxy", t); return null; } } @Override protected void createTorAndHiddenService() { int localPort = Utils.findFreeSystemPort(); executor.submit(() -> { try { Tor.setDefault(torMode.getTor()); long ts = System.currentTimeMillis(); hiddenServiceSocket = new HiddenServiceSocket(localPort, torMode.getHiddenServiceDirectory(), servicePort); nodeAddressProperty.set(new NodeAddress(hiddenServiceSocket.getServiceName() + ":" + hiddenServiceSocket.getHiddenServicePort())); UserThread.execute(() -> setupListeners.forEach(SetupListener::onTorNodeReady)); hiddenServiceSocket.addReadyListener(socket -> { log.info("\n################################################################\n" + "Tor hidden service published after {} ms. Socket={}\n" + "################################################################", System.currentTimeMillis() - ts, socket); UserThread.execute(() -> { nodeAddressProperty.set(new NodeAddress(hiddenServiceSocket.getServiceName() + ":" + hiddenServiceSocket.getHiddenServicePort())); startServer(socket); setupListeners.forEach(SetupListener::onHiddenServicePublished); }); return null; }); } catch (TorCtlException e) { log.error("Starting tor node failed", e); if (e.getCause() instanceof IOException) { UserThread.execute(() -> setupListeners.forEach(s -> s.onSetupFailed(new RuntimeException(e.getMessage())))); } else { UserThread.execute(() -> setupListeners.forEach(SetupListener::onRequestCustomBridges)); log.warn("We shutdown as starting tor with the default bridges failed. We request user to add custom bridges."); shutDown(null); } } catch (IOException e) { log.error("Could not connect to running Tor", e); UserThread.execute(() -> setupListeners.forEach(s -> s.onSetupFailed(new RuntimeException(e.getMessage())))); } catch (Throwable ignore) { } return null; }); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/peers/BroadcastHandler.java ================================================ /* * 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 . */ package haveno.network.p2p.peers; import haveno.network.p2p.BundleOfEnvelopes; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.NetworkNode; import haveno.network.p2p.storage.messages.BroadcastMessage; import haveno.common.Timer; import haveno.common.UserThread; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @Slf4j public class BroadcastHandler implements PeerManager.Listener { private static final long BASE_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(120); /////////////////////////////////////////////////////////////////////////////////////////// // Listener /////////////////////////////////////////////////////////////////////////////////////////// interface ResultHandler { void onCompleted(BroadcastHandler broadcastHandler); } public interface Listener { void onSufficientlyBroadcast(List broadcastRequests); void onNotSufficientlyBroadcast(int numOfCompletedBroadcasts, int numOfFailedBroadcast); } /////////////////////////////////////////////////////////////////////////////////////////// // Instance fields /////////////////////////////////////////////////////////////////////////////////////////// private final NetworkNode networkNode; private final PeerManager peerManager; @Nullable private final ResultHandler resultHandler; private final String uid; private final AtomicBoolean stopped = new AtomicBoolean(); private final AtomicBoolean timeoutTriggered = new AtomicBoolean(); private final AtomicInteger numOfCompletedBroadcasts = new AtomicInteger(); private final AtomicInteger numOfFailedBroadcasts = new AtomicInteger(); private final AtomicInteger numPeersForBroadcast = new AtomicInteger(); @Nullable private Timer timeoutTimer; private final Set> sendMessageFutures = new CopyOnWriteArraySet<>(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// BroadcastHandler(NetworkNode networkNode, PeerManager peerManager, ResultHandler resultHandler) { this.networkNode = networkNode; this.peerManager = peerManager; this.resultHandler = resultHandler; uid = UUID.randomUUID().toString(); peerManager.addListener(this); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void broadcast(List broadcastRequests, boolean shutDownRequested, ListeningExecutorService executor) { if (broadcastRequests.isEmpty()) { return; } List confirmedConnections = new ArrayList<>(networkNode.getConfirmedConnections()); Collections.shuffle(confirmedConnections); int delay; if (shutDownRequested) { delay = 1; // We sent to all peers as in case we had offers we want that it gets removed with higher reliability numPeersForBroadcast.set(confirmedConnections.size()); } else { if (requestsContainOwnMessage(broadcastRequests)) { // The broadcastRequests contains at least 1 message we have originated, so we send to all peers and with shorter delay numPeersForBroadcast.set(confirmedConnections.size()); delay = 50; } else { // Relay nodes only send to max 7 peers and with longer delay numPeersForBroadcast.set(Math.min(7, confirmedConnections.size())); delay = 100; } } setupTimeoutHandler(broadcastRequests, delay, shutDownRequested); int iterations = numPeersForBroadcast.get(); for (int i = 0; i < iterations; i++) { long minDelay = (i + 1) * delay; long maxDelay = (i + 2) * delay; Connection connection = confirmedConnections.get(i); UserThread.runAfterRandomDelay(() -> { if (stopped.get()) { return; } // We use broadcastRequests which have excluded the requests for messages the connection has // originated to avoid sending back the message we received. We also remove messages not satisfying // capability checks. List broadcastRequestsForConnection = getBroadcastRequestsForConnection( connection, broadcastRequests); // Could be empty list... if (broadcastRequestsForConnection.isEmpty()) { // We decrease numPeers in that case for making completion checks correct. if (numPeersForBroadcast.get() > 0) { numPeersForBroadcast.decrementAndGet(); } checkForCompletion(); return; } if (connection.isStopped()) { // Connection has died in the meantime. We skip it. // We decrease numPeers in that case for making completion checks correct. if (numPeersForBroadcast.get() > 0) { numPeersForBroadcast.decrementAndGet(); } checkForCompletion(); return; } try { sendToPeer(connection, broadcastRequestsForConnection, executor); } catch (RejectedExecutionException e) { log.error("RejectedExecutionException at broadcast ", e); cleanup(); } }, minDelay, maxDelay, TimeUnit.MILLISECONDS); } } public void cancel() { cleanup(); } /////////////////////////////////////////////////////////////////////////////////////////// // PeerManager.Listener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onAllConnectionsLost() { cleanup(); } @Override public void onNewConnectionAfterAllConnectionsLost() { } @Override public void onAwakeFromStandby() { } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// // Check if we have at least one message originated by ourselves private boolean requestsContainOwnMessage(List broadcastRequests) { NodeAddress myAddress = networkNode.getNodeAddress(); if (myAddress == null) return false; return broadcastRequests.stream().anyMatch(e -> myAddress.equals(e.getSender())); } private void setupTimeoutHandler(List broadcastRequests, int delay, boolean shutDownRequested) { // In case of shutdown we try to complete fast and set a short 1 second timeout long baseTimeoutMs = shutDownRequested ? TimeUnit.SECONDS.toMillis(1) : BASE_TIMEOUT_MS; long timeoutDelay = baseTimeoutMs + delay * (numPeersForBroadcast.get() + 1); // We added 1 in the loop timeoutTimer = UserThread.runAfter(() -> { if (stopped.get()) { return; } timeoutTriggered.set(true); numOfFailedBroadcasts.incrementAndGet(); log.warn("Broadcast did not complete after {} sec.\n" + "numPeersForBroadcast={}\n" + "numOfCompletedBroadcasts={}\n" + "numOfFailedBroadcasts={}", timeoutDelay / 1000d, numPeersForBroadcast, numOfCompletedBroadcasts, numOfFailedBroadcasts); maybeNotifyListeners(broadcastRequests); cleanup(); }, timeoutDelay, TimeUnit.MILLISECONDS); } // We exclude the requests containing a message we received from that connection // Also we filter out messages which requires a capability but peer does not // support it. private List getBroadcastRequestsForConnection(Connection connection, List broadcastRequests) { return broadcastRequests.stream() .filter(broadcastRequest -> !connection.getPeersNodeAddressOptional().isPresent() || !connection.getPeersNodeAddressOptional().get().equals(broadcastRequest.getSender())) .filter(broadcastRequest -> connection.testCapability(broadcastRequest.getMessage())) .collect(Collectors.toList()); } private void sendToPeer(Connection connection, List broadcastRequestsForConnection, ListeningExecutorService executor) { // Can be BundleOfEnvelopes or a single BroadcastMessage BroadcastMessage broadcastMessage = getMessage(broadcastRequestsForConnection); SettableFuture future = networkNode.sendMessage(connection, broadcastMessage, executor); sendMessageFutures.add(future); Futures.addCallback(future, new FutureCallback<>() { @Override public void onSuccess(Connection connection) { numOfCompletedBroadcasts.incrementAndGet(); if (stopped.get()) { return; } maybeNotifyListeners(broadcastRequestsForConnection); checkForCompletion(); } @Override public void onFailure(@NotNull Throwable throwable) { if (stopped.get()) { return; } log.warn("Broadcast to " + connection.getPeersNodeAddressOptional() + " failed. ", throwable); numOfFailedBroadcasts.incrementAndGet(); maybeNotifyListeners(broadcastRequestsForConnection); checkForCompletion(); } }, MoreExecutors.directExecutor()); } private BroadcastMessage getMessage(List broadcastRequests) { if (broadcastRequests.size() == 1) { // If we only have 1 message we avoid the overhead of the BundleOfEnvelopes and send the message directly return broadcastRequests.get(0).getMessage(); } else { return new BundleOfEnvelopes(broadcastRequests.stream() .map(Broadcaster.BroadcastRequest::getMessage) .collect(Collectors.toList())); } } private void maybeNotifyListeners(List broadcastRequests) { int numOfCompletedBroadcastsTarget = Math.max(1, Math.min(numPeersForBroadcast.get(), 3)); // We use equal checks to avoid duplicated listener calls as it would be the // case with >= checks. if (numOfCompletedBroadcasts.get() == numOfCompletedBroadcastsTarget) { // We have heard back from 3 peers (or all peers if numPeers is lower) so we // consider the message was sufficiently broadcast. broadcastRequests.stream() .map(Broadcaster.BroadcastRequest::getListener) .filter(Objects::nonNull) .forEach(listener -> listener.onSufficientlyBroadcast(broadcastRequests)); } else { // We check if number of open requests to peers is less than we need to reach numOfCompletedBroadcastsTarget. // Thus we never can reach required resilience as too many numOfFailedBroadcasts occurred. int maxPossibleSuccessCases = numPeersForBroadcast.get() - numOfFailedBroadcasts.get(); // We subtract 1 as we want to have it called only once, with a < comparision we would trigger repeatedly. boolean notEnoughSucceededOrOpen = maxPossibleSuccessCases == numOfCompletedBroadcastsTarget - 1; // We did not reach resilience level and timeout prevents to reach it later boolean timeoutAndNotEnoughSucceeded = timeoutTriggered.get() && numOfCompletedBroadcasts.get() < numOfCompletedBroadcastsTarget; if (notEnoughSucceededOrOpen || timeoutAndNotEnoughSucceeded) { broadcastRequests.stream() .map(Broadcaster.BroadcastRequest::getListener) .filter(Objects::nonNull) .forEach(listener -> listener.onNotSufficientlyBroadcast(numOfCompletedBroadcasts.get(), numOfFailedBroadcasts.get())); } } } private void checkForCompletion() { if (numOfCompletedBroadcasts.get() + numOfFailedBroadcasts.get() == numPeersForBroadcast.get()) { cleanup(); } } private void cleanup() { if (stopped.get()) { return; } stopped.set(true); if (timeoutTimer != null) { timeoutTimer.stop(); timeoutTimer = null; } sendMessageFutures.stream() .filter(future -> !future.isCancelled() && !future.isDone()) .forEach(future -> { try { future.cancel(true); } catch (Exception e) { if (networkNode.isShutDownStarted()) return; // ignore if shut down throw e; } }); sendMessageFutures.clear(); peerManager.removeListener(this); resultHandler.onCompleted(this); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof BroadcastHandler)) return false; BroadcastHandler that = (BroadcastHandler) o; return uid.equals(that.uid); } @Override public int hashCode() { return uid.hashCode(); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/peers/Broadcaster.java ================================================ /* * 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 . */ package haveno.network.p2p.peers; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.config.Config; import haveno.common.util.Utilities; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.network.NetworkNode; import haveno.network.p2p.storage.messages.BroadcastMessage; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import lombok.Value; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.Nullable; @Slf4j public class Broadcaster implements BroadcastHandler.ResultHandler { private static final long BROADCAST_INTERVAL_MS = 2000; private final NetworkNode networkNode; private final PeerManager peerManager; private final Set broadcastHandlers = new CopyOnWriteArraySet<>(); private final List broadcastRequests = new ArrayList<>(); private Timer timer; private boolean shutDownRequested; private Runnable shutDownResultHandler; private final ListeningExecutorService executor; private final Object lock = new Object(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public Broadcaster(NetworkNode networkNode, PeerManager peerManager, @Named(Config.MAX_CONNECTIONS) int maxConnections) { this.networkNode = networkNode; this.peerManager = peerManager; ThreadPoolExecutor threadPoolExecutor = Utilities.getThreadPoolExecutor("Broadcaster", maxConnections * 3, maxConnections * 4, 30, 30); executor = MoreExecutors.listeningDecorator(threadPoolExecutor); } public void shutDown(Runnable resultHandler) { log.info("Broadcaster shutdown started"); shutDownRequested = true; shutDownResultHandler = resultHandler; synchronized (lock) { if (broadcastRequests.isEmpty()) { doShutDown(); } else { // We set delay of broadcasts and timeout to very low values, // so we can expect that we get onCompleted called very fast and trigger the // doShutDown from there. maybeBroadcastBundle(); } } executor.shutdown(); } public void flush() { maybeBroadcastBundle(); } private void doShutDown() { log.info("Broadcaster doShutDown started"); synchronized (lock) { broadcastHandlers.forEach(BroadcastHandler::cancel); if (timer != null) { timer.stop(); } } shutDownResultHandler.run(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void broadcast(BroadcastMessage message, @Nullable NodeAddress sender) { broadcast(message, sender, null); } public void broadcast(BroadcastMessage message, @Nullable NodeAddress sender, @Nullable BroadcastHandler.Listener listener) { synchronized (lock) { broadcastRequests.add(new BroadcastRequest(message, sender, listener)); if (timer == null) { timer = UserThread.runAfter(this::maybeBroadcastBundle, BROADCAST_INTERVAL_MS, TimeUnit.MILLISECONDS); } } } private void maybeBroadcastBundle() { synchronized (lock) { if (!broadcastRequests.isEmpty()) { BroadcastHandler broadcastHandler = new BroadcastHandler(networkNode, peerManager, this); broadcastHandlers.add(broadcastHandler); broadcastHandler.broadcast(new ArrayList<>(broadcastRequests), shutDownRequested, executor); broadcastRequests.clear(); if (timer != null) { timer.stop(); } timer = null; } } } /////////////////////////////////////////////////////////////////////////////////////////// // BroadcastHandler.ResultHandler implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onCompleted(BroadcastHandler broadcastHandler) { synchronized (lock) { broadcastHandlers.remove(broadcastHandler); if (shutDownRequested) { doShutDown(); } } } /////////////////////////////////////////////////////////////////////////////////////////// // BroadcastRequest class /////////////////////////////////////////////////////////////////////////////////////////// @Value public static class BroadcastRequest { private BroadcastMessage message; @Nullable private NodeAddress sender; @Nullable private BroadcastHandler.Listener listener; private BroadcastRequest(BroadcastMessage message, @Nullable NodeAddress sender, @Nullable BroadcastHandler.Listener listener) { this.message = message; this.sender = sender; this.listener = listener; } } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/peers/PeerManager.java ================================================ /* * 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 . */ package haveno.network.p2p.peers; import com.google.common.annotations.VisibleForTesting; import static com.google.common.base.Preconditions.checkArgument; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.ClockWatcher; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.app.Capabilities; import haveno.common.app.Capability; import haveno.common.config.Config; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.persistable.PersistedDataHost; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.network.CloseConnectionReason; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.ConnectionListener; import haveno.network.p2p.network.InboundConnection; import haveno.network.p2p.network.NetworkNode; import haveno.network.p2p.network.PeerType; import haveno.network.p2p.network.RuleViolation; import haveno.network.p2p.peers.peerexchange.Peer; import haveno.network.p2p.peers.peerexchange.PeerList; import haveno.network.p2p.seed.SeedNodeRepository; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Random; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import javax.annotation.Nullable; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @Slf4j public final class PeerManager implements ConnectionListener, PersistedDataHost { /////////////////////////////////////////////////////////////////////////////////////////// // Static /////////////////////////////////////////////////////////////////////////////////////////// private static final long CHECK_MAX_CONN_DELAY_SEC = 10; // Use a long delay as the bootstrapping peer might need a while until it knows its onion address private static final long REMOVE_ANONYMOUS_PEER_SEC = 240; private static final int MAX_REPORTED_PEERS = 1000; private static final int MAX_PERSISTED_PEERS = 500; // max age for reported peers is 14 days private static final long MAX_AGE = TimeUnit.DAYS.toMillis(14); // Age of what we consider connected peers still as live peers private static final long MAX_AGE_LIVE_PEERS = TimeUnit.MINUTES.toMillis(30); private static final boolean PRINT_REPORTED_PEERS_DETAILS = true; private Timer printStatisticsTimer; private boolean shutDownRequested; private int numOnConnections; /////////////////////////////////////////////////////////////////////////////////////////// // Listener /////////////////////////////////////////////////////////////////////////////////////////// public interface Listener { void onAllConnectionsLost(); void onNewConnectionAfterAllConnectionsLost(); void onAwakeFromStandby(); } /////////////////////////////////////////////////////////////////////////////////////////// // Instance fields /////////////////////////////////////////////////////////////////////////////////////////// private final NetworkNode networkNode; private final ClockWatcher clockWatcher; private final Set seedNodeAddresses; private final PersistenceManager persistenceManager; private final ClockWatcher.Listener clockWatcherListener; private final List listeners = new CopyOnWriteArrayList<>(); // Persistable peerList private final PeerList peerList = new PeerList(); // Peers we got reported from other peers @Getter private final Set reportedPeers = new HashSet<>(); // Most recent peers with activity date of last 30 min. private final Set latestLivePeers = new HashSet<>(); private Timer checkMaxConnectionsTimer; private boolean stopped; private boolean lostAllConnections; private int maxConnections; @Getter private int minConnections; private int outBoundPeerTrigger; private int initialDataExchangeTrigger; private int maxConnectionsAbsolute; @Getter private int peakNumConnections; @Getter private int numAllConnectionsLostEvents; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public PeerManager(NetworkNode networkNode, SeedNodeRepository seedNodeRepository, ClockWatcher clockWatcher, PersistenceManager persistenceManager, @Named(Config.MAX_CONNECTIONS) int maxConnections) { this.networkNode = networkNode; this.seedNodeAddresses = new HashSet<>(seedNodeRepository.getSeedNodeAddresses()); this.clockWatcher = clockWatcher; this.persistenceManager = persistenceManager; this.persistenceManager.initialize(peerList, PersistenceManager.Source.PRIVATE_LOW_PRIO); this.networkNode.addConnectionListener(this); setConnectionLimits(maxConnections); // we check if app was idle for more then 5 sec. clockWatcherListener = new ClockWatcher.Listener() { @Override public void onSecondTick() { } @Override public void onMinuteTick() { } @Override public void onAwakeFromStandby(long missedMs) { // We got probably stopped set to true when we got a longer interruption (e.g. lost all connections), // now we get awake again, so set stopped to false. stopped = false; synchronized (listeners) { listeners.forEach(Listener::onAwakeFromStandby); } } }; clockWatcher.addListener(clockWatcherListener); printStatisticsTimer = UserThread.runPeriodically(this::printStatistics, TimeUnit.MINUTES.toSeconds(60)); } public void shutDown() { shutDownRequested = true; networkNode.removeConnectionListener(this); clockWatcher.removeListener(clockWatcherListener); stopCheckMaxConnectionsTimer(); if (printStatisticsTimer != null) { printStatisticsTimer.stop(); printStatisticsTimer = null; } } /////////////////////////////////////////////////////////////////////////////////////////// // PersistedDataHost implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void readPersisted(Runnable completeHandler) { persistenceManager.readPersisted(persisted -> { peerList.setAll(persisted.getSet()); completeHandler.run(); }, completeHandler); } /////////////////////////////////////////////////////////////////////////////////////////// // ConnectionListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onConnection(Connection connection) { connection.getConnectionState().setSeedNode(isSeedNode(connection)); doHouseKeeping(); numOnConnections++; if (lostAllConnections) { lostAllConnections = false; stopped = false; log.info("\n------------------------------------------------------------\n" + "Established a new connection from/to {} after all connections lost.\n" + "------------------------------------------------------------", connection.getPeersNodeAddressOptional()); synchronized (listeners) { listeners.forEach(Listener::onNewConnectionAfterAllConnectionsLost); } } connection.getPeersNodeAddressOptional() .flatMap(this::findPeer) .ifPresent(Peer::onConnection); } @Override public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { log.debug("onDisconnect called: nodeAddress={}, closeConnectionReason={}", connection.getPeersNodeAddressOptional(), closeConnectionReason); handleConnectionFault(connection); boolean previousLostAllConnections = lostAllConnections; lostAllConnections = networkNode.getAllConnections().isEmpty(); // At start-up we ignore if we would lose a connection and would fall back to no connections if (lostAllConnections && numOnConnections > 2) { stopped = true; if (!shutDownRequested) { if (!previousLostAllConnections) { // If we enter to 'All connections lost' we count the event. numAllConnectionsLostEvents++; } log.warn("\n------------------------------------------------------------\n" + "All connections lost\n" + "------------------------------------------------------------"); synchronized (listeners) { listeners.forEach(Listener::onAllConnectionsLost); } } } maybeRemoveBannedPeer(closeConnectionReason, connection); } /////////////////////////////////////////////////////////////////////////////////////////// // Connection /////////////////////////////////////////////////////////////////////////////////////////// public boolean hasSufficientConnections() { return networkNode.getConfirmedConnections().size() >= minConnections; } // Checks if that connection has the peers node address public boolean isConfirmed(NodeAddress nodeAddress) { return networkNode.getNodeAddressesOfConfirmedConnections().contains(nodeAddress); } public void handleConnectionFault(Connection connection) { connection.getPeersNodeAddressOptional().ifPresent(nodeAddress -> handleConnectionFault(nodeAddress, connection)); } public void handleConnectionFault(NodeAddress nodeAddress) { handleConnectionFault(nodeAddress, null); } public void handleConnectionFault(NodeAddress nodeAddress, @Nullable Connection connection) { boolean doRemovePersistedPeer = false; removeReportedPeer(nodeAddress); Optional persistedPeerOptional = findPersistedPeer(nodeAddress); if (persistedPeerOptional.isPresent()) { Peer persistedPeer = persistedPeerOptional.get(); persistedPeer.onDisconnect(); doRemovePersistedPeer = persistedPeer.tooManyFailedConnectionAttempts(); } boolean ruleViolation = connection != null && connection.getRuleViolation() != null; doRemovePersistedPeer = doRemovePersistedPeer || ruleViolation; if (doRemovePersistedPeer) removePersistedPeer(nodeAddress); else removeTooOldPersistedPeers(); } public boolean isSeedNode(Connection connection) { return connection.getPeersNodeAddressOptional().isPresent() && isSeedNode(connection.getPeersNodeAddressOptional().get()); } public boolean isSelf(NodeAddress nodeAddress) { return nodeAddress.equals(networkNode.getNodeAddress()); } private boolean isSeedNode(Peer peer) { synchronized (seedNodeAddresses) { return seedNodeAddresses.contains(peer.getNodeAddress()); } } public boolean isSeedNode(NodeAddress nodeAddress) { synchronized (seedNodeAddresses) { return seedNodeAddresses.contains(nodeAddress); } } public boolean isPeerBanned(CloseConnectionReason closeConnectionReason, Connection connection) { return closeConnectionReason == CloseConnectionReason.PEER_BANNED && connection.getPeersNodeAddressOptional().isPresent(); } private void maybeRemoveBannedPeer(CloseConnectionReason closeConnectionReason, Connection connection) { if (connection.getPeersNodeAddressOptional().isPresent() && isPeerBanned(closeConnectionReason, connection)) { NodeAddress nodeAddress = connection.getPeersNodeAddressOptional().get(); removeSeedNodeAddress(nodeAddress); removePersistedPeer(nodeAddress); removeReportedPeer(nodeAddress); } } public void maybeResetNumAllConnectionsLostEvents() { if (!networkNode.getAllConnections().isEmpty()) { numAllConnectionsLostEvents = 0; } } /////////////////////////////////////////////////////////////////////////////////////////// // Peer /////////////////////////////////////////////////////////////////////////////////////////// @SuppressWarnings("unused") public Optional findPeer(NodeAddress peersNodeAddress) { return getAllPeers().stream() .filter(peer -> peer.getNodeAddress().equals(peersNodeAddress)) .findAny(); } public Set getAllPeers() { Set allPeers = null; synchronized (latestLivePeers) { allPeers = new HashSet<>(getLivePeers()); } synchronized (getPersistedPeers()) { allPeers.addAll(getPersistedPeers()); } synchronized (reportedPeers) { allPeers.addAll(reportedPeers); } return allPeers; } public Collection getPersistedPeers() { synchronized (peerList.getSet()) { return peerList.getSet(); } } public void addToReportedPeers(Set reportedPeersToAdd, Connection connection, Capabilities capabilities) { applyCapabilities(connection, capabilities); Set peers = reportedPeersToAdd.stream() .filter(peer -> !isSelf(peer.getNodeAddress())) .collect(Collectors.toSet()); printNewReportedPeers(peers); // We check if the reported msg is not violating our rules if (peers.size() <= (MAX_REPORTED_PEERS + maxConnectionsAbsolute + 10)) { synchronized (reportedPeers) { reportedPeers.addAll(peers); purgeReportedPeersIfExceeds(); synchronized (getPersistedPeers()) { getPersistedPeers().addAll(peers); purgePersistedPeersIfExceeds(); } requestPersistence(); printReportedPeers(); } } else { // If a node is trying to send too many list we treat it as rule violation. // Reported list include the connected list. We use the max value and give some extra headroom. // Will trigger a shutdown after 2nd time sending too much connection.reportInvalidRequest(RuleViolation.TOO_MANY_REPORTED_PEERS_SENT, "Too many reported peers sent"); } } // Delivers the live peers from the last 30 min (MAX_AGE_LIVE_PEERS) // We include older peers to avoid risks for network partitioning public Set getLivePeers() { return getLivePeers(null); } public Set getLivePeers(@Nullable NodeAddress excludedNodeAddress) { synchronized (latestLivePeers) { int oldNumLatestLivePeers = latestLivePeers.size(); Set peers = new HashSet<>(latestLivePeers); Set currentLivePeers = getConnectedReportedPeers().stream() .filter(e -> !isSeedNode(e)) .filter(e -> !e.getNodeAddress().equals(excludedNodeAddress)) .collect(Collectors.toSet()); peers.addAll(currentLivePeers); long maxAge = new Date().getTime() - MAX_AGE_LIVE_PEERS; latestLivePeers.clear(); Set recentPeers = peers.stream() .filter(peer -> peer.getDateAsLong() > maxAge) .collect(Collectors.toSet()); latestLivePeers.addAll(recentPeers); if (oldNumLatestLivePeers != latestLivePeers.size()) log.info("Num of latestLivePeers={}", latestLivePeers.size()); return latestLivePeers; } } /////////////////////////////////////////////////////////////////////////////////////////// // Capabilities /////////////////////////////////////////////////////////////////////////////////////////// public boolean peerHasCapability(NodeAddress peersNodeAddress, Capability capability) { return findPeersCapabilities(peersNodeAddress) .map(capabilities -> capabilities.contains(capability)) .orElse(false); } public Optional findPeersCapabilities(NodeAddress nodeAddress) { // We look up first our connections as that is our own data. If not found there we look up the peers which // include reported peers. Optional optionalCapabilities = networkNode.findPeersCapabilities(nodeAddress); if (optionalCapabilities.isPresent() && !optionalCapabilities.get().isEmpty()) { return optionalCapabilities; } // Reported peers are not trusted data. We could get capabilities which miss the // peers real capability or we could get maliciously altered capabilities telling us the peer supports a // capability which is in fact not supported. This could lead to connection loss as we might send data not // recognized by the peer. As we register a listener on connection if we don't have set the capability from our // own sources we would get it fixed as soon we have a connection with that peer, rendering such an attack // inefficient. // Also this risk is only for not updated peers, so in case that would be abused for an // attack all users have a strong incentive to update ;-). return getAllPeers().stream() .filter(peer -> peer.getNodeAddress().equals(nodeAddress)) .findAny() .map(Peer::getCapabilities); } private void applyCapabilities(Connection connection, Capabilities newCapabilities) { if (newCapabilities == null || newCapabilities.isEmpty()) { return; } connection.getPeersNodeAddressOptional().ifPresent(nodeAddress -> { getAllPeers().stream() .filter(peer -> peer.getNodeAddress().equals(nodeAddress)) .filter(peer -> peer.getCapabilities().hasLess(newCapabilities)) .forEach(peer -> peer.setCapabilities(newCapabilities)); }); requestPersistence(); } /////////////////////////////////////////////////////////////////////////////////////////// // Housekeeping /////////////////////////////////////////////////////////////////////////////////////////// private void doHouseKeeping() { if (checkMaxConnectionsTimer == null) { printConnectedPeers(); checkMaxConnectionsTimer = UserThread.runAfter(() -> { stopCheckMaxConnectionsTimer(); if (!stopped) { Set allConnections = new HashSet<>(networkNode.getAllConnections()); int size = allConnections.size(); peakNumConnections = Math.max(peakNumConnections, size); removeAnonymousPeers(); removeTooOldReportedPeers(); removeTooOldPersistedPeers(); checkMaxConnections(); } else { log.debug("We have stopped already. We ignore that checkMaxConnectionsTimer.run call."); } }, CHECK_MAX_CONN_DELAY_SEC); } } @VisibleForTesting boolean checkMaxConnections() { Set allConnections = new HashSet<>(networkNode.getAllConnections()); int size = allConnections.size(); log.info("We have {} connections open. Our limit is {}", size, maxConnections); if (size <= maxConnections) { log.debug("We have not exceeded the maxConnections limit of {} " + "so don't need to close any connections.", size); return false; } log.info("We have too many connections open. " + "Lets try first to remove the inbound connections of type PEER."); List candidates = allConnections.stream() .filter(e -> e instanceof InboundConnection) .filter(e -> e.getConnectionState().getPeerType() == PeerType.PEER) .sorted(Comparator.comparingLong(o -> o.getStatistic().getLastActivityTimestamp())) .collect(Collectors.toList()); if (candidates.isEmpty()) { log.info("No candidates found. We check if we exceed our " + "outBoundPeerTrigger of {}", outBoundPeerTrigger); if (size <= outBoundPeerTrigger) { log.info("We have not exceeded outBoundPeerTrigger of {} " + "so don't need to close any connections", outBoundPeerTrigger); return false; } log.info("We have exceeded outBoundPeerTrigger of {}. " + "Lets try to remove outbound connection of type PEER.", outBoundPeerTrigger); candidates = allConnections.stream() .filter(e -> e.getConnectionState().getPeerType() == PeerType.PEER) .sorted(Comparator.comparingLong(o -> o.getStatistic().getLastActivityTimestamp())) .collect(Collectors.toList()); if (candidates.isEmpty()) { log.info("No candidates found. We check if we exceed our " + "initialDataExchangeTrigger of {}", initialDataExchangeTrigger); if (size <= initialDataExchangeTrigger) { log.info("We have not exceeded initialDataExchangeTrigger of {} " + "so don't need to close any connections", initialDataExchangeTrigger); return false; } log.info("We have exceeded initialDataExchangeTrigger of {} " + "Lets try to remove the oldest INITIAL_DATA_EXCHANGE connection.", initialDataExchangeTrigger); candidates = allConnections.stream() .filter(e -> e.getConnectionState().getPeerType() == PeerType.INITIAL_DATA_EXCHANGE) .sorted(Comparator.comparingLong(o -> o.getConnectionState().getLastInitialDataMsgTimeStamp())) .collect(Collectors.toList()); if (candidates.isEmpty()) { log.info("No candidates found. We check if we exceed our " + "maxConnectionsAbsolute limit of {}", maxConnectionsAbsolute); if (size <= maxConnectionsAbsolute) { log.info("We have not exceeded maxConnectionsAbsolute limit of {} " + "so don't need to close any connections", maxConnectionsAbsolute); return false; } log.info("We reached abs. max. connections. Lets try to remove ANY connection."); candidates = allConnections.stream() .sorted(Comparator.comparingLong(o -> o.getStatistic().getLastActivityTimestamp())) .collect(Collectors.toList()); } } } if (!candidates.isEmpty()) { Connection connection = candidates.remove(0); log.info("checkMaxConnections: Num candidates (inbound/peer) for shut down={}. We close oldest connection to peer {}", candidates.size(), connection.getPeersNodeAddressOptional()); if (!connection.isStopped()) { connection.shutDown(CloseConnectionReason.TOO_MANY_CONNECTIONS_OPEN, () -> UserThread.runAfter(this::checkMaxConnections, 100, TimeUnit.MILLISECONDS)); return true; } } log.info("No candidates found to remove. " + "size={}, allConnections={}", size, allConnections); return false; } private void removeAnonymousPeers() { networkNode.getAllConnections().stream() .filter(connection -> !connection.hasPeersNodeAddress()) .filter(connection -> connection.getConnectionState().getPeerType() == PeerType.PEER) .forEach(connection -> UserThread.runAfter(() -> { // todo we keep a potentially dead connection in memory for too long... // We give 240 seconds delay and check again if still no address is set // Keep the delay long as we don't want to disconnect a peer in case we are a seed node just // because he needs longer for the HS publishing if (!connection.isStopped() && !connection.hasPeersNodeAddress()) { log.info("removeAnonymousPeers: We close the connection as the peer address is still unknown. " + "Peer: {}", connection.getPeersNodeAddressOptional()); connection.shutDown(CloseConnectionReason.UNKNOWN_PEER_ADDRESS); } }, REMOVE_ANONYMOUS_PEER_SEC)); } /////////////////////////////////////////////////////////////////////////////////////////// // Reported peers /////////////////////////////////////////////////////////////////////////////////////////// private void removeReportedPeer(Peer reportedPeer) { synchronized (reportedPeers) { reportedPeers.remove(reportedPeer); printReportedPeers(); } } private void removeReportedPeer(NodeAddress nodeAddress) { synchronized (reportedPeers) { List reportedPeersClone = new ArrayList<>(reportedPeers); reportedPeersClone.stream() .filter(e -> e.getNodeAddress().equals(nodeAddress)) .findAny() .ifPresent(this::removeReportedPeer); } } private void removeTooOldReportedPeers() { synchronized (reportedPeers) { List reportedPeersClone = new ArrayList<>(reportedPeers); Set reportedPeersToRemove = reportedPeersClone.stream() .filter(reportedPeer -> new Date().getTime() - reportedPeer.getDate().getTime() > MAX_AGE) .collect(Collectors.toSet()); reportedPeersToRemove.forEach(this::removeReportedPeer); } } private void purgeReportedPeersIfExceeds() { synchronized (reportedPeers) { int size = reportedPeers.size(); if (size > MAX_REPORTED_PEERS) { log.info("We have already {} reported peers which exceeds our limit of {}." + "We remove random peers from the reported peers list.", size, MAX_REPORTED_PEERS); int diff = size - MAX_REPORTED_PEERS; List list = new ArrayList<>(reportedPeers); // we don't use sorting by lastActivityDate to keep it more random for (int i = 0; i < diff; i++) { if (!list.isEmpty()) { Peer toRemove = list.remove(new Random().nextInt(list.size())); removeReportedPeer(toRemove); } } } else { log.trace("No need to purge reported peers.\n\tWe don't have more then {} reported peers yet.", MAX_REPORTED_PEERS); } } } private void printReportedPeers() { synchronized (reportedPeers) { if (!reportedPeers.isEmpty()) { if (PRINT_REPORTED_PEERS_DETAILS) { StringBuilder result = new StringBuilder("\n\n------------------------------------------------------------\n" + "Collected reported peers:"); List reportedPeersClone = new ArrayList<>(reportedPeers); reportedPeersClone.forEach(e -> result.append("\n").append(e)); result.append("\n------------------------------------------------------------\n"); log.trace(result.toString()); } log.debug("Number of reported peers: {}", reportedPeers.size()); } } } private void printNewReportedPeers(Set reportedPeers) { synchronized (reportedPeers) { if (PRINT_REPORTED_PEERS_DETAILS) { StringBuilder result = new StringBuilder("We received new reportedPeers:"); List reportedPeersClone = new ArrayList<>(reportedPeers); reportedPeersClone.forEach(e -> result.append("\n\t").append(e)); log.trace(result.toString()); } log.debug("Number of new arrived reported peers: {}", reportedPeers.size()); } } /////////////////////////////////////////////////////////////////////////////////////////// // Persisted peers /////////////////////////////////////////////////////////////////////////////////////////// private boolean removePersistedPeer(Peer persistedPeer) { synchronized (getPersistedPeers()) { if (getPersistedPeers().contains(persistedPeer)) { getPersistedPeers().remove(persistedPeer); requestPersistence(); return true; } else { return false; } } } private void requestPersistence() { persistenceManager.requestPersistence(); } private boolean removeSeedNodeAddress(NodeAddress nodeAddress) { synchronized (seedNodeAddresses) { return seedNodeAddresses.remove(nodeAddress); } } @SuppressWarnings("UnusedReturnValue") private boolean removePersistedPeer(NodeAddress nodeAddress) { Optional optionalPersistedPeer = findPersistedPeer(nodeAddress); return optionalPersistedPeer.isPresent() && removePersistedPeer(optionalPersistedPeer.get()); } private Optional findPersistedPeer(NodeAddress nodeAddress) { synchronized (getPersistedPeers()) { return getPersistedPeers().stream() .filter(e -> e.getNodeAddress().equals(nodeAddress)) .findAny(); } } private void removeTooOldPersistedPeers() { synchronized (getPersistedPeers()) { Set persistedPeersToRemove = getPersistedPeers().stream() .filter(reportedPeer -> new Date().getTime() - reportedPeer.getDate().getTime() > MAX_AGE) .collect(Collectors.toSet()); persistedPeersToRemove.forEach(this::removePersistedPeer); } } private void purgePersistedPeersIfExceeds() { synchronized (getPersistedPeers()) { int size = getPersistedPeers().size(); int limit = MAX_PERSISTED_PEERS; if (size > limit) { log.trace("We have already {} persisted peers which exceeds our limit of {}." + "We remove random peers from the persisted peers list.", size, limit); int diff = size - limit; List list = new ArrayList<>(getPersistedPeers()); // we don't use sorting by lastActivityDate to avoid attack vectors and keep it more random for (int i = 0; i < diff; i++) { if (!list.isEmpty()) { Peer toRemove = list.remove(new Random().nextInt(list.size())); removePersistedPeer(toRemove); } } } else { log.trace("No need to purge persisted peers.\n\tWe don't have more then {} persisted peers yet.", MAX_PERSISTED_PEERS); } } } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// public int getMaxConnections() { return maxConnections; } /////////////////////////////////////////////////////////////////////////////////////////// // Listeners /////////////////////////////////////////////////////////////////////////////////////////// public void addListener(Listener listener) { synchronized (listeners) { listeners.add(listener); } } public void removeListener(Listener listener) { synchronized (listeners) { listeners.remove(listener); } } /////////////////////////////////////////////////////////////////////////////////////////// // Private misc /////////////////////////////////////////////////////////////////////////////////////////// // Modify this to change the relationships between connection limits. // maxConnections default 12 private void setConnectionLimits(int maxConnections) { this.maxConnections = maxConnections; // app node 12; seedNode 20 minConnections = Math.max(1, (int) Math.round(maxConnections * 0.7)); // app node 8; seedNode 14 outBoundPeerTrigger = Math.max(4, (int) Math.round(maxConnections * 1.3)); // app node 16; seedNode 26 initialDataExchangeTrigger = Math.max(8, (int) Math.round(maxConnections * 1.7)); // app node 20; seedNode 34 maxConnectionsAbsolute = Math.max(12, (int) Math.round(maxConnections * 2.5)); // app node 30; seedNode 50 } private Set getConnectedReportedPeers() { // networkNode.getConfirmedConnections includes: // filter(connection -> connection.getPeersNodeAddressOptional().isPresent()) return networkNode.getConfirmedConnections().stream() .map((Connection connection) -> { Capabilities supportedCapabilities = new Capabilities(connection.getCapabilities()); // If we have a new connection the supportedCapabilities is empty. // We lookup if we have already stored the supportedCapabilities at the persisted or reported peers // and if so we use that. Optional peersNodeAddressOptional = connection.getPeersNodeAddressOptional(); checkArgument(peersNodeAddressOptional.isPresent()); // getConfirmedConnections delivers only connections where we know the address NodeAddress peersNodeAddress = peersNodeAddressOptional.get(); boolean capabilitiesNotFoundInConnection = supportedCapabilities.isEmpty(); if (capabilitiesNotFoundInConnection) { // If not found in connection we look up if we got the Capabilities set from any of the // reported or persisted peers Set persistedAndReported = null; synchronized (getPersistedPeers()) { persistedAndReported = new HashSet<>(getPersistedPeers()); } synchronized (reportedPeers) { persistedAndReported.addAll(reportedPeers); } Optional candidate = persistedAndReported.stream() .filter(peer -> peer.getNodeAddress().equals(peersNodeAddress)) .filter(peer -> !peer.getCapabilities().isEmpty()) .findAny(); if (candidate.isPresent()) { supportedCapabilities = new Capabilities(candidate.get().getCapabilities()); } } Peer peer = new Peer(peersNodeAddress, supportedCapabilities); // If we did not found the capability from our own connection we add a listener, // so once we get a connection with that peer and exchange a message containing the capabilities // we get set the capabilities. if (capabilitiesNotFoundInConnection) { connection.addWeakCapabilitiesListener(peer); } return peer; }) .collect(Collectors.toSet()); } private void stopCheckMaxConnectionsTimer() { if (checkMaxConnectionsTimer != null) { checkMaxConnectionsTimer.stop(); checkMaxConnectionsTimer = null; } } private void printStatistics() { String ls = System.lineSeparator(); StringBuilder sb = new StringBuilder("Connection statistics: " + ls); AtomicInteger counter = new AtomicInteger(); networkNode.getAllConnections().stream() .sorted(Comparator.comparingLong(o -> o.getConnectionStatistics().getConnectionCreationTimeStamp())) .forEach(e -> sb.append(ls).append("Connection ") .append(counter.incrementAndGet()).append(ls) .append(e.getConnectionStatistics().getInfo()).append(ls)); log.debug(sb.toString()); } private void printConnectedPeers() { if (!networkNode.getConfirmedConnections().isEmpty()) { StringBuilder result = new StringBuilder("\n\n------------------------------------------------------------\n" + "Connected peers for node " + networkNode.getNodeAddress() + ":"); networkNode.getConfirmedConnections().forEach(e -> result.append("\n") .append(e.getPeersNodeAddressOptional()).append(" ").append(e.getConnectionState().getPeerType())); result.append("\n------------------------------------------------------------\n"); log.debug(result.toString()); } } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/peers/getdata/GetDataRequestHandler.java ================================================ /* * 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 . */ package haveno.network.p2p.peers.getdata; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import haveno.common.Timer; import haveno.common.UserThread; import haveno.network.p2p.network.CloseConnectionReason; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.NetworkNode; import haveno.network.p2p.peers.getdata.messages.GetDataRequest; import haveno.network.p2p.peers.getdata.messages.GetDataResponse; import haveno.network.p2p.storage.P2PDataStorage; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @Slf4j public class GetDataRequestHandler { private static final long TIMEOUT = 240; private static final int MAX_ENTRIES = 5000; /////////////////////////////////////////////////////////////////////////////////////////// // Listener /////////////////////////////////////////////////////////////////////////////////////////// public interface Listener { void onComplete(int serializedSize); void onFault(String errorMessage, Connection connection); } /////////////////////////////////////////////////////////////////////////////////////////// // Class fields /////////////////////////////////////////////////////////////////////////////////////////// private final NetworkNode networkNode; private final P2PDataStorage dataStorage; private final Listener listener; private Timer timeoutTimer; private boolean stopped; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public GetDataRequestHandler(NetworkNode networkNode, P2PDataStorage dataStorage, Listener listener) { this.networkNode = networkNode; this.dataStorage = dataStorage; this.listener = listener; } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void handle(GetDataRequest getDataRequest, final Connection connection) { long ts = System.currentTimeMillis(); String connectionInfo = "connectionInfo" + connection.getPeersNodeAddressOptional() .map(e -> "node address " + e.getFullAddress()) .orElseGet(() -> "connection UID " + connection.getUid()); AtomicBoolean wasPersistableNetworkPayloadsTruncated = new AtomicBoolean(false); AtomicBoolean wasProtectedStorageEntriesTruncated = new AtomicBoolean(false); GetDataResponse getDataResponse = dataStorage.buildGetDataResponse( getDataRequest, MAX_ENTRIES, wasPersistableNetworkPayloadsTruncated, wasProtectedStorageEntriesTruncated, connection.getCapabilities()); if (wasPersistableNetworkPayloadsTruncated.get()) { log.info("The getDataResponse for peer {} got truncated.", connectionInfo); } if (wasProtectedStorageEntriesTruncated.get()) { log.info("The getDataResponse for peer {} got truncated.", connectionInfo); } log.info("The getDataResponse to peer with {} contains {} ProtectedStorageEntries and {} PersistableNetworkPayloads", connectionInfo, getDataResponse.getDataSet().size(), getDataResponse.getPersistableNetworkPayloadSet().size()); if (timeoutTimer == null) { timeoutTimer = UserThread.runAfter(() -> { // setup before sending to avoid race conditions String errorMessage = "A timeout occurred for getDataResponse " + " on connection:" + connection; handleFault(errorMessage, CloseConnectionReason.SEND_MSG_TIMEOUT, connection); }, TIMEOUT, TimeUnit.SECONDS); } SettableFuture future = networkNode.sendMessage(connection, getDataResponse); Futures.addCallback(future, new FutureCallback<>() { @Override public void onSuccess(Connection connection) { if (!stopped) { log.trace("Send DataResponse to {} succeeded. getDataResponse={}", connection.getPeersNodeAddressOptional(), getDataResponse); listener.onComplete(getDataResponse.toProtoNetworkEnvelope().getSerializedSize()); cleanup(); } else { log.trace("We have stopped already. We ignore that networkNode.sendMessage.onSuccess call."); } } @Override public void onFailure(@NotNull Throwable throwable) { if (!stopped) { String errorMessage = "Sending getDataResponse to " + connection + " failed. That is expected if the peer is offline. getDataResponse=" + getDataResponse + "." + "Exception: " + throwable.getMessage(); handleFault(errorMessage, CloseConnectionReason.SEND_MSG_FAILURE, connection); } else { log.trace("We have stopped already. We ignore that networkNode.sendMessage.onFailure call."); } } }, MoreExecutors.directExecutor()); log.info("handle GetDataRequest took {} ms", System.currentTimeMillis() - ts); } public void stop() { cleanup(); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void handleFault(String errorMessage, CloseConnectionReason closeConnectionReason, Connection connection) { if (!stopped) { log.info(errorMessage + "\n\tcloseConnectionReason=" + closeConnectionReason); cleanup(); listener.onFault(errorMessage, connection); } else { log.warn("We have already stopped (handleFault)"); } } private void cleanup() { stopped = true; if (timeoutTimer != null) { timeoutTimer.stop(); timeoutTimer = null; } } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/peers/getdata/RequestDataHandler.java ================================================ /* * 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 . */ package haveno.network.p2p.peers.getdata; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.proto.network.NetworkPayload; import haveno.common.util.Tuple2; import haveno.common.util.Utilities; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.network.CloseConnectionReason; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.MessageListener; import haveno.network.p2p.network.NetworkNode; import haveno.network.p2p.peers.PeerManager; import haveno.network.p2p.peers.getdata.messages.GetDataRequest; import haveno.network.p2p.peers.getdata.messages.GetDataResponse; import haveno.network.p2p.storage.P2PDataStorage; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.HashMap; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; @Slf4j class RequestDataHandler implements MessageListener { private static final long TIMEOUT = 240; private NodeAddress peersNodeAddress; private String getDataRequestType; /* */ /** * when we are run as a seed node, we spawn a RequestDataHandler every hour. However, we do not want to receive * {@link PersistableNetworkPayload}s (for now, as there are hardly any cases where such data goes out of sync). This * flag indicates whether we already received our first set of {@link PersistableNetworkPayload}s. *//* private static boolean firstRequest = true;*/ /////////////////////////////////////////////////////////////////////////////////////////// // Listener /////////////////////////////////////////////////////////////////////////////////////////// public interface Listener { void onComplete(boolean wasTruncated); @SuppressWarnings("UnusedParameters") void onFault(String errorMessage, @SuppressWarnings("SameParameterValue") @Nullable Connection connection); } /////////////////////////////////////////////////////////////////////////////////////////// // Class fields /////////////////////////////////////////////////////////////////////////////////////////// private final NetworkNode networkNode; private final P2PDataStorage dataStorage; private final PeerManager peerManager; private final Listener listener; private Timer timeoutTimer; private final int nonce = new Random().nextInt(); private boolean stopped; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// RequestDataHandler(NetworkNode networkNode, P2PDataStorage dataStorage, PeerManager peerManager, Listener listener) { this.networkNode = networkNode; this.dataStorage = dataStorage; this.peerManager = peerManager; this.listener = listener; } public void cancel() { cleanup(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// void requestData(NodeAddress nodeAddress, boolean isPreliminaryDataRequest) { peersNodeAddress = nodeAddress; if (!stopped) { GetDataRequest getDataRequest; if (isPreliminaryDataRequest) getDataRequest = dataStorage.buildPreliminaryGetDataRequest(nonce); else getDataRequest = dataStorage.buildGetUpdatedDataRequest(networkNode.getNodeAddress(), nonce); if (timeoutTimer == null) { timeoutTimer = UserThread.runAfter(() -> { // setup before sending to avoid race conditions if (!stopped) { String errorMessage = "A timeout occurred at sending getDataRequest:" + getDataRequest + " on nodeAddress:" + nodeAddress; log.debug(errorMessage + " / RequestDataHandler=" + RequestDataHandler.this); handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_TIMEOUT); } else { log.trace("We have stopped already. We ignore that timeoutTimer.run call. " + "Might be caused by a previous networkNode.sendMessage.onFailure."); } }, TIMEOUT); } getDataRequestType = getDataRequest.getClass().getSimpleName(); log.info("\n\n>> We send a {} to peer {}\n", getDataRequestType, nodeAddress); networkNode.addMessageListener(this); try { SettableFuture future = networkNode.sendMessage(nodeAddress, getDataRequest); //noinspection UnstableApiUsage Futures.addCallback(future, new FutureCallback<>() { @Override public void onSuccess(Connection connection) { if (!stopped) { log.trace("Send {} to {} succeeded.", getDataRequest, nodeAddress); } else { log.trace("We have stopped already. We ignore that networkNode.sendMessage.onSuccess call." + "Might be caused by a previous timeout."); } } @Override public void onFailure(@NotNull Throwable throwable) { if (!stopped) { String errorMessage = "Sending getDataRequest to " + nodeAddress + " failed. That is expected if the peer is offline.\n\t" + "getDataRequest=" + getDataRequest + "." + "\n\tException=" + throwable.getMessage(); handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_FAILURE); } else { log.trace("We have stopped already. We ignore that networkNode.sendMessage.onFailure call. " + "Might be caused by a previous timeout."); } } }, MoreExecutors.directExecutor()); } catch (Exception e) { if (!networkNode.isShutDownStarted()) throw e; } } else { log.warn("We have stopped already. We ignore that requestData call."); } } /////////////////////////////////////////////////////////////////////////////////////////// // MessageListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { if (networkEnvelope instanceof GetDataResponse) { if (connection.getPeersNodeAddressOptional().isPresent() && connection.getPeersNodeAddressOptional().get().equals(peersNodeAddress)) { if (!stopped) { long ts1 = System.currentTimeMillis(); GetDataResponse getDataResponse = (GetDataResponse) networkEnvelope; logContents(getDataResponse); if (getDataResponse.getRequestNonce() == nonce) { stopTimeoutTimer(); if (!connection.getPeersNodeAddressOptional().isPresent()) { log.error("RequestDataHandler.onMessage: connection.getPeersNodeAddressOptional() must be present " + "at that moment"); return; } dataStorage.processGetDataResponse(getDataResponse, connection.getPeersNodeAddressOptional().get()); cleanup(); listener.onComplete(getDataResponse.isWasTruncated()); } else { log.warn("Nonce not matching. That can happen rarely if we get a response after a canceled " + "handshake (timeout causes connection close but peer might have sent a msg before " + "connection was closed).\n\t" + "We drop that message. nonce={} / requestNonce={}", nonce, getDataResponse.getRequestNonce()); } log.info("Processing GetDataResponse took {} ms", System.currentTimeMillis() - ts1); } else { log.warn("We have stopped already. We ignore that onDataRequest call."); } } else { log.debug("We got the message from another connection and ignore it on that handler. That is expected if we have several requests open."); } } } public void stop() { cleanup(); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void logContents(GetDataResponse getDataResponse) { Set dataSet = getDataResponse.getDataSet(); Set persistableNetworkPayloadSet = getDataResponse.getPersistableNetworkPayloadSet(); Map> numPayloadsByClassName = new HashMap<>(); dataSet.forEach(protectedStorageEntry -> { String className = protectedStorageEntry.getProtectedStoragePayload().getClass().getSimpleName(); addDetails(numPayloadsByClassName, protectedStorageEntry, className); }); persistableNetworkPayloadSet.forEach(persistableNetworkPayload -> { String className = persistableNetworkPayload.getClass().getSimpleName(); addDetails(numPayloadsByClassName, persistableNetworkPayload, className); }); StringBuilder sb = new StringBuilder(); String sep = System.lineSeparator(); sb.append(sep).append("#################################################################").append(sep); sb.append("Data provided by node: ").append(peersNodeAddress.getFullAddress()).append(sep); int items = dataSet.size() + persistableNetworkPayloadSet.size(); sb.append("Received ").append(items).append(" instances from a ") .append(getDataRequestType).append(sep); numPayloadsByClassName.forEach((key, value) -> sb.append(key) .append(": ") .append(value.first.get()) .append(" / ") .append(Utilities.readableFileSize(value.second.get())) .append(sep)); sb.append("#################################################################\n"); log.info(sb.toString()); } private void addDetails(Map> numPayloadsByClassName, NetworkPayload networkPayload, String className) { numPayloadsByClassName.putIfAbsent(className, new Tuple2<>(new AtomicInteger(0), new AtomicInteger(0))); numPayloadsByClassName.get(className).first.getAndIncrement(); // toProtoMessage().getSerializedSize() is not very cheap. For about 1500 objects it takes about 20 ms // I think its justified to get accurate metrics but if it turns out to be a performance issue we might need // to remove it and use some more rough estimation by taking only the size of one data type and multiply it. numPayloadsByClassName.get(className).second.getAndAdd(networkPayload.toProtoMessage().getSerializedSize()); } @SuppressWarnings("UnusedParameters") private void handleFault(String errorMessage, NodeAddress nodeAddress, CloseConnectionReason closeConnectionReason) { cleanup(); log.info(errorMessage); //peerManager.shutDownConnection(nodeAddress, closeConnectionReason); peerManager.handleConnectionFault(nodeAddress); listener.onFault(errorMessage, null); } private void cleanup() { stopped = true; networkNode.removeMessageListener(this); stopTimeoutTimer(); } private void stopTimeoutTimer() { if (timeoutTimer != null) { timeoutTimer.stop(); timeoutTimer = null; } } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/peers/getdata/RequestDataManager.java ================================================ /* * 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 . */ package haveno.network.p2p.peers.getdata; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.Inject; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.app.Version; import haveno.common.proto.network.NetworkEnvelope; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.network.CloseConnectionReason; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.ConnectionListener; import haveno.network.p2p.network.MessageListener; import haveno.network.p2p.network.NetworkNode; import haveno.network.p2p.peers.PeerManager; import haveno.network.p2p.peers.getdata.messages.GetDataRequest; import haveno.network.p2p.peers.peerexchange.Peer; import haveno.network.p2p.seed.SeedNodeRepository; import haveno.network.p2p.storage.P2PDataStorage; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.Nullable; @Slf4j public class RequestDataManager implements MessageListener, ConnectionListener, PeerManager.Listener { private static final long RETRY_DELAY_SEC = 10; private static final long CLEANUP_TIMER = 120; // How many seeds we request the PreliminaryGetDataRequest from private static int NUM_SEEDS_FOR_PRELIMINARY_REQUEST = 2; // how many seeds additional to the first responding PreliminaryGetDataRequest seed we request the GetUpdatedDataRequest from private static int NUM_ADDITIONAL_SEEDS_FOR_UPDATE_REQUEST = 1; private static int MAX_REPEATED_REQUESTS = 30; private boolean isPreliminaryDataRequest = true; /////////////////////////////////////////////////////////////////////////////////////////// // Listener /////////////////////////////////////////////////////////////////////////////////////////// public interface Listener { void onPreliminaryDataReceived(); void onUpdatedDataReceived(); void onDataReceived(); default void onNoPeersAvailable() { } default void onNoSeedNodeAvailable() { } } public interface ResponseListener { void onSuccess(int serializedSize); void onFault(); } /////////////////////////////////////////////////////////////////////////////////////////// // Class fields /////////////////////////////////////////////////////////////////////////////////////////// private final NetworkNode networkNode; private final P2PDataStorage dataStorage; private final PeerManager peerManager; private final List seedNodeAddresses; private final List responseListeners = new CopyOnWriteArrayList<>(); // As we use Guice injection we cannot set the listener in our constructor but the P2PService calls the setListener // in it's constructor so we can guarantee it is not null. private Listener listener; private final Map handlerMap = new HashMap<>(); private final Map getDataRequestHandlers = new HashMap<>(); private Optional nodeAddressOfPreliminaryDataRequest = Optional.empty(); private Timer retryTimer; private boolean dataUpdateRequested; private boolean allDataReceived; private boolean stopped; private int numRepeatedRequests = 0; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public RequestDataManager(NetworkNode networkNode, SeedNodeRepository seedNodeRepository, P2PDataStorage dataStorage, PeerManager peerManager) { this.networkNode = networkNode; this.dataStorage = dataStorage; this.peerManager = peerManager; this.networkNode.addMessageListener(this); this.networkNode.addConnectionListener(this); this.peerManager.addListener(this); this.seedNodeAddresses = new ArrayList<>(seedNodeRepository.getSeedNodeAddresses()); // We shuffle only once so that we use the same seed nodes for preliminary and updated data requests. Collections.shuffle(seedNodeAddresses); this.networkNode.nodeAddressProperty().addListener((observable, oldValue, myAddress) -> { if (myAddress != null) { seedNodeAddresses.remove(myAddress); if (seedNodeRepository.isSeedNode(myAddress)) { NUM_SEEDS_FOR_PRELIMINARY_REQUEST = 3; NUM_ADDITIONAL_SEEDS_FOR_UPDATE_REQUEST = 2; MAX_REPEATED_REQUESTS = 100; } } }); } public void shutDown() { stopped = true; stopRetryTimer(); networkNode.removeMessageListener(this); networkNode.removeConnectionListener(this); peerManager.removeListener(this); closeAllHandlers(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// // We only support one listener as P2PService will manage calls on other clients in the correct order of execution. // The listener is set from the P2PService constructor so we can guarantee it is not null. public void setListener(Listener listener) { this.listener = listener; } public void requestPreliminaryData() { ArrayList nodeAddresses = new ArrayList<>(seedNodeAddresses); if (!nodeAddresses.isEmpty()) { ArrayList finalNodeAddresses = new ArrayList<>(nodeAddresses); final int size = Math.min(NUM_SEEDS_FOR_PRELIMINARY_REQUEST, finalNodeAddresses.size()); for (int i = 0; i < size; i++) { NodeAddress nodeAddress = finalNodeAddresses.get(i); nodeAddresses.remove(nodeAddress); // We clone list to avoid mutable change during iterations List remainingNodeAddresses = new ArrayList<>(nodeAddresses); UserThread.runAfter(() -> requestData(nodeAddress, remainingNodeAddresses), (i * 200 + 1), TimeUnit.MILLISECONDS); } isPreliminaryDataRequest = true; } else { checkNotNull(listener).onNoSeedNodeAvailable(); } } public void requestUpdateData() { checkArgument(nodeAddressOfPreliminaryDataRequest.isPresent(), "nodeAddressOfPreliminaryDataRequest must be present"); dataUpdateRequested = true; isPreliminaryDataRequest = false; List nodeAddresses = new ArrayList<>(seedNodeAddresses); if (!nodeAddresses.isEmpty()) { // We use the node we have already connected to to request again nodeAddressOfPreliminaryDataRequest.ifPresent(candidate -> { nodeAddresses.remove(candidate); requestData(candidate, nodeAddresses); ArrayList finalNodeAddresses = new ArrayList<>(nodeAddresses); int numRequests = 0; for (int i = 0; i < finalNodeAddresses.size() && numRequests < NUM_ADDITIONAL_SEEDS_FOR_UPDATE_REQUEST; i++) { NodeAddress nodeAddress = finalNodeAddresses.get(i); nodeAddresses.remove(nodeAddress); // It might be that we have a prelim. request open for the same seed, if so we skip to the next. if (!handlerMap.containsKey(nodeAddress)) { UserThread.runAfter(() -> requestData(nodeAddress, nodeAddresses), (i * 200 + 1), TimeUnit.MILLISECONDS); numRequests++; } } }); } } public Optional getNodeAddressOfPreliminaryDataRequest() { return nodeAddressOfPreliminaryDataRequest; } public void addResponseListener(ResponseListener responseListener) { responseListeners.add(responseListener); } /////////////////////////////////////////////////////////////////////////////////////////// // ConnectionListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onConnection(Connection connection) { } @Override public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { closeHandler(connection); if (peerManager.isPeerBanned(closeConnectionReason, connection) && connection.getPeersNodeAddressOptional().isPresent()) { NodeAddress nodeAddress = connection.getPeersNodeAddressOptional().get(); seedNodeAddresses.remove(nodeAddress); handlerMap.remove(nodeAddress); } } /////////////////////////////////////////////////////////////////////////////////////////// // PeerManager.Listener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onAllConnectionsLost() { closeAllHandlers(); stopRetryTimer(); stopped = true; restart(); } @Override public void onNewConnectionAfterAllConnectionsLost() { closeAllHandlers(); stopped = false; restart(); } @Override public void onAwakeFromStandby() { closeAllHandlers(); stopped = false; if (!networkNode.getAllConnections().isEmpty()) restart(); } /////////////////////////////////////////////////////////////////////////////////////////// // MessageListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { if (networkEnvelope instanceof GetDataRequest) { if (!stopped) { GetDataRequest getDataRequest = (GetDataRequest) networkEnvelope; if (getDataRequest.getVersion() == null || !Version.isNewVersion(getDataRequest.getVersion(), "0.0.0")) { connection.shutDown(CloseConnectionReason.MANDATORY_CAPABILITIES_NOT_SUPPORTED); return; } final String uid = connection.getUid(); if (!getDataRequestHandlers.containsKey(uid)) { GetDataRequestHandler getDataRequestHandler = new GetDataRequestHandler(networkNode, dataStorage, new GetDataRequestHandler.Listener() { @Override public void onComplete(int serializedSize) { getDataRequestHandlers.remove(uid); log.trace("requestDataHandshake completed.\n\tConnection={}", connection); responseListeners.forEach(listener -> listener.onSuccess(serializedSize)); } @Override public void onFault(String errorMessage, @Nullable Connection connection) { getDataRequestHandlers.remove(uid); if (!stopped) { log.trace("GetDataRequestHandler failed.\n\tConnection={}\n\t" + "ErrorMessage={}", connection, errorMessage); peerManager.handleConnectionFault(connection); responseListeners.forEach(ResponseListener::onFault); } else { log.warn("We have stopped already. We ignore that getDataRequestHandler.handle.onFault call."); } } }); getDataRequestHandlers.put(uid, getDataRequestHandler); getDataRequestHandler.handle(getDataRequest, connection); } else { log.warn("We have already a GetDataRequestHandler for that connection started. " + "We start a cleanup timer if the handler has not closed by itself in between 2 minutes."); UserThread.runAfter(() -> { if (getDataRequestHandlers.containsKey(uid)) { GetDataRequestHandler handler = getDataRequestHandlers.get(uid); handler.stop(); getDataRequestHandlers.remove(uid); } }, CLEANUP_TIMER); } } else { log.warn("We have stopped already. We ignore that onMessage call."); } } } /////////////////////////////////////////////////////////////////////////////////////////// // RequestData /////////////////////////////////////////////////////////////////////////////////////////// private void requestData(NodeAddress nodeAddress, List remainingNodeAddresses) { if (!stopped) { if (!handlerMap.containsKey(nodeAddress)) { RequestDataHandler requestDataHandler = new RequestDataHandler(networkNode, dataStorage, peerManager, new RequestDataHandler.Listener() { @Override public void onComplete(boolean wasTruncated) { log.trace("RequestDataHandshake of outbound connection complete. nodeAddress={}", nodeAddress); stopRetryTimer(); // need to remove before listeners are notified as they cause the update call handlerMap.remove(nodeAddress); // 1. We get a response from requestPreliminaryData if (!nodeAddressOfPreliminaryDataRequest.isPresent()) { nodeAddressOfPreliminaryDataRequest = Optional.of(nodeAddress); // We delay because it can be that we get the HS published before we receive the // preliminary data and the onPreliminaryDataReceived call triggers the // dataUpdateRequested set to true, so we would also call the onUpdatedDataReceived. UserThread.runAfter(checkNotNull(listener)::onPreliminaryDataReceived, 100, TimeUnit.MILLISECONDS); } // 2. Later we get a response from requestUpdatesData if (dataUpdateRequested) { dataUpdateRequested = false; checkNotNull(listener).onUpdatedDataReceived(); } if (wasTruncated) { if (numRepeatedRequests < MAX_REPEATED_REQUESTS) { // If we had allDataReceived already set to true but get a response with truncated flag, // we still repeat the request to that node for higher redundancy. Otherwise, one seed node // providing incomplete data would stop others to fill the gaps. log.info("DataResponse did not contain all data, so we repeat request until we got all data"); UserThread.runAfter(() -> requestData(nodeAddress, remainingNodeAddresses), 2); } else if (!allDataReceived) { allDataReceived = true; log.warn("\n#################################################################\n" + "Loading initial data from {} did not complete after 20 repeated requests. \n" + "#################################################################\n", nodeAddress); checkNotNull(listener).onDataReceived(); } } else if (!allDataReceived) { allDataReceived = true; log.info("\n\n#################################################################\n" + "Loading initial data from {} completed\n" + "#################################################################\n", nodeAddress); checkNotNull(listener).onDataReceived(); } } @Override public void onFault(String errorMessage, @Nullable Connection connection) { log.trace("requestDataHandshake with outbound connection failed.\n\tnodeAddress={}\n\t" + "ErrorMessage={}", nodeAddress, errorMessage); peerManager.handleConnectionFault(nodeAddress); handlerMap.remove(nodeAddress); if (!remainingNodeAddresses.isEmpty()) { log.debug("There are remaining nodes available for requesting data. " + "We will try requestDataFromPeers again."); NodeAddress nextCandidate = remainingNodeAddresses.get(0); remainingNodeAddresses.remove(nextCandidate); requestData(nextCandidate, remainingNodeAddresses); } else if (handlerMap.isEmpty()) { // If not other connection attempts are in the handlerMap we assume that no seed // nodes are available. log.debug("There is no remaining node available for requesting data. " + "That is expected if no other node is online.\n\t" + "We will try to use reported peers (if no available we use persisted peers) " + "and try again to request data from our seed nodes after a random pause."); // Notify listeners if (!nodeAddressOfPreliminaryDataRequest.isPresent()) { if (peerManager.isSeedNode(nodeAddress)) { checkNotNull(listener).onNoSeedNodeAvailable(); } else { checkNotNull(listener).onNoPeersAvailable(); } } requestFromNonSeedNodePeers(); } else { log.info("We could not connect to seed node {} but we have other connection attempts open.", nodeAddress.getFullAddress()); } } }); handlerMap.put(nodeAddress, requestDataHandler); numRepeatedRequests++; requestDataHandler.requestData(nodeAddress, isPreliminaryDataRequest); } else { log.warn("We have started already a requestDataHandshake to peer. nodeAddress=" + nodeAddress + "\n" + "We start a cleanup timer if the handler has not closed by itself in between 2 minutes."); UserThread.runAfter(() -> { if (handlerMap.containsKey(nodeAddress)) { RequestDataHandler handler = handlerMap.get(nodeAddress); handler.stop(); handlerMap.remove(nodeAddress); } }, CLEANUP_TIMER); } } else { log.warn("We have stopped already. We ignore that requestData call."); } } /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// private void requestFromNonSeedNodePeers() { List list = getFilteredNonSeedNodeList(getSortedNodeAddresses(peerManager.getReportedPeers()), new ArrayList<>()); List filteredPersistedPeers = getFilteredNonSeedNodeList(getSortedNodeAddresses(peerManager.getPersistedPeers()), list); list.addAll(filteredPersistedPeers); if (!list.isEmpty()) { NodeAddress nextCandidate = list.get(0); list.remove(nextCandidate); requestData(nextCandidate, list); } } private void restart() { if (retryTimer == null) { retryTimer = UserThread.runAfter(() -> { stopped = false; stopRetryTimer(); // We create a new list of candidates // 1. shuffled seedNodes // 2. reported peers sorted by last activity date // 3. Add as last persisted peers sorted by last activity date List list = getFilteredList(new ArrayList<>(seedNodeAddresses), new ArrayList<>()); Collections.shuffle(list); List filteredReportedPeers = getFilteredNonSeedNodeList(getSortedNodeAddresses(peerManager.getReportedPeers()), list); list.addAll(filteredReportedPeers); List filteredPersistedPeers = getFilteredNonSeedNodeList(getSortedNodeAddresses(peerManager.getPersistedPeers()), list); list.addAll(filteredPersistedPeers); if (!list.isEmpty()) { NodeAddress nextCandidate = list.get(0); list.remove(nextCandidate); requestData(nextCandidate, list); } }, RETRY_DELAY_SEC); } } private List getSortedNodeAddresses(Collection collection) { return new ArrayList<>(collection) .stream() .sorted((o1, o2) -> o2.getDate().compareTo(o1.getDate())) .map(Peer::getNodeAddress) .collect(Collectors.toList()); } private List getFilteredList(Collection collection, List list) { return collection.stream() .filter(e -> !list.contains(e) && !peerManager.isSelf(e)) .collect(Collectors.toList()); } private List getFilteredNonSeedNodeList(Collection collection, List list) { return getFilteredList(collection, list).stream() .filter(e -> !peerManager.isSeedNode(e)) .collect(Collectors.toList()); } private void stopRetryTimer() { if (retryTimer != null) { retryTimer.stop(); retryTimer = null; } } private void closeHandler(Connection connection) { Optional peersNodeAddressOptional = connection.getPeersNodeAddressOptional(); if (peersNodeAddressOptional.isPresent()) { NodeAddress nodeAddress = peersNodeAddressOptional.get(); if (handlerMap.containsKey(nodeAddress)) { handlerMap.get(nodeAddress).cancel(); handlerMap.remove(nodeAddress); } } else { log.trace("closeRequestDataHandler: nodeAddress not set in connection {}", connection); } } private void closeAllHandlers() { handlerMap.values().forEach(RequestDataHandler::cancel); handlerMap.clear(); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/peers/getdata/messages/GetDataRequest.java ================================================ /* * 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 . */ package haveno.network.p2p.peers.getdata.messages; import haveno.common.proto.network.NetworkEnvelope; import haveno.network.p2p.ExtendedDataSizePermission; import haveno.network.p2p.InitialDataRequest; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; import javax.annotation.Nullable; import java.util.Set; @EqualsAndHashCode(callSuper = true) @Getter @ToString public abstract class GetDataRequest extends NetworkEnvelope implements ExtendedDataSizePermission, InitialDataRequest { protected final int nonce; // Keys for ProtectedStorageEntry items to be excluded from the request because the peer has them already protected final Set excludedKeys; // Added at v1.4.0 // The version of the requester. Used for response to send potentially missing historical data @Nullable protected final String version; public GetDataRequest(String messageVersion, int nonce, Set excludedKeys, @Nullable String version) { super(messageVersion); this.nonce = nonce; this.excludedKeys = excludedKeys; this.version = version; } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/peers/getdata/messages/GetDataResponse.java ================================================ /* * 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 . */ package haveno.network.p2p.peers.getdata.messages; import haveno.common.app.Capabilities; import haveno.common.app.Version; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.proto.network.NetworkProtoResolver; import haveno.common.util.Utilities; import haveno.network.p2p.ExtendedDataSizePermission; import haveno.network.p2p.InitialDataRequest; import haveno.network.p2p.InitialDataResponse; import haveno.network.p2p.SupportedCapabilitiesMessage; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import haveno.network.p2p.storage.payload.ProtectedMailboxStorageEntry; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import lombok.EqualsAndHashCode; import lombok.Value; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import java.util.Set; import java.util.stream.Collectors; @Slf4j @EqualsAndHashCode(callSuper = true) @Value public final class GetDataResponse extends NetworkEnvelope implements SupportedCapabilitiesMessage, ExtendedDataSizePermission, InitialDataResponse { // Set of ProtectedStorageEntry objects private final Set dataSet; // Set of PersistableNetworkPayload objects // We added that in v 0.6 and the fromProto code will create an empty HashSet if it doesn't exist private final Set persistableNetworkPayloadSet; private final int requestNonce; private final boolean isGetUpdatedDataResponse; private final Capabilities supportedCapabilities; // Added at v1.9.6 private final boolean wasTruncated; public GetDataResponse(@NotNull Set dataSet, @NotNull Set persistableNetworkPayloadSet, int requestNonce, boolean isGetUpdatedDataResponse, boolean wasTruncated) { this(dataSet, persistableNetworkPayloadSet, requestNonce, isGetUpdatedDataResponse, wasTruncated, Capabilities.app, Version.getP2PMessageVersion()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private GetDataResponse(@NotNull Set dataSet, @NotNull Set persistableNetworkPayloadSet, int requestNonce, boolean isGetUpdatedDataResponse, boolean wasTruncated, @NotNull Capabilities supportedCapabilities, String messageVersion) { super(messageVersion); this.dataSet = dataSet; this.persistableNetworkPayloadSet = persistableNetworkPayloadSet; this.requestNonce = requestNonce; this.isGetUpdatedDataResponse = isGetUpdatedDataResponse; this.wasTruncated = wasTruncated; this.supportedCapabilities = supportedCapabilities; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { final protobuf.GetDataResponse.Builder builder = protobuf.GetDataResponse.newBuilder() .addAllDataSet(dataSet.stream() .map(protectedStorageEntry -> protectedStorageEntry instanceof ProtectedMailboxStorageEntry ? protobuf.StorageEntryWrapper.newBuilder() .setProtectedMailboxStorageEntry((protobuf.ProtectedMailboxStorageEntry) protectedStorageEntry.toProtoMessage()) .build() : protobuf.StorageEntryWrapper.newBuilder() .setProtectedStorageEntry((protobuf.ProtectedStorageEntry) protectedStorageEntry.toProtoMessage()) .build()) .collect(Collectors.toList())) .addAllPersistableNetworkPayloadItems(persistableNetworkPayloadSet.stream() .map(PersistableNetworkPayload::toProtoMessage) .collect(Collectors.toList())) .setRequestNonce(requestNonce) .setIsGetUpdatedDataResponse(isGetUpdatedDataResponse) .setWasTruncated(wasTruncated) .addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities)); protobuf.NetworkEnvelope proto = getNetworkEnvelopeBuilder() .setGetDataResponse(builder) .build(); log.info("Sending a GetDataResponse with {}", Utilities.readableFileSize(proto.getSerializedSize())); return proto; } public static GetDataResponse fromProto(protobuf.GetDataResponse proto, NetworkProtoResolver resolver, String messageVersion) { boolean wasTruncated = proto.getWasTruncated(); log.info("Received a GetDataResponse with {} {}", Utilities.readableFileSize(proto.getSerializedSize()), wasTruncated ? " (was truncated)" : ""); Set dataSet = proto.getDataSetList().stream() .map(entry -> (ProtectedStorageEntry) resolver.fromProto(entry)).collect(Collectors.toSet()); Set persistableNetworkPayloadSet = proto.getPersistableNetworkPayloadItemsList().stream() .map(e -> (PersistableNetworkPayload) resolver.fromProto(e)).collect(Collectors.toSet()); return new GetDataResponse(dataSet, persistableNetworkPayloadSet, proto.getRequestNonce(), proto.getIsGetUpdatedDataResponse(), wasTruncated, Capabilities.fromIntList(proto.getSupportedCapabilitiesList()), messageVersion); } @Override public Class associatedRequest() { return isGetUpdatedDataResponse ? GetUpdatedDataRequest.class : PreliminaryGetDataRequest.class; } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/peers/getdata/messages/GetUpdatedDataRequest.java ================================================ /* * 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 . */ package haveno.network.p2p.peers.getdata.messages; import com.google.protobuf.ByteString; import haveno.common.app.Version; import haveno.common.proto.ProtoUtil; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.SendersNodeAddressMessage; import lombok.EqualsAndHashCode; import lombok.Value; import lombok.extern.slf4j.Slf4j; import protobuf.NetworkEnvelope; import javax.annotation.Nullable; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @Slf4j @EqualsAndHashCode(callSuper = true) @Value public final class GetUpdatedDataRequest extends GetDataRequest implements SendersNodeAddressMessage { private final NodeAddress senderNodeAddress; public GetUpdatedDataRequest(NodeAddress senderNodeAddress, int nonce, Set excludedKeys) { this(senderNodeAddress, nonce, excludedKeys, Version.VERSION, Version.getP2PMessageVersion()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private GetUpdatedDataRequest(NodeAddress senderNodeAddress, int nonce, Set excludedKeys, @Nullable String version, String messageVersion) { super(messageVersion, nonce, excludedKeys, version); this.senderNodeAddress = senderNodeAddress; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { protobuf.GetUpdatedDataRequest.Builder builder = protobuf.GetUpdatedDataRequest.newBuilder() .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setNonce(nonce) .addAllExcludedKeys(excludedKeys.stream() .map(ByteString::copyFrom) .collect(Collectors.toList())); Optional.ofNullable(version).ifPresent(builder::setVersion); NetworkEnvelope proto = getNetworkEnvelopeBuilder() .setGetUpdatedDataRequest(builder) .build(); log.info("Sending a GetUpdatedDataRequest with {} kB and {} excluded key entries. Requesters version={}", proto.getSerializedSize() / 1000d, excludedKeys.size(), version); return proto; } public static GetUpdatedDataRequest fromProto(protobuf.GetUpdatedDataRequest proto, String messageVersion) { Set excludedKeys = ProtoUtil.byteSetFromProtoByteStringList(proto.getExcludedKeysList()); String requestersVersion = ProtoUtil.stringOrNullFromProto(proto.getVersion()); log.info("Received a GetUpdatedDataRequest with {} kB and {} excluded key entries. Requesters version={}", proto.getSerializedSize() / 1000d, excludedKeys.size(), requestersVersion); return new GetUpdatedDataRequest(NodeAddress.fromProto(proto.getSenderNodeAddress()), proto.getNonce(), excludedKeys, requestersVersion, messageVersion); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/peers/getdata/messages/PreliminaryGetDataRequest.java ================================================ /* * 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 . */ package haveno.network.p2p.peers.getdata.messages; import com.google.protobuf.ByteString; import haveno.common.app.Capabilities; import haveno.common.app.Version; import haveno.common.proto.ProtoUtil; import haveno.network.p2p.AnonymousMessage; import haveno.network.p2p.SupportedCapabilitiesMessage; import lombok.EqualsAndHashCode; import lombok.Value; import lombok.extern.slf4j.Slf4j; import protobuf.NetworkEnvelope; import javax.annotation.Nullable; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @Slf4j @EqualsAndHashCode(callSuper = true) @Value public final class PreliminaryGetDataRequest extends GetDataRequest implements AnonymousMessage, SupportedCapabilitiesMessage { private final Capabilities supportedCapabilities; public PreliminaryGetDataRequest(int nonce, Set excludedKeys) { this(nonce, excludedKeys, Version.VERSION, Capabilities.app, Version.getP2PMessageVersion()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private PreliminaryGetDataRequest(int nonce, Set excludedKeys, @Nullable String version, Capabilities supportedCapabilities, String messageVersion) { super(messageVersion, nonce, excludedKeys, version); this.supportedCapabilities = supportedCapabilities; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { protobuf.PreliminaryGetDataRequest.Builder builder = protobuf.PreliminaryGetDataRequest.newBuilder() .addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities)) .setNonce(nonce) .addAllExcludedKeys(excludedKeys.stream() .map(ByteString::copyFrom) .collect(Collectors.toList())); Optional.ofNullable(version).ifPresent(builder::setVersion); NetworkEnvelope proto = getNetworkEnvelopeBuilder() .setPreliminaryGetDataRequest(builder) .build(); log.info("Sending a PreliminaryGetDataRequest with {} kB and {} excluded key entries. Requesters version={}", proto.getSerializedSize() / 1000d, excludedKeys.size(), version); return proto; } public static PreliminaryGetDataRequest fromProto(protobuf.PreliminaryGetDataRequest proto, String messageVersion) { Set excludedKeys = ProtoUtil.byteSetFromProtoByteStringList(proto.getExcludedKeysList()); String requestersVersion = ProtoUtil.stringOrNullFromProto(proto.getVersion()); log.info("Received a PreliminaryGetDataRequest with {} kB and {} excluded key entries. Requesters version={}", proto.getSerializedSize() / 1000d, excludedKeys.size(), requestersVersion); return new PreliminaryGetDataRequest(proto.getNonce(), excludedKeys, requestersVersion, Capabilities.fromIntList(proto.getSupportedCapabilitiesList()), messageVersion); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/peers/keepalive/KeepAliveHandler.java ================================================ /* * 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 . */ package haveno.network.p2p.peers.keepalive; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.proto.network.NetworkEnvelope; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.MessageListener; import haveno.network.p2p.network.NetworkNode; import haveno.network.p2p.peers.PeerManager; import haveno.network.p2p.peers.keepalive.messages.Ping; import haveno.network.p2p.peers.keepalive.messages.Pong; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.util.Random; import java.util.concurrent.TimeUnit; class KeepAliveHandler implements MessageListener { private static final Logger log = LoggerFactory.getLogger(KeepAliveHandler.class); private static final int DELAY_MS = 10_000; private static final long LOG_THROTTLE_INTERVAL_MS = 60000; // throttle logging warnings to once every 60 seconds private static long lastLoggedWarningTs = 0; private static int numThrottledWarnings = 0; /////////////////////////////////////////////////////////////////////////////////////////// // Listener /////////////////////////////////////////////////////////////////////////////////////////// public interface Listener { void onComplete(); @SuppressWarnings("UnusedParameters") void onFault(String errorMessage); } /////////////////////////////////////////////////////////////////////////////////////////// // Class fields /////////////////////////////////////////////////////////////////////////////////////////// private final NetworkNode networkNode; private final PeerManager peerManager; private final Listener listener; private final int nonce = new Random().nextInt(); @Nullable private Connection connection; private boolean stopped; private Timer delayTimer; private long sendTs; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public KeepAliveHandler(NetworkNode networkNode, PeerManager peerManager, Listener listener) { this.networkNode = networkNode; this.peerManager = peerManager; this.listener = listener; } public void cancel() { cleanup(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void sendPingAfterRandomDelay(Connection connection) { delayTimer = UserThread.runAfterRandomDelay(() -> sendPing(connection), 1, DELAY_MS, TimeUnit.MILLISECONDS); } private void sendPing(Connection connection) { if (!stopped) { Ping ping = new Ping(nonce, connection.getStatistic().roundTripTimeProperty().get()); sendTs = System.currentTimeMillis(); SettableFuture future = networkNode.sendMessage(connection, ping); Futures.addCallback(future, new FutureCallback() { @Override public void onSuccess(Connection connection) { if (!stopped) { KeepAliveHandler.this.connection = connection; connection.addMessageListener(KeepAliveHandler.this); } else { log.trace("We have stopped already. We ignore that networkNode.sendMessage.onSuccess call."); } } @Override public void onFailure(@NotNull Throwable throwable) { if (!stopped) { String errorMessage = "Sending ping to " + connection + " failed. That is expected if the peer is offline.\n\tping=" + ping + ".\n\tException=" + throwable.getMessage(); cleanup(); //peerManager.shutDownConnection(connection, CloseConnectionReason.SEND_MSG_FAILURE); log.info(errorMessage); peerManager.handleConnectionFault(connection); listener.onFault(errorMessage); } else { log.trace("We have stopped already. We ignore that networkNode.sendMessage.onFailure call."); } } }, MoreExecutors.directExecutor()); } else { log.trace("We have stopped already. We ignore that sendPing call."); } } /////////////////////////////////////////////////////////////////////////////////////////// // MessageListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { if (networkEnvelope instanceof Pong) { if (!stopped) { Pong pong = (Pong) networkEnvelope; if (pong.getRequestNonce() == nonce) { int roundTripTime = (int) (System.currentTimeMillis() - sendTs); connection.getStatistic().setRoundTripTime(roundTripTime); cleanup(); listener.onComplete(); } else { throttleWarn("Nonce not matching. That should never happen.\n" + "\tWe drop that message. nonce=" + nonce + ", requestNonce=" + pong.getRequestNonce() + ", peerNodeAddress=" + connection.getPeersNodeAddressOptional().orElseGet(null)); } } else { log.trace("We have stopped already. We ignore that onMessage call."); } } } private void cleanup() { stopped = true; if (connection != null) connection.removeMessageListener(this); if (delayTimer != null) { delayTimer.stop(); delayTimer = null; } } private synchronized void throttleWarn(String msg) { boolean logWarning = System.currentTimeMillis() - lastLoggedWarningTs > LOG_THROTTLE_INTERVAL_MS; if (logWarning) { log.warn(msg); if (numThrottledWarnings > 0) log.warn("We received {} throttled warnings since the last log entry" + (numThrottledWarnings >= Connection.POSSIBLE_DOS_THRESHOLD ? ". Possible DoS attack detected" : ""), numThrottledWarnings); numThrottledWarnings = 0; lastLoggedWarningTs = System.currentTimeMillis(); } else { numThrottledWarnings++; } } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/peers/keepalive/KeepAliveManager.java ================================================ /* * 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 . */ package haveno.network.p2p.peers.keepalive; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import com.google.inject.Inject; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.util.Utilities; import haveno.network.p2p.network.CloseConnectionReason; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.ConnectionListener; import haveno.network.p2p.network.MessageListener; import haveno.network.p2p.network.NetworkNode; import haveno.network.p2p.network.OutboundConnection; import haveno.network.p2p.peers.PeerManager; import haveno.network.p2p.peers.keepalive.messages.Ping; import haveno.network.p2p.peers.keepalive.messages.Pong; import java.util.HashMap; import java.util.Map; import java.util.Random; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class KeepAliveManager implements MessageListener, ConnectionListener, PeerManager.Listener { private static final Logger log = LoggerFactory.getLogger(KeepAliveManager.class); private static final int INTERVAL_SEC = new Random().nextInt(30) + 30; private static final long LAST_ACTIVITY_AGE_MS = INTERVAL_SEC * 1000 / 2; private final NetworkNode networkNode; private final PeerManager peerManager; private final Map handlerMap = new HashMap<>(); private boolean stopped; private Timer keepAliveTimer; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public KeepAliveManager(NetworkNode networkNode, PeerManager peerManager) { this.networkNode = networkNode; this.peerManager = peerManager; this.networkNode.addMessageListener(this); this.networkNode.addConnectionListener(this); this.peerManager.addListener(this); } public void shutDown() { stopped = true; networkNode.removeMessageListener(this); networkNode.removeConnectionListener(this); peerManager.removeListener(this); closeAllHandlers(); stopKeepAliveTimer(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void start() { restart(); } /////////////////////////////////////////////////////////////////////////////////////////// // MessageListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { if (networkEnvelope instanceof Ping) { if (!stopped) { Ping ping = (Ping) networkEnvelope; // We get from peer last measured rrt connection.getStatistic().setRoundTripTime(ping.getLastRoundTripTime()); Pong pong = new Pong(ping.getNonce()); SettableFuture future = networkNode.sendMessage(connection, pong); Futures.addCallback(future, Utilities.failureCallback(throwable -> { if (!stopped) { String errorMessage = "Sending pong to " + connection + " failed. That is expected if the peer is offline. " + "Exception: " + throwable.getMessage(); log.info(errorMessage); peerManager.handleConnectionFault(connection); } else { log.warn("We have stopped already. We ignore that networkNode.sendMessage.onFailure call."); } }), MoreExecutors.directExecutor()); } else { log.warn("We have stopped already. We ignore that onMessage call."); } } } /////////////////////////////////////////////////////////////////////////////////////////// // ConnectionListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onConnection(Connection connection) { } @Override public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { closeHandler(connection); } /////////////////////////////////////////////////////////////////////////////////////////// // PeerManager.Listener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onAllConnectionsLost() { closeAllHandlers(); stopKeepAliveTimer(); stopped = true; restart(); } @Override public void onNewConnectionAfterAllConnectionsLost() { closeAllHandlers(); stopped = false; restart(); } @Override public void onAwakeFromStandby() { closeAllHandlers(); stopped = false; if (!networkNode.getAllConnections().isEmpty()) restart(); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void restart() { if (keepAliveTimer == null) keepAliveTimer = UserThread.runPeriodically(() -> { stopped = false; keepAlive(); }, INTERVAL_SEC); } private void keepAlive() { if (!stopped) { networkNode.getConfirmedConnections().stream() .filter(connection -> connection instanceof OutboundConnection && connection.getStatistic().getLastActivityAge() > LAST_ACTIVITY_AGE_MS) .forEach(connection -> { final String uid = connection.getUid(); synchronized (handlerMap) { if (!handlerMap.containsKey(uid)) { KeepAliveHandler keepAliveHandler = new KeepAliveHandler(networkNode, peerManager, new KeepAliveHandler.Listener() { @Override public void onComplete() { synchronized (handlerMap) { handlerMap.remove(uid); } } @Override public void onFault(String errorMessage) { synchronized (handlerMap) { handlerMap.remove(uid); } } }); handlerMap.put(uid, keepAliveHandler); keepAliveHandler.sendPingAfterRandomDelay(connection); } else { // TODO check if this situation causes any issues log.debug("Connection with id {} has not completed and is still in our map. " + "We will try to ping that peer at the next schedule.", uid); } } }); int size = handlerMap.size(); log.debug("handlerMap size=" + size); if (size > peerManager.getMaxConnections()) log.warn("Seems we didn't clean up out map correctly.\n" + "handlerMap size={}, peerManager.getMaxConnections()={}", size, peerManager.getMaxConnections()); } else { log.warn("We have stopped already. We ignore that keepAlive call."); } } private void stopKeepAliveTimer() { stopped = true; if (keepAliveTimer != null) { keepAliveTimer.stop(); keepAliveTimer = null; } } private void closeHandler(Connection connection) { synchronized (handlerMap) { String uid = connection.getUid(); if (handlerMap.containsKey(uid)) { handlerMap.get(uid).cancel(); handlerMap.remove(uid); } } } private void closeAllHandlers() { synchronized (handlerMap) { handlerMap.values().stream().forEach(KeepAliveHandler::cancel); handlerMap.clear(); } } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/peers/keepalive/messages/KeepAliveMessage.java ================================================ /* * 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 . */ package haveno.network.p2p.peers.keepalive.messages; public interface KeepAliveMessage { } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/peers/keepalive/messages/Ping.java ================================================ /* * 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 . */ package haveno.network.p2p.peers.keepalive.messages; import haveno.common.app.Version; import haveno.common.proto.network.NetworkEnvelope; import lombok.EqualsAndHashCode; import lombok.Value; @EqualsAndHashCode(callSuper = true) @Value public final class Ping extends NetworkEnvelope implements KeepAliveMessage { private final int nonce; private final int lastRoundTripTime; public Ping(int nonce, int lastRoundTripTime) { this(nonce, lastRoundTripTime, Version.getP2PMessageVersion()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private Ping(int nonce, int lastRoundTripTime, String messageVersion) { super(messageVersion); this.nonce = nonce; this.lastRoundTripTime = lastRoundTripTime; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { return getNetworkEnvelopeBuilder() .setPing(protobuf.Ping.newBuilder() .setNonce(nonce) .setLastRoundTripTime(lastRoundTripTime)) .build(); } public static Ping fromProto(protobuf.Ping proto, String messageVersion) { return new Ping(proto.getNonce(), proto.getLastRoundTripTime(), messageVersion); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/peers/keepalive/messages/Pong.java ================================================ /* * 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 . */ package haveno.network.p2p.peers.keepalive.messages; import haveno.common.app.Version; import haveno.common.proto.network.NetworkEnvelope; import lombok.EqualsAndHashCode; import lombok.Value; @EqualsAndHashCode(callSuper = true) @Value public final class Pong extends NetworkEnvelope implements KeepAliveMessage { private final int requestNonce; public Pong(int requestNonce) { this(requestNonce, Version.getP2PMessageVersion()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private Pong(int requestNonce, String messageVersion) { super(messageVersion); this.requestNonce = requestNonce; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { return getNetworkEnvelopeBuilder() .setPong(protobuf.Pong.newBuilder() .setRequestNonce(requestNonce)) .build(); } public static Pong fromProto(protobuf.Pong proto, String messageVersion) { return new Pong(proto.getRequestNonce(), messageVersion); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/peers/peerexchange/GetPeersRequestHandler.java ================================================ /* * 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 . */ package haveno.network.p2p.peers.peerexchange; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import haveno.common.Timer; import haveno.common.UserThread; import haveno.network.p2p.network.CloseConnectionReason; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.NetworkNode; import haveno.network.p2p.peers.PeerManager; import haveno.network.p2p.peers.peerexchange.messages.GetPeersRequest; import haveno.network.p2p.peers.peerexchange.messages.GetPeersResponse; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import java.util.HashSet; import java.util.concurrent.TimeUnit; import static com.google.common.base.Preconditions.checkArgument; @Slf4j class GetPeersRequestHandler { // We want to keep timeout short here private static final long TIMEOUT = 90; /////////////////////////////////////////////////////////////////////////////////////////// // Listener /////////////////////////////////////////////////////////////////////////////////////////// public interface Listener { void onComplete(); void onFault(String errorMessage, Connection connection); } /////////////////////////////////////////////////////////////////////////////////////////// // Class fields /////////////////////////////////////////////////////////////////////////////////////////// private final NetworkNode networkNode; private final PeerManager peerManager; private final Listener listener; private Timer timeoutTimer; private boolean stopped; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public GetPeersRequestHandler(NetworkNode networkNode, PeerManager peerManager, Listener listener) { this.networkNode = networkNode; this.peerManager = peerManager; this.listener = listener; } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void handle(GetPeersRequest getPeersRequest, Connection connection) { checkArgument(connection.getPeersNodeAddressOptional().isPresent(), "The peers address must have been already set at the moment"); GetPeersResponse getPeersResponse = new GetPeersResponse(getPeersRequest.getNonce(), new HashSet<>(peerManager.getLivePeers(connection.getPeersNodeAddressOptional().get()))); checkArgument(timeoutTimer == null, "onGetPeersRequest must not be called twice."); timeoutTimer = UserThread.runAfter(() -> { // setup before sending to avoid race conditions if (!stopped) { String errorMessage = "A timeout occurred at sending getPeersResponse:" + getPeersResponse + " on connection:" + connection; log.debug(errorMessage + " / PeerExchangeHandshake=" + GetPeersRequestHandler.this); log.debug("timeoutTimer called. this=" + this); handleFault(errorMessage, CloseConnectionReason.SEND_MSG_TIMEOUT, connection); } else { log.trace("We have stopped already. We ignore that timeoutTimer.run call."); } }, TIMEOUT, TimeUnit.SECONDS); SettableFuture future = networkNode.sendMessage(connection, getPeersResponse); Futures.addCallback(future, new FutureCallback() { @Override public void onSuccess(Connection connection) { if (!stopped) { log.trace("GetPeersResponse sent successfully"); cleanup(); listener.onComplete(); } else { log.trace("We have stopped already. We ignore that networkNode.sendMessage.onSuccess call."); } } @Override public void onFailure(@NotNull Throwable throwable) { if (!stopped) { String errorMessage = "Sending getPeersResponse to " + connection + " failed. That is expected if the peer is offline. getPeersResponse=" + getPeersResponse + "." + "Exception: " + throwable.getMessage(); log.info(errorMessage); handleFault(errorMessage, CloseConnectionReason.SEND_MSG_FAILURE, connection); } else { log.trace("We have stopped already. We ignore that networkNode.sendMessage.onFailure call."); } } }, MoreExecutors.directExecutor()); peerManager.addToReportedPeers(getPeersRequest.getReportedPeers(), connection, getPeersRequest.getSupportedCapabilities()); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// @SuppressWarnings("UnusedParameters") private void handleFault(String errorMessage, CloseConnectionReason closeConnectionReason, Connection connection) { cleanup(); //peerManager.shutDownConnection(connection, closeConnectionReason); listener.onFault(errorMessage, connection); } private void cleanup() { stopped = true; if (timeoutTimer != null) { timeoutTimer.stop(); timeoutTimer = null; } } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/peers/peerexchange/Peer.java ================================================ /* * 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 . */ package haveno.network.p2p.peers.peerexchange; import haveno.common.app.Capabilities; import haveno.common.app.HasCapabilities; import haveno.common.proto.network.NetworkPayload; import haveno.common.proto.persistable.PersistablePayload; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.network.SupportedCapabilitiesListener; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.util.Date; @Getter @Slf4j public final class Peer implements HasCapabilities, NetworkPayload, PersistablePayload, SupportedCapabilitiesListener { private static final int MAX_FAILED_CONNECTION_ATTEMPTS = 8; private final NodeAddress nodeAddress; private final long date; @Setter transient private int failedConnectionAttempts = 0; @Setter private Capabilities capabilities = new Capabilities(); public Peer(NodeAddress nodeAddress, @Nullable Capabilities supportedCapabilities) { this(nodeAddress, new Date().getTime(), supportedCapabilities); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private Peer(NodeAddress nodeAddress, long date, Capabilities supportedCapabilities) { super(); this.nodeAddress = nodeAddress; this.date = date; this.capabilities.addAll(supportedCapabilities); } @Override public protobuf.Peer toProtoMessage() { return protobuf.Peer.newBuilder() .setNodeAddress(nodeAddress.toProtoMessage()) .setDate(date) .addAllSupportedCapabilities(Capabilities.toIntList(getCapabilities())) .build(); } public static Peer fromProto(protobuf.Peer proto) { return new Peer(NodeAddress.fromProto(proto.getNodeAddress()), proto.getDate(), Capabilities.fromIntList(proto.getSupportedCapabilitiesList())); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void onDisconnect() { this.failedConnectionAttempts++; } public void onConnection() { this.failedConnectionAttempts--; } public boolean tooManyFailedConnectionAttempts() { return failedConnectionAttempts >= MAX_FAILED_CONNECTION_ATTEMPTS; } public Date getDate() { return new Date(date); } public long getDateAsLong() { return date; } @Override public void onChanged(Capabilities supportedCapabilities) { if (!supportedCapabilities.isEmpty()) { capabilities.set(supportedCapabilities); } } // We use only node address for equals and hashcode @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Peer)) return false; Peer peer = (Peer) o; return nodeAddress != null ? nodeAddress.equals(peer.nodeAddress) : peer.nodeAddress == null; } @Override public int hashCode() { return nodeAddress != null ? nodeAddress.hashCode() : 0; } @Override public String toString() { return "Peer{" + "\n nodeAddress=" + nodeAddress + ",\n date=" + date + ",\n failedConnectionAttempts=" + failedConnectionAttempts + ",\n capabilities=" + capabilities + "\n}"; } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/peers/peerexchange/PeerExchangeHandler.java ================================================ /* * 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 . */ package haveno.network.p2p.peers.peerexchange; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.proto.network.NetworkEnvelope; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.network.CloseConnectionReason; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.MessageListener; import haveno.network.p2p.network.NetworkNode; import haveno.network.p2p.peers.PeerManager; import haveno.network.p2p.peers.peerexchange.messages.GetPeersRequest; import haveno.network.p2p.peers.peerexchange.messages.GetPeersResponse; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; import java.util.HashSet; import java.util.Random; import java.util.concurrent.TimeUnit; @Slf4j class PeerExchangeHandler implements MessageListener { // We want to keep timeout short here private static final long TIMEOUT = 90; private static final int DELAY_MS = 500; private static final long LOG_THROTTLE_INTERVAL_MS = 60000; // throttle logging warnings to once every 60 seconds private static long lastLoggedWarningTs = 0; private static int numThrottledWarnings = 0; /////////////////////////////////////////////////////////////////////////////////////////// // Listener /////////////////////////////////////////////////////////////////////////////////////////// public interface Listener { void onComplete(); @SuppressWarnings("UnusedParameters") void onFault(String errorMessage, @Nullable Connection connection); } /////////////////////////////////////////////////////////////////////////////////////////// // Class fields /////////////////////////////////////////////////////////////////////////////////////////// private final NetworkNode networkNode; private final PeerManager peerManager; private final Listener listener; private final int nonce = new Random().nextInt(); private Timer timeoutTimer; private Connection connection; private boolean stopped; private Timer delayTimer; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public PeerExchangeHandler(NetworkNode networkNode, PeerManager peerManager, Listener listener) { this.networkNode = networkNode; this.peerManager = peerManager; this.listener = listener; } public void cancel() { cleanup(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void sendGetPeersRequestAfterRandomDelay(NodeAddress nodeAddress) { delayTimer = UserThread.runAfterRandomDelay(() -> sendGetPeersRequest(nodeAddress), 1, DELAY_MS, TimeUnit.MILLISECONDS); } private void sendGetPeersRequest(NodeAddress nodeAddress) { log.debug("sendGetPeersRequest to nodeAddress={}", nodeAddress); if (!stopped) { if (networkNode.getNodeAddress() != null) { GetPeersRequest getPeersRequest = new GetPeersRequest(networkNode.getNodeAddress(), nonce, new HashSet<>(peerManager.getLivePeers(nodeAddress))); if (timeoutTimer == null) { timeoutTimer = UserThread.runAfter(() -> { // setup before sending to avoid race conditions if (!stopped) { String errorMessage = "A timeout occurred at sending getPeersRequest. nodeAddress=" + nodeAddress; handleFault(errorMessage, CloseConnectionReason.SEND_MSG_TIMEOUT, nodeAddress); } else { log.trace("We have stopped that handler already. We ignore that timeoutTimer.run call."); } }, TIMEOUT, TimeUnit.SECONDS); } try { SettableFuture future = networkNode.sendMessage(nodeAddress, getPeersRequest); Futures.addCallback(future, new FutureCallback() { @Override public void onSuccess(Connection connection) { if (!stopped) { //TODO /*if (!connection.getPeersNodeAddressOptional().isPresent()) { connection.setPeersNodeAddress(nodeAddress); log.warn("sendGetPeersRequest: !connection.getPeersNodeAddressOptional().isPresent()"); }*/ PeerExchangeHandler.this.connection = connection; connection.addMessageListener(PeerExchangeHandler.this); } else { log.trace("We have stopped that handler already. We ignore that sendGetPeersRequest.onSuccess call."); } } @Override public void onFailure(@NotNull Throwable throwable) { if (!stopped) { String errorMessage = "Sending getPeersRequest to " + nodeAddress + " failed. That is expected if the peer is offline. Exception=" + throwable.getMessage(); handleFault(errorMessage, CloseConnectionReason.SEND_MSG_FAILURE, nodeAddress); } else { log.trace("We have stopped that handler already. We ignore that sendGetPeersRequest.onFailure call."); } } }, MoreExecutors.directExecutor()); } catch (Exception e) { if (!networkNode.isShutDownStarted()) throw e; } } else { log.debug("My node address is still null at sendGetPeersRequest. We ignore that call."); } } else { log.trace("We have stopped that handler already. We ignore that sendGetPeersRequest call."); } } /////////////////////////////////////////////////////////////////////////////////////////// // MessageListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { if (networkEnvelope instanceof GetPeersResponse) { if (!stopped) { GetPeersResponse getPeersResponse = (GetPeersResponse) networkEnvelope; // Check if the response is for our request if (getPeersResponse.getRequestNonce() == nonce) { peerManager.addToReportedPeers(getPeersResponse.getReportedPeers(), connection, getPeersResponse.getSupportedCapabilities()); cleanup(); listener.onComplete(); } else { throttleWarn("Nonce not matching. That should never happen.\n" + "\tWe drop that message. nonce=" + nonce + ", requestNonce=" + getPeersResponse.getRequestNonce() + ", peerNodeAddress=" + connection.getPeersNodeAddressOptional().orElseGet(null)); } } else { log.trace("We have stopped that handler already. We ignore that onMessage call."); } } } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// @SuppressWarnings("UnusedParameters") private void handleFault(String errorMessage, CloseConnectionReason closeConnectionReason, NodeAddress nodeAddress) { cleanup(); /* if (connection == null) peerManager.shutDownConnection(nodeAddress, closeConnectionReason); else peerManager.shutDownConnection(connection, closeConnectionReason);*/ peerManager.handleConnectionFault(nodeAddress, connection); listener.onFault(errorMessage, connection); } private void cleanup() { stopped = true; if (connection != null) connection.removeMessageListener(this); if (timeoutTimer != null) { timeoutTimer.stop(); timeoutTimer = null; } if (delayTimer != null) { delayTimer.stop(); delayTimer = null; } } private synchronized void throttleWarn(String msg) { boolean logWarning = System.currentTimeMillis() - lastLoggedWarningTs > LOG_THROTTLE_INTERVAL_MS; if (logWarning) { log.warn(msg); if (numThrottledWarnings > 0) log.warn("We received {} throttled warnings since the last log entry" + (numThrottledWarnings >= Connection.POSSIBLE_DOS_THRESHOLD ? ". Possible DoS attack detected" : ""), numThrottledWarnings); numThrottledWarnings = 0; lastLoggedWarningTs = System.currentTimeMillis(); } else { numThrottledWarnings++; } } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/peers/peerexchange/PeerExchangeManager.java ================================================ /* * 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 . */ package haveno.network.p2p.peers.peerexchange; import com.google.common.base.Preconditions; import com.google.inject.Inject; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.proto.network.NetworkEnvelope; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.network.CloseConnectionReason; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.ConnectionListener; import haveno.network.p2p.network.MessageListener; import haveno.network.p2p.network.NetworkNode; import haveno.network.p2p.peers.PeerManager; import haveno.network.p2p.peers.peerexchange.messages.GetPeersRequest; import haveno.network.p2p.seed.SeedNodeRepository; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Random; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import javax.annotation.Nullable; import lombok.extern.slf4j.Slf4j; @Slf4j public class PeerExchangeManager implements MessageListener, ConnectionListener, PeerManager.Listener { private static final long RETRY_DELAY_SEC = 10; private static final long RETRY_DELAY_AFTER_ALL_CON_LOST_SEC = 3; private static final long REQUEST_PERIODICALLY_INTERVAL_MIN = 10; private final NetworkNode networkNode; private final PeerManager peerManager; private final Set seedNodeAddresses; private final Map handlerMap = new HashMap<>(); private Timer retryTimer, periodicTimer; private boolean stopped; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public PeerExchangeManager(NetworkNode networkNode, SeedNodeRepository seedNodeRepository, PeerManager peerManager) { this.networkNode = networkNode; this.peerManager = peerManager; this.networkNode.addMessageListener(this); this.networkNode.addConnectionListener(this); this.peerManager.addListener(this); this.seedNodeAddresses = new HashSet<>(seedNodeRepository.getSeedNodeAddresses()); } public void shutDown() { stopped = true; networkNode.removeMessageListener(this); networkNode.removeConnectionListener(this); peerManager.removeListener(this); stopPeriodicTimer(); stopRetryTimer(); closeAllHandlers(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void requestReportedPeersFromSeedNodes(NodeAddress nodeAddress) { Preconditions.checkNotNull(networkNode.getNodeAddress(), "My node address must not be null at requestReportedPeers"); ArrayList remainingNodeAddresses = new ArrayList<>(seedNodeAddresses); remainingNodeAddresses.remove(nodeAddress); Collections.shuffle(remainingNodeAddresses); requestReportedPeers(nodeAddress, remainingNodeAddresses); startPeriodicTimer(); } public void initialRequestPeersFromReportedOrPersistedPeers() { if (!peerManager.getReportedPeers().isEmpty() || !peerManager.getPersistedPeers().isEmpty()) { // We will likely get more connections as the GetPeersResponse onComplete handler triggers a new request if the confirmed // connections have not reached the min connection target. // So we potentially request 2 times 8 but we prefer to get fast connected // and disconnect afterwards when we exceed max connections rather to delay connection in case many of our peers from the list are dead. for (int i = 0; i < Math.min(8, peerManager.getMaxConnections()); i++) requestWithAvailablePeers(); } else { log.info("We don't have any reported or persisted peers, so we need to wait until we receive from the seed node the initial peer list."); } } /////////////////////////////////////////////////////////////////////////////////////////// // ConnectionListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onConnection(Connection connection) { } @Override public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { log.debug("onDisconnect closeConnectionReason={}, nodeAddressOpt={}", closeConnectionReason, connection.getPeersNodeAddressOptional()); closeHandler(connection); if (retryTimer == null) { retryTimer = UserThread.runAfter(() -> { log.trace("ConnectToMorePeersTimer called from onDisconnect code path"); stopRetryTimer(); requestWithAvailablePeers(); }, RETRY_DELAY_SEC); } if (peerManager.isPeerBanned(closeConnectionReason, connection)) { connection.getPeersNodeAddressOptional().ifPresent(seedNodeAddresses::remove); } } /////////////////////////////////////////////////////////////////////////////////////////// // PeerManager.Listener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onAllConnectionsLost() { closeAllHandlers(); stopPeriodicTimer(); stopRetryTimer(); stopped = true; restart(); } @Override public void onNewConnectionAfterAllConnectionsLost() { closeAllHandlers(); stopped = false; restart(); } @Override public void onAwakeFromStandby() { closeAllHandlers(); stopped = false; if (!networkNode.getAllConnections().isEmpty()) restart(); } /////////////////////////////////////////////////////////////////////////////////////////// // MessageListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { if (networkEnvelope instanceof GetPeersRequest) { if (!stopped) { GetPeersRequestHandler getPeersRequestHandler = new GetPeersRequestHandler(networkNode, peerManager, new GetPeersRequestHandler.Listener() { @Override public void onComplete() { log.trace("PeerExchangeHandshake completed.\n\tConnection={}", connection); } @Override public void onFault(String errorMessage, Connection connection) { log.trace("PeerExchangeHandshake failed.\n\terrorMessage={}\n\t" + "connection={}", errorMessage, connection); peerManager.handleConnectionFault(connection); } }); getPeersRequestHandler.handle((GetPeersRequest) networkEnvelope, connection); } else { log.warn("We have stopped already. We ignore that onMessage call."); } } } /////////////////////////////////////////////////////////////////////////////////////////// // Request /////////////////////////////////////////////////////////////////////////////////////////// private void requestReportedPeers(NodeAddress nodeAddress, List remainingNodeAddresses) { log.debug("requestReportedPeers nodeAddress={}; remainingNodeAddresses.size={}", nodeAddress, remainingNodeAddresses.size()); if (!stopped) { if (!handlerMap.containsKey(nodeAddress)) { PeerExchangeHandler peerExchangeHandler = new PeerExchangeHandler(networkNode, peerManager, new PeerExchangeHandler.Listener() { @Override public void onComplete() { handlerMap.remove(nodeAddress); requestWithAvailablePeers(); } @Override public void onFault(String errorMessage, @Nullable Connection connection) { log.debug("PeerExchangeHandshake of outbound connection failed.\n\terrorMessage={}\n\t" + "nodeAddress={}", errorMessage, nodeAddress); peerManager.handleConnectionFault(nodeAddress); handlerMap.remove(nodeAddress); if (!remainingNodeAddresses.isEmpty()) { if (!peerManager.hasSufficientConnections()) { log.debug("There are remaining nodes available for requesting peers. " + "We will try getReportedPeers again."); NodeAddress nextCandidate = remainingNodeAddresses.get(new Random().nextInt(remainingNodeAddresses.size())); remainingNodeAddresses.remove(nextCandidate); requestReportedPeers(nextCandidate, remainingNodeAddresses); } else { // That path will rarely be reached log.debug("We have already sufficient connections."); } } else { log.debug("There is no remaining node available for requesting peers. " + "That is expected if no other node is online.\n\t" + "We will try again after a pause."); if (retryTimer == null) retryTimer = UserThread.runAfter(() -> { if (!stopped) { log.trace("retryTimer called from requestReportedPeers code path"); stopRetryTimer(); requestWithAvailablePeers(); } else { stopRetryTimer(); log.warn("We have stopped already. We ignore that retryTimer.run call."); } }, RETRY_DELAY_SEC); } } }); handlerMap.put(nodeAddress, peerExchangeHandler); peerExchangeHandler.sendGetPeersRequestAfterRandomDelay(nodeAddress); } else { log.trace("We have started already a peerExchangeHandler. " + "We ignore that call. nodeAddress={}", nodeAddress); } } else { log.trace("We have stopped already. We ignore that requestReportedPeers call."); } } private void requestWithAvailablePeers() { if (!stopped) { if (!peerManager.hasSufficientConnections()) { // We create a new list of not connected candidates // 1. shuffled reported peers // 2. shuffled persisted peers // 3. Add as last shuffled seedNodes (least priority) List list = getFilteredNonSeedNodeList(getNodeAddresses(peerManager.getReportedPeers()), new ArrayList<>()); Collections.shuffle(list); List filteredPersistedPeers = getFilteredNonSeedNodeList(getNodeAddresses(peerManager.getPersistedPeers()), list); Collections.shuffle(filteredPersistedPeers); list.addAll(filteredPersistedPeers); List filteredSeedNodeAddresses = getFilteredList(new ArrayList<>(seedNodeAddresses), list); Collections.shuffle(filteredSeedNodeAddresses); list.addAll(filteredSeedNodeAddresses); log.debug("Number of peers in list for connectToMorePeers: {}", list.size()); log.trace("Filtered connectToMorePeers list: list={}", list); if (!list.isEmpty()) { // Don't shuffle as we want the seed nodes at the last entries NodeAddress nextCandidate = list.get(0); list.remove(nextCandidate); requestReportedPeers(nextCandidate, list); } else { log.debug("No more peers are available for requestReportedPeers. We will try again after a pause."); if (retryTimer == null) retryTimer = UserThread.runAfter(() -> { if (!stopped) { log.trace("retryTimer called from requestWithAvailablePeers code path"); stopRetryTimer(); requestWithAvailablePeers(); } else { stopRetryTimer(); log.warn("We have stopped already. We ignore that retryTimer.run call."); } }, RETRY_DELAY_SEC); } } else { log.debug("We have already sufficient connections."); } } else { log.trace("We have stopped already. We ignore that requestWithAvailablePeers call."); } } /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// private void startPeriodicTimer() { stopped = false; if (periodicTimer == null) periodicTimer = UserThread.runPeriodically(this::requestWithAvailablePeers, REQUEST_PERIODICALLY_INTERVAL_MIN, TimeUnit.MINUTES); } private void restart() { startPeriodicTimer(); if (retryTimer == null) { retryTimer = UserThread.runAfter(() -> { stopped = false; log.trace("retryTimer called from restart"); stopRetryTimer(); requestWithAvailablePeers(); }, RETRY_DELAY_AFTER_ALL_CON_LOST_SEC); } else { log.debug("retryTimer already started"); } } private List getNodeAddresses(Collection collection) { return collection.stream() .map(Peer::getNodeAddress) .collect(Collectors.toList()); } private List getFilteredList(Collection collection, List list) { return collection.stream() .filter(e -> !list.contains(e) && !peerManager.isSelf(e) && !peerManager.isConfirmed(e)) .collect(Collectors.toList()); } private List getFilteredNonSeedNodeList(Collection collection, List list) { return getFilteredList(collection, list).stream() .filter(e -> !peerManager.isSeedNode(e)) .collect(Collectors.toList()); } private void stopPeriodicTimer() { stopped = true; if (periodicTimer != null) { periodicTimer.stop(); periodicTimer = null; } } private void stopRetryTimer() { if (retryTimer != null) { retryTimer.stop(); retryTimer = null; } } private void closeHandler(Connection connection) { Optional peersNodeAddressOptional = connection.getPeersNodeAddressOptional(); if (peersNodeAddressOptional.isPresent()) { NodeAddress nodeAddress = peersNodeAddressOptional.get(); if (handlerMap.containsKey(nodeAddress)) { handlerMap.get(nodeAddress).cancel(); handlerMap.remove(nodeAddress); } } } private void closeAllHandlers() { handlerMap.values().stream().forEach(PeerExchangeHandler::cancel); handlerMap.clear(); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/peers/peerexchange/PeerList.java ================================================ /* * 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 . */ package haveno.network.p2p.peers.peerexchange; import com.google.protobuf.Message; import haveno.common.proto.persistable.PersistableEnvelope; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.util.Collection; import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; @Slf4j @EqualsAndHashCode public class PeerList implements PersistableEnvelope { @Getter private final Set set = new HashSet<>(); public PeerList() { } public PeerList(Set set) { setAll(set); } public int size() { synchronized (set) { return set.size(); } } @Override public Message toProtoMessage() { return protobuf.PersistableEnvelope.newBuilder() .setPeerList(protobuf.PeerList.newBuilder() .addAllPeer(set.stream().map(Peer::toProtoMessage).collect(Collectors.toList()))) .build(); } public static PeerList fromProto(protobuf.PeerList proto) { return new PeerList(proto.getPeerList().stream() .map(Peer::fromProto) .collect(Collectors.toSet())); } public void setAll(Collection collection) { synchronized (set) { this.set.clear(); this.set.addAll(collection); } } @Override public String toString() { return "PeerList{" + "\n set=" + set + "\n}"; } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/peers/peerexchange/messages/GetPeersRequest.java ================================================ /* * 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 . */ package haveno.network.p2p.peers.peerexchange.messages; import haveno.common.app.Capabilities; import haveno.common.app.Version; import haveno.common.proto.network.NetworkEnvelope; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.SendersNodeAddressMessage; import haveno.network.p2p.SupportedCapabilitiesMessage; import haveno.network.p2p.peers.peerexchange.Peer; import lombok.EqualsAndHashCode; import lombok.Value; import javax.annotation.Nullable; import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkNotNull; @EqualsAndHashCode(callSuper = true) @Value public final class GetPeersRequest extends NetworkEnvelope implements PeerExchangeMessage, SendersNodeAddressMessage, SupportedCapabilitiesMessage { private final NodeAddress senderNodeAddress; private final int nonce; private final Set reportedPeers; @Nullable private final Capabilities supportedCapabilities; public GetPeersRequest(NodeAddress senderNodeAddress, int nonce, Set reportedPeers) { this(senderNodeAddress, nonce, reportedPeers, Capabilities.app, Version.getP2PMessageVersion()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private GetPeersRequest(NodeAddress senderNodeAddress, int nonce, Set reportedPeers, @Nullable Capabilities supportedCapabilities, String messageVersion) { super(messageVersion); checkNotNull(senderNodeAddress, "senderNodeAddress must not be null at GetPeersRequest"); this.senderNodeAddress = senderNodeAddress; this.nonce = nonce; this.reportedPeers = reportedPeers; this.supportedCapabilities = supportedCapabilities; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { // We clone to avoid ConcurrentModificationExceptions Set clone = new HashSet<>(reportedPeers); protobuf.GetPeersRequest.Builder builder = protobuf.GetPeersRequest.newBuilder() .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setNonce(nonce) .addAllReportedPeers(clone.stream() .map(Peer::toProtoMessage) .collect(Collectors.toList())); Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities))); return getNetworkEnvelopeBuilder() .setGetPeersRequest(builder) .build(); } public static GetPeersRequest fromProto(protobuf.GetPeersRequest proto, String messageVersion) { return new GetPeersRequest(NodeAddress.fromProto(proto.getSenderNodeAddress()), proto.getNonce(), new HashSet<>(proto.getReportedPeersList().stream() .map(Peer::fromProto) .collect(Collectors.toSet())), Capabilities.fromIntList(proto.getSupportedCapabilitiesList()), messageVersion); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/peers/peerexchange/messages/GetPeersResponse.java ================================================ /* * 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 . */ package haveno.network.p2p.peers.peerexchange.messages; import haveno.common.app.Capabilities; import haveno.common.app.Version; import haveno.common.proto.network.NetworkEnvelope; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.SupportedCapabilitiesMessage; import haveno.network.p2p.peers.peerexchange.Peer; import lombok.EqualsAndHashCode; import lombok.Value; import javax.annotation.Nullable; import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @EqualsAndHashCode(callSuper = true) @Value public final class GetPeersResponse extends NetworkEnvelope implements PeerExchangeMessage, SupportedCapabilitiesMessage { private final int requestNonce; private final Set reportedPeers; @Nullable private final Capabilities supportedCapabilities; public GetPeersResponse(int requestNonce, Set reportedPeers) { this(requestNonce, reportedPeers, Capabilities.app, Version.getP2PMessageVersion()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private GetPeersResponse(int requestNonce, Set reportedPeers, @Nullable Capabilities supportedCapabilities, String messageVersion) { super(messageVersion); this.requestNonce = requestNonce; this.reportedPeers = reportedPeers; this.supportedCapabilities = supportedCapabilities; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { // We clone to avoid ConcurrentModificationExceptions Set clone = new HashSet<>(reportedPeers); protobuf.GetPeersResponse.Builder builder = protobuf.GetPeersResponse.newBuilder() .setRequestNonce(requestNonce) .addAllReportedPeers(clone.stream() .map(Peer::toProtoMessage) .collect(Collectors.toList())); Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities))); return getNetworkEnvelopeBuilder() .setGetPeersResponse(builder) .build(); } public static GetPeersResponse fromProto(protobuf.GetPeersResponse proto, String messageVersion) { HashSet reportedPeers = proto.getReportedPeersList() .stream() .map(peer -> { NodeAddress nodeAddress = new NodeAddress(peer.getNodeAddress().getHostName(), peer.getNodeAddress().getPort()); return new Peer(nodeAddress, Capabilities.fromIntList(peer.getSupportedCapabilitiesList())); }) .collect(Collectors.toCollection(HashSet::new)); return new GetPeersResponse(proto.getRequestNonce(), reportedPeers, Capabilities.fromIntList(proto.getSupportedCapabilitiesList()), messageVersion); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/peers/peerexchange/messages/PeerExchangeMessage.java ================================================ /* * 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 . */ package haveno.network.p2p.peers.peerexchange.messages; public interface PeerExchangeMessage { } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/seed/SeedNodeRepository.java ================================================ /* * 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 . */ package haveno.network.p2p.seed; import haveno.network.p2p.NodeAddress; import java.util.Collection; public interface SeedNodeRepository { boolean isSeedNode(NodeAddress nodeAddress); Collection getSeedNodeAddresses(); } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/HashMapChangedListener.java ================================================ /* * 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 . */ package haveno.network.p2p.storage; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import java.util.Collection; public interface HashMapChangedListener { void onAdded(Collection protectedStorageEntries); default void onRemoved(Collection protectedStorageEntries) { // Often we are only interested in added data as there is no use case for remove } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/P2PDataStorage.java ================================================ /* * 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 . */ package haveno.network.p2p.storage; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Maps; import com.google.inject.Inject; import com.google.inject.name.Named; import com.google.protobuf.ByteString; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.app.Capabilities; import haveno.common.crypto.CryptoException; import haveno.common.crypto.Hash; import haveno.common.crypto.Sig; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.network.GetDataResponsePriority; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.proto.network.NetworkPayload; import haveno.common.proto.persistable.PersistablePayload; import haveno.common.proto.persistable.PersistedDataHost; import haveno.common.util.Hex; import haveno.common.util.Tuple2; import haveno.common.util.Utilities; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.network.CloseConnectionReason; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.ConnectionListener; import haveno.network.p2p.network.MessageListener; import haveno.network.p2p.network.NetworkNode; import haveno.network.p2p.peers.BroadcastHandler; import haveno.network.p2p.peers.Broadcaster; import haveno.network.p2p.peers.getdata.messages.GetDataRequest; import haveno.network.p2p.peers.getdata.messages.GetDataResponse; import haveno.network.p2p.peers.getdata.messages.GetUpdatedDataRequest; import haveno.network.p2p.peers.getdata.messages.PreliminaryGetDataRequest; import haveno.network.p2p.storage.messages.AddDataMessage; import haveno.network.p2p.storage.messages.AddOncePayload; import haveno.network.p2p.storage.messages.AddPersistableNetworkPayloadMessage; import haveno.network.p2p.storage.messages.BroadcastMessage; import haveno.network.p2p.storage.messages.RefreshOfferMessage; import haveno.network.p2p.storage.messages.RemoveDataMessage; import haveno.network.p2p.storage.messages.RemoveMailboxDataMessage; import haveno.network.p2p.storage.payload.CapabilityRequiringPayload; import haveno.network.p2p.storage.payload.DateSortedTruncatablePayload; import haveno.network.p2p.storage.payload.DateTolerantPayload; import haveno.network.p2p.storage.payload.MailboxStoragePayload; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import haveno.network.p2p.storage.payload.ProcessOncePersistableNetworkPayload; import haveno.network.p2p.storage.payload.ProtectedMailboxStorageEntry; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import haveno.network.p2p.storage.payload.ProtectedStoragePayload; import haveno.network.p2p.storage.payload.RequiresOwnerIsOnlinePayload; import haveno.network.p2p.storage.persistence.AppendOnlyDataStoreListener; import haveno.network.p2p.storage.persistence.AppendOnlyDataStoreService; import haveno.network.p2p.storage.persistence.HistoricalDataStoreService; import haveno.network.p2p.storage.persistence.PersistableNetworkPayloadStore; import haveno.network.p2p.storage.persistence.ProtectedDataStoreService; import haveno.network.p2p.storage.persistence.RemovedPayloadsService; import haveno.network.p2p.storage.persistence.ResourceDataStoreService; import haveno.network.p2p.storage.persistence.SequenceNumberMap; import java.security.KeyPair; import java.security.PublicKey; import java.time.Clock; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javax.annotation.Nullable; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.monadic.MonadicBinding; @Slf4j public class P2PDataStorage implements MessageListener, ConnectionListener, PersistedDataHost { /** * How many days to keep an entry before it is purged. */ @VisibleForTesting public static final int PURGE_AGE_DAYS = 10; @VisibleForTesting public static final int CHECK_TTL_INTERVAL_SEC = 60; private boolean initialRequestApplied = false; private final Broadcaster broadcaster; @VisibleForTesting final AppendOnlyDataStoreService appendOnlyDataStoreService; private final ProtectedDataStoreService protectedDataStoreService; private final ResourceDataStoreService resourceDataStoreService; @Getter private final Map map = new ConcurrentHashMap<>(); private final Set hashMapChangedListeners = new CopyOnWriteArraySet<>(); private Timer removeExpiredEntriesTimer; private final PersistenceManager persistenceManager; @VisibleForTesting final SequenceNumberMap sequenceNumberMap = new SequenceNumberMap(); private final Set appendOnlyDataStoreListeners = new CopyOnWriteArraySet<>(); private final RemovedPayloadsService removedPayloadsService; private final Clock clock; /// The maximum number of items that must exist in the SequenceNumberMap before it is scheduled for a purge /// which removes entries after PURGE_AGE_DAYS. private final int maxSequenceNumberMapSizeBeforePurge; // Don't convert to local variable as it might get GC'ed. private MonadicBinding readFromResourcesCompleteBinding; @Setter private Predicate filterPredicate; // Set from FilterManager /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public P2PDataStorage(NetworkNode networkNode, Broadcaster broadcaster, AppendOnlyDataStoreService appendOnlyDataStoreService, ProtectedDataStoreService protectedDataStoreService, ResourceDataStoreService resourceDataStoreService, PersistenceManager persistenceManager, RemovedPayloadsService removedPayloadsService, Clock clock, @Named("MAX_SEQUENCE_NUMBER_MAP_SIZE_BEFORE_PURGE") int maxSequenceNumberBeforePurge) { this.broadcaster = broadcaster; this.appendOnlyDataStoreService = appendOnlyDataStoreService; this.protectedDataStoreService = protectedDataStoreService; this.resourceDataStoreService = resourceDataStoreService; this.persistenceManager = persistenceManager; this.removedPayloadsService = removedPayloadsService; this.clock = clock; this.maxSequenceNumberMapSizeBeforePurge = maxSequenceNumberBeforePurge; networkNode.addMessageListener(this); networkNode.addConnectionListener(this); this.persistenceManager.initialize(sequenceNumberMap, PersistenceManager.Source.PRIVATE_LOW_PRIO); } /////////////////////////////////////////////////////////////////////////////////////////// // PersistedDataHost /////////////////////////////////////////////////////////////////////////////////////////// @Override public void readPersisted(Runnable completeHandler) { persistenceManager.readPersisted(persisted -> { synchronized (persisted.getMap()) { sequenceNumberMap.setMap(getPurgedSequenceNumberMap(persisted.getMap())); } completeHandler.run(); }, completeHandler); } // Uses synchronous execution on the userThread. Only used by tests. The async methods should be used by app code. @VisibleForTesting public void readPersistedSync() { SequenceNumberMap persisted = persistenceManager.getPersisted(); if (persisted != null) { synchronized (persisted.getMap()) { sequenceNumberMap.setMap(getPurgedSequenceNumberMap(persisted.getMap())); } } } // Threading is done on the persistenceManager level public void readFromResources(String postFix, Runnable completeHandler) { BooleanProperty appendOnlyDataStoreServiceReady = new SimpleBooleanProperty(); BooleanProperty protectedDataStoreServiceReady = new SimpleBooleanProperty(); BooleanProperty resourceDataStoreServiceReady = new SimpleBooleanProperty(); readFromResourcesCompleteBinding = EasyBind.combine(appendOnlyDataStoreServiceReady, protectedDataStoreServiceReady, resourceDataStoreServiceReady, (a, b, c) -> a && b && c); readFromResourcesCompleteBinding.subscribe((observable, oldValue, newValue) -> { if (newValue) { completeHandler.run(); } }); appendOnlyDataStoreService.readFromResources(postFix, () -> appendOnlyDataStoreServiceReady.set(true)); protectedDataStoreService.readFromResources(postFix, () -> { synchronized (map) { map.putAll(protectedDataStoreService.getMap()); protectedDataStoreServiceReady.set(true); } }); resourceDataStoreService.readFromResources(postFix, () -> resourceDataStoreServiceReady.set(true)); } // Uses synchronous execution on the userThread. Only used by tests. The async methods should be used by app code. @VisibleForTesting public void readFromResourcesSync(String postFix) { synchronized (map) { appendOnlyDataStoreService.readFromResourcesSync(postFix); protectedDataStoreService.readFromResourcesSync(postFix); resourceDataStoreService.readFromResourcesSync(postFix); map.putAll(protectedDataStoreService.getMap()); } } // We get added mailbox message data from MailboxMessageService. We want to add those early so we can get it added // to our excluded keys to reduce initial data response data size. public void addProtectedMailboxStorageEntryToMap(ProtectedStorageEntry protectedStorageEntry) { synchronized (map) { ProtectedStoragePayload protectedStoragePayload = protectedStorageEntry.getProtectedStoragePayload(); ByteArray hashOfPayload = get32ByteHashAsByteArray(protectedStoragePayload); map.put(hashOfPayload, protectedStorageEntry); //log.trace("## addProtectedMailboxStorageEntryToMap hashOfPayload={}, map={}", hashOfPayload, printMap()); } } /////////////////////////////////////////////////////////////////////////////////////////// // RequestData API /////////////////////////////////////////////////////////////////////////////////////////// /** * Returns a PreliminaryGetDataRequest that can be sent to a peer node to request missing Payload data. */ public PreliminaryGetDataRequest buildPreliminaryGetDataRequest(int nonce) { return new PreliminaryGetDataRequest(nonce, getKnownPayloadHashes()); } /** * Returns a GetUpdatedDataRequest that can be sent to a peer node to request missing Payload data. */ public GetUpdatedDataRequest buildGetUpdatedDataRequest(NodeAddress senderNodeAddress, int nonce) { return new GetUpdatedDataRequest(senderNodeAddress, nonce, getKnownPayloadHashes()); } /** * Returns the set of known payload hashes. This is used in the GetData path to request missing data from peer nodes */ private Set getKnownPayloadHashes() { // We collect the keys of the PersistableNetworkPayload items so we exclude them in our request. // PersistedStoragePayload items don't get removed, so we don't have an issue with the case that // an object gets removed in between PreliminaryGetDataRequest and the GetUpdatedDataRequest and we would // miss that event if we do not load the full set or use some delta handling. Map mapForDataRequest = getMapForDataRequest(); Set excludedKeys = getKeysAsByteSet(mapForDataRequest); Set excludedKeysFromProtectedStorageEntryMap = getKeysAsByteSet(map); excludedKeys.addAll(excludedKeysFromProtectedStorageEntryMap); return excludedKeys; } /** * Returns a GetDataResponse object that contains the Payloads known locally, but not remotely. */ public GetDataResponse buildGetDataResponse( GetDataRequest getDataRequest, int maxEntriesPerType, AtomicBoolean wasPersistableNetworkPayloadsTruncated, AtomicBoolean wasProtectedStorageEntriesTruncated, Capabilities peerCapabilities) { Set excludedKeysAsByteArray = P2PDataStorage.ByteArray.convertBytesSetToByteArraySet(getDataRequest.getExcludedKeys()); // Pre v 1.4.0 requests do not have set the requesters version field so it is null. // The methods in HistoricalDataStoreService will return all historical data in that case. // mapForDataResponse contains the filtered by version data from HistoricalDataStoreService as well as all other // maps of the remaining appendOnlyDataStoreServices. Map mapForDataResponse = getMapForDataResponse(getDataRequest.getVersion()); // Give a bit of tolerance for message overhead double maxSize = Connection.getMaxPermittedMessageSize() * 0.6; // 25% of space is allocated for PersistableNetworkPayloads long limit = Math.round(maxSize * 0.25); Set filteredPersistableNetworkPayloads = filterKnownHashes( mapForDataResponse, Function.identity(), excludedKeysAsByteArray, peerCapabilities, maxEntriesPerType, limit, wasPersistableNetworkPayloadsTruncated, true); log.info("{} PersistableNetworkPayload entries remained after filtered by excluded keys. " + "Original map had {} entries.", filteredPersistableNetworkPayloads.size(), mapForDataResponse.size()); log.trace("## buildGetDataResponse filteredPersistableNetworkPayloadHashes={}", filteredPersistableNetworkPayloads.stream() .map(e -> Utilities.encodeToHex(e.getHash())) .toArray()); // We give 75% space to ProtectedStorageEntries as they contain MailBoxMessages and those can be larger. limit = Math.round(maxSize * 0.75); Set filteredProtectedStorageEntries = filterKnownHashes( map, ProtectedStorageEntry::getProtectedStoragePayload, excludedKeysAsByteArray, peerCapabilities, maxEntriesPerType, limit, wasProtectedStorageEntriesTruncated, false); log.info("{} ProtectedStorageEntry entries remained after filtered by excluded keys. " + "Original map had {} entries.", filteredProtectedStorageEntries.size(), map.size()); log.trace("## buildGetDataResponse filteredProtectedStorageEntryHashes={}", filteredProtectedStorageEntries.stream() .map(e -> get32ByteHashAsByteArray((e.getProtectedStoragePayload()))) .toArray()); boolean wasTruncated = wasPersistableNetworkPayloadsTruncated.get() || wasProtectedStorageEntriesTruncated.get(); return new GetDataResponse( filteredProtectedStorageEntries, filteredPersistableNetworkPayloads, getDataRequest.getNonce(), getDataRequest instanceof GetUpdatedDataRequest, wasTruncated); } /////////////////////////////////////////////////////////////////////////////////////////// // Utils for collecting the exclude hashes /////////////////////////////////////////////////////////////////////////////////////////// private Map getMapForDataRequest() { Map map = new HashMap<>(); appendOnlyDataStoreService.getServices() .forEach(service -> { Map serviceMap; if (service instanceof HistoricalDataStoreService) { var historicalDataStoreService = (HistoricalDataStoreService) service; // As we add the version to our request we only use the live data. Eventually missing data will be // derived from the version. serviceMap = historicalDataStoreService.getMapOfLiveData(); } else { serviceMap = service.getMap(); } map.putAll(serviceMap); log.debug("We added {} entries from {} to the excluded key set of our request", serviceMap.size(), service.getClass().getSimpleName()); }); return map; } public Map getMapForDataResponse(String requestersVersion) { Map map = new HashMap<>(); appendOnlyDataStoreService.getServices() .forEach(service -> { Map serviceMap; if (service instanceof HistoricalDataStoreService) { var historicalDataStoreService = (HistoricalDataStoreService) service; serviceMap = historicalDataStoreService.getMapSinceVersion(requestersVersion); } else { serviceMap = service.getMap(); } map.putAll(serviceMap); log.info("We added {} entries from {} to be filtered by excluded keys", serviceMap.size(), service.getClass().getSimpleName()); }); return map; } /** * Generic function that can be used to filter a Map * by a given set of keys and peer capabilities. */ static private Set filterKnownHashes( Map toFilter, Function asPayload, Set knownHashes, Capabilities peerCapabilities, int maxEntries, long limit, AtomicBoolean outTruncated, boolean isPersistableNetworkPayload) { log.info("Filter {} data based on {} knownHashes", isPersistableNetworkPayload ? "PersistableNetworkPayload" : "ProtectedStorageEntry", knownHashes.size()); AtomicLong totalSize = new AtomicLong(); AtomicBoolean exceededSizeLimit = new AtomicBoolean(); Set> entries = toFilter.entrySet(); Map numItemsByClassName = new HashMap<>(); entries.forEach(entry -> { String name = asPayload.apply(entry.getValue()).getClass().getSimpleName(); numItemsByClassName.putIfAbsent(name, new AtomicInteger()); numItemsByClassName.get(name).incrementAndGet(); }); log.info("numItemsByClassName: {}", numItemsByClassName); // Map.Entry.value can be ProtectedStorageEntry or PersistableNetworkPayload. We call it item in the steam iterations. List filteredItems = entries.stream() .filter(entry -> !knownHashes.contains(entry.getKey())) .map(Map.Entry::getValue) .filter(item -> shouldTransmitPayloadToPeer(peerCapabilities, asPayload.apply(item))) .collect(Collectors.toList()); List resultItems = new ArrayList<>(); // Truncation follows this rules // 1. Add all payloads with GetDataResponsePriority.MID // 2. Add all payloads with GetDataResponsePriority.LOW && !DateSortedTruncatablePayload until exceededSizeLimit is reached // 3. if(!exceededSizeLimit) Add all payloads with GetDataResponsePriority.LOW && DateSortedTruncatablePayload until // exceededSizeLimit is reached and truncate by maxItems (sorted by date). We add the sublist to our resultItems in // reverse order so in case we cut off at next step we cut off oldest items. // 4. We truncate list if resultList size > maxEntries // 5. Add all payloads with GetDataResponsePriority.HIGH // 1. Add all payloads with GetDataResponsePriority.MID List midPrioItems = filteredItems.stream() .filter(item -> item.getGetDataResponsePriority() == GetDataResponsePriority.MID) .collect(Collectors.toList()); resultItems.addAll(midPrioItems); log.info("Number of items with GetDataResponsePriority.MID: {}", midPrioItems.size()); // 2. Add all payloads with GetDataResponsePriority.LOW && !DateSortedTruncatablePayload until exceededSizeLimit is reached List lowPrioItems = filteredItems.stream() .filter(item -> item.getGetDataResponsePriority() == GetDataResponsePriority.LOW) .filter(item -> !(asPayload.apply(item) instanceof DateSortedTruncatablePayload)) .filter(item -> { if (exceededSizeLimit.get()) { return false; } if (totalSize.addAndGet(item.toProtoMessage().getSerializedSize()) > limit) { exceededSizeLimit.set(true); return false; } return true; }) .collect(Collectors.toList()); resultItems.addAll(lowPrioItems); log.info("Number of items with GetDataResponsePriority.LOW and !DateSortedTruncatablePayload: {}. Exceeded size limit: {}", lowPrioItems.size(), exceededSizeLimit.get()); // 3. if(!exceededSizeLimit) Add all payloads with GetDataResponsePriority.LOW && DateSortedTruncatablePayload until // exceededSizeLimit is reached and truncate by maxItems (sorted by date). We add the sublist to our resultItems in // reverse order so in case we cut off at next step we cut off oldest items. if (!exceededSizeLimit.get()) { List dateSortedItems = filteredItems.stream() .filter(item -> item.getGetDataResponsePriority() == GetDataResponsePriority.LOW) .filter(item -> asPayload.apply(item) instanceof DateSortedTruncatablePayload) .filter(item -> { if (exceededSizeLimit.get()) { return false; } if (totalSize.addAndGet(item.toProtoMessage().getSerializedSize()) > limit) { exceededSizeLimit.set(true); return false; } return true; }) .sorted(Comparator.comparing(item -> ((DateSortedTruncatablePayload) asPayload.apply(item)).getDate())) .collect(Collectors.toList()); if (!dateSortedItems.isEmpty()) { int maxItems = ((DateSortedTruncatablePayload) asPayload.apply(dateSortedItems.get(0))).maxItems(); int size = dateSortedItems.size(); if (size > maxItems) { int fromIndex = size - maxItems; dateSortedItems = dateSortedItems.subList(fromIndex, size); outTruncated.set(true); log.info("Num truncated dateSortedItems {}", size); log.info("Removed oldest {} dateSortedItems as we exceeded {}", fromIndex, maxItems); } } log.info("Number of items with GetDataResponsePriority.LOW and DateSortedTruncatablePayload: {}. Was truncated: {}", dateSortedItems.size(), outTruncated.get()); // We reverse sorting so in case we get truncated we cut off the older items Comparator comparator = Comparator.comparing(item -> ((DateSortedTruncatablePayload) asPayload.apply(item)).getDate()); dateSortedItems.sort(comparator.reversed()); resultItems.addAll(dateSortedItems); } else { log.info("No dateSortedItems added as we exceeded already the exceededSizeLimit of {}", limit); } // 4. We truncate list if resultList size > maxEntries int size = resultItems.size(); if (size > maxEntries) { resultItems = resultItems.subList(0, maxEntries); outTruncated.set(true); log.info("Removed last {} items as we exceeded {}", size - maxEntries, maxEntries); } outTruncated.set(outTruncated.get() || exceededSizeLimit.get()); // 5. Add all payloads with GetDataResponsePriority.HIGH List highPrioItems = filteredItems.stream() .filter(item -> item.getGetDataResponsePriority() == GetDataResponsePriority.HIGH) .collect(Collectors.toList()); resultItems.addAll(highPrioItems); log.info("Number of items with GetDataResponsePriority.HIGH: {}", highPrioItems.size()); log.info("Number of result items we send to requester: {}", resultItems.size()); return new HashSet<>(resultItems); } public Collection getPersistableNetworkPayloadCollection() { return getMapForDataRequest().values(); } private Set getKeysAsByteSet(Map map) { return map.keySet().stream() .map(e -> e.bytes) .collect(Collectors.toSet()); } /** * Returns true if a Payload should be transmit to a peer given the peer's supported capabilities. */ private static boolean shouldTransmitPayloadToPeer(Capabilities peerCapabilities, NetworkPayload payload) { // Sanity check to ensure this isn't used outside P2PDataStorage if (!(payload instanceof ProtectedStoragePayload || payload instanceof PersistableNetworkPayload)) return false; // If the payload doesn't have a required capability, we should transmit it if (!(payload instanceof CapabilityRequiringPayload)) return true; // Otherwise, only transmit the Payload if the peer supports all capabilities required by the payload boolean shouldTransmit = peerCapabilities.containsAll(((CapabilityRequiringPayload) payload).getRequiredCapabilities()); if (!shouldTransmit) { log.debug("We do not send the message to the peer because they do not support the required capability for that message type.\n" + "storagePayload is: " + Utilities.toTruncatedString(payload)); } return shouldTransmit; } /** * Processes a GetDataResponse message and updates internal state. Does not broadcast updates to the P2P network * or domain listeners. */ public void processGetDataResponse(GetDataResponse getDataResponse, NodeAddress sender) { Set protectedStorageEntries = getDataResponse.getDataSet(); Set persistableNetworkPayloadSet = getDataResponse.getPersistableNetworkPayloadSet(); long ts = System.currentTimeMillis(); protectedStorageEntries.forEach(protectedStorageEntry -> { // We rebroadcast high priority data after a delay for better resilience if (protectedStorageEntry.getProtectedStoragePayload().getGetDataResponsePriority() == GetDataResponsePriority.HIGH) { UserThread.runAfter(() -> { log.info("Rebroadcast {}", protectedStorageEntry.getProtectedStoragePayload().getClass().getSimpleName()); broadcaster.broadcast(new AddDataMessage(protectedStorageEntry), sender, null); }, 60); } // We don't broadcast here (last param) as we are only connected to the seed node and would be pointless addProtectedStorageEntry(protectedStorageEntry, sender, null, false); }); log.info("Processing {} protectedStorageEntries took {} ms.", protectedStorageEntries.size(), this.clock.millis() - ts); ts = this.clock.millis(); persistableNetworkPayloadSet.forEach(e -> { if (e instanceof ProcessOncePersistableNetworkPayload) { // We use an optimized method as many checks are not required in that case to avoid // performance issues. // Processing 82645 items took now 61 ms compared to earlier version where it took ages (> 2min). // Usually we only get about a few hundred or max. a few 1000 items. 82645 is all // trade stats and all account age witness data. // We only apply it once from first response if (!initialRequestApplied || getDataResponse.isWasTruncated()) { addPersistableNetworkPayloadFromInitialRequest(e); } } else { // We don't broadcast here as we are only connected to the seed node and would be pointless addPersistableNetworkPayload(e, sender, false, false, false); } }); log.info("Processing {} persistableNetworkPayloads took {} ms.", persistableNetworkPayloadSet.size(), this.clock.millis() - ts); // We only process PersistableNetworkPayloads implementing ProcessOncePersistableNetworkPayload once. It can cause performance // issues and since the data is rarely out of sync it is not worth it to apply them from multiple peers during // startup. initialRequestApplied = true; } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void shutDown() { if (removeExpiredEntriesTimer != null) removeExpiredEntriesTimer.stop(); } @VisibleForTesting void removeExpiredEntries() { synchronized (map) { // The moment when an object becomes expired will not be synchronous in the network and we could // get add network_messages after the object has expired. To avoid repeated additions of already expired // object when we get it sent from new peers, we don’t remove the sequence number from the map. // That way an ADD message for an already expired data will fail because the sequence number // is equal and not larger as expected. ArrayList> toRemoveList = map.entrySet().stream() .filter(entry -> entry.getValue().isExpired(this.clock)) .collect(Collectors.toCollection(ArrayList::new)); // Batch processing can cause performance issues, so do all of the removes first, then update the listeners // to let them know about the removes. if (log.isDebugEnabled()) { toRemoveList.forEach(toRemoveItem -> { log.debug("We found an expired data entry. We remove the protectedData:\n\t{}", Utilities.toTruncatedString(toRemoveItem.getValue())); }); } removeFromMapAndDataStore(toRemoveList); synchronized (sequenceNumberMap.getMap()) { if (sequenceNumberMap.size() > this.maxSequenceNumberMapSizeBeforePurge) { sequenceNumberMap.setMap(getPurgedSequenceNumberMap(sequenceNumberMap.getMap())); requestPersistence(); } } } } public void onBootstrapped() { removeExpiredEntriesTimer = UserThread.runPeriodically(this::removeExpiredEntries, CHECK_TTL_INTERVAL_SEC); } /////////////////////////////////////////////////////////////////////////////////////////// // MessageListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { if (networkEnvelope instanceof BroadcastMessage) { connection.getPeersNodeAddressOptional().ifPresent(peersNodeAddress -> { if (networkEnvelope instanceof AddDataMessage) { addProtectedStorageEntry(((AddDataMessage) networkEnvelope).getProtectedStorageEntry(), peersNodeAddress, null, true); } else if (networkEnvelope instanceof RemoveDataMessage) { remove(((RemoveDataMessage) networkEnvelope).getProtectedStorageEntry(), peersNodeAddress); } else if (networkEnvelope instanceof RemoveMailboxDataMessage) { remove(((RemoveMailboxDataMessage) networkEnvelope).getProtectedMailboxStorageEntry(), peersNodeAddress); } else if (networkEnvelope instanceof RefreshOfferMessage) { refreshTTL((RefreshOfferMessage) networkEnvelope, peersNodeAddress); } else if (networkEnvelope instanceof AddPersistableNetworkPayloadMessage) { addPersistableNetworkPayload(((AddPersistableNetworkPayloadMessage) networkEnvelope).getPersistableNetworkPayload(), peersNodeAddress, true, false, true); } }); } } /////////////////////////////////////////////////////////////////////////////////////////// // ConnectionListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onConnection(Connection connection) { } @Override public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { if (closeConnectionReason.isIntended) return; if (!connection.getPeersNodeAddressOptional().isPresent()) return; NodeAddress peersNodeAddress = connection.getPeersNodeAddressOptional().get(); // Backdate all the eligible payloads based on the node that disconnected synchronized (map) { map.values().stream() .filter(protectedStorageEntry -> protectedStorageEntry.getProtectedStoragePayload() instanceof RequiresOwnerIsOnlinePayload) .filter(protectedStorageEntry -> ((RequiresOwnerIsOnlinePayload) protectedStorageEntry.getProtectedStoragePayload()).getOwnerNodeAddress().equals(peersNodeAddress)) .forEach(protectedStorageEntry -> { // We only set the data back by half of the TTL and remove the data only if is has // expired after that back dating. // We might get connection drops which are not caused by the node going offline, so // we give more tolerance with that approach, giving the node the chance to // refresh the TTL with a refresh message. // We observed those issues during stress tests, but it might have been caused by the // test set up (many nodes/connections over 1 router) // TODO investigate what causes the disconnections. // Usually the are: SOCKET_TIMEOUT ,TERMINATED (EOFException) log.debug("Backdating {} due to closeConnectionReason={}", protectedStorageEntry, closeConnectionReason); protectedStorageEntry.backDate(); }); } } /////////////////////////////////////////////////////////////////////////////////////////// // Client API /////////////////////////////////////////////////////////////////////////////////////////// /** * Adds a PersistableNetworkPayload to the local P2P data storage. If it does not already exist locally, it will * be broadcast to the P2P network. * @param payload PersistableNetworkPayload to add to the network * @param sender local NodeAddress, if available * @param allowReBroadcast true if the PersistableNetworkPayload should be rebroadcast even if it * already exists locally * @return true if the PersistableNetworkPayload passes all validation and exists in the P2PDataStore * on completion */ public boolean addPersistableNetworkPayload(PersistableNetworkPayload payload, @Nullable NodeAddress sender, boolean allowReBroadcast) { return addPersistableNetworkPayload( payload, sender, true, allowReBroadcast, false); } private boolean addPersistableNetworkPayload(PersistableNetworkPayload payload, @Nullable NodeAddress sender, boolean allowBroadcast, boolean reBroadcast, boolean checkDate) { log.debug("addPersistableNetworkPayload payload={}", payload); // Payload hash size does not match expectation for that type of message. if (!payload.verifyHashSize()) { log.warn("addPersistableNetworkPayload failed due to unexpected hash size"); return false; } ByteArray hashAsByteArray = new ByteArray(payload.getHash()); boolean payloadHashAlreadyInStore = appendOnlyDataStoreService.getMap(payload).containsKey(hashAsByteArray); // Store already knows about this payload. Ignore it unless the caller specifically requests a republish. if (payloadHashAlreadyInStore && !reBroadcast) { log.debug("addPersistableNetworkPayload failed due to duplicate payload"); return false; } // DateTolerantPayloads are only checked for tolerance from the onMessage handler (checkDate == true). If not in // tolerance, ignore it. if (checkDate && payload instanceof DateTolerantPayload && !((DateTolerantPayload) payload).isDateInTolerance((clock))) { log.warn("addPersistableNetworkPayload failed due to payload time outside tolerance.\n" + "Payload={}; now={}", payload.toString(), new Date()); return false; } // Add the payload and publish the state update to the appendOnlyDataStoreListeners boolean wasAdded = false; if (!payloadHashAlreadyInStore) { wasAdded = appendOnlyDataStoreService.put(hashAsByteArray, payload); if (wasAdded) { appendOnlyDataStoreListeners.forEach(e -> e.onAdded(payload)); } } // Broadcast the payload if requested by caller if (allowBroadcast && wasAdded) broadcaster.broadcast(new AddPersistableNetworkPayloadMessage(payload), sender); return true; } // When we receive initial data we skip several checks to improve performance. We requested only missing entries so we // do not need to check again if the item is contained in the map, which is a bit slow as the map can be very large. // Overwriting an entry would be also no issue. We also skip notifying listeners as we get called before the domain // is ready so no listeners are set anyway. We might get called twice from a redundant call later, so listeners // might be added then but as we have the data already added calling them would be irrelevant as well. private void addPersistableNetworkPayloadFromInitialRequest(PersistableNetworkPayload payload) { byte[] hash = payload.getHash(); if (payload.verifyHashSize()) { ByteArray hashAsByteArray = new ByteArray(hash); appendOnlyDataStoreService.put(hashAsByteArray, payload); } else { log.warn("We got a hash exceeding our permitted size"); } } public boolean addProtectedStorageEntry(ProtectedStorageEntry protectedStorageEntry, @Nullable NodeAddress sender, @Nullable BroadcastHandler.Listener listener) { return addProtectedStorageEntry(protectedStorageEntry, sender, listener, true); } /** * Adds a ProtectedStorageEntry to the local P2P data storage and broadcast if all checks have been successful. * * @param protectedStorageEntry ProtectedStorageEntry to add to the network * @param sender Senders nodeAddress, if available * @param listener optional listener that can be used to receive events on broadcast * @param allowBroadcast Flag to allow broadcast * @return true if the ProtectedStorageEntry was added to the local P2P data storage */ private boolean addProtectedStorageEntry(ProtectedStorageEntry protectedStorageEntry, @Nullable NodeAddress sender, @Nullable BroadcastHandler.Listener listener, boolean allowBroadcast) { synchronized (map) { ProtectedStoragePayload protectedStoragePayload = protectedStorageEntry.getProtectedStoragePayload(); ByteArray hashOfPayload = get32ByteHashAsByteArray(protectedStoragePayload); //log.trace("## call addProtectedStorageEntry hash={}, map={}", hashOfPayload, printMap()); // We do that check early as it is a very common case for returning, so we return early // If we have seen a more recent operation for this payload and we have a payload locally, ignore it ProtectedStorageEntry storedEntry = map.get(hashOfPayload); if (storedEntry != null && !hasSequenceNrIncreased(protectedStorageEntry.getSequenceNumber(), hashOfPayload)) { log.trace("## hasSequenceNrIncreased is false. hash={}", hashOfPayload); return false; } if (hasAlreadyRemovedAddOncePayload(protectedStoragePayload, hashOfPayload)) { log.trace("## We have already removed that AddOncePayload by a previous removeDataMessage. " + "We ignore that message. ProtectedStoragePayload: {}", protectedStoragePayload.toString()); return false; } // To avoid that expired data get stored and broadcast we check for expire date. if (protectedStorageEntry.isExpired(clock)) { String peer = sender != null ? sender.getFullAddress() : "sender is null"; log.trace("## We received an expired protectedStorageEntry from peer {}. ProtectedStoragePayload={}", peer, protectedStorageEntry.getProtectedStoragePayload().getClass().getSimpleName()); return false; } // We want to allow add operations for equal sequence numbers if we don't have the payload locally. This is // the case for non-persistent Payloads that need to be reconstructed from peer and seed nodes each startup. MapValue sequenceNumberMapValue = sequenceNumberMap.get(hashOfPayload); if (sequenceNumberMapValue != null && protectedStorageEntry.getSequenceNumber() < sequenceNumberMapValue.sequenceNr) { log.trace("## sequenceNr too low hash={}", hashOfPayload); return false; } // Verify the ProtectedStorageEntry is well formed and valid for the add operation if (!protectedStorageEntry.isValidForAddOperation()) { log.trace("## !isValidForAddOperation hash={}", hashOfPayload); return false; } // If we have already seen an Entry with the same hash, verify the metadata is equal if (storedEntry != null && !protectedStorageEntry.matchesRelevantPubKey(storedEntry)) { log.trace("## !matchesRelevantPubKey hash={}", hashOfPayload); return false; } // Test against filterPredicate set from FilterManager if (filterPredicate != null && !filterPredicate.test(protectedStorageEntry.getProtectedStoragePayload())) { log.debug("filterPredicate test failed. hashOfPayload={}", hashOfPayload); return false; } // This is an updated entry. Record it and signal listeners. map.put(hashOfPayload, protectedStorageEntry); hashMapChangedListeners.forEach(e -> e.onAdded(Collections.singletonList(protectedStorageEntry))); // Record the updated sequence number and persist it. Higher delay so we can batch more items. sequenceNumberMap.put(hashOfPayload, new MapValue(protectedStorageEntry.getSequenceNumber(), this.clock.millis())); requestPersistence(); //log.trace("## ProtectedStorageEntry added to map. hash={}, map={}", hashOfPayload, printMap()); // Optionally, broadcast the add/update depending on the calling environment if (allowBroadcast) { broadcaster.broadcast(new AddDataMessage(protectedStorageEntry), sender, listener); log.trace("## broadcasted ProtectedStorageEntry. hash={}", hashOfPayload); } // Persist ProtectedStorageEntries carrying PersistablePayload payloads if (protectedStoragePayload instanceof PersistablePayload) protectedDataStoreService.put(hashOfPayload, protectedStorageEntry); return true; } } /** * We do not do all checks as it is used for republishing existing mailbox messages from seed nodes which * only got stored if they had been valid when we received them. * * @param protectedMailboxStorageEntry ProtectedMailboxStorageEntry to add to the network * @param sender Senders nodeAddress, if available * @param listener optional listener that can be used to receive events on broadcast */ public void republishExistingProtectedMailboxStorageEntry(ProtectedMailboxStorageEntry protectedMailboxStorageEntry, @Nullable NodeAddress sender, @Nullable BroadcastHandler.Listener listener) { ProtectedStoragePayload protectedStoragePayload = protectedMailboxStorageEntry.getProtectedStoragePayload(); ByteArray hashOfPayload = get32ByteHashAsByteArray(protectedStoragePayload); //log.trace("## call republishProtectedStorageEntry hash={}, map={}", hashOfPayload, printMap()); if (hasAlreadyRemovedAddOncePayload(protectedStoragePayload, hashOfPayload)) { log.trace("## We have already removed that AddOncePayload by a previous removeDataMessage. " + "We ignore that message. ProtectedStoragePayload: {}", protectedStoragePayload.toString()); return; } broadcaster.broadcast(new AddDataMessage(protectedMailboxStorageEntry), sender, listener); log.trace("## broadcasted ProtectedStorageEntry. hash={}", hashOfPayload); } public boolean hasAlreadyRemovedAddOncePayload(ProtectedStoragePayload protectedStoragePayload, ByteArray hashOfPayload) { return protectedStoragePayload instanceof AddOncePayload && removedPayloadsService.wasRemoved(hashOfPayload); } /** * Updates a local RefreshOffer with TTL changes and broadcasts those changes to the network * * @param refreshTTLMessage refreshTTLMessage containing the update * @param sender local NodeAddress, if available * @return true if the RefreshOffer was successfully updated and changes broadcast */ public boolean refreshTTL(RefreshOfferMessage refreshTTLMessage, @Nullable NodeAddress sender) { synchronized (map) { try { ByteArray hashOfPayload = new ByteArray(refreshTTLMessage.getHashOfPayload()); ProtectedStorageEntry storedData = map.get(hashOfPayload); if (storedData == null) { log.debug("We don't have data for that refresh message in our map. That is expected if we missed the data publishing."); return false; } ProtectedStorageEntry storedEntry = map.get(hashOfPayload); ProtectedStorageEntry updatedEntry = new ProtectedStorageEntry( storedEntry.getProtectedStoragePayload(), storedEntry.getOwnerPubKey(), refreshTTLMessage.getSequenceNumber(), refreshTTLMessage.getSignature(), this.clock); // If we have seen a more recent operation for this payload, we ignore the current one if (!hasSequenceNrIncreased(updatedEntry.getSequenceNumber(), hashOfPayload)) return false; // Verify the updated ProtectedStorageEntry is well formed and valid for update if (!updatedEntry.isValidForAddOperation()) return false; // Update the hash map with the updated entry map.put(hashOfPayload, updatedEntry); // Record the latest sequence number and persist it sequenceNumberMap.put(hashOfPayload, new MapValue(updatedEntry.getSequenceNumber(), this.clock.millis())); requestPersistence(); // Always broadcast refreshes broadcaster.broadcast(refreshTTLMessage, sender); } catch (IllegalArgumentException e) { log.error("refreshTTL failed, missing data: {}\n", e.toString(), e); return false; } return true; } } /** * Removes a ProtectedStorageEntry from the local P2P data storage. If it is successful, it will broadcast that * change to the P2P network. * * @param protectedStorageEntry ProtectedStorageEntry to add to the network * @param sender local NodeAddress, if available * @return true if the ProtectedStorageEntry was removed from the local P2P data storage and broadcast */ public boolean remove(ProtectedStorageEntry protectedStorageEntry, @Nullable NodeAddress sender) { synchronized (map) { ProtectedStoragePayload protectedStoragePayload = protectedStorageEntry.getProtectedStoragePayload(); ByteArray hashOfPayload = get32ByteHashAsByteArray(protectedStoragePayload); // If we have seen a more recent operation for this payload, ignore this one if (!hasSequenceNrIncreased(protectedStorageEntry.getSequenceNumber(), hashOfPayload)) return false; // Verify the ProtectedStorageEntry is well formed and valid for the remove operation if (!protectedStorageEntry.isValidForRemoveOperation()) return false; // If we have already seen an Entry with the same hash, verify the metadata is the same ProtectedStorageEntry storedEntry = map.get(hashOfPayload); if (storedEntry != null && !protectedStorageEntry.matchesRelevantPubKey(storedEntry)) return false; // Record the latest sequence number and persist it sequenceNumberMap.put(hashOfPayload, new MapValue(protectedStorageEntry.getSequenceNumber(), this.clock.millis())); requestPersistence(); // Update that we have seen this AddOncePayload so the next time it is seen it fails verification if (protectedStoragePayload instanceof AddOncePayload) { removedPayloadsService.addHash(hashOfPayload); } if (storedEntry != null) { // Valid remove entry, do the remove and signal listeners removeFromMapAndDataStore(protectedStorageEntry, hashOfPayload); } /* else { // This means the RemoveData or RemoveMailboxData was seen prior to the AddData. We have already updated // the SequenceNumberMap appropriately so the stale Add will not pass validation, but we still want to // broadcast the remove to peers so they can update their state appropriately } */ printData("after remove"); if (protectedStorageEntry instanceof ProtectedMailboxStorageEntry) { broadcaster.broadcast(new RemoveMailboxDataMessage((ProtectedMailboxStorageEntry) protectedStorageEntry), sender); } else { broadcaster.broadcast(new RemoveDataMessage(protectedStorageEntry), sender); } return true; } } public ProtectedStorageEntry getProtectedStorageEntry(ProtectedStoragePayload protectedStoragePayload, KeyPair ownerStoragePubKey) throws CryptoException { ByteArray hashOfData = get32ByteHashAsByteArray(protectedStoragePayload); int sequenceNumber; if (sequenceNumberMap.containsKey(hashOfData)) sequenceNumber = sequenceNumberMap.get(hashOfData).sequenceNr + 1; else sequenceNumber = 1; byte[] hashOfDataAndSeqNr = P2PDataStorage.get32ByteHash(new DataAndSeqNrPair(protectedStoragePayload, sequenceNumber)); byte[] signature = Sig.sign(ownerStoragePubKey.getPrivate(), hashOfDataAndSeqNr); return new ProtectedStorageEntry(protectedStoragePayload, ownerStoragePubKey.getPublic(), sequenceNumber, signature, this.clock); } public RefreshOfferMessage getRefreshTTLMessage(ProtectedStoragePayload protectedStoragePayload, KeyPair ownerStoragePubKey) throws CryptoException { ByteArray hashOfPayload = get32ByteHashAsByteArray(protectedStoragePayload); int sequenceNumber; if (sequenceNumberMap.containsKey(hashOfPayload)) sequenceNumber = sequenceNumberMap.get(hashOfPayload).sequenceNr + 1; else sequenceNumber = 1; byte[] hashOfDataAndSeqNr = P2PDataStorage.get32ByteHash(new DataAndSeqNrPair(protectedStoragePayload, sequenceNumber)); byte[] signature = Sig.sign(ownerStoragePubKey.getPrivate(), hashOfDataAndSeqNr); return new RefreshOfferMessage(hashOfDataAndSeqNr, signature, hashOfPayload.bytes, sequenceNumber); } public ProtectedMailboxStorageEntry getMailboxDataWithSignedSeqNr(MailboxStoragePayload expirableMailboxStoragePayload, KeyPair storageSignaturePubKey, PublicKey receiversPublicKey) throws CryptoException { ByteArray hashOfData = get32ByteHashAsByteArray(expirableMailboxStoragePayload); int sequenceNumber; if (sequenceNumberMap.containsKey(hashOfData)) sequenceNumber = sequenceNumberMap.get(hashOfData).sequenceNr + 1; else sequenceNumber = 1; byte[] hashOfDataAndSeqNr = P2PDataStorage.get32ByteHash(new DataAndSeqNrPair(expirableMailboxStoragePayload, sequenceNumber)); byte[] signature = Sig.sign(storageSignaturePubKey.getPrivate(), hashOfDataAndSeqNr); return new ProtectedMailboxStorageEntry(expirableMailboxStoragePayload, storageSignaturePubKey.getPublic(), sequenceNumber, signature, receiversPublicKey, this.clock); } public void addHashMapChangedListener(HashMapChangedListener hashMapChangedListener) { hashMapChangedListeners.add(hashMapChangedListener); } public void removeHashMapChangedListener(HashMapChangedListener hashMapChangedListener) { hashMapChangedListeners.remove(hashMapChangedListener); } public void addAppendOnlyDataStoreListener(AppendOnlyDataStoreListener listener) { appendOnlyDataStoreListeners.add(listener); } public void removeAppendOnlyDataStoreListener(AppendOnlyDataStoreListener listener) { appendOnlyDataStoreListeners.remove(listener); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void removeFromMapAndDataStore(ProtectedStorageEntry protectedStorageEntry, ByteArray hashOfPayload) { removeFromMapAndDataStore(Collections.singletonList(Maps.immutableEntry(hashOfPayload, protectedStorageEntry))); } private void removeFromMapAndDataStore(Collection> entriesToRemove) { synchronized (map) { if (entriesToRemove.isEmpty()) return; List removedProtectedStorageEntries = new ArrayList<>(entriesToRemove.size()); entriesToRemove.forEach(entry -> { ByteArray hashOfPayload = entry.getKey(); ProtectedStorageEntry protectedStorageEntry = entry.getValue(); //log.trace("## removeFromMapAndDataStore: hashOfPayload={}, map before remove={}", hashOfPayload, printMap()); map.remove(hashOfPayload); //log.trace("## removeFromMapAndDataStore: map after remove={}", printMap()); // We inform listeners even the entry was not found in our map removedProtectedStorageEntries.add(protectedStorageEntry); ProtectedStoragePayload protectedStoragePayload = protectedStorageEntry.getProtectedStoragePayload(); if (protectedStoragePayload instanceof PersistablePayload) { ProtectedStorageEntry previous = protectedDataStoreService.remove(hashOfPayload, protectedStorageEntry); if (previous == null) log.warn("We cannot remove the protectedStorageEntry from the protectedDataStoreService as it does not exist."); } }); hashMapChangedListeners.forEach(e -> e.onRemoved(removedProtectedStorageEntries)); } } private boolean hasSequenceNrIncreased(int newSequenceNumber, ByteArray hashOfData) { if (sequenceNumberMap.containsKey(hashOfData)) { int storedSequenceNumber = sequenceNumberMap.get(hashOfData).sequenceNr; if (newSequenceNumber > storedSequenceNumber) { /*log.debug("Sequence number has increased (>). sequenceNumber = " + newSequenceNumber + " / storedSequenceNumber=" + storedSequenceNumber + " / hashOfData=" + hashOfData.toString());*/ return true; } else if (newSequenceNumber == storedSequenceNumber) { if (newSequenceNumber == 0) { log.debug("Sequence number is equal to the stored one and both are 0." + "That is expected for network_messages which never got updated (mailbox msg)."); } else { log.debug("Sequence number is equal to the stored one. sequenceNumber = {} / storedSequenceNumber={}", newSequenceNumber, storedSequenceNumber); } return false; } else { log.debug("Sequence number is invalid. sequenceNumber = {} / storedSequenceNumber={} " + "That can happen if the data owner gets an old delayed data storage message.", newSequenceNumber, storedSequenceNumber); return false; } } else { return true; } } private void requestPersistence() { persistenceManager.requestPersistence(); } public static ByteArray get32ByteHashAsByteArray(NetworkPayload data) { return new ByteArray(P2PDataStorage.get32ByteHash(data)); } // Get a new map with entries older than PURGE_AGE_DAYS purged from the given map. private Map getPurgedSequenceNumberMap(Map persisted) { Map purged = new HashMap<>(); long maxAgeTs = this.clock.millis() - TimeUnit.DAYS.toMillis(PURGE_AGE_DAYS); persisted.forEach((key, value) -> { if (value.timeStamp > maxAgeTs) purged.put(key, value); }); return purged; } private void printData(String info) { if (log.isTraceEnabled()) { StringBuilder sb = new StringBuilder("\n\n------------------------------------------------------------\n"); sb.append("Data set ").append(info).append(" operation"); // We print the items sorted by hash with the payload class name and id List> tempList = map.values().stream() .map(e -> new Tuple2<>(org.bitcoinj.core.Utils.HEX.encode(get32ByteHashAsByteArray(e.getProtectedStoragePayload()).bytes), e)) .sorted(Comparator.comparing(o -> o.first)) .collect(Collectors.toList()); tempList.forEach(e -> { ProtectedStorageEntry storageEntry = e.second; ProtectedStoragePayload protectedStoragePayload = storageEntry.getProtectedStoragePayload(); MapValue mapValue = sequenceNumberMap.get(get32ByteHashAsByteArray(protectedStoragePayload)); sb.append("\n") .append("Hash=") .append(e.first) .append("; Class=") .append(protectedStoragePayload.getClass().getSimpleName()) .append("; SequenceNumbers (Object/Stored)=") .append(storageEntry.getSequenceNumber()) .append(" / ") .append(mapValue != null ? mapValue.sequenceNr : "null") .append("; TimeStamp (Object/Stored)=") .append(storageEntry.getCreationTimeStamp()) .append(" / ") .append(mapValue != null ? mapValue.timeStamp : "null") .append("; Payload=") .append(Utilities.toTruncatedString(protectedStoragePayload)); }); sb.append("\n------------------------------------------------------------\n"); log.debug(sb.toString()); //log.debug("Data set " + info + " operation: size=" + map.values().size()); } } private String printMap() { return Arrays.toString(map.entrySet().stream().map(e -> Hex.encode(e.getKey().bytes) + ": " + e.getValue().getProtectedStoragePayload().getClass().getSimpleName()).toArray()); } private String printPersistableNetworkPayloadMap(Map map) { return Arrays.toString(map.entrySet().stream().map(e -> Hex.encode(e.getKey().bytes) + ": " + e.getValue().getClass().getSimpleName()).toArray()); } /** * @param data Network payload * @return Hash of data */ public static byte[] get32ByteHash(NetworkPayload data) { return Hash.getSha256Hash(data.toProtoMessage().toByteArray()); } /////////////////////////////////////////////////////////////////////////////////////////// // Static class /////////////////////////////////////////////////////////////////////////////////////////// /** * Used as container for calculating cryptographic hash of data and sequenceNumber. */ @EqualsAndHashCode @ToString public static final class DataAndSeqNrPair implements NetworkPayload { // data are only used for calculating cryptographic hash from both values so they are kept private private final ProtectedStoragePayload protectedStoragePayload; private final int sequenceNumber; public DataAndSeqNrPair(ProtectedStoragePayload protectedStoragePayload, int sequenceNumber) { this.protectedStoragePayload = protectedStoragePayload; this.sequenceNumber = sequenceNumber; } // Used only for calculating hash of byte array from PB object @Override public com.google.protobuf.Message toProtoMessage() { return protobuf.DataAndSeqNrPair.newBuilder() .setPayload((protobuf.StoragePayload) protectedStoragePayload.toProtoMessage()) .setSequenceNumber(sequenceNumber) .build(); } } /** * Used as key object in map for cryptographic hash of stored data as byte[] as primitive data type cannot be * used as key */ @EqualsAndHashCode public static final class ByteArray implements PersistablePayload { // That object is saved to disc. We need to take care of changes to not break deserialization. public final byte[] bytes; public ByteArray(byte[] bytes) { this.bytes = bytes; verifyBytesNotEmpty(); } public void verifyBytesNotEmpty() { if (this.bytes == null) throw new IllegalArgumentException("Cannot create P2PDataStorage.ByteArray with null byte[] array argument."); if (this.bytes.length == 0) throw new IllegalArgumentException("Cannot create P2PDataStorage.ByteArray with empty byte[] array argument."); } @Override public String toString() { return "ByteArray{" + "bytes as Hex=" + Hex.encode(bytes) + '}'; } /////////////////////////////////////////////////////////////////////////////////////////// // Protobuffer /////////////////////////////////////////////////////////////////////////////////////////// @Override public protobuf.ByteArray toProtoMessage() { return protobuf.ByteArray.newBuilder().setBytes(ByteString.copyFrom(bytes)).build(); } public static ByteArray fromProto(protobuf.ByteArray proto) { return new ByteArray(proto.getBytes().toByteArray()); } /////////////////////////////////////////////////////////////////////////////////////////// // Util /////////////////////////////////////////////////////////////////////////////////////////// public String getHex() { return Utilities.encodeToHex(bytes); } public static Set convertBytesSetToByteArraySet(Set set) { return set != null ? set.stream() .map(P2PDataStorage.ByteArray::new) .collect(Collectors.toSet()) : new HashSet<>(); } } /** * Used as value in map */ @EqualsAndHashCode @ToString public static final class MapValue implements PersistablePayload { // That object is saved to disc. We need to take care of changes to not break deserialization. final public int sequenceNr; final public long timeStamp; MapValue(int sequenceNr, long timeStamp) { this.sequenceNr = sequenceNr; this.timeStamp = timeStamp; } @Override public protobuf.MapValue toProtoMessage() { return protobuf.MapValue.newBuilder().setSequenceNr(sequenceNr).setTimeStamp(timeStamp).build(); } public static MapValue fromProto(protobuf.MapValue proto) { return new MapValue(proto.getSequenceNr(), proto.getTimeStamp()); } } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/messages/AddDataMessage.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.messages; import com.google.protobuf.Message; import haveno.common.app.Version; import haveno.common.proto.network.NetworkProtoResolver; import haveno.network.p2p.storage.payload.ProtectedMailboxStorageEntry; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import lombok.EqualsAndHashCode; import lombok.Value; @EqualsAndHashCode(callSuper = true) @Value public final class AddDataMessage extends BroadcastMessage { private final ProtectedStorageEntry protectedStorageEntry; public AddDataMessage(ProtectedStorageEntry protectedStorageEntry) { this(protectedStorageEntry, Version.getP2PMessageVersion()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private AddDataMessage(ProtectedStorageEntry protectedStorageEntry, String messageVersion) { super(messageVersion); this.protectedStorageEntry = protectedStorageEntry; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { protobuf.StorageEntryWrapper.Builder builder = protobuf.StorageEntryWrapper.newBuilder(); final Message message = protectedStorageEntry.toProtoMessage(); if (protectedStorageEntry instanceof ProtectedMailboxStorageEntry) builder.setProtectedMailboxStorageEntry((protobuf.ProtectedMailboxStorageEntry) message); else builder.setProtectedStorageEntry((protobuf.ProtectedStorageEntry) message); return getNetworkEnvelopeBuilder() .setAddDataMessage(protobuf.AddDataMessage.newBuilder() .setEntry(builder)) .build(); } public static AddDataMessage fromProto(protobuf.AddDataMessage proto, NetworkProtoResolver resolver, String messageVersion) { return new AddDataMessage((ProtectedStorageEntry) resolver.fromProto(proto.getEntry()), messageVersion); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/messages/AddOncePayload.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.messages; /** * Marker interface for messages which must not be added again after a remove message has been received (e.g. MailboxMessages). */ public interface AddOncePayload { } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/messages/AddPersistableNetworkPayloadMessage.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.messages; import haveno.common.app.Version; import haveno.common.proto.network.NetworkProtoResolver; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import lombok.EqualsAndHashCode; import lombok.Value; @EqualsAndHashCode(callSuper = true) @Value public final class AddPersistableNetworkPayloadMessage extends BroadcastMessage { private final PersistableNetworkPayload persistableNetworkPayload; public AddPersistableNetworkPayloadMessage(PersistableNetworkPayload persistableNetworkPayload) { this(persistableNetworkPayload, Version.getP2PMessageVersion()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private AddPersistableNetworkPayloadMessage(PersistableNetworkPayload persistableNetworkPayload, String messageVersion) { super(messageVersion); this.persistableNetworkPayload = persistableNetworkPayload; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { return getNetworkEnvelopeBuilder() .setAddPersistableNetworkPayloadMessage(protobuf.AddPersistableNetworkPayloadMessage.newBuilder() .setPayload(persistableNetworkPayload.toProtoMessage())) .build(); } public static AddPersistableNetworkPayloadMessage fromProto(protobuf.AddPersistableNetworkPayloadMessage proto, NetworkProtoResolver resolver, String messageVersion) { return new AddPersistableNetworkPayloadMessage((PersistableNetworkPayload) resolver.fromProto(proto.getPayload()), messageVersion); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/messages/BroadcastMessage.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.messages; import haveno.common.proto.network.NetworkEnvelope; import lombok.EqualsAndHashCode; @EqualsAndHashCode(callSuper = true) public abstract class BroadcastMessage extends NetworkEnvelope { protected BroadcastMessage(String messageVersion) { super(messageVersion); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/messages/RefreshOfferMessage.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.messages; import com.google.protobuf.ByteString; import haveno.common.app.Version; import lombok.EqualsAndHashCode; import lombok.Value; @EqualsAndHashCode(callSuper = true) @Value public final class RefreshOfferMessage extends BroadcastMessage { private final byte[] hashOfDataAndSeqNr; // 32 bytes private final byte[] signature; // 46 bytes private final byte[] hashOfPayload; // 32 bytes private final int sequenceNumber; // 4 bytes public RefreshOfferMessage(byte[] hashOfDataAndSeqNr, byte[] signature, byte[] hashOfPayload, int sequenceNumber) { this(hashOfDataAndSeqNr, signature, hashOfPayload, sequenceNumber, Version.getP2PMessageVersion()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private RefreshOfferMessage(byte[] hashOfDataAndSeqNr, byte[] signature, byte[] hashOfPayload, int sequenceNumber, String messageVersion) { super(messageVersion); this.hashOfDataAndSeqNr = hashOfDataAndSeqNr; this.signature = signature; this.hashOfPayload = hashOfPayload; this.sequenceNumber = sequenceNumber; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { return getNetworkEnvelopeBuilder() .setRefreshOfferMessage(protobuf.RefreshOfferMessage.newBuilder() .setHashOfDataAndSeqNr(ByteString.copyFrom(hashOfDataAndSeqNr)) .setSignature(ByteString.copyFrom(signature)) .setHashOfPayload(ByteString.copyFrom(hashOfPayload)) .setSequenceNumber(sequenceNumber)) .build(); } public static RefreshOfferMessage fromProto(protobuf.RefreshOfferMessage proto, String messageVersion) { return new RefreshOfferMessage(proto.getHashOfDataAndSeqNr().toByteArray(), proto.getSignature().toByteArray(), proto.getHashOfPayload().toByteArray(), proto.getSequenceNumber(), messageVersion); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/messages/RemoveDataMessage.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.messages; import haveno.common.app.Version; import haveno.common.proto.network.NetworkProtoResolver; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import lombok.EqualsAndHashCode; import lombok.Value; @EqualsAndHashCode(callSuper = true) @Value public final class RemoveDataMessage extends BroadcastMessage { private final ProtectedStorageEntry protectedStorageEntry; public RemoveDataMessage(ProtectedStorageEntry protectedStorageEntry) { this(protectedStorageEntry, Version.getP2PMessageVersion()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private RemoveDataMessage(ProtectedStorageEntry protectedStorageEntry, String messageVersion) { super(messageVersion); this.protectedStorageEntry = protectedStorageEntry; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { return getNetworkEnvelopeBuilder() .setRemoveDataMessage(protobuf.RemoveDataMessage.newBuilder() .setProtectedStorageEntry((protobuf.ProtectedStorageEntry) protectedStorageEntry.toProtoMessage())) .build(); } public static RemoveDataMessage fromProto(protobuf.RemoveDataMessage proto, NetworkProtoResolver resolver, String messageVersion) { return new RemoveDataMessage(ProtectedStorageEntry.fromProto(proto.getProtectedStorageEntry(), resolver), messageVersion); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/messages/RemoveMailboxDataMessage.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.messages; import haveno.common.app.Version; import haveno.common.proto.network.NetworkProtoResolver; import haveno.network.p2p.storage.payload.ProtectedMailboxStorageEntry; import lombok.EqualsAndHashCode; import lombok.Value; @EqualsAndHashCode(callSuper = true) @Value public final class RemoveMailboxDataMessage extends BroadcastMessage { private final ProtectedMailboxStorageEntry protectedMailboxStorageEntry; public RemoveMailboxDataMessage(ProtectedMailboxStorageEntry protectedMailboxStorageEntry) { this(protectedMailboxStorageEntry, Version.getP2PMessageVersion()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private RemoveMailboxDataMessage(ProtectedMailboxStorageEntry protectedMailboxStorageEntry, String messageVersion) { super(messageVersion); this.protectedMailboxStorageEntry = protectedMailboxStorageEntry; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { return getNetworkEnvelopeBuilder() .setRemoveMailboxDataMessage(protobuf.RemoveMailboxDataMessage.newBuilder() .setProtectedStorageEntry(protectedMailboxStorageEntry.toProtoMessage())) .build(); } public static RemoveMailboxDataMessage fromProto(protobuf.RemoveMailboxDataMessage proto, NetworkProtoResolver resolver, String messageVersion) { return new RemoveMailboxDataMessage(ProtectedMailboxStorageEntry.fromProto(proto.getProtectedStorageEntry(), resolver), messageVersion); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/payload/CapabilityRequiringPayload.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.payload; import haveno.common.app.Capabilities; import haveno.common.proto.network.NetworkPayload; /** * Used for payloads which requires certain capability. *

    * This is used for TradeStatistics to be able to support old versions which don't know about that class. * We only send the data to nodes which are capable to handle that data (e.g. TradeStatistics supported from v. 0.4.9.1 on). */ public interface CapabilityRequiringPayload extends NetworkPayload { /** * @return Capabilities the other node need to support to receive that message */ Capabilities getRequiredCapabilities(); } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/payload/DateSortedTruncatablePayload.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.payload; import java.util.Date; /** * Marker interface for PersistableNetworkPayloads which get truncated at initial data response in case we exceed * the max items defined for that type of object. The truncation happens on a sorted list where we use the date for * sorting so in case of truncation we prefer to receive the most recent data. */ public interface DateSortedTruncatablePayload extends PersistableNetworkPayload { Date getDate(); int maxItems(); } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/payload/DateTolerantPayload.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.payload; import java.time.Clock; /** * Interface for PersistableNetworkPayload which only get added if the date is inside a tolerance range. * Used for AccountAgeWitness. */ public interface DateTolerantPayload extends PersistableNetworkPayload { boolean isDateInTolerance(Clock clock); } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/payload/ExpirablePayload.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.payload; import haveno.common.proto.network.NetworkPayload; /** * Messages which support a time to live *

    * Implementations: * * @see ProtectedStoragePayload * @see MailboxStoragePayload */ public interface ExpirablePayload extends NetworkPayload { /** * @return Time to live in milli seconds */ long getTTL(); } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/payload/MailboxStoragePayload.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.payload; import com.google.protobuf.ByteString; import haveno.common.crypto.Sig; import haveno.common.util.CollectionUtils; import haveno.common.util.ExtraDataMapValidator; import haveno.network.p2p.PrefixedSealedAndSignedMessage; import haveno.network.p2p.storage.messages.AddOncePayload; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; import java.security.PublicKey; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; /** * Payload which supports a time to live and sender and receiver's pub keys for storage operations. * It differs from the ProtectedExpirableMessage in the way that the sender is permitted to do an add operation * but only the receiver is permitted to remove the data. * That is the typical requirement for a mailbox like system. *

    * Typical payloads are trade or dispute network_messages to be stored when the peer is offline. * Size depends on payload but typical size is 2000-3000 bytes */ @Getter @EqualsAndHashCode @Slf4j public final class MailboxStoragePayload implements ProtectedStoragePayload, ExpirablePayload, AddOncePayload { public static final long TTL = TimeUnit.DAYS.toMillis(15); // Added in 1.5.5 public static final String EXTRA_MAP_KEY_TTL = "ttl"; private final PrefixedSealedAndSignedMessage prefixedSealedAndSignedMessage; private PublicKey senderPubKeyForAddOperation; private final byte[] senderPubKeyForAddOperationBytes; private PublicKey ownerPubKey; private final byte[] ownerPubKeyBytes; // Should be only used in emergency case if we need to add data but do not want to break backward compatibility // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new // field in a class would break that hash and therefore break the storage mechanism. // We add optional TTL entry in v 1.5.5 so we can support different TTL for trade messages and for AckMessages @Nullable private Map extraDataMap; public MailboxStoragePayload(PrefixedSealedAndSignedMessage prefixedSealedAndSignedMessage, @NotNull PublicKey senderPubKeyForAddOperation, PublicKey ownerPubKey, long ttl) { this.prefixedSealedAndSignedMessage = prefixedSealedAndSignedMessage; this.senderPubKeyForAddOperation = senderPubKeyForAddOperation; this.ownerPubKey = ownerPubKey; senderPubKeyForAddOperationBytes = Sig.getPublicKeyBytes(senderPubKeyForAddOperation); ownerPubKeyBytes = Sig.getPublicKeyBytes(ownerPubKey); // We do not permit longer TTL as the default one if (ttl < TTL) { extraDataMap = new HashMap<>(); extraDataMap.put(EXTRA_MAP_KEY_TTL, String.valueOf(ttl)); } } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private MailboxStoragePayload(PrefixedSealedAndSignedMessage prefixedSealedAndSignedMessage, byte[] senderPubKeyForAddOperationBytes, byte[] ownerPubKeyBytes, @Nullable Map extraDataMap) { this.prefixedSealedAndSignedMessage = prefixedSealedAndSignedMessage; this.senderPubKeyForAddOperationBytes = senderPubKeyForAddOperationBytes; this.ownerPubKeyBytes = ownerPubKeyBytes; this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); senderPubKeyForAddOperation = Sig.getPublicKeyFromBytes(senderPubKeyForAddOperationBytes); ownerPubKey = Sig.getPublicKeyFromBytes(ownerPubKeyBytes); } @Override public protobuf.StoragePayload toProtoMessage() { final protobuf.MailboxStoragePayload.Builder builder = protobuf.MailboxStoragePayload.newBuilder() .setPrefixedSealedAndSignedMessage(prefixedSealedAndSignedMessage.toProtoNetworkEnvelope().getPrefixedSealedAndSignedMessage()) .setSenderPubKeyForAddOperationBytes(ByteString.copyFrom(senderPubKeyForAddOperationBytes)) .setOwnerPubKeyBytes(ByteString.copyFrom(ownerPubKeyBytes)); Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); return protobuf.StoragePayload.newBuilder().setMailboxStoragePayload(builder).build(); } public static MailboxStoragePayload fromProto(protobuf.MailboxStoragePayload proto) { return new MailboxStoragePayload( PrefixedSealedAndSignedMessage.fromPayloadProto(proto.getPrefixedSealedAndSignedMessage()), proto.getSenderPubKeyForAddOperationBytes().toByteArray(), proto.getOwnerPubKeyBytes().toByteArray(), CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap()); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public long getTTL() { if (extraDataMap != null && extraDataMap.containsKey(EXTRA_MAP_KEY_TTL)) { try { long ttl = Long.parseLong(extraDataMap.get(EXTRA_MAP_KEY_TTL)); if (ttl < TTL) { return ttl; } } catch (Throwable ignore) { } } // If not set in extraDataMap or value is invalid or too large we return default TTL return TTL; } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/payload/PersistableNetworkPayload.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.payload; import haveno.common.proto.ProtoResolver; import haveno.common.proto.network.NetworkPayload; import haveno.common.proto.persistable.PersistablePayload; /** * Marker interface for NetworkPayload which gets persisted in PersistableNetworkPayloadMap. * We store it as a list in PB to keep storage size small (map would use hash as key which is in data object anyway). * Not using a map also give more tolerance with data structure changes. * This data structure does not use a verification of the owners signature. ProtectedStoragePayload is used if that is required. * Currently we use it only for the AccountAgeWitness and TradeStatistics data. * It is used for an append only data storage because removal would require owner verification. */ public interface PersistableNetworkPayload extends NetworkPayload, PersistablePayload { static PersistableNetworkPayload fromProto(protobuf.PersistableNetworkPayload payload, ProtoResolver resolver) { return (PersistableNetworkPayload) resolver.fromProto(payload); } protobuf.PersistableNetworkPayload toProtoMessage(); // Hash which will be used as key in the in-memory hashMap byte[] getHash(); boolean verifyHashSize(); } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/payload/PersistableProtectedPayload.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.payload; import haveno.common.proto.network.NetworkProtoResolver; import haveno.common.proto.persistable.PersistablePayload; /** * ProtectedStoragePayload which are persistable and removable */ public interface PersistableProtectedPayload extends ProtectedStoragePayload, PersistablePayload { static PersistableProtectedPayload fromProto(protobuf.StoragePayload storagePayload, NetworkProtoResolver networkProtoResolver) { return (PersistableProtectedPayload) networkProtoResolver.fromProto(storagePayload); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/payload/ProcessOncePersistableNetworkPayload.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.payload; import haveno.common.Payload; /** * Marker interface for PersistableNetworkPayloads that are only added during the FIRST call to * P2PDataStorage::processDataResponse. This improves performance for objects that don't go out * of sync frequently. */ public interface ProcessOncePersistableNetworkPayload extends Payload { } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/payload/ProtectedMailboxStorageEntry.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.payload; import com.google.protobuf.ByteString; import haveno.common.crypto.Sig; import haveno.common.proto.network.NetworkProtoResolver; import haveno.common.util.Utilities; import lombok.EqualsAndHashCode; import lombok.Value; import lombok.extern.slf4j.Slf4j; import java.security.PublicKey; import java.time.Clock; @Slf4j @EqualsAndHashCode(callSuper = true) @Value public class ProtectedMailboxStorageEntry extends ProtectedStorageEntry { private final byte[] receiversPubKeyBytes; transient private PublicKey receiversPubKey; public ProtectedMailboxStorageEntry(MailboxStoragePayload mailboxStoragePayload, PublicKey ownerPubKey, int sequenceNumber, byte[] signature, PublicKey receiversPubKey, Clock clock) { this(mailboxStoragePayload, Sig.getPublicKeyBytes(ownerPubKey), ownerPubKey, sequenceNumber, signature, Sig.getPublicKeyBytes(receiversPubKey), receiversPubKey, clock.millis(), clock); } private ProtectedMailboxStorageEntry(MailboxStoragePayload mailboxStoragePayload, byte[] ownerPubKeyBytes, PublicKey ownerPubKey, int sequenceNumber, byte[] signature, byte[] receiversPubKeyBytes, PublicKey receiversPubKey, long creationTimeStamp, Clock clock) { super(mailboxStoragePayload, ownerPubKeyBytes, ownerPubKey, sequenceNumber, signature, creationTimeStamp, clock); this.receiversPubKey = receiversPubKey; this.receiversPubKeyBytes = receiversPubKeyBytes; } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public MailboxStoragePayload getMailboxStoragePayload() { return (MailboxStoragePayload) getProtectedStoragePayload(); } /* * Returns true if this Entry is valid for an add operation. For mailbox Entrys, the entry owner must * match the valid sender Public Key specified in the payload. (Only sender can add) */ @Override public boolean isValidForAddOperation() { if (!this.isSignatureValid()) return false; MailboxStoragePayload mailboxStoragePayload = this.getMailboxStoragePayload(); // Verify the Entry.receiversPubKey matches the Payload.ownerPubKey. This is a requirement for removal if (!mailboxStoragePayload.getOwnerPubKey().equals(this.receiversPubKey)) { log.debug("Entry receiversPubKey does not match payload owner which is a requirement for adding MailboxStoragePayloads"); return false; } boolean result = mailboxStoragePayload.getSenderPubKeyForAddOperation().equals(this.getOwnerPubKey()); if (!result) { String res1 = this.toString(); String res2 = "null"; if (mailboxStoragePayload.getOwnerPubKey() != null) res2 = Utilities.encodeToHex(mailboxStoragePayload.getSenderPubKeyForAddOperation().getEncoded(),true); log.warn("ProtectedMailboxStorageEntry::isValidForAddOperation() failed. " + "Entry owner does not match sender key in payload:\nProtectedStorageEntry=%{}\n" + "SenderPubKeyForAddOperation=%{}", res1, res2); } return result; } /* * Returns true if the Entry is valid for a remove operation. For mailbox Entrys, the entry owner must * match the payload owner. (Only receiver can remove) */ @Override public boolean isValidForRemoveOperation() { if (!this.isSignatureValid()) return false; MailboxStoragePayload mailboxStoragePayload = this.getMailboxStoragePayload(); // Verify the Entry has the correct receiversPubKey for removal if (!mailboxStoragePayload.getOwnerPubKey().equals(this.receiversPubKey)) { log.debug("Entry receiversPubKey does not match payload owner which is a requirement for removing MailboxStoragePayloads"); return false; } boolean result = mailboxStoragePayload.getOwnerPubKey() != null && mailboxStoragePayload.getOwnerPubKey().equals(this.getOwnerPubKey()); if (!result) { String res1 = this.toString(); String res2 = "null"; if (mailboxStoragePayload.getOwnerPubKey() != null) res2 = Utilities.encodeToHex(mailboxStoragePayload.getOwnerPubKey().getEncoded(), true); log.warn("ProtectedMailboxStorageEntry::isValidForRemoveOperation() failed. " + "Entry owner does not match Payload owner:\nProtectedStorageEntry={}\n" + "PayloadOwner={}", res1, res2); } return result; } @Override /* * Returns true if the Entry metadata that is expected to stay constant between different versions of the same object * matches. For ProtectedMailboxStorageEntry, the receiversPubKey must stay the same. */ public boolean matchesRelevantPubKey(ProtectedStorageEntry protectedStorageEntry) { if (!(protectedStorageEntry instanceof ProtectedMailboxStorageEntry)) { log.error("ProtectedMailboxStorageEntry::isMetadataEquals() failed due to object type mismatch. " + "ProtectedMailboxStorageEntry required, but got\n" + protectedStorageEntry); return false; } ProtectedMailboxStorageEntry protectedMailboxStorageEntry = (ProtectedMailboxStorageEntry) protectedStorageEntry; boolean result = protectedMailboxStorageEntry.getReceiversPubKey().equals(this.receiversPubKey); if (!result) { log.warn("ProtectedMailboxStorageEntry::isMetadataEquals() failed due to metadata mismatch. " + "new.receiversPubKey=" + Utilities.bytesAsHexString(protectedMailboxStorageEntry.getReceiversPubKeyBytes()) + "stored.receiversPubKey=" + Utilities.bytesAsHexString(this.getReceiversPubKeyBytes())); } return result; } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private ProtectedMailboxStorageEntry(MailboxStoragePayload mailboxStoragePayload, byte[] ownerPubKeyBytes, int sequenceNumber, byte[] signature, byte[] receiversPubKeyBytes, long creationTimeStamp, Clock clock) { this(mailboxStoragePayload, ownerPubKeyBytes, Sig.getPublicKeyFromBytes(ownerPubKeyBytes), sequenceNumber, signature, receiversPubKeyBytes, Sig.getPublicKeyFromBytes(receiversPubKeyBytes), creationTimeStamp, clock); } public protobuf.ProtectedMailboxStorageEntry toProtoMessage() { return protobuf.ProtectedMailboxStorageEntry.newBuilder() .setEntry((protobuf.ProtectedStorageEntry) super.toProtoMessage()) .setReceiversPubKeyBytes(ByteString.copyFrom(receiversPubKeyBytes)) .build(); } public static ProtectedMailboxStorageEntry fromProto(protobuf.ProtectedMailboxStorageEntry proto, NetworkProtoResolver resolver) { ProtectedStorageEntry entry = ProtectedStorageEntry.fromProto(proto.getEntry(), resolver); return new ProtectedMailboxStorageEntry( (MailboxStoragePayload) entry.getProtectedStoragePayload(), entry.getOwnerPubKey().getEncoded(), entry.getSequenceNumber(), entry.getSignature(), proto.getReceiversPubKeyBytes().toByteArray(), entry.getCreationTimeStamp(), resolver.getClock()); } @Override public String toString() { return "ProtectedMailboxStorageEntry{" + "\n\tReceivers Public Key: " + Utilities.bytesAsHexString(receiversPubKeyBytes) + "\n" + super.toString(); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/payload/ProtectedStorageEntry.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.payload; import com.google.common.base.Preconditions; import com.google.protobuf.ByteString; import com.google.protobuf.Message; import haveno.common.crypto.CryptoException; import haveno.common.crypto.Sig; import haveno.common.proto.network.GetDataResponsePriority; import haveno.common.proto.network.NetworkPayload; import haveno.common.proto.network.NetworkProtoResolver; import haveno.common.proto.persistable.PersistablePayload; import haveno.common.util.Utilities; import haveno.network.p2p.storage.P2PDataStorage; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import java.security.PublicKey; import java.time.Clock; @Getter @EqualsAndHashCode @Slf4j public class ProtectedStorageEntry implements NetworkPayload, PersistablePayload { private final ProtectedStoragePayload protectedStoragePayload; private final byte[] ownerPubKeyBytes; transient private final PublicKey ownerPubKey; private final int sequenceNumber; private final byte[] signature; private long creationTimeStamp; public ProtectedStorageEntry(@NotNull ProtectedStoragePayload protectedStoragePayload, @NotNull PublicKey ownerPubKey, int sequenceNumber, byte[] signature, Clock clock) { this(protectedStoragePayload, Sig.getPublicKeyBytes(ownerPubKey), ownerPubKey, sequenceNumber, signature, clock.millis(), clock); } protected ProtectedStorageEntry(@NotNull ProtectedStoragePayload protectedStoragePayload, byte[] ownerPubKeyBytes, @NotNull PublicKey ownerPubKey, int sequenceNumber, byte[] signature, long creationTimeStamp, Clock clock) { Preconditions.checkArgument(!(protectedStoragePayload instanceof PersistableNetworkPayload)); this.protectedStoragePayload = protectedStoragePayload; this.ownerPubKeyBytes = ownerPubKeyBytes; this.ownerPubKey = ownerPubKey; this.sequenceNumber = sequenceNumber; this.signature = signature; // We don't allow creation date in the future, but we cannot be too strict as clocks are not synced this.creationTimeStamp = Math.min(creationTimeStamp, clock.millis()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private ProtectedStorageEntry(@NotNull ProtectedStoragePayload protectedStoragePayload, byte[] ownerPubKeyBytes, int sequenceNumber, byte[] signature, long creationTimeStamp, Clock clock) { this(protectedStoragePayload, ownerPubKeyBytes, Sig.getPublicKeyFromBytes(ownerPubKeyBytes), sequenceNumber, signature, creationTimeStamp, clock); } public Message toProtoMessage() { return protobuf.ProtectedStorageEntry.newBuilder() .setStoragePayload((protobuf.StoragePayload) protectedStoragePayload.toProtoMessage()) .setOwnerPubKeyBytes(ByteString.copyFrom(ownerPubKeyBytes)) .setSequenceNumber(sequenceNumber) .setSignature(ByteString.copyFrom(signature)) .setCreationTimeStamp(creationTimeStamp) .build(); } public protobuf.ProtectedStorageEntry toProtectedStorageEntry() { return (protobuf.ProtectedStorageEntry) toProtoMessage(); } public static ProtectedStorageEntry fromProto(protobuf.ProtectedStorageEntry proto, NetworkProtoResolver resolver) { return new ProtectedStorageEntry( ProtectedStoragePayload.fromProto(proto.getStoragePayload(), resolver), proto.getOwnerPubKeyBytes().toByteArray(), proto.getSequenceNumber(), proto.getSignature().toByteArray(), proto.getCreationTimeStamp(), resolver.getClock()); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void backDate() { if (protectedStoragePayload instanceof ExpirablePayload) creationTimeStamp -= ((ExpirablePayload) protectedStoragePayload).getTTL() / 2; } public boolean isExpired(Clock clock) { return protectedStoragePayload instanceof ExpirablePayload && (clock.millis() - creationTimeStamp) > ((ExpirablePayload) protectedStoragePayload).getTTL(); } public GetDataResponsePriority getGetDataResponsePriority() { return protectedStoragePayload.getGetDataResponsePriority(); } /* * Returns true if the Entry is valid for an add operation. For non-mailbox Entrys, the entry owner must * match the payload owner. */ public boolean isValidForAddOperation() { if (!this.isSignatureValid()) return false; // TODO: The code currently supports MailboxStoragePayload objects inside ProtectedStorageEntry. Fix this. if (protectedStoragePayload instanceof MailboxStoragePayload) { MailboxStoragePayload mailboxStoragePayload = (MailboxStoragePayload) this.getProtectedStoragePayload(); return mailboxStoragePayload.getSenderPubKeyForAddOperation().equals(this.getOwnerPubKey()); } else { boolean result = this.ownerPubKey.equals(protectedStoragePayload.getOwnerPubKey()); if (!result) { String res1 = this.toString(); String res2 = "null"; if (protectedStoragePayload.getOwnerPubKey() != null) res2 = Utilities.encodeToHex(protectedStoragePayload.getOwnerPubKey().getEncoded(), true); log.warn("ProtectedStorageEntry::isValidForAddOperation() failed. Entry owner does not match Payload owner:\n" + "ProtectedStorageEntry={}\nPayloadOwner={}", res1, res2); } return result; } } /* * Returns true if the Entry is valid for a remove operation. For non-mailbox Entrys, the entry owner must * match the payload owner. */ public boolean isValidForRemoveOperation() { // Same requirements as add() boolean result = this.isValidForAddOperation(); if (!result) { String res1 = this.toString(); String res2 = "null"; if (protectedStoragePayload.getOwnerPubKey() != null) res2 = Utilities.encodeToHex(protectedStoragePayload.getOwnerPubKey().getEncoded(), true); log.warn("ProtectedStorageEntry::isValidForRemoveOperation() failed. Entry owner does not match Payload owner:\n" + "ProtectedStorageEntry={}\nPayloadOwner={}", res1, res2); } return result; } /* * Returns true if the signature for the Entry is valid for the payload, sequence number, and ownerPubKey */ boolean isSignatureValid() { try { byte[] hashOfDataAndSeqNr = P2PDataStorage.get32ByteHash( new P2PDataStorage.DataAndSeqNrPair(this.protectedStoragePayload, this.sequenceNumber)); boolean result = Sig.verify(this.ownerPubKey, hashOfDataAndSeqNr, this.signature); if (!result) log.warn("ProtectedStorageEntry::isSignatureValid() failed.\n{}}", this); return result; } catch (CryptoException e) { log.error("ProtectedStorageEntry::isSignatureValid() exception {}", e.toString()); return false; } } /* * Returns true if the Entry metadata that is expected to stay constant between different versions of the same object * matches. */ public boolean matchesRelevantPubKey(ProtectedStorageEntry protectedStorageEntry) { boolean result = protectedStorageEntry.getOwnerPubKey().equals(this.ownerPubKey); if (!result) { log.warn("New data entry does not match our stored data. storedData.ownerPubKey={}, ownerPubKey={}}", protectedStorageEntry.getOwnerPubKey().toString(), this.ownerPubKey); } return result; } @Override public String toString() { return "ProtectedStorageEntry {" + "\n\tPayload: " + protectedStoragePayload + "\n\tOwner Public Key: " + Utilities.bytesAsHexString(this.ownerPubKeyBytes) + "\n\tSequence Number: " + this.sequenceNumber + "\n\tSignature: " + Utilities.bytesAsHexString(this.signature) + "\n\tTimestamp: " + this.creationTimeStamp + "\n} "; } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/payload/ProtectedStoragePayload.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.payload; import haveno.common.proto.network.NetworkPayload; import haveno.common.proto.network.NetworkProtoResolver; import javax.annotation.Nullable; import java.security.PublicKey; import java.util.Map; /** * Messages which support ownership protection (using signatures) and a time to live *

    * Implementations: * io.haveno.alert.Alert * io.haveno.arbitration.Arbitrator * io.haveno.trade.offer.OfferPayload */ public interface ProtectedStoragePayload extends NetworkPayload { /** * Used for check if the add or remove operation is permitted. * Only data owner can add or remove the data. * OwnerPubKey has to be equal to the ownerPubKey of the ProtectedStorageEntry * * @return The public key of the data owner. */ PublicKey getOwnerPubKey(); // Should be only used in emergency case if we need to add data but do not want to break backward compatibility // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new // field in a class would break that hash and therefore break the storage mechanism. @Nullable Map getExtraDataMap(); static ProtectedStoragePayload fromProto(protobuf.StoragePayload storagePayload, NetworkProtoResolver networkProtoResolver) { return (ProtectedStoragePayload) networkProtoResolver.fromProto(storagePayload); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/payload/RequiresOwnerIsOnlinePayload.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.payload; import haveno.common.proto.network.NetworkPayload; import haveno.network.p2p.NodeAddress; /** * Used for network_messages which require that the data owner is online. *

    * This is used for the offers to avoid dead offers in case the maker is in standby mode or the app has * terminated without sending the remove message (e.g. network connection lost or in case of a crash). */ public interface RequiresOwnerIsOnlinePayload extends NetworkPayload { /** * @return NodeAddress of the data owner */ NodeAddress getOwnerNodeAddress(); } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/persistence/AppendOnlyDataStoreListener.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.persistence; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; public interface AppendOnlyDataStoreListener { void onAdded(PersistableNetworkPayload payload); } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/persistence/AppendOnlyDataStoreService.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.persistence; import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; import haveno.network.p2p.storage.P2PDataStorage; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; /** * Used for PersistableNetworkPayload data which gets appended to a map storage. */ @Slf4j public class AppendOnlyDataStoreService { @Getter private final List, PersistableNetworkPayload>> services = new ArrayList<>(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public AppendOnlyDataStoreService() { } public void addService(MapStoreService, PersistableNetworkPayload> service) { services.add(service); } public void readFromResources(String postFix, Runnable completeHandler) { if (services.isEmpty()) { completeHandler.run(); return; } AtomicInteger remaining = new AtomicInteger(services.size()); services.forEach(service -> { service.readFromResources(postFix, () -> { if (remaining.decrementAndGet() == 0) { completeHandler.run(); } }); }); } // Uses synchronous execution on the userThread. Only used by tests. The async methods should be used by app code. @VisibleForTesting public void readFromResourcesSync(String postFix) { services.forEach(service -> service.readFromResourcesSync(postFix)); } public Map getMap(PersistableNetworkPayload payload) { return findService(payload) .map(service -> service instanceof HistoricalDataStoreService ? ((HistoricalDataStoreService) service).getMapOfAllData() : service.getMap()) .orElse(new HashMap<>()); } public boolean put(P2PDataStorage.ByteArray hashAsByteArray, PersistableNetworkPayload payload) { Optional, PersistableNetworkPayload>> optionalService = findService(payload); optionalService.ifPresent(service -> service.putIfAbsent(hashAsByteArray, payload)); return optionalService.isPresent(); } @NotNull private Optional, PersistableNetworkPayload>> findService( PersistableNetworkPayload payload) { return services.stream() .filter(service -> service.canHandle(payload)) .findAny(); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/persistence/HistoricalDataStoreService.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.persistence; import com.google.common.collect.ImmutableMap; import haveno.common.app.DevEnv; import haveno.common.app.Version; import haveno.common.persistence.PersistenceManager; import haveno.network.p2p.storage.P2PDataStorage; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import lombok.extern.slf4j.Slf4j; import java.io.File; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; /** * Manages historical data stores tagged with the release versions. * New data is added to the default map in the store (live data). Historical data is created from resource files. * For initial data requests we only use the live data as the users version is sent with the * request so the responding (seed)node can figure out if we miss any of the historical data. */ @Slf4j public abstract class HistoricalDataStoreService> extends MapStoreService { private ImmutableMap> storesByVersion; // Cache to avoid that we have to recreate the historical data at each request private ImmutableMap allHistoricalPayloads; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public HistoricalDataStoreService(File storageDir, PersistenceManager persistenceManager) { super(storageDir, persistenceManager); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// // We give back a map of our live map and all historical maps newer than the requested version. // If requestersVersion is null we return all historical data. public Map getMapSinceVersion(String requestersVersion) { // We add all our live data Map result = new HashMap<>(store.getMap()); // If we have a store with a newer version than the requesters version we will add those as well. storesByVersion.entrySet().stream() .filter(entry -> { // Old nodes not sending the version will get delivered all data if (requestersVersion == null) { log.info("The requester did not send a version. This is expected for not updated nodes."); return true; } // Otherwise we only add data if the requesters version is older then // the version of the particular store. String storeVersion = entry.getKey(); boolean newVersion = Version.isNewVersion(storeVersion, requestersVersion); String details = newVersion ? "As our historical store is a newer version we add the data to our result map." : "As the requester version is not older as our historical store we do not " + "add the data to the result map."; log.trace("The requester had version {}. Our historical data store has version {}.\n{}", requestersVersion, storeVersion, details); return newVersion; }) .map(e -> e.getValue().getMap()) .forEach(result::putAll); log.info("We found {} entries since requesters version {}", result.size(), requestersVersion); return result; } public Map getMapOfLiveData() { return store.getMap(); } public Map getMapOfAllData() { Map result = new HashMap<>(getMapOfLiveData()); result.putAll(allHistoricalPayloads); return result; } /////////////////////////////////////////////////////////////////////////////////////////// // MapStoreService /////////////////////////////////////////////////////////////////////////////////////////// @Override public Map getMap() { DevEnv.logErrorAndThrowIfDevMode("HistoricalDataStoreService.getMap should not be used by domain " + "clients but rather the custom methods getMapOfAllData, getMapOfLiveData or getMapSinceVersion"); return getMapOfAllData(); } @Override protected void put(P2PDataStorage.ByteArray hash, PersistableNetworkPayload payload) { if (anyMapContainsKey(hash)) { return; } getMapOfLiveData().put(hash, payload); requestPersistence(); } @Override protected PersistableNetworkPayload putIfAbsent(P2PDataStorage.ByteArray hash, PersistableNetworkPayload payload) { if (anyMapContainsKey(hash)) { return null; } // We do not return the value from getMapOfLiveData().put as we checked before that it does not contain any value. // So it will be always null. We still keep the return type as we override the method from MapStoreService which // follow the Map.putIfAbsent signature. getMapOfLiveData().put(hash, payload); requestPersistence(); return null; } @Override protected void readFromResources(String postFix, Runnable completeHandler) { readStore(persisted -> { log.debug("We have created the {} store for the live data and filled it with {} entries from the persisted data.", getFileName(), getMapOfLiveData().size()); // Now we add our historical data stores. Map allHistoricalPayloads = new HashMap<>(); Map> storesByVersion = new HashMap<>(); AtomicInteger numFiles = new AtomicInteger(Version.HISTORICAL_RESOURCE_FILE_VERSION_TAGS.size()); Version.HISTORICAL_RESOURCE_FILE_VERSION_TAGS.forEach(version -> readHistoricalStoreFromResources(version, postFix, allHistoricalPayloads, storesByVersion, () -> { if (numFiles.decrementAndGet() == 0) { // At last iteration we set the immutable map this.allHistoricalPayloads = ImmutableMap.copyOf(allHistoricalPayloads); this.storesByVersion = ImmutableMap.copyOf(storesByVersion); completeHandler.run(); } })); }); } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void readHistoricalStoreFromResources(String version, String postFix, Map allHistoricalPayloads, Map> storesByVersion, Runnable completeHandler) { String fileName = getFileName() + "_" + version; boolean wasCreatedFromResources = makeFileFromResourceFile(fileName, postFix); // If resource file does not exist we do not create a new store as it would never get filled. persistenceManager.readPersisted(fileName, persisted -> { storesByVersion.put(version, persisted); allHistoricalPayloads.putAll(persisted.getMap()); log.debug("We have read from {} {} historical items.", fileName, persisted.getMap().size()); pruneStore(persisted, version); completeHandler.run(); }, completeHandler::run); } private void pruneStore(PersistableNetworkPayloadStore historicalStore, String version) { Map mapOfLiveData = getMapOfLiveData(); int preLive = mapOfLiveData.size(); mapOfLiveData.keySet().removeAll(historicalStore.getMap().keySet()); int postLive = mapOfLiveData.size(); if (preLive > postLive) { log.debug("We pruned data from our live data store which are already contained in the historical data store with version {}. " + "The live map had {} entries before pruning and has {} entries afterwards.", version, preLive, postLive); } else { log.debug("No pruning from historical data store with version {} was applied", version); } requestPersistence(); } private boolean anyMapContainsKey(P2PDataStorage.ByteArray hash) { return getMapOfLiveData().containsKey(hash) || allHistoricalPayloads.containsKey(hash); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/persistence/MapStoreService.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.persistence; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.persistable.PersistableEnvelope; import haveno.common.proto.persistable.PersistablePayload; import haveno.network.p2p.storage.P2PDataStorage; import lombok.extern.slf4j.Slf4j; import java.io.File; import java.util.Map; /** * Handles persisted data which is stored in a map. * * @param * @param */ @Slf4j public abstract class MapStoreService extends StoreService { /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public MapStoreService(File storageDir, PersistenceManager persistenceManager) { super(storageDir, persistenceManager); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public abstract Map getMap(); public abstract boolean canHandle(R payload); void put(P2PDataStorage.ByteArray hash, R payload) { getMap().put(hash, payload); requestPersistence(); } protected R putIfAbsent(P2PDataStorage.ByteArray hash, R payload) { R previous = getMap().putIfAbsent(hash, payload); requestPersistence(); return previous; } R remove(P2PDataStorage.ByteArray hash) { R result = getMap().remove(hash); requestPersistence(); return result; } boolean containsKey(P2PDataStorage.ByteArray hash) { return getMap().containsKey(hash); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/persistence/PersistableNetworkPayloadStore.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.persistence; import haveno.common.proto.persistable.PersistableEnvelope; import haveno.network.p2p.storage.P2PDataStorage; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.util.Collection; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Store for PersistableNetworkPayload map entries with it's data hash as key. */ @Slf4j public abstract class PersistableNetworkPayloadStore implements PersistableEnvelope { @Getter protected final Map map = new ConcurrentHashMap<>(); protected PersistableNetworkPayloadStore() { } protected PersistableNetworkPayloadStore(Collection collection) { collection.forEach(item -> map.put(new P2PDataStorage.ByteArray(item.getHash()), item)); } public boolean containsKey(P2PDataStorage.ByteArray hash) { return map.containsKey(hash); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/persistence/ProtectedDataStoreService.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.persistence; import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; import haveno.common.proto.persistable.PersistableEnvelope; import haveno.network.p2p.storage.P2PDataStorage; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; /** * Used for data which can be added and removed. ProtectedStorageEntry is used for verifying ownership. */ @Slf4j public class ProtectedDataStoreService { private final List> services = new ArrayList<>(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public ProtectedDataStoreService() { } public void addService(MapStoreService service) { services.add(service); } public void readFromResources(String postFix, Runnable completeHandler) { if (services.isEmpty()) { completeHandler.run(); return; } AtomicInteger remaining = new AtomicInteger(services.size()); services.forEach(service -> { service.readFromResources(postFix, () -> { if (remaining.decrementAndGet() == 0) { completeHandler.run(); } }); }); } // Uses synchronous execution on the userThread. Only used by tests. The async methods should be used by app code. @VisibleForTesting public void readFromResourcesSync(String postFix) { services.forEach(service -> service.readFromResourcesSync(postFix)); } public Map getMap() { return services.stream() .flatMap(service -> service.getMap().entrySet().stream()) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } public void put(P2PDataStorage.ByteArray hash, ProtectedStorageEntry entry) { services.stream() .filter(service -> service.canHandle(entry)) .forEach(service -> { service.put(hash, entry); }); } public ProtectedStorageEntry remove(P2PDataStorage.ByteArray hash, ProtectedStorageEntry protectedStorageEntry) { AtomicReference result = new AtomicReference<>(); services.stream() .filter(service -> service.canHandle(protectedStorageEntry)) .forEach(service -> result.set(service.remove(hash))); return result.get(); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/persistence/RemovedPayloadsMap.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.persistence; import haveno.common.proto.persistable.PersistableEnvelope; import haveno.common.util.Utilities; import haveno.network.p2p.storage.P2PDataStorage; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; @Slf4j public class RemovedPayloadsMap implements PersistableEnvelope { @Getter private final Map dateByHashes; public RemovedPayloadsMap() { this.dateByHashes = new HashMap<>(); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private RemovedPayloadsMap(Map dateByHashes) { this.dateByHashes = dateByHashes; } // Protobuf map only supports strings or integers as key, but no bytes or complex object so we convert the // bytes to a hex string, otherwise we would need to make a extra value object to wrap it. @Override public protobuf.PersistableEnvelope toProtoMessage() { protobuf.RemovedPayloadsMap.Builder builder = protobuf.RemovedPayloadsMap.newBuilder() .putAllDateByHashes(dateByHashes.entrySet().stream() .collect(Collectors.toMap(e -> Utilities.encodeToHex(e.getKey().bytes), Map.Entry::getValue))); return protobuf.PersistableEnvelope.newBuilder() .setRemovedPayloadsMap(builder) .build(); } public static RemovedPayloadsMap fromProto(protobuf.RemovedPayloadsMap proto) { Map dateByHashes = proto.getDateByHashesMap().entrySet().stream() .collect(Collectors.toMap(e -> new P2PDataStorage.ByteArray(Utilities.decodeFromHex(e.getKey())), Map.Entry::getValue)); return new RemovedPayloadsMap(dateByHashes); } @Override public String toString() { return "RemovedPayloadsMap{" + "\n dateByHashes=" + dateByHashes + "\n}"; } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/persistence/RemovedPayloadsService.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.persistence; import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.persistable.PersistedDataHost; import haveno.network.p2p.storage.P2PDataStorage; import haveno.network.p2p.storage.payload.MailboxStoragePayload; import lombok.extern.slf4j.Slf4j; /** * We persist the hashes and timestamp when a AddOncePayload payload got removed. This protects that it could be * added again for instance if the sequence number map would be inconsistent/deleted or when we receive data from * seed nodes where we do skip some checks. */ @Singleton @Slf4j public class RemovedPayloadsService implements PersistedDataHost { private final PersistenceManager persistenceManager; private final RemovedPayloadsMap removedPayloadsMap = new RemovedPayloadsMap(); @Inject public RemovedPayloadsService(PersistenceManager persistenceManager) { this.persistenceManager = persistenceManager; this.persistenceManager.initialize(removedPayloadsMap, PersistenceManager.Source.PRIVATE_LOW_PRIO); } /////////////////////////////////////////////////////////////////////////////////////////// // PersistedDataHost /////////////////////////////////////////////////////////////////////////////////////////// @Override public void readPersisted(Runnable completeHandler) { long cutOffDate = System.currentTimeMillis() - MailboxStoragePayload.TTL; persistenceManager.readPersisted(persisted -> { persisted.getDateByHashes().entrySet().stream() .filter(e -> e.getValue() > cutOffDate) .forEach(e -> removedPayloadsMap.getDateByHashes().put(e.getKey(), e.getValue())); log.trace("## readPersisted: removedPayloadsMap size={}", removedPayloadsMap.getDateByHashes().size()); persistenceManager.requestPersistence(); completeHandler.run(); }, completeHandler); } public boolean wasRemoved(P2PDataStorage.ByteArray hashOfPayload) { log.trace("## called wasRemoved: hashOfPayload={}, removedPayloadsMap={}", hashOfPayload.toString(), removedPayloadsMap); return removedPayloadsMap.getDateByHashes().containsKey(hashOfPayload); } public void addHash(P2PDataStorage.ByteArray hashOfPayload) { log.trace("## called addHash: hashOfPayload={}, removedPayloadsMap={}", hashOfPayload.toString(), removedPayloadsMap); removedPayloadsMap.getDateByHashes().putIfAbsent(hashOfPayload, System.currentTimeMillis()); persistenceManager.requestPersistence(); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/persistence/ResourceDataStoreService.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.persistence; import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; import haveno.common.proto.persistable.PersistableEnvelope; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import lombok.extern.slf4j.Slf4j; /** * Used for handling data from resource files. */ @Slf4j public class ResourceDataStoreService { private final List> services; @Inject public ResourceDataStoreService() { services = new ArrayList<>(); } public void addService(StoreService service) { services.add(service); } public void readFromResources(String postFix, Runnable completeHandler) { if (services.isEmpty()) { completeHandler.run(); return; } AtomicInteger remaining = new AtomicInteger(services.size()); services.forEach(service -> { service.readFromResources(postFix, () -> { if (remaining.decrementAndGet() == 0) { completeHandler.run(); } }); }); } // Uses synchronous execution on the userThread. Only used by tests. The async methods should be used by app code. @VisibleForTesting public void readFromResourcesSync(String postFix) { services.forEach(service -> service.readFromResourcesSync(postFix)); } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/persistence/SequenceNumberMap.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.persistence; import haveno.common.proto.persistable.PersistableEnvelope; import haveno.network.p2p.storage.P2PDataStorage; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; /** * This class was not generalized to HashMapPersistable (like we did with #ListPersistable) because * in protobuffer the map construct can't be anything, so the straightforward mapping was not possible. * Hence this Persistable class. */ public class SequenceNumberMap implements PersistableEnvelope { private Map map = new ConcurrentHashMap<>(); public SequenceNumberMap() { } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// private SequenceNumberMap(Map map) { synchronized (this.map) { this.map.putAll(map); } } @Override public protobuf.PersistableEnvelope toProtoMessage() { synchronized (map) { return protobuf.PersistableEnvelope.newBuilder() .setSequenceNumberMap(protobuf.SequenceNumberMap.newBuilder() .addAllSequenceNumberEntries(map.entrySet().stream() .map(entry -> protobuf.SequenceNumberEntry.newBuilder() .setBytes(entry.getKey().toProtoMessage()) .setMapValue(entry.getValue().toProtoMessage()) .build()) .collect(Collectors.toList()))) .build(); } } public static SequenceNumberMap fromProto(protobuf.SequenceNumberMap proto) { HashMap map = new HashMap<>(); proto.getSequenceNumberEntriesList() .forEach(e -> map.put(P2PDataStorage.ByteArray.fromProto(e.getBytes()), P2PDataStorage.MapValue.fromProto(e.getMapValue()))); return new SequenceNumberMap(map); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public Map getMap() { synchronized (map) { return map; } } public void setMap(Map map) { synchronized (this.map) { this.map = map; } } // Delegates public int size() { synchronized (map) { return map.size(); } } public boolean containsKey(P2PDataStorage.ByteArray key) { synchronized (map) { return map.containsKey(key); } } public P2PDataStorage.MapValue get(P2PDataStorage.ByteArray key) { synchronized (map) { return map.get(key); } } public void put(P2PDataStorage.ByteArray key, P2PDataStorage.MapValue value) { synchronized (map) { map.put(key, value); } } } ================================================ FILE: p2p/src/main/java/haveno/network/p2p/storage/persistence/StoreService.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.persistence; import com.google.common.annotations.VisibleForTesting; import haveno.common.file.FileUtil; import haveno.common.file.ResourceNotFoundException; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.persistable.PersistableEnvelope; import lombok.extern.slf4j.Slf4j; import java.io.File; import java.nio.file.Paths; import java.util.function.Consumer; /** * Base class for handling of persisted data. *

    * We handle several different cases: *

    * 1 Check if local db file exists. * 1a If it does not exist try to read the resource file. * 1aa If the resource file exists we copy it and use that as our local db file. We are done. * 1ab If the resource file does not exist we create a new fresh/empty db file. We are done. * 1b If we have already a local db file we read it. We are done. */ @Slf4j public abstract class StoreService { protected final PersistenceManager persistenceManager; protected final String absolutePathOfStorageDir; protected T store; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public StoreService(File storageDir, PersistenceManager persistenceManager) { this.persistenceManager = persistenceManager; absolutePathOfStorageDir = storageDir.getAbsolutePath(); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// protected void requestPersistence() { persistenceManager.requestPersistence(); } protected T getStore() { return store; } public abstract String getFileName(); /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// protected void readFromResources(String postFix, Runnable completeHandler) { String fileName = getFileName(); makeFileFromResourceFile(fileName, postFix); try { readStore(persisted -> completeHandler.run()); } catch (Throwable t) { makeFileFromResourceFile(fileName, postFix); readStore(persisted -> completeHandler.run()); } } // Uses synchronous execution on the userThread. Only used by tests. The async methods should be used by app code. @VisibleForTesting protected void readFromResourcesSync(String postFix) { String fileName = getFileName(); makeFileFromResourceFile(fileName, postFix); try { readStoreSync(); } catch (Throwable t) { makeFileFromResourceFile(fileName, postFix); readStoreSync(); } } protected boolean makeFileFromResourceFile(String fileName, String postFix) { String resourceFileName = fileName + postFix; File dbDir = new File(absolutePathOfStorageDir); if (!dbDir.exists() && !dbDir.mkdir()) log.warn("make dir failed.\ndbDir=" + dbDir.getAbsolutePath()); File destinationFile = new File(Paths.get(absolutePathOfStorageDir, fileName).toString()); if (!destinationFile.exists()) { try { log.debug("We copy resource to file: resourceFileName={}, destinationFile={}", resourceFileName, destinationFile); FileUtil.resourceToFile(resourceFileName, destinationFile); return true; } catch (ResourceNotFoundException e) { log.debug("Could not find resourceFile " + resourceFileName + ". That is expected if none is provided yet."); } catch (Throwable e) { log.error("Could not copy resourceFile " + resourceFileName + " to " + destinationFile.getAbsolutePath() + ".\n", e); } } else { log.debug("No resource file was copied. {} exists already.", fileName); } return false; } protected void readStore(String fileName, Consumer consumer) { persistenceManager.readPersisted(fileName, consumer, () -> consumer.accept(createStore())); } protected void readStore(Consumer consumer) { readStore(getFileName(), persisted -> { store = persisted; initializePersistenceManager(); consumer.accept(persisted); }); } // Uses synchronous execution on the userThread. Only used by tests. The async methods should be used by app code. @VisibleForTesting protected T getStoreSync(String fileName) { T store = persistenceManager.getPersisted(fileName); if (store == null) { store = createStore(); } return store; } // Uses synchronous execution on the userThread. Only used by tests. The async methods should be used by app code. @VisibleForTesting protected void readStoreSync() { store = getStoreSync(getFileName()); initializePersistenceManager(); } protected abstract void initializePersistenceManager(); protected abstract T createStore(); } ================================================ FILE: p2p/src/main/java/haveno/network/utils/CapabilityUtils.java ================================================ /* * 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 . */ package haveno.network.utils; import haveno.common.app.Capabilities; import haveno.common.proto.network.NetworkEnvelope; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.peers.PeerManager; import haveno.network.p2p.storage.payload.CapabilityRequiringPayload; import lombok.extern.slf4j.Slf4j; import java.util.Optional; @Slf4j public class CapabilityUtils { public static boolean capabilityRequiredAndCapabilityNotSupported(NodeAddress peersNodeAddress, NetworkEnvelope message, PeerManager peerManager) { if (!(message instanceof CapabilityRequiringPayload)) return false; // We might have multiple entries of the same peer without the supportedCapabilities field set if we received // it from old versions, so we filter those. Optional optionalCapabilities = peerManager.findPeersCapabilities(peersNodeAddress); if (optionalCapabilities.isPresent()) { boolean result = optionalCapabilities.get().containsAll(((CapabilityRequiringPayload) message).getRequiredCapabilities()); if (!result) log.warn("We don't send the message because the peer does not support the required capability. " + "peersNodeAddress={}", peersNodeAddress); return !result; } log.warn("We don't have the peer in our persisted peers so we don't know their capabilities. " + "We decide to not sent the msg. peersNodeAddress={}", peersNodeAddress); return true; } } ================================================ FILE: p2p/src/main/java/haveno/network/utils/Utils.java ================================================ /* * 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 . */ package haveno.network.utils; import java.io.IOException; import java.net.ServerSocket; import java.util.Random; public class Utils { public static int findFreeSystemPort() { try { ServerSocket server = new ServerSocket(0); int port = server.getLocalPort(); server.close(); return port; } catch (IOException ignored) { return new Random().nextInt(10000) + 50000; } } public static boolean isV3Address(String address) { return address.matches("[a-z2-7]{56}.onion"); } } ================================================ FILE: p2p/src/test/java/haveno/network/crypto/EncryptionServiceTests.java ================================================ /* * 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 . */ package haveno.network.crypto; import haveno.common.crypto.CryptoException; import haveno.common.crypto.KeyRing; import haveno.common.crypto.KeyStorage; import haveno.common.file.FileUtil; import haveno.common.proto.network.NetworkEnvelope; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; public class EncryptionServiceTests { private static final Logger log = LoggerFactory.getLogger(EncryptionServiceTests.class); private KeyRing keyRing; private File dir; @BeforeEach public void setup() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, CryptoException { dir = File.createTempFile("temp_tests", ""); //noinspection ResultOfMethodCallIgnored dir.delete(); //noinspection ResultOfMethodCallIgnored dir.mkdir(); KeyStorage keyStorage = new KeyStorage(dir); keyRing = new KeyRing(keyStorage, null, true); } @AfterEach public void tearDown() throws IOException { FileUtil.deleteDirectory(dir); } //TODO Use NetworkProtoResolver, PersistenceProtoResolver or ProtoResolver which are all in io.haveno.common. /* @Test public void testDecryptAndVerifyMessage() throws CryptoException { EncryptionService encryptionService = new EncryptionService(keyRing, TestUtils.getProtobufferResolver()); final PrivateNotificationPayload privateNotification = new PrivateNotificationPayload("test"); privateNotification.setSigAndPubKey("", pubKeyRing.getSignaturePubKey()); final NodeAddress nodeAddress = new NodeAddress("localhost", 2222); PrivateNotificationMessage data = new PrivateNotificationMessage(privateNotification, nodeAddress, UUID.randomUUID().toString()); PrefixedSealedAndSignedMessage encrypted = new PrefixedSealedAndSignedMessage(nodeAddress, encryptionService.encryptAndSign(pubKeyRing, data), Hash.getHash("localhost"), UUID.randomUUID().toString()); DecryptedMsgWithPubKey decrypted = encryptionService.decryptAndVerify(encrypted.sealedAndSigned); assertEquals(data.privateNotificationPayload.message, ((PrivateNotificationMessage) decrypted.message).privateNotificationPayload.message); } @Test public void testDecryptHybridWithSignature() { long ts = System.currentTimeMillis(); log.trace("start "); for (int i = 0; i < 100; i++) { Ping payload = new Ping(new Random().nextInt(), 10); SealedAndSigned sealedAndSigned = null; try { sealedAndSigned = Encryption.encryptHybridWithSignature(payload, keyRing.getSignatureKeyPair(), keyRing.getPubKeyRing().getEncryptionPubKey()); } catch (CryptoException e) { log.error("encryptHybridWithSignature failed"); e.printStackTrace(); assertTrue(false); } try { EncryptionService encryptionService = new EncryptionService(null, TestUtils.getProtobufferResolver()); DecryptedDataTuple tuple = encryptionService.decryptHybridWithSignature(sealedAndSigned, keyRing.getEncryptionKeyPair().getPrivate()); assertEquals(((Ping) tuple.payload).nonce, payload.nonce); } catch (CryptoException e) { log.error("decryptHybridWithSignature failed"); e.printStackTrace(); assertTrue(false); } } log.trace("took " + (System.currentTimeMillis() - ts) + " ms."); }*/ private static class MockMessage extends NetworkEnvelope { public final int nonce; public MockMessage(int nonce) { super("0"); this.nonce = nonce; } @Override public String getMessageVersion() { return "0"; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { return protobuf.NetworkEnvelope.newBuilder().setPing(protobuf.Ping.newBuilder().setNonce(nonce)).build(); } } } /*@Value final class TestMessage implements MailboxMessage { public String data = "test"; private final String messageVersion = Version.getP2PMessageVersion(); private final String uid; private final String senderNodeAddress; public TestMessage(String data) { this.data = data; uid = UUID.randomUUID().toString(); senderNodeAddress = null; } @Override public PB.NetworkEnvelope toProtoNetworkEnvelope() { throw new NotImplementedException(); } }*/ ================================================ FILE: p2p/src/test/java/haveno/network/p2p/DummySeedNode.java ================================================ /* * 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 . */ package haveno.network.p2p; import ch.qos.logback.classic.Level; import com.google.common.annotations.VisibleForTesting; import haveno.common.UserThread; import haveno.common.app.Log; import haveno.common.app.Version; import haveno.common.config.Config; import haveno.common.util.Utilities; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import static com.google.common.base.Preconditions.checkArgument; // Previously used seednode class, replaced now by bootstrap module. We keep it here as it was used in tests... @SuppressWarnings("ALL") public class DummySeedNode { private static final Logger log = LoggerFactory.getLogger(DummySeedNode.class); public static final int MAX_CONNECTIONS_LIMIT = 1000; public static final int MAX_CONNECTIONS_DEFAULT = 50; public static final String SEED_NODES_LIST = "seedNodes"; public static final String HELP = "help"; private NodeAddress mySeedNodeAddress = new NodeAddress("localhost:8001"); private int maxConnections = MAX_CONNECTIONS_DEFAULT; // we keep default a higher connection size for seed nodes private boolean useLocalhostForP2P = false; private Set progArgSeedNodes; private P2PService seedNodeP2PService; private boolean stopped; private final String defaultUserDataDir; private Level logLevel = Level.WARN; public DummySeedNode(String defaultUserDataDir) { this.defaultUserDataDir = defaultUserDataDir; } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// // args: bitcoinNetworkId maxConnections useLocalhostForP2P seedNodes (separated with |) // 2. and 3. args are optional // eg. lmvdenjkyvx2ovga.onion:8001 0 20 false eo5ay2lyzrfvx2nr.onion:8002|si3uu56adkyqkldl.onion:8003 // or when using localhost: localhost:8001 2 20 true localhost:8002|localhost:8003 // BitcoinNetworkId: The id for the bitcoin network (Mainnet = 0, TestNet = 1, Regtest = 2) // localhost:3002 2 50 true // localhost:3002 2 50 localhost:4442|localhost:4443 true // Usage: -networkId= -maxConnections= -useLocalhostForP2P=false -seedNodes=si3uu56adkyqkldl.onion:8002|eo5ay2lyzrfvx2nr.onion:8002 -ignore=4543y2lyzrfvx2nr.onion:8002|876572lyzrfvx2nr.onion:8002 // Example usage: -networkId=0 -maxConnections=20 -useLocalhostForP2P=false -seedNodes=si3uu56adkyqkldl.onion:8002|eo5ay2lyzrfvx2nr.onion:8002 -ignore=4543y2lyzrfvx2nr.onion:8002|876572lyzrfvx2nr.onion:8002 public static final String USAGE = "Usage:\n" + "--networkId=[0|1|2] (Mainnet = 0, TestNet = 1, Regtest = 2)\n" + "--maxConnections=\n" + "--useLocalhostForP2P=[true|false]\n" + "--logLevel=Log level [OFF, ALL, ERROR, WARN, INFO, DEBUG, TRACE]\n" + "--seedNodes=[onion addresses separated with comma]\n" + "--banList=[onion addresses separated with comma]\n" + "--help"; public void processArgs(String[] args) { int networkId = -1; try { for (String arg1 : args) { String arg = arg1; if (arg.startsWith("--")) arg = arg.substring(2); if (arg.startsWith("networkId")) { arg = arg.substring("networkId".length() + 1); networkId = Integer.parseInt(arg); log.debug("From processArgs: networkId=" + networkId); checkArgument(networkId > -1 && networkId < 3, "networkId out of scope (Mainnet = 0, TestNet = 1, Regtest = 2)"); Version.setBaseCryptoNetworkId(networkId); } else if (arg.startsWith(Config.MAX_CONNECTIONS)) { arg = arg.substring(Config.MAX_CONNECTIONS.length() + 1); maxConnections = Integer.parseInt(arg); log.debug("From processArgs: maxConnections=" + maxConnections); checkArgument(maxConnections < MAX_CONNECTIONS_LIMIT, "maxConnections seems to be a bit too high..."); } else if (arg.startsWith(Config.USE_LOCALHOST_FOR_P2P)) { arg = arg.substring(Config.USE_LOCALHOST_FOR_P2P.length() + 1); checkArgument(arg.equals("true") || arg.equals("false")); useLocalhostForP2P = ("true").equals(arg); log.debug("From processArgs: useLocalhostForP2P=" + useLocalhostForP2P); } else if (arg.startsWith(Config.LOG_LEVEL)) { arg = arg.substring(Config.LOG_LEVEL.length() + 1); logLevel = Level.toLevel(arg.toUpperCase()); log.debug("From processArgs: logLevel=" + logLevel); } else if (arg.startsWith(SEED_NODES_LIST)) { arg = arg.substring(SEED_NODES_LIST.length() + 1); checkArgument(arg.contains(":") && arg.split(":").length > 1 && arg.split(":")[1].length() > 3, "Wrong program argument " + arg); List list = Arrays.asList(arg.split(",")); progArgSeedNodes = new HashSet<>(); list.forEach(e -> { checkArgument(e.contains(":") && e.split(":").length == 2 && e.split(":")[1].length() == 4, "Wrong program argument " + e); progArgSeedNodes.add(new NodeAddress(e)); }); log.debug("From processArgs: progArgSeedNodes=" + progArgSeedNodes); progArgSeedNodes.remove(mySeedNodeAddress); } else if (arg.startsWith(Config.BAN_LIST)) { arg = arg.substring(Config.BAN_LIST.length() + 1); checkArgument(arg.contains(":") && arg.split(":").length > 1 && arg.split(":")[1].length() > 3, "Wrong program argument " + arg); List list = Arrays.asList(arg.split(",")); list.forEach(e -> { checkArgument(e.contains(":") && e.split(":").length == 2 && e.split(":")[1].length() == 4, "Wrong program argument " + e); }); log.debug("From processArgs: ignoreList=" + list); } else if (arg.startsWith(HELP)) { log.debug(USAGE); } else { log.error("Invalid argument. " + arg + "\n" + USAGE); } } if (mySeedNodeAddress == null) log.error("My seed node must be set.\n" + USAGE); if (networkId == -1) log.error("NetworkId must be set.\n" + USAGE); } catch (Throwable t) { log.error("Some arguments caused an exception. " + Arrays.toString(args) + "\nException: " + t.getMessage() + "\n" + USAGE); shutDown(); } } public void createAndStartP2PService(boolean useDetailedLogging) { createAndStartP2PService(mySeedNodeAddress, maxConnections, useLocalhostForP2P, Version.getBaseCurrencyNetwork(), useDetailedLogging, progArgSeedNodes, null); } @VisibleForTesting public void createAndStartP2PService(NodeAddress mySeedNodeAddress, int maxConnections, boolean useLocalhostForP2P, int networkId, @SuppressWarnings("UnusedParameters") boolean useDetailedLogging, @Nullable Set progArgSeedNodes, @Nullable P2PServiceListener listener) { Path appPath = Paths.get(defaultUserDataDir, "haveno_seed_node_" + String.valueOf(mySeedNodeAddress.getFullAddress().replace(":", "_"))); String logPath = Paths.get(appPath.toString(), "logs").toString(); Log.setup(logPath); log.debug("Log files under: " + logPath); Version.printVersion(); Utilities.printSysInfo(); Log.setLevel(logLevel); /* SeedNodeRepository seedNodesRepository = new SeedNodeRepository(); if (progArgSeedNodes != null && !progArgSeedNodes.isEmpty()) { if (useLocalhostForP2P) seedNodesRepository.setLocalhostSeedNodeAddresses(progArgSeedNodes); else seedNodesRepository.setTorSeedNodeAddresses(progArgSeedNodes); }*/ File storageDir = Paths.get(appPath.toString(), "db").toFile(); if (storageDir.mkdirs()) log.debug("Created storageDir at " + storageDir.getAbsolutePath()); File torDir = Paths.get(appPath.toString(), "tor").toFile(); if (torDir.mkdirs()) log.debug("Created torDir at " + torDir.getAbsolutePath()); // seedNodesRepository.setNodeAddressToExclude(mySeedNodeAddress); /* seedNodeP2PService = new P2PService(seedNodesRepository, mySeedNodeAddress.getPort(), maxConnections, torDir, useLocalhostForP2P, networkId, storageDir, null, null, null, new ClockWatcher(), null, null, null, TestUtils.getNetworkProtoResolver(), TestUtils.getPersistenceProtoResolver()); seedNodeP2PService.start(listener);*/ } @VisibleForTesting public P2PService getSeedNodeP2PService() { return seedNodeP2PService; } private void shutDown() { shutDown(null); } public void shutDown(@Nullable Runnable shutDownCompleteHandler) { if (!stopped) { stopped = true; seedNodeP2PService.shutDown(() -> { if (shutDownCompleteHandler != null) UserThread.execute(shutDownCompleteHandler); }); } } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/MockNode.java ================================================ /* * 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 . */ package haveno.network.p2p; import haveno.common.ClockWatcher; import haveno.common.crypto.KeyRing; import haveno.common.file.CorruptedStorageFileHandler; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.persistable.PersistenceProtoResolver; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.ConnectionState; import haveno.network.p2p.network.ConnectionStatistics; import haveno.network.p2p.network.InboundConnection; import haveno.network.p2p.network.NetworkNode; import haveno.network.p2p.network.OutboundConnection; import haveno.network.p2p.network.PeerType; import haveno.network.p2p.network.Statistic; import haveno.network.p2p.peers.PeerManager; import haveno.network.p2p.peers.peerexchange.PeerList; import haveno.network.p2p.seed.SeedNodeRepository; import lombok.Getter; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.HashSet; import java.util.Set; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class MockNode { @Getter private NetworkNode networkNode; @Getter private PeerManager peerManager; @Getter private Set connections; @Getter private int maxConnections; @Getter private PersistenceManager persistenceManager; public MockNode(int maxConnections) throws IOException { this.maxConnections = maxConnections; networkNode = mock(NetworkNode.class); File storageDir = Files.createTempDirectory("storage").toFile(); persistenceManager = new PersistenceManager<>(storageDir, mock(PersistenceProtoResolver.class), mock(CorruptedStorageFileHandler.class), mock(KeyRing.class)); peerManager = new PeerManager(networkNode, mock(SeedNodeRepository.class), new ClockWatcher(), persistenceManager, maxConnections); connections = new HashSet<>(); when(networkNode.getAllConnections()).thenReturn(connections); } public void addInboundConnection(PeerType peerType) { InboundConnection inboundConnection = mock(InboundConnection.class); ConnectionStatistics connectionStatistics = mock(ConnectionStatistics.class); when(connectionStatistics.getConnectionCreationTimeStamp()).thenReturn(0L); when(inboundConnection.getConnectionStatistics()).thenReturn(connectionStatistics); ConnectionState connectionState = mock(ConnectionState.class); when(connectionState.getPeerType()).thenReturn(peerType); when(inboundConnection.getConnectionState()).thenReturn(connectionState); Statistic statistic = mock(Statistic.class); long lastActivityTimestamp = System.currentTimeMillis(); when(statistic.getLastActivityTimestamp()).thenReturn(lastActivityTimestamp); when(inboundConnection.getStatistic()).thenReturn(statistic); doNothing().when(inboundConnection).run(); connections.add(inboundConnection); } public void addOutboundConnection(PeerType peerType) { OutboundConnection outboundConnection = mock(OutboundConnection.class); ConnectionStatistics connectionStatistics = mock(ConnectionStatistics.class); when(connectionStatistics.getConnectionCreationTimeStamp()).thenReturn(0L); when(outboundConnection.getConnectionStatistics()).thenReturn(connectionStatistics); ConnectionState connectionState = mock(ConnectionState.class); when(connectionState.getPeerType()).thenReturn(peerType); when(outboundConnection.getConnectionState()).thenReturn(connectionState); Statistic statistic = mock(Statistic.class); long lastActivityTimestamp = System.currentTimeMillis(); when(statistic.getLastActivityTimestamp()).thenReturn(lastActivityTimestamp); when(outboundConnection.getStatistic()).thenReturn(statistic); doNothing().when(outboundConnection).run(); connections.add(outboundConnection); } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/PeerServiceTest.java ================================================ /* * 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 . */ package haveno.network.p2p; import haveno.network.p2p.network.LocalhostNetworkNode; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.CountDownLatch; // TorNode created. Took 6 sec. // Hidden service created. Took 40-50 sec. // Connection establishment takes about 4 sec. // Please Note: You need to edit seed node addresses first before using tor version. // Run it once then lookup for onion address at: tor/hiddenservice/hostname and use that for the NodeAddress param. // TODO deactivated because outdated @SuppressWarnings({"UnusedAssignment", "EmptyMethod"}) @Disabled public class PeerServiceTest { private static final Logger log = LoggerFactory.getLogger(PeerServiceTest.class); private static final int MAX_CONNECTIONS = 100; final boolean useLocalhostForP2P = true; private CountDownLatch latch; private int sleepTime; private DummySeedNode seedNode1, seedNode2, seedNode3; private final Set seedNodeAddresses = new HashSet<>(); private final List seedNodes = new ArrayList<>(); private final String test_dummy_dir = "test_dummy_dir"; @BeforeEach public void setup() throws InterruptedException { LocalhostNetworkNode.setSimulateTorDelayTorNode(50); LocalhostNetworkNode.setSimulateTorDelayHiddenService(8); //noinspection ConstantConditions if (useLocalhostForP2P) { seedNodeAddresses.add(new NodeAddress("localhost:8001")); seedNodeAddresses.add(new NodeAddress("localhost:8002")); seedNodeAddresses.add(new NodeAddress("localhost:8003")); sleepTime = 100; } else { seedNodeAddresses.add(new NodeAddress("3omjuxn7z73pxoee.onion:8001")); seedNodeAddresses.add(new NodeAddress("j24fxqyghjetgpdx.onion:8002")); seedNodeAddresses.add(new NodeAddress("45367tl6unwec6kw.onion:8003")); sleepTime = 1000; } } @AfterEach public void tearDown() throws InterruptedException { Thread.sleep(sleepTime); seedNodes.stream().forEach(seedNode -> { CountDownLatch shutDownLatch = new CountDownLatch(1); seedNode.getSeedNodeP2PService().shutDown(() -> { }); seedNode.shutDown(shutDownLatch::countDown); try { shutDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } }); seedNodeAddresses.clear(); } @Test public void testSingleSeedNode() throws InterruptedException { LocalhostNetworkNode.setSimulateTorDelayTorNode(0); LocalhostNetworkNode.setSimulateTorDelayHiddenService(0); seedNodeAddresses.clear(); for (int i = 0; i < 10; i++) { int port = 8000 + i; NodeAddress nodeAddress = new NodeAddress("localhost:" + port); seedNodeAddresses.add(nodeAddress); DummySeedNode seedNode = new DummySeedNode(test_dummy_dir); seedNodes.add(seedNode); seedNode.createAndStartP2PService(true); seedNode.getSeedNodeP2PService().start(new P2PServiceListener() { @Override public void onDataReceived() { } @Override public void onNoSeedNodeAvailable() { } @Override public void onNoPeersAvailable() { } @Override public void onUpdatedDataReceived() { } @Override public void onTorNodeReady() { } @Override public void onHiddenServicePublished() { } @Override public void onSetupFailed(Throwable throwable) { } @Override public void onRequestCustomBridges() { } }); } Thread.sleep(30_000); /* latch = new CountDownLatch(2); seedNode.createAndStartP2PService(nodeAddress, MAX_CONNECTIONS, useLocalhostForP2P, 2, true, seedNodeAddresses, new P2PServiceListener() { @Override public void onRequestingDataCompleted() { latch.countDown(); } @Override public void onTorNodeReady() { } @Override public void onNoSeedNodeAvailable() { } @Override public void onNoPeersAvailable() { } @Override public void onBootstrapComplete() { } @Override public void onHiddenServicePublished() { latch.countDown(); } @Override public void onSetupFailed(Throwable throwable) { } }); P2PService p2PService = seedNode.getSeedNodeP2PService(); latch.await(); Thread.sleep(500);*/ //Assert.assertEquals(0, p2PService1.getPeerManager().getAuthenticatedAndReportedPeers().size()); } //@Test public void test2SeedNodes() throws InterruptedException { LocalhostNetworkNode.setSimulateTorDelayTorNode(0); LocalhostNetworkNode.setSimulateTorDelayHiddenService(0); seedNodeAddresses.clear(); NodeAddress nodeAddress1 = new NodeAddress("localhost:8001"); seedNodeAddresses.add(nodeAddress1); NodeAddress nodeAddress2 = new NodeAddress("localhost:8002"); seedNodeAddresses.add(nodeAddress2); latch = new CountDownLatch(6); seedNode1 = new DummySeedNode("test_dummy_dir"); seedNode1.createAndStartP2PService(nodeAddress1, MAX_CONNECTIONS, useLocalhostForP2P, 2, true, seedNodeAddresses, new P2PServiceListener() { @Override public void onDataReceived() { latch.countDown(); } @Override public void onNoSeedNodeAvailable() { } @Override public void onTorNodeReady() { } @Override public void onNoPeersAvailable() { } @Override public void onUpdatedDataReceived() { latch.countDown(); } @Override public void onHiddenServicePublished() { latch.countDown(); } @Override public void onSetupFailed(Throwable throwable) { } @Override public void onRequestCustomBridges() { } }); P2PService p2PService1 = seedNode1.getSeedNodeP2PService(); Thread.sleep(500); seedNode2 = new DummySeedNode("test_dummy_dir"); seedNode2.createAndStartP2PService(nodeAddress2, MAX_CONNECTIONS, useLocalhostForP2P, 2, true, seedNodeAddresses, new P2PServiceListener() { @Override public void onDataReceived() { latch.countDown(); } @Override public void onNoSeedNodeAvailable() { } @Override public void onTorNodeReady() { } @Override public void onNoPeersAvailable() { } @Override public void onUpdatedDataReceived() { latch.countDown(); } @Override public void onHiddenServicePublished() { latch.countDown(); } @Override public void onSetupFailed(Throwable throwable) { } @Override public void onRequestCustomBridges() { } }); P2PService p2PService2 = seedNode2.getSeedNodeP2PService(); latch.await(); // Assert.assertEquals(1, p2PService1.getPeerManager().getAuthenticatedAndReportedPeers().size()); // Assert.assertEquals(1, p2PService2.getPeerManager().getAuthenticatedAndReportedPeers().size()); } // @Test public void testAuthentication() throws InterruptedException { log.debug("### start"); LocalhostNetworkNode.setSimulateTorDelayTorNode(0); LocalhostNetworkNode.setSimulateTorDelayHiddenService(0); DummySeedNode seedNode1 = getAndStartSeedNode(8001); log.debug("### seedNode1"); Thread.sleep(100); log.debug("### seedNode1 100"); Thread.sleep(1000); DummySeedNode seedNode2 = getAndStartSeedNode(8002); // authentication: // node2 -> node1 RequestAuthenticationMessage // node1: close connection // node1 -> node2 ChallengeMessage on new connection // node2: authentication to node1 done if nonce ok // node2 -> node1 GetPeersMessage // node1: authentication to node2 done if nonce ok // node1 -> node2 PeersMessage // first authentication from seedNode2 to seedNode1, then from seedNode1 to seedNode2 //TODO /* CountDownLatch latch1 = new CountDownLatch(2); AuthenticationListener routingListener1 = new AuthenticationListener() { @Override public void onConnectionAuthenticated(Connection connection) { log.debug("onConnectionAuthenticated " + connection); latch1.countDown(); } }; seedNode1.getP2PService().getPeerGroup().addPeerListener(routingListener1); AuthenticationListener routingListener2 = new AuthenticationListener() { @Override public void onConnectionAuthenticated(Connection connection) { log.debug("onConnectionAuthenticated " + connection); latch1.countDown(); } }; seedNode2.getP2PService().getPeerGroup().addPeerListener(routingListener2); latch1.await(); seedNode1.getP2PService().getPeerGroup().removePeerListener(routingListener1); seedNode2.getP2PService().getPeerGroup().removePeerListener(routingListener2); // wait until Peers msg finished Thread.sleep(sleepTime); // authentication: // authentication from seedNode3 to seedNode1, then from seedNode1 to seedNode3 // authentication from seedNode3 to seedNode2, then from seedNode2 to seedNode3 SeedNode seedNode3 = getAndStartSeedNode(8003); CountDownLatch latch2 = new CountDownLatch(3); seedNode1.getP2PService().getPeerGroup().addPeerListener(new AuthenticationListener() { @Override public void onConnectionAuthenticated(Connection connection) { log.debug("onConnectionAuthenticated " + connection); latch2.countDown(); } }); seedNode2.getP2PService().getPeerGroup().addPeerListener(new AuthenticationListener() { @Override public void onConnectionAuthenticated(Connection connection) { log.debug("onConnectionAuthenticated " + connection); latch2.countDown(); } }); seedNode3.getP2PService().getPeerGroup().addPeerListener(new AuthenticationListener() { @Override public void onConnectionAuthenticated(Connection connection) { log.debug("onConnectionAuthenticated " + connection); latch2.countDown(); } }); latch2.await(); // wait until Peers msg finished Thread.sleep(sleepTime); CountDownLatch shutDownLatch = new CountDownLatch(3); seedNode1.shutDown(() -> shutDownLatch.countDown()); seedNode2.shutDown(() -> shutDownLatch.countDown()); seedNode3.shutDown(() -> shutDownLatch.countDown()); shutDownLatch.await();*/ } //@Test public void testAuthenticationWithDisconnect() throws InterruptedException { //TODO /* LocalhostNetworkNode.setSimulateTorDelayTorNode(0); LocalhostNetworkNode.setSimulateTorDelayHiddenService(0); SeedNode seedNode1 = getAndStartSeedNode(8001); SeedNode seedNode2 = getAndStartSeedNode(8002); // authentication: // node2 -> node1 RequestAuthenticationMessage // node1: close connection // node1 -> node2 ChallengeMessage on new connection // node2: authentication to node1 done if nonce ok // node2 -> node1 GetPeersMessage // node1: authentication to node2 done if nonce ok // node1 -> node2 PeersMessage // first authentication from seedNode2 to seedNode1, then from seedNode1 to seedNode2 CountDownLatch latch1 = new CountDownLatch(2); AuthenticationListener routingListener1 = new AuthenticationListener() { @Override public void onConnectionAuthenticated(Connection connection) { log.debug("onConnectionAuthenticated " + connection); latch1.countDown(); } }; seedNode1.getP2PService().getPeerGroup().addPeerListener(routingListener1); AuthenticationListener routingListener2 = new AuthenticationListener() { @Override public void onConnectionAuthenticated(Connection connection) { log.debug("onConnectionAuthenticated " + connection); latch1.countDown(); } }; seedNode2.getP2PService().getPeerGroup().addPeerListener(routingListener2); latch1.await(); // shut down node 2 Thread.sleep(sleepTime); seedNode1.getP2PService().getPeerGroup().removePeerListener(routingListener1); seedNode2.getP2PService().getPeerGroup().removePeerListener(routingListener2); CountDownLatch shutDownLatch1 = new CountDownLatch(1); seedNode2.shutDown(() -> shutDownLatch1.countDown()); shutDownLatch1.await(); // restart node 2 seedNode2 = getAndStartSeedNode(8002); CountDownLatch latch3 = new CountDownLatch(1); routingListener2 = new AuthenticationListener() { @Override public void onConnectionAuthenticated(Connection connection) { log.debug("onConnectionAuthenticated " + connection); latch3.countDown(); } }; seedNode2.getP2PService().getPeerGroup().addPeerListener(routingListener2); latch3.await(); Thread.sleep(sleepTime); CountDownLatch shutDownLatch = new CountDownLatch(2); seedNode1.shutDown(() -> shutDownLatch.countDown()); seedNode2.shutDown(() -> shutDownLatch.countDown()); shutDownLatch.await();*/ } //@Test public void testAuthenticationWithManyNodes() throws InterruptedException { //TODO /* int authentications = 0; int length = 3; SeedNode[] nodes = new SeedNode[length]; for (int i = 0; i < length; i++) { SeedNode node = getAndStartSeedNode(8001 + i); nodes[i] = node; latch = new CountDownLatch(i * 2); authentications += (i * 2); node.getP2PService().getPeerGroup().addPeerListener(new AuthenticationListener() { @Override public void onConnectionAuthenticated(Connection connection) { log.debug("onConnectionAuthenticated " + connection); latch.countDown(); } }); latch.await(); Thread.sleep(sleepTime); } log.debug("total authentications " + authentications); Profiler.printSystemLoad(log); // total authentications at 8 nodes = 56 // total authentications at com nodes = 90, System load (no. threads/used memory (MB)): 170/20 // total authentications at 20 nodes = 380, System load (no. threads/used memory (MB)): 525/46 for (int i = 0; i < length; i++) { nodes[i].getP2PService().getPeerGroup().printAuthenticatedPeers(); nodes[i].getP2PService().getPeerGroup().printReportedPeers(); } CountDownLatch shutDownLatch = new CountDownLatch(length); for (int i = 0; i < length; i++) { nodes[i].shutDown(() -> shutDownLatch.countDown()); } shutDownLatch.await();*/ } private DummySeedNode getAndStartSeedNode(int port) throws InterruptedException { DummySeedNode seedNode = new DummySeedNode("test_dummy_dir"); latch = new CountDownLatch(1); seedNode.createAndStartP2PService(new NodeAddress("localhost", port), MAX_CONNECTIONS, useLocalhostForP2P, 2, true, seedNodeAddresses, new P2PServiceListener() { @Override public void onDataReceived() { latch.countDown(); } @Override public void onNoSeedNodeAvailable() { } @Override public void onTorNodeReady() { } @Override public void onNoPeersAvailable() { } @Override public void onUpdatedDataReceived() { } @Override public void onHiddenServicePublished() { } @Override public void onSetupFailed(Throwable throwable) { } @Override public void onRequestCustomBridges() { } }); latch.await(); Thread.sleep(sleepTime); return seedNode; } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/TestUtils.java ================================================ /* * 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 . */ package haveno.network.p2p; import haveno.common.Payload; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.proto.network.NetworkPayload; import haveno.common.proto.network.NetworkProtoResolver; import haveno.common.proto.persistable.PersistablePayload; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.time.Clock; import java.util.Set; import java.util.concurrent.CountDownLatch; @SuppressWarnings("ALL") public class TestUtils { private static final Logger log = LoggerFactory.getLogger(TestUtils.class); public static int sleepTime; public static final String test_dummy_dir = "test_dummy_dir"; public static KeyPair generateKeyPair() throws NoSuchAlgorithmException { long ts = System.currentTimeMillis(); final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA"); keyPairGenerator.initialize(2048); KeyPair keyPair = keyPairGenerator.genKeyPair(); log.trace("Generate storageSignatureKeyPair needed {} ms", System.currentTimeMillis() - ts); return keyPair; } public static DummySeedNode getAndStartSeedNode(int port, boolean useLocalhostForP2P, Set seedNodes) throws InterruptedException { DummySeedNode seedNode; if (useLocalhostForP2P) { seedNodes.add(new NodeAddress("localhost:8001")); seedNodes.add(new NodeAddress("localhost:8002")); seedNodes.add(new NodeAddress("localhost:8003")); sleepTime = 100; seedNode = new DummySeedNode(test_dummy_dir); } else { seedNodes.add(new NodeAddress("3omjuxn7z73pxoee.onion:8001")); seedNodes.add(new NodeAddress("j24fxqyghjetgpdx.onion:8002")); seedNodes.add(new NodeAddress("45367tl6unwec6kw.onion:8003")); sleepTime = 10000; seedNode = new DummySeedNode(test_dummy_dir); } CountDownLatch latch = new CountDownLatch(1); seedNode.createAndStartP2PService(new NodeAddress("localhost", port), DummySeedNode.MAX_CONNECTIONS_DEFAULT, useLocalhostForP2P, 2, true, seedNodes, new P2PServiceListener() { @Override public void onDataReceived() { } @Override public void onNoSeedNodeAvailable() { } @Override public void onNoPeersAvailable() { } @Override public void onUpdatedDataReceived() { } @Override public void onTorNodeReady() { } @Override public void onHiddenServicePublished() { latch.countDown(); } @Override public void onSetupFailed(Throwable throwable) { } @Override public void onRequestCustomBridges() { } }); latch.await(); Thread.sleep(sleepTime); return seedNode; } /* public static P2PService getAndAuthenticateP2PService(int port, EncryptionService encryptionService, KeyRing keyRing, boolean useLocalhostForP2P, Set seedNodes) throws InterruptedException { CountDownLatch latch = new CountDownLatch(1); SeedNodeRepository seedNodesRepository = new SeedNodeRepository(); if (seedNodes != null && !seedNodes.isEmpty()) { if (useLocalhostForP2P) seedNodesRepository.setLocalhostSeedNodeAddresses(seedNodes); else seedNodesRepository.setTorSeedNodeAddresses(seedNodes); } P2PService p2PService = new P2PService(seedNodesRepository, port, new File("seed_node_" + port), useLocalhostForP2P, 2, P2PService.MAX_CONNECTIONS_DEFAULT, new File("dummy"), null, null, null, new ClockWatcher(), null, encryptionService, keyRing, getNetworkProtoResolver(), getPersistenceProtoResolver()); p2PService.start(new P2PServiceListener() { @Override public void onRequestingDataCompleted() { } @Override public void onNoSeedNodeAvailable() { } @Override public void onNoPeersAvailable() { } @Override public void onTorNodeReady() { } @Override public void onBootstrapComplete() { latch.countDown(); } @Override public void onHiddenServicePublished() { } @Override public void onSetupFailed(Throwable throwable) { } }); latch.await(); Thread.sleep(2000); return p2PService; } */ public static NetworkProtoResolver getNetworkProtoResolver() { return new NetworkProtoResolver() { @Override public Payload fromProto(protobuf.PaymentAccountPayload proto) { return null; } @Override public PersistablePayload fromProto(protobuf.PersistableNetworkPayload persistable) { return null; } @Override public NetworkEnvelope fromProto(protobuf.NetworkEnvelope envelope) { return null; } @Override public NetworkPayload fromProto(protobuf.StoragePayload proto) { return null; } @Override public NetworkPayload fromProto(protobuf.StorageEntryWrapper proto) { return null; } @Override public Clock getClock() { return null; } }; } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/mocks/MockMailboxPayload.java ================================================ /* * 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 . */ package haveno.network.p2p.mocks; import haveno.common.app.Version; import haveno.common.proto.network.NetworkEnvelope; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.mailbox.MailboxMessage; import haveno.network.p2p.storage.payload.ExpirablePayload; import lombok.Getter; import org.apache.commons.lang3.NotImplementedException; import java.util.UUID; @Getter public final class MockMailboxPayload extends NetworkEnvelope implements MailboxMessage, ExpirablePayload { private final String messageVersion = Version.getP2PMessageVersion(); public final String msg; public final NodeAddress senderNodeAddress; public long ttl = 0; private final String uid; public MockMailboxPayload(String msg, NodeAddress senderNodeAddress) { super("0"); this.msg = msg; this.senderNodeAddress = senderNodeAddress; uid = UUID.randomUUID().toString(); } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { throw new NotImplementedException("toProtoNetworkEnvelope not impl."); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof MockMailboxPayload)) return false; MockMailboxPayload that = (MockMailboxPayload) o; return !(msg != null ? !msg.equals(that.msg) : that.msg != null); } @Override public int hashCode() { return msg != null ? msg.hashCode() : 0; } @Override public String toString() { return "MockData{" + "msg='" + msg + '\'' + '}'; } @Override public long getTTL() { return ttl; } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/mocks/MockPayload.java ================================================ /* * 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 . */ package haveno.network.p2p.mocks; import haveno.common.app.Version; import haveno.common.proto.network.NetworkEnvelope; import haveno.network.p2p.storage.payload.ExpirablePayload; import org.apache.commons.lang3.NotImplementedException; @SuppressWarnings("ALL") public final class MockPayload extends NetworkEnvelope implements ExpirablePayload { public final String msg; public long ttl; private final String messageVersion = Version.getP2PMessageVersion(); public MockPayload(String msg) { super("0"); this.msg = msg; } @Override public String getMessageVersion() { return messageVersion; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { throw new NotImplementedException("toProtoNetworkEnvelope not impl."); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof MockPayload)) return false; MockPayload that = (MockPayload) o; return !(msg != null ? !msg.equals(that.msg) : that.msg != null); } @Override public int hashCode() { return msg != null ? msg.hashCode() : 0; } @Override public String toString() { return "MockData{" + "msg='" + msg + '\'' + '}'; } @Override public long getTTL() { return ttl; } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/network/LocalhostNetworkNodeTest.java ================================================ /* * 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 . */ package haveno.network.p2p.network; import haveno.network.p2p.TestUtils; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.concurrent.CountDownLatch; // TorNode created. Took 6 sec. // Hidden service created. Took 40-50 sec. // Connection establishment takes about 4 sec. //TODO P2P network tests are outdated @Disabled public class LocalhostNetworkNodeTest { private static final Logger log = LoggerFactory.getLogger(LocalhostNetworkNodeTest.class); @Test public void testMessage() throws InterruptedException, IOException { CountDownLatch msgLatch = new CountDownLatch(2); LocalhostNetworkNode node1 = new LocalhostNetworkNode(9001, TestUtils.getNetworkProtoResolver(), null, 12); node1.addMessageListener((message, connection) -> { log.debug("onMessage node1 " + message); msgLatch.countDown(); }); CountDownLatch startupLatch = new CountDownLatch(2); node1.start(new SetupListener() { @Override public void onTorNodeReady() { log.debug("onTorNodeReady"); } @Override public void onHiddenServicePublished() { log.debug("onHiddenServiceReady"); startupLatch.countDown(); } @Override public void onSetupFailed(Throwable throwable) { log.debug("onSetupFailed"); } @Override public void onRequestCustomBridges() { } }); LocalhostNetworkNode node2 = new LocalhostNetworkNode(9002, TestUtils.getNetworkProtoResolver(), null, 12); node2.addMessageListener((message, connection) -> { log.debug("onMessage node2 " + message); msgLatch.countDown(); }); node2.start(new SetupListener() { @Override public void onTorNodeReady() { log.debug("onTorNodeReady 2"); } @Override public void onHiddenServicePublished() { log.debug("onHiddenServiceReady 2"); startupLatch.countDown(); } @Override public void onSetupFailed(Throwable throwable) { log.debug("onSetupFailed 2"); } @Override public void onRequestCustomBridges() { } }); startupLatch.await(); msgLatch.await(); CountDownLatch shutDownLatch = new CountDownLatch(2); node1.shutDown(shutDownLatch::countDown); node2.shutDown(shutDownLatch::countDown); shutDownLatch.await(); } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/network/TorNetworkNodeTest.java ================================================ /* * 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 . */ package haveno.network.p2p.network; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import haveno.network.p2p.TestUtils; import haveno.network.p2p.mocks.MockPayload; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; // TorNode created. Took 6 sec. // Hidden service created. Took 40-50 sec. // Connection establishment takes about 4 sec. //TODO P2P network tests are outdated @SuppressWarnings("ConstantConditions") @Disabled public class TorNetworkNodeTest { private static final Logger log = LoggerFactory.getLogger(TorNetworkNodeTest.class); private CountDownLatch latch; @Test public void testTorNodeBeforeSecondReady() throws InterruptedException, IOException { latch = new CountDownLatch(1); int port = 9001; TorNetworkNode node1 = new TorNetworkNodeNetlayer(port, TestUtils.getNetworkProtoResolver(), new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, false, "127.0.0.1"); node1.start(new SetupListener() { @Override public void onTorNodeReady() { log.debug("onReadyForSendingMessages"); } @Override public void onHiddenServicePublished() { log.debug("onReadyForReceivingMessages"); latch.countDown(); } @Override public void onSetupFailed(Throwable throwable) { } @Override public void onRequestCustomBridges() { } }); latch.await(); latch = new CountDownLatch(1); int port2 = 9002; TorNetworkNode node2 = new TorNetworkNodeNetlayer(port2, TestUtils.getNetworkProtoResolver(), new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, false, "127.0.0.1"); node2.start(new SetupListener() { @Override public void onTorNodeReady() { log.debug("onReadyForSendingMessages"); latch.countDown(); } @Override public void onHiddenServicePublished() { log.debug("onReadyForReceivingMessages"); } @Override public void onSetupFailed(Throwable throwable) { } @Override public void onRequestCustomBridges() { } }); latch.await(); latch = new CountDownLatch(2); node1.addMessageListener((message, connection) -> { log.debug("onMessage node1 " + message); latch.countDown(); }); SettableFuture future = node2.sendMessage(node1.getNodeAddress(), new MockPayload("msg1")); Futures.addCallback(future, new FutureCallback() { @Override public void onSuccess(Connection connection) { log.debug("onSuccess "); latch.countDown(); } @Override public void onFailure(@NotNull Throwable throwable) { log.debug("onFailure "); } }, MoreExecutors.directExecutor()); latch.await(); latch = new CountDownLatch(2); node1.shutDown(latch::countDown); node2.shutDown(latch::countDown); latch.await(); } //@Test public void testTorNodeAfterBothReady() throws InterruptedException, IOException { latch = new CountDownLatch(2); int port = 9001; TorNetworkNode node1 = new TorNetworkNodeNetlayer(port, TestUtils.getNetworkProtoResolver(), new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, false, "127.0.0.1"); node1.start(new SetupListener() { @Override public void onTorNodeReady() { log.debug("onReadyForSendingMessages"); } @Override public void onHiddenServicePublished() { log.debug("onReadyForReceivingMessages"); latch.countDown(); } @Override public void onSetupFailed(Throwable throwable) { } @Override public void onRequestCustomBridges() { } }); int port2 = 9002; TorNetworkNode node2 = new TorNetworkNodeNetlayer(port2, TestUtils.getNetworkProtoResolver(), new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, false, "127.0.0.1"); node2.start(new SetupListener() { @Override public void onTorNodeReady() { log.debug("onReadyForSendingMessages"); } @Override public void onHiddenServicePublished() { log.debug("onReadyForReceivingMessages"); latch.countDown(); } @Override public void onSetupFailed(Throwable throwable) { } @Override public void onRequestCustomBridges() { } }); latch.await(); latch = new CountDownLatch(2); node2.addMessageListener((message, connection) -> { log.debug("onMessage node2 " + message); latch.countDown(); }); SettableFuture future = node1.sendMessage(node2.getNodeAddress(), new MockPayload("msg1")); Futures.addCallback(future, new FutureCallback() { @Override public void onSuccess(Connection connection) { log.debug("onSuccess "); latch.countDown(); } @Override public void onFailure(@NotNull Throwable throwable) { log.debug("onFailure "); } }, MoreExecutors.directExecutor()); latch.await(); latch = new CountDownLatch(2); node1.shutDown(latch::countDown); node2.shutDown(latch::countDown); latch.await(); } public List getBridgeAddresses() { return new ArrayList<>(); } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/peers/PeerManagerTest.java ================================================ /* * 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 . */ package haveno.network.p2p.peers; import haveno.network.p2p.MockNode; import haveno.network.p2p.network.CloseConnectionReason; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.InboundConnection; import haveno.network.p2p.network.PeerType; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.io.IOException; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class PeerManagerTest { private MockNode node; private int maxConnectionsPeer; private int maxConnectionsNonDirect; @BeforeEach public void setUp() throws IOException { node = new MockNode(2); maxConnectionsPeer = Math.max(4, (int) Math.round(node.getMaxConnections() * 1.3)); maxConnectionsNonDirect = Math.max(8, (int) Math.round(node.getMaxConnections() * 1.7)); } @AfterEach public void tearDown() { node.getPersistenceManager().shutdown(); } @Test public void testCheckMaxConnectionsNotExceeded() { for (int i = 0; i < 2; i++) { node.addInboundConnection(PeerType.PEER); } assertEquals(2, node.getNetworkNode().getAllConnections().size()); assertFalse(node.getPeerManager().checkMaxConnections()); node.getNetworkNode().getAllConnections().forEach(connection -> verify(connection, never()).shutDown(eq(CloseConnectionReason.TOO_MANY_CONNECTIONS_OPEN), isA(Runnable.class))); } @Test public void testCheckMaxConnectionsExceededWithInboundPeers() throws InterruptedException { for (int i = 0; i < 3; i++) { node.addInboundConnection(PeerType.PEER); } assertEquals(3, node.getNetworkNode().getAllConnections().size()); List inboundSortedPeerConnections = node.getNetworkNode().getAllConnections().stream() .filter(e -> e instanceof InboundConnection) .filter(e -> e.getConnectionState().getPeerType() == PeerType.PEER) .sorted(Comparator.comparingLong(o -> o.getStatistic().getLastActivityTimestamp())) .collect(Collectors.toList()); Connection oldestConnection = inboundSortedPeerConnections.remove(0); assertTrue(node.getPeerManager().checkMaxConnections()); // Need to wait because the shutDownCompleteHandler calls // checkMaxConnections on the user thread after a delay Thread.sleep(500); verify(oldestConnection, times(1)).shutDown( eq(CloseConnectionReason.TOO_MANY_CONNECTIONS_OPEN), isA(Runnable.class)); inboundSortedPeerConnections.forEach(connection -> verify(connection, never()).shutDown( eq(CloseConnectionReason.TOO_MANY_CONNECTIONS_OPEN), isA(Runnable.class))); } @Test public void testCheckMaxConnectionsPeerLimitNotExceeded() { for (int i = 0; i < maxConnectionsPeer; i++) { node.addOutboundConnection(PeerType.PEER); } assertEquals(maxConnectionsPeer, node.getNetworkNode().getAllConnections().size()); assertFalse(node.getPeerManager().checkMaxConnections()); node.getNetworkNode().getAllConnections().forEach(connection -> verify(connection, never()).shutDown(eq(CloseConnectionReason.TOO_MANY_CONNECTIONS_OPEN), isA(Runnable.class))); } @Test public void testCheckMaxConnectionsPeerLimitExceeded() throws InterruptedException { for (int i = 0; i < maxConnectionsPeer + 1; i++) { node.addOutboundConnection(PeerType.PEER); } assertEquals(maxConnectionsPeer + 1, node.getNetworkNode().getAllConnections().size()); List sortedPeerConnections = node.getNetworkNode().getAllConnections().stream() .filter(e -> e.getConnectionState().getPeerType() == PeerType.PEER) .sorted(Comparator.comparingLong(o -> o.getStatistic().getLastActivityTimestamp())) .collect(Collectors.toList()); Connection oldestConnection = sortedPeerConnections.remove(0); assertTrue(node.getPeerManager().checkMaxConnections()); // Need to wait because the shutDownCompleteHandler calls // checkMaxConnections on the user thread after a delay Thread.sleep(500); verify(oldestConnection, times(1)).shutDown( eq(CloseConnectionReason.TOO_MANY_CONNECTIONS_OPEN), isA(Runnable.class)); sortedPeerConnections.forEach(connection -> verify(connection, never()).shutDown( eq(CloseConnectionReason.TOO_MANY_CONNECTIONS_OPEN), isA(Runnable.class))); } @Test public void testCheckMaxConnectionsNonDirectLimitNotExceeded() { for (int i = 0; i < maxConnectionsNonDirect; i++) { node.addOutboundConnection(PeerType.INITIAL_DATA_EXCHANGE); } assertEquals(maxConnectionsNonDirect, node.getNetworkNode().getAllConnections().size()); assertFalse(node.getPeerManager().checkMaxConnections()); node.getNetworkNode().getAllConnections().forEach(connection -> verify(connection, never()).shutDown(eq(CloseConnectionReason.TOO_MANY_CONNECTIONS_OPEN), isA(Runnable.class))); } @Test @Disabled public void testCheckMaxConnectionsNonDirectLimitExceeded() throws InterruptedException { for (int i = 0; i < maxConnectionsNonDirect + 1; i++) { node.addOutboundConnection(PeerType.INITIAL_DATA_EXCHANGE); } assertEquals(maxConnectionsNonDirect + 1, node.getNetworkNode().getAllConnections().size()); List sortedPeerConnections = node.getNetworkNode().getAllConnections().stream() .filter(e -> e.getConnectionState().getPeerType() != PeerType.PEER) .filter(e -> e.getConnectionState().getPeerType() == PeerType.INITIAL_DATA_EXCHANGE) .sorted(Comparator.comparingLong(o -> o.getStatistic().getLastActivityTimestamp())) .collect(Collectors.toList()); Connection oldestConnection = sortedPeerConnections.remove(0); assertTrue(node.getPeerManager().checkMaxConnections()); // Need to wait because the shutDownCompleteHandler calls // checkMaxConnections on the user thread after a delay Thread.sleep(500); //TODO it reports "Wanted but not invoked:" but when debugging into it it is called. So seems to be some // mock setup issue verify(oldestConnection, times(1)).shutDown( eq(CloseConnectionReason.TOO_MANY_CONNECTIONS_OPEN), isA(Runnable.class)); sortedPeerConnections.forEach(connection -> verify(connection, never()).shutDown( eq(CloseConnectionReason.TOO_MANY_CONNECTIONS_OPEN), isA(Runnable.class))); } @Test public void testCheckMaxConnectionsExceededWithOutboundSeeds() { for (int i = 0; i < 3; i++) { node.addOutboundConnection(PeerType.INITIAL_DATA_EXCHANGE); } assertEquals(3, node.getNetworkNode().getAllConnections().size()); assertFalse(node.getPeerManager().checkMaxConnections()); node.getNetworkNode().getAllConnections().forEach(connection -> verify(connection, never()).shutDown(eq(CloseConnectionReason.TOO_MANY_CONNECTIONS_OPEN), isA(Runnable.class))); } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/storage/P2PDataStorageBuildGetDataResponseTest.java ================================================ /* * 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 . */ package haveno.network.p2p.storage; import com.google.protobuf.Message; import haveno.common.app.Capabilities; import haveno.common.app.Capability; import haveno.common.crypto.Sig; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.TestUtils; import haveno.network.p2p.network.NetworkNode; import haveno.network.p2p.peers.getdata.messages.GetDataRequest; import haveno.network.p2p.peers.getdata.messages.GetDataResponse; import haveno.network.p2p.peers.getdata.messages.GetUpdatedDataRequest; import haveno.network.p2p.peers.getdata.messages.PreliminaryGetDataRequest; import haveno.network.p2p.storage.mocks.PersistableNetworkPayloadStub; import haveno.network.p2p.storage.payload.CapabilityRequiringPayload; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import haveno.network.p2p.storage.payload.ProtectedStoragePayload; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; import java.security.KeyPair; import java.security.NoSuchAlgorithmException; import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.mockito.Mockito.withSettings; public class P2PDataStorageBuildGetDataResponseTest { @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) // there are unused stubs in TestState & elsewhere abstract static class P2PDataStorageBuildGetDataResponseTestBase { // GIVEN null & non-null supportedCapabilities private TestState testState; abstract GetDataRequest buildGetDataRequest(int nonce, Set knownKeys); @Mock NetworkNode networkNode; private NodeAddress localNodeAddress; @BeforeEach public void setUp() { this.testState = new TestState(); this.localNodeAddress = new NodeAddress("localhost", 8080); when(networkNode.getNodeAddress()).thenReturn(this.localNodeAddress); // Set up basic capabilities to ensure message contains it Capabilities.app.addAll(Capability.MEDIATION); } static class RequiredCapabilitiesPNPStub extends PersistableNetworkPayloadStub implements CapabilityRequiringPayload { Capabilities capabilities; RequiredCapabilitiesPNPStub(Capabilities capabilities, byte[] hash) { super(hash); this.capabilities = capabilities; } @Override public Capabilities getRequiredCapabilities() { return capabilities; } } /** * Generates a unique ProtectedStorageEntry that is valid for add. This is used to initialize P2PDataStorage state * so the tests can validate the correct behavior. Adds of identical payloads with different sequence numbers * is not supported. */ private ProtectedStorageEntry getProtectedStorageEntryForAdd() throws NoSuchAlgorithmException { return getProtectedStorageEntryForAdd(null); } private ProtectedStorageEntry getProtectedStorageEntryForAdd(Capabilities requiredCapabilities) throws NoSuchAlgorithmException { KeyPair ownerKeys = TestUtils.generateKeyPair(); // Payload stub ProtectedStoragePayload protectedStoragePayload; if (requiredCapabilities == null) protectedStoragePayload = mock(ProtectedStoragePayload.class); else { protectedStoragePayload = mock(ProtectedStoragePayload.class, withSettings().extraInterfaces(CapabilityRequiringPayload.class)); when(((CapabilityRequiringPayload) protectedStoragePayload).getRequiredCapabilities()) .thenReturn(requiredCapabilities); } Message messageMock = mock(Message.class); when(messageMock.toByteArray()).thenReturn(Sig.getPublicKeyBytes(ownerKeys.getPublic())); when(protectedStoragePayload.toProtoMessage()).thenReturn(messageMock); // Entry stub ProtectedStorageEntry stub = mock(ProtectedStorageEntry.class); when(stub.getOwnerPubKey()).thenReturn(ownerKeys.getPublic()); when(stub.isValidForAddOperation()).thenReturn(true); when(stub.matchesRelevantPubKey(any(ProtectedStorageEntry.class))).thenReturn(true); when(stub.getSequenceNumber()).thenReturn(1); when(stub.getProtectedStoragePayload()).thenReturn(protectedStoragePayload); return stub; } // TESTCASE: Given a GetDataRequest w/ unknown PNP, nothing is sent back @Test public void buildGetDataResponse_unknownPNPDoNothing() { PersistableNetworkPayload fromPeer = new PersistableNetworkPayloadStub(new byte[]{1}); GetDataRequest getDataRequest = this.buildGetDataRequest(1, new HashSet<>(Collections.singletonList(fromPeer.getHash()))); AtomicBoolean outPNPTruncated = new AtomicBoolean(false); AtomicBoolean outPSETruncated = new AtomicBoolean(false); Capabilities peerCapabilities = new Capabilities(); GetDataResponse getDataResponse = this.testState.mockedStorage.buildGetDataResponse( getDataRequest, 1, outPNPTruncated, outPSETruncated, peerCapabilities); assertFalse(outPNPTruncated.get()); assertFalse(outPSETruncated.get()); assertEquals(1, getDataResponse.getRequestNonce()); assertEquals(getDataRequest instanceof GetUpdatedDataRequest, getDataResponse.isGetUpdatedDataResponse()); assertEquals(getDataResponse.getSupportedCapabilities(), Capabilities.app); assertTrue(getDataResponse.getPersistableNetworkPayloadSet().isEmpty()); assertTrue(getDataResponse.getDataSet().isEmpty()); } // TESTCASE: Given a GetDataRequest w/ known PNP, nothing is sent back @Test public void buildGetDataResponse_knownPNPDoNothing() { PersistableNetworkPayload fromPeerAndLocal = new PersistableNetworkPayloadStub(new byte[]{1}); this.testState.mockedStorage.addPersistableNetworkPayload( fromPeerAndLocal, this.localNodeAddress, false); GetDataRequest getDataRequest = this.buildGetDataRequest( 1, new HashSet<>(Collections.singletonList(fromPeerAndLocal.getHash()))); AtomicBoolean outPNPTruncated = new AtomicBoolean(false); AtomicBoolean outPSETruncated = new AtomicBoolean(false); Capabilities peerCapabilities = new Capabilities(); GetDataResponse getDataResponse = this.testState.mockedStorage.buildGetDataResponse( getDataRequest, 1, outPNPTruncated, outPSETruncated, peerCapabilities); assertFalse(outPNPTruncated.get()); assertFalse(outPSETruncated.get()); assertEquals(1, getDataResponse.getRequestNonce()); assertEquals(getDataRequest instanceof GetUpdatedDataRequest, getDataResponse.isGetUpdatedDataResponse()); assertEquals(getDataResponse.getSupportedCapabilities(), Capabilities.app); assertTrue(getDataResponse.getPersistableNetworkPayloadSet().isEmpty()); assertTrue(getDataResponse.getDataSet().isEmpty()); } // TESTCASE: Given a GetDataRequest w/o known PNP, send it back @Test public void buildGetDataResponse_unknownPNPSendBack() { PersistableNetworkPayload onlyLocal = new PersistableNetworkPayloadStub(new byte[]{1}); this.testState.mockedStorage.addPersistableNetworkPayload( onlyLocal, this.localNodeAddress, false); GetDataRequest getDataRequest = this.buildGetDataRequest(1, new HashSet<>()); AtomicBoolean outPNPTruncated = new AtomicBoolean(false); AtomicBoolean outPSETruncated = new AtomicBoolean(false); Capabilities peerCapabilities = new Capabilities(); GetDataResponse getDataResponse = this.testState.mockedStorage.buildGetDataResponse( getDataRequest, 1, outPNPTruncated, outPSETruncated, peerCapabilities); assertFalse(outPNPTruncated.get()); assertFalse(outPSETruncated.get()); assertEquals(1, getDataResponse.getRequestNonce()); assertEquals(getDataRequest instanceof GetUpdatedDataRequest, getDataResponse.isGetUpdatedDataResponse()); assertEquals(getDataResponse.getSupportedCapabilities(), Capabilities.app); assertTrue(getDataResponse.getPersistableNetworkPayloadSet().contains(onlyLocal)); assertTrue(getDataResponse.getDataSet().isEmpty()); } // TESTCASE: Given a GetDataRequest w/o known PNP, don't send more than truncation limit @Test public void buildGetDataResponse_unknownPNPSendBackTruncation() { PersistableNetworkPayload onlyLocal1 = new PersistableNetworkPayloadStub(new byte[]{1}); PersistableNetworkPayload onlyLocal2 = new PersistableNetworkPayloadStub(new byte[]{2}); this.testState.mockedStorage.addPersistableNetworkPayload( onlyLocal1, this.localNodeAddress, false); this.testState.mockedStorage.addPersistableNetworkPayload( onlyLocal2, this.localNodeAddress, false); GetDataRequest getDataRequest = this.buildGetDataRequest(1, new HashSet<>()); AtomicBoolean outPNPTruncated = new AtomicBoolean(false); AtomicBoolean outPSETruncated = new AtomicBoolean(false); Capabilities peerCapabilities = new Capabilities(); GetDataResponse getDataResponse = this.testState.mockedStorage.buildGetDataResponse( getDataRequest, 1, outPNPTruncated, outPSETruncated, peerCapabilities); assertTrue(outPNPTruncated.get()); assertFalse(outPSETruncated.get()); assertEquals(1, getDataResponse.getRequestNonce()); assertEquals(getDataRequest instanceof GetUpdatedDataRequest, getDataResponse.isGetUpdatedDataResponse()); assertEquals(getDataResponse.getSupportedCapabilities(), Capabilities.app); assertEquals(1, getDataResponse.getPersistableNetworkPayloadSet().size()); Set persistableNetworkPayloadSet = getDataResponse.getPersistableNetworkPayloadSet(); // We use a set at the filter so it is not deterministic which item get truncated assertEquals(1, persistableNetworkPayloadSet.size()); assertTrue(getDataResponse.getDataSet().isEmpty()); } // TESTCASE: Given a GetDataRequest w/o known PNP, but missing required capabilities, nothing is sent back @Test public void buildGetDataResponse_unknownPNPCapabilitiesMismatchDontSendBack() { PersistableNetworkPayload onlyLocal = new RequiredCapabilitiesPNPStub(new Capabilities(Collections.singletonList(Capability.MEDIATION)), new byte[]{1}); this.testState.mockedStorage.addPersistableNetworkPayload( onlyLocal, this.localNodeAddress, false); GetDataRequest getDataRequest = this.buildGetDataRequest(1, new HashSet<>()); AtomicBoolean outPNPTruncated = new AtomicBoolean(false); AtomicBoolean outPSETruncated = new AtomicBoolean(false); Capabilities peerCapabilities = new Capabilities(); GetDataResponse getDataResponse = this.testState.mockedStorage.buildGetDataResponse( getDataRequest, 2, outPNPTruncated, outPSETruncated, peerCapabilities); assertFalse(outPNPTruncated.get()); assertFalse(outPSETruncated.get()); assertEquals(1, getDataResponse.getRequestNonce()); assertEquals(getDataRequest instanceof GetUpdatedDataRequest, getDataResponse.isGetUpdatedDataResponse()); assertEquals(getDataResponse.getSupportedCapabilities(), Capabilities.app); assertTrue(getDataResponse.getPersistableNetworkPayloadSet().isEmpty()); assertTrue(getDataResponse.getDataSet().isEmpty()); } // TESTCASE: Given a GetDataRequest w/o known PNP that requires capabilities (and they match) send it back @Test public void buildGetDataResponse_unknownPNPCapabilitiesMatch() { PersistableNetworkPayload onlyLocal = new RequiredCapabilitiesPNPStub(new Capabilities(Collections.singletonList(Capability.MEDIATION)), new byte[]{1}); this.testState.mockedStorage.addPersistableNetworkPayload( onlyLocal, this.localNodeAddress, false); GetDataRequest getDataRequest = this.buildGetDataRequest(1, new HashSet<>()); AtomicBoolean outPNPTruncated = new AtomicBoolean(false); AtomicBoolean outPSETruncated = new AtomicBoolean(false); Capabilities peerCapabilities = new Capabilities(Collections.singletonList(Capability.MEDIATION)); GetDataResponse getDataResponse = this.testState.mockedStorage.buildGetDataResponse( getDataRequest, 2, outPNPTruncated, outPSETruncated, peerCapabilities); assertFalse(outPNPTruncated.get()); assertFalse(outPSETruncated.get()); assertEquals(1, getDataResponse.getRequestNonce()); assertEquals(getDataRequest instanceof GetUpdatedDataRequest, getDataResponse.isGetUpdatedDataResponse()); assertEquals(getDataResponse.getSupportedCapabilities(), Capabilities.app); assertTrue(getDataResponse.getPersistableNetworkPayloadSet().contains(onlyLocal)); assertTrue(getDataResponse.getDataSet().isEmpty()); } // TESTCASE: Given a GetDataRequest w/ unknown PSE, nothing is sent back @Test public void buildGetDataResponse_unknownPSEDoNothing() throws NoSuchAlgorithmException { ProtectedStorageEntry fromPeer = getProtectedStorageEntryForAdd(); GetDataRequest getDataRequest = this.buildGetDataRequest(1, new HashSet<>(Collections.singletonList( P2PDataStorage.get32ByteHash(fromPeer.getProtectedStoragePayload())))); AtomicBoolean outPNPTruncated = new AtomicBoolean(false); AtomicBoolean outPSETruncated = new AtomicBoolean(false); Capabilities peerCapabilities = new Capabilities(); GetDataResponse getDataResponse = this.testState.mockedStorage.buildGetDataResponse( getDataRequest, 1, outPNPTruncated, outPSETruncated, peerCapabilities); assertFalse(outPNPTruncated.get()); assertFalse(outPSETruncated.get()); assertEquals(1, getDataResponse.getRequestNonce()); assertEquals(getDataRequest instanceof GetUpdatedDataRequest, getDataResponse.isGetUpdatedDataResponse()); assertEquals(getDataResponse.getSupportedCapabilities(), Capabilities.app); assertTrue(getDataResponse.getPersistableNetworkPayloadSet().isEmpty()); assertTrue(getDataResponse.getDataSet().isEmpty()); } // TESTCASE: Given a GetDataRequest w/ known PSE, nothing is sent back @Test public void buildGetDataResponse_knownPSEDoNothing() throws NoSuchAlgorithmException { ProtectedStorageEntry fromPeerAndLocal = getProtectedStorageEntryForAdd(); GetDataRequest getDataRequest = this.buildGetDataRequest(1, new HashSet<>(Collections.singletonList( P2PDataStorage.get32ByteHash(fromPeerAndLocal.getProtectedStoragePayload())))); this.testState.mockedStorage.addProtectedStorageEntry( fromPeerAndLocal, this.localNodeAddress, null); AtomicBoolean outPNPTruncated = new AtomicBoolean(false); AtomicBoolean outPSETruncated = new AtomicBoolean(false); Capabilities peerCapabilities = new Capabilities(); GetDataResponse getDataResponse = this.testState.mockedStorage.buildGetDataResponse( getDataRequest, 1, outPNPTruncated, outPSETruncated, peerCapabilities); assertFalse(outPNPTruncated.get()); assertFalse(outPSETruncated.get()); assertEquals(1, getDataResponse.getRequestNonce()); assertEquals(getDataRequest instanceof GetUpdatedDataRequest, getDataResponse.isGetUpdatedDataResponse()); assertEquals(getDataResponse.getSupportedCapabilities(), Capabilities.app); assertTrue(getDataResponse.getPersistableNetworkPayloadSet().isEmpty()); assertTrue(getDataResponse.getDataSet().isEmpty()); } // TESTCASE: Given a GetDataRequest w/o known PSE, send it back // @Test public void buildGetDataResponse_unknownPSESendBack() throws NoSuchAlgorithmException { ProtectedStorageEntry onlyLocal = getProtectedStorageEntryForAdd(); GetDataRequest getDataRequest = this.buildGetDataRequest(1, new HashSet<>()); this.testState.mockedStorage.addProtectedStorageEntry( onlyLocal, this.localNodeAddress, null); AtomicBoolean outPNPTruncated = new AtomicBoolean(false); AtomicBoolean outPSETruncated = new AtomicBoolean(false); Capabilities peerCapabilities = new Capabilities(); GetDataResponse getDataResponse = this.testState.mockedStorage.buildGetDataResponse( getDataRequest, 1, outPNPTruncated, outPSETruncated, peerCapabilities); assertFalse(outPNPTruncated.get()); assertFalse(outPSETruncated.get()); assertEquals(1, getDataResponse.getRequestNonce()); assertEquals(getDataRequest instanceof GetUpdatedDataRequest, getDataResponse.isGetUpdatedDataResponse()); assertEquals(getDataResponse.getSupportedCapabilities(), Capabilities.app); assertTrue(getDataResponse.getPersistableNetworkPayloadSet().isEmpty()); assertTrue(getDataResponse.getDataSet().contains(onlyLocal)); } // TESTCASE: Given a GetDataRequest w/o known PNP, don't send more than truncation limit // @Test public void buildGetDataResponse_unknownPSESendBackTruncation() throws NoSuchAlgorithmException { ProtectedStorageEntry onlyLocal1 = getProtectedStorageEntryForAdd(); ProtectedStorageEntry onlyLocal2 = getProtectedStorageEntryForAdd(); GetDataRequest getDataRequest = this.buildGetDataRequest(1, new HashSet<>()); this.testState.mockedStorage.addProtectedStorageEntry( onlyLocal1, this.localNodeAddress, null); this.testState.mockedStorage.addProtectedStorageEntry( onlyLocal2, this.localNodeAddress, null); AtomicBoolean outPNPTruncated = new AtomicBoolean(false); AtomicBoolean outPSETruncated = new AtomicBoolean(false); Capabilities peerCapabilities = new Capabilities(); GetDataResponse getDataResponse = this.testState.mockedStorage.buildGetDataResponse( getDataRequest, 1, outPNPTruncated, outPSETruncated, peerCapabilities); assertFalse(outPNPTruncated.get()); assertTrue(outPSETruncated.get()); assertEquals(1, getDataResponse.getRequestNonce()); assertEquals(getDataRequest instanceof GetUpdatedDataRequest, getDataResponse.isGetUpdatedDataResponse()); assertEquals(getDataResponse.getSupportedCapabilities(), Capabilities.app); assertTrue(getDataResponse.getPersistableNetworkPayloadSet().isEmpty()); assertEquals(1, getDataResponse.getDataSet().size()); assertTrue( getDataResponse.getDataSet().contains(onlyLocal1) || getDataResponse.getDataSet().contains(onlyLocal2)); } // TESTCASE: Given a GetDataRequest w/o known PNP, but missing required capabilities, nothing is sent back @Test public void buildGetDataResponse_unknownPSECapabilitiesMismatchDontSendBack() throws NoSuchAlgorithmException { ProtectedStorageEntry onlyLocal = getProtectedStorageEntryForAdd(new Capabilities(Collections.singletonList(Capability.MEDIATION))); this.testState.mockedStorage.addProtectedStorageEntry( onlyLocal, this.localNodeAddress, null); GetDataRequest getDataRequest = this.buildGetDataRequest(1, new HashSet<>()); AtomicBoolean outPNPTruncated = new AtomicBoolean(false); AtomicBoolean outPSETruncated = new AtomicBoolean(false); Capabilities peerCapabilities = new Capabilities(); GetDataResponse getDataResponse = this.testState.mockedStorage.buildGetDataResponse( getDataRequest, 2, outPNPTruncated, outPSETruncated, peerCapabilities); assertFalse(outPNPTruncated.get()); assertFalse(outPSETruncated.get()); assertEquals(1, getDataResponse.getRequestNonce()); assertEquals(getDataRequest instanceof GetUpdatedDataRequest, getDataResponse.isGetUpdatedDataResponse()); assertEquals(getDataResponse.getSupportedCapabilities(), Capabilities.app); assertTrue(getDataResponse.getPersistableNetworkPayloadSet().isEmpty()); assertTrue(getDataResponse.getDataSet().isEmpty()); } // TESTCASE: Given a GetDataRequest w/o known PNP that requires capabilities (and they match) send it back // @Test public void buildGetDataResponse_unknownPSECapabilitiesMatch() throws NoSuchAlgorithmException { ProtectedStorageEntry onlyLocal = getProtectedStorageEntryForAdd(new Capabilities(Collections.singletonList(Capability.MEDIATION))); this.testState.mockedStorage.addProtectedStorageEntry( onlyLocal, this.localNodeAddress, null); GetDataRequest getDataRequest = this.buildGetDataRequest(1, new HashSet<>()); AtomicBoolean outPNPTruncated = new AtomicBoolean(false); AtomicBoolean outPSETruncated = new AtomicBoolean(false); Capabilities peerCapabilities = new Capabilities(Collections.singletonList(Capability.MEDIATION)); GetDataResponse getDataResponse = this.testState.mockedStorage.buildGetDataResponse( getDataRequest, 2, outPNPTruncated, outPSETruncated, peerCapabilities); assertFalse(outPNPTruncated.get()); assertFalse(outPSETruncated.get()); assertEquals(1, getDataResponse.getRequestNonce()); assertEquals(getDataRequest instanceof GetUpdatedDataRequest, getDataResponse.isGetUpdatedDataResponse()); assertEquals(getDataResponse.getSupportedCapabilities(), Capabilities.app); assertTrue(getDataResponse.getPersistableNetworkPayloadSet().isEmpty()); assertTrue(getDataResponse.getDataSet().contains(onlyLocal)); } } public static class P2PDataStorageBuildGetDataResponseTestPreliminary extends P2PDataStorageBuildGetDataResponseTestBase { @Override GetDataRequest buildGetDataRequest(int nonce, Set knownKeys) { return new PreliminaryGetDataRequest(nonce, knownKeys); } } public static class P2PDataStorageBuildGetDataResponseTestUpdated extends P2PDataStorageBuildGetDataResponseTestBase { @Override GetDataRequest buildGetDataRequest(int nonce, Set knownKeys) { return new GetUpdatedDataRequest(new NodeAddress("peer", 10), nonce, knownKeys); } } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/storage/P2PDataStorageClientAPITest.java ================================================ /* * 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 . */ package haveno.network.p2p.storage; import haveno.common.app.Version; import haveno.common.crypto.CryptoException; import haveno.network.p2p.TestUtils; import haveno.network.p2p.network.Connection; import haveno.network.p2p.storage.messages.AddDataMessage; import haveno.network.p2p.storage.messages.RefreshOfferMessage; import haveno.network.p2p.storage.mocks.ExpirableProtectedStoragePayloadStub; import haveno.network.p2p.storage.payload.MailboxStoragePayload; import haveno.network.p2p.storage.payload.ProtectedMailboxStorageEntry; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import haveno.network.p2p.storage.payload.ProtectedStoragePayload; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.security.KeyPair; import java.security.NoSuchAlgorithmException; import java.util.Optional; import static haveno.network.p2p.storage.TestState.SavedTestState; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * Tests of the P2PDataStore Client API entry points. * * These tests validate the client code path that uses the pattern addProtectedStorageEntry(getProtectedStorageEntry()) * as opposed to the onMessage() handler or DataRequest paths. */ public class P2PDataStorageClientAPITest { private TestState testState; @BeforeEach public void setUp() { this.testState = new TestState(); // Deep in the bowels of protobuf we grab the messageID from the version module. This is required to hash the // full MailboxStoragePayload so make sure it is initialized. Version.setBaseCryptoNetworkId(1); } // TESTCASE: Adding an entry from the getProtectedStorageEntry API correctly adds the item @Test public void getProtectedStorageEntry_NoExist() throws NoSuchAlgorithmException, CryptoException { KeyPair ownerKeys = TestUtils.generateKeyPair(); ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayloadStub(ownerKeys.getPublic()); ProtectedStorageEntry protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); assertTrue(this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, TestState.getTestNodeAddress(), null)); this.testState.assertProtectedStorageAdd(beforeState, protectedStorageEntry, true, true, true, true); } // TESTCASE: Adding an entry from the getProtectedStorageEntry API of an existing item correctly updates the item @Test public void getProtectedStorageEntry() throws NoSuchAlgorithmException, CryptoException { KeyPair ownerKeys = TestUtils.generateKeyPair(); ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayloadStub(ownerKeys.getPublic()); ProtectedStorageEntry protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); assertTrue(this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, TestState.getTestNodeAddress(), null)); SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, TestState.getTestNodeAddress(), null); this.testState.assertProtectedStorageAdd(beforeState, protectedStorageEntry, true, true, true, true); } // TESTCASE: Adding an entry from the getProtectedStorageEntry API of an existing item (added from onMessage path) correctly updates the item @Test public void getProtectedStorageEntry_FirstOnMessageSecondAPI() throws NoSuchAlgorithmException, CryptoException { KeyPair ownerKeys = TestUtils.generateKeyPair(); ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayloadStub(ownerKeys.getPublic()); ProtectedStorageEntry protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); Connection mockedConnection = mock(Connection.class); when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(TestState.getTestNodeAddress())); this.testState.mockedStorage.onMessage(new AddDataMessage(protectedStorageEntry), mockedConnection); SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); assertTrue(this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, TestState.getTestNodeAddress(), null)); this.testState.assertProtectedStorageAdd(beforeState, protectedStorageEntry, true, true, true, true); } // TESTCASE: Updating an entry from the getRefreshTTLMessage API correctly errors if the item hasn't been seen @Test public void getRefreshTTLMessage_NoExists() throws NoSuchAlgorithmException, CryptoException { KeyPair ownerKeys = TestUtils.generateKeyPair(); ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayloadStub(ownerKeys.getPublic()); RefreshOfferMessage refreshOfferMessage = this.testState.mockedStorage.getRefreshTTLMessage(protectedStoragePayload, ownerKeys); SavedTestState beforeState = this.testState.saveTestState(refreshOfferMessage); assertFalse(this.testState.mockedStorage.refreshTTL(refreshOfferMessage, TestState.getTestNodeAddress())); this.testState.verifyRefreshTTL(beforeState, refreshOfferMessage, false); } // TESTCASE: Updating an entry from the getRefreshTTLMessage API correctly "refreshes" the item @Test public void getRefreshTTLMessage() throws NoSuchAlgorithmException, CryptoException { KeyPair ownerKeys = TestUtils.generateKeyPair(); ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayloadStub(ownerKeys.getPublic()); ProtectedStorageEntry protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, TestState.getTestNodeAddress(), null); RefreshOfferMessage refreshOfferMessage = this.testState.mockedStorage.getRefreshTTLMessage(protectedStoragePayload, ownerKeys); this.testState.mockedStorage.refreshTTL(refreshOfferMessage, TestState.getTestNodeAddress()); refreshOfferMessage = this.testState.mockedStorage.getRefreshTTLMessage(protectedStoragePayload, ownerKeys); this.testState.incrementClock(); SavedTestState beforeState = this.testState.saveTestState(refreshOfferMessage); assertTrue(this.testState.mockedStorage.refreshTTL(refreshOfferMessage, TestState.getTestNodeAddress())); this.testState.verifyRefreshTTL(beforeState, refreshOfferMessage, true); } // TESTCASE: Updating an entry from the getRefreshTTLMessage API correctly "refreshes" the item when it was originally added from onMessage path @Test public void getRefreshTTLMessage_FirstOnMessageSecondAPI() throws NoSuchAlgorithmException, CryptoException { KeyPair ownerKeys = TestUtils.generateKeyPair(); ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayloadStub(ownerKeys.getPublic()); ProtectedStorageEntry protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, TestState.getTestNodeAddress(), null); Connection mockedConnection = mock(Connection.class); when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(TestState.getTestNodeAddress())); this.testState.mockedStorage.onMessage(new AddDataMessage(protectedStorageEntry), mockedConnection); RefreshOfferMessage refreshOfferMessage = this.testState.mockedStorage.getRefreshTTLMessage(protectedStoragePayload, ownerKeys); this.testState.incrementClock(); SavedTestState beforeState = this.testState.saveTestState(refreshOfferMessage); assertTrue(this.testState.mockedStorage.refreshTTL(refreshOfferMessage, TestState.getTestNodeAddress())); this.testState.verifyRefreshTTL(beforeState, refreshOfferMessage, true); } // TESTCASE: Removing a non-existent mailbox entry from the getMailboxDataWithSignedSeqNr API @Test public void getMailboxDataWithSignedSeqNr_RemoveNoExist() throws NoSuchAlgorithmException, CryptoException { KeyPair receiverKeys = TestUtils.generateKeyPair(); KeyPair senderKeys = TestUtils.generateKeyPair(); MailboxStoragePayload mailboxStoragePayload = TestState.buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); ProtectedMailboxStorageEntry protectedMailboxStorageEntry = this.testState.mockedStorage.getMailboxDataWithSignedSeqNr(mailboxStoragePayload, receiverKeys, receiverKeys.getPublic()); SavedTestState beforeState = this.testState.saveTestState(protectedMailboxStorageEntry); assertTrue(this.testState.mockedStorage.remove(protectedMailboxStorageEntry, TestState.getTestNodeAddress())); this.testState.verifyProtectedStorageRemove(beforeState, protectedMailboxStorageEntry, false, false, true, true); } // TESTCASE: Adding, then removing a mailbox message from the getMailboxDataWithSignedSeqNr API @Test public void getMailboxDataWithSignedSeqNr_AddThenRemove() throws NoSuchAlgorithmException, CryptoException { KeyPair receiverKeys = TestUtils.generateKeyPair(); KeyPair senderKeys = TestUtils.generateKeyPair(); MailboxStoragePayload mailboxStoragePayload = TestState.buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); ProtectedMailboxStorageEntry protectedMailboxStorageEntry = this.testState.mockedStorage.getMailboxDataWithSignedSeqNr(mailboxStoragePayload, senderKeys, receiverKeys.getPublic()); assertTrue(this.testState.mockedStorage.addProtectedStorageEntry(protectedMailboxStorageEntry, TestState.getTestNodeAddress(), null)); protectedMailboxStorageEntry = this.testState.mockedStorage.getMailboxDataWithSignedSeqNr(mailboxStoragePayload, receiverKeys, receiverKeys.getPublic()); SavedTestState beforeState = this.testState.saveTestState(protectedMailboxStorageEntry); assertTrue(this.testState.mockedStorage.remove(protectedMailboxStorageEntry, TestState.getTestNodeAddress())); this.testState.verifyProtectedStorageRemove(beforeState, protectedMailboxStorageEntry, true, true, true, true); } // TESTCASE: Removing a mailbox message that was added from the onMessage handler @Test public void getMailboxDataWithSignedSeqNr_ValidRemoveAddFromMessage() throws NoSuchAlgorithmException, CryptoException { KeyPair receiverKeys = TestUtils.generateKeyPair(); KeyPair senderKeys = TestUtils.generateKeyPair(); MailboxStoragePayload mailboxStoragePayload = TestState.buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); ProtectedMailboxStorageEntry protectedMailboxStorageEntry = this.testState.mockedStorage.getMailboxDataWithSignedSeqNr(mailboxStoragePayload, senderKeys, receiverKeys.getPublic()); Connection mockedConnection = mock(Connection.class); when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(TestState.getTestNodeAddress())); this.testState.mockedStorage.onMessage(new AddDataMessage(protectedMailboxStorageEntry), mockedConnection); mailboxStoragePayload = (MailboxStoragePayload) protectedMailboxStorageEntry.getProtectedStoragePayload(); protectedMailboxStorageEntry = this.testState.mockedStorage.getMailboxDataWithSignedSeqNr(mailboxStoragePayload, receiverKeys, receiverKeys.getPublic()); SavedTestState beforeState = this.testState.saveTestState(protectedMailboxStorageEntry); assertTrue(this.testState.mockedStorage.remove(protectedMailboxStorageEntry, TestState.getTestNodeAddress())); this.testState.verifyProtectedStorageRemove(beforeState, protectedMailboxStorageEntry, true, true, true, true); } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/storage/P2PDataStorageGetDataIntegrationTest.java ================================================ /* * 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 . */ package haveno.network.p2p.storage; import haveno.common.app.Capabilities; import haveno.network.p2p.TestUtils; import haveno.network.p2p.peers.getdata.messages.GetDataRequest; import haveno.network.p2p.peers.getdata.messages.GetDataResponse; import haveno.network.p2p.storage.mocks.PersistableExpirableProtectedStoragePayloadStub; import haveno.network.p2p.storage.mocks.ProtectedStoragePayloadStub; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import haveno.network.p2p.storage.payload.ProtectedStoragePayload; import org.junit.jupiter.api.Test; import java.security.KeyPair; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class P2PDataStorageGetDataIntegrationTest { /** * Generates a unique ProtectedStorageEntry that is valid for add and remove. */ private ProtectedStorageEntry getProtectedStorageEntry() throws NoSuchAlgorithmException { KeyPair ownerKeys = TestUtils.generateKeyPair(); return getProtectedStorageEntry( ownerKeys.getPublic(), new ProtectedStoragePayloadStub(ownerKeys.getPublic()), 1); } private ProtectedStorageEntry getProtectedStorageEntry( PublicKey ownerPubKey, ProtectedStoragePayload protectedStoragePayload, int sequenceNumber) { ProtectedStorageEntry stub = mock(ProtectedStorageEntry.class); when(stub.getOwnerPubKey()).thenReturn(ownerPubKey); when(stub.isValidForAddOperation()).thenReturn(true); when(stub.isValidForRemoveOperation()).thenReturn(true); when(stub.matchesRelevantPubKey(any(ProtectedStorageEntry.class))).thenReturn(true); when(stub.getSequenceNumber()).thenReturn(sequenceNumber); when(stub.getProtectedStoragePayload()).thenReturn(protectedStoragePayload); return stub; } // TESTCASE: Basic synchronization of a ProtectedStorageEntry works between a seed node and client node //@Test public void basicSynchronizationWorks() throws NoSuchAlgorithmException { TestState seedNodeTestState = new TestState(); P2PDataStorage seedNode = seedNodeTestState.mockedStorage; TestState clientNodeTestState = new TestState(); P2PDataStorage clientNode = clientNodeTestState.mockedStorage; ProtectedStorageEntry onSeedNode = getProtectedStorageEntry(); seedNode.addProtectedStorageEntry(onSeedNode, null, null); GetDataRequest getDataRequest = clientNode.buildPreliminaryGetDataRequest(1); GetDataResponse getDataResponse = seedNode.buildGetDataResponse( getDataRequest, 1, new AtomicBoolean(), new AtomicBoolean(), new Capabilities()); TestState.SavedTestState beforeState = clientNodeTestState.saveTestState(onSeedNode); clientNode.processGetDataResponse(getDataResponse, null); clientNodeTestState.assertProtectedStorageAdd( beforeState, onSeedNode, true, true, false, true); } // TESTCASE: Synchronization after peer restart works for in-memory ProtectedStorageEntrys // @Test public void basicSynchronizationWorksAfterRestartTransient() throws NoSuchAlgorithmException { ProtectedStorageEntry transientEntry = getProtectedStorageEntry(); TestState seedNodeTestState = new TestState(); P2PDataStorage seedNode = seedNodeTestState.mockedStorage; TestState clientNodeTestState = new TestState(); P2PDataStorage clientNode = clientNodeTestState.mockedStorage; seedNode.addProtectedStorageEntry(transientEntry, null, null); clientNode.addProtectedStorageEntry(transientEntry, null, null); clientNodeTestState.simulateRestart(); clientNode = clientNodeTestState.mockedStorage; GetDataRequest getDataRequest = clientNode.buildPreliminaryGetDataRequest(1); GetDataResponse getDataResponse = seedNode.buildGetDataResponse( getDataRequest, 1, new AtomicBoolean(), new AtomicBoolean(), new Capabilities()); TestState.SavedTestState beforeState = clientNodeTestState.saveTestState(transientEntry); clientNode.processGetDataResponse(getDataResponse, null); clientNodeTestState.assertProtectedStorageAdd( beforeState, transientEntry, true, true, false, true); } // TESTCASE: Synchronization after peer restart works for in-memory ProtectedStorageEntrys @Test public void basicSynchronizationWorksAfterRestartPersistent() throws NoSuchAlgorithmException { KeyPair ownerKeys = TestUtils.generateKeyPair(); ProtectedStoragePayload persistentPayload = new PersistableExpirableProtectedStoragePayloadStub(ownerKeys.getPublic()); ProtectedStorageEntry persistentEntry = getProtectedStorageEntry(ownerKeys.getPublic(), persistentPayload, 1); TestState seedNodeTestState = new TestState(); P2PDataStorage seedNode = seedNodeTestState.mockedStorage; TestState clientNodeTestState = new TestState(); P2PDataStorage clientNode = clientNodeTestState.mockedStorage; seedNode.addProtectedStorageEntry(persistentEntry, null, null); clientNode.addProtectedStorageEntry(persistentEntry, null, null); clientNodeTestState.simulateRestart(); clientNode = clientNodeTestState.mockedStorage; GetDataRequest getDataRequest = clientNode.buildPreliminaryGetDataRequest(1); GetDataResponse getDataResponse = seedNode.buildGetDataResponse( getDataRequest, 1, new AtomicBoolean(), new AtomicBoolean(), new Capabilities()); TestState.SavedTestState beforeState = clientNodeTestState.saveTestState(persistentEntry); clientNode.processGetDataResponse(getDataResponse, null); clientNodeTestState.assertProtectedStorageAdd( beforeState, persistentEntry, false, false, false, false); assertTrue(clientNodeTestState.mockedStorage.getMap().containsValue(persistentEntry)); } // TESTCASE: Removes seen only by the seednode should be replayed on the client node // during startup // XXXBUGXXX: #3610 Lost removes are never replayed. @Test public void lostRemoveNeverUpdated() throws NoSuchAlgorithmException { TestState seedNodeTestState = new TestState(); P2PDataStorage seedNode = seedNodeTestState.mockedStorage; TestState clientNodeTestState = new TestState(); P2PDataStorage clientNode = clientNodeTestState.mockedStorage; // Both nodes see the add KeyPair ownerKeys = TestUtils.generateKeyPair(); ProtectedStoragePayload protectedStoragePayload = new ProtectedStoragePayloadStub(ownerKeys.getPublic()); ProtectedStorageEntry onSeedNodeAndClientNode = getProtectedStorageEntry( ownerKeys.getPublic(), protectedStoragePayload, 1); seedNode.addProtectedStorageEntry(onSeedNodeAndClientNode, null, null); clientNode.addProtectedStorageEntry(onSeedNodeAndClientNode, null, null); // Seed node sees the remove, but client node does not seedNode.remove(getProtectedStorageEntry( ownerKeys.getPublic(), protectedStoragePayload, 2), null); GetDataRequest getDataRequest = clientNode.buildPreliminaryGetDataRequest(1); GetDataResponse getDataResponse = seedNode.buildGetDataResponse( getDataRequest, 1, new AtomicBoolean(), new AtomicBoolean(), new Capabilities()); TestState.SavedTestState beforeState = clientNodeTestState.saveTestState(onSeedNodeAndClientNode); clientNode.processGetDataResponse(getDataResponse, null); // Should succeed clientNodeTestState.verifyProtectedStorageRemove( beforeState, onSeedNodeAndClientNode, false, false, false, false); } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/storage/P2PDataStorageOnMessageHandlerTest.java ================================================ /* * 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 . */ package haveno.network.p2p.storage; import haveno.common.proto.network.NetworkEnvelope; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.mocks.MockPayload; import haveno.network.p2p.network.Connection; import haveno.network.p2p.storage.messages.AddPersistableNetworkPayloadMessage; import haveno.network.p2p.storage.messages.BroadcastMessage; import haveno.network.p2p.storage.mocks.PersistableNetworkPayloadStub; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.Optional; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * Tests of the P2PDataStore MessageListener interface failure cases. The success cases are covered in the * PersistableNetworkPayloadTest and ProtectedStorageEntryTest tests, */ public class P2PDataStorageOnMessageHandlerTest { private TestState testState; @BeforeEach public void setup() { this.testState = new TestState(); } static class UnsupportedBroadcastMessage extends BroadcastMessage { UnsupportedBroadcastMessage() { super("0"); } } @Test public void invalidBroadcastMessage() { NetworkEnvelope envelope = new MockPayload("Mock"); Connection mockedConnection = mock(Connection.class); when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(TestState.getTestNodeAddress())); this.testState.mockedStorage.onMessage(envelope, mockedConnection); verify(this.testState.appendOnlyDataStoreListener, never()).onAdded(any(PersistableNetworkPayload.class)); verify(this.testState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class)); } @Test public void unsupportedBroadcastMessage() { NetworkEnvelope envelope = new UnsupportedBroadcastMessage(); Connection mockedConnection = mock(Connection.class); when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(TestState.getTestNodeAddress())); this.testState.mockedStorage.onMessage(envelope, mockedConnection); verify(this.testState.appendOnlyDataStoreListener, never()).onAdded(any(PersistableNetworkPayload.class)); verify(this.testState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class)); } @Test public void invalidConnectionObject() { PersistableNetworkPayload persistableNetworkPayload = new PersistableNetworkPayloadStub(true); NetworkEnvelope envelope = new AddPersistableNetworkPayloadMessage(persistableNetworkPayload); Connection mockedConnection = mock(Connection.class); when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.empty()); this.testState.mockedStorage.onMessage(envelope, mockedConnection); verify(this.testState.appendOnlyDataStoreListener, never()).onAdded(any(PersistableNetworkPayload.class)); verify(this.testState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class)); } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/storage/P2PDataStoragePersistableNetworkPayloadTest.java ================================================ /* * 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 . */ package haveno.network.p2p.storage; import haveno.network.p2p.network.Connection; import haveno.network.p2p.storage.messages.AddPersistableNetworkPayloadMessage; import haveno.network.p2p.storage.mocks.DateTolerantPayloadStub; import haveno.network.p2p.storage.mocks.PersistableNetworkPayloadStub; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import java.util.Optional; import java.util.stream.Stream; import static haveno.network.p2p.storage.TestState.SavedTestState; import static java.util.stream.Stream.of; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * Tests of the P2PDataStore entry points that use the PersistableNetworkPayload type *

    * The abstract base class AddPersistableNetworkPayloadTest defines the common test cases and Payload type * that needs to be tested is set up through extending the base class and overriding the createInstance() methods to * give the common tests a different payload to test. *

    * Each subclass (Payload type) can optionally add additional tests that verify functionality only relevant * to that payload. *

    * Each test case is run through 3 entry points to verify the correct behavior: *

    * 1 & 2 Client API [addPersistableNetworkPayload(reBroadcast=(true && false))] * 3. onMessage() [onMessage(AddPersistableNetworkPayloadMessage)] */ public class P2PDataStoragePersistableNetworkPayloadTest { public abstract static class AddPersistableNetworkPayloadTest { TestState testState; PersistableNetworkPayload persistableNetworkPayload; abstract PersistableNetworkPayload createInstance(); enum TestCase { PUBLIC_API, ON_MESSAGE, } @BeforeEach public void setup() { this.persistableNetworkPayload = this.createInstance(); this.testState = new TestState(); } void assertAndDoAdd(PersistableNetworkPayload persistableNetworkPayload, TestCase testCase, boolean reBroadcast, boolean expectedReturnValue, boolean expectedHashMapAndDataStoreUpdated, boolean expectedListenersSignaled, boolean expectedBroadcast) { SavedTestState beforeState = testState.saveTestState(persistableNetworkPayload); if (testCase == TestCase.PUBLIC_API) { assertEquals(expectedReturnValue, this.testState.mockedStorage.addPersistableNetworkPayload(persistableNetworkPayload, TestState.getTestNodeAddress(), reBroadcast)); } else { // onMessage Connection mockedConnection = mock(Connection.class); when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(TestState.getTestNodeAddress())); testState.mockedStorage.onMessage(new AddPersistableNetworkPayloadMessage(persistableNetworkPayload), mockedConnection); } this.testState.verifyPersistableAdd(beforeState, persistableNetworkPayload, expectedHashMapAndDataStoreUpdated, expectedListenersSignaled, expectedBroadcast); } static Stream data() { return of( new Object[]{TestCase.ON_MESSAGE, false}, new Object[]{TestCase.PUBLIC_API, true}, new Object[]{TestCase.PUBLIC_API, false} ); } @MethodSource("data") @ParameterizedTest(name = "{index}: Test with TestCase={0} allowBroadcast={1} reBroadcast={2} checkDate={3}") public void addPersistableNetworkPayload(TestCase testCase, boolean reBroadcast) { // First add should succeed regardless of parameters assertAndDoAdd(this.persistableNetworkPayload, testCase, reBroadcast, true, true, true, true); } @MethodSource("data") @ParameterizedTest(name = "{index}: Test with TestCase={0} allowBroadcast={1} reBroadcast={2} checkDate={3}") public void addPersistableNetworkPayloadDuplicate(TestCase testCase, boolean reBroadcast) { assertAndDoAdd(this.persistableNetworkPayload, testCase, reBroadcast, true, true, true, true); // We return true and broadcast if reBroadcast is set // assertAndDoAdd(this.persistableNetworkPayload, testCase, reBroadcast, reBroadcast, false, false, reBroadcast); } } /** * Runs the common test cases defined in AddPersistableNetworkPayloadTest against a PersistableNetworkPayload */ public static class AddPersistableNetworkPayloadStubTest extends AddPersistableNetworkPayloadTest { @Override PersistableNetworkPayloadStub createInstance() { return new PersistableNetworkPayloadStub(true); } @MethodSource("data") @ParameterizedTest(name = "{index}: Test with TestCase={0} allowBroadcast={1} reBroadcast={2} checkDate={3}") public void invalidHash(TestCase testCase, boolean reBroadcast) { PersistableNetworkPayload persistableNetworkPayload = new PersistableNetworkPayloadStub(false); assertAndDoAdd(persistableNetworkPayload, testCase, reBroadcast, false, false, false, false); } } /** * Runs the common test cases defined in AddPersistableNetworkPayloadTest against a PersistableNetworkPayload using * the DateTolerant marker interface. */ public static class AddPersistableDateTolerantPayloadTest extends AddPersistableNetworkPayloadTest { @Override DateTolerantPayloadStub createInstance() { return new DateTolerantPayloadStub(true); } @MethodSource("data") @ParameterizedTest(name = "{index}: Test with TestCase={0} allowBroadcast={1} reBroadcast={2} checkDate={3}") public void outOfTolerance(TestCase testCase, boolean reBroadcast) { PersistableNetworkPayload persistableNetworkPayload = new DateTolerantPayloadStub(false); // The onMessage path checks for tolerance boolean expectedReturn = testCase != TestCase.ON_MESSAGE; assertAndDoAdd(persistableNetworkPayload, testCase, reBroadcast, expectedReturn, expectedReturn, expectedReturn, expectedReturn); } } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/storage/P2PDataStorageProcessGetDataResponse.java ================================================ /* * 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 . */ package haveno.network.p2p.storage; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.TestUtils; import haveno.network.p2p.peers.getdata.messages.GetDataResponse; import haveno.network.p2p.storage.mocks.PersistableNetworkPayloadStub; import haveno.network.p2p.storage.mocks.ProtectedStoragePayloadStub; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import haveno.network.p2p.storage.payload.ProcessOncePersistableNetworkPayload; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import haveno.network.p2p.storage.payload.ProtectedStoragePayload; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.security.KeyPair; import java.security.NoSuchAlgorithmException; import java.util.Collections; import java.util.HashSet; import java.util.List; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class P2PDataStorageProcessGetDataResponse { private TestState testState; private NodeAddress peerNodeAddress; @BeforeEach public void setUp() { this.testState = new TestState(); this.peerNodeAddress = new NodeAddress("peer", 8080); } static private GetDataResponse buildGetDataResponse(PersistableNetworkPayload persistableNetworkPayload) { return buildGetDataResponse(Collections.emptyList(), Collections.singletonList(persistableNetworkPayload)); } static private GetDataResponse buildGetDataResponse(ProtectedStorageEntry protectedStorageEntry) { return buildGetDataResponse(Collections.singletonList(protectedStorageEntry), Collections.emptyList()); } static private GetDataResponse buildGetDataResponse( List protectedStorageEntries, List persistableNetworkPayloads) { return new GetDataResponse( new HashSet<>(protectedStorageEntries), new HashSet<>(persistableNetworkPayloads), 1, false, false); } /** * Generates a unique ProtectedStorageEntry that is valid for add. This is used to initialize P2PDataStorage state * so the tests can validate the correct behavior. Adds of identical payloads with different sequence numbers * is not supported. */ private ProtectedStorageEntry getProtectedStorageEntryForAdd() throws NoSuchAlgorithmException { KeyPair ownerKeys = TestUtils.generateKeyPair(); ProtectedStoragePayload protectedStoragePayload = new ProtectedStoragePayloadStub(ownerKeys.getPublic()); ProtectedStorageEntry stub = mock(ProtectedStorageEntry.class); when(stub.getOwnerPubKey()).thenReturn(ownerKeys.getPublic()); when(stub.isValidForAddOperation()).thenReturn(true); when(stub.matchesRelevantPubKey(any(ProtectedStorageEntry.class))).thenReturn(true); when(stub.getSequenceNumber()).thenReturn(1); when(stub.getProtectedStoragePayload()).thenReturn(protectedStoragePayload); return stub; } static class LazyPersistableNetworkPayloadStub extends PersistableNetworkPayloadStub implements ProcessOncePersistableNetworkPayload { LazyPersistableNetworkPayloadStub(byte[] hash) { super(hash); } LazyPersistableNetworkPayloadStub(boolean validHashSize) { super(validHashSize); } } // TESTCASE: GetDataResponse w/ missing PNP is added with no broadcast or listener signal // XXXBUGXXX: We signal listeners w/ non ProcessOncePersistableNetworkPayloads @Test public void processGetDataResponse_newPNPUpdatesState() { PersistableNetworkPayload persistableNetworkPayload = new PersistableNetworkPayloadStub(new byte[]{1}); GetDataResponse getDataResponse = buildGetDataResponse(persistableNetworkPayload); TestState.SavedTestState beforeState = this.testState.saveTestState(persistableNetworkPayload); this.testState.mockedStorage.processGetDataResponse(getDataResponse, this.peerNodeAddress); this.testState.verifyPersistableAdd( beforeState, persistableNetworkPayload, true, true, false); } // TESTCASE: GetDataResponse w/ invalid PNP does nothing (LazyProcessed) @Test public void processGetDataResponse_newInvalidPNPDoesNothing() { PersistableNetworkPayload persistableNetworkPayload = new LazyPersistableNetworkPayloadStub(false); GetDataResponse getDataResponse = buildGetDataResponse(persistableNetworkPayload); TestState.SavedTestState beforeState = this.testState.saveTestState(persistableNetworkPayload); this.testState.mockedStorage.processGetDataResponse(getDataResponse, this.peerNodeAddress); this.testState.verifyPersistableAdd( beforeState, persistableNetworkPayload, false, false, false); } // TESTCASE: GetDataResponse w/ existing PNP changes no state @Test public void processGetDataResponse_duplicatePNPDoesNothing() { PersistableNetworkPayload persistableNetworkPayload = new PersistableNetworkPayloadStub(new byte[]{1}); this.testState.mockedStorage.addPersistableNetworkPayload(persistableNetworkPayload, this.peerNodeAddress, false); GetDataResponse getDataResponse = buildGetDataResponse(persistableNetworkPayload); TestState.SavedTestState beforeState = this.testState.saveTestState(persistableNetworkPayload); this.testState.mockedStorage.processGetDataResponse(getDataResponse, this.peerNodeAddress); this.testState.verifyPersistableAdd( beforeState, persistableNetworkPayload, false, false, false); } // TESTCASE: GetDataResponse w/ missing PNP is added with no broadcast or listener signal (ProcessOncePersistableNetworkPayload) @Test public void processGetDataResponse_newPNPUpdatesState_LazyProcessed() { PersistableNetworkPayload persistableNetworkPayload = new LazyPersistableNetworkPayloadStub(new byte[]{1}); GetDataResponse getDataResponse = buildGetDataResponse(persistableNetworkPayload); TestState.SavedTestState beforeState = this.testState.saveTestState(persistableNetworkPayload); this.testState.mockedStorage.processGetDataResponse(getDataResponse, this.peerNodeAddress); this.testState.verifyPersistableAdd( beforeState, persistableNetworkPayload, true, false, false); } // TESTCASE: GetDataResponse w/ existing PNP changes no state (ProcessOncePersistableNetworkPayload) @Test public void processGetDataResponse_duplicatePNPDoesNothing_LazyProcessed() { PersistableNetworkPayload persistableNetworkPayload = new LazyPersistableNetworkPayloadStub(new byte[]{1}); this.testState.mockedStorage.addPersistableNetworkPayload(persistableNetworkPayload, this.peerNodeAddress, false); GetDataResponse getDataResponse = buildGetDataResponse(persistableNetworkPayload); TestState.SavedTestState beforeState = this.testState.saveTestState(persistableNetworkPayload); this.testState.mockedStorage.processGetDataResponse(getDataResponse, this.peerNodeAddress); this.testState.verifyPersistableAdd( beforeState, persistableNetworkPayload, false, false, false); } // TESTCASE: Second call to processGetDataResponse adds PNP for non-ProcessOncePersistableNetworkPayloads @Test public void processGetDataResponse_secondProcessNewPNPUpdatesState() { PersistableNetworkPayload addFromFirstProcess = new PersistableNetworkPayloadStub(new byte[]{1}); GetDataResponse getDataResponse = buildGetDataResponse(addFromFirstProcess); TestState.SavedTestState beforeState = this.testState.saveTestState(addFromFirstProcess); this.testState.mockedStorage.processGetDataResponse(getDataResponse, this.peerNodeAddress); this.testState.verifyPersistableAdd( beforeState, addFromFirstProcess, true, true, false); PersistableNetworkPayload addFromSecondProcess = new PersistableNetworkPayloadStub(new byte[]{2}); getDataResponse = buildGetDataResponse(addFromSecondProcess); beforeState = this.testState.saveTestState(addFromSecondProcess); this.testState.mockedStorage.processGetDataResponse(getDataResponse, this.peerNodeAddress); this.testState.verifyPersistableAdd( beforeState, addFromSecondProcess, true, true, false); } // TESTCASE: Second call to processGetDataResponse does not add any PNP (LazyProcessed) @Test public void processGetDataResponse_secondProcessNoPNPUpdates_LazyProcessed() { PersistableNetworkPayload addFromFirstProcess = new LazyPersistableNetworkPayloadStub(new byte[]{1}); GetDataResponse getDataResponse = buildGetDataResponse(addFromFirstProcess); TestState.SavedTestState beforeState = this.testState.saveTestState(addFromFirstProcess); this.testState.mockedStorage.processGetDataResponse(getDataResponse, this.peerNodeAddress); this.testState.verifyPersistableAdd( beforeState, addFromFirstProcess, true, false, false); PersistableNetworkPayload addFromSecondProcess = new LazyPersistableNetworkPayloadStub(new byte[]{2}); getDataResponse = buildGetDataResponse(addFromSecondProcess); beforeState = this.testState.saveTestState(addFromSecondProcess); this.testState.mockedStorage.processGetDataResponse(getDataResponse, this.peerNodeAddress); this.testState.verifyPersistableAdd( beforeState, addFromSecondProcess, false, false, false); } // TESTCASE: GetDataResponse w/ missing PSE is added with no broadcast or listener signal // XXXBUGXXX: We signal listeners for all ProtectedStorageEntrys @Test public void processGetDataResponse_newPSEUpdatesState() throws NoSuchAlgorithmException { ProtectedStorageEntry protectedStorageEntry = getProtectedStorageEntryForAdd(); GetDataResponse getDataResponse = buildGetDataResponse(protectedStorageEntry); TestState.SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); this.testState.mockedStorage.processGetDataResponse(getDataResponse, this.peerNodeAddress); this.testState.assertProtectedStorageAdd( beforeState, protectedStorageEntry, true, true, false, true); } // TESTCASE: GetDataResponse w/ existing PSE changes no state @Test public void processGetDataResponse_duplicatePSEDoesNothing() throws NoSuchAlgorithmException { ProtectedStorageEntry protectedStorageEntry = getProtectedStorageEntryForAdd(); this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, this.peerNodeAddress, null); GetDataResponse getDataResponse = buildGetDataResponse(protectedStorageEntry); this.testState.mockedStorage.processGetDataResponse(getDataResponse, this.peerNodeAddress); TestState.SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); this.testState.assertProtectedStorageAdd( beforeState, protectedStorageEntry, false, false, false, false); } // TESTCASE: GetDataResponse w/ missing PSE is added with no broadcast or listener signal // XXXBUGXXX: We signal listeners for all ProtectedStorageEntrys @Test public void processGetDataResponse_secondCallNewPSEUpdatesState() throws NoSuchAlgorithmException { ProtectedStorageEntry protectedStorageEntry = getProtectedStorageEntryForAdd(); GetDataResponse getDataResponse = buildGetDataResponse(protectedStorageEntry); TestState.SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); this.testState.mockedStorage.processGetDataResponse(getDataResponse, this.peerNodeAddress); this.testState.assertProtectedStorageAdd( beforeState, protectedStorageEntry, true, true, false, true); protectedStorageEntry = getProtectedStorageEntryForAdd(); getDataResponse = buildGetDataResponse(protectedStorageEntry); beforeState = this.testState.saveTestState(protectedStorageEntry); this.testState.mockedStorage.processGetDataResponse(getDataResponse, this.peerNodeAddress); this.testState.assertProtectedStorageAdd( beforeState, protectedStorageEntry, true, true, false, true); } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/storage/P2PDataStorageProtectedStorageEntryTest.java ================================================ /* * 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 . */ package haveno.network.p2p.storage; import haveno.common.app.Version; import haveno.common.crypto.CryptoException; import haveno.common.crypto.Sig; import haveno.network.p2p.TestUtils; import haveno.network.p2p.network.Connection; import haveno.network.p2p.storage.messages.AddDataMessage; import haveno.network.p2p.storage.messages.RefreshOfferMessage; import haveno.network.p2p.storage.messages.RemoveDataMessage; import haveno.network.p2p.storage.messages.RemoveMailboxDataMessage; import haveno.network.p2p.storage.mocks.PersistableExpirableProtectedStoragePayloadStub; import haveno.network.p2p.storage.mocks.ProtectedStoragePayloadStub; import haveno.network.p2p.storage.payload.ProtectedMailboxStorageEntry; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import haveno.network.p2p.storage.payload.ProtectedStoragePayload; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import java.security.KeyPair; import java.security.NoSuchAlgorithmException; import java.util.Map; import static haveno.network.p2p.storage.TestState.SavedTestState; import static haveno.network.p2p.storage.TestState.getTestNodeAddress; import static java.util.Optional.of; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * Tests of the P2PDataStore entry points that use the ProtectedStorageEntry type * * The abstract base class ProtectedStorageEntryTestBase defines the common test cases and each Entry and Payload type * that needs to be tested is set up through extending the base class and overriding the createInstance() and * getEntryClass() methods to give the common tests a different combination to test. * * Each subclass (Entry & Payload combination) can optionally add additional tests that verify functionality only relevant * to that combination. * * Each test case is run through 2 entry points to validate the correct behavior * 1. Client API [addProtectedStorageEntry(), refreshTTL(), remove()] * 2. onMessage() [AddDataMessage, RefreshOfferMessage, RemoveDataMessage] */ @SuppressWarnings("unused") public class P2PDataStorageProtectedStorageEntryTest { abstract public static class ProtectedStorageEntryTestBase { TestState testState; Class entryClass; protected abstract ProtectedStoragePayload createInstance(KeyPair payloadOwnerKeys); protected abstract Class getEntryClass(); // Used for tests of ProtectedStorageEntry and subclasses private ProtectedStoragePayload protectedStoragePayload; KeyPair payloadOwnerKeys; public boolean useMessageHandler; @BeforeEach public void setUp() throws CryptoException, NoSuchAlgorithmException { testState = new TestState(); payloadOwnerKeys = TestUtils.generateKeyPair(); protectedStoragePayload = createInstance(payloadOwnerKeys); entryClass = getEntryClass(); } boolean doRemove(ProtectedStorageEntry entry) { if (useMessageHandler) { Connection mockedConnection = mock(Connection.class); when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(of(getTestNodeAddress())); testState.mockedStorage.onMessage(new RemoveDataMessage(entry), mockedConnection); return true; } else { return testState.mockedStorage.remove(entry, getTestNodeAddress()); } } boolean doAdd(ProtectedStorageEntry protectedStorageEntry) { if (useMessageHandler) { Connection mockedConnection = mock(Connection.class); when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(of(getTestNodeAddress())); testState.mockedStorage.onMessage(new AddDataMessage(protectedStorageEntry), mockedConnection); return true; } else { return testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, getTestNodeAddress(), null); } } boolean doRefreshTTL(RefreshOfferMessage refreshOfferMessage) { if (useMessageHandler) { Connection mockedConnection = mock(Connection.class); when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(of(getTestNodeAddress())); testState.mockedStorage.onMessage(refreshOfferMessage, mockedConnection); return true; } else { return testState.mockedStorage.refreshTTL(refreshOfferMessage, getTestNodeAddress()); } } ProtectedStorageEntry getProtectedStorageEntryForAdd(int sequenceNumber, boolean validForAdd, boolean matchesRelevantPubKey) { ProtectedStorageEntry stub = mock(entryClass); when(stub.getOwnerPubKey()).thenReturn(payloadOwnerKeys.getPublic()); when(stub.isValidForAddOperation()).thenReturn(validForAdd); when(stub.matchesRelevantPubKey(any(ProtectedStorageEntry.class))).thenReturn(matchesRelevantPubKey); when(stub.getSequenceNumber()).thenReturn(sequenceNumber); when(stub.getProtectedStoragePayload()).thenReturn(protectedStoragePayload); return stub; } // Return a ProtectedStorageEntry that will pass all validity checks for add. ProtectedStorageEntry getProtectedStorageEntryForAdd(int sequenceNumber) { return getProtectedStorageEntryForAdd(sequenceNumber, true, true); } // Return a ProtectedStorageEntry that will pass all validity checks for remove. ProtectedStorageEntry getProtectedStorageEntryForRemove(int sequenceNumber, boolean validForRemove, boolean matchesRelevantPubKey) { ProtectedStorageEntry stub = mock(entryClass); when(stub.getOwnerPubKey()).thenReturn(payloadOwnerKeys.getPublic()); when(stub.isValidForRemoveOperation()).thenReturn(validForRemove); when(stub.matchesRelevantPubKey(any(ProtectedStorageEntry.class))).thenReturn(matchesRelevantPubKey); when(stub.getSequenceNumber()).thenReturn(sequenceNumber); when(stub.getProtectedStoragePayload()).thenReturn(protectedStoragePayload); return stub; } ProtectedStorageEntry getProtectedStorageEntryForRemove(int sequenceNumber) { return getProtectedStorageEntryForRemove(sequenceNumber, true, true); } void assertAndDoProtectedStorageAdd(ProtectedStorageEntry protectedStorageEntry, boolean expectedReturnValue, boolean expectedStateChange) { SavedTestState beforeState = testState.saveTestState(protectedStorageEntry); boolean addResult = doAdd(protectedStorageEntry); if (!useMessageHandler) assertEquals(expectedReturnValue, addResult); if (expectedStateChange) { testState.assertProtectedStorageAdd( beforeState, protectedStorageEntry, true, true, true, true); } else{ testState.assertProtectedStorageAdd( beforeState, protectedStorageEntry, false, false, false, false); } } void assertAndDoProtectedStorageRemove(ProtectedStorageEntry entry, boolean expectedReturnValue, boolean expectedHashMapAndDataStoreUpdated, boolean expectedListenersSignaled, boolean expectedBroadcast, boolean expectedSeqNrWrite) { SavedTestState beforeState = testState.saveTestState(entry); boolean addResult = doRemove(entry); if (!useMessageHandler) assertEquals(expectedReturnValue, addResult); testState.verifyProtectedStorageRemove(beforeState, entry, expectedHashMapAndDataStoreUpdated, expectedListenersSignaled, expectedBroadcast, expectedSeqNrWrite); } @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void addProtectedStorageEntry(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entryForAdd = getProtectedStorageEntryForAdd(1); assertAndDoProtectedStorageAdd(entryForAdd, true, true); } // TESTCASE: Adding duplicate payload w/ same sequence number @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void addProtectedStorageEntryDuplicateSeqNrGt0(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entryForAdd = getProtectedStorageEntryForAdd(1); assertAndDoProtectedStorageAdd(entryForAdd, true, true); assertAndDoProtectedStorageAdd(entryForAdd, false, false); } // TESTCASE: Adding duplicate payload w/ 0 sequence number (special branch in code for logging) @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void addProtectedStorageEntryDuplicateSeqNrEq0(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entryForAdd = getProtectedStorageEntryForAdd(0); assertAndDoProtectedStorageAdd(entryForAdd, true, true); assertAndDoProtectedStorageAdd(entryForAdd, false, false); } // TESTCASE: Adding duplicate payload for w/ lower sequence number @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void addProtectedStorageEntryLowerSeqNr(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entryForAdd2 = getProtectedStorageEntryForAdd(2); ProtectedStorageEntry entryForAdd1 = getProtectedStorageEntryForAdd(1); assertAndDoProtectedStorageAdd(entryForAdd2, true, true); assertAndDoProtectedStorageAdd(entryForAdd1, false, false); } // TESTCASE: Adding duplicate payload for w/ greater sequence number @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void addProtectedStorageEntryGreaterSeqNr(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entryForAdd2 = getProtectedStorageEntryForAdd(1); ProtectedStorageEntry entryForAdd1 = getProtectedStorageEntryForAdd(2); assertAndDoProtectedStorageAdd(entryForAdd2, true, true); assertAndDoProtectedStorageAdd(entryForAdd1, true, true); } // TESTCASE: Add w/ same sequence number after remove of sequence number // Regression test for old remove() behavior that succeeded if add.seq# == remove.seq# @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void addProtectedStorageEntryAfterRemoveSameSeqNr(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entryForAdd = getProtectedStorageEntryForAdd(1); ProtectedStorageEntry entryForRemove = getProtectedStorageEntryForRemove(1); assertAndDoProtectedStorageAdd(entryForAdd, true, true); assertAndDoProtectedStorageRemove(entryForRemove, false, false, false, false, false); assertAndDoProtectedStorageAdd(entryForAdd, false, false); } // Invalid add tests (isValidForAddOperation() || matchesRelevantPubKey()) returns false // TESTCASE: Add fails if Entry is not valid for add @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void addProtectedStorageEntryNotisValidForAddOperation(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entryForAdd = getProtectedStorageEntryForAdd(1, false, true); assertAndDoProtectedStorageAdd(entryForAdd, false, false); } // TESTCASE: Add fails if Entry metadata does not match existing Entry @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void addProtectedStorageEntryNotMatchesRelevantPubKey(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; // Add a valid entry ProtectedStorageEntry entryForAdd = getProtectedStorageEntryForAdd(1); assertAndDoProtectedStorageAdd(entryForAdd, true, true); // Add an entry where metadata is different from first add, but otherwise is valid entryForAdd = getProtectedStorageEntryForAdd(2, true, false); assertAndDoProtectedStorageAdd(entryForAdd, false, false); } // TESTCASE: Add fails if Entry metadata does not match existing Entry and is not valid for add @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void addProtectedStorageEntryNotMatchesRelevantPubKeyNotisValidForAddOperation(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; // Add a valid entry ProtectedStorageEntry entryForAdd = getProtectedStorageEntryForAdd(1); assertAndDoProtectedStorageAdd(entryForAdd, true, true); // Add an entry where entry is not valid and metadata is different from first add entryForAdd = getProtectedStorageEntryForAdd(2, false, false); assertAndDoProtectedStorageAdd(entryForAdd, false, false); } /// Valid remove tests (isValidForRemove() and isMetadataEquals() return true) // TESTCASE: Removing an item after successfully added (remove seq # == add seq #) @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void removeSeqNrEqAddSeqNr(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entryForAdd = getProtectedStorageEntryForAdd(1); ProtectedStorageEntry entryForRemove = getProtectedStorageEntryForRemove(1); assertAndDoProtectedStorageAdd(entryForAdd, true, true); assertAndDoProtectedStorageRemove(entryForRemove, false, false, false, false, false); } // TESTCASE: Removing an item after successfully added (remove seq # > add seq #) @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void removeSeqNrGtAddSeqNr(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entryForAdd = getProtectedStorageEntryForAdd(1); ProtectedStorageEntry entryForRemove = getProtectedStorageEntryForRemove(2); assertAndDoProtectedStorageAdd(entryForAdd, true, true); assertAndDoProtectedStorageRemove(entryForRemove, true, true, true, true, true); } // TESTCASE: Removing an item before it was added. This triggers a SequenceNumberMap write and broadcast @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void removeNotExists(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entryForRemove = getProtectedStorageEntryForRemove(1); assertAndDoProtectedStorageRemove(entryForRemove, true, false, false, true, true); } // TESTCASE: Removing an item after successfully adding (remove seq # < add seq #) @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void removeSeqNrLessAddSeqNr(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entryForAdd = getProtectedStorageEntryForAdd(2); ProtectedStorageEntry entryForRemove = getProtectedStorageEntryForRemove(1); assertAndDoProtectedStorageAdd(entryForAdd, true, true); assertAndDoProtectedStorageRemove(entryForRemove, false, false, false, false, false); } // TESTCASE: Add after removed (same seq #) @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void addAfterRemoveSameSeqNr(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entryForAdd = getProtectedStorageEntryForAdd(1); assertAndDoProtectedStorageAdd(entryForAdd, true, true); ProtectedStorageEntry entryForRemove = getProtectedStorageEntryForRemove(2); assertAndDoProtectedStorageRemove(entryForRemove, true, true, true, true, true); assertAndDoProtectedStorageAdd(entryForAdd, false, false); } // TESTCASE: Add after removed (greater seq #) @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void addAfterRemoveGreaterSeqNr(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entryForAdd = getProtectedStorageEntryForAdd(1); assertAndDoProtectedStorageAdd(entryForAdd, true, true); ProtectedStorageEntry entryForRemove = getProtectedStorageEntryForRemove(2); assertAndDoProtectedStorageRemove(entryForRemove, true, true, true, true, true); entryForAdd = getProtectedStorageEntryForAdd(3); assertAndDoProtectedStorageAdd(entryForAdd, true, true); } /// Invalid remove tests (isValidForRemoveOperation() || matchesRelevantPubKey()) returns false // TESTCASE: Remove fails if Entry isn't valid for remove @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void removeEntryNotisValidForRemoveOperation(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entryForAdd = getProtectedStorageEntryForAdd(1); assertAndDoProtectedStorageAdd(entryForAdd, true, true); ProtectedStorageEntry entryForRemove = getProtectedStorageEntryForRemove(2, false, true); assertAndDoProtectedStorageRemove(entryForRemove, false, false, false, false, false); } // TESTCASE: Remove fails if Entry is valid for remove, but metadata doesn't match remove target @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void removeEntryNotMatchesRelevantPubKey(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entryForAdd = getProtectedStorageEntryForAdd(1); assertAndDoProtectedStorageAdd(entryForAdd, true, true); ProtectedStorageEntry entryForRemove = getProtectedStorageEntryForRemove(2, true, false); assertAndDoProtectedStorageRemove(entryForRemove, false, false, false, false, false); } // TESTCASE: Remove fails if Entry is not valid for remove and metadata doesn't match remove target @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void removeEntryNotisValidForRemoveOperationNotMatchesRelevantPubKey(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entryForAdd = getProtectedStorageEntryForAdd(1); assertAndDoProtectedStorageAdd(entryForAdd, true, true); ProtectedStorageEntry entryForRemove = getProtectedStorageEntryForRemove(2, false, false); assertAndDoProtectedStorageRemove(entryForRemove, false, false, false, false, false); } // TESTCASE: Add after removed (lower seq #) @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void addAfterRemoveLessSeqNr(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entryForAdd = getProtectedStorageEntryForAdd(2); assertAndDoProtectedStorageAdd(entryForAdd, true, true); ProtectedStorageEntry entryForRemove = getProtectedStorageEntryForRemove(3); assertAndDoProtectedStorageRemove(entryForRemove, true, true, true, true, true); entryForAdd = getProtectedStorageEntryForAdd(1); assertAndDoProtectedStorageAdd(entryForAdd, false, false); } // TESTCASE: Received remove for nonexistent item that was later received @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void removeLateAdd(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entryForAdd = getProtectedStorageEntryForAdd(1); ProtectedStorageEntry entryForRemove = getProtectedStorageEntryForRemove(2); doRemove(entryForRemove); assertAndDoProtectedStorageAdd(entryForAdd, false, false); } // TESTCASE: Invalid remove doesn't block a valid add (isValidForRemove == false | matchesRelevantPubKey == false) @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void removeEntryNotIsValidForRemoveDoesNotBlockAdd1(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entryForAdd = getProtectedStorageEntryForAdd(1); ProtectedStorageEntry entryForRemove = getProtectedStorageEntryForRemove(1, false, false); doRemove(entryForRemove); assertAndDoProtectedStorageAdd(entryForAdd, true, true); } // TESTCASE: Invalid remove doesn't block a valid add (isValidForRemove == false | matchesRelevantPubKey == true) @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void removeEntryNotIsValidForRemoveDoesNotBlockAdd2(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entryForAdd = getProtectedStorageEntryForAdd(1); ProtectedStorageEntry entryForRemove = getProtectedStorageEntryForRemove(1, false, true); doRemove(entryForRemove); assertAndDoProtectedStorageAdd(entryForAdd, true, true); } } /** * Runs the common test cases defined in ProtectedStorageEntryTestBase against a ProtectedStorageEntry * wrapper and ProtectedStoragePayload payload. */ public static class ProtectedStorageEntryTest extends ProtectedStorageEntryTestBase { @Override protected ProtectedStoragePayload createInstance(KeyPair payloadOwnerKeys) { return new ProtectedStoragePayloadStub(payloadOwnerKeys.getPublic()); } @Override protected Class getEntryClass() { return ProtectedStorageEntry.class; } static RefreshOfferMessage buildRefreshOfferMessage(ProtectedStoragePayload protectedStoragePayload, KeyPair ownerKeys, int sequenceNumber) throws CryptoException { P2PDataStorage.ByteArray hashOfPayload = P2PDataStorage.get32ByteHashAsByteArray(protectedStoragePayload); byte[] hashOfDataAndSeqNr = P2PDataStorage.get32ByteHash(new P2PDataStorage.DataAndSeqNrPair(protectedStoragePayload, sequenceNumber)); byte[] signature = Sig.sign(ownerKeys.getPrivate(), hashOfDataAndSeqNr); return new RefreshOfferMessage(hashOfDataAndSeqNr, signature, hashOfPayload.bytes, sequenceNumber); } RefreshOfferMessage buildRefreshOfferMessage(ProtectedStorageEntry protectedStorageEntry, KeyPair ownerKeys, int sequenceNumber) throws CryptoException { return buildRefreshOfferMessage(protectedStorageEntry.getProtectedStoragePayload(), ownerKeys, sequenceNumber); } void assertAndDoRefreshTTL(RefreshOfferMessage refreshOfferMessage, boolean expectedReturnValue, boolean expectStateChange) { SavedTestState beforeState = testState.saveTestState(refreshOfferMessage); boolean returnValue = doRefreshTTL(refreshOfferMessage); if (!useMessageHandler) assertEquals(expectedReturnValue, returnValue); testState.verifyRefreshTTL(beforeState, refreshOfferMessage, expectStateChange); } // TESTCASE: Refresh an entry that doesn't exist @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void refreshTTLNoExist(boolean useMessageHandler) throws CryptoException { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entry = getProtectedStorageEntryForAdd(1); assertAndDoRefreshTTL(buildRefreshOfferMessage(entry, payloadOwnerKeys,1), false, false); } // TESTCASE: Refresh an entry where seq # is equal to last seq # seen @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void refreshTTLExistingEntry(boolean useMessageHandler) throws CryptoException { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entry = getProtectedStorageEntryForAdd(1); assertAndDoProtectedStorageAdd(entry, true, true); assertAndDoRefreshTTL(buildRefreshOfferMessage(entry, payloadOwnerKeys,1), false, false); } // TESTCASE: Duplicate refresh message (same seq #) @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void refreshTTLDuplicateRefreshSeqNrEqual(boolean useMessageHandler) throws CryptoException { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entry = getProtectedStorageEntryForAdd(1); assertAndDoProtectedStorageAdd(entry, true, true); testState.incrementClock(); assertAndDoRefreshTTL(buildRefreshOfferMessage(entry, payloadOwnerKeys, 2), true, true); testState.incrementClock(); assertAndDoRefreshTTL(buildRefreshOfferMessage(entry, payloadOwnerKeys, 2), false, false); } // TESTCASE: Duplicate refresh message (greater seq #) @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void refreshTTLDuplicateRefreshSeqNrGreater(boolean useMessageHandler) throws CryptoException { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entry = getProtectedStorageEntryForAdd(1); assertAndDoProtectedStorageAdd(entry, true, true); testState.incrementClock(); assertAndDoRefreshTTL(buildRefreshOfferMessage(entry, payloadOwnerKeys,2), true, true); testState.incrementClock(); assertAndDoRefreshTTL(buildRefreshOfferMessage(entry, payloadOwnerKeys,3), true, true); } // TESTCASE: Duplicate refresh message (lower seq #) @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void refreshTTLDuplicateRefreshSeqNrLower(boolean useMessageHandler) throws CryptoException { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entry = getProtectedStorageEntryForAdd(1); assertAndDoProtectedStorageAdd(entry, true, true); testState.incrementClock(); assertAndDoRefreshTTL(buildRefreshOfferMessage(entry, payloadOwnerKeys,3), true, true); testState.incrementClock(); assertAndDoRefreshTTL(buildRefreshOfferMessage(entry, payloadOwnerKeys,2), false, false); } // TESTCASE: Refresh previously removed entry @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void refreshTTLRefreshAfterRemove(boolean useMessageHandler) throws CryptoException { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entryForAdd = getProtectedStorageEntryForAdd(1); ProtectedStorageEntry entryForRemove = getProtectedStorageEntryForRemove(2); assertAndDoProtectedStorageAdd(entryForAdd, true, true); assertAndDoProtectedStorageRemove(entryForRemove, true, true, true, true, true); assertAndDoRefreshTTL(buildRefreshOfferMessage(entryForAdd, payloadOwnerKeys,3), false, false); } // TESTCASE: Refresh an entry, but owner doesn't match PubKey of original add owner @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void refreshTTLRefreshEntryOwnerOriginalOwnerMismatch(boolean useMessageHandler) throws CryptoException, NoSuchAlgorithmException { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entry = getProtectedStorageEntryForAdd(1); assertAndDoProtectedStorageAdd(entry, true, true); KeyPair notOwner = TestUtils.generateKeyPair(); assertAndDoRefreshTTL(buildRefreshOfferMessage(entry, notOwner, 2), false, false); } // TESTCASE: After restart, identical sequence numbers are accepted ONCE. We need a way to reconstruct // in-memory ProtectedStorageEntrys from seed and peer nodes around startup time. @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void addProtectedStorageEntryAfterRestartCanAddDuplicateSeqNr(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry toAdd1 = getProtectedStorageEntryForAdd(1); assertAndDoProtectedStorageAdd(toAdd1, true, true); testState.simulateRestart(); // Can add equal seqNr only once assertAndDoProtectedStorageAdd(toAdd1, true, true); // Can't add equal seqNr twice assertAndDoProtectedStorageAdd(toAdd1, false, false); } // TESTCASE: After restart, old sequence numbers are not accepted @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void addProtectedStorageEntryAfterRestartCanNotAddLowerSeqNr(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry toAdd1 = getProtectedStorageEntryForAdd(1); ProtectedStorageEntry toAdd2 = getProtectedStorageEntryForAdd(2); assertAndDoProtectedStorageAdd(toAdd2, true, true); testState.simulateRestart(); assertAndDoProtectedStorageAdd(toAdd1, false, false); } } /** * Runs the common test cases defined in ProtectedStorageEntryTestBase against a ProtectedStorageEntry * wrapper and PersistableExpirableProtectedStoragePayload payload. */ public static class PersistableExpirableProtectedStoragePayloadStubTest extends ProtectedStorageEntryTestBase { @Override protected ProtectedStoragePayload createInstance(KeyPair payloadOwnerKeys) { return new PersistableExpirableProtectedStoragePayloadStub(payloadOwnerKeys.getPublic()); } @Override protected Class getEntryClass() { return ProtectedStorageEntry.class; } // Tests that just apply to PersistablePayload objects // TESTCASE: Ensure the HashMap is the same before and after a restart @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void addProtectedStorageEntryAfterReadFromResourcesWithDuplicate3629RegressionTest(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry protectedStorageEntry = getProtectedStorageEntryForAdd(1); assertAndDoProtectedStorageAdd(protectedStorageEntry, true, true); Map beforeRestart = testState.mockedStorage.getMap(); testState.simulateRestart(); assertEquals(beforeRestart, testState.mockedStorage.getMap()); } // TESTCASE: After restart, identical sequence numbers are not accepted for persistent payloads @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") public void addProtectedStorageEntryAfterRestartCanNotAddDuplicateSeqNr(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry toAdd1 = getProtectedStorageEntryForAdd(1); assertAndDoProtectedStorageAdd(toAdd1, true, true); testState.simulateRestart(); // Can add equal seqNr only once assertAndDoProtectedStorageAdd(toAdd1, false, false); } } /** * Runs the common test cases defined in ProtectedStorageEntryTestBase against a ProtectedMailboxStorageEntry * wrapper and MailboxStoragePayload payload. */ public static class MailboxPayloadTest extends ProtectedStorageEntryTestBase { @Override protected ProtectedStoragePayload createInstance(KeyPair payloadOwnerKeys) { return TestState.buildMailboxStoragePayload(payloadOwnerKeys.getPublic(), payloadOwnerKeys.getPublic()); } @Override protected Class getEntryClass() { return ProtectedMailboxStorageEntry.class; } @Override @BeforeEach public void setUp() throws CryptoException, NoSuchAlgorithmException { super.setUp(); // Deep in the bowels of protobuf we grab the messageID from the version module. This is required to hash the // full MailboxStoragePayload so make sure it is initialized. Version.setBaseCryptoNetworkId(1); } @Override boolean doRemove(ProtectedStorageEntry entry) { if (useMessageHandler) { Connection mockedConnection = mock(Connection.class); when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(of(getTestNodeAddress())); testState.mockedStorage.onMessage(new RemoveMailboxDataMessage((ProtectedMailboxStorageEntry) entry), mockedConnection); return true; } else { return testState.mockedStorage.remove(entry, getTestNodeAddress()); } } // TESTCASE: Add after removed when add-once required (greater seq #) @Override @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "{index}: Test with useMessageHandler={0}") @Disabled //TODO fix test public void addAfterRemoveGreaterSeqNr(boolean useMessageHandler) { this.useMessageHandler = useMessageHandler; ProtectedStorageEntry entryForAdd = getProtectedStorageEntryForAdd(1); assertAndDoProtectedStorageAdd(entryForAdd, true, true); ProtectedStorageEntry entryForRemove = getProtectedStorageEntryForRemove(2); assertAndDoProtectedStorageRemove(entryForRemove, true, true, true, true, true); entryForAdd = getProtectedStorageEntryForAdd(3); assertAndDoProtectedStorageAdd(entryForAdd, false, false); } } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/storage/P2PDataStorageRemoveExpiredTest.java ================================================ /* * 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 . */ package haveno.network.p2p.storage; import haveno.common.app.Version; import haveno.common.crypto.CryptoException; import haveno.network.p2p.TestUtils; import haveno.network.p2p.storage.mocks.ExpirableProtectedStoragePayloadStub; import haveno.network.p2p.storage.mocks.PersistableExpirableProtectedStoragePayloadStub; import haveno.network.p2p.storage.mocks.PersistableNetworkPayloadStub; import haveno.network.p2p.storage.mocks.ProtectedStoragePayloadStub; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import haveno.network.p2p.storage.payload.ProtectedStoragePayload; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.security.KeyPair; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.concurrent.TimeUnit; import static haveno.network.p2p.storage.TestState.MAX_SEQUENCE_NUMBER_MAP_SIZE_BEFORE_PURGE; import static haveno.network.p2p.storage.TestState.SavedTestState; import static haveno.network.p2p.storage.TestState.getTestNodeAddress; import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests of the P2PDataStore behavior that expires old Entries periodically. */ public class P2PDataStorageRemoveExpiredTest { private TestState testState; @BeforeEach public void setUp() { testState = new TestState(); // Deep in the bowels of protobuf we grab the messageID from the version module. This is required to hash the // full MailboxStoragePayload so make sure it is initialized. Version.setBaseCryptoNetworkId(1); } // TESTCASE: Correctly skips entries that are not expirable @Test public void removeExpiredEntries_SkipsNonExpirableEntries() throws NoSuchAlgorithmException, CryptoException { KeyPair ownerKeys = TestUtils.generateKeyPair(); ProtectedStoragePayload protectedStoragePayload = new ProtectedStoragePayloadStub(ownerKeys.getPublic()); ProtectedStorageEntry protectedStorageEntry = testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); assertTrue(testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, getTestNodeAddress(), null)); SavedTestState beforeState = testState.saveTestState(protectedStorageEntry); testState.mockedStorage.removeExpiredEntries(); testState.verifyProtectedStorageRemove(beforeState, protectedStorageEntry, false, false, false, false); } // TESTCASE: Correctly skips all PersistableNetworkPayloads since they are not expirable @Test public void removeExpiredEntries_skipsPersistableNetworkPayload() { PersistableNetworkPayload persistableNetworkPayload = new PersistableNetworkPayloadStub(true); assertTrue(testState.mockedStorage.addPersistableNetworkPayload(persistableNetworkPayload, getTestNodeAddress(), false)); testState.mockedStorage.removeExpiredEntries(); assertTrue(testState.mockedStorage.appendOnlyDataStoreService.getMap(persistableNetworkPayload).containsKey(new P2PDataStorage.ByteArray(persistableNetworkPayload.getHash()))); } // TESTCASE: Correctly skips non-persistable entries that are not expired @Test public void removeExpiredEntries_SkipNonExpiredExpirableEntries() throws CryptoException, NoSuchAlgorithmException { KeyPair ownerKeys = TestUtils.generateKeyPair(); ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayloadStub(ownerKeys.getPublic()); ProtectedStorageEntry protectedStorageEntry = testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); assertTrue(testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, getTestNodeAddress(), null)); SavedTestState beforeState = testState.saveTestState(protectedStorageEntry); testState.mockedStorage.removeExpiredEntries(); testState.verifyProtectedStorageRemove(beforeState, protectedStorageEntry, false, false, false, false); } // TESTCASE: Correctly expires non-persistable entries that are expired @Test public void removeExpiredEntries_ExpiresExpiredExpirableEntries() throws CryptoException, NoSuchAlgorithmException { KeyPair ownerKeys = TestUtils.generateKeyPair(); ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayloadStub(ownerKeys.getPublic(), 0); ProtectedStorageEntry protectedStorageEntry = testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); assertTrue(testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, getTestNodeAddress(), null)); // Increment the clock by an hour which will cause the Payloads to be outside the TTL range testState.incrementClock(); SavedTestState beforeState = testState.saveTestState(protectedStorageEntry); testState.mockedStorage.removeExpiredEntries(); testState.verifyProtectedStorageRemove(beforeState, protectedStorageEntry, true, true, false, false); } // TESTCASE: Correctly skips persistable entries that are not expired @Test public void removeExpiredEntries_SkipNonExpiredPersistableExpirableEntries() throws CryptoException, NoSuchAlgorithmException { KeyPair ownerKeys = TestUtils.generateKeyPair(); ProtectedStoragePayload protectedStoragePayload = new PersistableExpirableProtectedStoragePayloadStub(ownerKeys.getPublic()); ProtectedStorageEntry protectedStorageEntry = testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); assertTrue(testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, getTestNodeAddress(), null)); SavedTestState beforeState = testState.saveTestState(protectedStorageEntry); testState.mockedStorage.removeExpiredEntries(); testState.verifyProtectedStorageRemove(beforeState, protectedStorageEntry, false, false, false, false); } // TESTCASE: Correctly expires persistable entries that are expired @Test public void removeExpiredEntries_ExpiresExpiredPersistableExpirableEntries() throws CryptoException, NoSuchAlgorithmException { KeyPair ownerKeys = TestUtils.generateKeyPair(); ProtectedStoragePayload protectedStoragePayload = new PersistableExpirableProtectedStoragePayloadStub(ownerKeys.getPublic(), 0); ProtectedStorageEntry protectedStorageEntry = testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); assertTrue(testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, getTestNodeAddress(), null)); // Increment the clock by an hour which will cause the Payloads to be outside the TTL range testState.incrementClock(); SavedTestState beforeState = testState.saveTestState(protectedStorageEntry); testState.mockedStorage.removeExpiredEntries(); testState.verifyProtectedStorageRemove(beforeState, protectedStorageEntry, true, true, false, false); } // TESTCASE: Ensure we try to purge old entries sequence number map when size exceeds the maximum size // and that entries less than PURGE_AGE_DAYS remain @Test public void removeExpiredEntries_PurgeSeqNrMap() throws CryptoException, NoSuchAlgorithmException { final int initialClockIncrement = 5; ArrayList expectedRemoves = new ArrayList<>(); // Add 4 entries to our sequence number map that will be purged KeyPair purgedOwnerKeys = TestUtils.generateKeyPair(); ProtectedStoragePayload purgedProtectedStoragePayload = new PersistableExpirableProtectedStoragePayloadStub(purgedOwnerKeys.getPublic(), 0); ProtectedStorageEntry purgedProtectedStorageEntry = testState.mockedStorage.getProtectedStorageEntry(purgedProtectedStoragePayload, purgedOwnerKeys); expectedRemoves.add(purgedProtectedStorageEntry); assertTrue(testState.mockedStorage.addProtectedStorageEntry(purgedProtectedStorageEntry, getTestNodeAddress(), null)); for (int i = 0; i < MAX_SEQUENCE_NUMBER_MAP_SIZE_BEFORE_PURGE - 1; ++i) { KeyPair ownerKeys = TestUtils.generateKeyPair(); ProtectedStoragePayload protectedStoragePayload = new PersistableExpirableProtectedStoragePayloadStub(ownerKeys.getPublic(), 0); ProtectedStorageEntry tmpEntry = testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); expectedRemoves.add(tmpEntry); assertTrue(testState.mockedStorage.addProtectedStorageEntry(tmpEntry, getTestNodeAddress(), null)); } // Increment the time by 5 days which is less than the purge requirement. This will allow the map to have // some values that will be purged and others that will stay. testState.clockFake.increment(TimeUnit.DAYS.toMillis(initialClockIncrement)); // Add a final entry that will not be purged KeyPair keepOwnerKeys = TestUtils.generateKeyPair(); ProtectedStoragePayload keepProtectedStoragePayload = new PersistableExpirableProtectedStoragePayloadStub(keepOwnerKeys.getPublic(), 0); ProtectedStorageEntry keepProtectedStorageEntry = testState.mockedStorage.getProtectedStorageEntry(keepProtectedStoragePayload, keepOwnerKeys); expectedRemoves.add(keepProtectedStorageEntry); assertTrue(testState.mockedStorage.addProtectedStorageEntry(keepProtectedStorageEntry, getTestNodeAddress(), null)); // P2PDataStorage::PURGE_AGE_DAYS == 10 days // Advance time past it so they will be valid purge targets testState.clockFake.increment(TimeUnit.DAYS.toMillis(P2PDataStorage.PURGE_AGE_DAYS + 1 - initialClockIncrement)); // The first 4 entries (11 days old) should be purged from the SequenceNumberMap SavedTestState beforeState = testState.saveTestState(purgedProtectedStorageEntry); testState.mockedStorage.removeExpiredEntries(); testState.verifyProtectedStorageRemove(beforeState, expectedRemoves, true, true, false, false); } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/storage/P2PDataStorageRequestDataTest.java ================================================ /* * 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 . */ package haveno.network.p2p.storage; import haveno.common.app.Capabilities; import haveno.common.app.Capability; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.TestUtils; import haveno.network.p2p.peers.getdata.messages.GetUpdatedDataRequest; import haveno.network.p2p.peers.getdata.messages.PreliminaryGetDataRequest; import haveno.network.p2p.storage.mocks.PersistableNetworkPayloadStub; import haveno.network.p2p.storage.mocks.ProtectedStoragePayloadStub; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import haveno.network.p2p.storage.payload.ProtectedStoragePayload; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.security.KeyPair; import java.security.NoSuchAlgorithmException; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class P2PDataStorageRequestDataTest { private TestState testState; private NodeAddress localNodeAddress; @BeforeEach public void setUp() { this.testState = new TestState(); this.localNodeAddress = new NodeAddress("localhost", 8080); // Set up basic capabilities to ensure message contains it Capabilities.app.addAll(Capability.MEDIATION); } /** * Returns true if the target bytes are found in the container set. */ private boolean byteSetContains(Set container, byte[] target) { // Set.contains() doesn't do a deep compare, so generate a Set so equals() does what // we want Set translatedContainer = P2PDataStorage.ByteArray.convertBytesSetToByteArraySet(container); return translatedContainer.contains(new P2PDataStorage.ByteArray(target)); } /** * Generates a unique ProtectedStorageEntry that is valid for add. This is used to initialize P2PDataStorage state * so the tests can validate the correct behavior. Adds of identical payloads with different sequence numbers * is not supported. */ private ProtectedStorageEntry getProtectedStorageEntryForAdd() throws NoSuchAlgorithmException { KeyPair ownerKeys = TestUtils.generateKeyPair(); ProtectedStoragePayload protectedStoragePayload = new ProtectedStoragePayloadStub(ownerKeys.getPublic()); ProtectedStorageEntry stub = mock(ProtectedStorageEntry.class); when(stub.getOwnerPubKey()).thenReturn(ownerKeys.getPublic()); when(stub.isValidForAddOperation()).thenReturn(true); when(stub.matchesRelevantPubKey(any(ProtectedStorageEntry.class))).thenReturn(true); when(stub.getSequenceNumber()).thenReturn(1); when(stub.getProtectedStoragePayload()).thenReturn(protectedStoragePayload); return stub; } // TESTCASE: P2PDataStorage with no entries returns an empty PreliminaryGetDataRequest @Test public void buildPreliminaryGetDataRequest_EmptyP2PDataStore() { PreliminaryGetDataRequest getDataRequest = this.testState.mockedStorage.buildPreliminaryGetDataRequest(1); assertEquals(getDataRequest.getNonce(), 1); assertEquals(getDataRequest.getSupportedCapabilities(), Capabilities.app); assertTrue(getDataRequest.getExcludedKeys().isEmpty()); } // TESTCASE: P2PDataStorage with no entries returns an empty PreliminaryGetDataRequest @Test public void buildGetUpdatedDataRequest_EmptyP2PDataStore() { GetUpdatedDataRequest getDataRequest = this.testState.mockedStorage.buildGetUpdatedDataRequest(this.localNodeAddress, 1); assertEquals(getDataRequest.getNonce(), 1); assertEquals(getDataRequest.getSenderNodeAddress(), this.localNodeAddress); assertTrue(getDataRequest.getExcludedKeys().isEmpty()); } // TESTCASE: P2PDataStorage with PersistableNetworkPayloads and ProtectedStorageEntry generates // correct GetDataRequestMessage with both sets of keys. @Test public void buildPreliminaryGetDataRequest_FilledP2PDataStore() throws NoSuchAlgorithmException { PersistableNetworkPayload toAdd1 = new PersistableNetworkPayloadStub(new byte[]{1}); PersistableNetworkPayload toAdd2 = new PersistableNetworkPayloadStub(new byte[]{2}); ProtectedStorageEntry toAdd3 = getProtectedStorageEntryForAdd(); ProtectedStorageEntry toAdd4 = getProtectedStorageEntryForAdd(); this.testState.mockedStorage.addPersistableNetworkPayload(toAdd1, this.localNodeAddress, false); this.testState.mockedStorage.addPersistableNetworkPayload(toAdd2, this.localNodeAddress, false); this.testState.mockedStorage.addProtectedStorageEntry(toAdd3, this.localNodeAddress, null); this.testState.mockedStorage.addProtectedStorageEntry(toAdd4, this.localNodeAddress, null); PreliminaryGetDataRequest getDataRequest = this.testState.mockedStorage.buildPreliminaryGetDataRequest(1); assertEquals(getDataRequest.getNonce(), 1); assertEquals(getDataRequest.getSupportedCapabilities(), Capabilities.app); assertEquals(4, getDataRequest.getExcludedKeys().size()); assertTrue(byteSetContains(getDataRequest.getExcludedKeys(), toAdd1.getHash())); assertTrue(byteSetContains(getDataRequest.getExcludedKeys(), toAdd2.getHash())); assertTrue(byteSetContains(getDataRequest.getExcludedKeys(), P2PDataStorage.get32ByteHash(toAdd3.getProtectedStoragePayload()))); assertTrue(byteSetContains(getDataRequest.getExcludedKeys(), P2PDataStorage.get32ByteHash(toAdd4.getProtectedStoragePayload()))); } // TESTCASE: P2PDataStorage with PersistableNetworkPayloads and ProtectedStorageEntry generates // correct GetDataRequestMessage with both sets of keys. @Test public void requestData_FilledP2PDataStore_GetUpdatedDataRequest() throws NoSuchAlgorithmException { PersistableNetworkPayload toAdd1 = new PersistableNetworkPayloadStub(new byte[]{1}); PersistableNetworkPayload toAdd2 = new PersistableNetworkPayloadStub(new byte[]{2}); ProtectedStorageEntry toAdd3 = getProtectedStorageEntryForAdd(); ProtectedStorageEntry toAdd4 = getProtectedStorageEntryForAdd(); this.testState.mockedStorage.addPersistableNetworkPayload(toAdd1, this.localNodeAddress, false); this.testState.mockedStorage.addPersistableNetworkPayload(toAdd2, this.localNodeAddress, false); this.testState.mockedStorage.addProtectedStorageEntry(toAdd3, this.localNodeAddress, null); this.testState.mockedStorage.addProtectedStorageEntry(toAdd4, this.localNodeAddress, null); GetUpdatedDataRequest getDataRequest = this.testState.mockedStorage.buildGetUpdatedDataRequest(this.localNodeAddress, 1); assertEquals(getDataRequest.getNonce(), 1); assertEquals(getDataRequest.getSenderNodeAddress(), this.localNodeAddress); assertEquals(4, getDataRequest.getExcludedKeys().size()); assertTrue(byteSetContains(getDataRequest.getExcludedKeys(), toAdd1.getHash())); assertTrue(byteSetContains(getDataRequest.getExcludedKeys(), toAdd2.getHash())); assertTrue(byteSetContains(getDataRequest.getExcludedKeys(), P2PDataStorage.get32ByteHash(toAdd3.getProtectedStoragePayload()))); assertTrue(byteSetContains(getDataRequest.getExcludedKeys(), P2PDataStorage.get32ByteHash(toAdd4.getProtectedStoragePayload()))); } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/storage/P2PDataStoreDisconnectTest.java ================================================ /* * 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 . */ package haveno.network.p2p.storage; import haveno.common.crypto.CryptoException; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.TestUtils; import haveno.network.p2p.network.CloseConnectionReason; import haveno.network.p2p.network.Connection; import haveno.network.p2p.storage.mocks.ExpirableProtectedStoragePayloadStub; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import haveno.network.p2p.storage.payload.ProtectedStoragePayload; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.security.KeyPair; import java.security.NoSuchAlgorithmException; import java.util.Optional; import java.util.concurrent.TimeUnit; import static haveno.network.p2p.storage.TestState.SavedTestState; import static haveno.network.p2p.storage.TestState.getTestNodeAddress; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * Tests of the P2PDataStore ConnectionListener interface. */ public class P2PDataStoreDisconnectTest { private TestState testState; private Connection mockedConnection; private static ProtectedStorageEntry populateTestState(TestState testState, long ttl) throws CryptoException, NoSuchAlgorithmException { KeyPair ownerKeys = TestUtils.generateKeyPair(); ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayloadStub(ownerKeys.getPublic(), ttl); ProtectedStorageEntry protectedStorageEntry = testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, getTestNodeAddress(), null); return protectedStorageEntry; } private static void verifyStateAfterDisconnect(TestState currentState, SavedTestState beforeState, boolean wasTTLReduced) { ProtectedStorageEntry protectedStorageEntry = beforeState.protectedStorageEntryBeforeOp; currentState.verifyProtectedStorageRemove(beforeState, protectedStorageEntry, false, false, false, false); if (wasTTLReduced) { assertTrue(protectedStorageEntry.getCreationTimeStamp() < beforeState.creationTimestampBeforeUpdate); } else { assertEquals(protectedStorageEntry.getCreationTimeStamp(), beforeState.creationTimestampBeforeUpdate); } } @BeforeEach public void setUp() { this.mockedConnection = mock(Connection.class); this.testState = new TestState(); } // TESTCASE: Bad peer info @Test public void peerConnectionUnknown() throws CryptoException, NoSuchAlgorithmException { when(this.mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.empty()); ProtectedStorageEntry protectedStorageEntry = populateTestState(testState, 2); SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); this.testState.mockedStorage.onDisconnect(CloseConnectionReason.SOCKET_CLOSED, mockedConnection); verifyStateAfterDisconnect(this.testState, beforeState, false); } // TESTCASE: Intended disconnects don't trigger expiration @Test public void connectionClosedIntended() throws CryptoException, NoSuchAlgorithmException { when(this.mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(getTestNodeAddress())); ProtectedStorageEntry protectedStorageEntry = populateTestState(testState, 2); SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); this.testState.mockedStorage.onDisconnect(CloseConnectionReason.CLOSE_REQUESTED_BY_PEER, mockedConnection); verifyStateAfterDisconnect(this.testState, beforeState, false); } // TESTCASE: Peer NodeAddress unknown @Test public void connectionClosedSkipsItemsPeerInfoBadState() throws NoSuchAlgorithmException, CryptoException { when(this.mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.empty()); ProtectedStorageEntry protectedStorageEntry = populateTestState(testState, 2); SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); this.testState.mockedStorage.onDisconnect(CloseConnectionReason.SOCKET_CLOSED, mockedConnection); verifyStateAfterDisconnect(this.testState, beforeState, false); } // TESTCASE: Unintended disconnects reduce the TTL for entrys that match disconnected peer @Test public void connectionClosedReduceTTL() throws NoSuchAlgorithmException, CryptoException { when(this.mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(getTestNodeAddress())); ProtectedStorageEntry protectedStorageEntry = populateTestState(testState, TimeUnit.DAYS.toMillis(90)); SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); this.testState.mockedStorage.onDisconnect(CloseConnectionReason.SOCKET_CLOSED, mockedConnection); verifyStateAfterDisconnect(this.testState, beforeState, true); } // TESTCASE: Unintended disconnects don't reduce TTL for entrys that are not from disconnected peer @Test public void connectionClosedSkipsItemsNotFromPeer() throws NoSuchAlgorithmException, CryptoException { when(this.mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(new NodeAddress("notTestNode", 2020))); ProtectedStorageEntry protectedStorageEntry = populateTestState(testState, 2); SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); this.testState.mockedStorage.onDisconnect(CloseConnectionReason.SOCKET_CLOSED, mockedConnection); verifyStateAfterDisconnect(this.testState, beforeState, false); } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/storage/TestState.java ================================================ /* * 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 . */ package haveno.network.p2p.storage; import haveno.common.crypto.Sig; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.persistable.PersistablePayload; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.network.NetworkNode; import haveno.network.p2p.peers.Broadcaster; import haveno.network.p2p.storage.messages.AddDataMessage; import haveno.network.p2p.storage.messages.AddPersistableNetworkPayloadMessage; import haveno.network.p2p.storage.messages.BroadcastMessage; import haveno.network.p2p.storage.messages.RefreshOfferMessage; import haveno.network.p2p.storage.messages.RemoveDataMessage; import haveno.network.p2p.storage.messages.RemoveMailboxDataMessage; import haveno.network.p2p.storage.mocks.AppendOnlyDataStoreServiceFake; import haveno.network.p2p.storage.mocks.ClockFake; import haveno.network.p2p.storage.mocks.MapStoreServiceFake; import haveno.network.p2p.storage.payload.MailboxStoragePayload; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import haveno.network.p2p.storage.payload.ProtectedMailboxStorageEntry; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import haveno.network.p2p.storage.persistence.AppendOnlyDataStoreListener; import haveno.network.p2p.storage.persistence.ProtectedDataStoreService; import haveno.network.p2p.storage.persistence.RemovedPayloadsService; import haveno.network.p2p.storage.persistence.ResourceDataStoreService; import haveno.network.p2p.storage.persistence.SequenceNumberMap; import org.mockito.ArgumentCaptor; import java.security.PublicKey; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.any; import static org.mockito.Mockito.isNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.nullable; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * Test object that stores a P2PDataStore instance as well as the mock objects necessary for state validation. *

    * Used in the P2PDataStorage*Test(s) in order to leverage common test set up and validation. */ public class TestState { static final int MAX_SEQUENCE_NUMBER_MAP_SIZE_BEFORE_PURGE = 5; P2PDataStorage mockedStorage; final Broadcaster mockBroadcaster; final AppendOnlyDataStoreListener appendOnlyDataStoreListener; private final HashMapChangedListener hashMapChangedListener; private final PersistenceManager mockSeqNrPersistenceManager; private final ProtectedDataStoreService protectedDataStoreService; final ClockFake clockFake; private RemovedPayloadsService removedPayloadsService; TestState() { mockBroadcaster = mock(Broadcaster.class); mockSeqNrPersistenceManager = mock(PersistenceManager.class); removedPayloadsService = mock(RemovedPayloadsService.class); clockFake = new ClockFake(); protectedDataStoreService = new ProtectedDataStoreService(); mockedStorage = new P2PDataStorage(mock(NetworkNode.class), mockBroadcaster, new AppendOnlyDataStoreServiceFake(), protectedDataStoreService, mock(ResourceDataStoreService.class), mockSeqNrPersistenceManager, removedPayloadsService, clockFake, MAX_SEQUENCE_NUMBER_MAP_SIZE_BEFORE_PURGE); appendOnlyDataStoreListener = mock(AppendOnlyDataStoreListener.class); hashMapChangedListener = mock(HashMapChangedListener.class); protectedDataStoreService.addService(new MapStoreServiceFake()); mockedStorage = createP2PDataStorageForTest( mockBroadcaster, protectedDataStoreService, mockSeqNrPersistenceManager, clockFake, hashMapChangedListener, appendOnlyDataStoreListener, removedPayloadsService); when(mockSeqNrPersistenceManager.getPersisted()) .thenReturn(mockedStorage.sequenceNumberMap); } /** * Re-initializes the in-memory data structures from the storage objects to simulate a node restarting. Important * to note that the current TestState uses Test Doubles instead of actual disk storage so this is just "simulating" * not running the entire storage code paths. */ void simulateRestart() { removedPayloadsService = mock(RemovedPayloadsService.class); mockedStorage = createP2PDataStorageForTest( mockBroadcaster, protectedDataStoreService, mockSeqNrPersistenceManager, clockFake, hashMapChangedListener, appendOnlyDataStoreListener, removedPayloadsService); when(mockSeqNrPersistenceManager.getPersisted()) .thenReturn(mockedStorage.sequenceNumberMap); } private static P2PDataStorage createP2PDataStorageForTest( Broadcaster broadcaster, ProtectedDataStoreService protectedDataStoreService, PersistenceManager sequenceNrMapPersistenceManager, ClockFake clock, HashMapChangedListener hashMapChangedListener, AppendOnlyDataStoreListener appendOnlyDataStoreListener, RemovedPayloadsService removedPayloadsService) { P2PDataStorage p2PDataStorage = new P2PDataStorage(mock(NetworkNode.class), broadcaster, new AppendOnlyDataStoreServiceFake(), protectedDataStoreService, mock(ResourceDataStoreService.class), sequenceNrMapPersistenceManager, removedPayloadsService, clock, MAX_SEQUENCE_NUMBER_MAP_SIZE_BEFORE_PURGE); // Currently TestState only supports reading ProtectedStorageEntries off disk. p2PDataStorage.readFromResourcesSync("unused"); p2PDataStorage.readPersistedSync(); p2PDataStorage.addHashMapChangedListener(hashMapChangedListener); p2PDataStorage.addAppendOnlyDataStoreListener(appendOnlyDataStoreListener); return p2PDataStorage; } private void resetState() { reset(mockBroadcaster); reset(appendOnlyDataStoreListener); reset(hashMapChangedListener); } void incrementClock() { clockFake.increment(TimeUnit.HOURS.toMillis(1)); } public static NodeAddress getTestNodeAddress() { return new NodeAddress("address", 8080); } /** * Common test helpers that verify the correct events were signaled based on the test expectation and before/after states. */ private void verifySequenceNumberMapWriteContains(P2PDataStorage.ByteArray payloadHash, int sequenceNumber) { assertEquals(sequenceNumber, mockSeqNrPersistenceManager.getPersisted().get(payloadHash).sequenceNr); } void verifyPersistableAdd(SavedTestState beforeState, PersistableNetworkPayload persistableNetworkPayload, boolean expectedHashMapAndDataStoreUpdated, boolean expectedListenersSignaled, boolean expectedBroadcast) { P2PDataStorage.ByteArray hash = new P2PDataStorage.ByteArray(persistableNetworkPayload.getHash()); if (expectedHashMapAndDataStoreUpdated) assertEquals(persistableNetworkPayload, mockedStorage.appendOnlyDataStoreService.getMap(persistableNetworkPayload).get(hash)); else assertEquals(beforeState.persistableNetworkPayloadBeforeOp, mockedStorage.appendOnlyDataStoreService.getMap(persistableNetworkPayload).get(hash)); if (expectedListenersSignaled) verify(appendOnlyDataStoreListener).onAdded(persistableNetworkPayload); else verify(appendOnlyDataStoreListener, never()).onAdded(persistableNetworkPayload); if (expectedBroadcast) verify(mockBroadcaster).broadcast(any(AddPersistableNetworkPayloadMessage.class), nullable(NodeAddress.class)); else verify(mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), nullable(NodeAddress.class)); } void assertProtectedStorageAdd(SavedTestState beforeState, ProtectedStorageEntry protectedStorageEntry, boolean expectedHashMapAndDataStoreUpdated, boolean expectedListenersSignaled, boolean expectedBroadcast, boolean expectedSequenceNrMapWrite) { P2PDataStorage.ByteArray hashMapHash = P2PDataStorage.get32ByteHashAsByteArray(protectedStorageEntry.getProtectedStoragePayload()); if (expectedHashMapAndDataStoreUpdated) { assertEquals(protectedStorageEntry, mockedStorage.getMap().get(hashMapHash)); if (protectedStorageEntry.getProtectedStoragePayload() instanceof PersistablePayload) assertEquals(protectedStorageEntry, protectedDataStoreService.getMap().get(hashMapHash)); } else { assertEquals(beforeState.protectedStorageEntryBeforeOp, mockedStorage.getMap().get(hashMapHash)); assertEquals(beforeState.protectedStorageEntryBeforeOpDataStoreMap, protectedDataStoreService.getMap().get(hashMapHash)); } if (expectedListenersSignaled) { verify(hashMapChangedListener).onAdded(Collections.singletonList(protectedStorageEntry)); } else { verify(hashMapChangedListener, never()).onAdded(Collections.singletonList(protectedStorageEntry)); } if (expectedBroadcast) { final ArgumentCaptor captor = ArgumentCaptor.forClass(BroadcastMessage.class); // If we remove the last argument (isNull()) tests fail. No idea why as the broadcast method has an // overloaded method with nullable listener. Seems a testframework issue as it should not matter if the // method with listener is called with null argument or the other method with no listener. We removed the // null value from all other calls but here we can't as it breaks the test. verify(mockBroadcaster).broadcast(captor.capture(), nullable(NodeAddress.class), isNull()); BroadcastMessage broadcastMessage = captor.getValue(); assertTrue(broadcastMessage instanceof AddDataMessage); assertEquals(protectedStorageEntry, ((AddDataMessage) broadcastMessage).getProtectedStorageEntry()); } else { verify(mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), nullable(NodeAddress.class)); } if (expectedSequenceNrMapWrite) { verifySequenceNumberMapWriteContains(P2PDataStorage.get32ByteHashAsByteArray(protectedStorageEntry.getProtectedStoragePayload()), protectedStorageEntry.getSequenceNumber()); } } void verifyProtectedStorageRemove(SavedTestState beforeState, ProtectedStorageEntry protectedStorageEntry, boolean expectedHashMapAndDataStoreUpdated, boolean expectedListenersSignaled, boolean expectedBroadcast, boolean expectedSeqNrWrite) { verifyProtectedStorageRemove(beforeState, Collections.singletonList(protectedStorageEntry), expectedHashMapAndDataStoreUpdated, expectedListenersSignaled, expectedBroadcast, expectedSeqNrWrite); } void verifyProtectedStorageRemove(SavedTestState beforeState, Collection protectedStorageEntries, boolean expectedHashMapAndDataStoreUpdated, boolean expectedListenersSignaled, boolean expectedBroadcast, boolean expectedSeqNrWrite) { // The default matcher expects orders to stay the same. So, create a custom matcher function since // we don't care about the order. if (expectedListenersSignaled) { final ArgumentCaptor> argument = ArgumentCaptor.forClass(Collection.class); verify(hashMapChangedListener).onRemoved(argument.capture()); Set actual = new HashSet<>(argument.getValue()); Set expected = new HashSet<>(protectedStorageEntries); // Ensure we didn't remove duplicates assertEquals(protectedStorageEntries.size(), expected.size()); assertEquals(argument.getValue().size(), actual.size()); assertEquals(expected, actual); } else { verify(hashMapChangedListener, never()).onRemoved(any()); } if (!expectedBroadcast) verify(mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), nullable(NodeAddress.class)); protectedStorageEntries.forEach(protectedStorageEntry -> { P2PDataStorage.ByteArray hashMapHash = P2PDataStorage.get32ByteHashAsByteArray(protectedStorageEntry.getProtectedStoragePayload()); if (expectedSeqNrWrite) verifySequenceNumberMapWriteContains(P2PDataStorage.get32ByteHashAsByteArray( protectedStorageEntry.getProtectedStoragePayload()), protectedStorageEntry.getSequenceNumber()); if (expectedBroadcast) { if (protectedStorageEntry instanceof ProtectedMailboxStorageEntry) verify(mockBroadcaster).broadcast(any(RemoveMailboxDataMessage.class), nullable(NodeAddress.class)); else verify(mockBroadcaster).broadcast(any(RemoveDataMessage.class), nullable(NodeAddress.class)); } if (expectedHashMapAndDataStoreUpdated) { assertNull(mockedStorage.getMap().get(hashMapHash)); if (protectedStorageEntry.getProtectedStoragePayload() instanceof PersistablePayload) assertNull(protectedDataStoreService.getMap().get(hashMapHash)); } else { assertEquals(beforeState.protectedStorageEntryBeforeOp, mockedStorage.getMap().get(hashMapHash)); } }); } void verifyRefreshTTL(SavedTestState beforeState, RefreshOfferMessage refreshOfferMessage, boolean expectedStateChange) { P2PDataStorage.ByteArray payloadHash = new P2PDataStorage.ByteArray(refreshOfferMessage.getHashOfPayload()); ProtectedStorageEntry entryAfterRefresh = mockedStorage.getMap().get(payloadHash); if (expectedStateChange) { assertNotNull(entryAfterRefresh); assertEquals(refreshOfferMessage.getSequenceNumber(), entryAfterRefresh.getSequenceNumber()); assertEquals(refreshOfferMessage.getSignature(), entryAfterRefresh.getSignature()); assertTrue(entryAfterRefresh.getCreationTimeStamp() > beforeState.creationTimestampBeforeUpdate); final ArgumentCaptor captor = ArgumentCaptor.forClass(BroadcastMessage.class); verify(mockBroadcaster).broadcast(captor.capture(), nullable(NodeAddress.class)); BroadcastMessage broadcastMessage = captor.getValue(); assertTrue(broadcastMessage instanceof RefreshOfferMessage); assertEquals(refreshOfferMessage, broadcastMessage); verifySequenceNumberMapWriteContains(payloadHash, refreshOfferMessage.getSequenceNumber()); } else { // Verify the existing entry is unchanged if (beforeState.protectedStorageEntryBeforeOp != null) { assertEquals(entryAfterRefresh, beforeState.protectedStorageEntryBeforeOp); assertEquals(beforeState.protectedStorageEntryBeforeOp.getSequenceNumber(), entryAfterRefresh.getSequenceNumber()); assertEquals(beforeState.protectedStorageEntryBeforeOp.getSignature(), entryAfterRefresh.getSignature()); assertEquals(beforeState.creationTimestampBeforeUpdate, entryAfterRefresh.getCreationTimeStamp()); } verify(mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), nullable(NodeAddress.class)); } } static MailboxStoragePayload buildMailboxStoragePayload(PublicKey senderKey, PublicKey receiverKey) { // Need to be able to take the hash which leverages protobuf Messages protobuf.StoragePayload messageMock = mock(protobuf.StoragePayload.class); when(messageMock.toByteArray()).thenReturn(Sig.getPublicKeyBytes(receiverKey)); MailboxStoragePayload payloadMock = mock(MailboxStoragePayload.class); when(payloadMock.getOwnerPubKey()).thenReturn(receiverKey); when(payloadMock.getSenderPubKeyForAddOperation()).thenReturn(senderKey); when(payloadMock.toProtoMessage()).thenReturn(messageMock); return payloadMock; } SavedTestState saveTestState(PersistableNetworkPayload persistableNetworkPayload) { return new SavedTestState(this, persistableNetworkPayload); } SavedTestState saveTestState(ProtectedStorageEntry protectedStorageEntry) { return new SavedTestState(this, protectedStorageEntry); } SavedTestState saveTestState(RefreshOfferMessage refreshOfferMessage) { return new SavedTestState(this, refreshOfferMessage); } /** * Wrapper object for TestState state that needs to be saved for future validation. Used in multiple tests * to verify that the state before and after an operation matched the expectation. */ static class SavedTestState { final TestState state; // Used in PersistableNetworkPayload tests PersistableNetworkPayload persistableNetworkPayloadBeforeOp; // Used in ProtectedStorageEntry tests ProtectedStorageEntry protectedStorageEntryBeforeOp; ProtectedStorageEntry protectedStorageEntryBeforeOpDataStoreMap; long creationTimestampBeforeUpdate; private SavedTestState(TestState state) { this.state = state; creationTimestampBeforeUpdate = 0; state.resetState(); } private SavedTestState(TestState testState, PersistableNetworkPayload persistableNetworkPayload) { this(testState); P2PDataStorage.ByteArray hash = new P2PDataStorage.ByteArray(persistableNetworkPayload.getHash()); persistableNetworkPayloadBeforeOp = testState.mockedStorage.appendOnlyDataStoreService.getMap(persistableNetworkPayload).get(hash); } private SavedTestState(TestState testState, ProtectedStorageEntry protectedStorageEntry) { this(testState); P2PDataStorage.ByteArray hashMapHash = P2PDataStorage.get32ByteHashAsByteArray(protectedStorageEntry.getProtectedStoragePayload()); protectedStorageEntryBeforeOp = testState.mockedStorage.getMap().get(hashMapHash); protectedStorageEntryBeforeOpDataStoreMap = testState.protectedDataStoreService.getMap().get(hashMapHash); creationTimestampBeforeUpdate = (protectedStorageEntryBeforeOp != null) ? protectedStorageEntryBeforeOp.getCreationTimeStamp() : 0; } private SavedTestState(TestState testState, RefreshOfferMessage refreshOfferMessage) { this(testState); P2PDataStorage.ByteArray hashMapHash = new P2PDataStorage.ByteArray(refreshOfferMessage.getHashOfPayload()); protectedStorageEntryBeforeOp = testState.mockedStorage.getMap().get(hashMapHash); creationTimestampBeforeUpdate = (protectedStorageEntryBeforeOp != null) ? protectedStorageEntryBeforeOp.getCreationTimeStamp() : 0; } } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/storage/messages/AddDataMessageTest.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.messages; import haveno.common.app.Version; import haveno.common.crypto.CryptoException; import haveno.common.crypto.KeyRing; import haveno.common.crypto.KeyStorage; import haveno.common.crypto.SealedAndSigned; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.PrefixedSealedAndSignedMessage; import haveno.network.p2p.storage.payload.MailboxStoragePayload; import haveno.network.p2p.storage.payload.ProtectedMailboxStorageEntry; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.File; import java.io.IOException; import java.security.InvalidKeyException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.SignatureException; import java.security.cert.CertificateException; import java.time.Clock; @SuppressWarnings("UnusedAssignment") @Slf4j public class AddDataMessageTest { private KeyRing keyRing1; private File dir1; @BeforeEach public void setup() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException { dir1 = File.createTempFile("temp_tests1", ""); //noinspection ResultOfMethodCallIgnored dir1.delete(); //noinspection ResultOfMethodCallIgnored dir1.mkdir(); keyRing1 = new KeyRing(new KeyStorage(dir1), null, true); Version.setBaseCryptoNetworkId(1); } @Test public void toProtoBuf() throws Exception { SealedAndSigned sealedAndSigned = new SealedAndSigned(RandomUtils.nextBytes(10), RandomUtils.nextBytes(10), RandomUtils.nextBytes(10), keyRing1.getPubKeyRing().getSignaturePubKey()); PrefixedSealedAndSignedMessage prefixedSealedAndSignedMessage = new PrefixedSealedAndSignedMessage(new NodeAddress("host", 1000), sealedAndSigned); MailboxStoragePayload mailboxStoragePayload = new MailboxStoragePayload(prefixedSealedAndSignedMessage, keyRing1.getPubKeyRing().getSignaturePubKey(), keyRing1.getPubKeyRing().getSignaturePubKey(), MailboxStoragePayload.TTL); ProtectedStorageEntry protectedStorageEntry = new ProtectedMailboxStorageEntry(mailboxStoragePayload, keyRing1.getSignatureKeyPair().getPublic(), 1, RandomUtils.nextBytes(10), keyRing1.getPubKeyRing().getSignaturePubKey(), Clock.systemDefaultZone()); AddDataMessage dataMessage1 = new AddDataMessage(protectedStorageEntry); protobuf.NetworkEnvelope envelope = dataMessage1.toProtoNetworkEnvelope(); //TODO Use NetworkProtoResolver, PersistenceProtoResolver or ProtoResolver which are all in io.haveno.common. /* AddDataMessage dataMessage2 = (AddDataMessage) ProtoBufferUtilities.getAddDataMessage(envelope); assertTrue(dataMessage1.protectedStorageEntry.getStoragePayload().equals(dataMessage2.protectedStorageEntry.getStoragePayload())); assertTrue(dataMessage1.protectedStorageEntry.equals(dataMessage2.protectedStorageEntry)); assertTrue(dataMessage1.equals(dataMessage2));*/ } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/storage/mocks/AppendOnlyDataStoreServiceFake.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.mocks; import haveno.network.p2p.storage.P2PDataStorage; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import haveno.network.p2p.storage.persistence.AppendOnlyDataStoreService; /** * Implementation of an in-memory AppendOnlyDataStoreService that can be used in tests. Removes overhead * involving files, resources, and services for tests that don't need it. * * @see Reference */ public class AppendOnlyDataStoreServiceFake extends AppendOnlyDataStoreService { public AppendOnlyDataStoreServiceFake() { addService(new MapStoreServiceFake()); } public boolean put(P2PDataStorage.ByteArray hashAsByteArray, PersistableNetworkPayload payload) { return super.put(hashAsByteArray, payload); } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/storage/mocks/ClockFake.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.mocks; import java.time.Clock; import java.time.Instant; import java.time.ZoneId; /** * Fake implementation of the Clock object that can be used in tests that need finer control over the current time. * * @see Reference */ public class ClockFake extends Clock { private Instant currentInstant; public ClockFake() { this.currentInstant = Instant.now(); } @Override public ZoneId getZone() { throw new UnsupportedOperationException("ClockFake does not support getZone"); } @Override public Clock withZone(ZoneId zoneId) { throw new UnsupportedOperationException("ClockFake does not support withZone"); } @Override public Instant instant() { return this.currentInstant; } public void increment(long milliseconds) { this.currentInstant = this.currentInstant.plusMillis(milliseconds); } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/storage/mocks/DateTolerantPayloadStub.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.mocks; import haveno.network.p2p.storage.payload.DateTolerantPayload; import java.time.Clock; /** * Stub implementation of a ProtectedStoragePayload implementing the DateTolerantPayload marker interface * that can be used in tests to provide canned answers to calls. Useful if the tests don't care about the implementation * details of the ProtectedStoragePayload. * * @see Reference */ public class DateTolerantPayloadStub implements DateTolerantPayload { private final boolean dateInTolerance; public DateTolerantPayloadStub(boolean dateInTolerance) { this.dateInTolerance = dateInTolerance; } @Override public boolean isDateInTolerance(Clock clock) { return this.dateInTolerance; } @Override public protobuf.PersistableNetworkPayload toProtoMessage() { throw new UnsupportedOperationException("Stub does not support protobuf"); } @Override public byte[] getHash() { return new byte[] { 1 }; } @Override public boolean verifyHashSize() { return true; } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/storage/mocks/ExpirableProtectedStoragePayloadStub.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.mocks; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.storage.TestState; import haveno.network.p2p.storage.payload.ExpirablePayload; import haveno.network.p2p.storage.payload.RequiresOwnerIsOnlinePayload; import java.security.PublicKey; import java.util.concurrent.TimeUnit; /** * Stub implementation of a ProtectedStoragePayloadStub implementing the ExpirablePayload & RequiresOwnerIsOnlinePayload * marker interfaces that can be used in tests to provide canned answers to calls. Useful if the tests don't care about * the implementation details of the ProtectedStoragePayload. * * @see Reference */ public class ExpirableProtectedStoragePayloadStub extends ProtectedStoragePayloadStub implements ExpirablePayload, RequiresOwnerIsOnlinePayload { private long ttl; public ExpirableProtectedStoragePayloadStub(PublicKey ownerPubKey) { super(ownerPubKey); ttl = TimeUnit.DAYS.toMillis(90); } public ExpirableProtectedStoragePayloadStub(PublicKey ownerPubKey, long ttl) { this(ownerPubKey); this.ttl = ttl; } @Override public NodeAddress getOwnerNodeAddress() { return TestState.getTestNodeAddress(); } @Override public long getTTL() { return this.ttl; } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/storage/mocks/MapStoreServiceFake.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.mocks; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.persistable.PersistableEnvelope; import haveno.common.proto.persistable.PersistablePayload; import haveno.network.p2p.storage.P2PDataStorage; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import haveno.network.p2p.storage.persistence.MapStoreService; import lombok.Getter; import java.io.File; import java.util.HashMap; import java.util.Map; import static org.mockito.Mockito.mock; /** * Implementation of an in-memory MapStoreService that can be used in tests. Removes overhead * involving files, resources, and services for tests that don't need it. * * @see Reference */ public class MapStoreServiceFake extends MapStoreService { @Getter private final Map map; public MapStoreServiceFake() { super(mock(File.class), mock(PersistenceManager.class)); this.map = new HashMap<>(); } @Override public String getFileName() { return null; } @Override protected PersistableEnvelope createStore() { return null; } @Override public boolean canHandle(PersistablePayload payload) { return true; } protected void readFromResourcesSync(String postFix) { // do nothing. This Fake only supports in-memory storage. } @Override protected void initializePersistenceManager() { } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/storage/mocks/MockData.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.mocks; import haveno.network.p2p.storage.payload.ExpirablePayload; import haveno.network.p2p.storage.payload.ProtectedStoragePayload; import org.apache.commons.lang3.NotImplementedException; import javax.annotation.Nullable; import java.security.PublicKey; import java.util.Map; @SuppressWarnings("ALL") public class MockData implements ProtectedStoragePayload, ExpirablePayload { public final String msg; public final PublicKey publicKey; public long ttl; @Nullable private Map extraDataMap; public MockData(String msg, PublicKey publicKey) { this.msg = msg; this.publicKey = publicKey; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof MockData)) return false; MockData that = (MockData) o; return !(msg != null ? !msg.equals(that.msg) : that.msg != null); } @Override public int hashCode() { return msg != null ? msg.hashCode() : 0; } @Override public String toString() { return "MockData{" + "msg='" + msg + '\'' + '}'; } @Nullable @Override public Map getExtraDataMap() { return extraDataMap; } @Override public long getTTL() { return ttl; } @Override public PublicKey getOwnerPubKey() { return publicKey; } @Override public protobuf.ProtectedMailboxStorageEntry toProtoMessage() { throw new NotImplementedException("toProtoMessage not impl."); } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/storage/mocks/PersistableExpirableProtectedStoragePayloadStub.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.mocks; import haveno.common.proto.persistable.PersistablePayload; import java.security.PublicKey; /** * Stub implementation of a ProtectedStoragePayloadStub implementing the ExpirablePayload & RequiresOwnerIsOnlinePayload * & PersistablePayload marker interfaces that can be used in tests to provide canned answers to calls. Useful if the * tests don't care about the implementation details of the ProtectedStoragePayload. * * @see Reference */ public class PersistableExpirableProtectedStoragePayloadStub extends ExpirableProtectedStoragePayloadStub implements PersistablePayload { public PersistableExpirableProtectedStoragePayloadStub(PublicKey ownerPubKey) { super(ownerPubKey); } public PersistableExpirableProtectedStoragePayloadStub(PublicKey ownerPubKey, long ttl) { super(ownerPubKey, ttl); } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/storage/mocks/PersistableNetworkPayloadStub.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.mocks; import haveno.network.p2p.storage.payload.PersistableNetworkPayload; import static org.mockito.Mockito.mock; /** * Stub implementation of a PersistableNetworkPayload that can be used in tests * to provide canned answers to calls. Useful if the tests don't care about the implementation * * details of the PersistableNetworkPayload. * * @see Reference */ public class PersistableNetworkPayloadStub implements PersistableNetworkPayload { private final boolean hashSizeValid; private final byte[] hash; private final protobuf.PersistableNetworkPayload mockPayload; public PersistableNetworkPayloadStub(boolean hashSizeValid) { this(hashSizeValid, new byte[]{1}); } public PersistableNetworkPayloadStub(byte[] hash) { this(true, hash); } private PersistableNetworkPayloadStub(boolean hashSizeValid, byte[] hash) { this.hashSizeValid = hashSizeValid; this.hash = hash; mockPayload = mock(protobuf.PersistableNetworkPayload.class); } @Override public protobuf.PersistableNetworkPayload toProtoMessage() { return mockPayload; } @Override public byte[] getHash() { return hash; } @Override public boolean verifyHashSize() { return this.hashSizeValid; } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/storage/mocks/ProtectedStoragePayloadStub.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.mocks; import com.google.protobuf.Message; import haveno.common.crypto.Sig; import haveno.network.p2p.storage.payload.ProtectedStoragePayload; import lombok.Getter; import javax.annotation.Nullable; import java.security.PublicKey; import java.util.Map; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * Stub implementation of a ProtectedStoragePayload that can be used in tests * to provide canned answers to calls. Useful if the tests don't care about the implementation * details of the ProtectedStoragePayload. * * @see Reference */ public class ProtectedStoragePayloadStub implements ProtectedStoragePayload { @Getter private PublicKey ownerPubKey; protected final Message messageMock; public ProtectedStoragePayloadStub(PublicKey ownerPubKey) { this.ownerPubKey = ownerPubKey; // Need to be able to take the hash which leverages protobuf Messages this.messageMock = mock(protobuf.StoragePayload.class); when(this.messageMock.toByteArray()).thenReturn(Sig.getPublicKeyBytes(ownerPubKey)); } @Nullable @Override public Map getExtraDataMap() { return null; } @Override public Message toProtoMessage() { return this.messageMock; } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/storage/payload/ProtectedMailboxStorageEntryTest.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.payload; import haveno.common.app.Version; import haveno.common.crypto.CryptoException; import haveno.common.crypto.Sig; import haveno.network.p2p.PrefixedSealedAndSignedMessage; import haveno.network.p2p.TestUtils; import haveno.network.p2p.storage.P2PDataStorage; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.security.KeyPair; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.time.Clock; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class ProtectedMailboxStorageEntryTest { private static MailboxStoragePayload buildMailboxStoragePayload(PublicKey payloadSenderPubKeyForAddOperation, PublicKey payloadOwnerPubKey) { // Mock out the PrefixedSealedAndSignedMessage with a version that just serializes to the DEFAULT_INSTANCE // in protobuf. This object is never validated in the test, but needs to be hashed as part of the testing path. PrefixedSealedAndSignedMessage prefixedSealedAndSignedMessageMock = mock(PrefixedSealedAndSignedMessage.class); protobuf.NetworkEnvelope networkEnvelopeMock = mock(protobuf.NetworkEnvelope.class); when(networkEnvelopeMock.getPrefixedSealedAndSignedMessage()).thenReturn( protobuf.PrefixedSealedAndSignedMessage.getDefaultInstance()); when(prefixedSealedAndSignedMessageMock.toProtoNetworkEnvelope()).thenReturn(networkEnvelopeMock); return new MailboxStoragePayload( prefixedSealedAndSignedMessageMock, payloadSenderPubKeyForAddOperation, payloadOwnerPubKey, MailboxStoragePayload.TTL); } private static ProtectedMailboxStorageEntry buildProtectedMailboxStorageEntry( MailboxStoragePayload mailboxStoragePayload, KeyPair ownerKey, PublicKey receiverKey, int sequenceNumber) throws CryptoException { byte[] hashOfDataAndSeqNr = P2PDataStorage.get32ByteHash(new P2PDataStorage.DataAndSeqNrPair(mailboxStoragePayload, sequenceNumber)); byte[] signature = Sig.sign(ownerKey.getPrivate(), hashOfDataAndSeqNr); return new ProtectedMailboxStorageEntry(mailboxStoragePayload, ownerKey.getPublic(), sequenceNumber, signature, receiverKey, Clock.systemDefaultZone()); } @BeforeEach public void SetUp() { // Deep in the bowels of protobuf we grab the messageID from the version module. This is required to hash the // full MailboxStoragePayload so make sure it is initialized. Version.setBaseCryptoNetworkId(1); } // TESTCASE: validForAddOperation() should return true if the Entry owner and sender key specified in payload match @Test public void isValidForAddOperation() throws NoSuchAlgorithmException, CryptoException { KeyPair senderKeys = TestUtils.generateKeyPair(); KeyPair receiverKeys = TestUtils.generateKeyPair(); MailboxStoragePayload mailboxStoragePayload = buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); ProtectedStorageEntry protectedStorageEntry = buildProtectedMailboxStorageEntry(mailboxStoragePayload, senderKeys, receiverKeys.getPublic(), 1); assertTrue(protectedStorageEntry.isValidForAddOperation()); } // TESTCASE: validForAddOperation() should return false if the Entry owner and sender key specified in payload don't match @Test public void isValidForAddOperation_EntryOwnerPayloadReceiverMismatch() throws NoSuchAlgorithmException, CryptoException { KeyPair senderKeys = TestUtils.generateKeyPair(); KeyPair receiverKeys = TestUtils.generateKeyPair(); MailboxStoragePayload mailboxStoragePayload = buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); ProtectedStorageEntry protectedStorageEntry = buildProtectedMailboxStorageEntry(mailboxStoragePayload, receiverKeys, receiverKeys.getPublic(), 1); assertFalse(protectedStorageEntry.isValidForAddOperation()); } // TESTCASE: validForAddOperation() should fail if Entry.receiversPubKey and Payload.ownerPubKey don't match @Test public void isValidForAddOperation_EntryReceiverPayloadReceiverMismatch() throws NoSuchAlgorithmException, CryptoException { KeyPair senderKeys = TestUtils.generateKeyPair(); KeyPair receiverKeys = TestUtils.generateKeyPair(); MailboxStoragePayload mailboxStoragePayload = buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); ProtectedStorageEntry protectedStorageEntry = buildProtectedMailboxStorageEntry(mailboxStoragePayload, senderKeys, senderKeys.getPublic(), 1); assertFalse(protectedStorageEntry.isValidForAddOperation()); } // TESTCASE: validForAddOperation() should fail if the signature isn't valid @Test public void isValidForAddOperation_BadSignature() throws NoSuchAlgorithmException { KeyPair senderKeys = TestUtils.generateKeyPair(); KeyPair receiverKeys = TestUtils.generateKeyPair(); MailboxStoragePayload mailboxStoragePayload = buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); ProtectedStorageEntry protectedStorageEntry = new ProtectedMailboxStorageEntry( mailboxStoragePayload, senderKeys.getPublic(), 1, new byte[] { 0 }, receiverKeys.getPublic(), Clock.systemDefaultZone()); assertFalse(protectedStorageEntry.isValidForAddOperation()); } // TESTCASE: validForRemoveOperation() should return true if the Entry owner and payload owner match @Test public void validForRemove() throws NoSuchAlgorithmException, CryptoException { KeyPair senderKeys = TestUtils.generateKeyPair(); KeyPair receiverKeys = TestUtils.generateKeyPair(); MailboxStoragePayload mailboxStoragePayload = buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); ProtectedStorageEntry protectedStorageEntry = buildProtectedMailboxStorageEntry(mailboxStoragePayload, receiverKeys, receiverKeys.getPublic(), 1); assertTrue(protectedStorageEntry.isValidForRemoveOperation()); } // TESTCASE: validForRemoveOperation() should return false if the Entry owner and payload owner don't match @Test public void validForRemoveEntryOwnerPayloadOwnerMismatch() throws NoSuchAlgorithmException, CryptoException { KeyPair senderKeys = TestUtils.generateKeyPair(); KeyPair receiverKeys = TestUtils.generateKeyPair(); MailboxStoragePayload mailboxStoragePayload = buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); ProtectedStorageEntry protectedStorageEntry = buildProtectedMailboxStorageEntry(mailboxStoragePayload, senderKeys, receiverKeys.getPublic(), 1); assertFalse(protectedStorageEntry.isValidForRemoveOperation()); } // TESTCASE: isValidForRemoveOperation() should fail if the signature is bad @Test public void isValidForRemoveOperation_BadSignature() throws NoSuchAlgorithmException { KeyPair senderKeys = TestUtils.generateKeyPair(); KeyPair receiverKeys = TestUtils.generateKeyPair(); MailboxStoragePayload mailboxStoragePayload = buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); ProtectedStorageEntry protectedStorageEntry = new ProtectedMailboxStorageEntry(mailboxStoragePayload, receiverKeys.getPublic(), 1, new byte[] { 0 }, receiverKeys.getPublic(), Clock.systemDefaultZone()); assertFalse(protectedStorageEntry.isValidForRemoveOperation()); } // TESTCASE: isValidForRemoveOperation() should fail if the receiversPubKey does not match the Entry owner @Test public void isValidForRemoveOperation_ReceiversPubKeyMismatch() throws NoSuchAlgorithmException, CryptoException { KeyPair senderKeys = TestUtils.generateKeyPair(); KeyPair receiverKeys = TestUtils.generateKeyPair(); MailboxStoragePayload mailboxStoragePayload = buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); ProtectedStorageEntry protectedStorageEntry = buildProtectedMailboxStorageEntry(mailboxStoragePayload, receiverKeys, senderKeys.getPublic(), 1); assertFalse(protectedStorageEntry.isValidForRemoveOperation()); } // TESTCASE: isMetadataEquals() should succeed if the sequence number changes @Test public void isMetadataEquals() throws NoSuchAlgorithmException, CryptoException { KeyPair senderKeys = TestUtils.generateKeyPair(); KeyPair receiverKeys = TestUtils.generateKeyPair(); MailboxStoragePayload mailboxStoragePayload = buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); ProtectedStorageEntry seqNrOne = buildProtectedMailboxStorageEntry(mailboxStoragePayload, senderKeys, receiverKeys.getPublic(), 1); ProtectedStorageEntry seqNrTwo = buildProtectedMailboxStorageEntry(mailboxStoragePayload, senderKeys, receiverKeys.getPublic(), 2); assertTrue(seqNrOne.matchesRelevantPubKey(seqNrTwo)); } // TESTCASE: isMetadataEquals() should fail if the receiversPubKey changes @Test public void isMetadataEquals_receiverPubKeyChanged() throws NoSuchAlgorithmException, CryptoException { KeyPair senderKeys = TestUtils.generateKeyPair(); KeyPair receiverKeys = TestUtils.generateKeyPair(); MailboxStoragePayload mailboxStoragePayload = buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); ProtectedStorageEntry seqNrOne = buildProtectedMailboxStorageEntry(mailboxStoragePayload, senderKeys, receiverKeys.getPublic(), 1); ProtectedStorageEntry seqNrTwo = buildProtectedMailboxStorageEntry(mailboxStoragePayload, senderKeys, senderKeys.getPublic(), 1); assertFalse(seqNrOne.matchesRelevantPubKey(seqNrTwo)); } } ================================================ FILE: p2p/src/test/java/haveno/network/p2p/storage/payload/ProtectedStorageEntryTest.java ================================================ /* * 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 . */ package haveno.network.p2p.storage.payload; import haveno.common.app.Version; import haveno.common.crypto.CryptoException; import haveno.common.crypto.Sig; import haveno.network.p2p.PrefixedSealedAndSignedMessage; import haveno.network.p2p.TestUtils; import haveno.network.p2p.storage.P2PDataStorage; import haveno.network.p2p.storage.mocks.ProtectedStoragePayloadStub; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.security.KeyPair; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.time.Clock; import java.time.Duration; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class ProtectedStorageEntryTest { private static ProtectedStorageEntry buildProtectedStorageEntry(KeyPair payloadOwner, KeyPair entryOwner, int sequenceNumber) throws CryptoException { return buildProtectedStorageEntry(new ProtectedStoragePayloadStub(payloadOwner.getPublic()), entryOwner, sequenceNumber); } private static ProtectedStorageEntry buildProtectedStorageEntry(ProtectedStoragePayload protectedStoragePayload, KeyPair entryOwner, int sequenceNumber) throws CryptoException { byte[] hashOfDataAndSeqNr = P2PDataStorage.get32ByteHash(new P2PDataStorage.DataAndSeqNrPair(protectedStoragePayload, sequenceNumber)); byte[] signature = Sig.sign(entryOwner.getPrivate(), hashOfDataAndSeqNr); return new ProtectedStorageEntry(protectedStoragePayload, entryOwner.getPublic(), sequenceNumber, signature, Clock.systemDefaultZone()); } private static MailboxStoragePayload buildMailboxStoragePayload(PublicKey payloadSenderPubKeyForAddOperation, PublicKey payloadOwnerPubKey) { // Mock out the PrefixedSealedAndSignedMessage with a version that just serializes to the DEFAULT_INSTANCE // in protobuf. This object is never validated in the test, but needs to be hashed as part of the testing path. PrefixedSealedAndSignedMessage prefixedSealedAndSignedMessageMock = mock(PrefixedSealedAndSignedMessage.class); protobuf.NetworkEnvelope networkEnvelopeMock = mock(protobuf.NetworkEnvelope.class); when(networkEnvelopeMock.getPrefixedSealedAndSignedMessage()).thenReturn( protobuf.PrefixedSealedAndSignedMessage.getDefaultInstance()); when(prefixedSealedAndSignedMessageMock.toProtoNetworkEnvelope()).thenReturn(networkEnvelopeMock); return new MailboxStoragePayload( prefixedSealedAndSignedMessageMock, payloadSenderPubKeyForAddOperation, payloadOwnerPubKey, MailboxStoragePayload.TTL); } @BeforeEach public void SetUp() { // Deep in the bowels of protobuf we grab the messageID from the version module. This is required to hash the // full MailboxStoragePayload so make sure it is initialized. Version.setBaseCryptoNetworkId(1); } // TESTCASE: validForAddOperation() should return true if the Entry owner and payload owner match @Test public void isValidForAddOperation() throws NoSuchAlgorithmException, CryptoException { KeyPair ownerKeys = TestUtils.generateKeyPair(); ProtectedStorageEntry protectedStorageEntry = buildProtectedStorageEntry(ownerKeys, ownerKeys, 1); assertTrue(protectedStorageEntry.isValidForAddOperation()); } // TESTCASE: validForAddOperation() should return false if the Entry owner and payload owner don't match @Test public void isValidForAddOperation_Mismatch() throws NoSuchAlgorithmException, CryptoException { KeyPair ownerKeys = TestUtils.generateKeyPair(); KeyPair notOwnerKeys = TestUtils.generateKeyPair(); ProtectedStorageEntry protectedStorageEntry = buildProtectedStorageEntry(ownerKeys, notOwnerKeys, 1); assertFalse(protectedStorageEntry.isValidForAddOperation()); } // TESTCASE: validForAddOperation() should fail if the entry is a MailboxStoragePayload wrapped in a // ProtectedStorageEntry and the Entry is owned by the sender // XXXBUGXXX: Currently, a mis-wrapped MailboxStorageEntry will circumvent the senderPubKeyForAddOperation checks @Test public void isValidForAddOperation_invalidMailboxPayloadSender() throws NoSuchAlgorithmException, CryptoException { KeyPair senderKeys = TestUtils.generateKeyPair(); KeyPair receiverKeys = TestUtils.generateKeyPair(); ProtectedStorageEntry protectedStorageEntry = buildProtectedStorageEntry( buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()), senderKeys, 1); // should be assertFalse assertTrue(protectedStorageEntry.isValidForAddOperation()); } // TESTCASE: validForAddOperation() should fail if the entry is a MailboxStoragePayload wrapped in a // ProtectedStorageEntry and the Entry is owned by the receiver @Test public void isValidForAddOperation_invalidMailboxPayloadReceiver() throws NoSuchAlgorithmException, CryptoException { KeyPair senderKeys = TestUtils.generateKeyPair(); KeyPair receiverKeys = TestUtils.generateKeyPair(); ProtectedStorageEntry protectedStorageEntry = buildProtectedStorageEntry( buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()), receiverKeys, 1); assertFalse(protectedStorageEntry.isValidForAddOperation()); } // TESTCASE: validForAddOperation() should fail if the signature isn't valid @Test public void isValidForAddOperation_BadSignature() throws NoSuchAlgorithmException { KeyPair ownerKeys = TestUtils.generateKeyPair(); ProtectedStoragePayload protectedStoragePayload = new ProtectedStoragePayloadStub(ownerKeys.getPublic()); ProtectedStorageEntry protectedStorageEntry = new ProtectedStorageEntry(protectedStoragePayload, ownerKeys.getPublic(), 1, new byte[] { 0 }, Clock.systemDefaultZone()); assertFalse(protectedStorageEntry.isValidForAddOperation()); } // TESTCASE: validForRemoveOperation() should return true if the Entry owner and payload owner match @Test public void isValidForRemoveOperation() throws NoSuchAlgorithmException, CryptoException { KeyPair ownerKeys = TestUtils.generateKeyPair(); ProtectedStorageEntry protectedStorageEntry = buildProtectedStorageEntry(ownerKeys, ownerKeys, 1); assertTrue(protectedStorageEntry.isValidForRemoveOperation()); } // TESTCASE: validForRemoveOperation() should return false if the Entry owner and payload owner don't match @Test public void isValidForRemoveOperation_Mismatch() throws NoSuchAlgorithmException, CryptoException { KeyPair ownerKeys = TestUtils.generateKeyPair(); KeyPair notOwnerKeys = TestUtils.generateKeyPair(); ProtectedStorageEntry protectedStorageEntry = buildProtectedStorageEntry(ownerKeys, notOwnerKeys, 1); assertFalse(protectedStorageEntry.isValidForRemoveOperation()); } // TESTCASE: validForRemoveOperation() should fail if the entry is a MailboxStoragePayload wrapped in a // ProtectedStorageEntry and the Entry is owned by the sender // XXXBUGXXX: Currently, a mis-wrapped MailboxStoragePayload will succeed @Test public void isValidForRemoveOperation_invalidMailboxPayloadSender() throws NoSuchAlgorithmException, CryptoException { KeyPair senderKeys = TestUtils.generateKeyPair(); KeyPair receiverKeys = TestUtils.generateKeyPair(); ProtectedStorageEntry protectedStorageEntry = buildProtectedStorageEntry( buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()), senderKeys, 1); // should be assertFalse assertTrue(protectedStorageEntry.isValidForRemoveOperation()); } @Test public void isValidForRemoveOperation_invalidMailboxPayloadReceiver() throws NoSuchAlgorithmException, CryptoException { KeyPair senderKeys = TestUtils.generateKeyPair(); KeyPair receiverKeys = TestUtils.generateKeyPair(); ProtectedStorageEntry protectedStorageEntry = buildProtectedStorageEntry( buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()), receiverKeys, 1); assertFalse(protectedStorageEntry.isValidForRemoveOperation()); } // TESTCASE: isValidForRemoveOperation() should fail if the signature is bad @Test public void isValidForRemoveOperation_BadSignature() throws NoSuchAlgorithmException { KeyPair ownerKeys = TestUtils.generateKeyPair(); ProtectedStoragePayload protectedStoragePayload = new ProtectedStoragePayloadStub(ownerKeys.getPublic()); ProtectedStorageEntry protectedStorageEntry = new ProtectedStorageEntry(protectedStoragePayload, ownerKeys.getPublic(), 1, new byte[] { 0 }, Clock.systemDefaultZone()); assertFalse(protectedStorageEntry.isValidForRemoveOperation()); } // TESTCASE: isMetadataEquals() should succeed if the sequence number changes @Test public void isMetadataEquals() throws NoSuchAlgorithmException, CryptoException { KeyPair ownerKeys = TestUtils.generateKeyPair(); ProtectedStorageEntry seqNrOne = buildProtectedStorageEntry(ownerKeys, ownerKeys, 1); ProtectedStorageEntry seqNrTwo = buildProtectedStorageEntry(ownerKeys, ownerKeys, 2); assertTrue(seqNrOne.matchesRelevantPubKey(seqNrTwo)); } // TESTCASE: isMetadataEquals() should fail if the OwnerPubKey changes @Test public void isMetadataEquals_OwnerPubKeyChanged() throws NoSuchAlgorithmException, CryptoException { KeyPair ownerKeys = TestUtils.generateKeyPair(); KeyPair notOwner = TestUtils.generateKeyPair(); ProtectedStorageEntry protectedStorageEntryOne = buildProtectedStorageEntry(ownerKeys, ownerKeys, 1); ProtectedStorageEntry protectedStorageEntryTwo = buildProtectedStorageEntry(ownerKeys, notOwner, 1); assertFalse(protectedStorageEntryOne.matchesRelevantPubKey(protectedStorageEntryTwo)); } // TESTCASE: Payload implementing ProtectedStoragePayload & PersistableNetworkPayload is invalid // We rely on the fact that a payload is either a ProtectedStoragePayload OR PersistableNetworkPayload, but Java // does not have a clean way to specify mutually exclusive interfaces. // // We also want to guarantee that ONLY ProtectedStoragePayload objects are valid as payloads in // ProtectedStorageEntrys. This test will give a defense in case future development work breaks that expectation. @Test public void protectedStoragePayloadPersistableNetworkPayloadIncompatible() throws NoSuchAlgorithmException { class IncompatiblePayload extends ProtectedStoragePayloadStub implements PersistableNetworkPayload { private IncompatiblePayload(PublicKey ownerPubKey) { super(ownerPubKey); } @Override public byte[] getHash() { return new byte[0]; } @Override public boolean verifyHashSize() { return true; } @Override public protobuf.PersistableNetworkPayload toProtoMessage() { return (protobuf.PersistableNetworkPayload) this.messageMock; } } KeyPair ownerKeys = TestUtils.generateKeyPair(); IncompatiblePayload incompatiblePayload = new IncompatiblePayload(ownerKeys.getPublic()); assertThrows(IllegalArgumentException.class, () -> new ProtectedStorageEntry(incompatiblePayload,ownerKeys.getPublic(), 1, new byte[] { 0 }, Clock.systemDefaultZone()) ); } // TESTCASE: PSEs received with future-dated timestamps are updated to be min(currentTime, creationTimeStamp) @Test public void futureTimestampIsSanitized() throws NoSuchAlgorithmException { KeyPair ownerKeys = TestUtils.generateKeyPair(); Clock baseClock = Clock.systemDefaultZone(); Clock futureClock = Clock.offset(baseClock, Duration.ofDays(1)); ProtectedStoragePayload protectedStoragePayload = new ProtectedStoragePayloadStub(ownerKeys.getPublic()); ProtectedStorageEntry protectedStorageEntry = new ProtectedStorageEntry(protectedStoragePayload, Sig.getPublicKeyBytes(ownerKeys.getPublic()), ownerKeys.getPublic(), 1, new byte[] { 0 }, futureClock.millis(), baseClock); assertTrue(protectedStorageEntry.getCreationTimeStamp() <= baseClock.millis()); } } ================================================ FILE: p2p/src/test/java/haveno/network/utils/UtilsTest.java ================================================ /* * 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 . */ package haveno.network.utils; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class UtilsTest { @Test public void checkV2Address() { assertFalse(Utils.isV3Address("xmh57jrzrnw6insl.onion")); } @Test public void checkV3Address() { assertTrue(Utils.isV3Address("vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd.onion")); } } ================================================ FILE: p2p/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker ================================================ mock-maker-inline # allow mocking of final classes in tests ================================================ FILE: proto/src/main/proto/grpc.proto ================================================ /* * 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 . */ /* * This file is part of Haveno. * * Haveno 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. * * Haveno 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 Haveno. If not, see . */ syntax = "proto3"; package io.haveno.protobuffer; import "pb.proto"; option java_package = "haveno.proto.grpc"; option java_multiple_files = true; /////////////////////////////////////////////////////////////////////////////////////////// // Help /////////////////////////////////////////////////////////////////////////////////////////// service Help { rpc GetMethodHelp (GetMethodHelpRequest) returns (GetMethodHelpReply) { } } message GetMethodHelpRequest { string method_name = 1; } message GetMethodHelpReply { string method_help = 1; } /////////////////////////////////////////////////////////////////////////////////////////// // Version /////////////////////////////////////////////////////////////////////////////////////////// service GetVersion { rpc GetVersion (GetVersionRequest) returns (GetVersionReply) { } } message GetVersionRequest { } message GetVersionReply { string version = 1; } /////////////////////////////////////////////////////////////////////////////////////////// // Account /////////////////////////////////////////////////////////////////////////////////////////// service Account { rpc AccountExists (AccountExistsRequest) returns (AccountExistsReply) { } rpc IsAccountOpen (IsAccountOpenRequest) returns (IsAccountOpenReply) { } rpc CreateAccount (CreateAccountRequest) returns (CreateAccountReply) { } rpc OpenAccount (OpenAccountRequest) returns (OpenAccountReply) { } rpc IsAppInitialized (IsAppInitializedRequest) returns (IsAppInitializedReply) { } rpc ChangePassword (ChangePasswordRequest) returns (ChangePasswordReply) { } rpc CloseAccount (CloseAccountRequest) returns (CloseAccountReply) { } rpc DeleteAccount (DeleteAccountRequest) returns (DeleteAccountReply) { } rpc BackupAccount (BackupAccountRequest) returns (stream BackupAccountReply) { } rpc RestoreAccount (RestoreAccountRequest) returns (RestoreAccountReply) { } } message AccountExistsRequest { } message AccountExistsReply { bool account_exists = 1; } message IsAccountOpenRequest { } message IsAccountOpenReply { bool is_account_open = 1; } message CreateAccountRequest { string password = 1; } message CreateAccountReply { } message OpenAccountRequest { string password = 1; } message OpenAccountReply { } message IsAppInitializedRequest { } message IsAppInitializedReply { bool is_app_initialized = 1; } message ChangePasswordRequest { string old_password = 1; string new_password = 2; } message ChangePasswordReply { } message CloseAccountRequest { } message CloseAccountReply { } message DeleteAccountRequest { } message DeleteAccountReply { } message BackupAccountRequest { } message BackupAccountReply { bytes zip_bytes = 1; } message RestoreAccountRequest { bytes zip_bytes = 1; uint64 offset = 2; uint64 total_length = 3; bool has_more = 4; } message RestoreAccountReply { } /////////////////////////////////////////////////////////////////////////////////////////// // Disputes /////////////////////////////////////////////////////////////////////////////////////////// service Disputes { rpc GetDispute (GetDisputeRequest) returns (GetDisputeReply) { } rpc GetDisputes (GetDisputesRequest) returns (GetDisputesReply) { } rpc OpenDispute (OpenDisputeRequest) returns (OpenDisputeReply) { } rpc ResolveDispute (ResolveDisputeRequest) returns (ResolveDisputeReply) { } rpc SendDisputeChatMessage (SendDisputeChatMessageRequest) returns (SendDisputeChatMessageReply) { } } message GetDisputesRequest { } message GetDisputesReply { repeated Dispute disputes = 1; // pb.proto } message GetDisputeRequest { string trade_id = 1; } message GetDisputeReply { Dispute dispute = 1; // pb.proto } message OpenDisputeRequest { string trade_id = 1; } message OpenDisputeReply { } message ResolveDisputeReply { } message ResolveDisputeRequest { string trade_id = 1; DisputeResult.Winner winner = 2; DisputeResult.Reason reason = 3; string summary_notes = 4; uint64 custom_payout_amount = 5 [jstype = JS_STRING]; } message SendDisputeChatMessageRequest { string dispute_id = 1; string message = 2; repeated Attachment attachments = 3; // pb.proto } message SendDisputeChatMessageReply { } /////////////////////////////////////////////////////////////////////////////////////////// // DisputeAgents /////////////////////////////////////////////////////////////////////////////////////////// service DisputeAgents { rpc RegisterDisputeAgent (RegisterDisputeAgentRequest) returns (RegisterDisputeAgentReply) { } rpc UnregisterDisputeAgent (UnregisterDisputeAgentRequest) returns (UnregisterDisputeAgentReply) { } } message RegisterDisputeAgentRequest { string dispute_agent_type = 1; string registration_key = 2; } message RegisterDisputeAgentReply { } message UnregisterDisputeAgentRequest { string dispute_agent_type = 1; } message UnregisterDisputeAgentReply { } /////////////////////////////////////////////////////////////////////////////////////////// // Notifications /////////////////////////////////////////////////////////////////////////////////////////// service Notifications { rpc RegisterNotificationListener (RegisterNotificationListenerRequest) returns (stream NotificationMessage) { } rpc SendNotification (SendNotificationRequest) returns (SendNotificationReply) { // only used for testing } } message RegisterNotificationListenerRequest { } message NotificationMessage { enum NotificationType { ERROR = 0; APP_INITIALIZED = 1; KEEP_ALIVE = 2; TRADE_UPDATE = 3; CHAT_MESSAGE = 4; } string id = 1; NotificationType type = 2; int64 timestamp = 3; string title = 4; string message = 5; TradeInfo trade = 6; ChatMessage chat_message = 7; } message SendNotificationRequest { NotificationMessage notification = 1; } message SendNotificationReply { } /////////////////////////////////////////////////////////////////////////////////////////// // XmrConnections /////////////////////////////////////////////////////////////////////////////////////////// service XmrConnections { rpc AddConnection (AddConnectionRequest) returns (AddConnectionReply) { } rpc RemoveConnection(RemoveConnectionRequest) returns (RemoveConnectionReply) { } rpc GetConnection(GetConnectionRequest) returns (GetConnectionReply) { } rpc GetConnections(GetConnectionsRequest) returns (GetConnectionsReply) { } rpc SetConnection(SetConnectionRequest) returns (SetConnectionReply) { } rpc CheckConnection(CheckConnectionRequest) returns (CheckConnectionReply) { } rpc GetBestConnection(GetBestConnectionRequest) returns (GetBestConnectionReply) { } rpc SetAutoSwitch(SetAutoSwitchRequest) returns (SetAutoSwitchReply) { } rpc GetAutoSwitch(GetAutoSwitchRequest) returns (GetAutoSwitchReply) { } } message UrlConnection { enum OnlineStatus { UNKNOWN = 0; ONLINE = 1; OFFLINE = 2; } enum AuthenticationStatus { NO_AUTHENTICATION = 0; AUTHENTICATED = 1; NOT_AUTHENTICATED = 2; } string url = 1; string username = 2; // request only string password = 3; // request only int32 priority = 4; OnlineStatus online_status = 5; // reply only AuthenticationStatus authentication_status = 6; // reply only } message AddConnectionRequest { UrlConnection connection = 1; } message AddConnectionReply {} message RemoveConnectionRequest { string url = 1; } message RemoveConnectionReply {} message GetConnectionRequest {} message GetConnectionReply { UrlConnection connection = 1; } message GetConnectionsRequest {} message GetConnectionsReply { repeated UrlConnection connections = 1; } message SetConnectionRequest { string url = 1; UrlConnection connection = 2; } message SetConnectionReply {} message CheckConnectionRequest {} message CheckConnectionReply { UrlConnection connection = 1; } message GetBestConnectionRequest {} message GetBestConnectionReply { UrlConnection connection = 1; } message SetAutoSwitchRequest { bool auto_switch = 1; } message SetAutoSwitchReply {} message GetAutoSwitchRequest {} message GetAutoSwitchReply { bool auto_switch = 1; } /////////////////////////////////////////////////////////////////////////////////////////// // XmrNode /////////////////////////////////////////////////////////////////////////////////////////// service XmrNode { rpc IsXmrNodeOnline (IsXmrNodeOnlineRequest) returns (IsXmrNodeOnlineReply) { } rpc GetXmrNodeSettings (GetXmrNodeSettingsRequest) returns (GetXmrNodeSettingsReply) { } rpc StartXmrNode (StartXmrNodeRequest) returns (StartXmrNodeReply) { } rpc StopXmrNode (StopXmrNodeRequest) returns (StopXmrNodeReply) { } } message IsXmrNodeOnlineRequest { } message IsXmrNodeOnlineReply { bool is_running = 1; } message GetXmrNodeSettingsRequest { } message GetXmrNodeSettingsReply { XmrNodeSettings settings = 1; // pb.proto } message StartXmrNodeRequest { XmrNodeSettings settings = 1; } message StartXmrNodeReply { } message StopXmrNodeRequest { } message StopXmrNodeReply { } /////////////////////////////////////////////////////////////////////////////////////////// // Offers /////////////////////////////////////////////////////////////////////////////////////////// service Offers { rpc GetOffer (GetOfferRequest) returns (GetOfferReply) { } rpc GetMyOffer (GetMyOfferRequest) returns (GetMyOfferReply) { } rpc GetOffers (GetOffersRequest) returns (GetOffersReply) { } rpc GetMyOffers (GetMyOffersRequest) returns (GetMyOffersReply) { } rpc PostOffer (PostOfferRequest) returns (PostOfferReply) { } rpc EditOffer (EditOfferRequest) returns (EditOfferReply) { } rpc DeactivateOffer (DeactivateOfferRequest) returns (DeactivateOfferReply) { } rpc ActivateOffer (ActivateOfferRequest) returns (ActivateOfferReply) { } rpc CancelOffer (CancelOfferRequest) returns (CancelOfferReply) { } } message GetOfferRequest { string id = 1; } message GetOfferReply { OfferInfo offer = 1; } message GetMyOfferRequest { string id = 1; } message GetMyOfferReply { OfferInfo offer = 1; } message GetOffersRequest { string direction = 1; string currency_code = 2; } message GetOffersReply { repeated OfferInfo offers = 1; } message GetMyOffersRequest { string direction = 1; string currency_code = 2; } message GetMyOffersReply { repeated OfferInfo offers = 1; } message PostOfferRequest { string currency_code = 1; string direction = 2; string price = 3; bool use_market_based_price = 4; double market_price_margin_pct = 5; uint64 amount = 6 [jstype = JS_STRING]; uint64 min_amount = 7 [jstype = JS_STRING]; double security_deposit_pct = 8; string trigger_price = 9; bool reserve_exact_amount = 10; string payment_account_id = 11; bool is_private_offer = 12; bool buyer_as_taker_without_deposit = 13; string extra_info = 14; string source_offer_id = 15; } message PostOfferReply { OfferInfo offer = 1; } message EditOfferRequest { string offer_id = 1; string currency_code = 2; string price = 3; bool use_market_based_price = 4; double market_price_margin_pct = 5; string trigger_price = 6; string payment_account_id = 7; string extra_info = 8; } message EditOfferReply { OfferInfo offer = 1; } message DeactivateOfferRequest { string offer_id = 1; } message DeactivateOfferReply { } message ActivateOfferRequest { string offer_id = 1; } message ActivateOfferReply { } message CancelOfferRequest { string id = 1; } message CancelOfferReply { } message OfferInfo { string id = 1; string direction = 2; string price = 3; bool use_market_based_price = 4; double market_price_margin_pct = 5; uint64 amount = 6 [jstype = JS_STRING]; uint64 min_amount = 7 [jstype = JS_STRING]; double maker_fee_pct = 8; double taker_fee_pct = 9; double penalty_fee_pct = 10; double buyer_security_deposit_pct = 11; double seller_security_deposit_pct = 12; string volume = 13; string min_volume = 14; string trigger_price = 15; string payment_account_id = 16; string payment_method_id = 17; string payment_method_short_name = 18; string base_currency_code = 19; string counter_currency_code = 20; uint64 date = 21; string state = 22; bool is_activated = 23; bool is_my_offer = 24; string owner_node_address = 25; string pub_key_ring = 26; string version_nr = 27; int32 protocol_version = 28; string arbitrator_signer = 29; string split_output_tx_hash = 30; uint64 split_output_tx_fee = 31 [jstype = JS_STRING]; bool is_private_offer = 32; string challenge = 33; string extra_info = 34; repeated string accepted_country_codes = 35; string accepted_countries_string = 36; string city = 37; } message AvailabilityResultWithDescription { AvailabilityResult availability_result = 1; string description = 2; } /////////////////////////////////////////////////////////////////////////////////////////// // PaymentAccounts /////////////////////////////////////////////////////////////////////////////////////////// service PaymentAccounts { rpc CreatePaymentAccount (CreatePaymentAccountRequest) returns (CreatePaymentAccountReply) { } rpc GetPaymentAccounts (GetPaymentAccountsRequest) returns (GetPaymentAccountsReply) { } rpc GetPaymentMethods (GetPaymentMethodsRequest) returns (GetPaymentMethodsReply) { } rpc GetPaymentAccountForm (GetPaymentAccountFormRequest) returns (GetPaymentAccountFormReply) { } rpc GetPaymentAccountFormAsJson (GetPaymentAccountFormAsJsonRequest) returns (GetPaymentAccountFormAsJsonReply) { } rpc CreateCryptoCurrencyPaymentAccount (CreateCryptoCurrencyPaymentAccountRequest) returns (CreateCryptoCurrencyPaymentAccountReply) { } rpc DeletePaymentAccount (DeletePaymentAccountRequest) returns (DeletePaymentAccountReply) { } rpc GetCryptoCurrencyPaymentMethods (GetCryptoCurrencyPaymentMethodsRequest) returns (GetCryptoCurrencyPaymentMethodsReply) { } rpc ValidateFormField (ValidateFormFieldRequest) returns (ValidateFormFieldReply) { } } message CreatePaymentAccountRequest { PaymentAccountForm payment_account_form = 1; string payment_account_form_as_json = 2; } message CreatePaymentAccountReply { PaymentAccount payment_account = 1; } message GetPaymentAccountsRequest { } message GetPaymentAccountsReply { repeated PaymentAccount payment_accounts = 1; } message GetPaymentMethodsRequest { } message GetPaymentMethodsReply { repeated PaymentMethod payment_methods = 1; } message GetPaymentAccountFormRequest { string payment_method_id = 1; PaymentAccountPayload payment_account_payload = 2; } message GetPaymentAccountFormReply { PaymentAccountForm payment_account_form = 1; } message GetPaymentAccountFormAsJsonRequest { string payment_method_id = 1; } message GetPaymentAccountFormAsJsonReply { string payment_account_form_as_json = 1; } message CreateCryptoCurrencyPaymentAccountRequest { string account_name = 1; string currency_code = 2; string address = 3; bool trade_instant = 4; } message DeletePaymentAccountRequest { string payment_account_id = 1; } message DeletePaymentAccountReply { } message CreateCryptoCurrencyPaymentAccountReply { PaymentAccount payment_account = 1; } message GetCryptoCurrencyPaymentMethodsRequest { } message GetCryptoCurrencyPaymentMethodsReply { repeated PaymentMethod payment_methods = 1; } message ValidateFormFieldRequest { PaymentAccountForm form = 1; PaymentAccountFormField.FieldId field_id = 2; string value = 3; } message ValidateFormFieldReply { } /////////////////////////////////////////////////////////////////////////////////////////// // Price /////////////////////////////////////////////////////////////////////////////////////////// service Price { rpc GetMarketPrice (MarketPriceRequest) returns (MarketPriceReply) { } rpc GetMarketPrices (MarketPricesRequest) returns (MarketPricesReply) { } rpc GetMarketDepth (MarketDepthRequest) returns (MarketDepthReply) { } } message MarketPriceRequest { string currency_code = 1; } message MarketPriceReply { double price = 1; } message MarketPricesRequest { } message MarketPricesReply { repeated MarketPriceInfo market_price = 1; } message MarketPriceInfo { string currency_code = 1; double price = 2; } message MarketDepthRequest { string currency_code = 1; } message MarketDepthReply { MarketDepthInfo market_depth = 1; } message MarketDepthInfo { string currency_code = 1; repeated double buy_prices = 2; repeated double buy_depth = 3; repeated double sell_prices = 4; repeated double sell_depth = 5; } /////////////////////////////////////////////////////////////////////////////////////////// // GetTradeStatistics /////////////////////////////////////////////////////////////////////////////////////////// service GetTradeStatistics { rpc GetTradeStatistics (GetTradeStatisticsRequest) returns (GetTradeStatisticsReply) { } } message GetTradeStatisticsRequest { } message GetTradeStatisticsReply { repeated TradeStatistics3 trade_statistics = 1; } /////////////////////////////////////////////////////////////////////////////////////////// // Shutdown /////////////////////////////////////////////////////////////////////////////////////////// service ShutdownServer { rpc Stop (StopRequest) returns (StopReply) { } } message StopRequest { } message StopReply { } /////////////////////////////////////////////////////////////////////////////////////////// // Trades /////////////////////////////////////////////////////////////////////////////////////////// service Trades { rpc GetTrade (GetTradeRequest) returns (GetTradeReply) { } rpc GetTrades (GetTradesRequest) returns (GetTradesReply) { } rpc TakeOffer (TakeOfferRequest) returns (TakeOfferReply) { } rpc ConfirmPaymentSent (ConfirmPaymentSentRequest) returns (ConfirmPaymentSentReply) { } rpc ConfirmPaymentReceived (ConfirmPaymentReceivedRequest) returns (ConfirmPaymentReceivedReply) { } rpc CompleteTrade (CompleteTradeRequest) returns (CompleteTradeReply) { } rpc WithdrawFunds (WithdrawFundsRequest) returns (WithdrawFundsReply) { } rpc GetChatMessages (GetChatMessagesRequest) returns (GetChatMessagesReply) { } rpc SendChatMessage (SendChatMessageRequest) returns (SendChatMessageReply) { } } message TakeOfferRequest { string offer_id = 1; string payment_account_id = 2; uint64 amount = 3 [jstype = JS_STRING]; string challenge = 4; } message TakeOfferReply { TradeInfo trade = 1; AvailabilityResultWithDescription failure_reason = 2; } message ConfirmPaymentSentRequest { string trade_id = 1; } message ConfirmPaymentSentReply { } message ConfirmPaymentReceivedRequest { string trade_id = 1; } message ConfirmPaymentReceivedReply { } message GetTradeRequest { string trade_id = 1; } message GetTradeReply { TradeInfo trade = 1; } message GetTradesRequest { // Rpc method GetTrades parameter determining what category of trade list is is being requested. enum Category { OPEN = 0; // Get all currently open trades. CLOSED = 1; // Get all completed trades. FAILED = 2; // Get all failed trades. } Category category = 1; } message GetTradesReply { repeated TradeInfo trades = 1; } message CompleteTradeRequest { string trade_id = 1; } message CompleteTradeReply { } message WithdrawFundsRequest { string trade_id = 1; string address = 2; string memo = 3; } message WithdrawFundsReply { } message GetChatMessagesRequest { string trade_id = 1; } message GetChatMessagesReply { repeated ChatMessage message = 1; } message SendChatMessageRequest { string trade_id = 1; string message = 2; } message SendChatMessageReply { } message TradeInfo { OfferInfo offer = 1; string trade_id = 2; string short_id = 3; uint64 date = 4; string role = 5; uint64 amount = 6 [jstype = JS_STRING]; uint64 maker_fee = 7 [jstype = JS_STRING]; uint64 taker_fee = 8 [jstype = JS_STRING]; uint64 buyer_security_deposit = 9 [jstype = JS_STRING]; uint64 seller_security_deposit = 10 [jstype = JS_STRING]; uint64 buyer_deposit_tx_fee = 11 [jstype = JS_STRING]; uint64 seller_deposit_tx_fee = 12 [jstype = JS_STRING]; uint64 buyer_payout_tx_fee = 13 [jstype = JS_STRING]; uint64 seller_payout_tx_fee = 14 [jstype = JS_STRING]; uint64 buyer_payout_amount = 15 [jstype = JS_STRING]; uint64 seller_payout_amount = 16 [jstype = JS_STRING]; string price = 17; string arbitrator_node_address = 18; string trade_peer_node_address = 19; string state = 20; string phase = 21; string period_state = 22; string payout_state = 23; string dispute_state = 24; bool is_deposits_published = 25; bool is_deposits_confirmed = 26; bool is_deposits_unlocked = 27; bool is_deposits_finalized = 43; bool is_payment_sent = 28; bool is_payment_received = 29; bool is_payout_published = 30; bool is_payout_confirmed = 31; bool is_payout_unlocked = 32; bool is_payout_finalized = 44; bool is_completed = 33; string contract_as_json = 34; ContractInfo contract = 35; string trade_volume = 36; string maker_deposit_tx_id = 37; string taker_deposit_tx_id = 38; string payout_tx_id = 39; uint64 start_time = 40; uint64 max_duration_ms = 41; uint64 deadline_time = 42; } message ContractInfo { string buyer_node_address = 1; string seller_node_address = 2; string arbitrator_node_address = 3; reserved 4; // was mediator_node_address reserved 5; // was refund_agent_node_address bool is_buyer_maker_and_seller_taker = 6; string maker_account_id = 7; string taker_account_id = 8; PaymentAccountPayload maker_payment_account_payload = 9; PaymentAccountPayload taker_payment_account_payload = 10; string maker_payout_address_string = 11; string taker_payout_address_string = 12; uint64 lock_time = 13; } /////////////////////////////////////////////////////////////////////////////////////////// // Wallets /////////////////////////////////////////////////////////////////////////////////////////// service Wallets { rpc GetBalances (GetBalancesRequest) returns (GetBalancesReply) { } rpc GetXmrSeed (GetXmrSeedRequest) returns (GetXmrSeedReply) { } rpc GetXmrPrimaryAddress (GetXmrPrimaryAddressRequest) returns (GetXmrPrimaryAddressReply) { } rpc GetXmrNewSubaddress (GetXmrNewSubaddressRequest) returns (GetXmrNewSubaddressReply) { } rpc GetXmrTxs (GetXmrTxsRequest) returns (GetXmrTxsReply) { } rpc CreateXmrTx (CreateXmrTxRequest) returns (CreateXmrTxReply) { } rpc CreateXmrSweepTxs (CreateXmrSweepTxsRequest) returns (CreateXmrSweepTxsReply) { } rpc RelayXmrTxs (RelayXmrTxsRequest) returns (RelayXmrTxsReply) { } rpc GetAddressBalance (GetAddressBalanceRequest) returns (GetAddressBalanceReply) { } rpc GetFundingAddresses (GetFundingAddressesRequest) returns (GetFundingAddressesReply) { } rpc SetWalletPassword (SetWalletPasswordRequest) returns (SetWalletPasswordReply) { } rpc RemoveWalletPassword (RemoveWalletPasswordRequest) returns (RemoveWalletPasswordReply) { } rpc LockWallet (LockWalletRequest) returns (LockWalletReply) { } rpc UnlockWallet (UnlockWalletRequest) returns (UnlockWalletReply) { } rpc GetHeight (GetWalletHeightRequest) returns (GetWalletHeightReply) { } } message GetBalancesRequest { string currency_code = 1; } message GetBalancesReply { BalancesInfo balances = 1; } message GetXmrSeedRequest { } message GetXmrSeedReply { string seed = 1; } message GetXmrPrimaryAddressRequest { } message GetXmrPrimaryAddressReply { string primary_address = 1; } message GetXmrNewSubaddressRequest { } message GetXmrNewSubaddressReply { string subaddress = 1; } message GetXmrTxsRequest { } message GetXmrTxsReply { repeated XmrTx txs = 1; } message XmrTx { string hash = 1; string fee = 2; bool is_confirmed = 3; bool is_locked = 4; uint64 height = 5; uint64 timestamp = 6; repeated XmrIncomingTransfer incoming_transfers = 7; XmrOutgoingTransfer outgoing_transfer = 8; string metadata = 9; } message XmrDestination { string address = 1; string amount = 2; } message XmrIncomingTransfer { string amount = 1; int32 account_index = 2; int32 subaddress_index = 3; string address = 4; uint64 num_suggested_confirmations = 5; } message XmrOutgoingTransfer { string amount = 1; int32 account_index = 2; repeated int32 subaddress_indices = 3; repeated XmrDestination destinations = 4; } message CreateXmrTxRequest { repeated XmrDestination destinations = 1; } message CreateXmrTxReply { XmrTx tx = 1; } message CreateXmrSweepTxsRequest { string address = 1; } message CreateXmrSweepTxsReply { repeated XmrTx txs = 1; } message RelayXmrTxsRequest { repeated string metadatas = 1; } message RelayXmrTxsReply { repeated string hashes = 2; } message GetAddressBalanceRequest { string address = 1; } message GetAddressBalanceReply { AddressBalanceInfo address_balance_info = 1; } message SendBtcRequest { string address = 1; string amount = 2; string tx_fee_rate = 3; string memo = 4; } message GetFundingAddressesRequest { } message GetFundingAddressesReply { repeated AddressBalanceInfo address_balance_info = 1; } message SetWalletPasswordRequest { string password = 1; string new_password = 2; } message SetWalletPasswordReply { } message RemoveWalletPasswordRequest { string password = 1; } message RemoveWalletPasswordReply { } message LockWalletRequest { } message LockWalletReply { } message UnlockWalletRequest { string password = 1; uint64 timeout = 2; } message UnlockWalletReply { } message BalancesInfo { // Field names are shortened for readability's sake, i.e., // balancesInfo.getBtc().getAvailableBalance() is cleaner than // balancesInfo.getBtcBalanceInfo().getAvailableBalance(). BtcBalanceInfo btc = 1; XmrBalanceInfo xmr = 2; } message BtcBalanceInfo { uint64 available_balance = 1; uint64 reserved_balance = 2; uint64 total_available_balance = 3; uint64 locked_balance = 4; } message XmrBalanceInfo { uint64 balance = 1 [jstype = JS_STRING]; uint64 available_balance = 2 [jstype = JS_STRING]; uint64 pending_balance = 3 [jstype = JS_STRING]; uint64 reserved_offer_balance = 4 [jstype = JS_STRING]; uint64 reserved_trade_balance = 5 [jstype = JS_STRING]; } message AddressBalanceInfo { string address = 1; int64 balance = 2; int64 num_confirmations = 3; bool is_address_unused = 4; } message GetWalletHeightRequest { } message GetWalletHeightReply { int64 height = 1; int64 target_height = 2; } ================================================ FILE: proto/src/main/proto/pb.proto ================================================ syntax = "proto3"; package io.haveno.protobuffer; // // Protobuffer v3 definitions of network messages and persisted objects. // option java_package = "protobuf"; option java_multiple_files = true; /////////////////////////////////////////////////////////////////////////////////////////// // Network messages /////////////////////////////////////////////////////////////////////////////////////////// // Those are messages sent over wire message NetworkEnvelope { string message_version = 1; oneof message { PreliminaryGetDataRequest preliminary_get_data_request = 2; GetDataResponse get_data_response = 3; GetUpdatedDataRequest get_updated_data_request = 4; GetPeersRequest get_peers_request = 5; GetPeersResponse get_peers_response = 6; Ping ping = 7; Pong pong = 8; OfferAvailabilityRequest offer_availability_request = 9; OfferAvailabilityResponse offer_availability_response = 10; RefreshOfferMessage refresh_offer_message = 11; AddDataMessage add_data_message = 12; RemoveDataMessage remove_data_message = 13; RemoveMailboxDataMessage remove_mailbox_data_message = 14; CloseConnectionMessage close_connection_message = 15; PrefixedSealedAndSignedMessage prefixed_sealed_and_signed_message = 16; PrivateNotificationMessage private_notification_message = 17; AddPersistableNetworkPayloadMessage add_persistable_network_payload_message = 18; AckMessage ack_message = 19; BundleOfEnvelopes bundle_of_envelopes = 20; GetInventoryRequest get_inventory_request = 21; GetInventoryResponse get_inventory_response = 22; SignOfferRequest sign_offer_request = 23; SignOfferResponse sign_offer_response = 24; InitTradeRequest init_trade_request = 25; InitMultisigRequest init_multisig_request = 26; SignContractRequest sign_contract_request = 27; SignContractResponse sign_contract_response = 28; DepositRequest deposit_request = 29; DepositResponse deposit_response = 30; DepositsConfirmedMessage deposits_confirmed_message = 31; PaymentSentMessage payment_sent_message = 32; PaymentReceivedMessage payment_received_message = 33; DisputeOpenedMessage dispute_opened_message = 34; DisputeClosedMessage dispute_closed_message = 35; ChatMessage chat_message = 36; MediatedPayoutTxSignatureMessage mediated_payout_tx_signature_message = 37; MediatedPayoutTxPublishedMessage mediated_payout_tx_published_message = 38; FileTransferPart file_transfer_part = 39; } } /////////////////////////////////////////////////////////////////////////////////////////// // Implementations of NetworkEnvelope /////////////////////////////////////////////////////////////////////////////////////////// message BundleOfEnvelopes { repeated NetworkEnvelope envelopes = 1; } // get data message PreliminaryGetDataRequest { int32 nonce = 21; // This was set to 21 instead of 1 in some old commit so we cannot change it. repeated bytes excluded_keys = 2; repeated int32 supported_capabilities = 3; string version = 4; } message GetDataResponse { int32 request_nonce = 1; bool is_get_updated_data_response = 2; repeated StorageEntryWrapper data_set = 3; repeated int32 supported_capabilities = 4; repeated PersistableNetworkPayload persistable_network_payload_items = 5; bool was_truncated = 6; } message GetUpdatedDataRequest { NodeAddress sender_node_address = 1; int32 nonce = 2; repeated bytes excluded_keys = 3; string version = 4; } message FileTransferPart { NodeAddress sender_node_address = 1; string uid = 2; string trade_id = 3; int32 trader_id = 4; int64 seq_num_or_file_length = 5; bytes message_data = 6; } message GetPeersRequest { NodeAddress sender_node_address = 1; int32 nonce = 2; repeated int32 supported_capabilities = 3; repeated Peer reported_peers = 4; } message GetPeersResponse { int32 request_nonce = 1; repeated Peer reported_peers = 2; repeated int32 supported_capabilities = 3; } message Ping { int32 nonce = 1; int32 last_round_trip_time = 2; } message Pong { int32 request_nonce = 1; } message GetInventoryRequest { string version = 1; } message GetInventoryResponse { map inventory = 1; } message SignOfferRequest { string offer_id = 1; NodeAddress sender_node_address = 2; PubKeyRing pub_key_ring = 3; string sender_account_id = 4; OfferPayload offer_payload = 5; string uid = 6; int64 current_date = 7; string reserve_tx_hash = 8; string reserve_tx_hex = 9; string reserve_tx_key = 10; repeated string reserve_tx_key_images = 11; string payout_address = 12; } message SignOfferResponse { string offer_id = 1; string uid = 2; OfferPayload signed_offer_payload = 3; } message OfferAvailabilityRequest { string offer_id = 1; PubKeyRing pub_key_ring = 2; int64 takers_trade_price = 3; repeated int32 supported_capabilities = 4; string uid = 5; bool is_taker_api_user = 6; InitTradeRequest trade_request = 7; } message OfferAvailabilityResponse { string offer_id = 1; AvailabilityResult availability_result = 2; repeated int32 supported_capabilities = 3; string uid = 4; bytes maker_signature = 5; } message RefreshOfferMessage { bytes hash_of_data_and_seq_nr = 1; bytes signature = 2; bytes hash_of_payload = 3; int32 sequence_number = 4; } message AddDataMessage { StorageEntryWrapper entry = 1; } message RemoveDataMessage { ProtectedStorageEntry protected_storage_entry = 1; } message RemoveMailboxDataMessage { ProtectedMailboxStorageEntry protected_storage_entry = 1; } message AddPersistableNetworkPayloadMessage { PersistableNetworkPayload payload = 1; } message CloseConnectionMessage { string reason = 1; } message AckMessage { string uid = 1; NodeAddress sender_node_address = 2; string source_type = 3; // enum name. e.g. TradeMessage, DisputeMessage,... string source_msg_class_name = 4; string source_uid = 5; // uid of source (TradeMessage) string source_id = 6; // id of source (tradeId, disputeId) bool success = 7; // true if source message was processed successfully string error_message = 8; // optional error message if source message processing failed string updated_multisig_hex = 9; // data to update the multisig state } message PrefixedSealedAndSignedMessage { NodeAddress node_address = 1; SealedAndSigned sealed_and_signed = 2; bytes address_prefix_hash = 3; string uid = 4; } enum TradeProtocolVersion { MULTISIG_2_3 = 0; } message InitTradeRequest { TradeProtocolVersion trade_protocol_version = 1; string offer_id = 2; int64 trade_amount = 3; int64 trade_price = 4; string payment_method_id = 5; string maker_account_id = 6; string taker_account_id = 7; string maker_payment_account_id = 8; string taker_payment_account_id = 9; PubKeyRing taker_pub_key_ring = 10; string uid = 11; bytes account_age_witness_signature_of_offer_id = 12; int64 current_date = 13; NodeAddress maker_node_address = 14; NodeAddress taker_node_address = 15; NodeAddress arbitrator_node_address = 16; string reserve_tx_hash = 17; string reserve_tx_hex = 18; string reserve_tx_key = 19; string payout_address = 20; string challenge = 21; } message InitMultisigRequest { string trade_id = 1; string uid = 2; int64 current_date = 3; string prepared_multisig_hex = 4; string made_multisig_hex = 5; string exchanged_multisig_hex = 6; string trade_fee_address = 7; } message SignContractRequest { string trade_id = 1; string uid = 2; int64 current_date = 3; string account_id = 4; bytes payment_account_payload_hash = 5; string payout_address = 6; string deposit_tx_hash = 7; bytes account_age_witness_signature_of_deposit_hash = 8; } message SignContractResponse { string trade_id = 1; string uid = 2; int64 current_date = 3; string contract_as_json = 4; bytes contract_signature = 5; bytes encrypted_payment_account_payload = 6; } message DepositRequest { string trade_id = 1; string uid = 2; int64 current_date = 3; bytes contract_signature = 4; string deposit_tx_hex = 5; string deposit_tx_key = 6; bytes payment_account_key = 7; } message DepositResponse { string trade_id = 1; string uid = 2; int64 current_date = 3; string error_message = 4; int64 buyerSecurityDeposit = 5; int64 sellerSecurityDeposit = 6; } message DepositsConfirmedMessage { string trade_id = 1; NodeAddress sender_node_address = 2; PubKeyRing pub_key_ring = 3; string uid = 4; bytes seller_payment_account_key = 5; string updated_multisig_hex = 6; } message PaymentSentMessage { string trade_id = 1; NodeAddress sender_node_address = 2; string counter_currency_tx_id = 3; string uid = 4; string counter_currency_extra_data = 5; string payout_tx_hex = 6; string updated_multisig_hex = 7; bytes payment_account_key = 8; AccountAgeWitness seller_account_age_witness = 9; bytes buyer_signature = 10; } message PaymentReceivedMessage { string trade_id = 1; NodeAddress sender_node_address = 2; string uid = 3; string unsigned_payout_tx_hex = 4; string signed_payout_tx_hex = 5; string updated_multisig_hex = 6; bool defer_publish_payout = 7; AccountAgeWitness buyer_account_age_witness = 8; SignedWitness buyer_signed_witness = 9; PaymentSentMessage payment_sent_message = 10; bytes seller_signature = 11; string payout_tx_id = 12; } message MediatedPayoutTxPublishedMessage { string trade_id = 1; bytes payout_tx = 2; NodeAddress sender_node_address = 3; string uid = 4; } message MediatedPayoutTxSignatureMessage { string uid = 1; string trade_id = 3; bytes tx_signature = 2; NodeAddress sender_node_address = 4; } message ChatMessage { int64 date = 1; string trade_id = 2; int32 trader_id = 3; bool sender_is_trader = 4; string message = 5; repeated Attachment attachments = 6; bool arrived = 7; bool stored_in_mailbox = 8; bool is_system_message = 9; NodeAddress sender_node_address = 10; string uid = 11; string send_message_error = 12; bool acknowledged = 13; string ack_error = 14; SupportType type = 15; bool was_displayed = 16; } // dispute enum SupportType { ARBITRATION = 0; MEDIATION = 1; TRADE = 2; REFUND = 3; } message DisputeOpenedMessage { Dispute dispute = 1; NodeAddress sender_node_address = 2; string uid = 3; SupportType type = 4; string opener_updated_multisig_hex = 5; PaymentSentMessage payment_sent_message = 6; } message DisputeClosedMessage { string uid = 1; DisputeResult dispute_result = 2; NodeAddress sender_node_address = 3; SupportType type = 4; string updated_multisig_hex = 5; string unsigned_payout_tx_hex = 6; bool defer_publish_payout = 7; } message PrivateNotificationMessage { string uid = 1; NodeAddress sender_node_address = 2; PrivateNotificationPayload private_notification_payload = 3; } /////////////////////////////////////////////////////////////////////////////////////////// // Payload /////////////////////////////////////////////////////////////////////////////////////////// // core message NodeAddress { string host_name = 1; int32 port = 2; } message Peer { NodeAddress node_address = 1; int64 date = 2; repeated int32 supported_capabilities = 3; } message PubKeyRing { bytes signature_pub_key_bytes = 1; bytes encryption_pub_key_bytes = 2; } message SealedAndSigned { bytes encrypted_secret_key = 1; bytes encrypted_payload_with_hmac = 2; bytes signature = 3; bytes sig_public_key_bytes = 4; } // storage message StoragePayload { oneof message { Alert alert = 1; Arbitrator arbitrator = 2; Mediator mediator = 3; Filter filter = 4; MailboxStoragePayload mailbox_storage_payload = 5; OfferPayload offer_payload = 6; RefundAgent refund_agent = 7; } } message PersistableNetworkPayload { oneof message { AccountAgeWitness account_age_witness = 1; SignedWitness signed_witness = 2; TradeStatistics3 trade_statistics3 = 3; } } message ProtectedStorageEntry { StoragePayload storagePayload = 1; bytes owner_pub_key_bytes = 2; int32 sequence_number = 3; bytes signature = 4; int64 creation_time_stamp = 5; } // mailbox message StorageEntryWrapper { oneof message { ProtectedStorageEntry protected_storage_entry = 1; ProtectedMailboxStorageEntry protected_mailbox_storage_entry = 2; } } message ProtectedMailboxStorageEntry { ProtectedStorageEntry entry = 1; bytes receivers_pub_key_bytes = 2; } message DataAndSeqNrPair { StoragePayload payload = 1; int32 sequence_number = 2; } message MailboxMessageList { repeated MailboxItem mailbox_item = 1; } message RemovedPayloadsMap { map date_by_hashes = 1; } message IgnoredMailboxMap { map data = 1; } message MailboxItem { ProtectedMailboxStorageEntry protected_mailbox_storage_entry = 1; DecryptedMessageWithPubKey decrypted_message_with_pub_key = 2; } message DecryptedMessageWithPubKey { NetworkEnvelope network_envelope = 1; bytes signature_pub_key_bytes = 2; } // misc message PrivateNotificationPayload { string message = 1; string signature_as_base64 = 2; bytes sig_public_key_bytes = 3; } message PaymentAccountFilter { string payment_method_id = 1; string get_method_name = 2; string value = 3; } /////////////////////////////////////////////////////////////////////////////////////////// // Storage payload /////////////////////////////////////////////////////////////////////////////////////////// message Alert { string message = 1; string version = 2; bool is_update_info = 3; string signature_as_base64 = 4; bytes owner_pub_key_bytes = 5; map extra_data = 6; bool is_pre_release_info = 7; } message Arbitrator { NodeAddress node_address = 1; repeated string language_codes = 2; int64 registration_date = 3; string registration_signature = 4; bytes registration_pub_key = 5; PubKeyRing pub_key_ring = 6; string email_address = 7; string info = 8; map extra_data = 9; } message Mediator { NodeAddress node_address = 1; repeated string language_codes = 2; int64 registration_date = 3; string registration_signature = 4; bytes registration_pub_key = 5; PubKeyRing pub_key_ring = 6; string email_address = 7; string info = 8; map extra_data = 9; } message RefundAgent { NodeAddress node_address = 1; repeated string language_codes = 2; int64 registration_date = 3; string registration_signature = 4; bytes registration_pub_key = 5; PubKeyRing pub_key_ring = 6; string email_address = 7; string info = 8; map extra_data = 9; } message Filter { repeated string node_addresses_banned_from_trading = 1; repeated string banned_offer_ids = 2; repeated PaymentAccountFilter banned_payment_accounts = 3; string signature_as_base64 = 4; bytes owner_pub_key_bytes = 5; map extra_data = 6; repeated string banned_currencies = 7; repeated string banned_payment_methods = 8; repeated string arbitrators = 9; repeated string seed_nodes = 10; repeated string price_relay_nodes = 11; bool prevent_public_xmr_network = 12; repeated string xmr_nodes = 13; string disable_trade_below_version = 14; repeated string mediators = 15; repeated string refundAgents = 16; repeated string bannedSignerPubKeys = 17; repeated string xmr_fee_receiver_addresses = 18; int64 creation_date = 19; string signer_pub_key_as_hex = 20; repeated string bannedPrivilegedDevPubKeys = 21; bool disable_auto_conf = 22; repeated string banned_auto_conf_explorers = 23; repeated string node_addresses_banned_from_network = 24; bool disable_api = 25; bool disable_mempool_validation = 26; } message TradeStatistics3 { string currency = 1; int64 price = 2; int64 amount = 3; string payment_method = 4; int64 date = 5; string arbitrator = 6; bytes hash = 7; string maker_deposit_tx_id = 8; string taker_deposit_tx_id = 9; map extra_data = 10; } message MailboxStoragePayload { PrefixedSealedAndSignedMessage prefixed_sealed_and_signed_message = 1; bytes sender_pub_key_for_add_operation_bytes = 2; bytes owner_pub_key_bytes = 3; map extra_data = 4; } message OfferPayload { string id = 1; int64 date = 2; NodeAddress owner_node_address = 3; PubKeyRing pub_key_ring = 4; OfferDirection direction = 5; int64 price = 6; double market_price_margin_pct = 7; bool use_market_based_price = 8; int64 amount = 9; int64 min_amount = 10; double maker_fee_pct = 11; double taker_fee_pct = 12; double penalty_fee_pct = 13; double buyer_security_deposit_pct = 14; double seller_security_deposit_pct = 15; string base_currency_code = 16; string counter_currency_code = 17; string payment_method_id = 18; string maker_payment_account_id = 19; string country_code = 20; repeated string accepted_country_codes = 21; string bank_id = 22; repeated string accepted_bank_ids = 23; string version_nr = 24; int64 block_height_at_offer_creation = 25; int64 max_trade_limit = 26; int64 max_trade_period = 27; bool use_auto_close = 28; bool use_re_open_after_auto_close = 29; int64 lower_close_price = 30; int64 upper_close_price = 31; bool is_private_offer = 32; string challenge_hash = 33; map extra_data = 34; int32 protocol_version = 35; NodeAddress arbitrator_signer = 36; bytes arbitrator_signature = 37; repeated string reserve_tx_key_images = 38; string extra_info = 39; } enum OfferDirection { OFFER_DIRECTION_UNDEFINED = 0; BUY = 1; SELL = 2; } message AccountAgeWitness { bytes hash = 1; int64 date = 2; } message SignedWitness { enum VerificationMethod { PB_ERROR = 0; ARBITRATOR = 1; TRADE = 2; } VerificationMethod verification_method = 1; bytes account_age_witness_hash = 2; bytes signature = 3; bytes signer_pub_key = 4; bytes witness_owner_pub_key = 5; int64 date = 6; int64 trade_amount = 7; } /////////////////////////////////////////////////////////////////////////////////////////// // Dispute payload /////////////////////////////////////////////////////////////////////////////////////////// message Dispute { enum State { NEEDS_UPGRADE = 0; NEW = 1; OPEN = 2; REOPENED = 3; CLOSED = 4; } string trade_id = 1; string id = 2; int32 trader_id = 3; bool is_opener = 4; bool dispute_opener_is_buyer = 5; bool dispute_opener_is_maker = 6; int64 opening_date = 7; PubKeyRing trader_pub_key_ring = 8; int64 trade_date = 9; Contract contract = 10; bytes contract_hash = 11; bytes payout_tx_serialized = 12; string payout_tx_id = 13; string contract_as_json = 14; bytes maker_contract_signature = 15; bytes taker_contract_signature = 16; PaymentAccountPayload maker_payment_account_payload = 17; PaymentAccountPayload taker_payment_account_payload = 18; PubKeyRing agent_pub_key_ring = 19; bool is_support_ticket = 20; repeated ChatMessage chat_message = 21; bool is_closed = 22; DisputeResult dispute_result = 23; string dispute_payout_tx_id = 24; SupportType support_type = 25; string mediators_dispute_result = 26; string delayed_payout_tx_id = 27; string donation_address_of_delayed_payout_tx = 28; State state = 29; int64 trade_period_end = 30; map extra_data = 31; } message Attachment { string file_name = 1; bytes bytes = 2; } message DisputeResult { enum Winner { PB_ERROR_WINNER = 0; BUYER = 1; SELLER = 2; } enum Reason { PB_ERROR_REASON = 0; OTHER = 1; BUG = 2; USABILITY = 3; SCAM = 4; PROTOCOL_VIOLATION = 5; NO_REPLY = 6; BANK_PROBLEMS = 7; OPTION_TRADE = 8; SELLER_NOT_RESPONDING = 9; WRONG_SENDER_ACCOUNT = 10; TRADE_ALREADY_SETTLED = 11; PEER_WAS_LATE = 12; } enum SubtractFeeFrom { BUYER_ONLY = 0; SELLER_ONLY = 1; BUYER_AND_SELLER = 2; } string trade_id = 1; int32 trader_id = 2; Winner winner = 3; int32 reason_ordinal = 4; bool tamper_proof_evidence = 5; bool id_verification = 6; bool screen_cast = 7; string summary_notes = 8; ChatMessage chat_message = 9; bytes arbitrator_signature = 10; int64 buyer_payout_amount_before_cost = 11; int64 seller_payout_amount_before_cost = 12; SubtractFeeFrom subtract_fee_from = 13; bytes arbitrator_pub_key = 14; int64 close_date = 15; bool is_loser_publisher = 16; } /////////////////////////////////////////////////////////////////////////////////////////// // Trade payload /////////////////////////////////////////////////////////////////////////////////////////// message Contract { OfferPayload offer_payload = 1; int64 trade_amount = 2; int64 trade_price = 3; NodeAddress arbitrator_node_address = 4; bool is_buyer_maker_and_seller_taker = 5; string maker_account_id = 6; string taker_account_id = 7; string maker_payment_method_id = 8; string taker_payment_method_id = 9; bytes maker_payment_account_payload_hash = 10; bytes taker_payment_account_payload_hash = 11; PubKeyRing maker_pub_key_ring = 12; PubKeyRing taker_pub_key_ring = 13; NodeAddress buyer_node_address = 14; NodeAddress seller_node_address = 15; string maker_payout_address_string = 16; string taker_payout_address_string = 17; string maker_deposit_tx_hash = 18; string taker_deposit_tx_hash = 19; } message RawTransactionInput { int64 index = 1; bytes parent_transaction = 2; int64 value = 3; } enum AvailabilityResult { PB_ERROR = 0; UNKNOWN_FAILURE = 1; AVAILABLE = 2; OFFER_TAKEN = 3; PRICE_OUT_OF_TOLERANCE = 4; MARKET_PRICE_NOT_AVAILABLE = 5; NO_ARBITRATORS = 6; NO_MEDIATORS = 7; USER_IGNORED = 8; MISSING_MANDATORY_CAPABILITY = 9; NO_REFUND_AGENTS = 10; UNCONF_TX_LIMIT_HIT = 11; MAKER_DENIED_API_USER = 12; PRICE_CHECK_FAILED = 13; MAKER_DENIED_TAKER = 14; } /////////////////////////////////////////////////////////////////////////////////////////// // PaymentAccount payload /////////////////////////////////////////////////////////////////////////////////////////// message PaymentAccountPayload { string id = 1; string payment_method_id = 2; int64 max_trade_period = 3; // not used map exclude_from_json_data = 4; oneof message { AliPayAccountPayload ali_pay_account_payload = 5; ChaseQuickPayAccountPayload chase_quick_pay_account_payload = 6; ZelleAccountPayload zelle_account_payload = 7; CountryBasedPaymentAccountPayload country_based_payment_account_payload = 8; CryptoCurrencyAccountPayload crypto_currency_account_payload = 9; FasterPaymentsAccountPayload faster_payments_account_payload = 10; InteracETransferAccountPayload interac_e_transfer_account_payload = 11; OKPayAccountPayload o_k_pay_account_payload = 12 [deprecated = true]; PerfectMoneyAccountPayload perfect_money_account_payload = 13; SwishAccountPayload swish_account_payload = 14; USPostalMoneyOrderAccountPayload u_s_postal_money_order_account_payload = 15; UpholdAccountPayload uphold_account_payload = 16; CashAppAccountPayload cash_app_account_payload = 17; MoneyBeamAccountPayload money_beam_account_payload = 18; VenmoAccountPayload venmo_account_payload = 19; PopmoneyAccountPayload popmoney_account_payload = 20; RevolutAccountPayload revolut_account_payload = 21; WeChatPayAccountPayload we_chat_pay_account_payload = 22; MoneyGramAccountPayload money_gram_account_payload = 23; HalCashAccountPayload hal_cash_account_payload = 24; PromptPayAccountPayload prompt_pay_account_payload = 25; AdvancedCashAccountPayload advanced_cash_account_payload = 26; InstantCryptoCurrencyAccountPayload instant_crypto_currency_account_payload = 27; JapanBankAccountPayload japan_bank_account_payload = 28; TransferwiseAccountPayload Transferwise_account_payload = 29; AustraliaPayidPayload australia_payid_payload = 30; AmazonGiftCardAccountPayload amazon_gift_card_account_payload = 31; PayByMailAccountPayload pay_by_mail_account_payload = 32; CapitualAccountPayload capitual_account_payload = 33; PayseraAccountPayload Paysera_account_payload = 34; PaxumAccountPayload paxum_account_payload = 35; SwiftAccountPayload swift_account_payload = 36; CelPayAccountPayload cel_pay_account_payload = 37; MoneseAccountPayload monese_account_payload = 38; VerseAccountPayload verse_account_payload = 39; CashAtAtmAccountPayload cash_at_atm_account_payload = 40; PayPalAccountPayload paypal_account_payload = 41; PaysafeAccountPayload paysafe_account_payload = 42; } } message AliPayAccountPayload { string account_nr = 1; } message WeChatPayAccountPayload { string account_nr = 1; } message ChaseQuickPayAccountPayload { string email = 1; string holder_name = 2; } message ZelleAccountPayload { string holder_name = 1; string email_or_mobile_nr = 2; } message CountryBasedPaymentAccountPayload { string country_code = 1; repeated string accepted_country_codes = 2; oneof message { BankAccountPayload bank_account_payload = 3; CashDepositAccountPayload cash_deposit_account_payload = 4; SepaAccountPayload sepa_account_payload = 5; WesternUnionAccountPayload western_union_account_payload = 6; SepaInstantAccountPayload sepa_instant_account_payload = 7; F2FAccountPayload f2f_account_payload = 8; UpiAccountPayload upi_account_payload = 9; PaytmAccountPayload paytm_account_payload = 10; IfscBasedAccountPayload ifsc_based_account_payload = 11; NequiAccountPayload nequi_account_payload = 12; BizumAccountPayload bizum_account_payload = 13; PixAccountPayload pix_account_payload = 14; SatispayAccountPayload satispay_account_payload = 15; StrikeAccountPayload strike_account_payload = 16; TikkieAccountPayload tikkie_account_payload = 17; TransferwiseUsdAccountPayload transferwise_usd_account_payload = 18; SwiftAccountPayload swift_account_payload = 19; } } message BankAccountPayload { string holder_name = 1; string bank_name = 2; string bank_id = 3; string branch_id = 4; string account_nr = 5; string account_type = 6; string holder_tax_id = 7; string email = 8 [deprecated = true]; oneof message { NationalBankAccountPayload national_bank_account_payload = 9; SameBankAccountPayload same_bank_accont_payload = 10; SpecificBanksAccountPayload specific_banks_account_payload = 11; AchTransferAccountPayload ach_transfer_account_payload = 13; DomesticWireTransferAccountPayload domestic_wire_transfer_account_payload = 14; } string national_account_id = 12; } message AchTransferAccountPayload { string holder_address = 1; } message DomesticWireTransferAccountPayload { string holder_address = 1; } message NationalBankAccountPayload { } message SameBankAccountPayload { } message JapanBankAccountPayload { string bank_name = 1; string bank_code = 2; string bank_branch_name = 3; string bank_branch_code = 4; string bank_account_type = 5; string bank_account_name = 6; string bank_account_number = 7; } message AustraliaPayidPayload { string bank_account_name = 1; string payid = 2; string extra_info = 3; } message SpecificBanksAccountPayload { repeated string accepted_banks = 1; } message CashDepositAccountPayload { string holder_name = 1; string holder_email = 2; string bank_name = 3; string bank_id = 4; string branch_id = 5; string account_nr = 6; string account_type = 7; string requirements = 8; string holder_tax_id = 9; string national_account_id = 10; } message MoneyGramAccountPayload { string holder_name = 1; string country_code = 2; string state = 3; string email = 4; } message HalCashAccountPayload { string mobile_nr = 1; } message WesternUnionAccountPayload { string holder_name = 1; string city = 2; string state = 3; string email = 4; } message AmazonGiftCardAccountPayload { string email_or_mobile_nr = 1; string country_code = 2; } message SepaAccountPayload { string holder_name = 1; string iban = 2; string bic = 3; string email = 4 [deprecated = true]; } message SepaInstantAccountPayload { string holder_name = 1; string iban = 2; string bic = 3; } message CryptoCurrencyAccountPayload { string address = 1; } message InstantCryptoCurrencyAccountPayload { string address = 1; } message FasterPaymentsAccountPayload { string holder_name = 1; string sort_code = 2; string account_nr = 3; } message InteracETransferAccountPayload { string email_or_mobile_nr = 1; string holder_name = 2; string question = 3; string answer = 4; } // Deprecated, not used anymore message OKPayAccountPayload { string account_nr = 1; } message UpholdAccountPayload { string account_id = 1; string account_owner = 2; } message CashAppAccountPayload { string email_or_mobile_nr_or_cashtag = 1; string extra_info = 2; } message MoneyBeamAccountPayload { string account_id = 1; string holder_name = 2; } message VenmoAccountPayload { string email_or_mobile_nr_or_username = 1; } message PayPalAccountPayload { string email_or_mobile_nr_or_username = 1; string extra_info = 2; } message PopmoneyAccountPayload { string account_id = 1; string holder_name = 2; } message RevolutAccountPayload { string username = 1; } message PerfectMoneyAccountPayload { string account_nr = 1; } message SwishAccountPayload { string mobile_nr = 1; string holder_name = 2; } message USPostalMoneyOrderAccountPayload { string postal_address = 1; string holder_name = 2; } message F2FAccountPayload { string contact = 1; string city = 2; string extra_info = 3; } message IfscBasedAccountPayload { string holder_name = 1; string account_nr = 2; string ifsc = 3; oneof message { NeftAccountPayload neft_account_payload = 4; RtgsAccountPayload rtgs_account_payload = 5; ImpsAccountPayload imps_account_payload = 6; } } message NeftAccountPayload { } message RtgsAccountPayload { } message ImpsAccountPayload { } message UpiAccountPayload { string virtual_payment_address = 1; } message PaytmAccountPayload { string email_or_mobile_nr = 1; } message PayByMailAccountPayload { string postal_address = 1; string contact = 2; string extra_info = 3; } message CashAtAtmAccountPayload { string extra_info = 1; } message PromptPayAccountPayload { string prompt_pay_id = 1; } message AdvancedCashAccountPayload { string account_nr = 1; } message TransferwiseAccountPayload { string email = 1; string holder_name = 2; } message TransferwiseUsdAccountPayload { string email = 1; string holder_name = 2; string holder_address = 3; } message PayseraAccountPayload { string email = 1; } message PaxumAccountPayload { string email = 1; } message CapitualAccountPayload { string account_nr = 1; } message CelPayAccountPayload { string email = 1; } message NequiAccountPayload { string mobile_nr = 1; } message BizumAccountPayload { string mobile_nr = 1; } message PixAccountPayload { string pix_key = 1; string holder_name = 2; } message MoneseAccountPayload { string mobile_nr = 1; string holder_name = 2; } message SatispayAccountPayload { string mobile_nr = 1; string holder_name = 2; } message StrikeAccountPayload { string holder_name = 1; } message TikkieAccountPayload { string iban = 1; } message VerseAccountPayload { string holder_name = 1; } message SwiftAccountPayload { string beneficiary_name = 1; string beneficiary_account_nr = 2; string beneficiary_address = 3; string beneficiary_city = 4; string beneficiary_phone = 5; string special_instructions = 6; string bank_swift_code = 7; string bank_country_code = 8; string bank_name = 9; string bank_branch = 10; string bank_address = 11; string intermediary_swift_code = 12; string intermediary_country_code = 13; string intermediary_name = 14; string intermediary_branch = 15; string intermediary_address = 16; } message PaysafeAccountPayload { string email = 1; } message PersistableEnvelope { oneof message { SequenceNumberMap sequence_number_map = 1; PeerList peer_list = 2; AddressEntryList address_entry_list = 3; NavigationPath navigation_path = 4; TradableList tradable_list = 5; ArbitrationDisputeList arbitration_dispute_list = 6; PreferencesPayload preferences_payload = 7; UserPayload user_payload = 8; PaymentAccountList payment_account_list = 9; AccountAgeWitnessStore account_age_witness_store = 10; SignedWitnessStore signed_witness_store = 11; MediationDisputeList mediation_dispute_list = 12; RefundDisputeList refund_dispute_list = 13; TradeStatistics3Store trade_statistics3_store = 14; MailboxMessageList mailbox_message_list = 15; IgnoredMailboxMap ignored_mailbox_map = 16; RemovedPayloadsMap removed_payloads_map = 17; XmrAddressEntryList xmr_address_entry_list = 18; SignedOfferList signed_offer_list = 19; EncryptedConnectionList encrypted_connection_list = 20; } } message SequenceNumberMap { repeated SequenceNumberEntry sequence_number_entries = 1; } message SequenceNumberEntry { ByteArray bytes = 1; MapValue map_value = 2; } message ByteArray { bytes bytes = 1; } message MapValue { int32 sequence_nr = 1; int64 time_stamp = 2; } // We use a list not a hash map to save disc space. The hash can be calculated from the payload anyway message AccountAgeWitnessStore { repeated AccountAgeWitness items = 1; } message SignedWitnessStore { repeated SignedWitness items = 1; } message TradeStatistics3Store { repeated TradeStatistics3 items = 1; } message PeerList { repeated Peer peer = 1; } message AddressEntryList { repeated AddressEntry address_entry = 1; } message AddressEntry { enum Context { PB_ERROR = 0; ARBITRATOR = 1; AVAILABLE = 2; OFFER_FUNDING = 3; RESERVED_FOR_TRADE = 4; MULTI_SIG = 5; TRADE_PAYOUT = 6; } string offer_id = 7; Context context = 8; bytes pub_key = 9; bytes pub_key_hash = 10; int64 coin_locked_in_multi_sig = 11; bool segwit = 12; } message XmrAddressEntryList { repeated XmrAddressEntry xmr_address_entry = 1; } message XmrAddressEntry { enum Context { PB_ERROR = 0; ARBITRATOR = 1; BASE_ADDRESS = 2; AVAILABLE = 3; OFFER_FUNDING = 4; TRADE_PAYOUT = 5; } int32 subaddress_index = 7; string address_string = 8; string offer_id = 9; Context context = 10; int64 coin_locked_in_multi_sig = 11; } message NavigationPath { repeated string path = 1; } message PaymentAccountList { repeated PaymentAccount payment_account = 1; } /////////////////////////////////////////////////////////////////////////////////////////// // Offer/Trade /////////////////////////////////////////////////////////////////////////////////////////// message TradableList { repeated Tradable tradable = 1; } message Offer { enum State { PB_ERROR = 0; UNKNOWN = 1; OFFER_FEE_RESERVED = 2; AVAILABLE = 3; NOT_AVAILABLE = 4; REMOVED = 5; MAKER_OFFLINE = 6; INVALID = 7; } OfferPayload offer_payload = 1; } message SignedOfferList { repeated SignedOffer signed_offer = 1; } message SignedOffer { int64 time_stamp = 1; int32 trader_id = 2; string offer_id = 3; uint64 trade_amount = 4; uint64 penalty_amount = 5; string reserve_tx_hash = 6; string reserve_tx_hex = 7; repeated string reserve_tx_key_images = 8; uint64 reserve_tx_miner_fee = 9; bytes arbitrator_signature = 10; } message OpenOffer { enum State { PB_ERROR = 0; PENDING = 1; AVAILABLE = 2; RESERVED = 3; CLOSED = 4; CANCELED = 5; DEACTIVATED = 6; } Offer offer = 1; State state = 2; int64 trigger_price = 3; bool reserve_exact_amount = 4; string split_output_tx_hash = 5; int64 split_output_tx_fee = 6; repeated string scheduled_tx_hashes = 7; string scheduled_amount = 8; // BigInteger string reserve_tx_hash = 9; string reserve_tx_hex = 10; string reserve_tx_key = 11; string challenge = 12; bool deactivated_by_trigger = 13; string group_id = 14; } message Tradable { oneof message { OpenOffer open_offer = 1; SignedOffer signed_offer = 2; BuyerAsMakerTrade buyer_as_maker_trade = 3; BuyerAsTakerTrade buyer_as_taker_trade = 4; SellerAsMakerTrade seller_as_maker_trade = 5; SellerAsTakerTrade seller_as_taker_trade = 6; ArbitratorTrade arbitrator_trade = 7; } } message Trade { enum State { PB_ERROR_STATE = 0; PREPARATION = 1; MULTISIG_PREPARED = 2; MULTISIG_MADE = 3; MULTISIG_EXCHANGED = 4; MULTISIG_COMPLETED = 5; CONTRACT_SIGNATURE_REQUESTED = 6; CONTRACT_SIGNED = 7; SENT_PUBLISH_DEPOSIT_TX_REQUEST = 8; SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST = 9; SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST = 10; PUBLISH_DEPOSIT_TX_REQUEST_FAILED = 11; ARBITRATOR_PUBLISHED_DEPOSIT_TXS = 12; DEPOSIT_TXS_SEEN_IN_NETWORK = 13; DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN = 14; DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN = 15; DEPOSIT_TXS_FINALIZED_IN_BLOCKCHAIN = 28; BUYER_CONFIRMED_PAYMENT_SENT = 16; BUYER_SENT_PAYMENT_SENT_MSG = 17; BUYER_SEND_FAILED_PAYMENT_SENT_MSG = 18; BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG = 19; BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG = 20; SELLER_RECEIVED_PAYMENT_SENT_MSG = 21; SELLER_CONFIRMED_PAYMENT_RECEIPT = 22; SELLER_SENT_PAYMENT_RECEIVED_MSG = 23; SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG = 24; SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG = 25; SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG = 26; BUYER_RECEIVED_PAYMENT_RECEIVED_MSG = 27; } enum Phase { PB_ERROR_PHASE = 0; INIT = 1; DEPOSIT_REQUESTED = 2; DEPOSITS_PUBLISHED = 3; DEPOSITS_CONFIRMED = 4; DEPOSITS_UNLOCKED = 5; DEPOSITS_FINALIZED = 8; PAYMENT_SENT = 6; PAYMENT_RECEIVED = 7; } enum PayoutState { PAYOUT_UNPUBLISHED = 0; PAYOUT_PUBLISHED = 1; PAYOUT_CONFIRMED = 2; PAYOUT_UNLOCKED = 3; PAYOUT_FINALIZED = 4; } enum DisputeState { PB_ERROR_DISPUTE_STATE = 0; NO_DISPUTE = 1; DISPUTE_PREPARING = 15; DISPUTE_REQUESTED = 2; DISPUTE_OPENED = 3; ARBITRATOR_SENT_DISPUTE_CLOSED_MSG = 4; ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG = 5; ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG = 6; ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG = 7; DISPUTE_CLOSED = 8; MEDIATION_REQUESTED = 9; MEDIATION_STARTED_BY_PEER = 10; MEDIATION_CLOSED = 11; REFUND_REQUESTED = 12; REFUND_REQUEST_STARTED_BY_PEER = 13; REFUND_REQUEST_CLOSED = 14; } enum TradePeriodState { PB_ERROR_TRADE_PERIOD_STATE = 0; FIRST_HALF = 1; SECOND_HALF = 2; TRADE_PERIOD_OVER = 3; } Offer offer = 1; ProcessModel process_model = 2; string payout_tx_id = 3; string payout_tx_hex = 4; string payout_tx_key = 5; int64 amount = 6; int64 take_offer_date = 7; int64 price = 8; State state = 9; PayoutState payout_state = 10; DisputeState dispute_state = 11; TradePeriodState period_state = 12; Contract contract = 13; string contract_as_json = 14; bytes contract_hash = 15; NodeAddress arbitrator_node_address = 16; NodeAddress mediator_node_address = 17; string error_message = 18; string counter_currency_tx_id = 19; repeated ChatMessage chat_message = 20; MediationResultState mediation_result_state = 21; int64 lock_time = 22; int64 start_time = 23; NodeAddress refund_agent_node_address = 24; RefundResultState refund_result_state = 25; string counter_currency_extra_data = 26; string uid = 27; bool is_completed = 28; string challenge = 29; } message BuyerAsMakerTrade { Trade trade = 1; } message BuyerAsTakerTrade { Trade trade = 1; } message SellerAsMakerTrade { Trade trade = 1; } message SellerAsTakerTrade { Trade trade = 1; } message ArbitratorTrade { Trade trade = 1; } message ProcessModel { string offer_id = 1; string account_id = 2; PubKeyRing pub_key_ring = 3; bytes payout_tx_signature = 4; bool use_savings_wallet = 5; int64 funds_needed_for_trade = 6; string payment_sent_message_state_seller = 7 [deprecated = true]; string payment_sent_message_state_arbitrator = 8 [deprecated = true]; bytes maker_signature = 9; TradePeer maker = 10; TradePeer taker = 11; TradePeer arbitrator = 12; NodeAddress temp_trade_peer_node_address = 13; string multisig_address = 14; bytes mediated_payout_tx_signature = 15; // placeholder if mediation used in future int64 buyer_payout_amount_from_mediation = 16; int64 seller_payout_amount_from_mediation = 17; int64 trade_protocol_error_height = 18; string trade_fee_address = 19; bool import_multisig_hex_scheduled = 20; bool payment_sent_payout_tx_stale = 21; bool error_on_payment_received_msg = 22 [deprecated = true]; // used during debugging across clients (can be repurposed) } message TradePeer { NodeAddress node_address = 1; PubKeyRing pub_key_ring = 2; string account_id = 3; string payment_account_id = 4; string payment_method_id = 5; bytes payment_account_payload_hash = 6; bytes encrypted_payment_account_payload = 7; bytes payment_account_key = 8; PaymentAccountPayload payment_account_payload = 9; string payout_address_string = 10; string contract_as_json = 11; bytes contract_signature = 12; bytes account_age_witness_nonce = 18; bytes account_age_witness_signature = 19; AccountAgeWitness account_age_witness = 20; int64 current_date = 21; bytes mediated_payout_tx_signature = 22; PaymentSentMessage payment_sent_message = 23; PaymentReceivedMessage payment_received_message = 24; DisputeOpenedMessage dispute_opened_message = 46; DisputeClosedMessage dispute_closed_message = 25; string reserve_tx_hash = 26; string reserve_tx_hex = 27; string reserve_tx_key = 28; repeated string reserve_tx_key_images = 29; string prepared_multisig_hex = 30; string made_multisig_hex = 31; string exchanged_multisig_hex = 32; string updated_multisig_hex = 33; bool deposits_confirmed_message_acked = 34 [deprecated = true]; string deposit_tx_hash = 35; string deposit_tx_hex = 36; string deposit_tx_key = 37; int64 deposit_tx_fee = 38; int64 security_deposit = 39; string unsigned_payout_tx_hex = 40; int64 payout_tx_fee = 41; int64 payout_amount = 42; string deposits_confirmed_message_state = 43; string payment_sent_message_state = 44; string payment_received_message_state = 45; string dispute_opened_message_state = 47; string dispute_closed_message_state = 48; } /////////////////////////////////////////////////////////////////////////////////////////// // Connections /////////////////////////////////////////////////////////////////////////////////////////// message EncryptedConnection { string url = 1; string username = 2; bytes encrypted_password = 3; bytes encryption_salt = 4; int32 priority = 5; } message EncryptedConnectionList { bytes salt = 1; repeated EncryptedConnection items = 2; string current_connection_url = 3; int64 refresh_period = 4; // negative: no automated refresh is activated, zero: automated refresh with default period, positive: automated refresh with configured period (value) bool auto_switch = 5; } /////////////////////////////////////////////////////////////////////////////////////////// // Dispute /////////////////////////////////////////////////////////////////////////////////////////// message ArbitrationDisputeList { repeated Dispute dispute = 1; } message MediationDisputeList { repeated Dispute dispute = 1; } message RefundDisputeList { repeated Dispute dispute = 1; } enum MediationResultState { PB_ERROR_MEDIATION_RESULT = 0; UNDEFINED_MEDIATION_RESULT = 1; MEDIATION_RESULT_ACCEPTED = 2; MEDIATION_RESULT_REJECTED = 3; SIG_MSG_SENT = 4; SIG_MSG_ARRIVED = 5; SIG_MSG_IN_MAILBOX = 6; SIG_MSG_SEND_FAILED = 7; RECEIVED_SIG_MSG = 8; PAYOUT_TX_PUBLISHED = 9; PAYOUT_TX_PUBLISHED_MSG_SENT = 10; PAYOUT_TX_PUBLISHED_MSG_ARRIVED = 11; PAYOUT_TX_PUBLISHED_MSG_IN_MAILBOX = 12; PAYOUT_TX_PUBLISHED_MSG_SEND_FAILED = 13; RECEIVED_PAYOUT_TX_PUBLISHED_MSG = 14; PAYOUT_TX_SEEN_IN_NETWORK = 15; } //todo enum RefundResultState { PB_ERROR_REFUND_RESULT = 0; UNDEFINED_REFUND_RESULT = 1; } /////////////////////////////////////////////////////////////////////////////////////////// // Preferences /////////////////////////////////////////////////////////////////////////////////////////// message PreferencesPayload { string user_language = 1; Country user_country = 2; repeated TradeCurrency traditional_currencies = 3; repeated TradeCurrency crypto_currencies = 4; BlockChainExplorer block_chain_explorer_main_net = 5; BlockChainExplorer block_chain_explorer_test_net = 6; string backup_directory = 7; bool auto_select_arbitrators = 8; map dont_show_again_map = 9; bool tac_accepted = 10; int32 use_tor_for_xmr_ordinal = 11; bool show_own_offers_in_offer_book = 12; TradeCurrency preferred_trade_currency = 13; int64 withdrawal_tx_fee_in_vbytes = 14; bool use_custom_withdrawal_tx_fee = 15; double max_price_distance_in_percent = 16; string offer_book_chart_screen_currency_code = 17; string trade_charts_screen_currency_code = 18; string buy_screen_currency_code = 19; string sell_screen_currency_code = 20; int32 trade_statistics_tick_unit_index = 21; bool resync_spv_requested = 22; bool sort_market_currencies_numerically = 23; bool use_percentage_based_price = 24; map peer_tag_map = 25; string monero_nodes = 26; repeated string ignore_traders_list = 27; string directory_chooser_path = 28; bool use_animations = 29; PaymentAccount selectedPayment_account_for_createOffer = 30; repeated string bridge_addresses = 31; int32 bridge_option_ordinal = 32; int32 tor_transport_ordinal = 33; string custom_bridges = 34; int32 monero_nodes_option_ordinal = 35; string referral_id = 36; string phone_key_and_token = 37; bool use_sound_for_mobile_notifications = 38; bool use_trade_notifications = 39; bool use_market_notifications = 40; bool use_price_notifications = 41; bool use_standby_mode = 42; string rpc_user = 43; string rpc_pw = 44; string take_offer_selected_payment_account_id = 45; double security_deposit_as_percent = 46; int32 ignore_dust_threshold = 47; double security_deposit_as_percent_for_crypto = 48; int32 block_notify_port = 49; int32 css_theme = 50; bool tac_accepted_v120 = 51; repeated AutoConfirmSettings auto_confirm_settings = 52; double bsq_average_trim_threshold = 53; bool hide_non_account_payment_methods = 54; bool show_offers_matching_my_accounts = 55; bool deny_api_taker = 56; bool notify_on_pre_release = 57; XmrNodeSettings xmr_node_settings = 58; int32 clear_data_after_days = 59; string buy_screen_crypto_currency_code = 60; string sell_screen_crypto_currency_code = 61; bool split_offer_output = 62; bool use_sound_for_notifications = 63; bool use_sound_for_notifications_initialized = 64; string buy_screen_other_currency_code = 65; string sell_screen_other_currency_code = 66; bool show_private_offers = 67; } message AutoConfirmSettings { bool enabled = 1; int32 required_confirmations = 2; int64 trade_limit = 3; repeated string service_addresses = 4; string currency_code = 5; } message XmrNodeSettings { string blockchain_path = 1; string bootstrap_url = 2; repeated string startup_flags = 3; bool sync_blockchain = 4; } /////////////////////////////////////////////////////////////////////////////////////////// // UserPayload /////////////////////////////////////////////////////////////////////////////////////////// message UserPayload { string account_id = 1; repeated PaymentAccount payment_accounts = 2; PaymentAccount current_payment_account = 3; repeated string accepted_language_locale_codes = 4; Alert developers_alert = 5; Alert displayed_alert = 6; Filter developers_filter = 7; repeated Arbitrator accepted_arbitrators = 8; repeated Mediator accepted_mediators = 9; Arbitrator registered_arbitrator = 10; Mediator registered_mediator = 11; PriceAlertFilter price_alert_filter = 12; repeated MarketAlertFilter market_alert_filters = 13; repeated RefundAgent accepted_refund_agents = 14; RefundAgent registered_refund_agent = 15; map cookie = 16; int64 wallet_creation_date = 17; } message BlockChainExplorer { string name = 1; string tx_url = 2; } message PaymentAccount { string id = 1; int64 creation_date = 2 [jstype = JS_STRING]; PaymentMethod payment_method = 3; string account_name = 4; repeated TradeCurrency trade_currencies = 5; TradeCurrency selected_trade_currency = 6; PaymentAccountPayload payment_account_payload = 7; } message PaymentMethod { string id = 1; int64 max_trade_period = 2 [jstype = JS_STRING]; int64 max_trade_limit = 3 [jstype = JS_STRING]; repeated string supported_asset_codes = 4; } message Currency { string currency_code = 1; } message TradeCurrency { string code = 1; string name = 2; oneof message { CryptoCurrency crypto_currency = 3; TraditionalCurrency traditional_currency = 4; } } message CryptoCurrency { bool is_asset = 1; } message TraditionalCurrency { } message Country { string code = 1; string name = 2; Region region = 3; } message Region { string code = 1; string name = 2; } message PriceAlertFilter { string currencyCode = 1; int64 high = 2; int64 low = 3; } message MarketAlertFilter { PaymentAccount payment_account = 1; int32 trigger_value = 2; bool is_buy_offer = 3; repeated string alert_ids = 4; } message MockMailboxPayload { string message = 1; NodeAddress sender_node_address = 2; string uid = 3; } message MockPayload { string message_version = 1; string message = 2; } message PaymentAccountForm { enum FormId { BLOCK_CHAINS = 0; REVOLUT = 1; SEPA = 2; SEPA_INSTANT = 3; TRANSFERWISE = 4; ZELLE = 5; SWIFT = 6; F2F = 7; STRIKE = 8; MONEY_GRAM = 9; FASTER_PAYMENTS = 10; UPHOLD = 11; PAXUM = 12; PAY_BY_MAIL = 13; CASH_AT_ATM = 14; AUSTRALIA_PAYID = 15; CASH_APP = 16; PAYPAL = 17; VENMO = 18; PAYSAFE = 19; WECHAT_PAY = 20; ALI_PAY = 21; SWISH = 22; TRANSFERWISE_USD = 23; AMAZON_GIFT_CARD = 24; ACH_TRANSFER = 25; INTERAC_E_TRANSFER = 26; US_POSTAL_MONEY_ORDER = 27; PIX = 28; } FormId id = 1; repeated PaymentAccountFormField fields = 2; } message PaymentAccountFormField { enum FieldId { ADDRESS = 0; ACCEPTED_COUNTRY_CODES = 1; ACCOUNT_ID = 2; ACCOUNT_NAME = 3; ACCOUNT_NR = 4; ACCOUNT_OWNER = 5; ACCOUNT_TYPE = 6; ANSWER = 7; BANK_ACCOUNT_NAME = 8; BANK_ACCOUNT_NUMBER = 9; BANK_ACCOUNT_TYPE = 10; BANK_ADDRESS = 11; BANK_BRANCH = 12; BANK_BRANCH_CODE = 13; BANK_BRANCH_NAME = 14; BANK_CODE = 15; BANK_COUNTRY_CODE = 16; BANK_ID = 17; BANK_NAME = 18; BANK_SWIFT_CODE = 19; BENEFICIARY_ACCOUNT_NR = 20; BENEFICIARY_ADDRESS = 21; BENEFICIARY_CITY = 22; BENEFICIARY_NAME = 23; BENEFICIARY_PHONE = 24; BIC = 25; BRANCH_ID = 26; CITY = 27; CONTACT = 28; COUNTRY = 29; EMAIL = 30; EMAIL_OR_MOBILE_NR = 31; EXTRA_INFO = 32; HOLDER_ADDRESS = 33; HOLDER_EMAIL = 34; HOLDER_NAME = 35; HOLDER_TAX_ID = 36; IBAN = 37; IFSC = 38; INTERMEDIARY_ADDRESS = 39; INTERMEDIARY_BRANCH = 40; INTERMEDIARY_COUNTRY_CODE = 41; INTERMEDIARY_NAME = 42; INTERMEDIARY_SWIFT_CODE = 43; MOBILE_NR = 44; NATIONAL_ACCOUNT_ID = 45; PAYID = 46; PIX_KEY = 47; POSTAL_ADDRESS = 48; PROMPT_PAY_ID = 49; QUESTION = 50; REQUIREMENTS = 51; SALT = 52; SORT_CODE = 53; SPECIAL_INSTRUCTIONS = 54; STATE = 55; TRADE_CURRENCIES = 56; USERNAME = 57; EMAIL_OR_MOBILE_NR_OR_USERNAME = 58; EMAIL_OR_MOBILE_NR_OR_CASHTAG = 59; } enum Component { TEXT = 0; TEXTAREA = 1; SELECT_ONE = 2; SELECT_MULTIPLE = 3; } FieldId id = 1; Component component = 2; string type = 3; string label = 4; string value = 5; uint32 minLength = 6; uint32 maxLength = 7; repeated TradeCurrency supported_currencies = 8; repeated Country supported_countries = 9; repeated Country supported_sepa_euro_countries = 10; repeated Country supported_sepa_non_euro_countries = 11; repeated string required_for_countries = 12; } ================================================ FILE: relay/Procfile ================================================ web: if [ "$HIDDEN" == true ]; then ./tor/bin/run_tor java -jar -Dserver.port=$PORT build/libs/haveno-relay.jar; else java -jar -Dserver.port=$PORT build/libs/haveno-relay.jar; fi ================================================ FILE: relay/src/main/java/haveno/relay/RelayMain.java ================================================ /* * 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 . */ package haveno.relay; import ch.qos.logback.classic.Level; import haveno.common.app.Log; import haveno.common.util.Utilities; import org.apache.commons.codec.binary.Hex; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.util.Locale; import static spark.Spark.get; import static spark.Spark.port; public class RelayMain { private static final Logger log = LoggerFactory.getLogger(RelayMain.class); private static final String VERSION = "0.1.0"; private static RelayService relayService; static { // Need to set default locale initially otherwise we get problems at non-English OS Locale.setDefault(new Locale("en", Locale.getDefault().getCountry())); } /** * @param args Pass port as program argument if other port than default port 8080 is wanted. */ public static void main(String[] args) { final String logPath = System.getProperty("user.home") + File.separator + "provider"; Log.setup(logPath); Log.setLevel(Level.INFO); log.info("Log files under: " + logPath); log.info("RelayVersion.VERSION: " + VERSION); Utilities.printSysInfo(); String appleCertPwPath; if (args.length > 0) appleCertPwPath = args[0]; else throw new RuntimeException("You need to set the path to the password text file for the Apple push certificate as first argument."); String appleCertPath; if (args.length > 1) appleCertPath = args[1]; else throw new RuntimeException("You need to set the path to the Apple push certificate as second argument."); String appleBundleId; if (args.length > 2) appleBundleId = args[2]; else throw new RuntimeException("You need to set the Apple bundle ID as third argument."); String androidCertPath; if (args.length > 3) androidCertPath = args[3]; else throw new RuntimeException("You need to set the Android certificate path as 4th argument."); int port = 8080; if (args.length > 4) port = Integer.parseInt(args[4]); port(port); relayService = new RelayService(appleCertPwPath, appleCertPath, appleBundleId, androidCertPath); handleRelay(); keepRunning(); } private static void handleRelay() { get("/relay", (request, response) -> { log.info("Incoming relay request from: " + request.userAgent()); boolean isAndroid = request.queryParams("isAndroid").equalsIgnoreCase("true"); boolean useSound = request.queryParams("snd").equalsIgnoreCase("true"); String token = new String(Hex.decodeHex(request.queryParams("token").toCharArray()), "UTF-8"); String encryptedMessage = new String(Hex.decodeHex(request.queryParams("msg").toCharArray()), "UTF-8"); log.info("isAndroid={}\nuseSound={}\napsTokenHex={}\nencryptedMessage={}", isAndroid, useSound, token, encryptedMessage); if (isAndroid) { return relayService.sendAndroidMessage(token, encryptedMessage, useSound); } else { boolean isProduction = request.queryParams("isProduction").equalsIgnoreCase("true"); boolean isContentAvailable = request.queryParams("isContentAvailable").equalsIgnoreCase("true"); return relayService.sendAppleMessage(isProduction, isContentAvailable, token, encryptedMessage, useSound); } }); } private static void keepRunning() { //noinspection InfiniteLoopStatement while (true) { try { Thread.sleep(Long.MAX_VALUE); } catch (InterruptedException ignore) { } } } } ================================================ FILE: relay/src/main/java/haveno/relay/RelayService.java ================================================ /* * 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 . */ package haveno.relay; import com.google.auth.oauth2.GoogleCredentials; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.messaging.FirebaseMessaging; import com.google.firebase.messaging.FirebaseMessagingException; import com.google.firebase.messaging.Message; import com.google.firebase.messaging.Notification; import com.turo.pushy.apns.ApnsClient; import com.turo.pushy.apns.ApnsClientBuilder; import com.turo.pushy.apns.PushNotificationResponse; import com.turo.pushy.apns.util.ApnsPayloadBuilder; import com.turo.pushy.apns.util.SimpleApnsPushNotification; import com.turo.pushy.apns.util.concurrent.PushNotificationFuture; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Scanner; import java.util.concurrent.ExecutionException; class RelayService { private static final Logger log = LoggerFactory.getLogger(RelayMain.class); private static final String ANDROID_DATABASE_URL = "https://havenonotifications.firebaseio.com"; // Used in Haveno app to check for success state. We won't want a code dependency just for that string so we keep it // duplicated in core and here. Must not be changed. private static final String SUCCESS = "success"; private final String appleBundleId; private ApnsClient productionApnsClient; private ApnsClient devApnsClient; // used for iOS development in XCode RelayService(String appleCertPwPath, String appleCertPath, String appleBundleId, String androidCertPath) { this.appleBundleId = appleBundleId; setupForAndroid(androidCertPath); setupForApple(appleCertPwPath, appleCertPath); } private void setupForAndroid(String androidCertPath) { try { InputStream androidCertStream = new FileInputStream(androidCertPath); FirebaseOptions options = new FirebaseOptions.Builder() .setCredentials(GoogleCredentials.fromStream(androidCertStream)) .setDatabaseUrl(ANDROID_DATABASE_URL) .build(); FirebaseApp.initializeApp(options); } catch (IOException e) { log.error(e.toString()); e.printStackTrace(); } } private void setupForApple(String appleCertPwPath, String appleCertPath) { try { InputStream certInputStream = new FileInputStream(appleCertPwPath); Scanner scanner = new Scanner(certInputStream); String password = scanner.next(); productionApnsClient = new ApnsClientBuilder() .setApnsServer(ApnsClientBuilder.PRODUCTION_APNS_HOST) .setClientCredentials(new File(appleCertPath), password) .build(); devApnsClient = new ApnsClientBuilder() .setApnsServer(ApnsClientBuilder.DEVELOPMENT_APNS_HOST) .setClientCredentials(new File(appleCertPath), password) .build(); } catch (IOException e) { log.error(e.toString()); e.printStackTrace(); } } String sendAppleMessage(boolean isProduction, boolean isContentAvailable, String apsTokenHex, String encryptedMessage, boolean useSound) { ApnsPayloadBuilder payloadBuilder = new ApnsPayloadBuilder(); if (useSound) payloadBuilder.setSoundFileName("default"); payloadBuilder.setAlertBody("Haveno notification"); payloadBuilder.setContentAvailable(isContentAvailable); payloadBuilder.addCustomProperty("encrypted", encryptedMessage); final String payload = payloadBuilder.buildWithDefaultMaximumLength(); log.info("payload " + payload); SimpleApnsPushNotification simpleApnsPushNotification = new SimpleApnsPushNotification(apsTokenHex, appleBundleId, payload); ApnsClient apnsClient = isProduction ? productionApnsClient : devApnsClient; PushNotificationFuture> notificationFuture = apnsClient.sendNotification(simpleApnsPushNotification); try { PushNotificationResponse pushNotificationResponse = notificationFuture.get(); if (pushNotificationResponse.isAccepted()) { log.info("Push notification accepted by APNs gateway."); return SUCCESS; } else { String msg1 = "Notification rejected by the APNs gateway: " + pushNotificationResponse.getRejectionReason(); String msg2 = ""; if (pushNotificationResponse.getTokenInvalidationTimestamp() != null) msg2 = " and the token is invalid as of " + pushNotificationResponse.getTokenInvalidationTimestamp(); log.info(msg1 + msg2); return "Error: " + msg1 + msg2; } } catch (InterruptedException | ExecutionException e) { log.error(e.toString()); e.printStackTrace(); return "Error: " + e.toString(); } } String sendAndroidMessage(String apsTokenHex, String encryptedMessage, boolean useSound) { Message.Builder messageBuilder = Message.builder(); Notification notification = new Notification("Haveno", "Notification"); messageBuilder.setNotification(notification); messageBuilder.putData("encrypted", encryptedMessage); messageBuilder.setToken(apsTokenHex); if (useSound) messageBuilder.putData("sound", "default"); Message message = messageBuilder.build(); try { FirebaseMessaging firebaseMessaging = FirebaseMessaging.getInstance(); firebaseMessaging.send(message); return SUCCESS; } catch (FirebaseMessagingException e) { log.error(e.toString()); e.printStackTrace(); return "Error: " + e.toString(); } } } ================================================ FILE: relay/src/main/resources/logback.xml ================================================ %highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{15}: %msg %xEx%n) ================================================ FILE: relay/src/main/resources/version.txt ================================================ 0.0.1-SNAPSHOT ================================================ FILE: relay/torrc ================================================ HiddenServiceDir build/tor-hidden-service HiddenServicePort 80 127.0.0.1:8080 ================================================ FILE: scripts/deployment/haveno-pricenode.env ================================================ JAVA_OPTS="-XX:+ExitOnOutOfMemoryError" ================================================ FILE: scripts/deployment/haveno-pricenode.service ================================================ [Unit] Description=Haveno Price Node After=network.target [Service] SyslogIdentifier=haveno-pricenode EnvironmentFile=/etc/default/haveno-pricenode.env ExecStart=/home/haveno-pricenode/haveno-pricenode/haveno-pricenode 2 ExecStop=/bin/kill -TERM ${MAINPID} Restart=on-failure User=haveno-pricenode Group=haveno-pricenode PrivateTmp=true ProtectSystem=full NoNewPrivileges=true PrivateDevices=true MemoryDenyWriteExecute=false [Install] WantedBy=multi-user.target ================================================ FILE: scripts/deployment/haveno-seednode.service ================================================ [Unit] Description=Haveno seednode After=network.target [Service] User=haveno Group=haveno SyslogIdentifier=Haveno-Seednode ExecStart=/bin/sh /home/haveno/haveno/haveno-seednode --baseCurrencyNetwork=XMR_STAGENET\ --useLocalhostForP2P=false\ --useDevPrivilegeKeys=false\ # Uncomment the following line to use external tor # --hiddenServiceAddress=example.onion\ --nodePort=2002\ --appName=haveno-XMR_STAGENET_Seed_2002\ # --logLevel=trace\ --xmrNode=http://[::1]:38088\ --xmrNodeUsername=admin\ --xmrNodePassword=password ExecStop=/bin/kill ${MAINPID} Restart=always # Hardening PrivateTmp=true ProtectSystem=full NoNewPrivileges=true PrivateDevices=true MemoryDenyWriteExecute=false ProtectControlGroups=true ProtectKernelTunables=true RestrictSUIDSGID=true # limit memory usage to 2gb LimitRSS=2000000000 [Install] WantedBy=multi-user.target ================================================ FILE: scripts/deployment/haveno-seednode2.service ================================================ [Unit] Description=Haveno seednode 2 After=network.target [Service] User=haveno Group=haveno SyslogIdentifier=Haveno-Seednode2 ExecStart=/bin/sh /home/haveno/haveno/haveno-seednode --baseCurrencyNetwork=XMR_STAGENET\ --useLocalhostForP2P=false\ --useDevPrivilegeKeys=false\ # Uncomment the following line to use external tor # --hiddenServiceAddress=example.onion\ --nodePort=2003\ --appName=haveno-XMR_STAGENET_Seed_2003\ # --logLevel=trace\ --xmrNode=http://[::1]:38088\ --xmrNodeUsername=admin\ --xmrNodePassword=password ExecStop=/bin/kill ${MAINPID} Restart=always # Hardening PrivateTmp=true ProtectSystem=full NoNewPrivileges=true PrivateDevices=true MemoryDenyWriteExecute=false ProtectControlGroups=true ProtectKernelTunables=true RestrictSUIDSGID=true # limit memory usage to 2gb LimitRSS=2000000000 [Install] WantedBy=multi-user.target ================================================ FILE: scripts/deployment/monero-stagenet.service ================================================ [Unit] Description=Monero stagenet node After=network.target [Service] User=monero-stagenet Group=monero-stagenet Type=simple ExecStart=/home/monero-stagenet/monerod --config-file /home/monero-stagenet/shared-stagenet.conf --non-interactive SyslogIdentifier=stagenet-node Restart=always # Hardening PrivateTmp=true #ProtectSystem=full NoNewPrivileges=true PrivateDevices=true MemoryDenyWriteExecute=false ProtectControlGroups=true ProtectKernelTunables=true RestrictSUIDSGID=true # limit memory usage to 4gb LimitRSS=4000000000 [Install] WantedBy=multi-user.target ================================================ FILE: scripts/deployment/private-stagenet.conf ================================================ stagenet=1 data-dir=/home/monero-stagenet/private-stagenet/ log-file=/home/monero-stagenet/logs/ p2p-bind-ip=0.0.0.0 p2p-bind-port=38079 hide-my-port=1 no-zmq=1 # RPC #rpc-bind-ip=136.244.105.131 rpc-bind-ip=127.0.0.1 rpc-bind-port=38088 rpc-login=admin:password confirm-external-bind=1 restricted-rpc=0 # must be unrestricted for arbitrator no-igd=1 # second vps peer add-priority-node=45.63.8.26:38080 ================================================ FILE: scripts/deployment/private-stagenet.service ================================================ [Unit] Description=Private stagenet node After=network.target [Service] User=monero-stagenet Group=monero-stagenet Type=simple ExecStart=/home/monero-stagenet/monerod --config-file /home/monero-stagenet/private-stagenet.conf --non-interactive SyslogIdentifier=private-stagenet-node Restart=always # Hardening PrivateTmp=true #ProtectSystem=full NoNewPrivileges=true PrivateDevices=true MemoryDenyWriteExecute=false ProtectControlGroups=true ProtectKernelTunables=true RestrictSUIDSGID=true # limit memory usage to 4gb LimitRSS=4000000000 [Install] WantedBy=multi-user.target ================================================ FILE: scripts/deployment/run-arbitrator-daemon.sh ================================================ #!/bin/bash # # Start arbitrator daemon on Monero's stagenet (Haveno testnet) runArbitrator() { ./haveno-daemon --baseCurrencyNetwork=XMR_STAGENET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=7777 \ --appName=haveno-XMR_STAGENET_arbitrator \ --xmrNode=http://127.0.0.1:38088 \ --xmrNodeUsername=admin \ --xmrNodePassword=password } cd /home/haveno/haveno && \ runArbitrator ================================================ FILE: scripts/deployment/run-arbitrator-gui.sh ================================================ #!/bin/bash # # Start arbitrator GUI on Monero's stagenet (Haveno testnet) runArbitrator() { ./haveno-desktop --baseCurrencyNetwork=XMR_STAGENET \ --useLocalhostForP2P=false \ --useDevPrivilegeKeys=false \ --nodePort=7777 \ --appName=haveno-XMR_STAGENET_arbitrator \ --xmrNode=http://127.0.0.1:38088 \ --xmrNodeUsername=admin \ --xmrNodePassword=password } cd /home/haveno/haveno && \ runArbitrator ================================================ FILE: scripts/deployment/shared-stagenet.conf ================================================ stagenet=1 data-dir=/home/monero-stagenet/shared-stagenet/ log-file=/home/monero-stagenet/logs/ p2p-bind-ip=0.0.0.0 p2p-bind-port=38080 #hide-my-port=1 no-zmq=1 # RPC #rpc-bind-ip=136.244.105.131 rpc-bind-ip=0.0.0.0 rpc-bind-port=38081 confirm-external-bind=1 restricted-rpc=1 no-igd=1 # second vps peer #add-peer=70.34.196.88:38080 ================================================ FILE: scripts/install_java.bat ================================================ :: This script will download and install the appropriate JDK for use with Haveno development. :: It will also configure it as the default system JDK. :: If you need to change to another default JDK for another purpose later, you just need to :: change the JAVA_HOME environment variable. For example, use the following command: :: setx /M JAVA_HOME "" @echo off :: Ensure we have administrative privileges in order to install files and set environment variables >nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32\config\system" if '%errorlevel%' == '0' ( ::If no error is encountered, we have administrative privileges goto GotAdminPrivileges ) echo Requesting administrative privileges... echo Set UAC = CreateObject^("Shell.Application"^) > "%temp%\getadminprivileges.vbs" set params = %*:"="" echo UAC.ShellExecute "%~s0", "%params%", "", "runas", 1 >> "%temp%\getadminprivileges.vbs" "%temp%\getadminprivileges.vbs" exit /B :GotAdminPrivileges if exist "%temp%\getadminprivileges.vbs" ( del "%temp%\getadminprivileges.vbs" ) pushd "%CD%" cd /D "%~dp0" title Install Java set jdk_version=21.0.9 set jdk_build=10 set jdk_filename=OpenJDK21U-jdk_x64_windows_hotspot_%jdk_version%_%jdk_build% set jdk_url=https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.9+10/OpenJDK21U-jdk_x64_windows_hotspot_21.0.9_10.zip if exist "%PROGRAMFILES%\Java\openjdk\jdk-%jdk_version%" ( echo %PROGRAMFILES%\Java\openjdk\jdk-%jdk_version% already exists, skipping install goto SetEnvVars ) echo Downloading required files to %TEMP% powershell -Command "Invoke-WebRequest %jdk_url% -OutFile $env:temp\%jdk_filename%.zip" echo Extracting and installing JDK to %PROGRAMFILES%\Java\openjdk\jdk-%jdk_version% powershell -Command "Expand-Archive $env:temp\%jdk_filename%.zip -DestinationPath %TEMP%\openjdk-%jdk_version% -Force" md "%PROGRAMFILES%\Java\openjdk" move "%TEMP%\openjdk-%jdk_version%\jdk-%jdk_version%" "%PROGRAMFILES%\Java\openjdk" echo Removing downloaded files rmdir /S /Q %TEMP%\openjdk-%jdk_version% del /Q %TEMP%\%jdk_filename%.zip :SetEnvVars echo Setting environment variables powershell -Command "[Environment]::SetEnvironmentVariable('JAVA_HOME', '%PROGRAMFILES%\Java\openjdk\jdk-%jdk_version%', 'Machine')" set java_bin=%%JAVA_HOME%%\bin echo %PATH%|find /i "%java_bin%">nul || powershell -Command "[Environment]::SetEnvironmentVariable('PATH', '%PATH%;%java_bin%', 'Machine')" echo Done! pause ================================================ FILE: scripts/install_java.sh ================================================ #!/usr/bin/env bash # This script will download and install the appropriate JDK for use with Haveno development. # It will also configure it as the default system JDK. # If you need to change to another default JDK for another purpose later, you can use the # following commands and select the default JDK: # Linux: # update-alternatives --config java # update-alternatives --config javac # MacOS: # echo 'export JAVA_HOME=/Library/Java/JavaVirtualMachines//Contents/Home' >>~/.bash_profile # echo 'export PATH=$JAVA_HOME/bin:$PATH' >>~/.bash_profile # source ~/.bash_profile set -e unameOut="$(uname -s)" case "${unameOut}" in Linux*) JAVA_HOME=/usr/lib/jvm/openjdk-21.0.9 JDK_FILENAME=OpenJDK21U-jdk_x64_linux_hotspot_21.0.9_10.tar.gz JDK_URL=https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.9+10/OpenJDK21U-jdk_x64_linux_hotspot_21.0.9_10.tar.gz # Determine which package manager to use depending on the distribution declare -A osInfo; osInfo[/etc/redhat-release]=yum osInfo[/etc/arch-release]=pacman osInfo[/etc/gentoo-release]=emerge osInfo[/etc/SuSE-release]=zypp osInfo[/etc/debian_version]=apt-get for f in "${!osInfo[@]}" do if [[ -f $f ]]; then PACKAGE_MANAGER=${osInfo[$f]} break fi done if [ ! -d "$JAVA_HOME" ]; then # Ensure curl is installed since it may not be $PACKAGE_MANAGER -y install curl curl -L -O $JDK_URL mkdir -p $JAVA_HOME tar -zxf $JDK_FILENAME -C $JAVA_HOME --strip 1 rm $JDK_FILENAME update-alternatives --install /usr/bin/java java $JAVA_HOME/bin/java 2000 update-alternatives --install /usr/bin/javac javac $JAVA_HOME/bin/javac 2000 fi update-alternatives --set java $JAVA_HOME/bin/java update-alternatives --set javac $JAVA_HOME/bin/javac ;; Darwin*) JAVA_HOME=/Library/Java/JavaVirtualMachines/openjdk-21.0.9.jdk/Contents/Home JDK_FILENAME=OpenJDK21U-jdk_x64_mac_hotspot_21.0.9_10.tar.gz JDK_URL=https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.9+10/OpenJDK21U-jdk_x64_mac_hotspot_21.0.9_10.tar.gz if [ ! -d "$JAVA_HOME" ]; then if [[ $(command -v brew) == "" ]]; then echo "Installing Homebrew" /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" else echo "Updating Homebrew" brew update fi brew install curl curl -L -O $JDK_URL sudo mkdir /Library/Java/JavaVirtualMachines/openjdk-21.0.9.jdk | sudo bash gunzip -c $JDK_FILENAME | tar xopf - sudo mv jdk-21.0.9+10/* /Library/Java/JavaVirtualMachines/openjdk-21.0.9.jdk sudo rmdir jdk-21.0.9+10 rm $JDK_FILENAME fi echo export JAVA_HOME=$JAVA_HOME >>~/.bash_profile echo export PATH=$JAVA_HOME/bin:"$PATH" >>~/.bash_profile source "$HOME/.bash_profile" ;; *) esac java -version ================================================ FILE: scripts/install_tails/README.md ================================================ # Install Haveno on Tails After you already have a [Tails USB](https://tails.net/install/linux/index.en.html#download): 1. Enable [persistent storage](https://tails.net/doc/persistent_storage/index.en.html). 2. Set up [administration password](https://tails.net/doc/first_steps/welcome_screen/administration_password/). 3. Activate dotfiles in persistent storage settings. 4. Execute the following command in the terminal to download and execute the installation script. ``` curl -fsSLO https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/haveno-install.sh && bash haveno-install.sh ``` Replace the installer URL and PGP fingerprint for the network you're using. For example: ``` curl -fsSLO https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/haveno-install.sh && bash haveno-install.sh https://github.com/havenoexample/haveno-example/releases/latest/download/haveno-v1.2.3-linux-x86_64-installer.deb FAA24D878B8D36C90120A897CA02DAC12DAE2D0F ``` 5. Start Haveno by finding the icon in the launcher under **Applications > Other**. > [!note] > If you have already installed Haveno on Tails, we recommend moving your data directory (/home/amnesia/Persistent/Haveno-example) to the new default location (/home/amnesia/Persistent/haveno/Data/Haveno-example), to retain your history and for future support. ================================================ FILE: scripts/install_tails/assets/exec.sh ================================================ #!/bin/bash # This script serves as the execution entry point for the Haveno application from a desktop menu icon, # specifically tailored for use in the Tails OS. It is intended to be linked as the 'Exec' command # in a .desktop file, enabling users to start Haveno directly from the desktop interface. # # FUNCTIONAL OVERVIEW: # - Automatic installation and configuration of Haveno if not already set up. # - Linking Haveno data directories to persistent storage to preserve user data across sessions. # # NOTE: # This script assumes that Haveno's related utility scripts and files are correctly placed and accessible # in the specified directories. # Function to print messages in blue echo_blue() { if [ -t 1 ]; then # If File descriptor 1 (stdout) is open and refers to a terminal echo -e "\033[1;34m$1\033[0m" else # If stdout is not a terminal, send a desktop notification notify-send -i "/home/amnesia/Persistent/haveno/App/utils/icon.png" "Starting Haveno" "$1" fi } # Function to print error messages in red echo_red() { if [ -t 1 ]; then # If File descriptor 1 (stdout) is open and refers to a terminal echo -e "\033[0;31m$1\033[0m" else # If stdout is not a terminal, send a desktop notification notify-send -u critical -i "error" "Staring Haveno" "$1\nExiting..." fi } # Define file locations persistence_dir="/home/amnesia/Persistent" data_dir="${persistence_dir}/haveno/Data" # Create data dir mkdir -p "${data_dir}" # Check if Haveno is already installed and configured if [ ! -f "/opt/haveno/bin/Haveno" ] || [ ! -f "/etc/onion-grater.d/haveno.yml" ]; then echo_blue "Installing Haveno and configuring system..." pkexec "${persistence_dir}/haveno/App/utils/install.sh" # Redirect user data to Tails Persistent Storage ln -s "${data_dir}" /home/amnesia/.local/share/Haveno else echo_blue "Haveno is already installed and configured." fi echo_blue "Starting Haveno..." /opt/haveno/bin/Haveno --torControlPort 951 --torControlCookieFile=/var/run/tor/control.authcookie --torControlUseSafeCookieAuth --userDataDir=${data_dir} --useTorForXmr=on --socks5ProxyXmrAddress=127.0.0.1:9062 ================================================ FILE: scripts/install_tails/assets/haveno.desktop ================================================ [Desktop Entry] Name=RetoSwap Comment=A decentralized monero exchange network. Exec=/home/amnesia/Persistent/haveno/App/utils/exec.sh Icon=/home/amnesia/Persistent/haveno/App/utils/icon.png Terminal=false Type=Application Categories=Other MimeType= ================================================ FILE: scripts/install_tails/assets/haveno.yml ================================================ --- - apparmor-profiles: - '/opt/haveno/bin/Haveno' users: - 'amnesia' commands: AUTHCHALLENGE: - 'SAFECOOKIE .*' SETEVENTS: - 'CIRC ORCONN INFO NOTICE WARN ERR HS_DESC HS_DESC_CONTENT' GETINFO: - pattern: 'status/bootstrap-phase' response: - pattern: '250-status/bootstrap-phase=*' replacement: '250-status/bootstrap-phase=NOTICE BOOTSTRAP PROGRESS=100 TAG=done SUMMARY="Done"' - 'net/listeners/socks' ADD_ONION: - pattern: 'NEW:(\S+) Port=9999,(\S+)' replacement: 'NEW:{} Port=9999,{client-address}:{}' - pattern: '(\S+):(\S+) Port=9999,(\S+)' replacement: '{}:{} Port=9999,{client-address}:{}' DEL_ONION: - '.+' HSFETCH: - '.+' events: CIRC: suppress: true ORCONN: suppress: true INFO: suppress: true NOTICE: suppress: true WARN: suppress: true ERR: suppress: true HS_DESC: response: - pattern: '650 HS_DESC CREATED (\S+) (\S+) (\S+) \S+ (.+)' replacement: '650 HS_DESC CREATED {} {} {} redacted {}' - pattern: '650 HS_DESC UPLOAD (\S+) (\S+) .*' replacement: '650 HS_DESC UPLOAD {} {} redacted redacted' - pattern: '650 HS_DESC UPLOADED (\S+) (\S+) .+' replacement: '650 HS_DESC UPLOADED {} {} redacted' - pattern: '650 HS_DESC REQUESTED (\S+) NO_AUTH' replacement: '650 HS_DESC REQUESTED {} NO_AUTH' - pattern: '650 HS_DESC REQUESTED (\S+) NO_AUTH \S+ \S+' replacement: '650 HS_DESC REQUESTED {} NO_AUTH redacted redacted' - pattern: '650 HS_DESC RECEIVED (\S+) NO_AUTH \S+ \S+' replacement: '650 HS_DESC RECEIVED {} NO_AUTH redacted redacted' - pattern: '.*' replacement: '' HS_DESC_CONTENT: suppress: true ================================================ FILE: scripts/install_tails/assets/install.sh ================================================ #!/bin/bash # This script automates the installation and configuration of Haveno on a Tails OS system, # # FUNCTIONAL OVERVIEW: # - Verification of the Haveno installer's presence. # - Installation of the Haveno application with dpkg. # - Removal of automatically created desktop icons to clean up after installation. # - Deployment of Tor configuration for Haveno. # - Restart of the onion-grater service to apply new configurations. # # The script requires administrative privileges to perform system modifications. # Function to print messages in blue echo_blue() { if [ -t 1 ]; then # If File descriptor 1 (stdout) is open and refers to a terminal echo -e "\033[1;34m$1\033[0m" else # If stdout is not a terminal, send a desktop notification notify-send -i "/home/amnesia/Persistent/haveno/App/utils/icon.png" "Starting Haveno" "$1" fi } # Function to print error messages in red echo_red() { if [ -t 1 ]; then # If File descriptor 1 (stdout) is open and refers to a terminal echo -e "\033[0;31m$1\033[0m" else # If stdout is not a terminal, send a desktop notification notify-send -u critical -i "error" "Staring Haveno" "$1\nExiting..." fi } # Define file locations persistence_dir="/home/amnesia/Persistent" app_dir="${persistence_dir}/haveno/App" install_dir="${persistence_dir}/haveno/Install" haveno_installer="${install_dir}/haveno.deb" haveno_config_file="${app_dir}/utils/haveno.yml" # Check if the Haveno installer exists if [ ! -f "${haveno_installer}" ]; then echo_red "Haveno installer not found at ${haveno_installer}." exit 1 fi # Install Haveno echo_blue "Installing Haveno..." dpkg -i "${haveno_installer}" || { echo_red "Failed to install Haveno."; exit 1; } # Remove installed desktop menu icon rm -f /usr/share/applications/haveno-Haveno.desktop # Change access rights for Tor control cookie echo_blue "Changing access rights for Tor control cookie..." chmod o+r /var/run/tor/control.authcookie || { echo_red "Failed to change access rights for Tor control cookie."; exit 1; } # Copy haveno.yml configuration file echo_blue "Copying Tor onion-grater configuration to /etc/onion-grater.d/..." cp "${haveno_config_file}" /etc/onion-grater.d/haveno.yml || { echo_red "Failed to copy haveno.yml."; exit 1; } # Restart onion-grater service echo_blue "Restarting onion-grater service..." systemctl restart onion-grater.service || { echo_red "Failed to restart onion-grater service."; exit 1; } echo_blue "Haveno installation and configuration complete." ================================================ FILE: scripts/install_tails/deprecated/README.md ================================================ # Steps to use (This has serious security concerns to tails threat model only run when you need to access haveno) ## 1. Enable persistent storage and admin password before starting tails ## 2. Get your haveno deb file in persistent storage (amd64 version for tails) ## 3. Edit the path to the haveno deb file if necessary then run ```sudo ./haveno-install.sh``` ## 4. As amnesia run ```source ~/.bashrc``` ## 5. Start haveno using ```haveno-tails``` ## You will need to run this script after each reset, but your data will be saved persistently in /home/amnesia/Persistence/Haveno ================================================ FILE: scripts/install_tails/deprecated/haveno-install.sh ================================================ #!/bin/bash ############################################################################# # Written by BrandyJson, with heavy inspiration from bisq.wiki tails script # ############################################################################# echo "Installing dpkg from persistent, (1.07-1, if this is out of date change the deb path in the script or manually install after running" dpkg -i "/home/amnesia/Persistent/haveno_1.0.7-1_amd64.deb" echo -e "Allowing amnesia to read tor control port cookie, only run this script when you actually want to use haveno\n\n!!! not secure !!!\n" chmod o+r /var/run/tor/control.authcookie echo "Updating apparmor-profile" echo "--- - apparmor-profiles: - '/opt/haveno/bin/Haveno' users: - 'amnesia' commands: AUTHCHALLENGE: - 'SAFECOOKIE .*' SETEVENTS: - 'CIRC ORCONN INFO NOTICE WARN ERR HS_DESC HS_DESC_CONTENT' GETINFO: - pattern: 'status/bootstrap-phase' response: - pattern: '250-status/bootstrap-phase=*' replacement: '250-status/bootstrap-phase=NOTICE BOOTSTRAP PROGRESS=100 TAG=done SUMMARY="Done"' - 'net/listeners/socks' ADD_ONION: - pattern: 'NEW:(\S+) Port=9999,(\S+)' replacement: 'NEW:{} Port=9999,{client-address}:{}' - pattern: '(\S+):(\S+) Port=9999,(\S+)' replacement: '{}:{} Port=9999,{client-address}:{}' DEL_ONION: - '.+' HSFETCH: - '.+' events: CIRC: suppress: true ORCONN: suppress: true INFO: suppress: true NOTICE: suppress: true WARN: suppress: true ERR: suppress: true HS_DESC: response: - pattern: '650 HS_DESC CREATED (\S+) (\S+) (\S+) \S+ (.+)' replacement: '650 HS_DESC CREATED {} {} {} redacted {}' - pattern: '650 HS_DESC UPLOAD (\S+) (\S+) .*' replacement: '650 HS_DESC UPLOAD {} {} redacted redacted' - pattern: '650 HS_DESC UPLOADED (\S+) (\S+) .+' replacement: '650 HS_DESC UPLOADED {} {} redacted' - pattern: '650 HS_DESC REQUESTED (\S+) NO_AUTH' replacement: '650 HS_DESC REQUESTED {} NO_AUTH' - pattern: '650 HS_DESC REQUESTED (\S+) NO_AUTH \S+ \S+' replacement: '650 HS_DESC REQUESTED {} NO_AUTH redacted redacted' - pattern: '650 HS_DESC RECEIVED (\S+) NO_AUTH \S+ \S+' replacement: '650 HS_DESC RECEIVED {} NO_AUTH redacted redacted' - pattern: '.*' replacement: '' HS_DESC_CONTENT: suppress: true" > /etc/onion-grater.d/haveno.yml echo "Adding rule to iptables to allow for monero-wallet-rpc to work" iptables -I OUTPUT 2 -p tcp -d 127.0.0.1 -m tcp --dport 18081 -m owner --uid-owner 1855 -j ACCEPT echo "Updating torsocks to allow for inbound connection" sed -i 's/#AllowInbound/AllowInbound/g' /etc/tor/torsocks.conf echo "Restarting onion-grater service" systemctl restart onion-grater.service echo "alias haveno-tails='torsocks /opt/haveno/bin/Haveno --torControlPort 951 --torControlCookieFile=/var/run/tor/control.authcookie --torControlUseSafeCookieAuth --useTorForXmr=ON --userDataDir=/home/amnesia/Persistent/'" >> /home/amnesia/.bashrc echo -e "Everything is set up just run\n\nsource ~/.bashrc\n\nThen you can start haveno using haveno-tails" ================================================ FILE: scripts/install_tails/haveno-install.sh ================================================ #!/bin/bash # This script facilitates the setup and installation of the Haveno application on Tails OS. # # FUNCTIONAL OVERVIEW: # - Creating necessary persistent directories and copying utility files. # - Downloading Haveno binary, signature file, and GPG key for verification. # - Importing and verifying the GPG key to ensure the authenticity of the download. # - Setting up desktop icons in both local and persistent directories. # Function to print messages in blue echo_blue() { echo -e "\033[1;34m$1\033[0m" } # Function to print error messages in red echo_red() { echo -e "\033[0;31m$1\033[0m" } # Define version and file locations user_url=$1 base_url=$(printf ${user_url} | awk -F'/' -v OFS='/' '{$NF=""}1') expected_fingerprint=$2 binary_filename=$(awk -F'/' '{ print $NF }' <<< "$user_url") package_filename="haveno.deb" signature_filename="${binary_filename}.sig" key_filename="$(printf "$expected_fingerprint" | tr -d ' ' | sed -E 's/.*(................)/\1/' )".asc assets_dir="/tmp/assets" persistence_dir="/home/amnesia/Persistent" app_dir="${persistence_dir}/haveno/App" data_dir="${persistence_dir}/haveno/Data" install_dir="${persistence_dir}/haveno/Install" dotfiles_dir="/live/persistence/TailsData_unlocked/dotfiles" persistent_desktop_dir="$dotfiles_dir/.local/share/applications" local_desktop_dir="/home/amnesia/.local/share/applications" wget_flags="--tries=10 --timeout=10 --waitretry=5 --retry-connrefused --show-progress" # Create temp location for downloads echo_blue "Creating temporary directory for Haveno resources ..." mkdir -p "${assets_dir}" || { echo_red "Failed to create directory ${assets_dir}"; exit 1; } # Download resources echo_blue "Downloading resources for Haveno on Tails ..." wget "${wget_flags}" -cqP "${assets_dir}" https://github.com/retoaccess1/haveno-reto/raw/master/scripts/install_tails/assets/exec.sh || { echo_red "Failed to download resource (exec.sh)."; exit 1; } wget "${wget_flags}" -cqP "${assets_dir}" https://github.com/retoaccess1/haveno-reto/raw/master/scripts/install_tails/assets/install.sh || { echo_red "Failed to download resource (install.sh)."; exit 1; } wget "${wget_flags}" -cqP "${assets_dir}" https://github.com/retoaccess1/haveno-reto/raw/master/scripts/install_tails/assets/haveno.desktop || { echo_red "Failed to resource (haveno.desktop)."; exit 1; } wget "${wget_flags}" -cqP "${assets_dir}" https://raw.githubusercontent.com/retoaccess1/haveno-reto/master/scripts/install_tails/assets/icon.png || { echo_red "Failed to download resource (icon.png)."; exit 1; } wget "${wget_flags}" -cqP "${assets_dir}" https://github.com/retoaccess1/haveno-reto/raw/master/scripts/install_tails/assets/haveno.yml || { echo_red "Failed to download resource (haveno.yml)."; exit 1; } # Create persistent directory echo_blue "Creating persistent directory for Haveno ..." mkdir -p "${app_dir}" || { echo_red "Failed to create directory ${app_dir}"; exit 1; } # Copy utility files to persistent storage and make scripts executable echo_blue "Copying haveno utility files to persistent storage ..." rsync -av "${assets_dir}/" "${app_dir}/utils/" || { echo_red "Failed to rsync files to ${app_dir}/utils/"; exit 1; } find "${app_dir}/utils/" -type f -name "*.sh" -exec chmod +x {} \; || { echo_red "Failed to make scripts executable"; exit 1; } echo_blue "Creating desktop menu icon ..." # Create desktop directories mkdir -p "${local_desktop_dir}" mkdir -p "${persistent_desktop_dir}" # Copy .desktop file to persistent directory cp "${assets_dir}/haveno.desktop" "${persistent_desktop_dir}" || { echo_red "Failed to copy .desktop file to persistent directory $persistent_desktop_dir"; exit 1; } # Create a symbolic link to it in the local .desktop directory, if it doesn't exist if [ ! -L "${local_desktop_dir}/haveno.desktop" ]; then ln -s "${persistent_desktop_dir}/haveno.desktop" "${local_desktop_dir}/haveno.desktop" || { echo_red "Failed to create symbolic link for .desktop file"; exit 1; } fi # Download Haveno binary echo_blue "Downloading Haveno from URL provided ..." wget "${wget_flags}" -cq "${user_url}" || { echo_red "Failed to download Haveno binary."; exit 1; } # Download Haveno signature file echo_blue "Downloading Haveno signature ..." wget "${wget_flags}" -cq "${base_url}""${signature_filename}" || { echo_red "Failed to download Haveno signature."; exit 1; } # Download the GPG key echo_blue "Downloading signing GPG key ..." wget "${wget_flags}" -cqO "${key_filename}" "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$(echo "$expected_fingerprint" | tr -d ' ')" || { echo_red "Failed to download GPG key."; exit 1; } # Import the GPG key echo_blue "Importing the GPG key ..." gpg --import "${key_filename}" || { echo_red "Failed to import GPG key."; exit 1; } # Extract imported fingerprints imported_fingerprints=$(gpg --with-colons --fingerprint | grep -A 1 'pub' | grep 'fpr' | cut -d: -f10 | tr -d '\n') # Remove spaces from the expected fingerprint for comparison formatted_expected_fingerprint=$(echo "${expected_fingerprint}" | tr -d ' ') # Check if the expected fingerprint is in the list of imported fingerprints if [[ ! "${imported_fingerprints}" =~ "${formatted_expected_fingerprint}" ]]; then echo_red "The imported GPG key fingerprint does not match the expected fingerprint." exit 1 fi # Verify the downloaded binary with the signature echo_blue "Verifying the signature of the downloaded file ..." OUTPUT=$(gpg --digest-algo SHA256 --verify "${signature_filename}" "${binary_filename}" 2>&1) if ! echo "$OUTPUT" | grep -q "Good signature from"; then echo_red "Verification failed: $OUTPUT" exit 1; else mv -f "${binary_filename}" "${package_filename}" fi echo_blue "Haveno binaries have been successfully verified." # Move the binary and its signature to the persistent directory mkdir -p "${install_dir}" # Delete old Haveno binaries #rm -f "${install_dir}/"*.deb* mv "${package_filename}" "${key_filename}" "${signature_filename}" "${install_dir}" echo_blue "Files moved to persistent directory ${install_dir}" # Remove stale resources rm -rf "${assets_dir}" # Completed confirmation echo_blue "Haveno installation setup completed successfully." ================================================ FILE: scripts/install_whonix_qubes/INSTALL.md ================================================ # Haveno on Qubes/Whonix ## **Conventions:** + \# – Requires given linux commands to be executed with root privileges either directly as a root user or by use of sudo command + $ or % – Requires given linux commands to be executed as a regular non-privileged user + \ – Used to indicate user supplied variable --- ## **Installation - Scripted & Manual (GUI + CLI):** ### *Acquire release files:* #### In `dispXXXX` AppVM: ##### Clone repository ```shell % git clone --depth=1 https://github.com/haveno-dex/haveno ``` --- ### **Create TemplateVM, NetVM & AppVM:** #### Scripted ##### In `dispXXXX` AppVM: ###### Prepare files for transfer to `dom0` ```shell % tar -C haveno/scripts/install_qubes/scripts/0-dom0 -zcvf /tmp/haveno.tgz . ``` ##### In `dom0`: ###### Copy files to `dom0` ```shell $ mkdir -p /tmp/haveno && qvm-run -p dispXXXX 'cat /tmp/haveno.tgz' > /tmp/haveno.tgz && tar -C /tmp/haveno -zxfv /tmp/haveno.tgz $ bash /tmp/haveno/0.0-dom0.sh && bash /tmp/haveno/0.1-dom0.sh && bash /tmp/haveno/0.2-dom0.sh ``` #### GUI ##### TemplateVM ###### Via `Qubes Manager`: + Locate & highlight whonix-workstation-17 (TemplateVM) + Right-Click "whonix-workstation-17" and select "Clone qube" from Drop-Down + Enter "haveno-template" in "Name" + Click OK Button ##### NetVM ###### Via `Qubes Manager`: + Click "New qube" Button + Enter "sys-haveno" for "Name and label" + Click the Button Beside "Name and label" and Select "orange" + Select "whonix-gateway-17" from "Template" Drop-Down + Select "sys-firewall" from "Networking" Drop-Down + Tick "Launch settings after creation" Radio-Box + Click OK + Click "Advanced" Tab + Enter "512" for "Initial memory"

    (Within reason, can adjust to personal preference)

    + Enter "512" for "Max memory"

    (Within reason, can adjust to personal preference)

    + Tick "Provides network" Radio-Box + Click "Apply" Button + Click "OK" Button ##### AppVM ###### Via `Qubes Manager`: + Click "New qube" Button + Enter "haveno" for "Name and label" + Click the Button Beside "Name and label" and Select "orange" + Select "haveno-template" from "Template" Drop-Down + Select "sys-haveno" from "Networking" Drop-Down + Tick "Launch settings after creation" Radio-Box + Click OK + Click "Advanced" Tab + Enter "2048" for "Initial memory"

    (Within reason, can adjust to personal preference)

    + Enter "4096" for "Max memory"

    (Within reason, can adjust to personal preference)

    + Click "Apply" Button + Click "OK" Button #### CLI ##### TemplateVM ###### In `dom0`: ```shell $ qvm-clone whonix-workstation-17 haveno-template ``` ##### NetVM ##### In `dom0`: ```shell $ qvm-create --template whonix-gateway-17 --class AppVM --label=orange --property memory=512 --property maxmem=512 --property netvm=sys-firewall sys-haveno && qvm-prefs --set sys-haveno provides_network True ``` #### AppVM ##### In `dom0`: ```shell $ qvm-create --template haveno-template --class AppVM --label=orange --property memory=2048 --property maxmem=4096 --property netvm=sys-haveno haveno $ printf 'haveno-Haveno.desktop' | qvm-appmenus --set-whitelist – haveno ``` --- ### **Build TemplateVM, NetVM & AppVM:** #### *TemplateVM Using Precompiled Package via `git` Repository (Scripted)* ##### In `dispXXXX` AppVM: ```shell % qvm-copy haveno/scripts/install_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh ``` + Select "haveno-template" for "Target" of Pop-Up + Click OK ##### In `haveno-template` TemplateVM: ```shell % sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "" "" ```

    Example:

    ```shell % sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "https://github.com/havenoexample/haveno-example/releases/download/1.2.3/haveno-v1.2.3-linux-x86_64-installer.deb" "ABAF11C65A2970B130ABE3C479BE3E4300411886" ``` #### *TemplateVM Using Precompiled Package From `git` Repository (CLI)* ##### In `haveno-template` TemplateVM: ###### Download & Import Project PGP Key

    For Whonix On Qubes OS:

    ```shell # export https_proxy=http://127.0.0.1:8082 # export KEY_SEARCH="" # curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_SEARCH" | gpg --import ```

    Example:

    ```shell # export https_proxy=http://127.0.0.1:8082 # export KEY_SEARCH="ABAF11C65A2970B130ABE3C479BE3E4300411886" # curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_SEARCH" | gpg --import ```

    For Whonix On Anything Other Than Qubes OS:

    ```shell # export KEY_SEARCH="" # curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_SEARCH" | gpg --import ```

    Example:

    ```shell # export KEY_SEARCH="ABAF11C65A2970B130ABE3C479BE3E4300411886" # curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_SEARCH" | gpg --import ``` ###### Download Release Files

    For Whonix On Qubes OS:

    ```shell # export https_proxy=http://127.0.0.1:8082 # curl -sSLo /tmp/haveno.deb https://github.com/havenoexample/haveno-example/releases/download/1.2.0/haveno-v1.2.0-linux-x86_64-installer.deb # curl -sSLo /tmp/haveno.deb.sig https://github.com/havenoexample/haveno-example/releases/download/1.2.0/haveno-v1.2.0-linux-x86_64-installer.deb.sig # curl -sSLo /tmp/haveno-jar.SHA-256 https://github.com/havenoexample/haveno-example/releases/download/1.2.0/haveno-v1.2.0-linux-x86_64-SNAPSHOT-all.jar.SHA-256 ```

    Note:

    Above are dummy URLS which MUST be replaced with actual working URLs

    For Whonix On Anything Other Than Qubes OS:

    ```shell # curl -sSLo /tmp/haveno.deb https://github.com/havenoexample/haveno-example/releases/download/1.2.0/haveno-v1.2.0-linux-x86_64-installer.deb # curl -sSLo /tmp/haveno.deb.sig https://github.com/havenoexample/haveno-example/releases/download/1.2.0/haveno-v1.2.0-linux-x86_64-installer.deb.sig # curl -sSLo /tmp/haveno-jar.SHA-256 https://github.com/havenoexample/haveno-example/releases/download/1.2.0/haveno-v1.2.0-linux-x86_64-SNAPSHOT-all.jar.SHA-256 ```

    Note:

    Above are dummy URLS which MUST be replaced with actual working URLs

    ###### Verify & Install Package File ```shell # if gpg --digest-algo SHA256 --verify /tmp/haveno.deb.sig >/dev/null 2>&1; then printf $'PACKAGE file has a VALID signature!\n' && mkdir -p /usr/share/desktop-directories && apt install -y /tmp/haveno*.deb; else printf $'PACKAGE failed signature check\n' && sleep 5 && exit 1; fi ``` ###### Verify Jar ```shell # if [[ $(cat /tmp/haveno-jar.SHA-256) =~ $(sha256sum /opt/haveno/lib/app/desktop*.jar | awk '{ print $1 }') ]] ; then printf $'SHA Hash IS valid!\n' && printf 'Happy trading!\n'; else printf $'WARNING: Bad Hash!\n' && exit; fi ``` #### *TemplateVM Building From Source via `git` Repository (Scripted)* ##### In `dispXXXX` AppVM: ```shell % bash haveno/scripts/install_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh "" "" "" ```

    Example:

    ```shell % bash haveno/scripts/install_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh "https://download.bell-sw.com/java/21.0.6+10/bellsoft-jdk21.0.6+10-linux-amd64.deb" "a5e3fd9f5323de5fc188180c91e0caa777863b5b" "https://github.com/haveno-dex/haveno" ``` + Upon Successful Compilation & Packaging, A `Filecopy` Confirmation Will Be Presented + Select "haveno-template" for "Target" of Pop-Up + Click OK ##### In `haveno-template` TemplateVM: ```shell % sudo apt install -y ./QubesIncoming/dispXXXX/haveno.deb ``` #### *NetVM (Scripted)* ##### In `dispXXXX` AppVM: ```shell $ qvm-copy haveno/scripts/install_qubes/scripts/2-NetVM/2.0-haveno-netvm.sh ``` + Select "sys-haveno" for "Target" Within Pop-Up + Click "OK" Button ##### In `sys-haveno` NetVM: (Allow bootstrap process to complete) ```shell % sudo zsh QubesIncoming/dispXXXX/2.0-haveno-netvm.sh ``` #### *NetVM (CLI)* ##### In `sys-haveno` NetVM: ###### Add `onion-grater` Profile ```shell # onion-grater-add 40_haveno ``` ###### Restart `onion-grater` Service ```shell # systemctl restart onion-grater.service # poweroff ``` #### *AppVM (Scripted)* ##### In `dispXXXX` AppVM: ```shell $ qvm-copy haveno/scripts/install_qubes/scripts/3-AppVM/3.0-haveno-appvm.sh ``` + Select "haveno" for "Target" of Pop-Up + Click OK ##### In `haveno` AppVM: ```shell % sudo zsh QubesIncoming/dispXXXX/3.0-haveno-appvm.sh ``` #### *AppVM (CLI)* ##### In `haveno` AppVM: ###### Adjust `sdwdate` Configuration ```shell # mkdir /usr/local/etc/sdwdate-gui.d # printf "gateway=sys-haveno\n" > /usr/local/etc/sdwdate-gui.d/50_user.conf # systemctl restart sdwdate ``` ###### Prepare Firewall Settings via `/rw/config/rc.local` ```shell # printf "\n# Prepare Local FW Settings\nmkdir -p /usr/local/etc/whonix_firewall.d\n" >> /rw/config/rc.local # printf "\n# Poke FW\nprintf \"EXTERNAL_OPEN_PORTS+=\\\\\" 9999 \\\\\"\\\n\" | tee /usr/local/etc/whonix_firewall.d/50_user.conf\n" >> /rw/config/rc.local # printf "\n# Restart FW\nwhonix_firewall\n\n" >> /rw/config/rc.local ``` ###### View & Verify Change ```shell # tail /rw/config/rc.local ```

    Confirm output contains:

    > # Poke FW > printf "EXTERNAL_OPEN_PORTS+=\" 9999 \"\n" | tee /usr/local/etc/whonix_firewall.d/50_user.conf > > # Restart FW > whonix_firewall ###### Restart `whonix_firewall` ```shell # whonix_firewall ``` ###### Create `haveno-Haveno.desktop` ```shell # mkdir -p /home/$(ls /home)/\.local/share/applications # sed 's|/opt/haveno/bin/Haveno|/opt/haveno/bin/Haveno --torControlPort=9051 --socks5ProxyXmrAddress=127.0.0.1:9050 --useTorForXmr=on|g' /opt/haveno/lib/haveno-Haveno.desktop > /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop # chown -R $(ls /home):$(ls /home) /home/$(ls /home)/.local/share/applications ``` ###### View & Verify Change ```shell # tail /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop ```

    Confirm output contains:

    > [Desktop Entry] > Name=Haveno > Comment=Haveno > Exec=/opt/haveno/bin/Haveno --torControlPort=9051 --socks5ProxyXmrAddress=127.0.0.1:9050 --useTorForXmr=on > Icon=/opt/haveno/lib/Haveno.png > Terminal=false > Type=Application > Categories=Network > MimeType= ###### Poweroff ```shell # poweroff ``` ### **Remove TemplateVM, NetVM & AppVM:** #### Scripted ##### In `dom0`: ```shell $ bash /tmp/haveno/0.3-dom0.sh ``` #### GUI ##### Via `Qubes Manager`: + Highlight "haveno" (AppVM) + Click "Delete qube" + Enter "haveno" + Click "OK" Button + Highlight "haveno-template" (TemplateVM) + Click "Delete qube" + Enter "haveno-template" + Click "OK" Button + Highlight "sys-haveno" (NetVM) + Click "Delete qube" + Enter "sys-haveno" + Click "OK" Button #### CLI ##### In `dom0`: ```shell $ qvm-shutdown --force --quiet haveno haveno-template sys-haveno && qvm-remove --force --quiet haveno haveno-template sys-haveno ``` ================================================ FILE: scripts/install_whonix_qubes/README.md ================================================ # Install Haveno on Qubes/Whonix After you already have [`Qubes`](https://www.qubes-os.org/downloads) or [`Whonix`](https://www.whonix.org/wiki/Download) installed: 1. Download [scripts](https://github.com/haveno-dex/haveno/tree/master/scripts/install_whonix_qubes/scripts). 2. Move script(s) to their respective destination (`0.*-dom0.sh` -> `dom0`, `1.0-haveno-templatevm.sh` -> `haveno-template`, etc.). 3. Consecutively execute the following commands in their respective destinations. --- ## **Create VMs** [`Qubes`](https://www.qubes-os.org/downloads) ### **In `dom0`:** ```shell $ bash 0.0-dom0.sh && bash 0.1-dom0.sh && bash 0.2-dom0.sh ``` [`Whonix`](https://www.whonix.org/wiki/Download) On Anything Other Than [`Qubes`](https://www.qubes-os.org/downloads) - Clone `Whonix Workstation` To VM Named `haveno-template` - Clone `Whonix Gateway` To VM Named `sys-haveno` - Create New Linked VM Clone Based On `haveno-template` Named `haveno` ## **Build TemplateVM** ### *Via Package* #### **In `haveno-template` `TemplateVM`:** ```shell % sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "" "" ```

    Example:

    ```shell % sudo bash 1.0-haveno-templatevm.sh "https://github.com/havenoexample/haveno-example/releases/download/1.2.0/haveno-v1.2.0-linux-x86_64-installer.deb" "ABAF11C65A2970B130ABE3C479BE3E4300411886" ``` ### *Via Source* #### **In `dispXXXX` `AppVM`:** ```shell % bash 1.0-haveno-templatevm.sh "" "" "" ```

    Example:

    ```shell % bash 1.0-haveno-templatevm.sh "https://download.bell-sw.com/java/21.0.6+10/bellsoft-jdk21.0.6+10-linux-amd64.deb" "a5e3fd9f5323de5fc188180c91e0caa777863b5b" "https://github.com/haveno-dex/haveno" ``` #### **In `haveno-template` `TemplateVM`:** ```shell % sudo apt install -y haveno.deb ``` ## **Build NetVM** ### **In `sys-haveno` `NetVM`:** ```shell % sudo zsh 3.0-haveno-appvm.sh ``` ## **Build AppVM** ### **In `haveno` `AppVM`:** ```shell % sudo zsh 3.0-haveno-appvm.sh ``` --- Complete Documentation Can Be Found [Here](https://github.com/haveno-dex/haveno/blob/master/scripts/install_whonix_qubes/INSTALL.md). ================================================ FILE: scripts/install_whonix_qubes/scripts/0-dom0/0.0-dom0.sh ================================================ #!/bin/bash ## ./haveno-on-qubes/scripts/0.0-dom0.sh ## Create Haveno TemplateVM: qvm-clone whonix-workstation-17 haveno-template ================================================ FILE: scripts/install_whonix_qubes/scripts/0-dom0/0.1-dom0.sh ================================================ #!/bin/bash ## ./haveno-on-qubes/scripts/0.1-dom0.sh ## Create Haveno NetVM: qvm-create --template whonix-gateway-17 --class AppVM --label=orange --property memory=512 --property maxmem=512 --property netvm=sys-firewall sys-haveno && qvm-prefs --set sys-haveno provides_network True ================================================ FILE: scripts/install_whonix_qubes/scripts/0-dom0/0.2-dom0.sh ================================================ #!/bin/bash ## ./haveno-on-qubes/scripts/0.2-dom0.sh ## Create Haveno AppVM: qvm-create --template haveno-template --class AppVM --label=orange --property memory=2048 --property maxmem=4096 --property netvm=sys-haveno haveno printf 'haveno-Haveno.desktop' | qvm-appmenus --set-whitelist - haveno ================================================ FILE: scripts/install_whonix_qubes/scripts/0-dom0/0.3-dom0.sh ================================================ #!/bin/bash ## ./haveno-on-qubes/scripts/0.3-dom0.sh ## Remove Haveno GuestVMs qvm-shutdown --force --quiet haveno haveno-template sys-haveno && qvm-remove --force --quiet haveno haveno-template sys-haveno ================================================ FILE: scripts/install_whonix_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh ================================================ #!/bin/bash ## ./haveno-on-qubes/scripts/1.1-haveno-templatevm_maker.sh function remote { if [[ -z $PACKAGE_URL || -z $FINGERPRINT ]]; then printf "\nNo arguments provided!\n\nThis script requires two arguments to be provided:\nPackage URL & PGP Fingerprint\n\nPlease review documentation and try again.\n\nExiting now ...\n" exit 1 fi ## Update & Upgrade apt update && apt upgrade -y ## Install wget apt install -y wget ## Function to print messages in blue: echo_blue() { echo -e "\033[1;34m$1\033[0m" } # Function to print error messages in red: echo_red() { echo -e "\033[0;31m$1\033[0m" } ## Sweep for old release files rm *.asc desktop-*-SNAPSHOT-all.jar.SHA-256 haveno* ## Define URL & PGP Fingerprint etc. vars: user_url=$PACKAGE_URL base_url=$(printf ${user_url} | awk -F'/' -v OFS='/' '{$NF=""}1') expected_fingerprint=$FINGERPRINT package_filename=$(awk -F'/' '{ print $NF }' <<< "$user_url") signature_filename="${package_filename}.sig" key_filename="$(printf "$expected_fingerprint" | tr -d ' ' | sed -E 's/.*(................)/\1/' )".asc wget_flags="--tries=10 --timeout=10 --waitretry=5 --retry-connrefused --show-progress" ## Debug: printf "\nUser URL=$user_url\n" printf "\nBase URL=$base_url\n" printf "\nFingerprint=$expected_fingerprint\n" printf "\nPackage Name=$package_filename\n" printf "\nSig Filename=$signature_filename\n" printf "\nKey Filename=$key_filename\n" ## Configure for tinyproxy: export https_proxy=http://127.0.0.1:8082 ## Download Haveno binary: echo_blue "Downloading Haveno from URL provided ..." wget "${wget_flags}" -cq "${user_url}" || { echo_red "Failed to download Haveno binary."; exit 1; } ## Download Haveno signature file: echo_blue "Downloading Haveno signature ..." wget "${wget_flags}" -cq "${base_url}""${signature_filename}" || { echo_red "Failed to download Haveno signature."; exit 1; } ## Download the GPG key: echo_blue "Downloading signing GPG key ..." wget "${wget_flags}" -cqO "${key_filename}" "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$(echo "$expected_fingerprint" | tr -d ' ')" || { echo_red "Failed to download GPG key."; exit 1; } ## Import the GPG key: echo_blue "Importing the GPG key ..." gpg --import "${key_filename}" || { echo_red "Failed to import GPG key."; exit 1; } ## Extract imported fingerprints: imported_fingerprints=$(gpg --with-colons --fingerprint | grep -A 1 'pub' | grep 'fpr' | cut -d: -f10 | tr -d '\n') ## Remove spaces from the expected fingerprint for comparison: formatted_expected_fingerprint=$(echo "${expected_fingerprint}" | tr -d ' ') ## Check if the expected fingerprint is in the list of imported fingerprints: if [[ ! "${imported_fingerprints}" =~ "${formatted_expected_fingerprint}" ]]; then echo_red "The imported GPG key fingerprint does not match the expected fingerprint." exit 1 fi ## Verify the downloaded binary with the signature: echo_blue "Verifying the signature of the downloaded file ..." if gpg --digest-algo SHA256 --verify "${signature_filename}" >/dev/null 2>&1; then mkdir -p /usr/share/desktop-directories; else echo_red "Verification failed!" && sleep 5 exit 1; fi echo_blue "Haveno binaries have been successfully verified." # Install Haveno: echo_blue "Installing Haveno ..." apt install -y ./"${package_filename}" || { echo_red "Failed to install Haveno."; exit 1; } ## Finalize echo_blue "Haveno TemplateVM installation and configuration complete." echo_blue "\nHappy Trading\!\n" printf "%s \n" "Press [ENTER] to complete ..." read ans #exit poweroff } function build { if [[ -z $JAVA_URL || -z $JAVA_SHA1 || -z $SOURCE_URL ]]; then printf "\nNo arguments provided!\n\nThis script requires three argument to be provided:\n\nURL for Java 21 JDK Debian Package\n\nSHA1 Hash for Java 21 JDK Debian Package\n\nURL for Remote Git Source Repository\n\nPlease review documentation and try again.\n\nExiting now ...\n" exit 1 fi # Dependancies sudo apt install -y make git expect fakeroot binutils # Java curl -fsSLo jdk21.deb ${JAVA_URL} if [[ $(shasum ./jdk21.deb | awk '{ print $1 }') == ${JAVA_SHA1} ]] ; then printf $'SHA Hash IS valid!\n'; else printf $'WARNING: Bad Hash!\n' && exit; fi sudo apt install -y ./jdk21.deb # Build git clone --depth=1 $SOURCE_URL GIT_DIR=$(awk -F'/' '{ print $NF }' <<< "$SOURCE_URL") cd ${GIT_DIR} git checkout master sed -i 's|XMR_STAGENET|XMR_MAINNET|g' desktop/package/package.gradle ./gradlew clean build --refresh-keys --refresh-dependencies # Package # Expect cat <> /tmp/haveno_package_deb.exp set send_slow {1 .1} proc send {ignore arg} { sleep 1.1 exp_send -s -- \$arg } set timeout -1 spawn ./gradlew packageInstallers --console=plain match_max 100000 expect -exact "" send -- "y\r" expect -exact "" send -- "y\r" expect -exact "" send -- "y\r" expect -exact "app-image" send -- \x03 expect eof DONE # Package expect -f /tmp/haveno_package_deb.exp && find ./ -name '*.deb' -exec qvm-copy {} \; printf "\nHappy Trading!\n" } if ! [[ $# -eq 2 || $# -eq 3 ]] ; then printf "\nFor this script to function, user supplied arguments are required.\n\n" printf "\nPlease review documentation and try again.\n\n" fi if [[ $# -eq 2 ]] ; then PACKAGE_URL=$1 FINGERPRINT=$2 remote fi if [[ $# -eq 3 ]] ; then JAVA_URL=$1 JAVA_SHA1=$2 SOURCE_URL=$3 build fi ================================================ FILE: scripts/install_whonix_qubes/scripts/2-NetVM/2.0-haveno-netvm.sh ================================================ #!/bin/zsh ## ./haveno-on-qubes/scripts/2.0-haveno-netvm_taker.sh ## Function to print messages in blue: echo_blue() { echo -e "\033[1;34m$1\033[0m" } # Function to print error messages in red: echo_red() { echo -e "\033[0;31m$1\033[0m" } ## onion-grater # Add onion-grater Profile echo_blue "\nAdding onion-grater Profile ..." onion-grater-add 40_haveno # Restart onion-grater echo_blue "\nRestarting onion-grater Service ..." systemctl restart onion-grater.service echo_blue "Haveno NetVM configuration complete." printf "%s \n" "Press [ENTER] to complete ..." read ans #exit poweroff ================================================ FILE: scripts/install_whonix_qubes/scripts/3-AppVM/3.0-haveno-appvm.sh ================================================ #!/bin/zsh ## ./haveno-on-qubes/scripts/3.0-haveno-appvm_taker.sh ## Function to print messages in blue: echo_blue() { echo -e "\033[1;34m$1\033[0m" } # Function to print error messages in red: echo_red() { echo -e "\033[0;31m$1\033[0m" } ## Adjust sdwdate Configuration mkdir -p /usr/local/etc/sdwdate-gui.d printf "gateway=sys-haveno\n" > /usr/local/etc/sdwdate-gui.d/50_user.conf systemctl restart sdwdate ## Prepare Firewall Settings echo_blue "\nConfiguring FW ..." printf "\n# Prepare Local FW Settings\nmkdir -p /usr/local/etc/whonix_firewall.d\n" >> /rw/config/rc.local printf "\n# Poke FW\nprintf \"EXTERNAL_OPEN_PORTS+=\\\\\" 9999 \\\\\"\\\n\" | tee /usr/local/etc/whonix_firewall.d/50_user.conf\n" >> /rw/config/rc.local printf "\n# Restart FW\nwhonix_firewall\n\n" >> /rw/config/rc.local ## View & Verify Change echo_blue "\nReview the following output and be certain in matches documentation!\n" tail /rw/config/rc.local printf "%s \n" "Press [ENTER] to continue ..." read ans : ## Restart FW echo_blue "\nRestarting Whonix FW ..." whonix_firewall ### Create Desktop Launcher: echo_blue "Creating desktop launcher ..." mkdir -p /home/$(ls /home)/\.local/share/applications sed 's|/opt/haveno/bin/Haveno|/opt/haveno/bin/Haveno --torControlPort=9051 --socks5ProxyXmrAddress=127.0.0.1:9050 --useTorForXmr=on|g' /opt/haveno/lib/haveno-Haveno.desktop > /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop chown -R $(ls /home):$(ls /home) /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop ## View & Verify Change echo_blue "\nReview the following output and be certain in matches documentation!\n" tail /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop printf "%s \n" "Press [ENTER] to continue ..." read ans : echo_blue "Haveno AppVM configuration complete." echo_blue "Refresh applications via Qubes Manager GUI now." printf "%s \n" "Press [ENTER] to complete ..." read ans #exit poweroff ================================================ FILE: seednode/.dockerignore ================================================ docs/ .git/ .dockerignore .editorconfig .travis.yml docker-compose.yml docker/development/ docker/prod/ docker/README.md # Gradle .gradle build # IDEA .idea *.iml # macOS .DS_Store # Vim *.sw[op] ================================================ FILE: seednode/README.md ================================================ # Haveno Seednode Currently a seednode can be deployed using the Dockerfile in the `docker/` folder. Make sure you have Tor installed in your host environment (`apt install tor`), then navigate to the `docker` folder and from there build the Docker image: ``` docker build -t haveno-seednode . ``` Then create a container from it: ``` docker run -it -p 9050 -p 2002 --restart unless-stopped --name haveno-seednode haveno-seednode ``` After the seednode is deployed, you'll see a message similar to this in the log: ``` [TorControlParser] INFO o.b.n.tor.Tor: Hidden Service 3jrnfkgkoh463zic54csvntz5w62dm2zno54c3c6jgvusafosqrgmnqd.onion has been announced to the Tor network. ``` Note the onion address. It will be needed by the Haveno instances wanting to connect to your seednode. ================================================ FILE: seednode/blocknotify.sh ================================================ #!/bin/sh echo $1 | nc -w 1 127.0.0.1 5120 ================================================ FILE: seednode/create_jar.sh ================================================ #!/bin/bash ./gradlew build -x test shadowJar ================================================ FILE: seednode/create_jaronly_archive.sh ================================================ #!/bin/sh set -x ./gradlew build -x test tar zvcf haveno-seednode-jaronly.tgz \ seednode/build/app/lib/assets.jar \ seednode/build/app/lib/common.jar \ seednode/build/app/lib/core.jar \ seednode/build/app/lib/p2p.jar \ seednode/build/app/lib/seednode.jar ls -la haveno-seednode-jaronly.tgz exit 0 ================================================ FILE: seednode/docker/Dockerfile ================================================ # docker run -it -p 9050 -p 2002 --restart-policy unless-stopped --name haveno-seednode haveno-seednode # TODO: image very heavy, but it's hard to significantly reduce the size without bins FROM eclipse-temurin:21 RUN set -ex && \ apt update && \ apt --no-install-recommends --yes install \ make \ git \ tor RUN set -ex && adduser --system --group --disabled-password haveno && \ mkdir -p /home/haveno && \ chown -R haveno:haveno /home/haveno USER haveno WORKDIR /home/haveno RUN set -ex && git clone https://github.com/haveno-dex/haveno.git && \ cd haveno && \ make skip-tests WORKDIR /home/haveno/haveno ENTRYPOINT [ "./haveno-seednode" ] CMD ["--baseCurrencyNetwork=XMR_STAGENET", "--useLocalhostForP2P=false", "--useDevPrivilegeKeys=false", "--nodePort=2002", "--appName=haveno-XMR_STAGENET_Seed_2002" ] ================================================ FILE: seednode/haveno-seednode.service ================================================ [Unit] Description=Haveno seednode After=network.target [Service] User=haveno Group=haveno SyslogIdentifier=Haveno-Seednode # $PATH is a placeholder ExecStart=/bin/sh $PATH/haveno-seednode --baseCurrencyNetwork=XMR_STAGENET\ --useLocalhostForP2P=false\ --useDevPrivilegeKeys=false\ # Uncomment the following line to use external tor # --hiddenServiceAddress=example.onion\ --nodePort=2002\ --appName=haveno-XMR_STAGENET_Seed_2002\ --xmrNode=http://[::1]:38088 ExecStop=/bin/kill ${MAINPID} ; sleep 5 Restart=always # Hardening PrivateTmp=true ProtectSystem=full NoNewPrivileges=true PrivateDevices=true MemoryDenyWriteExecute=false ProtectControlGroups=true ProtectKernelTunables=true RestrictSUIDSGID=true [Install] WantedBy=multi-user.target ================================================ FILE: seednode/haveno.env ================================================ # configuration for haveno service # install in /etc/default/haveno.env # java home, set to latest openjdk 21 from os repository JAVA_HOME=/usr/lib/jvm/openjdk-21 # java memory and remote management options JAVA_OPTS="-Xms4096M -Xmx4096M -XX:+ExitOnOutOfMemoryError" # use external tor (change to -1 for internal tor binary) HAVENO_EXTERNAL_TOR_PORT=9051 # bitcoin rpc credentials BITCOIN_RPC_USER=__BITCOIN_RPC_USER__ BITCOIN_RPC_PASS=__BITCOIN_RPC_PASS__ # bitcoin p2p settings BITCOIN_P2P_HOST=__BITCOIN_P2P_HOST__ BITCOIN_P2P_PORT=__BITCOIN_P2P_PORT__ # bitcoind rpc ports BITCOIN_RPC_HOST=__BITCOIN_RPC_HOST__ BITCOIN_RPC_PORT=__BITCOIN_RPC_PORT__ BITCOIN_RPC_BLOCKNOTIFY_HOST=127.0.0.1 BITCOIN_RPC_BLOCKNOTIFY_PORT=5120 # haveno pathnames HAVENO_HOME=__HAVENO_HOME__ HAVENO_APP_NAME=haveno-seednode HAVENO_ENTRYPOINT=haveno-seednode HAVENO_BASE_CURRENCY=xmr_mainnet # haveno node settings HAVENO_NODE_PORT=8000 HAVENO_MAX_CONNECTIONS=20 HAVENO_MAX_MEMORY=4000 ================================================ FILE: seednode/install_seednode_debian.sh ================================================ #!/bin/sh set -e echo "[*] Haveno Seednode installation script" ##### change paths if necessary for your system ROOT_USER=root ROOT_GROUP=root ROOT_PKG="build-essential libtool autotools-dev automake pkg-config bsdmainutils python3 git vim screen ufw openjdk-21-jdk" ROOT_HOME=/root SYSTEMD_SERVICE_HOME=/etc/systemd/system SYSTEMD_ENV_HOME=/etc/default HAVENO_REPO_URL=https://github.com/haveno-dex/haveno HAVENO_REPO_NAME=haveno HAVENO_REPO_TAG=master HAVENO_LATEST_RELEASE=$(curl -s https://api.github.com/repos/haveno-dex/haveno/releases/latest|grep tag_name|head -1|cut -d '"' -f4) HAVENO_HOME=/haveno HAVENO_USER=haveno # by default, this script will not build and setup bitcoin full-node BITCOIN_INSTALL=false BITCOIN_REPO_URL=https://github.com/bitcoin/bitcoin BITCOIN_REPO_NAME=bitcoin BITCOIN_REPO_TAG=$(curl -s https://api.github.com/repos/bitcoin/bitcoin/releases/latest|grep tag_name|head -1|cut -d '"' -f4) BITCOIN_HOME=/bitcoin BITCOIN_USER=bitcoin BITCOIN_GROUP=bitcoin BITCOIN_PKG="libevent-dev libboost-system-dev libboost-filesystem-dev libboost-chrono-dev libboost-test-dev libboost-thread-dev libdb-dev libssl-dev" BITCOIN_P2P_HOST=127.0.0.1 BITCOIN_P2P_PORT=8333 BITCOIN_RPC_HOST=127.0.0.1 BITCOIN_RPC_PORT=8332 # set below settings to use existing bitcoin node #BITCOIN_INSTALL=false #BITCOIN_P2P_HOST=192.168.1.1 #BITCOIN_P2P_PORT=8333 #BITCOIN_RPC_HOST=192.168.1.1 #BITCOIN_RPC_PORT=8332 #BITCOIN_RPC_USER=foo #BITCOIN_RPC_PASS=bar TOR_PKG="tor deb.torproject.org-keyring" TOR_USER=debian-tor TOR_GROUP=debian-tor TOR_HOME=/etc/tor ##### echo "[*] Updating apt repo sources" sudo -H -i -u "${ROOT_USER}" DEBIAN_FRONTEND=noninteractive apt-get update -q echo "[*] Upgrading OS packages" sudo -H -i -u "${ROOT_USER}" DEBIAN_FRONTEND=noninteractive apt-get upgrade -qq -y echo "[*] Installing base packages" sudo -H -i -u "${ROOT_USER}" DEBIAN_FRONTEND=noninteractive apt-get install -qq -y ${ROOT_PKG} echo "[*] Cloning Haveno repo" sudo -H -i -u "${ROOT_USER}" git config --global advice.detachedHead false sudo -H -i -u "${ROOT_USER}" git clone --branch "${HAVENO_REPO_TAG}" "${HAVENO_REPO_URL}" "${ROOT_HOME}/${HAVENO_REPO_NAME}" echo "[*] Installing Tor" sudo -H -i -u "${ROOT_USER}" wget -qO- https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | sudo gpg --dearmor | sudo tee /usr/share/keyrings/tor-archive-keyring.gpg >/dev/null echo "deb [arch=amd64 signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org focal main" | sudo -H -i -u "${ROOT_USER}" tee /etc/apt/sources.list.d/tor.list sudo -H -i -u "${ROOT_USER}" DEBIAN_FRONTEND=noninteractive apt-get update -q sudo -H -i -u "${ROOT_USER}" DEBIAN_FRONTEND=noninteractive apt-get install -qq -y ${TOR_PKG} echo "[*] Installing Tor configuration" sudo -H -i -u "${ROOT_USER}" install -c -m 644 "${ROOT_HOME}/${HAVENO_REPO_NAME}/seednode/torrc" "${TOR_HOME}/torrc" if [ "${BITCOIN_INSTALL}" = true ];then echo "[*] Creating Bitcoin user with Tor access" sudo -H -i -u "${ROOT_USER}" useradd -d "${BITCOIN_HOME}" -G "${TOR_GROUP}" "${BITCOIN_USER}" echo "[*] Installing Bitcoin build dependencies" sudo -H -i -u "${ROOT_USER}" DEBIAN_FRONTEND=noninteractive apt-get install -qq -y ${BITCOIN_PKG} echo "[*] Creating Bitcoin homedir" sudo -H -i -u "${ROOT_USER}" mkdir -p "${BITCOIN_HOME}" sudo -H -i -u "${ROOT_USER}" chown "${BITCOIN_USER}":"${BITCOIN_GROUP}" ${BITCOIN_HOME} sudo -H -i -u "${BITCOIN_USER}" ln -s . .bitcoin echo "[*] Cloning Bitcoin repo" sudo -H -i -u "${BITCOIN_USER}" git config --global advice.detachedHead false sudo -H -i -u "${BITCOIN_USER}" git clone --branch "${BITCOIN_REPO_TAG}" "${BITCOIN_REPO_URL}" "${BITCOIN_HOME}/${BITCOIN_REPO_NAME}" echo "[*] Building Bitcoin from source" sudo -H -i -u "${BITCOIN_USER}" sh -c "cd ${BITCOIN_REPO_NAME} && ./autogen.sh --quiet && ./configure --quiet --disable-wallet --with-incompatible-bdb && make -j9" echo "[*] Installing Bitcoin into OS" sudo -H -i -u "${ROOT_USER}" sh -c "cd ${BITCOIN_HOME}/${BITCOIN_REPO_NAME} && make install >/dev/null" echo "[*] Installing Bitcoin configuration" sudo -H -i -u "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 644 "${ROOT_HOME}/${HAVENO_REPO_NAME}/seednode/bitcoin.conf" "${BITCOIN_HOME}/bitcoin.conf" sudo -H -i -u "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${ROOT_HOME}/${HAVENO_REPO_NAME}/seednode/blocknotify.sh" "${BITCOIN_HOME}/blocknotify.sh" echo "[*] Generating Bitcoin RPC credentials" BITCOIN_RPC_USER=$(head -150 /dev/urandom | md5sum | awk '{print $1}') sudo sed -i -e "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_HOME}/bitcoin.conf" BITCOIN_RPC_PASS=$(head -150 /dev/urandom | md5sum | awk '{print $1}') sudo sed -i -e "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_HOME}/bitcoin.conf" echo "[*] Installing Bitcoin init scripts" sudo -H -i -u "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${ROOT_HOME}/${HAVENO_REPO_NAME}/seednode/bitcoin.service" "${SYSTEMD_SERVICE_HOME}" fi echo "[*] Creating Haveno user with Tor access" sudo -H -i -u "${ROOT_USER}" useradd -d "${HAVENO_HOME}" -G "${TOR_GROUP}" "${HAVENO_USER}" echo "[*] Creating Haveno homedir" sudo -H -i -u "${ROOT_USER}" mkdir -p "${HAVENO_HOME}" sudo -H -i -u "${ROOT_USER}" chown "${HAVENO_USER}":"${HAVENO_GROUP}" ${HAVENO_HOME} echo "[*] Moving Haveno repo" sudo -H -i -u "${ROOT_USER}" mv "${ROOT_HOME}/${HAVENO_REPO_NAME}" "${HAVENO_HOME}/${HAVENO_REPO_NAME}" sudo -H -i -u "${ROOT_USER}" chown -R "${HAVENO_USER}:${HAVENO_GROUP}" "${HAVENO_HOME}/${HAVENO_REPO_NAME}" echo "[*] Installing Haveno init script" sudo -H -i -u "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${HAVENO_HOME}/${HAVENO_REPO_NAME}/seednode/haveno-seednode.service" "${SYSTEMD_SERVICE_HOME}/haveno-seednode.service" if [ "${BITCOIN_INSTALL}" = true ];then sudo sed -i -e "s/#Requires=bitcoin.service/Requires=bitcoin.service/" "${SYSTEMD_SERVICE_HOME}/haveno-seednode.service" sudo sed -i -e "s/#BindsTo=bitcoin.service/BindsTo=bitcoin.service/" "${SYSTEMD_SERVICE_HOME}/haveno-seednode.service" fi sudo sed -i -e "s/__HAVENO_REPO_NAME__/${HAVENO_REPO_NAME}/" "${SYSTEMD_SERVICE_HOME}/haveno-seednode.service" sudo sed -i -e "s!__HAVENO_HOME__!${HAVENO_HOME}!" "${SYSTEMD_SERVICE_HOME}/haveno-seednode.service" echo "[*] Installing Haveno environment file with Bitcoin RPC credentials" sudo -H -i -u "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${HAVENO_HOME}/${HAVENO_REPO_NAME}/seednode/haveno.env" "${SYSTEMD_ENV_HOME}/haveno.env" sudo sed -i -e "s/__BITCOIN_P2P_HOST__/${BITCOIN_P2P_HOST}/" "${SYSTEMD_ENV_HOME}/haveno.env" sudo sed -i -e "s/__BITCOIN_P2P_PORT__/${BITCOIN_P2P_PORT}/" "${SYSTEMD_ENV_HOME}/haveno.env" sudo sed -i -e "s/__BITCOIN_RPC_HOST__/${BITCOIN_RPC_HOST}/" "${SYSTEMD_ENV_HOME}/haveno.env" sudo sed -i -e "s/__BITCOIN_RPC_PORT__/${BITCOIN_RPC_PORT}/" "${SYSTEMD_ENV_HOME}/haveno.env" sudo sed -i -e "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${SYSTEMD_ENV_HOME}/haveno.env" sudo sed -i -e "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${SYSTEMD_ENV_HOME}/haveno.env" sudo sed -i -e "s!__HAVENO_APP_NAME__!${HAVENO_APP_NAME}!" "${SYSTEMD_ENV_HOME}/haveno.env" sudo sed -i -e "s!__HAVENO_HOME__!${HAVENO_HOME}!" "${SYSTEMD_ENV_HOME}/haveno.env" echo "[*] Checking out Haveno ${HAVENO_LATEST_RELEASE}" sudo -H -i -u "${HAVENO_USER}" sh -c "cd ${HAVENO_HOME}/${HAVENO_REPO_NAME} && git checkout ${HAVENO_LATEST_RELEASE}" echo "[*] Building Haveno from source" sudo -H -i -u "${HAVENO_USER}" sh -c "cd ${HAVENO_HOME}/${HAVENO_REPO_NAME} && ./gradlew build -x test < /dev/null" # redirect from /dev/null is necessary to workaround gradlew non-interactive shell hanging issue echo "[*] Updating systemd daemon configuration" sudo -H -i -u "${ROOT_USER}" systemctl daemon-reload sudo -H -i -u "${ROOT_USER}" systemctl enable tor.service sudo -H -i -u "${ROOT_USER}" systemctl enable haveno-seednode.service if [ "${BITCOIN_INSTALL}" = true ];then sudo -H -i -u "${ROOT_USER}" systemctl enable bitcoin.service fi echo "[*] Preparing firewall" sudo -H -i -u "${ROOT_USER}" ufw default deny incoming sudo -H -i -u "${ROOT_USER}" ufw default allow outgoing echo "[*] Starting Tor" sudo -H -i -u "${ROOT_USER}" systemctl start tor if [ "${BITCOIN_INSTALL}" = true ];then echo "[*] Starting Bitcoin" sudo -H -i -u "${ROOT_USER}" systemctl start bitcoin sudo -H -i -u "${ROOT_USER}" journalctl --no-pager --unit bitcoin sudo -H -i -u "${ROOT_USER}" tail "${BITCOIN_HOME}/debug.log" fi echo "[*] Adding notes to motd" sudo -H -i -u "${ROOT_USER}" sh -c 'echo " " >> /etc/motd' sudo -H -i -u "${ROOT_USER}" sh -c 'echo "Haveno Seednode instructions:" >> /etc/motd' sudo -H -i -u "${ROOT_USER}" sh -c 'echo "https://github.com/haveno-dex/haveno/tree/master/seednode" >> /etc/motd' sudo -H -i -u "${ROOT_USER}" sh -c 'echo " " >> /etc/motd' sudo -H -i -u "${ROOT_USER}" sh -c 'echo "How to check logs for Haveno-Seednode service:" >> /etc/motd' sudo -H -i -u "${ROOT_USER}" sh -c 'echo "sudo journalctl --no-pager --unit haveno-seednode" >> /etc/motd' sudo -H -i -u "${ROOT_USER}" sh -c 'echo " " >> /etc/motd' sudo -H -i -u "${ROOT_USER}" sh -c 'echo "How to restart Haveno-Seednode service:" >> /etc/motd' sudo -H -i -u "${ROOT_USER}" sh -c 'echo "sudo service haveno-seednode restart" >> /etc/motd' echo '[*] Done!' echo ' ' echo '[*] DONT FORGET TO ENABLE FIREWALL!!!11' echo '[*] Follow all the README instructions!' echo ' ' ================================================ FILE: seednode/src/main/java/haveno/seednode/SeedNode.java ================================================ /* * 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 . */ package haveno.seednode; import com.google.inject.Injector; import haveno.core.app.misc.AppSetup; import haveno.core.app.misc.AppSetupWithP2P; import haveno.core.network.p2p.inventory.GetInventoryRequestHandler; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @Slf4j public class SeedNode { @Setter private Injector injector; private AppSetup appSetup; private GetInventoryRequestHandler getInventoryRequestHandler; public SeedNode() { } public void startApplication() { appSetup = injector.getInstance(AppSetupWithP2P.class); appSetup.start(); getInventoryRequestHandler = injector.getInstance(GetInventoryRequestHandler.class); } public void shutDown() { getInventoryRequestHandler.shutDown(); } } ================================================ FILE: seednode/src/main/java/haveno/seednode/SeedNodeMain.java ================================================ /* * 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 . */ package haveno.seednode; import com.google.inject.Key; import com.google.inject.name.Names; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.app.AppModule; import haveno.common.app.Capabilities; import haveno.common.app.Capability; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; import haveno.common.handlers.ResultHandler; import haveno.core.app.TorSetup; import haveno.core.app.misc.ExecutableForAppWithP2p; import haveno.core.app.misc.ModuleForAppWithP2p; import haveno.core.user.Cookie; import haveno.core.user.CookieKey; import haveno.core.user.User; import haveno.network.p2p.P2PService; import haveno.network.p2p.P2PServiceListener; import haveno.network.p2p.peers.PeerManager; import lombok.extern.slf4j.Slf4j; @Slf4j public class SeedNodeMain extends ExecutableForAppWithP2p { private static final long CHECK_CONNECTION_LOSS_SEC = 30; private static final String VERSION = "1.2.3"; private SeedNode seedNode; private Timer checkConnectionLossTime; public SeedNodeMain() { super("Haveno Seednode", "haveno-seednode", "haveno_seednode", VERSION); } public static void main(String[] args) { System.out.println("SeedNode.VERSION: " + VERSION); new SeedNodeMain().execute(args); } @Override protected int doExecute() { super.doExecute(); checkMemory(config, this); return keepRunning(); } @Override protected void addCapabilities() { Capabilities.app.addAll(Capability.SEED_NODE); } @Override protected void launchApplication() { UserThread.execute(() -> { try { seedNode = new SeedNode(); UserThread.execute(this::onApplicationLaunched); } catch (Exception e) { log.error("Error launching seed node: {}\n", e.toString(), e); } }); } @Override protected void onApplicationLaunched() { super.onApplicationLaunched(); } /////////////////////////////////////////////////////////////////////////////////////////// // We continue with a series of synchronous execution tasks /////////////////////////////////////////////////////////////////////////////////////////// @Override protected AppModule getModule() { return new ModuleForAppWithP2p(config); } @Override protected void applyInjector() { super.applyInjector(); seedNode.setInjector(injector); } @Override protected void startApplication() { Cookie cookie = injector.getInstance(User.class).getCookie(); cookie.getAsOptionalBoolean(CookieKey.CLEAN_TOR_DIR_AT_RESTART).ifPresent(wasCleanTorDirSet -> { if (wasCleanTorDirSet) { injector.getInstance(TorSetup.class).cleanupTorFiles(() -> { log.info("Tor directory reset"); cookie.remove(CookieKey.CLEAN_TOR_DIR_AT_RESTART); }, log::error); } }); seedNode.startApplication(); injector.getInstance(P2PService.class).addP2PServiceListener(new P2PServiceListener() { @Override public void onDataReceived() { // Do nothing } @Override public void onNoSeedNodeAvailable() { // Do nothing } @Override public void onNoPeersAvailable() { // Do nothing } @Override public void onUpdatedDataReceived() { // Do nothing } @Override public void onTorNodeReady() { // Do nothing } @Override public void onHiddenServicePublished() { boolean preventPeriodicShutdownAtSeedNode = injector.getInstance(Key.get(boolean.class, Names.named(Config.PREVENT_PERIODIC_SHUTDOWN_AT_SEED_NODE))); if (!preventPeriodicShutdownAtSeedNode) { startShutDownInterval(SeedNodeMain.this); } UserThread.runAfter(() -> setupConnectionLossCheck(), 60); } @Override public void onSetupFailed(Throwable throwable) { // Do nothing } @Override public void onRequestCustomBridges() { // Do nothing } }); } private void setupConnectionLossCheck() { // For dev testing (usually on XMR_LOCAL) we don't want to get the seed shut // down as it is normal that the seed is the only actively running node. if (Config.baseCurrencyNetwork() != BaseCurrencyNetwork.XMR_MAINNET) { return; } if (checkConnectionLossTime != null) { return; } checkConnectionLossTime = UserThread.runPeriodically(() -> { if (injector.getInstance(PeerManager.class).getNumAllConnectionsLostEvents() > 1) { // We set a flag to clear tor cache files at re-start. We cannot clear it now as Tor is used and // that can cause problems. injector.getInstance(User.class).getCookie().putAsBoolean(CookieKey.CLEAN_TOR_DIR_AT_RESTART, true); shutDown(this); } }, CHECK_CONNECTION_LOSS_SEC); } @Override public void gracefulShutDown(ResultHandler resultHandler) { seedNode.shutDown(); super.gracefulShutDown(resultHandler); } } ================================================ FILE: seednode/src/main/resources/logback.xml ================================================ %hl2(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{40}: %msg %xEx%n) ================================================ FILE: seednode/src/test/java/haveno/seednode/GuiceSetupTest.java ================================================ package haveno.seednode; import com.google.inject.Guice; import haveno.common.config.Config; import haveno.core.app.misc.AppSetupWithP2P; import haveno.core.app.misc.ModuleForAppWithP2p; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import org.junit.jupiter.api.Test; public class GuiceSetupTest { @Test public void testGuiceSetup() { Res.setup(); CurrencyUtil.setup(); ModuleForAppWithP2p module = new ModuleForAppWithP2p(new Config()); Guice.createInjector(module).getInstance(AppSetupWithP2P.class); } } ================================================ FILE: seednode/torrc ================================================ ## Configuration file for Haveno Seednode ## ## To start/reload/etc this instance, run "systemctl start tor" (or reload, or..). ## This instance will run as user debian-tor; its data directory is /var/lib/tor. ## ## This file is configured via: ## /usr/share/tor/tor-service-defaults-torrc ## ## See 'man tor', for more options you can use in this file. ## Tor opens a socks proxy on port 9050 by default -- even if you don't ## configure one below. Set "SocksPort 0" if you plan to run Tor only ## as a relay, and not make any local application connections yourself. #SocksPort 9050 # Default: Bind to localhost:9050 for local connections. # ### SocksPort flag: OnionTrafficOnly ### ## Tell the tor client to only connect to .onion addresses in response to SOCKS5 requests on this connection. ## This is equivalent to NoDNSRequest, NoIPv4Traffic, NoIPv6Traffic. # ### SocksPort flag: ExtendedErrors ### ## Return extended error code in the SOCKS reply. So far, the possible errors are: # X'F0' Onion Service Descriptor Can Not be Found # X'F1' Onion Service Descriptor Is Invalid # X'F2' Onion Service Introduction Failed # X'F3' Onion Service Rendezvous Failed # X'F4' Onion Service Missing Client Authorization # X'F5' Onion Service Wrong Client Authorization # X'F6' Onion Service Invalid Address # X'F7' Onion Service Introduction Timed Out SocksPort 9050 OnionTrafficOnly ExtendedErrors ## Entry policies to allow/deny SOCKS requests based on IP address. ## First entry that matches wins. If no SocksPolicy is set, we accept ## all (and only) requests that reach a SocksPort. Untrusted users who ## can access your SocksPort may be able to learn about the connections ## you make. SocksPolicy accept 127.0.0.1 SocksPolicy accept6 [::1] SocksPolicy reject * ## Tor will reject application connections that use unsafe variants of the socks protocol ## — ones that only provide an IP address, meaning the application is doing a DNS resolve first. ## Specifically, these are socks4 and socks5 when not doing remote DNS. (Default: 0) #SafeSocks 1 ## Tor will make a notice-level log entry for each connection to the Socks port indicating ## whether the request used a safe socks protocol or an unsafe one (see above entry on SafeSocks). ## This helps to determine whether an application using Tor is possibly leaking DNS requests. (Default: 0) TestSocks 1 ## Logs go to stdout at level "notice" unless redirected by something ## else, like one of the below lines. You can have as many Log lines as ## you want. ## ## We advise using "notice" in most cases, since anything more verbose ## may provide sensitive information to an attacker who obtains the logs. ## ## Send all messages of level 'notice' or higher to /var/log/tor/notices.log #Log notice file /var/log/tor/notices.log ## Send every possible message to /var/log/tor/debug.log #Log debug file /var/log/tor/debug.log ## Use the system log instead of Tor's logfiles (This is default) #Log notice syslog ## To send all messages to stderr: #Log debug stderr # Try to write to disk less frequently than we would otherwise. This is useful when running on flash memory. AvoidDiskWrites 1 ## TODO: This option has no effect. Bisq/Haveno is tor client &/or hidden service. 'man torrc': ## Relays and bridges only. When this option is enabled, a Tor relay writes obfuscated statistics on its ## role as hidden-service directory, introduction point, or rendezvous point to disk every 24 hours. ## If ExtraInfoStatistics is enabled, it will be published as part of the extra-info document. (Default: 1) HiddenServiceStatistics 0 ## NOTE: In order to use the ControlPort, the must belong to the tor group. ## sudo usermod -aG debian-tor ## ## The port on which Tor will listen for local connections from Tor ## controller applications, as documented in control-spec.txt. #ControlPort 9051 ## If you enable the controlport, be sure to enable one of these ## authentication methods, to prevent attackers from accessing it. ## ## Compute the hash of a password with "tor --hash-password password". #HashedControlPassword 16:872860B76453A77D60CA2BB8C1A7042072093276A3D701AD684053EC4C CookieAuthentication 0 # (Default: 1) ## MetricsPort provides an interface to the underlying Tor relay metrics. ## Exposing publicly is dangerous, set a very strict access policy. ## Retrieve the metrics with: curl http://127.0.0.1:9035/metrics MetricsPort 127.0.0.1:9035 MetricsPortPolicy accept 127.0.0.1 MetricsPortPolicy accept [::1] ############### This section is just for location-hidden services ### ## Once you have configured a hidden service, you can look at the ## contents of the file ".../hidden_service/hostname" for the address ## to tell people. e.g.: 'sudo cat /var/lib/tor/haveno_seednode/hostname' ## ## HiddenServicePort x y:z says to redirect requests on port x to the ## address y:z. ## ## If you plan to keep your service available for a long time, you might want to make a backup copy ## of the private_key file or complete folder /var/lib/tor/hidden_service somewhere. #### Haveno seednode incoming anonymity connections ### HiddenServiceDir /var/lib/tor/haveno_seednode HiddenServicePort 2002 127.0.0.1:2002 HiddenServicePort 2002 [::1]:2002 ## NOTE: HiddenService options are per onion service ## https://community.torproject.org/onion-services/advanced/dos/ ## ## Rate limiting at the Introduction Points ## Intropoint protections prevents onion service DoS from becoming a DoS for the entire machine and its guard. HiddenServiceEnableIntroDoSDefense 1 #HiddenServiceEnableIntroDoSRatePerSec 25 # (Default: 25) #HiddenServiceEnableIntroDoSBurstPerSec 200 # (Default: 200) # Number of introduction points the hidden service will have. You can’t have more than 20. #HiddenServiceNumIntroductionPoints 3 # (Default: 3) ## https://tpo.pages.torproject.net/onion-services/ecosystem/technology/pow/#configuring-an-onion-service-with-the-pow-protection ## Proof of Work (PoW) before establishing Rendezvous Circuits ## The lower the queue and burst rates, the higher the puzzle effort tends to be for users. HiddenServicePoWDefensesEnabled 1 HiddenServicePoWQueueRate 50 # (Default: 250) HiddenServicePoWQueueBurst 250 # (Default: 2500) ## Stream limits in the established Rendezvous Circuits ## The maximum number of simultaneous streams (connections) per rendezvous circuit. The max value allowed is 65535. (0 = unlimited) HiddenServiceMaxStreams 25 #HiddenServiceMaxStreamsCloseCircuit 1 #### Haveno seednode2 incoming anonymity connections ### HiddenServiceDir /var/lib/tor/haveno_seednode2 HiddenServicePort 2003 127.0.0.1:2003 HiddenServicePort 2003 [::1]:2003 HiddenServiceEnableIntroDoSDefense 1 #HiddenServiceEnableIntroDoSRatePerSec 25 # (Default: 25) #HiddenServiceEnableIntroDoSBurstPerSec 200 # (Default: 200) #HiddenServiceNumIntroductionPoints 3 # (Default: 3) HiddenServicePoWDefensesEnabled 1 HiddenServicePoWQueueRate 50 # (Default: 250) HiddenServicePoWQueueBurst 250 # (Default: 2500) HiddenServiceMaxStreams 25 #HiddenServiceMaxStreamsCloseCircuit 1 ##################################################################### LongLivedPorts 2002,2003 ## Default: 21, 22, 706, 1863, 5050, 5190, 5222, 5223, 6523, 6667, 6697, 8300 ================================================ FILE: seednode/uninstall_seednode_debian.sh ================================================ #!/bin/sh echo "[*] Uninstalling Bitcoin and Haveno, will delete all data!!" sleep 10 sudo rm -rf /root/haveno sudo systemctl stop bitcoin sudo systemctl stop haveno-seednode sudo systemctl disable bitcoin sudo systemctl disable haveno-seednode sudo userdel -f -r haveno sudo userdel -f -r bitcoin echo "[*] Done!" ================================================ FILE: settings.gradle ================================================ include 'proto' include 'assets' include 'common' include 'p2p' include 'core' include 'cli' include 'daemon' include 'desktop' include 'monitor' include 'relay' include 'seednode' include 'statsnode' include 'inventory' include 'apitest' rootProject.name = 'haveno' ================================================ FILE: shell.nix ================================================ {pkgs ? import {}}: pkgs.mkShell { buildInputs = with pkgs; [ gnumake wget git javaPackages.openjfx21 (pkgs.jdk21.override {enableJavaFX = true;}) ]; } ================================================ FILE: statsnode/src/main/java/haveno/statistics/Statistics.java ================================================ /* * 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 . */ package haveno.statistics; import com.google.inject.Injector; import haveno.core.app.misc.AppSetup; import haveno.core.app.misc.AppSetupWithP2P; import haveno.core.offer.OfferBookService; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.network.p2p.BootstrapListener; import haveno.network.p2p.P2PService; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @Slf4j public class Statistics { @Setter private Injector injector; private OfferBookService offerBookService; // pin to not get GC'ed private PriceFeedService priceFeedService; private TradeStatisticsManager tradeStatisticsManager; private P2PService p2pService; private AppSetup appSetup; public Statistics() { } public void startApplication() { p2pService = injector.getInstance(P2PService.class); offerBookService = injector.getInstance(OfferBookService.class); priceFeedService = injector.getInstance(PriceFeedService.class); tradeStatisticsManager = injector.getInstance(TradeStatisticsManager.class); // We need the price feed for market based offers priceFeedService.setCurrencyCode("USD"); p2pService.addP2PServiceListener(new BootstrapListener() { @Override public void onDataReceived() { // we need to have tor ready log.info("onBootstrapComplete: we start requestPriceFeed"); priceFeedService.startRequestingPrices(price -> log.info("requestPriceFeed. price=" + price), (errorMessage, throwable) -> log.warn("Exception at requestPriceFeed: " + throwable.getMessage())); tradeStatisticsManager.onAllServicesInitialized(); } }); appSetup = injector.getInstance(AppSetupWithP2P.class); appSetup.start(); } } ================================================ FILE: statsnode/src/main/java/haveno/statistics/StatisticsMain.java ================================================ /* * 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 . */ package haveno.statistics; import haveno.common.UserThread; import haveno.common.app.AppModule; import haveno.core.app.misc.ExecutableForAppWithP2p; import haveno.core.app.misc.ModuleForAppWithP2p; import lombok.extern.slf4j.Slf4j; @Slf4j public class StatisticsMain extends ExecutableForAppWithP2p { private static final String VERSION = "1.0.1"; private Statistics statistics; public StatisticsMain() { super("Haveno Statsnode", "haveno-statistics", "haveno_statistics", VERSION); } public static void main(String[] args) { log.info("Statistics.VERSION: " + VERSION); new StatisticsMain().execute(args); } @Override protected int doExecute() { super.doExecute(); checkMemory(config, this); return keepRunning(); } @Override protected void addCapabilities() { } @Override protected void launchApplication() { UserThread.execute(() -> { try { statistics = new Statistics(); UserThread.execute(this::onApplicationLaunched); } catch (Exception e) { e.printStackTrace(); } }); } @Override protected void onApplicationLaunched() { super.onApplicationLaunched(); } /////////////////////////////////////////////////////////////////////////////////////////// // We continue with a series of synchronous execution tasks /////////////////////////////////////////////////////////////////////////////////////////// @Override protected AppModule getModule() { return new ModuleForAppWithP2p(config); } @Override protected void applyInjector() { super.applyInjector(); statistics.setInjector(injector); } @Override protected void startApplication() { statistics.startApplication(); } } ================================================ FILE: statsnode/src/main/resources/logback.xml ================================================ %highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{15}: %msg %xEx%n)